@adatechnology/game-floorplan-three 0.9.3 → 0.10.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.
@@ -4361,6 +4361,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
4361
4361
  // src/house/walls.ts
4362
4362
  function installHouseWalls(K, shared) {
4363
4363
  const { EPS: EPS3, Y0, boxC, bx, cursor, effectiveHeight, pieceRecord, structurePiece, wallColor } = K;
4364
+ const sh = (color2, factor) => K.sh(color2, factor);
4364
4365
  function wallBox2(alongU, L, a, b, ya, yb, th, cL, cH, mL, mH) {
4365
4366
  if (yb - ya < 1e-4 || b - a < 1e-4) return;
4366
4367
  if (alongU) {
@@ -4404,6 +4405,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
4404
4405
  function walls() {
4405
4406
  const R = shared.ctx.r, cut = shared.ctx.cut, nA = R.floors.length, vMin = Math.min(...R.walls.map((w) => w.axis === "u" ? w.v0 : Math.min(w.v0, w.v1)));
4406
4407
  const corners = /* @__PURE__ */ new Map();
4408
+ const looks = /* @__PURE__ */ new Map();
4407
4409
  for (const w of R.walls) {
4408
4410
  const f = w.floor;
4409
4411
  cursor.group = "f" + f;
@@ -4413,6 +4415,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
4413
4415
  const hh = cut ? Math.min(h, 1.2) : h, L = alongU ? w.v0 : w.u0, s0 = Math.min(alongU ? w.u0 : w.v0, alongU ? w.u1 : w.v1), s1 = Math.max(alongU ? w.u0 : w.v0, alongU ? w.u1 : w.v1);
4414
4416
  const front = alongU && Math.abs(L - vMin) < 0.05;
4415
4417
  const sideL = sideOf(w.sides[0], front), sideH = sideOf(w.sides[1], front);
4418
+ if (w.id) {
4419
+ const face = sideL.exterior ? sideL : sideH;
4420
+ looks.set(w.id, { mat: face.mat, color: face.color, top: y0 + hh });
4421
+ }
4416
4422
  shared.ctx.colliders.push({ type: "wall", piece: w.id, f, x0: alongU ? s0 : L - th / 2, z0: alongU ? L - th / 2 : s0, x1: alongU ? s1 : L + th / 2, z1: alongU ? L + th / 2 : s1 });
4417
4423
  if (w.type === "railing") {
4418
4424
  const c0 = shared.ctx.colors.metal != null ? shared.ctx.colors.metal : 9080466;
@@ -4459,8 +4465,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
4459
4465
  cursor.group = "f" + e.f;
4460
4466
  bx(e.color.mat, e.pu - e.th / 2, e.y0, e.pv - e.th / 2, e.pu + e.th / 2, e.y0 + e.hh, e.pv + e.th / 2, e.color.color);
4461
4467
  }
4468
+ if (shared.ctx.showPillars) wallPillars(looks);
4469
+ }
4470
+ const PILLAR_IN_WALL = /^(wall_column_|column_\d|jamb_column_)/;
4471
+ const wallOf = (pillar, parts) => {
4472
+ const u = (pillar.aabb[0] + pillar.aabb[2]) / 2, v = (pillar.aabb[1] + pillar.aabb[3]) / 2;
4473
+ return parts.find((q) => (q.role === "wall" || q.role === "partition") && q.assembly !== void 0 && q.floor === pillar.floor && u >= q.aabb[0] - 1e-3 && u <= q.aabb[2] + 1e-3 && v >= q.aabb[1] - 1e-3 && v <= q.aabb[3] + 1e-3);
4474
+ };
4475
+ function wallPillars(looks) {
4476
+ const parts = Object.values(shared.ctx.structureIndex);
4477
+ for (const e of parts) {
4478
+ if (e.role !== "column" || !PILLAR_IN_WALL.test(e.id)) continue;
4479
+ const host = wallOf(e, parts);
4480
+ const look = host && looks.get(host.id);
4481
+ if (!host || !look) continue;
4482
+ cursor.group = "f" + e.floor;
4483
+ cursor.pc = structurePiece(host.id);
4484
+ bx(look.mat, e.aabb[0], e.y0, e.aabb[1], e.aabb[2], Math.min(e.y1, look.top), e.aabb[3], sh(look.color != null ? look.color : 15262420, 0.93));
4485
+ }
4486
+ cursor.pc = null;
4462
4487
  }
4463
- Object.assign(K, { wallBox: wallBox2, sideOf, pieceOf, nearestWall, walls });
4488
+ Object.assign(K, { wallBox: wallBox2, sideOf, pieceOf, nearestWall, walls, wallPillars });
4464
4489
  }
4465
4490
 
4466
4491
  // src/house/doors.ts
@@ -11433,6 +11458,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11433
11458
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
11434
11459
  };
11435
11460
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
11461
+ var splitAlong = (box3, count, axis, rnd) => {
11462
+ const shares = Array.from({ length: count }, () => 0.25 + 1.5 * rnd());
11463
+ const total = shares.reduce((sum, share) => sum + share, 0);
11464
+ const out = [];
11465
+ let from = box3.c[axis] - box3.s[axis] / 2;
11466
+ for (const share of shares) {
11467
+ const length = box3.s[axis] * share / total;
11468
+ const c = [box3.c[0], box3.c[1], box3.c[2]];
11469
+ const size = [box3.s[0], box3.s[1], box3.s[2]];
11470
+ c[axis] = from + length / 2;
11471
+ size[axis] = length;
11472
+ out.push({ c, s: size });
11473
+ from += length;
11474
+ }
11475
+ return out;
11476
+ };
11436
11477
  var splinterBox = (inp, box3, limit = inp.maxFragments) => {
11437
11478
  const { rnd } = inp;
11438
11479
  const dims = box3.s;
@@ -11442,35 +11483,81 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11442
11483
  const w = Math.max(fmin * 1.5, 0.01);
11443
11484
  const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
11444
11485
  const maxLen = fmax * 4;
11445
- const cells = bsp(box3, c, [a, b], rnd);
11486
+ const cells = [];
11487
+ for (const cell of bsp(box3, c, [a, b], rnd)) {
11488
+ const wide = cell.s[a] >= cell.s[b] ? a : b;
11489
+ if (rnd() < 0.4 && cell.s[wide] > fmin * 0.5) for (const part of splitAlong(cell, 2, wide, rnd)) cells.push(part);
11490
+ else cells.push(cell);
11491
+ }
11446
11492
  const pieces = [];
11447
11493
  for (const cell of cells) {
11448
- const extra = rnd() < 0.4 ? 1 : 0;
11494
+ const extra = Math.floor(rnd() * 3);
11449
11495
  const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
11450
- for (const p of bsp(cell, k, [g], rnd)) pieces.push(p);
11496
+ for (const p of splitAlong(cell, k, g, rnd)) pieces.push(p);
11451
11497
  }
11452
11498
  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] }));
11453
11499
  };
11500
+ var PLY = 3e-3;
11501
+ var PLY_GAP = 0.6;
11502
+ var PLY_SHEET = 0.012;
11503
+ var plyBreak = (inp, root, limit) => {
11504
+ const { rnd } = inp;
11505
+ const dims = root.s;
11506
+ const sorted = [0, 1, 2].sort((x, y) => dims[x] - dims[y]);
11507
+ const t = sorted[0];
11508
+ const [u, v] = [sorted[1], sorted[2]].sort((x, y) => x - y);
11509
+ const along = dims[u] >= dims[v] ? u : v;
11510
+ const across = along === u ? v : u;
11511
+ const layers = clamp(Math.round(dims[t] / PLY / 2), 2, 3);
11512
+ const pitch = dims[t] / layers;
11513
+ const th = Math.min(pitch, PLY_SHEET);
11514
+ const per = Math.max(2, Math.floor(limit / layers));
11515
+ const out = [];
11516
+ for (let k = 0; k < layers; k++) {
11517
+ const layer = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
11518
+ layer.c[t] = root.c[t] + (-dims[t] / 2 + (k + 0.5) * pitch) * (pitch > PLY_SHEET ? 1 : 1 + PLY_GAP);
11519
+ layer.s[t] = th;
11520
+ for (const cell of bsp(layer, per, [u, v], rnd)) {
11521
+ if (out.length >= limit) return out;
11522
+ if (rnd() < 0.35) {
11523
+ const strand = cell.s[across] * (0.15 + 0.2 * rnd());
11524
+ out.push({ center: cell.c, size: [cell.s[along], strand, th * 0.7], q: frame(along, across, t), shape: "splinter", weight: cell.s[along] * strand * th });
11525
+ } else {
11526
+ out.push({ center: cell.c, size: [cell.s[u], cell.s[v], th], q: frame(u, v, t, (rnd() - 0.5) * 0.5), shape: "scrap", weight: cell.s[u] * cell.s[v] * th });
11527
+ }
11528
+ }
11529
+ }
11530
+ return out;
11531
+ };
11532
+ var granules = (inp, box3, count) => bsp(box3, count, [0, 1, 2], inp.rnd).map((b) => ({ center: b.c, size: b.s, q: quat.identity(), shape: "chunk", weight: b.s[0] * b.s[1] * b.s[2] }));
11533
+ var CRUMBS = 6;
11454
11534
  var severAt = (inp, root, at2, gap) => {
11455
11535
  const dims = root.s;
11456
11536
  const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
11457
11537
  const half = dims[L] / 2;
11458
11538
  const lo = clamp(at2 - gap / 2, -half, half), hi = clamp(at2 + gap / 2, -half, half);
11459
11539
  const out = [];
11460
- const stub = (from, to) => {
11540
+ const fibrous = inp.form === "slivers" || inp.form === "plies";
11541
+ const [a, b] = [0, 1, 2].filter((i) => i !== L);
11542
+ const stub = (from, to, breakAtEnd) => {
11461
11543
  if (to - from < 0.02) return;
11462
11544
  const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
11463
11545
  c[L] = (from + to) / 2;
11464
11546
  sz[L] = to - from;
11465
- out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
11547
+ const weight = sz[0] * sz[1] * sz[2];
11548
+ if (fibrous) out.push({ center: c, size: [sz[L], sz[a], sz[b]], q: frame(L, a, b, breakAtEnd ? 0 : Math.PI), shape: "slab", weight });
11549
+ else out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight });
11466
11550
  };
11467
- stub(-half, lo);
11468
- stub(hi, half);
11551
+ stub(-half, lo, true);
11552
+ stub(hi, half, false);
11469
11553
  const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
11470
11554
  band.c[L] = (lo + hi) / 2;
11471
11555
  band.s[L] = Math.max(hi - lo, 0.01);
11472
- if (inp.style === "snap") {
11473
- 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] });
11556
+ const room = inp.maxFragments - out.length;
11557
+ if (inp.style === "snap" && inp.form === "plies") for (const p of plyBreak(inp, band, Math.max(3, room))) out.push(p);
11558
+ else if (inp.style === "snap" && inp.form === "granules") for (const p of granules(inp, band, clamp(room, 3, 12))) out.push(p);
11559
+ else if (inp.style === "snap") {
11560
+ for (const b2 of bsp(band, Math.max(3, Math.min(6, room)), [0, 1, 2], inp.rnd)) out.push({ center: b2.c, size: b2.s, q: quat.identity(), shape: "chunk", weight: b2.s[0] * b2.s[1] * b2.s[2] });
11474
11561
  } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
