@adatechnology/game-floorplan-structure 0.0.0-stage → 1.0.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.
@@ -0,0 +1,3690 @@
1
+ "use strict";
2
+ var GameFloorplanStructure = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
22
+
23
+ // src/index.ts
24
+ var index_exports = {};
25
+ __export(index_exports, {
26
+ CONSTRUCTIONS: () => CONSTRUCTIONS,
27
+ CONSTRUCTION_SYSTEMS: () => CONSTRUCTION_SYSTEMS,
28
+ DENSITY: () => DENSITY,
29
+ GROUND: () => GROUND,
30
+ ROOF_COVERINGS: () => ROOF_COVERINGS,
31
+ buildStructure: () => buildStructure,
32
+ buildWallSpecimen: () => buildWallSpecimen,
33
+ campConstruction: () => campConstruction,
34
+ chaletConstruction: () => chaletConstruction,
35
+ constructionOfSystem: () => constructionOfSystem,
36
+ logCabinConstruction: () => logCabinConstruction,
37
+ masonryConstruction: () => masonryConstruction,
38
+ planWaterTank: () => planWaterTank,
39
+ register: () => register,
40
+ registerConstruction: () => registerConstruction,
41
+ registerConstructionSystem: () => registerConstructionSystem,
42
+ registerRoofCovering: () => registerRoofCovering,
43
+ shackConstruction: () => shackConstruction,
44
+ structureLayer: () => structureLayer,
45
+ toDestructibleSpec: () => toDestructibleSpec,
46
+ version: () => version,
47
+ woodFrameConstruction: () => woodFrameConstruction
48
+ });
49
+
50
+ // src/registry.ts
51
+ var uniform = (joint) => ({ wall: joint, slab: joint, beam: joint, column: joint, truss: joint });
52
+ var CONSTRUCTION_SYSTEMS = /* @__PURE__ */ new Map([
53
+ ["masonry", { walls: "brick_wall_plastered", joints: { wall: "mortar", slab: "rebar", beam: "mortar", column: "rebar", truss: "bolt" }, slabThickness: 0.12, plateThickness: 0.2, timber: false, wallRole: "rules", wallFill: 1 }],
54
+ ["wood_frame", { walls: "wood_frame_wall", joints: uniform("nail"), slabThickness: 0.28, plateThickness: 0.1, timber: true, wallRole: "rules", wallFill: 0.35, studs: { spacing: 0.4, section: "38x89" } }],
55
+ ["log", { walls: "log_wall", joints: { wall: "bolt", slab: "nail", beam: "bolt", column: "bolt", truss: "bolt" }, slabThickness: 0.2, plateThickness: 0.1, timber: true, wallRole: "bearing", wallFill: 1 }],
56
+ ["improvised", { walls: "sheet_metal_shack", joints: uniform("nail"), slabThickness: 0.2, plateThickness: 0.1, timber: true, wallRole: "partition", wallFill: 1 }],
57
+ ["canvas", { walls: "", joints: uniform("tie"), slabThickness: 0, plateThickness: 0, timber: false, wallRole: "partition", wallFill: 1 }]
58
+ ]);
59
+ function registerConstructionSystem(id, def) {
60
+ CONSTRUCTION_SYSTEMS.set(id, def);
61
+ }
62
+ var ROOF_COVERINGS = /* @__PURE__ */ new Map([
63
+ ["colonial", { material: "ceramic", joint: "resting" }],
64
+ ["fiber_cement", { material: "concrete", joint: "bolt" }],
65
+ ["slab", { material: "concrete", joint: "rebar" }],
66
+ ["asphalt", { material: "plastic", joint: "nail" }],
67
+ ["zinc", { material: "steel", joint: "nail" }],
68
+ ["wood", { material: "wood_soft", joint: "nail" }],
69
+ ["canvas", { material: "fabric", joint: "tie" }],
70
+ ["thatch", { material: "foliage", joint: "tie" }]
71
+ ]);
72
+ function registerRoofCovering(id, def) {
73
+ ROOF_COVERINGS.set(id, def);
74
+ }
75
+ var CONSTRUCTIONS = /* @__PURE__ */ new Map();
76
+ function registerConstruction(id, builder) {
77
+ CONSTRUCTIONS.set(id, builder);
78
+ }
79
+
80
+ // src/geometry.ts
81
+ var WALL_THICKNESS = 0.15;
82
+ var round2 = (value) => Math.round(value * 100) / 100;
83
+ function touches(a, b, tolerance) {
84
+ return a[0] < b[2] + tolerance && a[2] > b[0] - tolerance && a[1] < b[3] + tolerance && a[3] > b[1] - tolerance;
85
+ }
86
+ function distanceBetween(a, b) {
87
+ return Math.hypot(Math.max(a[0] - b[2], b[0] - a[2], 0), Math.max(a[1] - b[3], b[1] - a[3], 0));
88
+ }
89
+ function unionOf(rects) {
90
+ const out = [1e9, 1e9, -1e9, -1e9];
91
+ for (const rect of rects) {
92
+ out[0] = Math.min(out[0], rect[0]);
93
+ out[1] = Math.min(out[1], rect[1]);
94
+ out[2] = Math.max(out[2], rect[2]);
95
+ out[3] = Math.max(out[3], rect[3]);
96
+ }
97
+ return out;
98
+ }
99
+ function wallBox(wall) {
100
+ const half = (wall.thickness ?? WALL_THICKNESS) / 2;
101
+ return [Math.min(wall.u0, wall.u1) - half, Math.min(wall.v0, wall.v1) - half, Math.max(wall.u0, wall.u1) + half, Math.max(wall.v0, wall.v1) + half];
102
+ }
103
+
104
+ // src/presets.ts
105
+ var SLAB_COVERING = "slab";
106
+ var PLASTERED_FINISHES = ["plaster", "partial_plaster"];
107
+ function masonryConstruction(source) {
108
+ const unit = source.floorNumber !== void 0;
109
+ const covering = unit ? SLAB_COVERING : source.facade.roof ?? "colonial";
110
+ const finish = source.facade.finish ?? "roughcast";
111
+ const budget = source.style === "budget";
112
+ const flat = unit || covering === SLAB_COVERING;
113
+ return {
114
+ system: "masonry",
115
+ walls: { material: "brick", cladding: PLASTERED_FINISHES.includes(finish) ? "plaster" : null, finish, thickness: WALL_THICKNESS, ...finish === "exposed_block" ? { assembly: "concrete_block_wall" } : {} },
116
+ floor: { material: budget ? "concrete" : "ceramic", finish: budget ? "burnt_cement" : "ceramic_tile" },
117
+ roof: { material: ROOF_COVERINGS.get(covering)?.material ?? "ceramic", covering, shape: flat ? "slab" : "gable", pitch: flat ? 0 : covering === "colonial" ? 30 : 15, timber: "wood_soft" },
118
+ frame: { material: "concrete", pillars: "concrete", beams: "concrete", slabs: "concrete", timber: "wood_soft", rebar: "steel" },
119
+ foundation: { type: unit ? "building" : "strip_footing", material: "concrete", elevation: 0 }
120
+ };
121
+ }
122
+ var FOUNDATIONS = {
123
+ basement: { type: "basement", material: "concrete", elevation: 0.5, depth: 2.4 },
124
+ crawl_space: { type: "crawl_space", material: "concrete", elevation: 0.6 },
125
+ raft: { type: "raft", material: "concrete", elevation: 0.2 }
126
+ };
127
+ var TIMBER_FRAME = { material: "wood_soft", pillars: "wood_soft", beams: "wood_soft", slabs: "plywood", timber: "wood_soft", rebar: null };
128
+ function woodFrameConstruction(options = {}) {
129
+ const covering = options.covering ?? "asphalt";
130
+ return {
131
+ system: "wood_frame",
132
+ walls: { material: "wood_soft", interior: "plaster", cladding: options.cladding ?? "wood_soft", finish: options.finish ?? "wood_siding", thickness: WALL_THICKNESS, studs: "38x89 every 40 cm", insulation: "foam" },
133
+ floor: { material: "wood_hard", finish: "wood_boards" },
134
+ roof: { material: ROOF_COVERINGS.get(covering)?.material ?? "plastic", covering, shape: "gable", pitch: 28, timber: "wood_soft", eave: 0.5 },
135
+ frame: { ...TIMBER_FRAME },
136
+ foundation: { ...FOUNDATIONS[options.foundation ?? "raft"] }
137
+ };
138
+ }
139
+ function chaletConstruction(options = {}) {
140
+ const covering = options.covering ?? "colonial";
141
+ return {
142
+ system: "wood_frame",
143
+ walls: { material: "wood_soft", interior: "wood_soft", cladding: "wood_hard", finish: options.finish ?? "vertical_boards", thickness: WALL_THICKNESS, studs: "38x140 every 40 cm" },
144
+ floor: { material: "wood_hard", finish: "wood_boards" },
145
+ roof: { material: ROOF_COVERINGS.get(covering)?.material ?? "wood_soft", covering, shape: "gable", pitch: 52, timber: "wood_soft", eave: 0.8, axis: "v", profile: "a_frame", gables: ["front", "back"] },
146
+ frame: { ...TIMBER_FRAME },
147
+ foundation: options.piles ? { type: "piles", material: "stone", elevation: 0.9 } : { type: "raft", material: "concrete", elevation: 0.2 }
148
+ };
149
+ }
150
+ function logCabinConstruction(options = {}) {
151
+ const covering = options.covering ?? "wood";
152
+ return {
153
+ system: "log",
154
+ walls: { material: "wood_hard", interior: "wood_hard", cladding: null, finish: "log", thickness: WALL_THICKNESS, logDiameter: 0.26, joint: "notched_corners" },
155
+ floor: { material: "wood_hard", finish: "wood_boards" },
156
+ roof: { material: ROOF_COVERINGS.get(covering)?.material ?? "wood_soft", covering, shape: "gable", pitch: 32, timber: "wood_hard", eave: 0.6 },
157
+ frame: { material: "wood_hard", pillars: "wood_hard", beams: "wood_hard", slabs: "wood_hard", timber: "wood_hard", rebar: null },
158
+ foundation: { type: "piles", material: "stone", elevation: 0.5 }
159
+ };
160
+ }
161
+ var MIXED_SCRAP = ["wood_soft", "steel", "plywood", "steel", "wood_soft"];
162
+ function shackConstruction(options = {}) {
163
+ const material = options.material === "zinc" ? "steel" : "wood_soft";
164
+ const onSoil = options.floor === "soil";
165
+ return {
166
+ system: "improvised",
167
+ walls: { material, interior: options.material === "zinc" ? "plywood" : material, cladding: null, finish: "scrap", thickness: WALL_THICKNESS, ...options.material === "mixed" ? { mixed: MIXED_SCRAP } : {} },
168
+ floor: onSoil ? { material: "soil", finish: "packed_earth" } : { material: "concrete", finish: "burnt_cement" },
169
+ roof: { material: "steel", covering: "zinc", shape: "gable", pitch: 12, timber: "wood_soft", eave: 0.3, fastening: "nails_and_stones" },
170
+ frame: { material: "wood_soft", pillars: "wood_soft", beams: "wood_soft", slabs: "concrete", timber: "wood_soft", rebar: null },
171
+ foundation: onSoil ? { type: "ground", material: "soil", elevation: 0 } : { type: "raft", material: "concrete", elevation: 0.08 }
172
+ };
173
+ }
174
+ function campConstruction() {
175
+ return {
176
+ system: "canvas",
177
+ walls: { material: "fabric", interior: "fabric", cladding: null, finish: "nylon", thickness: 2e-3 },
178
+ floor: { material: "plastic", finish: "waterproof_mat" },
179
+ roof: { material: "fabric", covering: "canvas", shape: "tent", pitch: 0, timber: "aluminium", eave: 0 },
180
+ frame: { material: "aluminium", pillars: "aluminium", beams: "aluminium", slabs: "plastic", timber: "aluminium", rebar: null },
181
+ foundation: { type: "ground", material: "soil", elevation: 0 }
182
+ };
183
+ }
184
+ function constructionOfSystem(system, source) {
185
+ const covering = source.facade.roof;
186
+ const keep = covering && covering !== SLAB_COVERING ? { covering } : {};
187
+ switch (system) {
188
+ case "masonry":
189
+ return masonryConstruction(source);
190
+ case "wood_frame":
191
+ return woodFrameConstruction(keep);
192
+ case "log":
193
+ return logCabinConstruction(keep);
194
+ case "improvised":
195
+ return shackConstruction();
196
+ case "canvas":
197
+ return campConstruction();
198
+ default:
199
+ return void 0;
200
+ }
201
+ }
202
+
203
+ // src/construction.ts
204
+ function resolveConstruction(source) {
205
+ const extras = source.structureExtras ?? {};
206
+ return { construction: source.construction ?? masonryConstruction(source), extras };
207
+ }
208
+ var TENT_LABEL = /^tent_(dome|tunnel)(?:_|$)/;
209
+ function tentsOf(source) {
210
+ const tents = [];
211
+ for (const room of source.rooms) {
212
+ if (room.type !== "tent" || room.floor !== 0 || !room.parts[0]) continue;
213
+ const { label } = room;
214
+ tents.push({ id: room.id, kind: TENT_LABEL.exec(typeof label === "string" ? label : "")?.[1] ?? "tunnel", height: room.ceiling ?? source.floors[0]?.ceiling ?? 2.4, aabb: room.parts[0] });
215
+ }
216
+ return tents;
217
+ }
218
+ function constructionFor(context) {
219
+ const forced = context.options?.construction;
220
+ const named = typeof forced === "string" ? forced : context.template?.construction;
221
+ if (named === void 0) return { construction: masonryConstruction(context.result) };
222
+ const registered = CONSTRUCTIONS.get(named);
223
+ if (registered) return registered(context);
224
+ const preset = constructionOfSystem(named, context.result);
225
+ if (preset) return { construction: preset, ...named === "canvas" ? { extras: { tents: tentsOf(context.result) } } : {} };
226
+ throw new Error(`Unknown construction "${named}": register it with registerConstruction`);
227
+ }
228
+
229
+ // src/locales/en.json
230
+ var en_default = {
231
+ ui: {
232
+ structure_layer: "Structure",
233
+ structure_column_concrete: "Concrete pillar",
234
+ structure_column_steel: "Steel post",
235
+ structure_column_masonry: "Masonry pillar",
236
+ structure_column_wood: "Wood post",
237
+ structure_beam_concrete: "Bond beam / beam (concrete)",
238
+ structure_beam_wood: "Plate / beam (wood)",
239
+ structure_roof_frame: "Trusses, rafters and ridge (roof view)"
240
+ }
241
+ };
242
+
243
+ // src/locales/pt-BR.json
244
+ var pt_BR_default = {
245
+ ui: {
246
+ structure_layer: "Estrutura",
247
+ structure_column_concrete: "Pilar de concreto",
248
+ structure_column_steel: "Poste de a\xE7o",
249
+ structure_column_masonry: "Pilar de alvenaria",
250
+ structure_column_wood: "Pilar de madeira",
251
+ structure_beam_concrete: "Cinta / viga (concreto)",
252
+ structure_beam_wood: "Prato / viga (madeira)",
253
+ structure_roof_frame: "Tesouras, caibros e cumeeira (vista do telhado)"
254
+ }
255
+ };
256
+
257
+ // src/types.ts
258
+ var GROUND = "ground";
259
+
260
+ // src/density.ts
261
+ var DENSITY = {
262
+ steel: 7850,
263
+ cast_iron: 7200,
264
+ aluminium: 2700,
265
+ copper: 8900,
266
+ wood_soft: 500,
267
+ wood_hard: 800,
268
+ plywood: 600,
269
+ mdf: 750,
270
+ glass: 2500,
271
+ tempered_glass: 2500,
272
+ ceramic: 2e3,
273
+ concrete: 2400,
274
+ brick: 1800,
275
+ plaster: 1e3,
276
+ stone: 2600,
277
+ plastic: 950,
278
+ rubber: 1100,
279
+ fabric: 400,
280
+ foam: 30,
281
+ leather: 900,
282
+ paper: 800,
283
+ foliage: 100,
284
+ green_wood: 900,
285
+ cactus: 700,
286
+ flesh_fruit: 900,
287
+ wax: 900,
288
+ soil: 1600
289
+ };
290
+ var DEFAULT_DENSITY = 500;
291
+ var densityOf = (material) => DENSITY[material] ?? DEFAULT_DENSITY;
292
+
293
+ // src/structure/builder.ts
294
+ var StructureBuilder = class {
295
+ constructor() {
296
+ __publicField(this, "parts", []);
297
+ __publicField(this, "supports", []);
298
+ __publicField(this, "ids", /* @__PURE__ */ new Set());
299
+ }
300
+ add(draft, options = {}) {
301
+ const { id, role, material, aabb, y0, y1 } = draft;
302
+ if (this.ids.has(id)) throw new Error(`Part repeated in the structure: ${id}`);
303
+ this.ids.add(id);
304
+ const { floor, fill, ...extras } = options;
305
+ const volume = Math.max(0, (aabb[2] - aabb[0]) * (aabb[3] - aabb[1]) * (y1 - y0)) * (fill ?? 1);
306
+ const part = {
307
+ id,
308
+ role,
309
+ material,
310
+ aabb: aabb.map(round2),
311
+ y0: round2(y0),
312
+ y1: round2(y1),
313
+ floor: floor ?? 0,
314
+ mass: Math.max(1, Math.round(volume * densityOf(material)))
315
+ };
316
+ for (const [key, value] of Object.entries(extras)) if (value !== void 0) part[key] = value;
317
+ this.parts.push(part);
318
+ return part;
319
+ }
320
+ rest(part, on, joint) {
321
+ const id = typeof part === "string" ? part : part.id;
322
+ const base = typeof on === "string" ? on : on.id;
323
+ this.supports.push({ part: id, on: base === GROUND ? GROUND : base, joint });
324
+ }
325
+ has(id) {
326
+ return this.ids.has(id);
327
+ }
328
+ find(id) {
329
+ return this.parts.find((part) => part.id === id);
330
+ }
331
+ };
332
+
333
+ // src/structure/legacy-key.ts
334
+ function hashLegacyKey(key) {
335
+ let hash = 1779033703 ^ key.length;
336
+ for (let i = 0; i < key.length; i++) {
337
+ hash = Math.imul(hash ^ key.charCodeAt(i), 3432918353);
338
+ hash = hash << 13 | hash >>> 19;
339
+ }
340
+ hash = Math.imul(hash ^ hash >>> 16, 2246822507);
341
+ hash = Math.imul(hash ^ hash >>> 13, 3266489909);
342
+ return (hash ^ hash >>> 16) >>> 0;
343
+ }
344
+
345
+ // src/structure/context.ts
346
+ var OPEN_ROOM_TYPES = ["balcony", "terrace", "porch"];
347
+ var GUARD_HEIGHT = 1.1;
348
+ function createContext(source, construction, extras, tank, options) {
349
+ const system = CONSTRUCTION_SYSTEMS.get(construction.system);
350
+ if (!system) throw new Error(`Unknown construction system "${construction.system}"`);
351
+ const floorCount = source.floors.length;
352
+ const isUnit = source.floorNumber !== void 0;
353
+ const elevation = construction.foundation.elevation || 0;
354
+ const level = (floor) => round2(elevation + (source.floors[floor]?.y0 ?? 0));
355
+ const ceilingOf = (floor) => source.floors[floor]?.ceiling ?? 0;
356
+ const topOf = (floor) => floor < floorCount - 1 && !isUnit ? level(floor + 1) - system.slabThickness : level(floor) + ceilingOf(floor);
357
+ const rectsOf = (floor, exclude = []) => source.rooms.filter((room) => room.floor === floor && !exclude.includes(room.type)).flatMap((room) => room.parts);
358
+ const builder = new StructureBuilder();
359
+ const context = {
360
+ source,
361
+ construction,
362
+ system,
363
+ extras,
364
+ tank,
365
+ builder,
366
+ porchRoofOnPostsOnly: options.porchRoofOnPostsOnly,
367
+ rafterRoof: options.rafterRoof,
368
+ floorCount,
369
+ isUnit,
370
+ elevation,
371
+ isTimber: system.timber,
372
+ walls: [],
373
+ wallParts: /* @__PURE__ */ new Map(),
374
+ carriers: /* @__PURE__ */ new Map(),
375
+ plates: /* @__PURE__ */ new Map(),
376
+ pillars: /* @__PURE__ */ new Map(),
377
+ porches: /* @__PURE__ */ new Map(),
378
+ base0: GROUND,
379
+ houseBox: [0, 0, 0, 0],
380
+ topBox: [0, 0, 0, 0],
381
+ hasBasement: false,
382
+ roof: { shape: "slab", covering: "", material: "", pitch: 0, bearingFloor: 0, panes: [], trusses: 0 },
383
+ level,
384
+ ceilingOf,
385
+ topOf,
386
+ rectsOf,
387
+ baseOf: (floor) => floor === 0 ? context.base0 : builder.find(`slab_${floor}`)
388
+ };
389
+ const closed = rectsOf(0, ["balcony", "porch"]);
390
+ context.houseBox = unionOf(closed.length ? closed : rectsOf(0));
391
+ context.walls = classifyWalls(context);
392
+ return context;
393
+ }
394
+ var isBearingOnTop = (wall, walls) => {
395
+ const along = (w, pick) => w.axis === "u" ? pick(w.u0, w.u1) : pick(w.v0, w.v1);
396
+ const across = (w) => w.axis === "u" ? w.v0 : w.u0;
397
+ return walls.some(
398
+ (other) => other.floor === wall.floor + 1 && other.type === "wall" && other.axis === wall.axis && Math.abs(across(other) - across(wall)) < 0.2 && Math.min(along(wall, Math.max), along(other, Math.max)) - Math.max(along(wall, Math.min), along(other, Math.min)) > 0.3
399
+ );
400
+ };
401
+ function roleOf(context, wall, walls, porchIds) {
402
+ if (wall.type === "railing") return "partition";
403
+ const { wallRole } = context.system;
404
+ if (wallRole === "bearing") return "wall";
405
+ if (wallRole === "partition") return "partition";
406
+ const bearingFloor = context.construction.roof.bearingFloor ?? context.floorCount - 1;
407
+ if (wall.floor > bearingFloor) return "partition";
408
+ if (wall.exterior || wall.sides.some((id) => porchIds.has(id))) return "wall";
409
+ return wall.floor < context.floorCount - 1 && isBearingOnTop(wall, walls) ? "wall" : "partition";
410
+ }
411
+ function materialOf(context, wall) {
412
+ const { walls, guard } = context.construction;
413
+ if (wall.type === "railing") return guard ?? (context.isTimber ? "wood_soft" : "steel");
414
+ if (walls.mixed?.length) return walls.mixed[hashLegacyKey(wall.legacyKey) % walls.mixed.length];
415
+ return wall.exterior ? walls.material : walls.interior ?? walls.material;
416
+ }
417
+ function classifyWalls(context) {
418
+ const countByFloor = /* @__PURE__ */ new Map();
419
+ const all = context.source.walls.map((wall) => {
420
+ const index = countByFloor.get(wall.floor) ?? 0;
421
+ countByFloor.set(wall.floor, index + 1);
422
+ return { ...wall, id: `wall_${wall.floor}_${index}`, legacyKey: `w${wall.floor}_${index}`, role: "wall", material: "", box: wallBox(wall) };
423
+ });
424
+ const built = all.filter((wall) => wall.type === "wall" || wall.type === "railing");
425
+ const porchIds = new Set(context.source.rooms.filter((room) => room.type === "porch").map((room) => room.id));
426
+ for (const wall of built) {
427
+ wall.role = roleOf(context, wall, built, porchIds);
428
+ wall.material = materialOf(context, wall);
429
+ }
430
+ return built;
431
+ }
432
+ var guardHeightOf = (wall) => wall.h || GUARD_HEIGHT;
433
+
434
+ // src/structure/balconies.ts
435
+ function addBalconies(context) {
436
+ const { builder, construction, system } = context;
437
+ for (const room of context.source.rooms.filter((r) => r.type === "balcony" && r.floor >= 0)) {
438
+ const floor = room.floor;
439
+ const box = unionOf(room.parts);
440
+ const base = floor === 0 ? void 0 : context.baseOf(floor);
441
+ const deck = builder.add({ id: `slab_${floor}_${room.id}`, role: "slab", material: construction.frame.slabs, aabb: box, y0: context.level(floor) - system.slabThickness, y1: context.level(floor) }, { floor });
442
+ if (base) {
443
+ builder.rest(deck, base, system.joints.slab);
444
+ continue;
445
+ }
446
+ const pillar = builder.add({ id: `balcony_column_${room.id}`, role: "column", material: construction.frame.pillars, aabb: [box[0] - 0.08, box[1] - 0.08, box[0] + 0.08, box[1] + 0.08], y0: 0, y1: context.level(floor) - system.slabThickness }, { floor: 0 });
447
+ builder.rest(pillar, GROUND, "rebar");
448
+ builder.rest(deck, pillar, "rebar");
449
+ }
450
+ }
451
+
452
+ // src/structure/canvas.ts
453
+ var GROUNDSHEET = 0.02;
454
+ function buildCanvasStructure(construction, extras) {
455
+ const builder = new StructureBuilder();
456
+ const panes = [];
457
+ for (const tent of extras.tents ?? []) {
458
+ const [u0, v0, u1, v1] = tent.aabb;
459
+ const centreU = (u0 + u1) / 2;
460
+ const centreV = (v0 + v1) / 2;
461
+ const sheet = builder.add({ id: `groundsheet_${tent.id}`, role: "slab", material: "plastic", aabb: tent.aabb, y0: 0, y1: GROUNDSHEET }, { floor: 0, tent: tent.id });
462
+ builder.rest(sheet, GROUND, "resting");
463
+ const canvas = builder.add({ id: `canvas_${tent.id}`, role: "roof", material: "fabric", aabb: tent.aabb, y0: GROUNDSHEET, y1: tent.height }, { floor: 0, fill: 0.04, tent: tent.id, form: tent.kind });
464
+ panes.push(canvas.id);
465
+ const poles = tent.kind === "tunnel" ? [0.25, 0.75].map((fraction, k) => [[u0 + (u1 - u0) * fraction - 0.015, v0 + 0.05, u0 + (u1 - u0) * fraction + 0.015, v1 - 0.05], k]) : [[[u0 + 0.05, centreV - 0.015, u1 - 0.05, centreV + 0.015], 0], [[centreU - 0.015, v0 + 0.05, centreU + 0.015, v1 - 0.05], 1]];
466
+ for (const [aabb, k] of poles) {
467
+ const pole = builder.add({ id: `pole_${tent.id}_${k}`, role: "column", material: construction.frame.pillars, aabb, y0: GROUNDSHEET, y1: tent.height - GROUNDSHEET }, { floor: 0, fill: 0.05, tent: tent.id });
468
+ builder.rest(pole, GROUND, "resting");
469
+ builder.rest(canvas, pole, "tie");
470
+ }
471
+ const corners = [[u0 - 0.2, v0 - 0.2], [u1 + 0.2, v0 - 0.2], [u0 - 0.2, v1 + 0.2], [u1 + 0.2, v1 + 0.2]];
472
+ corners.forEach(([u, v], k) => {
473
+ const peg = builder.add({ id: `peg_${tent.id}_${k}`, role: "column", material: "wood_soft", aabb: [u - 0.02, v - 0.02, u + 0.02, v + 0.02], y0: 0, y1: 0.2 }, { floor: 0 });
474
+ builder.rest(peg, GROUND, "nail");
475
+ builder.rest(canvas, peg, "tie");
476
+ });
477
+ }
478
+ const roof = { shape: "tent", covering: "canvas", material: "fabric", pitch: 0, bearingFloor: 0, panes, trusses: 0 };
479
+ return { parts: builder.parts, supports: builder.supports, levels: [0], elevation: 0, roof, construction, framed: false, openings: [] };
480
+ }
481
+
482
+ // src/structure/extras.ts
483
+ var CHIMNEY_CLEARANCE_ABOVE_RIDGE = 0.6;
484
+ function addChimney(context) {
485
+ const chimney = context.extras.chimney;
486
+ if (!chimney) return;
487
+ const { builder } = context;
488
+ const top = context.floorCount - 1;
489
+ const ridge = context.roof.ridge || context.level(top) + context.ceilingOf(top);
490
+ const height = Math.max(ridge, chimney.top || 0) + CHIMNEY_CLEARANCE_ABOVE_RIDGE;
491
+ const part = builder.add({ id: "chimney", role: "column", material: chimney.material || "brick", aabb: chimney.aabb, y0: chimney.flue ? context.level(0) : 0, y1: height }, { floor: 0, fill: chimney.flue ? 0.15 : 0.7 });
492
+ if (chimney.flue) builder.rest(part, context.base0 ?? GROUND, "resting");
493
+ else builder.rest(part, GROUND, "mortar");
494
+ const walls = builder.parts.filter((wall) => wall.role === "wall" && wall.floor === 0 && !wall.id.startsWith("basement") && touches(wall.aabb, part.aabb, 0.1));
495
+ for (const wall of walls) builder.rest(part, wall, "bolt");
496
+ chimney.height = round2(height);
497
+ }
498
+ function addWell(context) {
499
+ const well = context.extras.well;
500
+ if (!well) return;
501
+ const part = context.builder.add({ id: "well", role: "column", material: "stone", aabb: well.aabb, y0: -0.5, y1: well.height || 0.9 }, { floor: 0, fill: 0.5 });
502
+ context.builder.rest(part, GROUND, "mortar");
503
+ }
504
+
505
+ // src/structure/floor-deck.ts
506
+ var JOIST_PITCH = 0.6;
507
+ function addJoists(context, options) {
508
+ const { floor, box, thickness, carriers, material, joint, suffix = "" } = options;
509
+ const length = box[2] - box[0];
510
+ const depth = box[3] - box[1];
511
+ const alongU = length >= depth;
512
+ const span = alongU ? length : depth;
513
+ const count = Math.max(2, Math.round(span / JOIST_PITCH) + 1);
514
+ const top = context.level(floor) - thickness * 0.15;
515
+ const out = [];
516
+ for (let k = 0; k < count; k++) {
517
+ const centre = (alongU ? box[0] : box[1]) + 0.05 + k * (span - 0.1) / (count - 1);
518
+ const aabb = alongU ? [centre - 0.025, box[1], centre + 0.025, box[3]] : [box[0], centre - 0.025, box[2], centre + 0.025];
519
+ 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 });
520
+ out.push(joist);
521
+ const below = carriers.filter((carrier) => carrier.y0 <= top + 0.05);
522
+ const touching = below.filter((carrier) => touches(aabb, carrier.aabb, 0.2));
523
+ const pool = below.length ? below : carriers;
524
+ const chosen = touching.length ? touching : pool.slice().sort((a, b) => distanceBetween(aabb, a.aabb) - distanceBetween(aabb, b.aabb)).slice(0, 1);
525
+ for (const carrier of chosen) context.builder.rest(joist, carrier, joint);
526
+ }
527
+ return out;
528
+ }
529
+ function addDeck(context, id, material, options, joists) {
530
+ const { floor, box, thickness, joint } = options;
531
+ const deck = context.builder.add({ id, role: "slab", material, aabb: box, y0: context.level(floor) - thickness * 0.15, y1: context.level(floor) }, { floor });
532
+ for (const joist of joists) context.builder.rest(deck, joist, joint);
533
+ return deck;
534
+ }
535
+
536
+ // src/structure/foundation.ts
537
+ var raft = (context, thickness, material) => {
538
+ const [u0, v0, u1, v1] = context.houseBox;
539
+ const part = context.builder.add({ id: "foundation_raft", role: "slab", material, aabb: [u0 - 0.1, v0 - 0.1, u1 + 0.1, v1 + 0.1], y0: 0, y1: thickness }, { floor: 0 });
540
+ context.builder.rest(part, GROUND, "resting");
541
+ return part;
542
+ };
543
+ function addBuildingBase(context) {
544
+ const { builder, construction, houseBox: box } = context;
545
+ const corners = [[box[0], box[1]], [box[2], box[1]], [box[0], box[3]], [box[2], box[3]]];
546
+ const top = context.level(0) - context.system.slabThickness;
547
+ const pillars = corners.map(([u, v], k) => {
548
+ const pillar = builder.add({ id: `building_column_${k}`, role: "column", material: construction.frame.pillars, aabb: [u - 0.15, v - 0.15, u + 0.15, v + 0.15], y0: 0, y1: top }, { floor: 0 });
549
+ builder.rest(pillar, GROUND, "rebar");
550
+ return pillar;
551
+ });
552
+ const slab = builder.add({ id: "slab_base", role: "slab", material: construction.frame.slabs, aabb: box, y0: top, y1: context.level(0) }, { floor: 0 });
553
+ for (const pillar of pillars) builder.rest(slab, pillar, "rebar");
554
+ context.base0 = slab;
555
+ }
556
+ function addBasement(context) {
557
+ const { builder, houseBox: box, elevation } = context;
558
+ const depth = context.construction.foundation.depth || 2.4;
559
+ const floor = builder.add({ id: "basement_slab", role: "slab", material: "concrete", aabb: box, y0: -depth - 0.15, y1: -depth }, { floor: -1 });
560
+ builder.rest(floor, GROUND, "resting");
561
+ const sides = [
562
+ ["front", [box[0], box[1] - 0.1, box[2], box[1] + 0.1]],
563
+ ["back", [box[0], box[3] - 0.1, box[2], box[3] + 0.1]],
564
+ ["left", [box[0] - 0.1, box[1], box[0] + 0.1, box[3]]],
565
+ ["right", [box[2] - 0.1, box[1], box[2] + 0.1, box[3]]]
566
+ ];
567
+ const carried = [];
568
+ for (const [name, aabb] of sides) {
569
+ const wall = builder.add({ id: `basement_wall_${name}`, role: "wall", material: "concrete", aabb, y0: -depth, y1: elevation }, { floor: -1 });
570
+ builder.rest(wall, GROUND, "rebar");
571
+ builder.rest(wall, floor, "rebar");
572
+ carried.push(wall);
573
+ }
574
+ const count = Math.max(1, Math.floor((box[2] - box[0]) / 3));
575
+ for (let k = 0; k < count; k++) {
576
+ const u = box[0] + (k + 0.5) * (box[2] - box[0]) / count;
577
+ const v = (box[1] + box[3]) / 2;
578
+ const column = builder.add({ id: `basement_column_${k}`, role: "column", material: "concrete", aabb: [u - 0.15, v - 0.15, u + 0.15, v + 0.15], y0: -depth, y1: elevation - 0.3 }, { floor: -1 });
579
+ builder.rest(column, floor, "rebar");
580
+ builder.rest(column, GROUND, "rebar");
581
+ carried.push(column);
582
+ }
583
+ context.hasBasement = true;
584
+ return carried;
585
+ }
586
+ function addPiers(context) {
587
+ const { builder, houseBox: box, elevation, construction } = context;
588
+ const columns = Math.max(2, Math.ceil((box[2] - box[0]) / 2) + 1);
589
+ const rows = Math.max(2, Math.ceil((box[3] - box[1]) / 2) + 1);
590
+ const height = Math.max(0.1, elevation - context.system.slabThickness - 0.15);
591
+ const piers = [];
592
+ for (let j = 0; j < rows; j++) {
593
+ for (let i = 0; i < columns; i++) {
594
+ const u = box[0] + i * (box[2] - box[0]) / (columns - 1);
595
+ const v = box[1] + j * (box[3] - box[1]) / (rows - 1);
596
+ const pier = builder.add({ id: `footing_pier_${j}_${i}`, role: "column", material: construction.foundation.material || "concrete", aabb: [u - 0.12, v - 0.12, u + 0.12, v + 0.12], y0: 0, y1: height }, { floor: 0 });
597
+ builder.rest(pier, GROUND, "resting");
598
+ piers.push(pier);
599
+ }
600
+ }
601
+ const alongU = box[2] - box[0] >= box[3] - box[1];
602
+ const beams = [];
603
+ const lines = alongU ? rows : columns;
604
+ for (let line = 0; line < lines; line++) {
605
+ const at2 = alongU ? box[1] + line * (box[3] - box[1]) / (rows - 1) : box[0] + line * (box[2] - box[0]) / (columns - 1);
606
+ const aabb = alongU ? [box[0], at2 - 0.07, box[2], at2 + 0.07] : [at2 - 0.07, box[1], at2 + 0.07, box[3]];
607
+ const beam = builder.add({ id: `base_beam_${line}`, role: "beam", material: construction.frame.timber, aabb, y0: height, y1: elevation - context.system.slabThickness }, { floor: 0 });
608
+ const centre = (q) => alongU ? (q.aabb[1] + q.aabb[3]) / 2 : (q.aabb[0] + q.aabb[2]) / 2;
609
+ for (const pier of piers.filter((q) => Math.abs(centre(q) - at2) < 0.05)) builder.rest(beam, pier, "bolt");
610
+ beams.push(beam);
611
+ }
612
+ return beams;
613
+ }
614
+ function addRaisedFloor(context) {
615
+ const { construction, system, houseBox: box } = context;
616
+ const carriers = construction.foundation.type === "basement" ? addBasement(context) : addPiers(context);
617
+ const thickness = system.slabThickness;
618
+ const joists = addJoists(context, { floor: 0, box, thickness, carriers, material: construction.frame.timber, joint: construction.foundation.type === "basement" ? "bolt" : "nail" });
619
+ const deckMaterial = context.isTimber ? construction.floor.material === "wood_hard" ? "wood_hard" : "plywood" : construction.frame.slabs;
620
+ context.base0 = addDeck(context, "slab_0", deckMaterial, { floor: 0, box, thickness, joint: "nail" }, joists);
621
+ }
622
+ function addFoundation(context) {
623
+ const { type, material } = context.construction.foundation;
624
+ if (context.isUnit) return addBuildingBase(context);
625
+ if (type === "raft") context.base0 = raft(context, Math.max(0.1, context.elevation), material || "concrete");
626
+ else if (type === "crawl_space" || type === "piles" || type === "basement") addRaisedFloor(context);
627
+ else if (context.isTimber && type !== "ground") context.base0 = raft(context, Math.max(0.12, context.elevation), "concrete");
628
+ }
629
+
630
+ // src/structure/plates.ts
631
+ var MAX_GAP_WITHIN_RUN = 3.2;
632
+ var POST_SPACING = 2.4;
633
+ var POST_SIDE = 0.05;
634
+ var alongOf = (wall, pick) => wall.axis === "u" ? pick(wall.u0, wall.u1) : pick(wall.v0, wall.v1);
635
+ var acrossOf = (wall) => wall.axis === "u" ? wall.v0 : wall.u0;
636
+ function runsOf(context, walls, floor) {
637
+ const lines = /* @__PURE__ */ new Map();
638
+ for (const wall of walls) {
639
+ if (!wall.exterior || wall.type !== "wall") continue;
640
+ const top = round2(context.wallParts.get(wall.id).y1);
641
+ const key = `${floor}|${wall.axis}|${acrossOf(wall).toFixed(2)}|${top.toFixed(2)}`;
642
+ lines.set(key, [...lines.get(key) ?? [], wall]);
643
+ }
644
+ return [...lines.values()].map((line) => {
645
+ const sorted = line.slice().sort((a, b) => alongOf(a, Math.min) - alongOf(b, Math.min));
646
+ const runs = [];
647
+ for (const wall of sorted) {
648
+ const from = alongOf(wall, Math.min);
649
+ const to = alongOf(wall, Math.max);
650
+ const last = runs[runs.length - 1];
651
+ if (last && from - last.to <= MAX_GAP_WITHIN_RUN) {
652
+ last.to = Math.max(last.to, to);
653
+ last.walls.push(wall);
654
+ } else runs.push({ from, to, walls: [wall] });
655
+ }
656
+ return runs;
657
+ });
658
+ }
659
+ function addShackPosts(context, run, plate, floor, index) {
660
+ const first = run.walls[0];
661
+ const across = acrossOf(first);
662
+ const top = context.wallParts.get(first.id).y1;
663
+ const count = Math.max(1, Math.ceil((run.to - run.from) / POST_SPACING));
664
+ const posts = [];
665
+ for (let k = 0; k <= count; k++) {
666
+ const along = run.from + (run.to - run.from) * k / count;
667
+ const [u, v] = first.axis === "u" ? [along, across] : [across, along];
668
+ const half = 0.05 + POST_SIDE / 2;
669
+ const post = context.builder.add({ id: `post_${floor}_${index}_${k}`, role: "column", material: "wood_soft", aabb: [u - half, v - half, u + half, v + half], y0: context.level(floor), y1: top }, { floor });
670
+ const base = context.baseOf(floor);
671
+ if (base === GROUND || !base) context.builder.rest(post, GROUND, "tie");
672
+ else context.builder.rest(post, base, "nail");
673
+ context.builder.rest(plate, post, "nail");
674
+ posts.push(post);
675
+ }
676
+ return posts;
677
+ }
678
+ function addPlates(context, walls, floor) {
679
+ const { builder, system, construction } = context;
680
+ const plates = [];
681
+ const pillars = [];
682
+ let counter = 0;
683
+ for (const runs of runsOf(context, walls, floor)) {
684
+ for (const run of runs) {
685
+ const first = run.walls[0];
686
+ const half = (first.thickness ?? 0.15) / 2;
687
+ const across = acrossOf(first);
688
+ const top = context.wallParts.get(first.id).y1;
689
+ 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];
690
+ const material = context.isTimber ? construction.frame.timber : construction.frame.beams;
691
+ const plate = builder.add({ id: `plate_${floor}_${counter++}`, role: "beam", material, aabb, y0: top, y1: top + system.plateThickness }, { floor, beamKind: context.isTimber ? "plate" : "bond_beam" });
692
+ if (construction.system === "improvised") pillars.push(...addShackPosts(context, run, plate, floor, counter));
693
+ else for (const wall of run.walls) builder.rest(plate, context.wallParts.get(wall.id), system.joints.beam);
694
+ plates.push(plate);
695
+ }
696
+ }
697
+ return { plates, pillars };
698
+ }
699
+ function cornerPoints(walls) {
700
+ const ends = walls.filter((wall) => wall.exterior && wall.type === "wall").flatMap(
701
+ (wall) => wall.axis === "u" ? [[Math.min(wall.u0, wall.u1), wall.v0, "u"], [Math.max(wall.u0, wall.u1), wall.v0, "u"]] : [[wall.u0, Math.min(wall.v0, wall.v1), "v"], [wall.u0, Math.max(wall.v0, wall.v1), "v"]]
702
+ );
703
+ const points = [];
704
+ for (const a of ends) {
705
+ const joinsOtherAxis = ends.some((b) => b[2] !== a[2] && Math.hypot(a[0] - b[0], a[1] - b[1]) < 0.12);
706
+ if (joinsOtherAxis && !points.some((q) => Math.hypot(q[0] - a[0], q[1] - a[1]) < 0.12)) points.push([a[0], a[1]]);
707
+ }
708
+ return points;
709
+ }
710
+ function addCornerPillars(context, walls, floor, plates) {
711
+ const { builder } = context;
712
+ const topOfFloor = context.topOf(floor);
713
+ return cornerPoints(walls).map(([u, v], k) => {
714
+ const box = [u - 0.1, v - 0.1, u + 0.1, v + 0.1];
715
+ const top = Math.max(topOfFloor, ...plates.filter((plate) => touches(plate.aabb, box, 0.05)).map((plate) => plate.y0));
716
+ const pillar = builder.add({ id: `column_${floor}_${k}`, role: "column", material: context.construction.frame.pillars, aabb: box, y0: context.level(floor), y1: top }, { floor });
717
+ const base = context.baseOf(floor);
718
+ builder.rest(pillar, base === GROUND || !base ? GROUND : base, "rebar");
719
+ for (const plate of plates.filter((p) => touches(p.aabb, pillar.aabb, 0.05))) builder.rest(plate, pillar, "rebar");
720
+ return pillar;
721
+ });
722
+ }
723
+ function addPlatesAndPillars(context, floor) {
724
+ const walls = context.walls.filter((wall) => wall.floor === floor);
725
+ const { plates, pillars } = addPlates(context, walls, floor);
726
+ if (context.construction.system === "masonry") pillars.push(...addCornerPillars(context, walls, floor, plates));
727
+ context.pillars.set(floor, pillars);
728
+ context.plates.set(floor, plates);
729
+ const innerBearing = walls.filter((wall) => wall.role === "wall" && !wall.exterior).map((wall) => context.wallParts.get(wall.id));
730
+ context.carriers.set(floor, [...plates, ...pillars, ...innerBearing]);
731
+ }
732
+
733
+ // src/structure/porch.ts
734
+ var POST_HALF = 0.08;
735
+ var FOOTING_HALF = 0.12;
736
+ var BEAM_DEPTH = 0.2;
737
+ var BEAM_WIDTH = 0.16;
738
+ function addPosts(context, porchId, rect, deck, floor) {
739
+ const { builder, construction } = context;
740
+ const hasUpperFloor = floor < context.floorCount - 1;
741
+ const ceilingY = hasUpperFloor ? context.level(floor + 1) - context.system.slabThickness : context.level(floor) + context.ceilingOf(floor);
742
+ const deckDepth = Math.min(0.2, Math.max(0.06, context.elevation * 0.5));
743
+ const spots = [[rect[0] + 0.1, rect[1] + 0.1], [rect[2] - 0.1, rect[1] + 0.1]];
744
+ if (rect[2] - rect[0] > 4.5) spots.push([(rect[0] + rect[2]) / 2, rect[1] + 0.1]);
745
+ const posts = spots.map(([u, v], k) => {
746
+ const footing = builder.add({ id: `porch_footing_${porchId}_${k}`, role: "column", material: "concrete", aabb: [u - FOOTING_HALF, v - FOOTING_HALF, u + FOOTING_HALF, v + FOOTING_HALF], y0: 0, y1: context.level(floor) - deckDepth }, { floor: 0 });
747
+ builder.rest(footing, GROUND, "resting");
748
+ builder.rest(deck, footing, "resting");
749
+ const material = context.isTimber ? construction.frame.timber : construction.frame.pillars;
750
+ const post = builder.add({ id: `porch_post_${porchId}_${k}`, role: "column", material, aabb: [u - POST_HALF, v - POST_HALF, u + POST_HALF, v + POST_HALF], y0: context.level(floor), y1: hasUpperFloor ? ceilingY - BEAM_DEPTH : ceilingY }, { floor });
751
+ builder.rest(post, deck, context.isTimber ? "nail" : "rebar");
752
+ return post;
753
+ });
754
+ context.porches.set(porchId, { posts, ceilingY });
755
+ return posts;
756
+ }
757
+ function addUpperBeams(context, porchId, rect, posts, floor) {
758
+ const { builder, construction } = context;
759
+ const material = context.isTimber ? construction.frame.timber : construction.frame.beams;
760
+ const joint = context.isTimber ? "nail" : "rebar";
761
+ const ceilingY = context.porches.get(porchId).ceilingY;
762
+ const y0 = ceilingY - BEAM_DEPTH;
763
+ const carriers = context.carriers.get(floor);
764
+ const head = builder.add({ id: `porch_beam_${porchId}`, role: "beam", material, aabb: [rect[0], rect[1], rect[2], rect[1] + BEAM_WIDTH], y0, y1: ceilingY }, { floor });
765
+ for (const post of posts) builder.rest(head, post, joint);
766
+ carriers.push(head);
767
+ const sides = [["e", [rect[0], rect[1], rect[0] + BEAM_WIDTH, rect[3]], posts[0]], ["d", [rect[2] - BEAM_WIDTH, rect[1], rect[2], rect[3]], posts[1]]];
768
+ for (const [name, aabb, post] of sides) {
769
+ const side = builder.add({ id: `porch_beam_${porchId}_${name}`, role: "beam", material, aabb, y0, y1: ceilingY }, { floor });
770
+ builder.rest(side, post, joint);
771
+ builder.rest(side, head, joint);
772
+ for (const wall of context.walls.filter((w) => w.floor === floor && w.role === "wall" && touches(w.box, side.aabb, 0.3))) builder.rest(side, context.wallParts.get(wall.id), joint);
773
+ carriers.push(side);
774
+ }
775
+ }
776
+ function addPorches(context, floor) {
777
+ const { builder, construction } = context;
778
+ for (const room of context.source.rooms.filter((r) => r.type === "porch" && r.floor === floor)) {
779
+ const rect = room.parts[0];
780
+ const base = context.baseOf(floor);
781
+ const deckDepth = Math.min(0.2, Math.max(0.06, context.elevation * 0.5));
782
+ const deck = builder.add({ id: `porch_floor_${room.id}`, role: "slab", material: context.isTimber ? "wood_soft" : construction.frame.slabs, aabb: rect, y0: context.level(floor) - deckDepth, y1: context.level(floor) }, { floor });
783
+ if (base && base !== GROUND) builder.rest(deck, base, context.isTimber ? "nail" : context.system.joints.slab);
784
+ const posts = addPosts(context, room.id, rect, deck, floor);
785
+ if (floor < context.floorCount - 1) addUpperBeams(context, room.id, rect, posts, floor);
786
+ }
787
+ }
788
+
789
+ // src/structure/porch-roof.ts
790
+ var ROOF_RISE = 0.6;
791
+ function addPorchRoofs(context) {
792
+ const { builder, construction } = context;
793
+ for (const room of context.source.rooms.filter((r) => r.type === "porch")) {
794
+ const floor = room.floor;
795
+ const rect = room.parts[0];
796
+ const posts = context.porches.get(room.id);
797
+ if (floor !== context.floorCount - 1 || !posts) continue;
798
+ const material = ROOF_COVERINGS.get(construction.roof.covering)?.material ?? "ceramic";
799
+ const roof = builder.add({ id: `porch_roof_${room.id}`, role: "roof", material, aabb: [rect[0] - 0.3, rect[1] - 0.5, rect[2] + 0.3, rect[3]], y0: posts.ceilingY, y1: posts.ceilingY + ROOF_RISE }, { floor });
800
+ for (const post of posts.posts) builder.rest(roof, post, context.isTimber ? "nail" : "bolt");
801
+ const housewalls = context.walls.filter((w) => w.floor === floor && !w.exterior && w.sides.includes(room.id) && w.type === "wall");
802
+ for (const wall of housewalls) {
803
+ if (context.porchRoofOnPostsOnly) builder.supports.push({ part: roof.id, on: wall.id, joint: "nail", bears: false });
804
+ else builder.rest(roof, context.wallParts.get(wall.id), "nail");
805
+ }
806
+ }
807
+ }
808
+
809
+ // src/structure/roof.ts
810
+ var TRUSS_PITCH = 1.2;
811
+ var DEFAULT_PITCH = 20;
812
+ var DEFAULT_EAVE = 0.4;
813
+ var CLOSED_ONLY = [...OPEN_ROOM_TYPES, "void"];
814
+ function roofUnits(context, bearing) {
815
+ const units = [{ suffix: "", box: context.topBox, floor: bearing, annex: false }];
816
+ if (context.floorCount <= 1 || context.isUnit || bearing !== context.floorCount - 1) return units;
817
+ for (let floor = 0; floor < context.floorCount - 1; floor++) {
818
+ const above = context.source.rooms.filter((room) => room.floor === floor + 1).flatMap((room) => room.parts);
819
+ const rooms = context.source.rooms.filter((room) => room.floor === floor && !OPEN_ROOM_TYPES.includes(room.type) && room.type !== "void" && room.type !== "elevator");
820
+ for (const room of rooms) {
821
+ const area = room.parts.reduce((total, part) => total + (part[2] - part[0]) * (part[3] - part[1]), 0);
822
+ const covered = room.parts.reduce((total, part) => total + above.reduce((sum, other) => sum + Math.max(0, Math.min(part[2], other[2]) - Math.max(part[0], other[0])) * Math.max(0, Math.min(part[3], other[3]) - Math.max(part[1], other[1])), 0), 0);
823
+ if (covered < area * 0.7) units.push({ suffix: `_${room.id}`, box: unionOf(room.parts), floor, annex: true });
824
+ }
825
+ }
826
+ return units;
827
+ }
828
+ function roofCarriers(context, unit) {
829
+ const platesHere = context.plates.get(unit.floor) ?? [];
830
+ const allPlates = [...context.plates.values()].flat();
831
+ const eaveHeight = platesHere.length ? Math.max(...platesHere.map((plate) => plate.y1)) : 0;
832
+ const plates = allPlates.filter((plate) => Math.abs(plate.y1 - eaveHeight) < 0.4 && touches(plate.aabb, unit.box, 0.3));
833
+ const pillars = (context.pillars.get(unit.floor) ?? []).filter((pillar) => touches(pillar.aabb, unit.box, 0.3));
834
+ const walls = context.walls.filter((wall) => wall.floor === unit.floor && wall.role === "wall" && !wall.exterior && touches(wall.box, unit.box, 0.2)).map((wall) => context.wallParts.get(wall.id));
835
+ return [...plates, ...pillars, ...walls];
836
+ }
837
+ function addFlatRoof(context, unit, carriers) {
838
+ const { builder, construction, system } = context;
839
+ const top = context.level(unit.floor) + context.ceilingOf(unit.floor);
840
+ const [u0, v0, u1, v1] = unit.box;
841
+ const slab = builder.add({ id: `slab_top${unit.suffix}`, role: "slab", material: construction.frame.slabs, aabb: [u0 - 0.15, v0 - 0.15, u1 + 0.15, v1 + 0.15], y0: top, y1: top + 0.12 }, { floor: unit.floor, cover: true });
842
+ for (const carrier of carriers.length ? carriers : context.plates.get(unit.floor) ?? []) builder.rest(slab, carrier, system.joints.slab);
843
+ if (unit.suffix === "") context.topSlab = slab;
844
+ return { id: slab.id, aabb: slab.aabb, y0: slab.y0, y1: slab.y1, flat: true };
845
+ }
846
+ function addTrusses(context, unit, carriers, geometry) {
847
+ const { builder, construction, system } = context;
848
+ const { box } = unit;
849
+ const { axis, length, eave, rise, eaveY } = geometry;
850
+ const count = Math.max(2, Math.ceil(length / TRUSS_PITCH) + 1);
851
+ const trusses = [];
852
+ for (let k = 0; k < count; k++) {
853
+ const at2 = (axis === "u" ? box[0] : box[1]) + k * length / (count - 1);
854
+ const aabb = axis === "u" ? [at2 - 0.04, box[1] - eave * 0.5, at2 + 0.04, box[3] + eave * 0.5] : [box[0] - eave * 0.5, at2 - 0.04, box[2] + eave * 0.5, at2 + 0.04];
855
+ const truss = builder.add({ id: `truss${unit.suffix}_${k}`, role: "beam", material: construction.frame.timber, aabb, y0: eaveY, y1: eaveY + rise }, { floor: unit.floor, fill: 0.25 });
856
+ const touching = carriers.filter((carrier) => touches(aabb, carrier.aabb, 0.2) && carrier.role !== "slab");
857
+ const chosen = touching.length ? touching : carriers.slice().sort((a, b) => distanceBetween(aabb, a.aabb) - distanceBetween(aabb, b.aabb)).slice(0, 2);
858
+ for (const carrier of chosen) builder.rest(truss, carrier, system.joints.truss);
859
+ trusses.push(truss);
860
+ }
861
+ return trusses;
862
+ }
863
+ function geometryOf(context, unit) {
864
+ const { box } = unit;
865
+ const roof = context.construction.roof;
866
+ const platesHere = context.plates.get(unit.floor) ?? [];
867
+ const width = box[2] - box[0];
868
+ const depth = box[3] - box[1];
869
+ const axis = roof.axis ?? (width >= depth ? "u" : "v");
870
+ const span = axis === "u" ? depth : width;
871
+ const pitch = unit.annex ? Math.min(roof.pitch || DEFAULT_PITCH, 20) : roof.pitch || DEFAULT_PITCH;
872
+ const eaveY = platesHere.length ? Math.max(...platesHere.map((plate) => plate.y1)) : context.level(unit.floor) + context.ceilingOf(unit.floor);
873
+ return { axis, length: axis === "u" ? width : depth, eave: roof.eave ?? DEFAULT_EAVE, rise: round2(span / 2 * Math.tan(pitch * Math.PI / 180)), eaveY };
874
+ }
875
+ function addRafters(context, unit, carriers, geometry) {
876
+ const { builder, construction, system } = context;
877
+ const { box } = unit;
878
+ const { axis, length, eave, rise, eaveY } = geometry;
879
+ const count = Math.max(2, Math.ceil(length / TRUSS_PITCH) + 1);
880
+ const middle = axis === "u" ? (box[1] + box[3]) / 2 : (box[0] + box[2]) / 2;
881
+ const frames = { all: [], sideA: [], sideB: [] };
882
+ for (let k = 0; k < count; k++) {
883
+ const at2 = (axis === "u" ? box[0] : box[1]) + k * length / (count - 1);
884
+ const halves = axis === "u" ? [["a", [at2 - 0.04, box[1] - eave * 0.5, at2 + 0.04, middle]], ["b", [at2 - 0.04, middle, at2 + 0.04, box[3] + eave * 0.5]]] : [["a", [box[0] - eave * 0.5, at2 - 0.04, middle, at2 + 0.04]], ["b", [middle, at2 - 0.04, box[2] + eave * 0.5, at2 + 0.04]]];
885
+ for (const [side, aabb] of halves) {
886
+ const rafter = builder.add({ id: `rafter${unit.suffix}_${side}_${k}`, role: "beam", material: construction.frame.timber, aabb, y0: eaveY, y1: eaveY + rise }, { floor: unit.floor, fill: 0.25 });
887
+ const touching = carriers.filter((carrier) => touches(aabb, carrier.aabb, 0.2) && carrier.role !== "slab");
888
+ const chosen = touching.length ? touching : carriers.slice().sort((x, y) => distanceBetween(aabb, x.aabb) - distanceBetween(aabb, y.aabb)).slice(0, 2);
889
+ for (const carrier of chosen) builder.rest(rafter, carrier, system.joints.truss);
890
+ frames.all.push(rafter);
891
+ if (side === "a") frames.sideA.push(rafter);
892
+ else frames.sideB.push(rafter);
893
+ }
894
+ }
895
+ if (!unit.annex) {
896
+ const ridgeBox = axis === "u" ? [box[0] - eave, middle - 0.05, box[2] + eave, middle + 0.05] : [middle - 0.05, box[1] - eave, middle + 0.05, box[3] + eave];
897
+ const ridge = builder.add({ id: `ridge${unit.suffix}`, role: "beam", material: construction.frame.timber, aabb: ridgeBox, y0: eaveY + rise - 0.12, y1: eaveY + rise }, { floor: unit.floor });
898
+ for (const rafter of frames.all) builder.rest(ridge, rafter, system.joints.truss);
899
+ }
900
+ context.roof.trusses += count;
901
+ return frames;
902
+ }
903
+ function panesOf(unit, geometry) {
904
+ const [u0, v0, u1, v1] = unit.box;
905
+ const { eave, axis } = geometry;
906
+ if (unit.annex) return [[u0 - eave, v0 - eave, u1 + eave, v1 + eave]];
907
+ const midV = (v0 + v1) / 2;
908
+ const midU = (u0 + u1) / 2;
909
+ return axis === "u" ? [[u0 - eave, v0 - eave, u1 + eave, midV], [u0 - eave, midV, u1 + eave, v1 + eave]] : [[u0 - eave, v0 - eave, midU, v1 + eave], [midU, v0 - eave, u1 + eave, v1 + eave]];
910
+ }
911
+ function coarseFrames(context, unit, carriers, geometry) {
912
+ const trusses = addTrusses(context, unit, carriers, geometry);
913
+ context.roof.trusses += trusses.length;
914
+ return { all: trusses, sideA: trusses, sideB: trusses };
915
+ }
916
+ function addGableRoof(context, unit, carriers, covering) {
917
+ const { builder, construction } = context;
918
+ const geometry = geometryOf(context, unit);
919
+ const frames = context.rafterRoof ? addRafters(context, unit, carriers, geometry) : coarseFrames(context, unit, carriers, geometry);
920
+ const panes = [];
921
+ panesOf(unit, geometry).forEach((aabb, k) => {
922
+ const name = unit.annex ? "annex" : k === 0 ? "a" : "b";
923
+ const options = { floor: unit.floor, fill: construction.roof.covering === "colonial" ? 0.12 : 0.05, ...construction.roof.pitch === void 0 ? {} : { pitch: construction.roof.pitch } };
924
+ const pane = builder.add({ id: `roof${unit.suffix}_${name}`, role: "roof", material: construction.roof.material, aabb, y0: geometry.eaveY, y1: geometry.eaveY + geometry.rise + 0.05 }, options);
925
+ const carried = unit.annex || !context.rafterRoof ? frames.all : k === 0 ? frames.sideA : frames.sideB;
926
+ for (const frame of carried) builder.rest(pane, frame, covering);
927
+ panes.push({ id: pane.id, aabb: pane.aabb, y0: pane.y0, y1: pane.y1 });
928
+ });
929
+ if (!unit.annex) {
930
+ const roof = context.roof;
931
+ roof.axis = geometry.axis;
932
+ roof.ridge = round2(geometry.eaveY + geometry.rise);
933
+ roof.eave = geometry.eave;
934
+ roof.y0 = round2(geometry.eaveY);
935
+ roof.aabb = [unit.box[0] - geometry.eave, unit.box[1] - geometry.eave, unit.box[2] + geometry.eave, unit.box[3] + geometry.eave].map(round2);
936
+ }
937
+ return panes;
938
+ }
939
+ function addCeilingJoists(context) {
940
+ const { builder, construction } = context;
941
+ const bearing = context.roof.bearingFloor;
942
+ const rects = context.rectsOf(bearing, CLOSED_ONLY);
943
+ const box = unionOf(rects.length ? rects : [context.topBox]);
944
+ const carriers = [...context.plates.get(bearing) ?? [], ...context.walls.filter((w) => w.floor === bearing && w.role === "wall" && !w.exterior).map((w) => context.wallParts.get(w.id))];
945
+ const length = box[2] - box[0];
946
+ const depth = box[3] - box[1];
947
+ const alongU = length >= depth;
948
+ const span = alongU ? length : depth;
949
+ const count = Math.max(2, Math.round(span / 0.6) + 1);
950
+ const beamTops = carriers.filter((carrier) => carrier.role === "beam").map((carrier) => carrier.y0);
951
+ const y0 = carriers.length ? Math.max(...beamTops, context.level(bearing) + 1) : context.topOf(bearing);
952
+ const joists = [];
953
+ for (let k = 0; k < count; k++) {
954
+ const at2 = (alongU ? box[0] : box[1]) + 0.05 + k * (span - 0.1) / (count - 1);
955
+ const aabb = alongU ? [at2 - 0.025, box[1], at2 + 0.025, box[3]] : [box[0], at2 - 0.025, box[2], at2 + 0.025];
956
+ const joist = builder.add({ id: `ceiling_joist_${k}`, role: "beam", material: construction.frame.timber, aabb, y0: y0 - 0.02, y1: y0 + context.system.plateThickness - 0.02 }, { floor: bearing, spacing: 0.6 });
957
+ const beams = carriers.filter((carrier) => carrier.role === "beam" && touches(aabb, carrier.aabb, 0.2));
958
+ for (const carrier of beams.length ? beams : carriers.slice(0, 2)) builder.rest(joist, carrier, "nail");
959
+ joists.push(joist);
960
+ }
961
+ context.ceilingJoists = joists;
962
+ }
963
+ function addRoof(context) {
964
+ const { construction } = context;
965
+ const covering = ROOF_COVERINGS.get(construction.roof.covering);
966
+ const bearing = construction.roof.bearingFloor ?? context.floorCount - 1;
967
+ const open = context.rectsOf(context.floorCount - 1, OPEN_ROOM_TYPES);
968
+ const topRects = context.rectsOf(context.floorCount - 1, CLOSED_ONLY);
969
+ const body = unionOf(open.length ? open : topRects);
970
+ context.topBox = context.floorCount - 1 === bearing ? body : unionOf(context.rectsOf(bearing, OPEN_ROOM_TYPES));
971
+ context.roof = { shape: construction.roof.shape, covering: construction.roof.covering, material: construction.roof.material, pitch: construction.roof.pitch || 0, bearingFloor: bearing, panes: [], trusses: 0 };
972
+ for (const unit of roofUnits(context, bearing)) {
973
+ const carriers = roofCarriers(context, unit);
974
+ if (construction.roof.shape === "slab") context.roof.panes.push(addFlatRoof(context, unit, carriers));
975
+ else context.roof.panes.push(...addGableRoof(context, unit, carriers, covering?.joint ?? "nail"));
976
+ }
977
+ if (context.isTimber && construction.roof.shape !== "slab") addCeilingJoists(context);
978
+ }
979
+
980
+ // src/structure/stairs.ts
981
+ function flightsOf(items) {
982
+ const flights = /* @__PURE__ */ new Map();
983
+ for (const item of items.filter((it) => it.category === "stairs" && it.link)) {
984
+ const key = `${item.link.from}_${item.link.to}`;
985
+ if (!flights.has(key) || item.floor === item.link.from) flights.set(key, item);
986
+ }
987
+ return flights;
988
+ }
989
+ function addStairs(context) {
990
+ const { builder, construction } = context;
991
+ const material = context.isTimber ? construction.frame.timber : construction.stairs ?? "concrete";
992
+ for (const [key, item2] of flightsOf(context.source.items)) {
993
+ const { from, to } = item2.link;
994
+ const flight2 = builder.add({ id: `stairs_${key}`, role: "stair", material, aabb: item2.aabb, y0: context.level(from), y1: context.level(to) }, { floor: from, steps: Math.round((context.level(to) - context.level(from)) / 0.18) });
995
+ const base = context.baseOf(from);
996
+ builder.rest(flight2, !base || base === GROUND ? GROUND : base, !base || base === GROUND ? "resting" : context.isTimber ? "nail" : "mortar");
997
+ const upper = context.baseOf(to);
998
+ if (upper && upper !== GROUND) builder.rest(flight2, upper, context.isTimber ? "nail" : "bolt");
999
+ }
1000
+ if (!context.hasBasement) return;
1001
+ const item = context.source.items.find((it) => it.type === "basement_stairs");
1002
+ if (!item?.aabb) return;
1003
+ const depth = construction.foundation.depth || 2.4;
1004
+ const flight = builder.add({ id: "basement_stairs", role: "stair", material: "wood_soft", aabb: item.aabb, y0: -depth, y1: context.level(0) }, { floor: -1, steps: Math.round((context.level(0) + depth) / 0.18) });
1005
+ builder.rest(flight, builder.find("basement_slab"), "resting");
1006
+ if (context.base0 && context.base0 !== GROUND) builder.rest(flight, context.base0, "nail");
1007
+ }
1008
+
1009
+ // src/structure/openings.ts
1010
+ var TOLERANCE = 0.02;
1011
+ var DOOR_HEIGHT = 2.1;
1012
+ var LINTEL_DEPTH = 0.2;
1013
+ var LINTEL_BEARING = 0.2;
1014
+ var crossOf = (wall) => wall.axis === "u" ? wall.v0 : wall.u0;
1015
+ 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)];
1016
+ function findOpenings(context) {
1017
+ const openings = [];
1018
+ for (const window of context.source.windows ?? []) {
1019
+ const from = window.at - window.width / 2;
1020
+ const to = window.at + window.width / 2;
1021
+ const wall = context.walls.find((w) => {
1022
+ if (w.floor !== window.floor || w.axis !== window.axis || w.type !== "wall" || Math.abs(crossOf(w) - window.pos) > TOLERANCE) return false;
1023
+ const [low, high] = alongRange(w);
1024
+ return from >= low - TOLERANCE && to <= high + TOLERANCE;
1025
+ });
1026
+ if (!wall) continue;
1027
+ const base = context.level(window.floor);
1028
+ openings.push({ id: window.id, kind: "window", wall: wall.id, from, to, y0: base + window.y0, y1: base + window.y1 });
1029
+ }
1030
+ return openings;
1031
+ }
1032
+ function addDoorLintels(context) {
1033
+ const { builder, construction, system } = context;
1034
+ for (const door of context.source.doors.filter((d) => d.a !== void 0 && d.type !== "zipper")) {
1035
+ const along = door.axis === "u" ? door.u : door.v;
1036
+ const cross = door.axis === "u" ? door.v : door.u;
1037
+ const from = along - door.width / 2;
1038
+ const to = along + door.width / 2;
1039
+ const beside = context.walls.filter((w) => w.floor === door.floor && w.axis === door.axis && w.type === "wall" && Math.abs(crossOf(w) - cross) <= TOLERANCE && (Math.abs(alongRange(w)[1] - from) <= 0.05 || Math.abs(alongRange(w)[0] - to) <= 0.05));
1040
+ if (!beside.length) continue;
1041
+ const half = (beside[0].thickness ?? 0.15) / 2;
1042
+ const lo = from - LINTEL_BEARING;
1043
+ const hi = to + LINTEL_BEARING;
1044
+ const box = door.axis === "u" ? [lo, cross - half, hi, cross + half] : [cross - half, lo, cross + half, hi];
1045
+ const y0 = context.level(door.floor) + DOOR_HEIGHT;
1046
+ const lintel = builder.add({ id: `lintel_${door.id}`, role: "beam", material: system.timber ? construction.frame.timber : "concrete", aabb: box, y0, y1: y0 + LINTEL_DEPTH }, { floor: door.floor, ...system.timber ? {} : { assembly: "reinforced_concrete" } });
1047
+ for (const wall of beside) builder.rest(lintel, context.wallParts.get(wall.id), system.joints.beam);
1048
+ for (const plate of context.plates.get(door.floor) ?? []) if (overlapsInPlan(plate.aabb, box)) builder.rest(plate, lintel, system.joints.beam);
1049
+ }
1050
+ }
1051
+ var overlapsInPlan = (a, b) => a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1];
1052
+
1053
+ // src/structure/steps.ts
1054
+ var RISER = 0.18;
1055
+ var TREAD = 0.28;
1056
+ var stepCount = (elevation) => Math.max(1, Math.ceil(elevation / RISER));
1057
+ function addPorchSteps(context) {
1058
+ const { builder } = context;
1059
+ for (const room of context.source.rooms.filter((r) => r.type === "porch")) {
1060
+ const deck = builder.find(`porch_floor_${room.id}`);
1061
+ for (const door of context.source.doors.filter((d) => d.floor === room.floor && d.b === "outside" && d.a === room.id)) {
1062
+ const count = stepCount(context.elevation);
1063
+ const depth = count * TREAD;
1064
+ const aabb = [door.u - door.width / 2, door.v - depth, door.u + door.width / 2, door.v];
1065
+ const flight = builder.add({ id: `steps_${door.id}`, role: "stair", material: context.isTimber ? "wood_soft" : "concrete", aabb, y0: 0, y1: context.level(room.floor) }, { floor: 0, steps: count, door: door.id });
1066
+ builder.rest(flight, GROUND, "resting");
1067
+ if (deck) builder.rest(flight, deck, context.isTimber ? "nail" : "mortar");
1068
+ }
1069
+ }
1070
+ }
1071
+ function boxOfSteps(door, depth) {
1072
+ const direction = door.axis === "u" ? door.side === "front" ? -1 : 1 : door.side === "left" ? -1 : 1;
1073
+ if (door.axis === "u") return [door.u - door.width / 2, Math.min(door.v, door.v + direction * depth), door.u + door.width / 2, Math.max(door.v, door.v + direction * depth)];
1074
+ return [Math.min(door.u, door.u + direction * depth), door.v - door.width / 2, Math.max(door.u, door.u + direction * depth), door.v + door.width / 2];
1075
+ }
1076
+ function addDoorSteps(context) {
1077
+ if (context.elevation <= 0.1) return;
1078
+ const { builder } = context;
1079
+ const doors = context.source.doors.filter((d) => d.floor === 0 && d.exterior && d.model !== "steel" && d.type !== "steps" && d.type !== "zipper");
1080
+ for (const door of doors) {
1081
+ const count = stepCount(context.elevation);
1082
+ const depth = Math.min(TREAD * count, 0.9);
1083
+ const flight = builder.add({ id: `steps_${door.id}`, role: "stair", material: context.isTimber ? "wood_soft" : "concrete", aabb: boxOfSteps(door, depth), y0: 0, y1: context.level(0) }, { floor: 0, steps: count, door: door.id });
1084
+ builder.rest(flight, GROUND, "resting");
1085
+ const base = context.base0;
1086
+ if (base && base !== GROUND) builder.rest(flight, base, context.isTimber ? "nail" : "mortar");
1087
+ }
1088
+ }
1089
+ function addExteriorSteps(context) {
1090
+ addPorchSteps(context);
1091
+ addDoorSteps(context);
1092
+ }
1093
+
1094
+ // src/structure/tank-parts.ts
1095
+ var TANK_FILL = 0.08;
1096
+ var squareAround = (u, v, side) => [u - side / 2, v - side / 2, u + side / 2, v + side / 2];
1097
+ function addTank(context, tank, floor, y0) {
1098
+ return context.builder.add({ id: "water_tank", role: "none", material: tank.material ?? "plastic", aabb: tank.aabb, y0, y1: y0 + tank.h }, { floor, fill: TANK_FILL, liters: tank.capacity });
1099
+ }
1100
+ function ensureCeilingSlab(context, y) {
1101
+ const { builder, construction, system } = context;
1102
+ const existing = builder.find("slab_ceiling");
1103
+ if (existing) return existing;
1104
+ const top = context.floorCount - 1;
1105
+ const box = context.topBox;
1106
+ const carriers = builder.parts.filter(
1107
+ (part) => (part.role === "beam" && part.id.startsWith(`plate_${top}_`) || part.role === "column" && part.floor === top || part.role === "wall" && part.floor === top && !part.id.startsWith("basement")) && touches(box, part.aabb, 0.1)
1108
+ );
1109
+ const slab = builder.add({ id: "slab_ceiling", role: "slab", material: construction.frame.slabs, aabb: box, y0: y - 0.12, y1: y }, { floor: top });
1110
+ for (const carrier of carriers) builder.rest(slab, carrier, system.joints.slab);
1111
+ return slab;
1112
+ }
1113
+ function addRaisedPlatform(context, tank) {
1114
+ const { builder, construction } = context;
1115
+ const platform = tank.platform;
1116
+ const top = context.floorCount - 1;
1117
+ const ceiling = ensureCeilingSlab(context, platform.baseY);
1118
+ const pillars = platform.pillars.map((q, k) => {
1119
+ const pillar = builder.add({ id: `tank_column_${k}`, role: "column", material: construction.frame.pillars, aabb: squareAround(q.u, q.v, q.side), y0: platform.baseY, y1: platform.topY - platform.thickness }, { floor: top });
1120
+ builder.rest(pillar, ceiling, "rebar");
1121
+ return pillar;
1122
+ });
1123
+ const slab = builder.add({ id: "tank_slab", role: "slab", material: construction.frame.slabs, aabb: platform.aabb, y0: platform.topY - platform.thickness, y1: platform.topY }, { floor: top });
1124
+ for (const pillar of pillars) builder.rest(slab, pillar, "rebar");
1125
+ builder.rest(addTank(context, tank, top, tank.y), slab, "resting");
1126
+ }
1127
+ function addAtticTank(context, tank) {
1128
+ const { builder } = context;
1129
+ const top = context.floorCount - 1;
1130
+ if (!context.isTimber) {
1131
+ const ceiling = ensureCeilingSlab(context, tank.y);
1132
+ builder.rest(addTank(context, tank, top, tank.y), ceiling, "resting");
1133
+ return;
1134
+ }
1135
+ const supports = context.topSlab ? [context.topSlab] : context.ceilingJoists ?? builder.parts.filter((part) => part.role === "beam" && part.id.startsWith(`plate_${top}_`));
1136
+ const y0 = supports[0] ? supports[0].y1 : tank.y - 0.03;
1137
+ const floor = builder.add({ id: "attic_floor", role: "slab", material: "plywood", aabb: context.topBox, y0, y1: y0 + 0.03 }, { floor: top });
1138
+ for (const support of supports) if (touches(floor.aabb, support.aabb, 0.1)) builder.rest(floor, support, "nail");
1139
+ builder.rest(addTank(context, tank, top, tank.y), floor, "resting");
1140
+ }
1141
+ function addSlabTank(context, tank) {
1142
+ const { builder } = context;
1143
+ const supports = builder.parts.filter((part2) => part2.role === "slab" && Math.abs(part2.y1 - tank.y) < 0.3 && touches(part2.aabb, tank.aabb, 0.1));
1144
+ const carrier = supports.sort((a, b) => Math.abs(a.y1 - tank.y) - Math.abs(b.y1 - tank.y))[0] ?? context.topSlab;
1145
+ const part = addTank(context, tank, tank.floor, tank.y);
1146
+ builder.rest(part, carrier ?? GROUND, "resting");
1147
+ }
1148
+ function addTrestleTank(context, tank) {
1149
+ const { builder } = context;
1150
+ const stand = tank.stand;
1151
+ const columns = stand.pillars.map((q, k) => {
1152
+ const column = builder.add({ id: `trestle_column_${k}`, role: "column", material: stand.material, aabb: squareAround(q.u, q.v, q.side), y0: 0, y1: stand.height }, { floor: 0 });
1153
+ builder.rest(column, GROUND, stand.baseJoint ?? "resting");
1154
+ return column;
1155
+ });
1156
+ const [u0, v0, u1, v1] = tank.aabb;
1157
+ const beams = [[u0 - 0.1, v0 - 0.1, u1 + 0.1, v0 + 0.05], [u0 - 0.1, v1 - 0.05, u1 + 0.1, v1 + 0.1]].map((aabb, k) => {
1158
+ const beam = builder.add({ id: `trestle_beam_${k}`, role: "beam", material: stand.material, aabb, y0: stand.height - 0.12, y1: stand.height }, { floor: 0 });
1159
+ for (const column of columns.filter((c) => touches(beam.aabb, c.aabb, 0.15))) builder.rest(beam, column, stand.joint ?? "nail");
1160
+ return beam;
1161
+ });
1162
+ const part = addTank(context, tank, 0, stand.height);
1163
+ for (const beam of beams) builder.rest(part, beam, stand.tie ?? "resting");
1164
+ }
1165
+ function addWaterTank(context) {
1166
+ const tank = context.tank;
1167
+ if (!tank) return;
1168
+ switch (tank.place) {
1169
+ case "slab":
1170
+ return addSlabTank(context, tank);
1171
+ case "raised_slab":
1172
+ return addRaisedPlatform(context, tank);
1173
+ case "attic":
1174
+ return addAtticTank(context, tank);
1175
+ case "trestle":
1176
+ return addTrestleTank(context, tank);
1177
+ }
1178
+ }
1179
+
1180
+ // src/structure/upper-floor.ts
1181
+ function addUpperFloor(context, floor) {
1182
+ if (floor >= context.floorCount - 1 || context.isUnit) return;
1183
+ const next = floor + 1;
1184
+ const rects = context.rectsOf(next, ["balcony", "porch", "void"]);
1185
+ const box = rects.length ? unionOf(rects) : context.houseBox;
1186
+ const carriers = context.carriers.get(floor);
1187
+ const { system, construction, builder } = context;
1188
+ const thickness = system.slabThickness;
1189
+ if (context.isTimber) {
1190
+ const joists = addJoists(context, { floor: next, box, thickness, carriers, material: construction.frame.timber, joint: system.joints.beam });
1191
+ addDeck(context, `slab_${next}`, construction.frame.slabs, { floor: next, box, thickness, joint: system.joints.slab }, joists);
1192
+ return;
1193
+ }
1194
+ const slab = builder.add({ id: `slab_${next}`, role: "slab", material: construction.frame.slabs, aabb: box, y0: context.level(next) - thickness, y1: context.level(next) }, { floor: next });
1195
+ const below = carriers.filter((carrier) => carrier.y0 <= slab.y0 + 0.05);
1196
+ const touching = below.filter((carrier) => touches(slab.aabb, carrier.aabb, 0.2));
1197
+ for (const carrier of touching.length ? touching : below) builder.rest(slab, carrier, system.joints.slab);
1198
+ }
1199
+
1200
+ // src/structure/walls.ts
1201
+ function wallCeiling(context, wall) {
1202
+ const rooms = wall.sides.map((id) => context.source.rooms.find((room) => room.id === id && room.floor === wall.floor)).filter((room) => room !== void 0);
1203
+ return Math.max(context.ceilingOf(wall.floor), ...rooms.map((room) => room.ceiling || 0));
1204
+ }
1205
+ function wallTop(context, wall, floor) {
1206
+ const y0 = context.level(floor);
1207
+ if (wall.type === "railing") return y0 + guardHeightOf(wall);
1208
+ const ceiling = wallCeiling(context, wall);
1209
+ const height = wall.structureHeight || wall.h || context.ceilingOf(floor);
1210
+ if (wall.exterior && floor < context.floorCount - 1 && !context.isUnit && ceiling > context.ceilingOf(floor) + 0.01) return y0 + ceiling;
1211
+ if (floor === context.floorCount - 1 || context.isUnit) return y0 + Math.max(height, ceiling);
1212
+ const top = context.topOf(floor);
1213
+ return Math.max(y0 + height, y0 + ceiling > top ? top : y0 + ceiling);
1214
+ }
1215
+ function railingBase(context, wall, floor) {
1216
+ const open = wall.sides.map((id) => context.source.rooms.find((room) => room.id === id && room.floor === floor)).find((room) => room && (room.type === "balcony" || room.type === "porch"));
1217
+ if (!open) return context.baseOf(floor);
1218
+ return open.type === "porch" ? `porch_floor_${open.id}` : `slab_${floor}_${open.id}`;
1219
+ }
1220
+ var assemblyOf = (context) => context.construction.walls.assembly ?? (context.system.walls || void 0);
1221
+ function addWallPart(context, wall, floor) {
1222
+ const { system, builder } = context;
1223
+ const isRailing = wall.type === "railing";
1224
+ const y0 = context.level(floor);
1225
+ const top = wallTop(context, wall, floor);
1226
+ const y1 = isRailing ? top : wall.exterior && !wall.gable ? top - system.plateThickness : top;
1227
+ const framed = system.studs !== void 0 && !isRailing;
1228
+ const part = builder.add(
1229
+ { id: wall.id, role: wall.role, material: wall.material, aabb: wall.box, y0, y1: Math.max(y1, y0 + 0.2) },
1230
+ { floor, fill: context.isTimber && wall.role === "wall" ? system.wallFill : 1, ...framed ? { studs: system.studs } : {}, ...isRailing ? {} : { assembly: assemblyOf(context) } }
1231
+ );
1232
+ context.wallParts.set(wall.id, part);
1233
+ const base = isRailing ? railingBase(context, wall, floor) : context.baseOf(floor);
1234
+ const joint = isRailing ? "nail" : system.joints.wall;
1235
+ if (base === GROUND || floor === 0 && !base) builder.rest(part, GROUND, joint);
1236
+ else if (base) builder.rest(part, base, joint);
1237
+ }
1238
+ function addWalls(context, floor) {
1239
+ for (const wall of context.walls.filter((w) => w.floor === floor)) addWallPart(context, wall, floor);
1240
+ }
1241
+
1242
+ // src/structure/index.ts
1243
+ function buildFloors(context) {
1244
+ for (let floor = 0; floor < context.floorCount; floor++) {
1245
+ addWalls(context, floor);
1246
+ addPlatesAndPillars(context, floor);
1247
+ addPorches(context, floor);
1248
+ addUpperFloor(context, floor);
1249
+ }
1250
+ }
1251
+ function buildStructure(source, options = {}) {
1252
+ const refined = options.wallDetail !== "coarse";
1253
+ const built = options.construction ? { construction: options.construction, extras: options.extras ?? {} } : resolveConstruction(source);
1254
+ const construction = built.construction;
1255
+ const extras = options.extras ?? built.extras ?? {};
1256
+ if (construction.system === "canvas") return buildCanvasStructure(construction, extras);
1257
+ const tank = options.tank !== void 0 ? options.tank : source.waterTank ?? null;
1258
+ const context = createContext(source, construction, extras, tank, { porchRoofOnPostsOnly: options.porchRoofOnPostsOnly ?? refined, rafterRoof: options.rafterRoof ?? refined });
1259
+ addFoundation(context);
1260
+ buildFloors(context);
1261
+ addBalconies(context);
1262
+ addPorchRoofs(context);
1263
+ addExteriorSteps(context);
1264
+ addStairs(context);
1265
+ addRoof(context);
1266
+ if (refined) addDoorLintels(context);
1267
+ addWaterTank(context);
1268
+ addChimney(context);
1269
+ addWell(context);
1270
+ const levels = source.floors.map((_, floor) => context.level(floor));
1271
+ const framed = construction.system === "masonry" && (context.isUnit || source.style === "commercial" || context.floorCount >= 3);
1272
+ return { parts: context.builder.parts, supports: context.builder.supports, levels, elevation: context.elevation, roof: context.roof, construction, framed, openings: findOpenings(context) };
1273
+ }
1274
+
1275
+ // src/utilities/content-tables.ts
1276
+ var entry = (itemId, min, max, group, pickable, weight) => ({ itemId, min, max, group, pickable, weight });
1277
+ var FRIDGE = [
1278
+ entry("water_bottle", 1, 3, "water", true, 3),
1279
+ entry("milk_carton", 1, 2, "food", true, 2),
1280
+ entry("eggs", 1, 1, "food", true, 2),
1281
+ entry("packed_meat", 1, 2, "food", true, 1.5),
1282
+ entry("fruit", 1, 4, "food", true, 2),
1283
+ entry("yogurt", 1, 3, "food", true, 1),
1284
+ entry("soda", 1, 2, "water", true, 1),
1285
+ entry("cheese", 1, 1, "food", true, 1),
1286
+ entry("insulin", 1, 1, "medicine", true, 0.4),
1287
+ entry("leftovers", 1, 2, "food", true, 1)
1288
+ ];
1289
+ var BEDSIDE = [entry("medicine", 1, 2, "medicine", true, 2), entry("book", 1, 2, "document", true, 1.5), entry("charger", 1, 1, "tool", true, 1), entry("flashlight", 1, 1, "tool", true, 0.7), entry("glasses", 1, 1, "tool", true, 0.7)];
1290
+ var WARDROBE = [
1291
+ entry("shirt", 2, 6, "clothes", true, 3),
1292
+ entry("trousers", 1, 4, "clothes", true, 3),
1293
+ entry("coat", 1, 2, "clothes", true, 1.5),
1294
+ entry("sneakers", 1, 2, "clothes", true, 1.5),
1295
+ entry("shoes", 1, 2, "clothes", true, 1),
1296
+ entry("blanket", 1, 2, "clothes", true, 1.5),
1297
+ entry("backpack", 1, 1, "tool", true, 0.8)
1298
+ ];
1299
+ var DRESSER = [entry("underwear", 2, 6, "clothes", true, 3), entry("socks", 2, 6, "clothes", true, 2), entry("t_shirt", 1, 4, "clothes", true, 2), entry("documents", 1, 2, "document", true, 1), entry("flashlight", 1, 1, "tool", true, 0.7), entry("batteries", 1, 2, "tool", true, 0.7)];
1300
+ var FILING = [entry("documents", 1, 4, "document", true, 3), entry("pen", 1, 3, "tool", true, 1.5), entry("keys", 1, 2, "tool", true, 1), entry("stapler", 1, 1, "improvised_weapon", true, 0.5)];
1301
+ var TRASH = [entry("full_trash_bag", 1, 1, "trash", false, 2), entry("packaging", 1, 4, "trash", true, 3), entry("empty_can", 1, 3, "trash", true, 2), entry("crumpled_paper", 1, 3, "trash", true, 1)];
1302
+ var CONTENT_TABLES = {
1303
+ fridge: FRIDGE,
1304
+ display_fridge: [entry("soda", 2, 6, "water", true, 3), entry("water_bottle", 2, 6, "water", true, 3), entry("yogurt", 2, 5, "food", true, 2)],
1305
+ freezer: [entry("frozen_meat", 2, 5, "food", true, 3), entry("ice_cream", 1, 2, "food", true, 1)],
1306
+ kitchen_cabinet: [
1307
+ entry("pot", 1, 2, "tool", true, 1.5),
1308
+ entry("kitchen_knife", 1, 2, "improvised_weapon", true, 1.5),
1309
+ entry("dish_soap", 1, 2, "cleaning", true, 2),
1310
+ entry("sponge", 1, 2, "cleaning", true, 1),
1311
+ entry("rice", 1, 2, "food", true, 1.5),
1312
+ entry("beans", 1, 2, "food", true, 1.5),
1313
+ entry("pasta", 1, 2, "food", true, 1),
1314
+ entry("cooking_oil", 1, 1, "food", true, 1),
1315
+ entry("canned_tuna", 1, 3, "food", true, 1.5),
1316
+ entry("disinfectant", 1, 1, "cleaning", true, 1),
1317
+ entry("trash_bag", 1, 2, "trash", true, 0.7)
1318
+ ],
1319
+ overhead_cabinet: [entry("plates", 4, 8, "utensil", false, 3), entry("glasses", 3, 6, "utensil", false, 3), entry("mug", 1, 3, "utensil", true, 1.5), entry("coffee", 1, 2, "food", true, 1.5), entry("canned_corn", 1, 3, "food", true, 1.5), entry("spices", 1, 3, "food", true, 1)],
1320
+ washbasin: [
1321
+ entry("painkiller", 1, 2, "medicine", true, 2),
1322
+ entry("bandages", 1, 2, "medicine", true, 2),
1323
+ entry("soap", 1, 3, "cleaning", true, 2),
1324
+ entry("toilet_paper", 1, 4, "cleaning", true, 2),
1325
+ entry("shampoo", 1, 2, "cleaning", true, 1),
1326
+ entry("toothbrush", 1, 2, "cleaning", true, 1),
1327
+ entry("scissors", 1, 1, "improvised_weapon", true, 0.5),
1328
+ entry("rubbing_alcohol", 1, 1, "medicine", true, 1)
1329
+ ],
1330
+ wardrobe: WARDROBE,
1331
+ wardrobe_large: WARDROBE,
1332
+ dresser: DRESSER,
1333
+ console_table: DRESSER,
1334
+ nightstand: BEDSIDE,
1335
+ filing_cabinet: FILING,
1336
+ pantry: [
1337
+ entry("rice", 2, 4, "food", true, 2),
1338
+ entry("beans", 2, 4, "food", true, 2),
1339
+ entry("canned_tuna", 2, 5, "food", true, 2),
1340
+ entry("water_jug", 1, 2, "water", true, 2),
1341
+ entry("flour", 1, 2, "food", true, 1),
1342
+ entry("sugar", 1, 2, "food", true, 1),
1343
+ entry("cooking_oil", 1, 2, "food", true, 1),
1344
+ entry("biscuits", 1, 4, "food", true, 1.5)
1345
+ ],
1346
+ tv_unit: [entry("remote_control", 1, 1, "tool", true, 2), entry("dvds", 1, 4, "document", true, 1), entry("cables", 1, 3, "tool", true, 1.5), entry("batteries", 1, 2, "tool", true, 1)],
1347
+ laundry_cabinet: [entry("washing_powder", 1, 2, "cleaning", true, 3), entry("bleach", 1, 2, "cleaning", true, 2), entry("disinfectant", 1, 2, "cleaning", true, 2), entry("bucket", 1, 1, "tool", true, 1), entry("broom", 1, 1, "improvised_weapon", true, 1), entry("rubber_gloves", 1, 2, "cleaning", true, 1)],
1348
+ laundry_shelf: [entry("washing_powder", 1, 1, "cleaning", true, 2), entry("softener", 1, 2, "cleaning", true, 2), entry("clothespins", 1, 1, "tool", true, 1)],
1349
+ workbench: [entry("hammer", 1, 1, "improvised_weapon", true, 2), entry("screwdriver", 1, 2, "tool", true, 2), entry("pliers", 1, 1, "tool", true, 1.5), entry("insulating_tape", 1, 2, "tool", true, 1), entry("screws", 1, 2, "tool", true, 1)],
1350
+ tool_shelf: [entry("hammer", 1, 1, "improvised_weapon", true, 1.5), entry("hand_saw", 1, 1, "improvised_weapon", true, 1), entry("paint_can", 1, 3, "trash", true, 1.5), entry("rope", 1, 1, "tool", true, 1), entry("nails", 1, 2, "tool", true, 1.5)],
1351
+ mailbox: [entry("letter", 1, 4, "document", true, 3), entry("bill", 1, 2, "document", true, 2), entry("flyer", 1, 4, "trash", true, 2)],
1352
+ trash_bin: TRASH,
1353
+ outdoor_bin: TRASH,
1354
+ china_cabinet: [entry("china_plates", 3, 6, "utensil", false, 3), entry("wine_glasses", 2, 5, "utensil", false, 2)]
1355
+ };
1356
+ var SUPPLY_GROUPS = ["food", "water", "medicine", "tool"];
1357
+
1358
+ // src/utilities/content.ts
1359
+ var FRIDGES = ["fridge", "display_fridge", "freezer"];
1360
+ var isSupply = (entry2) => SUPPLY_GROUPS.includes(entry2.group);
1361
+ var CONTENT_STATES = {
1362
+ normal: { kinds: (base) => base, weight: (entry2) => entry2.weight, extraQuantity: () => 0 },
1363
+ fortified: { kinds: (base, available) => Math.min(available, base + 2), weight: (entry2) => isSupply(entry2) ? entry2.weight * 2.5 : entry2.weight, extraQuantity: (entry2) => isSupply(entry2) ? 1 : 0 },
1364
+ abandoned: {
1365
+ kinds: (_base, _available, rng) => rng() < 0.4 ? 0 : 1,
1366
+ weight: (entry2) => entry2.weight,
1367
+ extraQuantity: () => 0,
1368
+ leftovers(found, container, rng) {
1369
+ if (found.length === 0 && rng() < 0.5) found.push({ itemId: "scraps_and_papers", count: 1, pickable: false, group: "trash" });
1370
+ if (FRIDGES.includes(container)) found.push({ itemId: "spoiled_food", count: 1, pickable: false, group: "trash" });
1371
+ }
1372
+ }
1373
+ };
1374
+ function weightedOrder(candidates, rng) {
1375
+ const pool = candidates.slice();
1376
+ const out = [];
1377
+ while (pool.length) {
1378
+ let x = rng() * pool.reduce((sum, c) => sum + c.weight, 0);
1379
+ let i = 0;
1380
+ for (; i < pool.length - 1; i++) {
1381
+ x -= pool[i].weight;
1382
+ if (x < 0) break;
1383
+ }
1384
+ out.push(pool.splice(i, 1)[0]);
1385
+ }
1386
+ return out;
1387
+ }
1388
+ function contentOf(query) {
1389
+ const pool = CONTENT_TABLES[query.container];
1390
+ if (!pool) return [];
1391
+ const rule = CONTENT_STATES[query.state] ?? CONTENT_STATES.normal;
1392
+ const { rng } = query;
1393
+ const base = Math.max(2, Math.min(5, Math.round(1.5 + query.footprint * 3 + rng() * 1.5)));
1394
+ const kinds = rule.kinds(base, pool.length, rng);
1395
+ const found = [];
1396
+ for (const candidate of weightedOrder(pool.map((entry2) => ({ entry: entry2, weight: rule.weight(entry2) })), rng).slice(0, kinds)) {
1397
+ const { entry: entry2 } = candidate;
1398
+ const count = entry2.min + Math.floor(rng() * (entry2.max - entry2.min + 1)) + rule.extraQuantity(entry2);
1399
+ found.push({ itemId: entry2.itemId, count, pickable: entry2.pickable, group: entry2.group });
1400
+ }
1401
+ rule.leftovers?.(found, query.container, rng);
1402
+ return found;
1403
+ }
1404
+ var hasContent = (container) => container in CONTENT_TABLES;
1405
+
1406
+ // src/utilities/shared.ts
1407
+ var HALF_WALL = 0.075;
1408
+ var round22 = (value) => Math.round(value * 100) / 100;
1409
+ var clamp = (value, low, high) => Math.max(low, Math.min(high, value));
1410
+ var overlaps = (a, b, epsilon = 1e-6) => a[0] < b[2] - epsilon && a[2] > b[0] + epsilon && a[1] < b[3] - epsilon && a[3] > b[1] + epsilon;
1411
+ var inner = (rect) => [rect[0] + HALF_WALL, rect[1] + HALF_WALL, rect[2] - HALF_WALL, rect[3] - HALF_WALL];
1412
+ var isSolid = (item) => !item.flat && !item.on && (!item.decor || item.mount === "floor");
1413
+ var distanceToRect = (u, v, rect) => Math.hypot(Math.max(rect[0] - u, 0, u - rect[2]), Math.max(rect[1] - v, 0, v - rect[3]));
1414
+ var NO_SOCKET_ROOMS = ["void", "elevator", "stairs", "balcony", "terrace", "porch", "tent"];
1415
+ var outwardOf = (wall) => {
1416
+ const sign = wall.sides[0] === "outside" ? -1 : 1;
1417
+ return wall.axis === "u" ? [0, sign] : [sign, 0];
1418
+ };
1419
+ var sideNameOf = (vector) => Math.abs(vector[0]) > Math.abs(vector[1]) ? vector[0] > 0 ? "right" : "left" : vector[1] > 0 ? "back" : "front";
1420
+ function groundExteriorWalls(plan) {
1421
+ return plan.walls.filter((wall) => wall.exterior && wall.floor === 0 && wall.type === "wall" && Math.hypot(wall.u1 - wall.u0, wall.v1 - wall.v0) >= 1);
1422
+ }
1423
+
1424
+ // src/utilities/wall-points.ts
1425
+ var STEP = 0.05;
1426
+ var SURFACE_OFFSET = 0.012;
1427
+ function wallPointsOf(plan, room, items, margin = 0.3) {
1428
+ const points = [];
1429
+ const windows = plan.windows.filter((w) => w.floor === room.floor);
1430
+ const solids = items.filter((m) => m.floor === room.floor && !m.outdoor && m.room === room.id && isSolid(m));
1431
+ for (const wall of plan.walls) {
1432
+ if (wall.floor !== room.floor || wall.type !== "wall" || !wall.sides.includes(room.id)) continue;
1433
+ const side = wall.sides[1] === room.id ? 1 : -1;
1434
+ const a0 = wall.axis === "u" ? Math.min(wall.u0, wall.u1) : Math.min(wall.v0, wall.v1);
1435
+ const a1 = wall.axis === "u" ? Math.max(wall.u0, wall.u1) : Math.max(wall.v0, wall.v1);
1436
+ const pos = wall.axis === "u" ? wall.v0 : wall.u0;
1437
+ const nearby = windows.filter((w) => w.axis === wall.axis && Math.abs(w.pos - pos) < 1e-3 && w.at + w.width / 2 > a0 && w.at - w.width / 2 < a1);
1438
+ for (let c = a0 + margin; c <= a1 - margin + 1e-9; c += STEP) {
1439
+ const u = wall.axis === "u" ? c : pos + side * (HALF_WALL + SURFACE_OFFSET);
1440
+ const v = wall.axis === "u" ? pos + side * (HALF_WALL + SURFACE_OFFSET) : c;
1441
+ points.push({
1442
+ u: round22(u),
1443
+ v: round22(v),
1444
+ c: round22(c),
1445
+ axis: wall.axis,
1446
+ pos,
1447
+ side,
1448
+ nu: wall.axis === "u" ? 0 : side,
1449
+ nv: wall.axis === "u" ? side : 0,
1450
+ a0,
1451
+ a1,
1452
+ exterior: wall.exterior,
1453
+ windows: nearby,
1454
+ blocked: (height) => solids.some((m) => m.aabb && Math.hypot(Math.max(m.aabb[0] - u, 0, u - m.aabb[2]), Math.max(m.aabb[1] - v, 0, v - m.aabb[3])) < 0.12 && (m.y || 0) + m.h > height)
1455
+ });
1456
+ }
1457
+ }
1458
+ return points;
1459
+ }
1460
+ var hasWindowAt = (point, y, clearance = 0.2) => point.windows.some((w) => y + 0.1 > w.y0 - 0.05 && y - 0.1 < w.y1 + 0.05 && Math.abs(point.c - w.at) < w.width / 2 + clearance);
1461
+ var atDoor = (plan, room, point, clearance) => plan.doors.some((d) => d.floor === room.floor && d.axis === point.axis && Math.abs(d.pos - point.pos) < 1e-3 && Math.abs(d.at - point.c) < d.width / 2 + clearance);
1462
+
1463
+ // src/utilities/electrical-outlets.ts
1464
+ var APPLIANCES = ["fridge", "display_fridge", "freezer", "tv", "appliance", "washing_machine", "air_conditioner", "cash_register", "table_light"];
1465
+ var COUNTERS = ["kitchen_cabinet", "workbench", "checkout_counter"];
1466
+ var LAMPS = ["table_light", "lamp"];
1467
+ var needsSocket = (item) => APPLIANCES.includes(item.category) || item.category === "shower";
1468
+ function addOutlets(layout) {
1469
+ const { plan, items, rooms, electrical, pointsOf } = layout;
1470
+ let counter = 0;
1471
+ const outletsOf = (room) => electrical.outlets.filter((t) => t.room === room.id && t.floor === room.floor);
1472
+ const newOutlet = (room, point, y) => {
1473
+ const outlet = { id: `outlet${++counter}`, room: room.id, floor: room.floor, u: point.u, v: point.v, y, c: point.c, axis: point.axis, pos: point.pos, frontU: point.nu, frontV: point.nv, type: "2P+T", voltage: 127 };
1474
+ electrical.outlets.push(outlet);
1475
+ return outlet;
1476
+ };
1477
+ const counters = items.filter((m) => COUNTERS.includes(m.category) && !m.outdoor);
1478
+ const heightFor = (room, point) => room.type === "kitchen" && counters.some((m) => m.floor === room.floor && m.room === room.id && distanceToRect(point.u, point.v, m.aabb) < 0.45) ? 1.1 : 0.3;
1479
+ const valid = (room, point, y) => !hasWindowAt(point, y, 0.25) && !atDoor(plan, room, point, 0.45) && !outletsOf(room).some((t) => Math.hypot(t.u - point.u, t.v - point.v) < 0.5 && t.y === y);
1480
+ const pick = (room, score, fixedY, ignoreBlocked) => {
1481
+ let best = null;
1482
+ for (const point of pointsOf.get(keyOf(room)) ?? []) {
1483
+ const y = fixedY ?? heightFor(room, point);
1484
+ if (!valid(room, point, y)) continue;
1485
+ const total = score(point) + (point.blocked(y) && !ignoreBlocked ? 5 : 0);
1486
+ if (!best || total < best.score) best = { point, y, score: total };
1487
+ }
1488
+ return best;
1489
+ };
1490
+ const roomOf = (item) => rooms.find((x) => x.id === item.room && x.floor === item.floor);
1491
+ const appliances = items.filter((m) => !m.outdoor && needsSocket(m) && rooms.some((c) => c.id === m.room && c.floor === m.floor) && m.category !== "table_light");
1492
+ for (const item of appliances) connectAppliance(item);
1493
+ function connectAppliance(item) {
1494
+ const room = roomOf(item);
1495
+ const target = item.aabb;
1496
+ const nearest = outletsOf(room).map((t) => ({ t, d: distanceToRect(t.u, t.v, target) })).filter((o) => o.d <= 1.2 && o.t.y <= 1.1).sort((a, b) => a.d - b.d)[0];
1497
+ const isShower = item.category === "shower";
1498
+ let outlet = nearest && !isShower ? nearest.t : null;
1499
+ if (!outlet) {
1500
+ const height = isShower ? 1.1 : item.y > 0.7 ? 1.1 : null;
1501
+ const best = pick(room, (p) => {
1502
+ const d = distanceToRect(p.u, p.v, target);
1503
+ return isShower && d < 0.6 ? 50 + d : d;
1504
+ }, height);
1505
+ let chosen = best;
1506
+ if (!isShower && (!best || distanceToRect(best.point.u, best.point.v, target) > 1.4)) {
1507
+ const behind = pick(room, (p) => distanceToRect(p.u, p.v, target), item.y > 0.7 ? 1.1 : null, true);
1508
+ if (behind && (!best || distanceToRect(behind.point.u, behind.point.v, target) < distanceToRect(best.point.u, best.point.v, target))) chosen = behind;
1509
+ }
1510
+ if (chosen) outlet = newOutlet(room, chosen.point, chosen.y);
1511
+ }
1512
+ if (!outlet) return;
1513
+ item.outlet = outlet.id;
1514
+ if (isShower) item.showerHeater = { electric: true, powerW: 5500, outlet: outlet.id };
1515
+ }
1516
+ for (const room of rooms) {
1517
+ const area = room.parts.reduce((sum, q) => sum + (q[2] - q[0]) * (q[3] - q[1]), 0);
1518
+ const wanted = Math.max(room.type === "kitchen" ? 3 : 2, Math.min(6, Math.round(area / 7)));
1519
+ const farthest = (existing) => (p) => -Math.min(9, ...existing.map((t) => Math.hypot(t.u - p.u, t.v - p.v)));
1520
+ for (let guard = 0; outletsOf(room).length < wanted && guard < 12; guard++) {
1521
+ const best = pick(room, farthest(outletsOf(room)), null);
1522
+ if (!best) break;
1523
+ newOutlet(room, best.point, best.y);
1524
+ }
1525
+ if (room.type !== "kitchen" || !counters.some((m) => m.room === room.id)) continue;
1526
+ for (let guard = 0; outletsOf(room).filter((t) => t.y === 1.1).length < 2 && guard < 4; guard++) {
1527
+ const existing = outletsOf(room);
1528
+ const best = pick(room, (p) => (heightFor(room, p) === 1.1 ? 0 : 20) - Math.min(9, ...existing.map((t) => Math.hypot(t.u - p.u, t.v - p.v))), 1.1);
1529
+ if (!best) break;
1530
+ newOutlet(room, best.point, 1.1);
1531
+ }
1532
+ }
1533
+ for (const item of appliances) {
1534
+ if (item.category === "shower" || !item.outlet) continue;
1535
+ const room = roomOf(item);
1536
+ const current = outletsOf(room).find((t) => t.id === item.outlet);
1537
+ if (!current || distanceToRect(current.u, current.v, item.aabb) <= 1.2) continue;
1538
+ const other = outletsOf(room).filter((t) => t.y <= 1.1).map((t) => ({ t, d: distanceToRect(t.u, t.v, item.aabb) })).sort((a, b) => a.d - b.d)[0];
1539
+ if (other && other.d < distanceToRect(current.u, current.v, item.aabb) && other.d <= 1.2) item.outlet = other.t.id;
1540
+ }
1541
+ for (const item of items.filter((m) => LAMPS.includes(m.category) && !m.outdoor)) {
1542
+ const room = roomOf(item);
1543
+ if (!room) continue;
1544
+ const nearest = outletsOf(room).map((t) => ({ t, d: distanceToRect(t.u, t.v, item.aabb) })).sort((a, b) => a.d - b.d)[0];
1545
+ if (nearest) item.outlet = nearest.t.id;
1546
+ }
1547
+ }
1548
+
1549
+ // src/utilities/electrical-entrance.ts
1550
+ var METER_SIZE = { w: 0.35, d: 0.2, h: 0.5 };
1551
+ var mainDoorOf = (plan) => plan.doors.find((p) => p.floor === 0 && p.exterior && p.side === "front") || plan.doors.find((p) => p.floor === 0 && p.exterior);
1552
+ function placeEntrance(layout) {
1553
+ const { plan, electrical } = layout;
1554
+ const gate = plan.gates.find((g) => g.type === "pedestrian");
1555
+ const door = mainDoorOf(plan);
1556
+ const lot = plan.lot;
1557
+ if (lot && plan.style !== "commercial") {
1558
+ const rect = lot.rect;
1559
+ const poleU = clamp(gate ? gate.u + 3 : plan.w / 2, rect[0] + 0.5, rect[2] - 0.5);
1560
+ const poleV = rect[1] - 0.6;
1561
+ let meter;
1562
+ if (gate) {
1563
+ const direction = gate.u - gate.width / 2 - 0.4 >= rect[0] + 0.3 ? -1 : 1;
1564
+ meter = { place: "gate_pillar", u: round22(gate.u + direction * (gate.width / 2 + 0.25)), v: round22(gate.v), y: 1.4, side: "front", ...METER_SIZE };
1565
+ } else meter = { place: "facade", u: round22(door ? door.at + 1.2 : plan.w / 2), v: -0.1, y: 1.4, side: "front", ...METER_SIZE };
1566
+ meter.id = "meter";
1567
+ electrical.serviceEntrance = { pole: { u: round22(poleU), v: round22(poleV), h: 7.5 }, drop: [[round22(poleU), round22(poleV), 6.5], [meter.u, meter.v, 2.6]], meter, descent: [[meter.u, meter.v, 2.6], [meter.u, meter.v, 1.65]], internalRun: null };
1568
+ } else if (lot === null || plan.style === "commercial") {
1569
+ const front = door ? { u: round22(door.at + 1), v: -0.1 } : { u: round22(plan.w / 2), v: -0.1 };
1570
+ electrical.serviceEntrance = {
1571
+ pole: { u: round22(front.u + 2), v: -3, h: 7.5 },
1572
+ drop: [[round22(front.u + 2), -3, 6.5], [front.u, front.v, 3.2]],
1573
+ meter: { id: "meter", place: "facade", u: front.u, v: front.v, y: 1.4, side: "front", ...METER_SIZE },
1574
+ descent: [[front.u, front.v, 3.2], [front.u, front.v, 1.65]],
1575
+ internalRun: null
1576
+ };
1577
+ }
1578
+ }
1579
+ function placePanel(layout) {
1580
+ const { plan, rooms, electrical, pointsOf } = layout;
1581
+ const door = mainDoorOf(plan);
1582
+ const room = door && rooms.find((c) => c.id === door.a && c.floor === 0) || rooms.find((c) => c.floor === 0) || rooms[0];
1583
+ if (!room) return;
1584
+ const meter = electrical.serviceEntrance?.meter;
1585
+ const targetU = meter ? meter.u : plan.w / 2;
1586
+ const targetV = meter ? Math.max(0, meter.v) : 0;
1587
+ let best = null;
1588
+ for (const point of pointsOf.get(keyOf(room)) ?? []) {
1589
+ if (point.blocked(1.5) || hasWindowAt(point, 1.5, 0.3) || atDoor(plan, room, point, 0.8)) continue;
1590
+ const score = Math.hypot(point.u - targetU, point.v - targetV) + (point.exterior ? 0 : 3);
1591
+ if (!best || score < best.score) best = { point, score };
1592
+ }
1593
+ if (!best) best = { point: (pointsOf.get(keyOf(room)) ?? [])[0], score: 0 };
1594
+ if (best.point) {
1595
+ const p = best.point;
1596
+ electrical.panel = { id: "panel", room: room.id, floor: room.floor, u: p.u, v: p.v, y: 1.5, c: p.c, axis: p.axis, pos: p.pos, frontU: p.nu, frontV: p.nv, w: 0.3, h: 0.4, breakers: 0 };
1597
+ }
1598
+ if (electrical.serviceEntrance && electrical.panel) {
1599
+ const entrance = electrical.serviceEntrance;
1600
+ entrance.internalRun = [[entrance.meter.u, entrance.meter.v, 0.1], [electrical.panel.u, electrical.panel.v, heightOf(plan, electrical.panel.floor, electrical.panel.y)]];
1601
+ }
1602
+ }
1603
+
1604
+ // src/utilities/electrical-circuits.ts
1605
+ var LAMP_CATEGORIES = ["ceiling_light", "lamp", "table_light"];
1606
+ var lampsOf = (layout) => layout.items.filter((m) => !m.outdoor && LAMP_CATEGORIES.includes(m.category));
1607
+ function addSwitches(layout) {
1608
+ const { plan, items, rooms, electrical, pointsOf } = layout;
1609
+ const lamps = lampsOf(layout);
1610
+ const door = mainDoorOf(plan);
1611
+ let counter = 0;
1612
+ const positionFor = (room) => {
1613
+ const doors = plan.doors.filter((p) => p.floor === room.floor && (p.a === room.id || p.b === room.id)).sort((a, b) => (a.opensTo === room.id ? 0 : 1) - (b.opensTo === room.id ? 0 : 1) || (a.width < b.width ? 1 : -1));
1614
+ const points = wallPointsOf(plan, room, items, 0.12);
1615
+ for (const tolerance of [0.16, 0.3]) {
1616
+ for (const d of doors) {
1617
+ for (const direction of [1, -1]) {
1618
+ const target = d.at + direction * (d.width / 2 + 0.18);
1619
+ const point = points.filter((q) => q.axis === d.axis && Math.abs(q.pos - d.pos) < 1e-3 && !hasWindowAt(q, 1.1, 0.2)).sort((a, b) => Math.abs(a.c - target) - Math.abs(b.c - target))[0];
1620
+ if (point && Math.abs(point.c - target) <= tolerance && !atDoor(plan, room, point, 0) && Math.abs(point.c - d.at) >= d.width / 2 + 0.12) return { point, door: d, latch: direction === 1 };
1621
+ }
1622
+ }
1623
+ }
1624
+ return null;
1625
+ };
1626
+ for (const room of rooms.concat(plan.rooms.filter((x) => ["stairs"].includes(x.type)))) {
1627
+ const roomLamps = lamps.filter((m) => m.room === room.id && m.floor === room.floor);
1628
+ const extra = door && door.a === room.id && room.floor === 0 && plan.lot ? [`outdoor_${door.id}`] : [];
1629
+ if (!roomLamps.length && !extra.length) continue;
1630
+ if (!pointsOf.get(keyOf(room))) pointsOf.set(keyOf(room), wallPointsOf(plan, room, items));
1631
+ const found = positionFor(room);
1632
+ if (!found) continue;
1633
+ const p = found.point;
1634
+ electrical.switches.push({ id: `switch${++counter}`, room: room.id, floor: room.floor, u: p.u, v: p.v, y: 1.1, c: p.c, axis: p.axis, pos: p.pos, frontU: p.nu, frontV: p.nv, door: found.door.id, latchSide: found.latch, controls: roomLamps.map((m) => m.id).concat(extra) });
1635
+ }
1636
+ if (door && plan.lot) {
1637
+ const id = `outdoor_${door.id}`;
1638
+ electrical.outdoorLamps.push({ id, u: round22(door.at + door.width / 2 + 0.32), v: round22(door.pos), y: 2.15, door: door.id, switch: electrical.switches.find((s) => s.controls.includes(id))?.id ?? null });
1639
+ }
1640
+ }
1641
+ function addConduits(layout) {
1642
+ const { plan, rooms, electrical } = layout;
1643
+ const panel = electrical.panel;
1644
+ const lamps = lampsOf(layout);
1645
+ for (const room of rooms) {
1646
+ const outlets = electrical.outlets.filter((t) => t.room === room.id && t.floor === room.floor);
1647
+ const switches = electrical.switches.filter((i) => i.room === room.id && i.floor === room.floor);
1648
+ const roomLamps = lamps.filter((m) => m.room === room.id && m.floor === room.floor);
1649
+ if (!outlets.length && !switches.length) continue;
1650
+ const ceiling = (room.ceiling || plan.floors[room.floor].ceiling) - 0.12;
1651
+ const part = room.parts[0];
1652
+ const centreU = round22((part[0] + part[2]) / 2);
1653
+ const centreV = round22((part[1] + part[3]) / 2);
1654
+ const circuit = { id: `circuit_${room.id}_${room.floor}`, room: room.id, floor: room.floor, outlets: outlets.map((t) => t.id), switches: switches.map((i) => i.id), lamps: roomLamps.map((m) => m.id), exposed: electrical.exposed, gauge_mm2: outlets.length > 3 ? 2.5 : 1.5 };
1655
+ electrical.circuits.push(circuit);
1656
+ const base = heightOf(plan, room.floor, 0);
1657
+ const riser = panel ? [[panel.u, panel.v, heightOf(plan, panel.floor, panel.y)], [panel.u, panel.v, heightOf(plan, panel.floor, 0) + ceiling]] : [];
1658
+ if (panel) electrical.conduits.push({ id: `conduit_${circuit.id}`, circuit: circuit.id, from: "panel", to: room.id, exposed: electrical.exposed, diameter: 0.025, points: riser.concat([[centreU, centreV, base + ceiling]]) });
1659
+ for (const point of outlets.concat(switches)) {
1660
+ electrical.conduits.push({ id: `conduit_${point.id}`, circuit: circuit.id, from: circuit.id, to: point.id, exposed: electrical.exposed, diameter: 0.02, points: [[centreU, centreV, base + ceiling], [point.u, point.v, base + ceiling], [point.u, point.v, base + point.y]] });
1661
+ }
1662
+ }
1663
+ if (panel) panel.breakers = electrical.circuits.length + 1;
1664
+ }
1665
+
1666
+ // src/utilities/electrical.ts
1667
+ var keyOf = (room) => `${room.id}@${room.floor}`;
1668
+ var absolute = (plan, floor, y) => (plan.floors[floor] ? plan.floors[floor].y0 : 0) + y;
1669
+ var heightOf = absolute;
1670
+ function planElectrical(context) {
1671
+ const { plan, items, rng, energy } = context;
1672
+ const mode = energy?.mode ?? "grid";
1673
+ const electrical = { mode, serviceEntrance: null, panel: null, outlets: [], switches: [], circuits: [], conduits: [], exposed: false, outdoorLamps: [] };
1674
+ electrical.exposed = plan.style === "budget" ? rng() < 0.65 : plan.style === "mid" ? rng() < 0.12 : false;
1675
+ if (mode !== "grid") electrical.exposed = true;
1676
+ if (mode === "offgrid") {
1677
+ electrical.source = energy?.solar ? "solar" : "lamp";
1678
+ return electrical;
1679
+ }
1680
+ const rooms = plan.rooms.filter((room) => !NO_SOCKET_ROOMS.includes(room.type) && !room.outdoor);
1681
+ const pointsOf = /* @__PURE__ */ new Map();
1682
+ for (const room of rooms) pointsOf.set(keyOf(room), wallPointsOf(plan, room, items));
1683
+ const layout = { plan, items, rooms, electrical, pointsOf };
1684
+ addOutlets(layout);
1685
+ placeEntrance(layout);
1686
+ placePanel(layout);
1687
+ addSwitches(layout);
1688
+ addConduits(layout);
1689
+ if (mode === "tap" && electrical.serviceEntrance?.meter) {
1690
+ const entrance = electrical.serviceEntrance;
1691
+ const meter = entrance.meter;
1692
+ entrance.tap = true;
1693
+ entrance.connection = { u: meter.u, v: meter.v, y: 2.6 };
1694
+ entrance.meter = null;
1695
+ }
1696
+ return electrical;
1697
+ }
1698
+
1699
+ // src/utilities/gas.ts
1700
+ var CYLINDER_TYPE = "cooking_gas_cylinder";
1701
+ var CYLINDER = { w: 0.36, d: 0.36, h: 0.48 };
1702
+ var CYLINDER_KG = 13;
1703
+ function placeMeter(plan) {
1704
+ 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));
1705
+ const wall = segments.find((w) => w.axis === "u" && w.v0 === 0) || segments[0];
1706
+ if (!wall) return null;
1707
+ const centre = wall.axis === "u" ? (wall.u0 + wall.u1) / 2 : (wall.v0 + wall.v1) / 2;
1708
+ const vector = outwardOf(wall);
1709
+ const offset = HALF_WALL + 0.1;
1710
+ return {
1711
+ id: "gas.meter",
1712
+ u: round22(wall.axis === "u" ? centre : wall.u0 + vector[0] * offset),
1713
+ v: round22(wall.axis === "u" ? wall.v0 + vector[1] * offset : centre),
1714
+ y: 1.2,
1715
+ side: sideNameOf(vector),
1716
+ w: 0.3,
1717
+ d: 0.15,
1718
+ h: 0.4
1719
+ };
1720
+ }
1721
+ function besideStove(context, stove, room, solids, doorZones) {
1722
+ const t = [-stove.frontV, stove.frontU];
1723
+ const parts = room.parts.map(inner);
1724
+ for (const s of context.rng() < 0.5 ? [1, -1] : [-1, 1]) {
1725
+ const cx = stove.x + t[0] * s * (stove.w / 2 + CYLINDER.w / 2 + 0.03) - stove.frontU * (stove.d - CYLINDER.d) / 2;
1726
+ const cz = stove.z + t[1] * s * (stove.w / 2 + CYLINDER.w / 2 + 0.03) - stove.frontV * (stove.d - CYLINDER.d) / 2;
1727
+ const rect = [cx - CYLINDER.w / 2, cz - CYLINDER.d / 2, cx + CYLINDER.w / 2, cz + CYLINDER.d / 2].map(round22);
1728
+ const inside = parts.some((q) => rect[0] >= q[0] - 0.011 && rect[2] <= q[2] + 0.011 && rect[1] >= q[1] - 0.011 && rect[3] <= q[3] + 0.011);
1729
+ if (inside && !solids.some((o) => overlaps(rect, o, 0.011)) && !doorZones.some((z) => overlaps(rect, z, 0.011))) return { mode: "beside", x: cx, z: cz, rect };
1730
+ }
1731
+ return void 0;
1732
+ }
1733
+ function inNiche(context, stove, room, doorZones) {
1734
+ const { plan, items, stateItems } = context;
1735
+ const wall = plan.walls.find(
1736
+ (p) => p.exterior && p.floor === stove.floor && p.type === "wall" && p.sides.includes(room.id) && (p.axis === "u" ? Math.abs(stove.x - clamp(stove.x, p.u0, p.u1)) < 1.6 && Math.abs(stove.z - p.v0) < 1.6 : Math.abs(stove.z - clamp(stove.z, p.v0, p.v1)) < 1.6 && Math.abs(stove.x - p.u0) < 1.6)
1737
+ );
1738
+ if (!wall || stove.floor !== 0) return void 0;
1739
+ const vector = outwardOf(wall);
1740
+ const cx = wall.axis === "u" ? clamp(stove.x, wall.u0 + 0.4, wall.u1 - 0.4) : wall.u0 + vector[0] * (HALF_WALL + CYLINDER.w / 2 + 0.05);
1741
+ const cz = wall.axis === "u" ? wall.v0 + vector[1] * (HALF_WALL + CYLINDER.d / 2 + 0.05) : clamp(stove.z, wall.v0 + 0.4, wall.v1 - 0.4);
1742
+ const rect = [cx - CYLINDER.w / 2, cz - CYLINDER.d / 2, cx + CYLINDER.w / 2, cz + CYLINDER.d / 2].map(round22);
1743
+ const blockedByItem = items.some((o) => o.outdoor && isSolid(o) && overlaps(rect, o.aabb, 0.011));
1744
+ const blockedByState = stateItems.some((o) => o.outdoor && o.aabb && !o.flat && o.mount !== "ground" && overlaps(rect, o.aabb, 0.011));
1745
+ const blockedByPath = plan.paths.some((c) => c.rects.some((z) => overlaps(rect, z, 0.011)));
1746
+ const blockedByGate = plan.gates.some((g) => g.zones.some((z) => overlaps(rect, z, 0.011)));
1747
+ const blockedByFence = !plan.lot || plan.lot.perimeter.some((q) => overlaps(rect, [Math.min(q.u0, q.u1) - HALF_WALL, Math.min(q.v0, q.v1) - HALF_WALL, Math.max(q.u0, q.u1) + HALF_WALL, Math.max(q.v0, q.v1) + HALF_WALL], 0.011));
1748
+ if (blockedByItem || blockedByState || blockedByPath || doorZones.some((z) => overlaps(rect, z, 0.011)) || blockedByGate || blockedByFence) return void 0;
1749
+ return { mode: "niche", x: cx, z: cz, rect, outdoor: true };
1750
+ }
1751
+ function underCounter(context, stove) {
1752
+ const distance = (m) => Math.hypot(m.x - stove.x, m.z - stove.z);
1753
+ const cabinets = context.items.filter(
1754
+ (m) => m.floor === stove.floor && m.room === stove.room && !m.outdoor && !m.on && !m.flat && ["kitchen_cabinet", "workbench"].includes(m.category) && m.aabb && m.id !== stove.id && Math.min(m.aabb[2] - m.aabb[0], m.aabb[3] - m.aabb[1]) >= CYLINDER.d - 0.011 && Math.max(m.aabb[2] - m.aabb[0], m.aabb[3] - m.aabb[1]) >= CYLINDER.w + 0.02
1755
+ ).sort((a, b) => (a.category === "kitchen_cabinet" ? 0 : 1) - (b.category === "kitchen_cabinet" ? 0 : 1) || distance(a) - distance(b));
1756
+ for (const cabinet of cabinets) {
1757
+ if (distance(cabinet) > 3) continue;
1758
+ const box = cabinet.aabb;
1759
+ const hw = CYLINDER.w / 2;
1760
+ const hd = CYLINDER.d / 2;
1761
+ const cx = clamp(stove.x, box[0] + hw + 0.01, box[2] - hw - 0.01);
1762
+ const cz = clamp(stove.z, box[1] + hd + 0.01, box[3] - hd - 0.01);
1763
+ const rect = [cx - hw, cz - hd, cx + hw, cz + hd].map(round22);
1764
+ if (rect[0] < box[0] || rect[2] > box[2] || rect[1] < box[1] || rect[3] > box[3] || overlaps(rect, stove.aabb, 0.011)) continue;
1765
+ return { mode: "under", x: cx, z: cz, rect, cabinet: cabinet.id };
1766
+ }
1767
+ return void 0;
1768
+ }
1769
+ var PLACEMENT = { beside: "beside", niche: "external_niche", under: "under_counter" };
1770
+ function cylinderItem(stove, spot) {
1771
+ const item = {
1772
+ id: `${stove.room}.${CYLINDER_TYPE}`,
1773
+ type: CYLINDER_TYPE,
1774
+ category: CYLINDER_TYPE,
1775
+ x: round22(spot.x),
1776
+ z: round22(spot.z),
1777
+ ry: 0,
1778
+ w: CYLINDER.w,
1779
+ d: CYLINDER.d,
1780
+ h: CYLINDER.h,
1781
+ y: 0,
1782
+ frontU: -stove.frontU,
1783
+ frontV: -stove.frontV,
1784
+ room: stove.room,
1785
+ floor: stove.floor,
1786
+ outdoor: !!spot.outdoor,
1787
+ slot: "cylinder",
1788
+ on: null,
1789
+ mode: null,
1790
+ clearance: 0,
1791
+ flat: false,
1792
+ low: false,
1793
+ aabb: spot.rect,
1794
+ link: stove.id,
1795
+ placement: PLACEMENT[spot.mode],
1796
+ hose: [[round22(spot.x), round22(spot.z), 0.3], [stove.x, stove.z, 0.5]],
1797
+ capacityKg: CYLINDER_KG
1798
+ };
1799
+ if (spot.cabinet) {
1800
+ item.on = spot.cabinet;
1801
+ item.hidden = true;
1802
+ }
1803
+ if (spot.outdoor) item.floor = -1;
1804
+ item.color = stove.color;
1805
+ return item;
1806
+ }
1807
+ function planGas(context) {
1808
+ const { plan, items, stateItems, rng } = context;
1809
+ const stoves = items.filter((m) => m.category === "stove" && !m.outdoor);
1810
+ if (!stoves.length) return null;
1811
+ const high = plan.style === "high";
1812
+ const piped = plan.style !== "commercial" && (high ? rng() < 0.8 : plan.style === "mid" ? rng() < 0.1 : false);
1813
+ const out = { type: piped ? "piped" : "cylinder", meter: null };
1814
+ const ensureMeter = () => {
1815
+ if (!out.meter) out.meter = placeMeter(plan);
1816
+ };
1817
+ for (const stove of stoves) {
1818
+ const room = plan.rooms.find((c) => c.id === stove.room && c.floor === stove.floor);
1819
+ if (!room) continue;
1820
+ stove.gas = piped ? "piped" : "cylinder";
1821
+ if (piped) {
1822
+ ensureMeter();
1823
+ stove.gasMeter = "gas.meter";
1824
+ continue;
1825
+ }
1826
+ const solids = items.filter((m) => m.floor === stove.floor && m.room === stove.room && isSolid(m) && m.id !== stove.id).map((m) => m.aabb).concat(stateItems.filter((m) => m.aabb && m.room === stove.room && m.floor === stove.floor && !m.flat && !m.on && m.mount !== "window" && m.mount !== "ground").map((m) => m.aabb));
1827
+ const doorZones = plan.doors.filter((p) => p.floor === stove.floor).flatMap((p) => p.zones);
1828
+ const order = rng() < 0.7 ? ["beside", "niche", "under"] : ["niche", "beside", "under"];
1829
+ let spot;
1830
+ for (const mode of order) {
1831
+ if (spot) break;
1832
+ spot = mode === "beside" ? besideStove(context, stove, room, solids, doorZones) : mode === "niche" ? inNiche(context, stove, room, doorZones) : underCounter(context, stove);
1833
+ }
1834
+ if (!spot) {
1835
+ stove.gas = "piped";
1836
+ out.type = "piped";
1837
+ ensureMeter();
1838
+ stove.gasMeter = "gas.meter";
1839
+ continue;
1840
+ }
1841
+ const cylinder = cylinderItem(stove, spot);
1842
+ items.push(cylinder);
1843
+ stove.cylinder = cylinder.id;
1844
+ }
1845
+ return out;
1846
+ }
1847
+
1848
+ // src/utilities/plumbing.ts
1849
+ var FIXTURES = { kitchen_cabinet: "tap", washbasin: "tap", shower: "shower", toilet: "flush", laundry_tub: "tub", washing_machine: "machine" };
1850
+ var FLOW_LPM = { tap: 6, shower: 9, flush: 6, tub: 8, machine: 12 };
1851
+ var POINT_HEIGHT = { shower: 2.1, flush: 0.9, machine: 0.8 };
1852
+ var DEFAULT_CAPACITY = { budget: 500, mid: 1e3 };
1853
+ var absolute2 = (plan, floor, y) => (plan.floors[floor] ? plan.floors[floor].y0 : 0) + y;
1854
+ function planPlumbing(context) {
1855
+ const { plan, items, tank } = context;
1856
+ const fixtures = items.filter((m) => FIXTURES[m.category] && !m.decor);
1857
+ const points = [];
1858
+ const runs = [];
1859
+ const capacity = tank?.capacity ?? (plan.style === "budget" ? DEFAULT_CAPACITY.budget : plan.style === "mid" ? DEFAULT_CAPACITY.mid : plan.tpl?.id === "mansion" ? 3e3 : 2e3);
1860
+ const plumbing = { tank: tank ? "water_tank" : null, capacityL: tank || !plan.tpl.noWalls ? capacity : 0, points, visibleRuns: runs };
1861
+ const top = plan.floors.length - 1;
1862
+ fixtures.forEach((item, index) => {
1863
+ const kind = FIXTURES[item.category];
1864
+ points.push({ id: `pt${index + 1}`, kind, item: item.id, room: item.room, floor: item.outdoor ? -1 : item.floor, u: item.x, v: item.z, y: POINT_HEIGHT[kind] ?? 0.9, flowLpm: FLOW_LPM[kind], run: null });
1865
+ });
1866
+ if (!tank) return plumbing;
1867
+ const slabY = absolute2(plan, top, plan.floors[top].ceiling);
1868
+ const tankY = tank.place === "raised_slab" ? tank.platform.topY : tank.place === "attic" ? slabY + 0.5 : tank.y;
1869
+ runs.push({ id: "run_outlet", from: "water_tank", to: "ceiling", points: [[tank.u, tank.v, round22(tankY + 0.15)], [tank.u, tank.v, round22(slabY - 0.15)]], diameter: 0.032, exposed: "tank_outlet" });
1870
+ for (const point of points) {
1871
+ const item = fixtures.find((x) => x.id === point.item);
1872
+ if (point.kind !== "tap" || item.outdoor) continue;
1873
+ const front = [item.frontU || 0, item.frontV || 1];
1874
+ const behind = [item.x - front[0] * (item.d / 2 + 0.02), item.z - front[1] * (item.d / 2 + 0.02)];
1875
+ runs.push({ id: `run_under_${point.id}`, from: "valve", to: point.id, points: [[round22(behind[0]), round22(behind[1]), absolute2(plan, item.floor, 0.5)], [item.x, item.z, absolute2(plan, item.floor, 0.5)], [item.x, item.z, absolute2(plan, item.floor, 0.75)]], diameter: 0.025, exposed: "under_sink" });
1876
+ }
1877
+ const wet = [];
1878
+ for (const point of points) {
1879
+ if (!wet.some((c) => c.room === point.room && c.floor === point.floor)) wet.push({ room: point.room, floor: point.floor, points: points.filter((q) => q.room === point.room && q.floor === point.floor) });
1880
+ }
1881
+ for (const room of wet) {
1882
+ const found = plan.rooms.find((c) => c.id === room.room && c.floor === room.floor);
1883
+ const zone = plan.zones.find((z) => z.id === room.room);
1884
+ const parts = (found || zone || { parts: [[tank.u, tank.v, tank.u, tank.v]] }).parts;
1885
+ const centreU = (parts[0][0] + parts[0][2]) / 2;
1886
+ const centreV = (parts[0][1] + parts[0][3]) / 2;
1887
+ const ceiling = found ? found.ceiling || plan.floors[found.floor].ceiling : 2.8;
1888
+ const y0 = found ? absolute2(plan, found.floor, 0) : 0;
1889
+ const id = `run_wall_${room.room}${room.floor === -1 ? "_outside" : `_${room.floor}`}`;
1890
+ runs.push({ id, from: "water_tank", to: room.room, points: [[tank.u, tank.v, round22(slabY - 0.15)], [round22(centreU), round22(centreV), round22(slabY - 0.15)], [round22(centreU), round22(centreV), round22(y0 + ceiling - 0.15)], [round22(centreU), round22(centreV), round22(y0 + 0.6)]], diameter: 0.025, exposed: "wall" });
1891
+ for (const point of room.points) point.run = id;
1892
+ }
1893
+ return plumbing;
1894
+ }
1895
+
1896
+ // src/utilities/solar.ts
1897
+ var PANEL_WIDTH = 1;
1898
+ var PANEL_LENGTH = 1.65;
1899
+ var NO_PANELS_UNDER = ["void", "elevator", "stairs", "balcony", "terrace", "porch"];
1900
+ function placeInverter(plan) {
1901
+ const wall = groundExteriorWalls(plan).filter((q) => q.sides.includes("outside")).sort((a, b) => (a.axis === "v" ? 0 : 1) - (b.axis === "v" ? 0 : 1))[0];
1902
+ if (!wall) return null;
1903
+ const vector = outwardOf(wall);
1904
+ const centre = wall.axis === "u" ? (wall.u0 + wall.u1) / 2 : (wall.v0 + wall.v1) / 2;
1905
+ const offset = HALF_WALL + 0.08;
1906
+ return { id: "inverter", u: round22(wall.axis === "u" ? centre : wall.u0 + vector[0] * offset), v: round22(wall.axis === "u" ? wall.v0 + vector[1] * offset : centre), y: 1.5, side: sideNameOf(vector), w: 0.4, d: 0.15, h: 0.5, outdoor: true, link: "panel" };
1907
+ }
1908
+ function planSolar(context) {
1909
+ const { plan, rng, energy } = context;
1910
+ if (!plan.lot || plan.style === "commercial" || !plan.facade) return null;
1911
+ const chance = plan.style === "budget" ? 0.06 : plan.style === "mid" ? 0.25 : 0.5;
1912
+ const forced = energy ? energy.solar ? 1 : 0 : null;
1913
+ const draw = rng();
1914
+ if (forced !== null ? !forced : draw >= chance) return null;
1915
+ const floor = plan.floors.length - 1;
1916
+ const level = plan.floors[floor];
1917
+ const flat = plan.facade.roof === "slab";
1918
+ const tops = plan.rooms.filter((c) => c.floor === floor && !NO_PANELS_UNDER.includes(c.type));
1919
+ if (!tops.length) return null;
1920
+ const parts = tops.flatMap((c) => c.parts);
1921
+ const box = [Math.min(...parts.map((q) => q[0])), Math.min(...parts.map((q) => q[1])), Math.max(...parts.map((q) => q[2])), Math.max(...parts.map((q) => q[3]))];
1922
+ const north = rng() < 0.5 ? "front" : "back";
1923
+ const angle = flat ? 15 : 25;
1924
+ const projection = round22(PANEL_LENGTH * Math.cos(angle * Math.PI / 180));
1925
+ const margin = 0.6;
1926
+ const uStart = box[0] + margin;
1927
+ const uEnd = box[2] - margin;
1928
+ const columns = Math.max(1, Math.floor((uEnd - uStart) / (PANEL_WIDTH + 0.05)));
1929
+ const vMiddle = (box[1] + box[3]) / 2;
1930
+ const vArea = flat ? [box[1] + margin, box[3] - margin] : north === "front" ? [box[1] + 0.4, vMiddle - 0.2] : [vMiddle + 0.2, box[3] - 0.4];
1931
+ const step = flat ? projection + 0.5 : projection + 0.05;
1932
+ const rowsMax = Math.max(1, Math.floor((vArea[1] - vArea[0]) / step));
1933
+ const wanted = forced !== null ? 1 + Math.floor(rng() * 2) : 3 + Math.floor(rng() * (plan.style === "high" ? 12 : 8));
1934
+ const rows = Math.max(1, Math.min(rowsMax, Math.ceil(wanted / columns)));
1935
+ const count = Math.min(wanted, rows * columns);
1936
+ const u0 = uStart + (uEnd - uStart - columns * (PANEL_WIDTH + 0.05)) / 2;
1937
+ const vStart = north === "front" ? vArea[0] : vArea[1] - rows * step;
1938
+ const panels = [];
1939
+ for (let k = 0; k < count; k++) {
1940
+ const row = Math.floor(k / columns);
1941
+ const column = k % columns;
1942
+ panels.push({ id: `pv${k + 1}`, u: round22(u0 + column * (PANEL_WIDTH + 0.05) + PANEL_WIDTH / 2), v: round22(vStart + row * step + (flat ? 0.25 : 0) + projection / 2), w: PANEL_WIDTH, d: projection, length: PANEL_LENGTH });
1943
+ }
1944
+ const area = [round22(u0), round22(vStart), round22(u0 + columns * (PANEL_WIDTH + 0.05)), round22(vStart + rows * step)];
1945
+ return { type: flat ? "slab_rack" : "roof", orientation: "north", north, slopeSide: flat ? null : north, tilt: angle, area, areaM2: round22(count * PANEL_WIDTH * PANEL_LENGTH), count, floor, y: round22(level.y0 + level.ceiling), panels, inverter: placeInverter(plan) };
1946
+ }
1947
+
1948
+ // src/utilities/index.ts
1949
+ function planUtilities(context) {
1950
+ const { plan, items, stateItems, tank, energy, rngFor } = context;
1951
+ const gas = planGas({ plan, items, stateItems, rng: rngFor("gas") });
1952
+ const electrical = planElectrical({ plan, items, rng: rngFor("eletrica"), ...energy ? { energy } : {} });
1953
+ const solar = planSolar({ plan, rng: rngFor("solar"), ...energy ? { energy } : {} });
1954
+ const plumbing = planPlumbing({ plan, items, tank });
1955
+ return { gas, electrical, solar, plumbing };
1956
+ }
1957
+ var OVERHEAD = "overhead_cabinet";
1958
+ var ALWAYS_HOLD = ["trash_bin", "outdoor_bin", "mailbox"];
1959
+ function planContents(context) {
1960
+ const out = {};
1961
+ const ask = (item, container, key) => context.findsFor({ place: context.place, container, state: context.state, rng: context.rngFor(key), footprint: item.w * item.d, key: item.id, ...context.day === void 0 ? {} : { day: context.day }, ...context.region === void 0 ? {} : { region: context.region } });
1962
+ for (const item of context.items) {
1963
+ if (item.decor && item.mount !== "wall_top") continue;
1964
+ const container = hasContent(item.type) ? item.type : item.category;
1965
+ if (!hasContent(container)) continue;
1966
+ if (!ALWAYS_HOLD.includes(container) && item.decor) continue;
1967
+ out[item.id] = ask(item, container, `content:${item.id}`);
1968
+ if (container === "kitchen_cabinet" && item.overhead !== false) out[`${item.id}#overhead`] = ask(item, OVERHEAD, `content:${item.id}:overhead`);
1969
+ }
1970
+ return out;
1971
+ }
1972
+
1973
+ // src/utilities/outbuildings.ts
1974
+ var WALL = 0.15;
1975
+ var DOOR_WIDTH = 0.85;
1976
+ var DOOR_HEIGHT2 = 2.1;
1977
+ var USES = { guest_room: ["single_bed", "nightstand"], office: ["desk", "shelf_unit_small"], storage: ["tool_shelf", "crate"], gourmet: ["dining_table_small", "workbench"] };
1978
+ var USE_WEIGHTS = { guest_room: 3, office: 2, storage: 2, gourmet: 2 };
1979
+ var PATH_WIDTH = 1;
1980
+ var LEGACY_ROOMS = { backyard: "quintal" };
1981
+ var LEGACY_SLOTS = { outbuilding: "edicula" };
1982
+ var streamOf = (itemId) => {
1983
+ const [room = "", ...slot] = itemId.split(".");
1984
+ return `edif:${LEGACY_ROOMS[room] ?? room}.${slot.map((part) => LEGACY_SLOTS[part] ?? part).join(".")}`;
1985
+ };
1986
+ function pickWeighted(rng, weights) {
1987
+ const keys = Object.keys(weights);
1988
+ let x = rng() * keys.reduce((sum, key) => sum + weights[key], 0);
1989
+ for (const key of keys) {
1990
+ x -= weights[key];
1991
+ if (x < 0) return key;
1992
+ }
1993
+ return keys[keys.length - 1];
1994
+ }
1995
+ var sideOf = (x, z) => Math.abs(x) > Math.abs(z) ? x > 0 ? "right" : "left" : z > 0 ? "back" : "front";
1996
+ var at = (frame, a, b) => [frame.cx + frame.n[0] * a + frame.t[0] * b, frame.cz + frame.n[1] * a + frame.t[1] * b];
1997
+ function opening(frame, id, a, b, vector, width, extra) {
1998
+ const [u, v] = at(frame, a, b);
1999
+ const alongU = Math.abs(vector[0]) !== 1;
2000
+ return { id, axis: alongU ? "u" : "v", pos: round22(alongU ? v : u), at: round22(alongU ? u : v), u: round22(u), v: round22(v), width: round22(width), side: sideOf(vector[0], vector[1]), ...extra };
2001
+ }
2002
+ function wallsOf(box, door, height) {
2003
+ const walls = [];
2004
+ const sides = [
2005
+ { axis: "u", pos: box[1], i0: box[0], i1: box[2], side: "front" },
2006
+ { axis: "u", pos: box[3], i0: box[0], i1: box[2], side: "back" },
2007
+ { axis: "v", pos: box[0], i0: box[1], i1: box[3], side: "left" },
2008
+ { axis: "v", pos: box[2], i0: box[1], i1: box[3], side: "right" }
2009
+ ];
2010
+ for (const w of sides) {
2011
+ const cut = w.axis === door.axis && Math.abs(w.pos - door.pos) < 0.02 ? [door.at - door.width / 2, door.at + door.width / 2] : null;
2012
+ const segments = cut ? [[w.i0, cut[0]], [cut[1], w.i1]] : [[w.i0, w.i1]];
2013
+ for (const [x0, x1] of segments) {
2014
+ if (x1 - x0 <= 1e-3) continue;
2015
+ walls.push(w.axis === "u" ? { axis: "u", u0: round22(x0), v0: round22(w.pos), u1: round22(x1), v1: round22(w.pos), thickness: WALL, h: height, side: w.side } : { axis: "v", u0: round22(w.pos), v0: round22(x0), u1: round22(w.pos), v1: round22(x1), thickness: WALL, h: height, side: w.side });
2016
+ }
2017
+ }
2018
+ return walls;
2019
+ }
2020
+ function furnish(context, frame, use, interior, insideDoor) {
2021
+ const placed = [];
2022
+ USES[use].forEach((type, k) => {
2023
+ const def = context.catalog[type];
2024
+ const facing = k === 0 ? frame.n : [frame.t[0] * frame.sideToMiddle, frame.t[1] * frame.sideToMiddle];
2025
+ const [a, b] = k === 0 ? [-frame.depth / 2 + WALL + def.d / 2, frame.sideToMiddle * -0.4] : [-0.1, -frame.sideToMiddle * (frame.width / 2 - WALL - def.d / 2)];
2026
+ const [u, v] = at(frame, a, b);
2027
+ const eu = Math.abs(facing[0]) === 1 ? def.d : def.w;
2028
+ const ev = Math.abs(facing[0]) === 1 ? def.w : def.d;
2029
+ const box = [u - eu / 2, v - ev / 2, u + eu / 2, v + ev / 2].map(round22);
2030
+ if (box[0] < interior[0] - 0.011 || box[2] > interior[2] + 0.011 || box[1] < interior[1] - 0.011 || box[3] > interior[3] + 0.011 || overlaps(box, insideDoor) || placed.some((o) => overlaps(box, o.aabb))) return;
2031
+ placed.push({ type, category: def.category, u: round22(u), v: round22(v), w: def.w, d: def.d, h: def.h, ry: Math.atan2(facing[0], facing[1]), frontU: facing[0], frontV: facing[1], aabb: box });
2032
+ });
2033
+ return placed;
2034
+ }
2035
+ function planOutbuildings(context) {
2036
+ const { plan, items } = context;
2037
+ const lot = plan.lot;
2038
+ if (!lot) return [];
2039
+ const out = [];
2040
+ for (const item of items.filter((m) => m.category === "outbuilding")) {
2041
+ const rng = context.rngFor(streamOf(item.id));
2042
+ const [a0, a1, a2, a3] = item.aabb;
2043
+ const n = [item.frontU, item.frontV];
2044
+ const t = [-item.frontV, item.frontU];
2045
+ const horizontal = Math.abs(item.frontU) === 1;
2046
+ const cx = (a0 + a2) / 2;
2047
+ const cz = (a1 + a3) / 2;
2048
+ const depth = horizontal ? a2 - a0 : a3 - a1;
2049
+ const width = horizontal ? a3 - a1 : a2 - a0;
2050
+ const sideToMiddle = ((lot.rect[0] + lot.rect[2]) / 2 - cx) * t[0] + ((lot.rect[1] + lot.rect[3]) / 2 - cz) * t[1] >= 0 ? 1 : -1;
2051
+ const frame = { n, t, cx, cz, depth, width, horizontal, sideToMiddle };
2052
+ const doorOffset = sideToMiddle * (width / 2 - 0.95);
2053
+ const door = opening(frame, `${item.id}.d0`, depth / 2, doorOffset, n, DOOR_WIDTH, { height: DOOR_HEIGHT2, exterior: true });
2054
+ const windowFront = opening(frame, `${item.id}.w0`, depth / 2, -sideToMiddle * (width / 2 - 0.9), n, Math.min(1, width / 2 - 0.6), { y0: 1, y1: 2 });
2055
+ const windowSide = opening(frame, `${item.id}.w1`, 0, sideToMiddle * width / 2, [t[0] * sideToMiddle, t[1] * sideToMiddle], Math.min(1, depth - 0.8), { y0: 1, y1: 2 });
2056
+ const [p, q] = [at(frame, depth / 2, doorOffset), at(frame, depth / 2 - 0.9, doorOffset)];
2057
+ door.zone = (horizontal ? [Math.min(p[0], q[0]), p[1] - door.width / 2, Math.max(p[0], q[0]), p[1] + door.width / 2] : [p[0] - door.width / 2, Math.min(p[1], q[1]), p[0] + door.width / 2, Math.max(p[1], q[1])]).map(round22);
2058
+ const interior = [a0 + WALL, a1 + WALL, a2 - WALL, a3 - WALL];
2059
+ const use = pickWeighted(rng, USE_WEIGHTS);
2060
+ out.push({ id: item.id, type: "outbuilding", use, floor: 0, aabb: item.aabb.slice(), interior: [interior.map(round22)], ceiling: item.h, thickness: WALL, front: n, walls: wallsOf(item.aabb, door, item.h), doors: [door], windows: [windowFront, windowSide], items: furnish(context, frame, use, interior, door.zone) });
2061
+ }
2062
+ return out;
2063
+ }
2064
+ function addOutbuildingPaths(lot, outbuildings, items) {
2065
+ const half = PATH_WIDTH / 2;
2066
+ const rect = lot.rect;
2067
+ const solids = items.filter((m) => m.outdoor && m.aabb && isSolid(m)).map((m) => m.aabb);
2068
+ const fence = lot.perimeter.map((q) => [Math.min(q.u0, q.u1) - 0.075, Math.min(q.v0, q.v1) - 0.075, Math.max(q.u0, q.u1) + 0.075, Math.max(q.v0, q.v1) + 0.075]);
2069
+ const strip = (p, q) => [Math.min(p[0], q[0]) - half, Math.min(p[1], q[1]) - half, Math.max(p[0], q[0]) + half, Math.max(p[1], q[1]) + half];
2070
+ for (const building of outbuildings) {
2071
+ for (const door of building.doors) {
2072
+ if (lot.paths.some((c) => c.to === door.id)) continue;
2073
+ const z = door.zone;
2074
+ const zc = door.axis === "v" ? (z[0] + z[2]) / 2 : (z[1] + z[3]) / 2;
2075
+ const outside = zc > door.pos ? -1 : 1;
2076
+ const A = door.axis === "v" ? [door.pos, door.at] : [door.at, door.pos];
2077
+ const S = door.axis === "v" ? [door.pos + outside * PATH_WIDTH, door.at] : [door.at, door.pos + outside * PATH_WIDTH];
2078
+ const stub = door.axis === "v" ? [Math.min(A[0], S[0]), A[1] - half, Math.max(A[0], S[0]), A[1] + half] : [A[0] - half, Math.min(A[1], S[1]), A[0] + half, Math.max(A[1], S[1])];
2079
+ const cost = (rs) => {
2080
+ let n = 0;
2081
+ for (const r of rs) {
2082
+ const c = [r[0] + 0.02, r[1] + 0.02, r[2] - 0.02, r[3] - 0.02];
2083
+ for (const q of solids) if (overlaps(c, q, 0.011)) n += 2;
2084
+ if (overlaps(c, building.aabb, 0.011)) n += 3;
2085
+ for (const q of fence) if (overlaps(c, q, 0.011)) n += 3;
2086
+ if (r[0] < rect[0] || r[1] < rect[1] || r[2] > rect[2] || r[3] > rect[3]) n += 3;
2087
+ }
2088
+ return n;
2089
+ };
2090
+ let best = null;
2091
+ for (const c of lot.paths.filter((p) => p.type === "pedestrian" || p.id === "cargo_path")) {
2092
+ for (const r of c.rects) {
2093
+ const T = [Math.min(Math.max(S[0], r[0] + half), r[2] - half), Math.min(Math.max(S[1], r[1] + half), r[3] - half)];
2094
+ const candidates = [];
2095
+ if (Math.abs(T[0] - S[0]) < 1e-6 && Math.abs(T[1] - S[1]) < 1e-6) candidates.push({ rects: [stub], points: [A, S] });
2096
+ else for (const via of [[T[0], S[1]], [S[0], T[1]]]) candidates.push({ rects: [stub, strip(S, via), strip(via, T)].filter((q) => q[2] - q[0] > 1e-6), points: [A, S, via, T] });
2097
+ for (const k of candidates) {
2098
+ const distance = k.points.slice(1).reduce((sum, q, i) => sum + Math.abs(q[0] - k.points[i][0]) + Math.abs(q[1] - k.points[i][1]), 0);
2099
+ const value = cost(k.rects.slice(1)) * 100 + distance;
2100
+ if (!best || value < best.v) best = { v: value, k, from: c.from };
2101
+ }
2102
+ }
2103
+ }
2104
+ const chosen = best ? best.k : { rects: [stub], points: [A, S] };
2105
+ lot.paths.push({ id: `path_${door.id}`, type: "pedestrian", from: best ? best.from : null, to: door.id, width: PATH_WIDTH, rects: chosen.rects.map((q) => q.map(round22)), points: chosen.points.map((q) => q.map(round22)) });
2106
+ }
2107
+ }
2108
+ }
2109
+
2110
+ // src/water-tank.ts
2111
+ var TANK_SIDE = 1.1;
2112
+ var NOT_UNDER_TANK = ["void", "elevator", "stairs", "balcony", "porch"];
2113
+ var WET_ROOMS = ["bathroom", "kitchen", "laundry"];
2114
+ var ATTIC_CHANCE = 0.12;
2115
+ var clamp2 = (value, low, high) => Math.max(low, Math.min(high, value));
2116
+ var DEFAULT_CAPACITIES = { budget: 500, mid: 1e3, mansion: 3e3 };
2117
+ function capacityOf(source, capacities) {
2118
+ return capacities[source.style ?? ""] ?? capacities[source.template ?? ""] ?? 2e3;
2119
+ }
2120
+ function bestSpot(source, floor, reference) {
2121
+ const centre = reference ? [reference.u, reference.v] : [source.w / 2, source.d / 2];
2122
+ let best;
2123
+ for (const room of source.rooms.filter((r) => r.floor === floor && !NOT_UNDER_TANK.includes(r.type))) {
2124
+ for (const part of room.parts) {
2125
+ const rect = [part[0] + 0.4, part[1] + 0.4, part[2] - 0.4, part[3] - 0.4];
2126
+ if (rect[2] - rect[0] < TANK_SIDE || rect[3] - rect[1] < TANK_SIDE) continue;
2127
+ const base = WET_ROOMS.includes(room.type) ? 0 : room.type === "terrace" ? 500 : 100;
2128
+ const weight = base + Math.hypot((rect[0] + rect[2]) / 2 - centre[0], (rect[1] + rect[3]) / 2 - centre[1]);
2129
+ if (!best || weight < best.weight) best = { roomType: room.type, roomId: room.id, rect, weight };
2130
+ }
2131
+ }
2132
+ return best;
2133
+ }
2134
+ function raisedPlatform(source, u, v, baseY) {
2135
+ const margin = 0.12;
2136
+ const edge = Math.max(0, Math.min(u, source.w - u, v, source.d - v));
2137
+ const rise = round2(clamp2(edge * 0.35 + 0.7, 1.1, 2.4));
2138
+ const thickness = 0.12;
2139
+ const reach = TANK_SIDE / 2 + margin - 0.06;
2140
+ const pillars = [[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([a, b]) => ({ u: round2(u + a * reach), v: round2(v + b * reach), side: 0.12 }));
2141
+ const half = TANK_SIDE / 2 + margin;
2142
+ return { aabb: [u - half, v - half, u + half, v + half].map(round2), baseY, height: rise, thickness, topY: round2(baseY + rise + thickness), pillars, ladder: true };
2143
+ }
2144
+ function planWaterTank(source, rng, options = {}) {
2145
+ if (!source.lot || source.style === "commercial") return null;
2146
+ if (options.custom) return options.custom(source, rng);
2147
+ const floor = source.floors.length - 1;
2148
+ const level = source.floors[floor];
2149
+ const capacity = capacityOf(source, { ...DEFAULT_CAPACITIES, ...options.capacities });
2150
+ const height = capacity >= 2e3 ? 1.4 : capacity === 1e3 ? 1 : 0.85;
2151
+ const place = options.location ? options.location(source, rng) : source.facade.roof === "slab" ? "slab" : rng() < ATTIC_CHANCE ? "attic" : "raised_slab";
2152
+ const reference = source.facade.slab?.waterTank;
2153
+ const spot = bestSpot(source, floor, reference);
2154
+ if (!spot) return null;
2155
+ const target = reference ? [reference.u, reference.v] : [(spot.rect[0] + spot.rect[2]) / 2, (spot.rect[1] + spot.rect[3]) / 2];
2156
+ const u = clamp2(target[0], spot.rect[0] + TANK_SIDE / 2, spot.rect[2] - TANK_SIDE / 2);
2157
+ const v = clamp2(target[1], spot.rect[1] + TANK_SIDE / 2, spot.rect[3] - TANK_SIDE / 2);
2158
+ const where = spot.roomType === "terrace" ? "slab" : place;
2159
+ const slabY = round2(spot.roomType === "terrace" ? level.y0 : level.y0 + level.ceiling);
2160
+ const tank = {
2161
+ id: "water_tank",
2162
+ kind: "water_tank",
2163
+ itemType: "water_tank",
2164
+ place: where,
2165
+ floor,
2166
+ room: spot.roomId,
2167
+ u: round2(u),
2168
+ v: round2(v),
2169
+ y: slabY,
2170
+ w: TANK_SIDE,
2171
+ d: TANK_SIDE,
2172
+ h: height,
2173
+ capacity,
2174
+ aabb: [u - TANK_SIDE / 2, v - TANK_SIDE / 2, u + TANK_SIDE / 2, v + TANK_SIDE / 2].map(round2)
2175
+ };
2176
+ if (where === "raised_slab") {
2177
+ tank.platform = raisedPlatform(source, u, v, slabY);
2178
+ tank.y = tank.platform.topY;
2179
+ }
2180
+ return tank;
2181
+ }
2182
+
2183
+ // src/viewer-layer.ts
2184
+ var COLORS = { concrete: "#7d8590", steel: "#3d6fb0", masonry: "#b5654b", wood: "#a0693a" };
2185
+ var BEAM_IDS = ["plate_", "porch_beam", "base_beam", "trestle_beam"];
2186
+ var LAYER_ID = "structure";
2187
+ function groupOf(material) {
2188
+ if (material === "concrete") return "concrete";
2189
+ if (material === "steel" || material === "aluminium" || material === "cast_iron") return "steel";
2190
+ if (material === "brick" || material === "stone" || material === "ceramic") return "masonry";
2191
+ return "wood";
2192
+ }
2193
+ var isRoofFrame = (part) => ["truss", "rafter", "ridge"].some((prefix) => part.id.startsWith(prefix));
2194
+ var isDrawnBeam = (part) => part.role === "beam" && BEAM_IDS.some((prefix) => part.id.startsWith(prefix));
2195
+ function drawColumns(context, parts) {
2196
+ for (const part of parts.filter((p) => p.role === "column")) {
2197
+ const color = COLORS[groupOf(part.material)];
2198
+ if (groupOf(part.material) === "wood") context.rect(part.aabb, null, color, 2);
2199
+ else context.rect(part.aabb, color, null);
2200
+ context.hit(part.aabb, `${part.id} \xB7 column \xB7 ${part.material} \xB7 ${part.mass} kg`);
2201
+ }
2202
+ }
2203
+ function drawBeams(context, parts) {
2204
+ context.g.save();
2205
+ context.g.setLineDash([6, 4]);
2206
+ for (const part of parts.filter(isDrawnBeam)) {
2207
+ context.rect(part.aabb, null, COLORS[groupOf(part.material)], 1.5);
2208
+ context.hit(part.aabb, `${part.id} \xB7 beam \xB7 ${part.material} \xB7 ${part.mass} kg`);
2209
+ }
2210
+ context.g.restore();
2211
+ }
2212
+ function drawRoofFrames(context, parts) {
2213
+ context.g.save();
2214
+ context.g.strokeStyle = COLORS.wood;
2215
+ context.g.lineWidth = 1;
2216
+ for (const part of parts.filter(isRoofFrame)) {
2217
+ const [u0, v0, u1, v1] = part.aabb;
2218
+ const alongV = v1 - v0 > u1 - u0;
2219
+ context.g.beginPath();
2220
+ if (alongV) {
2221
+ context.g.moveTo(context.X((u0 + u1) / 2), context.Z(v0));
2222
+ context.g.lineTo(context.X((u0 + u1) / 2), context.Z(v1));
2223
+ } else {
2224
+ context.g.moveTo(context.X(u0), context.Z((v0 + v1) / 2));
2225
+ context.g.lineTo(context.X(u1), context.Z((v0 + v1) / 2));
2226
+ }
2227
+ context.g.stroke();
2228
+ }
2229
+ context.g.restore();
2230
+ }
2231
+ function drawPorchRoofs(context, parts) {
2232
+ context.g.save();
2233
+ context.g.setLineDash([2, 4]);
2234
+ for (const part of parts.filter((p) => p.id.startsWith("porch_roof"))) context.rect(part.aabb, null, COLORS.wood, 1);
2235
+ context.g.restore();
2236
+ }
2237
+ function drawStructure(context) {
2238
+ const structure = context.plan.structure;
2239
+ if (!structure) return;
2240
+ const floor = context.all ? 0 : context.floor;
2241
+ const here = structure.parts.filter((part) => part.floor === floor);
2242
+ drawPorchRoofs(context, here);
2243
+ if (floor === (context.plan.roof?.bearingFloor ?? 0)) drawRoofFrames(context, structure.parts);
2244
+ drawBeams(context, here);
2245
+ drawColumns(context, here);
2246
+ }
2247
+ function structureLegend(plan) {
2248
+ const parts = plan.structure?.parts ?? [];
2249
+ const entries = [];
2250
+ for (const group of ["concrete", "steel", "masonry", "wood"]) {
2251
+ const columns = parts.some((p) => p.role === "column" && groupOf(p.material) === group);
2252
+ if (columns) entries.push({ label: `structure_column_${group}`, color: COLORS[group], shape: group === "wood" ? "hollow" : "filled" });
2253
+ }
2254
+ for (const group of ["concrete", "wood"]) {
2255
+ if (parts.some((p) => isDrawnBeam(p) && groupOf(p.material) === group)) entries.push({ label: `structure_beam_${group}`, color: COLORS[group], shape: "dashed" });
2256
+ }
2257
+ if (parts.some(isRoofFrame)) entries.push({ label: "structure_roof_frame", color: COLORS.wood, shape: "line" });
2258
+ return entries;
2259
+ }
2260
+ var structureLayer = {
2261
+ id: LAYER_ID,
2262
+ label: "structure_layer",
2263
+ defaultOn: false,
2264
+ draw: drawStructure,
2265
+ legend: structureLegend
2266
+ };
2267
+
2268
+ // src/register.ts
2269
+ var CACHE = /* @__PURE__ */ new WeakMap();
2270
+ var cacheOf = (result) => {
2271
+ const cache = CACHE.get(result);
2272
+ if (!cache) throw new Error("The construction extension did not run for this plan");
2273
+ return cache;
2274
+ };
2275
+ function constructionExtension(context) {
2276
+ const build = constructionFor({ result: context.result, seed: context.seed, template: context.plan.tpl, options: context.options });
2277
+ CACHE.set(context.result, { build });
2278
+ return build.construction;
2279
+ }
2280
+ function structureExtension(context) {
2281
+ const cache = cacheOf(context.result);
2282
+ const options = { construction: cache.build.construction, extras: cache.build.extras ?? {}, tank: context.result.waterTank ?? null, wallDetail: context.options.wallDetail ?? "panels" };
2283
+ const { roof, ...structure } = buildStructure(context.result, options);
2284
+ cache.roof = roof;
2285
+ for (const part of structure.parts) {
2286
+ if (part.door === void 0 || part.steps === void 0) continue;
2287
+ const door = context.result.doors.find((d) => d.id === part.door);
2288
+ if (door) door.steps = part.steps;
2289
+ }
2290
+ return structure;
2291
+ }
2292
+ var COOKING_CYLINDER = { type: "cooking_gas_cylinder", category: "cooking_gas_cylinder", w: 0.36, d: 0.36, h: 0.48 };
2293
+ function energyOf(context) {
2294
+ const energy = context.plan.tpl.energy;
2295
+ return typeof energy === "function" ? energy(context.options, context.plan.w, context.plan.d) : void 0;
2296
+ }
2297
+ function register(engine, options = {}) {
2298
+ engine.registerLocale("en", en_default);
2299
+ engine.registerLocale("pt-BR", pt_BR_default);
2300
+ engine.registerResultExtension("construction", constructionExtension);
2301
+ engine.registerResultExtension("structureExtras", (context) => cacheOf(context.result).build.extras ?? {}, { after: ["construction"] });
2302
+ engine.registerResultExtension(
2303
+ "waterTank",
2304
+ (context) => {
2305
+ const tank = { capacities: options.tankCapacities, custom: context.plan.tpl.tank, location: context.plan.tpl.tankLocation };
2306
+ return planWaterTank(context.result, context.rng, tank);
2307
+ },
2308
+ { after: ["construction"], seedKey: "caixa" }
2309
+ );
2310
+ engine.registerResultExtension("structure", structureExtension, { after: ["construction", "structureExtras", "waterTank"] });
2311
+ engine.registerResultExtension("roof", (context) => cacheOf(context.result).roof ?? null, { after: ["structure"] });
2312
+ engine.registerViewerLayer(structureLayer);
2313
+ engine.registerItem(COOKING_CYLINDER);
2314
+ engine.registerResultExtension("contents", (context) => planContents({ items: context.result.items, state: context.result.state, place: context.plan.tpl.place ?? "house", ...typeof context.options.day === "number" ? { day: context.options.day } : {}, ...typeof context.options.region === "string" ? { region: context.options.region } : {}, rngFor: (key) => engine.createRng(context.seed, key), findsFor: (query) => {
2315
+ const answered = engine.findsAnswer(query);
2316
+ return answered ?? contentOf({ container: query.container, state: query.state, rng: query.rng, footprint: query.footprint ?? 0.5 });
2317
+ } }));
2318
+ engine.registerResultExtension("outbuildings", (context) => {
2319
+ const outbuildings = planOutbuildings({ plan: context.plan, items: context.result.items, catalog: engine.catalog, rngFor: (key) => engine.createRng(context.seed, key) });
2320
+ if (context.result.lot) addOutbuildingPaths(context.result.lot, outbuildings, context.result.items);
2321
+ return outbuildings;
2322
+ }, { after: ["contents"] });
2323
+ engine.registerResultExtension("utilities", (context) => planUtilities({ plan: context.plan, items: context.result.items, stateItems: context.result.stateItems, tank: context.result.waterTank ?? null, ...energyOf(context) ? { energy: energyOf(context) } : {}, rngFor: (key) => engine.createRng(context.seed, key) }), { after: ["outbuildings", "waterTank"] });
2324
+ }
2325
+
2326
+ // ../game-destruction/dist/index.js
2327
+ var materials_default = [
2328
+ {
2329
+ id: "steel",
2330
+ density: 7850,
2331
+ hpPerKg: 400,
2332
+ penetration: 110,
2333
+ ductility: 0.9,
2334
+ fracture: "none",
2335
+ damageScale: {
2336
+ slash: 0.1,
2337
+ blunt: 0.8
2338
+ },
2339
+ hitDebris: "sparks",
2340
+ breakDebris: "sparks",
2341
+ ricochetDeg: 20,
2342
+ thermal: {
2343
+ meltC: 1450,
2344
+ meltRate: 0.05,
2345
+ conductivity: 0.5,
2346
+ heatCapacity: 3
2347
+ },
2348
+ visual: {
2349
+ glows: true
2350
+ },
2351
+ tags: [
2352
+ "metal",
2353
+ "conductive"
2354
+ ],
2355
+ compressive: 2e6
2356
+ },
2357
+ {
2358
+ id: "cast_iron",
2359
+ density: 7200,
2360
+ hpPerKg: 150,
2361
+ penetration: 160,
2362
+ ductility: 0.2,
2363
+ fracture: "snap",
2364
+ fragmentSize: [
2365
+ 0.08,
2366
+ 0.3
2367
+ ],
2368
+ damageScale: {
2369
+ slash: 0.1,
2370
+ blunt: 1.2
2371
+ },
2372
+ hitDebris: "sparks",
2373
+ breakDebris: "chips",
2374
+ ricochetDeg: 15,
2375
+ thermal: {
2376
+ meltC: 1200,
2377
+ meltRate: 0.05,
2378
+ conductivity: 0.45,
2379
+ heatCapacity: 3
2380
+ },
2381
+ visual: {
2382
+ glows: true
2383
+ },
2384
+ tags: [
2385
+ "metal",
2386
+ "conductive"
2387
+ ],
2388
+ compressive: 15e5
2389
+ },
2390
+ {
2391
+ id: "aluminium",
2392
+ density: 2700,
2393
+ hpPerKg: 250,
2394
+ penetration: 60,
2395
+ ductility: 0.85,
2396
+ fracture: "none",
2397
+ damageScale: {
2398
+ slash: 0.2
2399
+ },
2400
+ hitDebris: "sparks",
2401
+ breakDebris: "sparks",
2402
+ ricochetDeg: 15,
2403
+ thermal: {
2404
+ meltC: 660,
2405
+ meltRate: 0.06,
2406
+ conductivity: 0.8,
2407
+ heatCapacity: 1.5
2408
+ },
2409
+ visual: {
2410
+ glows: true
2411
+ },
2412
+ tags: [
2413
+ "metal",
2414
+ "conductive"
2415
+ ],
2416
+ compressive: 8e5
2417
+ },
2418
+ {
2419
+ id: "copper",
2420
+ density: 8960,
2421
+ hpPerKg: 300,
2422
+ penetration: 70,
2423
+ ductility: 0.95,
2424
+ fracture: "none",
2425
+ damageScale: {
2426
+ slash: 0.2
2427
+ },
2428
+ hitDebris: "sparks",
2429
+ breakDebris: "sparks",
2430
+ ricochetDeg: 15,
2431
+ thermal: {
2432
+ meltC: 1085,
2433
+ meltRate: 0.05,
2434
+ conductivity: 1,
2435
+ heatCapacity: 2.5
2436
+ },
2437
+ visual: {
2438
+ glows: true
2439
+ },
2440
+ tags: [
2441
+ "metal",
2442
+ "conductive"
2443
+ ],
2444
+ compressive: 1e6
2445
+ },
2446
+ {
2447
+ id: "wood_soft",
2448
+ density: 500,
2449
+ hpPerKg: 90,
2450
+ penetration: 9,
2451
+ ductility: 0.35,
2452
+ fracture: "splinter",
2453
+ fragmentSize: [
2454
+ 0.03,
2455
+ 0.15
2456
+ ],
2457
+ damageScale: {
2458
+ blunt: 1.2,
2459
+ slash: 1.5
2460
+ },
2461
+ hitDebris: "splinters",
2462
+ breakDebris: "splinters",
2463
+ thermal: {
2464
+ ignitionC: 300,
2465
+ flameC: 800,
2466
+ fuelFraction: 0.9,
2467
+ burnRate: 0.012,
2468
+ conductivity: 0.05,
2469
+ heatCapacity: 1.2,
2470
+ smoke: "grey"
2471
+ },
2472
+ visual: {
2473
+ charColor: 1709072
2474
+ },
2475
+ tags: [
2476
+ "wood",
2477
+ "organic",
2478
+ "flammable"
2479
+ ],
2480
+ compressive: 1e5
2481
+ },
2482
+ {
2483
+ id: "wood_hard",
2484
+ density: 750,
2485
+ hpPerKg: 140,
2486
+ penetration: 14,
2487
+ ductility: 0.3,
2488
+ fracture: "splinter",
2489
+ fragmentSize: [
2490
+ 0.03,
2491
+ 0.2
2492
+ ],
2493
+ damageScale: {
2494
+ slash: 1.2
2495
+ },
2496
+ hitDebris: "splinters",
2497
+ breakDebris: "splinters",
2498
+ thermal: {
2499
+ ignitionC: 330,
2500
+ flameC: 800,
2501
+ fuelFraction: 0.9,
2502
+ burnRate: 0.01,
2503
+ conductivity: 0.06,
2504
+ heatCapacity: 1.4,
2505
+ smoke: "grey"
2506
+ },
2507
+ visual: {
2508
+ charColor: 1709072
2509
+ },
2510
+ tags: [
2511
+ "wood",
2512
+ "organic",
2513
+ "flammable"
2514
+ ],
2515
+ compressive: 15e4
2516
+ },
2517
+ {
2518
+ id: "plywood",
2519
+ density: 600,
2520
+ hpPerKg: 70,
2521
+ penetration: 7,
2522
+ ductility: 0.3,
2523
+ fracture: "snap",
2524
+ fragmentSize: [
2525
+ 0.1,
2526
+ 0.4
2527
+ ],
2528
+ damageScale: {
2529
+ slash: 1.3
2530
+ },
2531
+ hitDebris: "splinters",
2532
+ breakDebris: "splinters",
2533
+ thermal: {
2534
+ ignitionC: 300,
2535
+ flameC: 800,
2536
+ fuelFraction: 0.85,
2537
+ burnRate: 0.013,
2538
+ conductivity: 0.05,
2539
+ heatCapacity: 1.2,
2540
+ smoke: "grey"
2541
+ },
2542
+ visual: {
2543
+ charColor: 1709072
2544
+ },
2545
+ tags: [
2546
+ "wood",
2547
+ "organic",
2548
+ "flammable"
2549
+ ],
2550
+ compressive: 6e4
2551
+ },
2552
+ {
2553
+ id: "mdf",
2554
+ density: 750,
2555
+ hpPerKg: 45,
2556
+ penetration: 6,
2557
+ ductility: 0.15,
2558
+ fracture: "snap",
2559
+ fragmentSize: [
2560
+ 0.08,
2561
+ 0.35
2562
+ ],
2563
+ hitDebris: "chips",
2564
+ breakDebris: "dust",
2565
+ thermal: {
2566
+ ignitionC: 290,
2567
+ flameC: 750,
2568
+ fuelFraction: 0.85,
2569
+ burnRate: 0.014,
2570
+ conductivity: 0.05,
2571
+ heatCapacity: 1.2,
2572
+ smoke: "grey"
2573
+ },
2574
+ visual: {
2575
+ charColor: 1709072
2576
+ },
2577
+ tags: [
2578
+ "wood",
2579
+ "organic",
2580
+ "flammable"
2581
+ ],
2582
+ compressive: 3e4
2583
+ },
2584
+ {
2585
+ id: "glass",
2586
+ density: 2500,
2587
+ hpPerKg: 12,
2588
+ penetration: 6,
2589
+ ductility: 0.02,
2590
+ fracture: "shatter",
2591
+ fragmentSize: [
2592
+ 0.05,
2593
+ 0.25
2594
+ ],
2595
+ damageScale: {
2596
+ blunt: 1.5
2597
+ },
2598
+ hitDebris: "shards",
2599
+ breakDebris: "shards",
2600
+ ricochetDeg: 8,
2601
+ thermal: {
2602
+ meltC: 1400,
2603
+ meltRate: 0.03,
2604
+ conductivity: 0.1,
2605
+ heatCapacity: 1.5,
2606
+ thermalShockC: 150
2607
+ },
2608
+ visual: {
2609
+ glows: true,
2610
+ transparent: true
2611
+ },
2612
+ tags: [
2613
+ "glass",
2614
+ "brittle"
2615
+ ],
2616
+ compressive: 1e5
2617
+ },
2618
+ {
2619
+ id: "tempered_glass",
2620
+ density: 2500,
2621
+ hpPerKg: 30,
2622
+ penetration: 9,
2623
+ ductility: 0.02,
2624
+ fracture: "crumble",
2625
+ fragmentSize: [
2626
+ 6e-3,
2627
+ 0.02
2628
+ ],
2629
+ hitDebris: "shards",
2630
+ breakDebris: "shards",
2631
+ ricochetDeg: 8,
2632
+ thermal: {
2633
+ meltC: 1400,
2634
+ meltRate: 0.03,
2635
+ conductivity: 0.1,
2636
+ heatCapacity: 1.5,
2637
+ thermalShockC: 250
2638
+ },
2639
+ visual: {
2640
+ glows: true,
2641
+ transparent: true
2642
+ },
2643
+ tags: [
2644
+ "glass",
2645
+ "brittle"
2646
+ ],
2647
+ compressive: 2e5
2648
+ },
2649
+ {
2650
+ id: "ceramic",
2651
+ density: 2400,
2652
+ hpPerKg: 25,
2653
+ penetration: 12,
2654
+ ductility: 0.02,
2655
+ fracture: "shatter",
2656
+ fragmentSize: [
2657
+ 0.03,
2658
+ 0.12
2659
+ ],
2660
+ damageScale: {
2661
+ blunt: 1.3
2662
+ },
2663
+ hitDebris: "chips",
2664
+ breakDebris: "shards",
2665
+ thermal: {
2666
+ conductivity: 0.1,
2667
+ heatCapacity: 1.5,
2668
+ thermalShockC: 200
2669
+ },
2670
+ tags: [
2671
+ "ceramic",
2672
+ "brittle"
2673
+ ],
2674
+ compressive: 15e4
2675
+ },
2676
+ {
2677
+ id: "concrete",
2678
+ density: 2300,
2679
+ hpPerKg: 14,
2680
+ penetration: 18,
2681
+ ductility: 0.03,
2682
+ fracture: "crumble",
2683
+ fragmentSize: [
2684
+ 0.05,
2685
+ 0.25
2686
+ ],
2687
+ damageScale: {
2688
+ explosive: 1.2
2689
+ },
2690
+ hitDebris: "chips",
2691
+ breakDebris: "dust",
2692
+ thermal: {
2693
+ conductivity: 0.15,
2694
+ heatCapacity: 2.5
2695
+ },
2696
+ tags: [
2697
+ "masonry",
2698
+ "brittle"
2699
+ ],
2700
+ compressive: 444e3
2701
+ },
2702
+ {
2703
+ id: "brick",
2704
+ density: 1900,
2705
+ hpPerKg: 12,
2706
+ penetration: 14,
2707
+ ductility: 0.05,
2708
+ fracture: "crumble",
2709
+ fragmentSize: [
2710
+ 0.04,
2711
+ 0.12
2712
+ ],
2713
+ hitDebris: "chips",
2714
+ breakDebris: "dust",
2715
+ thermal: {
2716
+ conductivity: 0.15,
2717
+ heatCapacity: 2.5
2718
+ },
2719
+ tags: [
2720
+ "masonry",
2721
+ "brittle"
2722
+ ],
2723
+ compressive: 4e4
2724
+ },
2725
+ {
2726
+ id: "plaster",
2727
+ density: 850,
2728
+ hpPerKg: 18,
2729
+ penetration: 3,
2730
+ ductility: 0.05,
2731
+ fracture: "crumble",
2732
+ fragmentSize: [
2733
+ 0.05,
2734
+ 0.3
2735
+ ],
2736
+ hitDebris: "dust",
2737
+ breakDebris: "dust",
2738
+ thermal: {
2739
+ conductivity: 0.1,
2740
+ heatCapacity: 1.5
2741
+ },
2742
+ tags: [
2743
+ "masonry",
2744
+ "brittle"
2745
+ ],
2746
+ compressive: 8e3
2747
+ },
2748
+ {
2749
+ id: "stone",
2750
+ density: 2700,
2751
+ hpPerKg: 25,
2752
+ penetration: 25,
2753
+ ductility: 0.03,
2754
+ fracture: "crumble",
2755
+ fragmentSize: [
2756
+ 0.06,
2757
+ 0.3
2758
+ ],
2759
+ hitDebris: "chips",
2760
+ breakDebris: "dust",
2761
+ thermal: {
2762
+ conductivity: 0.2,
2763
+ heatCapacity: 2.5
2764
+ },
2765
+ tags: [
2766
+ "masonry",
2767
+ "brittle"
2768
+ ],
2769
+ compressive: 3e5
2770
+ },
2771
+ {
2772
+ id: "plastic",
2773
+ density: 1100,
2774
+ hpPerKg: 60,
2775
+ penetration: 8,
2776
+ ductility: 0.5,
2777
+ fracture: "snap",
2778
+ fragmentSize: [
2779
+ 0.05,
2780
+ 0.3
2781
+ ],
2782
+ hitDebris: "chips",
2783
+ breakDebris: "chips",
2784
+ thermal: {
2785
+ ignitionC: 400,
2786
+ flameC: 700,
2787
+ fuelFraction: 0.8,
2788
+ burnRate: 0.02,
2789
+ meltC: 200,
2790
+ meltRate: 0.06,
2791
+ conductivity: 0.05,
2792
+ heatCapacity: 1,
2793
+ smoke: "black"
2794
+ },
2795
+ visual: {
2796
+ charColor: 855309
2797
+ },
2798
+ tags: [
2799
+ "plastic",
2800
+ "flammable"
2801
+ ],
2802
+ compressive: 4e4
2803
+ },
2804
+ {
2805
+ id: "rubber",
2806
+ density: 1100,
2807
+ hpPerKg: 80,
2808
+ penetration: 12,
2809
+ ductility: 0.9,
2810
+ fracture: "tear",
2811
+ fragmentSize: [
2812
+ 0.05,
2813
+ 0.2
2814
+ ],
2815
+ hitDebris: "chips",
2816
+ breakDebris: "fibers",
2817
+ thermal: {
2818
+ ignitionC: 350,
2819
+ flameC: 750,
2820
+ fuelFraction: 0.6,
2821
+ burnRate: 0.015,
2822
+ conductivity: 0.05,
2823
+ heatCapacity: 1.2,
2824
+ smoke: "black"
2825
+ },
2826
+ visual: {
2827
+ charColor: 855309
2828
+ },
2829
+ tags: [
2830
+ "rubber",
2831
+ "flammable"
2832
+ ],
2833
+ compressive: 2e4
2834
+ },
2835
+ {
2836
+ id: "fabric",
2837
+ density: 400,
2838
+ hpPerKg: 20,
2839
+ penetration: 0.8,
2840
+ ductility: 0.8,
2841
+ fracture: "tear",
2842
+ fragmentSize: [
2843
+ 0.05,
2844
+ 0.25
2845
+ ],
2846
+ damageScale: {
2847
+ slash: 4,
2848
+ ballistic: 0.5
2849
+ },
2850
+ hitDebris: "fibers",
2851
+ breakDebris: "fibers",
2852
+ thermal: {
2853
+ ignitionC: 230,
2854
+ flameC: 600,
2855
+ fuelFraction: 1,
2856
+ burnRate: 0.05,
2857
+ conductivity: 0.03,
2858
+ heatCapacity: 0.6,
2859
+ smoke: "grey"
2860
+ },
2861
+ visual: {
2862
+ charColor: 1709072
2863
+ },
2864
+ tags: [
2865
+ "fabric",
2866
+ "organic",
2867
+ "flammable"
2868
+ ],
2869
+ compressive: 500
2870
+ },
2871
+ {
2872
+ id: "foam",
2873
+ density: 30,
2874
+ hpPerKg: 15,
2875
+ penetration: 0.5,
2876
+ ductility: 0.7,
2877
+ fracture: "tear",
2878
+ fragmentSize: [
2879
+ 0.03,
2880
+ 0.15
2881
+ ],
2882
+ damageScale: {
2883
+ slash: 2,
2884
+ ballistic: 0.4
2885
+ },
2886
+ hitDebris: "fibers",
2887
+ breakDebris: "fibers",
2888
+ thermal: {
2889
+ ignitionC: 250,
2890
+ flameC: 900,
2891
+ fuelFraction: 0.95,
2892
+ burnRate: 0.09,
2893
+ conductivity: 0.02,
2894
+ heatCapacity: 0.5,
2895
+ smoke: "black"
2896
+ },
2897
+ visual: {
2898
+ charColor: 855309
2899
+ },
2900
+ tags: [
2901
+ "fabric",
2902
+ "flammable"
2903
+ ],
2904
+ compressive: 100
2905
+ },
2906
+ {
2907
+ id: "leather",
2908
+ density: 900,
2909
+ hpPerKg: 50,
2910
+ penetration: 4,
2911
+ ductility: 0.8,
2912
+ fracture: "tear",
2913
+ fragmentSize: [
2914
+ 0.05,
2915
+ 0.25
2916
+ ],
2917
+ damageScale: {
2918
+ slash: 2
2919
+ },
2920
+ hitDebris: "fibers",
2921
+ breakDebris: "fibers",
2922
+ thermal: {
2923
+ ignitionC: 320,
2924
+ flameC: 650,
2925
+ fuelFraction: 0.7,
2926
+ burnRate: 0.01,
2927
+ conductivity: 0.05,
2928
+ heatCapacity: 1,
2929
+ smoke: "grey"
2930
+ },
2931
+ visual: {
2932
+ charColor: 1709072
2933
+ },
2934
+ tags: [
2935
+ "fabric",
2936
+ "organic",
2937
+ "flammable"
2938
+ ],
2939
+ compressive: 2e3
2940
+ },
2941
+ {
2942
+ id: "paper",
2943
+ density: 700,
2944
+ hpPerKg: 8,
2945
+ penetration: 0.3,
2946
+ ductility: 0.5,
2947
+ fracture: "tear",
2948
+ fragmentSize: [
2949
+ 0.03,
2950
+ 0.15
2951
+ ],
2952
+ damageScale: {
2953
+ slash: 3
2954
+ },
2955
+ hitDebris: "fibers",
2956
+ breakDebris: "fibers",
2957
+ thermal: {
2958
+ ignitionC: 230,
2959
+ flameC: 600,
2960
+ fuelFraction: 1,
2961
+ burnRate: 0.06,
2962
+ conductivity: 0.03,
2963
+ heatCapacity: 0.5,
2964
+ smoke: "grey"
2965
+ },
2966
+ visual: {
2967
+ charColor: 1709072
2968
+ },
2969
+ tags: [
2970
+ "fabric",
2971
+ "organic",
2972
+ "flammable"
2973
+ ],
2974
+ compressive: 500
2975
+ },
2976
+ {
2977
+ id: "foliage",
2978
+ density: 150,
2979
+ hpPerKg: 20,
2980
+ penetration: 0.5,
2981
+ ductility: 0.6,
2982
+ fracture: "tear",
2983
+ fragmentSize: [
2984
+ 0.03,
2985
+ 0.1
2986
+ ],
2987
+ damageScale: {
2988
+ slash: 4
2989
+ },
2990
+ hitDebris: "leaves",
2991
+ breakDebris: "leaves",
2992
+ thermal: {
2993
+ ignitionC: 300,
2994
+ flameC: 700,
2995
+ fuelFraction: 0.8,
2996
+ burnRate: 0.08,
2997
+ conductivity: 0.03,
2998
+ heatCapacity: 0.8,
2999
+ smoke: "white"
3000
+ },
3001
+ visual: {
3002
+ charColor: 1709072
3003
+ },
3004
+ tags: [
3005
+ "plant",
3006
+ "organic",
3007
+ "flammable"
3008
+ ],
3009
+ compressive: 500
3010
+ },
3011
+ {
3012
+ id: "green_wood",
3013
+ density: 650,
3014
+ hpPerKg: 5,
3015
+ penetration: 10,
3016
+ ductility: 0.5,
3017
+ fracture: "splinter",
3018
+ fragmentSize: [
3019
+ 0.03,
3020
+ 0.2
3021
+ ],
3022
+ damageScale: {
3023
+ slash: 1.3,
3024
+ ballistic: 0.1
3025
+ },
3026
+ hitDebris: "splinters",
3027
+ breakDebris: "leaves",
3028
+ thermal: {
3029
+ ignitionC: 400,
3030
+ flameC: 700,
3031
+ fuelFraction: 0.8,
3032
+ burnRate: 6e-3,
3033
+ conductivity: 0.06,
3034
+ heatCapacity: 2,
3035
+ smoke: "white"
3036
+ },
3037
+ visual: {
3038
+ charColor: 1709072
3039
+ },
3040
+ tags: [
3041
+ "plant",
3042
+ "wood",
3043
+ "organic",
3044
+ "flammable"
3045
+ ],
3046
+ compressive: 6e4
3047
+ },
3048
+ {
3049
+ id: "cactus",
3050
+ density: 900,
3051
+ hpPerKg: 30,
3052
+ penetration: 2,
3053
+ ductility: 0.6,
3054
+ fracture: "tear",
3055
+ fragmentSize: [
3056
+ 0.04,
3057
+ 0.15
3058
+ ],
3059
+ damageScale: {
3060
+ slash: 3
3061
+ },
3062
+ hitDebris: "fibers",
3063
+ breakDebris: "fibers",
3064
+ thermal: {
3065
+ ignitionC: 450,
3066
+ flameC: 650,
3067
+ fuelFraction: 0.3,
3068
+ burnRate: 0.01,
3069
+ conductivity: 0.05,
3070
+ heatCapacity: 3,
3071
+ smoke: "white"
3072
+ },
3073
+ visual: {
3074
+ charColor: 1709072
3075
+ },
3076
+ tags: [
3077
+ "plant",
3078
+ "organic",
3079
+ "flammable"
3080
+ ],
3081
+ compressive: 5e3
3082
+ },
3083
+ {
3084
+ id: "flesh_fruit",
3085
+ density: 950,
3086
+ hpPerKg: 15,
3087
+ penetration: 1.5,
3088
+ ductility: 0.7,
3089
+ fracture: "tear",
3090
+ fragmentSize: [
3091
+ 0.02,
3092
+ 0.08
3093
+ ],
3094
+ damageScale: {
3095
+ slash: 2
3096
+ },
3097
+ hitDebris: "drips",
3098
+ breakDebris: "drips",
3099
+ thermal: {
3100
+ conductivity: 0.05,
3101
+ heatCapacity: 3
3102
+ },
3103
+ tags: [
3104
+ "plant",
3105
+ "organic"
3106
+ ],
3107
+ compressive: 1e3
3108
+ },
3109
+ {
3110
+ id: "wax",
3111
+ density: 900,
3112
+ hpPerKg: 30,
3113
+ penetration: 3,
3114
+ ductility: 0.5,
3115
+ fracture: "snap",
3116
+ fragmentSize: [
3117
+ 0.05,
3118
+ 0.2
3119
+ ],
3120
+ hitDebris: "chips",
3121
+ breakDebris: "chips",
3122
+ thermal: {
3123
+ ignitionC: 230,
3124
+ flameC: 700,
3125
+ fuelFraction: 0.9,
3126
+ burnRate: 0.02,
3127
+ meltC: 60,
3128
+ meltRate: 0.2,
3129
+ conductivity: 0.05,
3130
+ heatCapacity: 0.8,
3131
+ smoke: "grey"
3132
+ },
3133
+ tags: [
3134
+ "wax",
3135
+ "flammable"
3136
+ ],
3137
+ compressive: 5e3
3138
+ },
3139
+ {
3140
+ id: "soil",
3141
+ density: 1300,
3142
+ hpPerKg: 3,
3143
+ penetration: 5,
3144
+ ductility: 0.3,
3145
+ fracture: "crumble",
3146
+ fragmentSize: [
3147
+ 0.03,
3148
+ 0.12
3149
+ ],
3150
+ hitDebris: "dust",
3151
+ breakDebris: "dust",
3152
+ thermal: {
3153
+ conductivity: 0.1,
3154
+ heatCapacity: 2.5
3155
+ },
3156
+ tags: [
3157
+ "soil",
3158
+ "masonry"
3159
+ ],
3160
+ compressive: 2e4
3161
+ }
3162
+ ];
3163
+ var material_hints_default = {
3164
+ "cast iron": "cast_iron",
3165
+ cast_iron: "cast_iron",
3166
+ castiron: "cast_iron",
3167
+ metal: "steel",
3168
+ steel: "steel",
3169
+ iron: "steel",
3170
+ chrome: "steel",
3171
+ alumin: "aluminium",
3172
+ alu_: "aluminium",
3173
+ "alu-": "aluminium",
3174
+ "alu ": "aluminium",
3175
+ copper: "copper",
3176
+ brass: "copper",
3177
+ bronze: "copper",
3178
+ wood: "wood_soft",
3179
+ timber: "wood_soft",
3180
+ plank: "wood_soft",
3181
+ lumber: "wood_soft",
3182
+ pine: "wood_soft",
3183
+ hardwood: "wood_hard",
3184
+ oak: "wood_hard",
3185
+ plywood: "plywood",
3186
+ mdf: "mdf",
3187
+ chipboard: "mdf",
3188
+ glass: "glass",
3189
+ window: "glass",
3190
+ mirror: "glass",
3191
+ tempered: "tempered_glass",
3192
+ windshield: "tempered_glass",
3193
+ fabric: "fabric",
3194
+ cloth: "fabric",
3195
+ cushion: "fabric",
3196
+ sofa: "fabric",
3197
+ curtain: "fabric",
3198
+ carpet: "fabric",
3199
+ textile: "fabric",
3200
+ canvas: "fabric",
3201
+ foam: "foam",
3202
+ sponge: "foam",
3203
+ mattress: "foam",
3204
+ leather: "leather",
3205
+ paper: "paper",
3206
+ cardboard: "paper",
3207
+ leaf: "foliage",
3208
+ leaves: "foliage",
3209
+ foliage: "foliage",
3210
+ bush: "foliage",
3211
+ grass: "foliage",
3212
+ trunk: "green_wood",
3213
+ branch: "green_wood",
3214
+ twig: "green_wood",
3215
+ cactus: "cactus",
3216
+ fruit: "flesh_fruit",
3217
+ apple: "flesh_fruit",
3218
+ melon: "flesh_fruit",
3219
+ wax: "wax",
3220
+ candle: "wax",
3221
+ concrete: "concrete",
3222
+ cement: "concrete",
3223
+ brick: "brick",
3224
+ tile: "ceramic",
3225
+ ceramic: "ceramic",
3226
+ porcelain: "ceramic",
3227
+ stone: "stone",
3228
+ rock: "stone",
3229
+ marble: "stone",
3230
+ plaster: "plaster",
3231
+ drywall: "plaster",
3232
+ gypsum: "plaster",
3233
+ plastic: "plastic",
3234
+ pvc: "plastic",
3235
+ acrylic: "plastic",
3236
+ rubber: "rubber",
3237
+ tire: "rubber",
3238
+ tyre: "rubber",
3239
+ soil: "soil",
3240
+ dirt: "soil",
3241
+ potting: "soil",
3242
+ mud: "soil"
3243
+ };
3244
+ var MATERIALS = materials_default;
3245
+ var MATERIAL_MAP = new Map(MATERIALS.map((m) => [m.id, m]));
3246
+ var HINTS = Object.entries(material_hints_default).sort((x, y) => y[0].length - x[0].length);
3247
+ var assemblies_default = [
3248
+ {
3249
+ id: "brick_wall_plastered",
3250
+ name: "Plastered brick wall",
3251
+ core: { material: "brick", thickness: 0.09 },
3252
+ layers: [{ material: "plaster", thickness: 0.02, faces: "both" }],
3253
+ units: { size: [0.23, 0.075, 0.11], bond: "running", mortar: "plaster" },
3254
+ breach: { area: 0.25 }
3255
+ },
3256
+ {
3257
+ id: "concrete_block_wall",
3258
+ name: "Concrete block wall",
3259
+ core: { material: "concrete", thickness: 0.14 },
3260
+ fill: 0.55,
3261
+ units: { size: [0.39, 0.19, 0.14], bond: "running", mortar: "plaster" },
3262
+ breach: { area: 0.25 }
3263
+ },
3264
+ {
3265
+ id: "reinforced_concrete",
3266
+ name: "Reinforced concrete wall",
3267
+ core: { material: "concrete", thickness: 0.2 },
3268
+ reinforcement: { material: "steel", spacing: 0.18, diameter: 8e-3, residualCapacity: 0.15 },
3269
+ units: { size: [0.6, 0.42, 0.2], bond: "stack", mortar: "concrete" },
3270
+ breach: { area: 0.25 }
3271
+ },
3272
+ {
3273
+ id: "drywall",
3274
+ name: "Drywall partition on steel studs",
3275
+ core: { material: "plaster", thickness: 0.0125 },
3276
+ layers: [{ material: "plaster", thickness: 0.0125, faces: "+" }],
3277
+ depth: 0.095,
3278
+ breach: { area: 0.25 }
3279
+ },
3280
+ {
3281
+ id: "wood_frame_wall",
3282
+ name: "Wood frame wall",
3283
+ core: { material: "plywood", thickness: 0.012 },
3284
+ layers: [{ material: "plaster", thickness: 0.0125, faces: "-" }],
3285
+ depth: 0.1135,
3286
+ breach: { area: 0.25 }
3287
+ },
3288
+ {
3289
+ id: "log_wall",
3290
+ name: "Log wall",
3291
+ core: { material: "wood_soft", thickness: 0.2 },
3292
+ fill: 0.785,
3293
+ breach: { area: 0.25 }
3294
+ },
3295
+ {
3296
+ id: "sheet_metal_shack",
3297
+ name: "Corrugated sheet metal wall",
3298
+ core: { material: "steel", thickness: 6e-4 },
3299
+ depth: 0.02,
3300
+ breach: { area: 0.25 }
3301
+ },
3302
+ {
3303
+ id: "glass_pane",
3304
+ name: "Glass pane",
3305
+ core: { material: "glass", thickness: 6e-3 },
3306
+ breach: { area: 0.1 }
3307
+ }
3308
+ ];
3309
+ var ASSEMBLIES = assemblies_default;
3310
+ var getAssembly = (id) => ASSEMBLIES.find((a) => a.id === id);
3311
+ var faceCount = (layer) => layer.faces === "both" ? 2 : 1;
3312
+ var assemblyDepth = (def) => def.depth ?? def.core.thickness + (def.layers ?? []).reduce((sum, l) => sum + l.thickness * faceCount(l), 0);
3313
+ var assemblyPart = (assemblyId, options) => {
3314
+ const def = getAssembly(assemblyId);
3315
+ if (!def) throw new Error(`unknown assembly "${assemblyId}"`);
3316
+ const densityOf2 = (id) => {
3317
+ const m = options.materials?.get(id) ?? MATERIALS.find((x) => x.id === id);
3318
+ if (!m) throw new Error(`unknown material "${id}" in assembly "${assemblyId}"`);
3319
+ return m.density;
3320
+ };
3321
+ const fill = def.fill ?? 1;
3322
+ const area = options.width * options.height;
3323
+ const coreMass = area * def.core.thickness * fill * densityOf2(def.core.material);
3324
+ const layerMass = (def.layers ?? []).reduce((sum, l) => sum + area * l.thickness * faceCount(l) * densityOf2(l.material), 0);
3325
+ return {
3326
+ id: options.id,
3327
+ material: def.core.material,
3328
+ shape: { kind: "box", size: [options.width, options.height, assemblyDepth(def)] },
3329
+ position: options.position,
3330
+ ...options.rotation ? { rotation: options.rotation } : {},
3331
+ mass: coreMass + layerMass,
3332
+ thickness: def.core.thickness * fill,
3333
+ ...def.layers ? { layers: def.layers.map((l) => ({ ...l })) } : {},
3334
+ ...def.units ? { units: { ...def.units, size: [...def.units.size] } } : {},
3335
+ ...def.reinforcement ? { reinforcement: { ...def.reinforcement } } : {},
3336
+ ...def.breach ? { breach: { ...def.breach } } : {},
3337
+ ...options.structure ? { structure: options.structure } : {},
3338
+ ...options.extra
3339
+ };
3340
+ };
3341
+
3342
+ // src/loads.ts
3343
+ function loadsAtRest(parts, supports, without) {
3344
+ const byId = new Map(parts.filter((part) => part.id !== without).map((part) => [part.id, part]));
3345
+ const resting = /* @__PURE__ */ new Map();
3346
+ const carrying = /* @__PURE__ */ new Map();
3347
+ const push = (map, key, value) => {
3348
+ const list = map.get(key);
3349
+ if (list) list.push(value);
3350
+ else map.set(key, [value]);
3351
+ };
3352
+ for (const support of supports) {
3353
+ if (support.bears === false) continue;
3354
+ if (!byId.has(support.part) || support.on !== GROUND && !byId.has(support.on)) continue;
3355
+ push(resting, support.part, support.on);
3356
+ push(carrying, support.on, support.part);
3357
+ }
3358
+ const partition = (id) => byId.get(id)?.role === "partition";
3359
+ const structural = (id) => byId.has(id) && byId.get(id).role !== "none";
3360
+ const distance = /* @__PURE__ */ new Map([[GROUND, 0]]);
3361
+ const queue = [GROUND];
3362
+ for (let head = 0; head < queue.length; head++) {
3363
+ const current = queue[head];
3364
+ if (current !== GROUND && partition(current)) continue;
3365
+ for (const above of carrying.get(current) ?? []) {
3366
+ if (distance.has(above) || !structural(above)) continue;
3367
+ distance.set(above, distance.get(current) + 1);
3368
+ queue.push(above);
3369
+ }
3370
+ }
3371
+ const incoming = /* @__PURE__ */ new Map();
3372
+ const loads = /* @__PURE__ */ new Map();
3373
+ for (let head = queue.length - 1; head >= 1; head--) {
3374
+ const id = queue[head];
3375
+ const total = (byId.get(id).mass || 1) + (incoming.get(id) ?? 0);
3376
+ loads.set(id, total);
3377
+ const below = (resting.get(id) ?? []).filter((other) => distance.has(other) && distance.get(other) < distance.get(id) && !(other !== GROUND && partition(other)));
3378
+ for (const other of below) incoming.set(other, (incoming.get(other) ?? 0) + total / below.length);
3379
+ }
3380
+ return loads;
3381
+ }
3382
+ var isBearing = (part) => part.role === "column" || part.role === "wall";
3383
+ function sizeCapacities(parts, supports, options) {
3384
+ const loads = loadsAtRest(parts, supports);
3385
+ const worst = /* @__PURE__ */ new Map();
3386
+ for (const part of parts.filter(isBearing)) {
3387
+ for (const [id, load] of loadsAtRest(parts, supports, part.id)) {
3388
+ const before = loads.get(id);
3389
+ if (before && load / before > (worst.get(id) ?? 1)) worst.set(id, load / before);
3390
+ }
3391
+ }
3392
+ const capacities = /* @__PURE__ */ new Map();
3393
+ for (const part of parts) {
3394
+ const load = loads.get(part.id);
3395
+ if (load === void 0 || part.role === "none") continue;
3396
+ const factor = isBearing(part) ? Math.max(1 / options.safety, (worst.get(part.id) ?? 1) / options.redundancy) : 1 / options.safetyOthers;
3397
+ capacities.set(part.id, Math.max(1, load * factor));
3398
+ }
3399
+ return capacities;
3400
+ }
3401
+
3402
+ // src/wall-detail/apply.ts
3403
+ function applySplits(input, splits) {
3404
+ if (!splits.size) return input;
3405
+ const boxes = new Map(input.parts.map((part) => [part.id, part.aabb]));
3406
+ const pieceBox = /* @__PURE__ */ new Map();
3407
+ for (const split of splits.values()) for (const piece of split.pieces) pieceBox.set(piece.id, piece.aabb);
3408
+ const parts = input.parts.flatMap((part) => splits.get(part.id)?.pieces ?? [part]);
3409
+ const supports = [];
3410
+ for (const support of input.supports) {
3411
+ const own = splits.get(support.part);
3412
+ const host = splits.get(support.on);
3413
+ const resting = boxes.get(support.part);
3414
+ const targets = host ? host.top.filter((id) => resting && touches(pieceBox.get(id), resting, 0.05)) : [support.on];
3415
+ const carriers = host ? targets.length ? targets : host.top : targets;
3416
+ for (const part of own ? own.base : [support.part]) for (const on of carriers) supports.push({ ...support, part, on });
3417
+ }
3418
+ for (const split of splits.values()) supports.push(...split.internal);
3419
+ return { parts, supports };
3420
+ }
3421
+
3422
+ // src/wall-detail/courses.ts
3423
+ var MIN_UNIT = 0.02;
3424
+ function cutAt(from, to, openings, y0, y1) {
3425
+ let stretches = [[from, to]];
3426
+ for (const opening2 of openings.filter((o) => o.y0 < y1 - MIN_UNIT && o.y1 > y0 + MIN_UNIT)) {
3427
+ stretches = stretches.flatMap(([a, b]) => {
3428
+ if (opening2.to <= a || opening2.from >= b) return [[a, b]];
3429
+ return [[a, Math.min(b, opening2.from)], [Math.max(a, opening2.to), b]].filter(([x, y]) => y - x > MIN_UNIT);
3430
+ });
3431
+ }
3432
+ return stretches;
3433
+ }
3434
+ function unitsOf(from, to, row, spec) {
3435
+ const length = spec.unitLength;
3436
+ if (!length) return [[from, to]];
3437
+ const offset = spec.bond === "running" && row % 2 === 1 ? length / 2 : 0;
3438
+ const cuts = [from];
3439
+ for (let at2 = from + (offset || length); at2 < to - MIN_UNIT; at2 += length) cuts.push(at2);
3440
+ cuts.push(to);
3441
+ return cuts.slice(0, -1).map((start, k) => [start, cuts[k + 1]]).filter(([a, b]) => b - a > MIN_UNIT);
3442
+ }
3443
+ function splitIntoCourses(wall, openings, spec) {
3444
+ const alongU = wall.aabb[2] - wall.aabb[0] >= wall.aabb[3] - wall.aabb[1];
3445
+ const from = alongU ? wall.aabb[0] : wall.aabb[1];
3446
+ const to = alongU ? wall.aabb[2] : wall.aabb[3];
3447
+ const cross = alongU ? [wall.aabb[1], wall.aabb[3]] : [wall.aabb[0], wall.aabb[2]];
3448
+ const rows = Math.max(1, Math.round((wall.y1 - wall.y0) / spec.rowHeight));
3449
+ const rowHeight = (wall.y1 - wall.y0) / rows;
3450
+ const cells = [];
3451
+ for (let row = 0; row < rows; row++) {
3452
+ const y0 = wall.y0 + row * rowHeight;
3453
+ const y1 = wall.y0 + (row + 1) * rowHeight;
3454
+ for (const [a, b] of cutAt(from, to, openings.filter((o) => o.wall === wall.id), y0, y1)) for (const [x0, x1] of unitsOf(a, b, row, spec)) cells.push({ from: x0, to: x1, row, y0, y1 });
3455
+ }
3456
+ const total = cells.reduce((sum, cell) => sum + (cell.to - cell.from) * (cell.y1 - cell.y0), 0);
3457
+ const box = (cell) => alongU ? [cell.from, cross[0], cell.to, cross[1]] : [cross[0], cell.from, cross[1], cell.to];
3458
+ const pieces = cells.map((cell, index) => ({
3459
+ id: `${wall.id}_${spec.unitLength ? "u" : "log"}${cell.row}_${index}`,
3460
+ role: wall.role,
3461
+ material: wall.material,
3462
+ aabb: box(cell),
3463
+ y0: cell.y0,
3464
+ y1: cell.y1,
3465
+ floor: wall.floor,
3466
+ mass: round2(Math.max(0.1, wall.mass * (cell.to - cell.from) * (cell.y1 - cell.y0) / total)),
3467
+ host: wall.id,
3468
+ assembly: spec.assembly,
3469
+ unit: true
3470
+ }));
3471
+ const internal = [];
3472
+ cells.forEach((cell, index) => {
3473
+ if (cell.row === 0) return;
3474
+ const below = cells.map((other, k) => ({ other, k })).filter(({ other }) => other.row === cell.row - 1 && Math.min(other.to, cell.to) - Math.max(other.from, cell.from) > MIN_UNIT);
3475
+ for (const { k } of below) internal.push({ part: pieces[index].id, on: pieces[k].id, joint: spec.joint });
3476
+ });
3477
+ return { pieces, internal, base: pieces.filter((_, i) => cells[i].row === 0).map((p) => p.id), top: pieces.filter((_, i) => cells[i].row === rows - 1).map((p) => p.id) };
3478
+ }
3479
+
3480
+ // src/wall-detail/panels.ts
3481
+ var LINTEL_DEPTH2 = 0.2;
3482
+ var LINTEL_BEARING2 = 0.2;
3483
+ var MIN_PIECE = 0.05;
3484
+ var frameOf = (wall) => {
3485
+ const alongU = wall.aabb[2] - wall.aabb[0] >= wall.aabb[3] - wall.aabb[1];
3486
+ return alongU ? { alongU, from: wall.aabb[0], to: wall.aabb[2], cross: [wall.aabb[1], wall.aabb[3]] } : { alongU, from: wall.aabb[1], to: wall.aabb[3], cross: [wall.aabb[0], wall.aabb[2]] };
3487
+ };
3488
+ var boxOf = (frame, from, to) => frame.alongU ? [from, frame.cross[0], to, frame.cross[1]] : [frame.cross[0], from, frame.cross[1], to];
3489
+ function splitIntoPanels(wall, openings, style) {
3490
+ const frame = frameOf(wall);
3491
+ const own = openings.filter((o) => o.wall === wall.id && o.to > frame.from + MIN_PIECE && o.from < frame.to - MIN_PIECE).sort((a, b) => a.from - b.from);
3492
+ if (!own.length) return { pieces: [wall], internal: [], base: [wall.id], top: [wall.id] };
3493
+ const drafts = [];
3494
+ const piers = [];
3495
+ const lintels = [];
3496
+ const spandrels = [];
3497
+ const height = wall.y1 - wall.y0;
3498
+ let cursor = frame.from;
3499
+ const roleOf2 = wall.role;
3500
+ const addPier = (to) => {
3501
+ if (to - cursor < MIN_PIECE) return;
3502
+ piers.push({ id: `${wall.id}_pier_${piers.length}`, role: roleOf2, material: wall.material, from: cursor, to, y0: wall.y0, y1: wall.y1, volume: (to - cursor) * height });
3503
+ };
3504
+ own.forEach((opening2, k) => {
3505
+ addPier(Math.max(cursor, opening2.from));
3506
+ const lintelTop = Math.min(wall.y1, opening2.y1 + LINTEL_DEPTH2);
3507
+ const lintelFrom = Math.max(frame.from, opening2.from - LINTEL_BEARING2);
3508
+ const lintelTo = Math.min(frame.to, opening2.to + LINTEL_BEARING2);
3509
+ const lintelHeight = lintelTop - opening2.y1;
3510
+ if (opening2.y0 - wall.y0 > MIN_PIECE) drafts.push({ id: `${wall.id}_sill_${k}`, role: "partition", material: wall.material, from: opening2.from, to: opening2.to, y0: wall.y0, y1: opening2.y0, volume: (opening2.to - opening2.from) * (opening2.y0 - wall.y0) });
3511
+ 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 });
3512
+ 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);
3513
+ cursor = opening2.to;
3514
+ });
3515
+ addPier(frame.to);
3516
+ for (const pier of piers) {
3517
+ for (const lintel of lintels) {
3518
+ const overlap = Math.max(0, Math.min(pier.to, lintel.to) - Math.max(pier.from, lintel.from));
3519
+ pier.volume -= overlap * (lintel.y1 - lintel.y0);
3520
+ }
3521
+ }
3522
+ const all = [...piers, ...drafts, ...lintels, ...spandrels.filter((d) => d !== void 0)];
3523
+ const total = all.reduce((sum, d) => sum + d.volume, 0);
3524
+ const pieces = all.map((d) => ({
3525
+ id: d.id,
3526
+ role: d.role,
3527
+ material: d.material,
3528
+ aabb: boxOf(frame, d.from, d.to),
3529
+ y0: d.y0,
3530
+ y1: d.y1,
3531
+ floor: wall.floor,
3532
+ mass: round2(Math.max(0.1, wall.mass * d.volume / total)),
3533
+ host: wall.id,
3534
+ ...d.role === "beam" ? style.lintelAssembly ? { assembly: style.lintelAssembly } : {} : wall.assembly ? { assembly: wall.assembly } : {}
3535
+ }));
3536
+ const internal = [];
3537
+ lintels.forEach((lintel, k) => {
3538
+ 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 });
3539
+ const spandrel = spandrels[k];
3540
+ if (spandrel) internal.push({ part: spandrel.id, on: lintel.id, joint: style.wallJoint });
3541
+ });
3542
+ const base = [...piers, ...drafts.filter((d) => d.id.includes("_sill_"))].map((d) => d.id);
3543
+ const top = [...piers.map((p) => p.id), ...lintels.map((l, k) => spandrels[k]?.id ?? l.id)];
3544
+ return { pieces, internal, base, top };
3545
+ }
3546
+
3547
+ // src/wall-detail/index.ts
3548
+ var LOG_DIAMETER = 0.26;
3549
+ var isWall = (part) => part.id.startsWith("wall_") && part.assembly !== void 0 && (part.role === "wall" || part.role === "partition");
3550
+ function logSpec(construction, part) {
3551
+ const diameter = construction.walls.logDiameter ?? LOG_DIAMETER;
3552
+ return { rowHeight: diameter, bond: "stack", joint: CONSTRUCTION_SYSTEMS.get(construction.system).joints.wall, assembly: part.assembly };
3553
+ }
3554
+ function panelSplits(input, structure) {
3555
+ const { construction } = structure;
3556
+ const system = CONSTRUCTION_SYSTEMS.get(construction.system);
3557
+ const splits = /* @__PURE__ */ new Map();
3558
+ for (const part of input.parts.filter(isWall)) {
3559
+ if (construction.system === "log") {
3560
+ splits.set(part.id, splitIntoCourses(part, structure.openings, logSpec(construction, part)));
3561
+ continue;
3562
+ }
3563
+ const split = splitIntoPanels(part, structure.openings, {
3564
+ lintelMaterial: system.timber ? construction.frame.timber : "concrete",
3565
+ ...system.timber ? {} : { lintelAssembly: "reinforced_concrete" },
3566
+ lintelJoint: system.joints.beam,
3567
+ wallJoint: system.joints.wall
3568
+ });
3569
+ if (split.pieces.length > 1) splits.set(part.id, split);
3570
+ }
3571
+ return splits;
3572
+ }
3573
+ function applyPanels(input, structure, detail) {
3574
+ if (detail === "coarse") return input;
3575
+ return applySplits(input, panelSplits(input, structure));
3576
+ }
3577
+ function applyUnits(input, structure) {
3578
+ const splits = /* @__PURE__ */ new Map();
3579
+ for (const part of input.parts) {
3580
+ if (part.unit || !part.assembly || part.role === "beam" || part.role === "none") continue;
3581
+ const units = getAssembly(part.assembly)?.units;
3582
+ if (!units) continue;
3583
+ const joint = CONSTRUCTION_SYSTEMS.get(structure.construction.system).joints.wall;
3584
+ splits.set(part.id, splitIntoCourses(part, [], { rowHeight: units.size[1], unitLength: units.size[0], bond: units.bond, joint: units.mortar === "concrete" ? "mortar" : joint, assembly: part.assembly }));
3585
+ }
3586
+ const next = applySplits(input, splits);
3587
+ const capacities = new Map(input.capacities);
3588
+ for (const [id, split] of splits) {
3589
+ const capacity = input.capacities.get(id);
3590
+ capacities.delete(id);
3591
+ if (capacity === void 0) continue;
3592
+ const along = (box) => Math.max(box[2] - box[0], box[3] - box[1]);
3593
+ const length = along(input.parts.find((part) => part.id === id).aabb);
3594
+ for (const piece of split.pieces) capacities.set(piece.id, Math.max(1, capacity * along(piece.aabb) / length));
3595
+ }
3596
+ return { ...next, capacities };
3597
+ }
3598
+
3599
+ // src/to-destructible-spec.ts
3600
+ var CONTRACT_ROLE = { column: "column", beam: "beam", slab: "slab", wall: "wall", partition: "partition", roof: "roof", stair: "stair", none: "none" };
3601
+ var TERRAIN_MARGIN = 6;
3602
+ var TERRAIN_THICKNESS = 0.5;
3603
+ var boxSpec = (part, capacity) => {
3604
+ const [u0, v0, u1, v1] = part.aabb;
3605
+ return {
3606
+ id: part.id,
3607
+ material: part.material,
3608
+ shape: { kind: "box", size: [Math.max(0.01, u1 - u0), Math.max(0.01, part.y1 - part.y0), Math.max(0.01, v1 - v0)] },
3609
+ position: [(u0 + u1) / 2, (part.y0 + part.y1) / 2, (v0 + v1) / 2],
3610
+ mass: Math.max(0.1, part.mass || 1),
3611
+ structure: capacity === void 0 ? { role: CONTRACT_ROLE[part.role] } : { role: CONTRACT_ROLE[part.role], capacity }
3612
+ };
3613
+ };
3614
+ var assemblySpec = (part, capacity) => {
3615
+ const [u0, v0, u1, v1] = part.aabb;
3616
+ const alongU = u1 - u0 >= v1 - v0;
3617
+ const spec = assemblyPart(part.assembly, {
3618
+ id: part.id,
3619
+ width: alongU ? u1 - u0 : v1 - v0,
3620
+ height: part.y1 - part.y0,
3621
+ position: [(u0 + u1) / 2, (part.y0 + part.y1) / 2, (v0 + v1) / 2],
3622
+ ...alongU ? {} : { rotation: [0, Math.PI / 2, 0] },
3623
+ structure: capacity === void 0 ? { role: CONTRACT_ROLE[part.role] } : { role: CONTRACT_ROLE[part.role], capacity },
3624
+ extra: { mass: Math.max(0.1, part.mass || 1) }
3625
+ });
3626
+ if (part.unit) {
3627
+ delete spec.units;
3628
+ delete spec.breach;
3629
+ }
3630
+ return spec;
3631
+ };
3632
+ var partSpecOf = (part, capacity, detailed) => detailed && part.assembly ? assemblySpec(part, capacity) : boxSpec(part, capacity);
3633
+ function toDestructibleSpec(structure, options = {}) {
3634
+ const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
3635
+ const kept = structure.parts.filter((part) => !options.filter || options.filter(part));
3636
+ const portico = options.portico ?? (options.wallDetail !== "coarse" && structure.framed);
3637
+ const roled = portico ? kept.map((part) => part.role === "wall" ? { ...part, role: "partition" } : part) : kept;
3638
+ const ids = new Set(roled.map((part) => part.id));
3639
+ const filtered = structure.supports.filter((support) => !options.filter || ids.has(support.part) && (support.on === GROUND || ids.has(support.on)));
3640
+ const detail = options.wallDetail ?? "panels";
3641
+ const panels = applyPanels({ parts: roled, supports: filtered }, structure, detail);
3642
+ const sizing = { safety, redundancy, safetyOthers };
3643
+ const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts, panels.supports, sizing) };
3644
+ const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
3645
+ const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse"));
3646
+ const margin = options.terrainMargin ?? TERRAIN_MARGIN;
3647
+ const x0 = Math.min(...parts.map((part) => part.aabb[0]));
3648
+ const z0 = Math.min(...parts.map((part) => part.aabb[1]));
3649
+ const x1 = Math.max(...parts.map((part) => part.aabb[2]));
3650
+ const z1 = Math.max(...parts.map((part) => part.aabb[3]));
3651
+ if (parts.length) specs.push({ id: GROUND, material: "concrete", shape: { kind: "box", size: [x1 - x0 + margin, TERRAIN_THICKNESS, z1 - z0 + margin] }, position: [(x0 + x1) / 2, -TERRAIN_THICKNESS / 2, (z0 + z1) / 2], hp: 1e12, structure: { role: "foundation" }, anchored: true, tags: ["terrain"] });
3652
+ const present = /* @__PURE__ */ new Set([...parts.map((part) => part.id), GROUND]);
3653
+ const joints = supports.filter((s) => present.has(s.part) && present.has(s.on)).map((s) => ({ id: `${s.part}~${s.on}`, a: s.part, b: s.on, kind: s.joint, bears: s.bears === false ? false : "a_on_b" }));
3654
+ return { name: options.name ?? "house", parts: specs, joints, tags: ["structure", "house"] };
3655
+ }
3656
+
3657
+ // src/wall-specimen.ts
3658
+ var SYSTEM_OF_ASSEMBLY = { log_wall: "log", wood_frame_wall: "wood_frame", sheet_metal_shack: "improvised" };
3659
+ var DOOR_WIDTH2 = 0.9;
3660
+ var WINDOW_WIDTH = 1;
3661
+ function openingOf(kind, height) {
3662
+ return kind === "door" ? { id: "door", kind, wall: "wall_specimen", from: -DOOR_WIDTH2 / 2, to: DOOR_WIDTH2 / 2, y0: 0, y1: height * 0.8 } : { id: "window", kind, wall: "wall_specimen", from: -WINDOW_WIDTH / 2, to: WINDOW_WIDTH / 2, y0: height * 0.33, y1: height * 0.8 };
3663
+ }
3664
+ function buildWallSpecimen(options) {
3665
+ const { assembly, w, h, opening: opening2 = null, detail = "panels" } = options;
3666
+ const def = getAssembly(assembly);
3667
+ if (!def) throw new Error(`unknown assembly "${assembly}"`);
3668
+ const system = SYSTEM_OF_ASSEMBLY[assembly] ?? "masonry";
3669
+ const depth = assemblyDepth(def);
3670
+ const mass = assemblyPart(assembly, { id: "wall_specimen", width: w, height: h, position: [0, h / 2, 0] }).mass ?? 1;
3671
+ const construction = constructionOfSystem(system, { facade: {}, floors: [], rooms: [], walls: [], doors: [], items: [], w, d: depth });
3672
+ const joint = CONSTRUCTION_SYSTEMS.get(system).joints.wall;
3673
+ const structure = {
3674
+ parts: [{ id: "wall_specimen", role: "wall", material: def.core.material, aabb: [-w / 2, -depth / 2, w / 2, depth / 2], y0: 0, y1: h, floor: 0, mass, assembly }],
3675
+ supports: [{ part: "wall_specimen", on: GROUND, joint }],
3676
+ openings: opening2 ? [openingOf(opening2, h)] : [],
3677
+ levels: [0],
3678
+ elevation: 0,
3679
+ construction,
3680
+ framed: false,
3681
+ roof: { shape: "slab", covering: "", material: "", pitch: 0, bearingFloor: 0, panes: [], trusses: 0 }
3682
+ };
3683
+ return toDestructibleSpec(structure, { wallDetail: detail, sized: false, terrainMargin: 0.6, name: `wall_${assembly}` });
3684
+ }
3685
+
3686
+ // src/index.ts
3687
+ var version = "0.1.0";
3688
+ return __toCommonJS(index_exports);
3689
+ })();
3690
+ //# sourceMappingURL=index.global.js.map