Skip to content
Open
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ Press `?` to see all shortcuts in the app. Key shortcuts:
| | `X` / `Y` | Flip H/V |
| | `Arrows` | Nudge selection |
| **Wires** | `\` | Add waypoint to selected edge |
| **Labels** | `L` | Toggle port labels |
| **View** | `F` | Fit view |
| | `H` | Go to root |
| | `T` | Toggle theme |
Expand Down Expand Up @@ -621,6 +622,15 @@ Shapes are defined in `src/lib/nodes/shapes/registry.ts` and applied via CSS cla

Colors are CSS-driven - see `src/app.css` for variables and `src/lib/utils/colors.ts` for palettes.

### Port Labels

Port labels show the name of each input/output port alongside the node. Toggle globally with `L` key, or per-node via right-click menu.

- **Global toggle**: Press `L` to show/hide port labels for all nodes
- **Per-node override**: Right-click node → "Show Input Labels" / "Show Output Labels"
- **Truncation**: Labels are truncated to 5 characters for compact display
- **SVG export**: Port labels are included when exporting the graph as SVG

### Adding Custom Shapes

1. Register the shape in `src/lib/nodes/shapes/registry.ts`:
Expand Down
12 changes: 0 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

115 changes: 107 additions & 8 deletions src/lib/components/contextMenuBuilders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { hasExportableData, exportRecordingData } from '$lib/utils/csvExport';
import { exportToSVG } from '$lib/export/svg';
import { downloadSvg } from '$lib/utils/download';
import { plotSettingsStore, DEFAULT_BLOCK_SETTINGS } from '$lib/stores/plotSettings';
import { portLabelsStore } from '$lib/stores/portLabels';

/** Divider menu item */
const DIVIDER: MenuItemType = { label: '', action: () => {}, divider: true };
Expand Down Expand Up @@ -73,7 +74,13 @@ function buildNodeMenu(nodeId: string): MenuItemType[] {

// Interface blocks have limited options
if (isInterface) {
return [
const globalLabels = get(portLabelsStore);
const showInputLabels = (node.params?.['_showInputLabels'] as boolean | undefined) ?? globalLabels;
const showOutputLabels = (node.params?.['_showOutputLabels'] as boolean | undefined) ?? globalLabels;
const hasInputs = node.inputs && node.inputs.length > 0;
const hasOutputs = node.outputs && node.outputs.length > 0;

const items: MenuItemType[] = [
{
label: 'Properties',
icon: 'settings',
Expand All @@ -84,19 +91,52 @@ function buildNodeMenu(nodeId: string): MenuItemType[] {
label: 'Exit Subsystem',
icon: 'exit',
action: () => graphStore.drillUp()
},
}
];

if (hasInputs || hasOutputs) {
items.push(DIVIDER);
if (hasInputs) {
items.push({
label: showInputLabels ? 'Hide Input Labels' : 'Show Input Labels',
icon: 'tag',
action: () => historyStore.mutate(() =>
graphStore.updateNodeParams(nodeId, { _showInputLabels: !showInputLabels })
)
});
}
if (hasOutputs) {
items.push({
label: showOutputLabels ? 'Hide Output Labels' : 'Show Output Labels',
icon: 'tag',
action: () => historyStore.mutate(() =>
graphStore.updateNodeParams(nodeId, { _showOutputLabels: !showOutputLabels })
)
});
}
}

items.push(
DIVIDER,
{
label: 'View Code',
icon: 'braces',
action: () => showBlockCode(nodeId)
}
];
);

return items;
}

// Subsystem blocks get "Enter" option
if (isSubsystem) {
return [
const globalLabels = get(portLabelsStore);
const showInputLabels = (node.params?.['_showInputLabels'] as boolean | undefined) ?? globalLabels;
const showOutputLabels = (node.params?.['_showOutputLabels'] as boolean | undefined) ?? globalLabels;
const hasInputs = node.inputs && node.inputs.length > 0;
const hasOutputs = node.outputs && node.outputs.length > 0;

const items: MenuItemType[] = [
{
label: 'Properties',
icon: 'settings',
Expand All @@ -107,7 +147,32 @@ function buildNodeMenu(nodeId: string): MenuItemType[] {
icon: 'enter',
shortcut: 'Dbl-click',
action: () => graphStore.drillDown(nodeId)
},
}
];

if (hasInputs || hasOutputs) {
items.push(DIVIDER);
if (hasInputs) {
items.push({
label: showInputLabels ? 'Hide Input Labels' : 'Show Input Labels',
icon: 'tag',
action: () => historyStore.mutate(() =>
graphStore.updateNodeParams(nodeId, { _showInputLabels: !showInputLabels })
)
});
}
if (hasOutputs) {
items.push({
label: showOutputLabels ? 'Hide Output Labels' : 'Show Output Labels',
icon: 'tag',
action: () => historyStore.mutate(() =>
graphStore.updateNodeParams(nodeId, { _showOutputLabels: !showOutputLabels })
)
});
}
}

items.push(
DIVIDER,
{
label: 'View Code',
Expand Down Expand Up @@ -145,28 +210,62 @@ function buildNodeMenu(nodeId: string): MenuItemType[] {
shortcut: 'Del',
action: () => historyStore.mutate(() => graphStore.removeNode(nodeId))
}
];
);

return items;
}

// Check if this is a recording node (Scope or Spectrum)
const isRecordingNode = node.type === 'Scope' || node.type === 'Spectrum';
const dataSource = node.type === 'Scope' ? 'scope' : 'spectrum';

// Per-node port label visibility (undefined = follow global)
const globalLabels = get(portLabelsStore);
const showInputLabels = (node.params?.['_showInputLabels'] as boolean | undefined) ?? globalLabels;
const showOutputLabels = (node.params?.['_showOutputLabels'] as boolean | undefined) ?? globalLabels;
const hasInputs = node.inputs && node.inputs.length > 0;
const hasOutputs = node.outputs && node.outputs.length > 0;

// Regular blocks
const items: MenuItemType[] = [
{
label: 'Properties',
icon: 'settings',
shortcut: 'Dbl-click',
action: () => openNodeDialog(nodeId)
},
}
];

if (hasInputs || hasOutputs) {
items.push(DIVIDER);
if (hasInputs) {
items.push({
label: showInputLabels ? 'Hide Input Labels' : 'Show Input Labels',
icon: 'tag',
action: () => historyStore.mutate(() =>
graphStore.updateNodeParams(nodeId, { _showInputLabels: !showInputLabels })
)
});
}
if (hasOutputs) {
items.push({
label: showOutputLabels ? 'Hide Output Labels' : 'Show Output Labels',
icon: 'tag',
action: () => historyStore.mutate(() =>
graphStore.updateNodeParams(nodeId, { _showOutputLabels: !showOutputLabels })
)
});
}
}

items.push(
DIVIDER,
{
label: 'View Code',
icon: 'braces',
action: () => showBlockCode(nodeId)
}
];
);

// Add CSV export for recording nodes
if (isRecordingNode) {
Expand Down
1 change: 1 addition & 0 deletions src/lib/components/dialogs/KeyboardShortcutsDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
{ keys: ['H'], description: 'Go to root' },
{ keys: ['+'], description: 'Zoom in' },
{ keys: ['-'], description: 'Zoom out' },
{ keys: ['L'], description: 'Port labels' },
{ keys: ['T'], description: 'Theme' }
]
},
Expand Down
5 changes: 5 additions & 0 deletions src/lib/components/icons/Icon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,11 @@
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
{:else if name === 'tag'}
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/>
<line x1="7" y1="7" x2="7.01" y2="7"/>
</svg>
{:else if name === 'font-size-increase'}
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" stroke="none">
<text x="2" y="18" font-size="16" font-weight="700" font-family="system-ui, sans-serif">A</text>
Expand Down
Loading