@adatechnology/game-floorplan-three 0.4.2 → 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.
@@ -3551,8 +3551,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
3551
3551
  var OPEN_ROOM_TYPES = /* @__PURE__ */ new Set(["balcony", "terrace", "veranda", "balcony_gourmet"]);
3552
3552
  var rectsOf = (c) => c.parts && c.parts.length ? c.parts : [[c.u0, c.v0, c.u1, c.v1]];
3553
3553
  var isOpenArea = (c) => OPEN_ROOM_TYPES.has(c.type) || c.outdoor === true;
3554
- function planLod(houses, focus, budget) {
3555
- const l = houses.map((c) => ({ c, d: c.distance(focus), n0: c.stats.drawCalls, n1: c.stats.drawCallsLOD1, level: 0 })).sort((a, b) => a.d - b.d);
3554
+ function planLod(houses2, focus, budget) {
3555
+ const l = houses2.map((c) => ({ c, d: c.distance(focus), n0: c.stats.drawCalls, n1: c.stats.drawCallsLOD1, level: 0 })).sort((a, b) => a.d - b.d);
3556
3556
  let total = l.reduce((s, x) => s + x.n0, 0);
3557
3557
  for (let i = l.length - 1; i >= 0 && total > budget; i--) {
3558
3558
  total -= l[i].n0 - l[i].n1;
@@ -10695,12 +10695,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
10695
10695
  A.invalidate = () => {
10696
10696
  for (let i = 0; i < NS; i++) PL[i].shadow.needsUpdate = true;
10697
10697
  };
10698
- A.update = function(houses, focus, op) {
10698
+ A.update = function(houses2, focus, op) {
10699
10699
  op = op || {};
10700
- houses = (Array.isArray(houses) ? houses : [houses]).filter(Boolean);
10700
+ houses2 = (Array.isArray(houses2) ? houses2 : [houses2]).filter(Boolean);
10701
10701
  _f.set(focus.x, focus.y, focus.z);
10702
10702
  const rooms = [], exterior = [];
10703
- for (const house of houses) {
10703
+ for (const house of houses2) {
10704
10704
  if (house.group && !house.group.visible) continue;
10705
10705
  house.house.updateMatrixWorld(true);
10706
10706
  const M = house.house.matrixWorld, by = /* @__PURE__ */ new Map();
@@ -11232,6 +11232,47 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11232
11232
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
11233
11233
  };
11234
11234
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
11235
+ var splinterBox = (inp, box3, limit = inp.maxFragments) => {
11236
+ const { rnd } = inp;
11237
+ const dims = box3.s;
11238
+ const [fmin, fmax] = inp.fragmentSize;
11239
+ const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
11240
+ const [a, b] = [0, 1, 2].filter((i) => i !== g);
11241
+ const w = Math.max(fmin * 1.5, 0.01);
11242
+ const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
11243
+ const maxLen = fmax * 4;
11244
+ const cells = bsp(box3, c, [a, b], rnd);
11245
+ const pieces = [];
11246
+ for (const cell of cells) {
11247
+ const extra = rnd() < 0.4 ? 1 : 0;
11248
+ const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
11249
+ for (const p of bsp(cell, k, [g], rnd)) pieces.push(p);
11250
+ }
11251
+ return pieces.slice(0, limit).map((p) => ({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] }));
11252
+ };
11253
+ var severAt = (inp, root, at, gap) => {
11254
+ const dims = root.s;
11255
+ const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
11256
+ const half = dims[L] / 2;
11257
+ const lo = clamp(at - gap / 2, -half, half), hi = clamp(at + gap / 2, -half, half);
11258
+ const out = [];
11259
+ const stub = (from, to) => {
11260
+ if (to - from < 0.02) return;
11261
+ const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
11262
+ c[L] = (from + to) / 2;
11263
+ sz[L] = to - from;
11264
+ out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
11265
+ };
11266
+ stub(-half, lo);
11267
+ stub(hi, half);
11268
+ const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
11269
+ band.c[L] = (lo + hi) / 2;
11270
+ band.s[L] = Math.max(hi - lo, 0.01);
11271
+ if (inp.style === "snap") {
11272
+ for (const b of bsp(band, Math.max(3, Math.min(6, inp.maxFragments - out.length)), [0, 1, 2], inp.rnd)) out.push({ center: b.c, size: b.s, q: quat.identity(), shape: "chunk", weight: b.s[0] * b.s[1] * b.s[2] });
11273
+ } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
11274
+ return out;
11275
+ };
11235
11276
  var tearStack = (inp, root, t, u, v, n, sheet) => {
11236
11277
  const { dims, rnd, maxFragments } = inp;
11237
11278
  const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
@@ -11260,6 +11301,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11260
11301
  const t = sorted[0];
11261
11302
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
11262
11303
  if (style === "snap") {
11304
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
11263
11305
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
11264
11306
  for (const b of bsp(root, n2, [0, 1, 2], rnd)) out.push({ center: b.c, size: b.s, q: idq, shape: "slab", weight: b.s[0] * b.s[1] * b.s[2] });
11265
11307
  return out;
@@ -11270,23 +11312,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11270
11312
  return out;
11271
11313
  }
11272
11314
  if (style === "splinter") {
11273
- const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
11274
- const [a, b] = [0, 1, 2].filter((i) => i !== g);
11275
- const w = Math.max(fmin * 1.5, 0.01);
11276
- const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
11277
- const maxLen = fmax * 4;
11278
- const cells = bsp(root, c, [a, b], rnd);
11279
- const pieces = [];
11280
- for (const cell of cells) {
11281
- let list2 = [cell];
11282
- const extra = rnd() < 0.4 ? 1 : 0;
11283
- const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
11284
- list2 = bsp(cell, k, [g], rnd);
11285
- for (const p of list2) pieces.push(p);
11286
- }
11287
- for (const p of pieces.slice(0, maxFragments)) {
11288
- out.push({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] });
11289
- }
11315
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
11316
+ for (const p of splinterBox(inp, root)) out.push(p);
11290
11317
  return out;
11291
11318
  }
11292
11319
  if (style === "tear") {
@@ -11448,6 +11475,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11448
11475
  for (const x of spread(dims[u], COVER, spacing, MAX_BARS_PER_DIRECTION)) out.push(bar(v, dims[v] / 2 - 0.02, [[u, x], [thin, 0]]));
11449
11476
  return out;
11450
11477
  };
11478
+ var clipBarsToBox = (bars, u, v, center, size) => {
11479
+ const out = [];
11480
+ for (const [a, b] of bars) {
11481
+ const axis = a[u] !== b[u] ? u : v;
11482
+ const other = axis === u ? v : u;
11483
+ if (Math.abs(a[other] - center[other]) > size[other] / 2) continue;
11484
+ const lo = Math.max(Math.min(a[axis], b[axis]), center[axis] - size[axis] / 2), hi = Math.min(Math.max(a[axis], b[axis]), center[axis] + size[axis] / 2);
11485
+ if (hi - lo < 1e-6) continue;
11486
+ const p = [a[0], a[1], a[2]], q = [b[0], b[1], b[2]];
11487
+ p[axis] = lo;
11488
+ q[axis] = hi;
11489
+ out.push([p, q]);
11490
+ }
11491
+ return out;
11492
+ };
11493
+ var bendBar = ([a, b], thin, bow, i) => {
11494
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
11495
+ const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
11496
+ m[thin] += (i % 2 === 0 ? 1 : -1) * bow * length;
11497
+ return [[a, m], [m, b]];
11498
+ };
11451
11499
  var clipBarsToDisc = (bars, u, v, center, radius) => {
11452
11500
  const out = [];
11453
11501
  for (const [a, b] of bars) {
@@ -12368,14 +12416,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12368
12416
  smoke: "grey"
12369
12417
  },
12370
12418
  visual: {
12371
- charColor: 1709072
12419
+ charColor: 1709072,
12420
+ innerColor: 14730131
12372
12421
  },
12373
12422
  tags: [
12374
12423
  "wood",
12375
12424
  "organic",
12376
12425
  "flammable"
12377
12426
  ],
12378
- compressive: 1e5
12427
+ compressive: 1e5,
12428
+ hits: {
12429
+ blunt: {
12430
+ tear: "splinters",
12431
+ tearJoules: 60
12432
+ }
12433
+ }
12379
12434
  },
12380
12435
  {
12381
12436
  id: "wood_hard",
@@ -12405,14 +12460,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12405
12460
  smoke: "grey"
12406
12461
  },
12407
12462
  visual: {
12408
- charColor: 1709072
12463
+ charColor: 1709072,
12464
+ innerColor: 13213802
12409
12465
  },
12410
12466
  tags: [
12411
12467
  "wood",
12412
12468
  "organic",
12413
12469
  "flammable"
12414
12470
  ],
12415
- compressive: 15e4
12471
+ compressive: 15e4,
12472
+ hits: {
12473
+ blunt: {
12474
+ tear: "splinters",
12475
+ tearJoules: 150
12476
+ }
12477
+ }
12416
12478
  },
12417
12479
  {
12418
12480
  id: "plywood",
@@ -12442,14 +12504,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12442
12504
  smoke: "grey"
12443
12505
  },
12444
12506
  visual: {
12445
- charColor: 1709072
12507
+ charColor: 1709072,
12508
+ innerColor: 14862234
12446
12509
  },
12447
12510
  tags: [
12448
12511
  "wood",
12449
12512
  "organic",
12450
12513
  "flammable"
12451
12514
  ],
12452
- compressive: 6e4
12515
+ compressive: 6e4,
12516
+ hits: {
12517
+ ballistic: {
12518
+ tear: "splinters"
12519
+ },
12520
+ blunt: {
12521
+ tear: "splinters",
12522
+ tearJoules: 80
12523
+ }
12524
+ }
12453
12525
  },
12454
12526
  {
12455
12527
  id: "mdf",
@@ -12476,14 +12548,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12476
12548
  smoke: "grey"
12477
12549
  },
12478
12550
  visual: {
12479
- charColor: 1709072
12551
+ charColor: 1709072,
12552
+ innerColor: 12096362
12480
12553
  },
12481
12554
  tags: [
12482
12555
  "wood",
12483
12556
  "organic",
12484
12557
  "flammable"
12485
12558
  ],
12486
- compressive: 3e4
12559
+ compressive: 3e4,
12560
+ hits: {
12561
+ ballistic: {
12562
+ tear: "lumps",
12563
+ tearJoules: 120
12564
+ },
12565
+ blunt: {
12566
+ tear: "lumps",
12567
+ tearJoules: 20
12568
+ }
12569
+ }
12487
12570
  },
12488
12571
  {
12489
12572
  id: "glass",
@@ -12597,6 +12680,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12597
12680
  damageScale: {
12598
12681
  explosive: 1.2
12599
12682
  },
12683
+ hits: {
12684
+ ballistic: {
12685
+ unitJoules: 500,
12686
+ floor: 900,
12687
+ crater: true
12688
+ },
12689
+ blunt: {
12690
+ unitJoules: 1500,
12691
+ floor: 60
12692
+ }
12693
+ },
12600
12694
  hitDebris: "chips",
12601
12695
  breakDebris: "dust",
12602
12696
  thermal: {
@@ -12965,7 +13059,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12965
13059
  smoke: "white"
12966
13060
  },
12967
13061
  visual: {
12968
- charColor: 1709072
13062
+ charColor: 1709072,
13063
+ innerColor: 14207126
12969
13064
  },
12970
13065
  tags: [
12971
13066
  "plant",
@@ -12973,7 +13068,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12973
13068
  "organic",
12974
13069
  "flammable"
12975
13070
  ],
12976
- compressive: 6e4
13071
+ compressive: 6e4,
13072
+ hits: {
13073
+ blunt: {
13074
+ tear: "none"
13075
+ }
13076
+ }
12977
13077
  },
12978
13078
  {
12979
13079
  id: "cactus",
@@ -13088,6 +13188,130 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13088
13188
  "masonry"
13089
13189
  ],
13090
13190
  compressive: 2e4
13191
+ },
13192
+ {
13193
+ id: "asphalt",
13194
+ density: 2350,
13195
+ hpPerKg: 10,
13196
+ penetration: 14,
13197
+ penetrationClass: "concrete",
13198
+ penetrationScale: 0.8,
13199
+ ductility: 0.1,
13200
+ fracture: "crumble",
13201
+ fragmentSize: [
13202
+ 0.03,
13203
+ 0.15
13204
+ ],
13205
+ hitDebris: "chips",
13206
+ breakDebris: "dust",
13207
+ thermal: {
13208
+ conductivity: 0.1,
13209
+ heatCapacity: 2,
13210
+ ignitionC: 400,
13211
+ burnRate: 2e-3,
13212
+ smoke: "black"
13213
+ },
13214
+ tags: [
13215
+ "masonry",
13216
+ "brittle"
13217
+ ],
13218
+ compressive: 3e4
13219
+ },
13220
+ {
13221
+ id: "sand",
13222
+ density: 1600,
13223
+ hpPerKg: 2,
13224
+ penetration: 6,
13225
+ ductility: 0.4,
13226
+ fracture: "crumble",
13227
+ fragmentSize: [
13228
+ 0.01,
13229
+ 0.04
13230
+ ],
13231
+ hitDebris: "dust",
13232
+ breakDebris: "dust",
13233
+ thermal: {
13234
+ conductivity: 0.1,
13235
+ heatCapacity: 2
13236
+ },
13237
+ tags: [
13238
+ "soil"
13239
+ ],
13240
+ compressive: 1e4
13241
+ },
13242
+ {
13243
+ id: "gravel",
13244
+ density: 1800,
13245
+ hpPerKg: 3,
13246
+ penetration: 8,
13247
+ ductility: 0.3,
13248
+ fracture: "crumble",
13249
+ fragmentSize: [
13250
+ 0.02,
13251
+ 0.06
13252
+ ],
13253
+ hitDebris: "chips",
13254
+ breakDebris: "dust",
13255
+ thermal: {
13256
+ conductivity: 0.1,
13257
+ heatCapacity: 2
13258
+ },
13259
+ tags: [
13260
+ "soil",
13261
+ "masonry"
13262
+ ],
13263
+ compressive: 2e4
13264
+ },
13265
+ {
13266
+ id: "turf",
13267
+ density: 900,
13268
+ hpPerKg: 3,
13269
+ penetration: 4,
13270
+ ductility: 0.5,
13271
+ fracture: "crumble",
13272
+ fragmentSize: [
13273
+ 0.03,
13274
+ 0.1
13275
+ ],
13276
+ hitDebris: "dust",
13277
+ breakDebris: "dust",
13278
+ thermal: {
13279
+ conductivity: 0.1,
13280
+ heatCapacity: 1.5,
13281
+ ignitionC: 250,
13282
+ burnRate: 0.03,
13283
+ smoke: "white"
13284
+ },
13285
+ tags: [
13286
+ "soil",
13287
+ "organic"
13288
+ ],
13289
+ compressive: 15e3
13290
+ },
13291
+ {
13292
+ id: "mortar",
13293
+ density: 1900,
13294
+ hpPerKg: 10,
13295
+ penetration: 12,
13296
+ penetrationClass: "concrete",
13297
+ penetrationScale: 1.2,
13298
+ ductility: 0.03,
13299
+ fracture: "crumble",
13300
+ fragmentSize: [
13301
+ 0.02,
13302
+ 0.08
13303
+ ],
13304
+ hitDebris: "chips",
13305
+ breakDebris: "dust",
13306
+ thermal: {
13307
+ conductivity: 0.15,
13308
+ heatCapacity: 2.5
13309
+ },
13310
+ tags: [
13311
+ "masonry",
13312
+ "brittle"
13313
+ ],
13314
+ compressive: 1e5
13091
13315
  }
13092
13316
  ];