11475
11562
  return out;
11476
11563
  };
@@ -11503,8 +11590,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11503
11590
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
11504
11591
  if (style === "snap") {
11505
11592
  if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
11593
+ if (inp.form === "plies") return plyBreak(inp, root, maxFragments);
11506
11594
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
11507
11595
  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] });
11596
+ if (inp.form !== "granules") return out;
11597
+ const crumbs = Math.min(CRUMBS, maxFragments - out.length);
11598
+ if (crumbs <= 0) return out;
11599
+ const band = { c: [0, 0, 0], s: [0, 0, 0] };
11600
+ for (let i = 0; i < 3; i++) {
11601
+ band.s[i] = Math.min(dims[i], Math.max(fmin, dims[i] * 0.2));
11602
+ band.c[i] = clamp(inp.hit[i], -dims[i] / 2 + band.s[i] / 2, dims[i] / 2 - band.s[i] / 2);
11603
+ }
11604
+ for (const p of granules(inp, band, crumbs)) out.push({ ...p, weight: p.weight * 0.5 });
11508
11605
  return out;
11509
11606
  }
11510
11607
  if (style === "crumble" || style === "none") {
@@ -11611,6 +11708,88 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11611
11708
  }
11612
11709
  return marks;
11613
11710
  };
11711
+ var JOINT_GAP = 1e-4;
11712
+ var MIN_BEARING_SHARE = 0.25;
11713
+ var unsupportedCells = (cells, thin, dims, standing) => {
11714
+ if (thin === 1) return [];
11715
+ const [u, v] = planeAxes(thin);
11716
+ const bottom = -dims[v] / 2;
11717
+ const rowOf = (y) => Math.round((y - bottom) * 1e4);
11718
+ const rows = /* @__PURE__ */ new Map();
11719
+ for (let i = 0; i < cells.length; i++) {
11720
+ if (!standing(i)) continue;
11721
+ const cell = cells[i];
11722
+ const key = rowOf(cell.center[v] - cell.size[v] / 2);
11723
+ const row = rows.get(key);
11724
+ if (row) row.push(i);
11725
+ else rows.set(key, [i]);
11726
+ }
11727
+ const held = /* @__PURE__ */ new Set();
11728
+ const queue = [];
11729
+ for (const i of rows.get(0) ?? []) {
11730
+ held.add(i);
11731
+ queue.push(i);
11732
+ }
11733
+ const overlap = (a, b) => Math.min(a.center[u] + a.size[u] / 2, b.center[u] + b.size[u] / 2) - Math.max(a.center[u] - a.size[u] / 2, b.center[u] - b.size[u] / 2);
11734
+ const climb = () => {
11735
+ while (queue.length > 0) {
11736
+ const cell = cells[queue.pop()];
11737
+ for (const j of rows.get(rowOf(cell.center[v] + cell.size[v] / 2)) ?? []) {
11738
+ const other = cells[j];
11739
+ if (held.has(j) || overlap(cell, other) <= Math.max(JOINT_GAP, MIN_BEARING_SHARE * Math.min(cell.size[u], other.size[u]))) continue;
11740
+ held.add(j);
11741
+ queue.push(j);
11742
+ }
11743
+ }
11744
+ };
11745
+ for (const row of rows.values()) row.sort((a, b) => cells[a].center[u] - cells[b].center[u]);
11746
+ const touching = (a, b) => Math.abs(a.center[u] + a.size[u] / 2 - (b.center[u] - b.size[u] / 2)) <= 1e-3;
11747
+ for (let wedged = true; wedged; ) {
11748
+ climb();
11749
+ wedged = false;
11750
+ for (const row of rows.values()) {
11751
+ for (let k = 1; k < row.length - 1; k++) {
11752
+ const i = row[k];
11753
+ if (held.has(i) || !held.has(row[k - 1]) || !held.has(row[k + 1])) continue;
11754
+ if (!touching(cells[row[k - 1]], cells[i]) || !touching(cells[i], cells[row[k + 1]])) continue;
11755
+ held.add(i);
11756
+ queue.push(i);
11757
+ wedged = true;
11758
+ }
11759
+ }
11760
+ }
11761
+ const loose = [];
11762
+ for (const row of rows.values()) for (const i of row) if (!held.has(i)) loose.push(i);
11763
+ return loose;
11764
+ };
11765
+ var clumpCells = (cells, thin, cap) => {
11766
+ if (cells.length <= cap) return [...cells];
11767
+ const [u, v] = planeAxes(thin);
11768
+ let lo0 = Infinity, lo1 = Infinity, hi0 = -Infinity, hi1 = -Infinity;
11769
+ for (const cell of cells) {
11770
+ lo0 = Math.min(lo0, cell.center[u] - cell.size[u] / 2);
11771
+ hi0 = Math.max(hi0, cell.center[u] + cell.size[u] / 2);
11772
+ lo1 = Math.min(lo1, cell.center[v] - cell.size[v] / 2);
11773
+ hi1 = Math.max(hi1, cell.center[v] + cell.size[v] / 2);
11774
+ }
11775
+ let side = Math.sqrt((hi0 - lo0) * (hi1 - lo1) / cap);
11776
+ for (; ; ) {
11777
+ const bins = /* @__PURE__ */ new Map();
11778
+ for (const cell of cells) {
11779
+ const key = `${Math.floor((cell.center[u] - lo0) / side)}:${Math.floor((cell.center[v] - lo1) / side)}`;
11780
+ const bin = bins.get(key) ?? { lo: [Infinity, Infinity, Infinity], hi: [-Infinity, -Infinity, -Infinity] };
11781
+ for (let k = 0; k < 3; k++) {
11782
+ bin.lo[k] = Math.min(bin.lo[k], cell.center[k] - cell.size[k] / 2);
11783
+ bin.hi[k] = Math.max(bin.hi[k], cell.center[k] + cell.size[k] / 2);
11784
+ }
11785
+ bins.set(key, bin);
11786
+ }
11787
+ if (bins.size <= cap) {
11788
+ return [...bins.values()].map(({ lo, hi }) => ({ center: [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2], size: [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]] }));
11789
+ }
11790
+ side *= 1.25;
11791
+ }
11792
+ };
11614
11793
  var MAX_SPALLS = 64;
11615
11794
  var thinAxisOf = (dims) => dims[0] <= dims[1] && dims[0] <= dims[2] ? 0 : dims[1] <= dims[2] ? 1 : 2;
11616
11795
  var resolveLayer = (def, index, mat) => ({ index, def, mat, energy: mat.penetration * def.thickness * 1e3 });
@@ -12590,6 +12769,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12590
12769
  },
12591
12770
  {
12592
12771
  id: "wood_soft",
12772
+ splitForm: "slivers",
12593
12773
  density: 500,
12594
12774
  hpPerKg: 90,
12595
12775
  penetration: 9,
@@ -12635,6 +12815,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12635
12815
  },
12636
12816
  {
12637
12817
  id: "wood_hard",
12818
+ splitForm: "slivers",
12638
12819
  density: 750,
12639
12820
  hpPerKg: 140,
12640
12821
  penetration: 14,
@@ -12679,6 +12860,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12679
12860
  },
12680
12861
  {
12681
12862
  id: "plywood",
12863
+ splitForm: "plies",
12682
12864
  density: 600,
12683
12865
  hpPerKg: 70,
12684
12866
  penetration: 7,
@@ -12726,6 +12908,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12726
12908
  },
12727
12909
  {
12728
12910
  id: "mdf",
12911
+ splitForm: "granules",
12729
12912
  density: 750,
12730
12913
  hpPerKg: 45,
12731
12914
  penetration: 6,
@@ -13237,6 +13420,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13237
13420
  },
13238
13421
  {
13239
13422
  id: "green_wood",
13423
+ splitForm: "slivers",
13240
13424
  density: 650,
13241
13425
  hpPerKg: 5,
13242
13426
  penetration: 10,
@@ -13517,6 +13701,125 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13517
13701
  "brittle"
13518
13702
  ],
13519
13703
  compressive: 1e5
13704
+ },
13705
+ {
13706
+ id: "mud",
13707
+ density: 1700,
13708
+ hpPerKg: 1,
13709
+ penetration: 5,
13710
+ ductility: 0.8,
13711
+ fracture: "crumble",
13712
+ fragmentSize: [
13713
+ 0.02,
13714
+ 0.08
13715
+ ],
13716
+ hitDebris: "dust",
13717
+ breakDebris: "dust",
13718
+ thermal: {
13719
+ conductivity: 0.5,
13720
+ heatCapacity: 3.2
13721
+ },
13722
+ tags: [
13723
+ "soil",
13724
+ "wet"
13725
+ ],
13726
+ compressive: 5e3
13727
+ },
13728
+ {
13729
+ id: "clay",
13730
+ density: 1800,
13731
+ hpPerKg: 5,
13732
+ penetration: 7,
13733
+ ductility: 0.05,
13734
+ fracture: "crumble",
13735
+ fragmentSize: [
13736
+ 0.02,
13737
+ 0.1
13738
+ ],
13739
+ hitDebris: "chips",
13740
+ breakDebris: "dust",
13741
+ thermal: {
13742
+ conductivity: 0.15,
13743
+ heatCapacity: 2.2
13744
+ },
13745
+ tags: [
13746
+ "soil",
13747
+ "brittle"
13748
+ ],
13749
+ compressive: 6e4
13750
+ },
13751
+ {
13752
+ id: "snow",
13753
+ density: 300,
13754
+ hpPerKg: 1,
13755
+ penetration: 3,
13756
+ ductility: 0.6,
13757
+ fracture: "crumble",
13758
+ fragmentSize: [
13759
+ 0.01,
13760
+ 0.05
13761
+ ],
13762
+ hitDebris: "dust",
13763
+ breakDebris: "dust",
13764
+ thermal: {
13765
+ conductivity: 0.1,
13766
+ heatCapacity: 2.1
13767
+ },
13768
+ tags: [
13769
+ "soil",
13770
+ "frozen"
13771
+ ],
13772
+ compressive: 1e3
13773
+ },
13774
+ {
13775
+ id: "cobble",
13776
+ density: 2650,
13777
+ hpPerKg: 25,
13778
+ penetration: 25,
13779
+ penetrationClass: "concrete",
13780
+ penetrationScale: 0.9,
13781
+ ductility: 0.03,
13782
+ fracture: "crumble",
13783
+ fragmentSize: [
13784
+ 0.05,
13785
+ 0.15
13786
+ ],
13787
+ hitDebris: "chips",
13788
+ breakDebris: "chips",
13789
+ thermal: {
13790
+ conductivity: 0.2,
13791
+ heatCapacity: 2.5
13792
+ },
13793
+ tags: [
13794
+ "masonry",
13795
+ "brittle"
13796
+ ],
13797
+ compressive: 3e5
13798
+ },
13799
+ {
13800
+ id: "paver",
13801
+ density: 2300,
13802
+ hpPerKg: 14,
13803
+ penetration: 18,
13804
+ penetrationClass: "concrete",
13805
+ penetrationScale: 1,
13806
+ ductility: 0.03,
13807
+ fracture: "crumble",
13808
+ fragmentSize: [
13809
+ 0.04,
13810
+ 0.12
13811
+ ],
13812
+ hitDebris: "chips",
13813
+ breakDebris: "chips",
13814
+ thermal: {
13815
+ conductivity: 0.15,
13816
+ heatCapacity: 2.5
13817
+ },
13818
+ tags: [
13819
+ "masonry",
13820
+ "brittle"
13821
+ ],
13822
+ compressive: 35e4
13520
13823
  }
