@adatechnology/game-floorplan-three 0.5.0 → 0.7.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.
@@ -11268,7 +11268,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11268
11268
  const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
11269
11269
  band.c[L] = (lo + hi) / 2;
11270
11270
  band.s[L] = Math.max(hi - lo, 0.01);
11271
- for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
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);
11272
11274
  return out;
11273
11275
  };
11274
11276
  var tearStack = (inp, root, t, u, v, n, sheet) => {
@@ -11299,6 +11301,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11299
11301
  const t = sorted[0];
11300
11302
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
11301
11303
  if (style === "snap") {
11304
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
11302
11305
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
11303
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] });
11304
11307
  return out;
@@ -11472,6 +11475,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11472
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]]));
11473
11476
  return out;
11474
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
+ };
11475
11499
  var clipBarsToDisc = (bars, u, v, center, radius) => {
11476
11500
  const out = [];
11477
11501
  for (const [a, b] of bars) {
@@ -12392,14 +12416,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12392
12416
  smoke: "grey"
12393
12417
  },
12394
12418
  visual: {
12395
- charColor: 1709072
12419
+ charColor: 1709072,
12420
+ innerColor: 14730131
12396
12421
  },
12397
12422
  tags: [
12398
12423
  "wood",
12399
12424
  "organic",
12400
12425
  "flammable"
12401
12426
  ],
12402
- compressive: 1e5
12427
+ compressive: 1e5,
12428
+ hits: {
12429
+ blunt: {
12430
+ tear: "splinters",
12431
+ tearJoules: 60
12432
+ }
12433
+ }
12403
12434
  },
12404
12435
  {
12405
12436
  id: "wood_hard",
@@ -12429,14 +12460,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12429
12460
  smoke: "grey"
12430
12461
  },
12431
12462
  visual: {
12432
- charColor: 1709072
12463
+ charColor: 1709072,
12464
+ innerColor: 13213802
12433
12465
  },
12434
12466
  tags: [
12435
12467
  "wood",
12436
12468
  "organic",
12437
12469
  "flammable"
12438
12470
  ],
12439
- compressive: 15e4
12471
+ compressive: 15e4,
12472
+ hits: {
12473
+ blunt: {
12474
+ tear: "splinters",
12475
+ tearJoules: 150
12476
+ }
12477
+ }
12440
12478
  },
12441
12479
  {
12442
12480
  id: "plywood",
@@ -12466,14 +12504,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12466
12504
  smoke: "grey"
12467
12505
  },
12468
12506
  visual: {
12469
- charColor: 1709072
12507
+ charColor: 1709072,
12508
+ innerColor: 14862234
12470
12509
  },
12471
12510
  tags: [
12472
12511
  "wood",
12473
12512
  "organic",
12474
12513
  "flammable"
12475
12514
  ],
12476
- compressive: 6e4
12515
+ compressive: 6e4,
12516
+ hits: {
12517
+ ballistic: {
12518
+ tear: "splinters"
12519
+ },
12520
+ blunt: {
12521
+ tear: "splinters",
12522
+ tearJoules: 80
12523
+ }
12524
+ }
12477
12525
  },
12478
12526
  {
12479
12527
  id: "mdf",
@@ -12500,14 +12548,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12500
12548
  smoke: "grey"
12501
12549
  },
12502
12550
  visual: {
12503
- charColor: 1709072
12551
+ charColor: 1709072,
12552
+ innerColor: 12096362
12504
12553
  },
12505
12554
  tags: [
12506
12555
  "wood",
12507
12556
  "organic",
12508
12557
  "flammable"
12509
12558
  ],
12510
- 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
+ }
12511
12570
  },
12512
12571
  {
12513
12572
  id: "glass",
@@ -12621,8 +12680,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12621
12680
  damageScale: {
12622
12681
  explosive: 1.2
12623
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
+ },
12624
12694
  hitDebris: "chips",
12625
12695
  breakDebris: "dust",
12696
+ visual: { innerColor: 10197652 },
12626
12697
  thermal: {
12627
12698
  conductivity: 0.15,
12628
12699
  heatCapacity: 2.5
@@ -12653,6 +12724,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12653
12724
  ],
12654
12725
  hitDebris: "chips",
12655
12726
  breakDebris: "dust",
12727
+ visual: { innerColor: 12085567 },
12656
12728
  thermal: {
12657
12729
  conductivity: 0.15,
12658
12730
  heatCapacity: 2.5
@@ -12683,6 +12755,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12683
12755
  ],
12684
12756
  hitDebris: "dust",
12685
12757
  breakDebris: "dust",
12758
+ visual: { innerColor: 14999507 },
12686
12759
  thermal: {
12687
12760
  conductivity: 0.1,
12688
12761
  heatCapacity: 1.5
@@ -12708,6 +12781,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12708
12781
  ],
12709
12782
  hitDebris: "chips",
12710
12783
  breakDebris: "dust",
12784
+ visual: { innerColor: 9407364 },
12711
12785
  thermal: {
12712
12786
  conductivity: 0.2,
12713
12787
  heatCapacity: 2.5
@@ -12989,7 +13063,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12989
13063
  smoke: "white"
12990
13064
  },
12991
13065
  visual: {
12992
- charColor: 1709072
13066
+ charColor: 1709072,
13067
+ innerColor: 14207126
12993
13068
  },
12994
13069
  tags: [
12995
13070
  "plant",
@@ -12997,7 +13072,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12997
13072
  "organic",
12998
13073
  "flammable"
12999
13074
  ],
13000
- compressive: 6e4
13075
+ compressive: 6e4,
13076
+ hits: {
13077
+ blunt: {
13078
+ tear: "none"
13079
+ }
13080
+ }
13001
13081
  },
13002
13082
  {
13003
13083
  id: "cactus",
@@ -13112,6 +13192,130 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13112
13192
  "masonry"
13113
13193
  ],
13114
13194
  compressive: 2e4