13093
13317
  var material_hints_default = {
@@ -13261,6 +13485,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13261
13485
  return (1 + 4 * (0.3 - ductility) / 0.3) * Math.min(25, Math.max(1, (0.02 / Math.max(thickness, 1e-4)) ** 2));
13262
13486
  };
13263
13487
  var THIN_SHEET_DISHING = 5e-3;
13488
+ var SWEEP_BACK = 0.01;
13489
+ function sweepOrigins(box3, d, spacing) {
13490
+ const origins = [];
13491
+ const line = (from, to) => {
13492
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
13493
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
13494
+ };
13495
+ for (const axis of [0, 1, 2]) {
13496
+ if (Math.abs(d[axis]) < 1e-3) continue;
13497
+ const lead = d[axis] > 0 ? box3.max[axis] - SWEEP_BACK : box3.min[axis] + SWEEP_BACK;
13498
+ const u = (axis + 1) % 3;
13499
+ const v = (axis + 2) % 3;
13500
+ for (const a of line(box3.min[u], box3.max[u])) {
13501
+ for (const b of line(box3.min[v], box3.max[v])) {
13502
+ const point = [0, 0, 0];
13503
+ point[axis] = lead;
13504
+ point[u] = a;
13505
+ point[v] = b;
13506
+ origins.push(point);
13507
+ }
13508
+ }
13509
+ }
13510
+ return origins;
13511
+ }
13512
+ var THIN_SHEET_CRUSH = 5e-4;
13513
+ var THIN_SHEET_CONTACT = 0.025;
13264
13514
  var DMG_PER_J = 0.02;
