@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,512 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNodeId,
4
+ type AssetInput,
5
+ acquireSceneHistoryPause,
6
+ beginSceneHistoryPauseSession,
7
+ clearSceneHistory,
8
+ emitter,
9
+ getSceneHistoryPauseDepth,
10
+ ItemNode,
11
+ initSpaceDetectionSync,
12
+ LevelNode,
13
+ pauseSceneHistory,
14
+ resumeSceneHistory,
15
+ type SceneCommit,
16
+ sceneRegistry,
17
+ spatialGridManager,
18
+ subscribeSceneCommits,
19
+ useLiveNodeOverrides,
20
+ useLiveTransforms,
21
+ useScene,
22
+ WallNode,
23
+ } from '@pascal-app/core'
24
+ import { useViewer } from '@pascal-app/viewer'
25
+ import { Html } from '@react-three/drei'
26
+ import { act, create } from '@react-three/test-renderer'
27
+ import { Children, Component, cloneElement, isValidElement, type ReactNode } from 'react'
28
+ import { Group } from 'three'
29
+ import useEditor from '../../../store/use-editor'
30
+ import useInteractionScope from '../../../store/use-interaction-scope'
31
+ import { useDraftNode } from './use-draft-node'
32
+ import { usePlacementCoordinator } from './use-placement-coordinator'
33
+
34
+ // The item move / placement history contract, driven through the real placement coordinator
35
+ // with core's space-detection sync attached. The carry pauses no history: the draft is kept out
36
+ // of history snapshots, so the drop is one ordinary tracked step, a write someone else makes
37
+ // mid-carry is its own step (in commit order, reconciled by space detection), and undoing that
38
+ // step mid-carry leaves the draft alone. Every exit path releases only its own pauses.
39
+
40
+ const level = LevelNode.parse({ id: 'level_placement_history', level: 0 })
41
+ const wallA = WallNode.parse({
42
+ id: 'wall_placement_history_a',
43
+ parentId: level.id,
44
+ start: [-6, -6],
45
+ end: [6, -6],
46
+ })
47
+ const wallB = WallNode.parse({
48
+ id: 'wall_placement_history_b',
49
+ parentId: level.id,
50
+ start: [6, -6],
51
+ end: [6, 6],
52
+ })
53
+ const wallC = WallNode.parse({
54
+ id: 'wall_placement_history_c',
55
+ parentId: level.id,
56
+ start: [6, 6],
57
+ end: [-6, 6],
58
+ })
59
+ const closingWall = WallNode.parse({
60
+ id: 'wall_placement_history_closing',
61
+ parentId: level.id,
62
+ start: [-6, 6],
63
+ end: [-6, -6],
64
+ })
65
+ const bathtub: AssetInput = {
66
+ id: 'bathtub',
67
+ name: 'Bathtub',
68
+ category: 'bathroom',
69
+ thumbnail: '',
70
+ src: '/bathtub.glb',
71
+ dimensions: [1.7, 0.6, 0.8],
72
+ }
73
+ const item = ItemNode.parse({
74
+ id: 'item_placement_history_bathtub',
75
+ parentId: level.id,
76
+ asset: bathtub,
77
+ position: [0, 0, 0],
78
+ rotation: [0, 0, 0],
79
+ })
80
+
81
+ let savedScene: ReturnType<typeof useScene.getState>
82
+ let savedEditor: ReturnType<typeof useEditor.getState>
83
+ let savedViewer: ReturnType<typeof useViewer.getState>
84
+ let savedScope: ReturnType<typeof useInteractionScope.getState>
85
+ let restoreGlobals: () => void
86
+ let stopSpaceDetection: () => void
87
+
88
+ beforeEach(() => {
89
+ savedScene = useScene.getState()
90
+ savedEditor = useEditor.getState()
91
+ savedViewer = useViewer.getState()
92
+ savedScope = useInteractionScope.getState()
93
+ const names = ['window', 'document', 'requestAnimationFrame', 'cancelAnimationFrame'] as const
94
+ const descriptors = names.map((name) => Object.getOwnPropertyDescriptor(globalThis, name))
95
+ restoreGlobals = () =>
96
+ names.forEach((name, i) => {
97
+ const descriptor = descriptors[i]
98
+ if (descriptor) Object.defineProperty(globalThis, name, descriptor)
99
+ else Reflect.deleteProperty(globalThis, name)
100
+ })
101
+ globalThis.window = new EventTarget() as Window & typeof globalThis
102
+ globalThis.document = { body: { style: { cursor: '' } } } as Document
103
+ globalThis.requestAnimationFrame = () => 0
104
+ globalThis.cancelAnimationFrame = () => {}
105
+
106
+ useScene.setState({
107
+ nodes: {
108
+ [level.id]: { ...level, children: [wallA.id, wallB.id, wallC.id, item.id] },
109
+ [wallA.id]: structuredClone(wallA),
110
+ [wallB.id]: structuredClone(wallB),
111
+ [wallC.id]: structuredClone(wallC),
112
+ [item.id]: structuredClone(item),
113
+ },
114
+ rootNodeIds: [level.id],
115
+ collections: {},
116
+ materials: {},
117
+ dirtyNodes: new Set(),
118
+ readOnly: false,
119
+ } as never)
120
+ clearSceneHistory()
121
+ spatialGridManager.clear()
122
+ useLiveNodeOverrides.getState().clearAll()
123
+ useLiveTransforms.getState().clearAll()
124
+ useInteractionScope.getState().end()
125
+ useEditor.setState({
126
+ mode: 'build',
127
+ tool: 'item',
128
+ movingNodeOrigin: '3d',
129
+ placementDragMode: false,
130
+ })
131
+ useEditor.getState().setSnappingMode('item', 'grid')
132
+ useEditor.setState({ gridSnapStep: 0.1 })
133
+ useViewer.setState({
134
+ selection: { buildingId: null, levelId: level.id, zoneId: null, selectedIds: [] },
135
+ })
136
+ sceneRegistry.nodes.set(level.id, new Group())
137
+ stopSpaceDetection = initSpaceDetectionSync(useScene, {
138
+ getState: () => ({ spaces: {}, setSpaces: () => {} }),
139
+ })
140
+ })
141
+
142
+ afterEach(() => {
143
+ stopSpaceDetection()
144
+ sceneRegistry.nodes.clear()
145
+ spatialGridManager.clear()
146
+ useLiveNodeOverrides.getState().clearAll()
147
+ useLiveTransforms.getState().clearAll()
148
+ clearSceneHistory()
149
+ useScene.setState(savedScene)
150
+ useEditor.setState(savedEditor)
151
+ useViewer.setState(savedViewer)
152
+ useInteractionScope.setState(savedScope)
153
+ restoreGlobals()
154
+ })
155
+
156
+ // The DOM-only measurement labels need a canvas; the cursor refs they sit beside do not.
157
+ function withoutLabels(element: ReactNode): ReactNode {
158
+ if (!isValidElement<{ children?: ReactNode }>(element)) return element
159
+ if (element.type === Html) return null
160
+ return cloneElement(element, {}, Children.map(element.props.children, withoutLabels))
161
+ }
162
+
163
+ function Placement({
164
+ source,
165
+ repeat = false,
166
+ failSetup = false,
167
+ }: {
168
+ source?: ItemNode
169
+ repeat?: boolean
170
+ failSetup?: boolean
171
+ }) {
172
+ const draftNode = useDraftNode()
173
+ return withoutLabels(
174
+ usePlacementCoordinator({
175
+ asset: source?.asset ?? bathtub,
176
+ draftNode,
177
+ initDraft: (position) => {
178
+ if (failSetup) throw new Error('injected setup failure')
179
+ if (source) {
180
+ draftNode.adopt(source)
181
+ position.set(...draftNode.current!.position)
182
+ } else draftNode.create(position, bathtub)
183
+ },
184
+ onCommitted: () => repeat,
185
+ onCancel: () => draftNode.destroy(),
186
+ }),
187
+ )
188
+ }
189
+
190
+ class SetupBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
191
+ state = { failed: false }
192
+ static getDerivedStateFromError() {
193
+ return { failed: true }
194
+ }
195
+ render() {
196
+ return this.state.failed ? null : this.props.children
197
+ }
198
+ }
199
+
200
+ const history = () => {
201
+ const temporal = useScene.temporal.getState()
202
+ return {
203
+ past: temporal.pastStates.length,
204
+ tracking: temporal.isTracking,
205
+ depth: getSceneHistoryPauseDepth(),
206
+ }
207
+ }
208
+ const liveItem = () => useScene.getState().nodes[item.id as AnyNodeId] as ItemNode
209
+ const levelItems = () =>
210
+ Object.values(useScene.getState().nodes).filter(
211
+ (node) => node.type === 'item' && !node.metadata?.isTransient,
212
+ )
213
+
214
+ async function grid(kind: 'move' | 'click', x: number, z = 0) {
215
+ await act(async () => {
216
+ emitter.emit(`grid:${kind}`, {
217
+ position: [x, 0, z],
218
+ localPosition: [x, 0, z],
219
+ nativeEvent: {},
220
+ stopPropagation() {},
221
+ } as never)
222
+ await new Promise((resolve) => setTimeout(resolve, 5))
223
+ })
224
+ }
225
+
226
+ async function carry(from: number, to: number) {
227
+ for (let step = from; step <= to; step++) await grid('move', step * 0.1, 0.5)
228
+ }
229
+
230
+ /** Another refcounted owner's balanced pause/resume pair landing mid-carry. */
231
+ function foreignPausePair() {
232
+ pauseSceneHistory(useScene)
233
+ resumeSceneHistory(useScene)
234
+ }
235
+
236
+ /** Someone else closes the room mid-carry; the space-detection sync owes it a slab and ceiling. */
237
+ function closeRoom() {
238
+ useScene.getState().createNode(structuredClone(closingWall), level.id as AnyNodeId)
239
+ }
240
+ const autoRoomNodes = () =>
241
+ Object.values(useScene.getState().nodes).filter(
242
+ (node) => (node.type === 'slab' || node.type === 'ceiling') && 'autoFromWalls' in node,
243
+ )
244
+ const hasNode = (id: string) => Boolean(useScene.getState().nodes[id as AnyNodeId])
245
+
246
+ describe('item move history', () => {
247
+ test('a move adds one entry and one undo restores the item', async () => {
248
+ const before = history()
249
+ const renderer = await create(<Placement source={item} />)
250
+ try {
251
+ await carry(1, 6)
252
+ expect(history().past).toBe(before.past)
253
+ await grid('click', 0.6, 0.5)
254
+ } finally {
255
+ await renderer.unmount()
256
+ }
257
+ expect(liveItem().position.map((value) => Number(value.toFixed(6)))).toEqual([0.6, 0, 0.5])
258
+ expect(history()).toEqual({ past: before.past + 1, tracking: true, depth: 0 })
259
+ useScene.temporal.getState().undo()
260
+ expect(liveItem()).toEqual(item)
261
+ })
262
+
263
+ test('a foreign pause owner mid-carry leaves the drop one entry', async () => {
264
+ const before = history()
265
+ const renderer = await create(<Placement source={item} />)
266
+ try {
267
+ await carry(1, 3)
268
+ foreignPausePair()
269
+ await carry(4, 6)
270
+ expect(history().past).toBe(before.past)
271
+ await grid('click', 0.6, 0.5)
272
+ } finally {
273
+ await renderer.unmount()
274
+ }
275
+ expect(history()).toEqual({ past: before.past + 1, tracking: true, depth: 0 })
276
+ useScene.temporal.getState().undo()
277
+ expect(liveItem()).toEqual(item)
278
+ })
279
+
280
+ test('a room closed mid-carry is its own step, in commit order, before the drop', async () => {
281
+ const before = history()
282
+ const commits: SceneCommit[] = []
283
+ const stop = subscribeSceneCommits((commit) => commits.push(commit))
284
+ const renderer = await create(<Placement source={item} />)
285
+ try {
286
+ await carry(1, 3)
287
+ closeRoom()
288
+ expect(history().past).toBe(before.past + 1)
289
+ await carry(4, 6)
290
+ await grid('click', 0.6, 0.5)
291
+ } finally {
292
+ await renderer.unmount()
293
+ stop()
294
+ }
295
+ expect(history()).toEqual({ past: before.past + 2, tracking: true, depth: 0 })
296
+ expect(
297
+ autoRoomNodes()
298
+ .map((node) => node.type)
299
+ .sort(),
300
+ ).toEqual(['ceiling', 'slab'])
301
+ const local = commits.filter((commit) => commit.origin === 'local')
302
+ expect(
303
+ local.map(
304
+ (commit) =>
305
+ Boolean(commit.current.nodes[closingWall.id as AnyNodeId]) &&
306
+ !commit.before.nodes[closingWall.id as AnyNodeId],
307
+ ),
308
+ ).toEqual([true, false])
309
+ const dropped = local[1]!.current.nodes[item.id as AnyNodeId] as ItemNode
310
+ expect(dropped.position.map((value) => Number(value.toFixed(6)))).toEqual([0.6, 0, 0.5])
311
+ // The draft never reaches history: every recorded snapshot has the item as it was.
312
+ expect(local[0]!.current.nodes[item.id as AnyNodeId]).toEqual(item)
313
+
314
+ useScene.temporal.getState().undo()
315
+ expect(liveItem()).toEqual(item)
316
+ expect(hasNode(closingWall.id)).toBe(true)
317
+ expect(autoRoomNodes()).toHaveLength(2)
318
+
319
+ useScene.temporal.getState().undo()
320
+ expect(liveItem()).toEqual(item)
321
+ expect(hasNode(closingWall.id)).toBe(false)
322
+ expect(autoRoomNodes()).toHaveLength(0)
323
+ expect(history().past).toBe(before.past)
324
+ })
325
+
326
+ test('undoing the foreign step mid-carry leaves the draft where it is', async () => {
327
+ const before = history()
328
+ const renderer = await create(<Placement source={item} />)
329
+ try {
330
+ await carry(1, 3)
331
+ closeRoom()
332
+ const carried = liveItem()
333
+ useScene.temporal.getState().undo()
334
+ expect(hasNode(closingWall.id)).toBe(false)
335
+ expect(liveItem()).toEqual(carried)
336
+ await carry(4, 6)
337
+ await grid('click', 0.6, 0.5)
338
+ } finally {
339
+ await renderer.unmount()
340
+ }
341
+ expect(history()).toEqual({ past: before.past + 1, tracking: true, depth: 0 })
342
+ expect(liveItem().position.map((value) => Number(value.toFixed(6)))).toEqual([0.6, 0, 0.5])
343
+ useScene.temporal.getState().undo()
344
+ expect(liveItem()).toEqual(item)
345
+ })
346
+
347
+ test('a room closed during a cancelled carry is its own step and the item is unchanged', async () => {
348
+ const before = history()
349
+ const renderer = await create(<Placement source={item} />)
350
+ try {
351
+ await carry(1, 3)
352
+ closeRoom()
353
+ await act(async () => {
354
+ emitter.emit('tool:cancel')
355
+ })
356
+ } finally {
357
+ await renderer.unmount()
358
+ }
359
+ expect(liveItem()).toEqual(item)
360
+ expect(history()).toEqual({ past: before.past + 1, tracking: true, depth: 0 })
361
+ useScene.temporal.getState().undo()
362
+ expect(hasNode(closingWall.id)).toBe(false)
363
+ expect(liveItem()).toEqual(item)
364
+ })
365
+
366
+ test('cancel and unmount add no entry and restore the item', async () => {
367
+ for (const exit of ['cancel', 'unmount'] as const) {
368
+ const before = history()
369
+ const renderer = await create(<Placement source={item} />)
370
+ await carry(1, 4)
371
+ if (exit === 'cancel') await act(async () => emitter.emit('tool:cancel'))
372
+ await renderer.unmount()
373
+ expect(liveItem()).toEqual(item)
374
+ expect(history()).toEqual({ past: before.past, tracking: true, depth: 0 })
375
+ }
376
+ })
377
+
378
+ test('with history at its 50-entry limit, the wall step and the drop still undo in order', async () => {
379
+ for (let index = 0; index < 60; index++) {
380
+ useScene.getState().updateNode(wallA.id as AnyNodeId, { thickness: 0.1 + index / 1000 })
381
+ }
382
+ expect(history().past).toBe(50)
383
+ const renderer = await create(<Placement source={item} />)
384
+ try {
385
+ await carry(1, 3)
386
+ closeRoom()
387
+ await grid('click', 0.3, 0.5)
388
+ } finally {
389
+ await renderer.unmount()
390
+ }
391
+ expect(history().past).toBe(50)
392
+ useScene.temporal.getState().undo()
393
+ expect(liveItem()).toEqual(item)
394
+ expect(hasNode(closingWall.id)).toBe(true)
395
+ useScene.temporal.getState().undo()
396
+ expect(hasNode(closingWall.id)).toBe(false)
397
+ })
398
+
399
+ test('in split view the 2D overlay co-owns the gesture, and the 3D drop is one step', async () => {
400
+ useEditor.getState().setMovingNode(item)
401
+ const overlay = beginSceneHistoryPauseSession(useScene, { gesture: item.id })
402
+ const before = history()
403
+ const renderer = await create(<Placement source={item} />)
404
+ try {
405
+ await carry(1, 6)
406
+ await grid('click', 0.6, 0.5)
407
+ } finally {
408
+ await renderer.unmount()
409
+ overlay.end()
410
+ }
411
+ expect(history()).toEqual({ past: before.past + 1, tracking: true, depth: 0 })
412
+ useScene.temporal.getState().undo()
413
+ expect(liveItem()).toEqual(item)
414
+ })
415
+
416
+ test("never releases another owner's pause on commit or unmount", async () => {
417
+ const releaseOuter = acquireSceneHistoryPause(useScene)
418
+ const before = history()
419
+ const renderer = await create(<Placement source={item} />)
420
+ try {
421
+ await carry(1, 6)
422
+ await grid('click', 0.6, 0.5)
423
+ expect(history().tracking).toBe(false)
424
+ } finally {
425
+ await renderer.unmount()
426
+ }
427
+ expect(history()).toEqual({ past: before.past, tracking: false, depth: 1 })
428
+ releaseOuter()
429
+ expect(history()).toEqual({ past: before.past, tracking: true, depth: 0 })
430
+ })
431
+
432
+ test('repeat placement adds one entry per drop', async () => {
433
+ const before = history()
434
+ const itemsBefore = levelItems().length
435
+ const renderer = await create(<Placement repeat />)
436
+ try {
437
+ await carry(10, 14)
438
+ await grid('click', 1.4, 0.5)
439
+ expect(history().past).toBe(before.past + 1)
440
+ await carry(20, 24)
441
+ foreignPausePair()
442
+ await grid('click', 2.4, 0.5)
443
+ expect(history().past).toBe(before.past + 2)
444
+ await carry(30, 32)
445
+ } finally {
446
+ await renderer.unmount()
447
+ }
448
+ expect(history()).toEqual({ past: before.past + 2, tracking: true, depth: 0 })
449
+ expect(levelItems()).toHaveLength(itemsBefore + 2)
450
+ useScene.temporal.getState().undo()
451
+ expect(levelItems()).toHaveLength(itemsBefore + 1)
452
+ expect(liveItem()).toEqual(item)
453
+ })
454
+
455
+ test('a setup failure cannot leave history paused', async () => {
456
+ const before = history()
457
+ const reportError = globalThis.reportError
458
+ const consoleError = console.error
459
+ globalThis.reportError = () => {}
460
+ console.error = () => {}
461
+ try {
462
+ const renderer = await create(
463
+ <SetupBoundary>
464
+ <Placement failSetup source={item} />
465
+ </SetupBoundary>,
466
+ )
467
+ await renderer.unmount()
468
+ } finally {
469
+ globalThis.reportError = reportError
470
+ console.error = consoleError
471
+ }
472
+ expect(history()).toEqual({ past: before.past, tracking: true, depth: 0 })
473
+ })
474
+ })
475
+
476
+ describe('item placement selection', () => {
477
+ // A surface `:click` is synthesized on pointer-up (use-node-events), and the browser fires its
478
+ // own click after it. By then the editor is back in select mode, where the grid takes that click
479
+ // for one on empty ground and drops the selection of the item just placed.
480
+ test('a drop on pointer-up selects the new item and eats the click that follows', async () => {
481
+ const renderer = await create(<Placement />)
482
+ try {
483
+ await carry(10, 14)
484
+ const dropOnRelease = () =>
485
+ emitter.emit('node:click', {
486
+ node: { id: 'site_placement_history', type: 'site' },
487
+ position: [1.4, 0, 0.5],
488
+ localPosition: [1.4, 0, 0.5],
489
+ nativeEvent: { nativeEvent: {} },
490
+ stopPropagation() {},
491
+ } as never)
492
+ window.addEventListener('pointerup', dropOnRelease)
493
+ await act(async () => {
494
+ window.dispatchEvent(new Event('pointerup'))
495
+ })
496
+ window.removeEventListener('pointerup', dropOnRelease)
497
+ const placed = levelItems().find((node) => node.id !== item.id)
498
+ expect(placed).toBeDefined()
499
+ expect(useViewer.getState().selection.selectedIds).toEqual([placed!.id])
500
+ const click = new Event('click', { cancelable: true })
501
+ window.dispatchEvent(click)
502
+ expect(click.defaultPrevented).toBe(true)
503
+ // Only that one click: once the swallow window closes, a click reaches the editor again.
504
+ await new Promise((resolve) => setTimeout(resolve, 350))
505
+ const next = new Event('click', { cancelable: true })
506
+ window.dispatchEvent(next)
507
+ expect(next.defaultPrevented).toBe(false)
508
+ } finally {
509
+ await renderer.unmount()
510
+ }
511
+ })
512
+ })
@@ -1,6 +1,8 @@
1
1
  import { beforeEach, describe, expect, test } from 'bun:test'
