@pascal-app/editor 1.0.0-beta.5 → 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 (245) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +13 -7
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/bake-exporter.tsx +1 -0
  6. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  7. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  8. package/src/components/editor/capture-camera-rig.tsx +59 -0
  9. package/src/components/editor/custom-camera-controls.tsx +20 -11
  10. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  11. package/src/components/editor/editor-layout-v2.tsx +9 -2
  12. package/src/components/editor/export-manager.tsx +175 -33
  13. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  14. package/src/components/editor/first-person-controls.tsx +180 -12
  15. package/src/components/editor/floating-action-menu.tsx +42 -51
  16. package/src/components/editor/floorplan-panel.tsx +76 -12
  17. package/src/components/editor/grid.tsx +12 -7
  18. package/src/components/editor/group-actions.ts +34 -50
  19. package/src/components/editor/group-move-3d.ts +51 -21
  20. package/src/components/editor/group-transform-shared.test.ts +78 -0
  21. package/src/components/editor/group-transform-shared.ts +74 -7
  22. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  23. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  24. package/src/components/editor/handles/linear-resize-drag.ts +67 -0
  25. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  26. package/src/components/editor/handles/resize-snap.ts +10 -1
  27. package/src/components/editor/handles/use-handle-drag.ts +18 -1
  28. package/src/components/editor/index.tsx +140 -21
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +47 -58
  31. package/src/components/editor/selection-manager.tsx +104 -42
  32. package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
  33. package/src/components/editor/snapshot-capture.test.ts +422 -0
  34. package/src/components/editor/snapshot-capture.ts +145 -0
  35. package/src/components/editor/thumbnail-generator.tsx +284 -237
  36. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  37. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  38. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  39. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  41. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  42. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  43. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  44. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  45. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  46. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +180 -32
  47. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  48. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  49. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  50. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  51. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  52. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  53. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  54. package/src/components/systems/selection-affordance-services.ts +19 -0
  55. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  56. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  57. package/src/components/tools/fence/fence-drafting.ts +6 -25
  58. package/src/components/tools/item/draft-creation.test.ts +12 -0
  59. package/src/components/tools/item/draft-creation.ts +10 -0
  60. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  61. package/src/components/tools/item/face-host-commit.ts +56 -0
  62. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  63. package/src/components/tools/item/face-host-preview.ts +82 -0
  64. package/src/components/tools/item/move-tool.tsx +4 -3
  65. package/src/components/tools/item/placement-math.test.ts +11 -1
  66. package/src/components/tools/item/placement-math.ts +0 -20
  67. package/src/components/tools/item/placement-strategies.test.ts +331 -4
  68. package/src/components/tools/item/placement-strategies.ts +135 -2
  69. package/src/components/tools/item/placement-surface.test.ts +61 -0
  70. package/src/components/tools/item/placement-surface.ts +24 -0
  71. package/src/components/tools/item/placement-types.ts +6 -0
  72. package/src/components/tools/item/test-face-host.ts +121 -0
  73. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  74. package/src/components/tools/item/use-draft-node.ts +26 -4
  75. package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
  76. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  77. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  78. package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
  79. package/src/components/tools/registry-tool-context.tsx +30 -0
  80. package/src/components/tools/select/box-select-tool.tsx +3 -1
  81. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  82. package/src/components/tools/select/marquee-footprint.ts +29 -0
  83. package/src/components/tools/shared/placement-box.tsx +57 -7
  84. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  85. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  86. package/src/components/tools/shared/pointer-support-cap.ts +45 -8
  87. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  88. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  89. package/src/components/tools/stair/stair-tool.tsx +127 -74
  90. package/src/components/tools/tool-manager.tsx +25 -8
  91. package/src/components/tools/wall/wall-drafting.test.ts +320 -2
  92. package/src/components/tools/wall/wall-drafting.ts +23 -114
  93. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  94. package/src/components/ui/action-menu/index.tsx +11 -1
  95. package/src/components/ui/command-palette/editor-commands.tsx +9 -12
  96. package/src/components/ui/command-palette/index.tsx +7 -2
  97. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  98. package/src/components/ui/controls/material-picker.tsx +4 -4
  99. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  100. package/src/components/ui/controls/segmented-control.tsx +8 -1
  101. package/src/components/ui/controls/slider-control.tsx +19 -3
  102. package/src/components/ui/controls/toggle-control.tsx +18 -5
  103. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  104. package/src/components/ui/floating-level-selector.tsx +22 -5
  105. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  106. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  107. package/src/components/ui/helpers/item-helper.tsx +1 -1
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  109. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  110. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  111. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  112. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  113. package/src/components/ui/panels/multi-field-value.ts +163 -0
  114. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  115. package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
  116. package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
  117. package/src/components/ui/panels/node-display.ts +4 -0
  118. package/src/components/ui/panels/panel-manager.tsx +77 -3
  119. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  120. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  121. package/src/components/ui/panels/parametric-inspector.tsx +29 -162
  122. package/src/components/ui/panels/reference-panel.tsx +31 -1
  123. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  124. package/src/components/ui/scene-loader.tsx +35 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
  127. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
  128. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
  129. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  130. package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
  131. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  132. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  133. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  134. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
  135. package/src/components/ui/sidebar/tab-bar.tsx +2 -0
  136. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  137. package/src/components/viewer/viewer-scene-header.tsx +2 -1
  138. package/src/components/viewer/viewer-stage.tsx +4 -1
  139. package/src/components/viewer-overlay.tsx +1 -22
  140. package/src/components/viewer-zone-system.tsx +6 -2
  141. package/src/components/walkthrough-hud.tsx +19 -10
  142. package/src/hooks/use-auto-save.test.ts +73 -1
  143. package/src/hooks/use-auto-save.ts +76 -5
  144. package/src/hooks/use-ceiling-events.test.ts +122 -0
  145. package/src/hooks/use-drag-action.ts +17 -3
  146. package/src/hooks/use-grid-events.test.ts +240 -0
  147. package/src/hooks/use-grid-events.ts +190 -11
  148. package/src/hooks/use-keyboard.test.ts +244 -0
  149. package/src/hooks/use-keyboard.ts +129 -62
  150. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  151. package/src/hooks/use-save-shortcut.ts +33 -0
  152. package/src/index.tsx +82 -4
  153. package/src/lib/active-placement-surface.test.ts +16 -0
  154. package/src/lib/active-placement-surface.ts +14 -0
  155. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  156. package/src/lib/ceiling-surface-raycast.ts +12 -0
  157. package/src/lib/contextual-help-extension.ts +17 -0
  158. package/src/lib/continuation.test.ts +11 -0
  159. package/src/lib/continuation.ts +8 -1
  160. package/src/lib/direct-manipulation.test.ts +132 -0
  161. package/src/lib/direct-manipulation.ts +38 -1
  162. package/src/lib/editor-api.ts +6 -3
  163. package/src/lib/elevation-guides.test.ts +28 -1
  164. package/src/lib/elevation-guides.ts +22 -7
  165. package/src/lib/floating-menu-scale.test.ts +29 -0
  166. package/src/lib/floating-menu-scale.ts +14 -0
  167. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  168. package/src/lib/floorplan/apply-alignment.ts +10 -1
  169. package/src/lib/floorplan/floorplan-export.test.ts +397 -1
  170. package/src/lib/floorplan/floorplan-export.tsx +111 -16
  171. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  172. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  173. package/src/lib/floorplan/floorplan-readonly.ts +1 -0
  174. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  175. package/src/lib/floorplan-grid-event-point.ts +13 -0
  176. package/src/lib/glb-export.test.ts +1169 -21
  177. package/src/lib/glb-export.ts +629 -101
  178. package/src/lib/grid-event-presentation.ts +16 -0
  179. package/src/lib/history.test.ts +528 -147
  180. package/src/lib/history.ts +81 -7
  181. package/src/lib/host-tree-children.test.ts +27 -0
  182. package/src/lib/host-tree-children.ts +79 -0
  183. package/src/lib/interaction/hot-set.test.ts +10 -1
  184. package/src/lib/interaction/hot-set.ts +3 -1
  185. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  186. package/src/lib/interaction/overlay-policy.ts +16 -0
  187. package/src/lib/interaction/registered-drafting.ts +36 -0
  188. package/src/lib/interaction/scope.ts +31 -0
  189. package/src/lib/level-print-export.test.ts +484 -0
  190. package/src/lib/level-print-export.ts +526 -0
  191. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  192. package/src/lib/local-project-presentation-persistence.ts +335 -0
  193. package/src/lib/material-paint.ts +2 -0
  194. package/src/lib/model-export.ts +29 -0
  195. package/src/lib/paint-preview-owner.ts +71 -0
  196. package/src/lib/planar-cursor-placement.test.ts +178 -1
  197. package/src/lib/planar-cursor-placement.ts +61 -5
  198. package/src/lib/plugin-panels.test.ts +92 -1
  199. package/src/lib/plugin-panels.ts +48 -0
  200. package/src/lib/portable-export.test.ts +366 -0
  201. package/src/lib/portable-export.ts +969 -0
  202. package/src/lib/print-3mf.test.ts +86 -0
  203. package/src/lib/print-3mf.ts +161 -0
  204. package/src/lib/print-content-scope.test.ts +107 -0
  205. package/src/lib/print-content-scope.ts +90 -0
  206. package/src/lib/print-export.test.ts +294 -0
  207. package/src/lib/print-export.ts +735 -0
  208. package/src/lib/print-feature-thickness.test.ts +147 -0
  209. package/src/lib/print-feature-thickness.ts +180 -0
  210. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  211. package/src/lib/print-golden-house.test.ts +249 -0
  212. package/src/lib/print-roof-solids.test.ts +133 -0
  213. package/src/lib/print-roof-solids.ts +630 -0
  214. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  215. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  216. package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
  217. package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
  218. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  219. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  220. package/src/lib/print-shell-compiler-protocol.ts +30 -0
  221. package/src/lib/print-shell-compiler.ts +273 -0
  222. package/src/lib/print-wall-solids.test.ts +194 -0
  223. package/src/lib/print-wall-solids.ts +328 -0
  224. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  225. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  226. package/src/lib/scene.ts +22 -8
  227. package/src/lib/selection-routing.test.ts +105 -1
  228. package/src/lib/selection-routing.ts +19 -0
  229. package/src/lib/sfx-bus.ts +4 -0
  230. package/src/lib/sfx-player.ts +8 -0
  231. package/src/lib/snapping-mode.test.ts +32 -4
  232. package/src/lib/snapping-mode.ts +11 -7
  233. package/src/lib/usdz-export.ts +38 -0
  234. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  235. package/src/store/live-draft-preview-stores.test.ts +3 -0
  236. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  237. package/src/store/tool-mode.test.ts +87 -0
  238. package/src/store/use-camera-hint-focus.ts +48 -0
  239. package/src/store/use-editor.tsx +299 -188
  240. package/src/store/use-floorplan-draft-preview.ts +9 -0
  241. package/src/store/use-interaction-scope.test.ts +23 -1
  242. package/src/store/use-interaction-scope.ts +63 -2
  243. package/src/store/use-placement-preview.ts +56 -3
  244. package/src/components/tools/roof/roof-tool.tsx +0 -656
  245. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'bun:test'
