import type { NodeDefinition } from '@pascal-app/core' import { buildFenceFloorplan } from './floorplan' import { buildFenceGeometry } from './geometry' import { fenceParametrics } from './parametrics' import { FenceNode } from './schema' /** * Fence — Phase 5 batch kind. Stage B complete: `def.geometry` drives * the rebuild via the generic ``; `` * mounts the empty group. No per-kind renderer or system file. * * Capabilities: * - **No `movable`**: fence move is bespoke endpoint-drag. Capability- * driven dispatch keeps the legacy MoveFenceTool until the * affordance port (Stage D). * - `surfaces.sides`, `selectable`, `duplicable`, `deletable` standard. * * Relations: `linkedBy: 'endpoint-match'` for corner cascade. */ export const fenceDefinition: NodeDefinition = { kind: 'fence', schemaVersion: 1, schema: FenceNode, category: 'structure', defaults: () => ({ object: 'node', parentId: null, visible: true, metadata: {}, start: [0, 0], end: [3, 0], height: 1.8, thickness: 0.08, baseHeight: 0.22, postSpacing: 2, postSize: 0.1, topRailHeight: 0.04, groundClearance: 0, edgeInset: 0.015, baseStyle: 'grounded', showInfill: true, color: '#ffffff', style: 'slat', }), capabilities: { selectable: { hitVolume: 'bbox' }, surfaces: { sides: { faces: 'all' } }, duplicable: true, deletable: true, }, relations: { linkedBy: 'endpoint-match', cascadeDelete: 'none', }, parametrics: fenceParametrics, // Stage D: kind-owned placement tool. Two-click flow (start → end) // with live preview, length / angle HUD, snap to walls / fences / // grid. Mounted by ToolManager when `useEditor.tool === 'fence'` // via the registry's getRegistryTool() — falls back to the legacy // tools[phase][tool] map when missing. tool: () => import('./tool'), // Stage B: pure geometry function. Generic rebuilds // on dirtyNodes; mounts the empty group. // `renderer` + `system` fields dropped along with their files. geometry: buildFenceGeometry, // Stage C: floor-plan rendering. FloorplanRegistryLayer iterates kinds // with `floorplan` set and renders via FloorplanGeometryRenderer. // Legacy `floorplanFenceEntries` short-circuits to [] when fence is // registered (see floorplan-panel.tsx). floorplan: buildFenceFloorplan, // Stage D (in progress): drag-affordance components owned by the kind. // ToolManager looks up these lazy modules at runtime when the matching // editor state activates — no static import from editor → nodes // (which would create a circular dep). affordanceTools: { // Triggered by useEditor.curvingFence. Pure DragAction logic in // actions/curve.ts; this component is the React wrapper using // useDragAction + the cursor visuals. curve: () => import('./curve-tool'), // Triggered by useEditor.movingFenceEndpoint. Pure logic in // actions/move-endpoint.ts (linked-fence cascade, alt-detach, // single-undo dance). Wrapper owns the angle label + detach badge. 'move-endpoint': () => import('./move-endpoint-tool'), // Triggered by useEditor.movingNode when the moving node is a // fence. Whole-fence rigid translation with linked-fence cascade. // Pure logic in actions/move.ts uses the live-drag exception // (mesh.position + useLiveTransforms) to avoid rebuilding fence // geometry every pointer tick; commits the final start/end with // the single-undo dance. move: () => import('./move-tool'), }, toolHints: [ { key: 'Left click', label: 'Set fence start / end' }, { key: 'Shift', label: 'Allow non-45° angles' }, { key: 'Esc', label: 'Cancel' }, ], presentation: { label: 'Fence', description: 'A straight or curved fence segment with configurable posts and infill.', icon: { kind: 'iconify', name: 'lucide:fence' }, paletteSection: 'structure', paletteOrder: 20, }, mcp: { description: 'A fence segment defined by start + end points, with optional curve sagitta.', }, }