@adatechnology/game-floorplan-structure 2.0.1 → 2.1.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.
@@ -23,6 +23,7 @@ var GameFloorplanStructure = (() => {
23
23
  // src/index.ts
24
24
  var index_exports = {};
25
25
  __export(index_exports, {
26
+ BEAM_DEPTH_RATIO: () => BEAM_DEPTH_RATIO,
26
27
  CONSTRUCTIONS: () => CONSTRUCTIONS,
27
28
  CONSTRUCTION_SYSTEMS: () => CONSTRUCTION_SYSTEMS,
28
29
  CORE_DEFAULTS: () => CORE_DEFAULTS,
@@ -31,8 +32,14 @@ var GameFloorplanStructure = (() => {
31
32
  DENSITY: () => DENSITY,
32
33
  FLAT_ROOF_DEFAULTS: () => FLAT_ROOF_DEFAULTS,
33
34
  GROUND: () => GROUND,
35
+ GROUND_BEAM_DEPTH: () => GROUND_BEAM_DEPTH,
36
+ MAX_LINTEL_ON_MASONRY: () => MAX_LINTEL_ON_MASONRY,
37
+ MAX_PILLAR_SPACING: () => MAX_PILLAR_SPACING,
34
38
  MECHANICAL_DEFAULTS: () => MECHANICAL_DEFAULTS,
35
39
  MEMBER_CAGE: () => MEMBER_CAGE,
40
+ MIN_BEAM_WIDTH: () => MIN_BEAM_WIDTH,
41
+ MIN_PILLAR_AREA: () => MIN_PILLAR_AREA,
42
+ MIN_PILLAR_SIDE: () => MIN_PILLAR_SIDE,
36
43
  PILE_AREA: () => PILE_AREA,
37
44
  PILE_DEFAULTS: () => PILE_DEFAULTS,
38
45
  PLATE_DEFAULTS: () => PLATE_DEFAULTS,
@@ -40,10 +47,15 @@ var GameFloorplanStructure = (() => {
40
47
  RAFT_THICKNESS: () => RAFT_THICKNESS,
41
48
  REBAR_RATIOS: () => REBAR_RATIOS,
42
49
  ROOF_COVERINGS: () => ROOF_COVERINGS,
50
+ SILL_BEAM_DEPTH: () => SILL_BEAM_DEPTH,
43
51
  SKYSCRAPER_DEFAULTS: () => SKYSCRAPER_DEFAULTS,
44
52
  SKYSCRAPER_MAX_STOREYS: () => SKYSCRAPER_MAX_STOREYS,
45
53
  SLAB_PANEL_DEFAULTS: () => SLAB_PANEL_DEFAULTS,
54
+ STRUCTURAL_VIOLATION: () => STRUCTURAL_VIOLATION,
55
+ TANK_PILLAR_SIDE: () => TANK_PILLAR_SIDE,
46
56
  TOWER_DEFAULTS: () => TOWER_DEFAULTS,
57
+ WALL_PILLAR_SIDE: () => WALL_PILLAR_SIDE,
58
+ WALL_PILLAR_SPACING: () => WALL_PILLAR_SPACING,
47
59
  addCore: () => addCore,
48
60
  addCurtainWall: () => addCurtainWall,
49
61
  addFlatRoof: () => addFlatRoof2,
@@ -57,16 +69,19 @@ var GameFloorplanStructure = (() => {
57
69
  buildStructure: () => buildStructure,
58
70
  buildTower: () => buildTower,
59
71
  buildWallSpecimen: () => buildWallSpecimen,
72
+ cageResidualCapacity: () => cageResidualCapacity,
60
73
  campConstruction: () => campConstruction,
61
74
  chaletConstruction: () => chaletConstruction,
62
75
  constructionOfSystem: () => constructionOfSystem,
63
76
  coreSize: () => coreSize,
77
+ countViolations: () => countViolations,
64
78
  damperMass: () => damperMass,
65
79
  exteriorWindows: () => exteriorWindows,
66
80
  logCabinConstruction: () => logCabinConstruction,
67
81
  masonryConstruction: () => masonryConstruction,
68
82
  mechanicalStoreys: () => mechanicalStoreys,
69
83
  pilesFor: () => pilesFor,
84
+ pillarStations: () => pillarStations,
70
85
  planToSpace: () => planToSpace,
71
86
  planWaterTank: () => planWaterTank,
72
87
  register: () => register,
@@ -87,6 +102,8 @@ var GameFloorplanStructure = (() => {
87
102
  towerLocales: () => towerLocales,
88
103
  towerPlate: () => towerPlate,
89
104
  towerWater: () => towerWater,
105
+ validateSpec: () => validateSpec,
106
+ validateStructure: () => validateStructure,
90
107
  version: () => version,
91
108
  windExposureOf: () => windExposureOf,
92
109
  woodFrameConstruction: () => woodFrameConstruction
@@ -424,12 +441,15 @@ var GameFloorplanStructure = (() => {
424
441
  builder,
425
442
  porchRoofOnPostsOnly: options.porchRoofOnPostsOnly,
426
443
  rafterRoof: options.rafterRoof,
444
+ wallPillarSpacing: options.wallPillarSpacing,
445
+ visorSections: options.visorSections,
427
446
  floorCount,
428
447
  isUnit,
429
448
  elevation,
430
449
  isTimber: system.timber,
431
450
  walls: [],
432
451
  wallParts: /* @__PURE__ */ new Map(),
452
+ groundBeams: /* @__PURE__ */ new Map(),
433
453
  carriers: /* @__PURE__ */ new Map(),
434
454
  plates: /* @__PURE__ */ new Map(),
435
455
  pillars: /* @__PURE__ */ new Map(),
@@ -502,7 +522,8 @@ var GameFloorplanStructure = (() => {
502
522
  builder.rest(deck, base, system.joints.slab);
503
523
  continue;
504
524
  }
505
- const pillar = builder.add({ id: `balcony_column_${room.id}`, role: "column", material: construction.frame.pillars, aabb: [box2[0] - 0.08, box2[1] - 0.08, box2[0] + 0.08, box2[1] + 0.08], y0: 0, y1: context.level(floor) - system.slabThickness }, { floor: 0 });
525
+ const half = context.visorSections ? 0.08 : 0.1;
526
+ const pillar = builder.add({ id: `balcony_column_${room.id}`, role: "column", material: construction.frame.pillars, aabb: [box2[0] - half, box2[1] - half, box2[0] + half, box2[1] + half], y0: 0, y1: context.level(floor) - system.slabThickness }, { floor: 0 });
506
527
  builder.rest(pillar, GROUND, "rebar");
507
528
  builder.rest(deck, pillar, "rebar");
508
529
  }
@@ -573,8 +594,8 @@ var GameFloorplanStructure = (() => {
573
594
  const top = context.level(floor) - thickness * 0.15;
574
595
  const out = [];
575
596
  for (let k = 0; k < count; k++) {
576
- const centre = (alongU ? box2[0] : box2[1]) + 0.05 + k * (span - 0.1) / (count - 1);
577
- const aabb = alongU ? [centre - 0.025, box2[1], centre + 0.025, box2[3]] : [box2[0], centre - 0.025, box2[2], centre + 0.025];
597
+ const centre2 = (alongU ? box2[0] : box2[1]) + 0.05 + k * (span - 0.1) / (count - 1);
598
+ const aabb = alongU ? [centre2 - 0.025, box2[1], centre2 + 0.025, box2[3]] : [box2[0], centre2 - 0.025, box2[2], centre2 + 0.025];
578
599
  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 });
579
600
  out.push(joist);
580
601
  const below = carriers.filter((carrier) => carrier.y0 <= top + 0.05);
@@ -664,8 +685,8 @@ var GameFloorplanStructure = (() => {
664
685
  const at2 = alongU ? box2[1] + line * (box2[3] - box2[1]) / (rows - 1) : box2[0] + line * (box2[2] - box2[0]) / (columns - 1);
665
686
  const aabb = alongU ? [box2[0], at2 - 0.07, box2[2], at2 + 0.07] : [at2 - 0.07, box2[1], at2 + 0.07, box2[3]];
666
687
  const beam = builder.add({ id: `base_beam_${line}`, role: "beam", material: construction.frame.timber, aabb, y0: height, y1: elevation - context.system.slabThickness }, { floor: 0 });
667
- const centre = (q) => alongU ? (q.aabb[1] + q.aabb[3]) / 2 : (q.aabb[0] + q.aabb[2]) / 2;
668
- for (const pier of piers.filter((q) => Math.abs(centre(q) - at2) < 0.05)) builder.rest(beam, pier, "bolt");
688
+ const centre2 = (q) => alongU ? (q.aabb[1] + q.aabb[3]) / 2 : (q.aabb[0] + q.aabb[2]) / 2;
689
+ for (const pier of piers.filter((q) => Math.abs(centre2(q) - at2) < 0.05)) builder.rest(beam, pier, "bolt");
669
690
  beams.push(beam);
670
691
  }
671
692
  return beams;
@@ -686,18 +707,115 @@ var GameFloorplanStructure = (() => {
686
707
  else if (context.isTimber && type !== "ground") context.base0 = raft(context, Math.max(0.12, context.elevation), "concrete");
687
708
  }
688
709
 
710
+ // src/structure/ground-beams.ts
711
+ var GROUND_BEAM_DEPTH = 0.3;
712
+ function addGroundBeams(context) {
713
+ const { builder, construction } = context;
714
+ if (construction.system !== "masonry" || context.isUnit || construction.foundation.type !== "strip_footing") return;
715
+ const top = context.level(0);
716
+ for (const wall of context.walls) {
717
+ if (wall.floor !== 0 || wall.type !== "wall") continue;
718
+ 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" });
719
+ builder.rest(beam, GROUND, "rebar");
720
+ context.groundBeams.set(wall.id, beam);
721
+ }
722
+ }
723
+
724
+ // src/structure/wall-pillars.ts
725
+ var WALL_PILLAR_SPACING = 4;
726
+ var WALL_PILLAR_SIDE = 0.2;
727
+ var MERGE_DISTANCE = 0.45;
728
+ var HALF = WALL_PILLAR_SIDE / 2;
729
+ var TOLERANCE = 0.03;
730
+ 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)];
731
+ var acrossOf = (wall) => wall.axis === "u" ? wall.v0 : wall.u0;
732
+ function stretchesOf(context, wall) {
733
+ const [low, high] = alongRange(wall);
734
+ 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]);
735
+ const out = [];
736
+ let cursor = low;
737
+ for (const [from, to] of windows) {
738
+ if (from - cursor > TOLERANCE) out.push({ wall, from: cursor, to: from });
739
+ cursor = Math.max(cursor, to);
740
+ }
741
+ if (high - cursor > TOLERANCE) out.push({ wall, from: cursor, to: high });
742
+ return out;
743
+ }
744
+ function junctionsOf(context, wall) {
745
+ const [low, high] = alongRange(wall);
746
+ const line = acrossOf(wall);
747
+ const out = [];
748
+ for (const other of context.walls) {
749
+ if (other === wall || other.floor !== wall.floor || other.axis === wall.axis || other.type !== "wall") continue;
750
+ const at2 = acrossOf(other);
751
+ const [a, b] = alongRange(other);
752
+ const meets = Math.abs(a - line) <= (wall.thickness ?? 0.15) / 2 + TOLERANCE || Math.abs(b - line) <= (wall.thickness ?? 0.15) / 2 + TOLERANCE;
753
+ if (meets && at2 > low + HALF && at2 < high - HALF) out.push(at2);
754
+ }
755
+ return out;
756
+ }
757
+ function pillarStations(from, to, junctions, spacing) {
758
+ const length = to - from;
759
+ if (length < WALL_PILLAR_SIDE - 1e-6) return [];
760
+ if (length < 2 * WALL_PILLAR_SIDE) return [(from + to) / 2];
761
+ if (length < WALL_PILLAR_SIDE + MERGE_DISTANCE) return [from + HALF, to - HALF];
762
+ const fixed = [from + HALF, ...junctions.filter((at2) => at2 > from + HALF + MERGE_DISTANCE && at2 < to - HALF - MERGE_DISTANCE), to - HALF].sort((a, b) => a - b);
763
+ const out = [];
764
+ fixed.forEach((at2, k) => {
765
+ out.push(at2);
766
+ const next = fixed[k + 1];
767
+ if (next === void 0) return;
768
+ const spans = Math.ceil((next - at2) / spacing - 1e-6);
769
+ for (let s = 1; s < spans; s++) out.push(at2 + (next - at2) * s / spans);
770
+ });
771
+ return out;
772
+ }
773
+ function addWallPillars(context, walls, floor, plates, taken, spacing) {
774
+ const { builder } = context;
775
+ const pillars = [];
776
+ const placed = taken.map((p) => [(p.aabb[0] + p.aabb[2]) / 2, (p.aabb[1] + p.aabb[3]) / 2]);
777
+ const base = context.baseOf(floor);
778
+ for (const wall of walls) {
779
+ if (!wall.exterior || wall.type !== "wall") continue;
780
+ const top = context.wallParts.get(wall.id).y1;
781
+ const across = acrossOf(wall);
782
+ const junctions = junctionsOf(context, wall);
783
+ for (const stretch of stretchesOf(context, wall)) {
784
+ 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];
785
+ const first = standing(stretch.from);
786
+ const last = standing(stretch.to);
787
+ const from = first === void 0 ? stretch.from : Math.min(stretch.from, first - HALF);
788
+ const to = last === void 0 ? stretch.to : Math.max(stretch.to, last + HALF);
789
+ for (const along of pillarStations(from, to, junctions, spacing)) {
790
+ const [u, v] = wall.axis === "u" ? [along, across] : [across, along];
791
+ const end = Math.abs(along - stretch.from) < Math.abs(along - stretch.to) ? stretch.from : stretch.to;
792
+ const endPoint = wall.axis === "u" ? [end, across] : [across, end];
793
+ const isEnd = Math.abs(along - end) <= HALF + TOLERANCE;
794
+ 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;
795
+ placed.push([u, v]);
796
+ const box2 = [u - HALF, v - HALF, u + HALF, v + HALF];
797
+ const pillar = builder.add({ id: `wall_column_${floor}_${pillars.length}`, role: "column", material: context.construction.frame.pillars, aabb: box2, y0: context.level(floor), y1: top }, { floor });
798
+ builder.rest(pillar, base === GROUND || !base ? GROUND : base, "rebar");
799
+ for (const plate of plates.filter((p) => touches(p.aabb, box2, 0.01))) builder.rest(plate, pillar, "rebar");
800
+ pillars.push(pillar);
801
+ }
802
+ }
803
+ }
804
+ return pillars;
805
+ }
806
+
689
807
  // src/structure/plates.ts
690
808
  var MAX_GAP_WITHIN_RUN = 3.2;
691
809
  var POST_SPACING = 2.4;
692
810
  var POST_SIDE = 0.05;
693
811
  var alongOf = (wall, pick) => wall.axis === "u" ? pick(wall.u0, wall.u1) : pick(wall.v0, wall.v1);
694
- var acrossOf = (wall) => wall.axis === "u" ? wall.v0 : wall.u0;
812
+ var acrossOf2 = (wall) => wall.axis === "u" ? wall.v0 : wall.u0;
695
813
  function runsOf(context, walls, floor) {
696
814
  const lines = /* @__PURE__ */ new Map();
697
815
  for (const wall of walls) {
698
816
  if (!wall.exterior || wall.type !== "wall") continue;
699
817
  const top = round2(context.wallParts.get(wall.id).y1);
700
- const key = `${floor}|${wall.axis}|${acrossOf(wall).toFixed(2)}|${top.toFixed(2)}`;
818
+ const key = `${floor}|${wall.axis}|${acrossOf2(wall).toFixed(2)}|${top.toFixed(2)}`;
701
819
  lines.set(key, [...lines.get(key) ?? [], wall]);
702
820
  }
703
821
  return [...lines.values()].map((line) => {
@@ -717,7 +835,7 @@ var GameFloorplanStructure = (() => {
717
835
  }
718
836
  function addShackPosts(context, run, plate, floor, index) {
719
837
  const first = run.walls[0];
720
- const across = acrossOf(first);
838
+ const across = acrossOf2(first);
721
839
  const top = context.wallParts.get(first.id).y1;
722
840
  const count = Math.max(1, Math.ceil((run.to - run.from) / POST_SPACING));
723
841
  const posts = [];
@@ -743,7 +861,7 @@ var GameFloorplanStructure = (() => {
743
861
  for (const run of runs) {
744
862
  const first = run.walls[0];
745
863
  const half = (first.thickness ?? 0.15) / 2;
746
- const across = acrossOf(first);
864
+ const across = acrossOf2(first);
747
865
  const top = context.wallParts.get(first.id).y1;
748
866
  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];
749
867
  const material = context.isTimber ? construction.frame.timber : construction.frame.beams;
@@ -782,7 +900,11 @@ var GameFloorplanStructure = (() => {
782
900
  function addPlatesAndPillars(context, floor) {
783
901
  const walls = context.walls.filter((wall) => wall.floor === floor);
784
902
  const { plates, pillars } = addPlates(context, walls, floor);
785
- if (context.construction.system === "masonry") pillars.push(...addCornerPillars(context, walls, floor, plates));
903
+ if (context.construction.system === "masonry") {
904
+ const corners = addCornerPillars(context, walls, floor, plates);
905
+ pillars.push(...corners);
906
+ if (context.wallPillarSpacing) pillars.push(...addWallPillars(context, walls, floor, plates, corners, context.wallPillarSpacing));
907
+ }
786
908
  context.pillars.set(floor, pillars);
787
909
  context.plates.set(floor, plates);
788
910
  const innerBearing = walls.filter((wall) => wall.role === "wall" && !wall.exterior).map((wall) => context.wallParts.get(wall.id));
@@ -1067,21 +1189,21 @@ var GameFloorplanStructure = (() => {
1067
1189
  }
1068
1190
 
1069
1191
  // src/structure/openings.ts
1070
- var TOLERANCE = 0.02;
1192
+ var TOLERANCE2 = 0.02;
1071
1193
  var DOOR_HEIGHT = 2.1;
1072
1194
  var LINTEL_DEPTH = 0.2;
1073
1195
  var LINTEL_BEARING = 0.2;
1074
1196
  var crossOf = (wall) => wall.axis === "u" ? wall.v0 : wall.u0;
1075
- 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)];
1197
+ 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)];
1076
1198
  function findOpenings(context) {
1077
1199
  const openings = [];
1078
1200
  for (const window of context.source.windows ?? []) {
1079
1201
  const from = window.at - window.width / 2;
1080
1202
  const to = window.at + window.width / 2;
1081
1203
  const wall = context.walls.find((w) => {
1082
- if (w.floor !== window.floor || w.axis !== window.axis || w.type !== "wall" || Math.abs(crossOf(w) - window.pos) > TOLERANCE) return false;
1083
- const [low, high] = alongRange(w);
1084
- return from >= low - TOLERANCE && to <= high + TOLERANCE;
1204
+ if (w.floor !== window.floor || w.axis !== window.axis || w.type !== "wall" || Math.abs(crossOf(w) - window.pos) > TOLERANCE2) return false;
1205
+ const [low, high] = alongRange2(w);
1206
+ return from >= low - TOLERANCE2 && to <= high + TOLERANCE2;
1085
1207
  });
1086
1208
  if (!wall) continue;
1087
1209
  const base = context.level(window.floor);
@@ -1096,18 +1218,39 @@ var GameFloorplanStructure = (() => {
1096
1218
  const cross = door.axis === "u" ? door.v : door.u;
1097
1219
  const from = along - door.width / 2;
1098
1220
  const to = along + door.width / 2;
1099
- 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));
1221
+ 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));
1100
1222
  if (!beside.length) continue;
1101
1223
  const half = (beside[0].thickness ?? 0.15) / 2;
1102
1224
  const lo = from - LINTEL_BEARING;
1103
1225
  const hi = to + LINTEL_BEARING;
1104
1226
  const box2 = door.axis === "u" ? [lo, cross - half, hi, cross + half] : [cross - half, lo, cross + half, hi];
1105
1227
  const y0 = context.level(door.floor) + DOOR_HEIGHT;
1106
- const lintel = builder.add({ id: `lintel_${door.id}`, role: "beam", material: system.timber ? construction.frame.timber : "concrete", aabb: box2, y0, y1: y0 + LINTEL_DEPTH }, { floor: door.floor, ...system.timber ? {} : { assembly: "reinforced_concrete" } });
1228
+ const depth = Math.max(LINTEL_DEPTH, (door.width + LINTEL_BEARING) / 10);
1229
+ const lintel = builder.add({ id: `lintel_${door.id}`, role: "beam", material: system.timber ? construction.frame.timber : "concrete", aabb: box2, y0, y1: y0 + depth }, { floor: door.floor, ...system.timber ? {} : { assembly: "reinforced_concrete" } });
1107
1230
  for (const wall of beside) builder.rest(lintel, context.wallParts.get(wall.id), system.joints.beam);
1231
+ const jambs = (context.pillars.get(door.floor) ?? []).filter((pillar) => pillar.material === "concrete" && overlapsInPlan(pillar.aabb, box2) && pillar.y1 >= y0);
1232
+ if (construction.system === "masonry" && hi - lo > WIDE_DOOR) jambs.push(...addJambPillars(context, { door, beside, from, to, cross }, jambs));
1233
+ for (const pillar of jambs) builder.rest(lintel, pillar, "rebar");
1108
1234
  for (const plate of context.plates.get(door.floor) ?? []) if (overlapsInPlan(plate.aabb, box2)) builder.rest(plate, lintel, system.joints.beam);
1109
1235
  }
1110
1236
  }
1237
+ var WIDE_DOOR = 3;
1238
+ var HALF_PILLAR = WALL_PILLAR_SIDE / 2;
1239
+ function addJambPillars(context, gap, standing) {
1240
+ const { builder, construction } = context;
1241
+ const top = Math.max(...gap.beside.map((w) => context.wallParts.get(w.id).y1));
1242
+ const base = context.baseOf(gap.door.floor);
1243
+ const out = [];
1244
+ for (const [k, along] of [[0, gap.from - HALF_PILLAR], [1, gap.to + HALF_PILLAR]]) {
1245
+ const [u, v] = gap.door.axis === "u" ? [along, gap.cross] : [gap.cross, along];
1246
+ const box2 = [u - HALF_PILLAR, v - HALF_PILLAR, u + HALF_PILLAR, v + HALF_PILLAR];
1247
+ if (standing.some((p) => overlapsInPlan(p.aabb, box2))) continue;
1248
+ const pillar = builder.add({ id: `jamb_column_${gap.door.id}_${k}`, role: "column", material: construction.frame.pillars, aabb: box2, y0: context.level(gap.door.floor), y1: top }, { floor: gap.door.floor });
1249
+ builder.rest(pillar, base === GROUND || !base ? GROUND : base, "rebar");
1250
+ out.push(pillar);
1251
+ }
1252
+ return out;
1253
+ }
1111
1254
  var overlapsInPlan = (a, b) => a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1];
1112
1255
 
1113
1256
  // src/structure/steps.ts
@@ -1287,10 +1430,10 @@ var GameFloorplanStructure = (() => {
1287
1430
  const framed = system.studs !== void 0 && !isRailing;
1288
1431
  const part = builder.add(
1289
1432
  { id: wall.id, role: wall.role, material: wall.material, aabb: wall.box, y0, y1: Math.max(y1, y0 + 0.2) },
1290
- { floor, fill: context.isTimber && wall.role === "wall" ? system.wallFill : 1, ...framed ? { studs: system.studs } : {}, ...isRailing ? {} : { assembly: assemblyOf(context) } }
1433
+ { floor, fill: context.isTimber && wall.role === "wall" ? system.wallFill : 1, ...framed ? { studs: system.studs } : {}, ...isRailing ? {} : { assembly: assemblyOf(context) }, ...wall.exterior && !isRailing ? { exterior: true } : {} }
1291
1434
  );
1292
1435
  context.wallParts.set(wall.id, part);
1293
- const base = isRailing ? railingBase(context, wall, floor) : context.baseOf(floor);
1436
+ const base = isRailing ? railingBase(context, wall, floor) : context.groundBeams.get(wall.id) ?? context.baseOf(floor);
1294
1437
  const joint = isRailing ? "nail" : system.joints.wall;
1295
1438
  if (base === GROUND || floor === 0 && !base) builder.rest(part, GROUND, joint);
1296
1439
  else if (base) builder.rest(part, base, joint);
@@ -1308,6 +1451,11 @@ var GameFloorplanStructure = (() => {
1308
1451
  addUpperFloor(context, floor);
1309
1452
  }
1310
1453
  }
1454
+ var wallPillarSpacingOf = (options) => {
1455
+ const pillars = options.wallPillars ?? true;
1456
+ if (pillars === false) return 0;
1457
+ return pillars === true ? WALL_PILLAR_SPACING : pillars.spacing ?? WALL_PILLAR_SPACING;
1458
+ };
1311
1459
  function buildStructure(source, options = {}) {
1312
1460
  const refined = options.wallDetail !== "coarse";
1313
1461
  const built = options.construction ? { construction: options.construction, extras: options.extras ?? {} } : resolveConstruction(source);
@@ -1315,8 +1463,9 @@ var GameFloorplanStructure = (() => {
1315
1463
  const extras = options.extras ?? built.extras ?? {};
1316
1464
  if (construction.system === "canvas") return buildCanvasStructure(construction, extras);
1317
1465
  const tank = options.tank !== void 0 ? options.tank : source.waterTank ?? null;
1318
- const context = createContext(source, construction, extras, tank, { porchRoofOnPostsOnly: options.porchRoofOnPostsOnly ?? refined, rafterRoof: options.rafterRoof ?? refined });
1466
+ const context = createContext(source, construction, extras, tank, { porchRoofOnPostsOnly: options.porchRoofOnPostsOnly ?? refined, rafterRoof: options.rafterRoof ?? refined, wallPillarSpacing: wallPillarSpacingOf(options), visorSections: options.visorSections === true });
1319
1467
  addFoundation(context);
1468
+ if (options.groundBeams !== false) addGroundBeams(context);
1320
1469
  buildFloors(context);
1321
1470
  addBalconies(context);
1322
1471
  addPorchRoofs(context);
@@ -1765,13 +1914,13 @@ var GameFloorplanStructure = (() => {
1765
1914
  const segments = groundExteriorWalls(plan).sort((a, b) => (b.sides.includes("outside") ? 1 : 0) - (a.sides.includes("outside") ? 1 : 0) || (a.axis === "u" && a.v0 === 0 ? -1 : 1));
1766
1915
  const wall = segments.find((w) => w.axis === "u" && w.v0 === 0) || segments[0];
1767
1916
  if (!wall) return null;
1768
- const centre = wall.axis === "u" ? (wall.u0 + wall.u1) / 2 : (wall.v0 + wall.v1) / 2;
1917
+ const centre2 = wall.axis === "u" ? (wall.u0 + wall.u1) / 2 : (wall.v0 + wall.v1) / 2;
1769
1918
  const vector = outwardOf(wall);
1770
1919
  const offset = HALF_WALL + 0.1;
1771
1920
  return {
1772
1921
  id: "gas.meter",
1773
- u: round22(wall.axis === "u" ? centre : wall.u0 + vector[0] * offset),
1774
- v: round22(wall.axis === "u" ? wall.v0 + vector[1] * offset : centre),
1922
+ u: round22(wall.axis === "u" ? centre2 : wall.u0 + vector[0] * offset),
1923
+ v: round22(wall.axis === "u" ? wall.v0 + vector[1] * offset : centre2),
1775
1924
  y: 1.2,
1776
1925
  side: sideNameOf(vector),
1777
1926
  w: 0.3,
@@ -1962,9 +2111,9 @@ var GameFloorplanStructure = (() => {
1962
2111
  const wall = groundExteriorWalls(plan).filter((q) => q.sides.includes("outside")).sort((a, b) => (a.axis === "v" ? 0 : 1) - (b.axis === "v" ? 0 : 1))[0];
1963
2112
  if (!wall) return null;
1964
2113
  const vector = outwardOf(wall);
1965
- const centre = wall.axis === "u" ? (wall.u0 + wall.u1) / 2 : (wall.v0 + wall.v1) / 2;
2114
+ const centre2 = wall.axis === "u" ? (wall.u0 + wall.u1) / 2 : (wall.v0 + wall.v1) / 2;
1966
2115
  const offset = HALF_WALL + 0.08;
1967
- return { id: "inverter", u: round22(wall.axis === "u" ? centre : wall.u0 + vector[0] * offset), v: round22(wall.axis === "u" ? wall.v0 + vector[1] * offset : centre), y: 1.5, side: sideNameOf(vector), w: 0.4, d: 0.15, h: 0.5, outdoor: true, link: "panel" };
2116
+ return { id: "inverter", u: round22(wall.axis === "u" ? centre2 : wall.u0 + vector[0] * offset), v: round22(wall.axis === "u" ? wall.v0 + vector[1] * offset : centre2), y: 1.5, side: sideNameOf(vector), w: 0.4, d: 0.15, h: 0.5, outdoor: true, link: "panel" };
1968
2117
  }
1969
2118
  function planSolar(context) {
1970
2119
  const { plan, rng, energy } = context;
@@ -2172,6 +2321,8 @@ var GameFloorplanStructure = (() => {
2172
2321
 
2173
2322
  // src/water-tank.ts
2174
2323
  var TANK_SIDE = 1.1;
2324
+ var TANK_PILLAR_SIDE = 0.2;
2325
+ var VISOR_TANK_PILLAR_SIDE = 0.12;
2175
2326
  var NOT_UNDER_TANK = ["void", "elevator", "stairs", "balcony", "porch"];
2176
2327
  var WET_ROOMS = ["bathroom", "kitchen", "laundry"];
2177
2328
  var ATTIC_CHANCE = 0.12;
@@ -2181,26 +2332,27 @@ var GameFloorplanStructure = (() => {
2181
2332
  return capacities[source.style ?? ""] ?? capacities[source.template ?? ""] ?? 2e3;
2182
2333
  }
2183
2334
  function bestSpot(source, floor, reference) {
2184
- const centre = reference ? [reference.u, reference.v] : [source.w / 2, source.d / 2];
2335
+ const centre2 = reference ? [reference.u, reference.v] : [source.w / 2, source.d / 2];
2185
2336
  let best;
2186
2337
  for (const room of source.rooms.filter((r) => r.floor === floor && !NOT_UNDER_TANK.includes(r.type))) {
2187
2338
  for (const part of room.parts) {
2188
2339
  const rect = [part[0] + 0.4, part[1] + 0.4, part[2] - 0.4, part[3] - 0.4];
2189
2340
  if (rect[2] - rect[0] < TANK_SIDE || rect[3] - rect[1] < TANK_SIDE) continue;
2190
2341
  const base = WET_ROOMS.includes(room.type) ? 0 : room.type === "terrace" ? 500 : 100;
2191
- const weight = base + Math.hypot((rect[0] + rect[2]) / 2 - centre[0], (rect[1] + rect[3]) / 2 - centre[1]);
2342
+ const weight = base + Math.hypot((rect[0] + rect[2]) / 2 - centre2[0], (rect[1] + rect[3]) / 2 - centre2[1]);
2192
2343
  if (!best || weight < best.weight) best = { roomType: room.type, roomId: room.id, rect, weight };
2193
2344
  }
2194
2345
  }
2195
2346
  return best;
2196
2347
  }
2197
- function raisedPlatform(source, u, v, baseY) {
2348
+ function raisedPlatform(source, spot, side) {
2349
+ const { u, v, baseY } = spot;
2198
2350
  const margin = 0.12;
2199
2351
  const edge = Math.max(0, Math.min(u, source.w - u, v, source.d - v));
2200
2352
  const rise = round2(clamp2(edge * 0.35 + 0.7, 1.1, 2.4));
2201
2353
  const thickness = 0.12;
2202
- const reach = TANK_SIDE / 2 + margin - 0.06;
2203
- const pillars = [[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([a, b]) => ({ u: round2(u + a * reach), v: round2(v + b * reach), side: 0.12 }));
2354
+ const reach = TANK_SIDE / 2 + margin - side / 2;
2355
+ const pillars = [[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([a, b]) => ({ u: round2(u + a * reach), v: round2(v + b * reach), side }));
2204
2356
  const half = TANK_SIDE / 2 + margin;
2205
2357
  return { aabb: [u - half, v - half, u + half, v + half].map(round2), baseY, height: rise, thickness, topY: round2(baseY + rise + thickness), pillars, ladder: true };
2206
2358
  }
@@ -2237,7 +2389,7 @@ var GameFloorplanStructure = (() => {
2237
2389
  aabb: [u - TANK_SIDE / 2, v - TANK_SIDE / 2, u + TANK_SIDE / 2, v + TANK_SIDE / 2].map(round2)
2238
2390
  };
2239
2391
  if (where === "raised_slab") {
2240
- tank.platform = raisedPlatform(source, u, v, slabY);
2392
+ tank.platform = raisedPlatform(source, { u, v, baseY: slabY }, options.pillarSide ?? TANK_PILLAR_SIDE);
2241
2393
  tank.y = tank.platform.topY;
2242
2394
  }
2243
2395
  return tank;
@@ -2342,7 +2494,7 @@ var GameFloorplanStructure = (() => {
2342
2494
  }
2343
2495
  function structureExtension(context) {
2344
2496
  const cache = cacheOf(context.result);
2345
- const options = { construction: cache.build.construction, extras: cache.build.extras ?? {}, tank: tankOf(context), wallDetail: context.options.wallDetail ?? "panels" };
2497
+ const options = { construction: cache.build.construction, extras: cache.build.extras ?? {}, tank: tankOf(context), wallDetail: context.options.wallDetail ?? "panels", ...context.options.wallPillars !== void 0 ? { wallPillars: context.options.wallPillars } : {}, ...context.options.groundBeams !== void 0 ? { groundBeams: context.options.groundBeams === true } : {}, ...context.options.visorSections === true ? { visorSections: true } : {} };
2346
2498
  const { roof, ...structure } = buildStructure(context.result, options);
2347
2499
  cache.roof = roof;
2348
2500
  for (const part of structure.parts) {
@@ -2366,7 +2518,7 @@ var GameFloorplanStructure = (() => {
2366
2518
  engine.registerResultExtension(
2367
2519
  "waterTank",
2368
2520
  (context) => {
2369
- const tank = { capacities: options.tankCapacities, custom: context.plan.tpl.tank, location: context.plan.tpl.tankLocation };
2521
+ const tank = { capacities: options.tankCapacities, custom: context.plan.tpl.tank, location: context.plan.tpl.tankLocation, ...context.options.visorSections === true ? { pillarSide: VISOR_TANK_PILLAR_SIDE } : {} };
2370
2522
  return planWaterTank(context.result, context.rng, tank);
2371
2523
  },
2372
2524
  { after: ["construction"], seedKey: "caixa" }
@@ -2388,6 +2540,7 @@ var GameFloorplanStructure = (() => {
2388
2540
  }
2389
2541
 
2390
2542
  // ../game-contracts/dist/index.js
2543
+ var DEFAULT_BLAST_IMPULSE = { bodyArea: 0.7, lift: 0.15, minSpeed: 0.3, tntPerJoule: 0.24 / 2e5 };
2391
2544
  var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
2392
2545
  var DEFAULT_IGNITION_RULES = {
2393
2546
  fillCurve: [
@@ -2810,6 +2963,22 @@ var GameFloorplanStructure = (() => {
2810
2963
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
2811
2964
  };
2812
2965
  var clamp3 = (v, a, b) => Math.max(a, Math.min(b, v));
2966
+ var splitAlong = (box2, count, axis, rnd) => {
2967
+ const shares = Array.from({ length: count }, () => 0.25 + 1.5 * rnd());
2968
+ const total = shares.reduce((sum, share) => sum + share, 0);
2969
+ const out = [];
2970
+ let from = box2.c[axis] - box2.s[axis] / 2;
2971
+ for (const share of shares) {
2972
+ const length = box2.s[axis] * share / total;
2973
+ const c = [box2.c[0], box2.c[1], box2.c[2]];
2974
+ const size = [box2.s[0], box2.s[1], box2.s[2]];
2975
+ c[axis] = from + length / 2;
2976
+ size[axis] = length;
2977
+ out.push({ c, s: size });
2978
+ from += length;
2979
+ }
2980
+ return out;
2981
+ };
2813
2982
  var splinterBox = (inp, box2, limit = inp.maxFragments) => {
2814
2983
  const { rnd } = inp;
2815
2984
  const dims = box2.s;
@@ -2819,35 +2988,81 @@ var GameFloorplanStructure = (() => {
2819
2988
  const w = Math.max(fmin * 1.5, 0.01);
2820
2989
  const c = clamp3(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
2821
2990
  const maxLen = fmax * 4;
2822
- const cells = bsp(box2, c, [a, b], rnd);
2991
+ const cells = [];
2992
+ for (const cell of bsp(box2, c, [a, b], rnd)) {
2993
+ const wide = cell.s[a] >= cell.s[b] ? a : b;
2994
+ if (rnd() < 0.4 && cell.s[wide] > fmin * 0.5) for (const part of splitAlong(cell, 2, wide, rnd)) cells.push(part);
2995
+ else cells.push(cell);
2996
+ }
2823
2997
  const pieces = [];
2824
2998
  for (const cell of cells) {
2825
- const extra = rnd() < 0.4 ? 1 : 0;
2999
+ const extra = Math.floor(rnd() * 3);
2826
3000
  const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
2827
- for (const p of bsp(cell, k, [g], rnd)) pieces.push(p);
3001
+ for (const p of splitAlong(cell, k, g, rnd)) pieces.push(p);
2828
3002
  }
2829
3003
  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] }));
2830
3004
  };
3005
+ var PLY = 3e-3;
3006
+ var PLY_GAP = 0.6;
3007
+ var PLY_SHEET = 0.012;
3008
+ var plyBreak = (inp, root, limit) => {
3009
+ const { rnd } = inp;
3010
+ const dims = root.s;
3011
+ const sorted = [0, 1, 2].sort((x, y) => dims[x] - dims[y]);
3012
+ const t = sorted[0];
3013
+ const [u, v] = [sorted[1], sorted[2]].sort((x, y) => x - y);
3014
+ const along = dims[u] >= dims[v] ? u : v;
3015
+ const across = along === u ? v : u;
3016
+ const layers = clamp3(Math.round(dims[t] / PLY / 2), 2, 3);
3017
+ const pitch = dims[t] / layers;
3018
+ const th = Math.min(pitch, PLY_SHEET);
3019
+ const per = Math.max(2, Math.floor(limit / layers));
3020
+ const out = [];
3021
+ for (let k = 0; k < layers; k++) {
3022
+ const layer = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
3023
+ layer.c[t] = root.c[t] + (-dims[t] / 2 + (k + 0.5) * pitch) * (pitch > PLY_SHEET ? 1 : 1 + PLY_GAP);
3024
+ layer.s[t] = th;
3025
+ for (const cell of bsp(layer, per, [u, v], rnd)) {
3026
+ if (out.length >= limit) return out;
3027
+ if (rnd() < 0.35) {
3028
+ const strand = cell.s[across] * (0.15 + 0.2 * rnd());
3029
+ 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 });
3030
+ } else {
3031
+ 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 });
3032
+ }
3033
+ }
3034
+ }
3035
+ return out;
3036
+ };
3037
+ var granules = (inp, box2, count) => bsp(box2, 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] }));
3038
+ var CRUMBS = 6;
2831
3039
  var severAt = (inp, root, at2, gap) => {
2832
3040
  const dims = root.s;
2833
3041
  const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
2834
3042
  const half = dims[L] / 2;
2835
3043
  const lo = clamp3(at2 - gap / 2, -half, half), hi = clamp3(at2 + gap / 2, -half, half);
2836
3044
  const out = [];
2837
- const stub = (from, to) => {
3045
+ const fibrous = inp.form === "slivers" || inp.form === "plies";
3046
+ const [a, b] = [0, 1, 2].filter((i) => i !== L);
3047
+ const stub = (from, to, breakAtEnd) => {
2838
3048
  if (to - from < 0.02) return;
2839
3049
  const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
2840
3050
  c[L] = (from + to) / 2;
2841
3051
  sz[L] = to - from;
2842
- out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
3052
+ const weight = sz[0] * sz[1] * sz[2];
3053
+ 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 });
3054
+ else out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight });
2843
3055
  };
2844
- stub(-half, lo);
2845
- stub(hi, half);
3056
+ stub(-half, lo, true);
3057
+ stub(hi, half, false);
2846
3058
  const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
2847
3059
  band.c[L] = (lo + hi) / 2;
2848
3060
  band.s[L] = Math.max(hi - lo, 0.01);
2849
- if (inp.style === "snap") {
2850
- 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] });
3061
+ const room = inp.maxFragments - out.length;
3062
+ if (inp.style === "snap" && inp.form === "plies") for (const p of plyBreak(inp, band, Math.max(3, room))) out.push(p);
3063
+ else if (inp.style === "snap" && inp.form === "granules") for (const p of granules(inp, band, clamp3(room, 3, 12))) out.push(p);
3064
+ else if (inp.style === "snap") {
3065
+ 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] });
2851
3066
  } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
2852
3067
  return out;
2853
3068
  };
@@ -2880,8 +3095,18 @@ var GameFloorplanStructure = (() => {
2880
3095
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
2881
3096
  if (style === "snap") {
2882
3097
  if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
3098
+ if (inp.form === "plies") return plyBreak(inp, root, maxFragments);
2883
3099
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
2884
3100
  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] });
3101
+ if (inp.form !== "granules") return out;
3102
+ const crumbs = Math.min(CRUMBS, maxFragments - out.length);
3103
+ if (crumbs <= 0) return out;
3104
+ const band = { c: [0, 0, 0], s: [0, 0, 0] };
3105
+ for (let i = 0; i < 3; i++) {
3106
+ band.s[i] = Math.min(dims[i], Math.max(fmin, dims[i] * 0.2));
3107
+ band.c[i] = clamp3(inp.hit[i], -dims[i] / 2 + band.s[i] / 2, dims[i] / 2 - band.s[i] / 2);
3108
+ }
3109
+ for (const p of granules(inp, band, crumbs)) out.push({ ...p, weight: p.weight * 0.5 });
2885
3110
  return out;
2886
3111
  }
2887
3112
  if (style === "crumble" || style === "none") {
@@ -2988,6 +3213,88 @@ var GameFloorplanStructure = (() => {
2988
3213
  }
2989
3214
  return marks;
2990
3215
  };
3216
+ var JOINT_GAP = 1e-4;
3217
+ var MIN_BEARING_SHARE = 0.25;
3218
+ var unsupportedCells = (cells, thin, dims, standing) => {
3219
+ if (thin === 1) return [];
3220
+ const [u, v] = planeAxes(thin);
3221
+ const bottom = -dims[v] / 2;
3222
+ const rowOf = (y) => Math.round((y - bottom) * 1e4);
3223
+ const rows = /* @__PURE__ */ new Map();
3224
+ for (let i = 0; i < cells.length; i++) {
3225
+ if (!standing(i)) continue;
3226
+ const cell = cells[i];
3227
+ const key = rowOf(cell.center[v] - cell.size[v] / 2);
3228
+ const row = rows.get(key);
3229
+ if (row) row.push(i);
3230
+ else rows.set(key, [i]);
3231
+ }
3232
+ const held = /* @__PURE__ */ new Set();
3233
+ const queue = [];
3234
+ for (const i of rows.get(0) ?? []) {
3235
+ held.add(i);
3236
+ queue.push(i);
3237
+ }
3238
+ 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);
3239
+ const climb = () => {
3240
+ while (queue.length > 0) {
3241
+ const cell = cells[queue.pop()];
3242
+ for (const j of rows.get(rowOf(cell.center[v] + cell.size[v] / 2)) ?? []) {
3243
+ const other = cells[j];
3244
+ if (held.has(j) || overlap(cell, other) <= Math.max(JOINT_GAP, MIN_BEARING_SHARE * Math.min(cell.size[u], other.size[u]))) continue;
3245
+ held.add(j);
3246
+ queue.push(j);
3247
+ }
3248
+ }
3249
+ };
3250
+ for (const row of rows.values()) row.sort((a, b) => cells[a].center[u] - cells[b].center[u]);
3251
+ const touching = (a, b) => Math.abs(a.center[u] + a.size[u] / 2 - (b.center[u] - b.size[u] / 2)) <= 1e-3;
3252
+ for (let wedged = true; wedged; ) {
3253
+ climb();
3254
+ wedged = false;
3255
+ for (const row of rows.values()) {
3256
+ for (let k = 1; k < row.length - 1; k++) {
3257
+ const i = row[k];
3258
+ if (held.has(i) || !held.has(row[k - 1]) || !held.has(row[k + 1])) continue;
3259
+ if (!touching(cells[row[k - 1]], cells[i]) || !touching(cells[i], cells[row[k + 1]])) continue;
3260
+ held.add(i);
3261
+ queue.push(i);
3262
+ wedged = true;
3263
+ }
3264
+ }
3265
+ }
3266
+ const loose = [];
3267
+ for (const row of rows.values()) for (const i of row) if (!held.has(i)) loose.push(i);
3268
+ return loose;
3269
+ };
3270
+ var clumpCells = (cells, thin, cap) => {
3271
+ if (cells.length <= cap) return [...cells];
3272
+ const [u, v] = planeAxes(thin);
3273
+ let lo0 = Infinity, lo1 = Infinity, hi0 = -Infinity, hi1 = -Infinity;
3274
+ for (const cell of cells) {
3275
+ lo0 = Math.min(lo0, cell.center[u] - cell.size[u] / 2);
3276
+ hi0 = Math.max(hi0, cell.center[u] + cell.size[u] / 2);
3277
+ lo1 = Math.min(lo1, cell.center[v] - cell.size[v] / 2);
3278
+ hi1 = Math.max(hi1, cell.center[v] + cell.size[v] / 2);
3279
+ }
3280
+ let side = Math.sqrt((hi0 - lo0) * (hi1 - lo1) / cap);
3281
+ for (; ; ) {
3282
+ const bins = /* @__PURE__ */ new Map();
3283
+ for (const cell of cells) {
3284
+ const key = `${Math.floor((cell.center[u] - lo0) / side)}:${Math.floor((cell.center[v] - lo1) / side)}`;
3285
+ const bin = bins.get(key) ?? { lo: [Infinity, Infinity, Infinity], hi: [-Infinity, -Infinity, -Infinity] };
3286
+ for (let k = 0; k < 3; k++) {
3287
+ bin.lo[k] = Math.min(bin.lo[k], cell.center[k] - cell.size[k] / 2);
3288
+ bin.hi[k] = Math.max(bin.hi[k], cell.center[k] + cell.size[k] / 2);
3289
+ }
3290
+ bins.set(key, bin);
3291
+ }
3292
+ if (bins.size <= cap) {
3293
+ 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]] }));
3294
+ }
3295
+ side *= 1.25;
3296
+ }
3297
+ };
2991
3298
  var MAX_SPALLS = 64;
2992
3299
  var thinAxisOf = (dims) => dims[0] <= dims[1] && dims[0] <= dims[2] ? 0 : dims[1] <= dims[2] ? 1 : 2;
2993
3300
  var resolveLayer = (def, index, mat) => ({ index, def, mat, energy: mat.penetration * def.thickness * 1e3 });
@@ -3659,7 +3966,7 @@ var GameFloorplanStructure = (() => {
3659
3966
  eau[j] += e * area * g[i].u * w;
3660
3967
  }
3661
3968
  }
3662
- const centre = ea.map((v, j) => v > 0 ? eau[j] / v : 0);
3969
+ const centre2 = ea.map((v, j) => v > 0 ? eau[j] / v : 0);
3663
3970
  const ei = new Array(n).fill(0);
3664
3971
  const ga = new Array(n).fill(0);
3665
3972
  for (const i of vertical) {
@@ -3668,7 +3975,7 @@ var GameFloorplanStructure = (() => {
3668
3975
  for (let j = 0; j < n; j++) {
3669
3976
  const w = share(p, j) / bands[j];
3670
3977
  if (w <= 0) continue;
3671
- ei[j] += e * (g[i].du ** 3 * g[i].dv / 12 + g[i].du * g[i].dv * (g[i].u - centre[j]) ** 2) * w;
3978
+ ei[j] += e * (g[i].du ** 3 * g[i].dv / 12 + g[i].du * g[i].dv * (g[i].u - centre2[j]) ** 2) * w;
3672
3979
  }
3673
3980
  }
3674
3981
  for (let i = 0; i < parts.length; i++) {
@@ -3715,7 +4022,7 @@ var GameFloorplanStructure = (() => {
3715
4022
  for (let j = 0; j < n; j++) {
3716
4023
  const w = share(p, j);
3717
4024
  if (w <= 0) continue;
3718
- sum += w * p.elasticPa * c.stiffnessFactor * kappa[j] * g[i].du * g[i].dv * (g[i].u - centre[j]);
4025
+ sum += w * p.elasticPa * c.stiffnessFactor * kappa[j] * g[i].du * g[i].dv * (g[i].u - centre2[j]);
3719
4026
  tot += w;
3720
4027
  }
3721
4028
  axial[i] = tot > 0 ? sum / tot : 0;
@@ -3967,6 +4274,7 @@ var GameFloorplanStructure = (() => {
3967
4274
  },
3968
4275
  {
3969
4276
  id: "wood_soft",
4277
+ splitForm: "slivers",
3970
4278
  density: 500,
3971
4279
  hpPerKg: 90,
3972
4280
  penetration: 9,
@@ -4012,6 +4320,7 @@ var GameFloorplanStructure = (() => {
4012
4320
  },
4013
4321
  {
4014
4322
  id: "wood_hard",
4323
+ splitForm: "slivers",
4015
4324
  density: 750,
4016
4325
  hpPerKg: 140,
4017
4326
  penetration: 14,
@@ -4056,6 +4365,7 @@ var GameFloorplanStructure = (() => {
4056
4365
  },
4057
4366
  {
4058
4367
  id: "plywood",
4368
+ splitForm: "plies",
4059
4369
  density: 600,
4060
4370
  hpPerKg: 70,
4061
4371
  penetration: 7,
@@ -4103,6 +4413,7 @@ var GameFloorplanStructure = (() => {
4103
4413
  },
4104
4414
  {
4105
4415
  id: "mdf",
4416
+ splitForm: "granules",
4106
4417
  density: 750,
4107
4418
  hpPerKg: 45,
4108
4419
  penetration: 6,
@@ -4614,6 +4925,7 @@ var GameFloorplanStructure = (() => {
4614
4925
  },
4615
4926
  {
4616
4927
  id: "green_wood",
4928
+ splitForm: "slivers",
4617
4929
  density: 650,
4618
4930
  hpPerKg: 5,
4619
4931
  penetration: 10,
@@ -4894,6 +5206,125 @@ var GameFloorplanStructure = (() => {
4894
5206
  "brittle"
4895
5207
  ],
4896
5208
  compressive: 1e5
5209
+ },
5210
+ {
5211
+ id: "mud",
5212
+ density: 1700,
5213
+ hpPerKg: 1,
5214
+ penetration: 5,
5215
+ ductility: 0.8,
5216
+ fracture: "crumble",
5217
+ fragmentSize: [
5218
+ 0.02,
5219
+ 0.08
5220
+ ],
5221
+ hitDebris: "dust",
5222
+ breakDebris: "dust",
5223
+ thermal: {
5224
+ conductivity: 0.5,
5225
+ heatCapacity: 3.2
5226
+ },
5227
+ tags: [
5228
+ "soil",
5229
+ "wet"
5230
+ ],
5231
+ compressive: 5e3
5232
+ },
5233
+ {
5234
+ id: "clay",
5235
+ density: 1800,
5236
+ hpPerKg: 5,
5237
+ penetration: 7,
5238
+ ductility: 0.05,
5239
+ fracture: "crumble",
5240
+ fragmentSize: [
5241
+ 0.02,
5242
+ 0.1
5243
+ ],
5244
+ hitDebris: "chips",
5245
+ breakDebris: "dust",
5246
+ thermal: {
5247
+ conductivity: 0.15,
5248
+ heatCapacity: 2.2
5249
+ },
5250
+ tags: [
5251
+ "soil",
5252
+ "brittle"
5253
+ ],
5254
+ compressive: 6e4
5255
+ },
5256
+ {
5257
+ id: "snow",
5258
+ density: 300,
5259
+ hpPerKg: 1,
5260
+ penetration: 3,
5261
+ ductility: 0.6,
5262
+ fracture: "crumble",
5263
+ fragmentSize: [
5264
+ 0.01,
5265
+ 0.05
5266
+ ],
5267
+ hitDebris: "dust",
5268
+ breakDebris: "dust",
5269
+ thermal: {
5270
+ conductivity: 0.1,
5271
+ heatCapacity: 2.1
5272
+ },
5273
+ tags: [
5274
+ "soil",
5275
+ "frozen"
5276
+ ],
5277
+ compressive: 1e3
5278
+ },
5279
+ {
5280
+ id: "cobble",
5281
+ density: 2650,
5282
+ hpPerKg: 25,
5283
+ penetration: 25,
5284
+ penetrationClass: "concrete",
5285
+ penetrationScale: 0.9,
5286
+ ductility: 0.03,
5287
+ fracture: "crumble",
5288
+ fragmentSize: [
5289
+ 0.05,
5290
+ 0.15
5291
+ ],
5292
+ hitDebris: "chips",
5293
+ breakDebris: "chips",
5294
+ thermal: {
5295
+ conductivity: 0.2,
5296
+ heatCapacity: 2.5
5297
+ },
5298
+ tags: [
5299
+ "masonry",
5300
+ "brittle"
5301
+ ],
5302
+ compressive: 3e5
5303
+ },
5304
+ {
5305
+ id: "paver",
5306
+ density: 2300,
5307
+ hpPerKg: 14,
5308
+ penetration: 18,
5309
+ penetrationClass: "concrete",
5310
+ penetrationScale: 1,
5311
+ ductility: 0.03,
5312
+ fracture: "crumble",
5313
+ fragmentSize: [
5314
+ 0.04,
5315
+ 0.12
5316
+ ],
5317
+ hitDebris: "chips",
5318
+ breakDebris: "chips",
5319
+ thermal: {
5320
+ conductivity: 0.15,
5321
+ heatCapacity: 2.5
5322
+ },
5323
+ tags: [
5324
+ "masonry",
5325
+ "brittle"
5326
+ ],
5327
+ compressive: 35e4
4897
5328
  }
4898
5329
  ];
4899
5330
  var material_hints_default = {
@@ -5106,6 +5537,11 @@ var GameFloorplanStructure = (() => {
5106
5537
  var BREACH_BLAST_FACTOR = 6;
5107
5538
  var BREACH_AREA = 0.25;
5108
5539
  var BREACH_BLAST_REACH = 0.12;
5540
+ var MEMBER_CUT_REACH = 0.5;
5541
+ var BLAST_REFERENCE_RESISTANCE = 200 * 1900 * 0.13;
5542
+ var BLAST_EDGE_RAGGED = 0.3;
5543
+ var BLAST_EDGE_LOOSE = 1.35;
5544
+ var BLAST_EDGE_WEAR = 0.6;
5109
5545
  var RESIDUAL_CAPACITY = 0.15;
5110
5546
  var SEVER_BAND = 0.1;
5111
5547
  var SEVER_ASPECT = 3;
@@ -6358,7 +6794,7 @@ var GameFloorplanStructure = (() => {
6358
6794
  if (j.st.open) continue;
6359
6795
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
6360
6796
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
6361
- j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
6797
+ j.st.hp -= span === void 0 ? amount : Math.min(amount, j.st.maxHp) * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
6362
6798
  if (j.st.hp <= 0) {
6363
6799
  j.st.hp = 0;
6364
6800
  j.st.open = true;
@@ -6486,6 +6922,7 @@ var GameFloorplanStructure = (() => {
6486
6922
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
6487
6923
  const cut = (_b = p.rt).craters ?? (_b.craters = {});
6488
6924
  const out = [];
6925
+ const wholeBefore = Object.values(worn).filter((w) => w === -1).length;
6489
6926
  const shot = kind === "ballistic";
6490
6927
  for (let i = 0; i < grid.length; i++) {
6491
6928
  const cell = grid[i];
@@ -6544,6 +6981,7 @@ var GameFloorplanStructure = (() => {
6544
6981
  emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
6545
6982
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
6546
6983
  }
6984
+ if (Object.values(worn).filter((w) => w === -1).length > wholeBefore) dropUnsupported(o, p, src);
6547
6985
  if (p.spec.reinforcement) exposeRebar(o, p, src);
6548
6986
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
6549
6987
  if (p.structural && !isLost(p)) {
@@ -6647,10 +7085,122 @@ var GameFloorplanStructure = (() => {
6647
7085
  setStage(o, p, 1, {}, src);
6648
7086
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
6649
7087
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
7088
+ dropUnsupported(o, p, src);
6650
7089
  const reinforcement = p.spec.reinforcement;
6651
7090
  if (reinforcement) exposeRebar(o, p, src);
6652
7091
  if (p.structural) solveStructure(o);
6653
7092
  };
7093
+ const blastReachOf = (p, energy) => {
7094
+ const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
7095
+ const resistance = Math.max(1, perKg * p.mat.density * p.thickness);
7096
+ return BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3) * clamp22(Math.cbrt(BLAST_REFERENCE_RESISTANCE / resistance), 0.3, 2);
7097
+ };
7098
+ const blastUnits = (o, p, wp, energy, dist, src) => {
7099
+ var _a, _b;
7100
+ if (!p.staged || p.st.stripped || isLost(p) || chipEnergy <= 0) return false;
7101
+ const reach = blastReachOf(p, energy);
7102
+ if (dist >= reach) return false;
7103
+ const radius = Math.sqrt(reach * reach - dist * dist);
7104
+ const [u, v] = planeAxes(p.thin);
7105
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
7106
+ const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
7107
+ const grid = gridOf(p);
7108
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
7109
+ const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
7110
+ const lobes = [rnd() * 2 * Math.PI, rnd() * 2 * Math.PI];
7111
+ const out = [];
7112
+ for (let i = 0; i < grid.length; i++) {
7113
+ const cell = grid[i];
7114
+ if (worn[i] === -1) continue;
7115
+ if (op && p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius) continue;
7116
+ const du = cell.center[u] - lp[u], dv = cell.center[v] - lp[v];
7117
+ const angle = Math.atan2(dv, du);
7118
+ 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)));
7119
+ const d = Math.hypot(du, dv);
7120
+ if (d <= edge) {
7121
+ worn[i] = -1;
7122
+ out.push(i);
7123
+ } else if (d <= edge * BLAST_EDGE_LOOSE) {
7124
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
7125
+ worn[i] = Math.max(worn[i] ?? 0, perKg * mass * BLAST_EDGE_WEAR);
7126
+ }
7127
+ }
7128
+ if (out.length === 0) return false;
7129
+ 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));
7130
+ for (const i of loose) worn[i] = -1;
7131
+ const blown = out.map((i) => grid[i]), fallen = loose.map((i) => grid[i]);
7132
+ const volume = (cells2) => cells2.reduce((sum, cell) => sum + cell.size[0] * cell.size[1] * cell.size[2], 0);
7133
+ const blownMass = Math.min(p.st.mass, p.mat.density * volume(blown));
7134
+ const fallenMass = Math.min(p.st.mass - blownMass, p.mat.density * volume(fallen));
7135
+ const speed = clamp22(Math.sqrt(2 * energy * 0.02 / Math.max(blownMass, 0.05)), 1, 12);
7136
+ const { c, q } = partXf(o, p);
7137
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
7138
+ const tag = `x${chipped.length}_`;
7139
+ const fragments = [
7140
+ ...cellFragments(o, p, clumpCells(blown, p.thin, maxFragments), wp, null, speed, blownMass, tag, true),
7141
+ ...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) : []
7142
+ ];
7143
+ const cells = [...blown, ...fallen];
7144
+ const holes = localHolesOf(p);
7145
+ p.st.holes = p.st.holes.filter((_, k) => !cells.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
7146
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
7147
+ for (const cell of cells) {
7148
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
7149
+ for (let k = 0; k < 3; k++) {
7150
+ lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
7151
+ hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
7152
+ }
7153
+ }
7154
+ p.st.mass = Math.max(0, p.st.mass - blownMass - fallenMass);
7155
+ p.rt.removed = (p.rt.removed ?? 0) + blownMass + fallenMass;
7156
+ markDamaged(p);
7157
+ setStage(o, p, 1, {}, src);
7158
+ const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
7159
+ const units = cells.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
7160
+ 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 });
7161
+ debris(o, p, p.mat.breakDebris, center, vec.normalize(vec.sub(center, wp), [0, 1, 0]), 14, 3, src);
7162
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
7163
+ if (p.structural && !isLost(p)) {
7164
+ solveStructure(o);
7165
+ collapseCheck(o, src);
7166
+ }
7167
+ return true;
7168
+ };
7169
+ const dropUnsupported = (o, p, src) => {
7170
+ var _a, _b;
7171
+ if (!p.spec.units || p.spec.units.mortar === p.spec.material || p.st.stripped || isLost(p)) return 0;
7172
+ const [u, v] = planeAxes(p.thin);
7173
+ const grid = gridOf(p);
7174
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
7175
+ const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
7176
+ const opened = (cell) => !!op && !!p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius;
7177
+ const loose = unsupportedCells(grid, p.thin, p.dims, (i) => worn[i] !== -1 && !opened(grid[i]));
7178
+ if (loose.length === 0) return 0;
7179
+ for (const i of loose) worn[i] = -1;
7180
+ const fallen = loose.map((i) => grid[i]);
7181
+ 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));
7182
+ const { c, q } = partXf(o, p);
7183
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
7184
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
7185
+ for (const cell of fallen) {
7186
+ for (let k = 0; k < 3; k++) {
7187
+ lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
7188
+ hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
7189
+ }
7190
+ }
7191
+ const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
7192
+ 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);
7193
+ const holes = localHolesOf(p);
7194
+ p.st.holes = p.st.holes.filter((_, k) => !fallen.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
7195
+ 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]] });
7196
+ p.st.mass = Math.max(0, p.st.mass - mass);
7197
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
7198
+ markDamaged(p);
7199
+ const units = fallen.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
7200
+ 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 });
7201
+ debris(o, p, p.mat.breakDebris, center, [0, -1, 0], 6, 1, src);
7202
+ return loose.length;
7203
+ };
6654
7204
  const stripPart = (o, p, reinforcement, src) => {
6655
7205
  const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
6656
7206
  p.st.stripped = true;
@@ -6717,6 +7267,7 @@ var GameFloorplanStructure = (() => {
6717
7267
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
6718
7268
  const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
6719
7269
  const cutAt2 = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
7270
+ const planRnd = p.mat.splitForm ? splitStream(o.id, p.spec.id, hitLocal) : rnd;
6720
7271
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
6721
7272
  style,
6722
7273
  dims: p.dims,
@@ -6725,8 +7276,9 @@ var GameFloorplanStructure = (() => {
6725
7276
  hit: hitLocal,
6726
7277
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
6727
7278
  maxFragments,
6728
- rnd,
7279
+ rnd: planRnd,
6729
7280
  sheet: p.mat.sheet,
7281
+ ...p.mat.splitForm ? { form: p.mat.splitForm } : {},
6730
7282
  ...cutAt2 !== void 0 ? { cut: { at: cutAt2, gap: SEVER_BAND } } : {}
6731
7283
  });
6732
7284
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
@@ -6771,7 +7323,7 @@ var GameFloorplanStructure = (() => {
6771
7323
  return;
6772
7324
  }
6773
7325
  if (ex) {
6774
- emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, ...ex.effect !== void 0 ? { effect: ex.effect } : {} });
7326
+ emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, ...ex.tnt !== void 0 ? { tnt: ex.tnt } : {}, ...ex.effect !== void 0 ? { effect: ex.effect } : {} });
6775
7327
  blasts.push({ point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, source: src });
6776
7328
  }
6777
7329
  partLost(o, p, src);
@@ -6838,11 +7390,11 @@ var GameFloorplanStructure = (() => {
6838
7390
  o.active = true;
6839
7391
  }
6840
7392
  };
6841
- const lightLeaksNear = (centre, radius) => {
7393
+ const lightLeaksNear = (centre2, radius) => {
6842
7394
  for (const o of objects.values()) {
6843
7395
  for (const q of o.parts) {
6844
7396
  if (isLost(q) || q.rt.lit || q.rt.jet || !leaksFlammable(q)) continue;
6845
- if (vec.distance(partXf(o, q).c, centre) - q.bound > radius) continue;
7397
+ if (vec.distance(partXf(o, q).c, centre2) - q.bound > radius) continue;
6846
7398
  q.rt.lit = true;
6847
7399
  o.active = true;
6848
7400
  }
@@ -6865,7 +7417,8 @@ var GameFloorplanStructure = (() => {
6865
7417
  const fragments = [];
6866
7418
  for (let i = 0; i < n; i++) {
6867
7419
  const back = !exit || i % 3 === 2;
6868
- 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());
7420
+ const plies = p.mat.splitForm === "plies";
7421
+ 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());
6869
7422
  const size = [w, w, w];
6870
7423
  size[along] = len;
6871
7424
  size[(along + 1) % 3] = w;
@@ -7026,7 +7579,7 @@ var GameFloorplanStructure = (() => {
7026
7579
  checkIgnite(o, p, src);
7027
7580
  }
7028
7581
  }
7029
- wearJoints(o, p, Ein * 0.5, void 0, 0, src);
7582
+ wearJoints(o, p, Ein * 0.5, void 0, 0, src, false, 2 * BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3));
7030
7583
  const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
7031
7584
  if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
7032
7585
  frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
@@ -7036,7 +7589,10 @@ var GameFloorplanStructure = (() => {
7036
7589
  const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
7037
7590
  res.damage += dealt;
7038
7591
  if (p.bend && flexure.enabled) bendByBlast(o, p, point, energy, radius, reach, src);
7039
- if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
7592
+ const breachReach = BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3);
7593
+ if (p.spec.units && p.spec.units.mortar !== p.spec.material) blastUnits(o, p, wp, energy, dist, src);
7594
+ else if (p.spec.reinforcement && p.spec.units && dist > MEMBER_CUT_REACH * breachReach) blastUnits(o, p, wp, energy, dist, src);
7595
+ else if (dist <= breachReach) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
7040
7596
  return reach;
7041
7597
  };
7042
7598
  const blastProbe = { partsTested: 0, partsNear: 0 };
@@ -7282,12 +7838,17 @@ var GameFloorplanStructure = (() => {
7282
7838
  }
7283
7839
  return h | 0;
7284
7840
  };
7285
- const throwFragment = (frag, objectId, from, g, centre, index, src, shields, results, reports, mult) => {
7841
+ const splitStream = (objectId, partId, hit) => {
7842
+ const codes = Array.from(`${objectId}|${partId}`, (char) => char.charCodeAt(0) / 1e3);
7843
+ const stream = createRng(seedOf(rng.state() / 1e3, ...codes, hit[0], hit[1], hit[2]));
7844
+ return () => stream.next();
7845
+ };
7846
+ const throwFragment = (frag, objectId, from, g, centre2, index, src, shields, results, reports, mult) => {
7286
7847
  const v = frag.velocity;
7287
7848
  const speed = Math.hypot(v[0], v[1], v[2]);
7288
7849
  const energy = g.energyFactor * 0.5 * frag.mass * speed * speed;
7289
7850
  if (speed < 0.5 || energy < 1) return;
7290
- const r = createRng(seedOf(options.seed ?? 1, centre[0], centre[1], centre[2], index, frag.center[0], frag.center[1], frag.center[2]));
7851
+ const r = createRng(seedOf(options.seed ?? 1, centre2[0], centre2[1], centre2[2], index, frag.center[0], frag.center[1], frag.center[2]));
7291
7852
  let dir = vec.scale(v, 1 / speed);
7292
7853
  const behind = from ? shields.get(from) : void 0;
7293
7854
  if (behind) {
@@ -9161,6 +9722,8 @@ var GameFloorplanStructure = (() => {
9161
9722
  // src/wall-detail/panels.ts
9162
9723
  var LINTEL_DEPTH2 = 0.2;
9163
9724
  var LINTEL_BEARING2 = 0.2;
9725
+ var SILL_BEAM_DEPTH = 0.1;
9726
+ var LINTEL_SPAN_RATIO = 10;
9164
9727
  var MIN_PIECE = 0.05;
9165
9728
  var frameOf = (wall) => {
9166
9729
  const alongU = wall.aabb[2] - wall.aabb[0] >= wall.aabb[3] - wall.aabb[1];
@@ -9174,6 +9737,7 @@ var GameFloorplanStructure = (() => {
9174
9737
  const drafts = [];
9175
9738
  const piers = [];
9176
9739
  const lintels = [];
9740
+ const sillBeams = [];
9177
9741
  const spandrels = [];
9178
9742
  const height = wall.y1 - wall.y0;
9179
9743
  let cursor = frame2.from;
@@ -9184,23 +9748,26 @@ var GameFloorplanStructure = (() => {
9184
9748
  };
9185
9749
  own.forEach((opening2, k) => {
9186
9750
  addPier(Math.max(cursor, opening2.from));
9187
- const lintelTop = Math.min(wall.y1, opening2.y1 + Math.max(LINTEL_DEPTH2, style.lintelDepth?.(opening2.to - opening2.from) ?? 0));
9751
+ const span = opening2.to - opening2.from;
9752
+ const lintelTop = Math.min(wall.y1, opening2.y1 + Math.max(LINTEL_DEPTH2, style.lintelDepth?.(span) ?? 0, (span + LINTEL_BEARING2) / LINTEL_SPAN_RATIO));
9188
9753
  const lintelFrom = Math.max(frame2.from, opening2.from - LINTEL_BEARING2);
9189
9754
  const lintelTo = Math.min(frame2.to, opening2.to + LINTEL_BEARING2);
9190
9755
  const lintelHeight = lintelTop - opening2.y1;
9191
- 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) });
9756
+ const sillTop = style.sillBeam && opening2.y0 - wall.y0 > SILL_BEAM_DEPTH + MIN_PIECE ? opening2.y0 - SILL_BEAM_DEPTH : opening2.y0;
9757
+ 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) });
9758
+ 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 });
9192
9759
  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 });
9193
9760
  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);
9194
9761
  cursor = opening2.to;
9195
9762
  });
9196
9763
  addPier(frame2.to);
9197
9764
  for (const pier of piers) {
9198
- for (const lintel of lintels) {
9765
+ for (const lintel of [...lintels, ...sillBeams]) {
9199
9766
  const overlap = Math.max(0, Math.min(pier.to, lintel.to) - Math.max(pier.from, lintel.from));
9200
9767
  pier.volume -= overlap * (lintel.y1 - lintel.y0);
9201
9768
  }
9202
9769
  }
9203
- const all = [...piers, ...drafts, ...lintels, ...spandrels.filter((d) => d !== void 0)];
9770
+ const all = [...piers, ...drafts, ...sillBeams, ...lintels, ...spandrels.filter((d) => d !== void 0)];
9204
9771
  const total = all.reduce((sum, d) => sum + d.volume, 0);
9205
9772
  const pieces = all.map((d) => ({
9206
9773
  id: d.id,
@@ -9215,6 +9782,11 @@ var GameFloorplanStructure = (() => {
9215
9782
  ...d.role === "beam" ? style.lintelAssembly ? { assembly: style.lintelAssembly } : {} : wall.assembly ? { assembly: wall.assembly } : {}
9216
9783
  }));
9217
9784
  const internal = [];
9785
+ for (const beam of sillBeams) {
9786
+ const sill = drafts.find((d) => d.id === beam.id.replace("_sillbeam_", "_sill_"));
9787
+ if (sill) internal.push({ part: beam.id, on: sill.id, joint: style.lintelJoint });
9788
+ 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 });
9789
+ }
9218
9790
  lintels.forEach((lintel, k) => {
9219
9791
  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 });
9220
9792
  const spandrel = spandrels[k];
@@ -9246,7 +9818,8 @@ var GameFloorplanStructure = (() => {
9246
9818
  ...system.timber ? {} : { lintelAssembly: "reinforced_concrete" },
9247
9819
  lintelJoint: system.joints.beam,
9248
9820
  wallJoint: system.joints.wall,
9249
- ...lintelDepth && !system.timber ? { lintelDepth } : {}
9821
+ ...lintelDepth && !system.timber ? { lintelDepth } : {},
9822
+ ...construction.system === "masonry" ? { sillBeam: true } : {}
9250
9823
  });
9251
9824
  if (split.pieces.length > 1) splits.set(part.id, split);
9252
9825
  }
@@ -9254,7 +9827,18 @@ var GameFloorplanStructure = (() => {
9254
9827
  }
9255
9828
  function applyPanels(input, structure, detail, lintelDepth) {
9256
9829
  if (detail === "coarse") return input;
9257
- return applySplits(input, panelSplits(input, structure, lintelDepth));
9830
+ return onJambPillars(applySplits(input, panelSplits(input, structure, lintelDepth)));
9831
+ }
9832
+ var OPENING_BEAM = /_(lintel|sillbeam)_\d+$/;
9833
+ var overlaps2 = (a, b) => a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1];
9834
+ function onJambPillars(split) {
9835
+ const pillars = split.parts.filter((part) => part.role === "column" && part.material === "concrete");
9836
+ const extra = [];
9837
+ for (const beam of split.parts) {
9838
+ if (beam.role !== "beam" || !OPENING_BEAM.test(beam.id)) continue;
9839
+ 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" });
9840
+ }
9841
+ return extra.length ? { parts: split.parts, supports: [...split.supports, ...extra] } : split;
9258
9842
  }
9259
9843
  function applyUnits(input, structure) {
9260
9844
  const splits = /* @__PURE__ */ new Map();
@@ -9318,21 +9902,35 @@ var GameFloorplanStructure = (() => {
9318
9902
  };
9319
9903
  var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
9320
9904
  var CAGED_ROLES = /* @__PURE__ */ new Set(["beam", "column"]);
9905
+ var CAGE_COVER = 0.03;
9906
+ var CAGE_BUCKLING_SHARE = 0.5;
9907
+ var DEFAULT_RESIDUAL = 0.15;
9908
+ function cageResidualCapacity(size, reinforcement, capacity) {
9909
+ const [a = 0, b = 0] = [...size].sort((x, y) => x - y);
9910
+ const perSide = (w) => Math.max(2, Math.floor(Math.max(0, w - 2 * CAGE_COVER) / reinforcement.spacing) + 1);
9911
+ const bars = 2 * perSide(a) + 2 * perSide(b) - 4;
9912
+ const steel = MATERIALS.find((m) => m.id === reinforcement.material)?.compressive ?? 0;
9913
+ const carries2 = bars * Math.PI * (reinforcement.diameter / 2) ** 2 * steel * CAGE_BUCKLING_SHARE;
9914
+ if (!capacity || capacity <= 0 || carries2 <= 0) return DEFAULT_RESIDUAL;
9915
+ return Math.min(1, Math.max(DEFAULT_RESIDUAL, carries2 / capacity));
9916
+ }
9321
9917
  var caged = (spec, part, rebar) => {
9322
9918
  if (!rebar || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
9323
9919
  const size = spec.shape.kind === "box" ? spec.shape.size : null;
9324
9920
  if (!size) return spec;
9325
9921
  const thin = Math.min(...size);
9922
+ const reinforcement = { material: rebar, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter };
9326
9923
  return {
9327
9924
  ...spec,
9328
- reinforcement: { material: rebar, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter },
9925
+ reinforcement: part.role === "column" ? { ...reinforcement, residualCapacity: cageResidualCapacity(size, reinforcement, spec.structure?.capacity) } : reinforcement,
9329
9926
  units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
9330
9927
  };
9331
9928
  };
9332
9929
  var partSpecOf = (part, capacity, detailed, rebar) => {
9333
9930
  if (detailed && part.assembly) return assemblySpec(part, capacity);
9334
9931
  const spec = boxSpec(part, capacity);
9335
- return detailed ? caged(spec, part, rebar) : spec;
9932
+ const out = detailed ? caged(spec, part, rebar) : spec;
9933
+ return part.beamKind === "ground_beam" ? { ...out, anchored: true, structure: { role: "foundation" } } : out;
9336
9934
  };
9337
9935
  function toDestructibleSpec(structure, options = {}) {
9338
9936
  const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
@@ -9348,7 +9946,9 @@ var GameFloorplanStructure = (() => {
9348
9946
  const walled = applyPanels(slabbed, structure, detail, slabOptions && structure.construction.frame.rebar ? (span) => beamDepthFor(span, slabOptions) : void 0);
9349
9947
  const panels = slabOptions && detail !== "coarse" ? deepenBondBeams(walled, structure, slabOptions) : walled;
9350
9948
  const sizing = { safety, redundancy, safetyOthers };
9351
- const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts, panels.supports, sizing) };
9949
+ const foundation = new Set(panels.parts.filter((part) => part.beamKind === "ground_beam").map((part) => part.id));
9950
+ const onFoundation = foundation.size ? panels.supports.filter((s) => !foundation.has(s.part)).map((s) => foundation.has(s.on) ? { ...s, on: GROUND } : s) : panels.supports;
9951
+ const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts.filter((part) => !foundation.has(part.id)), onFoundation, sizing) };
9352
9952
  const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
9353
9953
  const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
9354
9954
  const margin = options.terrainMargin ?? TERRAIN_MARGIN;
@@ -9414,6 +10014,359 @@ var GameFloorplanStructure = (() => {
9414
10014
  return out;
9415
10015
  }
9416
10016
 
10017
+ // src/validate/codes.ts
10018
+ var STRUCTURAL_VIOLATION = {
10019
+ /** A pillar with no load path to the ground, or that starts above what it rests on. */
10020
+ PILLAR_NOT_GROUNDED: "PILLAR_NOT_GROUNDED",
10021
+ /** A pillar of a masonry wall that stops short of the bond beam, or that the bond beam does not rest on. */
10022
+ PILLAR_DOES_NOT_REACH_BOND_BEAM: "PILLAR_DOES_NOT_REACH_BOND_BEAM",
10023
+ /** A masonry wall that runs more than `MAX_PILLAR_SPACING` between two of its pillars. */
10024
+ WALL_SPAN_WITHOUT_PILLAR: "WALL_SPAN_WITHOUT_PILLAR",
10025
+ /** A free end of a masonry wall (a door jamb, the end of a garden wall) with no pillar. */
10026
+ WALL_END_WITHOUT_PILLAR: "WALL_END_WITHOUT_PILLAR",
10027
+ /** Two masonry walls meet at a corner with no pillar there. */
10028
+ CORNER_WITHOUT_PILLAR: "CORNER_WITHOUT_PILLAR",
10029
+ /** Another wall meets a masonry wall inside its length (a T) with no pillar there. */
10030
+ WALL_JUNCTION_WITHOUT_PILLAR: "WALL_JUNCTION_WITHOUT_PILLAR",
10031
+ /** A window or door in a masonry wall with no lintel (verga) over it. */
10032
+ OPENING_WITHOUT_LINTEL: "OPENING_WITHOUT_LINTEL",
10033
+ /** A window in a masonry wall with no sill beam (contraverga) under it. */
10034
+ OPENING_WITHOUT_SILL: "OPENING_WITHOUT_SILL",
10035
+ /** An exterior masonry wall with no bond beam (cinta de amarração) on top. */
10036
+ MISSING_BOND_BEAM: "MISSING_BOND_BEAM",
10037
+ /** A ground-floor masonry wall standing on the soil with no ground beam (viga baldrame) or foundation under it. */
10038
+ MISSING_GROUND_BEAM: "MISSING_GROUND_BEAM",
10039
+ /** A part with no load path down to the ground. */
10040
+ FLOATING_PART: "FLOATING_PART",
10041
+ /** A concrete beam longer than a lintel that rests on masonry alone (no pillar, beam or slab carries it). */
10042
+ BEAM_ON_WEAK_MASONRY: "BEAM_ON_WEAK_MASONRY",
10043
+ /** A concrete pillar or beam below the NBR 6118 minimum section, or a beam between point supports shallower than a tenth of its span. */
10044
+ CONCRETE_BELOW_MIN_SECTION: "CONCRETE_BELOW_MIN_SECTION",
10045
+ /** A concrete pillar or beam with no steel (the construction has no rebar). */
10046
+ UNREINFORCED_CONCRETE: "UNREINFORCED_CONCRETE",
10047
+ /** A brick or block wall laid in stack bond: the vertical joints line up and the wall splits along them. */
10048
+ UNBONDED_MASONRY: "UNBONDED_MASONRY",
10049
+ /** A plastered brick wall without plaster on both faces. */
10050
+ MISSING_PLASTER: "MISSING_PLASTER"
10051
+ };
10052
+ var MAX_PILLAR_SPACING = 4;
10053
+ var MIN_PILLAR_SIDE = 0.19;
10054
+ var MIN_PILLAR_AREA = 0.036;
10055
+ var MIN_BEAM_WIDTH = 0.12;
10056
+ var BEAM_DEPTH_RATIO = 10;
10057
+ var MAX_LINTEL_ON_MASONRY = 3;
10058
+
10059
+ // src/validate/load-path.ts
10060
+ var CONTACT = 0.05;
10061
+ var EPSILON = 5e-3;
10062
+ function groundedParts(parts, supports) {
10063
+ const ids = new Set(parts.map((part) => part.id));
10064
+ const grounded = /* @__PURE__ */ new Set([GROUND]);
10065
+ const bearing = supports.filter((s) => s.bears !== false && ids.has(s.part));
10066
+ let grew = true;
10067
+ while (grew) {
10068
+ grew = false;
10069
+ for (const support of bearing) {
10070
+ if (grounded.has(support.part) || !grounded.has(support.on)) continue;
10071
+ grounded.add(support.part);
10072
+ grew = true;
10073
+ }
10074
+ }
10075
+ return grounded;
10076
+ }
10077
+ function loadPathViolations(structure) {
10078
+ const { parts, supports } = structure;
10079
+ const grounded = groundedParts(parts, supports);
10080
+ const byId = new Map(parts.map((part) => [part.id, part]));
10081
+ const out = [];
10082
+ for (const part of parts) {
10083
+ if (part.role === "none") continue;
10084
+ const pillar = part.role === "column";
10085
+ if (!grounded.has(part.id)) {
10086
+ out.push({ code: pillar ? STRUCTURAL_VIOLATION.PILLAR_NOT_GROUNDED : STRUCTURAL_VIOLATION.FLOATING_PART, part: part.id, message: `${part.id} has no load path to the ground` });
10087
+ continue;
10088
+ }
10089
+ if (!pillar) continue;
10090
+ const bases = supports.filter((s) => s.part === part.id && s.bears !== false);
10091
+ const standsOnBase = bases.some((s) => {
10092
+ if (s.on === GROUND) return part.y0 <= CONTACT;
10093
+ const base = byId.get(s.on);
10094
+ return base !== void 0 && part.y0 >= base.y0 - EPSILON && part.y0 <= base.y1 + CONTACT;
10095
+ });
10096
+ if (!standsOnBase) out.push({ code: STRUCTURAL_VIOLATION.PILLAR_NOT_GROUNDED, part: part.id, message: `${part.id} starts at ${part.y0} m, above what it rests on` });
10097
+ }
10098
+ return out;
10099
+ }
10100
+ var alongOf2 = (part) => {
10101
+ const alongU = part.aabb[2] - part.aabb[0] >= part.aabb[3] - part.aabb[1];
10102
+ return alongU ? { alongU, from: part.aabb[0], to: part.aabb[2], width: part.aabb[3] - part.aabb[1] } : { alongU, from: part.aabb[1], to: part.aabb[3], width: part.aabb[2] - part.aabb[0] };
10103
+ };
10104
+ function bearingsOf(beam, supports, byId) {
10105
+ const frame2 = alongOf2(beam);
10106
+ return supports.filter((s) => s.part === beam.id && s.bears !== false && s.on !== GROUND).map((s) => byId.get(s.on)).filter((on) => on !== void 0).map((on) => {
10107
+ const [low, high] = frame2.alongU ? [on.aabb[0], on.aabb[2]] : [on.aabb[1], on.aabb[3]];
10108
+ return { on, from: Math.max(frame2.from, low), to: Math.min(frame2.to, high) };
10109
+ }).filter((bearing) => bearing.to - bearing.from > -EPSILON);
10110
+ }
10111
+ var isWallLike = (part) => part.role === "wall" || part.role === "partition";
10112
+ var PEDESTAL_RATIO = 3;
10113
+ var isPedestal = (part) => part.role === "column" && part.y1 - part.y0 <= PEDESTAL_RATIO * Math.min(part.aabb[2] - part.aabb[0], part.aabb[3] - part.aabb[1]);
10114
+ function concreteViolations(structure, isMasonry) {
10115
+ const { parts, supports } = structure;
10116
+ const byId = new Map(parts.map((part) => [part.id, part]));
10117
+ const out = [];
10118
+ for (const part of parts) {
10119
+ if (part.material !== "concrete" || part.role !== "column" && part.role !== "beam") continue;
10120
+ if (!structure.construction.frame.rebar && !isPedestal(part)) out.push({ code: STRUCTURAL_VIOLATION.UNREINFORCED_CONCRETE, part: part.id, message: `${part.id} is concrete without steel` });
10121
+ if (part.role === "column") {
10122
+ const sides = [part.aabb[2] - part.aabb[0], part.aabb[3] - part.aabb[1]];
10123
+ if (Math.min(...sides) < MIN_PILLAR_SIDE - EPSILON || sides[0] * sides[1] < MIN_PILLAR_AREA - EPSILON)
10124
+ out.push({ code: STRUCTURAL_VIOLATION.CONCRETE_BELOW_MIN_SECTION, part: part.id, message: `${part.id} is ${sides.map((s) => s.toFixed(2)).join(" x ")} m: NBR 6118 asks at least ${MIN_PILLAR_SIDE} m a side and ${MIN_PILLAR_AREA} m\xB2` });
10125
+ continue;
10126
+ }
10127
+ const castOn = supports.filter((s) => s.on === part.id && s.bears !== false).map((s) => byId.get(s.part)).filter((top) => top !== void 0 && top.role === "beam" && top.material === "concrete" && Math.abs(top.y0 - part.y1) < 0.01).reduce((most, top) => Math.max(most, top.y1 - top.y0), 0);
10128
+ out.push(...beamViolations(part, bearingsOf(part, supports, byId), { isMasonry, castOn }));
10129
+ }
10130
+ return out;
10131
+ }
10132
+ function effectiveSpan(bearings, depth) {
10133
+ const sorted = [...bearings].sort((a, b) => a.from - b.from);
10134
+ let most = 0;
10135
+ for (let k = 1; k < sorted.length; k++) {
10136
+ const previous = sorted.slice(0, k).reduce((a, b) => b.to > a.to ? b : a);
10137
+ const next = sorted[k];
10138
+ const clear = next.from - previous.to;
10139
+ if (clear <= 0) continue;
10140
+ most = Math.max(most, clear + Math.min((previous.to - previous.from) / 2, 0.3 * depth) + Math.min((next.to - next.from) / 2, 0.3 * depth));
10141
+ }
10142
+ return most;
10143
+ }
10144
+ function beamViolations(beam, bearings, context) {
10145
+ const out = [];
10146
+ const frame2 = alongOf2(beam);
10147
+ const length = frame2.to - frame2.from;
10148
+ const depth = beam.y1 - beam.y0 + context.castOn;
10149
+ if (frame2.width < MIN_BEAM_WIDTH - EPSILON) out.push({ code: STRUCTURAL_VIOLATION.CONCRETE_BELOW_MIN_SECTION, part: beam.id, message: `${beam.id} is ${frame2.width.toFixed(2)} m wide: NBR 6118 asks at least ${MIN_BEAM_WIDTH} m` });
10150
+ if (!bearings.length) return out;
10151
+ const continuous = bearings.some((b) => isWallLike(b.on) && b.to - b.from >= length / 2);
10152
+ const span = continuous ? 0 : effectiveSpan(bearings, depth);
10153
+ if (depth < span / BEAM_DEPTH_RATIO - EPSILON) out.push({ code: STRUCTURAL_VIOLATION.CONCRETE_BELOW_MIN_SECTION, part: beam.id, message: `${beam.id} is ${depth.toFixed(2)} m deep over a ${span.toFixed(2)} m span: it needs at least L/${BEAM_DEPTH_RATIO} = ${(span / BEAM_DEPTH_RATIO).toFixed(2)} m` });
10154
+ if (length > MAX_LINTEL_ON_MASONRY && bearings.every((b) => context.isMasonry(b.on)))
10155
+ out.push({ code: STRUCTURAL_VIOLATION.BEAM_ON_WEAK_MASONRY, part: beam.id, message: `${beam.id} spans ${length.toFixed(2)} m on masonry alone: no pillar carries it` });
10156
+ return out;
10157
+ }
10158
+
10159
+ // src/validate/masonry.ts
10160
+ var TOLERANCE3 = 0.05;
10161
+ var AT_POINT = 0.45;
10162
+ var EXPOSED_FINISHES = ["exposed_brick", "exposed_block"];
10163
+ var lineOf2 = (part) => {
10164
+ const alongU = part.aabb[2] - part.aabb[0] >= part.aabb[3] - part.aabb[1];
10165
+ const half = (alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0]) / 2;
10166
+ const [from, to] = alongU ? [part.aabb[0], part.aabb[2]] : [part.aabb[1], part.aabb[3]];
10167
+ return { part, alongU, line: alongU ? part.aabb[1] + half : part.aabb[0] + half, from: from + half, to: to - half, half };
10168
+ };
10169
+ function isMasonryWall(part) {
10170
+ if (!part.id.startsWith("wall_") || !part.assembly || part.role !== "wall" && part.role !== "partition") return false;
10171
+ const units = getAssembly(part.assembly)?.units;
10172
+ return units !== void 0 && units.mortar !== "concrete";
10173
+ }
10174
+ var pointOf2 = (wall, along) => wall.alongU ? [along, wall.line] : [wall.line, along];
10175
+ var alongOfPoint = (wall, point) => wall.alongU ? point[0] : point[1];
10176
+ var acrossOfPoint = (wall, point) => wall.alongU ? point[1] : point[0];
10177
+ var centre = (part) => [(part.aabb[0] + part.aabb[2]) / 2, (part.aabb[1] + part.aabb[3]) / 2];
10178
+ var onLine = (wall, point, slack = 0) => Math.abs(acrossOfPoint(wall, point) - wall.line) <= wall.half + TOLERANCE3 && alongOfPoint(wall, point) >= wall.from - wall.half - slack && alongOfPoint(wall, point) <= wall.to + wall.half + slack;
10179
+ function endKind(wall, along, others) {
10180
+ const point = pointOf2(wall, along);
10181
+ const meeting = others.filter((other) => onLine(other, point));
10182
+ const across = meeting.filter((other) => other.alongU !== wall.alongU);
10183
+ const continued = meeting.length > across.length;
10184
+ if (!across.length) return continued ? "continued" : "free";
10185
+ const atEnd = (other) => Math.min(Math.abs(alongOfPoint(other, point) - other.from), Math.abs(alongOfPoint(other, point) - other.to)) <= other.half + wall.half + TOLERANCE3;
10186
+ return !continued && across.every(atEnd) ? "corner" : "junction";
10187
+ }
10188
+ var masonryBetween = (from, to, windows) => windows.reduce((left, [a, b]) => left - Math.max(0, Math.min(to, b) - Math.max(from, a)), to - from);
10189
+ function pointViolations(context, others) {
10190
+ const { wall, stations } = context;
10191
+ const out = [];
10192
+ const covered = (along) => stations.some((at2) => Math.abs(at2 - along) <= AT_POINT);
10193
+ for (const along of [wall.from, wall.to]) {
10194
+ const kind = endKind(wall, along, others);
10195
+ if (kind === "continued" || covered(along)) continue;
10196
+ const code = kind === "corner" ? STRUCTURAL_VIOLATION.CORNER_WITHOUT_PILLAR : kind === "junction" ? STRUCTURAL_VIOLATION.WALL_JUNCTION_WITHOUT_PILLAR : STRUCTURAL_VIOLATION.WALL_END_WITHOUT_PILLAR;
10197
+ out.push({ code, part: wall.part.id, message: `${wall.part.id} has no pillar at its ${kind} end (${along.toFixed(2)} m along)` });
10198
+ }
10199
+ for (const other of others) {
10200
+ if (other.alongU === wall.alongU) continue;
10201
+ for (const end of [other.from, other.to]) {
10202
+ const point = pointOf2(other, end);
10203
+ const at2 = alongOfPoint(wall, point);
10204
+ if (!onLine(wall, point) || at2 <= wall.from + AT_POINT || at2 >= wall.to - AT_POINT || covered(at2)) continue;
10205
+ out.push({ code: STRUCTURAL_VIOLATION.WALL_JUNCTION_WITHOUT_PILLAR, part: wall.part.id, message: `${other.part.id} meets ${wall.part.id} at ${at2.toFixed(2)} m along with no pillar there` });
10206
+ }
10207
+ }
10208
+ const line = [...stations].sort((a, b) => a - b);
10209
+ for (let k = 1; k < line.length; k++) {
10210
+ const gap = masonryBetween(line[k - 1], line[k], context.windows);
10211
+ if (gap > MAX_PILLAR_SPACING + TOLERANCE3) out.push({ code: STRUCTURAL_VIOLATION.WALL_SPAN_WITHOUT_PILLAR, part: wall.part.id, message: `${wall.part.id} runs ${gap.toFixed(2)} m between pillars (at most ${MAX_PILLAR_SPACING} m)` });
10212
+ }
10213
+ if (!line.length && wall.to - wall.from > MAX_PILLAR_SPACING) out.push({ code: STRUCTURAL_VIOLATION.WALL_SPAN_WITHOUT_PILLAR, part: wall.part.id, message: `${wall.part.id} runs ${(wall.to - wall.from).toFixed(2)} m with no pillar` });
10214
+ return out;
10215
+ }
10216
+ function bondBeamViolations(context, structure) {
10217
+ const { wall, pillars } = context;
10218
+ const resting = new Set(structure.supports.filter((s) => s.on === wall.part.id).map((s) => s.part));
10219
+ const beams = structure.parts.filter((p) => p.beamKind === "bond_beam" && resting.has(p.id));
10220
+ if (!beams.length) return [{ code: STRUCTURAL_VIOLATION.MISSING_BOND_BEAM, part: wall.part.id, message: `${wall.part.id} has no bond beam on top` }];
10221
+ const out = [];
10222
+ for (const pillar of pillars) {
10223
+ const [u, v] = centre(pillar);
10224
+ const over = beams.filter((b) => u >= b.aabb[0] - TOLERANCE3 && u <= b.aabb[2] + TOLERANCE3 && v >= b.aabb[1] - TOLERANCE3 && v <= b.aabb[3] + TOLERANCE3);
10225
+ const carries2 = over.some((b) => pillar.y1 >= b.y0 - TOLERANCE3 && structure.supports.some((s) => s.part === b.id && s.on === pillar.id && s.bears !== false));
10226
+ if (!carries2) out.push({ code: STRUCTURAL_VIOLATION.PILLAR_DOES_NOT_REACH_BOND_BEAM, part: pillar.id, message: `${pillar.id} stops at ${pillar.y1} m and does not carry the bond beam of ${wall.part.id}` });
10227
+ }
10228
+ return out;
10229
+ }
10230
+ function assemblyViolations(part, finish) {
10231
+ const def = getAssembly(part.assembly ?? "");
10232
+ if (!def) return [];
10233
+ const out = [];
10234
+ if (def.units && def.units.bond !== "running") out.push({ code: STRUCTURAL_VIOLATION.UNBONDED_MASONRY, part: part.id, message: `${part.id} is laid in ${def.units.bond} bond: the joints must be staggered` });
10235
+ const plastered = (def.layers ?? []).some((layer) => layer.material === "plaster" && layer.faces === "both");
10236
+ if (def.core.material === "brick" && !plastered && !EXPOSED_FINISHES.includes(finish ?? "")) out.push({ code: STRUCTURAL_VIOLATION.MISSING_PLASTER, part: part.id, message: `${part.id} is brick without plaster on both faces` });
10237
+ return out;
10238
+ }
10239
+ function masonryViolations(structure) {
10240
+ const walls = structure.parts.filter(isMasonryWall);
10241
+ const columns = structure.parts.filter((p) => p.role === "column" && p.material === "concrete");
10242
+ const out = [];
10243
+ for (const part of walls) {
10244
+ out.push(...assemblyViolations(part, structure.construction.walls.finish));
10245
+ if (part.floor === 0 && structure.supports.some((s) => s.part === part.id && s.on === GROUND))
10246
+ out.push({ code: STRUCTURAL_VIOLATION.MISSING_GROUND_BEAM, part: part.id, message: `${part.id} stands on the soil with no ground beam under it` });
10247
+ if (!part.exterior) continue;
10248
+ const wall = lineOf2(part);
10249
+ const pillars = columns.filter((c) => c.floor === part.floor && onLine(wall, centre(c), AT_POINT) && Math.min(c.y1, part.y1) - Math.max(c.y0, part.y0) > 0.5);
10250
+ const windows = structure.openings.filter((o) => o.wall === part.id).map((o) => [o.from, o.to]);
10251
+ const context = { wall, pillars, stations: pillars.map((p) => alongOfPoint(wall, centre(p))), windows };
10252
+ const others = walls.filter((w) => w !== part && w.floor === part.floor).map(lineOf2);
10253
+ out.push(...pointViolations(context, others), ...bondBeamViolations(context, structure));
10254
+ }
10255
+ return out;
10256
+ }
10257
+
10258
+ // src/validate/openings.ts
10259
+ var TOLERANCE4 = 0.05;
10260
+ var LOW_WINDOW = SILL_BEAM_DEPTH + 0.05;
10261
+ function beamAcross(parts, wall, span, side, y) {
10262
+ return parts.some((part) => {
10263
+ const off = part[side] - y;
10264
+ if (part.role !== "beam" || off < -TOLERANCE4 || off > (side === "y0" ? LINTEL_DEPTH2 : TOLERANCE4)) return false;
10265
+ const [from, to, low, high] = wall.alongU ? [part.aabb[0], part.aabb[2], part.aabb[1], part.aabb[3]] : [part.aabb[1], part.aabb[3], part.aabb[0], part.aabb[2]];
10266
+ return from <= span.from + TOLERANCE4 && to >= span.to - TOLERANCE4 && low <= wall.line + TOLERANCE4 && high >= wall.line - TOLERANCE4;
10267
+ });
10268
+ }
10269
+ function windowViolations(opening2, wall, built) {
10270
+ const line = lineOf2(wall);
10271
+ const out = [];
10272
+ if (!beamAcross(built, line, opening2, "y0", opening2.y1)) out.push({ code: STRUCTURAL_VIOLATION.OPENING_WITHOUT_LINTEL, part: opening2.id, message: `window ${opening2.id} in ${wall.id} has no lintel over it` });
10273
+ if (opening2.y0 - wall.y0 > LOW_WINDOW && !beamAcross(built, line, opening2, "y1", opening2.y0))
10274
+ out.push({ code: STRUCTURAL_VIOLATION.OPENING_WITHOUT_SILL, part: opening2.id, message: `window ${opening2.id} in ${wall.id} has no sill beam under it` });
10275
+ return out;
10276
+ }
10277
+ function wallsBeside(door, walls) {
10278
+ const along = door.axis === "u" ? door.u : door.v;
10279
+ const across = door.axis === "u" ? door.v : door.u;
10280
+ const from = along - door.width / 2;
10281
+ const to = along + door.width / 2;
10282
+ return walls.filter((w) => w.floor === door.floor).map(lineOf2).filter((w) => w.alongU === (door.axis === "u") && Math.abs(w.line - across) <= TOLERANCE4 && (Math.abs(w.to - from) <= TOLERANCE4 || Math.abs(w.from - to) <= TOLERANCE4));
10283
+ }
10284
+ function openingViolations(structure, built, doors) {
10285
+ const walls = structure.parts.filter(isMasonryWall);
10286
+ const hosts = new Map(walls.map((wall) => [wall.id, wall]));
10287
+ const out = [];
10288
+ for (const opening2 of structure.openings) {
10289
+ const wall = hosts.get(opening2.wall);
10290
+ if (wall) out.push(...windowViolations(opening2, wall, built));
10291
+ }
10292
+ for (const door of doors) {
10293
+ const beside = wallsBeside(door, walls);
10294
+ if (!beside.length) continue;
10295
+ const along = door.axis === "u" ? door.u : door.v;
10296
+ const span = { from: along - door.width / 2, to: along + door.width / 2 };
10297
+ const lintel = beamAcross(built, beside[0], span, "y0", (structure.levels[door.floor] ?? 0) + DOOR_HEIGHT);
10298
+ if (!lintel) out.push({ code: STRUCTURAL_VIOLATION.OPENING_WITHOUT_LINTEL, part: door.id, message: `door ${door.id} has no lintel over it` });
10299
+ }
10300
+ return out;
10301
+ }
10302
+
10303
+ // src/validate/spec.ts
10304
+ var EPSILON2 = 5e-3;
10305
+ var carries = (part) => part?.structure !== void 0 && part.structure.role !== "none";
10306
+ function flows(joint, byId) {
10307
+ const bears = joint.bears ?? (carries(byId.get(joint.a)) && carries(byId.get(joint.b)));
10308
+ if (bears === false) return [];
10309
+ if (bears === "a_on_b") return [[joint.a, joint.b]];
10310
+ if (bears === "b_on_a") return [[joint.b, joint.a]];
10311
+ return [[joint.a, joint.b], [joint.b, joint.a]];
10312
+ }
10313
+ function groundedOf(spec, byId) {
10314
+ const grounded = new Set(spec.parts.filter((p) => p.anchored || p.structure?.role === "foundation").map((p) => p.id));
10315
+ const edges = (spec.joints ?? []).flatMap((joint) => flows(joint, byId));
10316
+ let grew = true;
10317
+ while (grew) {
10318
+ grew = false;
10319
+ for (const [part, on] of edges) {
10320
+ if (grounded.has(part) || !grounded.has(on)) continue;
10321
+ grounded.add(part);
10322
+ grew = true;
10323
+ }
10324
+ }
10325
+ return grounded;
10326
+ }
10327
+ function sectionViolation(part) {
10328
+ if (part.material !== "concrete" || part.shape.kind !== "box") return null;
10329
+ const role = part.structure?.role;
10330
+ const [x, y, z] = part.shape.size;
10331
+ if (role === "column" && (Math.min(x, z) < MIN_PILLAR_SIDE - EPSILON2 || x * z < MIN_PILLAR_AREA - EPSILON2))
10332
+ return { code: STRUCTURAL_VIOLATION.CONCRETE_BELOW_MIN_SECTION, part: part.id, message: `${part.id} is ${x.toFixed(2)} x ${z.toFixed(2)} m: NBR 6118 asks at least ${MIN_PILLAR_SIDE} m a side` };
10333
+ if (role === "beam" && Math.min(x, z) < MIN_BEAM_WIDTH - EPSILON2 && y >= Math.min(x, z))
10334
+ return { code: STRUCTURAL_VIOLATION.CONCRETE_BELOW_MIN_SECTION, part: part.id, message: `${part.id} is ${Math.min(x, z).toFixed(2)} m wide: NBR 6118 asks at least ${MIN_BEAM_WIDTH} m` };
10335
+ return null;
10336
+ }
10337
+ function specViolations(spec) {
10338
+ const byId = new Map(spec.parts.map((part) => [part.id, part]));
10339
+ const grounded = groundedOf(spec, byId);
10340
+ const out = [];
10341
+ for (const part of spec.parts) {
10342
+ if (!carries(part)) continue;
10343
+ if (!grounded.has(part.id)) {
10344
+ const pillar = part.structure.role === "column";
10345
+ out.push({ code: pillar ? STRUCTURAL_VIOLATION.PILLAR_NOT_GROUNDED : STRUCTURAL_VIOLATION.FLOATING_PART, part: part.id, message: `${part.id} has no load path to an anchored part` });
10346
+ }
10347
+ const section = sectionViolation(part);
10348
+ if (section) out.push(section);
10349
+ }
10350
+ return out;
10351
+ }
10352
+
10353
+ // src/validate/index.ts
10354
+ function validateStructure(structure, options = {}) {
10355
+ const detail = options.wallDetail ?? "panels";
10356
+ const built = applyPanels({ parts: structure.parts, supports: structure.supports }, structure, detail);
10357
+ const out = [...loadPathViolations(structure), ...concreteViolations({ ...built, construction: structure.construction }, isMasonryWall)];
10358
+ if (structure.construction.system !== "masonry") return out;
10359
+ return [...out, ...masonryViolations(structure), ...openingViolations(structure, built.parts, options.doors ?? [])];
10360
+ }
10361
+ function validateSpec(spec) {
10362
+ return specViolations(spec);
10363
+ }
10364
+ function countViolations(violations) {
10365
+ const out = {};
10366
+ for (const violation of violations) out[violation.code] = (out[violation.code] ?? 0) + 1;
10367
+ return out;
10368
+ }
10369
+
9417
10370
  // src/wall-specimen.ts
9418
10371
  var SYSTEM_OF_ASSEMBLY = { log_wall: "log", wood_frame_wall: "wood_frame", sheet_metal_shack: "improvised" };
9419
10372
  var DOOR_WIDTH2 = 0.9;
@@ -9602,7 +10555,7 @@ var GameFloorplanStructure = (() => {
9602
10555
  parts.push({ id, material: c.material, shape: box(size), position, structure: { role: "partition" }, tags: ["infill"] });
9603
10556
  for (const o of on) rests(joints, id, o, "mortar");
9604
10557
  };
9605
- const bay = (side, k, along, centre, fixed, length, c1, c2) => {
10558
+ const bay = (side, k, along, centre2, fixed, length, c1, c2) => {
9606
10559
  const clear = length - g.columnSize;
9607
10560
  const win = clear * c.window;
9608
10561
  const pier = (clear - win) / 2;
@@ -9613,11 +10566,11 @@ var GameFloorplanStructure = (() => {
9613
10566
  wall(`infill_${f}_${side}_${k}_${name}`, size(u, h), [p[0], y, p[2]], on);
9614
10567
  };
9615
10568
  const id = (name) => `infill_${f}_${side}_${k}_${name}`;
9616
- put("sill", clear, sill, yb + sill / 2, centre, [c1, c2]);
9617
- put("pl", pier, head - sill, yb + sill + (head - sill) / 2, centre - clear / 2 + pier / 2, [c1, id("sill")]);
9618
- put("pr", pier, head - sill, yb + sill + (head - sill) / 2, centre + clear / 2 - pier / 2, [c2, id("sill")]);
9619
- put("head", clear, room - head, yb + head + (room - head) / 2, centre, [c1, c2]);
9620
- const gp = at2(centre);
10569
+ put("sill", clear, sill, yb + sill / 2, centre2, [c1, c2]);
10570
+ put("pl", pier, head - sill, yb + sill + (head - sill) / 2, centre2 - clear / 2 + pier / 2, [c1, id("sill")]);
10571
+ put("pr", pier, head - sill, yb + sill + (head - sill) / 2, centre2 + clear / 2 - pier / 2, [c2, id("sill")]);
10572
+ put("head", clear, room - head, yb + head + (room - head) / 2, centre2, [c1, c2]);
10573
+ const gp = at2(centre2);
9621
10574
  const gid = `glass_${f}_${side}_${k}`;
9622
10575
  parts.push({
9623
10576
  id: gid,