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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +15 -8
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/bake-exporter.tsx +5 -0
  6. package/src/components/editor/bake-thumbnail.tsx +4 -1
  7. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  8. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  9. package/src/components/editor/capture-camera-rig.tsx +59 -0
  10. package/src/components/editor/custom-camera-controls.tsx +20 -11
  11. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  12. package/src/components/editor/editor-layout-v2.tsx +9 -2
  13. package/src/components/editor/export-manager.tsx +175 -33
  14. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  15. package/src/components/editor/first-person-controls.tsx +180 -12
  16. package/src/components/editor/floating-action-menu.tsx +55 -51
  17. package/src/components/editor/floorplan-panel.tsx +99 -15
  18. package/src/components/editor/grid.tsx +12 -7
  19. package/src/components/editor/group-actions.ts +34 -50
  20. package/src/components/editor/group-move-3d.ts +51 -21
  21. package/src/components/editor/group-transform-shared.test.ts +78 -0
  22. package/src/components/editor/group-transform-shared.ts +74 -7
  23. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  24. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  25. package/src/components/editor/handles/handle-placement.test.ts +111 -0
  26. package/src/components/editor/handles/handle-placement.ts +69 -0
  27. package/src/components/editor/handles/linear-resize-drag.ts +78 -0
  28. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  29. package/src/components/editor/handles/resize-snap.ts +10 -1
  30. package/src/components/editor/handles/use-handle-drag.ts +23 -3
  31. package/src/components/editor/index.tsx +151 -24
  32. package/src/components/editor/node-action-menu.tsx +14 -1
  33. package/src/components/editor/node-arrow-handles.tsx +67 -119
  34. package/src/components/editor/selection-manager.tsx +153 -42
  35. package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
  36. package/src/components/editor/snapshot-capture.test.ts +422 -0
  37. package/src/components/editor/snapshot-capture.ts +145 -0
  38. package/src/components/editor/thumbnail-generator.tsx +295 -239
  39. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  40. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  41. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  42. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  43. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
  44. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  45. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  46. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  47. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  48. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  49. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  50. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
  51. package/src/components/procedural-items/inspector.tsx +223 -0
  52. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  53. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  54. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  55. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  56. package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
  57. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  58. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  59. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  60. package/src/components/systems/selection-affordance-services.ts +19 -0
  61. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  62. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  63. package/src/components/tools/fence/fence-drafting.ts +6 -25
  64. package/src/components/tools/item/draft-creation.test.ts +12 -0
  65. package/src/components/tools/item/draft-creation.ts +10 -0
  66. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  67. package/src/components/tools/item/face-host-commit.ts +56 -0
  68. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  69. package/src/components/tools/item/face-host-preview.ts +82 -0
  70. package/src/components/tools/item/item-surface-placement.test.ts +175 -0
  71. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
  72. package/src/components/tools/item/move-tool.tsx +4 -3
  73. package/src/components/tools/item/placement-math.test.ts +11 -1
  74. package/src/components/tools/item/placement-math.ts +0 -20
  75. package/src/components/tools/item/placement-strategies.test.ts +331 -4
  76. package/src/components/tools/item/placement-strategies.ts +304 -171
  77. package/src/components/tools/item/placement-surface.test.ts +61 -0
  78. package/src/components/tools/item/placement-surface.ts +24 -0
  79. package/src/components/tools/item/placement-types.ts +10 -0
  80. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
  81. package/src/components/tools/item/test-face-host.ts +121 -0
  82. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  83. package/src/components/tools/item/use-draft-node.ts +116 -44
  84. package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
  85. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  86. package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
  87. package/src/components/tools/registry/item-surface-move.test.ts +501 -0
  88. package/src/components/tools/registry/item-surface-move.ts +441 -0
  89. package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
  90. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  91. package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
  92. package/src/components/tools/registry-tool-context.tsx +30 -0
  93. package/src/components/tools/select/box-select-tool.tsx +3 -1
  94. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  95. package/src/components/tools/select/marquee-footprint.ts +29 -0
  96. package/src/components/tools/shared/placement-box.tsx +57 -7
  97. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  98. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  99. package/src/components/tools/shared/pointer-support-cap.ts +56 -17
  100. package/src/components/tools/shared/shelf-stickiness.ts +54 -0
  101. package/src/components/tools/shared/surface-hit.test.ts +56 -0
  102. package/src/components/tools/shared/surface-hit.ts +23 -0
  103. package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
  104. package/src/components/tools/shared/surface-rejection.tsx +29 -0
  105. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  106. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  107. package/src/components/tools/stair/stair-tool.tsx +127 -74
  108. package/src/components/tools/tool-manager.tsx +25 -8
  109. package/src/components/tools/wall/wall-drafting.test.ts +320 -2
  110. package/src/components/tools/wall/wall-drafting.ts +23 -114
  111. package/src/components/tools/zone/zone-tool.tsx +13 -4
  112. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  113. package/src/components/ui/action-menu/index.tsx +11 -1
  114. package/src/components/ui/command-palette/editor-commands.tsx +63 -13
  115. package/src/components/ui/command-palette/index.tsx +7 -2
  116. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  117. package/src/components/ui/controls/material-picker.tsx +4 -4
  118. package/src/components/ui/controls/metric-control.tsx +29 -13
  119. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  120. package/src/components/ui/controls/segmented-control.tsx +8 -1
  121. package/src/components/ui/controls/slider-control.tsx +44 -21
  122. package/src/components/ui/controls/toggle-control.tsx +18 -5
  123. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  124. package/src/components/ui/floating-level-selector.tsx +103 -10
  125. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  126. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  127. package/src/components/ui/helpers/item-helper.tsx +1 -1
  128. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  129. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  130. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  131. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  132. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  133. package/src/components/ui/panels/multi-field-value.ts +163 -0
  134. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  135. package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
  136. package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
  137. package/src/components/ui/panels/node-display.ts +4 -0
  138. package/src/components/ui/panels/panel-manager.tsx +77 -3
  139. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  140. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  141. package/src/components/ui/panels/parametric-inspector.tsx +35 -164
  142. package/src/components/ui/panels/reference-panel.tsx +34 -4
  143. package/src/components/ui/primitives/checkbox.tsx +34 -0
  144. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  145. package/src/components/ui/scene-loader.tsx +35 -0
  146. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
  147. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
  148. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
  149. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
  150. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
  151. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  152. package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
  153. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
  154. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  155. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  156. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
  157. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  158. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
  159. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
  160. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
  161. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
  162. package/src/components/ui/sidebar/tab-bar.tsx +2 -0
  163. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  164. package/src/components/ui/snap-target-badge.tsx +2 -0
  165. package/src/components/viewer/viewer-scene-header.tsx +5 -1
  166. package/src/components/viewer/viewer-stage.tsx +4 -1
  167. package/src/components/viewer/viewer-units-panel.tsx +116 -0
  168. package/src/components/viewer-overlay.tsx +1 -22
  169. package/src/components/viewer-zone-system.tsx +6 -2
  170. package/src/components/walkthrough-hud.tsx +19 -10
  171. package/src/hooks/use-auto-save.test.ts +73 -1
  172. package/src/hooks/use-auto-save.ts +76 -5
  173. package/src/hooks/use-ceiling-events.test.ts +144 -0
  174. package/src/hooks/use-ceiling-events.ts +5 -1
  175. package/src/hooks/use-drag-action.ts +17 -3
  176. package/src/hooks/use-grid-events.test.ts +240 -0
  177. package/src/hooks/use-grid-events.ts +190 -11
  178. package/src/hooks/use-keyboard.test.ts +244 -0
  179. package/src/hooks/use-keyboard.ts +132 -63
  180. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  181. package/src/hooks/use-save-shortcut.ts +33 -0
  182. package/src/index.tsx +87 -4
  183. package/src/lib/active-placement-surface.test.ts +16 -0
  184. package/src/lib/active-placement-surface.ts +14 -0
  185. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  186. package/src/lib/ceiling-surface-raycast.ts +12 -0
  187. package/src/lib/contextual-help-extension.ts +17 -0
  188. package/src/lib/continuation.test.ts +11 -0
  189. package/src/lib/continuation.ts +8 -1
  190. package/src/lib/direct-manipulation.test.ts +132 -0
  191. package/src/lib/direct-manipulation.ts +38 -1
  192. package/src/lib/editor-api.ts +6 -3
  193. package/src/lib/elevation-guides.test.ts +28 -1
  194. package/src/lib/elevation-guides.ts +22 -7
  195. package/src/lib/export-texture-utils.test.ts +167 -0
  196. package/src/lib/export-texture-utils.ts +121 -0
  197. package/src/lib/export-user-data.test.ts +48 -0
  198. package/src/lib/export-user-data.ts +69 -0
  199. package/src/lib/floating-menu-scale.test.ts +29 -0
  200. package/src/lib/floating-menu-scale.ts +14 -0
  201. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  202. package/src/lib/floorplan/apply-alignment.ts +10 -1
  203. package/src/lib/floorplan/floorplan-export.test.ts +536 -2
  204. package/src/lib/floorplan/floorplan-export.tsx +120 -19
  205. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  206. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  207. package/src/lib/floorplan/floorplan-readonly.ts +5 -0
  208. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  209. package/src/lib/floorplan-grid-event-point.ts +13 -0
  210. package/src/lib/fresh-planar-placement.ts +13 -5
  211. package/src/lib/glb-export-serialize.test.ts +280 -0
  212. package/src/lib/glb-export.test.ts +1375 -21
  213. package/src/lib/glb-export.ts +781 -106
  214. package/src/lib/grid-event-presentation.ts +16 -0
  215. package/src/lib/history.test.ts +528 -147
  216. package/src/lib/history.ts +81 -7
  217. package/src/lib/host-tree-children.test.ts +27 -0
  218. package/src/lib/host-tree-children.ts +79 -0
  219. package/src/lib/interaction/hot-set.test.ts +10 -1
  220. package/src/lib/interaction/hot-set.ts +3 -1
  221. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  222. package/src/lib/interaction/overlay-policy.ts +16 -0
  223. package/src/lib/interaction/registered-drafting.ts +36 -0
  224. package/src/lib/interaction/scope.ts +31 -0
  225. package/src/lib/level-duplication.test.ts +73 -0
  226. package/src/lib/level-duplication.ts +32 -2
  227. package/src/lib/level-print-export.test.ts +484 -0
  228. package/src/lib/level-print-export.ts +526 -0
  229. package/src/lib/linear-display.test.ts +59 -0
  230. package/src/lib/linear-display.ts +39 -0
  231. package/src/lib/local-guide-image.test.ts +24 -0
  232. package/src/lib/local-guide-image.ts +34 -1
  233. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  234. package/src/lib/local-project-presentation-persistence.ts +335 -0
  235. package/src/lib/material-paint.ts +2 -0
  236. package/src/lib/model-export.ts +29 -0
  237. package/src/lib/node-action-movement.ts +21 -0
  238. package/src/lib/paint-preview-owner.ts +71 -0
  239. package/src/lib/planar-cursor-placement.test.ts +178 -1
  240. package/src/lib/planar-cursor-placement.ts +61 -5
  241. package/src/lib/plugin-panels.test.ts +92 -1
  242. package/src/lib/plugin-panels.ts +48 -0
  243. package/src/lib/portable-export.test.ts +455 -0
  244. package/src/lib/portable-export.ts +1042 -0
  245. package/src/lib/print-3mf.test.ts +86 -0
  246. package/src/lib/print-3mf.ts +161 -0
  247. package/src/lib/print-content-scope.test.ts +107 -0
  248. package/src/lib/print-content-scope.ts +90 -0
  249. package/src/lib/print-export.test.ts +294 -0
  250. package/src/lib/print-export.ts +735 -0
  251. package/src/lib/print-feature-thickness.test.ts +147 -0
  252. package/src/lib/print-feature-thickness.ts +180 -0
  253. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  254. package/src/lib/print-golden-house.test.ts +249 -0
  255. package/src/lib/print-roof-solids.test.ts +133 -0
  256. package/src/lib/print-roof-solids.ts +630 -0
  257. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  258. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  259. package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
  260. package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
  261. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  262. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  263. package/src/lib/print-shell-compiler-protocol.ts +38 -0
  264. package/src/lib/print-shell-compiler.ts +273 -0
  265. package/src/lib/print-wall-solids.test.ts +194 -0
  266. package/src/lib/print-wall-solids.ts +328 -0
  267. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  268. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  269. package/src/lib/scene-clipboard.ts +1 -1
  270. package/src/lib/scene.ts +22 -8
  271. package/src/lib/selection-routing.test.ts +105 -1
  272. package/src/lib/selection-routing.ts +19 -0
  273. package/src/lib/send-to-app.test.ts +108 -0
  274. package/src/lib/send-to-app.ts +121 -0
  275. package/src/lib/sfx/movement-tick.ts +37 -0
  276. package/src/lib/sfx-bus.ts +4 -0
  277. package/src/lib/sfx-player.ts +8 -0
  278. package/src/lib/snapping-mode.test.ts +32 -4
  279. package/src/lib/snapping-mode.ts +11 -7
  280. package/src/lib/surface-attachment.ts +77 -0
  281. package/src/lib/units.ts +254 -0
  282. package/src/lib/usdz-export.ts +65 -0
  283. package/src/lib/use-linear-display.ts +8 -32
  284. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  285. package/src/store/live-draft-preview-stores.test.ts +3 -0
  286. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  287. package/src/store/tool-mode.test.ts +87 -0
  288. package/src/store/use-camera-hint-focus.ts +48 -0
  289. package/src/store/use-editor.tsx +299 -188
  290. package/src/store/use-floorplan-draft-preview.ts +9 -0
  291. package/src/store/use-handle-group.ts +4 -0
  292. package/src/store/use-interaction-scope.test.ts +23 -1
  293. package/src/store/use-interaction-scope.ts +63 -2
  294. package/src/store/use-placement-preview.ts +56 -3
  295. package/src/store/use-send-to-app.ts +21 -0
  296. package/src/components/tools/roof/roof-tool.tsx +0 -656
  297. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -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,