13521
13824
  ];
13522
13825
  var material_hints_default = {
@@ -13729,6 +14032,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13729
14032
  var BREACH_BLAST_FACTOR = 6;
13730
14033
  var BREACH_AREA = 0.25;
13731
14034
  var BREACH_BLAST_REACH = 0.12;
14035
+ var MEMBER_CUT_REACH = 0.5;
14036
+ var BLAST_REFERENCE_RESISTANCE = 200 * 1900 * 0.13;
14037
+ var BLAST_EDGE_RAGGED = 0.3;
14038
+ var BLAST_EDGE_LOOSE = 1.35;
14039
+ var BLAST_EDGE_WEAR = 0.6;
13732
14040
  var RESIDUAL_CAPACITY = 0.15;
13733
14041
  var SEVER_BAND = 0.1;
13734
14042
  var SEVER_ASPECT = 3;
@@ -14981,7 +15289,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14981
15289
  if (j.st.open) continue;
14982
15290
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
14983
15291
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
14984
- j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
15292
+ j.st.hp -= span === void 0 ? amount : Math.min(amount, j.st.maxHp) * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
14985
15293
  if (j.st.hp <= 0) {
14986
15294
  j.st.hp = 0;
14987
15295
  j.st.open = true;
@@ -15109,6 +15417,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15109
15417
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
15110
15418
  const cut = (_b = p.rt).craters ?? (_b.craters = {});
15111
15419
  const out = [];
15420
+ const wholeBefore = Object.values(worn).filter((w) => w === -1).length;
15112
15421
  const shot = kind === "ballistic";
15113
15422
  for (let i = 0; i < grid.length; i++) {
15114
15423
  const cell = grid[i];
@@ -15167,6 +15476,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15167
15476
  emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
15168
15477
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
15169
15478
  }
15479
+ if (Object.values(worn).filter((w) => w === -1).length > wholeBefore) dropUnsupported(o, p, src);
15170
15480
  if (p.spec.reinforcement) exposeRebar(o, p, src);
15171
15481
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
15172
15482
  if (p.structural && !isLost(p)) {
@@ -15270,10 +15580,122 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15270
15580
  setStage(o, p, 1, {}, src);
15271
15581
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
15272
15582
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
15583
+ dropUnsupported(o, p, src);
15273
15584
  const reinforcement = p.spec.reinforcement;
15274
15585
  if (reinforcement) exposeRebar(o, p, src);
15275
15586
  if (p.structural) solveStructure(o);
15276
15587
  };
15588
+ const blastReachOf = (p, energy) => {
15589
+ const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
15590
+ const resistance = Math.max(1, perKg * p.mat.density * p.thickness);
15591
+ return BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3) * clamp2(Math.cbrt(BLAST_REFERENCE_RESISTANCE / resistance), 0.3, 2);
15592
+ };
15593
+ const blastUnits = (o, p, wp, energy, dist, src) => {
15594
+ var _a, _b;
15595
+ if (!p.staged || p.st.stripped || isLost(p) || chipEnergy <= 0) return false;
15596
+ const reach = blastReachOf(p, energy);
15597
+ if (dist >= reach) return false;
15598
+ const radius = Math.sqrt(reach * reach - dist * dist);
15599
+ const [u, v] = planeAxes(p.thin);
15600
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
15601
+ const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
15602
+ const grid = gridOf(p);
15603
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
15604
+ const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
15605
+ const lobes = [rnd() * 2 * Math.PI, rnd() * 2 * Math.PI];
15606
+ const out = [];
15607
+ for (let i = 0; i < grid.length; i++) {
15608
+ const cell = grid[i];
15609
+ if (worn[i] === -1) continue;
15610
+ if (op && p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius) continue;
15611
+ const du = cell.center[u] - lp[u], dv = cell.center[v] - lp[v];
15612
+ const angle = Math.atan2(dv, du);
15613
+ const edge = radius * (1 + BLAST_EDGE_RAGGED * (0.5 * Math.sin(3 * angle + lobes[0]) + 0.3 * Math.sin(5 * angle + lobes[1]) + 0.4 * (rnd() - 0.5)));
15614
+ const d = Math.hypot(du, dv);
15615
+ if (d <= edge) {
15616
+ worn[i] = -1;
15617
+ out.push(i);
15618
+ } else if (d <= edge * BLAST_EDGE_LOOSE) {
15619
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
15620
+ worn[i] = Math.max(worn[i] ?? 0, perKg * mass * BLAST_EDGE_WEAR);
15621
+ }
15622
+ }
15623
+ if (out.length === 0) return false;
15624
+ const loose = unsupportedCells(grid, p.thin, p.dims, (i) => worn[i] !== -1 && !(op && p.st.opening && Math.hypot(grid[i].center[u] - op[u], grid[i].center[v] - op[v]) <= p.st.opening.radius));
15625
+ for (const i of loose) worn[i] = -1;
15626
+ const blown = out.map((i) => grid[i]), fallen = loose.map((i) => grid[i]);
15627
+ const volume = (cells2) => cells2.reduce((sum, cell) => sum + cell.size[0] * cell.size[1] * cell.size[2], 0);
15628
+ const blownMass = Math.min(p.st.mass, p.mat.density * volume(blown));
15629
+ const fallenMass = Math.min(p.st.mass - blownMass, p.mat.density * volume(fallen));
15630
+ const speed = clamp2(Math.sqrt(2 * energy * 0.02 / Math.max(blownMass, 0.05)), 1, 12);
15631
+ const { c, q } = partXf(o, p);
15632
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
15633
+ const tag = `x${chipped.length}_`;
15634
+ const fragments = [
15635
+ ...cellFragments(o, p, clumpCells(blown, p.thin, maxFragments), wp, null, speed, blownMass, tag, true),
15636
+ ...fallen.length > 0 ? cellFragments(o, p, clumpCells(fallen, p.thin, Math.max(2, Math.round(maxFragments / 4))), wp, null, 0.3, fallenMass, `${tag}f`, true) : []
15637
+ ];
15638
+ const cells = [...blown, ...fallen];
15639
+ const holes = localHolesOf(p);
15640
+ p.st.holes = p.st.holes.filter((_, k) => !cells.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
15641
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
15642
+ for (const cell of cells) {
15643
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
15644
+ for (let k = 0; k < 3; k++) {
15645
+ lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
15646
+ hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
15647
+ }
15648
+ }
15649
+ p.st.mass = Math.max(0, p.st.mass - blownMass - fallenMass);
15650
+ p.rt.removed = (p.rt.removed ?? 0) + blownMass + fallenMass;
15651
+ markDamaged(p);
15652
+ setStage(o, p, 1, {}, src);
15653
+ const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
15654
+ const units = cells.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
15655
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]], rotation: q, fragments, units });
15656
+ debris(o, p, p.mat.breakDebris, center, vec.normalize(vec.sub(center, wp), [0, 1, 0]), 14, 3, src);
15657
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
15658
+ if (p.structural && !isLost(p)) {
15659
+ solveStructure(o);
15660
+ collapseCheck(o, src);
15661
+ }
15662
+ return true;
15663
+ };
15664
+ const dropUnsupported = (o, p, src) => {
15665
+ var _a, _b;
15666
+ if (!p.spec.units || p.spec.units.mortar === p.spec.material || p.st.stripped || isLost(p)) return 0;
15667
+ const [u, v] = planeAxes(p.thin);
15668
+ const grid = gridOf(p);
15669
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
15670
+ const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
15671
+ const opened = (cell) => !!op && !!p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius;
15672
+ const loose = unsupportedCells(grid, p.thin, p.dims, (i) => worn[i] !== -1 && !opened(grid[i]));
15673
+ if (loose.length === 0) return 0;
15674
+ for (const i of loose) worn[i] = -1;
15675
+ const fallen = loose.map((i) => grid[i]);
15676
+ const mass = Math.min(p.st.mass, p.mat.density * fallen.reduce((sum, cell) => sum + cell.size[0] * cell.size[1] * cell.size[2], 0));
15677
+ const { c, q } = partXf(o, p);
15678
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
15679
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
15680
+ for (const cell of fallen) {
15681
+ for (let k = 0; k < 3; k++) {
15682
+ lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
15683
+ hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
15684
+ }
15685
+ }
15686
+ const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
15687
+ const fragments = cellFragments(o, p, clumpCells(fallen, p.thin, Math.max(2, Math.round(maxFragments / 4))), vec.add(center, [0, 1, 0]), null, 0.3, mass, `u${chipped.length}_`, true);
15688
+ const holes = localHolesOf(p);
15689
+ p.st.holes = p.st.holes.filter((_, k) => !fallen.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
15690
+ for (const cell of fallen) chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
15691
+ p.st.mass = Math.max(0, p.st.mass - mass);
15692
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
15693
+ markDamaged(p);
15694
+ const units = fallen.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
15695
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]], rotation: q, fragments, units });
15696
+ debris(o, p, p.mat.breakDebris, center, [0, -1, 0], 6, 1, src);
15697
+ return loose.length;
15698
+ };
15277
15699
  const stripPart = (o, p, reinforcement, src) => {
15278
15700
  const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
15279
15701
  p.st.stripped = true;
@@ -15340,6 +15762,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15340
15762
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
15341
15763
  const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
15342
15764
  const cutAt2 = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
15765
+ const planRnd = p.mat.splitForm ? splitStream(o.id, p.spec.id, hitLocal) : rnd;
15343
15766
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
15344
15767
  style,
15345
15768
  dims: p.dims,
@@ -15348,8 +15771,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15348
15771
  hit: hitLocal,
15349
15772
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
15350
15773
  maxFragments,
15351
- rnd,
15774
+ rnd: planRnd,
15352
15775
  sheet: p.mat.sheet,
15776
+ ...p.mat.splitForm ? { form: p.mat.splitForm } : {},
15353
15777
  ...cutAt2 !== void 0 ? { cut: { at: cutAt2, gap: SEVER_BAND } } : {}
15354
15778
  });
15355
15779
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
@@ -15488,7 +15912,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15488
15912
  const fragments = [];
15489
15913
  for (let i = 0; i < n; i++) {
15490
15914
  const back = !exit || i % 3 === 2;
15491
- 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());
15915
+ const plies = p.mat.splitForm === "plies";
15916
+ 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() : plies ? 0.15 + 0.15 * rnd() : 0.4 + 0.4 * rnd());
15492
15917
  const size = [w, w, w];
15493
15918
  size[along] = len;
15494
15919
  size[(along + 1) % 3] = w;
@@ -15649,7 +16074,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15649
16074
  checkIgnite(o, p, src);
15650
16075
  }
15651
16076
  }
15652
- wearJoints(o, p, Ein * 0.5, void 0, 0, src);
16077
+ wearJoints(o, p, Ein * 0.5, void 0, 0, src, false, 2 * BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3));
15653
16078
  const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
15654
16079
  if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
15655
16080
  frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
@@ -15659,7 +16084,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15659
16084
  const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
15660
16085
  res.damage += dealt;
15661
16086
  if (p.bend && flexure.enabled) bendByBlast(o, p, point, energy, radius, reach, src);
