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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +15 -8
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
  6. package/src/components/editor/bake-exporter.tsx +1 -0
  7. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  8. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  9. package/src/components/editor/capture-camera-rig.tsx +59 -0
  10. package/src/components/editor/custom-camera-controls.tsx +20 -11
  11. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  12. package/src/components/editor/editor-layout-v2.tsx +17 -5
  13. package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
  14. package/src/components/editor/export-manager.tsx +175 -33
  15. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  16. package/src/components/editor/first-person-controls.tsx +182 -57
  17. package/src/components/editor/floating-action-menu.tsx +55 -55
  18. package/src/components/editor/floorplan-background-selection.test.ts +31 -4
  19. package/src/components/editor/floorplan-background-selection.ts +27 -7
  20. package/src/components/editor/floorplan-panel.tsx +81 -57
  21. package/src/components/editor/grid.tsx +12 -7
  22. package/src/components/editor/group-actions.ts +34 -50
  23. package/src/components/editor/group-floating-action-menu.tsx +30 -15
  24. package/src/components/editor/group-move-3d.ts +51 -21
  25. package/src/components/editor/group-transform-shared.test.ts +78 -0
  26. package/src/components/editor/group-transform-shared.ts +74 -7
  27. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  28. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  29. package/src/components/editor/handles/linear-resize-drag.ts +67 -0
  30. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  31. package/src/components/editor/handles/resize-snap.ts +10 -1
  32. package/src/components/editor/handles/use-handle-drag.ts +18 -1
  33. package/src/components/editor/index.tsx +252 -46
  34. package/src/components/editor/measurement-pill.tsx +12 -12
  35. package/src/components/editor/node-action-menu.tsx +42 -1
  36. package/src/components/editor/node-arrow-handles.tsx +47 -58
  37. package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
  38. package/src/components/editor/selection-manager.tsx +132 -47
  39. package/src/components/editor/site-edge-labels.tsx +2 -1
  40. package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
  41. package/src/components/editor/snapshot-capture.test.ts +422 -0
  42. package/src/components/editor/snapshot-capture.ts +145 -0
  43. package/src/components/editor/thumbnail-generator.tsx +295 -232
  44. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  45. package/src/components/editor/wall-measurement-label.tsx +3 -2
  46. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  47. package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
  48. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  49. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  50. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  51. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  52. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  53. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  54. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
  57. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  59. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  60. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  61. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  62. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  63. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  64. package/src/components/systems/selection-affordance-services.ts +19 -0
  65. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  66. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  67. package/src/components/tools/fence/fence-drafting.ts +6 -25
  68. package/src/components/tools/item/draft-creation.test.ts +12 -0
  69. package/src/components/tools/item/draft-creation.ts +10 -0
  70. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  71. package/src/components/tools/item/face-host-commit.ts +56 -0
  72. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  73. package/src/components/tools/item/face-host-preview.ts +82 -0
  74. package/src/components/tools/item/move-tool.tsx +4 -3
  75. package/src/components/tools/item/placement-math.test.ts +11 -1
  76. package/src/components/tools/item/placement-math.ts +0 -20
  77. package/src/components/tools/item/placement-strategies.test.ts +494 -0
  78. package/src/components/tools/item/placement-strategies.ts +192 -24
  79. package/src/components/tools/item/placement-surface.test.ts +61 -0
  80. package/src/components/tools/item/placement-surface.ts +24 -0
  81. package/src/components/tools/item/placement-types.ts +6 -0
  82. package/src/components/tools/item/test-face-host.ts +121 -0
  83. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  84. package/src/components/tools/item/use-draft-node.ts +26 -4
  85. package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
  86. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  87. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  88. package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
  89. package/src/components/tools/registry-tool-context.tsx +30 -0
  90. package/src/components/tools/select/box-select-tool.tsx +3 -1
  91. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  92. package/src/components/tools/select/marquee-footprint.ts +29 -0
  93. package/src/components/tools/shared/placement-box.tsx +63 -8
  94. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  95. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  96. package/src/components/tools/shared/pointer-support-cap.ts +45 -8
  97. package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
  98. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  99. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  100. package/src/components/tools/stair/stair-tool.tsx +127 -74
  101. package/src/components/tools/tool-manager.tsx +25 -8
  102. package/src/components/tools/wall/wall-drafting.test.ts +527 -4
  103. package/src/components/tools/wall/wall-drafting.ts +55 -462
  104. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  105. package/src/components/ui/action-menu/index.tsx +11 -1
  106. package/src/components/ui/command-palette/editor-commands.tsx +12 -13
  107. package/src/components/ui/command-palette/index.tsx +7 -2
  108. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  109. package/src/components/ui/controls/material-picker.tsx +4 -4
  110. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  111. package/src/components/ui/controls/segmented-control.tsx +8 -1
  112. package/src/components/ui/controls/slider-control.tsx +19 -3
  113. package/src/components/ui/controls/toggle-control.tsx +18 -5
  114. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  115. package/src/components/ui/floating-level-selector.tsx +23 -6
  116. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  117. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  118. package/src/components/ui/helpers/item-helper.tsx +1 -1
  119. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  120. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  121. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  122. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  123. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  124. package/src/components/ui/panels/multi-field-value.ts +163 -0
  125. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  126. package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
  127. package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
  128. package/src/components/ui/panels/node-display.ts +4 -0
  129. package/src/components/ui/panels/panel-manager.tsx +77 -3
  130. package/src/components/ui/panels/panel-wrapper.tsx +228 -12
  131. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  132. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  133. package/src/components/ui/panels/parametric-inspector.tsx +29 -162
  134. package/src/components/ui/panels/reference-panel.tsx +31 -1
  135. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  136. package/src/components/ui/scene-loader.tsx +35 -0
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
  138. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
  139. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
  140. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
  142. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  143. package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
  144. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  145. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  146. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  147. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
  148. package/src/components/ui/sidebar/tab-bar.tsx +9 -1
  149. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  150. package/src/components/viewer/floorplan-compass-button.tsx +50 -0
  151. package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
  152. package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
  153. package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
  154. package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
  155. package/src/components/viewer/floorplan-preview.tsx +1069 -0
  156. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
  157. package/src/components/viewer/viewer-scene-header.tsx +2 -1
  158. package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
  159. package/src/components/viewer/viewer-stage-modes.ts +49 -0
  160. package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
  161. package/src/components/viewer/viewer-stage.test.tsx +47 -0
  162. package/src/components/viewer/viewer-stage.tsx +221 -0
  163. package/src/components/viewer-overlay.tsx +13 -30
  164. package/src/components/viewer-zone-system.tsx +6 -2
  165. package/src/components/walkthrough-hud.tsx +19 -10
  166. package/src/hooks/use-auto-save.test.ts +122 -1
  167. package/src/hooks/use-auto-save.ts +119 -13
  168. package/src/hooks/use-ceiling-events.test.ts +122 -0
  169. package/src/hooks/use-drag-action.ts +17 -3
  170. package/src/hooks/use-grid-events.test.ts +240 -0
  171. package/src/hooks/use-grid-events.ts +190 -11
  172. package/src/hooks/use-keyboard.test.ts +244 -0
  173. package/src/hooks/use-keyboard.ts +164 -63
  174. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  175. package/src/hooks/use-save-shortcut.ts +33 -0
  176. package/src/index.tsx +109 -4
  177. package/src/lib/active-placement-surface.test.ts +16 -0
  178. package/src/lib/active-placement-surface.ts +14 -0
  179. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  180. package/src/lib/ceiling-surface-raycast.ts +12 -0
  181. package/src/lib/contextual-help-extension.ts +17 -0
  182. package/src/lib/contextual-help.test.ts +8 -0
  183. package/src/lib/contextual-help.ts +8 -0
  184. package/src/lib/continuation.test.ts +11 -0
  185. package/src/lib/continuation.ts +8 -1
  186. package/src/lib/direct-manipulation.test.ts +132 -0
  187. package/src/lib/direct-manipulation.ts +38 -1
  188. package/src/lib/editor-api.ts +6 -3
  189. package/src/lib/elevation-guides.test.ts +28 -1
  190. package/src/lib/elevation-guides.ts +22 -7
  191. package/src/lib/floating-menu-scale.test.ts +29 -0
  192. package/src/lib/floating-menu-scale.ts +14 -0
  193. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  194. package/src/lib/floorplan/apply-alignment.ts +10 -1
  195. package/src/lib/floorplan/floorplan-export.test.ts +397 -1
  196. package/src/lib/floorplan/floorplan-export.tsx +111 -16
  197. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  198. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  199. package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
  200. package/src/lib/floorplan/floorplan-readonly.ts +85 -0
  201. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  202. package/src/lib/floorplan-grid-event-point.ts +13 -0
  203. package/src/lib/glb-export.test.ts +1169 -21
  204. package/src/lib/glb-export.ts +629 -101
  205. package/src/lib/grid-event-presentation.ts +16 -0
  206. package/src/lib/history.test.ts +523 -77
  207. package/src/lib/history.ts +133 -14
  208. package/src/lib/host-tree-children.test.ts +27 -0
  209. package/src/lib/host-tree-children.ts +79 -0
  210. package/src/lib/inspector-card-mode.test.ts +92 -0
  211. package/src/lib/inspector-card-mode.ts +60 -0
  212. package/src/lib/interaction/hot-set.test.ts +10 -1
  213. package/src/lib/interaction/hot-set.ts +3 -1
  214. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  215. package/src/lib/interaction/overlay-policy.ts +16 -0
  216. package/src/lib/interaction/registered-drafting.ts +36 -0
  217. package/src/lib/interaction/scope.ts +31 -0
  218. package/src/lib/level-print-export.test.ts +484 -0
  219. package/src/lib/level-print-export.ts +526 -0
  220. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  221. package/src/lib/local-project-presentation-persistence.ts +335 -0
  222. package/src/lib/material-paint.ts +2 -0
  223. package/src/lib/measurement-parser.ts +3 -0
  224. package/src/lib/measurements.test.ts +4 -0
  225. package/src/lib/model-export.ts +29 -0
  226. package/src/lib/paint-preview-owner.ts +71 -0
  227. package/src/lib/paint-scope.test.ts +58 -2
  228. package/src/lib/paint-scope.ts +8 -2
  229. package/src/lib/planar-cursor-placement.test.ts +178 -1
  230. package/src/lib/planar-cursor-placement.ts +61 -5
  231. package/src/lib/plugin-panels.test.ts +92 -1
  232. package/src/lib/plugin-panels.ts +48 -0
  233. package/src/lib/portable-export.test.ts +366 -0
  234. package/src/lib/portable-export.ts +969 -0
  235. package/src/lib/print-3mf.test.ts +86 -0
  236. package/src/lib/print-3mf.ts +161 -0
  237. package/src/lib/print-content-scope.test.ts +107 -0
  238. package/src/lib/print-content-scope.ts +90 -0
  239. package/src/lib/print-export.test.ts +294 -0
  240. package/src/lib/print-export.ts +735 -0
  241. package/src/lib/print-feature-thickness.test.ts +147 -0
  242. package/src/lib/print-feature-thickness.ts +180 -0
  243. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  244. package/src/lib/print-golden-house.test.ts +249 -0
  245. package/src/lib/print-roof-solids.test.ts +133 -0
  246. package/src/lib/print-roof-solids.ts +630 -0
  247. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  248. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  249. package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
  250. package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
  251. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  252. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  253. package/src/lib/print-shell-compiler-protocol.ts +30 -0
  254. package/src/lib/print-shell-compiler.ts +273 -0
  255. package/src/lib/print-wall-solids.test.ts +194 -0
  256. package/src/lib/print-wall-solids.ts +328 -0
  257. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  258. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  259. package/src/lib/scene.ts +22 -8
  260. package/src/lib/selection-routing.test.ts +125 -6
  261. package/src/lib/selection-routing.ts +34 -1
  262. package/src/lib/session-groups.test.ts +88 -0
  263. package/src/lib/session-groups.ts +201 -0
  264. package/src/lib/sfx-bus.ts +4 -0
  265. package/src/lib/sfx-player.ts +8 -0
  266. package/src/lib/snapping-mode.test.ts +32 -4
  267. package/src/lib/snapping-mode.ts +11 -7
  268. package/src/lib/structured-clone-fallback.test.ts +45 -0
  269. package/src/lib/structured-clone-fallback.ts +24 -0
  270. package/src/lib/usdz-export.ts +38 -0
  271. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  272. package/src/store/live-draft-preview-stores.test.ts +3 -0
  273. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  274. package/src/store/tool-mode.test.ts +87 -0
  275. package/src/store/use-camera-hint-focus.ts +48 -0
  276. package/src/store/use-editor.tsx +299 -188
  277. package/src/store/use-floorplan-draft-preview.ts +9 -0
  278. package/src/store/use-interaction-scope.test.ts +23 -1
  279. package/src/store/use-interaction-scope.ts +63 -2
  280. package/src/store/use-placement-preview.ts +56 -3
  281. package/src/store/use-session-groups.ts +103 -0
  282. package/src/components/tools/roof/roof-tool.tsx +0 -656
  283. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -0,0 +1,173 @@
