@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
@@ -0,0 +1,206 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import type { AnyNode } from '@pascal-app/core'
3
+ import {
4
+ type ActiveInteractionScope,
5
+ curveReshapeScope,
6
+ editingHoleInfo,
7
+ handleDragInfo,
8
+ isActive,
9
+ isIdle,
10
+ isToolDrivenReshape,
11
+ scopeNodeId,
12
+ selectionEnabled,
13
+ } from '../lib/interaction/scope'
14
+ import useInteractionScope from './use-interaction-scope'
15
+
16
+ // A placing/moving scope carries the node inline. Tests only assert on id/type,
17
+ // so a structural stand-in is enough.
18
+ const mockNode = (id: string, type: string): AnyNode => ({ id, type }) as unknown as AnyNode
19
+
20
+ function reset() {
21
+ useInteractionScope.getState().end()
22
+ }
23
+ afterEach(reset)
24
+
25
+ describe('use-interaction-scope state machine', () => {
26
+ test('starts idle', () => {
27
+ expect(useInteractionScope.getState().scope.kind).toBe('idle')
28
+ expect(isIdle(useInteractionScope.getState().scope)).toBe(true)
29
+ })
30
+
31
+ test('begin enters an interaction; end returns to idle atomically', () => {
32
+ const s = useInteractionScope.getState()
33
+ s.begin({
34
+ kind: 'moving',
35
+ node: mockNode('item_1', 'item'),
36
+ nodeId: 'item_1',
37
+ nodeType: 'item',
38
+ view: '3d',
39
+ })
40
+ expect(useInteractionScope.getState().scope).toEqual({
41
+ kind: 'moving',
42
+ node: mockNode('item_1', 'item'),
43
+ nodeId: 'item_1',
44
+ nodeType: 'item',
45
+ view: '3d',
46
+ })
47
+ s.end()
48
+ // No interaction payload leaks past end — the scope is plain idle, so a
49
+ // stale nodeId/handle is unrepresentable.
50
+ expect(useInteractionScope.getState().scope).toEqual({ kind: 'idle' })
51
+ expect(scopeNodeId(useInteractionScope.getState().scope)).toBeNull()
52
+ })
53
+
54
+ test('begin is single-owner: a new interaction replaces the prior one', () => {
55
+ const s = useInteractionScope.getState()
56
+ s.begin({ kind: 'drafting', tool: 'wall' })
57
+ s.begin({ kind: 'handle-drag', nodeId: 'wall_1', handle: 'height' })
58
+ const scope = useInteractionScope.getState().scope
59
+ expect(scope.kind).toBe('handle-drag')
60
+ // The prior drafting payload is gone — illegal "drafting + handle-drag"
61
+ // combination is unrepresentable.
62
+ expect(scopeNodeId(scope)).toBe('wall_1')
63
+ })
64
+
65
+ test('update patches the live payload of the active scope', () => {
66
+ const s = useInteractionScope.getState()
67
+ s.begin({
68
+ kind: 'placing',
69
+ node: mockNode('i1', 'item'),
70
+ nodeId: 'i1',
71
+ nodeType: 'item',
72
+ view: '3d',
73
+ pressDrag: false,
74
+ })
75
+ s.update({ pressDrag: true })
76
+ const scope = useInteractionScope.getState().scope
77
+ expect(scope.kind === 'placing' && scope.pressDrag).toBe(true)
78
+ })
79
+
80
+ test('update is a no-op when idle', () => {
81
+ useInteractionScope.getState().update({
82
+ kind: 'moving',
83
+ node: mockNode('x', 'item'),
84
+ nodeId: 'x',
85
+ nodeType: 'item',
86
+ view: '3d',
87
+ })
88
+ expect(useInteractionScope.getState().scope.kind).toBe('idle')
89
+ })
90
+
91
+ test('update cannot change which interaction is running', () => {
92
+ const s = useInteractionScope.getState()
93
+ s.begin({
94
+ kind: 'moving',
95
+ node: mockNode('i1', 'item'),
96
+ nodeId: 'i1',
97
+ nodeType: 'item',
98
+ view: '3d',
99
+ })
100
+ s.update({
101
+ kind: 'placing',
102
+ node: mockNode('i1', 'item'),
103
+ nodeId: 'i1',
104
+ nodeType: 'item',
105
+ view: '3d',
106
+ pressDrag: true,
107
+ })
108
+ expect(useInteractionScope.getState().scope.kind).toBe('moving')
109
+ })
110
+
111
+ test('selectionEnabled only while idle', () => {
112
+ const s = useInteractionScope.getState()
113
+ expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(true)
114
+ s.begin({ kind: 'box-select' })
115
+ expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(false)
116
+ expect(isActive(useInteractionScope.getState().scope)).toBe(true)
117
+ })
118
+
119
+ test('end is idempotent', () => {
120
+ const s = useInteractionScope.getState()
121
+ s.end()
122
+ s.end()
123
+ expect(useInteractionScope.getState().scope.kind).toBe('idle')
124
+ })
125
+ })
126
+
127
+ describe('derived flag views are leak-free (no parallel flags)', () => {
128
+ const scope = () => useInteractionScope.getState().scope
129
+
130
+ test('handleDragInfo mirrors handle-drag and clears on end', () => {
131
+ const s = useInteractionScope.getState()
132
+ s.begin({ kind: 'handle-drag', nodeId: 'wall_1', handle: 'height' })
133
+ expect(handleDragInfo(scope())).toEqual({ nodeId: 'wall_1', label: 'height' })
134
+ s.end()
135
+ // After end the derived view is null — a stale activeHandleDrag is
136
+ // unrepresentable because it is a pure function of the single scope.
137
+ expect(handleDragInfo(scope())).toBeNull()
138
+ })
139
+
140
+ test('editingHoleInfo mirrors a hole reshape and clears on end', () => {
141
+ const s = useInteractionScope.getState()
142
+ s.begin({
143
+ kind: 'reshaping',
144
+ nodeId: 'slab_1',
145
+ reshape: 'hole',
146
+ driver: 'tool',
147
+ holeIndex: 2,
148
+ })
149
+ expect(editingHoleInfo(scope())).toEqual({ nodeId: 'slab_1', holeIndex: 2 })
150
+ s.end()
151
+ expect(editingHoleInfo(scope())).toBeNull()
152
+ })
153
+
154
+ test('a non-hole reshape never reads as an editing hole', () => {
155
+ const s = useInteractionScope.getState()
156
+ s.begin({ kind: 'reshaping', nodeId: 'wall_1', reshape: 'curve', driver: 'tool' })
157
+ expect(editingHoleInfo(scope())).toBeNull()
158
+ })
159
+
160
+ test('a floorplan-owned curve drag does not activate the registry reshape tool', () => {
161
+ expect(isToolDrivenReshape(curveReshapeScope('wall_1', 'floorplan'))).toBe(false)
162
+ expect(isToolDrivenReshape(curveReshapeScope('wall_1', 'tool'))).toBe(true)
163
+ })
164
+
165
+ test('switching interactions never leaks the prior derived view', () => {
166
+ const s = useInteractionScope.getState()
167
+ s.begin({ kind: 'handle-drag', nodeId: 'wall_1', handle: 'height' })
168
+ // Single-owner replacement: the handle-drag view must vanish the instant a
169
+ // different interaction begins — the two cannot be simultaneously active.
170
+ s.begin({
171
+ kind: 'reshaping',
172
+ nodeId: 'slab_1',
173
+ reshape: 'hole',
174
+ driver: 'tool',
175
+ holeIndex: 0,
176
+ })
177
+ expect(handleDragInfo(scope())).toBeNull()
178
+ expect(editingHoleInfo(scope())).toEqual({ nodeId: 'slab_1', holeIndex: 0 })
179
+ })
180
+
181
+ test('every active scope kind leaves at most the views it owns', () => {
182
+ const s = useInteractionScope.getState()
183
+ const kinds: ActiveInteractionScope[] = [
184
+ {
185
+ kind: 'placing',
186
+ node: mockNode('i', 'item'),
187
+ nodeId: 'i',
188
+ nodeType: 'item',
189
+ view: '3d',
190
+ pressDrag: false,
191
+ },
192
+ { kind: 'moving', node: mockNode('i', 'item'), nodeId: 'i', nodeType: 'item', view: '3d' },
193
+ { kind: 'drafting', tool: 'wall' },
194
+ { kind: 'box-select' },
195
+ { kind: 'painting' },
196
+ { kind: 'sculpting' },
197
+ ]
198
+ for (const k of kinds) {
199
+ s.begin(k)
200
+ // None of these own a handle-drag or hole view.
201
+ expect(handleDragInfo(scope())).toBeNull()
202
+ expect(editingHoleInfo(scope())).toBeNull()
203
+ }
204
+ s.end()
205
+ })
206
+ })
@@ -0,0 +1,140 @@
1
+ 'use client'
2
+
3
+ import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core'
4
+ import { useRef } from 'react'
5
+ import { create } from 'zustand'
6
+ import { useShallow } from 'zustand/react/shallow'
7
+ import {
8
+ type ActiveInteractionScope,
9
+ controlPointReshapeInfo,
10
+ editingHoleInfo,
11
+ endpointReshapeInfo,
12
+ handleDragInfo,
13
+ IDLE_SCOPE,
14
+ type InteractionScope,
15
+ isCurveReshape,
16
+ isFloorplanDrivenReshape,
17
+ isToolDrivenReshape,
18
+ movingNodeOf,
19
+ reshapingNodeId,
20
+ tangentReshapeInfo,
21
+ } from '../lib/interaction/scope'
22
+
23
+ // The authoritative interaction state machine. A single owner holds exactly one
24
+ // scope at a time. `begin` enters an interaction (atomically replacing any prior
25
+ // one — a single owner, no producer races), `update` narrows the live payload,
26
+ // and `end` returns to idle atomically so no interaction payload can leak past
27
+ // the end of its interaction. There is no setter that can leave the store in an
28
+ // illegal half-state: the only writable shape is `InteractionScope`.
29
+
30
+ export type InteractionScopeState = {
31
+ scope: InteractionScope
32
+ // Enter an interaction. If one is already active it is ended first, so the
33
+ // store is always single-owner.
34
+ begin: (scope: ActiveInteractionScope) => void
35
+ // Patch the current scope's payload. Ignored when idle, or when the patch's
36
+ // implied kind differs from the active kind — payload updates must not change
37
+ // which interaction is running (use `begin` for that).
38
+ update: (patch: Partial<ActiveInteractionScope>) => void
39
+ // Return to idle atomically. Both commit and cancel paths call this; the
40
+ // distinction (write vs revert) lives in the interaction body, not here.
41
+ end: () => void
42
+ // Return to idle only if the active scope matches `match`. Used when scope is
43
+ // driven from independent legacy flag clears, so clearing one flag (e.g. a
44
+ // fence curve) cannot stomp an unrelated active scope (e.g. a wall move).
45
+ endIf: (match: (scope: ActiveInteractionScope) => boolean) => void
46
+ }
47
+
48
+ const useInteractionScope = create<InteractionScopeState>((set, get) => ({
49
+ scope: IDLE_SCOPE,
50
+ begin: (scope) => set({ scope }),
51
+ update: (patch) =>
52
+ set((state) => {
53
+ if (state.scope.kind === 'idle') return state
54
+ if ('kind' in patch && patch.kind !== state.scope.kind) return state
55
+ return { scope: { ...state.scope, ...patch } as InteractionScope }
56
+ }),
57
+ end: () => {
58
+ if (get().scope.kind === 'idle') return
59
+ set({ scope: IDLE_SCOPE })
60
+ },
61
+ endIf: (match) => {
62
+ const scope = get().scope
63
+ if (scope.kind === 'idle') return
64
+ if (match(scope)) set({ scope: IDLE_SCOPE })
65
+ },
66
+ }))
67
+
68
+ // Derived, reference-stable views of the active scope, replacing the legacy
69
+ // `useEditor.activeHandleDrag` / `useEditor.editingHole` flags. `useShallow`
70
+ // keeps the result reference-stable across unrelated scope changes, so hot-path
71
+ // subscribers (camera controls, floating menu) don't re-render on every update.
72
+ export const useActiveHandleDrag = (): { nodeId: string; label: string } | null =>
73
+ useInteractionScope(useShallow((s) => handleDragInfo(s.scope)))
74
+
75
+ export const useEditingHole = (): { nodeId: string; holeIndex: number } | null =>
76
+ useInteractionScope(useShallow((s) => editingHoleInfo(s.scope)))
77
+
78
+ // Imperative (non-React) reads for event handlers / effects.
79
+ export const getEditingHole = (): { nodeId: string; holeIndex: number } | null =>
80
+ editingHoleInfo(useInteractionScope.getState().scope)
81
+
82
+ export const getIsCurveReshape = (): boolean => isCurveReshape(useInteractionScope.getState().scope)
83
+
84
+ // Replaces the legacy `curvingWall` / `curvingFence` existence flags. The
85
+ // wall-vs-fence distinction (both now map to one `reshaping/'curve'` scope) is
86
+ // recovered by reading the reshaped node's type from `useReshapingNode`.
87
+ export const useIsCurveReshape = (): boolean => useInteractionScope((s) => isCurveReshape(s.scope))
88
+
89
+ export const useIsToolDrivenReshape = (): boolean =>
90
+ useInteractionScope((s) => isToolDrivenReshape(s.scope))
91
+
92
+ export const useIsFloorplanDrivenReshape = (): boolean =>
93
+ useInteractionScope((s) => isFloorplanDrivenReshape(s.scope))
94
+
95
+ // Replaces the legacy `movingWallEndpoint` / `movingFenceEndpoint` payloads,
96
+ // minus the node (fetch it from `useReshapingNode`).
97
+ export const useEndpointReshape = (): { nodeId: string; endpoint: 'start' | 'end' } | null =>
98
+ useInteractionScope(useShallow((s) => endpointReshapeInfo(s.scope)))
99
+
100
+ export const useControlPointReshape = (): { nodeId: string; index: number } | null =>
101
+ useInteractionScope(useShallow((s) => controlPointReshapeInfo(s.scope)))
102
+
103
+ export const useTangentReshape = (): { nodeId: string; index: number; side: 'in' | 'out' } | null =>
104
+ useInteractionScope(useShallow((s) => tangentReshapeInfo(s.scope)))
105
+
106
+ // The node currently being reshaped (curve / endpoint / hole), looked up live
107
+ // from the scene by the scope's `nodeId`. During a reshape the scene node holds
108
+ // the same data the legacy `curvingWall` / `movingWallEndpoint.wall` carried, so
109
+ // consumers that need the full node (affordance-tool mounts, wall-vs-fence type
110
+ // checks) read it here instead of from a parallel flag.
111
+ export const useReshapingNode = (): AnyNode | null => {
112
+ const nodeId = useInteractionScope((s) => reshapingNodeId(s.scope))
113
+ // Snapshot the node ONCE when the reshape begins (keyed on nodeId), like the
114
+ // legacy `curvingWall` / `movingWallEndpoint.wall` flags did. The affordance
115
+ // tools write the node live during the drag; subscribing to the live scene
116
+ // node would feed those writes straight back into the tool — the curve resets
117
+ // on pointer-stop, the endpoint drag loops and freezes. nodeId is stable for
118
+ // the whole gesture, so a ref snapshot stays frozen until the next reshape.
119
+ const snapshot = useRef<{ id: string | null; node: AnyNode | null }>({ id: null, node: null })
120
+ if (snapshot.current.id !== nodeId) {
121
+ snapshot.current = {
122
+ id: nodeId,
123
+ node: nodeId ? (useScene.getState().nodes[nodeId as AnyNodeId] ?? null) : null,
124
+ }
125
+ }
126
+ return snapshot.current.node
127
+ }
128
+
129
+ // The node currently being placed or moved. Replaces the legacy
130
+ // `useEditor.movingNode` flag. Unlike `useReshapingNode`, no `useRef` snapshot is
131
+ // needed: the node is carried inline in the scope and set once at `begin`, so it
132
+ // is already a stable reference for the whole gesture (nothing calls `begin` mid
133
+ // drag). Returns null whenever no placing/moving interaction is active.
134
+ export const useMovingNode = (): AnyNode | null => useInteractionScope((s) => movingNodeOf(s.scope))
135
+
136
+ // Imperative (non-React) read for event handlers / effects.
137
+ export const getMovingNode = (): AnyNode | null =>
138
+ movingNodeOf(useInteractionScope.getState().scope)
139
+
140
+ export default useInteractionScope