15662
- if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
16087
+ const breachReach = BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3);
16088
+ if (p.spec.units && p.spec.units.mortar !== p.spec.material) blastUnits(o, p, wp, energy, dist, src);
16089
+ else if (p.spec.reinforcement && p.spec.units && dist > MEMBER_CUT_REACH * breachReach) blastUnits(o, p, wp, energy, dist, src);
16090
+ else if (dist <= breachReach) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
15663
16091
  return reach;
15664
16092
  };
15665
16093
  const blastProbe = { partsTested: 0, partsNear: 0 };
@@ -15905,6 +16333,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15905
16333
  }
15906
16334
  return h | 0;
15907
16335
  };
16336
+ const splitStream = (objectId, partId, hit) => {
16337
+ const codes = Array.from(`${objectId}|${partId}`, (char) => char.charCodeAt(0) / 1e3);
16338
+ const stream = createRng(seedOf2(rng2.state() / 1e3, ...codes, hit[0], hit[1], hit[2]));
16339
+ return () => stream.next();
16340
+ };
15908
16341
  const throwFragment = (frag, objectId, from, g, centre, index, src, shields, results, reports, mult) => {
15909
16342
  const v = frag.velocity;
15910
16343
  const speed = Math.hypot(v[0], v[1], v[2]);
@@ -17321,12 +17754,15 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17321
17754
  builder,
17322
17755
  porchRoofOnPostsOnly: options.porchRoofOnPostsOnly,
17323
17756
  rafterRoof: options.rafterRoof,
17757
+ wallPillarSpacing: options.wallPillarSpacing,
17758
+ visorSections: options.visorSections,
17324
17759
  floorCount: floorCount2,
17325
17760
  isUnit,
17326
17761
  elevation,
17327
17762
  isTimber: system.timber,
17328
17763
  walls: [],
17329
17764
  wallParts: /* @__PURE__ */ new Map(),
17765
+ groundBeams: /* @__PURE__ */ new Map(),
17330
17766
  carriers: /* @__PURE__ */ new Map(),
17331
17767
  plates: /* @__PURE__ */ new Map(),
17332
17768
  pillars: /* @__PURE__ */ new Map(),
@@ -17397,7 +17833,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17397
17833
  builder.rest(deck, base, system.joints.slab);
17398
17834
  continue;
17399
17835
  }
17400
- const pillar = builder.add({ id: `balcony_column_${room.id}`, role: "column", material: construction.frame.pillars, aabb: [box22[0] - 0.08, box22[1] - 0.08, box22[0] + 0.08, box22[1] + 0.08], y0: 0, y1: context.level(floor) - system.slabThickness }, { floor: 0 });
17836
+ const half = context.visorSections ? 0.08 : 0.1;
17837
+ const pillar = builder.add({ id: `balcony_column_${room.id}`, role: "column", material: construction.frame.pillars, aabb: [box22[0] - half, box22[1] - half, box22[0] + half, box22[1] + half], y0: 0, y1: context.level(floor) - system.slabThickness }, { floor: 0 });
17401
17838
  builder.rest(pillar, GROUND, "rebar");
17402
17839
  builder.rest(deck, pillar, "rebar");
17403
17840
  }
@@ -17462,8 +17899,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17462
17899
  const top = context.level(floor) - thickness * 0.15;
17463
17900
  const out = [];
17464
17901
  for (let k = 0; k < count; k++) {
17465
- const centre = (alongU ? box22[0] : box22[1]) + 0.05 + k * (span - 0.1) / (count - 1);
17466
- const aabb = alongU ? [centre - 0.025, box22[1], centre + 0.025, box22[3]] : [box22[0], centre - 0.025, box22[2], centre + 0.025];
17902
+ const centre2 = (alongU ? box22[0] : box22[1]) + 0.05 + k * (span - 0.1) / (count - 1);
17903
+ const aabb = alongU ? [centre2 - 0.025, box22[1], centre2 + 0.025, box22[3]] : [box22[0], centre2 - 0.025, box22[2], centre2 + 0.025];
17467
17904
  const joist = context.builder.add({ id: `joist_${floor}${suffix}_${k}`, role: "beam", material, aabb, y0: context.level(floor) - thickness, y1: top }, { floor, fill: 1, spacing: JOIST_PITCH });
17468
17905
  out.push(joist);
17469
17906
  const below = carriers.filter((carrier) => carrier.y0 <= top + 0.05);
@@ -17551,8 +17988,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17551
17988
  const at2 = alongU ? box22[1] + line * (box22[3] - box22[1]) / (rows - 1) : box22[0] + line * (box22[2] - box22[0]) / (columns - 1);
17552
17989
  const aabb = alongU ? [box22[0], at2 - 0.07, box22[2], at2 + 0.07] : [at2 - 0.07, box22[1], at2 + 0.07, box22[3]];
17553
17990
  const beam = builder.add({ id: `base_beam_${line}`, role: "beam", material: construction.frame.timber, aabb, y0: height, y1: elevation - context.system.slabThickness }, { floor: 0 });
17554
- const centre = (q) => alongU ? (q.aabb[1] + q.aabb[3]) / 2 : (q.aabb[0] + q.aabb[2]) / 2;
17555
- for (const pier of piers.filter((q) => Math.abs(centre(q) - at2) < 0.05)) builder.rest(beam, pier, "bolt");
17991
+ const centre2 = (q) => alongU ? (q.aabb[1] + q.aabb[3]) / 2 : (q.aabb[0] + q.aabb[2]) / 2;
17992
+ for (const pier of piers.filter((q) => Math.abs(centre2(q) - at2) < 0.05)) builder.rest(beam, pier, "bolt");
17556
17993
  beams.push(beam);
17557
17994
  }
17558
17995
  return beams;
@@ -17572,17 +18009,110 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17572
18009
  else if (type === "crawl_space" || type === "piles" || type === "basement") addRaisedFloor(context);
17573
18010
  else if (context.isTimber && type !== "ground") context.base0 = raft(context, Math.max(0.12, context.elevation), "concrete");
17574
18011
  }
18012
+ var GROUND_BEAM_DEPTH = 0.3;
18013
+ function addGroundBeams(context) {
18014
+ const { builder, construction } = context;
18015
+ if (construction.system !== "masonry" || context.isUnit || construction.foundation.type !== "strip_footing") return;
18016
+ const top = context.level(0);
18017
+ for (const wall of context.walls) {
18018
+ if (wall.floor !== 0 || wall.type !== "wall") continue;
18019
+ const beam = builder.add({ id: `ground_beam_${context.groundBeams.size}`, role: "beam", material: construction.frame.beams, aabb: wall.box, y0: top - GROUND_BEAM_DEPTH, y1: top }, { floor: 0, beamKind: "ground_beam" });
18020
+ builder.rest(beam, GROUND, "rebar");
18021
+ context.groundBeams.set(wall.id, beam);
18022
+ }
18023
+ }
18024
+ var WALL_PILLAR_SPACING = 4;
18025
+ var WALL_PILLAR_SIDE = 0.2;
18026
+ var MERGE_DISTANCE = 0.45;
18027
+ var HALF = WALL_PILLAR_SIDE / 2;
18028
+ var TOLERANCE = 0.03;
18029
+ var alongRange = (wall) => wall.axis === "u" ? [Math.min(wall.u0, wall.u1), Math.max(wall.u0, wall.u1)] : [Math.min(wall.v0, wall.v1), Math.max(wall.v0, wall.v1)];
18030
+ var acrossOf = (wall) => wall.axis === "u" ? wall.v0 : wall.u0;
18031
+ function stretchesOf(context, wall) {
18032
+ const [low, high] = alongRange(wall);
18033
+ const windows = (context.source.windows ?? []).filter((w) => w.floor === wall.floor && w.axis === wall.axis && Math.abs(w.pos - acrossOf(wall)) <= TOLERANCE && w.at + w.width / 2 > low && w.at - w.width / 2 < high).map((w) => [w.at - w.width / 2, w.at + w.width / 2]).sort((a, b) => a[0] - b[0]);
18034
+ const out = [];
18035
+ let cursor = low;
18036
+ for (const [from, to] of windows) {
18037
+ if (from - cursor > TOLERANCE) out.push({ wall, from: cursor, to: from });
18038
+ cursor = Math.max(cursor, to);
18039
+ }
18040
+ if (high - cursor > TOLERANCE) out.push({ wall, from: cursor, to: high });
18041
+ return out;
18042
+ }
18043
+ function junctionsOf(context, wall) {
18044
+ const [low, high] = alongRange(wall);
18045
+ const line = acrossOf(wall);
18046
+ const out = [];
18047
+ for (const other of context.walls) {
18048
+ if (other === wall || other.floor !== wall.floor || other.axis === wall.axis || other.type !== "wall") continue;
18049
+ const at2 = acrossOf(other);
18050
+ const [a, b] = alongRange(other);
18051
+ const meets = Math.abs(a - line) <= (wall.thickness ?? 0.15) / 2 + TOLERANCE || Math.abs(b - line) <= (wall.thickness ?? 0.15) / 2 + TOLERANCE;
18052
+ if (meets && at2 > low + HALF && at2 < high - HALF) out.push(at2);
18053
+ }
18054
+ return out;
18055
+ }
18056
+ function pillarStations(from, to, junctions, spacing) {
18057
+ const length = to - from;
18058
+ if (length < WALL_PILLAR_SIDE - 1e-6) return [];
18059
+ if (length < 2 * WALL_PILLAR_SIDE) return [(from + to) / 2];
18060
+ if (length < WALL_PILLAR_SIDE + MERGE_DISTANCE) return [from + HALF, to - HALF];
18061
+ const fixed = [from + HALF, ...junctions.filter((at2) => at2 > from + HALF + MERGE_DISTANCE && at2 < to - HALF - MERGE_DISTANCE), to - HALF].sort((a, b) => a - b);
18062
+ const out = [];
18063
+ fixed.forEach((at2, k) => {
18064
+ out.push(at2);
18065
+ const next = fixed[k + 1];
18066
+ if (next === void 0) return;
18067
+ const spans = Math.ceil((next - at2) / spacing - 1e-6);
18068
+ for (let s = 1; s < spans; s++) out.push(at2 + (next - at2) * s / spans);
18069
+ });
18070
+ return out;
18071
+ }
18072
+ function addWallPillars(context, walls, floor, plates, taken, spacing) {
18073
+ const { builder } = context;
18074
+ const pillars = [];
18075
+ const placed = taken.map((p) => [(p.aabb[0] + p.aabb[2]) / 2, (p.aabb[1] + p.aabb[3]) / 2]);
18076
+ const base = context.baseOf(floor);
18077
+ for (const wall of walls) {
18078
+ if (!wall.exterior || wall.type !== "wall") continue;
18079
+ const top = context.wallParts.get(wall.id).y1;
18080
+ const across = acrossOf(wall);
18081
+ const junctions = junctionsOf(context, wall);
18082
+ for (const stretch of stretchesOf(context, wall)) {
18083
+ const standing = (end) => placed.map(([pu, pv]) => wall.axis === "u" ? [pu, pv] : [pv, pu]).find(([a, c]) => Math.abs(c - across) <= TOLERANCE && Math.abs(a - end) < MERGE_DISTANCE + HALF)?.[0];
18084
+ const first = standing(stretch.from);
18085
+ const last = standing(stretch.to);
18086
+ const from = first === void 0 ? stretch.from : Math.min(stretch.from, first - HALF);
18087
+ const to = last === void 0 ? stretch.to : Math.max(stretch.to, last + HALF);
18088
+ for (const along of pillarStations(from, to, junctions, spacing)) {
18089
+ const [u, v] = wall.axis === "u" ? [along, across] : [across, along];
18090
+ const end = Math.abs(along - stretch.from) < Math.abs(along - stretch.to) ? stretch.from : stretch.to;
18091
+ const endPoint = wall.axis === "u" ? [end, across] : [across, end];
18092
+ const isEnd = Math.abs(along - end) <= HALF + TOLERANCE;
18093
+ if (placed.some(([pu, pv]) => Math.hypot(pu - u, pv - v) < MERGE_DISTANCE && (!isEnd || Math.hypot(pu - endPoint[0], pv - endPoint[1]) < MERGE_DISTANCE))) continue;
18094
+ placed.push([u, v]);
18095
+ const box22 = [u - HALF, v - HALF, u + HALF, v + HALF];
18096
+ const pillar = builder.add({ id: `wall_column_${floor}_${pillars.length}`, role: "column", material: context.construction.frame.pillars, aabb: box22, y0: context.level(floor), y1: top }, { floor });
18097
+ builder.rest(pillar, base === GROUND || !base ? GROUND : base, "rebar");
18098
+ for (const plate of plates.filter((p) => touches(p.aabb, box22, 0.01))) builder.rest(plate, pillar, "rebar");
18099
+ pillars.push(pillar);
18100
+ }
18101
+ }
18102
+ }
18103
+ return pillars;
18104
+ }
17575
18105
  var MAX_GAP_WITHIN_RUN = 3.2;
