@pascal-app/editor 0.7.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/package.json +12 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +755 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +62 -54
  6. package/src/components/editor/export-manager.tsx +32 -3
  7. package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
  8. package/src/components/editor/first-person/build-collider-world.ts +203 -87
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
  10. package/src/components/editor/first-person-controls.tsx +836 -31
  11. package/src/components/editor/floating-action-menu.tsx +287 -196
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floorplan-panel.tsx +5837 -12006
  14. package/src/components/editor/grid.tsx +51 -17
  15. package/src/components/editor/group-move-handle.tsx +316 -0
  16. package/src/components/editor/group-rotate-handle.tsx +382 -0
  17. package/src/components/editor/group-transform-shared.test.ts +205 -0
  18. package/src/components/editor/group-transform-shared.ts +256 -0
  19. package/src/components/editor/handles/handle-arrow.tsx +523 -0
  20. package/src/components/editor/handles/use-handle-drag.ts +213 -0
  21. package/src/components/editor/index.tsx +234 -190
  22. package/src/components/editor/measurement-pill.tsx +75 -0
  23. package/src/components/editor/node-arrow-handles.tsx +1453 -0
  24. package/src/components/editor/selection-manager.tsx +435 -158
  25. package/src/components/editor/site-edge-labels.tsx +44 -20
  26. package/src/components/editor/slab-hole-highlights.tsx +466 -0
  27. package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
  28. package/src/components/editor/thumbnail-generator.tsx +27 -64
  29. package/src/components/editor/use-floorplan-background-placement.ts +122 -35
  30. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  31. package/src/components/editor/use-floorplan-scene-data.ts +14 -2
  32. package/src/components/editor/wall-measurement-label.tsx +11 -6
  33. package/src/components/editor/wall-move-side-handles.tsx +855 -0
  34. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  35. package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
  36. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
  37. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  38. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  39. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
  41. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  42. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
  43. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  44. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
  45. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  46. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
  47. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  49. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
  50. package/src/components/editor-2d/svg-paths.ts +10 -2
  51. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
  52. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  53. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  54. package/src/components/systems/stair/stair-edit-system.tsx +2 -2
  55. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  56. package/src/components/systems/zone/zone-system.tsx +0 -0
  57. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  58. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  59. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  60. package/src/components/tools/fence/fence-drafting.ts +64 -22
  61. package/src/components/tools/item/move-tool.tsx +40 -107
  62. package/src/components/tools/item/placement-math.test.ts +10 -0
  63. package/src/components/tools/item/placement-math.ts +33 -9
  64. package/src/components/tools/item/placement-strategies.ts +201 -25
  65. package/src/components/tools/item/placement-types.ts +10 -1
  66. package/src/components/tools/item/use-draft-node.ts +20 -2
  67. package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
  68. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  69. package/src/components/tools/roof/roof-tool.tsx +108 -12
  70. package/src/components/tools/select/box-select-state.ts +77 -0
  71. package/src/components/tools/select/box-select-tool.tsx +324 -543
  72. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  73. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  74. package/src/components/tools/select/select-candidates.test.ts +109 -0
  75. package/src/components/tools/select/select-candidates.ts +85 -0
  76. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  77. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  78. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  79. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  80. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  81. package/src/components/tools/shared/placement-box.tsx +123 -0
  82. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  83. package/src/components/tools/shared/segment-angle.ts +57 -1
  84. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  85. package/src/components/tools/site/site-flag-model.ts +18 -0
  86. package/src/components/tools/stair/stair-defaults.ts +1 -1
  87. package/src/components/tools/stair/stair-tool.tsx +269 -35
  88. package/src/components/tools/tool-manager.tsx +172 -56
  89. package/src/components/tools/wall/wall-drafting.ts +98 -98
  90. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  91. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  92. package/src/components/tools/zone/zone-tool.tsx +36 -11
  93. package/src/components/ui/action-menu/action-button.tsx +21 -1
  94. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  95. package/src/components/ui/action-menu/control-modes.tsx +5 -76
  96. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  97. package/src/components/ui/action-menu/index.tsx +33 -196
  98. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  99. package/src/components/ui/action-menu/view-toggles.tsx +262 -37
  100. package/src/components/ui/command-palette/editor-commands.tsx +24 -17
  101. package/src/components/ui/command-palette/index.tsx +6 -256
  102. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  103. package/src/components/ui/controls/material-picker.tsx +17 -35
  104. package/src/components/ui/controls/metric-control.tsx +33 -11
  105. package/src/components/ui/floating-level-selector.tsx +48 -11
  106. package/src/components/ui/helpers/helper-manager.tsx +21 -17
  107. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  108. package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
  109. package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
  110. package/src/components/ui/level-duplicate-dialog.tsx +5 -6
  111. package/src/components/ui/panels/node-display.ts +1 -0
  112. package/src/components/ui/panels/paint-panel.tsx +66 -41
  113. package/src/components/ui/panels/panel-manager.tsx +33 -74
  114. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  115. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  116. package/src/components/ui/panels/reference-panel.tsx +28 -13
  117. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  118. package/src/components/ui/primitives/number-input.tsx +1 -1
  119. package/src/components/ui/primitives/sidebar.tsx +0 -0
  120. package/src/components/ui/scene-loader.tsx +1 -3
  121. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  122. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  123. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  124. package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  127. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  128. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  129. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  132. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  133. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  134. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  135. package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
  136. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  137. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  138. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  139. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  140. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  141. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  142. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
  144. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  145. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
  146. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  147. package/src/components/ui/slider.tsx +1 -1
  148. package/src/components/viewer-overlay.tsx +169 -52
  149. package/src/components/viewer-zone-system.tsx +1 -1
  150. package/src/hooks/use-auto-save.ts +24 -3
  151. package/src/hooks/use-ceiling-events.ts +175 -0
  152. package/src/hooks/use-drag-action.ts +124 -0
  153. package/src/hooks/use-keyboard.ts +125 -62
  154. package/src/hooks/use-selection.ts +64 -0
  155. package/src/index.tsx +243 -4
  156. package/src/lib/constants.ts +6 -2
  157. package/src/lib/editor-api.ts +62 -0
  158. package/src/lib/elevator-support.ts +96 -0
  159. package/src/lib/floorplan/apply-alignment.ts +111 -0
  160. package/src/lib/floorplan/index.ts +7 -0
  161. package/src/lib/floorplan/selection-tool.ts +40 -0
  162. package/src/lib/floorplan/stairs.ts +12 -1
  163. package/src/lib/fresh-planar-placement.test.ts +102 -0
  164. package/src/lib/fresh-planar-placement.ts +63 -0
  165. package/src/lib/history.ts +2 -1
  166. package/src/lib/level-duplication.test.ts +0 -2
  167. package/src/lib/level-duplication.ts +55 -43
  168. package/src/lib/level-name.ts +11 -0
  169. package/src/lib/material-paint.ts +212 -20
  170. package/src/lib/placement-metadata.ts +29 -0
  171. package/src/lib/planar-cursor-placement.test.ts +43 -0
  172. package/src/lib/planar-cursor-placement.ts +42 -0
  173. package/src/lib/roof-duplication.ts +2 -2
  174. package/src/lib/scene-bounds.ts +1 -1
  175. package/src/lib/scene-clipboard.ts +267 -0
  176. package/src/lib/scene.ts +13 -4
  177. package/src/lib/sfx-bus.ts +25 -1
  178. package/src/lib/sfx-player.ts +75 -27
  179. package/src/lib/site-boundary.ts +1 -0
  180. package/src/lib/stair-duplication.ts +2 -2
  181. package/src/lib/stair-levels.test.ts +146 -0
  182. package/src/lib/stair-levels.ts +177 -0
  183. package/src/lib/world-grid-snap.ts +199 -0
  184. package/src/lib/zone-content.ts +130 -0
  185. package/src/store/use-alignment-guides.ts +21 -0
  186. package/src/store/use-editor.tsx +342 -91
  187. package/src/store/use-placement-preview.ts +31 -0
  188. package/src/store/use-wall-move-ghosts.ts +36 -0
  189. package/src/store/use-wall-snap-indicator.ts +32 -0
  190. package/tsconfig.json +2 -1
  191. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  192. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  193. package/src/components/feedback-dialog.tsx +0 -265
  194. package/src/components/pascal-radio.tsx +0 -280
  195. package/src/components/preview-button.tsx +0 -16
  196. package/src/components/tools/building/move-building-tool.tsx +0 -157
  197. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  198. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  199. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  200. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
  201. package/src/components/tools/column/column-tool.tsx +0 -97
  202. package/src/components/tools/column/move-column-tool.tsx +0 -105
  203. package/src/components/tools/door/door-math.ts +0 -110
  204. package/src/components/tools/door/door-tool.tsx +0 -324
  205. package/src/components/tools/door/move-door-tool.tsx +0 -412
  206. package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
  207. package/src/components/tools/fence/fence-tool.tsx +0 -346
  208. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
  209. package/src/components/tools/fence/move-fence-tool.tsx +0 -298
  210. package/src/components/tools/item/item-tool.tsx +0 -26
  211. package/src/components/tools/roof/move-roof-tool.tsx +0 -359
  212. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  213. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  214. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  215. package/src/components/tools/slab/slab-tool.tsx +0 -322
  216. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  217. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  218. package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
  219. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
  220. package/src/components/tools/wall/move-wall-tool.tsx +0 -356
  221. package/src/components/tools/wall/wall-tool.tsx +0 -332
  222. package/src/components/tools/window/move-window-tool.tsx +0 -447
  223. package/src/components/tools/window/window-math.ts +0 -117
  224. package/src/components/tools/window/window-tool.tsx +0 -332
  225. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  226. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  227. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  228. package/src/components/ui/panels/ceiling-panel.tsx +0 -250
  229. package/src/components/ui/panels/column-panel.tsx +0 -715
  230. package/src/components/ui/panels/door-panel.tsx +0 -1292
  231. package/src/components/ui/panels/fence-panel.tsx +0 -227
  232. package/src/components/ui/panels/item-panel.tsx +0 -306
  233. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  234. package/src/components/ui/panels/roof-panel.tsx +0 -230
  235. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  236. package/src/components/ui/panels/slab-panel.tsx +0 -250
  237. package/src/components/ui/panels/spawn-panel.tsx +0 -155
  238. package/src/components/ui/panels/stair-panel.tsx +0 -525
  239. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  240. package/src/components/ui/panels/wall-panel.tsx +0 -177
  241. package/src/components/ui/panels/window-panel.tsx +0 -960
  242. package/src/components/ui/viewer-toolbar.tsx +0 -436
  243. package/src/contexts/presets-context.tsx +0 -121
  244. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -0,0 +1,227 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ type CeilingNode,
