@adatechnology/game-floorplan-structure 2.0.2 → 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" }
@@ -2811,6 +2963,22 @@ var GameFloorplanStructure = (() => {
2811
2963
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
2812
2964
  };
2813
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
+ };
2814
2982
  var splinterBox = (inp, box2, limit = inp.maxFragments) => {
2815
2983
  const { rnd } = inp;
2816
2984
  const dims = box2.s;
@@ -2820,35 +2988,81 @@ var GameFloorplanStructure = (() => {
2820
2988
  const w = Math.max(fmin * 1.5, 0.01);
2821
2989
  const c = clamp3(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
2822
2990
  const maxLen = fmax * 4;
2823
- 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
+ }
2824
2997
  const pieces = [];
2825
2998
  for (const cell of cells) {
2826
- const extra = rnd() < 0.4 ? 1 : 0;
2999
+ const extra = Math.floor(rnd() * 3);
2827
3000
  const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
2828
- 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);
2829
3002
  }
2830
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] }));
2831
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;
2832
3039
  var severAt = (inp, root, at2, gap) => {
2833
3040
  const dims = root.s;
2834
3041
  const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
2835
3042
  const half = dims[L] / 2;
2836
3043
  const lo = clamp3(at2 - gap / 2, -half, half), hi = clamp3(at2 + gap / 2, -half, half);
2837
3044
  const out = [];
2838
- 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) => {
2839
3048
  if (to - from < 0.02) return;
2840
3049
  const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
2841
3050
  c[L] = (from + to) / 2;
2842
3051
  sz[L] = to - from;
2843
- 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 });
2844
3055
  };
2845
- stub(-half, lo);
2846
- stub(hi, half);
3056
+ stub(-half, lo, true);
3057
+ stub(hi, half, false);
2847
3058
  const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
2848
3059
  band.c[L] = (lo + hi) / 2;
2849
3060
  band.s[L] = Math.max(hi - lo, 0.01);
2850
- if (inp.style === "snap") {
2851
- 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] });
2852
3066
  } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
2853
3067
  return out;
2854
3068
  };
@@ -2881,8 +3095,18 @@ var GameFloorplanStructure = (() => {
2881
3095
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
2882
3096
  if (style === "snap") {
2883
3097
  if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
3098
+ if (inp.form === "plies") return plyBreak(inp, root, maxFragments);
2884
3099
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
2885
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 });
2886
3110
  return out;
2887
3111
  }
2888
3112
  if (style === "crumble" || style === "none") {
@@ -2989,6 +3213,88 @@ var GameFloorplanStructure = (() => {
2989
3213
  }
2990
3214
  return marks;
2991
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
+ };
2992
3298
  var MAX_SPALLS = 64;
2993
3299
  var thinAxisOf = (dims) => dims[0] <= dims[1] && dims[0] <= dims[2] ? 0 : dims[1] <= dims[2] ? 1 : 2;
2994
3300
  var resolveLayer = (def, index, mat) => ({ index, def, mat, energy: mat.penetration * def.thickness * 1e3 });
@@ -3660,7 +3966,7 @@ var GameFloorplanStructure = (() => {
3660
3966
  eau[j] += e * area * g[i].u * w;
3661
3967
  }
3662
3968
  }
3663
- 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);
3664
3970
  const ei = new Array(n).fill(0);
3665
3971
  const ga = new Array(n).fill(0);
