@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
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,24 +1,26 @@
1
1
  {
2
2
  "name": "@pascal-app/editor",
3
- "version": "1.0.0-beta.4",
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",
8
9
  "./catalog": "./src/components/ui/item-catalog/catalog-items.tsx"
9
10
  },
10
11
  "scripts": {
11
- "check-types": "tsgo --noEmit"
12
+ "check-types": "tsgo --noEmit",
13
+ "test": "bun test src"
12
14
  },
13
15
  "peerDependencies": {
14
- "@pascal-app/core": "^1.0.0-beta.4",
15
- "@pascal-app/viewer": "^1.0.0-beta.4",
16
+ "@pascal-app/core": "^1.0.0",
17
+ "@pascal-app/viewer": "^1.0.0",
16
18
  "@react-three/drei": "^10",
17
19
  "@react-three/fiber": "^9",
18
20
  "next": ">=15",
19
21
  "react": "^18 || ^19",
20
22
  "react-dom": "^18 || ^19",
21
- "three": "^0.185"
23
+ "three": "^0.186"
22
24
  },
23
25
  "dependencies": {
24
26
  "@dnd-kit/core": "^6.3.1",
@@ -44,21 +46,25 @@
44
46
  "class-variance-authority": "^0.7.1",
45
47
  "clsx": "^2.1.1",
46
48
  "cmdk": "^1.1.1",
49
+ "fflate": "^0.8.3",
47
50
  "howler": "^2.2.4",
48
51
  "lucide-react": "^1.7.0",
52
+ "manifold-3d": "3.5.1",
49
53
  "mitt": "^3.0.1",
50
54
  "motion": "^12.34.3",
51
55
  "nanoid": "^5.1.6",
52
56
  "pdfkit": "^0.19.1",
53
57
  "tailwind-merge": "^3.5.0",
58
+ "three-bvh-csg": "^0.0.18",
54
59
  "three-mesh-bvh": "~0.9.8",
55
- "zod": "^4.3.6",
60
+ "zod": ">=4.5.4 <4.6",
56
61
  "zustand": "^5.0.11"
57
62
  },
58
63
  "devDependencies": {
59
- "@pascal-app/core": "^1.0.0-beta.4",
60
- "@pascal-app/viewer": "^1.0.0-beta.4",
64
+ "@pascal-app/core": "^1.0.0",
65
+ "@pascal-app/viewer": "^1.0.0",
61
66
  "@pascal/typescript-config": "*",
67
+ "@react-three/test-renderer": "^9.1.0",
62
68
  "@types/blob-stream": "^0.1.33",
63
69
  "@types/bun": "^1.3.0",
64
70
  "@types/howler": "^2.2.12",
@@ -66,6 +72,7 @@
66
72
  "@types/react": "19.2.2",
67
73
  "@types/react-dom": "19.2.2",
68
74
  "@types/three": "^0.184.0",
75
+ "fast-xml-parser": "^5.4.2",
69
76
  "typescript": "6.0.3"
70
77
  }