2
2
  import {
3
3
  BlockNode,
4
+ type CeilingEvent,
5
+ type CeilingNode,
4
6
  type GridEvent,
5
7
  ItemNode,
6
8
  type LevelNode,
@@ -10,7 +12,12 @@ import {
10
12
  type WallNode,
11
13
  } from '@pascal-app/core'
12
14
  import { BufferGeometry, Mesh, MeshBasicMaterial, type Object3D, Vector3 } from 'three'
13
- import { faceHostStrategy, floorStrategy, wallStrategy } from './placement-strategies'
15
+ import {
16
+ ceilingStrategy,
17
+ faceHostStrategy,
18
+ floorStrategy,
19
+ wallStrategy,
20
+ } from './placement-strategies'
14
21
  import type { PlacementContext, SpatialValidators } from './placement-types'
15
22
  import { registerTestBlockFaceHost } from './test-face-host'
16
23
 
@@ -492,3 +499,41 @@ describe('floorStrategy.move', () => {
492
499
  expect(result.nodeUpdate?.position?.[1]).toBe(0)
493
500
  })
494
501
  })
502
+
503
+ describe('ceilingStrategy.move', () => {
504
+ const ceiling = { id: 'ceiling_test', type: 'ceiling' } as unknown as CeilingNode
505
+ const ceilingEvent = (): CeilingEvent =>
506
+ ({
507
+ node: ceiling,
508
+ position: [1, 2.5, 1],
509
+ localPosition: [1, 0, 1],
510
+ object: new Mesh(),
511
+ stopPropagation: () => {},
512
+ }) as unknown as CeilingEvent
513
+ const onCeiling = (draftItem: ItemNode): PlacementContext => ({
514
+ ...ceilingContext(),
515
+ asset: draftItem.asset,
516
+ draftItem,
517
+ state: { ...ceilingContext().state, surface: 'ceiling', ceilingId: ceiling.id },
518
+ })
519
+ const hanging = (source?: unknown) =>
520
+ ({
521
+ ...makeDraft(),
522
+ asset: { ...makeDraft().asset, attachTo: 'ceiling', dimensions: [0.9, 1, 0.6] },
523
+ ...(source ? { source } : {}),
524
+ }) as unknown as ItemNode
525
+
526
+ test('hangs a catalog model its full height below the ceiling', () => {
527
+ const result = ceilingStrategy.move(onCeiling(hanging()), ceilingEvent())
528
+ expect(result?.gridPosition[1]).toBeCloseTo(-1, 6)
529
+ })
530
+
531
+ test('hangs a scripted object from its top, where the compiler put its origin', () => {
532
+ const source = {
533
+ kind: 'script',
534
+ manifest: { bounds: { min: [-0.45, -1, -0.3], max: [0.45, 0, 0.3] } },
535
+ }
536
+ const result = ceilingStrategy.move(onCeiling(hanging(source)), ceilingEvent())
537
+ expect(result?.gridPosition[1]).toBeCloseTo(0, 6)
538
+ })
539
+ })
@@ -15,11 +15,14 @@ import type {
15
15
  WallNode,
16
16
  } from '@pascal-app/core'
