fix: weight slider, Output node preview, blend_mode serialization
Three bugs fixed: 1. Weight slider had no effect — evaluate_graph called apply_layer but never applied layer.weight. Now applies the same lerp as the old apply_stack: effective = clamp(255*(1-w) + raw*w, 0, 255). w=0 → background, w=1 → identity, w>1 → amplified toward ink. 2. Output node showed thresholded binary (vizB64) instead of the raw response map. Root cause: Output was filtered out of node_previews, so NodeGraph fell back to outputImageB64 which is the post-threshold viz. Fix: include Output in node_previews (only Source is excluded); NodeGraph now uses nodePreviews[node.id] for all nodes except Source. The threshold slider in Hulls & Contours no longer affects detection graph thumbnails. 3. Combine blend_mode was always "Average" because the frontend stored the field as blendMode (camelCase) but the Rust payload expected blend_mode (snake_case) — serde found nothing and defaulted. Changed to snake_case throughout NodeGraph.jsx to match the rest of the payload convention. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -417,7 +417,6 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
nodePreviews={passes[activePass].nodePreviews}
|
nodePreviews={passes[activePass].nodePreviews}
|
||||||
sourceImageB64={image?.preview_b64 ?? null}
|
sourceImageB64={image?.preview_b64 ?? null}
|
||||||
outputImageB64={passes[activePass]?.vizB64 ?? null}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Viewport
|
<Viewport
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function bezier(from, to) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Component ──────────────────────────────────────────────────────────────────
|
// ── Component ──────────────────────────────────────────────────────────────────
|
||||||
export default function NodeGraph({ graph, onChange, nodePreviews, sourceImageB64, outputImageB64 }) {
|
export default function NodeGraph({ graph, onChange, nodePreviews, sourceImageB64 }) {
|
||||||
const canvasRef = useRef(null)
|
const canvasRef = useRef(null)
|
||||||
const [pan, setPan] = useState({ x: 40, y: 40 })
|
const [pan, setPan] = useState({ x: 40, y: 40 })
|
||||||
const [zoom, setZoom] = useState(1)
|
const [zoom, setZoom] = useState(1)
|
||||||
@@ -190,7 +190,7 @@ export default function NodeGraph({ graph, onChange, nodePreviews, sourceImageB6
|
|||||||
const id = newNodeId(kind)
|
const id = newNodeId(kind)
|
||||||
const node = kind === 'Kernel'
|
const node = kind === 'Kernel'
|
||||||
? { id, kind, x, y, ...defaultKernelProps() }
|
? { id, kind, x, y, ...defaultKernelProps() }
|
||||||
: { id, kind, x, y, blendMode: 'Average', inputCount: 2 }
|
: { id, kind, x, y, blend_mode: 'Average', inputCount: 2 }
|
||||||
const g = graphRef.current
|
const g = graphRef.current
|
||||||
onChangeRef.current({ ...g, nodes: [...g.nodes, node] })
|
onChangeRef.current({ ...g, nodes: [...g.nodes, node] })
|
||||||
}
|
}
|
||||||
@@ -226,10 +226,9 @@ export default function NodeGraph({ graph, onChange, nodePreviews, sourceImageB6
|
|||||||
: (node.kind === 'Kernel' || node.kind === 'Output') ? 1 : 0
|
: (node.kind === 'Kernel' || node.kind === 'Output') ? 1 : 0
|
||||||
const hasOut = node.kind !== 'Output'
|
const hasOut = node.kind !== 'Output'
|
||||||
|
|
||||||
// Preview image: explicit source/output images, or per-node detection map
|
// Source shows the original image; all other nodes (including Output)
|
||||||
const preview = node.kind === 'Source' ? sourceImageB64
|
// show the raw response map from nodePreviews — consistent and threshold-independent.
|
||||||
: node.kind === 'Output' ? outputImageB64
|
const preview = node.kind === 'Source' ? sourceImageB64 : nodePreviews?.[node.id]
|
||||||
: nodePreviews?.[node.id]
|
|
||||||
|
|
||||||
const accentColor = node.kind === 'Source' ? '#7c3aed'
|
const accentColor = node.kind === 'Source' ? '#7c3aed'
|
||||||
: node.kind === 'Output' ? '#b45309'
|
: node.kind === 'Output' ? '#b45309'
|
||||||
@@ -326,11 +325,11 @@ export default function NodeGraph({ graph, onChange, nodePreviews, sourceImageB6
|
|||||||
{node.kind === 'Combine' && (<>
|
{node.kind === 'Combine' && (<>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
|
||||||
{BLEND_MODES.map(m => (
|
{BLEND_MODES.map(m => (
|
||||||
<button key={m} onMouseDown={e => e.stopPropagation()} onClick={() => updateNode(node.id, { blendMode: m })}
|
<button key={m} onMouseDown={e => e.stopPropagation()} onClick={() => updateNode(node.id, { blend_mode: m })}
|
||||||
style={{
|
style={{
|
||||||
padding: '1px 5px', borderRadius: 3, fontSize: 10, cursor: 'pointer', border: 'none',
|
padding: '1px 5px', borderRadius: 3, fontSize: 10, cursor: 'pointer', border: 'none',
|
||||||
background: node.blendMode === m ? '#0f766e' : '#1e293b',
|
background: node.blend_mode === m ? '#0f766e' : '#1e293b',
|
||||||
color: node.blendMode === m ? '#fff' : '#94a3b8',
|
color: node.blend_mode === m ? '#fff' : '#94a3b8',
|
||||||
}}
|
}}
|
||||||
>{m}</button>
|
>{m}</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -505,7 +505,18 @@ pub fn evaluate_graph(
|
|||||||
let node = node_map[id];
|
let node = node_map[id];
|
||||||
let result: Option<Vec<u8>> = match &node.kind {
|
let result: Option<Vec<u8>> = match &node.kind {
|
||||||
NodeKind::Source => None,
|
NodeKind::Source => None,
|
||||||
NodeKind::Kernel(layer) => Some(apply_layer(rgb, layer)),
|
NodeKind::Kernel(layer) => {
|
||||||
|
let raw = apply_layer(rgb, layer);
|
||||||
|
let w = layer.weight;
|
||||||
|
// w=0 → full background, w=1 → identity, w>1 → amplify toward ink
|
||||||
|
Some(if (w - 1.0).abs() < 1e-6 {
|
||||||
|
raw
|
||||||
|
} else {
|
||||||
|
raw.iter().map(|&r| {
|
||||||
|
(255.0 * (1.0 - w) + r as f32 * w).clamp(0.0, 255.0) as u8
|
||||||
|
}).collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
NodeKind::Combine(mode) => {
|
NodeKind::Combine(mode) => {
|
||||||
let mut ins = incoming[id].clone();
|
let mut ins = incoming[id].clone();
|
||||||
ins.sort_by_key(|&(_, p)| p);
|
ins.sort_by_key(|&(_, p)| p);
|
||||||
@@ -530,12 +541,12 @@ pub fn evaluate_graph(
|
|||||||
.and_then(|n| outputs.get(n.id.as_str()).cloned())
|
.and_then(|n| outputs.get(n.id.as_str()).cloned())
|
||||||
.unwrap_or_else(bg);
|
.unwrap_or_else(bg);
|
||||||
|
|
||||||
// Per-node previews — omit Source (no output) and Output (same as final_map)
|
// Per-node previews — omit only Source (no map output); include Output so
|
||||||
|
// the graph editor can show the raw response map (not the thresholded vizB64).
|
||||||
let previews: HashMap<String, Vec<u8>> = outputs.into_iter()
|
let previews: HashMap<String, Vec<u8>> = outputs.into_iter()
|
||||||
.filter(|(id, _)| {
|
.filter(|(id, _)| {
|
||||||
let key: &str = id;
|
let key: &str = id;
|
||||||
node_map.get(key).map_or(true, |n|
|
node_map.get(key).map_or(true, |n| !matches!(n.kind, NodeKind::Source))
|
||||||
!matches!(n.kind, NodeKind::Source | NodeKind::Output))
|
|
||||||
})
|
})
|
||||||
.map(|(id, map)| (id.to_string(), map))
|
.map(|(id, map)| (id.to_string(), map))
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
Reference in New Issue
Block a user