diff --git a/frontend/src/components/GraphCanvas.svelte b/frontend/src/components/GraphCanvas.svelte index c5b089f..391bf72 100644 --- a/frontend/src/components/GraphCanvas.svelte +++ b/frontend/src/components/GraphCanvas.svelte @@ -13,6 +13,7 @@ saveProfile, loadProfile, deleteProfile, setNodeVolume, setNodeMute, createNullSink, createLoopback, loadModule, + getQuantum, setQuantum, } from '../lib/stores'; import type { Node, Port, Link } from '../lib/types'; @@ -41,6 +42,7 @@ let showProfileDialog = $state(false); let showRuleDialog = $state(false); let showVirtualMenu = $state(false); + let splitNodes = $state(false); let showNetworkDialog = $state<{ type: string } | null>(null); let netHost = $state('127.0.0.1'); let netPort = $state('4713'); @@ -50,6 +52,7 @@ // Positions let nodePositions = $state>({}); + let currentQuantum = $state(0); const POS_KEY = 'pwweb_positions'; function loadPositions() { @@ -136,20 +139,26 @@ // Filter hidden nodes let visible = n.filter(nd => !isNodeHidden(nd.name)); - // Merge nodes by prefix - const merged = new Map(); - for (const nd of visible) { - const mergedName = getMergedName(nd.name); - const existing = merged.get(mergedName); - if (existing) { - // Merge port IDs - existing.port_ids = [...new Set([...existing.port_ids, ...nd.port_ids])]; - existing.name = mergedName; - } else { - merged.set(mergedName, { ...nd, name: mergedName }); + // Merge nodes by prefix (unless split mode) + if (!splitNodes) { + const merged = new Map(); + for (const nd of visible) { + const mergedName = getMergedName(nd.name); + const existing = merged.get(mergedName); + if (existing) { + existing.port_ids = [...new Set([...existing.port_ids, ...nd.port_ids])]; + existing.name = mergedName; + const mergedInPorts = existing.port_ids.map(pid => portMap.get(pid)).filter((p): p is Port => !!p && p.mode === 'input'); + const mergedOutPorts = existing.port_ids.map(pid => portMap.get(pid)).filter((p): p is Port => !!p && p.mode === 'output'); + if (mergedInPorts.length > 0 && mergedOutPorts.length > 0) { + existing.mode = 'duplex'; + } + } else { + merged.set(mergedName, { ...nd, name: mergedName }); + } } + visible = Array.from(merged.values()); } - visible = Array.from(merged.values()); const out = visible.filter(nd => nd.mode === 'output'); const inp = visible.filter(nd => nd.mode === 'input'); @@ -420,7 +429,7 @@ } } - onMount(() => { initGraph(); loadPositions(); }); + onMount(() => { initGraph(); loadPositions(); getQuantum().then(q => { currentQuantum = q; }); }); onDestroy(() => { destroyGraph(); }); @@ -457,9 +466,19 @@ + + + {$nodes.length}N {$ports.length}P {$links.length}L {#if $patchbay.pinned_connections.length > 0}{$patchbay.pinned_connections.length}p{/if} @@ -562,9 +581,10 @@ {#each graphNodes as nd (nd.id)} {@const isSource = nd.mode === 'output'} {@const isSink = nd.mode === 'input'} - {@const bg = isSource ? '#1a2a1a' : isSink ? '#2a1a1a' : '#1e1e2e'} - {@const border = isSource ? '#4a9' : isSink ? '#a44' : '#555'} - {@const headerBg = isSource ? '#263826' : isSink ? '#382626' : '#262638'} + {@const isDuplex = nd.mode === 'duplex'} + {@const bg = isSource ? '#1a2a1a' : isSink ? '#2a1a1a' : isDuplex ? '#1a1a2a' : '#1e1e2e'} + {@const border = isSource ? '#4a9' : isSink ? '#a44' : isDuplex ? '#49a' : '#555'} + {@const headerBg = isSource ? '#263826' : isSink ? '#382626' : isDuplex ? '#262638' : '#262638'} @@ -821,8 +841,8 @@