diff --git a/Cargo.lock b/Cargo.lock index 9849dfe..c6ee6ab 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,19 +455,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "av-data" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca67ba5d317924c02180c576157afd54babe48a76ebc66ce6d34bb8ba08308e" -dependencies = [ - "byte-slice-cast", - "bytes", - "num-derive", - "num-rational", - "num-traits", -] - [[package]] name = "av-scenechange" version = "0.14.1" @@ -592,15 +579,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "bitreader" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "886559b1e163d56c765bc3a985febb4eee8009f625244511d8ee3c432e08c066" -dependencies = [ - "cfg-if", -] - [[package]] name = "bitstream-io" version = "4.10.0" @@ -686,12 +664,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" -[[package]] -name = "byte-slice-cast" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" - [[package]] name = "bytemuck" version = "1.25.0" @@ -793,16 +765,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cfg-expr" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -1148,28 +1110,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" -[[package]] -name = "dav1d" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee89cb860616069c67520dcd66cacdb900b57c799f634a0eb6d91f6e2a82b61" -dependencies = [ - "av-data", - "bitflags 2.13.0", - "dav1d-sys", - "static_assertions", -] - -[[package]] -name = "dav1d-sys" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c91aea6668645415331133ed6f8ddf0e7f40160cd97a12d59e68716a58704b" -dependencies = [ - "libc", - "system-deps", -] - [[package]] name = "dconf_rs" version = "0.3.0" @@ -1873,15 +1813,6 @@ dependencies = [ "zune-inflate", ] -[[package]] -name = "fallible_collections" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88c69768c0a15262df21899142bc6df9b9b823546d4b4b9a7bc2d6c448ec6fd" -dependencies = [ - "hashbrown 0.13.2", -] - [[package]] name = "fast-srgb8" version = "1.0.0" @@ -2515,15 +2446,6 @@ dependencies = [ "ahash 0.7.8", ] -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash 0.8.12", -] - [[package]] name = "hashbrown" version = "0.14.5" @@ -2826,6 +2748,7 @@ dependencies = [ "proptest", "psd", "rand 0.8.6", + "rayon", "resvg 0.43.0", "rfd", "rstest", @@ -3453,12 +3376,10 @@ dependencies = [ "bytemuck", "byteorder-lite", "color_quant", - "dav1d", "exr", "gif 0.14.2", "image-webp 0.2.4", "moxcms", - "mp4parse", "num-traits", "png 0.18.1", "qoi", @@ -4039,20 +3960,6 @@ dependencies = [ "pxfm", ] -[[package]] -name = "mp4parse" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63a35203d3c6ce92d5251c77520acb2e57108c88728695aa883f70023624c570" -dependencies = [ - "bitreader", - "byteorder", - "fallible_collections", - "log", - "num-traits", - "static_assertions", -] - [[package]] name = "mutate_once" version = "0.1.2" @@ -5956,15 +5863,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -6354,31 +6252,12 @@ dependencies = [ "libc", ] -[[package]] -name = "system-deps" -version = "7.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - [[package]] name = "tap" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "target-lexicon" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" - [[package]] name = "tempfile" version = "3.27.0" @@ -6591,21 +6470,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -6636,12 +6500,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - [[package]] name = "tower" version = "0.5.3" @@ -7047,12 +6905,6 @@ dependencies = [ "vello_common", ] -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - [[package]] name = "version_check" version = "0.9.5" diff --git a/bevel_emboss_grid_results_v3.txt b/bevel_emboss_grid_results_v3.txt new file mode 100644 index 0000000..d32621e --- /dev/null +++ b/bevel_emboss_grid_results_v3.txt @@ -0,0 +1,2 @@ +lighting_blur=100 hl_scale=0.10 sh_scale=0.10 +avg_mae=29.5671 samples=864 combos=343 diff --git a/bevel_emboss_grid_results_v4.txt b/bevel_emboss_grid_results_v4.txt new file mode 100644 index 0000000..1a03f87 --- /dev/null +++ b/bevel_emboss_grid_results_v4.txt @@ -0,0 +1,2 @@ +lighting_blur=0 hl_scale=0.050 sh_scale=16.00 tilt_scale=0.05 +avg_mae=23.1730 samples=864 combos=432 diff --git a/emboss_results.txt b/emboss_results.txt new file mode 100644 index 0000000..10106af --- /dev/null +++ b/emboss_results.txt @@ -0,0 +1,3 @@ +=== Emboss Tuned === + Params: blur=5, hl_scale=0.05, sh_scale=2.50 + MAE: 14.583 diff --git a/hcie-engine-api/src/stroke_cache.rs b/hcie-engine-api/src/stroke_cache.rs index 720ae37..e8e3e12 100644 --- a/hcie-engine-api/src/stroke_cache.rs +++ b/hcie-engine-api/src/stroke_cache.rs @@ -222,6 +222,7 @@ impl Engine { if !layer.effects.is_empty() || !layer.styles.is_empty() { layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release); + self.document.composite_dirty = true; } // Take the pooled before buffer (no allocation) diff --git a/hcie-fx/src/apply_effects.rs b/hcie-fx/src/apply_effects.rs index 74aba8c..8af4b09 100644 --- a/hcie-fx/src/apply_effects.rs +++ b/hcie-fx/src/apply_effects.rs @@ -42,6 +42,14 @@ pub fn apply_layer_effects( let alpha = extract_alpha(pixels, px_count); + // Storage for Emboss/OuterBevel outside portions (applied to `behind` later) + let mut bevel_hl_outside: Option> = None; + let mut bevel_sh_outside: Option> = None; + let mut bevel_hl_blend: hcie_blend::BlendMode = hcie_blend::BlendMode::Screen; + let mut bevel_sh_blend: hcie_blend::BlendMode = hcie_blend::BlendMode::Multiply; + let mut bevel_hl_op: f32 = 1.0; + let mut bevel_sh_op: f32 = 1.0; + // 1. Prepare inside content (original pixels + inside effects) let mut inside = vec![0u8; px_count * 4]; inside.copy_from_slice(pixels); @@ -67,8 +75,38 @@ pub fn apply_layer_effects( *direction, *technique, *style, *highlight_color, *shadow_color, ); - composite_effect(&mut inside, &highlight, *highlight_blend, *highlight_opacity, px_count); - composite_effect(&mut inside, &shadow, *shadow_blend, *shadow_opacity, px_count); + + // For Emboss and OuterBevel, the effect extends outside the shape. + // Inside pixels go to `inside`, outside pixels go to `behind`. + let has_outer = matches!(*style, crate::types::BevelStyle::Emboss | crate::types::BevelStyle::OuterBevel); + if has_outer { + // Split highlight/shadow into inside and outside portions + let mut hl_inside = vec![0u8; px_count * 4]; + let mut hl_outside = vec![0u8; px_count * 4]; + let mut sh_inside = vec![0u8; px_count * 4]; + let mut sh_outside = vec![0u8; px_count * 4]; + for i in 0..px_count { + if alpha[i] > 0 { + hl_inside[i * 4..i * 4 + 4].copy_from_slice(&highlight[i * 4..i * 4 + 4]); + sh_inside[i * 4..i * 4 + 4].copy_from_slice(&shadow[i * 4..i * 4 + 4]); + } else { + hl_outside[i * 4..i * 4 + 4].copy_from_slice(&highlight[i * 4..i * 4 + 4]); + sh_outside[i * 4..i * 4 + 4].copy_from_slice(&shadow[i * 4..i * 4 + 4]); + } + } + composite_effect(&mut inside, &hl_inside, *highlight_blend, *highlight_opacity, px_count); + composite_effect(&mut inside, &sh_inside, *shadow_blend, *shadow_opacity, px_count); + // Store outside portions to apply to `behind` later + bevel_hl_outside = Some(hl_outside); + bevel_sh_outside = Some(sh_outside); + bevel_hl_blend = *highlight_blend; + bevel_sh_blend = *shadow_blend; + bevel_hl_op = *highlight_opacity; + bevel_sh_op = *shadow_opacity; + } else { + composite_effect(&mut inside, &highlight, *highlight_blend, *highlight_opacity, px_count); + composite_effect(&mut inside, &shadow, *shadow_blend, *shadow_opacity, px_count); + } } // b. Satin @@ -159,6 +197,15 @@ pub fn apply_layer_effects( } } + // d. Emboss/OuterBevel outside portion (highlight then shadow) + // Applied to `behind` so the outer bevel band renders behind the shape. + if let Some(hl_out) = &bevel_hl_outside { + composite_effect(&mut behind, hl_out, bevel_hl_blend, bevel_hl_op, px_count); + } + if let Some(sh_out) = &bevel_sh_outside { + composite_effect(&mut behind, sh_out, bevel_sh_blend, bevel_sh_op, px_count); + } + // 3. Blend inside ON TOP of behind using Normal blend for i in 0..px_count { let b = [behind[i * 4], behind[i * 4 + 1], behind[i * 4 + 2], behind[i * 4 + 3]]; diff --git a/hcie-fx/src/drop_shadow.rs b/hcie-fx/src/drop_shadow.rs index 73f637b..407378c 100644 --- a/hcie-fx/src/drop_shadow.rs +++ b/hcie-fx/src/drop_shadow.rs @@ -68,8 +68,6 @@ pub(crate) fn generate_shadow( // MAE-optimized against Krita ground truth (768 samples, 350 combos, 800x800) // Best: blur_factor=5.0, passes=5, spread_scale=1.00 - // Actually use the spread parameter (0.0 to 100.0) from the user - apply_spread(&mut offset_alpha, spread); let radius = size.max(0.0).round() as i32; if radius > 0 { @@ -81,6 +79,9 @@ pub(crate) fn generate_shadow( offset_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect(); } + // Actually use the spread parameter (0.0 to 100.0) from the user AFTER the blur! + apply_spread(&mut offset_alpha, spread); + for i in 0..px { let a = offset_alpha[i]; if a > 0 { diff --git a/hcie-fx/src/emboss.rs b/hcie-fx/src/emboss.rs index ba96d95..76a0844 100644 --- a/hcie-fx/src/emboss.rs +++ b/hcie-fx/src/emboss.rs @@ -1,5 +1,44 @@ use crate::types; use crate::helpers::box_blur_f32; + +/// Generate bevel & emboss highlight and shadow buffers. +/// +/// **Purpose:** Renders Photoshop-style Bevel & Emboss layer effect. Produces +/// two separate RGBA buffers (highlight + shadow) that the caller composites +/// over the layer content using Screen and Multiply blend modes respectively. +/// +/// **Logic & Workflow:** +/// 1. Compute inside/outside Euclidean Distance Transforms (EDT) from the +/// layer alpha mask. For Emboss and OuterBevel, also track which opaque +/// pixel is nearest to each outside pixel so the outer bevel can inherit +/// the alpha of the nearest content pixel. +/// 2. Build a continuous height field from the EDT values. The profile shape +/// depends on the BevelStyle: +/// - InnerBevel: ramps up from edge to center inside the shape. +/// - OuterBevel: ramps down from edge outward outside the shape. +/// - Emboss: combines both — ramps inside (raised) and outside (lowered). +/// 3. Apply a box-blur soften pass on the height field. +/// 4. Compute per-pixel lighting via central-difference normals · light vector. +/// 5. Apply a lighting blur to smooth wrinkles at sharp corners. +/// 6. Map lighting to highlight/shadow alpha. For outside pixels, use the +/// nearest opaque pixel's alpha instead of the (zero) local alpha. +/// +/// **Arguments:** +/// * `alpha` — Layer alpha mask (one byte per pixel) +/// * `w`, `h` — Canvas dimensions +/// * `depth` — Bevel depth (0–1000+, controls slope steepness) +/// * `size` — Bevel size in pixels (radius of the bevel band) +/// * `soften` — Soften amount in pixels (extra blur on height field) +/// * `angle` — Light azimuth angle in degrees +/// * `altitude` — Light altitude angle in degrees +/// * `direction` — Up or Down (flips lighting) +/// * `technique` — Smooth, ChiselHard, ChiselSoft +/// * `style` — InnerBevel, OuterBevel, Emboss, etc. +/// * `highlight_color` — RGBA color for the highlight channel +/// * `shadow_color` — RGBA color for the shadow channel +/// +/// **Returns:** +/// `(highlight_buf, shadow_buf)` — two RGBA buffers, same size as canvas. pub fn generate_bevel_emboss( alpha: &[u8], w: u32, @@ -27,6 +66,16 @@ pub fn generate_bevel_emboss( let dist_inside_sq = edt_inside(alpha, iw, ih); let dist_outside_sq = edt_outside(alpha, iw, ih); + // For Emboss and OuterBevel, track the nearest opaque source pixel so + // the outer bevel band can inherit the alpha of the nearest content pixel. + // This is critical: without it, alpha[i]==0 outside the shape produces + // zero-strength highlight/shadow, making Emboss/OuterBevel invisible. + let outside_src = if matches!(style, types::BevelStyle::Emboss | types::BevelStyle::OuterBevel) { + edt_outside_src(alpha, iw, ih) + } else { + vec![0usize; px] + }; + // Compute shape center for Emboss tilt let (cx, cy) = { let mut min_x = iw; @@ -76,15 +125,17 @@ pub fn generate_bevel_emboss( } } types::BevelStyle::Emboss => { + // Emboss = inner bevel (inside) + outer bevel (outside) + tilt. + // tilt_scale controls the global slope for 3D emboss effect. + // Negate tilt so direction=Down correctly produces highlight on + // the light side and shadow on the dark side. let px_x = (i % iw) as f32; let px_y = (i / iw) as f32; - let tilt = (px_x - cx) * light_x + (px_y - cy) * light_y; + let tilt = ((px_x - cx) * light_x + (px_y - cy) * light_y) * radius * 0.05; if alpha[i] > 0 { - let edge_factor = (d_in / radius).min(1.0); - radius * edge_factor + tilt + (d_in / radius).min(1.0) * radius + tilt } else { - let t = (d_out / radius).min(1.0); - (radius * (1.0 - t) * 0.5 + tilt).max(0.0) + ((radius - d_out).max(0.0) / radius) * radius + tilt } } _ => { @@ -155,13 +206,7 @@ pub fn generate_bevel_emboss( let ndl = nx * light_x + ny * light_y + nz * light_z; - let factor = if matches!(style, types::BevelStyle::Emboss) && is_inner { - let height_factor = height[i] / radius; - let blended = ndl * 0.4 + height_factor * 0.6; - blended * depth_scale - } else { - ndl - }; + let factor = ndl; lighting[i] = match direction { types::Direction::Up => factor, @@ -171,46 +216,59 @@ pub fn generate_bevel_emboss( } // Smooth the lighting to reduce wrinkles at sharp corners. - // MAE-optimized against Krita ground truth (320 samples, 256×256). - // Best: lighting_blur=5, hl_scale=0.30, sh_scale=2.00, avg_mae=17.18 + // Emboss uses tilt for 3D effect and needs sharp lighting (no blur). + // Inner/Outer bevel need blur for smooth corners. + // MAE-optimized against Photoshop ground truth (288 emboss samples, 256×256). // BEGIN_TUNING_ZONE - let lighting_blur = (soften_radius + 5).max(1); + let lighting_blur = if matches!(style, crate::types::BevelStyle::Emboss) { 0 } else { 5 }; lighting = box_blur_f32(&lighting, w, h, lighting_blur); for i in 0..px { let is_inner = alpha[i] > 0; let show = match style { - types::BevelStyle::InnerBevel => is_inner, - types::BevelStyle::OuterBevel => !is_inner, - types::BevelStyle::Emboss => true, + crate::types::BevelStyle::InnerBevel => is_inner, + crate::types::BevelStyle::OuterBevel => !is_inner, + crate::types::BevelStyle::Emboss => true, _ => is_inner, }; if !show { continue; } let dist_abs = if is_inner { dist_inside_sq[i].sqrt() } else { dist_outside_sq[i].sqrt() }; if !is_inner && dist_abs > radius { continue; } let technique_show = match technique { - types::Technique::Smooth => true, + crate::types::Technique::Smooth => true, _ => dist_abs <= radius, }; if !technique_show { continue; } let ndl = lighting[i]; - let is_flat = is_inner && dist_abs > radius; - + // Emboss has tilt so the interior is never truly flat — skip the + // is_flat shortcut for emboss to allow tilt-based highlight/shadow + // across the entire shape interior. + let is_flat = is_inner && dist_abs > radius && !matches!(style, crate::types::BevelStyle::Emboss); if is_flat { - if style == types::BevelStyle::Emboss && depth > 100.0 { - shadow_buf[i * 4 + 3] = alpha[i]; - } continue; } - // MAE-optimized against Krita ground truth (320 samples, 256×256). - // Best: lighting_blur=5, hl_scale=0.30, sh_scale=2.00, avg_mae=17.18 - let hl_pred = (ndl * 0.30).clamp(0.0, 1.0); - let sh_pred = (-ndl * 2.00).clamp(0.0, 1.0); + let (hl_scale, sh_scale) = if matches!(style, crate::types::BevelStyle::Emboss) { + (0.05, 16.00) + } else { + (0.05, 2.50) + }; + let hl_pred = (ndl * hl_scale).clamp(0.0, 1.0); + let sh_pred = (-ndl * sh_scale).clamp(0.0, 1.0); - let hl_a = (alpha[i] as f32 * hl_pred).round() as u8; - let sh_a = (alpha[i] as f32 * sh_pred).round() as u8; + // For outside pixels (alpha[i]==0), use the nearest opaque pixel's + // alpha as the strength base. This is what Photoshop does: the outer + // bevel band fades out based on distance to the shape edge, and the + // effect intensity is proportional to the nearest content alpha. + let base_alpha = if is_inner { + alpha[i] as f32 + } else { + alpha[outside_src[i]] as f32 + }; + + let hl_a = (base_alpha * hl_pred).round() as u8; + let sh_a = (base_alpha * sh_pred).round() as u8; if hl_a > 0 { highlight_buf[i * 4] = highlight_color[0]; @@ -267,6 +325,33 @@ fn edt_outside(alpha: &[u8], w: usize, h: usize) -> Vec { dt } +/// EDT for outside pixels with source index tracking. +/// +/// **Purpose:** Same as `edt_outside` but also returns, for each pixel, the +/// index of the nearest opaque pixel. This is needed by Emboss and OuterBevel +/// to give outside (transparent) pixels the alpha of the nearest content pixel. +/// +/// **Returns:** `Vec` where each element is the pixel index of the +/// nearest opaque source pixel. +fn edt_outside_src(alpha: &[u8], w: usize, h: usize) -> Vec { + let n = w * h; + let mut dt = vec![1e20f32; n]; + let mut src = vec![0usize; n]; + + for y in 0..h { + for x in 0..w { + let i = y * w + x; + if alpha[i] > 0 { + dt[i] = 0.0; + src[i] = i; + } + } + } + + edt_2d_src(&mut dt, &mut src, w, h); + src +} + /// 2D Euclidean Distance Transform using the Felzenszwalb & Huttenlocher algorithm. fn edt_2d(dt: &mut [f32], w: usize, h: usize) { let mut col = vec![0.0f32; h]; @@ -336,4 +421,84 @@ fn edt_1d(f: &mut [f32]) { } f.copy_from_slice(&d); +} + +/// 2D EDT with source index tracking (Felzenszwalb & Huttenlocher). +fn edt_2d_src(dt: &mut [f32], src_idx: &mut [usize], w: usize, h: usize) { + let mut col = vec![0.0f32; h]; + let mut col_src = vec![0usize; h]; + for x in 0..w { + for y in 0..h { + col[y] = dt[y * w + x]; + col_src[y] = src_idx[y * w + x]; + } + edt_1d_src(&mut col, &mut col_src); + for y in 0..h { + dt[y * w + x] = col[y]; + src_idx[y * w + x] = col_src[y]; + } + } + + let mut row = vec![0.0f32; w]; + let mut row_src = vec![0usize; w]; + for y in 0..h { + for x in 0..w { + row[x] = dt[y * w + x]; + row_src[x] = src_idx[y * w + x]; + } + edt_1d_src(&mut row, &mut row_src); + for x in 0..w { + dt[y * w + x] = row[x]; + src_idx[y * w + x] = row_src[x]; + } + } +} + +/// 1D EDT with source index tracking (Felzenszwalb & Huttenlocher). +fn edt_1d_src(f: &mut [f32], src_idx: &mut [usize]) { + let n = f.len(); + if n == 0 { return; } + + let mut d = vec![0.0f32; n]; + let mut d_src = vec![0usize; n]; + let mut v = vec![0usize; n]; + let mut z = vec![0.0f32; n + 1]; + + let mut k = 0usize; + v[0] = 0; + z[0] = f32::NEG_INFINITY; + z[1] = f32::INFINITY; + + for q in 1..n { + let qf = q as f32; + let fq = f[q] + qf * qf; + let vk = v[k]; + let fv_vk = f[vk] + vk as f32 * vk as f32; + + let mut s = (fq - fv_vk) / (2.0 * qf - 2.0 * vk as f32); + while s <= z[k] { + k = k.wrapping_sub(1); + if k == usize::MAX { k = 0; break; } + let vk2 = v[k]; + let fv_vk2 = f[vk2] + vk2 as f32 * vk2 as f32; + s = (fq - fv_vk2) / (2.0 * qf - 2.0 * vk2 as f32); + } + k += 1; + v[k] = q; + z[k] = s; + z[k + 1] = f32::INFINITY; + } + + k = 0; + for q in 0..n { + let qf = q as f32; + while z[k + 1] < qf { k += 1; } + let vk = v[k]; + let diff = qf - vk as f32; + d[q] = diff * diff + f[vk]; + d_src[q] = src_idx[v[k]]; + } + + f.copy_from_slice(&d); + src_idx.copy_from_slice(&d_src); } \ No newline at end of file diff --git a/hcie-fx/src/inner_shadow.rs b/hcie-fx/src/inner_shadow.rs index 5d1959a..1059ee3 100644 --- a/hcie-fx/src/inner_shadow.rs +++ b/hcie-fx/src/inner_shadow.rs @@ -64,8 +64,6 @@ pub(crate) fn generate_inner_shadow( } } - apply_spread(&mut offset_alpha, choke); - let radius = size.max(0.0).round() as i32; if radius > 0 { let gaussian_r = (radius as f32 / 10.0).round().max(1.0) as i32; @@ -75,6 +73,8 @@ pub(crate) fn generate_inner_shadow( offset_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect(); } + apply_spread(&mut offset_alpha, choke); + for i in 0..px { let a = ((offset_alpha[i] as f32 * 0.50) as u32 * alpha[i] as u32 / 255) as u8; if a > 0 { diff --git a/hcie-fx/src/tuned.rs b/hcie-fx/src/tuned.rs index 6f088cd..041f910 100644 --- a/hcie-fx/src/tuned.rs +++ b/hcie-fx/src/tuned.rs @@ -19,6 +19,7 @@ pub fn generate_bevel_tuned( hl_scale: f32, sh_scale: f32, profile_exp: f32, + tilt_scale: f32, ) -> (Vec, Vec) { let hl_color = [255u8; 4]; let sh_color = [0u8; 4]; @@ -33,6 +34,14 @@ pub fn generate_bevel_tuned( let dist_inside_sq = edt_inside(alpha, iw, ih); let dist_outside_sq = edt_outside(alpha, iw, ih); + // For Emboss and OuterBevel, track the nearest opaque source pixel so + // the outer bevel band can inherit the alpha of the nearest content pixel. + let outside_src = if matches!(*style, types::BevelStyle::Emboss | types::BevelStyle::OuterBevel) { + edt_outside_src(alpha, iw, ih) + } else { + vec![0usize; px] + }; + // Compute shape center for Emboss tilt let (cx, cy) = { let mut min_x = iw; @@ -81,15 +90,22 @@ pub fn generate_bevel_tuned( } } types::BevelStyle::Emboss => { + // Emboss = inner bevel (inside) + outer bevel (outside) + tilt. + // tilt_scale controls how much global slope is added across the + // shape based on light direction. The tilt makes one side of + // the shape raised (toward light = highlight) and the other + // lowered (away from light = shadow), producing the 3D effect. let px_x = (i % iw) as f32; let px_y = (i / iw) as f32; - let tilt = (px_x - cx) * light_x + (px_y - cy) * light_y; + // Negate tilt so direction=Down (default) correctly produces + // highlight on the light side and shadow on the dark side. + let tilt = ((px_x - cx) * light_x + (px_y - cy) * light_y) * radius * tilt_scale; if alpha[i] > 0 { let t = (d_in / radius).min(1.0); - radius * t.powf(profile_exp) + tilt + t.powf(profile_exp) * radius + tilt } else { - let t = (d_out / radius).min(1.0); - (radius * (1.0 - t).powf(profile_exp) * 0.5 + tilt).max(0.0) + let t = ((radius - d_out).max(0.0) / radius).min(1.0); + (1.0 - t).powf(profile_exp) * radius + tilt } } _ => { @@ -142,13 +158,7 @@ pub fn generate_bevel_tuned( let ndl = nx * light_x + ny * light_y + nz * light_z; - let factor = if matches!(style, types::BevelStyle::Emboss) && is_inner { - let height_factor = height[i] / radius; - let blended = ndl * 0.4 + height_factor * 0.6; - blended * depth_scale - } else { - ndl - }; + let factor = ndl; lighting[i] = match direction { types::Direction::Up => factor, @@ -171,19 +181,27 @@ pub fn generate_bevel_tuned( let dist_abs = if is_inner { dist_inside_sq[i].sqrt() } else { dist_outside_sq[i].sqrt() }; if !is_inner && dist_abs > radius { continue; } let ndl = lighting[i]; - let is_flat = is_inner && dist_abs > radius; + // Emboss has tilt so the interior is never truly flat — skip the + // is_flat shortcut for emboss to allow tilt-based highlight/shadow + // across the entire shape interior. + let is_flat = is_inner && dist_abs > radius && !matches!(*style, types::BevelStyle::Emboss); if is_flat { - if *style == types::BevelStyle::Emboss && depth > 100.0 { - shadow_buf[i * 4 + 3] = alpha[i]; - } continue; } let hl_pred = (ndl * hl_scale).clamp(0.0, 1.0); let sh_pred = (-ndl * sh_scale).clamp(0.0, 1.0); - let hl_a = (alpha[i] as f32 * hl_pred).round() as u8; - let sh_a = (alpha[i] as f32 * sh_pred).round() as u8; + // For outside pixels (alpha[i]==0), use the nearest opaque pixel's + // alpha as the strength base, matching Photoshop behavior. + let base_alpha = if is_inner { + alpha[i] as f32 + } else { + alpha[outside_src[i]] as f32 + }; + + let hl_a = (base_alpha * hl_pred).round() as u8; + let sh_a = (base_alpha * sh_pred).round() as u8; if hl_a > 0 { highlight_buf[i * 4] = hl_color[0]; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs index 4fa3af9..249c335 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -129,6 +129,9 @@ pub struct HcieIcedApp { pub style_color_picker_idx: usize, /// HSL color state for the style color picker (hue 0..360, sat 0..1, light 0..1). pub style_color_hsl: (f32, f32, f32), + /// True when the color picker is editing the shadow color (BevelEmboss), + /// false when editing the highlight color or the single-color style's color. + pub style_color_is_shadow: bool, /// Currently active color picker tab (0=Wheel, 1=Sliders, 2=Grid). pub color_tab: usize, /// Layer Style dialog drag offset from center (x, y) in pixels. @@ -295,7 +298,12 @@ pub enum Message { LayerStyleUpdateParam(usize, String, f32), LayerStyleUpdateBlendMode(usize, String), LayerStyleUpdateColor(usize, [u8; 4]), + LayerStyleUpdateString(usize, String, String), + LayerStyleUpdateShadowColor(usize, [u8; 4]), + LayerStyleUpdateShadowBlend(usize, String), + LayerStyleUpdateShadowOpacity(usize, f32), ShowStyleColorPicker(usize), + ShowStyleShadowColorPicker(usize), HideStyleColorPicker, OpenLayerStyleDialog, LayerStyleDialogDragStart(f32, f32), @@ -532,6 +540,7 @@ impl HcieIcedApp { show_style_color_picker: false, style_color_picker_idx: 0, style_color_hsl: (0.0, 0.0, 0.0), + style_color_is_shadow: false, color_tab: 0, layer_style_offset: (0.0, 0.0), layer_style_dragging: false, @@ -1238,6 +1247,12 @@ impl HcieIcedApp { *highlight_opacity = value; *shadow_opacity = value; }, + (LayerStyle::BevelEmboss { highlight_opacity, .. }, "highlight_opacity") => { + *highlight_opacity = value; + }, + (LayerStyle::BevelEmboss { shadow_opacity, .. }, "shadow_opacity") => { + *shadow_opacity = value; + }, (LayerStyle::Satin { opacity, .. }, "opacity") => *opacity = value, (LayerStyle::Satin { angle, .. }, "angle") => *angle = value, (LayerStyle::Satin { distance, .. }, "distance") => *distance = value, @@ -1292,6 +1307,7 @@ impl HcieIcedApp { | LayerStyle::GradientOverlay { blend_mode, .. } | LayerStyle::PatternOverlay { blend_mode, .. } | LayerStyle::Stroke { blend_mode, .. } => *blend_mode = mode, + LayerStyle::BevelEmboss { highlight_blend_mode, .. } => *highlight_blend_mode = mode, _ => {} } self.documents[self.active_doc].engine.update_layer_style(layer_id, new_style); @@ -1333,7 +1349,13 @@ impl HcieIcedApp { | LayerStyle::ColorOverlay { color, .. } | LayerStyle::Satin { color, .. } | LayerStyle::Stroke { color, .. } => *color = new_color, - LayerStyle::BevelEmboss { highlight_color, .. } => *highlight_color = new_color, + LayerStyle::BevelEmboss { highlight_color, shadow_color, .. } => { + if self.style_color_is_shadow { + *shadow_color = new_color; + } else { + *highlight_color = new_color; + } + } _ => {} } self.documents[self.active_doc].engine.update_layer_style(layer_id, updated_style); @@ -1353,9 +1375,151 @@ impl HcieIcedApp { self.refresh_composite_if_needed(); return Task::perform(async {}, |_| Message::CompositeRefresh); } + Message::LayerStyleUpdateString(index, param, value) => { + let layer_id = self.documents[self.active_doc].engine.active_layer_id(); + if layer_id != 0 { + let styles = self.documents[self.active_doc].engine.get_layer_styles(layer_id); + let disc_names = ["DropShadow", "InnerShadow", "OuterGlow", "InnerGlow", + "BevelEmboss", "Satin", "ColorOverlay", "GradientOverlay", "PatternOverlay", "Stroke"]; + if index < disc_names.len() { + let disc = disc_names[index]; + let existing = styles.iter().find(|s| { + matches!((disc, s), + ("BevelEmboss", LayerStyle::BevelEmboss { .. }) + | ("DropShadow", LayerStyle::DropShadow { .. }) + | ("InnerShadow", LayerStyle::InnerShadow { .. }) + ) + }); + if let Some(style) = existing { + let mut new_style = style.clone(); + match (&mut new_style, param.as_str()) { + (LayerStyle::BevelEmboss { style, .. }, "style") => *style = value, + (LayerStyle::BevelEmboss { technique, .. }, "technique") => *technique = value, + (LayerStyle::BevelEmboss { direction, .. }, "direction") => *direction = value, + (LayerStyle::BevelEmboss { highlight_blend_mode, .. }, "highlight_blend") => *highlight_blend_mode = value, + (LayerStyle::BevelEmboss { shadow_blend_mode, .. }, "shadow_blend") => *shadow_blend_mode = value, + _ => {} + } + self.documents[self.active_doc].engine.update_layer_style(layer_id, new_style); + } + } + } + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Message::LayerStyleUpdateShadowColor(index, new_color) => { + let layer_id = self.documents[self.active_doc].engine.active_layer_id(); + if layer_id != 0 { + let styles = self.documents[self.active_doc].engine.get_layer_styles(layer_id); + let disc_names = ["DropShadow", "InnerShadow", "OuterGlow", "InnerGlow", + "BevelEmboss", "Satin", "ColorOverlay", "GradientOverlay", "PatternOverlay", "Stroke"]; + if index < disc_names.len() { + let disc = disc_names[index]; + let existing = styles.iter().find(|s| { + matches!((disc, s), ("BevelEmboss", LayerStyle::BevelEmboss { .. })) + }); + if let Some(style) = existing { + let mut updated_style = style.clone(); + if let LayerStyle::BevelEmboss { shadow_color, .. } = &mut updated_style { + *shadow_color = new_color; + } + self.documents[self.active_doc].engine.update_layer_style(layer_id, updated_style); + } + } + } + let r = new_color[0] as f32 / 255.0; + let g = new_color[1] as f32 / 255.0; + let b = new_color[2] as f32 / 255.0; + let max = r.max(g).max(b); + let min = r.min(g).min(b); + let l = (max + min) / 2.0; + let s = if max == min { 0.0 } else if l > 0.5 { (max - min) / (2.0 - max - min) } else { (max - min) / (max + min) }; + let h = if max == min { 0.0 } else if max == r { ((g - b) / (max - min) + if g < b { 6.0 } else { 0.0 }) / 6.0 * 360.0 } else if max == g { ((b - r) / (max - min) + 2.0) / 6.0 * 360.0 } else { ((r - g) / (max - min) + 4.0) / 6.0 * 360.0 }; + self.style_color_hsl = (h, s, l); + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Message::LayerStyleUpdateShadowBlend(index, mode) => { + let layer_id = self.documents[self.active_doc].engine.active_layer_id(); + if layer_id != 0 { + let styles = self.documents[self.active_doc].engine.get_layer_styles(layer_id); + let disc_names = ["DropShadow", "InnerShadow", "OuterGlow", "InnerGlow", + "BevelEmboss", "Satin", "ColorOverlay", "GradientOverlay", "PatternOverlay", "Stroke"]; + if index < disc_names.len() { + let disc = disc_names[index]; + let existing = styles.iter().find(|s| { + matches!((disc, s), ("BevelEmboss", LayerStyle::BevelEmboss { .. })) + }); + if let Some(style) = existing { + let mut updated_style = style.clone(); + if let LayerStyle::BevelEmboss { shadow_blend_mode, .. } = &mut updated_style { + *shadow_blend_mode = mode; + } + self.documents[self.active_doc].engine.update_layer_style(layer_id, updated_style); + } + } + } + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Message::LayerStyleUpdateShadowOpacity(index, value) => { + let layer_id = self.documents[self.active_doc].engine.active_layer_id(); + if layer_id != 0 { + let styles = self.documents[self.active_doc].engine.get_layer_styles(layer_id); + let disc_names = ["DropShadow", "InnerShadow", "OuterGlow", "InnerGlow", + "BevelEmboss", "Satin", "ColorOverlay", "GradientOverlay", "PatternOverlay", "Stroke"]; + if index < disc_names.len() { + let disc = disc_names[index]; + let existing = styles.iter().find(|s| { + matches!((disc, s), ("BevelEmboss", LayerStyle::BevelEmboss { .. })) + }); + if let Some(style) = existing { + let mut updated_style = style.clone(); + if let LayerStyle::BevelEmboss { shadow_opacity, .. } = &mut updated_style { + *shadow_opacity = value; + } + self.documents[self.active_doc].engine.update_layer_style(layer_id, updated_style); + } + } + } + self.refresh_composite_if_needed(); + return Task::perform(async {}, |_| Message::CompositeRefresh); + } + Message::ShowStyleShadowColorPicker(idx) => { + self.show_style_color_picker = true; + self.style_color_picker_idx = idx; + self.style_color_is_shadow = true; + let disc_names = ["DropShadow", "InnerShadow", "OuterGlow", "InnerGlow", + "BevelEmboss", "Satin", "ColorOverlay", "GradientOverlay", "PatternOverlay", "Stroke"]; + let layer_id = self.documents[self.active_doc].engine.active_layer_id(); + if layer_id != 0 && idx < disc_names.len() { + let disc = disc_names[idx]; + let styles = self.documents[self.active_doc].engine.get_layer_styles(layer_id); + if let Some(style) = styles.iter().find(|s| { + matches!((disc, s), ("BevelEmboss", LayerStyle::BevelEmboss { .. })) + }) { + let color = if let LayerStyle::BevelEmboss { shadow_color, .. } = style { + *shadow_color + } else { + [0, 0, 0, 255] + }; + let r = color[0] as f32 / 255.0; + let g = color[1] as f32 / 255.0; + let b = color[2] as f32 / 255.0; + let max = r.max(g).max(b); + let min = r.min(g).min(b); + let l = (max + min) / 2.0; + let s = if max == min { 0.0 } else if l > 0.5 { (max - min) / (2.0 - max - min) } else { (max - min) / (max + min) }; + let h = if max == min { 0.0 } else if max == r { ((g - b) / (max - min) + if g < b { 6.0 } else { 0.0 }) / 6.0 * 360.0 } else if max == g { ((b - r) / (max - min) + 2.0) / 6.0 * 360.0 } else { ((r - g) / (max - min) + 4.0) / 6.0 * 360.0 }; + self.style_color_hsl = (h, s, l); + } + } + return Task::none(); + } Message::ShowStyleColorPicker(idx) => { self.show_style_color_picker = true; self.style_color_picker_idx = idx; + self.style_color_is_shadow = false; // Initialize HSL from current color let disc_names = ["DropShadow", "InnerShadow", "OuterGlow", "InnerGlow", "BevelEmboss", "Satin", "ColorOverlay", "GradientOverlay", "PatternOverlay", "Stroke"]; diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dock/state.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dock/state.rs index 3984672..977db64 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/dock/state.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dock/state.rs @@ -38,8 +38,6 @@ pub enum PaneType { Script, /// AI Chat panel. AiChat, - /// Layer Styles panel. - LayerStyles, /// Layer Details panel. LayerDetails, /// Geometry panel (vector shapes). @@ -59,7 +57,6 @@ impl PaneType { PaneType::Properties => "Properties", PaneType::Script => "Script", PaneType::AiChat => "AI Assistant", - PaneType::LayerStyles => "Layer Styles", PaneType::LayerDetails => "Layer Details", PaneType::Geometry => "Geometry", } @@ -225,7 +222,7 @@ impl DockState { if let Some(target) = target_pane { let axis = match pane_type { - PaneType::Layers | PaneType::Properties | PaneType::History | PaneType::LayerStyles | PaneType::LayerDetails => { + PaneType::Layers | PaneType::Properties | PaneType::History | PaneType::LayerDetails => { iced::widget::pane_grid::Axis::Horizontal } _ => iced::widget::pane_grid::Axis::Vertical, diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs b/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs index 76eb3c1..f665de7 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs @@ -88,20 +88,6 @@ pub fn dock_view<'a>( PaneType::AiChat => { crate::panels::ai_chat_panel::view("", &[], colors) } - PaneType::LayerStyles => { - let doc = &app.documents[app.active_doc]; - let styles = doc.engine.get_layer_styles(doc.engine.active_layer_id()); - // Leak the Vec so the Element can borrow it for the full lifetime. - // The leaked memory is reclaimed on next frame when the panel rebuilds. - let styles: &'static [hcie_engine_api::LayerStyle] = Box::leak(styles.into_boxed_slice()); - crate::panels::layer_styles::view( - styles, app.selected_style, colors, - app.show_style_color_picker, - app.style_color_picker_idx, - app.style_color_hsl, - app.layer_style_offset, - ) - } PaneType::LayerDetails => { let doc = &app.documents[app.active_doc]; crate::panels::layer_details::view(&doc.cached_layers, doc.engine.active_layer_id(), colors) @@ -296,7 +282,6 @@ fn pane_type_icon(pane_type: PaneType) -> Option<&'static str> { PaneType::Properties => Some("icons/panel-settings.svg"), PaneType::Script => Some("icons/panel-pen.svg"), PaneType::AiChat => Some("icons/panel-ai.svg"), - PaneType::LayerStyles => Some("icons/glow.svg"), PaneType::LayerDetails => Some("icons/panel-layers.svg"), PaneType::Geometry => Some("icons/vector_rect.svg"), } diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_styles.rs b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_styles.rs index c623567..ba6488b 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_styles.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/panels/layer_styles.rs @@ -35,6 +35,72 @@ const BLEND_MODES: &[&str] = &[ "Subtract", "Divide", "Hue", "Saturation", "Color", "Luminosity", ]; +/// Bevel & Emboss style options. +const BEVEL_STYLES: &[&str] = &[ + "Inner Bevel", "Outer Bevel", "Emboss", "Pillow Emboss", "Stroke Emboss", +]; + +/// Bevel & Emboss technique options. +const BEVEL_TECHNIQUES: &[&str] = &[ + "Smooth", "Chisel Hard", "Chisel Soft", +]; + +/// Bevel & Emboss direction options. +const BEVEL_DIRECTIONS: &[&str] = &[ + "Up", "Down", +]; + +/// Bevel & Emboss contour options. +const BEVEL_CONTOURS: &[&str] = &[ + "Linear", "Cone", "Cone-Inverted", "Gaussian", "Half Round", + "Round", "Ring", "Ring-Double", "Sawtooth", "Square", "Valley", + "Shallow-Slope", "Wave", "Cove", "Washboard", +]; + +/// Mapping: human-readable BevelEmboss style name -> internal enum string. +fn bevel_style_to_internal(label: &str) -> &str { + match label { + "Inner Bevel" => "InnerBevel", + "Outer Bevel" => "OuterBevel", + "Emboss" => "Emboss", + "Pillow Emboss" => "PillowEmboss", + "Stroke Emboss" => "StrokeEmboss", + _ => "InnerBevel", + } +} + +/// Mapping: internal BevelEmboss style enum string -> human-readable label. +fn bevel_style_to_label(internal: &str) -> &str { + match internal { + "InnerBevel" => "Inner Bevel", + "OuterBevel" => "Outer Bevel", + "Emboss" => "Emboss", + "PillowEmboss" => "Pillow Emboss", + "StrokeEmboss" => "Stroke Emboss", + _ => "Inner Bevel", + } +} + +/// Mapping: human-readable technique label -> internal enum string. +fn bevel_technique_to_internal(label: &str) -> &str { + match label { + "Smooth" => "Smooth", + "Chisel Hard" => "ChiselHard", + "Chisel Soft" => "ChiselSoft", + _ => "Smooth", + } +} + +/// Mapping: internal technique enum string -> human-readable label. +fn bevel_technique_to_label(internal: &str) -> &str { + match internal { + "Smooth" => "Smooth", + "ChiselHard" => "Chisel Hard", + "ChiselSoft" => "Chisel Soft", + _ => "Smooth", + } +} + /// Check if a LayerStyle matches a discriminant name. fn style_matches(discriminant: &str, style: &LayerStyle) -> bool { matches!( @@ -399,12 +465,119 @@ fn build_style_params<'a>( params = params.push(slider_row("Size", size, 0.0..=250.0, move |v| Message::LayerStyleUpdateParam(selected, "size".to_string(), v), colors)); } "BevelEmboss" => { - let (depth, size, angle, altitude, soften) = match style { - Some(LayerStyle::BevelEmboss { depth, size, angle, altitude, soften, .. }) => - (*depth, *size, *angle, *altitude, *soften), - _ => (1.0, 5.0, 120.0, 30.0, 0.0), + let (depth, size, angle, altitude, soften, style_str, technique_str, direction_str, + highlight_opacity, shadow_opacity, hl_color, sh_color, hl_blend, sh_blend) = match style { + Some(LayerStyle::BevelEmboss { + depth, size, angle, altitude, soften, style, technique, direction, + highlight_opacity, shadow_opacity, highlight_color, shadow_color, + highlight_blend_mode, shadow_blend_mode, .. + }) => (*depth, *size, *angle, *altitude, *soften, + style.clone(), technique.clone(), direction.clone(), + *highlight_opacity, *shadow_opacity, *highlight_color, *shadow_color, + highlight_blend_mode.clone(), shadow_blend_mode.clone()), + _ => (1.0, 5.0, 120.0, 30.0, 0.0, + "InnerBevel".to_string(), "Smooth".to_string(), "Up".to_string(), + 0.75, 0.75, [255, 255, 255, 255], [0, 0, 0, 255], + "Screen".to_string(), "Multiply".to_string()), }; - params = params.push(slider_row("Depth", depth, 0.0..=10.0, move |v| Message::LayerStyleUpdateParam(selected, "depth".to_string(), v), colors)); + + // ── Structure section: Style, Technique, Direction ── + let style_label_str = bevel_style_to_label(&style_str); + let style_static: &'static str = Box::leak(style_label_str.to_string().into_boxed_str()); + let style_list = pick_list(BEVEL_STYLES, Some(style_static), move |v: &str| { + Message::LayerStyleUpdateString(selected, "style".to_string(), bevel_style_to_internal(v).to_string()) + }).text_size(10); + + let tech_label_str = bevel_technique_to_label(&technique_str); + let tech_static: &'static str = Box::leak(tech_label_str.to_string().into_boxed_str()); + let tech_list = pick_list(BEVEL_TECHNIQUES, Some(tech_static), move |v: &str| { + Message::LayerStyleUpdateString(selected, "technique".to_string(), bevel_technique_to_internal(v).to_string()) + }).text_size(10); + + let dir_static: &'static str = Box::leak(direction_str.clone().into_boxed_str()); + let dir_list = pick_list(BEVEL_DIRECTIONS, Some(dir_static), move |v: &str| { + Message::LayerStyleUpdateString(selected, "direction".to_string(), v.to_string()) + }).text_size(10); + + params = params.push(row![text("Style:").size(10), style_list, Space::with_width(Length::Fixed(10.0)), + text("Tech:").size(10), tech_list, Space::with_width(Length::Fixed(10.0)), + text("Dir:").size(10), dir_list].spacing(3).align_y(iced::Alignment::Center)); + + params = params.push(horizontal_rule(1)); + + // ── Shading section: Highlight ── + params = params.push(text("Shading").size(10).font(iced::Font::MONOSPACE)); + + // Highlight blend mode dropdown + let hl_blend_static: &'static str = Box::leak(hl_blend.clone().into_boxed_str()); + let hl_blend_list = pick_list(BLEND_MODES, Some(hl_blend_static), move |mode: &str| { + Message::LayerStyleUpdateString(selected, "highlight_blend".to_string(), mode.to_string()) + }).width(Length::Fixed(110.0)).text_size(10); + + params = params.push(row![text("Hl Mode:").size(10), hl_blend_list].spacing(3).align_y(iced::Alignment::Center)); + + // Highlight opacity slider + params = params.push(slider_row("Hl Opacity", highlight_opacity, 0.0..=1.0, move |v| Message::LayerStyleUpdateParam(selected, "highlight_opacity".to_string(), v), colors)); + + // Highlight color swatch + let hc = hl_color; + let hl_swatch_bg = container(text("")) + .width(32).height(24) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba( + hc[0] as f32 / 255.0, hc[1] as f32 / 255.0, hc[2] as f32 / 255.0, 1.0, + ))), + border: iced::Border::default().color(iced::Color::from_rgba(1.0, 1.0, 1.0, 0.15)).width(1).rounded(4), + shadow: iced::Shadow { color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.6), offset: iced::Vector::new(0.0, 1.0), blur_radius: 4.0 }, + ..Default::default() + }); + let hl_swatch_clickable = iced::widget::mouse_area(hl_swatch_bg) + .on_press(Message::ShowStyleColorPicker(selected)) + .interaction(iced::mouse::Interaction::Pointer); + params = params.push(row![text("Hl Color:").size(10), hl_swatch_clickable].spacing(4).align_y(iced::Alignment::Center)); + + // ── Shadow ── + // Shadow blend mode dropdown + let sh_blend_static: &'static str = Box::leak(sh_blend.clone().into_boxed_str()); + let sh_blend_list = pick_list(BLEND_MODES, Some(sh_blend_static), move |mode: &str| { + Message::LayerStyleUpdateShadowBlend(selected, mode.to_string()) + }).width(Length::Fixed(110.0)).text_size(10); + + params = params.push(row![text("Sh Mode:").size(10), sh_blend_list].spacing(3).align_y(iced::Alignment::Center)); + + // Shadow opacity slider + params = params.push(slider_row("Sh Opacity", shadow_opacity, 0.0..=1.0, move |v| Message::LayerStyleUpdateShadowOpacity(selected, v), colors)); + + // Shadow color swatch + let sc = sh_color; + let sh_swatch_bg = container(text("")) + .width(32).height(24) + .style(move |_theme| iced::widget::container::Style { + background: Some(iced::Background::Color(iced::Color::from_rgba( + sc[0] as f32 / 255.0, sc[1] as f32 / 255.0, sc[2] as f32 / 255.0, 1.0, + ))), + border: iced::Border::default().color(iced::Color::from_rgba(1.0, 1.0, 1.0, 0.15)).width(1).rounded(4), + shadow: iced::Shadow { color: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.6), offset: iced::Vector::new(0.0, 1.0), blur_radius: 4.0 }, + ..Default::default() + }); + let sh_swatch_clickable = iced::widget::mouse_area(sh_swatch_bg) + .on_press(Message::ShowStyleShadowColorPicker(selected)) + .interaction(iced::mouse::Interaction::Pointer); + params = params.push(row![text("Sh Color:").size(10), sh_swatch_clickable].spacing(4).align_y(iced::Alignment::Center)); + + params = params.push(horizontal_rule(1)); + + // ── Geometry section ── + params = params.push(text("Geometry").size(10).font(iced::Font::MONOSPACE)); + + // Contour dropdown + let contour_static: &'static str = Box::leak("Linear".to_string().into_boxed_str()); + let contour_list: iced::widget::PickList<&str, &[&str], &str, Message> = pick_list(BEVEL_CONTOURS, Some(contour_static), move |v: &str| { + Message::LayerStyleUpdateString(selected, "contour".to_string(), v.to_string()) + }).width(Length::Fixed(120.0)).text_size(10); + params = params.push(row![text("Contour:").size(10), contour_list].spacing(3)); + + params = params.push(slider_row("Depth", depth, 0.0..=500.0, move |v| Message::LayerStyleUpdateParam(selected, "depth".to_string(), v), colors)); params = params.push(slider_row("Size", size, 0.0..=250.0, move |v| Message::LayerStyleUpdateParam(selected, "size".to_string(), v), colors)); params = params.push(slider_row("Angle", angle, 0.0..=360.0, move |v| Message::LayerStyleUpdateParam(selected, "angle".to_string(), v), colors)); params = params.push(slider_row("Altitude", altitude, 0.0..=90.0, move |v| Message::LayerStyleUpdateParam(selected, "altitude".to_string(), v), colors)); @@ -538,9 +711,11 @@ fn slider_row<'a>( let value_label = text(format!("{:.1}", value)).size(9).width(Length::Fixed(40.0)); let label_text = text(label).size(10).width(Length::Fixed(70.0)); + let step = if *range.end() <= 1.0 { 0.01 } else { 1.0 }; + row![ label_text, - slider(range, value, on_change).step(0.1).width(Length::Fill), + slider(range, value, on_change).step(step).width(Length::Fill), value_label, ] .spacing(3) diff --git a/hcie-io/Cargo.toml b/hcie-io/Cargo.toml index e5fad2c..ac5b897 100644 --- a/hcie-io/Cargo.toml +++ b/hcie-io/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "bmp", "gif", "tiff", "ico", "pnm", "qoi", "ff", "hdr", "dds", "avif", "avif-native", "tga", "exr"] } +image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "bmp", "gif", "tiff", "ico", "pnm", "qoi", "ff", "hdr", "dds", "avif", "tga", "exr"] } log = "0.4" serde = { version = "1.0", features = ["derive"] } rand = "0.8" @@ -34,3 +34,4 @@ zip = { version = "2.2", default-features = false, features = ["deflate"] } +rayon = "1.10" diff --git a/hcie-io/examples/compare_psd.rs b/hcie-io/examples/compare_psd.rs index 947adf7..42bc9d1 100644 --- a/hcie-io/examples/compare_psd.rs +++ b/hcie-io/examples/compare_psd.rs @@ -2,8 +2,8 @@ //! layer records with the original. fn main() { - let input_path = "/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/_test_images/example3/Example3-mini.psd"; - let output_path = "/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/_test_images/example3/Example3-mini-hcie-v2.psd"; + let input_path = "/mnt/extra/00_PROJECTS/hcie-rust-_images/_test_images/example3/Example3-mini.psd"; + let output_path = "/mnt/extra/00_PROJECTS/hcie-rust-_images/_test_images/example3/Example3-mini-hcie-v2.psd"; println!("Reading original PSD..."); let orig_bytes = std::fs::read(input_path).unwrap(); diff --git a/hcie-io/examples/eval_styles.rs b/hcie-io/examples/eval_styles.rs new file mode 100644 index 0000000..bf3167d --- /dev/null +++ b/hcie-io/examples/eval_styles.rs @@ -0,0 +1,100 @@ +use std::fs; +use hcie_io::psd::PsdLoader; +use hcie_fx::generate_bevel_tuned; +use image::{RgbaImage, GenericImageView}; +use std::path::Path; + +fn main() { + let base_dir = "_tmp/psd_tunes/bevel_emboss_png_set"; + let mut samples = vec![]; + for entry in fs::read_dir(base_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().unwrap() == "png" { + let name = path.file_stem().unwrap().to_str().unwrap().to_string(); + samples.push((name, path)); + } + } + + let mut inner_mae = 0.0; + let mut inner_count = 0; + let mut outer_mae = 0.0; + let mut outer_count = 0; + let mut emboss_mae = 0.0; + let mut emboss_count = 0; + + let lb = 15; + let hl = 1.0; + let sh = 1.0; + + println!("Evaluating {} samples", samples.len()); + for (name, png_path) in samples { + let psd_name = name.split("_").next().unwrap().to_string() + ".psd"; + let psd_path = format!("_tmp/psd_tunes/{}", psd_name); + if !Path::new(&psd_path).exists() { continue; } + + let target = image::open(&png_path).unwrap().to_rgba8(); + let mut psd = PsdLoader::new(&psd_path).unwrap(); + let layers = psd.get_layers(); + + let mut layer = None; + for l in layers { + if l.effects.is_some() && l.effects.as_ref().unwrap().bevel_emboss.is_some() { + layer = Some(l); + break; + } + } + if layer.is_none() { continue; } + let layer = layer.unwrap(); + let fx = layer.effects.unwrap(); + let bevel = fx.bevel_emboss.unwrap(); + + use hcie_fx::types::*; + let mut style = BevelStyle::InnerBevel; + if name.contains("InnerBevel") { style = BevelStyle::InnerBevel; } + if name.contains("OuterBevel") { style = BevelStyle::OuterBevel; } + if name.contains("Emboss") { style = BevelStyle::Emboss; } + + let (out_w, out_h, out_pixels) = generate_bevel_tuned( + layer.width as usize, layer.height as usize, &layer.rgba, + bevel.size as f32, bevel.depth as f32, bevel.soften as f32, bevel.angle as f32, bevel.altitude as f32, + style, lb, hl, sh + ); + + let mut diff_sum = 0.0; + let mut px_count = 0; + + let offset_x = (out_w as i32 - target.width() as i32) / 2; + let offset_y = (out_h as i32 - target.height() as i32) / 2; + + for y in 0..target.height() { + for x in 0..target.width() { + let sx = x as i32 + offset_x; + let sy = y as i32 + offset_y; + if sx >= 0 && sx < out_w as i32 && sy >= 0 && sy < out_h as i32 { + let s_idx = ((sy as usize) * out_w + (sx as usize)) * 4; + let t_idx = ((y as usize) * target.width() as usize + (x as usize)) * 4; + + let src_a = layer.rgba[s_idx+3]; + if src_a > 0 { + diff_sum += (out_pixels[s_idx] as f32 - target[t_idx] as f32).abs(); + diff_sum += (out_pixels[s_idx+1] as f32 - target[t_idx+1] as f32).abs(); + diff_sum += (out_pixels[s_idx+2] as f32 - target[t_idx+2] as f32).abs(); + px_count += 3; // R, G, B + } + } + } + } + + if px_count > 0 { + let mae = diff_sum / px_count as f32; + if name.contains("InnerBevel") { inner_mae += mae; inner_count += 1; } + if name.contains("OuterBevel") { outer_mae += mae; outer_count += 1; } + if name.contains("Emboss") { emboss_mae += mae; emboss_count += 1; } + } + } + + println!("Inner Bevel MAE: {:.4} ({} samples)", inner_mae / inner_count as f32, inner_count); + println!("Outer Bevel MAE: {:.4} ({} samples)", outer_mae / outer_count as f32, outer_count); + println!("Emboss MAE: {:.4} ({} samples)", emboss_mae / emboss_count as f32, emboss_count); +} diff --git a/hcie-io/examples/inspect_diff.rs b/hcie-io/examples/inspect_diff.rs new file mode 100644 index 0000000..2a3e2d8 --- /dev/null +++ b/hcie-io/examples/inspect_diff.rs @@ -0,0 +1,36 @@ +use image::GenericImageView; + +fn main() { + let ref_img = image::open("_images/_psd_stil_test/test_2/Emboss.png").unwrap().to_rgba8(); + let test_img = image::open("/tmp/test2_Emboss.png").unwrap().to_rgba8(); + + let mut diff_sum = 0.0; + let mut max_diff = 0; + let mut px = 0; + + let mut printed = 0; + + for y in 0..ref_img.height() { + for x in 0..ref_img.width() { + let rp = ref_img.get_pixel(x, y); + let tp = test_img.get_pixel(x, y); + + let d = (rp[0] as i32 - tp[0] as i32).abs() + + (rp[1] as i32 - tp[1] as i32).abs() + + (rp[2] as i32 - tp[2] as i32).abs() + + (rp[3] as i32 - tp[3] as i32).abs(); + + diff_sum += d as f64; + max_diff = max_diff.max(d); + px += 4; + + if d > 100 && printed < 20 { + println!("x: {}, y: {} | REF: {:?} | TEST: {:?}", x, y, rp, tp); + printed += 1; + } + } + } + + println!("Avg diff per channel: {:.2}", diff_sum / px as f64); + println!("Max pixel diff: {}", max_diff); +} diff --git a/hcie-io/examples/inspect_emboss.rs b/hcie-io/examples/inspect_emboss.rs new file mode 100644 index 0000000..f9c566d --- /dev/null +++ b/hcie-io/examples/inspect_emboss.rs @@ -0,0 +1,192 @@ +#![allow(dead_code, unused_imports, unused_variables, unused_macros)] +/// Visual inspection tool for Bevel & Emboss rendering. +/// +/// Loads a PSD from `_tmp/psd_tunes/bevel_emboss_psd_set/`, applies the +/// current `generate_bevel_emboss` implementation, composites highlight/shadow +/// over the layer fill, and writes the result to `_tmp/emboss_inspect/` so we +/// can compare side-by-side with the Photoshop ground truth PNG. +/// +/// Usage: +/// cargo run --example inspect_emboss -p hcie-io -- +/// cargo run --example inspect_emboss -p hcie-io -- be_L_emboss_d100_sz10_sf0_down_alt30_smooth +/// +/// If no stem is given, runs a small representative subset. + +use hcie_fx::types::{BevelStyle, Direction, Technique}; +use hcie_blend::BlendMode; + +const PSD_DIR: &str = "_tmp/psd_tunes/bevel_emboss_psd_set"; +const PNG_DIR: &str = "_tmp/psd_tunes/bevel_emboss_png_set"; +const OUT_DIR: &str = "_tmp/emboss_inspect"; + +fn parse_stem(stem: &str) -> Option<(String, String, f32, f32, f32, String, f32, String)> { + // be_{shape}_{style}_d{depth}_sz{size}_sf{soften}_{dir}_alt{altitude}_{technique} + let parts: Vec<&str> = stem.split('_').collect(); + if parts.len() < 9 || parts[0] != "be" { return None; } + let shape = parts[1].to_string(); + let style = parts[2].to_string(); + let depth = parts[3].strip_prefix('d')?.parse::().ok()?; + let size = parts[4].strip_prefix("sz")?.parse::().ok()?; + let soften = parts[5].strip_prefix("sf")?.parse::().ok()?; + let direction = parts[6].to_string(); + let altitude = parts[7].strip_prefix("alt")?.parse::().ok()?; + let technique = parts[8].to_string(); + Some((shape, style, depth, size, soften, direction, altitude, technique)) +} + +fn render_one(stem: &str) { + let params = match parse_stem(stem) { + Some(p) => p, + None => { eprintln!(" cannot parse stem: {}", stem); return; } + }; + let (shape, style_str, depth, size, soften, direction_str, altitude, _technique) = params; + + let style_enum = match style_str.as_str() { + "emboss" => BevelStyle::Emboss, + "outer" => BevelStyle::OuterBevel, + _ => BevelStyle::InnerBevel, + }; + let dir_enum = match direction_str.as_str() { + "down" => Direction::Down, + _ => Direction::Up, + }; + + let psd_path = format!("{}/{}.psd", PSD_DIR, stem); + let png_path = format!("{}/{}.png", PNG_DIR, stem); + + let layers = match hcie_psd::import_psd(std::path::Path::new(&psd_path)) { + Ok(l) => l, + Err(e) => { eprintln!(" SKIP {}: {}", stem, e); return; } + }; + + // Find the layer with effects + let layer = match layers.iter().find(|l| !l.effects.is_empty()) { + Some(l) => l, + None => { eprintln!(" SKIP {}: no effects layer", stem); return; } + }; + + let w = layer.width; + let h = layer.height; + let px = (w * h) as usize; + + // Extract alpha from layer pixels + let mut alpha = vec![0u8; px]; + for i in 0..px { + alpha[i] = layer.pixels[i * 4 + 3]; + } + + // Get the bevel params from the parsed effect — but we'll use apply_layer_effects + // for the full pipeline (inside + behind split for emboss/outerbevel) + let _ = { + let mut angle = 120.0f32; + let mut hl_color = [255u8; 4]; + let mut sh_color = [0u8; 4]; + for e in &layer.effects { + if let hcie_protocol::effects::LayerEffect::BevelEmboss { + angle: a, highlight_color, shadow_color, .. + } = e + { + angle = *a; + hl_color = *highlight_color; + sh_color = *shadow_color; + } + } + (angle, hl_color, sh_color) + }; + + // Convert protocol effects to hcie-fx effects and run the full pipeline + let fx_effects: Vec = layer.effects.iter().map(|e| { + hcie_fx::types::protocol_to_hcie_fx_effect(e) + }).collect(); + + // Use apply_layer_effects for the full rendering pipeline + let out = hcie_fx::apply_layer_effects( + &layer.pixels, w, h, + &fx_effects, + layer.fill_opacity, + ); + + // Compute MAE against reference + let ref_img = match image::open(&png_path) { + Ok(i) => i.to_rgba8(), + Err(e) => { eprintln!(" SKIP ref {}: {}", stem, e); return; } + }; + let (rw, rh) = (ref_img.width(), ref_img.height()); + let ref_raw = ref_img.into_raw(); + + let mut diff_sum = 0.0f64; + let mut content_pixels = 0u64; + for i in 0..px { + let ra = ref_raw[i * 4 + 3]; + let oa = out[i * 4 + 3]; + if ra > 0 || oa > 0 { + content_pixels += 1; + for c in 0..4 { + diff_sum += (out[i * 4 + c] as i32 - ref_raw[i * 4 + c] as i32).abs() as f64; + } + } + } + let mae = if content_pixels > 0 { + diff_sum / content_pixels as f64 / 4.0 + } else { + 0.0 + }; + + // Save our rendering + let out_img: image::RgbaImage = image::ImageBuffer::from_raw(w, h, out.clone()).unwrap(); + let out_path = format!("{}/{}.png", OUT_DIR, stem); + let _ = out_img.save(&out_path); + + // Also save a side-by-side comparison + let combined_w = w + rw + 4; + let combined_h = h.max(rh); + let mut combined = image::RgbaImage::new(combined_w, combined_h); + // our output (left) + for y in 0..h { + for x in 0..w { + let i = (y * w + x) as usize * 4; + combined.put_pixel(x, y, image::Rgba([out[i], out[i+1], out[i+2], out[i+3]])); + } + } + // ref (right, after 4px gap) + for y in 0..rh { + for x in 0..rw { + let i = (y * rw + x) as usize * 4; + combined.put_pixel(w as u32 + 4 + x, y, image::Rgba([ref_raw[i], ref_raw[i+1], ref_raw[i+2], ref_raw[i+3]])); + } + } + let cmp_path = format!("{}/{}_cmp.png", OUT_DIR, stem); + let _ = combined.save(&cmp_path); + + println!(" {} : MAE={:.2} (saved {})", stem, mae, cmp_path); +} + +fn main() { + let _ = std::fs::create_dir_all(OUT_DIR); + + let args: Vec = std::env::args().collect(); + if args.len() > 1 { + // Render specific stems + for stem in &args[1..] { + render_one(stem); + } + } else { + // Representative subset: different shapes, styles, depths, sizes + let subset = [ + "be_L_emboss_d100_sz10_sf0_down_alt30_smooth", + "be_L_emboss_d100_sz25_sf0_up_alt60_smooth", + "be_L_inner_d100_sz10_sf0_down_alt30_smooth", + "be_L_inner_d100_sz25_sf0_up_alt60_smooth", + "be_rect_emboss_d100_sz10_sf0_down_alt30_smooth", + "be_rect_inner_d100_sz10_sf0_down_alt30_smooth", + "be_circle_emboss_d100_sz10_sf0_down_alt30_smooth", + "be_circle_inner_d100_sz10_sf0_down_alt30_smooth", + "be_star_emboss_d200_sz25_sf5_up_alt60_smooth", + "be_star_inner_d200_sz25_sf5_up_alt60_smooth", + ]; + println!("Rendering {} representative samples to {}...", subset.len(), OUT_DIR); + for stem in &subset { + render_one(stem); + } + } +} \ No newline at end of file diff --git a/hcie-io/examples/inspect_layer.rs b/hcie-io/examples/inspect_layer.rs index bb3fb38..552d457 100644 --- a/hcie-io/examples/inspect_layer.rs +++ b/hcie-io/examples/inspect_layer.rs @@ -1,7 +1,7 @@ use std::path::Path; fn main() { - let orig_path = Path::new("/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/_test_images/example3/Example3-mini.psd"); + let orig_path = Path::new("/mnt/extra/00_PROJECTS/hcie-rust-_images/_test_images/example3/Example3-mini.psd"); let saved_path = Path::new("/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/Example3-mini-roundtrip.psd"); let orig_layers = hcie_psd::import_psd(orig_path).unwrap(); diff --git a/hcie-io/examples/inspect_psd.rs b/hcie-io/examples/inspect_psd.rs index c812896..574be24 100644 --- a/hcie-io/examples/inspect_psd.rs +++ b/hcie-io/examples/inspect_psd.rs @@ -1,5 +1,5 @@ fn main() { - let orig = std::fs::read("/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/_test_images/example3/Example3-mini.psd").unwrap(); + let orig = std::fs::read("/mnt/extra/00_PROJECTS/hcie-rust-_images/_test_images/example3/Example3-mini.psd").unwrap(); let saved = std::fs::read("/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/Example3-mini-roundtrip.psd").unwrap(); let orig_psd = hcie_psd::Psd::from_bytes(&orig).unwrap(); diff --git a/hcie-io/examples/tune_mae_bevel_emboss_grid.rs b/hcie-io/examples/tune_mae_bevel_emboss_grid.rs index 83cf1ad..63e6df0 100644 --- a/hcie-io/examples/tune_mae_bevel_emboss_grid.rs +++ b/hcie-io/examples/tune_mae_bevel_emboss_grid.rs @@ -1,88 +1,38 @@ #![allow(dead_code, unused_imports, unused_variables, unused_macros)] /// Grid-search Bevel & Emboss MAE tuning against Photoshop ground-truth PNGs. /// -/// Expects `_tmp/bevel_emboss_png_set/` with transparent PNGs exported -/// from `_tmp/bevel_emboss_psd_set/`. +/// Expects `_tmp/psd_tunes/bevel_emboss_png_set/` with transparent PNGs exported +/// from `_tmp/psd_tunes/bevel_emboss_psd_set/`. +/// +/// Uses the full `apply_layer_effects` pipeline (including inside/behind split +/// for Emboss/OuterBevel) so the tuning matches real rendering behavior. +/// +/// Usage: +/// cargo run --example tune_mae_bevel_emboss_grid -p hcie-io --release +use std::fs; +use rayon::prelude::*; use std::io::Write; -const GT_DIR: &str = "_tmp/bevel_emboss_png_set"; -const CANVAS: u32 = 256; -const MARGIN: u32 = 64; -const RESULTS_FILE: &str = "bevel_emboss_grid_results_v3.txt"; +const GT_DIR: &str = "_tmp/psd_tunes/bevel_emboss_png_set"; +const RESULTS_FILE: &str = "bevel_emboss_grid_results_v4.txt"; struct Sample { name: String, - shape: String, style: String, depth: f32, size: f32, soften: f32, direction: String, altitude: f32, + angle: f32, ref_pixels: Vec, content_mask: Vec, -} - -fn make_shape_alpha(shape: &str) -> Vec { - let n = (CANVAS * CANVAS) as usize; - let mut a = vec![0u8; n]; - let cx = CANVAS / 2; - let cy = CANVAS / 2; - - for y in 0..CANVAS { - for x in 0..CANVAS { - let inside = match shape { - "rect" => x >= 64 && x < CANVAS - 64 && y >= 64 && y < CANVAS - 64, - "tall" => x >= 96 && x < CANVAS - 96 && y >= 32 && y < CANVAS - 32, - "wide" => x >= 32 && x < CANVAS - 32 && y >= 96 && y < CANVAS - 96, - "circle" => { - let dx = x as f32 - cx as f32; - let dy = y as f32 - cy as f32; - let r = (CANVAS / 2 - 64) as f32; - (dx * dx + dy * dy).sqrt() <= r - } - "triangle" => { - let dx = (x as i32 - cx as i32).abs(); - let top = 64i32; - let bottom = (CANVAS - 64) as i32; - if (y as i32) < top || (y as i32) > bottom { false } else { - let y_span = ((y as i32) - top) as f32 / (bottom - top) as f32; - let half_width_at_y = (CANVAS - 128) as f32 / 2.0 * (1.0 - y_span); - (dx as f32) <= half_width_at_y - } - } - "star" => { - let dx = x as f32 - cx as f32; - let dy = y as f32 - cy as f32; - let r = (dx * dx + dy * dy).sqrt(); - if r > 96.0 || r < 20.0 { false } else { - let angle = dy.atan2(dx); - let sector = (angle * 5.0).cos(); - let limit = 64.0 + sector * 32.0; - r <= limit - } - } - "L" => { - (x >= 64 && x < 96 && y >= 64 && y < CANVAS - 64) - || (x >= 64 && x < CANVAS - 64 && y >= CANVAS - 96 && y < CANVAS - 64) - } - "T" => { - (y >= 64 && y < 96 && x >= 64 && x < CANVAS - 64) - || (x >= 112 && x < 144 && y >= 64 && y < CANVAS - 64) - } - "cross" => { - (x >= 112 && x < 144 && y >= 48 && y < CANVAS - 48) - || (y >= 112 && y < 144 && x >= 48 && x < CANVAS - 48) - } - _ => false, - }; - if inside { - a[(y * CANVAS + x) as usize] = 255; - } - } - } - a + layer_pixels: Vec, + fx_effects: Vec, + fill_opacity: f32, + w: u32, + h: u32, } fn main() { @@ -109,7 +59,7 @@ fn main() { None => { eprintln!(" SKIP: {}", stem); continue; } }; - let (shape, style, depth, size, soften, direction, altitude, _technique) = params; + let (_shape, style, depth, size, soften, direction, altitude, _technique) = params; let img = match image::open(&path) { Ok(i) => i, @@ -118,17 +68,52 @@ fn main() { let rgba = img.to_rgba8(); let raw = rgba.into_raw(); - let npix = (CANVAS * CANVAS) as usize; + let w = img.width(); + let h = img.height(); + let npix = (w * h) as usize; let mut content_mask = vec![false; npix]; for i in 0..npix { content_mask[i] = raw[i * 4 + 3] > 0; } + let psd_path_str = format!("_tmp/psd_tunes/bevel_emboss_psd_set/{}.psd", stem); + let (layer_pixels, fx_effects, fill_opacity, angle) = if let Ok(layers) = hcie_psd::import_psd(std::path::Path::new(&psd_path_str)) { + let mut found = None; + for l in layers { + if !l.effects.is_empty() { + let fx_effects: Vec = l.effects.iter() + .map(|e| hcie_fx::types::protocol_to_hcie_fx_effect(e)) + .collect(); + // Extract angle from BevelEmboss effect + let mut angle = 120.0f32; + for e in &l.effects { + if let hcie_protocol::effects::LayerEffect::BevelEmboss { angle: a, .. } = e { + angle = *a; + } + } + found = Some((l.pixels, fx_effects, l.fill_opacity, angle)); + break; + } + } + match found { + Some(v) => v, + None => { eprintln!(" SKIP no effects layer: {}", stem); continue; } + } + } else { + eprintln!(" SKIP PSD missing for {}: {}", stem, psd_path_str); + continue; + }; + samples.push(Sample { name: stem, - shape, style, depth, size, soften, direction, altitude, + style, depth, size, soften, direction, altitude, + angle, ref_pixels: raw, content_mask, + layer_pixels, + fx_effects, + fill_opacity, + w, h, }); } @@ -139,42 +124,33 @@ fn main() { } let content_count = samples[0].content_mask.iter().filter(|&&b| b).count(); - println!("Content pixels per image: {} / {} ({:.1}%)", - content_count, npix(), content_count as f64 / npix() as f64 * 100.0); + println!("Content pixels per image: {}", content_count); - let rect_alpha: Vec = make_shape_alpha("rect"); - let circle_alpha: Vec = make_shape_alpha("circle"); + // Use all samples (inner bevel, outer bevel, emboss) for combined tuning - let lighting_blurs: &[i32] = &[0, 5, 10, 15, 25, 40]; - let hl_scales: &[f32] = &[0.5, 0.75, 1.0, 1.25, 1.5, 2.0]; - let sh_scales: &[f32] = &[0.5, 0.75, 1.0, 1.25, 1.5, 2.0]; + // Grid search over lighting_blur, hl_scale, sh_scale, tilt_scale + // Wide ranges to cover high-depth PSDs (e.g. Sultan depth=350) and + // various bevel sizes (10, 25+). Each axis spans low→high so the + // search can find optima for both simple and complex shapes. + let lighting_blurs: &[i32] = &[0, 2, 5]; + let hl_scales: &[f32] = &[0.05, 0.1, 0.2, 0.5]; + let sh_scales: &[f32] = &[2.0, 8.0, 16.0]; + let tilt_scales: &[f32] = &[0.05, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, 5.0, 8.0]; - let total_combos = lighting_blurs.len() * hl_scales.len() * sh_scales.len(); + let total_combos = lighting_blurs.len() * hl_scales.len() * sh_scales.len() * tilt_scales.len(); println!("Grid: {} combos x {} samples = {} evals", total_combos, samples.len(), total_combos * samples.len()); std::io::stdout().flush().ok(); let mut best_avg_mae = f64::MAX; - let mut best_params = (0i32, 0.0f32, 0.0f32); + let mut best_params = (0i32, 0.0f32, 0.0f32, 0.0f32); let mut eval_count = 0usize; for &lighting_blur in lighting_blurs { for &hl_scale in hl_scales { for &sh_scale in sh_scales { - let mut total_mae = 0.0f64; - - for s in &samples { - let alpha = match s.shape.as_str() { - "circle" => &circle_alpha, - "rect" => &rect_alpha, - other => { - let owned = make_shape_alpha(other); - let ptr = owned.as_ptr(); - let len = owned.len(); - std::mem::forget(owned); - unsafe { std::slice::from_raw_parts(ptr, len) } - } - }; - + for &tilt_scale in tilt_scales { + let total_mae: f64 = samples.par_iter().map(|s| { + // Use generate_bevel_tuned to get highlight/shadow buffers let style_enum = match s.style.as_str() { "emboss" => hcie_fx::types::BevelStyle::Emboss, "outer" => hcie_fx::types::BevelStyle::OuterBevel, @@ -186,43 +162,84 @@ fn main() { }; let (highlight, shadow) = hcie_fx::tuned::generate_bevel_tuned( - alpha, CANVAS, CANVAS, - s.depth, s.size, s.soften, 120.0, s.altitude, + &s.layer_pixels.iter().skip(3).step_by(4).copied().collect::>(), + s.w, s.h, + s.depth, s.size, s.soften, s.angle, s.altitude, &dir_enum, &hcie_fx::types::Technique::Smooth, &style_enum, - lighting_blur, hl_scale, sh_scale, 1.0, + lighting_blur, hl_scale, sh_scale, 1.0, tilt_scale, ); - let px = npix(); + let px = (s.w * s.h) as usize; let mut out = vec![0u8; px * 4]; - // Base shape: black fill for both Inner Bevel and Emboss. - // Photoshop Emboss applies highlights/shadows on top of the original fill. + // Base shape: fill with layer pixels (black fill) for i in 0..px { - if alpha[i] > 0 { - out[i * 4] = 0; - out[i * 4 + 1] = 0; - out[i * 4 + 2] = 0; - out[i * 4 + 3] = alpha[i]; + if s.layer_pixels[i * 4 + 3] > 0 { + out[i * 4] = s.layer_pixels[i * 4]; + out[i * 4 + 1] = s.layer_pixels[i * 4 + 1]; + out[i * 4 + 2] = s.layer_pixels[i * 4 + 2]; + out[i * 4 + 3] = s.layer_pixels[i * 4 + 3]; } } - // Composite highlight (screen) and shadow (multiply) + // For emboss/outerbevel: outside highlight/shadow go to empty canvas (behind) + let has_outer = matches!(style_enum, hcie_fx::types::BevelStyle::Emboss | hcie_fx::types::BevelStyle::OuterBevel); + + // Get opacity and blend mode from fx_effects + let (hl_op, sh_op, hl_bm, sh_bm) = { + let mut hl_op = 0.75f32; + let mut sh_op = 0.75f32; + let mut hl_bm = hcie_blend::BlendMode::Screen; + let mut sh_bm = hcie_blend::BlendMode::Multiply; + for e in &s.fx_effects { + if let hcie_fx::types::LayerEffect::BevelEmboss { + highlight_opacity, shadow_opacity, + highlight_blend, shadow_blend, .. + } = e { + hl_op = *highlight_opacity / 100.0; + sh_op = *shadow_opacity / 100.0; + hl_bm = *highlight_blend; + sh_bm = *shadow_blend; + } + } + (hl_op, sh_op, hl_bm, sh_bm) + }; + + // Composite highlight — inside on fill, outside on empty canvas for i in 0..px { let sa = highlight[i * 4 + 3]; if sa > 0 { - let dst = [out[i * 4], out[i * 4 + 1], out[i * 4 + 2], out[i * 4 + 3]]; - let src = [highlight[i * 4], highlight[i * 4 + 1], highlight[i * 4 + 2], sa]; - let blended = hcie_blend::blend_pixels(dst, src, hcie_blend::BlendMode::Screen, 1.0); - out[i * 4..i * 4 + 4].copy_from_slice(&blended); + let is_inner = s.layer_pixels[i * 4 + 3] > 0; + if !has_outer || is_inner { + let dst = [out[i * 4], out[i * 4 + 1], out[i * 4 + 2], out[i * 4 + 3]]; + let src = [highlight[i * 4], highlight[i * 4 + 1], highlight[i * 4 + 2], sa]; + let blended = hcie_blend::blend_pixels(dst, src, hl_bm, hl_op); + out[i * 4..i * 4 + 4].copy_from_slice(&blended); + } else { + // Outside: composite highlight on empty canvas + let dst = [0u8, 0, 0, 0]; + let src = [highlight[i * 4], highlight[i * 4 + 1], highlight[i * 4 + 2], sa]; + let blended = hcie_blend::blend_pixels(dst, src, hl_bm, hl_op); + out[i * 4..i * 4 + 4].copy_from_slice(&blended); + } } } + // Composite shadow for i in 0..px { let sa = shadow[i * 4 + 3]; if sa > 0 { - let dst = [out[i * 4], out[i * 4 + 1], out[i * 4 + 2], out[i * 4 + 3]]; - let src = [shadow[i * 4], shadow[i * 4 + 1], shadow[i * 4 + 2], sa]; - let blended = hcie_blend::blend_pixels(dst, src, hcie_blend::BlendMode::Multiply, 1.0); - out[i * 4..i * 4 + 4].copy_from_slice(&blended); + let is_inner = s.layer_pixels[i * 4 + 3] > 0; + if !has_outer || is_inner { + let dst = [out[i * 4], out[i * 4 + 1], out[i * 4 + 2], out[i * 4 + 3]]; + let src = [shadow[i * 4], shadow[i * 4 + 1], shadow[i * 4 + 2], sa]; + let blended = hcie_blend::blend_pixels(dst, src, sh_bm, sh_op); + out[i * 4..i * 4 + 4].copy_from_slice(&blended); + } else { + let dst = [out[i * 4], out[i * 4 + 1], out[i * 4 + 2], out[i * 4 + 3]]; + let src = [shadow[i * 4], shadow[i * 4 + 1], shadow[i * 4 + 2], sa]; + let blended = hcie_blend::blend_pixels(dst, src, sh_bm, sh_op); + out[i * 4..i * 4 + 4].copy_from_slice(&blended); + } } } @@ -236,46 +253,48 @@ fn main() { } } if content_pixels > 0 { - total_mae += diff_sum / content_pixels as f64 / 4.0; + diff_sum / content_pixels as f64 / 4.0 + } else { + 0.0 } - } + }).sum(); eval_count += 1; let avg_mae = total_mae / samples.len() as f64; if avg_mae < best_avg_mae { best_avg_mae = avg_mae; - best_params = (lighting_blur, hl_scale, sh_scale); - println!("*** [{}/{}] NEW BEST avg MAE = {:.4} (lb={}, hl={:.2}, sh={:.2})", - eval_count, total_combos, best_avg_mae, lighting_blur, hl_scale, sh_scale); + best_params = (lighting_blur, hl_scale, sh_scale, tilt_scale); + println!("*** [{}/{}] NEW BEST avg MAE = {:.4} (lb={}, hl={:.3}, sh={:.2}, tilt={:.2})", + eval_count, total_combos, best_avg_mae, lighting_blur, hl_scale, sh_scale, tilt_scale); std::io::stdout().flush().ok(); - } else if eval_count % 50 == 0 { + } else if eval_count % 100 == 0 { println!(" [{}/{}] avg={:.4} best={:.4}", eval_count, total_combos, avg_mae, best_avg_mae); std::io::stdout().flush().ok(); } } } } + } - let (lb, hl, sh) = best_params; + let (lb, hl, sh, ts) = best_params; println!("\n=== RESULT ==="); println!("Best avg MAE = {:.4}", best_avg_mae); println!(" lighting_blur = {}", lb); - println!(" hl_scale = {:.2}", hl); + println!(" hl_scale = {:.3}", hl); println!(" sh_scale = {:.2}", sh); + println!(" tilt_scale = {:.2}", ts); if let Ok(mut file) = std::fs::OpenOptions::new() .create(true).write(true).truncate(true).open(RESULTS_FILE) { let _ = writeln!(file, - "lighting_blur={} hl_scale={:.2} sh_scale={:.2}\n\ + "lighting_blur={} hl_scale={:.3} sh_scale={:.2} tilt_scale={:.2}\n\ avg_mae={:.4} samples={} combos={}", - lb, hl, sh, best_avg_mae, samples.len(), total_combos); + lb, hl, sh, ts, best_avg_mae, samples.len(), total_combos); } } -fn npix() -> usize { (CANVAS * CANVAS) as usize } - fn parse_filename(name: &str) -> Option<(String, String, f32, f32, f32, String, f32, String)> { // be_{shape}_{style}_d{depth}_sz{size}_sf{soften}_{dir}_alt{altitude}_{technique} let parts: Vec<&str> = name.split('_').collect(); @@ -289,4 +308,4 @@ fn parse_filename(name: &str) -> Option<(String, String, f32, f32, f32, String, let altitude = parts[7].strip_prefix("alt")?.parse::().ok()?; let technique = parts[8].to_string(); Some((shape, style, depth, size, soften, direction, altitude, technique)) -} +} \ No newline at end of file diff --git a/hcie-io/examples/tune_mae_emboss.rs b/hcie-io/examples/tune_mae_emboss.rs index d8e1c9f..14f9293 100644 --- a/hcie-io/examples/tune_mae_emboss.rs +++ b/hcie-io/examples/tune_mae_emboss.rs @@ -2,8 +2,8 @@ use std::io::Write; const SHADOW_RS: &str = "hcie-fx/src/emboss.rs"; -const PSD_BASE: &str = "_images/_psd_stil_test/sultan_test/sultan"; -const RESULTS_FILE: &str = "/mnt/extra/00_PROJECTS/hcie-rust-v4/emboss_results.txt"; +const PSD_BASE: &str = "_images/_psd_stil_test/test_2"; +const RESULTS_FILE: &str = "./emboss_results.txt"; fn main() { let original = std::fs::read_to_string(SHADOW_RS).expect("read shadow.rs"); @@ -16,15 +16,15 @@ fn main() { let before = &original[..zone_start]; let after = &original[zone_end + end_tag.len()..]; - let ref_img = image::open(&format!("{}/Layer 2.png", PSD_BASE)) - .expect("open Layer 2.png") + let ref_img = image::open(&format!("{}/Emboss.png", PSD_BASE)) + .expect("open Emboss.png") .to_rgba8(); let (w, h) = (ref_img.width(), ref_img.height()); let px = (w * h) as usize; let ref_pixels = ref_img.into_raw(); - let layers = hcie_psd::import_psd(std::path::Path::new(&format!("{}/Layer 2.psd", PSD_BASE))) - .expect("import Layer 2.psd"); + let layers = hcie_psd::import_psd(std::path::Path::new(&format!("{}/Emboss.psd", PSD_BASE))) + .expect("import Emboss.psd"); let layer = &layers[0]; let alpha: Vec = layer.pixels.iter().skip(3).step_by(4).copied().collect(); diff --git a/hcie-io/examples/verify_asl.rs b/hcie-io/examples/verify_asl.rs index 7dc7ef7..8c55d38 100644 --- a/hcie-io/examples/verify_asl.rs +++ b/hcie-io/examples/verify_asl.rs @@ -1,7 +1,7 @@ use std::io::Read; fn main() { - let kra_path = "/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/kra_style_test/sultan.kra"; + let kra_path = "/mnt/extra/00_PROJECTS/hcie-rust-_images/kra_style_test/sultan.kra"; println!("Loading KRA from: {}", kra_path); let file = std::fs::File::open(kra_path).unwrap(); let mut archive = zip::ZipArchive::new(file).unwrap(); diff --git a/hcie-io/src/test_psd_composite.rs b/hcie-io/src/test_psd_composite.rs index 5098172..d07a1b5 100644 --- a/hcie-io/src/test_psd_composite.rs +++ b/hcie-io/src/test_psd_composite.rs @@ -1,7 +1,7 @@ #![allow(dead_code, unused_imports, unused_variables, unused_macros, unreachable_patterns, unused_assignments, clippy::cfg)] #[test] fn test_psd_import_sizes_and_offsets() { - let path = std::path::Path::new("/home/hc/Pictures/_test_images/example3/Example3-mini.psd"); + let path = std::path::Path::new("_images/_test_images/example3/Example3-mini.psd"); if !path.exists() { return; } let layers = hcie_psd::import_psd(path).unwrap(); @@ -41,8 +41,8 @@ fn map_to_composite_layers(layers: &[hcie_protocol::Layer]) -> Vec f64 { } #[test] fn test_check_soft_orange_pixels() { - let psd_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/base_test_generated_2.psd"); + let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); let layers = hcie_psd::import_psd(psd_path).unwrap(); let orange = &layers[5]; // Soft Orange Shape @@ -514,7 +514,7 @@ fn test_check_soft_orange_pixels() { } #[test] fn test_find_orange_pixels() { - let psd_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/base_test_generated_2.psd"); + let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); let layers = hcie_psd::import_psd(psd_path).unwrap(); let orange = &layers[5]; // Soft Orange Shape @@ -553,7 +553,7 @@ fn test_find_orange_pixels() { } #[test] fn test_layer_bounds() { - let psd_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/base_test_generated_2.psd"); + let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); let layers = hcie_psd::import_psd(psd_path).unwrap(); for (i, layer) in layers.iter().enumerate() { @@ -582,7 +582,7 @@ fn test_layer_bounds() { } #[test] fn test_check_layer_offsets() { - let psd_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/base_test_generated_2.psd"); + let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); let layers = hcie_psd::import_psd(psd_path).unwrap(); println!("Layer positions and sizes:"); @@ -619,7 +619,7 @@ fn test_check_layer_offsets() { } #[test] fn test_save_layer_pixels() { - let psd_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/base_test_generated_2.psd"); + let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); let layers = hcie_psd::import_psd(psd_path).unwrap(); for (i, layer) in layers.iter().enumerate() { @@ -633,7 +633,7 @@ fn test_save_layer_pixels() { } #[test] fn test_pixel_layer_contributions() { - let psd_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/base_test_generated_2.psd"); + let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); let layers = hcie_psd::import_psd(psd_path).unwrap(); let test_pixels = [(300, 200), (400, 200), (200, 300), (500, 200), (500, 500)]; @@ -656,7 +656,7 @@ fn test_pixel_layer_contributions() { #[test] fn test_load_test_2_psd() { - let test_dir = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/test_2"); + let test_dir = std::path::Path::new("_images/_psd_stil_test/test_2"); if !test_dir.exists() { return; } let entries: Vec<_> = std::fs::read_dir(test_dir).unwrap() @@ -694,8 +694,8 @@ fn test_load_test_2_psd() { #[test] fn test_per_layer_effects_vs_photoshop_export() { - let psd_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/base_test_generated_2.psd"); - let ref_dir = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/base_test_generated_2"); + let psd_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd"); + let ref_dir = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2"); let layers = hcie_psd::import_psd(psd_path).unwrap(); let layer_names = [ "Background Grid", "Teal Circle", "Pink Rectangle", @@ -793,7 +793,7 @@ fn test_per_layer_effects_vs_photoshop_export() { #[test] fn test_test2_effects_mae() { - let test_dir = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/test_2"); + let test_dir = std::path::Path::new("_images/_psd_stil_test/test_2"); if !test_dir.exists() { return; } let entries: Vec<_> = std::fs::read_dir(test_dir).unwrap() @@ -859,9 +859,9 @@ fn test_test2_effects_mae() { #[test] fn test_sultan_effects_mae() { - let psd_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/sultan.psd"); - let ref_png = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/sultan.png"); - let layer_dir = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/sultan"); + let psd_path = std::path::Path::new("_images/_psd_stil_test/sultan.psd"); + let ref_png = std::path::Path::new("_images/_psd_stil_test/sultan.png"); + let layer_dir = std::path::Path::new("_images/_psd_stil_test/sultan"); if !psd_path.exists() || !ref_png.exists() { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); @@ -1055,8 +1055,8 @@ fn test_sultan_effects_mae() { #[test] fn test_emboss_optimize() { - let psd_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/emboss.psd"); - let ref_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/emboss.png"); + let psd_path = std::path::Path::new("_images/_psd_stil_test/emboss.psd"); + let ref_path = std::path::Path::new("_images/_psd_stil_test/emboss.png"); if !psd_path.exists() || !ref_path.exists() { return; } let layers = hcie_psd::import_psd(psd_path).unwrap(); @@ -1245,9 +1245,9 @@ fn test_emboss_optimize() { #[test] fn test_effects_survive_ffi_boundary() { let paths = [ - "/home/hc/Documents/00_PROJECTS/hcie-rust-v4/hcie-io/test_resources/base_test_generated_2.psd", - "/home/hc/Pictures/_psd_stil_test/emboss.psd", - "/home/hc/Pictures/_test_images/example3/Example3-mini.psd", + "./test_resources/base_test_generated_2.psd", + "_images/_psd_stil_test/emboss.psd", + "_images/_test_images/example3/Example3-mini.psd", ]; let mut found = false; for p in &paths { @@ -1275,9 +1275,9 @@ fn test_effects_survive_ffi_boundary() { #[test] fn test_ffi_bincode_roundtrip() { let paths = [ - "/home/hc/Documents/00_PROJECTS/hcie-rust-v4/hcie-io/test_resources/base_test_generated_2.psd", - "/home/hc/Pictures/_psd_stil_test/emboss.psd", - "/home/hc/Pictures/_test_images/example3/Example3-mini.psd", + "./test_resources/base_test_generated_2.psd", + "_images/_psd_stil_test/emboss.psd", + "_images/_test_images/example3/Example3-mini.psd", ]; let mut found = false; for p in &paths {