fix: DPI slider belongs on Fill cards, not Kernel cards

I misread the previous direction — "DPI on the algos that rasterize
from the hull" was about Fill nodes, not Kernel nodes. Reverted the
Kernel DPI work and put the slider where it actually belongs.

Reverted:
- DetectionLayer.kernel_dpi field
- apply_layer_with_dpi helper
- evaluate_graph's canvas_dpi parameter
- defaultKernelProps.kernel_dpi
- Kernel card DPI slider

Added:
- defaultFillParams.dpi (default 254 = 10 px/mm, what
  FILL_INTERNAL_PX_PER_MM hardcoded before)
- Fill card "Internal DPI" slider, 50–600 step 25
- detect::NodeKind::Fill { ..., dpi: u32 }
- GraphNodePayload.dpi (Option<u32>) — used by Fill, also covers
  Source's dpi for any future backend consumer
- Fingerprint includes fill_dpi so cache invalidates on slider move
- Each mm-fill wrapper takes `internal_px_per_mm` instead of using
  the FILL_INTERNAL_PX_PER_MM constant; dispatcher derives it from
  the per-Fill DPI knob.

Now: Fill cards control their own internal raster resolution, and
each fill can be tuned independently — a hatch fill can stay at 254
DPI while a slow circle_pack runs at 100 DPI.
This commit is contained in:
Mitchell Hansen
2026-05-09 01:05:33 -07:00
parent de8f0ff24d
commit fcc6014ea1
5 changed files with 49 additions and 79 deletions

View File

@@ -621,10 +621,6 @@ export default function NodeGraph({ graph, onChange, nodePreviews, nodeWidth = 2
Invert Invert
</label> </label>
</>)} </>)}
{/* Per-kernel internal DPI: 0 = canvas DPI (no resample),
lower values downsample input for speed. */}
<Slider label="Kernel DPI" value={node.kernel_dpi ?? 0} min={0} max={600} step={25}
onChange={v => updateNode(node.id, { kernel_dpi: v > 0 ? v : null })} />
</>)} </>)}
{node.kind === 'Combine' && (<> {node.kind === 'Combine' && (<>
@@ -702,6 +698,8 @@ export default function NodeGraph({ graph, onChange, nodePreviews, nodeWidth = 2
onChange={v => updateNode(node.id, { param: v })} /> onChange={v => updateNode(node.id, { param: v })} />
) )
})()} })()}
<Slider label="Internal DPI" value={node.dpi ?? 254} min={50} max={600} step={25}
onChange={v => updateNode(node.id, { dpi: v })} />
<Slider label="Smooth RDP" value={node.smooth_rdp ?? 1.0} min={0} max={5} step={0.1} <Slider label="Smooth RDP" value={node.smooth_rdp ?? 1.0} min={0} max={5} step={0.1}
onChange={v => updateNode(node.id, { smooth_rdp: v })} /> onChange={v => updateNode(node.id, { smooth_rdp: v })} />
<Slider label="Chaikin" value={node.smooth_iters ?? 2} min={0} max={4} step={1} <Slider label="Chaikin" value={node.smooth_iters ?? 2} min={0} max={4} step={1}

View File

@@ -59,9 +59,6 @@ export function defaultKernelProps() {
xdog_sigma2: 1.6, xdog_tau: 0.98, xdog_phi: 10.0, xdog_sigma2: 1.6, xdog_tau: 0.98, xdog_phi: 10.0,
color_filter: buildColorIsolateFilter(ci_color, ci_hue_tolerance, ci_sat_min, ci_val_min), color_filter: buildColorIsolateFilter(ci_color, ci_hue_tolerance, ci_sat_min, ci_val_min),
ci_color, ci_hue_tolerance, ci_sat_min, ci_val_min, ci_color, ci_hue_tolerance, ci_sat_min, ci_val_min,
// null = run at canvas DPI (= max source DPI). Lower values
// downsample the kernel's input internally for speed.
kernel_dpi: null,
} }
} }
@@ -85,11 +82,13 @@ export function defaultColorFilter() {
} }
export function defaultFillParams() { export function defaultFillParams() {
// spacing is in mm (DPI-independent, paper-relative). Backend converts // spacing is in mm (DPI-independent, paper-relative).
// to pipeline pixels via canvas_w / paper_w_mm at process time. // dpi controls the internal raster resolution this fill uses to
// sample the hull polygon — higher = smoother fill paths, slower.
return { return {
strategy: 'hatch', spacing: 2.0, angle: 0, param: 1.0, strategy: 'hatch', spacing: 2.0, angle: 0, param: 1.0,
smooth_rdp: 1.0, smooth_iters: 2, smooth_rdp: 1.0, smooth_iters: 2,
dpi: 254, // 10 px/mm — finer than any plotter resolves
} }
} }

