Any node can become zoomable: add dimension via right-click
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) <noreply@anthropic.com>
This commit is contained in:
@@ -772,7 +772,7 @@ const renderDimension = (dimData, opacity, parentGroup) => {
|
|||||||
})
|
})
|
||||||
.attr('stroke-width', 2)
|
.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)
|
nodeGroups.filter(d => d.children && (d.children.nodes?.length ?? 0) > 0)
|
||||||
.append('circle')
|
.append('circle')
|
||||||
.attr('r', 16)
|
.attr('r', 16)
|
||||||
@@ -783,7 +783,6 @@ const renderDimension = (dimData, opacity, parentGroup) => {
|
|||||||
.attr('opacity', 0.5)
|
.attr('opacity', 0.5)
|
||||||
.attr('filter', 'url(#childGlow)')
|
.attr('filter', 'url(#childGlow)')
|
||||||
|
|
||||||
// "[+]" label for nodes with children
|
|
||||||
nodeGroups.filter(d => d.children && (d.children.nodes?.length ?? 0) > 0)
|
nodeGroups.filter(d => d.children && (d.children.nodes?.length ?? 0) > 0)
|
||||||
.append('text')
|
.append('text')
|
||||||
.attr('x', 13)
|
.attr('x', 13)
|
||||||
@@ -794,6 +793,26 @@ const renderDimension = (dimData, opacity, parentGroup) => {
|
|||||||
.attr('opacity', 0.8)
|
.attr('opacity', 0.8)
|
||||||
.text('[+]')
|
.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
|
// Station label
|
||||||
nodeGroups.append('text')
|
nodeGroups.append('text')
|
||||||
.attr('x', 0)
|
.attr('x', 0)
|
||||||
@@ -880,22 +899,35 @@ const handleContextEdit = () => {
|
|||||||
closeContextMenu()
|
closeContextMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleContextAddChild = () => {
|
/** "Add dimension" — attach an empty children object to a node, making it zoomable */
|
||||||
if (contextMenu.value.node) {
|
const handleContextAddDimension = () => {
|
||||||
emit('create-node', {
|
const node = contextMenu.value.node
|
||||||
x: contextMenu.value.canvasX,
|
if (!node) { closeContextMenu(); return }
|
||||||
y: contextMenu.value.canvasY,
|
|
||||||
parentNodeId: contextMenu.value.node.id,
|
// Create empty dimension structure on the node
|
||||||
parentEntityType: contextMenu.value.node.entityType ?? null,
|
node.children = {
|
||||||
parentEntityId: contextMenu.value.node.entityId ?? null,
|
lines: [],
|
||||||
dimensionId: contextMenu.value.node.id,
|
nodes: [],
|
||||||
depth: currentDepth.value + 1,
|
connections: [],
|
||||||
addToNode: contextMenu.value.node,
|
parentEntityType: node.entityType ?? null,
|
||||||
})
|
parentEntityId: node.entityId ?? null,
|
||||||
|
parentName: node.name ?? null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-render to show the new [○] indicator
|
||||||
|
renderMap()
|
||||||
closeContextMenu()
|
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 = () => {
|
const handleContextDelete = () => {
|
||||||
if (contextMenu.value.node) emit('delete-node', contextMenu.value.node)
|
if (contextMenu.value.node) emit('delete-node', contextMenu.value.node)
|
||||||
closeContextMenu()
|
closeContextMenu()
|
||||||
@@ -1055,9 +1087,16 @@ defineExpose({
|
|||||||
<button
|
<button
|
||||||
v-if="contextMenu.node?.children"
|
v-if="contextMenu.node?.children"
|
||||||
class="context-item"
|
class="context-item"
|
||||||
@click="handleContextAddChild"
|
@click="handleContextZoomIn"
|
||||||
>
|
>
|
||||||
+ Add child node
|
> Open dimension
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!contextMenu.node?.children"
|
||||||
|
class="context-item"
|
||||||
|
@click="handleContextAddDimension"
|
||||||
|
>
|
||||||
|
+ Add dimension
|
||||||
</button>
|
</button>
|
||||||
<button class="context-item danger" @click="handleContextDelete">Delete</button>
|
<button class="context-item danger" @click="handleContextDelete">Delete</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user