@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
@@ -10,6 +10,7 @@ import {
10
10
  type FloorplanGeometry,
11
11
  type FloorplanPalette,
12
12
  type FloorplanPoint,
13
+ type FloorplanScope,
13
14
  type GeometryContext,
14
15
  isNodeKindEnabled,
15
16
  isRegistryMovable,
@@ -26,7 +27,7 @@ import {
26
27
  useLiveTransforms,
27
28
  useScene,
28
29
  } from '@pascal-app/core'
29
- import { useViewer } from '@pascal-app/viewer'
30
+ import { beginPerfAction, cancelPerfAction, commitPerfAction, useViewer } from '@pascal-app/viewer'
30
31
  import {
31
32
  type ComponentProps,
32
33
  memo,
@@ -39,6 +40,7 @@ import {
39
40
  useRef,
40
41
  useState,
41
42
  } from 'react'
43
+ import { useShallow } from 'zustand/react/shallow'
42
44
  import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
43
45
  import { ROTATE_HANDLE_DRAG_LABEL } from '../../../lib/contextual-help'
44
46
  import {
@@ -46,6 +48,7 @@ import {
46
48
  resolveDirectManipulationNode,
47
49
  resolveDirectRotationDragDelta,
48
50
  resolveDirectRotationPatch,
51
+ shouldStartDirectMoveDrag,
49
52
  snapDirectRotationDelta,
50
53
  } from '../../../lib/direct-manipulation'
51
54
  import { createEditorApi } from '../../../lib/editor-api'
@@ -85,6 +88,7 @@ import {
85
88
  import { emitCanvasNodeSelection } from '../../../lib/selection-routing'
86
89
  import { sfxEmitter } from '../../../lib/sfx-bus'
87
90
  import { clearSurfacePlanSnapFeedback } from '../../../lib/surface-plan-snap'
91
+ import { paintZoneMembership } from '../../../lib/units'
88
92
  import useDirectManipulationFeedback from '../../../store/use-direct-manipulation-feedback'
89
93
  import useDrawingView from '../../../store/use-drawing-view'
90
94
  import useEditor, { isAngleSnapActive } from '../../../store/use-editor'
@@ -246,6 +250,7 @@ export function cancelFloorplanAffordanceDrag(
246
250
  for (const id of drag.session.affectedIds) effects.clearPreview(id)
247
251
  effects.endReshapeScope(drag)
248
252
  effects.clearDragFeedback?.()
253
+ cancelPerfAction()
249
254
  return true
250
255
  }
251
256
 
@@ -310,6 +315,14 @@ export function floorplanAffordanceReshapeScope(
310
315
  return null
311
316
  }
312
317
 
318
+ function floorplanAffordancePerfAction(node: AnyNode, affordance: string): string {
319
+ if (affordance.includes('endpoint')) return `drag:${node.type}-endpoint`
320
+ if (affordance.includes('resize')) return 'drag:resize'
321
+ if (affordance.includes('rotate')) return 'drag:rotate'
322
+ if (affordance.includes('move')) return 'drag:move'
323
+ return 'drag:reshape'
324
+ }
325
+
313
326
  /**
314
327
  * Transient live-rotation readout state. Rebuilt each pointer-move while a
315
328
  * rotate-arrow is dragged and cleared on release. World-plan coords.
@@ -328,6 +341,15 @@ type FloorplanEntryDescriptor = {
328
341
  node: AnyNode
329
342
  dependsOnSiblingInputs: boolean
330
343
  ctxOverrides?: FloorplanContextOverrides
344
+ scopeRank?: number
345
+ visibilityRootId?: AnyNodeId
346
+ }
347
+
348
+ /** The focused unit and its members; one stable object per focus change so
349
+ * cached zone geometry invalidates exactly when the focus does. */
350
+ export type FloorplanUnitFocus = {
351
+ focusedUnitId: string
352
+ focusedUnitMemberIds: readonly string[]
331
353
  }
332
354
 
333
355
  type NodeDeps = {
@@ -343,10 +365,12 @@ type NodeDeps = {
343
365
  moving: boolean
344
366
  liveOverride: LiveNodeOverrides | undefined
345
367
  palette: FloorplanPalette | undefined
368
+ unitFocus: FloorplanUnitFocus | undefined
346
369
  siblingEpoch: number
347
370
  committedNodes: Record<string, AnyNode> | null
348
371
  dependencyNodes: AnyNode[]
349
372
  interactiveElevators: unknown
373
+ ctxOverrides: FloorplanContextOverrides | undefined
350
374
  }
351
375
 
352
376
  type CacheEntry = {
@@ -367,6 +391,40 @@ type FloorplanContextOverrides = {
367
391
  children: AnyNode[]
368
392
  siblings: AnyNode[]
369
393
  parent: AnyNode | null
394
+ outputTransform?: { translate?: FloorplanPoint; rotate?: number }
395
+ trackAllNodes?: boolean
396
+ }
397
+
398
+ export function collectDirectFloorplanScopeNodes(
399
+ nodes: Record<string, AnyNode>,
400
+ parent: AnyNode,
401
+ scope: FloorplanScope,
402
+ ): AnyNode[] {
403
+ const scopedKinds = new Set(kindsWithFloorplanScope(scope))
404
+ const declaredChildren = new Set(
405
+ Array.isArray((parent as { children?: AnyNodeId[] }).children)
406
+ ? (parent as { children: AnyNodeId[] }).children
407
+ : [],
408
+ )
409
+ return Object.values(nodes).filter(
410
+ (node) =>
411
+ scopedKinds.has(node.type) && (node.parentId === parent.id || declaredChildren.has(node.id)),
412
+ )
413
+ }
414
+
415
+ export function siteToFloorplanTransform(
416
+ buildingPosition: readonly [number, number, number],
417
+ buildingRotationY: number,
418
+ ): { translate: FloorplanPoint; rotate: number } {
419
+ const cos = Math.cos(buildingRotationY)
420
+ const sin = Math.sin(buildingRotationY)
421
+ return {
422
+ translate: [
423
+ -buildingPosition[0] * cos + buildingPosition[2] * sin,
424
+ -buildingPosition[0] * sin - buildingPosition[2] * cos,
425
+ ],
426
+ rotate: buildingRotationY,
427
+ }
370
428
  }
371
429
 
372
430
  type FloorplanLevelDataHook = (args: {
@@ -428,6 +486,7 @@ function snapshotsToUpdates(snapshots: NodeSnapshot[]) {
428
486
  // Stable empty sentinel used by per-entry builders while the floor plan is
429
487
  // hidden; committed scene edits still flow through `useScene`.
430
488
  const EMPTY_LIVE_OVERRIDES: Map<string, LiveNodeOverrides> = new Map()
489
+ const NO_MEMBER_IDS: readonly string[] = []
431
490
 
432
491
  export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
433
492
  const selectedLevelId = useViewer((s) => s.selection.levelId)
@@ -436,6 +495,17 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
436
495
  const metricNotation = useViewer((s) => s.metricNotation)
437
496
  const setSelection = useViewer((s) => s.setSelection)
438
497
  const nodes = useScene((s) => s.nodes)
498
+ const focusedUnitId = useViewer((s) => s.focusedUnitId)
499
+ const focusedUnitMemberIds = useScene(
500
+ useShallow((s) => {
501
+ const focusedUnit = focusedUnitId ? s.nodes[focusedUnitId] : undefined
502
+ return focusedUnit?.type === 'unit' ? focusedUnit.members : NO_MEMBER_IDS
503
+ }),
504
+ )
505
+ const unitFocus = useMemo<FloorplanUnitFocus | undefined>(
506
+ () => (focusedUnitId ? { focusedUnitId, focusedUnitMemberIds } : undefined),
507
+ [focusedUnitId, focusedUnitMemberIds],
508
+ )
439
509
  const installedPlugins = useScene((s) => s.installedPlugins)
440
510
  const movingNode = useMovingNode()
441
511
  // When a building is being moved, its explicit selection may be
@@ -477,6 +547,13 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
477
547
 
478
548
  const levelId = selectedLevelId ?? ambientLevelId
479
549
  const isAmbient = !selectedLevelId && !!ambientLevelId
550
+ const activeBuildingId = useMemo(
551
+ () => (levelId ? resolveBuildingForLevel(levelId as AnyNodeId, nodes) : null),
552
+ [levelId, nodes],
553
+ )
554
+ const activeBuildingLiveTransform = useLiveTransforms((state) =>
555
+ activeBuildingId ? state.transforms.get(activeBuildingId) : undefined,
556
+ )
480
557
  const renderCtx = useFloorplanStaticRender()
481
558
  const sceneRotationDeg = renderCtx?.getSceneRotationDeg() ?? 0
482
559
  const setMovingNode = useEditor((s) => s.setMovingNode)
@@ -595,6 +672,13 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
595
672
 
596
673
  const applyEntrySelection = useCallback(
597
674
  (id: AnyNodeId, options: { shouldToggle: boolean; isolateMember: boolean }) => {
675
+ const focusedUnitId = useViewer.getState().focusedUnitId
676
+ const clickedNode = useScene.getState().nodes[id]
677
+ if (focusedUnitId && clickedNode?.type === 'zone') {
678
+ paintZoneMembership(focusedUnitId, clickedNode.id)
679
+ swallowNextClick(200)
680
+ return
681
+ }
598
682
  const currentSelectedIds = useViewer.getState().selection.selectedIds
599
683
  let nextSelectedIds: string[]
600
684
  if (options.shouldToggle) {
@@ -644,16 +728,24 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
644
728
 
645
729
  const startDirectMoveDrag = useCallback(
646
730
  (id: AnyNodeId, event: ReactPointerEvent<SVGGElement>): boolean => {
647
- if (event.button !== 0 || !(event.metaKey || event.ctrlKey)) return false
731
+ if (event.button !== 0) return false
648
732
 
649
733
  const node = useScene.getState().nodes[id]
650
734
  if (!node || !isRegistryMovable(node.type)) return false
651
- // Sole selection only: per-node direct manipulation stands down for a
652
- // multi-selection (the group session owns plain drags there, and Cmd is
653
- // the selection-toggle key — a wobbly Cmd+click must not yank one
654
- // member out of the group).
655
735
  const currentSelectedIds = useViewer.getState().selection.selectedIds
656
- if (currentSelectedIds.length !== 1 || currentSelectedIds[0] !== id) return false
736
+ const allowPlainDrag = nodeRegistry.get(node.type)?.capabilities?.movable?.directDrag === true
737
+ const commandModifier = event.metaKey || event.ctrlKey
738
+ if (
739
+ !shouldStartDirectMoveDrag({
740
+ allowPlainDrag,
741
+ commandModifier,
742
+ handleOwnsPointer: false,
743
+ nodeId: id,
744
+ selectedIds: currentSelectedIds,
745
+ })
746
+ ) {
747
+ return false
748
+ }
657
749
 
658
750
  event.preventDefault()
659
751
  event.stopPropagation()
@@ -705,9 +797,8 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
705
797
  if (endEvent.pointerId !== pointerId) return
706
798
  cleanup()
707
799
  if (!engaged) {
708
- // Cmd/Ctrl+click without drag: toggle member (options object, not bare boolean).
709
800
  applyEntrySelection(id, {
710
- shouldToggle: true,
801
+ shouldToggle: commandModifier,
711
802
  isolateMember: false,
712
803
  })
713
804
  }
@@ -929,7 +1020,12 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
929
1020
 
930
1021
  collectLevelDataKind(levelId as AnyNodeId)
931
1022
 
932
- const pushEntry = (id: AnyNodeId, node: AnyNode, ctxOverrides?: FloorplanContextOverrides) => {
1023
+ const pushEntry = (
1024
+ id: AnyNodeId,
1025
+ node: AnyNode,
1026
+ ctxOverrides?: FloorplanContextOverrides,
1027
+ options?: { scopeRank?: number; visibilityRootId?: AnyNodeId },
1028
+ ) => {
933
1029
  if (!isNodeKindEnabled(node.type, installedPlugins)) return
934
1030
  const drawingNode = resolveNodeForDrawingType(node, nodes, drawingType)
935
1031
  if (!drawingNode) return
@@ -942,6 +1038,8 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
942
1038
  )
943
1039
  const descriptor: FloorplanEntryDescriptor = { id, node: drawingNode, dependsOnSiblingInputs }
944
1040
  if (ctxOverrides) descriptor.ctxOverrides = ctxOverrides
1041
+ if (options?.scopeRank !== undefined) descriptor.scopeRank = options.scopeRank
1042
+ if (options?.visibilityRootId) descriptor.visibilityRootId = options.visibilityRootId
945
1043
  out.push(descriptor)
946
1044
  }
947
1045
 
@@ -977,13 +1075,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
977
1075
  // as siblings of the level, not under it — the `visit(levelId)` DFS
978
1076
  // above doesn't reach them. Walk every node of those kinds whose
979
1077
  // parent matches the active level's building, and synthesise a
980
- // `GeometryContext` whose `parent` is the active level (so kind
981
- // builders that gate on the current floor — e.g. elevator service
982
- // range — keep working). Pure registry-driven dispatch: no kind
983
- // name appears in this file.
984
- const activeBuildingId = activeLevelNode
985
- ? resolveBuildingForLevel(levelId as AnyNodeId, nodes)
986
- : null
1078
+ // `GeometryContext` whose `parent` is the active level.
987
1079
  if (activeLevelNode && activeBuildingId) {
988
1080
  const buildingScopedKinds = kindsWithFloorplanScope('building')
989
1081
  const buildingScopedKindSet = new Set(buildingScopedKinds)
@@ -1000,6 +1092,45 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1000
1092
  }
1001
1093
  }
1002
1094
 
1095
+ // Site-scoped kinds are semantic children of the Site and therefore
1096
+ // outside both the active-level DFS and the building sibling scan.
1097
+ // Their builders stay site-local; wrap their output in the inverse active
1098
+ // building transform so it aligns with this building-local plan.
1099
+ const activeBuildingNode = activeBuildingId ? nodes[activeBuildingId] : undefined
1100
+ const activeSiteNode =
1101
+ activeBuildingNode?.type === 'building' && activeBuildingNode.parentId
1102
+ ? nodes[activeBuildingNode.parentId as AnyNodeId]
1103
+ : undefined
1104
+ if (activeSiteNode?.type === 'site' && activeBuildingNode?.type === 'building') {
1105
+ const siteScopedNodes = collectDirectFloorplanScopeNodes(nodes, activeSiteNode, 'site')
1106
+ const siteProjection = siteToFloorplanTransform(
1107
+ activeBuildingLiveTransform?.position ?? activeBuildingNode.position,
1108
+ activeBuildingLiveTransform?.rotation ?? activeBuildingNode.rotation[1],
1109
+ )
1110
+ for (const node of siteScopedNodes) {
1111
+ const children = Array.isArray((node as { children?: AnyNodeId[] }).children)
1112
+ ? (node as { children: AnyNodeId[] }).children
1113
+ .map((id) => nodes[id])
1114
+ .filter((child): child is AnyNode => child !== undefined)
1115
+ : []
1116
+ const siblings = siteScopedNodes.filter(
1117
+ (candidate) => candidate.id !== node.id && candidate.type === node.type,
1118
+ )
1119
+ pushEntry(
1120
+ node.id,
1121
+ node,
1122
+ {
1123
+ children,
1124
+ siblings,
1125
+ parent: activeSiteNode,
1126
+ outputTransform: siteProjection,
1127
+ trackAllNodes: true,
1128
+ },
1129
+ { scopeRank: -1, visibilityRootId: activeSiteNode.id },
1130
+ )
1131
+ }
1132
+ }
1133
+
1003
1134
  // Stable z-order sort. SVG renders in document order — later siblings
1004
1135
  // paint on top of earlier ones — so anything that should sit *under*
1005
1136
  // other floor-plan geometry has to come first in the entries array.
@@ -1007,7 +1138,11 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1007
1138
  // all belong on top of them. Within a layer bucket we preserve the
1008
1139
  // DFS visit order (stable sort) so siblings keep their relative
1009
1140
  // priority.
1010
- out.sort((a, b) => floorplanLayerRank(a.node.type) - floorplanLayerRank(b.node.type))
1141
+ out.sort(
1142
+ (a, b) =>
1143
+ (a.scopeRank ?? 0) - (b.scopeRank ?? 0) ||
1144
+ floorplanLayerRank(a.node.type) - floorplanLayerRank(b.node.type),
1145
+ )
1011
1146
  const entryIds = new Set(out.map((entry) => entry.id))
1012
1147
  for (const id of geometryCacheRef.current.keys()) {
1013
1148
  if (!entryIds.has(id as AnyNodeId)) geometryCacheRef.current.delete(id)
@@ -1016,7 +1151,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1016
1151
  if (!levelNodeIdsByType.has(type)) levelDataCacheRef.current.delete(type)
1017
1152
  }
1018
1153
  return { entries: out, levelNodeIdsByType }
1019
- }, [drawingType, installedPlugins, levelId, nodes])
1154
+ }, [activeBuildingId, activeBuildingLiveTransform, drawingType, installedPlugins, levelId, nodes])
1020
1155
 
1021
1156
  // ── Generic 2D affordance dispatch ─────────────────────────────────
1022
1157
  //
@@ -1048,6 +1183,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1048
1183
  nodes: sceneNodes,
1049
1184
  initialPlanPoint,
1050
1185
  gridSnapStep: useEditor.getState().gridSnapStep,
1186
+ sceneApi: createSceneApi(useScene),
1051
1187
  })
1052
1188
  if (!(session.commit && session.canCommit())) return
1053
1189
  session.commit()
@@ -1085,12 +1221,14 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1085
1221
  event.stopPropagation()
1086
1222
  suppressBoxSelectForPointer(event)
1087
1223
 
1224
+ beginPerfAction(floorplanAffordancePerfAction(node, affordance), `${node.type}:${node.id}`)
1088
1225
  const session = handler.start({
1089
1226
  node,
1090
1227
  payload,
1091
1228
  nodes: sceneNodes,
1092
1229
  initialPlanPoint,
1093
1230
  gridSnapStep: useEditor.getState().gridSnapStep,
1231
+ sceneApi: createSceneApi(useScene),
1094
1232
  })
1095
1233
 
1096
1234
  const snapshots: NodeSnapshot[] = []
@@ -1246,6 +1384,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1246
1384
  drag.historyPaused = false
1247
1385
  }
1248
1386
  drag.session.commit()
1387
+ commitPerfAction()
1249
1388
  sfxEmitter.emit('sfx:structure-build')
1250
1389
  clearSurfacePlanSnapFeedback()
1251
1390
  endReshapeScope(drag)
@@ -1287,6 +1426,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1287
1426
  drag.historyPaused = false
1288
1427
  }
1289
1428
  useScene.getState().updateNodes(finalUpdates)
1429
+ commitPerfAction()
1290
1430
  sfxEmitter.emit('sfx:structure-build')
1291
1431
  } else {
1292
1432
  // Either no net change or canCommit() rejected — revert and
@@ -1300,6 +1440,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1300
1440
  }
1301
1441
  const overrides = useLiveNodeOverrides.getState()
1302
1442
  for (const id of drag.session.affectedIds) overrides.clear(id)
1443
+ cancelPerfAction()
1303
1444
  }
1304
1445
 
1305
1446
  clearSurfacePlanSnapFeedback()
@@ -1417,6 +1558,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1417
1558
  onHandleDoubleClick={commitAffordanceAction}
1418
1559
  onHandlePointerDown={startAffordanceDrag}
1419
1560
  palette={palette}
1561
+ unitFocus={unitFocus}
1420
1562
  pass="base"
1421
1563
  sceneRotationDeg={sceneRotationDeg}
1422
1564
  setMovingNode={setMovingNode}
@@ -1425,7 +1567,9 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1425
1567
  unit={unit}
1426
1568
  metricNotation={metricNotation}
1427
1569
  wallDimensionReference={effectiveWallDimensionReference}
1428
- visibilityRootId={entry.ctxOverrides ? undefined : (levelId as AnyNodeId)}
1570
+ visibilityRootId={
1571
+ entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
1572
+ }
1429
1573
  ctxOverrides={entry.ctxOverrides}
1430
1574
  />
1431
1575
  ))}
