From 22cbc26b7563d19d8752226a5e338b8ef3201817 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Wed, 8 Apr 2026 10:10:40 +0200 Subject: [PATCH] Any node can become zoomable: add dimension via right-click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual distinction: - Populated dimension: dashed cyan ring + [+] indicator - Empty dimension: subtle gray dashed ring + [○] indicator - No dimension: plain station (no ring) Context menu changes: - "Open dimension" on nodes that have children (zooms in directly) - "Add dimension" on nodes without children (creates empty zoomable space) - Removed old "Add child node" option Add dimension creates an empty children structure on the node with proper parent metadata. The node immediately shows the [○] indicator and becomes zoomable. User enters and builds tracks with FAB + branch handles. Truly recursive metro — any station can contain a world. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../js/Components/MetroMap/MetroCanvas.vue | 71 ++++++++++++++----- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/resources/js/Components/MetroMap/MetroCanvas.vue b/resources/js/Components/MetroMap/MetroCanvas.vue index 2d4774d..913d9b0 100644 --- a/resources/js/Components/MetroMap/MetroCanvas.vue +++ b/resources/js/Components/MetroMap/MetroCanvas.vue @@ -772,7 +772,7 @@ const renderDimension = (dimData, opacity, parentGroup) => { }) .attr('stroke-width', 2) - // Child-dimension indicator + // Child-dimension indicator: populated dimension (has nodes inside) nodeGroups.filter(d => d.children && (d.children.nodes?.length ?? 0) > 0) .append('circle') .attr('r', 16) @@ -783,7 +783,6 @@ const renderDimension = (dimData, opacity, parentGroup) => { .attr('opacity', 0.5) .attr('filter', 'url(#childGlow)') - // "[+]" label for nodes with children nodeGroups.filter(d => d.children && (d.children.nodes?.length ?? 0) > 0) .append('text') .attr('x', 13) @@ -794,6 +793,26 @@ const renderDimension = (dimData, opacity, parentGroup) => { .attr('opacity', 0.8) .text('[+]') + // Empty dimension indicator (dimension exists but no nodes yet) + nodeGroups.filter(d => d.children && (d.children.nodes?.length ?? 0) === 0) + .append('circle') + .attr('r', 14) + .attr('fill', 'none') + .attr('stroke', '#8892b0') + .attr('stroke-width', 0.8) + .attr('stroke-dasharray', '2,4') + .attr('opacity', 0.4) + + nodeGroups.filter(d => d.children && (d.children.nodes?.length ?? 0) === 0) + .append('text') + .attr('x', 13) + .attr('y', -13) + .attr('fill', '#8892b0') + .attr('font-family', "'VT323', monospace") + .attr('font-size', '10px') + .attr('opacity', 0.5) + .text('[○]') + // Station label nodeGroups.append('text') .attr('x', 0) @@ -880,22 +899,35 @@ const handleContextEdit = () => { closeContextMenu() } -const handleContextAddChild = () => { - if (contextMenu.value.node) { - emit('create-node', { - x: contextMenu.value.canvasX, - y: contextMenu.value.canvasY, - parentNodeId: contextMenu.value.node.id, - parentEntityType: contextMenu.value.node.entityType ?? null, - parentEntityId: contextMenu.value.node.entityId ?? null, - dimensionId: contextMenu.value.node.id, - depth: currentDepth.value + 1, - addToNode: contextMenu.value.node, - }) +/** "Add dimension" — attach an empty children object to a node, making it zoomable */ +const handleContextAddDimension = () => { + const node = contextMenu.value.node + if (!node) { closeContextMenu(); return } + + // Create empty dimension structure on the node + node.children = { + lines: [], + nodes: [], + connections: [], + parentEntityType: node.entityType ?? null, + parentEntityId: node.entityId ?? null, + parentName: node.name ?? null, } + + // Re-render to show the new [○] indicator + renderMap() closeContextMenu() } +/** "Open dimension" — zoom into a node that already has children */ +const handleContextZoomIn = () => { + const node = contextMenu.value.node + if (!node?.children) { closeContextMenu(); return } + + closeContextMenu() + commitDimensionChange('in', node) +} + const handleContextDelete = () => { if (contextMenu.value.node) emit('delete-node', contextMenu.value.node) closeContextMenu() @@ -1055,9 +1087,16 @@ defineExpose({ +