sources at native dims — no project DPI, no canvas letterbox
Removes the canvas-letterbox-to-paper step from process_pass. Each Source image now flows through the pipeline at its native pixel dims. Kernels run at source dims, Hull extracts at source dims, MmHull conversion uses per-source `source.width_px / img_w_mm` for px/mm. Project DPI is gone: - Source card no longer has a DPI slider — kernels just operate at whatever pixel grid the source happens to have. - App.jsx no longer computes max(source.dpi) for the payload. - payload.dpi is unused (kept on the struct for back-compat with old saved projects; backend ignores it). Backend changes: - evaluate_graph drops canvas_w/canvas_h params; bg() looks up each node's source dims via node_rgbs to size empty maps. Combine's blend_maps n is the input map's length. - process_pass_work drops canvas_w/canvas_h; computes per-source px_per_mm via source_rgbs map. Hull extraction uses each Hull's source RGB dims. MmHull conversion uses per-Hull px_per_mm. - Coverage viz_b64 sized from first Hull's source dims (was the global canvas dims). - Pen radius for preview computed inside render_pen_preview from pen_tip_mm and PREVIEW_PX_PER_MM (already DPI-independent). Removed: - Source's letterbox-to-canvas loop in process_pass (~30 lines). - Synthetic blank canvas for text-only graphs (text strokes are mm, no backing pixels needed). - defaultSourceParams.dpi - App.jsx projectDpi calculation. Behaviour: dropping a 4000×3000 photo into a Source now processes at 4000×3000 px directly. Kernels see actual image detail. Hull contour vertex count tracks source resolution (intrinsic to the input file, not a project knob). Fill and Pen output are unchanged — they're mm. Tests: cargo test --lib (85 pass), npm test (70 pass), both builds clean.
This commit is contained in:
@@ -152,19 +152,13 @@ export default function App() {
|
||||
updatePass(idx, { status: 'Processing…', vizB64: null, hullCount: 0, strokeCount: 0 })
|
||||
const t0 = performance.now()
|
||||
try {
|
||||
// Backend letterboxes every Source into the paper canvas, so we hand
|
||||
// it the paper dimensions directly — no per-image scaling knob anymore.
|
||||
// DPI lives per-Source now; the project canvas runs at the highest
|
||||
// source DPI so no source loses detail. Default 150 if no Source.
|
||||
// No more project DPI. Source images process at native pixel
|
||||
// dims; mm conversion happens at the polygon boundary.
|
||||
const paperW = gcodeConfigRef.current.paper_w_mm
|
||||
const paperH = gcodeConfigRef.current.paper_h_mm
|
||||
const projectDpi = Math.max(150, ...(pass.graph?.nodes ?? [])
|
||||
.filter(n => n.kind === 'Source')
|
||||
.map(n => n.dpi ?? 150))
|
||||
const result = await tauri.processPass({
|
||||
pass_index: idx,
|
||||
graph: pass.graph,
|
||||
dpi: projectDpi,
|
||||
img_w_mm: paperW,
|
||||
img_h_mm: paperH,
|
||||
pen_tip_mm: gcodeConfigRef.current.pen_tip_mm ?? 0.5,
|
||||
|
||||
@@ -557,8 +557,6 @@ export default function NodeGraph({ graph, onChange, nodePreviews, nodeWidth = 2
|
||||
{node.file_path}
|
||||
</div>
|
||||
)}
|
||||
<Slider label="Sampling DPI" value={node.dpi ?? 150} min={50} max={600} step={25}
|
||||
onChange={v => updateNode(node.id, { dpi: v })} />
|
||||
</>)}
|
||||
|
||||
{node.kind === 'Kernel' && (<>
|
||||
|
||||
@@ -127,12 +127,12 @@ export function defaultHullParams() {
|
||||
}
|
||||
|
||||
export function defaultSourceParams() {
|
||||
// dpi is the source's sampling resolution: how many pixels per mm of
|
||||
// paper the source image gets letterboxed into. Higher = finer detail
|
||||
// captured from the source. With multi-source projects, the project
|
||||
// canvas runs at max(source.dpi) so the highest-detail source isn't
|
||||
// limited by the lowest.
|
||||
return { file_path: null, dpi: 150 }
|
||||
// The source image is processed at its native pixel dims. No
|
||||
// resampling, no project-wide DPI. Kernels and Hull operate on the
|
||||
// source's own pixel grid; mm conversion happens at the polygon
|
||||
// boundary using `source.width / img_w_mm` as the per-source
|
||||
// pixels-per-mm rate.
|
||||
return { file_path: null }
|
||||
}
|
||||
|
||||
export function defaultGraph() {
|
||||
|
||||
@@ -543,24 +543,29 @@ pub struct GraphMaps {
|
||||
pub raw_maps: std::collections::HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
/// Run the detection graph. With multi-source graphs, every Kernel node
|
||||
/// is tied to its tree's Source via `node_rgbs`; the lookup happens per
|
||||
/// Kernel evaluation. `canvas_w/canvas_h` give the pixel dimensions for
|
||||
/// the response maps — every Source has been letterboxed to this size
|
||||
/// upstream so all maps share a coord frame.
|
||||
/// Run the detection graph. With multi-source graphs, every Kernel
|
||||
/// node is tied to its tree's Source via `node_rgbs`; the lookup
|
||||
/// happens per Kernel evaluation, and each kernel processes at its
|
||||
/// source image's native pixel dims. Response maps for each node are
|
||||
/// at that node's source dims (so a single graph can have nodes at
|
||||
/// different resolutions if their sources differ).
|
||||
pub fn evaluate_graph(
|
||||
graph: &DetectionGraph,
|
||||
node_rgbs: &std::collections::HashMap<String, RgbImage>,
|
||||
canvas_w: u32,
|
||||
canvas_h: u32,
|
||||
) -> GraphMaps {
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
let n = (canvas_w * canvas_h) as usize;
|
||||
let bg = || vec![255u8; n];
|
||||
// Empty-map helper sized to a given node's source RGB; for nodes
|
||||
// with no source (rare — e.g. orphan Combine/Output) returns empty.
|
||||
let bg_for = |id: &str| -> Vec<u8> {
|
||||
match node_rgbs.get(id) {
|
||||
Some(rgb) => vec![255u8; (rgb.width() * rgb.height()) as usize],
|
||||
None => Vec::new(),
|
||||
}
|
||||
};
|
||||
|
||||
if graph.nodes.is_empty() {
|
||||
return GraphMaps { response: bg(), raw_maps: HashMap::new() };
|
||||
return GraphMaps { response: Vec::new(), raw_maps: HashMap::new() };
|
||||
}
|
||||
|
||||
// Build adjacency: incoming edges per node (sorted by port for combine)
|
||||
@@ -594,7 +599,7 @@ pub fn evaluate_graph(
|
||||
}
|
||||
}
|
||||
if order.len() != graph.nodes.len() {
|
||||
return GraphMaps { response: bg(), raw_maps: HashMap::new() }; // cycle
|
||||
return GraphMaps { response: Vec::new(), raw_maps: HashMap::new() }; // cycle
|
||||
}
|
||||
|
||||
let node_map: HashMap<&str, &GraphNode> = graph.nodes.iter()
|
||||
@@ -614,7 +619,7 @@ pub fn evaluate_graph(
|
||||
// this kernel operates on. Missing entry = source has no file
|
||||
// loaded; produce nothing.
|
||||
let src_rgb = match node_rgbs.get(id) { Some(r) => r, None => {
|
||||
outputs.insert(id, bg()); continue;
|
||||
outputs.insert(id, bg_for(id)); continue;
|
||||
}};
|
||||
// If an upstream response map exists (e.g. from a Combine node),
|
||||
// convert it to a grayscale RgbImage and apply the kernel to that
|
||||
@@ -646,12 +651,13 @@ pub fn evaluate_graph(
|
||||
let maps: Vec<&[u8]> = ins.iter()
|
||||
.filter_map(|(fid, _)| outputs.get(fid).map(|v| v.as_slice()))
|
||||
.collect();
|
||||
Some(if maps.is_empty() { bg() } else { blend_maps(&maps, *mode, n) })
|
||||
let n = maps.first().map(|m| m.len()).unwrap_or(0);
|
||||
Some(if maps.is_empty() { bg_for(id) } else { blend_maps(&maps, *mode, n) })
|
||||
}
|
||||
NodeKind::Output => {
|
||||
let upstream = incoming[id].iter()
|
||||
.find_map(|(fid, _)| outputs.get(fid).cloned());
|
||||
Some(upstream.unwrap_or_else(bg))
|
||||
Some(upstream.unwrap_or_else(|| bg_for(id)))
|
||||
}
|
||||
// Hull nodes store their upstream Map so lib.rs can retrieve it for
|
||||
// hull extraction without re-evaluating the detection portion.
|
||||
@@ -689,7 +695,7 @@ pub fn evaluate_graph(
|
||||
.find(|n| matches!(n.kind, NodeKind::Hull { .. }))
|
||||
.and_then(|n| raw_maps.get(&n.id).cloned())
|
||||
})
|
||||
.unwrap_or_else(bg);
|
||||
.unwrap_or_default();
|
||||
|
||||
GraphMaps { response, raw_maps }
|
||||
}
|
||||
|
||||
128
src/lib.rs
128
src/lib.rs
@@ -686,8 +686,6 @@ fn render_hull_preview(response: &[u8], hulls_list: &[hulls::Hull],
|
||||
|
||||
fn process_pass_work(
|
||||
source_rgbs: std::collections::HashMap<String, image::RgbImage>,
|
||||
canvas_w: u32,
|
||||
canvas_h: u32,
|
||||
payload: ProcessPassPayload,
|
||||
mut cache: NodeCache,
|
||||
) -> (Vec<hulls::Hull>, Vec<PenResult>, Vec<u8>, ProcessResult, NodeCache) {
|
||||
@@ -698,17 +696,18 @@ fn process_pass_work(
|
||||
let mut cache_hits = 0u32;
|
||||
let mut cache_misses = 0u32;
|
||||
|
||||
// Each Source is already letterboxed to the paper canvas by the caller,
|
||||
// so all of them share `(canvas_w, canvas_h)` and downstream hulls land
|
||||
// in a single coord frame.
|
||||
let (w, h) = (canvas_w, canvas_h);
|
||||
|
||||
// Paper dims (mm) — shared by mm-fill conversion, preview rendering,
|
||||
// and gradient-fill source sampling. Hoisted so every stage uses the
|
||||
// same numbers.
|
||||
// Paper dims (mm). The image's mm width on paper is `paper_w_mm_for_scale`
|
||||
// (= img_w_mm payload, defaulted to A4); per-source pixels-per-mm is
|
||||
// computed below from each source's native dims.
|
||||
let paper_w_mm_for_scale = payload.img_w_mm.unwrap_or(210.0).max(1.0);
|
||||
let paper_h_mm_for_scale = payload.img_h_mm.unwrap_or(297.0).max(1.0);
|
||||
let px_per_mm_fill = w as f32 / paper_w_mm_for_scale;
|
||||
|
||||
// Per-source pixels-per-mm: divides each source's native width by its
|
||||
// mm width on paper. Used by Hull → MmHull conversion and gradient
|
||||
// fills. Each source has its own rate.
|
||||
let source_px_per_mm: std::collections::HashMap<String, f32> = source_rgbs.iter()
|
||||
.map(|(id, rgb)| (id.clone(), rgb.width() as f32 / paper_w_mm_for_scale))
|
||||
.collect();
|
||||
|
||||
// ── Per-node Source RGB lookup ────────────────────────────────────────────
|
||||
// Trees can't merge (frontend enforces; backend assumes), so each non-
|
||||
@@ -784,7 +783,7 @@ fn process_pass_work(
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
cache.detect_fp = detect_fp;
|
||||
cache.detect_response = maps.response.clone();
|
||||
cache.detect_maps = maps.raw_maps.clone();
|
||||
@@ -820,6 +819,11 @@ fn process_pass_work(
|
||||
| detect::NodeKind::PenOutput { .. }
|
||||
));
|
||||
if !is_detect_node { continue; }
|
||||
// Each node's response map is at its source RGB's native dims.
|
||||
let (nw, nh) = match node_rgbs.get(id) {
|
||||
Some(rgb) => rgb.dimensions(),
|
||||
None => continue, // shouldn't happen for detect nodes
|
||||
};
|
||||
let node_fp = node_fps.get(id).copied().unwrap_or(0);
|
||||
let preview = if let Some((cached_fp, cached_prev)) = cache.preview_cache.get(id) {
|
||||
if *cached_fp == node_fp && node_fp != 0 {
|
||||
@@ -827,13 +831,13 @@ fn process_pass_work(
|
||||
cached_prev.clone()
|
||||
} else {
|
||||
cache_misses += 1;
|
||||
let p = map_to_b64_small(map, w, h);
|
||||
let p = map_to_b64_small(map, nw, nh);
|
||||
cache.preview_cache.insert(id.clone(), (node_fp, p.clone()));
|
||||
p
|
||||
}
|
||||
} else {
|
||||
cache_misses += 1;
|
||||
let p = map_to_b64_small(map, w, h);
|
||||
let p = map_to_b64_small(map, nw, nh);
|
||||
cache.preview_cache.insert(id.clone(), (node_fp, p.clone()));
|
||||
p
|
||||
};
|
||||
@@ -847,6 +851,7 @@ fn process_pass_work(
|
||||
let mut hull_resp_maps: std::collections::HashMap<String, Vec<u8>> = Default::default();
|
||||
let mut first_hull_response: Option<Vec<u8>> = None;
|
||||
let mut first_hull_threshold: u8 = 128;
|
||||
let mut first_hull_dims: Option<(u32, u32)> = None;
|
||||
|
||||
for node in &det_graph.nodes {
|
||||
if let detect::NodeKind::Hull {
|
||||
@@ -856,6 +861,12 @@ fn process_pass_work(
|
||||
Some(m) => m,
|
||||
None => continue,
|
||||
};
|
||||
// Hull works at its source's native dims.
|
||||
let src_rgb = match node_rgbs.get(&node.id) {
|
||||
Some(r) => r,
|
||||
None => continue,
|
||||
};
|
||||
let (sw, sh) = src_rgb.dimensions();
|
||||
let hull_fp = node_fps.get(&node.id).copied().unwrap_or(0);
|
||||
|
||||
let (filtered, preview) = if hull_fp != 0 {
|
||||
@@ -871,10 +882,11 @@ fn process_pass_work(
|
||||
if first_hull_response.is_none() {
|
||||
first_hull_response = Some(entry.resp_map.clone());
|
||||
first_hull_threshold = *threshold;
|
||||
first_hull_dims = Some((sw, sh));
|
||||
}
|
||||
all_hulls.extend(entry.hulls.clone());
|
||||
let p = preview.unwrap_or_else(|| {
|
||||
let p = render_hull_preview(response, &entry.hulls, w, h, paper_w_mm_for_scale, paper_h_mm_for_scale);
|
||||
let p = render_hull_preview(response, &entry.hulls, sw, sh, paper_w_mm_for_scale, paper_h_mm_for_scale);
|
||||
cache.preview_cache.insert(node.id.clone(), (hull_fp, p.clone()));
|
||||
p
|
||||
});
|
||||
@@ -883,20 +895,14 @@ fn process_pass_work(
|
||||
}
|
||||
}
|
||||
cache_misses += 1;
|
||||
// Cache miss — compute. Each Hull belongs to exactly one
|
||||
// Source tree; pull that tree's RGB for color extraction.
|
||||
let src_rgb = match node_rgbs.get(&node.id) {
|
||||
Some(r) => r,
|
||||
None => continue, // Hull with no upstream Source RGB
|
||||
};
|
||||
let hull_params = hulls::HullParams {
|
||||
threshold: *threshold,
|
||||
min_area: *min_area,
|
||||
connectivity: if *eight_conn { hulls::Connectivity::Eight }
|
||||
else { hulls::Connectivity::Four },
|
||||
};
|
||||
let extracted = hulls::extract_hulls(response, src_rgb, w, h, &hull_params);
|
||||
let preview = render_hull_preview(response, &extracted, w, h, paper_w_mm_for_scale, paper_h_mm_for_scale);
|
||||
let extracted = hulls::extract_hulls(response, src_rgb, sw, sh, &hull_params);
|
||||
let preview = render_hull_preview(response, &extracted, sw, sh, paper_w_mm_for_scale, paper_h_mm_for_scale);
|
||||
cache.hull_entries.insert(node.id.clone(), HullCacheEntry {
|
||||
fp: hull_fp,
|
||||
hulls: extracted.clone(),
|
||||
@@ -906,18 +912,14 @@ fn process_pass_work(
|
||||
(extracted, preview)
|
||||
} else {
|
||||
// No fingerprint — always compute, never cache
|
||||
let src_rgb = match node_rgbs.get(&node.id) {
|
||||
Some(r) => r,
|
||||
None => continue,
|
||||
};
|
||||
let hull_params = hulls::HullParams {
|
||||
threshold: *threshold,
|
||||
min_area: *min_area,
|
||||
connectivity: if *eight_conn { hulls::Connectivity::Eight }
|
||||
else { hulls::Connectivity::Four },
|
||||
};
|
||||
let extracted = hulls::extract_hulls(response, src_rgb, w, h, &hull_params);
|
||||
let preview = render_hull_preview(response, &extracted, w, h, paper_w_mm_for_scale, paper_h_mm_for_scale);
|
||||
let extracted = hulls::extract_hulls(response, src_rgb, sw, sh, &hull_params);
|
||||
let preview = render_hull_preview(response, &extracted, sw, sh, paper_w_mm_for_scale, paper_h_mm_for_scale);
|
||||
(extracted, preview)
|
||||
};
|
||||
|
||||
@@ -925,6 +927,7 @@ fn process_pass_work(
|
||||
if first_hull_response.is_none() {
|
||||
first_hull_response = Some(response.clone());
|
||||
first_hull_threshold = *threshold;
|
||||
first_hull_dims = Some((sw, sh));
|
||||
}
|
||||
hull_outputs.insert(node.id.clone(), filtered.clone());
|
||||
hull_resp_maps.insert(node.id.clone(), response.clone());
|
||||
@@ -975,15 +978,21 @@ fn process_pass_work(
|
||||
|
||||
// Compute. Convert pixel hulls to mm hulls once; mm fills handle
|
||||
// their own internal rasterization at the per-Fill `dpi` knob.
|
||||
// Per-source px-to-mm rate: source.width / paper_w_mm.
|
||||
let response_arc: std::sync::Arc<[u8]> = resp_for_fill.into();
|
||||
let (strategy, spacing_mm, angle, param, smooth_rdp, smooth_iters, fill_dpi) =
|
||||
(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 src_px_per_mm = node_to_source.get(&node.id)
|
||||
.and_then(|sid| source_px_per_mm.get(sid).copied())
|
||||
.unwrap_or(150.0 / 25.4);
|
||||
let img_w = node_to_source.get(&node.id)
|
||||
.and_then(|sid| source_rgbs.get(sid))
|
||||
.map(|r| r.width())
|
||||
.unwrap_or(0);
|
||||
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, src_px_per_mm))
|
||||
.collect();
|
||||
let img_w = w;
|
||||
let src_px_per_mm = px_per_mm_fill;
|
||||
let raw: Vec<fill::FillResult> = mm_hulls.par_iter().map(|mh| {
|
||||
match strategy.as_str() {
|
||||
"outline" => fill::outline_mm(mh),
|
||||
@@ -1039,10 +1048,9 @@ fn process_pass_work(
|
||||
t = lap!(steps, "text", t);
|
||||
|
||||
// ── PenOutput nodes ────────────────────────────────────────────────────────
|
||||
// Pen preview stamps a disk at each Bresenham step; radius scales the
|
||||
// physical pen tip into preview-canvas pixels via px_per_mm_fill.
|
||||
// Pen preview renders at fixed PREVIEW_PX_PER_MM internally; the
|
||||
// stamp radius is computed inside render_pen_preview from pen_tip_mm.
|
||||
let pen_tip_mm = payload.pen_tip_mm.unwrap_or(0.5).max(0.05);
|
||||
let pen_radius_px = (pen_tip_mm * px_per_mm_fill / 2.0).max(0.5);
|
||||
|
||||
let mut pen_results: Vec<PenResult> = Vec::new();
|
||||
let mut pen_output_results: Vec<PenOutputResult> = Vec::new();
|
||||
@@ -1092,20 +1100,22 @@ fn process_pass_work(
|
||||
// ── Coverage + binary viz ──────────────────────────────────────────────────
|
||||
let response_for_viz = first_hull_response.as_deref().unwrap_or(&graph_maps.response);
|
||||
let threshold = first_hull_threshold;
|
||||
let (viz_w, viz_h) = first_hull_dims.unwrap_or((1, 1));
|
||||
|
||||
let total_dark = response_for_viz.iter().filter(|&&p| p < threshold).count();
|
||||
let hull_px: usize = all_hulls.iter().map(|h| h.pixels.len()).sum();
|
||||
let coverage_pct = if total_dark > 0 { hull_px * 100 / total_dark } else { 0 };
|
||||
let stroke_count: usize = pen_results.iter().map(|pr| pr.fill.strokes.len()).sum();
|
||||
|
||||
let mut rgba = vec![0u8; (w * h * 4) as usize];
|
||||
let mut rgba = vec![0u8; (viz_w * viz_h * 4) as usize];
|
||||
for (i, &r) in response_for_viz.iter().enumerate() {
|
||||
if i * 4 + 3 >= rgba.len() { break; }
|
||||
let v = if r < threshold { 0u8 } else { 220u8 };
|
||||
rgba[i*4] = v; rgba[i*4+1] = v; rgba[i*4+2] = v; rgba[i*4+3] = 255;
|
||||
}
|
||||
t = lap!(steps, "viz build", t);
|
||||
|
||||
let viz_b64 = rgba_to_b64_png(&rgba, w, h);
|
||||
let viz_b64 = rgba_to_b64_png(&rgba, viz_w, viz_h);
|
||||
lap!(steps, "png encode", t);
|
||||
steps.push(StepTime {
|
||||
label: format!("total (cache {cache_hits}hit/{cache_misses}miss)"),
|
||||
@@ -1118,7 +1128,7 @@ fn process_pass_work(
|
||||
(all_hulls, pen_results, response_map,
|
||||
ProcessResult { hull_count, coverage_pct, stroke_count, viz_b64,
|
||||
pen_outputs: pen_output_results, node_previews, timings: steps,
|
||||
img_w: w, img_h: h },
|
||||
img_w: viz_w, img_h: viz_h },
|
||||
cache)
|
||||
}
|
||||
|
||||
@@ -1171,15 +1181,13 @@ fn set_pass_count(count: usize, state: State<Mutex<AppState>>) {
|
||||
|
||||
#[tauri::command]
|
||||
async fn process_pass(payload: ProcessPassPayload, state: State<'_, Mutex<AppState>>) -> Result<ProcessResult, String> {
|
||||
// Resolve every Source node's file_path against the image cache, then
|
||||
// letterbox-fit each one into a paper-sized canvas so all Sources end
|
||||
// up at identical pixel dimensions — downstream hulls/strokes share
|
||||
// one coord frame, gcode export uses one img_w/img_h.
|
||||
let dpi = payload.dpi.unwrap_or(150).max(1) as f32;
|
||||
// Each Source node references an image in the cache; pull each one
|
||||
// out at its NATIVE pixel dims. No letterbox, no synthetic canvas —
|
||||
// kernels and Hull operate at the source image's own resolution,
|
||||
// mm-conversion happens at the polygon boundary using
|
||||
// `source.width_px / img_w_mm` as the per-source pixels-per-mm rate.
|
||||
let paper_w = payload.img_w_mm.unwrap_or(210.0).max(1.0);
|
||||
let paper_h = payload.img_h_mm.unwrap_or(297.0).max(1.0);
|
||||
let canvas_w = ((paper_w * dpi / 25.4).round() as u32).max(1);
|
||||
let canvas_h = ((paper_h * dpi / 25.4).round() as u32).max(1);
|
||||
|
||||
let source_rgbs: std::collections::HashMap<String, image::RgbImage> = {
|
||||
let st = state.lock().unwrap();
|
||||
@@ -1188,26 +1196,11 @@ async fn process_pass(payload: ProcessPassPayload, state: State<'_, Mutex<AppSta
|
||||
if n.kind != "Source" { continue; }
|
||||
let path = match n.file_path.as_deref() {
|
||||
Some(p) if !p.is_empty() => p,
|
||||
_ => continue, // Source with no file picked yet → skip
|
||||
_ => continue,
|
||||
};
|
||||
let raw = match st.images.get(path) {
|
||||
Some(img) => img,
|
||||
None => continue, // file referenced but not loaded → skip
|
||||
};
|
||||
// Letterbox-fit into the paper canvas: scale uniformly to fit
|
||||
// inside (canvas_w × canvas_h), centre, fill background white.
|
||||
let (rw, rh) = raw.dimensions();
|
||||
let scale = (canvas_w as f32 / rw as f32).min(canvas_h as f32 / rh as f32);
|
||||
let tw = ((rw as f32 * scale).round() as u32).max(1);
|
||||
let th = ((rh as f32 * scale).round() as u32).max(1);
|
||||
let resized = image::DynamicImage::ImageRgb8(raw.clone())
|
||||
.resize_exact(tw, th, image::imageops::FilterType::CatmullRom)
|
||||
.to_rgb8();
|
||||
let mut canvas = image::RgbImage::from_pixel(canvas_w, canvas_h, image::Rgb([255, 255, 255]));
|
||||
let off_x = (canvas_w.saturating_sub(tw)) / 2;
|
||||
let off_y = (canvas_h.saturating_sub(th)) / 2;
|
||||
image::imageops::overlay(&mut canvas, &resized, off_x as i64, off_y as i64);
|
||||
out.insert(n.id.clone(), canvas);
|
||||
if let Some(raw) = st.images.get(path) {
|
||||
out.insert(n.id.clone(), raw.clone());
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
@@ -1222,7 +1215,7 @@ async fn process_pass(payload: ProcessPassPayload, state: State<'_, Mutex<AppSta
|
||||
|
||||
let (new_hulls, new_fill, response_map, result, new_cache) =
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
process_pass_work(source_rgbs, canvas_w, canvas_h, payload, cache)
|
||||
process_pass_work(source_rgbs, payload, cache)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -2428,9 +2421,8 @@ mod blocking_tests {
|
||||
if n.kind == "Source" { n.file_path = Some("test.png".into()); }
|
||||
}
|
||||
|
||||
let (cw, ch) = (800u32, 600u32);
|
||||
let work = tokio::task::spawn_blocking(move || {
|
||||
process_pass_work(work_sources, cw, ch, payload, NodeCache::default())
|
||||
process_pass_work(work_sources, payload, NodeCache::default())
|
||||
});
|
||||
|
||||
// Give the blocking thread a moment to start, then try to grab the mutex.
|
||||
@@ -2732,7 +2724,7 @@ mod viz_tests {
|
||||
};
|
||||
let node_rgbs: std::collections::HashMap<String, image::RgbImage> =
|
||||
graph.nodes.iter().map(|n| (n.id.clone(), img.clone())).collect();
|
||||
let gm = detect::evaluate_graph(&graph, &node_rgbs, w, h);
|
||||
let gm = detect::evaluate_graph(&graph, &node_rgbs);
|
||||
let response = gm.raw_maps.get("hull").cloned().unwrap_or(gm.response);
|
||||
let params = HullParams { threshold, min_area,
|
||||
connectivity: hulls::Connectivity::Four };
|
||||
|
||||
Reference in New Issue
Block a user