@@ -1463,6 +1607,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1463
1607
  onHandleDoubleClick={commitAffordanceAction}
1464
1608
  onHandlePointerDown={startAffordanceDrag}
1465
1609
  palette={palette}
1610
+ unitFocus={unitFocus}
1466
1611
  pass="overlay"
1467
1612
  sceneRotationDeg={sceneRotationDeg}
1468
1613
  setMovingNode={setMovingNode}
@@ -1471,7 +1616,9 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1471
1616
  unit={unit}
1472
1617
  metricNotation={metricNotation}
1473
1618
  wallDimensionReference={effectiveWallDimensionReference}
1474
- visibilityRootId={entry.ctxOverrides ? undefined : (levelId as AnyNodeId)}
1619
+ visibilityRootId={
1620
+ entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
1621
+ }
1475
1622
  ctxOverrides={entry.ctxOverrides}
1476
1623
  />
1477
1624
  ))}
@@ -1792,6 +1939,7 @@ type FloorplanRegistryEntryProps = {
1792
1939
  rotationPivot?: FloorplanPoint,
1793
1940
  ) => void
1794
1941
  palette: FloorplanPalette | undefined
1942
+ unitFocus: FloorplanUnitFocus | undefined
1795
1943
  pass: FloorplanRenderPass
