@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
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { Icon } from '@iconify/react'
4
- import { Copy, Move, Search, Spline, Trash2 } from 'lucide-react'
4
+ import { Copy, Group, Move, PencilRuler, Search, Spline, Trash2, Ungroup } from 'lucide-react'
5
5
  import type { MouseEventHandler, PointerEventHandler } from 'react'
6
6
 
7
7
  type NodeActionMenuProps = {
@@ -10,7 +10,12 @@ type NodeActionMenuProps = {
10
10
  onDelete?: MouseEventHandler<HTMLButtonElement>
11
11
  onDuplicate?: MouseEventHandler<HTMLButtonElement>
12
12
  onMove?: MouseEventHandler<HTMLButtonElement>
13
+ onEditMesh?: MouseEventHandler<HTMLButtonElement>
13
14
  onCurve?: MouseEventHandler<HTMLButtonElement>
15
+ /** Session group (Ctrl/Cmd+G) — multi-selection floating pill. */
16
+ onGroup?: MouseEventHandler<HTMLButtonElement>
17
+ /** Dissolve session group (Ctrl/Cmd+Shift+G). */
18
+ onUngroup?: MouseEventHandler<HTMLButtonElement>
14
19
  onPointerDown?: PointerEventHandler<HTMLDivElement>
15
20
  onPointerUp?: PointerEventHandler<HTMLDivElement>
16
21
  onPointerEnter?: PointerEventHandler<HTMLDivElement>
@@ -23,7 +28,10 @@ export function NodeActionMenu({
23
28
  onDelete,
24
29
  onDuplicate,
25
30
  onMove,
31
+ onEditMesh,
26
32
  onCurve,
33
+ onGroup,
34
+ onUngroup,
27
35
  onPointerDown,
28
36
  onPointerUp,
29
37
  onPointerEnter,
@@ -59,6 +67,39 @@ export function NodeActionMenu({
59
67
  <Move className="h-4 w-4" />
60
68
  </button>
61
69
  )}
70
+ {onEditMesh && (
71
+ <button
72
+ aria-label="Edit mesh"
73
+ className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
74
+ onClick={onEditMesh}
75
+ title="Edit mesh"
76
+ type="button"
77
+ >
78
+ <PencilRuler className="h-4 w-4" />
79
+ </button>
80
+ )}
81
+ {onGroup && (
82
+ <button
83
+ aria-label="Group selection"
84
+ className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
85
+ onClick={onGroup}
86
+ title="Group (Ctrl/Cmd+G)"
87
+ type="button"
88
+ >
89
+ <Group className="h-4 w-4" />
90
+ </button>
91
+ )}
92
+ {onUngroup && (
93
+ <button
94
+ aria-label="Ungroup selection"
95
+ className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
96
+ onClick={onUngroup}
97
+ title="Ungroup (Ctrl/Cmd+Shift+G)"
98
+ type="button"
99
+ >
100
+ <Ungroup className="h-4 w-4" />
101
+ </button>
102
+ )}
62
103
  {onCurve && (
63
104
  <button
64
105
  aria-label="Curve"
@@ -62,7 +62,7 @@ import {
62
62
  HandleArrow,
63
63
  NO_RAYCAST,
64
64
  } from './handles/handle-arrow'
65
- import { replacePreviewOverrideIds } from './handles/preview-overrides'
65
+ import { createLinearResizeDragBinding } from './handles/linear-resize-drag'
66
66
  import { resolveResizeSnapValue } from './handles/resize-snap'
67
67
  import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
68
68
 
@@ -235,16 +235,15 @@ export function NodeArrowHandles() {
235
235
  typeof def.handles === 'function'
236
236
  ? def.handles(node as never, descriptorSceneApi)
237
237
  : (def.handles as HandleDescriptor[])
238
- // The whole-node move-cross gizmo is gone: moving is now click-to-move on
239
- // the selected node body (see selection-manager). Drop both flavours — the
240
- // `translate` ground cross (column/roof/shelf/spawn) and the `tap-action`
241
- // `move-cross` (item/door/window/elevator/stair) — keep rotate/resize.
242
- return all.filter(
243
- (d) =>
244
- d.kind !== 'translate' &&
245
- !('shape' in d && d.shape === 'move-cross') &&
246
- (d.kind !== 'linear-resize' || d.visible?.(node as never, descriptorSceneApi) !== false),
247
- )
238
+ return all.filter((descriptor) => {
239
+ if (descriptor.kind === 'translate') return false
240
+ const visible =
241
+ 'visible' in descriptor
242
+ ? descriptor.visible?.(node as never, descriptorSceneApi)
243
+ : undefined
244
+ if ('shape' in descriptor && descriptor.shape === 'move-cross') return visible === true
245
+ return visible !== false
246
+ })
248
247
  }, [node, def, descriptorSceneApi])
249
248
 
250
249
  const shouldRender =
@@ -293,18 +292,25 @@ function NodeArrowHandlesForNode({
293
292
  descriptors: HandleDescriptor[]
294
293
  }) {
295
294
  const parentId = node.parentId ?? null
296
- const grandparentId = useScene((state) => {
297
- if (!parentId) return null
298
- const parent = state.nodes[parentId as AnyNodeId]
299
- return parent?.parentId ?? null
300
- })
301
295
 
302
296
  const portalMode: HandlePortal = descriptors.some((d) => d.portal === 'grandparent')
303
297
  ? 'grandparent'
304
298
  : 'parent'
305
299
 
300
+ const portalTargetResolver = descriptors.find(
301
+ (descriptor) => descriptor.portalTarget !== undefined,
302
+ )?.portalTarget
303
+ const descriptorSceneApi = useMemo(() => createSceneApi(useScene), [])
304
+
306
305
  // Portal target: the mesh we createPortal into.
307
- const portalTargetId = portalMode === 'grandparent' ? grandparentId : parentId
306
+ const portalTargetId = useScene((state) => {
307
+ if (portalTargetResolver) {
308
+ return portalTargetResolver(node as never, descriptorSceneApi) ?? null
309
+ }
310
+ const parentId = node.parentId ?? null
311
+ if (!parentId || portalMode === 'parent') return parentId
312
+ return state.nodes[parentId as AnyNodeId]?.parentId ?? null
313
+ })
308
314
  // Outer wrapper mirrors this mesh's local pose. For 'parent' mode the
309
315
  // outer IS the node (so handles + drag math both live in node-local).
310
316
  // For 'grandparent' the outer rides the parent and an inner group adds
@@ -710,10 +716,17 @@ function LinearArrow({
710
716
  getPointerRay(event.nativeEvent.clientX, event.nativeEvent.clientY, _resizeRay),
711
717
  ) / localToWorldScale
712
718
 
713
- const overrideId =
714
- (descriptor.kind === 'linear-resize'
715
- ? descriptor.overrideTarget?.(initialNode as never, sceneApi)
716
- : undefined) ?? nodeId
719
+ const linearBinding =
720
+ descriptor.kind === 'linear-resize'
721
+ ? createLinearResizeDragBinding({
722
+ descriptor,
723
+ initialNode,
724
+ nodeId,
725
+ sceneApi,
726
+ initialModifiers: { altKey: event.nativeEvent.altKey },
727
+ })
728
+ : null
729
+ const overrideId = linearBinding?.overrideId ?? nodeId
717
730
  const initialValue = descriptor.currentValue(initialNode)
718
731
  const minBound = resolveBound(descriptor.min, Number.NEGATIVE_INFINITY, initialNode, sceneApi)
719
732
  const maxBound = resolveBound(descriptor.max, Number.POSITIVE_INFINITY, initialNode, sceneApi)
@@ -730,14 +743,9 @@ function LinearArrow({
730
743
  // when the (snapped + clamped) value actually changes, so the cue
731
744
  // tracks real size steps instead of every sub-pixel pointer jitter.
732
745
  let lastTickValue = initialValue
733
- let previewOverrideIds = new Set<AnyNodeId>()
734
-
735
746
  return {
736
747
  overrideId,
737
- commit:
738
- descriptor.kind === 'linear-resize' && descriptor.commit
739
- ? (patch) => descriptor.commit?.(initialNode, patch, sceneApi)
740
- : undefined,
748
+ commit: linearBinding?.commit,
741
749
  onBegin: () => {
742
750
  // Always claim the handle-drag scope so the HUD knows a resize is the
743
751
  // active interaction (keeps the idle select hints off-screen). The
@@ -755,12 +763,9 @@ function LinearArrow({
755
763
  descriptor.onDragEnd?.(initialNode as never, sceneApi)
756
764
  }
757
765
  if (onDrag) useOpeningGuides.getState().clear()
758
- for (const previewId of previewOverrideIds) {
759
- useLiveNodeOverrides.getState().clear(previewId)
760
- useScene.getState().markDirty(previewId)
761
- }
766
+ linearBinding?.clearPreview()
762
767
  },
763
- move: ({ event: moveEvent, getPointerRay: getMovePointerRay }) => {
768
+ move: ({ event: moveEvent, modifiers, getPointerRay: getMovePointerRay }) => {
764
769
  const currentPointer =
765
770
  closestAxisParameterToRay(
766
771
  _resizeOriginW,
@@ -772,43 +777,27 @@ function LinearArrow({
772
777
  const linearDescriptor = descriptor.kind === 'linear-resize' ? descriptor : null
773
778
  const snappedNext = resolveResizeSnapValue({
774
779
  rawValue: rawNext,
780
+ fallbackValue: lastTickValue,
775
781
  gridSnapEnabled: linearDescriptor?.gridSnap === true,
776
- gridSnapActive: isGridSnapActive(),
782
+ gridSnapActive: isGridSnapActive() && !modifiers.altKey,
777
783
  gridSnapStep: useEditor.getState().gridSnapStep,
778
- magneticSnapActive: isMagneticSnapActive(),
784
+ magneticSnapActive: isMagneticSnapActive() && !modifiers.altKey,
779
785
  magneticSnap: linearDescriptor?.magneticSnap
780
786
  ? (value) => linearDescriptor.magneticSnap?.(initialNode, value, sceneApi) ?? value
781
787
  : undefined,
788
+ connectionSnapActive: !modifiers.altKey,
789
+ connectionSnap: linearDescriptor?.connectionSnap
790
+ ? (value) => linearDescriptor.connectionSnap?.(initialNode, value, sceneApi) ?? value
791
+ : undefined,
782
792
  })
783
793
  const next = Math.min(maxBound, Math.max(minBound, snappedNext))
784
794
  if (next !== lastTickValue) {
785
795
  lastTickValue = next
786
796
  sfxEmitter.emit('sfx:resize')
787
797
  }
788
- const patch = descriptor.apply(initialNode as never, next, sceneApi) as Partial<AnyNode>
789
- if (descriptor.kind === 'linear-resize' && descriptor.previewOverrides) {
790
- const previewEntries = descriptor.previewOverrides(initialNode as never, next, sceneApi)
791
- const nextPreviewOverrideIds = replacePreviewOverrideIds(
792
- previewOverrideIds,
793
- previewEntries,
794
- (previewId) => {
795
- useLiveNodeOverrides.getState().clear(previewId)
796
- useScene.getState().markDirty(previewId)
797
- },
798
- )
799
- useLiveNodeOverrides
800
- .getState()
801
- .setMany(
802
- previewEntries.map(([id, previewPatch]) => [
803
- id,
804
- previewPatch as Record<string, unknown>,
805
- ]),
806
- )
807
- for (const [previewId] of previewEntries) {
808
- useScene.getState().markDirty(previewId)
809
- }
810
- previewOverrideIds = nextPreviewOverrideIds
811
- }
798
+ const patch = linearBinding
799
+ ? linearBinding.apply(next, modifiers)
800
+ : (descriptor.apply(initialNode as never, next, sceneApi) as Partial<AnyNode>)
812
801
  // Let the kind publish live guides for the edge being resized.
813
802
  onDrag?.({ ...(initialNode as object), ...patch } as AnyNode, sceneApi)
814
803
  return patch
@@ -53,17 +53,26 @@ const mid = (a: OpeningGuideVec3, b: OpeningGuideVec3): OpeningGuideVec3 => [
53
53
  export const OpeningGuides3DLayer = memo(function OpeningGuides3DLayer() {
54
54
  const guides = useOpeningGuides((s) => s.guides)
55
55
  const unit = useViewer((s) => s.unit)
56
+ const metricNotation = useViewer((s) => s.metricNotation)
56
57
  if (guides.length === 0) return null
57
58
  return (
58
59
  <>
59
60
  {guides.map((guide) => (
60
- <OpeningGuide guide={guide} key={guide.id} unit={unit} />
61
+ <OpeningGuide guide={guide} key={guide.id} metricNotation={metricNotation} unit={unit} />
61
62
  ))}
62
63
  </>
63
64
  )
64
65
  })
65
66
 
66
- function OpeningGuide({ guide, unit }: { guide: OpeningGuide3D; unit: 'metric' | 'imperial' }) {
67
+ function OpeningGuide({
68
+ guide,
69
+ metricNotation,
70
+ unit,
71
+ }: {
72
+ guide: OpeningGuide3D
73
+ metricNotation: 'meters' | 'millimeters'
74
+ unit: 'metric' | 'imperial'
75
+ }) {
67
76
  if (guide.kind === 'badge') {
68
77
  return (
69
78
  <Html
@@ -76,7 +85,7 @@ function OpeningGuide({ guide, unit }: { guide: OpeningGuide3D; unit: 'metric' |
76
85
  className="whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-sans font-semibold text-[11px] text-white"
77
86
  style={{ backgroundColor: BADGE_PILL }}
78
87
  >
79
- {`= ${formatMeasurement(guide.value, unit)}`}
88
+ {`= ${formatMeasurement(guide.value, unit, metricNotation)}`}
80
89
  </div>
81
90
  </Html>
82
91
  )
@@ -97,7 +106,7 @@ function OpeningGuide({ guide, unit }: { guide: OpeningGuide3D; unit: 'metric' |
97
106
  className="whitespace-nowrap rounded-[3px] px-[5px] py-[2px] font-medium font-sans text-[11px] text-white"
98
107
  style={{ backgroundColor: DIMENSION_PILL }}
99
108
  >
100
- {formatMeasurement(guide.value, unit)}
109
+ {formatMeasurement(guide.value, unit, metricNotation)}
101
110
  </div>
102
111
  </Html>
103
112
  ) : null}