17576
18106
  var POST_SPACING = 2.4;
17577
18107
  var POST_SIDE = 0.05;
17578
18108
  var alongOf = (wall, pick2) => wall.axis === "u" ? pick2(wall.u0, wall.u1) : pick2(wall.v0, wall.v1);
17579
- var acrossOf = (wall) => wall.axis === "u" ? wall.v0 : wall.u0;
18109
+ var acrossOf2 = (wall) => wall.axis === "u" ? wall.v0 : wall.u0;
17580
18110
  function runsOf(context, walls, floor) {
17581
18111
  const lines = /* @__PURE__ */ new Map();
17582
18112
  for (const wall of walls) {
17583
18113
  if (!wall.exterior || wall.type !== "wall") continue;
17584
18114
  const top = round2(context.wallParts.get(wall.id).y1);
17585
- const key = `${floor}|${wall.axis}|${acrossOf(wall).toFixed(2)}|${top.toFixed(2)}`;
18115
+ const key = `${floor}|${wall.axis}|${acrossOf2(wall).toFixed(2)}|${top.toFixed(2)}`;
17586
18116
  lines.set(key, [...lines.get(key) ?? [], wall]);
17587
18117
  }
17588
18118
  return [...lines.values()].map((line) => {
@@ -17602,7 +18132,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17602
18132
  }
17603
18133
  function addShackPosts(context, run, plate, floor, index) {
17604
18134
  const first = run.walls[0];
17605
- const across = acrossOf(first);
18135
+ const across = acrossOf2(first);
17606
18136
  const top = context.wallParts.get(first.id).y1;
17607
18137
  const count = Math.max(1, Math.ceil((run.to - run.from) / POST_SPACING));
17608
18138
  const posts = [];
@@ -17628,7 +18158,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17628
18158
  for (const run of runs) {
17629
18159
  const first = run.walls[0];
17630
18160
  const half = (first.thickness ?? 0.15) / 2;
17631
- const across = acrossOf(first);
18161
+ const across = acrossOf2(first);
17632
18162
  const top = context.wallParts.get(first.id).y1;
17633
18163
  const aabb = first.axis === "u" ? [run.from - half, across - half, run.to + half, across + half] : [across - half, run.from - half, across + half, run.to + half];
17634
18164
  const material = context.isTimber ? construction.frame.timber : construction.frame.beams;
@@ -17667,7 +18197,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17667
18197
  function addPlatesAndPillars(context, floor) {
17668
18198
  const walls = context.walls.filter((wall) => wall.floor === floor);
17669
18199
  const { plates, pillars } = addPlates(context, walls, floor);
17670
- if (context.construction.system === "masonry") pillars.push(...addCornerPillars(context, walls, floor, plates));
18200
+ if (context.construction.system === "masonry") {
18201
+ const corners = addCornerPillars(context, walls, floor, plates);
18202
+ pillars.push(...corners);
18203
+ if (context.wallPillarSpacing) pillars.push(...addWallPillars(context, walls, floor, plates, corners, context.wallPillarSpacing));
18204
+ }
17671
18205
  context.pillars.set(floor, pillars);
17672
18206
  context.plates.set(floor, plates);
17673
18207
  const innerBearing = walls.filter((wall) => wall.role === "wall" && !wall.exterior).map((wall) => context.wallParts.get(wall.id));
@@ -17942,21 +18476,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17942
18476
  builder.rest(flight, builder.find("basement_slab"), "resting");
17943
18477
  if (context.base0 && context.base0 !== GROUND) builder.rest(flight, context.base0, "nail");
17944
18478
  }
17945
- var TOLERANCE = 0.02;
18479
+ var TOLERANCE2 = 0.02;
17946
18480
  var DOOR_HEIGHT = 2.1;
17947
18481
  var LINTEL_DEPTH = 0.2;
17948
18482
  var LINTEL_BEARING = 0.2;
17949
18483
  var crossOf = (wall) => wall.axis === "u" ? wall.v0 : wall.u0;
17950
- var alongRange = (wall) => wall.axis === "u" ? [Math.min(wall.u0, wall.u1), Math.max(wall.u0, wall.u1)] : [Math.min(wall.v0, wall.v1), Math.max(wall.v0, wall.v1)];
18484
+ var alongRange2 = (wall) => wall.axis === "u" ? [Math.min(wall.u0, wall.u1), Math.max(wall.u0, wall.u1)] : [Math.min(wall.v0, wall.v1), Math.max(wall.v0, wall.v1)];
17951
18485
  function findOpenings(context) {
17952
18486
  const openings = [];
17953
18487
  for (const window of context.source.windows ?? []) {
17954
18488
  const from = window.at - window.width / 2;
17955
18489
  const to = window.at + window.width / 2;
17956
18490
  const wall = context.walls.find((w) => {
17957
- if (w.floor !== window.floor || w.axis !== window.axis || w.type !== "wall" || Math.abs(crossOf(w) - window.pos) > TOLERANCE) return false;
17958
- const [low, high] = alongRange(w);
17959
- return from >= low - TOLERANCE && to <= high + TOLERANCE;
18491
+ if (w.floor !== window.floor || w.axis !== window.axis || w.type !== "wall" || Math.abs(crossOf(w) - window.pos) > TOLERANCE2) return false;
18492
+ const [low, high] = alongRange2(w);
18493
+ return from >= low - TOLERANCE2 && to <= high + TOLERANCE2;
17960
18494
  });
17961
18495
  if (!wall) continue;
17962
18496
  const base = context.level(window.floor);
@@ -17971,18 +18505,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17971
18505
  const cross = door.axis === "u" ? door.v : door.u;
17972
18506
  const from = along - door.width / 2;
17973
18507
  const to = along + door.width / 2;
17974
- const beside = context.walls.filter((w) => w.floor === door.floor && w.axis === door.axis && w.type === "wall" && Math.abs(crossOf(w) - cross) <= TOLERANCE && (Math.abs(alongRange(w)[1] - from) <= 0.05 || Math.abs(alongRange(w)[0] - to) <= 0.05));
18508
+ const beside = context.walls.filter((w) => w.floor === door.floor && w.axis === door.axis && w.type === "wall" && Math.abs(crossOf(w) - cross) <= TOLERANCE2 && (Math.abs(alongRange2(w)[1] - from) <= 0.05 || Math.abs(alongRange2(w)[0] - to) <= 0.05));
17975
18509
  if (!beside.length) continue;
17976
18510
  const half = (beside[0].thickness ?? 0.15) / 2;
17977
18511
  const lo = from - LINTEL_BEARING;
17978
18512
  const hi = to + LINTEL_BEARING;
17979
18513
  const box22 = door.axis === "u" ? [lo, cross - half, hi, cross + half] : [cross - half, lo, cross + half, hi];
17980
18514
  const y0 = context.level(door.floor) + DOOR_HEIGHT;
17981
- const lintel = builder.add({ id: `lintel_${door.id}`, role: "beam", material: system.timber ? construction.frame.timber : "concrete", aabb: box22, y0, y1: y0 + LINTEL_DEPTH }, { floor: door.floor, ...system.timber ? {} : { assembly: "reinforced_concrete" } });
18515
+ const depth = Math.max(LINTEL_DEPTH, (door.width + LINTEL_BEARING) / 10);
18516
+ const lintel = builder.add({ id: `lintel_${door.id}`, role: "beam", material: system.timber ? construction.frame.timber : "concrete", aabb: box22, y0, y1: y0 + depth }, { floor: door.floor, ...system.timber ? {} : { assembly: "reinforced_concrete" } });
17982
18517
  for (const wall of beside) builder.rest(lintel, context.wallParts.get(wall.id), system.joints.beam);
18518
+ const jambs = (context.pillars.get(door.floor) ?? []).filter((pillar) => pillar.material === "concrete" && overlapsInPlan(pillar.aabb, box22) && pillar.y1 >= y0);
18519
+ if (construction.system === "masonry" && hi - lo > WIDE_DOOR) jambs.push(...addJambPillars(context, { door, beside, from, to, cross }, jambs));
18520
+ for (const pillar of jambs) builder.rest(lintel, pillar, "rebar");
17983
18521
  for (const plate of context.plates.get(door.floor) ?? []) if (overlapsInPlan(plate.aabb, box22)) builder.rest(plate, lintel, system.joints.beam);
17984
18522
  }
17985
18523
  }
18524
+ var WIDE_DOOR = 3;
18525
+ var HALF_PILLAR = WALL_PILLAR_SIDE / 2;
18526
+ function addJambPillars(context, gap, standing) {
18527
+ const { builder, construction } = context;
18528
+ const top = Math.max(...gap.beside.map((w) => context.wallParts.get(w.id).y1));
18529
+ const base = context.baseOf(gap.door.floor);
18530
+ const out = [];
18531
+ for (const [k, along] of [[0, gap.from - HALF_PILLAR], [1, gap.to + HALF_PILLAR]]) {
18532
+ const [u, v] = gap.door.axis === "u" ? [along, gap.cross] : [gap.cross, along];
18533
+ const box22 = [u - HALF_PILLAR, v - HALF_PILLAR, u + HALF_PILLAR, v + HALF_PILLAR];
18534
+ if (standing.some((p) => overlapsInPlan(p.aabb, box22))) continue;
18535
+ const pillar = builder.add({ id: `jamb_column_${gap.door.id}_${k}`, role: "column", material: construction.frame.pillars, aabb: box22, y0: context.level(gap.door.floor), y1: top }, { floor: gap.door.floor });
18536
+ builder.rest(pillar, base === GROUND || !base ? GROUND : base, "rebar");
18537
+ out.push(pillar);
18538
+ }
18539
+ return out;
18540
+ }
17986
18541
  var overlapsInPlan = (a, b) => a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1];
17987
18542
  var RISER = 0.18;
17988
18543
  var TREAD = 0.28;
@@ -18154,10 +18709,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18154
18709
  const framed = system.studs !== void 0 && !isRailing;
18155
18710
  const part = builder.add(
18156
18711
  { id: wall.id, role: wall.role, material: wall.material, aabb: wall.box, y0, y1: Math.max(y1, y0 + 0.2) },
18157
- { floor, fill: context.isTimber && wall.role === "wall" ? system.wallFill : 1, ...framed ? { studs: system.studs } : {}, ...isRailing ? {} : { assembly: assemblyOf(context) } }
18712
+ { floor, fill: context.isTimber && wall.role === "wall" ? system.wallFill : 1, ...framed ? { studs: system.studs } : {}, ...isRailing ? {} : { assembly: assemblyOf(context) }, ...wall.exterior && !isRailing ? { exterior: true } : {} }
18158
18713
  );
18159
18714
  context.wallParts.set(wall.id, part);
18160
- const base = isRailing ? railingBase(context, wall, floor) : context.baseOf(floor);
18715
+ const base = isRailing ? railingBase(context, wall, floor) : context.groundBeams.get(wall.id) ?? context.baseOf(floor);
18161
18716
  const joint = isRailing ? "nail" : system.joints.wall;
18162
18717
  if (base === GROUND || floor === 0 && !base) builder.rest(part, GROUND, joint);
18163
18718
  else if (base) builder.rest(part, base, joint);
@@ -18173,6 +18728,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18173
18728
  addUpperFloor(context, floor);
18174
18729
  }
18175
18730
  }
18731
+ var wallPillarSpacingOf = (options) => {
18732
+ const pillars = options.wallPillars ?? true;
18733
+ if (pillars === false) return 0;
18734
+ return pillars === true ? WALL_PILLAR_SPACING : pillars.spacing ?? WALL_PILLAR_SPACING;
18735
+ };
18176
18736
  function buildStructure(source, options = {}) {
18177
18737
  const refined = options.wallDetail !== "coarse";
18178
18738
  const built = options.construction ? { construction: options.construction, extras: options.extras ?? {} } : resolveConstruction(source);
@@ -18180,8 +18740,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18180
18740
  const extras = options.extras ?? built.extras ?? {};
18181
18741
  if (construction.system === "canvas") return buildCanvasStructure(construction, extras);
18182
18742
  const tank = options.tank !== void 0 ? options.tank : source.waterTank ?? null;
18183
- const context = createContext(source, construction, extras, tank, { porchRoofOnPostsOnly: options.porchRoofOnPostsOnly ?? refined, rafterRoof: options.rafterRoof ?? refined });
18743
+ const context = createContext(source, construction, extras, tank, { porchRoofOnPostsOnly: options.porchRoofOnPostsOnly ?? refined, rafterRoof: options.rafterRoof ?? refined, wallPillarSpacing: wallPillarSpacingOf(options), visorSections: options.visorSections === true });
18184
18744
  addFoundation(context);
18745
+ if (options.groundBeams !== false) addGroundBeams(context);
18185
18746
  buildFloors(context);
18186
18747
  addBalconies(context);
18187
18748
  addPorchRoofs(context);
@@ -18930,6 +19491,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18930
19491
  }
18931
19492
  var LINTEL_DEPTH2 = 0.2;
18932
19493
  var LINTEL_BEARING2 = 0.2;
19494
+ var SILL_BEAM_DEPTH = 0.1;
19495
+ var LINTEL_SPAN_RATIO = 10;
18933
19496
  var MIN_PIECE = 0.05;
18934
19497
  var frameOf = (wall) => {
18935
19498
  const alongU = wall.aabb[2] - wall.aabb[0] >= wall.aabb[3] - wall.aabb[1];
@@ -18943,6 +19506,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18943
19506
  const drafts = [];
18944
19507
  const piers = [];
18945
19508
  const lintels = [];
19509
+ const sillBeams = [];
18946
19510
  const spandrels = [];
18947
19511
  const height = wall.y1 - wall.y0;
18948
19512
  let cursor = frame2.from;
@@ -18953,23 +19517,26 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18953
19517
  };
18954
19518
  own.forEach((opening2, k) => {
18955
19519
  addPier(Math.max(cursor, opening2.from));
18956
- const lintelTop = Math.min(wall.y1, opening2.y1 + Math.max(LINTEL_DEPTH2, style.lintelDepth?.(opening2.to - opening2.from) ?? 0));
19520
+ const span = opening2.to - opening2.from;
19521
+ const lintelTop = Math.min(wall.y1, opening2.y1 + Math.max(LINTEL_DEPTH2, style.lintelDepth?.(span) ?? 0, (span + LINTEL_BEARING2) / LINTEL_SPAN_RATIO));
18957
19522
  const lintelFrom = Math.max(frame2.from, opening2.from - LINTEL_BEARING2);
18958
19523
  const lintelTo = Math.min(frame2.to, opening2.to + LINTEL_BEARING2);
18959
19524
  const lintelHeight = lintelTop - opening2.y1;
18960
- if (opening2.y0 - wall.y0 > MIN_PIECE) drafts.push({ id: `${wall.id}_sill_${k}`, role: "partition", material: wall.material, from: opening2.from, to: opening2.to, y0: wall.y0, y1: opening2.y0, volume: (opening2.to - opening2.from) * (opening2.y0 - wall.y0) });
19525
+ const sillTop = style.sillBeam && opening2.y0 - wall.y0 > SILL_BEAM_DEPTH + MIN_PIECE ? opening2.y0 - SILL_BEAM_DEPTH : opening2.y0;
19526
+ if (sillTop - wall.y0 > MIN_PIECE) drafts.push({ id: `${wall.id}_sill_${k}`, role: "partition", material: wall.material, from: opening2.from, to: opening2.to, y0: wall.y0, y1: sillTop, volume: (opening2.to - opening2.from) * (sillTop - wall.y0) });
19527
+ if (sillTop < opening2.y0) sillBeams.push({ id: `${wall.id}_sillbeam_${k}`, role: "beam", material: style.lintelMaterial, from: lintelFrom, to: lintelTo, y0: sillTop, y1: opening2.y0, volume: (lintelTo - lintelFrom) * SILL_BEAM_DEPTH });
18961
19528
  lintels.push({ id: `${wall.id}_lintel_${k}`, role: "beam", material: style.lintelMaterial, from: lintelFrom, to: lintelTo, y0: opening2.y1, y1: lintelTop, volume: (lintelTo - lintelFrom) * lintelHeight });
18962
19529
  spandrels.push(wall.y1 - lintelTop > MIN_PIECE ? { id: `${wall.id}_spandrel_${k}`, role: roleOf2, material: wall.material, from: opening2.from, to: opening2.to, y0: lintelTop, y1: wall.y1, volume: (opening2.to - opening2.from) * (wall.y1 - lintelTop) } : void 0);
18963
19530
  cursor = opening2.to;
18964
19531
  });
18965
19532
  addPier(frame2.to);
18966
19533
  for (const pier of piers) {
18967
- for (const lintel of lintels) {
19534
+ for (const lintel of [...lintels, ...sillBeams]) {
18968
19535
  const overlap = Math.max(0, Math.min(pier.to, lintel.to) - Math.max(pier.from, lintel.from));
18969
19536
  pier.volume -= overlap * (lintel.y1 - lintel.y0);
18970
19537
  }
18971
19538
  }
18972
- const all = [...piers, ...drafts, ...lintels, ...spandrels.filter((d) => d !== void 0)];
19539
+ const all = [...piers, ...drafts, ...sillBeams, ...lintels, ...spandrels.filter((d) => d !== void 0)];
18973
19540
  const total = all.reduce((sum, d) => sum + d.volume, 0);
18974
19541
  const pieces = all.map((d) => ({
18975
19542
  id: d.id,
@@ -18984,6 +19551,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18984
19551
  ...d.role === "beam" ? style.lintelAssembly ? { assembly: style.lintelAssembly } : {} : wall.assembly ? { assembly: wall.assembly } : {}
18985
19552
  }));
18986
19553
  const internal = [];
19554
+ for (const beam of sillBeams) {
19555
+ const sill = drafts.find((d) => d.id === beam.id.replace("_sillbeam_", "_sill_"));
19556
+ if (sill) internal.push({ part: beam.id, on: sill.id, joint: style.lintelJoint });
19557
+ for (const pier of piers) if (Math.min(pier.to, beam.to) - Math.max(pier.from, beam.from) > 0) internal.push({ part: beam.id, on: pier.id, joint: style.lintelJoint });
19558
+ }
18987
19559
  lintels.forEach((lintel, k) => {
18988
19560
  for (const pier of piers) if (Math.min(pier.to, lintel.to) - Math.max(pier.from, lintel.from) > 0) internal.push({ part: lintel.id, on: pier.id, joint: style.lintelJoint });
18989
19561
  const spandrel = spandrels[k];
@@ -19013,7 +19585,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19013
19585
  ...system.timber ? {} : { lintelAssembly: "reinforced_concrete" },
19014
19586
  lintelJoint: system.joints.beam,
19015
19587
  wallJoint: system.joints.wall,
19016
- ...lintelDepth && !system.timber ? { lintelDepth } : {}
19588
+ ...lintelDepth && !system.timber ? { lintelDepth } : {},
19589
+ ...construction.system === "masonry" ? { sillBeam: true } : {}
19017
19590
  });
19018
19591
  if (split.pieces.length > 1) splits.set(part.id, split);
19019
19592
  }
@@ -19021,7 +19594,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19021
19594
  }