1796
1944
  sceneRotationDeg: number
1797
1945
  setMovingNode: ReturnType<typeof useEditor.getState>['setMovingNode']
@@ -1827,6 +1975,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
1827
1975
  onHandleDoubleClick,
1828
1976
  onHandlePointerDown,
1829
1977
  palette,
1978
+ unitFocus,
1830
1979
  pass,
1831
1980
  sceneRotationDeg,
1832
1981
  setMovingNode,
@@ -1851,7 +2000,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
1851
2000
  const hovered = useViewer((state) => state.hoveredId === selectionProxyId)
1852
2001
  const setHoveredId = useViewer((state) => state.setHoveredId)
1853
2002
  const referencedAnnotationRole = useViewer((state) =>
1854
- floorplanEntryReferencedAnnotationRole(node, new Set(state.selection.selectedIds)),
2003
+ floorplanEntryReferencedAnnotationRole(node, nodes, new Set(state.selection.selectedIds)),
1855
2004
  )
1856
2005
  const activeRotateNodeId = useDirectManipulationFeedback((state) =>
1857
2006
  state.activeRotateNodeId === nodeId ? nodeId : null,
@@ -1943,12 +2092,12 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
1943
2092
  // the body-drag gesture — the whole selection slides, not one member.
1944
2093
  if (onGroupMovePointerDown(nodeId, event)) return
1945
2094
  sfxEmitter.emit('sfx:item-pick')
1946
- setMovingNode(currentNode as never)
2095
+ createEditorApi().engageMove(currentNode)
1947
2096
  // Claim 2D ownership of this move at the source. `setMovingNode`
1948
2097
  // resets the origin to null, so this must follow it.
1949
2098
  setMovingNodeOrigin('2d')
1950
2099
  },