3666
3972
  for (const i of vertical) {
@@ -3669,7 +3975,7 @@ var GameFloorplanStructure = (() => {
3669
3975
  for (let j = 0; j < n; j++) {
3670
3976
  const w = share(p, j) / bands[j];
3671
3977
  if (w <= 0) continue;
3672
- 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;
3673
3979
  }
3674
3980
  }
3675
3981
  for (let i = 0; i < parts.length; i++) {
@@ -3716,7 +4022,7 @@ var GameFloorplanStructure = (() => {
3716
4022
  for (let j = 0; j < n; j++) {
3717
4023
  const w = share(p, j);
3718
4024
  if (w <= 0) continue;
3719
- 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]);
3720
4026
  tot += w;
3721
4027
  }
3722
4028
  axial[i] = tot > 0 ? sum / tot : 0;
@@ -3968,6 +4274,7 @@ var GameFloorplanStructure = (() => {
3968
4274
  },
3969
4275
  {
3970
4276
  id: "wood_soft",
4277
+ splitForm: "slivers",
3971
4278
  density: 500,
3972
4279
  hpPerKg: 90,
3973
4280
  penetration: 9,
@@ -4013,6 +4320,7 @@ var GameFloorplanStructure = (() => {
4013
4320
  },
4014
4321
  {
4015
4322
  id: "wood_hard",
4323
+ splitForm: "slivers",
4016
4324
  density: 750,
4017
4325
  hpPerKg: 140,
4018
4326
  penetration: 14,
@@ -4057,6 +4365,7 @@ var GameFloorplanStructure = (() => {
4057
4365
  },
4058
4366
  {
4059
4367
  id: "plywood",
4368
+ splitForm: "plies",
4060
4369
  density: 600,
4061
4370
  hpPerKg: 70,
4062
4371
  penetration: 7,
@@ -4104,6 +4413,7 @@ var GameFloorplanStructure = (() => {
4104
4413
  },
4105
4414
  {
4106
4415
  id: "mdf",
4416
+ splitForm: "granules",
4107
4417
  density: 750,
4108
4418
  hpPerKg: 45,
4109
4419
  penetration: 6,
@@ -4615,6 +4925,7 @@ var GameFloorplanStructure = (() => {
4615
4925
  },
4616
4926
  {
4617
4927
  id: "green_wood",
4928
+ splitForm: "slivers",
4618
4929
  density: 650,
4619
4930
  hpPerKg: 5,
4620
4931
  penetration: 10,
@@ -4895,6 +5206,125 @@ var GameFloorplanStructure = (() => {
4895
5206
  "brittle"
4896
5207
  ],
4897
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
4898
5328
  }
4899
5329
  ];
4900
5330
  var material_hints_default = {
@@ -5107,6 +5537,11 @@ var GameFloorplanStructure = (() => {
5107
5537
  var BREACH_BLAST_FACTOR = 6;
5108
5538
  var BREACH_AREA = 0.25;
5109
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;
5110
5545
  var RESIDUAL_CAPACITY = 0.15;
5111
5546
  var SEVER_BAND = 0.1;
5112
5547
  var SEVER_ASPECT = 3;
@@ -6359,7 +6794,7 @@ var GameFloorplanStructure = (() => {
6359
6794
  if (j.st.open) continue;
6360
6795
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
6361
6796
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
6362
- 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));
6363
6798
  if (j.st.hp <= 0) {
6364
6799
  j.st.hp = 0;
6365
6800
  j.st.open = true;
@@ -6487,6 +6922,7 @@ var GameFloorplanStructure = (() => {
6487
6922
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
6488
6923
  const cut = (_b = p.rt).craters ?? (_b.craters = {});
6489
6924
  const out = [];
6925
+ const wholeBefore = Object.values(worn).filter((w) => w === -1).length;
6490
6926
  const shot = kind === "ballistic";
6491
6927
  for (let i = 0; i < grid.length; i++) {
6492
6928
  const cell = grid[i];
@@ -6545,6 +6981,7 @@ var GameFloorplanStructure = (() => {
6545
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 });
6546
6982
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
6547
6983
  }
6984
+ if (Object.values(worn).filter((w) => w === -1).length > wholeBefore) dropUnsupported(o, p, src);
6548
6985
  if (p.spec.reinforcement) exposeRebar(o, p, src);
6549
6986
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
6550
6987
  if (p.structural && !isLost(p)) {
@@ -6648,10 +7085,122 @@ var GameFloorplanStructure = (() => {
6648
7085
  setStage(o, p, 1, {}, src);
6649
7086
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
6650
7087
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
7088
+ dropUnsupported(o, p, src);
6651
7089
  const reinforcement = p.spec.reinforcement;
6652
7090
  if (reinforcement) exposeRebar(o, p, src);
6653
7091
  if (p.structural) solveStructure(o);
6654
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
+ };
6655
7204
  const stripPart = (o, p, reinforcement, src) => {
6656
7205
  const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
6657
7206
  p.st.stripped = true;
@@ -6718,6 +7267,7 @@ var GameFloorplanStructure = (() => {
6718
7267
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
6719
7268
  const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
6720
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;
6721
7271
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
6722
7272
  style,
6723
7273
  dims: p.dims,
@@ -6726,8 +7276,9 @@ var GameFloorplanStructure = (() => {
6726
7276
  hit: hitLocal,
6727
7277
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
6728
7278
  maxFragments,
6729
- rnd,
7279
+ rnd: planRnd,
6730
7280
  sheet: p.mat.sheet,
7281
+ ...p.mat.splitForm ? { form: p.mat.splitForm } : {},
6731
7282
  ...cutAt2 !== void 0 ? { cut: { at: cutAt2, gap: SEVER_BAND } } : {}
6732
7283
  });
6733
7284
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
@@ -6839,11 +7390,11 @@ var GameFloorplanStructure = (() => {
6839
7390
  o.active = true;
6840
7391
  }
6841
7392
  };
6842
- const lightLeaksNear = (centre, radius) => {
7393
+ const lightLeaksNear = (centre2, radius) => {
6843
7394
  for (const o of objects.values()) {
6844
7395
  for (const q of o.parts) {
6845
7396
  if (isLost(q) || q.rt.lit || q.rt.jet || !leaksFlammable(q)) continue;
6846
- 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;
6847
7398
  q.rt.lit = true;
6848
7399
  o.active = true;
6849
7400
  }
@@ -6866,7 +7417,8 @@ var GameFloorplanStructure = (() => {
6866
7417
  const fragments = [];
6867
7418
  for (let i = 0; i < n; i++) {
6868
7419
  const back = !exit || i % 3 === 2;
6869
- 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());
6870
7422
  const size = [w, w, w];
6871
7423
  size[along] = len;
6872
7424
  size[(along + 1) % 3] = w;
@@ -7027,7 +7579,7 @@ var GameFloorplanStructure = (() => {
7027
7579
  checkIgnite(o, p, src);
7028
7580
  }
7029
7581
  }
7030
- 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));
7031
7583
  const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
7032
7584
  if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
7033
7585
  frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
@@ -7037,7 +7589,10 @@ var GameFloorplanStructure = (() => {
7037
7589
  const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
7038
7590
  res.damage += dealt;
7039
7591
  if (p.bend && flexure.enabled) bendByBlast(o, p, point, energy, radius, reach, src);
7040
- 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);
7041
7596
  return reach;
7042
7597
  };
7043
7598
  const blastProbe = { partsTested: 0, partsNear: 0 };
@@ -7283,12 +7838,17 @@ var GameFloorplanStructure = (() => {
7283
7838
  }
7284
7839
  return h | 0;
7285
7840
  };
7286
- 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) => {
7287
7847
  const v = frag.velocity;
7288
7848
  const speed = Math.hypot(v[0], v[1], v[2]);
7289
7849
  const energy = g.energyFactor * 0.5 * frag.mass * speed * speed;
7290
7850
  if (speed < 0.5 || energy < 1) return;
7291
- 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]));
7292
7852
  let dir = vec.scale(v, 1 / speed);
7293
7853
  const behind = from ? shields.get(from) : void 0;
7294
7854
  if (behind) {
@@ -9162,6 +9722,8 @@ var GameFloorplanStructure = (() => {
9162
9722
  // src/wall-detail/panels.ts
9163
9723
  var LINTEL_DEPTH2 = 0.2;
9164
9724
  var LINTEL_BEARING2 = 0.2;
9725
+ var SILL_BEAM_DEPTH = 0.1;
9726
+ var LINTEL_SPAN_RATIO = 10;
9165
9727
  var MIN_PIECE = 0.05;
9166
9728
  var frameOf = (wall) => {
9167
9729
  const alongU = wall.aabb[2] - wall.aabb[0] >= wall.aabb[3] - wall.aabb[1];
@@ -9175,6 +9737,7 @@ var GameFloorplanStructure = (() => {
9175
9737
  const drafts = [];
9176
9738
  const piers = [];
9177
9739
  const lintels = [];
9740
+ const sillBeams = [];
9178
9741
  const spandrels = [];
9179
9742
  const height = wall.y1 - wall.y0;
9180
9743
  let cursor = frame2.from;
@@ -9185,23 +9748,26 @@ var GameFloorplanStructure = (() => {
9185
9748
  };
9186
9749
  own.forEach((opening2, k) => {
9187
9750
  addPier(Math.max(cursor, opening2.from));
9188
- 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));
9189
9753
  const lintelFrom = Math.max(frame2.from, opening2.from - LINTEL_BEARING2);
9190
9754
  const lintelTo = Math.min(frame2.to, opening2.to + LINTEL_BEARING2);
9191
9755
  const lintelHeight = lintelTop - opening2.y1;
9192
- 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 });
9193
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 });
9194
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);
9195
9761
  cursor = opening2.to;
9196
9762
  });
9197
9763
  addPier(frame2.to);
9198
9764
  for (const pier of piers) {
9199
- for (const lintel of lintels) {
9765
+ for (const lintel of [...lintels, ...sillBeams]) {
9200
9766
  const overlap = Math.max(0, Math.min(pier.to, lintel.to) - Math.max(pier.from, lintel.from));
9201
9767
  pier.volume -= overlap * (lintel.y1 - lintel.y0);
9202
9768
  }
9203
9769
  }
9204
- const all = [...piers, ...drafts, ...lintels, ...spandrels.filter((d) => d !== void 0)];
9770
+ const all = [...piers, ...drafts, ...sillBeams, ...lintels, ...spandrels.filter((d) => d !== void 0)];
9205
9771
  const total = all.reduce((sum, d) => sum + d.volume, 0);
9206
9772
  const pieces = all.map((d) => ({
9207
9773
  id: d.id,
@@ -9216,6 +9782,11 @@ var GameFloorplanStructure = (() => {
9216
9782
  ...d.role === "beam" ? style.lintelAssembly ? { assembly: style.lintelAssembly } : {} : wall.assembly ? { assembly: wall.assembly } : {}
9217
9783
  }));
9218
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
+ }
9219
9790
  lintels.forEach((lintel, k) => {
9220
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 });
9221
9792
  const spandrel = spandrels[k];
@@ -9247,7 +9818,8 @@ var GameFloorplanStructure = (() => {
9247
9818
  ...system.timber ? {} : { lintelAssembly: "reinforced_concrete" },
9248
9819
  lintelJoint: system.joints.beam,
9249
9820
  wallJoint: system.joints.wall,
9250
- ...lintelDepth && !system.timber ? { lintelDepth } : {}
9821
+ ...lintelDepth && !system.timber ? { lintelDepth } : {},
9822
+ ...construction.system === "masonry" ? { sillBeam: true } : {}
9251
9823
  });
9252
9824
  if (split.pieces.length > 1) splits.set(part.id, split);
9253
9825
  }
@@ -9255,7 +9827,18 @@ var GameFloorplanStructure = (() => {
9255
9827
  }
9256
9828
  function applyPanels(input, structure, detail, lintelDepth) {
9257
9829
  if (detail === "coarse") return input;
9258
- 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;
9259
9842
  }
9260
9843
  function applyUnits(input, structure) {
9261
9844
  const splits = /* @__PURE__ */ new Map();
@@ -9319,21 +9902,35 @@ var GameFloorplanStructure = (() => {
9319
9902
  };
9320
9903
  var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
9321
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
+ }
9322
9917
  var caged = (spec, part, rebar) => {
9323
9918
  if (!rebar || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
9324
9919
  const size = spec.shape.kind === "box" ? spec.shape.size : null;
9325
9920
  if (!size) return spec;
9326
9921
  const thin = Math.min(...size);
9922
+ const reinforcement = { material: rebar, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter };
9327
9923
  return {
9328
9924
  ...spec,
9329
- 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,
9330
9926
  units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
9331
9927
  };
9332
9928
  };
9333
9929
  var partSpecOf = (part, capacity, detailed, rebar) => {
9334
9930
  if (detailed && part.assembly) return assemblySpec(part, capacity);
9335
9931
  const spec = boxSpec(part, capacity);
9336
- 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;
9337
9934
  };
9338
9935
  function toDestructibleSpec(structure, options = {}) {
9339
9936
  const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
@@ -9349,7 +9946,9 @@ var GameFloorplanStructure = (() => {
9349
9946
  const walled = applyPanels(slabbed, structure, detail, slabOptions && structure.construction.frame.rebar ? (span) => beamDepthFor(span, slabOptions) : void 0);
9350
9947
  const panels = slabOptions && detail !== "coarse" ? deepenBondBeams(walled, structure, slabOptions) : walled;
9351
9948
  const sizing = { safety, redundancy, safetyOthers };
9352
- 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) };
9353
9952
  const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
9354
9953
  const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
9355
9954
  const margin = options.terrainMargin ?? TERRAIN_MARGIN;
@@ -9415,6 +10014,359 @@ var GameFloorplanStructure = (() => {
9415
10014
  return out;
9416
10015
  }
9417
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
+
9418
10370
  // src/wall-specimen.ts
9419
10371
  var SYSTEM_OF_ASSEMBLY = { log_wall: "log", wood_frame_wall: "wood_frame", sheet_metal_shack: "improvised" };
9420
10372
  var DOOR_WIDTH2 = 0.9;
@@ -9603,7 +10555,7 @@ var GameFloorplanStructure = (() => {
9603
10555
  parts.push({ id, material: c.material, shape: box(size), position, structure: { role: "partition" }, tags: ["infill"] });
9604
10556
  for (const o of on) rests(joints, id, o, "mortar");
9605
10557
  };
9606
- const bay = (side, k, along, centre, fixed, length, c1, c2) => {
10558
+ const bay = (side, k, along, centre2, fixed, length, c1, c2) => {
9607
10559
  const clear = length - g.columnSize;
9608
10560
  const win = clear * c.window;
9609
10561
  const pier = (clear - win) / 2;
@@ -9614,11 +10566,11 @@ var GameFloorplanStructure = (() => {
9614
10566
  wall(`infill_${f}_${side}_${k}_${name}`, size(u, h), [p[0], y, p[2]], on);
9615
10567
  };
9616
10568
  const id = (name) => `infill_${f}_${side}_${k}_${name}`;
9617
- put("sill", clear, sill, yb + sill / 2, centre, [c1, c2]);
9618
- put("pl", pier, head - sill, yb + sill + (head - sill) / 2, centre - clear / 2 + pier / 2, [c1, id("sill")]);
9619
- put("pr", pier, head - sill, yb + sill + (head - sill) / 2, centre + clear / 2 - pier / 2, [c2, id("sill")]);
9620
- put("head", clear, room - head, yb + head + (room - head) / 2, centre, [c1, c2]);
9621
- 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);
9622
10574
  const gid = `glass_${f}_${side}_${k}`;
9623
10575
  parts.push({
9624
10576
  id: gid,