fix: gradient hatch param default; hull viz shows response gradient
gradient_hatch param bug: defaultPass always set param=1.0. The slider rendered pass.param??p.default which resolved to 1.0 regardless of the strategy's declared default (0.25). min_scale=1.0 gives uniform spacing, indistinguishable from parallel_hatch. Fix: PassPanel now sets param=strategy_default when a strategy is selected, so switching to gradient_hatch immediately uses min_scale=0.25. Hull visualization: Was SVG-filled contours with flat hash_color per hull — no gradient info. Now renders a per-pixel JPEG where each hull pixel is colored by its response value: intensity=(255-resp)/255, so darkest ink=full hue, near-threshold pixels fade toward black. The response_map stored in PassState after each detect run is used directly — no extra computation. Background stays dark gray (15,15,15). Falls back to full hue if response_map is empty (pre-process-pass). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -79,7 +79,7 @@ export default function PassPanel({
|
|||||||
<div className="flex flex-wrap gap-1 mb-2">
|
<div className="flex flex-wrap gap-1 mb-2">
|
||||||
{FILL_STRATEGIES.map(s => (
|
{FILL_STRATEGIES.map(s => (
|
||||||
<button key={s}
|
<button key={s}
|
||||||
onClick={() => setFill({ strategy: s })}
|
onClick={() => setFill({ strategy: s, param: FILL_STRATEGY_PARAMS[s]?.default ?? 1.0 })}
|
||||||
className={`px-2 py-0.5 rounded text-xs transition-colors ${
|
className={`px-2 py-0.5 rounded text-xs transition-colors ${
|
||||||
pass.strategy === s
|
pass.strategy === s
|
||||||
? 'bg-purple-700 text-white'
|
? 'bg-purple-700 text-white'
|
||||||
|
|||||||
28
src/lib.rs
28
src/lib.rs
@@ -659,21 +659,25 @@ fn get_pass_viz(pass_index: usize, mode: String, state: State<Mutex<AppState>>)
|
|||||||
|
|
||||||
match mode.as_str() {
|
match mode.as_str() {
|
||||||
"hulls" => {
|
"hulls" => {
|
||||||
let mut svg = format!(
|
// Per-pixel raster: hull hue modulated by response intensity.
|
||||||
r##"<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}"><rect width="{w}" height="{h}" fill="#0f0f0f"/>"##
|
// intensity = (255 - response) / 255: max ink (resp=0) → full hue,
|
||||||
);
|
// threshold edge (resp≈threshold) → near black.
|
||||||
|
let response = &pass.response_map;
|
||||||
|
let mut rgba = vec![15u8; (w * h * 4) as usize];
|
||||||
|
for px in rgba.chunks_mut(4) { px[3] = 255; }
|
||||||
|
|
||||||
for hull in &pass.hulls {
|
for hull in &pass.hulls {
|
||||||
let Some(mut d) = contour_path_d(&hull.contour) else { continue };
|
let (hr, hg, hb) = hash_color(hull.id);
|
||||||
if let Some(holes) = hull_holes.get(&hull.id) {
|
for &(px, py) in &hull.pixels {
|
||||||
for hole in holes {
|
let resp = response.get((py * w + px) as usize).copied().unwrap_or(0);
|
||||||
if let Some(hd) = hole_path_d(hole) { d.push(' '); d.push_str(&hd); }
|
let intensity = (255u32 - resp as u32) as f32 / 255.0;
|
||||||
|
let i = ((py * w + px) * 4) as usize;
|
||||||
|
rgba[i] = (hr as f32 * intensity) as u8;
|
||||||
|
rgba[i+1] = (hg as f32 * intensity) as u8;
|
||||||
|
rgba[i+2] = (hb as f32 * intensity) as u8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let (r, g, b) = hash_color(hull.id);
|
Ok(rgba_to_b64_png(&rgba, w, h))
|
||||||
svg.push_str(&format!(r#"<path d="{d}" fill="rgb({r},{g},{b})" fill-rule="evenodd"/>"#));
|
|
||||||
}
|
|
||||||
svg.push_str("</svg>");
|
|
||||||
Ok(B64.encode(svg.as_bytes()))
|
|
||||||
}
|
}
|
||||||
"contours" => {
|
"contours" => {
|
||||||
let mut svg = format!(
|
let mut svg = format!(
|
||||||
|
|||||||
Reference in New Issue
Block a user