@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,238 @@
1
+ 'use client'
2
+
3
+ import { type AnyNode, findOpenWallEnds, type OpenWallEnd, useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import { useEffect, useMemo, useRef, useState } from 'react'
6
+ import { useShallow } from 'zustand/react/shallow'
7
+ import {
8
+ collapseMutualOpenWallEnds,
9
+ joinOpenWallEnd,
10
+ openWallEndKey,
11
+ visibleOpenWallEnds,
12
+ } from '../lib/floorplan/open-wall-ends'
13
+ import { sfxEmitter } from '../lib/sfx-bus'
14
+ import useEditor from '../store/use-editor'
15
+ import { useFloorplanDraftPreview } from '../store/use-floorplan-draft-preview'
16
+ import useInteractionScope from '../store/use-interaction-scope'
17
+
18
+ type Boundary = Extract<AnyNode, { type: 'wall' | 'separator' }>
19
+
20
+ const NO_ENDS: OpenWallEnd[] = []
21
+
22
+ const isBoundary = (node: AnyNode): node is Boundary =>
23
+ node.type === 'wall' || node.type === 'separator'
24
+
25
+ let lastAnalysis: {
26
+ levelId: string
27
+ nodes: readonly AnyNode[]
28
+ ends: OpenWallEnd[]
29
+ } | null = null
30
+
31
+ /**
32
+ * The room-graph pass behind the open-end markers, shared by the floor plan
33
+ * and the 3D view: split view mounts both, and they must not pay for it twice.
34
+ * `nodes` is the level's walls and separators plus what the walls host: an
35
+ * opening can stop core squaring a corner, and the preview must land where
36
+ * Join walls will. Recomputed only when one of those nodes changes (compared
37
+ * by reference), never per frame or on unrelated edits.
38
+ */
39
+ export function analyseOpenWallEnds(levelId: string, nodes: readonly AnyNode[]): OpenWallEnd[] {
40
+ const last = lastAnalysis
41
+ if (
42
+ last &&
43
+ last.levelId === levelId &&
44
+ last.nodes.length === nodes.length &&
45
+ last.nodes.every((node, index) => node === nodes[index])
46
+ ) {
47
+ return last.ends
48
+ }
49
+ const ends = findOpenWallEnds(Object.fromEntries(nodes.map((node) => [node.id, node])), levelId)
50
+ lastAnalysis = { levelId, nodes, ends }
51
+ return ends
52
+ }
53
+
54
+ /** Moving, placing, reshaping or dragging a handle: the markers step aside. */
55
+ export function useOpenWallEndsSuppressed(): boolean {
56
+ return useInteractionScope(
57
+ (state) =>
58
+ state.scope.kind === 'moving' ||
59
+ state.scope.kind === 'placing' ||
60
+ state.scope.kind === 'reshaping' ||
61
+ state.scope.kind === 'handle-drag',
62
+ )
63
+ }
64
+
65
+ export type OpenWallEndsState = {
66
+ /** The wall tool (any variant) or Divide is active. */
67
+ drawing: boolean
68
+ /** A wall or divide segment is mid-draft: markers stay visible but out of the way. */
69
+ drafting: boolean
70
+ /** Ends to mark, mutual pairs collapsed to the end that slides straight. */
71
+ ends: OpenWallEnd[]
72
+ /** Walls on the active level that bound no room. */
73
+ roomlessWallIds: string[]
74
+ /** Whether the overlay is up at all (drawing, or a near miss to show). */
75
+ shown: boolean
76
+ }
77
+
78
+ /**
79
+ * Open wall ends on the active level and when to show them. On while walls or
80
+ * rooms are being drawn; otherwise only when a wall that bounds no room has a
81
+ * near miss.
82
+ */
83
+ export function useOpenWallEnds(): OpenWallEndsState {
84
+ const levelId = useViewer((state) => state.selection.levelId)
85
+ const mode = useEditor((state) => state.mode)
86
+ const tool = useEditor((state) => state.tool)
87
+ const spaces = useEditor(
88
+ useShallow((state) => Object.values(state.spaces).filter((space) => space.levelId === levelId)),
89
+ )
90
+ const isDividing = useInteractionScope((state) => state.scope.kind === 'room-divide')
91
+ const dividingDraft = useInteractionScope(
92
+ (state) => state.scope.kind === 'room-divide' && state.scope.points.length > 0,
93
+ )
94
+ const wallDrafting = useFloorplanDraftPreview(
95
+ (state) =>
96
+ state.wallDraftStart !== null ||
97
+ state.wallRectangleDraftStart !== null ||
98
+ state.wallPolygonDraftPoints.length > 0,
99
+ )
100
+ // Walls and separators first, then every node a wall hosts — one flat list,
101
+ // so a door moving re-runs the analysis but an unrelated item edit does not.
102
+ const analysisNodes = useScene(
103
+ useShallow((state) => {
104
+ const level = levelId ? state.nodes[levelId as AnyNode['id']] : undefined
105
+ if (level?.type !== 'level') return [] as AnyNode[]
106
+ const boundaries = level.children
107
+ .map((id) => state.nodes[id])
108
+ .filter((node): node is Boundary => !!node && isBoundary(node))
109
+ const hosted = boundaries.flatMap((node) =>
110
+ node.type === 'wall'
111
+ ? node.children
112
+ .map((id) => state.nodes[id])
113
+ .filter((child): child is AnyNode => child !== undefined)
114
+ : [],
115
+ )
116
+ return [...boundaries, ...hosted]
117
+ }),
118
+ )
119
+ const boundaries = useMemo(() => analysisNodes.filter(isBoundary), [analysisNodes])
120
+
121
+ const drawing = (mode === 'build' && tool === 'wall') || isDividing
122
+ const roomWallIds = useMemo(() => {
123
+ const ids = new Set<string>()
124
+ for (const space of spaces) for (const wallId of space.wallIds) ids.add(wallId)
125
+ return ids
126
+ }, [spaces])
127
+ const roomlessWallIds = useMemo(
128
+ () =>
129
+ boundaries
130
+ .filter((node) => node.type === 'wall' && !roomWallIds.has(node.id))
131
+ .map((node) => node.id as string),
132
+ [boundaries, roomWallIds],
133
+ )
134
+ const hasWalls = boundaries.some((node) => node.type === 'wall')
135
+ const shouldAnalyse = !!levelId && hasWalls && (drawing || roomlessWallIds.length > 0)
136
+ const openEnds = useMemo(
137
+ () => (shouldAnalyse && levelId ? analyseOpenWallEnds(levelId, analysisNodes) : NO_ENDS),
138
+ [shouldAnalyse, levelId, analysisNodes],
139
+ )
140
+ const ends = useMemo(() => {
141
+ const walls = new Map(boundaries.map((node) => [node.id as string, node]))
142
+ return collapseMutualOpenWallEnds(
143
+ visibleOpenWallEnds(openEnds, roomWallIds, drawing),
144
+ (wallId) => {
145
+ const wall = walls.get(wallId)
146
+ return wall?.type === 'wall'
147
+ ? [wall.end[0] - wall.start[0], wall.end[1] - wall.start[1]]
148
+ : null
149
+ },
150
+ )
151
+ }, [openEnds, roomWallIds, drawing, boundaries])
152
+
153
+ return {
154
+ drawing,
155
+ drafting: wallDrafting || dividingDraft,
156
+ ends,
157
+ roomlessWallIds,
158
+ shown: drawing || ends.length > 0,
159
+ }
160
+ }
161
+
162
+ const HOVER_RELEASE_MS = 250
163
+
164
+ function clearReleaseTimer(timer: { current: ReturnType<typeof setTimeout> | null }) {
165
+ if (timer.current) clearTimeout(timer.current)
166
+ timer.current = null
167
+ }
168
+
169
+ /**
170
+ * Hover / pin / join for one view's markers — the floor plan and the 3D view
171
+ * each keep their own, so hovering a dot in one does not pop a pill in the
172
+ * other. A pinned pill closes on Escape or a press outside any
173
+ * `[data-open-wall-end]` element.
174
+ */
175
+ export function useOpenWallEndFocus(ends: readonly OpenWallEnd[], drafting: boolean) {
176
+ const [hoveredKey, setHoveredKey] = useState<string | null>(null)
177
+ const [pinnedKey, setPinnedKey] = useState<string | null>(null)
178
+ const [refusal, setRefusal] = useState<{ key: string; message: string } | null>(null)
179
+ const releaseTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
180
+
181
+ const clearTimer = () => clearReleaseTimer(releaseTimer)
182
+ useEffect(() => () => clearReleaseTimer(releaseTimer), [])
183
+
184
+ const activeKey = pinnedKey ?? hoveredKey
185
+ const activeEnd = drafting
186
+ ? null
187
+ : (ends.find((end) => end.candidate && openWallEndKey(end) === activeKey) ?? null)
188
+
189
+ useEffect(() => {
190
+ if (!pinnedKey) return
191
+ const onPointerDown = (event: PointerEvent) => {
192
+ const target = event.target instanceof Element ? event.target : null
193
+ if (target?.closest('[data-open-wall-end]')) return
194
+ setPinnedKey(null)
195
+ }
196
+ window.addEventListener('pointerdown', onPointerDown, true)
197
+ return () => window.removeEventListener('pointerdown', onPointerDown, true)
198
+ }, [pinnedKey])
199
+
200
+ useEffect(() => {
201
+ if (!activeEnd) return
202
+ const onKeyDown = (event: KeyboardEvent) => {
203
+ if (event.key !== 'Escape') return
204
+ event.stopPropagation()
205
+ clearReleaseTimer(releaseTimer)
206
+ setPinnedKey(null)
207
+ setHoveredKey(null)
208
+ setRefusal(null)
209
+ }
210
+ window.addEventListener('keydown', onKeyDown, true)
211
+ return () => window.removeEventListener('keydown', onKeyDown, true)
212
+ }, [activeEnd])
213
+
214
+ return {
215
+ activeKey,
216
+ activeEnd,
217
+ refusal: activeEnd && refusal?.key === openWallEndKey(activeEnd) ? refusal.message : null,
218
+ keepHover: (key: string) => {
219
+ clearTimer()
220
+ setHoveredKey(key)
221
+ },
222
+ releaseHover: () => {
223
+ clearTimer()
224
+ releaseTimer.current = setTimeout(() => setHoveredKey(null), HOVER_RELEASE_MS)
225
+ },
226
+ togglePin: (key: string) => setPinnedKey((current) => (current === key ? null : key)),
227
+ pin: (key: string) => setPinnedKey(key),
228
+ join: (end: OpenWallEnd) => {
229
+ const key = openWallEndKey(end)
230
+ const message = joinOpenWallEnd(end)
231
+ setRefusal(message ? { key, message } : null)
232
+ if (message) return
233
+ sfxEmitter.emit('sfx:structure-build')
234
+ setPinnedKey(null)
235
+ setHoveredKey(null)
236
+ },
237
+ }
238
+ }
@@ -0,0 +1,71 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { type AnyNode, registerNode } from '@pascal-app/core'
3
+ import {
4
+ placementNoticeOf,
5
+ previewPlacementNoticeOf,
6
+ transientDraftId,
7
+ } from './use-placement-notice'
8
+
9
+ // The editor asks a node's definition for its placement notice; it names no kind.
10
+ function registerKind(placementNotice?: unknown): string {
11
+ const kind = `test:notice-${crypto.randomUUID()}`
12
+ registerNode({
13
+ kind,
14
+ schemaVersion: 1,
15
+ category: 'furnish',
16
+ defaults: () => ({}),
17
+ capabilities: {},
18
+ ...(placementNotice ? { placementNotice } : {}),
19
+ } as never)
20
+ return kind
21
+ }
22
+
23
+ const nodeOf = (id: string, type: string, metadata?: Record<string, unknown>) =>
24
+ ({ id, type, parentId: 'level_a', metadata: metadata ?? {} }) as unknown as AnyNode
25
+
26
+ describe('the placement notice of the node in hand', () => {
27
+ test("comes from the node's definition, at its live pose", () => {
28
+ const kind = registerKind((node: AnyNode, ctx: { live?: { position: number[] } }) => ({
29
+ line: `${node.id} at ${ctx.live?.position.join(',') ?? 'rest'}`,
30
+ }))
31
+ const nodes = { a: nodeOf('a', kind) }
32
+ expect(placementNoticeOf(nodes, 'a')).toEqual({ line: 'a at rest' })
33
+ expect(placementNoticeOf(nodes, 'a', { position: [1, 0, 2], rotation: 0 })).toEqual({
34
+ line: 'a at 1,0,2',
35
+ })
36
+ })
37
+
38
+ test('is null for a kind that declares none, and for a node not in the scene', () => {
39
+ const nodes = { b: nodeOf('b', registerKind()) }
40
+ expect(placementNoticeOf(nodes, 'b')).toBeNull()
41
+ expect(placementNoticeOf(nodes, 'missing')).toBeNull()
42
+ })
43
+
44
+ test("a tool's transient draft is found whatever its kind", () => {
45
+ const nodes = {
46
+ kept: nodeOf('kept', registerKind()),
47
+ draft: nodeOf('draft', registerKind(), { isTransient: true }),
48
+ }
49
+ expect(transientDraftId(nodes) as string | null).toBe('draft')
50
+ expect(transientDraftId({ kept: nodes.kept })).toBeNull()
51
+ })
52
+ })
53
+
54
+ test('ephemeral preview notices see their context without inserting a draft into the scene', () => {
55
+ const kind = registerKind((node: AnyNode, ctx: { nodes: Record<string, AnyNode> }) => ({
56
+ line: `${node.id}:${ctx.nodes.child?.name}`,
57
+ }))
58
+ const nodes = {}
59
+ const draft = nodeOf('preview', kind)
60
+ expect(
61
+ previewPlacementNoticeOf(nodes, {
62
+ node: draft,
63
+ parentNode: null,
64
+ contextNodes: [{ ...nodeOf('child', kind), name: 'draft child' }],
65
+ }),
66
+ ).toEqual({ line: 'preview:draft child' })
67
+ expect(nodes).toEqual({})
68
+ expect(
69
+ previewPlacementNoticeOf(nodes, { node: null, parentNode: null, contextNodes: [] }),
70
+ ).toBeNull()
71
+ })
@@ -0,0 +1,73 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ type LiveTransformLike,
5
+ nodeRegistry,
6
+ type PlacementNotice,
7
+ useLiveTransforms,
8
+ useScene,
9
+ } from '@pascal-app/core'
10
+ import { useMemo } from 'react'
11
+ import { useShallow } from 'zustand/react/shallow'
12
+ import usePlacementPreview from '../store/use-placement-preview'
13
+
14
+ /** The notice `nodeId`'s kind gives for where it stands (`placementNotice`), if it declares one. */
15
+ export function placementNoticeOf(
16
+ nodes: Readonly<Record<string, AnyNode>>,
17
+ nodeId: string,
18
+ live?: LiveTransformLike,
19
+ ): PlacementNotice | null {
20
+ const node = nodes[nodeId as AnyNodeId]
21
+ const notice = node ? nodeRegistry.get(node.type)?.placementNotice : undefined
22
+ if (!(node && notice)) return null
23
+ return notice(node as never, live ? { nodes, live } : { nodes })
24
+ }
25
+
26
+ export function previewPlacementNoticeOf(
27
+ nodes: Readonly<Record<string, AnyNode>>,
28
+ draft: Pick<
29
+ ReturnType<typeof usePlacementPreview.getState>,
30
+ 'node' | 'parentNode' | 'contextNodes'
31
+ >,
32
+ ): PlacementNotice | null {
33
+ if (!draft.node) return null
34
+ const context = {
35
+ ...nodes,
36
+ ...Object.fromEntries(draft.contextNodes.map((node) => [node.id, node])),
37
+ ...(draft.parentNode ? { [draft.parentNode.id]: draft.parentNode } : {}),
38
+ [draft.node.id]: draft.node,
39
+ }
40
+ return placementNoticeOf(context, draft.node.id)
41
+ }
42
+
43
+ /**
44
+ * What the node in hand, or the selected node, says about where it stands, read at its live pose
45
+ * while it is placed or moved. Subscribes to the scene only while a node is given, so an
46
+ * always-mounted caller doesn't re-render on every write.
47
+ */
48
+ export function usePlacementNotice(
49
+ nodeId: string | null | undefined,
50
+ preview = false,
51
+ ): PlacementNotice | null {
52
+ const draft = usePlacementPreview(
53
+ useShallow((state) =>
54
+ preview
55
+ ? { node: state.node, parentNode: state.parentNode, contextNodes: state.contextNodes }
56
+ : null,
57
+ ),
58
+ )
59
+ const nodes = useScene((state) => (nodeId || draft?.node ? state.nodes : null))
60
+ const live = useLiveTransforms((state) => (nodeId ? state.get(nodeId) : undefined))
61
+ return useMemo(() => {
62
+ if (!nodes) return null
63
+ if (nodeId) return placementNoticeOf(nodes, nodeId, live)
64
+ if (!draft?.node) return null
65
+ return previewPlacementNoticeOf(nodes, draft)
66
+ }, [nodes, live, nodeId, draft])
67
+ }
68
+
69
+ /** The node a construction tool is placing: its transient draft. */
70
+ export const transientDraftId = (nodes: Readonly<Record<string, AnyNode>>) =>
71
+ Object.values(nodes).find(
72
+ (node) => !!(node.metadata as { isTransient?: boolean } | null)?.isTransient,
73
+ )?.id ?? null
@@ -0,0 +1,143 @@
1
+ 'use client'
2
+
3
+ import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import { useCallback, useSyncExternalStore } from 'react'
6
+ import { selectionEnabled } from '../lib/interaction/scope'
7
+ import {
8
+ type RoomKey,
9
+ RoomSelectionIndex,
10
+ type RoomSelectionRecord,
11
+ resolveRoomHit,
12
+ } from '../lib/room-selection'
13
+ import { shouldInterceptRoom } from '../lib/room-selection-commands'
14
+ import { type SelectionModifierKeys, selectionModifiersFromEvent } from '../lib/selection-routing'
15
+ import useEditor from '../store/use-editor'
16
+ import useInteractionScope from '../store/use-interaction-scope'
17
+
18
+ const indexes = new Map<string, RoomSelectionIndex>()
19
+ const subscriptions = new Map<string, { listeners: Set<() => void>; dispose: () => void }>()
20
+ const EMPTY: RoomSelectionRecord[] = []
21
+
22
+ export function getRoomSelectionIndex(levelId: string) {
23
+ let index = indexes.get(levelId)
24
+ if (!index) {
25
+ index = new RoomSelectionIndex(levelId)
26
+ indexes.set(levelId, index)
27
+ }
28
+ index.update(useScene.getState().nodes)
29
+ return index
30
+ }
31
+
32
+ function subscribeLevel(levelId: string | null, listener: () => void) {
33
+ if (!levelId) return () => {}
34
+ let subscription = subscriptions.get(levelId)
35
+ if (!subscription) {
36
+ const listeners = new Set<() => void>()
37
+ const dispose = useScene.subscribe((state, previous) => {
38
+ if (state.nodes === previous.nodes) return
39
+ const rooms = getRoomSelectionIndex(levelId).update(state.nodes)
40
+ const editor = useEditor.getState()
41
+ if (
42
+ editor.room?.levelId === levelId &&
43
+ !rooms.some((room) => room.key.zoneId === editor.room?.zoneId)
44
+ )
45
+ editor.clearRoom()
46
+ for (const notify of listeners) notify()
47
+ })
48
+ subscription = { listeners, dispose }
49
+ subscriptions.set(levelId, subscription)
50
+ }
51
+ subscription.listeners.add(listener)
52
+ return () => {
53
+ subscription.listeners.delete(listener)
54
+ if (!subscription.listeners.size) {
55
+ subscription.dispose()
56
+ subscriptions.delete(levelId)
57
+ indexes.delete(levelId)
58
+ }
59
+ }
60
+ }
61
+
62
+ export function useRoomRecords(levelId: string | null) {
63
+ const subscribe = useCallback((notify: () => void) => subscribeLevel(levelId, notify), [levelId])
64
+ const snapshot = useCallback(
65
+ () => (levelId ? getRoomSelectionIndex(levelId).update(useScene.getState().nodes) : EMPTY),
66
+ [levelId],
67
+ )
68
+ return useSyncExternalStore(subscribe, snapshot, snapshot)
69
+ }
70
+
71
+ export function useSelectedRoom() {
72
+ const key = useEditor((state) => state.room)
73
+ const rooms = useRoomRecords(key?.levelId ?? null)
74
+ return rooms.find((room) => room.key.zoneId === key?.zoneId) ?? null
75
+ }
76
+
77
+ /**
78
+ * The room a selected zone is, for the room panel: its record when the room
79
+ * index knows it, `{ record: null }` for a room the walls do not close, null
80
+ * for anything that is not a room.
81
+ */
82
+ export function useZoneRoom(zoneId: string | null): { record: RoomSelectionRecord | null } | null {
83
+ const levelId = useScene((state) => {
84
+ const zone = zoneId ? state.nodes[zoneId as AnyNodeId] : undefined
85
+ return zone?.type === 'zone' && zone.spaceRole === 'room' ? (zone.parentId ?? null) : null
86
+ })
87
+ const rooms = useRoomRecords(levelId)
88
+ if (!(zoneId && levelId)) return null
89
+ return { record: rooms.find((room) => room.key.zoneId === zoneId) ?? null }
90
+ }
91
+
92
+ export function useHighlightedRoom() {
93
+ const key = useEditor((state) => state.hoveredRoom ?? state.room)
94
+ const rooms = useRoomRecords(key?.levelId ?? null)
95
+ return rooms.find((room) => room.key.zoneId === key?.zoneId) ?? null
96
+ }
97
+
98
+ const hitPoint: [number, number] = [0, 0]
99
+
100
+ export function resolveEditorRoomHit(
101
+ node: AnyNode | null,
102
+ levelId: string,
103
+ point: readonly [number, number],
104
+ view: '2d' | '3d',
105
+ ) {
106
+ if (node && node.type !== 'wall' && node.type !== 'slab' && node.type !== 'ceiling') return null
107
+ const index = getRoomSelectionIndex(levelId)
108
+ hitPoint[0] = point[0]
109
+ hitPoint[1] = point[1]
110
+ const room = resolveRoomHit(index, levelId, node, hitPoint, view)
111
+ return room?.key ?? null
112
+ }
113
+
114
+ /** Rooms pick inside the building; from site a click picks the whole building. */
115
+ export function roomPickingEnabled() {
116
+ const editor = useEditor.getState()
117
+ return (
118
+ editor.phase !== 'site' &&
119
+ editor.mode === 'select' &&
120
+ !useViewer.getState().focusedUnitId &&
121
+ selectionEnabled(useInteractionScope.getState().scope)
122
+ )
123
+ }
124
+
125
+ export function hoverRoomFromHit(
126
+ hit: RoomKey | null,
127
+ modifiers: SelectionModifierKeys,
128
+ nodeId: string | null,
129
+ ) {
130
+ const editor = useEditor.getState()
131
+ const room = shouldInterceptRoom(hit, modifiers, nodeId) ? hit : null
132
+ editor.setHoveredRoom(room)
133
+ if (room) useViewer.getState().setHoveredId(null)
134
+ return !!room
135
+ }
136
+
137
+ export function resolvePlanRoomHit(nodeId: string | null, point: readonly [number, number]) {
138
+ const node = nodeId ? useScene.getState().nodes[nodeId as AnyNodeId] : null
139
+ const levelId = node?.parentId ?? useViewer.getState().selection.levelId
140
+ return levelId ? resolveEditorRoomHit(node ?? null, levelId, point, '2d') : null
141
+ }
142
+
143
+ export { selectionModifiersFromEvent }