@pascal-app/editor 0.6.0 → 0.7.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/package.json +9 -5
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +75 -7
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +20 -0
- package/src/components/editor/first-person/build-collider-world.ts +365 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +32 -55
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +9855 -3298
- package/src/components/editor/index.tsx +269 -21
- package/src/components/editor/selection-manager.tsx +575 -13
- package/src/components/editor/thumbnail-generator.tsx +38 -7
- package/src/components/editor/use-floorplan-background-placement.ts +257 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
- package/src/components/editor/use-floorplan-scene-data.ts +189 -0
- package/src/components/editor/wall-measurement-label.tsx +267 -36
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
- package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
- package/src/components/editor-2d/svg-paths.ts +119 -0
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/column/column-tool.tsx +97 -0
- package/src/components/tools/column/move-column-tool.tsx +105 -0
- package/src/components/tools/door/door-tool.tsx +7 -0
- package/src/components/tools/door/move-door-tool.tsx +28 -8
- package/src/components/tools/fence/fence-drafting.ts +10 -3
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +129 -18
- package/src/components/tools/fence/move-fence-tool.tsx +101 -34
- package/src/components/tools/item/move-tool.tsx +10 -1
- package/src/components/tools/item/placement-math.ts +30 -1
- package/src/components/tools/item/placement-strategies.ts +109 -31
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +2 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +660 -52
- package/src/components/tools/roof/move-roof-tool.tsx +22 -15
- package/src/components/tools/shared/polygon-editor.tsx +153 -28
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
- package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
- package/src/components/tools/spawn/spawn-tool.tsx +130 -0
- package/src/components/tools/tool-manager.tsx +18 -3
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +121 -20
- package/src/components/tools/wall/wall-drafting.ts +18 -9
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +18 -0
- package/src/components/tools/window/window-tool.tsx +5 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +28 -1
- package/src/components/ui/action-menu/index.tsx +91 -1
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +424 -35
- package/src/components/ui/command-palette/editor-commands.tsx +18 -1
- package/src/components/ui/controls/material-picker.tsx +152 -165
- package/src/components/ui/controls/slider-control.tsx +66 -18
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1116 -1219
- package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
- package/src/components/ui/level-duplicate-dialog.tsx +115 -0
- package/src/components/ui/panels/ceiling-panel.tsx +1 -25
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +981 -289
- package/src/components/ui/panels/fence-panel.tsx +3 -45
- package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
- package/src/components/ui/panels/node-display.ts +39 -0
- package/src/components/ui/panels/paint-panel.tsx +138 -0
- package/src/components/ui/panels/panel-manager.tsx +210 -1
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +238 -5
- package/src/components/ui/panels/roof-panel.tsx +4 -105
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
- package/src/components/ui/panels/slab-panel.tsx +4 -30
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +11 -117
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
- package/src/components/ui/panels/wall-panel.tsx +1 -95
- package/src/components/ui/panels/window-panel.tsx +660 -139
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +109 -24
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +9 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +42 -1
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-keyboard.ts +64 -7
- package/src/hooks/use-mobile.ts +12 -12
- package/src/lib/door-interaction.ts +88 -0
- package/src/lib/floorplan/geometry.ts +263 -0
- package/src/lib/floorplan/index.ts +38 -0
- package/src/lib/floorplan/items.ts +179 -0
- package/src/lib/floorplan/selection-tool.ts +231 -0
- package/src/lib/floorplan/stairs.ts +478 -0
- package/src/lib/floorplan/types.ts +57 -0
- package/src/lib/floorplan/walls.ts +23 -0
- package/src/lib/guide-events.ts +10 -0
- package/src/lib/level-duplication.test.ts +72 -0
- package/src/lib/level-duplication.ts +153 -0
- package/src/lib/local-guide-image.ts +42 -0
- package/src/lib/material-paint.ts +284 -0
- package/src/lib/roof-duplication.ts +214 -0
- package/src/lib/scene-bounds.test.ts +183 -0
- package/src/lib/scene-bounds.ts +169 -0
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +164 -8
|
@@ -9,11 +9,17 @@ import type {
|
|
|
9
9
|
WallEvent,
|
|
10
10
|
WallNode,
|
|
11
11
|
} from '@pascal-app/core'
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
getScaledDimensions,
|
|
14
|
+
isLowProfileItemSurface,
|
|
15
|
+
sceneRegistry,
|
|
16
|
+
useScene,
|
|
17
|
+
} from '@pascal-app/core'
|
|
18
|
+
import { Euler, Matrix3, Quaternion, Vector3 } from 'three'
|
|
14
19
|
import {
|
|
15
20
|
calculateCursorRotation,
|
|
16
21
|
calculateItemRotation,
|
|
22
|
+
getGridAlignedDimensions,
|
|
17
23
|
getSideFromNormal,
|
|
18
24
|
isValidWallSideFace,
|
|
19
25
|
snapToGrid,
|
|
@@ -30,6 +36,46 @@ import type {
|
|
|
30
36
|
} from './placement-types'
|
|
31
37
|
|
|
32
38
|
const DEFAULT_DIMENSIONS: [number, number, number] = [1, 1, 1]
|
|
39
|
+
const UPWARD_SURFACE_NORMAL_MIN_Y = 0.75
|
|
40
|
+
|
|
41
|
+
function getWorldNormalY(event: ItemEvent): number | null {
|
|
42
|
+
if (!event.normal) return null
|
|
43
|
+
|
|
44
|
+
const normal = new Vector3(event.normal[0], event.normal[1], event.normal[2])
|
|
45
|
+
normal.applyNormalMatrix(new Matrix3().getNormalMatrix(event.object.matrixWorld)).normalize()
|
|
46
|
+
return normal.y
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isUpwardItemSurfaceHit(event: ItemEvent): boolean {
|
|
50
|
+
const normalY = getWorldNormalY(event)
|
|
51
|
+
return normalY !== null && normalY >= UPWARD_SURFACE_NORMAL_MIN_Y
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getSurfacePlacementHeight(surfaceItem: ItemNode, event: ItemEvent, localPos: Vector3) {
|
|
55
|
+
if (isLowProfileItemSurface(surfaceItem)) return null
|
|
56
|
+
if (!isUpwardItemSurfaceHit(event)) return null
|
|
57
|
+
|
|
58
|
+
if (surfaceItem.asset.surface) {
|
|
59
|
+
return surfaceItem.asset.surface.height * surfaceItem.scale[1]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!Number.isFinite(localPos.y)) return null
|
|
63
|
+
return localPos.y
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isDescendantOfItem(
|
|
67
|
+
candidate: ItemNode,
|
|
68
|
+
ancestor: ItemNode,
|
|
69
|
+
nodes: Record<string, AnyNode>,
|
|
70
|
+
): boolean {
|
|
71
|
+
let parentId = candidate.parentId
|
|
72
|
+
while (parentId) {
|
|
73
|
+
if (parentId === ancestor.id) return true
|
|
74
|
+
const parent = nodes[parentId as AnyNodeId]
|
|
75
|
+
parentId = parent?.parentId ?? null
|
|
76
|
+
}
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
33
79
|
|
|
34
80
|
// ============================================================================
|
|
35
81
|
// FLOOR STRATEGY
|
|
@@ -43,9 +89,10 @@ export const floorStrategy = {
|
|
|
43
89
|
move(ctx: PlacementContext, event: GridEvent): PlacementResult | null {
|
|
44
90
|
if (ctx.state.surface !== 'floor') return null
|
|
45
91
|
|
|
46
|
-
const
|
|
92
|
+
const rawDims = ctx.draftItem
|
|
47
93
|
? getScaledDimensions(ctx.draftItem)
|
|
48
94
|
: (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
|
95
|
+
const dims = getGridAlignedDimensions(rawDims, ctx.asset.attachTo)
|
|
49
96
|
const [dimX, , dimZ] = dims
|
|
50
97
|
const rotY = ctx.draftItem?.rotation?.[1] ?? 0
|
|
51
98
|
const swapDims = Math.abs(Math.sin(rotY)) > 0.9
|
|
@@ -80,7 +127,7 @@ export const floorStrategy = {
|
|
|
80
127
|
const valid = validators.canPlaceOnFloor(
|
|
81
128
|
ctx.levelId,
|
|
82
129
|
pos,
|
|
83
|
-
getScaledDimensions(ctx.draftItem),
|
|
130
|
+
getGridAlignedDimensions(getScaledDimensions(ctx.draftItem), ctx.draftItem.asset.attachTo),
|
|
84
131
|
ctx.draftItem.rotation,
|
|
85
132
|
[ctx.draftItem.id],
|
|
86
133
|
).valid
|
|
@@ -133,14 +180,15 @@ export const wallStrategy = {
|
|
|
133
180
|
const z = snapToHalf(event.localPosition[2])
|
|
134
181
|
|
|
135
182
|
// Get auto-adjusted Y position from validator
|
|
183
|
+
const rawDims = ctx.draftItem
|
|
184
|
+
? getScaledDimensions(ctx.draftItem)
|
|
185
|
+
: (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
|
136
186
|
const validation = validators.canPlaceOnWall(
|
|
137
187
|
ctx.levelId,
|
|
138
188
|
event.node.id,
|
|
139
189
|
x,
|
|
140
190
|
y,
|
|
141
|
-
|
|
142
|
-
? getScaledDimensions(ctx.draftItem)
|
|
143
|
-
: (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS),
|
|
191
|
+
getGridAlignedDimensions(rawDims, attachTo),
|
|
144
192
|
attachTo,
|
|
145
193
|
side,
|
|
146
194
|
[],
|
|
@@ -195,7 +243,7 @@ export const wallStrategy = {
|
|
|
195
243
|
event.node.id,
|
|
196
244
|
snappedX,
|
|
197
245
|
snappedY,
|
|
198
|
-
getScaledDimensions(ctx.draftItem),
|
|
246
|
+
getGridAlignedDimensions(getScaledDimensions(ctx.draftItem), ctx.draftItem.asset.attachTo),
|
|
199
247
|
ctx.draftItem.asset.attachTo as 'wall' | 'wall-side',
|
|
200
248
|
side,
|
|
201
249
|
[ctx.draftItem.id],
|
|
@@ -239,7 +287,7 @@ export const wallStrategy = {
|
|
|
239
287
|
ctx.state.wallId as WallNode['id'],
|
|
240
288
|
ctx.gridPosition.x,
|
|
241
289
|
ctx.gridPosition.y,
|
|
242
|
-
getScaledDimensions(ctx.draftItem),
|
|
290
|
+
getGridAlignedDimensions(getScaledDimensions(ctx.draftItem), ctx.draftItem.asset.attachTo),
|
|
243
291
|
ctx.draftItem.asset.attachTo as 'wall' | 'wall-side',
|
|
244
292
|
ctx.draftItem.side,
|
|
245
293
|
[ctx.draftItem.id],
|
|
@@ -301,16 +349,20 @@ export const ceilingStrategy = {
|
|
|
301
349
|
const ceilingLevelId = resolveLevelId(event.node, nodes)
|
|
302
350
|
if (ctx.levelId !== ceilingLevelId) return null
|
|
303
351
|
|
|
304
|
-
const
|
|
352
|
+
const rawDims = ctx.draftItem
|
|
305
353
|
? getScaledDimensions(ctx.draftItem)
|
|
306
354
|
: (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
|
355
|
+
const dims = getGridAlignedDimensions(rawDims, ctx.asset.attachTo)
|
|
307
356
|
const [dimX, , dimZ] = dims
|
|
308
|
-
const itemHeight =
|
|
357
|
+
const itemHeight = rawDims[1]
|
|
309
358
|
const rotY = ctx.draftItem?.rotation?.[1] ?? 0
|
|
310
359
|
const swapDims = Math.abs(Math.sin(rotY)) > 0.9
|
|
311
360
|
|
|
312
|
-
|
|
313
|
-
|
|
361
|
+
// Ceiling items are stored in ceiling-local coordinates, so snapping must
|
|
362
|
+
// use the ceiling hit's local position rather than world position.
|
|
363
|
+
const x = snapToGrid(event.localPosition[0], swapDims ? dimZ : dimX)
|
|
364
|
+
const z = snapToGrid(event.localPosition[2], swapDims ? dimX : dimZ)
|
|
365
|
+
const worldSnapped = event.object.localToWorld(new Vector3(x, -itemHeight, z))
|
|
314
366
|
|
|
315
367
|
return {
|
|
316
368
|
stateUpdate: { surface: 'ceiling', ceilingId: event.node.id },
|
|
@@ -320,7 +372,7 @@ export const ceilingStrategy = {
|
|
|
320
372
|
},
|
|
321
373
|
cursorRotationY: 0,
|
|
322
374
|
gridPosition: [x, -itemHeight, z],
|
|
323
|
-
cursorPosition: [x,
|
|
375
|
+
cursorPosition: [worldSnapped.x, worldSnapped.y, worldSnapped.z],
|
|
324
376
|
stopPropagation: true,
|
|
325
377
|
}
|
|
326
378
|
},
|
|
@@ -332,18 +384,20 @@ export const ceilingStrategy = {
|
|
|
332
384
|
if (ctx.state.surface !== 'ceiling') return null
|
|
333
385
|
if (!ctx.draftItem) return null
|
|
334
386
|
|
|
335
|
-
const
|
|
387
|
+
const rawDims = getScaledDimensions(ctx.draftItem)
|
|
388
|
+
const dims = getGridAlignedDimensions(rawDims, ctx.draftItem.asset.attachTo)
|
|
336
389
|
const [dimX, , dimZ] = dims
|
|
337
|
-
const itemHeight =
|
|
390
|
+
const itemHeight = rawDims[1]
|
|
338
391
|
const rotY = ctx.draftItem.rotation?.[1] ?? 0
|
|
339
392
|
const swapDims = Math.abs(Math.sin(rotY)) > 0.9
|
|
340
393
|
|
|
341
|
-
const x = snapToGrid(event.
|
|
342
|
-
const z = snapToGrid(event.
|
|
394
|
+
const x = snapToGrid(event.localPosition[0], swapDims ? dimZ : dimX)
|
|
395
|
+
const z = snapToGrid(event.localPosition[2], swapDims ? dimX : dimZ)
|
|
396
|
+
const worldSnapped = event.object.localToWorld(new Vector3(x, -itemHeight, z))
|
|
343
397
|
|
|
344
398
|
return {
|
|
345
399
|
gridPosition: [x, -itemHeight, z],
|
|
346
|
-
cursorPosition: [x,
|
|
400
|
+
cursorPosition: [worldSnapped.x, worldSnapped.y, worldSnapped.z],
|
|
347
401
|
cursorRotationY: 0,
|
|
348
402
|
nodeUpdate: null,
|
|
349
403
|
stopPropagation: true,
|
|
@@ -371,7 +425,7 @@ export const ceilingStrategy = {
|
|
|
371
425
|
const valid = validators.canPlaceOnCeiling(
|
|
372
426
|
ctx.state.ceilingId as CeilingNode['id'],
|
|
373
427
|
pos,
|
|
374
|
-
getScaledDimensions(ctx.draftItem),
|
|
428
|
+
getGridAlignedDimensions(getScaledDimensions(ctx.draftItem), ctx.draftItem.asset.attachTo),
|
|
375
429
|
ctx.draftItem.rotation,
|
|
376
430
|
[ctx.draftItem.id],
|
|
377
431
|
).valid
|
|
@@ -425,8 +479,11 @@ export const itemSurfaceStrategy = {
|
|
|
425
479
|
const surfaceItem = event.node as ItemNode
|
|
426
480
|
// Don't surface-place on the draft itself
|
|
427
481
|
if (surfaceItem.id === ctx.draftItem?.id) return null
|
|
428
|
-
|
|
429
|
-
|
|
482
|
+
if (ctx.state.surface === 'item-surface' && ctx.state.surfaceItemId === surfaceItem.id) {
|
|
483
|
+
return null
|
|
484
|
+
}
|
|
485
|
+
const nodes = useScene.getState().nodes
|
|
486
|
+
if (ctx.draftItem && isDescendantOfItem(surfaceItem, ctx.draftItem, nodes)) return null
|
|
430
487
|
|
|
431
488
|
// Size check: our footprint must fit on surface item's footprint
|
|
432
489
|
const ourDims = ctx.draftItem
|
|
@@ -440,17 +497,32 @@ export const itemSurfaceStrategy = {
|
|
|
440
497
|
|
|
441
498
|
const worldPos = new Vector3(event.position[0], event.position[1], event.position[2])
|
|
442
499
|
const localPos = surfaceMesh.worldToLocal(worldPos)
|
|
500
|
+
const surfaceHeight = getSurfacePlacementHeight(surfaceItem, event, localPos)
|
|
501
|
+
if (surfaceHeight === null) return null
|
|
443
502
|
|
|
444
503
|
const x = snapToGrid(localPos.x, ourDims[0])
|
|
445
504
|
const z = snapToGrid(localPos.z, ourDims[2])
|
|
446
|
-
const y =
|
|
505
|
+
const y = surfaceHeight
|
|
447
506
|
|
|
448
507
|
const worldSnapped = surfaceMesh.localToWorld(new Vector3(x, y, z))
|
|
449
508
|
|
|
450
509
|
return {
|
|
451
510
|
stateUpdate: { surface: 'item-surface', surfaceItemId: surfaceItem.id },
|
|
452
|
-
nodeUpdate: {
|
|
453
|
-
|
|
511
|
+
nodeUpdate: {
|
|
512
|
+
position: [x, y, z],
|
|
513
|
+
parentId: surfaceItem.id,
|
|
514
|
+
rotation: [
|
|
515
|
+
(ctx.draftItem?.rotation ?? [0, 0, 0])[0],
|
|
516
|
+
(() => {
|
|
517
|
+
const surfaceQuat = new Quaternion()
|
|
518
|
+
surfaceMesh.getWorldQuaternion(surfaceQuat)
|
|
519
|
+
const surfaceWorldY = new Euler().setFromQuaternion(surfaceQuat, 'YXZ').y
|
|
520
|
+
return ctx.currentCursorRotationY - surfaceWorldY
|
|
521
|
+
})(),
|
|
522
|
+
(ctx.draftItem?.rotation ?? [0, 0, 0])[2],
|
|
523
|
+
] as [number, number, number],
|
|
524
|
+
},
|
|
525
|
+
cursorRotationY: ctx.currentCursorRotationY,
|
|
454
526
|
gridPosition: [x, y, z],
|
|
455
527
|
cursorPosition: [worldSnapped.x, worldSnapped.y, worldSnapped.z],
|
|
456
528
|
stopPropagation: true,
|
|
@@ -463,10 +535,11 @@ export const itemSurfaceStrategy = {
|
|
|
463
535
|
move(ctx: PlacementContext, event: ItemEvent): PlacementResult | null {
|
|
464
536
|
if (ctx.state.surface !== 'item-surface') return null
|
|
465
537
|
if (!(ctx.state.surfaceItemId && ctx.draftItem)) return null
|
|
538
|
+
if (event.node.id !== ctx.state.surfaceItemId) return null
|
|
466
539
|
|
|
467
540
|
const nodes = useScene.getState().nodes
|
|
468
541
|
const surfaceItem = nodes[ctx.state.surfaceItemId as AnyNodeId] as ItemNode | undefined
|
|
469
|
-
if (!surfaceItem
|
|
542
|
+
if (!surfaceItem) return null
|
|
470
543
|
|
|
471
544
|
const surfaceMesh = sceneRegistry.nodes.get(ctx.state.surfaceItemId)
|
|
472
545
|
if (!surfaceMesh) return null
|
|
@@ -474,17 +547,19 @@ export const itemSurfaceStrategy = {
|
|
|
474
547
|
const ourDims = getScaledDimensions(ctx.draftItem)
|
|
475
548
|
const worldPos = new Vector3(event.position[0], event.position[1], event.position[2])
|
|
476
549
|
const localPos = surfaceMesh.worldToLocal(worldPos)
|
|
550
|
+
const surfaceHeight = getSurfacePlacementHeight(surfaceItem, event, localPos)
|
|
551
|
+
if (surfaceHeight === null) return null
|
|
477
552
|
|
|
478
553
|
const x = snapToGrid(localPos.x, ourDims[0])
|
|
479
554
|
const z = snapToGrid(localPos.z, ourDims[2])
|
|
480
|
-
const y =
|
|
555
|
+
const y = surfaceHeight
|
|
481
556
|
|
|
482
557
|
const worldSnapped = surfaceMesh.localToWorld(new Vector3(x, y, z))
|
|
483
558
|
|
|
484
559
|
return {
|
|
485
560
|
gridPosition: [x, y, z],
|
|
486
561
|
cursorPosition: [worldSnapped.x, worldSnapped.y, worldSnapped.z],
|
|
487
|
-
cursorRotationY:
|
|
562
|
+
cursorRotationY: ctx.currentCursorRotationY,
|
|
488
563
|
nodeUpdate: { position: [x, y, z] },
|
|
489
564
|
stopPropagation: true,
|
|
490
565
|
dirtyNodeId: null,
|
|
@@ -497,6 +572,7 @@ export const itemSurfaceStrategy = {
|
|
|
497
572
|
click(ctx: PlacementContext, _event: ItemEvent): CommitResult | null {
|
|
498
573
|
if (ctx.state.surface !== 'item-surface') return null
|
|
499
574
|
if (!(ctx.draftItem && ctx.state.surfaceItemId)) return null
|
|
575
|
+
if (_event.node.id !== ctx.state.surfaceItemId) return null
|
|
500
576
|
|
|
501
577
|
return {
|
|
502
578
|
nodeUpdate: {
|
|
@@ -528,12 +604,14 @@ export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidato
|
|
|
528
604
|
|
|
529
605
|
const attachTo = ctx.draftItem.asset.attachTo
|
|
530
606
|
|
|
607
|
+
const alignedDims = getGridAlignedDimensions(getScaledDimensions(ctx.draftItem), attachTo)
|
|
608
|
+
|
|
531
609
|
if (attachTo === 'ceiling') {
|
|
532
610
|
if (ctx.state.surface !== 'ceiling' || !ctx.state.ceilingId) return false
|
|
533
611
|
return validators.canPlaceOnCeiling(
|
|
534
612
|
ctx.state.ceilingId as CeilingNode['id'],
|
|
535
613
|
[ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
536
|
-
|
|
614
|
+
alignedDims,
|
|
537
615
|
ctx.draftItem.rotation,
|
|
538
616
|
[ctx.draftItem.id],
|
|
539
617
|
).valid
|
|
@@ -546,7 +624,7 @@ export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidato
|
|
|
546
624
|
ctx.state.wallId as WallNode['id'],
|
|
547
625
|
ctx.gridPosition.x,
|
|
548
626
|
ctx.gridPosition.y,
|
|
549
|
-
|
|
627
|
+
alignedDims,
|
|
550
628
|
attachTo,
|
|
551
629
|
ctx.draftItem.side,
|
|
552
630
|
[ctx.draftItem.id],
|
|
@@ -557,7 +635,7 @@ export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidato
|
|
|
557
635
|
return validators.canPlaceOnFloor(
|
|
558
636
|
ctx.levelId,
|
|
559
637
|
[ctx.gridPosition.x, 0, ctx.gridPosition.z],
|
|
560
|
-
|
|
638
|
+
alignedDims,
|
|
561
639
|
ctx.draftItem.rotation,
|
|
562
640
|
[ctx.draftItem.id],
|
|
563
641
|
).valid
|
|
@@ -38,6 +38,13 @@ export interface PlacementContext {
|
|
|
38
38
|
draftItem: ItemNode | null
|
|
39
39
|
gridPosition: Vector3
|
|
40
40
|
state: PlacementState
|
|
41
|
+
/**
|
|
42
|
+
* Current world Y rotation of the placement cursor — the user's intended
|
|
43
|
+
* orientation, preserved across surface transitions. Strategies that
|
|
44
|
+
* re-parent the draft (e.g. floor → item-surface) read this to compute the
|
|
45
|
+
* matching parent-local rotation so the world orientation doesn't jump.
|
|
46
|
+
*/
|
|
47
|
+
currentCursorRotationY: number
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
// ============================================================================
|
|
@@ -130,6 +130,7 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
130
130
|
useScene.getState().updateNode(draft.id, {
|
|
131
131
|
position: updateProps.position ?? draft.position,
|
|
132
132
|
rotation: updateProps.rotation ?? draft.rotation,
|
|
133
|
+
scale: updateProps.scale ?? draft.scale,
|
|
133
134
|
side: updateProps.side ?? draft.side,
|
|
134
135
|
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
|
135
136
|
parentId: parentId as string,
|
|
@@ -161,6 +162,7 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
161
162
|
asset: draft.asset,
|
|
162
163
|
position: updateProps.position ?? draft.position,
|
|
163
164
|
rotation: updateProps.rotation ?? draft.rotation,
|
|
165
|
+
scale: updateProps.scale ?? draft.scale,
|
|
164
166
|
side: updateProps.side ?? draft.side,
|
|
165
167
|
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
|
166
168
|
})
|