@pascal-app/editor 1.0.0-beta.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +63 -8
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +12 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +23 -6
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/panel-wrapper.tsx +228 -12
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-session-groups.ts +103 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
GridEvent,
|
|
7
7
|
ItemEvent,
|
|
8
8
|
ItemNode,
|
|
9
|
+
NodeEvent,
|
|
9
10
|
RoofEvent,
|
|
10
11
|
RoofNode,
|
|
11
12
|
RoofSegmentNode,
|
|
@@ -30,7 +31,6 @@ import { Euler, Matrix3, Quaternion, Vector3 } from 'three'
|
|
|
30
31
|
import { hasRoofFaceChildOverlap, resolveRoofWallHit } from '../../../lib/roof-wall-hit'
|
|
31
32
|
import { snapWorldXZForActiveBuilding } from '../../../lib/world-grid-snap'
|
|
32
33
|
import {
|
|
33
|
-
calculateCursorRotation,
|
|
34
34
|
calculateItemRotation,
|
|
35
35
|
getGridAlignedDimensions,
|
|
36
36
|
getSideFromNormal,
|
|
@@ -178,6 +178,51 @@ export const floorStrategy = {
|
|
|
178
178
|
// WALL STRATEGY
|
|
179
179
|
// ============================================================================
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Resolve the wall-local node position AND the world pose of the placement
|
|
183
|
+
* wireframe from ONE wall-local point, so the box can't drift from the item it
|
|
184
|
+
* previews.
|
|
185
|
+
*
|
|
186
|
+
* `event.object` is the wall's collision mesh — the frame `event.localPosition`
|
|
187
|
+
* was measured in — so `localToWorld` is the exact inverse of the hit. Snapping
|
|
188
|
+
* the raw world hit per axis instead (the old path) put the box on a different
|
|
189
|
+
* lattice than the node: wall-local X runs from `wall.start`, and wall-local Y
|
|
190
|
+
* is measured from the supporting slab's elevation, not from world zero.
|
|
191
|
+
*
|
|
192
|
+
* `z` follows the hosting convention rather than the hit depth: `wall` items
|
|
193
|
+
* center in the thickness, `wall-side` items mount on the hit face — mirroring
|
|
194
|
+
* `ItemSystem`'s per-frame push, so the wireframe's `z = 0` face lands flush
|
|
195
|
+
* with the wall instead of extending through it.
|
|
196
|
+
*/
|
|
197
|
+
function resolveWallPlacementPose(
|
|
198
|
+
event: WallEvent,
|
|
199
|
+
localX: number,
|
|
200
|
+
localY: number,
|
|
201
|
+
attachTo: 'wall' | 'wall-side',
|
|
202
|
+
side: 'front' | 'back',
|
|
203
|
+
itemRotation: number,
|
|
204
|
+
): {
|
|
205
|
+
position: [number, number, number]
|
|
206
|
+
cursorPosition: [number, number, number]
|
|
207
|
+
cursorRotationY: number
|
|
208
|
+
} {
|
|
209
|
+
const localZ =
|
|
210
|
+
attachTo === 'wall-side' ? ((event.node.thickness ?? 0.1) / 2) * (side === 'front' ? 1 : -1) : 0
|
|
211
|
+
event.object.updateWorldMatrix(true, false)
|
|
212
|
+
const world = event.object.localToWorld(new Vector3(localX, localY, localZ))
|
|
213
|
+
const wallYaw = -Math.atan2(
|
|
214
|
+
event.node.end[1] - event.node.start[1],
|
|
215
|
+
event.node.end[0] - event.node.start[0],
|
|
216
|
+
)
|
|
217
|
+
return {
|
|
218
|
+
position: [localX, localY, localZ],
|
|
219
|
+
cursorPosition: [world.x, world.y, world.z],
|
|
220
|
+
// Same composition the 2D floorplan resolves a wall child with
|
|
221
|
+
// (`resolveItemTransform`): the cursor frame IS the item frame.
|
|
222
|
+
cursorRotationY: wallYaw + itemRotation,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
181
226
|
export const wallStrategy = {
|
|
182
227
|
/**
|
|
183
228
|
* Handle wall:enter — transition from floor to wall surface.
|
|
@@ -201,11 +246,9 @@ export const wallStrategy = {
|
|
|
201
246
|
|
|
202
247
|
const side = getSideFromNormal(event.normal)
|
|
203
248
|
const itemRotation = calculateItemRotation(event.normal)
|
|
204
|
-
const cursorRotation = calculateCursorRotation(event.normal, event.node.start, event.node.end)
|
|
205
249
|
|
|
206
250
|
const x = snapToHalf(event.localPosition[0])
|
|
207
251
|
const y = snapToHalf(event.localPosition[1])
|
|
208
|
-
const z = snapToHalf(event.localPosition[2])
|
|
209
252
|
|
|
210
253
|
// Get auto-adjusted Y position from validator
|
|
211
254
|
const rawDims = ctx.draftItem
|
|
@@ -223,25 +266,27 @@ export const wallStrategy = {
|
|
|
223
266
|
)
|
|
224
267
|
|
|
225
268
|
const adjustedY = validation.adjustedY ?? y
|
|
269
|
+
const pose = resolveWallPlacementPose(event, x, adjustedY, attachTo, side, itemRotation)
|
|
226
270
|
|
|
227
271
|
return {
|
|
228
|
-
stateUpdate: {
|
|
272
|
+
stateUpdate: {
|
|
273
|
+
surface: 'wall',
|
|
274
|
+
wallId: event.node.id,
|
|
275
|
+
roofSegmentId: null,
|
|
276
|
+
},
|
|
229
277
|
nodeUpdate: {
|
|
230
|
-
position:
|
|
278
|
+
position: pose.position,
|
|
231
279
|
parentId: event.node.id,
|
|
232
280
|
// The draft may arrive from a roof-segment wall face.
|
|
233
281
|
roofSegmentId: undefined,
|
|
234
282
|
roofFace: undefined,
|
|
283
|
+
blockFaceId: undefined,
|
|
235
284
|
side,
|
|
236
285
|
rotation: [0, itemRotation, 0],
|
|
237
286
|
},
|
|
238
|
-
cursorRotationY:
|
|
239
|
-
gridPosition:
|
|
240
|
-
cursorPosition:
|
|
241
|
-
snapToHalf(event.position[0]),
|
|
242
|
-
snapToHalf(event.position[1]),
|
|
243
|
-
snapToHalf(event.position[2]),
|
|
244
|
-
],
|
|
287
|
+
cursorRotationY: pose.cursorRotationY,
|
|
288
|
+
gridPosition: pose.position,
|
|
289
|
+
cursorPosition: pose.cursorPosition,
|
|
245
290
|
stopPropagation: true,
|
|
246
291
|
}
|
|
247
292
|
},
|
|
@@ -262,11 +307,10 @@ export const wallStrategy = {
|
|
|
262
307
|
|
|
263
308
|
const side = getSideFromNormal(event.normal)
|
|
264
309
|
const itemRotation = calculateItemRotation(event.normal)
|
|
265
|
-
const
|
|
310
|
+
const attachTo = ctx.draftItem.asset.attachTo as 'wall' | 'wall-side'
|
|
266
311
|
|
|
267
312
|
const snappedX = snapToHalf(event.localPosition[0])
|
|
268
313
|
const snappedY = snapToHalf(event.localPosition[1])
|
|
269
|
-
const snappedZ = snapToHalf(event.localPosition[2])
|
|
270
314
|
|
|
271
315
|
// Get auto-adjusted Y position from validator
|
|
272
316
|
const validation = validators.canPlaceOnWall(
|
|
@@ -275,23 +319,20 @@ export const wallStrategy = {
|
|
|
275
319
|
snappedX,
|
|
276
320
|
snappedY,
|
|
277
321
|
getGridAlignedDimensions(getScaledDimensions(ctx.draftItem), ctx.draftItem.asset.attachTo),
|
|
278
|
-
|
|
322
|
+
attachTo,
|
|
279
323
|
side,
|
|
280
324
|
[ctx.draftItem.id],
|
|
281
325
|
)
|
|
282
326
|
|
|
283
327
|
const adjustedY = validation.adjustedY ?? snappedY
|
|
328
|
+
const pose = resolveWallPlacementPose(event, snappedX, adjustedY, attachTo, side, itemRotation)
|
|
284
329
|
|
|
285
330
|
return {
|
|
286
|
-
gridPosition:
|
|
287
|
-
cursorPosition:
|
|
288
|
-
|
|
289
|
-
snapToHalf(event.position[1]),
|
|
290
|
-
snapToHalf(event.position[2]),
|
|
291
|
-
],
|
|
292
|
-
cursorRotationY: cursorRotation,
|
|
331
|
+
gridPosition: pose.position,
|
|
332
|
+
cursorPosition: pose.cursorPosition,
|
|
333
|
+
cursorRotationY: pose.cursorRotationY,
|
|
293
334
|
nodeUpdate: {
|
|
294
|
-
position:
|
|
335
|
+
position: pose.position,
|
|
295
336
|
side,
|
|
296
337
|
rotation: [0, itemRotation, 0],
|
|
297
338
|
},
|
|
@@ -332,6 +373,7 @@ export const wallStrategy = {
|
|
|
332
373
|
parentId: event.node.id,
|
|
333
374
|
roofSegmentId: undefined,
|
|
334
375
|
roofFace: undefined,
|
|
376
|
+
blockFaceId: undefined,
|
|
335
377
|
side: ctx.draftItem.side,
|
|
336
378
|
rotation: ctx.draftItem.rotation,
|
|
337
379
|
metadata: stripTransient(ctx.draftItem.metadata),
|
|
@@ -485,12 +527,17 @@ export const roofWallStrategy = {
|
|
|
485
527
|
if (!target) return null
|
|
486
528
|
|
|
487
529
|
return {
|
|
488
|
-
stateUpdate: {
|
|
530
|
+
stateUpdate: {
|
|
531
|
+
surface: 'roof-wall',
|
|
532
|
+
roofSegmentId: target.segment.id,
|
|
533
|
+
wallId: null,
|
|
534
|
+
},
|
|
489
535
|
nodeUpdate: {
|
|
490
536
|
position: target.position,
|
|
491
537
|
parentId: target.segment.id,
|
|
492
538
|
roofSegmentId: target.segment.id,
|
|
493
539
|
roofFace: target.faceId,
|
|
540
|
+
blockFaceId: undefined,
|
|
494
541
|
wallId: undefined,
|
|
495
542
|
side: 'front',
|
|
496
543
|
rotation: [0, 0, 0],
|
|
@@ -548,6 +595,7 @@ export const roofWallStrategy = {
|
|
|
548
595
|
parentId: ctx.state.roofSegmentId,
|
|
549
596
|
roofSegmentId: ctx.state.roofSegmentId,
|
|
550
597
|
roofFace: ctx.draftItem.roofFace,
|
|
598
|
+
blockFaceId: undefined,
|
|
551
599
|
wallId: undefined,
|
|
552
600
|
side: 'front',
|
|
553
601
|
rotation: [0, 0, 0],
|
|
@@ -580,6 +628,114 @@ export const roofWallStrategy = {
|
|
|
580
628
|
},
|
|
581
629
|
}
|
|
582
630
|
|
|
631
|
+
// ============================================================================
|
|
632
|
+
// FACE HOST STRATEGY
|
|
633
|
+
// ============================================================================
|
|
634
|
+
|
|
635
|
+
function resolveFaceHostTarget(ctx: PlacementContext, event: NodeEvent) {
|
|
636
|
+
const faceHost = nodeRegistry.get(event.node.type)?.capabilities.faceHost
|
|
637
|
+
if (!faceHost) return null
|
|
638
|
+
const rawDimensions = ctx.draftItem
|
|
639
|
+
? getScaledDimensions(ctx.draftItem)
|
|
640
|
+
: (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
|
641
|
+
return faceHost.resolvePlacement({
|
|
642
|
+
host: event.node,
|
|
643
|
+
asset: ctx.asset,
|
|
644
|
+
draftItem: ctx.draftItem,
|
|
645
|
+
localPosition: event.localPosition,
|
|
646
|
+
faceIndex: event.faceIndex,
|
|
647
|
+
object: event.object,
|
|
648
|
+
currentFaceId: faceHost.currentFaceId(ctx.draftItem),
|
|
649
|
+
rawDimensions,
|
|
650
|
+
dimensions: getGridAlignedDimensions(rawDimensions, ctx.asset.attachTo),
|
|
651
|
+
snapScalar: snapToHalf,
|
|
652
|
+
})
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function clearFaceHostItemFields(ctx: PlacementContext): Partial<ItemNode> {
|
|
656
|
+
const host = ctx.state.blockId ? useScene.getState().nodes[ctx.state.blockId] : undefined
|
|
657
|
+
const clearFields = host
|
|
658
|
+
? nodeRegistry.get(host.type)?.capabilities.faceHost?.clearItemFields
|
|
659
|
+
: undefined
|
|
660
|
+
const patch: Partial<ItemNode> = {}
|
|
661
|
+
for (const field of clearFields ?? []) {
|
|
662
|
+
;(patch as Record<string, unknown>)[field] = undefined
|
|
663
|
+
}
|
|
664
|
+
return patch
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export const faceHostStrategy = {
|
|
668
|
+
enter(ctx: PlacementContext, event: NodeEvent): TransitionResult | null {
|
|
669
|
+
const target = resolveFaceHostTarget(ctx, event)
|
|
670
|
+
if (!target) return null
|
|
671
|
+
return {
|
|
672
|
+
stateUpdate: {
|
|
673
|
+
surface: 'block-face',
|
|
674
|
+
blockId: event.node.id,
|
|
675
|
+
wallId: null,
|
|
676
|
+
roofSegmentId: null,
|
|
677
|
+
},
|
|
678
|
+
nodeUpdate: {
|
|
679
|
+
...target.nodeUpdate,
|
|
680
|
+
},
|
|
681
|
+
gridPosition: target.position,
|
|
682
|
+
cursorPosition: target.cursorPosition,
|
|
683
|
+
cursorRotationY: target.cursorRotation[1],
|
|
684
|
+
cursorRotation: target.cursorRotation,
|
|
685
|
+
stopPropagation: true,
|
|
686
|
+
hostFaceId: target.faceId,
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
|
|
690
|
+
move(ctx: PlacementContext, event: NodeEvent): PlacementResult | null {
|
|
691
|
+
if (ctx.state.surface !== 'block-face' || !ctx.draftItem) return null
|
|
692
|
+
const target = resolveFaceHostTarget(ctx, event)
|
|
693
|
+
if (!target || event.node.id !== ctx.state.blockId) return null
|
|
694
|
+
return {
|
|
695
|
+
gridPosition: target.position,
|
|
696
|
+
cursorPosition: target.cursorPosition,
|
|
697
|
+
cursorRotationY: target.cursorRotation[1],
|
|
698
|
+
cursorRotation: target.cursorRotation,
|
|
699
|
+
nodeUpdate: target.nodeUpdate,
|
|
700
|
+
stopPropagation: true,
|
|
701
|
+
dirtyNodeId: null,
|
|
702
|
+
hostFaceId: target.faceId,
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
|
|
706
|
+
click(ctx: PlacementContext, event: NodeEvent): CommitResult | null {
|
|
707
|
+
if (ctx.state.surface !== 'block-face' || !ctx.draftItem) return null
|
|
708
|
+
const target = resolveFaceHostTarget(ctx, event)
|
|
709
|
+
if (!target || event.node.id !== ctx.state.blockId) return null
|
|
710
|
+
return {
|
|
711
|
+
nodeUpdate: {
|
|
712
|
+
...target.nodeUpdate,
|
|
713
|
+
metadata: stripTransient(ctx.draftItem.metadata),
|
|
714
|
+
},
|
|
715
|
+
stopPropagation: true,
|
|
716
|
+
dirtyNodeId: null,
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
|
|
720
|
+
leave(ctx: PlacementContext): TransitionResult | null {
|
|
721
|
+
if (ctx.state.surface !== 'block-face') return null
|
|
722
|
+
const floorPosition: [number, number, number] = [ctx.gridPosition.x, 0, ctx.gridPosition.z]
|
|
723
|
+
return {
|
|
724
|
+
stateUpdate: { surface: 'floor', blockId: null },
|
|
725
|
+
nodeUpdate: {
|
|
726
|
+
...clearFaceHostItemFields(ctx),
|
|
727
|
+
position: floorPosition,
|
|
728
|
+
parentId: ctx.levelId,
|
|
729
|
+
rotation: [0, ctx.currentCursorRotationY, 0],
|
|
730
|
+
},
|
|
731
|
+
cursorRotationY: ctx.currentCursorRotationY,
|
|
732
|
+
gridPosition: floorPosition,
|
|
733
|
+
cursorPosition: floorPosition,
|
|
734
|
+
stopPropagation: true,
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
}
|
|
738
|
+
|
|
583
739
|
// ============================================================================
|
|
584
740
|
// CEILING STRATEGY
|
|
585
741
|
// ============================================================================
|
|
@@ -1020,6 +1176,18 @@ export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidato
|
|
|
1020
1176
|
|
|
1021
1177
|
const alignedDims = getGridAlignedDimensions(getScaledDimensions(ctx.draftItem), attachTo)
|
|
1022
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
|
+
|
|
1023
1191
|
if (attachTo === 'ceiling') {
|
|
1024
1192
|
if (ctx.state.surface !== 'ceiling' || !ctx.state.ceilingId) return false
|
|
1025
1193
|
return validators.canPlaceOnCeiling(
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { Matrix4, Quaternion, Vector3 } from 'three'
|
|
3
|
+
import { resolveItemPlacementSurfaceNormal } from './placement-surface'
|
|
4
|
+
|
|
5
|
+
describe('resolveItemPlacementSurfaceNormal', () => {
|
|
6
|
+
test('uses the full face normal for a sloped block host', () => {
|
|
7
|
+
const normal = new Vector3(0, 0.6, 0.8).normalize()
|
|
8
|
+
const xAxis = new Vector3(1, 0, 0)
|
|
9
|
+
const yAxis = new Vector3().crossVectors(normal, xAxis).normalize()
|
|
10
|
+
const faceQuaternion = new Quaternion().setFromRotationMatrix(
|
|
11
|
+
new Matrix4().makeBasis(xAxis, yAxis, normal),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const resolved = resolveItemPlacementSurfaceNormal(
|
|
15
|
+
'block-face',
|
|
16
|
+
faceQuaternion,
|
|
17
|
+
null,
|
|
18
|
+
new Vector3(),
|
|
19
|
+
'wall',
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
expect(resolved.toArray()).toEqual(normal.toArray())
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('uses the upward host normal for a floor item on a block top face', () => {
|
|
26
|
+
const faceQuaternion = new Quaternion().setFromRotationMatrix(
|
|
27
|
+
new Matrix4().makeBasis(new Vector3(1, 0, 0), new Vector3(0, 0, -1), new Vector3(0, 1, 0)),
|
|
28
|
+
)
|
|
29
|
+
const itemQuaternion = faceQuaternion.multiply(
|
|
30
|
+
new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), Math.PI / 2),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const resolved = resolveItemPlacementSurfaceNormal(
|
|
34
|
+
'block-face',
|
|
35
|
+
itemQuaternion,
|
|
36
|
+
null,
|
|
37
|
+
new Vector3(),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
expect(resolved.toArray().map((value) => Math.round(value))).toEqual([0, 1, 0])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('uses the downward host normal for a block ceiling attachment', () => {
|
|
44
|
+
const faceQuaternion = new Quaternion().setFromRotationMatrix(
|
|
45
|
+
new Matrix4().makeBasis(new Vector3(1, 0, 0), new Vector3(0, 0, 1), new Vector3(0, -1, 0)),
|
|
46
|
+
)
|
|
47
|
+
const itemQuaternion = faceQuaternion.multiply(
|
|
48
|
+
new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), -Math.PI / 2),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const resolved = resolveItemPlacementSurfaceNormal(
|
|
52
|
+
'block-face',
|
|
53
|
+
itemQuaternion,
|
|
54
|
+
null,
|
|
55
|
+
new Vector3(),
|
|
56
|
+
'ceiling',
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
expect(resolved.toArray().map((value) => Math.round(value))).toEqual([0, -1, 0])
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Quaternion, Vector3 } from 'three'
|
|
2
|
+
import type { SurfaceType } from './placement-types'
|
|
3
|
+
|
|
4
|
+
export function resolveItemPlacementSurfaceNormal(
|
|
5
|
+
surface: SurfaceType,
|
|
6
|
+
ghostWorldQuaternion: Quaternion,
|
|
7
|
+
hostedItemWorldQuaternion: Quaternion | null,
|
|
8
|
+
target: Vector3,
|
|
9
|
+
attachTo?: 'wall' | 'wall-side' | 'ceiling',
|
|
10
|
+
): Vector3 {
|
|
11
|
+
if (surface === 'block-face') {
|
|
12
|
+
if (attachTo === 'ceiling') target.set(0, -1, 0)
|
|
13
|
+
else if (attachTo === 'wall' || attachTo === 'wall-side') target.set(0, 0, 1)
|
|
14
|
+
else target.set(0, 1, 0)
|
|
15
|
+
target.applyQuaternion(ghostWorldQuaternion)
|
|
16
|
+
if (target.lengthSq() > 1e-6) return target.normalize()
|
|
17
|
+
}
|
|
18
|
+
if (surface === 'wall' || surface === 'roof-wall') {
|
|
19
|
+
target.set(0, 0, 1).applyQuaternion(hostedItemWorldQuaternion ?? ghostWorldQuaternion)
|
|
20
|
+
target.y = 0
|
|
21
|
+
if (target.lengthSq() > 1e-6) return target.normalize()
|
|
22
|
+
}
|
|
23
|
+
return target.set(0, 1, 0)
|
|
24
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AnyNode,
|
|
3
|
+
AnyNodeId,
|
|
3
4
|
AssetInput,
|
|
4
5
|
CeilingNode,
|
|
5
6
|
ItemNode,
|
|
@@ -16,6 +17,7 @@ export type SurfaceType =
|
|
|
16
17
|
| 'floor'
|
|
17
18
|
| 'wall'
|
|
18
19
|
| 'roof-wall'
|
|
20
|
+
| 'block-face'
|
|
19
21
|
| 'ceiling'
|
|
20
22
|
| 'item-surface'
|
|
21
23
|
| 'shelf-surface'
|
|
@@ -33,6 +35,8 @@ export interface PlacementState {
|
|
|
33
35
|
* (base walls + coplanar gable ends).
|
|
34
36
|
*/
|
|
35
37
|
roofSegmentId: string | null
|
|
38
|
+
/** Active planar node face used as a wall-like attachment host. */
|
|
39
|
+
blockId?: AnyNodeId | null
|
|
36
40
|
ceilingId: string | null
|
|
37
41
|
surfaceItemId: string | null
|
|
38
42
|
/**
|
|
@@ -81,6 +85,7 @@ export interface PlacementResult {
|
|
|
81
85
|
nodeUpdate: Partial<ItemNode> | null
|
|
82
86
|
stopPropagation: boolean
|
|
83
87
|
dirtyNodeId: AnyNode['id'] | null
|
|
88
|
+
hostFaceId?: string | null
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
/**
|
|
@@ -94,6 +99,7 @@ export interface TransitionResult {
|
|
|
94
99
|
cursorRotationY: number
|
|
95
100
|
cursorRotation?: [number, number, number]
|
|
96
101
|
stopPropagation: boolean
|
|
102
|
+
hostFaceId?: string | null
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
/**
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlockNode,
|
|
3
|
+
type BlockNode as BlockNodeType,
|
|
4
|
+
type FaceHostCapability,
|
|
5
|
+
type ItemNode,
|
|
6
|
+
nodeRegistry,
|
|
7
|
+
registerNode,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
|
|
10
|
+
const faceForHit = (object: unknown, faceIndex = 0) => {
|
|
11
|
+
const geometry = (object as { geometry?: { userData?: Record<string, unknown> } }).geometry
|
|
12
|
+
const faces = geometry?.userData?.blockFaces
|
|
13
|
+
if (!Array.isArray(faces)) return null
|
|
14
|
+
return (
|
|
15
|
+
faces.find((face) => {
|
|
16
|
+
if (!face || typeof face !== 'object') return false
|
|
17
|
+
const entry = face as { start?: number; count?: number }
|
|
18
|
+
return (
|
|
19
|
+
typeof entry.start === 'number' &&
|
|
20
|
+
typeof entry.count === 'number' &&
|
|
21
|
+
faceIndex * 3 >= entry.start &&
|
|
22
|
+
faceIndex * 3 < entry.start + entry.count
|
|
23
|
+
)
|
|
24
|
+
}) as { faceId?: string } | undefined
|
|
25
|
+
)?.faceId
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const testFaceHost: FaceHostCapability<BlockNodeType> = {
|
|
29
|
+
currentFaceId: (item) => item?.blockFaceId ?? null,
|
|
30
|
+
clearItemFields: ['position', 'rotation', 'blockFaceId'],
|
|
31
|
+
resolvePlacement: ({ asset, currentFaceId, faceIndex, host, localPosition, object }) => {
|
|
32
|
+
const hitFaceId = faceForHit(object, faceIndex) ?? null
|
|
33
|
+
const faceId = currentFaceId ?? hitFaceId
|
|
34
|
+
if (!faceId) return null
|
|
35
|
+
|
|
36
|
+
if (faceId === 'f-front') {
|
|
37
|
+
if (asset.attachTo !== 'wall-side' || localPosition[2] > -0.75) return null
|
|
38
|
+
return {
|
|
39
|
+
cursorPosition: [localPosition[0], localPosition[1], -1],
|
|
40
|
+
position: [localPosition[0], localPosition[1], -1],
|
|
41
|
+
rotation: [0, 0, 0],
|
|
42
|
+
cursorRotation: [0, 0, 0],
|
|
43
|
+
faceId,
|
|
44
|
+
nodeUpdate: {
|
|
45
|
+
parentId: host.id,
|
|
46
|
+
blockFaceId: faceId,
|
|
47
|
+
position: [localPosition[0], localPosition[1], 0],
|
|
48
|
+
rotation: [0, 0, 0],
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (faceId === 'f-bottom') {
|
|
54
|
+
if (asset.attachTo !== 'ceiling') return null
|
|
55
|
+
return {
|
|
56
|
+
cursorPosition: [0, -0.25, 0],
|
|
57
|
+
position: [0, 0, 0.25],
|
|
58
|
+
rotation: [-Math.PI / 2, 0, 0],
|
|
59
|
+
cursorRotation: [-Math.PI / 2, 0, 0],
|
|
60
|
+
faceId,
|
|
61
|
+
nodeUpdate: {
|
|
62
|
+
parentId: host.id,
|
|
63
|
+
blockFaceId: faceId,
|
|
64
|
+
position: [0, 0, 0.25],
|
|
65
|
+
rotation: [-Math.PI / 2, 0, 0],
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (faceId === 'f-top') {
|
|
71
|
+
if (asset.attachTo) return null
|
|
72
|
+
return {
|
|
73
|
+
cursorPosition: [0, 2.4, 0],
|
|
74
|
+
position: [0, 2.4, 0],
|
|
75
|
+
rotation: [Math.PI / 2, 0, 0],
|
|
76
|
+
cursorRotation: [Math.PI / 2, 0, 0],
|
|
77
|
+
faceId,
|
|
78
|
+
nodeUpdate: {
|
|
79
|
+
parentId: host.id,
|
|
80
|
+
blockFaceId: faceId,
|
|
81
|
+
position: [0, 0, 0],
|
|
82
|
+
rotation: [Math.PI / 2, 0, 0],
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null
|
|
88
|
+
},
|
|
89
|
+
storedPlacementPatch: ({ host, item, position }) => {
|
|
90
|
+
if (!item.blockFaceId) return null
|
|
91
|
+
return {
|
|
92
|
+
parentId: host.id,
|
|
93
|
+
blockFaceId: item.blockFaceId,
|
|
94
|
+
position: [position[0], position[1], position[2]],
|
|
95
|
+
rotation: item.rotation,
|
|
96
|
+
roofSegmentId: undefined,
|
|
97
|
+
roofFace: undefined,
|
|
98
|
+
wallId: undefined,
|
|
99
|
+
side: 'front',
|
|
100
|
+
} satisfies Partial<ItemNode>
|
|
101
|
+
},
|
|
102
|
+
isStoredPlacementValid: ({ item }) => Boolean(item.blockFaceId),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function registerTestBlockFaceHost() {
|
|
106
|
+
// The registry is a module singleton shared across test files, and other
|
|
107
|
+
// suites register their own minimal `block` (the wall drafting stub is
|
|
108
|
+
// floor-placed only). Skipping on name alone would leave that capability-less
|
|
109
|
+
// definition in place, so replace it unless it already hosts faces.
|
|
110
|
+
if (nodeRegistry.get('block')?.capabilities?.faceHost) return
|
|
111
|
+
if (nodeRegistry.has('block')) nodeRegistry._reset()
|
|
112
|
+
registerNode({
|
|
113
|
+
kind: 'block',
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
schema: BlockNode,
|
|
116
|
+
category: 'structure',
|
|
117
|
+
defaults: () => BlockNode.parse({ id: 'block_test', parentId: null }) as never,
|
|
118
|
+
capabilities: { faceHost: testFaceHost },
|
|
119
|
+
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
120
|
+
} as never)
|
|
121
|
+
}
|