@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,675 @@
1
+ 'use client'
2
+
3
+ import {
4
+ clampRoomFloorHandle,
5
+ type Point,
6
+ roomDrawnFloor,
7
+ sceneRegistry,
8
+ useScene,
9
+ } from '@pascal-app/core'
10
+ import { useViewer } from '@pascal-app/viewer'
11
+ import { Html } from '@react-three/drei'
12
+ import { type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
13
+ import { useEffect, useMemo, useRef, useState } from 'react'
14
+ import {
15
+ BufferGeometry,
16
+ type Camera,
17
+ DoubleSide,
18
+ Float32BufferAttribute,
19
+ type Group,
20
+ OrthographicCamera,
21
+ Path,
22
+ Plane,
23
+ type Raycaster,
24
+ Shape,
25
+ ShapeGeometry,
26
+ Vector2,
27
+ Vector3,
28
+ } from 'three'
29
+ import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
30
+ import { EDITOR_LAYER } from '../../lib/constants'
31
+ import {
32
+ clearStructuralElevationGuide,
33
+ publishStructuralElevationGuide,
34
+ resolveStructuralElevationSnap,
35
+ } from '../../lib/elevation-guides'
36
+ import { formatLinearMeasurement, MEASUREMENT_ACTIVE_COLOR } from '../../lib/measurements'
37
+ import { commitRoomFloorElevation, roomFloorBase } from '../../lib/room-construction-commands'
38
+ import {
39
+ type MezzanineEdgeHandle,
40
+ mezzanineEdgeHandles,
41
+ mezzanineElevationBounds,
42
+ ownFloorElevationBounds,
43
+ ROOM_ELEVATION_DRAG_LABEL,
44
+ type RoomHandleDrag,
45
+ roomFloorElevation,
46
+ roomPushHandles,
47
+ runMezzanineEdgeDrag,
48
+ runRoomHandleDrag,
49
+ useRoomHandleDrag,
50
+ } from '../../lib/room-handle-drag'
51
+ import type { RoomDimension } from '../../lib/room-push-dimensions'
52
+ import type { RoomSelectionRecord } from '../../lib/room-selection'
53
+ import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../store/use-editor'
54
+ import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
55
+ import { ARROW_COLOR, ARROW_SCALE, HandleArrow } from './handles/handle-arrow'
56
+ import { resolveResizeSnapValue } from './handles/resize-snap'
57
+ import { formatMeasurement } from './measurement-pill'
58
+ import { WallPushArrow } from './wall-push-arrow'
59
+
60
+ const PILL =
61
+ 'pointer-events-none flex items-center gap-2 whitespace-nowrap rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs tabular-nums shadow-sm backdrop-blur'
62
+
63
+ const noRaycast = () => {}
64
+
65
+ /** Plan-space centroid of the room's reference outline (the elevation handle's spot). */
66
+ export function outlineCentroid(rings: Point[][]): Point {
67
+ const outer = rings[0] ?? []
68
+ let a = 0,
69
+ x = 0,
70
+ z = 0
71
+ for (let i = 0; i < outer.length; i++) {
72
+ const p = outer[i]!,
73
+ q = outer[(i + 1) % outer.length]!
74
+ const c = p[0] * q[1] - q[0] * p[1]
75
+ a += c
76
+ x += (p[0] + q[0]) * c
77
+ z += (p[1] + q[1]) * c
78
+ }
79
+ if (Math.abs(a) < 1e-9)
80
+ return outer.length
81
+ ? [
82
+ outer.reduce((s, p) => s + p[0], 0) / outer.length,
83
+ outer.reduce((s, p) => s + p[1], 0) / outer.length,
84
+ ]
85
+ : [0, 0]
86
+ return [x / (3 * a), z / (3 * a)]
87
+ }
88
+
89
+ type DragPlumbing = {
90
+ camera: Camera
91
+ raycaster: Raycaster
92
+ dom: HTMLCanvasElement
93
+ levelId: string
94
+ }
95
+
96
+ function pointerRay({ camera, raycaster, dom }: DragPlumbing, clientX: number, clientY: number) {
97
+ const rect = dom.getBoundingClientRect()
98
+ raycaster.setFromCamera(
99
+ new Vector2(
100
+ ((clientX - rect.left) / rect.width) * 2 - 1,
101
+ -((clientY - rect.top) / rect.height) * 2 + 1,
102
+ ),
103
+ camera,
104
+ )
105
+ return raycaster.ray
106
+ }
107
+
108
+ export function useHandleScale() {
109
+ const { camera } = useThree()
110
+ return camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
111
+ }
112
+
113
+ function ElevationHandle({
114
+ room,
115
+ anchor,
116
+ elevation,
117
+ }: {
118
+ room: RoomSelectionRecord
119
+ anchor: Point
120
+ elevation: number
121
+ }) {
122
+ const [hover, setHover] = useState(false)
123
+ const { camera, raycaster, gl } = useThree()
124
+ const zoom = useHandleScale()
125
+ const levelId = room.key.levelId
126
+ const onPointerDown = (event: ThreeEvent<PointerEvent>) => {
127
+ if (event.button !== 0) return
128
+ event.stopPropagation()
129
+ suppressBoxSelectForPointer(event)
130
+ const level = sceneRegistry.nodes.get(levelId)
131
+ if (!level) return
132
+ level.updateWorldMatrix(true, false)
133
+ const toLocal = level.matrixWorld.clone().invert()
134
+ const world = new Vector3(anchor[0], elevation, anchor[1]).applyMatrix4(level.matrixWorld)
135
+ const normal = camera.getWorldPosition(new Vector3()).sub(world).setY(0)
136
+ if (normal.lengthSq() === 0) return
137
+ const plane = new Plane().setFromNormalAndCoplanarPoint(normal.normalize(), world)
138
+ const plumbing = { camera, raycaster, dom: gl.domElement, levelId }
139
+ const localY = (clientX: number, clientY: number) => {
140
+ const hit = pointerRay(plumbing, clientX, clientY).intersectPlane(plane, new Vector3())
141
+ return hit ? hit.applyMatrix4(toLocal).y : null
142
+ }
143
+ const startY = localY(event.nativeEvent.clientX, event.nativeEvent.clientY)
144
+ if (startY === null) return
145
+ const source = { nodeId: room.zoneId, levelId, anchor }
146
+ const outline = [room.polygon as Point[], ...(room.holes as Point[][])]
147
+ let value = elevation
148
+ const publish = (next: number) =>
149
+ useRoomHandleDrag.setState({
150
+ drag: {
151
+ kind: 'elevation',
152
+ zoneId: room.zoneId,
153
+ levelId,
154
+ anchor,
155
+ outline,
156
+ from: elevation,
157
+ value: next,
158
+ },
159
+ })
160
+ publish(value)
161
+ runRoomHandleDrag({
162
+ label: ROOM_ELEVATION_DRAG_LABEL,
163
+ nodeId: room.zoneId,
164
+ zoneId: room.zoneId,
165
+ levelId,
166
+ requires: [room.zoneId],
167
+ sample: (x, y) => {
168
+ const pointerY = localY(x, y)
169
+ if (pointerY === null) return null
170
+ const nodes = useScene.getState().nodes
171
+ const snapped = resolveResizeSnapValue({
172
+ rawValue: elevation + pointerY - startY,
173
+ gridSnapEnabled: true,
174
+ gridSnapActive: isGridSnapActive(),
175
+ gridSnapStep: Math.min(useEditor.getState().gridSnapStep, 0.05),
176
+ magneticSnapActive: isMagneticSnapActive(),
177
+ magneticSnap: (next) => resolveStructuralElevationSnap(source, next, nodes),
178
+ })
179
+ // A mezzanine stays inside the heights core accepts for it; a room on
180
+ // its own floor, inside its plate's Floor height range.
181
+ const bounds =
182
+ mezzanineElevationBounds(nodes, room.zoneId) ??
183
+ ownFloorElevationBounds(nodes, room.zoneId)
184
+ return bounds
185
+ ? Math.min(bounds.max, Math.max(bounds.min, snapped))
186
+ : clampRoomFloorHandle(nodes, room.zoneId, snapped)
187
+ },
188
+ onValue: (next) => {
189
+ value = next
190
+ publishStructuralElevationGuide(source, next, useScene.getState().nodes)
191
+ publish(next)
192
+ },
193
+ onCommit: () => {
194
+ clearStructuralElevationGuide(room.zoneId)
195
+ if (Math.abs(value - elevation) <= 1e-6) return
196
+ const { unit, metricNotation } = useViewer.getState()
197
+ commitRoomFloorElevation(room.zoneId, value, (meters) =>
198
+ formatLinearMeasurement(meters, unit, metricNotation),
199
+ )
200
+ },
201
+ onCancel: () => clearStructuralElevationGuide(room.zoneId),
202
+ })
203
+ }
204
+ return (
205
+ <ElevationArrows
206
+ anchor={[anchor[0], elevation, anchor[1]]}
207
+ hover={hover}
208
+ onHoverChange={setHover}
209
+ onPointerDown={onPointerDown}
210
+ zoom={zoom}
211
+ />
212
+ )
213
+ }
214
+
215
+ /**
216
+ * Centre-to-centre distance (m, before zoom) from the floor cube to each
217
+ * chevron: the cube's half size (0.08) + a chevron's half length along its
218
+ * axis (0.14 at ARROW_SCALE) + a clear gap wide enough to stay open when a
219
+ * camera looking down foreshortens the vertical, so arrow / gap / cube / gap /
220
+ * arrow never touch.
221
+ */
222
+ const ELEVATION_ARROW_GAP = 0.4
223
+ // The flat chevron tipped to point along ±Y: the duct riser pair's rotation.
224
+ const UP_TILT: [number, number, number] = [0, Math.PI / 2, Math.PI / 2]
225
+ const DOWN_TILT: [number, number, number] = [0, Math.PI / 2, -Math.PI / 2]
226
+
227
+ /**
228
+ * The floor's elevation handle: a cube between an up and a down chevron (the
229
+ * two-way look of the riser handles). The three share one hover and any of
230
+ * them starts the same vertical drag. The chevrons turn to face the camera so
231
+ * the plates never go edge-on.
232
+ */
233
+ export function ElevationArrows({
234
+ anchor,
235
+ hover,
236
+ zoom,
237
+ onHoverChange,
238
+ onPointerDown,
239
+ }: {
240
+ anchor: [number, number, number]
241
+ hover: boolean
242
+ zoom: number
243
+ onHoverChange: (hover: boolean) => void
244
+ onPointerDown: (event: ThreeEvent<PointerEvent>) => void
245
+ }) {
246
+ const group = useRef<Group>(null)
247
+ const { camera } = useThree()
248
+ const cameraPosition = useMemo(() => new Vector3(), [])
249
+ useFrame(() => {
250
+ const root = group.current
251
+ if (!root) return
252
+ const local = root.parent
253
+ ? root.parent.worldToLocal(camera.getWorldPosition(cameraPosition))
254
+ : camera.getWorldPosition(cameraPosition)
255
+ root.rotation.y = Math.atan2(local.x - anchor[0], local.z - anchor[2])
256
+ })
257
+ // Moving between the cube and a chevron must not drop the shared hover.
258
+ const hovered = useRef(new Set<string>())
259
+ const hoverPart = (part: string) => (next: boolean) => {
260
+ if (next) hovered.current.add(part)
261
+ else hovered.current.delete(part)
262
+ onHoverChange(hovered.current.size > 0)
263
+ }
264
+ const gap = ELEVATION_ARROW_GAP * zoom
265
+ const baseScale = zoom * ARROW_SCALE
266
+ return (
267
+ <group position={anchor} ref={group}>
268
+ <HandleArrow
269
+ cursor="ns-resize"
270
+ hover={hover}
271
+ onHoverChange={hoverPart('cube')}
272
+ onPointerDown={onPointerDown}
273
+ placement={{ position: [0, 0, 0], baseScale: zoom }}
274
+ shape="tracker"
275
+ />
276
+ {[
277
+ { key: 'up', y: gap, tilt: UP_TILT },
278
+ { key: 'down', y: -gap, tilt: DOWN_TILT },
279
+ ].map((arrow) => (
280
+ <HandleArrow
281
+ cursor="ns-resize"
282
+ hover={hover}
283
+ indicatorRotation={arrow.tilt}
284
+ key={arrow.key}
285
+ onHoverChange={hoverPart(arrow.key)}
286
+ onPointerDown={onPointerDown}
287
+ placement={{ position: [0, arrow.y, 0], baseScale }}
288
+ shape="chevron"
289
+ thin
290
+ />
291
+ ))}
292
+ </group>
293
+ )
294
+ }
295
+
296
+ /**
297
+ * A selected room's 3D handles: the floor elevation tracker at its centroid
298
+ * and one push/pull arrow outside every boundary wall. Rendered in the level
299
+ * frame by `RoomControls3D`, only while nothing else is in progress.
300
+ */
301
+ export function RoomHandles3D({ room }: { room: RoomSelectionRecord }) {
302
+ const nodes = useScene((s) => s.nodes)
303
+ const elevation = roomFloorElevation(nodes, room.zoneId)
304
+ // A drawn-slab floor's height is the slab's: the room has no height to drag.
305
+ const drawnFloor = roomDrawnFloor(nodes, room.zoneId) !== null
306
+ const anchor = useMemo(
307
+ () => outlineCentroid([room.polygon as Point[], ...(room.holes as Point[][])]),
308
+ [room.polygon, room.holes],
309
+ )
310
+ const handles = useMemo(() => roomPushHandles(nodes, room.spans), [nodes, room.spans])
311
+ // A mezzanine has no walls: its plate edges carry the push arrows instead.
312
+ const edges = useMemo(
313
+ () => (room.mezzanine ? mezzanineEdgeHandles(nodes, room.zoneId) : []),
314
+ [nodes, room.mezzanine, room.zoneId],
315
+ )
316
+ return (
317
+ <group>
318
+ {!drawnFloor && <ElevationHandle anchor={anchor} elevation={elevation} room={room} />}
319
+ {edges.map((handle) => (
320
+ <MezzanineEdgeArrow
321
+ handle={handle}
322
+ key={handle.key}
323
+ levelId={room.key.levelId}
324
+ zoneId={room.zoneId}
325
+ />
326
+ ))}
327
+ {handles.map((handle) => (
328
+ <WallPushArrow
329
+ handle={handle}
330
+ key={handle.key}
331
+ levelId={room.key.levelId}
332
+ zoneId={room.zoneId}
333
+ />
334
+ ))}
335
+ </group>
336
+ )
337
+ }
338
+
339
+ /** The drag in flight: the value pill, and for elevation the floor at its new height. */
340
+ type MezzanineEdgeDrag = Extract<RoomHandleDrag, { kind: 'mezzanine-edge' }>
341
+
342
+ const edgeFill = (color: string) =>
343
+ new MeshBasicNodeMaterial({
344
+ color,
345
+ depthTest: false,
346
+ depthWrite: false,
347
+ opacity: 0.25,
348
+ side: DoubleSide,
349
+ transparent: true,
350
+ })
351
+ const mezzanineEdgeValidFill = edgeFill(ARROW_COLOR)
352
+ const mezzanineEdgeRefusedFill = edgeFill('#ef4444')
353
+
354
+ /** The mezzanine outline an edge push would leave, at plate height, red with its label when refused. */
355
+ function MezzanineEdgeDragPreview({ drag }: { drag: MezzanineEdgeDrag }) {
356
+ const unit = useViewer((s) => s.unit)
357
+ const metricNotation = useViewer((s) => s.metricNotation)
358
+ const geometry = useMemo(() => {
359
+ if (drag.outline.length < 3) return null
360
+ const shape = new Shape(drag.outline.map(([x, z]) => new Vector2(x, z)))
361
+ return new ShapeGeometry(shape).rotateX(Math.PI / 2)
362
+ }, [drag.outline])
363
+ useEffect(() => () => geometry?.dispose(), [geometry])
364
+ const at: Point = [
365
+ drag.anchor[0] + drag.outward[0] * drag.distance,
366
+ drag.anchor[1] + drag.outward[1] * drag.distance,
367
+ ]
368
+ const magnitude = formatMeasurement(Math.abs(drag.distance), unit, metricNotation)
369
+ const signed = `${drag.distance > 0 ? '+' : drag.distance < 0 ? '−' : ''}${magnitude}`
370
+ return (
371
+ <group>
372
+ <RoomDimensionLines3D dimensions={drag.dimensions} />
373
+ {geometry && (
374
+ <mesh
375
+ geometry={geometry}
376
+ layers={EDITOR_LAYER}
377
+ material={drag.message ? mezzanineEdgeRefusedFill : mezzanineEdgeValidFill}
378
+ position-y={drag.elevation + 0.02}
379
+ raycast={noRaycast}
380
+ renderOrder={100}
381
+ />
382
+ )}
383
+ <Html center position={[at[0], drag.elevation + 0.45, at[1]]} zIndexRange={[25, 0]}>
384
+ <div className={PILL}>
385
+ {drag.message ? (
386
+ <span className="font-medium text-red-500">{drag.message}</span>
387
+ ) : (
388
+ <span className="font-medium text-foreground">{signed}</span>
389
+ )}
390
+ </div>
391
+ </Html>
392
+ </group>
393
+ )
394
+ }
395
+
396
+ const dimensionLine = new LineBasicNodeMaterial({
397
+ color: MEASUREMENT_ACTIVE_COLOR,
398
+ depthTest: false,
399
+ depthWrite: false,
400
+ })
401
+ /** Half length (m) of the cross tick closing each end of a room dimension. */
402
+ const DIMENSION_TICK = 0.1
403
+ const DIMENSION_LABEL_OUTLINE = [
404
+ [-1, -1],
405
+ [1, -1],
406
+ [-1, 1],
407
+ [1, 1],
408
+ ]
409
+ .map(([x, y]) => `${x}px ${y}px 0 ${MEASUREMENT_ACTIVE_COLOR}`)
410
+ .join(', ')
411
+
412
+ /**
413
+ * A push's live room widths: one measurement-tool line per dimension, just
414
+ * above its room's floor and drawn over the walls, closed by cross ticks, with
415
+ * the measurement's outlined label — apart from the moved-distance pill.
416
+ */
417
+ function RoomDimensionLines3D({ dimensions }: { dimensions: RoomDimension[] }) {
418
+ const unit = useViewer((s) => s.unit)
419
+ const metricNotation = useViewer((s) => s.metricNotation)
420
+ const geometry = useMemo(() => {
421
+ if (!dimensions.length) return null
422
+ const positions: number[] = []
423
+ for (const { from, to, elevation: y } of dimensions) {
424
+ const length = Math.hypot(to[0] - from[0], to[1] - from[1]) || 1
425
+ const tx = (-(to[1] - from[1]) / length) * DIMENSION_TICK
426
+ const tz = ((to[0] - from[0]) / length) * DIMENSION_TICK
427
+ positions.push(from[0], y, from[1], to[0], y, to[1])
428
+ for (const [x, z] of [from, to]) positions.push(x - tx, y, z - tz, x + tx, y, z + tz)
429
+ }
430
+ return new BufferGeometry().setAttribute('position', new Float32BufferAttribute(positions, 3))
431
+ }, [dimensions])
432
+ useEffect(() => () => geometry?.dispose(), [geometry])
433
+ if (!geometry) return null
434
+ return (
435
+ <group>
436
+ <lineSegments
437
+ frustumCulled={false}
438
+ geometry={geometry}
439
+ layers={EDITOR_LAYER}
440
+ material={dimensionLine}
441
+ raycast={noRaycast}
442
+ renderOrder={1001}
443
+ />
444
+ {dimensions.map((dimension) => (
445
+ <Html
446
+ center
447
+ key={dimension.key}
448
+ position={[
449
+ (dimension.from[0] + dimension.to[0]) / 2,
450
+ dimension.elevation,
451
+ (dimension.from[1] + dimension.to[1]) / 2,
452
+ ]}
453
+ style={{ pointerEvents: 'none' }}
454
+ zIndexRange={[20, 0]}
455
+ >
456
+ <div
457
+ className="-translate-y-3 whitespace-nowrap font-medium text-base text-white"
458
+ data-room-dimension-3d
459
+ style={{ textShadow: DIMENSION_LABEL_OUTLINE }}
460
+ >
461
+ {formatMeasurement(dimension.distance, unit, metricNotation)}
462
+ </div>
463
+ </Html>
464
+ ))}
465
+ </group>
466
+ )
467
+ }
468
+
469
+ /** A mezzanine edge's push arrow: the room push arrow's look, driving `setZoneEdges`. */
470
+ function MezzanineEdgeArrow({
471
+ handle,
472
+ zoneId,
473
+ levelId,
474
+ }: {
475
+ handle: MezzanineEdgeHandle
476
+ zoneId: string
477
+ levelId: string
478
+ }) {
479
+ const [hover, setHover] = useState(false)
480
+ const { camera, raycaster, gl } = useThree()
481
+ const zoom = useHandleScale()
482
+ const onPointerDown = (event: ThreeEvent<PointerEvent>) => {
483
+ if (event.button !== 0) return
484
+ event.stopPropagation()
485
+ suppressBoxSelectForPointer(event)
486
+ const level = sceneRegistry.nodes.get(levelId)
487
+ if (!level) return
488
+ level.updateWorldMatrix(true, false)
489
+ const toLocal = level.matrixWorld.clone().invert()
490
+ const origin = new Vector3(handle.position[0], handle.height, handle.position[1]).applyMatrix4(
491
+ level.matrixWorld,
492
+ )
493
+ const up = new Vector3(0, 1, 0).transformDirection(level.matrixWorld)
494
+ const plane = new Plane().setFromNormalAndCoplanarPoint(up, origin)
495
+ const plumbing = { camera, raycaster, dom: gl.domElement, levelId }
496
+ const along = (clientX: number, clientY: number) => {
497
+ const hit = pointerRay(plumbing, clientX, clientY).intersectPlane(plane, new Vector3())
498
+ if (!hit) return null
499
+ const local = hit.applyMatrix4(toLocal)
500
+ return (
501
+ (local.x - handle.position[0]) * handle.outward[0] +
502
+ (local.z - handle.position[1]) * handle.outward[1]
503
+ )
504
+ }
505
+ const from = along(event.nativeEvent.clientX, event.nativeEvent.clientY)
506
+ if (from === null) return
507
+ runMezzanineEdgeDrag({ handle, zoneId, levelId, from, along })
508
+ }
509
+ return (
510
+ <HandleArrow
511
+ cursor="grab"
512
+ hover={hover}
513
+ onHoverChange={setHover}
514
+ onPointerDown={onPointerDown}
515
+ placement={{
516
+ position: [handle.position[0], handle.height, handle.position[1]],
517
+ rotation: [0, Math.atan2(-handle.outward[1], handle.outward[0]), 0],
518
+ baseScale: zoom * ARROW_SCALE,
519
+ }}
520
+ shape="chevron"
521
+ thin
522
+ />
523
+ )
524
+ }
525
+
526
+ export function RoomHandleDragPreview3D() {
527
+ const drag = useRoomHandleDrag((s) => s.drag)
528
+ const unit = useViewer((s) => s.unit)
529
+ const metricNotation = useViewer((s) => s.metricNotation)
530
+ const elevation = drag?.kind === 'elevation' ? drag : null
531
+ const geometry = useMemo(() => {
532
+ if (!elevation) return null
533
+ const [outer, ...holes] = elevation.outline
534
+ if (!outer?.length) return null
535
+ const ring = <T extends Shape | Path>(target: T, points: Point[]) => {
536
+ for (const [i, [x, z]] of points.entries()) {
537
+ if (i) target.lineTo(x, z)
538
+ else target.moveTo(x, z)
539
+ }
540
+ target.closePath()
541
+ return target
542
+ }
543
+ const shape = ring(new Shape(), outer)
544
+ shape.holes = holes.map((points) => ring(new Path(), points))
545
+ const [ax, az] = elevation.anchor
546
+ return {
547
+ floor: new ShapeGeometry(shape).rotateX(Math.PI / 2),
548
+ leader: new BufferGeometry().setAttribute(
549
+ 'position',
550
+ new Float32BufferAttribute([ax, elevation.from, az, ax, elevation.value, az], 3),
551
+ ),
552
+ }
553
+ }, [elevation])
554
+ const materials = useMemo(
555
+ () => ({
556
+ line: new LineBasicNodeMaterial({ color: ARROW_COLOR, depthTest: false, depthWrite: false }),
557
+ fill: new MeshBasicNodeMaterial({
558
+ color: ARROW_COLOR,
559
+ depthTest: false,
560
+ depthWrite: false,
561
+ opacity: 0.2,
562
+ side: DoubleSide,
563
+ transparent: true,
564
+ }),
565
+ }),
566
+ [],
567
+ )
568
+ useEffect(
569
+ () => () => {
570
+ geometry?.floor.dispose()
571
+ geometry?.leader.dispose()
572
+ },
573
+ [geometry],
574
+ )
575
+ useEffect(
576
+ () => () => {
577
+ materials.line.dispose()
578
+ materials.fill.dispose()
579
+ },
580
+ [materials],
581
+ )
582
+ if (!drag) return null
583
+ if (drag.kind === 'mezzanine-edge') return <MezzanineEdgeDragPreview drag={drag} />
584
+ if (drag.kind === 'elevation') {
585
+ const [ax, az] = drag.anchor
586
+ return (
587
+ <group>
588
+ {geometry && (
589
+ <>
590
+ <mesh
591
+ geometry={geometry.floor}
592
+ layers={EDITOR_LAYER}
593
+ material={materials.fill}
594
+ position-y={drag.value + 0.01}
595
+ raycast={noRaycast}
596
+ renderOrder={100}
597
+ />
598
+ <lineSegments
599
+ geometry={geometry.leader}
600
+ layers={EDITOR_LAYER}
601
+ material={materials.line}
602
+ raycast={noRaycast}
603
+ renderOrder={1001}
604
+ />
605
+ </>
606
+ )}
607
+ <HandleArrow
608
+ cursor="ns-resize"
609
+ hover
610
+ onHoverChange={() => {}}
611
+ onPointerDown={() => {}}
612
+ placement={{ position: [ax, drag.value, az], baseScale: 1 }}
613
+ shape="tracker"
614
+ />
615
+ <Html center position={[ax, drag.value + 0.35, az]} zIndexRange={[25, 0]}>
616
+ <div className={PILL}>
617
+ <span className="font-medium text-foreground">Floor</span>
618
+ <span className="text-muted-foreground">
619
+ {formatMeasurement(
620
+ drag.value - roomFloorBase(useScene.getState().nodes, drag.zoneId),
621
+ unit,
622
+ metricNotation,
623
+ )}
624
+ </span>
625
+ </div>
626
+ </Html>
627
+ </group>
628
+ )
629
+ }
630
+ const at: Point = [
631
+ drag.anchor[0] + drag.outward[0] * drag.distance,
632
+ drag.anchor[1] + drag.outward[1] * drag.distance,
633
+ ]
634
+ const magnitude = formatMeasurement(Math.abs(drag.distance), unit, metricNotation)
635
+ const signed = `${drag.distance > 0 ? '+' : drag.distance < 0 ? '−' : ''}${magnitude}`
636
+ return (
637
+ <group>
638
+ <RoomDimensionLines3D dimensions={drag.dimensions} />
639
+ {drag.ghosts.map((ghost, index) => {
640
+ const length = Math.hypot(ghost.end[0] - ghost.start[0], ghost.end[1] - ghost.start[1])
641
+ if (length < 1e-3) return null
642
+ return (
643
+ <mesh
644
+ key={index}
645
+ layers={EDITOR_LAYER}
646
+ material={materials.fill}
647
+ position={[
648
+ (ghost.start[0] + ghost.end[0]) / 2,
649
+ drag.base + ghost.height / 2,
650
+ (ghost.start[1] + ghost.end[1]) / 2,
651
+ ]}
652
+ raycast={noRaycast}
653
+ renderOrder={99}
654
+ rotation={[
655
+ 0,
656
+ -Math.atan2(ghost.end[1] - ghost.start[1], ghost.end[0] - ghost.start[0]),
657
+ 0,
658
+ ]}
659
+ >
660
+ <boxGeometry args={[length, ghost.height, ghost.thickness]} />
661
+ </mesh>
662
+ )
663
+ })}
664
+ <Html center position={[at[0], drag.height + 0.45, at[1]]} zIndexRange={[25, 0]}>
665
+ <div className={PILL}>
666
+ {drag.message ? (
667
+ <span className="font-medium text-red-500">{drag.message}</span>
668
+ ) : (
669
+ <span className="font-medium text-foreground">{signed}</span>
670
+ )}
671
+ </div>
672
+ </Html>
673
+ </group>
674
+ )
675
+ }