13265
13515
  var BLAST_COUPLING = 0.1;
13266
13516
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -13274,9 +13524,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13274
13524
  var BREACH_AREA = 0.25;
13275
13525
  var BREACH_BLAST_REACH = 0.12;
13276
13526
  var RESIDUAL_CAPACITY = 0.15;
13527
+ var SEVER_BAND = 0.1;
13528
+ var SEVER_ASPECT = 3;
13529
+ var SEVER_BALLISTIC = 2;
13530
+ var TEAR_JOULES = 150;
13531
+ var tears = 0;
13277
13532
  var SUPPORT_TOLERANCE = 0.05;
13278
13533
  var UNIT_FRAGMENT_FACTOR = 4;
13279
13534
  var BREACH_GRID_CAP = 4096;
13535
+ var CHIP_ENERGY = 200;
13536
+ var CHIP_SPLASH = 0.5;
13537
+ var BAR_BOW = 0.08;
13280
13538
  var CUT_SLICE = 1;
13281
13539
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
13282
13540
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -13286,8 +13544,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13286
13544
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
13287
13545
  };
13288
13546
  var capacityScale = (p) => {
13289
- if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
13290
- return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
13547
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
13548
+ if (p.st.stripped) return residual;
13549
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
13550
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
13551
+ };
13552
+ var sectionLoss = (p) => {
13553
+ const cells = p.st.chipped;
13554
+ if (!cells?.length) return 0;
13555
+ const [u, v] = planeAxes(p.thin);
13556
+ const along = p.dims[u] >= p.dims[v] ? u : v;
13557
+ const across = along === u ? v : u;
13558
+ let worst = 0;
13559
+ for (const c of cells) {
13560
+ let gone = 0;
13561
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across];
13562
+ worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
13563
+ }
13564
+ return Math.min(1, worst);
13291
13565
  };
13292
13566
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
13293
13567
  var clone = (v) => structuredClone(v);
@@ -13320,6 +13594,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13320
13594
  let windCfg = windOpts ? resolveWind(windOpts) : null;
13321
13595
  const ignitionRules = resolveIgnitionRules(options.ignition);
13322
13596
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
13597
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
13323
13598
  const rng2 = createRng(options.seed ?? 1);
13324
13599
  const rnd = () => rng2.next();
13325
13600
  let time = 0;
@@ -13693,41 +13968,68 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13693
13968
  markDamaged(p);
13694
13969
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
13695
13970
  };
