Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 105 additions & 11 deletions src/lib/components/dialogs/BlockPropertiesDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
import { NODE_TYPES } from '$lib/constants/nodeTypes';
import { exportRecordingData } from '$lib/utils/csvExport';
import { createRecordingDataState } from '$lib/stores/recordingData.svelte';
import { getPortLabelConfigs } from '$lib/nodes/uiConfig';
import { PORT_NAME } from '$lib/constants/handles';

// Code preview state (declared early — referenced by subscription below)
let showCode = $state(false);
let showPortLabels = $state(false);
let previewCode = $state('');
let editorContainer = $state<HTMLDivElement | undefined>(undefined);
let editorView: import('@codemirror/view').EditorView | null = null;
Expand All @@ -49,10 +52,12 @@
node = graphStore.getNode(id) || null;
// Reset to properties view when opening a new node
showCode = false;
showPortLabels = false;
destroyEditor();
} else {
node = null;
showCode = false;
showPortLabels = false;
destroyEditor();
}
});
Expand Down Expand Up @@ -144,11 +149,23 @@
const blockCode = generateBlockCode(node, allNodes, allConnections);
previewCode = header + blockCode;
copied = false;
showPortLabels = false;
showCode = true;
setTimeout(() => initEditor(), 0);
}
}

// Toggle port-labels view (same mutual-exclusion pattern as code view)
function togglePortLabelsView() {
if (showPortLabels) {
showPortLabels = false;
} else {
showCode = false;
destroyEditor();
showPortLabels = true;
}
}

function copyToClipboard() {
navigator.clipboard.writeText(previewCode);
copied = true;
Expand Down Expand Up @@ -177,6 +194,24 @@
closeNodeDialog();
}

// Port-label editing — hide for blocks whose port names are driven by a
// regular param (Scope.labels, Adder.operations, …); for those the param
// itself is the source of truth and editing port names directly would be
// overwritten on the next param change.
const hasParamDrivenPortLabels = $derived(node ? getPortLabelConfigs(node.type).length > 0 : false);
const hasEditablePortLabels = $derived(
!!node && !hasParamDrivenPortLabels && (node.inputs.length > 0 || node.outputs.length > 0)
);

function handlePortNameChange(direction: 'input' | 'output', index: number, value: string) {
if (!node) return;
const id = node.id;
const trimmed = value.trim();
const fallback = direction === 'input' ? PORT_NAME.input(index) : PORT_NAME.output(index);
const name = trimmed === '' ? fallback : trimmed;
historyStore.mutate(() => graphStore.updateNodePortName(id, direction, index, name));
}

// Check if node is a recording node (Scope or Spectrum)
const isRecordingNode = $derived(node?.type === 'Scope' || node?.type === 'Spectrum');

