Files

1146 lines
64 KiB
Rust
Raw Permalink Normal View History

2026-07-09 02:59:53 +03:00
#![allow(deprecated)]
use eframe::{egui, App, Frame, NativeOptions};
use egui::{ColorImage, TextureOptions, TextureHandle, TextureFilter, Color32, Stroke, Rect, Vec2, Pos2, StrokeKind};
use hcie_blend::BlendMode;
const THUMB_W: usize = 40;
const THUMB_H: usize = 30;
const CHECKER_SIZE: f32 = 12.0;
const ZOOM_MIN: f32 = 0.01;
const ZOOM_MAX: f32 = 64.0;
struct LayerEntry {
layer_id: u64,
parent_id: Option<u64>,
clipping_mask: bool,
name: String,
visible: bool,
opacity: f32,
blend_mode: BlendMode,
layer_type: String,
width: u32,
height: u32,
pixels: Vec<u8>,
dirty: bool,
thumbnail: Option<TextureHandle>,
adjustment: Option<hcie_blend::Adjustment>,
mask_pixels: Option<Vec<u8>>,
mask_bounds: Option<[i32; 4]>,
mask_default_color: u8,
curve_knots: [Vec<(f32, f32)>; 3],
master_knots: Vec<(f32, f32)>,
effects: Vec<hcie_fx::LayerEffect>,
fill_opacity: f32,
}
struct HcieIoApp {
layers: Vec<LayerEntry>,
canvas_w: u32,
canvas_h: u32,
status: String,
temp_path: String,
temp_format: String,
zoom: f32,
pan_offset: Vec2,
composite_texture: Option<TextureHandle>,
composite_dirty: bool,
checked_path: String,
layer_id_counter: usize,
selected_layer: Option<usize>,
load_on_start: Option<std::path::PathBuf>,
curve_channel: usize,
curve_drag_knot: Option<usize>,
ref_texture: Option<TextureHandle>,
ref_path: Option<String>,
ref_pixels: Option<(Vec<u8>, u32, u32)>,
diff_texture: Option<TextureHandle>,
view_mode: usize,
compare_stats: String,
}
impl Default for HcieIoApp {
fn default() -> Self {
Self {
layers: vec![],
canvas_w: 800,
canvas_h: 600,
status: String::new(),
temp_path: "output.png".to_string(),
temp_format: "png".to_string(),
zoom: 1.0,
pan_offset: Vec2::ZERO,
composite_texture: None,
composite_dirty: false,
checked_path: String::new(),
layer_id_counter: 0,
selected_layer: None,
load_on_start: Some(std::path::PathBuf::from("_images/_psd_stil_test/sultan_test/sultan.psd")),
2026-07-09 02:59:53 +03:00
curve_channel: 0,
curve_drag_knot: None,
ref_texture: None,
ref_path: None,
ref_pixels: None,
diff_texture: None,
view_mode: 0,
compare_stats: String::new(),
}
}
}
impl HcieIoApp {
fn load_file(&mut self, path: &std::path::Path) {
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("").to_lowercase();
let result = match ext.as_str() {
"psd" => {
// Get canvas size from PSD header, not from first layer
if let Ok(bytes) = std::fs::read(path) {
if let Ok(psd) = hcie_psd::Psd::from_bytes(&bytes) {
self.canvas_w = psd.width();
self.canvas_h = psd.height();
}
}
hcie_psd::psd_import::import_psd(path)
},
"kra" => hcie_kra::import_kra(path),
_ => hcie_io::load_image(path).map(|l| vec![hcie_protocol::Layer::from_rgba(l.name, l.width, l.height, l.pixels)]),
};
match result {
Ok(layers) => {
if layers.is_empty() { self.status = "No layers found".to_string(); return; }
// canvas_w/h already set from PSD header above (for PSD files)
if ext != "psd" {
self.canvas_w = layers[0].width;
self.canvas_h = layers[0].height;
}
self.layers.clear();
for l in layers {
self.layer_id_counter += 1;
let curve_knots = if let Some(hcie_blend::Adjustment::Curves { lut_r, lut_g, lut_b }) = &l.adjustment {
if !l.curve_points.is_empty() {
let k = l.curve_points.iter().map(|&(o, i)| (i as f32, o as f32)).collect::<Vec<_>>();
[k.clone(), k.clone(), k]
} else {
[extract_knots(&lut_r[..]), extract_knots(&lut_g[..]), extract_knots(&lut_b[..])]
}
} else {
[vec![], vec![], vec![]]
};
let master_knots = curve_knots[0].clone();
self.layers.push(LayerEntry {
layer_id: l.id,
parent_id: l.parent_id,
name: l.name.clone(),
visible: l.visible,
opacity: l.opacity,
blend_mode: to_blend_mode(l.blend_mode),
clipping_mask: l.clipping_mask,
layer_type: format!("{:?}", l.layer_type),
width: l.width,
height: l.height,
pixels: l.pixels.clone(),
dirty: true,
thumbnail: None,
adjustment: l.adjustment.clone(),
mask_pixels: l.mask_pixels.clone(),
mask_bounds: l.mask_bounds,
mask_default_color: l.mask_default_color,
curve_knots,
master_knots,
effects: l.effects.iter().map(|e| hcie_fx::protocol_to_hcie_fx_effect(e)).collect(),
fill_opacity: l.fill_opacity,
});
}
self.selected_layer = if self.layers.is_empty() { None } else { Some(0) };
self.composite_dirty = true;
self.status = format!("Loaded {} layers: {}x{}", self.layers.len(), self.canvas_w, self.canvas_h);
// Auto-load reference PNG if exists next to PSD
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
let parent = path.parent().unwrap_or(std::path::Path::new("."));
let ref_png = parent.join(format!("{}.png", stem));
if ref_png.exists() {
self.load_reference(&ref_png);
}
}
Err(e) => self.status = format!("Error: {}", e),
}
}
fn load_reference(&mut self, path: &std::path::Path) {
match image::open(path) {
Ok(img) => {
let rgba = img.to_rgba8();
let w = rgba.width();
let h = rgba.height();
self.ref_pixels = Some((rgba.into_raw(), w, h));
self.ref_path = Some(path.display().to_string());
self.ref_texture = None;
self.diff_texture = None;
self.compare_stats = String::new();
self.status = format!("{} + Ref: {}x{}", self.status, w, h);
}
Err(e) => {
self.status = format!("{} (ref load error: {})", self.status, e);
}
}
}
fn save_image(&mut self) -> bool {
let layer = hcie_io::Layer::from_rgba("Export", self.canvas_w, self.canvas_h, self.composite());
match hcie_io::save_image(&layer, std::path::Path::new(&self.temp_path), &self.temp_format) {
Ok(()) => { self.status = "Saved successfully!".to_string(); true }
Err(e) => { self.status = format!("Save error: {}", e); false }
}
}
fn composite(&self) -> Vec<u8> {
let comp_layers: Vec<hcie_composite::Layer> = self.layers.iter().map(|l| hcie_composite::Layer {
name: l.name.clone(),
layer_type: hcie_protocol::LayerType::default(),
data: hcie_protocol::LayerData::default(),
pixels: l.pixels.clone(),
width: l.width,
height: l.height,
visible: l.visible,
opacity: l.opacity,
blend_mode: from_blend_mode(l.blend_mode),
locked: false,
dirty: false,
id: l.layer_id,
parent_id: l.parent_id,
styles: vec![],
clipping_mask: l.clipping_mask,
collapsed: false,
adjustment: l.adjustment.clone(),
mask_pixels: l.mask_pixels.clone(),
mask_bounds: l.mask_bounds,
mask_default_color: l.mask_default_color,
curve_points: vec![],
fill_opacity: l.fill_opacity,
effects: l.effects.iter().map(|e| hcie_fx::hcie_fx_effect_to_protocol(e)).collect(),
effects_dirty: std::sync::atomic::AtomicBool::new(false),
effects_cache: std::sync::Mutex::new(None),
adjustment_raw: None,
is_section_divider: false,
image_resources_raw: None,
}).collect();
hcie_composite::composite_layers(&comp_layers, self.canvas_w, self.canvas_h)
}
fn render_composition(&mut self, ctx: &egui::Context) {
let needs_render = self.composite_dirty
|| self.composite_texture.is_none()
|| self.composite_texture.as_ref().map_or(true, |t| t.size() != [self.canvas_w as usize, self.canvas_h as usize]);
if !needs_render && self.ref_texture.is_some() { return; }
let buf = self.composite();
self.composite_dirty = false;
let options = TextureOptions {
magnification: TextureFilter::Nearest,
minification: TextureFilter::Linear,
..Default::default()
};
let image = ColorImage::from_rgba_unmultiplied(
[self.canvas_w as usize, self.canvas_h as usize], &buf,
);
if let Some(tex) = &mut self.composite_texture {
if tex.size() == [self.canvas_w as usize, self.canvas_h as usize] {
tex.set(image, options);
} else {
self.composite_texture = Some(ctx.load_texture("composite-view", image, options));
}
} else {
self.composite_texture = Some(ctx.load_texture("composite-view", image, options));
}
// Create reference texture from stored pixels
if let Some((ref px, rw, rh)) = &self.ref_pixels {
if self.ref_texture.is_none() {
let ref_img = ColorImage::from_rgba_unmultiplied([*rw as usize, *rh as usize], px);
self.ref_texture = Some(ctx.load_texture("ref-view", ref_img, options));
}
// Generate diff image and stats
if self.diff_texture.is_none() {
let comp = &buf;
let comp_w = self.canvas_w;
let comp_h = self.canvas_h;
let total = (*rw * *rh) as usize;
let mut diff_pixels = Vec::with_capacity(total * 4);
let mut diff_sum = 0.0f64;
let mut max_diff = 0u32;
let mut match_count = 0usize;
let mut diff_counts = [0u32; 256];
for i in 0..total {
let cx = (i as u32 % rw).min(comp_w.saturating_sub(1)) as usize;
let cy = (i as u32 / rw).min(comp_h.saturating_sub(1)) as usize;
let ci = (cy * comp_w as usize + cx) * 4;
let ri = i * 4;
let dr = if ci + 3 < comp.len() { (comp[ci] as i32 - px[ri] as i32).unsigned_abs() } else { 255 };
let dg = if ci + 3 < comp.len() { (comp[ci+1] as i32 - px[ri+1] as i32).unsigned_abs() } else { 255 };
let db = if ci + 3 < comp.len() { (comp[ci+2] as i32 - px[ri+2] as i32).unsigned_abs() } else { 255 };
let da = if ci + 3 < comp.len() { (comp[ci+3] as i32 - px[ri+3] as i32).unsigned_abs() } else { 255 };
let pix_diff = dr.max(dg).max(db).max(da);
if pix_diff > max_diff { max_diff = pix_diff; }
diff_sum += (dr + dg + db + da) as f64 / 4.0;
if pix_diff <= 5 { match_count += 1; }
diff_counts[pix_diff.min(255) as usize] += 1;
let d = (pix_diff.min(255) * 4).min(255) as u8;
diff_pixels.extend_from_slice(&[d, d, d, 255]);
}
let mean_diff = diff_sum / total as f64;
let match_pct = match_count as f64 / total as f64 * 100.0;
let diff_img = ColorImage::from_rgba_unmultiplied([*rw as usize, *rh as usize], &diff_pixels);
self.diff_texture = Some(ctx.load_texture("diff-view", diff_img, options));
let mut stats = format!("MAE: {:.2} MaxDiff: {} Match(<=5): {:.1}% ({}/{})",
mean_diff, max_diff, match_pct, match_count, total);
stats.push_str("\nDiff histogram (0-20):");
for d in 0..=20 {
if diff_counts[d] > 0 {
stats.push_str(&format!("\n diff={}: {} px", d, diff_counts[d]));
}
}
self.compare_stats = stats;
}
}
}
fn update_layer_thumb(&mut self, idx: usize, ctx: &egui::Context) {
let layer = &self.layers[idx];
if !layer.dirty && self.layers[idx].thumbnail.is_some() { return; }
let thumb = thumbnail_nearest(&layer.pixels, layer.width, layer.height, THUMB_W as u32, THUMB_H as u32);
let pixels: Vec<Color32> = thumb.chunks_exact(4)
.map(|c| Color32::from_rgba_unmultiplied(c[0], c[1], c[2], c[3])).collect();
let img = ColorImage::new([THUMB_W, THUMB_H], pixels);
let opts = TextureOptions { magnification: TextureFilter::Nearest, minification: TextureFilter::Nearest, ..Default::default() };
let name = format!("thumb-{}", idx);
if let Some(tex) = &mut self.layers[idx].thumbnail {
tex.set(img, opts);
} else {
self.layers[idx].thumbnail = Some(ctx.load_texture(name, img, opts));
}
self.layers[idx].dirty = false;
}
}
impl App for HcieIoApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut Frame) {
let ctx = ui.ctx();
// Load startup file once
if let Some(path) = self.load_on_start.take() {
self.checked_path = path.display().to_string();
self.load_file(&path);
}
egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
ui.heading("hcie-io Test GUI");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("📁 Load").clicked() {
if let Some(path) = rfd::FileDialog::new()
.add_filter("PSD", &["psd"])
.add_filter("KRA", &["kra"])
.add_filter("Images", &["png", "jpg", "jpeg", "webp", "bmp", "gif"]).pick_file()
{
self.checked_path = path.display().to_string();
self.load_file(&path);
}
}
});
});
ui.horizontal(|ui| {
ui.label("Path:");
ui.add(egui::TextEdit::singleline(&mut self.checked_path).desired_width(200.0));
ui.label("Fmt:");
ui.add(egui::TextEdit::singleline(&mut self.temp_format).desired_width(60.0));
if ui.button("💾 Save").clicked() { self.save_image(); }
});
ui.horizontal(|ui| {
if ui.button("").clicked() { self.zoom = (self.zoom / 1.2).clamp(ZOOM_MIN, ZOOM_MAX); }
ui.label(format!("{:.0}%", self.zoom * 100.0));
if ui.button("").clicked() { self.zoom = (self.zoom * 1.2).clamp(ZOOM_MIN, ZOOM_MAX); }
if ui.button("1:1").clicked() { self.zoom = 1.0; }
if ui.button("⊙ Reset").clicked() { self.pan_offset = Vec2::ZERO; self.zoom = 1.0; }
if self.ref_pixels.is_some() {
ui.separator();
ui.label("View:");
ui.selectable_value(&mut self.view_mode, 0, "Composite");
ui.selectable_value(&mut self.view_mode, 1, "Reference");
ui.selectable_value(&mut self.view_mode, 2, "Diff");
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(format!("{}", self.status));
});
});
let available = ui.available_size();
let panel_w = 200.0_f32;
let canvas_w = available.x - panel_w * 2.0 - 12.0;
// ── Render composition pipeline (before drawing) ──
self.render_composition(ui.ctx());
ui.horizontal(|ui| {
// Layer panel
ui.allocate_ui(Vec2::new(panel_w, available.y), |ui| {
ui.vertical(|ui| {
ui.strong("Layers");
ui.separator();
let mut toggled: Vec<(usize, bool)> = Vec::new();
let num = self.layers.len();
egui::ScrollArea::vertical().show(ui, |ui| {
for i in (0..num).rev() {
self.update_layer_thumb(i, ui.ctx());
let is_sel = self.selected_layer == Some(i);
let row_h = 36.0;
let (row_id, row_rect) = ui.allocate_space(Vec2::new(ui.available_width(), row_h));
let row_resp = ui.interact(row_rect, row_id, egui::Sense::click());
let layer = &self.layers[i];
let indent = if layer.parent_id.is_some() || layer.layer_type.contains("Group") { 16.0 } else { 0.0 };
let inner = Rect::from_min_max(
Pos2::new(row_rect.min.x + 2.0 + indent, row_rect.min.y + 2.0),
Pos2::new(row_rect.max.x - 2.0, row_rect.max.y - 2.0),
);
let eye_rect = Rect::from_min_size(inner.left_top(), Vec2::new(20.0, row_h));
if row_resp.clicked() {
if let Some(press_pos) = ui.input(|i| i.pointer.press_origin()) {
if eye_rect.contains(press_pos) {
toggled.push((i, !layer.visible));
} else {
self.selected_layer = Some(i);
}
} else {
self.selected_layer = Some(i);
}
}
if is_sel {
ui.painter().rect_filled(row_rect, 2.0, ui.visuals().selection.bg_fill);
} else if row_resp.hovered() {
ui.painter().rect_filled(row_rect, 2.0, ui.visuals().extreme_bg_color.gamma_multiply(0.5));
}
let (icon, color) = if layer.visible {
("👁", ui.visuals().strong_text_color())
} else {
("👁", ui.visuals().weak_text_color().gamma_multiply(0.25))
};
ui.painter().text(eye_rect.center(), egui::Align2::CENTER_CENTER, icon,
egui::FontId::proportional(16.0), color);
let thumb_rect = Rect::from_min_size(Pos2::new(inner.left() + 22.0, inner.top() + 3.0),
Vec2::new(THUMB_W as f32, THUMB_H as f32));
checkerboard_painter(ui.painter(), thumb_rect, 4.0, Color32::from_gray(160), Color32::from_gray(200));
if let Some(tex) = &layer.thumbnail {
ui.painter().image(tex.id(), thumb_rect,
Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)), Color32::WHITE);
}
ui.painter().text(Pos2::new(thumb_rect.right() + 6.0, inner.center().y),
egui::Align2::LEFT_CENTER, &layer.name, egui::FontId::proportional(12.0),
ui.visuals().text_color());
}
});
for (i, vis) in toggled { self.layers[i].visible = vis; self.composite_dirty = true; }
});
});
// Properties panel
ui.allocate_ui(Vec2::new(panel_w, available.y), |ui| {
ui.vertical(|ui| {
ui.strong("Properties");
ui.separator();
if let Some(i) = self.selected_layer.filter(|&i| i < self.layers.len()) {
let layer = &mut self.layers[i];
ui.label(format!("Name: {}", layer.name));
ui.label(format!("Type: {}", layer.layer_type));
ui.label(format!("Size: {}x{} px", layer.width, layer.height));
ui.label(format!("Blend: {:?}", layer.blend_mode));
egui::ComboBox::from_id_source("blend_mode")
.selected_text(format!("{:?}", layer.blend_mode))
.show_ui(ui, |ui| {
let modes = [
BlendMode::Normal, BlendMode::Dissolve, BlendMode::Darken, BlendMode::Multiply,
BlendMode::ColorBurn, BlendMode::LinearBurn, BlendMode::DarkerColor,
BlendMode::Lighten, BlendMode::Screen, BlendMode::ColorDodge,
BlendMode::LinearDodge, BlendMode::LighterColor, BlendMode::Overlay,
BlendMode::SoftLight, BlendMode::HardLight, BlendMode::VividLight,
BlendMode::LinearLight, BlendMode::PinLight, BlendMode::HardMix,
BlendMode::Difference, BlendMode::Exclusion, BlendMode::Subtract,
BlendMode::Divide, BlendMode::Hue, BlendMode::Saturation,
BlendMode::Color, BlendMode::Luminosity,
];
for m in modes {
if ui.selectable_value(&mut layer.blend_mode, m, format!("{:?}", m)).changed() {
self.composite_dirty = true;
}
}
});
if ui.add(egui::Slider::new(&mut layer.opacity, 0.0..=1.0).text("Opacity")).changed() {
self.composite_dirty = true;
}
if ui.checkbox(&mut layer.visible, "Visible").changed() { self.composite_dirty = true; }
ui.label(format!("Pixels: {} bytes", layer.pixels.len()));
if let Some(hcie_blend::Adjustment::Curves { lut_r, lut_g, lut_b }) = &mut layer.adjustment {
ui.separator();
ui.strong("Curves Adjustment");
let mut curve_ch = self.curve_channel;
let mut ch_changed = None;
ui.horizontal(|ui| {
let btn = egui::SelectableLabel::new(curve_ch == 3, egui::RichText::new("Master").strong().color(Color32::from_gray(200)));
if ui.add(btn).clicked() { ch_changed = Some(3); }
ui.separator();
for (idx, name, col) in [
(0usize, "R", Color32::from_rgb(255, 80, 80)),
(1, "G", Color32::from_rgb(80, 220, 80)),
(2, "B", Color32::from_rgb(80, 120, 255)),
] {
let btn = egui::SelectableLabel::new(curve_ch == idx, egui::RichText::new(name).color(col));
if ui.add(btn).clicked() { ch_changed = Some(idx); }
}
});
if let Some(ch) = ch_changed {
self.curve_channel = ch;
curve_ch = ch;
self.curve_drag_knot = None;
}
let sz = ui.available_width().min(220.0).max(120.0);
let (resp, painter) = ui.allocate_painter(Vec2::new(sz, sz), egui::Sense::click_and_drag());
let rect = resp.rect;
let to_screen = |ix: f32, iy: f32| -> Pos2 {
Pos2::new(rect.min.x + (ix / 255.0) * sz, rect.max.y - (iy / 255.0) * sz)
};
let from_screen = |p: Pos2| -> (f32, f32) {
(((p.x - rect.min.x) / sz * 255.0).clamp(0.0, 255.0),
((rect.max.y - p.y) / sz * 255.0).clamp(0.0, 255.0))
};
painter.rect_filled(rect, 0.0, Color32::from_gray(25));
for i in 0..=4 {
let t = i as f32 / 4.0;
let x = rect.min.x + t * sz;
let y = rect.min.y + (1.0 - t) * sz;
painter.line_segment([Pos2::new(x, rect.min.y), Pos2::new(x, rect.max.y)], Stroke::new(0.5, Color32::from_gray(50)));
painter.line_segment([Pos2::new(rect.min.x, y), Pos2::new(rect.max.x, y)], Stroke::new(0.5, Color32::from_gray(50)));
}
painter.line_segment([
Pos2::new(rect.min.x, rect.max.y),
Pos2::new(rect.max.x, rect.min.y),
], Stroke::new(0.5, Color32::from_gray(70)));
let knot_radius = 5.0f32;
let colors = [
Color32::from_rgb(255, 80, 80),
Color32::from_rgb(80, 220, 80),
Color32::from_rgb(80, 120, 255),
];
// Draw master curve (gray, always visible)
if !layer.master_knots.is_empty() {
let ms = spline_lut(&layer.master_knots);
let mut mpts = Vec::with_capacity(256);
for i in 0..256u16 { mpts.push(to_screen(i as f32, ms[i as usize] as f32)); }
for w in mpts.windows(2) { painter.line_segment([w[0], w[1]], Stroke::new(1.5, Color32::from_gray(120))); }
}
// Draw per-channel curves when a specific channel is selected
if curve_ch < 3 && !layer.curve_knots[curve_ch].is_empty() {
let cs = spline_lut(&layer.curve_knots[curve_ch]);
let mut cpts = Vec::with_capacity(256);
for i in 0..256u16 { cpts.push(to_screen(i as f32, cs[i as usize] as f32)); }
for w in cpts.windows(2) { painter.line_segment([w[0], w[1]], Stroke::new(2.0, colors[curve_ch])); }
}
// Hover info
let hover_pos = ui.input(|i| i.pointer.hover_pos());
if resp.hovered() {
if let Some(pos) = hover_pos {
if rect.contains(pos) {
let (hx, _) = from_screen(pos);
let hi = hx.round().clamp(0.0, 255.0) as usize;
painter.line_segment([Pos2::new(pos.x, rect.min.y), Pos2::new(pos.x, rect.max.y)], Stroke::new(0.3, Color32::from_gray(100)));
painter.line_segment([Pos2::new(rect.min.x, pos.y), Pos2::new(rect.max.x, pos.y)], Stroke::new(0.3, Color32::from_gray(100)));
// Show master value
if !layer.master_knots.is_empty() {
let ms = spline_lut(&layer.master_knots);
let mv = ms[hi];
let dp = to_screen(hi as f32, mv as f32);
painter.circle_filled(dp, 3.0, Color32::from_gray(160));
}
// Show channel value
if curve_ch < 3 && !layer.curve_knots[curve_ch].is_empty() {
let cs = spline_lut(&layer.curve_knots[curve_ch]);
let cv = cs[hi];
let dp = to_screen(hi as f32, cv as f32);
painter.circle_filled(dp, 3.0, colors[curve_ch]);
ui.label(format!("Ch {} | {}{}", ["R","G","B"][curve_ch], hi, cv));
} else if curve_ch == 3 {
let ms = spline_lut(&layer.master_knots);
ui.label(format!("Master | {}{}", hi, ms[hi]));
}
}
}
}
// Find nearest knot on the ACTIVE curve
// Copy active knots for immutable reference during interaction
let active_knots = if curve_ch == 3 { layer.master_knots.clone() } else { layer.curve_knots[curve_ch].clone() };
let find_nearest = |pos: Pos2, knots: &[(f32, f32)]| -> Option<usize> {
let mut best = None;
let mut best_d = knot_radius * 2.5;
for (ki, &(kx, ky)) in knots.iter().enumerate() {
let kp = to_screen(kx, ky);
let d = (kp.x - pos.x).hypot(kp.y - pos.y);
if d < best_d { best_d = d; best = Some(ki); }
}
best
};
// Draw knots on active curve
for (ki, &(kx, ky)) in active_knots.iter().enumerate() {
let kp = to_screen(kx, ky);
let is_drag = self.curve_drag_knot == Some(ki);
let is_hover = hover_pos.map_or(false, |hp| (kp.x - hp.x).hypot(kp.y - hp.y) < knot_radius * 2.0);
let fill = if is_drag { Color32::WHITE } else if is_hover { Color32::from_gray(220) } else { Color32::from_gray(160) };
let sw = if is_drag || is_hover { 2.0 } else { 1.0 };
let sc = if curve_ch == 3 { Color32::from_gray(255) } else { colors[curve_ch] };
painter.circle_filled(kp, knot_radius, fill);
painter.circle_stroke(kp, knot_radius, Stroke::new(sw, sc));
}
let mut changed = false;
let pointer_pos = ui.input(|i| i.pointer.latest_pos());
let pointer_down = ui.input(|i| i.pointer.primary_down());
let pointer_released = ui.input(|i| i.pointer.primary_released());
let pointer_pressed = ui.input(|i| i.pointer.primary_pressed());
if let Some(pos) = pointer_pos {
if rect.contains(pos) {
if pointer_pressed {
if let Some(ki) = find_nearest(pos, &active_knots) {
self.curve_drag_knot = Some(ki);
} else {
let (nx, ny) = from_screen(pos);
let knots = if curve_ch == 3 { &mut layer.master_knots } else { &mut layer.curve_knots[curve_ch] };
knots.push((nx, ny));
knots.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
self.curve_drag_knot = knots.iter().position(|&(kx, ky)| (kx - nx).abs() < 0.01 && (ky - ny).abs() < 0.01);
changed = true;
}
}
if pointer_down {
if let (Some(ki), true) = (self.curve_drag_knot, true) {
let (_nx, ny) = from_screen(pos);
if curve_ch == 3 {
if ki < layer.master_knots.len() {
let kx = layer.master_knots[ki].0;
layer.master_knots[ki] = (kx, ny);
changed = true;
}
} else {
if ki < layer.curve_knots[curve_ch].len() {
let kx = layer.curve_knots[curve_ch][ki].0;
layer.curve_knots[curve_ch][ki] = (kx, ny);
changed = true;
}
}
}
}
if pointer_released {
self.curve_drag_knot = None;
}
} else if pointer_released {
self.curve_drag_knot = None;
}
}
// Right-click to remove
let right_clicked = ui.input(|i| i.pointer.secondary_released());
if right_clicked {
if let Some(pos) = pointer_pos {
if rect.contains(pos) {
if let Some(ki) = find_nearest(pos, &active_knots) {
let knots = if curve_ch == 3 { &mut layer.master_knots } else { &mut layer.curve_knots[curve_ch] };
if ki != 0 && ki != knots.len() - 1 {
knots.remove(ki);
changed = true;
self.curve_drag_knot = None;
}
}
}
}
}
if changed {
// Rebuild master LUT
let new_master = spline_lut(&layer.master_knots);
// Apply master to all channels, then apply per-channel overrides
for ch in 0..3usize {
let new_lut = if layer.curve_knots[ch].len() <= 2 {
// No per-channel override, use master directly
new_master.clone()
} else {
// Per-channel override: use per-channel knots
spline_lut(&layer.curve_knots[ch])
};
match ch {
0 => *lut_r = new_lut,
1 => *lut_g = new_lut,
_ => *lut_b = new_lut,
}
}
layer.dirty = true;
self.composite_dirty = true;
}
}
// Layer Effects panel
if !layer.effects.is_empty() {
ui.separator();
ui.horizontal(|ui| {
ui.strong("Layer Effects");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(egui::RichText::new(format!("{} effects", layer.effects.len()))
.color(Color32::from_gray(120)).small());
});
});
ui.add_space(2.0);
let fx_icons = [
("", Color32::from_rgb(180, 140, 100)), // Drop Shadow
("", Color32::from_rgb(100, 140, 180)), // Inner Shadow
("", Color32::from_rgb(200, 180, 100)), // Outer Glow
("", Color32::from_rgb(100, 180, 200)), // Inner Glow
("", Color32::from_rgb(180, 180, 180)), // Bevel
("", Color32::from_rgb(160, 140, 180)), // Satin
("", Color32::from_rgb(200, 100, 100)), // Color Overlay
("", Color32::from_rgb(100, 200, 100)), // Gradient Overlay
("", Color32::from_rgb(100, 100, 200)), // Pattern Overlay
("", Color32::from_rgb(200, 200, 200)), // Stroke
];
for (ei, fx) in layer.effects.iter().enumerate() {
let enabled = fx.is_enabled();
let name = fx.effect_name();
let icon_idx = match fx {
hcie_fx::LayerEffect::DropShadow { .. } => 0,
hcie_fx::LayerEffect::InnerShadow { .. } => 1,
hcie_fx::LayerEffect::OuterGlow { .. } => 2,
hcie_fx::LayerEffect::InnerGlow { .. } => 3,
hcie_fx::LayerEffect::BevelEmboss { .. } => 4,
hcie_fx::LayerEffect::Satin { .. } => 5,
hcie_fx::LayerEffect::ColorOverlay { .. } => 6,
hcie_fx::LayerEffect::GradientOverlay { .. } => 7,
hcie_fx::LayerEffect::PatternOverlay { .. } => 8,
hcie_fx::LayerEffect::Stroke { .. } => 9,
};
let (icon, icon_color) = fx_icons[icon_idx];
let text_color = if enabled {
ui.visuals().text_color()
} else {
ui.visuals().weak_text_color().gamma_multiply(0.5)
};
// Effect header row
let _bg_color = if enabled {
Color32::from_rgba_premultiplied(60, 60, 80, 180)
} else {
Color32::from_rgba_premultiplied(40, 40, 45, 120)
};
let header_rect = ui.available_rect_before_wrap();
let _header_rect = Rect::from_min_size(header_rect.min, Vec2::new(header_rect.width(), 20.0));
ui.horizontal(|ui| {
// Visibility icon (eye)
let eye = if enabled { "" } else { "" };
let eye_color = if enabled { Color32::from_rgb(100, 200, 100) } else { Color32::from_gray(80) };
ui.label(egui::RichText::new(eye).color(eye_color).size(10.0));
// Effect icon
ui.label(egui::RichText::new(icon).color(icon_color).size(12.0));
// Effect name
ui.label(egui::RichText::new(name).color(text_color).strong());
// Status badge
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if enabled {
ui.label(egui::RichText::new("ON")
.color(Color32::from_rgb(80, 180, 80)).small().strong());
} else {
ui.label(egui::RichText::new("OFF")
.color(Color32::from_gray(80)).small());
}
});
});
// Effect details (indented)
if enabled {
match fx {
hcie_fx::LayerEffect::DropShadow { blend_mode, color, opacity, angle, distance, size, spread, .. }
| hcie_fx::LayerEffect::InnerShadow { blend_mode, color, opacity, angle, distance, size, choke: spread, .. } => {
ui.indent(ei, |ui| {
ui.horizontal(|ui| {
// Color swatch
let c = Color32::from_rgb(color[0], color[1], color[2]);
let (rect, _) = ui.allocate_exact_size(Vec2::new(12.0, 12.0), egui::Sense::hover());
ui.painter().rect_filled(rect, 2.0, c);
ui.label(egui::RichText::new(format!("{:?}", blend_mode)).small());
ui.label(egui::RichText::new(format!("{:.0}%", opacity * 100.0)).small().color(Color32::from_gray(160)));
});
ui.label(egui::RichText::new(format!(
"Angle {:.0}° Distance {:.0}px Size {:.0}px Spread {:.0}%",
angle, distance, size, spread
)).small().color(Color32::from_gray(140)));
});
}
hcie_fx::LayerEffect::OuterGlow { blend_mode, color, opacity, size, spread, .. }
| hcie_fx::LayerEffect::InnerGlow { blend_mode, color, opacity, size, choke: spread, .. } => {
ui.indent(ei, |ui| {
ui.horizontal(|ui| {
let c = Color32::from_rgb(color[0], color[1], color[2]);
let (rect, _) = ui.allocate_exact_size(Vec2::new(12.0, 12.0), egui::Sense::hover());
ui.painter().rect_filled(rect, 2.0, c);
ui.label(egui::RichText::new(format!("{:?}", blend_mode)).small());
ui.label(egui::RichText::new(format!("{:.0}%", opacity * 100.0)).small().color(Color32::from_gray(160)));
});
ui.label(egui::RichText::new(format!("Size {:.0}px Spread {:.0}%", size, spread)).small().color(Color32::from_gray(140)));
});
}
hcie_fx::LayerEffect::BevelEmboss { style, technique, depth, size, angle, altitude, highlight_blend, shadow_blend, .. } => {
ui.indent(ei, |ui| {
ui.label(egui::RichText::new(format!("{:?} · {:?}", style, technique)).small());
ui.label(egui::RichText::new(format!(
"Depth {:.1} Size {:.0}px Angle {:.0}° Altitude {:.0}°",
depth, size, angle, altitude
)).small().color(Color32::from_gray(140)));
ui.label(egui::RichText::new(format!(
"Highlight: {:?} Shadow: {:?}",
highlight_blend, shadow_blend
)).small().color(Color32::from_gray(140)));
});
}
hcie_fx::LayerEffect::Satin { blend_mode, color, opacity, angle, distance, size, invert, .. } => {
ui.indent(ei, |ui| {
ui.horizontal(|ui| {
let c = Color32::from_rgb(color[0], color[1], color[2]);
let (rect, _) = ui.allocate_exact_size(Vec2::new(12.0, 12.0), egui::Sense::hover());
ui.painter().rect_filled(rect, 2.0, c);
ui.label(egui::RichText::new(format!("{:?}", blend_mode)).small());
ui.label(egui::RichText::new(format!("{:.0}%", opacity * 100.0)).small().color(Color32::from_gray(160)));
});
ui.label(egui::RichText::new(format!(
"Angle {:.0}° Distance {:.0}px Size {:.0}px Invert: {}",
angle, distance, size, if *invert { "Yes" } else { "No" }
)).small().color(Color32::from_gray(140)));
});
}
hcie_fx::LayerEffect::ColorOverlay { blend_mode, color, opacity, .. } => {
ui.indent(ei, |ui| {
ui.horizontal(|ui| {
let c = Color32::from_rgb(color[0], color[1], color[2]);
let (rect, _) = ui.allocate_exact_size(Vec2::new(12.0, 12.0), egui::Sense::hover());
ui.painter().rect_filled(rect, 2.0, c);
ui.label(egui::RichText::new(format!("{:?}", blend_mode)).small());
ui.label(egui::RichText::new(format!("{:.0}%", opacity * 100.0)).small().color(Color32::from_gray(160)));
});
});
}
hcie_fx::LayerEffect::GradientOverlay { blend_mode, opacity, angle, scale, .. } => {
ui.indent(ei, |ui| {
ui.label(egui::RichText::new(format!("{:?}", blend_mode)).small());
ui.label(egui::RichText::new(format!(
"Opacity {:.0}% Angle {:.0}° Scale {:.0}%",
opacity * 100.0, angle, scale
)).small().color(Color32::from_gray(140)));
});
}
hcie_fx::LayerEffect::PatternOverlay { blend_mode, opacity, scale, .. } => {
ui.indent(ei, |ui| {
ui.label(egui::RichText::new(format!("{:?}", blend_mode)).small());
ui.label(egui::RichText::new(format!(
"Opacity {:.0}% Scale {:.0}%",
opacity * 100.0, scale
)).small().color(Color32::from_gray(140)));
});
}
hcie_fx::LayerEffect::Stroke { position, fill_type, blend_mode, opacity, size, color, .. } => {
ui.indent(ei, |ui| {
ui.horizontal(|ui| {
let c = Color32::from_rgb(color[0], color[1], color[2]);
let (rect, _) = ui.allocate_exact_size(Vec2::new(12.0, 12.0), egui::Sense::hover());
ui.painter().rect_filled(rect, 2.0, c);
ui.label(egui::RichText::new(format!("{:?} {:?}", position, fill_type)).small());
});
ui.label(egui::RichText::new(format!(
"{:?} {:.0}% {:.0}px",
blend_mode, opacity * 100.0, size
)).small().color(Color32::from_gray(140)));
});
}
}
}
ui.add_space(2.0);
}
}
} else { ui.label("No layer selected"); }
});
});
// Canvas
ui.allocate_ui(Vec2::new(canvas_w, available.y), |ui| {
ui.vertical(|ui| {
let (resp, painter) = ui.allocate_painter(ui.available_size() - Vec2::new(0.0, if !self.compare_stats.is_empty() { 120.0 } else { 0.0 }), egui::Sense::click_and_drag());
let rect = resp.rect;
let scroll = ui.input(|i| i.smooth_scroll_delta.y);
if scroll != 0.0 && resp.hovered() {
let factor = if ui.input(|i| i.modifiers.command) {
if scroll > 0.0 { 1.1 } else { 1.0 / 1.1 }
} else { (scroll / 200.0).exp() };
let old = self.zoom;
self.zoom = (old * factor).clamp(ZOOM_MIN, ZOOM_MAX);
if let Some(mpos) = ui.input(|i| i.pointer.hover_pos()) {
let actual = self.zoom / old;
self.pan_offset += (mpos - rect.center() - self.pan_offset) * (1.0 - actual);
}
}
if resp.dragged_by(egui::PointerButton::Middle) { self.pan_offset += resp.drag_delta(); }
checkerboard_painter(&painter, rect, CHECKER_SIZE, Color32::from_gray(160), Color32::from_gray(200));
let active_tex = match self.view_mode {
1 => self.ref_texture.as_ref(),
2 => self.diff_texture.as_ref(),
_ => self.composite_texture.as_ref(),
};
if let Some(tex) = active_tex {
let tex_w = tex.size()[0] as f32;
let tex_h = tex.size()[1] as f32;
let cw = tex_w * self.zoom;
let ch = tex_h * self.zoom;
let center = rect.center() + self.pan_offset;
let img_rect = Rect::from_center_size(center, Vec2::new(cw, ch));
painter.image(tex.id(), img_rect, Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)), Color32::WHITE);
painter.rect_stroke(img_rect, 0.0, Stroke::new(1.0, Color32::from_gray(100)), StrokeKind::Outside);
} else {
let center = rect.center() + self.pan_offset;
let r = Rect::from_center_size(center, Vec2::new(self.canvas_w as f32 * self.zoom, self.canvas_h as f32 * self.zoom));
painter.rect_stroke(r, 0.0, Stroke::new(1.0, Color32::from_gray(100)), StrokeKind::Outside);
}
// Show comparison stats below canvas
if !self.compare_stats.is_empty() {
ui.separator();
ui.strong("Comparison Stats");
ui.label(egui::RichText::new(&self.compare_stats).monospace().small());
}
});
});
});
});
if self.composite_dirty {
ctx.request_repaint();
}
}
}
fn to_blend_mode(mode: hcie_protocol::BlendMode) -> BlendMode {
match mode {
hcie_protocol::BlendMode::Normal => BlendMode::Normal,
hcie_protocol::BlendMode::Dissolve => BlendMode::Dissolve,
hcie_protocol::BlendMode::Darken => BlendMode::Darken,
hcie_protocol::BlendMode::Multiply => BlendMode::Multiply,
hcie_protocol::BlendMode::ColorBurn => BlendMode::ColorBurn,
hcie_protocol::BlendMode::LinearBurn => BlendMode::LinearBurn,
hcie_protocol::BlendMode::DarkerColor => BlendMode::DarkerColor,
hcie_protocol::BlendMode::Lighten => BlendMode::Lighten,
hcie_protocol::BlendMode::Screen => BlendMode::Screen,
hcie_protocol::BlendMode::ColorDodge => BlendMode::ColorDodge,
hcie_protocol::BlendMode::LinearDodge => BlendMode::LinearDodge,
hcie_protocol::BlendMode::LighterColor=> BlendMode::LighterColor,
hcie_protocol::BlendMode::Overlay => BlendMode::Overlay,
hcie_protocol::BlendMode::SoftLight => BlendMode::SoftLight,
hcie_protocol::BlendMode::HardLight => BlendMode::HardLight,
hcie_protocol::BlendMode::VividLight => BlendMode::VividLight,
hcie_protocol::BlendMode::LinearLight => BlendMode::LinearLight,
hcie_protocol::BlendMode::PinLight => BlendMode::PinLight,
hcie_protocol::BlendMode::HardMix => BlendMode::HardMix,
hcie_protocol::BlendMode::Difference => BlendMode::Difference,
hcie_protocol::BlendMode::Exclusion => BlendMode::Exclusion,
hcie_protocol::BlendMode::Subtract => BlendMode::Subtract,
hcie_protocol::BlendMode::Divide => BlendMode::Divide,
hcie_protocol::BlendMode::Hue => BlendMode::Hue,
hcie_protocol::BlendMode::Saturation => BlendMode::Saturation,
hcie_protocol::BlendMode::Color => BlendMode::Color,
hcie_protocol::BlendMode::Luminosity => BlendMode::Luminosity,
hcie_protocol::BlendMode::PassThrough => BlendMode::Normal,
}
}
fn from_blend_mode(mode: BlendMode) -> hcie_protocol::BlendMode {
match mode {
BlendMode::Normal => hcie_protocol::BlendMode::Normal,
BlendMode::Dissolve => hcie_protocol::BlendMode::Dissolve,
BlendMode::Darken => hcie_protocol::BlendMode::Darken,
BlendMode::Multiply => hcie_protocol::BlendMode::Multiply,
BlendMode::ColorBurn => hcie_protocol::BlendMode::ColorBurn,
BlendMode::LinearBurn => hcie_protocol::BlendMode::LinearBurn,
BlendMode::DarkerColor => hcie_protocol::BlendMode::DarkerColor,
BlendMode::Lighten => hcie_protocol::BlendMode::Lighten,
BlendMode::Screen => hcie_protocol::BlendMode::Screen,
BlendMode::ColorDodge => hcie_protocol::BlendMode::ColorDodge,
BlendMode::LinearDodge => hcie_protocol::BlendMode::LinearDodge,
BlendMode::LighterColor=> hcie_protocol::BlendMode::LighterColor,
BlendMode::Overlay => hcie_protocol::BlendMode::Overlay,
BlendMode::SoftLight => hcie_protocol::BlendMode::SoftLight,
BlendMode::HardLight => hcie_protocol::BlendMode::HardLight,
BlendMode::VividLight => hcie_protocol::BlendMode::VividLight,
BlendMode::LinearLight => hcie_protocol::BlendMode::LinearLight,
BlendMode::PinLight => hcie_protocol::BlendMode::PinLight,
BlendMode::HardMix => hcie_protocol::BlendMode::HardMix,
BlendMode::Difference => hcie_protocol::BlendMode::Difference,
BlendMode::Exclusion => hcie_protocol::BlendMode::Exclusion,
BlendMode::Subtract => hcie_protocol::BlendMode::Subtract,
BlendMode::Divide => hcie_protocol::BlendMode::Divide,
BlendMode::Hue => hcie_protocol::BlendMode::Hue,
BlendMode::Saturation => hcie_protocol::BlendMode::Saturation,
BlendMode::Color => hcie_protocol::BlendMode::Color,
BlendMode::Luminosity => hcie_protocol::BlendMode::Luminosity,
BlendMode::PassThrough => hcie_protocol::BlendMode::Normal,
}
}
fn thumbnail_nearest(pixels: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
let mut out = vec![0u8; (dw * dh * 4) as usize];
for dy in 0..dh {
for dx in 0..dw {
let sx = (dx * sw / dw).min(sw.saturating_sub(1));
let sy = (dy * sh / dh).min(sh.saturating_sub(1));
let si = ((sy * sw + sx) * 4) as usize;
let di = ((dy * dw + dx) * 4) as usize;
out[di..di + 4].copy_from_slice(&pixels[si..si + 4]);
}
}
out
}
fn checkerboard_painter(painter: &egui::Painter, rect: Rect, ts: f32, ca: Color32, cb: Color32) {
let mut y = rect.min.y;
let mut row = 0;
while y < rect.max.y {
let mut x = rect.min.x;
let mut col = 0;
while x < rect.max.x {
let fill = if (row + col) % 2 == 0 { ca } else { cb };
painter.rect_filled(Rect::from_min_max(
Pos2::new(x, y),
Pos2::new((x + ts).min(rect.max.x), (y + ts).min(rect.max.y)),
), 0.0, fill);
x += ts; col += 1;
}
y += ts; row += 1;
}
}
fn extract_knots(lut: &[u8]) -> Vec<(f32, f32)> {
let mut knots = vec![(0.0, lut[0] as f32), (255.0, lut[255] as f32)];
fn sub(lut: &[u8], out: &mut Vec<(f32, f32)>, x0: f32, y0: f32, x1: f32, y1: f32, d: usize) {
if d > 8 { return; }
let xm = (x0 + x1) / 2.0;
let ym = lut[xm.round() as usize].min(255) as f32;
let yi = (y0 + y1) / 2.0;
if (ym - yi).abs() > 5.0 {
out.push((xm, ym));
sub(lut, out, x0, y0, xm, ym, d + 1);
sub(lut, out, xm, ym, x1, y1, d + 1);
}
}
sub(lut, &mut knots, 0.0, lut[0] as f32, 255.0, lut[255] as f32, 0);
knots.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
knots.dedup_by(|a, b| (a.0 - b.0).abs() < 0.5);
knots
}
fn spline_lut(knots: &[(f32, f32)]) -> Vec<u8> {
let n = knots.len();
let mut lut = vec![0u8; 256];
if n == 0 { for i in 0..256 { lut[i] = i as u8; } return lut; }
if n == 1 { let v = knots[0].1.clamp(0.0, 255.0).round() as u8; return vec![v; 256]; }
let mut pts: Vec<(f32, f32)> = knots.to_vec();
pts.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
pts.dedup_by(|a, b| (a.0 - b.0).abs() < 0.01);
let n = pts.len();
if n < 2 { let v = pts[0].1.clamp(0.0, 255.0).round() as u8; return vec![v; 256]; }
let mut h = vec![0.0f32; n - 1];
for i in 0..n - 1 { h[i] = pts[i + 1].0 - pts[i].0; }
let mut alpha = vec![0.0f32; n];
for i in 1..n - 1 {
alpha[i] = 3.0 * ((pts[i + 1].1 - pts[i].1) / h[i] - (pts[i].1 - pts[i - 1].1) / h[i - 1]);
}
let mut l = vec![1.0f32; n];
let mut mu = vec![0.0f32; n];
let mut z = vec![0.0f32; n];
for i in 1..n - 1 {
l[i] = 2.0 * (pts[i + 1].0 - pts[i - 1].0) - h[i - 1] * mu[i - 1];
if l[i].abs() < 1e-12 { continue; }
mu[i] = h[i] / l[i];
z[i] = (alpha[i] - h[i - 1] * z[i - 1]) / l[i];
}
let mut c = vec![0.0f32; n];
let mut b = vec![0.0f32; n.saturating_sub(1)];
let mut d = vec![0.0f32; n.saturating_sub(1)];
for j in (0..n - 1).rev() {
c[j] = z[j] - mu[j] * c[j + 1];
b[j] = (pts[j + 1].1 - pts[j].1) / h[j] - h[j] * (c[j + 1] + 2.0 * c[j]) / 3.0;
d[j] = (c[j + 1] - c[j]) / (3.0 * h[j]);
}
for i in 0..256 {
let x = i as f32;
if x <= pts[0].0 {
lut[i] = pts[0].1.clamp(0.0, 255.0).round() as u8;
} else if x >= pts[n - 1].0 {
lut[i] = pts[n - 1].1.clamp(0.0, 255.0).round() as u8;
} else {
for j in 0..n - 1 {
if x >= pts[j].0 && x <= pts[j + 1].0 {
let dx = x - pts[j].0;
let y = pts[j].1 + b[j] * dx + c[j] * dx * dx + d[j] * dx * dx * dx;
lut[i] = y.clamp(0.0, 255.0).round() as u8;
break;
}
}
}
}
lut
}
fn main() -> eframe::Result {
let mut opts = NativeOptions::default();
opts.viewport.inner_size = Some(Vec2::new(1400.0, 800.0));
eframe::run_native("hcie-io Test", opts, Box::new(|_cc| Ok(Box::new(HcieIoApp::default()))))
}