13971
+ const freeSpan = (p, supports) => {
13972
+ if (supports.length === 0) return Infinity;
13973
+ const d = p.dims;
13974
+ const L = [0, 1, 2].sort((a, b) => d[b] - d[a])[0];
13975
+ const spans = supports.map((k) => contactBox(p, k)).map(([lo, hi]) => [lo[L], hi[L]]);
13976
+ let far = 0;
13977
+ for (let i = 0; i <= 16; i++) {
13978
+ const x = (i / 16 - 0.5) * d[L];
13979
+ let near = Infinity;
13980
+ for (const [lo, hi] of spans) near = Math.min(near, Math.max(lo - x, x - hi, 0));
13981
+ far = Math.max(far, near);
13982
+ }
13983
+ return 2 * far;
13984
+ };
13696
13985
  const solveStructure = (o) => {
13697
13986
  o.maxStress = 0;
13698
13987
  if (!o.hasStructure) return;
13699
13988
  const parts = o.parts;
13700
13989
  const n = parts.length;
13701
13990
  const dist = new Array(n).fill(-1);
13702
- const queue = [];
13703
13991
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
13704
13992
  const isPartition = (i) => parts[i].spec.structure?.role === "partition";
13705
- for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
13706
- dist[i] = 0;
13707
- queue.push(i);
13708
- }
13709
- for (let h = 0; h < queue.length; h++) {
13710
- const s = queue[h];
13711
- if (isPartition(s)) continue;
13712
- for (const j of o.byPart.get(parts[s].spec.id)) {
13713
- if (j.st.open || !j.bears) continue;
13714
- const r = j.ia === s ? j.ib : j.ia;
13715
- if (dist[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
13716
- dist[r] = dist[s] + 1;
13717
- queue.push(r);
13993
+ const out = /* @__PURE__ */ new Set();
13994
+ let queue = [];
13995
+ let lowerOf = /* @__PURE__ */ new Map();
13996
+ for (; ; ) {
13997
+ dist.fill(-1);
13998
+ queue = [];
13999
+ for (let i = 0; i < n; i++) if (ok(i) && !out.has(i) && parts[i].anchored) {
14000
+ dist[i] = 0;
14001
+ queue.push(i);
14002
+ }
14003
+ for (let h = 0; h < queue.length; h++) {
14004
+ const s = queue[h];
14005
+ if (isPartition(s)) continue;
14006
+ for (const j of o.byPart.get(parts[s].spec.id)) {
14007
+ if (j.st.open || !j.bears) continue;
14008
+ const r = j.ia === s ? j.ib : j.ia;
14009
+ if (dist[r] >= 0 || !ok(r) || out.has(r) || restsOn(o, j, r) !== s) continue;
14010
+ dist[r] = dist[s] + 1;
14011
+ queue.push(r);
14012
+ }
13718
14013
  }
13719
- }
13720
- const incoming = new Array(n).fill(0);
13721
- const lowerOf = /* @__PURE__ */ new Map();
13722
- for (const i of queue) {
13723
- const lower = [];
13724
- for (const j of o.byPart.get(parts[i].spec.id)) {
13725
- if (j.st.open || !j.bears) continue;
13726
- const k = restsOn(o, j, i);
13727
- if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
14014
+ lowerOf = /* @__PURE__ */ new Map();
14015
+ let dropped = false;
14016
+ for (const i of queue) {
14017
+ const lower = [];
14018
+ for (const j of o.byPart.get(parts[i].spec.id)) {
14019
+ if (j.st.open || !j.bears) continue;
14020
+ const k = restsOn(o, j, i);
14021
+ if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
14022
+ }
14023
+ lowerOf.set(i, lower);
14024
+ const max = parts[i].spec.structure?.maxSpan;
14025
+ if (max !== void 0 && !parts[i].anchored && freeSpan(parts[i], lower.map((k) => parts[k])) > max) {
14026
+ out.add(i);
14027
+ dropped = true;
14028
+ }
13728
14029
  }
13729
- lowerOf.set(i, lower);
14030
+ if (!dropped) break;
13730
14031
  }
14032
+ const incoming = new Array(n).fill(0);
13731
14033
  const bending = new Array(n).fill(0);
13732
14034
  const through = parts.map(() => []);
13733
14035
  const pass = (k, at, mass) => {
@@ -14239,6 +14541,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14239
14541
  const r = rayPart(o, p, origin, d);
14240
14542
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
14241
14543
  if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
14544
+ if (p.st.chipped && insideChip(p, objLocal(o, vec.madd(origin, d, r.t0)))) continue;
14242
14545
  if (!best || r.t0 < best.distance) best = { object: o.id, part: p.spec.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
14243
14546
  }
14244
14547
  }
@@ -14439,6 +14742,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14439
14742
  debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
14440
14743
  setStage(o, p, 1, {}, src);
14441
14744
  };
14745
+ const crater = (o, p, wp, normal, radius, src) => {
14746
+ markDamaged(p);
14747
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
14748
+ debris(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
14749
+ setStage(o, p, 1, {}, src);
14750
+ };
14442
14751
  const insideOpening = (o, p, objectPoint) => {
14443
14752
  const op = p.st.opening;
14444
14753
  if (!op) return false;
@@ -14502,6 +14811,91 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14502
14811
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
14503
14812
  openBreach(o, p, [sum[0] / total, sum[1] / total, sum[2] / total], total, energy, dir, src);
14504
14813
  };
14814
+ const unitGrids = /* @__PURE__ */ new WeakMap();
14815
+ const gridOf = (p) => {
14816
+ let grid = unitGrids.get(p);
14817
+ if (!grid) unitGrids.set(p, grid = unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP));
14818
+ return grid;
14819
+ };
14820
+ const insideChip = (p, objectPoint) => {
14821
+ const cells = p.st.chipped;
14822
+ if (!cells?.length) return false;
14823
+ const [u, v] = planeAxes(p.thin);
14824
+ const lp = toLocal(p.pos, p.q, objectPoint);
14825
+ return cells.some((c) => Math.abs(lp[u] - c.center[u]) <= c.size[u] / 2 && Math.abs(lp[v] - c.center[v]) <= c.size[v] / 2);
14826
+ };
14827
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
14828
+ var _a, _b;
14829
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
14830
+ const rule = p.mat.hits?.[kind];
14831
+ const perKg = rule?.unitJoules ?? chipEnergy;
14832
+ const energy = hitEnergy - (rule?.floor ?? 0);
14833
+ if (perKg <= 0 || energy <= 0) return;
14834
+ const [u, v] = planeAxes(p.thin);
14835
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
14836
+ const grid = gridOf(p);
14837
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
14838
+ const out = [];
14839
+ for (let i = 0; i < grid.length; i++) {
14840
+ const cell = grid[i];
14841
+ if (worn[i] === -1) continue;
14842
+ const gap = Math.hypot(Math.max(0, Math.abs(lp[u] - cell.center[u]) - cell.size[u] / 2), Math.max(0, Math.abs(lp[v] - cell.center[v]) - cell.size[v] / 2));
14843
+ if (gap > radius) continue;
14844
+ const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
14845
+ const total = (worn[i] ?? 0) + energy * share;
14846
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
14847
+ if (total < perKg * mass) {
14848
+ worn[i] = total;
14849
+ continue;
14850
+ }
14851
+ worn[i] = -1;
14852
+ out.push(cell);
14853
+ }
14854
+ if (out.length === 0) return;
14855
+ const { c, q } = partXf(o, p);
14856
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
14857
+ const holes = localHolesOf(p);
14858
+ for (const cell of out) {
14859
+ const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
14860
+ const center = toWorld(c, q, cell.center);
14861
+ const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
14862
+ const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
14863
+ p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
14864
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
14865
+ p.st.mass = Math.max(0, p.st.mass - mass);
14866
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
14867
+ markDamaged(p);
14868
+ setStage(o, p, 1, {}, src);
14869
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
14870
+ debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
14871
+ }
14872
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
14873
+ for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
14874
+ if (p.structural && !isLost(p)) {
14875
+ solveStructure(o);
14876
+ collapseCheck(o, src);
14877
+ }
14878
+ };
14879
+ const exposeRebar = (o, p, src) => {
14880
+ const r = p.spec.reinforcement;
14881
+ if (!r) return;
14882
+ const [u, v] = planeAxes(p.thin);
14883
+ const all = rebarBars(p.dims, p.thin, r.spacing);
14884
+ let bars;
14885
+ if (p.st.stripped) bars = all;
14886
+ else {
14887
+ bars = [];
14888
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
14889
+ const op = p.st.opening;
14890
+ if (op) {
14891
+ const lc = toLocal(p.pos, p.q, op.center);
14892
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
14893
+ }
14894
+ }
14895
+ const bow = clamp2(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
14896
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
14897
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
14898
+ };
14505
14899
  const openBreach = (o, p, center, total, energy, dir, src) => {
14506
14900
  const [u, v] = planeAxes(p.thin);
14507
14901
  const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -14513,7 +14907,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14513
14907
  failPart(o, p, worldCenter, dir, energy, 12, src);
14514
14908
  return;
14515
14909
  }
14516
- const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP), p.thin, lc, radius);
14910
+ const worn = p.rt.chips;
14911
+ const cells = cellsWithin(gridOf(p).filter((_, i) => worn?.[i] !== -1), p.thin, lc, radius);
14517
14912
  const removed = p.st.mass * cells.reduce((sum, cell) => sum + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
14518
14913
  const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
14519
14914
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -14527,10 +14922,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14527
14922
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
14528
14923
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
14529
14924
  const reinforcement = p.spec.reinforcement;
14530
- if (reinforcement) {
14531
- const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
14532
- if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
14533
- }
14925
+ if (reinforcement) exposeRebar(o, p, src);
14534
14926
  if (p.structural) solveStructure(o);
14535
14927
  };
14536
14928
  const stripPart = (o, p, reinforcement, src) => {
@@ -14542,8 +14934,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14542
14934
  p.st.burning = false;
14543
14935
  p.rt.jet = false;
14544
14936
  markDamaged(p);
14545
- emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
14546
14937
  solveStructure(o);
14938
+ exposeRebar(o, p, src);
14547
14939
  collapseCheck(o, src);
14548
14940
  };
14549
14941
  const unitFragmentPlan = (p, cap) => {
@@ -14597,6 +14989,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14597
14989
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
14598
14990
  const hitLocal = toLocal(c, q, hitW);
14599
14991
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
14992
+ const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
14993
+ const cutAt2 = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
14600
14994
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
14601
14995
  style,
14602
14996
  dims: p.dims,
@@ -14606,7 +15000,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14606
15000
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
14607
15001
  maxFragments,
14608
15002
  rnd,
14609
- sheet: p.mat.sheet
15003
+ sheet: p.mat.sheet,
15004
+ ...cutAt2 !== void 0 ? { cut: { at: cutAt2, gap: SEVER_BAND } } : {}
14610
15005
  });
14611
15006
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
14612
15007
  const mass = p.st.mass;
@@ -14665,10 +15060,33 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14665
15060
  emit({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
14666
15061
  };
14667
15062
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
15063
+ const severAxis = (p) => {
15064
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
15065
+ const d = p.dims;
15066
+ const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
15067
+ return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
15068
+ };
15069
+ const bandOf = (o, p, axis, origin) => {
15070
+ const { c, q } = partXf(o, p);
15071
+ const along = toLocal(c, q, origin)[axis];
15072
+ const L = p.dims[axis];
15073
+ return clamp2(Math.floor((along + L / 2) / SEVER_BAND), 0, Math.ceil(L / SEVER_BAND) - 1);
15074
+ };
14668
15075
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
15076
+ var _a;
14669
15077
  if (isLost(p) || amount <= 0) return 0;
14670
15078
  const prev = p.st.hp;
14671
15079
  p.st.hp -= amount;
15080
+ const axis = origin && type !== "fire" && type !== "heat" ? severAxis(p) : -1;
15081
+ if (axis >= 0 && p.st.hp > 0) {
15082
+ const band = bandOf(o, p, axis, origin);
15083
+ const cuts = (_a = p.rt).cuts ?? (_a.cuts = {});
15084
+ cuts[band] = (cuts[band] ?? 0) + amount * (type === "ballistic" ? SEVER_BALLISTIC : 1);
15085
+ if (cuts[band] >= p.st.maxHp * SEVER_BAND / p.dims[axis]) {
15086
+ p.rt.cutAt = -p.dims[axis] / 2 + (band + 0.5) * SEVER_BAND;
15087
+ p.st.hp = 0;
15088
+ }
15089
+ }
14672
15090
  markDamaged(p);
14673
15091
  const trigger = CHAIN_TRIGGER_OF[type];
14674
15092
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -14704,10 +15122,53 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14704
15122
  }
14705
15123
  }