13195
+ },
13196
+ {
13197
+ id: "asphalt",
13198
+ density: 2350,
13199
+ hpPerKg: 10,
13200
+ penetration: 14,
13201
+ penetrationClass: "concrete",
13202
+ penetrationScale: 0.8,
13203
+ ductility: 0.1,
13204
+ fracture: "crumble",
13205
+ fragmentSize: [
13206
+ 0.03,
13207
+ 0.15
13208
+ ],
13209
+ hitDebris: "chips",
13210
+ breakDebris: "dust",
13211
+ thermal: {
13212
+ conductivity: 0.1,
13213
+ heatCapacity: 2,
13214
+ ignitionC: 400,
13215
+ burnRate: 2e-3,
13216
+ smoke: "black"
13217
+ },
13218
+ tags: [
13219
+ "masonry",
13220
+ "brittle"
13221
+ ],
13222
+ compressive: 3e4
13223
+ },
13224
+ {
13225
+ id: "sand",
13226
+ density: 1600,
13227
+ hpPerKg: 2,
13228
+ penetration: 6,
13229
+ ductility: 0.4,
13230
+ fracture: "crumble",
13231
+ fragmentSize: [
13232
+ 0.01,
13233
+ 0.04
13234
+ ],
13235
+ hitDebris: "dust",
13236
+ breakDebris: "dust",
13237
+ thermal: {
13238
+ conductivity: 0.1,
13239
+ heatCapacity: 2
13240
+ },
13241
+ tags: [
13242
+ "soil"
13243
+ ],
13244
+ compressive: 1e4
13245
+ },
13246
+ {
13247
+ id: "gravel",
13248
+ density: 1800,
13249
+ hpPerKg: 3,
13250
+ penetration: 8,
13251
+ ductility: 0.3,
13252
+ fracture: "crumble",
13253
+ fragmentSize: [
13254
+ 0.02,
13255
+ 0.06
13256
+ ],
13257
+ hitDebris: "chips",
13258
+ breakDebris: "dust",
13259
+ thermal: {
13260
+ conductivity: 0.1,
13261
+ heatCapacity: 2
13262
+ },
13263
+ tags: [
13264
+ "soil",
13265
+ "masonry"
13266
+ ],
13267
+ compressive: 2e4
13268
+ },
13269
+ {
13270
+ id: "turf",
13271
+ density: 900,
13272
+ hpPerKg: 3,
13273
+ penetration: 4,
13274
+ ductility: 0.5,
13275
+ fracture: "crumble",
13276
+ fragmentSize: [
13277
+ 0.03,
13278
+ 0.1
13279
+ ],
13280
+ hitDebris: "dust",
13281
+ breakDebris: "dust",
13282
+ thermal: {
13283
+ conductivity: 0.1,
13284
+ heatCapacity: 1.5,
13285
+ ignitionC: 250,
13286
+ burnRate: 0.03,
13287
+ smoke: "white"
13288
+ },
13289
+ tags: [
13290
+ "soil",
13291
+ "organic"
13292
+ ],
13293
+ compressive: 15e3
13294
+ },
13295
+ {
13296
+ id: "mortar",
13297
+ density: 1900,
13298
+ hpPerKg: 10,
13299
+ penetration: 12,
13300
+ penetrationClass: "concrete",
13301
+ penetrationScale: 1.2,
13302
+ ductility: 0.03,
13303
+ fracture: "crumble",
13304
+ fragmentSize: [
13305
+ 0.02,
13306
+ 0.08
13307
+ ],
13308
+ hitDebris: "chips",
13309
+ breakDebris: "dust",
13310
+ thermal: {
13311
+ conductivity: 0.15,
13312
+ heatCapacity: 2.5
13313
+ },
13314
+ tags: [
13315
+ "masonry",
13316
+ "brittle"
13317
+ ],
13318
+ compressive: 1e5
13115
13319
  }
13116
13320
  ];
13117
13321
  var material_hints_default = {
@@ -13285,6 +13489,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13285
13489
  return (1 + 4 * (0.3 - ductility) / 0.3) * Math.min(25, Math.max(1, (0.02 / Math.max(thickness, 1e-4)) ** 2));
13286
13490
  };
13287
13491
  var THIN_SHEET_DISHING = 5e-3;
13492
+ var SWEEP_BACK = 0.01;
13493
+ function sweepOrigins(box3, d, spacing) {
13494
+ const origins = [];
13495
+ const line = (from, to) => {
13496
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
13497
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
13498
+ };
13499
+ for (const axis of [0, 1, 2]) {
13500
+ if (Math.abs(d[axis]) < 1e-3) continue;
13501
+ const lead = d[axis] > 0 ? box3.max[axis] - SWEEP_BACK : box3.min[axis] + SWEEP_BACK;
13502
+ const u = (axis + 1) % 3;
13503
+ const v = (axis + 2) % 3;
13504
+ for (const a of line(box3.min[u], box3.max[u])) {
13505
+ for (const b of line(box3.min[v], box3.max[v])) {
13506
+ const point = [0, 0, 0];
13507
+ point[axis] = lead;
13508
+ point[u] = a;
13509
+ point[v] = b;
13510
+ origins.push(point);
13511
+ }
13512
+ }
13513
+ }
13514
+ return origins;
13515
+ }
13516
+ var THIN_SHEET_CRUSH = 5e-4;
13517
+ var THIN_SHEET_CONTACT = 0.025;
13288
13518
  var DMG_PER_J = 0.02;
13289
13519
  var BLAST_COUPLING = 0.1;
13290
13520
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -13301,11 +13531,15 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13301
13531
  var SEVER_BAND = 0.1;
13302
13532
  var SEVER_ASPECT = 3;
13303
13533
  var SEVER_BALLISTIC = 2;
13534
+ var TEAR_JOULES = 150;
13535
+ var tears = 0;
13304
13536
  var SUPPORT_TOLERANCE = 0.05;
13305
13537
  var UNIT_FRAGMENT_FACTOR = 4;
13306
13538
  var BREACH_GRID_CAP = 4096;
13307
13539
  var CHIP_ENERGY = 200;
13308
13540
  var CHIP_SPLASH = 0.5;
13541
+ var BAR_BOW = 0.08;
13542
+ var BULLET_JOINT_SPAN = 0.3;
13309
13543
  var CUT_SLICE = 1;
13310
13544
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
13311
13545
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -13315,8 +13549,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13315
13549
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
13316
13550
  };
13317
13551
  var capacityScale = (p) => {
13318
- if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
13319
- return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
13552
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
13553
+ if (p.st.stripped) return residual;
13554
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
13555
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
13556
+ };
13557
+ var sectionLoss = (p) => {
13558
+ const cells = p.st.chipped;
13559
+ if (!cells?.length) return 0;
13560
+ const [u, v] = planeAxes(p.thin);
13561
+ const along = p.dims[u] >= p.dims[v] ? u : v;
13562
+ const across = along === u ? v : u;
13563
+ let worst = 0;
13564
+ for (const c of cells) {
13565
+ let gone = 0;
13566
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across];
13567
+ worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
13568
+ }
13569
+ return Math.min(1, worst);
13320
13570
  };
13321
13571
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
13322
13572
  var clone = (v) => structuredClone(v);
@@ -14416,7 +14666,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14416
14666
  emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
