@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,324 @@
1
+ import { CameraControlsImpl } from '@react-three/drei'
2
+ import { Vector3 } from 'three'
3
+ import {
4
+ floorLimitAt,
5
+ glideFloorLimit,
6
+ ORBIT_FLOOR_CLEARANCE,
7
+ orbitCameraAboveFloor,
8
+ } from '../../lib/orbit-floor-clearance'
9
+
10
+ const virtualTarget = new Vector3()
11
+ const virtualPosition = new Vector3()
12
+ const groundedPosition = new Vector3()
13
+ const drawnTarget = new Vector3()
14
+ const shiftDelta = new Vector3()
15
+ // Below this height over the level's floor limit the surface under the camera
16
+ // is looked up; higher up it cannot matter.
17
+ const SURFACE_BAND = 3
18
+ // While the limit glides up over a platform's edge, the camera still never
19
+ // sits closer than this to the floor it is over.
20
+ const SURFACE_MIN_GAP = 0.05
21
+
22
+ /** The walkable surface under the camera (see `FloorSurface`). */
23
+ export type CameraFloorSurface = {
24
+ /** Changes whenever an earlier answer may no longer hold. */
25
+ readonly revision: number
26
+ topAt(x: number, z: number, levelY: number): number | null
27
+ }
28
+
29
+ /**
30
+ * Orbit controls whose own state never puts a perspective camera under the
31
+ * floor surface below it: an orbit past the clearance keeps the camera on it
32
+ * and pitches the view up (walls do not stop it: it passes through them). The
33
+ * push and pitch are applied to the controls' own target and angle, so every
34
+ * reader (`getPosition`, `getTarget`, `camera.position`, truck speed,
35
+ * snapshots) sees the pose that is drawn. `floorShift` is the
36
+ * push currently applied; the orbit underneath it is what a later rotate or
37
+ * zoom continues from, which is why orbiting back up returns the exact
38
+ * earlier pose.
39
+ */
40
+ export class FloorAwareCameraControls extends CameraControlsImpl {
41
+ /**
42
+ * The active level's floor plus the clearance: the limit where no floor
43
+ * surface is found under the camera. Null turns the floor off.
44
+ */
45
+ floorMinY: number | null = null
46
+ /** Walkable surface of the active level; the limit follows its top. */
47
+ floorSurface: CameraFloorSurface | null = null
48
+ // The floor limit under the camera, eased as the camera crosses floors of
49
+ // different heights; the last floor found on this level, kept over holes
50
+ // (a stair void) and through a slab's rebuild; and the last lookup, reused
51
+ // at rest.
52
+ private surfaceMinY: number | null = null
53
+ private surfaceTop: number | null = null
54
+ private surfaceLevelY: number | null = null
55
+ private surfaceQuery = { x: Number.NaN, z: Number.NaN, revision: -1, top: null as number | null }
56
+ private frame = 0
57
+ private frameDelta = 0
58
+ private glideFrame = -1
59
+ private readonly floorShift = new Vector3()
60
+ // How much steeper the drawn view is than the orbit (the look-up pitch).
61
+ private polarShift = 0
62
+ // A pose written with `setLookAt` (applied pose, saved view, snapshot
63
+ // replay) is shown exactly, even under the clearance; the floor holds at
64
+ // its height until the orbit heads back above the clearance or the floor
65
+ // itself changes (another level).
66
+ private heldMinY: number | null = null
67
+ private heldFloorMinY: number | null = null
68
+ private relativeMove = false
69
+ private relativeRotate = false
70
+ private readonly floorUpdateEvent = { type: 'update' }
71
+ /** The lowest the camera may sit right now: the clearance above the floor under it. */
72
+ get floorLimitY() {
73
+ return this.surfaceMinY ?? this.floorMinY
74
+ }
75
+
76
+ // Level navigation translates the orbit's floor, rather than its pushed target.
77
+ getOrbitTarget(out: Vector3) {
78
+ return this.getTarget(out).sub(this.floorShift)
79
+ }
80
+
81
+ /** The orbit's own polar angle, under the floor's look-up pitch. */
82
+ get orbitPolarAngle() {
83
+ return this._spherical.phi - this.polarShift
84
+ }
85
+
86
+ // Back to the orbit under the floor's push and pitch. Nothing is drawn in
87
+ // between: the next update re-derives the same drawn pose from it.
88
+ private dropFloorShift() {
89
+ this._target.sub(this.floorShift)
90
+ this._targetEnd.sub(this.floorShift)
91
+ this._spherical.phi -= this.polarShift
92
+ this._sphericalEnd.phi -= this.polarShift
93
+ this.floorShift.set(0, 0, 0)
94
+ this.polarShift = 0
95
+ }
96
+
97
+ override moveTo(x: number, y: number, z: number, enableTransition = false) {
98
+ // Absolute destinations replace the orbit; trucks keep its remembered push.
99
+ if (!this.relativeMove) {
100
+ // Travel from the orbit under the push, not from the pushed target: the
101
+ // push is recomputed on the way, so nothing jumps, and a low camera
102
+ // does not start the trip lifted above its orbit.
103
+ this.dropFloorShift()
104
+ this.heldMinY = null
105
+ }
106
+ return super.moveTo(x, y, z, enableTransition)
107
+ }
108
+
109
+ override fitToBox(...args: Parameters<CameraControlsImpl['fitToBox']>) {
110
+ // Framing starts from the orbit, not from the floor's look-up pitch.
111
+ this.dropFloorShift()
112
+ return super.fitToBox(...args)
113
+ }
114
+
115
+ override rotate(azimuthAngle: number, polarAngle: number, enableTransition = false) {
116
+ this.relativeRotate = true
117
+ try {
118
+ return super.rotate(azimuthAngle, polarAngle, enableTransition)
119
+ } finally {
120
+ this.relativeRotate = false
121
+ }
122
+ }
123
+
124
+ override rotateTo(azimuthAngle: number, polarAngle: number, enableTransition = false) {
125
+ // An absolute angle is the orbit's; drags (`rotate`) keep the look-up pitch.
126
+ if (!this.relativeRotate) this.dropFloorShift()
127
+ return super.rotateTo(azimuthAngle, polarAngle, enableTransition)
128
+ }
129
+
130
+ override truck(x: number, y: number, enableTransition = false) {
131
+ this.relativeMove = true
132
+ try {
133
+ return super.truck(x, y, enableTransition)
134
+ } finally {
135
+ this.relativeMove = false
136
+ }
137
+ }
138
+
139
+ override forward(distance: number, enableTransition = false) {
140
+ this.relativeMove = true
141
+ try {
142
+ return super.forward(distance, enableTransition)
143
+ } finally {
144
+ this.relativeMove = false
145
+ }
146
+ }
147
+
148
+ override elevate(height: number, enableTransition = false) {
149
+ this.relativeMove = true
150
+ try {
151
+ return super.elevate(height, enableTransition)
152
+ } finally {
153
+ this.relativeMove = false
154
+ }
155
+ }
156
+
157
+ override setLookAt(
158
+ positionX: number,
159
+ positionY: number,
160
+ positionZ: number,
161
+ targetX: number,
162
+ targetY: number,
163
+ targetZ: number,
164
+ enableTransition = false,
165
+ ) {
166
+ const done = super.setLookAt(
167
+ positionX,
168
+ positionY,
169
+ positionZ,
170
+ targetX,
171
+ targetY,
172
+ targetZ,
173
+ enableTransition,
174
+ )
175
+ this.floorShift.set(0, 0, 0)
176
+ this.polarShift = 0
177
+ this.heldMinY = positionY
178
+ this.heldFloorMinY = this.floorMinY
179
+ return done
180
+ }
181
+
182
+ override update(delta: number): boolean {
183
+ // The base constructor runs one update before this class's fields exist.
184
+ if (this.floorShift !== undefined) {
185
+ this.frame += 1
186
+ this.frameDelta = delta
187
+ }
188
+ const updated = super.update(delta)
189
+ // The base constructor runs one update before this class's fields exist.
190
+ if (this.floorShift !== undefined && this.keepAboveFloor() && !updated) {
191
+ super.dispatchEvent(this.floorUpdateEvent)
192
+ return true
193
+ }
194
+ return updated
195
+ }
196
+
197
+ override dispatchEvent(event: Parameters<CameraControlsImpl['dispatchEvent']>[0]) {
198
+ // Base update listeners run before update returns; publish the drawn pose.
199
+ if (event.type === 'update' && this.floorShift !== undefined) this.keepAboveFloor()
200
+ super.dispatchEvent(event)
201
+ }
202
+
203
+ private keepAboveFloor() {
204
+ const floorMinY = 'isOrthographicCamera' in this._camera ? null : this.floorMinY
205
+ if (floorMinY === null) {
206
+ // Floor off: what is drawn becomes the orbit, so turning it back on
207
+ // never jumps.
208
+ this.floorShift.set(0, 0, 0)
209
+ this.polarShift = 0
210
+ this.heldMinY = null
211
+ this.forgetFloorSurface()
212
+ return false
213
+ }
214
+
215
+ const levelY = floorMinY - ORBIT_FLOOR_CLEARANCE
216
+ if (levelY !== this.surfaceLevelY) {
217
+ this.forgetFloorSurface()
218
+ this.surfaceLevelY = levelY
219
+ }
220
+ const knownLimitY = this.surfaceMinY ?? floorMinY
221
+ const endCameraY =
222
+ this._targetEnd.y -
223
+ this.floorShift.y +
224
+ this._sphericalEnd.radius * Math.cos(this._sphericalEnd.phi - this.polarShift)
225
+ if (this.heldFloorMinY !== floorMinY || endCameraY >= knownLimitY) {
226
+ this.heldMinY = null
227
+ this.heldFloorMinY = floorMinY
228
+ }
229
+ // The orbit never aims below the level's floor: the pivot's destination is
230
+ // lifted, so a transition toward it glides rather than jumps.
231
+ const pivotEndY = this._targetEnd.y - this.floorShift.y
232
+ if (this.heldMinY === null && pivotEndY < levelY) {
233
+ this._targetEnd.y += levelY - pivotEndY
234
+ this._needsUpdate = true
235
+ }
236
+
237
+ const { radius, theta } = this._spherical
238
+ const orbitPolar = this._spherical.phi - this.polarShift
239
+ virtualTarget.copy(this._target).sub(this.floorShift)
240
+ virtualPosition.setFromSphericalCoords(radius, orbitPolar, theta).add(virtualTarget)
241
+ // Where the camera is headed at the limit known so far; then the floor
242
+ // under that spot, in the same pass, so no motion skips the lookup.
243
+ let minY = this.heldLimit(knownLimitY)
244
+ let drawnPolar = orbitCameraAboveFloor(groundedPosition, virtualPosition, virtualTarget, minY)
245
+ const limitY = this.resolveFloorLimit(groundedPosition, levelY)
246
+ if (limitY !== knownLimitY) {
247
+ minY = this.heldLimit(limitY)
248
+ drawnPolar = orbitCameraAboveFloor(groundedPosition, virtualPosition, virtualTarget, minY)
249
+ }
250
+ // The drawn target sits the orbit's distance out along the drawn view.
251
+ drawnTarget.setFromSphericalCoords(radius, drawnPolar, theta).negate().add(groundedPosition)
252
+ shiftDelta.subVectors(drawnTarget, this._target)
253
+ const polarDelta = drawnPolar - orbitPolar - this.polarShift
254
+ if (shiftDelta.lengthSq() < 1e-12 && Math.abs(polarDelta) < 1e-9) return false
255
+
256
+ this._target.add(shiftDelta)
257
+ this._targetEnd.add(shiftDelta)
258
+ this._spherical.phi += polarDelta
259
+ this._sphericalEnd.phi += polarDelta
260
+ this.floorShift.add(shiftDelta)
261
+ this.polarShift += polarDelta
262
+ this._camera.position.copy(groundedPosition)
263
+ this._camera.lookAt(this._target)
264
+ this._needsUpdate = true
265
+ return true
266
+ }
267
+
268
+ /**
269
+ * The user has taken over: a written pose under the clearance stops being
270
+ * held, and the limit eases up from where the camera is rather than popping.
271
+ */
272
+ releaseWrittenPose() {
273
+ if (this.heldMinY === null) return
274
+ this.heldMinY = null
275
+ const y = this._camera.position.y
276
+ this.surfaceMinY = Math.min(this.surfaceMinY ?? y, y)
277
+ }
278
+
279
+ // A written pose may sit under the clearance, but not under the floor itself.
280
+ private heldLimit(limitY: number) {
281
+ if (this.heldMinY === null) return limitY
282
+ return Math.min(limitY, Math.max(this.heldMinY, limitY - ORBIT_FLOOR_CLEARANCE))
283
+ }
284
+
285
+ private forgetFloorSurface() {
286
+ this.surfaceMinY = null
287
+ this.surfaceTop = null
288
+ this.surfaceQuery.revision = -1
289
+ }
290
+
291
+ /**
292
+ * The floor limit at `position`: the clearance above the floor surface under
293
+ * it, eased once per frame as the floor height changes. Over a hole, or
294
+ * while a slab is rebuilt, the last floor found on this level holds; with
295
+ * none found yet, the level's own floor does. Far above the floor nothing
296
+ * is cast and the floor is forgotten.
297
+ */
298
+ private resolveFloorLimit(position: Vector3, levelY: number) {
299
+ const surface = this.floorSurface
300
+ if (!surface || position.y > levelY + ORBIT_FLOOR_CLEARANCE + SURFACE_BAND) {
301
+ this.forgetFloorSurface()
302
+ return levelY + ORBIT_FLOOR_CLEARANCE
303
+ }
304
+
305
+ const query = this.surfaceQuery
306
+ const revision = surface.revision
307
+ if (position.x !== query.x || position.z !== query.z || revision !== query.revision) {
308
+ query.x = position.x
309
+ query.z = position.z
310
+ query.revision = revision
311
+ query.top = surface.topAt(position.x, position.z, levelY)
312
+ if (query.top !== null) this.surfaceTop = query.top
313
+ }
314
+ const floorTop = this.surfaceTop ?? levelY
315
+ const step = this.glideFrame === this.frame ? 0 : this.frameDelta
316
+ this.glideFrame = this.frame
317
+ const limit = Math.max(
318
+ glideFloorLimit(this.surfaceMinY, floorLimitAt(floorTop, levelY), step),
319
+ floorTop + SURFACE_MIN_GAP,
320
+ )
321
+ this.surfaceMinY = limit
322
+ return limit
323
+ }
324
+ }
@@ -0,0 +1,243 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ type FloorOpeningNode,
7
+ resolveCeilingHeight,
8
+ sceneRegistry,
9
+ useScene,
10
+ } from '@pascal-app/core'
11
+ import { useViewer } from '@pascal-app/viewer'
12
+ import { type ThreeEvent, useFrame } from '@react-three/fiber'
13
+ import { useEffect, useMemo, useRef } from 'react'
14
+ import {
15
+ BufferGeometry,
16
+ DoubleSide,
17
+ Float32BufferAttribute,
18
+ type Group,
19
+ ShapeUtils,
20
+ Vector2,
21
+ } from 'three'
22
+ import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
23
+ import { useCeilingEditSession } from '../../lib/ceiling-edit-session'
24
+ import { EDITOR_LAYER } from '../../lib/constants'
25
+ import { openingRoom, roomSurfaceOpenings, useFloorEditSession } from '../../lib/floor-edit-session'
26
+ import { useOpeningDraft } from '../../lib/floor-opening-draft'
27
+ import { roomFloorElevation } from '../../lib/room-handle-drag'
28
+ import { PolygonEditor } from '../tools/shared/polygon-editor'
29
+
30
+ // The openings of the room being edited ("Edit floor", "Edit ceiling", or a
31
+ // "Cut opening" in progress), outlined where they cut: amber, a light fill to
32
+ // click them by, and the selected one's corner handles. Openings are intent
33
+ // nodes with no mesh of their own — the surfaces they cut show the void.
34
+
35
+ const COLOR = '#f59e0b'
36
+ const LIFT = 0.015
37
+
38
+ type Nodes = Readonly<Record<string, AnyNode>>
39
+
40
+ /** Where an opening is drawn: its room's floor, or the ceiling it was drawn on. */
41
+ export function openingElevation(nodes: Nodes, opening: FloorOpeningNode): number {
42
+ if (opening.hostZoneId) return roomFloorElevation(nodes, opening.hostZoneId)
43
+ const zoneId = openingRoom(nodes, opening)
44
+ if (opening.drawnOn === 'ceiling') {
45
+ const ceiling = Object.values(nodes).find(
46
+ (node) =>
47
+ node.type === 'ceiling' && node.parentId === opening.parentId && node.zoneId === zoneId,
48
+ )
49
+ return ceiling?.type === 'ceiling'
50
+ ? resolveCeilingHeight(ceiling, nodes as Record<AnyNodeId, AnyNode>)
51
+ : 2.5
52
+ }
53
+ return zoneId ? roomFloorElevation(nodes, zoneId) : 0.05
54
+ }
55
+
56
+ function useShownOpenings(): { levelId: string | null; openings: FloorOpeningNode[] } {
57
+ const nodes = useScene((s) => s.nodes)
58
+ const floorSession = useFloorEditSession((s) => s.session)
59
+ const ceilingSession = useCeilingEditSession((s) => s.session)
60
+ const draftHost = useOpeningDraft((s) => s.host)
61
+ const selectedId = useViewer((s) =>
62
+ s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : null,
63
+ )
64
+ return useMemo(() => {
65
+ const shown = new Map<string, FloorOpeningNode>()
66
+ const add = (list: FloorOpeningNode[]) => {
67
+ for (const opening of list) shown.set(opening.id, opening)
68
+ }
69
+ let levelId: string | null = null
70
+ if (floorSession) {
71
+ add(roomSurfaceOpenings(nodes, floorSession.zoneId, 'floor'))
72
+ levelId = floorSession.levelId
73
+ }
74
+ if (ceilingSession?.zoneId) {
75
+ add(roomSurfaceOpenings(nodes, ceilingSession.zoneId, 'ceiling'))
76
+ levelId = ceilingSession.levelId
77
+ }
78
+ if (draftHost) {
79
+ add(roomSurfaceOpenings(nodes, draftHost.zoneId, draftHost.drawnOn))
80
+ levelId = draftHost.levelId
81
+ }
82
+ const selected = selectedId ? nodes[selectedId as AnyNodeId] : undefined
83
+ if (selected?.type === 'floor-opening') {
84
+ shown.set(selected.id, selected)
85
+ levelId = selected.parentId
86
+ }
87
+ return {
88
+ levelId,
89
+ openings: [...shown.values()].filter((opening) => opening.parentId === levelId),
90
+ }
91
+ }, [nodes, floorSession, ceilingSession, draftHost, selectedId])
92
+ }
93
+
94
+ function outlineGeometry(polygon: FloorOpeningNode['polygon'], y: number) {
95
+ const positions: number[] = []
96
+ polygon.forEach((a, i) => {
97
+ const b = polygon[(i + 1) % polygon.length]!
98
+ positions.push(a[0], y, a[1], b[0], y, b[1])
99
+ })
100
+ const geometry = new BufferGeometry()
101
+ geometry.setAttribute('position', new Float32BufferAttribute(positions, 3))
102
+ return geometry
103
+ }
104
+
105
+ function fillGeometry(polygon: FloorOpeningNode['polygon'], y: number) {
106
+ const contour = polygon.map(([x, z]) => new Vector2(x, z))
107
+ const positions: number[] = []
108
+ for (const triangle of ShapeUtils.triangulateShape(contour, []))
109
+ for (const index of triangle) positions.push(contour[index]!.x, y, contour[index]!.y)
110
+ const geometry = new BufferGeometry()
111
+ geometry.setAttribute('position', new Float32BufferAttribute(positions, 3))
112
+ return geometry
113
+ }
114
+
115
+ function OpeningOutline({
116
+ opening,
117
+ elevation,
118
+ selected,
119
+ materials,
120
+ }: {
121
+ opening: FloorOpeningNode
122
+ elevation: number
123
+ selected: boolean
124
+ materials: {
125
+ line: LineBasicNodeMaterial
126
+ fill: MeshBasicNodeMaterial
127
+ fillSelected: MeshBasicNodeMaterial
128
+ }
129
+ }) {
130
+ const geometry = useMemo(
131
+ () => ({
132
+ line: outlineGeometry(opening.polygon, elevation + LIFT),
133
+ fill: fillGeometry(opening.polygon, elevation + LIFT),
134
+ }),
135
+ [opening.polygon, elevation],
136
+ )
137
+ useEffect(
138
+ () => () => {
139
+ geometry.line.dispose()
140
+ geometry.fill.dispose()
141
+ },
142
+ [geometry],
143
+ )
144
+ const select = (event: ThreeEvent<MouseEvent>) => {
145
+ event.stopPropagation()
146
+ useViewer.getState().setSelection({ selectedIds: [opening.id] })
147
+ }
148
+ return (
149
+ <group data-floor-opening={opening.id}>
150
+ <lineSegments
151
+ geometry={geometry.line}
152
+ layers={EDITOR_LAYER}
153
+ material={materials.line}
154
+ raycast={() => {}}
155
+ renderOrder={1001}
156
+ />
157
+ <mesh
158
+ geometry={geometry.fill}
159
+ layers={EDITOR_LAYER}
160
+ material={selected ? materials.fillSelected : materials.fill}
161
+ onClick={select}
162
+ renderOrder={1000}
163
+ />
164
+ </group>
165
+ )
166
+ }
167
+
168
+ export function FloorOpeningsOverlay3D() {
169
+ const { levelId, openings } = useShownOpenings()
170
+ const nodes = useScene((s) => s.nodes)
171
+ const readOnly = useScene((s) => s.readOnly)
172
+ const selectedId = useViewer((s) =>
173
+ s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : null,
174
+ )
175
+ const root = useRef<Group>(null)
176
+ const materials = useMemo(
177
+ () => ({
178
+ line: new LineBasicNodeMaterial({ color: COLOR, depthTest: false, depthWrite: false }),
179
+ fill: new MeshBasicNodeMaterial({
180
+ color: COLOR,
181
+ depthTest: false,
182
+ depthWrite: false,
183
+ opacity: 0.12,
184
+ side: DoubleSide,
185
+ transparent: true,
186
+ }),
187
+ fillSelected: new MeshBasicNodeMaterial({
188
+ color: COLOR,
189
+ depthTest: false,
190
+ depthWrite: false,
191
+ opacity: 0.28,
192
+ side: DoubleSide,
193
+ transparent: true,
194
+ }),
195
+ }),
196
+ [],
197
+ )
198
+ useEffect(
199
+ () => () => {
200
+ materials.line.dispose()
201
+ materials.fill.dispose()
202
+ materials.fillSelected.dispose()
203
+ },
204
+ [materials],
205
+ )
206
+ useFrame(() => {
207
+ const level = levelId ? sceneRegistry.nodes.get(levelId) : null
208
+ if (!root.current) return
209
+ root.current.visible = !!level
210
+ if (level) {
211
+ level.updateWorldMatrix(true, false)
212
+ root.current.matrix.copy(level.matrixWorld)
213
+ }
214
+ })
215
+ if (!levelId || openings.length === 0) return null
216
+ const selected = openings.find((opening) => opening.id === selectedId) ?? null
217
+ return (
218
+ <>
219
+ <group matrixAutoUpdate={false} ref={root}>
220
+ {openings.map((opening) => (
221
+ <OpeningOutline
222
+ elevation={openingElevation(nodes, opening)}
223
+ key={opening.id}
224
+ materials={materials}
225
+ opening={opening}
226
+ selected={opening.id === selectedId}
227
+ />
228
+ ))}
229
+ </group>
230
+ {selected && !readOnly && selected.source !== 'stair' && selected.source !== 'elevator' && (
231
+ <PolygonEditor
232
+ allowEdgeMove
233
+ color={COLOR}
234
+ levelId={levelId}
235
+ minVertices={3}
236
+ onPolygonChange={(polygon) => useScene.getState().updateNode(selected.id, { polygon })}
237
+ polygon={selected.polygon}
238
+ surfaceHeight={openingElevation(nodes, selected)}
239
+ />
240
+ )}
241
+ </>
242
+ )
243
+ }