@pascal-app/editor 0.9.1 → 1.0.0-beta.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 (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -0,0 +1,94 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import type { AnyNode, AnyNodeId } from '@pascal-app/core'
3
+ import { collectQuickActionNodeScope } from './quick-action-nodes'
4
+
5
+ function fixtureNode({
6
+ id,
7
+ parentId,
8
+ children = [],
9
+ type = 'item',
10
+ }: {
11
+ id: string
12
+ parentId?: string
13
+ children?: string[]
14
+ type?: string
15
+ }) {
16
+ return {
17
+ id,
18
+ type,
19
+ parentId,
20
+ children,
21
+ } as unknown as AnyNode
22
+ }
23
+
24
+ describe('collectQuickActionNodeScope', () => {
25
+ test('includes nested children of a selected node sibling', () => {
26
+ const run = fixtureNode({
27
+ id: 'run',
28
+ children: ['left-base', 'selected-base'],
29
+ })
30
+ const leftBase = fixtureNode({
31
+ id: 'left-base',
32
+ parentId: run.id,
33
+ children: ['expanded-wall'],
34
+ })
35
+ const selectedBase = fixtureNode({ id: 'selected-base', parentId: run.id })
36
+ const expandedWall = fixtureNode({ id: 'expanded-wall', parentId: leftBase.id })
37
+ const nodes = Object.fromEntries(
38
+ [run, leftBase, selectedBase, expandedWall].map((node) => [node.id, node]),
39
+ ) as Record<AnyNodeId, AnyNode>
40
+
41
+ const collected = collectQuickActionNodeScope(nodes, selectedBase.id)
42
+
43
+ expect(collected?.[expandedWall.id as AnyNodeId]).toBe(expandedWall)
44
+ })
45
+
46
+ test('includes other run subtrees when the provider declares level scope', () => {
47
+ const level = fixtureNode({
48
+ id: 'level',
49
+ type: 'level',
50
+ children: ['selected-run', 'other-run'],
51
+ })
52
+ const selectedRun = fixtureNode({
53
+ id: 'selected-run',
54
+ parentId: level.id,
55
+ children: ['selected-base'],
56
+ })
57
+ const selectedBase = fixtureNode({ id: 'selected-base', parentId: selectedRun.id })
58
+ const otherRun = fixtureNode({
59
+ id: 'other-run',
60
+ parentId: level.id,
61
+ children: ['other-base'],
62
+ })
63
+ const otherBase = fixtureNode({
64
+ id: 'other-base',
65
+ parentId: otherRun.id,
66
+ children: ['expanded-wall'],
67
+ })
68
+ const expandedWall = fixtureNode({ id: 'expanded-wall', parentId: otherBase.id })
69
+ const nodes = Object.fromEntries(
70
+ [level, selectedRun, selectedBase, otherRun, otherBase, expandedWall].map((node) => [
71
+ node.id,
72
+ node,
73
+ ]),
74
+ ) as Record<AnyNodeId, AnyNode>
75
+
76
+ expect(
77
+ collectQuickActionNodeScope(nodes, selectedBase.id)?.[expandedWall.id as AnyNodeId],
78
+ ).toBeUndefined()
79
+ expect(
80
+ collectQuickActionNodeScope(nodes, selectedBase.id, 'level')?.[expandedWall.id as AnyNodeId],
81
+ ).toBe(expandedWall)
82
+ })
83
+
84
+ test('fails closed when a level-scoped provider has no level ancestor', () => {
85
+ const run = fixtureNode({ id: 'run', children: ['selected-base'] })
86
+ const selectedBase = fixtureNode({ id: 'selected-base', parentId: run.id })
87
+ const nodes = Object.fromEntries([run, selectedBase].map((node) => [node.id, node])) as Record<
88
+ AnyNodeId,
89
+ AnyNode
90
+ >
91
+
92
+ expect(collectQuickActionNodeScope(nodes, selectedBase.id, 'level')).toBeNull()
93
+ })
94
+ })
@@ -0,0 +1,47 @@
1
+ import type { AnyNode, AnyNodeId, NodeQuickActionNodeScope } from '@pascal-app/core'
2
+
3
+ export function collectQuickActionNodeScope(
4
+ nodes: Record<AnyNodeId, AnyNode>,
5
+ selectedId: string,
6
+ scope: NodeQuickActionNodeScope = 'family',
7
+ ): Record<AnyNodeId, AnyNode> | null {
8
+ const selected = nodes[selectedId as AnyNodeId]
9
+ if (!selected) return null
10
+
11
+ const collected: Record<AnyNodeId, AnyNode> = {}
12
+ const addSubtree = (rootId: string | null | undefined) => {
13
+ if (!rootId) return
14
+ const pending = [rootId]
15
+
16
+ while (pending.length > 0) {
17
+ const id = pending.pop()
18
+ if (!id || collected[id as AnyNodeId]) continue
19
+ const node = nodes[id as AnyNodeId]
20
+ if (!node) continue
21
+
22
+ collected[node.id as AnyNodeId] = node
23
+ for (const childId of (node as { children?: readonly string[] }).children ?? []) {
24
+ pending.push(childId)
25
+ }
26
+ }
27
+ }
28
+
29
+ if (scope === 'level') {
30
+ const visited = new Set<AnyNodeId>()
31
+ let current: AnyNode | undefined = selected
32
+ while (current && !visited.has(current.id as AnyNodeId)) {
33
+ visited.add(current.id as AnyNodeId)
34
+ if (current.type === 'level') {
35
+ addSubtree(current.id)
36
+ return collected
37
+ }
38
+ current = current.parentId ? nodes[current.parentId as AnyNodeId] : undefined
39
+ }
40
+ return null
41
+ }
42
+
43
+ addSubtree(selected.id)
44
+ addSubtree(selected.parentId)
45
+
46
+ return collected
47
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { createQuickMeasurementPointerScheduler } from './quick-measurement'
3
+
4
+ function pointer(clientX: number, clientY: number): PointerEvent {
5
+ return { clientX, clientY } as PointerEvent
6
+ }
7
+
8
+ function createFrameDriver() {
9
+ let nextId = 1
10
+ const callbacks = new Map<number, FrameRequestCallback>()
11
+ return {
12
+ driver: {
13
+ request: (callback: FrameRequestCallback) => {
14
+ const id = nextId++
15
+ callbacks.set(id, callback)
16
+ return id
17
+ },
18
+ cancel: (frameId: number) => callbacks.delete(frameId),
19
+ },
20
+ pending: () => callbacks.size,
21
+ runNext: (timestamp: number) => {
22
+ const next = callbacks.entries().next().value as [number, FrameRequestCallback] | undefined
23
+ if (!next) throw new Error('No animation frame scheduled')
24
+ callbacks.delete(next[0])
25
+ next[1](timestamp)
26
+ },
27
+ }
28
+ }
29
+
30
+ describe('createQuickMeasurementPointerScheduler', () => {
31
+ test('keeps only the latest pointer event and caps expensive work', () => {
32
+ const frames = createFrameDriver()
33
+ const processed: PointerEvent[] = []
34
+ const scheduler = createQuickMeasurementPointerScheduler(
35
+ (event) => processed.push(event),
36
+ frames.driver,
37
+ )
38
+
39
+ scheduler.enqueue(pointer(1, 1))
40
+ scheduler.enqueue(pointer(8, 4))
41
+ expect(frames.pending()).toBe(1)
42
+ frames.runNext(0)
43
+ expect(processed.map((event) => [event.clientX, event.clientY])).toEqual([[8, 4]])
44
+
45
+ scheduler.enqueue(pointer(12, 4))
46
+ frames.runNext(16)
47
+ expect(processed).toHaveLength(1)
48
+ expect(frames.pending()).toBe(1)
49
+
50
+ scheduler.enqueue(pointer(20, 7))
51
+ frames.runNext(32)
52
+ expect(processed.map((event) => [event.clientX, event.clientY])).toEqual([
53
+ [8, 4],
54
+ [20, 7],
55
+ ])
56
+ })
57
+
58
+ test('ignores sub-pixel jitter and cancels queued work', () => {
59
+ const frames = createFrameDriver()
60
+ const processed: PointerEvent[] = []
61
+ const scheduler = createQuickMeasurementPointerScheduler(
62
+ (event) => processed.push(event),
63
+ frames.driver,
64
+ )
65
+
66
+ scheduler.enqueue(pointer(10, 10))
67
+ frames.runNext(0)
68
+ scheduler.enqueue(pointer(10.5, 10.5))
69
+ frames.runNext(32)
70
+ expect(processed).toHaveLength(1)
71
+
72
+ scheduler.enqueue(pointer(20, 20))
73
+ scheduler.clear()
74
+ expect(frames.pending()).toBe(0)
75
+ expect(processed).toHaveLength(1)
76
+ })
77
+ })
@@ -0,0 +1,109 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ type GeometryContext,
5
+ nodeRegistry,
6
+ type QuickMeasurementReport,
7
+ } from '@pascal-app/core'
8
+
9
+ const QUICK_MEASUREMENT_QUERY_INTERVAL_MS = 30
10
+ const QUICK_MEASUREMENT_MIN_DISTANCE_SQ = 1
11
+
12
+ export function createQuickMeasurementPointerScheduler(
13
+ onPointerMove: (event: PointerEvent) => void,
14
+ frameDriver: {
15
+ request: (callback: FrameRequestCallback) => number
16
+ cancel: (frameId: number) => void
17
+ } = {
18
+ request: (callback) => requestAnimationFrame(callback),
19
+ cancel: (frameId) => cancelAnimationFrame(frameId),
20
+ },
21
+ ): {
22
+ enqueue: (event: PointerEvent) => void
23
+ clear: () => void
24
+ } {
25
+ let latestEvent: PointerEvent | null = null
26
+ let frameId: number | null = null
27
+ let lastProcessedAt = Number.NEGATIVE_INFINITY
28
+ let lastClientX = Number.NaN
29
+ let lastClientY = Number.NaN
30
+
31
+ const schedule = () => {
32
+ if (frameId === null) frameId = frameDriver.request(flush)
33
+ }
34
+ const flush = (timestamp: number) => {
35
+ frameId = null
36
+ if (!latestEvent) return
37
+ if (timestamp - lastProcessedAt < QUICK_MEASUREMENT_QUERY_INTERVAL_MS) {
38
+ schedule()
39
+ return
40
+ }
41
+
42
+ const event = latestEvent
43
+ latestEvent = null
44
+ const deltaX = event.clientX - lastClientX
45
+ const deltaY = event.clientY - lastClientY
46
+ if (
47
+ Number.isFinite(lastClientX) &&
48
+ deltaX * deltaX + deltaY * deltaY < QUICK_MEASUREMENT_MIN_DISTANCE_SQ
49
+ ) {
50
+ return
51
+ }
52
+
53
+ lastClientX = event.clientX
54
+ lastClientY = event.clientY
55
+ lastProcessedAt = timestamp
56
+ onPointerMove(event)
57
+ if (latestEvent) schedule()
58
+ }
59
+
60
+ return {
61
+ enqueue: (event) => {
62
+ latestEvent = event
63
+ schedule()
64
+ },
65
+ clear: () => {
66
+ if (frameId !== null) frameDriver.cancel(frameId)
67
+ latestEvent = null
68
+ frameId = null
69
+ lastProcessedAt = Number.NEGATIVE_INFINITY
70
+ lastClientX = Number.NaN
71
+ lastClientY = Number.NaN
72
+ },
73
+ }
74
+ }
75
+
76
+ export function quickMeasurementContext(
77
+ node: AnyNode,
78
+ nodes: Record<AnyNodeId, AnyNode>,
79
+ ): GeometryContext {
80
+ const resolve: GeometryContext['resolve'] = <N = AnyNode>(id: AnyNodeId) =>
81
+ nodes[id] as N | undefined
82
+ const childIds =
83
+ 'children' in node && Array.isArray(node.children) ? (node.children as AnyNodeId[]) : []
84
+ const children = childIds
85
+ .map((id) => nodes[id])
86
+ .filter((child): child is AnyNode => child !== undefined)
87
+ const parent = node.parentId ? (nodes[node.parentId as AnyNodeId] ?? null) : null
88
+ const siblings =
89
+ parent && 'children' in parent && Array.isArray(parent.children)
90
+ ? (parent.children as AnyNodeId[])
91
+ .map((id) => nodes[id])
92
+ .filter(
93
+ (sibling): sibling is AnyNode => sibling !== undefined && sibling.type === node.type,
94
+ )
95
+ : []
96
+
97
+ return { resolve, children, parent, siblings }
98
+ }
99
+
100
+ export function resolveQuickMeasurementReport(
101
+ nodeId: string | null,
102
+ nodes: Record<AnyNodeId, AnyNode>,
103
+ ): QuickMeasurementReport | null {
104
+ if (!nodeId) return null
105
+ const node = nodes[nodeId as AnyNodeId]
106
+ if (!node || node.visible === false) return null
107
+ const quickMeasure = nodeRegistry.get(node.type)?.measurement?.quickMeasure
108
+ return quickMeasure ? quickMeasure(node, quickMeasurementContext(node, nodes)) : null
109
+ }
@@ -80,7 +80,7 @@ export function duplicateRoofSubtree(
80
80
  const scene = useScene.getState()
81
81
  const sourceRoof = scene.nodes[sourceRoofId]
82
82
 
83
- if (!sourceRoof || sourceRoof.type !== 'roof') {
83
+ if (sourceRoof?.type !== 'roof') {
84
84
  throw new Error(`Node "${sourceRoofId}" is not a roof`)
85
85
  }
86
86
 
@@ -105,7 +105,7 @@ export function duplicateRoofSubtree(
105
105
  const segmentClones: RoofSegmentNode[] = []
106
106
  for (const childId of sourceRoof.children ?? []) {
107
107
  const childNode = scene.nodes[childId as AnyNodeId]
108
- if (!childNode || childNode.type !== 'roof-segment') {
108
+ if (childNode?.type !== 'roof-segment') {
109
109
  continue
110
110
  }
111
111
 
@@ -125,7 +125,7 @@ export function duplicateRoofSubtree(
125
125
 
126
126
  const nextScene = useScene.getState()
127
127
  const createdRoof = nextScene.nodes[roofClone.id as AnyNodeId]
128
- if (!createdRoof || createdRoof.type !== 'roof') {
128
+ if (createdRoof?.type !== 'roof') {
129
129
  throw new Error(`Duplicated roof "${roofClone.id}" was not created`)
130
130
  }
131
131
 
@@ -151,7 +151,7 @@ export function duplicateRoofSubtree(
151
151
 
152
152
  const invalidSegment = segmentIds.find((segmentId) => {
153
153
  const segment = nextScene.nodes[segmentId as AnyNodeId]
154
- return !segment || segment.type !== 'roof-segment' || segment.parentId !== createdRoof.id
154
+ return segment?.type !== 'roof-segment' || segment.parentId !== createdRoof.id
155
155
  })
156
156
  if (invalidSegment) {
157
157
  throw new Error(
@@ -179,7 +179,7 @@ export function clearRoofDuplicateMetadata(
179
179
  ) {
180
180
  const scene = useScene.getState()
181
181
  const roofNode = scene.nodes[roofId]
182
- if (!roofNode || roofNode.type !== 'roof') {
182
+ if (roofNode?.type !== 'roof') {
183
183
  return
184
184
  }
185
185
 
@@ -198,7 +198,7 @@ export function clearRoofDuplicateMetadata(
198
198
 
199
199
  for (const childId of roofNode.children ?? []) {
200
200
  const childNode = scene.nodes[childId as AnyNodeId]
201
- if (!childNode || childNode.type !== 'roof-segment') {
201
+ if (childNode?.type !== 'roof-segment') {
202
202
  continue
203
203
  }
204
204
 
@@ -0,0 +1,22 @@
1
+ import { type AnyNodeId, sceneRegistry, useScene } from '@pascal-app/core'
2
+ import type * as THREE from 'three'
3
+
4
+ export const HOVERED_ROOF_SEGMENT_OUTLINE_PROXY_NAME = '__roof-hover-outline-proxy__'
5
+
6
+ function hoveredRoofSegmentOutlineProxyName(segmentId: string) {
7
+ return `${HOVERED_ROOF_SEGMENT_OUTLINE_PROXY_NAME}:${segmentId}`
8
+ }
9
+
10
+ export function getHoveredRoofSegmentOutlineProxy(segmentId: string): THREE.Object3D | null {
11
+ const segment = useScene.getState().nodes[segmentId as AnyNodeId]
12
+ if (!(segment?.type === 'roof-segment' && segment.parentId)) return null
13
+ return (
14
+ sceneRegistry.nodes
15
+ .get(segment.parentId)
16
+ ?.getObjectByName(hoveredRoofSegmentOutlineProxyName(segmentId)) ?? null
17
+ )
18
+ }
19
+
20
+ export function getHoveredRoofSegmentOutlineProxyName(segmentId: string): string {
21
+ return hoveredRoofSegmentOutlineProxyName(segmentId)
22
+ }
@@ -0,0 +1,164 @@
1
+ import {
2
+ type AnyNodeId,
3
+ getRoofSegmentWallFaces,
4
+ getScaledDimensions,
5
+ type ItemNode,
6
+ type RoofNode,
7
+ type RoofSegmentNode,
8
+ type RoofSegmentWallFace,
9
+ type RoofWallFaceId,
10
+ sceneRegistry,
11
+ segmentPointToRoofWallFace,
12
+ useScene,
13
+ } from '@pascal-app/core'
14
+ import * as THREE from 'three'
15
+
16
+ const worldPoint = new THREE.Vector3()
17
+ const worldNormal = new THREE.Vector3()
18
+ const localPoint = new THREE.Vector3()
19
+ const localNormal = new THREE.Vector3()
20
+ const inverseMatrix = new THREE.Matrix4()
21
+
22
+ export type RoofWallHit = {
23
+ segment: RoofSegmentNode
24
+ face: RoofSegmentWallFace
25
+ /** Face coords of the hit (u along the face, v above the segment base). */
26
+ u: number
27
+ v: number
28
+ }
29
+
30
+ /** Pointer hits more than this far off the wall plane are not wall hits. */
31
+ const PLANE_TOLERANCE = 0.06
32
+ /** Reject faces whose normal disagrees with the hit normal (slope / soffit). */
33
+ const NORMAL_ALIGNMENT = 0.7
34
+ /** A wall face is vertical; slope faces on low pitches have |ny| ≫ 0. */
35
+ const MAX_NORMAL_Y = 0.4
36
+
37
+ /**
38
+ * Resolve a pointer hit on a roof to one of its segments' vertical wall
39
+ * faces (base walls under the roof + the coplanar gable/shed/gambrel end
40
+ * faces). Counterpart of `resolveRoofSegmentHit`, which resolves to the
41
+ * sloped top surface instead.
42
+ *
43
+ * `normal` must be the raw `NodeEvent.normal` (hit-object-local) together
44
+ * with the `object` it came from — roof events can originate from the
45
+ * merged-roof mesh (roof-local frame) or a painted segment mesh
46
+ * (segment-local frame), so the normal is normalised through world space
47
+ * here instead of trusting the event frame.
48
+ *
49
+ * Lives in `@pascal-app/editor` because both the kind-owned door/window
50
+ * tools (in `@pascal-app/nodes`, which depends on editor) and the item
51
+ * placement coordinator (in editor itself) consume it.
52
+ */
53
+ export function resolveRoofWallHit(
54
+ roof: RoofNode,
55
+ position: [number, number, number],
56
+ normal: [number, number, number] | undefined,
57
+ object: THREE.Object3D | undefined,
58
+ ): RoofWallHit | null {
59
+ if (!normal || !object) return null
60
+
61
+ worldPoint.set(position[0], position[1], position[2])
62
+ worldNormal.set(normal[0], normal[1], normal[2])
63
+ object.updateWorldMatrix(true, false)
64
+ worldNormal.transformDirection(object.matrixWorld)
65
+
66
+ const state = useScene.getState()
67
+ let best: { hit: RoofWallHit; score: number } | null = null
68
+
69
+ for (const childId of roof.children ?? []) {
70
+ const segment = state.nodes[childId as AnyNodeId] as RoofSegmentNode | undefined
71
+ if (segment?.type !== 'roof-segment') continue
72
+ const segObj = sceneRegistry.nodes.get(segment.id)
73
+ if (!segObj) continue
74
+ segObj.updateWorldMatrix(true, false)
75
+
76
+ localPoint.copy(worldPoint)
77
+ segObj.worldToLocal(localPoint)
78
+ inverseMatrix.copy(segObj.matrixWorld).invert()
79
+ localNormal.copy(worldNormal).transformDirection(inverseMatrix)
80
+
81
+ if (Math.abs(localNormal.y) > MAX_NORMAL_Y) continue
82
+
83
+ for (const face of getRoofSegmentWallFaces(segment)) {
84
+ const alignment =
85
+ localNormal.x * face.normal[0] +
86
+ localNormal.y * face.normal[1] +
87
+ localNormal.z * face.normal[2]
88
+ if (alignment < NORMAL_ALIGNMENT) continue
89
+
90
+ const { u, v, dist } = segmentPointToRoofWallFace(segment, face.id, [
91
+ localPoint.x,
92
+ localPoint.y,
93
+ localPoint.z,
94
+ ])
95
+ if (Math.abs(dist) > PLANE_TOLERANCE) continue
96
+ if (u < -PLANE_TOLERANCE || u > face.length + PLANE_TOLERANCE) continue
97
+ if (v < -PLANE_TOLERANCE) continue
98
+
99
+ const score = Math.abs(dist)
100
+ if (!best || score < best.score) {
101
+ best = { hit: { segment, face, u, v }, score }
102
+ }
103
+ }
104
+ }
105
+
106
+ return best?.hit ?? null
107
+ }
108
+
109
+ /**
110
+ * Overlap guard for nodes sharing a roof-segment wall face — the
111
+ * roof-host analogue of `hasWallChildOverlap`. Hosted children store
112
+ * FACE-LOCAL coords + an explicit `roofFace`, so siblings compare
113
+ * directly: doors/windows are center-anchored in v, wall items
114
+ * bottom-anchored.
115
+ */
116
+ export function hasRoofFaceChildOverlap(
117
+ segment: RoofSegmentNode,
118
+ faceId: RoofWallFaceId,
119
+ u: number,
120
+ v: number,
121
+ width: number,
122
+ height: number,
123
+ ignoreId?: string,
124
+ ): boolean {
125
+ const nodes = useScene.getState().nodes
126
+ const newLeft = u - width / 2
127
+ const newRight = u + width / 2
128
+ const newBottom = v - height / 2
129
+ const newTop = v + height / 2
130
+
131
+ for (const childId of segment.children ?? []) {
132
+ if (childId === ignoreId) continue
133
+ const child = nodes[childId as AnyNodeId]
134
+ if (!child) continue
135
+ if ((child as { roofFace?: RoofWallFaceId }).roofFace !== faceId) continue
136
+ const position = (child as { position?: [number, number, number] }).position
137
+ if (!position) continue
138
+
139
+ let childW: number
140
+ let childBottom: number
141
+ let childTop: number
142
+ if (child.type === 'door' || child.type === 'window') {
143
+ const opening = child as { width: number; height: number }
144
+ childW = opening.width
145
+ childBottom = position[1] - opening.height / 2
146
+ childTop = position[1] + opening.height / 2
147
+ } else if (child.type === 'item') {
148
+ const item = child as ItemNode
149
+ if (item.asset.attachTo !== 'wall' && item.asset.attachTo !== 'wall-side') continue
150
+ const [w, h] = getScaledDimensions(item)
151
+ childW = w
152
+ // Items anchor position[1] at their bottom edge.
153
+ childBottom = position[1]
154
+ childTop = position[1] + h
155
+ } else {
156
+ continue
157
+ }
158
+
159
+ const xOverlap = newLeft < position[0] + childW / 2 && newRight > position[0] - childW / 2
160
+ const yOverlap = newBottom < childTop && newTop > childBottom
161
+ if (xOverlap && yOverlap) return true
162
+ }
163
+ return false
164
+ }