14417
14667
  setStage(o, p, 1, {}, src);
14418
14668
  };
14419
- const debris = (o, p, kind, point, normal, count, speed, src) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed });
14669
+ const debris = (o, p, kind, point, normal, count, speed, src, material) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed, ...material && material !== p.spec.material ? { material } : {} });
14420
14670
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
14421
14671
  const detachEvents = (o, opened, by, src) => {
14422
14672
  for (const j of opened) emit({ ...base(o, src), type: "detach", joint: j.spec.id, parts: [j.spec.a, j.spec.b], by, groups: o.st.groups.map((g) => [...g]) });
@@ -14457,7 +14707,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14457
14707
  const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
14458
14708
  return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
14459
14709
  };
14460
- const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
14710
+ const jointLength = (o, j) => {
14711
+ const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
14712
+ let len = 0;
14713
+ for (let k = 0; k < 3; k++) len = Math.max(len, Math.min(A.max[k], B.max[k]) - Math.max(A.min[k], B.min[k]));
14714
+ return len;
14715
+ };
14716
+ const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false, span) => {
14461
14717
  if (amount <= 0) return;
14462
14718
  const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
14463
14719
  if (split && mine.length) amount /= mine.length;
@@ -14465,7 +14721,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14465
14721
  if (j.st.open) continue;
14466
14722
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
14467
14723
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
14468
- j.st.hp -= amount;
14724
+ j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
14469
14725
  if (j.st.hp <= 0) {
14470
14726
  j.st.hp = 0;
14471
14727
  j.st.open = true;
@@ -14494,7 +14750,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14494
14750
  if (spalls.length > MAX_SPALLS) spalls.shift();
14495
14751
  markDamaged(p);
14496
14752
  emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer.index, point: wp, area: Math.PI * radius * radius });
14497
- debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
14753
+ debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src, layer.mat.id);
14754
+ setStage(o, p, 1, {}, src);
14755
+ };
14756
+ const crater = (o, p, wp, normal, radius, src) => {
14757
+ markDamaged(p);
14758
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
14759
+ debris(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
14498
14760
  setStage(o, p, 1, {}, src);
14499
14761
  };
14500
14762
  const insideOpening = (o, p, objectPoint) => {
@@ -14573,9 +14835,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14573
14835
  const lp = toLocal(p.pos, p.q, objectPoint);
14574
14836
  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);
14575
14837
  };
14576
- const chipUnits = (o, p, wp, energy, dir, radius, src) => {
14838
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
14577
14839
  var _a, _b;
14578
- if (!p.spec.units || p.spec.reinforcement || !p.staged || chipEnergy <= 0 || energy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
14840
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
14841
+ const rule = p.mat.hits?.[kind];
14842
+ const perKg = rule?.unitJoules ?? chipEnergy;
14843
+ const energy = hitEnergy - (rule?.floor ?? 0);
14844
+ if (perKg <= 0 || energy <= 0) return;
14579
14845
  const [u, v] = planeAxes(p.thin);
14580
14846
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
14581
14847
  const grid = gridOf(p);
@@ -14589,7 +14855,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14589
14855
  const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
14590
14856
  const total = (worn[i] ?? 0) + energy * share;
14591
14857
  const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
14592
- if (total < chipEnergy * mass) {
14858
+ if (total < perKg * mass) {
14593
14859
  worn[i] = total;
14594
14860
  continue;
14595
14861
  }
@@ -14614,7 +14880,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14614
14880
  emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
14615
14881
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
14616
14882
  }
14883
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
14617
14884
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
14885
+ if (p.structural && !isLost(p)) {
14886
+ solveStructure(o);
14887
+ collapseCheck(o, src);
14888
+ }
14889
+ };
14890
+ const exposeRebar = (o, p, src) => {
14891
+ const r = p.spec.reinforcement;
14892
+ if (!r) return;
14893
+ const [u, v] = planeAxes(p.thin);
14894
+ const all = rebarBars(p.dims, p.thin, r.spacing);
14895
+ let bars;
14896
+ if (p.st.stripped) bars = all;
14897
+ else {
14898
+ bars = [];
14899
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
14900
+ const op = p.st.opening;
14901
+ if (op) {
14902
+ const lc = toLocal(p.pos, p.q, op.center);
14903
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
14904
+ }
14905
+ }
14906
+ const bow = clamp2(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
14907
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
14908
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
14618
14909
  };
14619
14910
  const openBreach = (o, p, center, total, energy, dir, src) => {
14620
14911
  const [u, v] = planeAxes(p.thin);
@@ -14642,10 +14933,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14642
14933
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
14643
14934
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
14644
14935
  const reinforcement = p.spec.reinforcement;
14645
- if (reinforcement) {
14646
- const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
14647
- if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
14648
- }
14936
+ if (reinforcement) exposeRebar(o, p, src);
14649
14937
  if (p.structural) solveStructure(o);
14650
14938
  };
14651
14939
  const stripPart = (o, p, reinforcement, src) => {
@@ -14657,8 +14945,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14657
14945
  p.st.burning = false;
14658
14946
  p.rt.jet = false;
14659
14947
  markDamaged(p);
14660
- emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
14661
14948
  solveStructure(o);
14949
+ exposeRebar(o, p, src);
14662
14950
  collapseCheck(o, src);
14663
14951
  };
14664
14952
  const unitFragmentPlan = (p, cap) => {
@@ -14712,7 +15000,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14712
15000
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
14713
15001
  const hitLocal = toLocal(c, q, hitW);
14714
15002
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
14715
- const axis = style === "splinter" ? severAxis(p) : -1;
15003
+ const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
14716
15004
  const cutAt2 = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
14717
15005
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
14718
15006
  style,
@@ -14784,7 +15072,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14784
15072
  };
14785
15073
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
14786
15074
  const severAxis = (p) => {
14787
- if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
15075
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
14788
15076
  const d = p.dims;
14789
15077
  const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
14790
15078
  return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
@@ -14845,6 +15133,49 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14845
15133
  }
14846
15134
  }
14847
15135
  };