View File

@@ -56,12 +56,6 @@ pub struct DetectionLayer {
pub ci_hue_min: f32, pub ci_hue_max: f32, pub ci_hue_min: f32, pub ci_hue_max: f32,
pub ci_sat_min: f32, pub ci_sat_max: f32, pub ci_sat_min: f32, pub ci_sat_max: f32,
pub ci_val_min: f32, pub ci_val_max: f32, pub ci_val_min: f32, pub ci_val_max: f32,
/// Per-kernel internal DPI. None = use canvas DPI. If set lower
/// than canvas DPI, the kernel's input gets downsampled to that
/// resolution before applying the layer (faster for slow kernels
/// like Canny on big images), then upsampled back to canvas dims
/// so downstream nodes see a consistent map.
pub kernel_dpi: Option<u32>,
} }
impl Default for DetectionLayer { impl Default for DetectionLayer {
@@ -80,37 +74,10 @@ impl Default for DetectionLayer {
ci_hue_min: 0.0, ci_hue_max: 360.0, ci_hue_min: 0.0, ci_hue_max: 360.0,
ci_sat_min: 0.0, ci_sat_max: 1.0, ci_sat_min: 0.0, ci_sat_max: 1.0,
ci_val_min: 0.0, ci_val_max: 1.0, ci_val_min: 0.0, ci_val_max: 1.0,
kernel_dpi: None,
} }
} }
} }
/// Apply a layer with optional DPI downsampling. If `layer.kernel_dpi`
/// is set lower than `canvas_dpi`, the input is downsampled to that
/// resolution before applying the kernel and the result upsampled back
/// to the input's dimensions — same output shape, kernel works on a
/// smaller image internally.
pub fn apply_layer_with_dpi(rgb: &RgbImage, layer: &DetectionLayer, canvas_dpi: f32) -> Vec<u8> {
if let Some(kdpi) = layer.kernel_dpi {
let kdpi = kdpi.max(1) as f32;
if kdpi + 0.5 < canvas_dpi {
let ratio = kdpi / canvas_dpi;
let new_w = (rgb.width() as f32 * ratio).round().max(1.0) as u32;
let new_h = (rgb.height() as f32 * ratio).round().max(1.0) as u32;
let small = image::DynamicImage::ImageRgb8(rgb.clone())
.resize_exact(new_w, new_h, image::imageops::FilterType::Triangle)
.to_rgb8();
let small_resp = apply_layer(&small, layer);
let small_gray = image::GrayImage::from_raw(new_w, new_h, small_resp).expect("gray buf");
let up = image::DynamicImage::ImageLuma8(small_gray)
.resize_exact(rgb.width(), rgb.height(), image::imageops::FilterType::Triangle)
.to_luma8();
return up.into_raw();
}
}
apply_layer(rgb, layer)
}
/// Ordered stack of detection layers combined by weighted average. /// Ordered stack of detection layers combined by weighted average.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DetectionParams { pub struct DetectionParams {
@@ -523,6 +490,9 @@ pub enum NodeKind {
param: f32, param: f32,
smooth_rdp: f32, smooth_rdp: f32,
smooth_iters: u32, smooth_iters: u32,
/// Internal raster resolution (DPI) the fill uses when sampling
/// the hull polygon. Higher = smoother fill paths, slower.
dpi: u32,
}, },
PenOutput { PenOutput {
color: [u8; 3], color: [u8; 3],
@@ -583,7 +553,6 @@ pub fn evaluate_graph(
node_rgbs: &std::collections::HashMap<String, RgbImage>, node_rgbs: &std::collections::HashMap<String, RgbImage>,
canvas_w: u32, canvas_w: u32,
canvas_h: u32, canvas_h: u32,
canvas_dpi: f32,
) -> GraphMaps { ) -> GraphMaps {
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
@@ -658,9 +627,9 @@ pub fn evaluate_graph(
let v = up[(y * src_rgb.width() + x) as usize]; let v = up[(y * src_rgb.width() + x) as usize];
image::Rgb([v, v, v]) image::Rgb([v, v, v])
}); });
apply_layer_with_dpi(&gray_rgb, layer, canvas_dpi) apply_layer(&gray_rgb, layer)
} else { } else {
apply_layer_with_dpi(src_rgb, layer, canvas_dpi) apply_layer(src_rgb, layer)
}; };
let w = layer.weight; let w = layer.weight;
Some(if (w - 1.0).abs() < 1e-6 { Some(if (w - 1.0).abs() < 1e-6 {

View File

@@ -119,10 +119,12 @@ pub fn outline_mm(mm: &MmHull) -> FillResult {
/// Wrapper macro: rasterize → run a pixel-space fill that takes /// Wrapper macro: rasterize → run a pixel-space fill that takes
/// `(hull, spacing_px)` → convert strokes back to mm. /// `(hull, spacing_px)` → convert strokes back to mm.
/// `internal_px_per_mm` is the per-Fill DPI knob — higher = smoother
/// fill paths at the cost of more allocation + compute.
macro_rules! mm_wrap_simple { macro_rules! mm_wrap_simple {
($name:ident, $px_fn:ident) => { ($name:ident, $px_fn:ident) => {
pub fn $name(mm: &MmHull, spacing_mm: f32) -> FillResult { pub fn $name(mm: &MmHull, spacing_mm: f32, internal_px_per_mm: f32) -> FillResult {
let s = FILL_INTERNAL_PX_PER_MM; let s = internal_px_per_mm.max(1.0);
let h = rasterize_mm_hull(mm, s); let h = rasterize_mm_hull(mm, s);
let spacing_px = (spacing_mm * s).max(0.5); let spacing_px = (spacing_mm * s).max(0.5);
let r = $px_fn(&h, spacing_px); let r = $px_fn(&h, spacing_px);
@@ -138,8 +140,8 @@ mm_wrap_simple!(hilbert_fill_mm, hilbert_fill);
/// Hatch + zigzag share the (spacing, angle) signature. /// Hatch + zigzag share the (spacing, angle) signature.
macro_rules! mm_wrap_hatch { macro_rules! mm_wrap_hatch {
($name:ident, $px_fn:ident) => { ($name:ident, $px_fn:ident) => {
pub fn $name(mm: &MmHull, spacing_mm: f32, angle_deg: f32) -> FillResult { pub fn $name(mm: &MmHull, spacing_mm: f32, angle_deg: f32, internal_px_per_mm: f32) -> FillResult {
let s = FILL_INTERNAL_PX_PER_MM; let s = internal_px_per_mm.max(1.0);
let h = rasterize_mm_hull(mm, s); let h = rasterize_mm_hull(mm, s);
let spacing_px = (spacing_mm * s).max(0.5); let spacing_px = (spacing_mm * s).max(0.5);
let r = $px_fn(&h, spacing_px, angle_deg); let r = $px_fn(&h, spacing_px, angle_deg);
@@ -150,24 +152,24 @@ macro_rules! mm_wrap_hatch {
mm_wrap_hatch!(parallel_hatch_mm, parallel_hatch); mm_wrap_hatch!(parallel_hatch_mm, parallel_hatch);
mm_wrap_hatch!(zigzag_hatch_mm, zigzag_hatch); mm_wrap_hatch!(zigzag_hatch_mm, zigzag_hatch);
pub fn circle_pack_mm(mm: &MmHull, spacing_mm: f32, min_radius_factor: f32) -> FillResult { pub fn circle_pack_mm(mm: &MmHull, spacing_mm: f32, min_radius_factor: f32, internal_px_per_mm: f32) -> FillResult {
let s = FILL_INTERNAL_PX_PER_MM; let s = internal_px_per_mm.max(1.0);
let h = rasterize_mm_hull(mm, s); let h = rasterize_mm_hull(mm, s);
let spacing_px = (spacing_mm * s).max(0.5); let spacing_px = (spacing_mm * s).max(0.5);
let r = circle_pack(&h, spacing_px, min_radius_factor); let r = circle_pack(&h, spacing_px, min_radius_factor);
FillResult { hull_id: mm.id, strokes: px_strokes_to_mm(r.strokes, s) } FillResult { hull_id: mm.id, strokes: px_strokes_to_mm(r.strokes, s) }
} }
pub fn wave_interference_mm(mm: &MmHull, spacing_mm: f32, num_sources: usize) -> FillResult { pub fn wave_interference_mm(mm: &MmHull, spacing_mm: f32, num_sources: usize, internal_px_per_mm: f32) -> FillResult {
let s = FILL_INTERNAL_PX_PER_MM; let s = internal_px_per_mm.max(1.0);
let h = rasterize_mm_hull(mm, s); let h = rasterize_mm_hull(mm, s);
let spacing_px = (spacing_mm * s).max(0.5); let spacing_px = (spacing_mm * s).max(0.5);
let r = wave_interference(&h, spacing_px, num_sources); let r = wave_interference(&h, spacing_px, num_sources);
FillResult { hull_id: mm.id, strokes: px_strokes_to_mm(r.strokes, s) } FillResult { hull_id: mm.id, strokes: px_strokes_to_mm(r.strokes, s) }
} }
pub fn flow_field_mm(mm: &MmHull, spacing_mm: f32, angle_deg: f32, amplitude_scale: f32) -> FillResult { pub fn flow_field_mm(mm: &MmHull, spacing_mm: f32, angle_deg: f32, amplitude_scale: f32, internal_px_per_mm: f32) -> FillResult {
let s = FILL_INTERNAL_PX_PER_MM; let s = internal_px_per_mm.max(1.0);
let h = rasterize_mm_hull(mm, s); let h = rasterize_mm_hull(mm, s);
let spacing_px = (spacing_mm * s).max(0.5); let spacing_px = (spacing_mm * s).max(0.5);
let r = flow_field(&h, spacing_px, angle_deg, amplitude_scale); let r = flow_field(&h, spacing_px, angle_deg, amplitude_scale);
@@ -1351,7 +1353,7 @@ mod tests {
#[test] #[test]
fn parallel_hatch_mm_strokes_are_in_mm_range() { fn parallel_hatch_mm_strokes_are_in_mm_range() {
let mm = rect_mm_hull(50.0, 30.0); let mm = rect_mm_hull(50.0, 30.0);
let r = parallel_hatch_mm(&mm, 5.0, 0.0); let r = parallel_hatch_mm(&mm, 5.0, 0.0, FILL_INTERNAL_PX_PER_MM);
assert!(!r.strokes.is_empty()); assert!(!r.strokes.is_empty());
for stroke in &r.strokes { for stroke in &r.strokes {
for &(x, y) in stroke { for &(x, y) in stroke {
@@ -1366,8 +1368,8 @@ mod tests {
// Same 50×30 mm rect built at 150 DPI vs 600 DPI should produce // Same 50×30 mm rect built at 150 DPI vs 600 DPI should produce
// matching mm strokes — the wrapper rasterizes at a fixed internal // matching mm strokes — the wrapper rasterizes at a fixed internal
// resolution, so source DPI is decoupled from output. // resolution, so source DPI is decoupled from output.
let r150 = parallel_hatch_mm(&pixel_rect_to_mm_hull(150.0), 5.0, 0.0); let r150 = parallel_hatch_mm(&pixel_rect_to_mm_hull(150.0), 5.0, 0.0, FILL_INTERNAL_PX_PER_MM);
let r600 = parallel_hatch_mm(&pixel_rect_to_mm_hull(600.0), 5.0, 0.0); let r600 = parallel_hatch_mm(&pixel_rect_to_mm_hull(600.0), 5.0, 0.0, FILL_INTERNAL_PX_PER_MM);
assert_eq!(r150.strokes.len(), r600.strokes.len(), assert_eq!(r150.strokes.len(), r600.strokes.len(),
"stroke count differs across DPI: 150→{}, 600→{}", "stroke count differs across DPI: 150→{}, 600→{}",
r150.strokes.len(), r600.strokes.len()); r150.strokes.len(), r600.strokes.len());

View File

@@ -83,7 +83,6 @@ fn fp_kernel(node: &GraphNodePayload, upstream_fp: u64) -> u64 {
h.write_u32(cf.sat_min.to_bits()); h.write_u32(cf.sat_max.to_bits()); h.write_u32(cf.sat_min.to_bits()); h.write_u32(cf.sat_max.to_bits());
h.write_u32(cf.val_min.to_bits()); h.write_u32(cf.val_max.to_bits()); h.write_u32(cf.val_min.to_bits()); h.write_u32(cf.val_max.to_bits());
} }
h.write_u32(node.kernel_dpi.unwrap_or(0));
h.finish() h.finish()
} }
fn fp_combine(node: &GraphNodePayload, upstream_fps: &[u64]) -> u64 { fn fp_combine(node: &GraphNodePayload, upstream_fps: &[u64]) -> u64 {
@@ -115,6 +114,7 @@ fn fp_fill(node: &GraphNodePayload, upstream_fp: u64) -> u64 {
h.write_u32(node.param.unwrap_or(1.0).to_bits()); h.write_u32(node.param.unwrap_or(1.0).to_bits());
h.write_u32(node.smooth_rdp.unwrap_or(1.0).to_bits()); h.write_u32(node.smooth_rdp.unwrap_or(1.0).to_bits());
h.write_u32(node.smooth_iters.unwrap_or(2)); h.write_u32(node.smooth_iters.unwrap_or(2));
h.write_u32(node.dpi.unwrap_or(254));
h.finish() h.finish()
} }
fn fp_pen(node: &GraphNodePayload, upstream_fp: u64, pen_tip_mm: Option<f32>) -> u64 { fn fp_pen(node: &GraphNodePayload, upstream_fp: u64, pen_tip_mm: Option<f32>) -> u64 {
@@ -250,7 +250,6 @@ pub struct GraphNodePayload {
pub xdog_phi: Option<f32>, pub xdog_phi: Option<f32>,
// Combine params (optional) // Combine params (optional)
pub blend_mode: Option<String>, pub blend_mode: Option<String>,
pub kernel_dpi: Option<u32>,
// Hull params (optional — only for kind="Hull") // Hull params (optional — only for kind="Hull")
pub threshold: Option<u8>, pub threshold: Option<u8>,
pub min_area: Option<u32>, pub min_area: Option<u32>,
@@ -263,6 +262,10 @@ pub struct GraphNodePayload {
pub param: Option<f32>, pub param: Option<f32>,
pub smooth_rdp: Option<f32>, pub smooth_rdp: Option<f32>,
pub smooth_iters: Option<u32>, pub smooth_iters: Option<u32>,
/// Per-node internal DPI. Used by Fill (sets the rasterisation
/// resolution when sampling the hull polygon) and by Source
/// (project canvas DPI, surfaced via the App-level max).
pub dpi: Option<u32>,
// PenOutput params (optional — only for kind="PenOutput") // PenOutput params (optional — only for kind="PenOutput")
pub pen_color: Option<Vec<u8>>, // [r, g, b] pub pen_color: Option<Vec<u8>>, // [r, g, b]
pub pen_label: Option<String>, pub pen_label: Option<String>,
@@ -436,7 +439,6 @@ fn to_detection_graph(payload: &DetectionGraphPayload) -> detect::DetectionGraph
ci_sat_max: cf.map(|f| f.sat_max).unwrap_or(1.0), ci_sat_max: cf.map(|f| f.sat_max).unwrap_or(1.0),
ci_val_min: cf.map(|f| f.val_min).unwrap_or(0.0), ci_val_min: cf.map(|f| f.val_min).unwrap_or(0.0),
ci_val_max: cf.map(|f| f.val_max).unwrap_or(1.0), ci_val_max: cf.map(|f| f.val_max).unwrap_or(1.0),
kernel_dpi: n.kernel_dpi,
}) })
} }
"Combine" => { "Combine" => {
@@ -462,6 +464,7 @@ fn to_detection_graph(payload: &DetectionGraphPayload) -> detect::DetectionGraph
param: n.param.unwrap_or(1.0), param: n.param.unwrap_or(1.0),
smooth_rdp: n.smooth_rdp.unwrap_or(1.0), smooth_rdp: n.smooth_rdp.unwrap_or(1.0),
smooth_iters: n.smooth_iters.unwrap_or(2), smooth_iters: n.smooth_iters.unwrap_or(2),
dpi: n.dpi.unwrap_or(254),
}, },
"Text" => detect::NodeKind::Text { "Text" => detect::NodeKind::Text {
text: n.text.clone().unwrap_or_default(), text: n.text.clone().unwrap_or_default(),
@@ -781,8 +784,7 @@ fn process_pass_work(
} }
} else { } else {
cache_misses += 1; cache_misses += 1;
let maps = detect::evaluate_graph(&det_graph, &node_rgbs, canvas_w, canvas_h, let maps = detect::evaluate_graph(&det_graph, &node_rgbs, canvas_w, canvas_h);
payload.dpi.unwrap_or(150) as f32);
cache.detect_fp = detect_fp; cache.detect_fp = detect_fp;
cache.detect_response = maps.response.clone(); cache.detect_response = maps.response.clone();
cache.detect_maps = maps.raw_maps.clone(); cache.detect_maps = maps.raw_maps.clone();
@@ -936,7 +938,7 @@ fn process_pass_work(
for node in &det_graph.nodes { for node in &det_graph.nodes {
if let detect::NodeKind::Fill { if let detect::NodeKind::Fill {
strategy, spacing, angle, param, smooth_rdp, smooth_iters strategy, spacing, angle, param, smooth_rdp, smooth_iters, dpi: fill_dpi
} = &node.kind { } = &node.kind {
let upstream = det_graph.edges.iter().find(|e| e.to == node.id && e.port == 0); let upstream = det_graph.edges.iter().find(|e| e.to == node.id && e.port == 0);
let (hulls_for_fill, resp_for_fill) = match upstream { let (hulls_for_fill, resp_for_fill) = match upstream {
@@ -972,10 +974,11 @@ fn process_pass_work(
} }
// Compute. Convert pixel hulls to mm hulls once; mm fills handle // Compute. Convert pixel hulls to mm hulls once; mm fills handle
// their own internal rasterization at FILL_INTERNAL_PX_PER_MM. // their own internal rasterization at the per-Fill `dpi` knob.
let response_arc: std::sync::Arc<[u8]> = resp_for_fill.into(); let response_arc: std::sync::Arc<[u8]> = resp_for_fill.into();
let (strategy, spacing_mm, angle, param, smooth_rdp, smooth_iters) = let (strategy, spacing_mm, angle, param, smooth_rdp, smooth_iters, fill_dpi) =
(strategy.clone(), *spacing, *angle, *param, *smooth_rdp, *smooth_iters); (strategy.clone(), *spacing, *angle, *param, *smooth_rdp, *smooth_iters, *fill_dpi);
let internal_px_per_mm = (fill_dpi as f32 / 25.4).max(1.0);
let mm_hulls: Vec<hulls::MmHull> = hulls_for_fill.iter() let mm_hulls: Vec<hulls::MmHull> = hulls_for_fill.iter()
.map(|h| hulls::MmHull::from_pixel_hull(h, px_per_mm_fill)) .map(|h| hulls::MmHull::from_pixel_hull(h, px_per_mm_fill))
.collect(); .collect();
@@ -984,17 +987,17 @@ fn process_pass_work(
let raw: Vec<fill::FillResult> = mm_hulls.par_iter().map(|mh| { let raw: Vec<fill::FillResult> = mm_hulls.par_iter().map(|mh| {
match strategy.as_str() { match strategy.as_str() {
"outline" => fill::outline_mm(mh), "outline" => fill::outline_mm(mh),
"zigzag" => fill::zigzag_hatch_mm(mh, spacing_mm, angle), "zigzag" => fill::zigzag_hatch_mm(mh, spacing_mm, angle, internal_px_per_mm),
"offset" => fill::contour_offset_mm(mh, spacing_mm), "offset" => fill::contour_offset_mm(mh, spacing_mm, internal_px_per_mm),
"spiral" => fill::spiral_mm(mh, spacing_mm), "spiral" => fill::spiral_mm(mh, spacing_mm, internal_px_per_mm),
"circles" => fill::circle_pack_mm(mh, spacing_mm, param.max(0.1)), "circles" => fill::circle_pack_mm(mh, spacing_mm, param.max(0.1), internal_px_per_mm),
"voronoi" => fill::voronoi_fill_mm(mh, spacing_mm), "voronoi" => fill::voronoi_fill_mm(mh, spacing_mm, internal_px_per_mm),
"hilbert" => fill::hilbert_fill_mm(mh, spacing_mm), "hilbert" => fill::hilbert_fill_mm(mh, spacing_mm, internal_px_per_mm),
"waves" => fill::wave_interference_mm(mh, spacing_mm, param.round().max(1.0) as usize), "waves" => fill::wave_interference_mm(mh, spacing_mm, param.round().max(1.0) as usize, internal_px_per_mm),
"flow" => fill::flow_field_mm(mh, spacing_mm, angle, param.max(0.0)), "flow" => fill::flow_field_mm(mh, spacing_mm, angle, param.max(0.0), internal_px_per_mm),
"gradient_hatch" => fill::gradient_hatch_mm(mh, &response_arc, img_w, src_px_per_mm, spacing_mm, angle, param.clamp(0.05, 1.0)), "gradient_hatch" => fill::gradient_hatch_mm(mh, &response_arc, img_w, src_px_per_mm, spacing_mm, angle, param.clamp(0.05, 1.0)),
"gradient_cross_hatch" => fill::gradient_cross_hatch_mm(mh, &response_arc, img_w, src_px_per_mm, spacing_mm, angle, param.clamp(0.05, 1.0)), "gradient_cross_hatch" => fill::gradient_cross_hatch_mm(mh, &response_arc, img_w, src_px_per_mm, spacing_mm, angle, param.clamp(0.05, 1.0)),
_ => fill::parallel_hatch_mm(mh, spacing_mm, angle), _ => fill::parallel_hatch_mm(mh, spacing_mm, angle, internal_px_per_mm),
} }
}).collect(); }).collect();
let smoothed: Vec<fill::FillResult> = raw.iter() let smoothed: Vec<fill::FillResult> = raw.iter()
@@ -2343,11 +2346,10 @@ mod blocking_tests {
sat_min_value: None, canny_low: None, canny_high: None, sat_min_value: None, canny_low: None, canny_high: None,
xdog_sigma2: None, xdog_tau: None, xdog_phi: None, xdog_sigma2: None, xdog_tau: None, xdog_phi: None,
blend_mode: None, blend_mode: None,
kernel_dpi: None,
threshold: None, min_area: None, threshold: None, min_area: None,
connectivity: None, color_filter: None, connectivity: None, color_filter: None,
strategy: None, spacing: None, angle: None, param: None, strategy: None, spacing: None, angle: None, param: None,
smooth_rdp: None, smooth_iters: None, smooth_rdp: None, smooth_iters: None, dpi: None,
pen_color: None, pen_label: None, pen_order: None, pen_color: None, pen_label: None, pen_order: None,
file_path: None, file_path: None,
text: None, font: None, font_size_mm: None, line_spacing_mm: None, text: None, font: None, font_size_mm: None, line_spacing_mm: None,
@@ -2730,7 +2732,7 @@ mod viz_tests {
}; };
let node_rgbs: std::collections::HashMap<String, image::RgbImage> = let node_rgbs: std::collections::HashMap<String, image::RgbImage> =
graph.nodes.iter().map(|n| (n.id.clone(), img.clone())).collect(); graph.nodes.iter().map(|n| (n.id.clone(), img.clone())).collect();
let gm = detect::evaluate_graph(&graph, &node_rgbs, w, h, 150.0); let gm = detect::evaluate_graph(&graph, &node_rgbs, w, h);
let response = gm.raw_maps.get("hull").cloned().unwrap_or(gm.response); let response = gm.raw_maps.get("hull").cloned().unwrap_or(gm.response);
let params = HullParams { threshold, min_area, let params = HullParams { threshold, min_area,
connectivity: hulls::Connectivity::Four }; connectivity: hulls::Connectivity::Four };