14706
15124
  };
15125
+ const tearOf = (mat, kind) => {
15126
+ const h = mat.hits?.[kind];
15127
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
15128
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
15129
+ };
15130
+ const tearAt = (o, p, kind, entry2, exit, d, E, scale, src) => {
15131
+ const { tear, joules } = tearOf(p.mat, kind);
15132
+ if (tear === "none") return;
15133
+ const shot = kind === "ballistic";
15134
+ const n = shot ? exit ? clamp2(Math.round(E / joules), 1, 7) : 1 : clamp2(Math.floor(E / joules), 0, 5);
15135
+ if (n === 0) return;
15136
+ const fibre = tear === "lumps";
15137
+ const { q } = partXf(o, p);
15138
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
15139
+ const fragments = [];
15140
+ for (let i = 0; i < n; i++) {
15141
+ const back = !exit || i % 3 === 2;
15142
+ const len = scale * (fibre ? 1.2 + 1.2 * rnd() : 3 + 4 * rnd()), w = scale * (0.8 + 0.8 * rnd()), t = scale * (fibre ? 0.7 + 0.6 * rnd() : 0.4 + 0.4 * rnd());
15143
+ const size = [w, w, w];
15144
+ size[along] = len;
15145
+ size[(along + 1) % 3] = w;
15146
+ size[(along + 2) % 3] = t;
15147
+ const spread3 = [(rnd() - 0.5) * 0.8, (rnd() - 0.2) * 0.8, (rnd() - 0.5) * 0.8];
15148
+ const speed = shot ? back ? 1.5 + 2 * rnd() : 2 + 4 * rnd() : 0.8 + 1.2 * rnd();
15149
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
15150
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
15151
+ fragments.push({
15152
+ id: `${p.spec.id}#t${tears++}-${i}`,
15153
+ part: p.spec.id,
15154
+ material: p.spec.material,
15155
+ shape: fibre ? "chunk" : "splinter",
15156
+ center: back || !exit ? vec.madd(entry2, d, -0.01) : vec.madd(exit, d, 0.01),
15157
+ size,
15158
+ rotation: q,
15159
+ velocity: v,
15160
+ inner: true,
15161
+ angularVelocity: [(rnd() - 0.5) * 30, (rnd() - 0.5) * 30, (rnd() - 0.5) * 30],
15162
+ mass
15163
+ });
15164
+ }
15165
+ const patch = [scale * 4, scale * 4, scale * 4];
15166
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments });
15167
+ };
14707
15168
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
14708
15169
  const mat = p.mat;
14709
15170
  const piercing = round3.piercing ?? 1;
14710
- if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2))) {
15171
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2)) || insideChip(p, objLocal(o, entry2))) {
14711
15172
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry2, d, Math.min(pathLen, p.thickness)), damage: 0 };
14712
15173
  }
14713
15174
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -14732,6 +15193,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14732
15193
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
14733
15194
  }
