import { useState } from 'react' import { ArrowDownToLine, ArrowDownUp, ArrowUpToLine, Box as BoxIcon, ChevronDown, ChevronRight, Combine, Eye, EyeOff, Folder, FolderPlus, Layers, MapPin, Spline, SquareDashed, Trash2, Trees, } from 'lucide-react' import { type CanvasSort, type DropPosition, findNode, isCollisionNode, type SceneNode, useMapStore, } from '../store/mapStore' import { scopedObjects, useLoadedTilesetsStore } from '../store/loadedTilesetsStore' /** The canvas sort toggle's per-state icon + label. */ const SORT_META: Record = { dynamic: { icon: ArrowDownUp, label: 'Dynamic — depth-sorted' }, ground: { icon: ArrowDownToLine, label: 'Ground — behind everything' }, sky: { icon: ArrowUpToLine, label: 'Sky — in front of everything' }, } import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' interface DropState { id: string pos: DropPosition } interface Dnd { dragId: string | null drop: DropState | null setDragId: (id: string | null) => void setDrop: (drop: DropState | null) => void commit: () => void end: () => void } function ToolbarButton({ label, onClick, disabled, children, }: { label: string onClick: () => void disabled?: boolean children: React.ReactNode }) { return ( {label} ) } /** The scene node graph: add nodes, reorder via drag/drop, rename, hide. The * selected node drives the right-side inspector. */ export default function MapNodeTree() { const nodes = useMapStore((s) => s.nodes) const grid = useMapStore((s) => s.grid) const addCanvas = useMapStore((s) => s.addCanvas) const addGroup = useMapStore((s) => s.addGroup) const addArea = useMapStore((s) => s.addArea) const addMarker = useMapStore((s) => s.addMarker) const addLink = useMapStore((s) => s.addLink) const addObject = useMapStore((s) => s.addObject) const removeNode = useMapStore((s) => s.removeNode) const moveNode = useMapStore((s) => s.moveNode) const activeNodeId = useMapStore((s) => s.activeNodeId) // object nodes default to the first object across the loaded tilesets const loadedTilesets = useLoadedTilesetsStore((s) => s.tilesets) const firstObject = scopedObjects(loadedTilesets)[0] const [editingId, setEditingId] = useState(null) const [dragId, setDragId] = useState(null) const [drop, setDrop] = useState(null) const dnd: Dnd = { dragId, drop, setDragId, setDrop, commit: () => { if (dragId && drop && dragId !== drop.id) moveNode(dragId, drop.id, drop.pos) setDragId(null) setDrop(null) }, end: () => { setDragId(null) setDrop(null) }, } const areaBox = { x: 0, y: 0, w: grid.tileWidth * 4, h: grid.tileHeight * 4 } // a new link defaults its source to the selected node when that node can be an // endpoint (anything but a group or another link); the inspector picks the target. const activeNode = activeNodeId ? findNode(nodes, activeNodeId) : null const linkSource = activeNode && activeNode.kind !== 'group' && activeNode.kind !== 'link' ? activeNode.id : '' return (

Nodes

addCanvas()}> addArea(areaBox)}> addMarker()}> addLink(linkSource)}> firstObject && addObject(firstObject.tilesetUri, firstObject.object.id)} > addGroup()}> activeNodeId && removeNode(activeNodeId)} >
{nodes.length === 0 ? (

No nodes — add one above.

) : (
    {nodes.map((n) => ( ))}
)}
) } function kindIcon(node: SceneNode) { switch (node.kind) { case 'group': return Folder case 'canvas': return Layers case 'object': return Trees case 'area': return isCollisionNode(node) ? BoxIcon : SquareDashed case 'marker': return MapPin case 'link': return Spline } } function NodeRow({ node, depth, editingId, setEditingId, dnd, }: { node: SceneNode depth: number editingId: string | null setEditingId: (id: string | null) => void dnd: Dnd }) { const activeNodeId = useMapStore((s) => s.activeNodeId) const setActiveNode = useMapStore((s) => s.setActiveNode) const toggleHidden = useMapStore((s) => s.toggleNodeHidden) const toggleCollapsed = useMapStore((s) => s.toggleNodeCollapsed) const renameNode = useMapStore((s) => s.renameNode) const cycleCanvasSort = useMapStore((s) => s.cycleCanvasSort) const toggleComposite = useMapStore((s) => s.toggleComposite) const isGroup = node.kind === 'group' const active = node.id === activeNodeId const editing = editingId === node.id const dropPos = dnd.drop && dnd.drop.id === node.id ? dnd.drop.pos : null const KindIcon = kindIcon(node) const sortMeta = node.kind === 'canvas' ? SORT_META[node.sort] : null const SortIcon = sortMeta?.icon const onDragOver = (e: React.DragEvent) => { if (!dnd.dragId || dnd.dragId === node.id) return e.preventDefault() e.stopPropagation() e.dataTransfer.dropEffect = 'move' const r = e.currentTarget.getBoundingClientRect() const y = (e.clientY - r.top) / r.height const pos: DropPosition = isGroup ? y < 0.25 ? 'before' : y > 0.75 ? 'after' : 'inside' : y < 0.5 ? 'before' : 'after' if (!dnd.drop || dnd.drop.id !== node.id || dnd.drop.pos !== pos) { dnd.setDrop({ id: node.id, pos }) } } return (
  • setActiveNode(node.id)} draggable={!editing} onDragStart={(e) => { e.stopPropagation() e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData('text/plain', node.id) dnd.setDragId(node.id) }} onDragOver={onDragOver} onDrop={(e) => { e.preventDefault() e.stopPropagation() dnd.commit() }} onDragEnd={(e) => { e.stopPropagation() dnd.end() }} > {isGroup ? ( ) : ( )} {editing ? ( e.stopPropagation()} onBlur={(e) => { renameNode(node.id, e.target.value) setEditingId(null) }} onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur() else if (e.key === 'Escape') setEditingId(null) }} /> ) : ( { e.stopPropagation() setEditingId(node.id) }} > {node.name} )} {isGroup && ( {node.composite ? 'Composite — sorts as one unit. Click to make organisational.' : 'Organisational group. Click to composite (sort as one unit).'} )} {node.kind === 'area' && ( {node.boxes.length} )} {node.kind === 'canvas' && sortMeta && SortIcon && ( <> {sortMeta.label} — click to cycle {node.layers.length} )}
    {isGroup && !node.collapsed && node.children.length > 0 && (
      {node.children.map((c) => ( ))}
    )}
  • ) }