@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
@@ -39,10 +39,13 @@ import {
39
39
  collectParticipants,
40
40
  computeGroupBox,
41
41
  expandToComponent,
42
+ type GroupPlanBounds,
43
+ groupPlanBounds,
42
44
  levelFrame,
43
- participantExtents,
45
+ planBoundsCenter,
44
46
  rotateGroupPatches,
45
47
  rotateGroupSnapshots,
48
+ rotatePlanBounds,
46
49
  translateGroupPatches,
47
50
  type Vec2,
48
51
  } from '../editor/group-transform-shared'
@@ -112,6 +115,9 @@ export function startFloorplanGroupMove(
112
115
  affectedIds: AnyNodeId[]
113
116
  candidates: ReturnType<typeof collectAlignmentAnchors>
114
117
  restAnchors: ReturnType<typeof bboxCornerAnchors>
118
+ startBounds: GroupPlanBounds
119
+ restBounds: GroupPlanBounds
120
+ rotation: number
115
121
  restCenter: Vec2
116
122
  lastDelta: Vec2 | null
117
123
  }
@@ -140,20 +146,16 @@ export function startFloorplanGroupMove(
140
146
  // The group aligns as one rigid footprint: its bbox corners + center are
141
147
  // the moving anchors. `computeGroupBox` is world-space (the 3D scene stays
142
148
  // mounted under every view mode); plan coords are level-frame, so convert.
143
- const restBox = computeGroupBox(fullIds)
144
149
  const { inverse: frameInv } = levelFrame(levelId)
145
- const boxMin = restBox ? restBox.min.clone().applyMatrix4(frameInv) : null
146
- const boxMax = restBox ? restBox.max.clone().applyMatrix4(frameInv) : null
147
- const restAnchors =
148
- boxMin && boxMax
149
- ? bboxCornerAnchors(
150
- 'group-move',
151
- Math.min(boxMin.x, boxMax.x),
152
- Math.min(boxMin.z, boxMax.z),
153
- Math.max(boxMin.x, boxMax.x),
154
- Math.max(boxMin.z, boxMax.z),
155
- )
156
- : []
150
+ const restBounds = groupPlanBounds(computeGroupBox(fullIds), starts, frameInv)
151
+ if (!restBounds) return null
152
+ const restAnchors = bboxCornerAnchors(
153
+ 'group-move',
154
+ restBounds.minX,
155
+ restBounds.minZ,
156
+ restBounds.maxX,
157
+ restBounds.maxZ,
158
+ )
157
159
 
158
160
  for (const id of affectedIds) {
159
161
  useLiveTransforms.getState().clear(id)
@@ -169,12 +171,23 @@ export function startFloorplanGroupMove(
169
171
  nodeId,
170
172
  handle: GROUP_MOVE_DRAG_LABEL,
171
173
  })
172
- // Rotation pivot for mid-drag R/T — the participant DATA extents' center
173
- // (stable across the drag; rotations re-seed around the same point).
174
- const ext = participantExtents(starts)
175
- const restCenter: Vec2 = ext ? [(ext.minX + ext.maxX) / 2, (ext.minZ + ext.maxZ) / 2] : [0, 0]
174
+ // Rotation pivot for mid-drag R/T — the START footprint's center, the same
175
+ // point the 3D body drag, the idle keyboard rotate and the rotate gizmos
176
+ // orbit. Stable across the drag; rotations re-seed around the same point.
177
+ const restCenter = planBoundsCenter(restBounds)
176
178
 
177
- return { starts, links, affectedIds, candidates, restAnchors, restCenter, lastDelta: null }
179
+ return {
180
+ starts,
181
+ links,
182
+ affectedIds,
183
+ candidates,
184
+ restAnchors,
185
+ startBounds: restBounds,
186
+ restBounds,
187
+ rotation: 0,
188
+ restCenter,
189
+ lastDelta: null,
190
+ }
178
191
  }
179
192
 
