@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,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,32 +273,43 @@ 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 {
|
|
156
281
|
const { nodes, selectedId, selectedMaterialTarget } = params
|
|
157
|
-
if (!selectedId
|
|
282
|
+
if (!(selectedId && selectedMaterialTarget) || selectedMaterialTarget.nodeId !== selectedId)
|
|
158
283
|
return null
|
|
159
284
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { resolvePlanarCursorPosition } from './planar-cursor-placement'
|
|
3
|
+
|
|
4
|
+
const snapHalf = (value: number) => Math.round(value / 0.5) * 0.5
|
|
5
|
+
|
|
6
|
+
describe('resolvePlanarCursorPosition', () => {
|
|
7
|
+
test('absolute mode places the point directly at the snapped cursor', () => {
|
|
8
|
+
const result = resolvePlanarCursorPosition({
|
|
9
|
+
cursor: [1.24, -2.26],
|
|
10
|
+
original: [10, 10],
|
|
11
|
+
anchor: null,
|
|
12
|
+
mode: 'absolute',
|
|
13
|
+
snap: snapHalf,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
expect(result.point).toEqual([1, -2.5])
|
|
17
|
+
expect(result.anchor).toBeNull()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('relative mode preserves the original grab offset from the first cursor sample', () => {
|
|
21
|
+
const start = resolvePlanarCursorPosition({
|
|
22
|
+
cursor: [4.1, 6.1],
|
|
23
|
+
original: [10, 20],
|
|
24
|
+
anchor: null,
|
|
25
|
+
mode: 'relative',
|
|
26
|
+
snap: snapHalf,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
expect(start.point).toEqual([10, 20])
|
|
30
|
+
expect(start.anchor).toEqual([4.1, 6.1])
|
|
31
|
+
|
|
32
|
+
const moved = resolvePlanarCursorPosition({
|
|
33
|
+
cursor: [4.9, 5.2],
|
|
34
|
+
original: [10, 20],
|
|
35
|
+
anchor: start.anchor,
|
|
36
|
+
mode: 'relative',
|
|
37
|
+
snap: snapHalf,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
expect(moved.point).toEqual([11, 19])
|
|
41
|
+
expect(moved.anchor).toEqual([4.1, 6.1])
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type PlanarPoint = [number, number]
|
|
2
|
+
|
|
3
|
+
export type PlanarCursorPlacementMode = 'absolute' | 'relative'
|
|
4
|
+
|
|
5
|
+
type ResolvePlanarCursorPositionArgs = {
|
|
6
|
+
cursor: PlanarPoint
|
|
7
|
+
original: PlanarPoint
|
|
8
|
+
anchor: PlanarPoint | null
|
|
9
|
+
mode: PlanarCursorPlacementMode
|
|
10
|
+
snap?: (value: number) => number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type ResolvePlanarCursorPositionResult = {
|
|
14
|
+
point: PlanarPoint
|
|
15
|
+
anchor: PlanarPoint | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const identity = (value: number) => value
|
|
19
|
+
|
|
20
|
+
export function resolvePlanarCursorPosition({
|
|
21
|
+
cursor,
|
|
22
|
+
original,
|
|
23
|
+
anchor,
|
|
24
|
+
mode,
|
|
25
|
+
snap = identity,
|
|
26
|
+
}: ResolvePlanarCursorPositionArgs): ResolvePlanarCursorPositionResult {
|
|
27
|
+
if (mode === 'absolute') {
|
|
28
|
+
return {
|
|
29
|
+
point: [snap(cursor[0]), snap(cursor[1])],
|
|
30
|
+
anchor,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resolvedAnchor = anchor ?? cursor
|
|
35
|
+
return {
|
|
36
|
+
point: [
|
|
37
|
+
original[0] + snap(cursor[0] - resolvedAnchor[0]),
|
|
38
|
+
original[1] + snap(cursor[1] - resolvedAnchor[1]),
|
|
39
|
+
],
|
|
40
|
+
anchor: resolvedAnchor,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -134,7 +134,7 @@ export function duplicateRoofSubtree(
|
|
|
134
134
|
createdParent && 'children' in createdParent && Array.isArray(createdParent.children)
|
|
135
135
|
? (createdParent.children as AnyNodeId[])
|
|
136
136
|
: null
|
|
137
|
-
if (!createdParent
|
|
137
|
+
if (!(createdParent && parentChildIds?.includes(createdRoof.id as AnyNodeId))) {
|
|
138
138
|
throw new Error(`Duplicated roof "${createdRoof.id}" was not linked to parent "${parentId}"`)
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -175,7 +175,7 @@ export function duplicateRoofSubtree(
|
|
|
175
175
|
|
|
176
176
|
export function clearRoofDuplicateMetadata(
|
|
177
177
|
roofId: AnyNodeId,
|
|
178
|
-
updates: Partial<Pick<RoofNode, 'position' | 'rotation' | 'metadata'>> = {},
|
|
178
|
+
updates: Partial<Pick<RoofNode, 'position' | 'rotation' | 'metadata' | 'visible'>> = {},
|
|
179
179
|
) {
|
|
180
180
|
const scene = useScene.getState()
|
|
181
181
|
const roofNode = scene.nodes[roofId]
|
package/src/lib/scene-bounds.ts
CHANGED
|
@@ -33,7 +33,7 @@ function extendPoint(
|
|
|
33
33
|
z: unknown,
|
|
34
34
|
): void {
|
|
35
35
|
if (typeof x !== 'number' || typeof z !== 'number') return
|
|
36
|
-
if (!Number.isFinite(x)
|
|
36
|
+
if (!(Number.isFinite(x) && Number.isFinite(z))) return
|
|
37
37
|
if (x < acc.minX) acc.minX = x
|
|
38
38
|
if (x > acc.maxX) acc.maxX = x
|
|
39
39
|
if (z < acc.minZ) acc.minZ = z
|