@adatechnology/game-floorplan-structure 2.0.2 → 2.1.1
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.
- package/README.md +18 -0
- package/dist/index.d.ts +487 -316
- package/dist/index.global.js +1028 -69
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +595 -50
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -334,12 +334,15 @@ function createContext(source, construction, extras, tank, options) {
|
|
|
334
334
|
builder,
|
|
335
335
|
porchRoofOnPostsOnly: options.porchRoofOnPostsOnly,
|
|
336
336
|
rafterRoof: options.rafterRoof,
|
|
337
|
+
wallPillarSpacing: options.wallPillarSpacing,
|
|
338
|
+
visorSections: options.visorSections,
|
|
337
339
|
floorCount,
|
|
338
340
|
isUnit,
|
|
339
341
|
elevation,
|
|
340
342
|
isTimber: system.timber,
|
|
341
343
|
walls: [],
|
|
342
344
|
wallParts: /* @__PURE__ */ new Map(),
|
|
345
|
+
groundBeams: /* @__PURE__ */ new Map(),
|
|
343
346
|
carriers: /* @__PURE__ */ new Map(),
|
|
344
347
|
plates: /* @__PURE__ */ new Map(),
|
|
345
348
|
pillars: /* @__PURE__ */ new Map(),
|
|
@@ -412,7 +415,8 @@ function addBalconies(context) {
|
|
|
412
415
|
builder.rest(deck, base, system.joints.slab);
|
|
413
416
|
continue;
|
|
414
417
|
}
|
|
415
|
-
const
|
|
418
|
+
const half = context.visorSections ? 0.08 : 0.1;
|
|
419
|
+
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 });
|
|
416
420
|
builder.rest(pillar, GROUND, "rebar");
|
|
417
421
|
builder.rest(deck, pillar, "rebar");
|
|
418
422
|
}
|
|
@@ -483,8 +487,8 @@ function addJoists(context, options) {
|
|
|
483
487
|
const top = context.level(floor) - thickness * 0.15;
|
|
484
488
|
const out = [];
|
|
485
489
|
for (let k = 0; k < count; k++) {
|
|
486
|
-
const
|
|
487
|
-
const aabb = alongU ? [
|
|
490
|
+
const centre2 = (alongU ? box2[0] : box2[1]) + 0.05 + k * (span - 0.1) / (count - 1);
|
|
491
|
+
const aabb = alongU ? [centre2 - 0.025, box2[1], centre2 + 0.025, box2[3]] : [box2[0], centre2 - 0.025, box2[2], centre2 + 0.025];
|
|
488
492
|
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 });
|
|
489
493
|
out.push(joist);
|
|
490
494
|
const below = carriers.filter((carrier) => carrier.y0 <= top + 0.05);
|
|
@@ -574,8 +578,8 @@ function addPiers(context) {
|
|
|
574
578
|
const at2 = alongU ? box2[1] + line * (box2[3] - box2[1]) / (rows - 1) : box2[0] + line * (box2[2] - box2[0]) / (columns - 1);
|
|
575
579
|
const aabb = alongU ? [box2[0], at2 - 0.07, box2[2], at2 + 0.07] : [at2 - 0.07, box2[1], at2 + 0.07, box2[3]];
|
|
576
580
|
const beam = builder.add({ id: `base_beam_${line}`, role: "beam", material: construction.frame.timber, aabb, y0: height, y1: elevation - context.system.slabThickness }, { floor: 0 });
|
|
577
|
-
const
|
|
578
|
-
for (const pier of piers.filter((q) => Math.abs(
|
|
581
|
+
const centre2 = (q) => alongU ? (q.aabb[1] + q.aabb[3]) / 2 : (q.aabb[0] + q.aabb[2]) / 2;
|
|
582
|
+
for (const pier of piers.filter((q) => Math.abs(centre2(q) - at2) < 0.05)) builder.rest(beam, pier, "bolt");
|
|
579
583
|
beams.push(beam);
|
|
580
584
|
}
|
|
581
585
|
return beams;
|
|
@@ -596,18 +600,115 @@ function addFoundation(context) {
|
|
|
596
600
|
else if (context.isTimber && type !== "ground") context.base0 = raft(context, Math.max(0.12, context.elevation), "concrete");
|
|
597
601
|
}
|
|
598
602
|
|
|
603
|
+
// src/structure/ground-beams.ts
|
|
604
|
+
var GROUND_BEAM_DEPTH = 0.3;
|
|
605
|
+
function addGroundBeams(context) {
|
|
606
|
+
const { builder, construction } = context;
|
|
607
|
+
if (construction.system !== "masonry" || context.isUnit || construction.foundation.type !== "strip_footing") return;
|
|
608
|
+
const top = context.level(0);
|
|
609
|
+
for (const wall of context.walls) {
|
|
610
|
+
if (wall.floor !== 0 || wall.type !== "wall") continue;
|
|
611
|
+
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" });
|
|
612
|
+
builder.rest(beam, GROUND, "rebar");
|
|
613
|
+
context.groundBeams.set(wall.id, beam);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/structure/wall-pillars.ts
|
|
618
|
+
var WALL_PILLAR_SPACING = 4;
|
|
619
|
+
var WALL_PILLAR_SIDE = 0.2;
|
|
620
|
+
var MERGE_DISTANCE = 0.45;
|
|
621
|
+
var HALF = WALL_PILLAR_SIDE / 2;
|
|
622
|
+
var TOLERANCE = 0.03;
|
|
623
|
+
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)];
|
|
624
|
+
var acrossOf = (wall) => wall.axis === "u" ? wall.v0 : wall.u0;
|
|
625
|
+
function stretchesOf(context, wall) {
|
|
626
|
+
const [low, high] = alongRange(wall);
|
|
627
|
+
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]);
|
|
628
|
+
const out = [];
|
|
629
|
+
let cursor = low;
|
|
630
|
+
for (const [from, to] of windows) {
|
|
631
|
+
if (from - cursor > TOLERANCE) out.push({ wall, from: cursor, to: from });
|
|
632
|
+
cursor = Math.max(cursor, to);
|
|
633
|
+
}
|
|
634
|
+
if (high - cursor > TOLERANCE) out.push({ wall, from: cursor, to: high });
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
637
|
+
function junctionsOf(context, wall) {
|
|
638
|
+
const [low, high] = alongRange(wall);
|
|
639
|
+
const line = acrossOf(wall);
|
|
640
|
+
const out = [];
|
|
641
|
+
for (const other of context.walls) {
|
|
642
|
+
if (other === wall || other.floor !== wall.floor || other.axis === wall.axis || other.type !== "wall") continue;
|
|
643
|
+
const at2 = acrossOf(other);
|
|
644
|
+
const [a, b] = alongRange(other);
|
|
645
|
+
const meets = Math.abs(a - line) <= (wall.thickness ?? 0.15) / 2 + TOLERANCE || Math.abs(b - line) <= (wall.thickness ?? 0.15) / 2 + TOLERANCE;
|
|
646
|
+
if (meets && at2 > low + HALF && at2 < high - HALF) out.push(at2);
|
|
647
|
+
}
|
|
648
|
+
return out;
|
|
649
|
+
}
|
|
650
|
+
function pillarStations(from, to, junctions, spacing) {
|
|
651
|
+
const length = to - from;
|
|
652
|
+
if (length < WALL_PILLAR_SIDE - 1e-6) return [];
|
|
653
|
+
if (length < 2 * WALL_PILLAR_SIDE) return [(from + to) / 2];
|
|
654
|
+
if (length < WALL_PILLAR_SIDE + MERGE_DISTANCE) return [from + HALF, to - HALF];
|
|
655
|
+
const fixed = [from + HALF, ...junctions.filter((at2) => at2 > from + HALF + MERGE_DISTANCE && at2 < to - HALF - MERGE_DISTANCE), to - HALF].sort((a, b) => a - b);
|
|
656
|
+
const out = [];
|
|
657
|
+
fixed.forEach((at2, k) => {
|
|
658
|
+
out.push(at2);
|
|
659
|
+
const next = fixed[k + 1];
|
|
660
|
+
if (next === void 0) return;
|
|
661
|
+
const spans = Math.ceil((next - at2) / spacing - 1e-6);
|
|
662
|
+
for (let s = 1; s < spans; s++) out.push(at2 + (next - at2) * s / spans);
|
|
663
|
+
});
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
function addWallPillars(context, walls, floor, plates, taken, spacing) {
|
|
667
|
+
const { builder } = context;
|
|
668
|
+
const pillars = [];
|
|
669
|
+
const placed = taken.map((p) => [(p.aabb[0] + p.aabb[2]) / 2, (p.aabb[1] + p.aabb[3]) / 2]);
|
|
670
|
+
const base = context.baseOf(floor);
|
|
671
|
+
for (const wall of walls) {
|
|
672
|
+
if (!wall.exterior || wall.type !== "wall") continue;
|
|
673
|
+
const top = context.wallParts.get(wall.id).y1;
|
|
674
|
+
const across = acrossOf(wall);
|
|
675
|
+
const junctions = junctionsOf(context, wall);
|
|
676
|
+
for (const stretch of stretchesOf(context, wall)) {
|
|
677
|
+
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];
|
|
678
|
+
const first = standing(stretch.from);
|
|
679
|
+
const last = standing(stretch.to);
|
|
680
|
+
const from = first === void 0 ? stretch.from : Math.min(stretch.from, first - HALF);
|
|
681
|
+
const to = last === void 0 ? stretch.to : Math.max(stretch.to, last + HALF);
|
|
682
|
+
for (const along of pillarStations(from, to, junctions, spacing)) {
|
|
683
|
+
const [u, v] = wall.axis === "u" ? [along, across] : [across, along];
|
|
684
|
+
const end = Math.abs(along - stretch.from) < Math.abs(along - stretch.to) ? stretch.from : stretch.to;
|
|
685
|
+
const endPoint = wall.axis === "u" ? [end, across] : [across, end];
|
|
686
|
+
const isEnd = Math.abs(along - end) <= HALF + TOLERANCE;
|
|
687
|
+
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;
|
|
688
|
+
placed.push([u, v]);
|
|
689
|
+
const box2 = [u - HALF, v - HALF, u + HALF, v + HALF];
|
|
690
|
+
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 });
|
|
691
|
+
builder.rest(pillar, base === GROUND || !base ? GROUND : base, "rebar");
|
|
692
|
+
for (const plate of plates.filter((p) => touches(p.aabb, box2, 0.01))) builder.rest(plate, pillar, "rebar");
|
|
693
|
+
pillars.push(pillar);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return pillars;
|
|
698
|
+
}
|
|
699
|
+
|
|
599
700
|
// src/structure/plates.ts
|
|
600
701
|
var MAX_GAP_WITHIN_RUN = 3.2;
|
|
601
702
|
var POST_SPACING = 2.4;
|
|
602
703
|
var POST_SIDE = 0.05;
|
|
603
704
|
var alongOf = (wall, pick) => wall.axis === "u" ? pick(wall.u0, wall.u1) : pick(wall.v0, wall.v1);
|
|
604
|
-
var
|
|
705
|
+
var acrossOf2 = (wall) => wall.axis === "u" ? wall.v0 : wall.u0;
|
|
605
706
|
function runsOf(context, walls, floor) {
|
|
606
707
|
const lines = /* @__PURE__ */ new Map();
|
|
607
708
|
for (const wall of walls) {
|
|
608
709
|
if (!wall.exterior || wall.type !== "wall") continue;
|
|
609
710
|
const top = round2(context.wallParts.get(wall.id).y1);
|
|
610
|
-
const key = `${floor}|${wall.axis}|${
|
|
711
|
+
const key = `${floor}|${wall.axis}|${acrossOf2(wall).toFixed(2)}|${top.toFixed(2)}`;
|
|
611
712
|
lines.set(key, [...lines.get(key) ?? [], wall]);
|
|
612
713
|
}
|
|
613
714
|
return [...lines.values()].map((line) => {
|
|
@@ -627,7 +728,7 @@ function runsOf(context, walls, floor) {
|
|
|
627
728
|
}
|
|
628
729
|
function addShackPosts(context, run, plate, floor, index) {
|
|
629
730
|
const first = run.walls[0];
|
|
630
|
-
const across =
|
|
731
|
+
const across = acrossOf2(first);
|
|
631
732
|
const top = context.wallParts.get(first.id).y1;
|
|
632
733
|
const count = Math.max(1, Math.ceil((run.to - run.from) / POST_SPACING));
|
|
633
734
|
const posts = [];
|
|
@@ -653,7 +754,7 @@ function addPlates(context, walls, floor) {
|
|
|
653
754
|
for (const run of runs) {
|
|
654
755
|
const first = run.walls[0];
|
|
655
756
|
const half = (first.thickness ?? 0.15) / 2;
|
|
656
|
-
const across =
|
|
757
|
+
const across = acrossOf2(first);
|
|
657
758
|
const top = context.wallParts.get(first.id).y1;
|
|
658
759
|
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];
|
|
659
760
|
const material = context.isTimber ? construction.frame.timber : construction.frame.beams;
|
|
@@ -692,7 +793,11 @@ function addCornerPillars(context, walls, floor, plates) {
|
|
|
692
793
|
function addPlatesAndPillars(context, floor) {
|
|
693
794
|
const walls = context.walls.filter((wall) => wall.floor === floor);
|
|
694
795
|
const { plates, pillars } = addPlates(context, walls, floor);
|
|
695
|
-
if (context.construction.system === "masonry")
|
|
796
|
+
if (context.construction.system === "masonry") {
|
|
797
|
+
const corners = addCornerPillars(context, walls, floor, plates);
|
|
798
|
+
pillars.push(...corners);
|
|
799
|
+
if (context.wallPillarSpacing) pillars.push(...addWallPillars(context, walls, floor, plates, corners, context.wallPillarSpacing));
|
|
800
|
+
}
|
|
696
801
|
context.pillars.set(floor, pillars);
|
|
697
802
|
context.plates.set(floor, plates);
|
|
698
803
|
const innerBearing = walls.filter((wall) => wall.role === "wall" && !wall.exterior).map((wall) => context.wallParts.get(wall.id));
|
|
@@ -977,21 +1082,21 @@ function addStairs(context) {
|
|
|
977
1082
|
}
|
|
978
1083
|
|
|
979
1084
|
// src/structure/openings.ts
|
|
980
|
-
var
|
|
1085
|
+
var TOLERANCE2 = 0.02;
|
|
981
1086
|
var DOOR_HEIGHT = 2.1;
|
|
982
1087
|
var LINTEL_DEPTH = 0.2;
|
|
983
1088
|
var LINTEL_BEARING = 0.2;
|
|
984
1089
|
var crossOf = (wall) => wall.axis === "u" ? wall.v0 : wall.u0;
|
|
985
|
-
var
|
|
1090
|
+
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)];
|
|
986
1091
|
function findOpenings(context) {
|
|
987
1092
|
const openings = [];
|
|
988
1093
|
for (const window of context.source.windows ?? []) {
|
|
989
1094
|
const from = window.at - window.width / 2;
|
|
990
1095
|
const to = window.at + window.width / 2;
|
|
991
1096
|
const wall = context.walls.find((w) => {
|
|
992
|
-
if (w.floor !== window.floor || w.axis !== window.axis || w.type !== "wall" || Math.abs(crossOf(w) - window.pos) >
|
|
993
|
-
const [low, high] =
|
|
994
|
-
return from >= low -
|
|
1097
|
+
if (w.floor !== window.floor || w.axis !== window.axis || w.type !== "wall" || Math.abs(crossOf(w) - window.pos) > TOLERANCE2) return false;
|
|
1098
|
+
const [low, high] = alongRange2(w);
|
|
1099
|
+
return from >= low - TOLERANCE2 && to <= high + TOLERANCE2;
|
|
995
1100
|
});
|
|
996
1101
|
if (!wall) continue;
|
|
997
1102
|
const base = context.level(window.floor);
|
|
@@ -1006,18 +1111,39 @@ function addDoorLintels(context) {
|
|
|
1006
1111
|
const cross = door.axis === "u" ? door.v : door.u;
|
|
1007
1112
|
const from = along - door.width / 2;
|
|
1008
1113
|
const to = along + door.width / 2;
|
|
1009
|
-
const beside = context.walls.filter((w) => w.floor === door.floor && w.axis === door.axis && w.type === "wall" && Math.abs(crossOf(w) - cross) <=
|
|
1114
|
+
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));
|
|
1010
1115
|
if (!beside.length) continue;
|
|
1011
1116
|
const half = (beside[0].thickness ?? 0.15) / 2;
|
|
1012
1117
|
const lo = from - LINTEL_BEARING;
|
|
1013
1118
|
const hi = to + LINTEL_BEARING;
|
|
1014
1119
|
const box2 = door.axis === "u" ? [lo, cross - half, hi, cross + half] : [cross - half, lo, cross + half, hi];
|
|
1015
1120
|
const y0 = context.level(door.floor) + DOOR_HEIGHT;
|
|
1016
|
-
const
|
|
1121
|
+
const depth = Math.max(LINTEL_DEPTH, (door.width + LINTEL_BEARING) / 10);
|
|
1122
|
+
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" } });
|
|
1017
1123
|
for (const wall of beside) builder.rest(lintel, context.wallParts.get(wall.id), system.joints.beam);
|
|
1124
|
+
const jambs = (context.pillars.get(door.floor) ?? []).filter((pillar) => pillar.material === "concrete" && overlapsInPlan(pillar.aabb, box2) && pillar.y1 >= y0);
|
|
1125
|
+
if (construction.system === "masonry" && hi - lo > WIDE_DOOR) jambs.push(...addJambPillars(context, { door, beside, from, to, cross }, jambs));
|
|
1126
|
+
for (const pillar of jambs) builder.rest(lintel, pillar, "rebar");
|
|
1018
1127
|
for (const plate of context.plates.get(door.floor) ?? []) if (overlapsInPlan(plate.aabb, box2)) builder.rest(plate, lintel, system.joints.beam);
|
|
1019
1128
|
}
|
|
1020
1129
|
}
|
|
1130
|
+
var WIDE_DOOR = 3;
|
|
1131
|
+
var HALF_PILLAR = WALL_PILLAR_SIDE / 2;
|
|
1132
|
+
function addJambPillars(context, gap, standing) {
|
|
1133
|
+
const { builder, construction } = context;
|
|
1134
|
+
const top = Math.max(...gap.beside.map((w) => context.wallParts.get(w.id).y1));
|
|
1135
|
+
const base = context.baseOf(gap.door.floor);
|
|
1136
|
+
const out = [];
|
|
1137
|
+
for (const [k, along] of [[0, gap.from - HALF_PILLAR], [1, gap.to + HALF_PILLAR]]) {
|
|
1138
|
+
const [u, v] = gap.door.axis === "u" ? [along, gap.cross] : [gap.cross, along];
|
|
1139
|
+
const box2 = [u - HALF_PILLAR, v - HALF_PILLAR, u + HALF_PILLAR, v + HALF_PILLAR];
|
|
1140
|
+
if (standing.some((p) => overlapsInPlan(p.aabb, box2))) continue;
|
|
1141
|
+
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 });
|
|
1142
|
+
builder.rest(pillar, base === GROUND || !base ? GROUND : base, "rebar");
|
|
1143
|
+
out.push(pillar);
|
|
1144
|
+
}
|
|
1145
|
+
return out;
|
|
1146
|
+
}
|
|
1021
1147
|
var overlapsInPlan = (a, b) => a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1];
|
|
1022
1148
|
|
|
1023
1149
|
// src/structure/steps.ts
|
|
@@ -1197,10 +1323,10 @@ function addWallPart(context, wall, floor) {
|
|
|
1197
1323
|
const framed = system.studs !== void 0 && !isRailing;
|
|
1198
1324
|
const part = builder.add(
|
|
1199
1325
|
{ id: wall.id, role: wall.role, material: wall.material, aabb: wall.box, y0, y1: Math.max(y1, y0 + 0.2) },
|
|
1200
|
-
{ floor, fill: context.isTimber && wall.role === "wall" ? system.wallFill : 1, ...framed ? { studs: system.studs } : {}, ...isRailing ? {} : { assembly: assemblyOf(context) } }
|
|
1326
|
+
{ floor, fill: context.isTimber && wall.role === "wall" ? system.wallFill : 1, ...framed ? { studs: system.studs } : {}, ...isRailing ? {} : { assembly: assemblyOf(context) }, ...wall.exterior && !isRailing ? { exterior: true } : {} }
|
|
1201
1327
|
);
|
|
1202
1328
|
context.wallParts.set(wall.id, part);
|
|
1203
|
-
const base = isRailing ? railingBase(context, wall, floor) : context.baseOf(floor);
|
|
1329
|
+
const base = isRailing ? railingBase(context, wall, floor) : context.groundBeams.get(wall.id) ?? context.baseOf(floor);
|
|
1204
1330
|
const joint = isRailing ? "nail" : system.joints.wall;
|
|
1205
1331
|
if (base === GROUND || floor === 0 && !base) builder.rest(part, GROUND, joint);
|
|
1206
1332
|
else if (base) builder.rest(part, base, joint);
|
|
@@ -1218,6 +1344,11 @@ function buildFloors(context) {
|
|
|
1218
1344
|
addUpperFloor(context, floor);
|
|
1219
1345
|
}
|
|
1220
1346
|
}
|
|
1347
|
+
var wallPillarSpacingOf = (options) => {
|
|
1348
|
+
const pillars = options.wallPillars ?? true;
|
|
1349
|
+
if (pillars === false) return 0;
|
|
1350
|
+
return pillars === true ? WALL_PILLAR_SPACING : pillars.spacing ?? WALL_PILLAR_SPACING;
|
|
1351
|
+
};
|
|
1221
1352
|
function buildStructure(source, options = {}) {
|
|
1222
1353
|
const refined = options.wallDetail !== "coarse";
|
|
1223
1354
|
const built = options.construction ? { construction: options.construction, extras: options.extras ?? {} } : resolveConstruction(source);
|
|
@@ -1225,8 +1356,9 @@ function buildStructure(source, options = {}) {
|
|
|
1225
1356
|
const extras = options.extras ?? built.extras ?? {};
|
|
1226
1357
|
if (construction.system === "canvas") return buildCanvasStructure(construction, extras);
|
|
1227
1358
|
const tank = options.tank !== void 0 ? options.tank : source.waterTank ?? null;
|
|
1228
|
-
const context = createContext(source, construction, extras, tank, { porchRoofOnPostsOnly: options.porchRoofOnPostsOnly ?? refined, rafterRoof: options.rafterRoof ?? refined });
|
|
1359
|
+
const context = createContext(source, construction, extras, tank, { porchRoofOnPostsOnly: options.porchRoofOnPostsOnly ?? refined, rafterRoof: options.rafterRoof ?? refined, wallPillarSpacing: wallPillarSpacingOf(options), visorSections: options.visorSections === true });
|
|
1229
1360
|
addFoundation(context);
|
|
1361
|
+
if (options.groundBeams !== false) addGroundBeams(context);
|
|
1230
1362
|
buildFloors(context);
|
|
1231
1363
|
addBalconies(context);
|
|
1232
1364
|
addPorchRoofs(context);
|
|
@@ -1675,13 +1807,13 @@ function placeMeter(plan) {
|
|
|
1675
1807
|
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));
|
|
1676
1808
|
const wall = segments.find((w) => w.axis === "u" && w.v0 === 0) || segments[0];
|
|
1677
1809
|
if (!wall) return null;
|
|
1678
|
-
const
|
|
1810
|
+
const centre2 = wall.axis === "u" ? (wall.u0 + wall.u1) / 2 : (wall.v0 + wall.v1) / 2;
|
|
1679
1811
|
const vector = outwardOf(wall);
|
|
1680
1812
|
const offset = HALF_WALL + 0.1;
|
|
1681
1813
|
return {
|
|
1682
1814
|
id: "gas.meter",
|
|
1683
|
-
u: round22(wall.axis === "u" ?
|
|
1684
|
-
v: round22(wall.axis === "u" ? wall.v0 + vector[1] * offset :
|
|
1815
|
+
u: round22(wall.axis === "u" ? centre2 : wall.u0 + vector[0] * offset),
|
|
1816
|
+
v: round22(wall.axis === "u" ? wall.v0 + vector[1] * offset : centre2),
|
|
1685
1817
|
y: 1.2,
|
|
1686
1818
|
side: sideNameOf(vector),
|
|
1687
1819
|
w: 0.3,
|
|
@@ -1872,9 +2004,9 @@ function placeInverter(plan) {
|
|
|
1872
2004
|
const wall = groundExteriorWalls(plan).filter((q) => q.sides.includes("outside")).sort((a, b) => (a.axis === "v" ? 0 : 1) - (b.axis === "v" ? 0 : 1))[0];
|
|
1873
2005
|
if (!wall) return null;
|
|
1874
2006
|
const vector = outwardOf(wall);
|
|
1875
|
-
const
|
|
2007
|
+
const centre2 = wall.axis === "u" ? (wall.u0 + wall.u1) / 2 : (wall.v0 + wall.v1) / 2;
|
|
1876
2008
|
const offset = HALF_WALL + 0.08;
|
|
1877
|
-
return { id: "inverter", u: round22(wall.axis === "u" ?
|
|
2009
|
+
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" };
|
|
1878
2010
|
}
|
|
1879
2011
|
function planSolar(context) {
|
|
1880
2012
|
const { plan, rng, energy } = context;
|
|
@@ -2082,6 +2214,8 @@ function addOutbuildingPaths(lot, outbuildings, items) {
|
|
|
2082
2214
|
|
|
2083
2215
|
// src/water-tank.ts
|
|
2084
2216
|
var TANK_SIDE = 1.1;
|
|
2217
|
+
var TANK_PILLAR_SIDE = 0.2;
|
|
2218
|
+
var VISOR_TANK_PILLAR_SIDE = 0.12;
|
|
2085
2219
|
var NOT_UNDER_TANK = ["void", "elevator", "stairs", "balcony", "porch"];
|
|
2086
2220
|
var WET_ROOMS = ["bathroom", "kitchen", "laundry"];
|
|
2087
2221
|
var ATTIC_CHANCE = 0.12;
|
|
@@ -2091,26 +2225,27 @@ function capacityOf(source, capacities) {
|
|
|
2091
2225
|
return capacities[source.style ?? ""] ?? capacities[source.template ?? ""] ?? 2e3;
|
|
2092
2226
|
}
|
|
2093
2227
|
function bestSpot(source, floor, reference) {
|
|
2094
|
-
const
|
|
2228
|
+
const centre2 = reference ? [reference.u, reference.v] : [source.w / 2, source.d / 2];
|
|
2095
2229
|
let best;
|
|
2096
2230
|
for (const room of source.rooms.filter((r) => r.floor === floor && !NOT_UNDER_TANK.includes(r.type))) {
|
|
2097
2231
|
for (const part of room.parts) {
|
|
2098
2232
|
const rect = [part[0] + 0.4, part[1] + 0.4, part[2] - 0.4, part[3] - 0.4];
|
|
2099
2233
|
if (rect[2] - rect[0] < TANK_SIDE || rect[3] - rect[1] < TANK_SIDE) continue;
|
|
2100
2234
|
const base = WET_ROOMS.includes(room.type) ? 0 : room.type === "terrace" ? 500 : 100;
|
|
2101
|
-
const weight = base + Math.hypot((rect[0] + rect[2]) / 2 -
|
|
2235
|
+
const weight = base + Math.hypot((rect[0] + rect[2]) / 2 - centre2[0], (rect[1] + rect[3]) / 2 - centre2[1]);
|
|
2102
2236
|
if (!best || weight < best.weight) best = { roomType: room.type, roomId: room.id, rect, weight };
|
|
2103
2237
|
}
|
|
2104
2238
|
}
|
|
2105
2239
|
return best;
|
|
2106
2240
|
}
|
|
2107
|
-
function raisedPlatform(source,
|
|
2241
|
+
function raisedPlatform(source, spot, side) {
|
|
2242
|
+
const { u, v, baseY } = spot;
|
|
2108
2243
|
const margin = 0.12;
|
|
2109
2244
|
const edge = Math.max(0, Math.min(u, source.w - u, v, source.d - v));
|
|
2110
2245
|
const rise = round2(clamp2(edge * 0.35 + 0.7, 1.1, 2.4));
|
|
2111
2246
|
const thickness = 0.12;
|
|
2112
|
-
const reach = TANK_SIDE / 2 + margin -
|
|
2113
|
-
const pillars = [[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([a, b]) => ({ u: round2(u + a * reach), v: round2(v + b * reach), side
|
|
2247
|
+
const reach = TANK_SIDE / 2 + margin - side / 2;
|
|
2248
|
+
const pillars = [[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([a, b]) => ({ u: round2(u + a * reach), v: round2(v + b * reach), side }));
|
|
2114
2249
|
const half = TANK_SIDE / 2 + margin;
|
|
2115
2250
|
return { aabb: [u - half, v - half, u + half, v + half].map(round2), baseY, height: rise, thickness, topY: round2(baseY + rise + thickness), pillars, ladder: true };
|
|
2116
2251
|
}
|
|
@@ -2147,7 +2282,7 @@ function planWaterTank(source, rng, options = {}) {
|
|
|
2147
2282
|
aabb: [u - TANK_SIDE / 2, v - TANK_SIDE / 2, u + TANK_SIDE / 2, v + TANK_SIDE / 2].map(round2)
|
|
2148
2283
|
};
|
|
2149
2284
|
if (where === "raised_slab") {
|
|
2150
|
-
tank.platform = raisedPlatform(source, u, v, slabY);
|
|
2285
|
+
tank.platform = raisedPlatform(source, { u, v, baseY: slabY }, options.pillarSide ?? TANK_PILLAR_SIDE);
|
|
2151
2286
|
tank.y = tank.platform.topY;
|
|
2152
2287
|
}
|
|
2153
2288
|
return tank;
|
|
@@ -2252,7 +2387,7 @@ function constructionExtension(context) {
|
|
|
2252
2387
|
}
|
|
2253
2388
|
function structureExtension(context) {
|
|
2254
2389
|
const cache = cacheOf(context.result);
|
|
2255
|
-
const options = { construction: cache.build.construction, extras: cache.build.extras ?? {}, tank: tankOf(context), wallDetail: context.options.wallDetail ?? "panels" };
|
|
2390
|
+
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 } : {} };
|
|
2256
2391
|
const { roof, ...structure } = buildStructure(context.result, options);
|
|
2257
2392
|
cache.roof = roof;
|
|
2258
2393
|
for (const part of structure.parts) {
|
|
@@ -2276,7 +2411,7 @@ function register(engine, options = {}) {
|
|
|
2276
2411
|
engine.registerResultExtension(
|
|
2277
2412
|
"waterTank",
|
|
2278
2413
|
(context) => {
|
|
2279
|
-
const tank = { capacities: options.tankCapacities, custom: context.plan.tpl.tank, location: context.plan.tpl.tankLocation };
|
|
2414
|
+
const tank = { capacities: options.tankCapacities, custom: context.plan.tpl.tank, location: context.plan.tpl.tankLocation, ...context.options.visorSections === true ? { pillarSide: VISOR_TANK_PILLAR_SIDE } : {} };
|
|
2280
2415
|
return planWaterTank(context.result, context.rng, tank);
|
|
2281
2416
|
},
|
|
2282
2417
|
{ after: ["construction"], seedKey: "caixa" }
|
|
@@ -2971,6 +3106,8 @@ function splitIntoCourses(wall, openings, spec) {
|
|
|
2971
3106
|
// src/wall-detail/panels.ts
|
|
2972
3107
|
var LINTEL_DEPTH2 = 0.2;
|
|
2973
3108
|
var LINTEL_BEARING2 = 0.2;
|
|
3109
|
+
var SILL_BEAM_DEPTH = 0.1;
|
|
3110
|
+
var LINTEL_SPAN_RATIO = 10;
|
|
2974
3111
|
var MIN_PIECE = 0.05;
|
|
2975
3112
|
var frameOf = (wall) => {
|
|
2976
3113
|
const alongU = wall.aabb[2] - wall.aabb[0] >= wall.aabb[3] - wall.aabb[1];
|
|
@@ -2984,6 +3121,7 @@ function splitIntoPanels(wall, openings, style) {
|
|
|
2984
3121
|
const drafts = [];
|
|
2985
3122
|
const piers = [];
|
|
2986
3123
|
const lintels = [];
|
|
3124
|
+
const sillBeams = [];
|
|
2987
3125
|
const spandrels = [];
|
|
2988
3126
|
const height = wall.y1 - wall.y0;
|
|
2989
3127
|
let cursor = frame.from;
|
|
@@ -2994,23 +3132,26 @@ function splitIntoPanels(wall, openings, style) {
|
|
|
2994
3132
|
};
|
|
2995
3133
|
own.forEach((opening2, k) => {
|
|
2996
3134
|
addPier(Math.max(cursor, opening2.from));
|
|
2997
|
-
const
|
|
3135
|
+
const span = opening2.to - opening2.from;
|
|
3136
|
+
const lintelTop = Math.min(wall.y1, opening2.y1 + Math.max(LINTEL_DEPTH2, style.lintelDepth?.(span) ?? 0, (span + LINTEL_BEARING2) / LINTEL_SPAN_RATIO));
|
|
2998
3137
|
const lintelFrom = Math.max(frame.from, opening2.from - LINTEL_BEARING2);
|
|
2999
3138
|
const lintelTo = Math.min(frame.to, opening2.to + LINTEL_BEARING2);
|
|
3000
3139
|
const lintelHeight = lintelTop - opening2.y1;
|
|
3001
|
-
|
|
3140
|
+
const sillTop = style.sillBeam && opening2.y0 - wall.y0 > SILL_BEAM_DEPTH + MIN_PIECE ? opening2.y0 - SILL_BEAM_DEPTH : opening2.y0;
|
|
3141
|
+
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) });
|
|
3142
|
+
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 });
|
|
3002
3143
|
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 });
|
|
3003
3144
|
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);
|
|
3004
3145
|
cursor = opening2.to;
|
|
3005
3146
|
});
|
|
3006
3147
|
addPier(frame.to);
|
|
3007
3148
|
for (const pier of piers) {
|
|
3008
|
-
for (const lintel of lintels) {
|
|
3149
|
+
for (const lintel of [...lintels, ...sillBeams]) {
|
|
3009
3150
|
const overlap = Math.max(0, Math.min(pier.to, lintel.to) - Math.max(pier.from, lintel.from));
|
|
3010
3151
|
pier.volume -= overlap * (lintel.y1 - lintel.y0);
|
|
3011
3152
|
}
|
|
3012
3153
|
}
|
|
3013
|
-
const all = [...piers, ...drafts, ...lintels, ...spandrels.filter((d) => d !== void 0)];
|
|
3154
|
+
const all = [...piers, ...drafts, ...sillBeams, ...lintels, ...spandrels.filter((d) => d !== void 0)];
|
|
3014
3155
|
const total = all.reduce((sum, d) => sum + d.volume, 0);
|
|
3015
3156
|
const pieces = all.map((d) => ({
|
|
3016
3157
|
id: d.id,
|
|
@@ -3025,6 +3166,11 @@ function splitIntoPanels(wall, openings, style) {
|
|
|
3025
3166
|
...d.role === "beam" ? style.lintelAssembly ? { assembly: style.lintelAssembly } : {} : wall.assembly ? { assembly: wall.assembly } : {}
|
|
3026
3167
|
}));
|
|
3027
3168
|
const internal = [];
|
|
3169
|
+
for (const beam of sillBeams) {
|
|
3170
|
+
const sill = drafts.find((d) => d.id === beam.id.replace("_sillbeam_", "_sill_"));
|
|
3171
|
+
if (sill) internal.push({ part: beam.id, on: sill.id, joint: style.lintelJoint });
|
|
3172
|
+
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 });
|
|
3173
|
+
}
|
|
3028
3174
|
lintels.forEach((lintel, k) => {
|
|
3029
3175
|
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 });
|
|
3030
3176
|
const spandrel = spandrels[k];
|
|
@@ -3056,7 +3202,8 @@ function panelSplits(input, structure, lintelDepth) {
|
|
|
3056
3202
|
...system.timber ? {} : { lintelAssembly: "reinforced_concrete" },
|
|
3057
3203
|
lintelJoint: system.joints.beam,
|
|
3058
3204
|
wallJoint: system.joints.wall,
|
|
3059
|
-
...lintelDepth && !system.timber ? { lintelDepth } : {}
|
|
3205
|
+
...lintelDepth && !system.timber ? { lintelDepth } : {},
|
|
3206
|
+
...construction.system === "masonry" ? { sillBeam: true } : {}
|
|
3060
3207
|
});
|
|
3061
3208
|
if (split.pieces.length > 1) splits.set(part.id, split);
|
|
3062
3209
|
}
|
|
@@ -3064,7 +3211,18 @@ function panelSplits(input, structure, lintelDepth) {
|
|
|
3064
3211
|
}
|
|
3065
3212
|
function applyPanels(input, structure, detail, lintelDepth) {
|
|
3066
3213
|
if (detail === "coarse") return input;
|
|
3067
|
-
return applySplits(input, panelSplits(input, structure, lintelDepth));
|
|
3214
|
+
return onJambPillars(applySplits(input, panelSplits(input, structure, lintelDepth)));
|
|
3215
|
+
}
|
|
3216
|
+
var OPENING_BEAM = /_(lintel|sillbeam)_\d+$/;
|
|
3217
|
+
var overlaps2 = (a, b) => a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1];
|
|
3218
|
+
function onJambPillars(split) {
|
|
3219
|
+
const pillars = split.parts.filter((part) => part.role === "column" && part.material === "concrete");
|
|
3220
|
+
const extra = [];
|
|
3221
|
+
for (const beam of split.parts) {
|
|
3222
|
+
if (beam.role !== "beam" || !OPENING_BEAM.test(beam.id)) continue;
|
|
3223
|
+
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" });
|
|
3224
|
+
}
|
|
3225
|
+
return extra.length ? { parts: split.parts, supports: [...split.supports, ...extra] } : split;
|
|
3068
3226
|
}
|
|
3069
3227
|
function applyUnits(input, structure) {
|
|
3070
3228
|
const splits = /* @__PURE__ */ new Map();
|
|
@@ -3128,21 +3286,35 @@ var assemblySpec = (part, capacity) => {
|
|
|
3128
3286
|
};
|
|
3129
3287
|
var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
|
|
3130
3288
|
var CAGED_ROLES = /* @__PURE__ */ new Set(["beam", "column"]);
|
|
3289
|
+
var CAGE_COVER = 0.03;
|
|
3290
|
+
var CAGE_BUCKLING_SHARE = 0.5;
|
|
3291
|
+
var DEFAULT_RESIDUAL = 0.15;
|
|
3292
|
+
function cageResidualCapacity(size, reinforcement, capacity) {
|
|
3293
|
+
const [a = 0, b = 0] = [...size].sort((x, y) => x - y);
|
|
3294
|
+
const perSide = (w) => Math.max(2, Math.floor(Math.max(0, w - 2 * CAGE_COVER) / reinforcement.spacing) + 1);
|
|
3295
|
+
const bars = 2 * perSide(a) + 2 * perSide(b) - 4;
|
|
3296
|
+
const steel = MATERIALS.find((m) => m.id === reinforcement.material)?.compressive ?? 0;
|
|
3297
|
+
const carries2 = bars * Math.PI * (reinforcement.diameter / 2) ** 2 * steel * CAGE_BUCKLING_SHARE;
|
|
3298
|
+
if (!capacity || capacity <= 0 || carries2 <= 0) return DEFAULT_RESIDUAL;
|
|
3299
|
+
return Math.min(1, Math.max(DEFAULT_RESIDUAL, carries2 / capacity));
|
|
3300
|
+
}
|
|
3131
3301
|
var caged = (spec, part, rebar) => {
|
|
3132
3302
|
if (!rebar || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
|
|
3133
3303
|
const size = spec.shape.kind === "box" ? spec.shape.size : null;
|
|
3134
3304
|
if (!size) return spec;
|
|
3135
3305
|
const thin = Math.min(...size);
|
|
3306
|
+
const reinforcement = { material: rebar, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter };
|
|
3136
3307
|
return {
|
|
3137
3308
|
...spec,
|
|
3138
|
-
reinforcement: {
|
|
3309
|
+
reinforcement: part.role === "column" ? { ...reinforcement, residualCapacity: cageResidualCapacity(size, reinforcement, spec.structure?.capacity) } : reinforcement,
|
|
3139
3310
|
units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
|
|
3140
3311
|
};
|
|
3141
3312
|
};
|
|
3142
3313
|
var partSpecOf = (part, capacity, detailed, rebar) => {
|
|
3143
3314
|
if (detailed && part.assembly) return assemblySpec(part, capacity);
|
|
3144
3315
|
const spec = boxSpec(part, capacity);
|
|
3145
|
-
|
|
3316
|
+
const out = detailed ? caged(spec, part, rebar) : spec;
|
|
3317
|
+
return part.beamKind === "ground_beam" ? { ...out, anchored: true, structure: { role: "foundation" } } : out;
|
|
3146
3318
|
};
|
|
3147
3319
|
function toDestructibleSpec(structure, options = {}) {
|
|
3148
3320
|
const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
|
|
@@ -3158,7 +3330,9 @@ function toDestructibleSpec(structure, options = {}) {
|
|
|
3158
3330
|
const walled = applyPanels(slabbed, structure, detail, slabOptions && structure.construction.frame.rebar ? (span) => beamDepthFor(span, slabOptions) : void 0);
|
|
3159
3331
|
const panels = slabOptions && detail !== "coarse" ? deepenBondBeams(walled, structure, slabOptions) : walled;
|
|
3160
3332
|
const sizing = { safety, redundancy, safetyOthers };
|
|
3161
|
-
const
|
|
3333
|
+
const foundation = new Set(panels.parts.filter((part) => part.beamKind === "ground_beam").map((part) => part.id));
|
|
3334
|
+
const onFoundation = foundation.size ? panels.supports.filter((s) => !foundation.has(s.part)).map((s) => foundation.has(s.on) ? { ...s, on: GROUND } : s) : panels.supports;
|
|
3335
|
+
const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts.filter((part) => !foundation.has(part.id)), onFoundation, sizing) };
|
|
3162
3336
|
const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
|
|
3163
3337
|
const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
|
|
3164
3338
|
const margin = options.terrainMargin ?? TERRAIN_MARGIN;
|
|
@@ -3224,8 +3398,362 @@ function onSoil(spec, structure, options) {
|
|
|
3224
3398
|
return out;
|
|
3225
3399
|
}
|
|
3226
3400
|
|
|
3401
|
+
// src/validate/codes.ts
|
|
3402
|
+
var STRUCTURAL_VIOLATION = {
|
|
3403
|
+
/** A pillar with no load path to the ground, or that starts above what it rests on. */
|
|
3404
|
+
PILLAR_NOT_GROUNDED: "PILLAR_NOT_GROUNDED",
|
|
3405
|
+
/** A pillar of a masonry wall that stops short of the bond beam, or that the bond beam does not rest on. */
|
|
3406
|
+
PILLAR_DOES_NOT_REACH_BOND_BEAM: "PILLAR_DOES_NOT_REACH_BOND_BEAM",
|
|
3407
|
+
/** A masonry wall that runs more than `MAX_PILLAR_SPACING` between two of its pillars. */
|
|
3408
|
+
WALL_SPAN_WITHOUT_PILLAR: "WALL_SPAN_WITHOUT_PILLAR",
|
|
3409
|
+
/** A free end of a masonry wall (a door jamb, the end of a garden wall) with no pillar. */
|
|
3410
|
+
WALL_END_WITHOUT_PILLAR: "WALL_END_WITHOUT_PILLAR",
|
|
3411
|
+
/** Two masonry walls meet at a corner with no pillar there. */
|
|
3412
|
+
CORNER_WITHOUT_PILLAR: "CORNER_WITHOUT_PILLAR",
|
|
3413
|
+
/** Another wall meets a masonry wall inside its length (a T) with no pillar there. */
|
|
3414
|
+
WALL_JUNCTION_WITHOUT_PILLAR: "WALL_JUNCTION_WITHOUT_PILLAR",
|
|
3415
|
+
/** A window or door in a masonry wall with no lintel (verga) over it. */
|
|
3416
|
+
OPENING_WITHOUT_LINTEL: "OPENING_WITHOUT_LINTEL",
|
|
3417
|
+
/** A window in a masonry wall with no sill beam (contraverga) under it. */
|
|
3418
|
+
OPENING_WITHOUT_SILL: "OPENING_WITHOUT_SILL",
|
|
3419
|
+
/** An exterior masonry wall with no bond beam (cinta de amarração) on top. */
|
|
3420
|
+
MISSING_BOND_BEAM: "MISSING_BOND_BEAM",
|
|
3421
|
+
/** A ground-floor masonry wall standing on the soil with no ground beam (viga baldrame) or foundation under it. */
|
|
3422
|
+
MISSING_GROUND_BEAM: "MISSING_GROUND_BEAM",
|
|
3423
|
+
/** A part with no load path down to the ground. */
|
|
3424
|
+
FLOATING_PART: "FLOATING_PART",
|
|
3425
|
+
/** A concrete beam longer than a lintel that rests on masonry alone (no pillar, beam or slab carries it). */
|
|
3426
|
+
BEAM_ON_WEAK_MASONRY: "BEAM_ON_WEAK_MASONRY",
|
|
3427
|
+
/** A concrete pillar or beam below the NBR 6118 minimum section, or a beam between point supports shallower than a tenth of its span. */
|
|
3428
|
+
CONCRETE_BELOW_MIN_SECTION: "CONCRETE_BELOW_MIN_SECTION",
|
|
3429
|
+
/** A concrete pillar or beam with no steel (the construction has no rebar). */
|
|
3430
|
+
UNREINFORCED_CONCRETE: "UNREINFORCED_CONCRETE",
|
|
3431
|
+
/** A brick or block wall laid in stack bond: the vertical joints line up and the wall splits along them. */
|
|
3432
|
+
UNBONDED_MASONRY: "UNBONDED_MASONRY",
|
|
3433
|
+
/** A plastered brick wall without plaster on both faces. */
|
|
3434
|
+
MISSING_PLASTER: "MISSING_PLASTER"
|
|
3435
|
+
};
|
|
3436
|
+
var MAX_PILLAR_SPACING = 4;
|
|
3437
|
+
var MIN_PILLAR_SIDE = 0.19;
|
|
3438
|
+
var MIN_PILLAR_AREA = 0.036;
|
|
3439
|
+
var MIN_BEAM_WIDTH = 0.12;
|
|
3440
|
+
var BEAM_DEPTH_RATIO = 10;
|
|
3441
|
+
var MAX_LINTEL_ON_MASONRY = 3;
|
|
3442
|
+
|
|
3443
|
+
// src/validate/load-path.ts
|
|
3444
|
+
var CONTACT = 0.05;
|
|
3445
|
+
var EPSILON = 5e-3;
|
|
3446
|
+
function groundedParts(parts, supports) {
|
|
3447
|
+
const ids = new Set(parts.map((part) => part.id));
|
|
3448
|
+
const grounded = /* @__PURE__ */ new Set([GROUND]);
|
|
3449
|
+
const bearing = supports.filter((s) => s.bears !== false && ids.has(s.part));
|
|
3450
|
+
let grew = true;
|
|
3451
|
+
while (grew) {
|
|
3452
|
+
grew = false;
|
|
3453
|
+
for (const support of bearing) {
|
|
3454
|
+
if (grounded.has(support.part) || !grounded.has(support.on)) continue;
|
|
3455
|
+
grounded.add(support.part);
|
|
3456
|
+
grew = true;
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
return grounded;
|
|
3460
|
+
}
|
|
3461
|
+
function loadPathViolations(structure) {
|
|
3462
|
+
const { parts, supports } = structure;
|
|
3463
|
+
const grounded = groundedParts(parts, supports);
|
|
3464
|
+
const byId = new Map(parts.map((part) => [part.id, part]));
|
|
3465
|
+
const out = [];
|
|
3466
|
+
for (const part of parts) {
|
|
3467
|
+
if (part.role === "none") continue;
|
|
3468
|
+
const pillar = part.role === "column";
|
|
3469
|
+
if (!grounded.has(part.id)) {
|
|
3470
|
+
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` });
|
|
3471
|
+
continue;
|
|
3472
|
+
}
|
|
3473
|
+
if (!pillar) continue;
|
|
3474
|
+
const bases = supports.filter((s) => s.part === part.id && s.bears !== false);
|
|
3475
|
+
const standsOnBase = bases.some((s) => {
|
|
3476
|
+
if (s.on === GROUND) return part.y0 <= CONTACT;
|
|
3477
|
+
const base = byId.get(s.on);
|
|
3478
|
+
return base !== void 0 && part.y0 >= base.y0 - EPSILON && part.y0 <= base.y1 + CONTACT;
|
|
3479
|
+
});
|
|
3480
|
+
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` });
|
|
3481
|
+
}
|
|
3482
|
+
return out;
|
|
3483
|
+
}
|
|
3484
|
+
var alongOf2 = (part) => {
|
|
3485
|
+
const alongU = part.aabb[2] - part.aabb[0] >= part.aabb[3] - part.aabb[1];
|
|
3486
|
+
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] };
|
|
3487
|
+
};
|
|
3488
|
+
function bearingsOf(beam, supports, byId) {
|
|
3489
|
+
const frame = alongOf2(beam);
|
|
3490
|
+
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) => {
|
|
3491
|
+
const [low, high] = frame.alongU ? [on.aabb[0], on.aabb[2]] : [on.aabb[1], on.aabb[3]];
|
|
3492
|
+
return { on, from: Math.max(frame.from, low), to: Math.min(frame.to, high) };
|
|
3493
|
+
}).filter((bearing) => bearing.to - bearing.from > -EPSILON);
|
|
3494
|
+
}
|
|
3495
|
+
var isWallLike = (part) => part.role === "wall" || part.role === "partition";
|
|
3496
|
+
var PEDESTAL_RATIO = 3;
|
|
3497
|
+
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]);
|
|
3498
|
+
function concreteViolations(structure, isMasonry) {
|
|
3499
|
+
const { parts, supports } = structure;
|
|
3500
|
+
const byId = new Map(parts.map((part) => [part.id, part]));
|
|
3501
|
+
const out = [];
|
|
3502
|
+
for (const part of parts) {
|
|
3503
|
+
if (part.material !== "concrete" || part.role !== "column" && part.role !== "beam") continue;
|
|
3504
|
+
if (!structure.construction.frame.rebar && !isPedestal(part)) out.push({ code: STRUCTURAL_VIOLATION.UNREINFORCED_CONCRETE, part: part.id, message: `${part.id} is concrete without steel` });
|
|
3505
|
+
if (part.role === "column") {
|
|
3506
|
+
const sides = [part.aabb[2] - part.aabb[0], part.aabb[3] - part.aabb[1]];
|
|
3507
|
+
if (Math.min(...sides) < MIN_PILLAR_SIDE - EPSILON || sides[0] * sides[1] < MIN_PILLAR_AREA - EPSILON)
|
|
3508
|
+
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` });
|
|
3509
|
+
continue;
|
|
3510
|
+
}
|
|
3511
|
+
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);
|
|
3512
|
+
out.push(...beamViolations(part, bearingsOf(part, supports, byId), { isMasonry, castOn }));
|
|
3513
|
+
}
|
|
3514
|
+
return out;
|
|
3515
|
+
}
|
|
3516
|
+
function effectiveSpan(bearings, depth) {
|
|
3517
|
+
const sorted = [...bearings].sort((a, b) => a.from - b.from);
|
|
3518
|
+
let most = 0;
|
|
3519
|
+
for (let k = 1; k < sorted.length; k++) {
|
|
3520
|
+
const previous = sorted.slice(0, k).reduce((a, b) => b.to > a.to ? b : a);
|
|
3521
|
+
const next = sorted[k];
|
|
3522
|
+
const clear = next.from - previous.to;
|
|
3523
|
+
if (clear <= 0) continue;
|
|
3524
|
+
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));
|
|
3525
|
+
}
|
|
3526
|
+
return most;
|
|
3527
|
+
}
|
|
3528
|
+
function beamViolations(beam, bearings, context) {
|
|
3529
|
+
const out = [];
|
|
3530
|
+
const frame = alongOf2(beam);
|
|
3531
|
+
const length = frame.to - frame.from;
|
|
3532
|
+
const depth = beam.y1 - beam.y0 + context.castOn;
|
|
3533
|
+
if (frame.width < MIN_BEAM_WIDTH - EPSILON) out.push({ code: STRUCTURAL_VIOLATION.CONCRETE_BELOW_MIN_SECTION, part: beam.id, message: `${beam.id} is ${frame.width.toFixed(2)} m wide: NBR 6118 asks at least ${MIN_BEAM_WIDTH} m` });
|
|
3534
|
+
if (!bearings.length) return out;
|
|
3535
|
+
const continuous = bearings.some((b) => isWallLike(b.on) && b.to - b.from >= length / 2);
|
|
3536
|
+
const span = continuous ? 0 : effectiveSpan(bearings, depth);
|
|
3537
|
+
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` });
|
|
3538
|
+
if (length > MAX_LINTEL_ON_MASONRY && bearings.every((b) => context.isMasonry(b.on)))
|
|
3539
|
+
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` });
|
|
3540
|
+
return out;
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
// src/validate/masonry.ts
|
|
3544
|
+
import { getAssembly as getAssembly2 } from "@adatechnology/game-destruction";
|
|
3545
|
+
var TOLERANCE3 = 0.05;
|
|
3546
|
+
var AT_POINT = 0.45;
|
|
3547
|
+
var EXPOSED_FINISHES = ["exposed_brick", "exposed_block"];
|
|
3548
|
+
var lineOf2 = (part) => {
|
|
3549
|
+
const alongU = part.aabb[2] - part.aabb[0] >= part.aabb[3] - part.aabb[1];
|
|
3550
|
+
const half = (alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0]) / 2;
|
|
3551
|
+
const [from, to] = alongU ? [part.aabb[0], part.aabb[2]] : [part.aabb[1], part.aabb[3]];
|
|
3552
|
+
return { part, alongU, line: alongU ? part.aabb[1] + half : part.aabb[0] + half, from: from + half, to: to - half, half };
|
|
3553
|
+
};
|
|
3554
|
+
function isMasonryWall(part) {
|
|
3555
|
+
if (!part.id.startsWith("wall_") || !part.assembly || part.role !== "wall" && part.role !== "partition") return false;
|
|
3556
|
+
const units = getAssembly2(part.assembly)?.units;
|
|
3557
|
+
return units !== void 0 && units.mortar !== "concrete";
|
|
3558
|
+
}
|
|
3559
|
+
var pointOf2 = (wall, along) => wall.alongU ? [along, wall.line] : [wall.line, along];
|
|
3560
|
+
var alongOfPoint = (wall, point) => wall.alongU ? point[0] : point[1];
|
|
3561
|
+
var acrossOfPoint = (wall, point) => wall.alongU ? point[1] : point[0];
|
|
3562
|
+
var centre = (part) => [(part.aabb[0] + part.aabb[2]) / 2, (part.aabb[1] + part.aabb[3]) / 2];
|
|
3563
|
+
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;
|
|
3564
|
+
function endKind(wall, along, others) {
|
|
3565
|
+
const point = pointOf2(wall, along);
|
|
3566
|
+
const meeting = others.filter((other) => onLine(other, point));
|
|
3567
|
+
const across = meeting.filter((other) => other.alongU !== wall.alongU);
|
|
3568
|
+
const continued = meeting.length > across.length;
|
|
3569
|
+
if (!across.length) return continued ? "continued" : "free";
|
|
3570
|
+
const atEnd = (other) => Math.min(Math.abs(alongOfPoint(other, point) - other.from), Math.abs(alongOfPoint(other, point) - other.to)) <= other.half + wall.half + TOLERANCE3;
|
|
3571
|
+
return !continued && across.every(atEnd) ? "corner" : "junction";
|
|
3572
|
+
}
|
|
3573
|
+
var masonryBetween = (from, to, windows) => windows.reduce((left, [a, b]) => left - Math.max(0, Math.min(to, b) - Math.max(from, a)), to - from);
|
|
3574
|
+
function pointViolations(context, others) {
|
|
3575
|
+
const { wall, stations } = context;
|
|
3576
|
+
const out = [];
|
|
3577
|
+
const covered = (along) => stations.some((at2) => Math.abs(at2 - along) <= AT_POINT);
|
|
3578
|
+
for (const along of [wall.from, wall.to]) {
|
|
3579
|
+
const kind = endKind(wall, along, others);
|
|
3580
|
+
if (kind === "continued" || covered(along)) continue;
|
|
3581
|
+
const code = kind === "corner" ? STRUCTURAL_VIOLATION.CORNER_WITHOUT_PILLAR : kind === "junction" ? STRUCTURAL_VIOLATION.WALL_JUNCTION_WITHOUT_PILLAR : STRUCTURAL_VIOLATION.WALL_END_WITHOUT_PILLAR;
|
|
3582
|
+
out.push({ code, part: wall.part.id, message: `${wall.part.id} has no pillar at its ${kind} end (${along.toFixed(2)} m along)` });
|
|
3583
|
+
}
|
|
3584
|
+
for (const other of others) {
|
|
3585
|
+
if (other.alongU === wall.alongU) continue;
|
|
3586
|
+
for (const end of [other.from, other.to]) {
|
|
3587
|
+
const point = pointOf2(other, end);
|
|
3588
|
+
const at2 = alongOfPoint(wall, point);
|
|
3589
|
+
if (!onLine(wall, point) || at2 <= wall.from + AT_POINT || at2 >= wall.to - AT_POINT || covered(at2)) continue;
|
|
3590
|
+
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` });
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
const line = [...stations].sort((a, b) => a - b);
|
|
3594
|
+
for (let k = 1; k < line.length; k++) {
|
|
3595
|
+
const gap = masonryBetween(line[k - 1], line[k], context.windows);
|
|
3596
|
+
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)` });
|
|
3597
|
+
}
|
|
3598
|
+
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` });
|
|
3599
|
+
return out;
|
|
3600
|
+
}
|
|
3601
|
+
function bondBeamViolations(context, structure) {
|
|
3602
|
+
const { wall, pillars } = context;
|
|
3603
|
+
const resting = new Set(structure.supports.filter((s) => s.on === wall.part.id).map((s) => s.part));
|
|
3604
|
+
const beams = structure.parts.filter((p) => p.beamKind === "bond_beam" && resting.has(p.id));
|
|
3605
|
+
if (!beams.length) return [{ code: STRUCTURAL_VIOLATION.MISSING_BOND_BEAM, part: wall.part.id, message: `${wall.part.id} has no bond beam on top` }];
|
|
3606
|
+
const out = [];
|
|
3607
|
+
for (const pillar of pillars) {
|
|
3608
|
+
const [u, v] = centre(pillar);
|
|
3609
|
+
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);
|
|
3610
|
+
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));
|
|
3611
|
+
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}` });
|
|
3612
|
+
}
|
|
3613
|
+
return out;
|
|
3614
|
+
}
|
|
3615
|
+
function assemblyViolations(part, finish) {
|
|
3616
|
+
const def = getAssembly2(part.assembly ?? "");
|
|
3617
|
+
if (!def) return [];
|
|
3618
|
+
const out = [];
|
|
3619
|
+
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` });
|
|
3620
|
+
const plastered = (def.layers ?? []).some((layer) => layer.material === "plaster" && layer.faces === "both");
|
|
3621
|
+
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` });
|
|
3622
|
+
return out;
|
|
3623
|
+
}
|
|
3624
|
+
function masonryViolations(structure) {
|
|
3625
|
+
const walls = structure.parts.filter(isMasonryWall);
|
|
3626
|
+
const columns = structure.parts.filter((p) => p.role === "column" && p.material === "concrete");
|
|
3627
|
+
const out = [];
|
|
3628
|
+
for (const part of walls) {
|
|
3629
|
+
out.push(...assemblyViolations(part, structure.construction.walls.finish));
|
|
3630
|
+
if (part.floor === 0 && structure.supports.some((s) => s.part === part.id && s.on === GROUND))
|
|
3631
|
+
out.push({ code: STRUCTURAL_VIOLATION.MISSING_GROUND_BEAM, part: part.id, message: `${part.id} stands on the soil with no ground beam under it` });
|
|
3632
|
+
if (!part.exterior) continue;
|
|
3633
|
+
const wall = lineOf2(part);
|
|
3634
|
+
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);
|
|
3635
|
+
const windows = structure.openings.filter((o) => o.wall === part.id).map((o) => [o.from, o.to]);
|
|
3636
|
+
const context = { wall, pillars, stations: pillars.map((p) => alongOfPoint(wall, centre(p))), windows };
|
|
3637
|
+
const others = walls.filter((w) => w !== part && w.floor === part.floor).map(lineOf2);
|
|
3638
|
+
out.push(...pointViolations(context, others), ...bondBeamViolations(context, structure));
|
|
3639
|
+
}
|
|
3640
|
+
return out;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
// src/validate/openings.ts
|
|
3644
|
+
var TOLERANCE4 = 0.05;
|
|
3645
|
+
var LOW_WINDOW = SILL_BEAM_DEPTH + 0.05;
|
|
3646
|
+
function beamAcross(parts, wall, span, side, y) {
|
|
3647
|
+
return parts.some((part) => {
|
|
3648
|
+
const off = part[side] - y;
|
|
3649
|
+
if (part.role !== "beam" || off < -TOLERANCE4 || off > (side === "y0" ? LINTEL_DEPTH2 : TOLERANCE4)) return false;
|
|
3650
|
+
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]];
|
|
3651
|
+
return from <= span.from + TOLERANCE4 && to >= span.to - TOLERANCE4 && low <= wall.line + TOLERANCE4 && high >= wall.line - TOLERANCE4;
|
|
3652
|
+
});
|
|
3653
|
+
}
|
|
3654
|
+
function windowViolations(opening2, wall, built) {
|
|
3655
|
+
const line = lineOf2(wall);
|
|
3656
|
+
const out = [];
|
|
3657
|
+
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` });
|
|
3658
|
+
if (opening2.y0 - wall.y0 > LOW_WINDOW && !beamAcross(built, line, opening2, "y1", opening2.y0))
|
|
3659
|
+
out.push({ code: STRUCTURAL_VIOLATION.OPENING_WITHOUT_SILL, part: opening2.id, message: `window ${opening2.id} in ${wall.id} has no sill beam under it` });
|
|
3660
|
+
return out;
|
|
3661
|
+
}
|
|
3662
|
+
function wallsBeside(door, walls) {
|
|
3663
|
+
const along = door.axis === "u" ? door.u : door.v;
|
|
3664
|
+
const across = door.axis === "u" ? door.v : door.u;
|
|
3665
|
+
const from = along - door.width / 2;
|
|
3666
|
+
const to = along + door.width / 2;
|
|
3667
|
+
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));
|
|
3668
|
+
}
|
|
3669
|
+
function openingViolations(structure, built, doors) {
|
|
3670
|
+
const walls = structure.parts.filter(isMasonryWall);
|
|
3671
|
+
const hosts = new Map(walls.map((wall) => [wall.id, wall]));
|
|
3672
|
+
const out = [];
|
|
3673
|
+
for (const opening2 of structure.openings) {
|
|
3674
|
+
const wall = hosts.get(opening2.wall);
|
|
3675
|
+
if (wall) out.push(...windowViolations(opening2, wall, built));
|
|
3676
|
+
}
|
|
3677
|
+
for (const door of doors) {
|
|
3678
|
+
const beside = wallsBeside(door, walls);
|
|
3679
|
+
if (!beside.length) continue;
|
|
3680
|
+
const along = door.axis === "u" ? door.u : door.v;
|
|
3681
|
+
const span = { from: along - door.width / 2, to: along + door.width / 2 };
|
|
3682
|
+
const lintel = beamAcross(built, beside[0], span, "y0", (structure.levels[door.floor] ?? 0) + DOOR_HEIGHT);
|
|
3683
|
+
if (!lintel) out.push({ code: STRUCTURAL_VIOLATION.OPENING_WITHOUT_LINTEL, part: door.id, message: `door ${door.id} has no lintel over it` });
|
|
3684
|
+
}
|
|
3685
|
+
return out;
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
// src/validate/spec.ts
|
|
3689
|
+
var EPSILON2 = 5e-3;
|
|
3690
|
+
var carries = (part) => part?.structure !== void 0 && part.structure.role !== "none";
|
|
3691
|
+
function flows(joint, byId) {
|
|
3692
|
+
const bears = joint.bears ?? (carries(byId.get(joint.a)) && carries(byId.get(joint.b)));
|
|
3693
|
+
if (bears === false) return [];
|
|
3694
|
+
if (bears === "a_on_b") return [[joint.a, joint.b]];
|
|
3695
|
+
if (bears === "b_on_a") return [[joint.b, joint.a]];
|
|
3696
|
+
return [[joint.a, joint.b], [joint.b, joint.a]];
|
|
3697
|
+
}
|
|
3698
|
+
function groundedOf(spec, byId) {
|
|
3699
|
+
const grounded = new Set(spec.parts.filter((p) => p.anchored || p.structure?.role === "foundation").map((p) => p.id));
|
|
3700
|
+
const edges = (spec.joints ?? []).flatMap((joint) => flows(joint, byId));
|
|
3701
|
+
let grew = true;
|
|
3702
|
+
while (grew) {
|
|
3703
|
+
grew = false;
|
|
3704
|
+
for (const [part, on] of edges) {
|
|
3705
|
+
if (grounded.has(part) || !grounded.has(on)) continue;
|
|
3706
|
+
grounded.add(part);
|
|
3707
|
+
grew = true;
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
return grounded;
|
|
3711
|
+
}
|
|
3712
|
+
function sectionViolation(part) {
|
|
3713
|
+
if (part.material !== "concrete" || part.shape.kind !== "box") return null;
|
|
3714
|
+
const role = part.structure?.role;
|
|
3715
|
+
const [x, y, z] = part.shape.size;
|
|
3716
|
+
if (role === "column" && (Math.min(x, z) < MIN_PILLAR_SIDE - EPSILON2 || x * z < MIN_PILLAR_AREA - EPSILON2))
|
|
3717
|
+
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` };
|
|
3718
|
+
if (role === "beam" && Math.min(x, z) < MIN_BEAM_WIDTH - EPSILON2 && y >= Math.min(x, z))
|
|
3719
|
+
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` };
|
|
3720
|
+
return null;
|
|
3721
|
+
}
|
|
3722
|
+
function specViolations(spec) {
|
|
3723
|
+
const byId = new Map(spec.parts.map((part) => [part.id, part]));
|
|
3724
|
+
const grounded = groundedOf(spec, byId);
|
|
3725
|
+
const out = [];
|
|
3726
|
+
for (const part of spec.parts) {
|
|
3727
|
+
if (!carries(part)) continue;
|
|
3728
|
+
if (!grounded.has(part.id)) {
|
|
3729
|
+
const pillar = part.structure.role === "column";
|
|
3730
|
+
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` });
|
|
3731
|
+
}
|
|
3732
|
+
const section = sectionViolation(part);
|
|
3733
|
+
if (section) out.push(section);
|
|
3734
|
+
}
|
|
3735
|
+
return out;
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
// src/validate/index.ts
|
|
3739
|
+
function validateStructure(structure, options = {}) {
|
|
3740
|
+
const detail = options.wallDetail ?? "panels";
|
|
3741
|
+
const built = applyPanels({ parts: structure.parts, supports: structure.supports }, structure, detail);
|
|
3742
|
+
const out = [...loadPathViolations(structure), ...concreteViolations({ ...built, construction: structure.construction }, isMasonryWall)];
|
|
3743
|
+
if (structure.construction.system !== "masonry") return out;
|
|
3744
|
+
return [...out, ...masonryViolations(structure), ...openingViolations(structure, built.parts, options.doors ?? [])];
|
|
3745
|
+
}
|
|
3746
|
+
function validateSpec(spec) {
|
|
3747
|
+
return specViolations(spec);
|
|
3748
|
+
}
|
|
3749
|
+
function countViolations(violations) {
|
|
3750
|
+
const out = {};
|
|
3751
|
+
for (const violation of violations) out[violation.code] = (out[violation.code] ?? 0) + 1;
|
|
3752
|
+
return out;
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3227
3755
|
// src/wall-specimen.ts
|
|
3228
|
-
import { assemblyDepth, assemblyPart as assemblyPart2, getAssembly as
|
|
3756
|
+
import { assemblyDepth, assemblyPart as assemblyPart2, getAssembly as getAssembly3 } from "@adatechnology/game-destruction";
|
|
3229
3757
|
var SYSTEM_OF_ASSEMBLY = { log_wall: "log", wood_frame_wall: "wood_frame", sheet_metal_shack: "improvised" };
|
|
3230
3758
|
var DOOR_WIDTH2 = 0.9;
|
|
3231
3759
|
var WINDOW_WIDTH = 1;
|
|
@@ -3234,7 +3762,7 @@ function openingOf(kind, height) {
|
|
|
3234
3762
|
}
|
|
3235
3763
|
function buildWallSpecimen(options) {
|
|
3236
3764
|
const { assembly, w, h, opening: opening2 = null, detail = "panels" } = options;
|
|
3237
|
-
const def =
|
|
3765
|
+
const def = getAssembly3(assembly);
|
|
3238
3766
|
if (!def) throw new Error(`unknown assembly "${assembly}"`);
|
|
3239
3767
|
const system = SYSTEM_OF_ASSEMBLY[assembly] ?? "masonry";
|
|
3240
3768
|
const depth = assemblyDepth(def);
|
|
@@ -3414,7 +3942,7 @@ function addPunchedFacade(spec, g, options = {}) {
|
|
|
3414
3942
|
parts.push({ id, material: c.material, shape: box(size), position, structure: { role: "partition" }, tags: ["infill"] });
|
|
3415
3943
|
for (const o of on) rests(joints, id, o, "mortar");
|
|
3416
3944
|
};
|
|
3417
|
-
const bay = (side, k, along,
|
|
3945
|
+
const bay = (side, k, along, centre2, fixed, length, c1, c2) => {
|
|
3418
3946
|
const clear = length - g.columnSize;
|
|
3419
3947
|
const win = clear * c.window;
|
|
3420
3948
|
const pier = (clear - win) / 2;
|
|
@@ -3425,11 +3953,11 @@ function addPunchedFacade(spec, g, options = {}) {
|
|
|
3425
3953
|
wall(`infill_${f}_${side}_${k}_${name}`, size(u, h), [p[0], y, p[2]], on);
|
|
3426
3954
|
};
|
|
3427
3955
|
const id = (name) => `infill_${f}_${side}_${k}_${name}`;
|
|
3428
|
-
put("sill", clear, sill, yb + sill / 2,
|
|
3429
|
-
put("pl", pier, head - sill, yb + sill + (head - sill) / 2,
|
|
3430
|
-
put("pr", pier, head - sill, yb + sill + (head - sill) / 2,
|
|
3431
|
-
put("head", clear, room - head, yb + head + (room - head) / 2,
|
|
3432
|
-
const gp = at2(
|
|
3956
|
+
put("sill", clear, sill, yb + sill / 2, centre2, [c1, c2]);
|
|
3957
|
+
put("pl", pier, head - sill, yb + sill + (head - sill) / 2, centre2 - clear / 2 + pier / 2, [c1, id("sill")]);
|
|
3958
|
+
put("pr", pier, head - sill, yb + sill + (head - sill) / 2, centre2 + clear / 2 - pier / 2, [c2, id("sill")]);
|
|
3959
|
+
put("head", clear, room - head, yb + head + (room - head) / 2, centre2, [c1, c2]);
|
|
3960
|
+
const gp = at2(centre2);
|
|
3433
3961
|
const gid = `glass_${f}_${side}_${k}`;
|
|
3434
3962
|
parts.push({
|
|
3435
3963
|
id: gid,
|
|
@@ -3950,6 +4478,7 @@ var towerLocales = { en: { labels: en_default.labels }, "pt-BR": { labels: pt_BR
|
|
|
3950
4478
|
// src/index.ts
|
|
3951
4479
|
var version = "0.1.0";
|
|
3952
4480
|
export {
|
|
4481
|
+
BEAM_DEPTH_RATIO,
|
|
3953
4482
|
CONSTRUCTIONS,
|
|
3954
4483
|
CONSTRUCTION_SYSTEMS,
|
|
3955
4484
|
CORE_DEFAULTS,
|
|
@@ -3958,8 +4487,14 @@ export {
|
|
|
3958
4487
|
DENSITY,
|
|
3959
4488
|
FLAT_ROOF_DEFAULTS,
|
|
3960
4489
|
GROUND,
|
|
4490
|
+
GROUND_BEAM_DEPTH,
|
|
4491
|
+
MAX_LINTEL_ON_MASONRY,
|
|
4492
|
+
MAX_PILLAR_SPACING,
|
|
3961
4493
|
MECHANICAL_DEFAULTS,
|
|
3962
4494
|
MEMBER_CAGE,
|
|
4495
|
+
MIN_BEAM_WIDTH,
|
|
4496
|
+
MIN_PILLAR_AREA,
|
|
4497
|
+
MIN_PILLAR_SIDE,
|
|
3963
4498
|
PILE_AREA,
|
|
3964
4499
|
PILE_DEFAULTS,
|
|
3965
4500
|
PLATE_DEFAULTS,
|
|
@@ -3967,10 +4502,15 @@ export {
|
|
|
3967
4502
|
RAFT_THICKNESS,
|
|
3968
4503
|
REBAR_RATIOS,
|
|
3969
4504
|
ROOF_COVERINGS,
|
|
4505
|
+
SILL_BEAM_DEPTH,
|
|
3970
4506
|
SKYSCRAPER_DEFAULTS,
|
|
3971
4507
|
SKYSCRAPER_MAX_STOREYS,
|
|
3972
4508
|
SLAB_PANEL_DEFAULTS,
|
|
4509
|
+
STRUCTURAL_VIOLATION,
|
|
4510
|
+
TANK_PILLAR_SIDE,
|
|
3973
4511
|
TOWER_DEFAULTS,
|
|
4512
|
+
WALL_PILLAR_SIDE,
|
|
4513
|
+
WALL_PILLAR_SPACING,
|
|
3974
4514
|
addCore,
|
|
3975
4515
|
addCurtainWall,
|
|
3976
4516
|
addFlatRoof2 as addFlatRoof,
|
|
@@ -3984,16 +4524,19 @@ export {
|
|
|
3984
4524
|
buildStructure,
|
|
3985
4525
|
buildTower,
|
|
3986
4526
|
buildWallSpecimen,
|
|
4527
|
+
cageResidualCapacity,
|
|
3987
4528
|
campConstruction,
|
|
3988
4529
|
chaletConstruction,
|
|
3989
4530
|
constructionOfSystem,
|
|
3990
4531
|
coreSize,
|
|
4532
|
+
countViolations,
|
|
3991
4533
|
damperMass,
|
|
3992
4534
|
exteriorWindows,
|
|
3993
4535
|
logCabinConstruction,
|
|
3994
4536
|
masonryConstruction,
|
|
3995
4537
|
mechanicalStoreys,
|
|
3996
4538
|
pilesFor,
|
|
4539
|
+
pillarStations,
|
|
3997
4540
|
planToSpace,
|
|
3998
4541
|
planWaterTank,
|
|
3999
4542
|
register,
|
|
@@ -4014,6 +4557,8 @@ export {
|
|
|
4014
4557
|
towerLocales,
|
|
4015
4558
|
towerPlate,
|
|
4016
4559
|
towerWater,
|
|
4560
|
+
validateSpec,
|
|
4561
|
+
validateStructure,
|
|
4017
4562
|
version,
|
|
4018
4563
|
windExposureOf,
|
|
4019
4564
|
woodFrameConstruction
|