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:
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
28
src/fill.rs
28
src/fill.rs
@@ -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());
|
||||||
|
|||||||
44
src/lib.rs
44
src/lib.rs
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user