Expand Down Expand Up @@ -256,6 +291,8 @@
<div class="dialog-header">
{#if showCode}
<span id="dialog-title">Python Code</span>
{:else if showPortLabels}
<span id="dialog-title">Port Labels</span>
{:else}
<div class="node-info">
<input
Expand Down Expand Up @@ -286,7 +323,7 @@
<Icon name="copy" size={16} />
{/if}
</button>
{:else}
{:else if !showPortLabels}
<!-- Color picker -->
<ColorPicker
currentColor={currentColor}
Expand Down Expand Up @@ -328,15 +365,28 @@
</button>
{/if}
{/if}
<!-- Toggle code view button -->
<button
class="icon-btn"
onclick={toggleCodeView}
use:tooltip={showCode ? "View Properties" : "View Python Code"}
aria-label={showCode ? "View Properties" : "View Python Code"}
>
<Icon name={showCode ? "settings" : "braces"} size={16} />
</button>
<!-- Toggle port labels view button (hidden in code view) -->
{#if showPortLabels || (!showCode && hasEditablePortLabels)}
<button
class="icon-btn"
onclick={togglePortLabelsView}
use:tooltip={showPortLabels ? "View Properties" : "Edit Port Labels"}
aria-label={showPortLabels ? "View Properties" : "Edit Port Labels"}
>
<Icon name={showPortLabels ? "settings" : "tag"} size={16} />
</button>
{/if}
<!-- Toggle code view button (hidden in port-labels view) -->
{#if !showPortLabels}
<button
class="icon-btn"
onclick={toggleCodeView}
use:tooltip={showCode ? "View Properties" : "View Python Code"}
aria-label={showCode ? "View Properties" : "View Python Code"}
>
<Icon name={showCode ? "settings" : "braces"} size={16} />
</button>
{/if}
<button class="icon-btn" onclick={closeNodeDialog} aria-label="Close">
<Icon name="x" size={16} />
</button>
Expand All @@ -351,6 +401,43 @@
<div class="loading">Loading...</div>
{/if}
</div>
{:else if showPortLabels}
<!-- Port labels view -->
{#if node.inputs.length === 0 && node.outputs.length === 0}
<div class="no-params">No ports to label</div>
{:else}
<div class="section">
<div class="params-grid">
{#each node.inputs as port, i (port.id)}
<div class="param-item">
<label for="port-in-{i}">in {i}</label>
<input
id="port-in-{i}"
type="text"
value={port.name}
placeholder={PORT_NAME.input(i)}
onchange={(e) => handlePortNameChange('input', i, e.currentTarget.value)}
/>
</div>
{/each}
{#if node.inputs.length > 0 && node.outputs.length > 0}
<div class="port-divider"></div>
{/if}
{#each node.outputs as port, i (port.id)}
<div class="param-item">
<label for="port-out-{i}">out {i}</label>
<input
id="port-out-{i}"
type="text"
value={port.name}
placeholder={PORT_NAME.output(i)}
onchange={(e) => handlePortNameChange('output', i, e.currentTarget.value)}
/>
</div>
{/each}
</div>
</div>
{/if}
{:else}
<!-- Parameters -->
{#if typeDef.params.length > 0}
Expand Down Expand Up @@ -413,7 +500,7 @@
{/if}
</div>

{#if !showCode}
{#if !showCode && !showPortLabels}
<div class="dialog-footer">
<span class="hint">R rotate · X flip horizontal · Y flip vertical</span>
</div>
Expand All @@ -436,6 +523,13 @@
min-width: 0;
}

.port-divider {
height: 1px;
background: var(--border);
/* Extend past the dialog-body padding so the line spans edge-to-edge. */
margin: var(--space-xs) calc(-1 * var(--space-md));
}

/* Pin button */
.pin-btn {
flex-shrink: 0;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/stores/graph/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ export function deriveInterfaceNode(
outputs: parentSubsystem.inputs.map((port, i) => ({
id: `${interfaceNode.id}-output-${i}`,
nodeId: interfaceNode.id,
name: `in ${i}`,
name: port.name,
direction: 'output' as const,
index: i,
color: port.color
})),
inputs: parentSubsystem.outputs.map((port, i) => ({
id: `${interfaceNode.id}-input-${i}`,
nodeId: interfaceNode.id,
name: `out ${i}`,
name: port.name,
direction: 'input' as const,
index: i,
color: port.color
Expand Down
1 change: 1 addition & 0 deletions src/lib/stores/graph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const graphStore = {
removeInputPort: ports.removeInputPort,
addOutputPort: ports.addOutputPort,
removeOutputPort: ports.removeOutputPort,
updateNodePortName: ports.updateNodePortName,

// ==================== ANNOTATION OPERATIONS ====================
addAnnotation: annotations.addAnnotation,
Expand Down
44 changes: 44 additions & 0 deletions src/lib/stores/graph/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,3 +331,47 @@ export function syncPortNamesFromLabels(
updateNodeById(nodeId, () => updated);
}
}

/**
* Rename a single port. For regular nodes this writes the new name into
* `node.{inputs|outputs}[index].name`. For Interface nodes, port names are
* derived from the parent Subsystem at read time, so we write to the parent
* instead (with the direction flipped, because Interface input ↔ Subsystem
* output).
*/
export function updateNodePortName(
nodeId: string,
direction: PortDirection,
index: number,
name: string
): void {
const currentGraph = getCurrentGraph();
const node = currentGraph.nodes.get(nodeId);
if (!node) return;

const path = get(currentPath);

if (node.type === NODE_TYPES.INTERFACE && path.length > 0) {
const parentId = path[path.length - 1];
const parentPath = path.slice(0, -1);
const parentPortsKey = direction === 'input' ? 'outputs' : 'inputs';

updateParentSubsystem(parentPath, parentId, (parent) => {
const ports = parent[parentPortsKey] as PortInstance[];
if (index < 0 || index >= ports.length) return parent;
if (ports[index].name === name) return parent;
const newPorts = ports.map((p, i) => (i === index ? { ...p, name } : p));
return { ...parent, [parentPortsKey]: newPorts };
});
return;
}

updateNodeById(nodeId, (n) => {
const portsKey = direction === 'input' ? 'inputs' : 'outputs';
const ports = n[portsKey] as PortInstance[];
if (index < 0 || index >= ports.length) return n;
if (ports[index].name === name) return n;
const newPorts = ports.map((p, i) => (i === index ? { ...p, name } : p));
return { ...n, [portsKey]: newPorts };
});
}
8 changes: 5 additions & 3 deletions src/lib/stores/graph/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,13 @@ export function getCurrentGraph(): {
...node,
name: subsystem.name,
color: subsystem.color,
// Subsystem inputs → Interface outputs (signals coming in)
// Subsystem inputs → Interface outputs (signals coming in).
// Port names come from the parent — that's the single source
// of truth so user-customised labels survive across renders.
outputs: subsystem.inputs.map((port, i) => ({
id: `${node.id}-output-${i}`,
nodeId: node.id,
name: `in ${i}`,
name: port.name,
direction: 'output' as const,
index: i,
color: port.color
Expand All @@ -108,7 +110,7 @@ export function getCurrentGraph(): {
inputs: subsystem.outputs.map((port, i) => ({
id: `${node.id}-input-${i}`,
nodeId: node.id,
name: `out ${i}`,
name: port.name,
direction: 'input' as const,
index: i,
color: port.color
Expand Down
Loading