1
+ 'use client'
2
+
3
+ import type { AnyNode, ParamField } from '@pascal-app/core'
4
+ import { SegmentedControl } from '../controls/segmented-control'
5
+ import { SliderControl } from '../controls/slider-control'
6
+ import { ToggleControl } from '../controls/toggle-control'
7
+ import { precisionForStep, prettifyEnumValue, prettifyKey } from './parametric-field-utils'
8
+
9
+ interface ParametricFieldControlProps {
10
+ field: ParamField<AnyNode>
11
+ value: unknown
12
+ mixed?: boolean
13
+ onChange: (patch: Partial<AnyNode>) => void
14
+ onCommit?: (patch: Partial<AnyNode>) => void
15
+ }
16
+
17
+ export function ParametricFieldControl({
18
+ field,
19
+ value,
20
+ mixed = false,
21
+ onChange,
22
+ onCommit,
23
+ }: ParametricFieldControlProps) {
24
+ const key = String(field.key)
25
+
26
+ switch (field.kind) {
27
+ case 'number': {
28
+ const num = typeof value === 'number' ? value : 0
29
+ const step = field.step ?? 0.01
30
+ const precision = precisionForStep(step)
31
+ return (
32
+ <SliderControl
33
+ label={prettifyKey(key)}
34
+ max={field.max}
35
+ min={field.min}
36
+ mixed={mixed}
37
+ onChange={(next) => onChange({ [key]: next } as Partial<AnyNode>)}
38
+ onCommit={onCommit ? (next) => onCommit({ [key]: next } as Partial<AnyNode>) : undefined}
39
+ precision={precision}
40
+ restoreOnCommit={!onCommit}
41
+ step={step}
42
+ unit={field.unit ?? ''}
43
+ value={num}
44
+ />
45
+ )
46
+ }
47
+
48
+ case 'boolean': {
49
+ const checked = !mixed && value === true
50
+ return (
51
+ <ToggleControl
52
+ checked={checked}
53
+ label={prettifyKey(key)}
54
+ mixed={mixed}
55
+ onChange={(next) => {
56
+ const patch = { [key]: next } as Partial<AnyNode>
57
+ onChange(patch)
58
+ onCommit?.(patch)
59
+ }}
60
+ />
61
+ )
62
+ }
63
+
64
+ case 'enum': {
65
+ const str = typeof value === 'string' ? value : (field.options[0] ?? '')
66
+ const apply = (next: string) => {
67
+ const patch = { [key]: next } as Partial<AnyNode>
68
+ onChange(patch)
69
+ onCommit?.(patch)
70
+ }
71
+ if (field.display === 'segmented') {
72
+ return (
73
+ <SegmentedControl
74
+ mixed={mixed}
75
+ onChange={apply}
76
+ options={field.options.map((opt) => ({ label: prettifyEnumValue(opt), value: opt }))}
77
+ value={str}
78
+ />
79
+ )
80
+ }
81
+ return (
82
+ <div className="flex items-center justify-between px-3 py-2">
83
+ <span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
84
+ <select
85
+ 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"
86
+ onChange={(e) => apply(e.target.value)}
87
+ value={mixed ? '' : str}
88
+ >
89
+ {mixed && (
90
+ <option disabled value="">
91
+ Mixed
92
+ </option>
93
+ )}
94
+ {field.options.map((opt) => (
95
+ <option key={opt} value={opt}>
96
+ {prettifyEnumValue(opt)}
97
+ </option>
98
+ ))}
99
+ </select>
100
+ </div>
101
+ )
102
+ }
103
+
104
+ case 'color': {
105
+ const str = typeof value === 'string' ? value : '#888888'
106
+ const apply = (next: string) => {
107
+ const patch = { [key]: next } as Partial<AnyNode>
108
+ onChange(patch)
109
+ onCommit?.(patch)
110
+ }
111
+ return (
112
+ <div className="flex items-center justify-between px-3 py-2">
113
+ <span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
114
+ <div className="flex items-center gap-2">
115
+ <input
116
+ className="h-6 w-8 cursor-pointer rounded border border-border/50 bg-transparent"
117
+ onChange={(e) => apply(e.target.value)}
118
+ type="color"
119
+ value={str}
120
+ />
121
+ <input
122
+ 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"
123
+ onChange={(e) => apply(e.target.value)}
124
+ type="text"
125
+ value={mixed ? 'Mixed' : str}
126
+ />
127
+ </div>
128
+ </div>
129
+ )
130
+ }
131
+
132
+ case 'vec3': {
133
+ const v =
134
+ Array.isArray(value) && value.length >= 3
135
+ ? (value as [number, number, number])
136
+ : ([0, 0, 0] as [number, number, number])
137
+ const axes: Array<{ label: string; index: 0 | 1 | 2 }> = [
138
+ { label: 'X', index: 0 },
139
+ { label: 'Y', index: 1 },
140
+ { label: 'Z', index: 2 },
141
+ ]
142
+ return (
143
+ <>
144
+ {axes.map(({ label, index }) => {
145
+ const axisValue = v[index] ?? 0
146
+ const apply = (next: number): Partial<AnyNode> => {
147
+ const updated = [...v] as [number, number, number]
148
+ updated[index] = next
149
+ return { [key]: updated } as Partial<AnyNode>
150
+ }
151
+ return (
152
+ <SliderControl
153
+ key={`${key}-${label}`}
154
+ label={label}
155
+ mixed={mixed}
156
+ onChange={(next) => onChange(apply(next))}
157
+ onCommit={onCommit ? (next) => onCommit(apply(next)) : undefined}
158
+ precision={2}
159
+ restoreOnCommit={!onCommit}
160
+ step={0.05}
161
+ unit="m"
162
+ value={Math.round(axisValue * 100) / 100}
163
+ />
164
+ )
165
+ })}
166
+ </>
167
+ )
168
+ }
169
+
170
+ default:
171
+ return null
172
+ }
173
+ }
@@ -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
  )
