@adatechnology/game-destruction-three 0.1.1 → 0.2.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
@@ -1255,6 +1255,8 @@ var Visuals = class {
1255
1255
  __publicField(this, "b");
1256
1256
  __publicField(this, "m4");
1257
1257
  __publicField(this, "box");
1258
+ __publicField(this, "t");
1259
+ __publicField(this, "lbox");
1258
1260
  /** Total live decals across meshes (kept in sync by add/remove). */
1259
1261
  __publicField(this, "decalCount", 0);
1260
1262
  __publicField(this, "tints", /* @__PURE__ */ new WeakMap());
@@ -1263,9 +1265,22 @@ var Visuals = class {
1263
1265
  this.b = tmpV(THREE);
1264
1266
  this.m4 = new THREE.Matrix4();
1265
1267
  this.box = new THREE.Box3();
1268
+ this.t = tmpV(THREE);
1269
+ this.lbox = new THREE.Box3();
1266
1270
  }
1267
- makeMeshVis(mesh, home) {
1268
- return {
1271
+ /**
1272
+ * @param range vertex range `[v0, v1)` of a merged mesh this part is drawn by (see `MeshRange`); `hosts` caches the shared state per mesh for one `bind`.
1273
+ */
1274
+ makeMeshVis(mesh, home, range, hosts) {
1275
+ let host = null;
1276
+ if (range) {
1277
+ host = hosts?.get(mesh) ?? null;
1278
+ if (!host) {
1279
+ host = { mesh, origGeometry: mesh.geometry, origMaterial: mesh.material, ownGeo: false, work: null, ownMat: false, sharers: [] };
1280
+ hosts?.set(mesh, host);
1281
+ }
1282
+ }
1283
+ const mv = {
1269
1284
  mesh,
1270
1285
  home,
1271
1286
  origGeometry: mesh.geometry,
@@ -1282,31 +1297,72 @@ var Visuals = class {
1282
1297
  decals: [],
1283
1298
  meltApplied: 0,
1284
1299
  bottomY: 0,
1285
- moved: false
1300
+ moved: false,
1301
+ host,
1302
+ range: range ? [range[0], range[1]] : null,
1303
+ merged: host && range ? { host, range: [range[0], range[1]] } : null,
1304
+ loose: false,
1305
+ collapsed: false,
1306
+ hidden: false,
1307
+ touched: false,
1308
+ charK: 0,
1309
+ charHex: 0,
1310
+ glowHex: 0,
1311
+ glowK: 0
1286
1312
  };
1313
+ host?.sharers.push(mv);
1314
+ return mv;
1287
1315
  }
1288
1316
  vertexCount(mv) {
1317
+ if (mv.range) return mv.range[1] - mv.range[0];
1289
1318
  return mv.mesh.geometry.getAttribute(POS_ATTR)?.count ?? 0;
1290
1319
  }
1320
+ /** Is the part drawn? (A ranged part shares its mesh, so `mesh.visible` says nothing about it.) */
1321
+ shown(mv) {
1322
+ return mv.host ? !mv.hidden : mv.mesh.visible;
1323
+ }
1324
+ /** Box of the part's vertices in the space of its mesh (the whole geometry for a plain mesh). */
1325
+ localBox(mv, out) {
1326
+ out.makeEmpty();
1327
+ if (!mv.host || !mv.range) {
1328
+ const geo = mv.mesh.geometry;
1329
+ if (!geo.boundingBox) geo.computeBoundingBox();
1330
+ return geo.boundingBox ? out.copy(geo.boundingBox) : out;
1331
+ }
1332
+ const arr = mv.host.work ?? positionAttr(mv.mesh.geometry)?.array;
1333
+ if (!arr) return out;
1334
+ for (let i = mv.range[0] * 3; i < mv.range[1] * 3; i += 3) out.expandByPoint(this.t.set(arr[i], arr[i + 1], arr[i + 2]));
1335
+ return out;
1336
+ }
1337
+ /** Box of the part in world space. */
1338
+ worldBox(mv, out) {
1339
+ if (!mv.host) return out.setFromObject(mv.mesh);
1340
+ this.localBox(mv, out);
1341
+ mv.mesh.updateWorldMatrix(true, false);
1342
+ return out.isEmpty() ? out : out.applyMatrix4(mv.mesh.matrixWorld);
1343
+ }
1291
1344
  /**
1292
1345
  * Average vertex colour of a mesh whose material uses `vertexColors` (computed once per mesh, from its original geometry), else null. Fragments
1293
1346
  * and debris of such a mesh multiply it by `material.color`; without it they come out white.
1294
1347
  */
1295
1348
  vertexTint(mv) {
1296
- if (this.tints.has(mv.mesh)) return this.tints.get(mv.mesh) ?? null;
1349
+ if (this.tints.has(mv)) return this.tints.get(mv) ?? null;
1297
1350
  let tint = null;
1298
1351
  const used = materialsOf(mv.origMaterial).some((m) => m.vertexColors === true);
1299
1352
  const attr = used ? mv.origGeometry.getAttribute("color") : void 0;
1300
1353
  if (attr && attr.count > 0 && attr.itemSize >= 3) {
1354
+ const from = mv.merged ? mv.merged.range[0] : 0;
1355
+ const to = mv.merged ? Math.min(attr.count, mv.merged.range[1]) : attr.count;
1301
1356
  let r = 0, g = 0, b = 0;
1302
- for (let i = 0; i < attr.count; i++) {
1357
+ for (let i = from; i < to; i++) {
1303
1358
  r += attr.getX(i);
1304
1359
  g += attr.getY(i);
1305
1360
  b += attr.getZ(i);
1306
1361
  }
1307
- tint = new this.THREE.Color(r / attr.count, g / attr.count, b / attr.count);
1362
+ const n = Math.max(1, to - from);
1363
+ tint = new this.THREE.Color(r / n, g / n, b / n);
1308
1364
  }
1309
- this.tints.set(mv.mesh, tint);
1365
+ this.tints.set(mv, tint);
1310
1366
  return tint;
1311
1367
  }
1312
1368
  /** Colour of the original material (before any char) times the vertex tint, `0xRRGGBB`, or -1. */
@@ -1330,6 +1386,7 @@ var Visuals = class {
1330
1386
  }
1331
1387
  /** Clone the geometry (vertex edits); false when its positions cannot be edited. */
1332
1388
  ownGeometry(mv) {
1389
+ if (mv.host) return this.ownHostGeometry(mv.host);
1333
1390
  if (mv.ownGeo) return true;
1334
1391
  const pos = positionAttr(mv.mesh.geometry);
1335
1392
  if (!pos) return false;
@@ -1339,6 +1396,16 @@ var Visuals = class {
1339
1396
  mv.work = new Float32Array(positionAttr(g).array);
1340
1397
  return true;
1341
1398
  }
1399
+ ownHostGeometry(host) {
1400
+ if (host.ownGeo) return true;
1401
+ const pos = positionAttr(host.mesh.geometry);
1402
+ if (!pos) return false;
1403
+ host.ownGeo = true;
1404
+ const g = host.mesh.geometry.clone();
1405
+ host.mesh.geometry = g;
1406
+ host.work = new Float32Array(positionAttr(g).array);
1407
+ return true;
1408
+ }
1342
1409
  // ------------------------------------------------------------------------------------------------------------------------------------
1343
1410
  // Dents
1344
1411
  // ------------------------------------------------------------------------------------------------------------------------------------
@@ -1356,10 +1423,15 @@ var Visuals = class {
1356
1423
  const r = dent.radius / s;
1357
1424
  const depth = dent.depth / s;
1358
1425
  const geo = mesh.geometry;
1359
- if (!geo.boundingBox) geo.computeBoundingBox();
1360
- if (geo.boundingBox && geo.boundingBox.distanceToPoint(lp) > r) return false;
1426
+ let bb;
1427
+ if (mv.host) bb = this.localBox(mv, this.lbox);
1428
+ else {
1429
+ if (!geo.boundingBox) geo.computeBoundingBox();
1430
+ bb = geo.boundingBox;
1431
+ }
1432
+ if (bb && !bb.isEmpty() && bb.distanceToPoint(lp) > r) return false;
1361
1433
  if (!this.ownGeometry(mv)) return false;
1362
- const work = mv.work;
1434
+ const work = mv.host ? mv.host.work : mv.work;
1363
1435
  const pos = positionAttr(mesh.geometry);
1364
1436
  const r2 = r * r;
1365
1437
  let touched = false;
@@ -1369,7 +1441,9 @@ var Visuals = class {
1369
1441
  const nx = ln.x;
1370
1442
  const ny = ln.y;
1371
1443
  const nz = ln.z;
1372
- for (let i = 0; i < work.length; i += 3) {
1444
+ const from = mv.range ? mv.range[0] * 3 : 0;
1445
+ const to = mv.range ? mv.range[1] * 3 : work.length;
1446
+ for (let i = from; i < to; i += 3) {
1373
1447
  const dx = work[i] - px;
1374
1448
  const dy = work[i + 1] - py;
1375
1449
  const dz = work[i + 2] - pz;
@@ -1383,11 +1457,16 @@ var Visuals = class {
1383
1457
  touched = true;
1384
1458
  }
1385
1459
  if (!touched) return false;
1460
+ mv.touched = true;
1386
1461
  this.writePositions(mv, pos);
1387
1462
  return true;
1388
1463
  }
1389
1464
  /** Copy `work` (dents) into the geometry, adding the melt sag when the mesh is melting. */
1390
1465
  writePositions(mv, pos) {
1466
+ if (mv.host) {
1467
+ this.rebuildHost(mv.host);
1468
+ return;
1469
+ }
1391
1470
  const work = mv.work;
1392
1471
  const arr = pos.array;
1393
1472
  const melt = mv.meltApplied;
@@ -1501,6 +1580,12 @@ var Visuals = class {
1501
1580
  pv.charApplied = char;
1502
1581
  const target = new this.THREE.Color(charColor);
1503
1582
  for (const mv of pv.meshes) {
1583
+ if (mv.host) {
1584
+ mv.charK = clamp(char, 0, 1);
1585
+ mv.charHex = charColor;
1586
+ this.paint(mv);
1587
+ continue;
1588
+ }
1504
1589
  this.ownMaterial(mv);
1505
1590
  const mats = materialsOf(mv.mesh.material);
1506
1591
  mats.forEach((m, i) => {
@@ -1518,6 +1603,12 @@ var Visuals = class {
1518
1603
  pv.glowApplied = key;
1519
1604
  const intensity = hex === 0 ? 0 : clamp(0.35 + (temperatureC - 450) / 900, 0, 1.4);
1520
1605
  for (const mv of pv.meshes) {
1606
+ if (mv.host) {
1607
+ mv.glowHex = hex;
1608
+ mv.glowK = intensity;
1609
+ this.paint(mv);
1610
+ continue;
1611
+ }
1521
1612
  this.ownMaterial(mv);
1522
1613
  for (const m of materialsOf(mv.mesh.material)) {
1523
1614
  const e = m.emissive;
@@ -1538,6 +1629,12 @@ var Visuals = class {
1538
1629
  pv.meltApplied = m;
1539
1630
  for (const mv of pv.meshes) {
1540
1631
  const mesh = mv.mesh;
1632
+ if (mv.host) {
1633
+ mv.meltApplied = m;
1634
+ mv.touched = true;
1635
+ if (!(this.lowQuality && this.vertexCount(mv) > 2e4) && this.ownGeometry(mv)) this.rebuildHost(mv.host);
1636
+ continue;
1637
+ }
1541
1638
  if (mv.meltApplied === 0 && m > 0) {
1542
1639
  mesh.updateWorldMatrix(true, false);
1543
1640
  mv.bottomY = this.box.setFromObject(mesh).min.y;
@@ -1568,10 +1665,270 @@ var Visuals = class {
1568
1665
  // ------------------------------------------------------------------------------------------------------------------------------------
1569
1666
  hide(pv, reason) {
1570
1667
  pv.gone = reason;
1571
- for (const mv of pv.meshes) mv.mesh.visible = false;
1668
+ for (const mv of pv.meshes) {
1669
+ if (!mv.host || mv.loose) {
1670
+ mv.mesh.visible = false;
1671
+ mv.hidden = true;
1672
+ continue;
1673
+ }
1674
+ mv.hidden = true;
1675
+ for (const d of mv.decals) d.obj.visible = false;
1676
+ this.collapse(mv);
1677
+ }
1678
+ }
1679
+ // ------------------------------------------------------------------------------------------------------------------------------------
1680
+ // Parts drawn by a vertex range of a merged mesh
1681
+ // ------------------------------------------------------------------------------------------------------------------------------------
1682
+ /** Collapse the part's vertices in the merged mesh to one point (its triangles become degenerate and vanish). */
1683
+ collapse(mv) {
1684
+ if (!mv.merged || !this.ownHostGeometry(mv.merged.host)) return;
1685
+ mv.collapsed = true;
1686
+ mv.touched = true;
1687
+ this.rebuildHost(mv.merged.host);
1688
+ }
1689
+ /** Write `work` into the geometry again with the melt of ranged parts and the collapsed ranges applied. */
1690
+ rebuildHost(host) {
1691
+ const geo = host.mesh.geometry;
1692
+ const pos = positionAttr(geo);
1693
+ const work = host.work;
1694
+ if (!pos || !work) return;
1695
+ const arr = pos.array;
1696
+ arr.set(work);
1697
+ for (const s of host.sharers) {
1698
+ if (s.merged && s.merged.host === host) {
1699
+ if (s.collapsed) this.collapseRange(arr, s.merged.range);
1700
+ else if (s.meltApplied > 0 && !s.loose) this.meltRange(arr, work, s.merged.range, s.meltApplied);
1701
+ } else if (s.host === host && s.range && s.meltApplied > 0) this.meltRange(arr, work, s.range, s.meltApplied);
1702
+ }
1703
+ pos.needsUpdate = true;
1704
+ geo.computeVertexNormals();
1705
+ geo.boundingBox = null;
1706
+ geo.boundingSphere = null;
1707
+ }
1708
+ collapseRange(arr, range) {
1709
+ const x = arr[range[0] * 3];
1710
+ const y = arr[range[0] * 3 + 1];
1711
+ const z = arr[range[0] * 3 + 2];
1712
+ for (let i = range[0] * 3; i < range[1] * 3; i += 3) {
1713
+ arr[i] = x;
1714
+ arr[i + 1] = y;
1715
+ arr[i + 2] = z;
1716
+ }
1717
+ }
1718
+ /** Melt of one range: shorter in Y (down to 25 %) with the bottom fixed, a bit wider around its centre, with the same noise as a whole mesh. */
1719
+ meltRange(arr, work, range, melt) {
1720
+ let minY = Infinity;
1721
+ let maxY = -Infinity;
1722
+ let cx = 0;
1723
+ let cz = 0;
1724
+ const n = Math.max(1, range[1] - range[0]);
1725
+ for (let i = range[0] * 3; i < range[1] * 3; i += 3) {
1726
+ minY = Math.min(minY, work[i + 1]);
1727
+ maxY = Math.max(maxY, work[i + 1]);
1728
+ cx += work[i];
1729
+ cz += work[i + 2];
1730
+ }
1731
+ cx /= n;
1732
+ cz /= n;
1733
+ const h = Math.max(1e-4, maxY - minY);
1734
+ const sy = lerp(1, 0.25, melt);
1735
+ const sxz = 1 + 0.18 * melt;
1736
+ for (let i = range[0] * 3, v = 0; i < range[1] * 3; i += 3, v++) {
1737
+ const noise = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
1738
+ arr[i] = cx + (work[i] - cx) * sxz;
1739
+ arr[i + 2] = cz + (work[i + 2] - cz) * sxz;
1740
+ arr[i + 1] = Math.max(minY, minY + (work[i + 1] - minY) * sy - noise * melt * h * 0.04);
1741
+ }
1742
+ }
1743
+ /** The colour attribute of the host's current geometry (created white when there is none) with vertex colours switched on in its material. */
1744
+ ensureColors(host) {
1745
+ if (!this.ownHostGeometry(host)) return null;
1746
+ const geo = host.mesh.geometry;
1747
+ let attr = geo.getAttribute("color");
1748
+ if (!attr) {
1749
+ const n = geo.getAttribute(POS_ATTR).count;
1750
+ attr = new this.THREE.BufferAttribute(new Float32Array(n * 3).fill(1), 3);
1751
+ geo.setAttribute("color", attr);
1752
+ }
1753
+ if (!host.ownMat) {
1754
+ host.ownMat = true;
1755
+ const src = materialsOf(host.mesh.material);
1756
+ const cloned = src.map((m) => m.clone());
1757
+ host.mesh.material = Array.isArray(host.mesh.material) ? cloned : cloned[0];
1758
+ }
1759
+ for (const m of materialsOf(host.mesh.material)) {
1760
+ if (m.vertexColors !== true) {
1761
+ ;
1762
+ m.vertexColors = true;
1763
+ m.needsUpdate = true;
1764
+ }
1765
+ }
1766
+ return attr;
1767
+ }
1768
+ /** Colour of the part's vertices from the original colours, its char and its heat. Only the part's range is written. */
1769
+ paint(mv) {
1770
+ if (!mv.host || !mv.range || !mv.merged) return;
1771
+ const idle = mv.charK <= 0 && mv.glowK <= 0;
1772
+ if (idle && !mv.touched) return;
1773
+ const attr = this.ensureColors(mv.host);
1774
+ if (!attr) return;
1775
+ const size = attr.itemSize;
1776
+ const arr = attr.array;
1777
+ const orig = mv.merged.host.origGeometry.getAttribute("color");
1778
+ const oSize = orig?.itemSize ?? 3;
1779
+ const oArr = orig?.array;
1780
+ const shift = mv.merged.range[0] - mv.range[0];
1781
+ const ch = new this.THREE.Color(mv.charHex);
1782
+ const heat = new this.THREE.Color(mv.glowHex);
1783
+ const k = clamp(mv.charK, 0, 1);
1784
+ const gk = clamp(mv.glowK, 0, 1);
1785
+ const boost = 1 + Math.max(0, mv.glowK - 1);
1786
+ for (let i = mv.range[0]; i < mv.range[1]; i++) {
1787
+ const o = (i + shift) * oSize;
1788
+ let r = oArr ? oArr[o] : 1;
1789
+ let g = oArr ? oArr[o + 1] : 1;
1790
+ let b = oArr ? oArr[o + 2] : 1;
1791
+ if (k > 0) {
1792
+ r = lerp(r, ch.r, k);
1793
+ g = lerp(g, ch.g, k);
1794
+ b = lerp(b, ch.b, k);
1795
+ }
1796
+ if (gk > 0) {
1797
+ r = lerp(r, heat.r * boost, gk);
1798
+ g = lerp(g, heat.g * boost, gk);
1799
+ b = lerp(b, heat.b * boost, gk);
1800
+ }
1801
+ arr[i * size] = r;
1802
+ arr[i * size + 1] = g;
1803
+ arr[i * size + 2] = b;
1804
+ }
1805
+ mv.touched = true;
1806
+ attr.needsUpdate = true;
1807
+ }
1808
+ /**
1809
+ * Move a ranged part to a mesh of its own: the faces whose vertices all lie in its range are copied (with the current dents and colours) into a
1810
+ * new mesh that takes the merged mesh's transform and material, then attached to `group`; the range is collapsed in the merged mesh. Decals
1811
+ * go with it.
1812
+ */
1813
+ extract(mv, group) {
1814
+ if (!mv.host || !mv.range || !mv.merged || mv.loose) return;
1815
+ const host = mv.host;
1816
+ const [v0, v1] = mv.range;
1817
+ const src = host.mesh.geometry;
1818
+ const geo = new this.THREE.BufferGeometry();
1819
+ const count = v1 - v0;
1820
+ for (const name of Object.keys(src.attributes)) {
1821
+ const a = src.getAttribute(name);
1822
+ if (a.isInterleavedBufferAttribute) continue;
1823
+ const size = a.itemSize;
1824
+ const from = (name === POS_ATTR ? host.work : null) ?? a.array;
1825
+ const slice = from.slice(v0 * size, v1 * size);
1826
+ const Ctor = a.array.constructor;
1827
+ geo.setAttribute(name, new this.THREE.BufferAttribute(new Ctor(slice), size, a.normalized));
1828
+ }
1829
+ const index = src.getIndex();
1830
+ if (index) {
1831
+ const out = [];
1832
+ for (let i = 0; i + 2 < index.count; i += 3) {
1833
+ const a = index.getX(i);
1834
+ const b = index.getX(i + 1);
1835
+ const c = index.getX(i + 2);
1836
+ if (a >= v0 && a < v1 && b >= v0 && b < v1 && c >= v0 && c < v1) out.push(a - v0, b - v0, c - v0);
1837
+ }
1838
+ geo.setIndex(count > 65535 ? new this.THREE.Uint32BufferAttribute(out, 1) : new this.THREE.Uint16BufferAttribute(out, 1));
1839
+ }
1840
+ const mats = materialsOf(host.mesh.material).map((m) => m.clone());
1841
+ const loose = new this.THREE.Mesh(geo, mats[0]);
1842
+ loose.name = `${host.mesh.name || "mesh"}:${v0}-${v1}`;
1843
+ loose.castShadow = host.mesh.castShadow;
1844
+ loose.receiveShadow = host.mesh.receiveShadow;
1845
+ loose.renderOrder = host.mesh.renderOrder;
1846
+ loose.frustumCulled = host.mesh.frustumCulled;
1847
+ loose.layers.mask = host.mesh.layers.mask;
1848
+ loose.position.copy(host.mesh.position);
1849
+ loose.quaternion.copy(host.mesh.quaternion);
1850
+ loose.scale.copy(host.mesh.scale);
1851
+ const parent = host.mesh.parent;
1852
+ if (parent) parent.add(loose);
1853
+ loose.updateMatrixWorld(true);
1854
+ this.collapse(mv);
1855
+ for (const d of mv.decals) {
1856
+ d.obj.visible = true;
1857
+ loose.add(d.obj);
1858
+ }
1859
+ const lp = positionAttr(geo);
1860
+ const lh = {
1861
+ mesh: loose,
1862
+ origGeometry: geo,
1863
+ origMaterial: loose.material,
1864
+ ownGeo: true,
1865
+ work: new Float32Array(lp.array),
1866
+ ownMat: true,
1867
+ sharers: [mv]
1868
+ };
1869
+ mv.host = lh;
1870
+ mv.range = [0, count];
1871
+ mv.mesh = loose;
1872
+ mv.loose = true;
1873
+ mv.moved = true;
1874
+ group.attach(loose);
1875
+ for (const m of materialsOf(loose.material)) m.needsUpdate = true;
1876
+ }
1877
+ restoreRanged(mv) {
1878
+ const merged = mv.merged;
1879
+ const host = merged.host;
1880
+ for (const d of mv.decals) this.disposeDecal(d);
1881
+ mv.decals.length = 0;
1882
+ if (mv.loose) {
1883
+ const loose = mv.mesh;
1884
+ loose.parent?.remove(loose);
1885
+ loose.geometry.dispose();
1886
+ for (const m of materialsOf(loose.material)) m.dispose();
1887
+ mv.host = host;
1888
+ mv.range = [merged.range[0], merged.range[1]];
1889
+ mv.mesh = host.mesh;
1890
+ mv.loose = false;
1891
+ mv.moved = false;
1892
+ }
1893
+ mv.collapsed = false;
1894
+ mv.hidden = false;
1895
+ mv.meltApplied = 0;
1896
+ mv.charK = 0;
1897
+ mv.glowK = 0;
1898
+ mv.touched = false;
1899
+ if (!host.ownGeo) return;
1900
+ const work = host.work;
1901
+ const op = positionAttr(host.origGeometry);
1902
+ const [v0, v1] = merged.range;
1903
+ if (op) work.set(op.array.subarray(v0 * 3, v1 * 3), v0 * 3);
1904
+ const attr = host.mesh.geometry.getAttribute("color");
1905
+ if (attr) {
1906
+ const orig = host.origGeometry.getAttribute("color");
1907
+ const size = attr.itemSize;
1908
+ const a = attr.array;
1909
+ for (let i = v0; i < v1; i++) {
1910
+ for (let c = 0; c < 3; c++) a[i * size + c] = orig ? orig.array[i * orig.itemSize + c] : 1;
1911
+ }
1912
+ attr.needsUpdate = true;
1913
+ }
1914
+ if (host.sharers.every((s) => !s.touched)) {
1915
+ host.mesh.geometry.dispose();
1916
+ host.mesh.geometry = host.origGeometry;
1917
+ host.work = null;
1918
+ host.ownGeo = false;
1919
+ if (host.ownMat) {
1920
+ for (const m of materialsOf(host.mesh.material)) m.dispose();
1921
+ host.mesh.material = host.origMaterial;
1922
+ host.ownMat = false;
1923
+ }
1924
+ } else this.rebuildHost(host);
1572
1925
  }
1573
1926
  /** Bring one mesh back to how it was when it was bound. */
1574
1927
  restoreMesh(mv) {
1928
+ if (mv.merged) {
1929
+ this.restoreRanged(mv);
1930
+ return;
1931
+ }
1575
1932
  const mesh = mv.mesh;
1576
1933
  for (const d of mv.decals) this.disposeDecal(d);
1577
1934
  mv.decals.length = 0;
@@ -1671,6 +2028,10 @@ function fallbackColor(id, def) {
1671
2028
  function materialsOf2(m) {
1672
2029
  return Array.isArray(m) ? m : [m];
1673
2030
  }
2031
+ function isMeshRange(v) {
2032
+ const r = v;
2033
+ return !!r.mesh && Array.isArray(r.range);
2034
+ }
1674
2035
  function createDestructionRenderer(THREE, options) {
1675
2036
  return new Renderer(THREE, options);
1676
2037
  }
@@ -1764,12 +2125,23 @@ var Renderer = class {
1764
2125
  const partIds = new Set((state?.parts ?? []).map((p) => p.id));
1765
2126
  const byPart = /* @__PURE__ */ new Map();
1766
2127
  const taken = /* @__PURE__ */ new Set();
2128
+ const put = (pid, e) => {
2129
+ const l = byPart.get(pid);
2130
+ if (l) l.push(e);
2131
+ else byPart.set(pid, [e]);
2132
+ };
1767
2133
  const add = (pid, m) => {
1768
2134
  if (taken.has(m)) return;
1769
2135
  taken.add(m);
1770
- const l = byPart.get(pid);
1771
- if (l) l.push(m);
1772
- else byPart.set(pid, [m]);
2136
+ put(pid, { mesh: m });
2137
+ };
2138
+ const addRange = (pid, r) => {
2139
+ const n = r.mesh.geometry.getAttribute("position")?.count ?? 0;
2140
+ const [v0, v1] = r.range;
2141
+ if (!Number.isInteger(v0) || !Number.isInteger(v1) || v0 < 0 || v1 <= v0 || v1 > n) {
2142
+ throw new Error(`game-destruction-three: invalid vertex range [${v0}, ${v1}) for part "${pid}" (the mesh has ${n} vertices)`);
2143
+ }
2144
+ put(pid, { mesh: r.mesh, range: [v0, v1] });
1773
2145
  };
1774
2146
  const meshesIn = (o) => {
1775
2147
  const out = [];
@@ -1780,7 +2152,10 @@ var Renderer = class {
1780
2152
  };
1781
2153
  if (options.parts) {
1782
2154
  for (const [pid, v] of Object.entries(options.parts)) {
1783
- for (const o of Array.isArray(v) ? v : [v]) for (const m of meshesIn(o)) add(pid, m);
2155
+ for (const o of Array.isArray(v) ? v : [v]) {
2156
+ if (isMeshRange(o)) addRange(pid, o);
2157
+ else for (const m of meshesIn(o)) add(pid, m);
2158
+ }
1784
2159
  }
1785
2160
  } else {
1786
2161
  const leftovers = [];
@@ -1793,14 +2168,15 @@ var Renderer = class {
1793
2168
  if (leftovers.length) this.matchByBox(objectId, root, leftovers, add);
1794
2169
  }
1795
2170
  const bound = { id: objectId, root, sync: options.syncTransform !== false, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: false };
1796
- for (const pid of partIds) bound.parts.set(pid, this.makePartVis(objectId, pid, state?.parts.find((p) => p.id === pid)?.material ?? "", byPart.get(pid) ?? []));
2171
+ const hosts = /* @__PURE__ */ new Map();
2172
+ for (const pid of partIds) bound.parts.set(pid, this.makePartVis(objectId, pid, state?.parts.find((p) => p.id === pid)?.material ?? "", byPart.get(pid) ?? [], hosts));
1797
2173
  for (const [pid, meshes] of byPart) {
1798
- if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes));
2174
+ if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes, hosts));
1799
2175
  }
1800
2176
  this.bounds.set(objectId, bound);
1801
2177
  }
1802
- makePartVis(home, id, material, meshes) {
1803
- return { id, home, material, meshes: meshes.map((m) => this.visuals.makeMeshVis(m, home)), charApplied: 0, glowApplied: 0, meltApplied: 0, gone: "none" };
2178
+ makePartVis(home, id, material, meshes, hosts) {
2179
+ 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" };
1804
2180
  }
1805
2181
  /** Match unlabelled meshes to the part whose shape contains the centre of the mesh's bounding box. */
1806
2182
  matchByBox(objectId, root, meshes, add) {
@@ -1860,11 +2236,13 @@ var Renderer = class {
1860
2236
  const inv = root.matrixWorld.clone().invert();
1861
2237
  const total = new this.THREE.Box3();
1862
2238
  const m4 = new this.THREE.Matrix4();
1863
- for (const m of meshes) {
1864
- const g = m.geometry;
1865
- if (!g.boundingBox) g.computeBoundingBox();
1866
- if (!g.boundingBox || g.boundingBox.isEmpty()) continue;
1867
- total.union(g.boundingBox.clone().applyMatrix4(m4.multiplyMatrices(inv, m.matrixWorld)));
2239
+ const lb = new this.THREE.Box3();
2240
+ for (const mv of meshes) {
2241
+ const m = mv.mesh;
2242
+ m.updateWorldMatrix(true, false);
2243
+ this.visuals.localBox(mv, lb);
2244
+ if (lb.isEmpty()) continue;
2245
+ total.union(lb.clone().applyMatrix4(m4.multiplyMatrices(inv, m.matrixWorld)));
1868
2246
  }
1869
2247
  if (total.isEmpty()) return { half: [0.25, 0.25, 0.25], center: new this.THREE.Vector3() };
1870
2248
  const size = total.getSize(new this.THREE.Vector3());
@@ -1872,7 +2250,7 @@ var Renderer = class {
1872
2250
  }
1873
2251
  meshesOf(b) {
1874
2252
  const out = [];
1875
- for (const pv of b.parts.values()) if (pv.gone === "none") for (const mv of pv.meshes) out.push(mv.mesh);
2253
+ for (const pv of b.parts.values()) if (pv.gone === "none") for (const mv of pv.meshes) out.push(mv);
1876
2254
  return out;
1877
2255
  }
1878
2256
  massOf(objectId) {
@@ -2003,6 +2381,9 @@ var Renderer = class {
2003
2381
  case "detach":
2004
2382
  this.onDetach(ev);
2005
2383
  break;
2384
+ case "collapse":
2385
+ this.splitLoose(ev.object);
2386
+ break;
2006
2387
  case "disassemble_progress": {
2007
2388
  const k = `unscrew:${ev.joint}`;
2008
2389
  if (this.time - (this.throttles.get(k) ?? -1) > 0.25) {
@@ -2121,8 +2502,8 @@ var Renderer = class {
2121
2502
  let best = null;
2122
2503
  let bd = Infinity;
2123
2504
  for (const mv of pv.meshes) {
2124
- if (!mv.mesh.visible) continue;
2125
- const d = this.box.setFromObject(mv.mesh).distanceToPoint(p);
2505
+ if (!this.visuals.shown(mv)) continue;
2506
+ const d = this.visuals.worldBox(mv, this.box).distanceToPoint(p);
2126
2507
  if (d < bd) {
2127
2508
  bd = d;
2128
2509
  best = mv;
@@ -2142,7 +2523,7 @@ var Renderer = class {
2142
2523
  const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
2143
2524
  const wn = this.worldDir(b, ev.dent.normal);
2144
2525
  for (const mv of pv.meshes) {
2145
- if (!mv.mesh.visible) continue;
2526
+ if (!this.visuals.shown(mv)) continue;
2146
2527
  this.visuals.applyDent(mv, wp, wn, ev.dent);
2147
2528
  }
2148
2529
  if (def?.tags?.includes("metal")) this.particles.burst("sparks", ev.worldPoint, [wn.x, wn.y, wn.z], 4 + Math.min(8, ev.dent.depth * 150), 3.2);
@@ -2233,16 +2614,20 @@ var Renderer = class {
2233
2614
  }
2234
2615
  onDetach(ev) {
2235
2616
  this.sound("detach", this.rootPoint(ev.object), 0.5);
2617
+ this.splitLoose(ev.object);
2618
+ }
2619
+ /** Turn every loose group of the object (a detach, a collapse) into a piece of its own. */
2620
+ splitLoose(objectId) {
2236
2621
  if (!this.autoSplit) return;
2237
- const b = this.bounds.get(ev.object);
2622
+ const b = this.bounds.get(objectId);
2238
2623
  if (!b) return;
2239
- const st = this.world.state(ev.object);
2624
+ const st = this.world.state(objectId);
2240
2625
  if (!st || st.groups.length < 2) return;
2241
2626
  const loose = st.groups.length - 1;
2242
2627
  for (let i = 0; i < loose; i++) {
2243
2628
  let newId;
2244
2629
  try {
2245
- newId = this.world.splitGroup(ev.object, 1);
2630
+ newId = this.world.splitGroup(objectId, 1);
2246
2631
  } catch {
2247
2632
  break;
2248
2633
  }
@@ -2253,7 +2638,7 @@ var Renderer = class {
2253
2638
  const { half, center } = this.measure(b.root, this.meshesOf(b));
2254
2639
  old.halfExtents = half;
2255
2640
  this.pieceSys.setCenter(old, center);
2256
- old.mass = Math.max(0.05, this.massOf(ev.object));
2641
+ old.mass = Math.max(0.05, this.massOf(objectId));
2257
2642
  old.sleeping = false;
2258
2643
  }
2259
2644
  }
@@ -2275,8 +2660,11 @@ var Renderer = class {
2275
2660
  from.parts.delete(p.id);
2276
2661
  nb.parts.set(p.id, pv);
2277
2662
  for (const mv of pv.meshes) {
2278
- group.attach(mv.mesh);
2279
- mv.moved = true;
2663
+ if (mv.host && !mv.loose) this.visuals.extract(mv, group);
2664
+ else {
2665
+ group.attach(mv.mesh);
2666
+ mv.moved = true;
2667
+ }
2280
2668
  }
2281
2669
  }
2282
2670
  this.bounds.set(newId, nb);
@@ -2392,7 +2780,7 @@ var Renderer = class {
2392
2780
  }
2393
2781
  onGone(objectId, pv, reason, def) {
2394
2782
  const bb = new this.THREE.Box3();
2395
- for (const mv of pv.meshes) bb.union(this.box.setFromObject(mv.mesh));
2783
+ for (const mv of pv.meshes) bb.union(this.visuals.worldBox(mv, this.box));
2396
2784
  this.visuals.hide(pv, reason);
2397
2785
  if (bb.isEmpty()) return;
2398
2786
  const c = bb.getCenter(new this.THREE.Vector3());
@@ -2409,7 +2797,7 @@ var Renderer = class {
2409
2797
  const bb = this.box;
2410
2798
  bb.makeEmpty();
2411
2799
  const tmp = new this.THREE.Box3();
2412
- for (const mv of pv.meshes) if (mv.mesh.visible) bb.union(tmp.setFromObject(mv.mesh));
2800
+ for (const mv of pv.meshes) if (this.visuals.shown(mv)) bb.union(this.visuals.worldBox(mv, tmp));
2413
2801
  let cx = f.point[0];
2414
2802
  let cy = f.point[1];
2415
2803
  let cz = f.point[2];