@pascal-app/editor 1.0.0-beta.5 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) 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/bake-exporter.tsx +5 -0
  6. package/src/components/editor/bake-thumbnail.tsx +4 -1
  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 +9 -2
  13. package/src/components/editor/export-manager.tsx +175 -33
  14. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  15. package/src/components/editor/first-person-controls.tsx +180 -12
  16. package/src/components/editor/floating-action-menu.tsx +55 -51
  17. package/src/components/editor/floorplan-panel.tsx +99 -15
  18. package/src/components/editor/grid.tsx +12 -7
  19. package/src/components/editor/group-actions.ts +34 -50
  20. package/src/components/editor/group-move-3d.ts +51 -21
  21. package/src/components/editor/group-transform-shared.test.ts +78 -0
  22. package/src/components/editor/group-transform-shared.ts +74 -7
  23. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  24. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  25. package/src/components/editor/handles/handle-placement.test.ts +111 -0
  26. package/src/components/editor/handles/handle-placement.ts +69 -0
  27. package/src/components/editor/handles/linear-resize-drag.ts +78 -0
  28. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  29. package/src/components/editor/handles/resize-snap.ts +10 -1
  30. package/src/components/editor/handles/use-handle-drag.ts +23 -3
  31. package/src/components/editor/index.tsx +151 -24
  32. package/src/components/editor/node-action-menu.tsx +14 -1
  33. package/src/components/editor/node-arrow-handles.tsx +67 -119
  34. package/src/components/editor/selection-manager.tsx +153 -42
  35. package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
  36. package/src/components/editor/snapshot-capture.test.ts +422 -0
  37. package/src/components/editor/snapshot-capture.ts +145 -0
  38. package/src/components/editor/thumbnail-generator.tsx +295 -239
  39. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  40. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  41. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  42. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  43. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
  44. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  45. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  46. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  47. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  48. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  49. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  50. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
  51. package/src/components/procedural-items/inspector.tsx +223 -0
  52. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  53. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  54. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  55. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  56. package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
  57. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  58. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  59. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  60. package/src/components/systems/selection-affordance-services.ts +19 -0
  61. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  62. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  63. package/src/components/tools/fence/fence-drafting.ts +6 -25
  64. package/src/components/tools/item/draft-creation.test.ts +12 -0
  65. package/src/components/tools/item/draft-creation.ts +10 -0
  66. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  67. package/src/components/tools/item/face-host-commit.ts +56 -0
  68. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  69. package/src/components/tools/item/face-host-preview.ts +82 -0
  70. package/src/components/tools/item/item-surface-placement.test.ts +175 -0
  71. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
  72. package/src/components/tools/item/move-tool.tsx +4 -3
  73. package/src/components/tools/item/placement-math.test.ts +11 -1
  74. package/src/components/tools/item/placement-math.ts +0 -20
  75. package/src/components/tools/item/placement-strategies.test.ts +331 -4
  76. package/src/components/tools/item/placement-strategies.ts +304 -171
  77. package/src/components/tools/item/placement-surface.test.ts +61 -0
  78. package/src/components/tools/item/placement-surface.ts +24 -0
  79. package/src/components/tools/item/placement-types.ts +10 -0
  80. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
  81. package/src/components/tools/item/test-face-host.ts +121 -0
  82. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  83. package/src/components/tools/item/use-draft-node.ts +116 -44
  84. package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
  85. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  86. package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
  87. package/src/components/tools/registry/item-surface-move.test.ts +501 -0
  88. package/src/components/tools/registry/item-surface-move.ts +441 -0
  89. package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
  90. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  91. package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
  92. package/src/components/tools/registry-tool-context.tsx +30 -0
  93. package/src/components/tools/select/box-select-tool.tsx +3 -1
  94. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  95. package/src/components/tools/select/marquee-footprint.ts +29 -0
  96. package/src/components/tools/shared/placement-box.tsx +57 -7
  97. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  98. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  99. package/src/components/tools/shared/pointer-support-cap.ts +56 -17
  100. package/src/components/tools/shared/shelf-stickiness.ts +54 -0
  101. package/src/components/tools/shared/surface-hit.test.ts +56 -0
  102. package/src/components/tools/shared/surface-hit.ts +23 -0
  103. package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
  104. package/src/components/tools/shared/surface-rejection.tsx +29 -0
  105. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  106. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  107. package/src/components/tools/stair/stair-tool.tsx +127 -74
  108. package/src/components/tools/tool-manager.tsx +25 -8
  109. package/src/components/tools/wall/wall-drafting.test.ts +320 -2
  110. package/src/components/tools/wall/wall-drafting.ts +23 -114
  111. package/src/components/tools/zone/zone-tool.tsx +13 -4
  112. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  113. package/src/components/ui/action-menu/index.tsx +11 -1
  114. package/src/components/ui/command-palette/editor-commands.tsx +63 -13
  115. package/src/components/ui/command-palette/index.tsx +7 -2
  116. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  117. package/src/components/ui/controls/material-picker.tsx +4 -4
  118. package/src/components/ui/controls/metric-control.tsx +29 -13
  119. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  120. package/src/components/ui/controls/segmented-control.tsx +8 -1
  121. package/src/components/ui/controls/slider-control.tsx +44 -21
  122. package/src/components/ui/controls/toggle-control.tsx +18 -5
  123. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  124. package/src/components/ui/floating-level-selector.tsx +103 -10
  125. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  126. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  127. package/src/components/ui/helpers/item-helper.tsx +1 -1
  128. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  129. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  130. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  131. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  132. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  133. package/src/components/ui/panels/multi-field-value.ts +163 -0
  134. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  135. package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
  136. package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
  137. package/src/components/ui/panels/node-display.ts +4 -0
  138. package/src/components/ui/panels/panel-manager.tsx +77 -3
  139. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  140. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  141. package/src/components/ui/panels/parametric-inspector.tsx +35 -164
  142. package/src/components/ui/panels/reference-panel.tsx +34 -4
  143. package/src/components/ui/primitives/checkbox.tsx +34 -0
  144. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  145. package/src/components/ui/scene-loader.tsx +35 -0
  146. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
  147. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
  148. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
  149. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
  150. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
  151. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  152. package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
  153. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
  154. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  155. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  156. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
  157. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  158. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
  159. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
  160. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
  161. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
  162. package/src/components/ui/sidebar/tab-bar.tsx +2 -0
  163. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  164. package/src/components/ui/snap-target-badge.tsx +2 -0
  165. package/src/components/viewer/viewer-scene-header.tsx +5 -1
  166. package/src/components/viewer/viewer-stage.tsx +4 -1
  167. package/src/components/viewer/viewer-units-panel.tsx +116 -0
  168. package/src/components/viewer-overlay.tsx +1 -22
  169. package/src/components/viewer-zone-system.tsx +6 -2
  170. package/src/components/walkthrough-hud.tsx +19 -10
  171. package/src/hooks/use-auto-save.test.ts +73 -1
  172. package/src/hooks/use-auto-save.ts +76 -5
  173. package/src/hooks/use-ceiling-events.test.ts +144 -0
  174. package/src/hooks/use-ceiling-events.ts +5 -1
  175. package/src/hooks/use-drag-action.ts +17 -3
  176. package/src/hooks/use-grid-events.test.ts +240 -0
  177. package/src/hooks/use-grid-events.ts +190 -11
  178. package/src/hooks/use-keyboard.test.ts +244 -0
  179. package/src/hooks/use-keyboard.ts +132 -63
  180. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  181. package/src/hooks/use-save-shortcut.ts +33 -0
  182. package/src/index.tsx +87 -4
  183. package/src/lib/active-placement-surface.test.ts +16 -0
  184. package/src/lib/active-placement-surface.ts +14 -0
  185. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  186. package/src/lib/ceiling-surface-raycast.ts +12 -0
  187. package/src/lib/contextual-help-extension.ts +17 -0
  188. package/src/lib/continuation.test.ts +11 -0
  189. package/src/lib/continuation.ts +8 -1
  190. package/src/lib/direct-manipulation.test.ts +132 -0
  191. package/src/lib/direct-manipulation.ts +38 -1
  192. package/src/lib/editor-api.ts +6 -3
  193. package/src/lib/elevation-guides.test.ts +28 -1
  194. package/src/lib/elevation-guides.ts +22 -7
  195. package/src/lib/export-texture-utils.test.ts +167 -0
  196. package/src/lib/export-texture-utils.ts +121 -0
  197. package/src/lib/export-user-data.test.ts +48 -0
  198. package/src/lib/export-user-data.ts +69 -0
  199. package/src/lib/floating-menu-scale.test.ts +29 -0
  200. package/src/lib/floating-menu-scale.ts +14 -0
  201. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  202. package/src/lib/floorplan/apply-alignment.ts +10 -1
  203. package/src/lib/floorplan/floorplan-export.test.ts +536 -2
  204. package/src/lib/floorplan/floorplan-export.tsx +120 -19
  205. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  206. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  207. package/src/lib/floorplan/floorplan-readonly.ts +5 -0
  208. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  209. package/src/lib/floorplan-grid-event-point.ts +13 -0
  210. package/src/lib/fresh-planar-placement.ts +13 -5
  211. package/src/lib/glb-export-serialize.test.ts +280 -0
  212. package/src/lib/glb-export.test.ts +1375 -21
  213. package/src/lib/glb-export.ts +781 -106
  214. package/src/lib/grid-event-presentation.ts +16 -0
  215. package/src/lib/history.test.ts +528 -147
  216. package/src/lib/history.ts +81 -7
  217. package/src/lib/host-tree-children.test.ts +27 -0
  218. package/src/lib/host-tree-children.ts +79 -0
  219. package/src/lib/interaction/hot-set.test.ts +10 -1
  220. package/src/lib/interaction/hot-set.ts +3 -1
  221. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  222. package/src/lib/interaction/overlay-policy.ts +16 -0
  223. package/src/lib/interaction/registered-drafting.ts +36 -0
  224. package/src/lib/interaction/scope.ts +31 -0
  225. package/src/lib/level-duplication.test.ts +73 -0
  226. package/src/lib/level-duplication.ts +32 -2
  227. package/src/lib/level-print-export.test.ts +484 -0
  228. package/src/lib/level-print-export.ts +526 -0
  229. package/src/lib/linear-display.test.ts +59 -0
  230. package/src/lib/linear-display.ts +39 -0
  231. package/src/lib/local-guide-image.test.ts +24 -0
  232. package/src/lib/local-guide-image.ts +34 -1
  233. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  234. package/src/lib/local-project-presentation-persistence.ts +335 -0
  235. package/src/lib/material-paint.ts +2 -0
  236. package/src/lib/model-export.ts +29 -0
  237. package/src/lib/node-action-movement.ts +21 -0
  238. package/src/lib/paint-preview-owner.ts +71 -0
  239. package/src/lib/planar-cursor-placement.test.ts +178 -1
  240. package/src/lib/planar-cursor-placement.ts +61 -5
  241. package/src/lib/plugin-panels.test.ts +92 -1
  242. package/src/lib/plugin-panels.ts +48 -0
  243. package/src/lib/portable-export.test.ts +455 -0
  244. package/src/lib/portable-export.ts +1042 -0
  245. package/src/lib/print-3mf.test.ts +86 -0
  246. package/src/lib/print-3mf.ts +161 -0
  247. package/src/lib/print-content-scope.test.ts +107 -0
  248. package/src/lib/print-content-scope.ts +90 -0
  249. package/src/lib/print-export.test.ts +294 -0
  250. package/src/lib/print-export.ts +735 -0
  251. package/src/lib/print-feature-thickness.test.ts +147 -0
  252. package/src/lib/print-feature-thickness.ts +180 -0
  253. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  254. package/src/lib/print-golden-house.test.ts +249 -0
  255. package/src/lib/print-roof-solids.test.ts +133 -0
  256. package/src/lib/print-roof-solids.ts +630 -0
  257. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  258. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  259. package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
  260. package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
  261. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  262. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  263. package/src/lib/print-shell-compiler-protocol.ts +38 -0
  264. package/src/lib/print-shell-compiler.ts +273 -0
  265. package/src/lib/print-wall-solids.test.ts +194 -0
  266. package/src/lib/print-wall-solids.ts +328 -0
  267. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  268. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  269. package/src/lib/scene-clipboard.ts +1 -1
  270. package/src/lib/scene.ts +22 -8
  271. package/src/lib/selection-routing.test.ts +105 -1
  272. package/src/lib/selection-routing.ts +19 -0
  273. package/src/lib/send-to-app.test.ts +108 -0
  274. package/src/lib/send-to-app.ts +121 -0
  275. package/src/lib/sfx/movement-tick.ts +37 -0
  276. package/src/lib/sfx-bus.ts +4 -0
  277. package/src/lib/sfx-player.ts +8 -0
  278. package/src/lib/snapping-mode.test.ts +32 -4
  279. package/src/lib/snapping-mode.ts +11 -7
  280. package/src/lib/surface-attachment.ts +77 -0
  281. package/src/lib/units.ts +254 -0
  282. package/src/lib/usdz-export.ts +65 -0
  283. package/src/lib/use-linear-display.ts +8 -32
  284. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  285. package/src/store/live-draft-preview-stores.test.ts +3 -0
  286. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  287. package/src/store/tool-mode.test.ts +87 -0
  288. package/src/store/use-camera-hint-focus.ts +48 -0
  289. package/src/store/use-editor.tsx +299 -188
  290. package/src/store/use-floorplan-draft-preview.ts +9 -0
  291. package/src/store/use-handle-group.ts +4 -0
  292. package/src/store/use-interaction-scope.test.ts +23 -1
  293. package/src/store/use-interaction-scope.ts +63 -2
  294. package/src/store/use-placement-preview.ts +56 -3
  295. package/src/store/use-send-to-app.ts +21 -0
  296. package/src/components/tools/roof/roof-tool.tsx +0 -656
  297. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -0,0 +1,18 @@
