@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
|
@@ -472,3 +472,41 @@ describe('item move history', () => {
|
|
|
472
472
|
expect(history()).toEqual({ past: before.past, tracking: true, depth: 0 })
|
|
473
473
|
})
|
|
474
474
|
})
|
|
475
|
+
|
|
476
|
+
describe('item placement selection', () => {
|
|
477
|
+
// A surface `:click` is synthesized on pointer-up (use-node-events), and the browser fires its
|
|
478
|
+
// own click after it. By then the editor is back in select mode, where the grid takes that click
|
|
479
|
+
// for one on empty ground and drops the selection of the item just placed.
|
|
480
|
+
test('a drop on pointer-up selects the new item and eats the click that follows', async () => {
|
|
481
|
+
const renderer = await create(<Placement />)
|
|
482
|
+
try {
|
|
483
|
+
await carry(10, 14)
|
|
484
|
+
const dropOnRelease = () =>
|
|
485
|
+
emitter.emit('node:click', {
|
|
486
|
+
node: { id: 'site_placement_history', type: 'site' },
|
|
487
|
+
position: [1.4, 0, 0.5],
|
|
488
|
+
localPosition: [1.4, 0, 0.5],
|
|
489
|
+
nativeEvent: { nativeEvent: {} },
|
|
490
|
+
stopPropagation() {},
|
|
491
|
+
} as never)
|
|
492
|
+
window.addEventListener('pointerup', dropOnRelease)
|
|
493
|
+
await act(async () => {
|
|
494
|
+
window.dispatchEvent(new Event('pointerup'))
|
|
495
|
+
})
|
|
496
|
+
window.removeEventListener('pointerup', dropOnRelease)
|
|
497
|
+
const placed = levelItems().find((node) => node.id !== item.id)
|
|
498
|
+
expect(placed).toBeDefined()
|
|
499
|
+
expect(useViewer.getState().selection.selectedIds).toEqual([placed!.id])
|
|
500
|
+
const click = new Event('click', { cancelable: true })
|
|
501
|
+
window.dispatchEvent(click)
|
|
502
|
+
expect(click.defaultPrevented).toBe(true)
|
|
503
|
+
// Only that one click: once the swallow window closes, a click reaches the editor again.
|
|
504
|
+
await new Promise((resolve) => setTimeout(resolve, 350))
|
|
505
|
+
const next = new Event('click', { cancelable: true })
|
|
506
|
+
window.dispatchEvent(next)
|
|
507
|
+
expect(next.defaultPrevented).toBe(false)
|
|
508
|
+
} finally {
|
|
509
|
+
await renderer.unmount()
|
|
510
|
+
}
|
|
511
|
+
})
|
|
512
|
+
})
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
BlockNode,
|
|
4
|
+
type CeilingEvent,
|
|
5
|
+
type CeilingNode,
|
|
4
6
|
type GridEvent,
|
|
5
7
|
ItemNode,
|
|
6
8
|
type LevelNode,
|
|
@@ -10,7 +12,12 @@ import {
|
|
|
10
12
|
type WallNode,
|
|
11
13
|
} from '@pascal-app/core'
|
|
12
14
|
import { BufferGeometry, Mesh, MeshBasicMaterial, type Object3D, Vector3 } from 'three'
|
|
13
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
ceilingStrategy,
|
|
17
|
+
faceHostStrategy,
|
|
18
|
+
floorStrategy,
|
|
19
|
+
wallStrategy,
|
|
20
|
+
} from './placement-strategies'
|
|
14
21
|
import type { PlacementContext, SpatialValidators } from './placement-types'
|
|
15
22
|
import { registerTestBlockFaceHost } from './test-face-host'
|
|
16
23
|
|
|
@@ -492,3 +499,41 @@ describe('floorStrategy.move', () => {
|
|
|
492
499
|
expect(result.nodeUpdate?.position?.[1]).toBe(0)
|
|
493
500
|
})
|
|
494
501
|
})
|
|
502
|
+
|
|
503
|
+
describe('ceilingStrategy.move', () => {
|
|
504
|
+
const ceiling = { id: 'ceiling_test', type: 'ceiling' } as unknown as CeilingNode
|
|
505
|
+
const ceilingEvent = (): CeilingEvent =>
|
|
506
|
+
({
|
|
507
|
+
node: ceiling,
|
|
508
|
+
position: [1, 2.5, 1],
|
|
509
|
+
localPosition: [1, 0, 1],
|
|
510
|
+
object: new Mesh(),
|
|
511
|
+
stopPropagation: () => {},
|
|
512
|
+
}) as unknown as CeilingEvent
|
|
513
|
+
const onCeiling = (draftItem: ItemNode): PlacementContext => ({
|
|
514
|
+
...ceilingContext(),
|
|
515
|
+
asset: draftItem.asset,
|
|
516
|
+
draftItem,
|
|
517
|
+
state: { ...ceilingContext().state, surface: 'ceiling', ceilingId: ceiling.id },
|
|
518
|
+
})
|
|
519
|
+
const hanging = (source?: unknown) =>
|
|
520
|
+
({
|
|
521
|
+
...makeDraft(),
|
|
522
|
+
asset: { ...makeDraft().asset, attachTo: 'ceiling', dimensions: [0.9, 1, 0.6] },
|
|
523
|
+
...(source ? { source } : {}),
|
|
524
|
+
}) as unknown as ItemNode
|
|
525
|
+
|
|
526
|
+
test('hangs a catalog model its full height below the ceiling', () => {
|
|
527
|
+
const result = ceilingStrategy.move(onCeiling(hanging()), ceilingEvent())
|
|
528
|
+
expect(result?.gridPosition[1]).toBeCloseTo(-1, 6)
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
test('hangs a scripted object from its top, where the compiler put its origin', () => {
|
|
532
|
+
const source = {
|
|
533
|
+
kind: 'script',
|
|
534
|
+
manifest: { bounds: { min: [-0.45, -1, -0.3], max: [0.45, 0, 0.3] } },
|
|
535
|
+
}
|
|
536
|
+
const result = ceilingStrategy.move(onCeiling(hanging(source)), ceilingEvent())
|
|
537
|
+
expect(result?.gridPosition[1]).toBeCloseTo(0, 6)
|
|
538
|
+
})
|
|
539
|
+
})
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
WallNode,
|
|
16
16
|
} from '@pascal-app/core'
|
|
17
17
|
import {
|
|
18
|
+
ceilingSeatY,
|
|
18
19
|
clampRectToRoofWallFace,
|
|
19
20
|
clearFaceHostItemFields,
|
|
20
21
|
createSceneApi,
|
|
@@ -747,9 +748,7 @@ export const ceilingStrategy = {
|
|
|
747
748
|
// use the ceiling hit's local position rather than world position.
|
|
748
749
|
const x = snapToGrid(event.localPosition[0], swapDims ? dimZ : dimX)
|
|
749
750
|
const z = snapToGrid(event.localPosition[2], swapDims ? dimX : dimZ)
|
|
750
|
-
|
|
751
|
-
// void above); everything else hangs its full height below the ceiling.
|
|
752
|
-
const seatY = ctx.asset.recessed ? 0 : -itemHeight
|
|
751
|
+
const seatY = ceilingSeatY(ctx.draftItem ?? { asset: ctx.asset }, itemHeight)
|
|
753
752
|
const worldSnapped = event.object.localToWorld(new Vector3(x, seatY, z))
|
|
754
753
|
|
|
755
754
|
return {
|
|
@@ -781,9 +780,7 @@ export const ceilingStrategy = {
|
|
|
781
780
|
|
|
782
781
|
const x = snapToGrid(event.localPosition[0], swapDims ? dimZ : dimX)
|
|
783
782
|
const z = snapToGrid(event.localPosition[2], swapDims ? dimX : dimZ)
|
|
784
|
-
|
|
785
|
-
// void above); everything else hangs its full height below the ceiling.
|
|
786
|
-
const seatY = ctx.draftItem.asset.recessed ? 0 : -itemHeight
|
|
783
|
+
const seatY = ceilingSeatY(ctx.draftItem, itemHeight)
|
|
787
784
|
const worldSnapped = event.object.localToWorld(new Vector3(x, seatY, z))
|
|
788
785
|
|
|
789
786
|
return {
|
|
@@ -117,39 +117,39 @@ function hit(y = 1, z = -0.1): ShelfEvent {
|
|
|
117
117
|
} as ShelfEvent
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
test.each([
|
|
121
|
-
'grid',
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
120
|
+
test.each(['grid', 'off'] as const)(
|
|
121
|
+
'midpoint keeps the first declared row and preserves X/Z with grid %s',
|
|
122
|
+
(mode) => {
|
|
123
|
+
useEditor.getState().setSnappingMode('item', mode)
|
|
124
|
+
for (const declared of [
|
|
125
|
+
[0.5, 1.5],
|
|
126
|
+
[1.5, 0.5],
|
|
127
|
+
]) {
|
|
128
|
+
rows = declared
|
|
129
|
+
const expected: [number, number, number] =
|
|
130
|
+
mode === 'grid' ? [0.25, rows[0]!, 0.125] : [0.37, rows[0]!, -0.1]
|
|
131
|
+
const ctx = context()
|
|
132
|
+
const entered = shelfSurfaceStrategy.enter(ctx, hit())!
|
|
133
|
+
expect(entered.nodeUpdate).toEqual({
|
|
134
|
+
position: expected,
|
|
135
|
+
parentId: host.id,
|
|
136
|
+
rotation: [0.1, 0.7, 0.3],
|
|
137
|
+
})
|
|
138
|
+
expect(entered.gridPosition).toEqual(expected)
|
|
139
|
+
expect(entered.cursorPosition).toEqual(expected)
|
|
140
|
+
const moved = shelfSurfaceStrategy.move(onShelf(ctx), hit())!
|
|
141
|
+
expect(moved.nodeUpdate).toEqual({ position: expected })
|
|
142
|
+
expect(moved.cursorPosition).toEqual(expected)
|
|
143
|
+
const committed = shelfSurfaceStrategy.click(
|
|
144
|
+
{ ...onShelf(ctx), gridPosition: new Vector3(...entered.gridPosition) },
|
|
145
|
+
hit(),
|
|
146
|
+
)!
|
|
147
|
+
expect(committed.nodeUpdate.position).toEqual(expected)
|
|
148
|
+
expect(committed.nodeUpdate.parentId).toBe(host.id)
|
|
149
|
+
}
|
|
150
|
+
expect(resolver).toHaveBeenCalledTimes(6)
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
153
|
|
|
154
154
|
test.each([
|
|
155
155
|
[0.999, 0.5],
|
|
@@ -182,29 +182,29 @@ test('a full-board object is refused when grid snapping puts its centre outside
|
|
|
182
182
|
expect(shelfSurfaceStrategy.enter(context(draft), hit(1, -0.39))).toBeNull()
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
-
test.each([
|
|
186
|
-
'grid',
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
185
|
+
test.each(['grid', 'off'] as const)(
|
|
186
|
+
'shelf frame conversion retains exact poses with grid %s',
|
|
187
|
+
(mode) => {
|
|
188
|
+
useEditor.getState().setSnappingMode('item', mode)
|
|
189
|
+
const mesh = sceneRegistry.nodes.get(host.id)!
|
|
190
|
+
mesh.position.set(4, 2, -3)
|
|
191
|
+
mesh.rotation.set(0, Math.PI / 3, 0)
|
|
192
|
+
mesh.updateMatrixWorld(true)
|
|
193
|
+
const event = hit(1.2)
|
|
194
|
+
const local = mesh.worldToLocal(new Vector3(...event.position))
|
|
195
|
+
const expected: [number, number, number] =
|
|
196
|
+
mode === 'grid' ? [0.25, 1.5, 0.125] : [local.x, 1.5, local.z]
|
|
197
|
+
const yaw = new Euler().setFromQuaternion(mesh.getWorldQuaternion(new Quaternion()), 'YXZ').y
|
|
198
|
+
const entered = shelfSurfaceStrategy.enter(context(), event)!
|
|
199
|
+
expect(entered.nodeUpdate).toEqual({
|
|
200
|
+
position: expected,
|
|
201
|
+
parentId: host.id,
|
|
202
|
+
rotation: [0.1, 0.7 - yaw, 0.3],
|
|
203
|
+
})
|
|
204
|
+
expect(entered.cursorPosition).toEqual(mesh.localToWorld(new Vector3(...expected)).toArray())
|
|
205
|
+
expect(shelfSurfaceStrategy.move(onShelf(context()), event)!.gridPosition).toEqual(expected)
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
208
|
|
|
209
209
|
test('only enter requires an upward normal; moves keep electing rows', () => {
|
|
210
210
|
for (const normal of [[1, 0, 0], [0, -1, 0], undefined] as ShelfEvent['normal'][]) {
|
|
@@ -89,105 +89,105 @@ describe('useDraftNode block face commit', () => {
|
|
|
89
89
|
finish,
|
|
90
90
|
})),
|
|
91
91
|
),
|
|
92
|
-
)(
|
|
93
|
-
failure,
|
|
94
|
-
finish
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
const updateNodes = useScene.getState().updateNodes
|
|
116
|
-
try {
|
|
117
|
-
if (failure === 'write') {
|
|
118
|
-
useScene.setState({
|
|
119
|
-
updateNodes: (updates) => {
|
|
120
|
-
if (
|
|
121
|
-
useScene.temporal.getState().isTracking &&
|
|
122
|
-
updates.some(
|
|
123
|
-
(update) =>
|
|
124
|
-
update.id === item.id &&
|
|
125
|
-
'position' in update.data &&
|
|
126
|
-
update.data.position?.[0] === 3,
|
|
127
|
-
)
|
|
128
|
-
) {
|
|
129
|
-
throw new Error('Rejected tracked write before publication')
|
|
130
|
-
}
|
|
131
|
-
return updateNodes(updates)
|
|
132
|
-
},
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
expect(() => commit(failure === 'parse' ? [Number.NaN, 0, 3] : [3, 0, 3])).toThrow()
|
|
136
|
-
useScene.setState({ updateNodes })
|
|
137
|
-
expect(draft.current?.id).toBe(item.id)
|
|
138
|
-
expect(useScene.getState().nodes).toEqual(originalNodes)
|
|
139
|
-
expect(useScene.temporal.getState().pastStates).toHaveLength(0)
|
|
140
|
-
expect(useScene.temporal.getState().isTracking).toBe(true)
|
|
141
|
-
expect(getSceneHistoryPauseDepth()).toBe(0)
|
|
142
|
-
|
|
143
|
-
draft.updateSurface({ position: [4, 0, 4] }, null)
|
|
144
|
-
useScene.getState().updateNode(item.id, { name: 'Foreign rename' })
|
|
145
|
-
expect(useScene.temporal.getState().pastStates).toHaveLength(1)
|
|
146
|
-
expect(useScene.temporal.getState().pastStates[0]?.nodes).toEqual(originalNodes)
|
|
147
|
-
const renamedNodes = {
|
|
148
|
-
...originalNodes,
|
|
149
|
-
[item.id]: { ...originalNodes[item.id]!, name: 'Foreign rename' },
|
|
92
|
+
)(
|
|
93
|
+
'an adopted $failure rejection retains owned history through $finish',
|
|
94
|
+
({ failure, finish }) => {
|
|
95
|
+
const item = ItemNode.parse({
|
|
96
|
+
parentId: LEVEL_ID,
|
|
97
|
+
position: [1, 0, 1],
|
|
98
|
+
asset: { id: 'box', name: 'Box', category: 'decor', thumbnail: '', src: '/box.glb' },
|
|
99
|
+
})
|
|
100
|
+
useScene.getState().createNode(item, LEVEL_ID as AnyNodeId)
|
|
101
|
+
useScene.temporal.getState().clear()
|
|
102
|
+
const originalNodes = structuredClone(useScene.getState().nodes)
|
|
103
|
+
const draft = draftNode!
|
|
104
|
+
draft.adopt(item)
|
|
105
|
+
draft.updateSurface({ position: [2, 0, 2] }, null)
|
|
106
|
+
const commit = (position: ItemNode['position']) => {
|
|
107
|
+
const drop = beginSceneHistoryPauseSession(useScene, { gesture: item.id })
|
|
108
|
+
try {
|
|
109
|
+
return drop.commitStep(() => draft.commit({ parentId: LEVEL_ID, position }))
|
|
110
|
+
} finally {
|
|
111
|
+
drop.end()
|
|
112
|
+
}
|
|
150
113
|
}
|
|
114
|
+
const updateNodes = useScene.getState().updateNodes
|
|
115
|
+
try {
|
|
116
|
+
if (failure === 'write') {
|
|
117
|
+
useScene.setState({
|
|
118
|
+
updateNodes: (updates) => {
|
|
119
|
+
if (
|
|
120
|
+
useScene.temporal.getState().isTracking &&
|
|
121
|
+
updates.some(
|
|
122
|
+
(update) =>
|
|
123
|
+
update.id === item.id &&
|
|
124
|
+
'position' in update.data &&
|
|
125
|
+
update.data.position?.[0] === 3,
|
|
126
|
+
)
|
|
127
|
+
) {
|
|
128
|
+
throw new Error('Rejected tracked write before publication')
|
|
129
|
+
}
|
|
130
|
+
return updateNodes(updates)
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
expect(() => commit(failure === 'parse' ? [Number.NaN, 0, 3] : [3, 0, 3])).toThrow()
|
|
135
|
+
useScene.setState({ updateNodes })
|
|
136
|
+
expect(draft.current?.id).toBe(item.id)
|
|
137
|
+
expect(useScene.getState().nodes).toEqual(originalNodes)
|
|
138
|
+
expect(useScene.temporal.getState().pastStates).toHaveLength(0)
|
|
139
|
+
expect(useScene.temporal.getState().isTracking).toBe(true)
|
|
140
|
+
expect(getSceneHistoryPauseDepth()).toBe(0)
|
|
151
141
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
expect(draft.current).toBeNull()
|
|
155
|
-
const committedNodes = structuredClone(useScene.getState().nodes)
|
|
156
|
-
expect(committedNodes[item.id]).toMatchObject({
|
|
157
|
-
name: 'Foreign rename',
|
|
158
|
-
position: [5, 0, 5],
|
|
159
|
-
})
|
|
160
|
-
expect(useScene.temporal.getState().pastStates).toHaveLength(2)
|
|
161
|
-
useScene.temporal.getState().undo()
|
|
162
|
-
expect(useScene.getState().nodes).toEqual(renamedNodes)
|
|
163
|
-
useScene.temporal.getState().redo()
|
|
164
|
-
expect(useScene.getState().nodes).toEqual(committedNodes)
|
|
165
|
-
} else if (finish === 'cancel') {
|
|
166
|
-
draft.destroy()
|
|
167
|
-
expect(useScene.getState().nodes).toEqual(renamedNodes)
|
|
142
|
+
draft.updateSurface({ position: [4, 0, 4] }, null)
|
|
143
|
+
useScene.getState().updateNode(item.id, { name: 'Foreign rename' })
|
|
168
144
|
expect(useScene.temporal.getState().pastStates).toHaveLength(1)
|
|
169
|
-
useScene.temporal.getState().
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
145
|
+
expect(useScene.temporal.getState().pastStates[0]?.nodes).toEqual(originalNodes)
|
|
146
|
+
const renamedNodes = {
|
|
147
|
+
...originalNodes,
|
|
148
|
+
[item.id]: { ...originalNodes[item.id]!, name: 'Foreign rename' },
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (finish === 'retry') {
|
|
152
|
+
expect(commit([5, 0, 5])).toBe(item.id)
|
|
153
|
+
expect(draft.current).toBeNull()
|
|
154
|
+
const committedNodes = structuredClone(useScene.getState().nodes)
|
|
155
|
+
expect(committedNodes[item.id]).toMatchObject({
|
|
156
|
+
name: 'Foreign rename',
|
|
157
|
+
position: [5, 0, 5],
|
|
158
|
+
})
|
|
159
|
+
expect(useScene.temporal.getState().pastStates).toHaveLength(2)
|
|
160
|
+
useScene.temporal.getState().undo()
|
|
161
|
+
expect(useScene.getState().nodes).toEqual(renamedNodes)
|
|
162
|
+
useScene.temporal.getState().redo()
|
|
163
|
+
expect(useScene.getState().nodes).toEqual(committedNodes)
|
|
164
|
+
} else if (finish === 'cancel') {
|
|
165
|
+
draft.destroy()
|
|
166
|
+
expect(useScene.getState().nodes).toEqual(renamedNodes)
|
|
167
|
+
expect(useScene.temporal.getState().pastStates).toHaveLength(1)
|
|
168
|
+
useScene.temporal.getState().undo()
|
|
169
|
+
expect(useScene.getState().nodes).toEqual(originalNodes)
|
|
170
|
+
useScene.temporal.getState().redo()
|
|
171
|
+
expect(useScene.getState().nodes).toEqual(renamedNodes)
|
|
172
|
+
} else {
|
|
173
|
+
useScene.getState().updateNode(item.id, { position: [7, 0, 7] })
|
|
174
|
+
const foreignNodes = structuredClone(useScene.getState().nodes)
|
|
175
|
+
draft.destroy()
|
|
176
|
+
expect(useScene.getState().nodes).toEqual(foreignNodes)
|
|
177
|
+
expect(useScene.temporal.getState().pastStates).toHaveLength(2)
|
|
178
|
+
useScene.temporal.getState().undo()
|
|
179
|
+
expect(useScene.getState().nodes).toEqual(renamedNodes)
|
|
180
|
+
useScene.temporal.getState().redo()
|
|
181
|
+
expect(useScene.getState().nodes).toEqual(foreignNodes)
|
|
182
|
+
}
|
|
183
|
+
expect(useScene.temporal.getState().isTracking).toBe(true)
|
|
184
|
+
expect(getSceneHistoryPauseDepth()).toBe(0)
|
|
185
|
+
} finally {
|
|
186
|
+
useScene.setState({ updateNodes })
|
|
176
187
|
draft.destroy()
|
|
177
|
-
expect(useScene.getState().nodes).toEqual(foreignNodes)
|
|
178
|
-
expect(useScene.temporal.getState().pastStates).toHaveLength(2)
|
|
179
|
-
useScene.temporal.getState().undo()
|
|
180
|
-
expect(useScene.getState().nodes).toEqual(renamedNodes)
|
|
181
|
-
useScene.temporal.getState().redo()
|
|
182
|
-
expect(useScene.getState().nodes).toEqual(foreignNodes)
|
|
183
188
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
} finally {
|
|
187
|
-
useScene.setState({ updateNodes })
|
|
188
|
-
draft.destroy()
|
|
189
|
-
}
|
|
190
|
-
})
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
191
|
|
|
192
192
|
test('a drop on an undone host waits for a new valid placement target', () => {
|
|
193
193
|
const item = ItemNode.parse({
|
|
@@ -59,6 +59,8 @@ import {
|
|
|
59
59
|
surfaceFramePose,
|
|
60
60
|
updateSurfaceNode as writeSurfaceNode,
|
|
61
61
|
} from '../../../lib/surface-attachment'
|
|
62
|
+
import { isTypingTarget } from '../../../lib/typing-target'
|
|
63
|
+
import { createWallPointerTracker } from '../../../lib/wall-placement-pointer'
|
|
62
64
|
import {
|
|
63
65
|
projectAlignmentGuidesWorldToActiveBuildingLocal,
|
|
64
66
|
resolveAlignmentForActiveBuilding,
|
|
@@ -102,6 +104,7 @@ import {
|
|
|
102
104
|
import {
|
|
103
105
|
getDetachedAttachmentPreviewLift,
|
|
104
106
|
getGridAlignedDimensions,
|
|
107
|
+
getSideFromNormal,
|
|
105
108
|
snapToGrid,
|
|
106
109
|
snapToHalf,
|
|
107
110
|
snapUpToGridStep,
|
|
@@ -606,6 +609,29 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
606
609
|
if (mesh) reconcileDraftRaycast(mesh)
|
|
607
610
|
}
|
|
608
611
|
|
|
612
|
+
// Eat the click the browser fires after a pointer-up that committed, so it
|
|
613
|
+
// neither commits a second time through a surface `:click` handler nor reaches
|
|
614
|
+
// the grid, in select mode by then, as a click on empty ground.
|
|
615
|
+
const swallowNextClick = () => {
|
|
616
|
+
const swallow = (e: Event) => {
|
|
617
|
+
e.stopPropagation()
|
|
618
|
+
e.preventDefault()
|
|
619
|
+
}
|
|
620
|
+
window.addEventListener('click', swallow, { capture: true, once: true })
|
|
621
|
+
setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 300)
|
|
622
|
+
}
|
|
623
|
+
// True between a press's pointer-up and the click the browser fires after it.
|
|
624
|
+
// Surface `:click` events are synthesized on pointer-up (use-node-events), so a
|
|
625
|
+
// commit they trigger still has that click to come; a `grid:click` commit is
|
|
626
|
+
// that click.
|
|
627
|
+
let releasing = false
|
|
628
|
+
const onPointerUpCapture = () => {
|
|
629
|
+
releasing = true
|
|
630
|
+
}
|
|
631
|
+
const onClickCapture = () => {
|
|
632
|
+
releasing = false
|
|
633
|
+
}
|
|
634
|
+
|
|
609
635
|
const finishCommittedPlacement = (
|
|
610
636
|
committedId: string | null,
|
|
611
637
|
wasAdopted: boolean,
|
|
@@ -628,6 +654,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
628
654
|
// lands on the just-placed (now selected) node instead of a tool-less build
|
|
629
655
|
// limbo. Repeat placements took the early return above and stay armed.
|
|
630
656
|
useEditor.getState().setMode('select')
|
|
657
|
+
if (releasing) swallowNextClick()
|
|
631
658
|
}
|
|
632
659
|
|
|
633
660
|
const commitDraft = (
|
|
@@ -797,10 +824,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
797
824
|
// Alignment guides are floor-only; clear them when the cursor moves
|
|
798
825
|
// onto a wall / ceiling / item surface (only those paths call this).
|
|
799
826
|
useAlignmentGuides.getState().clear()
|
|
800
|
-
//
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
827
|
+
// A floor transition can be a hit gap; other hosted surfaces are a new anchor.
|
|
828
|
+
if (
|
|
829
|
+
result.stateUpdate.surface &&
|
|
830
|
+
result.stateUpdate.surface !== 'floor' &&
|
|
831
|
+
result.stateUpdate.surface !== 'wall'
|
|
832
|
+
)
|
|
833
|
+
grabForgotten = true
|
|
804
834
|
Object.assign(placementState.current, result.stateUpdate)
|
|
805
835
|
gridPosition.current.set(...result.gridPosition)
|
|
806
836
|
|
|
@@ -924,15 +954,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
924
954
|
// for a given host, then offsets every later move by
|
|
925
955
|
// `start + (raw - anchor)` — mirroring the floor path and the door/window
|
|
926
956
|
// move tools so the item tracks the grabbed point instead of teleporting
|
|
927
|
-
// its origin under the cursor.
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
startX: number
|
|
934
|
-
startY: number
|
|
935
|
-
} | null = null
|
|
957
|
+
// its origin under the cursor. Changing hosts or faces clears the offset.
|
|
958
|
+
const wallPointer = createWallPointerTracker(
|
|
959
|
+
preserveDragOffset && grabStartPosition
|
|
960
|
+
? { wallId: grabWallId, side: draftNode.current?.side, position: grabStartPosition }
|
|
961
|
+
: undefined,
|
|
962
|
+
)
|
|
936
963
|
let ceilingDragAnchor: {
|
|
937
964
|
ceilingId: string
|
|
938
965
|
rawX: number
|
|
@@ -1053,16 +1080,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1053
1080
|
// replays it. Captured once at setup — a fresh coordinator mounts per move.
|
|
1054
1081
|
const dragMode = useEditor.getState().placementDragMode
|
|
1055
1082
|
let releaseCommit: (() => void) | null = null
|
|
1056
|
-
// Eat the click the browser fires after pointer-up so the surface
|
|
1057
|
-
// `:click` handlers don't commit a second time.
|
|
1058
|
-
const swallowNextClick = () => {
|
|
1059
|
-
const swallow = (e: Event) => {
|
|
1060
|
-
e.stopPropagation()
|
|
1061
|
-
e.preventDefault()
|
|
1062
|
-
}
|
|
1063
|
-
window.addEventListener('click', swallow, { capture: true, once: true })
|
|
1064
|
-
setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 300)
|
|
1065
|
-
}
|
|
1066
1083
|
const onReleaseCommit = () => {
|
|
1067
1084
|
gridDispatch.flush()
|
|
1068
1085
|
if (!releaseCommit) return
|
|
@@ -1150,6 +1167,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1150
1167
|
if (!cursorGroupRef.current) return
|
|
1151
1168
|
const result = floorStrategy.move(getContext(), floorEvent)
|
|
1152
1169
|
if (!result) return
|
|
1170
|
+
// Attached items cannot land on the floor; its hit can be a gap in their host.
|
|
1171
|
+
if (!asset.attachTo) grabForgotten = true
|
|
1153
1172
|
|
|
1154
1173
|
// Figma-style alignment snap layered on top of the floor strategy's
|
|
1155
1174
|
// grid snap: when the draft's edge lines up (on X or Z) with another
|
|
@@ -1379,28 +1398,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1379
1398
|
|
|
1380
1399
|
let wallMoveEvent = event
|
|
1381
1400
|
if (preserveDragOffset && draftNode.current) {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
// re-seeding from the carried-over position (the old behavior)
|
|
1390
|
-
// baked an arbitrary along-wall offset into the whole stay on that
|
|
1391
|
-
// wall. Once anchored elsewhere the grab is forgotten for good, so
|
|
1392
|
-
// re-entering the original wall centers under the cursor too.
|
|
1393
|
-
const preserveGrab = preserveGrabOn(event.node.id, grabWallId)
|
|
1394
|
-
wallDragAnchor = {
|
|
1395
|
-
wallId: event.node.id,
|
|
1396
|
-
rawX,
|
|
1397
|
-
rawY,
|
|
1398
|
-
startX: preserveGrab && grabStartPosition ? grabStartPosition[0] : rawX,
|
|
1399
|
-
startY: preserveGrab && grabStartPosition ? grabStartPosition[1] : rawY,
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
const correctedX = wallDragAnchor.startX + (rawX - wallDragAnchor.rawX)
|
|
1403
|
-
const correctedY = wallDragAnchor.startY + (rawY - wallDragAnchor.rawY)
|
|
1401
|
+
preserveGrabOn(event.node.id, grabWallId)
|
|
1402
|
+
if (grabForgotten) wallPointer.forget()
|
|
1403
|
+
const [correctedX, correctedY] = wallPointer.resolve(
|
|
1404
|
+
event.node.id,
|
|
1405
|
+
getSideFromNormal(event.normal),
|
|
1406
|
+
[event.localPosition[0], event.localPosition[1]],
|
|
1407
|
+
)
|
|
1404
1408
|
wallMoveEvent = {
|
|
1405
1409
|
...event,
|
|
1406
1410
|
localPosition: [correctedX, correctedY, event.localPosition[2]],
|
|
@@ -1503,7 +1507,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1503
1507
|
}
|
|
1504
1508
|
|
|
1505
1509
|
const onWallLeave = (event: WallEvent) => {
|
|
1506
|
-
|
|
1510
|
+
wallPointer.leave()
|
|
1507
1511
|
const result = wallStrategy.leave(getContext())
|
|
1508
1512
|
if (!result) return
|
|
1509
1513
|
|
|
@@ -2432,9 +2436,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2432
2436
|
}
|
|
2433
2437
|
|
|
2434
2438
|
// Don't intercept keys when focus is inside a text input
|
|
2435
|
-
if (event.target
|
|
2436
|
-
return
|
|
2437
|
-
}
|
|
2439
|
+
if (isTypingTarget(event.target)) return
|
|
2438
2440
|
|
|
2439
2441
|
const draft = draftNode.current
|
|
2440
2442
|
if (!draft) return
|
|
@@ -2798,11 +2800,15 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2798
2800
|
}
|
|
2799
2801
|
emitter.on('node:click', commitFloorOnSurfaceClick as never)
|
|
2800
2802
|
if (dragMode) window.addEventListener('pointerup', onReleaseCommit)
|
|
2803
|
+
window.addEventListener('pointerup', onPointerUpCapture, true)
|
|
2804
|
+
window.addEventListener('click', onClickCapture, true)
|
|
2801
2805
|
|
|
2802
2806
|
return () => {
|
|
2803
2807
|
tearingDown = true
|
|
2804
2808
|
feedback.clear()
|
|
2805
2809
|
if (dragMode) window.removeEventListener('pointerup', onReleaseCommit)
|
|
2810
|
+
window.removeEventListener('pointerup', onPointerUpCapture, true)
|
|
2811
|
+
window.removeEventListener('click', onClickCapture, true)
|
|
2806
2812
|
unsubDraftWatch()
|
|
2807
2813
|
useAlignmentGuides.getState().clear()
|
|
2808
2814
|
// Clear every live preview channel before restoring or deleting the draft.
|