@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
@@ -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,
@@ -46,6 +47,7 @@ import {
46
47
  resolveDirectManipulationNode,
47
48
  resolveDirectRotationDragDelta,
48
49
  resolveDirectRotationPatch,
50
+ shouldStartDirectMoveDrag,
49
51
  snapDirectRotationDelta,
50
52
  } from '../../../lib/direct-manipulation'
51
53
  import { createEditorApi } from '../../../lib/editor-api'
@@ -67,6 +69,10 @@ import {
67
69
  resolveFloorplanAnnotationVisibility,
68
70
  resolveFloorplanWallDimensionReference,
69
71
  } from '../../../lib/floorplan/floorplan-mode'
72
+ import {
73
+ buildFloorplanContext,
74
+ floorplanLayerRank,
75
+ } from '../../../lib/floorplan/floorplan-readonly'
70
76
  import { clientToPlan } from '../../../lib/floorplan/plan-coords'
71
77
  import {
72
78
  type ActiveInteractionScope,
@@ -91,6 +97,7 @@ import useInteractionScope, {
91
97
  useEndpointReshape,
92
98
  useMovingNode,
93
99
  } from '../../../store/use-interaction-scope'
100
+ import { expandSessionSelectionForNode } from '../../../store/use-session-groups'
94
101
  import { startGroupPickUp } from '../../editor/group-actions'
95
102
  import { classifyParticipant } from '../../editor/group-transform-shared'
96
103
  import { suppressBoxSelectForPointer } from '../../tools/select/box-select-state'
@@ -241,6 +248,7 @@ export function cancelFloorplanAffordanceDrag(
241
248
  for (const id of drag.session.affectedIds) effects.clearPreview(id)
242
249
  effects.endReshapeScope(drag)
243
250
  effects.clearDragFeedback?.()
251
+ cancelPerfAction()
244
252
  return true
245
253
  }
246
254
 
@@ -305,6 +313,14 @@ export function floorplanAffordanceReshapeScope(
305
313
  return null
306
314
  }
307
315
 
316
+ function floorplanAffordancePerfAction(node: AnyNode, affordance: string): string {
317
+ if (affordance.includes('endpoint')) return `drag:${node.type}-endpoint`
318
+ if (affordance.includes('resize')) return 'drag:resize'
319
+ if (affordance.includes('rotate')) return 'drag:rotate'
320
+ if (affordance.includes('move')) return 'drag:move'
321
+ return 'drag:reshape'
322
+ }
323
+
308
324
  /**
309
325
  * Transient live-rotation readout state. Rebuilt each pointer-move while a
310
326
  * rotate-arrow is dragged and cleared on release. World-plan coords.
@@ -323,6 +339,8 @@ type FloorplanEntryDescriptor = {
323
339
  node: AnyNode
324
340
  dependsOnSiblingInputs: boolean
325
341
  ctxOverrides?: FloorplanContextOverrides
342
+ scopeRank?: number
343
+ visibilityRootId?: AnyNodeId
326
344
  }
327
345
 
328
346
  type NodeDeps = {
@@ -342,6 +360,7 @@ type NodeDeps = {
342
360
  committedNodes: Record<string, AnyNode> | null
343
361
  dependencyNodes: AnyNode[]
344
362
  interactiveElevators: unknown
363
+ ctxOverrides: FloorplanContextOverrides | undefined
345
364
  }
346
365
 
347
366
  type CacheEntry = {
@@ -362,6 +381,40 @@ type FloorplanContextOverrides = {
362
381
  children: AnyNode[]
363
382
  siblings: AnyNode[]
364
383
  parent: AnyNode | null
384
+ outputTransform?: { translate?: FloorplanPoint; rotate?: number }
385
+ trackAllNodes?: boolean
386
+ }
387
+
388
+ export function collectDirectFloorplanScopeNodes(
389
+ nodes: Record<string, AnyNode>,
390
+ parent: AnyNode,
391
+ scope: FloorplanScope,
392
+ ): AnyNode[] {
393
+ const scopedKinds = new Set(kindsWithFloorplanScope(scope))
394
+ const declaredChildren = new Set(
395
+ Array.isArray((parent as { children?: AnyNodeId[] }).children)
396
+ ? (parent as { children: AnyNodeId[] }).children
397
+ : [],
398
+ )
399
+ return Object.values(nodes).filter(
400
+ (node) =>
401
+ scopedKinds.has(node.type) && (node.parentId === parent.id || declaredChildren.has(node.id)),
402
+ )
403
+ }
404
+
405
+ export function siteToFloorplanTransform(
406
+ buildingPosition: readonly [number, number, number],
407
+ buildingRotationY: number,
408
+ ): { translate: FloorplanPoint; rotate: number } {
409
+ const cos = Math.cos(buildingRotationY)
410
+ const sin = Math.sin(buildingRotationY)
411
+ return {
412
+ translate: [
413
+ -buildingPosition[0] * cos + buildingPosition[2] * sin,
414
+ -buildingPosition[0] * sin - buildingPosition[2] * cos,
415
+ ],
416
+ rotate: buildingRotationY,
417
+ }
365
418
  }
366
419
 
367
420
  type FloorplanLevelDataHook = (args: {
@@ -472,6 +525,13 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
472
525
 
473
526
  const levelId = selectedLevelId ?? ambientLevelId
474
527
  const isAmbient = !selectedLevelId && !!ambientLevelId
528
+ const activeBuildingId = useMemo(
529
+ () => (levelId ? resolveBuildingForLevel(levelId as AnyNodeId, nodes) : null),
530
+ [levelId, nodes],
531
+ )
532
+ const activeBuildingLiveTransform = useLiveTransforms((state) =>
533
+ activeBuildingId ? state.transforms.get(activeBuildingId) : undefined,
534
+ )
475
535
  const renderCtx = useFloorplanStaticRender()
476
536
  const sceneRotationDeg = renderCtx?.getSceneRotationDeg() ?? 0
477
537
  const setMovingNode = useEditor((s) => s.setMovingNode)
@@ -589,13 +649,19 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
589
649
  }, [bumpAffectedSiblingEpochs])
590
650
 
591
651
  const applyEntrySelection = useCallback(
592
- (id: AnyNodeId, shouldToggle: boolean) => {
652
+ (id: AnyNodeId, options: { shouldToggle: boolean; isolateMember: boolean }) => {
593
653
  const currentSelectedIds = useViewer.getState().selection.selectedIds
594
- const nextSelectedIds = shouldToggle
595
- ? currentSelectedIds.includes(id)
654
+ let nextSelectedIds: string[]
655
+ if (options.shouldToggle) {
656
+ nextSelectedIds = currentSelectedIds.includes(id)
596
657
  ? currentSelectedIds.filter((selectedId) => selectedId !== id)
597
658
  : [...currentSelectedIds, id]
598
- : [id]
659
+ } else if (options.isolateMember) {
660
+ nextSelectedIds = [id]
661
+ } else {
662
+ const expanded = expandSessionSelectionForNode(id)
663
+ nextSelectedIds = expanded && expanded.length > 1 ? expanded : [id]
664
+ }
599
665
  setSelection({ selectedIds: nextSelectedIds })
600
666
  if (nextSelectedIds.length === 1 && nextSelectedIds[0] === id) {
601
667
  const node = useScene.getState().nodes[id]
@@ -618,7 +684,10 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
618
684
  (id: AnyNodeId, event: React.PointerEvent<SVGGElement>) => {
619
685
  if (event.button !== 0) return
620
686
  event.stopPropagation()
621
- applyEntrySelection(id, event.metaKey || event.ctrlKey || event.shiftKey)
687
+ applyEntrySelection(id, {
688
+ shouldToggle: event.metaKey || event.ctrlKey || event.shiftKey,
689
+ isolateMember: event.altKey && !(event.metaKey || event.ctrlKey || event.shiftKey),
690
+ })
622
691
  },
623
692
  [applyEntrySelection],
624
693
  )
@@ -630,16 +699,24 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
630
699
 
631
700
  const startDirectMoveDrag = useCallback(
632
701
  (id: AnyNodeId, event: ReactPointerEvent<SVGGElement>): boolean => {
633
- if (event.button !== 0 || !(event.metaKey || event.ctrlKey)) return false
702
+ if (event.button !== 0) return false
634
703
 
635
704
  const node = useScene.getState().nodes[id]
636
705
  if (!node || !isRegistryMovable(node.type)) return false
637
- // Sole selection only: per-node direct manipulation stands down for a
638
- // multi-selection (the group session owns plain drags there, and Cmd is
639
- // the selection-toggle key — a wobbly Cmd+click must not yank one
640
- // member out of the group).
641
706
  const currentSelectedIds = useViewer.getState().selection.selectedIds
642
- if (currentSelectedIds.length !== 1 || currentSelectedIds[0] !== id) return false
707
+ const allowPlainDrag = nodeRegistry.get(node.type)?.capabilities?.movable?.directDrag === true
708
+ const commandModifier = event.metaKey || event.ctrlKey
709
+ if (
710
+ !shouldStartDirectMoveDrag({
711
+ allowPlainDrag,
712
+ commandModifier,
713
+ handleOwnsPointer: false,
714
+ nodeId: id,
715
+ selectedIds: currentSelectedIds,
716
+ })
717
+ ) {
718
+ return false
719
+ }
643
720
 
644
721
  event.preventDefault()
645
722
  event.stopPropagation()
@@ -691,7 +768,10 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
691
768
  if (endEvent.pointerId !== pointerId) return
692
769
  cleanup()
693
770
  if (!engaged) {
694
- applyEntrySelection(id, true)
771
+ applyEntrySelection(id, {
772
+ shouldToggle: commandModifier,
773
+ isolateMember: false,
774
+ })
695
775
  }
696
776
  }
697
777
 
@@ -911,7 +991,12 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
911
991
 
912
992
  collectLevelDataKind(levelId as AnyNodeId)
913
993
 
914
- const pushEntry = (id: AnyNodeId, node: AnyNode, ctxOverrides?: FloorplanContextOverrides) => {
994
+ const pushEntry = (
995
+ id: AnyNodeId,
996
+ node: AnyNode,
997
+ ctxOverrides?: FloorplanContextOverrides,
998
+ options?: { scopeRank?: number; visibilityRootId?: AnyNodeId },
999
+ ) => {
915
1000
  if (!isNodeKindEnabled(node.type, installedPlugins)) return
916
1001
  const drawingNode = resolveNodeForDrawingType(node, nodes, drawingType)
917
1002
  if (!drawingNode) return
@@ -924,6 +1009,8 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
924
1009
  )
925
1010
  const descriptor: FloorplanEntryDescriptor = { id, node: drawingNode, dependsOnSiblingInputs }
926
1011
  if (ctxOverrides) descriptor.ctxOverrides = ctxOverrides
1012
+ if (options?.scopeRank !== undefined) descriptor.scopeRank = options.scopeRank
1013
+ if (options?.visibilityRootId) descriptor.visibilityRootId = options.visibilityRootId
927
1014
  out.push(descriptor)
928
1015
  }
929
1016
 
@@ -959,13 +1046,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
959
1046
  // as siblings of the level, not under it — the `visit(levelId)` DFS
960
1047
  // above doesn't reach them. Walk every node of those kinds whose
961
1048
  // parent matches the active level's building, and synthesise a
962
- // `GeometryContext` whose `parent` is the active level (so kind
963
- // builders that gate on the current floor — e.g. elevator service
964
- // range — keep working). Pure registry-driven dispatch: no kind
965
- // name appears in this file.
966
- const activeBuildingId = activeLevelNode
967
- ? resolveBuildingForLevel(levelId as AnyNodeId, nodes)
968
- : null
1049
+ // `GeometryContext` whose `parent` is the active level.
969
1050
  if (activeLevelNode && activeBuildingId) {
970
1051
  const buildingScopedKinds = kindsWithFloorplanScope('building')
971
1052
  const buildingScopedKindSet = new Set(buildingScopedKinds)
@@ -982,6 +1063,45 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
982
1063
  }
983
1064
  }
984
1065
 
1066
+ // Site-scoped kinds are semantic children of the Site and therefore
1067
+ // outside both the active-level DFS and the building sibling scan.
1068
+ // Their builders stay site-local; wrap their output in the inverse active
1069
+ // building transform so it aligns with this building-local plan.
1070
+ const activeBuildingNode = activeBuildingId ? nodes[activeBuildingId] : undefined
1071
+ const activeSiteNode =
1072
+ activeBuildingNode?.type === 'building' && activeBuildingNode.parentId
1073
+ ? nodes[activeBuildingNode.parentId as AnyNodeId]
1074
+ : undefined
1075
+ if (activeSiteNode?.type === 'site' && activeBuildingNode?.type === 'building') {
1076
+ const siteScopedNodes = collectDirectFloorplanScopeNodes(nodes, activeSiteNode, 'site')
1077
+ const siteProjection = siteToFloorplanTransform(
1078
+ activeBuildingLiveTransform?.position ?? activeBuildingNode.position,
1079
+ activeBuildingLiveTransform?.rotation ?? activeBuildingNode.rotation[1],
1080
+ )
1081
+ for (const node of siteScopedNodes) {
1082
+ const children = Array.isArray((node as { children?: AnyNodeId[] }).children)
1083
+ ? (node as { children: AnyNodeId[] }).children
1084
+ .map((id) => nodes[id])
1085
+ .filter((child): child is AnyNode => child !== undefined)
1086
+ : []
1087
+ const siblings = siteScopedNodes.filter(
1088
+ (candidate) => candidate.id !== node.id && candidate.type === node.type,
1089
+ )
1090
+ pushEntry(
1091
+ node.id,
1092
+ node,
1093
+ {
1094
+ children,
1095
+ siblings,
1096
+ parent: activeSiteNode,
1097
+ outputTransform: siteProjection,
1098
+ trackAllNodes: true,
1099
+ },
1100
+ { scopeRank: -1, visibilityRootId: activeSiteNode.id },
1101
+ )
1102
+ }
1103
+ }
1104
+
985
1105
  // Stable z-order sort. SVG renders in document order — later siblings
986
1106
  // paint on top of earlier ones — so anything that should sit *under*
987
1107
  // other floor-plan geometry has to come first in the entries array.
@@ -989,7 +1109,11 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
989
1109
  // all belong on top of them. Within a layer bucket we preserve the
990
1110
  // DFS visit order (stable sort) so siblings keep their relative
991
1111
  // priority.
992
- out.sort((a, b) => floorplanLayerRank(a.node.type) - floorplanLayerRank(b.node.type))
1112
+ out.sort(
1113
+ (a, b) =>
1114
+ (a.scopeRank ?? 0) - (b.scopeRank ?? 0) ||
1115
+ floorplanLayerRank(a.node.type) - floorplanLayerRank(b.node.type),
1116
+ )
993
1117
  const entryIds = new Set(out.map((entry) => entry.id))
994
1118
  for (const id of geometryCacheRef.current.keys()) {
995
1119
  if (!entryIds.has(id as AnyNodeId)) geometryCacheRef.current.delete(id)
@@ -998,7 +1122,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
998
1122
  if (!levelNodeIdsByType.has(type)) levelDataCacheRef.current.delete(type)
999
1123
  }
1000
1124
  return { entries: out, levelNodeIdsByType }
1001
- }, [drawingType, installedPlugins, levelId, nodes])
1125
+ }, [activeBuildingId, activeBuildingLiveTransform, drawingType, installedPlugins, levelId, nodes])
1002
1126
 
1003
1127
  // ── Generic 2D affordance dispatch ─────────────────────────────────
1004
1128
  //
@@ -1030,6 +1154,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1030
1154
  nodes: sceneNodes,
1031
1155
  initialPlanPoint,
1032
1156
  gridSnapStep: useEditor.getState().gridSnapStep,
1157
+ sceneApi: createSceneApi(useScene),
1033
1158
  })
1034
1159
  if (!(session.commit && session.canCommit())) return
1035
1160
  session.commit()
@@ -1067,12 +1192,14 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1067
1192
  event.stopPropagation()
1068
1193
  suppressBoxSelectForPointer(event)
1069
1194
 
1195
+ beginPerfAction(floorplanAffordancePerfAction(node, affordance), `${node.type}:${node.id}`)
1070
1196
  const session = handler.start({
1071
1197
  node,
1072
1198
  payload,
1073
1199
  nodes: sceneNodes,
1074
1200
  initialPlanPoint,
1075
1201
  gridSnapStep: useEditor.getState().gridSnapStep,
1202
+ sceneApi: createSceneApi(useScene),
1076
1203
  })
1077
1204
 
1078
1205
  const snapshots: NodeSnapshot[] = []
@@ -1228,6 +1355,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1228
1355
  drag.historyPaused = false
1229
1356
  }
1230
1357
  drag.session.commit()
1358
+ commitPerfAction()
1231
1359
  sfxEmitter.emit('sfx:structure-build')
1232
1360
  clearSurfacePlanSnapFeedback()
1233
1361
  endReshapeScope(drag)
@@ -1269,6 +1397,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1269
1397
  drag.historyPaused = false
1270
1398
  }
1271
1399
  useScene.getState().updateNodes(finalUpdates)
1400
+ commitPerfAction()
1272
1401
  sfxEmitter.emit('sfx:structure-build')
1273
1402
  } else {
1274
1403
  // Either no net change or canCommit() rejected — revert and
@@ -1282,6 +1411,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1282
1411
  }
1283
1412
  const overrides = useLiveNodeOverrides.getState()
1284
1413
  for (const id of drag.session.affectedIds) overrides.clear(id)
1414
+ cancelPerfAction()
1285
1415
  }
1286
1416
 
1287
1417
  clearSurfacePlanSnapFeedback()
@@ -1407,7 +1537,9 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1407
1537
  unit={unit}
1408
1538
  metricNotation={metricNotation}
1409
1539
  wallDimensionReference={effectiveWallDimensionReference}
1410
- visibilityRootId={entry.ctxOverrides ? undefined : (levelId as AnyNodeId)}
1540
+ visibilityRootId={
1541
+ entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
1542
+ }
1411
1543
  ctxOverrides={entry.ctxOverrides}
1412
1544
  />
1413
1545
  ))}
@@ -1453,7 +1585,9 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1453
1585
  unit={unit}
1454
1586
  metricNotation={metricNotation}
1455
1587
  wallDimensionReference={effectiveWallDimensionReference}
1456
- visibilityRootId={entry.ctxOverrides ? undefined : (levelId as AnyNodeId)}
1588
+ visibilityRootId={
1589
+ entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
1590
+ }
1457
1591
  ctxOverrides={entry.ctxOverrides}
1458
1592
  />
1459
1593
  ))}
@@ -1925,12 +2059,12 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
1925
2059
  // the body-drag gesture — the whole selection slides, not one member.
1926
2060
  if (onGroupMovePointerDown(nodeId, event)) return
1927
2061
  sfxEmitter.emit('sfx:item-pick')
1928
- setMovingNode(currentNode as never)
2062
+ createEditorApi().engageMove(currentNode)
1929
2063
  // Claim 2D ownership of this move at the source. `setMovingNode`
1930
2064
  // resets the origin to null, so this must follow it.
1931
2065
  setMovingNodeOrigin('2d')
1932
2066
  },
1933
- [nodeId, onGroupMovePointerDown, setMovingNode, setMovingNodeOrigin],
2067
+ [nodeId, onGroupMovePointerDown, setMovingNodeOrigin],
1934
2068
  )
1935
2069
 
1936
2070
  const cacheEntry = buildFloorplanEntryGeometry({
@@ -2060,7 +2194,7 @@ function floorplanEntryReferencedAnnotationRole(
2060
2194
  return dependencyIds.some((id) => selectedIds.has(id)) ? role : undefined
2061
2195
  }
2062
2196
 
2063
- function buildFloorplanEntryGeometry({
2197
+ export function buildFloorplanEntryGeometry({
2064
2198
  automaticDimensions,
2065
2199
  ctxOverrides,
2066
2200
  geometryCache,
@@ -2105,6 +2239,7 @@ function buildFloorplanEntryGeometry({
2105
2239
  const deps: NodeDeps = {
2106
2240
  automaticDimensions,
2107
2241
  node,
2242
+ ctxOverrides,
2108
2243
  live,
2109
2244
  unit,
2110
2245
  metricNotation,
@@ -2118,7 +2253,7 @@ function buildFloorplanEntryGeometry({
2118
2253
  siblingEpoch: dependsOnSiblingInputs ? siblingEpoch : 0,
2119
2254
  // Sibling-dependent kinds (wall miters, opening cuts) read other nodes'
2120
2255
  // committed state via `ctx`, so committed sibling edits still invalidate.
2121
- committedNodes: dependsOnSiblingInputs ? nodes : null,
2256
+ committedNodes: dependsOnSiblingInputs || ctxOverrides?.trackAllNodes ? nodes : null,
2122
2257
  dependencyNodes,
2123
2258
  interactiveElevators,
2124
2259
  }
@@ -2143,7 +2278,12 @@ function buildFloorplanEntryGeometry({
2143
2278
  : r,
2144
2279
  } as AnyNode
2145
2280
  }
2146
- if ((def.capabilities?.floorPlaced || def.floorplanScope === 'building') && hasPosition) {
2281
+ if (
2282
+ (def.capabilities?.floorPlaced ||
2283
+ def.floorplanScope === 'building' ||
2284
+ def.floorplanScope === 'site') &&
2285
+ hasPosition
2286
+ ) {
2147
2287
  return applyPositionLiveTransform(sourceNode, live)
2148
2288
  }
2149
2289
  if (sourceNode.type === 'slab' || sourceNode.type === 'ceiling' || sourceNode.type === 'zone') {
@@ -2238,10 +2378,18 @@ function buildFloorplanEntryGeometry({
2238
2378
  const taggedContextualGeometry = withFloorplanGeometryMetadata(contextualGeometry, {
2239
2379
  annotationRole: 'contextual-dimension',
2240
2380
  })
2241
- const geometry =
2381
+ const unprojectedGeometry =
2242
2382
  modelGeometry && taggedContextualGeometry
2243
2383
  ? { kind: 'group' as const, children: [modelGeometry, taggedContextualGeometry] }
2244
2384
  : (modelGeometry ?? taggedContextualGeometry)
2385
+ const geometry =
2386
+ unprojectedGeometry && ctxOverrides?.outputTransform
2387
+ ? {
2388
+ kind: 'group' as const,
2389
+ children: [unprojectedGeometry],
2390
+ transform: ctxOverrides.outputTransform,
2391
+ }
2392
+ : unprojectedGeometry
2245
2393
  const { base, overlay } = geometry
2246
2394
  ? splitFloorplanOverlay(geometry)
2247
2395
  : { base: null, overlay: null }
@@ -2804,6 +2952,7 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2804
2952
  case 'midpoint-handle': {
2805
2953
  if (!palette) return <></>
2806
2954
  const handleId = makeHandleId(nodeId, g.payload)
2955
+ const isAction = g.activation === 'action'
2807
2956
  const isHovered = hoveredHandleId === handleId
2808
2957
  const isActive = activeDragId === handleId
2809
2958
  const stroke = palette.endpointHandleStroke
@@ -2816,7 +2965,12 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2816
2965
  return (
2817
2966
  <g
2818
2967
  key={keyHint}
2819
- onClick={(e) => e.stopPropagation()}
2968
+ onClick={(event) => {
2969
+ event.stopPropagation()
2970
+ if (!isAction) return
2971
+ event.preventDefault()
2972
+ onHandleDoubleClick(g.affordance, g.payload, event)
2973
+ }}
2820
2974
  onPointerEnter={() => onHandleHoverChange(handleId)}
2821
2975
  onPointerLeave={() => onHandleHoverChange(null)}
2822
2976
  >
@@ -2873,9 +3027,17 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2873
3027
  cx={g.point[0]}
2874
3028
  cy={g.point[1]}
2875
3029
  fill="transparent"
2876
- onPointerDown={(e) =>
2877
- onHandlePointerDown(g.affordance, g.payload, e as ReactPointerEvent<SVGGElement>)
2878
- }
3030
+ onPointerDown={(event) => {
3031
+ if (isAction) {
3032
+ event.stopPropagation()
3033
+ return
3034
+ }
3035
+ onHandlePointerDown(
3036
+ g.affordance,
3037
+ g.payload,
3038
+ event as ReactPointerEvent<SVGGElement>,
3039
+ )
3040
+ }}
2879
3041
  pointerEvents="all"
2880
3042
  r={radius + unitsPerPixel * 2}
2881
3043
  stroke="transparent"
@@ -3124,12 +3286,15 @@ export function isFloorplanNodeVisible(node: AnyNode, liveOverride?: LiveNodeOve
3124
3286
  return (node as { visible?: boolean }).visible !== false
3125
3287
  }
3126
3288
 
3127
- function isFloorplanHierarchyVisible(
3289
+ export function isFloorplanHierarchyVisible(
3128
3290
  node: AnyNode,
3129
3291
  nodes: Record<string, AnyNode>,
3130
3292
  liveOverrides: Map<string, LiveNodeOverrides>,
3131
3293
  rootId: AnyNodeId,
3132
3294
  ): boolean {
3295
+ const root = nodes[rootId]
3296
+ if (root && !isFloorplanNodeVisible(root, liveOverrides.get(root.id))) return false
3297
+
3133
3298
  let current: AnyNode | undefined = node
3134
3299
  const seen = new Set<AnyNodeId>()
3135
3300
  while (current) {
@@ -3144,73 +3309,7 @@ function isFloorplanHierarchyVisible(
3144
3309
  return true
3145
3310
  }
3146
3311
 
3147
- export function buildContext(
3148
- node: AnyNode,
3149
- nodes: Record<string, AnyNode>,
3150
- viewState: {
3151
- automaticDimensions?: boolean
3152
- selected: boolean
3153
- unit: 'metric' | 'imperial'
3154
- metricNotation?: 'meters' | 'millimeters'
3155
- purpose?: 'edit' | 'document'
3156
- wallDimensionReference?: FloorplanWallDimensionReference
3157
- highlighted: boolean
3158
- hovered: boolean
3159
- moving: boolean
3160
- palette: FloorplanPalette | undefined
3161
- },
3162
- levelData?: unknown,
3163
- ): GeometryContext {
3164
- const resolve = <N = AnyNode>(id: AnyNodeId): N | undefined => nodes[id] as N | undefined
3165
-
3166
- const childIds = (node as unknown as { children?: AnyNodeId[] }).children
3167
- const children: AnyNode[] = Array.isArray(childIds)
3168
- ? childIds.map((cid) => nodes[cid]).filter((n): n is AnyNode => n !== undefined)
3169
- : []
3170
-
3171
- const parentId = node.parentId as AnyNodeId | null
3172
- const parent: AnyNode | null = parentId ? (nodes[parentId] ?? null) : null
3173
-
3174
- let siblings: AnyNode[] = []
3175
- if (parent) {
3176
- const parentChildIds = (parent as unknown as { children?: AnyNodeId[] }).children
3177
- if (Array.isArray(parentChildIds)) {
3178
- for (const sid of parentChildIds) {
3179
- if (sid === node.id) continue
3180
- const s = nodes[sid]
3181
- if (s && s.type === node.type) siblings.push(s)
3182
- }
3183
- } else {
3184
- siblings = Object.values(nodes).filter(
3185
- (n) => n !== node && n.type === node.type && n.parentId === parentId,
3186
- )
3187
- }
3188
- }
3189
-
3190
- return {
3191
- resolve,
3192
- children,
3193
- siblings,
3194
- parent,
3195
- levelData,
3196
- extensions: createFloorplanContextExtensions({
3197
- automaticDimensions: viewState.automaticDimensions,
3198
- metricNotation: viewState.metricNotation ?? 'meters',
3199
- purpose: viewState.purpose ?? 'edit',
3200
- wallDimensionReference: viewState.wallDimensionReference,
3201
- }),
3202
- viewState: viewState.palette
3203
- ? {
3204
- selected: viewState.selected,
3205
- unit: viewState.unit,
3206
- highlighted: viewState.highlighted,
3207
- hovered: viewState.hovered,
3208
- moving: viewState.moving,
3209
- palette: viewState.palette,
3210
- }
3211
- : undefined,
3212
- }
3213
- }
3312
+ export const buildContext = buildFloorplanContext
3214
3313
 
3215
3314
  export function collectFloorplanLinkedLevelNodes(
3216
3315
  nodes: Record<string, AnyNode>,
@@ -3475,6 +3574,7 @@ function nodeDepsEqual(a: NodeDeps, b: NodeDeps): boolean {
3475
3574
  'liveOverride',
3476
3575
  'palette',
3477
3576
  'siblingEpoch',
3577
+ 'ctxOverrides',
3478
3578
  'committedNodes',
3479
3579
  'dependencyNodes',
3480
3580
  'interactiveElevators',
@@ -3512,17 +3612,7 @@ function depsValueEqual(a: unknown, b: unknown): boolean {
3512
3612
  * Sort is stable in modern JS engines, so siblings within the same
3513
3613
  * bucket keep their DFS order (= scene tree order).
3514
3614
  */
3515
- export function floorplanLayerRank(type: string): number {
3516
- switch (type) {
3517
- case 'zone':
3518
- return 0
3519
- case 'slab':
3520
- case 'ceiling':
3521
- return 1
3522
- default:
3523
- return 2
3524
- }
3525
- }
3615
+ export { floorplanLayerRank }
3526
3616
 
3527
3617
  function deepEqual(a: unknown, b: unknown): boolean {
3528
3618
  if (a === b) return true