19022
19595
  function applyPanels(input, structure, detail, lintelDepth) {
19023
19596
  if (detail === "coarse") return input;
19024
- return applySplits(input, panelSplits(input, structure, lintelDepth));
19597
+ return onJambPillars(applySplits(input, panelSplits(input, structure, lintelDepth)));
19598
+ }
19599
+ var OPENING_BEAM = /_(lintel|sillbeam)_\d+$/;
19600
+ var overlaps2 = (a, b) => a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1];
19601
+ function onJambPillars(split) {
19602
+ const pillars = split.parts.filter((part) => part.role === "column" && part.material === "concrete");
19603
+ const extra = [];
19604
+ for (const beam of split.parts) {
19605
+ if (beam.role !== "beam" || !OPENING_BEAM.test(beam.id)) continue;
19606
+ for (const pillar of pillars) if (pillar.floor === beam.floor && overlaps2(pillar.aabb, beam.aabb) && pillar.y0 <= beam.y0 && pillar.y1 >= beam.y1 - 0.01) extra.push({ part: beam.id, on: pillar.id, joint: "rebar" });
19607
+ }
19608
+ return extra.length ? { parts: split.parts, supports: [...split.supports, ...extra] } : split;
19025
19609
  }
19026
19610
  function applyUnits(input, structure) {
19027
19611
  const splits = /* @__PURE__ */ new Map();
@@ -19083,21 +19667,35 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19083
19667
  };
19084
19668
  var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
19085
19669
  var CAGED_ROLES = /* @__PURE__ */ new Set(["beam", "column"]);
