@adatechnology/game-floorplan-structure 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/index.d.ts +153 -35
- package/dist/index.global.js +1023 -70
- 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.global.js
CHANGED
|
@@ -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
|
|
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
|
|
577
|
-
const aabb = alongU ? [
|
|
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
|
|
668
|
-
for (const pier of piers.filter((q) => Math.abs(
|
|
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
|
|
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}|${
|
|
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 =
|
|
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 =
|
|
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")
|
|
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
|
|
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
|
|
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) >
|
|
1083
|
-
const [low, high] =
|
|
1084
|
-
return from >= low -
|
|
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) <=
|
|
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
|
|
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
|
|
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" ?
|
|
1774
|
-
v: round22(wall.axis === "u" ? wall.v0 + vector[1] * offset :
|
|
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
|
|
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" ?
|
|
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
|
|
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 -
|
|
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,
|
|
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 -
|
|
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
|
|
2354
|
+
const reach = TANK_SIDE / 2 + margin - side / 2;
|
|
2355
|
+
const pillars = [[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([a, b]) => ({ u: round2(u + a * reach), v: round2(v + b * reach), side }));
|
|
2204
2356
|
const half = TANK_SIDE / 2 + margin;
|
|
2205
2357
|
return { aabb: [u - half, v - half, u + half, v + half].map(round2), baseY, height: rise, thickness, topY: round2(baseY + rise + thickness), pillars, ladder: true };
|
|
2206
2358
|
}
|
|
@@ -2237,7 +2389,7 @@ var GameFloorplanStructure = (() => {
|
|
|
2237
2389
|
aabb: [u - TANK_SIDE / 2, v - TANK_SIDE / 2, u + TANK_SIDE / 2, v + TANK_SIDE / 2].map(round2)
|
|
2238
2390
|
};
|
|
2239
2391
|
if (where === "raised_slab") {
|
|
2240
|
-
tank.platform = raisedPlatform(source, u, v, slabY);
|
|
2392
|
+
tank.platform = raisedPlatform(source, { u, v, baseY: slabY }, options.pillarSide ?? TANK_PILLAR_SIDE);
|
|
2241
2393
|
tank.y = tank.platform.topY;
|
|
2242
2394
|
}
|
|
2243
2395
|
return tank;
|
|
@@ -2342,7 +2494,7 @@ var GameFloorplanStructure = (() => {
|
|
|
2342
2494
|
}
|
|
2343
2495
|
function structureExtension(context) {
|
|
2344
2496
|
const cache = cacheOf(context.result);
|
|
2345
|
-
const options = { construction: cache.build.construction, extras: cache.build.extras ?? {}, tank: tankOf(context), wallDetail: context.options.wallDetail ?? "panels" };
|
|
2497
|
+
const options = { construction: cache.build.construction, extras: cache.build.extras ?? {}, tank: tankOf(context), wallDetail: context.options.wallDetail ?? "panels", ...context.options.wallPillars !== void 0 ? { wallPillars: context.options.wallPillars } : {}, ...context.options.groundBeams !== void 0 ? { groundBeams: context.options.groundBeams === true } : {}, ...context.options.visorSections === true ? { visorSections: true } : {} };
|
|
2346
2498
|
const { roof, ...structure } = buildStructure(context.result, options);
|
|
2347
2499
|
cache.roof = roof;
|
|
2348
2500
|
for (const part of structure.parts) {
|
|
@@ -2366,7 +2518,7 @@ var GameFloorplanStructure = (() => {
|
|
|
2366
2518
|
engine.registerResultExtension(
|
|
2367
2519
|
"waterTank",
|
|
2368
2520
|
(context) => {
|
|
2369
|
-
const tank = { capacities: options.tankCapacities, custom: context.plan.tpl.tank, location: context.plan.tpl.tankLocation };
|
|
2521
|
+
const tank = { capacities: options.tankCapacities, custom: context.plan.tpl.tank, location: context.plan.tpl.tankLocation, ...context.options.visorSections === true ? { pillarSide: VISOR_TANK_PILLAR_SIDE } : {} };
|
|
2370
2522
|
return planWaterTank(context.result, context.rng, tank);
|
|
2371
2523
|
},
|
|
2372
2524
|
{ after: ["construction"], seedKey: "caixa" }
|
|
@@ -2388,6 +2540,7 @@ var GameFloorplanStructure = (() => {
|
|
|
2388
2540
|
}
|
|
2389
2541
|
|
|
2390
2542
|
// ../game-contracts/dist/index.js
|
|
2543
|
+
var DEFAULT_BLAST_IMPULSE = { bodyArea: 0.7, lift: 0.15, minSpeed: 0.3, tntPerJoule: 0.24 / 2e5 };
|
|
2391
2544
|
var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
|
|
2392
2545
|
var DEFAULT_IGNITION_RULES = {
|
|
2393
2546
|
fillCurve: [
|
|
@@ -2810,6 +2963,22 @@ var GameFloorplanStructure = (() => {
|
|
|
2810
2963
|
return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
|
|
2811
2964
|
};
|
|
2812
2965
|
var clamp3 = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
2966
|
+
var splitAlong = (box2, count, axis, rnd) => {
|
|
2967
|
+
const shares = Array.from({ length: count }, () => 0.25 + 1.5 * rnd());
|
|
2968
|
+
const total = shares.reduce((sum, share) => sum + share, 0);
|
|
2969
|
+
const out = [];
|
|
2970
|
+
let from = box2.c[axis] - box2.s[axis] / 2;
|
|
2971
|
+
for (const share of shares) {
|
|
2972
|
+
const length = box2.s[axis] * share / total;
|
|
2973
|
+
const c = [box2.c[0], box2.c[1], box2.c[2]];
|
|
2974
|
+
const size = [box2.s[0], box2.s[1], box2.s[2]];
|
|
2975
|
+
c[axis] = from + length / 2;
|
|
2976
|
+
size[axis] = length;
|
|
2977
|
+
out.push({ c, s: size });
|
|
2978
|
+
from += length;
|
|
2979
|
+
}
|
|
2980
|
+
return out;
|
|
2981
|
+
};
|
|
2813
2982
|
var splinterBox = (inp, box2, limit = inp.maxFragments) => {
|
|
2814
2983
|
const { rnd } = inp;
|
|
2815
2984
|
const dims = box2.s;
|
|
@@ -2819,35 +2988,81 @@ var GameFloorplanStructure = (() => {
|
|
|
2819
2988
|
const w = Math.max(fmin * 1.5, 0.01);
|
|
2820
2989
|
const c = clamp3(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
|
|
2821
2990
|
const maxLen = fmax * 4;
|
|
2822
|
-
const cells =
|
|
2991
|
+
const cells = [];
|
|
2992
|
+
for (const cell of bsp(box2, c, [a, b], rnd)) {
|
|
2993
|
+
const wide = cell.s[a] >= cell.s[b] ? a : b;
|
|
2994
|
+
if (rnd() < 0.4 && cell.s[wide] > fmin * 0.5) for (const part of splitAlong(cell, 2, wide, rnd)) cells.push(part);
|
|
2995
|
+
else cells.push(cell);
|
|
2996
|
+
}
|
|
2823
2997
|
const pieces = [];
|
|
2824
2998
|
for (const cell of cells) {
|
|
2825
|
-
const extra = rnd()
|
|
2999
|
+
const extra = Math.floor(rnd() * 3);
|
|
2826
3000
|
const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
|
|
2827
|
-
for (const p of
|
|
3001
|
+
for (const p of splitAlong(cell, k, g, rnd)) pieces.push(p);
|
|
2828
3002
|
}
|
|
2829
3003
|
return pieces.slice(0, limit).map((p) => ({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] }));
|
|
2830
3004
|
};
|
|
3005
|
+
var PLY = 3e-3;
|
|
3006
|
+
var PLY_GAP = 0.6;
|
|
3007
|
+
var PLY_SHEET = 0.012;
|
|
3008
|
+
var plyBreak = (inp, root, limit) => {
|
|
3009
|
+
const { rnd } = inp;
|
|
3010
|
+
const dims = root.s;
|
|
3011
|
+
const sorted = [0, 1, 2].sort((x, y) => dims[x] - dims[y]);
|
|
3012
|
+
const t = sorted[0];
|
|
3013
|
+
const [u, v] = [sorted[1], sorted[2]].sort((x, y) => x - y);
|
|
3014
|
+
const along = dims[u] >= dims[v] ? u : v;
|
|
3015
|
+
const across = along === u ? v : u;
|
|
3016
|
+
const layers = clamp3(Math.round(dims[t] / PLY / 2), 2, 3);
|
|
3017
|
+
const pitch = dims[t] / layers;
|
|
3018
|
+
const th = Math.min(pitch, PLY_SHEET);
|
|
3019
|
+
const per = Math.max(2, Math.floor(limit / layers));
|
|
3020
|
+
const out = [];
|
|
3021
|
+
for (let k = 0; k < layers; k++) {
|
|
3022
|
+
const layer = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
|
|
3023
|
+
layer.c[t] = root.c[t] + (-dims[t] / 2 + (k + 0.5) * pitch) * (pitch > PLY_SHEET ? 1 : 1 + PLY_GAP);
|
|
3024
|
+
layer.s[t] = th;
|
|
3025
|
+
for (const cell of bsp(layer, per, [u, v], rnd)) {
|
|
3026
|
+
if (out.length >= limit) return out;
|
|
3027
|
+
if (rnd() < 0.35) {
|
|
3028
|
+
const strand = cell.s[across] * (0.15 + 0.2 * rnd());
|
|
3029
|
+
out.push({ center: cell.c, size: [cell.s[along], strand, th * 0.7], q: frame(along, across, t), shape: "splinter", weight: cell.s[along] * strand * th });
|
|
3030
|
+
} else {
|
|
3031
|
+
out.push({ center: cell.c, size: [cell.s[u], cell.s[v], th], q: frame(u, v, t, (rnd() - 0.5) * 0.5), shape: "scrap", weight: cell.s[u] * cell.s[v] * th });
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
return out;
|
|
3036
|
+
};
|
|
3037
|
+
var granules = (inp, box2, count) => bsp(box2, count, [0, 1, 2], inp.rnd).map((b) => ({ center: b.c, size: b.s, q: quat.identity(), shape: "chunk", weight: b.s[0] * b.s[1] * b.s[2] }));
|
|
3038
|
+
var CRUMBS = 6;
|
|
2831
3039
|
var severAt = (inp, root, at2, gap) => {
|
|
2832
3040
|
const dims = root.s;
|
|
2833
3041
|
const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
|
|
2834
3042
|
const half = dims[L] / 2;
|
|
2835
3043
|
const lo = clamp3(at2 - gap / 2, -half, half), hi = clamp3(at2 + gap / 2, -half, half);
|
|
2836
3044
|
const out = [];
|
|
2837
|
-
const
|
|
3045
|
+
const fibrous = inp.form === "slivers" || inp.form === "plies";
|
|
3046
|
+
const [a, b] = [0, 1, 2].filter((i) => i !== L);
|
|
3047
|
+
const stub = (from, to, breakAtEnd) => {
|
|
2838
3048
|
if (to - from < 0.02) return;
|
|
2839
3049
|
const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
|
|
2840
3050
|
c[L] = (from + to) / 2;
|
|
2841
3051
|
sz[L] = to - from;
|
|
2842
|
-
|
|
3052
|
+
const weight = sz[0] * sz[1] * sz[2];
|
|
3053
|
+
if (fibrous) out.push({ center: c, size: [sz[L], sz[a], sz[b]], q: frame(L, a, b, breakAtEnd ? 0 : Math.PI), shape: "slab", weight });
|
|
3054
|
+
else out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight });
|
|
2843
3055
|
};
|
|
2844
|
-
stub(-half, lo);
|
|
2845
|
-
stub(hi, half);
|
|
3056
|
+
stub(-half, lo, true);
|
|
3057
|
+
stub(hi, half, false);
|
|
2846
3058
|
const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
|
|
2847
3059
|
band.c[L] = (lo + hi) / 2;
|
|
2848
3060
|
band.s[L] = Math.max(hi - lo, 0.01);
|
|
2849
|
-
|
|
2850
|
-
|
|
3061
|
+
const room = inp.maxFragments - out.length;
|
|
3062
|
+
if (inp.style === "snap" && inp.form === "plies") for (const p of plyBreak(inp, band, Math.max(3, room))) out.push(p);
|
|
3063
|
+
else if (inp.style === "snap" && inp.form === "granules") for (const p of granules(inp, band, clamp3(room, 3, 12))) out.push(p);
|
|
3064
|
+
else if (inp.style === "snap") {
|
|
3065
|
+
for (const b2 of bsp(band, Math.max(3, Math.min(6, room)), [0, 1, 2], inp.rnd)) out.push({ center: b2.c, size: b2.s, q: quat.identity(), shape: "chunk", weight: b2.s[0] * b2.s[1] * b2.s[2] });
|
|
2851
3066
|
} else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
|
|
2852
3067
|
return out;
|
|
2853
3068
|
};
|
|
@@ -2880,8 +3095,18 @@ var GameFloorplanStructure = (() => {
|
|
|
2880
3095
|
const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
|
|
2881
3096
|
if (style === "snap") {
|
|
2882
3097
|
if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
|
|
3098
|
+
if (inp.form === "plies") return plyBreak(inp, root, maxFragments);
|
|
2883
3099
|
const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
|
|
2884
3100
|
for (const b of bsp(root, n2, [0, 1, 2], rnd)) out.push({ center: b.c, size: b.s, q: idq, shape: "slab", weight: b.s[0] * b.s[1] * b.s[2] });
|
|
3101
|
+
if (inp.form !== "granules") return out;
|
|
3102
|
+
const crumbs = Math.min(CRUMBS, maxFragments - out.length);
|
|
3103
|
+
if (crumbs <= 0) return out;
|
|
3104
|
+
const band = { c: [0, 0, 0], s: [0, 0, 0] };
|
|
3105
|
+
for (let i = 0; i < 3; i++) {
|
|
3106
|
+
band.s[i] = Math.min(dims[i], Math.max(fmin, dims[i] * 0.2));
|
|
3107
|
+
band.c[i] = clamp3(inp.hit[i], -dims[i] / 2 + band.s[i] / 2, dims[i] / 2 - band.s[i] / 2);
|
|
3108
|
+
}
|
|
3109
|
+
for (const p of granules(inp, band, crumbs)) out.push({ ...p, weight: p.weight * 0.5 });
|
|
2885
3110
|
return out;
|
|
2886
3111
|
}
|
|
2887
3112
|
if (style === "crumble" || style === "none") {
|
|
@@ -2988,6 +3213,88 @@ var GameFloorplanStructure = (() => {
|
|
|
2988
3213
|
}
|
|
2989
3214
|
return marks;
|
|
2990
3215
|
};
|
|
3216
|
+
var JOINT_GAP = 1e-4;
|
|
3217
|
+
var MIN_BEARING_SHARE = 0.25;
|
|
3218
|
+
var unsupportedCells = (cells, thin, dims, standing) => {
|
|
3219
|
+
if (thin === 1) return [];
|
|
3220
|
+
const [u, v] = planeAxes(thin);
|
|
3221
|
+
const bottom = -dims[v] / 2;
|
|
3222
|
+
const rowOf = (y) => Math.round((y - bottom) * 1e4);
|
|
3223
|
+
const rows = /* @__PURE__ */ new Map();
|
|
3224
|
+
for (let i = 0; i < cells.length; i++) {
|
|
3225
|
+
if (!standing(i)) continue;
|
|
3226
|
+
const cell = cells[i];
|
|
3227
|
+
const key = rowOf(cell.center[v] - cell.size[v] / 2);
|
|
3228
|
+
const row = rows.get(key);
|
|
3229
|
+
if (row) row.push(i);
|
|
3230
|
+
else rows.set(key, [i]);
|
|
3231
|
+
}
|
|
3232
|
+
const held = /* @__PURE__ */ new Set();
|
|
3233
|
+
const queue = [];
|
|
3234
|
+
for (const i of rows.get(0) ?? []) {
|
|
3235
|
+
held.add(i);
|
|
3236
|
+
queue.push(i);
|
|
3237
|
+
}
|
|
3238
|
+
const overlap = (a, b) => Math.min(a.center[u] + a.size[u] / 2, b.center[u] + b.size[u] / 2) - Math.max(a.center[u] - a.size[u] / 2, b.center[u] - b.size[u] / 2);
|
|
3239
|
+
const climb = () => {
|
|
3240
|
+
while (queue.length > 0) {
|
|
3241
|
+
const cell = cells[queue.pop()];
|
|
3242
|
+
for (const j of rows.get(rowOf(cell.center[v] + cell.size[v] / 2)) ?? []) {
|
|
3243
|
+
const other = cells[j];
|
|
3244
|
+
if (held.has(j) || overlap(cell, other) <= Math.max(JOINT_GAP, MIN_BEARING_SHARE * Math.min(cell.size[u], other.size[u]))) continue;
|
|
3245
|
+
held.add(j);
|
|
3246
|
+
queue.push(j);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
};
|
|
3250
|
+
for (const row of rows.values()) row.sort((a, b) => cells[a].center[u] - cells[b].center[u]);
|
|
3251
|
+
const touching = (a, b) => Math.abs(a.center[u] + a.size[u] / 2 - (b.center[u] - b.size[u] / 2)) <= 1e-3;
|
|
3252
|
+
for (let wedged = true; wedged; ) {
|
|
3253
|
+
climb();
|
|
3254
|
+
wedged = false;
|
|
3255
|
+
for (const row of rows.values()) {
|
|
3256
|
+
for (let k = 1; k < row.length - 1; k++) {
|
|
3257
|
+
const i = row[k];
|
|
3258
|
+
if (held.has(i) || !held.has(row[k - 1]) || !held.has(row[k + 1])) continue;
|
|
3259
|
+
if (!touching(cells[row[k - 1]], cells[i]) || !touching(cells[i], cells[row[k + 1]])) continue;
|
|
3260
|
+
held.add(i);
|
|
3261
|
+
queue.push(i);
|
|
3262
|
+
wedged = true;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
const loose = [];
|
|
3267
|
+
for (const row of rows.values()) for (const i of row) if (!held.has(i)) loose.push(i);
|
|
3268
|
+
return loose;
|
|
3269
|
+
};
|
|
3270
|
+
var clumpCells = (cells, thin, cap) => {
|
|
3271
|
+
if (cells.length <= cap) return [...cells];
|
|
3272
|
+
const [u, v] = planeAxes(thin);
|
|
3273
|
+
let lo0 = Infinity, lo1 = Infinity, hi0 = -Infinity, hi1 = -Infinity;
|
|
3274
|
+
for (const cell of cells) {
|
|
3275
|
+
lo0 = Math.min(lo0, cell.center[u] - cell.size[u] / 2);
|
|
3276
|
+
hi0 = Math.max(hi0, cell.center[u] + cell.size[u] / 2);
|
|
3277
|
+
lo1 = Math.min(lo1, cell.center[v] - cell.size[v] / 2);
|
|
3278
|
+
hi1 = Math.max(hi1, cell.center[v] + cell.size[v] / 2);
|
|
3279
|
+
}
|
|
3280
|
+
let side = Math.sqrt((hi0 - lo0) * (hi1 - lo1) / cap);
|
|
3281
|
+
for (; ; ) {
|
|
3282
|
+
const bins = /* @__PURE__ */ new Map();
|
|
3283
|
+
for (const cell of cells) {
|
|
3284
|
+
const key = `${Math.floor((cell.center[u] - lo0) / side)}:${Math.floor((cell.center[v] - lo1) / side)}`;
|
|
3285
|
+
const bin = bins.get(key) ?? { lo: [Infinity, Infinity, Infinity], hi: [-Infinity, -Infinity, -Infinity] };
|
|
3286
|
+
for (let k = 0; k < 3; k++) {
|
|
3287
|
+
bin.lo[k] = Math.min(bin.lo[k], cell.center[k] - cell.size[k] / 2);
|
|
3288
|
+
bin.hi[k] = Math.max(bin.hi[k], cell.center[k] + cell.size[k] / 2);
|
|
3289
|
+
}
|
|
3290
|
+
bins.set(key, bin);
|
|
3291
|
+
}
|
|
3292
|
+
if (bins.size <= cap) {
|
|
3293
|
+
return [...bins.values()].map(({ lo, hi }) => ({ center: [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2], size: [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]] }));
|
|
3294
|
+
}
|
|
3295
|
+
side *= 1.25;
|
|
3296
|
+
}
|
|
3297
|
+
};
|
|
2991
3298
|
var MAX_SPALLS = 64;
|
|
2992
3299
|
var thinAxisOf = (dims) => dims[0] <= dims[1] && dims[0] <= dims[2] ? 0 : dims[1] <= dims[2] ? 1 : 2;
|
|
2993
3300
|
var resolveLayer = (def, index, mat) => ({ index, def, mat, energy: mat.penetration * def.thickness * 1e3 });
|
|
@@ -3659,7 +3966,7 @@ var GameFloorplanStructure = (() => {
|
|
|
3659
3966
|
eau[j] += e * area * g[i].u * w;
|
|
3660
3967
|
}
|
|
3661
3968
|
}
|
|
3662
|
-
const
|
|
3969
|
+
const centre2 = ea.map((v, j) => v > 0 ? eau[j] / v : 0);
|
|
3663
3970
|
const ei = new Array(n).fill(0);
|
|
3664
3971
|
const ga = new Array(n).fill(0);
|
|
3665
3972
|
for (const i of vertical) {
|
|
@@ -3668,7 +3975,7 @@ var GameFloorplanStructure = (() => {
|
|
|
3668
3975
|
for (let j = 0; j < n; j++) {
|
|
3669
3976
|
const w = share(p, j) / bands[j];
|
|
3670
3977
|
if (w <= 0) continue;
|
|
3671
|
-
ei[j] += e * (g[i].du ** 3 * g[i].dv / 12 + g[i].du * g[i].dv * (g[i].u -
|
|
3978
|
+
ei[j] += e * (g[i].du ** 3 * g[i].dv / 12 + g[i].du * g[i].dv * (g[i].u - centre2[j]) ** 2) * w;
|
|
3672
3979
|
}
|
|
3673
3980
|
}
|
|
3674
3981
|
for (let i = 0; i < parts.length; i++) {
|
|
@@ -3715,7 +4022,7 @@ var GameFloorplanStructure = (() => {
|
|
|
3715
4022
|
for (let j = 0; j < n; j++) {
|
|
3716
4023
|
const w = share(p, j);
|
|
3717
4024
|
if (w <= 0) continue;
|
|
3718
|
-
sum += w * p.elasticPa * c.stiffnessFactor * kappa[j] * g[i].du * g[i].dv * (g[i].u -
|
|
4025
|
+
sum += w * p.elasticPa * c.stiffnessFactor * kappa[j] * g[i].du * g[i].dv * (g[i].u - centre2[j]);
|
|
3719
4026
|
tot += w;
|
|
3720
4027
|
}
|
|
3721
4028
|
axial[i] = tot > 0 ? sum / tot : 0;
|
|
@@ -3967,6 +4274,7 @@ var GameFloorplanStructure = (() => {
|
|
|
3967
4274
|
},
|
|
3968
4275
|
{
|
|
3969
4276
|
id: "wood_soft",
|
|
4277
|
+
splitForm: "slivers",
|
|
3970
4278
|
density: 500,
|
|
3971
4279
|
hpPerKg: 90,
|
|
3972
4280
|
penetration: 9,
|
|
@@ -4012,6 +4320,7 @@ var GameFloorplanStructure = (() => {
|
|
|
4012
4320
|
},
|
|
4013
4321
|
{
|
|
4014
4322
|
id: "wood_hard",
|
|
4323
|
+
splitForm: "slivers",
|
|
4015
4324
|
density: 750,
|
|
4016
4325
|
hpPerKg: 140,
|
|
4017
4326
|
penetration: 14,
|
|
@@ -4056,6 +4365,7 @@ var GameFloorplanStructure = (() => {
|
|
|
4056
4365
|
},
|
|
4057
4366
|
{
|
|
4058
4367
|
id: "plywood",
|
|
4368
|
+
splitForm: "plies",
|
|
4059
4369
|
density: 600,
|
|
4060
4370
|
hpPerKg: 70,
|
|
4061
4371
|
penetration: 7,
|
|
@@ -4103,6 +4413,7 @@ var GameFloorplanStructure = (() => {
|
|
|
4103
4413
|
},
|
|
4104
4414
|
{
|
|
4105
4415
|
id: "mdf",
|
|
4416
|
+
splitForm: "granules",
|
|
4106
4417
|
density: 750,
|
|
4107
4418
|
hpPerKg: 45,
|
|
4108
4419
|
penetration: 6,
|
|
@@ -4614,6 +4925,7 @@ var GameFloorplanStructure = (() => {
|
|
|
4614
4925
|
},
|
|
4615
4926
|
{
|
|
4616
4927
|
id: "green_wood",
|
|
4928
|
+
splitForm: "slivers",
|
|
4617
4929
|
density: 650,
|
|
4618
4930
|
hpPerKg: 5,
|
|
4619
4931
|
penetration: 10,
|
|
@@ -4894,6 +5206,125 @@ var GameFloorplanStructure = (() => {
|
|
|
4894
5206
|
"brittle"
|
|
4895
5207
|
],
|
|
4896
5208
|
compressive: 1e5
|
|
5209
|
+
},
|
|
5210
|
+
{
|
|
5211
|
+
id: "mud",
|
|
5212
|
+
density: 1700,
|
|
5213
|
+
hpPerKg: 1,
|
|
5214
|
+
penetration: 5,
|
|
5215
|
+
ductility: 0.8,
|
|
5216
|
+
fracture: "crumble",
|
|
5217
|
+
fragmentSize: [
|
|
5218
|
+
0.02,
|
|
5219
|
+
0.08
|
|
5220
|
+
],
|
|
5221
|
+
hitDebris: "dust",
|
|
5222
|
+
breakDebris: "dust",
|
|
5223
|
+
thermal: {
|
|
5224
|
+
conductivity: 0.5,
|
|
5225
|
+
heatCapacity: 3.2
|
|
5226
|
+
},
|
|
5227
|
+
tags: [
|
|
5228
|
+
"soil",
|
|
5229
|
+
"wet"
|
|
5230
|
+
],
|
|
5231
|
+
compressive: 5e3
|
|
5232
|
+
},
|
|
5233
|
+
{
|
|
5234
|
+
id: "clay",
|
|
5235
|
+
density: 1800,
|
|
5236
|
+
hpPerKg: 5,
|
|
5237
|
+
penetration: 7,
|
|
5238
|
+
ductility: 0.05,
|
|
5239
|
+
fracture: "crumble",
|
|
5240
|
+
fragmentSize: [
|
|
5241
|
+
0.02,
|
|
5242
|
+
0.1
|
|
5243
|
+
],
|
|
5244
|
+
hitDebris: "chips",
|
|
5245
|
+
breakDebris: "dust",
|
|
5246
|
+
thermal: {
|
|
5247
|
+
conductivity: 0.15,
|
|
5248
|
+
heatCapacity: 2.2
|
|
5249
|
+
},
|
|
5250
|
+
tags: [
|
|
5251
|
+
"soil",
|
|
5252
|
+
"brittle"
|
|
5253
|
+
],
|
|
5254
|
+
compressive: 6e4
|
|
5255
|
+
},
|
|
5256
|
+
{
|
|
5257
|
+
id: "snow",
|
|
5258
|
+
density: 300,
|
|
5259
|
+
hpPerKg: 1,
|
|
5260
|
+
penetration: 3,
|
|
5261
|
+
ductility: 0.6,
|
|
5262
|
+
fracture: "crumble",
|
|
5263
|
+
fragmentSize: [
|
|
5264
|
+
0.01,
|
|
5265
|
+
0.05
|
|
5266
|
+
],
|
|
5267
|
+
hitDebris: "dust",
|
|
5268
|
+
breakDebris: "dust",
|
|
5269
|
+
thermal: {
|
|
5270
|
+
conductivity: 0.1,
|
|
5271
|
+
heatCapacity: 2.1
|
|
5272
|
+
},
|
|
5273
|
+
tags: [
|
|
5274
|
+
"soil",
|
|
5275
|
+
"frozen"
|
|
5276
|
+
],
|
|
5277
|
+
compressive: 1e3
|
|
5278
|
+
},
|
|
5279
|
+
{
|
|
5280
|
+
id: "cobble",
|
|
5281
|
+
density: 2650,
|
|
5282
|
+
hpPerKg: 25,
|
|
5283
|
+
penetration: 25,
|
|
5284
|
+
penetrationClass: "concrete",
|
|
5285
|
+
penetrationScale: 0.9,
|
|
5286
|
+
ductility: 0.03,
|
|
5287
|
+
fracture: "crumble",
|
|
5288
|
+
fragmentSize: [
|
|
5289
|
+
0.05,
|
|
5290
|
+
0.15
|
|
5291
|
+
],
|
|
5292
|
+
hitDebris: "chips",
|
|
5293
|
+
breakDebris: "chips",
|
|
5294
|
+
thermal: {
|
|
5295
|
+
conductivity: 0.2,
|
|
5296
|
+
heatCapacity: 2.5
|
|
5297
|
+
},
|
|
5298
|
+
tags: [
|
|
5299
|
+
"masonry",
|
|
5300
|
+
"brittle"
|
|
5301
|
+
],
|
|
5302
|
+
compressive: 3e5
|
|
5303
|
+
},
|
|
5304
|
+
{
|
|
5305
|
+
id: "paver",
|
|
5306
|
+
density: 2300,
|
|
5307
|
+
hpPerKg: 14,
|
|
5308
|
+
penetration: 18,
|
|
5309
|
+
penetrationClass: "concrete",
|
|
5310
|
+
penetrationScale: 1,
|
|
5311
|
+
ductility: 0.03,
|
|
5312
|
+
fracture: "crumble",
|
|
5313
|
+
fragmentSize: [
|
|
5314
|
+
0.04,
|
|
5315
|
+
0.12
|
|
5316
|
+
],
|
|
5317
|
+
hitDebris: "chips",
|
|
5318
|
+
breakDebris: "chips",
|
|
5319
|
+
thermal: {
|
|
5320
|
+
conductivity: 0.15,
|
|
5321
|
+
heatCapacity: 2.5
|
|
5322
|
+
},
|
|
5323
|
+
tags: [
|
|
5324
|
+
"masonry",
|
|
5325
|
+
"brittle"
|
|
5326
|
+
],
|
|
5327
|
+
compressive: 35e4
|
|
4897
5328
|
}
|
|
4898
5329
|
];
|
|
4899
5330
|
var material_hints_default = {
|
|
@@ -5106,6 +5537,11 @@ var GameFloorplanStructure = (() => {
|
|
|
5106
5537
|
var BREACH_BLAST_FACTOR = 6;
|
|
5107
5538
|
var BREACH_AREA = 0.25;
|
|
5108
5539
|
var BREACH_BLAST_REACH = 0.12;
|
|
5540
|
+
var MEMBER_CUT_REACH = 0.5;
|
|
5541
|
+
var BLAST_REFERENCE_RESISTANCE = 200 * 1900 * 0.13;
|
|
5542
|
+
var BLAST_EDGE_RAGGED = 0.3;
|
|
5543
|
+
var BLAST_EDGE_LOOSE = 1.35;
|
|
5544
|
+
var BLAST_EDGE_WEAR = 0.6;
|
|
5109
5545
|
var RESIDUAL_CAPACITY = 0.15;
|
|
5110
5546
|
var SEVER_BAND = 0.1;
|
|
5111
5547
|
var SEVER_ASPECT = 3;
|
|
@@ -6358,7 +6794,7 @@ var GameFloorplanStructure = (() => {
|
|
|
6358
6794
|
if (j.st.open) continue;
|
|
6359
6795
|
if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
|
|
6360
6796
|
if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
|
|
6361
|
-
j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
|
|
6797
|
+
j.st.hp -= span === void 0 ? amount : Math.min(amount, j.st.maxHp) * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
|
|
6362
6798
|
if (j.st.hp <= 0) {
|
|
6363
6799
|
j.st.hp = 0;
|
|
6364
6800
|
j.st.open = true;
|
|
@@ -6486,6 +6922,7 @@ var GameFloorplanStructure = (() => {
|
|
|
6486
6922
|
const worn = (_a = p.rt).chips ?? (_a.chips = {});
|
|
6487
6923
|
const cut = (_b = p.rt).craters ?? (_b.craters = {});
|
|
6488
6924
|
const out = [];
|
|
6925
|
+
const wholeBefore = Object.values(worn).filter((w) => w === -1).length;
|
|
6489
6926
|
const shot = kind === "ballistic";
|
|
6490
6927
|
for (let i = 0; i < grid.length; i++) {
|
|
6491
6928
|
const cell = grid[i];
|
|
@@ -6544,6 +6981,7 @@ var GameFloorplanStructure = (() => {
|
|
|
6544
6981
|
emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
|
|
6545
6982
|
debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
|
|
6546
6983
|
}
|
|
6984
|
+
if (Object.values(worn).filter((w) => w === -1).length > wholeBefore) dropUnsupported(o, p, src);
|
|
6547
6985
|
if (p.spec.reinforcement) exposeRebar(o, p, src);
|
|
6548
6986
|
for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
|
|
6549
6987
|
if (p.structural && !isLost(p)) {
|
|
@@ -6647,10 +7085,122 @@ var GameFloorplanStructure = (() => {
|
|
|
6647
7085
|
setStage(o, p, 1, {}, src);
|
|
6648
7086
|
setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
|
|
6649
7087
|
debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
|
|
7088
|
+
dropUnsupported(o, p, src);
|
|
6650
7089
|
const reinforcement = p.spec.reinforcement;
|
|
6651
7090
|
if (reinforcement) exposeRebar(o, p, src);
|
|
6652
7091
|
if (p.structural) solveStructure(o);
|
|
6653
7092
|
};
|
|
7093
|
+
const blastReachOf = (p, energy) => {
|
|
7094
|
+
const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
|
|
7095
|
+
const resistance = Math.max(1, perKg * p.mat.density * p.thickness);
|
|
7096
|
+
return BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3) * clamp22(Math.cbrt(BLAST_REFERENCE_RESISTANCE / resistance), 0.3, 2);
|
|
7097
|
+
};
|
|
7098
|
+
const blastUnits = (o, p, wp, energy, dist, src) => {
|
|
7099
|
+
var _a, _b;
|
|
7100
|
+
if (!p.staged || p.st.stripped || isLost(p) || chipEnergy <= 0) return false;
|
|
7101
|
+
const reach = blastReachOf(p, energy);
|
|
7102
|
+
if (dist >= reach) return false;
|
|
7103
|
+
const radius = Math.sqrt(reach * reach - dist * dist);
|
|
7104
|
+
const [u, v] = planeAxes(p.thin);
|
|
7105
|
+
const lp = toLocal(p.pos, p.q, objLocal(o, wp));
|
|
7106
|
+
const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
|
|
7107
|
+
const grid = gridOf(p);
|
|
7108
|
+
const worn = (_a = p.rt).chips ?? (_a.chips = {});
|
|
7109
|
+
const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
|
|
7110
|
+
const lobes = [rnd() * 2 * Math.PI, rnd() * 2 * Math.PI];
|
|
7111
|
+
const out = [];
|
|
7112
|
+
for (let i = 0; i < grid.length; i++) {
|
|
7113
|
+
const cell = grid[i];
|
|
7114
|
+
if (worn[i] === -1) continue;
|
|
7115
|
+
if (op && p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius) continue;
|
|
7116
|
+
const du = cell.center[u] - lp[u], dv = cell.center[v] - lp[v];
|
|
7117
|
+
const angle = Math.atan2(dv, du);
|
|
7118
|
+
const edge = radius * (1 + BLAST_EDGE_RAGGED * (0.5 * Math.sin(3 * angle + lobes[0]) + 0.3 * Math.sin(5 * angle + lobes[1]) + 0.4 * (rnd() - 0.5)));
|
|
7119
|
+
const d = Math.hypot(du, dv);
|
|
7120
|
+
if (d <= edge) {
|
|
7121
|
+
worn[i] = -1;
|
|
7122
|
+
out.push(i);
|
|
7123
|
+
} else if (d <= edge * BLAST_EDGE_LOOSE) {
|
|
7124
|
+
const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
|
|
7125
|
+
worn[i] = Math.max(worn[i] ?? 0, perKg * mass * BLAST_EDGE_WEAR);
|
|
7126
|
+
}
|
|
7127
|
+
}
|
|
7128
|
+
if (out.length === 0) return false;
|
|
7129
|
+
const loose = unsupportedCells(grid, p.thin, p.dims, (i) => worn[i] !== -1 && !(op && p.st.opening && Math.hypot(grid[i].center[u] - op[u], grid[i].center[v] - op[v]) <= p.st.opening.radius));
|
|
7130
|
+
for (const i of loose) worn[i] = -1;
|
|
7131
|
+
const blown = out.map((i) => grid[i]), fallen = loose.map((i) => grid[i]);
|
|
7132
|
+
const volume = (cells2) => cells2.reduce((sum, cell) => sum + cell.size[0] * cell.size[1] * cell.size[2], 0);
|
|
7133
|
+
const blownMass = Math.min(p.st.mass, p.mat.density * volume(blown));
|
|
7134
|
+
const fallenMass = Math.min(p.st.mass - blownMass, p.mat.density * volume(fallen));
|
|
7135
|
+
const speed = clamp22(Math.sqrt(2 * energy * 0.02 / Math.max(blownMass, 0.05)), 1, 12);
|
|
7136
|
+
const { c, q } = partXf(o, p);
|
|
7137
|
+
const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
|
|
7138
|
+
const tag = `x${chipped.length}_`;
|
|
7139
|
+
const fragments = [
|
|
7140
|
+
...cellFragments(o, p, clumpCells(blown, p.thin, maxFragments), wp, null, speed, blownMass, tag, true),
|
|
7141
|
+
...fallen.length > 0 ? cellFragments(o, p, clumpCells(fallen, p.thin, Math.max(2, Math.round(maxFragments / 4))), wp, null, 0.3, fallenMass, `${tag}f`, true) : []
|
|
7142
|
+
];
|
|
7143
|
+
const cells = [...blown, ...fallen];
|
|
7144
|
+
const holes = localHolesOf(p);
|
|
7145
|
+
p.st.holes = p.st.holes.filter((_, k) => !cells.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
|
|
7146
|
+
const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
|
|
7147
|
+
for (const cell of cells) {
|
|
7148
|
+
chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
|
|
7149
|
+
for (let k = 0; k < 3; k++) {
|
|
7150
|
+
lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
|
|
7151
|
+
hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
|
|
7152
|
+
}
|
|
7153
|
+
}
|
|
7154
|
+
p.st.mass = Math.max(0, p.st.mass - blownMass - fallenMass);
|
|
7155
|
+
p.rt.removed = (p.rt.removed ?? 0) + blownMass + fallenMass;
|
|
7156
|
+
markDamaged(p);
|
|
7157
|
+
setStage(o, p, 1, {}, src);
|
|
7158
|
+
const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
|
|
7159
|
+
const units = cells.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
|
|
7160
|
+
emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]], rotation: q, fragments, units });
|
|
7161
|
+
debris(o, p, p.mat.breakDebris, center, vec.normalize(vec.sub(center, wp), [0, 1, 0]), 14, 3, src);
|
|
7162
|
+
if (p.spec.reinforcement) exposeRebar(o, p, src);
|
|
7163
|
+
if (p.structural && !isLost(p)) {
|
|
7164
|
+
solveStructure(o);
|
|
7165
|
+
collapseCheck(o, src);
|
|
7166
|
+
}
|
|
7167
|
+
return true;
|
|
7168
|
+
};
|
|
7169
|
+
const dropUnsupported = (o, p, src) => {
|
|
7170
|
+
var _a, _b;
|
|
7171
|
+
if (!p.spec.units || p.spec.units.mortar === p.spec.material || p.st.stripped || isLost(p)) return 0;
|
|
7172
|
+
const [u, v] = planeAxes(p.thin);
|
|
7173
|
+
const grid = gridOf(p);
|
|
7174
|
+
const worn = (_a = p.rt).chips ?? (_a.chips = {});
|
|
7175
|
+
const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
|
|
7176
|
+
const opened = (cell) => !!op && !!p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius;
|
|
7177
|
+
const loose = unsupportedCells(grid, p.thin, p.dims, (i) => worn[i] !== -1 && !opened(grid[i]));
|
|
7178
|
+
if (loose.length === 0) return 0;
|
|
7179
|
+
for (const i of loose) worn[i] = -1;
|
|
7180
|
+
const fallen = loose.map((i) => grid[i]);
|
|
7181
|
+
const mass = Math.min(p.st.mass, p.mat.density * fallen.reduce((sum, cell) => sum + cell.size[0] * cell.size[1] * cell.size[2], 0));
|
|
7182
|
+
const { c, q } = partXf(o, p);
|
|
7183
|
+
const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
|
|
7184
|
+
const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
|
|
7185
|
+
for (const cell of fallen) {
|
|
7186
|
+
for (let k = 0; k < 3; k++) {
|
|
7187
|
+
lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
|
|
7188
|
+
hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
|
|
7189
|
+
}
|
|
7190
|
+
}
|
|
7191
|
+
const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
|
|
7192
|
+
const fragments = cellFragments(o, p, clumpCells(fallen, p.thin, Math.max(2, Math.round(maxFragments / 4))), vec.add(center, [0, 1, 0]), null, 0.3, mass, `u${chipped.length}_`, true);
|
|
7193
|
+
const holes = localHolesOf(p);
|
|
7194
|
+
p.st.holes = p.st.holes.filter((_, k) => !fallen.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
|
|
7195
|
+
for (const cell of fallen) chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
|
|
7196
|
+
p.st.mass = Math.max(0, p.st.mass - mass);
|
|
7197
|
+
p.rt.removed = (p.rt.removed ?? 0) + mass;
|
|
7198
|
+
markDamaged(p);
|
|
7199
|
+
const units = fallen.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
|
|
7200
|
+
emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]], rotation: q, fragments, units });
|
|
7201
|
+
debris(o, p, p.mat.breakDebris, center, [0, -1, 0], 6, 1, src);
|
|
7202
|
+
return loose.length;
|
|
7203
|
+
};
|
|
6654
7204
|
const stripPart = (o, p, reinforcement, src) => {
|
|
6655
7205
|
const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
|
|
6656
7206
|
p.st.stripped = true;
|
|
@@ -6717,6 +7267,7 @@ var GameFloorplanStructure = (() => {
|
|
|
6717
7267
|
const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
|
|
6718
7268
|
const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
|
|
6719
7269
|
const cutAt2 = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
|
|
7270
|
+
const planRnd = p.mat.splitForm ? splitStream(o.id, p.spec.id, hitLocal) : rnd;
|
|
6720
7271
|
const plan = unitPlan.length > 0 ? unitPlan : planFragments({
|
|
6721
7272
|
style,
|
|
6722
7273
|
dims: p.dims,
|
|
@@ -6725,8 +7276,9 @@ var GameFloorplanStructure = (() => {
|
|
|
6725
7276
|
hit: hitLocal,
|
|
6726
7277
|
fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
|
|
6727
7278
|
maxFragments,
|
|
6728
|
-
rnd,
|
|
7279
|
+
rnd: planRnd,
|
|
6729
7280
|
sheet: p.mat.sheet,
|
|
7281
|
+
...p.mat.splitForm ? { form: p.mat.splitForm } : {},
|
|
6730
7282
|
...cutAt2 !== void 0 ? { cut: { at: cutAt2, gap: SEVER_BAND } } : {}
|
|
6731
7283
|
});
|
|
6732
7284
|
const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
|
|
@@ -6771,7 +7323,7 @@ var GameFloorplanStructure = (() => {
|
|
|
6771
7323
|
return;
|
|
6772
7324
|
}
|
|
6773
7325
|
if (ex) {
|
|
6774
|
-
emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, ...ex.effect !== void 0 ? { effect: ex.effect } : {} });
|
|
7326
|
+
emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, ...ex.tnt !== void 0 ? { tnt: ex.tnt } : {}, ...ex.effect !== void 0 ? { effect: ex.effect } : {} });
|
|
6775
7327
|
blasts.push({ point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, source: src });
|
|
6776
7328
|
}
|
|
6777
7329
|
partLost(o, p, src);
|
|
@@ -6838,11 +7390,11 @@ var GameFloorplanStructure = (() => {
|
|
|
6838
7390
|
o.active = true;
|
|
6839
7391
|
}
|
|
6840
7392
|
};
|
|
6841
|
-
const lightLeaksNear = (
|
|
7393
|
+
const lightLeaksNear = (centre2, radius) => {
|
|
6842
7394
|
for (const o of objects.values()) {
|
|
6843
7395
|
for (const q of o.parts) {
|
|
6844
7396
|
if (isLost(q) || q.rt.lit || q.rt.jet || !leaksFlammable(q)) continue;
|
|
6845
|
-
if (vec.distance(partXf(o, q).c,
|
|
7397
|
+
if (vec.distance(partXf(o, q).c, centre2) - q.bound > radius) continue;
|
|
6846
7398
|
q.rt.lit = true;
|
|
6847
7399
|
o.active = true;
|
|
6848
7400
|
}
|
|
@@ -6865,7 +7417,8 @@ var GameFloorplanStructure = (() => {
|
|
|
6865
7417
|
const fragments = [];
|
|
6866
7418
|
for (let i = 0; i < n; i++) {
|
|
6867
7419
|
const back = !exit || i % 3 === 2;
|
|
6868
|
-
const
|
|
7420
|
+
const plies = p.mat.splitForm === "plies";
|
|
7421
|
+
const len = scale * (fibre ? 1.2 + 1.2 * rnd() : 3 + 4 * rnd()), w = scale * (0.8 + 0.8 * rnd()), t = scale * (fibre ? 0.7 + 0.6 * rnd() : plies ? 0.15 + 0.15 * rnd() : 0.4 + 0.4 * rnd());
|
|
6869
7422
|
const size = [w, w, w];
|
|
6870
7423
|
size[along] = len;
|
|
6871
7424
|
size[(along + 1) % 3] = w;
|
|
@@ -7026,7 +7579,7 @@ var GameFloorplanStructure = (() => {
|
|
|
7026
7579
|
checkIgnite(o, p, src);
|
|
7027
7580
|
}
|
|
7028
7581
|
}
|
|
7029
|
-
wearJoints(o, p, Ein * 0.5, void 0, 0, src);
|
|
7582
|
+
wearJoints(o, p, Ein * 0.5, void 0, 0, src, false, 2 * BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3));
|
|
7030
7583
|
const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
|
|
7031
7584
|
if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
|
|
7032
7585
|
frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
|
|
@@ -7036,7 +7589,10 @@ var GameFloorplanStructure = (() => {
|
|
|
7036
7589
|
const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
|
|
7037
7590
|
res.damage += dealt;
|
|
7038
7591
|
if (p.bend && flexure.enabled) bendByBlast(o, p, point, energy, radius, reach, src);
|
|
7039
|
-
|
|
7592
|
+
const breachReach = BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3);
|
|
7593
|
+
if (p.spec.units && p.spec.units.mortar !== p.spec.material) blastUnits(o, p, wp, energy, dist, src);
|
|
7594
|
+
else if (p.spec.reinforcement && p.spec.units && dist > MEMBER_CUT_REACH * breachReach) blastUnits(o, p, wp, energy, dist, src);
|
|
7595
|
+
else if (dist <= breachReach) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
|
|
7040
7596
|
return reach;
|
|
7041
7597
|
};
|
|
7042
7598
|
const blastProbe = { partsTested: 0, partsNear: 0 };
|
|
@@ -7282,12 +7838,17 @@ var GameFloorplanStructure = (() => {
|
|
|
7282
7838
|
}
|
|
7283
7839
|
return h | 0;
|
|
7284
7840
|
};
|
|
7285
|
-
const
|
|
7841
|
+
const splitStream = (objectId, partId, hit) => {
|
|
7842
|
+
const codes = Array.from(`${objectId}|${partId}`, (char) => char.charCodeAt(0) / 1e3);
|
|
7843
|
+
const stream = createRng(seedOf(rng.state() / 1e3, ...codes, hit[0], hit[1], hit[2]));
|
|
7844
|
+
return () => stream.next();
|
|
7845
|
+
};
|
|
7846
|
+
const throwFragment = (frag, objectId, from, g, centre2, index, src, shields, results, reports, mult) => {
|
|
7286
7847
|
const v = frag.velocity;
|
|
7287
7848
|
const speed = Math.hypot(v[0], v[1], v[2]);
|
|
7288
7849
|
const energy = g.energyFactor * 0.5 * frag.mass * speed * speed;
|
|
7289
7850
|
if (speed < 0.5 || energy < 1) return;
|
|
7290
|
-
const r = createRng(seedOf(options.seed ?? 1,
|
|
7851
|
+
const r = createRng(seedOf(options.seed ?? 1, centre2[0], centre2[1], centre2[2], index, frag.center[0], frag.center[1], frag.center[2]));
|
|
7291
7852
|
let dir = vec.scale(v, 1 / speed);
|
|
7292
7853
|
const behind = from ? shields.get(from) : void 0;
|
|
7293
7854
|
if (behind) {
|
|
@@ -9161,6 +9722,8 @@ var GameFloorplanStructure = (() => {
|
|
|
9161
9722
|
// src/wall-detail/panels.ts
|
|
9162
9723
|
var LINTEL_DEPTH2 = 0.2;
|
|
9163
9724
|
var LINTEL_BEARING2 = 0.2;
|
|
9725
|
+
var SILL_BEAM_DEPTH = 0.1;
|
|
9726
|
+
var LINTEL_SPAN_RATIO = 10;
|
|
9164
9727
|
var MIN_PIECE = 0.05;
|
|
9165
9728
|
var frameOf = (wall) => {
|
|
9166
9729
|
const alongU = wall.aabb[2] - wall.aabb[0] >= wall.aabb[3] - wall.aabb[1];
|
|
@@ -9174,6 +9737,7 @@ var GameFloorplanStructure = (() => {
|
|
|
9174
9737
|
const drafts = [];
|
|
9175
9738
|
const piers = [];
|
|
9176
9739
|
const lintels = [];
|
|
9740
|
+
const sillBeams = [];
|
|
9177
9741
|
const spandrels = [];
|
|
9178
9742
|
const height = wall.y1 - wall.y0;
|
|
9179
9743
|
let cursor = frame2.from;
|
|
@@ -9184,23 +9748,26 @@ var GameFloorplanStructure = (() => {
|
|
|
9184
9748
|
};
|
|
9185
9749
|
own.forEach((opening2, k) => {
|
|
9186
9750
|
addPier(Math.max(cursor, opening2.from));
|
|
9187
|
-
const
|
|
9751
|
+
const span = opening2.to - opening2.from;
|
|
9752
|
+
const lintelTop = Math.min(wall.y1, opening2.y1 + Math.max(LINTEL_DEPTH2, style.lintelDepth?.(span) ?? 0, (span + LINTEL_BEARING2) / LINTEL_SPAN_RATIO));
|
|
9188
9753
|
const lintelFrom = Math.max(frame2.from, opening2.from - LINTEL_BEARING2);
|
|
9189
9754
|
const lintelTo = Math.min(frame2.to, opening2.to + LINTEL_BEARING2);
|
|
9190
9755
|
const lintelHeight = lintelTop - opening2.y1;
|
|
9191
|
-
|
|
9756
|
+
const sillTop = style.sillBeam && opening2.y0 - wall.y0 > SILL_BEAM_DEPTH + MIN_PIECE ? opening2.y0 - SILL_BEAM_DEPTH : opening2.y0;
|
|
9757
|
+
if (sillTop - wall.y0 > MIN_PIECE) drafts.push({ id: `${wall.id}_sill_${k}`, role: "partition", material: wall.material, from: opening2.from, to: opening2.to, y0: wall.y0, y1: sillTop, volume: (opening2.to - opening2.from) * (sillTop - wall.y0) });
|
|
9758
|
+
if (sillTop < opening2.y0) sillBeams.push({ id: `${wall.id}_sillbeam_${k}`, role: "beam", material: style.lintelMaterial, from: lintelFrom, to: lintelTo, y0: sillTop, y1: opening2.y0, volume: (lintelTo - lintelFrom) * SILL_BEAM_DEPTH });
|
|
9192
9759
|
lintels.push({ id: `${wall.id}_lintel_${k}`, role: "beam", material: style.lintelMaterial, from: lintelFrom, to: lintelTo, y0: opening2.y1, y1: lintelTop, volume: (lintelTo - lintelFrom) * lintelHeight });
|
|
9193
9760
|
spandrels.push(wall.y1 - lintelTop > MIN_PIECE ? { id: `${wall.id}_spandrel_${k}`, role: roleOf2, material: wall.material, from: opening2.from, to: opening2.to, y0: lintelTop, y1: wall.y1, volume: (opening2.to - opening2.from) * (wall.y1 - lintelTop) } : void 0);
|
|
9194
9761
|
cursor = opening2.to;
|
|
9195
9762
|
});
|
|
9196
9763
|
addPier(frame2.to);
|
|
9197
9764
|
for (const pier of piers) {
|
|
9198
|
-
for (const lintel of lintels) {
|
|
9765
|
+
for (const lintel of [...lintels, ...sillBeams]) {
|
|
9199
9766
|
const overlap = Math.max(0, Math.min(pier.to, lintel.to) - Math.max(pier.from, lintel.from));
|
|
9200
9767
|
pier.volume -= overlap * (lintel.y1 - lintel.y0);
|
|
9201
9768
|
}
|
|
9202
9769
|
}
|
|
9203
|
-
const all = [...piers, ...drafts, ...lintels, ...spandrels.filter((d) => d !== void 0)];
|
|
9770
|
+
const all = [...piers, ...drafts, ...sillBeams, ...lintels, ...spandrels.filter((d) => d !== void 0)];
|
|
9204
9771
|
const total = all.reduce((sum, d) => sum + d.volume, 0);
|
|
9205
9772
|
const pieces = all.map((d) => ({
|
|
9206
9773
|
id: d.id,
|
|
@@ -9215,6 +9782,11 @@ var GameFloorplanStructure = (() => {
|
|
|
9215
9782
|
...d.role === "beam" ? style.lintelAssembly ? { assembly: style.lintelAssembly } : {} : wall.assembly ? { assembly: wall.assembly } : {}
|
|
9216
9783
|
}));
|
|
9217
9784
|
const internal = [];
|
|
9785
|
+
for (const beam of sillBeams) {
|
|
9786
|
+
const sill = drafts.find((d) => d.id === beam.id.replace("_sillbeam_", "_sill_"));
|
|
9787
|
+
if (sill) internal.push({ part: beam.id, on: sill.id, joint: style.lintelJoint });
|
|
9788
|
+
for (const pier of piers) if (Math.min(pier.to, beam.to) - Math.max(pier.from, beam.from) > 0) internal.push({ part: beam.id, on: pier.id, joint: style.lintelJoint });
|
|
9789
|
+
}
|
|
9218
9790
|
lintels.forEach((lintel, k) => {
|
|
9219
9791
|
for (const pier of piers) if (Math.min(pier.to, lintel.to) - Math.max(pier.from, lintel.from) > 0) internal.push({ part: lintel.id, on: pier.id, joint: style.lintelJoint });
|
|
9220
9792
|
const spandrel = spandrels[k];
|
|
@@ -9246,7 +9818,8 @@ var GameFloorplanStructure = (() => {
|
|
|
9246
9818
|
...system.timber ? {} : { lintelAssembly: "reinforced_concrete" },
|
|
9247
9819
|
lintelJoint: system.joints.beam,
|
|
9248
9820
|
wallJoint: system.joints.wall,
|
|
9249
|
-
...lintelDepth && !system.timber ? { lintelDepth } : {}
|
|
9821
|
+
...lintelDepth && !system.timber ? { lintelDepth } : {},
|
|
9822
|
+
...construction.system === "masonry" ? { sillBeam: true } : {}
|
|
9250
9823
|
});
|
|
9251
9824
|
if (split.pieces.length > 1) splits.set(part.id, split);
|
|
9252
9825
|
}
|
|
@@ -9254,7 +9827,18 @@ var GameFloorplanStructure = (() => {
|
|
|
9254
9827
|
}
|
|
9255
9828
|
function applyPanels(input, structure, detail, lintelDepth) {
|
|
9256
9829
|
if (detail === "coarse") return input;
|
|
9257
|
-
return applySplits(input, panelSplits(input, structure, lintelDepth));
|
|
9830
|
+
return onJambPillars(applySplits(input, panelSplits(input, structure, lintelDepth)));
|
|
9831
|
+
}
|
|
9832
|
+
var OPENING_BEAM = /_(lintel|sillbeam)_\d+$/;
|
|
9833
|
+
var overlaps2 = (a, b) => a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1];
|
|
9834
|
+
function onJambPillars(split) {
|
|
9835
|
+
const pillars = split.parts.filter((part) => part.role === "column" && part.material === "concrete");
|
|
9836
|
+
const extra = [];
|
|
9837
|
+
for (const beam of split.parts) {
|
|
9838
|
+
if (beam.role !== "beam" || !OPENING_BEAM.test(beam.id)) continue;
|
|
9839
|
+
for (const pillar of pillars) if (pillar.floor === beam.floor && overlaps2(pillar.aabb, beam.aabb) && pillar.y0 <= beam.y0 && pillar.y1 >= beam.y1 - 0.01) extra.push({ part: beam.id, on: pillar.id, joint: "rebar" });
|
|
9840
|
+
}
|
|
9841
|
+
return extra.length ? { parts: split.parts, supports: [...split.supports, ...extra] } : split;
|
|
9258
9842
|
}
|
|
9259
9843
|
function applyUnits(input, structure) {
|
|
9260
9844
|
const splits = /* @__PURE__ */ new Map();
|
|
@@ -9318,21 +9902,35 @@ var GameFloorplanStructure = (() => {
|
|
|
9318
9902
|
};
|
|
9319
9903
|
var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
|
|
9320
9904
|
var CAGED_ROLES = /* @__PURE__ */ new Set(["beam", "column"]);
|
|
9905
|
+
var CAGE_COVER = 0.03;
|
|
9906
|
+
var CAGE_BUCKLING_SHARE = 0.5;
|
|
9907
|
+
var DEFAULT_RESIDUAL = 0.15;
|
|
9908
|
+
function cageResidualCapacity(size, reinforcement, capacity) {
|
|
9909
|
+
const [a = 0, b = 0] = [...size].sort((x, y) => x - y);
|
|
9910
|
+
const perSide = (w) => Math.max(2, Math.floor(Math.max(0, w - 2 * CAGE_COVER) / reinforcement.spacing) + 1);
|
|
9911
|
+
const bars = 2 * perSide(a) + 2 * perSide(b) - 4;
|
|
9912
|
+
const steel = MATERIALS.find((m) => m.id === reinforcement.material)?.compressive ?? 0;
|
|
9913
|
+
const carries2 = bars * Math.PI * (reinforcement.diameter / 2) ** 2 * steel * CAGE_BUCKLING_SHARE;
|
|
9914
|
+
if (!capacity || capacity <= 0 || carries2 <= 0) return DEFAULT_RESIDUAL;
|
|
9915
|
+
return Math.min(1, Math.max(DEFAULT_RESIDUAL, carries2 / capacity));
|
|
9916
|
+
}
|
|
9321
9917
|
var caged = (spec, part, rebar) => {
|
|
9322
9918
|
if (!rebar || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
|
|
9323
9919
|
const size = spec.shape.kind === "box" ? spec.shape.size : null;
|
|
9324
9920
|
if (!size) return spec;
|
|
9325
9921
|
const thin = Math.min(...size);
|
|
9922
|
+
const reinforcement = { material: rebar, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter };
|
|
9326
9923
|
return {
|
|
9327
9924
|
...spec,
|
|
9328
|
-
reinforcement: {
|
|
9925
|
+
reinforcement: part.role === "column" ? { ...reinforcement, residualCapacity: cageResidualCapacity(size, reinforcement, spec.structure?.capacity) } : reinforcement,
|
|
9329
9926
|
units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
|
|
9330
9927
|
};
|
|
9331
9928
|
};
|
|
9332
9929
|
var partSpecOf = (part, capacity, detailed, rebar) => {
|
|
9333
9930
|
if (detailed && part.assembly) return assemblySpec(part, capacity);
|
|
9334
9931
|
const spec = boxSpec(part, capacity);
|
|
9335
|
-
|
|
9932
|
+
const out = detailed ? caged(spec, part, rebar) : spec;
|
|
9933
|
+
return part.beamKind === "ground_beam" ? { ...out, anchored: true, structure: { role: "foundation" } } : out;
|
|
9336
9934
|
};
|
|
9337
9935
|
function toDestructibleSpec(structure, options = {}) {
|
|
9338
9936
|
const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
|
|
@@ -9348,7 +9946,9 @@ var GameFloorplanStructure = (() => {
|
|
|
9348
9946
|
const walled = applyPanels(slabbed, structure, detail, slabOptions && structure.construction.frame.rebar ? (span) => beamDepthFor(span, slabOptions) : void 0);
|
|
9349
9947
|
const panels = slabOptions && detail !== "coarse" ? deepenBondBeams(walled, structure, slabOptions) : walled;
|
|
9350
9948
|
const sizing = { safety, redundancy, safetyOthers };
|
|
9351
|
-
const
|
|
9949
|
+
const foundation = new Set(panels.parts.filter((part) => part.beamKind === "ground_beam").map((part) => part.id));
|
|
9950
|
+
const onFoundation = foundation.size ? panels.supports.filter((s) => !foundation.has(s.part)).map((s) => foundation.has(s.on) ? { ...s, on: GROUND } : s) : panels.supports;
|
|
9951
|
+
const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts.filter((part) => !foundation.has(part.id)), onFoundation, sizing) };
|
|
9352
9952
|
const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
|
|
9353
9953
|
const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
|
|
9354
9954
|
const margin = options.terrainMargin ?? TERRAIN_MARGIN;
|
|
@@ -9414,6 +10014,359 @@ var GameFloorplanStructure = (() => {
|
|
|
9414
10014
|
return out;
|
|
9415
10015
|
}
|
|
9416
10016
|
|
|
10017
|
+
// src/validate/codes.ts
|
|
10018
|
+
var STRUCTURAL_VIOLATION = {
|
|
10019
|
+
/** A pillar with no load path to the ground, or that starts above what it rests on. */
|
|
10020
|
+
PILLAR_NOT_GROUNDED: "PILLAR_NOT_GROUNDED",
|
|
10021
|
+
/** A pillar of a masonry wall that stops short of the bond beam, or that the bond beam does not rest on. */
|
|
10022
|
+
PILLAR_DOES_NOT_REACH_BOND_BEAM: "PILLAR_DOES_NOT_REACH_BOND_BEAM",
|
|
10023
|
+
/** A masonry wall that runs more than `MAX_PILLAR_SPACING` between two of its pillars. */
|
|
10024
|
+
WALL_SPAN_WITHOUT_PILLAR: "WALL_SPAN_WITHOUT_PILLAR",
|
|
10025
|
+
/** A free end of a masonry wall (a door jamb, the end of a garden wall) with no pillar. */
|
|
10026
|
+
WALL_END_WITHOUT_PILLAR: "WALL_END_WITHOUT_PILLAR",
|
|
10027
|
+
/** Two masonry walls meet at a corner with no pillar there. */
|
|
10028
|
+
CORNER_WITHOUT_PILLAR: "CORNER_WITHOUT_PILLAR",
|
|
10029
|
+
/** Another wall meets a masonry wall inside its length (a T) with no pillar there. */
|
|
10030
|
+
WALL_JUNCTION_WITHOUT_PILLAR: "WALL_JUNCTION_WITHOUT_PILLAR",
|
|
10031
|
+
/** A window or door in a masonry wall with no lintel (verga) over it. */
|
|
10032
|
+
OPENING_WITHOUT_LINTEL: "OPENING_WITHOUT_LINTEL",
|
|
10033
|
+
/** A window in a masonry wall with no sill beam (contraverga) under it. */
|
|
10034
|
+
OPENING_WITHOUT_SILL: "OPENING_WITHOUT_SILL",
|
|
10035
|
+
/** An exterior masonry wall with no bond beam (cinta de amarração) on top. */
|
|
10036
|
+
MISSING_BOND_BEAM: "MISSING_BOND_BEAM",
|
|
10037
|
+
/** A ground-floor masonry wall standing on the soil with no ground beam (viga baldrame) or foundation under it. */
|
|
10038
|
+
MISSING_GROUND_BEAM: "MISSING_GROUND_BEAM",
|
|
10039
|
+
/** A part with no load path down to the ground. */
|
|
10040
|
+
FLOATING_PART: "FLOATING_PART",
|
|
10041
|
+
/** A concrete beam longer than a lintel that rests on masonry alone (no pillar, beam or slab carries it). */
|
|
10042
|
+
BEAM_ON_WEAK_MASONRY: "BEAM_ON_WEAK_MASONRY",
|
|
10043
|
+
/** A concrete pillar or beam below the NBR 6118 minimum section, or a beam between point supports shallower than a tenth of its span. */
|
|
10044
|
+
CONCRETE_BELOW_MIN_SECTION: "CONCRETE_BELOW_MIN_SECTION",
|
|
10045
|
+
/** A concrete pillar or beam with no steel (the construction has no rebar). */
|
|
10046
|
+
UNREINFORCED_CONCRETE: "UNREINFORCED_CONCRETE",
|
|
10047
|
+
/** A brick or block wall laid in stack bond: the vertical joints line up and the wall splits along them. */
|
|
10048
|
+
UNBONDED_MASONRY: "UNBONDED_MASONRY",
|
|
10049
|
+
/** A plastered brick wall without plaster on both faces. */
|
|
10050
|
+
MISSING_PLASTER: "MISSING_PLASTER"
|
|
10051
|
+
};
|
|
10052
|
+
var MAX_PILLAR_SPACING = 4;
|
|
10053
|
+
var MIN_PILLAR_SIDE = 0.19;
|
|
10054
|
+
var MIN_PILLAR_AREA = 0.036;
|
|
10055
|
+
var MIN_BEAM_WIDTH = 0.12;
|
|
10056
|
+
var BEAM_DEPTH_RATIO = 10;
|
|
10057
|
+
var MAX_LINTEL_ON_MASONRY = 3;
|
|
10058
|
+
|
|
10059
|
+
// src/validate/load-path.ts
|
|
10060
|
+
var CONTACT = 0.05;
|
|
10061
|
+
var EPSILON = 5e-3;
|
|
10062
|
+
function groundedParts(parts, supports) {
|
|
10063
|
+
const ids = new Set(parts.map((part) => part.id));
|
|
10064
|
+
const grounded = /* @__PURE__ */ new Set([GROUND]);
|
|
10065
|
+
const bearing = supports.filter((s) => s.bears !== false && ids.has(s.part));
|
|
10066
|
+
let grew = true;
|
|
10067
|
+
while (grew) {
|
|
10068
|
+
grew = false;
|
|
10069
|
+
for (const support of bearing) {
|
|
10070
|
+
if (grounded.has(support.part) || !grounded.has(support.on)) continue;
|
|
10071
|
+
grounded.add(support.part);
|
|
10072
|
+
grew = true;
|
|
10073
|
+
}
|
|
10074
|
+
}
|
|
10075
|
+
return grounded;
|
|
10076
|
+
}
|
|
10077
|
+
function loadPathViolations(structure) {
|
|
10078
|
+
const { parts, supports } = structure;
|
|
10079
|
+
const grounded = groundedParts(parts, supports);
|
|
10080
|
+
const byId = new Map(parts.map((part) => [part.id, part]));
|
|
10081
|
+
const out = [];
|
|
10082
|
+
for (const part of parts) {
|
|
10083
|
+
if (part.role === "none") continue;
|
|
10084
|
+
const pillar = part.role === "column";
|
|
10085
|
+
if (!grounded.has(part.id)) {
|
|
10086
|
+
out.push({ code: pillar ? STRUCTURAL_VIOLATION.PILLAR_NOT_GROUNDED : STRUCTURAL_VIOLATION.FLOATING_PART, part: part.id, message: `${part.id} has no load path to the ground` });
|
|
10087
|
+
continue;
|
|
10088
|
+
}
|
|
10089
|
+
if (!pillar) continue;
|
|
10090
|
+
const bases = supports.filter((s) => s.part === part.id && s.bears !== false);
|
|
10091
|
+
const standsOnBase = bases.some((s) => {
|
|
10092
|
+
if (s.on === GROUND) return part.y0 <= CONTACT;
|
|
10093
|
+
const base = byId.get(s.on);
|
|
10094
|
+
return base !== void 0 && part.y0 >= base.y0 - EPSILON && part.y0 <= base.y1 + CONTACT;
|
|
10095
|
+
});
|
|
10096
|
+
if (!standsOnBase) out.push({ code: STRUCTURAL_VIOLATION.PILLAR_NOT_GROUNDED, part: part.id, message: `${part.id} starts at ${part.y0} m, above what it rests on` });
|
|
10097
|
+
}
|
|
10098
|
+
return out;
|
|
10099
|
+
}
|
|
10100
|
+
var alongOf2 = (part) => {
|
|
10101
|
+
const alongU = part.aabb[2] - part.aabb[0] >= part.aabb[3] - part.aabb[1];
|
|
10102
|
+
return alongU ? { alongU, from: part.aabb[0], to: part.aabb[2], width: part.aabb[3] - part.aabb[1] } : { alongU, from: part.aabb[1], to: part.aabb[3], width: part.aabb[2] - part.aabb[0] };
|
|
10103
|
+
};
|
|
10104
|
+
function bearingsOf(beam, supports, byId) {
|
|
10105
|
+
const frame2 = alongOf2(beam);
|
|
10106
|
+
return supports.filter((s) => s.part === beam.id && s.bears !== false && s.on !== GROUND).map((s) => byId.get(s.on)).filter((on) => on !== void 0).map((on) => {
|
|
10107
|
+
const [low, high] = frame2.alongU ? [on.aabb[0], on.aabb[2]] : [on.aabb[1], on.aabb[3]];
|
|
10108
|
+
return { on, from: Math.max(frame2.from, low), to: Math.min(frame2.to, high) };
|
|
10109
|
+
}).filter((bearing) => bearing.to - bearing.from > -EPSILON);
|
|
10110
|
+
}
|
|
10111
|
+
var isWallLike = (part) => part.role === "wall" || part.role === "partition";
|
|
10112
|
+
var PEDESTAL_RATIO = 3;
|
|
10113
|
+
var isPedestal = (part) => part.role === "column" && part.y1 - part.y0 <= PEDESTAL_RATIO * Math.min(part.aabb[2] - part.aabb[0], part.aabb[3] - part.aabb[1]);
|
|
10114
|
+
function concreteViolations(structure, isMasonry) {
|
|
10115
|
+
const { parts, supports } = structure;
|
|
10116
|
+
const byId = new Map(parts.map((part) => [part.id, part]));
|
|
10117
|
+
const out = [];
|
|
10118
|
+
for (const part of parts) {
|
|
10119
|
+
if (part.material !== "concrete" || part.role !== "column" && part.role !== "beam") continue;
|
|
10120
|
+
if (!structure.construction.frame.rebar && !isPedestal(part)) out.push({ code: STRUCTURAL_VIOLATION.UNREINFORCED_CONCRETE, part: part.id, message: `${part.id} is concrete without steel` });
|
|
10121
|
+
if (part.role === "column") {
|
|
10122
|
+
const sides = [part.aabb[2] - part.aabb[0], part.aabb[3] - part.aabb[1]];
|
|
10123
|
+
if (Math.min(...sides) < MIN_PILLAR_SIDE - EPSILON || sides[0] * sides[1] < MIN_PILLAR_AREA - EPSILON)
|
|
10124
|
+
out.push({ code: STRUCTURAL_VIOLATION.CONCRETE_BELOW_MIN_SECTION, part: part.id, message: `${part.id} is ${sides.map((s) => s.toFixed(2)).join(" x ")} m: NBR 6118 asks at least ${MIN_PILLAR_SIDE} m a side and ${MIN_PILLAR_AREA} m\xB2` });
|
|
10125
|
+
continue;
|
|
10126
|
+
}
|
|
10127
|
+
const castOn = supports.filter((s) => s.on === part.id && s.bears !== false).map((s) => byId.get(s.part)).filter((top) => top !== void 0 && top.role === "beam" && top.material === "concrete" && Math.abs(top.y0 - part.y1) < 0.01).reduce((most, top) => Math.max(most, top.y1 - top.y0), 0);
|
|
10128
|
+
out.push(...beamViolations(part, bearingsOf(part, supports, byId), { isMasonry, castOn }));
|
|
10129
|
+
}
|
|
10130
|
+
return out;
|
|
10131
|
+
}
|
|
10132
|
+
function effectiveSpan(bearings, depth) {
|
|
10133
|
+
const sorted = [...bearings].sort((a, b) => a.from - b.from);
|
|
10134
|
+
let most = 0;
|
|
10135
|
+
for (let k = 1; k < sorted.length; k++) {
|
|
10136
|
+
const previous = sorted.slice(0, k).reduce((a, b) => b.to > a.to ? b : a);
|
|
10137
|
+
const next = sorted[k];
|
|
10138
|
+
const clear = next.from - previous.to;
|
|
10139
|
+
if (clear <= 0) continue;
|
|
10140
|
+
most = Math.max(most, clear + Math.min((previous.to - previous.from) / 2, 0.3 * depth) + Math.min((next.to - next.from) / 2, 0.3 * depth));
|
|
10141
|
+
}
|
|
10142
|
+
return most;
|
|
10143
|
+
}
|
|
10144
|
+
function beamViolations(beam, bearings, context) {
|
|
10145
|
+
const out = [];
|
|
10146
|
+
const frame2 = alongOf2(beam);
|
|
10147
|
+
const length = frame2.to - frame2.from;
|
|
10148
|
+
const depth = beam.y1 - beam.y0 + context.castOn;
|
|
10149
|
+
if (frame2.width < MIN_BEAM_WIDTH - EPSILON) out.push({ code: STRUCTURAL_VIOLATION.CONCRETE_BELOW_MIN_SECTION, part: beam.id, message: `${beam.id} is ${frame2.width.toFixed(2)} m wide: NBR 6118 asks at least ${MIN_BEAM_WIDTH} m` });
|
|
10150
|
+
if (!bearings.length) return out;
|
|
10151
|
+
const continuous = bearings.some((b) => isWallLike(b.on) && b.to - b.from >= length / 2);
|
|
10152
|
+
const span = continuous ? 0 : effectiveSpan(bearings, depth);
|
|
10153
|
+
if (depth < span / BEAM_DEPTH_RATIO - EPSILON) out.push({ code: STRUCTURAL_VIOLATION.CONCRETE_BELOW_MIN_SECTION, part: beam.id, message: `${beam.id} is ${depth.toFixed(2)} m deep over a ${span.toFixed(2)} m span: it needs at least L/${BEAM_DEPTH_RATIO} = ${(span / BEAM_DEPTH_RATIO).toFixed(2)} m` });
|
|
10154
|
+
if (length > MAX_LINTEL_ON_MASONRY && bearings.every((b) => context.isMasonry(b.on)))
|
|
10155
|
+
out.push({ code: STRUCTURAL_VIOLATION.BEAM_ON_WEAK_MASONRY, part: beam.id, message: `${beam.id} spans ${length.toFixed(2)} m on masonry alone: no pillar carries it` });
|
|
10156
|
+
return out;
|
|
10157
|
+
}
|
|
10158
|
+
|
|
10159
|
+
// src/validate/masonry.ts
|
|
10160
|
+
var TOLERANCE3 = 0.05;
|
|
10161
|
+
var AT_POINT = 0.45;
|
|
10162
|
+
var EXPOSED_FINISHES = ["exposed_brick", "exposed_block"];
|
|
10163
|
+
var lineOf2 = (part) => {
|
|
10164
|
+
const alongU = part.aabb[2] - part.aabb[0] >= part.aabb[3] - part.aabb[1];
|
|
10165
|
+
const half = (alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0]) / 2;
|
|
10166
|
+
const [from, to] = alongU ? [part.aabb[0], part.aabb[2]] : [part.aabb[1], part.aabb[3]];
|
|
10167
|
+
return { part, alongU, line: alongU ? part.aabb[1] + half : part.aabb[0] + half, from: from + half, to: to - half, half };
|
|
10168
|
+
};
|
|
10169
|
+
function isMasonryWall(part) {
|
|
10170
|
+
if (!part.id.startsWith("wall_") || !part.assembly || part.role !== "wall" && part.role !== "partition") return false;
|
|
10171
|
+
const units = getAssembly(part.assembly)?.units;
|
|
10172
|
+
return units !== void 0 && units.mortar !== "concrete";
|
|
10173
|
+
}
|
|
10174
|
+
var pointOf2 = (wall, along) => wall.alongU ? [along, wall.line] : [wall.line, along];
|
|
10175
|
+
var alongOfPoint = (wall, point) => wall.alongU ? point[0] : point[1];
|
|
10176
|
+
var acrossOfPoint = (wall, point) => wall.alongU ? point[1] : point[0];
|
|
10177
|
+
var centre = (part) => [(part.aabb[0] + part.aabb[2]) / 2, (part.aabb[1] + part.aabb[3]) / 2];
|
|
10178
|
+
var onLine = (wall, point, slack = 0) => Math.abs(acrossOfPoint(wall, point) - wall.line) <= wall.half + TOLERANCE3 && alongOfPoint(wall, point) >= wall.from - wall.half - slack && alongOfPoint(wall, point) <= wall.to + wall.half + slack;
|
|
10179
|
+
function endKind(wall, along, others) {
|
|
10180
|
+
const point = pointOf2(wall, along);
|
|
10181
|
+
const meeting = others.filter((other) => onLine(other, point));
|
|
10182
|
+
const across = meeting.filter((other) => other.alongU !== wall.alongU);
|
|
10183
|
+
const continued = meeting.length > across.length;
|
|
10184
|
+
if (!across.length) return continued ? "continued" : "free";
|
|
10185
|
+
const atEnd = (other) => Math.min(Math.abs(alongOfPoint(other, point) - other.from), Math.abs(alongOfPoint(other, point) - other.to)) <= other.half + wall.half + TOLERANCE3;
|
|
10186
|
+
return !continued && across.every(atEnd) ? "corner" : "junction";
|
|
10187
|
+
}
|
|
10188
|
+
var masonryBetween = (from, to, windows) => windows.reduce((left, [a, b]) => left - Math.max(0, Math.min(to, b) - Math.max(from, a)), to - from);
|
|
10189
|
+
function pointViolations(context, others) {
|
|
10190
|
+
const { wall, stations } = context;
|
|
10191
|
+
const out = [];
|
|
10192
|
+
const covered = (along) => stations.some((at2) => Math.abs(at2 - along) <= AT_POINT);
|
|
10193
|
+
for (const along of [wall.from, wall.to]) {
|
|
10194
|
+
const kind = endKind(wall, along, others);
|
|
10195
|
+
if (kind === "continued" || covered(along)) continue;
|
|
10196
|
+
const code = kind === "corner" ? STRUCTURAL_VIOLATION.CORNER_WITHOUT_PILLAR : kind === "junction" ? STRUCTURAL_VIOLATION.WALL_JUNCTION_WITHOUT_PILLAR : STRUCTURAL_VIOLATION.WALL_END_WITHOUT_PILLAR;
|
|
10197
|
+
out.push({ code, part: wall.part.id, message: `${wall.part.id} has no pillar at its ${kind} end (${along.toFixed(2)} m along)` });
|
|
10198
|
+
}
|
|
10199
|
+
for (const other of others) {
|
|
10200
|
+
if (other.alongU === wall.alongU) continue;
|
|
10201
|
+
for (const end of [other.from, other.to]) {
|
|
10202
|
+
const point = pointOf2(other, end);
|
|
10203
|
+
const at2 = alongOfPoint(wall, point);
|
|
10204
|
+
if (!onLine(wall, point) || at2 <= wall.from + AT_POINT || at2 >= wall.to - AT_POINT || covered(at2)) continue;
|
|
10205
|
+
out.push({ code: STRUCTURAL_VIOLATION.WALL_JUNCTION_WITHOUT_PILLAR, part: wall.part.id, message: `${other.part.id} meets ${wall.part.id} at ${at2.toFixed(2)} m along with no pillar there` });
|
|
10206
|
+
}
|
|
10207
|
+
}
|
|
10208
|
+
const line = [...stations].sort((a, b) => a - b);
|
|
10209
|
+
for (let k = 1; k < line.length; k++) {
|
|
10210
|
+
const gap = masonryBetween(line[k - 1], line[k], context.windows);
|
|
10211
|
+
if (gap > MAX_PILLAR_SPACING + TOLERANCE3) out.push({ code: STRUCTURAL_VIOLATION.WALL_SPAN_WITHOUT_PILLAR, part: wall.part.id, message: `${wall.part.id} runs ${gap.toFixed(2)} m between pillars (at most ${MAX_PILLAR_SPACING} m)` });
|
|
10212
|
+
}
|
|
10213
|
+
if (!line.length && wall.to - wall.from > MAX_PILLAR_SPACING) out.push({ code: STRUCTURAL_VIOLATION.WALL_SPAN_WITHOUT_PILLAR, part: wall.part.id, message: `${wall.part.id} runs ${(wall.to - wall.from).toFixed(2)} m with no pillar` });
|
|
10214
|
+
return out;
|
|
10215
|
+
}
|
|
10216
|
+
function bondBeamViolations(context, structure) {
|
|
10217
|
+
const { wall, pillars } = context;
|
|
10218
|
+
const resting = new Set(structure.supports.filter((s) => s.on === wall.part.id).map((s) => s.part));
|
|
10219
|
+
const beams = structure.parts.filter((p) => p.beamKind === "bond_beam" && resting.has(p.id));
|
|
10220
|
+
if (!beams.length) return [{ code: STRUCTURAL_VIOLATION.MISSING_BOND_BEAM, part: wall.part.id, message: `${wall.part.id} has no bond beam on top` }];
|
|
10221
|
+
const out = [];
|
|
10222
|
+
for (const pillar of pillars) {
|
|
10223
|
+
const [u, v] = centre(pillar);
|
|
10224
|
+
const over = beams.filter((b) => u >= b.aabb[0] - TOLERANCE3 && u <= b.aabb[2] + TOLERANCE3 && v >= b.aabb[1] - TOLERANCE3 && v <= b.aabb[3] + TOLERANCE3);
|
|
10225
|
+
const carries2 = over.some((b) => pillar.y1 >= b.y0 - TOLERANCE3 && structure.supports.some((s) => s.part === b.id && s.on === pillar.id && s.bears !== false));
|
|
10226
|
+
if (!carries2) out.push({ code: STRUCTURAL_VIOLATION.PILLAR_DOES_NOT_REACH_BOND_BEAM, part: pillar.id, message: `${pillar.id} stops at ${pillar.y1} m and does not carry the bond beam of ${wall.part.id}` });
|
|
10227
|
+
}
|
|
10228
|
+
return out;
|
|
10229
|
+
}
|
|
10230
|
+
function assemblyViolations(part, finish) {
|
|
10231
|
+
const def = getAssembly(part.assembly ?? "");
|
|
10232
|
+
if (!def) return [];
|
|
10233
|
+
const out = [];
|
|
10234
|
+
if (def.units && def.units.bond !== "running") out.push({ code: STRUCTURAL_VIOLATION.UNBONDED_MASONRY, part: part.id, message: `${part.id} is laid in ${def.units.bond} bond: the joints must be staggered` });
|
|
10235
|
+
const plastered = (def.layers ?? []).some((layer) => layer.material === "plaster" && layer.faces === "both");
|
|
10236
|
+
if (def.core.material === "brick" && !plastered && !EXPOSED_FINISHES.includes(finish ?? "")) out.push({ code: STRUCTURAL_VIOLATION.MISSING_PLASTER, part: part.id, message: `${part.id} is brick without plaster on both faces` });
|
|
10237
|
+
return out;
|
|
10238
|
+
}
|
|
10239
|
+
function masonryViolations(structure) {
|
|
10240
|
+
const walls = structure.parts.filter(isMasonryWall);
|
|
10241
|
+
const columns = structure.parts.filter((p) => p.role === "column" && p.material === "concrete");
|
|
10242
|
+
const out = [];
|
|
10243
|
+
for (const part of walls) {
|
|
10244
|
+
out.push(...assemblyViolations(part, structure.construction.walls.finish));
|
|
10245
|
+
if (part.floor === 0 && structure.supports.some((s) => s.part === part.id && s.on === GROUND))
|
|
10246
|
+
out.push({ code: STRUCTURAL_VIOLATION.MISSING_GROUND_BEAM, part: part.id, message: `${part.id} stands on the soil with no ground beam under it` });
|
|
10247
|
+
if (!part.exterior) continue;
|
|
10248
|
+
const wall = lineOf2(part);
|
|
10249
|
+
const pillars = columns.filter((c) => c.floor === part.floor && onLine(wall, centre(c), AT_POINT) && Math.min(c.y1, part.y1) - Math.max(c.y0, part.y0) > 0.5);
|
|
10250
|
+
const windows = structure.openings.filter((o) => o.wall === part.id).map((o) => [o.from, o.to]);
|
|
10251
|
+
const context = { wall, pillars, stations: pillars.map((p) => alongOfPoint(wall, centre(p))), windows };
|
|
10252
|
+
const others = walls.filter((w) => w !== part && w.floor === part.floor).map(lineOf2);
|
|
10253
|
+
out.push(...pointViolations(context, others), ...bondBeamViolations(context, structure));
|
|
10254
|
+
}
|
|
10255
|
+
return out;
|
|
10256
|
+
}
|
|
10257
|
+
|
|
10258
|
+
// src/validate/openings.ts
|
|
10259
|
+
var TOLERANCE4 = 0.05;
|
|
10260
|
+
var LOW_WINDOW = SILL_BEAM_DEPTH + 0.05;
|
|
10261
|
+
function beamAcross(parts, wall, span, side, y) {
|
|
10262
|
+
return parts.some((part) => {
|
|
10263
|
+
const off = part[side] - y;
|
|
10264
|
+
if (part.role !== "beam" || off < -TOLERANCE4 || off > (side === "y0" ? LINTEL_DEPTH2 : TOLERANCE4)) return false;
|
|
10265
|
+
const [from, to, low, high] = wall.alongU ? [part.aabb[0], part.aabb[2], part.aabb[1], part.aabb[3]] : [part.aabb[1], part.aabb[3], part.aabb[0], part.aabb[2]];
|
|
10266
|
+
return from <= span.from + TOLERANCE4 && to >= span.to - TOLERANCE4 && low <= wall.line + TOLERANCE4 && high >= wall.line - TOLERANCE4;
|
|
10267
|
+
});
|
|
10268
|
+
}
|
|
10269
|
+
function windowViolations(opening2, wall, built) {
|
|
10270
|
+
const line = lineOf2(wall);
|
|
10271
|
+
const out = [];
|
|
10272
|
+
if (!beamAcross(built, line, opening2, "y0", opening2.y1)) out.push({ code: STRUCTURAL_VIOLATION.OPENING_WITHOUT_LINTEL, part: opening2.id, message: `window ${opening2.id} in ${wall.id} has no lintel over it` });
|
|
10273
|
+
if (opening2.y0 - wall.y0 > LOW_WINDOW && !beamAcross(built, line, opening2, "y1", opening2.y0))
|
|
10274
|
+
out.push({ code: STRUCTURAL_VIOLATION.OPENING_WITHOUT_SILL, part: opening2.id, message: `window ${opening2.id} in ${wall.id} has no sill beam under it` });
|
|
10275
|
+
return out;
|
|
10276
|
+
}
|
|
10277
|
+
function wallsBeside(door, walls) {
|
|
10278
|
+
const along = door.axis === "u" ? door.u : door.v;
|
|
10279
|
+
const across = door.axis === "u" ? door.v : door.u;
|
|
10280
|
+
const from = along - door.width / 2;
|
|
10281
|
+
const to = along + door.width / 2;
|
|
10282
|
+
return walls.filter((w) => w.floor === door.floor).map(lineOf2).filter((w) => w.alongU === (door.axis === "u") && Math.abs(w.line - across) <= TOLERANCE4 && (Math.abs(w.to - from) <= TOLERANCE4 || Math.abs(w.from - to) <= TOLERANCE4));
|
|
10283
|
+
}
|
|
10284
|
+
function openingViolations(structure, built, doors) {
|
|
10285
|
+
const walls = structure.parts.filter(isMasonryWall);
|
|
10286
|
+
const hosts = new Map(walls.map((wall) => [wall.id, wall]));
|
|
10287
|
+
const out = [];
|
|
10288
|
+
for (const opening2 of structure.openings) {
|
|
10289
|
+
const wall = hosts.get(opening2.wall);
|
|
10290
|
+
if (wall) out.push(...windowViolations(opening2, wall, built));
|
|
10291
|
+
}
|
|
10292
|
+
for (const door of doors) {
|
|
10293
|
+
const beside = wallsBeside(door, walls);
|
|
10294
|
+
if (!beside.length) continue;
|
|
10295
|
+
const along = door.axis === "u" ? door.u : door.v;
|
|
10296
|
+
const span = { from: along - door.width / 2, to: along + door.width / 2 };
|
|
10297
|
+
const lintel = beamAcross(built, beside[0], span, "y0", (structure.levels[door.floor] ?? 0) + DOOR_HEIGHT);
|
|
10298
|
+
if (!lintel) out.push({ code: STRUCTURAL_VIOLATION.OPENING_WITHOUT_LINTEL, part: door.id, message: `door ${door.id} has no lintel over it` });
|
|
10299
|
+
}
|
|
10300
|
+
return out;
|
|
10301
|
+
}
|
|
10302
|
+
|
|
10303
|
+
// src/validate/spec.ts
|
|
10304
|
+
var EPSILON2 = 5e-3;
|
|
10305
|
+
var carries = (part) => part?.structure !== void 0 && part.structure.role !== "none";
|
|
10306
|
+
function flows(joint, byId) {
|
|
10307
|
+
const bears = joint.bears ?? (carries(byId.get(joint.a)) && carries(byId.get(joint.b)));
|
|
10308
|
+
if (bears === false) return [];
|
|
10309
|
+
if (bears === "a_on_b") return [[joint.a, joint.b]];
|
|
10310
|
+
if (bears === "b_on_a") return [[joint.b, joint.a]];
|
|
10311
|
+
return [[joint.a, joint.b], [joint.b, joint.a]];
|
|
10312
|
+
}
|
|
10313
|
+
function groundedOf(spec, byId) {
|
|
10314
|
+
const grounded = new Set(spec.parts.filter((p) => p.anchored || p.structure?.role === "foundation").map((p) => p.id));
|
|
10315
|
+
const edges = (spec.joints ?? []).flatMap((joint) => flows(joint, byId));
|
|
10316
|
+
let grew = true;
|
|
10317
|
+
while (grew) {
|
|
10318
|
+
grew = false;
|
|
10319
|
+
for (const [part, on] of edges) {
|
|
10320
|
+
if (grounded.has(part) || !grounded.has(on)) continue;
|
|
10321
|
+
grounded.add(part);
|
|
10322
|
+
grew = true;
|
|
10323
|
+
}
|
|
10324
|
+
}
|
|
10325
|
+
return grounded;
|
|
10326
|
+
}
|
|
10327
|
+
function sectionViolation(part) {
|
|
10328
|
+
if (part.material !== "concrete" || part.shape.kind !== "box") return null;
|
|
10329
|
+
const role = part.structure?.role;
|
|
10330
|
+
const [x, y, z] = part.shape.size;
|
|
10331
|
+
if (role === "column" && (Math.min(x, z) < MIN_PILLAR_SIDE - EPSILON2 || x * z < MIN_PILLAR_AREA - EPSILON2))
|
|
10332
|
+
return { code: STRUCTURAL_VIOLATION.CONCRETE_BELOW_MIN_SECTION, part: part.id, message: `${part.id} is ${x.toFixed(2)} x ${z.toFixed(2)} m: NBR 6118 asks at least ${MIN_PILLAR_SIDE} m a side` };
|
|
10333
|
+
if (role === "beam" && Math.min(x, z) < MIN_BEAM_WIDTH - EPSILON2 && y >= Math.min(x, z))
|
|
10334
|
+
return { code: STRUCTURAL_VIOLATION.CONCRETE_BELOW_MIN_SECTION, part: part.id, message: `${part.id} is ${Math.min(x, z).toFixed(2)} m wide: NBR 6118 asks at least ${MIN_BEAM_WIDTH} m` };
|
|
10335
|
+
return null;
|
|
10336
|
+
}
|
|
10337
|
+
function specViolations(spec) {
|
|
10338
|
+
const byId = new Map(spec.parts.map((part) => [part.id, part]));
|
|
10339
|
+
const grounded = groundedOf(spec, byId);
|
|
10340
|
+
const out = [];
|
|
10341
|
+
for (const part of spec.parts) {
|
|
10342
|
+
if (!carries(part)) continue;
|
|
10343
|
+
if (!grounded.has(part.id)) {
|
|
10344
|
+
const pillar = part.structure.role === "column";
|
|
10345
|
+
out.push({ code: pillar ? STRUCTURAL_VIOLATION.PILLAR_NOT_GROUNDED : STRUCTURAL_VIOLATION.FLOATING_PART, part: part.id, message: `${part.id} has no load path to an anchored part` });
|
|
10346
|
+
}
|
|
10347
|
+
const section = sectionViolation(part);
|
|
10348
|
+
if (section) out.push(section);
|
|
10349
|
+
}
|
|
10350
|
+
return out;
|
|
10351
|
+
}
|
|
10352
|
+
|
|
10353
|
+
// src/validate/index.ts
|
|
10354
|
+
function validateStructure(structure, options = {}) {
|
|
10355
|
+
const detail = options.wallDetail ?? "panels";
|
|
10356
|
+
const built = applyPanels({ parts: structure.parts, supports: structure.supports }, structure, detail);
|
|
10357
|
+
const out = [...loadPathViolations(structure), ...concreteViolations({ ...built, construction: structure.construction }, isMasonryWall)];
|
|
10358
|
+
if (structure.construction.system !== "masonry") return out;
|
|
10359
|
+
return [...out, ...masonryViolations(structure), ...openingViolations(structure, built.parts, options.doors ?? [])];
|
|
10360
|
+
}
|
|
10361
|
+
function validateSpec(spec) {
|
|
10362
|
+
return specViolations(spec);
|
|
10363
|
+
}
|
|
10364
|
+
function countViolations(violations) {
|
|
10365
|
+
const out = {};
|
|
10366
|
+
for (const violation of violations) out[violation.code] = (out[violation.code] ?? 0) + 1;
|
|
10367
|
+
return out;
|
|
10368
|
+
}
|
|
10369
|
+
|
|
9417
10370
|
// src/wall-specimen.ts
|
|
9418
10371
|
var SYSTEM_OF_ASSEMBLY = { log_wall: "log", wood_frame_wall: "wood_frame", sheet_metal_shack: "improvised" };
|
|
9419
10372
|
var DOOR_WIDTH2 = 0.9;
|
|
@@ -9602,7 +10555,7 @@ var GameFloorplanStructure = (() => {
|
|
|
9602
10555
|
parts.push({ id, material: c.material, shape: box(size), position, structure: { role: "partition" }, tags: ["infill"] });
|
|
9603
10556
|
for (const o of on) rests(joints, id, o, "mortar");
|
|
9604
10557
|
};
|
|
9605
|
-
const bay = (side, k, along,
|
|
10558
|
+
const bay = (side, k, along, centre2, fixed, length, c1, c2) => {
|
|
9606
10559
|
const clear = length - g.columnSize;
|
|
9607
10560
|
const win = clear * c.window;
|
|
9608
10561
|
const pier = (clear - win) / 2;
|
|
@@ -9613,11 +10566,11 @@ var GameFloorplanStructure = (() => {
|
|
|
9613
10566
|
wall(`infill_${f}_${side}_${k}_${name}`, size(u, h), [p[0], y, p[2]], on);
|
|
9614
10567
|
};
|
|
9615
10568
|
const id = (name) => `infill_${f}_${side}_${k}_${name}`;
|
|
9616
|
-
put("sill", clear, sill, yb + sill / 2,
|
|
9617
|
-
put("pl", pier, head - sill, yb + sill + (head - sill) / 2,
|
|
9618
|
-
put("pr", pier, head - sill, yb + sill + (head - sill) / 2,
|
|
9619
|
-
put("head", clear, room - head, yb + head + (room - head) / 2,
|
|
9620
|
-
const gp = at2(
|
|
10569
|
+
put("sill", clear, sill, yb + sill / 2, centre2, [c1, c2]);
|
|
10570
|
+
put("pl", pier, head - sill, yb + sill + (head - sill) / 2, centre2 - clear / 2 + pier / 2, [c1, id("sill")]);
|
|
10571
|
+
put("pr", pier, head - sill, yb + sill + (head - sill) / 2, centre2 + clear / 2 - pier / 2, [c2, id("sill")]);
|
|
10572
|
+
put("head", clear, room - head, yb + head + (room - head) / 2, centre2, [c1, c2]);
|
|
10573
|
+
const gp = at2(centre2);
|
|
9621
10574
|
const gid = `glass_${f}_${side}_${k}`;
|
|
9622
10575
|
parts.push({
|
|
9623
10576
|
id: gid,
|