14734
15195
  if (p.layers.length > 0) spallOuter(o, p, entry2, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
15196
+ else if (mat.hits?.ballistic?.crater) crater(o, p, entry2, n, clamp2(0.02 + Math.sqrt(E) * 15e-4, 0.02, 0.15), src);
14735
15197
  const brittle = mat.ductility < 0.3;
14736
15198
  const ratio = Ep / Math.max(thr, 1e-6);
14737
15199
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -14765,7 +15227,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14765
15227
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
14766
15228
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
14767
15229
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
14768
- const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry2, d, E * 0.1, 12, src);
15230
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
15231
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
15232
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
15233
+ chipUnits(o, p, "ballistic", entry2, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
14769
15234
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
14770
15235
  };
14771
15236
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -14773,14 +15238,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14773
15238
  const thrMm = mat.penetration * p.thickness * 1e3;
14774
15239
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
14775
15240
  const Ec = E * fragile;
15241
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
15242
+ const fold = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
15243
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
14776
15244
  if (E > 0) {
14777
15245
  if (kind === "blunt") {
14778
- if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 1e-4, 1e-3, 0.06), src);
15246
+ if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(patch, 0.015), clamp2(E * 1e-4 * fold, 1e-3, sheet ? Math.min(0.06, Math.min(...p.dims) * 0.45) : 0.06), src);
14779
15247
  else if (mat.ductility < 0.3) {
14780
15248
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
14781
15249
  } else if (E >= thrMm * 0.2) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 6e-5, 1e-3, 0.04), src);
14782
15250
  } else if (mat.fracture === "tear") {
14783
15251
  addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
15252
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
15253
+ addHole(o, p, point, n, clamp2(contactR, 0.01, 0.08), true, void 0, src);
15254
+ if (sheet) addDent(o, p, point, n, Math.max(patch * 0.6, 0.01), clamp2(E * 4e-5 * fold, 5e-4, 0.02), src);
14784
15255
  } else if (mat.ductility >= 0.5) {
14785
15256
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
14786
15257
  }
@@ -14793,6 +15264,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14793
15264
  const scale = mat.damageScale?.[kind] ?? 1;
14794
15265
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
14795
15266
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
15267
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
15268
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp2(contactR * 0.2, 6e-3, 0.02), src);
14796
15269
  return done;
14797
15270
  };
14798
15271
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -15371,6 +15844,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15371
15844
  if (p.rt.stressT <= 0) {
15372
15845
  p.rt.stressT = 0.5;
15373
15846
  emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
15847
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
15374
15848
  }
15375
15849
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
15376
15850
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -15587,6 +16061,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15587
16061
  const r = raycastInternal(origin, direction, maxDistance);
15588
16062
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
15589
16063
  },
16064
+ sweepBox(box3, direction, distance2, options2 = {}) {
16065
+ const d = vec.normalize(direction);
16066
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
16067
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
16068
+ let best = null;
16069
+ for (const origin of sweepOrigins(box3, d, options2.spacing ?? 0.2)) {
16070
+ const r = raycastInternal(origin, d, distance2 + SWEEP_BACK, ignore);
16071
+ if (!r || best && r.distance >= best.distance) continue;
16072
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
16073
+ }
16074
+ return best;
16075
+ },
15590
16076
  shoot(origin, direction, energy, caliber, source, round3) {
15591
16077
  const extras = typeof round3 === "number" ? { piercing: round3 } : round3 ?? {};
15592
16078
  return withCollapses(run(() => {
@@ -18152,7 +18638,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18152
18638
  }
18153
18639
  return spec;
18154
18640
  };
18155
- var partSpecOf = (part, capacity, detailed) => detailed && part.assembly ? assemblySpec(part, capacity) : boxSpec(part, capacity);
18641
+ var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
18642
+ var CAGED_ROLES = /* @__PURE__ */ new Set(["beam", "column"]);
18643
+ var caged = (spec, part, rebar) => {
18644
+ if (!rebar || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
18645
+ const size = spec.shape.kind === "box" ? spec.shape.size : null;
18646
+ if (!size) return spec;
18647
+ const thin = Math.min(...size);
18648
+ return {
18649
+ ...spec,
18650
+ reinforcement: { material: rebar, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter },
18651
+ units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
18652
+ };
18653
+ };
18654
+ var partSpecOf = (part, capacity, detailed, rebar) => {
18655
+ if (detailed && part.assembly) return assemblySpec(part, capacity);
18656
+ const spec = boxSpec(part, capacity);
18657
+ return detailed ? caged(spec, part, rebar) : spec;
18658
+ };
18156
18659
  function toDestructibleSpec(structure, options = {}) {
18157
18660
  const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
18158
18661
  const kept = structure.parts.filter((part) => !options.filter || options.filter(part));
@@ -18169,7 +18672,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18169
18672
  const sizing = { safety, redundancy, safetyOthers };
18170
18673
  const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts, panels.supports, sizing) };
18171
18674
  const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
18172
- const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse"));
18675
+ const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
18173
18676
  const margin = options.terrainMargin ?? TERRAIN_MARGIN;
18174
18677
  const x0 = Math.min(...parts.map((part) => part.aabb[0]));
18175
18678
  const z0 = Math.min(...parts.map((part) => part.aabb[1]));
@@ -19049,7 +19552,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19049
19552
  if (!pc || pc.unit) return sp;
19050
19553
  if (pc.role !== "wall" && pc.role !== "partition") {
19051
19554
  const def = getAssembly(pc.assembly || (pc.material === "concrete" && ["slab", "beam", "column", "stair"].includes(pc.role) ? "reinforced_concrete" : ""));
19052
- return def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
19555
+ const out = def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
19556
+ if (out.reinforcement && !out.units && pc.material === "concrete" && (pc.role === "beam" || pc.role === "column") && sp.shape.kind === "box") {
19557
+ return { ...out, units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, Math.min(...sp.shape.size)], bond: "stack", mortar: "concrete" } };
19558
+ }
19559
+ return out;
19053
19560
  }
19054
19561
  if (!pc.assembly) return sp;
19055
19562
  const [u0, v0, u1, v1] = pc.aabb, alongU = u1 - u0 >= v1 - v0;
@@ -19106,7 +19613,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19106
19613
  }
19107
19614
  return null;
19108
19615
  };
19109
- function courses(part, units, opening, paint) {
19616
+ const local = (p) => {
19617
+ house.updateMatrixWorld(true);
19618
+ const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
19619
+ return [v.x, v.y, v.z];
19620
+ };
19621
+ function courses(part, units, opening, chips, paint) {
19110
19622
  const [u0, v0, u1, v1] = part.aabb;
19111
19623
  const alongU = u1 - u0 >= v1 - v0;
19112
19624
  const len = alongU ? u1 - u0 : v1 - v0;
@@ -19130,23 +19642,54 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19130
19642
  const x = alongU ? u0 + mid : (u0 + u1) / 2;
19131
19643
  const z = alongU ? (v0 + v1) / 2 : v0 + mid;
19132
19644
  const y = part.y0 + (r + 0.5) * rowH;
19133
- const d = Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]);
19134
- if (d < opening.radius) continue;
19135
- list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: d < opening.radius + 0.25 || paint === null ? edge : paint });
19645
+ const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
19646
+ if (opening && d < opening.radius) continue;
19647
+ const gone = (c) => Math.abs(c[1] - y) <= rowH / 2 && (alongU ? c[0] >= u0 + a && c[0] <= u0 + b : c[2] >= v0 + a && c[2] <= v0 + b);
19648
+ if (chips.some(gone)) continue;
19649
+ const near = opening && d < opening.radius + 0.25 || chips.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
19650
+ list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint === null ? edge : paint });
19136
19651
  }
