feat: Hull/Fill nodes freely addable+deletable; remove Fill view tab

NodeGraph:
  isFixed now only applies to Source — Hull and Fill are fully deletable,
  supporting arbitrary multi-pass branching (multiple Hull/Fill chains from
  the same detection graph).
  addNode() handles Hull (defaultHullParams) and Fill (defaultFillParams).
  Toolbar gains teal '+ Hull' and purple '+ Fill' buttons alongside the
  existing Kernel/Combine buttons.

App.jsx:
  Remove 'fill' from VIEW_MODES — fill output is visible in the detection
  graph's Fill node thumbnail and in the G-code view.
  Remove the dead case 'fill': branch from the viewport refresh switch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 22:20:15 -07:00
parent 2f9b80c9f1
commit eef77a6f4c
2 changed files with 16 additions and 15 deletions

View File

@@ -8,7 +8,7 @@ import { defaultPass, defaultGcodeConfig, PAPER_SIZES } from './store.js'
import * as tauri from './hooks/useTauri.js'
import { useFps } from './hooks/useFps.js'
const VIEW_MODES = ['source', 'detection', 'contours', 'fill', 'gcode']
const VIEW_MODES = ['source', 'detection', 'contours', 'gcode']
export default function App() {
const [image, setImage] = useState(null)
@@ -101,10 +101,6 @@ export default function App() {
setDisplayB64(null)
}
break
case 'fill':
// Fill uses the same full-res canvas rendering as gcode (via strokes prop).
setDisplayB64(null)
break
case 'gcode':
if (passes.some(p => p.strokeCount > 0)) {
try {

View File

@@ -189,8 +189,9 @@ export default function NodeGraph({ graph, onChange, nodePreviews, sourceImageB6
const x = (r.width / 2 - p.x) / z - NODE_W / 2
const y = (r.height / 2 - p.y) / z - 60
const id = newNodeId(kind)
const node = kind === 'Kernel'
? { id, kind, x, y, ...defaultKernelProps() }
const node = kind === 'Kernel' ? { id, kind, x, y, ...defaultKernelProps() }
: kind === 'Hull' ? { id, kind, x, y, ...defaultHullParams() }
: kind === 'Fill' ? { id, kind, x, y, ...defaultFillParams() }
: { id, kind, x, y, blend_mode: 'Average', inputCount: 2 }
const g = graphRef.current
onChangeRef.current({ ...g, nodes: [...g.nodes, node] })
@@ -222,7 +223,7 @@ export default function NodeGraph({ graph, onChange, nodePreviews, sourceImageB6
// ── Node rendering ─────────────────────────────────────────────────────────
function renderNode(node) {
const isFixed = node.kind === 'Source' || node.kind === 'Hull' || node.kind === 'Fill'
const isFixed = node.kind === 'Source'
const inputCnt = node.kind === 'Combine' ? (node.inputCount ?? 2)
: (node.kind === 'Kernel' || node.kind === 'Output' || node.kind === 'Hull' || node.kind === 'Fill') ? 1 : 0
const hasOut = node.kind !== 'Output' && node.kind !== 'Hull' && node.kind !== 'Fill'
@@ -440,12 +441,16 @@ export default function NodeGraph({ graph, onChange, nodePreviews, sourceImageB6
>
{/* Toolbar */}
<div style={{ position: 'absolute', top: 8, left: 8, zIndex: 30, display: 'flex', gap: 4, alignItems: 'center' }}>
<button onClick={() => addNode('Kernel')}
style={{ padding: '3px 10px', borderRadius: 4, fontSize: 11, cursor: 'pointer', border: '1px solid #374151', background: '#1e293b', color: '#94a3b8' }}
>+ Kernel</button>
<button onClick={() => addNode('Combine')}
style={{ padding: '3px 10px', borderRadius: 4, fontSize: 11, cursor: 'pointer', border: '1px solid #374151', background: '#1e293b', color: '#94a3b8' }}
>+ Combine</button>
{[
['Kernel', '#374151', '#94a3b8'],
['Combine', '#374151', '#94a3b8'],
['Hull', '#0d9488', '#5eead4'],
['Fill', '#7c3aed', '#c4b5fd'],
].map(([kind, border, color]) => (
<button key={kind} onClick={() => addNode(kind)}
style={{ padding: '3px 10px', borderRadius: 4, fontSize: 11, cursor: 'pointer', border: `1px solid ${border}`, background: '#1e293b', color }}
>+ {kind}</button>
))}
<span style={{ fontSize: 10, color: '#2d3748', paddingLeft: 4 }}>
scroll=zoom · drag=pan · click wire=delete
</span>