@@ -70,6 +81,12 @@ import {
70
81
  shouldPreserveSelectedRoofHostTarget,
71
82
  } from '../../lib/selection-routing'
72
83
  import { emitDeleteSFX, sfxEmitter } from '../../lib/sfx-bus'
84
+ import {
85
+ cancelPendingZonePaint,
86
+ paintZoneMembership,
87
+ zoneAtLevelPoint,
88
+ zoneAtWorldPoint,
89
+ } from '../../lib/units'
73
90
  import useDirectManipulationFeedback from '../../store/use-direct-manipulation-feedback'
74
91
  import useEditor, { type MaterialTargetRole } from './../../store/use-editor'
75
92
  import useInteractionScope, {
@@ -112,8 +129,6 @@ type SelectableNodeType =
112
129
  | 'window'
113
130
  | 'door'
114
131
 
115
- type PaintPreviewCleanup = () => void
116
-
117
132
  type PaintInteraction = {
118
133
  key: string
119
134
  apply: (() => void) | null
@@ -805,6 +820,7 @@ export const SelectionManager = () => {
805
820
  if (mode !== 'material-paint') return
806
821
  if (movingNode || isCurveReshape) return
807
822
 
823
+ const previewOwner = createPaintPreviewOwner()
808
824
  let activePreview: { key: string; restore: PaintPreviewCleanup } | null = null
809
825
  // The last hover event, replayed when the application scope cycles so the
810
826
  // preview + chip update under a stationary cursor (Shift fires no pointer move).
@@ -826,7 +842,7 @@ export const SelectionManager = () => {
826
842
  selectedMaterialTarget: useEditor.getState().selectedMaterialTarget,
827
843
  })
828
844
 
829
- const getPaintInteraction = (event: NodeEvent): PaintInteraction | null => {
845
+ const resolvePaintInteraction = (event: NodeEvent): PaintInteraction | null => {
830
846
  const eraser = useEditor.getState().paintEraser
831
847
  const activePaintMaterial = resolveActivePaintMaterial()
832
848
  const node = event.node
@@ -949,27 +965,29 @@ export const SelectionManager = () => {
949
965
  // paint capability builds the preview; restores combine.
950
966
  const restores: PaintPreviewCleanup[] = []
951
967
  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)
968
+ try {
969
+ for (const target of scopeTargets) {
970
+ const targetNode = sceneNodes[target.nodeId]
971
+ const targetRoot = getRegisteredNodeObject(target.nodeId)
972
+ const targetCap = targetNode
973
+ ? nodeRegistry.get(targetNode.type)?.capabilities?.paint
974
+ : null
975
+ if (!(targetNode && targetRoot && targetCap)) continue
976
+ const restore = targetCap.applyPreview({
977
+ node: targetNode,
978
+ role: target.role,
979
+ material: paintSpec.material,
980
+ materialPreset: paintSpec.materialPreset,
981
+ root: targetRoot,
982
+ })
983
+ if (restore) restores.push(restore)
984
+ }
985
+ } catch (error) {
986
+ combinePaintPreviews(restores)()
987
+ throw error
967
988
  }
968
989
  if (restores.length === 0) return null
969
- return () => {
970
- for (let index = restores.length - 1; index >= 0; index -= 1)
971
- restores[index]?.()
972
- }
990
+ return combinePaintPreviews(restores)
973
991
  }
974
992
  : () => previewCursor('not-allowed'),
975
993
  }
@@ -1069,6 +1087,9 @@ export const SelectionManager = () => {
1069
1087
  return null
1070
1088
  }
1071
1089
 
1090
+ const getPaintInteraction = (event: NodeEvent) =>
1091
+ previewOwner.wrap(resolvePaintInteraction(event))
1092
+
1072
1093
  const onEnter = (event: NodeEvent) => {
1073
1094
  // A host-driven drag (handle resize/rotate) sets `inputDragging`.
1074
1095
  // useNodeEvents now emits hover events during such a drag so surface
@@ -1239,8 +1260,10 @@ export const SelectionManager = () => {
1239
1260
  if (movingNode || isCurveReshape) return
1240
1261
 
1241
1262
  const onPointerDown = (event: NodeEvent) => {
1263
+ if (!selectionEnabled(useInteractionScope.getState().scope)) return
1242
1264
  const pointer = pointerEventFromNodeEvent(event)
1243
1265
  if (pointer.button !== 0) return
1266
+ const handleOwnsPointer = pointerEventHitsEditorHandle(event.nativeEvent)
1244
1267
 
1245
1268
  // Plain press on a transformable member of a multi-selection arms the
1246
1269
  // group move — dragging slides the whole selection on the ground plane
@@ -1248,6 +1271,7 @@ export const SelectionManager = () => {
1248
1271
  // group-move gizmo cross). A plain click (no drag) still falls through
1249
1272
  // to the normal click handling, which collapses to the pressed node.
1250
1273
  if (
1274
+ !handleOwnsPointer &&
1251
1275
  !(pointer.shiftKey || pointer.altKey || isCommandModifier(pointer)) &&
1252
1276
  armGroupMove3d({
1253
1277
  nodeId: event.node.id as AnyNodeId,
@@ -1263,8 +1287,6 @@ export const SelectionManager = () => {
1263
1287
  return
1264
1288
  }
1265
1289
 
1266
- if (!isCommandModifier(pointer)) return
1267
-
1268
1290
  const eventNode = useScene.getState().nodes[event.node.id as AnyNodeId] ?? event.node
1269
1291
  const node = resolveCanvasSelectionNode({
1270
1292
  node: eventNode,
@@ -1272,18 +1294,26 @@ export const SelectionManager = () => {
1272
1294
  selectedIds: useViewer.getState().selection.selectedIds,
1273
1295
  })
1274
1296
  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
1297
  const currentSelectedIds = useViewer.getState().selection.selectedIds
1280
- if (currentSelectedIds.length !== 1 || currentSelectedIds[0] !== node.id) return
1298
+ const allowPlainDrag = nodeRegistry.get(node.type)?.capabilities?.movable?.directDrag === true
1299
+ if (
1300
+ !shouldStartDirectMoveDrag({
1301
+ allowPlainDrag,
1302
+ commandModifier: isCommandModifier(pointer),
1303
+ handleOwnsPointer,
1304
+ nodeId: node.id,
1305
+ selectedIds: currentSelectedIds,
1306
+ })
1307
+ ) {
1308
+ return
1309
+ }
1281
1310
 
1282
1311
  const startX = pointer.clientX
1283
1312
  const startY = pointer.clientY
1284
1313
  const pointerId = pointer.pointerId
1285
1314
  const pointerTarget = pointer.target instanceof EventTarget ? pointer.target : null
1286
1315
  let engaged = false
1316
+ let engagedTargetId: AnyNodeId | null = null
1287
1317
 
1288
1318
  const cleanup = () => {
1289
1319
  window.removeEventListener('pointermove', onMove)
@@ -1305,8 +1335,9 @@ export const SelectionManager = () => {
1305
1335
  useViewer.getState().setInputDragging(true)
1306
1336
  swallowNextClick()
1307
1337
  createEditorApi().engageMoveDrag(node)
1338
+ engagedTargetId = (getMovingNode()?.id as AnyNodeId | undefined) ?? null
1308
1339
  requestAnimationFrame(() => {
1309
- if (getMovingNode()?.id !== node.id) return
1340
+ if (!getMovingNode()) return
1310
1341
  pointerTarget?.dispatchEvent(
1311
1342
  new PointerEvent('pointermove', {
1312
1343
  altKey: moveEvent.altKey,
@@ -1330,7 +1361,7 @@ export const SelectionManager = () => {
1330
1361
  if (engaged) {
1331
1362
  requestAnimationFrame(() => {
1332
1363
  const editor = useEditor.getState()
1333
- if (getMovingNode()?.id !== node.id || !editor.placementDragMode) return
1364
+ if (getMovingNode()?.id !== engagedTargetId || !editor.placementDragMode) return
1334
1365
  editor.setMovingNode(null)
1335
1366
  })
1336
1367
  }
@@ -1392,7 +1423,7 @@ export const SelectionManager = () => {
1392
1423
  if (key === prevKey) return
1393
1424
  prevKey = key
1394
1425
  let wantsMove = false
1395
- if (hoveredId && !getMovingNode()) {
1426
+ if (hoveredId && !getMovingNode() && selectionEnabled(useInteractionScope.getState().scope)) {
1396
1427
  if (sole === hoveredId) {
1397
1428
  const node = useScene.getState().nodes[sole as AnyNodeId]
1398
1429
  wantsMove = !!node && canDirectMoveNode(node)
@@ -1557,6 +1588,7 @@ export const SelectionManager = () => {
1557
1588
  // body click so only the reshape tool handles the release. (Scoped to
1558
1589
  // `endpoint`: hole-edit relies on node clicks to exit, just below.)
1559
1590
  const activeScope = useInteractionScope.getState().scope
1591
+ if (activeScope.kind === 'mesh-editing') return
1560
1592
  if (activeScope.kind === 'reshaping' && activeScope.reshape === 'endpoint') return
1561
1593
 
1562
1594
  if (dispatchSceneAction(event.node, getEventObject(event))) {
@@ -1583,6 +1615,23 @@ export const SelectionManager = () => {
1583
1615
  // the click falls through to the item underneath.
1584
1616
  if (node.type === 'ceiling' && !event.viaHandle) return
1585
1617
 
1618
+ // Unit focus turns clicks inside a zone into the paint gesture:
1619
+ // membership toggles, the zone stays unselected, focus stays.
1620
+ const focusedUnitId = useViewer.getState().focusedUnitId
1621
+ if (focusedUnitId) {
1622
+ const zone =
1623
+ node.type === 'zone' ? node : zoneAtWorldPoint(event.position[0], event.position[2])
1624
+ if (zone) {
1625
+ event.stopPropagation()
1626
+ clickHandledRef.current = true
1627
+ setTimeout(() => {
1628
+ clickHandledRef.current = false
1629
+ }, 50)
1630
+ paintZoneMembership(focusedUnitId, zone.id)
1631
+ return
1632
+ }
1633
+ }
1634
+
1586
1635
  let currentPhase = useEditor.getState().phase
1587
1636
  let currentStructureLayer = useEditor.getState().structureLayer
1588
1637
  const selectedIdsBeforeRouting = useViewer.getState().selection.selectedIds
@@ -1655,9 +1704,16 @@ export const SelectionManager = () => {
1655
1704
  const hasModifier = nativeEvent.shiftKey || isCommandModifier(nativeEvent)
1656
1705
  const isAlreadySole =
1657
1706
  selectedIdsBeforeRouting.length === 1 && selectedIdsBeforeRouting[0] === nodeToSelect.id
1658
- if (!hasModifier && isAlreadySole && !getMovingNode() && canDirectMoveNode(nodeToSelect)) {
1707
+ if (
1708
+ useEditor.getState().mode !== 'delete' &&
1709
+ !hasModifier &&
1710
+ isAlreadySole &&
1711
+ !getMovingNode() &&
1712
+ canDirectMoveNode(nodeToSelect)
1713
+ ) {
1659
1714
  sfxEmitter.emit('sfx:item-pick')
1660
- useEditor.getState().setMovingNode(nodeToSelect as never)
1715
+ const moveTarget = resolveDirectManipulationNode(nodeToSelect, useScene.getState().nodes)
1716
+ useEditor.getState().setMovingNode(moveTarget as never)
1661
1717
  useViewer.getState().setSelection({ selectedIds: [] })
1662
1718
  return
1663
1719
  }
@@ -1773,8 +1829,17 @@ export const SelectionManager = () => {
1773
1829
  const onGridClick = (event: GridEvent) => {
1774
1830
  if (clickHandledRef.current) return
1775
1831
  if (boxSelectHandled) return
1832
+ if (useInteractionScope.getState().scope.kind === 'mesh-editing') return
1776
1833
  const nativeEvent = event.nativeEvent
1777
1834
  if (nativeEvent?.metaKey || nativeEvent?.ctrlKey || nativeEvent?.shiftKey) return
1835
+ // Unit focus: a ground click inside a zone paints it; elsewhere it
1836
+ // leaves focus and the unit selection alone.
1837
+ const focusedUnitId = useViewer.getState().focusedUnitId
1838
+ if (focusedUnitId) {
1839
+ const zone = zoneAtLevelPoint(event.localPosition[0], event.localPosition[2])
1840
+ if (zone) paintZoneMembership(focusedUnitId, zone.id)
1841
+ return
1842
+ }
1778
1843
  const { phase, structureLayer } = useEditor.getState()
1779
1844
  const activeStrategy = SELECTION_STRATEGIES[phase]
1780
1845
  if (activeStrategy) activeStrategy.handleDeselect()
@@ -1804,6 +1869,7 @@ export const SelectionManager = () => {
1804
1869
  if (movingNode || isCurveReshape) return
1805
1870
 
1806
1871
  const onEnter = (event: NodeEvent) => {
1872
+ if (useInteractionScope.getState().scope.kind === 'mesh-editing') return
1807
1873
  // A host-driven drag (handle resize/rotate, box-select) sets
1808
1874
  // `inputDragging`. useNodeEvents still emits hover events during it so
1809
1875
  // surface move tools keep tracking — but the select-hover outline must
@@ -1852,6 +1918,7 @@ export const SelectionManager = () => {
1852
1918
  }
1853
1919
 
1854
1920
  const onDoubleClick = (event: NodeEvent) => {
1921
+ if (useInteractionScope.getState().scope.kind === 'mesh-editing') return
1855
1922
  let node = resolveCanvasSelectionNode({
1856
1923
  node: resolveSelectModeNodeTarget(event),
1857
1924
  nodes: useScene.getState().nodes,
@@ -1883,6 +1950,24 @@ export const SelectionManager = () => {
1883
1950
  }
1884
1951
  }
1885
1952
 
1953
+ // While a unit is focused a double-click inside a zone selects that
1954
+ // zone (focus stays); the two clicks before it cancel each other's paint.
1955
+ if (useViewer.getState().focusedUnitId) {
1956
+ const zone =
1957
+ node.type === 'zone' ? node : zoneAtWorldPoint(event.position[0], event.position[2])
1958
+ if (zone) {
1959
+ event.stopPropagation()
1960
+ cancelPendingZonePaint(zone.id)
1961
+ SELECTION_STRATEGIES.structure?.handleSelect(
1962
+ zone,
1963
+ event.nativeEvent,
1964
+ modifierKeysRef.current,
1965
+ [],
1966
+ )
1967
+ return
1968
+ }
1969
+ }
1970
+
1886
1971
  if (node.type === 'zone') {
1887
1972
  return
1888
1973
  }
@@ -2119,6 +2204,7 @@ const SelectionMaterialSync = () => {
2119
2204
  const previewSelectedIds = useViewer((s) => s.previewSelectedIds)
2120
2205
  const hoveredId = useViewer((s) => s.hoveredId)
2121
2206
  const hoverHighlightMode = useViewer((s) => s.hoverHighlightMode)
2207
+ const registryVersion = useRegistryVersion()
2122
2208
  const geometryRevision = useViewer((s) => s.geometryRevision)
2123
2209
  const activeHighlightKindsRef = useRef(new Map<string, HighlightKind>())
2124
2210
  const highlightedMaterialsRef = useRef(
@@ -2141,6 +2227,10 @@ const SelectionMaterialSync = () => {
2141
2227
  continue
2142
2228
  }
2143
2229
 
2230
+ if (node && !isSelectionHighlightEnabled(node.type)) {
2231
+ continue
2232
+ }
2233
+
2144
2234
  const rootObject = sceneRegistry.nodes.get(id)
2145
2235
  if (!rootObject) {
2146
2236
  continue
@@ -2196,6 +2286,7 @@ const SelectionMaterialSync = () => {
2196
2286
  }, [])
2197
2287
 
2198
2288
  useEffect(() => {
2289
+ void registryVersion
2199
2290
  void geometryRevision
2200
2291
  const nextHighlightKinds = new Map<string, HighlightKind>()
2201
2292
 
@@ -2211,6 +2302,7 @@ const SelectionMaterialSync = () => {
2211
2302
  syncSelectionMaterials()
2212
2303
  }, [
2213
2304
  geometryRevision,
2305
+ registryVersion,
2214
2306
  hoverHighlightMode,
2215
2307
  hoveredId,
2216
2308
  previewSelectedIds,
@@ -2251,7 +2343,7 @@ const SelectionMaterialSync = () => {
2251
2343
  }, [])
2252
2344
 
2253
2345
  useEffect(() => {
2254
- return () => {
2346
+ const clearHighlights = () => {
2255
2347
  for (const [mesh, entry] of highlightedMaterialsRef.current.entries()) {
2256
2348
  if (mesh.material === entry.highlightedMaterial) {
2257
2349
  mesh.material = entry.originalMaterial
@@ -2261,6 +2353,11 @@ const SelectionMaterialSync = () => {
2261
2353
 
2262
2354
  highlightedMaterialsRef.current.clear()
2263
2355
  }
2356
+ const unsubscribe = registerMaterialCacheCleanup(clearHighlights)
2357
+ return () => {
2358
+ unsubscribe()
2359
+ clearHighlights()
2360
+ }
2264
2361
  }, [])
2265
2362
 
2266
2363
  return null
@@ -2272,11 +2369,13 @@ const EditorOutlinerSync = () => {
2272
2369
  const previewSelectedIds = useViewer((s) => s.previewSelectedIds)
2273
2370
  const hoveredId = useViewer((s) => s.hoveredId)
2274
2371
  const geometryRevision = useViewer((s) => s.geometryRevision)
2372
+ const registryVersion = useRegistryVersion()
2275
2373
  const outliner = useViewer((s) => s.outliner)
2276
2374
  const nodes = useScene((s) => s.nodes)
2277
2375
 
2278
2376
  useEffect(() => {
2279
2377
  void geometryRevision
2378
+ void registryVersion
2280
2379
  let idsToHighlight: string[] = []
2281
2380
 
2282
2381
  // 1. Determine what should be highlighted based on Phase
@@ -2311,7 +2410,8 @@ const EditorOutlinerSync = () => {
2311
2410
  // 2. Sync with the imperative outliner arrays (mutate in place to keep references)
2312
2411
  outliner.selectedObjects.length = 0
2313
2412
  for (const id of idsToHighlight) {
2314
- if (!nodes[id as AnyNodeId]) continue
2413
+ const node = nodes[id as AnyNodeId]
2414
+ if (!(node && isSelectionHighlightEnabled(node.type))) continue
2315
2415
  const obj = sceneRegistry.nodes.get(id)
2316
2416
  if (obj?.parent) outliner.selectedObjects.push(obj)
2317
2417
  }
@@ -2322,14 +2422,25 @@ const EditorOutlinerSync = () => {
2322
2422
  useViewer.setState({ hoveredId: null })
2323
2423
  } else {
2324
2424
  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)
2425
+ if (hoveredNode && isSelectionHighlightEnabled(hoveredNode.type)) {
2426
+ const obj =
2427
+ hoveredNode.type === 'roof-segment'
2428
+ ? (getHoveredRoofSegmentOutlineProxy(hoveredId) ?? sceneRegistry.nodes.get(hoveredId))
2429
+ : sceneRegistry.nodes.get(hoveredId)
2430
+ if (obj?.parent) outliner.hoveredObjects.push(obj)
2431
+ }
2330
2432
  }
2331
2433
  }
2332
- }, [geometryRevision, phase, previewSelectedIds, selection, hoveredId, outliner, nodes])
2434
+ }, [
2435
+ geometryRevision,
2436
+ registryVersion,
2437
+ phase,
2438
+ previewSelectedIds,
2439
+ selection,
2440
+ hoveredId,
2441
+ outliner,
2442
+ nodes,
2443
+ ])
2333
2444
 
2334
2445
  return null
2335
2446
  }