2026-07-09 02:59:53 +03:00
|
|
|
#![allow(dead_code, unused_imports, unused_variables, unused_macros)]
|
2026-07-16 22:10:22 +03:00
|
|
|
use rayon::prelude::*;
|
2026-07-09 02:59:53 +03:00
|
|
|
/// Grid-search Bevel & Emboss MAE tuning against Photoshop ground-truth PNGs.
|
|
|
|
|
///
|
2026-07-13 20:33:39 +03:00
|
|
|
/// Expects `_tmp/psd_tunes/bevel_emboss_png_set/` with transparent PNGs exported
|
|
|
|
|
/// from `_tmp/psd_tunes/bevel_emboss_psd_set/`.
|
|
|
|
|
///
|
|
|
|
|
/// Uses the full `apply_layer_effects` pipeline (including inside/behind split
|
|
|
|
|
/// for Emboss/OuterBevel) so the tuning matches real rendering behavior.
|
|
|
|
|
///
|
|
|
|
|
/// Usage:
|
|
|
|
|
/// cargo run --example tune_mae_bevel_emboss_grid -p hcie-io --release
|
|
|
|
|
use std::fs;
|
2026-07-09 02:59:53 +03:00
|
|
|
use std::io::Write;
|
|
|
|
|
|
2026-07-13 20:33:39 +03:00
|
|
|
const GT_DIR: &str = "_tmp/psd_tunes/bevel_emboss_png_set";
|
|
|
|
|
const RESULTS_FILE: &str = "bevel_emboss_grid_results_v4.txt";
|
2026-07-09 02:59:53 +03:00
|
|
|
|
|
|
|
|
struct Sample {
|
|
|
|
|
name: String,
|
|
|
|
|
style: String,
|
|
|
|
|
depth: f32,
|
|
|
|
|
size: f32,
|
|
|
|
|
soften: f32,
|
|
|
|
|
direction: String,
|
|
|
|
|
altitude: f32,
|
2026-07-13 20:33:39 +03:00
|
|
|
angle: f32,
|
2026-07-09 02:59:53 +03:00
|
|
|
ref_pixels: Vec<u8>,
|
|
|
|
|
content_mask: Vec<bool>,
|
2026-07-13 20:33:39 +03:00
|
|
|
layer_pixels: Vec<u8>,
|
|
|
|
|
fx_effects: Vec<hcie_fx::types::LayerEffect>,
|
|
|
|
|
fill_opacity: f32,
|
|
|
|
|
w: u32,
|
|
|
|
|
h: u32,
|
2026-07-09 02:59:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
|
let entries: Vec<_> = std::fs::read_dir(GT_DIR)
|
|
|
|
|
.unwrap_or_else(|e| panic!("cannot read {}: {}", GT_DIR, e))
|
|
|
|
|
.filter_map(|e| e.ok())
|
|
|
|
|
.filter(|e| {
|
|
|
|
|
let s = e.file_name();
|
|
|
|
|
let s = s.to_string_lossy();
|
|
|
|
|
s.starts_with("be_") && s.ends_with(".png")
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
println!("Found {} PNGs in {}", entries.len(), GT_DIR);
|
|
|
|
|
|
|
|
|
|
let mut samples: Vec<Sample> = Vec::new();
|
|
|
|
|
|
|
|
|
|
for entry in &entries {
|
|
|
|
|
let path = entry.path();
|
|
|
|
|
let stem = path.file_stem().unwrap().to_string_lossy().to_string();
|
|
|
|
|
|
|
|
|
|
let params = match parse_filename(&stem) {
|
|
|
|
|
Some(p) => p,
|
2026-07-16 22:10:22 +03:00
|
|
|
None => {
|
|
|
|
|
eprintln!(" SKIP: {}", stem);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-07-09 02:59:53 +03:00
|
|
|
};
|
|
|
|
|
|
2026-07-13 20:33:39 +03:00
|
|
|
let (_shape, style, depth, size, soften, direction, altitude, _technique) = params;
|
2026-07-09 02:59:53 +03:00
|
|
|
|
|
|
|
|
let img = match image::open(&path) {
|
|
|
|
|
Ok(i) => i,
|
2026-07-16 22:10:22 +03:00
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!(" SKIP {}: {}", stem, e);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-07-09 02:59:53 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let rgba = img.to_rgba8();
|
|
|
|
|
let raw = rgba.into_raw();
|
2026-07-13 20:33:39 +03:00
|
|
|
let w = img.width();
|
|
|
|
|
let h = img.height();
|
|
|
|
|
let npix = (w * h) as usize;
|
2026-07-09 02:59:53 +03:00
|
|
|
let mut content_mask = vec![false; npix];
|
|
|
|
|
for i in 0..npix {
|
|
|
|
|
content_mask[i] = raw[i * 4 + 3] > 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 20:33:39 +03:00
|
|
|
let psd_path_str = format!("_tmp/psd_tunes/bevel_emboss_psd_set/{}.psd", stem);
|
2026-07-16 22:10:22 +03:00
|
|
|
let (layer_pixels, fx_effects, fill_opacity, angle) = if let Ok(layers) =
|
|
|
|
|
hcie_psd::import_psd(std::path::Path::new(&psd_path_str))
|
|
|
|
|
{
|
2026-07-13 20:33:39 +03:00
|
|
|
let mut found = None;
|
|
|
|
|
for l in layers {
|
|
|
|
|
if !l.effects.is_empty() {
|
2026-07-16 22:10:22 +03:00
|
|
|
let fx_effects: Vec<hcie_fx::types::LayerEffect> = l
|
|
|
|
|
.effects
|
|
|
|
|
.iter()
|
2026-07-13 20:33:39 +03:00
|
|
|
.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 {
|
2026-07-16 22:10:22 +03:00
|
|
|
if let hcie_protocol::effects::LayerEffect::BevelEmboss {
|
|
|
|
|
angle: a, ..
|
|
|
|
|
} = e
|
|
|
|
|
{
|
2026-07-13 20:33:39 +03:00
|
|
|
angle = *a;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
found = Some((l.pixels, fx_effects, l.fill_opacity, angle));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
match found {
|
|
|
|
|
Some(v) => v,
|
2026-07-16 22:10:22 +03:00
|
|
|
None => {
|
|
|
|
|
eprintln!(" SKIP no effects layer: {}", stem);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-07-13 20:33:39 +03:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
eprintln!(" SKIP PSD missing for {}: {}", stem, psd_path_str);
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-09 02:59:53 +03:00
|
|
|
samples.push(Sample {
|
|
|
|
|
name: stem,
|
2026-07-16 22:10:22 +03:00
|
|
|
style,
|
|
|
|
|
depth,
|
|
|
|
|
size,
|
|
|
|
|
soften,
|
|
|
|
|
direction,
|
|
|
|
|
altitude,
|
2026-07-13 20:33:39 +03:00
|
|
|
angle,
|
2026-07-09 02:59:53 +03:00
|
|
|
ref_pixels: raw,
|
|
|
|
|
content_mask,
|
2026-07-13 20:33:39 +03:00
|
|
|
layer_pixels,
|
|
|
|
|
fx_effects,
|
|
|
|
|
fill_opacity,
|
2026-07-16 22:10:22 +03:00
|
|
|
w,
|
|
|
|
|
h,
|
2026-07-09 02:59:53 +03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
println!("Loaded {} samples", samples.len());
|
|
|
|
|
if samples.is_empty() {
|
|
|
|
|
eprintln!("No valid samples found.");
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let content_count = samples[0].content_mask.iter().filter(|&&b| b).count();
|
2026-07-13 20:33:39 +03:00
|
|
|
println!("Content pixels per image: {}", content_count);
|
2026-07-09 02:59:53 +03:00
|
|
|
|
2026-07-13 20:33:39 +03:00
|
|
|
// Use all samples (inner bevel, outer bevel, emboss) for combined tuning
|
2026-07-09 02:59:53 +03:00
|
|
|
|
2026-07-13 20:33:39 +03:00
|
|
|
// Grid search over lighting_blur, hl_scale, sh_scale, tilt_scale
|
|
|
|
|
// Wide ranges to cover high-depth PSDs (e.g. Sultan depth=350) and
|
|
|
|
|
// various bevel sizes (10, 25+). Each axis spans low→high so the
|
|
|
|
|
// search can find optima for both simple and complex shapes.
|
|
|
|
|
let lighting_blurs: &[i32] = &[0, 2, 5];
|
|
|
|
|
let hl_scales: &[f32] = &[0.05, 0.1, 0.2, 0.5];
|
|
|
|
|
let sh_scales: &[f32] = &[2.0, 8.0, 16.0];
|
|
|
|
|
let tilt_scales: &[f32] = &[0.05, 0.1, 0.2, 0.3, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, 5.0, 8.0];
|
2026-07-09 02:59:53 +03:00
|
|
|
|
2026-07-13 20:33:39 +03:00
|
|
|
let total_combos = lighting_blurs.len() * hl_scales.len() * sh_scales.len() * tilt_scales.len();
|
2026-07-16 22:10:22 +03:00
|
|
|
println!(
|
|
|
|
|
"Grid: {} combos x {} samples = {} evals",
|
|
|
|
|
total_combos,
|
|
|
|
|
samples.len(),
|
|
|
|
|
total_combos * samples.len()
|
|
|
|
|
);
|
2026-07-09 02:59:53 +03:00
|
|
|
std::io::stdout().flush().ok();
|
|
|
|
|
|
|
|
|
|
let mut best_avg_mae = f64::MAX;
|
2026-07-13 20:33:39 +03:00
|
|
|
let mut best_params = (0i32, 0.0f32, 0.0f32, 0.0f32);
|
2026-07-09 02:59:53 +03:00
|
|
|
let mut eval_count = 0usize;
|
|
|
|
|
|
|
|
|
|
for &lighting_blur in lighting_blurs {
|
|
|
|
|
for &hl_scale in hl_scales {
|
|
|
|
|
for &sh_scale in sh_scales {
|
2026-07-13 20:33:39 +03:00
|
|
|
for &tilt_scale in tilt_scales {
|
2026-07-16 22:10:22 +03:00
|
|
|
let total_mae: f64 = samples
|
|
|
|
|
.par_iter()
|
|
|
|
|
.map(|s| {
|
|
|
|
|
// Use generate_bevel_tuned to get highlight/shadow buffers
|
|
|
|
|
let style_enum = match s.style.as_str() {
|
|
|
|
|
"emboss" => hcie_fx::types::BevelStyle::Emboss,
|
|
|
|
|
"outer" => hcie_fx::types::BevelStyle::OuterBevel,
|
|
|
|
|
_ => hcie_fx::types::BevelStyle::InnerBevel,
|
|
|
|
|
};
|
|
|
|
|
let dir_enum = match s.direction.as_str() {
|
|
|
|
|
"down" => hcie_fx::types::Direction::Down,
|
|
|
|
|
_ => hcie_fx::types::Direction::Up,
|
|
|
|
|
};
|
2026-07-09 02:59:53 +03:00
|
|
|
|
2026-07-16 22:10:22 +03:00
|
|
|
let (highlight, shadow) = hcie_fx::tuned::generate_bevel_tuned(
|
|
|
|
|
&s.layer_pixels
|
|
|
|
|
.iter()
|
|
|
|
|
.skip(3)
|
|
|
|
|
.step_by(4)
|
|
|
|
|
.copied()
|
|
|
|
|
.collect::<Vec<u8>>(),
|
|
|
|
|
s.w,
|
|
|
|
|
s.h,
|
|
|
|
|
s.depth,
|
|
|
|
|
s.size,
|
|
|
|
|
s.soften,
|
|
|
|
|
s.angle,
|
|
|
|
|
s.altitude,
|
|
|
|
|
&dir_enum,
|
|
|
|
|
&hcie_fx::types::Technique::Smooth,
|
|
|
|
|
&style_enum,
|
|
|
|
|
lighting_blur,
|
|
|
|
|
hl_scale,
|
|
|
|
|
sh_scale,
|
|
|
|
|
1.0,
|
|
|
|
|
tilt_scale,
|
|
|
|
|
);
|
2026-07-09 02:59:53 +03:00
|
|
|
|
2026-07-16 22:10:22 +03:00
|
|
|
let px = (s.w * s.h) as usize;
|
|
|
|
|
let mut out = vec![0u8; px * 4];
|
2026-07-09 02:59:53 +03:00
|
|
|
|
2026-07-16 22:10:22 +03:00
|
|
|
// Base shape: fill with layer pixels (black fill)
|
|
|
|
|
for i in 0..px {
|
|
|
|
|
if s.layer_pixels[i * 4 + 3] > 0 {
|
|
|
|
|
out[i * 4] = s.layer_pixels[i * 4];
|
|
|
|
|
out[i * 4 + 1] = s.layer_pixels[i * 4 + 1];
|
|
|
|
|
out[i * 4 + 2] = s.layer_pixels[i * 4 + 2];
|
|
|
|
|
out[i * 4 + 3] = s.layer_pixels[i * 4 + 3];
|
|
|
|
|
}
|
2026-07-13 20:33:39 +03:00
|
|
|
}
|
|
|
|
|
|
2026-07-16 22:10:22 +03:00
|
|
|
// For emboss/outerbevel: outside highlight/shadow go to empty canvas (behind)
|
|
|
|
|
let has_outer = matches!(
|
|
|
|
|
style_enum,
|
|
|
|
|
hcie_fx::types::BevelStyle::Emboss
|
|
|
|
|
| hcie_fx::types::BevelStyle::OuterBevel
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Get opacity and blend mode from fx_effects
|
|
|
|
|
let (hl_op, sh_op, hl_bm, sh_bm) = {
|
|
|
|
|
let mut hl_op = 0.75f32;
|
|
|
|
|
let mut sh_op = 0.75f32;
|
|
|
|
|
let mut hl_bm = hcie_blend::BlendMode::Screen;
|
|
|
|
|
let mut sh_bm = hcie_blend::BlendMode::Multiply;
|
|
|
|
|
for e in &s.fx_effects {
|
|
|
|
|
if let hcie_fx::types::LayerEffect::BevelEmboss {
|
|
|
|
|
highlight_opacity,
|
|
|
|
|
shadow_opacity,
|
|
|
|
|
highlight_blend,
|
|
|
|
|
shadow_blend,
|
|
|
|
|
..
|
|
|
|
|
} = e
|
|
|
|
|
{
|
|
|
|
|
hl_op = *highlight_opacity / 100.0;
|
|
|
|
|
sh_op = *shadow_opacity / 100.0;
|
|
|
|
|
hl_bm = *highlight_blend;
|
|
|
|
|
sh_bm = *shadow_blend;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
(hl_op, sh_op, hl_bm, sh_bm)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Composite highlight — inside on fill, outside on empty canvas
|
|
|
|
|
for i in 0..px {
|
|
|
|
|
let sa = highlight[i * 4 + 3];
|
|
|
|
|
if sa > 0 {
|
|
|
|
|
let is_inner = s.layer_pixels[i * 4 + 3] > 0;
|
|
|
|
|
if !has_outer || is_inner {
|
|
|
|
|
let dst = [
|
|
|
|
|
out[i * 4],
|
|
|
|
|
out[i * 4 + 1],
|
|
|
|
|
out[i * 4 + 2],
|
|
|
|
|
out[i * 4 + 3],
|
|
|
|
|
];
|
|
|
|
|
let src = [
|
|
|
|
|
highlight[i * 4],
|
|
|
|
|
highlight[i * 4 + 1],
|
|
|
|
|
highlight[i * 4 + 2],
|
|
|
|
|
sa,
|
|
|
|
|
];
|
|
|
|
|
let blended =
|
|
|
|
|
hcie_blend::blend_pixels(dst, src, hl_bm, hl_op);
|
|
|
|
|
out[i * 4..i * 4 + 4].copy_from_slice(&blended);
|
|
|
|
|
} else {
|
|
|
|
|
// Outside: composite highlight on empty canvas
|
|
|
|
|
let dst = [0u8, 0, 0, 0];
|
|
|
|
|
let src = [
|
|
|
|
|
highlight[i * 4],
|
|
|
|
|
highlight[i * 4 + 1],
|
|
|
|
|
highlight[i * 4 + 2],
|
|
|
|
|
sa,
|
|
|
|
|
];
|
|
|
|
|
let blended =
|
|
|
|
|
hcie_blend::blend_pixels(dst, src, hl_bm, hl_op);
|
|
|
|
|
out[i * 4..i * 4 + 4].copy_from_slice(&blended);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Composite shadow
|
|
|
|
|
for i in 0..px {
|
|
|
|
|
let sa = shadow[i * 4 + 3];
|
|
|
|
|
if sa > 0 {
|
|
|
|
|
let is_inner = s.layer_pixels[i * 4 + 3] > 0;
|
|
|
|
|
if !has_outer || is_inner {
|
|
|
|
|
let dst = [
|
|
|
|
|
out[i * 4],
|
|
|
|
|
out[i * 4 + 1],
|
|
|
|
|
out[i * 4 + 2],
|
|
|
|
|
out[i * 4 + 3],
|
|
|
|
|
];
|
|
|
|
|
let src = [
|
|
|
|
|
shadow[i * 4],
|
|
|
|
|
shadow[i * 4 + 1],
|
|
|
|
|
shadow[i * 4 + 2],
|
|
|
|
|
sa,
|
|
|
|
|
];
|
|
|
|
|
let blended =
|
|
|
|
|
hcie_blend::blend_pixels(dst, src, sh_bm, sh_op);
|
|
|
|
|
out[i * 4..i * 4 + 4].copy_from_slice(&blended);
|
|
|
|
|
} else {
|
|
|
|
|
let dst = [
|
|
|
|
|
out[i * 4],
|
|
|
|
|
out[i * 4 + 1],
|
|
|
|
|
out[i * 4 + 2],
|
|
|
|
|
out[i * 4 + 3],
|
|
|
|
|
];
|
|
|
|
|
let src = [
|
|
|
|
|
shadow[i * 4],
|
|
|
|
|
shadow[i * 4 + 1],
|
|
|
|
|
shadow[i * 4 + 2],
|
|
|
|
|
sa,
|
|
|
|
|
];
|
|
|
|
|
let blended =
|
|
|
|
|
hcie_blend::blend_pixels(dst, src, sh_bm, sh_op);
|
|
|
|
|
out[i * 4..i * 4 + 4].copy_from_slice(&blended);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut diff_sum = 0.0f64;
|
|
|
|
|
let mut content_pixels = 0u64;
|
|
|
|
|
for i in 0..px {
|
|
|
|
|
if !s.content_mask[i] {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
content_pixels += 1;
|
|
|
|
|
for c in 0..4 {
|
|
|
|
|
diff_sum +=
|
|
|
|
|
(out[i * 4 + c] as i32 - s.ref_pixels[i * 4 + c] as i32)
|
|
|
|
|
.abs() as f64;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if content_pixels > 0 {
|
|
|
|
|
diff_sum / content_pixels as f64 / 4.0
|
2026-07-13 20:33:39 +03:00
|
|
|
} else {
|
2026-07-16 22:10:22 +03:00
|
|
|
0.0
|
2026-07-13 20:33:39 +03:00
|
|
|
}
|
2026-07-16 22:10:22 +03:00
|
|
|
})
|
|
|
|
|
.sum();
|
2026-07-09 02:59:53 +03:00
|
|
|
|
2026-07-16 22:10:22 +03:00
|
|
|
eval_count += 1;
|
|
|
|
|
let avg_mae = total_mae / samples.len() as f64;
|
2026-07-09 02:59:53 +03:00
|
|
|
|
2026-07-16 22:10:22 +03:00
|
|
|
if avg_mae < best_avg_mae {
|
|
|
|
|
best_avg_mae = avg_mae;
|
|
|
|
|
best_params = (lighting_blur, hl_scale, sh_scale, tilt_scale);
|
|
|
|
|
println!("*** [{}/{}] NEW BEST avg MAE = {:.4} (lb={}, hl={:.3}, sh={:.2}, tilt={:.2})",
|
2026-07-13 20:33:39 +03:00
|
|
|
eval_count, total_combos, best_avg_mae, lighting_blur, hl_scale, sh_scale, tilt_scale);
|
2026-07-16 22:10:22 +03:00
|
|
|
std::io::stdout().flush().ok();
|
|
|
|
|
} else if eval_count % 100 == 0 {
|
|
|
|
|
println!(
|
|
|
|
|
" [{}/{}] avg={:.4} best={:.4}",
|
|
|
|
|
eval_count, total_combos, avg_mae, best_avg_mae
|
|
|
|
|
);
|
|
|
|
|
std::io::stdout().flush().ok();
|
|
|
|
|
}
|
2026-07-09 02:59:53 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 20:33:39 +03:00
|
|
|
let (lb, hl, sh, ts) = best_params;
|
2026-07-09 02:59:53 +03:00
|
|
|
println!("\n=== RESULT ===");
|
|
|
|
|
println!("Best avg MAE = {:.4}", best_avg_mae);
|
|
|
|
|
println!(" lighting_blur = {}", lb);
|
2026-07-13 20:33:39 +03:00
|
|
|
println!(" hl_scale = {:.3}", hl);
|
2026-07-09 02:59:53 +03:00
|
|
|
println!(" sh_scale = {:.2}", sh);
|
2026-07-13 20:33:39 +03:00
|
|
|
println!(" tilt_scale = {:.2}", ts);
|
2026-07-09 02:59:53 +03:00
|
|
|
|
|
|
|
|
if let Ok(mut file) = std::fs::OpenOptions::new()
|
2026-07-16 22:10:22 +03:00
|
|
|
.create(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.truncate(true)
|
|
|
|
|
.open(RESULTS_FILE)
|
2026-07-09 02:59:53 +03:00
|
|
|
{
|
2026-07-16 22:10:22 +03:00
|
|
|
let _ = writeln!(
|
|
|
|
|
file,
|
2026-07-13 20:33:39 +03:00
|
|
|
"lighting_blur={} hl_scale={:.3} sh_scale={:.2} tilt_scale={:.2}\n\
|
2026-07-09 02:59:53 +03:00
|
|
|
avg_mae={:.4} samples={} combos={}",
|
2026-07-16 22:10:22 +03:00
|
|
|
lb,
|
|
|
|
|
hl,
|
|
|
|
|
sh,
|
|
|
|
|
ts,
|
|
|
|
|
best_avg_mae,
|
|
|
|
|
samples.len(),
|
|
|
|
|
total_combos
|
|
|
|
|
);
|
2026-07-09 02:59:53 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_filename(name: &str) -> Option<(String, String, f32, f32, f32, String, f32, String)> {
|
|
|
|
|
// be_{shape}_{style}_d{depth}_sz{size}_sf{soften}_{dir}_alt{altitude}_{technique}
|
|
|
|
|
let parts: Vec<&str> = name.split('_').collect();
|
2026-07-16 22:10:22 +03:00
|
|
|
if parts.len() < 9 || parts[0] != "be" {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
2026-07-09 02:59:53 +03:00
|
|
|
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();
|
2026-07-16 22:10:22 +03:00
|
|
|
Some((
|
|
|
|
|
shape, style, depth, size, soften, direction, altitude, technique,
|
|
|
|
|
))
|
|
|
|
|
}
|