@adatechnology/game-destruction-three 0.7.3 → 0.8.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/README.md +10 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.global.js +634 -152
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +366 -56
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
|
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
|
|
5
5
|
// src/renderer.ts
|
|
6
|
+
import { blastShove } from "@adatechnology/game-contracts";
|
|
6
7
|
import { createVfx, effectForEvent, EVENT_EFFECTS_MAP } from "@adatechnology/game-vfx";
|
|
7
8
|
import { createVfxRenderer } from "@adatechnology/game-vfx-three";
|
|
8
9
|
|
|
@@ -135,6 +136,7 @@ var FragmentSystem = class {
|
|
|
135
136
|
const sc = new this.THREE.Vector3(Math.max(5e-3, sz[0]), Math.max(5e-3, sz[1]), Math.max(3e-3, sz[2]));
|
|
136
137
|
mesh.scale.copy(sc);
|
|
137
138
|
mesh.castShadow = true;
|
|
139
|
+
mesh.userData.destructionDebris = true;
|
|
138
140
|
this.o.scene.add(mesh);
|
|
139
141
|
const live = {
|
|
140
142
|
mesh,
|
|
@@ -342,6 +344,7 @@ var GroundSpots = class {
|
|
|
342
344
|
mesh.position.set(x, y, z);
|
|
343
345
|
mesh.scale.setScalar(1e-3);
|
|
344
346
|
mesh.renderOrder = 1;
|
|
347
|
+
mesh.userData.destructionDebris = true;
|
|
345
348
|
this.scene.add(mesh);
|
|
346
349
|
this.spots.push({ mesh, kind, home, x, z, radius: 1e-3, target: Math.min(MAX_RADIUS, radius) });
|
|
347
350
|
}
|
|
@@ -770,7 +773,12 @@ var RebarCages = class {
|
|
|
770
773
|
};
|
|
771
774
|
|
|
772
775
|
// src/spec.ts
|
|
773
|
-
import { guessMaterial, inferJoints } from "@adatechnology/game-destruction";
|
|
776
|
+
import { guessMaterial, inferJoints, MATERIALS, partMass } from "@adatechnology/game-destruction";
|
|
777
|
+
function containsOf(value) {
|
|
778
|
+
if (!value || typeof value !== "object") return void 0;
|
|
779
|
+
const v = value;
|
|
780
|
+
return typeof v.fluid === "string" && typeof v.liters === "number" && v.liters > 0 ? { fluid: v.fluid, liters: v.liters } : void 0;
|
|
781
|
+
}
|
|
774
782
|
function partHintOf(value) {
|
|
775
783
|
if (typeof value === "string") return value ? { id: value } : null;
|
|
776
784
|
if (!value || typeof value !== "object") return null;
|
|
@@ -791,11 +799,17 @@ function isMesh(o) {
|
|
|
791
799
|
function sizeOf(box) {
|
|
792
800
|
return [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
|
|
793
801
|
}
|
|
802
|
+
function specFrameInverse(THREE, root) {
|
|
803
|
+
root.updateMatrixWorld(true);
|
|
804
|
+
const position = new THREE.Vector3();
|
|
805
|
+
const quaternion = new THREE.Quaternion();
|
|
806
|
+
root.matrixWorld.decompose(position, quaternion, new THREE.Vector3());
|
|
807
|
+
return new THREE.Matrix4().compose(position, quaternion, new THREE.Vector3(1, 1, 1)).invert();
|
|
808
|
+
}
|
|
794
809
|
function specFromObject3D(THREE, root, options = {}) {
|
|
795
810
|
const fallback = options.fallback ?? "plastic";
|
|
796
811
|
const minSize = options.minSize ?? 0.015;
|
|
797
|
-
root
|
|
798
|
-
const inv = root.matrixWorld.clone().invert();
|
|
812
|
+
const inv = specFrameInverse(THREE, root);
|
|
799
813
|
const entries = [];
|
|
800
814
|
const byKey = /* @__PURE__ */ new Map();
|
|
801
815
|
const visit = (o) => {
|
|
@@ -812,8 +826,10 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
812
826
|
const material = options.materialOf?.(o) ?? (typeof ud.destructionMaterial === "string" ? ud.destructionMaterial : void 0) ?? hint?.material ?? guessMaterial(`${o.name} ${matName} ${typeof ud.material === "string" ? ud.material : ""}`) ?? fallback;
|
|
813
827
|
const explicit = hint?.id ?? null;
|
|
814
828
|
const hollow = ud.hollow === true || /hollow/i.test(o.name);
|
|
829
|
+
const skin = ud.destructionSkin === true;
|
|
815
830
|
const fill = options.fillOf?.(o, material);
|
|
816
831
|
const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
|
|
832
|
+
const contains = containsOf(ud.destructionContains);
|
|
817
833
|
const existing = explicit ? byKey.get(explicit) : void 0;
|
|
818
834
|
if (existing) {
|
|
819
835
|
const before = sizeOf(existing.box);
|
|
@@ -823,9 +839,11 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
823
839
|
existing.meshes.push(o);
|
|
824
840
|
if (ns[0] * ns[1] * ns[2] > bv) existing.material = material;
|
|
825
841
|
existing.hollow = existing.hollow || hollow;
|
|
842
|
+
existing.skin = existing.skin && skin;
|
|
826
843
|
if (fill !== void 0) existing.fill = fill;
|
|
844
|
+
if (contains) existing.contains = contains;
|
|
827
845
|
} else {
|
|
828
|
-
const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, tags, fixedId: !!explicit, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
|
|
846
|
+
const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
|
|
829
847
|
entries.push(e);
|
|
830
848
|
if (explicit) byKey.set(explicit, e);
|
|
831
849
|
}
|
|
@@ -844,7 +862,7 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
844
862
|
const largest = (e) => Math.max(...sizeOf(e.box));
|
|
845
863
|
const kept = [];
|
|
846
864
|
const tiny = [];
|
|
847
|
-
for (const e of entries) (!e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
|
|
865
|
+
for (const e of entries) (e.skin || !e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
|
|
848
866
|
for (const t of tiny) {
|
|
849
867
|
let best = null;
|
|
850
868
|
let bestV = 0;
|
|
@@ -898,11 +916,21 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
898
916
|
part.thickness = Math.max(minSide, 5e-4);
|
|
899
917
|
}
|
|
900
918
|
if (e.tags) part.tags = [...e.tags];
|
|
919
|
+
if (e.contains) part.contains = { ...e.contains };
|
|
901
920
|
const ao = options.anchorBelow ?? -1;
|
|
902
921
|
if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
|
|
903
922
|
parts.push(part);
|
|
904
923
|
meshesOf[id] = e.meshes;
|
|
905
924
|
}
|
|
925
|
+
const knownMass = root.userData?.destructionMass;
|
|
926
|
+
if (typeof knownMass === "number" && knownMass > 0) {
|
|
927
|
+
const shares = parts.map((p) => {
|
|
928
|
+
const material = MATERIALS.find((m) => m.id === p.material);
|
|
929
|
+
return material ? partMass(p, material) : 0;
|
|
930
|
+
});
|
|
931
|
+
const total = shares.reduce((a, b) => a + b, 0);
|
|
932
|
+
if (total > 0) parts.forEach((p, i) => p.mass = knownMass * shares[i] / total);
|
|
933
|
+
}
|
|
906
934
|
const spec = { parts };
|
|
907
935
|
if (options.name !== void 0) spec.name = options.name;
|
|
908
936
|
else if (root.name) spec.name = root.name;
|
|
@@ -954,6 +982,46 @@ function heatColor(temperatureC) {
|
|
|
954
982
|
return last[1];
|
|
955
983
|
}
|
|
956
984
|
|
|
985
|
+
// src/skin-dent.ts
|
|
986
|
+
function shellDisplacementAt(THREE, worldPoint, shells, out) {
|
|
987
|
+
const triangle = new THREE.Triangle();
|
|
988
|
+
const corners = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()];
|
|
989
|
+
const local = new THREE.Vector3();
|
|
990
|
+
const closest = new THREE.Vector3();
|
|
991
|
+
const weights = new THREE.Vector3();
|
|
992
|
+
const moved = new THREE.Vector3();
|
|
993
|
+
let nearest = Infinity;
|
|
994
|
+
out.set(0, 0, 0);
|
|
995
|
+
let nearestScale = 1;
|
|
996
|
+
for (const shell of shells) {
|
|
997
|
+
shell.mesh.updateWorldMatrix(true, false);
|
|
998
|
+
shell.mesh.worldToLocal(local.copy(worldPoint));
|
|
999
|
+
const index = shell.mesh.geometry.index;
|
|
1000
|
+
const triangles = index ? index.count / 3 : shell.before.length / 9;
|
|
1001
|
+
for (let t = 0; t < triangles; t++) {
|
|
1002
|
+
const ids = [0, 1, 2].map((corner) => index ? index.getX(t * 3 + corner) : t * 3 + corner);
|
|
1003
|
+
ids.forEach((id, corner) => corners[corner].set(shell.before[id * 3], shell.before[id * 3 + 1], shell.before[id * 3 + 2]));
|
|
1004
|
+
triangle.set(corners[0], corners[1], corners[2]);
|
|
1005
|
+
triangle.closestPointToPoint(local, closest);
|
|
1006
|
+
const distance = closest.distanceToSquared(local);
|
|
1007
|
+
if (distance >= nearest) continue;
|
|
1008
|
+
nearest = distance;
|
|
1009
|
+
nearestScale = shell.mesh.matrixWorld.getMaxScaleOnAxis() || 1;
|
|
1010
|
+
triangle.getBarycoord(closest, weights);
|
|
1011
|
+
moved.set(0, 0, 0);
|
|
1012
|
+
ids.forEach((id, corner) => {
|
|
1013
|
+
const weight = corner === 0 ? weights.x : corner === 1 ? weights.y : weights.z;
|
|
1014
|
+
moved.x += weight * (shell.after[id * 3] - shell.before[id * 3]);
|
|
1015
|
+
moved.y += weight * (shell.after[id * 3 + 1] - shell.before[id * 3 + 1]);
|
|
1016
|
+
moved.z += weight * (shell.after[id * 3 + 2] - shell.before[id * 3 + 2]);
|
|
1017
|
+
});
|
|
1018
|
+
out.copy(moved).transformDirection(shell.mesh.matrixWorld);
|
|
1019
|
+
out.multiplyScalar(moved.length() * (shell.mesh.matrixWorld.getMaxScaleOnAxis() || 1));
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return Math.sqrt(nearest) * nearestScale;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
957
1025
|
// src/visual.ts
|
|
958
1026
|
var POS_ATTR = "position";
|
|
959
1027
|
function positionAttr(geo) {
|
|
@@ -986,7 +1054,8 @@ var DecalKit = class {
|
|
|
986
1054
|
depthWrite: false,
|
|
987
1055
|
polygonOffset: true,
|
|
988
1056
|
polygonOffsetFactor: -4,
|
|
989
|
-
polygonOffsetUnits: -4
|
|
1057
|
+
polygonOffsetUnits: -4,
|
|
1058
|
+
side: this.THREE.DoubleSide
|
|
990
1059
|
}));
|
|
991
1060
|
}
|
|
992
1061
|
/** Opaque patch in the colour of what shows under a spalled coat. */
|
|
@@ -1018,6 +1087,12 @@ var DecalKit = class {
|
|
|
1018
1087
|
}
|
|
1019
1088
|
};
|
|
1020
1089
|
var tmpV = (THREE) => new THREE.Vector3();
|
|
1090
|
+
var MIN_DENT_VERTICES = 10;
|
|
1091
|
+
var WIDEN_MARGIN = 1.1;
|
|
1092
|
+
var SKIN_FOLLOW = 0.85;
|
|
1093
|
+
var SKIN_SLACK = 5e-4;
|
|
1094
|
+
var SKIN_LAYER_SLACK = 4;
|
|
1095
|
+
var NEAREST_MARGIN = 1.5;
|
|
1021
1096
|
var Visuals = class {
|
|
1022
1097
|
constructor(THREE, lowQuality, decalCap) {
|
|
1023
1098
|
__publicField(this, "THREE", THREE);
|
|
@@ -1186,14 +1261,14 @@ var Visuals = class {
|
|
|
1186
1261
|
* Push the vertices of the mesh within `dent.radius` of `worldPoint` inwards along `worldNormal` by `depth * (1 - (d/r)^2)^2`.
|
|
1187
1262
|
* Returns false when nothing was changed (too far, low quality on a heavy mesh, or no editable positions).
|
|
1188
1263
|
*/
|
|
1189
|
-
applyDent(mv, worldPoint, worldNormal, dent) {
|
|
1264
|
+
applyDent(mv, worldPoint, worldNormal, dent, reachWorld = 0) {
|
|
1190
1265
|
const mesh = mv.mesh;
|
|
1191
1266
|
if (this.lowQuality && this.vertexCount(mv) > 2e4) return false;
|
|
1192
1267
|
mesh.updateWorldMatrix(true, false);
|
|
1193
1268
|
const s = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
|
|
1194
1269
|
const lp = mesh.worldToLocal(this.a.copy(worldPoint));
|
|
1195
1270
|
const ln = this.b.copy(worldNormal).transformDirection(this.m4.copy(mesh.matrixWorld).invert()).normalize();
|
|
1196
|
-
const r = dent.radius / s;
|
|
1271
|
+
const r = Math.max(dent.radius, reachWorld) / s;
|
|
1197
1272
|
const depth = dent.depth / s;
|
|
1198
1273
|
const geo = mesh.geometry;
|
|
1199
1274
|
let bb;
|
|
@@ -1206,34 +1281,110 @@ var Visuals = class {
|
|
|
1206
1281
|
if (!this.ownGeometry(mv)) return false;
|
|
1207
1282
|
const work = mv.host ? mv.host.work : mv.work;
|
|
1208
1283
|
const pos = positionAttr(mesh.geometry);
|
|
1209
|
-
const r2 = r * r;
|
|
1210
|
-
let touched = false;
|
|
1211
|
-
const px = lp.x;
|
|
1212
|
-
const py = lp.y;
|
|
1213
|
-
const pz = lp.z;
|
|
1214
|
-
const nx = ln.x;
|
|
1215
|
-
const ny = ln.y;
|
|
1216
|
-
const nz = ln.z;
|
|
1217
1284
|
const from = mv.range ? mv.range[0] * 3 : 0;
|
|
1218
1285
|
const to = mv.range ? mv.range[1] * 3 : work.length;
|
|
1286
|
+
const reach = r;
|
|
1287
|
+
const reach2 = reach * reach;
|
|
1219
1288
|
for (let i = from; i < to; i += 3) {
|
|
1220
|
-
const dx = work[i] -
|
|
1221
|
-
const dy = work[i + 1] -
|
|
1222
|
-
const dz = work[i + 2] -
|
|
1289
|
+
const dx = work[i] - lp.x;
|
|
1290
|
+
const dy = work[i + 1] - lp.y;
|
|
1291
|
+
const dz = work[i + 2] - lp.z;
|
|
1223
1292
|
const d2 = dx * dx + dy * dy + dz * dz;
|
|
1224
|
-
if (d2 >=
|
|
1225
|
-
const f = 1 - d2 /
|
|
1293
|
+
if (d2 >= reach2) continue;
|
|
1294
|
+
const f = 1 - d2 / reach2;
|
|
1226
1295
|
const k = depth * f * f;
|
|
1227
|
-
work[i] -=
|
|
1228
|
-
work[i + 1] -=
|
|
1229
|
-
work[i + 2] -=
|
|
1230
|
-
touched = true;
|
|
1296
|
+
work[i] -= ln.x * k;
|
|
1297
|
+
work[i + 1] -= ln.y * k;
|
|
1298
|
+
work[i + 2] -= ln.z * k;
|
|
1231
1299
|
}
|
|
1232
|
-
if (!touched) return false;
|
|
1233
1300
|
mv.touched = true;
|
|
1234
1301
|
this.writePositions(mv, pos);
|
|
1235
1302
|
return true;
|
|
1236
1303
|
}
|
|
1304
|
+
/**
|
|
1305
|
+
* World radius a dent needs on this mesh to reach `MIN_DENT_VERTICES` vertices (the dent's own radius when the mesh is fine enough). A part with several meshes
|
|
1306
|
+
* (a shell and its label) must dent them all with the largest of these, so the skin and the shell follow the same smooth surface and never cross.
|
|
1307
|
+
*/
|
|
1308
|
+
dentReach(mv, worldPoint, dent) {
|
|
1309
|
+
const mesh = mv.mesh;
|
|
1310
|
+
mesh.updateWorldMatrix(true, false);
|
|
1311
|
+
const s = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
|
|
1312
|
+
const lp = mesh.worldToLocal(this.a.copy(worldPoint));
|
|
1313
|
+
const source = (mv.host ? mv.host.work : mv.work) ?? positionAttr(mesh.geometry)?.array;
|
|
1314
|
+
if (!source) return dent.radius;
|
|
1315
|
+
let bounds;
|
|
1316
|
+
if (mv.host) bounds = this.localBox(mv, this.lbox);
|
|
1317
|
+
else {
|
|
1318
|
+
if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox();
|
|
1319
|
+
bounds = mesh.geometry.boundingBox;
|
|
1320
|
+
}
|
|
1321
|
+
if (bounds && !bounds.isEmpty() && bounds.distanceToPoint(lp) > dent.radius / s) return dent.radius;
|
|
1322
|
+
const from = mv.range ? mv.range[0] * 3 : 0;
|
|
1323
|
+
const to = mv.range ? mv.range[1] * 3 : source.length;
|
|
1324
|
+
const r2 = (dent.radius / s) ** 2;
|
|
1325
|
+
const distances = [];
|
|
1326
|
+
let inside = 0;
|
|
1327
|
+
for (let i = from; i < to; i += 3) {
|
|
1328
|
+
const dx = source[i] - lp.x;
|
|
1329
|
+
const dy = source[i + 1] - lp.y;
|
|
1330
|
+
const dz = source[i + 2] - lp.z;
|
|
1331
|
+
const d2 = dx * dx + dy * dy + dz * dz;
|
|
1332
|
+
if (d2 < r2) inside++;
|
|
1333
|
+
distances.push(d2);
|
|
1334
|
+
}
|
|
1335
|
+
if (inside >= MIN_DENT_VERTICES || distances.length < MIN_DENT_VERTICES) return dent.radius;
|
|
1336
|
+
distances.sort((x, y) => x - y);
|
|
1337
|
+
const nearest = Math.sqrt(distances[0]);
|
|
1338
|
+
return Math.max(dent.radius, Math.sqrt(distances[MIN_DENT_VERTICES - 1]) * s * WIDEN_MARGIN, nearest * s * NEAREST_MARGIN);
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Dent all the meshes of a part. The shells take the dent as `applyDent` gives it; a skin (label, coat: `userData.destructionSkin`) sits a hair above its shell
|
|
1342
|
+
* and moves with the shell's surface instead (see `shellDisplacementAt`), so the shell never shows through it whatever the two meshes' vertex grids are.
|
|
1343
|
+
*/
|
|
1344
|
+
applyPartDent(meshes, worldPoint, worldNormal, dent, reachWorld) {
|
|
1345
|
+
const isSkin = (mv) => mv.mesh.userData.destructionSkin === true && !mv.host;
|
|
1346
|
+
const shells = meshes.filter((mv) => !isSkin(mv));
|
|
1347
|
+
const snapshots = [];
|
|
1348
|
+
for (const mv of shells) {
|
|
1349
|
+
const heavy = this.lowQuality && this.vertexCount(mv) > 2e4;
|
|
1350
|
+
const plain = !mv.host && !heavy && this.ownGeometry(mv) && mv.work;
|
|
1351
|
+
const before = plain ? new Float32Array(mv.work) : null;
|
|
1352
|
+
this.applyDent(mv, worldPoint, worldNormal, dent, reachWorld);
|
|
1353
|
+
if (before) snapshots.push({ mesh: mv.mesh, before, after: mv.work });
|
|
1354
|
+
}
|
|
1355
|
+
const displacement = this.t;
|
|
1356
|
+
const vertex = this.a;
|
|
1357
|
+
for (const mv of meshes) {
|
|
1358
|
+
if (!isSkin(mv)) continue;
|
|
1359
|
+
if (snapshots.length === 0) {
|
|
1360
|
+
this.applyDent(mv, worldPoint, worldNormal, dent, reachWorld);
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
if (!this.ownGeometry(mv)) continue;
|
|
1364
|
+
const work = mv.work;
|
|
1365
|
+
const mesh = mv.mesh;
|
|
1366
|
+
mesh.updateWorldMatrix(true, false);
|
|
1367
|
+
const toLocal = this.m4.copy(mesh.matrixWorld).invert();
|
|
1368
|
+
const scale = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
|
|
1369
|
+
let touched = false;
|
|
1370
|
+
for (let i = 0; i < work.length; i += 3) {
|
|
1371
|
+
vertex.set(work[i], work[i + 1], work[i + 2]).applyMatrix4(mesh.matrixWorld);
|
|
1372
|
+
if (vertex.distanceTo(worldPoint) > reachWorld) continue;
|
|
1373
|
+
const height = shellDisplacementAt(this.THREE, vertex, snapshots, displacement);
|
|
1374
|
+
const length = displacement.length();
|
|
1375
|
+
if (length === 0) continue;
|
|
1376
|
+
const falloff = 1 - (vertex.distanceTo(worldPoint) / reachWorld) ** 2;
|
|
1377
|
+
const smooth = dent.depth * falloff * falloff;
|
|
1378
|
+
const pull = Math.max(0, Math.min(length, smooth) * SKIN_FOLLOW - SKIN_SLACK - height * SKIN_LAYER_SLACK);
|
|
1379
|
+
displacement.transformDirection(toLocal).multiplyScalar(pull / scale);
|
|
1380
|
+
work[i] += displacement.x;
|
|
1381
|
+
work[i + 1] += displacement.y;
|
|
1382
|
+
work[i + 2] += displacement.z;
|
|
1383
|
+
touched = true;
|
|
1384
|
+
}
|
|
1385
|
+
if (touched) this.writePositions(mv, positionAttr(mesh.geometry));
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1237
1388
|
/** Copy `work` (dents) into the geometry, adding the melt sag when the mesh is melting. */
|
|
1238
1389
|
writePositions(mv, pos) {
|
|
1239
1390
|
if (mv.host) {
|
|
@@ -1280,6 +1431,7 @@ var Visuals = class {
|
|
|
1280
1431
|
return { s, pos, quat };
|
|
1281
1432
|
}
|
|
1282
1433
|
addDecal(mv, decal) {
|
|
1434
|
+
decal.obj.userData.destructionDebris = true;
|
|
1283
1435
|
mv.mesh.add(decal.obj);
|
|
1284
1436
|
mv.decals.push(decal);
|
|
1285
1437
|
this.decalCount++;
|
|
@@ -1296,6 +1448,27 @@ var Visuals = class {
|
|
|
1296
1448
|
}
|
|
1297
1449
|
addHole(mv, worldPoint, worldNormal, diameter) {
|
|
1298
1450
|
const { s, pos, quat } = this.decalPose(mv.mesh, worldPoint, worldNormal, 25e-4);
|
|
1451
|
+
const work = mv.host ? mv.host.work : mv.work;
|
|
1452
|
+
if (work && mv.touched) {
|
|
1453
|
+
const ln = worldNormal.clone().transformDirection(this.m4.copy(mv.mesh.matrixWorld).invert()).normalize();
|
|
1454
|
+
let minDistSq = Infinity;
|
|
1455
|
+
let nearestDisp = 0;
|
|
1456
|
+
const from = mv.range ? mv.range[0] * 3 : 0;
|
|
1457
|
+
const to = mv.range ? mv.range[1] * 3 : work.length;
|
|
1458
|
+
for (let i = from; i < to; i += 3) {
|
|
1459
|
+
const dx = work[i] - pos.x;
|
|
1460
|
+
const dy = work[i + 1] - pos.y;
|
|
1461
|
+
const dz = work[i + 2] - pos.z;
|
|
1462
|
+
const d2 = dx * dx + dy * dy + dz * dz;
|
|
1463
|
+
if (d2 < minDistSq) {
|
|
1464
|
+
minDistSq = d2;
|
|
1465
|
+
nearestDisp = dx * ln.x + dy * ln.y + dz * ln.z;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (minDistSq < (0.06 / s) ** 2 && nearestDisp < 0) {
|
|
1469
|
+
pos.addScaledVector(ln, nearestDisp);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1299
1472
|
const disc = new this.THREE.Mesh(this.kit.discGeometry(), this.kit.discMaterial());
|
|
1300
1473
|
disc.position.copy(pos);
|
|
1301
1474
|
disc.quaternion.copy(quat);
|
|
@@ -1767,7 +1940,13 @@ var FLUID_COLORS = {
|
|
|
1767
1940
|
gas: 15921906,
|
|
1768
1941
|
sap: 12098362,
|
|
1769
1942
|
acid: 10211900,
|
|
1770
|
-
blood: 9046538
|
|
1943
|
+
blood: 9046538,
|
|
1944
|
+
soda: 3808786,
|
|
1945
|
+
coffee: 3875860,
|
|
1946
|
+
beer: 14259516,
|
|
1947
|
+
milk: 16052714,
|
|
1948
|
+
wine: 7016479,
|
|
1949
|
+
juice: 15238923
|
|
1771
1950
|
};
|
|
1772
1951
|
var SMOKE_TONES = { white: 15132390, grey: 9079434, black: 2763306 };
|
|
1773
1952
|
var DEFAULT_SPEED = {
|
|
@@ -1864,6 +2043,8 @@ var Renderer = class {
|
|
|
1864
2043
|
__publicField(this, "tmpQ");
|
|
1865
2044
|
__publicField(this, "v1");
|
|
1866
2045
|
__publicField(this, "v2");
|
|
2046
|
+
// The last warm-up keeps its materials until the next one: a material that does not use lights keeps the program of the lights it was first drawn with, so each warm-up needs materials of its own.
|
|
2047
|
+
__publicField(this, "warmProbes");
|
|
1867
2048
|
this.world = o.world;
|
|
1868
2049
|
this.scene = o.scene;
|
|
1869
2050
|
this.quality = o.quality ?? "medium";
|
|
@@ -1961,6 +2142,24 @@ var Renderer = class {
|
|
|
1961
2142
|
if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes, hosts));
|
|
1962
2143
|
}
|
|
1963
2144
|
this.bounds.set(objectId, bound);
|
|
2145
|
+
for (const pv of bound.parts.values()) this.tagPart(pv, objectId);
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Label the meshes of a part so a host can go from a raycast hit to the world: `userData.destructionPart` (the part id) and
|
|
2149
|
+
* `userData.destructionObject` (the id of the world object that part belongs to now; a loose piece has an id of its own). Parts drawn by a range
|
|
2150
|
+
* of a merged mesh share that mesh with others and are left alone until they get a mesh of their own. Decals are marked `destructionDebris`.
|
|
2151
|
+
*/
|
|
2152
|
+
tagPart(pv, objectId) {
|
|
2153
|
+
for (const mv of pv.meshes) {
|
|
2154
|
+
if (mv.merged && !mv.loose) continue;
|
|
2155
|
+
mv.mesh.traverse((c) => {
|
|
2156
|
+
if (!c.isMesh) return;
|
|
2157
|
+
const ud = c.userData;
|
|
2158
|
+
if (ud.destructionDebris) return;
|
|
2159
|
+
if (partHintOf(ud.destructionPart)?.id !== pv.id) ud.destructionPart = pv.id;
|
|
2160
|
+
ud.destructionObject = objectId;
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
1964
2163
|
}
|
|
1965
2164
|
makePartVis(home, id, material, meshes, hosts) {
|
|
1966
2165
|
return { id, home, material, meshes: meshes.map((m) => this.visuals.makeMeshVis(m.mesh, home, m.range, hosts)), charApplied: 0, glowApplied: 0, meltApplied: 0, gone: "none" };
|
|
@@ -1974,13 +2173,13 @@ var Renderer = class {
|
|
|
1974
2173
|
specParts = void 0;
|
|
1975
2174
|
}
|
|
1976
2175
|
if (!specParts) return;
|
|
1977
|
-
root
|
|
2176
|
+
const frame = specFrameInverse(this.THREE, root);
|
|
1978
2177
|
const e = new this.THREE.Euler();
|
|
1979
2178
|
const q = new this.THREE.Quaternion();
|
|
1980
2179
|
for (const m of meshes) {
|
|
1981
2180
|
this.box.setFromObject(m);
|
|
1982
2181
|
if (this.box.isEmpty()) continue;
|
|
1983
|
-
const c =
|
|
2182
|
+
const c = this.box.getCenter(this.v1).applyMatrix4(frame);
|
|
1984
2183
|
let best = null;
|
|
1985
2184
|
let bestVol = Infinity;
|
|
1986
2185
|
for (const p of specParts) {
|
|
@@ -2017,10 +2216,9 @@ var Renderer = class {
|
|
|
2017
2216
|
// ----------------------------------------------------------------------------------------------------------------------------------
|
|
2018
2217
|
// Pieces
|
|
2019
2218
|
// ----------------------------------------------------------------------------------------------------------------------------------
|
|
2020
|
-
/** Half extents and centre of the box around some meshes, in the
|
|
2219
|
+
/** Half extents and centre of the box around some meshes, in the metric frame of `root` (its pose removed, its scale kept). */
|
|
2021
2220
|
measure(root, meshes) {
|
|
2022
|
-
root
|
|
2023
|
-
const inv = root.matrixWorld.clone().invert();
|
|
2221
|
+
const inv = specFrameInverse(this.THREE, root);
|
|
2024
2222
|
const total = new this.THREE.Box3();
|
|
2025
2223
|
const m4 = new this.THREE.Matrix4();
|
|
2026
2224
|
const lb = new this.THREE.Box3();
|
|
@@ -2095,6 +2293,7 @@ var Renderer = class {
|
|
|
2095
2293
|
dt = clamp(dt, 0, 0.1);
|
|
2096
2294
|
this.time += dt;
|
|
2097
2295
|
this.drainQueue();
|
|
2296
|
+
for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
|
|
2098
2297
|
this.updateActive(dt);
|
|
2099
2298
|
this.updateLeaks(dt);
|
|
2100
2299
|
this.pieceSys.update(dt);
|
|
@@ -2113,6 +2312,27 @@ var Renderer = class {
|
|
|
2113
2312
|
}
|
|
2114
2313
|
}
|
|
2115
2314
|
}
|
|
2315
|
+
/**
|
|
2316
|
+
* The ground under an object settled or tilted: its parts are drawn under a node with the soil's offset (`ObjectState.soil.offset`), so the host's
|
|
2317
|
+
* root keeps the pose the host gave and the world keeps getting it from `syncTransform`.
|
|
2318
|
+
*/
|
|
2319
|
+
onSettle(objectId) {
|
|
2320
|
+
const b = this.bounds.get(objectId);
|
|
2321
|
+
const soil = this.world.state(objectId)?.soil;
|
|
2322
|
+
if (!b || !soil) return;
|
|
2323
|
+
let node = b.soilNode;
|
|
2324
|
+
if (!node) {
|
|
2325
|
+
node = new this.THREE.Group();
|
|
2326
|
+
node.name = "destruction-soil";
|
|
2327
|
+
for (const c of [...b.root.children]) node.add(c);
|
|
2328
|
+
b.root.add(node);
|
|
2329
|
+
b.soilNode = node;
|
|
2330
|
+
}
|
|
2331
|
+
const { position: p, rotation: q } = soil.offset;
|
|
2332
|
+
node.position.set(p[0], p[1], p[2]);
|
|
2333
|
+
node.quaternion.set(q[0], q[1], q[2], q[3]);
|
|
2334
|
+
node.updateMatrixWorld(true);
|
|
2335
|
+
}
|
|
2116
2336
|
syncTransforms() {
|
|
2117
2337
|
for (const b of this.bounds.values()) this.syncOne(b);
|
|
2118
2338
|
}
|
|
@@ -2167,6 +2387,9 @@ var Renderer = class {
|
|
|
2167
2387
|
case "detach":
|
|
2168
2388
|
this.onDetach(ev);
|
|
2169
2389
|
break;
|
|
2390
|
+
case "settle":
|
|
2391
|
+
this.onSettle(ev.object);
|
|
2392
|
+
break;
|
|
2170
2393
|
case "collapse":
|
|
2171
2394
|
this.splitLoose(ev.object);
|
|
2172
2395
|
this.playEvent({ kind: "collapse" }, this.rootPoint(ev.object));
|
|
@@ -2252,13 +2475,39 @@ var Renderer = class {
|
|
|
2252
2475
|
}
|
|
2253
2476
|
case "leak": {
|
|
2254
2477
|
const key = `${ev.object}|${ev.part}`;
|
|
2478
|
+
const b = this.bounds.get(ev.object);
|
|
2479
|
+
let localPoint;
|
|
2480
|
+
let localDirection;
|
|
2481
|
+
if (b) {
|
|
2482
|
+
b.root.updateWorldMatrix(true, false);
|
|
2483
|
+
const inv = new this.THREE.Matrix4().copy(b.root.matrixWorld).invert();
|
|
2484
|
+
const lp = this.v1.set(ev.point[0], ev.point[1], ev.point[2]).applyMatrix4(inv);
|
|
2485
|
+
const ld = this.v2.set(ev.direction[0], ev.direction[1], ev.direction[2]).transformDirection(inv).normalize();
|
|
2486
|
+
localPoint = [lp.x, lp.y, lp.z];
|
|
2487
|
+
localDirection = [ld.x, ld.y, ld.z];
|
|
2488
|
+
}
|
|
2255
2489
|
const same = this.leaks.find((l) => l.object === ev.object && l.part === ev.part && Math.hypot(l.point[0] - ev.point[0], l.point[1] - ev.point[1], l.point[2] - ev.point[2]) < 0.03);
|
|
2256
2490
|
if (same) {
|
|
2257
2491
|
same.rate = ev.rate;
|
|
2258
2492
|
same.direction = ev.direction;
|
|
2493
|
+
if (localPoint) same.localPoint = localPoint;
|
|
2494
|
+
if (localDirection) same.localDirection = localDirection;
|
|
2259
2495
|
} else {
|
|
2260
2496
|
if (this.leaks.length >= 24) this.leaks.shift();
|
|
2261
|
-
this.leaks.push({
|
|
2497
|
+
this.leaks.push({
|
|
2498
|
+
object: ev.object,
|
|
2499
|
+
part: ev.part,
|
|
2500
|
+
fluid: ev.fluid,
|
|
2501
|
+
point: ev.point,
|
|
2502
|
+
direction: ev.direction,
|
|
2503
|
+
rate: ev.rate,
|
|
2504
|
+
pressurized: ev.pressurized,
|
|
2505
|
+
acc: 0,
|
|
2506
|
+
opened: this.world.part(ev.object, ev.part)?.holes.some((h) => h.through) ?? false,
|
|
2507
|
+
localPoint,
|
|
2508
|
+
localDirection,
|
|
2509
|
+
life: 0
|
|
2510
|
+
});
|
|
2262
2511
|
}
|
|
2263
2512
|
void key;
|
|
2264
2513
|
break;
|
|
@@ -2335,10 +2584,9 @@ var Renderer = class {
|
|
|
2335
2584
|
const def = this.world.materials.get(pv.material);
|
|
2336
2585
|
const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
|
|
2337
2586
|
const wn = this.worldDir(b, ev.dent.normal);
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
}
|
|
2587
|
+
const shown = pv.meshes.filter((mv) => this.visuals.shown(mv));
|
|
2588
|
+
const reach = shown.reduce((widest, mv) => Math.max(widest, this.visuals.dentReach(mv, wp, ev.dent)), ev.dent.radius);
|
|
2589
|
+
this.visuals.applyPartDent(shown, wp, wn, ev.dent, reach);
|
|
2342
2590
|
if (def?.tags?.includes("metal")) this.vfx.burst("sparks", ev.worldPoint, [wn.x, wn.y, wn.z], 4 + Math.min(8, ev.dent.depth * 150), 3.2);
|
|
2343
2591
|
this.sound(impactKey(def), ev.worldPoint, clamp(0.3 + ev.dent.depth * 12, 0.3, 1));
|
|
2344
2592
|
}
|
|
@@ -2534,6 +2782,7 @@ var Renderer = class {
|
|
|
2534
2782
|
}
|
|
2535
2783
|
}
|
|
2536
2784
|
this.bounds.set(newId, nb);
|
|
2785
|
+
for (const pv of nb.parts.values()) this.tagPart(pv, newId);
|
|
2537
2786
|
const meshes = this.meshesOf(nb);
|
|
2538
2787
|
if (meshes.length === 0) return;
|
|
2539
2788
|
const { half, center } = this.measure(group, meshes);
|
|
@@ -2561,24 +2810,40 @@ var Renderer = class {
|
|
|
2561
2810
|
const report = options.report;
|
|
2562
2811
|
const confinement = report?.find((r) => r.blast !== void 0)?.blast?.confinement ?? 1;
|
|
2563
2812
|
const energy = ev.energy * confinement;
|
|
2564
|
-
|
|
2813
|
+
const reachOf = (objectId) => report?.find((r) => r.object === objectId)?.blast?.shelter ?? 1;
|
|
2814
|
+
const shove = (pc) => {
|
|
2565
2815
|
this.pieceSys.centerOf(pc, c);
|
|
2566
|
-
const
|
|
2567
|
-
|
|
2568
|
-
const dz = c.z - p[2];
|
|
2569
|
-
const d = Math.hypot(dx, dy, dz);
|
|
2570
|
-
if (d > ev.radius) continue;
|
|
2571
|
-
const fall = (1 - d / ev.radius) ** 2;
|
|
2572
|
-
const reach = report?.find((r) => r.object === pc.objectId)?.blast?.shelter ?? 1;
|
|
2573
|
-
const speed = Math.min(30, Math.sqrt(2 * energy * reach * 0.02 / pc.mass)) * fall;
|
|
2574
|
-
const inv = d > 1e-4 ? 1 / d : 0;
|
|
2575
|
-
const dir = d > 1e-4 ? [dx * inv, dy * inv + 0.3, dz * inv] : [0, 1, 0];
|
|
2816
|
+
const hit = blastShove({ point: p, energy, radius: ev.radius, reach: reachOf(pc.objectId) }, { position: [c.x, c.y, c.z], mass: pc.mass }, this.o.blastShove);
|
|
2817
|
+
if (!hit) return;
|
|
2576
2818
|
const spread = pc.halfExtents[0];
|
|
2577
|
-
|
|
2578
|
-
|
|
2819
|
+
const momentum = hit.speed * pc.mass;
|
|
2820
|
+
this.pieceSys.impulse(pc, [hit.direction[0] * momentum, hit.direction[1] * momentum, hit.direction[2] * momentum], [c.x + (this.rng() - 0.5) * spread, c.y + spread * 0.5, c.z + (this.rng() - 0.5) * spread]);
|
|
2821
|
+
};
|
|
2822
|
+
for (const pc of [...this.pieceSys.list]) shove(pc);
|
|
2823
|
+
if (this.o.shoveable) this.loosen(p, ev.radius, energy, reachOf);
|
|
2579
2824
|
this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(energy) * 0.08));
|
|
2580
2825
|
this.sound("explode", p, clamp(0.5 + ev.radius / 10, 0.5, 1));
|
|
2581
2826
|
}
|
|
2827
|
+
/** The bound objects without a piece that the host lets a blast move (`shoveable`): the ones it reaches with enough speed become pieces and get the shove. */
|
|
2828
|
+
loosen(point, radius, energy, reachOf) {
|
|
2829
|
+
const shoveable = this.o.shoveable;
|
|
2830
|
+
if (!shoveable) return;
|
|
2831
|
+
for (const b of [...this.bounds.values()]) {
|
|
2832
|
+
if (b.piece || this.pieceSys.get(b.id) || !shoveable(b.id) || !this.world.has(b.id)) continue;
|
|
2833
|
+
const { half, center } = this.measure(b.root, this.meshesOf(b));
|
|
2834
|
+
const world = center.clone().applyQuaternion(b.root.quaternion).add(b.root.position);
|
|
2835
|
+
const mass = this.massOf(b.id);
|
|
2836
|
+
const hit = blastShove({ point, energy, radius, reach: reachOf(b.id) }, { position: [world.x, world.y, world.z], mass }, this.o.blastShove);
|
|
2837
|
+
if (!hit) continue;
|
|
2838
|
+
const piece = this.makePiece(b.id, { mass });
|
|
2839
|
+
const body = this.pieceSys.get(b.id);
|
|
2840
|
+
if (!body) continue;
|
|
2841
|
+
this.o.onLoosened?.(b.id, piece);
|
|
2842
|
+
const spread = half[0];
|
|
2843
|
+
const momentum = hit.speed * mass;
|
|
2844
|
+
this.pieceSys.impulse(body, [hit.direction[0] * momentum, hit.direction[1] * momentum, hit.direction[2] * momentum], [world.x + (this.rng() - 0.5) * spread, world.y + spread * 0.5, world.z + (this.rng() - 0.5) * spread]);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2582
2847
|
playExplosion(ev, options) {
|
|
2583
2848
|
const named = options.effect ?? ev.effect;
|
|
2584
2849
|
const direction = options.direction;
|
|
@@ -2678,21 +2943,44 @@ var Renderer = class {
|
|
|
2678
2943
|
updateLeaks(dt) {
|
|
2679
2944
|
for (let i = this.leaks.length - 1; i >= 0; i--) {
|
|
2680
2945
|
const l = this.leaks[i];
|
|
2946
|
+
const b = this.bounds.get(l.object);
|
|
2681
2947
|
const ps = this.world.part(l.object, l.part);
|
|
2682
|
-
if (!ps || ps.liters === void 0 || ps.liters <=
|
|
2948
|
+
if (!b || !ps || ps.liters === void 0 || ps.liters <= 1e-4 || l.opened && !ps.holes.some((h) => h.through)) {
|
|
2683
2949
|
this.leaks.splice(i, 1);
|
|
2684
2950
|
continue;
|
|
2685
2951
|
}
|
|
2952
|
+
l.life = (l.life ?? 0) + dt;
|
|
2953
|
+
if (l.pressurized && l.life > 0.8) l.pressurized = false;
|
|
2954
|
+
if (l.lastLiters !== void 0 && Math.abs(ps.liters - l.lastLiters) < 1e-5) {
|
|
2955
|
+
l.idleSeconds = (l.idleSeconds ?? 0) + dt;
|
|
2956
|
+
if (l.idleSeconds > 0.4) {
|
|
2957
|
+
this.leaks.splice(i, 1);
|
|
2958
|
+
continue;
|
|
2959
|
+
}
|
|
2960
|
+
} else {
|
|
2961
|
+
l.idleSeconds = 0;
|
|
2962
|
+
l.lastLiters = ps.liters;
|
|
2963
|
+
}
|
|
2964
|
+
let pt = l.point;
|
|
2965
|
+
let dir = l.direction;
|
|
2966
|
+
if (l.localPoint && l.localDirection) {
|
|
2967
|
+
b.root.updateWorldMatrix(true, false);
|
|
2968
|
+
const wp = this.v1.set(l.localPoint[0], l.localPoint[1], l.localPoint[2]).applyMatrix4(b.root.matrixWorld);
|
|
2969
|
+
const wd = this.v2.set(l.localDirection[0], l.localDirection[1], l.localDirection[2]).transformDirection(b.root.matrixWorld).normalize();
|
|
2970
|
+
pt = [wp.x, wp.y, wp.z];
|
|
2971
|
+
dir = [wd.x, wd.y, wd.z];
|
|
2972
|
+
l.point = pt;
|
|
2973
|
+
l.direction = dir;
|
|
2974
|
+
}
|
|
2686
2975
|
l.acc += clamp(20 + l.rate * 400, 15, 120) * dt;
|
|
2687
2976
|
const color = FLUID_COLORS[l.fluid] ?? 11184810;
|
|
2688
|
-
const
|
|
2689
|
-
const dl = Math.hypot(d[0], d[1], d[2]) || 1;
|
|
2977
|
+
const dl = Math.hypot(dir[0], dir[1], dir[2]) || 1;
|
|
2690
2978
|
const sp = l.pressurized ? 2.5 + this.rng() * 1.5 : 0.3 + this.rng() * 0.3;
|
|
2691
2979
|
while (l.acc >= 1) {
|
|
2692
2980
|
l.acc -= 1;
|
|
2693
|
-
const vel = [
|
|
2694
|
-
if (l.fluid === "gas") this.vfx.spawn("steam",
|
|
2695
|
-
else this.vfx.spawn("drips",
|
|
2981
|
+
const vel = [dir[0] / dl * sp + (this.rng() - 0.5) * 0.25, dir[1] / dl * sp + (this.rng() - 0.5) * 0.25, dir[2] / dl * sp + (this.rng() - 0.5) * 0.25];
|
|
2982
|
+
if (l.fluid === "gas") this.vfx.spawn("steam", pt, vel, { color, size: 0.5, alpha: 0.5 });
|
|
2983
|
+
else this.vfx.spawn("drips", pt, vel, { color, tag: this.rng() < 0.03 ? this.tagOf(l.object) : 0, owner: this.owner });
|
|
2696
2984
|
}
|
|
2697
2985
|
}
|
|
2698
2986
|
}
|
|
@@ -2730,6 +3018,7 @@ var Renderer = class {
|
|
|
2730
3018
|
for (const [pid, pv] of [...b.parts]) {
|
|
2731
3019
|
if (pv.home !== home) continue;
|
|
2732
3020
|
this.visuals.restorePart(pv);
|
|
3021
|
+
this.tagPart(pv, home);
|
|
2733
3022
|
if (b !== homeBound && homeBound) {
|
|
2734
3023
|
b.parts.delete(pid);
|
|
2735
3024
|
homeBound.parts.set(pid, pv);
|
|
@@ -2771,10 +3060,31 @@ var Renderer = class {
|
|
|
2771
3060
|
const { puffs, sparks, glows, chunks } = this.vfx.stats();
|
|
2772
3061
|
return puffs + sparks + glows + chunks;
|
|
2773
3062
|
}
|
|
3063
|
+
warmUpProbes() {
|
|
3064
|
+
this.releaseWarmProbes();
|
|
3065
|
+
const { kit } = this.visuals;
|
|
3066
|
+
const T = this.THREE;
|
|
3067
|
+
const geometry = new T.BufferGeometry();
|
|
3068
|
+
geometry.setAttribute("position", new T.BufferAttribute(new Float32Array(6), 3));
|
|
3069
|
+
const materials = [kit.discMaterial().clone(), kit.patchMaterial(8421504).clone(), kit.lineMaterial(0).clone()];
|
|
3070
|
+
const group = new T.Group();
|
|
3071
|
+
group.add(new T.Mesh(kit.discGeometry(), materials[0]), new T.Mesh(kit.discGeometry(), materials[1]), new T.LineSegments(geometry, materials[2]));
|
|
3072
|
+
for (const probe of group.children) probe.frustumCulled = false;
|
|
3073
|
+
this.warmProbes = { group, geometry, materials };
|
|
3074
|
+
return { group, dispose: () => {
|
|
3075
|
+
} };
|
|
3076
|
+
}
|
|
3077
|
+
releaseWarmProbes() {
|
|
3078
|
+
if (!this.warmProbes) return;
|
|
3079
|
+
this.warmProbes.geometry.dispose();
|
|
3080
|
+
for (const material of this.warmProbes.materials) material.dispose();
|
|
3081
|
+
this.warmProbes = void 0;
|
|
3082
|
+
}
|
|
2774
3083
|
rebarCount() {
|
|
2775
3084
|
return this.cages.count;
|
|
2776
3085
|
}
|
|
2777
3086
|
dispose() {
|
|
3087
|
+
this.releaseWarmProbes();
|
|
2778
3088
|
if (this.disposed) return;
|
|
2779
3089
|
this.unsubscribe();
|
|
2780
3090
|
this.clear();
|