@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pascal Group Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/bunfig.toml ADDED
@@ -0,0 +1,4 @@
1
+ preload = ["../../scripts/bun-preload-three.ts"]
2
+
3
+ [test]
4
+ preload = ["../../scripts/bun-preload-three.ts"]
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@pascal-app/editor",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0",
4
4
  "description": "Pascal building editor component",
5
+ "license": "MIT",
5
6
  "type": "module",
6
7
  "exports": {
7
8
  ".": "./src/index.tsx",
@@ -12,14 +13,14 @@
12
13
  "test": "bun test src"
13
14
  },
14
15
  "peerDependencies": {
15
- "@pascal-app/core": "^1.0.0-beta.5",
16
- "@pascal-app/viewer": "^1.0.0-beta.5",
16
+ "@pascal-app/core": "^1.0.0",
17
+ "@pascal-app/viewer": "^1.0.0",
17
18
  "@react-three/drei": "^10",
18
19
  "@react-three/fiber": "^9",
19
20
  "next": ">=15",
20
21
  "react": "^18 || ^19",
21
22
  "react-dom": "^18 || ^19",
22
- "three": "^0.185"
23
+ "three": "^0.186"
23
24
  },
24
25
  "dependencies": {
25
26
  "@dnd-kit/core": "^6.3.1",
@@ -45,21 +46,25 @@
45
46
  "class-variance-authority": "^0.7.1",
46
47
  "clsx": "^2.1.1",
47
48
  "cmdk": "^1.1.1",
49
+ "fflate": "^0.8.3",
48
50
  "howler": "^2.2.4",
49
51
  "lucide-react": "^1.7.0",
52
+ "manifold-3d": "3.5.1",
50
53
  "mitt": "^3.0.1",
51
54
  "motion": "^12.34.3",
52
55
  "nanoid": "^5.1.6",
53
56
  "pdfkit": "^0.19.1",
54
57
  "tailwind-merge": "^3.5.0",
58
+ "three-bvh-csg": "^0.0.18",
55
59
  "three-mesh-bvh": "~0.9.8",
56
- "zod": "^4.3.6",
60
+ "zod": ">=4.5.4 <4.6",
57
61
  "zustand": "^5.0.11"
58
62
  },
59
63
  "devDependencies": {
60
- "@pascal-app/core": "^1.0.0-beta.5",
61
- "@pascal-app/viewer": "^1.0.0-beta.5",
64
+ "@pascal-app/core": "^1.0.0",
65
+ "@pascal-app/viewer": "^1.0.0",
62
66
  "@pascal/typescript-config": "*",
67
+ "@react-three/test-renderer": "^9.1.0",
63
68
  "@types/blob-stream": "^0.1.33",
64
69
  "@types/bun": "^1.3.0",
65
70
  "@types/howler": "^2.2.12",
@@ -67,6 +72,7 @@
67
72
  "@types/react": "19.2.2",
68
73
  "@types/react-dom": "19.2.2",
69
74
  "@types/three": "^0.184.0",
75
+ "fast-xml-parser": "^5.4.2",
70
76
  "typescript": "6.0.3"
71
77
  }
72
78
  }