15136
+ const tearOf = (mat, kind) => {
15137
+ const h = mat.hits?.[kind];
15138
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
15139
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
15140
+ };
15141
+ const tearAt = (o, p, kind, entry2, exit, d, E, scale, src) => {
15142
+ const { tear, joules } = tearOf(p.mat, kind);
15143
+ if (tear === "none") return;
15144
+ const shot = kind === "ballistic";
15145
+ const n = shot ? exit ? clamp2(Math.round(E / joules), 1, 7) : 1 : clamp2(Math.floor(E / joules), 0, 5);
15146
+ if (n === 0) return;
15147
+ const fibre = tear === "lumps";
15148
+ const { q } = partXf(o, p);
15149
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
15150
+ const fragments = [];
15151
+ for (let i = 0; i < n; i++) {
15152
+ const back = !exit || i % 3 === 2;
15153
+ 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());
15154
+ const size = [w, w, w];
15155
+ size[along] = len;
15156
+ size[(along + 1) % 3] = w;
15157
+ size[(along + 2) % 3] = t;
15158
+ const spread3 = [(rnd() - 0.5) * 0.8, (rnd() - 0.2) * 0.8, (rnd() - 0.5) * 0.8];
15159
+ const speed = shot ? back ? 1.5 + 2 * rnd() : 2 + 4 * rnd() : 0.8 + 1.2 * rnd();
15160
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
15161
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
15162
+ fragments.push({
15163
+ id: `${p.spec.id}#t${tears++}-${i}`,
15164
+ part: p.spec.id,
15165
+ material: p.spec.material,
15166
+ shape: fibre ? "chunk" : "splinter",
15167
+ center: back || !exit ? vec.madd(entry2, d, -0.01) : vec.madd(exit, d, 0.01),
15168
+ size,
15169
+ rotation: q,
15170
+ velocity: v,
15171
+ inner: true,
15172
+ angularVelocity: [(rnd() - 0.5) * 30, (rnd() - 0.5) * 30, (rnd() - 0.5) * 30],
15173
+ mass
15174
+ });
15175
+ }
15176
+ const patch = [scale * 4, scale * 4, scale * 4];
15177
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments });
15178
+ };
14848
15179
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
14849
15180
  const mat = p.mat;
14850
15181
  const piercing = round3.piercing ?? 1;
@@ -14873,6 +15204,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14873
15204
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
14874
15205
  }
14875
15206
  if (p.layers.length > 0) spallOuter(o, p, entry2, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
15207
+ else if (mat.hits?.ballistic?.crater) crater(o, p, entry2, n, clamp2(0.02 + Math.sqrt(E) * 15e-4, 0.02, 0.15), src);
14876
15208
  const brittle = mat.ductility < 0.3;
14877
15209
  const ratio = Ep / Math.max(thr, 1e-6);
14878
15210
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -14905,9 +15237,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14905
15237
  if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
14906
15238
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
14907
15239
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
14908
- wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
14909
- const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry2, d, E * 0.1, 12, src);
14910
- chipUnits(o, p, entry2, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
15240
+ wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
15241
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
15242
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
15243
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
15244
+ 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);
14911
15245
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
14912
15246
  };
14913
15247
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -14915,14 +15249,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14915
15249
  const thrMm = mat.penetration * p.thickness * 1e3;
14916
15250
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
14917
15251
  const Ec = E * fragile;
15252
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
15253
+ const fold = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
15254
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
14918
15255
  if (E > 0) {
14919
15256
  if (kind === "blunt") {
14920
- if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 1e-4, 1e-3, 0.06), src);
15257
+ 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);
14921
15258
  else if (mat.ductility < 0.3) {
14922
15259
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
14923
15260
  } 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);
14924
15261
  } else if (mat.fracture === "tear") {
14925
15262
  addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
15263
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
15264
+ addHole(o, p, point, n, clamp2(contactR, 0.01, 0.08), true, void 0, src);
15265
+ if (sheet) addDent(o, p, point, n, Math.max(patch * 0.6, 0.01), clamp2(E * 4e-5 * fold, 5e-4, 0.02), src);
14926
15266
  } else if (mat.ductility >= 0.5) {
14927
15267
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
14928
15268
  }
@@ -14935,7 +15275,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14935
15275
  const scale = mat.damageScale?.[kind] ?? 1;
14936
15276
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
14937
15277
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
14938
- if (kind === "blunt") chipUnits(o, p, point, E, d, Math.max(contactR, 0.03), src);
15278
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
15279
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp2(contactR * 0.2, 6e-3, 0.02), src);
14939
15280
  return done;
14940
15281
  };
14941
15282
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -15514,6 +15855,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15514
15855
  if (p.rt.stressT <= 0) {
15515
15856
  p.rt.stressT = 0.5;
15516
15857
  emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
15858
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
15517
15859
  }
15518
15860
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
15519
15861
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -15730,6 +16072,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15730
16072
  const r = raycastInternal(origin, direction, maxDistance);
15731
16073
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
15732
16074
  },
16075
+ sweepBox(box3, direction, distance2, options2 = {}) {
16076
+ const d = vec.normalize(direction);
16077
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
16078
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
16079
+ let best = null;
16080
+ for (const origin of sweepOrigins(box3, d, options2.spacing ?? 0.2)) {
16081
+ const r = raycastInternal(origin, d, distance2 + SWEEP_BACK, ignore);
16082
+ if (!r || best && r.distance >= best.distance) continue;
16083
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
16084
+ }
16085
+ return best;
16086
+ },
15733
16087
  shoot(origin, direction, energy, caliber, source, round3) {
15734
16088
  const extras = typeof round3 === "number" ? { piercing: round3 } : round3 ?? {};
15735
16089
  return withCollapses(run(() => {
@@ -18295,7 +18649,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18295
18649
  }
18296
18650
  return spec;
18297
18651
  };
18298
- var partSpecOf = (part, capacity, detailed) => detailed && part.assembly ? assemblySpec(part, capacity) : boxSpec(part, capacity);
18652
+ var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
18653
+ var CAGED_ROLES = /* @__PURE__ */ new Set(["beam", "column"]);
18654
+ var caged = (spec, part, rebar) => {
18655
+ if (!rebar || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
18656
+ const size = spec.shape.kind === "box" ? spec.shape.size : null;
18657
+ if (!size) return spec;
18658
+ const thin = Math.min(...size);
18659
+ return {
18660
+ ...spec,
18661
+ reinforcement: { material: rebar, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter },
18662
+ units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
18663
+ };
18664
+ };
18665
+ var partSpecOf = (part, capacity, detailed, rebar) => {
18666
+ if (detailed && part.assembly) return assemblySpec(part, capacity);
18667
+ const spec = boxSpec(part, capacity);
18668
+ return detailed ? caged(spec, part, rebar) : spec;
18669
+ };
18299
18670
  function toDestructibleSpec(structure, options = {}) {
18300
18671
  const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
18301
18672
  const kept = structure.parts.filter((part) => !options.filter || options.filter(part));
@@ -18312,7 +18683,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18312
18683
  const sizing = { safety, redundancy, safetyOthers };
18313
18684
  const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts, panels.supports, sizing) };
18314
18685
  const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
18315
- const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse"));
18686
+ const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
18316
18687
  const margin = options.terrainMargin ?? TERRAIN_MARGIN;
18317
18688
  const x0 = Math.min(...parts.map((part) => part.aabb[0]));
18318
18689
  const z0 = Math.min(...parts.map((part) => part.aabb[1]));
@@ -19192,7 +19563,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19192
19563
  if (!pc || pc.unit) return sp;
19193
19564
  if (pc.role !== "wall" && pc.role !== "partition") {
19194
19565
  const def = getAssembly(pc.assembly || (pc.material === "concrete" && ["slab", "beam", "column", "stair"].includes(pc.role) ? "reinforced_concrete" : ""));
19195
- return def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
19566
+ const out = def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
19567
+ if (out.reinforcement && !out.units && pc.material === "concrete" && (pc.role === "beam" || pc.role === "column") && sp.shape.kind === "box") {
19568
+ return { ...out, units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, Math.min(...sp.shape.size)], bond: "stack", mortar: "concrete" } };
19569
+ }
19570
+ return out;
19196
19571
  }