19137
19652
  }
19138
19653
  return list2;
19139
19654
  }
19140
- function open(event) {
19141
- if (lod !== 0 || views.has(event.part) || !event.opening) return false;
19142
- const part = structure.byId.get(event.part);
19143
- const spec = opts.spec()?.parts?.find((q) => q.id === event.part);
19144
- if (!part || !spec || !spec.units) return false;
19145
- const items = structure.meshesOf(event.part).filter((it) => it.mesh);
19146
- if (!items.length) return false;
19147
- const list2 = courses(part, spec.units, event.opening, wallColour(items));
19655
+ const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
19656
+ const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
19657
+ function hostOf(partId) {
19658
+ if (structure.meshesOf(partId).some((it) => it.mesh)) return partId;
19659
+ const part = structure.byId.get(partId);
19660
+ if (!part) return null;
19661
+ for (const q of structure.parts) {
19662
+ if (q.id !== partId && same(q.aabb, part.aabb) && Math.abs(q.y1 - part.y0) < 0.01 && structure.meshesOf(q.id).some((it) => it.mesh)) return q.id;
19663
+ }
19664
+ return null;
19665
+ }
19666
+ const riders = (hostId) => structure.parts.filter((q) => q.id !== hostId && specOf(q.id)?.units && hostOf(q.id) === hostId && !structure.meshesOf(q.id).some((it) => it.mesh)).map((q) => q.id);
19667
+ function draw2(partId, change) {
19668
+ if (lod !== 0) return false;
19669
+ const hostId = hostOf(partId);
19670
+ if (!hostId || !specOf(partId)?.units) return false;
19671
+ let view = views.get(hostId);
19672
+ if (!view) {
19673
+ const items = structure.meshesOf(hostId).filter((it) => it.mesh);
19674
+ if (!items.length) return false;
19675
+ const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
19676
+ view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
19677
+ }
19678
+ if (!view.chips.has(partId)) return false;
19679
+ if (change.opening) {
19680
+ if (view.openings.has(partId)) return false;
19681
+ view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
19682
+ if (partId === hostId) view.opening = view.openings.get(partId);
19683
+ }
19684
+ if (change.chip) view.chips.get(partId).push(local(change.chip));
19685
+ const list2 = [];
19686
+ for (const [id, chips] of view.chips) list2.push(...courses(structure.byId.get(id), specOf(id).units, view.openings.get(id) ?? null, chips, view.paint));
19687
+ if (view.mesh) {
19688
+ view.mesh.removeFromParent();
19689
+ view.mesh.dispose();
19690
+ }
19148
19691
  const mesh2 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
19149
- mesh2.name = "breach:" + event.part;
19692
+ mesh2.name = "breach:" + hostId;
19150
19693
  list2.forEach((u, i) => {
19151
19694
  tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
19152
19695
  mesh2.setMatrixAt(i, tmp.m);
@@ -19158,8 +19701,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19158
19701
  mesh2.castShadow = true;
19159
19702
  mesh2.receiveShadow = true;
19160
19703
  house.add(mesh2);
19161
- const saved = items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19162
- views.set(event.part, { mesh: mesh2, saved, units: list2.length, opening: event.opening });
19704
+ if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19705
+ view.mesh = mesh2;
19706
+ view.units = list2.length;
19707
+ views.set(hostId, view);
19163
19708
  invalidate();
19164
19709
  invalidateShadow();
19165
19710
  return true;
@@ -19173,7 +19718,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19173
19718
  return {
19174
19719
  /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
19175
19720
  stage(event) {
19176
- return event.stage === 2 ? open(event) : false;
19721
+ return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
19722
+ },
19723
+ /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
19724
+ chip(event) {
19725
+ return draw2(event.part, { chip: event.center });
19177
19726
  },
19178
19727
  /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
19179
19728
  drop(partId) {
@@ -19405,6 +19954,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19405
19954
  case "stage":
19406
19955
  breach.stage(e);
19407
19956
  break;
19957
+ case "chip":
19958
+ breach.chip(e);
19959
+ break;
19408
19960
  case "collapse":
19409
19961
  for (const id of e.parts) {
19410
19962
  breach.drop(id);
@@ -19459,6 +20011,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19459
20011
  if (D.active && D.world.has(D.id)) D.world.remove(D.id);
19460
20012
  D.active = false;
19461
20013
  };
20014
+ D.follow = (world, id, spec) => {
20015
+ D.spec = spec;
20016
+ const offFollow = world.on("*", (e) => {
20017
+ if (e.object !== id) return;
20018
+ switch (e.type) {
20019
+ case "stage":
20020
+ breach.stage(e);
20021
+ break;
20022
+ case "chip":
20023
+ breach.chip(e);
20024
+ break;
20025
+ case "fracture":
20026
+ breach.drop(e.part);
20027
+ break;
20028
+ case "collapse":
20029
+ for (const p of e.parts) breach.drop(p);
20030
+ break;
20031
+ default:
20032
+ break;
20033
+ }
20034
+ });
20035
+ return { restore: () => breach.restore(), dispose: () => {
20036
+ offFollow();
20037
+ breach.restore();
20038
+ } };
20039
+ };
19462
20040
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
19463
20041
  D.state = () => D.active ? D.world.state(D.id) : null;
19464
20042
  D.reset = () => {
@@ -21296,6 +21874,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21296
21874
  return {
21297
21875
  id: FLOORPLAN_AREA_ID,
21298
21876
  name: text.name,
21877
+ ground: { surface: "lawn" },
21299
21878
  group: text.group,
21300
21879
  get houses() {
21301
21880
  return placed;
@@ -22580,6 +23159,56 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22580
23159
 
22581
23160
  // src/furniture/spec.ts
22582
23161
  var WOOD_THIN = 0.035;
23162
+ var BOARD_TOP_THICK = 0.07;
23163
+ var BOARD_TOP_WIDTH = 0.5;
23164
+ var BOARD_WIDTH = 0.16;
23165
+ var SHELF_SPAN = 1.5;
23166
+ var SHELVING = /shelf|bookcase|rack/;
23167
+ function carcass(parts, drawn) {
23168
+ const all = [...drawn.values()];
23169
+ const width = Math.max(...all.map((p) => p.max[0])) - Math.min(...all.map((p) => p.min[0]));
23170
+ for (const part of parts) {
23171
+ if (LOOSE.has(part.material) || part.shape.kind !== "box" || part.rotation) continue;
23172
+ const [x, y, z] = part.shape.size;
23173
+ if (y >= x && y >= z) {
23174
+ if (z < x && x >= 0.6 * width) continue;
23175
+ part.structure = { role: "column" };
23176
+ } else if (y <= x && y <= z) part.structure = { role: "beam", maxSpan: SHELF_SPAN * Math.max(x, z) };
23177
+ }
23178
+ }
23179
+ function splitTopsIntoBoards(drawn, materialOf2, carcassed) {
23180
+ const tops = /* @__PURE__ */ new Map();
23181
+ if (carcassed) return tops;
23182
+ for (let i = drawn.length - 1; i >= 0; i--) {
23183
+ const p = drawn[i];
23184
+ if (p.key !== "wood" || materialOf2.wood !== "wood_hard" || p.shape.kind !== "box" || p.rotation) continue;
23185
+ const [sx, sy, sz] = p.shape.size;
23186
+ const across = sx < sz ? 0 : 2;
23187
+ const width = Math.min(sx, sz);
23188
+ if (sy < WOOD_THIN || sy > BOARD_TOP_THICK || sy >= width || width < BOARD_TOP_WIDTH) continue;
23189
+ const count = Math.round(width / BOARD_WIDTH);
23190
+ const boards = [];
23191
+ for (let b = 0; b < count; b++) {
23192
+ const size = [sx, sy, sz];
23193
+ size[across] = width / count;
23194
+ const position = [...p.position];
23195
+ position[across] += (b + 0.5) * size[across] - width / 2;
23196
+ const min = [...p.min];
23197
+ const max = [...p.max];
23198
+ min[across] = position[across] - size[across] / 2;
23199
+ max[across] = position[across] + size[across] / 2;
23200
+ boards.push({ ...p, id: `${p.id}_b${b}`, shape: { kind: "box", size }, position, min, max, color: shadeBoard(p.color, b) });
23201
+ }
23202
+ drawn.splice(i, 1, ...boards);
23203
+ tops.set(p.id, boards);
23204
+ }
23205
+ return tops;
23206
+ }
23207
+ function shadeBoard(color2, board) {
23208
+ const k = [1, 0.94, 1.05, 0.97, 1.03][board % 5];
23209
+ const channel = (shift) => Math.min(255, Math.round((color2 >> shift & 255) * k)) << shift;
23210
+ return channel(16) | channel(8) | channel(0);
23211
+ }
22583
23212
  var FLOOR_TOUCH = 5e-3;
22584
23213
  var LOOSE = /* @__PURE__ */ new Set(["paper"]);
22585
23214
  var BASE_MATERIAL = {
@@ -22657,10 +23286,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22657
23286
  };
22658
23287
  run(A);
22659
23288
  if (!drawn.length) throw new Error(`game-floorplan-three: the builder of "${name}" drew no box or cylinder`);
23289
+ const boardsOf = splitTopsIntoBoards(drawn, materialOf2, Boolean(opts.carcass));
22660
23290
  const parts = drawn.map((p) => {
22661
23291
  const thin = Math.min(...p.shape.kind === "box" ? p.shape.size : p.shape.kind === "cylinder" ? [p.shape.radius * 2, p.shape.height] : [p.shape.radius * 2]);
22662
23292
  let material = materialOf2[p.key] ?? "plastic";
22663
- if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
23293
+ if (p.key === "wood" && material === "wood_hard" && opts.carcass && !opts.materials?.wood) material = "mdf";
23294
+ else if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
22664
23295
  const part = { id: p.id, material, shape: p.shape, position: p.position };
22665
23296
  if (p.rotation) part.rotation = p.rotation;
22666
23297
  if (!LOOSE.has(material) && p.min[1] <= FLOOR_TOUCH) part.anchored = true;
@@ -22695,6 +23326,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22695
23326
  if (area3 < 0.6 * footprint || p.max[1] < 0.2) continue;
22696
23327
  if (!top || p.max[1] > top.y + 1e-6) top = { part: p.id, y: p.max[1] };
22697
23328
  }
23329
+ for (const boards of boardsOf.values()) {
23330
+ if (top && boards[0].max[1] <= top.y - 1e-6) continue;
23331
+ const mid = boards[boards.length >> 1];
23332
+ const min = [Math.min(...boards.map((b) => b.min[0])), Math.min(...boards.map((b) => b.min[2]))];
23333
+ const max = [Math.max(...boards.map((b) => b.max[0])), Math.max(...boards.map((b) => b.max[2]))];
23334
+ if ((max[0] - min[0]) * (max[1] - min[1]) >= 0.6 * footprint && mid.max[1] >= 0.2) top = { part: mid.id, y: mid.max[1], parts: boards.map((b) => b.id) };
23335
+ }
23336
+ if (opts.carcass) carcass(parts, byId);
22698
23337
  const spec = { name, parts, joints, tags: ["furniture"] };
22699
23338
  const colors = new Map(drawn.map((p) => [p.id, p.color]));
22700
23339
  const build = (T3) => {
@@ -22738,7 +23377,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22738
23377
  if (w === void 0 || d === void 0 || h === void 0) throw new Error(`game-floorplan-three: furniture type "${type}" is not in the catalogue, so give w, d and h`);
22739
23378
  const o = opts.options ?? {};
22740
23379
  const seed = Math.round(opts.seed ?? 1);
22741
- const plain2 = /shelf|bookcase|rack/.test(type) ? "paper" : "plastic";
23380
+ const plain2 = SHELVING.test(type) ? "paper" : "plastic";
22742
23381
  const root = { x: seed, z: 0, ry: 0, w, d, h, y: 0, base: 0, palette: o.palette, seed: `furniture_${type}_${seed}` };
22743
23382
  const { K, shared } = renderer.internals;
22744
23383
  const before = K.PARTS.length;
@@ -22757,7 +23396,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22757
23396
  shared.ctx = saved.ctx;
22758
23397
  }
22759
23398
  },
22760
- { materials: { plain: plain2, ...o.materials }, chairs: o.chairs }
23399
+ { materials: { plain: plain2, ...o.materials }, chairs: o.chairs, carcass: SHELVING.test(type) }
22761
23400
  );
22762
23401
  const spouts = K.PARTS.slice(before).filter((p) => p.type === "water_tap").map((p) => [p.pos[0] - root.x, p.pos[1], p.pos[2] - root.z]);
22763
23402
  K.PARTS.length = before;
@@ -22824,7 +23463,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22824
23463
  function draw(THREE, plan, options) {
22825
23464
  return create3(THREE, { ...options, fleet: null }).build(plan, { quality: "game", terrain: false, noPerimeter: true, pieces: "meshes" });
22826
23465
  }
23466
+ var houses = /* @__PURE__ */ new WeakMap();
22827
23467
  function label(house) {
23468
+ houses.set(house.group, house);
22828
23469
  for (const [id, items] of Object.entries(house.destruction.ranges())) {
22829
23470
  for (const item of items) {
22830
23471
  const object2 = item.isObject3D ? item : item.mesh;
@@ -22837,12 +23478,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22837
23478
  const plan = wallPlan(options);
22838
23479
  const houseOptions = options.house ?? {};
22839
23480
  const probe = draw(THREE, plan, { ...houseOptions, destruction: void 0, destructionRenderer: void 0 });
22840
- const spec = probe.structure.spec({ wallDetail: "coarse" });
23481
+ const spec = probe.structure.spec();
22841
23482
  probe.dispose();
22842
23483
  return {
22843
23484
  plan,
22844
23485
  spec,
22845
- build: (T2) => label(draw(T2, plan, houseOptions))
23486
+ build: (T2) => label(draw(T2, plan, houseOptions)),
23487
+ follow: (root, world, objectId) => houses.get(root)?.destruction?.follow?.(world, objectId, spec) ?? null
22846
23488
  };
22847
23489
  }
22848
23490
  return __toCommonJS(index_exports);