@adatechnology/game-vehicles 0.5.0 → 0.6.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/index.js CHANGED
@@ -892,7 +892,7 @@ function advance(geo, ti, deadline) {
892
892
  }
893
893
  return ti.builtBlocks === total;
894
894
  }
895
- function start(geo) {
895
+ function start(geo, store = cache) {
896
896
  const pos = geo.attributes.position;
897
897
  const triangles = trianglesOf(geo);
898
898
  const ti = {
@@ -905,7 +905,7 @@ function start(geo) {
905
905
  blocks: new Float64Array(blockCount(triangles) * 6),
906
906
  builtBlocks: 0
907
907
  };
908
- cache.set(geo, ti);
908
+ store.set(geo, ti);
909
909
  return ti;
910
910
  }
911
911
  function triIndexOf(geo) {
@@ -957,6 +957,106 @@ function boundsFromIndex(geo) {
957
957
  }
958
958
  return { box, version: ti.positionVersion };
959
959
  }
960
+ var rayCache = /* @__PURE__ */ new WeakMap();
961
+ function rayIndexOf(geo) {
962
+ const shared = cache.get(geo);
963
+ if (shared && isCurrent(geo, shared) && shared.builtBlocks === blockCount(shared.triangles)) return shared;
964
+ let ti = rayCache.get(geo);
965
+ if (!ti || !isCurrent(geo, ti)) ti = start(geo, rayCache);
966
+ advance(geo, ti, Infinity);
967
+ return ti;
968
+ }
969
+ var BLOCK_SLACK = 1e-6;
970
+ var scratch = /* @__PURE__ */ new WeakMap();
971
+ function scratchOf(THREE) {
972
+ let s = scratch.get(THREE);
973
+ if (!s) {
974
+ s = {
975
+ inverse: new THREE.Matrix4(),
976
+ local: new THREE.Ray(),
977
+ box: new THREE.Box3(),
978
+ a: new THREE.Vector3(),
979
+ b: new THREE.Vector3(),
980
+ c: new THREE.Vector3(),
981
+ point: new THREE.Vector3(),
982
+ world: new THREE.Vector3(),
983
+ sphere: new THREE.Sphere()
984
+ };
985
+ scratch.set(THREE, s);
986
+ }
987
+ return s;
988
+ }
989
+ function indexable(THREE, mesh) {
990
+ const geo = mesh.geometry;
991
+ if (mesh.raycast !== THREE.Mesh.prototype.raycast) return false;
992
+ if (!mesh.isMesh || mesh.isSkinnedMesh || mesh.isInstancedMesh || mesh.isBatchedMesh || !geo || !geo.isBufferGeometry) return false;
993
+ if (!mesh.material || Array.isArray(mesh.material)) return false;
994
+ if (geo.morphAttributes && geo.morphAttributes.position && geo.morphAttributes.position.length) return false;
995
+ return supports(geo);
996
+ }
997
+ function raycastMesh(THREE, raycaster, mesh, out) {
998
+ const geo = mesh.geometry;
999
+ const s = scratchOf(THREE);
1000
+ if (geo.boundingSphere === null) geo.computeBoundingSphere();
1001
+ s.sphere.copy(geo.boundingSphere).applyMatrix4(mesh.matrixWorld);
1002
+ if (!raycaster.ray.intersectsSphere(s.sphere)) return;
1003
+ const ti = rayIndexOf(geo);
1004
+ if (!ti) return;
1005
+ s.inverse.copy(mesh.matrixWorld).invert();
1006
+ s.local.copy(raycaster.ray).applyMatrix4(s.inverse);
1007
+ const material = mesh.material;
1008
+ const back = material.side === THREE.BackSide;
1009
+ const cull = material.side === THREE.FrontSide;
1010
+ const P = geo.attributes.position;
1011
+ const ix = geo.index;
1012
+ const count = ix ? ix.count : P.count;
1013
+ const first = Math.max(0, geo.drawRange.start);
1014
+ const last = Math.min(count, geo.drawRange.start + geo.drawRange.count);
1015
+ const t0 = Math.ceil(first / 3);
1016
+ const t1 = Math.floor(last / 3);
1017
+ const B = ti.boxes;
1018
+ const K = ti.blocks;
1019
+ for (let blk = Math.floor(t0 / TRI_BLOCK); blk * TRI_BLOCK < t1; blk++) {
1020
+ const o = blk * 6;
1021
+ s.box.min.set(K[o] - BLOCK_SLACK, K[o + 1] - BLOCK_SLACK, K[o + 2] - BLOCK_SLACK);
1022
+ s.box.max.set(K[o + 3] + BLOCK_SLACK, K[o + 4] + BLOCK_SLACK, K[o + 5] + BLOCK_SLACK);
1023
+ if (!s.local.intersectsBox(s.box)) continue;
1024
+ const end = Math.min(t1, (blk + 1) * TRI_BLOCK);
1025
+ for (let t = Math.max(t0, blk * TRI_BLOCK); t < end; t++) {
1026
+ const j = t * 6;
1027
+ s.box.min.set(B[j] - BLOCK_SLACK, B[j + 1] - BLOCK_SLACK, B[j + 2] - BLOCK_SLACK);
1028
+ s.box.max.set(B[j + 3] + BLOCK_SLACK, B[j + 4] + BLOCK_SLACK, B[j + 5] + BLOCK_SLACK);
1029
+ if (!s.local.intersectsBox(s.box)) continue;
1030
+ const a = ix ? ix.getX(t * 3) : t * 3;
1031
+ const b = ix ? ix.getX(t * 3 + 1) : t * 3 + 1;
1032
+ const c = ix ? ix.getX(t * 3 + 2) : t * 3 + 2;
1033
+ s.a.fromBufferAttribute(P, a);
1034
+ s.b.fromBufferAttribute(P, b);
1035
+ s.c.fromBufferAttribute(P, c);
1036
+ const hit = back ? s.local.intersectTriangle(s.c, s.b, s.a, true, s.point) : s.local.intersectTriangle(s.a, s.b, s.c, cull, s.point);
1037
+ if (hit === null) continue;
1038
+ s.world.copy(s.point).applyMatrix4(mesh.matrixWorld);
1039
+ const distance = raycaster.ray.origin.distanceTo(s.world);
1040
+ if (distance < raycaster.near || distance > raycaster.far) continue;
1041
+ const normal = new THREE.Vector3();
1042
+ THREE.Triangle.getNormal(s.a, s.b, s.c, normal);
1043
+ out.push({ distance, point: s.world.clone(), object: mesh, face: { a, b, c, normal, materialIndex: 0 }, faceIndex: Math.floor(t) });
1044
+ }
1045
+ }
1046
+ }
1047
+ function collect(THREE, raycaster, object, recursive, out) {
1048
+ if (object.layers.test(raycaster.layers)) {
1049
+ if (indexable(THREE, object)) raycastMesh(THREE, raycaster, object, out);
1050
+ else object.raycast(raycaster, out);
1051
+ }
1052
+ if (recursive) for (const child of object.children) collect(THREE, raycaster, child, true, out);
1053
+ }
1054
+ function raycastIndexed(THREE, raycaster, objects, recursive = false) {
1055
+ const out = [];
1056
+ for (const object of objects) collect(THREE, raycaster, object, recursive, out);
1057
+ out.sort((x, y) => x.distance - y.distance);
1058
+ return out;
1059
+ }
960
1060
 
961
1061
  // src/geometry.ts
962
1062
  function installGeometry(kit) {
@@ -2290,6 +2390,7 @@ function installBody(kit) {
2290
2390
  }
2291
2391
  kit.profX = profX;
2292
2392
  const _sr = new THREE.Raycaster();
2393
+ _sr.layers.enableAll();
2293
2394
  kit.surfaceAt = function(c, org, dir, o) {
2294
2395
  o = o || {};
2295
2396
  c.root.updateMatrixWorld(true);
@@ -2306,7 +2407,7 @@ function installBody(kit) {
2306
2407
  _sr.set(p0.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
2307
2408
  _sr.near = 0;
2308
2409
  _sr.far = o.max || 20;
2309
- const h = _sr.intersectObjects(cands, false)[0];
2410
+ const h = raycastIndexed(THREE, _sr, cands)[0];
2310
2411
  if (!h) return null;
2311
2412
  const n = h.face ? h.face.normal.clone().transformDirection(h.object.matrixWorld).transformDirection(inv) : d.clone().negate();
2312
2413
  if (n.dot(d) > 0) n.negate();
@@ -2328,6 +2429,7 @@ function installGlass(kit) {
2328
2429
  const V3 = THREE.Vector3;
2329
2430
  const I = kit._;
2330
2431
  const { MATERIALS, crease, cutGeometry, pathShape, subdivide, tumble, uvProject } = I;
2432
+ I.paneSeals = /* @__PURE__ */ new WeakMap();
2331
2433
  kit.addGlass = function(c, geo, label, type, o) {
2332
2434
  o = o || {};
2333
2435
  if (!geo.attributes.uv || o.reuv) o.aspect = uvProject(geo, o.uvAxes);
@@ -2394,7 +2496,7 @@ function installGlass(kit) {
2394
2496
  cy /= poly.length;
2395
2497
  const base = I.resolveLabel(label ? label(i, polys.length) : { key: "window" });
2396
2498
  const lab = { key: base.key, label: kit.t("parts.glass_side", { label: base.label, s }) };
2397
- kit.addGlass(c, geo, lab, o.type || "tempered", {
2499
+ const pane = kit.addGlass(c, geo, lab, o.type || "tempered", {
2398
2500
  mat: o.mat || (o.see === false ? MATERIALS.glassDark : MATERIALS.glass),
2399
2501
  pos: new V3(cx, cy, s * I.halfAt(Wg, T, cy)),
2400
2502
  outline: shp.getSpacedPoints(56),
@@ -2410,6 +2512,7 @@ function installGlass(kit) {
2410
2512
  pts.pop();
2411
2513
  const seal = c.tube(pts, 0.011, MATERIALS.trim, { closed: true, seg: 72, rs: 5 });
2412
2514
  seal.castShadow = false;
2515
+ I.paneSeals.set(pane, seal);
2413
2516
  });
2414
2517
  });
2415
2518
  };
@@ -4119,6 +4222,47 @@ function installWheels(kit) {
4119
4222
  Object.assign(I, {});
4120
4223
  }
4121
4224
 
4225
+ // src/particles.ts
4226
+ import { warnOnce } from "@adatechnology/game-contracts";
4227
+
4228
+ // src/routed-particles.ts
4229
+ var ROUTED_PARTICLE_TYPES = { smoke: "particle_puff", dust: "particle_puff", fire: "particle_glow" };
4230
+ var PUFF_LOOKS = {
4231
+ smokewhite: [10133927, 0.42],
4232
+ smokeblack: [1776412, 0.62],
4233
+ smokegray: [5593436, 0.5],
4234
+ smokesteam: [11975873, 0.3],
4235
+ dust: [7235160, 0.34],
4236
+ fire: [16777215, 0.9]
4237
+ };
4238
+ var LIFE = { dust: 1.6, smoke: 2.8, fire: 0.7 };
4239
+ var FIRE_COLOR = 16753968;
4240
+ var SMOKE_START_SCALE = 0.18;
4241
+ function routedParticleRequest(input) {
4242
+ const { type } = input;
4243
+ const look = PUFF_LOOKS[type + (input.variant ?? "")] ?? PUFF_LOOKS[type] ?? PUFF_LOOKS.smokewhite;
4244
+ const life = input.life ?? LIFE[type] ?? 99;
4245
+ const size = (input.size ?? 1) * (input.grow ?? 1);
4246
+ const isFire = type === "fire";
4247
+ const growth = isFire ? 1 : (SMOKE_START_SCALE + 1.5 * (1 - Math.exp(-life * 1.1)) + life * 0.12) / SMOKE_START_SCALE;
4248
+ return {
4249
+ effect: ROUTED_PARTICLE_TYPES[type],
4250
+ point: input.position,
4251
+ values: {
4252
+ vx: input.velocity[0],
4253
+ vy: input.velocity[1],
4254
+ vz: input.velocity[2],
4255
+ life,
4256
+ size: isFire ? size * 0.85 : size * SMOKE_START_SCALE,
4257
+ grow: growth,
4258
+ color: input.color ?? (isFire ? FIRE_COLOR : look[0]),
4259
+ alpha: input.alpha ?? look[1],
4260
+ gravity: isFire ? 0 : type === "dust" ? 0 : -0.08,
4261
+ drag: type === "dust" ? 2.5 : isFire ? 1.5 : 0.9
4262
+ }
4263
+ };
4264
+ }
4265
+
4122
4266
  // src/particles.ts
4123
4267
  function installParticles(kit) {
4124
4268
  const THREE = kit.THREE;
@@ -4210,7 +4354,7 @@ function installParticles(kit) {
4210
4354
  });
4211
4355
  }
4212
4356
  const list = fire ? FIRE_TEXTURES : SMOKE_TEXTURES;
4213
- const mat = new THREE.SpriteMaterial({
4357
+ const build = () => new THREE.SpriteMaterial({
4214
4358
  map: list[Math.floor(Math.random() * list.length)],
4215
4359
  color: new THREE.Color(col),
4216
4360
  transparent: true,
@@ -4218,6 +4362,7 @@ function installParticles(kit) {
4218
4362
  opacity: op,
4219
4363
  blending: fire ? THREE.AdditiveBlending : THREE.NormalBlending
4220
4364
  });
4365
+ const mat = kit.particleMaterials ? kit.particleMaterials.own(build) : build();
4221
4366
  const dis = { value: 0 };
4222
4367
  mat.userData.dis = dis;
4223
4368
  mat.onBeforeCompile = function(sh) {
@@ -4245,11 +4390,37 @@ function installParticles(kit) {
4245
4390
  function killParticle(c, i) {
4246
4391
  const P = c.particles[i];
4247
4392
  c.effects.remove(P.m);
4248
- if (P.own) P.m.material.dispose();
4393
+ if (P.own) {
4394
+ if (kit.particleMaterials) kit.particleMaterials.release(P.m.material);
4395
+ else P.m.material.dispose();
4396
+ }
4249
4397
  c.particles.splice(i, 1);
4250
4398
  }
4399
+ function routeParticle(c, effects, type, pos, vel, o) {
4400
+ c.effects.updateWorldMatrix(true, false);
4401
+ const at = pos.clone().applyMatrix4(c.effects.matrixWorld);
4402
+ const to = vel.clone().transformDirection(c.effects.matrixWorld).multiplyScalar(vel.length());
4403
+ const colour = o.col == null ? void 0 : new THREE.Color(o.col).getHex();
4404
+ effects.play(
4405
+ routedParticleRequest({
4406
+ type,
4407
+ position: [at.x, at.y, at.z],
4408
+ velocity: [to.x, to.y, to.z],
4409
+ ...o.variant ? { variant: o.variant } : {},
4410
+ ...colour !== void 0 ? { color: colour } : {},
4411
+ ...o.op != null ? { alpha: o.op } : {},
4412
+ ...o.s ? { size: o.s } : {},
4413
+ ...o.life ? { life: o.life } : {},
4414
+ ...o.grow ? { grow: o.grow } : {}
4415
+ })
4416
+ );
4417
+ return null;
4418
+ }
4251
4419
  kit.spawn = function(c, type, pos, vel, o) {
4252
4420
  o = o || {};
4421
+ const effects = ROUTED_PARTICLE_TYPES[type] ? kit.effects?.() : void 0;
4422
+ if (effects) return routeParticle(c, effects, type, pos, vel, o);
4423
+ if (ROUTED_PARTICLE_TYPES[type] && !kit.effects) warnOnce("game-vehicles.own-particles", "game-vehicles drew its smoke, fire and dust with sprites of its own because the host lent no effects world (options.effects); pass the host effects so the puffs share one budget");
4253
4424
  if (c.particles.length >= PARTICLE_LIMIT) {
4254
4425
  const idx = c.particles.findIndex(function(q) {
4255
4426
  return TRANSIENT_TYPES[q.type];
@@ -4521,6 +4692,7 @@ function installFluids(kit) {
4521
4692
  };
4522
4693
  kit.defineFluid("stain", { fresh: "#4a4a44", dry: "#26241f", wipeable: false });
4523
4694
  const _fr = new THREE.Raycaster();
4695
+ _fr.layers.enableAll();
4524
4696
  kit.splat = function(c, p, n, r, o) {
4525
4697
  o = o || {};
4526
4698
  n = n.clone().normalize();
@@ -4962,31 +5134,55 @@ function installFluids(kit) {
4962
5134
  }
4963
5135
 
4964
5136
  // src/glass-damage.ts
5137
+ function crackedGlassMaterial(THREE, texture) {
5138
+ return new THREE.MeshStandardMaterial({
5139
+ color: 16777215,
5140
+ map: texture,
5141
+ emissive: 6976118,
5142
+ emissiveMap: texture,
5143
+ transparent: true,
5144
+ roughness: 0.25,
5145
+ metalness: 0.1,
5146
+ side: THREE.DoubleSide,
5147
+ depthWrite: false
5148
+ });
5149
+ }
4965
5150
  function installGlassDamage(kit) {
4966
5151
  const THREE = kit.THREE;
4967
5152
  const V3 = THREE.Vector3;
4968
5153
  const PI = Math.PI;
4969
5154
  const I = kit._;
4970
5155
  const document2 = kit.document;
4971
- const { DOUBLE_SIDE, MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
4972
- function glassUV(msh, pLocal) {
5156
+ const { MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
5157
+ function glassUV(c, msh, pLocal) {
4973
5158
  const pos = msh.geometry.attributes.position;
4974
5159
  const uv = msh.geometry.attributes.uv;
4975
5160
  if (!uv) return [0.5, 0.5];
4976
- const inv = new THREE.Matrix4().copy(msh.matrix).invert();
4977
- const q = pLocal.clone().applyMatrix4(inv);
4978
- let best = 1e9;
4979
- let bi = 0;
4980
- const v = new V3();
4981
- for (let i = 0; i < pos.count; i++) {
4982
- v.fromBufferAttribute(pos, i);
4983
- const d = v.distanceToSquared(q);
4984
- if (d < best) {
4985
- best = d;
4986
- bi = i;
4987
- }
5161
+ c.root.updateMatrixWorld(true);
5162
+ const q = msh.worldToLocal(c.vehicle.localToWorld(pLocal.clone()));
5163
+ const idx = msh.geometry.index;
5164
+ const tri = new THREE.Triangle();
5165
+ const near = new V3();
5166
+ const bary = new V3();
5167
+ let best = Infinity;
5168
+ const out = [0.5, 0.5];
5169
+ const count = idx ? idx.count : pos.count;
5170
+ for (let f = 0; f + 2 < count; f += 3) {
5171
+ const ia = idx ? idx.getX(f) : f;
5172
+ const ib = idx ? idx.getX(f + 1) : f + 1;
5173
+ const ic = idx ? idx.getX(f + 2) : f + 2;
5174
+ tri.a.fromBufferAttribute(pos, ia);
5175
+ tri.b.fromBufferAttribute(pos, ib);
5176
+ tri.c.fromBufferAttribute(pos, ic);
5177
+ tri.closestPointToPoint(q, near);
5178
+ const d = near.distanceToSquared(q);
5179
+ if (d >= best) continue;
5180
+ best = d;
5181
+ tri.getBarycoord(near, bary);
5182
+ out[0] = uv.getX(ia) * bary.x + uv.getX(ib) * bary.y + uv.getX(ic) * bary.z;
5183
+ out[1] = uv.getY(ia) * bary.x + uv.getY(ib) * bary.y + uv.getY(ic) * bary.z;
4988
5184
  }
4989
- return [uv.getX(bi), uv.getY(bi)];
5185
+ return out;
4990
5186
  }
4991
5187
  function drawWeb(x, cx, cy, s, ax, rnd) {
4992
5188
  const n = 16 + Math.floor(rnd() * 8);
@@ -5063,19 +5259,9 @@ function installGlassDamage(kit) {
5063
5259
  x0.fillStyle = "rgba(48,66,72,0.5)";
5064
5260
  x0.fillRect(0, 0, 256, 256);
5065
5261
  G.tex = new THREE.CanvasTexture(G.cv);
5066
- msh.material = new THREE.MeshStandardMaterial({
5067
- color: 16777215,
5068
- map: G.tex,
5069
- emissive: 6976118,
5070
- emissiveMap: G.tex,
5071
- transparent: true,
5072
- roughness: 0.25,
5073
- metalness: 0.1,
5074
- side: DOUBLE_SIDE,
5075
- depthWrite: false
5076
- });
5262
+ msh.material = crackedGlassMaterial(THREE, G.tex);
5077
5263
  }
5078
- const uv = glassUV(msh, p);
5264
+ const uv = glassUV(c, msh, p);
5079
5265
  const x = G.cv.getContext("2d");
5080
5266
  drawWeb(x, uv[0] * 256, (1 - uv[1]) * 256, sc != null ? sc : 0.6 + force * 0.5, Math.max(0.4, G.aspect || 1), c.rand);
5081
5267
  G.tex.needsUpdate = true;
@@ -5220,6 +5406,13 @@ function installGlassDamage(kit) {
5220
5406
  }
5221
5407
  const seatGeo = [];
5222
5408
  const d = dir.clone().normalize();
5409
+ if (msh.parent && msh.parent !== c.vehicle) {
5410
+ c.root.updateMatrixWorld(true);
5411
+ const toVehicle = new THREE.Matrix4().copy(c.vehicle.matrixWorld).invert().multiply(msh.parent.matrixWorld);
5412
+ cubesAt.forEach(function(q) {
5413
+ q.applyMatrix4(toVehicle);
5414
+ });
5415
+ }
5223
5416
  cubesAt.forEach(function(p, i) {
5224
5417
  if (i % 3 === 0 && c.seatTops.length && !c.rollState && !c.flipped) {
5225
5418
  let best = null;
@@ -5422,6 +5615,7 @@ function installImpact(kit) {
5422
5615
  polygonOffsetUnits: -2
5423
5616
  });
5424
5617
  const _ray = new THREE.Raycaster();
5618
+ _ray.layers.enableAll();
5425
5619
  function findSurface(c, p, dir) {
5426
5620
  c.root.updateMatrixWorld(true);
5427
5621
  const Mw = c.vehicle.matrixWorld;
@@ -5503,6 +5697,7 @@ function installImpact(kit) {
5503
5697
  const v0 = new V3();
5504
5698
  const w = new V3();
5505
5699
  const sc = new V3();
5700
+ const local = new V3();
5506
5701
  const cab = m.cabin && !c.flipped && !c.rollState ? m.cabin : null;
5507
5702
  c.deform.forEach(function(msh) {
5508
5703
  if (!msh.parent || !msh.visible) return;
@@ -5517,7 +5712,15 @@ function installImpact(kit) {
5517
5712
  irel.copy(rel).invert();
5518
5713
  if (!g.userData.p0) g.userData.p0 = new Float32Array(pos.array);
5519
5714
  const p0 = g.userData.p0;
5715
+ local.copy(p).applyMatrix4(irel);
5716
+ const reach = H.R / Math.max(1e-6, Math.min(sc.x, sc.y, sc.z));
5717
+ const reach2 = reach * reach;
5718
+ const arr = pos.array;
5520
5719
  for (let i = 0; i < pos.count; i++) {
5720
+ const dx = arr[3 * i] - local.x;
5721
+ const dy = arr[3 * i + 1] - local.y;
5722
+ const dz = arr[3 * i + 2] - local.z;
5723
+ if (dx * dx + dy * dy + dz * dz >= reach2) continue;
5521
5724
  v.fromBufferAttribute(pos, i).applyMatrix4(rel);
5522
5725
  const a = dentField(v, H);
5523
5726
  if (a <= 0) continue;
@@ -5533,10 +5736,15 @@ function installImpact(kit) {
5533
5736
  }
5534
5737
  if (changed) {
5535
5738
  pos.needsUpdate = true;
5536
- if (g.userData.crease) crease(g, 42);
5739
+ if (g.userData.crease && c.smoothLater) c.smoothLater(msh);
5740
+ else if (g.userData.crease) crease(g, 42);
5537
5741
  else g.computeVertexNormals();
5538
- g.computeBoundingSphere();
5539
5742
  g.computeBoundingBox();
5743
+ const sphere = g.boundingSphere ?? (g.boundingSphere = new THREE.Sphere());
5744
+ g.boundingBox.getCenter(sphere.center);
5745
+ let far2 = 0;
5746
+ for (let i = 0; i < pos.count; i++) far2 = Math.max(far2, sphere.center.distanceToSquared(v.fromBufferAttribute(pos, i)));
5747
+ sphere.radius = Math.sqrt(far2);
5540
5748
  }
5541
5749
  });
5542
5750
  const dset = new Set(c.deform);
@@ -5620,13 +5828,14 @@ function installImpact(kit) {
5620
5828
  kit.impact = impact;
5621
5829
  kit.partEffects = {};
5622
5830
  const _pr = new THREE.Raycaster();
5831
+ _pr.layers.enableAll();
5623
5832
  function probe(c, o, d, far) {
5624
5833
  c.root.updateMatrixWorld(true);
5625
5834
  const Mw = c.vehicle.matrixWorld;
5626
5835
  const inv = new THREE.Matrix4().copy(Mw).invert();
5627
5836
  _pr.set(o.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
5628
5837
  _pr.far = far || 10;
5629
- const hits = _pr.intersectObject(c.vehicle, true);
5838
+ const hits = raycastIndexed(THREE, _pr, [c.vehicle], true);
5630
5839
  for (let i = 0; i < hits.length; i++) {
5631
5840
  const h = hits[i];
5632
5841
  const ob = h.object;
@@ -6992,6 +7201,7 @@ function installZoneDecals(kit) {
6992
7201
  return hole;
6993
7202
  }
6994
7203
  const _zr = new THREE.Raycaster();
7204
+ _zr.layers.enableAll();
6995
7205
  const PROXY_MIN_TRIANGLES = 500;
6996
7206
  const STROKE_REACH = 0.6;
6997
7207
  const _pv = new V3();
@@ -7226,6 +7436,7 @@ function installZoneDecals(kit) {
7226
7436
  return out;
7227
7437
  }
7228
7438
  const _rt = new THREE.Raycaster();
7439
+ _rt.layers.enableAll();
7229
7440
  function scratchPath(c, s, len, ang, cands, B) {
7230
7441
  const n = s.n.clone().normalize();
7231
7442
  const t = Math.abs(n.y) < 0.9 ? new V3().crossVectors(new V3(0, 1, 0), n) : new V3().crossVectors(new V3(1, 0, 0), n);
@@ -10507,6 +10718,8 @@ function createVehicleKit(options) {
10507
10718
  V3: THREE.Vector3,
10508
10719
  PI: Math.PI,
10509
10720
  document: doc,
10721
+ particleMaterials: opts.particleMaterials,
10722
+ effects: opts.effects,
10510
10723
  strings,
10511
10724
  t,
10512
10725
  matchers: createMatchers(strings),
@@ -10762,7 +10975,9 @@ function createVehicleKit(options) {
10762
10975
  assignDeep(strings, resolveStrings(opts.locale));
10763
10976
  return kit;
10764
10977
  };
10765
- const { THREE: _three, lod: _lod, locale: _locale, document: _document, plugins, ...config } = opts;
10978
+ const { THREE: _three, lod: _lod, locale: _locale, document: _document, particleMaterials: _particleMaterials, effects: _effects, plugins, ...config } = opts;
10979
+ void _particleMaterials;
10980
+ void _effects;
10766
10981
  void _three;
10767
10982
  void _lod;
10768
10983
  void _locale;
@@ -10861,6 +11076,14 @@ var floorPan = (b) => ({
10861
11076
  center: [b.center[0], b.center[1] - b.size[1] / 2 + Math.min(0.14, b.size[1]) / 2 + 0.04, b.center[2]],
10862
11077
  size: [b.size[0] * 0.5, Math.min(0.14, b.size[1]), b.size[2] * 0.95]
10863
11078
  });
11079
+ function paneBox(c, pane, inverse) {
11080
+ pane.geometry.computeBoundingBox();
11081
+ const box = pane.geometry.boundingBox.clone().applyMatrix4(pane.matrixWorld);
11082
+ box.applyMatrix4(inverse);
11083
+ const center = [(box.min.x + box.max.x) / 2, (box.min.y + box.max.y) / 2, (box.min.z + box.max.z) / 2];
11084
+ const size = [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
11085
+ return { center, size };
11086
+ }
10864
11087
  function buildVehicleModel(kit, c, options = {}) {
10865
11088
  const kind = kindOf(c);
10866
11089
  const m = c.meta;
@@ -11001,6 +11224,16 @@ function buildVehicleModel(kit, c, options = {}) {
11001
11224
  tags: [role, ...tags]
11002
11225
  });
11003
11226
  }
11227
+ const claimed = new Set(zones.filter((z) => roleOfZone(z) === "glass").flatMap((z) => z.glass ?? []));
11228
+ c.root.updateMatrixWorld(true);
11229
+ const inverse = c.vehicle.matrixWorld.clone().invert();
11230
+ for (const pane of c.glass ?? []) {
11231
+ if (claimed.has(pane)) continue;
11232
+ const G = pane.userData.glass;
11233
+ const bb = paneBox(c, pane, inverse);
11234
+ const laminated = G?.type === "laminated" || m.bullet;
11235
+ add({ id: `pane_${G?.key ?? "glass"}`, role: "glass", name: String(G?.label ?? G?.key ?? "glass"), material: "glass", box: bb, mass: 10 * areaOf(bb.size), hp: laminated ? 4e3 : 450, panes: [pane], visual: false, tags: ["glass"] });
11236
+ }
11004
11237
  if (!hubZone) {
11005
11238
  const h = Math.max(0.15, belt - clearance);
11006
11239
  add({
@@ -11336,6 +11569,76 @@ function buildVehicleModel(kit, c, options = {}) {
11336
11569
  }
11337
11570
 
11338
11571
  // src/destructible/visual.ts
11572
+ var HOLE_DECAL_SCALE = 2.4;
11573
+ var DOOR_DEPTH = 0.07;
11574
+ var DOOR_EDGE_PIECES = 16;
11575
+ var DOOR_CARD_COLOR = "#3b3e42";
11576
+ var DOOR_LINER_COLOR = "#5a5e64";
11577
+ var DOOR_LINER_DARK = 0.15;
11578
+ var DOOR_STRIP_THIN = 0.05;
11579
+ var DOOR_STRIP_NEAR = 0.04;
11580
+ var DOOR_STRIP_OVER = 0.3;
11581
+ var DOOR_TOP_OVER_PANE = 0.03;
11582
+ var DOOR_STRIP_SUB = 0.02;
11583
+ var HOLE_TEXTURE_SIZE = 64;
11584
+ var DOOR_HANDLE_INSET = 0.12;
11585
+ var DOOR_HANDLE_MIN = 0.07;
11586
+ var DOOR_HANDLE_MAX = 0.24;
11587
+ var DOOR_HANDLE_THIN = 0.07;
11588
+ var DOOR_HANDLE_OUTSIDE = 0.1;
11589
+ var PANE_SEAT_OUT = 0.05;
11590
+ var PANE_SEAT_IN = -0.05;
11591
+ var PANE_SEAT_REACH = 0.14;
11592
+ var PANE_SEAT_SAMPLES = 24;
11593
+ var PANE_SEAT_LIFT = 4e-3;
11594
+ var PANE_FRAME_NEAR = 0.07;
11595
+ var DRAWN_MIRROR_SIZE = 0.3;
11596
+ var DRAWN_MIRROR_REACH = 0.4;
11597
+ var DRAWN_MIRROR_MOUNT = 0.2;
11598
+ var MIRROR_GLASS_FIT = 0.76;
11599
+ var MIRROR_GLASS_THIN = 0.04;
11600
+ var MIRROR_GLASS_MIN = 0.05;
11601
+ var MIRROR_GLASS_COLOR = "#59636a";
11602
+ var HOLE_RIM = { steel: "#b7bcc0", aluminium: "#d3d6d8", plastic: "#d9d6cc", rubber: "#141414", glass: "#eef3f5" };
11603
+ var holeMaterials = /* @__PURE__ */ new WeakMap();
11604
+ var holeDisc = null;
11605
+ function holeDiscGeometry(THREE) {
11606
+ holeDisc = holeDisc ?? new THREE.CircleGeometry(1, 12);
11607
+ return holeDisc;
11608
+ }
11609
+ function holeMaterial(kit, material) {
11610
+ const key = kit;
11611
+ const cache2 = holeMaterials.get(key) ?? /* @__PURE__ */ new Map();
11612
+ holeMaterials.set(key, cache2);
11613
+ const known = cache2.get(material);
11614
+ if (known) return known;
11615
+ const I = kit._;
11616
+ const THREE = kit.THREE;
11617
+ const canvas = kit.document?.createElement?.("canvas");
11618
+ const x = canvas?.getContext?.("2d");
11619
+ let made = I.MATERIALS.dark;
11620
+ if (x) {
11621
+ canvas.width = canvas.height = HOLE_TEXTURE_SIZE;
11622
+ const mid = HOLE_TEXTURE_SIZE / 2;
11623
+ x.fillStyle = HOLE_RIM[material] ?? HOLE_RIM.steel;
11624
+ x.beginPath();
11625
+ for (let i = 0; i < 18; i++) {
11626
+ const a = i / 18 * Math.PI * 2;
11627
+ const r = mid * (i % 2 ? 0.98 : 0.76);
11628
+ x.lineTo(mid + Math.cos(a) * r, mid + Math.sin(a) * r);
11629
+ }
11630
+ x.closePath();
11631
+ x.fill();
11632
+ x.fillStyle = "#050505";
11633
+ x.beginPath();
11634
+ x.arc(mid, mid, mid * 0.42, 0, Math.PI * 2);
11635
+ x.fill();
11636
+ const map = new THREE.CanvasTexture(canvas);
11637
+ made = new THREE.MeshStandardMaterial({ map, transparent: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2, roughness: 0.55, metalness: material === "steel" || material === "aluminium" ? 0.6 : 0 });
11638
+ }
11639
+ cache2.set(material, made);
11640
+ return made;
11641
+ }
11339
11642
  var VehicleVisuals = class {
11340
11643
  constructor(kit, c, model) {
11341
11644
  __publicField(this, "kit");
@@ -11353,14 +11656,23 @@ var VehicleVisuals = class {
11353
11656
  __publicField(this, "syncPlates", /* @__PURE__ */ new Map());
11354
11657
  /** Doors and lids whose own skin was cut from the body and hung on the hinge (true), or that had none to take and keep the plate (false). */
11355
11658
  __publicField(this, "skins", /* @__PURE__ */ new Map());
11659
+ /** How high each door's cut reaches. */
11660
+ __publicField(this, "doorTops", /* @__PURE__ */ new Map());
11356
11661
  __publicField(this, "lightsOn", true);
11357
11662
  /** Heavy body meshes whose triangle index is still to build (null once done). */
11358
11663
  __publicField(this, "warming");
11359
11664
  /** A panel was torn off since the last `pump`: that frame already paid for the cut, the smoothing starts on the next one. */
11360
11665
  __publicField(this, "tornThisFrame", false);
11666
+ /** A shared crease queue owns the smoothing: `pump` only closes the frame of the cut. */
11667
+ __publicField(this, "queued", false);
11668
+ /** A panel was torn and the queue has not asked yet: the first `pendingUnits(frame)` after it holds the work for that frame. */
11669
+ __publicField(this, "holdArmed", false);
11670
+ __publicField(this, "holdFrame", -1);
11361
11671
  /** Normal smoothing still running, by mesh: a mesh cut again gets a new task and the old one is dropped. */
11362
11672
  __publicField(this, "smoothing", /* @__PURE__ */ new Map());
11363
11673
  __publicField(this, "hood");
11674
+ ;
11675
+ c.smoothLater = (mesh) => this.smoothLater(mesh);
11364
11676
  this.kit = kit;
11365
11677
  this.c = c;
11366
11678
  this.model = model;
@@ -11369,6 +11681,8 @@ var VehicleVisuals = class {
11369
11681
  for (const p of model.parts) this.byId.set(p.id, p);
11370
11682
  c.root.updateMatrixWorld(true);
11371
11683
  this.hood = this.nameHood();
11684
+ this.seatPanes();
11685
+ this.seatDoorHandles();
11372
11686
  this.warming = c.deform.filter((msh) => msh.geometry && msh.geometry.attributes.position && msh.geometry.attributes.position.count >= 1500);
11373
11687
  if (!this.warming.length) this.warming = null;
11374
11688
  for (const p of model.parts) {
@@ -11389,6 +11703,7 @@ var VehicleVisuals = class {
11389
11703
  const id = z ? z.id : this.model.parts.find((p) => p.role === "hood")?.id;
11390
11704
  if (!id) return null;
11391
11705
  c.hood.piv.name = id;
11706
+ c.hood.piv.userData.plateOf = id;
11392
11707
  return { id, open: c.hood.open, sign: c.hood.sign || 1 };
11393
11708
  }
11394
11709
  mirrorObjects(p) {
@@ -11476,6 +11791,7 @@ var VehicleVisuals = class {
11476
11791
  recess.userData.noFollow = true;
11477
11792
  const piv = new THREE.Group();
11478
11793
  piv.name = p.id;
11794
+ piv.userData.plateOf = p.id;
11479
11795
  piv.position.set(hx, cy, sk + s * 0.016);
11480
11796
  piv.userData.rigid = true;
11481
11797
  this.c.vehicle.add(piv);
@@ -11492,6 +11808,7 @@ var VehicleVisuals = class {
11492
11808
  recess.userData.noFollow = true;
11493
11809
  const piv = new THREE.Group();
11494
11810
  piv.name = p.id;
11811
+ piv.userData.plateOf = p.id;
11495
11812
  piv.position.set(sk - 0.016, B.y1 - 0.05, 0);
11496
11813
  piv.userData.rigid = true;
11497
11814
  this.c.vehicle.add(piv);
@@ -11555,10 +11872,19 @@ var VehicleVisuals = class {
11555
11872
  try {
11556
11873
  if (p.role === "door") {
11557
11874
  const dirty = /* @__PURE__ */ new Set();
11558
- const rect = [B.x0 + 0.03, B.x1 - 0.03, B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1), B.y1 - 0.02];
11559
- const pcs = I.cutPanel(this.c, z, rect, true, dirty).map((x) => x.mesh).filter((m) => !!m);
11875
+ const glassBox = new this.THREE.Box3();
11876
+ for (const g of I.windowsOver(this.c, z, inv)) glassBox.union(I.localBox(this.c, g, inv));
11877
+ const top = glassBox.isEmpty() ? B.y1 - 0.02 : Math.max(B.y1 - 0.02, glassBox.min.y + DOOR_TOP_OVER_PANE);
11878
+ this.doorTops.set(id, top);
11879
+ const rect = [B.x0 + 0.03, B.x1 - 0.03, B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1), top];
11880
+ const pcs = I.cutPanel(this.c, z, rect, true, dirty, { anyN: true }).map((x) => x.mesh).filter((m) => !!m);
11560
11881
  this.mend(dirty);
11561
- if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv), ...I.windowsOver(this.c, z, inv)];
11882
+ if (pcs.length) {
11883
+ const panes = I.windowsOver(this.c, z, inv);
11884
+ const seals = panes.map((g) => I.paneSeals.get(g)).filter((s) => !!s);
11885
+ const mirrors = this.mirrorsOn(z);
11886
+ objs = [...pcs, ...this.doorCards(pcs, z), ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors];
11887
+ }
11562
11888
  } else {
11563
11889
  const pcs = this.cutZone(z);
11564
11890
  if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv)];
@@ -11576,10 +11902,124 @@ var VehicleVisuals = class {
11576
11902
  const di = deform.indexOf(o);
11577
11903
  if (di >= 0) deform.splice(di, 1);
11578
11904
  }
11905
+ if (p.role === "door" && objs.length > 0) {
11906
+ this.doorEdges(piv, z, this.doorTops.get(id) ?? B.y1 - 0.02);
11907
+ this.liftDoorTrim(objs, z);
11908
+ this.cutStripsAcross(piv, z, inv);
11909
+ }
11579
11910
  piv.quaternion.copy(q);
11580
11911
  this.c.root.updateMatrixWorld(true);
11581
11912
  return objs.length > 0;
11582
11913
  }
11914
+ /** A door has edges: a band that follows the skin all round closes it to its card and hides where the cut followed the triangles of the shell; the same band, in the same paint as on a real car, lines the opening. */
11915
+ doorEdges(piv, z, y1) {
11916
+ const THREE = this.THREE;
11917
+ const B = z.box;
11918
+ const side2 = z.side < 0 ? -1 : 1;
11919
+ const flat = piv.position.z - side2 * 0.016;
11920
+ const x0 = B.x0 + 0.03;
11921
+ const x1 = B.x1 - 0.03;
11922
+ const y0 = B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1);
11923
+ const loop = [];
11924
+ const run = (ax, ay, bx, by) => {
11925
+ for (let i = 0; i < DOOR_EDGE_PIECES; i++) loop.push([ax + (bx - ax) * i / DOOR_EDGE_PIECES, ay + (by - ay) * i / DOOR_EDGE_PIECES]);
11926
+ };
11927
+ run(x0, y0, x1, y0);
11928
+ run(x1, y0, x1, y1);
11929
+ run(x1, y1, x0, y1);
11930
+ run(x0, y1, x0, y0);
11931
+ const heights = loop.map(([x, y]) => this.surface("z", side2, B, [[x, y]]) ?? flat);
11932
+ const band = (lift) => {
11933
+ const position = [];
11934
+ const index = [];
11935
+ loop.forEach(([x, y], i) => {
11936
+ position.push(x, y, heights[i] + side2 * lift, x, y, heights[i] + side2 * (lift - DOOR_DEPTH));
11937
+ const a = i * 2;
11938
+ const b = (i + 1) % loop.length * 2;
11939
+ index.push(a, a + 1, b, b, a + 1, b + 1);
11940
+ });
11941
+ const g = new THREE.BufferGeometry();
11942
+ g.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
11943
+ g.setIndex(index);
11944
+ g.computeVertexNormals();
11945
+ return g;
11946
+ };
11947
+ const paint = this.paint().clone();
11948
+ paint.side = THREE.DoubleSide;
11949
+ const edges = new THREE.Mesh(band(2e-3), paint);
11950
+ const lining = new THREE.Mesh(band(2e-3), paint);
11951
+ for (const m of [edges, lining]) {
11952
+ m.userData.noFollow = true;
11953
+ m.castShadow = false;
11954
+ this.c.vehicle.add(m);
11955
+ }
11956
+ piv.attach(edges);
11957
+ }
11958
+ /** A front door carries the mirror drawn at its corner. */
11959
+ mirrorsOn(z) {
11960
+ if (!z.mirror) return [];
11961
+ const own = (z.mirrors ?? []).filter((o) => o.parent);
11962
+ if (own.length) return own;
11963
+ const part = this.model.parts.find((q) => q.role === "mirror" && Math.sign(q.box.center[2]) === Math.sign(z.side));
11964
+ return part ? (this.extra.get(part.id) ?? []).filter((o) => o.parent) : [];
11965
+ }
11966
+ /** The panel drawn inside the door is lit like the rest of a cabin, not a black slab from end to end. */
11967
+ liftDoorTrim(objs, z) {
11968
+ const card = this.I.makeMaterial(DOOR_LINER_COLOR, { roughness: 0.85 });
11969
+ const inv = this.I.worldInverseOf(this.c);
11970
+ for (const o of objs) {
11971
+ if (!o.isMesh || o.userData.glass) continue;
11972
+ const box = this.I.localBox(this.c, o, inv);
11973
+ const size = box.getSize(new this.THREE.Vector3());
11974
+ if (size.x > (z.box.x1 - z.box.x0) * 0.4 && size.y > (z.box.y1 - z.box.y0) * 0.3 && size.z < DOOR_DEPTH * 1.5 && o.material?.color?.getHSL({ h: 0, s: 0, l: 0 }).l < DOOR_LINER_DARK) o.material = card;
11975
+ }
11976
+ }
11977
+ /** A long strip drawn along the side of the car (the chrome of the belt): the part over the door goes with the door, and nothing stays across the opening. */
11978
+ cutStripsAcross(piv, z, inv) {
11979
+ const THREE = this.THREE;
11980
+ const B = z.box;
11981
+ const x0 = B.x0 + 0.03;
11982
+ const x1 = B.x1 - 0.03;
11983
+ const y0 = B.y0 - DOOR_STRIP_NEAR;
11984
+ const y1 = B.y1 + DOOR_STRIP_NEAR;
11985
+ const deform = this.c.deform ?? [];
11986
+ for (const o of [...this.c.vehicle.children]) {
11987
+ if (!o.isMesh || o === piv || deform.includes(o) || o.userData.glass || o.userData.decal || !o.visible) continue;
11988
+ const box = this.I.localBox(this.c, o, inv);
11989
+ const size = box.getSize(new THREE.Vector3());
11990
+ const mid = box.getCenter(new THREE.Vector3());
11991
+ if (Math.sign(mid.z) !== Math.sign(z.side) || size.z > DOOR_STRIP_THIN || size.y > DOOR_STRIP_THIN || Math.min(box.max.x, x1) - Math.max(box.min.x, x0) < (x1 - x0) * DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
11992
+ const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
11993
+ const v = new THREE.Vector3();
11994
+ const sdf = (x, y, zz) => {
11995
+ v.set(x, y, zz).applyMatrix4(toVehicle);
11996
+ return Math.max(x0 - v.x, v.x - x1);
11997
+ };
11998
+ this.kit.cutMesh(o, [{ sdf }], 42, { sub: DOOR_STRIP_SUB });
11999
+ const from = Math.max(box.min.x, x0);
12000
+ const to = Math.min(box.max.x, x1);
12001
+ const piece = this.box(to - from, size.y, size.z, o.material, (from + to) / 2, mid.y, mid.z);
12002
+ piece.userData.noFollow = true;
12003
+ piv.attach(piece);
12004
+ }
12005
+ }
12006
+ /** The inside of a door: its skin brought in by the depth of the door, in the trim's dark plastic, so the open door is a door and not one sheet of paint. */
12007
+ doorCards(skins, z) {
12008
+ const cardMaterial = this.I.makeMaterial(DOOR_CARD_COLOR, { roughness: 0.8 });
12009
+ return skins.map((skin) => {
12010
+ const card = new this.THREE.Mesh(skin.geometry, cardMaterial);
12011
+ card.position.copy(skin.position);
12012
+ card.quaternion.copy(skin.quaternion);
12013
+ card.scale.copy(skin.scale);
12014
+ const side2 = z.side < 0 ? -1 : 1;
12015
+ const inward = -side2 * DOOR_DEPTH;
12016
+ card.position.z += inward;
12017
+ card.userData.noFollow = true;
12018
+ card.castShadow = false;
12019
+ this.c.vehicle.add(card);
12020
+ return card;
12021
+ });
12022
+ }
11583
12023
  mend(dirty) {
11584
12024
  dirty.forEach((msh) => {
11585
12025
  const g = msh.geometry;
@@ -11589,7 +12029,191 @@ var VehicleVisuals = class {
11589
12029
  g.computeBoundingBox();
11590
12030
  });
11591
12031
  }
12032
+ /**
12033
+ * A windshield or rear window is drawn as a flat quad, and the body under its edge is curved: the pane and its frame stand off the shell or rise past the roof. Each one
12034
+ * comes to the shell along its own normal, by how far the shell is just outside each of its vertices.
12035
+ */
12036
+ seatPanes() {
12037
+ const THREE = this.THREE;
12038
+ const c = this.c;
12039
+ c.root.updateMatrixWorld(true);
12040
+ const shell = c.deform.filter((o) => o.isMesh && !o.userData.glass && c.paints?.has(o.material));
12041
+ if (!shell.length) return;
12042
+ const ray = new THREE.Raycaster();
12043
+ ray.layers.enableAll();
12044
+ for (const pane of c.glass) {
12045
+ if (pane.userData.glass?.outline) continue;
12046
+ const seat = this.paneSeat(pane, shell, ray);
12047
+ if (!seat) continue;
12048
+ const near = c.vehicle.children.filter((o) => o.isMesh && o !== pane && !o.userData.glass && !shell.includes(o) && this.onPane(o, pane, seat));
12049
+ for (const m of [pane, ...near]) this.movePane(m, seat);
12050
+ const past = this.shellPast(pane, shell, ray, seat.normal);
12051
+ if (past > 0) for (const m of [pane, ...near]) this.movePane(m, { normal: seat.normal, samples: [{ at: seat.samples[0].at, by: past }] });
12052
+ }
12053
+ }
12054
+ /** The outward normal of a pane and how far the shell is at each sampled point (world). */
12055
+ paneSeat(pane, shell, ray) {
12056
+ const THREE = this.THREE;
12057
+ const pos = pane.geometry.attributes.position;
12058
+ const box = new THREE.Box3().setFromObject(pane);
12059
+ const centre = box.getCenter(new THREE.Vector3());
12060
+ const vehicleCentre = new THREE.Box3().setFromObject(this.c.root).getCenter(new THREE.Vector3());
12061
+ const tri = new THREE.Triangle();
12062
+ const normal = new THREE.Vector3();
12063
+ let largest = 0;
12064
+ const idx = pane.geometry.index;
12065
+ const count = idx ? idx.count : pos.count;
12066
+ for (let f = 0; f + 2 < count; f += 3) {
12067
+ tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
12068
+ tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
12069
+ tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
12070
+ if (tri.getArea() > largest) {
12071
+ largest = tri.getArea();
12072
+ tri.getNormal(normal);
12073
+ }
12074
+ }
12075
+ normal.transformDirection(pane.matrixWorld);
12076
+ if (normal.dot(centre.clone().sub(vehicleCentre)) < 0) normal.negate();
12077
+ const samples = [];
12078
+ const seen = /* @__PURE__ */ new Set();
12079
+ const v = new THREE.Vector3();
12080
+ const step = Math.max(1, Math.floor(pos.count / PANE_SEAT_SAMPLES));
12081
+ for (let i = 0; i < pos.count; i += step) {
12082
+ v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
12083
+ const key = `${Math.round(v.x * 100)},${Math.round(v.y * 100)},${Math.round(v.z * 100)}`;
12084
+ if (seen.has(key)) continue;
12085
+ seen.add(key);
12086
+ const away = v.clone().sub(centre);
12087
+ away.addScaledVector(normal, -away.dot(normal)).normalize();
12088
+ let by = -Infinity;
12089
+ for (const off of [PANE_SEAT_OUT, PANE_SEAT_IN, PANE_SEAT_IN * 3]) {
12090
+ const at = v.clone().addScaledVector(away, off);
12091
+ ray.set(at.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
12092
+ ray.far = PANE_SEAT_REACH * 2;
12093
+ const hit = raycastIndexed(THREE, ray, shell)[0];
12094
+ if (hit) by = Math.max(by, PANE_SEAT_REACH - hit.distance);
12095
+ }
12096
+ if (by > -Infinity) samples.push({ at: v.clone(), by });
12097
+ }
12098
+ return samples.length >= 3 ? { normal, samples } : null;
12099
+ }
12100
+ /** How far the shell still stands past a seated pane, over its triangles. */
12101
+ shellPast(pane, shell, ray, normal) {
12102
+ const THREE = this.THREE;
12103
+ const pos = pane.geometry.attributes.position;
12104
+ const idx = pane.geometry.index;
12105
+ const count = idx ? idx.count : pos.count;
12106
+ const tri = new THREE.Triangle();
12107
+ const step = Math.max(1, Math.floor(count / 3 / PANE_SEAT_SAMPLES)) * 3;
12108
+ let past = 0;
12109
+ for (let f = 0; f + 2 < count; f += step) {
12110
+ tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
12111
+ tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
12112
+ tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
12113
+ const mid = tri.getMidpoint(new THREE.Vector3()).applyMatrix4(pane.matrixWorld);
12114
+ ray.set(mid.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
12115
+ ray.far = PANE_SEAT_REACH;
12116
+ const hit = raycastIndexed(THREE, ray, shell)[0];
12117
+ if (hit) past = Math.max(past, PANE_SEAT_REACH - hit.distance + PANE_SEAT_LIFT);
12118
+ }
12119
+ return past;
12120
+ }
12121
+ /** A frame or a seal drawn on the pane: small, and close to its plane. */
12122
+ onPane(o, pane, seat) {
12123
+ const THREE = this.THREE;
12124
+ const box = new THREE.Box3().setFromObject(o);
12125
+ if (box.isEmpty() || !new THREE.Box3().setFromObject(pane).expandByScalar(PANE_FRAME_NEAR).containsBox(box)) return false;
12126
+ const at = box.getCenter(new THREE.Vector3());
12127
+ return Math.abs(at.clone().sub(seat.samples[0].at).dot(seat.normal)) < PANE_SEAT_REACH;
12128
+ }
12129
+ movePane(mesh, seat) {
12130
+ const THREE = this.THREE;
12131
+ const g = mesh.geometry.clone();
12132
+ const pos = g.attributes.position;
12133
+ const inverse = mesh.matrixWorld.clone().invert();
12134
+ const v = new THREE.Vector3();
12135
+ for (let i = 0; i < pos.count; i++) {
12136
+ v.fromBufferAttribute(pos, i).applyMatrix4(mesh.matrixWorld);
12137
+ let weight = 0;
12138
+ let by = 0;
12139
+ for (const s of seat.samples) {
12140
+ const w = 1 / (s.at.distanceToSquared(v) + 1e-4);
12141
+ weight += w;
12142
+ by += s.by * w;
12143
+ }
12144
+ v.addScaledVector(seat.normal, by / weight + PANE_SEAT_LIFT).applyMatrix4(inverse);
12145
+ pos.setXYZ(i, v.x, v.y, v.z);
12146
+ }
12147
+ pos.needsUpdate = true;
12148
+ g.computeBoundingBox();
12149
+ g.computeBoundingSphere();
12150
+ mesh.geometry = g;
12151
+ }
12152
+ /** A handle is at the free edge of its door, the end away from the hinge. */
12153
+ seatDoorHandles() {
12154
+ const THREE = this.THREE;
12155
+ const c = this.c;
12156
+ const inv = this.I.worldInverseOf(c);
12157
+ for (const p of this.model.parts) {
12158
+ if (p.role !== "door") continue;
12159
+ const z = this.zone(p);
12160
+ if (!z) continue;
12161
+ const B = z.box;
12162
+ const free = z.hinge === "rear" ? B.x1 - DOOR_HANDLE_INSET : B.x0 + DOOR_HANDLE_INSET;
12163
+ for (const o of c.vehicle.children) {
12164
+ if (!o.isMesh || o.material !== this.I.MATERIALS.chrome || o.userData.glass) continue;
12165
+ const box = this.I.localBox(c, o, inv);
12166
+ const size = box.getSize(new THREE.Vector3());
12167
+ const at = box.getCenter(new THREE.Vector3());
12168
+ const isHandle = size.x > DOOR_HANDLE_MIN && size.x < DOOR_HANDLE_MAX && size.y < DOOR_HANDLE_THIN && size.z < DOOR_HANDLE_THIN;
12169
+ if (!isHandle || Math.sign(at.z) !== Math.sign(z.side) || at.x < B.x0 || at.x > B.x1 || at.y < B.y0 || at.y > B.y1) continue;
12170
+ const here = this.surface("z", z.side < 0 ? -1 : 1, B, [[at.x, at.y]]);
12171
+ if (here != null && Math.abs(here - at.z) > DOOR_HANDLE_OUTSIDE) continue;
12172
+ const skin = this.surface("z", z.side < 0 ? -1 : 1, B, [[free, at.y]]);
12173
+ o.position.x += free - at.x;
12174
+ if (skin != null) o.position.z += skin + (z.side < 0 ? -1 : 1) * (size.z / 2) - at.z;
12175
+ }
12176
+ }
12177
+ c.root.updateMatrixWorld(true);
12178
+ }
12179
+ /** The glass of a drawn mirror sits inside its rounded housing, without corners standing past it. */
12180
+ seatMirrorGlass(glass) {
12181
+ const g = glass.geometry.clone();
12182
+ g.computeBoundingBox();
12183
+ const mid = g.boundingBox.getCenter(new this.THREE.Vector3());
12184
+ g.translate(-mid.x, -mid.y, -mid.z);
12185
+ g.scale(MIRROR_GLASS_FIT, MIRROR_GLASS_FIT, MIRROR_GLASS_FIT);
12186
+ g.translate(mid.x, mid.y, mid.z);
12187
+ glass.geometry = g;
12188
+ glass.material = this.I.makeMaterial(MIRROR_GLASS_COLOR, { metalness: 0.95, roughness: 0.12 });
12189
+ }
12190
+ /** The mirror the model already draws around where the part says, if it draws one. */
12191
+ drawnMirror(p) {
12192
+ const THREE = this.THREE;
12193
+ const [cx, cy, cz] = p.box.center;
12194
+ this.c.root.updateMatrixWorld(true);
12195
+ const inv = this.I.worldInverseOf(this.c);
12196
+ const small = this.c.vehicle.children.flatMap((o) => {
12197
+ if (o.userData.glass || o.userData.decal || !(o.isMesh || o.isGroup) || !o.visible || this.c.deform?.includes(o)) return [];
12198
+ const box = this.I.localBox(this.c, o, inv);
12199
+ if (box.isEmpty()) return [];
12200
+ const size = box.getSize(new THREE.Vector3());
12201
+ return Math.max(size.x, size.y, size.z) < DRAWN_MIRROR_SIZE ? [{ o, at: box.getCenter(new THREE.Vector3()) }] : [];
12202
+ });
12203
+ const housing = small.filter(({ at }) => Math.hypot(at.x - cx, at.y - cy, at.z - cz) < DRAWN_MIRROR_REACH && Math.abs(at.z) > Math.abs(cz) - DRAWN_MIRROR_REACH / 2);
12204
+ return small.filter(({ at }) => housing.some((h) => h.at.distanceTo(at) < DRAWN_MIRROR_MOUNT)).map(({ o }) => o);
12205
+ }
11592
12206
  makeMirror(p) {
12207
+ const drawn = this.drawnMirror(p);
12208
+ if (drawn.length) {
12209
+ for (const o of drawn) {
12210
+ if (!o.isMesh || o.material !== this.I.MATERIALS.chrome) continue;
12211
+ const size = new this.THREE.Box3().setFromObject(o).getSize(new this.THREE.Vector3());
12212
+ if (size.x < MIRROR_GLASS_THIN && size.y > MIRROR_GLASS_MIN && size.z > MIRROR_GLASS_MIN) this.seatMirrorGlass(o);
12213
+ }
12214
+ this.extra.set(p.id, drawn);
12215
+ return;
12216
+ }
11593
12217
  const [cx, cy, cz] = p.box.center;
11594
12218
  const s = cz < 0 ? -1 : 1;
11595
12219
  const mats = this.I.MATERIALS;
@@ -11630,17 +12254,28 @@ var VehicleVisuals = class {
11630
12254
  }
11631
12255
  // ---- marks -------------------------------------------------------------------------------------------------------------------------------
11632
12256
  /** A bullet hole: a dark disc lying on the surface at `point` (`c.vehicle` coordinates). */
11633
- holeDecal(point, normal, diameter) {
12257
+ holeDecal(point, normal, diameter, part) {
11634
12258
  const THREE = this.THREE;
11635
- const m = new THREE.Mesh(new THREE.CircleGeometry(Math.max(6e-3, diameter / 2), 10), this.I.MATERIALS.dark);
12259
+ const pane = part?.role === "glass" ? this.panesOf(part).find((g) => g.visible) : void 0;
12260
+ if (part?.role === "glass" && (!pane || pane.userData.glass?.broken)) return;
12261
+ const radius = Math.max(6e-3, diameter / 2) * HOLE_DECAL_SCALE;
12262
+ const m = new THREE.Mesh(holeDiscGeometry(THREE), holeMaterial(this.kit, part?.material ?? "steel"));
12263
+ m.scale.set(radius, radius, 1);
11636
12264
  const n = new THREE.Vector3(...normal);
11637
- m.position.set(...point).addScaledVector(n, 4e-3);
11638
- m.lookAt(m.position.clone().add(n));
12265
+ m.position.set(...point).addScaledVector(n, 3e-3);
12266
+ this.c.vehicle.add(m);
12267
+ m.lookAt(this.c.vehicle.localToWorld(m.position.clone().add(n)));
11639
12268
  m.userData.decal = true;
11640
12269
  m.userData.noFollow = true;
12270
+ m.renderOrder = 3;
11641
12271
  m.raycast = () => {
11642
12272
  };
11643
- this.c.vehicle.add(m);
12273
+ const carrier = (part && (this.plates.get(part.id) ?? (part.id === this.hood?.id ? this.c.hood?.piv : void 0))) ?? (pane && pane.parent !== this.c.vehicle ? pane.parent : null);
12274
+ if (carrier) carrier.attach(m);
12275
+ }
12276
+ /** The door or lid a hinge group stands for. */
12277
+ plateIdOf(o) {
12278
+ return o.userData.plateOf;
11644
12279
  }
11645
12280
  spark(point, dir, count = 8) {
11646
12281
  const V3 = this.THREE.Vector3;
@@ -11680,10 +12315,8 @@ var VehicleVisuals = class {
11680
12315
  }
11681
12316
  /** Glass: cracks (laminated) or shatters (tempered); returns whether it broke. */
11682
12317
  hitGlass(p, point, dir, force) {
11683
- const z = this.zone(p);
11684
- if (!z) return false;
11685
12318
  const V3 = this.THREE.Vector3;
11686
- const list = z.glass.filter((g) => g.parent && g.visible);
12319
+ const list = this.panesOf(p).filter((g) => g.parent && g.visible);
11687
12320
  if (!list.length) return false;
11688
12321
  const pt = new V3(...point);
11689
12322
  let best = list[0];
@@ -11700,9 +12333,16 @@ var VehicleVisuals = class {
11700
12333
  this.kit.hitGlass(this.c, best, pt, new V3(...dir), force);
11701
12334
  return !!best.userData.glass?.broken;
11702
12335
  }
12336
+ panesOf(p) {
12337
+ return p.panes ?? this.zone(p)?.glass ?? [];
12338
+ }
11703
12339
  glassBreak(p) {
11704
12340
  const z = this.zone(p);
11705
- if (!z) return;
12341
+ if (!z) {
12342
+ for (const g of p.panes ?? []) if (g.parent && g.visible) this.kit.hitGlass(this.c, g, g.position.clone(), new this.THREE.Vector3(0, -1, 0), 0.6);
12343
+ this.gone.add(p.id);
12344
+ return;
12345
+ }
11706
12346
  for (const g of z.glass) if (g.parent && g.visible) this.I.glassBand(this.c, z, g, 4);
11707
12347
  this.gone.add(p.id);
11708
12348
  }
@@ -11730,6 +12370,7 @@ var VehicleVisuals = class {
11730
12370
  smoothLater(msh) {
11731
12371
  this.smoothing.set(msh, this.I.creaseTask(msh.geometry, 42));
11732
12372
  this.tornThisFrame = true;
12373
+ this.holdArmed = true;
11733
12374
  }
11734
12375
  /** Spends at most `budgetMs` finishing the pending normal smoothing. Call once per frame. */
11735
12376
  pump(budgetMs = 2.5) {
@@ -11737,6 +12378,7 @@ var VehicleVisuals = class {
11737
12378
  this.tornThisFrame = false;
11738
12379
  return;
11739
12380
  }
12381
+ if (this.queued) return;
11740
12382
  if (!this.smoothing.size && !this.warming) return;
11741
12383
  const deadline = performance.now() + budgetMs;
11742
12384
  for (const [msh, task] of this.smoothing) {
@@ -11745,6 +12387,34 @@ var VehicleVisuals = class {
11745
12387
  }
11746
12388
  this.warmIndexes(deadline);
11747
12389
  }
12390
+ setQueued(isQueued) {
12391
+ this.queued = isQueued;
12392
+ }
12393
+ /** Units the shared queue may run now: 0 in the frame of the cut (the first ask after it holds the work for that frame, or `pump` closes it when there is no frame). */
12394
+ pendingUnits(frame) {
12395
+ if (this.holdArmed && frame !== void 0) {
12396
+ this.holdArmed = false;
12397
+ this.holdFrame = frame;
12398
+ }
12399
+ if (frame !== void 0 && frame === this.holdFrame) return 0;
12400
+ if (frame === void 0 && this.tornThisFrame) return 0;
12401
+ return this.smoothing.size + (this.warming ? 1 : 0);
12402
+ }
12403
+ /** One slice of the pending work, for the shared queue. */
12404
+ runStep(deadline) {
12405
+ const next = this.smoothing.entries().next();
12406
+ if (!next.done) {
12407
+ const [msh, task] = next.value;
12408
+ if (msh.geometry !== task.geo || task.step(deadline)) this.smoothing.delete(msh);
12409
+ return true;
12410
+ }
12411
+ if (!this.warming) return false;
12412
+ this.warmIndexes(deadline);
12413
+ return true;
12414
+ }
12415
+ finishAll() {
12416
+ while (this.runStep(Infinity)) ;
12417
+ }
11748
12418
  /** Measures the triangles of the heavy body meshes once, a slice per frame, so the first panel torn off finds them ready (see `triIndexOf`). */
11749
12419
  warmIndexes(deadline) {
11750
12420
  if (!this.warming) return;
@@ -11916,6 +12586,7 @@ var VehicleVisuals = class {
11916
12586
  }
11917
12587
  pieceTasks.forEach((task, mesh) => this.smoothing.set(mesh, task));
11918
12588
  this.tornThisFrame = true;
12589
+ this.holdArmed = true;
11919
12590
  dirty.forEach((msh) => {
11920
12591
  const g = msh.geometry;
11921
12592
  if (g.userData.crease || !g.index) this.smoothLater(msh);
@@ -12022,6 +12693,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12022
12693
  let clock = 0;
12023
12694
  let model;
12024
12695
  let vis = null;
12696
+ let releaseCrease;
12025
12697
  const ps = /* @__PURE__ */ new Map();
12026
12698
  let js = [];
12027
12699
  let leaks = /* @__PURE__ */ new Map();
@@ -12106,7 +12778,9 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12106
12778
  rand = rng(seedBase);
12107
12779
  igniteRand = rng(strHash(`${c.key}|${id}|ignition|${options.seed ?? 0}`));
12108
12780
  steerSign = rand() < 0.5 ? -1 : 1;
12781
+ releaseCrease?.();
12109
12782
  vis = headless ? null : new VehicleVisuals(kit, c, model);
12783
+ releaseCrease = vis && options.creaseQueue ? options.creaseQueue.track(vis) : void 0;
12110
12784
  recomputeGroups();
12111
12785
  };
12112
12786
  const recomputeGroups = () => {
@@ -12417,13 +13091,13 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12417
13091
  sync();
12418
13092
  const M = toFrameMatrix();
12419
13093
  const v = toLocal(h.point);
12420
- const p = locate(v, 0.2);
13094
+ const p = (h.part ? ps.get(h.part) : void 0) ?? locate(v, 0.2);
12421
13095
  if (!p) return;
12422
13096
  const d = p.def;
12423
13097
  const E = h.energy ?? 0;
12424
13098
  const cal = h.caliber ?? 9;
12425
13099
  const dir = dirToLocal(h.direction ?? [0, 0, -1]).normalize();
12426
- const n = normalOf(d, v);
13100
+ const n = h.normal ? dirToLocal(h.normal).normalize() : normalOf(d, v);
12427
13101
  const res = resist(d, p);
12428
13102
  const through = E >= res;
12429
13103
  const cosInc = Math.abs(dir.dot(n));
@@ -12442,7 +13116,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12442
13116
  const hp = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), diameter: r2(holeDiam), through };
12443
13117
  p.holes.push(hp);
12444
13118
  out.push(ev({ type: "hole", part: d.id, hole: hp, worldPoint, ...through ? { exitPoint: add3(worldPoint, r3(dirToWorld(dir.clone().multiplyScalar(Math.max(0.02, Math.min(d.box.size[0], d.box.size[1], d.box.size[2])))))) } : {} }));
12445
- vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam);
13119
+ vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam, d);
12446
13120
  startLeak(p, v, cal, out, dir);
12447
13121
  };
12448
13122
  switch (d.material) {
@@ -12743,6 +13417,46 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12743
13417
  for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT)) return true;
12744
13418
  return false;
12745
13419
  };
13420
+ const surfaceRays = new THREE.Raycaster();
13421
+ surfaceRays.layers.enableAll();
13422
+ surfaceRays.camera = new THREE.PerspectiveCamera();
13423
+ let meshParts = null;
13424
+ const paneParts = () => {
13425
+ if (meshParts) return meshParts;
13426
+ meshParts = /* @__PURE__ */ new Map();
13427
+ for (const d of model.parts) {
13428
+ if (d.role !== "glass") continue;
13429
+ const z = kit.zonesOf(c).find((q) => q.id === d.zoneId);
13430
+ for (const g of z?.glass ?? d.panes ?? []) meshParts.set(g, d.id);
13431
+ }
13432
+ return meshParts;
13433
+ };
13434
+ const isDrawnSurface = (o) => {
13435
+ for (let q = o; q && q !== c.vehicle; q = q.parent) if (!q.visible || q.userData.decal || q.userData.ghost) return false;
13436
+ return true;
13437
+ };
13438
+ const partOfSurface = (o, v) => {
13439
+ const pane = paneParts().get(o);
13440
+ if (pane) return ps.get(pane) ?? null;
13441
+ for (let q = o; q && q !== c.vehicle; q = q.parent) {
13442
+ if (vis?.plateIdOf(q)) return ps.get(vis.plateIdOf(q)) ?? null;
13443
+ }
13444
+ return locate(v, 0.08, (p) => p.def.role === "glass");
13445
+ };
13446
+ const surfaceRay = (ray) => {
13447
+ sync();
13448
+ surfaceRays.set(new V3(ray.origin[0], ray.origin[1], ray.origin[2]), new V3(ray.direction[0], ray.direction[1], ray.direction[2]));
13449
+ surfaceRays.far = ray.maxDistance;
13450
+ for (const h of surfaceRays.intersectObject(c.vehicle, true)) {
13451
+ if (!h.face || !isDrawnSurface(h.object)) continue;
13452
+ const p = partOfSurface(h.object, c.vehicle.worldToLocal(h.point.clone()));
13453
+ if (!p || p.lost || ray.ignored?.(p.def.id)) continue;
13454
+ const n = h.face.normal.clone().transformDirection(h.object.matrixWorld);
13455
+ if (n.dot(surfaceRays.ray.direction) > 0) n.negate();
13456
+ return { part: p.def.id, point: [h.point.x, h.point.y, h.point.z], normal: [n.x, n.y, n.z], distance: h.distance, pathLen: 0.02 };
13457
+ }
13458
+ return null;
13459
+ };
12746
13460
  const hit = (h) => {
12747
13461
  stateCache = void 0;
12748
13462
  const out = [];
@@ -12865,9 +13579,14 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12865
13579
  },
12866
13580
  describe,
12867
13581
  hit,
13582
+ surfaceRay,
12868
13583
  update,
12869
13584
  state,
12870
13585
  reset,
13586
+ dispose: () => {
13587
+ releaseCrease?.();
13588
+ releaseCrease = void 0;
13589
+ },
12871
13590
  disassemble,
12872
13591
  extinguish,
12873
13592
  serialize: () => ({
@@ -12906,6 +13625,62 @@ function buildVehicleItem(kit, key, options = {}) {
12906
13625
  return { root, vehicle };
12907
13626
  }
12908
13627
 
13628
+ // src/destructible/crease-queue.ts
13629
+ import { DEFERRED_QUEUE_ID } from "@adatechnology/game-contracts";
13630
+ function createCreaseQueue() {
13631
+ const tracked = [];
13632
+ let cursor = 0;
13633
+ let lastFrame;
13634
+ let attached = false;
13635
+ const deferred = {
13636
+ id: DEFERRED_QUEUE_ID.VEHICLE_CREASE,
13637
+ pending(frame) {
13638
+ lastFrame = frame;
13639
+ let units = 0;
13640
+ for (const work of tracked) units += work.pendingUnits(frame);
13641
+ return units;
13642
+ },
13643
+ run(deadline, clock) {
13644
+ let ran = 0;
13645
+ let skipped = 0;
13646
+ while (tracked.length > 0 && skipped < tracked.length) {
13647
+ if (ran > 0 && clock() >= deadline) break;
13648
+ const work = tracked[cursor % tracked.length];
13649
+ cursor = (cursor + 1) % tracked.length;
13650
+ if (work.pendingUnits(lastFrame) > 0 && work.runStep(deadline)) {
13651
+ ran++;
13652
+ skipped = 0;
13653
+ } else skipped++;
13654
+ }
13655
+ return ran;
13656
+ },
13657
+ flush() {
13658
+ for (const work of tracked) work.finishAll();
13659
+ },
13660
+ attach() {
13661
+ attached = true;
13662
+ for (const work of tracked) work.setQueued(true);
13663
+ },
13664
+ detach() {
13665
+ attached = false;
13666
+ for (const work of tracked) work.setQueued(false);
13667
+ }
13668
+ };
13669
+ return {
13670
+ deferredQueue: () => deferred,
13671
+ track(work) {
13672
+ tracked.push(work);
13673
+ work.setQueued(attached);
13674
+ return () => {
13675
+ const at = tracked.indexOf(work);
13676
+ if (at < 0) return;
13677
+ tracked.splice(at, 1);
13678
+ work.setQueued(false);
13679
+ };
13680
+ }
13681
+ };
13682
+ }
13683
+
12909
13684
  // src/destructible/enclosure.ts
12910
13685
  var CABIN_LEAK = 0.02;
12911
13686
  var HOLE_RATE_PER_AREA = 3e3;
@@ -13000,6 +13775,8 @@ export {
13000
13775
  cabinBoxOf,
13001
13776
  cabinEnclosure,
13002
13777
  clamp,
13778
+ crackedGlassMaterial,
13779
+ createCreaseQueue,
13003
13780
  createLod,
13004
13781
  createTranslate,
13005
13782
  createVehicleDestructible,