@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
@@ -0,0 +1,273 @@
1
+ 'use client'
2
+
3
+ import { type OpenWallEnd, sceneRegistry } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import { Html } from '@react-three/drei'
6
+ import { useFrame } from '@react-three/fiber'
7
+ import { memo, useEffect, useMemo, useRef } from 'react'
8
+ import { BoxGeometry, CircleGeometry, type Group, RingGeometry } from 'three'
9
+ import { MeshBasicNodeMaterial } from 'three/webgpu'
10
+ import {
11
+ useOpenWallEndFocus,
12
+ useOpenWallEnds,
13
+ useOpenWallEndsSuppressed,
14
+ } from '../../hooks/use-open-wall-ends'
15
+ import { EDITOR_LAYER } from '../../lib/constants'
16
+ import { openWallEndKey, openWallEndLabel } from '../../lib/floorplan/open-wall-ends'
17
+ import { MEASUREMENT_DANGLING_COLOR } from '../../lib/measurements'
18
+ import { cn } from '../../lib/utils'
19
+ import useEditor from '../../store/use-editor'
20
+ import { isViewVisible, VIEW_3D, visibleScene } from '../../store/view-layout'
21
+ import { JoinWallsPill } from './join-walls-pill'
22
+ import { OpenWallEndsHint } from './open-wall-ends-hint'
23
+
24
+ /**
25
+ * The 3D twin of `FloorplanOpenWallEndsLayer`: at each wall end that isn't
26
+ * joined, a red disc on the floor with a dashed ribbon to the wall it nearly
27
+ * meets, and a screen-space dot carrying the same label ("4 cm gap") and, on
28
+ * hover or click, the same "Join walls" pill. The floor geometry ignores depth
29
+ * so it reads through cutaway walls like the snap beacon; the dot is DOM, so
30
+ * it stays clickable where a wall stands between it and the camera.
31
+ *
32
+ * Shares the analysis with the floor plan (`useOpenWallEnds`). Mounted inside
33
+ * ToolManager's building-local group — wall points are building-local plan
34
+ * coordinates — and lifted to the active level's floor each frame.
35
+ */
36
+
37
+ const FLOOR_LIFT = 0.02
38
+ const DISC_RADIUS = 0.07
39
+ const DISC_OUTLINE = 0.095
40
+ const DASH_LEN = 0.06
41
+ const DASH_GAP = 0.04
42
+ const LINE_WIDTH = 0.025
43
+ const MAX_DASHES = 40
44
+ const NO_RAYCAST = () => null
45
+
46
+ function createMarkerResources() {
47
+ const materialOptions = {
48
+ depthTest: false,
49
+ depthWrite: false,
50
+ toneMapped: false,
51
+ transparent: true,
52
+ }
53
+ return {
54
+ redMaterial: new MeshBasicNodeMaterial({
55
+ ...materialOptions,
56
+ color: MEASUREMENT_DANGLING_COLOR,
57
+ }),
58
+ outlineMaterial: new MeshBasicNodeMaterial({ ...materialOptions, color: 0xff_ff_ff }),
59
+ discGeometry: new CircleGeometry(1, 24),
60
+ ringGeometry: new RingGeometry(0.7, 1, 24),
61
+ dashGeometry: new BoxGeometry(1, 1, 1),
62
+ }
63
+ }
64
+
65
+ type MarkerResources = ReturnType<typeof createMarkerResources>
66
+
67
+ type Vec3 = [number, number, number]
68
+
69
+ export const OpenWallEnds3DLayer = memo(function OpenWallEnds3DLayer() {
70
+ const visible = useEditor((state) => isViewVisible(state, VIEW_3D))
71
+ const suppressed = useOpenWallEndsSuppressed()
72
+ return visible && !suppressed ? <ActiveOpenWallEnds3DLayer /> : null
73
+ })
74
+
75
+ const ActiveOpenWallEnds3DLayer = memo(function ActiveOpenWallEnds3DLayer() {
76
+ const levelId = useViewer((state) => state.selection.levelId)
77
+ const unit = useViewer((state) => state.unit)
78
+ const { drafting, ends } = useOpenWallEnds()
79
+ // Split view shows the hint once, over the floor plan.
80
+ const hintHere = useEditor((state) => visibleScene(state) === '3d')
81
+ const focus = useOpenWallEndFocus(ends, drafting)
82
+ const groupRef = useRef<Group>(null)
83
+ const hasEnds = ends.length > 0
84
+ const resources = useMemo(() => (hasEnds ? createMarkerResources() : null), [hasEnds])
85
+ // Resources passed as mesh props are shared and aren't disposed by R3F.
86
+ useEffect(
87
+ () => () => {
88
+ if (resources) for (const resource of Object.values(resources)) resource.dispose()
89
+ },
90
+ [resources],
91
+ )
92
+
93
+ useFrame(() => {
94
+ const group = groupRef.current
95
+ if (!group) return
96
+ const levelMesh = levelId ? sceneRegistry.nodes.get(levelId) : null
97
+ group.position.y = levelMesh ? levelMesh.position.y : 0
98
+ })
99
+
100
+ if (!resources) return null
101
+ return (
102
+ <group ref={groupRef}>
103
+ {ends.map((end) => {
104
+ const key = openWallEndKey(end)
105
+ const active = focus.activeEnd !== null && key === focus.activeKey
106
+ return (
107
+ <OpenWallEndMarker3D
108
+ active={active}
109
+ end={end}
110
+ interactive={!!end.candidate && !drafting}
111
+ key={key}
112
+ label={openWallEndLabel(end, unit)}
113
+ onHoverEnd={focus.releaseHover}
114
+ onHoverStart={() => focus.keepHover(key)}
115
+ onJoin={() => focus.join(end)}
116
+ onPin={() => focus.togglePin(key)}
117
+ refusal={active ? focus.refusal : null}
118
+ resources={resources}
119
+ />
120
+ )
121
+ })}
122
+ {hintHere ? (
123
+ // Html only to reach the DOM from inside the canvas; the hint portals
124
+ // itself to the body and centres on the 3D view.
125
+ <Html style={{ pointerEvents: 'none' }}>
126
+ <OpenWallEndsHint ends={ends} onShow={(end) => focus.pin(openWallEndKey(end))} />
127
+ </Html>
128
+ ) : null}
129
+ </group>
130
+ )
131
+ })
132
+
133
+ function OpenWallEndMarker3D({
134
+ active,
135
+ end,
136
+ interactive,
137
+ label,
138
+ onHoverEnd,
139
+ onHoverStart,
140
+ onJoin,
141
+ onPin,
142
+ refusal,
143
+ resources,
144
+ }: {
145
+ active: boolean
146
+ end: OpenWallEnd
147
+ interactive: boolean
148
+ label: string | null
149
+ onHoverEnd: () => void
150
+ onHoverStart: () => void
151
+ onJoin: () => void
152
+ onPin: () => void
153
+ refusal: string | null
154
+ resources: MarkerResources
155
+ }) {
156
+ const { dashGeometry, discGeometry, ringGeometry, redMaterial, outlineMaterial } = resources
157
+ const [x, z] = end.point
158
+ const target = end.candidate?.point
159
+ const { dashes, angleY, dashLength } = useMemo(() => {
160
+ if (!target) return { dashes: [] as Vec3[], angleY: 0, dashLength: 0 }
161
+ const dx = target[0] - x
162
+ const dz = target[1] - z
163
+ const length = Math.hypot(dx, dz)
164
+ const angle = -Math.atan2(dz, dx)
165
+ if (length < 1e-4) return { dashes: [] as Vec3[], angleY: angle, dashLength: 0 }
166
+ const period = Math.max(DASH_LEN + DASH_GAP, length / MAX_DASHES)
167
+ const centres: Vec3[] = []
168
+ for (let d = Math.min(period, length) / 2; d < length; d += period) {
169
+ centres.push([x + (dx / length) * d, FLOOR_LIFT, z + (dz / length) * d])
170
+ }
171
+ return { dashes: centres, angleY: angle, dashLength: Math.min(DASH_LEN, length) }
172
+ }, [target, x, z])
173
+ const stop = (event: { stopPropagation: () => void }) => event.stopPropagation()
174
+
175
+ return (
176
+ <>
177
+ {dashes.map((centre, index) => (
178
+ <mesh
179
+ geometry={dashGeometry}
180
+ key={index}
181
+ layers={EDITOR_LAYER}
182
+ material={redMaterial}
183
+ position={centre}
184
+ raycast={NO_RAYCAST}
185
+ renderOrder={1005}
186
+ rotation={[0, angleY, 0]}
187
+ scale={[dashLength, 0.002, LINE_WIDTH]}
188
+ />
189
+ ))}
190
+ {target ? (
191
+ <mesh
192
+ geometry={ringGeometry}
193
+ layers={EDITOR_LAYER}
194
+ material={redMaterial}
195
+ position={[target[0], FLOOR_LIFT + 0.001, target[1]]}
196
+ raycast={NO_RAYCAST}
197
+ renderOrder={1006}
198
+ rotation={[-Math.PI / 2, 0, 0]}
199
+ scale={[DISC_RADIUS * 0.8, DISC_RADIUS * 0.8, 1]}
200
+ />
201
+ ) : null}
202
+ <mesh
203
+ geometry={discGeometry}
204
+ layers={EDITOR_LAYER}
205
+ material={outlineMaterial}
206
+ position={[x, FLOOR_LIFT + 0.002, z]}
207
+ raycast={NO_RAYCAST}
208
+ renderOrder={1006}
209
+ rotation={[-Math.PI / 2, 0, 0]}
210
+ scale={[DISC_OUTLINE, DISC_OUTLINE, 1]}
211
+ />
212
+ <mesh
213
+ geometry={discGeometry}
214
+ layers={EDITOR_LAYER}
215
+ material={redMaterial}
216
+ position={[x, FLOOR_LIFT + 0.003, z]}
217
+ raycast={NO_RAYCAST}
218
+ renderOrder={1007}
219
+ rotation={[-Math.PI / 2, 0, 0]}
220
+ scale={[DISC_RADIUS, DISC_RADIUS, 1]}
221
+ />
222
+ <Html
223
+ center
224
+ position={[x, FLOOR_LIFT, z]}
225
+ style={{ pointerEvents: 'none', userSelect: 'none' }}
226
+ zIndexRange={[25, 0]}
227
+ >
228
+ <div className="relative flex items-center justify-center">
229
+ <div className="pointer-events-none absolute bottom-full left-1/2 mb-1.5 flex -translate-x-1/2 flex-col items-center gap-1.5">
230
+ {active ? (
231
+ <JoinWallsPill
232
+ onHoverEnd={onHoverEnd}
233
+ onHoverStart={onHoverStart}
234
+ onJoin={onJoin}
235
+ refusal={refusal}
236
+ />
237
+ ) : null}
238
+ {label ? (
239
+ <div
240
+ className="whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-medium font-sans text-[11px] text-white"
241
+ style={{ backgroundColor: MEASUREMENT_DANGLING_COLOR }}
242
+ >
243
+ {label}
244
+ </div>
245
+ ) : null}
246
+ </div>
247
+ <button
248
+ aria-label={label ?? 'Wall end not joined'}
249
+ className={cn(
250
+ 'h-3.5 w-3.5 rounded-full border-2 border-white shadow-sm transition-transform',
251
+ interactive
252
+ ? 'pointer-events-auto cursor-pointer hover:scale-125'
253
+ : 'pointer-events-none',
254
+ active && 'scale-125',
255
+ )}
256
+ data-open-wall-end=""
257
+ onClick={(event) => {
258
+ event.stopPropagation()
259
+ onPin()
260
+ }}
261
+ onPointerDown={stop}
262
+ onPointerEnter={interactive ? onHoverStart : undefined}
263
+ onPointerLeave={interactive ? onHoverEnd : undefined}
264
+ onPointerUp={stop}
265
+ style={{ backgroundColor: MEASUREMENT_DANGLING_COLOR }}
266
+ tabIndex={interactive ? 0 : -1}
267
+ type="button"
268
+ />
269
+ </div>
270
+ </Html>
271
+ </>
272
+ )
273
+ }
@@ -0,0 +1,89 @@
1
+ 'use client'
2
+
3
+ import { type AnyNodeId, emitter, type OpenWallEnd } from '@pascal-app/core'
4
+ import { useEffect, useState } from 'react'
5
+ import { createPortal } from 'react-dom'
6
+ import { openWallEndsSummary } from '../../lib/floorplan/open-wall-ends'
7
+ import { MEASUREMENT_DANGLING_COLOR } from '../../lib/measurements'
8
+
9
+ // The viewer area holds the floor plan and the 3D view side by side, so its
10
+ // top centre clears both views' corner controls in 2D, 3D and split alike.
11
+ const viewerArea = () => document.querySelector('[data-pascal-viewer-3d]')?.parentElement ?? null
12
+
13
+ // The views slide in and out without resizing, so the anchor's box is read
14
+ // per frame while the hint is up, like the Join pill's anchor.
15
+ function useViewerAreaTopCenter(): { left: number; top: number } | null {
16
+ const [anchor, setAnchor] = useState<{ left: number; top: number } | null>(null)
17
+ useEffect(() => {
18
+ let raf = 0
19
+ const tick = () => {
20
+ raf = requestAnimationFrame(tick)
21
+ const rect = viewerArea()?.getBoundingClientRect()
22
+ const next =
23
+ rect && rect.width > 0 ? { left: rect.left + rect.width / 2, top: rect.top } : null
24
+ setAnchor((current) =>
25
+ current && next && current.left === next.left && current.top === next.top ? current : next,
26
+ )
27
+ }
28
+ tick()
29
+ return () => cancelAnimationFrame(raf)
30
+ }, [])
31
+ return anchor
32
+ }
33
+
34
+ /**
35
+ * "2 wall ends aren't joined — rooms can't close", centred at the top of the
36
+ * viewer area. Mounted by one view only (the floor plan when it shows, else
37
+ * the 3D view), so split view shows it once.
38
+ * Show walks the joinable ends one by one: it focuses the 3D camera on the
39
+ * wall and asks the view to pin that end's Join pill.
40
+ */
41
+ export function OpenWallEndsHint({
42
+ ends,
43
+ onShow,
44
+ }: {
45
+ ends: readonly OpenWallEnd[]
46
+ onShow: (end: OpenWallEnd) => void
47
+ }) {
48
+ const anchor = useViewerAreaTopCenter()
49
+ const [cursor, setCursor] = useState(0)
50
+ const joinable = ends.filter((end) => end.candidate)
51
+ const summary = openWallEndsSummary(joinable.length)
52
+ if (!(anchor && summary)) return null
53
+
54
+ const showNext = () => {
55
+ const target = joinable[cursor % joinable.length]
56
+ if (!target) return
57
+ setCursor((value) => value + 1)
58
+ onShow(target)
59
+ emitter.emit('camera-controls:focus', { nodeId: target.wallId as AnyNodeId })
60
+ }
61
+
62
+ return createPortal(
63
+ <div
64
+ className="pointer-events-auto fixed z-30 flex -translate-x-1/2 items-center gap-2 whitespace-nowrap rounded-full border border-border bg-background/95 py-1 pr-1 pl-3 text-xs shadow-lg backdrop-blur-md"
65
+ data-open-wall-end=""
66
+ onPointerDown={(event) => event.stopPropagation()}
67
+ onPointerUp={(event) => event.stopPropagation()}
68
+ style={{ left: anchor.left, top: anchor.top + 16 }}
69
+ >
70
+ <span
71
+ aria-hidden
72
+ className="h-2 w-2 shrink-0 rounded-full"
73
+ style={{ backgroundColor: MEASUREMENT_DANGLING_COLOR }}
74
+ />
75
+ <span className="text-foreground">{summary}</span>
76
+ <button
77
+ className="rounded-full px-3 py-1 font-medium text-foreground transition-colors hover:bg-accent"
78
+ onClick={(event) => {
79
+ event.stopPropagation()
80
+ showNext()
81
+ }}
82
+ type="button"
83
+ >
84
+ Show
85
+ </button>
86
+ </div>,
87
+ document.body,
88
+ )
89
+ }
@@ -6,11 +6,12 @@ import {
6
6
  selectQuickMeasurementHudEntry,
7
7
  useQuickMeasurementHud,
8
8
  } from '../../store/use-quick-measurement-hud'