19670
+ var CAGE_COVER = 0.03;
19671
+ var CAGE_BUCKLING_SHARE = 0.5;
19672
+ var DEFAULT_RESIDUAL = 0.15;
19673
+ function cageResidualCapacity(size, reinforcement, capacity) {
19674
+ const [a = 0, b = 0] = [...size].sort((x, y) => x - y);
19675
+ const perSide = (w) => Math.max(2, Math.floor(Math.max(0, w - 2 * CAGE_COVER) / reinforcement.spacing) + 1);
19676
+ const bars = 2 * perSide(a) + 2 * perSide(b) - 4;
19677
+ const steel = MATERIALS2.find((m) => m.id === reinforcement.material)?.compressive ?? 0;
19678
+ const carries2 = bars * Math.PI * (reinforcement.diameter / 2) ** 2 * steel * CAGE_BUCKLING_SHARE;
19679
+ if (!capacity || capacity <= 0 || carries2 <= 0) return DEFAULT_RESIDUAL;
19680
+ return Math.min(1, Math.max(DEFAULT_RESIDUAL, carries2 / capacity));
19681
+ }
19086
19682
  var caged = (spec, part, rebar) => {
19087
19683
  if (!rebar || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
19088
19684
  const size = spec.shape.kind === "box" ? spec.shape.size : null;
19089
19685
  if (!size) return spec;
19090
19686
  const thin = Math.min(...size);
19687
+ const reinforcement = { material: rebar, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter };
19091
19688
  return {
19092
19689
  ...spec,
19093
- reinforcement: { material: rebar, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter },
19690
+ reinforcement: part.role === "column" ? { ...reinforcement, residualCapacity: cageResidualCapacity(size, reinforcement, spec.structure?.capacity) } : reinforcement,
19094
19691
  units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
19095
19692
  };
19096
19693
  };
19097
19694
  var partSpecOf = (part, capacity, detailed, rebar) => {
19098
19695
  if (detailed && part.assembly) return assemblySpec(part, capacity);
19099
19696
  const spec = boxSpec(part, capacity);
19100
- return detailed ? caged(spec, part, rebar) : spec;
19697
+ const out = detailed ? caged(spec, part, rebar) : spec;
19698
+ return part.beamKind === "ground_beam" ? { ...out, anchored: true, structure: { role: "foundation" } } : out;
19101
19699
  };
19102
19700
  function toDestructibleSpec(structure, options = {}) {
19103
19701
  const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
@@ -19113,7 +19711,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19113
19711
  const walled = applyPanels(slabbed, structure, detail, slabOptions && structure.construction.frame.rebar ? (span) => beamDepthFor(span, slabOptions) : void 0);
19114
19712
  const panels = slabOptions && detail !== "coarse" ? deepenBondBeams(walled, structure, slabOptions) : walled;
19115
19713
  const sizing = { safety, redundancy, safetyOthers };
19116
- const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts, panels.supports, sizing) };
19714
+ const foundation = new Set(panels.parts.filter((part) => part.beamKind === "ground_beam").map((part) => part.id));
19715
+ const onFoundation = foundation.size ? panels.supports.filter((s) => !foundation.has(s.part)).map((s) => foundation.has(s.on) ? { ...s, on: GROUND } : s) : panels.supports;
19716
+ const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts.filter((part) => !foundation.has(part.id)), onFoundation, sizing) };
19117
19717
  const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
19118
19718
  const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
19119
19719
  const margin = options.terrainMargin ?? TERRAIN_MARGIN;
@@ -19178,6 +19778,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19178
19778
  out.parts = out.parts.map((p, i) => i === ground ? raft2 : p);
19179
19779
  return out;
19180
19780
  }
19781
+ var LOW_WINDOW = SILL_BEAM_DEPTH + 0.05;
19181
19782
  var slabId = (g, f) => f === g.floors - 1 && g.roof ? "roof" : `slab_${f}`;
19182
19783
  var storeyBase = (g, f) => f * (g.height + g.slab);
19183
19784
  var slabHalf = (g) => [(g.spanX + g.columnSize + 0.2) / 2, (g.spanZ + g.columnSize + 0.2) / 2];
@@ -19336,7 +19937,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19336
19937
  parts.push({ id, material: c.material, shape: box2(size), position, structure: { role: "partition" }, tags: ["infill"] });
19337
19938
  for (const o of on) rests(joints, id, o, "mortar");
19338
19939
  };
19339
- const bay = (side, k, along, centre, fixed, length, c1, c2) => {
19940
+ const bay = (side, k, along, centre2, fixed, length, c1, c2) => {
19340
19941
  const clear = length - g.columnSize;
19341
19942
  const win = clear * c.window;
19342
19943
  const pier = (clear - win) / 2;
@@ -19347,11 +19948,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19347
19948
  wall(`infill_${f}_${side}_${k}_${name}`, size(u, h), [p[0], y, p[2]], on);
19348
19949
  };
19349
19950
  const id = (name) => `infill_${f}_${side}_${k}_${name}`;
19350
- put("sill", clear, sill, yb + sill / 2, centre, [c1, c2]);
19351
- put("pl", pier, head - sill, yb + sill + (head - sill) / 2, centre - clear / 2 + pier / 2, [c1, id("sill")]);
19352
- put("pr", pier, head - sill, yb + sill + (head - sill) / 2, centre + clear / 2 - pier / 2, [c2, id("sill")]);
19353
- put("head", clear, room - head, yb + head + (room - head) / 2, centre, [c1, c2]);
19354
- const gp = at2(centre);
19951
+ put("sill", clear, sill, yb + sill / 2, centre2, [c1, c2]);
19952
+ put("pl", pier, head - sill, yb + sill + (head - sill) / 2, centre2 - clear / 2 + pier / 2, [c1, id("sill")]);
19953
+ put("pr", pier, head - sill, yb + sill + (head - sill) / 2, centre2 + clear / 2 - pier / 2, [c2, id("sill")]);
19954
+ put("head", clear, room - head, yb + head + (room - head) / 2, centre2, [c1, c2]);
19955
+ const gp = at2(centre2);
19355
19956
  const gid = `glass_${f}_${side}_${k}`;
19356
19957
  parts.push({
19357
19958
  id: gid,
@@ -19995,7 +20596,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19995
20596
  if (!pc || pc.unit) return sp;
19996
20597
  if (pc.role !== "wall" && pc.role !== "partition") {
19997
20598
  const def = getAssembly(pc.assembly || (pc.material === "concrete" && ["slab", "beam", "column", "stair"].includes(pc.role) ? "reinforced_concrete" : ""));
19998
- const out = def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
20599
+ const cage = def && def.reinforcement ? pc.role === "column" && sp.shape.kind === "box" ? { ...def.reinforcement, residualCapacity: cageResidualCapacity(sp.shape.size, def.reinforcement, sp.structure?.capacity) } : { ...def.reinforcement } : null;
20600
+ const out = cage && !sp.reinforcement ? { ...sp, reinforcement: cage } : sp;
19999
20601
  if (out.reinforcement && !out.units && pc.material === "concrete" && (pc.role === "beam" || pc.role === "column") && sp.shape.kind === "box") {
20000
20602
  return { ...out, units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, Math.min(...sp.shape.size)], bond: "stack", mortar: "concrete" } };
20001
20603
  }
@@ -20023,6 +20625,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20023
20625
  const a = pos.array;
20024
20626
  const u = mesh2.userData;
20025
20627
  if (!u._p0) u._p0 = a.slice();
20628
+ if (!u.plainPositions) Object.defineProperty(u, "plainPositions", { value: u._p0, enumerable: false, configurable: true, writable: true });
20026
20629
  const list2 = u.collapsedRanges ?? (u.collapsedRanges = []);
20027
20630
  if (!list2.some((r) => r[0] === v0 && r[1] === v1)) list2.push([v0, v1]);
20028
20631
  const saved = a.slice(v0 * 3, v1 * 3);
@@ -20106,10 +20709,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20106
20709
  const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
20107
20710
  return [v.x, v.y, v.z];
20108
20711
  };
20109
- function cells(part, units) {
20712
+ function cells(part, units, mask) {
20110
20713
  const [u0, v0, u1, v1] = part.aabb;
20111
- const alongU = u1 - u0 >= v1 - v0;
20714
+ const alongU = mask.alongU;
20112
20715
  const len = alongU ? u1 - u0 : v1 - v0;
20716
+ const offset = (alongU ? u0 : v0) - mask.start;
20113
20717
  const [ul, uh] = units.size;
20114
20718
  const rows = Math.max(1, Math.round((part.y1 - part.y0) / uh));
20115
20719
  const rowH = (part.y1 - part.y0) / rows;
@@ -20120,7 +20724,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20120
20724
  const shift = units.bond === "running" && r & 1 ? w / 2 : 0;
20121
20725
  for (let i = 0; i < per + (shift ? 1 : 0); i++) {
20122
20726
  const a = Math.max(0, i * w - shift), b = Math.min(len, i * w - shift + w);
20123
- if (b - a > 1e-3) out.push({ a, b, y0: part.y0 + r * rowH, y1: part.y0 + (r + 1) * rowH, r, i });
20727
+ if (b - a > 1e-3) out.push({ a: offset + a, b: offset + b, y0: part.y0 + r * rowH, y1: part.y0 + (r + 1) * rowH, r, i });
20124
20728
  }
20125
20729
  }
20126
20730
  return out;
@@ -20134,8 +20738,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20134
20738
  for (const q of structure.parts) {
20135
20739
  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;
20136
20740
  }
20741
+ return part.role === "column" ? wallAround(part) : null;
20742
+ }
20743
+ function wallAround(part) {
20744
+ const cu = (part.aabb[0] + part.aabb[2]) / 2, cv = (part.aabb[1] + part.aabb[3]) / 2;
20745
+ for (const q of structure.parts) {
20746
+ if (q.id === part.id || q.role !== "wall" && q.role !== "partition" || !structure.meshesOf(q.id).some((it) => it.mesh)) continue;
20747
+ const inside2 = cu >= q.aabb[0] - 1e-3 && cu <= q.aabb[2] + 1e-3 && cv >= q.aabb[1] - 1e-3 && cv <= q.aabb[3] + 1e-3;
20748
+ if (inside2 && Math.min(q.y1, part.y1) - Math.max(q.y0, part.y0) > 0.5) return q.id;
20749
+ }
20137
20750
  return null;
20138
20751
  }
20752
+ const pillarSpans = (view) => structure.parts.filter((q) => q.role === "column" && wallAround(q) === view.host).map((q) => {
20753
+ const a = (view.mask.alongU ? q.aabb[0] : q.aabb[1]) - view.mask.start, b = (view.mask.alongU ? q.aabb[2] : q.aabb[3]) - view.mask.start;
20754
+ return { a, b, y0: q.y0, y1: q.y1 };
20755
+ });
20139
20756
  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);
20140
20757
  function shell(it, mask) {
20141
20758
  const g0 = it.mesh.geometry, g = new THREE.BufferGeometry();
@@ -20224,6 +20841,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20224
20841
  texture.needsUpdate = true;
20225
20842
  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) };
20226
20843
  }
20844
+ function besidePillars(c, pillars) {
20845
+ let { a, b } = c;
20846
+ for (const k of pillars) {
20847
+ if (c.y1 <= k.y0 || c.y0 >= k.y1 || b <= k.a || a >= k.b) continue;
20848
+ const centre = (a + b) / 2;
20849
+ if (centre > k.a && centre < k.b) return [];
20850
+ if (centre <= k.a) b = Math.min(b, k.a);
20851
+ else a = Math.max(a, k.b);
20852
+ }
20853
+ return b - a > 1e-3 ? [{ ...c, a, b }] : [];
20854
+ }
20227
20855
  const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
20228
20856
  const ellipseDistance = (s, y, cs, cy, rs, ry) => (Math.hypot((s - cs) / Math.max(rs, 1e-4), (y - cy) / Math.max(ry, 1e-4)) - 1) * Math.min(rs, ry);