17
17
  import {
18
+ ceilingSeatY,
18
19
  clampRectToRoofWallFace,
19
20
  clearFaceHostItemFields,
20
21
  createSceneApi,
21
22
  getRoofSegmentWallFace,
22
23
  getScaledDimensions,
24
+ getWallBodyCenterOffset,
25
+ getWallLocalFaceZ,
23
26
  nodeRegistry,
24
27
  resolveSurfacePlacement,
25
28
  roofFacePointToSegment,
@@ -168,7 +171,9 @@ function resolveWallPlacementPose(
168
171
  cursorRotationY: number
169
172
  } {
170
173
  const localZ =
171
- attachTo === 'wall-side' ? ((event.node.thickness ?? 0.1) / 2) * (side === 'front' ? 1 : -1) : 0
174
+ attachTo === 'wall-side'
175
+ ? getWallLocalFaceZ(event.node, side === 'front' ? 'a' : 'b')
176
+ : getWallBodyCenterOffset(event.node)
172
177
  event.object.updateWorldMatrix(true, false)
173
178
  const world = event.object.localToWorld(new Vector3(localX, localY, localZ))
174
179
  const wallYaw = -Math.atan2(
@@ -176,7 +181,7 @@ function resolveWallPlacementPose(
176
181
  event.node.end[0] - event.node.start[0],
177
182
  )
178
183
  return {
179
- position: [localX, localY, localZ],
184
+ position: [localX, localY, attachTo === 'wall' ? 0 : localZ],
180
185
  cursorPosition: [world.x, world.y, world.z],
181
186
  // Same composition the 2D floorplan resolves a wall child with
182
187
  // (`resolveItemTransform`): the cursor frame IS the item frame.
@@ -428,7 +433,7 @@ function resolveRoofWallTarget(
428
433
  const u = snapToHalf(hit.u)
429
434
  const centerV = snapToHalf(hit.v) + height / 2
430
435
  const fitted = freePlace ? null : clampRectToRoofWallFace(hit.face, u, centerV, width, height)
431
- if (!fitted && !freePlace) return null
436
+ if (!(fitted || freePlace)) return null
432
437
  const finalU = fitted?.u ?? u
433
438
  const finalV = fitted?.v ?? centerV
434
439
 
@@ -559,7 +564,7 @@ export const roofWallStrategy = {
559
564
  if (!(ctx.draftItem && ctx.state.roofSegmentId)) return null
560
565
  // Alt mirrors the wall flow's stubbed validators: skip profile-fit
561
566
  // and overlap checks entirely.
562
- if (!freePlace && !canPlaceOnRoofWall(ctx)) return null
567
+ if (!(freePlace || canPlaceOnRoofWall(ctx))) return null
563
568
 
564
569
  return {
565
570
  nodeUpdate: {
@@ -743,9 +748,7 @@ export const ceilingStrategy = {
743
748
  // use the ceiling hit's local position rather than world position.
744
749
  const x = snapToGrid(event.localPosition[0], swapDims ? dimZ : dimX)
745
750
  const z = snapToGrid(event.localPosition[2], swapDims ? dimX : dimZ)
746
- // Recessed fixtures seat flush with the ceiling plane (body rising into the
747
- // void above); everything else hangs its full height below the ceiling.
748
- const seatY = ctx.asset.recessed ? 0 : -itemHeight
751
+ const seatY = ceilingSeatY(ctx.draftItem ?? { asset: ctx.asset }, itemHeight)
749
752
  const worldSnapped = event.object.localToWorld(new Vector3(x, seatY, z))
750
753
 
751
754
  return {
@@ -777,9 +780,7 @@ export const ceilingStrategy = {
777
780
 
778
781
  const x = snapToGrid(event.localPosition[0], swapDims ? dimZ : dimX)
779
782
  const z = snapToGrid(event.localPosition[2], swapDims ? dimX : dimZ)
780
- // Recessed fixtures seat flush with the ceiling plane (body rising into the
781
- // void above); everything else hangs its full height below the ceiling.
782
- const seatY = ctx.draftItem.asset.recessed ? 0 : -itemHeight
783
+ const seatY = ceilingSeatY(ctx.draftItem, itemHeight)
783
784
  const worldSnapped = event.object.localToWorld(new Vector3(x, seatY, z))
784
785
 
785
786
  return {