2
- import { ROTATE_HANDLE_DRAG_LABEL } from './contextual-help'
2
+ import { GROUP_MOVE_DRAG_LABEL, ROTATE_HANDLE_DRAG_LABEL } from './contextual-help'
3
3
  import {
4
4
  cycleSnappingModeIn,
5
5
  DEFAULT_SNAPPING_MODE,
@@ -49,12 +49,24 @@ describe('resolveSnapFlags', () => {
49
49
  })
50
50
 
51
51
  describe('per-context snapping', () => {
52
- it('items default to grid with no angle lock', () => {
53
- expect(defaultSnappingModeFor('item')).toBe('grid')
52
+ it('items default to magnetic alignment with no angle lock', () => {
53
+ expect(defaultSnappingModeFor('item')).toBe('lines')
54
54
  expect(snappingModesFor('item')).toEqual(['lines', 'grid', 'off'])
55
55
  expect(snappingModesFor('item')).not.toContain('angles')
56
56
  })
57
57
 
58
+ it('group moves use the magnetic item context by default', () => {
59
+ const context = snapContextOf({
60
+ scope: { kind: 'handle-drag', handle: GROUP_MOVE_DRAG_LABEL },
61
+ mode: 'select',
62
+ tool: null,
63
+ profileOf: () => undefined,
64
+ })
65
+
66
+ expect(context).toBe('item')
67
+ expect(resolveSnapFlags(defaultSnappingModeFor(context!)).magnetic).toBe(true)
68
+ })
69
+
58
70
  it('walls default to grid and expose the angle lock; polygons do NOT', () => {
59
71
  expect(defaultSnappingModeFor('wall')).toBe('grid')
60
72
  expect(defaultSnappingModeFor('polygon')).toBe('grid')
@@ -83,10 +95,17 @@ describe('snapContextOf (profile-driven, node-declared)', () => {
83
95
  ceiling: 'structural',
84
96
  roof: 'structural',
85
97
  zone: 'structural',
98
+ block: 'structural',
86
99
  }
87
100
  const profileOf = (t: string) => declared[t]
88
101
  const profileOfNode = (id: string) =>
89
- id === 'cabinet-module_1' ? declared.item : id === 'wall_1' ? declared.wall : undefined
102
+ id === 'cabinet-module_1'
103
+ ? declared.item
104
+ : id === 'wall_1'
105
+ ? declared.wall
106
+ : id === 'block_1'
107
+ ? declared['block']
108
+ : undefined
90
109
  const ctx = (
91
110
  scope: {
92
111
  kind: string
@@ -95,6 +114,7 @@ describe('snapContextOf (profile-driven, node-declared)', () => {
95
114
  nodeId?: string
96
115
  tool?: string
97
116
  handle?: string
117
+ operator?: string
98
118
  },
99
119
  mode = 'select',
100
120
  tool: string | null = null,
@@ -115,6 +135,14 @@ describe('snapContextOf (profile-driven, node-declared)', () => {
115
135
  ).toBeNull()
116
136
  })
117
137
 
138
+ it('gives mesh rotation the angle context and other edit operations polygon snapping', () => {
139
+ expect(ctx({ kind: 'mesh-editing', nodeId: 'block_1' })).toBe('polygon')
140
+ expect(ctx({ kind: 'mesh-editing', nodeId: 'block_1', operator: 'translate' })).toBe('polygon')
141
+ expect(ctx({ kind: 'mesh-editing', nodeId: 'block_1', operator: 'scale' })).toBe('polygon')
142
+ expect(ctx({ kind: 'mesh-editing', nodeId: 'block_1', operator: 'loop-cut' })).toBe('polygon')
143
+ expect(ctx({ kind: 'mesh-editing', nodeId: 'block_1', operator: 'rotate' })).toBe('wall')
144
+ })
145
+
118
146
  it('endpoint reshape is angle-bearing (wall); curve + polygon vertex edits are not', () => {
119
147
  expect(ctx({ kind: 'reshaping', reshape: 'endpoint' })).toBe('wall')
120
148
  expect(ctx({ kind: 'reshaping', reshape: 'curve' })).toBe('polygon')
@@ -4,10 +4,9 @@ import { GROUP_MOVE_DRAG_LABEL, ROTATE_HANDLE_DRAG_LABEL } from './contextual-he
4
4
  /**
5
5
  * Snapping mode is a single global, user-cyclable control that maps onto the
6
6
  * two pre-existing snap knobs (`gridSnapStep` grid snap + `magneticSnap`).
7
- * The default `'grid'` resolves to the exact pair the editor shipped with
8
- * before this control existed (grid on, magnetic on), so the default path is
9
- * behaviourally unchanged only when a user opts into `'lines'` or `'off'`
10
- * does any snap math get suppressed.
7
+ * Each context chooses its own default. Item movement defaults to magnetic
8
+ * alignment so a picked-up group catches neighboring geometry; grid and off
9
+ * remain explicit alternatives in the contextual chip.
11
10
  */
12
11
  export type SnappingMode = 'grid' | 'lines' | 'angles' | 'off'
13
12
 
@@ -82,9 +81,9 @@ type SnapModeSet = { modes: SnappingMode[]; default: SnappingMode }
82
81
  const SNAP_PROFILES: Record<SnapContext, SnapModeSet> = {
83
82
  // Wall / fence drafting + endpoint reshape: direction matters → angle lock.
84
83
  wall: { modes: ['grid', 'lines', 'angles', 'off'], default: 'grid' },
85
- // Item placement / move: grid by default; lines = magnetic alignment only (no
86
- // grid lattice), no angle lock (meaningless for a footprint).
87
- item: { modes: ['lines', 'grid', 'off'], default: 'grid' },
84
+ // Item placement / move: magnetic alignment by default; grid is an explicit
85
+ // alternative, and angle lock is meaningless for a footprint.
86
+ item: { modes: ['lines', 'grid', 'off'], default: 'lines' },
88
87
  // Structural / surface, no direction to set: slab / ceiling / roof draft+move,
89
88
  // whole wall/fence translate, curve reshape, polygon boundary edit. Grid by
90
89
  // default, NO angle lock.
@@ -148,6 +147,7 @@ export function snapContextOf(args: {
148
147
  nodeId?: string
149
148
  tool?: string
150
149
  handle?: string
150
+ operator?: string
151
151
  }
152
152
  mode: string
153
153
  tool: string | null
@@ -166,6 +166,10 @@ export function snapContextOf(args: {
166
166
  return 'item'
167
167
  }
168
168
  switch (scope.kind) {
169
+ case 'mesh-editing':
170
+ return scope.nodeId
171
+ ? contextForProfile(profileOfNode?.(scope.nodeId), scope.operator === 'rotate')
172
+ : null
169
173
  case 'handle-drag':
170
174
  if (scope.handle === ROTATE_HANDLE_DRAG_LABEL) return null
171
175
  return scope.nodeId ? contextForProfile(profileOfNode?.(scope.nodeId), false) : null
@@ -0,0 +1,38 @@
1
+ import type { AnyNode } from '@pascal-app/core'
2
+ import type { Object3D } from 'three'
3
+ import { USDZExporter } from 'three/examples/jsm/exporters/USDZExporter.js'
4
+ import * as WebGPUTextureUtils from 'three/examples/jsm/utils/WebGPUTextureUtils.js'
5
+ import { type GlbExportOptions, preparePortableSceneFromViewer } from './glb-export'
6
+ import { createUsdzScene, disposeExportResources } from './portable-export'
7
+
8
+ export type UsdzExportOptions = Pick<
9
+ GlbExportOptions,
10
+ 'excludedNodeTypes' | 'includedPresentationIds' | 'onlyVisible' | 'onWarning'
11
+ >
12
+
13
+ /** Export a native, self-contained USDZ with no glTF conversion fallback. */
14
+ export async function exportSceneToUsdz(
15
+ sceneGroup: Object3D,
16
+ nodes: Record<string, AnyNode>,
17
+ options: UsdzExportOptions = {},
18
+ ): Promise<Uint8Array<ArrayBuffer>> {
19
+ const prepared = await preparePortableSceneFromViewer(sceneGroup, nodes, {
20
+ ...options,
21
+ textures: 'embed',
22
+ })
23
+ for (const warning of prepared.warnings) options.onWarning?.(warning)
24
+
25
+ let scene: Object3D | null = null
26
+ try {
27
+ scene = createUsdzScene(prepared.scene)
28
+ const exporter = new USDZExporter()
29
+ exporter.textureUtils = WebGPUTextureUtils
30
+ return await exporter.parseAsync(scene, {
31
+ onlyVisible: options.onlyVisible ?? true,
32
+ quickLookCompatible: true,
33
+ })
34
+ } finally {
35
+ if (scene) disposeExportResources(scene, { textures: false })
36
+ prepared.dispose()
37
+ }
38
+ }
@@ -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