7
+ getWallMidpointHandlePoint,
8
+ nodeRegistry,
9
+ type SlabNode,
10
+ useLiveNodeOverrides,
11
+ useScene,
12
+ type WallNode,
13
+ } from '@pascal-app/core'
14
+ import { useViewer } from '@pascal-app/viewer'
15
+ import { useEffect, useState } from 'react'
16
+ import { createPortal } from 'react-dom'
17
+ import { sfxEmitter } from '../../lib/sfx-bus'
18
+ import useEditor from '../../store/use-editor'
19
+ import { NodeActionMenu } from '../editor/node-action-menu'
20
+
21
+ /**
22
+ * Floating Move / Duplicate / Delete buttons that appear above the
23
+ * selected registered kind in the floor plan view.
24
+ *
25
+ * Lives outside the floorplan-panel.tsx monolith. Reads selection from
26
+ * `useViewer`, finds the rendered `[data-node-id]` <g> inside the floor
27
+ * plan scene, polls its bounding rect via rAF while open, and portals
28
+ * an HTML overlay positioned at the top of the bounding box.
29
+ *
30
+ * Buttons:
31
+ * - Move: sets `movingNode` in useEditor. Enabled when the kind has
32
+ * `capabilities.movable`, `def.floorplanMoveTarget`, OR
33
+ * `def.affordanceTools.move` (slab / ceiling). The
34
+ * `<FloorplanRegistryMoveOverlay>` / dispatcher picks the right path.
35
+ * Walls are excluded — their move is reached via the side-arrow
36
+ * handles emitted from `def.floorplan`, not via a menu button.
37
+ * - Add hole (slab + ceiling only): inserts a small default-square
38
+ * hole at the polygon centroid via `updateNode`. Mirrors the legacy
39
+ * `handleAddHole` in `floating-action-menu.tsx`.
40
+ * - Duplicate: deep-clones the node, marks it new, sets it as the
41
+ * movingNode (placement cursor) — same UX pattern as 3D duplicate.
42
+ * - Delete: calls `deleteNode(id)`. Cascade is handled by the registry's
43
+ * `relations.cascadeDelete` if declared on the def.
44
+ *
45
+ * Hidden while in a move state (so we don't show buttons over a ghost).
46
+ */
47
+ export function FloorplanRegistryActionMenu() {
48
+ const selectedId = useViewer((s) => s.selection.selectedIds[0]) as AnyNodeId | undefined
49
+ const movingNode = useEditor((s) => s.movingNode)
50
+ const setMovingNode = useEditor((s) => s.setMovingNode)
51
+
52
+ const [position, setPosition] = useState<{ left: number; top: number } | null>(null)
53
+
54
+ // Only show for registered kinds (skip legacy kinds — they have their
55
+ // own FloorplanActionMenuLayer entries).
56
+ const selectedKind = useScene((s) => (selectedId ? (s.nodes[selectedId]?.type ?? null) : null))
57
+ const def = selectedKind ? nodeRegistry.get(selectedKind) : null
58
+ const isRegistryKind = !!def
59
+ const isVisible = isRegistryKind && !movingNode
60
+ const isWall = selectedKind === 'wall'
61
+
62
+ useEffect(() => {
63
+ if (!(isVisible && selectedId)) {
64
+ setPosition(null)
65
+ return
66
+ }
67
+ let raf = 0
68
+ const tick = () => {
69
+ raf = requestAnimationFrame(tick)
70
+ const sceneEl = document.querySelector('[data-floorplan-scene]') as SVGGElement | null
71
+ const svgEl = sceneEl?.ownerSVGElement ?? null
72
+ const ctm = sceneEl?.getScreenCTM() ?? null
73
+ if (!(sceneEl && svgEl && ctm)) {
74
+ setPosition(null)
75
+ return
76
+ }
77
+
78
+ // Walls: anchor at the wall midpoint in screen space so the menu
79
+ // sits over the centre of the wall (not the top of its screen-axis
80
+ // bounding box). Menu itself stays horizontal. Read live overrides
81
+ // too so the anchor tracks the wall during side-arrow / endpoint
82
+ // drags. For curved walls `getWallMidpointHandlePoint` returns the
83
+ // apex point on the arc at t=0.5, matching what the renderer draws.
84
+ if (isWall) {
85
+ const sceneNode = useScene.getState().nodes[selectedId] as WallNode | undefined
86
+ if (!sceneNode) {
87
+ setPosition(null)
88
+ return
89
+ }
90
+ const overrides = useLiveNodeOverrides.getState().get(selectedId) as
91
+ | Partial<WallNode>
92
+ | undefined
93
+ const wall = (overrides ? { ...sceneNode, ...overrides } : sceneNode) as WallNode
94
+ const planMid = getWallMidpointHandlePoint(wall)
95
+ const midPt = svgEl.createSVGPoint()
96
+ midPt.x = planMid.x
97
+ midPt.y = planMid.y
98
+ const midScreen = midPt.matrixTransform(ctm)
99
+ setPosition({ left: midScreen.x, top: midScreen.y })
100
+ return
101
+ }
102
+
103
+ const el = sceneEl.querySelector(`[data-node-id="${selectedId}"]`) as SVGGElement | null
104
+ if (el) {
105
+ const rect = el.getBoundingClientRect()
106
+ setPosition({ left: rect.left + rect.width / 2, top: rect.top })
107
+ } else {
108
+ setPosition(null)
109
+ }
110
+ }
111
+ raf = requestAnimationFrame(tick)
112
+ return () => cancelAnimationFrame(raf)
113
+ }, [isVisible, selectedId, isWall])
114
+
115
+ if (!(isVisible && selectedId && position && def)) return null
116
+
117
+ const node = useScene.getState().nodes[selectedId]
118
+ if (!node) return null
119
+
120
+ // Move button is enabled when any of:
121
+ // - `capabilities.movable` (generic translate-on-XZ — shelf / spawn / fence)
122
+ // - `def.floorplanMoveTarget` (anchor-aware 2D — door / window / item)
123
+ // - `def.affordanceTools.move` (kind-owned 3D mover — slab / ceiling / wall)
124
+ // From the menu's perspective all three are "this kind can move from
125
+ // the floor plan." The `MoveTool` dispatcher resolves the right path —
126
+ // walls land on their bespoke `MoveWallTool` (perpendicular slide
127
+ // with linked-wall cascade) via `affordanceTools.move`.
128
+ const canMove =
129
+ !!def.capabilities.movable || !!def.floorplanMoveTarget || !!def.affordanceTools?.move
130
+ const canDuplicate = def.capabilities.duplicable !== false
131
+ const canDelete = def.capabilities.deletable !== false
132
+ const canAddHole = node.type === 'slab' || node.type === 'ceiling'
133
+
134
+ const handleMove = () => {
135
+ sfxEmitter.emit('sfx:item-pick')
136
+ setMovingNode(node as never)
137
+ // Match the legacy 3D `floating-action-menu`: clear selection so
138
+ // selection-gated affordances unmount during the drag. Specifically
139
+ // the slab / ceiling boundary editor (`ToolManager` shows it when
140
+ // `selectedSlabId !== undefined`) would otherwise stay visible
141
+ // and render its vertex / edge handles on top of the moving mesh
142
+ // in split-view 3D. The move overlay reads `movingNode`, not the
143
+ // selection, so clearing it doesn't disturb the move itself; the
144
+ // commit path re-selects the node when it ends.
145
+ useViewer.getState().setSelection({ selectedIds: [] })
146
+ }
147
+
148
+ const handleAddHole = () => {
149
+ if (!canAddHole) return
150
+ const surfaceNode = node as SlabNode | CeilingNode
151
+ const polygon = surfaceNode.polygon
152
+ if (!polygon || polygon.length < 3) return
153
+
154
+ let cx = 0
155
+ let cz = 0
156
+ for (const [x, z] of polygon) {
157
+ cx += x
158
+ cz += z
159
+ }
160
+ cx /= polygon.length
161
+ cz /= polygon.length
162
+
163
+ const holeSize = 0.5
164
+ const newHole: Array<[number, number]> = [
165
+ [cx - holeSize, cz - holeSize],
166
+ [cx + holeSize, cz - holeSize],
167
+ [cx + holeSize, cz + holeSize],
168
+ [cx - holeSize, cz + holeSize],
169
+ ]
170
+ const currentHoles = surfaceNode.holes ?? []
171
+ const currentMetadata = currentHoles.map(
172
+ (_, index) => surfaceNode.holeMetadata?.[index] ?? { source: 'manual' as const },
173
+ )
174
+ sfxEmitter.emit('sfx:structure-build')
175
+ useScene.getState().updateNode(
176
+ selectedId as AnyNodeId,
177
+ {
178
+ holes: [...currentHoles, newHole],
179
+ holeMetadata: [...currentMetadata, { source: 'manual' as const }],
180
+ } as Partial<AnyNode>,
181
+ )
182
+ }
183
+
184
+ const handleDuplicate = () => {
185
+ if (!node.parentId) return
186
+ sfxEmitter.emit('sfx:item-pick')
187
+ useScene.temporal.getState().pause()
188
+ const cloned = structuredClone(node) as AnyNode & { id?: AnyNodeId }
189
+ delete (cloned as { id?: AnyNodeId }).id
190
+ const prevMeta =
191
+ cloned.metadata && typeof cloned.metadata === 'object' && !Array.isArray(cloned.metadata)
192
+ ? (cloned.metadata as Record<string, unknown>)
193
+ : {}
194
+ cloned.metadata = { ...prevMeta, isNew: true }
195
+ const parsed = def.schema.parse(cloned) as AnyNode
196
+ useScene.getState().createNode(parsed, node.parentId as AnyNodeId)
197
+ setMovingNode(parsed as never)
198
+ useScene.temporal.getState().resume()
199
+ }
200
+
201
+ const handleDelete = () => {
202
+ sfxEmitter.emit('sfx:item-delete')
203
+ useScene.getState().deleteNode(selectedId)
204
+ useViewer.getState().setSelection({ selectedIds: [] })
205
+ }
206
+
207
+ return createPortal(
208
+ <div
209
+ className="pointer-events-none fixed z-30"
210
+ style={{
211
+ left: position.left,
212
+ top: position.top,
213
+ transform: 'translate(-50%, calc(-100% - 32px))',
214
+ }}
215
+ >
216
+ <NodeActionMenu
217
+ onAddHole={canAddHole ? handleAddHole : undefined}
218
+ onDelete={canDelete ? handleDelete : undefined}
219
+ onDuplicate={canDuplicate ? handleDuplicate : undefined}
220
+ onMove={canMove ? handleMove : undefined}
221
+ onPointerDown={(event) => event.stopPropagation()}
222
+ onPointerUp={(event) => event.stopPropagation()}
223
+ />
224
+ </div>,
225
+ document.body,
226
+ )
227
+ }