1
+ export function precisionForStep(step: number): number {
2
+ if (step <= 0) return 0
3
+ return Math.max(0, Math.ceil(-Math.log10(step)))
4
+ }
5
+
6
+ export function prettifyKey(key: string): string {
7
+ const spaced = key.replace(/([A-Z])/g, ' $1').toLowerCase()
8
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1)
9
+ }
10
+
11
+ export function prettifyEnumValue(value: string): string {
12
+ return value
13
+ .split(/[-_\s]/)
14
+ .map((word, i) =>
15
+ i === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word.toLowerCase(),
16
+ )
17
+ .join(' ')
18
+ }
@@ -20,9 +20,7 @@ import { collectZoneContentIds } from '../../../lib/zone-content'
20
20
  import useEditor from '../../../store/use-editor'
21
21
  import { ActionButton, ActionGroup } from '../controls/action-button'
22
22
  import { PanelSection } from '../controls/panel-section'
23
- import { SegmentedControl } from '../controls/segmented-control'
24
- import { SliderControl } from '../controls/slider-control'
25
- import { ToggleControl } from '../controls/toggle-control'
23
+ import { ParametricFieldControl } from './parametric-field-control'
26
24
  import { InspectorFooterContext, PanelWrapper } from './panel-wrapper'
27
25
 