180
193
  const applyMove = (e: PointerEvent, s: Session) => {
@@ -241,18 +254,20 @@ export function startFloorplanGroupMove(
241
254
  // current delta — the carried group turns exactly like the idle keyboard
242
255
  // rotate, and the commit stays a single updateNodes.
243
256
  const rotateSession = (s: Session, direction: 1 | -1) => {
244
- const rotated = rotateGroupSnapshots(
245
- s.starts,
246
- s.links,
247
- { x: s.restCenter[0], z: s.restCenter[1] },
248
- -direction * (Math.PI / 4),
249
- )
257
+ const pivot = { x: s.restCenter[0], z: s.restCenter[1] }
258
+ const delta = -direction * (Math.PI / 4)
259
+ const rotated = rotateGroupSnapshots(s.starts, s.links, pivot, delta)
250
260
  s.starts = rotated.starts
251
261
  s.links = rotated.links
252
- const ext = participantExtents(rotated.starts)
253
- if (ext) {
254
- s.restAnchors = bboxCornerAnchors('group-move', ext.minX, ext.minZ, ext.maxX, ext.maxZ)
255
- }
262
+ s.rotation += delta
263
+ s.restBounds = rotatePlanBounds(s.startBounds, pivot, s.rotation)
264
+ s.restAnchors = bboxCornerAnchors(
265
+ 'group-move',
266
+ s.restBounds.minX,
267
+ s.restBounds.minZ,
268
+ s.restBounds.maxX,
269
+ s.restBounds.maxZ,
270
+ )
256
271
  sfxEmitter.emit('sfx:item-rotate')
257
272
  applyDelta(s, s.lastDelta?.[0] ?? 0, s.lastDelta?.[1] ?? 0)
258
273
  }
@@ -358,7 +373,22 @@ export function startFloorplanGroupMove(
358
373
  const onKeyDown = (e: KeyboardEvent) => {
359
374
  const key = e.key.toLowerCase()
360
375
  if ((key === 'r' || key === 't') && !e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey) {
361
- if (!session) return
376
+ // Armed but still under the drag threshold: engage first (exactly what
377
+ // the next pointer-move would do) so the rotation lands inside this
378
+ // session. Falling through to the global idle arm instead would write
379
+ // the scene behind snapshots already captured here, and the first
380
+ // `applyDelta` would republish them — undoing the rotation.
381
+ if (!session) {
382
+ session = engage()
383
+ if (!session) {
384
+ // No plane hit yet: swallow the chord and keep the gesture armed so
385
+ // the next pointer-move can still engage; the idle arm must not run
386
+ // behind the snapshots captured here.
387
+ e.preventDefault()
388
+ e.stopPropagation()
389
+ return
390
+ }
391
+ }
362
392
  e.preventDefault()
363
393
  e.stopPropagation()
364
394
  rotateSession(session, key === 'r' ? 1 : -1)
@@ -415,9 +445,13 @@ export function startFloorplanGroupRotate(event: {
415
445
  const { starts, links } = collectParticipants(fullIds, nodes, levelId)
416
446
  if (starts.length === 0) return false
417
447
  const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
418
- const ext = participantExtents(starts)
419
- if (!ext) return false
420
- const pivot = { x: (ext.minX + ext.maxX) / 2, z: (ext.minZ + ext.maxZ) / 2 }
448
+ const { inverse: frameInv } = levelFrame(levelId)
449
+ const bounds = groupPlanBounds(computeGroupBox(fullIds), starts, frameInv)
450
+ if (!bounds) return false
451
+ // Same pivot as the dashed box the handles hang off (and as the 3D rotate
452
+ // gizmo): its centre, not the anchor points' centre.
453
+ const [pivotX, pivotZ] = planBoundsCenter(bounds)
454
+ const pivot = { x: pivotX, z: pivotZ }
421
455
  const startPlan = clientToPlan(event.clientX, event.clientY)
422
456
  if (!startPlan) return false
423
457
  // Bearing around the pivot in the plan frame — the same atan2 x→z sense
@@ -3,6 +3,7 @@
3
3
  import { createSceneApi, nodeRegistry, useScene } from '@pascal-app/core'
4
4
  import { useViewer } from '@pascal-app/viewer'
5
5
  import { type ComponentType, lazy, Suspense, useCallback, useMemo } from 'react'
6
+ import { useRegisteredToolEnabled } from '../../hooks/use-registered-tool-enabled'
6
7
  import {
7
8
  type FloorplanToolContext,
8
9
  getFloorplanNodeExtension,
@@ -35,6 +36,7 @@ function registeredFloorplanTool(
35
36
  export function FloorplanRegisteredToolLayer() {
36
37
  const mode = useEditor((state) => state.mode)
37
38
  const tool = useEditor((state) => state.tool)
39
+ const registeredToolEnabled = useRegisteredToolEnabled(tool)
38
40
  const floorplanMode = useFloorplanMode((state) => state.mode)
39
41
  const gridSnapStep = useEditor((state) => state.gridSnapStep)
40
42
  const toolDefaults = useEditor((state) =>
@@ -54,7 +56,7 @@ export function FloorplanRegisteredToolLayer() {
54
56
  useEditor.getState().setMode('select')
55
57
  }, [])
56
58
  if (mode !== 'build') return null
57
- const Tool = registeredFloorplanTool(tool, floorplanMode)
59
+ const Tool = registeredToolEnabled ? registeredFloorplanTool(tool, floorplanMode) : null
58
60
  return Tool ? (
59
61
  <Suspense fallback={null}>
60
62
  <Tool
@@ -6,8 +6,11 @@ import {
6
6
  type AnyNodeId,
7
7
  bboxAnchors,
8
8
  bboxCornerAnchors,
9
+ createSceneApi,
9
10
  emitter,
10
11
  type FloorplanMoveTargetSession,
12
+ type GroupMoveSnapResult,
13
+ type MovableConfig,
11
14
  nodeRegistry,
12
15
  pauseSceneHistory,
13
16
  resumeSceneHistory,
@@ -20,7 +23,11 @@ import { useEffect } from 'react'
20
23
  import { commitFreshPlacementSubtree } from '../../lib/fresh-planar-placement'
21
24
  import { isHistoryShortcut } from '../../lib/history'
22
25
  import { isFreshPlacementMetadata, stripPlacementMetadataFlags } from '../../lib/placement-metadata'
23
- import { resolvePlanarCursorPosition } from '../../lib/planar-cursor-placement'
26
+ import { resolvePrioritizedPlanarCursorPosition } from '../../lib/planar-cursor-placement'
27
+ import {
28
+ resolveAttachmentPreviewRotation,
29
+ rigidPlanSvgTransform,
30
+ } from '../../lib/rigid-plan-svg-transform'
24
31
  import { movementSfxStepKey } from '../../lib/sfx/movement-tick'
25
32
  import { sfxEmitter } from '../../lib/sfx-bus'
26
33
  import { resolveAlignmentForFloorplanView } from '../../lib/world-grid-snap'
@@ -104,12 +111,14 @@ export function FloorplanRegistryMoveOverlay() {
104
111
  // ── Path 1 — kind-owned `floorplanMoveTarget` ───────────────────
105
112
  if (hasMoveTarget && def?.floorplanMoveTarget) {
106
113
  const sceneNodes = useScene.getState().nodes
114
+ const sceneApi = createSceneApi(useScene)
107
115
  const session: FloorplanMoveTargetSession = (
108
116
  def.floorplanMoveTarget as (a: {
109
117
  node: AnyNode
110
118
  nodes: Record<AnyNodeId, AnyNode>
119
+ sceneApi: ReturnType<typeof createSceneApi>
111
120
  }) => FloorplanMoveTargetSession
112
- )({ node: movingNode, nodes: sceneNodes })
121
+ )({ node: movingNode, nodes: sceneNodes, sceneApi })
113
122
 
114
123
  // Capture snapshots of every affected node BEFORE the first apply
115
124
  // so the single-undo dance has a clean baseline to revert to.
@@ -544,8 +553,19 @@ export function FloorplanRegistryMoveOverlay() {
544
553
  candidateAnchors.push(...bboxAnchors(otherId, b.x, b.y, b.x + b.width, b.y + b.height))
545
554
  }
546
555
 
547
- let lastSnapped: [number, number] | null = null
556
+ const storedRotation = (movingNode as { rotation?: unknown }).rotation
557
+ const originalRotation =
558
+ typeof storedRotation === 'number'
559
+ ? storedRotation
560
+ : Array.isArray(storedRotation)
561
+ ? ((storedRotation as [number?, number?, number?])[1] ?? 0)
562
+ : 0
563
+ let currentRotation = originalRotation
564
+ let lastSnapped: { point: [number, number]; rotation: number } | null = null
548
565
  let dragAnchor: [number, number] | null = null
566
+ let lastPositionValid = true
567
+ let forcePlace = false
568
+ const movableValidityConfig = (def?.capabilities?.movable as MovableConfig | undefined) ?? null
549
569
 
550
570
  // Footprint bounding box drawn around the dragged entry — the 2D
551
571
  // counterpart of the 3D `DragBoundingBox`, so a moved / duplicated node
@@ -572,22 +592,74 @@ export function FloorplanRegistryMoveOverlay() {
572
592
  if (!isPointerOverFloorplanScene(event.clientX, event.clientY)) return
573
593
  const m = toMeters(event.clientX, event.clientY)
574
594
  if (!m) return
595
+ forcePlace = event.altKey
575
596
 
576
- // 1) Grid snap baseline. Fresh catalog placement is absolute under
577
- // the cursor; existing moves preserve the cursor's grab offset. Grid
578
- // follows the active snapping mode (Shift cycles it); raw cursor in
579
- // any non-grid mode.
597
+ // 1) Wall attachment gets the raw proposal before grid/alignment. If no
598
+ // attachment is available, fresh placement is absolute under the cursor
599
+ // and existing moves preserve the cursor's grab offset before grid snap.
580
600
  const gridStep = useEditor.getState().gridSnapStep
581
601
  const snap = (value: number) =>
582
602
  isGridSnapActive() ? Math.round(value / gridStep) * gridStep : value
583
- const resolved = resolvePlanarCursorPosition({
603
+ const groupMoveSnap = def?.capabilities?.movable?.groupMoveSnap
604
+ const groupMoveSnapPose = def?.capabilities?.movable?.groupMoveSnapPose
605
+ const gridSnapPosition = def?.capabilities?.movable?.gridSnapPosition
606
+ const attachmentEnabled = isGridSnapActive() || isMagneticSnapActive()
607
+ let attachmentRotation: number | null = null
608
+ const resolved = resolvePrioritizedPlanarCursorPosition({
584
609
  cursor: [m[0], m[1]],
585
610
  original: [originalPosition[0], originalPosition[2]],
586
611
  anchor: dragAnchor,
587
612
  mode: isFreshPlacement ? 'absolute' : 'relative',
588
- snap,
613
+ snap: gridSnapPosition ? undefined : snap,
614
+ snapPoint:
615
+ isGridSnapActive() && gridSnapPosition
616
+ ? ([planX, planZ]) => {
617
+ const snappedPosition = gridSnapPosition({
618
+ node: movingNode,
619
+ candidatePosition: [planX, originalPosition[1], planZ],
620
+ candidateRotation: originalRotation,
621
+ movingIds: [movingNode.id as AnyNodeId],
622
+ nodes: useScene.getState().nodes as Record<string, AnyNode>,
623
+ levelId:
624
+ (useViewer.getState().selection.levelId as AnyNodeId | null) ??
625
+ (movingNode.parentId as AnyNodeId | undefined) ??
626
+ null,
627
+ gridStep,
628
+ })
629
+ return [snappedPosition[0], snappedPosition[2]]
630
+ }
631
+ : undefined,
632
+ resolveAttachment:
633
+ attachmentEnabled && (groupMoveSnapPose || groupMoveSnap)
634
+ ? ([planX, planZ]) => {
635
+ const snapArgs: Parameters<NonNullable<typeof groupMoveSnapPose>>[0] = {
636
+ node: movingNode,
637
+ candidatePosition: [planX, originalPosition[1], planZ],
638
+ candidateRotation: currentRotation,
639
+ movingIds: [movingNode.id as AnyNodeId],
640
+ nodes: useScene.getState().nodes as Record<string, AnyNode>,
641
+ levelId:
642
+ (useViewer.getState().selection.levelId as AnyNodeId | null) ??
643
+ (movingNode.parentId as AnyNodeId | undefined) ??
644
+ null,
645
+ }
646
+ const snappedPosition: GroupMoveSnapResult | null = groupMoveSnapPose
647
+ ? groupMoveSnapPose(snapArgs)
648
+ : (() => {
649
+ const position = groupMoveSnap?.(snapArgs)
650
+ return position ? { position } : null
651
+ })()
652
+ if (!snappedPosition) return null
653
+ attachmentRotation = snappedPosition.rotation ?? null
654
+ return [snappedPosition.position[0], snappedPosition.position[2]]
655
+ }
656
+ : undefined,
589
657
  })
590
658
  dragAnchor = resolved.anchor
659
+ currentRotation = resolveAttachmentPreviewRotation(
660
+ originalRotation,
661
+ resolved.attachmentSnapped ? attachmentRotation : null,
662
+ )
591
663
  const [gridX, gridZ] = resolved.point
592
664
 
593
665
  // 2) Alignment snap layered on top. Treat the grid-snapped point
@@ -598,7 +670,7 @@ export function FloorplanRegistryMoveOverlay() {
598
670
  // force-place, not a snap bypass.
599
671
  let finalX = gridX
600
672
  let finalZ = gridZ
601
- if (isAlignmentGuideActive() && candidateAnchors.length > 0) {
673
+ if (!resolved.attachmentSnapped && isAlignmentGuideActive() && candidateAnchors.length > 0) {
602
674
  // Translate the cached local bbox to the proposed pos to get the
603
675
  // moving anchors at that location. The entry's untransformed
604
676
  // bbox is in world meters relative to the node's origin, so a
@@ -635,35 +707,35 @@ export function FloorplanRegistryMoveOverlay() {
635
707
  useAlignmentGuides.getState().clear()
636
708
  }
637
709
 
638
- // 3) Kind-owned attachment snap (cabinet → wall) — 2D parity with the
639
- // 3D move tool's `groupMoveSnap` pass. An attach behavior, not an
640
- // alignment guide, so it runs in every snapping mode except Off.
641
- const groupMoveSnap = def?.capabilities?.movable?.groupMoveSnap
642
- if (groupMoveSnap && (isGridSnapActive() || isMagneticSnapActive())) {
643
- const snappedPosition = groupMoveSnap({
644
- node: movingNode,
645
- candidatePosition: [finalX, originalPosition[1], finalZ],
646
- movingIds: [movingNode.id as AnyNodeId],
647
- nodes: useScene.getState().nodes as Record<string, AnyNode>,
648
- levelId:
649
- (useViewer.getState().selection.levelId as AnyNodeId | null) ??
650
- (movingNode.parentId as AnyNodeId | undefined) ??
651
- null,
652
- })
653
- if (snappedPosition) {
654
- finalX = snappedPosition[0]
655
- finalZ = snappedPosition[2]
656
- useAlignmentGuides.getState().clear()
657
- }
658
- }
659
-
660
- const dx = finalX - originalPosition[0]
661
- const dz = finalZ - originalPosition[2]
710
+ const transform = rigidPlanSvgTransform({
711
+ from: [originalPosition[0], originalPosition[2]],
712
+ fromRotation: originalRotation,
713
+ to: [finalX, finalZ],
714
+ toRotation: currentRotation,
715
+ })
662
716
  for (const relatedEntry of relatedEntries) {
663
- relatedEntry.setAttribute('transform', `translate(${dx} ${dz})`)
717
+ relatedEntry.setAttribute('transform', transform)
664
718
  }
665
- boxEl.setAttribute('transform', `translate(${dx} ${dz})`)
666
- lastSnapped = [finalX, finalZ]
719
+ boxEl.setAttribute('transform', transform)
720
+ const oldY = originalPosition[1]
721
+ lastPositionValid = movableValidityConfig?.isValidPosition
722
+ ? movableValidityConfig.isValidPosition({
723
+ node: {
724
+ ...movingNode,
725
+ position: [finalX, oldY, finalZ],
726
+ rotation: currentRotation,
727
+ } as AnyNode,
728
+ position: [finalX, oldY, finalZ],
729
+ rotation: currentRotation,
730
+ levelId:
731
+ (useViewer.getState().selection.levelId as AnyNodeId | null) ??
732
+ (movingNode.parentId as AnyNodeId | null) ??
733
+ null,
734
+ nodes: useScene.getState().nodes as Record<string, AnyNode>,
735
+ })
736
+ : true
737
+ boxEl.setAttribute('stroke', lastPositionValid || forcePlace ? '#22c55e' : '#ef4444')
738
+ lastSnapped = { point: [finalX, finalZ], rotation: currentRotation }
667
739
  }
668
740
 
669
741
  const onPointerUp = (event: PointerEvent) => {
@@ -672,9 +744,26 @@ export function FloorplanRegistryMoveOverlay() {
672
744
 
673
745
  const snapped = lastSnapped
674
746
  if (!snapped) return
675
- const [sx, sz] = snapped
747
+ const [sx, sz] = snapped.point
676
748
  const [, oldY] = originalPosition
749
+ const rotation = Array.isArray(storedRotation)
750
+ ? [
751
+ (storedRotation as [number?, number?, number?])[0] ?? 0,
752
+ snapped.rotation,
753
+ (storedRotation as [number?, number?, number?])[2] ?? 0,
754
+ ]
755
+ : snapped.rotation
756
+ const rotationPatch = 'rotation' in movingNode ? { rotation } : {}
677
757
  setMovingNodeOrigin('2d')
758
+ if (!lastPositionValid && !forcePlace) {
759
+ for (const relatedEntry of relatedEntries) {
760
+ relatedEntry.removeAttribute('transform')
761
+ }
762
+ useAlignmentGuides.getState().clear()
763
+ setMovingNode(null)
764
+ swallowNextClick()
765
+ return
766
+ }
678
767
  let selectedId = movingNode.id as AnyNodeId
679
768
  if (originalPath) {
680
769
  // Polyline kinds: shift every point by the committed delta and
@@ -711,6 +800,7 @@ export function FloorplanRegistryMoveOverlay() {
711
800
  movingNode.id as AnyNodeId,
712
801
  {
713
802
  position: [sx, oldY, sz],
803
+ ...rotationPatch,
714
804
  metadata: stripPlacementMetadataFlags(
715
805
  (movingNode as { metadata?: unknown }).metadata,
716
806
  ),
@@ -722,6 +812,7 @@ export function FloorplanRegistryMoveOverlay() {
722
812
  movingNode.id as AnyNodeId,
723
813
  {
724
814
  position: [sx, oldY, sz],
815
+ ...rotationPatch,
725
816
  } as Partial<AnyNode>,
726
817
  )
727
818
  }
@@ -161,12 +161,14 @@ export function FloorplanDimensionRenderer({
161
161
  stroke = geometry.stroke ?? '#334155',
162
162
  annotationUnitsPerPoint,
163
163
  renderMode = 'screen',
164
+ onSelect,
164
165
  }: {
165
166
  geometry: DimensionGeometry
166
167
  sceneRotationDeg?: number
167
168
  stroke?: string
168
169
  annotationUnitsPerPoint?: number
169
170
  renderMode?: FloorplanDimensionRenderMode
171
+ onSelect?: () => void
170
172
  }): React.ReactElement | null {
171
173
  const layout = computeArchitecturalDimensionLayout(
172
174
  geometry,
@@ -200,7 +202,18 @@ export function FloorplanDimensionRenderer({
200
202
  : undefined
201
203
 
202
204
  return (
203
- <g data-floorplan-dimension="" pointerEvents="none">
205
+ <g
206
+ data-floorplan-dimension=""
207
+ onClick={
208
+ onSelect
209
+ ? (event) => {
210
+ event.stopPropagation()
211
+ onSelect()
212
+ }
213
+ : undefined
214
+ }
215
+ pointerEvents={onSelect ? 'auto' : 'none'}
216
+ >
204
217
  <line
205
218
  {...lineProps}
206
219
  x1={layout.extensionStart[0]}
@@ -366,4 +366,20 @@ describe('FloorplanGeometryRenderer static labels', () => {
366
366
  expect(markup).toContain('data-floorplan-annotation-obstacle="bounds"')
367
367
  expect(markup).toContain('data-floorplan-annotation-obstacle="outline"')
368
368
  })
369
+ test('forwards even-odd fill rules for compound plugin paths', () => {
370
+ const geometry = {
371
+ kind: 'path',
372
+ d: 'M0,0H4V4H0ZM1,1H3V3H1Z',
373
+ fill: '#3f6b2f',
374
+ fillRule: 'evenodd',
375
+ } satisfies FloorplanGeometry
376
+
377
+ const markup = renderToStaticMarkup(
378
+ <svg>
379
+ <FloorplanGeometryRenderer geometry={geometry} />
380
+ </svg>,
381
+ )
382
+
383
+ expect(markup).toContain('fill-rule="evenodd"')
384
+ })
369
385
  })
@@ -83,6 +83,7 @@ function styleAttrs(
83
83
  // filtered out by the caller's type bound).
84
84
  const s = g as unknown as {
85
85
  fill?: string
86
+ fillRule?: 'nonzero' | 'evenodd'
86
87
  fillOpacity?: number
87
88
  stroke?: string
88
89
  strokeWidth?: number
@@ -109,6 +110,7 @@ function styleAttrs(
109
110
  'data-floorplan-annotation-obstacle': floorplanAnnotationObstacleMode(g),
110
111
  'data-floorplan-annotation-role': annotationMetadata.annotationRole,
111
112
  fill: documentStyle.fill ?? s.fill ?? 'none',
113
+ fillRule: s.fillRule,
112
114
  fillOpacity: s.fillOpacity,
113
115
  stroke: documentStyle.stroke ?? s.stroke,
114
116
  strokeWidth,
@@ -10,13 +10,16 @@ import {
10
10
  } from '@pascal-app/core'
11
11
  import { useViewer } from '@pascal-app/viewer'
12
12
  import { memo, useMemo } from 'react'
13
+ import { formatLinearMeasurement } from '../../../lib/measurements'
13
14
  import usePlacementPreview from '../../../store/use-placement-preview'
14
- import { useFloorplanRender } from '../floorplan-render-context'
15
+ import { useFloorplanRender, useFloorplanSceneRotation } from '../floorplan-render-context'
16
+ import { FloorplanDimensionRenderer } from './floorplan-dimension-renderer'
15
17
  import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer'
16
18
 
17
19
  export interface FloorplanNodePreviewProps {
18
20
  node: AnyNode
19
21
  parentNode?: AnyNode | null
22
+ contextNodes?: AnyNode[]
20
23
  opacity?: number
21
24
  className?: string
22
25
  selected?: boolean
@@ -33,6 +36,7 @@ export interface FloorplanNodePreviewProps {
33
36
  export const FloorplanNodePreview = memo(function FloorplanNodePreview({
34
37
  node,
35
38
  parentNode = null,
39
+ contextNodes: previewContextNodes = [],
36
40
  opacity = 0.5,
37
41
  className,
38
42
  selected = false,
@@ -53,6 +57,9 @@ export const FloorplanNodePreview = memo(function FloorplanNodePreview({
53
57
  ...(nodes as Record<string, AnyNode>),
54
58
  [node.id]: node,
55
59
  }
60
+ for (const previewNode of previewContextNodes) {
61
+ contextNodes[previewNode.id] = previewNode
62
+ }
56
63
  if (parentNode) contextNodes[parentNode.id] = parentNode
57
64
  const resolvedParent =
58
65
  parentNode ?? (node.parentId ? (contextNodes[node.parentId] ?? null) : null)
@@ -90,7 +97,18 @@ export const FloorplanNodePreview = memo(function FloorplanNodePreview({
90
97
  }
91
98
 
92
99
  return (builder as (n: AnyNode, c: GeometryContext) => FloorplanGeometry | null)(node, ctx)
93
- }, [highlighted, hovered, moving, node, nodes, parentNode, renderContext, selected, unit])
100
+ }, [
101
+ highlighted,
102
+ hovered,
103
+ moving,
104
+ node,
105
+ nodes,
106
+ parentNode,
107
+ previewContextNodes,
108
+ renderContext,
109
+ selected,
110
+ unit,
111
+ ])
94
112
  if (!geometry) return null
95
113
 
96
114
  return (
@@ -117,11 +135,42 @@ export const FloorplanNodePreview = memo(function FloorplanNodePreview({
117
135
  export const FloorplanPlacementPreviewLayer = memo(function FloorplanPlacementPreviewLayer() {
118
136
  const node = usePlacementPreview((s) => s.node)
119
137
  const parentNode = usePlacementPreview((s) => s.parentNode)
138
+ const contextNodes = usePlacementPreview((s) => s.contextNodes)
139
+ const dimensions = usePlacementPreview((s) => s.dimensions)
140
+ const activeDimensionId = usePlacementPreview((s) => s.activeDimensionId)
141
+ const dimensionInput = usePlacementPreview((s) => s.dimensionInput)
142
+ const unit = useViewer((s) => s.unit)
143
+ const metricNotation = useViewer((s) => s.metricNotation)
144
+ const sceneRotationDeg = useFloorplanSceneRotation()
120
145
  if (!node) return null
121
146
 
122
147
  return (
123
148
  <g data-floorplan-placement-preview>
124
- <FloorplanNodePreview node={node} parentNode={parentNode} />
149
+ <FloorplanNodePreview contextNodes={contextNodes} node={node} parentNode={parentNode} />
150
+ <g data-floorplan-placement-dimensions>
151
+ {dimensions
152
+ .filter((dimension) => dimension.renderInFloorplan !== false)
153
+ .map((dimension) => (
154
+ <FloorplanDimensionRenderer
155
+ geometry={{
156
+ kind: 'dimension',
157
+ start: [dimension.start[0], dimension.start[2]],
158
+ end: [dimension.end[0], dimension.end[2]],
159
+ offsetNormal: dimension.offsetNormal,
160
+ offsetDistance: dimension.offsetDistance,
161
+ extensionOvershoot: 0.04,
162
+ text:
163
+ dimension.id === activeDimensionId && dimensionInput
164
+ ? dimensionInput
165
+ : formatLinearMeasurement(dimension.value, unit, metricNotation),
166
+ stroke: '#6366f1',
167
+ }}
168
+ key={dimension.id}
169
+ onSelect={() => usePlacementPreview.getState().selectDimension(dimension.id)}
170
+ sceneRotationDeg={sceneRotationDeg}
171
+ />
172
+ ))}
173
+ </g>
125
174
  </g>
126
175
  )
127
176
  })