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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +13 -7
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/bake-exporter.tsx +1 -0
  6. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  7. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  8. package/src/components/editor/capture-camera-rig.tsx +59 -0
  9. package/src/components/editor/custom-camera-controls.tsx +20 -11
  10. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  11. package/src/components/editor/editor-layout-v2.tsx +9 -2
  12. package/src/components/editor/export-manager.tsx +175 -33
  13. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  14. package/src/components/editor/first-person-controls.tsx +180 -12
  15. package/src/components/editor/floating-action-menu.tsx +42 -51
  16. package/src/components/editor/floorplan-panel.tsx +76 -12
  17. package/src/components/editor/grid.tsx +12 -7
  18. package/src/components/editor/group-actions.ts +34 -50
  19. package/src/components/editor/group-move-3d.ts +51 -21
  20. package/src/components/editor/group-transform-shared.test.ts +78 -0
  21. package/src/components/editor/group-transform-shared.ts +74 -7
  22. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  23. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  24. package/src/components/editor/handles/linear-resize-drag.ts +67 -0
  25. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  26. package/src/components/editor/handles/resize-snap.ts +10 -1
  27. package/src/components/editor/handles/use-handle-drag.ts +18 -1
  28. package/src/components/editor/index.tsx +140 -21
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +47 -58
  31. package/src/components/editor/selection-manager.tsx +104 -42
  32. package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
  33. package/src/components/editor/snapshot-capture.test.ts +422 -0
  34. package/src/components/editor/snapshot-capture.ts +145 -0
  35. package/src/components/editor/thumbnail-generator.tsx +284 -237
  36. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  37. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  38. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  39. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  41. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  42. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  43. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  44. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  45. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  46. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +180 -32
  47. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  48. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  49. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  50. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  51. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  52. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  53. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  54. package/src/components/systems/selection-affordance-services.ts +19 -0
  55. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  56. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  57. package/src/components/tools/fence/fence-drafting.ts +6 -25
  58. package/src/components/tools/item/draft-creation.test.ts +12 -0
  59. package/src/components/tools/item/draft-creation.ts +10 -0
  60. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  61. package/src/components/tools/item/face-host-commit.ts +56 -0
  62. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  63. package/src/components/tools/item/face-host-preview.ts +82 -0
  64. package/src/components/tools/item/move-tool.tsx +4 -3
  65. package/src/components/tools/item/placement-math.test.ts +11 -1
  66. package/src/components/tools/item/placement-math.ts +0 -20
  67. package/src/components/tools/item/placement-strategies.test.ts +331 -4
  68. package/src/components/tools/item/placement-strategies.ts +135 -2
  69. package/src/components/tools/item/placement-surface.test.ts +61 -0
  70. package/src/components/tools/item/placement-surface.ts +24 -0
  71. package/src/components/tools/item/placement-types.ts +6 -0
  72. package/src/components/tools/item/test-face-host.ts +121 -0
  73. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  74. package/src/components/tools/item/use-draft-node.ts +26 -4
  75. package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
  76. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  77. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  78. package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
  79. package/src/components/tools/registry-tool-context.tsx +30 -0
  80. package/src/components/tools/select/box-select-tool.tsx +3 -1
  81. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  82. package/src/components/tools/select/marquee-footprint.ts +29 -0
  83. package/src/components/tools/shared/placement-box.tsx +57 -7
  84. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  85. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  86. package/src/components/tools/shared/pointer-support-cap.ts +45 -8
  87. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  88. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  89. package/src/components/tools/stair/stair-tool.tsx +127 -74
  90. package/src/components/tools/tool-manager.tsx +25 -8
  91. package/src/components/tools/wall/wall-drafting.test.ts +320 -2
  92. package/src/components/tools/wall/wall-drafting.ts +23 -114
  93. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  94. package/src/components/ui/action-menu/index.tsx +11 -1
  95. package/src/components/ui/command-palette/editor-commands.tsx +9 -12
  96. package/src/components/ui/command-palette/index.tsx +7 -2
  97. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  98. package/src/components/ui/controls/material-picker.tsx +4 -4
  99. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  100. package/src/components/ui/controls/segmented-control.tsx +8 -1
  101. package/src/components/ui/controls/slider-control.tsx +19 -3
  102. package/src/components/ui/controls/toggle-control.tsx +18 -5
  103. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  104. package/src/components/ui/floating-level-selector.tsx +22 -5
  105. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  106. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  107. package/src/components/ui/helpers/item-helper.tsx +1 -1
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  109. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  110. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  111. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  112. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  113. package/src/components/ui/panels/multi-field-value.ts +163 -0
  114. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  115. package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
  116. package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
  117. package/src/components/ui/panels/node-display.ts +4 -0
  118. package/src/components/ui/panels/panel-manager.tsx +77 -3
  119. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  120. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  121. package/src/components/ui/panels/parametric-inspector.tsx +29 -162
  122. package/src/components/ui/panels/reference-panel.tsx +31 -1
  123. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  124. package/src/components/ui/scene-loader.tsx +35 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
  127. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
  128. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
  129. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  130. package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
  131. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  132. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  133. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  134. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
  135. package/src/components/ui/sidebar/tab-bar.tsx +2 -0
  136. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  137. package/src/components/viewer/viewer-scene-header.tsx +2 -1
  138. package/src/components/viewer/viewer-stage.tsx +4 -1
  139. package/src/components/viewer-overlay.tsx +1 -22
  140. package/src/components/viewer-zone-system.tsx +6 -2
  141. package/src/components/walkthrough-hud.tsx +19 -10
  142. package/src/hooks/use-auto-save.test.ts +73 -1
  143. package/src/hooks/use-auto-save.ts +76 -5
  144. package/src/hooks/use-ceiling-events.test.ts +122 -0
  145. package/src/hooks/use-drag-action.ts +17 -3
  146. package/src/hooks/use-grid-events.test.ts +240 -0
  147. package/src/hooks/use-grid-events.ts +190 -11
  148. package/src/hooks/use-keyboard.test.ts +244 -0
  149. package/src/hooks/use-keyboard.ts +129 -62
  150. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  151. package/src/hooks/use-save-shortcut.ts +33 -0
  152. package/src/index.tsx +82 -4
  153. package/src/lib/active-placement-surface.test.ts +16 -0
  154. package/src/lib/active-placement-surface.ts +14 -0
  155. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  156. package/src/lib/ceiling-surface-raycast.ts +12 -0
  157. package/src/lib/contextual-help-extension.ts +17 -0
  158. package/src/lib/continuation.test.ts +11 -0
  159. package/src/lib/continuation.ts +8 -1
  160. package/src/lib/direct-manipulation.test.ts +132 -0
  161. package/src/lib/direct-manipulation.ts +38 -1
  162. package/src/lib/editor-api.ts +6 -3
  163. package/src/lib/elevation-guides.test.ts +28 -1
  164. package/src/lib/elevation-guides.ts +22 -7
  165. package/src/lib/floating-menu-scale.test.ts +29 -0
  166. package/src/lib/floating-menu-scale.ts +14 -0
  167. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  168. package/src/lib/floorplan/apply-alignment.ts +10 -1
  169. package/src/lib/floorplan/floorplan-export.test.ts +397 -1
  170. package/src/lib/floorplan/floorplan-export.tsx +111 -16
  171. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  172. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  173. package/src/lib/floorplan/floorplan-readonly.ts +1 -0
  174. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  175. package/src/lib/floorplan-grid-event-point.ts +13 -0
  176. package/src/lib/glb-export.test.ts +1169 -21
  177. package/src/lib/glb-export.ts +629 -101
  178. package/src/lib/grid-event-presentation.ts +16 -0
  179. package/src/lib/history.test.ts +528 -147
  180. package/src/lib/history.ts +81 -7
  181. package/src/lib/host-tree-children.test.ts +27 -0
  182. package/src/lib/host-tree-children.ts +79 -0
  183. package/src/lib/interaction/hot-set.test.ts +10 -1
  184. package/src/lib/interaction/hot-set.ts +3 -1
  185. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  186. package/src/lib/interaction/overlay-policy.ts +16 -0
  187. package/src/lib/interaction/registered-drafting.ts +36 -0
  188. package/src/lib/interaction/scope.ts +31 -0
  189. package/src/lib/level-print-export.test.ts +484 -0
  190. package/src/lib/level-print-export.ts +526 -0
  191. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  192. package/src/lib/local-project-presentation-persistence.ts +335 -0
  193. package/src/lib/material-paint.ts +2 -0
  194. package/src/lib/model-export.ts +29 -0
  195. package/src/lib/paint-preview-owner.ts +71 -0
  196. package/src/lib/planar-cursor-placement.test.ts +178 -1
  197. package/src/lib/planar-cursor-placement.ts +61 -5
  198. package/src/lib/plugin-panels.test.ts +92 -1
  199. package/src/lib/plugin-panels.ts +48 -0
  200. package/src/lib/portable-export.test.ts +366 -0
  201. package/src/lib/portable-export.ts +969 -0
  202. package/src/lib/print-3mf.test.ts +86 -0
  203. package/src/lib/print-3mf.ts +161 -0
  204. package/src/lib/print-content-scope.test.ts +107 -0
  205. package/src/lib/print-content-scope.ts +90 -0
  206. package/src/lib/print-export.test.ts +294 -0
  207. package/src/lib/print-export.ts +735 -0
  208. package/src/lib/print-feature-thickness.test.ts +147 -0
  209. package/src/lib/print-feature-thickness.ts +180 -0
  210. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  211. package/src/lib/print-golden-house.test.ts +249 -0
  212. package/src/lib/print-roof-solids.test.ts +133 -0
  213. package/src/lib/print-roof-solids.ts +630 -0
  214. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  215. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  216. package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
  217. package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
  218. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  219. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  220. package/src/lib/print-shell-compiler-protocol.ts +30 -0
  221. package/src/lib/print-shell-compiler.ts +273 -0
  222. package/src/lib/print-wall-solids.test.ts +194 -0
  223. package/src/lib/print-wall-solids.ts +328 -0
  224. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  225. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  226. package/src/lib/scene.ts +22 -8
  227. package/src/lib/selection-routing.test.ts +105 -1
  228. package/src/lib/selection-routing.ts +19 -0
  229. package/src/lib/sfx-bus.ts +4 -0
  230. package/src/lib/sfx-player.ts +8 -0
  231. package/src/lib/snapping-mode.test.ts +32 -4
  232. package/src/lib/snapping-mode.ts +11 -7
  233. package/src/lib/usdz-export.ts +38 -0
  234. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  235. package/src/store/live-draft-preview-stores.test.ts +3 -0
  236. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  237. package/src/store/tool-mode.test.ts +87 -0
  238. package/src/store/use-camera-hint-focus.ts +48 -0
  239. package/src/store/use-editor.tsx +299 -188
  240. package/src/store/use-floorplan-draft-preview.ts +9 -0
  241. package/src/store/use-interaction-scope.test.ts +23 -1
  242. package/src/store/use-interaction-scope.ts +63 -2
  243. package/src/store/use-placement-preview.ts +56 -3
  244. package/src/components/tools/roof/roof-tool.tsx +0 -656
  245. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -11,6 +11,7 @@ import {
11
11
  getSelectableKinds,
12
12
  type ItemNode,
13
13
  isRegistrySelectable,
14
+ isSelectionHighlightEnabled,
14
15
  type NodeEvent,
15
16
  nodeRegistry,
16
17
  type RoofEvent,
@@ -31,6 +32,7 @@ import {
31
32
  createMaterial,
32
33
  createMaterialFromPresetRef,
33
34
  getRoofMaterialArray,
35
+ registerMaterialCacheCleanup,
34
36
  useViewer,
35
37
  } from '@pascal-app/viewer'
36
38
  import { useThree } from '@react-three/fiber'
@@ -39,10 +41,14 @@ import { type BufferGeometry, Color, type Material, type Mesh, type Object3D, Ve
39
41
  import {
40
42
  canDirectMoveNode,
41
43
  canDirectRotateNode,
44
+ pointerEventHitsEditorHandle,
45
+ resolveDirectManipulationNode,
42
46
  resolveDirectRotationDragDelta,
43
47
  resolveDirectRotationPatch,
48
+ shouldStartDirectMoveDrag,
44
49
  } from '../../lib/direct-manipulation'
45
50
  import { createEditorApi } from '../../lib/editor-api'
51
+ import { selectionEnabled } from '../../lib/interaction/scope'
46
52
  import {
47
53
  type ActivePaintMaterial,
48
54
  buildRoofSegmentSurfaceMaterialPatch,
@@ -50,6 +56,11 @@ import {
50
56
  hasActivePaintMaterial,
51
57
  resolveActivePaintMaterialFromSelection,
52
58
  } from '../../lib/material-paint'
59
+ import {
60
+ combinePaintPreviews,
61
+ createPaintPreviewOwner,
62
+ type PaintPreviewCleanup,
63
+ } from '../../lib/paint-preview-owner'
53
64
  import {
54
65
  availablePaintScopes,
55
66
  commitPaintScopeFanout,
@@ -112,8 +123,6 @@ type SelectableNodeType =
112
123
  | 'window'
113
124
  | 'door'
114
125
 
115
- type PaintPreviewCleanup = () => void
116
-
117
126
  type PaintInteraction = {
118
127
  key: string
119
128
  apply: (() => void) | null
@@ -805,6 +814,7 @@ export const SelectionManager = () => {
805
814
  if (mode !== 'material-paint') return
806
815
  if (movingNode || isCurveReshape) return
807
816
 
817
+ const previewOwner = createPaintPreviewOwner()
808
818
  let activePreview: { key: string; restore: PaintPreviewCleanup } | null = null
809
819
  // The last hover event, replayed when the application scope cycles so the
810
820
  // preview + chip update under a stationary cursor (Shift fires no pointer move).
@@ -826,7 +836,7 @@ export const SelectionManager = () => {
826
836
  selectedMaterialTarget: useEditor.getState().selectedMaterialTarget,
827
837
  })
828
838
 
829
- const getPaintInteraction = (event: NodeEvent): PaintInteraction | null => {
839
+ const resolvePaintInteraction = (event: NodeEvent): PaintInteraction | null => {
830
840
  const eraser = useEditor.getState().paintEraser
831
841
  const activePaintMaterial = resolveActivePaintMaterial()
832
842
  const node = event.node
@@ -949,27 +959,29 @@ export const SelectionManager = () => {
949
959
  // paint capability builds the preview; restores combine.
950
960
  const restores: PaintPreviewCleanup[] = []
951
961
  const sceneNodes = useScene.getState().nodes
952
- for (const target of scopeTargets) {
953
- const targetNode = sceneNodes[target.nodeId]
954
- const targetRoot = getRegisteredNodeObject(target.nodeId)
955
- const targetCap = targetNode
956
- ? nodeRegistry.get(targetNode.type)?.capabilities?.paint
957
- : null
958
- if (!(targetNode && targetRoot && targetCap)) continue
959
- const restore = targetCap.applyPreview({
960
- node: targetNode,
961
- role: target.role,
962
- material: paintSpec.material,
963
- materialPreset: paintSpec.materialPreset,
964
- root: targetRoot,
965
- })
966
- if (restore) restores.push(restore)
962
+ try {
963
+ for (const target of scopeTargets) {
964
+ const targetNode = sceneNodes[target.nodeId]
965
+ const targetRoot = getRegisteredNodeObject(target.nodeId)
966
+ const targetCap = targetNode
967
+ ? nodeRegistry.get(targetNode.type)?.capabilities?.paint
968
+ : null
969
+ if (!(targetNode && targetRoot && targetCap)) continue
970
+ const restore = targetCap.applyPreview({
971
+ node: targetNode,
972
+ role: target.role,
973
+ material: paintSpec.material,
974
+ materialPreset: paintSpec.materialPreset,
975
+ root: targetRoot,
976
+ })
977
+ if (restore) restores.push(restore)
978
+ }
979
+ } catch (error) {
980
+ combinePaintPreviews(restores)()
981
+ throw error
967
982
  }
968
983
  if (restores.length === 0) return null
969
- return () => {
970
- for (let index = restores.length - 1; index >= 0; index -= 1)
971
- restores[index]?.()
972
- }
984
+ return combinePaintPreviews(restores)
973
985
  }
974
986
  : () => previewCursor('not-allowed'),
975
987
  }
@@ -1069,6 +1081,9 @@ export const SelectionManager = () => {
1069
1081
  return null
1070
1082
  }
1071
1083
 
1084
+ const getPaintInteraction = (event: NodeEvent) =>
1085
+ previewOwner.wrap(resolvePaintInteraction(event))
1086
+
1072
1087
  const onEnter = (event: NodeEvent) => {
1073
1088
  // A host-driven drag (handle resize/rotate) sets `inputDragging`.
1074
1089
  // useNodeEvents now emits hover events during such a drag so surface
@@ -1239,8 +1254,10 @@ export const SelectionManager = () => {
1239
1254
  if (movingNode || isCurveReshape) return
1240
1255
 
1241
1256
  const onPointerDown = (event: NodeEvent) => {
1257
+ if (!selectionEnabled(useInteractionScope.getState().scope)) return
1242
1258
  const pointer = pointerEventFromNodeEvent(event)
1243
1259
  if (pointer.button !== 0) return
1260
+ const handleOwnsPointer = pointerEventHitsEditorHandle(event.nativeEvent)
1244
1261
 
1245
1262
  // Plain press on a transformable member of a multi-selection arms the
1246
1263
  // group move — dragging slides the whole selection on the ground plane
@@ -1248,6 +1265,7 @@ export const SelectionManager = () => {
1248
1265
  // group-move gizmo cross). A plain click (no drag) still falls through
1249
1266
  // to the normal click handling, which collapses to the pressed node.
1250
1267
  if (
1268
+ !handleOwnsPointer &&
1251
1269
  !(pointer.shiftKey || pointer.altKey || isCommandModifier(pointer)) &&
1252
1270
  armGroupMove3d({
1253
1271
  nodeId: event.node.id as AnyNodeId,
@@ -1263,8 +1281,6 @@ export const SelectionManager = () => {
1263
1281
  return
1264
1282
  }
1265
1283
 
1266
- if (!isCommandModifier(pointer)) return
1267
-
1268
1284
  const eventNode = useScene.getState().nodes[event.node.id as AnyNodeId] ?? event.node
1269
1285
  const node = resolveCanvasSelectionNode({
1270
1286
  node: eventNode,
@@ -1272,18 +1288,26 @@ export const SelectionManager = () => {
1272
1288
  selectedIds: useViewer.getState().selection.selectedIds,
1273
1289
  })
1274
1290
  if (!canDirectMoveNode(node)) return
1275
- // Sole selection only: per-node direct manipulation stands down for a
1276
- // multi-selection (the group sessions own plain drags there, and Cmd is
1277
- // the selection-toggle key — a wobbly Cmd+click must not yank one
1278
- // member out of the group).
1279
1291
  const currentSelectedIds = useViewer.getState().selection.selectedIds
1280
- if (currentSelectedIds.length !== 1 || currentSelectedIds[0] !== node.id) return
1292
+ const allowPlainDrag = nodeRegistry.get(node.type)?.capabilities?.movable?.directDrag === true
1293
+ if (
1294
+ !shouldStartDirectMoveDrag({
1295
+ allowPlainDrag,
1296
+ commandModifier: isCommandModifier(pointer),
1297
+ handleOwnsPointer,
1298
+ nodeId: node.id,
1299
+ selectedIds: currentSelectedIds,
1300
+ })
1301
+ ) {
1302
+ return
1303
+ }
1281
1304
 
1282
1305
  const startX = pointer.clientX
1283
1306
  const startY = pointer.clientY
1284
1307
  const pointerId = pointer.pointerId
1285
1308
  const pointerTarget = pointer.target instanceof EventTarget ? pointer.target : null
1286
1309
  let engaged = false
1310
+ let engagedTargetId: AnyNodeId | null = null
1287
1311
 
1288
1312
  const cleanup = () => {
1289
1313
  window.removeEventListener('pointermove', onMove)
@@ -1305,8 +1329,9 @@ export const SelectionManager = () => {
1305
1329
  useViewer.getState().setInputDragging(true)
1306
1330
  swallowNextClick()
1307
1331
  createEditorApi().engageMoveDrag(node)
1332
+ engagedTargetId = (getMovingNode()?.id as AnyNodeId | undefined) ?? null
1308
1333
  requestAnimationFrame(() => {
1309
- if (getMovingNode()?.id !== node.id) return
1334
+ if (!getMovingNode()) return
1310
1335
  pointerTarget?.dispatchEvent(
1311
1336
  new PointerEvent('pointermove', {
1312
1337
  altKey: moveEvent.altKey,
@@ -1330,7 +1355,7 @@ export const SelectionManager = () => {
1330
1355
  if (engaged) {
1331
1356
  requestAnimationFrame(() => {
1332
1357
  const editor = useEditor.getState()
1333
- if (getMovingNode()?.id !== node.id || !editor.placementDragMode) return
1358
+ if (getMovingNode()?.id !== engagedTargetId || !editor.placementDragMode) return
1334
1359
  editor.setMovingNode(null)
1335
1360
  })
1336
1361
  }
@@ -1392,7 +1417,7 @@ export const SelectionManager = () => {
1392
1417
  if (key === prevKey) return
1393
1418
  prevKey = key
1394
1419
  let wantsMove = false
1395
- if (hoveredId && !getMovingNode()) {
1420
+ if (hoveredId && !getMovingNode() && selectionEnabled(useInteractionScope.getState().scope)) {
1396
1421
  if (sole === hoveredId) {
1397
1422
  const node = useScene.getState().nodes[sole as AnyNodeId]
1398
1423
  wantsMove = !!node && canDirectMoveNode(node)
@@ -1557,6 +1582,7 @@ export const SelectionManager = () => {
1557
1582
  // body click so only the reshape tool handles the release. (Scoped to
1558
1583
  // `endpoint`: hole-edit relies on node clicks to exit, just below.)
1559
1584
  const activeScope = useInteractionScope.getState().scope
1585
+ if (activeScope.kind === 'mesh-editing') return
1560
1586
  if (activeScope.kind === 'reshaping' && activeScope.reshape === 'endpoint') return
1561
1587
 
1562
1588
  if (dispatchSceneAction(event.node, getEventObject(event))) {
@@ -1655,9 +1681,16 @@ export const SelectionManager = () => {
1655
1681
  const hasModifier = nativeEvent.shiftKey || isCommandModifier(nativeEvent)
1656
1682
  const isAlreadySole =
1657
1683
  selectedIdsBeforeRouting.length === 1 && selectedIdsBeforeRouting[0] === nodeToSelect.id
1658
- if (!hasModifier && isAlreadySole && !getMovingNode() && canDirectMoveNode(nodeToSelect)) {
1684
+ if (
1685
+ useEditor.getState().mode !== 'delete' &&
1686
+ !hasModifier &&
1687
+ isAlreadySole &&
1688
+ !getMovingNode() &&
1689
+ canDirectMoveNode(nodeToSelect)
1690
+ ) {
1659
1691
  sfxEmitter.emit('sfx:item-pick')
1660
- useEditor.getState().setMovingNode(nodeToSelect as never)
1692
+ const moveTarget = resolveDirectManipulationNode(nodeToSelect, useScene.getState().nodes)
1693
+ useEditor.getState().setMovingNode(moveTarget as never)
1661
1694
  useViewer.getState().setSelection({ selectedIds: [] })
1662
1695
  return
1663
1696
  }
@@ -1773,6 +1806,7 @@ export const SelectionManager = () => {
1773
1806
  const onGridClick = (event: GridEvent) => {
1774
1807
  if (clickHandledRef.current) return
1775
1808
  if (boxSelectHandled) return
1809
+ if (useInteractionScope.getState().scope.kind === 'mesh-editing') return
1776
1810
  const nativeEvent = event.nativeEvent
1777
1811
  if (nativeEvent?.metaKey || nativeEvent?.ctrlKey || nativeEvent?.shiftKey) return
1778
1812
  const { phase, structureLayer } = useEditor.getState()
@@ -1804,6 +1838,7 @@ export const SelectionManager = () => {
1804
1838
  if (movingNode || isCurveReshape) return
1805
1839
 
1806
1840
  const onEnter = (event: NodeEvent) => {
1841
+ if (useInteractionScope.getState().scope.kind === 'mesh-editing') return
1807
1842
  // A host-driven drag (handle resize/rotate, box-select) sets
1808
1843
  // `inputDragging`. useNodeEvents still emits hover events during it so
1809
1844
  // surface move tools keep tracking — but the select-hover outline must
@@ -1852,6 +1887,7 @@ export const SelectionManager = () => {
1852
1887
  }
1853
1888
 
1854
1889
  const onDoubleClick = (event: NodeEvent) => {
1890
+ if (useInteractionScope.getState().scope.kind === 'mesh-editing') return
1855
1891
  let node = resolveCanvasSelectionNode({
1856
1892
  node: resolveSelectModeNodeTarget(event),
1857
1893
  nodes: useScene.getState().nodes,
@@ -2119,6 +2155,7 @@ const SelectionMaterialSync = () => {
2119
2155
  const previewSelectedIds = useViewer((s) => s.previewSelectedIds)
2120
2156
  const hoveredId = useViewer((s) => s.hoveredId)
2121
2157
  const hoverHighlightMode = useViewer((s) => s.hoverHighlightMode)
2158
+ const registryVersion = useRegistryVersion()
2122
2159
  const geometryRevision = useViewer((s) => s.geometryRevision)
2123
2160
  const activeHighlightKindsRef = useRef(new Map<string, HighlightKind>())
2124
2161
  const highlightedMaterialsRef = useRef(
@@ -2141,6 +2178,10 @@ const SelectionMaterialSync = () => {
2141
2178
  continue
2142
2179
  }
2143
2180
 
2181
+ if (node && !isSelectionHighlightEnabled(node.type)) {
2182
+ continue
2183
+ }
2184
+
2144
2185
  const rootObject = sceneRegistry.nodes.get(id)
2145
2186
  if (!rootObject) {
2146
2187
  continue
@@ -2196,6 +2237,7 @@ const SelectionMaterialSync = () => {
2196
2237
  }, [])
2197
2238
 
2198
2239
  useEffect(() => {
2240
+ void registryVersion
2199
2241
  void geometryRevision
2200
2242
  const nextHighlightKinds = new Map<string, HighlightKind>()
2201
2243
 
@@ -2211,6 +2253,7 @@ const SelectionMaterialSync = () => {
2211
2253
  syncSelectionMaterials()
2212
2254
  }, [
2213
2255
  geometryRevision,
2256
+ registryVersion,
2214
2257
  hoverHighlightMode,
2215
2258
  hoveredId,
2216
2259
  previewSelectedIds,
@@ -2251,7 +2294,7 @@ const SelectionMaterialSync = () => {
2251
2294
  }, [])
2252
2295
 
2253
2296
  useEffect(() => {
2254
- return () => {
2297
+ const clearHighlights = () => {
2255
2298
  for (const [mesh, entry] of highlightedMaterialsRef.current.entries()) {
2256
2299
  if (mesh.material === entry.highlightedMaterial) {
2257
2300
  mesh.material = entry.originalMaterial
@@ -2261,6 +2304,11 @@ const SelectionMaterialSync = () => {
2261
2304
 
2262
2305
  highlightedMaterialsRef.current.clear()
2263
2306
  }
2307
+ const unsubscribe = registerMaterialCacheCleanup(clearHighlights)
2308
+ return () => {
2309
+ unsubscribe()
2310
+ clearHighlights()
2311
+ }
2264
2312
  }, [])
2265
2313
 
2266
2314
  return null
@@ -2272,11 +2320,13 @@ const EditorOutlinerSync = () => {
2272
2320
  const previewSelectedIds = useViewer((s) => s.previewSelectedIds)
2273
2321
  const hoveredId = useViewer((s) => s.hoveredId)
2274
2322
  const geometryRevision = useViewer((s) => s.geometryRevision)
2323
+ const registryVersion = useRegistryVersion()
2275
2324
  const outliner = useViewer((s) => s.outliner)
2276
2325
  const nodes = useScene((s) => s.nodes)
2277
2326
 
2278
2327
  useEffect(() => {
2279
2328
  void geometryRevision
2329
+ void registryVersion
2280
2330
  let idsToHighlight: string[] = []
2281
2331
 
2282
2332
  // 1. Determine what should be highlighted based on Phase
@@ -2311,7 +2361,8 @@ const EditorOutlinerSync = () => {
2311
2361
  // 2. Sync with the imperative outliner arrays (mutate in place to keep references)
2312
2362
  outliner.selectedObjects.length = 0
2313
2363
  for (const id of idsToHighlight) {
2314
- if (!nodes[id as AnyNodeId]) continue
2364
+ const node = nodes[id as AnyNodeId]
2365
+ if (!(node && isSelectionHighlightEnabled(node.type))) continue
2315
2366
  const obj = sceneRegistry.nodes.get(id)
2316
2367
  if (obj?.parent) outliner.selectedObjects.push(obj)
2317
2368
  }
@@ -2322,14 +2373,25 @@ const EditorOutlinerSync = () => {
2322
2373
  useViewer.setState({ hoveredId: null })
2323
2374
  } else {
2324
2375
  const hoveredNode = nodes[hoveredId as AnyNodeId]
2325
- const obj =
2326
- hoveredNode?.type === 'roof-segment'
2327
- ? (getHoveredRoofSegmentOutlineProxy(hoveredId) ?? sceneRegistry.nodes.get(hoveredId))
2328
- : sceneRegistry.nodes.get(hoveredId)
2329
- if (obj?.parent) outliner.hoveredObjects.push(obj)
2376
+ if (hoveredNode && isSelectionHighlightEnabled(hoveredNode.type)) {
2377
+ const obj =
2378
+ hoveredNode.type === 'roof-segment'
2379
+ ? (getHoveredRoofSegmentOutlineProxy(hoveredId) ?? sceneRegistry.nodes.get(hoveredId))
2380
+ : sceneRegistry.nodes.get(hoveredId)
2381
+ if (obj?.parent) outliner.hoveredObjects.push(obj)
2382
+ }
2330
2383
  }
2331
2384
  }
2332
- }, [geometryRevision, phase, previewSelectedIds, selection, hoveredId, outliner, nodes])
2385
+ }, [
2386
+ geometryRevision,
2387
+ registryVersion,
2388
+ phase,
2389
+ previewSelectedIds,
2390
+ selection,
2391
+ hoveredId,
2392
+ outliner,
2393
+ nodes,
2394
+ ])
2333
2395
 
2334
2396
  return null
2335
2397
  }