@pascal-app/editor 1.0.0-beta.4 → 1.0.0

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 (283) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +15 -8
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
  6. package/src/components/editor/bake-exporter.tsx +1 -0
  7. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  8. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  9. package/src/components/editor/capture-camera-rig.tsx +59 -0
  10. package/src/components/editor/custom-camera-controls.tsx +20 -11
  11. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  12. package/src/components/editor/editor-layout-v2.tsx +17 -5
  13. package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
  14. package/src/components/editor/export-manager.tsx +175 -33
  15. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  16. package/src/components/editor/first-person-controls.tsx +182 -57
  17. package/src/components/editor/floating-action-menu.tsx +55 -55
  18. package/src/components/editor/floorplan-background-selection.test.ts +31 -4
  19. package/src/components/editor/floorplan-background-selection.ts +27 -7
  20. package/src/components/editor/floorplan-panel.tsx +81 -57
  21. package/src/components/editor/grid.tsx +12 -7
  22. package/src/components/editor/group-actions.ts +34 -50
  23. package/src/components/editor/group-floating-action-menu.tsx +30 -15
  24. package/src/components/editor/group-move-3d.ts +51 -21
  25. package/src/components/editor/group-transform-shared.test.ts +78 -0
  26. package/src/components/editor/group-transform-shared.ts +74 -7
  27. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  28. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  29. package/src/components/editor/handles/linear-resize-drag.ts +67 -0
  30. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  31. package/src/components/editor/handles/resize-snap.ts +10 -1
  32. package/src/components/editor/handles/use-handle-drag.ts +18 -1
  33. package/src/components/editor/index.tsx +252 -46
  34. package/src/components/editor/measurement-pill.tsx +12 -12
  35. package/src/components/editor/node-action-menu.tsx +42 -1
  36. package/src/components/editor/node-arrow-handles.tsx +47 -58
  37. package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
  38. package/src/components/editor/selection-manager.tsx +132 -47
  39. package/src/components/editor/site-edge-labels.tsx +2 -1
  40. package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
  41. package/src/components/editor/snapshot-capture.test.ts +422 -0
  42. package/src/components/editor/snapshot-capture.ts +145 -0
  43. package/src/components/editor/thumbnail-generator.tsx +295 -232
  44. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  45. package/src/components/editor/wall-measurement-label.tsx +3 -2
  46. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  47. package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
  48. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  49. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  50. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  51. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  52. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  53. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  54. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
  57. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  59. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  60. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  61. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  62. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  63. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  64. package/src/components/systems/selection-affordance-services.ts +19 -0
  65. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  66. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  67. package/src/components/tools/fence/fence-drafting.ts +6 -25
  68. package/src/components/tools/item/draft-creation.test.ts +12 -0
  69. package/src/components/tools/item/draft-creation.ts +10 -0
  70. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  71. package/src/components/tools/item/face-host-commit.ts +56 -0
  72. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  73. package/src/components/tools/item/face-host-preview.ts +82 -0
  74. package/src/components/tools/item/move-tool.tsx +4 -3
  75. package/src/components/tools/item/placement-math.test.ts +11 -1
  76. package/src/components/tools/item/placement-math.ts +0 -20
  77. package/src/components/tools/item/placement-strategies.test.ts +494 -0
  78. package/src/components/tools/item/placement-strategies.ts +192 -24
  79. package/src/components/tools/item/placement-surface.test.ts +61 -0
  80. package/src/components/tools/item/placement-surface.ts +24 -0
  81. package/src/components/tools/item/placement-types.ts +6 -0
  82. package/src/components/tools/item/test-face-host.ts +121 -0
  83. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  84. package/src/components/tools/item/use-draft-node.ts +26 -4
  85. package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
  86. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  87. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  88. package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
  89. package/src/components/tools/registry-tool-context.tsx +30 -0
  90. package/src/components/tools/select/box-select-tool.tsx +3 -1
  91. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  92. package/src/components/tools/select/marquee-footprint.ts +29 -0
  93. package/src/components/tools/shared/placement-box.tsx +63 -8
  94. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  95. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  96. package/src/components/tools/shared/pointer-support-cap.ts +45 -8
  97. package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
  98. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  99. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  100. package/src/components/tools/stair/stair-tool.tsx +127 -74
  101. package/src/components/tools/tool-manager.tsx +25 -8
  102. package/src/components/tools/wall/wall-drafting.test.ts +527 -4
  103. package/src/components/tools/wall/wall-drafting.ts +55 -462
  104. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  105. package/src/components/ui/action-menu/index.tsx +11 -1
  106. package/src/components/ui/command-palette/editor-commands.tsx +12 -13
  107. package/src/components/ui/command-palette/index.tsx +7 -2
  108. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  109. package/src/components/ui/controls/material-picker.tsx +4 -4
  110. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  111. package/src/components/ui/controls/segmented-control.tsx +8 -1
  112. package/src/components/ui/controls/slider-control.tsx +19 -3
  113. package/src/components/ui/controls/toggle-control.tsx +18 -5
  114. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  115. package/src/components/ui/floating-level-selector.tsx +23 -6
  116. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  117. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  118. package/src/components/ui/helpers/item-helper.tsx +1 -1
  119. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  120. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  121. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  122. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  123. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  124. package/src/components/ui/panels/multi-field-value.ts +163 -0
  125. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  126. package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
  127. package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
  128. package/src/components/ui/panels/node-display.ts +4 -0
  129. package/src/components/ui/panels/panel-manager.tsx +77 -3
  130. package/src/components/ui/panels/panel-wrapper.tsx +228 -12
  131. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  132. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  133. package/src/components/ui/panels/parametric-inspector.tsx +29 -162
  134. package/src/components/ui/panels/reference-panel.tsx +31 -1
  135. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  136. package/src/components/ui/scene-loader.tsx +35 -0
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
  138. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
  139. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
  140. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
  142. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  143. package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
  144. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  145. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  146. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  147. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
  148. package/src/components/ui/sidebar/tab-bar.tsx +9 -1
  149. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  150. package/src/components/viewer/floorplan-compass-button.tsx +50 -0
  151. package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
  152. package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
  153. package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
  154. package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
  155. package/src/components/viewer/floorplan-preview.tsx +1069 -0
  156. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
  157. package/src/components/viewer/viewer-scene-header.tsx +2 -1
  158. package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
  159. package/src/components/viewer/viewer-stage-modes.ts +49 -0
  160. package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
  161. package/src/components/viewer/viewer-stage.test.tsx +47 -0
  162. package/src/components/viewer/viewer-stage.tsx +221 -0
  163. package/src/components/viewer-overlay.tsx +13 -30
  164. package/src/components/viewer-zone-system.tsx +6 -2
  165. package/src/components/walkthrough-hud.tsx +19 -10
  166. package/src/hooks/use-auto-save.test.ts +122 -1
  167. package/src/hooks/use-auto-save.ts +119 -13
  168. package/src/hooks/use-ceiling-events.test.ts +122 -0
  169. package/src/hooks/use-drag-action.ts +17 -3
  170. package/src/hooks/use-grid-events.test.ts +240 -0
  171. package/src/hooks/use-grid-events.ts +190 -11
  172. package/src/hooks/use-keyboard.test.ts +244 -0
  173. package/src/hooks/use-keyboard.ts +164 -63
  174. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  175. package/src/hooks/use-save-shortcut.ts +33 -0
  176. package/src/index.tsx +109 -4
  177. package/src/lib/active-placement-surface.test.ts +16 -0
  178. package/src/lib/active-placement-surface.ts +14 -0
  179. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  180. package/src/lib/ceiling-surface-raycast.ts +12 -0
  181. package/src/lib/contextual-help-extension.ts +17 -0
  182. package/src/lib/contextual-help.test.ts +8 -0
  183. package/src/lib/contextual-help.ts +8 -0
  184. package/src/lib/continuation.test.ts +11 -0
  185. package/src/lib/continuation.ts +8 -1
  186. package/src/lib/direct-manipulation.test.ts +132 -0
  187. package/src/lib/direct-manipulation.ts +38 -1
  188. package/src/lib/editor-api.ts +6 -3
  189. package/src/lib/elevation-guides.test.ts +28 -1
  190. package/src/lib/elevation-guides.ts +22 -7
  191. package/src/lib/floating-menu-scale.test.ts +29 -0
  192. package/src/lib/floating-menu-scale.ts +14 -0
  193. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  194. package/src/lib/floorplan/apply-alignment.ts +10 -1
  195. package/src/lib/floorplan/floorplan-export.test.ts +397 -1
  196. package/src/lib/floorplan/floorplan-export.tsx +111 -16
  197. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  198. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  199. package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
  200. package/src/lib/floorplan/floorplan-readonly.ts +85 -0
  201. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  202. package/src/lib/floorplan-grid-event-point.ts +13 -0
  203. package/src/lib/glb-export.test.ts +1169 -21
  204. package/src/lib/glb-export.ts +629 -101
  205. package/src/lib/grid-event-presentation.ts +16 -0
  206. package/src/lib/history.test.ts +523 -77
  207. package/src/lib/history.ts +133 -14
  208. package/src/lib/host-tree-children.test.ts +27 -0
  209. package/src/lib/host-tree-children.ts +79 -0
  210. package/src/lib/inspector-card-mode.test.ts +92 -0
  211. package/src/lib/inspector-card-mode.ts +60 -0
  212. package/src/lib/interaction/hot-set.test.ts +10 -1
  213. package/src/lib/interaction/hot-set.ts +3 -1
  214. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  215. package/src/lib/interaction/overlay-policy.ts +16 -0
  216. package/src/lib/interaction/registered-drafting.ts +36 -0
  217. package/src/lib/interaction/scope.ts +31 -0
  218. package/src/lib/level-print-export.test.ts +484 -0
  219. package/src/lib/level-print-export.ts +526 -0
  220. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  221. package/src/lib/local-project-presentation-persistence.ts +335 -0
  222. package/src/lib/material-paint.ts +2 -0
  223. package/src/lib/measurement-parser.ts +3 -0
  224. package/src/lib/measurements.test.ts +4 -0
  225. package/src/lib/model-export.ts +29 -0
  226. package/src/lib/paint-preview-owner.ts +71 -0
  227. package/src/lib/paint-scope.test.ts +58 -2
  228. package/src/lib/paint-scope.ts +8 -2
  229. package/src/lib/planar-cursor-placement.test.ts +178 -1
  230. package/src/lib/planar-cursor-placement.ts +61 -5
  231. package/src/lib/plugin-panels.test.ts +92 -1
  232. package/src/lib/plugin-panels.ts +48 -0
  233. package/src/lib/portable-export.test.ts +366 -0
  234. package/src/lib/portable-export.ts +969 -0
  235. package/src/lib/print-3mf.test.ts +86 -0
  236. package/src/lib/print-3mf.ts +161 -0
  237. package/src/lib/print-content-scope.test.ts +107 -0
  238. package/src/lib/print-content-scope.ts +90 -0
  239. package/src/lib/print-export.test.ts +294 -0
  240. package/src/lib/print-export.ts +735 -0
  241. package/src/lib/print-feature-thickness.test.ts +147 -0
  242. package/src/lib/print-feature-thickness.ts +180 -0
  243. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  244. package/src/lib/print-golden-house.test.ts +249 -0
  245. package/src/lib/print-roof-solids.test.ts +133 -0
  246. package/src/lib/print-roof-solids.ts +630 -0
  247. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  248. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  249. package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
  250. package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
  251. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  252. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  253. package/src/lib/print-shell-compiler-protocol.ts +30 -0
  254. package/src/lib/print-shell-compiler.ts +273 -0
  255. package/src/lib/print-wall-solids.test.ts +194 -0
  256. package/src/lib/print-wall-solids.ts +328 -0
  257. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  258. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  259. package/src/lib/scene.ts +22 -8
  260. package/src/lib/selection-routing.test.ts +125 -6
  261. package/src/lib/selection-routing.ts +34 -1
  262. package/src/lib/session-groups.test.ts +88 -0
  263. package/src/lib/session-groups.ts +201 -0
  264. package/src/lib/sfx-bus.ts +4 -0
  265. package/src/lib/sfx-player.ts +8 -0
  266. package/src/lib/snapping-mode.test.ts +32 -4
  267. package/src/lib/snapping-mode.ts +11 -7
  268. package/src/lib/structured-clone-fallback.test.ts +45 -0
  269. package/src/lib/structured-clone-fallback.ts +24 -0
  270. package/src/lib/usdz-export.ts +38 -0
  271. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  272. package/src/store/live-draft-preview-stores.test.ts +3 -0
  273. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  274. package/src/store/tool-mode.test.ts +87 -0
  275. package/src/store/use-camera-hint-focus.ts +48 -0
  276. package/src/store/use-editor.tsx +299 -188
  277. package/src/store/use-floorplan-draft-preview.ts +9 -0
  278. package/src/store/use-interaction-scope.test.ts +23 -1
  279. package/src/store/use-interaction-scope.ts +63 -2
  280. package/src/store/use-placement-preview.ts +56 -3
  281. package/src/store/use-session-groups.ts +103 -0
  282. package/src/components/tools/roof/roof-tool.tsx +0 -656
  283. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Grab pointer lock on the viewer canvas for a walkthrough (walk / drone)
