@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
package/dist/store/use-scene.js
CHANGED
|
@@ -55,11 +55,10 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
55
55
|
dirtyNodes: new Set(),
|
|
56
56
|
// 4. Collections
|
|
57
57
|
collections: {},
|
|
58
|
+
// 5. Read-only lock
|
|
59
|
+
readOnly: false,
|
|
60
|
+
setReadOnly: (readOnly) => set({ readOnly }),
|
|
58
61
|
unloadScene: () => {
|
|
59
|
-
// Clear temporal tracking to prevent memory leaks from stale node references
|
|
60
|
-
prevPastLength = 0;
|
|
61
|
-
prevFutureLength = 0;
|
|
62
|
-
prevNodesSnapshot = null;
|
|
63
62
|
set({
|
|
64
63
|
nodes: {},
|
|
65
64
|
rootNodeIds: [],
|
|
@@ -74,14 +73,22 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
74
73
|
setScene: (nodes, rootNodeIds) => {
|
|
75
74
|
// Apply backward compatibility migrations
|
|
76
75
|
const patchedNodes = migrateNodes(nodes);
|
|
76
|
+
// Remove orphans: nodes whose parentId points to a non-existent node
|
|
77
|
+
const cleanedNodes = { ...patchedNodes };
|
|
78
|
+
for (const node of Object.values(cleanedNodes)) {
|
|
79
|
+
if (node.parentId && !cleanedNodes[node.parentId]) {
|
|
80
|
+
console.warn('[Scene] Removing orphan node', node.id, '(parentId', node.parentId, 'not found)');
|
|
81
|
+
delete cleanedNodes[node.id];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
77
84
|
set({
|
|
78
|
-
nodes:
|
|
85
|
+
nodes: cleanedNodes,
|
|
79
86
|
rootNodeIds,
|
|
80
87
|
dirtyNodes: new Set(),
|
|
81
88
|
collections: {},
|
|
82
89
|
});
|
|
83
90
|
// Mark all nodes as dirty to trigger re-validation
|
|
84
|
-
Object.values(
|
|
91
|
+
Object.values(cleanedNodes).forEach((node) => {
|
|
85
92
|
get().markDirty(node.id);
|
|
86
93
|
});
|
|
87
94
|
},
|
|
@@ -129,6 +136,8 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
129
136
|
deleteNode: (id) => nodeActions.deleteNodesAction(set, get, [id]),
|
|
130
137
|
// --- COLLECTIONS ---
|
|
131
138
|
createCollection: (name, nodeIds = []) => {
|
|
139
|
+
if (get().readOnly)
|
|
140
|
+
return '';
|
|
132
141
|
const id = generateCollectionId();
|
|
133
142
|
const collection = { id, name, nodeIds };
|
|
134
143
|
set((state) => {
|
|
@@ -147,6 +156,8 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
147
156
|
return id;
|
|
148
157
|
},
|
|
149
158
|
deleteCollection: (id) => {
|
|
159
|
+
if (get().readOnly)
|
|
160
|
+
return;
|
|
150
161
|
set((state) => {
|
|
151
162
|
const col = state.collections[id];
|
|
152
163
|
const nextCollections = { ...state.collections };
|
|
@@ -166,6 +177,8 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
166
177
|
});
|
|
167
178
|
},
|
|
168
179
|
updateCollection: (id, data) => {
|
|
180
|
+
if (get().readOnly)
|
|
181
|
+
return;
|
|
169
182
|
set((state) => {
|
|
170
183
|
const col = state.collections[id];
|
|
171
184
|
if (!col)
|
|
@@ -174,6 +187,8 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
174
187
|
});
|
|
175
188
|
},
|
|
176
189
|
addToCollection: (id, nodeId) => {
|
|
190
|
+
if (get().readOnly)
|
|
191
|
+
return;
|
|
177
192
|
set((state) => {
|
|
178
193
|
const col = state.collections[id];
|
|
179
194
|
if (!col || col.nodeIds.includes(nodeId))
|
|
@@ -194,6 +209,8 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
194
209
|
});
|
|
195
210
|
},
|
|
196
211
|
removeFromCollection: (id, nodeId) => {
|
|
212
|
+
if (get().readOnly)
|
|
213
|
+
return;
|
|
197
214
|
set((state) => {
|
|
198
215
|
const col = state.collections[id];
|
|
199
216
|
if (!col)
|
|
@@ -227,19 +244,12 @@ export default useScene;
|
|
|
227
244
|
let prevPastLength = 0;
|
|
228
245
|
let prevFutureLength = 0;
|
|
229
246
|
let prevNodesSnapshot = null;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
* Should be called when unloading a scene to release node references.
|
|
233
|
-
*/
|
|
234
|
-
export function clearTemporalTracking() {
|
|
247
|
+
export function clearSceneHistory() {
|
|
248
|
+
useScene.temporal.getState().clear();
|
|
235
249
|
prevPastLength = 0;
|
|
236
250
|
prevFutureLength = 0;
|
|
237
251
|
prevNodesSnapshot = null;
|
|
238
252
|
}
|
|
239
|
-
export function clearSceneHistory() {
|
|
240
|
-
useScene.temporal.getState().clear();
|
|
241
|
-
clearTemporalTracking();
|
|
242
|
-
}
|
|
243
253
|
// Subscribe to the temporal store (Undo/Redo events)
|
|
244
254
|
useScene.temporal.subscribe((state) => {
|
|
245
255
|
const currentPastLength = state.pastStates.length;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"door-system.d.ts","sourceRoot":"","sources":["../../../src/systems/door/door-system.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"door-system.d.ts","sourceRoot":"","sources":["../../../src/systems/door/door-system.tsx"],"names":[],"mappings":"AAUA,eAAO,MAAM,UAAU,YA2BtB,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 baseMaterial = new MeshStandardNodeMaterial({
|
|
7
|
-
name: 'door-base',
|
|
8
|
-
color: '#f2f0ed',
|
|
9
|
-
roughness: 0.5,
|
|
10
|
-
metalness: 0,
|
|
11
|
-
});
|
|
12
|
-
const glassMaterial = new MeshStandardNodeMaterial({
|
|
13
|
-
name: 'door-glass',
|
|
14
|
-
color: 'lightblue',
|
|
15
|
-
roughness: 0.05,
|
|
16
|
-
metalness: 0.1,
|
|
17
|
-
transparent: true,
|
|
18
|
-
opacity: 0.35,
|
|
19
|
-
side: DoubleSide,
|
|
20
|
-
depthWrite: false,
|
|
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 DoorSystem = () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAA0B,MAAM,eAAe,CAAA;AAGvE,OAAO,KAAK,EAAgC,eAAe,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAA0B,MAAM,eAAe,CAAA;AAGvE,OAAO,KAAK,EAAgC,eAAe,EAAE,MAAM,cAAc,CAAA;AA+BjF,eAAO,MAAM,UAAU,YAuFtB,CAAA;AAuLD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,eAAe,GACpB;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,KAAK,CAAA;CAAE,GAAG,IAAI,CAsRlF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,eAAe,GAAG,KAAK,CAAC,cAAc,CAoDvF"}
|
|
@@ -7,7 +7,13 @@ import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
|
|
|
7
7
|
import useScene from '../../store/use-scene';
|
|
8
8
|
const csgEvaluator = new Evaluator();
|
|
9
9
|
csgEvaluator.useGroups = true;
|
|
10
|
+
csgEvaluator.consolidateGroups = false; // shared dummyMats across brushes causes consolidation to misalign groupIndices vs groupOrder indices → crash
|
|
10
11
|
csgEvaluator.attributes = ['position', 'normal'];
|
|
12
|
+
function prepareBrushForCSG(brush) {
|
|
13
|
+
brush.geometry.computeBoundsTree = computeBoundsTree;
|
|
14
|
+
brush.geometry.computeBoundsTree({ maxLeafSize: 10 });
|
|
15
|
+
brush.updateMatrixWorld();
|
|
16
|
+
}
|
|
11
17
|
// Pooled objects to avoid per-frame allocation in updateMergedRoofGeometry
|
|
12
18
|
const _matrix = new THREE.Matrix4();
|
|
13
19
|
const _position = new THREE.Vector3();
|
|
@@ -71,6 +77,9 @@ export const RoofSystem = () => {
|
|
|
71
77
|
}
|
|
72
78
|
clearDirty(id);
|
|
73
79
|
}
|
|
80
|
+
else {
|
|
81
|
+
clearDirty(id);
|
|
82
|
+
}
|
|
74
83
|
// Queue the parent roof for a merged geometry update
|
|
75
84
|
if (node.parentId) {
|
|
76
85
|
pendingRoofUpdates.add(node.parentId);
|
|
@@ -151,6 +160,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
151
160
|
const next = csgEvaluator.evaluate(totalShinSlab, brushes.shinSlab, ADDITION);
|
|
152
161
|
totalShinSlab.geometry.dispose();
|
|
153
162
|
brushes.shinSlab.geometry.dispose();
|
|
163
|
+
prepareBrushForCSG(next);
|
|
154
164
|
totalShinSlab = next;
|
|
155
165
|
}
|
|
156
166
|
else {
|
|
@@ -160,6 +170,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
160
170
|
const next = csgEvaluator.evaluate(totalDeckSlab, brushes.deckSlab, ADDITION);
|
|
161
171
|
totalDeckSlab.geometry.dispose();
|
|
162
172
|
brushes.deckSlab.geometry.dispose();
|
|
173
|
+
prepareBrushForCSG(next);
|
|
163
174
|
totalDeckSlab = next;
|
|
164
175
|
}
|
|
165
176
|
else {
|
|
@@ -169,6 +180,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
169
180
|
const next = csgEvaluator.evaluate(totalWall, brushes.wallBrush, ADDITION);
|
|
170
181
|
totalWall.geometry.dispose();
|
|
171
182
|
brushes.wallBrush.geometry.dispose();
|
|
183
|
+
prepareBrushForCSG(next);
|
|
172
184
|
totalWall = next;
|
|
173
185
|
}
|
|
174
186
|
else {
|
|
@@ -178,6 +190,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
178
190
|
const next = csgEvaluator.evaluate(totalInner, brushes.innerBrush, ADDITION);
|
|
179
191
|
totalInner.geometry.dispose();
|
|
180
192
|
brushes.innerBrush.geometry.dispose();
|
|
193
|
+
prepareBrushForCSG(next);
|
|
181
194
|
totalInner = next;
|
|
182
195
|
}
|
|
183
196
|
else {
|
|
@@ -380,6 +393,11 @@ export function getRoofSegmentBrushes(node) {
|
|
|
380
393
|
return null;
|
|
381
394
|
if (!geo.index)
|
|
382
395
|
return null;
|
|
396
|
+
// Strip zero-count groups — three-bvh-csg crashes with groupIndices[i] undefined
|
|
397
|
+
// when a group exists but covers no triangles (can happen after mergeVertices)
|
|
398
|
+
geo.groups = geo.groups.filter((g) => g.count > 0);
|
|
399
|
+
if (geo.groups.length === 0)
|
|
400
|
+
return null;
|
|
383
401
|
geo.computeBoundsTree = computeBoundsTree;
|
|
384
402
|
geo.computeBoundsTree({ maxLeafSize: 10 });
|
|
385
403
|
const brush = new Brush(geo, dummyMats);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAOvD,eAAO,MAAM,UAAU,YAwBtB,CAAA;
|
|
1
|
+
{"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAOvD,eAAO,MAAM,UAAU,YAwBtB,CAAA;AAuED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAAC,cAAc,CAG7E"}
|
|
@@ -34,6 +34,10 @@ function updateSlabGeometry(node, mesh) {
|
|
|
34
34
|
const newGeo = generateSlabGeometry(node);
|
|
35
35
|
mesh.geometry.dispose();
|
|
36
36
|
mesh.geometry = newGeo;
|
|
37
|
+
// For negative elevation, shift the mesh down so the top face sits at Y=elevation
|
|
38
|
+
// rather than at Y=0. Positive elevation stays at Y=0 (slab sits at floor level).
|
|
39
|
+
const elevation = node.elevation ?? 0.05;
|
|
40
|
+
mesh.position.y = elevation < 0 ? elevation : 0;
|
|
37
41
|
}
|
|
38
42
|
/** Half of default wall thickness — used to extend slab geometry under walls */
|
|
39
43
|
const SLAB_OUTSET = 0.05;
|
|
@@ -89,44 +93,85 @@ function outsetPolygon(polygon, amount) {
|
|
|
89
93
|
* Generates extruded slab geometry from polygon
|
|
90
94
|
*/
|
|
91
95
|
export function generateSlabGeometry(slabNode) {
|
|
96
|
+
const elevation = slabNode.elevation ?? 0.05;
|
|
97
|
+
return elevation < 0 ? generatePoolGeometry(slabNode) : generatePositiveSlabGeometry(slabNode);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Standard slab: flat extrusion upward from Y=0 by elevation thickness.
|
|
101
|
+
*/
|
|
102
|
+
function generatePositiveSlabGeometry(slabNode) {
|
|
92
103
|
const polygon = outsetPolygon(slabNode.polygon, SLAB_OUTSET);
|
|
93
104
|
const elevation = slabNode.elevation ?? 0.05;
|
|
94
|
-
if (polygon.length < 3)
|
|
105
|
+
if (polygon.length < 3)
|
|
95
106
|
return new THREE.BufferGeometry();
|
|
96
|
-
}
|
|
97
|
-
// Create shape from polygon
|
|
98
|
-
// Shape is in X-Y plane, we'll rotate to X-Z plane after extrusion
|
|
99
107
|
const shape = new THREE.Shape();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for (let i = 1; i < polygon.length; i++) {
|
|
104
|
-
const pt = polygon[i];
|
|
105
|
-
shape.lineTo(pt[0], -pt[1]);
|
|
106
|
-
}
|
|
108
|
+
shape.moveTo(polygon[0][0], -polygon[0][1]);
|
|
109
|
+
for (let i = 1; i < polygon.length; i++)
|
|
110
|
+
shape.lineTo(polygon[i][0], -polygon[i][1]);
|
|
107
111
|
shape.closePath();
|
|
108
|
-
|
|
109
|
-
const holes = slabNode.holes || [];
|
|
110
|
-
for (const holePolygon of holes) {
|
|
112
|
+
for (const holePolygon of slabNode.holes ?? []) {
|
|
111
113
|
if (holePolygon.length < 3)
|
|
112
114
|
continue;
|
|
113
115
|
const holePath = new THREE.Path();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const pt = holePolygon[i];
|
|
118
|
-
holePath.lineTo(pt[0], -pt[1]);
|
|
119
|
-
}
|
|
116
|
+
holePath.moveTo(holePolygon[0][0], -holePolygon[0][1]);
|
|
117
|
+
for (let i = 1; i < holePolygon.length; i++)
|
|
118
|
+
holePath.lineTo(holePolygon[i][0], -holePolygon[i][1]);
|
|
120
119
|
holePath.closePath();
|
|
121
120
|
shape.holes.push(holePath);
|
|
122
121
|
}
|
|
123
|
-
|
|
124
|
-
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
125
|
-
depth: elevation,
|
|
126
|
-
bevelEnabled: false,
|
|
127
|
-
});
|
|
128
|
-
// Rotate so extrusion direction (Z) becomes height direction (Y)
|
|
122
|
+
const geometry = new THREE.ExtrudeGeometry(shape, { depth: elevation, bevelEnabled: false });
|
|
129
123
|
geometry.rotateX(-Math.PI / 2);
|
|
130
124
|
geometry.computeVertexNormals();
|
|
131
125
|
return geometry;
|
|
132
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Pool / recessed slab: floor cap at Y=0 (local) + inner walls up to Y=|elevation|.
|
|
129
|
+
* No top cap — the opening at ground level is handled by the ground occluder hole.
|
|
130
|
+
* mesh.position.y must be set to elevation so the floor sits at the correct world Y.
|
|
131
|
+
*
|
|
132
|
+
* Geometry is built directly in 3D (Y-up) to avoid rotation confusion:
|
|
133
|
+
* - floor in XZ plane at Y=0, normals pointing +Y (visible when looking down into pool)
|
|
134
|
+
* - walls from Y=0 to Y=depth, inward-facing normals (visible from inside pool)
|
|
135
|
+
*/
|
|
136
|
+
function generatePoolGeometry(slabNode) {
|
|
137
|
+
const polygon = outsetPolygon(slabNode.polygon, SLAB_OUTSET);
|
|
138
|
+
const depth = Math.abs(slabNode.elevation ?? 0.05);
|
|
139
|
+
if (polygon.length < 3)
|
|
140
|
+
return new THREE.BufferGeometry();
|
|
141
|
+
const positions = [];
|
|
142
|
+
const indices = [];
|
|
143
|
+
const n = polygon.length;
|
|
144
|
+
// --- Floor at Y=0 ---
|
|
145
|
+
for (const [x, z] of polygon)
|
|
146
|
+
positions.push(x, 0, z);
|
|
147
|
+
const pts2d = polygon.map(([x, z]) => new THREE.Vector2(x, z));
|
|
148
|
+
const holesPts2d = (slabNode.holes ?? []).map((h) => h.map(([x, z]) => new THREE.Vector2(x, z)));
|
|
149
|
+
for (const hole of slabNode.holes ?? []) {
|
|
150
|
+
for (const [x, z] of hole)
|
|
151
|
+
positions.push(x, 0, z);
|
|
152
|
+
}
|
|
153
|
+
const floorTris = THREE.ShapeUtils.triangulateShape(pts2d, holesPts2d);
|
|
154
|
+
for (const tri of floorTris) {
|
|
155
|
+
// Reversed winding → normals point +Y (upward) in XZ plane
|
|
156
|
+
indices.push(tri[0], tri[2], tri[1]);
|
|
157
|
+
}
|
|
158
|
+
// --- Inner walls (no top cap at Y=depth) ---
|
|
159
|
+
// Standard winding on a CCW polygon in XZ gives inward-facing normals.
|
|
160
|
+
for (let i = 0; i < n; i++) {
|
|
161
|
+
const j = (i + 1) % n;
|
|
162
|
+
const [x0, z0] = polygon[i];
|
|
163
|
+
const [x1, z1] = polygon[j];
|
|
164
|
+
const vBase = positions.length / 3;
|
|
165
|
+
positions.push(x0, 0, z0); // v0 — floor level
|
|
166
|
+
positions.push(x1, 0, z1); // v1 — floor level
|
|
167
|
+
positions.push(x1, depth, z1); // v2 — ground level
|
|
168
|
+
positions.push(x0, depth, z0); // v3 — ground level
|
|
169
|
+
indices.push(vBase, vBase + 1, vBase + 2);
|
|
170
|
+
indices.push(vBase, vBase + 2, vBase + 3);
|
|
171
|
+
}
|
|
172
|
+
const geo = new THREE.BufferGeometry();
|
|
173
|
+
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
174
|
+
geo.setIndex(indices);
|
|
175
|
+
geo.computeVertexNormals();
|
|
176
|
+
return geo;
|
|
177
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stair-system.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-system.tsx"],"names":[],"mappings":"AAiBA,eAAO,MAAM,WAAW,YA4FvB,CAAA"}
|