@pascal-app/editor 0.8.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 +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- 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 +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- 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 +5829 -12005
- 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 +178 -149
- 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 +433 -156
- 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 +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- 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-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- 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 +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- 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 +1 -1
- 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 +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- 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 -538
- 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 +56 -0
- 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 +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- 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 +4 -6
- 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 +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -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 +10 -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 +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- 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.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- 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 +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- 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 +320 -37
- 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/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- 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 -264
- 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 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- 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 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- 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 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- 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 -280
- 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 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -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
|
|
|
@@ -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
|
+
}
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
4
6
|
type CeilingNode,
|
|
7
|
+
type ChimneyMaterialRole,
|
|
8
|
+
type ChimneyNode,
|
|
5
9
|
type ColumnNode,
|
|
10
|
+
type DormerSurfaceMaterialRole,
|
|
6
11
|
type FenceNode,
|
|
7
12
|
getCatalogMaterialById,
|
|
8
13
|
getEffectiveRoofSurfaceMaterial,
|
|
14
|
+
getEffectiveSegmentSurfaceMaterial,
|
|
9
15
|
getEffectiveStairSurfaceMaterial,
|
|
10
16
|
getEffectiveWallSurfaceMaterial,
|
|
11
17
|
getLibraryMaterialIdFromRef,
|
|
12
18
|
type MaterialSchema,
|
|
13
19
|
type MaterialTarget,
|
|
20
|
+
nodeRegistry,
|
|
14
21
|
type RoofNode,
|
|
22
|
+
type RoofSegmentNode,
|
|
23
|
+
type RoofSegmentSurfaceMaterialRole,
|
|
15
24
|
type RoofSurfaceMaterialRole,
|
|
25
|
+
type ShelfNode,
|
|
16
26
|
type SlabNode,
|
|
17
27
|
type StairNode,
|
|
18
28
|
type StairSurfaceMaterialRole,
|
|
@@ -22,7 +32,21 @@ import {
|
|
|
22
32
|
|
|
23
33
|
export type PaintableMaterialTarget = Extract<
|
|
24
34
|
MaterialTarget,
|
|
25
|
-
|
|
35
|
+
| 'wall'
|
|
36
|
+
| 'roof'
|
|
37
|
+
| 'stair'
|
|
38
|
+
| 'fence'
|
|
39
|
+
| 'column'
|
|
40
|
+
| 'slab'
|
|
41
|
+
| 'ceiling'
|
|
42
|
+
| 'shelf'
|
|
43
|
+
| 'chimney'
|
|
44
|
+
| 'dormer'
|
|
45
|
+
| 'box-vent'
|
|
46
|
+
| 'ridge-vent'
|
|
47
|
+
| 'turbine-vent'
|
|
48
|
+
| 'cupola'
|
|
49
|
+
| 'eyebrow-vent'
|
|
26
50
|
>
|
|
27
51
|
|
|
28
52
|
export type SingleSurfaceMaterialRole = 'surface'
|
|
@@ -104,6 +128,82 @@ export function buildRoofSurfaceMaterialPatch(
|
|
|
104
128
|
}
|
|
105
129
|
}
|
|
106
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Build a per-segment paint patch for one of the three surface roles. The
|
|
133
|
+
* segment ends up with role-specific fields set (and the legacy catch-all
|
|
134
|
+
* `material` cleared) so subsequent reads pick the role override over any
|
|
135
|
+
* parent-roof fallback.
|
|
136
|
+
*/
|
|
137
|
+
export function buildRoofSegmentSurfaceMaterialPatch(
|
|
138
|
+
node: RoofSegmentNode,
|
|
139
|
+
targetRole: RoofSegmentSurfaceMaterialRole,
|
|
140
|
+
material: MaterialSchema | undefined,
|
|
141
|
+
materialPreset: string | undefined,
|
|
142
|
+
): Partial<RoofSegmentNode> {
|
|
143
|
+
const nextSurfaceMaterial = { material, materialPreset }
|
|
144
|
+
const nextTop =
|
|
145
|
+
targetRole === 'top' ? nextSurfaceMaterial : getEffectiveSegmentSurfaceMaterial(node, 'top')
|
|
146
|
+
const nextEdge =
|
|
147
|
+
targetRole === 'edge' ? nextSurfaceMaterial : getEffectiveSegmentSurfaceMaterial(node, 'edge')
|
|
148
|
+
const nextWall =
|
|
149
|
+
targetRole === 'wall' ? nextSurfaceMaterial : getEffectiveSegmentSurfaceMaterial(node, 'wall')
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
topMaterial: nextTop.material,
|
|
153
|
+
topMaterialPreset: nextTop.materialPreset,
|
|
154
|
+
edgeMaterial: nextEdge.material,
|
|
155
|
+
edgeMaterialPreset: nextEdge.materialPreset,
|
|
156
|
+
wallMaterial: nextWall.material,
|
|
157
|
+
wallMaterialPreset: nextWall.materialPreset,
|
|
158
|
+
material: undefined,
|
|
159
|
+
materialPreset: undefined,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Clear every painted material on a node back to its default. Works for any
|
|
165
|
+
* kind without per-type knowledge: it nulls the catch-all `material` /
|
|
166
|
+
* `materialPreset` plus any role field (`*Material` / `*MaterialPreset`) that
|
|
167
|
+
* the node actually carries. For a roof it also resets every child segment, so
|
|
168
|
+
* a single call defaults the whole roof system. `updateNode` merges patches
|
|
169
|
+
* shallowly without re-validation, so the `undefined` values land as cleared
|
|
170
|
+
* fields and the renderer falls back to the theme defaults.
|
|
171
|
+
*/
|
|
172
|
+
export function buildResetSurfaceMaterialUpdates(
|
|
173
|
+
nodes: Record<string, AnyNode>,
|
|
174
|
+
node: AnyNode,
|
|
175
|
+
): { id: AnyNodeId; data: Partial<AnyNode> }[] {
|
|
176
|
+
const clearPatch = (target: AnyNode): Partial<AnyNode> => {
|
|
177
|
+
const patch: Record<string, undefined> = {}
|
|
178
|
+
for (const key of Object.keys(target)) {
|
|
179
|
+
if (
|
|
180
|
+
key === 'material' ||
|
|
181
|
+
key === 'materialPreset' ||
|
|
182
|
+
key.endsWith('Material') ||
|
|
183
|
+
key.endsWith('MaterialPreset')
|
|
184
|
+
) {
|
|
185
|
+
patch[key] = undefined
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return patch as Partial<AnyNode>
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const updates: { id: AnyNodeId; data: Partial<AnyNode> }[] = [
|
|
192
|
+
{ id: node.id as AnyNodeId, data: clearPatch(node) },
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
if (node.type === 'roof') {
|
|
196
|
+
for (const segmentId of (node as RoofNode).children ?? []) {
|
|
197
|
+
const segment = nodes[segmentId as AnyNodeId]
|
|
198
|
+
if (segment?.type === 'roof-segment') {
|
|
199
|
+
updates.push({ id: segment.id as AnyNodeId, data: clearPatch(segment) })
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return updates
|
|
205
|
+
}
|
|
206
|
+
|
|
107
207
|
export function buildStairSurfaceMaterialPatch(
|
|
108
208
|
node: StairNode,
|
|
109
209
|
targetRole: StairSurfaceMaterialRole,
|
|
@@ -133,7 +233,7 @@ export function buildStairSurfaceMaterialPatch(
|
|
|
133
233
|
}
|
|
134
234
|
|
|
135
235
|
export function buildSingleSurfaceMaterialPatch<
|
|
136
|
-
TNode extends FenceNode | ColumnNode | SlabNode | CeilingNode,
|
|
236
|
+
TNode extends FenceNode | ColumnNode | SlabNode | CeilingNode | ShelfNode,
|
|
137
237
|
>(material: MaterialSchema | undefined, materialPreset: string | undefined): Partial<TNode> {
|
|
138
238
|
return {
|
|
139
239
|
material,
|
|
@@ -141,6 +241,29 @@ export function buildSingleSurfaceMaterialPatch<
|
|
|
141
241
|
} as Partial<TNode>
|
|
142
242
|
}
|
|
143
243
|
|
|
244
|
+
// Chimney / dormer patch builders moved to
|
|
245
|
+
// `@pascal-app/nodes/<kind>/paint.ts` and are wired into the kind's
|
|
246
|
+
// `capabilities.paint.buildPatch`. The selection-manager invokes them
|
|
247
|
+
// through the registry; no editor-side helper needed here.
|
|
248
|
+
//
|
|
249
|
+
// `getEffectiveChimneyMaterial` below stays because
|
|
250
|
+
// `resolveActivePaintMaterialFromSelection` (also in this file) still
|
|
251
|
+
// has wall / roof / stair arms that follow the same shape — they all
|
|
252
|
+
// migrate together in a follow-up.
|
|
253
|
+
|
|
254
|
+
export function getEffectiveChimneyMaterial(
|
|
255
|
+
node: ChimneyNode,
|
|
256
|
+
role: ChimneyMaterialRole,
|
|
257
|
+
): { material: MaterialSchema | undefined; materialPreset: string | undefined } {
|
|
258
|
+
if (role === 'top') {
|
|
259
|
+
const hasTop = node.topMaterial !== undefined || node.topMaterialPreset !== undefined
|
|
260
|
+
if (hasTop) {
|
|
261
|
+
return { material: node.topMaterial, materialPreset: node.topMaterialPreset }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { material: node.material, materialPreset: node.materialPreset }
|
|
265
|
+
}
|
|
266
|
+
|
|
144
267
|
export function resolveActivePaintMaterialFromSelection(params: {
|
|
145
268
|
nodes: Record<string, any>
|
|
146
269
|
selectedId: string | null
|
|
@@ -150,6 +273,8 @@ export function resolveActivePaintMaterialFromSelection(params: {
|
|
|
150
273
|
| WallSurfaceSide
|
|
151
274
|
| StairSurfaceMaterialRole
|
|
152
275
|
| RoofSurfaceMaterialRole
|
|
276
|
+
| ChimneyMaterialRole
|
|
277
|
+
| DormerSurfaceMaterialRole
|
|
153
278
|
| SingleSurfaceMaterialRole
|
|
154
279
|
} | null
|
|
155
280
|
}): ActivePaintMaterial | null {
|
|
@@ -160,22 +285,31 @@ export function resolveActivePaintMaterialFromSelection(params: {
|
|
|
160
285
|
const selectedNode = nodes[selectedId]
|
|
161
286
|
if (!selectedNode) return null
|
|
162
287
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
288
|
+
// Registry-driven path. Kinds that declare
|
|
289
|
+
// `capabilities.paint.getEffectiveMaterial` resolve their effective
|
|
290
|
+
// material here without an editor-side per-kind arm. Wall,
|
|
291
|
+
// chimney, dormer use this; roof / stair stay legacy below.
|
|
292
|
+
const paintCap = nodeRegistry.get(selectedNode.type)?.capabilities?.paint
|
|
293
|
+
if (paintCap?.getEffectiveMaterial) {
|
|
294
|
+
const surface = paintCap.getEffectiveMaterial({
|
|
295
|
+
node: selectedNode,
|
|
296
|
+
role: selectedMaterialTarget.role as string,
|
|
297
|
+
nodes,
|
|
172
298
|
})
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
299
|
+
if (surface) {
|
|
300
|
+
const sourceTarget = selectedNode.type as PaintableMaterialTarget
|
|
301
|
+
return hasActivePaintMaterial({
|
|
302
|
+
material: surface.material,
|
|
303
|
+
materialPreset: surface.materialPreset,
|
|
304
|
+
sourceTarget,
|
|
305
|
+
})
|
|
306
|
+
? {
|
|
307
|
+
material: surface.material,
|
|
308
|
+
materialPreset: surface.materialPreset,
|
|
309
|
+
sourceTarget,
|
|
310
|
+
}
|
|
311
|
+
: null
|
|
312
|
+
}
|
|
179
313
|
}
|
|
180
314
|
|
|
181
315
|
if (
|
|
@@ -184,7 +318,27 @@ export function resolveActivePaintMaterialFromSelection(params: {
|
|
|
184
318
|
selectedMaterialTarget.role === 'edge' ||
|
|
185
319
|
selectedMaterialTarget.role === 'wall')
|
|
186
320
|
) {
|
|
187
|
-
|
|
321
|
+
let surface = getEffectiveRoofSurfaceMaterial(selectedNode, selectedMaterialTarget.role)
|
|
322
|
+
if (
|
|
323
|
+
selectedMaterialTarget.role === 'top' &&
|
|
324
|
+
surface.material === undefined &&
|
|
325
|
+
surface.materialPreset === undefined
|
|
326
|
+
) {
|
|
327
|
+
const roofNode = selectedNode as RoofNode
|
|
328
|
+
const fallbackSegment = (roofNode.children ?? [])
|
|
329
|
+
.map((id: AnyNodeId) => nodes[id as AnyNodeId] as RoofSegmentNode | undefined)
|
|
330
|
+
.find(
|
|
331
|
+
(segment: RoofSegmentNode | undefined) =>
|
|
332
|
+
segment?.type === 'roof-segment' &&
|
|
333
|
+
(segment.material !== undefined || segment.materialPreset !== undefined),
|
|
334
|
+
)
|
|
335
|
+
if (fallbackSegment) {
|
|
336
|
+
surface = {
|
|
337
|
+
material: fallbackSegment.material,
|
|
338
|
+
materialPreset: fallbackSegment.materialPreset,
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
188
342
|
return hasActivePaintMaterial({
|
|
189
343
|
material: surface.material,
|
|
190
344
|
materialPreset: surface.materialPreset,
|
|
@@ -218,13 +372,19 @@ export function resolveActivePaintMaterialFromSelection(params: {
|
|
|
218
372
|
: null
|
|
219
373
|
}
|
|
220
374
|
|
|
375
|
+
// Wall / chimney / dormer flow through the registry-driven path
|
|
376
|
+
// at the top of this function.
|
|
377
|
+
|
|
221
378
|
if (
|
|
222
379
|
(selectedNode.type === 'fence' ||
|
|
223
380
|
selectedNode.type === 'column' ||
|
|
224
381
|
selectedNode.type === 'slab' ||
|
|
225
|
-
selectedNode.type === 'ceiling'
|
|
382
|
+
selectedNode.type === 'ceiling' ||
|
|
383
|
+
selectedNode.type === 'shelf') &&
|
|
226
384
|
selectedMaterialTarget.role === 'surface'
|
|
227
385
|
) {
|
|
386
|
+
// Roof vents previously lived here too; they now resolve via the
|
|
387
|
+
// registry-driven `getEffectiveMaterial` path at the top of this function.
|
|
228
388
|
const target = selectedNode.type
|
|
229
389
|
return hasActivePaintMaterial({
|
|
230
390
|
material: selectedNode.material,
|
|
@@ -280,5 +440,37 @@ export function resolvePaintTargetFromSelection(params: {
|
|
|
280
440
|
return 'ceiling'
|
|
281
441
|
}
|
|
282
442
|
|
|
443
|
+
if (selectedNode.type === 'shelf') {
|
|
444
|
+
return 'shelf'
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (selectedNode.type === 'chimney') {
|
|
448
|
+
return 'chimney'
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (selectedNode.type === 'dormer') {
|
|
452
|
+
return 'dormer'
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (selectedNode.type === 'box-vent') {
|
|
456
|
+
return 'box-vent'
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (selectedNode.type === 'ridge-vent') {
|
|
460
|
+
return 'ridge-vent'
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (selectedNode.type === 'turbine-vent') {
|
|
464
|
+
return 'turbine-vent'
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (selectedNode.type === 'cupola') {
|
|
468
|
+
return 'cupola'
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (selectedNode.type === 'eyebrow-vent') {
|
|
472
|
+
return 'eyebrow-vent'
|
|
473
|
+
}
|
|
474
|
+
|
|
283
475
|
return null
|
|
284
476
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function getPlacementMetadataRecord(metadata: unknown): Record<string, unknown> {
|
|
2
|
+
if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) {
|
|
3
|
+
return {}
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return metadata as Record<string, unknown>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function addFreshPlacementMetadata(metadata: unknown): Record<string, unknown> {
|
|
10
|
+
return {
|
|
11
|
+
...getPlacementMetadataRecord(metadata),
|
|
12
|
+
isNew: true,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isFreshPlacementMetadata(metadata: unknown): boolean {
|
|
17
|
+
return getPlacementMetadataRecord(metadata).isNew === true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function stripPlacementMetadataFlags(metadata: unknown): unknown {
|
|
21
|
+
if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) {
|
|
22
|
+
return metadata
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const nextMeta = { ...(metadata as Record<string, unknown>) }
|
|
26
|
+
delete nextMeta.isNew
|
|
27
|
+
delete nextMeta.isTransient
|
|
28
|
+
return nextMeta
|
|
29
|
+
}
|