@pascal-app/editor 1.0.1 → 1.0.4

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 (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -66,12 +66,7 @@ export function buildSvgArcPath(
66
66
  endAngle: number,
67
67
  ) {
68
68
  const start = getArcPlanPoint(center, radius, startAngle)
69
- const end = getArcPlanPoint(center, radius, endAngle)
70
- const delta = endAngle - startAngle
71
- const largeArcFlag = Math.abs(delta) > Math.PI ? 1 : 0
72
- const sweepFlag = delta >= 0 ? 1 : 0
73
-
74
- return `M ${toSvgX(start.x)} ${toSvgY(start.y)} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${toSvgX(end.x)} ${toSvgY(end.y)}`
69
+ return `M ${toSvgX(start.x)} ${toSvgY(start.y)} ${svgArcCommands(center, radius, startAngle, endAngle)}`
75
70
  }
76
71
 
77
72
  export function buildSvgAnnularSectorPath(
@@ -82,23 +77,25 @@ export function buildSvgAnnularSectorPath(
82
77
  endAngle: number,
83
78
  ) {
84
79
  const outerStart = getArcPlanPoint(center, outerRadius, startAngle)
85
- const outerEnd = getArcPlanPoint(center, outerRadius, endAngle)
86
80
  const innerEnd = getArcPlanPoint(center, innerRadius, endAngle)
87
- const innerStart = getArcPlanPoint(center, innerRadius, startAngle)
88
- const delta = endAngle - startAngle
89
- const largeArcFlag = Math.abs(delta) > Math.PI ? 1 : 0
90
- const sweepFlag = delta >= 0 ? 1 : 0
91
- const reverseSweepFlag = sweepFlag ? 0 : 1
92
-
93
81
  return [
94
82
  `M ${toSvgX(outerStart.x)} ${toSvgY(outerStart.y)}`,
95
- `A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} ${sweepFlag} ${toSvgX(outerEnd.x)} ${toSvgY(outerEnd.y)}`,
83
+ svgArcCommands(center, outerRadius, startAngle, endAngle),
96
84
  `L ${toSvgX(innerEnd.x)} ${toSvgY(innerEnd.y)}`,
97
- `A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} ${reverseSweepFlag} ${toSvgX(innerStart.x)} ${toSvgY(innerStart.y)}`,
85
+ svgArcCommands(center, innerRadius, endAngle, startAngle),
98
86
  'Z',
99
87
  ].join(' ')
100
88
  }
101
89
 
90
+ function svgArcCommands(center: Point2D, radius: number, startAngle: number, endAngle: number) {
91
+ const delta = endAngle - startAngle
92
+ const count = Math.max(1, Math.ceil(Math.abs(delta) / Math.PI))
93
+ return Array.from({ length: count }, (_, index) => {
94
+ const end = getArcPlanPoint(center, radius, startAngle + (delta * (index + 1)) / count)
95
+ return `A ${radius} ${radius} 0 0 ${delta >= 0 ? 1 : 0} ${toSvgX(end.x)} ${toSvgY(end.y)}`
96
+ }).join(' ')
97
+ }
98
+
102
99
  export function formatSvgPolygonPoints(points: Point2D[]) {
103
100
  return points.map((point) => `${toSvgX(point.x)},${toSvgY(point.y)}`).join(' ')
104
101
  }
@@ -9,7 +9,9 @@ import { useViewer } from '@pascal-app/viewer'
9
9
  import { useEffect, useState } from 'react'
10
10
  import { useHandleGroup } from '../../store/use-handle-group'
11
11
  import { PanelSection } from '../ui/controls/panel-section'
12
+ import { SegmentedControl } from '../ui/controls/segmented-control'
12
13
  import { SliderControl } from '../ui/controls/slider-control'
14
+ import { ToggleControl } from '../ui/controls/toggle-control'
13
15
  import { PanelWrapper } from '../ui/panels/panel-wrapper'
14
16
  export function ProceduralInspector({
15
17
  nodeId,
@@ -75,21 +77,44 @@ export function ProceduralInspector({
75
77
  )}
76
78
  </div>
77
79
  <div className="space-y-3">
78
- {fields.map((p) => (
79
- <SliderControl
80
- key={p.id}
81
- label={p.label}
82
- value={node.parameters[p.id] ?? p.default}
83
- min={p.min}
84
- max={p.max}
85
- step={p.step}
86
- precision={3}
87
- unit={p.unit === 'm' ? 'm' : ''}
88
- onChange={(v) => change(p.id, v, false)}
89
- onCommit={(v) => change(p.id, v, true)}
90
- restoreOnCommit={false}
91
- />
92
- ))}
80
+ {fields.map((p) => {
81
+ const value = node.parameters[p.id] ?? p.default
82
+ if (p.unit === 'bool')
83
+ return (
84
+ <ToggleControl
85
+ key={p.id}
86
+ label={p.label}
87
+ checked={value === 1}
88
+ onChange={(checked) => change(p.id, checked ? 1 : 0, true)}
89
+ />
90
+ )
91
+ if (p.unit === 'choice' && p.options && p.options.length <= 4)
92
+ return (
93
+ <div key={p.id} className="space-y-1.5">
94
+ <div className="text-muted-foreground text-xs">{p.label}</div>
95
+ <SegmentedControl
96
+ value={String(value)}
97
+ options={p.options.map((label, index) => ({ label, value: String(index) }))}
98
+ onChange={(index) => change(p.id, Number(index), true)}
99
+ />
100
+ </div>
101
+ )
102
+ return (
103
+ <SliderControl
104
+ key={p.id}
105
+ label={p.unit === 'choice' ? `${p.label}: ${p.options?.[value] ?? value}` : p.label}
106
+ value={value}
107
+ min={p.min}
108
+ max={p.max}
109
+ step={p.step}
110
+ precision={p.unit === 'choice' ? 0 : 3}
111
+ unit={p.unit === 'm' ? 'm' : ''}
112
+ onChange={(v) => change(p.id, v, false)}
113
+ onCommit={(v) => change(p.id, v, true)}
114
+ restoreOnCommit={false}
115
+ />
116
+ )
117
+ })}
93
118
  </div>
94
119
  {partId && (
95
120
  <button
@@ -12,6 +12,7 @@ import { useViewer } from '@pascal-app/viewer'
12
12
  import { _roots, act, createRoot, events, extend, type RootState } from '@react-three/fiber'
13
13
  import { createElement } from 'react'
14
14
  import { Group, InstancedMesh, OrthographicCamera, Vector3, type WebGLRenderer } from 'three'
15
+ import { endCeilingEdit, startCeilingEdit } from '../../../lib/ceiling-edit-session'
15
16
  import { sfxEmitter } from '../../../lib/sfx-bus'
16
17
  import useEditor from '../../../store/use-editor'
17
18
  import useInteractionScope from '../../../store/use-interaction-scope'
@@ -33,7 +34,13 @@ type BracketHarness = {
33
34
  sounds: string[]
34
35
  }
35
36
 
36
- async function withMountedBrackets(run: (harness: BracketHarness) => Promise<void>) {
37
+ async function withMountedBrackets(
38
+ run: (harness: BracketHarness) => Promise<void>,
39
+ {
40
+ ceilingData = {},
41
+ session = true,
42
+ }: { ceilingData?: Partial<CeilingNode>; session?: boolean } = {},
43
+ ) {
37
44
  const previousScene = useScene.getState()
38
45
  const previousViewer = useViewer.getState()
39
46
  const previousEditor = useEditor.getState()
@@ -54,6 +61,7 @@ async function withMountedBrackets(run: (harness: BracketHarness) => Promise<voi
54
61
  const building = BuildingNode.parse({})
55
62
  const level = LevelNode.parse({ parentId: building.id, height: 3 })
56
63
  const ceiling = CeilingNode.parse({
64
+ ...ceilingData,
57
65
  parentId: level.id,
58
66
  height: 3,
59
67
  polygon: [
@@ -81,8 +89,9 @@ async function withMountedBrackets(run: (harness: BracketHarness) => Promise<voi
81
89
  hoveredId: null,
82
90
  selection: { buildingId: building.id, levelId: level.id, zoneId: null, selectedIds: [] },
83
91
  })
84
- useEditor.setState({ phase: 'structure', mode: 'select', structureLayer: 'elements' })
92
+ useEditor.setState({ phase: 'building', mode: 'select' })
85
93
  useInteractionScope.setState({ scope: { kind: 'idle' } })
94
+ if (session) startCeilingEdit(ceiling.id)
86
95
  const camera = Object.assign(new OrthographicCamera(-1, 5, 5, -1, 0.1, 100), { manual: true })
87
96
  camera.position.set(0, 10, 0)
88
97
  camera.up.set(0, 0, -1)
@@ -145,7 +154,10 @@ async function withMountedBrackets(run: (harness: BracketHarness) => Promise<voi
145
154
  sounds,
146
155
  })
147
156
  } finally {
148
- await act(async () => root.render(null))
157
+ await act(async () => {
158
+ endCeilingEdit()
159
+ root.render(null)
160
+ })
149
161
  clearBoxSelectHandled()
150
162
  emitter.off('ceiling:click', onClick)
151
163
  sfxEmitter.off('*', onSound)
@@ -186,6 +198,7 @@ test('mounted brackets preserve hover, clicks, drags, capture visibility, overri
186
198
  await dispatch('onPointerDown', 4, 0)
187
199
 
188
200
  const oldNormal = meshes().find((mesh) => mesh.renderOrder === 1000)
201
+ // Ceilings outside the session get no brackets.
189
202
  await act(async () => {
190
203
  const additions = Array.from({ length: 8 }, (_, index) =>
191
204
  CeilingNode.parse({
@@ -206,12 +219,43 @@ test('mounted brackets preserve hover, clicks, drags, capture visibility, overri
206
219
  },
207
220
  })
208
221
  })
222
+ expect(meshes().find((mesh) => mesh.renderOrder === 1000)).toBe(oldNormal)
223
+ expect(meshes().reduce((sum, mesh) => sum + mesh.count, 0)).toBe(12)
224
+ // Twelve corners overflow the initial capacity: the batch meshes are replaced
225
+ // while the pressed corner keeps its index.
226
+ await act(async () =>
227
+ useScene.setState({
228
+ nodes: {
229
+ ...useScene.getState().nodes,
230
+ [ceiling.id]: CeilingNode.parse({
231
+ ...ceiling,
232
+ polygon: [
233
+ [0, 0],
234
+ [4, 0],
235
+ [4, 1],
236
+ [4, 2],
237
+ [4, 3],
238
+ [4, 4],
239
+ [3, 4],
240
+ [2, 4],
241
+ [1, 4],
242
+ [0, 4],
243
+ [0, 3],
244
+ [0, 2],
245
+ ],
246
+ }),
247
+ },
248
+ }),
249
+ )
209
250
  expect(meshes()).toHaveLength(2)
210
251
  expect(meshes().find((mesh) => mesh.renderOrder === 1000)).not.toBe(oldNormal)
211
252
  expect(useViewer.getState().hoveredId).toBe(ceiling.id)
212
253
  await dispatch('onClick', 4, 0)
213
254
  expect(clicks).toHaveLength(2)
214
255
  expect(clicks[1].position).toEqual([4, 3, 0])
256
+ await act(async () =>
257
+ useScene.setState({ nodes: { ...useScene.getState().nodes, [ceiling.id]: ceiling } }),
258
+ )
215
259
  await dispatch('onPointerLeave', 4, 0)
216
260
  expect(useViewer.getState().hoveredId).toBeNull()
217
261
 
@@ -277,7 +321,7 @@ test('mounted brackets preserve hover, clicks, drags, capture visibility, overri
277
321
  )
278
322
  })
279
323
 
280
- test('coincident ceiling corners keep the same hover, click, and drag owner across batch transfers', async () => {
324
+ test('an adjacent ceiling outside the session has no brackets at a shared corner', async () => {
281
325
  await withMountedBrackets(async ({ ceiling, dispatch, dispatchWindow, meshes, clicks }) => {
282
326
  const adjacent = CeilingNode.parse({
283
327
  ...ceiling,
@@ -294,25 +338,64 @@ test('coincident ceiling corners keep the same hover, click, and drag owner acro
294
338
  nodes: { ...useScene.getState().nodes, [adjacent.id]: adjacent },
295
339
  }),
296
340
  )
297
- const owner = ceiling.id < adjacent.id ? ceiling : adjacent
298
- const other = owner === ceiling ? adjacent : ceiling
299
- for (let move = 0; move < 20; move++) {
300
- await dispatch('onPointerMove', 0, 0)
301
- expect(useViewer.getState().hoveredId).toBe(owner.id)
302
- expect(meshes().find((mesh) => mesh.renderOrder === 1001)!.count).toBe(7)
303
- }
341
+ expect(meshes().reduce((sum, mesh) => sum + mesh.count, 0)).toBe(12)
342
+ await dispatch('onPointerMove', 0, 0)
343
+ expect(useViewer.getState().hoveredId).toBe(ceiling.id)
304
344
  await dispatch('onPointerDown', 0, 0)
305
345
  await dispatch('onClick', 0, 0)
306
- expect(clicks[0].node.id).toBe(owner.id)
346
+ expect(clicks[0].node.id).toBe(ceiling.id)
307
347
  await dispatchWindow('pointermove', 0.5, 0.5)
308
- expect(useLiveNodeOverrides.getState().overrides.has(owner.id)).toBe(true)
309
- expect(useLiveNodeOverrides.getState().overrides.has(other.id)).toBe(false)
348
+ expect(useLiveNodeOverrides.getState().overrides.has(ceiling.id)).toBe(true)
349
+ expect(useLiveNodeOverrides.getState().overrides.has(adjacent.id)).toBe(false)
310
350
  await dispatchWindow('pointercancel', 0.5, 0.5)
311
351
  await dispatch('onPointerLeave', 0, 0)
312
352
  expect(useViewer.getState().hoveredId).toBeNull()
313
353
  })
314
354
  })
315
355
 
356
+ test('brackets and their hit boxes exist only inside the Edit ceiling session', async () => {
357
+ await withMountedBrackets(
358
+ async ({ ceiling, dispatch, meshes, levelObject }) => {
359
+ // A canvas selection of the ceiling alone shows nothing.
360
+ await act(async () => useViewer.getState().setSelection({ selectedIds: [ceiling.id] }))
361
+ expect(meshes()).toHaveLength(0)
362
+ expect(levelObject.children).toHaveLength(0)
363
+ await dispatch('onPointerMove', 0, 0)
364
+ expect(useViewer.getState().hoveredId).toBeNull()
365
+
366
+ await act(async () => {
367
+ startCeilingEdit(ceiling.id)
368
+ })
369
+ expect(meshes().reduce((sum, mesh) => sum + mesh.count, 0)).toBe(12)
370
+
371
+ await act(async () => useViewer.getState().setSelection({ selectedIds: [] }))
372
+ expect(meshes()).toHaveLength(0)
373
+ expect(levelObject.children).toHaveLength(0)
374
+ },
375
+ { session: false },
376
+ )
377
+ })
378
+
379
+ test('a corner drag detaches an auto ceiling; the session stays open', async () => {
380
+ await withMountedBrackets(
381
+ async ({ ceiling, dispatch, dispatchWindow, meshes }) => {
382
+ expect(ceiling.boundary).toBe('auto')
383
+ await dispatch('onPointerDown', 0, 0)
384
+ await dispatchWindow('pointermove', 0.5, 0.5)
385
+ const preview = useLiveNodeOverrides.getState().overrides.get(ceiling.id)
386
+ ?.polygon as CeilingNode['polygon']
387
+ await dispatchWindow('pointerup', 0.5, 0.5)
388
+ const committed = useScene.getState().nodes[ceiling.id] as CeilingNode
389
+ expect(committed.polygon).toEqual(preview)
390
+ expect(committed.boundary).toBeUndefined()
391
+ expect(committed.zoneId).toBeUndefined()
392
+ expect(useViewer.getState().selection.selectedIds).toEqual([ceiling.id])
393
+ expect(meshes().reduce((sum, mesh) => sum + mesh.count, 0)).toBe(12)
394
+ },
395
+ { ceilingData: { boundary: 'auto', zoneId: 'zone_auto_ceiling' } },
396
+ )
397
+ })
398
+
316
399
  test('same-id registry replacement rebinds the portal and drag plane without rewriting instance data', async () => {
317
400
  await withMountedBrackets(
318
401
  async ({ ceiling, level, levelObject, state, dispatch, dispatchWindow, meshes, clicks }) => {
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
+ type AnyNodeId,
4
5
  type CeilingNode,
5
6
  emitter,
6
7
  resolveCeilingHeight,
@@ -24,6 +25,7 @@ import {
24
25
  } from 'react'
25
26
  import { Group, type Object3D, Plane, Raycaster, Vector2, Vector3 } from 'three'
26
27
  import { useShallow } from 'zustand/react/shallow'
28
+ import { useCeilingEditCeilingId } from '../../../lib/ceiling-edit-session'
27
29
  import {
28
30
  clearCeilingSnapFeedback,
29
31
  resolveCeilingPlanPointSnap,
@@ -94,9 +96,7 @@ function clearCornerDragPreview(drag: CornerDragState) {
94
96
  }
95
97
 
96
98
  export const CeilingSelectionAffordanceSystem = () => {
97
- const phase = useEditor((state) => state.phase)
98
99
  const mode = useEditor((state) => state.mode)
99
- const structureLayer = useEditor((state) => state.structureLayer)
100
100
  // ANY active interaction (moving/placing a node, reshaping a boundary or
101
101
  // curve, dragging a handle) unmounts the brackets: their ceiling-height hit
102
102
  // boxes would otherwise catch drag-time hover, set `hoveredId` to the
@@ -105,26 +105,24 @@ export const CeilingSelectionAffordanceSystem = () => {
105
105
  // scope, so it can't unmount itself.
106
106
  const scopeIdle = useInteractionScope((state) => state.scope.kind === 'idle')
107
107
  const currentLevelId = useViewer((state) => state.selection.levelId)
108
+ // Brackets and their hit boxes exist only for the ceiling in an explicit
109
+ // Edit ceiling session (`lib/ceiling-edit-session.ts`).
110
+ const editCeilingId = useCeilingEditCeilingId()
108
111
 
109
112
  const ceilings = useScene(
110
- useShallow((state) =>
111
- Object.values(state.nodes).filter((node): node is CeilingNode => {
112
- return (
113
- node.type === 'ceiling' &&
114
- node.visible !== false &&
115
- currentLevelId !== null &&
116
- resolveLevelId(node, state.nodes) === currentLevelId
117
- )
118
- }),
119
- ),
113
+ useShallow((state) => {
114
+ const node = editCeilingId ? state.nodes[editCeilingId as AnyNodeId] : undefined
115
+ return node?.type === 'ceiling' &&
116
+ node.visible !== false &&
117
+ currentLevelId !== null &&
118
+ resolveLevelId(node, state.nodes) === currentLevelId
119
+ ? [node]
120
+ : []
121
+ }),
120
122
  )
121
123
 
122
124
  const shouldRender =
123
- phase === 'structure' &&
124
- mode === 'select' &&
125
- structureLayer === 'elements' &&
126
- scopeIdle &&
127
- currentLevelId !== null
125
+ mode === 'select' && scopeIdle && currentLevelId !== null && ceilings.length > 0
128
126
 
129
127
  if (!shouldRender) return null
130
128
 
@@ -528,7 +526,9 @@ const CeilingSelectionAffordance = memo(function CeilingSelectionAffordance({
528
526
  suppressNextClick()
529
527
 
530
528
  if (drag.previewPolygon) {
531
- useScene.getState().updateNode(drag.ceilingId, { polygon: drag.previewPolygon })
529
+ // A polygon edit converts a derived (`boundary: 'auto'`) ceiling into
530
+ // an authored one, like the boundary editor's commit.
531
+ useScene.getState().detachDerivedNode(drag.ceilingId, { polygon: drag.previewPolygon })
532
532
  useViewer.getState().setSelection({ selectedIds: [drag.ceilingId] })
533
533
  }
534
534
 
@@ -0,0 +1,47 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { useScene, ZoneNode } from '@pascal-app/core'
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import { act, create } from '@react-three/test-renderer'
5
+ import { createElement } from 'react'
6
+ import useEditor from '../../../store/use-editor'
7
+ import { ZoneSystem } from './zone-system'
8
+
9
+ test('zone volumes mount only while a unit is painted, never for a selected zone', async () => {
10
+ const previousScene = useScene.getState()
11
+ const previousViewer = useViewer.getState()
12
+ const previousEditor = useEditor.getState()
13
+ const zone = ZoneNode.parse({
14
+ name: 'Courtyard',
15
+ polygon: [
16
+ [0, 0],
17
+ [4, 0],
18
+ [4, 4],
19
+ [0, 4],
20
+ ],
21
+ })
22
+ useScene.setState({ nodes: { [zone.id]: zone } })
23
+ useViewer.setState({
24
+ focusedUnitId: null,
25
+ showZones: false,
26
+ selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [zone.id] },
27
+ })
28
+ useEditor.setState({ isCaptureMode: false })
29
+ const renderer = await create(createElement(ZoneSystem))
30
+ try {
31
+ // A selected drawn zone looks like a room: outline, no tinted volume.
32
+ expect(useViewer.getState().showZones).toBe(false)
33
+ await act(async () => useViewer.setState({ focusedUnitId: 'unit_paint' as never }))
34
+ expect(useViewer.getState().showZones).toBe(true)
35
+ await act(async () => useEditor.getState().setCaptureMode(true))
36
+ expect(useViewer.getState().showZones).toBe(false)
37
+ await act(async () => useEditor.getState().setCaptureMode(false))
38
+ expect(useViewer.getState().showZones).toBe(true)
39
+ await act(async () => useViewer.setState({ focusedUnitId: null }))
40
+ expect(useViewer.getState().showZones).toBe(false)
41
+ } finally {
42
+ await renderer.unmount()
43
+ useScene.setState(previousScene)
44
+ useViewer.setState(previousViewer)
45
+ useEditor.setState(previousEditor)
46
+ }
47
+ })
@@ -9,40 +9,32 @@ import useEditor from '../../../store/use-editor'
9
9
  import useInteractionScope from '../../../store/use-interaction-scope'
10
10
 
11
11
  // Disable raycasting on zone geometry so clicks pass through to items underneath.
12
- // Zone selection in the editor is handled exclusively via the HTML label overlay.
13
12
  const noopRaycast = () => {}
14
13
 
14
+ /**
15
+ * Zone volumes are a working view for one job only: painting a unit's
16
+ * membership, where every zone on the level has to be in view. Otherwise every
17
+ * zone, drawn or detected, shows as a room: a light label pill (`RoomLabels3D`)
18
+ * and the room outline when selected. Mounting is the viewer's `showZones`
19
+ * flag: unmounting skips the meshes and drei labels entirely, and the cleanup
20
+ * restores the default so preview / first-person surfaces keep their labels.
21
+ */
15
22
  export const ZoneSystem = () => {
16
- // Outside the zones layer (or during snapshot capture) zones unmount
17
- // entirely — meshes AND drei <Html> labels, which cost per-frame matrix work
18
- // + live DOM even at opacity 0. The renderer reads this viewer flag; the
19
- // unmount cleanup restores the default so preview / first-person surfaces
20
- // (which swap this system for ViewerZoneSystem) keep their labels.
21
- const structureLayerState = useEditor((s) => s.structureLayer)
22
- const isCaptureModeState = useEditor((s) => s.isCaptureMode)
23
+ const focusedUnitId = useViewer((s) => s.focusedUnitId)
24
+ const isCaptureMode = useEditor((s) => s.isCaptureMode)
25
+ const active = !!focusedUnitId && !isCaptureMode
23
26
  useEffect(() => {
24
- useViewer.getState().setShowZones(structureLayerState === 'zones' && !isCaptureModeState)
27
+ useViewer.getState().setShowZones(active)
25
28
  return () => useViewer.getState().setShowZones(true)
26
- }, [structureLayerState, isCaptureModeState])
29
+ }, [active])
27
30
 
28
31
  useFrame((_, delta) => {
29
32
  if (!useViewer.getState().showZones) return
30
33
 
31
- const structureLayer = useEditor.getState().structureLayer
32
- const editorMode = useEditor.getState().mode
33
- const selectedLevelId = useViewer.getState().selection.levelId
34
- const selectedZoneId = useViewer.getState().selection.zoneId
35
- const hoveredId = useViewer.getState().hoveredId
36
- // Snapshot capture is a clean, camera-only surface — never show zone
37
- // geometry or the HTML zone tags in the framed shot.
38
- const isCaptureMode = useEditor.getState().isCaptureMode
39
-
40
- // During any active interaction zone labels step back entirely — they are
41
- // not a primary editing concern and would distract / invite misclicks.
34
+ const { levelId: selectedLevelId } = useViewer.getState().selection
35
+ // During any active interaction zone labels step back entirely.
42
36
  const zoneLabelsHidden =
43
37
  resolveOverlayPolicy(useInteractionScope.getState().scope).zoneLabels === 'hidden'
44
-
45
- const zoneGeometryVisible = structureLayer === 'zones'
46
38
  const zones = sceneRegistry.byType.zone || new Set()
47
39
  const nodes = useScene.getState().nodes
48
40
  const lerpSpeed = 10 * delta
@@ -52,59 +44,32 @@ export const ZoneSystem = () => {
52
44
  if (!obj) return
53
45
 
54
46
  const zone = nodes[zoneId as ZoneNode['id']] as ZoneNode | undefined
55
-
56
47
  const isOnSelectedLevel = zone?.parentId === selectedLevelId
57
- const isSelected = zoneId === selectedZoneId
58
- const isDeleteHovered = editorMode === 'delete' && hoveredId === zoneId
59
-
60
- // Keep group visible (so <Html> labels stay active), hide/show meshes only.
61
- // Show meshes when: in zone mode, selected, or delete-hovered.
62
- if (!obj.visible) obj.visible = true
63
- const meshVisible = !isCaptureMode && (zoneGeometryVisible || isSelected || isDeleteHovered)
64
- const targetOpacity = isCaptureMode
65
- ? 0
66
- : isSelected || isDeleteHovered
67
- ? 1
68
- : zoneGeometryVisible
69
- ? 1
70
- : 0
71
-
72
- // Raycast is re-disabled per frame (not once per group): the meshes
73
- // remount whenever the zones layer toggles, so a one-shot flag on the
74
- // persistent group would leave fresh meshes clickable.
75
- const walls = (obj as Group).getObjectByName('walls') as Mesh | undefined
76
- if (walls) {
77
- walls.visible = meshVisible
78
- walls.raycast = noopRaycast
79
- const material = walls.material as MeshBasicNodeMaterial
80
- if (material?.userData?.uOpacity) {
81
- material.userData.uOpacity.value = MathUtils.lerp(
82
- material.userData.uOpacity.value,
83
- targetOpacity,
84
- lerpSpeed,
85
- )
86
- }
87
- }
48
+ // A zone the author hid (sidebar eye) takes its group with it; otherwise
49
+ // this per-frame write would undo the renderer's `visible` prop.
50
+ const nodeVisible = zone?.visible !== false
51
+ if (obj.visible !== nodeVisible) obj.visible = nodeVisible
52
+ const shown = nodeVisible && isOnSelectedLevel
88
53
 
89
- const floor = (obj as Group).getObjectByName('floor') as Mesh | undefined
90
- if (floor) {
91
- floor.visible = meshVisible
92
- floor.raycast = noopRaycast
93
- const material = floor.material as MeshBasicNodeMaterial
54
+ // Raycast is re-disabled per frame: the meshes remount whenever zones
55
+ // mount again, so a one-shot flag on the persistent group would leave
56
+ // fresh meshes clickable.
57
+ for (const name of ['walls', 'floor']) {
58
+ const mesh = (obj as Group).getObjectByName(name) as Mesh | undefined
59
+ if (!mesh) continue
60
+ mesh.visible = shown
61
+ mesh.raycast = noopRaycast
62
+ const material = mesh.material as MeshBasicNodeMaterial
94
63
  if (material?.userData?.uOpacity) {
95
64
  material.userData.uOpacity.value = MathUtils.lerp(
96
65
  material.userData.uOpacity.value,
97
- targetOpacity,
66
+ shown ? 1 : 0,
98
67
  lerpSpeed,
99
68
  )
100
69
  }
101
70
  }
102
71
 
103
- // Labels: visible on the current level (regardless of mode), but never
104
- // during snapshot capture.
105
- const showLabel =
106
- !isCaptureMode && !zoneLabelsHidden && !!selectedLevelId && isOnSelectedLevel
107
- const labelOpacity = showLabel ? '1' : '0'
72
+ const labelOpacity = shown && !zoneLabelsHidden ? '1' : '0'
108
73
  const labelEl = document.getElementById(`${zoneId}-label`)
109
74
  if (labelEl && labelEl.style.opacity !== labelOpacity) {
110
75
  labelEl.style.opacity = labelOpacity
@@ -0,0 +1,10 @@
1
+ import { type AnyNodeDefinition, registerNode } from '@pascal-app/core'
2
+
3
+ export function registerHostingTestNode(definition: AnyNodeDefinition) {
4
+ registerNode({
5
+ ...definition,
6
+ geometry: () => {
7
+ throw new Error('Unit-test host geometry must not be rendered')
8
+ },
9
+ })
10
+ }
@@ -1,15 +1,9 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
- import {
3
- type ItemEvent,
4
- ItemNode,
5
- nodeRegistry,
6
- registerNode,
7
- sceneRegistry,
8
- useScene,
9
- } from '@pascal-app/core'
2
+ import { type ItemEvent, ItemNode, nodeRegistry, sceneRegistry, useScene } from '@pascal-app/core'
10
3
  import { Group, Vector3 } from 'three'
11
4
  import useEditor from '../../../store/use-editor'
12
5
  import useInteractionScope from '../../../store/use-interaction-scope'
6
+ import { registerHostingTestNode } from '../__fixtures__/hosting'
13
7
  import { itemSurfaceStrategy } from './placement-strategies'
14
8
  import type { PlacementContext } from './placement-types'
15
9
 
@@ -28,7 +22,7 @@ const scopeState = useInteractionScope.getState()
28
22
  beforeEach(() => {
29
23
  restoreRegistry = nodeRegistry._snapshot()
30
24
  nodeRegistry._reset()
31
- registerNode({
25
+ registerHostingTestNode({
32
26
  kind: 'item',
33
27
  schemaVersion: 1,
34
28
  schema: ItemNode,
@@ -1,16 +1,10 @@
1
1
  import { afterEach, beforeEach, expect, spyOn, test } from 'bun:test'
2
2
  import * as core from '@pascal-app/core'
3
- import {
4
- type ItemEvent,
5
- ItemNode,
6
- nodeRegistry,
7
- registerNode,
8
- sceneRegistry,
9
- useScene,
10
- } from '@pascal-app/core'
3
+ import { type ItemEvent, ItemNode, nodeRegistry, sceneRegistry, useScene } from '@pascal-app/core'
11
4
  import { Group, Vector3 } from 'three'
12
5
  import useEditor from '../../../store/use-editor'
13
6
  import useInteractionScope from '../../../store/use-interaction-scope'
7
+ import { registerHostingTestNode } from '../__fixtures__/hosting'
14
8
  import { itemSurfaceStrategy } from './placement-strategies'
15
9
  import type { PlacementContext } from './placement-types'
16
10
 
@@ -37,7 +31,7 @@ let resolver: ReturnType<typeof spyOn<typeof core, 'resolveSurfacePlacement'>>
37
31
  beforeEach(() => {
38
32
  restoreRegistry = nodeRegistry._snapshot()
39
33
  nodeRegistry._reset()
40
- registerNode({
34
+ registerHostingTestNode({
41
35
  kind: 'item',
42
36
  schemaVersion: 1,
43
37
  schema: ItemNode,