@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
@@ -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,
@@ -23,6 +24,7 @@ import {
23
24
  type StairSurfaceMaterialRole,
24
25
  sceneRegistry,
25
26
  useLiveNodeOverrides,
27
+ useRegistryVersion,
26
28
  useScene,
27
29
  } from '@pascal-app/core'
28
30
 
@@ -30,6 +32,7 @@ import {
30
32
  createMaterial,
31
33
  createMaterialFromPresetRef,
32
34
  getRoofMaterialArray,
35
+ registerMaterialCacheCleanup,
33
36
  useViewer,
34
37
  } from '@pascal-app/viewer'
35
38
  import { useThree } from '@react-three/fiber'
@@ -38,10 +41,14 @@ import { type BufferGeometry, Color, type Material, type Mesh, type Object3D, Ve
38
41
  import {
39
42
  canDirectMoveNode,
40
43
  canDirectRotateNode,
44
+ pointerEventHitsEditorHandle,
45
+ resolveDirectManipulationNode,
41
46
  resolveDirectRotationDragDelta,
42
47
  resolveDirectRotationPatch,
48
+ shouldStartDirectMoveDrag,
43
49
  } from '../../lib/direct-manipulation'
44
50
  import { createEditorApi } from '../../lib/editor-api'
51
+ import { selectionEnabled } from '../../lib/interaction/scope'
45
52
  import {
46
53
  type ActivePaintMaterial,
47
54
  buildRoofSegmentSurfaceMaterialPatch,
@@ -49,6 +56,11 @@ import {
49
56
  hasActivePaintMaterial,
50
57
  resolveActivePaintMaterialFromSelection,
51
58
  } from '../../lib/material-paint'
59
+ import {
60
+ combinePaintPreviews,
61
+ createPaintPreviewOwner,
62
+ type PaintPreviewCleanup,
63
+ } from '../../lib/paint-preview-owner'
52
64
  import {
53
65
  availablePaintScopes,
54
66
  commitPaintScopeFanout,
@@ -77,6 +89,7 @@ import useInteractionScope, {
77
89
  useIsCurveReshape,
78
90
  useMovingNode,
79
91
  } from '../../store/use-interaction-scope'
92
+ import { expandSessionSelectionForNode } from '../../store/use-session-groups'
80
93
  import { boxSelectHandled, suppressBoxSelectForPointer } from '../tools/select/box-select-state'
81
94
  import { armGroupMove3d } from './group-move-3d'
82
95
  import { classifyParticipant } from './group-transform-shared'
@@ -110,8 +123,6 @@ type SelectableNodeType =
110
123
  | 'window'
111
124
  | 'door'
112
125
 
113
- type PaintPreviewCleanup = () => void
114
-
115
126
  type PaintInteraction = {
116
127
  key: string
117
128
  apply: (() => void) | null
@@ -590,6 +601,7 @@ const computeNextIds = (
590
601
  currentSelectedIds: selectedIds,
591
602
  modifierKeys: selectionModifiersFromEvent(event, modifierKeys),
592
603
  nodeId: node.id,
604
+ expandIdsForNode: expandSessionSelectionForNode,
593
605
  })
594
606
  }
595
607
 
@@ -774,11 +786,18 @@ export const SelectionManager = () => {
774
786
  meta: false,
775
787
  ctrl: false,
776
788
  shift: false,
789
+ alt: false,
777
790
  })
778
791
  const clickHandledRef = useRef(false)
779
792
 
780
793
  const movingNode = useMovingNode()
781
794
  const isCurveReshape = useIsCurveReshape()
795
+ // Plugin kinds register AFTER mount (async dynamic-import discovery), so
796
+ // every effect below that snapshots `getSelectableKinds()` into an emitter
797
+ // subscription list depends on this version — a late plugin load re-runs
798
+ // them and picks up the new kinds (hover / click / double-click / paint /
799
+ // pointerdown). Without it, plugin nodes select-but-never-hover in prod.
800
+ const registryVersion = useRegistryVersion()
782
801
 
783
802
  useEffect(() => {
784
803
  const nextHoverMode: HoverHighlightMode = mode === 'delete' ? 'delete' : 'default'
@@ -790,9 +809,12 @@ export const SelectionManager = () => {
790
809
  }, [mode, setHoverHighlightMode])
791
810
 
792
811
  useEffect(() => {
812
+ // re-subscribe when plugin kinds register after mount (async plugin load)
813
+ void registryVersion
793
814
  if (mode !== 'material-paint') return
794
815
  if (movingNode || isCurveReshape) return
795
816
 
817
+ const previewOwner = createPaintPreviewOwner()
796
818
  let activePreview: { key: string; restore: PaintPreviewCleanup } | null = null
797
819
  // The last hover event, replayed when the application scope cycles so the
798
820
  // preview + chip update under a stationary cursor (Shift fires no pointer move).
@@ -814,7 +836,7 @@ export const SelectionManager = () => {
814
836
  selectedMaterialTarget: useEditor.getState().selectedMaterialTarget,
815
837
  })
816
838
 
817
- const getPaintInteraction = (event: NodeEvent): PaintInteraction | null => {
839
+ const resolvePaintInteraction = (event: NodeEvent): PaintInteraction | null => {
818
840
  const eraser = useEditor.getState().paintEraser
819
841
  const activePaintMaterial = resolveActivePaintMaterial()
820
842
  const node = event.node
@@ -937,27 +959,29 @@ export const SelectionManager = () => {
937
959
  // paint capability builds the preview; restores combine.
938
960
  const restores: PaintPreviewCleanup[] = []
939
961
  const sceneNodes = useScene.getState().nodes
940
- for (const target of scopeTargets) {
941
- const targetNode = sceneNodes[target.nodeId]
942
- const targetRoot = getRegisteredNodeObject(target.nodeId)
943
- const targetCap = targetNode
944
- ? nodeRegistry.get(targetNode.type)?.capabilities?.paint
945
- : null
946
- if (!(targetNode && targetRoot && targetCap)) continue
947
- const restore = targetCap.applyPreview({
948
- node: targetNode,
949
- role: target.role,
950
- material: paintSpec.material,
951
- materialPreset: paintSpec.materialPreset,
952
- root: targetRoot,
953
- })
954
- 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
955
982
  }
956
983
  if (restores.length === 0) return null
957
- return () => {
958
- for (let index = restores.length - 1; index >= 0; index -= 1)
959
- restores[index]?.()
960
- }
984
+ return combinePaintPreviews(restores)
961
985
  }
962
986
  : () => previewCursor('not-allowed'),
963
987
  }
@@ -1057,6 +1081,9 @@ export const SelectionManager = () => {
1057
1081
  return null
1058
1082
  }
1059
1083
 
1084
+ const getPaintInteraction = (event: NodeEvent) =>
1085
+ previewOwner.wrap(resolvePaintInteraction(event))
1086
+
1060
1087
  const onEnter = (event: NodeEvent) => {
1061
1088
  // A host-driven drag (handle resize/rotate) sets `inputDragging`.
1062
1089
  // useNodeEvents now emits hover events during such a drag so surface
@@ -1185,25 +1212,28 @@ export const SelectionManager = () => {
1185
1212
  setHoverHighlightMode('default')
1186
1213
  useEditor.getState().setPaintHover(null)
1187
1214
  }
1188
- }, [isCurveReshape, mode, movingNode, setHoverHighlightMode])
1215
+ }, [isCurveReshape, mode, movingNode, setHoverHighlightMode, registryVersion])
1189
1216
 
1190
1217
  useEffect(() => {
1191
1218
  const onKeyDown = (event: KeyboardEvent) => {
1192
1219
  if (event.key === 'Meta') modifierKeysRef.current.meta = true
1193
1220
  if (event.key === 'Control') modifierKeysRef.current.ctrl = true
1194
1221
  if (event.key === 'Shift') modifierKeysRef.current.shift = true
1222
+ if (event.key === 'Alt') modifierKeysRef.current.alt = true
1195
1223
  }
1196
1224
 
1197
1225
  const onKeyUp = (event: KeyboardEvent) => {
1198
1226
  if (event.key === 'Meta') modifierKeysRef.current.meta = false
1199
1227
  if (event.key === 'Control') modifierKeysRef.current.ctrl = false
1200
1228
  if (event.key === 'Shift') modifierKeysRef.current.shift = false
1229
+ if (event.key === 'Alt') modifierKeysRef.current.alt = false
1201
1230
  }
1202
1231
 
1203
1232
  const clearModifiers = () => {
1204
1233
  modifierKeysRef.current.meta = false
1205
1234
  modifierKeysRef.current.ctrl = false
1206
1235
  modifierKeysRef.current.shift = false
1236
+ modifierKeysRef.current.alt = false
1207
1237
  }
1208
1238
 
1209
1239
  window.addEventListener('keydown', onKeyDown)
@@ -1218,12 +1248,16 @@ export const SelectionManager = () => {
1218
1248
  }, [])
1219
1249
 
1220
1250
  useEffect(() => {
1251
+ // re-subscribe when plugin kinds register after mount (async plugin load)
1252
+ void registryVersion
1221
1253
  if (mode !== 'select') return
1222
1254
  if (movingNode || isCurveReshape) return
1223
1255
 
1224
1256
  const onPointerDown = (event: NodeEvent) => {
1257
+ if (!selectionEnabled(useInteractionScope.getState().scope)) return
1225
1258
  const pointer = pointerEventFromNodeEvent(event)
1226
1259
  if (pointer.button !== 0) return
1260
+ const handleOwnsPointer = pointerEventHitsEditorHandle(event.nativeEvent)
1227
1261
 
1228
1262
  // Plain press on a transformable member of a multi-selection arms the
1229
1263
  // group move — dragging slides the whole selection on the ground plane
@@ -1231,6 +1265,7 @@ export const SelectionManager = () => {
1231
1265
  // group-move gizmo cross). A plain click (no drag) still falls through
1232
1266
  // to the normal click handling, which collapses to the pressed node.
1233
1267
  if (
1268
+ !handleOwnsPointer &&
1234
1269
  !(pointer.shiftKey || pointer.altKey || isCommandModifier(pointer)) &&
1235
1270
  armGroupMove3d({
1236
1271
  nodeId: event.node.id as AnyNodeId,
@@ -1246,8 +1281,6 @@ export const SelectionManager = () => {
1246
1281
  return
1247
1282
  }
1248
1283
 
1249
- if (!isCommandModifier(pointer)) return
1250
-
1251
1284
  const eventNode = useScene.getState().nodes[event.node.id as AnyNodeId] ?? event.node
1252
1285
  const node = resolveCanvasSelectionNode({
1253
1286
  node: eventNode,
@@ -1255,18 +1288,26 @@ export const SelectionManager = () => {
1255
1288
  selectedIds: useViewer.getState().selection.selectedIds,
1256
1289
  })
1257
1290
  if (!canDirectMoveNode(node)) return
1258
- // Sole selection only: per-node direct manipulation stands down for a
1259
- // multi-selection (the group sessions own plain drags there, and Cmd is
1260
- // the selection-toggle key — a wobbly Cmd+click must not yank one
1261
- // member out of the group).
1262
1291
  const currentSelectedIds = useViewer.getState().selection.selectedIds
1263
- 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
+ }
1264
1304
 
1265
1305
  const startX = pointer.clientX
1266
1306
  const startY = pointer.clientY
1267
1307
  const pointerId = pointer.pointerId
1268
1308
  const pointerTarget = pointer.target instanceof EventTarget ? pointer.target : null
1269
1309
  let engaged = false
1310
+ let engagedTargetId: AnyNodeId | null = null
1270
1311
 
1271
1312
  const cleanup = () => {
1272
1313
  window.removeEventListener('pointermove', onMove)
@@ -1288,8 +1329,9 @@ export const SelectionManager = () => {
1288
1329
  useViewer.getState().setInputDragging(true)
1289
1330
  swallowNextClick()
1290
1331
  createEditorApi().engageMoveDrag(node)
1332
+ engagedTargetId = (getMovingNode()?.id as AnyNodeId | undefined) ?? null
1291
1333
  requestAnimationFrame(() => {
1292
- if (getMovingNode()?.id !== node.id) return
1334
+ if (!getMovingNode()) return
1293
1335
  pointerTarget?.dispatchEvent(
1294
1336
  new PointerEvent('pointermove', {
1295
1337
  altKey: moveEvent.altKey,
@@ -1313,7 +1355,7 @@ export const SelectionManager = () => {
1313
1355
  if (engaged) {
1314
1356
  requestAnimationFrame(() => {
1315
1357
  const editor = useEditor.getState()
1316
- if (getMovingNode()?.id !== node.id || !editor.placementDragMode) return
1358
+ if (getMovingNode()?.id !== engagedTargetId || !editor.placementDragMode) return
1317
1359
  editor.setMovingNode(null)
1318
1360
  })
1319
1361
  }
@@ -1357,7 +1399,7 @@ export const SelectionManager = () => {
1357
1399
  emitter.off(`${type}:pointerdown` as any, onPointerDown as any)
1358
1400
  }
1359
1401
  }
1360
- }, [isCurveReshape, mode, movingNode, camera, raycaster, glDomElement])
1402
+ }, [isCurveReshape, mode, movingNode, camera, raycaster, glDomElement, registryVersion])
1361
1403
 
1362
1404
  // Move cursor over the selected movable node: the visual cue that clicking it
1363
1405
  // picks it up (replaces the removed move-cross gizmo). Reacts only when the
@@ -1375,7 +1417,7 @@ export const SelectionManager = () => {
1375
1417
  if (key === prevKey) return
1376
1418
  prevKey = key
1377
1419
  let wantsMove = false
1378
- if (hoveredId && !getMovingNode()) {
1420
+ if (hoveredId && !getMovingNode() && selectionEnabled(useInteractionScope.getState().scope)) {
1379
1421
  if (sole === hoveredId) {
1380
1422
  const node = useScene.getState().nodes[sole as AnyNodeId]
1381
1423
  wantsMove = !!node && canDirectMoveNode(node)
@@ -1523,6 +1565,8 @@ export const SelectionManager = () => {
1523
1565
  }, [isCurveReshape, mode, movingNode])
1524
1566
 
1525
1567
  useEffect(() => {
1568
+ // re-subscribe when plugin kinds register after mount (async plugin load)
1569
+ void registryVersion
1526
1570
  if (mode !== 'select') return
1527
1571
  if (movingNode || isCurveReshape) return
1528
1572
 
@@ -1538,6 +1582,7 @@ export const SelectionManager = () => {
1538
1582
  // body click so only the reshape tool handles the release. (Scoped to
1539
1583
  // `endpoint`: hole-edit relies on node clicks to exit, just below.)
1540
1584
  const activeScope = useInteractionScope.getState().scope
1585
+ if (activeScope.kind === 'mesh-editing') return
1541
1586
  if (activeScope.kind === 'reshaping' && activeScope.reshape === 'endpoint') return
1542
1587
 
1543
1588
  if (dispatchSceneAction(event.node, getEventObject(event))) {
@@ -1636,9 +1681,16 @@ export const SelectionManager = () => {
1636
1681
  const hasModifier = nativeEvent.shiftKey || isCommandModifier(nativeEvent)
1637
1682
  const isAlreadySole =
1638
1683
  selectedIdsBeforeRouting.length === 1 && selectedIdsBeforeRouting[0] === nodeToSelect.id
1639
- if (!hasModifier && isAlreadySole && !getMovingNode() && canDirectMoveNode(nodeToSelect)) {
1684
+ if (
1685
+ useEditor.getState().mode !== 'delete' &&
1686
+ !hasModifier &&
1687
+ isAlreadySole &&
1688
+ !getMovingNode() &&
1689
+ canDirectMoveNode(nodeToSelect)
1690
+ ) {
1640
1691
  sfxEmitter.emit('sfx:item-pick')
1641
- useEditor.getState().setMovingNode(nodeToSelect as never)
1692
+ const moveTarget = resolveDirectManipulationNode(nodeToSelect, useScene.getState().nodes)
1693
+ useEditor.getState().setMovingNode(moveTarget as never)
1642
1694
  useViewer.getState().setSelection({ selectedIds: [] })
1643
1695
  return
1644
1696
  }
@@ -1754,6 +1806,7 @@ export const SelectionManager = () => {
1754
1806
  const onGridClick = (event: GridEvent) => {
1755
1807
  if (clickHandledRef.current) return
1756
1808
  if (boxSelectHandled) return
1809
+ if (useInteractionScope.getState().scope.kind === 'mesh-editing') return
1757
1810
  const nativeEvent = event.nativeEvent
1758
1811
  if (nativeEvent?.metaKey || nativeEvent?.ctrlKey || nativeEvent?.shiftKey) return
1759
1812
  const { phase, structureLayer } = useEditor.getState()
@@ -1775,14 +1828,17 @@ export const SelectionManager = () => {
1775
1828
  })
1776
1829
  emitter.off('grid:click', onGridClick)
1777
1830
  }
1778
- }, [isCurveReshape, mode, movingNode])
1831
+ }, [isCurveReshape, mode, movingNode, registryVersion])
1779
1832
 
1780
1833
  // Global double-click handler for auto-switching phases and cross-phase hover
1781
1834
  useEffect(() => {
1835
+ // re-subscribe when plugin kinds register after mount (async plugin load)
1836
+ void registryVersion
1782
1837
  if (mode !== 'select') return
1783
1838
  if (movingNode || isCurveReshape) return
1784
1839
 
1785
1840
  const onEnter = (event: NodeEvent) => {
1841
+ if (useInteractionScope.getState().scope.kind === 'mesh-editing') return
1786
1842
  // A host-driven drag (handle resize/rotate, box-select) sets
1787
1843
  // `inputDragging`. useNodeEvents still emits hover events during it so
1788
1844
  // surface move tools keep tracking — but the select-hover outline must
@@ -1831,6 +1887,7 @@ export const SelectionManager = () => {
1831
1887
  }
1832
1888
 
1833
1889
  const onDoubleClick = (event: NodeEvent) => {
1890
+ if (useInteractionScope.getState().scope.kind === 'mesh-editing') return
1834
1891
  let node = resolveCanvasSelectionNode({
1835
1892
  node: resolveSelectModeNodeTarget(event),
1836
1893
  nodes: useScene.getState().nodes,
@@ -1930,10 +1987,12 @@ export const SelectionManager = () => {
1930
1987
  emitter.off(`${type}:double-click` as any, onDoubleClick as any)
1931
1988
  })
1932
1989
  }
1933
- }, [isCurveReshape, mode, movingNode])
1990
+ }, [isCurveReshape, mode, movingNode, registryVersion])
1934
1991
 
1935
1992
  // Delete mode: click-to-delete (sledgehammer tool)
1936
1993
  useEffect(() => {
1994
+ // re-subscribe when plugin kinds register after mount (async plugin load)
1995
+ void registryVersion
1937
1996
  if (mode !== 'delete') return
1938
1997
 
1939
1998
  const onClick = (event: NodeEvent) => {
@@ -2006,7 +2065,7 @@ export const SelectionManager = () => {
2006
2065
  }
2007
2066
  useViewer.setState({ hoveredId: null })
2008
2067
  }
2009
- }, [mode])
2068
+ }, [mode, registryVersion])
2010
2069
 
2011
2070
  return (
2012
2071
  <>
@@ -2096,6 +2155,7 @@ const SelectionMaterialSync = () => {
2096
2155
  const previewSelectedIds = useViewer((s) => s.previewSelectedIds)
2097
2156
  const hoveredId = useViewer((s) => s.hoveredId)
2098
2157
  const hoverHighlightMode = useViewer((s) => s.hoverHighlightMode)
2158
+ const registryVersion = useRegistryVersion()
2099
2159
  const geometryRevision = useViewer((s) => s.geometryRevision)
2100
2160
  const activeHighlightKindsRef = useRef(new Map<string, HighlightKind>())
2101
2161
  const highlightedMaterialsRef = useRef(
@@ -2118,6 +2178,10 @@ const SelectionMaterialSync = () => {
2118
2178
  continue
2119
2179
  }
2120
2180
 
2181
+ if (node && !isSelectionHighlightEnabled(node.type)) {
2182
+ continue
2183
+ }
2184
+
2121
2185
  const rootObject = sceneRegistry.nodes.get(id)
2122
2186
  if (!rootObject) {
2123
2187
  continue
@@ -2173,6 +2237,7 @@ const SelectionMaterialSync = () => {
2173
2237
  }, [])
2174
2238
 
2175
2239
  useEffect(() => {
2240
+ void registryVersion
2176
2241
  void geometryRevision
2177
2242
  const nextHighlightKinds = new Map<string, HighlightKind>()
2178
2243
 
@@ -2188,6 +2253,7 @@ const SelectionMaterialSync = () => {
2188
2253
  syncSelectionMaterials()
2189
2254
  }, [
2190
2255
  geometryRevision,
2256
+ registryVersion,
2191
2257
  hoverHighlightMode,
2192
2258
  hoveredId,
2193
2259
  previewSelectedIds,
@@ -2228,7 +2294,7 @@ const SelectionMaterialSync = () => {
2228
2294
  }, [])
2229
2295
 
2230
2296
  useEffect(() => {
2231
- return () => {
2297
+ const clearHighlights = () => {
2232
2298
  for (const [mesh, entry] of highlightedMaterialsRef.current.entries()) {
2233
2299
  if (mesh.material === entry.highlightedMaterial) {
2234
2300
  mesh.material = entry.originalMaterial
@@ -2238,6 +2304,11 @@ const SelectionMaterialSync = () => {
2238
2304
 
2239
2305
  highlightedMaterialsRef.current.clear()
2240
2306
  }
2307
+ const unsubscribe = registerMaterialCacheCleanup(clearHighlights)
2308
+ return () => {
2309
+ unsubscribe()
2310
+ clearHighlights()
2311
+ }
2241
2312
  }, [])
2242
2313
 
2243
2314
  return null
@@ -2249,11 +2320,13 @@ const EditorOutlinerSync = () => {
2249
2320
  const previewSelectedIds = useViewer((s) => s.previewSelectedIds)
2250
2321
  const hoveredId = useViewer((s) => s.hoveredId)
2251
2322
  const geometryRevision = useViewer((s) => s.geometryRevision)
2323
+ const registryVersion = useRegistryVersion()
2252
2324
  const outliner = useViewer((s) => s.outliner)
2253
2325
  const nodes = useScene((s) => s.nodes)
2254
2326
 
2255
2327
  useEffect(() => {
2256
2328
  void geometryRevision
2329
+ void registryVersion
2257
2330
  let idsToHighlight: string[] = []
2258
2331
 
2259
2332
  // 1. Determine what should be highlighted based on Phase
@@ -2288,7 +2361,8 @@ const EditorOutlinerSync = () => {
2288
2361
  // 2. Sync with the imperative outliner arrays (mutate in place to keep references)
2289
2362
  outliner.selectedObjects.length = 0
2290
2363
  for (const id of idsToHighlight) {
2291
- if (!nodes[id as AnyNodeId]) continue
2364
+ const node = nodes[id as AnyNodeId]
2365
+ if (!(node && isSelectionHighlightEnabled(node.type))) continue
2292
2366
  const obj = sceneRegistry.nodes.get(id)
2293
2367
  if (obj?.parent) outliner.selectedObjects.push(obj)
2294
2368
  }
@@ -2299,14 +2373,25 @@ const EditorOutlinerSync = () => {
2299
2373
  useViewer.setState({ hoveredId: null })
2300
2374
  } else {
2301
2375
  const hoveredNode = nodes[hoveredId as AnyNodeId]
2302
- const obj =
2303
- hoveredNode?.type === 'roof-segment'
2304
- ? (getHoveredRoofSegmentOutlineProxy(hoveredId) ?? sceneRegistry.nodes.get(hoveredId))
2305
- : sceneRegistry.nodes.get(hoveredId)
2306
- 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
+ }
2307
2383
  }
2308
2384
  }
2309
- }, [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
+ ])
2310
2395
 
2311
2396
  return null
2312
2397
  }
@@ -50,6 +50,7 @@ export function SiteEdgeLabels() {
50
50
  })
51
51
  const activeHandleDrag = useActiveHandleDrag()
52
52
  const unit = useViewer((state) => state.unit)
53
+ const metricNotation = useViewer((state) => state.metricNotation)
53
54
  const cameraMode = useViewer((state) => state.cameraMode)
54
55
  const isNight = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
55
56
  const camera = useThree((state) => state.camera)
@@ -132,7 +133,7 @@ export function SiteEdgeLabels() {
132
133
  textShadow: `-1.5px -1.5px 0 ${shadowColor}, 1.5px -1.5px 0 ${shadowColor}, -1.5px 1.5px 0 ${shadowColor}, 1.5px 1.5px 0 ${shadowColor}, 0 0 4px ${shadowColor}, 0 0 4px ${shadowColor}`,
133
134
  }}
134
135
  >
135
- {formatLinearMeasurement(edge.dist, unit)}
136
+ {formatLinearMeasurement(edge.dist, unit, metricNotation)}
136
137
  </div>
137
138
  </Html>
138
139
  ))}