@pascal-app/editor 1.0.0-beta.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +284 -237
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +180 -32
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +135 -2
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +9 -12
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +22 -5
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +1 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -6,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
|
|
@@ -427,17 +466,50 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
427
466
|
): [number, number, number] => {
|
|
428
467
|
const draft = draftNode.current
|
|
429
468
|
if (!(draft && !asset?.attachTo)) return position
|
|
430
|
-
const previewNode = getGridAlignedPreviewNode({
|
|
469
|
+
const previewNode = getGridAlignedPreviewNode({
|
|
470
|
+
...draft,
|
|
471
|
+
...nodeUpdate,
|
|
472
|
+
...(pointerSupportSurfaceRef.current?.sourceNodeId
|
|
473
|
+
? { supportSlabId: frozenSupportSlabIdRef.current }
|
|
474
|
+
: {}),
|
|
475
|
+
} as ItemNode)
|
|
431
476
|
return getFloorStackPreviewPosition({
|
|
432
477
|
node: previewNode,
|
|
433
478
|
position,
|
|
434
479
|
rotation: previewNode.rotation,
|
|
435
|
-
maxElevation:
|
|
480
|
+
maxElevation: pointerSupportSurfaceRef.current?.sourceNodeId
|
|
481
|
+
? null
|
|
482
|
+
: pointerSupportCapRef.current,
|
|
436
483
|
})
|
|
437
484
|
},
|
|
438
485
|
[asset?.attachTo, draftNode],
|
|
439
486
|
)
|
|
440
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
|
+
|
|
441
513
|
useEffect(() => {
|
|
442
514
|
if (!asset) return
|
|
443
515
|
useScene.temporal.getState().pause()
|
|
@@ -452,6 +524,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
452
524
|
// layer's frame.
|
|
453
525
|
let alignmentCandidates: AlignmentAnchor[] | null = null
|
|
454
526
|
let floorDragAnchor: [number, number] | null = null
|
|
527
|
+
let pendingFaceHostId: string | null = null
|
|
455
528
|
|
|
456
529
|
// Reset placement state
|
|
457
530
|
placementState.current = configRef.current.initialState ?? {
|
|
@@ -465,10 +538,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
465
538
|
// No pointer surface known yet — fall back to the uncapped election
|
|
466
539
|
// (an adopted move draft keeps its persisted host until the first move).
|
|
467
540
|
pointerSupportCapRef.current = null
|
|
541
|
+
pointerSupportSurfaceRef.current = null
|
|
542
|
+
frozenSupportSlabIdRef.current = undefined
|
|
468
543
|
if (!asset.attachTo && placementState.current.surface === 'floor') {
|
|
469
544
|
gridPosition.current.y = 0
|
|
470
545
|
if (cursorGroupRef.current) {
|
|
471
|
-
cursorGroupRef.current.position.y =
|
|
546
|
+
cursorGroupRef.current.position.y = getPlacementLevelY(draftNode.current)
|
|
472
547
|
}
|
|
473
548
|
}
|
|
474
549
|
|
|
@@ -493,6 +568,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
493
568
|
}
|
|
494
569
|
: validators
|
|
495
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
|
+
|
|
496
578
|
const finishCommittedPlacement = (
|
|
497
579
|
committedId: string | null,
|
|
498
580
|
wasAdopted: boolean,
|
|
@@ -517,6 +599,40 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
517
599
|
useEditor.getState().setMode('select')
|
|
518
600
|
}
|
|
519
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
|
+
|
|
520
636
|
const revalidate = (): boolean => {
|
|
521
637
|
const placeable = altFreeRef.current || checkCanPlace(getContext(), validators)
|
|
522
638
|
const color = placeable ? 0x22_c5_5e : 0xef_44_44 // green-500 : red-500
|
|
@@ -540,6 +656,21 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
540
656
|
return buildingMesh ? buildingMesh.localToWorld(new Vector3(x, y, z)) : new Vector3(x, y, z)
|
|
541
657
|
}
|
|
542
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
|
+
|
|
543
674
|
const applyTransition = (result: TransitionResult) => {
|
|
544
675
|
// Alignment guides are floor-only; clear them when the cursor moves
|
|
545
676
|
// onto a wall / ceiling / item surface (only those paths call this).
|
|
@@ -555,7 +686,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
555
686
|
if (cursorGroupRef.current) {
|
|
556
687
|
cursorGroupRef.current.position.set(c.x, c.y, c.z)
|
|
557
688
|
if (result.cursorRotation) {
|
|
558
|
-
cursorGroupRef.current.rotation.set(
|
|
689
|
+
cursorGroupRef.current.rotation.set(
|
|
690
|
+
...worldRotationToBuildingLocal(result.cursorRotation),
|
|
691
|
+
)
|
|
559
692
|
} else {
|
|
560
693
|
cursorGroupRef.current.rotation.set(0, result.cursorRotationY, 0)
|
|
561
694
|
}
|
|
@@ -575,7 +708,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
575
708
|
if (cursorGroupRef.current) {
|
|
576
709
|
cursorGroupRef.current.position.set(c.x, c.y, c.z)
|
|
577
710
|
if (result.cursorRotation) {
|
|
578
|
-
cursorGroupRef.current.rotation.set(
|
|
711
|
+
cursorGroupRef.current.rotation.set(
|
|
712
|
+
...worldRotationToBuildingLocal(result.cursorRotation),
|
|
713
|
+
)
|
|
579
714
|
} else {
|
|
580
715
|
cursorGroupRef.current.rotation.set(0, result.cursorRotationY, 0)
|
|
581
716
|
}
|
|
@@ -600,6 +735,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
600
735
|
Object.assign(draft, result.nodeUpdate)
|
|
601
736
|
// One-time setup: put node in the right parent so it renders correctly
|
|
602
737
|
useScene.getState().updateNode(draft.id, result.nodeUpdate)
|
|
738
|
+
disableDraftRaycastNow()
|
|
603
739
|
}
|
|
604
740
|
|
|
605
741
|
const previewBounds = expandBoundsToGrid(
|
|
@@ -775,7 +911,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
775
911
|
}
|
|
776
912
|
}
|
|
777
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.
|
|
778
916
|
cursorGroupRef.current.position.copy(gridPosition.current)
|
|
917
|
+
cursorGroupRef.current.position.y += getPlacementLevelY(draftNode.current)
|
|
779
918
|
cursorGroupRef.current.rotation.y = draftNode.current.rotation[1] ?? 0
|
|
780
919
|
}
|
|
781
920
|
}
|
|
@@ -858,7 +997,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
858
997
|
const onGridMove = (event: GridEvent) => {
|
|
859
998
|
releaseCommit = () => onGridClick(event)
|
|
860
999
|
// Lazy draft creation: if no draft yet (e.g. level wasn't ready during init), create now
|
|
861
|
-
if (
|
|
1000
|
+
if (
|
|
1001
|
+
shouldCreateFloorDraft(draftNode.current, asset.attachTo, placementState.current.surface)
|
|
1002
|
+
) {
|
|
862
1003
|
configRef.current.initDraft(gridPosition.current)
|
|
863
1004
|
}
|
|
864
1005
|
|
|
@@ -875,6 +1016,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
875
1016
|
// a drag over a deck-above-a-floor hop between the two surfaces).
|
|
876
1017
|
const pointed = resolvePointerSupportSurface(cameraRef.current, event.position)
|
|
877
1018
|
pointerSupportCapRef.current = pointed?.elevation ?? null
|
|
1019
|
+
pointerSupportSurfaceRef.current = pointed
|
|
878
1020
|
const surfaceEvent: GridEvent =
|
|
879
1021
|
pointed?.worldPoint && pointed.localPoint
|
|
880
1022
|
? { ...event, position: pointed.worldPoint, localPosition: pointed.localPoint }
|
|
@@ -946,11 +1088,37 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
946
1088
|
useAlignmentGuides.getState().clear()
|
|
947
1089
|
}
|
|
948
1090
|
|
|
949
|
-
|
|
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] = [
|
|
950
1098
|
result.gridPosition[0] + alignX,
|
|
951
1099
|
result.gridPosition[1],
|
|
952
1100
|
result.gridPosition[2] + alignZ,
|
|
953
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
|
+
}
|
|
954
1122
|
|
|
955
1123
|
// Play snap sound when grid position changes
|
|
956
1124
|
if (
|
|
@@ -966,7 +1134,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
966
1134
|
if (!draft && asset.attachTo) {
|
|
967
1135
|
cursorPosition[1] += getDetachedAttachmentPreviewLift(asset.attachTo)
|
|
968
1136
|
}
|
|
969
|
-
cursorGroupRef.current.position.set(
|
|
1137
|
+
cursorGroupRef.current.position.set(
|
|
1138
|
+
cursorPosition[0],
|
|
1139
|
+
cursorPosition[1] + getPlacementLevelY(draft),
|
|
1140
|
+
cursorPosition[2],
|
|
1141
|
+
)
|
|
970
1142
|
// Floor items only rotate on Y; keep the preview box (and the live
|
|
971
1143
|
// transform the 2D floorplan mirrors) aligned with the draft's
|
|
972
1144
|
// rotation. Without this the box stays at its seed rotation until a
|
|
@@ -1002,20 +1174,15 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1002
1174
|
0,
|
|
1003
1175
|
]
|
|
1004
1176
|
|
|
1005
|
-
// Clear live transform before commit
|
|
1006
|
-
if (draftNode.current) {
|
|
1007
|
-
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
const committedId = draftNode.current?.id ?? null
|
|
1011
|
-
const wasAdopted = draftNode.isAdopted
|
|
1012
1177
|
// Carry the pointer surface cap into the commit so the persisted
|
|
1013
1178
|
// supportSlabId reproduces the capped election (elects the aimed-at
|
|
1014
1179
|
// lower slab — or the ground — instead of a deck hanging above).
|
|
1015
|
-
const
|
|
1180
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate, {
|
|
1016
1181
|
supportElevationCap: pointerSupportCapRef.current,
|
|
1182
|
+
preferredSupportSlabId: pointerSupportSurfaceRef.current?.supportSlabId,
|
|
1183
|
+
pinSupport: pointerSupportSurfaceRef.current?.sourceNodeId != null,
|
|
1017
1184
|
})
|
|
1018
|
-
finishCommittedPlacement(
|
|
1185
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1019
1186
|
draftNode.create(
|
|
1020
1187
|
gridPosition.current,
|
|
1021
1188
|
asset,
|
|
@@ -1208,18 +1375,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1208
1375
|
if (!result) return
|
|
1209
1376
|
|
|
1210
1377
|
event.stopPropagation()
|
|
1211
|
-
|
|
1212
|
-
if (draftNode.current) {
|
|
1213
|
-
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
1214
|
-
}
|
|
1215
|
-
const committedId = draftNode.current?.id ?? null
|
|
1216
|
-
const wasAdopted = draftNode.isAdopted
|
|
1217
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
1378
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1218
1379
|
if (result.dirtyNodeId) {
|
|
1219
1380
|
useScene.getState().dirtyNodes.add(result.dirtyNodeId)
|
|
1220
1381
|
}
|
|
1221
1382
|
|
|
1222
|
-
finishCommittedPlacement(
|
|
1383
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1223
1384
|
const nodes = useScene.getState().nodes
|
|
1224
1385
|
const enterResult = wallStrategy.enter(
|
|
1225
1386
|
getContext(),
|
|
@@ -1368,14 +1529,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1368
1529
|
if (!result) return
|
|
1369
1530
|
|
|
1370
1531
|
event.stopPropagation()
|
|
1371
|
-
|
|
1372
|
-
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
1373
|
-
}
|
|
1374
|
-
const committedId = draftNode.current?.id ?? null
|
|
1375
|
-
const wasAdopted = draftNode.isAdopted
|
|
1376
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
1532
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1377
1533
|
|
|
1378
|
-
finishCommittedPlacement(
|
|
1534
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1379
1535
|
const enterResult = roofWallStrategy.enter(getContext(), event, altFreeRef.current)
|
|
1380
1536
|
if (enterResult) {
|
|
1381
1537
|
applyTransition(enterResult)
|
|
@@ -1408,6 +1564,119 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1408
1564
|
}
|
|
1409
1565
|
}
|
|
1410
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
|
+
|
|
1411
1680
|
// ---- Item Surface Handlers ----
|
|
1412
1681
|
|
|
1413
1682
|
const detachItemSurfaceToFloor = (event: ItemEvent) => {
|
|
@@ -1449,7 +1718,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1449
1718
|
levelId ? { parentId: levelId } : undefined,
|
|
1450
1719
|
)
|
|
1451
1720
|
if (cursorGroupRef.current) {
|
|
1452
|
-
cursorGroupRef.current.position.set(
|
|
1721
|
+
cursorGroupRef.current.position.set(
|
|
1722
|
+
floorVisualPosition[0],
|
|
1723
|
+
floorVisualPosition[1] + getPlacementLevelY(draftNode.current),
|
|
1724
|
+
floorVisualPosition[2],
|
|
1725
|
+
)
|
|
1453
1726
|
}
|
|
1454
1727
|
|
|
1455
1728
|
const draft = draftNode.current
|
|
@@ -1613,6 +1886,15 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1613
1886
|
// have to aim around the cursor preview to drop the item.
|
|
1614
1887
|
if (event.node.id === draftNode.current?.id) {
|
|
1615
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
|
+
}
|
|
1616
1898
|
if (ctx.state.surface === 'shelf-surface' && ctx.state.shelfId) {
|
|
1617
1899
|
const shelfNode = useScene.getState().nodes[ctx.state.shelfId as AnyNodeId]
|
|
1618
1900
|
if (shelfNode && shelfNode.type === 'shelf') {
|
|
@@ -1620,13 +1902,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1620
1902
|
const result = shelfSurfaceStrategy.click(ctx, synthetic as never)
|
|
1621
1903
|
if (result) {
|
|
1622
1904
|
event.stopPropagation()
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
}
|
|
1626
|
-
const committedId = draftNode.current?.id ?? null
|
|
1627
|
-
const wasAdopted = draftNode.isAdopted
|
|
1628
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
1629
|
-
finishCommittedPlacement(finalId ?? committedId, wasAdopted, () => {
|
|
1905
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1906
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1630
1907
|
const enterResult = shelfSurfaceStrategy.enter(ctx, synthetic as never)
|
|
1631
1908
|
if (enterResult) {
|
|
1632
1909
|
applyTransition(enterResult)
|
|
@@ -1648,13 +1925,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1648
1925
|
const result = itemSurfaceStrategy.click(ctx, synthetic)
|
|
1649
1926
|
if (result) {
|
|
1650
1927
|
event.stopPropagation()
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
}
|
|
1654
|
-
const committedId = draftNode.current?.id ?? null
|
|
1655
|
-
const wasAdopted = draftNode.isAdopted
|
|
1656
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
1657
|
-
finishCommittedPlacement(finalId ?? committedId, wasAdopted, () => {
|
|
1928
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1929
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1658
1930
|
const enterResult = itemSurfaceStrategy.enter(ctx, synthetic)
|
|
1659
1931
|
if (enterResult) {
|
|
1660
1932
|
applyTransition(enterResult)
|
|
@@ -1679,13 +1951,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1679
1951
|
const result = ceilingStrategy.click(ctx, synthetic, getActiveValidators())
|
|
1680
1952
|
if (result) {
|
|
1681
1953
|
event.stopPropagation()
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
}
|
|
1685
|
-
const committedId = draftNode.current?.id ?? null
|
|
1686
|
-
const wasAdopted = draftNode.isAdopted
|
|
1687
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
1688
|
-
finishCommittedPlacement(finalId ?? committedId, wasAdopted, () => {
|
|
1954
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1955
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1689
1956
|
const nodes = useScene.getState().nodes
|
|
1690
1957
|
const enterResult = ceilingStrategy.enter(
|
|
1691
1958
|
getContext(),
|
|
@@ -1710,15 +1977,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1710
1977
|
if (!result) return
|
|
1711
1978
|
|
|
1712
1979
|
event.stopPropagation()
|
|
1713
|
-
|
|
1714
|
-
if (draftNode.current) {
|
|
1715
|
-
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
1716
|
-
}
|
|
1717
|
-
const committedId = draftNode.current?.id ?? null
|
|
1718
|
-
const wasAdopted = draftNode.isAdopted
|
|
1719
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
1980
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1720
1981
|
|
|
1721
|
-
finishCommittedPlacement(
|
|
1982
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1722
1983
|
// Try to set up next draft on the same surface
|
|
1723
1984
|
const enterResult = itemSurfaceStrategy.enter(getContext(), event)
|
|
1724
1985
|
if (enterResult) {
|
|
@@ -1841,15 +2102,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1841
2102
|
if (!result) return
|
|
1842
2103
|
|
|
1843
2104
|
event.stopPropagation()
|
|
1844
|
-
|
|
1845
|
-
if (draftNode.current) {
|
|
1846
|
-
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
1847
|
-
}
|
|
1848
|
-
const committedId = draftNode.current?.id ?? null
|
|
1849
|
-
const wasAdopted = draftNode.isAdopted
|
|
1850
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
2105
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1851
2106
|
|
|
1852
|
-
finishCommittedPlacement(
|
|
2107
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1853
2108
|
const nodes = useScene.getState().nodes
|
|
1854
2109
|
const enterResult = ceilingStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
|
1855
2110
|
if (enterResult) {
|
|
@@ -1982,14 +2237,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1982
2237
|
if (!result) return
|
|
1983
2238
|
|
|
1984
2239
|
event.stopPropagation()
|
|
1985
|
-
|
|
1986
|
-
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
1987
|
-
}
|
|
1988
|
-
const committedId = draftNode.current?.id ?? null
|
|
1989
|
-
const wasAdopted = draftNode.isAdopted
|
|
1990
|
-
const finalId = draftNode.commit(result.nodeUpdate)
|
|
2240
|
+
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
1991
2241
|
|
|
1992
|
-
finishCommittedPlacement(
|
|
2242
|
+
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1993
2243
|
const enterResult = shelfSurfaceStrategy.enter(getContext(), event)
|
|
1994
2244
|
if (enterResult) {
|
|
1995
2245
|
applyTransition(enterResult)
|
|
@@ -2018,7 +2268,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2018
2268
|
|
|
2019
2269
|
// Roof-wall drafts live flat in the host face frame (yaw 0) —
|
|
2020
2270
|
// manual rotation would skew them off the wall plane.
|
|
2021
|
-
if (
|
|
2271
|
+
if (
|
|
2272
|
+
placementState.current.surface === 'roof-wall' ||
|
|
2273
|
+
placementState.current.surface === 'block-face'
|
|
2274
|
+
)
|
|
2275
|
+
return
|
|
2022
2276
|
|
|
2023
2277
|
let rotationDir: 1 | -1 | 0 = 0
|
|
2024
2278
|
if ((event.key === 'r' || event.key === 'R') && !event.metaKey && !event.ctrlKey)
|
|
@@ -2034,9 +2288,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2034
2288
|
const newRotationY = steppedRotation(currentRotation[1] ?? 0, rotationDir)
|
|
2035
2289
|
draft.rotation = [currentRotation[0], newRotationY, currentRotation[2]]
|
|
2036
2290
|
|
|
2037
|
-
//
|
|
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.
|
|
2038
2294
|
if (cursorGroupRef.current) {
|
|
2039
|
-
cursorGroupRef.current.rotation.y
|
|
2295
|
+
cursorGroupRef.current.rotation.y += newRotationY - currentRotation[1]
|
|
2040
2296
|
}
|
|
2041
2297
|
const mesh = sceneRegistry.nodes.get(draft.id)
|
|
2042
2298
|
if (mesh) mesh.rotation.y = newRotationY
|
|
@@ -2053,8 +2309,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2053
2309
|
draft.position = [x, gridPosition.current.y, z]
|
|
2054
2310
|
if (cursorGroupRef.current) {
|
|
2055
2311
|
if (surface === 'floor') {
|
|
2312
|
+
const visual = getFloorVisualPosition([x, gridPosition.current.y, z])
|
|
2056
2313
|
cursorGroupRef.current.position.set(
|
|
2057
|
-
|
|
2314
|
+
visual[0],
|
|
2315
|
+
visual[1] + getPlacementLevelY(draft),
|
|
2316
|
+
visual[2],
|
|
2058
2317
|
)
|
|
2059
2318
|
} else {
|
|
2060
2319
|
cursorGroupRef.current.position.x = x
|
|
@@ -2100,24 +2359,40 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2100
2359
|
}
|
|
2101
2360
|
}
|
|
2102
2361
|
|
|
2103
|
-
//
|
|
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 })
|
|
2104
2366
|
const currentLive = useLiveTransforms.getState().get(draft.id)
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
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
|
+
)
|
|
2121
2396
|
}
|
|
2122
2397
|
|
|
2123
2398
|
revalidate()
|
|
@@ -2228,6 +2503,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2228
2503
|
emitter.on('roof:move', onRoofWallMove)
|
|
2229
2504
|
emitter.on('roof:click', onRoofWallClick)
|
|
2230
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)
|
|
2231
2510
|
emitter.on('ceiling:enter', onCeilingEnter)
|
|
2232
2511
|
emitter.on('ceiling:move', onCeilingMove)
|
|
2233
2512
|
emitter.on('ceiling:click', onCeilingClick)
|
|
@@ -2250,11 +2529,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2250
2529
|
if (placementState.current.surface !== 'floor') return
|
|
2251
2530
|
onGridClick(event as unknown as GridEvent)
|
|
2252
2531
|
}
|
|
2253
|
-
emitter.on('
|
|
2254
|
-
emitter.on('item:click', commitFloorOnSurfaceClick as never)
|
|
2255
|
-
emitter.on('ceiling:click', commitFloorOnSurfaceClick as never)
|
|
2256
|
-
emitter.on('roof:click', commitFloorOnSurfaceClick as never)
|
|
2257
|
-
emitter.on('shelf:click', commitFloorOnSurfaceClick as never)
|
|
2532
|
+
emitter.on('node:click', commitFloorOnSurfaceClick as never)
|
|
2258
2533
|
if (dragMode) window.addEventListener('pointerup', onReleaseCommit)
|
|
2259
2534
|
|
|
2260
2535
|
return () => {
|
|
@@ -2262,9 +2537,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2262
2537
|
if (dragMode) window.removeEventListener('pointerup', onReleaseCommit)
|
|
2263
2538
|
unsubDraftWatch()
|
|
2264
2539
|
useAlignmentGuides.getState().clear()
|
|
2265
|
-
// Clear live
|
|
2540
|
+
// Clear every live preview channel before restoring or deleting the draft.
|
|
2266
2541
|
if (draftNode.current) {
|
|
2267
2542
|
useLiveTransforms.getState().clear(draftNode.current.id)
|
|
2543
|
+
useLiveNodeOverrides.getState().clearFields(draftNode.current.id, ['rotation'])
|
|
2268
2544
|
}
|
|
2269
2545
|
draftNode.destroy()
|
|
2270
2546
|
useScene.temporal.getState().resume()
|
|
@@ -2282,6 +2558,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2282
2558
|
emitter.off('roof:move', onRoofWallMove)
|
|
2283
2559
|
emitter.off('roof:click', onRoofWallClick)
|
|
2284
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)
|
|
2285
2565
|
emitter.off('ceiling:enter', onCeilingEnter)
|
|
2286
2566
|
emitter.off('ceiling:move', onCeilingMove)
|
|
2287
2567
|
emitter.off('ceiling:click', onCeilingClick)
|
|
@@ -2290,11 +2570,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2290
2570
|
emitter.off('shelf:move', onShelfMove)
|
|
2291
2571
|
emitter.off('shelf:click', onShelfClick)
|
|
2292
2572
|
emitter.off('shelf:leave', onShelfLeave)
|
|
2293
|
-
emitter.off('
|
|
2294
|
-
emitter.off('item:click', commitFloorOnSurfaceClick as never)
|
|
2295
|
-
emitter.off('ceiling:click', commitFloorOnSurfaceClick as never)
|
|
2296
|
-
emitter.off('roof:click', commitFloorOnSurfaceClick as never)
|
|
2297
|
-
emitter.off('shelf:click', commitFloorOnSurfaceClick as never)
|
|
2573
|
+
emitter.off('node:click', commitFloorOnSurfaceClick as never)
|
|
2298
2574
|
emitter.off('tool:cancel', onCancel)
|
|
2299
2575
|
window.removeEventListener('keydown', onKeyDown)
|
|
2300
2576
|
window.removeEventListener('keyup', onKeyUp)
|
|
@@ -2312,6 +2588,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2312
2588
|
gridSnapStep,
|
|
2313
2589
|
updateDimensionGuides,
|
|
2314
2590
|
updatePreviewGeometry,
|
|
2591
|
+
reconcileDraftRaycast,
|
|
2315
2592
|
])
|
|
2316
2593
|
|
|
2317
2594
|
// Refresh wireframe when the grid step changes mid-placement so the green/red
|
|
@@ -2343,39 +2620,17 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2343
2620
|
const draftParent = draft.parentId
|
|
2344
2621
|
? useScene.getState().nodes[draft.parentId as AnyNodeId]
|
|
2345
2622
|
: undefined
|
|
2346
|
-
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
|
|
2347
2630
|
draft.parentId = viewerLevelId
|
|
2348
2631
|
useScene.getState().updateNode(draft.id as AnyNodeId, { parentId: viewerLevelId })
|
|
2349
2632
|
}, [viewerLevelId, draftNode, asset])
|
|
2350
2633
|
|
|
2351
|
-
// Disable raycasting on the live draft mesh (and restore it when the draft
|
|
2352
|
-
// changes or goes away) so the cursor ray passes through the item being
|
|
2353
|
-
// moved and lands on the surface beneath it.
|
|
2354
|
-
const reconcileDraftRaycast = useCallback((mesh: Object3D | null) => {
|
|
2355
|
-
if (raycastDisabledMeshRef.current !== mesh) {
|
|
2356
|
-
// New draft root (or cleared): restore the prior mesh and reset tracking.
|
|
2357
|
-
for (const restore of restoreRaycastsRef.current) restore()
|
|
2358
|
-
restoreRaycastsRef.current = []
|
|
2359
|
-
raycastDisabledChildrenRef.current = new WeakSet()
|
|
2360
|
-
raycastDisabledMeshRef.current = mesh
|
|
2361
|
-
}
|
|
2362
|
-
if (!mesh) return
|
|
2363
|
-
// Disable any descendant not handled yet. Item drafts are GLB models whose
|
|
2364
|
-
// child meshes mount asynchronously (Suspense), so a one-shot traverse
|
|
2365
|
-
// misses them — those late children keep intercepting the ray and corrupt
|
|
2366
|
-
// the shelf-row hit the moment the item moves onto a row. Re-walking each
|
|
2367
|
-
// frame is cheap: the WeakSet makes it idempotent, so only new children pay.
|
|
2368
|
-
mesh.traverse((child) => {
|
|
2369
|
-
if (raycastDisabledChildrenRef.current.has(child)) return
|
|
2370
|
-
raycastDisabledChildrenRef.current.add(child)
|
|
2371
|
-
const original = child.raycast
|
|
2372
|
-
child.raycast = () => {}
|
|
2373
|
-
restoreRaycastsRef.current.push(() => {
|
|
2374
|
-
child.raycast = original
|
|
2375
|
-
})
|
|
2376
|
-
})
|
|
2377
|
-
}, [])
|
|
2378
|
-
|
|
2379
2634
|
// Restore the draft mesh's raycast when the coordinator unmounts (tool change).
|
|
2380
2635
|
useEffect(() => () => reconcileDraftRaycast(null), [reconcileDraftRaycast])
|
|
2381
2636
|
|
|
@@ -2387,8 +2642,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2387
2642
|
// moving existing node has no draft here, so the grid reads that case straight
|
|
2388
2643
|
// off the node's mesh. Cleared when idle.
|
|
2389
2644
|
const surfaceNormalRef = useRef(new Vector3(0, 1, 0))
|
|
2645
|
+
const surfaceWorldPointRef = useRef(new Vector3())
|
|
2390
2646
|
const facingForwardRef = useRef(new Vector3(0, 0, 1))
|
|
2391
2647
|
const facingQuatRef = useRef(new Quaternion())
|
|
2648
|
+
const ghostSurfaceQuatRef = useRef(new Quaternion())
|
|
2392
2649
|
useFrame(() => {
|
|
2393
2650
|
const ghost = cursorGroupRef.current
|
|
2394
2651
|
if (!(asset && ghost)) {
|
|
@@ -2403,7 +2660,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2403
2660
|
// the item's forward on the floor, and the triangle rides at the ghost's Y.
|
|
2404
2661
|
let facingYaw = ghost.rotation.y
|
|
2405
2662
|
let facingY = ghost.position.y
|
|
2406
|
-
if (surf === 'wall' || surf === 'roof-wall') {
|
|
2663
|
+
if (surf === 'wall' || surf === 'roof-wall' || surf === 'block-face') {
|
|
2407
2664
|
// Wall/roof-segment faces: the cursor group's yaw is the symmetric
|
|
2408
2665
|
// wireframe yaw (π off the real facing for a wall, and a different frame
|
|
2409
2666
|
// for a roof face), so derive the item's TRUE outward facing from the
|
|
@@ -2411,21 +2668,31 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2411
2668
|
// surface. This keeps BOTH the grid normal and the triangle correct for
|
|
2412
2669
|
// wall and roof-segment hosts alike, rather than the old quaternion read
|
|
2413
2670
|
// that pointed the wrong way.
|
|
2414
|
-
const mesh =
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
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)
|
|
2421
2686
|
// The forward triangle is a floor aid; drop it to the building-local floor
|
|
2422
|
-
// under the
|
|
2423
|
-
facingY =
|
|
2424
|
-
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)
|
|
2425
2689
|
} else {
|
|
2426
|
-
|
|
2690
|
+
ghost.getWorldQuaternion(ghostSurfaceQuatRef.current)
|
|
2691
|
+
resolveItemPlacementSurfaceNormal(surf, ghostSurfaceQuatRef.current, null, n)
|
|
2427
2692
|
}
|
|
2428
|
-
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)
|
|
2429
2696
|
|
|
2430
2697
|
if (shape.depth > 0) {
|
|
2431
2698
|
useFacingPose.getState().set({
|
|
@@ -2493,8 +2760,16 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2493
2760
|
gridPosition.current.z,
|
|
2494
2761
|
])
|
|
2495
2762
|
mesh.position.y = visualPosition[1]
|
|
2496
|
-
cursorGroupRef.current.position.y =
|
|
2497
|
-
|
|
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
|
+
)
|
|
2498
2773
|
}
|
|
2499
2774
|
})
|
|
2500
2775
|
|