@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,95 @@
1
+ // The one place that answers "where is the ground under this pointer ray?".
2
+ //
3
+ // Before terrain there was no question to answer: the ground was `y = 0` and
4
+ // every consumer intersected its own horizontal plane. Terrain makes those
5
+ // planes wrong in a way that is invisible on flat ground and badly wrong on a
6
+ // slope — a ray intersected at the datum instead of at the surface reports an XZ
7
+ // that slides along the ray, further the more oblique the camera. Item placement
8
+ // already repairs this locally (`resolvePointerSupportSurface`); this module
9
+ // exists so the repair is shared instead of copied into column, wall, fence, and
10
+ // the sculpt tool.
11
+ //
12
+ // It lives in `editor` rather than `core` because the decision needs one piece of
13
+ // editor knowledge — which plane a consumer is asking about — while the geometry
14
+ // it delegates to (`raycastTerrain`, `heightAt`) is pure core.
15
+
16
+ import {
17
+ heightAt,
18
+ isSiteDatum,
19
+ raycastTerrain,
20
+ type SiteNode,
21
+ type TerrainField,
22
+ type TerrainHit,
23
+ terrainFieldOf,
24
+ useScene,
25
+ } from '@pascal-app/core'
26
+
27
+ /** The active site's terrain field, or null when the scene has no sculpted ground. */
28
+ export function siteTerrainField(): TerrainField | null {
29
+ const { nodes, rootNodeIds } = useScene.getState()
30
+ const siteId = rootNodeIds[0]
31
+ const site = siteId ? nodes[siteId] : null
32
+ if (site?.type !== 'site') return null
33
+ return terrainFieldOf(site as SiteNode)
34
+ }
35
+
36
+ /**
37
+ * True when a horizontal query plane at `planeY` is the *site ground*, as opposed
38
+ * to a built flat surface that happens to be horizontal.
39
+ *
40
+ * This is the whole scoping rule, and getting it wrong in either direction is
41
+ * worse than not having terrain at all:
42
+ *
43
+ * - Treat every horizontal plane as ground and a second-storey wall draft snaps
44
+ * to the hillside under the building.
45
+ * - Treat none of them as ground and every ground tool keeps the perspective skew
46
+ * terrain was supposed to remove.
47
+ *
48
+ * The datum is exactly the right discriminator because terrain *is* the surface at
49
+ * the datum: the site renderer removes the flat ground fill when terrain mounts,
50
+ * so at the datum there is nothing else to hit. Every other horizontal plane —
51
+ * a storey base, a slab top, a shelf — is a real flat surface built by the user,
52
+ * and a plane is the correct model for it.
53
+ *
54
+ * `isSiteDatum` comes from core so this rule and the one the support resolver
55
+ * applies (`terrain-support.ts`) cannot drift into disagreeing about which
56
+ * surfaces are ground.
57
+ */
58
+ export function isSiteGroundPlane(planeY: number): boolean {
59
+ return isSiteDatum(planeY)
60
+ }
61
+
62
+ /**
63
+ * Where a pointer ray meets the sculpted ground, or null when the flat-plane path
64
+ * should be used instead — no terrain, the ray misses the field, or the query
65
+ * plane is a built surface rather than the ground.
66
+ *
67
+ * Returning null rather than a fallback hit is deliberate: the caller already owns
68
+ * a correct plane intersection for its own frame (world for grid events,
69
+ * level-local for support election), and duplicating that here would mean two
70
+ * definitions of the fallback to keep in sync.
71
+ */
72
+ export function resolveTerrainGroundHit(
73
+ origin: readonly [number, number, number],
74
+ direction: readonly [number, number, number],
75
+ planeY: number,
76
+ ): TerrainHit | null {
77
+ if (!isSiteGroundPlane(planeY)) return null
78
+ const field = siteTerrainField()
79
+ if (!field) return null
80
+ return raycastTerrain(field, origin, direction)
81
+ }
82
+
83
+ /**
84
+ * Ground height at a world XZ, for consumers with no ray to march.
85
+ *
86
+ * The 2D plan view is the case this exists for: an orthographic top-down camera
87
+ * makes the pointer ray vertical, so the march degenerates to a single sample and
88
+ * there is no XZ skew to correct — only a Y to report.
89
+ */
90
+ export function groundHeightAt(x: number, z: number, planeY: number): number | null {
91
+ if (!isSiteGroundPlane(planeY)) return null
92
+ const field = siteTerrainField()
93
+ if (!field) return null
94
+ return heightAt(field, x, z)
95
+ }
@@ -0,0 +1,96 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeId,
5
+ BuildingNode,
6
+ clearSceneHistory,
7
+ LevelNode,
8
+ useScene,
9
+ } from '@pascal-app/core'
10
+ import { installHistoryCommandDelegate, runRedo, runUndo } from './history'
11
+
12
+ type RafFn = (cb: (time: number) => void) => number
13
+ ;(globalThis as unknown as { requestAnimationFrame?: RafFn }).requestAnimationFrame ??= (cb) => {
14
+ cb(0)
15
+ return 0
16
+ }
17
+ ;(globalThis as unknown as { cancelAnimationFrame?: (id: number) => void }).cancelAnimationFrame ??=
18
+ () => {}
19
+
20
+ const BUILDING_ID = 'building_history_controller' as AnyNodeId
21
+ const LEVEL_ID = 'level_history_controller' as AnyNodeId
22
+ let disposeController = () => {}
23
+
24
+ function levelNumber(): number {
25
+ return (useScene.getState().nodes[LEVEL_ID] as { level: number }).level
26
+ }
27
+
28
+ describe('editor history controller', () => {
29
+ beforeEach(() => {
30
+ disposeController()
31
+ disposeController = () => {}
32
+ const level = LevelNode.parse({
33
+ id: LEVEL_ID,
34
+ parentId: BUILDING_ID,
35
+ children: [],
36
+ level: 0,
37
+ })
38
+ const building = BuildingNode.parse({
39
+ id: BUILDING_ID,
40
+ parentId: null,
41
+ children: [LEVEL_ID],
42
+ })
43
+ useScene.setState({
44
+ nodes: { [BUILDING_ID]: building, [LEVEL_ID]: level },
45
+ rootNodeIds: [BUILDING_ID],
46
+ dirtyNodes: new Set<AnyNodeId>(),
47
+ collections: {},
48
+ materials: {},
49
+ readOnly: false,
50
+ } as never)
51
+ clearSceneHistory()
52
+ useScene.getState().updateNode(LEVEL_ID, { level: 1 } as Partial<AnyNode>)
53
+ })
54
+
55
+ afterEach(() => {
56
+ disposeController()
57
+ disposeController = () => {}
58
+ })
59
+
60
+ test('delegates undo and redo while a host delegate is installed', () => {
61
+ const undo = mock(() => {})
62
+ const redo = mock(() => {})
63
+ disposeController = installHistoryCommandDelegate({ undo, redo })
64
+
65
+ runUndo()
66
+ runRedo()
67
+
68
+ expect(undo).toHaveBeenCalledTimes(1)
69
+ expect(redo).toHaveBeenCalledTimes(1)
70
+ expect(levelNumber()).toBe(1)
71
+ expect(useScene.temporal.getState().pastStates).toHaveLength(1)
72
+ })
73
+
74
+ test('falls back to standalone Zundo undo and redo when no controller is installed', () => {
75
+ runUndo()
76
+ expect(levelNumber()).toBe(0)
77
+ expect(useScene.temporal.getState().futureStates).toHaveLength(1)
78
+
79
+ runRedo()
80
+ expect(levelNumber()).toBe(1)
81
+ expect(useScene.temporal.getState().pastStates).toHaveLength(1)
82
+ })
83
+
84
+ test('an older cleanup cannot uninstall a newer controller', () => {
85
+ const firstUndo = mock(() => {})
86
+ const stopFirst = installHistoryCommandDelegate({ undo: firstUndo, redo: () => {} })
87
+ const secondUndo = mock(() => {})
88
+ disposeController = installHistoryCommandDelegate({ undo: secondUndo, redo: () => {} })
89
+
90
+ stopFirst()
91
+ runUndo()
92
+
93
+ expect(firstUndo).toHaveBeenCalledTimes(0)
94
+ expect(secondUndo).toHaveBeenCalledTimes(1)
95
+ })
96
+ })
@@ -1,5 +1,19 @@
1
1
  import { useLiveNodeOverrides, useLiveTransforms, useScene } from '@pascal-app/core'