71
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
+ }
@@ -66,6 +66,7 @@ export const Alignment3DGuideLayer = memo(function Alignment3DGuideLayer() {
66
66
  const guides = useAlignmentGuides((s) => s.guides)
67
67
  const levelId = useViewer((s) => s.selection.levelId)
68
68
  const unit = useViewer((s) => s.unit)
69
+ const metricNotation = useViewer((s) => s.metricNotation)
69
70
  const groupRef = useRef<Group>(null)
70
71
 
71
72
  // Guides carry only XZ in WORLD coords; their Y has to track the active
@@ -85,16 +86,24 @@ export const Alignment3DGuideLayer = memo(function Alignment3DGuideLayer() {
85
86
  return (
86
87
  <group ref={groupRef}>
87
88
  {guides.map((guide, i) => (
88
- <GuideLine guide={guide} key={i} unit={unit} />
89
+ <GuideLine guide={guide} key={i} metricNotation={metricNotation} unit={unit} />
89
90
  ))}
90
91
  </group>
91
92
  )
92
93
  })
93
94
 
94
- function GuideLine({ guide, unit }: { guide: AlignmentGuide; unit: 'metric' | 'imperial' }) {
95
+ function GuideLine({
96
+ guide,
97
+ metricNotation,
98
+ unit,
99
+ }: {
100
+ guide: AlignmentGuide
101
+ metricNotation: 'meters' | 'millimeters'
102
+ unit: 'metric' | 'imperial'
103
+ }) {
95
104
  const { x: fx, z: fz } = guide.from
96
105
  const { x: tx, z: tz } = guide.to
97
- const distLabel = formatMeasurement(guide.distance, unit)
106
+ const distLabel = formatMeasurement(guide.distance, unit, metricNotation)
98
107
 
99
108
  // Lay out the dash centres along the from→to direction. The ribbon
100
109
  // stretches the dash period up if the line is long enough to exceed the
@@ -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)
@@ -77,6 +77,11 @@ function LeftColumn({
77
77
  // up to the minimum so the panel always returns to a usable size.
78
78
  const handleRailClick = useCallback(
79
79
  (id: string) => {
80
+ // noPanel tabs drive the stage, not the panel — leave collapse state alone.
81
+ if (tabs.find((t) => t.id === id)?.noPanel) {
82
+ setActivePanel(id)
83
+ return
84
+ }
80
85
  if (isCollapsed) {
81
86
  setIsCollapsed(false)
82
87
  if (width < SIDEBAR_MIN_WIDTH) setWidth(SIDEBAR_MIN_WIDTH)
@@ -89,7 +94,7 @@ function LeftColumn({
89
94
  }
90
95
  setActivePanel(id)
91
96
  },
92
- [isCollapsed, width, activePanel, setIsCollapsed, setWidth, setActivePanel],
97
+ [tabs, isCollapsed, width, activePanel, setIsCollapsed, setWidth, setActivePanel],
93
98
  )
94
99
 
95
100
  useEffect(() => {
@@ -126,7 +131,7 @@ function LeftColumn({
126
131
  onIconClick={handleRailClick}
127
132
  tabs={tabs}
128
133
  />
129
- {!isCollapsed && (
134
+ {!isCollapsed && !tabs.find((t) => t.id === activePanel)?.noPanel && (
130
135
  <div
131
136
  className="relative flex h-full flex-col"
132
137
  style={{
@@ -183,8 +188,15 @@ function RightColumn({
183
188
  <div className="pointer-events-auto flex items-center gap-2">{toolbarRight}</div>
184
189
  </div>
185
190
  )}
186
- {/* Canvas area */}
187
- <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>
188
200
  {/* Stage overlay — replaces the canvas visually (e.g. studio gallery)
189
201
  while keeping it mounted. Sits below the viewer toolbar (z-20) so
190
202
  the stage switch stays reachable. */}
@@ -239,7 +251,7 @@ export function EditorLayoutV2({
239
251
  overlays={overlays}
240
252
  renderTabContent={renderTabContent}
241
253
  sidebarOverlay={sidebarOverlay}
242
- sidebarTabs={sidebarTabs}
254
+ sidebarTabs={sidebarTabs.filter((t) => !t.noPanel)}
243
255
  viewerContent={viewerContent}
244
256
  viewerToolbarLeft={viewerToolbarLeft}
245
257
  viewerToolbarRight={viewerToolbarRight}
@@ -44,6 +44,7 @@ type Vec3 = [number, number, number]
44
44
  export const Elevation3DGuideLayer = memo(function Elevation3DGuideLayer() {
45
45
  const guide = useElevationGuides((state) => state.guide)
46
46
  const unit = useViewer((state) => state.unit)
47
+ const metricNotation = useViewer((state) => state.metricNotation)
47
48
  const groupRef = useRef<Group>(null)
48
49
 
49
50
  useFrame(() => {
@@ -76,8 +77,8 @@ export const Elevation3DGuideLayer = memo(function Elevation3DGuideLayer() {
76
77
  ]
77
78
  const elevationText =
78
79
  Math.abs(guide.elevation) < 1e-6
79
- ? formatMeasurement(0, unit)
80
- : `${guide.elevation < 0 ? '-' : '+'}${formatMeasurement(Math.abs(guide.elevation), unit)}`
80
+ ? formatMeasurement(0, unit, metricNotation)
81
+ : `${guide.elevation < 0 ? '-' : '+'}${formatMeasurement(Math.abs(guide.elevation), unit, metricNotation)}`
81
82
 
82
83
  return (
83
84
  <group ref={groupRef}>