20229
20857
  function covered(c, boxes) {
@@ -20246,9 +20874,15 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20246
20874
  const discs = [];
20247
20875
  const flakes = [];
20248
20876
  const shown = [];
20877
+ const pillars = pillarSpans(view);
20249
20878
  for (const id of view.parts) {
20250
20879
  const part = structure.byId.get(id), units2 = specOf(id).units, chips = view.chips.get(id), opening = view.openings.get(id), spalls = view.spalls.get(id);
20251
- const all = cells(part, units2);
20880
+ const own = cells(part, units2, mask);
20881
+ const all = id === view.host ? own.flatMap((c) => besidePillars(c, pillars)) : own;
20882
+ if (view.fallen.has(id)) {
20883
+ holes.push(...all);
20884
+ continue;
20885
+ }
20252
20886
  const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
20253
20887
  const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
20254
20888
  if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
@@ -20344,52 +20978,55 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20344
20978
  }
20345
20979
  return { center: local(chip.center), along: a, up: u };
20346
20980
  }
20981
+ function newView(hostId) {
20982
+ const items = structure.meshesOf(hostId).filter((it) => it.mesh);
20983
+ if (!items.length) return null;
20984
+ const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
20985
+ const mask = maskOf(hostId, parts), core = maskOf(hostId, parts);
20986
+ return {
20987
+ host: hostId,
20988
+ parts,
20989
+ items,
20990
+ mask,
20991
+ core,
20992
+ unitMaterial: unitsMaterial(core),
20993
+ shells: [],
20994
+ saved: null,
20995
+ units: null,
20996
+ gone: 0,
20997
+ pits: 0,
20998
+ bounds: drawnBounds(items),
20999
+ chips: new Map(parts.map((id) => [id, []])),
21000
+ spalls: new Map(parts.map((id) => [id, []])),
21001
+ openings: /* @__PURE__ */ new Map(),
21002
+ flakes: 0,
21003
+ fallen: /* @__PURE__ */ new Set()
21004
+ };
21005
+ }
21006
+ function cut(view) {
21007
+ if (view.saved) return;
21008
+ view.shells = view.items.map((it) => shell(it, view.mask));
21009
+ for (const m of view.shells) m.userData.behind = () => view.units;
21010
+ view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
21011
+ }
20347
21012
  function draw2(partId, change) {
20348
21013
  if (lod !== 0) return false;
20349
21014
  const hostId = hostOf(partId);
20350
21015
  if (!hostId || !specOf(partId)?.units) return false;
20351
- let view = views.get(hostId);
20352
- if (!view) {
20353
- const items = structure.meshesOf(hostId).filter((it) => it.mesh);
20354
- if (!items.length) return false;
20355
- const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
20356
- if (!parts.includes(partId)) return false;
20357
- const mask = maskOf(hostId, parts), core = maskOf(hostId, parts);
20358
- view = {
20359
- host: hostId,
20360
- parts,
20361
- items,
20362
- mask,
20363
- core,
20364
- unitMaterial: unitsMaterial(core),
20365
- shells: [],
20366
- saved: null,
20367
- units: null,
20368
- gone: 0,
20369
- pits: 0,
20370
- bounds: drawnBounds(items),
20371
- chips: new Map(parts.map((id) => [id, []])),
20372
- spalls: new Map(parts.map((id) => [id, []])),
20373
- openings: /* @__PURE__ */ new Map(),
20374
- flakes: 0
20375
- };
20376
- }
20377
- if (!view.chips.has(partId)) return false;
21016
+ const view = views.get(hostId) ?? newView(hostId);
21017
+ if (!view || !view.chips.has(partId)) return false;
20378
21018
  if (change.opening) {
20379
21019
  if (view.openings.has(partId)) return false;
20380
21020
  view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
20381
21021
  }
20382
21022
  if (change.chip) view.chips.get(partId).push(chipBox(change.chip, view.mask.alongU));
21023
+ for (const chip of change.chips ?? []) view.chips.get(partId).push(chipBox(chip, view.mask.alongU));
20383
21024
  if (change.spall) {
20384
21025
  const list2 = view.spalls.get(partId);
20385
21026
  list2.push({ center: local(change.spall.center), radius: change.spall.radius });
20386
21027
  if (list2.length > MAX_FLAKES) list2.shift();
20387
21028
  }
20388
- if (!view.saved) {
20389
- view.shells = view.items.map((it) => shell(it, view.mask));
20390
- for (const m of view.shells) m.userData.behind = () => view.units;
20391
- view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
20392
- }
21029
+ cut(view);
20393
21030
  views.set(hostId, view);
20394
21031
  if (opts.deferPaint && change.spall) dirty.add(view);
20395
21032
  else {
@@ -20500,14 +21137,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20500
21137
  */
20501
21138
  chip(event) {
20502
21139
  if (event.torn) return false;
20503
- return draw2(event.part, { chip: { center: event.center, size: event.size ?? [0, 0, 0], rotation: event.rotation ?? [0, 0, 0, 1] } });
21140
+ const rotation = event.rotation ?? [0, 0, 0, 1];
21141
+ if (event.units?.length) return draw2(event.part, { chips: event.units.map((unit) => ({ center: unit.center, size: unit.size, rotation })) });
21142
+ return draw2(event.part, { chip: { center: event.center, size: event.size ?? [0, 0, 0], rotation } });
20504
21143
  },
20505
21144
  /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
20506
21145
  drop(partId) {
20507
- const v = views.get(partId);
20508
- if (!v) return;
21146
+ let host = views.get(partId) ?? [...views.values()].find((view) => view.parts.includes(partId));
21147
+ if (!host && lod === 0 && riders(partId).length > 0) {
21148
+ host = newView(partId);
21149
+ if (host) {
21150
+ cut(host);
21151
+ views.set(partId, host);
21152
+ }
21153
+ }
21154
+ if (!host) return;
21155
+ host.fallen.add(partId);
21156
+ if (host.parts.some((id) => !host.fallen.has(id))) {
21157
+ dirty.delete(host);
21158
+ paint(host);
21159
+ invalidate();
21160
+ invalidateShadow();
21161
+ return;
21162
+ }
21163
+ const v = host;
20509
21164
  dispose(v);
20510
- views.delete(partId);
21165
+ views.delete(v.host);
20511
21166
  if (v.saved) dropped.push(v);
20512
21167
  },
20513
21168
  /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
@@ -20986,6 +21641,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20986
21641
  shared.ctx.noPiece = false;
20987
21642
  cursor.separate = opts.pieces === "meshes";
20988
21643
  cursor.pc = null;
21644
+ shared.ctx.showPillars = opts.pillars !== "hidden";
20989
21645
  shared.ctx.apartment = r.template === "apartment_modern";
20990
21646
  shared.ctx.pa = Math.max(3, Math.min(20, pr.floors != null ? pr.floors : 5));
20991
21647
  shared.ctx.pf = Math.max(1, Math.min(shared.ctx.pa, pr.floor != null ? pr.floor : 3));
@@ -22153,10 +22809,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22153
22809
  const list2 = events(room);
22154
22810
  const columns = groundColumns(parts);
22155
22811
  const laj = slabs(parts);
22156
- const first = columns[0];
22157
- const mate = columns.slice(1).filter((p) => Math.abs(p.position[0] - first.position[0]) < 0.5).sort((a, b) => Math.abs(b.position[2] - first.position[2]) - Math.abs(a.position[2] - first.position[2]))[0] ?? columns[1];
22158
- blunt(room, oid, first.id, centre(first.id), 3e6);
22159
- blunt(room, oid, mate.id, centre(mate.id), 3e6);
22812
+ const first = columns.find((p) => !/^wall_column_/.test(p.id)) ?? columns[0];
22813
+ const mate = columns.filter((p) => p !== first && !/^wall_column_/.test(p.id)).filter((p) => Math.abs(p.position[0] - first.position[0]) < 0.5).sort((a, b) => Math.abs(b.position[2] - first.position[2]) - Math.abs(a.position[2] - first.position[2]))[0] ?? columns[1];
22814
+ const along = columns.filter((p) => /^wall_column_/.test(p.id) && Math.abs(p.position[0] - first.position[0]) < 0.5);
22815
+ for (const p of [first, mate, ...along]) blunt(room, oid, p.id, centre(p.id), 3e6);
22160
22816
  step(90);
22161
22817
  const fell = collapsed(list2);
22162
22818
  expect(laj.some((id) => fell.includes(id)), fill(T2.two_slab_fell, { n: fell.length }));
@@ -24329,6 +24985,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24329
24985
  }
24330
24986
 
24331
24987
  // src/wall.ts
24988
+ var WARM_UP_SHOT = { type: "ballistic", point: [0, 1.2, 0.1], direction: [0, 0, -1], energy: 3500, caliber: 7.62 };
24989
+ var WARM_UP_FRAMES = 5;
24990
+ var isTexture = (value) => typeof value === "object" && value !== null && value.isTexture === true;
24332
24991
  var PLASTER = 15262420;
24333
24992
  var WALL_ID = "wall_0_0";
24334
24993
  function wallPlan(options) {
@@ -24387,7 +25046,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24387
25046
  plan,
24388
25047
  spec,
24389
25048
  build: (T2) => label(draw(T2, plan, houseOptions)),
24390
- follow: (root, world, objectId) => houses.get(root)?.destruction?.follow?.(world, objectId, spec) ?? null
25049
+ follow: (root, world, objectId) => houses.get(root)?.destruction?.follow?.(world, objectId, spec) ?? null,
25050
+ warmUpProbes: (T2) => {
25051
+ const house = draw(T2, plan, houseOptions);
25052
+ const root = label(house);
25053
+ const world = createDestruction({ seed: 1 });
25054
+ const objectId = world.add(spec, { id: "warm_up_wall" });
25055
+ const destruction = house.destruction;
25056
+ const follower = destruction?.follow?.(world, objectId, spec) ?? null;
25057
+ world.hitPart(objectId, WALL_ID, WARM_UP_SHOT);
25058
+ for (let frame2 = 0; frame2 < WARM_UP_FRAMES; frame2++) {
25059
+ world.update(1 / 60);
25060
+ follower?.update?.();
25061
+ }
25062
+ for (let guard = 0; guard < WARM_UP_FRAMES && (destruction?.breach?.flush?.(Infinity) ?? 0) > 0; guard++) ;
25063
+ root.traverse((node) => {
25064
+ node.visible = true;
25065
+ node.frustumCulled = false;
25066
+ });
25067
+ return {
25068
+ group: root,
25069
+ // the materials stay: the warmup keeps them as anchors, and disposing the last material of a program frees the program it was drawn for (so the
25070
+ // follower is not disposed either: it would put the wall back and dispose the breach's materials; its world goes with the probe)
25071
+ dispose: () => {
25072
+ root.traverse((node) => {
25073
+ node.geometry?.dispose();
25074
+ for (const material of Array.isArray(node.material) ? node.material : node.material ? [node.material] : []) {
25075
+ for (const uniform2 of Object.values(material.uniforms ?? {})) if (isTexture(uniform2.value)) uniform2.value.dispose();
25076
+ material.map?.dispose();
25077
+ }
25078
+ });
25079
+ }
25080
+ };
25081
+ }
24391
25082
  };
24392
25083
  }
24393
25084
  return __toCommonJS(index_exports);