@pascal-app/editor 0.9.1 → 1.0.0-beta.1

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 (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -1,195 +1,178 @@
1
1
  'use client'
2
2
 
3
- import type {
4
- AnyNode,
5
- CeilingNode,
6
- ColumnNode,
7
- DoorNode,
8
- ElevatorNode,
9
- ItemNode,
10
- Point2D,
11
- RoofNode,
12
- RoofSegmentNode,
13
- SlabNode,
14
- StairNode,
15
- StairSegmentNode,
16
- WallNode,
17
- WindowNode,
18
- } from '@pascal-app/core'
19
- import { useCallback } from 'react'
20
- import {
21
- getFloorplanHitNodeId,
22
- getFloorplanSelectionIdsInBounds,
23
- } from '../../lib/floorplan/selection-tool'
3
+ import { type RefObject, useCallback } from 'react'
24
4
  import type { FloorplanSelectionBounds } from '../../lib/floorplan/types'
5
+ import {
6
+ type Point2 as MarqueePoint2,
7
+ polygonsIntersect,
8
+ segmentIntersectsPolygon,
9
+ } from '../tools/select/marquee-geometry'
10
+ import { collectSelectableCandidateIds } from '../tools/select/select-candidates'
25
11
  import type { WallPlanPoint } from '../tools/wall/wall-drafting'
26
12
 
27
- type OpeningNode = WindowNode | DoorNode
13
+ const REGISTRY_ENTRY_SELECTOR = '.floorplan-registry-base .floorplan-registry-entry[data-node-id]'
14
+ const REGISTRY_GEOMETRY_SELECTOR = 'path, polygon, polyline, rect, circle, line, image'
15
+ const POINT_HIT_EPSILON = 1e-4
28
16
 
29
- type WallPolygonEntry = {
30
- wall: WallNode
31
- polygon: Point2D[]
17
+ type FloorplanHitTestingOptions = {
18
+ sceneRef: RefObject<SVGGElement | null>
32
19
  }
33
20
 
34
- type OpeningPolygonEntry = {
35
- opening: OpeningNode
36
- polygon: Point2D[]
21
+ function boundsPolygon(bounds: FloorplanSelectionBounds): MarqueePoint2[] {
22
+ return [
23
+ [bounds.minX, bounds.minY],
24
+ [bounds.maxX, bounds.minY],
25
+ [bounds.maxX, bounds.maxY],
26
+ [bounds.minX, bounds.maxY],
27
+ ]
37
28
  }
38
29
 
39
- type SlabPolygonEntry = {
40
- slab: SlabNode
41
- polygon: Point2D[]
42
- holes: Point2D[][]
43
- }
30
+ function transformElementBoxToScene(
31
+ element: SVGGraphicsElement,
32
+ scene: SVGGElement,
33
+ ): MarqueePoint2[] | null {
34
+ const elementMatrix = element.getScreenCTM()
35
+ const sceneMatrix = scene.getScreenCTM()
36
+ const svg = scene.ownerSVGElement
37
+ if (!(elementMatrix && sceneMatrix && svg)) {
38
+ return null
39
+ }
44
40
 
45
- type CeilingPolygonEntry = {
46
- ceiling: CeilingNode
47
- polygon: Point2D[]
48
- holes: Point2D[][]
49
- }
41
+ let box: DOMRect
42
+ try {
43
+ box = element.getBBox()
44
+ } catch {
45
+ return null
46
+ }
50
47
 
51
- type ColumnPolygonEntry = {
52
- column: ColumnNode
53
- polygon: Point2D[]
48
+ const inverseSceneMatrix = sceneMatrix.inverse()
49
+ const corners: MarqueePoint2[] = [
50
+ [box.x, box.y],
51
+ [box.x + box.width, box.y],
52
+ [box.x + box.width, box.y + box.height],
53
+ [box.x, box.y + box.height],
54
+ ]
55
+ return corners.map(([x, y]) => {
56
+ const point = svg.createSVGPoint()
57
+ point.x = x
58
+ point.y = y
59
+ const screenPoint = point.matrixTransform(elementMatrix)
60
+ const scenePoint = screenPoint.matrixTransform(inverseSceneMatrix)
61
+ return [scenePoint.x, scenePoint.y] as MarqueePoint2
62
+ })
54
63
  }
55
64
 
56
- type ElevatorPolygonEntry = {
57
- elevator: ElevatorNode
58
- polygon: Point2D[]
59
- }
65
+ function elementIntersectsPlanPolygon(
66
+ element: SVGGraphicsElement,
67
+ scene: SVGGElement,
68
+ selectionPolygon: MarqueePoint2[],
69
+ ): boolean {
70
+ const polygon = transformElementBoxToScene(element, scene)
71
+ if (!polygon) {
72
+ return false
73
+ }
60
74
 
61
- type FloorplanRoofEntry = {
62
- roof: RoofNode
63
- segments: Array<{
64
- polygon: Point2D[]
65
- segment: RoofSegmentNode
66
- }>
67
- }
75
+ const [first, second, third] = polygon
76
+ if (!(first && second && third)) {
77
+ return false
78
+ }
68
79
 
69
- type FloorplanItemEntry = {
70
- item: ItemNode
71
- polygon: Point2D[]
72
- }
80
+ const width = Math.hypot(second[0] - first[0], second[1] - first[1])
81
+ const height = Math.hypot(third[0] - second[0], third[1] - second[1])
82
+ if (width <= POINT_HIT_EPSILON || height <= POINT_HIT_EPSILON) {
83
+ const end = width >= height ? second : third
84
+ return segmentIntersectsPolygon(first, end, selectionPolygon)
85
+ }
73
86
 
74
- type FloorplanStairSegmentEntry = {
75
- polygon: Point2D[]
76
- segment: StairSegmentNode | AnyNode
87
+ return polygonsIntersect(polygon, selectionPolygon)
77
88
  }
78
89
 
79
- type FloorplanStairEntry = {
80
- hitPolygons: Point2D[][]
81
- stair: StairNode
82
- segments: FloorplanStairSegmentEntry[]
90
+ export function collectRegistrySelectionIdsInBounds(
91
+ scene: SVGGElement,
92
+ bounds: FloorplanSelectionBounds,
93
+ candidateIds = collectSelectableCandidateIds(),
94
+ ): string[] {
95
+ const candidateIdSet = new Set(candidateIds)
96
+ const selectionPolygon = boundsPolygon(bounds)
97
+ const hitIds = new Set<string>()
98
+
99
+ for (const entry of scene.querySelectorAll<SVGGElement>(REGISTRY_ENTRY_SELECTOR)) {
100
+ const nodeId = entry.dataset.nodeId
101
+ if (!nodeId || !candidateIdSet.has(nodeId)) {
102
+ continue
103
+ }
104
+
105
+ const geometry = entry.querySelectorAll<SVGGraphicsElement>(REGISTRY_GEOMETRY_SELECTOR)
106
+ const elements = geometry.length > 0 ? Array.from(geometry) : [entry]
107
+ if (
108
+ elements.some((element) => elementIntersectsPlanPolygon(element, scene, selectionPolygon))
109
+ ) {
110
+ hitIds.add(nodeId)
111
+ }
112
+ }
113
+
114
+ return candidateIds.filter((id) => hitIds.has(id))
83
115
  }
84
116
 
85
- type UseFloorplanHitTestingArgs = {
86
- ceilingPolygons: CeilingPolygonEntry[]
87
- columnPolygons: ColumnPolygonEntry[]
88
- displaySlabPolygons: SlabPolygonEntry[]
89
- displayWallPolygons: WallPolygonEntry[]
90
- floorplanElevatorEntries: ElevatorPolygonEntry[]
91
- floorplanItemEntries: FloorplanItemEntry[]
92
- floorplanOpeningHitTolerance: number
93
- floorplanRoofEntries: FloorplanRoofEntry[]
94
- floorplanStairEntries: FloorplanStairEntry[]
95
- floorplanWallHitTolerance: number
96
- getOpeningCenterLine: (polygon: Point2D[]) => { start: Point2D; end: Point2D } | null
97
- isFloorplanItemContextActive: boolean
98
- openingsPolygons: OpeningPolygonEntry[]
99
- phase: 'site' | 'structure' | 'furnish'
100
- toPoint2D: (point: WallPlanPoint) => Point2D
117
+ export function getRegistryHitIdAtPlanPoint(
118
+ scene: SVGGElement,
119
+ planPoint: WallPlanPoint,
120
+ candidateIds = collectSelectableCandidateIds(),
121
+ ): string | null {
122
+ const sceneMatrix = scene.getScreenCTM()
123
+ const svg = scene.ownerSVGElement
124
+ if (!(sceneMatrix && svg)) {
125
+ return null
126
+ }
127
+
128
+ const point = svg.createSVGPoint()
129
+ point.x = planPoint[0]
130
+ point.y = planPoint[1]
131
+ const screenPoint = point.matrixTransform(sceneMatrix)
132
+ const candidateIdSet = new Set(candidateIds)
133
+
134
+ if (typeof document !== 'undefined' && typeof document.elementsFromPoint === 'function') {
135
+ for (const element of document.elementsFromPoint(screenPoint.x, screenPoint.y)) {
136
+ const entry = element.closest<SVGGElement>('.floorplan-registry-entry[data-node-id]')
137
+ const nodeId = entry?.dataset.nodeId
138
+ if (entry && nodeId && candidateIdSet.has(nodeId) && scene.contains(entry)) {
139
+ return nodeId
140
+ }
141
+ }
142
+ }
143
+
144
+ const hits = collectRegistrySelectionIdsInBounds(
145
+ scene,
146
+ {
147
+ minX: planPoint[0] - POINT_HIT_EPSILON,
148
+ maxX: planPoint[0] + POINT_HIT_EPSILON,
149
+ minY: planPoint[1] - POINT_HIT_EPSILON,
150
+ maxY: planPoint[1] + POINT_HIT_EPSILON,
151
+ },
152
+ candidateIds,
153
+ )
154
+ return hits.at(-1) ?? null
101
155
  }
102
156
 
103
- export function useFloorplanHitTesting({
104
- ceilingPolygons,
105
- columnPolygons,
106
- displaySlabPolygons,
107
- displayWallPolygons,
108
- floorplanElevatorEntries,
109
- floorplanItemEntries,
110
- floorplanOpeningHitTolerance,
111
- floorplanRoofEntries,
112
- floorplanStairEntries,
113
- floorplanWallHitTolerance,
114
- getOpeningCenterLine,
115
- isFloorplanItemContextActive,
116
- openingsPolygons,
117
- phase,
118
- toPoint2D,
119
- }: UseFloorplanHitTestingArgs) {
157
+ export function useFloorplanHitTesting({ sceneRef }: FloorplanHitTestingOptions) {
120
158
  const getFloorplanHitIdAtPoint = useCallback(
121
159
  (planPoint: WallPlanPoint) => {
122
- const point = toPoint2D(planPoint)
123
- return getFloorplanHitNodeId({
124
- point,
125
- ceilings: ceilingPolygons,
126
- phase,
127
- isItemContextActive: isFloorplanItemContextActive,
128
- items: floorplanItemEntries,
129
- openings: openingsPolygons,
130
- roofs: floorplanRoofEntries,
131
- stairs: floorplanStairEntries,
132
- elevators: floorplanElevatorEntries,
133
- walls: displayWallPolygons,
134
- slabs: displaySlabPolygons,
135
- openingHitTolerance: floorplanOpeningHitTolerance,
136
- wallHitTolerance: floorplanWallHitTolerance,
137
- columns: columnPolygons,
138
- getOpeningCenterLine,
139
- })
160
+ const scene = sceneRef.current
161
+ return scene ? getRegistryHitIdAtPlanPoint(scene, planPoint) : null
140
162
  },
141
- [
142
- ceilingPolygons,
143
- columnPolygons,
144
- displaySlabPolygons,
145
- displayWallPolygons,
146
- floorplanItemEntries,
147
- floorplanElevatorEntries,
148
- floorplanOpeningHitTolerance,
149
- floorplanRoofEntries,
150
- floorplanStairEntries,
151
- floorplanWallHitTolerance,
152
- getOpeningCenterLine,
153
- isFloorplanItemContextActive,
154
- openingsPolygons,
155
- phase,
156
- toPoint2D,
157
- ],
163
+ [sceneRef],
158
164
  )
159
165
 
160
- const getFloorplanSelectionIdsInBoundsForArea = useCallback(
161
- (bounds: FloorplanSelectionBounds) =>
162
- getFloorplanSelectionIdsInBounds({
163
- bounds,
164
- ceilings: ceilingPolygons,
165
- phase,
166
- isItemContextActive: isFloorplanItemContextActive,
167
- items: floorplanItemEntries,
168
- walls: displayWallPolygons,
169
- openings: openingsPolygons,
170
- roofs: floorplanRoofEntries,
171
- slabs: displaySlabPolygons,
172
- columns: columnPolygons,
173
- elevators: floorplanElevatorEntries,
174
- stairs: floorplanStairEntries,
175
- }),
176
- [
177
- ceilingPolygons,
178
- columnPolygons,
179
- displaySlabPolygons,
180
- displayWallPolygons,
181
- floorplanItemEntries,
182
- floorplanElevatorEntries,
183
- floorplanRoofEntries,
184
- floorplanStairEntries,
185
- isFloorplanItemContextActive,
186
- openingsPolygons,
187
- phase,
188
- ],
166
+ const getFloorplanSelectionIdsInBounds = useCallback(
167
+ (bounds: FloorplanSelectionBounds) => {
168
+ const scene = sceneRef.current
169
+ return scene ? collectRegistrySelectionIdsInBounds(scene, bounds) : []
170
+ },
171
+ [sceneRef],
189
172
  )
190
173
 
191
174
  return {
192
175
  getFloorplanHitIdAtPoint,
193
- getFloorplanSelectionIdsInBounds: getFloorplanSelectionIdsInBoundsForArea,
176
+ getFloorplanSelectionIdsInBounds,
194
177
  }
195
178
  }
@@ -36,7 +36,7 @@ function useLevelChildren<TNode extends AnyNode>(
36
36
  }
37
37
 
38
38
  const levelNode = state.nodes[levelId]
39
- if (!levelNode || levelNode.type !== 'level') {
39
+ if (levelNode?.type !== 'level') {
40
40
  return [] as TNode[]
41
41
  }
42
42
 
@@ -105,7 +105,7 @@ export function useFloorplanSceneData({
105
105
  }
106
106
 
107
107
  const buildingNode = state.nodes[currentBuildingId]
108
- if (!buildingNode || buildingNode.type !== 'building') {
108
+ if (buildingNode?.type !== 'building') {
109
109
  return [] as LevelNode[]
110
110
  }
111
111
 
@@ -133,7 +133,7 @@ export function useFloorplanSceneData({
133
133
  }
134
134
 
135
135
  const nextLevelNode = state.nodes[levelId]
136
- if (!nextLevelNode || nextLevelNode.type !== 'level') {
136
+ if (nextLevelNode?.type !== 'level') {
137
137
  return [] as RoofNode[]
138
138
  }
139
139
 
@@ -149,7 +149,7 @@ export function useFloorplanSceneData({
149
149
  }
150
150
 
151
151
  const nextLevelNode = state.nodes[levelId]
152
- if (!nextLevelNode || nextLevelNode.type !== 'level') {
152
+ if (nextLevelNode?.type !== 'level') {
153
153
  return [] as OpeningNode[]
154
154
  }
155
155
 
@@ -171,7 +171,7 @@ export function useFloorplanSceneData({
171
171
  }
172
172
 
173
173
  const nextLevelNode = state.nodes[levelId]
174
- if (!nextLevelNode || nextLevelNode.type !== 'level') {
174
+ if (nextLevelNode?.type !== 'level') {
175
175
  return [] as AnyNode[]
176
176
  }
177
177
 
@@ -0,0 +1,27 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ /**
4
+ * Bumps after the 3D meshes have had a chance to settle following a scene
5
+ * change. `computeGroupBox` reads mesh WORLD bounds, but a scene commit
6
+ * (undo/redo included) reaches the meshes asynchronously — renderers
7
+ * reconcile on the next React commit and the geometry systems rebuild in the
8
+ * next frame — so anything that derives geometry from the meshes in the same
9
+ * render as the `nodes` change reads STALE positions. Depend on this epoch
10
+ * (alongside `nodes`) to recompute once more after two animation frames,
11
+ * when transforms and rebuilt geometry are in place.
12
+ */
13
+ export function useMeshSettleEpoch(nodes: unknown): number {
14
+ const [epoch, setEpoch] = useState(0)
15
+ useEffect(() => {
16
+ void nodes
17
+ let raf2 = 0
18
+ const raf1 = requestAnimationFrame(() => {
19
+ raf2 = requestAnimationFrame(() => setEpoch((e) => e + 1))
20
+ })
21
+ return () => {
22
+ cancelAnimationFrame(raf1)
23
+ cancelAnimationFrame(raf2)
24
+ }
25
+ }, [nodes])
26
+ return epoch
27
+ }
@@ -1,10 +1,11 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
+ type AnyNode,
4
5
  type AnyNodeId,
5
6
  calculateLevelMiters,
6
- DEFAULT_WALL_HEIGHT,
7
7
  getWallCurveLength,
8
+ getWallEffectiveHeightForNodes,
8
9
  getWallMiterBoundaryPoints,
9
10
  getWallPlanFootprint,
10
11
  getWallSurfacePolygon,
@@ -23,6 +24,7 @@ import { Html } from '@react-three/drei'
23
24
  import { createPortal, useFrame } from '@react-three/fiber'
24
25
  import { useMemo, useState } from 'react'
25
26
  import * as THREE from 'three'
27
+ import { formatLinearMeasurement } from '../../lib/measurements'
26
28
 
27
29
  const GUIDE_Y_OFFSET = 0.08
28
30
  const LABEL_LIFT = 0.08
@@ -62,17 +64,6 @@ type WallFaceLine = {
62
64
  end: Point2D
63
65
  }
64
66
 
65
- function formatMeasurement(value: number, unit: 'metric' | 'imperial') {
66
- if (unit === 'imperial') {
67
- const feet = value * 3.280_84
68
- const wholeFeet = Math.floor(feet)
69
- const inches = Math.round((feet - wholeFeet) * 12)
70
- if (inches === 12) return `${wholeFeet + 1}'0"`
71
- return `${wholeFeet}'${inches}"`
72
- }
73
- return `${Number.parseFloat(value.toFixed(2))}m`
74
- }
75
-
76
67
  export function WallMeasurementLabel() {
77
68
  const selectedIds = useViewer((state) => state.selection.selectedIds)
78
69
  const nodes = useScene((state) => state.nodes)
@@ -101,10 +92,7 @@ export function WallMeasurementLabel() {
101
92
  return createPortal(<SelectedMeasurementAnnotation node={measurableNode} />, selectedObject)
102
93
  }
103
94
 
104
- function getLevelWalls(
105
- wall: WallNode,
106
- nodes: Record<string, WallNode | { type: string; children?: string[] }>,
107
- ): WallNode[] {
95
+ function getLevelWalls(wall: WallNode, nodes: Record<string, AnyNode>): WallNode[] {
108
96
  if (!wall.parentId) return [wall]
109
97
 
110
98
  const levelNode = nodes[wall.parentId as AnyNodeId]
@@ -318,7 +306,7 @@ function getCurvedWallMeasurementPath(
318
306
 
319
307
  function buildMeasurementGuide(
320
308
  wall: WallNode,
321
- nodes: Record<string, WallNode | { type: string; children?: string[] }>,
309
+ nodes: Record<string, AnyNode>,
322
310
  ): MeasurementGuide | null {
323
311
  const levelWalls = getLevelWalls(wall, nodes)
324
312
  const miterData = calculateLevelMiters(levelWalls)
@@ -327,7 +315,7 @@ function buildMeasurementGuide(
327
315
  const measurementPoints = measurementLine ?? fallbackMiddlePoints
328
316
  if (!measurementPoints) return null
329
317
 
330
- const height = wall.height ?? DEFAULT_WALL_HEIGHT
318
+ const height = getWallEffectiveHeightForNodes(wall, nodes)
331
319
  const startLocal = worldPointToWallLocal(wall, measurementPoints.start)
332
320
  const endLocal = worldPointToWallLocal(wall, measurementPoints.end)
333
321
  const curvedMeasurementPath = isCurvedWall(wall)
@@ -526,14 +514,7 @@ function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
526
514
  const color = isNight ? '#ffffff' : '#111111'
527
515
  const shadowColor = isNight ? '#111111' : '#ffffff'
528
516
 
529
- const guide = useMemo(
530
- () =>
531
- buildMeasurementGuide(
532
- wall,
533
- nodes as Record<string, WallNode | { type: string; children?: string[] }>,
534
- ),
535
- [nodes, wall],
536
- )
517
+ const guide = useMemo(() => buildMeasurementGuide(wall, nodes), [nodes, wall])
537
518
  const length = useMemo(() => {
538
519
  if (!guide?.guidePath?.length || guide.guidePath.length < 2) {
539
520
  return getWallCurveLength(wall)
@@ -547,8 +528,9 @@ function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
547
528
  }
548
529
  return total
549
530
  }, [guide, wall])
550
- const label = formatMeasurement(length, unit)
551
- const heightLabel = `H ${formatMeasurement(wall.height ?? DEFAULT_WALL_HEIGHT, unit)}`
531
+ const label = formatLinearMeasurement(length, unit)
532
+ const height = useMemo(() => getWallEffectiveHeightForNodes(wall, nodes), [nodes, wall])
533
+ const heightLabel = `H ${formatLinearMeasurement(height, unit)}`
552
534
 
553
535
  if (!(guide && Number.isFinite(length) && length >= 0.01)) return null
554
536