19197
19572
  if (!pc.assembly) return sp;
19198
19573
  const [u0, v0, u1, v1] = pc.aabb, alongU = u1 - u0 >= v1 - v0;
@@ -19229,6 +19604,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19229
19604
  pos.array.set(saved, v0 * 3);
19230
19605
  pos.needsUpdate = true;
19231
19606
  }
19607
+ function noise2(x, y, cell) {
19608
+ const h = (i2, j2) => {
19609
+ const s = Math.sin(i2 * 127.1 + j2 * 311.7) * 43758.5453;
19610
+ return s - Math.floor(s);
19611
+ };
19612
+ const fx = x / cell, fy = y / cell, i = Math.floor(fx), j = Math.floor(fy), u = fx - i, v = fy - j;
19613
+ const su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v);
19614
+ return (h(i, j) * (1 - su) + h(i + 1, j) * su) * (1 - sv) + (h(i, j + 1) * (1 - su) + h(i + 1, j + 1) * su) * sv;
19615
+ }
19616
+ var MASK_TEXEL = 0.02;
19617
+ var SPALL_MIN = 0.025;
19618
+ var SPALL_RAGGED = 0.045;
19619
+ var COVER2 = 0.015;
19232
19620
  function installDestructionBreach(K, _shared) {
19233
19621
  const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
19234
19622
  function createBreachView(opts) {
@@ -19238,94 +19626,198 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19238
19626
  const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
19239
19627
  const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
19240
19628
  const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
19241
- const wallColour = (items) => {
19242
- for (const it of items) {
19243
- if (!it.mesh) continue;
19244
- const c = it.mesh.geometry.attributes.color;
19245
- if (!c) continue;
19246
- const a = c.array;
19247
- tmp.c.setRGB(a[it.v0 * 3], a[it.v0 * 3 + 1], a[it.v0 * 3 + 2]);
19248
- return tmp.c.getHex();
19249
- }
19250
- return null;
19251
- };
19252
19629
  const local = (p) => {
19253
19630
  house.updateMatrixWorld(true);
19254
19631
  const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
19255
19632
  return [v.x, v.y, v.z];
19256
19633
  };
19257
- function courses(part, units, opening, chips, paint) {
19634
+ function cells(part, units) {
19258
19635
  const [u0, v0, u1, v1] = part.aabb;
19259
19636
  const alongU = u1 - u0 >= v1 - v0;
19260
19637
  const len = alongU ? u1 - u0 : v1 - v0;
19261
- const thick = alongU ? v1 - v0 : u1 - u0;
19262
19638
  const [ul, uh] = units.size;
19263
19639
  const rows = Math.max(1, Math.round((part.y1 - part.y0) / uh));
19264
19640
  const rowH = (part.y1 - part.y0) / rows;
19265
19641
  const per = Math.max(1, Math.round(len / ul));
19266
19642
  const w = len / per;
19267
- const edge = fragmentMaterial(part.material).color.getHex();
19268
- const list2 = [];
19643
+ const out = [];
19269
19644
  for (let r = 0; r < rows; r++) {
19270
19645
  const shift = units.bond === "running" && r & 1 ? w / 2 : 0;
19271
- const cells = per + (shift ? 1 : 0);
19272
- for (let i = 0; i < cells; i++) {
19273
- let a = i * w - shift, b = a + w;
19274
- a = Math.max(0, a);
19275
- b = Math.min(len, b);
19276
- if (b - a < 1e-3) continue;
19277
- const mid = (a + b) / 2;
19278
- const x = alongU ? u0 + mid : (u0 + u1) / 2;
19279
- const z = alongU ? (v0 + v1) / 2 : v0 + mid;
19280
- const y = part.y0 + (r + 0.5) * rowH;
19281
- const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
19282
- if (opening && d < opening.radius) continue;
19283
- 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);
19284
- if (chips.some(gone)) continue;
19285
- const near = opening && d < opening.radius + 0.25 || chips.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
19286
- list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint === null ? edge : paint });
19646
+ for (let i = 0; i < per + (shift ? 1 : 0); i++) {
19647
+ const a = Math.max(0, i * w - shift), b = Math.min(len, i * w - shift + w);
19648
+ if (b - a > 1e-3) out.push({ a, b, y0: part.y0 + r * rowH, y1: part.y0 + (r + 1) * rowH, r, i });
19287
19649
  }
19288
19650
  }
19289
- return list2;
19651
+ return out;
19652
+ }
19653
+ const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
19654
+ const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
19655
+ function hostOf(partId) {
19656
+ if (structure.meshesOf(partId).some((it) => it.mesh)) return partId;
19657
+ const part = structure.byId.get(partId);
19658
+ if (!part) return null;
19659
+ for (const q of structure.parts) {
19660
+ 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;
19661
+ }
19662
+ return null;
19663
+ }
19664
+ 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);
19665
+ function shell(it, mask) {
19666
+ const g0 = it.mesh.geometry, g = new THREE.BufferGeometry();
19667
+ const src = it.mesh.userData._p0 || g0.attributes.position.array;
19668
+ for (const [name, attr] of Object.entries(g0.attributes)) {
19669
+ const k = attr.itemSize;
19670
+ g.setAttribute(name, new THREE.BufferAttribute((name === "position" ? src : attr.array).slice(it.v0 * k, it.v1 * k), k));
19671
+ }
19672
+ if (g0.index) {
19673
+ const ix = g0.index.array.slice(it.i0, it.i1);
19674
+ for (let k = 0; k < ix.length; k++) ix[k] -= it.v0;
19675
+ g.setIndex(new THREE.BufferAttribute(ix, 1));
19676
+ }
19677
+ g.computeBoundingBox();
19678
+ g.computeBoundingSphere();
19679
+ const base = it.mesh.material;
19680
+ const material = base.clone();
19681
+ const toHouse = { value: new THREE.Matrix4() };
19682
+ material.onBeforeCompile = (sh) => {
19683
+ base.onBeforeCompile?.(sh);
19684
+ sh.uniforms.uToHouse = toHouse;
19685
+ sh.uniforms.uMask = { value: mask.texture };
19686
+ sh.uniforms.uMaskRect = { value: mask.rect };
19687
+ sh.uniforms.uMaskAlongX = { value: mask.alongU ? 1 : 0 };
19688
+ sh.vertexShader = sh.vertexShader.replace("void main() {", "uniform mat4 uToHouse;\nvarying vec3 vHousePos;\nvoid main() {").replace("#include <begin_vertex>", "#include <begin_vertex>\nvHousePos = (uToHouse * vec4(transformed, 1.0)).xyz;");
19689
+ sh.fragmentShader = sh.fragmentShader.replace("void main() {", "uniform sampler2D uMask;\nuniform vec4 uMaskRect;\nuniform float uMaskAlongX;\nvarying vec3 vHousePos;\nvoid main() {\n vec2 mUv = vec2(((uMaskAlongX > 0.5 ? vHousePos.x : vHousePos.z) - uMaskRect.x) / uMaskRect.z, (vHousePos.y - uMaskRect.y) / uMaskRect.w);\n if (mUv.x >= 0.0 && mUv.x <= 1.0 && mUv.y >= 0.0 && mUv.y <= 1.0 && texture2D(uMask, mUv).r > 0.5) discard;");
19690
+ };
19691
+ material.customProgramCacheKey = () => "breachMask" + (base.customProgramCacheKey?.() ?? "");
19692
+ const mesh2 = new THREE.Mesh(g, material);
19693
+ mesh2.name = "breach-shell";
19694
+ mesh2.userData.breachMask = mask;
19695
+ mesh2.position.copy(it.mesh.position);
19696
+ mesh2.quaternion.copy(it.mesh.quaternion);
19697
+ mesh2.scale.copy(it.mesh.scale);
19698
+ mesh2.castShadow = it.mesh.castShadow;
19699
+ mesh2.receiveShadow = it.mesh.receiveShadow;
19700
+ mesh2.renderOrder = it.mesh.renderOrder;
19701
+ mesh2.onBeforeRender = () => {
19702
+ house.updateMatrixWorld();
19703
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
19704
+ };
19705
+ it.mesh.parent.add(mesh2);
19706
+ mesh2.updateMatrixWorld(true);
19707
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
19708
+ return mesh2;
19709
+ }
19710
+ function maskOf(hostId, parts) {
19711
+ const host = structure.byId.get(hostId);
19712
+ const [u0, v0, u1, v1] = host.aabb;
19713
+ const alongU = u1 - u0 >= v1 - v0;
19714
+ const y0 = Math.min(...parts.map((id) => structure.byId.get(id).y0)), y1 = Math.max(...parts.map((id) => structure.byId.get(id).y1));
19715
+ const len = alongU ? u1 - u0 : v1 - v0;
19716
+ const w = Math.max(2, Math.min(2048, Math.ceil(len / MASK_TEXEL))), h = Math.max(2, Math.min(1024, Math.ceil((y1 - y0) / MASK_TEXEL)));
19717
+ const data = new Uint8Array(w * h);
19718
+ const texture = new THREE.DataTexture(data, w, h, THREE.RedFormat);
19719
+ texture.magFilter = THREE.LinearFilter;
19720
+ texture.minFilter = THREE.LinearFilter;
19721
+ texture.needsUpdate = true;
19722
+ return { data, texture, w, h, alongU, start: alongU ? u0 : v0, len, y0, height: y1 - y0, rect: new THREE.Vector4(alongU ? u0 : v0, y0, len, y1 - y0) };
19723
+ }
19724
+ const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
19725
+ function paint(view) {
19726
+ const { mask } = view;
19727
+ const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
19728
+ const holes = [];
19729
+ const discs = [];
19730
+ const shown = [];
19731
+ for (const id of view.parts) {
19732
+ const part = structure.byId.get(id), units2 = specOf(id).units, chips = view.chips.get(id), opening = view.openings.get(id);
19733
+ const all = cells(part, units2);
19734
+ const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
19735
+ const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
19736
+ if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
19737
+ const gone = (c) => chips.some((p) => p[1] >= c.y0 && p[1] <= c.y1 && along(p) >= c.a && along(p) <= c.b) || opening && Math.hypot((c.a + c.b) / 2 - along(opening.center), (c.y0 + c.y1) / 2 - opening.center[1]) < opening.radius;
19738
+ const kept = [];
19739
+ for (const c of all) (gone(c) ? holes : kept).push(c);
19740
+ for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
19741
+ }
19742
+ const reach = SPALL_MIN + SPALL_RAGGED;
19743
+ mask.data.fill(0);
19744
+ const stamp = (s0, s1, y0, y1, inside2) => {
19745
+ const i0 = Math.max(0, Math.floor((s0 - reach) / mask.len * mask.w)), i1 = Math.min(mask.w - 1, Math.ceil((s1 + reach) / mask.len * mask.w));
19746
+ const j0 = Math.max(0, Math.floor((y0 - reach - mask.y0) / mask.height * mask.h)), j1 = Math.min(mask.h - 1, Math.ceil((y1 + reach - mask.y0) / mask.height * mask.h));
19747
+ for (let j = j0; j <= j1; j++) {
19748
+ const y = mask.y0 + (j + 0.5) / mask.h * mask.height;
19749
+ for (let i = i0; i <= i1; i++) {
19750
+ const s = (i + 0.5) / mask.w * mask.len;
19751
+ const edge = SPALL_MIN + SPALL_RAGGED * (0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011));
19752
+ if (inside2(s, y) <= edge) mask.data[j * mask.w + i] = 255;
19753
+ }
19754
+ }
19755
+ };
19756
+ for (const c of holes) stamp(c.a, c.b, c.y0, c.y1, (s, y) => boxDistance(s, y, c.a, c.b, c.y0, c.y1));
19757
+ for (const d of discs) stamp(d.s - d.r, d.s + d.r, d.y - d.r, d.y + d.r, (s, y) => Math.max(0, Math.hypot(s - d.s, y - d.y) - d.r));
19758
+ mask.texture.needsUpdate = true;
19759
+ const near = (c) => holes.some((h) => boxDistance((c.a + c.b) / 2, (c.y0 + c.y1) / 2, h.a - (c.b - c.a) / 2, h.b + (c.b - c.a) / 2, h.y0 - (c.y1 - c.y0) / 2, h.y1 + (c.y1 - c.y0) / 2) <= reach) || discs.some((d) => boxDistance(d.s, d.y, c.a, c.b, c.y0, c.y1) <= d.r + reach);
19760
+ const list2 = [];
19761
+ const tone = (hex2, k) => tmp.c.setHex(hex2).multiplyScalar(k).getHex();
19762
+ for (const { c, part, units: units2, thick, mid } of shown) {
19763
+ if (!near(c)) continue;
19764
+ const centre = mask.start + (c.a + c.b) / 2, y = (c.y0 + c.y1) / 2, len = c.b - c.a, h = c.y1 - c.y0;
19765
+ const at = (s, dy, d, colour) => list2.push({ x: mask.alongU ? centre : mid, y, z: mask.alongU ? mid : centre, sx: mask.alongU ? s : d, sy: dy, sz: mask.alongU ? d : s, colour });
19766
+ const base = fragmentMaterial(part.material).color.getHex();
19767
+ const n = noise2(centre * 3.1, y * 2.7, 0.07);
19768
+ if (units2.mortar === "concrete") {
19769
+ at(len, h, thick - 2 * COVER2 - 0.03 * n, tone(base, 0.86 + 0.2 * n));
19770
+ } else {
19771
+ at(len, h, thick - 2 * COVER2 - 0.012, 10327948);
19772
+ at(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
19773
+ }
19774
+ }
19775
+ if (view.units) {
19776
+ view.units.removeFromParent();
19777
+ view.units.dispose();
19778
+ }
19779
+ const units = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
19780
+ units.name = "breach:" + view.host;
19781
+ list2.forEach((u, i) => {
19782
+ tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
19783
+ units.setMatrixAt(i, tmp.m);
19784
+ units.setColorAt(i, tmp.c.setHex(u.colour));
19785
+ });
19786
+ units.count = list2.length;
19787
+ units.instanceMatrix.needsUpdate = true;
19788
+ if (units.instanceColor) units.instanceColor.needsUpdate = true;
19789
+ units.castShadow = true;
19790
+ units.receiveShadow = true;
19791
+ units.visible = lod === 0;
19792
+ house.add(units);
19793
+ view.units = units;
19794
+ view.gone = holes.length;
19290
19795
  }
