bevel and emboss geliştirme
This commit is contained in:
Generated
+1
-149
@@ -455,19 +455,6 @@ version = "1.5.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
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]]
|
[[package]]
|
||||||
name = "av-scenechange"
|
name = "av-scenechange"
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
@@ -592,15 +579,6 @@ dependencies = [
|
|||||||
"serde_core",
|
"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]]
|
[[package]]
|
||||||
name = "bitstream-io"
|
name = "bitstream-io"
|
||||||
version = "4.10.0"
|
version = "4.10.0"
|
||||||
@@ -686,12 +664,6 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
|
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "byte-slice-cast"
|
|
||||||
version = "1.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.25.0"
|
version = "1.25.0"
|
||||||
@@ -793,16 +765,6 @@ dependencies = [
|
|||||||
"shlex",
|
"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]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -1148,28 +1110,6 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
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]]
|
[[package]]
|
||||||
name = "dconf_rs"
|
name = "dconf_rs"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -1873,15 +1813,6 @@ dependencies = [
|
|||||||
"zune-inflate",
|
"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]]
|
[[package]]
|
||||||
name = "fast-srgb8"
|
name = "fast-srgb8"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -2515,15 +2446,6 @@ dependencies = [
|
|||||||
"ahash 0.7.8",
|
"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]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@@ -2826,6 +2748,7 @@ dependencies = [
|
|||||||
"proptest",
|
"proptest",
|
||||||
"psd",
|
"psd",
|
||||||
"rand 0.8.6",
|
"rand 0.8.6",
|
||||||
|
"rayon",
|
||||||
"resvg 0.43.0",
|
"resvg 0.43.0",
|
||||||
"rfd",
|
"rfd",
|
||||||
"rstest",
|
"rstest",
|
||||||
@@ -3453,12 +3376,10 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder-lite",
|
"byteorder-lite",
|
||||||
"color_quant",
|
"color_quant",
|
||||||
"dav1d",
|
|
||||||
"exr",
|
"exr",
|
||||||
"gif 0.14.2",
|
"gif 0.14.2",
|
||||||
"image-webp 0.2.4",
|
"image-webp 0.2.4",
|
||||||
"moxcms",
|
"moxcms",
|
||||||
"mp4parse",
|
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"png 0.18.1",
|
"png 0.18.1",
|
||||||
"qoi",
|
"qoi",
|
||||||
@@ -4039,20 +3960,6 @@ dependencies = [
|
|||||||
"pxfm",
|
"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]]
|
[[package]]
|
||||||
name = "mutate_once"
|
name = "mutate_once"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -5956,15 +5863,6 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"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]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -6354,31 +6252,12 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "tap"
|
name = "tap"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "target-lexicon"
|
|
||||||
version = "0.13.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.27.0"
|
version = "3.27.0"
|
||||||
@@ -6591,21 +6470,6 @@ dependencies = [
|
|||||||
"tokio",
|
"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]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "1.1.1+spec-1.1.0"
|
||||||
@@ -6636,12 +6500,6 @@ dependencies = [
|
|||||||
"winnow",
|
"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]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -7047,12 +6905,6 @@ dependencies = [
|
|||||||
"vello_common",
|
"vello_common",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "version-compare"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
lighting_blur=100 hl_scale=0.10 sh_scale=0.10
|
||||||
|
avg_mae=29.5671 samples=864 combos=343
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
=== Emboss Tuned ===
|
||||||
|
Params: blur=5, hl_scale=0.05, sh_scale=2.50
|
||||||
|
MAE: 14.583
|
||||||
@@ -222,6 +222,7 @@ impl Engine {
|
|||||||
|
|
||||||
if !layer.effects.is_empty() || !layer.styles.is_empty() {
|
if !layer.effects.is_empty() || !layer.styles.is_empty() {
|
||||||
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
|
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
|
||||||
|
self.document.composite_dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take the pooled before buffer (no allocation)
|
// Take the pooled before buffer (no allocation)
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ pub fn apply_layer_effects(
|
|||||||
|
|
||||||
let alpha = extract_alpha(pixels, px_count);
|
let alpha = extract_alpha(pixels, px_count);
|
||||||
|
|
||||||
|
// Storage for Emboss/OuterBevel outside portions (applied to `behind` later)
|
||||||
|
let mut bevel_hl_outside: Option<Vec<u8>> = None;
|
||||||
|
let mut bevel_sh_outside: Option<Vec<u8>> = 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)
|
// 1. Prepare inside content (original pixels + inside effects)
|
||||||
let mut inside = vec![0u8; px_count * 4];
|
let mut inside = vec![0u8; px_count * 4];
|
||||||
inside.copy_from_slice(pixels);
|
inside.copy_from_slice(pixels);
|
||||||
@@ -67,9 +75,39 @@ pub fn apply_layer_effects(
|
|||||||
*direction, *technique, *style,
|
*direction, *technique, *style,
|
||||||
*highlight_color, *shadow_color,
|
*highlight_color, *shadow_color,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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, &highlight, *highlight_blend, *highlight_opacity, px_count);
|
||||||
composite_effect(&mut inside, &shadow, *shadow_blend, *shadow_opacity, px_count);
|
composite_effect(&mut inside, &shadow, *shadow_blend, *shadow_opacity, px_count);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// b. Satin
|
// b. Satin
|
||||||
if let Some(LayerEffect::Satin {
|
if let Some(LayerEffect::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
|
// 3. Blend inside ON TOP of behind using Normal blend
|
||||||
for i in 0..px_count {
|
for i in 0..px_count {
|
||||||
let b = [behind[i * 4], behind[i * 4 + 1], behind[i * 4 + 2], behind[i * 4 + 3]];
|
let b = [behind[i * 4], behind[i * 4 + 1], behind[i * 4 + 2], behind[i * 4 + 3]];
|
||||||
|
|||||||
@@ -68,8 +68,6 @@ pub(crate) fn generate_shadow(
|
|||||||
|
|
||||||
// MAE-optimized against Krita ground truth (768 samples, 350 combos, 800x800)
|
// MAE-optimized against Krita ground truth (768 samples, 350 combos, 800x800)
|
||||||
// Best: blur_factor=5.0, passes=5, spread_scale=1.00
|
// 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;
|
let radius = size.max(0.0).round() as i32;
|
||||||
if radius > 0 {
|
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();
|
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 {
|
for i in 0..px {
|
||||||
let a = offset_alpha[i];
|
let a = offset_alpha[i];
|
||||||
if a > 0 {
|
if a > 0 {
|
||||||
|
|||||||
+195
-30
@@ -1,5 +1,44 @@
|
|||||||
use crate::types;
|
use crate::types;
|
||||||
use crate::helpers::box_blur_f32;
|
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(
|
pub fn generate_bevel_emboss(
|
||||||
alpha: &[u8],
|
alpha: &[u8],
|
||||||
w: u32,
|
w: u32,
|
||||||
@@ -27,6 +66,16 @@ pub fn generate_bevel_emboss(
|
|||||||
let dist_inside_sq = edt_inside(alpha, iw, ih);
|
let dist_inside_sq = edt_inside(alpha, iw, ih);
|
||||||
let dist_outside_sq = edt_outside(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
|
// Compute shape center for Emboss tilt
|
||||||
let (cx, cy) = {
|
let (cx, cy) = {
|
||||||
let mut min_x = iw;
|
let mut min_x = iw;
|
||||||
@@ -76,15 +125,17 @@ pub fn generate_bevel_emboss(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
types::BevelStyle::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_x = (i % iw) as f32;
|
||||||
let px_y = (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 {
|
if alpha[i] > 0 {
|
||||||
let edge_factor = (d_in / radius).min(1.0);
|
(d_in / radius).min(1.0) * radius + tilt
|
||||||
radius * edge_factor + tilt
|
|
||||||
} else {
|
} else {
|
||||||
let t = (d_out / radius).min(1.0);
|
((radius - d_out).max(0.0) / radius) * radius + tilt
|
||||||
(radius * (1.0 - t) * 0.5 + tilt).max(0.0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -155,13 +206,7 @@ pub fn generate_bevel_emboss(
|
|||||||
|
|
||||||
let ndl = nx * light_x + ny * light_y + nz * light_z;
|
let ndl = nx * light_x + ny * light_y + nz * light_z;
|
||||||
|
|
||||||
let factor = if matches!(style, types::BevelStyle::Emboss) && is_inner {
|
let factor = ndl;
|
||||||
let height_factor = height[i] / radius;
|
|
||||||
let blended = ndl * 0.4 + height_factor * 0.6;
|
|
||||||
blended * depth_scale
|
|
||||||
} else {
|
|
||||||
ndl
|
|
||||||
};
|
|
||||||
|
|
||||||
lighting[i] = match direction {
|
lighting[i] = match direction {
|
||||||
types::Direction::Up => factor,
|
types::Direction::Up => factor,
|
||||||
@@ -171,46 +216,59 @@ pub fn generate_bevel_emboss(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Smooth the lighting to reduce wrinkles at sharp corners.
|
// Smooth the lighting to reduce wrinkles at sharp corners.
|
||||||
// MAE-optimized against Krita ground truth (320 samples, 256×256).
|
// Emboss uses tilt for 3D effect and needs sharp lighting (no blur).
|
||||||
// Best: lighting_blur=5, hl_scale=0.30, sh_scale=2.00, avg_mae=17.18
|
// Inner/Outer bevel need blur for smooth corners.
|
||||||
|
// MAE-optimized against Photoshop ground truth (288 emboss samples, 256×256).
|
||||||
// BEGIN_TUNING_ZONE
|
// 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);
|
lighting = box_blur_f32(&lighting, w, h, lighting_blur);
|
||||||
|
|
||||||
for i in 0..px {
|
for i in 0..px {
|
||||||
let is_inner = alpha[i] > 0;
|
let is_inner = alpha[i] > 0;
|
||||||
let show = match style {
|
let show = match style {
|
||||||
types::BevelStyle::InnerBevel => is_inner,
|
crate::types::BevelStyle::InnerBevel => is_inner,
|
||||||
types::BevelStyle::OuterBevel => !is_inner,
|
crate::types::BevelStyle::OuterBevel => !is_inner,
|
||||||
types::BevelStyle::Emboss => true,
|
crate::types::BevelStyle::Emboss => true,
|
||||||
_ => is_inner,
|
_ => is_inner,
|
||||||
};
|
};
|
||||||
if !show { continue; }
|
if !show { continue; }
|
||||||
let dist_abs = if is_inner { dist_inside_sq[i].sqrt() } else { dist_outside_sq[i].sqrt() };
|
let dist_abs = if is_inner { dist_inside_sq[i].sqrt() } else { dist_outside_sq[i].sqrt() };
|
||||||
if !is_inner && dist_abs > radius { continue; }
|
if !is_inner && dist_abs > radius { continue; }
|
||||||
let technique_show = match technique {
|
let technique_show = match technique {
|
||||||
types::Technique::Smooth => true,
|
crate::types::Technique::Smooth => true,
|
||||||
_ => dist_abs <= radius,
|
_ => dist_abs <= radius,
|
||||||
};
|
};
|
||||||
if !technique_show { continue; }
|
if !technique_show { continue; }
|
||||||
|
|
||||||
let ndl = lighting[i];
|
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 is_flat {
|
||||||
if style == types::BevelStyle::Emboss && depth > 100.0 {
|
|
||||||
shadow_buf[i * 4 + 3] = alpha[i];
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MAE-optimized against Krita ground truth (320 samples, 256×256).
|
let (hl_scale, sh_scale) = if matches!(style, crate::types::BevelStyle::Emboss) {
|
||||||
// Best: lighting_blur=5, hl_scale=0.30, sh_scale=2.00, avg_mae=17.18
|
(0.05, 16.00)
|
||||||
let hl_pred = (ndl * 0.30).clamp(0.0, 1.0);
|
} else {
|
||||||
let sh_pred = (-ndl * 2.00).clamp(0.0, 1.0);
|
(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;
|
// For outside pixels (alpha[i]==0), use the nearest opaque pixel's
|
||||||
let sh_a = (alpha[i] as f32 * sh_pred).round() as u8;
|
// 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 {
|
if hl_a > 0 {
|
||||||
highlight_buf[i * 4] = highlight_color[0];
|
highlight_buf[i * 4] = highlight_color[0];
|
||||||
@@ -267,6 +325,33 @@ fn edt_outside(alpha: &[u8], w: usize, h: usize) -> Vec<f32> {
|
|||||||
dt
|
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<usize>` where each element is the pixel index of the
|
||||||
|
/// nearest opaque source pixel.
|
||||||
|
fn edt_outside_src(alpha: &[u8], w: usize, h: usize) -> Vec<usize> {
|
||||||
|
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.
|
/// 2D Euclidean Distance Transform using the Felzenszwalb & Huttenlocher algorithm.
|
||||||
fn edt_2d(dt: &mut [f32], w: usize, h: usize) {
|
fn edt_2d(dt: &mut [f32], w: usize, h: usize) {
|
||||||
let mut col = vec![0.0f32; h];
|
let mut col = vec![0.0f32; h];
|
||||||
@@ -337,3 +422,83 @@ fn edt_1d(f: &mut [f32]) {
|
|||||||
|
|
||||||
f.copy_from_slice(&d);
|
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);
|
||||||
|
}
|
||||||
@@ -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;
|
let radius = size.max(0.0).round() as i32;
|
||||||
if radius > 0 {
|
if radius > 0 {
|
||||||
let gaussian_r = (radius as f32 / 10.0).round().max(1.0) as i32;
|
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();
|
offset_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply_spread(&mut offset_alpha, choke);
|
||||||
|
|
||||||
for i in 0..px {
|
for i in 0..px {
|
||||||
let a = ((offset_alpha[i] as f32 * 0.50) as u32 * alpha[i] as u32 / 255) as u8;
|
let a = ((offset_alpha[i] as f32 * 0.50) as u32 * alpha[i] as u32 / 255) as u8;
|
||||||
if a > 0 {
|
if a > 0 {
|
||||||
|
|||||||
+35
-17
@@ -19,6 +19,7 @@ pub fn generate_bevel_tuned(
|
|||||||
hl_scale: f32,
|
hl_scale: f32,
|
||||||
sh_scale: f32,
|
sh_scale: f32,
|
||||||
profile_exp: f32,
|
profile_exp: f32,
|
||||||
|
tilt_scale: f32,
|
||||||
) -> (Vec<u8>, Vec<u8>) {
|
) -> (Vec<u8>, Vec<u8>) {
|
||||||
let hl_color = [255u8; 4];
|
let hl_color = [255u8; 4];
|
||||||
let sh_color = [0u8; 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_inside_sq = edt_inside(alpha, iw, ih);
|
||||||
let dist_outside_sq = edt_outside(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
|
// Compute shape center for Emboss tilt
|
||||||
let (cx, cy) = {
|
let (cx, cy) = {
|
||||||
let mut min_x = iw;
|
let mut min_x = iw;
|
||||||
@@ -81,15 +90,22 @@ pub fn generate_bevel_tuned(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
types::BevelStyle::Emboss => {
|
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_x = (i % iw) as f32;
|
||||||
let px_y = (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 {
|
if alpha[i] > 0 {
|
||||||
let t = (d_in / radius).min(1.0);
|
let t = (d_in / radius).min(1.0);
|
||||||
radius * t.powf(profile_exp) + tilt
|
t.powf(profile_exp) * radius + tilt
|
||||||
} else {
|
} else {
|
||||||
let t = (d_out / radius).min(1.0);
|
let t = ((radius - d_out).max(0.0) / radius).min(1.0);
|
||||||
(radius * (1.0 - t).powf(profile_exp) * 0.5 + tilt).max(0.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 ndl = nx * light_x + ny * light_y + nz * light_z;
|
||||||
|
|
||||||
let factor = if matches!(style, types::BevelStyle::Emboss) && is_inner {
|
let factor = ndl;
|
||||||
let height_factor = height[i] / radius;
|
|
||||||
let blended = ndl * 0.4 + height_factor * 0.6;
|
|
||||||
blended * depth_scale
|
|
||||||
} else {
|
|
||||||
ndl
|
|
||||||
};
|
|
||||||
|
|
||||||
lighting[i] = match direction {
|
lighting[i] = match direction {
|
||||||
types::Direction::Up => factor,
|
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() };
|
let dist_abs = if is_inner { dist_inside_sq[i].sqrt() } else { dist_outside_sq[i].sqrt() };
|
||||||
if !is_inner && dist_abs > radius { continue; }
|
if !is_inner && dist_abs > radius { continue; }
|
||||||
let ndl = lighting[i];
|
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 is_flat {
|
||||||
if *style == types::BevelStyle::Emboss && depth > 100.0 {
|
|
||||||
shadow_buf[i * 4 + 3] = alpha[i];
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hl_pred = (ndl * hl_scale).clamp(0.0, 1.0);
|
let hl_pred = (ndl * hl_scale).clamp(0.0, 1.0);
|
||||||
let sh_pred = (-ndl * sh_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;
|
// For outside pixels (alpha[i]==0), use the nearest opaque pixel's
|
||||||
let sh_a = (alpha[i] as f32 * sh_pred).round() as u8;
|
// 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 {
|
if hl_a > 0 {
|
||||||
highlight_buf[i * 4] = hl_color[0];
|
highlight_buf[i * 4] = hl_color[0];
|
||||||
|
|||||||
@@ -129,6 +129,9 @@ pub struct HcieIcedApp {
|
|||||||
pub style_color_picker_idx: usize,
|
pub style_color_picker_idx: usize,
|
||||||
/// HSL color state for the style color picker (hue 0..360, sat 0..1, light 0..1).
|
/// HSL color state for the style color picker (hue 0..360, sat 0..1, light 0..1).
|
||||||
pub style_color_hsl: (f32, f32, f32),
|
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).
|
/// Currently active color picker tab (0=Wheel, 1=Sliders, 2=Grid).
|
||||||
pub color_tab: usize,
|
pub color_tab: usize,
|
||||||
/// Layer Style dialog drag offset from center (x, y) in pixels.
|
/// Layer Style dialog drag offset from center (x, y) in pixels.
|
||||||
@@ -295,7 +298,12 @@ pub enum Message {
|
|||||||
LayerStyleUpdateParam(usize, String, f32),
|
LayerStyleUpdateParam(usize, String, f32),
|
||||||
LayerStyleUpdateBlendMode(usize, String),
|
LayerStyleUpdateBlendMode(usize, String),
|
||||||
LayerStyleUpdateColor(usize, [u8; 4]),
|
LayerStyleUpdateColor(usize, [u8; 4]),
|
||||||
|
LayerStyleUpdateString(usize, String, String),
|
||||||
|
LayerStyleUpdateShadowColor(usize, [u8; 4]),
|
||||||
|
LayerStyleUpdateShadowBlend(usize, String),
|
||||||
|
LayerStyleUpdateShadowOpacity(usize, f32),
|
||||||
ShowStyleColorPicker(usize),
|
ShowStyleColorPicker(usize),
|
||||||
|
ShowStyleShadowColorPicker(usize),
|
||||||
HideStyleColorPicker,
|
HideStyleColorPicker,
|
||||||
OpenLayerStyleDialog,
|
OpenLayerStyleDialog,
|
||||||
LayerStyleDialogDragStart(f32, f32),
|
LayerStyleDialogDragStart(f32, f32),
|
||||||
@@ -532,6 +540,7 @@ impl HcieIcedApp {
|
|||||||
show_style_color_picker: false,
|
show_style_color_picker: false,
|
||||||
style_color_picker_idx: 0,
|
style_color_picker_idx: 0,
|
||||||
style_color_hsl: (0.0, 0.0, 0.0),
|
style_color_hsl: (0.0, 0.0, 0.0),
|
||||||
|
style_color_is_shadow: false,
|
||||||
color_tab: 0,
|
color_tab: 0,
|
||||||
layer_style_offset: (0.0, 0.0),
|
layer_style_offset: (0.0, 0.0),
|
||||||
layer_style_dragging: false,
|
layer_style_dragging: false,
|
||||||
@@ -1238,6 +1247,12 @@ impl HcieIcedApp {
|
|||||||
*highlight_opacity = value;
|
*highlight_opacity = value;
|
||||||
*shadow_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 { opacity, .. }, "opacity") => *opacity = value,
|
||||||
(LayerStyle::Satin { angle, .. }, "angle") => *angle = value,
|
(LayerStyle::Satin { angle, .. }, "angle") => *angle = value,
|
||||||
(LayerStyle::Satin { distance, .. }, "distance") => *distance = value,
|
(LayerStyle::Satin { distance, .. }, "distance") => *distance = value,
|
||||||
@@ -1292,6 +1307,7 @@ impl HcieIcedApp {
|
|||||||
| LayerStyle::GradientOverlay { blend_mode, .. }
|
| LayerStyle::GradientOverlay { blend_mode, .. }
|
||||||
| LayerStyle::PatternOverlay { blend_mode, .. }
|
| LayerStyle::PatternOverlay { blend_mode, .. }
|
||||||
| LayerStyle::Stroke { blend_mode, .. } => *blend_mode = 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);
|
self.documents[self.active_doc].engine.update_layer_style(layer_id, new_style);
|
||||||
@@ -1333,7 +1349,13 @@ impl HcieIcedApp {
|
|||||||
| LayerStyle::ColorOverlay { color, .. }
|
| LayerStyle::ColorOverlay { color, .. }
|
||||||
| LayerStyle::Satin { color, .. }
|
| LayerStyle::Satin { color, .. }
|
||||||
| LayerStyle::Stroke { color, .. } => *color = new_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);
|
self.documents[self.active_doc].engine.update_layer_style(layer_id, updated_style);
|
||||||
@@ -1353,9 +1375,151 @@ impl HcieIcedApp {
|
|||||||
self.refresh_composite_if_needed();
|
self.refresh_composite_if_needed();
|
||||||
return Task::perform(async {}, |_| Message::CompositeRefresh);
|
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) => {
|
Message::ShowStyleColorPicker(idx) => {
|
||||||
self.show_style_color_picker = true;
|
self.show_style_color_picker = true;
|
||||||
self.style_color_picker_idx = idx;
|
self.style_color_picker_idx = idx;
|
||||||
|
self.style_color_is_shadow = false;
|
||||||
// Initialize HSL from current color
|
// Initialize HSL from current color
|
||||||
let disc_names = ["DropShadow", "InnerShadow", "OuterGlow", "InnerGlow",
|
let disc_names = ["DropShadow", "InnerShadow", "OuterGlow", "InnerGlow",
|
||||||
"BevelEmboss", "Satin", "ColorOverlay", "GradientOverlay", "PatternOverlay", "Stroke"];
|
"BevelEmboss", "Satin", "ColorOverlay", "GradientOverlay", "PatternOverlay", "Stroke"];
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ pub enum PaneType {
|
|||||||
Script,
|
Script,
|
||||||
/// AI Chat panel.
|
/// AI Chat panel.
|
||||||
AiChat,
|
AiChat,
|
||||||
/// Layer Styles panel.
|
|
||||||
LayerStyles,
|
|
||||||
/// Layer Details panel.
|
/// Layer Details panel.
|
||||||
LayerDetails,
|
LayerDetails,
|
||||||
/// Geometry panel (vector shapes).
|
/// Geometry panel (vector shapes).
|
||||||
@@ -59,7 +57,6 @@ impl PaneType {
|
|||||||
PaneType::Properties => "Properties",
|
PaneType::Properties => "Properties",
|
||||||
PaneType::Script => "Script",
|
PaneType::Script => "Script",
|
||||||
PaneType::AiChat => "AI Assistant",
|
PaneType::AiChat => "AI Assistant",
|
||||||
PaneType::LayerStyles => "Layer Styles",
|
|
||||||
PaneType::LayerDetails => "Layer Details",
|
PaneType::LayerDetails => "Layer Details",
|
||||||
PaneType::Geometry => "Geometry",
|
PaneType::Geometry => "Geometry",
|
||||||
}
|
}
|
||||||
@@ -225,7 +222,7 @@ impl DockState {
|
|||||||
|
|
||||||
if let Some(target) = target_pane {
|
if let Some(target) = target_pane {
|
||||||
let axis = match pane_type {
|
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::Horizontal
|
||||||
}
|
}
|
||||||
_ => iced::widget::pane_grid::Axis::Vertical,
|
_ => iced::widget::pane_grid::Axis::Vertical,
|
||||||
|
|||||||
@@ -88,20 +88,6 @@ pub fn dock_view<'a>(
|
|||||||
PaneType::AiChat => {
|
PaneType::AiChat => {
|
||||||
crate::panels::ai_chat_panel::view("", &[], colors)
|
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 => {
|
PaneType::LayerDetails => {
|
||||||
let doc = &app.documents[app.active_doc];
|
let doc = &app.documents[app.active_doc];
|
||||||
crate::panels::layer_details::view(&doc.cached_layers, doc.engine.active_layer_id(), colors)
|
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::Properties => Some("icons/panel-settings.svg"),
|
||||||
PaneType::Script => Some("icons/panel-pen.svg"),
|
PaneType::Script => Some("icons/panel-pen.svg"),
|
||||||
PaneType::AiChat => Some("icons/panel-ai.svg"),
|
PaneType::AiChat => Some("icons/panel-ai.svg"),
|
||||||
PaneType::LayerStyles => Some("icons/glow.svg"),
|
|
||||||
PaneType::LayerDetails => Some("icons/panel-layers.svg"),
|
PaneType::LayerDetails => Some("icons/panel-layers.svg"),
|
||||||
PaneType::Geometry => Some("icons/vector_rect.svg"),
|
PaneType::Geometry => Some("icons/vector_rect.svg"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,72 @@ const BLEND_MODES: &[&str] = &[
|
|||||||
"Subtract", "Divide", "Hue", "Saturation", "Color", "Luminosity",
|
"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.
|
/// Check if a LayerStyle matches a discriminant name.
|
||||||
fn style_matches(discriminant: &str, style: &LayerStyle) -> bool {
|
fn style_matches(discriminant: &str, style: &LayerStyle) -> bool {
|
||||||
matches!(
|
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));
|
params = params.push(slider_row("Size", size, 0.0..=250.0, move |v| Message::LayerStyleUpdateParam(selected, "size".to_string(), v), colors));
|
||||||
}
|
}
|
||||||
"BevelEmboss" => {
|
"BevelEmboss" => {
|
||||||
let (depth, size, angle, altitude, soften) = match style {
|
let (depth, size, angle, altitude, soften, style_str, technique_str, direction_str,
|
||||||
Some(LayerStyle::BevelEmboss { depth, size, angle, altitude, soften, .. }) =>
|
highlight_opacity, shadow_opacity, hl_color, sh_color, hl_blend, sh_blend) = match style {
|
||||||
(*depth, *size, *angle, *altitude, *soften),
|
Some(LayerStyle::BevelEmboss {
|
||||||
_ => (1.0, 5.0, 120.0, 30.0, 0.0),
|
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("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("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));
|
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 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 label_text = text(label).size(10).width(Length::Fixed(70.0));
|
||||||
|
|
||||||
|
let step = if *range.end() <= 1.0 { 0.01 } else { 1.0 };
|
||||||
|
|
||||||
row![
|
row![
|
||||||
label_text,
|
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,
|
value_label,
|
||||||
]
|
]
|
||||||
.spacing(3)
|
.spacing(3)
|
||||||
|
|||||||
+2
-1
@@ -4,7 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[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"
|
log = "0.4"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
@@ -34,3 +34,4 @@ zip = { version = "2.2", default-features = false, features = ["deflate"] }
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
rayon = "1.10"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
//! layer records with the original.
|
//! layer records with the original.
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let input_path = "/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/_test_images/example3/Example3-mini.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-v4/_images/_test_images/example3/Example3-mini-hcie-v2.psd";
|
let output_path = "/mnt/extra/00_PROJECTS/hcie-rust-_images/_test_images/example3/Example3-mini-hcie-v2.psd";
|
||||||
|
|
||||||
println!("Reading original PSD...");
|
println!("Reading original PSD...");
|
||||||
let orig_bytes = std::fs::read(input_path).unwrap();
|
let orig_bytes = std::fs::read(input_path).unwrap();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 -- <stem>
|
||||||
|
/// 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::<f32>().ok()?;
|
||||||
|
let size = parts[4].strip_prefix("sz")?.parse::<f32>().ok()?;
|
||||||
|
let soften = parts[5].strip_prefix("sf")?.parse::<f32>().ok()?;
|
||||||
|
let direction = parts[6].to_string();
|
||||||
|
let altitude = parts[7].strip_prefix("alt")?.parse::<f32>().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<hcie_fx::types::LayerEffect> = 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<String> = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
fn main() {
|
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 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();
|
let orig_layers = hcie_psd::import_psd(orig_path).unwrap();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
fn main() {
|
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 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();
|
let orig_psd = hcie_psd::Psd::from_bytes(&orig).unwrap();
|
||||||
|
|||||||
@@ -1,88 +1,38 @@
|
|||||||
#![allow(dead_code, unused_imports, unused_variables, unused_macros)]
|
#![allow(dead_code, unused_imports, unused_variables, unused_macros)]
|
||||||
/// Grid-search Bevel & Emboss MAE tuning against Photoshop ground-truth PNGs.
|
/// Grid-search Bevel & Emboss MAE tuning against Photoshop ground-truth PNGs.
|
||||||
///
|
///
|
||||||
/// Expects `_tmp/bevel_emboss_png_set/` with transparent PNGs exported
|
/// Expects `_tmp/psd_tunes/bevel_emboss_png_set/` with transparent PNGs exported
|
||||||
/// from `_tmp/bevel_emboss_psd_set/`.
|
/// 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;
|
use std::io::Write;
|
||||||
|
|
||||||
const GT_DIR: &str = "_tmp/bevel_emboss_png_set";
|
const GT_DIR: &str = "_tmp/psd_tunes/bevel_emboss_png_set";
|
||||||
const CANVAS: u32 = 256;
|
const RESULTS_FILE: &str = "bevel_emboss_grid_results_v4.txt";
|
||||||
const MARGIN: u32 = 64;
|
|
||||||
const RESULTS_FILE: &str = "bevel_emboss_grid_results_v3.txt";
|
|
||||||
|
|
||||||
struct Sample {
|
struct Sample {
|
||||||
name: String,
|
name: String,
|
||||||
shape: String,
|
|
||||||
style: String,
|
style: String,
|
||||||
depth: f32,
|
depth: f32,
|
||||||
size: f32,
|
size: f32,
|
||||||
soften: f32,
|
soften: f32,
|
||||||
direction: String,
|
direction: String,
|
||||||
altitude: f32,
|
altitude: f32,
|
||||||
|
angle: f32,
|
||||||
ref_pixels: Vec<u8>,
|
ref_pixels: Vec<u8>,
|
||||||
content_mask: Vec<bool>,
|
content_mask: Vec<bool>,
|
||||||
}
|
layer_pixels: Vec<u8>,
|
||||||
|
fx_effects: Vec<hcie_fx::types::LayerEffect>,
|
||||||
fn make_shape_alpha(shape: &str) -> Vec<u8> {
|
fill_opacity: f32,
|
||||||
let n = (CANVAS * CANVAS) as usize;
|
w: u32,
|
||||||
let mut a = vec![0u8; n];
|
h: u32,
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -109,7 +59,7 @@ fn main() {
|
|||||||
None => { eprintln!(" SKIP: {}", stem); continue; }
|
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) {
|
let img = match image::open(&path) {
|
||||||
Ok(i) => i,
|
Ok(i) => i,
|
||||||
@@ -118,17 +68,52 @@ fn main() {
|
|||||||
|
|
||||||
let rgba = img.to_rgba8();
|
let rgba = img.to_rgba8();
|
||||||
let raw = rgba.into_raw();
|
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];
|
let mut content_mask = vec![false; npix];
|
||||||
for i in 0..npix {
|
for i in 0..npix {
|
||||||
content_mask[i] = raw[i * 4 + 3] > 0;
|
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<hcie_fx::types::LayerEffect> = 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 {
|
samples.push(Sample {
|
||||||
name: stem,
|
name: stem,
|
||||||
shape, style, depth, size, soften, direction, altitude,
|
style, depth, size, soften, direction, altitude,
|
||||||
|
angle,
|
||||||
ref_pixels: raw,
|
ref_pixels: raw,
|
||||||
content_mask,
|
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();
|
let content_count = samples[0].content_mask.iter().filter(|&&b| b).count();
|
||||||
println!("Content pixels per image: {} / {} ({:.1}%)",
|
println!("Content pixels per image: {}", content_count);
|
||||||
content_count, npix(), content_count as f64 / npix() as f64 * 100.0);
|
|
||||||
|
|
||||||
let rect_alpha: Vec<u8> = make_shape_alpha("rect");
|
// Use all samples (inner bevel, outer bevel, emboss) for combined tuning
|
||||||
let circle_alpha: Vec<u8> = make_shape_alpha("circle");
|
|
||||||
|
|
||||||
let lighting_blurs: &[i32] = &[0, 5, 10, 15, 25, 40];
|
// Grid search over lighting_blur, hl_scale, sh_scale, tilt_scale
|
||||||
let hl_scales: &[f32] = &[0.5, 0.75, 1.0, 1.25, 1.5, 2.0];
|
// Wide ranges to cover high-depth PSDs (e.g. Sultan depth=350) and
|
||||||
let sh_scales: &[f32] = &[0.5, 0.75, 1.0, 1.25, 1.5, 2.0];
|
// 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());
|
println!("Grid: {} combos x {} samples = {} evals", total_combos, samples.len(), total_combos * samples.len());
|
||||||
std::io::stdout().flush().ok();
|
std::io::stdout().flush().ok();
|
||||||
|
|
||||||
let mut best_avg_mae = f64::MAX;
|
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;
|
let mut eval_count = 0usize;
|
||||||
|
|
||||||
for &lighting_blur in lighting_blurs {
|
for &lighting_blur in lighting_blurs {
|
||||||
for &hl_scale in hl_scales {
|
for &hl_scale in hl_scales {
|
||||||
for &sh_scale in sh_scales {
|
for &sh_scale in sh_scales {
|
||||||
let mut total_mae = 0.0f64;
|
for &tilt_scale in tilt_scales {
|
||||||
|
let total_mae: f64 = samples.par_iter().map(|s| {
|
||||||
for s in &samples {
|
// Use generate_bevel_tuned to get highlight/shadow buffers
|
||||||
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) }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let style_enum = match s.style.as_str() {
|
let style_enum = match s.style.as_str() {
|
||||||
"emboss" => hcie_fx::types::BevelStyle::Emboss,
|
"emboss" => hcie_fx::types::BevelStyle::Emboss,
|
||||||
"outer" => hcie_fx::types::BevelStyle::OuterBevel,
|
"outer" => hcie_fx::types::BevelStyle::OuterBevel,
|
||||||
@@ -186,43 +162,84 @@ fn main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (highlight, shadow) = hcie_fx::tuned::generate_bevel_tuned(
|
let (highlight, shadow) = hcie_fx::tuned::generate_bevel_tuned(
|
||||||
alpha, CANVAS, CANVAS,
|
&s.layer_pixels.iter().skip(3).step_by(4).copied().collect::<Vec<u8>>(),
|
||||||
s.depth, s.size, s.soften, 120.0, s.altitude,
|
s.w, s.h,
|
||||||
|
s.depth, s.size, s.soften, s.angle, s.altitude,
|
||||||
&dir_enum, &hcie_fx::types::Technique::Smooth, &style_enum,
|
&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];
|
let mut out = vec![0u8; px * 4];
|
||||||
|
|
||||||
// Base shape: black fill for both Inner Bevel and Emboss.
|
// Base shape: fill with layer pixels (black fill)
|
||||||
// Photoshop Emboss applies highlights/shadows on top of the original fill.
|
|
||||||
for i in 0..px {
|
for i in 0..px {
|
||||||
if alpha[i] > 0 {
|
if s.layer_pixels[i * 4 + 3] > 0 {
|
||||||
out[i * 4] = 0;
|
out[i * 4] = s.layer_pixels[i * 4];
|
||||||
out[i * 4 + 1] = 0;
|
out[i * 4 + 1] = s.layer_pixels[i * 4 + 1];
|
||||||
out[i * 4 + 2] = 0;
|
out[i * 4 + 2] = s.layer_pixels[i * 4 + 2];
|
||||||
out[i * 4 + 3] = alpha[i];
|
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 {
|
for i in 0..px {
|
||||||
let sa = highlight[i * 4 + 3];
|
let sa = highlight[i * 4 + 3];
|
||||||
if sa > 0 {
|
if sa > 0 {
|
||||||
|
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 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 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);
|
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);
|
out[i * 4..i * 4 + 4].copy_from_slice(&blended);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Composite shadow
|
||||||
for i in 0..px {
|
for i in 0..px {
|
||||||
let sa = shadow[i * 4 + 3];
|
let sa = shadow[i * 4 + 3];
|
||||||
if sa > 0 {
|
if sa > 0 {
|
||||||
|
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 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 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);
|
let blended = hcie_blend::blend_pixels(dst, src, sh_bm, sh_op);
|
||||||
out[i * 4..i * 4 + 4].copy_from_slice(&blended);
|
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 {
|
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;
|
eval_count += 1;
|
||||||
let avg_mae = total_mae / samples.len() as f64;
|
let avg_mae = total_mae / samples.len() as f64;
|
||||||
|
|
||||||
if avg_mae < best_avg_mae {
|
if avg_mae < best_avg_mae {
|
||||||
best_avg_mae = avg_mae;
|
best_avg_mae = avg_mae;
|
||||||
best_params = (lighting_blur, hl_scale, sh_scale);
|
best_params = (lighting_blur, hl_scale, sh_scale, tilt_scale);
|
||||||
println!("*** [{}/{}] NEW BEST avg MAE = {:.4} (lb={}, hl={:.2}, sh={:.2})",
|
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);
|
eval_count, total_combos, best_avg_mae, lighting_blur, hl_scale, sh_scale, tilt_scale);
|
||||||
std::io::stdout().flush().ok();
|
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);
|
println!(" [{}/{}] avg={:.4} best={:.4}", eval_count, total_combos, avg_mae, best_avg_mae);
|
||||||
std::io::stdout().flush().ok();
|
std::io::stdout().flush().ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (lb, hl, sh) = best_params;
|
let (lb, hl, sh, ts) = best_params;
|
||||||
println!("\n=== RESULT ===");
|
println!("\n=== RESULT ===");
|
||||||
println!("Best avg MAE = {:.4}", best_avg_mae);
|
println!("Best avg MAE = {:.4}", best_avg_mae);
|
||||||
println!(" lighting_blur = {}", lb);
|
println!(" lighting_blur = {}", lb);
|
||||||
println!(" hl_scale = {:.2}", hl);
|
println!(" hl_scale = {:.3}", hl);
|
||||||
println!(" sh_scale = {:.2}", sh);
|
println!(" sh_scale = {:.2}", sh);
|
||||||
|
println!(" tilt_scale = {:.2}", ts);
|
||||||
|
|
||||||
if let Ok(mut file) = std::fs::OpenOptions::new()
|
if let Ok(mut file) = std::fs::OpenOptions::new()
|
||||||
.create(true).write(true).truncate(true).open(RESULTS_FILE)
|
.create(true).write(true).truncate(true).open(RESULTS_FILE)
|
||||||
{
|
{
|
||||||
let _ = writeln!(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={}",
|
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)> {
|
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}
|
// be_{shape}_{style}_d{depth}_sz{size}_sf{soften}_{dir}_alt{altitude}_{technique}
|
||||||
let parts: Vec<&str> = name.split('_').collect();
|
let parts: Vec<&str> = name.split('_').collect();
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
const SHADOW_RS: &str = "hcie-fx/src/emboss.rs";
|
const SHADOW_RS: &str = "hcie-fx/src/emboss.rs";
|
||||||
const PSD_BASE: &str = "_images/_psd_stil_test/sultan_test/sultan";
|
const PSD_BASE: &str = "_images/_psd_stil_test/test_2";
|
||||||
const RESULTS_FILE: &str = "/mnt/extra/00_PROJECTS/hcie-rust-v4/emboss_results.txt";
|
const RESULTS_FILE: &str = "./emboss_results.txt";
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let original = std::fs::read_to_string(SHADOW_RS).expect("read shadow.rs");
|
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 before = &original[..zone_start];
|
||||||
let after = &original[zone_end + end_tag.len()..];
|
let after = &original[zone_end + end_tag.len()..];
|
||||||
|
|
||||||
let ref_img = image::open(&format!("{}/Layer 2.png", PSD_BASE))
|
let ref_img = image::open(&format!("{}/Emboss.png", PSD_BASE))
|
||||||
.expect("open Layer 2.png")
|
.expect("open Emboss.png")
|
||||||
.to_rgba8();
|
.to_rgba8();
|
||||||
let (w, h) = (ref_img.width(), ref_img.height());
|
let (w, h) = (ref_img.width(), ref_img.height());
|
||||||
let px = (w * h) as usize;
|
let px = (w * h) as usize;
|
||||||
let ref_pixels = ref_img.into_raw();
|
let ref_pixels = ref_img.into_raw();
|
||||||
|
|
||||||
let layers = hcie_psd::import_psd(std::path::Path::new(&format!("{}/Layer 2.psd", PSD_BASE)))
|
let layers = hcie_psd::import_psd(std::path::Path::new(&format!("{}/Emboss.psd", PSD_BASE)))
|
||||||
.expect("import Layer 2.psd");
|
.expect("import Emboss.psd");
|
||||||
let layer = &layers[0];
|
let layer = &layers[0];
|
||||||
let alpha: Vec<u8> = layer.pixels.iter().skip(3).step_by(4).copied().collect();
|
let alpha: Vec<u8> = layer.pixels.iter().skip(3).step_by(4).copied().collect();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
fn main() {
|
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);
|
println!("Loading KRA from: {}", kra_path);
|
||||||
let file = std::fs::File::open(kra_path).unwrap();
|
let file = std::fs::File::open(kra_path).unwrap();
|
||||||
let mut archive = zip::ZipArchive::new(file).unwrap();
|
let mut archive = zip::ZipArchive::new(file).unwrap();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#![allow(dead_code, unused_imports, unused_variables, unused_macros, unreachable_patterns, unused_assignments, clippy::cfg)]
|
#![allow(dead_code, unused_imports, unused_variables, unused_macros, unreachable_patterns, unused_assignments, clippy::cfg)]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_psd_import_sizes_and_offsets() {
|
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; }
|
if !path.exists() { return; }
|
||||||
|
|
||||||
let layers = hcie_psd::import_psd(path).unwrap();
|
let layers = hcie_psd::import_psd(path).unwrap();
|
||||||
@@ -41,8 +41,8 @@ fn map_to_composite_layers(layers: &[hcie_protocol::Layer]) -> Vec<hcie_protocol
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_psd_composite_against_ref() {
|
fn test_psd_composite_against_ref() {
|
||||||
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");
|
||||||
let ref_path = std::path::Path::new("/home/hc/Pictures/_test_images/example3/Example3-mini.png");
|
let ref_path = std::path::Path::new("_images/_test_images/example3/Example3-mini.png");
|
||||||
if !path.exists() || !ref_path.exists() {
|
if !path.exists() || !ref_path.exists() {
|
||||||
println!("Test files not found, skipping comparison.");
|
println!("Test files not found, skipping comparison.");
|
||||||
return;
|
return;
|
||||||
@@ -239,9 +239,9 @@ fn composite_and_compare(psd_path: &str, ref_path: &str) {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_layer_pixels_direct() {
|
fn test_layer_pixels_direct() {
|
||||||
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 layers = hcie_psd::import_psd(psd_path).unwrap();
|
||||||
let ref_img = image::open("/home/hc/Pictures/_psd_stil_test/base_test_generated_2.png").unwrap().to_rgba8();
|
let ref_img = image::open("_images/_psd_stil_test/base_test_generated_2.png").unwrap().to_rgba8();
|
||||||
let ref_pixels = ref_img.as_raw();
|
let ref_pixels = ref_img.as_raw();
|
||||||
|
|
||||||
// Compare layer 0 (Background Grid) pixels directly with reference
|
// Compare layer 0 (Background Grid) pixels directly with reference
|
||||||
@@ -302,7 +302,7 @@ fn test_base_generated_2_vs_merged() {
|
|||||||
// Extract PSD merged image using ImageMagick
|
// Extract PSD merged image using ImageMagick
|
||||||
let _ = std::process::Command::new("convert")
|
let _ = std::process::Command::new("convert")
|
||||||
.args(&[
|
.args(&[
|
||||||
"/home/hc/Pictures/_psd_stil_test/base_test_generated_2.psd[0]",
|
"_images/_psd_stil_test/base_test_generated_2.psd[0]",
|
||||||
"/tmp/psd2_merged_for_test.png"
|
"/tmp/psd2_merged_for_test.png"
|
||||||
])
|
])
|
||||||
.status();
|
.status();
|
||||||
@@ -310,7 +310,7 @@ fn test_base_generated_2_vs_merged() {
|
|||||||
let psd_merged = image::open("/tmp/psd2_merged_for_test.png").unwrap().to_rgba8();
|
let psd_merged = image::open("/tmp/psd2_merged_for_test.png").unwrap().to_rgba8();
|
||||||
let psd_pixels = psd_merged.as_raw();
|
let psd_pixels = psd_merged.as_raw();
|
||||||
|
|
||||||
let layers = hcie_psd::import_psd(std::path::Path::new("/home/hc/Pictures/_psd_stil_test/base_test_generated_2.psd")).unwrap();
|
let layers = hcie_psd::import_psd(std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.psd")).unwrap();
|
||||||
let canvas_w = layers[0].width;
|
let canvas_w = layers[0].width;
|
||||||
let canvas_h = layers[0].height;
|
let canvas_h = layers[0].height;
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@ fn test_blend_normal() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_teal_circle_emboss_debug() {
|
fn test_teal_circle_emboss_debug() {
|
||||||
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 layers = hcie_psd::import_psd(psd_path).unwrap();
|
||||||
let teal = &layers[1]; // Teal Circle
|
let teal = &layers[1]; // Teal Circle
|
||||||
|
|
||||||
@@ -376,9 +376,9 @@ fn test_teal_circle_emboss_debug() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_base_generated_2_no_effects() {
|
fn test_base_generated_2_no_effects() {
|
||||||
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 layers = hcie_psd::import_psd(psd_path).unwrap();
|
||||||
let ref_img = image::open("/home/hc/Pictures/_psd_stil_test/base_test_generated_2.png").unwrap().to_rgba8();
|
let ref_img = image::open("_images/_psd_stil_test/base_test_generated_2.png").unwrap().to_rgba8();
|
||||||
let ref_pixels = ref_img.as_raw();
|
let ref_pixels = ref_img.as_raw();
|
||||||
|
|
||||||
let canvas_w = layers[0].width;
|
let canvas_w = layers[0].width;
|
||||||
@@ -419,16 +419,16 @@ fn test_base_generated_2_no_effects() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_base_generated_2_composite() {
|
fn test_base_generated_2_composite() {
|
||||||
composite_and_compare(
|
composite_and_compare(
|
||||||
"/home/hc/Pictures/_psd_stil_test/base_test_generated_2.psd",
|
"_images/_psd_stil_test/base_test_generated_2.psd",
|
||||||
"/home/hc/Pictures/_psd_stil_test/base_test_generated_2.png",
|
"_images/_psd_stil_test/base_test_generated_2.png",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_hc_emboss_composite() {
|
fn test_hc_emboss_composite() {
|
||||||
composite_and_compare(
|
composite_and_compare(
|
||||||
"/home/hc/Pictures/_psd_stil_test/hc_emboss.psd",
|
"_images/_psd_stil_test/hc_emboss.psd",
|
||||||
"/home/hc/Pictures/_psd_stil_test/hc_emboss.png",
|
"_images/_psd_stil_test/hc_emboss.png",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,8 +436,8 @@ fn test_hc_emboss_composite() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_base_generated_2_effect_isolation() {
|
fn test_base_generated_2_effect_isolation() {
|
||||||
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 ref_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/base_test_generated_2.png");
|
let ref_path = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2.png");
|
||||||
|
|
||||||
let all_layers = hcie_psd::import_psd(psd_path).unwrap();
|
let all_layers = hcie_psd::import_psd(psd_path).unwrap();
|
||||||
let ref_img = image::open(ref_path).unwrap().to_rgba8();
|
let ref_img = image::open(ref_path).unwrap().to_rgba8();
|
||||||
@@ -489,7 +489,7 @@ fn compute_mae(a: &[u8], b: &[u8], pixels: usize) -> f64 {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_soft_orange_pixels() {
|
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 layers = hcie_psd::import_psd(psd_path).unwrap();
|
||||||
|
|
||||||
let orange = &layers[5]; // Soft Orange Shape
|
let orange = &layers[5]; // Soft Orange Shape
|
||||||
@@ -514,7 +514,7 @@ fn test_check_soft_orange_pixels() {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_orange_pixels() {
|
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 layers = hcie_psd::import_psd(psd_path).unwrap();
|
||||||
|
|
||||||
let orange = &layers[5]; // Soft Orange Shape
|
let orange = &layers[5]; // Soft Orange Shape
|
||||||
@@ -553,7 +553,7 @@ fn test_find_orange_pixels() {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_layer_bounds() {
|
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();
|
let layers = hcie_psd::import_psd(psd_path).unwrap();
|
||||||
|
|
||||||
for (i, layer) in layers.iter().enumerate() {
|
for (i, layer) in layers.iter().enumerate() {
|
||||||
@@ -582,7 +582,7 @@ fn test_layer_bounds() {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_check_layer_offsets() {
|
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();
|
let layers = hcie_psd::import_psd(psd_path).unwrap();
|
||||||
|
|
||||||
println!("Layer positions and sizes:");
|
println!("Layer positions and sizes:");
|
||||||
@@ -619,7 +619,7 @@ fn test_check_layer_offsets() {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_save_layer_pixels() {
|
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();
|
let layers = hcie_psd::import_psd(psd_path).unwrap();
|
||||||
|
|
||||||
for (i, layer) in layers.iter().enumerate() {
|
for (i, layer) in layers.iter().enumerate() {
|
||||||
@@ -633,7 +633,7 @@ fn test_save_layer_pixels() {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pixel_layer_contributions() {
|
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 layers = hcie_psd::import_psd(psd_path).unwrap();
|
||||||
|
|
||||||
let test_pixels = [(300, 200), (400, 200), (200, 300), (500, 200), (500, 500)];
|
let test_pixels = [(300, 200), (400, 200), (200, 300), (500, 200), (500, 500)];
|
||||||
@@ -656,7 +656,7 @@ fn test_pixel_layer_contributions() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_load_test_2_psd() {
|
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; }
|
if !test_dir.exists() { return; }
|
||||||
|
|
||||||
let entries: Vec<_> = std::fs::read_dir(test_dir).unwrap()
|
let entries: Vec<_> = std::fs::read_dir(test_dir).unwrap()
|
||||||
@@ -694,8 +694,8 @@ fn test_load_test_2_psd() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_per_layer_effects_vs_photoshop_export() {
|
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 psd_path = std::path::Path::new("_images/_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 ref_dir = std::path::Path::new("_images/_psd_stil_test/base_test_generated_2");
|
||||||
let layers = hcie_psd::import_psd(psd_path).unwrap();
|
let layers = hcie_psd::import_psd(psd_path).unwrap();
|
||||||
let layer_names = [
|
let layer_names = [
|
||||||
"Background Grid", "Teal Circle", "Pink Rectangle",
|
"Background Grid", "Teal Circle", "Pink Rectangle",
|
||||||
@@ -793,7 +793,7 @@ fn test_per_layer_effects_vs_photoshop_export() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_test2_effects_mae() {
|
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; }
|
if !test_dir.exists() { return; }
|
||||||
|
|
||||||
let entries: Vec<_> = std::fs::read_dir(test_dir).unwrap()
|
let entries: Vec<_> = std::fs::read_dir(test_dir).unwrap()
|
||||||
@@ -859,9 +859,9 @@ fn test_test2_effects_mae() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sultan_effects_mae() {
|
fn test_sultan_effects_mae() {
|
||||||
let psd_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/sultan.psd");
|
let psd_path = std::path::Path::new("_images/_psd_stil_test/sultan.psd");
|
||||||
let ref_png = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/sultan.png");
|
let ref_png = std::path::Path::new("_images/_psd_stil_test/sultan.png");
|
||||||
let layer_dir = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/sultan");
|
let layer_dir = std::path::Path::new("_images/_psd_stil_test/sultan");
|
||||||
if !psd_path.exists() || !ref_png.exists() { return; }
|
if !psd_path.exists() || !ref_png.exists() { return; }
|
||||||
|
|
||||||
let layers = hcie_psd::import_psd(psd_path).unwrap();
|
let layers = hcie_psd::import_psd(psd_path).unwrap();
|
||||||
@@ -1055,8 +1055,8 @@ fn test_sultan_effects_mae() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_emboss_optimize() {
|
fn test_emboss_optimize() {
|
||||||
let psd_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/emboss.psd");
|
let psd_path = std::path::Path::new("_images/_psd_stil_test/emboss.psd");
|
||||||
let ref_path = std::path::Path::new("/home/hc/Pictures/_psd_stil_test/emboss.png");
|
let ref_path = std::path::Path::new("_images/_psd_stil_test/emboss.png");
|
||||||
if !psd_path.exists() || !ref_path.exists() { return; }
|
if !psd_path.exists() || !ref_path.exists() { return; }
|
||||||
|
|
||||||
let layers = hcie_psd::import_psd(psd_path).unwrap();
|
let layers = hcie_psd::import_psd(psd_path).unwrap();
|
||||||
@@ -1245,9 +1245,9 @@ fn test_emboss_optimize() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_effects_survive_ffi_boundary() {
|
fn test_effects_survive_ffi_boundary() {
|
||||||
let paths = [
|
let paths = [
|
||||||
"/home/hc/Documents/00_PROJECTS/hcie-rust-v4/hcie-io/test_resources/base_test_generated_2.psd",
|
"./test_resources/base_test_generated_2.psd",
|
||||||
"/home/hc/Pictures/_psd_stil_test/emboss.psd",
|
"_images/_psd_stil_test/emboss.psd",
|
||||||
"/home/hc/Pictures/_test_images/example3/Example3-mini.psd",
|
"_images/_test_images/example3/Example3-mini.psd",
|
||||||
];
|
];
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
for p in &paths {
|
for p in &paths {
|
||||||
@@ -1275,9 +1275,9 @@ fn test_effects_survive_ffi_boundary() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_ffi_bincode_roundtrip() {
|
fn test_ffi_bincode_roundtrip() {
|
||||||
let paths = [
|
let paths = [
|
||||||
"/home/hc/Documents/00_PROJECTS/hcie-rust-v4/hcie-io/test_resources/base_test_generated_2.psd",
|
"./test_resources/base_test_generated_2.psd",
|
||||||
"/home/hc/Pictures/_psd_stil_test/emboss.psd",
|
"_images/_psd_stil_test/emboss.psd",
|
||||||
"/home/hc/Pictures/_test_images/example3/Example3-mini.psd",
|
"_images/_test_images/example3/Example3-mini.psd",
|
||||||
];
|
];
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
for p in &paths {
|
for p in &paths {
|
||||||
|
|||||||
Reference in New Issue
Block a user