@pascal-app/editor 1.0.0-beta.5 → 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 +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- 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 +9 -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 +180 -12
- package/src/components/editor/floating-action-menu.tsx +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- 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 +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- 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 +284 -237
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- 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 +180 -32
- 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 +331 -4
- package/src/components/tools/item/placement-strategies.ts +135 -2
- 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 +428 -153
- 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 +57 -7
- 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-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 +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- 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 +9 -12
- 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 +22 -5
- 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 +48 -37
- 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/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/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -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 +116 -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 +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer-overlay.tsx +1 -22
- 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 +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- 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 +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -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/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.ts +1 -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 +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -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/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- 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 +105 -1
- package/src/lib/selection-routing.ts +19 -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/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/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -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
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
2
2
|
import type {
|
|
3
3
|
AnyNode,
|
|
4
4
|
AnyNodeId,
|
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
floorplanGeometryMetadata,
|
|
17
17
|
} from '../../../lib/floorplan/floorplan-extension'
|
|
18
18
|
import {
|
|
19
|
+
buildFloorplanEntryGeometry,
|
|
19
20
|
cancelFloorplanAffordanceDrag,
|
|
21
|
+
collectDirectFloorplanScopeNodes,
|
|
20
22
|
collectFloorplanDependencyNodes,
|
|
21
23
|
collectFloorplanLinkedLevelNodes,
|
|
22
24
|
computeAffectedSiblingIds,
|
|
@@ -25,10 +27,185 @@ import {
|
|
|
25
27
|
InteractiveGeometry,
|
|
26
28
|
isFloorplanOpeningPlacementState,
|
|
27
29
|
resolveFloorplanHandleUnitsPerPixel,
|
|
30
|
+
siteToFloorplanTransform,
|
|
28
31
|
splitFloorplanOverlay,
|
|
29
32
|
subscribeFloorplanAffordanceToolCancel,
|
|
30
33
|
} from './floorplan-registry-layer'
|
|
31
34
|
|
|
35
|
+
describe('site-scoped floorplan discovery', () => {
|
|
36
|
+
let restoreRegistry: () => void
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
restoreRegistry = nodeRegistry._snapshot()
|
|
40
|
+
nodeRegistry._reset()
|
|
41
|
+
registerNode({
|
|
42
|
+
kind: 'test:site-overlay',
|
|
43
|
+
schemaVersion: 1,
|
|
44
|
+
schema: z.object({ type: z.literal('test:site-overlay') }) as never,
|
|
45
|
+
category: 'utility',
|
|
46
|
+
defaults: () => ({}) as never,
|
|
47
|
+
capabilities: {},
|
|
48
|
+
floorplanScope: 'site',
|
|
49
|
+
floorplan: (node) => {
|
|
50
|
+
const positioned = node as unknown as {
|
|
51
|
+
position?: [number, number, number]
|
|
52
|
+
rotation?: [number, number, number]
|
|
53
|
+
}
|
|
54
|
+
if (!(positioned.position && positioned.rotation)) return null
|
|
55
|
+
return {
|
|
56
|
+
kind: 'group',
|
|
57
|
+
children: [{ kind: 'circle', cx: 0, cy: 0, r: 1 }],
|
|
58
|
+
transform: {
|
|
59
|
+
translate: [positioned.position[0], positioned.position[2]],
|
|
60
|
+
rotate: positioned.rotation[1],
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
} as AnyNodeDefinition)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
afterEach(() => restoreRegistry())
|
|
68
|
+
|
|
69
|
+
test('collects only direct children of the active Site', () => {
|
|
70
|
+
const activeSite = {
|
|
71
|
+
id: 'site_active',
|
|
72
|
+
type: 'site',
|
|
73
|
+
parentId: null,
|
|
74
|
+
children: ['overlay_declared'],
|
|
75
|
+
} as unknown as AnyNode
|
|
76
|
+
const nodes = {
|
|
77
|
+
[activeSite.id]: activeSite,
|
|
78
|
+
overlay_declared: {
|
|
79
|
+
id: 'overlay_declared',
|
|
80
|
+
type: 'test:site-overlay',
|
|
81
|
+
parentId: null,
|
|
82
|
+
} as unknown as AnyNode,
|
|
83
|
+
overlay_parented: {
|
|
84
|
+
id: 'overlay_parented',
|
|
85
|
+
type: 'test:site-overlay',
|
|
86
|
+
parentId: activeSite.id,
|
|
87
|
+
} as unknown as AnyNode,
|
|
88
|
+
overlay_other_site: {
|
|
89
|
+
id: 'overlay_other_site',
|
|
90
|
+
type: 'test:site-overlay',
|
|
91
|
+
parentId: 'site_other',
|
|
92
|
+
} as unknown as AnyNode,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
expect(
|
|
96
|
+
collectDirectFloorplanScopeNodes(nodes, activeSite, 'site').map((node) => String(node.id)),
|
|
97
|
+
).toEqual(['overlay_declared', 'overlay_parented'])
|
|
98
|
+
})
|
|
99
|
+
test('projects site-local coordinates through the inverse Three.js building transform', () => {
|
|
100
|
+
const transform = siteToFloorplanTransform([10, 0, 5], Math.PI / 2)
|
|
101
|
+
const [tx, ty] = transform.translate
|
|
102
|
+
const sitePoint = [10, 3] as const
|
|
103
|
+
const cos = Math.cos(transform.rotate)
|
|
104
|
+
const sin = Math.sin(transform.rotate)
|
|
105
|
+
|
|
106
|
+
expect(tx + sitePoint[0] * cos - sitePoint[1] * sin).toBeCloseTo(2)
|
|
107
|
+
|
|
108
|
+
expect(ty + sitePoint[0] * sin + sitePoint[1] * cos).toBeCloseTo(0)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('rebuilds site geometry from a live pose and restores the committed pose when cleared', () => {
|
|
112
|
+
const site = {
|
|
113
|
+
id: 'site_active',
|
|
114
|
+
type: 'site',
|
|
115
|
+
parentId: null,
|
|
116
|
+
children: ['overlay_pond'],
|
|
117
|
+
visible: true,
|
|
118
|
+
} as unknown as AnyNode
|
|
119
|
+
const overlay = {
|
|
120
|
+
id: 'overlay_pond',
|
|
121
|
+
type: 'test:site-overlay',
|
|
122
|
+
parentId: site.id,
|
|
123
|
+
children: [],
|
|
124
|
+
visible: true,
|
|
125
|
+
position: [1, 0, 2],
|
|
126
|
+
rotation: [0, 0.25, 0],
|
|
127
|
+
} as unknown as AnyNode
|
|
128
|
+
const nodes = {
|
|
129
|
+
[site.id]: site,
|
|
130
|
+
[overlay.id]: overlay,
|
|
131
|
+
}
|
|
132
|
+
const geometryCache = new Map()
|
|
133
|
+
const liveOverrides = new Map<string, LiveNodeOverrides>()
|
|
134
|
+
const siteProjection = siteToFloorplanTransform([10, 0, 5], Math.PI / 2)
|
|
135
|
+
const common = {
|
|
136
|
+
automaticDimensions: false,
|
|
137
|
+
ctxOverrides: {
|
|
138
|
+
children: [],
|
|
139
|
+
siblings: [],
|
|
140
|
+
parent: site,
|
|
141
|
+
outputTransform: siteProjection,
|
|
142
|
+
trackAllNodes: true,
|
|
143
|
+
},
|
|
144
|
+
geometryCache,
|
|
145
|
+
highlighted: false,
|
|
146
|
+
hovered: false,
|
|
147
|
+
interactiveElevators: {},
|
|
148
|
+
levelDataCache: new Map(),
|
|
149
|
+
levelNodeIdsByType: new Map(),
|
|
150
|
+
liveOverride: undefined,
|
|
151
|
+
liveOverrides,
|
|
152
|
+
moving: true,
|
|
153
|
+
node: overlay,
|
|
154
|
+
nodeId: overlay.id,
|
|
155
|
+
nodes,
|
|
156
|
+
palette: undefined,
|
|
157
|
+
selected: false,
|
|
158
|
+
siblingEpoch: 0,
|
|
159
|
+
unit: 'metric' as const,
|
|
160
|
+
metricNotation: 'meters' as const,
|
|
161
|
+
wallDimensionReference: 'finished-faces' as const,
|
|
162
|
+
visibilityRootId: site.id,
|
|
163
|
+
}
|
|
164
|
+
const committedSnapshot = structuredClone(overlay)
|
|
165
|
+
const livePose = {
|
|
166
|
+
position: [4, 0, 5] as [number, number, number],
|
|
167
|
+
rotation: Math.PI / 3,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const liveEntry = buildFloorplanEntryGeometry({ ...common, live: livePose })
|
|
171
|
+
|
|
172
|
+
expect(liveEntry?.node).toMatchObject({
|
|
173
|
+
position: livePose.position,
|
|
174
|
+
rotation: [0, livePose.rotation, 0],
|
|
175
|
+
parentId: null,
|
|
176
|
+
})
|
|
177
|
+
expect(liveEntry?.base).toEqual({
|
|
178
|
+
kind: 'group',
|
|
179
|
+
children: [
|
|
180
|
+
{
|
|
181
|
+
kind: 'group',
|
|
182
|
+
children: [{ kind: 'circle', cx: 0, cy: 0, r: 1 }],
|
|
183
|
+
transform: { translate: [4, 5], rotate: livePose.rotation },
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
transform: siteProjection,
|
|
187
|
+
})
|
|
188
|
+
expect(overlay).toEqual(committedSnapshot)
|
|
189
|
+
|
|
190
|
+
const committedEntry = buildFloorplanEntryGeometry({ ...common, live: undefined })
|
|
191
|
+
|
|
192
|
+
expect(committedEntry).not.toBe(liveEntry)
|
|
193
|
+
expect(committedEntry?.node).toBe(overlay)
|
|
194
|
+
expect(committedEntry?.base).toEqual({
|
|
195
|
+
kind: 'group',
|
|
196
|
+
children: [
|
|
197
|
+
{
|
|
198
|
+
kind: 'group',
|
|
199
|
+
children: [{ kind: 'circle', cx: 0, cy: 0, r: 1 }],
|
|
200
|
+
transform: { translate: [1, 2], rotate: 0.25 },
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
transform: siteProjection,
|
|
204
|
+
})
|
|
205
|
+
expect(overlay).toEqual(committedSnapshot)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
32
209
|
describe('floorplan selection handle sizing', () => {
|
|
33
210
|
test('caps visual handle growth at extreme zoom-out', () => {
|
|
34
211
|
expect(resolveFloorplanHandleUnitsPerPixel(0.01)).toBe(0.01)
|
|
@@ -447,12 +624,20 @@ describe('floorplan annotation overlay routing', () => {
|
|
|
447
624
|
})
|
|
448
625
|
|
|
449
626
|
describe('computeAffectedSiblingIds', () => {
|
|
627
|
+
// The cabinet fixture definitions have no `capabilities` — leaking them
|
|
628
|
+
// past this describe crashes any later test FILE that enumerates the
|
|
629
|
+
// registry (night-8 CI: pointer-support-cap.test.ts, run 32580694134).
|
|
630
|
+
let restoreRegistry: () => void
|
|
631
|
+
|
|
450
632
|
beforeEach(() => {
|
|
633
|
+
restoreRegistry = nodeRegistry._snapshot()
|
|
451
634
|
nodeRegistry._reset()
|
|
452
635
|
registerCabinetFloorplanDefinition('cabinet')
|
|
453
636
|
registerCabinetFloorplanDefinition('cabinet-module')
|
|
454
637
|
})
|
|
455
638
|
|
|
639
|
+
afterEach(() => restoreRegistry())
|
|
640
|
+
|
|
456
641
|
test('propagates cabinet live overrides through the cabinet family', () => {
|
|
457
642
|
const run = cabinetRun('cabinet_run', ['cabinet-module_main', 'cabinet-module_corner'])
|
|
458
643
|
const module = cabinetModule('cabinet-module_main', run.id)
|
|
@@ -545,6 +730,16 @@ describe('collectFloorplanDependencyNodes', () => {
|
|
|
545
730
|
})
|
|
546
731
|
|
|
547
732
|
describe('collectFloorplanLinkedLevelNodes', () => {
|
|
733
|
+
// Same containment as computeAffectedSiblingIds above: the fixture
|
|
734
|
+
// definition has no `capabilities`, so it must not outlive this describe.
|
|
735
|
+
let restoreRegistry: () => void
|
|
736
|
+
|
|
737
|
+
beforeEach(() => {
|
|
738
|
+
restoreRegistry = nodeRegistry._snapshot()
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
afterEach(() => restoreRegistry())
|
|
742
|
+
|
|
548
743
|
test('projects a node onto a linked destination level with its real children', () => {
|
|
549
744
|
nodeRegistry._reset()
|
|
550
745
|
registerNode({
|