@pascal-app/editor 1.0.0-beta.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +63 -8
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +12 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +23 -6
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/panel-wrapper.tsx +228 -12
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-session-groups.ts +103 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -6,14 +6,19 @@ import {
|
|
|
6
6
|
type CeilingEvent,
|
|
7
7
|
collectAlignmentAnchors,
|
|
8
8
|
emitter,
|
|
9
|
+
findLevelAncestorId,
|
|
9
10
|
type GridEvent,
|
|
10
11
|
getScaledDimensions,
|
|
11
12
|
type ItemEvent,
|
|
12
13
|
movingFootprintAnchors,
|
|
14
|
+
type NodeEvent,
|
|
15
|
+
nodeRegistry,
|
|
13
16
|
type RoofEvent,
|
|
17
|
+
resolveFrozenFloorPlacementPatch,
|
|
14
18
|
resolveLevelId,
|
|
15
19
|
type ShelfEvent,
|
|
16
20
|
sceneRegistry,
|
|
21
|
+
useLiveNodeOverrides,
|
|
17
22
|
useLiveTransforms,
|
|
18
23
|
useScene,
|
|
19
24
|
useSpatialQuery,
|
|
@@ -55,6 +60,7 @@ import useAlignmentGuides from '../../../store/use-alignment-guides'
|
|
|
55
60
|
import useEditor, { isAlignmentGuideActive, isMagneticSnapActive } from '../../../store/use-editor'
|
|
56
61
|
|
|
57
62
|
import useFacingPose from '../../../store/use-facing-pose'
|
|
63
|
+
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
58
64
|
import { getFloorStackPreviewPosition } from '../shared/floor-stack-preview'
|
|
59
65
|
import {
|
|
60
66
|
createLineGeometry,
|
|
@@ -63,9 +69,17 @@ import {
|
|
|
63
69
|
updateLineGeometry,
|
|
64
70
|
} from '../shared/placement-box-geometry'
|
|
65
71
|
import {
|
|
72
|
+
type PointerSupportSurface,
|
|
66
73
|
resolvePointerSupportElevation,
|
|
67
74
|
resolvePointerSupportSurface,
|
|
68
75
|
} from '../shared/pointer-support-cap'
|
|
76
|
+
import { shouldCreateFloorDraft } from './draft-creation'
|
|
77
|
+
import { commitFaceHostClick, resolveFaceHostPreviewCommit } from './face-host-commit'
|
|
78
|
+
import {
|
|
79
|
+
applyFaceHostPreviewPose,
|
|
80
|
+
resolveFaceHostSwitch,
|
|
81
|
+
shouldDetachFaceHostOnLeave,
|
|
82
|
+
} from './face-host-preview'
|
|
69
83
|
import {
|
|
70
84
|
getDetachedAttachmentPreviewLift,
|
|
71
85
|
getGridAlignedDimensions,
|
|
@@ -77,12 +91,14 @@ import {
|
|
|
77
91
|
import {
|
|
78
92
|
ceilingStrategy,
|
|
79
93
|
checkCanPlace,
|
|
94
|
+
faceHostStrategy,
|
|
80
95
|
floorStrategy,
|
|
81
96
|
itemSurfaceStrategy,
|
|
82
97
|
roofWallStrategy,
|
|
83
98
|
shelfSurfaceStrategy,
|
|
84
99
|
wallStrategy,
|
|
85
100
|
} from './placement-strategies'
|
|
101
|
+
import { resolveItemPlacementSurfaceNormal } from './placement-surface'
|
|
86
102
|
import type { PlacementState, TransitionResult } from './placement-types'
|
|
87
103
|
import type { DraftNodeHandle } from './use-draft-node'
|
|
88
104
|
|
|
@@ -178,6 +194,27 @@ function getGridAlignedPreviewNode(item: ItemNode): ItemNode {
|
|
|
178
194
|
}
|
|
179
195
|
}
|
|
180
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Building-local Y of the storey the floor-path ghost belongs to.
|
|
199
|
+
*
|
|
200
|
+
* The cursor group is mounted inside ToolManager's building-local group, which
|
|
201
|
+
* carries no per-floor elevation, while every floor-path position (grid
|
|
202
|
+
* position, `getFloorVisualPosition`) is LEVEL-local — so on an upper storey the
|
|
203
|
+
* wireframe and its dimension labels render a floor too low. The wall / ceiling
|
|
204
|
+
* / item-surface paths don't need this: they convert a world hit through
|
|
205
|
+
* `worldToBuildingLocal`, which already carries the storey.
|
|
206
|
+
*
|
|
207
|
+
* Read off the level mesh (same source as `LevelOffsetGroup`) rather than the
|
|
208
|
+
* stored elevation so the ghost also follows the exploded-view lerp.
|
|
209
|
+
*/
|
|
210
|
+
function getPlacementLevelY(draft: ItemNode | null | undefined): number {
|
|
211
|
+
const levelId =
|
|
212
|
+
(draft ? findLevelAncestorId(draft.id, useScene.getState().nodes) : null) ??
|
|
213
|
+
useViewer.getState().selection.levelId
|
|
214
|
+
const levelMesh = levelId ? sceneRegistry.nodes.get(levelId as AnyNodeId) : null
|
|
215
|
+
return levelMesh ? levelMesh.position.y : 0
|
|
216
|
+
}
|
|
217
|
+
|
|
181
218
|
// Shared materials for placement cursor - we just change colors, not swap materials
|
|
182
219
|
// Note: EdgesGeometry doesn't work with dashed lines, so using solid lines
|
|
183
220
|
const edgeMaterial = new LineBasicNodeMaterial({
|
|
@@ -280,6 +317,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
280
317
|
// by the MAX overlapping slab and a deck above the aimed-at floor
|
|
281
318
|
// captures the item (and the grid-plane feedback makes it blink).
|
|
282
319
|
const pointerSupportCapRef = useRef<number | null>(null)
|
|
320
|
+
const pointerSupportSurfaceRef = useRef<PointerSupportSurface | null>(null)
|
|
321
|
+
const frozenSupportSlabIdRef = useRef<string | undefined>(undefined)
|
|
283
322
|
const [dimensionBounds, setDimensionBounds] = useState<PreviewBounds | null>(null)
|
|
284
323
|
|
|
285
324
|
// Live camera ref — the shelf-stickiness test reconstructs the cursor world
|
|
@@ -295,6 +334,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
295
334
|
const { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling } = useSpatialQuery()
|
|
296
335
|
const { asset, draftNode } = config
|
|
297
336
|
const unit = useViewer((state) => state.unit)
|
|
337
|
+
const metricNotation = useViewer((state) => state.metricNotation)
|
|
298
338
|
const gridSnapStep = useEditor((s) => s.gridSnapStep)
|
|
299
339
|
const updatePreviewGeometry = useCallback((bounds: PreviewBounds) => {
|
|
300
340
|
const [width, height, depth] = bounds.dimensions
|
|
@@ -426,17 +466,50 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
426
466
|
): [number, number, number] => {
|
|
427
467
|
const draft = draftNode.current
|
|
428
468
|
if (!(draft && !asset?.attachTo)) return position
|
|
429
|
-
const previewNode = getGridAlignedPreviewNode({
|
|
469
|
+
const previewNode = getGridAlignedPreviewNode({
|
|
470
|
+
...draft,
|
|
471
|
+
...nodeUpdate,
|
|
472
|
+
...(pointerSupportSurfaceRef.current?.sourceNodeId
|
|
473
|
+
? { supportSlabId: frozenSupportSlabIdRef.current }
|
|
474
|
+
: {}),
|
|
475
|
+
} as ItemNode)
|
|
430
476
|
return getFloorStackPreviewPosition({
|
|
431
477
|
node: previewNode,
|
|
432
478
|
position,
|
|
433
479
|
rotation: previewNode.rotation,
|
|
434
|
-
maxElevation:
|
|
480
|
+
maxElevation: pointerSupportSurfaceRef.current?.sourceNodeId
|
|
481
|
+
? null
|
|
482
|
+
: pointerSupportCapRef.current,
|
|
435
483
|
})
|
|
436
484
|
},
|
|
437
485
|
[asset?.attachTo, draftNode],
|
|
438
486
|
)
|
|
439
487
|
|
|
488
|
+
// Disable raycasting on the live draft mesh (and restore it when the draft
|
|
489
|
+
// changes or goes away) so the cursor ray passes through the item being
|
|
490
|
+
// moved and lands on the surface beneath it.
|
|
491
|
+
const reconcileDraftRaycast = useCallback((mesh: Object3D | null) => {
|
|
492
|
+
if (raycastDisabledMeshRef.current !== mesh) {
|
|
493
|
+
// New draft root (or cleared): restore the prior mesh and reset tracking.
|
|
494
|
+
for (const restore of restoreRaycastsRef.current) restore()
|
|
495
|
+
restoreRaycastsRef.current = []
|
|
496
|
+
raycastDisabledChildrenRef.current = new WeakSet()
|
|
497
|
+
raycastDisabledMeshRef.current = mesh
|
|
498
|
+
}
|
|
499
|
+
if (!mesh) return
|
|
500
|
+
// Item models can mount descendants asynchronously. Re-walk the root so
|
|
501
|
+
// newly mounted meshes cannot intercept the next pointer event.
|
|
502
|
+
mesh.traverse((child) => {
|
|
503
|
+
if (raycastDisabledChildrenRef.current.has(child)) return
|
|
504
|
+
raycastDisabledChildrenRef.current.add(child)
|
|
505
|
+
const original = child.raycast
|
|
506
|
+
child.raycast = () => {}
|
|
507
|
+
restoreRaycastsRef.current.push(() => {
|
|
508
|
+
child.raycast = original
|
|
509
|
+
})
|
|
510
|
+
})
|
|
511
|
+
}, [])
|
|
512
|
+
|
|
440
513
|
useEffect(() => {
|
|
441
514
|
if (!asset) return
|
|
442
515
|
useScene.temporal.getState().pause()
|
|
@@ -451,6 +524,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
451
524
|
// layer's frame.
|
|
452
525
|
let alignmentCandidates: AlignmentAnchor[] | null = null
|
|
453
526
|
let floorDragAnchor: [number, number] | null = null
|
|
527
|
+
let pendingFaceHostId: string | null = null
|
|
454
528
|
|
|
455
529
|
// Reset placement state
|
|
456
530
|
placementState.current = configRef.current.initialState ?? {
|
|
@@ -464,10 +538,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
464
538
|
// No pointer surface known yet — fall back to the uncapped election
|
|
465
539
|
// (an adopted move draft keeps its persisted host until the first move).
|
|
466
540
|
pointerSupportCapRef.current = null
|
|
541
|
+
pointerSupportSurfaceRef.current = null
|
|
542
|
+
frozenSupportSlabIdRef.current = undefined
|
|
467
543
|
if (!asset.attachTo && placementState.current.surface === 'floor') {
|
|
468
544
|
gridPosition.current.y = 0
|
|
469
545
|
if (cursorGroupRef.current) {
|
|
470
|
-
cursorGroupRef.current.position.y =
|
|
546
|
+
cursorGroupRef.current.position.y = getPlacementLevelY(draftNode.current)
|
|
471
547
|
}
|
|
472
548
|
}
|
|
473
549
|
|
|
@@ -492,6 +568,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
492
568
|
}
|
|
493
569
|
: validators
|
|
494
570
|
|
|
571
|
+
const disableDraftRaycastNow = () => {
|
|
572
|
+
const draft = draftNode.current
|
|
573
|
+
if (!draft) return
|
|
574
|
+
const mesh = sceneRegistry.nodes.get(draft.id)
|
|
575
|
+
if (mesh) reconcileDraftRaycast(mesh)
|
|
576
|
+
}
|
|
577
|
+
|
|
495
578
|
const finishCommittedPlacement = (
|
|
496
579
|
committedId: string | null,
|
|
497
580
|
wasAdopted: boolean,
|
|
@@ -516,6 +599,40 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
516
599
|
useEditor.getState().setMode('select')
|
|
517
600
|
}
|
|
518
601
|
|
|
602
|
+
const commitDraft = (
|
|
603
|
+
nodeUpdate: Partial<ItemNode>,
|
|
604
|
+
options?: {
|
|
605
|
+
supportElevationCap?: number | null
|
|
606
|
+
preferredSupportSlabId?: string | null
|
|
607
|
+
pinSupport?: boolean
|
|
608
|
+
},
|
|
609
|
+
) => {
|
|
610
|
+
const draftId = draftNode.current?.id ?? null
|
|
611
|
+
const wasAdopted = draftNode.isAdopted
|
|
612
|
+
const finalId = draftNode.commit(nodeUpdate, options)
|
|
613
|
+
if (draftId) {
|
|
614
|
+
useLiveTransforms.getState().clear(draftId)
|
|
615
|
+
useLiveNodeOverrides.getState().clearFields(draftId, faceHostClearFields(draftNode.current))
|
|
616
|
+
}
|
|
617
|
+
return { committedId: finalId ?? draftId, wasAdopted }
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const faceHostClearFields = (draft: ItemNode | null | undefined): Array<keyof ItemNode> => {
|
|
621
|
+
if (!draft?.parentId) return ['position', 'rotation']
|
|
622
|
+
const host = useScene.getState().nodes[draft.parentId as AnyNodeId]
|
|
623
|
+
return host
|
|
624
|
+
? [...(nodeRegistry.get(host.type)?.capabilities.faceHost?.clearItemFields ?? [])]
|
|
625
|
+
: ['position', 'rotation']
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const currentFaceHostId = (draft: ItemNode | null | undefined): string | null => {
|
|
629
|
+
if (!draft?.parentId) return null
|
|
630
|
+
const host = useScene.getState().nodes[draft.parentId as AnyNodeId]
|
|
631
|
+
return host
|
|
632
|
+
? (nodeRegistry.get(host.type)?.capabilities.faceHost?.currentFaceId(draft) ?? null)
|
|
633
|
+
: null
|
|
634
|
+
}
|
|
635
|
+
|
|
519
636
|
const revalidate = (): boolean => {
|
|
520
637
|
const placeable = altFreeRef.current || checkCanPlace(getContext(), validators)
|
|
521
638
|
const color = placeable ? 0x22_c5_5e : 0xef_44_44 // green-500 : red-500
|
|
@@ -539,6 +656,21 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
539
656
|
return buildingMesh ? buildingMesh.localToWorld(new Vector3(x, y, z)) : new Vector3(x, y, z)
|
|
540
657
|
}
|
|
541
658
|
|
|
659
|
+
const worldRotationToBuildingLocal = (
|
|
660
|
+
rotation: [number, number, number],
|
|
661
|
+
): [number, number, number] => {
|
|
662
|
+
const worldQuaternion = new Quaternion().setFromEuler(new Euler(...rotation))
|
|
663
|
+
const buildingId = useViewer.getState().selection.buildingId
|
|
664
|
+
const buildingMesh = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
|
|
665
|
+
if (buildingMesh) {
|
|
666
|
+
const buildingWorldQuaternion = new Quaternion()
|
|
667
|
+
buildingMesh.getWorldQuaternion(buildingWorldQuaternion)
|
|
668
|
+
worldQuaternion.premultiply(buildingWorldQuaternion.invert())
|
|
669
|
+
}
|
|
670
|
+
const localRotation = new Euler().setFromQuaternion(worldQuaternion, 'XYZ')
|
|
671
|
+
return [localRotation.x, localRotation.y, localRotation.z]
|
|
672
|
+
}
|
|
673
|
+
|
|
542
674
|
const applyTransition = (result: TransitionResult) => {
|
|
543
675
|
// Alignment guides are floor-only; clear them when the cursor moves
|
|
544
676
|
// onto a wall / ceiling / item surface (only those paths call this).
|
|
@@ -554,7 +686,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
554
686
|
if (cursorGroupRef.current) {
|
|
555
687
|
cursorGroupRef.current.position.set(c.x, c.y, c.z)
|
|
556
688
|
if (result.cursorRotation) {
|
|
557
|
-
cursorGroupRef.current.rotation.set(
|
|
689
|
+
cursorGroupRef.current.rotation.set(
|
|
690
|
+
...worldRotationToBuildingLocal(result.cursorRotation),
|
|
691
|
+
)
|
|
558
692
|
} else {
|
|
559
693
|
cursorGroupRef.current.rotation.set(0, result.cursorRotationY, 0)
|
|
560
694
|
}
|
|
@@ -574,7 +708,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
574
708
|
if (cursorGroupRef.current) {
|
|
575
709
|
cursorGroupRef.current.position.set(c.x, c.y, c.z)
|
|
576
710
|
if (result.cursorRotation) {
|
|
577
|
-
cursorGroupRef.current.rotation.set(
|
|
711
|
+
cursorGroupRef.current.rotation.set(
|
|
712
|
+
...worldRotationToBuildingLocal(result.cursorRotation),
|
|
713
|
+
)
|
|
578
714
|
} else {
|
|
579
715
|
cursorGroupRef.current.rotation.set(0, result.cursorRotationY, 0)
|
|
580
716
|
}
|
|
@@ -599,6 +735,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
599
735
|
Object.assign(draft, result.nodeUpdate)
|
|
600
736
|
// One-time setup: put node in the right parent so it renders correctly
|
|
601
737
|
useScene.getState().updateNode(draft.id, result.nodeUpdate)
|
|
738
|
+
disableDraftRaycastNow()
|
|
602
739
|
}
|
|
603
740
|
|
|
604
741
|
const previewBounds = expandBoundsToGrid(
|
|
@@ -774,7 +911,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
774
911
|
}
|
|
775
912
|
}
|
|
776
913
|
} else if (cursorGroupRef.current) {
|
|
914
|
+
// No registered mesh yet (a just-created draft renders next tick), so
|
|
915
|
+
// fall back to the level-local grid position lifted onto its storey.
|
|
777
916
|
cursorGroupRef.current.position.copy(gridPosition.current)
|
|
917
|
+
cursorGroupRef.current.position.y += getPlacementLevelY(draftNode.current)
|
|
778
918
|
cursorGroupRef.current.rotation.y = draftNode.current.rotation[1] ?? 0
|
|
779
919
|
}
|
|
780
920
|
}
|
|
@@ -857,7 +997,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
857
997
|
const onGridMove = (event: GridEvent) => {
|
|
858
998
|
releaseCommit = () => onGridClick(event)
|
|
859
999
|
// Lazy draft creation: if no draft yet (e.g. level wasn't ready during init), create now
|
|
860
|
-
if (
|
|
1000
|
+
if (
|
|
1001
|
+
shouldCreateFloorDraft(draftNode.current, asset.attachTo, placementState.current.surface)
|
|
1002
|
+
) {
|
|
861
1003
|
configRef.current.initDraft(gridPosition.current)
|
|
862
1004
|
}
|
|
863
1005
|
|
|
@@ -874,6 +1016,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
874
1016
|
// a drag over a deck-above-a-floor hop between the two surfaces).
|
|
875
1017
|
const pointed = resolvePointerSupportSurface(cameraRef.current, event.position)
|
|
876
1018
|
pointerSupportCapRef.current = pointed?.elevation ?? null
|
|
1019
|
+
pointerSupportSurfaceRef.current = pointed
|
|
877
1020
|
const surfaceEvent: GridEvent =
|
|
878
1021
|
pointed?.worldPoint && pointed.localPoint
|
|
879
1022
|
? { ...event, position: pointed.worldPoint, localPosition: pointed.localPoint }
|
|
@@ -945,11 +1088,37 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
945
1088
|
useAlignmentGuides.getState().clear()
|
|
946
1089
|
}
|
|
947
1090
|
|
|
948
|
-
|
|
1091
|
+
// `result.gridPosition[1]` is the LIVE `gridPosition.current.y` — seeded
|
|
1092
|
+
// from the draft's authored Y by `initDraft` (so a block-face / raised
|
|
1093
|
+
// construction-plane item keeps its height) and zeroed by
|
|
1094
|
+
// `detachItemSurfaceToFloor` / `faceHostStrategy.leave` when the item
|
|
1095
|
+
// comes back down. Freezing it at drag start instead left an item taken
|
|
1096
|
+
// off a shelf floating at the shelf's height.
|
|
1097
|
+
let gridPos: [number, number, number] = [
|
|
949
1098
|
result.gridPosition[0] + alignX,
|
|
950
1099
|
result.gridPosition[1],
|
|
951
1100
|
result.gridPosition[2] + alignZ,
|
|
952
1101
|
]
|
|
1102
|
+
frozenSupportSlabIdRef.current = undefined
|
|
1103
|
+
if (draft && pointed?.sourceNodeId) {
|
|
1104
|
+
const effectiveNode = {
|
|
1105
|
+
...draft,
|
|
1106
|
+
position: gridPos,
|
|
1107
|
+
parentId: useViewer.getState().selection.levelId ?? draft.parentId,
|
|
1108
|
+
} as ItemNode
|
|
1109
|
+
const frozenPatch = resolveFrozenFloorPlacementPatch(
|
|
1110
|
+
effectiveNode,
|
|
1111
|
+
useScene.getState().nodes,
|
|
1112
|
+
{
|
|
1113
|
+
position: gridPos,
|
|
1114
|
+
rotation: effectiveNode.rotation,
|
|
1115
|
+
elevation: pointed.elevation,
|
|
1116
|
+
preferredSlabId: pointed.supportSlabId,
|
|
1117
|
+
},
|
|
1118
|
+
)
|
|
1119
|
+
gridPos = frozenPatch.position
|
|
1120
|
+
frozenSupportSlabIdRef.current = frozenPatch.supportSlabId
|
|
1121
|
+
}
|
|
953
1122
|
|
|
954
1123
|
// Play snap sound when grid position changes
|
|
955
1124
|
if (
|
|
@@ -965,7 +1134,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
965
1134
|
if (!draft && asset.attachTo) {
|
|
966
1135
|
cursorPosition[1] += getDetachedAttachmentPreviewLift(asset.attachTo)
|
|
967
1136
|
}
|
|
968
|
-
cursorGroupRef.current.position.set(
|
|
1137
|
+
cursorGroupRef.current.position.set(
|
|
1138
|
+
cursorPosition[0],
|
|
1139
|
+
cursorPosition[1] + getPlacementLevelY(draft),
|
|
1140
|
+
cursorPosition[2],
|
|
1141
|
+
)
|
|
969
1142
|
// Floor items only rotate on Y; keep the preview box (and the live
|
|
970
1143
|
// transform the 2D floorplan mirrors) aligned with the draft's
|
|
971
1144
|
// rotation. Without this the box stays at its seed rotation until a
|
|
@@ -1001,20 +1174,15 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1001
1174
|
0,
|
|
1002
1175
|
]
|
|
1003
1176
|
|
|
1004
|
-
// Clear live transform before commit
|
|
1005
|
-
if (draftNode.current) {
|
|
1006
|
-
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
const committedId = draftNode.current?.id ?? null
|
|
1010
|
-
const wasAdopted = draftNode.isAdopted
|
|
1011
1177
|
// Carry the pointer surface cap into the commit so the persisted
|
|
1012
1178
|
// supportSlabId reproduces the capped election (elects the aimed-at
|
|
1013
1179
|
// lower slab — or the ground — instead of a deck hanging above).
|
|
1014
|
-
const
|
|
1180
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate, {
|
|
1015
1181
|
supportElevationCap: pointerSupportCapRef.current,
|
|
1182
|
+
preferredSupportSlabId: pointerSupportSurfaceRef.current?.supportSlabId,
|
|
1183
|
+
pinSupport: pointerSupportSurfaceRef.current?.sourceNodeId != null,
|
|
1016
1184
|
})
|
|
1017
|
-
finishCommittedPlacement(
|
|
1185
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1018
1186
|
draftNode.create(
|
|
1019
1187
|
gridPosition.current,
|
|
1020
1188
|
asset,
|
|
@@ -1129,19 +1297,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1129
1297
|
}
|
|
1130
1298
|
const correctedX = wallDragAnchor.startX + (rawX - wallDragAnchor.rawX)
|
|
1131
1299
|
const correctedY = wallDragAnchor.startY + (rawY - wallDragAnchor.rawY)
|
|
1132
|
-
const wallMesh = sceneRegistry.nodes.get(event.node.id)
|
|
1133
|
-
// Derive the world cursor from the corrected wall-local point so the
|
|
1134
|
-
// visual cursor (world) and the stored position (wall-local) agree; if
|
|
1135
|
-
// the wall mesh is somehow absent, keep the raw world hit unchanged.
|
|
1136
|
-
const correctedWorld = wallMesh
|
|
1137
|
-
? wallMesh.localToWorld(new Vector3(correctedX, correctedY, event.localPosition[2]))
|
|
1138
|
-
: null
|
|
1139
1300
|
wallMoveEvent = {
|
|
1140
1301
|
...event,
|
|
1141
1302
|
localPosition: [correctedX, correctedY, event.localPosition[2]],
|
|
1142
|
-
position: correctedWorld
|
|
1143
|
-
? [correctedWorld.x, correctedWorld.y, correctedWorld.z]
|
|
1144
|
-
: event.position,
|
|
1145
1303
|
}
|
|
1146
1304
|
}
|
|
1147
1305
|
const result = wallStrategy.move(ctx, wallMoveEvent, getActiveValidators())
|
|
@@ -1203,22 +1361,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1203
1361
|
|
|
1204
1362
|
// Publish live transform for the 2D floorplan. The floorplan resolves a
|
|
1205
1363
|
// wall item's footprint (and its wall-side depth offset) from this
|
|
1206
|
-
// rotation as a PLAN-space yaw
|
|
1207
|
-
//
|
|
1208
|
-
// raw flips the footprint to the far side of the wall during placement.
|
|
1209
|
-
// Publish the plan rotation (wall angle + the item's wall-local yaw) so
|
|
1210
|
-
// the preview matches what the committed node resolves to.
|
|
1211
|
-
let liveRotation = result.cursorRotationY
|
|
1212
|
-
const liveWallId = placementState.current.wallId
|
|
1213
|
-
const liveWall = liveWallId ? useScene.getState().nodes[liveWallId as AnyNodeId] : undefined
|
|
1214
|
-
if (liveWall?.type === 'wall') {
|
|
1215
|
-
const w = liveWall as WallNode
|
|
1216
|
-
const wallPlanRotation = -Math.atan2(w.end[1] - w.start[1], w.end[0] - w.start[0])
|
|
1217
|
-
liveRotation = wallPlanRotation + (draft.rotation[1] ?? 0)
|
|
1218
|
-
}
|
|
1364
|
+
// rotation as a PLAN-space yaw — which is exactly what the wall strategy
|
|
1365
|
+
// composes `cursorRotationY` as (wall yaw + the item's wall-local yaw).
|
|
1219
1366
|
useLiveTransforms.getState().set(draft.id, {
|
|
1220
1367
|
position: result.cursorPosition,
|
|
1221
|
-
rotation:
|
|
1368
|
+
rotation: result.cursorRotationY,
|
|
1222
1369
|
})
|
|
1223
1370
|
}
|
|
1224
1371
|
}
|
|
@@ -1228,18 +1375,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1228
1375
|
if (!result) return
|
|
1229
1376
|
|
|
1230
1377
|
event.stopPropagation()
|
|
1231
|
-
|
|
1232
|
-
if (draftNode.current) {
|
|
1233
|
-
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
1234
|
-
}
|
|
1235
|
-
const committedId = draftNode.current?.id ?? null
|
|
1236
|
-
const wasAdopted = draftNode.isAdopted
|
|
1237
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
1378
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1238
1379
|
if (result.dirtyNodeId) {
|
|
1239
1380
|
useScene.getState().dirtyNodes.add(result.dirtyNodeId)
|
|
1240
1381
|
}
|
|
1241
1382
|
|
|
1242
|
-
finishCommittedPlacement(
|
|
1383
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1243
1384
|
const nodes = useScene.getState().nodes
|
|
1244
1385
|
const enterResult = wallStrategy.enter(
|
|
1245
1386
|
getContext(),
|
|
@@ -1388,14 +1529,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1388
1529
|
if (!result) return
|
|
1389
1530
|
|
|
1390
1531
|
event.stopPropagation()
|
|
1391
|
-
|
|
1392
|
-
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
1393
|
-
}
|
|
1394
|
-
const committedId = draftNode.current?.id ?? null
|
|
1395
|
-
const wasAdopted = draftNode.isAdopted
|
|
1396
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
1532
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1397
1533
|
|
|
1398
|
-
finishCommittedPlacement(
|
|
1534
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1399
1535
|
const enterResult = roofWallStrategy.enter(getContext(), event, altFreeRef.current)
|
|
1400
1536
|
if (enterResult) {
|
|
1401
1537
|
applyTransition(enterResult)
|
|
@@ -1428,6 +1564,119 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1428
1564
|
}
|
|
1429
1565
|
}
|
|
1430
1566
|
|
|
1567
|
+
// ---- Face Host Handlers ----
|
|
1568
|
+
|
|
1569
|
+
const enterFaceHost = (event: NodeEvent): boolean => {
|
|
1570
|
+
const result = faceHostStrategy.enter(getContext(), event)
|
|
1571
|
+
if (!result) return false
|
|
1572
|
+
pendingFaceHostId = null
|
|
1573
|
+
event.stopPropagation()
|
|
1574
|
+
applyTransition(result)
|
|
1575
|
+
if (!draftNode.current) {
|
|
1576
|
+
ensureDraft(result)
|
|
1577
|
+
} else if (result.nodeUpdate.parentId) {
|
|
1578
|
+
useScene.getState().updateNode(draftNode.current.id, result.nodeUpdate)
|
|
1579
|
+
disableDraftRaycastNow()
|
|
1580
|
+
}
|
|
1581
|
+
if (draftNode.current) useLiveTransforms.getState().clear(draftNode.current.id)
|
|
1582
|
+
return true
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
const onFaceHostEnter = (event: NodeEvent) => {
|
|
1586
|
+
has3DPointerDrivenMoveRef.current = true
|
|
1587
|
+
enterFaceHost(event)
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
const onFaceHostMove = (event: NodeEvent) => {
|
|
1591
|
+
has3DPointerDrivenMoveRef.current = true
|
|
1592
|
+
if (!cursorGroupRef.current) return
|
|
1593
|
+
const ctx = getContext()
|
|
1594
|
+
if (ctx.state.surface !== 'block-face' || !draftNode.current) {
|
|
1595
|
+
if (enterFaceHost(event)) releaseCommit = () => onFaceHostClick(event)
|
|
1596
|
+
return
|
|
1597
|
+
}
|
|
1598
|
+
const result = faceHostStrategy.move(ctx, event)
|
|
1599
|
+
if (!result) {
|
|
1600
|
+
event.stopPropagation()
|
|
1601
|
+
return
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
event.stopPropagation()
|
|
1605
|
+
const draft = draftNode.current
|
|
1606
|
+
const nextFaceId = result.hostFaceId
|
|
1607
|
+
const faceSwitch = resolveFaceHostSwitch(
|
|
1608
|
+
currentFaceHostId(draft),
|
|
1609
|
+
nextFaceId,
|
|
1610
|
+
pendingFaceHostId,
|
|
1611
|
+
)
|
|
1612
|
+
pendingFaceHostId = faceSwitch.pendingFaceId
|
|
1613
|
+
if (!faceSwitch.accept) return
|
|
1614
|
+
releaseCommit = () => onFaceHostClick(event)
|
|
1615
|
+
|
|
1616
|
+
const posChanged =
|
|
1617
|
+
gridPosition.current.x !== result.gridPosition[0] ||
|
|
1618
|
+
gridPosition.current.y !== result.gridPosition[1] ||
|
|
1619
|
+
gridPosition.current.z !== result.gridPosition[2]
|
|
1620
|
+
if (posChanged) sfxEmitter.emit('sfx:grid-snap')
|
|
1621
|
+
gridPosition.current.set(...result.gridPosition)
|
|
1622
|
+
const cursor = worldToBuildingLocal(...result.cursorPosition)
|
|
1623
|
+
cursorGroupRef.current.position.copy(cursor)
|
|
1624
|
+
if (result.cursorRotation) {
|
|
1625
|
+
cursorGroupRef.current.rotation.set(...worldRotationToBuildingLocal(result.cursorRotation))
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
if (draft && result.nodeUpdate) {
|
|
1629
|
+
Object.assign(draft, result.nodeUpdate)
|
|
1630
|
+
const mesh = sceneRegistry.nodes.get(draft.id)
|
|
1631
|
+
const rotation = result.nodeUpdate.rotation ?? draft.rotation
|
|
1632
|
+
if (mesh) applyFaceHostPreviewPose(mesh, result.gridPosition, rotation)
|
|
1633
|
+
useLiveNodeOverrides.getState().set(draft.id, {
|
|
1634
|
+
position: result.gridPosition,
|
|
1635
|
+
rotation,
|
|
1636
|
+
...result.nodeUpdate,
|
|
1637
|
+
})
|
|
1638
|
+
}
|
|
1639
|
+
revalidate()
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
const onFaceHostClick = (event: NodeEvent) => {
|
|
1643
|
+
const outcome = commitFaceHostClick({
|
|
1644
|
+
commitDraft,
|
|
1645
|
+
enterFaceHost,
|
|
1646
|
+
event,
|
|
1647
|
+
getContext,
|
|
1648
|
+
})
|
|
1649
|
+
if (!outcome) return
|
|
1650
|
+
const { committedId, wasAdopted } = outcome
|
|
1651
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1652
|
+
const enterResult = faceHostStrategy.enter(getContext(), event)
|
|
1653
|
+
if (enterResult) applyTransition(enterResult)
|
|
1654
|
+
else revalidate()
|
|
1655
|
+
})
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
const onFaceHostLeave = (event: NodeEvent) => {
|
|
1659
|
+
pendingFaceHostId = null
|
|
1660
|
+
if (!shouldDetachFaceHostOnLeave(asset.attachTo)) {
|
|
1661
|
+
event.stopPropagation()
|
|
1662
|
+
return
|
|
1663
|
+
}
|
|
1664
|
+
const result = faceHostStrategy.leave(getContext())
|
|
1665
|
+
if (!result) return
|
|
1666
|
+
event.stopPropagation()
|
|
1667
|
+
const draft = draftNode.current
|
|
1668
|
+
if (draft) {
|
|
1669
|
+
useLiveNodeOverrides.getState().clearFields(draft.id, faceHostClearFields(draft))
|
|
1670
|
+
}
|
|
1671
|
+
if (draftNode.isAdopted) {
|
|
1672
|
+
applyTransition(result)
|
|
1673
|
+
if (draft) useScene.getState().updateNode(draft.id, result.nodeUpdate)
|
|
1674
|
+
} else {
|
|
1675
|
+
draftNode.destroy()
|
|
1676
|
+
Object.assign(placementState.current, result.stateUpdate)
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1431
1680
|
// ---- Item Surface Handlers ----
|
|
1432
1681
|
|
|
1433
1682
|
const detachItemSurfaceToFloor = (event: ItemEvent) => {
|
|
@@ -1469,7 +1718,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1469
1718
|
levelId ? { parentId: levelId } : undefined,
|
|
1470
1719
|
)
|
|
1471
1720
|
if (cursorGroupRef.current) {
|
|
1472
|
-
cursorGroupRef.current.position.set(
|
|
1721
|
+
cursorGroupRef.current.position.set(
|
|
1722
|
+
floorVisualPosition[0],
|
|
1723
|
+
floorVisualPosition[1] + getPlacementLevelY(draftNode.current),
|
|
1724
|
+
floorVisualPosition[2],
|
|
1725
|
+
)
|
|
1473
1726
|
}
|
|
1474
1727
|
|
|
1475
1728
|
const draft = draftNode.current
|
|
@@ -1633,6 +1886,15 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1633
1886
|
// have to aim around the cursor preview to drop the item.
|
|
1634
1887
|
if (event.node.id === draftNode.current?.id) {
|
|
1635
1888
|
const ctx = getContext()
|
|
1889
|
+
if (ctx.state.surface === 'block-face') {
|
|
1890
|
+
const result = resolveFaceHostPreviewCommit(ctx)
|
|
1891
|
+
if (result) {
|
|
1892
|
+
event.stopPropagation()
|
|
1893
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1894
|
+
finishCommittedPlacement(committedId, wasAdopted, revalidate)
|
|
1895
|
+
return
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1636
1898
|
if (ctx.state.surface === 'shelf-surface' && ctx.state.shelfId) {
|
|
1637
1899
|
const shelfNode = useScene.getState().nodes[ctx.state.shelfId as AnyNodeId]
|
|
1638
1900
|
if (shelfNode && shelfNode.type === 'shelf') {
|
|
@@ -1640,13 +1902,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1640
1902
|
const result = shelfSurfaceStrategy.click(ctx, synthetic as never)
|
|
1641
1903
|
if (result) {
|
|
1642
1904
|
event.stopPropagation()
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
}
|
|
1646
|
-
const committedId = draftNode.current?.id ?? null
|
|
1647
|
-
const wasAdopted = draftNode.isAdopted
|
|
1648
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
1649
|
-
finishCommittedPlacement(finalId ?? committedId, wasAdopted, () => {
|
|
1905
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1906
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1650
1907
|
const enterResult = shelfSurfaceStrategy.enter(ctx, synthetic as never)
|
|
1651
1908
|
if (enterResult) {
|
|
1652
1909
|
applyTransition(enterResult)
|
|
@@ -1668,13 +1925,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1668
1925
|
const result = itemSurfaceStrategy.click(ctx, synthetic)
|
|
1669
1926
|
if (result) {
|
|
1670
1927
|
event.stopPropagation()
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
}
|
|
1674
|
-
const committedId = draftNode.current?.id ?? null
|
|
1675
|
-
const wasAdopted = draftNode.isAdopted
|
|
1676
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
1677
|
-
finishCommittedPlacement(finalId ?? committedId, wasAdopted, () => {
|
|
1928
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1929
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1678
1930
|
const enterResult = itemSurfaceStrategy.enter(ctx, synthetic)
|
|
1679
1931
|
if (enterResult) {
|
|
1680
1932
|
applyTransition(enterResult)
|
|
@@ -1699,13 +1951,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1699
1951
|
const result = ceilingStrategy.click(ctx, synthetic, getActiveValidators())
|
|
1700
1952
|
if (result) {
|
|
1701
1953
|
event.stopPropagation()
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
}
|
|
1705
|
-
const committedId = draftNode.current?.id ?? null
|
|
1706
|
-
const wasAdopted = draftNode.isAdopted
|
|
1707
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
1708
|
-
finishCommittedPlacement(finalId ?? committedId, wasAdopted, () => {
|
|
1954
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1955
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1709
1956
|
const nodes = useScene.getState().nodes
|
|
1710
1957
|
const enterResult = ceilingStrategy.enter(
|
|
1711
1958
|
getContext(),
|
|
@@ -1730,15 +1977,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1730
1977
|
if (!result) return
|
|
1731
1978
|
|
|
1732
1979
|
event.stopPropagation()
|
|
1733
|
-
|
|
1734
|
-
if (draftNode.current) {
|
|
1735
|
-
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
1736
|
-
}
|
|
1737
|
-
const committedId = draftNode.current?.id ?? null
|
|
1738
|
-
const wasAdopted = draftNode.isAdopted
|
|
1739
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
1980
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1740
1981
|
|
|
1741
|
-
finishCommittedPlacement(
|
|
1982
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1742
1983
|
// Try to set up next draft on the same surface
|
|
1743
1984
|
const enterResult = itemSurfaceStrategy.enter(getContext(), event)
|
|
1744
1985
|
if (enterResult) {
|
|
@@ -1861,15 +2102,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1861
2102
|
if (!result) return
|
|
1862
2103
|
|
|
1863
2104
|
event.stopPropagation()
|
|
1864
|
-
|
|
1865
|
-
if (draftNode.current) {
|
|
1866
|
-
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
1867
|
-
}
|
|
1868
|
-
const committedId = draftNode.current?.id ?? null
|
|
1869
|
-
const wasAdopted = draftNode.isAdopted
|
|
1870
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
2105
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1871
2106
|
|
|
1872
|
-
finishCommittedPlacement(
|
|
2107
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1873
2108
|
const nodes = useScene.getState().nodes
|
|
1874
2109
|
const enterResult = ceilingStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
|
1875
2110
|
if (enterResult) {
|
|
@@ -2002,14 +2237,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2002
2237
|
if (!result) return
|
|
2003
2238
|
|
|
2004
2239
|
event.stopPropagation()
|
|
2005
|
-
|
|
2006
|
-
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
2007
|
-
}
|
|
2008
|
-
const committedId = draftNode.current?.id ?? null
|
|
2009
|
-
const wasAdopted = draftNode.isAdopted
|
|
2010
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
2240
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
2011
2241
|
|
|
2012
|
-
finishCommittedPlacement(
|
|
2242
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
2013
2243
|
const enterResult = shelfSurfaceStrategy.enter(getContext(), event)
|
|
2014
2244
|
if (enterResult) {
|
|
2015
2245
|
applyTransition(enterResult)
|
|
@@ -2038,7 +2268,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2038
2268
|
|
|
2039
2269
|
// Roof-wall drafts live flat in the host face frame (yaw 0) —
|
|
2040
2270
|
// manual rotation would skew them off the wall plane.
|
|
2041
|
-
if (
|
|
2271
|
+
if (
|
|
2272
|
+
placementState.current.surface === 'roof-wall' ||
|
|
2273
|
+
placementState.current.surface === 'block-face'
|
|
2274
|
+
)
|
|
2275
|
+
return
|
|
2042
2276
|
|
|
2043
2277
|
let rotationDir: 1 | -1 | 0 = 0
|
|
2044
2278
|
if ((event.key === 'r' || event.key === 'R') && !event.metaKey && !event.ctrlKey)
|
|
@@ -2054,9 +2288,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2054
2288
|
const newRotationY = steppedRotation(currentRotation[1] ?? 0, rotationDir)
|
|
2055
2289
|
draft.rotation = [currentRotation[0], newRotationY, currentRotation[2]]
|
|
2056
2290
|
|
|
2057
|
-
//
|
|
2291
|
+
// Rotate the building-local cursor by the same delta as the host-local
|
|
2292
|
+
// draft. This preserves the host's composed yaw for items resting on a
|
|
2293
|
+
// table or shelf while still matching the draft exactly on the floor.
|
|
2058
2294
|
if (cursorGroupRef.current) {
|
|
2059
|
-
cursorGroupRef.current.rotation.y
|
|
2295
|
+
cursorGroupRef.current.rotation.y += newRotationY - currentRotation[1]
|
|
2060
2296
|
}
|
|
2061
2297
|
const mesh = sceneRegistry.nodes.get(draft.id)
|
|
2062
2298
|
if (mesh) mesh.rotation.y = newRotationY
|
|
@@ -2073,8 +2309,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2073
2309
|
draft.position = [x, gridPosition.current.y, z]
|
|
2074
2310
|
if (cursorGroupRef.current) {
|
|
2075
2311
|
if (surface === 'floor') {
|
|
2312
|
+
const visual = getFloorVisualPosition([x, gridPosition.current.y, z])
|
|
2076
2313
|
cursorGroupRef.current.position.set(
|
|
2077
|
-
|
|
2314
|
+
visual[0],
|
|
2315
|
+
visual[1] + getPlacementLevelY(draft),
|
|
2316
|
+
visual[2],
|
|
2078
2317
|
)
|
|
2079
2318
|
} else {
|
|
2080
2319
|
cursorGroupRef.current.position.x = x
|
|
@@ -2120,24 +2359,40 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2120
2359
|
}
|
|
2121
2360
|
}
|
|
2122
2361
|
|
|
2123
|
-
//
|
|
2362
|
+
// Keep both preview renderers on the rotated draft. The item renderer
|
|
2363
|
+
// consumes live node overrides, while the floor-plan renderer consumes
|
|
2364
|
+
// the live transform and placement-preview snapshot.
|
|
2365
|
+
useLiveNodeOverrides.getState().set(draft.id, { rotation: draft.rotation })
|
|
2124
2366
|
const currentLive = useLiveTransforms.getState().get(draft.id)
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2367
|
+
const livePosition: [number, number, number] =
|
|
2368
|
+
surface === 'floor'
|
|
2369
|
+
? [draft.position[0], draft.position[1], draft.position[2]]
|
|
2370
|
+
: cursorGroupRef.current
|
|
2371
|
+
? [
|
|
2372
|
+
cursorGroupRef.current.position.x,
|
|
2373
|
+
cursorGroupRef.current.position.y,
|
|
2374
|
+
cursorGroupRef.current.position.z,
|
|
2375
|
+
]
|
|
2376
|
+
: [draft.position[0], draft.position[1], draft.position[2]]
|
|
2377
|
+
useLiveTransforms.getState().set(draft.id, {
|
|
2378
|
+
...currentLive,
|
|
2379
|
+
position: livePosition,
|
|
2380
|
+
rotation: cursorGroupRef.current?.rotation.y ?? newRotationY,
|
|
2381
|
+
})
|
|
2382
|
+
|
|
2383
|
+
const placementPreview = usePlacementPreview.getState()
|
|
2384
|
+
if (placementPreview.node?.id === draft.id) {
|
|
2385
|
+
const parentNode = draft.parentId
|
|
2386
|
+
? (useScene.getState().nodes[draft.parentId as AnyNodeId] ?? null)
|
|
2387
|
+
: null
|
|
2388
|
+
placementPreview.set(
|
|
2389
|
+
{
|
|
2390
|
+
...draft,
|
|
2391
|
+
position: [...draft.position],
|
|
2392
|
+
rotation: [...draft.rotation],
|
|
2393
|
+
},
|
|
2394
|
+
parentNode,
|
|
2395
|
+
)
|
|
2141
2396
|
}
|
|
2142
2397
|
|
|
2143
2398
|
revalidate()
|
|
@@ -2248,6 +2503,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2248
2503
|
emitter.on('roof:move', onRoofWallMove)
|
|
2249
2504
|
emitter.on('roof:click', onRoofWallClick)
|
|
2250
2505
|
emitter.on('roof:leave', onRoofWallLeave)
|
|
2506
|
+
emitter.on('node:enter', onFaceHostEnter)
|
|
2507
|
+
emitter.on('node:move', onFaceHostMove)
|
|
2508
|
+
emitter.on('node:click', onFaceHostClick)
|
|
2509
|
+
emitter.on('node:leave', onFaceHostLeave)
|
|
2251
2510
|
emitter.on('ceiling:enter', onCeilingEnter)
|
|
2252
2511
|
emitter.on('ceiling:move', onCeilingMove)
|
|
2253
2512
|
emitter.on('ceiling:click', onCeilingClick)
|
|
@@ -2270,11 +2529,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2270
2529
|
if (placementState.current.surface !== 'floor') return
|
|
2271
2530
|
onGridClick(event as unknown as GridEvent)
|
|
2272
2531
|
}
|
|
2273
|
-
emitter.on('
|
|
2274
|
-
emitter.on('item:click', commitFloorOnSurfaceClick as never)
|
|
2275
|
-
emitter.on('ceiling:click', commitFloorOnSurfaceClick as never)
|
|
2276
|
-
emitter.on('roof:click', commitFloorOnSurfaceClick as never)
|
|
2277
|
-
emitter.on('shelf:click', commitFloorOnSurfaceClick as never)
|
|
2532
|
+
emitter.on('node:click', commitFloorOnSurfaceClick as never)
|
|
2278
2533
|
if (dragMode) window.addEventListener('pointerup', onReleaseCommit)
|
|
2279
2534
|
|
|
2280
2535
|
return () => {
|
|
@@ -2282,9 +2537,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2282
2537
|
if (dragMode) window.removeEventListener('pointerup', onReleaseCommit)
|
|
2283
2538
|
unsubDraftWatch()
|
|
2284
2539
|
useAlignmentGuides.getState().clear()
|
|
2285
|
-
// Clear live
|
|
2540
|
+
// Clear every live preview channel before restoring or deleting the draft.
|
|
2286
2541
|
if (draftNode.current) {
|
|
2287
2542
|
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
2543
|
+
useLiveNodeOverrides.getState().clearFields(draftNode.current.id, ['rotation'])
|
|
2288
2544
|
}
|
|
2289
2545
|
draftNode.destroy()
|
|
2290
2546
|
useScene.temporal.getState().resume()
|
|
@@ -2302,6 +2558,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2302
2558
|
emitter.off('roof:move', onRoofWallMove)
|
|
2303
2559
|
emitter.off('roof:click', onRoofWallClick)
|
|
2304
2560
|
emitter.off('roof:leave', onRoofWallLeave)
|
|
2561
|
+
emitter.off('node:enter', onFaceHostEnter)
|
|
2562
|
+
emitter.off('node:move', onFaceHostMove)
|
|
2563
|
+
emitter.off('node:click', onFaceHostClick)
|
|
2564
|
+
emitter.off('node:leave', onFaceHostLeave)
|
|
2305
2565
|
emitter.off('ceiling:enter', onCeilingEnter)
|
|
2306
2566
|
emitter.off('ceiling:move', onCeilingMove)
|
|
2307
2567
|
emitter.off('ceiling:click', onCeilingClick)
|
|
@@ -2310,11 +2570,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2310
2570
|
emitter.off('shelf:move', onShelfMove)
|
|
2311
2571
|
emitter.off('shelf:click', onShelfClick)
|
|
2312
2572
|
emitter.off('shelf:leave', onShelfLeave)
|
|
2313
|
-
emitter.off('
|
|
2314
|
-
emitter.off('item:click', commitFloorOnSurfaceClick as never)
|
|
2315
|
-
emitter.off('ceiling:click', commitFloorOnSurfaceClick as never)
|
|
2316
|
-
emitter.off('roof:click', commitFloorOnSurfaceClick as never)
|
|
2317
|
-
emitter.off('shelf:click', commitFloorOnSurfaceClick as never)
|
|
2573
|
+
emitter.off('node:click', commitFloorOnSurfaceClick as never)
|
|
2318
2574
|
emitter.off('tool:cancel', onCancel)
|
|
2319
2575
|
window.removeEventListener('keydown', onKeyDown)
|
|
2320
2576
|
window.removeEventListener('keyup', onKeyUp)
|
|
@@ -2332,6 +2588,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2332
2588
|
gridSnapStep,
|
|
2333
2589
|
updateDimensionGuides,
|
|
2334
2590
|
updatePreviewGeometry,
|
|
2591
|
+
reconcileDraftRaycast,
|
|
2335
2592
|
])
|
|
2336
2593
|
|
|
2337
2594
|
// Refresh wireframe when the grid step changes mid-placement so the green/red
|
|
@@ -2363,39 +2620,17 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2363
2620
|
const draftParent = draft.parentId
|
|
2364
2621
|
? useScene.getState().nodes[draft.parentId as AnyNodeId]
|
|
2365
2622
|
: undefined
|
|
2366
|
-
if (
|
|
2623
|
+
if (
|
|
2624
|
+
draftParent?.type === 'item' ||
|
|
2625
|
+
draftParent?.type === 'shelf' ||
|
|
2626
|
+
(draftParent &&
|
|
2627
|
+
nodeRegistry.get(draftParent.type)?.capabilities.faceHost?.currentFaceId(draft))
|
|
2628
|
+
)
|
|
2629
|
+
return
|
|
2367
2630
|
draft.parentId = viewerLevelId
|
|
2368
2631
|
useScene.getState().updateNode(draft.id as AnyNodeId, { parentId: viewerLevelId })
|
|
2369
2632
|
}, [viewerLevelId, draftNode, asset])
|
|
2370
2633
|
|
|
2371
|
-
// Disable raycasting on the live draft mesh (and restore it when the draft
|
|
2372
|
-
// changes or goes away) so the cursor ray passes through the item being
|
|
2373
|
-
// moved and lands on the surface beneath it.
|
|
2374
|
-
const reconcileDraftRaycast = useCallback((mesh: Object3D | null) => {
|
|
2375
|
-
if (raycastDisabledMeshRef.current !== mesh) {
|
|
2376
|
-
// New draft root (or cleared): restore the prior mesh and reset tracking.
|
|
2377
|
-
for (const restore of restoreRaycastsRef.current) restore()
|
|
2378
|
-
restoreRaycastsRef.current = []
|
|
2379
|
-
raycastDisabledChildrenRef.current = new WeakSet()
|
|
2380
|
-
raycastDisabledMeshRef.current = mesh
|
|
2381
|
-
}
|
|
2382
|
-
if (!mesh) return
|
|
2383
|
-
// Disable any descendant not handled yet. Item drafts are GLB models whose
|
|
2384
|
-
// child meshes mount asynchronously (Suspense), so a one-shot traverse
|
|
2385
|
-
// misses them — those late children keep intercepting the ray and corrupt
|
|
2386
|
-
// the shelf-row hit the moment the item moves onto a row. Re-walking each
|
|
2387
|
-
// frame is cheap: the WeakSet makes it idempotent, so only new children pay.
|
|
2388
|
-
mesh.traverse((child) => {
|
|
2389
|
-
if (raycastDisabledChildrenRef.current.has(child)) return
|
|
2390
|
-
raycastDisabledChildrenRef.current.add(child)
|
|
2391
|
-
const original = child.raycast
|
|
2392
|
-
child.raycast = () => {}
|
|
2393
|
-
restoreRaycastsRef.current.push(() => {
|
|
2394
|
-
child.raycast = original
|
|
2395
|
-
})
|
|
2396
|
-
})
|
|
2397
|
-
}, [])
|
|
2398
|
-
|
|
2399
2634
|
// Restore the draft mesh's raycast when the coordinator unmounts (tool change).
|
|
2400
2635
|
useEffect(() => () => reconcileDraftRaycast(null), [reconcileDraftRaycast])
|
|
2401
2636
|
|
|
@@ -2407,8 +2642,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2407
2642
|
// moving existing node has no draft here, so the grid reads that case straight
|
|
2408
2643
|
// off the node's mesh. Cleared when idle.
|
|
2409
2644
|
const surfaceNormalRef = useRef(new Vector3(0, 1, 0))
|
|
2645
|
+
const surfaceWorldPointRef = useRef(new Vector3())
|
|
2410
2646
|
const facingForwardRef = useRef(new Vector3(0, 0, 1))
|
|
2411
2647
|
const facingQuatRef = useRef(new Quaternion())
|
|
2648
|
+
const ghostSurfaceQuatRef = useRef(new Quaternion())
|
|
2412
2649
|
useFrame(() => {
|
|
2413
2650
|
const ghost = cursorGroupRef.current
|
|
2414
2651
|
if (!(asset && ghost)) {
|
|
@@ -2423,7 +2660,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2423
2660
|
// the item's forward on the floor, and the triangle rides at the ghost's Y.
|
|
2424
2661
|
let facingYaw = ghost.rotation.y
|
|
2425
2662
|
let facingY = ghost.position.y
|
|
2426
|
-
if (surf === 'wall' || surf === 'roof-wall') {
|
|
2663
|
+
if (surf === 'wall' || surf === 'roof-wall' || surf === 'block-face') {
|
|
2427
2664
|
// Wall/roof-segment faces: the cursor group's yaw is the symmetric
|
|
2428
2665
|
// wireframe yaw (π off the real facing for a wall, and a different frame
|
|
2429
2666
|
// for a roof face), so derive the item's TRUE outward facing from the
|
|
@@ -2431,21 +2668,31 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2431
2668
|
// surface. This keeps BOTH the grid normal and the triangle correct for
|
|
2432
2669
|
// wall and roof-segment hosts alike, rather than the old quaternion read
|
|
2433
2670
|
// that pointed the wrong way.
|
|
2434
|
-
const mesh =
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2671
|
+
const mesh =
|
|
2672
|
+
surf === 'block-face' || !draftNode.current
|
|
2673
|
+
? null
|
|
2674
|
+
: sceneRegistry.nodes.get(draftNode.current.id)
|
|
2675
|
+
ghost.getWorldQuaternion(ghostSurfaceQuatRef.current)
|
|
2676
|
+
const hostedQuaternion = mesh ? mesh.getWorldQuaternion(facingQuatRef.current) : null
|
|
2677
|
+
resolveItemPlacementSurfaceNormal(
|
|
2678
|
+
surf,
|
|
2679
|
+
ghostSurfaceQuatRef.current,
|
|
2680
|
+
hostedQuaternion,
|
|
2681
|
+
n,
|
|
2682
|
+
asset.attachTo,
|
|
2683
|
+
)
|
|
2684
|
+
const fwd = facingForwardRef.current.copy(n)
|
|
2685
|
+
if (fwd.lengthSq() > 1e-6) facingYaw = Math.atan2(fwd.x, fwd.z)
|
|
2441
2686
|
// The forward triangle is a floor aid; drop it to the building-local floor
|
|
2442
|
-
// under the
|
|
2443
|
-
facingY =
|
|
2444
|
-
n.set(Math.sin(facingYaw), 0, Math.cos(facingYaw))
|
|
2687
|
+
// under the hosted plane — the storey's floor, not world ground.
|
|
2688
|
+
facingY = getPlacementLevelY(draftNode.current)
|
|
2445
2689
|
} else {
|
|
2446
|
-
|
|
2690
|
+
ghost.getWorldQuaternion(ghostSurfaceQuatRef.current)
|
|
2691
|
+
resolveItemPlacementSurfaceNormal(surf, ghostSurfaceQuatRef.current, null, n)
|
|
2447
2692
|
}
|
|
2448
|
-
publishPlacementSurface(
|
|
2693
|
+
// `publishPlacementSurface` is a WORLD-space contract (the grid reads it in
|
|
2694
|
+
// world space), but the ghost lives in the building-local tool group.
|
|
2695
|
+
publishPlacementSurface(ghost.getWorldPosition(surfaceWorldPointRef.current), n)
|
|
2449
2696
|
|
|
2450
2697
|
if (shape.depth > 0) {
|
|
2451
2698
|
useFacingPose.getState().set({
|
|
@@ -2513,8 +2760,16 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2513
2760
|
gridPosition.current.z,
|
|
2514
2761
|
])
|
|
2515
2762
|
mesh.position.y = visualPosition[1]
|
|
2516
|
-
cursorGroupRef.current.position.y =
|
|
2517
|
-
|
|
2763
|
+
cursorGroupRef.current.position.y =
|
|
2764
|
+
visualPosition[1] + getPlacementLevelY(draftNode.current)
|
|
2765
|
+
}
|
|
2766
|
+
} else if (placementState.current.surface === 'block-face') {
|
|
2767
|
+
const rotation = draftNode.current.rotation
|
|
2768
|
+
applyFaceHostPreviewPose(
|
|
2769
|
+
mesh,
|
|
2770
|
+
[gridPosition.current.x, gridPosition.current.y, gridPosition.current.z],
|
|
2771
|
+
rotation,
|
|
2772
|
+
)
|
|
2518
2773
|
}
|
|
2519
2774
|
})
|
|
2520
2775
|
|
|
@@ -2558,9 +2813,21 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2558
2813
|
depth: currentDimensionBounds.dimensions[2],
|
|
2559
2814
|
center: [currentDimensionBounds.center[0], currentDimensionBounds.center[2]],
|
|
2560
2815
|
}
|
|
2561
|
-
const widthLabel = formatLinearMeasurement(
|
|
2562
|
-
|
|
2563
|
-
|
|
2816
|
+
const widthLabel = formatLinearMeasurement(
|
|
2817
|
+
currentDimensionBounds.dimensions[0],
|
|
2818
|
+
unit,
|
|
2819
|
+
metricNotation,
|
|
2820
|
+
)
|
|
2821
|
+
const depthLabel = formatLinearMeasurement(
|
|
2822
|
+
currentDimensionBounds.dimensions[2],
|
|
2823
|
+
unit,
|
|
2824
|
+
metricNotation,
|
|
2825
|
+
)
|
|
2826
|
+
const heightLabel = formatLinearMeasurement(
|
|
2827
|
+
currentDimensionBounds.dimensions[1],
|
|
2828
|
+
unit,
|
|
2829
|
+
metricNotation,
|
|
2830
|
+
)
|
|
2564
2831
|
const widthLabelPosition: [number, number, number] = [
|
|
2565
2832
|
currentDimensionBounds.center[0],
|
|
2566
2833
|
0.04,
|