@@ -0,0 +1,89 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import { resolve } from 'node:path'
3
+ import { prepareSceneForExport } from '../src/lib/glb-export'
4
+ import { exportSceneLevelsForPrint } from '../src/lib/level-print-export'
5
+ import { filterPreparedSceneForPrintContent } from '../src/lib/print-content-scope'
6
+ import { createPrintGoldenHouseFixture } from '../src/lib/print-golden-house.test-fixture'
7
+ import { compileManifoldMeshData } from '../src/lib/print-shell-compiler-manifold-core'
8
+ import { compileSemanticPrintShellWithManifold } from '../src/lib/print-shell-compiler-manifold-worker'
9
+
10
+ const outputArgument = process.argv[2]
11
+ if (!outputArgument) {
12
+ throw new Error(
13
+ 'Usage: bun packages/editor/scripts/generate-print-golden-house.ts <output-directory>',
14
+ )
15
+ }
16
+
17
+ const outputDirectory = resolve(outputArgument)
18
+ const fixture = createPrintGoldenHouseFixture()
19
+
20
+ async function sha256(data: Uint8Array): Promise<string> {
21
+ const digest = await crypto.subtle.digest('SHA-256', data)
22
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('')
23
+ }
24
+
25
+ try {
26
+ const prepared = prepareSceneForExport(fixture.root, fixture.nodes, { onlyVisible: true })
27
+ const structure = filterPreparedSceneForPrintContent(prepared.scene, fixture.nodes, 'structure')
28
+ const compileShell = (source: Parameters<typeof exportSceneLevelsForPrint>[0]) =>
29
+ compileSemanticPrintShellWithManifold(source, fixture.nodes, {
30
+ runner: compileManifoldMeshData,
31
+ })
32
+ const common = {
33
+ scale: 100,
34
+ plinth: { marginMm: 2, thicknessMm: 3 },
35
+ compileShells: true,
36
+ compileShell,
37
+ }
38
+ const threeMf = await exportSceneLevelsForPrint(structure, fixture.nodes, {
39
+ ...common,
40
+ format: '3mf',
41
+ })
42
+ const stl = await exportSceneLevelsForPrint(structure, fixture.nodes, {
43
+ ...common,
44
+ format: 'stl',
45
+ })
46
+ if (threeMf.report.status === 'blocked' || stl.report.status === 'blocked') {
47
+ throw new Error('The golden house failed print preflight and was not written.')
48
+ }
49
+
50
+ await mkdir(outputDirectory, { recursive: true })
51
+ const files = [
52
+ { name: 'pascal-golden-house-levels.3mf', data: threeMf.data },
53
+ { name: 'pascal-golden-house-levels-stl.zip', data: stl.data },
54
+ ]
55
+ for (const file of files) await writeFile(resolve(outputDirectory, file.name), file.data)
56
+
57
+ const manifest = {
58
+ kind: 'pascal-print-golden-house',
59
+ version: 1,
60
+ scale: 100,
61
+ units: 'millimeter',
62
+ files: await Promise.all(
63
+ files.map(async (file) => ({
64
+ name: file.name,
65
+ bytes: file.data.byteLength,
66
+ sha256: await sha256(file.data),
67
+ })),
68
+ ),
69
+ parts: threeMf.report.parts.map((part) => ({
70
+ kind: part.kind,
71
+ label: part.label,
72
+ sourceBaseMeters: part.sourceBaseMeters,
73
+ bounds: part.report.bounds,
74
+ triangles: part.report.triangleCount,
75
+ connectedComponentCount: part.report.connectedComponentCount,
76
+ solidComponentCount: part.report.solidComponentCount,
77
+ invertedWinding: part.report.invertedWinding,
78
+ volumeMm3: part.report.volumeMm3,
79
+ minimumFeatureThicknessMm: part.report.minimumFeatureThicknessMm,
80
+ })),
81
+ }
82
+ await writeFile(
83
+ resolve(outputDirectory, 'manifest.json'),
84
+ `${JSON.stringify(manifest, null, 2)}\n`,
85
+ )
86
+ process.stdout.write(`${JSON.stringify({ outputDirectory, ...manifest }, null, 2)}\n`)
87
+ } finally {
88
+ fixture.dispose()
89
+ }
@@ -31,6 +31,7 @@ export function BakeExporter({
31
31
  if (!sceneGroup) throw new Error('scene-renderer group not found')
32
32
  const buffer = await exportSceneToGlb(sceneGroup, useScene.getState().nodes, {
33
33
  textures: 'reference',
34
+ purpose: 'viewer',
34
35
  })
35
36
  onComplete(buffer)
36
37
  } catch (err) {
@@ -46,4 +46,45 @@ describe('camera dragging lifecycle', () => {
46
46
  expect(scheduled).toBeNull()
47
47
  expect(dragging).toEqual([true, true])
48
48
  })
49
+
50
+ test('pause ends damping without rest, cancels wheel fallback and re-arms on resume', () => {
51
+ let dragging = false
52
+ let scheduled: (() => void) | null = null
53
+ const lifecycle = createCameraDraggingLifecycle({
54
+ setDragging: (value) => {
55
+ dragging = value
56
+ },
57
+ schedule: (callback) => {
58
+ scheduled = callback
59
+ return 1 as unknown as ReturnType<typeof globalThis.setTimeout>
60
+ },
61
+ cancel: () => {
62
+ scheduled = null
63
+ },
64
+ })
65
+
66
+ lifecycle.begin()
67
+ expect(dragging).toBe(true)
68
+ lifecycle.setPaused(true)
69
+ expect(dragging).toBe(false)
70
+ lifecycle.begin()
71
+ lifecycle.scheduleEnd()
72
+ expect(dragging).toBe(false)
73
+ expect(scheduled).toBeNull()
74
+
75
+ lifecycle.setPaused(false)
76
+ expect(dragging).toBe(false)
77
+ lifecycle.begin()
78
+ lifecycle.scheduleEnd()
79
+ expect(dragging).toBe(true)
80
+ expect(scheduled).not.toBeNull()
81
+ lifecycle.setPaused(true)
82
+ expect(dragging).toBe(false)
83
+ expect(scheduled).toBeNull()
84
+ lifecycle.setPaused(false)
85
+ lifecycle.begin()
86
+ expect(dragging).toBe(true)
87
+ lifecycle.end()
88
+ expect(dragging).toBe(false)
89
+ })
49
90
  })
