diff --git a/src/lib/components/FlowCanvas.svelte b/src/lib/components/FlowCanvas.svelte index 6ad76ddf..25c8636b 100644 --- a/src/lib/components/FlowCanvas.svelte +++ b/src/lib/components/FlowCanvas.svelte @@ -521,8 +521,13 @@ cleanups.push(graphStore.nodes.subscribe((graphNodesMap: Map) => { if (isSyncing) return; - // Convert Map to array for processing - const filteredGraphNodes = Array.from(graphNodesMap.values()); + // Convert Map to array for processing — exclude nodes hidden via `_hidden` + // param. Hidden nodes stay in the store (simulation still uses them) but + // disappear from the canvas; toggling `_hidden` triggers this subscriber + // again so they come back the same way they were removed. + const filteredGraphNodes = Array.from(graphNodesMap.values()).filter( + (n) => !n.params?.['_hidden'] + ); // Track nodes that need handle updates (port count changed) const nodesToUpdate: string[] = []; @@ -689,25 +694,46 @@ annotationNodes = Array.from(annotationsMap.values()).map(toAnnotationNode); })); + // Compute the set of node IDs currently visible (not `_hidden`). Read on + // every edge rebuild so connections to hidden nodes can be filtered out. + function getVisibleNodeIds(): Set { + const graphNodesMap = get(graphStore.nodes); + const ids = new Set(); + for (const n of graphNodesMap.values()) { + if (!n.params?.['_hidden']) ids.add(n.id); + } + return ids; + } + + function rebuildEdges(connections: Connection[]): void { + const visibleIds = getVisibleNodeIds(); + const currentEdgeSelection = new Map(edges.map((e) => [e.id, e.selected])); + edges = connections + .filter((c) => visibleIds.has(c.sourceNodeId) && visibleIds.has(c.targetNodeId)) + .map((conn) => { + const edge = toFlowEdge(conn); + if (currentEdgeSelection.get(conn.id)) edge.selected = true; + return edge; + }); + } + // Subscribe to current connections (filtered by current navigation context) cleanups.push(graphStore.connections.subscribe((connections: Connection[]) => { if (isSyncing) return; - // Preserve selection state from existing edges - const currentEdgeSelection = new Map(edges.map(e => [e.id, e.selected])); - edges = connections.map(conn => { - const edge = toFlowEdge(conn); - // Preserve selection state - const wasSelected = currentEdgeSelection.get(conn.id); - if (wasSelected) { - edge.selected = true; - } - return edge; - }); + rebuildEdges(connections); // Recalculate routes when connections change // Debounced to coalesce rapid changes (paste, undo, bulk operations) scheduleRoutingUpdate(); })); + // When `_hidden` flips on a node, the connections store doesn't fire — the + // connections themselves are unchanged. Subscribe to the nodes store too + // and rebuild edges so newly hidden/visible nodes' edges follow along. + cleanups.push(graphStore.nodes.subscribe(() => { + if (isSyncing) return; + rebuildEdges(get(graphStore.connections)); + })); + // Track last snapped positions during drag for discrete routing updates let lastDraggedPositions = new Map(); diff --git a/src/lib/components/contextMenuBuilders.ts b/src/lib/components/contextMenuBuilders.ts index ccbbefb1..645194be 100644 --- a/src/lib/components/contextMenuBuilders.ts +++ b/src/lib/components/contextMenuBuilders.ts @@ -179,6 +179,13 @@ function buildNodeMenu(nodeId: string): MenuItemType[] { items.push( DIVIDER, + { + label: 'Hide', + icon: 'eye-off', + action: () => historyStore.mutate(() => + graphStore.updateNodeParams(nodeId, { _hidden: true }) + ) + }, { label: 'View Code', icon: 'braces', @@ -239,6 +246,13 @@ function buildNodeMenu(nodeId: string): MenuItemType[] { items.push( DIVIDER, + { + label: 'Hide', + icon: 'eye-off', + action: () => historyStore.mutate(() => + graphStore.updateNodeParams(nodeId, { _hidden: true }) + ) + }, { label: 'View Code', icon: 'braces', @@ -316,6 +330,16 @@ function buildSelectionMenu( action: () => clipboardStore.copy() }, DIVIDER, + { + label: `Hide ${count} nodes`, + icon: 'eye-off', + action: () => historyStore.mutate(() => { + for (const id of nodeIds) { + graphStore.updateNodeParams(id, { _hidden: true }); + } + }) + }, + DIVIDER, { label: `Delete ${count} nodes`, icon: 'trash', diff --git a/src/lib/components/dialogs/BlockPropertiesDialog.svelte b/src/lib/components/dialogs/BlockPropertiesDialog.svelte index 1d4f43b2..a71b6428 100644 --- a/src/lib/components/dialogs/BlockPropertiesDialog.svelte +++ b/src/lib/components/dialogs/BlockPropertiesDialog.svelte @@ -165,6 +165,18 @@ // Check if node can be exported (not Interface blocks) const canExport = $derived(node?.type !== NODE_TYPES.INTERFACE); + // Hide button is meaningless for Interface blocks (they define the + // subsystem's outer ports) — skip them. All other nodes can be hidden. + const canHide = $derived(node?.type !== NODE_TYPES.INTERFACE); + + function handleHide() { + if (!node) return; + const id = node.id; + historyStore.mutate(() => graphStore.updateNodeParams(id, { _hidden: true })); + // Dialog targets a node that's now invisible; close it. + closeNodeDialog(); + } + // Check if node is a recording node (Scope or Spectrum) const isRecordingNode = $derived(node?.type === 'Scope' || node?.type === 'Spectrum'); @@ -304,6 +316,17 @@ {/if} + + {#if canHide} + + {/if} {/if} + + {#if hiddenNodes.length > 0} + +
+ + {#if hiddenMenuOpen} + + {/if} +
+ {/if} +