@pascal-app/editor 1.0.0-beta.5 → 1.0.1
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/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- 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 +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- 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/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +78 -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 +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -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 +295 -239
- 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-action-menu.tsx +12 -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 +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- 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/ceiling/ceiling-system.tsx +3 -1
- 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/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -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 +304 -171
- 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 +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -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 +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- 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 +56 -17
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- 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/tools/zone/zone-tool.tsx +13 -4
- 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 +63 -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/metric-control.tsx +29 -13
- 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 +44 -21
- 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 +103 -10
- 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 +266 -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 +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- 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 +502 -31
- 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 +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- 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/shelf-tree-node.tsx +3 -8
- 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 +20 -7
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- 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 +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- 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 +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -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/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- 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 +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- 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 +5 -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/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- 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-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- 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/node-action-movement.ts +21 -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 +455 -0
- package/src/lib/portable-export.ts +1042 -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 +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -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 +38 -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-clipboard.ts +1 -1
- 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/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -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/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- 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-handle-group.ts +4 -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-send-to-app.ts +21 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -4,8 +4,8 @@ import type {
|
|
|
4
4
|
CeilingEvent,
|
|
5
5
|
CeilingNode,
|
|
6
6
|
GridEvent,
|
|
7
|
-
ItemEvent,
|
|
8
7
|
ItemNode,
|
|
8
|
+
NodeEvent,
|
|
9
9
|
RoofEvent,
|
|
10
10
|
RoofNode,
|
|
11
11
|
RoofSegmentNode,
|
|
@@ -16,19 +16,20 @@ import type {
|
|
|
16
16
|
WallNode,
|
|
17
17
|
} from '@pascal-app/core'
|
|
18
18
|
import {
|
|
19
|
-
canHostOnTop,
|
|
20
19
|
clampRectToRoofWallFace,
|
|
20
|
+
createSceneApi,
|
|
21
21
|
getRoofSegmentWallFace,
|
|
22
22
|
getScaledDimensions,
|
|
23
|
-
isLowProfileItemSurface,
|
|
24
23
|
nodeRegistry,
|
|
24
|
+
resolveSurfacePlacement,
|
|
25
25
|
roofFacePointToSegment,
|
|
26
26
|
sceneRegistry,
|
|
27
27
|
useScene,
|
|
28
28
|
} from '@pascal-app/core'
|
|
29
|
-
import { Euler,
|
|
29
|
+
import { Euler, Quaternion, Vector3 } from 'three'
|
|
30
30
|
import { hasRoofFaceChildOverlap, resolveRoofWallHit } from '../../../lib/roof-wall-hit'
|
|
31
31
|
import { snapWorldXZForActiveBuilding } from '../../../lib/world-grid-snap'
|
|
32
|
+
import { itemEventToSurfaceHit, surfaceWorldNormalY } from '../shared/surface-hit'
|
|
32
33
|
import {
|
|
33
34
|
calculateItemRotation,
|
|
34
35
|
getGridAlignedDimensions,
|
|
@@ -48,42 +49,14 @@ import type {
|
|
|
48
49
|
} from './placement-types'
|
|
49
50
|
|
|
50
51
|
const DEFAULT_DIMENSIONS: [number, number, number] = [1, 1, 1]
|
|
51
|
-
const UPWARD_SURFACE_NORMAL_MIN_Y = 0.75
|
|
52
|
-
|
|
53
|
-
function getWorldNormalY(event: ItemEvent): number | null {
|
|
54
|
-
if (!event.normal) return null
|
|
55
|
-
|
|
56
|
-
const normal = new Vector3(event.normal[0], event.normal[1], event.normal[2])
|
|
57
|
-
normal.applyNormalMatrix(new Matrix3().getNormalMatrix(event.object.matrixWorld)).normalize()
|
|
58
|
-
return normal.y
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isUpwardItemSurfaceHit(event: ItemEvent): boolean {
|
|
62
|
-
const normalY = getWorldNormalY(event)
|
|
63
|
-
return normalY !== null && normalY >= UPWARD_SURFACE_NORMAL_MIN_Y
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function getSurfacePlacementHeight(surfaceItem: ItemNode, event: ItemEvent, localPos: Vector3) {
|
|
67
|
-
if (!canHostOnTop(surfaceItem)) return null
|
|
68
|
-
if (isLowProfileItemSurface(surfaceItem)) return null
|
|
69
|
-
if (!isUpwardItemSurfaceHit(event)) return null
|
|
70
|
-
|
|
71
|
-
if (surfaceItem.asset.surface) {
|
|
72
|
-
return surfaceItem.asset.surface.height * surfaceItem.scale[1]
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!Number.isFinite(localPos.y)) return null
|
|
76
|
-
return localPos.y
|
|
77
|
-
}
|
|
78
|
-
|
|
79
52
|
function isDescendantOfItem(
|
|
80
|
-
candidate:
|
|
81
|
-
|
|
53
|
+
candidate: AnyNode,
|
|
54
|
+
ancestorId: string,
|
|
82
55
|
nodes: Record<string, AnyNode>,
|
|
83
56
|
): boolean {
|
|
84
57
|
let parentId = candidate.parentId
|
|
85
58
|
while (parentId) {
|
|
86
|
-
if (parentId ===
|
|
59
|
+
if (parentId === ancestorId) return true
|
|
87
60
|
const parent = nodes[parentId as AnyNodeId]
|
|
88
61
|
parentId = parent?.parentId ?? null
|
|
89
62
|
}
|
|
@@ -268,13 +241,18 @@ export const wallStrategy = {
|
|
|
268
241
|
const pose = resolveWallPlacementPose(event, x, adjustedY, attachTo, side, itemRotation)
|
|
269
242
|
|
|
270
243
|
return {
|
|
271
|
-
stateUpdate: {
|
|
244
|
+
stateUpdate: {
|
|
245
|
+
surface: 'wall',
|
|
246
|
+
wallId: event.node.id,
|
|
247
|
+
roofSegmentId: null,
|
|
248
|
+
},
|
|
272
249
|
nodeUpdate: {
|
|
273
250
|
position: pose.position,
|
|
274
251
|
parentId: event.node.id,
|
|
275
252
|
// The draft may arrive from a roof-segment wall face.
|
|
276
253
|
roofSegmentId: undefined,
|
|
277
254
|
roofFace: undefined,
|
|
255
|
+
blockFaceId: undefined,
|
|
278
256
|
side,
|
|
279
257
|
rotation: [0, itemRotation, 0],
|
|
280
258
|
},
|
|
@@ -367,6 +345,7 @@ export const wallStrategy = {
|
|
|
367
345
|
parentId: event.node.id,
|
|
368
346
|
roofSegmentId: undefined,
|
|
369
347
|
roofFace: undefined,
|
|
348
|
+
blockFaceId: undefined,
|
|
370
349
|
side: ctx.draftItem.side,
|
|
371
350
|
rotation: ctx.draftItem.rotation,
|
|
372
351
|
metadata: stripTransient(ctx.draftItem.metadata),
|
|
@@ -520,12 +499,17 @@ export const roofWallStrategy = {
|
|
|
520
499
|
if (!target) return null
|
|
521
500
|
|
|
522
501
|
return {
|
|
523
|
-
stateUpdate: {
|
|
502
|
+
stateUpdate: {
|
|
503
|
+
surface: 'roof-wall',
|
|
504
|
+
roofSegmentId: target.segment.id,
|
|
505
|
+
wallId: null,
|
|
506
|
+
},
|
|
524
507
|
nodeUpdate: {
|
|
525
508
|
position: target.position,
|
|
526
509
|
parentId: target.segment.id,
|
|
527
510
|
roofSegmentId: target.segment.id,
|
|
528
511
|
roofFace: target.faceId,
|
|
512
|
+
blockFaceId: undefined,
|
|
529
513
|
wallId: undefined,
|
|
530
514
|
side: 'front',
|
|
531
515
|
rotation: [0, 0, 0],
|
|
@@ -583,6 +567,7 @@ export const roofWallStrategy = {
|
|
|
583
567
|
parentId: ctx.state.roofSegmentId,
|
|
584
568
|
roofSegmentId: ctx.state.roofSegmentId,
|
|
585
569
|
roofFace: ctx.draftItem.roofFace,
|
|
570
|
+
blockFaceId: undefined,
|
|
586
571
|
wallId: undefined,
|
|
587
572
|
side: 'front',
|
|
588
573
|
rotation: [0, 0, 0],
|
|
@@ -615,6 +600,114 @@ export const roofWallStrategy = {
|
|
|
615
600
|
},
|
|
616
601
|
}
|
|
617
602
|
|
|
603
|
+
// ============================================================================
|
|
604
|
+
// FACE HOST STRATEGY
|
|
605
|
+
// ============================================================================
|
|
606
|
+
|
|
607
|
+
function resolveFaceHostTarget(ctx: PlacementContext, event: NodeEvent) {
|
|
608
|
+
const faceHost = nodeRegistry.get(event.node.type)?.capabilities.faceHost
|
|
609
|
+
if (!faceHost) return null
|
|
610
|
+
const rawDimensions = ctx.draftItem
|
|
611
|
+
? getScaledDimensions(ctx.draftItem)
|
|
612
|
+
: (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
|
613
|
+
return faceHost.resolvePlacement({
|
|
614
|
+
host: event.node,
|
|
615
|
+
asset: ctx.asset,
|
|
616
|
+
draftItem: ctx.draftItem,
|
|
617
|
+
localPosition: event.localPosition,
|
|
618
|
+
faceIndex: event.faceIndex,
|
|
619
|
+
object: event.object,
|
|
620
|
+
currentFaceId: faceHost.currentFaceId(ctx.draftItem),
|
|
621
|
+
rawDimensions,
|
|
622
|
+
dimensions: getGridAlignedDimensions(rawDimensions, ctx.asset.attachTo),
|
|
623
|
+
snapScalar: snapToHalf,
|
|
624
|
+
})
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function clearFaceHostItemFields(ctx: PlacementContext): Partial<ItemNode> {
|
|
628
|
+
const host = ctx.state.blockId ? useScene.getState().nodes[ctx.state.blockId] : undefined
|
|
629
|
+
const clearFields = host
|
|
630
|
+
? nodeRegistry.get(host.type)?.capabilities.faceHost?.clearItemFields
|
|
631
|
+
: undefined
|
|
632
|
+
const patch: Partial<ItemNode> = {}
|
|
633
|
+
for (const field of clearFields ?? []) {
|
|
634
|
+
;(patch as Record<string, unknown>)[field] = undefined
|
|
635
|
+
}
|
|
636
|
+
return patch
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export const faceHostStrategy = {
|
|
640
|
+
enter(ctx: PlacementContext, event: NodeEvent): TransitionResult | null {
|
|
641
|
+
const target = resolveFaceHostTarget(ctx, event)
|
|
642
|
+
if (!target) return null
|
|
643
|
+
return {
|
|
644
|
+
stateUpdate: {
|
|
645
|
+
surface: 'block-face',
|
|
646
|
+
blockId: event.node.id,
|
|
647
|
+
wallId: null,
|
|
648
|
+
roofSegmentId: null,
|
|
649
|
+
},
|
|
650
|
+
nodeUpdate: {
|
|
651
|
+
...target.nodeUpdate,
|
|
652
|
+
},
|
|
653
|
+
gridPosition: target.position,
|
|
654
|
+
cursorPosition: target.cursorPosition,
|
|
655
|
+
cursorRotationY: target.cursorRotation[1],
|
|
656
|
+
cursorRotation: target.cursorRotation,
|
|
657
|
+
stopPropagation: true,
|
|
658
|
+
hostFaceId: target.faceId,
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
|
|
662
|
+
move(ctx: PlacementContext, event: NodeEvent): PlacementResult | null {
|
|
663
|
+
if (ctx.state.surface !== 'block-face' || !ctx.draftItem) return null
|
|
664
|
+
const target = resolveFaceHostTarget(ctx, event)
|
|
665
|
+
if (!target || event.node.id !== ctx.state.blockId) return null
|
|
666
|
+
return {
|
|
667
|
+
gridPosition: target.position,
|
|
668
|
+
cursorPosition: target.cursorPosition,
|
|
669
|
+
cursorRotationY: target.cursorRotation[1],
|
|
670
|
+
cursorRotation: target.cursorRotation,
|
|
671
|
+
nodeUpdate: target.nodeUpdate,
|
|
672
|
+
stopPropagation: true,
|
|
673
|
+
dirtyNodeId: null,
|
|
674
|
+
hostFaceId: target.faceId,
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
|
|
678
|
+
click(ctx: PlacementContext, event: NodeEvent): CommitResult | null {
|
|
679
|
+
if (ctx.state.surface !== 'block-face' || !ctx.draftItem) return null
|
|
680
|
+
const target = resolveFaceHostTarget(ctx, event)
|
|
681
|
+
if (!target || event.node.id !== ctx.state.blockId) return null
|
|
682
|
+
return {
|
|
683
|
+
nodeUpdate: {
|
|
684
|
+
...target.nodeUpdate,
|
|
685
|
+
metadata: stripTransient(ctx.draftItem.metadata),
|
|
686
|
+
},
|
|
687
|
+
stopPropagation: true,
|
|
688
|
+
dirtyNodeId: null,
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
|
|
692
|
+
leave(ctx: PlacementContext): TransitionResult | null {
|
|
693
|
+
if (ctx.state.surface !== 'block-face') return null
|
|
694
|
+
const floorPosition: [number, number, number] = [ctx.gridPosition.x, 0, ctx.gridPosition.z]
|
|
695
|
+
return {
|
|
696
|
+
stateUpdate: { surface: 'floor', blockId: null },
|
|
697
|
+
nodeUpdate: {
|
|
698
|
+
...clearFaceHostItemFields(ctx),
|
|
699
|
+
position: floorPosition,
|
|
700
|
+
parentId: ctx.levelId,
|
|
701
|
+
rotation: [0, ctx.currentCursorRotationY, 0],
|
|
702
|
+
},
|
|
703
|
+
cursorRotationY: ctx.currentCursorRotationY,
|
|
704
|
+
gridPosition: floorPosition,
|
|
705
|
+
cursorPosition: floorPosition,
|
|
706
|
+
stopPropagation: true,
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
}
|
|
710
|
+
|
|
618
711
|
// ============================================================================
|
|
619
712
|
// CEILING STRATEGY
|
|
620
713
|
// ============================================================================
|
|
@@ -760,65 +853,114 @@ export const ceilingStrategy = {
|
|
|
760
853
|
// ITEM SURFACE STRATEGY
|
|
761
854
|
// ============================================================================
|
|
762
855
|
|
|
856
|
+
function resolveCatalogItemSurfacePlacement(
|
|
857
|
+
host: AnyNode,
|
|
858
|
+
event: NodeEvent<AnyNode>,
|
|
859
|
+
dimensions: [number, number, number],
|
|
860
|
+
worldYaw: number,
|
|
861
|
+
onReject?: PlacementContext['onSurfaceReject'],
|
|
862
|
+
rawEvent = event,
|
|
863
|
+
) {
|
|
864
|
+
const mesh = sceneRegistry.nodes.get(host.id)
|
|
865
|
+
if (!mesh) return null
|
|
866
|
+
const hit = itemEventToSurfaceHit(host, event)
|
|
867
|
+
if (!hit) return null
|
|
868
|
+
const quaternion = mesh.getWorldQuaternion(new Quaternion())
|
|
869
|
+
const hostYaw = new Euler().setFromQuaternion(quaternion, 'YXZ').y
|
|
870
|
+
const placement = resolveSurfacePlacement({
|
|
871
|
+
host,
|
|
872
|
+
childKind: 'item',
|
|
873
|
+
childFootprint: { size: dimensions, rotationY: worldYaw - hostYaw },
|
|
874
|
+
hit: host.type === 'procedural-item' ? (itemEventToSurfaceHit(host, rawEvent) ?? hit) : hit,
|
|
875
|
+
origin: host.type === 'procedural-item' ? hit.point : undefined,
|
|
876
|
+
scene: createSceneApi(useScene),
|
|
877
|
+
snapScalar: snapToGrid,
|
|
878
|
+
checkFootprint: true,
|
|
879
|
+
onReject,
|
|
880
|
+
})
|
|
881
|
+
if (!placement) return null
|
|
882
|
+
return {
|
|
883
|
+
position: [...placement.position] as [number, number, number],
|
|
884
|
+
rotationY: placement.rotationY,
|
|
885
|
+
surfaceId: placement.surfaceId,
|
|
886
|
+
worldPosition: mesh.localToWorld(new Vector3(...placement.position)).toArray(),
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
export function validCatalogCounterPose(ctx: PlacementContext): boolean {
|
|
891
|
+
const hostId = ctx.state.surface === 'shelf-surface' ? ctx.state.shelfId : ctx.state.surfaceItemId
|
|
892
|
+
const host = hostId ? useScene.getState().nodes[hostId as AnyNodeId] : undefined
|
|
893
|
+
if (
|
|
894
|
+
host?.type !== 'cabinet' &&
|
|
895
|
+
host?.type !== 'shelf' &&
|
|
896
|
+
host?.type !== 'item' &&
|
|
897
|
+
host?.type !== 'procedural-item'
|
|
898
|
+
)
|
|
899
|
+
return true
|
|
900
|
+
if (!ctx.draftItem) return false
|
|
901
|
+
const placement = resolveSurfacePlacement({
|
|
902
|
+
host,
|
|
903
|
+
childKind: 'item',
|
|
904
|
+
childFootprint: {
|
|
905
|
+
size: getScaledDimensions(ctx.draftItem),
|
|
906
|
+
rotationY: ctx.draftItem.rotation[1],
|
|
907
|
+
rotation: ctx.draftItem.rotation,
|
|
908
|
+
},
|
|
909
|
+
hit: { point: ctx.gridPosition.toArray(), normalWorldY: 1 },
|
|
910
|
+
scene: createSceneApi(useScene),
|
|
911
|
+
onReject: ctx.onSurfaceReject,
|
|
912
|
+
})
|
|
913
|
+
return (
|
|
914
|
+
!!placement &&
|
|
915
|
+
(host.type !== 'cabinet' || Math.abs(placement.position[1] - ctx.gridPosition.y) < 1e-5)
|
|
916
|
+
)
|
|
917
|
+
}
|
|
918
|
+
|
|
763
919
|
export const itemSurfaceStrategy = {
|
|
764
920
|
/**
|
|
765
921
|
* Handle item:enter — transition from floor to an item surface.
|
|
766
922
|
* Returns null if: item has no surface, our item doesn't fit, or it's the draft itself.
|
|
767
923
|
*/
|
|
768
|
-
enter(ctx: PlacementContext, event:
|
|
924
|
+
enter(ctx: PlacementContext, event: NodeEvent<AnyNode>): TransitionResult | null {
|
|
769
925
|
// Only floor items can be placed on surfaces
|
|
770
926
|
if (ctx.asset.attachTo) return null
|
|
771
927
|
|
|
772
|
-
const surfaceItem = event.node
|
|
928
|
+
const surfaceItem = event.node
|
|
773
929
|
// Don't surface-place on the draft itself
|
|
774
930
|
if (surfaceItem.id === ctx.draftItem?.id) return null
|
|
775
931
|
if (ctx.state.surface === 'item-surface' && ctx.state.surfaceItemId === surfaceItem.id) {
|
|
776
932
|
return null
|
|
777
933
|
}
|
|
778
|
-
const nodes = useScene.getState().nodes
|
|
779
|
-
if (ctx.draftItem && isDescendantOfItem(surfaceItem, ctx.draftItem, nodes)) return null
|
|
780
|
-
|
|
781
|
-
// Size check: our footprint must fit on surface item's footprint
|
|
782
934
|
const ourDims = ctx.draftItem
|
|
783
935
|
? getScaledDimensions(ctx.draftItem)
|
|
784
936
|
: (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const y = surfaceHeight
|
|
799
|
-
|
|
800
|
-
const worldSnapped = surfaceMesh.localToWorld(new Vector3(x, y, z))
|
|
801
|
-
|
|
802
|
-
// Counter-rotate so the draft's world Y rotation stays continuous when
|
|
803
|
-
// the user drags onto a rotated surface item. The cursor wireframe
|
|
804
|
-
// already shows the user's intended world rotation; we just need to
|
|
805
|
-
// store the right local value relative to the new parent.
|
|
806
|
-
const surfaceQuat = new Quaternion()
|
|
807
|
-
surfaceMesh.getWorldQuaternion(surfaceQuat)
|
|
808
|
-
const surfaceWorldY = new Euler().setFromQuaternion(surfaceQuat, 'YXZ').y
|
|
809
|
-
const localRotationY = ctx.currentCursorRotationY - surfaceWorldY
|
|
937
|
+
if (
|
|
938
|
+
ctx.draftItem &&
|
|
939
|
+
isDescendantOfItem(surfaceItem, ctx.draftItem.id, useScene.getState().nodes)
|
|
940
|
+
)
|
|
941
|
+
return null
|
|
942
|
+
const pose = resolveCatalogItemSurfacePlacement(
|
|
943
|
+
surfaceItem,
|
|
944
|
+
event,
|
|
945
|
+
ourDims,
|
|
946
|
+
ctx.currentCursorRotationY,
|
|
947
|
+
ctx.onSurfaceReject,
|
|
948
|
+
)
|
|
949
|
+
if (!pose) return null
|
|
810
950
|
const draftRotation = ctx.draftItem?.rotation ?? [0, 0, 0]
|
|
811
951
|
|
|
812
952
|
return {
|
|
953
|
+
surfaceId: pose.surfaceId,
|
|
813
954
|
stateUpdate: { surface: 'item-surface', surfaceItemId: surfaceItem.id },
|
|
814
955
|
nodeUpdate: {
|
|
815
|
-
position:
|
|
956
|
+
position: pose.position,
|
|
816
957
|
parentId: surfaceItem.id,
|
|
817
|
-
|
|
958
|
+
supportSlabId: undefined,
|
|
959
|
+
rotation: [draftRotation[0], pose.rotationY, draftRotation[2]],
|
|
818
960
|
},
|
|
819
961
|
cursorRotationY: ctx.currentCursorRotationY,
|
|
820
|
-
gridPosition:
|
|
821
|
-
cursorPosition:
|
|
962
|
+
gridPosition: pose.position,
|
|
963
|
+
cursorPosition: pose.worldPosition,
|
|
822
964
|
stopPropagation: true,
|
|
823
965
|
}
|
|
824
966
|
},
|
|
@@ -826,35 +968,31 @@ export const itemSurfaceStrategy = {
|
|
|
826
968
|
/**
|
|
827
969
|
* Handle item:move — update position while on an item surface.
|
|
828
970
|
*/
|
|
829
|
-
move(ctx: PlacementContext, event:
|
|
971
|
+
move(ctx: PlacementContext, event: NodeEvent<AnyNode>, rawEvent = event): PlacementResult | null {
|
|
830
972
|
if (ctx.state.surface !== 'item-surface') return null
|
|
831
973
|
if (!(ctx.state.surfaceItemId && ctx.draftItem)) return null
|
|
832
974
|
if (event.node.id !== ctx.state.surfaceItemId) return null
|
|
833
975
|
|
|
834
976
|
const nodes = useScene.getState().nodes
|
|
835
|
-
const surfaceItem = nodes[ctx.state.surfaceItemId as AnyNodeId]
|
|
977
|
+
const surfaceItem = nodes[ctx.state.surfaceItemId as AnyNodeId]
|
|
836
978
|
if (!surfaceItem) return null
|
|
837
979
|
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const x = snapToGrid(localPos.x, ourDims[0])
|
|
848
|
-
const z = snapToGrid(localPos.z, ourDims[2])
|
|
849
|
-
const y = surfaceHeight
|
|
850
|
-
|
|
851
|
-
const worldSnapped = surfaceMesh.localToWorld(new Vector3(x, y, z))
|
|
980
|
+
const pose = resolveCatalogItemSurfacePlacement(
|
|
981
|
+
surfaceItem,
|
|
982
|
+
event,
|
|
983
|
+
getScaledDimensions(ctx.draftItem),
|
|
984
|
+
ctx.currentCursorRotationY,
|
|
985
|
+
ctx.onSurfaceReject,
|
|
986
|
+
rawEvent,
|
|
987
|
+
)
|
|
988
|
+
if (!pose) return null
|
|
852
989
|
|
|
853
990
|
return {
|
|
854
|
-
|
|
855
|
-
|
|
991
|
+
surfaceId: pose.surfaceId,
|
|
992
|
+
gridPosition: pose.position,
|
|
993
|
+
cursorPosition: pose.worldPosition,
|
|
856
994
|
cursorRotationY: ctx.currentCursorRotationY,
|
|
857
|
-
nodeUpdate: { position:
|
|
995
|
+
nodeUpdate: { position: pose.position },
|
|
858
996
|
stopPropagation: true,
|
|
859
997
|
dirtyNodeId: null,
|
|
860
998
|
}
|
|
@@ -863,10 +1001,11 @@ export const itemSurfaceStrategy = {
|
|
|
863
1001
|
/**
|
|
864
1002
|
* Handle item:click — commit placement on item surface.
|
|
865
1003
|
*/
|
|
866
|
-
click(ctx: PlacementContext, _event:
|
|
1004
|
+
click(ctx: PlacementContext, _event: NodeEvent<AnyNode>): CommitResult | null {
|
|
867
1005
|
if (ctx.state.surface !== 'item-surface') return null
|
|
868
1006
|
if (!(ctx.draftItem && ctx.state.surfaceItemId)) return null
|
|
869
1007
|
if (_event.node.id !== ctx.state.surfaceItemId) return null
|
|
1008
|
+
if (!validCatalogCounterPose(ctx)) return null
|
|
870
1009
|
|
|
871
1010
|
return {
|
|
872
1011
|
nodeUpdate: {
|
|
@@ -884,32 +1023,40 @@ export const itemSurfaceStrategy = {
|
|
|
884
1023
|
// SHELF SURFACE STRATEGY
|
|
885
1024
|
// ============================================================================
|
|
886
1025
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1026
|
+
function resolveShelfSurfacePlacement(
|
|
1027
|
+
host: ShelfNode,
|
|
1028
|
+
event: ShelfEvent,
|
|
1029
|
+
dimensions: [number, number, number],
|
|
1030
|
+
worldYaw: number,
|
|
1031
|
+
entering: boolean,
|
|
1032
|
+
onReject?: PlacementContext['onSurfaceReject'],
|
|
1033
|
+
) {
|
|
1034
|
+
const mesh = sceneRegistry.nodes.get(host.id)
|
|
1035
|
+
if (!mesh) return null
|
|
1036
|
+
const local = mesh.worldToLocal(new Vector3(...event.position))
|
|
1037
|
+
const quaternion = mesh.getWorldQuaternion(new Quaternion())
|
|
1038
|
+
const hostYaw = new Euler().setFromQuaternion(quaternion, 'YXZ').y
|
|
1039
|
+
const placement = resolveSurfacePlacement({
|
|
1040
|
+
host,
|
|
1041
|
+
childKind: 'item',
|
|
1042
|
+
childFootprint: { size: dimensions, rotationY: worldYaw - hostYaw },
|
|
1043
|
+
hit: {
|
|
1044
|
+
point: local.toArray(),
|
|
1045
|
+
// Existing shelf movement elects rows even over side faces or board gaps;
|
|
1046
|
+
// only entering requires an upward hit.
|
|
1047
|
+
normalWorldY: entering ? surfaceWorldNormalY(event.normal, event.object.matrixWorld) : 1,
|
|
1048
|
+
},
|
|
1049
|
+
scene: createSceneApi(useScene),
|
|
1050
|
+
snapScalar: snapToGrid,
|
|
1051
|
+
onReject,
|
|
1052
|
+
checkFootprint: true,
|
|
1053
|
+
})
|
|
1054
|
+
if (!placement) return null
|
|
1055
|
+
return {
|
|
1056
|
+
position: [...placement.position] as [number, number, number],
|
|
1057
|
+
rotationY: placement.rotationY,
|
|
1058
|
+
worldPosition: mesh.localToWorld(new Vector3(...placement.position)).toArray(),
|
|
911
1059
|
}
|
|
912
|
-
return best?.position[1] ?? null
|
|
913
1060
|
}
|
|
914
1061
|
|
|
915
1062
|
export const shelfSurfaceStrategy = {
|
|
@@ -928,43 +1075,30 @@ export const shelfSurfaceStrategy = {
|
|
|
928
1075
|
if (ctx.state.surface === 'shelf-surface' && ctx.state.shelfId === shelfNode.id) {
|
|
929
1076
|
return null
|
|
930
1077
|
}
|
|
931
|
-
if (!isUpwardShelfSurfaceHit(event)) return null
|
|
932
|
-
|
|
933
|
-
// Size check: draft footprint must fit on the shelf board (width × depth).
|
|
934
1078
|
const ourDims = ctx.draftItem
|
|
935
1079
|
? getScaledDimensions(ctx.draftItem)
|
|
936
1080
|
: (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
if (
|
|
946
|
-
|
|
947
|
-
const x = snapToGrid(localPos.x, ourDims[0])
|
|
948
|
-
const z = snapToGrid(localPos.z, ourDims[2])
|
|
949
|
-
|
|
950
|
-
const worldSnapped = shelfMesh.localToWorld(new Vector3(x, rowY, z))
|
|
951
|
-
|
|
952
|
-
const surfaceQuat = new Quaternion()
|
|
953
|
-
shelfMesh.getWorldQuaternion(surfaceQuat)
|
|
954
|
-
const surfaceWorldY = new Euler().setFromQuaternion(surfaceQuat, 'YXZ').y
|
|
955
|
-
const localRotationY = ctx.currentCursorRotationY - surfaceWorldY
|
|
1081
|
+
const pose = resolveShelfSurfacePlacement(
|
|
1082
|
+
shelfNode,
|
|
1083
|
+
event,
|
|
1084
|
+
ourDims,
|
|
1085
|
+
ctx.currentCursorRotationY,
|
|
1086
|
+
true,
|
|
1087
|
+
ctx.onSurfaceReject,
|
|
1088
|
+
)
|
|
1089
|
+
if (!pose) return null
|
|
956
1090
|
const draftRotation = ctx.draftItem?.rotation ?? [0, 0, 0]
|
|
957
1091
|
|
|
958
1092
|
return {
|
|
959
1093
|
stateUpdate: { surface: 'shelf-surface', shelfId: shelfNode.id },
|
|
960
1094
|
nodeUpdate: {
|
|
961
|
-
position:
|
|
1095
|
+
position: pose.position,
|
|
962
1096
|
parentId: shelfNode.id,
|
|
963
|
-
rotation: [draftRotation[0],
|
|
1097
|
+
rotation: [draftRotation[0], pose.rotationY, draftRotation[2]],
|
|
964
1098
|
},
|
|
965
1099
|
cursorRotationY: ctx.currentCursorRotationY,
|
|
966
|
-
gridPosition:
|
|
967
|
-
cursorPosition:
|
|
1100
|
+
gridPosition: pose.position,
|
|
1101
|
+
cursorPosition: pose.worldPosition,
|
|
968
1102
|
stopPropagation: true,
|
|
969
1103
|
}
|
|
970
1104
|
},
|
|
@@ -978,25 +1112,21 @@ export const shelfSurfaceStrategy = {
|
|
|
978
1112
|
if (!(ctx.state.shelfId && ctx.draftItem)) return null
|
|
979
1113
|
if (event.node.id !== ctx.state.shelfId) return null
|
|
980
1114
|
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
if (
|
|
990
|
-
|
|
991
|
-
const x = snapToGrid(localPos.x, ourDims[0])
|
|
992
|
-
const z = snapToGrid(localPos.z, ourDims[2])
|
|
993
|
-
const worldSnapped = shelfMesh.localToWorld(new Vector3(x, rowY, z))
|
|
1115
|
+
const pose = resolveShelfSurfacePlacement(
|
|
1116
|
+
event.node as ShelfNode,
|
|
1117
|
+
event,
|
|
1118
|
+
getScaledDimensions(ctx.draftItem),
|
|
1119
|
+
ctx.currentCursorRotationY,
|
|
1120
|
+
false,
|
|
1121
|
+
ctx.onSurfaceReject,
|
|
1122
|
+
)
|
|
1123
|
+
if (!pose) return null
|
|
994
1124
|
|
|
995
1125
|
return {
|
|
996
|
-
gridPosition:
|
|
997
|
-
cursorPosition:
|
|
1126
|
+
gridPosition: pose.position,
|
|
1127
|
+
cursorPosition: pose.worldPosition,
|
|
998
1128
|
cursorRotationY: ctx.currentCursorRotationY,
|
|
999
|
-
nodeUpdate: { position:
|
|
1129
|
+
nodeUpdate: { position: pose.position },
|
|
1000
1130
|
stopPropagation: true,
|
|
1001
1131
|
dirtyNodeId: null,
|
|
1002
1132
|
}
|
|
@@ -1009,6 +1139,7 @@ export const shelfSurfaceStrategy = {
|
|
|
1009
1139
|
if (ctx.state.surface !== 'shelf-surface') return null
|
|
1010
1140
|
if (!(ctx.draftItem && ctx.state.shelfId)) return null
|
|
1011
1141
|
if (event.node.id !== ctx.state.shelfId) return null
|
|
1142
|
+
if (!validCatalogCounterPose(ctx)) return null
|
|
1012
1143
|
|
|
1013
1144
|
return {
|
|
1014
1145
|
nodeUpdate: {
|
|
@@ -1022,14 +1153,6 @@ export const shelfSurfaceStrategy = {
|
|
|
1022
1153
|
},
|
|
1023
1154
|
}
|
|
1024
1155
|
|
|
1025
|
-
/** Same upward-normal heuristic as `isUpwardItemSurfaceHit`, but typed
|
|
1026
|
-
* for `ShelfEvent`. Re-uses the matrix-driven world normal calculation
|
|
1027
|
-
* via a tiny `ItemEvent`-shaped adapter — the function only reads
|
|
1028
|
-
* `event.normal` + `event.object`. */
|
|
1029
|
-
function isUpwardShelfSurfaceHit(event: ShelfEvent): boolean {
|
|
1030
|
-
return isUpwardItemSurfaceHit(event as unknown as ItemEvent)
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
1156
|
// ============================================================================
|
|
1034
1157
|
// VALIDATION
|
|
1035
1158
|
// ============================================================================
|
|
@@ -1041,20 +1164,30 @@ function isUpwardShelfSurfaceHit(event: ShelfEvent): boolean {
|
|
|
1041
1164
|
export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidators): boolean {
|
|
1042
1165
|
if (!(ctx.levelId && ctx.draftItem)) return false
|
|
1043
1166
|
|
|
1044
|
-
// Item surface: valid if we entered (size check was in enter)
|
|
1045
1167
|
if (ctx.state.surface === 'item-surface') {
|
|
1046
|
-
return ctx.state.surfaceItemId !== null
|
|
1168
|
+
return ctx.state.surfaceItemId !== null && validCatalogCounterPose(ctx)
|
|
1047
1169
|
}
|
|
1048
1170
|
|
|
1049
|
-
// Shelf surface: same — size check already happened on enter
|
|
1050
1171
|
if (ctx.state.surface === 'shelf-surface') {
|
|
1051
|
-
return ctx.state.shelfId !== null
|
|
1172
|
+
return ctx.state.shelfId !== null && validCatalogCounterPose(ctx)
|
|
1052
1173
|
}
|
|
1053
1174
|
|
|
1054
1175
|
const attachTo = ctx.draftItem.asset.attachTo
|
|
1055
1176
|
|
|
1056
1177
|
const alignedDims = getGridAlignedDimensions(getScaledDimensions(ctx.draftItem), attachTo)
|
|
1057
1178
|
|
|
1179
|
+
if (ctx.state.surface === 'block-face') {
|
|
1180
|
+
const hostId = ctx.state.blockId
|
|
1181
|
+
const host = hostId ? useScene.getState().nodes[hostId as AnyNodeId] : undefined
|
|
1182
|
+
const faceHost = host ? nodeRegistry.get(host.type)?.capabilities.faceHost : undefined
|
|
1183
|
+
if (!(host && faceHost)) return false
|
|
1184
|
+
return faceHost.isStoredPlacementValid({
|
|
1185
|
+
host,
|
|
1186
|
+
item: ctx.draftItem,
|
|
1187
|
+
asset: ctx.draftItem.asset,
|
|
1188
|
+
})
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1058
1191
|
if (attachTo === 'ceiling') {
|
|
1059
1192
|
if (ctx.state.surface !== 'ceiling' || !ctx.state.ceilingId) return false
|
|
1060
1193
|
return validators.canPlaceOnCeiling(
|