@pascal-app/core 0.3.3 → 0.4.0
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/dist/events/bus.d.ts +4 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts +2 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +3 -0
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +11 -3
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -8
- package/dist/materials.d.ts +10 -0
- package/dist/materials.d.ts.map +1 -0
- package/dist/materials.js +22 -0
- package/dist/schema/index.d.ts +3 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +3 -1
- package/dist/schema/material.d.ts +2 -2
- package/dist/schema/nodes/ceiling.d.ts +1 -1
- package/dist/schema/nodes/door.d.ts +1 -1
- package/dist/schema/nodes/item.d.ts +2 -2
- package/dist/schema/nodes/level.d.ts +1 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +2 -0
- package/dist/schema/nodes/roof-segment.d.ts +1 -1
- package/dist/schema/nodes/roof.d.ts +1 -1
- package/dist/schema/nodes/site.d.ts +1 -1
- package/dist/schema/nodes/slab.d.ts +1 -1
- package/dist/schema/nodes/stair-segment.d.ts +81 -0
- package/dist/schema/nodes/stair-segment.d.ts.map +1 -0
- package/dist/schema/nodes/stair-segment.js +42 -0
- package/dist/schema/nodes/stair.d.ts +56 -0
- package/dist/schema/nodes/stair.d.ts.map +1 -0
- package/dist/schema/nodes/stair.js +22 -0
- package/dist/schema/nodes/wall.d.ts +1 -1
- package/dist/schema/nodes/window.d.ts +1 -1
- package/dist/schema/types.d.ts +128 -10
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +4 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +25 -29
- package/dist/store/use-live-transforms.d.ts +14 -0
- package/dist/store/use-live-transforms.d.ts.map +1 -0
- package/dist/store/use-live-transforms.js +20 -0
- package/dist/store/use-scene.d.ts +2 -5
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +25 -15
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +1 -17
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +18 -0
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +71 -26
- package/dist/systems/stair/stair-system.d.ts +2 -0
- package/dist/systems/stair/stair-system.d.ts.map +1 -0
- package/dist/systems/stair/stair-system.js +354 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +2 -0
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +8 -24
- package/dist/utils/clone-scene-graph.d.ts +25 -1
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +160 -5
- package/package.json +6 -1
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { useFrame } from '@react-three/fiber';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
4
|
+
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
|
|
5
|
+
import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager';
|
|
6
|
+
import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
|
|
7
|
+
import useScene from '../../store/use-scene';
|
|
8
|
+
const pendingStairUpdates = new Set();
|
|
9
|
+
const MAX_STAIRS_PER_FRAME = 2;
|
|
10
|
+
const MAX_SEGMENTS_PER_FRAME = 4;
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// STAIR SYSTEM
|
|
13
|
+
// ============================================================================
|
|
14
|
+
export const StairSystem = () => {
|
|
15
|
+
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
16
|
+
const clearDirty = useScene((state) => state.clearDirty);
|
|
17
|
+
const rootNodeIds = useScene((state) => state.rootNodeIds);
|
|
18
|
+
useFrame(() => {
|
|
19
|
+
if (rootNodeIds.length === 0) {
|
|
20
|
+
pendingStairUpdates.clear();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (dirtyNodes.size === 0 && pendingStairUpdates.size === 0)
|
|
24
|
+
return;
|
|
25
|
+
const nodes = useScene.getState().nodes;
|
|
26
|
+
// --- Pass 1: Process dirty stair-segments (throttled) ---
|
|
27
|
+
// Collect parent stair IDs that need segment transform recomputation
|
|
28
|
+
const parentsNeedingSegmentSync = new Set();
|
|
29
|
+
let segmentsProcessed = 0;
|
|
30
|
+
dirtyNodes.forEach((id) => {
|
|
31
|
+
const node = nodes[id];
|
|
32
|
+
if (!node)
|
|
33
|
+
return;
|
|
34
|
+
if (node.type === 'stair-segment') {
|
|
35
|
+
const mesh = sceneRegistry.nodes.get(id);
|
|
36
|
+
if (mesh) {
|
|
37
|
+
const isVisible = mesh.parent?.visible !== false;
|
|
38
|
+
if (isVisible && segmentsProcessed < MAX_SEGMENTS_PER_FRAME) {
|
|
39
|
+
// Geometry will be updated; chained position is applied in the parent sync pass below
|
|
40
|
+
updateStairSegmentGeometry(node, mesh);
|
|
41
|
+
if (node.parentId)
|
|
42
|
+
parentsNeedingSegmentSync.add(node.parentId);
|
|
43
|
+
segmentsProcessed++;
|
|
44
|
+
}
|
|
45
|
+
else if (isVisible) {
|
|
46
|
+
return; // Over budget — keep dirty, process next frame
|
|
47
|
+
}
|
|
48
|
+
else if (mesh.geometry.type === 'BoxGeometry') {
|
|
49
|
+
// Replace BoxGeometry placeholder with empty geometry
|
|
50
|
+
mesh.geometry.dispose();
|
|
51
|
+
const placeholder = new THREE.BufferGeometry();
|
|
52
|
+
placeholder.setAttribute('position', new THREE.Float32BufferAttribute([], 3));
|
|
53
|
+
mesh.geometry = placeholder;
|
|
54
|
+
}
|
|
55
|
+
clearDirty(id);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
clearDirty(id);
|
|
59
|
+
}
|
|
60
|
+
// Queue the parent stair for a merged geometry update
|
|
61
|
+
if (node.parentId) {
|
|
62
|
+
pendingStairUpdates.add(node.parentId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (node.type === 'stair') {
|
|
66
|
+
pendingStairUpdates.add(id);
|
|
67
|
+
// Also sync individual segment positions when in edit mode
|
|
68
|
+
parentsNeedingSegmentSync.add(id);
|
|
69
|
+
clearDirty(id);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// --- Pass 1b: Sync chained transforms to individual segment meshes (edit mode) ---
|
|
73
|
+
for (const stairId of parentsNeedingSegmentSync) {
|
|
74
|
+
const stairNode = nodes[stairId];
|
|
75
|
+
if (!stairNode || stairNode.type !== 'stair')
|
|
76
|
+
continue;
|
|
77
|
+
const group = sceneRegistry.nodes.get(stairId);
|
|
78
|
+
if (group) {
|
|
79
|
+
syncStairGroupElevation(stairNode, group, nodes);
|
|
80
|
+
}
|
|
81
|
+
syncSegmentMeshTransforms(stairNode, nodes);
|
|
82
|
+
}
|
|
83
|
+
// --- Pass 2: Process pending merged-stair updates (throttled) ---
|
|
84
|
+
let stairsProcessed = 0;
|
|
85
|
+
for (const id of pendingStairUpdates) {
|
|
86
|
+
if (stairsProcessed >= MAX_STAIRS_PER_FRAME)
|
|
87
|
+
break;
|
|
88
|
+
const node = nodes[id];
|
|
89
|
+
if (!node || node.type !== 'stair') {
|
|
90
|
+
pendingStairUpdates.delete(id);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const group = sceneRegistry.nodes.get(id);
|
|
94
|
+
if (group) {
|
|
95
|
+
const mergedMesh = group.getObjectByName('merged-stair');
|
|
96
|
+
if (mergedMesh?.visible !== false) {
|
|
97
|
+
updateMergedStairGeometry(node, group, nodes);
|
|
98
|
+
stairsProcessed++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
pendingStairUpdates.delete(id);
|
|
102
|
+
}
|
|
103
|
+
}, 5);
|
|
104
|
+
return null;
|
|
105
|
+
};
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// SEGMENT GEOMETRY
|
|
108
|
+
// ============================================================================
|
|
109
|
+
/**
|
|
110
|
+
* Generates the step/landing profile as a THREE.Shape (in the XY plane),
|
|
111
|
+
* then extrudes along Z for the segment width.
|
|
112
|
+
*/
|
|
113
|
+
function generateStairSegmentGeometry(segment, absoluteHeight) {
|
|
114
|
+
const { width, length, height, stepCount, segmentType, fillToFloor, thickness } = segment;
|
|
115
|
+
const shape = new THREE.Shape();
|
|
116
|
+
if (segmentType === 'landing') {
|
|
117
|
+
shape.moveTo(0, 0);
|
|
118
|
+
shape.lineTo(length, 0);
|
|
119
|
+
if (fillToFloor) {
|
|
120
|
+
shape.lineTo(length, -absoluteHeight);
|
|
121
|
+
shape.lineTo(0, -absoluteHeight);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
shape.lineTo(length, -thickness);
|
|
125
|
+
shape.lineTo(0, -thickness);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const riserHeight = height / stepCount;
|
|
130
|
+
const treadDepth = length / stepCount;
|
|
131
|
+
shape.moveTo(0, 0);
|
|
132
|
+
// Draw step profile
|
|
133
|
+
for (let i = 0; i < stepCount; i++) {
|
|
134
|
+
shape.lineTo(i * treadDepth, (i + 1) * riserHeight);
|
|
135
|
+
shape.lineTo((i + 1) * treadDepth, (i + 1) * riserHeight);
|
|
136
|
+
}
|
|
137
|
+
if (fillToFloor) {
|
|
138
|
+
shape.lineTo(length, -absoluteHeight);
|
|
139
|
+
shape.lineTo(0, -absoluteHeight);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// Sloped bottom with consistent thickness
|
|
143
|
+
const angle = Math.atan(riserHeight / treadDepth);
|
|
144
|
+
const vOff = thickness / Math.cos(angle);
|
|
145
|
+
// Bottom-back corner
|
|
146
|
+
shape.lineTo(length, height - vOff);
|
|
147
|
+
if (absoluteHeight === 0) {
|
|
148
|
+
// Ground floor: slope hits the ground (y=0)
|
|
149
|
+
const m = riserHeight / treadDepth;
|
|
150
|
+
const xGround = length - (height - vOff) / m;
|
|
151
|
+
if (xGround > 0) {
|
|
152
|
+
shape.lineTo(xGround, 0);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Floating: parallel slope
|
|
157
|
+
shape.lineTo(0, -vOff);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
shape.lineTo(0, 0);
|
|
162
|
+
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
163
|
+
steps: 1,
|
|
164
|
+
depth: width,
|
|
165
|
+
bevelEnabled: false,
|
|
166
|
+
});
|
|
167
|
+
// Rotate so extrusion is along X (width), and the shape is in the XZ plane
|
|
168
|
+
// Shape is drawn in XY, extruded along Z → rotate -90° around Y then offset
|
|
169
|
+
const matrix = new THREE.Matrix4();
|
|
170
|
+
matrix.makeRotationY(-Math.PI / 2);
|
|
171
|
+
matrix.setPosition(width / 2, 0, 0);
|
|
172
|
+
geometry.applyMatrix4(matrix);
|
|
173
|
+
return geometry;
|
|
174
|
+
}
|
|
175
|
+
function updateStairSegmentGeometry(node, mesh) {
|
|
176
|
+
// Compute absolute height from parent chain
|
|
177
|
+
const absoluteHeight = computeAbsoluteHeight(node);
|
|
178
|
+
const newGeometry = generateStairSegmentGeometry(node, absoluteHeight);
|
|
179
|
+
mesh.geometry.dispose();
|
|
180
|
+
mesh.geometry = newGeometry;
|
|
181
|
+
// NOTE: position/rotation are NOT set here — they're set by syncSegmentMeshTransforms
|
|
182
|
+
// which computes the chained position based on segment order and attachmentSide.
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Applies chained transforms to individual segment meshes (edit mode).
|
|
186
|
+
* Each segment's world position is determined by the chain of previous segments,
|
|
187
|
+
* not by the node's stored position field.
|
|
188
|
+
*/
|
|
189
|
+
function syncSegmentMeshTransforms(stairNode, nodes) {
|
|
190
|
+
const segments = (stairNode.children ?? [])
|
|
191
|
+
.map((childId) => nodes[childId])
|
|
192
|
+
.filter((n) => n?.type === 'stair-segment');
|
|
193
|
+
if (segments.length === 0)
|
|
194
|
+
return;
|
|
195
|
+
const transforms = computeSegmentTransforms(segments);
|
|
196
|
+
for (let i = 0; i < segments.length; i++) {
|
|
197
|
+
const segment = segments[i];
|
|
198
|
+
const transform = transforms[i];
|
|
199
|
+
const mesh = sceneRegistry.nodes.get(segment.id);
|
|
200
|
+
if (mesh) {
|
|
201
|
+
mesh.position.set(transform.position[0], transform.position[1], transform.position[2]);
|
|
202
|
+
mesh.rotation.y = transform.rotation;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function syncStairGroupElevation(stairNode, group, nodes) {
|
|
207
|
+
const levelId = resolveLevelId(stairNode, nodes);
|
|
208
|
+
const slabElevation = getStairSlabElevation(levelId, stairNode, nodes);
|
|
209
|
+
group.position.y = stairNode.position[1] + slabElevation;
|
|
210
|
+
}
|
|
211
|
+
function getStairSlabElevation(levelId, stairNode, nodes) {
|
|
212
|
+
const segments = (stairNode.children ?? [])
|
|
213
|
+
.map((childId) => nodes[childId])
|
|
214
|
+
.filter((n) => n?.type === 'stair-segment');
|
|
215
|
+
if (segments.length === 0)
|
|
216
|
+
return 0;
|
|
217
|
+
const transforms = computeSegmentTransforms(segments);
|
|
218
|
+
let maxElevation = Number.NEGATIVE_INFINITY;
|
|
219
|
+
for (let i = 0; i < segments.length; i++) {
|
|
220
|
+
const segment = segments[i];
|
|
221
|
+
const transform = transforms[i];
|
|
222
|
+
const [centerOffsetX, centerOffsetZ] = rotateXZ(0, segment.length / 2, transform.rotation);
|
|
223
|
+
const centerInGroupX = transform.position[0] + centerOffsetX;
|
|
224
|
+
const centerInGroupZ = transform.position[2] + centerOffsetZ;
|
|
225
|
+
const [centerOffsetWorldX, centerOffsetWorldZ] = rotateXZ(centerInGroupX, centerInGroupZ, stairNode.rotation);
|
|
226
|
+
const slabElevation = spatialGridManager.getSlabElevationForItem(levelId, [
|
|
227
|
+
stairNode.position[0] + centerOffsetWorldX,
|
|
228
|
+
stairNode.position[1] + transform.position[1],
|
|
229
|
+
stairNode.position[2] + centerOffsetWorldZ,
|
|
230
|
+
], [segment.width, Math.max(segment.height, segment.thickness, 0.01), segment.length], [0, stairNode.rotation + transform.rotation, 0]);
|
|
231
|
+
if (slabElevation > maxElevation) {
|
|
232
|
+
maxElevation = slabElevation;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return maxElevation === Number.NEGATIVE_INFINITY ? 0 : maxElevation;
|
|
236
|
+
}
|
|
237
|
+
// ============================================================================
|
|
238
|
+
// MERGED STAIR GEOMETRY
|
|
239
|
+
// ============================================================================
|
|
240
|
+
const _matrix = new THREE.Matrix4();
|
|
241
|
+
const _position = new THREE.Vector3();
|
|
242
|
+
const _quaternion = new THREE.Quaternion();
|
|
243
|
+
const _scale = new THREE.Vector3(1, 1, 1);
|
|
244
|
+
const _yAxis = new THREE.Vector3(0, 1, 0);
|
|
245
|
+
function updateMergedStairGeometry(stairNode, group, nodes) {
|
|
246
|
+
const mergedMesh = group.getObjectByName('merged-stair');
|
|
247
|
+
if (!mergedMesh)
|
|
248
|
+
return;
|
|
249
|
+
const children = stairNode.children ?? [];
|
|
250
|
+
const segments = children
|
|
251
|
+
.map((childId) => nodes[childId])
|
|
252
|
+
.filter((n) => n?.type === 'stair-segment');
|
|
253
|
+
if (segments.length === 0) {
|
|
254
|
+
mergedMesh.geometry.dispose();
|
|
255
|
+
mergedMesh.geometry = new THREE.BufferGeometry();
|
|
256
|
+
mergedMesh.geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Compute chained transforms for segments
|
|
260
|
+
const transforms = computeSegmentTransforms(segments);
|
|
261
|
+
const geometries = [];
|
|
262
|
+
for (let i = 0; i < segments.length; i++) {
|
|
263
|
+
const segment = segments[i];
|
|
264
|
+
const transform = transforms[i];
|
|
265
|
+
const absoluteHeight = transform.position[1];
|
|
266
|
+
const geo = generateStairSegmentGeometry(segment, absoluteHeight);
|
|
267
|
+
// Apply segment transform (position + rotation) relative to parent stair
|
|
268
|
+
_position.set(transform.position[0], transform.position[1], transform.position[2]);
|
|
269
|
+
_quaternion.setFromAxisAngle(_yAxis, transform.rotation);
|
|
270
|
+
_matrix.compose(_position, _quaternion, _scale);
|
|
271
|
+
geo.applyMatrix4(_matrix);
|
|
272
|
+
geometries.push(geo);
|
|
273
|
+
}
|
|
274
|
+
const merged = mergeGeometries(geometries, false);
|
|
275
|
+
if (merged) {
|
|
276
|
+
mergedMesh.geometry.dispose();
|
|
277
|
+
mergedMesh.geometry = merged;
|
|
278
|
+
}
|
|
279
|
+
// Dispose individual geometries
|
|
280
|
+
for (const geo of geometries) {
|
|
281
|
+
geo.dispose();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Computes world-relative transforms for each segment by chaining
|
|
286
|
+
* based on attachmentSide. This mirrors the prototype's StairSystem logic.
|
|
287
|
+
*/
|
|
288
|
+
function computeSegmentTransforms(segments) {
|
|
289
|
+
const transforms = [];
|
|
290
|
+
let currentPos = new THREE.Vector3(0, 0, 0);
|
|
291
|
+
let currentRot = 0;
|
|
292
|
+
for (let i = 0; i < segments.length; i++) {
|
|
293
|
+
const segment = segments[i];
|
|
294
|
+
if (i === 0) {
|
|
295
|
+
transforms.push({
|
|
296
|
+
position: [currentPos.x, currentPos.y, currentPos.z],
|
|
297
|
+
rotation: currentRot,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
const prev = segments[i - 1];
|
|
302
|
+
const localAttachPos = new THREE.Vector3();
|
|
303
|
+
let rotChange = 0;
|
|
304
|
+
switch (segment.attachmentSide) {
|
|
305
|
+
case 'front':
|
|
306
|
+
localAttachPos.set(0, prev.height, prev.length);
|
|
307
|
+
rotChange = 0;
|
|
308
|
+
break;
|
|
309
|
+
case 'left':
|
|
310
|
+
localAttachPos.set(prev.width / 2, prev.height, prev.length / 2);
|
|
311
|
+
rotChange = Math.PI / 2;
|
|
312
|
+
break;
|
|
313
|
+
case 'right':
|
|
314
|
+
localAttachPos.set(-prev.width / 2, prev.height, prev.length / 2);
|
|
315
|
+
rotChange = -Math.PI / 2;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
// Rotate local attachment point by previous global rotation
|
|
319
|
+
localAttachPos.applyAxisAngle(new THREE.Vector3(0, 1, 0), currentRot);
|
|
320
|
+
currentPos = currentPos.clone().add(localAttachPos);
|
|
321
|
+
currentRot += rotChange;
|
|
322
|
+
transforms.push({
|
|
323
|
+
position: [currentPos.x, currentPos.y, currentPos.z],
|
|
324
|
+
rotation: currentRot,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return transforms;
|
|
329
|
+
}
|
|
330
|
+
function rotateXZ(x, z, angle) {
|
|
331
|
+
const cos = Math.cos(angle);
|
|
332
|
+
const sin = Math.sin(angle);
|
|
333
|
+
return [x * cos + z * sin, -x * sin + z * cos];
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Computes the absolute Y height of a segment by traversing the stair's segment chain.
|
|
337
|
+
*/
|
|
338
|
+
function computeAbsoluteHeight(node) {
|
|
339
|
+
const nodes = useScene.getState().nodes;
|
|
340
|
+
if (!node.parentId)
|
|
341
|
+
return 0;
|
|
342
|
+
const parent = nodes[node.parentId];
|
|
343
|
+
if (!parent || parent.type !== 'stair')
|
|
344
|
+
return 0;
|
|
345
|
+
const stair = parent;
|
|
346
|
+
const segments = (stair.children ?? [])
|
|
347
|
+
.map((childId) => nodes[childId])
|
|
348
|
+
.filter((n) => n?.type === 'stair-segment');
|
|
349
|
+
const transforms = computeSegmentTransforms(segments);
|
|
350
|
+
const index = segments.findIndex((s) => s.id === node.id);
|
|
351
|
+
if (index < 0)
|
|
352
|
+
return 0;
|
|
353
|
+
return transforms[index]?.position[1] ?? 0;
|
|
354
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAM9B,OAAO,KAAK,EAAE,OAAO,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAGhE,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAM9B,OAAO,KAAK,EAAE,OAAO,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAGhE,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAA;AAUxB,eAAO,MAAM,UAAU,YAuDtB,CAAA;AA0DD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,oFA+FlB"}
|
|
@@ -13,6 +13,7 @@ const csgEvaluator = new Evaluator();
|
|
|
13
13
|
// ============================================================================
|
|
14
14
|
// WALL SYSTEM
|
|
15
15
|
// ============================================================================
|
|
16
|
+
let useFrameNb = 0;
|
|
16
17
|
export const WallSystem = () => {
|
|
17
18
|
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
18
19
|
const clearDirty = useScene((state) => state.clearDirty);
|
|
@@ -22,6 +23,7 @@ export const WallSystem = () => {
|
|
|
22
23
|
const nodes = useScene.getState().nodes;
|
|
23
24
|
// Collect dirty walls and their levels
|
|
24
25
|
const dirtyWallsByLevel = new Map();
|
|
26
|
+
useFrameNb += 1;
|
|
25
27
|
dirtyNodes.forEach((id) => {
|
|
26
28
|
const node = nodes[id];
|
|
27
29
|
if (!node || node.type !== 'wall')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAUA,eAAO,MAAM,YAAY,YA2BxB,CAAA"}
|
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
import { useFrame } from '@react-three/fiber';
|
|
2
2
|
import * as THREE from 'three';
|
|
3
|
-
import { DoubleSide, MeshStandardNodeMaterial } from 'three/webgpu';
|
|
4
3
|
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
|
|
4
|
+
import { baseMaterial, glassMaterial } from '../../materials';
|
|
5
5
|
import useScene from '../../store/use-scene';
|
|
6
|
-
const glassMaterial = new MeshStandardNodeMaterial({
|
|
7
|
-
name: 'glass',
|
|
8
|
-
color: 'lightblue',
|
|
9
|
-
roughness: 0.05,
|
|
10
|
-
metalness: 0.1,
|
|
11
|
-
transparent: true,
|
|
12
|
-
opacity: 0.3,
|
|
13
|
-
side: DoubleSide,
|
|
14
|
-
depthWrite: false,
|
|
15
|
-
});
|
|
16
|
-
const frameMaterial = new MeshStandardNodeMaterial({
|
|
17
|
-
name: 'window-frame',
|
|
18
|
-
color: '#e8e8e8',
|
|
19
|
-
roughness: 0.6,
|
|
20
|
-
metalness: 0,
|
|
21
|
-
});
|
|
22
6
|
// Invisible material for root mesh — used as selection hitbox only
|
|
23
7
|
const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
|
|
24
8
|
export const WindowSystem = () => {
|
|
@@ -71,11 +55,11 @@ function updateWindowMesh(node, mesh) {
|
|
|
71
55
|
const innerH = height - 2 * frameThickness;
|
|
72
56
|
// ── Frame members ──
|
|
73
57
|
// Top / bottom — full width
|
|
74
|
-
addBox(mesh,
|
|
75
|
-
addBox(mesh,
|
|
58
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
59
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, -height / 2 + frameThickness / 2, 0);
|
|
76
60
|
// Left / right — inner height to avoid corner overlap
|
|
77
|
-
addBox(mesh,
|
|
78
|
-
addBox(mesh,
|
|
61
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
62
|
+
addBox(mesh, baseMaterial, frameThickness, innerH, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
79
63
|
// ── Pane grid ──
|
|
80
64
|
const numCols = columnRatios.length;
|
|
81
65
|
const numRows = rowRatios.length;
|
|
@@ -107,7 +91,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
107
91
|
cx = -innerW / 2;
|
|
108
92
|
for (let c = 0; c < numCols - 1; c++) {
|
|
109
93
|
cx += colWidths[c];
|
|
110
|
-
addBox(mesh,
|
|
94
|
+
addBox(mesh, baseMaterial, columnDividerThickness, innerH, frameDepth, cx + columnDividerThickness / 2, 0, 0);
|
|
111
95
|
cx += columnDividerThickness;
|
|
112
96
|
}
|
|
113
97
|
// Row dividers — per column width, so they don't overlap column dividers (top to bottom)
|
|
@@ -116,7 +100,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
116
100
|
cy -= rowHeights[r];
|
|
117
101
|
const divY = cy - rowDividerThickness / 2;
|
|
118
102
|
for (let c = 0; c < numCols; c++) {
|
|
119
|
-
addBox(mesh,
|
|
103
|
+
addBox(mesh, baseMaterial, colWidths[c], rowDividerThickness, frameDepth, colXCenters[c], divY, 0);
|
|
120
104
|
}
|
|
121
105
|
cy -= rowDividerThickness;
|
|
122
106
|
}
|
|
@@ -132,7 +116,7 @@ function updateWindowMesh(node, mesh) {
|
|
|
132
116
|
const sillW = width + sillDepth * 0.4; // slightly wider than frame
|
|
133
117
|
// Protrudes from the front face of the frame (+Z)
|
|
134
118
|
const sillZ = frameDepth / 2 + sillDepth / 2;
|
|
135
|
-
addBox(mesh,
|
|
119
|
+
addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
|
|
136
120
|
}
|
|
137
121
|
// ── Cutout (for wall CSG) — always full window dimensions, 1m deep ──
|
|
138
122
|
let cutout = mesh.getObjectByName('cutout');
|
|
@@ -10,9 +10,33 @@ export type SceneGraph = {
|
|
|
10
10
|
* parent-child relationships and other internal references.
|
|
11
11
|
*
|
|
12
12
|
* This is useful for:
|
|
13
|
+
* - Duplicating a project (host app creates a new project record, then loads the cloned scene)
|
|
13
14
|
* - Copying nodes between different projects
|
|
14
|
-
* - Duplicating a subset of a scene within the same project
|
|
15
15
|
* - Multi-scene in-memory scenarios
|
|
16
16
|
*/
|
|
17
17
|
export declare function cloneSceneGraph(sceneGraph: SceneGraph): SceneGraph;
|
|
18
|
+
/**
|
|
19
|
+
* Deep clones a level node and all its descendants with fresh IDs.
|
|
20
|
+
* All internal references (parentId, children, wallId) are remapped to the new IDs.
|
|
21
|
+
* The cloned level node's parentId is preserved (building ID) — not remapped.
|
|
22
|
+
*
|
|
23
|
+
* Unlike `cloneSceneGraph` (which operates on serialized data), this function works
|
|
24
|
+
* on live runtime nodes that may have non-serializable properties (Three.js objects,
|
|
25
|
+
* etc.). It uses JSON roundtrip to safely strip them.
|
|
26
|
+
*
|
|
27
|
+
* @returns clonedNodes - flat array of all cloned nodes (level + descendants)
|
|
28
|
+
* @returns newLevelId - the ID of the cloned level node
|
|
29
|
+
* @returns idMap - old ID → new ID mapping
|
|
30
|
+
*/
|
|
31
|
+
export declare function cloneLevelSubtree(nodes: Record<AnyNodeId, AnyNode>, levelId: AnyNodeId): {
|
|
32
|
+
clonedNodes: AnyNode[];
|
|
33
|
+
newLevelId: AnyNodeId;
|
|
34
|
+
idMap: Map<string, string>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Forks a scene graph for use as a new project: clones with new IDs and strips
|
|
38
|
+
* scan and guide nodes (and their references) since those contain user-uploaded
|
|
39
|
+
* imagery that shouldn't carry over to a forked project.
|
|
40
|
+
*/
|
|
41
|
+
export declare function forkSceneGraph(sceneGraph: SceneGraph): SceneGraph;
|
|
18
42
|
//# sourceMappingURL=clone-scene-graph.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clone-scene-graph.d.ts","sourceRoot":"","sources":["../../src/utils/clone-scene-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAErE,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACjC,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;CAC/C,CAAA;AAUD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"clone-scene-graph.d.ts","sourceRoot":"","sources":["../../src/utils/clone-scene-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAErE,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACjC,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;CAC/C,CAAA;AAUD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CAuGlE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,OAAO,EAAE,SAAS,GACjB;IAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAAC,UAAU,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CA8E/E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CAgEjE"}
|