1951
- [nodeId, onGroupMovePointerDown, setMovingNode, setMovingNodeOrigin],
2100
+ [nodeId, onGroupMovePointerDown, setMovingNodeOrigin],
1952
2101
  )
1953
2102
 
1954
2103
  const cacheEntry = buildFloorplanEntryGeometry({
@@ -1968,6 +2117,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
1968
2117
  nodeId,
1969
2118
  nodes,
1970
2119
  palette,
2120
+ unitFocus,
1971
2121
  selected,
1972
2122
  siblingEpoch,
1973
2123
  unit,
@@ -2037,6 +2187,7 @@ type BuildFloorplanEntryGeometryArgs = {
2037
2187
  nodeId: AnyNodeId
2038
2188
  nodes: Record<string, AnyNode>
2039
2189
  palette: FloorplanPalette | undefined
2190
+ unitFocus?: FloorplanUnitFocus
2040
2191
  selected: boolean
2041
2192
  siblingEpoch: number
2042
2193
  unit: 'metric' | 'imperial'
@@ -2051,7 +2202,7 @@ export function collectFloorplanDependencyNodes(
2051
2202
  nodes: Record<string, AnyNode>,
2052
2203
  liveOverrides?: Map<string, LiveNodeOverrides>,
2053
2204
  ): AnyNode[] {
2054
- return (def.floorplanDependencies?.(node) ?? []).flatMap((id) => {
2205
+ return (def.floorplanDependencies?.(node, nodes) ?? []).flatMap((id) => {
2055
2206
  const dependency = nodes[id]
2056
2207
  if (!dependency) return []
2057
2208
  const dependencyOverride = liveOverrides?.get(dependency.id)
@@ -2068,17 +2219,18 @@ export function collectFloorplanDependencyNodes(
2068
2219
 
2069
2220
  function floorplanEntryReferencedAnnotationRole(
2070
2221
  node: AnyNode,
2222
+ nodes: Record<string, AnyNode>,
2071
2223
  selectedIds: ReadonlySet<string>,
2072
2224
  ): FloorplanAnnotationRole | undefined {
2073
2225
  if (selectedIds.size === 0) return undefined
2074
2226
  const definition = nodeRegistry.get(node.type)
2075
2227
  const role = getFloorplanNodeExtension(definition)?.referencedSelectionAnnotationRole
2076
2228
  if (!role) return undefined
2077
- const dependencyIds = definition?.floorplanDependencies?.(node) ?? []
2229
+ const dependencyIds = definition?.floorplanDependencies?.(node, nodes) ?? []
2078
2230
  return dependencyIds.some((id) => selectedIds.has(id)) ? role : undefined
2079
2231
  }
2080
2232
 
2081
- function buildFloorplanEntryGeometry({
2233
+ export function buildFloorplanEntryGeometry({
2082
2234
  automaticDimensions,
2083
2235
  ctxOverrides,
2084
2236
  geometryCache,
@@ -2095,6 +2247,7 @@ function buildFloorplanEntryGeometry({
2095
2247
  nodeId,
2096
2248
  nodes,
2097
2249
  palette,
2250
+ unitFocus,
2098
2251
  selected,
2099
2252
  siblingEpoch,
2100
2253
  unit,
@@ -2123,6 +2276,7 @@ function buildFloorplanEntryGeometry({
2123
2276
  const deps: NodeDeps = {
2124
2277
  automaticDimensions,
2125
2278
  node,
2279
+ ctxOverrides,
2126
2280
  live,
2127
2281
  unit,
2128
2282
  metricNotation,
@@ -2133,10 +2287,11 @@ function buildFloorplanEntryGeometry({
2133
2287
  moving,
2134
2288
  liveOverride,
2135
2289
  palette,
2290
+ unitFocus,
2136
2291
  siblingEpoch: dependsOnSiblingInputs ? siblingEpoch : 0,
2137
2292
  // Sibling-dependent kinds (wall miters, opening cuts) read other nodes'
2138
2293
  // committed state via `ctx`, so committed sibling edits still invalidate.
2139
- committedNodes: dependsOnSiblingInputs ? nodes : null,
2294
+ committedNodes: dependsOnSiblingInputs || ctxOverrides?.trackAllNodes ? nodes : null,
2140
2295
  dependencyNodes,
2141
2296
  interactiveElevators,
2142
2297
  }
@@ -2161,7 +2316,12 @@ function buildFloorplanEntryGeometry({
2161
2316
  : r,
2162
2317
  } as AnyNode
2163
2318
  }
2164
- if ((def.capabilities?.floorPlaced || def.floorplanScope === 'building') && hasPosition) {
2319
+ if (
2320
+ (def.capabilities?.floorPlaced ||
2321
+ def.floorplanScope === 'building' ||
2322
+ def.floorplanScope === 'site') &&
2323
+ hasPosition
2324
+ ) {
2165
2325
  return applyPositionLiveTransform(sourceNode, live)
2166
2326
  }
2167
2327
  if (sourceNode.type === 'slab' || sourceNode.type === 'ceiling' || sourceNode.type === 'zone') {
@@ -2210,6 +2370,8 @@ function buildFloorplanEntryGeometry({
2210
2370
  highlighted,
2211
2371
  hovered,
2212
2372
  moving,
2373
+ focusedUnitId: unitFocus?.focusedUnitId,
2374
+ focusedUnitMemberIds: unitFocus?.focusedUnitMemberIds,
2213
2375
  palette,
2214
2376
  }
2215
2377
  const resolveContextNode = <N = AnyNode>(rid: AnyNodeId): N | undefined => {
@@ -2238,6 +2400,8 @@ function buildFloorplanEntryGeometry({
2238
2400
  highlighted,
2239
2401
  hovered,
2240
2402
  moving,
2403
+ focusedUnitId: unitFocus?.focusedUnitId,
2404
+ focusedUnitMemberIds: unitFocus?.focusedUnitMemberIds,
2241
2405
  palette,
2242
2406
  }
2243
2407
  : undefined,
@@ -2256,10 +2420,18 @@ function buildFloorplanEntryGeometry({
2256
2420
  const taggedContextualGeometry = withFloorplanGeometryMetadata(contextualGeometry, {
2257
2421
  annotationRole: 'contextual-dimension',
2258
2422
  })
2259
- const geometry =
2423
+ const unprojectedGeometry =
2260
2424
  modelGeometry && taggedContextualGeometry
2261
2425
  ? { kind: 'group' as const, children: [modelGeometry, taggedContextualGeometry] }
2262
2426
  : (modelGeometry ?? taggedContextualGeometry)
2427
+ const geometry =
2428
+ unprojectedGeometry && ctxOverrides?.outputTransform
2429
+ ? {
2430
+ kind: 'group' as const,
2431
+ children: [unprojectedGeometry],
2432
+ transform: ctxOverrides.outputTransform,
2433
+ }
2434
+ : unprojectedGeometry
2263
2435
  const { base, overlay } = geometry
2264
2436
  ? splitFloorplanOverlay(geometry)
2265
2437
  : { base: null, overlay: null }
@@ -2822,6 +2994,7 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2822
2994
  case 'midpoint-handle': {
2823
2995
  if (!palette) return <></>
2824
2996
  const handleId = makeHandleId(nodeId, g.payload)
2997
+ const isAction = g.activation === 'action'
2825
2998
  const isHovered = hoveredHandleId === handleId
2826
2999
  const isActive = activeDragId === handleId
2827
3000
  const stroke = palette.endpointHandleStroke
@@ -2834,7 +3007,12 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2834
3007
  return (
2835
3008
  <g
2836
3009
  key={keyHint}
2837
- onClick={(e) => e.stopPropagation()}
3010
+ onClick={(event) => {
3011
+ event.stopPropagation()
3012
+ if (!isAction) return
3013
+ event.preventDefault()
3014
+ onHandleDoubleClick(g.affordance, g.payload, event)
3015
+ }}
2838
3016
  onPointerEnter={() => onHandleHoverChange(handleId)}
2839
3017
  onPointerLeave={() => onHandleHoverChange(null)}
2840
3018
  >
@@ -2891,9 +3069,17 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2891
3069
  cx={g.point[0]}
2892
3070
  cy={g.point[1]}
2893
3071
  fill="transparent"
2894
- onPointerDown={(e) =>
2895
- onHandlePointerDown(g.affordance, g.payload, e as ReactPointerEvent<SVGGElement>)
2896
- }
3072
+ onPointerDown={(event) => {
3073
+ if (isAction) {
3074
+ event.stopPropagation()
3075
+ return
3076
+ }
3077
+ onHandlePointerDown(
3078
+ g.affordance,
3079
+ g.payload,
3080
+ event as ReactPointerEvent<SVGGElement>,
3081
+ )
3082
+ }}
2897
3083
  pointerEvents="all"
2898
3084
  r={radius + unitsPerPixel * 2}
2899
3085
  stroke="transparent"
@@ -3142,12 +3328,15 @@ export function isFloorplanNodeVisible(node: AnyNode, liveOverride?: LiveNodeOve
3142
3328
  return (node as { visible?: boolean }).visible !== false
3143
3329
  }
3144
3330
 
3145
- function isFloorplanHierarchyVisible(
3331
+ export function isFloorplanHierarchyVisible(
3146
3332
  node: AnyNode,
3147
3333
  nodes: Record<string, AnyNode>,
3148
3334
  liveOverrides: Map<string, LiveNodeOverrides>,
3149
3335
  rootId: AnyNodeId,
3150
3336
  ): boolean {
3337
+ const root = nodes[rootId]
3338
+ if (root && !isFloorplanNodeVisible(root, liveOverrides.get(root.id))) return false
3339
+
3151
3340
  let current: AnyNode | undefined = node
3152
3341
  const seen = new Set<AnyNodeId>()
3153
3342
  while (current) {
@@ -3426,7 +3615,9 @@ function nodeDepsEqual(a: NodeDeps, b: NodeDeps): boolean {
3426
3615
  'moving',
3427
3616
  'liveOverride',
3428
3617
  'palette',
3618
+ 'unitFocus',
3429
3619
  'siblingEpoch',
3620
+ 'ctxOverrides',
3430
3621
  'committedNodes',
3431
3622
  'dependencyNodes',
3432
3623
  'interactiveElevators',