28
26
  /**
@@ -67,7 +65,7 @@ export function ParametricInspector({
67
65
  const node = scene.nodes[selectedId]
68
66
  if (parametrics?.derive && node) {
69
67
  const next = { ...node, ...patch } as AnyNode
70
- patch = { ...patch, ...parametrics.derive(next, patch) }
68
+ patch = { ...patch, ...parametrics.derive(next, patch, node as AnyNode) }
71
69
  }
72
70
  // Bundle the edited node + any reconcile follow-ups into ONE
73
71
  // updateNodes call so a single inspector edit is a single undo step.
@@ -133,7 +131,7 @@ export function ParametricInspector({
133
131
  return (
134
132
  <InspectorFooterContext.Provider value={footer}>
135
133
  <Suspense fallback={null}>
136
- <CustomPanel />
134
+ <CustomPanelSlot Component={CustomPanel} nodeId={selectedId} />
137
135
  </Suspense>
138
136
  </InspectorFooterContext.Provider>
139
137
  )
@@ -159,7 +157,11 @@ export function ParametricInspector({
159
157
  width={320}
160
158
  >
161
159
  {parametrics.groups.map((group, gi) => (
162
- <PanelSection key={`group-${gi}`} title={group.label}>
160
+ <PanelSection
161
+ defaultExpanded={group.defaultExpanded}
162
+ key={`${nodeType}-${group.label}-${gi}`}
163
+ title={group.label}
164
+ >
163
165
  {group.fields.map((field, fi) => (
164
166
  <FieldRenderer
165
167
  key={`field-${gi}-${fi}-${String(field.key)}`}
@@ -172,12 +174,12 @@ export function ParametricInspector({
172
174
  ))}
173
175
  {TrailingSection && (
174
176
  <Suspense fallback={null}>
175
- <TrailingSection />
177
+ <CustomPanelSlot Component={TrailingSection} nodeId={selectedId} />
176
178
  </Suspense>
177
179
  )}
178
180
  {(canMove || canDelete || (parametrics.actions && parametrics.actions.length > 0)) && (
179
181
  <PanelSection title="Actions">
180
- <ActionGroup className={isZone ? 'flex-col' : undefined}>
182
+ <ActionGroup className={isZone ? 'flex-col' : parametrics.actions?.length ? 'grid grid-cols-2 gap-2' : undefined}>
181
183
  {canMove && (
182
184
  <ActionButton icon={<Move className="h-4 w-4" />} label="Move" onClick={handleMove} />
183
185
  )}
@@ -272,14 +274,31 @@ function renderIcon(ref: IconRef | undefined): React.ReactNode | undefined {
272
274
 
273
275
  // Cache lazy custom panel components by their loader so React.lazy isn't
274
276
  // re-invoked across renders.
275
- const customPanelCache = new WeakMap<() => Promise<unknown>, ComponentType>()
277
+ const customPanelCache = new WeakMap<() => Promise<unknown>, ComponentType<{ node: AnyNode }>>()
276
278
 
277
- function resolveCustomPanel(loader: () => Promise<{ default: ComponentType<any> }>): ComponentType {
279
+ function resolveCustomPanel(
280
+ loader: () => Promise<{ default: ComponentType<any> }>,
281
+ ): ComponentType<{ node: AnyNode }> {
278
282
  const cached = customPanelCache.get(loader)
279
283
  if (cached) return cached
280
284
  const Comp = lazy(loader)
281
- customPanelCache.set(loader, Comp as ComponentType)
282
- return Comp as ComponentType
285
+ customPanelCache.set(loader, Comp as ComponentType<{ node: AnyNode }>)
286
+ return Comp as ComponentType<{ node: AnyNode }>
287
+ }
288
+
289
+ // Subscribe to the full node only where the custom panel contract needs it.
290
+ // Keeping this below ParametricInspector preserves the inspector's narrow
291
+ // per-field subscriptions while ensuring lazy panels receive their live node.
292
+ function CustomPanelSlot({
293
+ Component,
294
+ nodeId,
295
+ }: {
296
+ Component: ComponentType<{ node: AnyNode }>
297
+ nodeId: AnyNodeId
298
+ }) {
299
+ const node = useScene((s) => s.nodes[nodeId])
300
+ if (!node) return null
301
+ return <Component node={node as AnyNode} />
283
302
  }
284
303
 
285
304
  // ─── Per-field renderers ─────────────────────────────────────────────
@@ -311,136 +330,11 @@ function FieldRenderer({ field, nodeId, onUpdate }: FieldRendererProps) {
311
330
  })
312
331
  if (!visible) return null
313
332
 
314
- switch (field.kind) {
315
- case 'number': {
316
- const num = typeof value === 'number' ? value : 0
317
- const step = field.step ?? 0.01
318
- const precision = precisionForStep(step)
319
- return (
320
- <SliderControl
321
- label={prettifyKey(key)}
322
- max={field.max}
323
- min={field.min}
324
- onChange={(next) => onUpdate({ [key]: next } as Partial<AnyNode>)}
325
- precision={precision}
326
- step={step}
327
- unit={field.unit ?? ''}
328
- value={num}
329
- />
330
- )
331
- }
332
-
333
- case 'boolean': {
334
- const checked = value === true
335
- return (
336
- <ToggleControl
337
- checked={checked}
338
- label={prettifyKey(key)}
339
- onChange={(next) => onUpdate({ [key]: next } as Partial<AnyNode>)}
340
- />
341
- )
342
- }
343
-
344
- case 'enum': {
345
- const str = typeof value === 'string' ? value : (field.options[0] ?? '')
346
- if (field.display === 'segmented') {
347
- return (
348
- <SegmentedControl
349
- onChange={(next) => onUpdate({ [key]: next } as Partial<AnyNode>)}
350
- options={field.options.map((opt) => ({ label: prettifyEnumValue(opt), value: opt }))}
351
- value={str}
352
- />
353
- )
354
- }
355
- return (
356
- <div className="flex items-center justify-between px-3 py-2">
357
- <span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
358
- <select
359
- className="rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
360
- onChange={(e) => onUpdate({ [key]: e.target.value } as Partial<AnyNode>)}
361
- value={str}
362
- >
363
- {field.options.map((opt) => (
364
- <option key={opt} value={opt}>
365
- {prettifyEnumValue(opt)}
366
- </option>
367
- ))}
368
- </select>
369
- </div>
370
- )
371
- }
372
-
373
- case 'color': {
374
- const str = typeof value === 'string' ? value : '#888888'
375
- return (
376
- <div className="flex items-center justify-between px-3 py-2">
377
- <span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
378
- <div className="flex items-center gap-2">
379
- <input
380
- className="h-6 w-8 cursor-pointer rounded border border-border/50 bg-transparent"
381
- onChange={(e) => onUpdate({ [key]: e.target.value } as Partial<AnyNode>)}
382
- type="color"
383
- value={str}
384
- />
385
- <input
386
- className="w-20 rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
387
- onChange={(e) => onUpdate({ [key]: e.target.value } as Partial<AnyNode>)}
388
- type="text"
389
- value={str}
390
- />
391
- </div>
392
- </div>
393
- )
394
- }
395
-
396
- case 'vec3': {
397
- const v = Array.isArray(value) && value.length >= 3
398
- ? (value as [number, number, number])
399
- : [0, 0, 0]
400
- const axes: Array<{ label: string; index: 0 | 1 | 2 }> = [
401
- { label: 'X', index: 0 },
402
- { label: 'Y', index: 1 },
403
- { label: 'Z', index: 2 },
404
- ]
405
- return (
406
- <>
407
- {axes.map(({ label, index }) => {
408
- // v is a [number, number, number] tuple; the explicit local
409
- // resolves TS's noUncheckedIndexedAccess concern that v[index]
410
- // could be undefined.
411
- const axisValue = v[index] ?? 0
412
- return (
413
- <SliderControl
414
- key={`${key}-${label}`}
415
- label={label}
416
- max={axisValue + 5}
417
- min={axisValue - 5}
418
- onChange={(next) => {
419
- const updated = [...v] as [number, number, number]
420
- updated[index] = next
421
- onUpdate({ [key]: updated } as Partial<AnyNode>)
422
- }}
423
- precision={2}
424
- step={0.05}
425
- unit="m"
426
- value={Math.round(axisValue * 100) / 100}
427
- />
428
- )
429
- })}
430
- </>
431
- )
432
- }
433
-
434
- case 'custom':
435
- // The field owns its rendering and update logic — used for
436
- // derived values (length from start/end), dynamic-bounded
437
- // sliders (curve sagitta), composed editors.
438
- return <CustomFieldRenderer Comp={field.component} nodeId={nodeId} onUpdate={onUpdate} />
439
-
440
- default:
441
- // material / ref / unrecognized kinds — not implemented in v1.
442
- return null
333
+ if (field.kind === 'custom') {
334
+ return <CustomFieldRenderer Comp={field.component} nodeId={nodeId} onUpdate={onUpdate} />
443
335
  }
336
+
337
+ return <ParametricFieldControl field={field} onChange={onUpdate} value={value} />
444
338
  }
445
339
 
446
340
  function CustomFieldRenderer({
@@ -459,26 +353,3 @@ function CustomFieldRenderer({
459
353
  if (!node) return null
460
354
  return <Comp node={node} onUpdate={onUpdate} />
461
355
  }
462
-
463
- // ─── helpers ─────────────────────────────────────────────────────────
464
-
465
- function precisionForStep(step: number): number {
466
- if (step <= 0) return 0
467
- return Math.max(0, Math.ceil(-Math.log10(step)))
468
- }
469
-
470
- function prettifyKey(key: string): string {
471
- // 'bracketStyle' → 'Bracket style'
472
- const spaced = key.replace(/([A-Z])/g, ' $1').toLowerCase()
473
- return spaced.charAt(0).toUpperCase() + spaced.slice(1)
474
- }
475
-
476
- function prettifyEnumValue(value: string): string {
477
- // 'minimal' → 'Minimal'; 'roof-segment' → 'Roof segment'
478
- return value
479
- .split(/[-_\s]/)
480
- .map((word, i) =>
481
- i === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word.toLowerCase(),
482
- )
483
- .join(' ')
484
- }
@@ -13,6 +13,7 @@ import {
13
13
  EyeOff,
14
14
  LocateFixed,
15
15
  Lock,
16
+ Move,
16
17
  RotateCcw,
17
18
  Ruler,
18
19
  Trash2,
@@ -144,6 +145,12 @@ export function ReferencePanel() {
144
145
  guideEmitter.emit('guide:cancel-reference-scale')
145
146
  }, [])
146
147
 
148
+ const handleMoveScan = useCallback(() => {
149
+ if (node?.type !== 'scan') return
150
+ useEditor.getState().setMovingNode(node as never)
151
+ setSelectedReferenceId(null)
152
+ }, [node, setSelectedReferenceId])
153
+
147
154
  useEffect(() => {
148
155
  if (node?.type !== 'guide' || !node.url.startsWith('asset://')) {
149
156
  setIsAssetMissing(false)
@@ -172,7 +179,7 @@ export function ReferencePanel() {
172
179
  return (
173
180
  <PanelWrapper
174
181
  onClose={handleClose}
175
- title={node.name || (isScan ? '3D Scan' : 'Guide Image')}
182
+ title={node.name || (isScan ? 'Capture' : 'Guide Image')}
176
183
  width={300}
177
184
  >
178
185
  {!isScan && (
@@ -329,6 +336,29 @@ export function ReferencePanel() {
329
336
  </>
330
337
  )}
331
338
 
339
+ {isScan && (
340
+ <PanelSection title="Capture">
341
+ <ActionGroup>
342
+ <ActionButton
343
+ icon={<Move className="h-3.5 w-3.5" />}
344
+ label="Move"
345
+ onClick={handleMoveScan}
346
+ />
347
+ <ActionButton
348
+ icon={
349
+ node.visible === false ? (
350
+ <EyeOff className="h-3.5 w-3.5" />
351
+ ) : (
352
+ <Eye className="h-3.5 w-3.5" />
353
+ )
354
+ }
355
+ label={node.visible === false ? 'Show' : 'Hide'}
356
+ onClick={() => handleUpdate({ visible: node.visible === false })}
357
+ />
358
+ </ActionGroup>
359
+ </PanelSection>
360
+ )}
361
+
332
362
  <PanelSection title="Position">
333
363
  <SliderControl
334
364
  label={
@@ -346,7 +376,7 @@ export function ReferencePanel() {
346
376
  precision={2}
347
377
  step={0.1}
348
378
  unit="m"
349
- value={Math.round(node.position[0] * 100) / 100}
379
+ value={node.position[0]}
350
380
  />
351
381
  <SliderControl
352
382
  label={
@@ -364,7 +394,7 @@ export function ReferencePanel() {
364
394
  precision={2}
365
395
  step={0.1}
366
396
  unit="m"
367
- value={Math.round(node.position[1] * 100) / 100}
397
+ value={node.position[1]}
368
398
  />
369
399
  <SliderControl
370
400
  label={
@@ -382,7 +412,7 @@ export function ReferencePanel() {
382
412
  precision={2}
383
413
  step={0.1}
384
414
  unit="m"
385
- value={Math.round(node.position[2] * 100) / 100}
415
+ value={node.position[2]}
386
416
  />
387
417
  </PanelSection>
388
418
 
@@ -0,0 +1,34 @@
1
+ 'use client'
2
+
3
+ import { Check } from 'lucide-react'
4
+ import type { ButtonHTMLAttributes } from 'react'
5
+ import { cn } from './../../../lib/utils'
6
+
7
+ interface CheckboxProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onChange'> {
8
+ checked: boolean
9
+ onCheckedChange: (checked: boolean) => void
10
+ }
11
+
12
+ export function Checkbox({ checked, onCheckedChange, className, ...props }: CheckboxProps) {
13
+ return (
14
+ <button
15
+ aria-checked={checked}
16
+ className={cn(
17
+ 'flex h-3.5 w-3.5 shrink-0 cursor-pointer items-center justify-center rounded-[4px] border transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring',
18
+ checked
19
+ ? 'border-primary bg-primary text-primary-foreground'
20
+ : 'border-border bg-background/60 text-transparent hover:border-foreground/40',
21
+ className,
22
+ )}
23
+ role="checkbox"
24
+ type="button"
25
+ {...props}
26
+ onClick={(event) => {
27
+ event.stopPropagation()
28
+ onCheckedChange(!checked)
29
+ }}
30
+ >
31
+ <Check className="h-2.5 w-2.5" strokeWidth={3} />
32
+ </button>
33
+ )
34
+ }
@@ -33,6 +33,28 @@ const COMMAND_VALUES = new Set(['Cmd/Ctrl', 'Cmd', 'Command', 'Meta'])
33
33
  const IS_MAC =
34
34
  typeof navigator !== 'undefined' && navigator.platform.toUpperCase().includes('MAC')
35
35
 
36
+ // Keys whose printed name and their symbol differ. Kept beside the token rather
37
+ // than in any one consumer so every surface that prints a shortcut — the
38
+ // Keyboard Shortcuts dialog, the community getting-started guide — resolves the
39
+ // same glyph for the same key.
40
+ //
41
+ // Only symbols people actually read are listed. The arrows are unambiguous, and
42
+ // ⌘ is printed on the key it means. The rest are not: ⎋, ␣ and ⌫ are typographic
43
+ // conventions that no keyboard prints, so they read as "some symbol" rather than
44
+ // as Escape, Space and Delete. Those spell their names out instead — and for
45
+ // Delete, the name the current platform actually puts on the keycap.
46
+ const KEY_DISPLAY_MAP: Record<string, string> = {
47
+ 'Arrow Down': '↓',
48
+ 'Arrow Up': '↑',
49
+ }
50
+
51
+ /** The glyph to print for a shortcut key on the current platform. */
52
+ function shortcutDisplayValue(value: string): string {
53
+ if (COMMAND_VALUES.has(value)) return IS_MAC ? '⌘' : 'Ctrl'
54
+ if (value === 'Delete / Backspace') return IS_MAC ? 'Delete' : 'Backspace'
55
+ return KEY_DISPLAY_MAP[value] ?? value
56
+ }
57
+
36
58
  type ShortcutTokenProps = React.ComponentProps<'kbd'> & {
37
59
  value: string
38
60
  displayValue?: string
@@ -97,4 +119,4 @@ function ShortcutToken({ className, displayValue, value, ...props }: ShortcutTok
97
119
  )
98
120
  }