@@ -172,7 +170,7 @@ export function ParametricInspector({
172
170
  ))}
173
171
  {TrailingSection && (
174
172
  <Suspense fallback={null}>
175
- <TrailingSection />
173
+ <CustomPanelSlot Component={TrailingSection} nodeId={selectedId} />
176
174
  </Suspense>
177
175
  )}
178
176
  {(canMove || canDelete || (parametrics.actions && parametrics.actions.length > 0)) && (
@@ -272,14 +270,31 @@ function renderIcon(ref: IconRef | undefined): React.ReactNode | undefined {
272
270
 
273
271
  // Cache lazy custom panel components by their loader so React.lazy isn't
274
272
  // re-invoked across renders.
275
- const customPanelCache = new WeakMap<() => Promise<unknown>, ComponentType>()
273
+ const customPanelCache = new WeakMap<() => Promise<unknown>, ComponentType<{ node: AnyNode }>>()
276
274
 
277
- function resolveCustomPanel(loader: () => Promise<{ default: ComponentType<any> }>): ComponentType {
275
+ function resolveCustomPanel(
276
+ loader: () => Promise<{ default: ComponentType<any> }>,
277
+ ): ComponentType<{ node: AnyNode }> {
278
278
  const cached = customPanelCache.get(loader)
279
279
  if (cached) return cached
280
280
  const Comp = lazy(loader)
281
- customPanelCache.set(loader, Comp as ComponentType)
282
- return Comp as ComponentType
281
+ customPanelCache.set(loader, Comp as ComponentType<{ node: AnyNode }>)
282
+ return Comp as ComponentType<{ node: AnyNode }>
283
+ }
284
+
285
+ // Subscribe to the full node only where the custom panel contract needs it.
286
+ // Keeping this below ParametricInspector preserves the inspector's narrow
287
+ // per-field subscriptions while ensuring lazy panels receive their live node.
288
+ function CustomPanelSlot({
289
+ Component,
290
+ nodeId,
291
+ }: {
292
+ Component: ComponentType<{ node: AnyNode }>
293
+ nodeId: AnyNodeId
294
+ }) {
295
+ const node = useScene((s) => s.nodes[nodeId])
296
+ if (!node) return null
297
+ return <Component node={node as AnyNode} />
283
298
  }
284
299
 
285
300
  // ─── Per-field renderers ─────────────────────────────────────────────
@@ -311,136 +326,11 @@ function FieldRenderer({ field, nodeId, onUpdate }: FieldRendererProps) {
311
326
  })
312
327
  if (!visible) return null
313
328
 
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
329
+ if (field.kind === 'custom') {
330
+ return <CustomFieldRenderer Comp={field.component} nodeId={nodeId} onUpdate={onUpdate} />
443
331
  }
332
+
333
+ return <ParametricFieldControl field={field} onChange={onUpdate} value={value} />
444
334
  }
445
335
 
446
336
  function CustomFieldRenderer({
@@ -459,26 +349,3 @@ function CustomFieldRenderer({
459
349
  if (!node) return null
460
350
  return <Comp node={node} onUpdate={onUpdate} />
461
351
  }
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={
@@ -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
+ }
@@ -118,9 +118,14 @@ export function FunctionTreePanel({
118
118
  <div className="flex h-full flex-col">
119
119
  {/* Root nodes as a category grid — icon when available, otherwise a
120
120
  two-letter abbreviation, with the full name in a hover tooltip.
121
- Mirrors the Build tab's tile grid so the two panels read the same. */}
121
+ Mirrors the Build tab's tile grid so the two panels read the same.
122
+
123
+ `functionTree` is embedder-supplied and unbounded, and these tiles are
124
+ `aspect-square`, so row count grows with it while the item list below is
125
+ the only scroller. Without the cap a large taxonomy pushes the list out
126
+ of the panel entirely and nothing can scroll it back. */}
122
127
  <TooltipProvider delayDuration={0} disableHoverableContent>
123
- <div className="grid shrink-0 grid-cols-5 gap-1.5 border-border/70 border-b p-2">
128
+ <div className="grid max-h-[40%] shrink-0 grid-cols-5 gap-1.5 overflow-y-auto border-border/70 border-b p-2">
124
129
  {functionTree.map((root) => {
125
130
  const isActive = activeRoot?.slug === root.slug
126
131
  return (