@pascal-app/editor 1.0.4 → 1.0.6
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/README.md +106 -0
- package/package.json +40 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/bake-thumbnail.tsx +6 -15
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +81 -8
- package/src/components/editor/editor-layout-mobile.tsx +1 -1
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/export-manager.tsx +14 -19
- package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
- package/src/components/editor/first-person/build-collider-world.ts +22 -5
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
- package/src/components/editor/first-person/mechanism-targets.ts +52 -0
- package/src/components/editor/first-person-controls.tsx +19 -11
- package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +72 -23
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/group-actions.ts +2 -0
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- package/src/components/editor/index.tsx +166 -79
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- package/src/components/editor/room-highlight.tsx +13 -5
- package/src/components/editor/room-labels.test.ts +621 -128
- package/src/components/editor/room-labels.tsx +1090 -190
- package/src/components/editor/selection-manager.tsx +38 -117
- package/src/components/editor/slab-hole-highlights.tsx +2 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
- package/src/components/editor/snapshot-capture.test.ts +69 -58
- package/src/components/editor/snapshot-capture.ts +7 -1
- package/src/components/editor/thumbnail-generator.test.ts +350 -4
- package/src/components/editor/thumbnail-generator.tsx +128 -54
- package/src/components/editor/use-floorplan-background-placement.ts +3 -1
- package/src/components/editor/views/plugins-view.tsx +1 -8
- package/src/components/editor/views/view-bar.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +140 -17
- package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- package/src/components/systems/roof/roof-edit-system.tsx +1 -0
- package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +38 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +3 -6
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +94 -94
- package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
- package/src/components/tools/registry/item-surface-move.test.ts +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
- package/src/components/tools/tool-manager.tsx +14 -5
- package/src/components/tools/wall/wall-drafting.test.ts +170 -57
- package/src/components/ui/action-menu/action-button.tsx +6 -1
- package/src/components/ui/action-menu/camera-actions.tsx +7 -3
- package/src/components/ui/action-menu/control-modes.tsx +13 -7
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +11 -9
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +10 -3
- package/src/components/ui/command-palette/editor-commands.tsx +3 -12
- package/src/components/ui/command-palette/index.tsx +45 -15
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +150 -45
- package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
- package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
- package/src/components/ui/helpers/helper-manager.tsx +7 -1
- package/src/components/ui/helpers/item-helper.tsx +9 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
- package/src/components/ui/panels/panel-wrapper.tsx +53 -27
- package/src/components/ui/panels/parametric-field-control.tsx +2 -2
- package/src/components/ui/panels/parametric-inspector.tsx +3 -3
- package/src/components/ui/panels/room-panel.tsx +65 -0
- package/src/components/ui/primitives/badge.tsx +8 -0
- package/src/components/ui/primitives/sidebar.tsx +1 -1
- package/src/components/ui/right-stack.tsx +21 -2
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +3 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/tab-bar.tsx +4 -4
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-rooms.tsx +16 -0
- package/src/hooks/use-auto-save.integration.test.tsx +96 -38
- package/src/hooks/use-auto-save.ts +26 -1
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +65 -16
- package/src/hooks/use-keyboard.ts +29 -12
- package/src/hooks/use-placement-notice.test.ts +24 -1
- package/src/hooks/use-placement-notice.ts +37 -6
- package/src/index.tsx +71 -24
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -0
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.ts +9 -2
- package/src/lib/editor-views.ts +35 -1
- package/src/lib/export-texture-utils.ts +2 -4
- package/src/lib/floor-foundation-support.test.ts +1 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
- package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
- package/src/lib/floorplan/sheet-export.test.ts +24 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
- package/src/lib/floorplan/site-plan/coverage.ts +2 -4
- package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
- package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
- package/src/lib/floorplan/site-plan/geometry.ts +1 -416
- package/src/lib/floorplan/site-plan/index.ts +0 -16
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
- package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
- package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +112 -0
- package/src/lib/fresh-planar-placement.ts +57 -3
- package/src/lib/geometry-script/author.ts +6 -1
- package/src/lib/geometry-script/client.ts +8 -1
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +1 -0
- package/src/lib/glb-export.test.ts +187 -162
- package/src/lib/glb-export.ts +4 -1
- package/src/lib/history.test.ts +12 -7
- package/src/lib/interaction/scope.ts +4 -0
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- package/src/lib/lot/clean-ring.test.ts +2 -1
- package/src/lib/lot/drop-in.ts +9 -24
- package/src/lib/lot/lot-patch.test.ts +1 -1
- package/src/lib/lot/lot-patch.ts +2 -2
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -0
- package/src/lib/orbit-floor-clearance.test.ts +36 -0
- package/src/lib/orbit-floor-clearance.ts +48 -0
- package/src/lib/paint-pass-through.test.ts +11 -0
- package/src/lib/paint-pass-through.ts +4 -2
- package/src/lib/paint-scope.ts +1 -0
- package/src/lib/parametric-node-update.ts +16 -7
- package/src/lib/plugin-chat.ts +15 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- package/src/lib/room-assembly-overlay.ts +2 -3
- package/src/lib/room-selection.test.ts +45 -1
- package/src/lib/scene-clipboard.test.ts +246 -1
- package/src/lib/scene-clipboard.ts +44 -14
- package/src/lib/selection-routing.ts +1 -1
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -0
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.ts +2 -1
- package/src/lib/viewer-selection.ts +22 -11
- package/src/lib/wall-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -0
- package/src/lib/window-changes.ts +35 -0
- package/src/store/use-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- package/src/store/use-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-stair-build-preview.ts +25 -5
- package/src/store/view-layout.test.ts +27 -1
- package/src/store/view-layout.ts +4 -1
- package/src/test-utils/render-current-markup.tsx +16 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -15
- package/src/components/tools/stair/stair-tool.tsx +0 -682
|
@@ -443,67 +443,66 @@ describe('registry item-surface move session', () => {
|
|
|
443
443
|
expect((useScene.getState().nodes[host.id] as ItemNode).children as string[]).toEqual([])
|
|
444
444
|
})
|
|
445
445
|
|
|
446
|
-
test.each([
|
|
447
|
-
'
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
.
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
})
|
|
446
|
+
test.each(['floor', 'same host', 'floor then host'])(
|
|
447
|
+
'a hosted move to %s commits once and undo restores the original parent and pose',
|
|
448
|
+
(destination) => {
|
|
449
|
+
const initial = createRegistryItemSurfaceMove(original)!
|
|
450
|
+
const hostedPose = initial.enter(eventFor().event, dimensions, 0.3)!
|
|
451
|
+
const hosted = useScene.getState().nodes[original.id]!
|
|
452
|
+
const session = createRegistryItemSurfaceMove(hosted)!
|
|
453
|
+
let pose: { position: [number, number, number]; rotationY: number } = session.enter(
|
|
454
|
+
eventFor().event,
|
|
455
|
+
dimensions,
|
|
456
|
+
hostedPose.rotationY,
|
|
457
|
+
)!
|
|
458
|
+
if (destination !== 'same host') {
|
|
459
|
+
const world = sceneRegistry.nodes
|
|
460
|
+
.get('level_surface-move')!
|
|
461
|
+
.localToWorld(new Vector3(8, 0, 9))
|
|
462
|
+
pose = session.detach(world.toArray(), pose.rotationY)!
|
|
463
|
+
const detached = useScene.getState().nodes[original.id]!
|
|
464
|
+
expect(
|
|
465
|
+
getFloorPlacedElevation({
|
|
466
|
+
node: detached,
|
|
467
|
+
nodes: useScene.getState().nodes,
|
|
468
|
+
position: pose.position,
|
|
469
|
+
maxElevation: 0,
|
|
470
|
+
}),
|
|
471
|
+
).toBe(0)
|
|
472
|
+
const footprints = getFloorPlacedFootprints(
|
|
473
|
+
nodeRegistry.get(original.type)!.capabilities.floorPlaced!,
|
|
474
|
+
detached,
|
|
475
|
+
{ nodes: useScene.getState().nodes },
|
|
476
|
+
).map((footprint) => ({ ...footprint, position: footprint.position ?? pose.position }))
|
|
477
|
+
expect(
|
|
478
|
+
spatialGridManager.canPlaceOnFloorFootprints('level_surface-move', footprints, [
|
|
479
|
+
original.id,
|
|
480
|
+
]).valid,
|
|
481
|
+
).toBe(true)
|
|
482
|
+
}
|
|
483
|
+
if (destination !== 'floor') {
|
|
484
|
+
pose = session.enter(eventFor(host, [0.6, 1, -0.4]).event, dimensions, pose.rotationY)!
|
|
485
|
+
}
|
|
486
|
+
const final = useScene.getState().nodes[original.id]!
|
|
487
|
+
const patch = {
|
|
488
|
+
...final,
|
|
489
|
+
...resolveSupportSlabPatch(final, useScene.getState().nodes, { maxElevation: 0 }),
|
|
490
|
+
}
|
|
491
|
+
session.restore()
|
|
492
|
+
useScene.temporal.getState().resume()
|
|
493
|
+
useScene.getState().updateNodes([{ id: original.id, data: patch }])
|
|
494
|
+
useScene.temporal.getState().pause()
|
|
495
|
+
expect(useScene.getState().nodes[original.id]?.parentId).toBe(
|
|
496
|
+
destination === 'floor' ? 'level_surface-move' : host.id,
|
|
497
|
+
)
|
|
498
|
+
expect(useScene.temporal.getState().pastStates).toHaveLength(1)
|
|
499
|
+
useScene.temporal.getState().undo()
|
|
500
|
+
expect(useScene.getState().nodes[original.id]).toEqual(hosted)
|
|
501
|
+
expect((useScene.getState().nodes[host.id] as ItemNode).children as string[]).toContain(
|
|
502
|
+
original.id,
|
|
503
|
+
)
|
|
504
|
+
useScene.temporal.getState().redo()
|
|
505
|
+
expect(useScene.getState().nodes[original.id]).toEqual(patch)
|
|
506
|
+
},
|
|
507
|
+
)
|
|
509
508
|
})
|
|
@@ -105,6 +105,8 @@ function parentWorldYaw(parentId: string | null | undefined): number {
|
|
|
105
105
|
export function createRegistryItemSurfaceMove(node: AnyNode) {
|
|
106
106
|
const capabilities = nodeRegistry.get(node.type)?.capabilities
|
|
107
107
|
const floorPlaced = capabilities?.floorPlaced
|
|
108
|
+
if (capabilities?.movable?.preserveLevelAndElevation && !isFreshPlacementMetadata(node.metadata))
|
|
109
|
+
return null
|
|
108
110
|
if (!floorPlaced || (floorPlaced.applies && !floorPlaced.applies(node))) return null
|
|
109
111
|
|
|
110
112
|
const original = node as AnyNode & {
|
|
@@ -219,32 +219,32 @@ test('the mover rejects an indirect descendant before asking the resolver', () =
|
|
|
219
219
|
expect(useScene.getState().nodes).toBe(before)
|
|
220
220
|
})
|
|
221
221
|
|
|
222
|
-
test.each([
|
|
223
|
-
'
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
222
|
+
test.each(['host-local', 'surface-local'] as const)(
|
|
223
|
+
'writes the resolver pose in its %s child frame',
|
|
224
|
+
(childFrame) => {
|
|
225
|
+
const placement: SurfacePlacement = {
|
|
226
|
+
position: [0.25, 1.5, -0.75],
|
|
227
|
+
rotationY: 0.4,
|
|
228
|
+
surfaceId: 'test-surface',
|
|
229
|
+
childFrame,
|
|
230
|
+
surfaceLocal: { position: [-0.5, 0, 0.25], rotationY: -0.2, rotation: [0.7, -0.2, 0.8] },
|
|
231
|
+
}
|
|
232
|
+
resolver.mockReturnValue(placement)
|
|
233
|
+
const session = createRegistryItemSurfaceMove(child)!
|
|
234
|
+
const pose = session.enter(hit(), dimensions, 0.3)!
|
|
235
|
+
const expected = childFrame === 'surface-local' ? placement.surfaceLocal! : placement
|
|
236
|
+
expect(pose.position).toEqual([...expected.position])
|
|
237
|
+
expect(pose.rotationY).toBe(expected.rotationY)
|
|
238
|
+
expect(useScene.getState().nodes[child.id]).toMatchObject({
|
|
239
|
+
parentId: host.id,
|
|
240
|
+
position: expected.position,
|
|
241
|
+
rotation:
|
|
242
|
+
childFrame === 'surface-local'
|
|
243
|
+
? placement.surfaceLocal!.rotation
|
|
244
|
+
: [0.1, placement.rotationY, 0.2],
|
|
245
|
+
})
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
248
|
|
|
249
249
|
test('non-item events never reach the resolver', () => {
|
|
250
250
|
const session = createRegistryItemSurfaceMove(child)!
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
1
|
+
import { expect, jest, test } from 'bun:test'
|
|
2
|
+
import type {
|
|
3
|
+
AnyNode,
|
|
4
|
+
AnyNodeDefinition,
|
|
5
|
+
AnyNodeId,
|
|
6
|
+
BuildingNode,
|
|
7
|
+
LevelNode,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import type { Catalogue } from '@react-three/fiber'
|
|
2
10
|
import { resolveMoveRotationStep } from './move-registry-node-tool'
|
|
3
11
|
|
|
4
12
|
test('applies a free rotation step while the move is unattached', () => {
|
|
@@ -8,3 +16,262 @@ test('applies a free rotation step while the move is unattached', () => {
|
|
|
8
16
|
test('rejects rotation steps while the move is wall-attached', () => {
|
|
9
17
|
expect(resolveMoveRotationStep(0.5, 0.25, Math.PI / 2)).toBeNull()
|
|
10
18
|
})
|
|
19
|
+
|
|
20
|
+
// Contract-derived controller regression, not a helper replay: editable keys
|
|
21
|
+
// leave a live move alone; outside R/T and Alt still affect the real controller.
|
|
22
|
+
// R3F reconciles real Three objects with a rendererless context. No drawing,
|
|
23
|
+
// native typing, canvas hit-testing or GPU evidence is claimed by this test.
|
|
24
|
+
test('focus completion: mounted registry mover owns only outside R T and Alt', async () => {
|
|
25
|
+
const core = await import('@pascal-app/core')
|
|
26
|
+
const { useViewer } = await import('@pascal-app/viewer')
|
|
27
|
+
const { createRoot, extend } = await import('@react-three/fiber')
|
|
28
|
+
const { act, createElement } = await import('react')
|
|
29
|
+
const three = await import('three')
|
|
30
|
+
const { z } = await import('zod')
|
|
31
|
+
const { MoveRegistryNodeTool } = await import('./move-registry-node-tool')
|
|
32
|
+
const { default: useEditor } = await import('../../../store/use-editor')
|
|
33
|
+
const { default: useInteractionScope } = await import('../../../store/use-interaction-scope')
|
|
34
|
+
const { default: useAlignmentGuides } = await import('../../../store/use-alignment-guides')
|
|
35
|
+
const { default: useFacingPose } = await import('../../../store/use-facing-pose')
|
|
36
|
+
const { default: usePlacementPreview } = await import('../../../store/use-placement-preview')
|
|
37
|
+
const globals = [
|
|
38
|
+
'window',
|
|
39
|
+
'document',
|
|
40
|
+
'requestAnimationFrame',
|
|
41
|
+
'cancelAnimationFrame',
|
|
42
|
+
'IS_REACT_ACT_ENVIRONMENT',
|
|
43
|
+
] as const
|
|
44
|
+
const descriptors = globals.map((key) => Object.getOwnPropertyDescriptor(globalThis, key))
|
|
45
|
+
const stores = [
|
|
46
|
+
core.useScene,
|
|
47
|
+
core.useScene.temporal,
|
|
48
|
+
useViewer,
|
|
49
|
+
useEditor,
|
|
50
|
+
useInteractionScope,
|
|
51
|
+
core.useLiveTransforms,
|
|
52
|
+
core.useLiveNodeOverrides,
|
|
53
|
+
useAlignmentGuides,
|
|
54
|
+
useFacingPose,
|
|
55
|
+
usePlacementPreview,
|
|
56
|
+
]
|
|
57
|
+
const originalStates = stores.map((store) => store.getState())
|
|
58
|
+
const restoreRegistry = core.nodeRegistry._snapshot()
|
|
59
|
+
const sceneObjects = new Map(core.sceneRegistry.nodes)
|
|
60
|
+
const frames = new Map<number, FrameRequestCallback>()
|
|
61
|
+
let nextFrame = 0
|
|
62
|
+
let time = 0
|
|
63
|
+
const flushFrames = () => {
|
|
64
|
+
const pending = [...frames.values()]
|
|
65
|
+
frames.clear()
|
|
66
|
+
for (const callback of pending) {
|
|
67
|
+
time += 16
|
|
68
|
+
callback(time)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
let root: ReturnType<typeof createRoot> | undefined
|
|
72
|
+
let mountedScene: import('three').Scene | undefined
|
|
73
|
+
const mesh = new three.Mesh(new three.BoxGeometry(1, 1, 1), new three.MeshBasicMaterial())
|
|
74
|
+
const id = 'completion_registry' as AnyNodeId
|
|
75
|
+
const buildingId = 'building_completion_registry' as BuildingNode['id']
|
|
76
|
+
const levelId = 'level_completion_registry' as LevelNode['id']
|
|
77
|
+
const node = {
|
|
78
|
+
id,
|
|
79
|
+
type: 'completion:registry',
|
|
80
|
+
object: 'node',
|
|
81
|
+
parentId: levelId,
|
|
82
|
+
children: [],
|
|
83
|
+
metadata: {},
|
|
84
|
+
visible: true,
|
|
85
|
+
position: [2, 0, 3],
|
|
86
|
+
rotation: 0.25,
|
|
87
|
+
} as unknown as AnyNode
|
|
88
|
+
jest.useFakeTimers()
|
|
89
|
+
try {
|
|
90
|
+
const target = new EventTarget()
|
|
91
|
+
const documentFixture = { body: { style: { cursor: '' } }, querySelector: () => null }
|
|
92
|
+
Object.defineProperties(globalThis, {
|
|
93
|
+
window: {
|
|
94
|
+
configurable: true,
|
|
95
|
+
value: Object.assign(target, {
|
|
96
|
+
matchMedia: () => Object.assign(new EventTarget(), { matches: false }),
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
document: { configurable: true, value: documentFixture },
|
|
100
|
+
requestAnimationFrame: {
|
|
101
|
+
configurable: true,
|
|
102
|
+
value: (callback: FrameRequestCallback) => {
|
|
103
|
+
frames.set(++nextFrame, callback)
|
|
104
|
+
return nextFrame
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
cancelAnimationFrame: { configurable: true, value: (key: number) => frames.delete(key) },
|
|
108
|
+
IS_REACT_ACT_ENVIRONMENT: { configurable: true, value: true },
|
|
109
|
+
})
|
|
110
|
+
core.registerNode({
|
|
111
|
+
kind: node.type,
|
|
112
|
+
schemaVersion: 1,
|
|
113
|
+
category: 'utility',
|
|
114
|
+
schema: z.object({ type: z.literal(node.type) }).passthrough(),
|
|
115
|
+
defaults: () => ({}),
|
|
116
|
+
capabilities: {
|
|
117
|
+
movable: { isValidPosition: () => false },
|
|
118
|
+
floorPlaced: {
|
|
119
|
+
collides: true,
|
|
120
|
+
footprint: (candidate: AnyNode) => ({
|
|
121
|
+
dimensions: [1, 1, 1],
|
|
122
|
+
rotation: [0, (candidate as unknown as { rotation: number }).rotation, 0],
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
} as unknown as AnyNodeDefinition)
|
|
127
|
+
core.useScene.temporal.getState().pause()
|
|
128
|
+
// Placement validity is level-scoped. Without a real selected level the
|
|
129
|
+
// mover deliberately treats placement as valid before consulting the kind.
|
|
130
|
+
core.useScene.setState({
|
|
131
|
+
nodes: {
|
|
132
|
+
[buildingId]: {
|
|
133
|
+
id: buildingId,
|
|
134
|
+
type: 'building',
|
|
135
|
+
parentId: null,
|
|
136
|
+
children: [levelId],
|
|
137
|
+
position: [0, 0, 0],
|
|
138
|
+
rotation: 0,
|
|
139
|
+
} as unknown as AnyNode,
|
|
140
|
+
[levelId]: {
|
|
141
|
+
id: levelId,
|
|
142
|
+
type: 'level',
|
|
143
|
+
parentId: buildingId,
|
|
144
|
+
children: [id],
|
|
145
|
+
elevation: 0,
|
|
146
|
+
height: 3,
|
|
147
|
+
} as unknown as AnyNode,
|
|
148
|
+
[id]: node,
|
|
149
|
+
},
|
|
150
|
+
rootNodeIds: [buildingId],
|
|
151
|
+
})
|
|
152
|
+
core.useScene.temporal.getState().resume()
|
|
153
|
+
core.clearSceneHistory()
|
|
154
|
+
core.useScene.getState().updateNode(id, { name: 'history sentinel' } as never)
|
|
155
|
+
flushFrames()
|
|
156
|
+
useViewer.getState().setSelection({ selectedIds: [id], levelId, buildingId, zoneId: null })
|
|
157
|
+
useEditor.setState({ mode: 'select', tool: null })
|
|
158
|
+
useEditor.getState().setMovingNode(core.useScene.getState().nodes[id]!)
|
|
159
|
+
mesh.position.set(2, 0, 3)
|
|
160
|
+
mesh.rotation.y = 0.25
|
|
161
|
+
core.sceneRegistry.nodes.set(id, mesh)
|
|
162
|
+
extend(three as unknown as Catalogue)
|
|
163
|
+
// Supplying a renderer-shaped context prevents WebGLRenderer construction.
|
|
164
|
+
// Drawing is a hard failure, never a stub returning fake pixels.
|
|
165
|
+
const gl = {
|
|
166
|
+
render: () => {
|
|
167
|
+
throw new Error('CPU controller acceptance must not render')
|
|
168
|
+
},
|
|
169
|
+
setSize: () => {},
|
|
170
|
+
setPixelRatio: () => {},
|
|
171
|
+
domElement: {},
|
|
172
|
+
}
|
|
173
|
+
root = createRoot({} as HTMLCanvasElement)
|
|
174
|
+
await root.configure({
|
|
175
|
+
gl: gl as never,
|
|
176
|
+
frameloop: 'never',
|
|
177
|
+
size: { width: 300, height: 300, top: 0, left: 0 },
|
|
178
|
+
onCreated: (state) => {
|
|
179
|
+
mountedScene = state.scene
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
await act(async () => {
|
|
183
|
+
root!.render(
|
|
184
|
+
createElement(MoveRegistryNodeTool, { node: core.useScene.getState().nodes[id]! }),
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
const colours = () => {
|
|
188
|
+
const result: number[] = []
|
|
189
|
+
mountedScene!.traverse((object) => {
|
|
190
|
+
const material = (object as import('three').Mesh).material
|
|
191
|
+
for (const entry of Array.isArray(material) ? material : [material]) {
|
|
192
|
+
if (entry && 'color' in entry)
|
|
193
|
+
result.push((entry.color as import('three').Color).getHex())
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
return result.sort()
|
|
197
|
+
}
|
|
198
|
+
const snapshot = () =>
|
|
199
|
+
JSON.parse(
|
|
200
|
+
JSON.stringify({
|
|
201
|
+
nodes: core.useScene.getState().nodes,
|
|
202
|
+
live: [...core.useLiveTransforms.getState().transforms],
|
|
203
|
+
overrides: [...core.useLiveNodeOverrides.getState().overrides],
|
|
204
|
+
scope: useInteractionScope.getState().scope,
|
|
205
|
+
history: {
|
|
206
|
+
past: core.useScene.temporal.getState().pastStates,
|
|
207
|
+
future: core.useScene.temporal.getState().futureStates,
|
|
208
|
+
tracking: core.useScene.temporal.getState().isTracking,
|
|
209
|
+
pauseDepth: core.getSceneHistoryPauseDepth(),
|
|
210
|
+
},
|
|
211
|
+
pose: { position: mesh.position.toArray(), rotation: mesh.rotation.y },
|
|
212
|
+
colours: colours(),
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
const key = async (value: string, editable: boolean, type = 'keydown') => {
|
|
216
|
+
const event = new Event(type, { cancelable: true })
|
|
217
|
+
Object.defineProperties(event, {
|
|
218
|
+
key: { value },
|
|
219
|
+
target: {
|
|
220
|
+
value: editable
|
|
221
|
+
? { tagName: 'TEXTAREA', closest: () => ({ tagName: 'TEXTAREA' }) }
|
|
222
|
+
: target,
|
|
223
|
+
},
|
|
224
|
+
altKey: { value: value === 'Alt' },
|
|
225
|
+
ctrlKey: { value: false },
|
|
226
|
+
metaKey: { value: false },
|
|
227
|
+
})
|
|
228
|
+
await act(async () => {
|
|
229
|
+
target.dispatchEvent(event)
|
|
230
|
+
flushFrames()
|
|
231
|
+
})
|
|
232
|
+
return event
|
|
233
|
+
}
|
|
234
|
+
expect((await key('r', false)).defaultPrevented).toBe(true)
|
|
235
|
+
expect(mesh.rotation.y).toBeCloseTo(0.25 + Math.PI / 4)
|
|
236
|
+
expect(colours()).toContain(0xef4444)
|
|
237
|
+
const before = snapshot()
|
|
238
|
+
expect(before.live.length).toBeGreaterThan(0)
|
|
239
|
+
expect(before.history.past.length).toBeGreaterThan(0)
|
|
240
|
+
expect(before.history.tracking).toBe(false)
|
|
241
|
+
expect(before.scope.kind).toBe('moving')
|
|
242
|
+
for (const value of ['r', 't', 'Alt']) {
|
|
243
|
+
expect((await key(value, true)).defaultPrevented).toBe(false)
|
|
244
|
+
expect(snapshot()).toEqual(before)
|
|
245
|
+
}
|
|
246
|
+
// Key-up reset remains active even if focus moved into a field.
|
|
247
|
+
await key('Alt', false)
|
|
248
|
+
expect(colours()).toContain(0x22c55e)
|
|
249
|
+
await key('Alt', true, 'keyup')
|
|
250
|
+
expect(colours()).toContain(0xef4444)
|
|
251
|
+
expect((await key('t', false)).defaultPrevented).toBe(true)
|
|
252
|
+
expect(mesh.rotation.y).toBeCloseTo(0.25)
|
|
253
|
+
expect(core.useLiveTransforms.getState().transforms.get(id)?.rotation).toBeCloseTo(0.25)
|
|
254
|
+
} finally {
|
|
255
|
+
if (root)
|
|
256
|
+
await act(async () => {
|
|
257
|
+
root!.unmount()
|
|
258
|
+
})
|
|
259
|
+
// R3F's delayed disposal is advanced, not slept through, before globals go.
|
|
260
|
+
jest.runOnlyPendingTimers()
|
|
261
|
+
frames.clear()
|
|
262
|
+
restoreRegistry()
|
|
263
|
+
core.sceneRegistry.nodes.clear()
|
|
264
|
+
for (const [key, value] of sceneObjects) core.sceneRegistry.nodes.set(key, value)
|
|
265
|
+
stores.forEach((store, index) => {
|
|
266
|
+
;(store.setState as (snapshot: unknown, replace: true) => void)(originalStates[index], true)
|
|
267
|
+
})
|
|
268
|
+
mesh.geometry.dispose()
|
|
269
|
+
mesh.material.dispose()
|
|
270
|
+
jest.useRealTimers()
|
|
271
|
+
globals.forEach((key, index) => {
|
|
272
|
+
const descriptor = descriptors[index]
|
|
273
|
+
if (descriptor) Object.defineProperty(globalThis, key, descriptor)
|
|
274
|
+
else Reflect.deleteProperty(globalThis, key)
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
}, 15_000)
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
footprintAABBFrom,
|
|
18
18
|
type GridEvent,
|
|
19
19
|
type GroupMoveSnapResult,
|
|
20
|
+
getFloorPlacedElevation,
|
|
20
21
|
getFloorPlacedFootprints,
|
|
21
22
|
type MovableConfig,
|
|
22
23
|
movingFootprintAnchors,
|
|
@@ -60,6 +61,7 @@ import {
|
|
|
60
61
|
surfaceAttachmentUpdates,
|
|
61
62
|
updateSurfaceNode,
|
|
62
63
|
} from '../../../lib/surface-attachment'
|
|
64
|
+
import { isTypingTarget } from '../../../lib/typing-target'
|
|
63
65
|
import useAlignmentGuides from '../../../store/use-alignment-guides'
|
|
64
66
|
import useEditor, {
|
|
65
67
|
getActiveSnappingMode,
|
|
@@ -416,6 +418,9 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
416
418
|
const [cursorRotationY, setCursorRotationY] = useState(() => previewRotationY(originalRotationY))
|
|
417
419
|
const { isFreshPlacement, previewVisible, revealFreshPlacement, useAbsoluteCursorPlacement } =
|
|
418
420
|
useFreshPlacementVisibility({ node })
|
|
421
|
+
const preserveLevelAndElevation =
|
|
422
|
+
!isFreshPlacement &&
|
|
423
|
+
nodeRegistry.get(node.type)?.capabilities?.movable?.preserveLevelAndElevation === true
|
|
419
424
|
// Kinds that declare `movable.cursorAttached` (duct fittings) pin to the
|
|
420
425
|
// cursor instead of preserving the grab offset — small connector-like
|
|
421
426
|
// nodes read an offset drag as "lagging behind the mouse".
|
|
@@ -479,7 +484,24 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
479
484
|
return Array.isArray(rotation) ? [rotation[0] ?? 0, y, rotation[2] ?? 0] : y
|
|
480
485
|
}
|
|
481
486
|
|
|
487
|
+
const keepStartingElevation = (position: [number, number, number], yaw: number) => {
|
|
488
|
+
if (!preserveLevelAndElevation) return position
|
|
489
|
+
const rotation = toCommitRotation(yaw)
|
|
490
|
+
const currentNodes = useScene.getState().nodes
|
|
491
|
+
const effectiveNode = { ...node, position, rotation } as AnyNode
|
|
492
|
+
const lift = getFloorPlacedElevation({
|
|
493
|
+
node: effectiveNode,
|
|
494
|
+
nodes: { ...currentNodes, [node.id]: effectiveNode },
|
|
495
|
+
position,
|
|
496
|
+
rotation,
|
|
497
|
+
})
|
|
498
|
+
return [position[0], originalPlanPosition[1] - lift, position[2]] as [number, number, number]
|
|
499
|
+
}
|
|
500
|
+
|
|
482
501
|
const currentLevelId = () =>
|
|
502
|
+
(preserveLevelAndElevation
|
|
503
|
+
? findLevelAncestorId(node.id, useScene.getState().nodes)
|
|
504
|
+
: null) ??
|
|
483
505
|
useViewer.getState().selection.levelId ??
|
|
484
506
|
(itemSurfaceMove ? findLevelAncestorId(node.id, useScene.getState().nodes) : node.parentId)
|
|
485
507
|
|
|
@@ -805,10 +827,14 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
805
827
|
// test are plane-height independent, so the elected surface is a
|
|
806
828
|
// single fixed point per pointer ray.
|
|
807
829
|
const pointed = resolvePointerSupportSurface(cameraRef.current, event.position)
|
|
808
|
-
supportCapRef.current = pointed?.elevation ?? null
|
|
809
|
-
supportSurfaceRef.current = pointed
|
|
810
|
-
const rawX =
|
|
811
|
-
|
|
830
|
+
supportCapRef.current = preserveLevelAndElevation ? null : (pointed?.elevation ?? null)
|
|
831
|
+
supportSurfaceRef.current = preserveLevelAndElevation ? null : pointed
|
|
832
|
+
const rawX = preserveLevelAndElevation
|
|
833
|
+
? event.localPosition[0]
|
|
834
|
+
: (pointed?.localPoint?.[0] ?? event.localPosition[0])
|
|
835
|
+
const rawZ = preserveLevelAndElevation
|
|
836
|
+
? event.localPosition[2]
|
|
837
|
+
: (pointed?.localPoint?.[2] ?? event.localPosition[2])
|
|
812
838
|
revealFreshPlacement()
|
|
813
839
|
|
|
814
840
|
const magnetic = isMagneticSnapActive()
|
|
@@ -982,7 +1008,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
982
1008
|
if (guides.length > 0) useAlignmentGuides.getState().set(guides)
|
|
983
1009
|
}
|
|
984
1010
|
}
|
|
985
|
-
if (!parentFrame && pointed?.sourceNodeId) {
|
|
1011
|
+
if (!preserveLevelAndElevation && !parentFrame && pointed?.sourceNodeId) {
|
|
986
1012
|
const rotation = toCommitRotation(rotationRef.current)
|
|
987
1013
|
const effectiveNode = {
|
|
988
1014
|
...((useScene.getState().nodes[node.id] ?? node) as Record<string, unknown>),
|
|
@@ -1000,6 +1026,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1000
1026
|
},
|
|
1001
1027
|
).position
|
|
1002
1028
|
}
|
|
1029
|
+
position = keepStartingElevation(position, rotationRef.current)
|
|
1003
1030
|
const visualPosition = getVisualPosition(position)
|
|
1004
1031
|
hasMovedRef.current = true
|
|
1005
1032
|
setCursorPosition(visualPosition)
|
|
@@ -1107,18 +1134,20 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1107
1134
|
// The pointer cap makes the persisted host reproduce the capped
|
|
1108
1135
|
// election — a drop under a deck stores the aimed-at lower slab
|
|
1109
1136
|
// (or the ground), not the deck hanging above.
|
|
1110
|
-
...
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1137
|
+
...(preserveLevelAndElevation
|
|
1138
|
+
? {}
|
|
1139
|
+
: resolveSupportSlabPatch(
|
|
1140
|
+
effectiveNode,
|
|
1141
|
+
{
|
|
1142
|
+
...useScene.getState().nodes,
|
|
1143
|
+
[node.id]: effectiveNode,
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
maxElevation: supportCapRef.current,
|
|
1147
|
+
preferredSlabId: supportSurfaceRef.current?.supportSlabId,
|
|
1148
|
+
pinSupport: supportSurfaceRef.current?.sourceNodeId != null,
|
|
1149
|
+
},
|
|
1150
|
+
)),
|
|
1122
1151
|
...(isNew
|
|
1123
1152
|
? {
|
|
1124
1153
|
metadata: stripPlacementMetadataFlags(node.metadata),
|
|
@@ -1191,18 +1220,20 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1191
1220
|
const committedNode = def.schema.parse({
|
|
1192
1221
|
...reparsed,
|
|
1193
1222
|
parentId: node.parentId,
|
|
1194
|
-
...
|
|
1195
|
-
{
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1223
|
+
...(preserveLevelAndElevation
|
|
1224
|
+
? {}
|
|
1225
|
+
: resolveSupportSlabPatch(
|
|
1226
|
+
{ ...reparsed, parentId: node.parentId } as AnyNode,
|
|
1227
|
+
{
|
|
1228
|
+
...useScene.getState().nodes,
|
|
1229
|
+
[reparsed.id]: reparsed,
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
maxElevation: supportCapRef.current,
|
|
1233
|
+
preferredSlabId: supportSurfaceRef.current?.supportSlabId,
|
|
1234
|
+
pinSupport: supportSurfaceRef.current?.sourceNodeId != null,
|
|
1235
|
+
},
|
|
1236
|
+
)),
|
|
1206
1237
|
}) as AnyNode
|
|
1207
1238
|
useScene.temporal.getState().resume()
|
|
1208
1239
|
useScene.getState().createNode(committedNode, node.parentId as AnyNodeId)
|
|
@@ -1247,6 +1278,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1247
1278
|
// item placement keys (and the "Rotate" hints the move HUD shows). Applied
|
|
1248
1279
|
// imperatively + mirrored to the live transform; committed on drop.
|
|
1249
1280
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
1281
|
+
if (isTypingTarget(e.target)) return
|
|
1250
1282
|
// Hold Alt (free place) to force placement on an invalid (red) footprint,
|
|
1251
1283
|
// matching the GLB item tool. Recolour the box to green while held.
|
|
1252
1284
|
if (e.key === 'Alt') {
|
|
@@ -1287,6 +1319,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1287
1319
|
)
|
|
1288
1320
|
position = canonicalPositionFromPlan(planOrigin[0], position[1], planOrigin[2])
|
|
1289
1321
|
}
|
|
1322
|
+
position = keepStartingElevation(position, hostedPose?.rotationY ?? nextFreeRotation)
|
|
1290
1323
|
lastCursorRef.current = position
|
|
1291
1324
|
freeRotationRef.current = hostedPose?.rotationY ?? nextFreeRotation
|
|
1292
1325
|
rotationRef.current = freeRotationRef.current
|
|
@@ -1423,6 +1456,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
|
1423
1456
|
originalPosition,
|
|
1424
1457
|
originalPlanPosition,
|
|
1425
1458
|
originalRotationY,
|
|
1459
|
+
preserveLevelAndElevation,
|
|
1426
1460
|
previewRotationY,
|
|
1427
1461
|
revealFreshPlacement,
|
|
1428
1462
|
useAbsoluteCursorPlacement,
|