Skip to content

Commit b5cdda6

Browse files
authored
Merge pull request #293 from pathsim/feature/custom-port-labels
Custom port labels
2 parents 81d65dd + 7eef9d7 commit b5cdda6

5 files changed

Lines changed: 157 additions & 16 deletions

File tree

src/lib/components/dialogs/BlockPropertiesDialog.svelte

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
import { NODE_TYPES } from '$lib/constants/nodeTypes';
2222
import { exportRecordingData } from '$lib/utils/csvExport';
2323
import { createRecordingDataState } from '$lib/stores/recordingData.svelte';
24+
import { getPortLabelConfigs } from '$lib/nodes/uiConfig';
25+
import { PORT_NAME } from '$lib/constants/handles';
2426
2527
// Code preview state (declared early — referenced by subscription below)
2628
let showCode = $state(false);
29+
let showPortLabels = $state(false);
2730
let previewCode = $state('');
2831
let editorContainer = $state<HTMLDivElement | undefined>(undefined);
2932
let editorView: import('@codemirror/view').EditorView | null = null;
@@ -49,10 +52,12 @@
4952
node = graphStore.getNode(id) || null;
5053
// Reset to properties view when opening a new node
5154
showCode = false;
55+
showPortLabels = false;
5256
destroyEditor();
5357
} else {
5458
node = null;
5559
showCode = false;
60+
showPortLabels = false;
5661
destroyEditor();
5762
}
5863
});
@@ -144,11 +149,23 @@
144149
const blockCode = generateBlockCode(node, allNodes, allConnections);
145150
previewCode = header + blockCode;
146151
copied = false;
152+
showPortLabels = false;
147153
showCode = true;
148154
setTimeout(() => initEditor(), 0);
149155
}
150156
}
151157
158+
// Toggle port-labels view (same mutual-exclusion pattern as code view)
159+
function togglePortLabelsView() {
160+
if (showPortLabels) {
161+
showPortLabels = false;
162+
} else {
163+
showCode = false;
164+
destroyEditor();
165+
showPortLabels = true;
166+
}
167+
}
168+
152169
function copyToClipboard() {
153170
navigator.clipboard.writeText(previewCode);
154171
copied = true;
@@ -177,6 +194,24 @@
177194
closeNodeDialog();
178195
}
179196
197+
// Port-label editing — hide for blocks whose port names are driven by a
198+
// regular param (Scope.labels, Adder.operations, …); for those the param
199+
// itself is the source of truth and editing port names directly would be
200+
// overwritten on the next param change.
201+
const hasParamDrivenPortLabels = $derived(node ? getPortLabelConfigs(node.type).length > 0 : false);
202+
const hasEditablePortLabels = $derived(
203+
!!node && !hasParamDrivenPortLabels && (node.inputs.length > 0 || node.outputs.length > 0)
204+
);
205+
206+
function handlePortNameChange(direction: 'input' | 'output', index: number, value: string) {
207+
if (!node) return;
208+
const id = node.id;
209+
const trimmed = value.trim();
210+
const fallback = direction === 'input' ? PORT_NAME.input(index) : PORT_NAME.output(index);
211+
const name = trimmed === '' ? fallback : trimmed;
212+
historyStore.mutate(() => graphStore.updateNodePortName(id, direction, index, name));
213+
}
214+
180215
// Check if node is a recording node (Scope or Spectrum)
181216
const isRecordingNode = $derived(node?.type === 'Scope' || node?.type === 'Spectrum');
182217
@@ -256,6 +291,8 @@
256291
<div class="dialog-header">
257292
{#if showCode}
258293
<span id="dialog-title">Python Code</span>
294+
{:else if showPortLabels}
295+
<span id="dialog-title">Port Labels</span>
259296
{:else}
260297
<div class="node-info">
261298
<input
@@ -286,7 +323,7 @@
286323
<Icon name="copy" size={16} />
287324
{/if}
288325
</button>
289-
{:else}
326+
{:else if !showPortLabels}
290327
<!-- Color picker -->
291328
<ColorPicker
292329
currentColor={currentColor}
@@ -328,15 +365,28 @@
328365
</button>
329366
{/if}
330367
{/if}
331-
<!-- Toggle code view button -->
332-
<button
333-
class="icon-btn"
334-
onclick={toggleCodeView}
335-
use:tooltip={showCode ? "View Properties" : "View Python Code"}
336-
aria-label={showCode ? "View Properties" : "View Python Code"}
337-
>
338-
<Icon name={showCode ? "settings" : "braces"} size={16} />
339-
</button>
368+
<!-- Toggle port labels view button (hidden in code view) -->
369+
{#if showPortLabels || (!showCode && hasEditablePortLabels)}
370+
<button
371+
class="icon-btn"
372+
onclick={togglePortLabelsView}
373+
use:tooltip={showPortLabels ? "View Properties" : "Edit Port Labels"}
374+
aria-label={showPortLabels ? "View Properties" : "Edit Port Labels"}
375+
>
376+
<Icon name={showPortLabels ? "settings" : "tag"} size={16} />
377+
</button>
378+
{/if}
379+
<!-- Toggle code view button (hidden in port-labels view) -->
380+
{#if !showPortLabels}
381+
<button
382+
class="icon-btn"
383+
onclick={toggleCodeView}
384+
use:tooltip={showCode ? "View Properties" : "View Python Code"}
385+
aria-label={showCode ? "View Properties" : "View Python Code"}
386+
>
387+
<Icon name={showCode ? "settings" : "braces"} size={16} />
388+
</button>
389+
{/if}
340390
<button class="icon-btn" onclick={closeNodeDialog} aria-label="Close">
341391
<Icon name="x" size={16} />
342392
</button>
@@ -351,6 +401,43 @@
351401
<div class="loading">Loading...</div>
352402
{/if}
353403
</div>
404+
{:else if showPortLabels}
405+
<!-- Port labels view -->
406+
{#if node.inputs.length === 0 && node.outputs.length === 0}
407+
<div class="no-params">No ports to label</div>
408+
{:else}
409+
<div class="section">
410+
<div class="params-grid">
411+
{#each node.inputs as port, i (port.id)}
412+
<div class="param-item">
413+
<label for="port-in-{i}">in {i}</label>
414+
<input
415+
id="port-in-{i}"
416+
type="text"
417+
value={port.name}
418+
placeholder={PORT_NAME.input(i)}
419+
onchange={(e) => handlePortNameChange('input', i, e.currentTarget.value)}
420+
/>
421+
</div>
422+
{/each}
423+
{#if node.inputs.length > 0 && node.outputs.length > 0}
424+
<div class="port-divider"></div>
425+
{/if}
426+
{#each node.outputs as port, i (port.id)}
427+
<div class="param-item">
428+
<label for="port-out-{i}">out {i}</label>
429+
<input
430+
id="port-out-{i}"
431+
type="text"
432+
value={port.name}
433+
placeholder={PORT_NAME.output(i)}
434+
onchange={(e) => handlePortNameChange('output', i, e.currentTarget.value)}
435+
/>
436+
</div>
437+
{/each}
438+
</div>
439+
</div>
440+
{/if}
354441
{:else}
355442
<!-- Parameters -->
356443
{#if typeDef.params.length > 0}
@@ -413,7 +500,7 @@
413500
{/if}
414501
</div>
415502

416-
{#if !showCode}
503+
{#if !showCode && !showPortLabels}
417504
<div class="dialog-footer">
418505
<span class="hint">R rotate · X flip horizontal · Y flip vertical</span>
419506
</div>
@@ -436,6 +523,13 @@
436523
min-width: 0;
437524
}
438525
526+
.port-divider {
527+
height: 1px;
528+
background: var(--border);
529+
/* Extend past the dialog-body padding so the line spans edge-to-edge. */
530+
margin: var(--space-xs) calc(-1 * var(--space-md));
531+
}
532+
439533
/* Pin button */
440534
.pin-btn {
441535
flex-shrink: 0;

src/lib/stores/graph/helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,15 @@ export function deriveInterfaceNode(
6969
outputs: parentSubsystem.inputs.map((port, i) => ({
7070
id: `${interfaceNode.id}-output-${i}`,
7171
nodeId: interfaceNode.id,
72-
name: `in ${i}`,
72+
name: port.name,
7373
direction: 'output' as const,
7474
index: i,
7575
color: port.color
7676
})),
7777
inputs: parentSubsystem.outputs.map((port, i) => ({
7878
id: `${interfaceNode.id}-input-${i}`,
7979
nodeId: interfaceNode.id,
80-
name: `out ${i}`,
80+
name: port.name,
8181
direction: 'input' as const,
8282
index: i,
8383
color: port.color

src/lib/stores/graph/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export const graphStore = {
8484
removeInputPort: ports.removeInputPort,
8585
addOutputPort: ports.addOutputPort,
8686
removeOutputPort: ports.removeOutputPort,
87+
updateNodePortName: ports.updateNodePortName,
8788

8889
// ==================== ANNOTATION OPERATIONS ====================
8990
addAnnotation: annotations.addAnnotation,

src/lib/stores/graph/ports.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,3 +331,47 @@ export function syncPortNamesFromLabels(
331331
updateNodeById(nodeId, () => updated);
332332
}
333333
}
334+
335+
/**
336+
* Rename a single port. For regular nodes this writes the new name into
337+
* `node.{inputs|outputs}[index].name`. For Interface nodes, port names are
338+
* derived from the parent Subsystem at read time, so we write to the parent
339+
* instead (with the direction flipped, because Interface input ↔ Subsystem
340+
* output).
341+
*/
342+
export function updateNodePortName(
343+
nodeId: string,
344+
direction: PortDirection,
345+
index: number,
346+
name: string
347+
): void {
348+
const currentGraph = getCurrentGraph();
349+
const node = currentGraph.nodes.get(nodeId);
350+
if (!node) return;
351+
352+
const path = get(currentPath);
353+
354+
if (node.type === NODE_TYPES.INTERFACE && path.length > 0) {
355+
const parentId = path[path.length - 1];
356+
const parentPath = path.slice(0, -1);
357+
const parentPortsKey = direction === 'input' ? 'outputs' : 'inputs';
358+
359+
updateParentSubsystem(parentPath, parentId, (parent) => {
360+
const ports = parent[parentPortsKey] as PortInstance[];
361+
if (index < 0 || index >= ports.length) return parent;
362+
if (ports[index].name === name) return parent;
363+
const newPorts = ports.map((p, i) => (i === index ? { ...p, name } : p));
364+
return { ...parent, [parentPortsKey]: newPorts };
365+
});
366+
return;
367+
}
368+
369+
updateNodeById(nodeId, (n) => {
370+
const portsKey = direction === 'input' ? 'inputs' : 'outputs';
371+
const ports = n[portsKey] as PortInstance[];
372+
if (index < 0 || index >= ports.length) return n;
373+
if (ports[index].name === name) return n;
374+
const newPorts = ports.map((p, i) => (i === index ? { ...p, name } : p));
375+
return { ...n, [portsKey]: newPorts };
376+
});
377+
}

src/lib/stores/graph/state.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,13 @@ export function getCurrentGraph(): {
9595
...node,
9696
name: subsystem.name,
9797
color: subsystem.color,
98-
// Subsystem inputs → Interface outputs (signals coming in)
98+
// Subsystem inputs → Interface outputs (signals coming in).
99+
// Port names come from the parent — that's the single source
100+
// of truth so user-customised labels survive across renders.
99101
outputs: subsystem.inputs.map((port, i) => ({
100102
id: `${node.id}-output-${i}`,
101103
nodeId: node.id,
102-
name: `in ${i}`,
104+
name: port.name,
103105
direction: 'output' as const,
104106
index: i,
105107
color: port.color
@@ -108,7 +110,7 @@ export function getCurrentGraph(): {
108110
inputs: subsystem.outputs.map((port, i) => ({
109111
id: `${node.id}-input-${i}`,
110112
nodeId: node.id,
111-
name: `out ${i}`,
113+
name: port.name,
112114
direction: 'input' as const,
113115
index: i,
114116
color: port.color

0 commit comments

Comments
 (0)