@adatechnology/game-destruction-three 0.1.2 → 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.
@@ -2329,6 +2329,8 @@ void main() {
2329
2329
  __publicField(this, "b");
2330
2330
  __publicField(this, "m4");
2331
2331
  __publicField(this, "box");
2332
+ __publicField(this, "t");
2333
+ __publicField(this, "lbox");
2332
2334
  /** Total live decals across meshes (kept in sync by add/remove). */
2333
2335
  __publicField(this, "decalCount", 0);
2334
2336
  __publicField(this, "tints", /* @__PURE__ */ new WeakMap());
@@ -2337,9 +2339,22 @@ void main() {
2337
2339
  this.b = tmpV(THREE);
2338
2340
  this.m4 = new THREE.Matrix4();
2339
2341
  this.box = new THREE.Box3();
2342
+ this.t = tmpV(THREE);
2343
+ this.lbox = new THREE.Box3();
2340
2344
  }
2341
- makeMeshVis(mesh, home) {
2342
- return {
2345
+ /**
2346
+ * @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`.
2347
+ */
2348
+ makeMeshVis(mesh, home, range, hosts) {
2349
+ let host = null;
2350
+ if (range) {
2351
+ host = hosts?.get(mesh) ?? null;
2352
+ if (!host) {
2353
+ host = { mesh, origGeometry: mesh.geometry, origMaterial: mesh.material, ownGeo: false, work: null, ownMat: false, sharers: [] };
2354
+ hosts?.set(mesh, host);
2355
+ }
2356
+ }
2357
+ const mv = {
2343
2358
  mesh,
2344
2359
  home,
2345
2360
  origGeometry: mesh.geometry,
@@ -2356,31 +2371,72 @@ void main() {
2356
2371
  decals: [],
2357
2372
  meltApplied: 0,
2358
2373
  bottomY: 0,
2359
- moved: false
2374
+ moved: false,
2375
+ host,
2376
+ range: range ? [range[0], range[1]] : null,
2377
+ merged: host && range ? { host, range: [range[0], range[1]] } : null,
2378
+ loose: false,
2379
+ collapsed: false,
2380
+ hidden: false,
2381
+ touched: false,
2382
+ charK: 0,
2383
+ charHex: 0,
2384
+ glowHex: 0,
2385
+ glowK: 0
2360
2386
  };
2387
+ host?.sharers.push(mv);
2388
+ return mv;
2361
2389
  }
2362
2390
  vertexCount(mv) {
2391
+ if (mv.range) return mv.range[1] - mv.range[0];
2363
2392
  return mv.mesh.geometry.getAttribute(POS_ATTR)?.count ?? 0;
2364
2393
  }
2394
+ /** Is the part drawn? (A ranged part shares its mesh, so `mesh.visible` says nothing about it.) */
2395
+ shown(mv) {
2396
+ return mv.host ? !mv.hidden : mv.mesh.visible;
2397
+ }
2398
+ /** Box of the part's vertices in the space of its mesh (the whole geometry for a plain mesh). */
2399
+ localBox(mv, out) {
2400
+ out.makeEmpty();
2401
+ if (!mv.host || !mv.range) {
2402
+ const geo = mv.mesh.geometry;
2403
+ if (!geo.boundingBox) geo.computeBoundingBox();
2404
+ return geo.boundingBox ? out.copy(geo.boundingBox) : out;
2405
+ }
2406
+ const arr = mv.host.work ?? positionAttr(mv.mesh.geometry)?.array;
2407
+ if (!arr) return out;
2408
+ 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]));
2409
+ return out;
2410
+ }
2411
+ /** Box of the part in world space. */
2412
+ worldBox(mv, out) {
2413
+ if (!mv.host) return out.setFromObject(mv.mesh);
2414
+ this.localBox(mv, out);
2415
+ mv.mesh.updateWorldMatrix(true, false);
2416
+ return out.isEmpty() ? out : out.applyMatrix4(mv.mesh.matrixWorld);
2417
+ }
2365
2418
  /**
2366
2419
  * Average vertex colour of a mesh whose material uses `vertexColors` (computed once per mesh, from its original geometry), else null. Fragments
2367
2420
  * and debris of such a mesh multiply it by `material.color`; without it they come out white.
2368
2421
  */
2369
2422
  vertexTint(mv) {
2370
- if (this.tints.has(mv.mesh)) return this.tints.get(mv.mesh) ?? null;
2423
+ if (this.tints.has(mv)) return this.tints.get(mv) ?? null;
2371
2424
  let tint = null;
2372
2425
  const used = materialsOf(mv.origMaterial).some((m) => m.vertexColors === true);
2373
2426
  const attr = used ? mv.origGeometry.getAttribute("color") : void 0;
2374
2427
  if (attr && attr.count > 0 && attr.itemSize >= 3) {
2428
+ const from = mv.merged ? mv.merged.range[0] : 0;
2429
+ const to = mv.merged ? Math.min(attr.count, mv.merged.range[1]) : attr.count;
2375
2430
  let r = 0, g = 0, b = 0;
2376
- for (let i = 0; i < attr.count; i++) {
2431
+ for (let i = from; i < to; i++) {
2377
2432
  r += attr.getX(i);
2378
2433
  g += attr.getY(i);
2379
2434
  b += attr.getZ(i);
2380
2435
  }
2381
- tint = new this.THREE.Color(r / attr.count, g / attr.count, b / attr.count);
2436
+ const n = Math.max(1, to - from);
2437
+ tint = new this.THREE.Color(r / n, g / n, b / n);
2382
2438
  }
2383
- this.tints.set(mv.mesh, tint);
2439
+ this.tints.set(mv, tint);
2384
2440
  return tint;
2385
2441
  }
2386
2442
  /** Colour of the original material (before any char) times the vertex tint, `0xRRGGBB`, or -1. */
@@ -2404,6 +2460,7 @@ void main() {
2404
2460
  }
2405
2461
  /** Clone the geometry (vertex edits); false when its positions cannot be edited. */
2406
2462
  ownGeometry(mv) {
2463
+ if (mv.host) return this.ownHostGeometry(mv.host);
2407
2464
  if (mv.ownGeo) return true;
2408
2465
  const pos = positionAttr(mv.mesh.geometry);
2409
2466
  if (!pos) return false;
@@ -2413,6 +2470,16 @@ void main() {
2413
2470
  mv.work = new Float32Array(positionAttr(g).array);
2414
2471
  return true;
2415
2472
  }
2473
+ ownHostGeometry(host) {
2474
+ if (host.ownGeo) return true;
2475
+ const pos = positionAttr(host.mesh.geometry);
2476
+ if (!pos) return false;
2477
+ host.ownGeo = true;
2478
+ const g = host.mesh.geometry.clone();
2479
+ host.mesh.geometry = g;
2480
+ host.work = new Float32Array(positionAttr(g).array);
2481
+ return true;
2482
+ }
2416
2483
  // ------------------------------------------------------------------------------------------------------------------------------------
2417
2484
  // Dents
2418
2485
  // ------------------------------------------------------------------------------------------------------------------------------------
@@ -2430,10 +2497,15 @@ void main() {
2430
2497
  const r = dent.radius / s;
2431
2498
  const depth = dent.depth / s;
2432
2499
  const geo = mesh.geometry;
2433
- if (!geo.boundingBox) geo.computeBoundingBox();
2434
- if (geo.boundingBox && geo.boundingBox.distanceToPoint(lp) > r) return false;
2500
+ let bb;
2501
+ if (mv.host) bb = this.localBox(mv, this.lbox);
2502
+ else {
2503
+ if (!geo.boundingBox) geo.computeBoundingBox();
2504
+ bb = geo.boundingBox;
2505
+ }
2506
+ if (bb && !bb.isEmpty() && bb.distanceToPoint(lp) > r) return false;
2435
2507
  if (!this.ownGeometry(mv)) return false;
2436
- const work = mv.work;
2508
+ const work = mv.host ? mv.host.work : mv.work;
2437
2509
  const pos = positionAttr(mesh.geometry);
2438
2510
  const r2 = r * r;
2439
2511
  let touched = false;
@@ -2443,7 +2515,9 @@ void main() {
2443
2515
  const nx = ln.x;
2444
2516
  const ny = ln.y;
2445
2517
  const nz = ln.z;
2446
- for (let i = 0; i < work.length; i += 3) {
2518
+ const from = mv.range ? mv.range[0] * 3 : 0;
2519
+ const to = mv.range ? mv.range[1] * 3 : work.length;
2520
+ for (let i = from; i < to; i += 3) {
2447
2521
  const dx = work[i] - px;
2448
2522
  const dy = work[i + 1] - py;
2449
2523
  const dz = work[i + 2] - pz;
@@ -2457,11 +2531,16 @@ void main() {
2457
2531
  touched = true;
2458
2532
  }
2459
2533
  if (!touched) return false;
2534
+ mv.touched = true;
2460
2535
  this.writePositions(mv, pos);
2461
2536
  return true;
2462
2537
  }
2463
2538
  /** Copy `work` (dents) into the geometry, adding the melt sag when the mesh is melting. */
2464
2539
  writePositions(mv, pos) {
2540
+ if (mv.host) {
2541
+ this.rebuildHost(mv.host);
2542
+ return;
2543
+ }
2465
2544
  const work = mv.work;
2466
2545
  const arr = pos.array;
2467
2546
  const melt = mv.meltApplied;
@@ -2575,6 +2654,12 @@ void main() {
2575
2654
  pv.charApplied = char;
2576
2655
  const target = new this.THREE.Color(charColor);
2577
2656
  for (const mv of pv.meshes) {
2657
+ if (mv.host) {
2658
+ mv.charK = clamp(char, 0, 1);
2659
+ mv.charHex = charColor;
2660
+ this.paint(mv);
2661
+ continue;
2662
+ }
2578
2663
  this.ownMaterial(mv);
2579
2664
  const mats = materialsOf(mv.mesh.material);
2580
2665
  mats.forEach((m, i) => {
@@ -2592,6 +2677,12 @@ void main() {
2592
2677
  pv.glowApplied = key;
2593
2678
  const intensity = hex === 0 ? 0 : clamp(0.35 + (temperatureC - 450) / 900, 0, 1.4);
2594
2679
  for (const mv of pv.meshes) {
2680
+ if (mv.host) {
2681
+ mv.glowHex = hex;
2682
+ mv.glowK = intensity;
2683
+ this.paint(mv);
2684
+ continue;
2685
+ }
2595
2686
  this.ownMaterial(mv);
2596
2687
  for (const m of materialsOf(mv.mesh.material)) {
2597
2688
  const e = m.emissive;
@@ -2612,6 +2703,12 @@ void main() {
2612
2703
  pv.meltApplied = m;
2613
2704
  for (const mv of pv.meshes) {
2614
2705
  const mesh = mv.mesh;
2706
+ if (mv.host) {
2707
+ mv.meltApplied = m;
2708
+ mv.touched = true;
2709
+ if (!(this.lowQuality && this.vertexCount(mv) > 2e4) && this.ownGeometry(mv)) this.rebuildHost(mv.host);
2710
+ continue;
2711
+ }
2615
2712
  if (mv.meltApplied === 0 && m > 0) {
2616
2713
  mesh.updateWorldMatrix(true, false);
2617
2714
  mv.bottomY = this.box.setFromObject(mesh).min.y;
@@ -2642,10 +2739,270 @@ void main() {
2642
2739
  // ------------------------------------------------------------------------------------------------------------------------------------
2643
2740
  hide(pv, reason) {
2644
2741
  pv.gone = reason;
2645
- for (const mv of pv.meshes) mv.mesh.visible = false;
2742
+ for (const mv of pv.meshes) {
2743
+ if (!mv.host || mv.loose) {
2744
+ mv.mesh.visible = false;
2745
+ mv.hidden = true;
2746
+ continue;
2747
+ }
2748
+ mv.hidden = true;
2749
+ for (const d of mv.decals) d.obj.visible = false;
2750
+ this.collapse(mv);
2751
+ }
2752
+ }
2753
+ // ------------------------------------------------------------------------------------------------------------------------------------
2754
+ // Parts drawn by a vertex range of a merged mesh
2755
+ // ------------------------------------------------------------------------------------------------------------------------------------
2756
+ /** Collapse the part's vertices in the merged mesh to one point (its triangles become degenerate and vanish). */
2757
+ collapse(mv) {
2758
+ if (!mv.merged || !this.ownHostGeometry(mv.merged.host)) return;
2759
+ mv.collapsed = true;
2760
+ mv.touched = true;
2761
+ this.rebuildHost(mv.merged.host);
2762
+ }
2763
+ /** Write `work` into the geometry again with the melt of ranged parts and the collapsed ranges applied. */
2764
+ rebuildHost(host) {
2765
+ const geo = host.mesh.geometry;
2766
+ const pos = positionAttr(geo);
2767
+ const work = host.work;
2768
+ if (!pos || !work) return;
2769
+ const arr = pos.array;
2770
+ arr.set(work);
2771
+ for (const s of host.sharers) {
2772
+ if (s.merged && s.merged.host === host) {
2773
+ if (s.collapsed) this.collapseRange(arr, s.merged.range);
2774
+ else if (s.meltApplied > 0 && !s.loose) this.meltRange(arr, work, s.merged.range, s.meltApplied);
2775
+ } else if (s.host === host && s.range && s.meltApplied > 0) this.meltRange(arr, work, s.range, s.meltApplied);
2776
+ }
2777
+ pos.needsUpdate = true;
2778
+ geo.computeVertexNormals();
2779
+ geo.boundingBox = null;
2780
+ geo.boundingSphere = null;
2781
+ }
2782
+ collapseRange(arr, range) {
2783
+ const x = arr[range[0] * 3];
2784
+ const y = arr[range[0] * 3 + 1];
2785
+ const z = arr[range[0] * 3 + 2];
2786
+ for (let i = range[0] * 3; i < range[1] * 3; i += 3) {
2787
+ arr[i] = x;
2788
+ arr[i + 1] = y;
2789
+ arr[i + 2] = z;
2790
+ }
2791
+ }
2792
+ /** 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. */
2793
+ meltRange(arr, work, range, melt) {
2794
+ let minY = Infinity;
2795
+ let maxY = -Infinity;
2796
+ let cx = 0;
2797
+ let cz = 0;
2798
+ const n = Math.max(1, range[1] - range[0]);
2799
+ for (let i = range[0] * 3; i < range[1] * 3; i += 3) {
2800
+ minY = Math.min(minY, work[i + 1]);
2801
+ maxY = Math.max(maxY, work[i + 1]);
2802
+ cx += work[i];
2803
+ cz += work[i + 2];
2804
+ }
2805
+ cx /= n;
2806
+ cz /= n;
2807
+ const h = Math.max(1e-4, maxY - minY);
2808
+ const sy = lerp(1, 0.25, melt);
2809
+ const sxz = 1 + 0.18 * melt;
2810
+ for (let i = range[0] * 3, v = 0; i < range[1] * 3; i += 3, v++) {
2811
+ const noise = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
2812
+ arr[i] = cx + (work[i] - cx) * sxz;
2813
+ arr[i + 2] = cz + (work[i + 2] - cz) * sxz;
2814
+ arr[i + 1] = Math.max(minY, minY + (work[i + 1] - minY) * sy - noise * melt * h * 0.04);
2815
+ }
2816
+ }
2817
+ /** The colour attribute of the host's current geometry (created white when there is none) with vertex colours switched on in its material. */
2818
+ ensureColors(host) {
2819
+ if (!this.ownHostGeometry(host)) return null;
2820
+ const geo = host.mesh.geometry;
2821
+ let attr = geo.getAttribute("color");
2822
+ if (!attr) {
2823
+ const n = geo.getAttribute(POS_ATTR).count;
2824
+ attr = new this.THREE.BufferAttribute(new Float32Array(n * 3).fill(1), 3);
2825
+ geo.setAttribute("color", attr);
2826
+ }
2827
+ if (!host.ownMat) {
2828
+ host.ownMat = true;
2829
+ const src = materialsOf(host.mesh.material);
2830
+ const cloned = src.map((m) => m.clone());
2831
+ host.mesh.material = Array.isArray(host.mesh.material) ? cloned : cloned[0];
2832
+ }
2833
+ for (const m of materialsOf(host.mesh.material)) {
2834
+ if (m.vertexColors !== true) {
2835
+ ;
2836
+ m.vertexColors = true;
2837
+ m.needsUpdate = true;
2838
+ }
2839
+ }
2840
+ return attr;
2841
+ }
2842
+ /** Colour of the part's vertices from the original colours, its char and its heat. Only the part's range is written. */
2843
+ paint(mv) {
2844
+ if (!mv.host || !mv.range || !mv.merged) return;
2845
+ const idle = mv.charK <= 0 && mv.glowK <= 0;
2846
+ if (idle && !mv.touched) return;
2847
+ const attr = this.ensureColors(mv.host);
2848
+ if (!attr) return;
2849
+ const size = attr.itemSize;
2850
+ const arr = attr.array;
2851
+ const orig = mv.merged.host.origGeometry.getAttribute("color");
2852
+ const oSize = orig?.itemSize ?? 3;
2853
+ const oArr = orig?.array;
2854
+ const shift = mv.merged.range[0] - mv.range[0];
2855
+ const ch = new this.THREE.Color(mv.charHex);
2856
+ const heat = new this.THREE.Color(mv.glowHex);
2857
+ const k = clamp(mv.charK, 0, 1);
2858
+ const gk = clamp(mv.glowK, 0, 1);
2859
+ const boost = 1 + Math.max(0, mv.glowK - 1);
2860
+ for (let i = mv.range[0]; i < mv.range[1]; i++) {
2861
+ const o = (i + shift) * oSize;
2862
+ let r = oArr ? oArr[o] : 1;
2863
+ let g = oArr ? oArr[o + 1] : 1;
2864
+ let b = oArr ? oArr[o + 2] : 1;
2865
+ if (k > 0) {
2866
+ r = lerp(r, ch.r, k);
2867
+ g = lerp(g, ch.g, k);
2868
+ b = lerp(b, ch.b, k);
2869
+ }
2870
+ if (gk > 0) {
2871
+ r = lerp(r, heat.r * boost, gk);
2872
+ g = lerp(g, heat.g * boost, gk);
2873
+ b = lerp(b, heat.b * boost, gk);
2874
+ }
2875
+ arr[i * size] = r;
2876
+ arr[i * size + 1] = g;
2877
+ arr[i * size + 2] = b;
2878
+ }
2879
+ mv.touched = true;
2880
+ attr.needsUpdate = true;
2881
+ }
2882
+ /**
2883
+ * 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
2884
+ * new mesh that takes the merged mesh's transform and material, then attached to `group`; the range is collapsed in the merged mesh. Decals
2885
+ * go with it.
2886
+ */
2887
+ extract(mv, group) {
2888
+ if (!mv.host || !mv.range || !mv.merged || mv.loose) return;
2889
+ const host = mv.host;
2890
+ const [v0, v1] = mv.range;
2891
+ const src = host.mesh.geometry;
2892
+ const geo = new this.THREE.BufferGeometry();
2893
+ const count = v1 - v0;
2894
+ for (const name of Object.keys(src.attributes)) {
2895
+ const a = src.getAttribute(name);
2896
+ if (a.isInterleavedBufferAttribute) continue;
2897
+ const size = a.itemSize;
2898
+ const from = (name === POS_ATTR ? host.work : null) ?? a.array;
2899
+ const slice = from.slice(v0 * size, v1 * size);
2900
+ const Ctor = a.array.constructor;
2901
+ geo.setAttribute(name, new this.THREE.BufferAttribute(new Ctor(slice), size, a.normalized));
2902
+ }
2903
+ const index = src.getIndex();
2904
+ if (index) {
2905
+ const out = [];
2906
+ for (let i = 0; i + 2 < index.count; i += 3) {
2907
+ const a = index.getX(i);
2908
+ const b = index.getX(i + 1);
2909
+ const c = index.getX(i + 2);
2910
+ if (a >= v0 && a < v1 && b >= v0 && b < v1 && c >= v0 && c < v1) out.push(a - v0, b - v0, c - v0);
2911
+ }
2912
+ geo.setIndex(count > 65535 ? new this.THREE.Uint32BufferAttribute(out, 1) : new this.THREE.Uint16BufferAttribute(out, 1));
2913
+ }
2914
+ const mats = materialsOf(host.mesh.material).map((m) => m.clone());
2915
+ const loose = new this.THREE.Mesh(geo, mats[0]);
2916
+ loose.name = `${host.mesh.name || "mesh"}:${v0}-${v1}`;
2917
+ loose.castShadow = host.mesh.castShadow;
2918
+ loose.receiveShadow = host.mesh.receiveShadow;
2919
+ loose.renderOrder = host.mesh.renderOrder;
2920
+ loose.frustumCulled = host.mesh.frustumCulled;
2921
+ loose.layers.mask = host.mesh.layers.mask;
2922
+ loose.position.copy(host.mesh.position);
2923
+ loose.quaternion.copy(host.mesh.quaternion);
2924
+ loose.scale.copy(host.mesh.scale);
2925
+ const parent = host.mesh.parent;
2926
+ if (parent) parent.add(loose);
2927
+ loose.updateMatrixWorld(true);
2928
+ this.collapse(mv);
2929
+ for (const d of mv.decals) {
2930
+ d.obj.visible = true;
2931
+ loose.add(d.obj);
2932
+ }
2933
+ const lp = positionAttr(geo);
2934
+ const lh = {
2935
+ mesh: loose,
2936
+ origGeometry: geo,
2937
+ origMaterial: loose.material,
2938
+ ownGeo: true,
2939
+ work: new Float32Array(lp.array),
2940
+ ownMat: true,
2941
+ sharers: [mv]
2942
+ };
2943
+ mv.host = lh;
2944
+ mv.range = [0, count];
2945
+ mv.mesh = loose;
2946
+ mv.loose = true;
2947
+ mv.moved = true;
2948
+ group.attach(loose);
2949
+ for (const m of materialsOf(loose.material)) m.needsUpdate = true;
2950
+ }
2951
+ restoreRanged(mv) {
2952
+ const merged = mv.merged;
2953
+ const host = merged.host;
2954
+ for (const d of mv.decals) this.disposeDecal(d);
2955
+ mv.decals.length = 0;
2956
+ if (mv.loose) {
2957
+ const loose = mv.mesh;
2958
+ loose.parent?.remove(loose);
2959
+ loose.geometry.dispose();
2960
+ for (const m of materialsOf(loose.material)) m.dispose();
2961
+ mv.host = host;
2962
+ mv.range = [merged.range[0], merged.range[1]];
2963
+ mv.mesh = host.mesh;
2964
+ mv.loose = false;
2965
+ mv.moved = false;
2966
+ }
2967
+ mv.collapsed = false;
2968
+ mv.hidden = false;
2969
+ mv.meltApplied = 0;
2970
+ mv.charK = 0;
2971
+ mv.glowK = 0;
2972
+ mv.touched = false;
2973
+ if (!host.ownGeo) return;
2974
+ const work = host.work;
2975
+ const op = positionAttr(host.origGeometry);
2976
+ const [v0, v1] = merged.range;
2977
+ if (op) work.set(op.array.subarray(v0 * 3, v1 * 3), v0 * 3);
2978
+ const attr = host.mesh.geometry.getAttribute("color");
2979
+ if (attr) {
2980
+ const orig = host.origGeometry.getAttribute("color");
2981
+ const size = attr.itemSize;
2982
+ const a = attr.array;
2983
+ for (let i = v0; i < v1; i++) {
2984
+ for (let c = 0; c < 3; c++) a[i * size + c] = orig ? orig.array[i * orig.itemSize + c] : 1;
2985
+ }
2986
+ attr.needsUpdate = true;
2987
+ }
2988
+ if (host.sharers.every((s) => !s.touched)) {
2989
+ host.mesh.geometry.dispose();
2990
+ host.mesh.geometry = host.origGeometry;
2991
+ host.work = null;
2992
+ host.ownGeo = false;
2993
+ if (host.ownMat) {
2994
+ for (const m of materialsOf(host.mesh.material)) m.dispose();
2995
+ host.mesh.material = host.origMaterial;
2996
+ host.ownMat = false;
2997
+ }
2998
+ } else this.rebuildHost(host);
2646
2999
  }
2647
3000
  /** Bring one mesh back to how it was when it was bound. */
2648
3001
  restoreMesh(mv) {
3002
+ if (mv.merged) {
3003
+ this.restoreRanged(mv);
3004
+ return;
3005
+ }
2649
3006
  const mesh = mv.mesh;
2650
3007
  for (const d of mv.decals) this.disposeDecal(d);
2651
3008
  mv.decals.length = 0;
@@ -2745,6 +3102,10 @@ void main() {
2745
3102
  function materialsOf2(m) {
2746
3103
  return Array.isArray(m) ? m : [m];
2747
3104
  }
3105
+ function isMeshRange(v) {
3106
+ const r = v;
3107
+ return !!r.mesh && Array.isArray(r.range);
3108
+ }
2748
3109
  function createDestructionRenderer(THREE, options) {
2749
3110
  return new Renderer(THREE, options);
2750
3111
  }
@@ -2838,12 +3199,23 @@ void main() {
2838
3199
  const partIds = new Set((state?.parts ?? []).map((p) => p.id));
2839
3200
  const byPart = /* @__PURE__ */ new Map();
2840
3201
  const taken = /* @__PURE__ */ new Set();
3202
+ const put = (pid, e) => {
3203
+ const l = byPart.get(pid);
3204
+ if (l) l.push(e);
3205
+ else byPart.set(pid, [e]);
3206
+ };
2841
3207
  const add = (pid, m) => {
2842
3208
  if (taken.has(m)) return;
2843
3209
  taken.add(m);
2844
- const l = byPart.get(pid);
2845
- if (l) l.push(m);
2846
- else byPart.set(pid, [m]);
3210
+ put(pid, { mesh: m });
3211
+ };
3212
+ const addRange = (pid, r) => {
3213
+ const n = r.mesh.geometry.getAttribute("position")?.count ?? 0;
3214
+ const [v0, v1] = r.range;
3215
+ if (!Number.isInteger(v0) || !Number.isInteger(v1) || v0 < 0 || v1 <= v0 || v1 > n) {
3216
+ throw new Error(`game-destruction-three: invalid vertex range [${v0}, ${v1}) for part "${pid}" (the mesh has ${n} vertices)`);
3217
+ }
3218
+ put(pid, { mesh: r.mesh, range: [v0, v1] });
2847
3219
  };
2848
3220
  const meshesIn = (o) => {
2849
3221
  const out = [];
@@ -2854,7 +3226,10 @@ void main() {
2854
3226
  };
2855
3227
  if (options.parts) {
2856
3228
  for (const [pid, v] of Object.entries(options.parts)) {
2857
- for (const o of Array.isArray(v) ? v : [v]) for (const m of meshesIn(o)) add(pid, m);
3229
+ for (const o of Array.isArray(v) ? v : [v]) {
3230
+ if (isMeshRange(o)) addRange(pid, o);
3231
+ else for (const m of meshesIn(o)) add(pid, m);
3232
+ }
2858
3233
  }
2859
3234
  } else {
2860
3235
  const leftovers = [];
@@ -2867,14 +3242,15 @@ void main() {
2867
3242
  if (leftovers.length) this.matchByBox(objectId, root, leftovers, add);
2868
3243
  }
2869
3244
  const bound = { id: objectId, root, sync: options.syncTransform !== false, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: false };
2870
- for (const pid of partIds) bound.parts.set(pid, this.makePartVis(objectId, pid, state?.parts.find((p) => p.id === pid)?.material ?? "", byPart.get(pid) ?? []));
3245
+ const hosts = /* @__PURE__ */ new Map();
3246
+ 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));
2871
3247
  for (const [pid, meshes] of byPart) {
2872
- if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes));
3248
+ if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes, hosts));
2873
3249
  }
2874
3250
  this.bounds.set(objectId, bound);
2875
3251
  }
2876
- makePartVis(home, id, material, meshes) {
2877
- return { id, home, material, meshes: meshes.map((m) => this.visuals.makeMeshVis(m, home)), charApplied: 0, glowApplied: 0, meltApplied: 0, gone: "none" };
3252
+ makePartVis(home, id, material, meshes, hosts) {
3253
+ 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" };
2878
3254
  }
2879
3255
  /** Match unlabelled meshes to the part whose shape contains the centre of the mesh's bounding box. */
2880
3256
  matchByBox(objectId, root, meshes, add) {
@@ -2934,11 +3310,13 @@ void main() {
2934
3310
  const inv = root.matrixWorld.clone().invert();
2935
3311
  const total = new this.THREE.Box3();
2936
3312
  const m4 = new this.THREE.Matrix4();
2937
- for (const m of meshes) {
2938
- const g = m.geometry;
2939
- if (!g.boundingBox) g.computeBoundingBox();
2940
- if (!g.boundingBox || g.boundingBox.isEmpty()) continue;
2941
- total.union(g.boundingBox.clone().applyMatrix4(m4.multiplyMatrices(inv, m.matrixWorld)));
3313
+ const lb = new this.THREE.Box3();
3314
+ for (const mv of meshes) {
3315
+ const m = mv.mesh;
3316
+ m.updateWorldMatrix(true, false);
3317
+ this.visuals.localBox(mv, lb);
3318
+ if (lb.isEmpty()) continue;
3319
+ total.union(lb.clone().applyMatrix4(m4.multiplyMatrices(inv, m.matrixWorld)));
2942
3320
  }
2943
3321
  if (total.isEmpty()) return { half: [0.25, 0.25, 0.25], center: new this.THREE.Vector3() };
2944
3322
  const size = total.getSize(new this.THREE.Vector3());
@@ -2946,7 +3324,7 @@ void main() {
2946
3324
  }
2947
3325
  meshesOf(b) {
2948
3326
  const out = [];
2949
- for (const pv of b.parts.values()) if (pv.gone === "none") for (const mv of pv.meshes) out.push(mv.mesh);
3327
+ for (const pv of b.parts.values()) if (pv.gone === "none") for (const mv of pv.meshes) out.push(mv);
2950
3328
  return out;
2951
3329
  }
2952
3330
  massOf(objectId) {
@@ -3077,6 +3455,9 @@ void main() {
3077
3455
  case "detach":
3078
3456
  this.onDetach(ev);
3079
3457
  break;
3458
+ case "collapse":
3459
+ this.splitLoose(ev.object);
3460
+ break;
3080
3461
  case "disassemble_progress": {
3081
3462
  const k = `unscrew:${ev.joint}`;
3082
3463
  if (this.time - (this.throttles.get(k) ?? -1) > 0.25) {
@@ -3195,8 +3576,8 @@ void main() {
3195
3576
  let best = null;
3196
3577
  let bd = Infinity;
3197
3578
  for (const mv of pv.meshes) {
3198
- if (!mv.mesh.visible) continue;
3199
- const d = this.box.setFromObject(mv.mesh).distanceToPoint(p);
3579
+ if (!this.visuals.shown(mv)) continue;
3580
+ const d = this.visuals.worldBox(mv, this.box).distanceToPoint(p);
3200
3581
  if (d < bd) {
3201
3582
  bd = d;
3202
3583
  best = mv;
@@ -3216,7 +3597,7 @@ void main() {
3216
3597
  const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
3217
3598
  const wn = this.worldDir(b, ev.dent.normal);
3218
3599
  for (const mv of pv.meshes) {
3219
- if (!mv.mesh.visible) continue;
3600
+ if (!this.visuals.shown(mv)) continue;
3220
3601
  this.visuals.applyDent(mv, wp, wn, ev.dent);
3221
3602
  }
3222
3603
  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);
@@ -3307,16 +3688,20 @@ void main() {
3307
3688
  }
3308
3689
  onDetach(ev) {
3309
3690
  this.sound("detach", this.rootPoint(ev.object), 0.5);
3691
+ this.splitLoose(ev.object);
3692
+ }
3693
+ /** Turn every loose group of the object (a detach, a collapse) into a piece of its own. */
3694
+ splitLoose(objectId) {
3310
3695
  if (!this.autoSplit) return;
3311
- const b = this.bounds.get(ev.object);
3696
+ const b = this.bounds.get(objectId);
3312
3697
  if (!b) return;
3313
- const st = this.world.state(ev.object);
3698
+ const st = this.world.state(objectId);
3314
3699
  if (!st || st.groups.length < 2) return;
3315
3700
  const loose = st.groups.length - 1;
3316
3701
  for (let i = 0; i < loose; i++) {
3317
3702
  let newId;
3318
3703
  try {
3319
- newId = this.world.splitGroup(ev.object, 1);
3704
+ newId = this.world.splitGroup(objectId, 1);
3320
3705
  } catch {
3321
3706
  break;
3322
3707
  }
@@ -3327,7 +3712,7 @@ void main() {
3327
3712
  const { half, center } = this.measure(b.root, this.meshesOf(b));
3328
3713
  old.halfExtents = half;
3329
3714
  this.pieceSys.setCenter(old, center);
3330
- old.mass = Math.max(0.05, this.massOf(ev.object));
3715
+ old.mass = Math.max(0.05, this.massOf(objectId));
3331
3716
  old.sleeping = false;
3332
3717
  }
3333
3718
  }
@@ -3349,8 +3734,11 @@ void main() {
3349
3734
  from.parts.delete(p.id);
3350
3735
  nb.parts.set(p.id, pv);
3351
3736
  for (const mv of pv.meshes) {
3352
- group.attach(mv.mesh);
3353
- mv.moved = true;
3737
+ if (mv.host && !mv.loose) this.visuals.extract(mv, group);
3738
+ else {
3739
+ group.attach(mv.mesh);
3740
+ mv.moved = true;
3741
+ }
3354
3742
  }
3355
3743
  }
3356
3744
  this.bounds.set(newId, nb);
@@ -3466,7 +3854,7 @@ void main() {
3466
3854
  }
3467
3855
  onGone(objectId, pv, reason, def) {
3468
3856
  const bb = new this.THREE.Box3();
3469
- for (const mv of pv.meshes) bb.union(this.box.setFromObject(mv.mesh));
3857
+ for (const mv of pv.meshes) bb.union(this.visuals.worldBox(mv, this.box));
3470
3858
  this.visuals.hide(pv, reason);
3471
3859
  if (bb.isEmpty()) return;
3472
3860
  const c = bb.getCenter(new this.THREE.Vector3());
@@ -3483,7 +3871,7 @@ void main() {
3483
3871
  const bb = this.box;
3484
3872
  bb.makeEmpty();
3485
3873
  const tmp = new this.THREE.Box3();
3486
- for (const mv of pv.meshes) if (mv.mesh.visible) bb.union(tmp.setFromObject(mv.mesh));
3874
+ for (const mv of pv.meshes) if (this.visuals.shown(mv)) bb.union(this.visuals.worldBox(mv, tmp));
3487
3875
  let cx = f.point[0];
3488
3876
  let cy = f.point[1];
3489
3877
  let cz = f.point[2];