@@ -11,6 +11,7 @@ export function createCameraDraggingLifecycle({
11
11
  schedule?: (callback: () => void, delay: number) => TimerHandle
12
12
  cancel?: (timer: TimerHandle) => void
13
13
  }) {
14
+ let paused = false
14
15
  let releaseTimer: TimerHandle | null = null
15
16
 
16
17
  const clearScheduledEnd = () => {
@@ -21,7 +22,7 @@ export function createCameraDraggingLifecycle({
21
22
 
22
23
  const begin = () => {
23
24
  clearScheduledEnd()
24
- setDragging(true)
25
+ if (!paused) setDragging(true)
25
26
  }
26
27
 
27
28
  const end = () => {
@@ -31,11 +32,18 @@ export function createCameraDraggingLifecycle({
31
32
 
32
33
  const scheduleEnd = () => {
33
34
  clearScheduledEnd()
35
+ if (paused) return
34
36
  releaseTimer = schedule(() => {
35
37
  releaseTimer = null
36
38
  setDragging(false)
37
39
  }, fallbackMs)
38
40
  }
39
41
 
40
- return { begin, end, scheduleEnd }
42
+ const setPaused = (value: boolean) => {
43
+ paused = value
44
+ // Paused controls cannot advance damping to rest/sleep.
45
+ if (paused) end()
46
+ }
47
+
48
+ return { begin, end, scheduleEnd, setPaused }
41
49
  }
@@ -0,0 +1,59 @@
1
+ 'use client'
2
+
3
+ import { useThree } from '@react-three/fiber'
4
+ import { useEffect, useRef } from 'react'
5
+ import type { PerspectiveCamera } from 'three'
6
+ import useEditor from '../../store/use-editor'
7
+
8
+ /**
9
+ * Owns the main camera's field of view while the snapshot capture overlay is
10
+ * open, and is mounted only for that window. `ThumbnailGenerator` copies the
11
+ * main camera's fov on every shot, so the value written here is what lands in
12
+ * the saved image.
13
+ *
14
+ * The overlay's slider cannot reach the camera itself (it renders outside the
15
+ * canvas), so the store carries the value and this rig applies it.
16
+ */
17
+ export const CaptureCameraRig = () => {
18
+ const camera = useThree((state) => state.camera)
19
+ const captureFov = useEditor((state) => state.captureFov)
20
+ const appliedFovRef = useRef<number | null>(null)
21
+
22
+ useEffect(() => {
23
+ const perspectiveCamera = camera as PerspectiveCamera
24
+ if (!perspectiveCamera.isPerspectiveCamera) {
25
+ // Orthographic captures have no fov to drive — the overlay hides the
26
+ // control while the store is disarmed.
27
+ useEditor.getState().armCaptureFov(null)
28
+ return
29
+ }
30
+
31
+ const entryFov = perspectiveCamera.fov
32
+ useEditor.getState().armCaptureFov(entryFov)
33
+
34
+ return () => {
35
+ useEditor.getState().armCaptureFov(null)
36
+ // Walkthrough restores its own pre-entry fov when it unmounts, which can
37
+ // happen in the same commit as this rig (leaving capture also leaves
38
+ // walk/drone). Only put the entry fov back if the camera still holds our
39
+ // last write — otherwise someone else has already re-owned it.
40
+ if (appliedFovRef.current !== null && perspectiveCamera.fov === appliedFovRef.current) {
41
+ perspectiveCamera.fov = entryFov
42
+ perspectiveCamera.updateProjectionMatrix()
43
+ }
44
+ appliedFovRef.current = null
45
+ }
46
+ }, [camera])
47
+
48
+ useEffect(() => {
49
+ const perspectiveCamera = camera as PerspectiveCamera
50
+ if (!perspectiveCamera.isPerspectiveCamera || captureFov === null) return
51
+
52
+ appliedFovRef.current = captureFov
53
+ if (perspectiveCamera.fov === captureFov) return
54
+ perspectiveCamera.fov = captureFov
55
+ perspectiveCamera.updateProjectionMatrix()
56
+ }, [camera, captureFov])
57
+
58
+ return null
59
+ }
@@ -9,10 +9,10 @@ import {
9
9
  sceneRegistry,
10
10
  useScene,
11
11
  } from '@pascal-app/core'
12
- import { GRID_LAYER, useViewer, ZONE_LAYER } from '@pascal-app/viewer'
12
+ import { GRID_LAYER, getLevelPresentationY, useViewer, ZONE_LAYER } from '@pascal-app/viewer'
13
13
  import { CameraControls, CameraControlsImpl } from '@react-three/drei'
14
14
  import { useFrame, useThree } from '@react-three/fiber'
15
- import { useCallback, useEffect, useMemo, useRef } from 'react'
15
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'
16
16
  import {
17
17
  Box3,
18
18
  type Camera,
@@ -347,7 +347,7 @@ function useFirstPersonCameraPoseRestore(
347
347
  return useCallback(() => isRestoring.current, [])
348
348
  }
349
349
 
350
- export const CustomCameraControls = () => {
350
+ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) => {
351
351
  const controls = useRef<CameraControlsImpl | null>(null)
352
352
  const pendingAppliedPose = useRef<CameraPoseApplicationPlan | null>(null)
353
353
  const activePoseInterpolation = useRef<{
@@ -366,6 +366,8 @@ export const CustomCameraControls = () => {
366
366
  const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
367
367
  const allowUndergroundCamera = useEditor((s) => s.allowUndergroundCamera)
368
368
  const selection = useViewer((s) => s.selection)
369
+ const levelMode = useViewer((s) => s.levelMode)
370
+ const renderPaused = useViewer((state) => state.renderPaused)
369
371
  const cameraMode = useViewer((state) => state.cameraMode)
370
372
  const isRestoringFirstPersonPose = useFirstPersonCameraPoseRestore(
371
373
  controls,
@@ -528,21 +530,24 @@ export const CustomCameraControls = () => {
528
530
 
529
531
  useEffect(() => {
530
532
  if (isPreviewMode || isFirstPersonMode || isRestoringFirstPersonPose()) return
531
- let targetY = 0
532
- if (currentLevelId) {
533
- const levelMesh = sceneRegistry.nodes.get(currentLevelId)
534
- if (levelMesh) {
535
- targetY = levelMesh.position.y
536
- }
537
- }
533
+ // Analytic destination, not `sceneRegistry` mesh position: a level created
534
+ // this frame still sits at y=0 (LevelSystem lerps it later), and a mode
535
+ // switch leaves every level mid-lerp — the camera must pan to where the
536
+ // level will settle, in the CURRENT presentation mode.
537
+ const targetY = currentLevelId
538
+ ? getLevelPresentationY(currentLevelId, useScene.getState().nodes, levelMode)
539
+ : 0
538
540
  if (!controls.current) return
539
541
  if (firstLoad.current) {
540
542
  firstLoad.current = false
541
543
  controls.current.setLookAt(20, 20, 20, 0, 0, 0, true)
542
544
  }
543
545
  controls.current.getTarget(currentTarget)
546
+ // Idempotence guard: skip when already there — also swallows the thumbnail
547
+ // generator's synchronous stacked→restore levelMode round-trip.
548
+ if (Math.abs(currentTarget.y - targetY) < 1e-3) return
544
549
  controls.current.moveTo(currentTarget.x, targetY, currentTarget.z, true)
545
- }, [currentLevelId, isPreviewMode, isFirstPersonMode, isRestoringFirstPersonPose])
550
+ }, [currentLevelId, levelMode, isPreviewMode, isFirstPersonMode, isRestoringFirstPersonPose])
546
551
 
547
552
  useEffect(() => {
548
553
  if (isFirstPersonMode || !controls.current) return
@@ -1274,6 +1279,10 @@ export const CustomCameraControls = () => {
1274
1279
  cameraDraggingLifecycle.end()
1275
1280
  }, [cameraDraggingLifecycle])
1276
1281
 
1282
+ useLayoutEffect(() => {
1283
+ cameraDraggingLifecycle.setPaused(paused || renderPaused)
1284
+ }, [cameraDraggingLifecycle, paused, renderPaused])
1285
+
1277
1286
  const onControlEnd = useCallback(() => {
1278
1287
  // A mapped-button tap with zero camera movement never wakes the
1279
1288
  // controls, so no rest/sleep follows — clear the dragging flag on
@@ -83,18 +83,18 @@ export function EditorLayoutMobile({
83
83
  // desktop "Furnish" action which itself opens the Items panel).
84
84
  // - Leaving Items while still furnishing exits the build mode.
85
85
  useEffect(() => {
86
- const { phase, mode, setMode, setPhase } = useEditor.getState()
86
+ const { armToolMode, phase, mode, setPhase } = useEditor.getState()
87
87
  if (activePanel === 'ai' && mode === 'build') {
88
- setMode('select')
88
+ armToolMode({ mode: 'select' })
89
89
  return
90
90
  }
91
91
  if (activePanel === 'items') {
92
92
  if (phase !== 'furnish') setPhase('furnish')
93
- if (mode !== 'build') setMode('build')
93
+ if (mode !== 'build') armToolMode({ mode: 'build', tool: 'item' })
94
94
  return
95
95
  }
96
96
  if (phase === 'furnish' && mode === 'build') {
97
- setMode('select')
97
+ armToolMode({ mode: 'select' })
98
98
  }
99
99
  }, [activePanel])
100
100
 
@@ -160,9 +160,9 @@ export function EditorLayoutMobile({
160
160
  if (current > expandedThreshold) {
161
161
  sheetRef.current?.snapTo(SHEET_HANDLE_PX)
162
162
  // Closing the sheet disarms any build tool back to select
163
- const { mode, setMode } = useEditor.getState()
163
+ const { armToolMode, mode } = useEditor.getState()
164
164
  if (mode === 'build') {
165
- setMode('select')
165
+ armToolMode({ mode: 'select' })
166
166
  }
167
167
  } else {
168
168
  sheetRef.current?.snapTo(defaultPx)
@@ -188,8 +188,15 @@ function RightColumn({
188
188
  <div className="pointer-events-auto flex items-center gap-2">{toolbarRight}</div>
189
189
  </div>
190
190
  )}
191
- {/* Canvas area */}
192
- <div className="relative flex-1 overflow-hidden">{children}</div>
191
+ {/* Canvas area. `isolate` matters: drei's `<Html>` computes a z-index
192
+ from camera distance and defaults to a range topping out at
193
+ 16,777,271, and without a stacking context here those values compete
194
+ directly with the viewer toolbar (z-20), the stage overlay (z-10) and
195
+ the overlay band (z-30) — so an in-scene tool badge painted over all
196
+ three. Isolating pins every in-scene HTML layer inside the canvas,
197
+ where it belongs, and leaves their order relative to each other
198
+ untouched. */}
199
+ <div className="relative isolate flex-1 overflow-hidden">{children}</div>
193
200
  {/* Stage overlay — replaces the canvas visually (e.g. studio gallery)
194
201
  while keeping it mounted. Sits below the viewer toolbar (z-20) so
195
202
  the stage switch stays reachable. */}