@xeokit/xeokit-sdk 2.6.94 → 2.6.95
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/xeokit-sdk.cjs.js +4380 -4578
- package/dist/xeokit-sdk.es.js +4380 -4578
- package/dist/xeokit-sdk.es5.js +716 -746
- package/dist/xeokit-sdk.min.cjs.js +8 -8
- package/dist/xeokit-sdk.min.es.js +8 -8
- package/dist/xeokit-sdk.min.es5.js +7 -7
- package/package.json +1 -1
- package/src/viewer/scene/math/math.js +310 -1
- package/src/viewer/scene/model/SceneModelEntity.js +6 -3
- package/src/viewer/scene/scene/Scene.js +0 -4
- package/src/viewer/scene/sectionCaps/SectionCaps.js +136 -642
|
@@ -3,24 +3,8 @@ import { Mesh } from "../mesh/Mesh.js";
|
|
|
3
3
|
import { ReadableGeometry } from "../geometry/ReadableGeometry.js";
|
|
4
4
|
import { buildLineGeometry } from "../geometry/index.js";
|
|
5
5
|
import { PhongMaterial } from "../materials/PhongMaterial.js";
|
|
6
|
-
import earcut from '../libs/earcut.js';
|
|
7
6
|
|
|
8
|
-
const epsilon = 1e-6;
|
|
9
|
-
const worldUp = [0, 1, 0];
|
|
10
|
-
const worldRight = [1, 0, 0];
|
|
11
7
|
const tempVec3a = math.vec3();
|
|
12
|
-
const tempVec3b = math.vec3();
|
|
13
|
-
const tempVec3c = math.vec3();
|
|
14
|
-
const tempVec3d = math.vec3();
|
|
15
|
-
const planeOff = math.vec3();
|
|
16
|
-
|
|
17
|
-
function pointsEqual(p1, p2) {
|
|
18
|
-
return (
|
|
19
|
-
Math.abs(p1[0] - p2[0]) < epsilon &&
|
|
20
|
-
Math.abs(p1[1] - p2[1]) < epsilon &&
|
|
21
|
-
Math.abs(p1[2] - p2[2]) < epsilon
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
8
|
|
|
25
9
|
/**
|
|
26
10
|
* @desc Implements hatching for Solid objects on a {@link Scene}.
|
|
@@ -120,642 +104,152 @@ class SectionCaps {
|
|
|
120
104
|
* @constructor
|
|
121
105
|
*/
|
|
122
106
|
constructor(scene) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (dirty) {
|
|
176
|
-
this._update();
|
|
177
|
-
}
|
|
178
|
-
})
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if(!this._dirtyMap[modelId])
|
|
182
|
-
this._dirtyMap[modelId] = new Map();
|
|
183
|
-
|
|
184
|
-
this._dirtyMap[modelId].set(entityId, true);
|
|
185
|
-
this._update();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
_update() {
|
|
189
|
-
clearTimeout(this._updateTimeout);
|
|
190
|
-
this._deletePreviousModels();
|
|
191
|
-
this._updateTimeout = setTimeout(() => {
|
|
192
|
-
clearTimeout(this._updateTimeout);
|
|
193
|
-
const sceneModels = Object.values(this.scene.models).filter(sceneModel => sceneModel.visible);
|
|
194
|
-
this._addHatches(sceneModels, this._sectionPlanes.filter(sectionPlane => sectionPlane.active));
|
|
195
|
-
this._setAllDirty(false);
|
|
196
|
-
}, 100);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
_setAllDirty(value) {
|
|
200
|
-
for(const key in this._dirty) {
|
|
201
|
-
this._dirtyMap[key].forEach((_, key2) => this._dirtyMap[key].set(key2, value));
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
_addHatches(sceneModels, planes) {
|
|
206
|
-
|
|
207
|
-
planes.forEach((plane) => {
|
|
208
|
-
sceneModels.forEach((sceneModel) => {
|
|
209
|
-
if(!this._doesPlaneIntersectBoundingBox(sceneModel.aabb, plane)) return;
|
|
210
|
-
|
|
211
|
-
if(!this._dirtyMap[sceneModel.id]) return;
|
|
212
|
-
|
|
213
|
-
//#region calculating segments in unsorted way
|
|
214
|
-
//we calculate the segments by intersecting plane with each triangle
|
|
215
|
-
const unsortedSegments = new Map();
|
|
216
|
-
const objects = sceneModel.objects;
|
|
217
|
-
// Preallocate arrays for triangle vertices to avoid repeated allocation
|
|
218
|
-
const triangle = [
|
|
219
|
-
math.vec3(),
|
|
220
|
-
math.vec3(),
|
|
221
|
-
math.vec3()
|
|
222
|
-
];
|
|
223
|
-
|
|
224
|
-
this._dirtyMap[sceneModel.id].forEach((isDirty, objectId) => {
|
|
225
|
-
if (!isDirty) {
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const object = objects[objectId];
|
|
230
|
-
|
|
231
|
-
if(!this._doesPlaneIntersectBoundingBox(object.aabb, plane)) return;
|
|
232
|
-
|
|
233
|
-
if(!this._sceneModelsData[sceneModel.id]) {
|
|
234
|
-
const aabb = sceneModel.aabb;
|
|
235
|
-
this._sceneModelsData[sceneModel.id] = {
|
|
236
|
-
verticesMap: new Map(),
|
|
237
|
-
indicesMap: new Map(),
|
|
238
|
-
// modelOrigin is critical to use when handling models with large coordinates.
|
|
239
|
-
// See XCD-306 and examples/slicing/SectionCaps_at_distance.html for more details.
|
|
240
|
-
modelOrigin: math.vec3([
|
|
241
|
-
(aabb[0] + aabb[3]) / 2,
|
|
242
|
-
(aabb[1] + aabb[4]) / 2,
|
|
243
|
-
(aabb[2] + aabb[5]) / 2
|
|
244
|
-
])
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const sceneModelData = this._sceneModelsData[sceneModel.id];
|
|
249
|
-
const modelOrigin = sceneModelData.modelOrigin;
|
|
250
|
-
|
|
251
|
-
if(!sceneModelData.verticesMap.has(objectId)) {
|
|
252
|
-
const isSolid = object.meshes[0].isSolid();
|
|
253
|
-
const vertices = [ ];
|
|
254
|
-
const indices = [ ];
|
|
255
|
-
if(isSolid && object.capMaterial) {
|
|
256
|
-
object.getEachVertex(v => vertices.push(v[0]-modelOrigin[0], v[1]-modelOrigin[1], v[2]-modelOrigin[2]));
|
|
257
|
-
object.getEachIndex(i => indices.push(i));
|
|
258
|
-
}
|
|
259
|
-
sceneModelData.verticesMap.set(objectId, vertices);
|
|
260
|
-
sceneModelData.indicesMap.set(objectId, indices);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const vertices = sceneModelData.verticesMap.get(objectId);
|
|
264
|
-
const indices = sceneModelData.indicesMap.get(objectId);
|
|
265
|
-
const planeDist = -math.dotVec3(math.subVec3(plane.pos, modelOrigin, tempVec3a), plane.dir);
|
|
266
|
-
|
|
267
|
-
const capSegments = [];
|
|
268
|
-
const vertCount = indices.length;
|
|
269
|
-
|
|
270
|
-
for (let i = 0; i < vertCount; i += 3) {
|
|
271
|
-
// Reuse triangle buffer instead of creating new arrays
|
|
272
|
-
for (let j = 0; j < 3; j++) {
|
|
273
|
-
const idx = indices[i + j] * 3;
|
|
274
|
-
triangle[j][0] = vertices[idx];
|
|
275
|
-
triangle[j][1] = vertices[idx + 1];
|
|
276
|
-
triangle[j][2] = vertices[idx + 2];
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Early null check
|
|
280
|
-
if (!triangle[0][0] && !triangle[0][1] && !triangle[0][2]) continue;
|
|
281
|
-
|
|
282
|
-
const intersections = [];
|
|
283
|
-
for (let i = 0; i < 3; i++) {
|
|
284
|
-
const p1 = triangle[i];
|
|
285
|
-
const p2 = triangle[(i + 1) % 3];
|
|
286
|
-
|
|
287
|
-
const d1 = planeDist + math.dotVec3(plane.dir, p1);
|
|
288
|
-
const d2 = planeDist + math.dotVec3(plane.dir, p2);
|
|
289
|
-
|
|
290
|
-
if (d1 * d2 > 0) continue;
|
|
291
|
-
|
|
292
|
-
const t = -d1 / (d2 - d1);
|
|
293
|
-
|
|
294
|
-
intersections.push(math.lerpVec3(t, 0, 1, p1, p2, math.vec3()));
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if(intersections.length === 2) capSegments.push(intersections);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (capSegments.length > 0) {
|
|
301
|
-
unsortedSegments.set(objectId, capSegments);
|
|
302
|
-
}
|
|
303
|
-
})
|
|
304
|
-
//#endregion
|
|
305
|
-
|
|
306
|
-
//#region sorting the segments
|
|
307
|
-
const orderedSegments = new Map();
|
|
308
|
-
unsortedSegments.forEach((unsortedSegment, segmentedId) => {
|
|
309
|
-
orderedSegments.set(segmentedId, [
|
|
310
|
-
[
|
|
311
|
-
unsortedSegment[0] //this is also an array of two vectors
|
|
312
|
-
]
|
|
313
|
-
]);
|
|
314
|
-
unsortedSegment.splice(0, 1);
|
|
315
|
-
let index = 0;
|
|
316
|
-
while (unsortedSegment.length > 0) {
|
|
317
|
-
const lastPoint = orderedSegments.get(segmentedId)[index][orderedSegments.get(segmentedId)[index].length - 1][1];
|
|
318
|
-
let found = false;
|
|
319
|
-
|
|
320
|
-
for (let i = 0; i < unsortedSegment.length; i++) {
|
|
321
|
-
const [start, end] = unsortedSegment[i];
|
|
322
|
-
if (pointsEqual(lastPoint, start)) {
|
|
323
|
-
orderedSegments.get(segmentedId)[index].push(unsortedSegment[i]);
|
|
324
|
-
unsortedSegment.splice(i, 1);
|
|
325
|
-
found = true;
|
|
326
|
-
break;
|
|
327
|
-
} else if (pointsEqual(lastPoint, end)) {
|
|
328
|
-
orderedSegments.get(segmentedId)[index].push([end, start]);
|
|
329
|
-
unsortedSegment.splice(i, 1);
|
|
330
|
-
found = true;
|
|
331
|
-
break;
|
|
107
|
+
let destroy = null;
|
|
108
|
+
const modelCaches = { };
|
|
109
|
+
const sectionPlanes = [ ];
|
|
110
|
+
|
|
111
|
+
this.destroy = () => destroy && destroy();
|
|
112
|
+
|
|
113
|
+
let updateTimeout = null;
|
|
114
|
+
|
|
115
|
+
const update = () => {
|
|
116
|
+
clearTimeout(updateTimeout);
|
|
117
|
+
updateTimeout = setTimeout(() => {
|
|
118
|
+
const visibleSceneModels = Object.values(scene.models).filter(sceneModel => (sceneModel.id in modelCaches) && sceneModel.visible);
|
|
119
|
+
sectionPlanes.forEach((plane) => {
|
|
120
|
+
if (plane.active) {
|
|
121
|
+
const sliceMesh = math.makeSectionPlaneSlicer(plane.pos, plane.quaternion);
|
|
122
|
+
visibleSceneModels.forEach(sceneModel => {
|
|
123
|
+
const modelAABB = sceneModel.aabb;
|
|
124
|
+
if (math.planeIntersectsAABB3(plane, modelAABB)) {
|
|
125
|
+
// modelCenter is critical to use when handling models with large coordinates.
|
|
126
|
+
// See XCD-306 and examples/slicing/SectionCaps_at_distance.html for more details.
|
|
127
|
+
const modelCenter = math.getAABB3Center(modelAABB, math.vec3());
|
|
128
|
+
|
|
129
|
+
modelCaches[sceneModel.id].entityCaches.forEach((entityCache, entityId) => {
|
|
130
|
+
const entity = sceneModel.objects[entityId];
|
|
131
|
+
if (entityCache.generateCaps && entity.capMaterial && math.planeIntersectsAABB3(plane, entity.aabb)) {
|
|
132
|
+
entityCache.meshCaches ||= entity.meshes.filter(mesh => mesh.isSolid()).map(mesh => {
|
|
133
|
+
const meshIndices = [ ];
|
|
134
|
+
const meshVertices = [ ];
|
|
135
|
+
mesh.getEachIndex(i => meshIndices.push(i));
|
|
136
|
+
mesh.getEachVertex(v => meshVertices.push(...math.subVec3(v, modelCenter, tempVec3a)));
|
|
137
|
+
return { mesh: mesh, meshIndices: meshIndices, meshVertices: meshVertices };
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
entityCache.meshCaches.filter(meshCache => math.planeIntersectsAABB3(plane, meshCache.mesh.aabb)).forEach((meshCache, meshIdx) => {
|
|
141
|
+
sliceMesh(modelCenter, meshCache.meshIndices, meshCache.meshVertices).forEach((geo, geoIdx) => {
|
|
142
|
+
entityCache.capMeshes.push(new Mesh(scene, {
|
|
143
|
+
isObject: true,
|
|
144
|
+
id: `${plane.id}-${entityId}-${meshIdx}-${geoIdx}`,
|
|
145
|
+
material: entity.capMaterial,
|
|
146
|
+
origin: math.addVec3(modelCenter, math.mulVec3Scalar(plane.dir, 0.001, tempVec3a), tempVec3a),
|
|
147
|
+
geometry: new ReadableGeometry(scene, {
|
|
148
|
+
primitive: "triangles",
|
|
149
|
+
indices: geo.indices,
|
|
150
|
+
positions: geo.positions,
|
|
151
|
+
normals: geo.normals,
|
|
152
|
+
uv: geo.uv
|
|
153
|
+
})
|
|
154
|
+
}));
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
332
159
|
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
visibleSceneModels.forEach(sceneModel => modelCaches[sceneModel.id].entityCaches.forEach(entityCache => entityCache.generateCaps = false));
|
|
164
|
+
}, 100);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
this._onCapMaterialUpdated = (entity) => {
|
|
168
|
+
if (! destroy) {
|
|
169
|
+
updateTimeout = null;
|
|
170
|
+
|
|
171
|
+
const handleSectionPlane = (sectionPlane) => {
|
|
172
|
+
const onSectionPlaneUpdated = () => {
|
|
173
|
+
Object.values(modelCaches).forEach(modelCache => modelCache.entityCaches.forEach(entityCache => {
|
|
174
|
+
entityCache.destroyCaps();
|
|
175
|
+
entityCache.generateCaps = true;
|
|
176
|
+
}));
|
|
177
|
+
update();
|
|
178
|
+
};
|
|
179
|
+
sectionPlanes.push(sectionPlane);
|
|
180
|
+
sectionPlane.on('pos', onSectionPlaneUpdated);
|
|
181
|
+
sectionPlane.on('dir', onSectionPlaneUpdated);
|
|
182
|
+
sectionPlane.on('active', onSectionPlaneUpdated);
|
|
183
|
+
sectionPlane.once('destroyed', () => {
|
|
184
|
+
const idx = sectionPlanes.indexOf(sectionPlane);
|
|
185
|
+
if (idx >= 0) {
|
|
186
|
+
sectionPlanes.splice(idx, 1);
|
|
187
|
+
onSectionPlaneUpdated();
|
|
333
188
|
}
|
|
189
|
+
});
|
|
190
|
+
};
|
|
334
191
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
orderedSegments.get(segmentedId).push([
|
|
339
|
-
unsortedSegments.get(segmentedId)[0]
|
|
340
|
-
]);
|
|
341
|
-
unsortedSegment.splice(0, 1);
|
|
342
|
-
index++;
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
}
|
|
192
|
+
for (const key in scene.sectionPlanes){
|
|
193
|
+
handleSectionPlane(scene.sectionPlanes[key]);
|
|
194
|
+
}
|
|
348
195
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
196
|
+
const onSectionPlaneCreated = scene.on('sectionPlaneCreated', handleSectionPlane);
|
|
197
|
+
|
|
198
|
+
const onTick = scene.on("tick", () => {
|
|
199
|
+
//on ticks we only check if there is a model that we have saved vertices for,
|
|
200
|
+
//but it's no more available on the scene, or if its visibility changed
|
|
201
|
+
let doUpdate = false;
|
|
202
|
+
for (const sceneModelId in modelCaches) {
|
|
203
|
+
const sceneModel = scene.models[sceneModelId];
|
|
204
|
+
if (! sceneModel) {
|
|
205
|
+
modelCaches[sceneModelId].entityCaches.forEach(entityCache => entityCache.destroyCaps());
|
|
206
|
+
delete modelCaches[sceneModelId];
|
|
207
|
+
doUpdate = true;
|
|
208
|
+
} else if (modelCaches[sceneModelId].modelVisible != sceneModel.visible) {
|
|
209
|
+
const modelCache = modelCaches[sceneModelId];
|
|
210
|
+
modelCache.modelVisible = !!sceneModel.visible;
|
|
211
|
+
modelCache.entityCaches.forEach(entityCache => {
|
|
212
|
+
entityCache.destroyCaps();
|
|
213
|
+
entityCache.generateCaps = true;
|
|
214
|
+
});
|
|
215
|
+
doUpdate = true;
|
|
352
216
|
}
|
|
353
217
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
//#region projecting the segments to 2D
|
|
358
|
-
const projectedSegments = new Map();
|
|
359
|
-
orderedSegments.forEach((orderedSegment, key) => {
|
|
360
|
-
const arr = [];
|
|
361
|
-
for (let i = 0; i < orderedSegment.length; i++) {
|
|
362
|
-
arr.push([]);
|
|
363
|
-
orderedSegment[i].forEach((segment) => {
|
|
364
|
-
arr[i].push([
|
|
365
|
-
this._projectTo2D(segment[0], plane.dir),
|
|
366
|
-
this._projectTo2D(segment[1], plane.dir)
|
|
367
|
-
])
|
|
368
|
-
})
|
|
218
|
+
if (doUpdate) {
|
|
219
|
+
update();
|
|
369
220
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
//#region creating caps using earcut and then projecting them back to 3D
|
|
375
|
-
const caps = new Map();
|
|
376
|
-
let arr;
|
|
377
|
-
projectedSegments.forEach((segment, segmentId) => {
|
|
378
|
-
const modelOrigin = this._sceneModelsData[sceneModel.id].modelOrigin;
|
|
379
|
-
arr = [];
|
|
380
|
-
const loops = segment;
|
|
381
|
-
|
|
382
|
-
// Group related loops (outer boundaries with their holes)
|
|
383
|
-
|
|
384
|
-
const groupedLoops = [];
|
|
385
|
-
const used = new Set();
|
|
386
|
-
|
|
387
|
-
for (let i = 0; i < loops.length; i++) {
|
|
388
|
-
if (used.has(i)) continue;
|
|
389
|
-
|
|
390
|
-
const group = [loops[i]];
|
|
391
|
-
used.add(i);
|
|
392
|
-
|
|
393
|
-
// Check remaining loops
|
|
394
|
-
for (let j = i + 1; j < loops.length; j++) {
|
|
395
|
-
if (used.has(j)) continue;
|
|
396
|
-
|
|
397
|
-
if (this._isLoopInside(loops[i], loops[j]) ||
|
|
398
|
-
this._isLoopInside(loops[j], loops[i])) {
|
|
399
|
-
group.push(loops[j]);
|
|
400
|
-
used.add(j);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
groupedLoops.push(group);
|
|
221
|
+
});
|
|
222
|
+
destroy = () => {
|
|
223
|
+
for (const sceneModelId in modelCaches) {
|
|
224
|
+
modelCaches[sceneModelId].entityCaches.forEach(entityCache => entityCache.destroyCaps());
|
|
405
225
|
}
|
|
226
|
+
scene.off(onSectionPlaneCreated);
|
|
227
|
+
scene.off(onTick);
|
|
228
|
+
};
|
|
229
|
+
}
|
|
406
230
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
area -= loop[j][0][0] * loop[i][0][1];
|
|
421
|
-
}
|
|
422
|
-
return Math.abs(area) / 2;
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
// Find index of the loop with maximum area
|
|
426
|
-
const outerLoopIndex = areas.indexOf(Math.max(...areas));
|
|
427
|
-
|
|
428
|
-
// Add the outer boundary first
|
|
429
|
-
group[outerLoopIndex].forEach(segment => {
|
|
430
|
-
vertices.push(segment[0][0], segment[0][1]);
|
|
431
|
-
currentIndex += 2;
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
// Then add all other loops as holes
|
|
435
|
-
for (let i = 0; i < group.length; i++) {
|
|
436
|
-
if (i !== outerLoopIndex) {
|
|
437
|
-
// Store the starting vertex index for this hole
|
|
438
|
-
holes.push(currentIndex / 2);
|
|
439
|
-
|
|
440
|
-
group[i].forEach(segment => {
|
|
441
|
-
vertices.push(segment[0][0], segment[0][1]);
|
|
442
|
-
currentIndex += 2;
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Triangulate using earcut
|
|
448
|
-
const triangles = earcut(vertices, holes);
|
|
449
|
-
|
|
450
|
-
// // Convert triangulated 2D points back to 3D
|
|
451
|
-
const cap3D = [];
|
|
452
|
-
|
|
453
|
-
// Process each triangle
|
|
454
|
-
for (let i = 0; i < triangles.length; i += 3) {
|
|
455
|
-
const triangle = [];
|
|
456
|
-
|
|
457
|
-
// Convert each vertex
|
|
458
|
-
for (let j = 0; j < 3; j++) {
|
|
459
|
-
const idx = triangles[i + j] * 2;
|
|
460
|
-
const point2D = [vertices[idx], vertices[idx + 1]];
|
|
461
|
-
const point3D = this._convertTo3D(point2D, plane, modelOrigin);
|
|
462
|
-
triangle.push(point3D);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
cap3D.push(triangle);
|
|
466
|
-
}
|
|
467
|
-
arr.push(cap3D);
|
|
468
|
-
});
|
|
469
|
-
caps.set(segmentId, arr);
|
|
470
|
-
})
|
|
471
|
-
//#endregion
|
|
472
|
-
|
|
473
|
-
//#region converting caps to geometry
|
|
474
|
-
const geometryData = new Map();
|
|
475
|
-
|
|
476
|
-
caps.forEach((cap, capId) => {
|
|
477
|
-
arr = [];
|
|
478
|
-
cap.forEach(capTriangles => {
|
|
479
|
-
// Create a vertex map to reuse vertices
|
|
480
|
-
const vertexMap = new Map();
|
|
481
|
-
const vertices = [];
|
|
482
|
-
const indices = [];
|
|
483
|
-
let currentIndex = 0;
|
|
484
|
-
|
|
485
|
-
capTriangles.forEach(triangle => {
|
|
486
|
-
const triangleIndices = [];
|
|
487
|
-
|
|
488
|
-
// Process each vertex of the triangle
|
|
489
|
-
triangle.forEach(vertex => {
|
|
490
|
-
// Create a key for the vertex to check for duplicates
|
|
491
|
-
const vertexKey = `${vertex[0].toFixed(6)},${vertex[1].toFixed(6)},${vertex[2].toFixed(6)}`;
|
|
492
|
-
|
|
493
|
-
if (vertexMap.has(vertexKey)) {
|
|
494
|
-
// Reuse existing vertex
|
|
495
|
-
triangleIndices.push(vertexMap.get(vertexKey));
|
|
496
|
-
} else {
|
|
497
|
-
// Add new vertex
|
|
498
|
-
vertices.push(vertex[0], vertex[1], vertex[2]);
|
|
499
|
-
vertexMap.set(vertexKey, currentIndex);
|
|
500
|
-
triangleIndices.push(currentIndex);
|
|
501
|
-
currentIndex++;
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
// Add triangle indices
|
|
506
|
-
indices.push(...triangleIndices);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
arr.push({
|
|
510
|
-
positions: vertices,
|
|
511
|
-
indices: indices
|
|
512
|
-
});
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
geometryData.set(capId, arr);
|
|
516
|
-
})
|
|
517
|
-
//#endregion
|
|
518
|
-
|
|
519
|
-
//#region adding meshes to the scene
|
|
520
|
-
if(!this._prevIntersectionModelsMap[sceneModel.id])
|
|
521
|
-
this._prevIntersectionModelsMap[sceneModel.id] = new Map();
|
|
522
|
-
|
|
523
|
-
// Cache plane direction values
|
|
524
|
-
math.mulVec3Scalar(plane.dir, 0.001, planeOff); // Use dedicated planeOff, as tempVec* are overwritten by _createUVs
|
|
525
|
-
|
|
526
|
-
geometryData.forEach((geometries, objectId) => {
|
|
527
|
-
const modelOrigin = this._sceneModelsData[sceneModel.id].modelOrigin;
|
|
528
|
-
const meshArray = new Array(geometries.size); // Pre-allocate array with known size
|
|
529
|
-
let meshIndex = 0;
|
|
530
|
-
|
|
531
|
-
geometries.forEach((geometry, index) => {
|
|
532
|
-
const vertices = geometry.positions;
|
|
533
|
-
const indices = geometry.indices;
|
|
534
|
-
const verticesLength = vertices.length;
|
|
535
|
-
|
|
536
|
-
// Build normals and UVs in parallel if possible
|
|
537
|
-
const meshNormals = math.buildNormals(vertices, indices);
|
|
538
|
-
const uvs = this._createUVs(vertices, plane, modelOrigin);
|
|
539
|
-
|
|
540
|
-
// Create mesh with transformed vertices
|
|
541
|
-
meshArray[meshIndex++] = new Mesh(this.scene, {
|
|
542
|
-
id: `${plane.id}-${objectId}-${index}`,
|
|
543
|
-
geometry: new ReadableGeometry(this.scene, {
|
|
544
|
-
primitive: 'triangles',
|
|
545
|
-
positions: vertices, // Only copy what we need
|
|
546
|
-
indices,
|
|
547
|
-
normals: meshNormals,
|
|
548
|
-
uv: uvs
|
|
549
|
-
}),
|
|
550
|
-
origin: math.addVec3(modelOrigin, planeOff, tempVec3a),
|
|
551
|
-
position: [0, 0, 0],
|
|
552
|
-
rotation: [0, 0, 0],
|
|
553
|
-
material: sceneModel.objects[objectId].capMaterial
|
|
554
|
-
});
|
|
555
|
-
})
|
|
556
|
-
if(this._prevIntersectionModelsMap[sceneModel.id].has(objectId)) {
|
|
557
|
-
this._prevIntersectionModelsMap[sceneModel.id].get(objectId).push(...meshArray)
|
|
231
|
+
const model = entity.model;
|
|
232
|
+
const modelId = model.id;
|
|
233
|
+
modelCaches[modelId] ||= { modelVisible: model.visible, entityCaches: new Map() };
|
|
234
|
+
const entityCaches = modelCaches[modelId].entityCaches;
|
|
235
|
+
const entityId = entity.id;
|
|
236
|
+
if (! entityCaches.has(entityId)) {
|
|
237
|
+
const entityCache = {
|
|
238
|
+
capMeshes: [ ],
|
|
239
|
+
meshCaches: null,
|
|
240
|
+
generateCaps: false,
|
|
241
|
+
destroyCaps: () => {
|
|
242
|
+
entityCache.capMeshes.forEach(capMesh => capMesh.destroy());
|
|
243
|
+
entityCache.capMeshes.length = 0;
|
|
558
244
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
})
|
|
562
|
-
//#endregion
|
|
563
|
-
})
|
|
564
|
-
|
|
565
|
-
})
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
_doesPlaneIntersectBoundingBox(bb, plane) {
|
|
570
|
-
const min = [bb[0], bb[1], bb[2]];
|
|
571
|
-
const max = [bb[3], bb[4], bb[5]];
|
|
572
|
-
|
|
573
|
-
const corners = [
|
|
574
|
-
[min[0], min[1], min[2]], // 000
|
|
575
|
-
[max[0], min[1], min[2]], // 100
|
|
576
|
-
[min[0], max[1], min[2]], // 010
|
|
577
|
-
[max[0], max[1], min[2]], // 110
|
|
578
|
-
[min[0], min[1], max[2]], // 001
|
|
579
|
-
[max[0], min[1], max[2]], // 101
|
|
580
|
-
[min[0], max[1], max[2]], // 011
|
|
581
|
-
[max[0], max[1], max[2]] // 111
|
|
582
|
-
]
|
|
583
|
-
|
|
584
|
-
// Calculate distance from each corner to the plane
|
|
585
|
-
let hasPositive = false;
|
|
586
|
-
let hasNegative = false;
|
|
587
|
-
|
|
588
|
-
for (const corner of corners) {
|
|
589
|
-
const distance = plane.dist + math.dotVec3(plane.dir, corner);
|
|
590
|
-
|
|
591
|
-
if (distance > 0) hasPositive = true;
|
|
592
|
-
if (distance < 0) hasNegative = true;
|
|
593
|
-
|
|
594
|
-
// If we found points on both sides, the plane intersects the box
|
|
595
|
-
if (hasPositive && hasNegative) return true;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// If all points are on the same side, no intersection
|
|
599
|
-
return false;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
//not used but kept for debugging
|
|
603
|
-
_buildLines(sortedSegments) {
|
|
604
|
-
for (const key in sortedSegments) {
|
|
605
|
-
for (let i = 0; i < sortedSegments[key].length; i++) {
|
|
606
|
-
const segments = sortedSegments[key][i];
|
|
607
|
-
if (segments.length <= 0) continue;
|
|
608
|
-
segments.forEach((segment, index) => {
|
|
609
|
-
new Mesh(this.scene, {
|
|
610
|
-
clippable: false,
|
|
611
|
-
geometry: new ReadableGeometry(this.scene, buildLineGeometry({
|
|
612
|
-
startPoint: segment[0],
|
|
613
|
-
endPoint: segment[1],
|
|
614
|
-
})),
|
|
615
|
-
material: new PhongMaterial(this.scene, {
|
|
616
|
-
emissive: [1, 0, 0]
|
|
617
|
-
})
|
|
618
|
-
});
|
|
619
|
-
})
|
|
245
|
+
};
|
|
246
|
+
entityCaches.set(entityId, entityCache);
|
|
620
247
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
let u;
|
|
627
|
-
if (Math.abs(normal[0]) > Math.abs(normal[1]))
|
|
628
|
-
u = [-normal[2], 0, normal[0]];
|
|
629
|
-
else
|
|
630
|
-
u = [0, normal[2], -normal[1]];
|
|
631
|
-
|
|
632
|
-
u = math.normalizeVec3(u);
|
|
633
|
-
const normalTemp = math.vec3(normal);
|
|
634
|
-
const cross = math.cross3Vec3(normalTemp, u)
|
|
635
|
-
const v = math.normalizeVec3(cross);
|
|
636
|
-
const x = math.dotVec3(point, u);
|
|
637
|
-
const y = math.dotVec3(point, v);
|
|
638
|
-
|
|
639
|
-
return [x, y]
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
_isLoopInside(loop1, loop2) {
|
|
644
|
-
// Simple point-in-polygon test using the first point of loop1
|
|
645
|
-
const point = loop1[0][0]; // First point of first segment
|
|
646
|
-
let inside = false;
|
|
647
|
-
for (let i = 0, j = loop2.length - 1; i < loop2.length; j = i++) {
|
|
648
|
-
const xi = loop2[i][0][0], yi = loop2[i][0][1];
|
|
649
|
-
const xj = loop2[j][0][0], yj = loop2[j][0][1];
|
|
650
|
-
|
|
651
|
-
const intersect = ((yi > point[1]) !== (yj > point[1]))
|
|
652
|
-
&& (point[0] < (xj - xi) * (point[1] - yi) / (yj - yi) + xi);
|
|
653
|
-
|
|
654
|
-
if (intersect) inside = !inside;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
return inside;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
_convertTo3D(point2D, plane, origin) {
|
|
661
|
-
// Reconstruct the same basis vectors used in _projectTo2D
|
|
662
|
-
let u, normal = plane.dir, planePosition = plane.pos;
|
|
663
|
-
if (Math.abs(normal[0]) > Math.abs(normal[1])) {
|
|
664
|
-
u = [-normal[2], 0, normal[0]];
|
|
665
|
-
} else {
|
|
666
|
-
u = [0, normal[2], -normal[1]];
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
u = math.normalizeVec3(u);
|
|
670
|
-
const normalTemp = math.vec3(normal);
|
|
671
|
-
const cross = math.cross3Vec3(normalTemp, u);
|
|
672
|
-
const v = math.normalizeVec3(cross);
|
|
673
|
-
|
|
674
|
-
// Reconstruct 3D point using the basis vectors
|
|
675
|
-
const x = point2D[0];
|
|
676
|
-
const y = point2D[1];
|
|
677
|
-
const result = [
|
|
678
|
-
u[0] * x + v[0] * y,
|
|
679
|
-
u[1] * x + v[1] * y,
|
|
680
|
-
u[2] * x + v[2] * y
|
|
681
|
-
];
|
|
682
|
-
|
|
683
|
-
// Project the point onto the cutting plane
|
|
684
|
-
|
|
685
|
-
const t = math.dotVec3(normal, [
|
|
686
|
-
planePosition[0] - result[0] - origin[0],
|
|
687
|
-
planePosition[1] - result[1] - origin[1],
|
|
688
|
-
planePosition[2] - result[2] - origin[2]
|
|
689
|
-
]);
|
|
690
|
-
|
|
691
|
-
return [
|
|
692
|
-
result[0] + normal[0] * t,
|
|
693
|
-
result[1] + normal[1] * t,
|
|
694
|
-
result[2] + normal[2] * t
|
|
695
|
-
];
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
_deletePreviousModels() {
|
|
699
|
-
|
|
700
|
-
for(const sceneModelId in this._prevIntersectionModelsMap) {
|
|
701
|
-
const objects = this._prevIntersectionModelsMap[sceneModelId];
|
|
702
|
-
objects.forEach((value, objectId) => {
|
|
703
|
-
if(this._dirtyMap[sceneModelId].get(objectId)) {
|
|
704
|
-
value.forEach((mesh) => {
|
|
705
|
-
mesh.destroy();
|
|
706
|
-
})
|
|
707
|
-
this._prevIntersectionModelsMap[sceneModelId].delete(objectId);
|
|
708
|
-
}
|
|
709
|
-
})
|
|
710
|
-
if(this._prevIntersectionModelsMap[sceneModelId].size <= 0)
|
|
711
|
-
delete this._prevIntersectionModelsMap[sceneModelId];
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
_createUVs(vertices, plane, origin) {
|
|
718
|
-
const O = plane.pos;
|
|
719
|
-
const D = tempVec3a;
|
|
720
|
-
D.set(plane.dir);
|
|
721
|
-
math.normalizeVec3(D);
|
|
722
|
-
const P = tempVec3b;
|
|
723
|
-
|
|
724
|
-
const uvs = [ ];
|
|
725
|
-
for (let i = 0; i < vertices.length; i += 3) {
|
|
726
|
-
P[0] = vertices[i] + origin[0];
|
|
727
|
-
P[1] = vertices[i + 1] + origin[1];
|
|
728
|
-
P[2] = vertices[i + 2] + origin[2];
|
|
729
|
-
|
|
730
|
-
// Project P onto the plane
|
|
731
|
-
const OP = math.subVec3(P, O, tempVec3c);
|
|
732
|
-
const dist = math.dotVec3(OP, D);
|
|
733
|
-
math.subVec3(P, math.mulVec3Scalar(D, dist, tempVec3c), P);
|
|
734
|
-
|
|
735
|
-
const right = ((Math.abs(math.dotVec3(D, worldUp)) < 0.999)
|
|
736
|
-
? math.cross3Vec3(D, worldUp, tempVec3c)
|
|
737
|
-
: worldRight);
|
|
738
|
-
const v = math.cross3Vec3(D, right, tempVec3c);
|
|
739
|
-
math.normalizeVec3(v, v);
|
|
740
|
-
|
|
741
|
-
const OP_proj = math.subVec3(P, O, P);
|
|
742
|
-
uvs.push(
|
|
743
|
-
math.dotVec3(OP_proj, math.normalizeVec3(math.cross3Vec3(v, D, tempVec3d))),
|
|
744
|
-
math.dotVec3(OP_proj, v));
|
|
745
|
-
}
|
|
746
|
-
return uvs;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
destroy() {
|
|
750
|
-
this._deletePreviousModels();
|
|
751
|
-
if(this._resourcesAllocated) {
|
|
752
|
-
this.scene.off(this._onModelLoaded);
|
|
753
|
-
this.scene.off(this._onModelUnloaded);
|
|
754
|
-
this.scene.off(this._onSectionPlaneCreated);
|
|
755
|
-
this.scene.off(this._onTick);
|
|
756
|
-
}
|
|
757
|
-
|
|
248
|
+
const entityCache = entityCaches.get(entityId);
|
|
249
|
+
entityCache.destroyCaps();
|
|
250
|
+
entityCache.generateCaps = true;
|
|
251
|
+
update();
|
|
252
|
+
};
|
|
758
253
|
}
|
|
759
254
|
}
|
|
760
|
-
|
|
761
|
-
export { SectionCaps };
|
|
255
|
+
export { SectionCaps };
|