@pascal-app/editor 0.7.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -8,11 +8,12 @@ import {
|
|
|
8
8
|
} from '@pascal-app/core'
|
|
9
9
|
import { useViewer } from '@pascal-app/viewer'
|
|
10
10
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
11
|
+
import useEditor from '../../../store/use-editor'
|
|
11
12
|
import {
|
|
12
13
|
findWallSnapTarget,
|
|
14
|
+
getSegmentGridStep,
|
|
13
15
|
getWallAngleSnapStep,
|
|
14
|
-
|
|
15
|
-
isWallLongEnough,
|
|
16
|
+
isSegmentLongEnough,
|
|
16
17
|
snapPointTo45Degrees,
|
|
17
18
|
snapPointToGrid,
|
|
18
19
|
type WallPlanPoint,
|
|
@@ -20,6 +21,9 @@ import {
|
|
|
20
21
|
|
|
21
22
|
export type FencePlanPoint = WallPlanPoint
|
|
22
23
|
|
|
24
|
+
const FENCE_CORNER_SNAP_RADIUS = 0.28
|
|
25
|
+
const FENCE_SPAN_SNAP_RADIUS = 0.16
|
|
26
|
+
|
|
23
27
|
type SegmentNode = {
|
|
24
28
|
start: FencePlanPoint
|
|
25
29
|
end: FencePlanPoint
|
|
@@ -57,46 +61,68 @@ function findFenceSnapTarget(
|
|
|
57
61
|
fences: FenceNode[],
|
|
58
62
|
ignoreFenceIds: string[] = [],
|
|
59
63
|
): FencePlanPoint | null {
|
|
60
|
-
const
|
|
64
|
+
const cornerRadiusSquared = FENCE_CORNER_SNAP_RADIUS ** 2
|
|
65
|
+
const spanRadiusSquared = FENCE_SPAN_SNAP_RADIUS ** 2
|
|
61
66
|
const ignoredFenceIds = new Set(ignoreFenceIds)
|
|
62
|
-
let
|
|
63
|
-
let
|
|
67
|
+
let bestCornerTarget: FencePlanPoint | null = null
|
|
68
|
+
let bestCornerDistanceSquared = Number.POSITIVE_INFINITY
|
|
69
|
+
let bestSpanTarget: FencePlanPoint | null = null
|
|
70
|
+
let bestSpanDistanceSquared = Number.POSITIVE_INFINITY
|
|
64
71
|
|
|
65
72
|
for (const fence of fences) {
|
|
66
73
|
if (ignoredFenceIds.has(fence.id)) {
|
|
67
74
|
continue
|
|
68
75
|
}
|
|
69
76
|
|
|
70
|
-
const
|
|
77
|
+
for (const candidate of [fence.start, fence.end]) {
|
|
78
|
+
const candidateDistanceSquared = distanceSquared(point, candidate)
|
|
79
|
+
if (
|
|
80
|
+
candidateDistanceSquared > cornerRadiusSquared ||
|
|
81
|
+
candidateDistanceSquared >= bestCornerDistanceSquared
|
|
82
|
+
) {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
bestCornerTarget = candidate
|
|
87
|
+
bestCornerDistanceSquared = candidateDistanceSquared
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
if (isCurvedWall(fence)) {
|
|
72
91
|
const sampleCount = Math.max(8, Math.ceil(getWallCurveLength(fence) / 0.3))
|
|
73
|
-
for (let index =
|
|
92
|
+
for (let index = 1; index < sampleCount; index += 1) {
|
|
74
93
|
const frame = getWallCurveFrameAt(fence, index / sampleCount)
|
|
75
|
-
|
|
94
|
+
const candidate: FencePlanPoint = [frame.point.x, frame.point.y]
|
|
95
|
+
const candidateDistanceSquared = distanceSquared(point, candidate)
|
|
96
|
+
if (
|
|
97
|
+
candidateDistanceSquared > spanRadiusSquared ||
|
|
98
|
+
candidateDistanceSquared >= bestSpanDistanceSquared
|
|
99
|
+
) {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
bestSpanTarget = candidate
|
|
104
|
+
bestSpanDistanceSquared = candidateDistanceSquared
|
|
76
105
|
}
|
|
77
106
|
} else {
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
for (const candidate of candidates) {
|
|
107
|
+
const candidate = projectPointOntoSegment(point, fence)
|
|
82
108
|
if (!candidate) {
|
|
83
109
|
continue
|
|
84
110
|
}
|
|
85
111
|
|
|
86
112
|
const candidateDistanceSquared = distanceSquared(point, candidate)
|
|
87
113
|
if (
|
|
88
|
-
candidateDistanceSquared >
|
|
89
|
-
candidateDistanceSquared >=
|
|
114
|
+
candidateDistanceSquared > spanRadiusSquared ||
|
|
115
|
+
candidateDistanceSquared >= bestSpanDistanceSquared
|
|
90
116
|
) {
|
|
91
117
|
continue
|
|
92
118
|
}
|
|
93
119
|
|
|
94
|
-
|
|
95
|
-
|
|
120
|
+
bestSpanTarget = candidate
|
|
121
|
+
bestSpanDistanceSquared = candidateDistanceSquared
|
|
96
122
|
}
|
|
97
123
|
}
|
|
98
124
|
|
|
99
|
-
return
|
|
125
|
+
return bestCornerTarget ?? bestSpanTarget
|
|
100
126
|
}
|
|
101
127
|
|
|
102
128
|
export function snapFenceDraftPoint(args: {
|
|
@@ -106,14 +132,25 @@ export function snapFenceDraftPoint(args: {
|
|
|
106
132
|
start?: FencePlanPoint
|
|
107
133
|
angleSnap?: boolean
|
|
108
134
|
ignoreFenceIds?: string[]
|
|
135
|
+
/** Override the grid step (e.g. `WALL_FINE_GRID_STEP` for precision mode). */
|
|
136
|
+
step?: number
|
|
137
|
+
/**
|
|
138
|
+
* Optional grid-snap function. When provided, replaces the default
|
|
139
|
+
* local-axis snap — lets the 2D floor-plan keep snapping to the
|
|
140
|
+
* world XZ grid even when the building is rotated. Wall / fence
|
|
141
|
+
* endpoint snap precedence is preserved.
|
|
142
|
+
*/
|
|
143
|
+
gridSnap?: (point: FencePlanPoint) => FencePlanPoint
|
|
109
144
|
}): FencePlanPoint {
|
|
110
|
-
const { point, walls, fences, start, angleSnap = false, ignoreFenceIds } = args
|
|
111
|
-
const gridStep =
|
|
145
|
+
const { point, walls, fences, start, angleSnap = false, ignoreFenceIds, step, gridSnap } = args
|
|
146
|
+
const gridStep = step ?? getSegmentGridStep()
|
|
112
147
|
const angleStep = getWallAngleSnapStep(gridStep)
|
|
113
148
|
const basePoint =
|
|
114
149
|
start && angleSnap
|
|
115
|
-
? snapPointTo45Degrees(start, point, gridStep, angleStep)
|
|
116
|
-
:
|
|
150
|
+
? snapPointTo45Degrees(start, point, gridStep, angleStep, gridSnap)
|
|
151
|
+
: gridSnap
|
|
152
|
+
? gridSnap(point)
|
|
153
|
+
: snapPointToGrid(point, gridStep)
|
|
117
154
|
const fenceSnapTarget = findFenceSnapTarget(basePoint, fences, ignoreFenceIds)
|
|
118
155
|
|
|
119
156
|
return fenceSnapTarget ?? findWallSnapTarget(basePoint, walls) ?? basePoint
|
|
@@ -126,12 +163,17 @@ export function createFenceOnCurrentLevel(
|
|
|
126
163
|
const currentLevelId = useViewer.getState().selection.levelId
|
|
127
164
|
const { createNode, nodes } = useScene.getState()
|
|
128
165
|
|
|
129
|
-
if (!(currentLevelId &&
|
|
166
|
+
if (!(currentLevelId && isSegmentLongEnough(start, end))) {
|
|
130
167
|
return null
|
|
131
168
|
}
|
|
132
169
|
|
|
133
170
|
const fenceCount = Object.values(nodes).filter((node) => node.type === 'fence').length
|
|
171
|
+
// Build parameters seeded by a placed preset (height, style, post
|
|
172
|
+
// spacing, …) merge in first; `name`/`start`/`end` always win. The
|
|
173
|
+
// schema parse validates and drops anything unexpected.
|
|
174
|
+
const defaults = useEditor.getState().toolDefaults.fence ?? {}
|
|
134
175
|
const fence = FenceNode.parse({
|
|
176
|
+
...defaults,
|
|
135
177
|
name: `Fence ${fenceCount + 1}`,
|
|
136
178
|
start,
|
|
137
179
|
end,
|
|
@@ -1,115 +1,48 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
ColumnNode,
|
|
5
|
-
DoorNode,
|
|
6
|
-
FenceNode,
|
|
7
|
-
ItemNode,
|
|
8
|
-
RoofNode,
|
|
9
|
-
RoofSegmentNode,
|
|
10
|
-
SlabNode,
|
|
11
|
-
SpawnNode,
|
|
12
|
-
StairNode,
|
|
13
|
-
StairSegmentNode,
|
|
14
|
-
WallNode,
|
|
15
|
-
WindowNode,
|
|
16
|
-
} from '@pascal-app/core'
|
|
17
|
-
import { Vector3 } from 'three'
|
|
18
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
1
|
+
import type { AnyNodeId, ElevatorNode, SpawnNode } from '@pascal-app/core'
|
|
2
|
+
import { nodeRegistry } from '@pascal-app/core'
|
|
3
|
+
import { Suspense } from 'react'
|
|
19
4
|
import useEditor from '../../../store/use-editor'
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { MoveDoorTool } from '../door/move-door-tool'
|
|
24
|
-
import { MoveFenceTool } from '../fence/move-fence-tool'
|
|
25
|
-
import { MoveRoofTool } from '../roof/move-roof-tool'
|
|
26
|
-
import { MoveSlabTool } from '../slab/move-slab-tool'
|
|
27
|
-
import { MoveSpawnTool } from '../spawn/move-spawn-tool'
|
|
28
|
-
import { MoveWallTool } from '../wall/move-wall-tool'
|
|
29
|
-
import { MoveWindowTool } from '../window/move-window-tool'
|
|
30
|
-
import type { PlacementState } from './placement-types'
|
|
31
|
-
import { useDraftNode } from './use-draft-node'
|
|
32
|
-
import { usePlacementCoordinator } from './use-placement-coordinator'
|
|
33
|
-
|
|
34
|
-
function getInitialState(node: {
|
|
35
|
-
asset: { attachTo?: string }
|
|
36
|
-
parentId: string | null
|
|
37
|
-
}): PlacementState {
|
|
38
|
-
const attachTo = node.asset.attachTo
|
|
39
|
-
if (attachTo === 'wall' || attachTo === 'wall-side') {
|
|
40
|
-
return { surface: 'wall', wallId: node.parentId, ceilingId: null, surfaceItemId: null }
|
|
41
|
-
}
|
|
42
|
-
if (attachTo === 'ceiling') {
|
|
43
|
-
return { surface: 'ceiling', wallId: null, ceilingId: node.parentId, surfaceItemId: null }
|
|
44
|
-
}
|
|
45
|
-
return { surface: 'floor', wallId: null, ceilingId: null, surfaceItemId: null }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function MoveItemContent({ movingNode }: { movingNode: ItemNode }) {
|
|
49
|
-
const draftNode = useDraftNode()
|
|
50
|
-
|
|
51
|
-
const meta =
|
|
52
|
-
typeof movingNode.metadata === 'object' && movingNode.metadata !== null
|
|
53
|
-
? (movingNode.metadata as Record<string, unknown>)
|
|
54
|
-
: {}
|
|
55
|
-
const isNew = !!meta.isNew
|
|
56
|
-
|
|
57
|
-
const cursor = usePlacementCoordinator({
|
|
58
|
-
asset: movingNode.asset,
|
|
59
|
-
draftNode,
|
|
60
|
-
// Duplicates start fresh in floor mode; wall/ceiling draft is created lazily by ensureDraft
|
|
61
|
-
initialState: isNew
|
|
62
|
-
? { surface: 'floor', wallId: null, ceilingId: null, surfaceItemId: null }
|
|
63
|
-
: getInitialState(movingNode),
|
|
64
|
-
// Preserve the original item's scale so Y-position calculations use the correct height
|
|
65
|
-
defaultScale: isNew ? movingNode.scale : undefined,
|
|
66
|
-
initDraft: (gridPosition) => {
|
|
67
|
-
if (isNew) {
|
|
68
|
-
// Duplicate: use the same create() path as ItemTool so ghost rendering works correctly.
|
|
69
|
-
// Floor items get a draft immediately; wall/ceiling items are created lazily on surface entry.
|
|
70
|
-
gridPosition.copy(new Vector3(...movingNode.position))
|
|
71
|
-
if (!movingNode.asset.attachTo) {
|
|
72
|
-
draftNode.create(gridPosition, movingNode.asset, movingNode.rotation, movingNode.scale)
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
draftNode.adopt(movingNode)
|
|
76
|
-
gridPosition.copy(new Vector3(...movingNode.position))
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
onCommitted: () => {
|
|
80
|
-
sfxEmitter.emit('sfx:item-place')
|
|
81
|
-
useEditor.getState().setMovingNode(null)
|
|
82
|
-
return false
|
|
83
|
-
},
|
|
84
|
-
onCancel: () => {
|
|
85
|
-
draftNode.destroy()
|
|
86
|
-
useEditor.getState().setMovingNode(null)
|
|
87
|
-
},
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
return <>{cursor}</>
|
|
91
|
-
}
|
|
5
|
+
import { MoveElevatorTool } from '../elevator/move-elevator-tool'
|
|
6
|
+
import { MoveRegistryNodeTool } from '../registry/move-registry-node-tool'
|
|
7
|
+
import { getRegistryAffordanceTool } from '../shared/affordance-dispatch'
|
|
92
8
|
|
|
9
|
+
/**
|
|
10
|
+
* MoveTool dispatcher. Routes to (in order):
|
|
11
|
+
*
|
|
12
|
+
* 1. `MoveRegistryNodeTool` — generic translate-on-XZ for kinds that
|
|
13
|
+
* declare `capabilities.movable` (shelf, spawn, item-with-floor-attach,
|
|
14
|
+
* …).
|
|
15
|
+
* 2. `def.affordanceTools.move` — kind-owned move component, lazy-loaded
|
|
16
|
+
* via `getRegistryAffordanceTool`. Covers both generic movers
|
|
17
|
+
* (slab / ceiling / wall / fence / column / item / door / window) and
|
|
18
|
+
* the bespoke roof / roof-segment / stair / stair-segment / building
|
|
19
|
+
* movers ported into `@pascal-app/nodes`.
|
|
20
|
+
* 3. `elevator` is the lone remaining legacy arm — its bespoke cab/shaft
|
|
21
|
+
* mover hasn't been ported to a kind-owned affordance yet.
|
|
22
|
+
*/
|
|
93
23
|
export const MoveTool: React.FC<{
|
|
24
|
+
onNodeMoved?: (nodeId: AnyNodeId) => void
|
|
94
25
|
onSpawnMoved?: (nodeId: SpawnNode['id']) => void
|
|
95
|
-
}> = ({
|
|
26
|
+
}> = ({ onNodeMoved }) => {
|
|
96
27
|
const movingNode = useEditor((state) => state.movingNode)
|
|
97
28
|
|
|
98
29
|
if (!movingNode) return null
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
30
|
+
|
|
31
|
+
const def = nodeRegistry.get(movingNode.type)
|
|
32
|
+
if (def?.capabilities?.movable) {
|
|
33
|
+
return <MoveRegistryNodeTool node={movingNode} />
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const RegistryMove = getRegistryAffordanceTool(movingNode.type, 'move')
|
|
37
|
+
if (RegistryMove) {
|
|
38
|
+
return (
|
|
39
|
+
<Suspense fallback={null}>
|
|
40
|
+
<RegistryMove node={movingNode} />
|
|
41
|
+
</Suspense>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (movingNode.type === 'elevator')
|
|
46
|
+
return <MoveElevatorTool node={movingNode as ElevatorNode} onCommitted={onNodeMoved} />
|
|
47
|
+
return null
|
|
115
48
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { stripTransient } from './placement-math'
|
|
3
|
+
|
|
4
|
+
describe('stripTransient', () => {
|
|
5
|
+
test('removes placement-only metadata flags before commit', () => {
|
|
6
|
+
expect(stripTransient({ isNew: true, isTransient: true, label: 'copy' })).toEqual({
|
|
7
|
+
label: 'copy',
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AssetInput, isObject } from '@pascal-app/core'
|
|
2
|
+
import { Euler, Matrix3, type Matrix4, Quaternion, Vector3 } from 'three'
|
|
2
3
|
import useEditor from '../../../store/use-editor'
|
|
3
4
|
|
|
4
5
|
function getGridSnapStep(): number {
|
|
@@ -10,9 +11,7 @@ function positiveModulo(value: number, divisor: number): number {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* Snaps a position to
|
|
14
|
-
* For items with dimensions like 2.5, the center would be at 1.25 from the edge,
|
|
15
|
-
* which doesn't align with 0.5 grid. This adds an offset so edges align instead.
|
|
14
|
+
* Snaps a position to the active grid step, aligning item edges to grid lines.
|
|
16
15
|
*/
|
|
17
16
|
export function snapToGrid(position: number, dimension: number, step = getGridSnapStep()): number {
|
|
18
17
|
const halfDim = dimension / 2
|
|
@@ -21,7 +20,7 @@ export function snapToGrid(position: number, dimension: number, step = getGridSn
|
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
/**
|
|
24
|
-
* Snap a value to
|
|
23
|
+
* Snap a value to the active grid step (used for wall-local positions).
|
|
25
24
|
*/
|
|
26
25
|
export function snapToHalf(value: number, step = getGridSnapStep()): number {
|
|
27
26
|
return Math.round(value / step) * step
|
|
@@ -112,11 +111,36 @@ export function isValidWallSideFace(normal: [number, number, number] | undefined
|
|
|
112
111
|
return Math.abs(normal[2]) > 0.7
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
/**
|
|
116
|
-
* Strip the `isTransient` flag from node metadata before committing.
|
|
117
|
-
*/
|
|
114
|
+
/** Strip placement-only metadata flags before committing a draft. */
|
|
118
115
|
export function stripTransient(meta: any): any {
|
|
119
116
|
if (!isObject(meta)) return meta
|
|
120
|
-
const {
|
|
121
|
-
|
|
117
|
+
const nextMeta = { ...(meta as Record<string, any>) }
|
|
118
|
+
delete nextMeta.isNew
|
|
119
|
+
delete nextMeta.isTransient
|
|
120
|
+
return nextMeta
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const _up = new Vector3(0, 1, 0)
|
|
124
|
+
const _normal = new Vector3()
|
|
125
|
+
const _quat = new Quaternion()
|
|
126
|
+
const _euler = new Euler()
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Compute euler rotation that tilts an item so its local +Y aligns with a
|
|
130
|
+
* roof surface normal. The normal is in the hit mesh's local space and is
|
|
131
|
+
* transformed to world space via the mesh's matrixWorld.
|
|
132
|
+
*/
|
|
133
|
+
export function calculateRoofRotation(
|
|
134
|
+
normal: [number, number, number] | undefined,
|
|
135
|
+
objectMatrixWorld: Matrix4,
|
|
136
|
+
): [number, number, number] {
|
|
137
|
+
if (!normal) return [0, 0, 0]
|
|
138
|
+
|
|
139
|
+
_normal.set(normal[0], normal[1], normal[2])
|
|
140
|
+
_normal.applyNormalMatrix(new Matrix3().getNormalMatrix(objectMatrixWorld)).normalize()
|
|
141
|
+
|
|
142
|
+
_quat.setFromUnitVectors(_up, _normal)
|
|
143
|
+
_euler.setFromQuaternion(_quat, 'XYZ')
|
|
144
|
+
|
|
145
|
+
return [_euler.x, _euler.y, _euler.z]
|
|
122
146
|
}
|