@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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CeilingNode,
|
|
3
|
+
ColumnNode,
|
|
3
4
|
DoorNode,
|
|
5
|
+
ElevatorNode,
|
|
4
6
|
ItemNode,
|
|
5
7
|
Point2D,
|
|
6
8
|
RoofNode,
|
|
@@ -53,6 +55,16 @@ type CeilingEntry = {
|
|
|
53
55
|
holes: Point2D[][]
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
type ColumnEntry = {
|
|
59
|
+
column: ColumnNode
|
|
60
|
+
polygon: Point2D[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type ElevatorEntry = {
|
|
64
|
+
elevator: ElevatorNode
|
|
65
|
+
polygon: Point2D[]
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
type RoofEntry = {
|
|
57
69
|
roof: RoofNode
|
|
58
70
|
segments: Array<{
|
|
@@ -71,6 +83,8 @@ type FloorplanSelectionToolContext = {
|
|
|
71
83
|
walls: WallEntry[]
|
|
72
84
|
slabs: SlabEntry[]
|
|
73
85
|
ceilings: CeilingEntry[]
|
|
86
|
+
columns: ColumnEntry[]
|
|
87
|
+
elevators: ElevatorEntry[]
|
|
74
88
|
roofs: RoofEntry[]
|
|
75
89
|
openingHitTolerance: number
|
|
76
90
|
wallHitTolerance: number
|
|
@@ -123,6 +137,20 @@ export function getFloorplanHitNodeId(context: FloorplanSelectionToolContext) {
|
|
|
123
137
|
return stairHit.stair.id
|
|
124
138
|
}
|
|
125
139
|
|
|
140
|
+
const elevatorHit = context.elevators.find(({ polygon }) =>
|
|
141
|
+
isPointInsidePolygon(context.point, polygon),
|
|
142
|
+
)
|
|
143
|
+
if (elevatorHit) {
|
|
144
|
+
return elevatorHit.elevator.id
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const columnHit = context.columns.find(({ polygon }) =>
|
|
148
|
+
isPointInsidePolygon(context.point, polygon),
|
|
149
|
+
)
|
|
150
|
+
if (columnHit) {
|
|
151
|
+
return columnHit.column.id
|
|
152
|
+
}
|
|
153
|
+
|
|
126
154
|
const wallHit = context.walls.find(
|
|
127
155
|
({ wall, polygon }) =>
|
|
128
156
|
isPointInsidePolygon(context.point, polygon) ||
|
|
@@ -166,6 +194,8 @@ type FloorplanSelectionBoundsContext = {
|
|
|
166
194
|
openings: OpeningPolygonEntry[]
|
|
167
195
|
slabs: SlabEntry[]
|
|
168
196
|
ceilings: CeilingEntry[]
|
|
197
|
+
columns: ColumnEntry[]
|
|
198
|
+
elevators: ElevatorEntry[]
|
|
169
199
|
stairs: StairEntry[]
|
|
170
200
|
roofs: RoofEntry[]
|
|
171
201
|
}
|
|
@@ -179,6 +209,8 @@ export function getFloorplanSelectionIdsInBounds({
|
|
|
179
209
|
openings,
|
|
180
210
|
slabs,
|
|
181
211
|
ceilings,
|
|
212
|
+
columns,
|
|
213
|
+
elevators,
|
|
182
214
|
stairs,
|
|
183
215
|
roofs,
|
|
184
216
|
}: FloorplanSelectionBoundsContext) {
|
|
@@ -204,6 +236,12 @@ export function getFloorplanSelectionIdsInBounds({
|
|
|
204
236
|
const ceilingIds = ceilings
|
|
205
237
|
.filter(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds))
|
|
206
238
|
.map(({ ceiling }) => ceiling.id)
|
|
239
|
+
const columnIds = columns
|
|
240
|
+
.filter(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds))
|
|
241
|
+
.map(({ column }) => column.id)
|
|
242
|
+
const elevatorIds = elevators
|
|
243
|
+
.filter(({ polygon }) => doesPolygonIntersectSelectionBounds(polygon, bounds))
|
|
244
|
+
.map(({ elevator }) => elevator.id)
|
|
207
245
|
const stairIds = stairs
|
|
208
246
|
.filter((stair) =>
|
|
209
247
|
getStairHitPolygons(stair).some((polygon) =>
|
|
@@ -224,6 +262,8 @@ export function getFloorplanSelectionIdsInBounds({
|
|
|
224
262
|
...openingIds,
|
|
225
263
|
...slabIds,
|
|
226
264
|
...ceilingIds,
|
|
265
|
+
...columnIds,
|
|
266
|
+
...elevatorIds,
|
|
227
267
|
...stairIds,
|
|
228
268
|
...roofIds,
|
|
229
269
|
]),
|
|
@@ -209,6 +209,14 @@ function getNormalizedFloorplanStairSweepAngle(stair: StairNode) {
|
|
|
209
209
|
return baseSweepAngle
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
function clampFloorplanCircularSweepAngle(sweepAngle: number) {
|
|
213
|
+
if (Math.abs(sweepAngle) >= Math.PI * 2) {
|
|
214
|
+
return Math.sign(sweepAngle || 1) * (Math.PI * 2 - 0.001)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return sweepAngle
|
|
218
|
+
}
|
|
219
|
+
|
|
212
220
|
function getFloorplanSpiralLandingSweep(stair: StairNode, sweepAngle: number) {
|
|
213
221
|
if (
|
|
214
222
|
(stair.stairType ?? 'straight') !== 'spiral' ||
|
|
@@ -230,8 +238,11 @@ function getFloorplanSpiralLandingSweep(stair: StairNode, sweepAngle: number) {
|
|
|
230
238
|
function getFloorplanCurvedStairHitPolygon(stair: StairNode): Point2D[] {
|
|
231
239
|
const stairType = stair.stairType ?? 'straight'
|
|
232
240
|
const sweepAngle = getNormalizedFloorplanStairSweepAngle(stair)
|
|
241
|
+
const visualSweepAngle = clampFloorplanCircularSweepAngle(
|
|
242
|
+
sweepAngle + getFloorplanSpiralLandingSweep(stair, sweepAngle),
|
|
243
|
+
)
|
|
233
244
|
const startAngle = -stair.rotation - sweepAngle / 2
|
|
234
|
-
const endAngle = startAngle +
|
|
245
|
+
const endAngle = startAngle + visualSweepAngle
|
|
235
246
|
const center = {
|
|
236
247
|
x: stair.position[0],
|
|
237
248
|
y: stair.position[2],
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { AnyNode, AnyNodeId } from '@pascal-app/core'
|
|
3
|
+
import { useScene } from '@pascal-app/core'
|
|
4
|
+
import { commitFreshPlacementSubtree } from './fresh-planar-placement'
|
|
5
|
+
|
|
6
|
+
type RafFn = (cb: (time: number) => void) => number
|
|
7
|
+
;(globalThis as { requestAnimationFrame?: RafFn }).requestAnimationFrame ??= ((
|
|
8
|
+
cb: (time: number) => void,
|
|
9
|
+
) => {
|
|
10
|
+
cb(0)
|
|
11
|
+
return 0
|
|
12
|
+
}) as RafFn
|
|
13
|
+
;(globalThis as { cancelAnimationFrame?: (id: number) => void }).cancelAnimationFrame ??= () => {}
|
|
14
|
+
|
|
15
|
+
const LEVEL_ID = 'level_test' as AnyNodeId
|
|
16
|
+
const SHELF_ID = 'shelf_draft' as AnyNodeId
|
|
17
|
+
|
|
18
|
+
function level(children: AnyNodeId[]): AnyNode {
|
|
19
|
+
return {
|
|
20
|
+
id: LEVEL_ID,
|
|
21
|
+
type: 'level',
|
|
22
|
+
object: 'node',
|
|
23
|
+
parentId: null,
|
|
24
|
+
visible: true,
|
|
25
|
+
metadata: {},
|
|
26
|
+
children,
|
|
27
|
+
level: 0,
|
|
28
|
+
} as AnyNode
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shelf(): AnyNode {
|
|
32
|
+
return {
|
|
33
|
+
id: SHELF_ID,
|
|
34
|
+
type: 'shelf',
|
|
35
|
+
object: 'node',
|
|
36
|
+
parentId: LEVEL_ID,
|
|
37
|
+
visible: false,
|
|
38
|
+
metadata: { isNew: true, label: 'draft' },
|
|
39
|
+
children: [],
|
|
40
|
+
position: [0, 0, 0],
|
|
41
|
+
rotation: [0, 0, 0],
|
|
42
|
+
width: 1.2,
|
|
43
|
+
depth: 0.3,
|
|
44
|
+
thickness: 0.04,
|
|
45
|
+
height: 0.9,
|
|
46
|
+
style: 'wall-shelf',
|
|
47
|
+
rows: 1,
|
|
48
|
+
columns: 1,
|
|
49
|
+
withBack: false,
|
|
50
|
+
withSides: true,
|
|
51
|
+
withBottom: false,
|
|
52
|
+
bracketStyle: 'minimal',
|
|
53
|
+
} as AnyNode
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('commitFreshPlacementSubtree', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
useScene.setState({
|
|
59
|
+
nodes: {
|
|
60
|
+
[LEVEL_ID]: level([SHELF_ID]),
|
|
61
|
+
[SHELF_ID]: shelf(),
|
|
62
|
+
},
|
|
63
|
+
rootNodeIds: [LEVEL_ID],
|
|
64
|
+
collections: {},
|
|
65
|
+
dirtyNodes: new Set(),
|
|
66
|
+
} as never)
|
|
67
|
+
useScene.temporal.getState().clear()
|
|
68
|
+
useScene.temporal.getState().resume()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('commits a fresh draft as one undoable clean subtree', () => {
|
|
72
|
+
useScene.temporal.getState().pause()
|
|
73
|
+
|
|
74
|
+
const committedId = commitFreshPlacementSubtree(SHELF_ID, {
|
|
75
|
+
position: [2, 0, 3],
|
|
76
|
+
visible: true,
|
|
77
|
+
} as Partial<AnyNode>)
|
|
78
|
+
|
|
79
|
+
expect(committedId).toBeTruthy()
|
|
80
|
+
expect(committedId).not.toBe(SHELF_ID)
|
|
81
|
+
const finalId = committedId as AnyNodeId
|
|
82
|
+
expect(useScene.getState().nodes[SHELF_ID]).toBeUndefined()
|
|
83
|
+
|
|
84
|
+
const committed = useScene.getState().nodes[finalId] as
|
|
85
|
+
| (AnyNode & { position: [number, number, number]; metadata?: Record<string, unknown> })
|
|
86
|
+
| undefined
|
|
87
|
+
expect(committed?.position).toEqual([2, 0, 3])
|
|
88
|
+
expect(committed?.visible).toBe(true)
|
|
89
|
+
expect(committed?.metadata?.isNew).toBeUndefined()
|
|
90
|
+
expect(committed?.metadata?.label).toBe('draft')
|
|
91
|
+
expect((useScene.getState().nodes[LEVEL_ID] as { children: AnyNodeId[] }).children).toEqual([
|
|
92
|
+
finalId,
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
useScene.temporal.getState().resume()
|
|
96
|
+
useScene.temporal.getState().undo()
|
|
97
|
+
|
|
98
|
+
expect(useScene.getState().nodes[finalId]).toBeUndefined()
|
|
99
|
+
expect(useScene.getState().nodes[SHELF_ID]).toBeUndefined()
|
|
100
|
+
expect((useScene.getState().nodes[LEVEL_ID] as { children: AnyNodeId[] }).children).toEqual([])
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
cloneNodesInto,
|
|
5
|
+
collectSubtree,
|
|
6
|
+
useScene,
|
|
7
|
+
} from '@pascal-app/core'
|
|
8
|
+
import { stripPlacementMetadataFlags } from './placement-metadata'
|
|
9
|
+
|
|
10
|
+
function cleanPlacementMetadata<N extends AnyNode>(node: N): N {
|
|
11
|
+
return {
|
|
12
|
+
...node,
|
|
13
|
+
metadata: stripPlacementMetadataFlags(node.metadata),
|
|
14
|
+
} as N
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parentIdOf(node: AnyNode): AnyNodeId | undefined {
|
|
18
|
+
const parentId = (node as { parentId?: AnyNodeId | null }).parentId
|
|
19
|
+
return parentId ?? undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Finalises a fresh catalog/duplicate draft as a single undoable creation.
|
|
24
|
+
*
|
|
25
|
+
* Fresh drafts already exist in the scene so renderers and move tools can
|
|
26
|
+
* preview real geometry. On commit we delete that draft while history is
|
|
27
|
+
* paused, then create a clean clone at the final cursor position with history
|
|
28
|
+
* resumed. Undo therefore removes the placed node instead of resurrecting the
|
|
29
|
+
* hidden draft at its origin.
|
|
30
|
+
*/
|
|
31
|
+
export function commitFreshPlacementSubtree(
|
|
32
|
+
rootId: AnyNodeId,
|
|
33
|
+
rootPatch: Partial<AnyNode>,
|
|
34
|
+
): AnyNodeId | null {
|
|
35
|
+
const scene = useScene.getState()
|
|
36
|
+
const subtree = collectSubtree(scene.nodes, rootId)
|
|
37
|
+
if (!subtree) return null
|
|
38
|
+
|
|
39
|
+
const root = cleanPlacementMetadata({
|
|
40
|
+
...subtree.root,
|
|
41
|
+
...rootPatch,
|
|
42
|
+
} as AnyNode)
|
|
43
|
+
const descendants = subtree.descendants.map((node) => cleanPlacementMetadata(node))
|
|
44
|
+
const parentId = parentIdOf(root)
|
|
45
|
+
const cloned = cloneNodesInto([root, ...descendants], {
|
|
46
|
+
rootId,
|
|
47
|
+
parentId,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const temporal = useScene.temporal.getState()
|
|
51
|
+
const wasTracking = (temporal as { isTracking?: boolean }).isTracking !== false
|
|
52
|
+
if (wasTracking) temporal.pause()
|
|
53
|
+
useScene.getState().deleteNode(rootId)
|
|
54
|
+
temporal.resume()
|
|
55
|
+
useScene
|
|
56
|
+
.getState()
|
|
57
|
+
.createNodes(
|
|
58
|
+
cloned.nodes.map((node, index) => (index === 0 && parentId ? { node, parentId } : { node })),
|
|
59
|
+
)
|
|
60
|
+
if (!wasTracking) temporal.pause()
|
|
61
|
+
|
|
62
|
+
return cloned.rootId
|
|
63
|
+
}
|
package/src/lib/history.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useLiveTransforms, useScene } from '@pascal-app/core'
|
|
1
|
+
import { useLiveNodeOverrides, useLiveTransforms, useScene } from '@pascal-app/core'
|
|
2
2
|
|
|
3
3
|
function refreshSceneAfterHistoryJump() {
|
|
4
|
+
useLiveNodeOverrides.getState().clearAll()
|
|
4
5
|
useLiveTransforms.getState().clearAll()
|
|
5
6
|
|
|
6
7
|
const state = useScene.getState()
|
|
@@ -33,50 +33,62 @@ function shouldKeepNode(node: AnyNode, preset: LevelDuplicatePreset) {
|
|
|
33
33
|
return true
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Material field keys per kind, used by the `structure` duplicate preset
|
|
38
|
+
* to strip materials from the cloned subtree. Lookup table replaces the
|
|
39
|
+
* legacy per-kind switch — the Phase 6 grep gate flagged `case '<kind>':`
|
|
40
|
+
* in this file as the remaining per-kind dispatch outside the registry.
|
|
41
|
+
*
|
|
42
|
+
* Future: move this to a `capabilities.materialFields` declaration on
|
|
43
|
+
* each kind's `NodeDefinition` so adding a new kind with materials is a
|
|
44
|
+
* registry-only edit. Today the registry doesn't surface material fields
|
|
45
|
+
* in a uniform way (each kind's panel reads / writes them directly), so
|
|
46
|
+
* this map mirrors the legacy behavior 1:1.
|
|
47
|
+
*/
|
|
48
|
+
const MATERIAL_FIELDS_BY_KIND: Record<string, ReadonlyArray<string>> = {
|
|
49
|
+
wall: [
|
|
50
|
+
'material',
|
|
51
|
+
'materialPreset',
|
|
52
|
+
'interiorMaterial',
|
|
53
|
+
'interiorMaterialPreset',
|
|
54
|
+
'exteriorMaterial',
|
|
55
|
+
'exteriorMaterialPreset',
|
|
56
|
+
],
|
|
57
|
+
slab: ['material', 'materialPreset'],
|
|
58
|
+
ceiling: ['material', 'materialPreset'],
|
|
59
|
+
fence: ['material', 'materialPreset'],
|
|
60
|
+
shelf: ['material', 'materialPreset'],
|
|
61
|
+
'roof-segment': ['material', 'materialPreset'],
|
|
62
|
+
'stair-segment': ['material', 'materialPreset'],
|
|
63
|
+
window: ['material', 'materialPreset'],
|
|
64
|
+
door: ['material', 'materialPreset'],
|
|
65
|
+
roof: [
|
|
66
|
+
'material',
|
|
67
|
+
'materialPreset',
|
|
68
|
+
'topMaterial',
|
|
69
|
+
'topMaterialPreset',
|
|
70
|
+
'edgeMaterial',
|
|
71
|
+
'edgeMaterialPreset',
|
|
72
|
+
'wallMaterial',
|
|
73
|
+
'wallMaterialPreset',
|
|
74
|
+
],
|
|
75
|
+
stair: [
|
|
76
|
+
'material',
|
|
77
|
+
'materialPreset',
|
|
78
|
+
'railingMaterial',
|
|
79
|
+
'railingMaterialPreset',
|
|
80
|
+
'treadMaterial',
|
|
81
|
+
'treadMaterialPreset',
|
|
82
|
+
'sideMaterial',
|
|
83
|
+
'sideMaterialPreset',
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
|
|
36
87
|
function stripMaterials(node: AnyNode): AnyNode {
|
|
88
|
+
const fields = MATERIAL_FIELDS_BY_KIND[node.type]
|
|
89
|
+
if (!fields) return node
|
|
37
90
|
const next = { ...node } as Record<string, unknown>
|
|
38
|
-
|
|
39
|
-
switch (node.type) {
|
|
40
|
-
case 'wall':
|
|
41
|
-
delete next.material
|
|
42
|
-
delete next.materialPreset
|
|
43
|
-
delete next.interiorMaterial
|
|
44
|
-
delete next.interiorMaterialPreset
|
|
45
|
-
delete next.exteriorMaterial
|
|
46
|
-
delete next.exteriorMaterialPreset
|
|
47
|
-
break
|
|
48
|
-
case 'slab':
|
|
49
|
-
case 'ceiling':
|
|
50
|
-
case 'fence':
|
|
51
|
-
case 'roof-segment':
|
|
52
|
-
case 'stair-segment':
|
|
53
|
-
case 'window':
|
|
54
|
-
case 'door':
|
|
55
|
-
delete next.material
|
|
56
|
-
delete next.materialPreset
|
|
57
|
-
break
|
|
58
|
-
case 'roof':
|
|
59
|
-
delete next.material
|
|
60
|
-
delete next.materialPreset
|
|
61
|
-
delete next.topMaterial
|
|
62
|
-
delete next.topMaterialPreset
|
|
63
|
-
delete next.edgeMaterial
|
|
64
|
-
delete next.edgeMaterialPreset
|
|
65
|
-
delete next.wallMaterial
|
|
66
|
-
delete next.wallMaterialPreset
|
|
67
|
-
break
|
|
68
|
-
case 'stair':
|
|
69
|
-
delete next.material
|
|
70
|
-
delete next.materialPreset
|
|
71
|
-
delete next.railingMaterial
|
|
72
|
-
delete next.railingMaterialPreset
|
|
73
|
-
delete next.treadMaterial
|
|
74
|
-
delete next.treadMaterialPreset
|
|
75
|
-
delete next.sideMaterial
|
|
76
|
-
delete next.sideMaterialPreset
|
|
77
|
-
break
|
|
78
|
-
}
|
|
79
|
-
|
|
91
|
+
for (const field of fields) delete next[field]
|
|
80
92
|
return next as AnyNode
|
|
81
93
|
}
|
|
82
94
|
|
|
@@ -123,7 +135,7 @@ export function buildLevelDuplicateCreateOps({
|
|
|
123
135
|
const keptIds = new Set(filteredNodes.map((node) => node.id))
|
|
124
136
|
|
|
125
137
|
const cleanedNodes = filteredNodes.map((node) => {
|
|
126
|
-
if (!('children' in node
|
|
138
|
+
if (!('children' in node && Array.isArray(node.children))) {
|
|
127
139
|
return node
|
|
128
140
|
}
|
|
129
141
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LevelNode } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
export function getDefaultLevelName(level: number): string {
|
|
4
|
+
if (level === 0) return 'Ground Floor'
|
|
5
|
+
if (level > 0) return `Floor ${level}`
|
|
6
|
+
return `Basement ${-level}`
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getLevelDisplayName(level: Pick<LevelNode, 'name' | 'level'>): string {
|
|
10
|
+
return level.name || getDefaultLevelName(level.level)
|
|
11
|
+
}
|