99
121
 
100
- export { ShortcutToken }
122
+ export { ShortcutToken, shortcutDisplayValue }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect, useState } from 'react'
4
4
  import { cn } from '../../lib/utils'
5
+ import { Button } from './primitives/button'
5
6
 
6
7
  const LOADERS = [
7
8
  'pascal-loader-1',
@@ -36,3 +37,37 @@ export function SceneLoader({ className, fullScreen = false }: SceneLoaderProps)
36
37
  </div>
37
38
  )
38
39
  }
40
+
41
+ interface SceneLoadFailedProps {
42
+ className?: string
43
+ onRetry: () => void
44
+ }
45
+
46
+ /**
47
+ * Replaces the loader when the host could not deliver the scene. Rendered
48
+ * INSTEAD of falling back to an empty default scene: a session that shows
49
+ * scaffold nodes after a failed load autosaves that scaffold over the real
50
+ * project (prod scene-wipe class, 2026-09-02).
51
+ */
52
+ export function SceneLoadFailed({ className, onRetry }: SceneLoadFailedProps) {
53
+ return (
54
+ <div
55
+ className={cn(
56
+ 'z-100 flex flex-col items-center justify-center gap-4 bg-background/90 px-6 text-center backdrop-blur-md',
57
+ 'absolute inset-0',
58
+ className,
59
+ )}
60
+ role="alert"
61
+ >
62
+ <div className="flex flex-col gap-1">
63
+ <p className="font-medium text-foreground text-sm">This project couldn't be loaded</p>
64
+ <p className="text-muted-foreground text-sm">
65
+ Nothing was changed. Check your connection and try again.
66
+ </p>
67
+ </div>
68
+ <Button className="rounded-full" onClick={onRetry} size="sm" type="button">
69
+ Try again
70
+ </Button>
71
+ </div>
72
+ )
73
+ }