@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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
nodeRegistry,
|
|
5
|
+
resolveBuildingForLevel,
|
|
6
|
+
sceneRegistry,
|
|
7
|
+
} from '@pascal-app/core'
|
|
8
|
+
import { Box3, Matrix4 } from 'three'
|
|
9
|
+
|
|
10
|
+
// Shared plumbing for the group transform gizmos (rotate + move). Both operate
|
|
11
|
+
// on the same multi-selection: classify each participant by how its placement
|
|
12
|
+
// transforms, snapshot pre-drag state, and pull connected wall/fence neighbours
|
|
13
|
+
// along so junctions stay welded.
|
|
14
|
+
|
|
15
|
+
// Outward clearance from a bbox corner so a gizmo doesn't sit on the geometry.
|
|
16
|
+
export const CORNER_OFFSET = 0.3
|
|
17
|
+
// Two endpoints within this distance count as the same junction — a hair looser
|
|
18
|
+
// than the store's 1e-6 so near-but-not-exact corners still hold together.
|
|
19
|
+
const JUNCTION_EPS = 1e-4
|
|
20
|
+
|
|
21
|
+
export type Vec2 = [number, number]
|
|
22
|
+
export type Vec3 = [number, number, number]
|
|
23
|
+
|
|
24
|
+
const isVec3 = (v: unknown): v is Vec3 =>
|
|
25
|
+
Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === 'number')
|
|
26
|
+
const isVec2 = (v: unknown): v is Vec2 =>
|
|
27
|
+
Array.isArray(v) && v.length === 2 && v.every((n) => typeof n === 'number')
|
|
28
|
+
|
|
29
|
+
// How a participant's placement transforms rigidly around / with the group:
|
|
30
|
+
// - 'vec3' position + [x,y,z] rotation (items, …)
|
|
31
|
+
// - 'scalar' position + numeric rotation (columns)
|
|
32
|
+
// - 'endpoint' start/end tuples (walls, fences)
|
|
33
|
+
export type ParticipantKind = 'vec3' | 'scalar' | 'endpoint'
|
|
34
|
+
|
|
35
|
+
// A selected node qualifies when it belongs to the active level's horizontal
|
|
36
|
+
// frame: either parented to that level, or declared building-scoped and parented
|
|
37
|
+
// to the active level's building. Doors/windows parent to their wall, so they're
|
|
38
|
+
// excluded here and ride their wall.
|
|
39
|
+
function isInGroupTransformScope(
|
|
40
|
+
node: AnyNode | undefined,
|
|
41
|
+
levelId: string | null,
|
|
42
|
+
sceneNodes: Record<string, AnyNode | undefined>,
|
|
43
|
+
): boolean {
|
|
44
|
+
if (!node || !levelId) return false
|
|
45
|
+
if (node.parentId === levelId) return true
|
|
46
|
+
|
|
47
|
+
if (nodeRegistry.get(node.type)?.floorplanScope !== 'building') {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const buildingId = resolveBuildingForLevel(
|
|
52
|
+
levelId as AnyNodeId,
|
|
53
|
+
sceneNodes as Record<AnyNodeId, AnyNode>,
|
|
54
|
+
)
|
|
55
|
+
return Boolean(buildingId && node.parentId === buildingId)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getLegacyScenePosition(node: AnyNode): Vec3 | null {
|
|
59
|
+
if (node.type !== 'elevator') return null
|
|
60
|
+
const object = sceneRegistry.nodes.get(node.id)
|
|
61
|
+
if (!object) return [0, 0, 0]
|
|
62
|
+
return [object.position.x, object.position.y, object.position.z]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getParticipantPosition(node: AnyNode): Vec3 | null {
|
|
66
|
+
const p = (node as { position?: unknown }).position
|
|
67
|
+
if (isVec3(p)) return p
|
|
68
|
+
return getLegacyScenePosition(node)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getParticipantScalarRotation(node: AnyNode): number | null {
|
|
72
|
+
const r = (node as { rotation?: unknown }).rotation
|
|
73
|
+
if (typeof r === 'number' && Number.isFinite(r)) return r
|
|
74
|
+
if (node.type !== 'elevator') return null
|
|
75
|
+
return sceneRegistry.nodes.get(node.id)?.rotation.y ?? 0
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function classifyParticipant(
|
|
79
|
+
node: AnyNode | undefined,
|
|
80
|
+
levelId: string | null,
|
|
81
|
+
sceneNodes: Record<string, AnyNode | undefined>,
|
|
82
|
+
): ParticipantKind | null {
|
|
83
|
+
if (!node || !isInGroupTransformScope(node, levelId, sceneNodes)) return null
|
|
84
|
+
const p = getParticipantPosition(node)
|
|
85
|
+
const r = (node as { rotation?: unknown }).rotation
|
|
86
|
+
const start = (node as { start?: unknown }).start
|
|
87
|
+
const end = (node as { end?: unknown }).end
|
|
88
|
+
if (isVec3(p) && isVec3(r)) return 'vec3'
|
|
89
|
+
if (isVec3(p) && getParticipantScalarRotation(node) !== null) return 'scalar'
|
|
90
|
+
if (isVec2(start) && isVec2(end)) return 'endpoint'
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Pre-drag placement snapshot + how to transform it.
|
|
95
|
+
export type ParticipantStart =
|
|
96
|
+
| { id: AnyNodeId; kind: 'vec3'; position: Vec3; rotation: Vec3 }
|
|
97
|
+
| { id: AnyNodeId; kind: 'scalar'; position: Vec3; rotation: number }
|
|
98
|
+
| { id: AnyNodeId; kind: 'endpoint'; start: Vec2; end: Vec2 }
|
|
99
|
+
|
|
100
|
+
// An unselected wall/fence sharing a junction with a transforming endpoint. Only
|
|
101
|
+
// the touching endpoint(s) follow, so the neighbour stays attached while its far
|
|
102
|
+
// end stays put (it stretches, mirroring single-wall move).
|
|
103
|
+
export type LinkedNeighbor = {
|
|
104
|
+
id: AnyNodeId
|
|
105
|
+
start: Vec2
|
|
106
|
+
end: Vec2
|
|
107
|
+
startLinked: boolean
|
|
108
|
+
endLinked: boolean
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const nearPoint = (a: Vec2, b: Vec2) =>
|
|
112
|
+
Math.abs(a[0] - b[0]) <= JUNCTION_EPS && Math.abs(a[1] - b[1]) <= JUNCTION_EPS
|
|
113
|
+
|
|
114
|
+
// Snapshot the selected participants and the connected (unselected) wall/fence
|
|
115
|
+
// neighbours whose shared endpoints should follow the transform.
|
|
116
|
+
export function collectParticipants(
|
|
117
|
+
ids: string[],
|
|
118
|
+
sceneNodes: Record<string, AnyNode | undefined>,
|
|
119
|
+
levelId: string | null,
|
|
120
|
+
): { starts: ParticipantStart[]; links: LinkedNeighbor[] } {
|
|
121
|
+
const starts: ParticipantStart[] = []
|
|
122
|
+
for (const id of ids) {
|
|
123
|
+
const node = sceneNodes[id]
|
|
124
|
+
const kind = classifyParticipant(node, levelId, sceneNodes)
|
|
125
|
+
if (!node || !kind) continue
|
|
126
|
+
if (kind === 'vec3') {
|
|
127
|
+
const n = node as AnyNode & { position: Vec3; rotation: Vec3 }
|
|
128
|
+
const position = getParticipantPosition(node)
|
|
129
|
+
if (!position) continue
|
|
130
|
+
starts.push({
|
|
131
|
+
id: id as AnyNodeId,
|
|
132
|
+
kind,
|
|
133
|
+
position: [position[0], position[1], position[2]],
|
|
134
|
+
rotation: [n.rotation[0], n.rotation[1], n.rotation[2]],
|
|
135
|
+
})
|
|
136
|
+
} else if (kind === 'scalar') {
|
|
137
|
+
const position = getParticipantPosition(node)
|
|
138
|
+
const rotation = getParticipantScalarRotation(node)
|
|
139
|
+
if (!(position && rotation !== null)) continue
|
|
140
|
+
starts.push({
|
|
141
|
+
id: id as AnyNodeId,
|
|
142
|
+
kind,
|
|
143
|
+
position: [position[0], position[1], position[2]],
|
|
144
|
+
rotation,
|
|
145
|
+
})
|
|
146
|
+
} else {
|
|
147
|
+
const n = node as AnyNode & { start: Vec2; end: Vec2 }
|
|
148
|
+
starts.push({
|
|
149
|
+
id: id as AnyNodeId,
|
|
150
|
+
kind,
|
|
151
|
+
start: [n.start[0], n.start[1]],
|
|
152
|
+
end: [n.end[0], n.end[1]],
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const endpoints: Vec2[] = []
|
|
158
|
+
for (const s of starts) {
|
|
159
|
+
if (s.kind === 'endpoint') endpoints.push(s.start, s.end)
|
|
160
|
+
}
|
|
161
|
+
const links: LinkedNeighbor[] = []
|
|
162
|
+
if (endpoints.length > 0) {
|
|
163
|
+
const selected = new Set(starts.map((s) => s.id))
|
|
164
|
+
for (const [nid, node] of Object.entries(sceneNodes)) {
|
|
165
|
+
if (selected.has(nid as AnyNodeId)) continue
|
|
166
|
+
if (classifyParticipant(node, levelId, sceneNodes) !== 'endpoint') continue
|
|
167
|
+
const n = node as AnyNode & { start: Vec2; end: Vec2 }
|
|
168
|
+
const start: Vec2 = [n.start[0], n.start[1]]
|
|
169
|
+
const end: Vec2 = [n.end[0], n.end[1]]
|
|
170
|
+
const startLinked = endpoints.some((p) => nearPoint(start, p))
|
|
171
|
+
const endLinked = endpoints.some((p) => nearPoint(end, p))
|
|
172
|
+
if (startLinked || endLinked) {
|
|
173
|
+
links.push({ id: nid as AnyNodeId, start, end, startLinked, endLinked })
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { starts, links }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Grow a selection to the full connected component of walls/fences: any
|
|
181
|
+
// endpoint node transitively reachable through shared junctions from a selected
|
|
182
|
+
// endpoint node joins in, so the whole rigid structure transforms as one piece
|
|
183
|
+
// (rather than tearing/stretching at the boundary). Non-endpoint selections
|
|
184
|
+
// (items, columns) pass through unchanged.
|
|
185
|
+
export function expandToComponent(
|
|
186
|
+
selectedIds: string[],
|
|
187
|
+
sceneNodes: Record<string, AnyNode | undefined>,
|
|
188
|
+
levelId: string | null,
|
|
189
|
+
): string[] {
|
|
190
|
+
const endpoints: { id: string; start: Vec2; end: Vec2 }[] = []
|
|
191
|
+
for (const [id, node] of Object.entries(sceneNodes)) {
|
|
192
|
+
if (classifyParticipant(node, levelId, sceneNodes) === 'endpoint') {
|
|
193
|
+
const n = node as AnyNode & { start: Vec2; end: Vec2 }
|
|
194
|
+
endpoints.push({ id, start: [n.start[0], n.start[1]], end: [n.end[0], n.end[1]] })
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const included = new Set(selectedIds)
|
|
198
|
+
if (!endpoints.some((e) => included.has(e.id))) return selectedIds
|
|
199
|
+
|
|
200
|
+
let changed = true
|
|
201
|
+
while (changed) {
|
|
202
|
+
changed = false
|
|
203
|
+
for (const e of endpoints) {
|
|
204
|
+
if (included.has(e.id)) continue
|
|
205
|
+
const touches = endpoints.some(
|
|
206
|
+
(o) =>
|
|
207
|
+
included.has(o.id) &&
|
|
208
|
+
(nearPoint(e.start, o.start) ||
|
|
209
|
+
nearPoint(e.start, o.end) ||
|
|
210
|
+
nearPoint(e.end, o.start) ||
|
|
211
|
+
nearPoint(e.end, o.end)),
|
|
212
|
+
)
|
|
213
|
+
if (touches) {
|
|
214
|
+
included.add(e.id)
|
|
215
|
+
changed = true
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return Array.from(included)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Frozen world matrix of the level group + its inverse. A node's placement
|
|
223
|
+
// (`position` / `start` / `end`) is stored in its parent level's frame, but the
|
|
224
|
+
// gizmos raycast the ground plane in WORLD space. When the building is rotated
|
|
225
|
+
// those frames diverge, so a world-space drag delta / rotation pivot must be
|
|
226
|
+
// converted into the level frame before it's written back to placements —
|
|
227
|
+
// otherwise the move drifts off-axis from the cursor and the rotation orbits a
|
|
228
|
+
// displaced centre. Returns identity matrices when the level isn't mounted, which
|
|
229
|
+
// collapses to the old behaviour (world == local) for an unrotated building.
|
|
230
|
+
export function levelFrame(levelId: string | null): { matrix: Matrix4; inverse: Matrix4 } {
|
|
231
|
+
const obj = levelId ? sceneRegistry.nodes.get(levelId as AnyNodeId) : null
|
|
232
|
+
if (!obj) return { matrix: new Matrix4(), inverse: new Matrix4() }
|
|
233
|
+
obj.updateWorldMatrix(true, false)
|
|
234
|
+
const matrix = obj.matrixWorld.clone()
|
|
235
|
+
return { matrix, inverse: matrix.clone().invert() }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// World-space union bounding box of the selected meshes, or null if none are
|
|
239
|
+
// mounted yet. Used to place the gizmos (which are portalled to the scene root,
|
|
240
|
+
// so they live in world space); placement writes convert back to the level frame
|
|
241
|
+
// via `levelFrame`.
|
|
242
|
+
export function computeGroupBox(ids: string[]): Box3 | null {
|
|
243
|
+
const box = new Box3()
|
|
244
|
+
const tmp = new Box3()
|
|
245
|
+
let found = false
|
|
246
|
+
for (const id of ids) {
|
|
247
|
+
const obj = sceneRegistry.nodes.get(id)
|
|
248
|
+
if (!obj) continue
|
|
249
|
+
obj.updateWorldMatrix(true, true)
|
|
250
|
+
tmp.setFromObject(obj)
|
|
251
|
+
if (tmp.isEmpty()) continue
|
|
252
|
+
box.union(tmp)
|
|
253
|
+
found = true
|
|
254
|
+
}
|
|
255
|
+
return found ? box : null
|
|
256
|
+
}
|