19291
19796
  function draw2(partId, change) {
19292
19797
  if (lod !== 0) return false;
19293
- const part = structure.byId.get(partId);
19294
- const spec = opts.spec()?.parts?.find((q) => q.id === partId);
19295
- if (!part || !spec || !spec.units) return false;
19296
- let view = views.get(partId);
19798
+ const hostId = hostOf(partId);
19799
+ if (!hostId || !specOf(partId)?.units) return false;
19800
+ let view = views.get(hostId);
19297
19801
  if (!view) {
19298
- const items = structure.meshesOf(partId).filter((it) => it.mesh);
19802
+ const items = structure.meshesOf(hostId).filter((it) => it.mesh);
19299
19803
  if (!items.length) return false;
19300
- view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: [] };
19804
+ const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
19805
+ if (!parts.includes(partId)) return false;
19806
+ const mask = maskOf(hostId, parts);
19807
+ view = { host: hostId, parts, items, mask, shells: [], saved: null, units: null, gone: 0, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
19301
19808
  }
19809
+ if (!view.chips.has(partId)) return false;
19302
19810
  if (change.opening) {
19303
- if (view.opening) return false;
19304
- view.opening = { center: local(change.opening.center), radius: change.opening.radius };
19811
+ if (view.openings.has(partId)) return false;
19812
+ view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
19305
19813
  }
19306
- if (change.chip) view.chips.push(local(change.chip));
19307
- const list2 = courses(part, spec.units, view.opening, view.chips, view.paint);
19308
- if (view.mesh) {
19309
- view.mesh.removeFromParent();
19310
- view.mesh.dispose();
19814
+ if (change.chip) view.chips.get(partId).push(local(change.chip));
19815
+ if (!view.saved) {
19816
+ view.shells = view.items.map((it) => shell(it, view.mask));
19817
+ view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19311
19818
  }
19312
- const mesh2 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
19313
- mesh2.name = "breach:" + partId;
19314
- list2.forEach((u, i) => {
19315
- tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
19316
- mesh2.setMatrixAt(i, tmp.m);
19317
- mesh2.setColorAt(i, tmp.c.setHex(u.colour));
19318
- });
19319
- mesh2.count = list2.length;
19320
- mesh2.instanceMatrix.needsUpdate = true;
19321
- if (mesh2.instanceColor) mesh2.instanceColor.needsUpdate = true;
19322
- mesh2.castShadow = true;
19323
- mesh2.receiveShadow = true;
19324
- house.add(mesh2);
19325
- if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19326
- view.mesh = mesh2;
19327
- view.units = list2.length;
19328
- views.set(partId, view);
19819
+ paint(view);
19820
+ views.set(hostId, view);
19329
19821
  invalidate();
19330
19822
  invalidateShadow();
19331
19823
  return true;
@@ -19336,30 +19828,40 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19336
19828
  else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
19337
19829
  }
19338
19830
  }
