@adatechnology/game-floorplan-structure 2.0.2 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/index.d.ts +487 -316
- package/dist/index.global.js +1028 -69
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +595 -50
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.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" }
|
|
@@ -2811,6 +2963,22 @@ var GameFloorplanStructure = (() => {
|
|
|
2811
2963
|
return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
|
|
2812
2964
|
};
|
|
2813
2965
|
var clamp3 = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
2966
|
+
var splitAlong = (box2, count, axis, rnd) => {
|
|
2967
|
+
const shares = Array.from({ length: count }, () => 0.25 + 1.5 * rnd());
|
|
2968
|
+
const total = shares.reduce((sum, share) => sum + share, 0);
|
|
2969
|
+
const out = [];
|
|
2970
|
+
let from = box2.c[axis] - box2.s[axis] / 2;
|
|
2971
|
+
for (const share of shares) {
|
|
2972
|
+
const length = box2.s[axis] * share / total;
|
|
2973
|
+
const c = [box2.c[0], box2.c[1], box2.c[2]];
|
|
2974
|
+
const size = [box2.s[0], box2.s[1], box2.s[2]];
|
|
2975
|
+
c[axis] = from + length / 2;
|
|
2976
|
+
size[axis] = length;
|
|
2977
|
+
out.push({ c, s: size });
|
|
2978
|
+
from += length;
|
|
2979
|
+
}
|
|
2980
|
+
return out;
|
|
2981
|
+
};
|
|
2814
2982
|
var splinterBox = (inp, box2, limit = inp.maxFragments) => {
|
|
2815
2983
|
const { rnd } = inp;
|
|
2816
2984
|
const dims = box2.s;
|
|
@@ -2820,35 +2988,81 @@ var GameFloorplanStructure = (() => {
|
|
|
2820
2988
|
const w = Math.max(fmin * 1.5, 0.01);
|
|
2821
2989
|
const c = clamp3(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
|
|
2822
2990
|
const maxLen = fmax * 4;
|
|
2823
|
-
const cells =
|
|
2991
|
+
const cells = [];
|
|
2992
|
+
for (const cell of bsp(box2, c, [a, b], rnd)) {
|
|
2993
|
+
const wide = cell.s[a] >= cell.s[b] ? a : b;
|
|
2994
|
+
if (rnd() < 0.4 && cell.s[wide] > fmin * 0.5) for (const part of splitAlong(cell, 2, wide, rnd)) cells.push(part);
|
|
2995
|
+
else cells.push(cell);
|
|
2996
|
+
}
|
|
2824
2997
|
const pieces = [];
|
|
2825
2998
|
for (const cell of cells) {
|
|
2826
|
-
const extra = rnd()
|
|
2999
|
+
const extra = Math.floor(rnd() * 3);
|
|
2827
3000
|
const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
|
|
2828
|
-
for (const p of
|
|
3001
|
+
for (const p of splitAlong(cell, k, g, rnd)) pieces.push(p);
|
|
2829
3002
|
}
|
|
2830
3003
|
return pieces.slice(0, limit).map((p) => ({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] }));
|
|
2831
3004
|
};
|
|
3005
|
+
var PLY = 3e-3;
|
|
3006
|
+
var PLY_GAP = 0.6;
|
|
3007
|
+
var PLY_SHEET = 0.012;
|
|
3008
|
+
var plyBreak = (inp, root, limit) => {
|
|
3009
|
+
const { rnd } = inp;
|
|
3010
|
+
const dims = root.s;
|
|
3011
|
+
const sorted = [0, 1, 2].sort((x, y) => dims[x] - dims[y]);
|
|
3012
|
+
const t = sorted[0];
|
|
3013
|
+
const [u, v] = [sorted[1], sorted[2]].sort((x, y) => x - y);
|
|
3014
|
+
const along = dims[u] >= dims[v] ? u : v;
|
|
3015
|
+
const across = along === u ? v : u;
|
|
3016
|
+
const layers = clamp3(Math.round(dims[t] / PLY / 2), 2, 3);
|
|
3017
|
+
const pitch = dims[t] / layers;
|
|
3018
|
+
const th = Math.min(pitch, PLY_SHEET);
|
|
3019
|
+
const per = Math.max(2, Math.floor(limit / layers));
|
|
3020
|
+
const out = [];
|
|
3021
|
+
for (let k = 0; k < layers; k++) {
|
|
3022
|
+
const layer = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
|
|
3023
|
+
layer.c[t] = root.c[t] + (-dims[t] / 2 + (k + 0.5) * pitch) * (pitch > PLY_SHEET ? 1 : 1 + PLY_GAP);
|
|
3024
|
+
layer.s[t] = th;
|
|
3025
|
+
for (const cell of bsp(layer, per, [u, v], rnd)) {
|
|
3026
|
+
if (out.length >= limit) return out;
|
|
3027
|
+
if (rnd() < 0.35) {
|
|
3028
|
+
const strand = cell.s[across] * (0.15 + 0.2 * rnd());
|
|
3029
|
+
out.push({ center: cell.c, size: [cell.s[along], strand, th * 0.7], q: frame(along, across, t), shape: "splinter", weight: cell.s[along] * strand * th });
|
|
3030
|
+
} else {
|
|
3031
|
+
out.push({ center: cell.c, size: [cell.s[u], cell.s[v], th], q: frame(u, v, t, (rnd() - 0.5) * 0.5), shape: "scrap", weight: cell.s[u] * cell.s[v] * th });
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
return out;
|
|
3036
|
+
};
|
|
3037
|
+
var granules = (inp, box2, count) => bsp(box2, count, [0, 1, 2], inp.rnd).map((b) => ({ center: b.c, size: b.s, q: quat.identity(), shape: "chunk", weight: b.s[0] * b.s[1] * b.s[2] }));
|
|
3038
|
+
var CRUMBS = 6;
|
|
2832
3039
|
var severAt = (inp, root, at2, gap) => {
|
|
2833
3040
|
const dims = root.s;
|
|
2834
3041
|
const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
|
|
2835
3042
|
const half = dims[L] / 2;
|
|
2836
3043
|
const lo = clamp3(at2 - gap / 2, -half, half), hi = clamp3(at2 + gap / 2, -half, half);
|
|
2837
3044
|
const out = [];
|
|
2838
|
-
const
|
|
3045
|
+
const fibrous = inp.form === "slivers" || inp.form === "plies";
|
|
3046
|
+
const [a, b] = [0, 1, 2].filter((i) => i !== L);
|
|
3047
|
+
const stub = (from, to, breakAtEnd) => {
|
|
2839
3048
|
if (to - from < 0.02) return;
|
|
2840
3049
|
const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
|
|
2841
3050
|
c[L] = (from + to) / 2;
|
|
2842
3051
|
sz[L] = to - from;
|
|
2843
|
-
|
|
3052
|
+
const weight = sz[0] * sz[1] * sz[2];
|
|
3053
|
+
if (fibrous) out.push({ center: c, size: [sz[L], sz[a], sz[b]], q: frame(L, a, b, breakAtEnd ? 0 : Math.PI), shape: "slab", weight });
|
|
3054
|
+
else out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight });
|
|
2844
3055
|
};
|
|
2845
|
-
stub(-half, lo);
|
|
2846
|
-
stub(hi, half);
|
|
3056
|
+
stub(-half, lo, true);
|
|
3057
|
+
stub(hi, half, false);
|
|
2847
3058
|
const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
|
|
2848
3059
|
band.c[L] = (lo + hi) / 2;
|
|
2849
3060
|
band.s[L] = Math.max(hi - lo, 0.01);
|
|
2850
|
-
|
|
2851
|
-
|
|
3061
|
+
const room = inp.maxFragments - out.length;
|
|
3062
|
+
if (inp.style === "snap" && inp.form === "plies") for (const p of plyBreak(inp, band, Math.max(3, room))) out.push(p);
|
|
3063
|
+
else if (inp.style === "snap" && inp.form === "granules") for (const p of granules(inp, band, clamp3(room, 3, 12))) out.push(p);
|
|
3064
|
+
else if (inp.style === "snap") {
|
|
3065
|
+
for (const b2 of bsp(band, Math.max(3, Math.min(6, room)), [0, 1, 2], inp.rnd)) out.push({ center: b2.c, size: b2.s, q: quat.identity(), shape: "chunk", weight: b2.s[0] * b2.s[1] * b2.s[2] });
|
|
2852
3066
|
} else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
|
|
2853
3067
|
return out;
|
|
2854
3068
|
};
|
|
@@ -2881,8 +3095,18 @@ var GameFloorplanStructure = (() => {
|
|
|
2881
3095
|
const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
|
|
2882
3096
|
if (style === "snap") {
|
|
2883
3097
|
if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
|
|
3098
|
+
if (inp.form === "plies") return plyBreak(inp, root, maxFragments);
|
|
2884
3099
|
const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
|
|
2885
3100
|
for (const b of bsp(root, n2, [0, 1, 2], rnd)) out.push({ center: b.c, size: b.s, q: idq, shape: "slab", weight: b.s[0] * b.s[1] * b.s[2] });
|
|
3101
|
+
if (inp.form !== "granules") return out;
|
|
3102
|
+
const crumbs = Math.min(CRUMBS, maxFragments - out.length);
|
|
3103
|
+
if (crumbs <= 0) return out;
|
|
3104
|
+
const band = { c: [0, 0, 0], s: [0, 0, 0] };
|
|
3105
|
+
for (let i = 0; i < 3; i++) {
|
|
3106
|
+
band.s[i] = Math.min(dims[i], Math.max(fmin, dims[i] * 0.2));
|
|
3107
|
+
band.c[i] = clamp3(inp.hit[i], -dims[i] / 2 + band.s[i] / 2, dims[i] / 2 - band.s[i] / 2);
|
|
3108
|
+
}
|
|
3109
|
+
for (const p of granules(inp, band, crumbs)) out.push({ ...p, weight: p.weight * 0.5 });
|
|
2886
3110
|
return out;
|
|
2887
3111
|
}
|
|
2888
3112
|
if (style === "crumble" || style === "none") {
|
|
@@ -2989,6 +3213,88 @@ var GameFloorplanStructure = (() => {
|
|
|
2989
3213
|
}
|
|
2990
3214
|
return marks;
|
|
2991
3215
|
};
|
|
3216
|
+
var JOINT_GAP = 1e-4;
|
|
3217
|
+
var MIN_BEARING_SHARE = 0.25;
|
|
3218
|
+
var unsupportedCells = (cells, thin, dims, standing) => {
|
|
3219
|
+
if (thin === 1) return [];
|
|
3220
|
+
const [u, v] = planeAxes(thin);
|
|
3221
|
+
const bottom = -dims[v] / 2;
|
|
3222
|
+
const rowOf = (y) => Math.round((y - bottom) * 1e4);
|
|
3223
|
+
const rows = /* @__PURE__ */ new Map();
|
|
3224
|
+
for (let i = 0; i < cells.length; i++) {
|
|
3225
|
+
if (!standing(i)) continue;
|
|
3226
|
+
const cell = cells[i];
|
|
3227
|
+
const key = rowOf(cell.center[v] - cell.size[v] / 2);
|
|
3228
|
+
const row = rows.get(key);
|
|
3229
|
+
if (row) row.push(i);
|
|
3230
|
+
else rows.set(key, [i]);
|
|
3231
|
+
}
|
|
3232
|
+
const held = /* @__PURE__ */ new Set();
|
|
3233
|
+
const queue = [];
|
|
3234
|
+
for (const i of rows.get(0) ?? []) {
|
|
3235
|
+
held.add(i);
|
|
3236
|
+
queue.push(i);
|
|
3237
|
+
}
|
|
3238
|
+
const overlap = (a, b) => Math.min(a.center[u] + a.size[u] / 2, b.center[u] + b.size[u] / 2) - Math.max(a.center[u] - a.size[u] / 2, b.center[u] - b.size[u] / 2);
|
|
3239
|
+
const climb = () => {
|
|
3240
|
+
while (queue.length > 0) {
|
|
3241
|
+
const cell = cells[queue.pop()];
|
|
3242
|
+
for (const j of rows.get(rowOf(cell.center[v] + cell.size[v] / 2)) ?? []) {
|
|
3243
|
+
const other = cells[j];
|
|
3244
|
+
if (held.has(j) || overlap(cell, other) <= Math.max(JOINT_GAP, MIN_BEARING_SHARE * Math.min(cell.size[u], other.size[u]))) continue;
|
|
3245
|
+
held.add(j);
|
|
3246
|
+
queue.push(j);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
};
|
|
3250
|
+
for (const row of rows.values()) row.sort((a, b) => cells[a].center[u] - cells[b].center[u]);
|
|
3251
|
+
const touching = (a, b) => Math.abs(a.center[u] + a.size[u] / 2 - (b.center[u] - b.size[u] / 2)) <= 1e-3;
|
|
3252
|
+
for (let wedged = true; wedged; ) {
|
|
3253
|
+
climb();
|
|
3254
|
+
wedged = false;
|
|
3255
|
+
for (const row of rows.values()) {
|
|
3256
|
+
for (let k = 1; k < row.length - 1; k++) {
|
|
3257
|
+
const i = row[k];
|
|
3258
|
+
if (held.has(i) || !held.has(row[k - 1]) || !held.has(row[k + 1])) continue;
|
|
3259
|
+
if (!touching(cells[row[k - 1]], cells[i]) || !touching(cells[i], cells[row[k + 1]])) continue;
|
|
3260
|
+
held.add(i);
|
|
3261
|
+
queue.push(i);
|
|
3262
|
+
wedged = true;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
const loose = [];
|
|
3267
|
+
for (const row of rows.values()) for (const i of row) if (!held.has(i)) loose.push(i);
|
|
3268
|
+
return loose;
|
|
3269
|
+
};
|
|
3270
|
+
var clumpCells = (cells, thin, cap) => {
|
|
3271
|
+
if (cells.length <= cap) return [...cells];
|
|
3272
|
+
const [u, v] = planeAxes(thin);
|
|
3273
|
+
let lo0 = Infinity, lo1 = Infinity, hi0 = -Infinity, hi1 = -Infinity;
|
|
3274
|
+
for (const cell of cells) {
|
|
3275
|
+
lo0 = Math.min(lo0, cell.center[u] - cell.size[u] / 2);
|
|
3276
|
+
hi0 = Math.max(hi0, cell.center[u] + cell.size[u] / 2);
|
|
3277
|
+
lo1 = Math.min(lo1, cell.center[v] - cell.size[v] / 2);
|
|
3278
|
+
hi1 = Math.max(hi1, cell.center[v] + cell.size[v] / 2);
|
|
3279
|
+
}
|
|
3280
|
+
let side = Math.sqrt((hi0 - lo0) * (hi1 - lo1) / cap);
|
|
3281
|
+
for (; ; ) {
|
|
3282
|
+
const bins = /* @__PURE__ */ new Map();
|
|
3283
|
+
for (const cell of cells) {
|
|
3284
|
+
const key = `${Math.floor((cell.center[u] - lo0) / side)}:${Math.floor((cell.center[v] - lo1) / side)}`;
|
|
3285
|
+
const bin = bins.get(key) ?? { lo: [Infinity, Infinity, Infinity], hi: [-Infinity, -Infinity, -Infinity] };
|
|
3286
|
+
for (let k = 0; k < 3; k++) {
|
|
3287
|
+
bin.lo[k] = Math.min(bin.lo[k], cell.center[k] - cell.size[k] / 2);
|
|
3288
|
+
bin.hi[k] = Math.max(bin.hi[k], cell.center[k] + cell.size[k] / 2);
|
|
3289
|
+
}
|
|
3290
|
+
bins.set(key, bin);
|
|
3291
|
+
}
|
|
3292
|
+
if (bins.size <= cap) {
|
|
3293
|
+
return [...bins.values()].map(({ lo, hi }) => ({ center: [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2], size: [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]] }));
|
|
3294
|
+
}
|
|
3295
|
+
side *= 1.25;
|
|
3296
|
+
}
|
|
3297
|
+
};
|
|
2992
3298
|
var MAX_SPALLS = 64;
|
|
2993
3299
|
var thinAxisOf = (dims) => dims[0] <= dims[1] && dims[0] <= dims[2] ? 0 : dims[1] <= dims[2] ? 1 : 2;
|
|
2994
3300
|
var resolveLayer = (def, index, mat) => ({ index, def, mat, energy: mat.penetration * def.thickness * 1e3 });
|
|
@@ -3660,7 +3966,7 @@ var GameFloorplanStructure = (() => {
|
|
|
3660
3966
|
eau[j] += e * area * g[i].u * w;
|
|
3661
3967
|
}
|
|
3662
3968
|
}
|
|
3663
|
-
const
|
|
3969
|
+
const centre2 = ea.map((v, j) => v > 0 ? eau[j] / v : 0);
|
|
3664
3970
|
const ei = new Array(n).fill(0);
|
|
3665
3971
|
const ga = new Array(n).fill(0);
|
|
3666
3972
|
for (const i of vertical) {
|
|
@@ -3669,7 +3975,7 @@ var GameFloorplanStructure = (() => {
|
|
|
3669
3975
|
for (let j = 0; j < n; j++) {
|
|
3670
3976
|
const w = share(p, j) / bands[j];
|
|
3671
3977
|
if (w <= 0) continue;
|
|
3672
|
-
ei[j] += e * (g[i].du ** 3 * g[i].dv / 12 + g[i].du * g[i].dv * (g[i].u -
|
|
3978
|
+
ei[j] += e * (g[i].du ** 3 * g[i].dv / 12 + g[i].du * g[i].dv * (g[i].u - centre2[j]) ** 2) * w;
|
|
3673
3979
|
}
|
|
3674
3980
|
}
|
|
3675
3981
|
for (let i = 0; i < parts.length; i++) {
|
|
@@ -3716,7 +4022,7 @@ var GameFloorplanStructure = (() => {
|
|
|
3716
4022
|
for (let j = 0; j < n; j++) {
|
|
3717
4023
|
const w = share(p, j);
|
|
3718
4024
|
if (w <= 0) continue;
|
|
3719
|
-
sum += w * p.elasticPa * c.stiffnessFactor * kappa[j] * g[i].du * g[i].dv * (g[i].u -
|
|
4025
|
+
sum += w * p.elasticPa * c.stiffnessFactor * kappa[j] * g[i].du * g[i].dv * (g[i].u - centre2[j]);
|
|
3720
4026
|
tot += w;
|
|
3721
4027
|
}
|
|
3722
4028
|
axial[i] = tot > 0 ? sum / tot : 0;
|
|
@@ -3968,6 +4274,7 @@ var GameFloorplanStructure = (() => {
|
|
|
3968
4274
|
},
|
|
3969
4275
|
{
|
|
3970
4276
|
id: "wood_soft",
|
|
4277
|
+
splitForm: "slivers",
|
|
3971
4278
|
density: 500,
|
|
3972
4279
|
hpPerKg: 90,
|
|
3973
4280
|
penetration: 9,
|
|
@@ -4013,6 +4320,7 @@ var GameFloorplanStructure = (() => {
|
|
|
4013
4320
|
},
|
|
4014
4321
|
{
|
|
4015
4322
|
id: "wood_hard",
|
|
4323
|
+
splitForm: "slivers",
|
|
4016
4324
|
density: 750,
|
|
4017
4325
|
hpPerKg: 140,
|
|
4018
4326
|
penetration: 14,
|
|
@@ -4057,6 +4365,7 @@ var GameFloorplanStructure = (() => {
|
|
|
4057
4365
|
},
|
|
4058
4366
|
{
|
|
4059
4367
|
id: "plywood",
|
|
4368
|
+
splitForm: "plies",
|
|
4060
4369
|
density: 600,
|
|
4061
4370
|
hpPerKg: 70,
|
|
4062
4371
|
penetration: 7,
|
|
@@ -4104,6 +4413,7 @@ var GameFloorplanStructure = (() => {
|
|
|
4104
4413
|
},
|
|
4105
4414
|
{
|
|
4106
4415
|
id: "mdf",
|
|
4416
|
+
splitForm: "granules",
|
|
4107
4417
|
density: 750,
|
|
4108
4418
|
hpPerKg: 45,
|
|
4109
4419
|
penetration: 6,
|
|
@@ -4270,6 +4580,7 @@ var GameFloorplanStructure = (() => {
|
|
|
4270
4580
|
floor: 60
|
|
4271
4581
|
}
|
|
4272
4582
|
},
|
|
4583
|
+
ricochetDeg: 12,
|
|
4273
4584
|
hitDebris: "chips",
|
|
4274
4585
|
breakDebris: "dust",
|
|
4275
4586
|
visual: { innerColor: 10197652 },
|
|
@@ -4301,6 +4612,7 @@ var GameFloorplanStructure = (() => {
|
|
|
4301
4612
|
0.04,
|
|
4302
4613
|
0.12
|
|
4303
4614
|
],
|
|
4615
|
+
ricochetDeg: 10,
|
|
4304
4616
|
hitDebris: "chips",
|
|
4305
4617
|
breakDebris: "dust",
|
|
4306
4618
|
visual: { innerColor: 12085567 },
|
|
@@ -4358,6 +4670,7 @@ var GameFloorplanStructure = (() => {
|
|
|
4358
4670
|
0.06,
|
|
4359
4671
|
0.3
|
|
4360
4672
|
],
|
|
4673
|
+
ricochetDeg: 15,
|
|
4361
4674
|
hitDebris: "chips",
|
|
4362
4675
|
breakDebris: "dust",
|
|
4363
4676
|
visual: { innerColor: 9407364 },
|
|
@@ -4615,6 +4928,7 @@ var GameFloorplanStructure = (() => {
|
|
|
4615
4928
|
},
|
|
4616
4929
|
{
|
|
4617
4930
|
id: "green_wood",
|
|
4931
|
+
splitForm: "slivers",
|
|
4618
4932
|
density: 650,
|
|
4619
4933
|
hpPerKg: 5,
|
|
4620
4934
|
penetration: 10,
|
|
@@ -4785,6 +5099,7 @@ var GameFloorplanStructure = (() => {
|
|
|
4785
5099
|
0.03,
|
|
4786
5100
|
0.15
|
|
4787
5101
|
],
|
|
5102
|
+
ricochetDeg: 8,
|
|
4788
5103
|
hitDebris: "chips",
|
|
4789
5104
|
breakDebris: "dust",
|
|
4790
5105
|
thermal: {
|
|
@@ -4884,6 +5199,7 @@ var GameFloorplanStructure = (() => {
|
|
|
4884
5199
|
0.02,
|
|
4885
5200
|
0.08
|
|
4886
5201
|
],
|
|
5202
|
+
ricochetDeg: 8,
|
|
4887
5203
|
hitDebris: "chips",
|
|
4888
5204
|
breakDebris: "dust",
|
|
4889
5205
|
thermal: {
|
|
@@ -4895,6 +5211,127 @@ var GameFloorplanStructure = (() => {
|
|
|
4895
5211
|
"brittle"
|
|
4896
5212
|
],
|
|
4897
5213
|
compressive: 1e5
|
|
5214
|
+
},
|
|
5215
|
+
{
|
|
5216
|
+
id: "mud",
|
|
5217
|
+
density: 1700,
|
|
5218
|
+
hpPerKg: 1,
|
|
5219
|
+
penetration: 5,
|
|
5220
|
+
ductility: 0.8,
|
|
5221
|
+
fracture: "crumble",
|
|
5222
|
+
fragmentSize: [
|
|
5223
|
+
0.02,
|
|
5224
|
+
0.08
|
|
5225
|
+
],
|
|
5226
|
+
hitDebris: "dust",
|
|
5227
|
+
breakDebris: "dust",
|
|
5228
|
+
thermal: {
|
|
5229
|
+
conductivity: 0.5,
|
|
5230
|
+
heatCapacity: 3.2
|
|
5231
|
+
},
|
|
5232
|
+
tags: [
|
|
5233
|
+
"soil",
|
|
5234
|
+
"wet"
|
|
5235
|
+
],
|
|
5236
|
+
compressive: 5e3
|
|
5237
|
+
},
|
|
5238
|
+
{
|
|
5239
|
+
id: "clay",
|
|
5240
|
+
density: 1800,
|
|
5241
|
+
hpPerKg: 5,
|
|
5242
|
+
penetration: 7,
|
|
5243
|
+
ductility: 0.05,
|
|
5244
|
+
fracture: "crumble",
|
|
5245
|
+
fragmentSize: [
|
|
5246
|
+
0.02,
|
|
5247
|
+
0.1
|
|
5248
|
+
],
|
|
5249
|
+
hitDebris: "chips",
|
|
5250
|
+
breakDebris: "dust",
|
|
5251
|
+
thermal: {
|
|
5252
|
+
conductivity: 0.15,
|
|
5253
|
+
heatCapacity: 2.2
|
|
5254
|
+
},
|
|
5255
|
+
tags: [
|
|
5256
|
+
"soil",
|
|
5257
|
+
"brittle"
|
|
5258
|
+
],
|
|
5259
|
+
compressive: 6e4
|
|
5260
|
+
},
|
|
5261
|
+
{
|
|
5262
|
+
id: "snow",
|
|
5263
|
+
density: 300,
|
|
5264
|
+
hpPerKg: 1,
|
|
5265
|
+
penetration: 3,
|
|
5266
|
+
ductility: 0.6,
|
|
5267
|
+
fracture: "crumble",
|
|
5268
|
+
fragmentSize: [
|
|
5269
|
+
0.01,
|
|
5270
|
+
0.05
|
|
5271
|
+
],
|
|
5272
|
+
hitDebris: "dust",
|
|
5273
|
+
breakDebris: "dust",
|
|
5274
|
+
thermal: {
|
|
5275
|
+
conductivity: 0.1,
|
|
5276
|
+
heatCapacity: 2.1
|
|
5277
|
+
},
|
|
5278
|
+
tags: [
|
|
5279
|
+
"soil",
|
|
5280
|
+
"frozen"
|
|
5281
|
+
],
|
|
5282
|
+
compressive: 1e3
|
|
5283
|
+
},
|
|
5284
|
+
{
|
|
5285
|
+
id: "cobble",
|
|
5286
|
+
density: 2650,
|
|
5287
|
+
hpPerKg: 25,
|
|
5288
|
+
penetration: 25,
|
|
5289
|
+
penetrationClass: "concrete",
|
|
5290
|
+
penetrationScale: 0.9,
|
|
5291
|
+
ductility: 0.03,
|
|
5292
|
+
fracture: "crumble",
|
|
5293
|
+
fragmentSize: [
|
|
5294
|
+
0.05,
|
|
5295
|
+
0.15
|
|
5296
|
+
],
|
|
5297
|
+
ricochetDeg: 15,
|
|
5298
|
+
hitDebris: "chips",
|
|
5299
|
+
breakDebris: "chips",
|
|
5300
|
+
thermal: {
|
|
5301
|
+
conductivity: 0.2,
|
|
5302
|
+
heatCapacity: 2.5
|
|
5303
|
+
},
|
|
5304
|
+
tags: [
|
|
5305
|
+
"masonry",
|
|
5306
|
+
"brittle"
|
|
5307
|
+
],
|
|
5308
|
+
compressive: 3e5
|
|
5309
|
+
},
|
|
5310
|
+
{
|
|
5311
|
+
id: "paver",
|
|
5312
|
+
density: 2300,
|
|
5313
|
+
hpPerKg: 14,
|
|
5314
|
+
penetration: 18,
|
|
5315
|
+
penetrationClass: "concrete",
|
|
5316
|
+
penetrationScale: 1,
|
|
5317
|
+
ductility: 0.03,
|
|
5318
|
+
fracture: "crumble",
|
|
5319
|
+
fragmentSize: [
|
|
5320
|
+
0.04,
|
|
5321
|
+
0.12
|
|
5322
|
+
],
|
|
5323
|
+
ricochetDeg: 12,
|
|
5324
|
+
hitDebris: "chips",
|
|
5325
|
+
breakDebris: "chips",
|
|
5326
|
+
thermal: {
|
|
5327
|
+
conductivity: 0.15,
|
|
5328
|
+
heatCapacity: 2.5
|
|
5329
|
+
},
|
|
5330
|
+
tags: [
|
|
5331
|
+
"masonry",
|
|
5332
|
+
"brittle"
|
|
5333
|
+
],
|
|
5334
|
+
compressive: 35e4
|
|
4898
5335
|
}
|
|
4899
5336
|
];
|
|
4900
5337
|
var material_hints_default = {
|
|
@@ -5107,6 +5544,11 @@ var GameFloorplanStructure = (() => {
|
|
|
5107
5544
|
var BREACH_BLAST_FACTOR = 6;
|
|
5108
5545
|
var BREACH_AREA = 0.25;
|
|
5109
5546
|
var BREACH_BLAST_REACH = 0.12;
|
|
5547
|
+
var MEMBER_CUT_REACH = 0.5;
|
|
5548
|
+
var BLAST_REFERENCE_RESISTANCE = 200 * 1900 * 0.13;
|
|
5549
|
+
var BLAST_EDGE_RAGGED = 0.3;
|
|
5550
|
+
var BLAST_EDGE_LOOSE = 1.35;
|
|
5551
|
+
var BLAST_EDGE_WEAR = 0.6;
|
|
5110
5552
|
var RESIDUAL_CAPACITY = 0.15;
|
|
5111
5553
|
var SEVER_BAND = 0.1;
|
|
5112
5554
|
var SEVER_ASPECT = 3;
|
|
@@ -6359,7 +6801,7 @@ var GameFloorplanStructure = (() => {
|
|
|
6359
6801
|
if (j.st.open) continue;
|
|
6360
6802
|
if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
|
|
6361
6803
|
if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
|
|
6362
|
-
j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
|
|
6804
|
+
j.st.hp -= span === void 0 ? amount : Math.min(amount, j.st.maxHp) * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
|
|
6363
6805
|
if (j.st.hp <= 0) {
|
|
6364
6806
|
j.st.hp = 0;
|
|
6365
6807
|
j.st.open = true;
|
|
@@ -6487,6 +6929,7 @@ var GameFloorplanStructure = (() => {
|
|
|
6487
6929
|
const worn = (_a = p.rt).chips ?? (_a.chips = {});
|
|
6488
6930
|
const cut = (_b = p.rt).craters ?? (_b.craters = {});
|
|
6489
6931
|
const out = [];
|
|
6932
|
+
const wholeBefore = Object.values(worn).filter((w) => w === -1).length;
|
|
6490
6933
|
const shot = kind === "ballistic";
|
|
6491
6934
|
for (let i = 0; i < grid.length; i++) {
|
|
6492
6935
|
const cell = grid[i];
|
|
@@ -6545,6 +6988,7 @@ var GameFloorplanStructure = (() => {
|
|
|
6545
6988
|
emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
|
|
6546
6989
|
debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
|
|
6547
6990
|
}
|
|
6991
|
+
if (Object.values(worn).filter((w) => w === -1).length > wholeBefore) dropUnsupported(o, p, src);
|
|
6548
6992
|
if (p.spec.reinforcement) exposeRebar(o, p, src);
|
|
6549
6993
|
for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
|
|
6550
6994
|
if (p.structural && !isLost(p)) {
|
|
@@ -6648,10 +7092,122 @@ var GameFloorplanStructure = (() => {
|
|
|
6648
7092
|
setStage(o, p, 1, {}, src);
|
|
6649
7093
|
setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
|
|
6650
7094
|
debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
|
|
7095
|
+
dropUnsupported(o, p, src);
|
|
6651
7096
|
const reinforcement = p.spec.reinforcement;
|
|
6652
7097
|
if (reinforcement) exposeRebar(o, p, src);
|
|
6653
7098
|
if (p.structural) solveStructure(o);
|
|
6654
7099
|
};
|
|
7100
|
+
const blastReachOf = (p, energy) => {
|
|
7101
|
+
const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
|
|
7102
|
+
const resistance = Math.max(1, perKg * p.mat.density * p.thickness);
|
|
7103
|
+
return BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3) * clamp22(Math.cbrt(BLAST_REFERENCE_RESISTANCE / resistance), 0.3, 2);
|
|
7104
|
+
};
|
|
7105
|
+
const blastUnits = (o, p, wp, energy, dist, src) => {
|
|
7106
|
+
var _a, _b;
|
|
7107
|
+
if (!p.staged || p.st.stripped || isLost(p) || chipEnergy <= 0) return false;
|
|
7108
|
+
const reach = blastReachOf(p, energy);
|
|
7109
|
+
if (dist >= reach) return false;
|
|
7110
|
+
const radius = Math.sqrt(reach * reach - dist * dist);
|
|
7111
|
+
const [u, v] = planeAxes(p.thin);
|
|
7112
|
+
const lp = toLocal(p.pos, p.q, objLocal(o, wp));
|
|
7113
|
+
const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
|
|
7114
|
+
const grid = gridOf(p);
|
|
7115
|
+
const worn = (_a = p.rt).chips ?? (_a.chips = {});
|
|
7116
|
+
const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
|
|
7117
|
+
const lobes = [rnd() * 2 * Math.PI, rnd() * 2 * Math.PI];
|
|
7118
|
+
const out = [];
|
|
7119
|
+
for (let i = 0; i < grid.length; i++) {
|
|
7120
|
+
const cell = grid[i];
|
|
7121
|
+
if (worn[i] === -1) continue;
|
|
7122
|
+
if (op && p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius) continue;
|
|
7123
|
+
const du = cell.center[u] - lp[u], dv = cell.center[v] - lp[v];
|
|
7124
|
+
const angle = Math.atan2(dv, du);
|
|
7125
|
+
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)));
|
|
7126
|
+
const d = Math.hypot(du, dv);
|
|
7127
|
+
if (d <= edge) {
|
|
7128
|
+
worn[i] = -1;
|
|
7129
|
+
out.push(i);
|
|
7130
|
+
} else if (d <= edge * BLAST_EDGE_LOOSE) {
|
|
7131
|
+
const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
|
|
7132
|
+
worn[i] = Math.max(worn[i] ?? 0, perKg * mass * BLAST_EDGE_WEAR);
|
|
7133
|
+
}
|
|
7134
|
+
}
|
|
7135
|
+
if (out.length === 0) return false;
|
|
7136
|
+
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));
|
|
7137
|
+
for (const i of loose) worn[i] = -1;
|
|
7138
|
+
const blown = out.map((i) => grid[i]), fallen = loose.map((i) => grid[i]);
|
|
7139
|
+
const volume = (cells2) => cells2.reduce((sum, cell) => sum + cell.size[0] * cell.size[1] * cell.size[2], 0);
|
|
7140
|
+
const blownMass = Math.min(p.st.mass, p.mat.density * volume(blown));
|
|
7141
|
+
const fallenMass = Math.min(p.st.mass - blownMass, p.mat.density * volume(fallen));
|
|
7142
|
+
const speed = clamp22(Math.sqrt(2 * energy * 0.02 / Math.max(blownMass, 0.05)), 1, 12);
|
|
7143
|
+
const { c, q } = partXf(o, p);
|
|
7144
|
+
const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
|
|
7145
|
+
const tag = `x${chipped.length}_`;
|
|
7146
|
+
const fragments = [
|
|
7147
|
+
...cellFragments(o, p, clumpCells(blown, p.thin, maxFragments), wp, null, speed, blownMass, tag, true),
|
|
7148
|
+
...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) : []
|
|
7149
|
+
];
|
|
7150
|
+
const cells = [...blown, ...fallen];
|
|
7151
|
+
const holes = localHolesOf(p);
|
|
7152
|
+
p.st.holes = p.st.holes.filter((_, k) => !cells.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
|
|
7153
|
+
const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
|
|
7154
|
+
for (const cell of cells) {
|
|
7155
|
+
chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
|
|
7156
|
+
for (let k = 0; k < 3; k++) {
|
|
7157
|
+
lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
|
|
7158
|
+
hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
|
|
7159
|
+
}
|
|
7160
|
+
}
|
|
7161
|
+
p.st.mass = Math.max(0, p.st.mass - blownMass - fallenMass);
|
|
7162
|
+
p.rt.removed = (p.rt.removed ?? 0) + blownMass + fallenMass;
|
|
7163
|
+
markDamaged(p);
|
|
7164
|
+
setStage(o, p, 1, {}, src);
|
|
7165
|
+
const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
|
|
7166
|
+
const units = cells.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
|
|
7167
|
+
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 });
|
|
7168
|
+
debris(o, p, p.mat.breakDebris, center, vec.normalize(vec.sub(center, wp), [0, 1, 0]), 14, 3, src);
|
|
7169
|
+
if (p.spec.reinforcement) exposeRebar(o, p, src);
|
|
7170
|
+
if (p.structural && !isLost(p)) {
|
|
7171
|
+
solveStructure(o);
|
|
7172
|
+
collapseCheck(o, src);
|
|
7173
|
+
}
|
|
7174
|
+
return true;
|
|
7175
|
+
};
|
|
7176
|
+
const dropUnsupported = (o, p, src) => {
|
|
7177
|
+
var _a, _b;
|
|
7178
|
+
if (!p.spec.units || p.spec.units.mortar === p.spec.material || p.st.stripped || isLost(p)) return 0;
|
|
7179
|
+
const [u, v] = planeAxes(p.thin);
|
|
7180
|
+
const grid = gridOf(p);
|
|
7181
|
+
const worn = (_a = p.rt).chips ?? (_a.chips = {});
|
|
7182
|
+
const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
|
|
7183
|
+
const opened = (cell) => !!op && !!p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius;
|
|
7184
|
+
const loose = unsupportedCells(grid, p.thin, p.dims, (i) => worn[i] !== -1 && !opened(grid[i]));
|
|
7185
|
+
if (loose.length === 0) return 0;
|
|
7186
|
+
for (const i of loose) worn[i] = -1;
|
|
7187
|
+
const fallen = loose.map((i) => grid[i]);
|
|
7188
|
+
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));
|
|
7189
|
+
const { c, q } = partXf(o, p);
|
|
7190
|
+
const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
|
|
7191
|
+
const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
|
|
7192
|
+
for (const cell of fallen) {
|
|
7193
|
+
for (let k = 0; k < 3; k++) {
|
|
7194
|
+
lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
|
|
7195
|
+
hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
|
|
7196
|
+
}
|
|
7197
|
+
}
|
|
7198
|
+
const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
|
|
7199
|
+
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);
|
|
7200
|
+
const holes = localHolesOf(p);
|
|
7201
|
+
p.st.holes = p.st.holes.filter((_, k) => !fallen.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
|
|
7202
|
+
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]] });
|
|
7203
|
+
p.st.mass = Math.max(0, p.st.mass - mass);
|
|
7204
|
+
p.rt.removed = (p.rt.removed ?? 0) + mass;
|
|
7205
|
+
markDamaged(p);
|
|
7206
|
+
const units = fallen.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
|
|
7207
|
+
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 });
|
|
7208
|
+
debris(o, p, p.mat.breakDebris, center, [0, -1, 0], 6, 1, src);
|
|
7209
|
+
return loose.length;
|
|
7210
|
+
};
|
|
6655
7211
|
const stripPart = (o, p, reinforcement, src) => {
|
|
6656
7212
|
const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
|
|
6657
7213
|
p.st.stripped = true;
|
|
@@ -6718,6 +7274,7 @@ var GameFloorplanStructure = (() => {
|
|
|
6718
7274
|
const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
|
|
6719
7275
|
const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
|
|
6720
7276
|
const cutAt2 = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
|
|
7277
|
+
const planRnd = p.mat.splitForm ? splitStream(o.id, p.spec.id, hitLocal) : rnd;
|
|
6721
7278
|
const plan = unitPlan.length > 0 ? unitPlan : planFragments({
|
|
6722
7279
|
style,
|
|
6723
7280
|
dims: p.dims,
|
|
@@ -6726,8 +7283,9 @@ var GameFloorplanStructure = (() => {
|
|
|
6726
7283
|
hit: hitLocal,
|
|
6727
7284
|
fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
|
|
6728
7285
|
maxFragments,
|
|
6729
|
-
rnd,
|
|
7286
|
+
rnd: planRnd,
|
|
6730
7287
|
sheet: p.mat.sheet,
|
|
7288
|
+
...p.mat.splitForm ? { form: p.mat.splitForm } : {},
|
|
6731
7289
|
...cutAt2 !== void 0 ? { cut: { at: cutAt2, gap: SEVER_BAND } } : {}
|
|
6732
7290
|
});
|
|
6733
7291
|
const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
|
|
@@ -6839,11 +7397,11 @@ var GameFloorplanStructure = (() => {
|
|
|
6839
7397
|
o.active = true;
|
|
6840
7398
|
}
|
|
6841
7399
|
};
|
|
6842
|
-
const lightLeaksNear = (
|
|
7400
|
+
const lightLeaksNear = (centre2, radius) => {
|
|
6843
7401
|
for (const o of objects.values()) {
|
|
6844
7402
|
for (const q of o.parts) {
|
|
6845
7403
|
if (isLost(q) || q.rt.lit || q.rt.jet || !leaksFlammable(q)) continue;
|
|
6846
|
-
if (vec.distance(partXf(o, q).c,
|
|
7404
|
+
if (vec.distance(partXf(o, q).c, centre2) - q.bound > radius) continue;
|
|
6847
7405
|
q.rt.lit = true;
|
|
6848
7406
|
o.active = true;
|
|
6849
7407
|
}
|
|
@@ -6866,7 +7424,8 @@ var GameFloorplanStructure = (() => {
|
|
|
6866
7424
|
const fragments = [];
|
|
6867
7425
|
for (let i = 0; i < n; i++) {
|
|
6868
7426
|
const back = !exit || i % 3 === 2;
|
|
6869
|
-
const
|
|
7427
|
+
const plies = p.mat.splitForm === "plies";
|
|
7428
|
+
const len = scale * (fibre ? 1.2 + 1.2 * rnd() : 3 + 4 * rnd()), w = scale * (0.8 + 0.8 * rnd()), t = scale * (fibre ? 0.7 + 0.6 * rnd() : plies ? 0.15 + 0.15 * rnd() : 0.4 + 0.4 * rnd());
|
|
6870
7429
|
const size = [w, w, w];
|
|
6871
7430
|
size[along] = len;
|
|
6872
7431
|
size[(along + 1) % 3] = w;
|
|
@@ -7027,7 +7586,7 @@ var GameFloorplanStructure = (() => {
|
|
|
7027
7586
|
checkIgnite(o, p, src);
|
|
7028
7587
|
}
|
|
7029
7588
|
}
|
|
7030
|
-
wearJoints(o, p, Ein * 0.5, void 0, 0, src);
|
|
7589
|
+
wearJoints(o, p, Ein * 0.5, void 0, 0, src, false, 2 * BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3));
|
|
7031
7590
|
const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
|
|
7032
7591
|
if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
|
|
7033
7592
|
frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
|
|
@@ -7037,7 +7596,10 @@ var GameFloorplanStructure = (() => {
|
|
|
7037
7596
|
const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
|
|
7038
7597
|
res.damage += dealt;
|
|
7039
7598
|
if (p.bend && flexure.enabled) bendByBlast(o, p, point, energy, radius, reach, src);
|
|
7040
|
-
|
|
7599
|
+
const breachReach = BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3);
|
|
7600
|
+
if (p.spec.units && p.spec.units.mortar !== p.spec.material) blastUnits(o, p, wp, energy, dist, src);
|
|
7601
|
+
else if (p.spec.reinforcement && p.spec.units && dist > MEMBER_CUT_REACH * breachReach) blastUnits(o, p, wp, energy, dist, src);
|
|
7602
|
+
else if (dist <= breachReach) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
|
|
7041
7603
|
return reach;
|
|
7042
7604
|
};
|
|
7043
7605
|
const blastProbe = { partsTested: 0, partsNear: 0 };
|
|
@@ -7283,12 +7845,17 @@ var GameFloorplanStructure = (() => {
|
|
|
7283
7845
|
}
|
|
7284
7846
|
return h | 0;
|
|
7285
7847
|
};
|
|
7286
|
-
const
|
|
7848
|
+
const splitStream = (objectId, partId, hit) => {
|
|
7849
|
+
const codes = Array.from(`${objectId}|${partId}`, (char) => char.charCodeAt(0) / 1e3);
|
|
7850
|
+
const stream = createRng(seedOf(rng.state() / 1e3, ...codes, hit[0], hit[1], hit[2]));
|
|
7851
|
+
return () => stream.next();
|
|
7852
|
+
};
|
|
7853
|
+
const throwFragment = (frag, objectId, from, g, centre2, index, src, shields, results, reports, mult) => {
|
|
7287
7854
|
const v = frag.velocity;
|
|
7288
7855
|
const speed = Math.hypot(v[0], v[1], v[2]);
|
|
7289
7856
|
const energy = g.energyFactor * 0.5 * frag.mass * speed * speed;
|
|
7290
7857
|
if (speed < 0.5 || energy < 1) return;
|
|
7291
|
-
const r = createRng(seedOf(options.seed ?? 1,
|
|
7858
|
+
const r = createRng(seedOf(options.seed ?? 1, centre2[0], centre2[1], centre2[2], index, frag.center[0], frag.center[1], frag.center[2]));
|
|
7292
7859
|
let dir = vec.scale(v, 1 / speed);
|
|
7293
7860
|
const behind = from ? shields.get(from) : void 0;
|
|
7294
7861
|
if (behind) {
|
|
@@ -9162,6 +9729,8 @@ var GameFloorplanStructure = (() => {
|
|
|
9162
9729
|
// src/wall-detail/panels.ts
|
|
9163
9730
|
var LINTEL_DEPTH2 = 0.2;
|
|
9164
9731
|
var LINTEL_BEARING2 = 0.2;
|
|
9732
|
+
var SILL_BEAM_DEPTH = 0.1;
|
|
9733
|
+
var LINTEL_SPAN_RATIO = 10;
|
|
9165
9734
|
var MIN_PIECE = 0.05;
|
|
9166
9735
|
var frameOf = (wall) => {
|
|
9167
9736
|
const alongU = wall.aabb[2] - wall.aabb[0] >= wall.aabb[3] - wall.aabb[1];
|
|
@@ -9175,6 +9744,7 @@ var GameFloorplanStructure = (() => {
|
|
|
9175
9744
|
const drafts = [];
|
|
9176
9745
|
const piers = [];
|
|
9177
9746
|
const lintels = [];
|
|
9747
|
+
const sillBeams = [];
|
|
9178
9748
|
const spandrels = [];
|
|
9179
9749
|
const height = wall.y1 - wall.y0;
|
|
9180
9750
|
let cursor = frame2.from;
|
|
@@ -9185,23 +9755,26 @@ var GameFloorplanStructure = (() => {
|
|
|
9185
9755
|
};
|
|
9186
9756
|
own.forEach((opening2, k) => {
|
|
9187
9757
|
addPier(Math.max(cursor, opening2.from));
|
|
9188
|
-
const
|
|
9758
|
+
const span = opening2.to - opening2.from;
|
|
9759
|
+
const lintelTop = Math.min(wall.y1, opening2.y1 + Math.max(LINTEL_DEPTH2, style.lintelDepth?.(span) ?? 0, (span + LINTEL_BEARING2) / LINTEL_SPAN_RATIO));
|
|
9189
9760
|
const lintelFrom = Math.max(frame2.from, opening2.from - LINTEL_BEARING2);
|
|
9190
9761
|
const lintelTo = Math.min(frame2.to, opening2.to + LINTEL_BEARING2);
|
|
9191
9762
|
const lintelHeight = lintelTop - opening2.y1;
|
|
9192
|
-
|
|
9763
|
+
const sillTop = style.sillBeam && opening2.y0 - wall.y0 > SILL_BEAM_DEPTH + MIN_PIECE ? opening2.y0 - SILL_BEAM_DEPTH : opening2.y0;
|
|
9764
|
+
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) });
|
|
9765
|
+
if (sillTop < opening2.y0) sillBeams.push({ id: `${wall.id}_sillbeam_${k}`, role: "beam", material: style.lintelMaterial, from: lintelFrom, to: lintelTo, y0: sillTop, y1: opening2.y0, volume: (lintelTo - lintelFrom) * SILL_BEAM_DEPTH });
|
|
9193
9766
|
lintels.push({ id: `${wall.id}_lintel_${k}`, role: "beam", material: style.lintelMaterial, from: lintelFrom, to: lintelTo, y0: opening2.y1, y1: lintelTop, volume: (lintelTo - lintelFrom) * lintelHeight });
|
|
9194
9767
|
spandrels.push(wall.y1 - lintelTop > MIN_PIECE ? { id: `${wall.id}_spandrel_${k}`, role: roleOf2, material: wall.material, from: opening2.from, to: opening2.to, y0: lintelTop, y1: wall.y1, volume: (opening2.to - opening2.from) * (wall.y1 - lintelTop) } : void 0);
|
|
9195
9768
|
cursor = opening2.to;
|
|
9196
9769
|
});
|
|
9197
9770
|
addPier(frame2.to);
|
|
9198
9771
|
for (const pier of piers) {
|
|
9199
|
-
for (const lintel of lintels) {
|
|
9772
|
+
for (const lintel of [...lintels, ...sillBeams]) {
|
|
9200
9773
|
const overlap = Math.max(0, Math.min(pier.to, lintel.to) - Math.max(pier.from, lintel.from));
|
|
9201
9774
|
pier.volume -= overlap * (lintel.y1 - lintel.y0);
|
|
9202
9775
|
}
|
|
9203
9776
|
}
|
|
9204
|
-
const all = [...piers, ...drafts, ...lintels, ...spandrels.filter((d) => d !== void 0)];
|
|
9777
|
+
const all = [...piers, ...drafts, ...sillBeams, ...lintels, ...spandrels.filter((d) => d !== void 0)];
|
|
9205
9778
|
const total = all.reduce((sum, d) => sum + d.volume, 0);
|
|
9206
9779
|
const pieces = all.map((d) => ({
|
|
9207
9780
|
id: d.id,
|
|
@@ -9216,6 +9789,11 @@ var GameFloorplanStructure = (() => {
|
|
|
9216
9789
|
...d.role === "beam" ? style.lintelAssembly ? { assembly: style.lintelAssembly } : {} : wall.assembly ? { assembly: wall.assembly } : {}
|
|
9217
9790
|
}));
|
|
9218
9791
|
const internal = [];
|
|
9792
|
+
for (const beam of sillBeams) {
|
|
9793
|
+
const sill = drafts.find((d) => d.id === beam.id.replace("_sillbeam_", "_sill_"));
|
|
9794
|
+
if (sill) internal.push({ part: beam.id, on: sill.id, joint: style.lintelJoint });
|
|
9795
|
+
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 });
|
|
9796
|
+
}
|
|
9219
9797
|
lintels.forEach((lintel, k) => {
|
|
9220
9798
|
for (const pier of piers) if (Math.min(pier.to, lintel.to) - Math.max(pier.from, lintel.from) > 0) internal.push({ part: lintel.id, on: pier.id, joint: style.lintelJoint });
|
|
9221
9799
|
const spandrel = spandrels[k];
|
|
@@ -9247,7 +9825,8 @@ var GameFloorplanStructure = (() => {
|
|
|
9247
9825
|
...system.timber ? {} : { lintelAssembly: "reinforced_concrete" },
|
|
9248
9826
|
lintelJoint: system.joints.beam,
|
|
9249
9827
|
wallJoint: system.joints.wall,
|
|
9250
|
-
...lintelDepth && !system.timber ? { lintelDepth } : {}
|
|
9828
|
+
...lintelDepth && !system.timber ? { lintelDepth } : {},
|
|
9829
|
+
...construction.system === "masonry" ? { sillBeam: true } : {}
|
|
9251
9830
|
});
|
|
9252
9831
|
if (split.pieces.length > 1) splits.set(part.id, split);
|
|
9253
9832
|
}
|
|
@@ -9255,7 +9834,18 @@ var GameFloorplanStructure = (() => {
|
|
|
9255
9834
|
}
|
|
9256
9835
|
function applyPanels(input, structure, detail, lintelDepth) {
|
|
9257
9836
|
if (detail === "coarse") return input;
|
|
9258
|
-
return applySplits(input, panelSplits(input, structure, lintelDepth));
|
|
9837
|
+
return onJambPillars(applySplits(input, panelSplits(input, structure, lintelDepth)));
|
|
9838
|
+
}
|
|
9839
|
+
var OPENING_BEAM = /_(lintel|sillbeam)_\d+$/;
|
|
9840
|
+
var overlaps2 = (a, b) => a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1];
|
|
9841
|
+
function onJambPillars(split) {
|
|
9842
|
+
const pillars = split.parts.filter((part) => part.role === "column" && part.material === "concrete");
|
|
9843
|
+
const extra = [];
|
|
9844
|
+
for (const beam of split.parts) {
|
|
9845
|
+
if (beam.role !== "beam" || !OPENING_BEAM.test(beam.id)) continue;
|
|
9846
|
+
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" });
|
|
9847
|
+
}
|
|
9848
|
+
return extra.length ? { parts: split.parts, supports: [...split.supports, ...extra] } : split;
|
|
9259
9849
|
}
|
|
9260
9850
|
function applyUnits(input, structure) {
|
|
9261
9851
|
const splits = /* @__PURE__ */ new Map();
|
|
@@ -9319,21 +9909,35 @@ var GameFloorplanStructure = (() => {
|
|
|
9319
9909
|
};
|
|
9320
9910
|
var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
|
|
9321
9911
|
var CAGED_ROLES = /* @__PURE__ */ new Set(["beam", "column"]);
|
|
9912
|
+
var CAGE_COVER = 0.03;
|
|
9913
|
+
var CAGE_BUCKLING_SHARE = 0.5;
|
|
9914
|
+
var DEFAULT_RESIDUAL = 0.15;
|
|
9915
|
+
function cageResidualCapacity(size, reinforcement, capacity) {
|
|
9916
|
+
const [a = 0, b = 0] = [...size].sort((x, y) => x - y);
|
|
9917
|
+
const perSide = (w) => Math.max(2, Math.floor(Math.max(0, w - 2 * CAGE_COVER) / reinforcement.spacing) + 1);
|
|
9918
|
+
const bars = 2 * perSide(a) + 2 * perSide(b) - 4;
|
|
9919
|
+
const steel = MATERIALS.find((m) => m.id === reinforcement.material)?.compressive ?? 0;
|
|
9920
|
+
const carries2 = bars * Math.PI * (reinforcement.diameter / 2) ** 2 * steel * CAGE_BUCKLING_SHARE;
|
|
9921
|
+
if (!capacity || capacity <= 0 || carries2 <= 0) return DEFAULT_RESIDUAL;
|
|
9922
|
+
return Math.min(1, Math.max(DEFAULT_RESIDUAL, carries2 / capacity));
|
|
9923
|
+
}
|
|
9322
9924
|
var caged = (spec, part, rebar) => {
|
|
9323
9925
|
if (!rebar || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
|
|
9324
9926
|
const size = spec.shape.kind === "box" ? spec.shape.size : null;
|
|
9325
9927
|
if (!size) return spec;
|
|
9326
9928
|
const thin = Math.min(...size);
|
|
9929
|
+
const reinforcement = { material: rebar, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter };
|
|
9327
9930
|
return {
|
|
9328
9931
|
...spec,
|
|
9329
|
-
reinforcement: {
|
|
9932
|
+
reinforcement: part.role === "column" ? { ...reinforcement, residualCapacity: cageResidualCapacity(size, reinforcement, spec.structure?.capacity) } : reinforcement,
|
|
9330
9933
|
units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
|
|
9331
9934
|
};
|
|
9332
9935
|
};
|
|
9333
9936
|
var partSpecOf = (part, capacity, detailed, rebar) => {
|
|
9334
9937
|
if (detailed && part.assembly) return assemblySpec(part, capacity);
|
|
9335
9938
|
const spec = boxSpec(part, capacity);
|
|
9336
|
-
|
|
9939
|
+
const out = detailed ? caged(spec, part, rebar) : spec;
|
|
9940
|
+
return part.beamKind === "ground_beam" ? { ...out, anchored: true, structure: { role: "foundation" } } : out;
|
|
9337
9941
|
};
|
|
9338
9942
|
function toDestructibleSpec(structure, options = {}) {
|
|
9339
9943
|
const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
|
|
@@ -9349,7 +9953,9 @@ var GameFloorplanStructure = (() => {
|
|
|
9349
9953
|
const walled = applyPanels(slabbed, structure, detail, slabOptions && structure.construction.frame.rebar ? (span) => beamDepthFor(span, slabOptions) : void 0);
|
|
9350
9954
|
const panels = slabOptions && detail !== "coarse" ? deepenBondBeams(walled, structure, slabOptions) : walled;
|
|
9351
9955
|
const sizing = { safety, redundancy, safetyOthers };
|
|
9352
|
-
const
|
|
9956
|
+
const foundation = new Set(panels.parts.filter((part) => part.beamKind === "ground_beam").map((part) => part.id));
|
|
9957
|
+
const onFoundation = foundation.size ? panels.supports.filter((s) => !foundation.has(s.part)).map((s) => foundation.has(s.on) ? { ...s, on: GROUND } : s) : panels.supports;
|
|
9958
|
+
const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts.filter((part) => !foundation.has(part.id)), onFoundation, sizing) };
|
|
9353
9959
|
const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
|
|
9354
9960
|
const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
|
|
9355
9961
|
const margin = options.terrainMargin ?? TERRAIN_MARGIN;
|
|
@@ -9415,6 +10021,359 @@ var GameFloorplanStructure = (() => {
|
|
|
9415
10021
|
return out;
|
|
9416
10022
|
}
|
|
9417
10023
|
|
|
10024
|
+
// src/validate/codes.ts
|
|
10025
|
+
var STRUCTURAL_VIOLATION = {
|
|
10026
|
+
/** A pillar with no load path to the ground, or that starts above what it rests on. */
|
|
10027
|
+
PILLAR_NOT_GROUNDED: "PILLAR_NOT_GROUNDED",
|
|
10028
|
+
/** A pillar of a masonry wall that stops short of the bond beam, or that the bond beam does not rest on. */
|
|
10029
|
+
PILLAR_DOES_NOT_REACH_BOND_BEAM: "PILLAR_DOES_NOT_REACH_BOND_BEAM",
|
|
10030
|
+
/** A masonry wall that runs more than `MAX_PILLAR_SPACING` between two of its pillars. */
|
|
10031
|
+
WALL_SPAN_WITHOUT_PILLAR: "WALL_SPAN_WITHOUT_PILLAR",
|
|
10032
|
+
/** A free end of a masonry wall (a door jamb, the end of a garden wall) with no pillar. */
|
|
10033
|
+
WALL_END_WITHOUT_PILLAR: "WALL_END_WITHOUT_PILLAR",
|
|
10034
|
+
/** Two masonry walls meet at a corner with no pillar there. */
|
|
10035
|
+
CORNER_WITHOUT_PILLAR: "CORNER_WITHOUT_PILLAR",
|
|
10036
|
+
/** Another wall meets a masonry wall inside its length (a T) with no pillar there. */
|
|
10037
|
+
WALL_JUNCTION_WITHOUT_PILLAR: "WALL_JUNCTION_WITHOUT_PILLAR",
|
|
10038
|
+
/** A window or door in a masonry wall with no lintel (verga) over it. */
|
|
10039
|
+
OPENING_WITHOUT_LINTEL: "OPENING_WITHOUT_LINTEL",
|
|
10040
|
+
/** A window in a masonry wall with no sill beam (contraverga) under it. */
|
|
10041
|
+
OPENING_WITHOUT_SILL: "OPENING_WITHOUT_SILL",
|
|
10042
|
+
/** An exterior masonry wall with no bond beam (cinta de amarração) on top. */
|
|
10043
|
+
MISSING_BOND_BEAM: "MISSING_BOND_BEAM",
|
|
10044
|
+
/** A ground-floor masonry wall standing on the soil with no ground beam (viga baldrame) or foundation under it. */
|
|
10045
|
+
MISSING_GROUND_BEAM: "MISSING_GROUND_BEAM",
|
|
10046
|
+
/** A part with no load path down to the ground. */
|
|
10047
|
+
FLOATING_PART: "FLOATING_PART",
|
|
10048
|
+
/** A concrete beam longer than a lintel that rests on masonry alone (no pillar, beam or slab carries it). */
|
|
10049
|
+
BEAM_ON_WEAK_MASONRY: "BEAM_ON_WEAK_MASONRY",
|
|
10050
|
+
/** A concrete pillar or beam below the NBR 6118 minimum section, or a beam between point supports shallower than a tenth of its span. */
|
|
10051
|
+
CONCRETE_BELOW_MIN_SECTION: "CONCRETE_BELOW_MIN_SECTION",
|
|
10052
|
+
/** A concrete pillar or beam with no steel (the construction has no rebar). */
|
|
10053
|
+
UNREINFORCED_CONCRETE: "UNREINFORCED_CONCRETE",
|
|
10054
|
+
/** A brick or block wall laid in stack bond: the vertical joints line up and the wall splits along them. */
|
|
10055
|
+
UNBONDED_MASONRY: "UNBONDED_MASONRY",
|
|
10056
|
+
/** A plastered brick wall without plaster on both faces. */
|
|
10057
|
+
MISSING_PLASTER: "MISSING_PLASTER"
|
|
10058
|
+
};
|
|
10059
|
+
var MAX_PILLAR_SPACING = 4;
|
|
10060
|
+
var MIN_PILLAR_SIDE = 0.19;
|
|
10061
|
+
var MIN_PILLAR_AREA = 0.036;
|
|
10062
|
+
var MIN_BEAM_WIDTH = 0.12;
|
|
10063
|
+
var BEAM_DEPTH_RATIO = 10;
|
|
10064
|
+
var MAX_LINTEL_ON_MASONRY = 3;
|
|
10065
|
+
|
|
10066
|
+
// src/validate/load-path.ts
|
|
10067
|
+
var CONTACT = 0.05;
|
|
10068
|
+
var EPSILON = 5e-3;
|
|
10069
|
+
function groundedParts(parts, supports) {
|
|
10070
|
+
const ids = new Set(parts.map((part) => part.id));
|
|
10071
|
+
const grounded = /* @__PURE__ */ new Set([GROUND]);
|
|
10072
|
+
const bearing = supports.filter((s) => s.bears !== false && ids.has(s.part));
|
|
10073
|
+
let grew = true;
|
|
10074
|
+
while (grew) {
|
|
10075
|
+
grew = false;
|
|
10076
|
+
for (const support of bearing) {
|
|
10077
|
+
if (grounded.has(support.part) || !grounded.has(support.on)) continue;
|
|
10078
|
+
grounded.add(support.part);
|
|
10079
|
+
grew = true;
|
|
10080
|
+
}
|
|
10081
|
+
}
|
|
10082
|
+
return grounded;
|
|
10083
|
+
}
|
|
10084
|
+
function loadPathViolations(structure) {
|
|
10085
|
+
const { parts, supports } = structure;
|
|
10086
|
+
const grounded = groundedParts(parts, supports);
|
|
10087
|
+
const byId = new Map(parts.map((part) => [part.id, part]));
|
|
10088
|
+
const out = [];
|
|
10089
|
+
for (const part of parts) {
|
|
10090
|
+
if (part.role === "none") continue;
|
|
10091
|
+
const pillar = part.role === "column";
|
|
10092
|
+
if (!grounded.has(part.id)) {
|
|
10093
|
+
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` });
|
|
10094
|
+
continue;
|
|
10095
|
+
}
|
|
10096
|
+
if (!pillar) continue;
|
|
10097
|
+
const bases = supports.filter((s) => s.part === part.id && s.bears !== false);
|
|
10098
|
+
const standsOnBase = bases.some((s) => {
|
|
10099
|
+
if (s.on === GROUND) return part.y0 <= CONTACT;
|
|
10100
|
+
const base = byId.get(s.on);
|
|
10101
|
+
return base !== void 0 && part.y0 >= base.y0 - EPSILON && part.y0 <= base.y1 + CONTACT;
|
|
10102
|
+
});
|
|
10103
|
+
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` });
|
|
10104
|
+
}
|
|
10105
|
+
return out;
|
|
10106
|
+
}
|
|
10107
|
+
var alongOf2 = (part) => {
|
|
10108
|
+
const alongU = part.aabb[2] - part.aabb[0] >= part.aabb[3] - part.aabb[1];
|
|
10109
|
+
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] };
|
|
10110
|
+
};
|
|
10111
|
+
function bearingsOf(beam, supports, byId) {
|
|
10112
|
+
const frame2 = alongOf2(beam);
|
|
10113
|
+
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) => {
|
|
10114
|
+
const [low, high] = frame2.alongU ? [on.aabb[0], on.aabb[2]] : [on.aabb[1], on.aabb[3]];
|
|
10115
|
+
return { on, from: Math.max(frame2.from, low), to: Math.min(frame2.to, high) };
|
|
10116
|
+
}).filter((bearing) => bearing.to - bearing.from > -EPSILON);
|
|
10117
|
+
}
|
|
10118
|
+
var isWallLike = (part) => part.role === "wall" || part.role === "partition";
|
|
10119
|
+
var PEDESTAL_RATIO = 3;
|
|
10120
|
+
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]);
|
|
10121
|
+
function concreteViolations(structure, isMasonry) {
|
|
10122
|
+
const { parts, supports } = structure;
|
|
10123
|
+
const byId = new Map(parts.map((part) => [part.id, part]));
|
|
10124
|
+
const out = [];
|
|
10125
|
+
for (const part of parts) {
|
|
10126
|
+
if (part.material !== "concrete" || part.role !== "column" && part.role !== "beam") continue;
|
|
10127
|
+
if (!structure.construction.frame.rebar && !isPedestal(part)) out.push({ code: STRUCTURAL_VIOLATION.UNREINFORCED_CONCRETE, part: part.id, message: `${part.id} is concrete without steel` });
|
|
10128
|
+
if (part.role === "column") {
|
|
10129
|
+
const sides = [part.aabb[2] - part.aabb[0], part.aabb[3] - part.aabb[1]];
|
|
10130
|
+
if (Math.min(...sides) < MIN_PILLAR_SIDE - EPSILON || sides[0] * sides[1] < MIN_PILLAR_AREA - EPSILON)
|
|
10131
|
+
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` });
|
|
10132
|
+
continue;
|
|
10133
|
+
}
|
|
10134
|
+
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);
|
|
10135
|
+
out.push(...beamViolations(part, bearingsOf(part, supports, byId), { isMasonry, castOn }));
|
|
10136
|
+
}
|
|
10137
|
+
return out;
|
|
10138
|
+
}
|
|
10139
|
+
function effectiveSpan(bearings, depth) {
|
|
10140
|
+
const sorted = [...bearings].sort((a, b) => a.from - b.from);
|
|
10141
|
+
let most = 0;
|
|
10142
|
+
for (let k = 1; k < sorted.length; k++) {
|
|
10143
|
+
const previous = sorted.slice(0, k).reduce((a, b) => b.to > a.to ? b : a);
|
|
10144
|
+
const next = sorted[k];
|
|
10145
|
+
const clear = next.from - previous.to;
|
|
10146
|
+
if (clear <= 0) continue;
|
|
10147
|
+
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));
|
|
10148
|
+
}
|
|
10149
|
+
return most;
|
|
10150
|
+
}
|
|
10151
|
+
function beamViolations(beam, bearings, context) {
|
|
10152
|
+
const out = [];
|
|
10153
|
+
const frame2 = alongOf2(beam);
|
|
10154
|
+
const length = frame2.to - frame2.from;
|
|
10155
|
+
const depth = beam.y1 - beam.y0 + context.castOn;
|
|
10156
|
+
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` });
|
|
10157
|
+
if (!bearings.length) return out;
|
|
10158
|
+
const continuous = bearings.some((b) => isWallLike(b.on) && b.to - b.from >= length / 2);
|
|
10159
|
+
const span = continuous ? 0 : effectiveSpan(bearings, depth);
|
|
10160
|
+
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` });
|
|
10161
|
+
if (length > MAX_LINTEL_ON_MASONRY && bearings.every((b) => context.isMasonry(b.on)))
|
|
10162
|
+
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` });
|
|
10163
|
+
return out;
|
|
10164
|
+
}
|
|
10165
|
+
|
|
10166
|
+
// src/validate/masonry.ts
|
|
10167
|
+
var TOLERANCE3 = 0.05;
|
|
10168
|
+
var AT_POINT = 0.45;
|
|
10169
|
+
var EXPOSED_FINISHES = ["exposed_brick", "exposed_block"];
|
|
10170
|
+
var lineOf2 = (part) => {
|
|
10171
|
+
const alongU = part.aabb[2] - part.aabb[0] >= part.aabb[3] - part.aabb[1];
|
|
10172
|
+
const half = (alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0]) / 2;
|
|
10173
|
+
const [from, to] = alongU ? [part.aabb[0], part.aabb[2]] : [part.aabb[1], part.aabb[3]];
|
|
10174
|
+
return { part, alongU, line: alongU ? part.aabb[1] + half : part.aabb[0] + half, from: from + half, to: to - half, half };
|
|
10175
|
+
};
|
|
10176
|
+
function isMasonryWall(part) {
|
|
10177
|
+
if (!part.id.startsWith("wall_") || !part.assembly || part.role !== "wall" && part.role !== "partition") return false;
|
|
10178
|
+
const units = getAssembly(part.assembly)?.units;
|
|
10179
|
+
return units !== void 0 && units.mortar !== "concrete";
|
|
10180
|
+
}
|
|
10181
|
+
var pointOf2 = (wall, along) => wall.alongU ? [along, wall.line] : [wall.line, along];
|
|
10182
|
+
var alongOfPoint = (wall, point) => wall.alongU ? point[0] : point[1];
|
|
10183
|
+
var acrossOfPoint = (wall, point) => wall.alongU ? point[1] : point[0];
|
|
10184
|
+
var centre = (part) => [(part.aabb[0] + part.aabb[2]) / 2, (part.aabb[1] + part.aabb[3]) / 2];
|
|
10185
|
+
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;
|
|
10186
|
+
function endKind(wall, along, others) {
|
|
10187
|
+
const point = pointOf2(wall, along);
|
|
10188
|
+
const meeting = others.filter((other) => onLine(other, point));
|
|
10189
|
+
const across = meeting.filter((other) => other.alongU !== wall.alongU);
|
|
10190
|
+
const continued = meeting.length > across.length;
|
|
10191
|
+
if (!across.length) return continued ? "continued" : "free";
|
|
10192
|
+
const atEnd = (other) => Math.min(Math.abs(alongOfPoint(other, point) - other.from), Math.abs(alongOfPoint(other, point) - other.to)) <= other.half + wall.half + TOLERANCE3;
|
|
10193
|
+
return !continued && across.every(atEnd) ? "corner" : "junction";
|
|
10194
|
+
}
|
|
10195
|
+
var masonryBetween = (from, to, windows) => windows.reduce((left, [a, b]) => left - Math.max(0, Math.min(to, b) - Math.max(from, a)), to - from);
|
|
10196
|
+
function pointViolations(context, others) {
|
|
10197
|
+
const { wall, stations } = context;
|
|
10198
|
+
const out = [];
|
|
10199
|
+
const covered = (along) => stations.some((at2) => Math.abs(at2 - along) <= AT_POINT);
|
|
10200
|
+
for (const along of [wall.from, wall.to]) {
|
|
10201
|
+
const kind = endKind(wall, along, others);
|
|
10202
|
+
if (kind === "continued" || covered(along)) continue;
|
|
10203
|
+
const code = kind === "corner" ? STRUCTURAL_VIOLATION.CORNER_WITHOUT_PILLAR : kind === "junction" ? STRUCTURAL_VIOLATION.WALL_JUNCTION_WITHOUT_PILLAR : STRUCTURAL_VIOLATION.WALL_END_WITHOUT_PILLAR;
|
|
10204
|
+
out.push({ code, part: wall.part.id, message: `${wall.part.id} has no pillar at its ${kind} end (${along.toFixed(2)} m along)` });
|
|
10205
|
+
}
|
|
10206
|
+
for (const other of others) {
|
|
10207
|
+
if (other.alongU === wall.alongU) continue;
|
|
10208
|
+
for (const end of [other.from, other.to]) {
|
|
10209
|
+
const point = pointOf2(other, end);
|
|
10210
|
+
const at2 = alongOfPoint(wall, point);
|
|
10211
|
+
if (!onLine(wall, point) || at2 <= wall.from + AT_POINT || at2 >= wall.to - AT_POINT || covered(at2)) continue;
|
|
10212
|
+
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` });
|
|
10213
|
+
}
|
|
10214
|
+
}
|
|
10215
|
+
const line = [...stations].sort((a, b) => a - b);
|
|
10216
|
+
for (let k = 1; k < line.length; k++) {
|
|
10217
|
+
const gap = masonryBetween(line[k - 1], line[k], context.windows);
|
|
10218
|
+
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)` });
|
|
10219
|
+
}
|
|
10220
|
+
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` });
|
|
10221
|
+
return out;
|
|
10222
|
+
}
|
|
10223
|
+
function bondBeamViolations(context, structure) {
|
|
10224
|
+
const { wall, pillars } = context;
|
|
10225
|
+
const resting = new Set(structure.supports.filter((s) => s.on === wall.part.id).map((s) => s.part));
|
|
10226
|
+
const beams = structure.parts.filter((p) => p.beamKind === "bond_beam" && resting.has(p.id));
|
|
10227
|
+
if (!beams.length) return [{ code: STRUCTURAL_VIOLATION.MISSING_BOND_BEAM, part: wall.part.id, message: `${wall.part.id} has no bond beam on top` }];
|
|
10228
|
+
const out = [];
|
|
10229
|
+
for (const pillar of pillars) {
|
|
10230
|
+
const [u, v] = centre(pillar);
|
|
10231
|
+
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);
|
|
10232
|
+
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));
|
|
10233
|
+
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}` });
|
|
10234
|
+
}
|
|
10235
|
+
return out;
|
|
10236
|
+
}
|
|
10237
|
+
function assemblyViolations(part, finish) {
|
|
10238
|
+
const def = getAssembly(part.assembly ?? "");
|
|
10239
|
+
if (!def) return [];
|
|
10240
|
+
const out = [];
|
|
10241
|
+
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` });
|
|
10242
|
+
const plastered = (def.layers ?? []).some((layer) => layer.material === "plaster" && layer.faces === "both");
|
|
10243
|
+
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` });
|
|
10244
|
+
return out;
|
|
10245
|
+
}
|
|
10246
|
+
function masonryViolations(structure) {
|
|
10247
|
+
const walls = structure.parts.filter(isMasonryWall);
|
|
10248
|
+
const columns = structure.parts.filter((p) => p.role === "column" && p.material === "concrete");
|
|
10249
|
+
const out = [];
|
|
10250
|
+
for (const part of walls) {
|
|
10251
|
+
out.push(...assemblyViolations(part, structure.construction.walls.finish));
|
|
10252
|
+
if (part.floor === 0 && structure.supports.some((s) => s.part === part.id && s.on === GROUND))
|
|
10253
|
+
out.push({ code: STRUCTURAL_VIOLATION.MISSING_GROUND_BEAM, part: part.id, message: `${part.id} stands on the soil with no ground beam under it` });
|
|
10254
|
+
if (!part.exterior) continue;
|
|
10255
|
+
const wall = lineOf2(part);
|
|
10256
|
+
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);
|
|
10257
|
+
const windows = structure.openings.filter((o) => o.wall === part.id).map((o) => [o.from, o.to]);
|
|
10258
|
+
const context = { wall, pillars, stations: pillars.map((p) => alongOfPoint(wall, centre(p))), windows };
|
|
10259
|
+
const others = walls.filter((w) => w !== part && w.floor === part.floor).map(lineOf2);
|
|
10260
|
+
out.push(...pointViolations(context, others), ...bondBeamViolations(context, structure));
|
|
10261
|
+
}
|
|
10262
|
+
return out;
|
|
10263
|
+
}
|
|
10264
|
+
|
|
10265
|
+
// src/validate/openings.ts
|
|
10266
|
+
var TOLERANCE4 = 0.05;
|
|
10267
|
+
var LOW_WINDOW = SILL_BEAM_DEPTH + 0.05;
|
|
10268
|
+
function beamAcross(parts, wall, span, side, y) {
|
|
10269
|
+
return parts.some((part) => {
|
|
10270
|
+
const off = part[side] - y;
|
|
10271
|
+
if (part.role !== "beam" || off < -TOLERANCE4 || off > (side === "y0" ? LINTEL_DEPTH2 : TOLERANCE4)) return false;
|
|
10272
|
+
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]];
|
|
10273
|
+
return from <= span.from + TOLERANCE4 && to >= span.to - TOLERANCE4 && low <= wall.line + TOLERANCE4 && high >= wall.line - TOLERANCE4;
|
|
10274
|
+
});
|
|
10275
|
+
}
|
|
10276
|
+
function windowViolations(opening2, wall, built) {
|
|
10277
|
+
const line = lineOf2(wall);
|
|
10278
|
+
const out = [];
|
|
10279
|
+
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` });
|
|
10280
|
+
if (opening2.y0 - wall.y0 > LOW_WINDOW && !beamAcross(built, line, opening2, "y1", opening2.y0))
|
|
10281
|
+
out.push({ code: STRUCTURAL_VIOLATION.OPENING_WITHOUT_SILL, part: opening2.id, message: `window ${opening2.id} in ${wall.id} has no sill beam under it` });
|
|
10282
|
+
return out;
|
|
10283
|
+
}
|
|
10284
|
+
function wallsBeside(door, walls) {
|
|
10285
|
+
const along = door.axis === "u" ? door.u : door.v;
|
|
10286
|
+
const across = door.axis === "u" ? door.v : door.u;
|
|
10287
|
+
const from = along - door.width / 2;
|
|
10288
|
+
const to = along + door.width / 2;
|
|
10289
|
+
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));
|
|
10290
|
+
}
|
|
10291
|
+
function openingViolations(structure, built, doors) {
|
|
10292
|
+
const walls = structure.parts.filter(isMasonryWall);
|
|
10293
|
+
const hosts = new Map(walls.map((wall) => [wall.id, wall]));
|
|
10294
|
+
const out = [];
|
|
10295
|
+
for (const opening2 of structure.openings) {
|
|
10296
|
+
const wall = hosts.get(opening2.wall);
|
|
10297
|
+
if (wall) out.push(...windowViolations(opening2, wall, built));
|
|
10298
|
+
}
|
|
10299
|
+
for (const door of doors) {
|
|
10300
|
+
const beside = wallsBeside(door, walls);
|
|
10301
|
+
if (!beside.length) continue;
|
|
10302
|
+
const along = door.axis === "u" ? door.u : door.v;
|
|
10303
|
+
const span = { from: along - door.width / 2, to: along + door.width / 2 };
|
|
10304
|
+
const lintel = beamAcross(built, beside[0], span, "y0", (structure.levels[door.floor] ?? 0) + DOOR_HEIGHT);
|
|
10305
|
+
if (!lintel) out.push({ code: STRUCTURAL_VIOLATION.OPENING_WITHOUT_LINTEL, part: door.id, message: `door ${door.id} has no lintel over it` });
|
|
10306
|
+
}
|
|
10307
|
+
return out;
|
|
10308
|
+
}
|
|
10309
|
+
|
|
10310
|
+
// src/validate/spec.ts
|
|
10311
|
+
var EPSILON2 = 5e-3;
|
|
10312
|
+
var carries = (part) => part?.structure !== void 0 && part.structure.role !== "none";
|
|
10313
|
+
function flows(joint, byId) {
|
|
10314
|
+
const bears = joint.bears ?? (carries(byId.get(joint.a)) && carries(byId.get(joint.b)));
|
|
10315
|
+
if (bears === false) return [];
|
|
10316
|
+
if (bears === "a_on_b") return [[joint.a, joint.b]];
|
|
10317
|
+
if (bears === "b_on_a") return [[joint.b, joint.a]];
|
|
10318
|
+
return [[joint.a, joint.b], [joint.b, joint.a]];
|
|
10319
|
+
}
|
|
10320
|
+
function groundedOf(spec, byId) {
|
|
10321
|
+
const grounded = new Set(spec.parts.filter((p) => p.anchored || p.structure?.role === "foundation").map((p) => p.id));
|
|
10322
|
+
const edges = (spec.joints ?? []).flatMap((joint) => flows(joint, byId));
|
|
10323
|
+
let grew = true;
|
|
10324
|
+
while (grew) {
|
|
10325
|
+
grew = false;
|
|
10326
|
+
for (const [part, on] of edges) {
|
|
10327
|
+
if (grounded.has(part) || !grounded.has(on)) continue;
|
|
10328
|
+
grounded.add(part);
|
|
10329
|
+
grew = true;
|
|
10330
|
+
}
|
|
10331
|
+
}
|
|
10332
|
+
return grounded;
|
|
10333
|
+
}
|
|
10334
|
+
function sectionViolation(part) {
|
|
10335
|
+
if (part.material !== "concrete" || part.shape.kind !== "box") return null;
|
|
10336
|
+
const role = part.structure?.role;
|
|
10337
|
+
const [x, y, z] = part.shape.size;
|
|
10338
|
+
if (role === "column" && (Math.min(x, z) < MIN_PILLAR_SIDE - EPSILON2 || x * z < MIN_PILLAR_AREA - EPSILON2))
|
|
10339
|
+
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` };
|
|
10340
|
+
if (role === "beam" && Math.min(x, z) < MIN_BEAM_WIDTH - EPSILON2 && y >= Math.min(x, z))
|
|
10341
|
+
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` };
|
|
10342
|
+
return null;
|
|
10343
|
+
}
|
|
10344
|
+
function specViolations(spec) {
|
|
10345
|
+
const byId = new Map(spec.parts.map((part) => [part.id, part]));
|
|
10346
|
+
const grounded = groundedOf(spec, byId);
|
|
10347
|
+
const out = [];
|
|
10348
|
+
for (const part of spec.parts) {
|
|
10349
|
+
if (!carries(part)) continue;
|
|
10350
|
+
if (!grounded.has(part.id)) {
|
|
10351
|
+
const pillar = part.structure.role === "column";
|
|
10352
|
+
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` });
|
|
10353
|
+
}
|
|
10354
|
+
const section = sectionViolation(part);
|
|
10355
|
+
if (section) out.push(section);
|
|
10356
|
+
}
|
|
10357
|
+
return out;
|
|
10358
|
+
}
|
|
10359
|
+
|
|
10360
|
+
// src/validate/index.ts
|
|
10361
|
+
function validateStructure(structure, options = {}) {
|
|
10362
|
+
const detail = options.wallDetail ?? "panels";
|
|
10363
|
+
const built = applyPanels({ parts: structure.parts, supports: structure.supports }, structure, detail);
|
|
10364
|
+
const out = [...loadPathViolations(structure), ...concreteViolations({ ...built, construction: structure.construction }, isMasonryWall)];
|
|
10365
|
+
if (structure.construction.system !== "masonry") return out;
|
|
10366
|
+
return [...out, ...masonryViolations(structure), ...openingViolations(structure, built.parts, options.doors ?? [])];
|
|
10367
|
+
}
|
|
10368
|
+
function validateSpec(spec) {
|
|
10369
|
+
return specViolations(spec);
|
|
10370
|
+
}
|
|
10371
|
+
function countViolations(violations) {
|
|
10372
|
+
const out = {};
|
|
10373
|
+
for (const violation of violations) out[violation.code] = (out[violation.code] ?? 0) + 1;
|
|
10374
|
+
return out;
|
|
10375
|
+
}
|
|
10376
|
+
|
|
9418
10377
|
// src/wall-specimen.ts
|
|
9419
10378
|
var SYSTEM_OF_ASSEMBLY = { log_wall: "log", wood_frame_wall: "wood_frame", sheet_metal_shack: "improvised" };
|
|
9420
10379
|
var DOOR_WIDTH2 = 0.9;
|
|
@@ -9603,7 +10562,7 @@ var GameFloorplanStructure = (() => {
|
|
|
9603
10562
|
parts.push({ id, material: c.material, shape: box(size), position, structure: { role: "partition" }, tags: ["infill"] });
|
|
9604
10563
|
for (const o of on) rests(joints, id, o, "mortar");
|
|
9605
10564
|
};
|
|
9606
|
-
const bay = (side, k, along,
|
|
10565
|
+
const bay = (side, k, along, centre2, fixed, length, c1, c2) => {
|
|
9607
10566
|
const clear = length - g.columnSize;
|
|
9608
10567
|
const win = clear * c.window;
|
|
9609
10568
|
const pier = (clear - win) / 2;
|
|
@@ -9614,11 +10573,11 @@ var GameFloorplanStructure = (() => {
|
|
|
9614
10573
|
wall(`infill_${f}_${side}_${k}_${name}`, size(u, h), [p[0], y, p[2]], on);
|
|
9615
10574
|
};
|
|
9616
10575
|
const id = (name) => `infill_${f}_${side}_${k}_${name}`;
|
|
9617
|
-
put("sill", clear, sill, yb + sill / 2,
|
|
9618
|
-
put("pl", pier, head - sill, yb + sill + (head - sill) / 2,
|
|
9619
|
-
put("pr", pier, head - sill, yb + sill + (head - sill) / 2,
|
|
9620
|
-
put("head", clear, room - head, yb + head + (room - head) / 2,
|
|
9621
|
-
const gp = at2(
|
|
10576
|
+
put("sill", clear, sill, yb + sill / 2, centre2, [c1, c2]);
|
|
10577
|
+
put("pl", pier, head - sill, yb + sill + (head - sill) / 2, centre2 - clear / 2 + pier / 2, [c1, id("sill")]);
|
|
10578
|
+
put("pr", pier, head - sill, yb + sill + (head - sill) / 2, centre2 + clear / 2 - pier / 2, [c2, id("sill")]);
|
|
10579
|
+
put("head", clear, room - head, yb + head + (room - head) / 2, centre2, [c1, c2]);
|
|
10580
|
+
const gp = at2(centre2);
|
|
9622
10581
|
const gid = `glass_${f}_${side}_${k}`;
|
|
9623
10582
|
parts.push({
|
|
9624
10583
|
id: gid,
|