3
+ * entry. Must run synchronously inside a user-gesture task — callers flip the
4
+ * first-person flags in a `flushSync` first so the controls are mounted when
5
+ * the lock lands.
6
+ *
7
+ * `retryWhile`: the browser's re-lock cooldown (~1.25s after any unlock)
8
+ * rejects the request outright, which bites the natural "free the cursor,
9
+ * immediately pick the other camera" flow. When given, one delayed retry
10
+ * fires after the cooldown — only while the predicate still holds.
11
+ */
12
+ export function requestWalkthroughPointerLock(options?: { retryWhile?: () => boolean }) {
13
+ const canvas = document.querySelector<HTMLCanvasElement>('[data-pascal-viewer-3d] canvas')
14
+ if (!canvas) return
15
+
16
+ if (!canvas.hasAttribute('tabindex')) {
17
+ canvas.tabIndex = -1
18
+ }
19
+ canvas.focus({ preventScroll: true })
20
+
21
+ if (document.pointerLockElement === canvas) return
22
+
23
+ try {
24
+ // The request can also reject ASYNC (browser cooldown after a recent
25
+ // unlock) — swallow it like the P-resume path; clicking the canvas
26
+ // re-requests once the cooldown passes.
27
+ const result = canvas.requestPointerLock?.() as Promise<void> | undefined
28
+ if (result && typeof result.catch === 'function') {
29
+ result.catch(() => {
30
+ const retryWhile = options?.retryWhile
31
+ if (!retryWhile) return
32
+ window.setTimeout(() => {
33
+ if (!retryWhile()) return
34
+ if (document.pointerLockElement === canvas) return
35
+ try {
36
+ const retried = canvas.requestPointerLock?.() as Promise<void> | undefined
37
+ if (retried && typeof retried.catch === 'function') retried.catch(() => {})
38
+ } catch {
39
+ // Best effort — clicking the canvas still locks.
40
+ }
41
+ }, 1400)
42
+ })
43
+ }
44
+ } catch {
45
+ return
46
+ }
47
+ }
@@ -85,6 +85,7 @@ describe('live draft preview stores', () => {
85
85
  {
86
86
  angle: 90,
87
87
  branchAngle: 90,
88
+ damperAngle: 0,
88
89
  diameter: 6,
89
90
  diameter2: 6,
90
91
  ductMaterial: 'sheet-metal' as const,
@@ -94,6 +95,8 @@ describe('live draft preview stores', () => {
94
95
  id: 'duct-fitting_live-draft-0' as const,
95
96
  metadata: {},
96
97
  object: 'node' as const,
98
+ panelHeight: 0.15,
99
+ panelWidth: 0.25,
97
100
  parentId: 'level_1' as const,
98
101
  position: [1, 2, 3] as [number, number, number],
99
102
  rotation: [0, 0, 0] as [number, number, number],
@@ -52,8 +52,7 @@ describe('terrain-sculpt mode lifecycle', () => {
52
52
 
53
53
  test('a phase switch out of site drops the scope', () => {
54
54
  useEditor.getState().setMode('terrain-sculpt')
55
- // This path rewrites `mode` without going through `setMode`, which is
56
- // exactly why the scope sync is its own function.
55
+ // The phase setter delegates its mode rewrite to the ToolMode transition.
57
56
  useEditor.getState().setPhase('structure')
58
57
  expect(useInteractionScope.getState().scope.kind).toBe('idle')
59
58
  })
@@ -153,10 +152,8 @@ describe('the brush and the 3D canvas travel together', () => {
153
152
 
154
153
  describe('the modes that hide the editing canvas release the brush', () => {
155
154
  // Preview / walkthrough / studio all unmount `ToolManager` (via `noEditing`),
156
- // so the sculpt tool that would release the scope on unmount is gone. Each one
157
- // resets `mode` with a raw `set` rather than `setMode`, which is exactly the
158
- // bug class `setPhase` had: a `sculpting` scope leaked here disables selection
159
- // across the whole editor with nothing left mounted to clear it.
155
+ // so the sculpt tool that would release the scope on unmount is gone. Their
156
+ // ToolMode transitions must release the scope before the canvas disappears.
160
157
  test('entering preview releases it', () => {
161
158
  useEditor.getState().setMode('terrain-sculpt')
162
159
  useEditor.getState().setPreviewMode(true)
@@ -0,0 +1,87 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import useEditor, { normalizePersistedEditorUiState } from './use-editor'
3
+
4
+ function resetToolMode() {
5
+ useEditor.getState().setPhase('structure')
6
+ useEditor.getState().setStructureLayer('elements')
7
+ useEditor.getState().armToolMode({ mode: 'select' })
8
+ useEditor.getState().setActivePaintMaterial(null)
9
+ }
10
+
11
+ beforeEach(resetToolMode)
12
+ afterEach(resetToolMode)
13
+
14
+ describe('ToolMode transition', () => {
15
+ test('choosing a paint swatch while selected arms material paint', () => {
16
+ useEditor.getState().armToolMode({ mode: 'select' })
17
+ useEditor.getState().armMaterialPaint({
18
+ materialPreset: 'library:test-paint',
19
+ sourceTarget: 'wall',
20
+ })
21
+
22
+ expect(useEditor.getState().toolMode).toEqual({ mode: 'material-paint' })
23
+ expect(useEditor.getState().mode).toBe('material-paint')
24
+ expect(useEditor.getState().tool).toBeNull()
25
+ expect(useEditor.getState().activePaintMaterial?.materialPreset).toBe('library:test-paint')
26
+ })
27
+
28
+ test('leaving build clears the materialized tool', () => {
29
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
30
+ useEditor.getState().armToolMode({ mode: 'select' })
31
+
32
+ expect(useEditor.getState().toolMode).toEqual({ mode: 'select' })
33
+ expect(useEditor.getState().mode).toBe('select')
34
+ expect(useEditor.getState().tool).toBeNull()
35
+ })
36
+
37
+ test('the setMode compatibility wrapper elects a default build tool', () => {
38
+ useEditor.getState().setMode('build')
39
+
40
+ expect(useEditor.getState().toolMode).toEqual({ mode: 'build', tool: 'wall' })
41
+ expect(useEditor.getState().mode).toBe('build')
42
+ expect(useEditor.getState().tool).toBe('wall')
43
+ })
44
+
45
+ test('setTool enters build and null exits it', () => {
46
+ useEditor.getState().armToolMode({ mode: 'select' })
47
+ useEditor.getState().setTool('slab')
48
+
49
+ expect(useEditor.getState().toolMode).toEqual({ mode: 'build', tool: 'slab' })
50
+ expect(useEditor.getState().mode).toBe('build')
51
+ expect(useEditor.getState().tool).toBe('slab')
52
+
53
+ useEditor.getState().setTool(null)
54
+ expect(useEditor.getState().toolMode).toEqual({ mode: 'select' })
55
+ expect(useEditor.getState().tool).toBeNull()
56
+ })
57
+ })
58
+
59
+ describe('persisted ToolMode normalization', () => {
60
+ test('elects a default for build with a null tool', () => {
61
+ const state = normalizePersistedEditorUiState({
62
+ phase: 'structure',
63
+ toolMode: { mode: 'build', tool: null as never },
64
+ mode: 'select',
65
+ tool: 'slab',
66
+ structureLayer: 'elements',
67
+ })
68
+
69
+ expect(state.toolMode).toEqual({ mode: 'build', tool: 'wall' })
70
+ expect(state.mode).toBe('build')
71
+ expect(state.tool).toBe('wall')
72
+ })
73
+
74
+ test('clears a persisted tool from a non-build mode', () => {
75
+ const state = normalizePersistedEditorUiState({
76
+ phase: 'structure',
77
+ toolMode: { mode: 'select' },
78
+ mode: 'build',
79
+ tool: 'wall',
80
+ structureLayer: 'elements',
81
+ })
82
+
83
+ expect(state.toolMode).toEqual({ mode: 'select' })
84
+ expect(state.mode).toBe('select')
85
+ expect(state.tool).toBeNull()
86
+ })
87
+ })
@@ -0,0 +1,48 @@
1
+ import { create } from 'zustand'
2
+
3
+ /** The camera actions the canvas hint can name. */
4
+ export type CameraHintAction = 'Pan' | 'Rotate' | 'Zoom'
5
+
6
+ /**
7
+ * Which camera controls the canvas hint is currently allowed to name.
8
+ *
9
+ * `null` — the default — means all of them, which is what the editor shows on
10
+ * its own: a first-time user has no idea which button does what, and the panel
11
+ * is there to answer that once.
12
+ *
13
+ * A host teaching the camera one gesture at a time needs the opposite. Showing
14
+ * three controls while asking for one turns the answer into a search, and
15
+ * leaving all three up after the lesson leaves a permanent widget over the
16
+ * canvas explaining something the user has just been walked through. So a host
17
+ * can narrow the panel to the gesture it is asking for, and to nothing at all
18
+ * once it is done — an empty list hides the panel outright.
19
+ *
20
+ * Deliberately a store rather than a prop: the thing that knows which gesture
21
+ * is being taught is a host surface several levels away from the canvas, and
22
+ * threading it through would put a teaching concern in every component between.
23
+ * The editor knows only "show these actions"; it never learns why.
24
+ */
25
+ type CameraHintFocus = {
26
+ actions: readonly CameraHintAction[] | null
27
+ focus: (actions: readonly CameraHintAction[] | null) => void
28
+ }
29
+
30
+ export const useCameraHintFocus = create<CameraHintFocus>((set) => ({
31
+ actions: null,
32
+ focus: (actions) =>
33
+ set((state) => {
34
+ const current = state.actions
35
+ if (
36
+ current === actions ||
37
+ (current !== null &&
38
+ actions !== null &&
39
+ current.length === actions.length &&
40
+ current.every((action, index) => action === actions[index]))
41
+ ) {
42
+ return state
43
+ }
44
+ return { actions }
45
+ }),
46
+ }))
47
+
48
+ export default useCameraHintFocus