19831
+ function dispose(view) {
19832
+ view.units.removeFromParent();
19833
+ view.units.dispose();
19834
+ for (const m of view.shells) {
19835
+ m.removeFromParent();
19836
+ m.geometry.dispose();
19837
+ m.material.dispose();
19838
+ }
19839
+ view.mask.texture.dispose();
19840
+ }
19339
19841
  return {
19340
- /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
19842
+ /** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
19341
19843
  stage(event) {
19342
19844
  return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
19343
19845
  },
19344
- /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
19846
+ /** Handles a `chip` event: the unit that came out leaves a hole in the wall where it was. Returns true when the wall was cut. */
19345
19847
  chip(event) {
19346
19848
  return draw2(event.part, { chip: event.center });
19347
19849
  },
19348
- /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
19850
+ /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
19349
19851
  drop(partId) {
19350
19852
  const v = views.get(partId);
19351
19853
  if (!v) return;
19352
- v.mesh.removeFromParent();
19353
- v.mesh.dispose();
19854
+ dispose(v);
19354
19855
  views.delete(partId);
19355
19856
  },
19356
- /** LOD 0 draws the courses; LOD 1 and 2 give the wall its box back. */
19857
+ /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
19357
19858
  setLod(level) {
19358
19859
  const was = lod;
19359
19860
  lod = level;
19360
19861
  if (was === 0 === (level === 0)) return;
19361
19862
  for (const v of views.values()) {
19362
- v.mesh.visible = level === 0;
19863
+ v.units.visible = level === 0;
19864
+ for (const m of v.shells) m.visible = level === 0;
19363
19865
  plain2(v, level !== 0);
19364
19866
  }
19365
19867
  invalidate();
@@ -19368,17 +19870,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19368
19870
  /** Everything back to what it was (after `world.reset`). */
19369
19871
  restore() {
19370
19872
  for (const v of views.values()) {
19371
- v.mesh.removeFromParent();
19372
- v.mesh.dispose();
19873
+ dispose(v);
19373
19874
  if (lod === 0) plain2(v, true);
19374
19875
  }
19375
19876
  views.clear();
19376
19877
  },
19377
- /** The wall's opening, if it has courses. */
19878
+ /** The wall has damage drawn. */
19378
19879
  has(partId) {
19379
19880
  return views.has(partId);
19380
19881
  },
19381
- statistics: () => ({ walls: views.size, units: [...views.values()].reduce((n, v) => n + v.units, 0) })
19882
+ /** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
19883
+ statistics: () => ({
19884
+ walls: views.size,
19885
+ gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
19886
+ units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
19887
+ })
19382
19888
  };
19383
19889
  }
19384
19890
  Object.assign(K, { createBreachView });
@@ -21495,6 +22001,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21495
22001
  return {
21496
22002
  id: FLOORPLAN_AREA_ID,
21497
22003
  name: text.name,
22004
+ ground: { surface: "lawn" },
21498
22005
  group: text.group,
21499
22006
  get houses() {
21500
22007
  return placed;
@@ -22779,6 +23286,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22779
23286
 
22780
23287
  // src/furniture/spec.ts
22781
23288
  var WOOD_THIN = 0.035;
23289
+ var BOARD_TOP_THICK = 0.07;
23290
+ var BOARD_TOP_WIDTH = 0.5;
23291
+ var BOARD_WIDTH = 0.16;
22782
23292
  var SHELF_SPAN = 1.5;
22783
23293
  var SHELVING = /shelf|bookcase|rack/;
22784
23294
  function carcass(parts, drawn) {
@@ -22793,6 +23303,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22793
23303
  } else if (y <= x && y <= z) part.structure = { role: "beam", maxSpan: SHELF_SPAN * Math.max(x, z) };
22794
23304
  }
22795
23305
  }
23306
+ function splitTopsIntoBoards(drawn, materialOf2, carcassed) {
23307
+ const tops = /* @__PURE__ */ new Map();
23308
+ if (carcassed) return tops;
23309
+ for (let i = drawn.length - 1; i >= 0; i--) {
23310
+ const p = drawn[i];
23311
+ if (p.key !== "wood" || materialOf2.wood !== "wood_hard" || p.shape.kind !== "box" || p.rotation) continue;
23312
+ const [sx, sy, sz] = p.shape.size;
23313
+ const across = sx < sz ? 0 : 2;
23314
+ const width = Math.min(sx, sz);
23315
+ if (sy < WOOD_THIN || sy > BOARD_TOP_THICK || sy >= width || width < BOARD_TOP_WIDTH) continue;
23316
+ const count = Math.round(width / BOARD_WIDTH);
23317
+ const boards = [];
23318
+ for (let b = 0; b < count; b++) {
23319
+ const size = [sx, sy, sz];
23320
+ size[across] = width / count;
23321
+ const position = [...p.position];
23322
+ position[across] += (b + 0.5) * size[across] - width / 2;
23323
+ const min = [...p.min];
23324
+ const max = [...p.max];
23325
+ min[across] = position[across] - size[across] / 2;
23326
+ max[across] = position[across] + size[across] / 2;
23327
+ boards.push({ ...p, id: `${p.id}_b${b}`, shape: { kind: "box", size }, position, min, max, color: shadeBoard(p.color, b) });
23328
+ }
23329
+ drawn.splice(i, 1, ...boards);
23330
+ tops.set(p.id, boards);
23331
+ }
23332
+ return tops;
23333
+ }
23334
+ function shadeBoard(color2, board) {
23335
+ const k = [1, 0.94, 1.05, 0.97, 1.03][board % 5];
23336
+ const channel = (shift) => Math.min(255, Math.round((color2 >> shift & 255) * k)) << shift;
23337
+ return channel(16) | channel(8) | channel(0);
23338
+ }
22796
23339
  var FLOOR_TOUCH = 5e-3;
22797
23340
  var LOOSE = /* @__PURE__ */ new Set(["paper"]);
22798
23341
  var BASE_MATERIAL = {
@@ -22870,10 +23413,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22870
23413
  };
22871
23414
  run(A);
22872
23415
  if (!drawn.length) throw new Error(`game-floorplan-three: the builder of "${name}" drew no box or cylinder`);
23416
+ const boardsOf = splitTopsIntoBoards(drawn, materialOf2, Boolean(opts.carcass));
22873
23417
  const parts = drawn.map((p) => {
22874
23418
  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]);
22875
23419
  let material = materialOf2[p.key] ?? "plastic";
22876
- if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
23420
+ if (p.key === "wood" && material === "wood_hard" && opts.carcass && !opts.materials?.wood) material = "mdf";
23421
+ else if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
22877
23422
  const part = { id: p.id, material, shape: p.shape, position: p.position };
22878
23423
  if (p.rotation) part.rotation = p.rotation;
22879
23424
  if (!LOOSE.has(material) && p.min[1] <= FLOOR_TOUCH) part.anchored = true;
@@ -22908,6 +23453,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22908
23453
  if (area3 < 0.6 * footprint || p.max[1] < 0.2) continue;
22909
23454
  if (!top || p.max[1] > top.y + 1e-6) top = { part: p.id, y: p.max[1] };
22910
23455
  }
23456
+ for (const boards of boardsOf.values()) {
23457
+ if (top && boards[0].max[1] <= top.y - 1e-6) continue;
23458
+ const mid = boards[boards.length >> 1];
23459
+ const min = [Math.min(...boards.map((b) => b.min[0])), Math.min(...boards.map((b) => b.min[2]))];
23460
+ const max = [Math.max(...boards.map((b) => b.max[0])), Math.max(...boards.map((b) => b.max[2]))];
23461
+ 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) };
23462
+ }
22911
23463
  if (opts.carcass) carcass(parts, byId);
22912
23464
  const spec = { name, parts, joints, tags: ["furniture"] };
22913
23465
  const colors = new Map(drawn.map((p) => [p.id, p.color]));