2
2
 
3
+ export type HistoryCommandDelegate = {
4
+ undo: () => void
5
+ redo: () => void
6
+ }
7
+
8
+ let historyCommandDelegate: HistoryCommandDelegate | null = null
9
+
10
+ export function installHistoryCommandDelegate(delegate: HistoryCommandDelegate): () => void {
11
+ historyCommandDelegate = delegate
12
+ return () => {
13
+ if (historyCommandDelegate === delegate) historyCommandDelegate = null
14
+ }
15
+ }
16
+
3
17
  function refreshSceneAfterHistoryJump() {
4
18
  useLiveNodeOverrides.getState().clearAll()
5
19
  useLiveTransforms.getState().clearAll()
@@ -11,11 +25,28 @@ function refreshSceneAfterHistoryJump() {
11
25
  }
12
26
 
13
27
  export function runUndo() {
28
+ if (historyCommandDelegate) {
29
+ historyCommandDelegate.undo()
30
+ return
31
+ }
14
32
  useScene.temporal.getState().undo()
15
33
  refreshSceneAfterHistoryJump()
16
34
  }
17
35
 
18
36
  export function runRedo() {
37
+ if (historyCommandDelegate) {
38
+ historyCommandDelegate.redo()
39
+ return
40
+ }
19
41
  useScene.temporal.getState().redo()
20
42
  refreshSceneAfterHistoryJump()
21
43
  }
44
+
45
+ /**
46
+ * ⌘Z / ⌘⇧Z (undo/redo). Pointer-drag sessions intercept these in the capture
47
+ * phase and cancel the gesture instead — mid-drag, "undo" means "abort what my
48
+ * mouse is doing", never a history jump under a live pointer.
49
+ */
50
+ export function isHistoryShortcut(e: KeyboardEvent) {
51
+ return (e.metaKey || e.ctrlKey) && (e.key === 'z' || e.key === 'Z')
52
+ }
@@ -0,0 +1,133 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import type { AnyNode } from '@pascal-app/core'
3
+ import {
4
+ type AttachClass,
5
+ attachClassOf,
6
+ type HotSetCandidate,
7
+ isCandidateInHotSet,
8
+ isPickableForAttach,
9
+ } from './hot-set'
10
+
11
+ const mockNode = (id: string, type: string): AnyNode => ({ id, type }) as unknown as AnyNode
12
+
13
+ const floor: HotSetCandidate = {
14
+ type: 'level',
15
+ isFloorLike: true,
16
+ exposesTop: false,
17
+ attachClass: 'surface',
18
+ }
19
+ const wall: HotSetCandidate = {
20
+ type: 'wall',
21
+ isFloorLike: false,
22
+ exposesTop: false,
23
+ attachClass: 'surface',
24
+ }
25
+ const ceiling: HotSetCandidate = {
26
+ type: 'ceiling',
27
+ isFloorLike: false,
28
+ exposesTop: false,
29
+ attachClass: 'surface',
30
+ }
31
+ const table: HotSetCandidate = {
32
+ type: 'item',
33
+ isFloorLike: false,
34
+ exposesTop: true,
35
+ attachClass: 'surface',
36
+ }
37
+ const wallShelf: HotSetCandidate = {
38
+ type: 'shelf',
39
+ isFloorLike: false,
40
+ exposesTop: true,
41
+ attachClass: 'wall',
42
+ }
43
+ const ceilingFan: HotSetCandidate = {
44
+ type: 'item',
45
+ isFloorLike: false,
46
+ exposesTop: true,
47
+ attachClass: 'ceiling',
48
+ }
49
+
50
+ describe('attachClassOf', () => {
51
+ test('wall and wall-side collapse to wall', () => {
52
+ expect(attachClassOf('wall')).toBe('wall')
53
+ expect(attachClassOf('wall-side')).toBe('wall')
54
+ })
55
+ test('ceiling maps to ceiling', () => {
56
+ expect(attachClassOf('ceiling')).toBe('ceiling')
57
+ })
58
+ test('undefined/null/unknown is surface-resting', () => {
59
+ expect(attachClassOf(undefined)).toBe('surface')
60
+ expect(attachClassOf(null)).toBe('surface')
61
+ expect(attachClassOf('')).toBe('surface')
62
+ })
63
+ })
64
+
65
+ describe('isPickableForAttach — wall-mounted (window)', () => {
66
+ test('only walls are eligible; floor/ceiling/tops are not', () => {
67
+ expect(isPickableForAttach('wall', wall)).toBe(true)
68
+ expect(isPickableForAttach('wall', floor)).toBe(false)
69
+ expect(isPickableForAttach('wall', ceiling)).toBe(false)
70
+ expect(isPickableForAttach('wall', table)).toBe(false)
71
+ expect(isPickableForAttach('wall', wallShelf)).toBe(false)
72
+ })
73
+ })
74
+
75
+ describe('isPickableForAttach — ceiling-mounted', () => {
76
+ test('only ceilings are eligible', () => {
77
+ expect(isPickableForAttach('ceiling', ceiling)).toBe(true)
78
+ expect(isPickableForAttach('ceiling', wall)).toBe(false)
79
+ expect(isPickableForAttach('ceiling', floor)).toBe(false)
80
+ })
81
+ })
82
+
83
+ describe('isPickableForAttach — surface-resting (sofa / cactus)', () => {
84
+ test('floor is always eligible', () => {
85
+ expect(isPickableForAttach('surface', floor)).toBe(true)
86
+ })
87
+ test('host tops (table, wall-shelf top) are eligible', () => {
88
+ expect(isPickableForAttach('surface', table)).toBe(true)
89
+ expect(isPickableForAttach('surface', wallShelf)).toBe(true)
90
+ })
91
+ test('a wall (no top surface) is not eligible', () => {
92
+ expect(isPickableForAttach('surface', wall)).toBe(false)
93
+ })
94
+ test('a ceiling-mounted host (ceiling fan) is never eligible — Track E', () => {
95
+ expect(isPickableForAttach('surface', ceilingFan)).toBe(false)
96
+ })
97
+ })
98
+
99
+ describe('isCandidateInHotSet — by scope', () => {
100
+ const surfaceClass: AttachClass = 'surface'
101
+ test('idle: everything is in the hot-set (selection filtering lives elsewhere)', () => {
102
+ expect(isCandidateInHotSet({ kind: 'idle' }, null, ceilingFan)).toBe(true)
103
+ })
104
+ test('placing a surface item: derives from attach class', () => {
105
+ const scope = {
106
+ kind: 'placing' as const,
107
+ node: mockNode('i1', 'item'),
108
+ nodeId: 'i1',
109
+ nodeType: 'item',
110
+ view: '3d' as const,
111
+ pressDrag: false,
112
+ }
113
+ expect(isCandidateInHotSet(scope, surfaceClass, floor)).toBe(true)
114
+ expect(isCandidateInHotSet(scope, surfaceClass, ceilingFan)).toBe(false)
115
+ })
116
+ test('moving a wall-mounted item: only walls', () => {
117
+ const scope = {
118
+ kind: 'moving' as const,
119
+ node: mockNode('w1', 'window'),
120
+ nodeId: 'w1',
121
+ nodeType: 'window',
122
+ view: '2d' as const,
123
+ }
124
+ expect(isCandidateInHotSet(scope, 'wall', wall)).toBe(true)
125
+ expect(isCandidateInHotSet(scope, 'wall', table)).toBe(false)
126
+ })
127
+ test('non-placement active scopes target nothing in the scene', () => {
128
+ expect(isCandidateInHotSet({ kind: 'box-select' }, null, floor)).toBe(false)
129
+ expect(
130
+ isCandidateInHotSet({ kind: 'handle-drag', nodeId: 'x', handle: 'h' }, null, floor),
131
+ ).toBe(false)
132
+ })
133
+ })
@@ -0,0 +1,67 @@
1
+ // The hot-set: which scene objects are raycast-eligible during an interaction.
2
+ //
3
+ // It is never hand-authored per interaction. It falls out of the node's
4
+ // `asset.attachTo` plus whether a candidate exposes a top surface. "Floor item"
5
+ // really means surface-resting: it rests on the floor *or* any host's top
6
+ // surface. Walls and ceilings are the special attach modes. Adding a node kind
7
+ // = set `attachTo` (or leave blank); the hot-set follows with zero per-kind
8
+ // wiring.
9
+
10
+ import type { InteractionScope } from './scope'
11
+
12
+ // What a node attaches to, collapsed to the three classes the hot-set cares
13
+ // about. `wall-side` is a wall attachment; everything without an explicit
14
+ // `attachTo` is surface-resting.
15
+ export type AttachClass = 'wall' | 'ceiling' | 'surface'
16
+
17
+ export function attachClassOf(attachTo: string | undefined | null): AttachClass {
18
+ if (attachTo === 'wall' || attachTo === 'wall-side') return 'wall'
19
+ if (attachTo === 'ceiling') return 'ceiling'
20
+ return 'surface'
21
+ }
22
+
23
+ // The metadata the hot-set needs about a candidate host/surface. Derived from
24
+ // the candidate node + its registry definition by the caller, so this module
25
+ // stays pure and unit-testable without the scene or registry.
26
+ export type HotSetCandidate = {
27
+ type: string
28
+ // The level floor plane / ground a surface-resting node can always rest on.
29
+ isFloorLike: boolean
30
+ // The candidate exposes a usable top surface (registry
31
+ // `capabilities.surfaces.top`) — a table, a shelf, a slab.
32
+ exposesTop: boolean
33
+ // The candidate's own attach class. A ceiling fan is `ceiling`: it hangs from
34
+ // the ceiling and must never act as a host top (Track E).
35
+ attachClass: AttachClass
36
+ }
37
+
38
+ // For a node whose attach class is `placed`, is `candidate` a valid
39
+ // host/surface to pick during placement or move?
40
+ export function isPickableForAttach(placed: AttachClass, candidate: HotSetCandidate): boolean {
41
+ if (placed === 'wall') return candidate.type === 'wall'
42
+ if (placed === 'ceiling') return candidate.type === 'ceiling'
43
+ // Surface-resting: the floor, or any host that exposes a top surface — but
44
+ // never a ceiling-mounted host (a floor lamp must not land on a ceiling fan).
45
+ if (candidate.isFloorLike) return true
46
+ if (!candidate.exposesTop) return false
47
+ if (candidate.attachClass === 'ceiling') return false
48
+ return true
49
+ }
50
+
51
+ // The hot-set predicate for a whole scope. For placing/moving it derives from
52
+ // the moving node's attach class; for every other active scope nothing in the
53
+ // scene is a placement target, so the body's own raycast owns the pointer.
54
+ // `idle` returns true here — selection/phase filtering stays in the selection
55
+ // manager; this only narrows what an *active* interaction can target.
56
+ export function isCandidateInHotSet(
57
+ scope: InteractionScope,
58
+ placedAttachClass: AttachClass | null,
59
+ candidate: HotSetCandidate,
60
+ ): boolean {
61
+ if (scope.kind === 'idle') return true
62
+ if (scope.kind === 'placing' || scope.kind === 'moving') {
63
+ if (placedAttachClass === null) return true
64
+ return isPickableForAttach(placedAttachClass, candidate)
65
+ }
66
+ return false
67
+ }
@@ -0,0 +1,52 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import type { AnyNode } from '@pascal-app/core'
3
+ import { resolveOverlayPolicy } from './overlay-policy'
4
+ import type { ActiveInteractionScope } from './scope'
5
+
6
+ const mockNode = (id: string, type: string): AnyNode => ({ id, type }) as unknown as AnyNode
7
+
8
+ const ACTIVE_SCOPES: ActiveInteractionScope[] = [
9
+ {
10
+ kind: 'placing',
11
+ node: mockNode('i1', 'item'),
12
+ nodeId: 'i1',
13
+ nodeType: 'item',
14
+ view: '3d',
15
+ pressDrag: false,
16
+ },
17
+ { kind: 'moving', node: mockNode('i1', 'item'), nodeId: 'i1', nodeType: 'item', view: '2d' },
18
+ { kind: 'handle-drag', nodeId: 'w1', handle: 'height' },
19
+ { kind: 'drafting', tool: 'wall' },
20
+ { kind: 'reshaping', nodeId: 's1', reshape: 'hole', driver: 'tool', holeIndex: 0 },
21
+ { kind: 'box-select' },
22
+ { kind: 'painting' },
23
+ { kind: 'sculpting' },
24
+ ]
25
+
26
+ describe('resolveOverlayPolicy', () => {
27
+ test('idle keeps everything shown and pickable', () => {
28
+ const p = resolveOverlayPolicy({ kind: 'idle' })
29
+ expect(p.zoneLabels).toBe('shown')
30
+ expect(p.contextBadges).toBe('shown')
31
+ expect(p.conflictingControls).toBe('shown')
32
+ expect(p.sceneObjectsPickable).toBe(true)
33
+ })
34
+
35
+ test('every active scope hides zone labels, fades badges, hides conflicting controls', () => {
36
+ for (const scope of ACTIVE_SCOPES) {
37
+ const p = resolveOverlayPolicy(scope)
38
+ expect(p.zoneLabels).toBe('hidden')
39
+ expect(p.contextBadges).toBe('faded')
40
+ expect(p.conflictingControls).toBe('hidden')
41
+ expect(p.sceneObjectsPickable).toBe(false)
42
+ }
43
+ })
44
+
45
+ test('active affordances and the contextual HUD always stay interactive', () => {
46
+ for (const scope of [{ kind: 'idle' } as const, ...ACTIVE_SCOPES]) {
47
+ const p = resolveOverlayPolicy(scope)
48
+ expect(p.activeAffordances).toBe('shown')
49
+ expect(p.contextualHudInteractive).toBe(true)
50
+ }
51
+ })
52
+ })
@@ -0,0 +1,59 @@
1
+ // The overlay scope matrix — the "Sims-light" feel. During any non-idle
2
+ // interaction, two layers behave differently:
3
+ //
4
+ // - 3D scene objects stay VISIBLE but become NON-pickable (the hot-set owns
5
+ // what the active interaction can target). Context is preserved; you just
6
+ // can't grab the wrong thing.
7
+ // - DOM/HUD overlays step back, differentiated by how distracting they are:
8
+ // zone labels -> hidden (not a primary editing concern)
9
+ // context badges -> faded + pointer-events:none (hover name pills)
10
+ // other controls -> hard-hidden (other objects' handles, the floating
11
+ // action menu, conflicting controls)
12
+ //
13
+ // The active interaction's own affordances (ghost, snap guides, dimension
14
+ // labels, the active handle) always stay — "default-off, opt-in for the active
15
+ // action". The contextual control HUD is exempt from the pointer-events
16
+ // step-back because it *is* the active interaction's own controls.
17
+
18
+ import { type InteractionScope, isActive } from './scope'
19
+
20
+ export type OverlayVisibility = 'shown' | 'faded' | 'hidden'
21
+
22
+ export type OverlayPolicy = {
23
+ zoneLabels: OverlayVisibility
24
+ // Hover name pills / context badges.
25
+ contextBadges: OverlayVisibility
26
+ // Other objects' handles + the floating action menu — anything whose action
27
+ // would conflict with the active interaction.
28
+ conflictingControls: OverlayVisibility
29
+ // Non-active scene objects: visible always, pickable only when idle.
30
+ sceneObjectsPickable: boolean
31
+ // The active interaction's own ghost/guides/dimension labels/handle. Always
32
+ // shown; this field exists so consumers can assert the contract.
33
+ activeAffordances: 'shown'
34
+ // The contextual control HUD keeps pointer events even while everything else
35
+ // steps back, because it is the active interaction's own controls.
36
+ contextualHudInteractive: boolean
37
+ }
38
+
39
+ const IDLE_POLICY: OverlayPolicy = {
40
+ zoneLabels: 'shown',
41
+ contextBadges: 'shown',
42
+ conflictingControls: 'shown',
43
+ sceneObjectsPickable: true,
44
+ activeAffordances: 'shown',
45
+ contextualHudInteractive: true,
46
+ }
47
+
48
+ const ACTIVE_POLICY: OverlayPolicy = {
49
+ zoneLabels: 'hidden',
50
+ contextBadges: 'faded',
51
+ conflictingControls: 'hidden',
52
+ sceneObjectsPickable: false,
53
+ activeAffordances: 'shown',
54
+ contextualHudInteractive: true,
55
+ }
56
+
57
+ export function resolveOverlayPolicy(scope: InteractionScope): OverlayPolicy {
58
+ return isActive(scope) ? ACTIVE_POLICY : IDLE_POLICY
59
+ }