@pascal-app/editor 0.9.1 → 0.9.2
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 +12 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -6,19 +6,28 @@ import type {
|
|
|
6
6
|
GridEvent,
|
|
7
7
|
ItemEvent,
|
|
8
8
|
ItemNode,
|
|
9
|
+
RoofEvent,
|
|
10
|
+
RoofNode,
|
|
11
|
+
RoofSegmentNode,
|
|
12
|
+
RoofWallFaceId,
|
|
9
13
|
ShelfEvent,
|
|
10
14
|
ShelfNode,
|
|
11
15
|
WallEvent,
|
|
12
16
|
WallNode,
|
|
13
17
|
} from '@pascal-app/core'
|
|
14
18
|
import {
|
|
19
|
+
canHostOnTop,
|
|
20
|
+
clampRectToRoofWallFace,
|
|
21
|
+
getRoofSegmentWallFace,
|
|
15
22
|
getScaledDimensions,
|
|
16
23
|
isLowProfileItemSurface,
|
|
17
24
|
nodeRegistry,
|
|
25
|
+
roofFacePointToSegment,
|
|
18
26
|
sceneRegistry,
|
|
19
27
|
useScene,
|
|
20
28
|
} from '@pascal-app/core'
|
|
21
29
|
import { Euler, Matrix3, Quaternion, Vector3 } from 'three'
|
|
30
|
+
import { hasRoofFaceChildOverlap, resolveRoofWallHit } from '../../../lib/roof-wall-hit'
|
|
22
31
|
import { snapWorldXZForActiveBuilding } from '../../../lib/world-grid-snap'
|
|
23
32
|
import {
|
|
24
33
|
calculateCursorRotation,
|
|
@@ -56,6 +65,7 @@ function isUpwardItemSurfaceHit(event: ItemEvent): boolean {
|
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
function getSurfacePlacementHeight(surfaceItem: ItemNode, event: ItemEvent, localPos: Vector3) {
|
|
68
|
+
if (!canHostOnTop(surfaceItem)) return null
|
|
59
69
|
if (isLowProfileItemSurface(surfaceItem)) return null
|
|
60
70
|
if (!isUpwardItemSurfaceHit(event)) return null
|
|
61
71
|
|
|
@@ -105,10 +115,13 @@ export const floorStrategy = {
|
|
|
105
115
|
// is rotated; then project the world point back into building-local
|
|
106
116
|
// for storage. Without this, a rotated building drags placement off
|
|
107
117
|
// the world grid.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
118
|
+
// Snapping is governed by the active mode (snapToGrid returns raw in Off /
|
|
119
|
+
// non-grid modes); Alt is force-place only and never bypasses snapping here.
|
|
120
|
+
const [x, z] = snapWorldXZForActiveBuilding(
|
|
121
|
+
snapToGrid(event.position[0], swapDims ? dimZ : dimX),
|
|
122
|
+
snapToGrid(event.position[2], swapDims ? dimX : dimZ),
|
|
123
|
+
0,
|
|
124
|
+
).local
|
|
112
125
|
const y = ctx.gridPosition.y
|
|
113
126
|
|
|
114
127
|
return {
|
|
@@ -132,6 +145,7 @@ export const floorStrategy = {
|
|
|
132
145
|
): CommitResult | null {
|
|
133
146
|
if (ctx.state.surface !== 'floor') return null
|
|
134
147
|
if (!(ctx.levelId && ctx.draftItem)) return null
|
|
148
|
+
if (ctx.draftItem.asset.attachTo) return null
|
|
135
149
|
|
|
136
150
|
const pos: [number, number, number] = [
|
|
137
151
|
ctx.gridPosition.x,
|
|
@@ -211,10 +225,13 @@ export const wallStrategy = {
|
|
|
211
225
|
const adjustedY = validation.adjustedY ?? y
|
|
212
226
|
|
|
213
227
|
return {
|
|
214
|
-
stateUpdate: { surface: 'wall', wallId: event.node.id },
|
|
228
|
+
stateUpdate: { surface: 'wall', wallId: event.node.id, roofSegmentId: null },
|
|
215
229
|
nodeUpdate: {
|
|
216
230
|
position: [x, adjustedY, z],
|
|
217
231
|
parentId: event.node.id,
|
|
232
|
+
// The draft may arrive from a roof-segment wall face.
|
|
233
|
+
roofSegmentId: undefined,
|
|
234
|
+
roofFace: undefined,
|
|
218
235
|
side,
|
|
219
236
|
rotation: [0, itemRotation, 0],
|
|
220
237
|
},
|
|
@@ -313,6 +330,8 @@ export const wallStrategy = {
|
|
|
313
330
|
nodeUpdate: {
|
|
314
331
|
position: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
315
332
|
parentId: event.node.id,
|
|
333
|
+
roofSegmentId: undefined,
|
|
334
|
+
roofFace: undefined,
|
|
316
335
|
side: ctx.draftItem.side,
|
|
317
336
|
rotation: ctx.draftItem.rotation,
|
|
318
337
|
metadata: stripTransient(ctx.draftItem.metadata),
|
|
@@ -342,6 +361,225 @@ export const wallStrategy = {
|
|
|
342
361
|
},
|
|
343
362
|
}
|
|
344
363
|
|
|
364
|
+
// ============================================================================
|
|
365
|
+
// ROOF WALL STRATEGY
|
|
366
|
+
// ============================================================================
|
|
367
|
+
|
|
368
|
+
type RoofWallTarget = {
|
|
369
|
+
segment: RoofSegmentNode
|
|
370
|
+
faceId: RoofWallFaceId
|
|
371
|
+
faceYaw: number
|
|
372
|
+
/** Stored node position: FACE-LOCAL, y = bottom edge. */
|
|
373
|
+
position: [number, number, number]
|
|
374
|
+
/** Face-coord center of the placed rect (for the overlap guard). */
|
|
375
|
+
centerU: number
|
|
376
|
+
centerV: number
|
|
377
|
+
width: number
|
|
378
|
+
height: number
|
|
379
|
+
cursorPosition: [number, number, number]
|
|
380
|
+
cursorRotationY: number
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Resolve a roof pointer event to an item placement on a segment wall
|
|
385
|
+
* face. Items snap u / bottom-v to the 0.5m grid, then the rect is
|
|
386
|
+
* clamped inside the face profile (sliding under the gable slopes).
|
|
387
|
+
* Position frame matches wall hosting: y anchors the BOTTOM edge;
|
|
388
|
+
* `wall-side` items mount on the outer surface, `wall` items center in
|
|
389
|
+
* the wall thickness.
|
|
390
|
+
*
|
|
391
|
+
* `freePlace` mirrors the wall flow's Alt override (stubbed
|
|
392
|
+
* validators): the profile clamp is skipped, so the rect may overhang
|
|
393
|
+
* the face edges — placement follows the snapped cursor as-is.
|
|
394
|
+
*/
|
|
395
|
+
function resolveRoofWallTarget(
|
|
396
|
+
ctx: PlacementContext,
|
|
397
|
+
event: RoofEvent,
|
|
398
|
+
freePlace = false,
|
|
399
|
+
): RoofWallTarget | null {
|
|
400
|
+
const attachTo = ctx.asset.attachTo
|
|
401
|
+
if (attachTo !== 'wall' && attachTo !== 'wall-side') return null
|
|
402
|
+
|
|
403
|
+
const hit = resolveRoofWallHit(event.node as RoofNode, event.position, event.normal, event.object)
|
|
404
|
+
if (!hit) return null
|
|
405
|
+
|
|
406
|
+
const rawDims = ctx.draftItem
|
|
407
|
+
? getScaledDimensions(ctx.draftItem)
|
|
408
|
+
: (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
|
409
|
+
const dims = getGridAlignedDimensions(rawDims, attachTo)
|
|
410
|
+
const [width, height] = dims
|
|
411
|
+
|
|
412
|
+
// Snap follows the active mode (snapToHalf returns raw in Off/non-grid);
|
|
413
|
+
// `freePlace` (Alt) is force-place — it only skips the face-fit validity gate.
|
|
414
|
+
const u = snapToHalf(hit.u)
|
|
415
|
+
const centerV = snapToHalf(hit.v) + height / 2
|
|
416
|
+
const fitted = freePlace ? null : clampRectToRoofWallFace(hit.face, u, centerV, width, height)
|
|
417
|
+
if (!fitted && !freePlace) return null
|
|
418
|
+
const finalU = fitted?.u ?? u
|
|
419
|
+
const finalV = fitted?.v ?? centerV
|
|
420
|
+
|
|
421
|
+
// FACE-LOCAL storage (z = 0 → wall mid-plane; ItemSystem pushes
|
|
422
|
+
// wall-side items to the outer surface, exactly like wall hosting).
|
|
423
|
+
// The renderer mounts the node inside the live face frame, so items
|
|
424
|
+
// track segment resizes without any re-anchoring.
|
|
425
|
+
const position: [number, number, number] = [finalU, finalV - height / 2, 0]
|
|
426
|
+
|
|
427
|
+
const segObj = sceneRegistry.nodes.get(hit.segment.id)
|
|
428
|
+
if (!segObj) return null
|
|
429
|
+
segObj.updateWorldMatrix(true, false)
|
|
430
|
+
const segLocal = roofFacePointToSegment(hit.segment, hit.face.id, position)
|
|
431
|
+
const worldPos = segObj.localToWorld(new Vector3(segLocal[0], segLocal[1], segLocal[2]))
|
|
432
|
+
|
|
433
|
+
const nodes = useScene.getState().nodes
|
|
434
|
+
const roof = hit.segment.parentId
|
|
435
|
+
? (nodes[hit.segment.parentId as AnyNodeId] as RoofNode | undefined)
|
|
436
|
+
: undefined
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
segment: hit.segment,
|
|
440
|
+
faceId: hit.face.id,
|
|
441
|
+
faceYaw: hit.face.yaw,
|
|
442
|
+
position,
|
|
443
|
+
centerU: finalU,
|
|
444
|
+
centerV: finalV,
|
|
445
|
+
width,
|
|
446
|
+
height,
|
|
447
|
+
cursorPosition: [worldPos.x, worldPos.y, worldPos.z],
|
|
448
|
+
cursorRotationY: (roof?.rotation ?? 0) + (hit.segment.rotation ?? 0) + hit.face.yaw,
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Validation half of `checkCanPlace` for the roof-wall surface. */
|
|
453
|
+
function canPlaceOnRoofWall(ctx: PlacementContext): boolean {
|
|
454
|
+
const segmentId = ctx.state.roofSegmentId
|
|
455
|
+
if (!(segmentId && ctx.draftItem)) return false
|
|
456
|
+
const segment = useScene.getState().nodes[segmentId as AnyNodeId] as RoofSegmentNode | undefined
|
|
457
|
+
if (segment?.type !== 'roof-segment') return false
|
|
458
|
+
const faceId = ctx.draftItem.roofFace
|
|
459
|
+
if (!faceId) return false
|
|
460
|
+
const face = getRoofSegmentWallFace(segment, faceId)
|
|
461
|
+
|
|
462
|
+
const dims = getGridAlignedDimensions(
|
|
463
|
+
getScaledDimensions(ctx.draftItem),
|
|
464
|
+
ctx.draftItem.asset.attachTo,
|
|
465
|
+
)
|
|
466
|
+
const [width, height] = dims
|
|
467
|
+
// gridPosition carries the stored FACE-LOCAL coords (u, bottom-v, z).
|
|
468
|
+
const u = ctx.gridPosition.x
|
|
469
|
+
const centerV = ctx.gridPosition.y + height / 2
|
|
470
|
+
const clamped = clampRectToRoofWallFace(face, u, centerV, width, height)
|
|
471
|
+
if (!clamped || Math.abs(clamped.u - u) > 1e-3 || Math.abs(clamped.v - centerV) > 1e-3) {
|
|
472
|
+
return false
|
|
473
|
+
}
|
|
474
|
+
return !hasRoofFaceChildOverlap(segment, faceId, u, centerV, width, height, ctx.draftItem.id)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export const roofWallStrategy = {
|
|
478
|
+
/**
|
|
479
|
+
* Handle roof:enter / first hover — transition onto a segment wall
|
|
480
|
+
* face. Returns null when the item doesn't wall-attach or the pointer
|
|
481
|
+
* isn't over a placeable face.
|
|
482
|
+
*/
|
|
483
|
+
enter(ctx: PlacementContext, event: RoofEvent, freePlace = false): TransitionResult | null {
|
|
484
|
+
const target = resolveRoofWallTarget(ctx, event, freePlace)
|
|
485
|
+
if (!target) return null
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
stateUpdate: { surface: 'roof-wall', roofSegmentId: target.segment.id, wallId: null },
|
|
489
|
+
nodeUpdate: {
|
|
490
|
+
position: target.position,
|
|
491
|
+
parentId: target.segment.id,
|
|
492
|
+
roofSegmentId: target.segment.id,
|
|
493
|
+
roofFace: target.faceId,
|
|
494
|
+
wallId: undefined,
|
|
495
|
+
side: 'front',
|
|
496
|
+
rotation: [0, 0, 0],
|
|
497
|
+
},
|
|
498
|
+
cursorRotationY: target.cursorRotationY,
|
|
499
|
+
gridPosition: target.position,
|
|
500
|
+
cursorPosition: target.cursorPosition,
|
|
501
|
+
stopPropagation: true,
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Handle roof:move while on a segment wall face. Returns null when the
|
|
507
|
+
* pointer resolves to a DIFFERENT segment (the coordinator re-enters —
|
|
508
|
+
* segment transitions inside one roof never re-fire roof:enter) or to
|
|
509
|
+
* no placeable face.
|
|
510
|
+
*/
|
|
511
|
+
move(ctx: PlacementContext, event: RoofEvent, freePlace = false): PlacementResult | null {
|
|
512
|
+
if (ctx.state.surface !== 'roof-wall') return null
|
|
513
|
+
if (!ctx.draftItem) return null
|
|
514
|
+
|
|
515
|
+
const target = resolveRoofWallTarget(ctx, event, freePlace)
|
|
516
|
+
if (!target) return null
|
|
517
|
+
if (target.segment.id !== ctx.state.roofSegmentId) return null
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
gridPosition: target.position,
|
|
521
|
+
cursorPosition: target.cursorPosition,
|
|
522
|
+
cursorRotationY: target.cursorRotationY,
|
|
523
|
+
nodeUpdate: {
|
|
524
|
+
position: target.position,
|
|
525
|
+
side: 'front',
|
|
526
|
+
rotation: [0, 0, 0],
|
|
527
|
+
roofFace: target.faceId,
|
|
528
|
+
},
|
|
529
|
+
stopPropagation: true,
|
|
530
|
+
// Items don't cut the roof — no geometry rebuild needed.
|
|
531
|
+
dirtyNodeId: null,
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Handle roof:click — commit placement on the segment wall face.
|
|
537
|
+
*/
|
|
538
|
+
click(ctx: PlacementContext, _event: RoofEvent, freePlace = false): CommitResult | null {
|
|
539
|
+
if (ctx.state.surface !== 'roof-wall') return null
|
|
540
|
+
if (!(ctx.draftItem && ctx.state.roofSegmentId)) return null
|
|
541
|
+
// Alt mirrors the wall flow's stubbed validators: skip profile-fit
|
|
542
|
+
// and overlap checks entirely.
|
|
543
|
+
if (!freePlace && !canPlaceOnRoofWall(ctx)) return null
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
nodeUpdate: {
|
|
547
|
+
position: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
548
|
+
parentId: ctx.state.roofSegmentId,
|
|
549
|
+
roofSegmentId: ctx.state.roofSegmentId,
|
|
550
|
+
roofFace: ctx.draftItem.roofFace,
|
|
551
|
+
wallId: undefined,
|
|
552
|
+
side: 'front',
|
|
553
|
+
rotation: [0, 0, 0],
|
|
554
|
+
metadata: stripTransient(ctx.draftItem.metadata),
|
|
555
|
+
},
|
|
556
|
+
stopPropagation: true,
|
|
557
|
+
dirtyNodeId: null,
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Handle roof:leave — transition back to floor surface.
|
|
563
|
+
*/
|
|
564
|
+
leave(ctx: PlacementContext): TransitionResult | null {
|
|
565
|
+
if (ctx.state.surface !== 'roof-wall') return null
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
stateUpdate: { surface: 'floor', roofSegmentId: null },
|
|
569
|
+
nodeUpdate: {
|
|
570
|
+
position: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
571
|
+
parentId: ctx.levelId,
|
|
572
|
+
roofSegmentId: undefined,
|
|
573
|
+
roofFace: undefined,
|
|
574
|
+
},
|
|
575
|
+
cursorRotationY: 0,
|
|
576
|
+
gridPosition: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
577
|
+
cursorPosition: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
578
|
+
stopPropagation: true,
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
}
|
|
582
|
+
|
|
345
583
|
// ============================================================================
|
|
346
584
|
// CEILING STRATEGY
|
|
347
585
|
// ============================================================================
|
|
@@ -794,6 +1032,9 @@ export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidato
|
|
|
794
1032
|
}
|
|
795
1033
|
|
|
796
1034
|
if (attachTo === 'wall' || attachTo === 'wall-side') {
|
|
1035
|
+
if (ctx.state.surface === 'roof-wall') {
|
|
1036
|
+
return canPlaceOnRoofWall(ctx)
|
|
1037
|
+
}
|
|
797
1038
|
if (ctx.state.surface !== 'wall' || !ctx.state.wallId) return false
|
|
798
1039
|
return validators.canPlaceOnWall(
|
|
799
1040
|
ctx.levelId,
|
|
@@ -12,7 +12,13 @@ import type { Vector3 } from 'three'
|
|
|
12
12
|
// PLACEMENT STATE
|
|
13
13
|
// ============================================================================
|
|
14
14
|
|
|
15
|
-
export type SurfaceType =
|
|
15
|
+
export type SurfaceType =
|
|
16
|
+
| 'floor'
|
|
17
|
+
| 'wall'
|
|
18
|
+
| 'roof-wall'
|
|
19
|
+
| 'ceiling'
|
|
20
|
+
| 'item-surface'
|
|
21
|
+
| 'shelf-surface'
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* Tracks which surface the draft item is currently on.
|
|
@@ -21,6 +27,12 @@ export type SurfaceType = 'floor' | 'wall' | 'ceiling' | 'item-surface' | 'shelf
|
|
|
21
27
|
export interface PlacementState {
|
|
22
28
|
surface: SurfaceType
|
|
23
29
|
wallId: string | null
|
|
30
|
+
/**
|
|
31
|
+
* Active roof-segment when `surface === 'roof-wall'` — wall-attach
|
|
32
|
+
* items also host on the vertical wall faces a roof segment generates
|
|
33
|
+
* (base walls + coplanar gable ends).
|
|
34
|
+
*/
|
|
35
|
+
roofSegmentId: string | null
|
|
24
36
|
ceilingId: string | null
|
|
25
37
|
surfaceItemId: string | null
|
|
26
38
|
/**
|
|
@@ -15,6 +15,10 @@ interface OriginalState {
|
|
|
15
15
|
rotation: [number, number, number]
|
|
16
16
|
side: ItemNode['side']
|
|
17
17
|
parentId: string | null
|
|
18
|
+
// Roof-segment wall hosting — cleared/changed by surface transitions
|
|
19
|
+
// mid-move, so reverts must restore it alongside parentId.
|
|
20
|
+
roofSegmentId: ItemNode['roofSegmentId']
|
|
21
|
+
roofFace: ItemNode['roofFace']
|
|
18
22
|
metadata: ItemNode['metadata']
|
|
19
23
|
}
|
|
20
24
|
|
|
@@ -23,12 +27,14 @@ export interface DraftNodeHandle {
|
|
|
23
27
|
readonly current: ItemNode | null
|
|
24
28
|
/** Whether the current draft was adopted (move mode) vs created (create mode) */
|
|
25
29
|
readonly isAdopted: boolean
|
|
26
|
-
/** Create a new draft item at the given position. Returns the created node or null.
|
|
30
|
+
/** Create a new draft item at the given position. Returns the created node or null.
|
|
31
|
+
* `slots` seeds painted slot overrides so duplicates keep their materials. */
|
|
27
32
|
create: (
|
|
28
33
|
gridPosition: Vector3,
|
|
29
34
|
asset: AssetInput,
|
|
30
35
|
rotation?: [number, number, number],
|
|
31
36
|
scale?: [number, number, number],
|
|
37
|
+
slots?: ItemNode['slots'],
|
|
32
38
|
) => ItemNode | null
|
|
33
39
|
/** Take ownership of an existing scene node as the draft (for move mode). */
|
|
34
40
|
adopt: (node: ItemNode) => void
|
|
@@ -57,6 +63,7 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
57
63
|
asset: AssetInput,
|
|
58
64
|
rotation?: [number, number, number],
|
|
59
65
|
scale?: [number, number, number],
|
|
66
|
+
slots?: ItemNode['slots'],
|
|
60
67
|
): ItemNode | null => {
|
|
61
68
|
const currentLevelId = useViewer.getState().selection.levelId
|
|
62
69
|
if (!currentLevelId) return null
|
|
@@ -69,6 +76,7 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
69
76
|
asset,
|
|
70
77
|
parentId: currentLevelId,
|
|
71
78
|
metadata: { isTransient: true },
|
|
79
|
+
...(slots ? { slots } : {}),
|
|
72
80
|
})
|
|
73
81
|
|
|
74
82
|
useScene.getState().createNode(node, currentLevelId)
|
|
@@ -92,6 +100,8 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
92
100
|
rotation: [...node.rotation] as [number, number, number],
|
|
93
101
|
side: node.side,
|
|
94
102
|
parentId: node.parentId,
|
|
103
|
+
roofSegmentId: node.roofSegmentId,
|
|
104
|
+
roofFace: node.roofFace,
|
|
95
105
|
metadata: node.metadata,
|
|
96
106
|
}
|
|
97
107
|
|
|
@@ -121,6 +131,8 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
121
131
|
rotation: original.rotation,
|
|
122
132
|
side: original.side,
|
|
123
133
|
parentId: original.parentId,
|
|
134
|
+
roofSegmentId: original.roofSegmentId,
|
|
135
|
+
roofFace: original.roofFace,
|
|
124
136
|
metadata: original.metadata,
|
|
125
137
|
})
|
|
126
138
|
|
|
@@ -133,6 +145,15 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
133
145
|
side: updateProps.side ?? draft.side,
|
|
134
146
|
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
|
135
147
|
parentId: parentId as string,
|
|
148
|
+
// Forward the roof host explicitly: strategies set it on every
|
|
149
|
+
// commit (segment id on a roof face, undefined elsewhere), and
|
|
150
|
+
// dropping it here strands the item in the roof frame without
|
|
151
|
+
// the segment transform.
|
|
152
|
+
roofSegmentId: updateProps.roofSegmentId,
|
|
153
|
+
roofFace: updateProps.roofFace,
|
|
154
|
+
// Only when the strategy decided about wallId (roof commits clear
|
|
155
|
+
// it) — floor/ceiling commits never managed the field.
|
|
156
|
+
...('wallId' in updateProps ? { wallId: updateProps.wallId } : {}),
|
|
136
157
|
})
|
|
137
158
|
|
|
138
159
|
useScene.temporal.getState().pause()
|
|
@@ -163,6 +184,13 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
163
184
|
rotation: updateProps.rotation ?? draft.rotation,
|
|
164
185
|
scale: updateProps.scale ?? draft.scale,
|
|
165
186
|
side: updateProps.side ?? draft.side,
|
|
187
|
+
// Carry painted slot overrides so a duplicated item keeps its materials.
|
|
188
|
+
...(draft.slots ? { slots: draft.slots } : {}),
|
|
189
|
+
// Roof host — see the move-mode commit above for why this must be
|
|
190
|
+
// forwarded explicitly.
|
|
191
|
+
roofSegmentId: updateProps.roofSegmentId,
|
|
192
|
+
roofFace: updateProps.roofFace,
|
|
193
|
+
...('wallId' in updateProps ? { wallId: updateProps.wallId } : {}),
|
|
166
194
|
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
|
167
195
|
})
|
|
168
196
|
useScene.getState().createNode(finalNode, parentId)
|
|
@@ -207,6 +235,8 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
207
235
|
rotation: original.rotation,
|
|
208
236
|
side: original.side,
|
|
209
237
|
parentId: original.parentId,
|
|
238
|
+
roofSegmentId: original.roofSegmentId,
|
|
239
|
+
roofFace: original.roofFace,
|
|
210
240
|
metadata: original.metadata,
|
|
211
241
|
})
|
|
212
242
|
|