9
+ import { visibleScene } from '../../store/view-layout'
9
10
  import { QuickMeasurementCard } from './quick-measurement-card'
10
11
 
11
12
  export function QuickMeasurementHud() {
12
- const viewMode = useEditor((state) => state.viewMode)
13
- const entry = useQuickMeasurementHud((state) => selectQuickMeasurementHudEntry(state, viewMode))
13
+ const scene = useEditor(visibleScene)
14
+ const entry = useQuickMeasurementHud((state) => selectQuickMeasurementHudEntry(state, scene))
14
15
  const unit = useViewer((state) => state.unit)
15
16
  const metricNotation = useViewer((state) => state.metricNotation)
16
17
 
@@ -0,0 +1,63 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { type AnyNode, registerNode, useInteractive } from '@pascal-app/core'
3
+ import { renderToStaticMarkup } from 'react-dom/server'
4
+ import { MechanismButton, selectedMechanismNode } from './registry-action-contributions'
5
+
6
+ const switchMechanism = {
7
+ has: (node: AnyNode) => (node as unknown as { joints: number }).joints > 0,
8
+ isOn: (node: AnyNode, state: ReturnType<typeof useInteractive.getState>) =>
9
+ Boolean(state.mechanisms[node.id]),
10
+ set: (node: AnyNode, on: boolean) => useInteractive.getState().setMechanism(node.id, on),
11
+ }
12
+
13
+ // A kind that only declares the capability: no action-menu contribution, no E action.
14
+ function articulated(joints: number): AnyNode {
15
+ const kind = `test:mechanism-${crypto.randomUUID()}`
16
+ registerNode({
17
+ kind,
18
+ schemaVersion: 1,
19
+ category: 'furnish',
20
+ defaults: () => ({}),
21
+ capabilities: { mechanism: switchMechanism },
22
+ } as never)
23
+ return { object: 'node', id: `${kind}_1`, type: kind, joints } as unknown as AnyNode
24
+ }
25
+
26
+ const byId = (...nodes: AnyNode[]) => Object.fromEntries(nodes.map((node) => [node.id, node]))
27
+
28
+ afterEach(() => {
29
+ for (const id of Object.keys(useInteractive.getState().mechanisms))
30
+ useInteractive.getState().removeMechanism(id as never)
31
+ })
32
+
33
+ describe('mechanism action in the action menu', () => {
34
+ test('the only selected node gets Play when its kind declares a mechanism', () => {
35
+ const node = articulated(2)
36
+ expect(selectedMechanismNode([node.id], byId(node))).toBe(node)
37
+ })
38
+
39
+ test('no Play without moving parts, for a multi-selection, or without the capability', () => {
40
+ const still = articulated(0)
41
+ expect(selectedMechanismNode([still.id], byId(still))).toBeUndefined()
42
+
43
+ const node = articulated(2)
44
+ const other = { object: 'node', id: 'other_1', type: 'other' } as unknown as AnyNode
45
+ expect(selectedMechanismNode([node.id, other.id], byId(node, other))).toBeUndefined()
46
+ expect(selectedMechanismNode([other.id], byId(other))).toBeUndefined()
47
+ })
48
+
49
+ test('the button reads Play, then Stop while the mechanism runs', () => {
50
+ const node = articulated(1)
51
+ const idle = renderToStaticMarkup(
52
+ <MechanismButton mechanism={switchMechanism} node={node} running={false} />,
53
+ )
54
+ expect(idle).toContain('aria-label="Play"')
55
+ expect(idle).toContain('aria-pressed="false"')
56
+
57
+ const running = renderToStaticMarkup(
58
+ <MechanismButton mechanism={switchMechanism} node={node} running />,
59
+ )
60
+ expect(running).toContain('aria-label="Stop"')
61
+ expect(running).toContain('aria-pressed="true"')
62
+ })
63
+ })
@@ -0,0 +1,132 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ type MechanismCapability,
7
+ nodeMechanism,
8
+ nodeRegistry,
9
+ toggleMechanism,
10
+ useInteractive,
11
+ useScene,
12
+ } from '@pascal-app/core'
13
+ import { useViewer } from '@pascal-app/viewer'
14
+ import { DoorClosed, DoorOpen, PanelTopClose, PanelTopOpen, Play, Square } from 'lucide-react'
15
+ import { type ComponentType, lazy, Suspense } from 'react'
16
+ import { useShallow } from 'zustand/react/shallow'
17
+ import { getFloorplanNodeExtension } from '../../lib/floorplan/floorplan-extension'
18
+ import { ActionMenuButton } from './action-menu-button'
19
+
20
+ type Loader = () => Promise<{ default: ComponentType }>
21
+ const lazyCache = new WeakMap<Loader, ComponentType>()
22
+
23
+ function contribution(kind: string): ComponentType | null {
24
+ const loader = getFloorplanNodeExtension(nodeRegistry.get(kind))?.actionMenu?.actions
25
+ if (!loader) return null
26
+ const cached = lazyCache.get(loader)
27
+ if (cached) return cached
28
+ const component = lazy(loader)
29
+ lazyCache.set(loader, component)
30
+ return component
31
+ }
32
+
33
+ /** The node the action menu's Play/Stop runs: the only selected node, when its kind declares a mechanism. */
34
+ export function selectedMechanismNode(
35
+ selectedIds: readonly string[],
36
+ nodes: Readonly<Record<string, AnyNode>>,
37
+ ): AnyNode | undefined {
38
+ const node = selectedIds.length === 1 ? nodes[selectedIds[0]!] : undefined
39
+ return nodeMechanism(node) ? node : undefined
40
+ }
41
+
42
+ function MechanismGlyph({
43
+ icon,
44
+ running,
45
+ }: {
46
+ icon: MechanismCapability['icon']
47
+ running: boolean
48
+ }) {
49
+ const Glyph =
50
+ icon === 'door'
51
+ ? running
52
+ ? DoorClosed
53
+ : DoorOpen
54
+ : icon === 'window'
55
+ ? running
56
+ ? PanelTopClose
57
+ : PanelTopOpen
58
+ : running
59
+ ? Square
60
+ : Play
61
+ return <Glyph className="h-4 w-4" />
62
+ }
63
+
64
+ export function MechanismButton({
65
+ node,
66
+ mechanism,
67
+ running,
68
+ }: {
69
+ node: AnyNode
70
+ mechanism: MechanismCapability
71
+ running: boolean
72
+ }) {
73
+ const label = mechanism.icon ? (running ? 'Close' : 'Open') : running ? 'Stop' : 'Play'
74
+ return (
75
+ <ActionMenuButton
76
+ keys={['E']}
77
+ label={label}
78
+ onClick={(event) => {
79
+ event.stopPropagation()
80
+ toggleMechanism(mechanism, node)
81
+ }}
82
+ pressed={running}
83
+ >
84
+ <MechanismGlyph icon={mechanism.icon} running={running} />
85
+ </ActionMenuButton>
86
+ )
87
+ }
88
+
89
+ /** Play/Stop for a single selected node whose kind declares `capabilities.mechanism`. */
90
+ function MechanismAction() {
91
+ const selected = useViewer((s) => s.selection.selectedIds)
92
+ const node = useScene((s) => selectedMechanismNode(selected, s.nodes))
93
+ const mechanism = nodeMechanism(node)
94
+ const running = useInteractive((s) => (node && mechanism ? mechanism.isOn(node, s) : false))
95
+ return node && mechanism ? (
96
+ <MechanismButton mechanism={mechanism} node={node} running={running} />
97
+ ) : null
98
+ }
99
+
100
+ /**
101
+ * The buttons kinds add to the action menu for selections holding them
102
+ * (`extensions['pascal:editor/floorplan'].actionMenu.actions`), in 2D and 3D
103
+ * alike. Each contribution reads the selection and decides its own visibility.
104
+ */
105
+ export function RegistryActionContributions() {
106
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
107
+ const kinds = useScene(
108
+ useShallow((s) =>
109
+ Array.from(
110
+ new Set(
111
+ selectedIds.flatMap((id) => {
112
+ const type = s.nodes[id as AnyNodeId]?.type
113
+ return type ? [type] : []
114
+ }),
115
+ ),
116
+ ).sort(),
117
+ ),
118
+ )
119
+ return (
120
+ <>
121
+ <MechanismAction />
122
+ {kinds.map((kind) => {
123
+ const Contribution = contribution(kind)
124
+ return Contribution ? (
125
+ <Suspense fallback={null} key={kind}>
126
+ <Contribution />
127
+ </Suspense>
128
+ ) : null
129
+ })}
130
+ </>
131
+ )
132
+ }