@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.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +63 -8
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +12 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +23 -6
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/panel-wrapper.tsx +228 -12
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-session-groups.ts +103 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- 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
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
const restAnchors =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
const restCenter
|
|
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 {
|
|
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
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
|
419
|
-
|
|
420
|
-
|
|
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 {
|
|
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
|
-
|
|
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)
|
|
577
|
-
//
|
|
578
|
-
//
|
|
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
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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',
|
|
717
|
+
relatedEntry.setAttribute('transform', transform)
|
|
664
718
|
}
|
|
665
|
-
boxEl.setAttribute('transform',
|
|
666
|
-
|
|
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
|
|
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
|
-
}, [
|
|
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
|
})
|