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,
|
2026-07-13 14:37:22 +03:00
|
|
|
|
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()))))
|
|
|
|
|
|
}
|