@pascal-app/editor 0.9.2 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -7
- package/src/components/editor/bake-exporter.tsx +3 -1
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +307 -212
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +66 -2
- package/src/components/editor/first-person/build-collider-world.ts +20 -0
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +295 -132
- package/src/components/editor/floating-action-menu.tsx +59 -65
- package/src/components/editor/floorplan-background-selection.test.ts +16 -2
- package/src/components/editor/floorplan-background-selection.ts +4 -18
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +931 -861
- package/src/components/editor/group-actions.ts +155 -15
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-rotate-handle.tsx +1 -1
- package/src/components/editor/group-selection-box-3d.tsx +1 -1
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +17 -7
- package/src/components/editor/index.tsx +42 -9
- package/src/components/editor/measurement-pill.tsx +6 -1
- package/src/components/editor/node-arrow-handles.tsx +51 -15
- package/src/components/editor/quick-measurement-card.tsx +9 -3
- package/src/components/editor/quick-measurement-hud.tsx +7 -1
- package/src/components/editor/selection-manager.tsx +3 -1
- package/src/components/editor/site-edge-labels.tsx +24 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -1
- package/src/components/editor/thumbnail-generator.tsx +66 -294
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-mesh-settle-epoch.ts +1 -0
- package/src/components/editor/wall-measurement-label.tsx +8 -16
- package/src/components/editor/wall-move-side-handles.tsx +193 -18
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +6 -4
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +39 -8
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +47 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +21 -5
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +93 -59
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +27 -0
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +75 -1
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +92 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +93 -28
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +279 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +621 -281
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +12 -7
- package/src/components/tools/fence/fence-drafting.ts +27 -0
- package/src/components/tools/item/use-draft-node.ts +131 -72
- package/src/components/tools/item/use-placement-coordinator.tsx +52 -4
- package/src/components/tools/registry/move-registry-node-tool.tsx +75 -4
- package/src/components/tools/roof/roof-tool.tsx +16 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/site/site-boundary-editor.tsx +13 -6
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-tool.tsx +98 -47
- package/src/components/tools/tool-manager.tsx +67 -9
- package/src/components/tools/wall/wall-drafting.test.ts +118 -1
- package/src/components/tools/wall/wall-drafting.ts +128 -9
- package/src/components/tools/wall/wall-snap-geometry.test.ts +8 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +70 -3
- package/src/components/tools/zone/zone-tool.tsx +16 -0
- package/src/components/ui/action-menu/control-modes.tsx +4 -1
- package/src/components/ui/action-menu/measurement-control.tsx +107 -28
- package/src/components/ui/command-palette/editor-commands.tsx +14 -1
- package/src/components/ui/controls/material-paint-panel.tsx +7 -1
- package/src/components/ui/controls/material-picker.tsx +86 -7
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +73 -19
- package/src/components/ui/helpers/helper-manager.tsx +46 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/panel-manager.tsx +14 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -2
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +14 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +16 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +20 -655
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +53 -30
- package/src/index.tsx +137 -4
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +8 -1
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/direct-manipulation.test.ts +83 -0
- package/src/lib/direct-manipulation.ts +9 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +745 -131
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +2 -3
- package/src/lib/fresh-planar-placement.test.ts +44 -1
- package/src/lib/fresh-planar-placement.ts +21 -4
- package/src/lib/glb-export.test.ts +116 -1
- package/src/lib/glb-export.ts +204 -7
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +22 -0
- package/src/lib/interaction/overlay-policy.test.ts +2 -1
- package/src/lib/interaction/scope.ts +40 -8
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/measurements.test.ts +5 -0
- package/src/lib/measurements.ts +12 -1
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/scene-clipboard.test.ts +130 -1
- package/src/lib/scene-clipboard.ts +237 -13
- package/src/lib/selection-routing.test.ts +16 -1
- package/src/lib/selection-routing.ts +5 -0
- package/src/lib/sfx-bus.ts +33 -2
- package/src/lib/sfx-player.test.ts +148 -6
- package/src/lib/sfx-player.ts +156 -8
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/snapping-mode.test.ts +29 -2
- package/src/lib/snapping-mode.ts +15 -3
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +57 -85
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +184 -15
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-fence-curve-draft.ts +38 -8
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +54 -4
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +23 -3
- package/src/store/use-interaction-scope.ts +8 -0
- package/src/store/use-measurement-draft.test.ts +29 -0
- package/src/store/use-measurement-draft.ts +33 -6
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-stair-build-preview.ts +12 -2
- package/src/lib/floorplan/selection-tool.ts +0 -271
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
bboxCornerAnchors,
|
|
7
7
|
collectAlignmentAnchors,
|
|
8
8
|
DEFAULT_ANGLE_STEP,
|
|
9
|
+
type FloorplanGeometry,
|
|
9
10
|
type FloorplanPalette,
|
|
10
11
|
pauseSceneHistory,
|
|
11
12
|
pauseSpaceDetection,
|
|
@@ -22,13 +23,16 @@ import { GROUP_MOVE_DRAG_LABEL, GROUP_ROTATE_DRAG_LABEL } from '../../lib/contex
|
|
|
22
23
|
import { applyFloorplanAlignment } from '../../lib/floorplan/apply-alignment'
|
|
23
24
|
import { clientToPlan } from '../../lib/floorplan/plan-coords'
|
|
24
25
|
import { isHistoryShortcut } from '../../lib/history'
|
|
26
|
+
import { formatLinearMeasurement } from '../../lib/measurements'
|
|
25
27
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
26
28
|
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
27
29
|
import useEditor, {
|
|
28
30
|
isAlignmentGuideActive,
|
|
31
|
+
isAngleSnapActive,
|
|
29
32
|
isGridSnapActive,
|
|
30
33
|
isMagneticSnapActive,
|
|
31
34
|
} from '../../store/use-editor'
|
|
35
|
+
import useFloorplanMode from '../../store/use-floorplan-mode'
|
|
32
36
|
import useInteractionScope, { useMovingNode } from '../../store/use-interaction-scope'
|
|
33
37
|
import {
|
|
34
38
|
classifyParticipant,
|
|
@@ -44,6 +48,8 @@ import {
|
|
|
44
48
|
} from '../editor/group-transform-shared'
|
|
45
49
|
import { swallowNextClick } from '../editor/handles/use-handle-drag'
|
|
46
50
|
import { useMeshSettleEpoch } from '../editor/use-mesh-settle-epoch'
|
|
51
|
+
import { useFloorplanSceneRotation } from './floorplan-render-context'
|
|
52
|
+
import { FloorplanDimensionRenderer } from './renderers/floorplan-dimension-renderer'
|
|
47
53
|
|
|
48
54
|
// 2D sibling of the 3D body-drag group move (`group-move-3d.ts`): dragging
|
|
49
55
|
// any selected element of a multi-selection slides the whole selection
|
|
@@ -441,9 +447,9 @@ export function startFloorplanGroupRotate(event: {
|
|
|
441
447
|
let delta = angleOf([plan[0], plan[1]]) - initialAngle
|
|
442
448
|
while (delta > Math.PI) delta -= 2 * Math.PI
|
|
443
449
|
while (delta < -Math.PI) delta += 2 * Math.PI
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
450
|
+
if (isAngleSnapActive()) {
|
|
451
|
+
delta = Math.round(delta / DEFAULT_ANGLE_STEP) * DEFAULT_ANGLE_STEP
|
|
452
|
+
}
|
|
447
453
|
|
|
448
454
|
const entries = rotateGroupPatches(starts, links, pivot, delta)
|
|
449
455
|
const patchById = new Map(entries)
|
|
@@ -586,11 +592,15 @@ export const FloorplanGroupSelectionBox = memo(function FloorplanGroupSelectionB
|
|
|
586
592
|
}) {
|
|
587
593
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
588
594
|
const levelId = useViewer((s) => s.selection.levelId)
|
|
595
|
+
const unit = useViewer((s) => s.unit)
|
|
596
|
+
const metricNotation = useViewer((s) => s.metricNotation)
|
|
589
597
|
const nodes = useScene((s) => s.nodes)
|
|
590
598
|
const delta = useFloorplanGroupDrag((s) => s.delta)
|
|
591
599
|
const liveRotation = useFloorplanGroupDrag((s) => s.rotation)
|
|
592
600
|
const movingNode = useMovingNode()
|
|
593
601
|
const mode = useEditor((s) => s.mode)
|
|
602
|
+
const floorplanMode = useFloorplanMode((s) => s.mode)
|
|
603
|
+
const sceneRotationDeg = useFloorplanSceneRotation()
|
|
594
604
|
|
|
595
605
|
// While a selection modifier is held the box steps aside so clicks reach
|
|
596
606
|
// the entries underneath (toggle membership) instead of starting a drag.
|
|
@@ -613,6 +623,7 @@ export const FloorplanGroupSelectionBox = memo(function FloorplanGroupSelectionB
|
|
|
613
623
|
// which lag the `nodes` commit by a frame or two.
|
|
614
624
|
const meshEpoch = useMeshSettleEpoch(nodes)
|
|
615
625
|
const box = useMemo(() => {
|
|
626
|
+
void meshEpoch
|
|
616
627
|
if (selectedIds.length < 2 || !levelId) return null
|
|
617
628
|
const participantIds = selectedIds.filter(
|
|
618
629
|
(id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
|
|
@@ -630,7 +641,6 @@ export const FloorplanGroupSelectionBox = memo(function FloorplanGroupSelectionB
|
|
|
630
641
|
width: Math.abs(max.x - min.x),
|
|
631
642
|
depth: Math.abs(max.z - min.z),
|
|
632
643
|
}
|
|
633
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: meshEpoch re-measures settled meshes
|
|
634
644
|
}, [selectedIds, levelId, nodes, meshEpoch])
|
|
635
645
|
|
|
636
646
|
if (!box || movingNode || mode === 'delete') return null
|
|
@@ -638,6 +648,27 @@ export const FloorplanGroupSelectionBox = memo(function FloorplanGroupSelectionB
|
|
|
638
648
|
const pad = 6 * unitsPerPixel
|
|
639
649
|
const stroke = palette?.selectedStroke ?? '#3b82f6'
|
|
640
650
|
const interactive = !modifierHeld && !!onPointerDown
|
|
651
|
+
const dimensionOffset = pad + 0.28
|
|
652
|
+
const widthDimension = {
|
|
653
|
+
kind: 'dimension',
|
|
654
|
+
start: [box.x, box.z + box.depth],
|
|
655
|
+
end: [box.x + box.width, box.z + box.depth],
|
|
656
|
+
offsetNormal: [0, 1],
|
|
657
|
+
offsetDistance: dimensionOffset,
|
|
658
|
+
extensionOvershoot: 0.08,
|
|
659
|
+
text: formatLinearMeasurement(box.width, unit, metricNotation),
|
|
660
|
+
stroke,
|
|
661
|
+
} satisfies Extract<FloorplanGeometry, { kind: 'dimension' }>
|
|
662
|
+
const depthDimension = {
|
|
663
|
+
kind: 'dimension',
|
|
664
|
+
start: [box.x + box.width, box.z],
|
|
665
|
+
end: [box.x + box.width, box.z + box.depth],
|
|
666
|
+
offsetNormal: [1, 0],
|
|
667
|
+
offsetDistance: dimensionOffset,
|
|
668
|
+
extensionOvershoot: 0.08,
|
|
669
|
+
text: formatLinearMeasurement(box.depth, unit, metricNotation),
|
|
670
|
+
stroke,
|
|
671
|
+
} satisfies Extract<FloorplanGeometry, { kind: 'dimension' }>
|
|
641
672
|
// Mid-gesture the box rides the live delta (group move) or spins around the
|
|
642
673
|
// rotation pivot (corner rotate) — SVG rotate() is degrees around a plan
|
|
643
674
|
// point, and positive matches the atan2 x→z sense on the y-down plan.
|
|
@@ -664,6 +695,18 @@ export const FloorplanGroupSelectionBox = memo(function FloorplanGroupSelectionB
|
|
|
664
695
|
x={box.x - pad}
|
|
665
696
|
y={box.z - pad}
|
|
666
697
|
/>
|
|
698
|
+
{floorplanMode === 'default' ? (
|
|
699
|
+
<g pointerEvents="none">
|
|
700
|
+
<FloorplanDimensionRenderer
|
|
701
|
+
geometry={widthDimension}
|
|
702
|
+
sceneRotationDeg={sceneRotationDeg}
|
|
703
|
+
/>
|
|
704
|
+
<FloorplanDimensionRenderer
|
|
705
|
+
geometry={depthDimension}
|
|
706
|
+
sceneRotationDeg={sceneRotationDeg}
|
|
707
|
+
/>
|
|
708
|
+
</g>
|
|
709
|
+
) : null}
|
|
667
710
|
{/* Corner rotate handles — the 2D counterpart of the 3D rotate gizmo:
|
|
668
711
|
drag a corner to spin the group (15° steps, Shift = free). */}
|
|
669
712
|
{interactive && onRotatePointerDown
|
|
@@ -786,6 +786,7 @@ export function FloorplanMeasurementToolLayer() {
|
|
|
786
786
|
const draftLevelId = useMeasurementDraft((state) => state.levelId)
|
|
787
787
|
const activeLevelId = useViewer((state) => state.selection.levelId)
|
|
788
788
|
const unit = useViewer((state) => state.unit)
|
|
789
|
+
const metricNotation = useViewer((state) => state.metricNotation)
|
|
789
790
|
|
|
790
791
|
useEffect(() => {
|
|
791
792
|
if (active) {
|
|
@@ -1224,7 +1225,7 @@ export function FloorplanMeasurementToolLayer() {
|
|
|
1224
1225
|
angle: Math.atan2(end[2] - start[2], end[0] - start[0]),
|
|
1225
1226
|
point: [(start[0] + end[0]) / 2, 0, (start[2] + end[2]) / 2],
|
|
1226
1227
|
screenUpright: false,
|
|
1227
|
-
text: formatLinearMeasurement(measurementDistance(start, end), unit),
|
|
1228
|
+
text: formatLinearMeasurement(measurementDistance(start, end), unit, metricNotation),
|
|
1228
1229
|
}
|
|
1229
1230
|
} else if (kind === 'angle' && livePoints.length >= 3) {
|
|
1230
1231
|
const anglePoints = livePoints.slice(0, 3) as [
|
|
@@ -1248,7 +1249,7 @@ export function FloorplanMeasurementToolLayer() {
|
|
|
1248
1249
|
text:
|
|
1249
1250
|
kind === 'area'
|
|
1250
1251
|
? `A ${formatAreaLabel(measurementArea(livePoints), unit)}`
|
|
1251
|
-
: `P ${formatLinearMeasurement(measurementPerimeter(livePoints), unit)}`,
|
|
1252
|
+
: `P ${formatLinearMeasurement(measurementPerimeter(livePoints), unit, metricNotation)}`,
|
|
1252
1253
|
}
|
|
1253
1254
|
}
|
|
1254
1255
|
} else if (kind === 'volume' && center && baseNormal) {
|
|
@@ -1280,7 +1281,11 @@ export function FloorplanMeasurementToolLayer() {
|
|
|
1280
1281
|
(segmentStart[1] + segmentEnd[1]) / 2,
|
|
1281
1282
|
(segmentStart[2] + segmentEnd[2]) / 2,
|
|
1282
1283
|
],
|
|
1283
|
-
text: formatLinearMeasurement(
|
|
1284
|
+
text: formatLinearMeasurement(
|
|
1285
|
+
measurementDistance(segmentStart, segmentEnd),
|
|
1286
|
+
unit,
|
|
1287
|
+
metricNotation,
|
|
1288
|
+
),
|
|
1284
1289
|
}
|
|
1285
1290
|
}
|
|
1286
1291
|
}
|
|
@@ -1298,7 +1303,18 @@ export function FloorplanMeasurementToolLayer() {
|
|
|
1298
1303
|
polygonPoints,
|
|
1299
1304
|
segmentLabel,
|
|
1300
1305
|
}
|
|
1301
|
-
}, [
|
|
1306
|
+
}, [
|
|
1307
|
+
axisGuide,
|
|
1308
|
+
baseNormal,
|
|
1309
|
+
extrusionHeight,
|
|
1310
|
+
hover,
|
|
1311
|
+
kind,
|
|
1312
|
+
metricNotation,
|
|
1313
|
+
points,
|
|
1314
|
+
stage,
|
|
1315
|
+
unit,
|
|
1316
|
+
vertexDrag,
|
|
1317
|
+
])
|
|
1302
1318
|
|
|
1303
1319
|
if (smartActive) return <FloorplanQuickMeasureLayer />
|
|
1304
1320
|
if (!active || (draftLevelId && draftLevelId !== activeLevelId)) return null
|
|
@@ -1595,7 +1611,7 @@ export function FloorplanMeasurementToolLayer() {
|
|
|
1595
1611
|
text={`${hover.semantic.label}${
|
|
1596
1612
|
hover.semantic.length === null
|
|
1597
1613
|
? ''
|
|
1598
|
-
: ` · ${formatLinearMeasurement(hover.semantic.length, unit)}`
|
|
1614
|
+
: ` · ${formatLinearMeasurement(hover.semantic.length, unit, metricNotation)}`
|
|
1599
1615
|
}`}
|
|
1600
1616
|
textColor={labelText}
|
|
1601
1617
|
unitsPerPixel={unitsPerPixel}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createSceneApi, nodeRegistry, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { type ComponentType, lazy, Suspense, useCallback, useMemo } from 'react'
|
|
6
|
+
import {
|
|
7
|
+
type FloorplanToolContext,
|
|
8
|
+
getFloorplanNodeExtension,
|
|
9
|
+
} from '../../lib/floorplan/floorplan-extension'
|
|
10
|
+
import {
|
|
11
|
+
type FloorplanMode,
|
|
12
|
+
isFloorplanToolAvailableInMode,
|
|
13
|
+
} from '../../lib/floorplan/floorplan-mode'
|
|
14
|
+
import useEditor from '../../store/use-editor'
|
|
15
|
+
import useFloorplanMode from '../../store/use-floorplan-mode'
|
|
16
|
+
|
|
17
|
+
const lazyToolCache = new WeakMap<() => Promise<unknown>, ComponentType<FloorplanToolContext>>()
|
|
18
|
+
|
|
19
|
+
function registeredFloorplanTool(
|
|
20
|
+
tool: string | null,
|
|
21
|
+
mode: FloorplanMode,
|
|
22
|
+
): ComponentType<FloorplanToolContext> | null {
|
|
23
|
+
if (!tool) return null
|
|
24
|
+
const extension = getFloorplanNodeExtension(nodeRegistry.get(tool))
|
|
25
|
+
if (!isFloorplanToolAvailableInMode(extension?.availableModes, mode)) return null
|
|
26
|
+
const loader = extension?.tool
|
|
27
|
+
if (!loader) return null
|
|
28
|
+
const cached = lazyToolCache.get(loader)
|
|
29
|
+
if (cached) return cached
|
|
30
|
+
const component = lazy(loader)
|
|
31
|
+
lazyToolCache.set(loader, component)
|
|
32
|
+
return component
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function FloorplanRegisteredToolLayer() {
|
|
36
|
+
const mode = useEditor((state) => state.mode)
|
|
37
|
+
const tool = useEditor((state) => state.tool)
|
|
38
|
+
const floorplanMode = useFloorplanMode((state) => state.mode)
|
|
39
|
+
const gridSnapStep = useEditor((state) => state.gridSnapStep)
|
|
40
|
+
const toolDefaults = useEditor((state) =>
|
|
41
|
+
state.tool ? (state.toolDefaults[state.tool] ?? null) : null,
|
|
42
|
+
)
|
|
43
|
+
const activeLevelId = useViewer((state) => state.selection.levelId)
|
|
44
|
+
const unit = useViewer((state) => state.unit)
|
|
45
|
+
const metricNotation = useViewer((state) => state.metricNotation)
|
|
46
|
+
const sceneApi = useMemo(() => createSceneApi(useScene), [])
|
|
47
|
+
const selectNode = useCallback(
|
|
48
|
+
(id: Parameters<FloorplanToolContext['selectNode']>[0]) =>
|
|
49
|
+
useViewer.getState().setSelection({ selectedIds: [id] }),
|
|
50
|
+
[],
|
|
51
|
+
)
|
|
52
|
+
const finishTool = useCallback(() => {
|
|
53
|
+
useEditor.getState().setTool(null)
|
|
54
|
+
useEditor.getState().setMode('select')
|
|
55
|
+
}, [])
|
|
56
|
+
if (mode !== 'build') return null
|
|
57
|
+
const Tool = registeredFloorplanTool(tool, floorplanMode)
|
|
58
|
+
return Tool ? (
|
|
59
|
+
<Suspense fallback={null}>
|
|
60
|
+
<Tool
|
|
61
|
+
activeLevelId={activeLevelId}
|
|
62
|
+
finishTool={finishTool}
|
|
63
|
+
gridSnapStep={gridSnapStep}
|
|
64
|
+
metricNotation={metricNotation}
|
|
65
|
+
sceneApi={sceneApi}
|
|
66
|
+
selectNode={selectNode}
|
|
67
|
+
toolDefaults={toolDefaults}
|
|
68
|
+
unit={unit}
|
|
69
|
+
/>
|
|
70
|
+
</Suspense>
|
|
71
|
+
) : null
|
|
72
|
+
}
|
|
@@ -15,16 +15,27 @@ import {
|
|
|
15
15
|
type WallNode,
|
|
16
16
|
} from '@pascal-app/core'
|
|
17
17
|
import { useViewer } from '@pascal-app/viewer'
|
|
18
|
-
import { useEffect, useState } from 'react'
|
|
18
|
+
import { type MouseEvent, useEffect, useState } from 'react'
|
|
19
19
|
import { createPortal } from 'react-dom'
|
|
20
20
|
import { useShallow } from 'zustand/react/shallow'
|
|
21
|
+
import { useReducedMotion } from '../../hooks/use-reduced-motion'
|
|
22
|
+
import { resolveMoveActionNode } from '../../lib/direct-manipulation'
|
|
23
|
+
import { getFloorplanNodeExtension } from '../../lib/floorplan/floorplan-extension'
|
|
21
24
|
import {
|
|
22
25
|
createFreshPlacementSubtree,
|
|
23
26
|
duplicatesAsFreshSubtree,
|
|
27
|
+
prepareFreshPlacementRootDuplicate,
|
|
24
28
|
} from '../../lib/fresh-planar-placement'
|
|
29
|
+
import { curveReshapeScope } from '../../lib/interaction/scope'
|
|
30
|
+
import { playBlockedQuickActionFeedback } from '../../lib/quick-action-feedback'
|
|
31
|
+
import { collectQuickActionNodeScope } from '../../lib/quick-action-nodes'
|
|
25
32
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
33
|
+
import { cn } from '../../lib/utils'
|
|
26
34
|
import useEditor from '../../store/use-editor'
|
|
27
|
-
import {
|
|
35
|
+
import useInteractionScope, {
|
|
36
|
+
useIsCurveReshape,
|
|
37
|
+
useMovingNode,
|
|
38
|
+
} from '../../store/use-interaction-scope'
|
|
28
39
|
import { NodeActionMenu } from '../editor/node-action-menu'
|
|
29
40
|
import { IconRefGlyph } from '../ui/icon-ref'
|
|
30
41
|
|
|
@@ -80,26 +91,9 @@ function collectQuickActionNodes(
|
|
|
80
91
|
): Record<AnyNodeId, AnyNode> | null {
|
|
81
92
|
if (!selectedId) return null
|
|
82
93
|
const selected = nodes[selectedId as AnyNodeId]
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const add = (id: string | null | undefined) => {
|
|
87
|
-
if (!id) return
|
|
88
|
-
const node = nodes[id as AnyNodeId]
|
|
89
|
-
if (node) collected[node.id as AnyNodeId] = node
|
|
90
|
-
}
|
|
91
|
-
const addChildren = (node: AnyNode | undefined) => {
|
|
92
|
-
for (const childId of (node as { children?: readonly string[] } | undefined)?.children ?? []) {
|
|
93
|
-
add(childId)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
add(selected.parentId ?? null)
|
|
98
|
-
addChildren(selected)
|
|
99
|
-
const parent = selected.parentId ? nodes[selected.parentId as AnyNodeId] : undefined
|
|
100
|
-
addChildren(parent)
|
|
101
|
-
|
|
102
|
-
return collected
|
|
94
|
+
const def = selected ? nodeRegistry.get(selected.type) : undefined
|
|
95
|
+
if (!def?.quickActions) return null
|
|
96
|
+
return collectQuickActionNodeScope(nodes, selectedId, def.quickActionNodeScope)
|
|
103
97
|
}
|
|
104
98
|
|
|
105
99
|
/**
|
|
@@ -118,23 +112,27 @@ function collectQuickActionNodes(
|
|
|
118
112
|
* `<FloorplanRegistryMoveOverlay>` / dispatcher picks the right path.
|
|
119
113
|
* Walls are excluded — their move is reached via the side-arrow
|
|
120
114
|
* handles emitted from `def.floorplan`, not via a menu button.
|
|
115
|
+
* - Curve (wall only): enters curve reshape mode. The selected wall's
|
|
116
|
+
* midpoint curve handle remains visible so it can be dragged in plan.
|
|
121
117
|
* - Add hole (slab + ceiling only): inserts a small default-square
|
|
122
118
|
* hole at the polygon centroid via `updateNode`. Mirrors the legacy
|
|
123
119
|
* `handleAddHole` in `floating-action-menu.tsx`.
|
|
124
|
-
* - Duplicate:
|
|
125
|
-
*
|
|
120
|
+
* - Duplicate: creates a fresh subtree when the kind opts in, otherwise a
|
|
121
|
+
* root-only copy, then hands that real draft to the placement cursor.
|
|
126
122
|
* - Delete: calls `deleteNode(id)`. Cascade is handled by the registry's
|
|
127
123
|
* `relations.cascadeDelete` if declared on the def.
|
|
128
124
|
*
|
|
129
|
-
* Hidden while
|
|
125
|
+
* Hidden while moving or curving so the menu does not compete with the active affordance.
|
|
130
126
|
*/
|
|
131
127
|
export function FloorplanRegistryActionMenu() {
|
|
128
|
+
const reducedMotion = useReducedMotion()
|
|
132
129
|
// Sole selection only — a multi-selection gets the group menu
|
|
133
130
|
// (`FloorplanGroupActionMenu`), whose actions target the whole selection.
|
|
134
131
|
const selectedId = useViewer((s) =>
|
|
135
132
|
s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : undefined,
|
|
136
133
|
) as AnyNodeId | undefined
|
|
137
134
|
const movingNode = useMovingNode()
|
|
135
|
+
const isCurveReshape = useIsCurveReshape()
|
|
138
136
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
139
137
|
const setMovingNodeOrigin = useEditor((s) => s.setMovingNodeOrigin)
|
|
140
138
|
// Gate on floorplan hover so this 2D menu never coexists with the 3D
|
|
@@ -150,10 +148,28 @@ export function FloorplanRegistryActionMenu() {
|
|
|
150
148
|
// Only show for registered kinds (skip legacy kinds — they have their
|
|
151
149
|
// own FloorplanActionMenuLayer entries).
|
|
152
150
|
const selectedKind = useScene((s) => (selectedId ? (s.nodes[selectedId]?.type ?? null) : null))
|
|
151
|
+
const canCurve = useScene((s) => {
|
|
152
|
+
if (!selectedId) return false
|
|
153
|
+
const selectedNode = s.nodes[selectedId]
|
|
154
|
+
if (!selectedNode) return false
|
|
155
|
+
const definition = nodeRegistry.get(selectedNode.type)
|
|
156
|
+
const canCurveNode = getFloorplanNodeExtension(definition)?.actionMenu?.canCurve
|
|
157
|
+
return (
|
|
158
|
+
!!definition?.floorplanAffordances?.curve &&
|
|
159
|
+
!!canCurveNode?.({
|
|
160
|
+
node: selectedNode as never,
|
|
161
|
+
nodes: s.nodes,
|
|
162
|
+
})
|
|
163
|
+
)
|
|
164
|
+
})
|
|
153
165
|
const def = selectedKind ? nodeRegistry.get(selectedKind) : null
|
|
154
166
|
const isRegistryKind = !!def
|
|
155
167
|
const isVisible =
|
|
156
|
-
isRegistryKind &&
|
|
168
|
+
isRegistryKind &&
|
|
169
|
+
def?.presentation?.actionMenu !== false &&
|
|
170
|
+
!movingNode &&
|
|
171
|
+
!isCurveReshape &&
|
|
172
|
+
isFloorplanHovered
|
|
157
173
|
const isWall = selectedKind === 'wall'
|
|
158
174
|
const quickActionNodes = useScene(
|
|
159
175
|
useShallow((s) => collectQuickActionNodes(s.nodes, selectedId ?? null)),
|
|
@@ -241,7 +257,8 @@ export function FloorplanRegistryActionMenu() {
|
|
|
241
257
|
|
|
242
258
|
const handleMove = () => {
|
|
243
259
|
sfxEmitter.emit('sfx:item-pick')
|
|
244
|
-
|
|
260
|
+
const sceneNodes = useScene.getState().nodes
|
|
261
|
+
setMovingNode(resolveMoveActionNode(node, sceneNodes) as never)
|
|
245
262
|
// 2D-owned move: `FloorplanRegistryMoveOverlay` runs the whole gesture.
|
|
246
263
|
// Mark the origin (after `setMovingNode`, which resets it to null) so
|
|
247
264
|
// `ToolManager` keeps the 3D affordance mover from also adopting the node
|
|
@@ -294,38 +311,40 @@ export function FloorplanRegistryActionMenu() {
|
|
|
294
311
|
)
|
|
295
312
|
}
|
|
296
313
|
|
|
314
|
+
const handleCurve = () => {
|
|
315
|
+
if (!canCurve) return
|
|
316
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
317
|
+
useInteractionScope.getState().begin(curveReshapeScope(node.id))
|
|
318
|
+
}
|
|
319
|
+
|
|
297
320
|
const handleDuplicate = () => {
|
|
298
321
|
if (!node.parentId) return
|
|
299
322
|
sfxEmitter.emit('sfx:item-pick')
|
|
300
323
|
useScene.temporal.getState().pause()
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
324
|
+
let draftId: AnyNodeId | null = null
|
|
325
|
+
try {
|
|
326
|
+
if (duplicatesAsFreshSubtree(node as AnyNode)) {
|
|
327
|
+
draftId = createFreshPlacementSubtree(node.id as AnyNodeId)
|
|
328
|
+
const draft = draftId ? useScene.getState().nodes[draftId] : null
|
|
329
|
+
if (!draft) return
|
|
305
330
|
setMovingNode(draft as never)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
331
|
+
} else {
|
|
332
|
+
const cloned = prepareFreshPlacementRootDuplicate(node as AnyNode)
|
|
333
|
+
const parsed = def.schema.parse(cloned) as AnyNode
|
|
334
|
+
draftId = parsed.id as AnyNodeId
|
|
335
|
+
useScene.getState().createNode(parsed, node.parentId as AnyNodeId)
|
|
336
|
+
setMovingNode(parsed as never)
|
|
337
|
+
}
|
|
338
|
+
setMovingNodeOrigin('2d')
|
|
339
|
+
useViewer.getState().setSelection({ selectedIds: [] })
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (draftId && useScene.getState().nodes[draftId]) {
|
|
342
|
+
useScene.getState().deleteNode(draftId)
|
|
309
343
|
}
|
|
344
|
+
console.error('Failed to duplicate node', error)
|
|
345
|
+
} finally {
|
|
310
346
|
useScene.temporal.getState().resume()
|
|
311
|
-
return
|
|
312
347
|
}
|
|
313
|
-
const cloned = structuredClone(node) as AnyNode & { id?: AnyNodeId }
|
|
314
|
-
delete (cloned as { id?: AnyNodeId }).id
|
|
315
|
-
const prevMeta =
|
|
316
|
-
cloned.metadata && typeof cloned.metadata === 'object' && !Array.isArray(cloned.metadata)
|
|
317
|
-
? (cloned.metadata as Record<string, unknown>)
|
|
318
|
-
: {}
|
|
319
|
-
// Mark fresh + hand to the placement cursor so the copy follows the
|
|
320
|
-
// pointer and only lands on the next click — same gesture for every
|
|
321
|
-
// kind. Polyline runs (duct / pipe / lineset) ride the same path:
|
|
322
|
-
// `FloorplanRegistryMoveOverlay` translates their whole `path`, so they
|
|
323
|
-
// no longer need the old "offset + drop already-placed" special case.
|
|
324
|
-
cloned.metadata = { ...prevMeta, isNew: true }
|
|
325
|
-
const parsed = def.schema.parse(cloned) as AnyNode
|
|
326
|
-
useScene.getState().createNode(parsed, node.parentId as AnyNodeId)
|
|
327
|
-
setMovingNode(parsed as never)
|
|
328
|
-
useScene.temporal.getState().resume()
|
|
329
348
|
}
|
|
330
349
|
|
|
331
350
|
const handleDelete = () => {
|
|
@@ -334,8 +353,13 @@ export function FloorplanRegistryActionMenu() {
|
|
|
334
353
|
useViewer.getState().setSelection({ selectedIds: [] })
|
|
335
354
|
}
|
|
336
355
|
|
|
337
|
-
const handleQuickAction = (action: NodeQuickAction) => {
|
|
338
|
-
if (action.disabled)
|
|
356
|
+
const handleQuickAction = (action: NodeQuickAction, event: MouseEvent<HTMLButtonElement>) => {
|
|
357
|
+
if (action.disabled) {
|
|
358
|
+
if (action.blockedFeedback) {
|
|
359
|
+
playBlockedQuickActionFeedback(event.currentTarget, reducedMotion)
|
|
360
|
+
}
|
|
361
|
+
return
|
|
362
|
+
}
|
|
339
363
|
const run = () => action.run({ node, sceneApi: createSceneApi(useScene) })
|
|
340
364
|
const result = action.history === 'single' ? runAsSingleSceneHistoryStep(useScene, run) : run()
|
|
341
365
|
if (result?.selectedIds) useViewer.getState().setSelection({ selectedIds: result.selectedIds })
|
|
@@ -356,6 +380,7 @@ export function FloorplanRegistryActionMenu() {
|
|
|
356
380
|
>
|
|
357
381
|
<NodeActionMenu
|
|
358
382
|
onAddHole={canAddHole ? handleAddHole : undefined}
|
|
383
|
+
onCurve={canCurve ? handleCurve : undefined}
|
|
359
384
|
onDelete={canDelete ? handleDelete : undefined}
|
|
360
385
|
onDuplicate={canDuplicate ? handleDuplicate : undefined}
|
|
361
386
|
onMove={canMove ? handleMove : undefined}
|
|
@@ -370,18 +395,27 @@ export function FloorplanRegistryActionMenu() {
|
|
|
370
395
|
>
|
|
371
396
|
{quickActions.map((action) => (
|
|
372
397
|
<button
|
|
398
|
+
aria-disabled={action.disabled || undefined}
|
|
373
399
|
aria-label={action.title ?? action.label}
|
|
374
|
-
className=
|
|
375
|
-
|
|
400
|
+
className={cn(
|
|
401
|
+
'tooltip-trigger flex items-center rounded-md px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground',
|
|
402
|
+
action.disabled &&
|
|
403
|
+
'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-muted-foreground',
|
|
404
|
+
)}
|
|
405
|
+
disabled={action.disabled && !action.blockedFeedback}
|
|
376
406
|
key={action.id}
|
|
377
|
-
onClick={() => handleQuickAction(action)}
|
|
407
|
+
onClick={(event) => handleQuickAction(action, event)}
|
|
378
408
|
title={action.title ?? action.label}
|
|
379
409
|
type="button"
|
|
380
410
|
>
|
|
381
|
-
<span className="flex
|
|
382
|
-
<
|
|
411
|
+
<span className="flex items-center gap-1.5" data-quick-action-feedback>
|
|
412
|
+
<span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center text-current">
|
|
413
|
+
<QuickActionIcon action={action} />
|
|
414
|
+
</span>
|
|
415
|
+
<span className="whitespace-nowrap leading-none" data-quick-action-label>
|
|
416
|
+
{action.label}
|
|
417
|
+
</span>
|
|
383
418
|
</span>
|
|
384
|
-
<span className="whitespace-nowrap leading-none">{action.label}</span>
|
|
385
419
|
</button>
|
|
386
420
|
))}
|
|
387
421
|
</div>
|
|
@@ -197,6 +197,10 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
197
197
|
|
|
198
198
|
const commitFinalStateOrRevert = () => {
|
|
199
199
|
const commitValid = session.canCommit()
|
|
200
|
+
const freshPlacement = isFreshPlacementMetadata(
|
|
201
|
+
(useScene.getState().nodes[movingNode.id] as { metadata?: unknown } | undefined)
|
|
202
|
+
?.metadata,
|
|
203
|
+
)
|
|
200
204
|
|
|
201
205
|
// Claim ownership of the drag teardown so the 3D move tool's
|
|
202
206
|
// unmount-time cleanup skips its restore-from-snapshot — see
|
|
@@ -210,6 +214,29 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
210
214
|
// planner). For those we still do Phase 1 (revert to baseline)
|
|
211
215
|
// and Phase 2's resume — but Phase 2's write is delegated, and
|
|
212
216
|
// we skip the snapshot-diff finalUpdates path.
|
|
217
|
+
if (commitValid && freshPlacement) {
|
|
218
|
+
session.commit?.()
|
|
219
|
+
const stagedNode = useScene.getState().nodes[movingNode.id]
|
|
220
|
+
const committedId = stagedNode
|
|
221
|
+
? commitFreshPlacementSubtree(
|
|
222
|
+
movingNode.id as AnyNodeId,
|
|
223
|
+
{
|
|
224
|
+
metadata: stripPlacementMetadataFlags(stagedNode.metadata),
|
|
225
|
+
visible: true,
|
|
226
|
+
} as Partial<AnyNode>,
|
|
227
|
+
)
|
|
228
|
+
: null
|
|
229
|
+
if (historyPaused) {
|
|
230
|
+
resumeSceneHistory(useScene)
|
|
231
|
+
historyPaused = false
|
|
232
|
+
}
|
|
233
|
+
if (committedId) {
|
|
234
|
+
sfxEmitter.emit('sfx:item-place')
|
|
235
|
+
useViewer.getState().setSelection({ selectedIds: [committedId] })
|
|
236
|
+
}
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
213
240
|
if (commitValid && session.commit) {
|
|
214
241
|
useScene.getState().updateNodes(snapshotsToUpdates(snapshots))
|
|
215
242
|
if (historyPaused) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { createFloorplanRenderScaleReference } from './floorplan-render-context'
|
|
3
|
+
|
|
4
|
+
describe('floorplan render context', () => {
|
|
5
|
+
test('updates the live scale without changing the registry-facing reader', () => {
|
|
6
|
+
const scale = createFloorplanRenderScaleReference(0.02)
|
|
7
|
+
const read = scale.read
|
|
8
|
+
|
|
9
|
+
scale.update(0.01)
|
|
10
|
+
|
|
11
|
+
expect(scale.read).toBe(read)
|
|
12
|
+
expect(read()).toBe(0.01)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('notifies selected-handle renderers when a hidden floorplan becomes visible', () => {
|
|
16
|
+
const scale = createFloorplanRenderScaleReference(30)
|
|
17
|
+
let renderedCurveHandleRadius = 8 * scale.read()
|
|
18
|
+
const unsubscribe = scale.subscribe(() => {
|
|
19
|
+
renderedCurveHandleRadius = 8 * scale.read()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
scale.update(0.02)
|
|
23
|
+
|
|
24
|
+
expect(renderedCurveHandleRadius).toBe(0.16)
|
|
25
|
+
unsubscribe()
|
|
26
|
+
})
|
|
27
|
+
})
|