wadi-mcp 0.1.31 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.mjs +1557 -194
- package/package.json +1 -1
package/dist/server.mjs
CHANGED
|
@@ -14927,12 +14927,12 @@ var init_util = __esm({
|
|
|
14927
14927
|
function flattenError2(error53, mapper = (issue3) => issue3.message) {
|
|
14928
14928
|
const fieldErrors = {};
|
|
14929
14929
|
const formErrors = [];
|
|
14930
|
-
for (const
|
|
14931
|
-
if (
|
|
14932
|
-
fieldErrors[
|
|
14933
|
-
fieldErrors[
|
|
14930
|
+
for (const sub2 of error53.issues) {
|
|
14931
|
+
if (sub2.path.length > 0) {
|
|
14932
|
+
fieldErrors[sub2.path[0]] = fieldErrors[sub2.path[0]] || [];
|
|
14933
|
+
fieldErrors[sub2.path[0]].push(mapper(sub2));
|
|
14934
14934
|
} else {
|
|
14935
|
-
formErrors.push(mapper(
|
|
14935
|
+
formErrors.push(mapper(sub2));
|
|
14936
14936
|
}
|
|
14937
14937
|
}
|
|
14938
14938
|
return { formErrors, fieldErrors };
|
|
@@ -25307,13 +25307,13 @@ function _promise(Class3, innerType) {
|
|
|
25307
25307
|
}
|
|
25308
25308
|
// @__NO_SIDE_EFFECTS__
|
|
25309
25309
|
function _custom2(Class3, fn, _params) {
|
|
25310
|
-
const
|
|
25311
|
-
|
|
25310
|
+
const norm2 = normalizeParams2(_params);
|
|
25311
|
+
norm2.abort ?? (norm2.abort = true);
|
|
25312
25312
|
const schema = new Class3({
|
|
25313
25313
|
type: "custom",
|
|
25314
25314
|
check: "custom",
|
|
25315
25315
|
fn,
|
|
25316
|
-
...
|
|
25316
|
+
...norm2
|
|
25317
25317
|
});
|
|
25318
25318
|
return schema;
|
|
25319
25319
|
}
|
|
@@ -29624,7 +29624,20 @@ var init_houseConfig = __esm({
|
|
|
29624
29624
|
rise_height: positive().optional(),
|
|
29625
29625
|
step_rise: positive(),
|
|
29626
29626
|
step_tread: positive(),
|
|
29627
|
-
|
|
29627
|
+
// BOX MODEL (preferred): give a `width` (X) × `length` (Y) rectangle and the
|
|
29628
|
+
// WHOLE staircase — flights + turn landings — is packed to fit INSIDE it.
|
|
29629
|
+
// (start_x,start_y) is then the box's min corner, NOT the first step, and each
|
|
29630
|
+
// flight's width is DERIVED from the box: two switchback lanes =
|
|
29631
|
+
// (lateral − flight_gap)/2, or the full box for a single flight. `direction`
|
|
29632
|
+
// picks the run axis (N/S → run along length/Y; E/W → run along width/X). The
|
|
29633
|
+
// model errors if the box is too small for even the tightest split. Omit both
|
|
29634
|
+
// to use the legacy `step_width` + `max_run` form below.
|
|
29635
|
+
width: positive().optional(),
|
|
29636
|
+
length: positive().optional(),
|
|
29637
|
+
// Legacy per-step width (required by the old form; derived from the box in the
|
|
29638
|
+
// box model). The expanded single-flight staircases the renderer consumes always
|
|
29639
|
+
// carry it.
|
|
29640
|
+
step_width: positive().optional(),
|
|
29628
29641
|
// The direction the stair EXTENDS from its top — the whole assembly fills the
|
|
29629
29642
|
// allocated box from (start_x,start_y) going this way for up to `max_run`.
|
|
29630
29643
|
direction: side,
|
|
@@ -95426,7 +95439,7 @@ var require_three = __commonJS({
|
|
|
95426
95439
|
* @param {Color} cross - The cross line color.
|
|
95427
95440
|
* @return {CameraHelper} A reference to this helper.
|
|
95428
95441
|
*/
|
|
95429
|
-
setColors(frustum, cone, up, target,
|
|
95442
|
+
setColors(frustum, cone, up, target, cross2) {
|
|
95430
95443
|
const geometry = this.geometry;
|
|
95431
95444
|
const colorAttribute = geometry.getAttribute("color");
|
|
95432
95445
|
colorAttribute.setXYZ(0, frustum.r, frustum.g, frustum.b);
|
|
@@ -95469,16 +95482,16 @@ var require_three = __commonJS({
|
|
|
95469
95482
|
colorAttribute.setXYZ(37, up.r, up.g, up.b);
|
|
95470
95483
|
colorAttribute.setXYZ(38, target.r, target.g, target.b);
|
|
95471
95484
|
colorAttribute.setXYZ(39, target.r, target.g, target.b);
|
|
95472
|
-
colorAttribute.setXYZ(40,
|
|
95473
|
-
colorAttribute.setXYZ(41,
|
|
95474
|
-
colorAttribute.setXYZ(42,
|
|
95475
|
-
colorAttribute.setXYZ(43,
|
|
95476
|
-
colorAttribute.setXYZ(44,
|
|
95477
|
-
colorAttribute.setXYZ(45,
|
|
95478
|
-
colorAttribute.setXYZ(46,
|
|
95479
|
-
colorAttribute.setXYZ(47,
|
|
95480
|
-
colorAttribute.setXYZ(48,
|
|
95481
|
-
colorAttribute.setXYZ(49,
|
|
95485
|
+
colorAttribute.setXYZ(40, cross2.r, cross2.g, cross2.b);
|
|
95486
|
+
colorAttribute.setXYZ(41, cross2.r, cross2.g, cross2.b);
|
|
95487
|
+
colorAttribute.setXYZ(42, cross2.r, cross2.g, cross2.b);
|
|
95488
|
+
colorAttribute.setXYZ(43, cross2.r, cross2.g, cross2.b);
|
|
95489
|
+
colorAttribute.setXYZ(44, cross2.r, cross2.g, cross2.b);
|
|
95490
|
+
colorAttribute.setXYZ(45, cross2.r, cross2.g, cross2.b);
|
|
95491
|
+
colorAttribute.setXYZ(46, cross2.r, cross2.g, cross2.b);
|
|
95492
|
+
colorAttribute.setXYZ(47, cross2.r, cross2.g, cross2.b);
|
|
95493
|
+
colorAttribute.setXYZ(48, cross2.r, cross2.g, cross2.b);
|
|
95494
|
+
colorAttribute.setXYZ(49, cross2.r, cross2.g, cross2.b);
|
|
95482
95495
|
colorAttribute.needsUpdate = true;
|
|
95483
95496
|
return this;
|
|
95484
95497
|
}
|
|
@@ -116060,11 +116073,11 @@ No matching component was found for:
|
|
|
116060
116073
|
return reconciler.flushSyncFromReconciler(fn);
|
|
116061
116074
|
}
|
|
116062
116075
|
function createSubs(callback, subs) {
|
|
116063
|
-
const
|
|
116076
|
+
const sub2 = {
|
|
116064
116077
|
callback
|
|
116065
116078
|
};
|
|
116066
|
-
subs.add(
|
|
116067
|
-
return () => void subs.delete(
|
|
116079
|
+
subs.add(sub2);
|
|
116080
|
+
return () => void subs.delete(sub2);
|
|
116068
116081
|
}
|
|
116069
116082
|
var globalEffects = /* @__PURE__ */ new Set();
|
|
116070
116083
|
var globalAfterEffects = /* @__PURE__ */ new Set();
|
|
@@ -131121,11 +131134,11 @@ No matching component was found for:
|
|
|
131121
131134
|
return reconciler.flushSyncFromReconciler(fn);
|
|
131122
131135
|
}
|
|
131123
131136
|
function createSubs(callback, subs) {
|
|
131124
|
-
const
|
|
131137
|
+
const sub2 = {
|
|
131125
131138
|
callback
|
|
131126
131139
|
};
|
|
131127
|
-
subs.add(
|
|
131128
|
-
return () => void subs.delete(
|
|
131140
|
+
subs.add(sub2);
|
|
131141
|
+
return () => void subs.delete(sub2);
|
|
131129
131142
|
}
|
|
131130
131143
|
var globalEffects = /* @__PURE__ */ new Set();
|
|
131131
131144
|
var globalAfterEffects = /* @__PURE__ */ new Set();
|
|
@@ -134039,8 +134052,8 @@ var require_triangle_9e5a8229_cjs_prod = __commonJS({
|
|
|
134039
134052
|
if (arePointsCollinear(points)) return false;
|
|
134040
134053
|
var p2p1 = mv1.subVectors(p2, p1);
|
|
134041
134054
|
var p3p1 = mv2.subVectors(p3, p1);
|
|
134042
|
-
var
|
|
134043
|
-
return
|
|
134055
|
+
var cross2 = p3p1.cross(p2p1);
|
|
134056
|
+
return cross2 > 0;
|
|
134044
134057
|
}
|
|
134045
134058
|
var triangle = /* @__PURE__ */ Object.freeze({
|
|
134046
134059
|
__proto__: null,
|
|
@@ -134292,7 +134305,7 @@ var require_vector2_49e42f03_cjs_prod = __commonJS({
|
|
|
134292
134305
|
function addValue(a, n3) {
|
|
134293
134306
|
return [a[0] + n3, a[1] + n3];
|
|
134294
134307
|
}
|
|
134295
|
-
function
|
|
134308
|
+
function sub2(a, b) {
|
|
134296
134309
|
return [a[0] - b[0], a[1] - b[1]];
|
|
134297
134310
|
}
|
|
134298
134311
|
function subValue(a, n3) {
|
|
@@ -134319,7 +134332,7 @@ var require_vector2_49e42f03_cjs_prod = __commonJS({
|
|
|
134319
134332
|
one,
|
|
134320
134333
|
add,
|
|
134321
134334
|
addValue,
|
|
134322
|
-
sub,
|
|
134335
|
+
sub: sub2,
|
|
134323
134336
|
subValue,
|
|
134324
134337
|
scale,
|
|
134325
134338
|
dot: dot2,
|
|
@@ -134335,7 +134348,7 @@ var require_vector2_49e42f03_cjs_prod = __commonJS({
|
|
|
134335
134348
|
exports2.lengthSqr = lengthSqr;
|
|
134336
134349
|
exports2.one = one;
|
|
134337
134350
|
exports2.scale = scale;
|
|
134338
|
-
exports2.sub =
|
|
134351
|
+
exports2.sub = sub2;
|
|
134339
134352
|
exports2.subValue = subValue;
|
|
134340
134353
|
exports2.vector2 = vector2;
|
|
134341
134354
|
exports2.zero = zero;
|
|
@@ -134358,7 +134371,7 @@ var require_vector3_4bbdb053_cjs_prod = __commonJS({
|
|
|
134358
134371
|
function addValue(a, n3) {
|
|
134359
134372
|
return [a[0] + n3, a[1] + n3, a[2] + n3];
|
|
134360
134373
|
}
|
|
134361
|
-
function
|
|
134374
|
+
function sub2(a, b) {
|
|
134362
134375
|
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
134363
134376
|
}
|
|
134364
134377
|
function subValue(a, n3) {
|
|
@@ -134370,7 +134383,7 @@ var require_vector3_4bbdb053_cjs_prod = __commonJS({
|
|
|
134370
134383
|
function dot2(a, b) {
|
|
134371
134384
|
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
134372
134385
|
}
|
|
134373
|
-
function
|
|
134386
|
+
function cross2(a, b) {
|
|
134374
134387
|
var x = a[1] * b[2] - a[2] * b[1];
|
|
134375
134388
|
var y = a[2] * b[0] - a[0] * b[2];
|
|
134376
134389
|
var z2 = a[0] * b[1] - a[1] * b[0];
|
|
@@ -134391,25 +134404,25 @@ var require_vector3_4bbdb053_cjs_prod = __commonJS({
|
|
|
134391
134404
|
one,
|
|
134392
134405
|
add,
|
|
134393
134406
|
addValue,
|
|
134394
|
-
sub,
|
|
134407
|
+
sub: sub2,
|
|
134395
134408
|
subValue,
|
|
134396
134409
|
scale,
|
|
134397
134410
|
dot: dot2,
|
|
134398
|
-
cross,
|
|
134411
|
+
cross: cross2,
|
|
134399
134412
|
lengthSqr,
|
|
134400
134413
|
length,
|
|
134401
134414
|
distance
|
|
134402
134415
|
});
|
|
134403
134416
|
exports2.add = add;
|
|
134404
134417
|
exports2.addValue = addValue;
|
|
134405
|
-
exports2.cross =
|
|
134418
|
+
exports2.cross = cross2;
|
|
134406
134419
|
exports2.distance = distance;
|
|
134407
134420
|
exports2.dot = dot2;
|
|
134408
134421
|
exports2.length = length;
|
|
134409
134422
|
exports2.lengthSqr = lengthSqr;
|
|
134410
134423
|
exports2.one = one;
|
|
134411
134424
|
exports2.scale = scale;
|
|
134412
|
-
exports2.sub =
|
|
134425
|
+
exports2.sub = sub2;
|
|
134413
134426
|
exports2.subValue = subValue;
|
|
134414
134427
|
exports2.vector3 = vector3;
|
|
134415
134428
|
exports2.zero = zero;
|
|
@@ -136124,8 +136137,8 @@ var require_triangle_33ffdfef_cjs_dev = __commonJS({
|
|
|
136124
136137
|
if (arePointsCollinear(points)) return false;
|
|
136125
136138
|
var p2p1 = mv1.subVectors(p2, p1);
|
|
136126
136139
|
var p3p1 = mv2.subVectors(p3, p1);
|
|
136127
|
-
var
|
|
136128
|
-
return
|
|
136140
|
+
var cross2 = p3p1.cross(p2p1);
|
|
136141
|
+
return cross2 > 0;
|
|
136129
136142
|
}
|
|
136130
136143
|
var triangle = /* @__PURE__ */ Object.freeze({
|
|
136131
136144
|
__proto__: null,
|
|
@@ -136377,7 +136390,7 @@ var require_vector2_f44fd63e_cjs_dev = __commonJS({
|
|
|
136377
136390
|
function addValue(a, n3) {
|
|
136378
136391
|
return [a[0] + n3, a[1] + n3];
|
|
136379
136392
|
}
|
|
136380
|
-
function
|
|
136393
|
+
function sub2(a, b) {
|
|
136381
136394
|
return [a[0] - b[0], a[1] - b[1]];
|
|
136382
136395
|
}
|
|
136383
136396
|
function subValue(a, n3) {
|
|
@@ -136404,7 +136417,7 @@ var require_vector2_f44fd63e_cjs_dev = __commonJS({
|
|
|
136404
136417
|
one,
|
|
136405
136418
|
add,
|
|
136406
136419
|
addValue,
|
|
136407
|
-
sub,
|
|
136420
|
+
sub: sub2,
|
|
136408
136421
|
subValue,
|
|
136409
136422
|
scale,
|
|
136410
136423
|
dot: dot2,
|
|
@@ -136420,7 +136433,7 @@ var require_vector2_f44fd63e_cjs_dev = __commonJS({
|
|
|
136420
136433
|
exports2.lengthSqr = lengthSqr;
|
|
136421
136434
|
exports2.one = one;
|
|
136422
136435
|
exports2.scale = scale;
|
|
136423
|
-
exports2.sub =
|
|
136436
|
+
exports2.sub = sub2;
|
|
136424
136437
|
exports2.subValue = subValue;
|
|
136425
136438
|
exports2.vector2 = vector2;
|
|
136426
136439
|
exports2.zero = zero;
|
|
@@ -136443,7 +136456,7 @@ var require_vector3_5e723d1a_cjs_dev = __commonJS({
|
|
|
136443
136456
|
function addValue(a, n3) {
|
|
136444
136457
|
return [a[0] + n3, a[1] + n3, a[2] + n3];
|
|
136445
136458
|
}
|
|
136446
|
-
function
|
|
136459
|
+
function sub2(a, b) {
|
|
136447
136460
|
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
136448
136461
|
}
|
|
136449
136462
|
function subValue(a, n3) {
|
|
@@ -136455,7 +136468,7 @@ var require_vector3_5e723d1a_cjs_dev = __commonJS({
|
|
|
136455
136468
|
function dot2(a, b) {
|
|
136456
136469
|
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
136457
136470
|
}
|
|
136458
|
-
function
|
|
136471
|
+
function cross2(a, b) {
|
|
136459
136472
|
var x = a[1] * b[2] - a[2] * b[1];
|
|
136460
136473
|
var y = a[2] * b[0] - a[0] * b[2];
|
|
136461
136474
|
var z2 = a[0] * b[1] - a[1] * b[0];
|
|
@@ -136476,25 +136489,25 @@ var require_vector3_5e723d1a_cjs_dev = __commonJS({
|
|
|
136476
136489
|
one,
|
|
136477
136490
|
add,
|
|
136478
136491
|
addValue,
|
|
136479
|
-
sub,
|
|
136492
|
+
sub: sub2,
|
|
136480
136493
|
subValue,
|
|
136481
136494
|
scale,
|
|
136482
136495
|
dot: dot2,
|
|
136483
|
-
cross,
|
|
136496
|
+
cross: cross2,
|
|
136484
136497
|
lengthSqr,
|
|
136485
136498
|
length,
|
|
136486
136499
|
distance
|
|
136487
136500
|
});
|
|
136488
136501
|
exports2.add = add;
|
|
136489
136502
|
exports2.addValue = addValue;
|
|
136490
|
-
exports2.cross =
|
|
136503
|
+
exports2.cross = cross2;
|
|
136491
136504
|
exports2.distance = distance;
|
|
136492
136505
|
exports2.dot = dot2;
|
|
136493
136506
|
exports2.length = length;
|
|
136494
136507
|
exports2.lengthSqr = lengthSqr;
|
|
136495
136508
|
exports2.one = one;
|
|
136496
136509
|
exports2.scale = scale;
|
|
136497
|
-
exports2.sub =
|
|
136510
|
+
exports2.sub = sub2;
|
|
136498
136511
|
exports2.subValue = subValue;
|
|
136499
136512
|
exports2.vector3 = vector3;
|
|
136500
136513
|
exports2.zero = zero;
|
|
@@ -164912,7 +164925,7 @@ var require_MarchingCubes = __commonJS({
|
|
|
164912
164925
|
}
|
|
164913
164926
|
return numtris;
|
|
164914
164927
|
}
|
|
164915
|
-
function posnormtriv(pos,
|
|
164928
|
+
function posnormtriv(pos, norm2, colors, o1, o2, o3) {
|
|
164916
164929
|
const c = scope.count * 3;
|
|
164917
164930
|
scope.positionArray[c + 0] = pos[o1];
|
|
164918
164931
|
scope.positionArray[c + 1] = pos[o1 + 1];
|
|
@@ -164924,9 +164937,9 @@ var require_MarchingCubes = __commonJS({
|
|
|
164924
164937
|
scope.positionArray[c + 7] = pos[o3 + 1];
|
|
164925
164938
|
scope.positionArray[c + 8] = pos[o3 + 2];
|
|
164926
164939
|
if (scope.material.flatShading === true) {
|
|
164927
|
-
const nx = (
|
|
164928
|
-
const ny = (
|
|
164929
|
-
const nz = (
|
|
164940
|
+
const nx = (norm2[o1 + 0] + norm2[o2 + 0] + norm2[o3 + 0]) / 3;
|
|
164941
|
+
const ny = (norm2[o1 + 1] + norm2[o2 + 1] + norm2[o3 + 1]) / 3;
|
|
164942
|
+
const nz = (norm2[o1 + 2] + norm2[o2 + 2] + norm2[o3 + 2]) / 3;
|
|
164930
164943
|
scope.normalArray[c + 0] = nx;
|
|
164931
164944
|
scope.normalArray[c + 1] = ny;
|
|
164932
164945
|
scope.normalArray[c + 2] = nz;
|
|
@@ -164937,15 +164950,15 @@ var require_MarchingCubes = __commonJS({
|
|
|
164937
164950
|
scope.normalArray[c + 7] = ny;
|
|
164938
164951
|
scope.normalArray[c + 8] = nz;
|
|
164939
164952
|
} else {
|
|
164940
|
-
scope.normalArray[c + 0] =
|
|
164941
|
-
scope.normalArray[c + 1] =
|
|
164942
|
-
scope.normalArray[c + 2] =
|
|
164943
|
-
scope.normalArray[c + 3] =
|
|
164944
|
-
scope.normalArray[c + 4] =
|
|
164945
|
-
scope.normalArray[c + 5] =
|
|
164946
|
-
scope.normalArray[c + 6] =
|
|
164947
|
-
scope.normalArray[c + 7] =
|
|
164948
|
-
scope.normalArray[c + 8] =
|
|
164953
|
+
scope.normalArray[c + 0] = norm2[o1 + 0];
|
|
164954
|
+
scope.normalArray[c + 1] = norm2[o1 + 1];
|
|
164955
|
+
scope.normalArray[c + 2] = norm2[o1 + 2];
|
|
164956
|
+
scope.normalArray[c + 3] = norm2[o2 + 0];
|
|
164957
|
+
scope.normalArray[c + 4] = norm2[o2 + 1];
|
|
164958
|
+
scope.normalArray[c + 5] = norm2[o2 + 2];
|
|
164959
|
+
scope.normalArray[c + 6] = norm2[o3 + 0];
|
|
164960
|
+
scope.normalArray[c + 7] = norm2[o3 + 1];
|
|
164961
|
+
scope.normalArray[c + 8] = norm2[o3 + 2];
|
|
164949
164962
|
}
|
|
164950
164963
|
if (scope.enableUvs) {
|
|
164951
164964
|
const d = scope.count * 2;
|
|
@@ -193390,7 +193403,7 @@ var require_TeapotGeometry = __commonJS({
|
|
|
193390
193403
|
const vert = [];
|
|
193391
193404
|
const sdir = [];
|
|
193392
193405
|
const tdir = [];
|
|
193393
|
-
const
|
|
193406
|
+
const norm2 = new THREE.Vector3();
|
|
193394
193407
|
let tcoord;
|
|
193395
193408
|
let sstep, tstep;
|
|
193396
193409
|
let vertPerRow;
|
|
@@ -193475,12 +193488,12 @@ var require_TeapotGeometry = __commonJS({
|
|
|
193475
193488
|
}
|
|
193476
193489
|
vsdir.fromArray(sdir);
|
|
193477
193490
|
vtdir.fromArray(tdir);
|
|
193478
|
-
|
|
193479
|
-
|
|
193491
|
+
norm2.crossVectors(vtdir, vsdir);
|
|
193492
|
+
norm2.normalize();
|
|
193480
193493
|
if (vert[0] === 0 && vert[1] === 0) {
|
|
193481
193494
|
normOut.set(0, vert[2] > maxHeight2 ? 1 : -1, 0);
|
|
193482
193495
|
} else {
|
|
193483
|
-
normOut.set(
|
|
193496
|
+
normOut.set(norm2.x, norm2.z, -norm2.y);
|
|
193484
193497
|
}
|
|
193485
193498
|
vertices[vertCount++] = trueSize * vert[0];
|
|
193486
193499
|
vertices[vertCount++] = trueSize * (vert[2] - maxHeight2);
|
|
@@ -232411,7 +232424,7 @@ var require_lottie = __commonJS({
|
|
|
232411
232424
|
return 0;
|
|
232412
232425
|
}
|
|
232413
232426
|
var add = sum;
|
|
232414
|
-
function
|
|
232427
|
+
function sub2(a, b) {
|
|
232415
232428
|
var tOfA = typeof a;
|
|
232416
232429
|
var tOfB = typeof b;
|
|
232417
232430
|
if (isNumerable(tOfA, a) && isNumerable(tOfB, b)) {
|
|
@@ -232514,7 +232527,7 @@ var require_lottie = __commonJS({
|
|
|
232514
232527
|
return a % b;
|
|
232515
232528
|
}
|
|
232516
232529
|
var $bm_sum = sum;
|
|
232517
|
-
var $bm_sub =
|
|
232530
|
+
var $bm_sub = sub2;
|
|
232518
232531
|
var $bm_mul = mul;
|
|
232519
232532
|
var $bm_div = div;
|
|
232520
232533
|
var $bm_mod = mod;
|
|
@@ -240714,26 +240727,26 @@ var require_opentype = __commonJS({
|
|
|
240714
240727
|
}
|
|
240715
240728
|
return void 0;
|
|
240716
240729
|
};
|
|
240717
|
-
Substitution.prototype.add = function(feature,
|
|
240730
|
+
Substitution.prototype.add = function(feature, sub2, script, language) {
|
|
240718
240731
|
if (/ss\d\d/.test(feature)) {
|
|
240719
|
-
return this.addSingle(feature,
|
|
240732
|
+
return this.addSingle(feature, sub2, script, language);
|
|
240720
240733
|
}
|
|
240721
240734
|
switch (feature) {
|
|
240722
240735
|
case "aalt":
|
|
240723
240736
|
case "salt":
|
|
240724
|
-
if (typeof
|
|
240725
|
-
return this.addSingle(feature,
|
|
240737
|
+
if (typeof sub2.by === "number") {
|
|
240738
|
+
return this.addSingle(feature, sub2, script, language);
|
|
240726
240739
|
}
|
|
240727
|
-
return this.addAlternate(feature,
|
|
240740
|
+
return this.addAlternate(feature, sub2, script, language);
|
|
240728
240741
|
case "dlig":
|
|
240729
240742
|
case "liga":
|
|
240730
240743
|
case "rlig":
|
|
240731
|
-
return this.addLigature(feature,
|
|
240744
|
+
return this.addLigature(feature, sub2, script, language);
|
|
240732
240745
|
case "ccmp":
|
|
240733
|
-
if (
|
|
240734
|
-
return this.addMultiple(feature,
|
|
240746
|
+
if (sub2.by instanceof Array) {
|
|
240747
|
+
return this.addMultiple(feature, sub2, script, language);
|
|
240735
240748
|
}
|
|
240736
|
-
return this.addLigature(feature,
|
|
240749
|
+
return this.addLigature(feature, sub2, script, language);
|
|
240737
240750
|
}
|
|
240738
240751
|
return void 0;
|
|
240739
240752
|
};
|
|
@@ -297708,11 +297721,11 @@ var require_LightProbeGenerator = __commonJS({
|
|
|
297708
297721
|
}
|
|
297709
297722
|
}
|
|
297710
297723
|
}
|
|
297711
|
-
const
|
|
297724
|
+
const norm2 = 4 * Math.PI / totalWeight;
|
|
297712
297725
|
for (let j = 0; j < 9; j++) {
|
|
297713
|
-
shCoefficients[j].x *=
|
|
297714
|
-
shCoefficients[j].y *=
|
|
297715
|
-
shCoefficients[j].z *=
|
|
297726
|
+
shCoefficients[j].x *= norm2;
|
|
297727
|
+
shCoefficients[j].y *= norm2;
|
|
297728
|
+
shCoefficients[j].z *= norm2;
|
|
297716
297729
|
}
|
|
297717
297730
|
return new THREE.LightProbe(sh);
|
|
297718
297731
|
},
|
|
@@ -297773,11 +297786,11 @@ var require_LightProbeGenerator = __commonJS({
|
|
|
297773
297786
|
}
|
|
297774
297787
|
}
|
|
297775
297788
|
}
|
|
297776
|
-
const
|
|
297789
|
+
const norm2 = 4 * Math.PI / totalWeight;
|
|
297777
297790
|
for (let j = 0; j < 9; j++) {
|
|
297778
|
-
shCoefficients[j].x *=
|
|
297779
|
-
shCoefficients[j].y *=
|
|
297780
|
-
shCoefficients[j].z *=
|
|
297791
|
+
shCoefficients[j].x *= norm2;
|
|
297792
|
+
shCoefficients[j].y *= norm2;
|
|
297793
|
+
shCoefficients[j].z *= norm2;
|
|
297781
297794
|
}
|
|
297782
297795
|
return new THREE.LightProbe(sh);
|
|
297783
297796
|
}
|
|
@@ -369769,7 +369782,7 @@ __export(FurnitureItem_exports, {
|
|
|
369769
369782
|
function ItemGLB(props) {
|
|
369770
369783
|
const { src, dimensions, offset, corrRotation, corrScale, rig, cx, cz, baseY, yawDeg, userScale, unitsScale } = props;
|
|
369771
369784
|
const gltf = (0, import_drei.useGLTF)(src);
|
|
369772
|
-
const { node, seatX, seatY, seatZ, norm } = (0, import_react3.useMemo)(() => {
|
|
369785
|
+
const { node, seatX, seatY, seatZ, norm: norm2 } = (0, import_react3.useMemo)(() => {
|
|
369773
369786
|
const clone3 = gltf.scene.clone(true);
|
|
369774
369787
|
if (offset) clone3.position.set(offset[0], offset[1], offset[2]);
|
|
369775
369788
|
if (corrRotation) {
|
|
@@ -369795,7 +369808,7 @@ function ItemGLB(props) {
|
|
|
369795
369808
|
norm: targetHorizUnits / nativeHoriz
|
|
369796
369809
|
};
|
|
369797
369810
|
}, [gltf.scene, offset, corrRotation, corrScale, rig, dimensions, unitsScale]);
|
|
369798
|
-
return /* @__PURE__ */ React.createElement("group", { position: [cx, baseY, cz], rotation: [0, MathUtils.degToRad(yawDeg ?? 0), 0] }, /* @__PURE__ */ React.createElement("group", { scale:
|
|
369811
|
+
return /* @__PURE__ */ React.createElement("group", { position: [cx, baseY, cz], rotation: [0, MathUtils.degToRad(yawDeg ?? 0), 0] }, /* @__PURE__ */ React.createElement("group", { scale: norm2 * (userScale ?? 1) }, /* @__PURE__ */ React.createElement("group", { position: [seatX, seatY, seatZ] }, /* @__PURE__ */ React.createElement("primitive", { object: node }))));
|
|
369799
369812
|
}
|
|
369800
369813
|
function GhostBox({ dimensions, cx, cz, baseY, yawDeg, userScale, unitsScale, loading }) {
|
|
369801
369814
|
const s = unitsScale * (userScale ?? 1);
|
|
@@ -370177,6 +370190,9 @@ function stripMulti(sc) {
|
|
|
370177
370190
|
return rest;
|
|
370178
370191
|
}
|
|
370179
370192
|
function expandStaircase(sc, slabThickness, floorBelowHeight, floorOwnHeight) {
|
|
370193
|
+
if (sc.width != null && sc.length != null) {
|
|
370194
|
+
return expandStaircaseBox(sc, slabThickness, floorBelowHeight, floorOwnHeight);
|
|
370195
|
+
}
|
|
370180
370196
|
const up = (sc.climb ?? "down") === "up";
|
|
370181
370197
|
const tread = n(sc.step_tread);
|
|
370182
370198
|
const riser = n(sc.step_rise);
|
|
@@ -370247,7 +370263,7 @@ function expandStaircase(sc, slabThickness, floorBelowHeight, floorOwnHeight) {
|
|
|
370247
370263
|
if (t < numFlights - 1) {
|
|
370248
370264
|
items.push({
|
|
370249
370265
|
isStair: false,
|
|
370250
|
-
o: { type: "floor_slab", x: landingX, y: even ? flightRun :
|
|
370266
|
+
o: { type: "floor_slab", x: landingX, y: even ? flightRun : -landingDepth, width: landingWidth, length: landingDepth, thickness: landingThickness, z_offset: zTopFlight - landingThickness }
|
|
370251
370267
|
});
|
|
370252
370268
|
}
|
|
370253
370269
|
zCur = zTopFlight;
|
|
@@ -370260,7 +370276,7 @@ function expandStaircase(sc, slabThickness, floorBelowHeight, floorOwnHeight) {
|
|
|
370260
370276
|
if (t < numFlights - 1) {
|
|
370261
370277
|
items.push({
|
|
370262
370278
|
isStair: false,
|
|
370263
|
-
o: { type: "floor_slab", x: landingX, y: even ? flightRun :
|
|
370279
|
+
o: { type: "floor_slab", x: landingX, y: even ? flightRun : -landingDepth, width: landingWidth, length: landingDepth, thickness: landingThickness, z_offset: zBottom - landingThickness }
|
|
370264
370280
|
});
|
|
370265
370281
|
}
|
|
370266
370282
|
zCur = zBottom;
|
|
@@ -370285,6 +370301,114 @@ function expandStaircase(sc, slabThickness, floorBelowHeight, floorOwnHeight) {
|
|
|
370285
370301
|
return o;
|
|
370286
370302
|
});
|
|
370287
370303
|
}
|
|
370304
|
+
function expandStaircaseBox(sc, slabThickness, floorBelowHeight, floorOwnHeight) {
|
|
370305
|
+
const up = (sc.climb ?? "down") === "up";
|
|
370306
|
+
const tread = n(sc.step_tread);
|
|
370307
|
+
const riser = n(sc.step_rise);
|
|
370308
|
+
const direction = sc.direction ?? "south";
|
|
370309
|
+
const runAlongY = direction === "south" || direction === "north";
|
|
370310
|
+
const boxW = n(sc.width);
|
|
370311
|
+
const boxL = n(sc.length);
|
|
370312
|
+
const runBudget = runAlongY ? boxL : boxW;
|
|
370313
|
+
const lateralExtent = runAlongY ? boxW : boxL;
|
|
370314
|
+
const name = sc.name ?? "Stair";
|
|
370315
|
+
const defaultRise = up ? floorOwnHeight : floorBelowHeight;
|
|
370316
|
+
const riseHeight = typeof sc.rise_height === "number" && sc.rise_height > 0 ? sc.rise_height : defaultRise;
|
|
370317
|
+
const totalSteps = Math.max(1, Math.round(riseHeight / riser));
|
|
370318
|
+
const anchorZ = sc.z_offset !== void 0 ? n(sc.z_offset) : slabThickness;
|
|
370319
|
+
const gap = typeof sc.flight_gap === "number" && sc.flight_gap > 0 ? sc.flight_gap : 0;
|
|
370320
|
+
const laneWidth = (lateralExtent - gap) / 2;
|
|
370321
|
+
const landingDepth = typeof sc.landing_depth === "number" && sc.landing_depth > 0 ? sc.landing_depth : Math.max(1, laneWidth);
|
|
370322
|
+
const landingThickness = typeof sc.landing_thickness === "number" ? sc.landing_thickness : riser;
|
|
370323
|
+
const flightRunFor = (nf) => (Math.ceil(totalSteps / nf) - 1) * tread;
|
|
370324
|
+
const reserveFor = (_nf) => 2 * landingDepth;
|
|
370325
|
+
let numFlights = 0;
|
|
370326
|
+
for (let nf = 1; nf <= totalSteps; nf++) {
|
|
370327
|
+
if (nf >= 2 && Math.ceil(totalSteps / nf) < 2) break;
|
|
370328
|
+
if (flightRunFor(nf) + reserveFor(nf) <= runBudget + 1e-6) {
|
|
370329
|
+
numFlights = nf;
|
|
370330
|
+
break;
|
|
370331
|
+
}
|
|
370332
|
+
}
|
|
370333
|
+
if (numFlights === 0) {
|
|
370334
|
+
const minLen = tread + 2 * landingDepth;
|
|
370335
|
+
throw new Error(
|
|
370336
|
+
`staircase "${name}" doesn't fit its box along ${runAlongY ? "length (Y)" : "width (X)"}: need at least ${Math.ceil(minLen)} (one ${tread}-unit tread + a bottom approach + a turn landing, ${landingDepth} each), but only ${Math.floor(runBudget)} is allocated. Enlarge the box or reduce landing_depth.`
|
|
370337
|
+
);
|
|
370338
|
+
}
|
|
370339
|
+
const nLanes = numFlights >= 2 ? 2 : 1;
|
|
370340
|
+
const stairWidth = nLanes === 2 ? laneWidth : lateralExtent;
|
|
370341
|
+
if (!(stairWidth > 0)) {
|
|
370342
|
+
throw new Error(
|
|
370343
|
+
`staircase "${name}" doesn't fit its box across the stairs: the ${Math.floor(lateralExtent)}-unit side leaves no room for ${nLanes === 2 ? "two lanes" : "a flight"} past flight_gap ${gap}.`
|
|
370344
|
+
);
|
|
370345
|
+
}
|
|
370346
|
+
const perFlight = Math.ceil(totalSteps / numFlights);
|
|
370347
|
+
const risersFor = (t) => Math.max(0, Math.min(perFlight, totalSteps - t * perFlight));
|
|
370348
|
+
const flightRun = Math.max(1, perFlight - 1) * tread;
|
|
370349
|
+
const nearOffset = landingDepth;
|
|
370350
|
+
const anti = sc.turn === "anticlockwise";
|
|
370351
|
+
const laneA = anti ? stairWidth + gap : 0;
|
|
370352
|
+
const laneB = anti ? 0 : stairWidth + gap;
|
|
370353
|
+
const landingWidth = nLanes === 2 ? 2 * stairWidth + gap : stairWidth;
|
|
370354
|
+
const items = [];
|
|
370355
|
+
const stepFields = { step_rise: riser, step_tread: tread, step_width: stairWidth };
|
|
370356
|
+
let zCur = 0;
|
|
370357
|
+
for (let t = 0; t < numFlights; t++) {
|
|
370358
|
+
const risers = risersFor(t);
|
|
370359
|
+
if (risers <= 0) break;
|
|
370360
|
+
const treads = Math.max(1, risers - 1);
|
|
370361
|
+
const even = t % 2 === 0;
|
|
370362
|
+
const lane = even ? laneA : laneB;
|
|
370363
|
+
const run = treads * tread;
|
|
370364
|
+
if (up) {
|
|
370365
|
+
const zBottomFlight = zCur;
|
|
370366
|
+
const zTopFlight = zCur + risers * riser;
|
|
370367
|
+
items.push({
|
|
370368
|
+
isStair: true,
|
|
370369
|
+
o: even ? { type: "staircase", direction: "south", start_x: lane, start_y: nearOffset, num_steps: treads, ...stepFields, z_offset: zBottomFlight } : { type: "staircase", direction: "north", start_x: lane, start_y: nearOffset + flightRun, num_steps: treads, ...stepFields, z_offset: zBottomFlight }
|
|
370370
|
+
});
|
|
370371
|
+
{
|
|
370372
|
+
const ly = even ? nearOffset + flightRun : nearOffset - landingDepth;
|
|
370373
|
+
items.push({ isStair: false, o: { type: "floor_slab", x: 0, y: ly, width: landingWidth, length: landingDepth, thickness: landingThickness, z_offset: zTopFlight - landingThickness } });
|
|
370374
|
+
}
|
|
370375
|
+
zCur = zTopFlight;
|
|
370376
|
+
} else {
|
|
370377
|
+
const zBottom = zCur - risers * riser;
|
|
370378
|
+
items.push({
|
|
370379
|
+
isStair: true,
|
|
370380
|
+
o: even ? { type: "staircase", direction: "north", start_x: lane, start_y: nearOffset + run, num_steps: treads, ...stepFields, z_offset: zBottom } : { type: "staircase", direction: "south", start_x: lane, start_y: nearOffset + flightRun - run, num_steps: treads, ...stepFields, z_offset: zBottom }
|
|
370381
|
+
});
|
|
370382
|
+
{
|
|
370383
|
+
const ly = even ? nearOffset + flightRun : nearOffset - landingDepth;
|
|
370384
|
+
items.push({ isStair: false, o: { type: "floor_slab", x: 0, y: ly, width: landingWidth, length: landingDepth, thickness: landingThickness, z_offset: zBottom - landingThickness } });
|
|
370385
|
+
}
|
|
370386
|
+
zCur = zBottom;
|
|
370387
|
+
}
|
|
370388
|
+
}
|
|
370389
|
+
const rotated = items.map((it) => ({ ...it, o: rotateObject(it.o, direction) }));
|
|
370390
|
+
const R = ROT[direction];
|
|
370391
|
+
let minX = Infinity, minY = Infinity;
|
|
370392
|
+
for (const [cx, cy] of [[0, 0], [lateralExtent, 0], [0, runBudget], [lateralExtent, runBudget]]) {
|
|
370393
|
+
const [rx, ry] = R.m(cx, cy);
|
|
370394
|
+
if (rx < minX) minX = rx;
|
|
370395
|
+
if (ry < minY) minY = ry;
|
|
370396
|
+
}
|
|
370397
|
+
const dx = n(sc.start_x) - minX;
|
|
370398
|
+
const dy = n(sc.start_y) - minY;
|
|
370399
|
+
let fi = 0, li = 0;
|
|
370400
|
+
return rotated.map(({ o, isStair }) => {
|
|
370401
|
+
if (typeof o.x === "number") o.x += dx;
|
|
370402
|
+
if (typeof o.y === "number") o.y += dy;
|
|
370403
|
+
if (typeof o.start_x === "number") o.start_x += dx;
|
|
370404
|
+
if (typeof o.start_y === "number") o.start_y += dy;
|
|
370405
|
+
o.z_offset = n(o.z_offset) + anchorZ;
|
|
370406
|
+
if (sc.layer !== void 0) o.layer = sc.layer;
|
|
370407
|
+
if (isStair && sc.material !== void 0) o.material = sc.material;
|
|
370408
|
+
o.name = isStair ? `${name}_F${++fi}` : `${name}_L${++li}`;
|
|
370409
|
+
return o;
|
|
370410
|
+
});
|
|
370411
|
+
}
|
|
370288
370412
|
function flip180(d) {
|
|
370289
370413
|
return d === "south" ? "north" : d === "north" ? "south" : d === "east" ? "west" : "east";
|
|
370290
370414
|
}
|
|
@@ -370474,27 +370598,6 @@ function expandRoomWalls(houseConfig, wallThickness, opts, _depth = 0) {
|
|
|
370474
370598
|
if (typeof r2.y === "number") r2.y = r2.y - ot / 2;
|
|
370475
370599
|
if (typeof r2.width === "number") r2.width = r2.width + ot;
|
|
370476
370600
|
if (typeof r2.length === "number") r2.length = r2.length + ot;
|
|
370477
|
-
} else if (o.type === "roof") {
|
|
370478
|
-
const segs = r2.segments;
|
|
370479
|
-
if (Array.isArray(segs)) {
|
|
370480
|
-
const half = t / 2;
|
|
370481
|
-
for (const s of segs) {
|
|
370482
|
-
const st = s.start;
|
|
370483
|
-
const en = s.end;
|
|
370484
|
-
if (Array.isArray(st) && Array.isArray(en) && typeof st[0] === "number" && typeof st[1] === "number" && typeof en[0] === "number" && typeof en[1] === "number") {
|
|
370485
|
-
const dx = en[0] - st[0];
|
|
370486
|
-
const dy = en[1] - st[1];
|
|
370487
|
-
const len = Math.hypot(dx, dy);
|
|
370488
|
-
if (len > 0) {
|
|
370489
|
-
const ux = dx / len;
|
|
370490
|
-
const uy = dy / len;
|
|
370491
|
-
s.start = [st[0] - ux * half, st[1] - uy * half];
|
|
370492
|
-
s.end = [en[0] + ux * half, en[1] + uy * half];
|
|
370493
|
-
}
|
|
370494
|
-
}
|
|
370495
|
-
if (typeof s.width === "number") s.width = s.width + t;
|
|
370496
|
-
}
|
|
370497
|
-
}
|
|
370498
370601
|
}
|
|
370499
370602
|
}
|
|
370500
370603
|
}
|
|
@@ -370699,16 +370802,16 @@ function rotateSide(side2, cos, sin) {
|
|
|
370699
370802
|
return ry >= 0 ? "south" : "north";
|
|
370700
370803
|
}
|
|
370701
370804
|
function placeComponent(objs, dx, dy, dz, deg, wallThickness) {
|
|
370702
|
-
const
|
|
370703
|
-
const rad =
|
|
370805
|
+
const norm2 = (deg % 360 + 360) % 360;
|
|
370806
|
+
const rad = norm2 * Math.PI / 180;
|
|
370704
370807
|
const clean = (v) => Math.round(v * 1e12) / 1e12;
|
|
370705
370808
|
const cos = clean(Math.cos(rad));
|
|
370706
370809
|
const sin = clean(Math.sin(rad));
|
|
370707
|
-
const swap =
|
|
370810
|
+
const swap = norm2 === 90 || norm2 === 270;
|
|
370708
370811
|
const rp = (x, y) => [x * cos + y * sin, -x * sin + y * cos];
|
|
370709
370812
|
return objs.map((o) => {
|
|
370710
370813
|
const n3 = { ...o };
|
|
370711
|
-
if (
|
|
370814
|
+
if (norm2 !== 0) {
|
|
370712
370815
|
if (BOX_TYPES.has(n3.type)) {
|
|
370713
370816
|
const w = typeof n3.width === "number" ? n3.width : wallThickness;
|
|
370714
370817
|
const l = typeof n3.length === "number" ? n3.length : wallThickness;
|
|
@@ -370732,7 +370835,7 @@ function placeComponent(objs, dx, dy, dz, deg, wallThickness) {
|
|
|
370732
370835
|
for (const [k, v] of Object.entries(n3.wall_heights)) wh[isSide(k) ? rotateSide(k, cos, sin) : k] = v;
|
|
370733
370836
|
n3.wall_heights = wh;
|
|
370734
370837
|
}
|
|
370735
|
-
if (n3.type === "item" || n3.type === "model") n3.rotation = (((typeof n3.rotation === "number" ? n3.rotation : 0) +
|
|
370838
|
+
if (n3.type === "item" || n3.type === "model") n3.rotation = (((typeof n3.rotation === "number" ? n3.rotation : 0) + norm2) % 360 + 360) % 360;
|
|
370736
370839
|
}
|
|
370737
370840
|
if (typeof n3.x === "number") n3.x += dx;
|
|
370738
370841
|
if (typeof n3.y === "number") n3.y += dy;
|
|
@@ -372782,8 +372885,8 @@ var ZodError = class _ZodError extends Error {
|
|
|
372782
372885
|
constructor(issues) {
|
|
372783
372886
|
super();
|
|
372784
372887
|
this.issues = [];
|
|
372785
|
-
this.addIssue = (
|
|
372786
|
-
this.issues = [...this.issues,
|
|
372888
|
+
this.addIssue = (sub2) => {
|
|
372889
|
+
this.issues = [...this.issues, sub2];
|
|
372787
372890
|
};
|
|
372788
372891
|
this.addIssues = (subs = []) => {
|
|
372789
372892
|
this.issues = [...this.issues, ...subs];
|
|
@@ -372850,13 +372953,13 @@ var ZodError = class _ZodError extends Error {
|
|
|
372850
372953
|
flatten(mapper = (issue3) => issue3.message) {
|
|
372851
372954
|
const fieldErrors = {};
|
|
372852
372955
|
const formErrors = [];
|
|
372853
|
-
for (const
|
|
372854
|
-
if (
|
|
372855
|
-
const firstEl =
|
|
372956
|
+
for (const sub2 of this.issues) {
|
|
372957
|
+
if (sub2.path.length > 0) {
|
|
372958
|
+
const firstEl = sub2.path[0];
|
|
372856
372959
|
fieldErrors[firstEl] = fieldErrors[firstEl] || [];
|
|
372857
|
-
fieldErrors[firstEl].push(mapper(
|
|
372960
|
+
fieldErrors[firstEl].push(mapper(sub2));
|
|
372858
372961
|
} else {
|
|
372859
|
-
formErrors.push(mapper(
|
|
372962
|
+
formErrors.push(mapper(sub2));
|
|
372860
372963
|
}
|
|
372861
372964
|
}
|
|
372862
372965
|
return { formErrors, fieldErrors };
|
|
@@ -377156,12 +377259,12 @@ var $ZodRealError = $constructor("$ZodError", initializer, { Parent: Error });
|
|
|
377156
377259
|
function flattenError(error53, mapper = (issue3) => issue3.message) {
|
|
377157
377260
|
const fieldErrors = {};
|
|
377158
377261
|
const formErrors = [];
|
|
377159
|
-
for (const
|
|
377160
|
-
if (
|
|
377161
|
-
fieldErrors[
|
|
377162
|
-
fieldErrors[
|
|
377262
|
+
for (const sub2 of error53.issues) {
|
|
377263
|
+
if (sub2.path.length > 0) {
|
|
377264
|
+
fieldErrors[sub2.path[0]] = fieldErrors[sub2.path[0]] || [];
|
|
377265
|
+
fieldErrors[sub2.path[0]].push(mapper(sub2));
|
|
377163
377266
|
} else {
|
|
377164
|
-
formErrors.push(mapper(
|
|
377267
|
+
formErrors.push(mapper(sub2));
|
|
377165
377268
|
}
|
|
377166
377269
|
}
|
|
377167
377270
|
return { formErrors, fieldErrors };
|
|
@@ -379566,13 +379669,13 @@ function _array(Class3, element, params) {
|
|
|
379566
379669
|
});
|
|
379567
379670
|
}
|
|
379568
379671
|
function _custom(Class3, fn, _params) {
|
|
379569
|
-
const
|
|
379570
|
-
|
|
379672
|
+
const norm2 = normalizeParams(_params);
|
|
379673
|
+
norm2.abort ?? (norm2.abort = true);
|
|
379571
379674
|
const schema = new Class3({
|
|
379572
379675
|
type: "custom",
|
|
379573
379676
|
check: "custom",
|
|
379574
379677
|
fn,
|
|
379575
|
-
...
|
|
379678
|
+
...norm2
|
|
379576
379679
|
});
|
|
379577
379680
|
return schema;
|
|
379578
379681
|
}
|
|
@@ -405189,6 +405292,8 @@ var Roof = {
|
|
|
405189
405292
|
name: "name",
|
|
405190
405293
|
parapet_height: "parapet_height",
|
|
405191
405294
|
parapet_thickness: "parapet_thickness",
|
|
405295
|
+
purlin_pitch: "purlin_pitch",
|
|
405296
|
+
rafter_pitch: "rafter_pitch",
|
|
405192
405297
|
roof_type: "roof_type",
|
|
405193
405298
|
segments: "segments",
|
|
405194
405299
|
slab_thickness: "slab_thickness",
|
|
@@ -405256,7 +405361,9 @@ var Segment = {
|
|
|
405256
405361
|
sx: "sx",
|
|
405257
405362
|
sy: "sy",
|
|
405258
405363
|
tie_beam_count: "tie_beam_count",
|
|
405259
|
-
width: "width"
|
|
405364
|
+
width: "width",
|
|
405365
|
+
width_left: "width_left",
|
|
405366
|
+
width_right: "width_right"
|
|
405260
405367
|
};
|
|
405261
405368
|
var SelectInput = {
|
|
405262
405369
|
$type: "SelectInput",
|
|
@@ -405311,6 +405418,8 @@ var SpiralStaircase = {
|
|
|
405311
405418
|
};
|
|
405312
405419
|
var Staircase = {
|
|
405313
405420
|
$type: "Staircase",
|
|
405421
|
+
box_length: "box_length",
|
|
405422
|
+
box_width: "box_width",
|
|
405314
405423
|
climb: "climb",
|
|
405315
405424
|
direction: "direction",
|
|
405316
405425
|
enabled: "enabled",
|
|
@@ -406594,6 +406703,14 @@ var WadiAstReflection = class extends AbstractAstReflection {
|
|
|
406594
406703
|
name: Roof.parapet_thickness,
|
|
406595
406704
|
optional: true
|
|
406596
406705
|
},
|
|
406706
|
+
purlin_pitch: {
|
|
406707
|
+
name: Roof.purlin_pitch,
|
|
406708
|
+
optional: true
|
|
406709
|
+
},
|
|
406710
|
+
rafter_pitch: {
|
|
406711
|
+
name: Roof.rafter_pitch,
|
|
406712
|
+
optional: true
|
|
406713
|
+
},
|
|
406597
406714
|
roof_type: {
|
|
406598
406715
|
name: Roof.roof_type
|
|
406599
406716
|
},
|
|
@@ -406834,7 +406951,16 @@ var WadiAstReflection = class extends AbstractAstReflection {
|
|
|
406834
406951
|
optional: true
|
|
406835
406952
|
},
|
|
406836
406953
|
width: {
|
|
406837
|
-
name: Segment.width
|
|
406954
|
+
name: Segment.width,
|
|
406955
|
+
optional: true
|
|
406956
|
+
},
|
|
406957
|
+
width_left: {
|
|
406958
|
+
name: Segment.width_left,
|
|
406959
|
+
optional: true
|
|
406960
|
+
},
|
|
406961
|
+
width_right: {
|
|
406962
|
+
name: Segment.width_right,
|
|
406963
|
+
optional: true
|
|
406838
406964
|
}
|
|
406839
406965
|
},
|
|
406840
406966
|
superTypes: []
|
|
@@ -406996,6 +407122,14 @@ var WadiAstReflection = class extends AbstractAstReflection {
|
|
|
406996
407122
|
Staircase: {
|
|
406997
407123
|
name: Staircase.$type,
|
|
406998
407124
|
properties: {
|
|
407125
|
+
box_length: {
|
|
407126
|
+
name: Staircase.box_length,
|
|
407127
|
+
optional: true
|
|
407128
|
+
},
|
|
407129
|
+
box_width: {
|
|
407130
|
+
name: Staircase.box_width,
|
|
407131
|
+
optional: true
|
|
407132
|
+
},
|
|
406999
407133
|
climb: {
|
|
407000
407134
|
name: Staircase.climb,
|
|
407001
407135
|
optional: true
|
|
@@ -407052,7 +407186,8 @@ var WadiAstReflection = class extends AbstractAstReflection {
|
|
|
407052
407186
|
name: Staircase.step_tread
|
|
407053
407187
|
},
|
|
407054
407188
|
step_width: {
|
|
407055
|
-
name: Staircase.step_width
|
|
407189
|
+
name: Staircase.step_width,
|
|
407190
|
+
optional: true
|
|
407056
407191
|
},
|
|
407057
407192
|
turn: {
|
|
407058
407193
|
name: Staircase.turn,
|
|
@@ -413264,6 +413399,52 @@ var WadiGrammar = () => loadedWadiGrammar ?? (loadedWadiGrammar = loadGrammarFro
|
|
|
413264
413399
|
"$type": "Keyword",
|
|
413265
413400
|
"value": ")"
|
|
413266
413401
|
},
|
|
413402
|
+
{
|
|
413403
|
+
"$type": "Group",
|
|
413404
|
+
"elements": [
|
|
413405
|
+
{
|
|
413406
|
+
"$type": "Keyword",
|
|
413407
|
+
"value": "box"
|
|
413408
|
+
},
|
|
413409
|
+
{
|
|
413410
|
+
"$type": "Keyword",
|
|
413411
|
+
"value": "("
|
|
413412
|
+
},
|
|
413413
|
+
{
|
|
413414
|
+
"$type": "Assignment",
|
|
413415
|
+
"feature": "box_width",
|
|
413416
|
+
"operator": "=",
|
|
413417
|
+
"terminal": {
|
|
413418
|
+
"$type": "RuleCall",
|
|
413419
|
+
"rule": {
|
|
413420
|
+
"$ref": "#/rules@17"
|
|
413421
|
+
},
|
|
413422
|
+
"arguments": []
|
|
413423
|
+
}
|
|
413424
|
+
},
|
|
413425
|
+
{
|
|
413426
|
+
"$type": "Keyword",
|
|
413427
|
+
"value": ","
|
|
413428
|
+
},
|
|
413429
|
+
{
|
|
413430
|
+
"$type": "Assignment",
|
|
413431
|
+
"feature": "box_length",
|
|
413432
|
+
"operator": "=",
|
|
413433
|
+
"terminal": {
|
|
413434
|
+
"$type": "RuleCall",
|
|
413435
|
+
"rule": {
|
|
413436
|
+
"$ref": "#/rules@17"
|
|
413437
|
+
},
|
|
413438
|
+
"arguments": []
|
|
413439
|
+
}
|
|
413440
|
+
},
|
|
413441
|
+
{
|
|
413442
|
+
"$type": "Keyword",
|
|
413443
|
+
"value": ")"
|
|
413444
|
+
}
|
|
413445
|
+
],
|
|
413446
|
+
"cardinality": "?"
|
|
413447
|
+
},
|
|
413267
413448
|
{
|
|
413268
413449
|
"$type": "Keyword",
|
|
413269
413450
|
"value": "step"
|
|
@@ -413301,20 +413482,26 @@ var WadiGrammar = () => loadedWadiGrammar ?? (loadedWadiGrammar = loadGrammarFro
|
|
|
413301
413482
|
}
|
|
413302
413483
|
},
|
|
413303
413484
|
{
|
|
413304
|
-
"$type": "
|
|
413305
|
-
"
|
|
413306
|
-
|
|
413307
|
-
|
|
413308
|
-
|
|
413309
|
-
"feature": "step_width",
|
|
413310
|
-
"operator": "=",
|
|
413311
|
-
"terminal": {
|
|
413312
|
-
"$type": "RuleCall",
|
|
413313
|
-
"rule": {
|
|
413314
|
-
"$ref": "#/rules@17"
|
|
413485
|
+
"$type": "Group",
|
|
413486
|
+
"elements": [
|
|
413487
|
+
{
|
|
413488
|
+
"$type": "Keyword",
|
|
413489
|
+
"value": ","
|
|
413315
413490
|
},
|
|
413316
|
-
|
|
413317
|
-
|
|
413491
|
+
{
|
|
413492
|
+
"$type": "Assignment",
|
|
413493
|
+
"feature": "step_width",
|
|
413494
|
+
"operator": "=",
|
|
413495
|
+
"terminal": {
|
|
413496
|
+
"$type": "RuleCall",
|
|
413497
|
+
"rule": {
|
|
413498
|
+
"$ref": "#/rules@17"
|
|
413499
|
+
},
|
|
413500
|
+
"arguments": []
|
|
413501
|
+
}
|
|
413502
|
+
}
|
|
413503
|
+
],
|
|
413504
|
+
"cardinality": "?"
|
|
413318
413505
|
},
|
|
413319
413506
|
{
|
|
413320
413507
|
"$type": "Keyword",
|
|
@@ -415153,6 +415340,50 @@ var WadiGrammar = () => loadedWadiGrammar ?? (loadedWadiGrammar = loadGrammarFro
|
|
|
415153
415340
|
],
|
|
415154
415341
|
"cardinality": "?"
|
|
415155
415342
|
},
|
|
415343
|
+
{
|
|
415344
|
+
"$type": "Group",
|
|
415345
|
+
"elements": [
|
|
415346
|
+
{
|
|
415347
|
+
"$type": "Keyword",
|
|
415348
|
+
"value": "rafter_pitch"
|
|
415349
|
+
},
|
|
415350
|
+
{
|
|
415351
|
+
"$type": "Assignment",
|
|
415352
|
+
"feature": "rafter_pitch",
|
|
415353
|
+
"operator": "=",
|
|
415354
|
+
"terminal": {
|
|
415355
|
+
"$type": "RuleCall",
|
|
415356
|
+
"rule": {
|
|
415357
|
+
"$ref": "#/rules@17"
|
|
415358
|
+
},
|
|
415359
|
+
"arguments": []
|
|
415360
|
+
}
|
|
415361
|
+
}
|
|
415362
|
+
],
|
|
415363
|
+
"cardinality": "?"
|
|
415364
|
+
},
|
|
415365
|
+
{
|
|
415366
|
+
"$type": "Group",
|
|
415367
|
+
"elements": [
|
|
415368
|
+
{
|
|
415369
|
+
"$type": "Keyword",
|
|
415370
|
+
"value": "purlin_pitch"
|
|
415371
|
+
},
|
|
415372
|
+
{
|
|
415373
|
+
"$type": "Assignment",
|
|
415374
|
+
"feature": "purlin_pitch",
|
|
415375
|
+
"operator": "=",
|
|
415376
|
+
"terminal": {
|
|
415377
|
+
"$type": "RuleCall",
|
|
415378
|
+
"rule": {
|
|
415379
|
+
"$ref": "#/rules@17"
|
|
415380
|
+
},
|
|
415381
|
+
"arguments": []
|
|
415382
|
+
}
|
|
415383
|
+
}
|
|
415384
|
+
],
|
|
415385
|
+
"cardinality": "?"
|
|
415386
|
+
},
|
|
415156
415387
|
{
|
|
415157
415388
|
"$type": "RuleCall",
|
|
415158
415389
|
"rule": {
|
|
@@ -415438,20 +415669,75 @@ var WadiGrammar = () => loadedWadiGrammar ?? (loadedWadiGrammar = loadGrammarFro
|
|
|
415438
415669
|
"value": ")"
|
|
415439
415670
|
},
|
|
415440
415671
|
{
|
|
415441
|
-
"$type": "
|
|
415442
|
-
"
|
|
415443
|
-
|
|
415444
|
-
|
|
415445
|
-
|
|
415446
|
-
|
|
415447
|
-
|
|
415448
|
-
|
|
415449
|
-
|
|
415450
|
-
|
|
415451
|
-
|
|
415672
|
+
"$type": "Alternatives",
|
|
415673
|
+
"elements": [
|
|
415674
|
+
{
|
|
415675
|
+
"$type": "Group",
|
|
415676
|
+
"elements": [
|
|
415677
|
+
{
|
|
415678
|
+
"$type": "Keyword",
|
|
415679
|
+
"value": "width"
|
|
415680
|
+
},
|
|
415681
|
+
{
|
|
415682
|
+
"$type": "Assignment",
|
|
415683
|
+
"feature": "width",
|
|
415684
|
+
"operator": "=",
|
|
415685
|
+
"terminal": {
|
|
415686
|
+
"$type": "RuleCall",
|
|
415687
|
+
"rule": {
|
|
415688
|
+
"$ref": "#/rules@17"
|
|
415689
|
+
},
|
|
415690
|
+
"arguments": []
|
|
415691
|
+
}
|
|
415692
|
+
}
|
|
415693
|
+
]
|
|
415452
415694
|
},
|
|
415453
|
-
|
|
415454
|
-
|
|
415695
|
+
{
|
|
415696
|
+
"$type": "Group",
|
|
415697
|
+
"elements": [
|
|
415698
|
+
{
|
|
415699
|
+
"$type": "Keyword",
|
|
415700
|
+
"value": "widths"
|
|
415701
|
+
},
|
|
415702
|
+
{
|
|
415703
|
+
"$type": "Keyword",
|
|
415704
|
+
"value": "("
|
|
415705
|
+
},
|
|
415706
|
+
{
|
|
415707
|
+
"$type": "Assignment",
|
|
415708
|
+
"feature": "width_left",
|
|
415709
|
+
"operator": "=",
|
|
415710
|
+
"terminal": {
|
|
415711
|
+
"$type": "RuleCall",
|
|
415712
|
+
"rule": {
|
|
415713
|
+
"$ref": "#/rules@17"
|
|
415714
|
+
},
|
|
415715
|
+
"arguments": []
|
|
415716
|
+
}
|
|
415717
|
+
},
|
|
415718
|
+
{
|
|
415719
|
+
"$type": "Keyword",
|
|
415720
|
+
"value": ","
|
|
415721
|
+
},
|
|
415722
|
+
{
|
|
415723
|
+
"$type": "Assignment",
|
|
415724
|
+
"feature": "width_right",
|
|
415725
|
+
"operator": "=",
|
|
415726
|
+
"terminal": {
|
|
415727
|
+
"$type": "RuleCall",
|
|
415728
|
+
"rule": {
|
|
415729
|
+
"$ref": "#/rules@17"
|
|
415730
|
+
},
|
|
415731
|
+
"arguments": []
|
|
415732
|
+
}
|
|
415733
|
+
},
|
|
415734
|
+
{
|
|
415735
|
+
"$type": "Keyword",
|
|
415736
|
+
"value": ")"
|
|
415737
|
+
}
|
|
415738
|
+
]
|
|
415739
|
+
}
|
|
415740
|
+
]
|
|
415455
415741
|
},
|
|
415456
415742
|
{
|
|
415457
415743
|
"$type": "Group",
|
|
@@ -416551,9 +416837,14 @@ function staircase(s) {
|
|
|
416551
416837
|
start_y: put("start_y", s.start_y, 0),
|
|
416552
416838
|
step_rise: put("step_rise", s.step_rise, 1),
|
|
416553
416839
|
step_tread: put("step_tread", s.step_tread, 1),
|
|
416554
|
-
step_width: put("step_width", s.step_width, 1),
|
|
416555
416840
|
direction: s.direction
|
|
416556
416841
|
};
|
|
416842
|
+
if (s.box_width !== void 0 && s.box_length !== void 0) {
|
|
416843
|
+
o.width = put("width", s.box_width, 1);
|
|
416844
|
+
o.length = put("length", s.box_length, 1);
|
|
416845
|
+
}
|
|
416846
|
+
const sw = put("step_width", s.step_width, 1);
|
|
416847
|
+
if (sw !== void 0) o.step_width = sw;
|
|
416557
416848
|
if (s.name) o.name = unquote2(s.name);
|
|
416558
416849
|
if (s.climb) o.climb = s.climb;
|
|
416559
416850
|
const rh = put("rise_height", s.rise_height, 1);
|
|
@@ -416780,6 +417071,18 @@ function roof(r2) {
|
|
|
416780
417071
|
const gwt = put("gable_wall_thickness", r2.gable_wall_thickness);
|
|
416781
417072
|
if (gwt !== void 0) o.gable_wall_thickness = gwt;
|
|
416782
417073
|
if (r2.framing) o.framing = jsonObject(r2.framing);
|
|
417074
|
+
if (r2.rafter_pitch !== void 0 || r2.purlin_pitch !== void 0) {
|
|
417075
|
+
const framing = o.framing ?? {};
|
|
417076
|
+
const setPitch = (key, e, ph) => {
|
|
417077
|
+
if (e === void 0) return;
|
|
417078
|
+
const { value, formula } = fieldAndFormula(e, ph);
|
|
417079
|
+
framing[key] = value;
|
|
417080
|
+
if (formula) formulas[`framing.${key}`] = formula;
|
|
417081
|
+
};
|
|
417082
|
+
setPitch("rafter_spacing_in", r2.rafter_pitch, 36);
|
|
417083
|
+
setPitch("purlin_spacing_in", r2.purlin_pitch, 12);
|
|
417084
|
+
o.framing = framing;
|
|
417085
|
+
}
|
|
416783
417086
|
if (r2.segments.length) o.segments = r2.segments.map(roofSegment);
|
|
416784
417087
|
if (r2.trusses.length) o.trusses = r2.trusses.map(roofTruss);
|
|
416785
417088
|
applyCommon(o, formulas, r2);
|
|
@@ -416790,9 +417093,25 @@ function roofSegment(s) {
|
|
|
416790
417093
|
const seg = {
|
|
416791
417094
|
id: unquote2(s.id),
|
|
416792
417095
|
start: [put("start_x", s.sx, 0), put("start_y", s.sy, 0)],
|
|
416793
|
-
end: [put("end_x", s.ex, 0), put("end_y", s.ey, 0)]
|
|
416794
|
-
width: put("width", s.width, 1)
|
|
417096
|
+
end: [put("end_x", s.ex, 0), put("end_y", s.ey, 0)]
|
|
416795
417097
|
};
|
|
417098
|
+
if (s.width !== void 0) {
|
|
417099
|
+
seg.width = put("width", s.width, 1);
|
|
417100
|
+
} else if (s.width_left !== void 0 && s.width_right !== void 0) {
|
|
417101
|
+
const wl = put("width_left", s.width_left, 1) ?? 1;
|
|
417102
|
+
const wr = put("width_right", s.width_right, 1) ?? 1;
|
|
417103
|
+
seg.width_left = wl;
|
|
417104
|
+
seg.width_right = wr;
|
|
417105
|
+
seg.width = wl + wr;
|
|
417106
|
+
const lf = formulas.width_left, rf = formulas.width_right;
|
|
417107
|
+
if (lf || rf) {
|
|
417108
|
+
const lExpr = lf ? `(${lf.replace(/^=\s*/, "")})` : String(wl);
|
|
417109
|
+
const rExpr = rf ? `(${rf.replace(/^=\s*/, "")})` : String(wr);
|
|
417110
|
+
formulas.width = `= ${lExpr} + ${rExpr}`;
|
|
417111
|
+
}
|
|
417112
|
+
} else {
|
|
417113
|
+
seg.width = 1;
|
|
417114
|
+
}
|
|
416796
417115
|
if (s.shed_high_side) seg.shed_high_side = s.shed_high_side;
|
|
416797
417116
|
if (s.start_endpoint) seg.start_endpoint = s.start_endpoint;
|
|
416798
417117
|
if (s.end_endpoint) seg.end_endpoint = s.end_endpoint;
|
|
@@ -417336,7 +417655,26 @@ function isLeafEndpoint(entries, segmentId, which) {
|
|
|
417336
417655
|
if (!entry) return true;
|
|
417337
417656
|
return !entry.isJoint;
|
|
417338
417657
|
}
|
|
417339
|
-
function
|
|
417658
|
+
function insetQuad(rect, d) {
|
|
417659
|
+
if (!(d > 0)) return [rect[0], rect[1], rect[2], rect[3]];
|
|
417660
|
+
const cx = (rect[0][0] + rect[1][0] + rect[2][0] + rect[3][0]) / 4;
|
|
417661
|
+
const cy = (rect[0][1] + rect[1][1] + rect[2][1] + rect[3][1]) / 4;
|
|
417662
|
+
const ax = [rect[1][0] - rect[0][0], rect[1][1] - rect[0][1]];
|
|
417663
|
+
const bx = [rect[3][0] - rect[0][0], rect[3][1] - rect[0][1]];
|
|
417664
|
+
const al = Math.hypot(ax[0], ax[1]) || 1, bl = Math.hypot(bx[0], bx[1]) || 1;
|
|
417665
|
+
const ua = [ax[0] / al, ax[1] / al];
|
|
417666
|
+
const ub = [bx[0] / bl, bx[1] / bl];
|
|
417667
|
+
const move = (p) => {
|
|
417668
|
+
const dx = p[0] - cx, dy = p[1] - cy;
|
|
417669
|
+
let a = dx * ua[0] + dy * ua[1];
|
|
417670
|
+
let b = dx * ub[0] + dy * ub[1];
|
|
417671
|
+
a -= Math.sign(a) * Math.min(d, Math.abs(a));
|
|
417672
|
+
b -= Math.sign(b) * Math.min(d, Math.abs(b));
|
|
417673
|
+
return [cx + a * ua[0] + b * ub[0], cy + a * ua[1] + b * ub[1]];
|
|
417674
|
+
};
|
|
417675
|
+
return [move(rect[0]), move(rect[1]), move(rect[2]), move(rect[3])];
|
|
417676
|
+
}
|
|
417677
|
+
function ringBeamMembersForRect(rect, z2, segmentId, inset = 0) {
|
|
417340
417678
|
const out = [];
|
|
417341
417679
|
const labels = [
|
|
417342
417680
|
"right",
|
|
@@ -417344,9 +417682,10 @@ function ringBeamMembersForRect(rect, z2, segmentId) {
|
|
|
417344
417682
|
"left",
|
|
417345
417683
|
"back"
|
|
417346
417684
|
];
|
|
417685
|
+
const r2 = insetQuad(rect, inset);
|
|
417347
417686
|
for (let i2 = 0; i2 < 4; i2++) {
|
|
417348
|
-
const a =
|
|
417349
|
-
const b =
|
|
417687
|
+
const a = r2[i2];
|
|
417688
|
+
const b = r2[(i2 + 1) % 4];
|
|
417350
417689
|
out.push({
|
|
417351
417690
|
id: `${segmentId}.ring_beam.${labels[i2]}`,
|
|
417352
417691
|
start: [a[0], a[1], z2],
|
|
@@ -417369,6 +417708,11 @@ function slopeAngleRad(spec, crossHalf) {
|
|
|
417369
417708
|
return Math.atan(spec.ridge_h / crossHalf);
|
|
417370
417709
|
}
|
|
417371
417710
|
function resolvePitch(seg, cfg, crossHalf) {
|
|
417711
|
+
if (seg.width_left != null && seg.width_right != null && seg.width_left > 0 && seg.width_right > 0) {
|
|
417712
|
+
const L = seg.width_left, R = seg.width_right;
|
|
417713
|
+
const ridgeH2 = resolveRise(seg.slope_override ?? cfg.slope, crossHalf);
|
|
417714
|
+
return { ridgeH: ridgeH2, ridgeShift: (R - L) / 2, leftPitchTan: ridgeH2 / L, rightPitchTan: ridgeH2 / R };
|
|
417715
|
+
}
|
|
417372
417716
|
const left = seg.slope_left_override ?? cfg.slope_left;
|
|
417373
417717
|
const right = seg.slope_right_override ?? cfg.slope_right;
|
|
417374
417718
|
if (left && right) {
|
|
@@ -417426,7 +417770,7 @@ function derivePitchedRoof(cfg, opts) {
|
|
|
417426
417770
|
for (const seg of cfg.segments) {
|
|
417427
417771
|
const alongLen = segmentLength(seg);
|
|
417428
417772
|
if (alongLen === 0) continue;
|
|
417429
|
-
const crossHalf = seg.width / 2;
|
|
417773
|
+
const crossHalf = seg.width_left != null && seg.width_right != null ? (seg.width_left + seg.width_right) / 2 : seg.width / 2;
|
|
417430
417774
|
const { ridgeH, ridgeShift, leftPitchTan, rightPitchTan } = resolvePitch(seg, cfg, crossHalf);
|
|
417431
417775
|
const asym = ridgeShift !== 0;
|
|
417432
417776
|
if (!(ridgeH > 0)) {
|
|
@@ -417598,7 +417942,8 @@ function derivePitchedRoof(cfg, opts) {
|
|
|
417598
417942
|
);
|
|
417599
417943
|
}
|
|
417600
417944
|
const rect = segmentRect(seg);
|
|
417601
|
-
|
|
417945
|
+
const ringInset = (opts.wallThickness ?? 0) / 2;
|
|
417946
|
+
for (const rb of ringBeamMembersForRect(rect, opts.wallTopZ, seg.id, ringInset)) {
|
|
417602
417947
|
if (startRes === "open" && rb.id.endsWith(".back")) continue;
|
|
417603
417948
|
if (endRes === "open" && rb.id.endsWith(".front")) continue;
|
|
417604
417949
|
members.push(rb);
|
|
@@ -417916,7 +418261,7 @@ function deriveShedRoof(cfg, opts) {
|
|
|
417916
418261
|
const leftN = [-unit[1], unit[0]];
|
|
417917
418262
|
const highIsLeft = highSide === "left";
|
|
417918
418263
|
const rect = segmentRect(seg);
|
|
417919
|
-
for (const rb of ringBeamMembersForRect(rect, opts.wallTopZ, seg.id)) {
|
|
418264
|
+
for (const rb of ringBeamMembersForRect(rect, opts.wallTopZ, seg.id, (opts.wallThickness ?? 0) / 2)) {
|
|
417920
418265
|
if (startIsLeaf && rb.id.endsWith(".back")) continue;
|
|
417921
418266
|
if (endIsLeaf && rb.id.endsWith(".front")) continue;
|
|
417922
418267
|
const isHighEave = highIsLeft && rb.id.endsWith(".left") || !highIsLeft && rb.id.endsWith(".right");
|
|
@@ -418082,8 +418427,8 @@ function resolveJoints(cfg, spec, opts) {
|
|
|
418082
418427
|
if (segmentLength(segA) === 0 || segmentLength(segB) === 0) continue;
|
|
418083
418428
|
const uA = segmentUnitVector(segA);
|
|
418084
418429
|
const uB = segmentUnitVector(segB);
|
|
418085
|
-
const
|
|
418086
|
-
if (Math.abs(
|
|
418430
|
+
const cross2 = uA[0] * uB[1] - uA[1] * uB[0];
|
|
418431
|
+
if (Math.abs(cross2) < 1e-9) continue;
|
|
418087
418432
|
const insideA = insideSideMultiplier(segA, segB);
|
|
418088
418433
|
const insideB = insideSideMultiplier(segB, segA);
|
|
418089
418434
|
const wallA = offsetLine(segA, insideA * segA.width / 2);
|
|
@@ -419038,6 +419383,13 @@ function computeMergedV2Spec(config3, opts = {}) {
|
|
|
419038
419383
|
merged.members.push(...spec.members);
|
|
419039
419384
|
merged.planes.push(...spec.planes);
|
|
419040
419385
|
merged.trusses.push(...spec.trusses);
|
|
419386
|
+
merged.framing = {
|
|
419387
|
+
ring_beam_size_in: framing.ring_beam_size_in,
|
|
419388
|
+
ridge_size_in: framing.ridge_size_in,
|
|
419389
|
+
rafter_size_in: framing.rafter_size_in,
|
|
419390
|
+
purlin_size_in: framing.purlin_size_in,
|
|
419391
|
+
wall_thickness_u: wallThickness
|
|
419392
|
+
};
|
|
419041
419393
|
} catch (e) {
|
|
419042
419394
|
const name = obj.name;
|
|
419043
419395
|
const label = name ? `roof "${name}"` : "roof";
|
|
@@ -423944,9 +424296,9 @@ var Line$1 = class Line2 extends Shape2 {
|
|
|
423944
424296
|
return;
|
|
423945
424297
|
}
|
|
423946
424298
|
if (args.length === 1 && args[0] instanceof Object && args[0].name === "line") {
|
|
423947
|
-
let { pt, norm } = args[0];
|
|
424299
|
+
let { pt, norm: norm2 } = args[0];
|
|
423948
424300
|
this.pt = new Flatten.Point(pt);
|
|
423949
|
-
this.norm = new Flatten.Vector(
|
|
424301
|
+
this.norm = new Flatten.Vector(norm2);
|
|
423950
424302
|
return;
|
|
423951
424303
|
}
|
|
423952
424304
|
if (args.length === 2) {
|
|
@@ -430325,6 +430677,27 @@ function legendSvg(x, y) {
|
|
|
430325
430677
|
lines.push(`</g>`);
|
|
430326
430678
|
return lines.join("");
|
|
430327
430679
|
}
|
|
430680
|
+
function sectionBubble(x, y, letter, color) {
|
|
430681
|
+
return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="9" fill="#ffffff" stroke="${color}" stroke-width="1.4" /><text x="${x.toFixed(1)}" y="${(y + 3.5).toFixed(1)}" text-anchor="middle" font-size="11" font-weight="700" fill="${color}">${escapeXml(letter)}</text>`;
|
|
430682
|
+
}
|
|
430683
|
+
function detailMarker(x, y, side2, text, color, flip) {
|
|
430684
|
+
const tx = flip ? x - 12 : x + 12;
|
|
430685
|
+
const anchor = flip ? "end" : "start";
|
|
430686
|
+
return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="8" fill="${color}" stroke="#ffffff" stroke-width="1" /><text x="${x.toFixed(1)}" y="${(y + 3).toFixed(1)}" text-anchor="middle" font-size="9" font-weight="700" fill="#ffffff">${escapeXml(side2)}</text><text x="${tx.toFixed(1)}" y="${(y + 3).toFixed(1)}" text-anchor="${anchor}" font-size="9" font-weight="600" fill="none" stroke="#ffffff" stroke-width="3" stroke-linejoin="round">${escapeXml(text)}</text><text x="${tx.toFixed(1)}" y="${(y + 3).toFixed(1)}" text-anchor="${anchor}" font-size="9" font-weight="600" fill="${color}">${escapeXml(text)}</text>`;
|
|
430687
|
+
}
|
|
430688
|
+
function faceTag(x, y, label) {
|
|
430689
|
+
const common2 = `x="${x.toFixed(1)}" y="${(y + 4).toFixed(1)}" text-anchor="middle" font-size="12" font-weight="800"`;
|
|
430690
|
+
return `<text ${common2} fill="none" stroke="#ffffff" stroke-width="3.5" stroke-linejoin="round">${escapeXml(label)}</text><text ${common2} fill="#334155">${escapeXml(label)}</text>`;
|
|
430691
|
+
}
|
|
430692
|
+
function polyCentroid(verts) {
|
|
430693
|
+
let sx = 0, sy = 0;
|
|
430694
|
+
for (const v of verts) {
|
|
430695
|
+
sx += v[0];
|
|
430696
|
+
sy += v[1];
|
|
430697
|
+
}
|
|
430698
|
+
const n3 = verts.length || 1;
|
|
430699
|
+
return [sx / n3, sy / n3];
|
|
430700
|
+
}
|
|
430328
430701
|
function renderTopViewPanel(spec, opts) {
|
|
430329
430702
|
const { width, height } = opts;
|
|
430330
430703
|
const padding = opts.padding ?? 20;
|
|
@@ -430368,9 +430741,58 @@ function renderTopViewPanel(spec, opts) {
|
|
|
430368
430741
|
let deg = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
430369
430742
|
if (deg > 90) deg -= 180;
|
|
430370
430743
|
else if (deg < -90) deg += 180;
|
|
430371
|
-
|
|
430372
|
-
|
|
430373
|
-
);
|
|
430744
|
+
const ridgeCommon = `x="${lx.toFixed(1)}" y="${ly.toFixed(1)}" text-anchor="middle" dominant-baseline="middle" font-size="10" font-weight="700" transform="rotate(${deg.toFixed(1)} ${lx.toFixed(1)} ${ly.toFixed(1)})"`;
|
|
430745
|
+
const ridgeLabel = `ridge ${escapeXml(formatDimension(lenW))}`;
|
|
430746
|
+
body.push(`<text ${ridgeCommon} fill="none" stroke="#ffffff" stroke-width="3" stroke-linejoin="round">${ridgeLabel}</text>`);
|
|
430747
|
+
body.push(`<text ${ridgeCommon} fill="#b91c1c">${ridgeLabel}</text>`);
|
|
430748
|
+
}
|
|
430749
|
+
if (opts.markers) {
|
|
430750
|
+
const mk = opts.markers;
|
|
430751
|
+
const cx = (bounds.x_min + bounds.x_max) / 2;
|
|
430752
|
+
const cy = (bounds.y_min + bounds.y_max) / 2;
|
|
430753
|
+
const spanX = bounds.x_max - bounds.x_min || 1;
|
|
430754
|
+
const spanY = bounds.y_max - bounds.y_min || 1;
|
|
430755
|
+
const SECT = "#7c3aed", EAVE = "#0e7490";
|
|
430756
|
+
for (const s of mk.sections) {
|
|
430757
|
+
const p1 = s.axis === "x" ? toSvg(s.coord, bounds.y_min) : toSvg(bounds.x_min, s.coord);
|
|
430758
|
+
const p2 = s.axis === "x" ? toSvg(s.coord, bounds.y_max) : toSvg(bounds.x_max, s.coord);
|
|
430759
|
+
let ux = p2[0] - p1[0], uy = p2[1] - p1[1];
|
|
430760
|
+
const ul = Math.hypot(ux, uy) || 1;
|
|
430761
|
+
ux /= ul;
|
|
430762
|
+
uy /= ul;
|
|
430763
|
+
const e1 = [p1[0] - ux * 18, p1[1] - uy * 18];
|
|
430764
|
+
const e2 = [p2[0] + ux * 18, p2[1] + uy * 18];
|
|
430765
|
+
body.push(`<line x1="${e1[0].toFixed(1)}" y1="${e1[1].toFixed(1)}" x2="${e2[0].toFixed(1)}" y2="${e2[1].toFixed(1)}" stroke="${SECT}" stroke-width="1.3" stroke-dasharray="12 3 3 3" />`);
|
|
430766
|
+
body.push(sectionBubble(e1[0], e1[1], s.label, SECT));
|
|
430767
|
+
body.push(sectionBubble(e2[0], e2[1], s.label, SECT));
|
|
430768
|
+
}
|
|
430769
|
+
for (const e of mk.eaves) {
|
|
430770
|
+
for (const side2 of e.sides) {
|
|
430771
|
+
let wx = cx, wy = cy;
|
|
430772
|
+
if (side2 === "N") {
|
|
430773
|
+
wy = bounds.y_min + spanY * 0.05;
|
|
430774
|
+
} else if (side2 === "S") {
|
|
430775
|
+
wy = bounds.y_max - spanY * 0.05;
|
|
430776
|
+
} else if (side2 === "W") {
|
|
430777
|
+
wx = bounds.x_min + spanX * 0.05;
|
|
430778
|
+
} else if (side2 === "E") {
|
|
430779
|
+
wx = bounds.x_max - spanX * 0.05;
|
|
430780
|
+
}
|
|
430781
|
+
const [sx, sy] = toSvg(wx, wy);
|
|
430782
|
+
const flip = side2 === "E";
|
|
430783
|
+
body.push(detailMarker(sx, sy, side2, `${e.label} eave`, EAVE, flip));
|
|
430784
|
+
}
|
|
430785
|
+
}
|
|
430786
|
+
for (const f2 of mk.faces) {
|
|
430787
|
+
for (const id of f2.planeIds) {
|
|
430788
|
+
const pl = spec.planes.find((p) => p.id === id);
|
|
430789
|
+
if (!pl) continue;
|
|
430790
|
+
const [cxw, cyw] = polyCentroid(pl.vertices);
|
|
430791
|
+
const [fx, fy] = toSvg(cxw, cyw);
|
|
430792
|
+
body.push(faceTag(fx, fy, f2.label));
|
|
430793
|
+
}
|
|
430794
|
+
}
|
|
430795
|
+
body.push(`<text x="10" y="${(height - 8).toFixed(1)}" font-size="9" fill="#475569"><tspan fill="${SECT}" font-weight="700">A</tspan>\u2013A section cut \xB7 <tspan fill="${EAVE}" font-weight="700">\u25CF</tspan> eave detail \xB7 <tspan fill="#334155" font-weight="800">F#</tspan> roof face</text>`);
|
|
430374
430796
|
}
|
|
430375
430797
|
} else {
|
|
430376
430798
|
body.push(`<text x="${(width / 2).toFixed(1)}" y="${(height / 2).toFixed(1)}" text-anchor="middle" font-size="11" fill="#94a3b8">(empty roof spec)</text>`);
|
|
@@ -430386,8 +430808,11 @@ function escapeXml(s) {
|
|
|
430386
430808
|
}
|
|
430387
430809
|
|
|
430388
430810
|
// ../editor/src/svg2d/roof/v2/perspectivePanel.ts
|
|
430811
|
+
init_format();
|
|
430389
430812
|
var COS30 = Math.cos(30 * Math.PI / 180);
|
|
430390
430813
|
var SIN30 = Math.sin(30 * Math.PI / 180);
|
|
430814
|
+
var DIM_COLOR = "#b91c1c";
|
|
430815
|
+
var WIT_COLOR = "#e59aa0";
|
|
430391
430816
|
function iso(p) {
|
|
430392
430817
|
return [(p[0] - p[1]) * COS30, p[2] - (p[0] + p[1]) * SIN30];
|
|
430393
430818
|
}
|
|
@@ -430414,11 +430839,99 @@ function frameWidth(role) {
|
|
|
430414
430839
|
if (role === "pani_patti" || role === "eave_L_channel" || role === "corner_double_angle") return 1;
|
|
430415
430840
|
return 1.6;
|
|
430416
430841
|
}
|
|
430842
|
+
function buildFrameDimensions(spec) {
|
|
430843
|
+
const lines = [];
|
|
430844
|
+
const labels = [];
|
|
430845
|
+
const ring = spec.members.filter((m) => m.role === "ring_beam");
|
|
430846
|
+
const src = ring.length ? ring : spec.members;
|
|
430847
|
+
if (!src.length) return { lines, labels };
|
|
430848
|
+
let aX = Infinity, bX = -Infinity, aY = Infinity, bY = -Infinity, loZ = Infinity;
|
|
430849
|
+
for (const m of src) for (const p of [m.start, m.end]) {
|
|
430850
|
+
if (p[0] < aX) aX = p[0];
|
|
430851
|
+
if (p[0] > bX) bX = p[0];
|
|
430852
|
+
if (p[1] < aY) aY = p[1];
|
|
430853
|
+
if (p[1] > bY) bY = p[1];
|
|
430854
|
+
if (p[2] < loZ) loZ = p[2];
|
|
430855
|
+
}
|
|
430856
|
+
const baseZ = ring.length ? ring[0].start[2] : loZ;
|
|
430857
|
+
let apexZ = -Infinity;
|
|
430858
|
+
for (const m of spec.members) for (const p of [m.start, m.end]) if (p[2] > apexZ) apexZ = p[2];
|
|
430859
|
+
for (const t of spec.trusses) if (t.apex[2] > apexZ) apexZ = t.apex[2];
|
|
430860
|
+
if (!isFinite(apexZ)) apexZ = baseZ;
|
|
430861
|
+
const boxW = bX - aX, boxL = bY - aY, rise = apexZ - baseZ;
|
|
430862
|
+
const off = Math.max(0.12 * Math.max(boxW, boxL), 12);
|
|
430863
|
+
const addDim = (a, b, text) => {
|
|
430864
|
+
lines.push({ a, b, color: DIM_COLOR, w: 1, tick: true });
|
|
430865
|
+
labels.push({ at: [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2], text });
|
|
430866
|
+
};
|
|
430867
|
+
const addWit = (a, b) => lines.push({ a, b, color: WIT_COLOR, w: 0.5, dash: "3 2", tick: false });
|
|
430868
|
+
{
|
|
430869
|
+
const dy = aY - off;
|
|
430870
|
+
addDim([aX, dy, baseZ], [bX, dy, baseZ], formatDimension(boxW));
|
|
430871
|
+
addWit([aX, aY, baseZ], [aX, dy, baseZ]);
|
|
430872
|
+
addWit([bX, aY, baseZ], [bX, dy, baseZ]);
|
|
430873
|
+
}
|
|
430874
|
+
{
|
|
430875
|
+
const dx = aX - off;
|
|
430876
|
+
addDim([dx, aY, baseZ], [dx, bY, baseZ], formatDimension(boxL));
|
|
430877
|
+
addWit([aX, aY, baseZ], [dx, aY, baseZ]);
|
|
430878
|
+
addWit([aX, bY, baseZ], [dx, bY, baseZ]);
|
|
430879
|
+
}
|
|
430880
|
+
{
|
|
430881
|
+
const cx = aX - off, cy = aY - off;
|
|
430882
|
+
addDim([cx, cy, baseZ], [cx, cy, apexZ], formatDimension(rise));
|
|
430883
|
+
addWit([aX, aY, baseZ], [cx, cy, baseZ]);
|
|
430884
|
+
}
|
|
430885
|
+
if (spec.trusses.length > 1) {
|
|
430886
|
+
const cen = spec.trusses.map((t) => [
|
|
430887
|
+
(t.bottom_left[0] + t.bottom_right[0]) / 2,
|
|
430888
|
+
(t.bottom_left[1] + t.bottom_right[1]) / 2
|
|
430889
|
+
]);
|
|
430890
|
+
const spanX = Math.max(...cen.map((c) => c[0])) - Math.min(...cen.map((c) => c[0]));
|
|
430891
|
+
const spanY = Math.max(...cen.map((c) => c[1])) - Math.min(...cen.map((c) => c[1]));
|
|
430892
|
+
const axis = spanX >= spanY ? 0 : 1;
|
|
430893
|
+
const pos = cen.map((c) => c[axis]).sort((p, q) => p - q);
|
|
430894
|
+
if (axis === 1) {
|
|
430895
|
+
const dx = bX + off;
|
|
430896
|
+
const marks = [aY, ...pos, bY];
|
|
430897
|
+
for (let i2 = 0; i2 + 1 < marks.length; i2++) {
|
|
430898
|
+
const g = Math.abs(marks[i2 + 1] - marks[i2]);
|
|
430899
|
+
if (g < 0.5) continue;
|
|
430900
|
+
addDim([dx, marks[i2], baseZ], [dx, marks[i2 + 1], baseZ], formatDimension(g));
|
|
430901
|
+
}
|
|
430902
|
+
for (const mk of marks) addWit([bX, mk, baseZ], [dx, mk, baseZ]);
|
|
430903
|
+
} else {
|
|
430904
|
+
const dy = bY + off;
|
|
430905
|
+
const marks = [aX, ...pos, bX];
|
|
430906
|
+
for (let i2 = 0; i2 + 1 < marks.length; i2++) {
|
|
430907
|
+
const g = Math.abs(marks[i2 + 1] - marks[i2]);
|
|
430908
|
+
if (g < 0.5) continue;
|
|
430909
|
+
addDim([marks[i2], dy, baseZ], [marks[i2 + 1], dy, baseZ], formatDimension(g));
|
|
430910
|
+
}
|
|
430911
|
+
for (const mk of marks) addWit([mk, bY, baseZ], [mk, dy, baseZ]);
|
|
430912
|
+
}
|
|
430913
|
+
}
|
|
430914
|
+
return { lines, labels };
|
|
430915
|
+
}
|
|
430916
|
+
function haloText(x, y, text, opts) {
|
|
430917
|
+
const size = opts.size ?? 10;
|
|
430918
|
+
const weight = opts.weight ?? 600;
|
|
430919
|
+
const halo = opts.halo ?? 3;
|
|
430920
|
+
const xf = x.toFixed(1), yf = y.toFixed(1);
|
|
430921
|
+
const rot = opts.rotate != null ? ` transform="rotate(${opts.rotate.toFixed(1)} ${xf} ${yf})"` : "";
|
|
430922
|
+
const mid2 = opts.middle ? ` dominant-baseline="middle"` : "";
|
|
430923
|
+
const base = `x="${xf}" y="${yf}" text-anchor="middle"${mid2} font-size="${size}" font-weight="${weight}"${rot}`;
|
|
430924
|
+
return `<text ${base} fill="none" stroke="#fdfcfa" stroke-width="${halo}" stroke-linejoin="round">${text}</text>
|
|
430925
|
+
<text ${base} fill="${opts.fill}">${text}</text>
|
|
430926
|
+
`;
|
|
430927
|
+
}
|
|
430417
430928
|
function v2PerspectivePanel(x0, y0, width, height, spec, opts = {}) {
|
|
430929
|
+
const doDim = opts.dimensions === true;
|
|
430418
430930
|
const titleH = opts.title ? 40 : 0;
|
|
430419
|
-
const innerPad = 20;
|
|
430931
|
+
const innerPad = doDim ? 40 : 20;
|
|
430420
430932
|
const drawW = width - 2 * innerPad;
|
|
430421
430933
|
const drawH = height - titleH - 2 * innerPad;
|
|
430934
|
+
const dim = doDim ? buildFrameDimensions(spec) : { lines: [], labels: [] };
|
|
430422
430935
|
let minIx = Infinity, maxIx = -Infinity;
|
|
430423
430936
|
let minIy = Infinity, maxIy = -Infinity;
|
|
430424
430937
|
const consider = (p) => {
|
|
@@ -430433,6 +430946,10 @@ function v2PerspectivePanel(x0, y0, width, height, spec, opts = {}) {
|
|
|
430433
430946
|
consider(m.start);
|
|
430434
430947
|
consider(m.end);
|
|
430435
430948
|
}
|
|
430949
|
+
for (const wl of dim.lines) {
|
|
430950
|
+
consider(wl.a);
|
|
430951
|
+
consider(wl.b);
|
|
430952
|
+
}
|
|
430436
430953
|
if (!isFinite(minIx)) return "";
|
|
430437
430954
|
const isoW = maxIx - minIx || 1;
|
|
430438
430955
|
const isoH = maxIy - minIy || 1;
|
|
@@ -430466,12 +430983,218 @@ function v2PerspectivePanel(x0, y0, width, height, spec, opts = {}) {
|
|
|
430466
430983
|
const members = t.members ?? buildTrussMembers(t);
|
|
430467
430984
|
for (const m of members) svg += line2(m);
|
|
430468
430985
|
}
|
|
430986
|
+
if (doDim) {
|
|
430987
|
+
const pc = [x0 + width / 2, y0 + height / 2];
|
|
430988
|
+
for (const wl of dim.lines) {
|
|
430989
|
+
const [x1, y1] = toSvg(wl.a);
|
|
430990
|
+
const [x2, y2] = toSvg(wl.b);
|
|
430991
|
+
svg += `<line x1="${x1.toFixed(2)}" y1="${y1.toFixed(2)}" x2="${x2.toFixed(2)}" y2="${y2.toFixed(2)}" stroke="${wl.color}" stroke-width="${wl.w}"${wl.dash ? ` stroke-dasharray="${wl.dash}"` : ""} stroke-linecap="round"/>
|
|
430992
|
+
`;
|
|
430993
|
+
if (wl.tick) {
|
|
430994
|
+
let ux = x2 - x1, uy = y2 - y1;
|
|
430995
|
+
const d = Math.hypot(ux, uy) || 1;
|
|
430996
|
+
ux /= d;
|
|
430997
|
+
uy /= d;
|
|
430998
|
+
const px = -uy * 4, py = ux * 4;
|
|
430999
|
+
for (const [tx, ty] of [[x1, y1], [x2, y2]]) {
|
|
431000
|
+
svg += `<line x1="${(tx - px).toFixed(2)}" y1="${(ty - py).toFixed(2)}" x2="${(tx + px).toFixed(2)}" y2="${(ty + py).toFixed(2)}" stroke="${wl.color}" stroke-width="${wl.w}"/>
|
|
431001
|
+
`;
|
|
431002
|
+
}
|
|
431003
|
+
}
|
|
431004
|
+
}
|
|
431005
|
+
for (const lb of dim.labels) {
|
|
431006
|
+
const [mx, my] = toSvg(lb.at);
|
|
431007
|
+
let dx = mx - pc[0], dy = my - pc[1];
|
|
431008
|
+
const d = Math.hypot(dx, dy) || 1;
|
|
431009
|
+
dx /= d;
|
|
431010
|
+
dy /= d;
|
|
431011
|
+
const lx = mx + dx * 12, ly = my + dy * 12;
|
|
431012
|
+
svg += haloText(lx, ly + 3, lb.text, { fill: DIM_COLOR, size: 11, weight: 700 });
|
|
431013
|
+
}
|
|
431014
|
+
const majorEnds = [];
|
|
431015
|
+
for (const t of spec.trusses) majorEnds.push(t.apex);
|
|
431016
|
+
for (const mm of spec.members) {
|
|
431017
|
+
if (mm.role === "ridge" || mm.role === "hip" || mm.role === "valley" || mm.role === "ring_beam") {
|
|
431018
|
+
majorEnds.push(mm.start);
|
|
431019
|
+
majorEnds.push(mm.end);
|
|
431020
|
+
}
|
|
431021
|
+
}
|
|
431022
|
+
const segNodes = (m) => {
|
|
431023
|
+
const d = [m.end[0] - m.start[0], m.end[1] - m.start[1], m.end[2] - m.start[2]];
|
|
431024
|
+
const len2 = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
|
|
431025
|
+
const len = Math.sqrt(len2);
|
|
431026
|
+
if (len < 1e-6) return [0, 1];
|
|
431027
|
+
const raw = [];
|
|
431028
|
+
for (const p of majorEnds) {
|
|
431029
|
+
const t = ((p[0] - m.start[0]) * d[0] + (p[1] - m.start[1]) * d[1] + (p[2] - m.start[2]) * d[2]) / len2;
|
|
431030
|
+
if (t <= 0.02 || t >= 0.98) continue;
|
|
431031
|
+
const proj = [m.start[0] + t * d[0], m.start[1] + t * d[1], m.start[2] + t * d[2]];
|
|
431032
|
+
if (Math.hypot(p[0] - proj[0], p[1] - proj[1], p[2] - proj[2]) < 1) raw.push(t);
|
|
431033
|
+
}
|
|
431034
|
+
raw.sort((a, b) => a - b);
|
|
431035
|
+
const mid2 = [];
|
|
431036
|
+
for (const t of raw) if (!mid2.length || (t - mid2[mid2.length - 1]) * len > 2) mid2.push(t);
|
|
431037
|
+
return [0, ...mid2, 1];
|
|
431038
|
+
};
|
|
431039
|
+
const lerp3 = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
431040
|
+
for (const m of spec.members) {
|
|
431041
|
+
if (m.role !== "ridge" && m.role !== "hip" && m.role !== "valley") continue;
|
|
431042
|
+
const L = Math.hypot(m.end[0] - m.start[0], m.end[1] - m.start[1], m.end[2] - m.start[2]);
|
|
431043
|
+
if (L < 1) continue;
|
|
431044
|
+
const col = FRAME_STROKES[m.role] ?? "#334155";
|
|
431045
|
+
const nodes = segNodes(m);
|
|
431046
|
+
for (let k = 1; k < nodes.length - 1; k++) {
|
|
431047
|
+
const [nx0, ny0] = toSvg(lerp3(m.start, m.end, nodes[k]));
|
|
431048
|
+
const [ex, ey] = toSvg(m.end);
|
|
431049
|
+
const [sx, sy] = toSvg(m.start);
|
|
431050
|
+
let ux = ex - sx, uy = ey - sy;
|
|
431051
|
+
const ul = Math.hypot(ux, uy) || 1;
|
|
431052
|
+
ux /= ul;
|
|
431053
|
+
uy /= ul;
|
|
431054
|
+
svg += `<line x1="${(nx0 + uy * 4).toFixed(1)}" y1="${(ny0 - ux * 4).toFixed(1)}" x2="${(nx0 - uy * 4).toFixed(1)}" y2="${(ny0 + ux * 4).toFixed(1)}" stroke="${col}" stroke-width="1.2"/>
|
|
431055
|
+
`;
|
|
431056
|
+
}
|
|
431057
|
+
for (let k = 0; k + 1 < nodes.length; k++) {
|
|
431058
|
+
const segLen = (nodes[k + 1] - nodes[k]) * L;
|
|
431059
|
+
if (segLen < 1) continue;
|
|
431060
|
+
const [x1, y1] = toSvg(lerp3(m.start, m.end, nodes[k]));
|
|
431061
|
+
const [x2, y2] = toSvg(lerp3(m.start, m.end, nodes[k + 1]));
|
|
431062
|
+
const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
|
|
431063
|
+
let deg = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
|
|
431064
|
+
if (deg > 90) deg -= 180;
|
|
431065
|
+
else if (deg < -90) deg += 180;
|
|
431066
|
+
let nx = -(y2 - y1), ny = x2 - x1;
|
|
431067
|
+
const nl = Math.hypot(nx, ny) || 1;
|
|
431068
|
+
nx /= nl;
|
|
431069
|
+
ny /= nl;
|
|
431070
|
+
if (ny > 0) {
|
|
431071
|
+
nx = -nx;
|
|
431072
|
+
ny = -ny;
|
|
431073
|
+
}
|
|
431074
|
+
const lx = mx + nx * 8, ly = my + ny * 8;
|
|
431075
|
+
svg += haloText(lx, ly, formatDimension(segLen), { fill: col, size: 10, weight: 600, middle: true, rotate: deg });
|
|
431076
|
+
}
|
|
431077
|
+
}
|
|
431078
|
+
const eaves = spec.members.filter((m) => m.role === "pani_patti");
|
|
431079
|
+
if (eaves.length) {
|
|
431080
|
+
let rminX = Infinity, rmaxX = -Infinity, rminY = Infinity, rmaxY = -Infinity, wallTopZ = -Infinity;
|
|
431081
|
+
for (const m of spec.members) if (m.role === "ring_beam") for (const p of [m.start, m.end]) {
|
|
431082
|
+
if (p[0] < rminX) rminX = p[0];
|
|
431083
|
+
if (p[0] > rmaxX) rmaxX = p[0];
|
|
431084
|
+
if (p[1] < rminY) rminY = p[1];
|
|
431085
|
+
if (p[1] > rmaxY) rmaxY = p[1];
|
|
431086
|
+
if (p[2] > wallTopZ) wallTopZ = p[2];
|
|
431087
|
+
}
|
|
431088
|
+
const halfWall = (spec.framing?.wall_thickness_u ?? 0) / 2;
|
|
431089
|
+
rminX -= halfWall;
|
|
431090
|
+
rmaxX += halfWall;
|
|
431091
|
+
rminY -= halfWall;
|
|
431092
|
+
rmaxY += halfWall;
|
|
431093
|
+
const cx0 = (rminX + rmaxX) / 2, cy0 = (rminY + rmaxY) / 2;
|
|
431094
|
+
const front = eaves.map((m) => ({ m, key: (m.start[0] + m.end[0]) / 2 + (m.start[1] + m.end[1]) / 2 })).sort((a, b) => b.key - a.key).slice(0, 2).map((s) => s.m);
|
|
431095
|
+
const EAVE = "#0e7490";
|
|
431096
|
+
for (const m of front) {
|
|
431097
|
+
const L = Math.hypot(m.end[0] - m.start[0], m.end[1] - m.start[1], m.end[2] - m.start[2]);
|
|
431098
|
+
const [x1, y1] = toSvg(m.start), [x2, y2] = toSvg(m.end);
|
|
431099
|
+
const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
|
|
431100
|
+
let deg = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
|
|
431101
|
+
if (deg > 90) deg -= 180;
|
|
431102
|
+
else if (deg < -90) deg += 180;
|
|
431103
|
+
let tx = mx - pc[0], ty = my - pc[1];
|
|
431104
|
+
const tl = Math.hypot(tx, ty) || 1;
|
|
431105
|
+
const lx = mx - tx / tl * 12, ly = my - ty / tl * 12;
|
|
431106
|
+
svg += haloText(lx, ly, `eave ${formatDimension(L)}`, { fill: EAVE, size: 10, weight: 700, middle: true, rotate: deg });
|
|
431107
|
+
const vertical = Math.abs(m.start[0] - m.end[0]) < 1;
|
|
431108
|
+
const s = lerp3(m.start, m.end, 0.28);
|
|
431109
|
+
let inward, oh;
|
|
431110
|
+
if (vertical) {
|
|
431111
|
+
const X = m.start[0];
|
|
431112
|
+
const wallX = X > cx0 ? rmaxX : rminX;
|
|
431113
|
+
oh = Math.abs(X - wallX);
|
|
431114
|
+
inward = [wallX, s[1], s[2]];
|
|
431115
|
+
} else {
|
|
431116
|
+
const Y = m.start[1];
|
|
431117
|
+
const wallY = Y > cy0 ? rmaxY : rminY;
|
|
431118
|
+
oh = Math.abs(Y - wallY);
|
|
431119
|
+
inward = [s[0], wallY, s[2]];
|
|
431120
|
+
}
|
|
431121
|
+
const [ex, ey] = toSvg(s), [ix, iy] = toSvg(inward);
|
|
431122
|
+
svg += `<line x1="${ex.toFixed(1)}" y1="${ey.toFixed(1)}" x2="${ix.toFixed(1)}" y2="${iy.toFixed(1)}" stroke="${EAVE}" stroke-width="1"/>
|
|
431123
|
+
`;
|
|
431124
|
+
for (const [tx2, ty2] of [[ex, ey], [ix, iy]]) {
|
|
431125
|
+
let ux = ix - ex, uy = iy - ey;
|
|
431126
|
+
const ul = Math.hypot(ux, uy) || 1;
|
|
431127
|
+
ux /= ul;
|
|
431128
|
+
uy /= ul;
|
|
431129
|
+
svg += `<line x1="${(tx2 - uy * 3).toFixed(1)}" y1="${(ty2 + ux * 3).toFixed(1)}" x2="${(tx2 + uy * 3).toFixed(1)}" y2="${(ty2 - ux * 3).toFixed(1)}" stroke="${EAVE}" stroke-width="1"/>
|
|
431130
|
+
`;
|
|
431131
|
+
}
|
|
431132
|
+
const [wx, wy] = toSvg([inward[0], inward[1], wallTopZ]);
|
|
431133
|
+
svg += `<line x1="${ix.toFixed(1)}" y1="${iy.toFixed(1)}" x2="${wx.toFixed(1)}" y2="${wy.toFixed(1)}" stroke="${EAVE}" stroke-width="0.5" stroke-dasharray="2 2"/>
|
|
431134
|
+
`;
|
|
431135
|
+
const omx = (ex + ix) / 2, omy = (ey + iy) / 2;
|
|
431136
|
+
svg += haloText(omx, omy - 4, `\u21A4 ${formatDimension(oh)}`, { fill: EAVE, size: 9, weight: 600, halo: 2.5 });
|
|
431137
|
+
}
|
|
431138
|
+
}
|
|
431139
|
+
svg += `<text x="${(x0 + 12).toFixed(1)}" y="${(y0 + height - 12).toFixed(1)}" text-anchor="start" font-size="9" fill="#b91c1c">red = overall + truss spacing \xB7 brown = ridge & hip segments \xB7 <tspan fill="#0e7490">teal = front eave length + overhang from wall</tspan></text>
|
|
431140
|
+
`;
|
|
431141
|
+
}
|
|
430469
431142
|
svg += `</g>
|
|
430470
431143
|
`;
|
|
430471
431144
|
return svg;
|
|
430472
431145
|
}
|
|
430473
431146
|
|
|
431147
|
+
// ../editor/src/svg2d/roof/v2/panelDraw.ts
|
|
431148
|
+
function escapeXml2(s) {
|
|
431149
|
+
return s.replace(
|
|
431150
|
+
/[<>&'"]/g,
|
|
431151
|
+
(c) => c === "<" ? "<" : c === ">" ? ">" : c === "&" ? "&" : c === "'" ? "'" : """
|
|
431152
|
+
);
|
|
431153
|
+
}
|
|
431154
|
+
function dimLineH(x1, x2, y, label) {
|
|
431155
|
+
const lo = Math.min(x1, x2), hi = Math.max(x1, x2);
|
|
431156
|
+
return `<line x1="${lo.toFixed(1)}" y1="${y.toFixed(1)}" x2="${hi.toFixed(1)}" y2="${y.toFixed(1)}" stroke="#334155" stroke-width="0.8"/>
|
|
431157
|
+
<line x1="${lo.toFixed(1)}" y1="${(y - 3).toFixed(1)}" x2="${lo.toFixed(1)}" y2="${(y + 3).toFixed(1)}" stroke="#334155" stroke-width="0.8"/>
|
|
431158
|
+
<line x1="${hi.toFixed(1)}" y1="${(y - 3).toFixed(1)}" x2="${hi.toFixed(1)}" y2="${(y + 3).toFixed(1)}" stroke="#334155" stroke-width="0.8"/>
|
|
431159
|
+
<text x="${((lo + hi) / 2).toFixed(1)}" y="${(y - 4).toFixed(1)}" text-anchor="middle" font-size="10" fill="#334155">${escapeXml2(label)}</text>
|
|
431160
|
+
`;
|
|
431161
|
+
}
|
|
431162
|
+
function dimLineV(y1, y2, x, label) {
|
|
431163
|
+
const lo = Math.min(y1, y2), hi = Math.max(y1, y2);
|
|
431164
|
+
return `<line x1="${x.toFixed(1)}" y1="${lo.toFixed(1)}" x2="${x.toFixed(1)}" y2="${hi.toFixed(1)}" stroke="#334155" stroke-width="0.8"/>
|
|
431165
|
+
<line x1="${(x - 3).toFixed(1)}" y1="${lo.toFixed(1)}" x2="${(x + 3).toFixed(1)}" y2="${lo.toFixed(1)}" stroke="#334155" stroke-width="0.8"/>
|
|
431166
|
+
<line x1="${(x - 3).toFixed(1)}" y1="${hi.toFixed(1)}" x2="${(x + 3).toFixed(1)}" y2="${hi.toFixed(1)}" stroke="#334155" stroke-width="0.8"/>
|
|
431167
|
+
<text x="${(x + 4).toFixed(1)}" y="${((lo + hi) / 2).toFixed(1)}" text-anchor="middle" dominant-baseline="middle" font-size="10" fill="#334155" transform="rotate(-90 ${(x + 4).toFixed(1)} ${((lo + hi) / 2).toFixed(1)})">${escapeXml2(label)}</text>
|
|
431168
|
+
`;
|
|
431169
|
+
}
|
|
431170
|
+
function panelFrame(x0, y0, width, height, titleH, title, cls) {
|
|
431171
|
+
const cx = x0 + width / 2;
|
|
431172
|
+
return `<g class="${cls}">
|
|
431173
|
+
<rect x="${x0}" y="${y0}" width="${width}" height="${height}" fill="#fdfcfa" stroke="#333" stroke-width="1"/>
|
|
431174
|
+
<rect x="${x0}" y="${y0}" width="${width}" height="${titleH}" fill="#f2ede4" stroke="#333" stroke-width="1"/>
|
|
431175
|
+
<text x="${cx}" y="${y0 + 26}" text-anchor="middle" font-size="14" font-weight="600" fill="#222">${escapeXml2(title)}</text>
|
|
431176
|
+
`;
|
|
431177
|
+
}
|
|
431178
|
+
function keyPlanDisc(cx, cy, r2, letter, fill, stroke, textColor) {
|
|
431179
|
+
return `<circle cx="${cx.toFixed(1)}" cy="${cy.toFixed(1)}" r="${r2}" fill="${fill}" stroke="${stroke}" stroke-width="1.5"/><text x="${cx.toFixed(1)}" y="${(cy + r2 * 0.36).toFixed(1)}" text-anchor="middle" font-size="${Math.round(r2 * 1.1)}" font-weight="700" fill="${textColor}">${escapeXml2(letter)}</text>`;
|
|
431180
|
+
}
|
|
431181
|
+
function keyPlanTag(xRight, yTop, label, fill, textColor) {
|
|
431182
|
+
const w = 12 + label.length * 8;
|
|
431183
|
+
const x = xRight - w;
|
|
431184
|
+
return `<rect x="${x.toFixed(1)}" y="${yTop.toFixed(1)}" width="${w}" height="22" rx="5" fill="${fill}" stroke="#334155" stroke-width="0.9"/><text x="${(x + w / 2).toFixed(1)}" y="${(yTop + 15.5).toFixed(1)}" text-anchor="middle" font-size="13" font-weight="800" fill="${textColor}">${escapeXml2(label)}</text>`;
|
|
431185
|
+
}
|
|
431186
|
+
function sectionFtLabel(s) {
|
|
431187
|
+
if (!s) return null;
|
|
431188
|
+
const inch = (ft) => {
|
|
431189
|
+
const totalIn = Math.round(ft * 12);
|
|
431190
|
+
if (totalIn % 12 === 0 && totalIn >= 12) return `${totalIn / 12}'`;
|
|
431191
|
+
return `${totalIn}"`;
|
|
431192
|
+
};
|
|
431193
|
+
return `${inch(s[0])}\xD7${inch(s[1])}`;
|
|
431194
|
+
}
|
|
431195
|
+
|
|
430474
431196
|
// ../editor/src/svg2d/roof/v2/sectionPanel.ts
|
|
431197
|
+
init_format();
|
|
430475
431198
|
function planePolygonSectionSegments(poly, cutAxis, cutCoord) {
|
|
430476
431199
|
if (poly.length < 3) return [];
|
|
430477
431200
|
const idx = cutAxis === "x" ? 0 : 1;
|
|
@@ -430508,6 +431231,26 @@ function planePolygonSectionSegments(poly, cutAxis, cutCoord) {
|
|
|
430508
431231
|
}
|
|
430509
431232
|
return segs;
|
|
430510
431233
|
}
|
|
431234
|
+
function trussMembers(t) {
|
|
431235
|
+
if (t.members && t.members.length) return t.members;
|
|
431236
|
+
return (t.kind === "mono_pitch" ? buildMonoPitchTrussMembers : buildFinkTrussMembers)(t);
|
|
431237
|
+
}
|
|
431238
|
+
function trussForSection(spec, cutAxis) {
|
|
431239
|
+
const horIdx = cutAxis === "x" ? 1 : 0;
|
|
431240
|
+
let best = null;
|
|
431241
|
+
let bestScore = 0.7;
|
|
431242
|
+
for (const t of spec.trusses) {
|
|
431243
|
+
const dx = t.bottom_right[0] - t.bottom_left[0];
|
|
431244
|
+
const dy = t.bottom_right[1] - t.bottom_left[1];
|
|
431245
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
431246
|
+
const along = Math.abs((horIdx === 0 ? dx : dy) / len);
|
|
431247
|
+
if (along > bestScore) {
|
|
431248
|
+
bestScore = along;
|
|
431249
|
+
best = t;
|
|
431250
|
+
}
|
|
431251
|
+
}
|
|
431252
|
+
return best;
|
|
431253
|
+
}
|
|
430511
431254
|
var PLANE_STROKES = {
|
|
430512
431255
|
slope: "#8B4513",
|
|
430513
431256
|
hip_face: "#8B4513",
|
|
@@ -430518,8 +431261,10 @@ var PLANE_STROKES = {
|
|
|
430518
431261
|
function v2SectionPanel(x0, y0, width, height, spec, opts) {
|
|
430519
431262
|
const titleH = opts.title ? 40 : 0;
|
|
430520
431263
|
const innerPad = 20;
|
|
430521
|
-
const
|
|
430522
|
-
const
|
|
431264
|
+
const dimL = opts.dimensions ? 46 : 0;
|
|
431265
|
+
const dimB = opts.dimensions ? 58 : 0;
|
|
431266
|
+
const drawW = width - 2 * innerPad - dimL;
|
|
431267
|
+
const drawH = height - titleH - 2 * innerPad - dimB;
|
|
430523
431268
|
const allSegs = [];
|
|
430524
431269
|
for (const p of spec.planes) {
|
|
430525
431270
|
if (p.role === "parapet") continue;
|
|
@@ -430547,7 +431292,7 @@ function v2SectionPanel(x0, y0, width, height, spec, opts) {
|
|
|
430547
431292
|
const horSpan = maxH - minH || 1;
|
|
430548
431293
|
const zSpan = maxZ - minZ || 1;
|
|
430549
431294
|
const scale = Math.min(drawW / horSpan, drawH / zSpan);
|
|
430550
|
-
const offX = x0 + innerPad + (drawW - horSpan * scale) / 2 - minH * scale;
|
|
431295
|
+
const offX = x0 + innerPad + dimL + (drawW - horSpan * scale) / 2 - minH * scale;
|
|
430551
431296
|
const offY = y0 + titleH + innerPad + (drawH - zSpan * scale) / 2 + maxZ * scale;
|
|
430552
431297
|
const toSvg = (p) => [
|
|
430553
431298
|
offX + p[horIdx] * scale,
|
|
@@ -430561,6 +431306,7 @@ function v2SectionPanel(x0, y0, width, height, spec, opts) {
|
|
|
430561
431306
|
svg += `<text x="${x0 + width / 2}" y="${y0 + 24}" text-anchor="middle" font-size="14" font-weight="bold" fill="#222">${opts.title}</text>
|
|
430562
431307
|
`;
|
|
430563
431308
|
}
|
|
431309
|
+
if (opts.keyLabel) svg += keyPlanDisc(x0 + width - 26, y0 + 20, 13, opts.keyLabel, "#ffffff", "#7c3aed", "#7c3aed");
|
|
430564
431310
|
if (opts.wallTopZ != null) {
|
|
430565
431311
|
const [x1, y1] = toSvg([minH, minH, opts.wallTopZ]);
|
|
430566
431312
|
const [x2, y2] = toSvg([maxH, maxH, opts.wallTopZ]);
|
|
@@ -430593,6 +431339,66 @@ function v2SectionPanel(x0, y0, width, height, spec, opts) {
|
|
|
430593
431339
|
const color = MEMBER_MARK_COLORS[m.role] ?? "#334155";
|
|
430594
431340
|
const half = 3;
|
|
430595
431341
|
svg += `<rect x="${(sx - half).toFixed(2)}" y="${(sy - half).toFixed(2)}" width="${(half * 2).toFixed(2)}" height="${(half * 2).toFixed(2)}" fill="${color}" stroke="#111" stroke-width="0.4"/>
|
|
431342
|
+
`;
|
|
431343
|
+
}
|
|
431344
|
+
if (opts.overlayTruss) {
|
|
431345
|
+
const t = trussForSection(spec, opts.cutAxis);
|
|
431346
|
+
if (t) {
|
|
431347
|
+
const kindLabel = (t.kind ?? "fink") === "mono_pitch" ? "mono-pitch" : "fink";
|
|
431348
|
+
for (const m of trussMembers(t)) {
|
|
431349
|
+
const isChord = m.role === "truss_top_chord" || m.role === "truss_bottom_chord";
|
|
431350
|
+
const [x1, y1] = toSvg(m.start);
|
|
431351
|
+
const [x2, y2] = toSvg(m.end);
|
|
431352
|
+
svg += `<line x1="${x1.toFixed(2)}" y1="${y1.toFixed(2)}" x2="${x2.toFixed(2)}" y2="${y2.toFixed(2)}" stroke="${isChord ? "#7c2d12" : "#b45309"}" stroke-width="${isChord ? 1.8 : 1}" stroke-dasharray="${isChord ? "6 3" : "3 3"}" stroke-linecap="round"/>
|
|
431353
|
+
`;
|
|
431354
|
+
}
|
|
431355
|
+
svg += `<text x="${(x0 + width - 12).toFixed(1)}" y="${(y0 + height - 12).toFixed(1)}" text-anchor="end" font-size="9" fill="#7c2d12">\u2014 \u2014 ${kindLabel} truss profile at this bay (see truss drawing for member lengths)</text>
|
|
431356
|
+
`;
|
|
431357
|
+
}
|
|
431358
|
+
}
|
|
431359
|
+
if (opts.dimensions) {
|
|
431360
|
+
const hz = (h, z2) => {
|
|
431361
|
+
const p = [0, 0, z2];
|
|
431362
|
+
p[horIdx] = h;
|
|
431363
|
+
return p;
|
|
431364
|
+
};
|
|
431365
|
+
const ringHs = [];
|
|
431366
|
+
for (const m of spec.members) if (m.role === "ring_beam") ringHs.push(m.start[horIdx], m.end[horIdx]);
|
|
431367
|
+
const half = (spec.framing?.wall_thickness_u ?? 0) / 2;
|
|
431368
|
+
const eMinH = minH, eMaxH = maxH;
|
|
431369
|
+
const rMinH = (ringHs.length ? Math.min(...ringHs) : eMinH) - half;
|
|
431370
|
+
const rMaxH = (ringHs.length ? Math.max(...ringHs) : eMaxH) + half;
|
|
431371
|
+
let segMinZ = Infinity, segMaxZ = -Infinity;
|
|
431372
|
+
for (const { seg: [a, b] } of allSegs) for (const p of [a, b]) {
|
|
431373
|
+
if (p[2] < segMinZ) segMinZ = p[2];
|
|
431374
|
+
if (p[2] > segMaxZ) segMaxZ = p[2];
|
|
431375
|
+
}
|
|
431376
|
+
const eaveZ = isFinite(segMinZ) ? segMinZ : minZ;
|
|
431377
|
+
const ridgeZ = isFinite(segMaxZ) ? segMaxZ : maxZ;
|
|
431378
|
+
const wtZ = opts.wallTopZ ?? eaveZ;
|
|
431379
|
+
const EAVE = "#0e7490";
|
|
431380
|
+
const el = toSvg(hz(eMinH, eaveZ)), er = toSvg(hz(eMaxH, eaveZ));
|
|
431381
|
+
svg += `<line x1="${el[0].toFixed(1)}" y1="${el[1].toFixed(1)}" x2="${er[0].toFixed(1)}" y2="${er[1].toFixed(1)}" stroke="${EAVE}" stroke-width="1" stroke-dasharray="4 2"/>
|
|
431382
|
+
`;
|
|
431383
|
+
const drop = wtZ - eaveZ + Math.max((ridgeZ - eaveZ) * 0.18, 8);
|
|
431384
|
+
for (const h of [rMinH, rMaxH]) {
|
|
431385
|
+
const a = toSvg(hz(h, wtZ)), b = toSvg(hz(h, wtZ - drop));
|
|
431386
|
+
svg += `<line x1="${a[0].toFixed(1)}" y1="${a[1].toFixed(1)}" x2="${b[0].toFixed(1)}" y2="${b[1].toFixed(1)}" stroke="#8a6a3f" stroke-width="1.4"/>
|
|
431387
|
+
`;
|
|
431388
|
+
}
|
|
431389
|
+
const wallBottomY = toSvg(hz(rMinH, wtZ - drop))[1];
|
|
431390
|
+
const y1 = Math.max(el[1], wallBottomY) + 22;
|
|
431391
|
+
const y2 = y1 + 22;
|
|
431392
|
+
const sx = (h) => toSvg(hz(h, eaveZ))[0];
|
|
431393
|
+
if (rMinH - eMinH > 0.5) svg += dimLineH(sx(eMinH), sx(rMinH), y1, formatDimension(rMinH - eMinH));
|
|
431394
|
+
if (eMaxH - rMaxH > 0.5) svg += dimLineH(sx(rMaxH), sx(eMaxH), y1, formatDimension(eMaxH - rMaxH));
|
|
431395
|
+
svg += dimLineH(sx(rMinH), sx(rMaxH), y1, `wall ${formatDimension(rMaxH - rMinH)}`);
|
|
431396
|
+
svg += dimLineH(sx(eMinH), sx(eMaxH), y2, `eave ${formatDimension(eMaxH - eMinH)}`);
|
|
431397
|
+
if (ridgeZ - wtZ > 0.5) {
|
|
431398
|
+
const riseX = Math.min(el[0], er[0]) - 26;
|
|
431399
|
+
svg += dimLineV(toSvg(hz(eMinH, wtZ))[1], toSvg(hz(eMinH, ridgeZ))[1], riseX, `rise ${formatDimension(ridgeZ - wtZ)}`);
|
|
431400
|
+
}
|
|
431401
|
+
svg += `<text x="${(x0 + 12).toFixed(1)}" y="${(y0 + height - 12).toFixed(1)}" text-anchor="start" font-size="9" fill="${EAVE}">\u2014 \u2014 eave datum \xB7 brown = wall</text>
|
|
430596
431402
|
`;
|
|
430597
431403
|
}
|
|
430598
431404
|
svg += `</g>
|
|
@@ -430745,7 +431551,7 @@ function groupFaces(spec) {
|
|
|
430745
431551
|
(a, b) => b.geom.uMax - b.geom.uMin - (a.geom.uMax - a.geom.uMin)
|
|
430746
431552
|
);
|
|
430747
431553
|
}
|
|
430748
|
-
function v2FacePanel(x0, y0, width, height, group) {
|
|
431554
|
+
function v2FacePanel(x0, y0, width, height, group, keyLabel) {
|
|
430749
431555
|
const titleH = 40;
|
|
430750
431556
|
const innerPad = 30;
|
|
430751
431557
|
const drawW = width - 2 * innerPad;
|
|
@@ -430770,8 +431576,9 @@ function v2FacePanel(x0, y0, width, height, group) {
|
|
|
430770
431576
|
`;
|
|
430771
431577
|
svg += `<rect x="${x0}" y="${y0}" width="${width}" height="${titleH}" fill="#f2ede4" stroke="#333" stroke-width="1"/>
|
|
430772
431578
|
`;
|
|
430773
|
-
svg += `<text x="${x0 + width / 2}" y="${y0 + 26}" text-anchor="middle" font-size="14" font-weight="600" fill="#222">${
|
|
431579
|
+
svg += `<text x="${x0 + width / 2}" y="${y0 + 26}" text-anchor="middle" font-size="14" font-weight="600" fill="#222">${escapeXml3(title)}</text>
|
|
430774
431580
|
`;
|
|
431581
|
+
if (keyLabel) svg += keyPlanTag(x0 + width - 12, y0 + 9, keyLabel, "#eef2f7", "#334155");
|
|
430775
431582
|
const outlinePts = g.uv.map(toSvg).map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(" ");
|
|
430776
431583
|
svg += `<polygon points="${outlinePts}" fill="#f5e0c9" stroke="#5a2e0b" stroke-width="1.5" fill-opacity="0.5"/>
|
|
430777
431584
|
`;
|
|
@@ -430874,10 +431681,523 @@ function buildTitle(group) {
|
|
|
430874
431681
|
const ids = group.planes.map((p) => p.id).join(", ");
|
|
430875
431682
|
return `${first2.role.toUpperCase()}S (${shape}, ${n3} identical) \u2014 ${ids}`;
|
|
430876
431683
|
}
|
|
430877
|
-
function
|
|
431684
|
+
function escapeXml3(s) {
|
|
430878
431685
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
430879
431686
|
}
|
|
430880
431687
|
|
|
431688
|
+
// ../editor/src/svg2d/roof/v2/trussPanel.ts
|
|
431689
|
+
init_format();
|
|
431690
|
+
function escapeXml4(s) {
|
|
431691
|
+
return s.replace(
|
|
431692
|
+
/[<>&'"]/g,
|
|
431693
|
+
(c) => c === "<" ? "<" : c === ">" ? ">" : c === "&" ? "&" : c === "'" ? "'" : """
|
|
431694
|
+
);
|
|
431695
|
+
}
|
|
431696
|
+
var len3 = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
431697
|
+
function trussMembers2(t) {
|
|
431698
|
+
if (t.members && t.members.length) return t.members;
|
|
431699
|
+
return (t.kind === "mono_pitch" ? buildMonoPitchTrussMembers : buildFinkTrussMembers)(
|
|
431700
|
+
{ ...t, id: t.id, source_segment_id: t.source_segment_id }
|
|
431701
|
+
);
|
|
431702
|
+
}
|
|
431703
|
+
function trussGeom(t) {
|
|
431704
|
+
const bl = t.bottom_left, br = t.bottom_right;
|
|
431705
|
+
const span = Math.hypot(br[0] - bl[0], br[1] - bl[1]);
|
|
431706
|
+
const rise = t.apex[2] - bl[2];
|
|
431707
|
+
return { span, rise, kind: t.kind ?? "fink" };
|
|
431708
|
+
}
|
|
431709
|
+
function groupTrusses(spec) {
|
|
431710
|
+
const groups = /* @__PURE__ */ new Map();
|
|
431711
|
+
for (const t of spec.trusses) {
|
|
431712
|
+
const g = trussGeom(t);
|
|
431713
|
+
const r2 = (x) => Math.round(x * 10) / 10;
|
|
431714
|
+
const sig = `${g.kind}|${r2(g.span)}|${r2(g.rise)}`;
|
|
431715
|
+
const existing = groups.get(sig);
|
|
431716
|
+
if (existing) {
|
|
431717
|
+
existing.count += 1;
|
|
431718
|
+
} else {
|
|
431719
|
+
groups.set(sig, { signature: sig, rep: t, members: trussMembers2(t), geom: g, count: 1 });
|
|
431720
|
+
}
|
|
431721
|
+
}
|
|
431722
|
+
return [...groups.values()].sort((a, b) => b.geom.span - a.geom.span);
|
|
431723
|
+
}
|
|
431724
|
+
function v2TrussPanel(x0, y0, width, height, group) {
|
|
431725
|
+
const titleH = 40;
|
|
431726
|
+
const innerPad = 34;
|
|
431727
|
+
const drawW = width - 2 * innerPad;
|
|
431728
|
+
const drawH = height - titleH - 2 * innerPad - 20;
|
|
431729
|
+
const t = group.rep;
|
|
431730
|
+
const bl = t.bottom_left;
|
|
431731
|
+
const br = t.bottom_right;
|
|
431732
|
+
const { span, rise, kind } = group.geom;
|
|
431733
|
+
const dx = br[0] - bl[0], dy = br[1] - bl[1];
|
|
431734
|
+
const horiz = Math.hypot(dx, dy) || 1;
|
|
431735
|
+
const ux = dx / horiz, uy = dy / horiz;
|
|
431736
|
+
const baseZ = bl[2];
|
|
431737
|
+
const toUV = (p) => [
|
|
431738
|
+
(p[0] - bl[0]) * ux + (p[1] - bl[1]) * uy,
|
|
431739
|
+
p[2] - baseZ
|
|
431740
|
+
];
|
|
431741
|
+
const scale = Math.min(drawW / (span || 1), drawH / (rise || 1)) * 0.8;
|
|
431742
|
+
const cx = x0 + width / 2;
|
|
431743
|
+
const cy = y0 + titleH + innerPad + drawH / 2;
|
|
431744
|
+
const toSvg = (uv) => [
|
|
431745
|
+
cx + (uv[0] - span / 2) * scale,
|
|
431746
|
+
cy - (uv[1] - rise / 2) * scale
|
|
431747
|
+
];
|
|
431748
|
+
const pitchDeg = Math.atan2(rise, span / 2) * 180 / Math.PI;
|
|
431749
|
+
const nice = kind === "mono_pitch" ? "MONO-PITCH TRUSS" : "FINK TRUSS";
|
|
431750
|
+
const title = `${nice} \u2014 Elevation${group.count > 1 ? ` (\xD7 ${group.count} identical)` : ""}`;
|
|
431751
|
+
let svg = `<g class="v2-truss-panel">
|
|
431752
|
+
`;
|
|
431753
|
+
svg += `<rect x="${x0}" y="${y0}" width="${width}" height="${height}" fill="#fdfcfa" stroke="#333" stroke-width="1"/>
|
|
431754
|
+
`;
|
|
431755
|
+
svg += `<rect x="${x0}" y="${y0}" width="${width}" height="${titleH}" fill="#f2ede4" stroke="#333" stroke-width="1"/>
|
|
431756
|
+
`;
|
|
431757
|
+
svg += `<text x="${cx}" y="${y0 + 26}" text-anchor="middle" font-size="14" font-weight="600" fill="#222">${escapeXml4(title)}</text>
|
|
431758
|
+
`;
|
|
431759
|
+
const isChord = (m) => m.role === "truss_top_chord" || m.role === "truss_bottom_chord";
|
|
431760
|
+
for (const m of group.members) {
|
|
431761
|
+
const [x1, y1] = toSvg(toUV(m.start));
|
|
431762
|
+
const [x2, y2] = toSvg(toUV(m.end));
|
|
431763
|
+
const stroke = isChord(m) ? "#5a2e0b" : "#b45309";
|
|
431764
|
+
const sw = isChord(m) ? 2.2 : 1.2;
|
|
431765
|
+
svg += `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="${stroke}" stroke-width="${sw}" stroke-linecap="round"/>
|
|
431766
|
+
`;
|
|
431767
|
+
}
|
|
431768
|
+
const seen = /* @__PURE__ */ new Set();
|
|
431769
|
+
for (const m of group.members) {
|
|
431770
|
+
const L = len3(m.start, m.end);
|
|
431771
|
+
if (L < 1) continue;
|
|
431772
|
+
const a = toSvg(toUV(m.start));
|
|
431773
|
+
const b = toSvg(toUV(m.end));
|
|
431774
|
+
const mx = (a[0] + b[0]) / 2, my = (a[1] + b[1]) / 2;
|
|
431775
|
+
let deg = Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI;
|
|
431776
|
+
if (deg > 90) deg -= 180;
|
|
431777
|
+
else if (deg < -90) deg += 180;
|
|
431778
|
+
let nx = -(b[1] - a[1]), ny = b[0] - a[0];
|
|
431779
|
+
const nl = Math.hypot(nx, ny) || 1;
|
|
431780
|
+
nx /= nl;
|
|
431781
|
+
ny /= nl;
|
|
431782
|
+
if (ny > 0) {
|
|
431783
|
+
nx = -nx;
|
|
431784
|
+
ny = -ny;
|
|
431785
|
+
}
|
|
431786
|
+
const lx = mx + nx * 9, ly = my + ny * 9;
|
|
431787
|
+
const key = `${Math.round(L)}|${Math.round(Math.abs(deg))}`;
|
|
431788
|
+
if (seen.has(key) && isChord(m) === false) continue;
|
|
431789
|
+
seen.add(key);
|
|
431790
|
+
svg += `<text x="${lx.toFixed(1)}" y="${ly.toFixed(1)}" text-anchor="middle" dominant-baseline="middle" font-size="10" fill="#7c2d12" transform="rotate(${deg.toFixed(1)} ${lx.toFixed(1)} ${ly.toFixed(1)})">${formatDimension(L)}</text>
|
|
431791
|
+
`;
|
|
431792
|
+
}
|
|
431793
|
+
const blS = toSvg(toUV(bl));
|
|
431794
|
+
const brS = toSvg(toUV(br));
|
|
431795
|
+
const spanY = Math.max(blS[1], brS[1]) + 26;
|
|
431796
|
+
svg += dimLineH2(blS[0], brS[0], spanY, `span ${formatDimension(span)}`);
|
|
431797
|
+
svg += `<line x1="${blS[0].toFixed(1)}" y1="${blS[1].toFixed(1)}" x2="${blS[0].toFixed(1)}" y2="${spanY.toFixed(1)}" stroke="#94a3b8" stroke-width="0.5"/>
|
|
431798
|
+
`;
|
|
431799
|
+
svg += `<line x1="${brS[0].toFixed(1)}" y1="${brS[1].toFixed(1)}" x2="${brS[0].toFixed(1)}" y2="${spanY.toFixed(1)}" stroke="#94a3b8" stroke-width="0.5"/>
|
|
431800
|
+
`;
|
|
431801
|
+
const apexS = toSvg(toUV(t.apex));
|
|
431802
|
+
const baseAtApex = toSvg([span / 2, 0]);
|
|
431803
|
+
const riseX = apexS[0] + (kind === "mono_pitch" ? 22 : Math.min(brS[0] - apexS[0] + 22, 40));
|
|
431804
|
+
svg += dimLineV2(apexS[1], baseAtApex[1], riseX, `rise ${formatDimension(rise)}`);
|
|
431805
|
+
svg += `<line x1="${apexS[0].toFixed(1)}" y1="${apexS[1].toFixed(1)}" x2="${riseX.toFixed(1)}" y2="${apexS[1].toFixed(1)}" stroke="#94a3b8" stroke-width="0.5" stroke-dasharray="2 2"/>
|
|
431806
|
+
`;
|
|
431807
|
+
svg += `<line x1="${baseAtApex[0].toFixed(1)}" y1="${baseAtApex[1].toFixed(1)}" x2="${riseX.toFixed(1)}" y2="${baseAtApex[1].toFixed(1)}" stroke="#94a3b8" stroke-width="0.5" stroke-dasharray="2 2"/>
|
|
431808
|
+
`;
|
|
431809
|
+
svg += `<text x="${cx}" y="${(y0 + titleH + 18).toFixed(1)}" text-anchor="middle" font-size="13" font-weight="600" fill="#8B4513">TOP-CHORD PITCH: ${pitchDeg.toFixed(1)}\xB0</text>
|
|
431810
|
+
`;
|
|
431811
|
+
const chord = group.members.find(isChord)?.section_size ?? DEFAULT_TRUSS_SECTION.chord_size_in;
|
|
431812
|
+
const web = group.members.find((m) => m.role === "truss_web")?.section_size ?? DEFAULT_TRUSS_SECTION.web_size_in;
|
|
431813
|
+
const secStr = (s) => `${s[0]}"\xD7${s[1]}"`;
|
|
431814
|
+
const footY = y0 + height - 12;
|
|
431815
|
+
svg += `<text x="${(x0 + 12).toFixed(1)}" y="${footY.toFixed(1)}" text-anchor="start" font-size="10" fill="#475569">chord ${secStr(chord)} \xB7 web ${secStr(web)}${group.rep.members?.[0]?.material ? " \xB7 " + escapeXml4(group.rep.members[0].material) : ""}</text>
|
|
431816
|
+
`;
|
|
431817
|
+
svg += `<text x="${(x0 + width - 12).toFixed(1)}" y="${footY.toFixed(1)}" text-anchor="end" font-size="9" fill="#94a3b8">lengths = true member lengths (for fabrication)</text>
|
|
431818
|
+
`;
|
|
431819
|
+
svg += `</g>
|
|
431820
|
+
`;
|
|
431821
|
+
return svg;
|
|
431822
|
+
}
|
|
431823
|
+
function dimLineH2(x1, x2, y, label) {
|
|
431824
|
+
const lo = Math.min(x1, x2), hi = Math.max(x1, x2);
|
|
431825
|
+
return `<line x1="${lo.toFixed(1)}" y1="${y.toFixed(1)}" x2="${hi.toFixed(1)}" y2="${y.toFixed(1)}" stroke="#334155" stroke-width="0.8"/>
|
|
431826
|
+
<line x1="${lo.toFixed(1)}" y1="${(y - 3).toFixed(1)}" x2="${lo.toFixed(1)}" y2="${(y + 3).toFixed(1)}" stroke="#334155" stroke-width="0.8"/>
|
|
431827
|
+
<line x1="${hi.toFixed(1)}" y1="${(y - 3).toFixed(1)}" x2="${hi.toFixed(1)}" y2="${(y + 3).toFixed(1)}" stroke="#334155" stroke-width="0.8"/>
|
|
431828
|
+
<text x="${((lo + hi) / 2).toFixed(1)}" y="${(y - 4).toFixed(1)}" text-anchor="middle" font-size="10" fill="#334155">${escapeXml4(label)}</text>
|
|
431829
|
+
`;
|
|
431830
|
+
}
|
|
431831
|
+
function dimLineV2(y1, y2, x, label) {
|
|
431832
|
+
const lo = Math.min(y1, y2), hi = Math.max(y1, y2);
|
|
431833
|
+
return `<line x1="${x.toFixed(1)}" y1="${lo.toFixed(1)}" x2="${x.toFixed(1)}" y2="${hi.toFixed(1)}" stroke="#334155" stroke-width="0.8"/>
|
|
431834
|
+
<line x1="${(x - 3).toFixed(1)}" y1="${lo.toFixed(1)}" x2="${(x + 3).toFixed(1)}" y2="${lo.toFixed(1)}" stroke="#334155" stroke-width="0.8"/>
|
|
431835
|
+
<line x1="${(x - 3).toFixed(1)}" y1="${hi.toFixed(1)}" x2="${(x + 3).toFixed(1)}" y2="${hi.toFixed(1)}" stroke="#334155" stroke-width="0.8"/>
|
|
431836
|
+
<text x="${(x + 4).toFixed(1)}" y="${((lo + hi) / 2).toFixed(1)}" text-anchor="middle" dominant-baseline="middle" font-size="10" fill="#334155" transform="rotate(-90 ${(x + 4).toFixed(1)} ${((lo + hi) / 2).toFixed(1)})">${escapeXml4(label)}</text>
|
|
431837
|
+
`;
|
|
431838
|
+
}
|
|
431839
|
+
|
|
431840
|
+
// ../editor/src/svg2d/roof/v2/frameDimPanel.ts
|
|
431841
|
+
init_format();
|
|
431842
|
+
function membersBounds(members) {
|
|
431843
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
431844
|
+
for (const m of members) {
|
|
431845
|
+
for (const p of [m.start, m.end]) {
|
|
431846
|
+
if (p[0] < minX) minX = p[0];
|
|
431847
|
+
if (p[0] > maxX) maxX = p[0];
|
|
431848
|
+
if (p[1] < minY) minY = p[1];
|
|
431849
|
+
if (p[1] > maxY) maxY = p[1];
|
|
431850
|
+
}
|
|
431851
|
+
}
|
|
431852
|
+
if (!isFinite(minX)) return null;
|
|
431853
|
+
return { minX, maxX, minY, maxY };
|
|
431854
|
+
}
|
|
431855
|
+
function v2FrameDimPanel(x0, y0, width, height, spec) {
|
|
431856
|
+
const titleH = 40;
|
|
431857
|
+
const padL = 104, padR = 40, padT = titleH + 30, padB = 96;
|
|
431858
|
+
const drawW = width - padL - padR;
|
|
431859
|
+
const drawH = height - padT - padB;
|
|
431860
|
+
const ring = spec.members.filter((m) => m.role === "ring_beam");
|
|
431861
|
+
const ridges = spec.members.filter((m) => m.role === "ridge");
|
|
431862
|
+
const hips = spec.members.filter((m) => m.role === "hip");
|
|
431863
|
+
const ties = spec.members.filter((m) => m.role === "tie_beam");
|
|
431864
|
+
const eaves = spec.members.filter((m) => m.role === "pani_patti");
|
|
431865
|
+
const trusses = spec.trusses;
|
|
431866
|
+
const datum = membersBounds(ring.length ? ring : spec.members);
|
|
431867
|
+
if (!datum) return panelFrame(x0, y0, width, height, titleH, "Main frame \u2014 dimensions", "v2-frame-dim") + "</g>\n";
|
|
431868
|
+
const { minX, maxX, minY, maxY } = datum;
|
|
431869
|
+
const spanX = maxX - minX || 1;
|
|
431870
|
+
const spanY = maxY - minY || 1;
|
|
431871
|
+
const half = (spec.framing?.wall_thickness_u ?? 0) / 2;
|
|
431872
|
+
const wallSpanX = spanX + 2 * half, wallSpanY = spanY + 2 * half;
|
|
431873
|
+
const fit = membersBounds([...ring, ...hips, ...ridges, ...ties, ...eaves]) ?? datum;
|
|
431874
|
+
const fSpanX = fit.maxX - fit.minX || 1;
|
|
431875
|
+
const fSpanY = fit.maxY - fit.minY || 1;
|
|
431876
|
+
const scale = Math.min(drawW / fSpanX, drawH / fSpanY);
|
|
431877
|
+
const offX = x0 + padL + (drawW - fSpanX * scale) / 2 - fit.minX * scale;
|
|
431878
|
+
const offY = y0 + padT + (drawH - fSpanY * scale) / 2 - fit.minY * scale;
|
|
431879
|
+
const toSvg = (p) => [
|
|
431880
|
+
offX + p[0] * scale,
|
|
431881
|
+
offY + p[1] * scale
|
|
431882
|
+
];
|
|
431883
|
+
const title = "MAIN FRAME \u2014 Fabrication dimensions (wall outer face = floor plan \xB7 ring on wall centre)";
|
|
431884
|
+
let svg = panelFrame(x0, y0, width, height, titleH, title, "v2-frame-dim");
|
|
431885
|
+
const line2 = (a, b, stroke, w, dash) => `<line x1="${a[0].toFixed(1)}" y1="${a[1].toFixed(1)}" x2="${b[0].toFixed(1)}" y2="${b[1].toFixed(1)}" stroke="${stroke}" stroke-width="${w}"${dash ? ` stroke-dasharray="${dash}"` : ""} stroke-linecap="round"/>
|
|
431886
|
+
`;
|
|
431887
|
+
const c0 = toSvg([minX, minY]);
|
|
431888
|
+
const c1 = toSvg([maxX, maxY]);
|
|
431889
|
+
const f0 = toSvg([fit.minX, fit.minY]);
|
|
431890
|
+
const f1 = toSvg([fit.maxX, fit.maxY]);
|
|
431891
|
+
const ringBottom = Math.max(c0[1], c1[1]), ringLeft = Math.min(c0[0], c1[0]);
|
|
431892
|
+
const fullBottom = Math.max(f0[1], f1[1]), fullLeft = Math.min(f0[0], f1[0]);
|
|
431893
|
+
const rectX = Math.min(c0[0], c1[0]), rectY = Math.min(c0[1], c1[1]);
|
|
431894
|
+
const rectW = Math.abs(c1[0] - c0[0]), rectH = Math.abs(c1[1] - c0[1]);
|
|
431895
|
+
svg += `<rect x="${rectX.toFixed(1)}" y="${rectY.toFixed(1)}" width="${rectW.toFixed(1)}" height="${rectH.toFixed(1)}" fill="#ecfdf5" stroke="none"/>
|
|
431896
|
+
`;
|
|
431897
|
+
for (const m of ring) svg += line2(toSvg(m.start), toSvg(m.end), "#16a34a", 3);
|
|
431898
|
+
for (const m of eaves) svg += line2(toSvg(m.start), toSvg(m.end), "#94a3b8", 1.2);
|
|
431899
|
+
for (const m of hips) svg += line2(toSvg(m.start), toSvg(m.end), "#ea580c", 1.4, "5 3");
|
|
431900
|
+
for (const m of ridges) svg += line2(toSvg(m.start), toSvg(m.end), "#dc2626", 2, "6 3");
|
|
431901
|
+
for (const m of ties) svg += line2(toSvg(m.start), toSvg(m.end), "#0ea5e9", 1.2, "3 3");
|
|
431902
|
+
const centroid = (i2) => {
|
|
431903
|
+
const t = trusses[i2];
|
|
431904
|
+
return (t.bottom_left[0] + t.bottom_right[0] + t.apex[0]) / 3;
|
|
431905
|
+
};
|
|
431906
|
+
const centroidY = (i2) => {
|
|
431907
|
+
const t = trusses[i2];
|
|
431908
|
+
return (t.bottom_left[1] + t.bottom_right[1] + t.apex[1]) / 3;
|
|
431909
|
+
};
|
|
431910
|
+
let spreadX = 0, spreadY = 0;
|
|
431911
|
+
if (trusses.length > 1) {
|
|
431912
|
+
const xs = trusses.map((_, i2) => centroid(i2));
|
|
431913
|
+
const ys = trusses.map((_, i2) => centroidY(i2));
|
|
431914
|
+
spreadX = Math.max(...xs) - Math.min(...xs);
|
|
431915
|
+
spreadY = Math.max(...ys) - Math.min(...ys);
|
|
431916
|
+
}
|
|
431917
|
+
const distAxis = spreadX >= spreadY ? 0 : 1;
|
|
431918
|
+
const stations = trusses.map((t, i2) => ({ i: i2, pos: distAxis === 0 ? centroid(i2) : centroidY(i2), t })).sort((a, b) => a.pos - b.pos);
|
|
431919
|
+
stations.forEach((s, k) => {
|
|
431920
|
+
const a = toSvg(s.t.bottom_left);
|
|
431921
|
+
const b = toSvg(s.t.bottom_right);
|
|
431922
|
+
svg += line2(a, b, "#7c2d12", 2);
|
|
431923
|
+
const lx = (a[0] + b[0]) / 2, ly = (a[1] + b[1]) / 2;
|
|
431924
|
+
svg += `<circle cx="${lx.toFixed(1)}" cy="${ly.toFixed(1)}" r="8" fill="#fff7ed" stroke="#7c2d12" stroke-width="0.8"/>
|
|
431925
|
+
`;
|
|
431926
|
+
svg += `<text x="${lx.toFixed(1)}" y="${(ly + 3).toFixed(1)}" text-anchor="middle" font-size="9" font-weight="700" fill="#7c2d12">T${k + 1}</text>
|
|
431927
|
+
`;
|
|
431928
|
+
});
|
|
431929
|
+
const chainY = fullBottom + 20;
|
|
431930
|
+
const edgeLo = distAxis === 0 ? minX : minY;
|
|
431931
|
+
const edgeHi = distAxis === 0 ? maxX : maxY;
|
|
431932
|
+
const svgOf = (pos) => distAxis === 0 ? toSvg([pos, minY])[0] : toSvg([minX, pos])[1];
|
|
431933
|
+
if (distAxis === 0) {
|
|
431934
|
+
const marks = [edgeLo, ...stations.map((s) => s.pos), edgeHi];
|
|
431935
|
+
for (let j = 0; j + 1 < marks.length; j++) {
|
|
431936
|
+
const gap = Math.abs(marks[j + 1] - marks[j]);
|
|
431937
|
+
if (gap < 0.5) continue;
|
|
431938
|
+
svg += dimLineH(svgOf(marks[j]), svgOf(marks[j + 1]), chainY, formatDimension(gap));
|
|
431939
|
+
}
|
|
431940
|
+
for (const mk of marks) {
|
|
431941
|
+
const sx = svgOf(mk);
|
|
431942
|
+
svg += line2([sx, ringBottom], [sx, chainY], "#94a3b8", 0.5, "2 2");
|
|
431943
|
+
}
|
|
431944
|
+
svg += dimLineH(toSvg([minX - half, minY])[0], toSvg([maxX + half, minY])[0], chainY + 22, `WALL WIDTH ${formatDimension(wallSpanX)}`);
|
|
431945
|
+
svg += dimLineV(toSvg([minX, minY - half])[1], toSvg([minX, maxY + half])[1], fullLeft - 24, `WALL LENGTH ${formatDimension(wallSpanY)}`);
|
|
431946
|
+
} else {
|
|
431947
|
+
const chainX = fullLeft - 20;
|
|
431948
|
+
const marks = [edgeLo, ...stations.map((s) => s.pos), edgeHi];
|
|
431949
|
+
for (let j = 0; j + 1 < marks.length; j++) {
|
|
431950
|
+
const gap = Math.abs(marks[j + 1] - marks[j]);
|
|
431951
|
+
if (gap < 0.5) continue;
|
|
431952
|
+
svg += dimLineV(svgOf(marks[j]), svgOf(marks[j + 1]), chainX, formatDimension(gap));
|
|
431953
|
+
}
|
|
431954
|
+
for (const mk of marks) {
|
|
431955
|
+
const sy = svgOf(mk);
|
|
431956
|
+
svg += line2([ringLeft, sy], [chainX, sy], "#94a3b8", 0.5, "2 2");
|
|
431957
|
+
}
|
|
431958
|
+
svg += dimLineV(toSvg([minX, minY - half])[1], toSvg([minX, maxY + half])[1], chainX - 22, `WALL LENGTH ${formatDimension(wallSpanY)}`);
|
|
431959
|
+
svg += dimLineH(toSvg([minX - half, minY])[0], toSvg([maxX + half, minY])[0], fullBottom + 20, `WALL WIDTH ${formatDimension(wallSpanX)}`);
|
|
431960
|
+
}
|
|
431961
|
+
if (eaves.length) {
|
|
431962
|
+
const half2 = (spec.framing?.wall_thickness_u ?? 0) / 2;
|
|
431963
|
+
const wfMinX = minX - half2, wfMaxX = maxX + half2, wfMinY = minY - half2, wfMaxY = maxY + half2;
|
|
431964
|
+
const w0 = toSvg([wfMinX, wfMinY]), w1 = toSvg([wfMaxX, wfMaxY]);
|
|
431965
|
+
const wfL = Math.min(w0[0], w1[0]), wfR = Math.max(w0[0], w1[0]);
|
|
431966
|
+
const wfT = Math.min(w0[1], w1[1]), wfB = Math.max(w0[1], w1[1]);
|
|
431967
|
+
const midXsvg = (wfL + wfR) / 2, midYsvg = (wfT + wfB) / 2;
|
|
431968
|
+
if (half2 > 0.01) svg += `<rect x="${wfL.toFixed(1)}" y="${wfT.toFixed(1)}" width="${(wfR - wfL).toFixed(1)}" height="${(wfB - wfT).toFixed(1)}" fill="none" stroke="#8a6a3f" stroke-width="0.9"/>
|
|
431969
|
+
`;
|
|
431970
|
+
const eW = fit.maxX - fit.minX, eL = fit.maxY - fit.minY;
|
|
431971
|
+
svg += dimLineH(f0[0], f1[0], fullBottom + 42, `EAVE WIDTH ${formatDimension(eW)}`);
|
|
431972
|
+
svg += dimLineV(f0[1], f1[1], fullLeft - 66, `EAVE LENGTH ${formatDimension(eL)}`);
|
|
431973
|
+
const ohL = wfMinX - fit.minX, ohR = fit.maxX - wfMaxX, ohT = wfMinY - fit.minY, ohB = fit.maxY - wfMaxY;
|
|
431974
|
+
if (ohL > 0.5) svg += dimLineH(f0[0], wfL, midYsvg, formatDimension(ohL));
|
|
431975
|
+
if (ohR > 0.5) svg += dimLineH(wfR, f1[0], midYsvg, formatDimension(ohR));
|
|
431976
|
+
if (ohT > 0.5) svg += dimLineV(f0[1], wfT, midXsvg, formatDimension(ohT));
|
|
431977
|
+
if (ohB > 0.5) svg += dimLineV(wfB, f1[1], midXsvg, formatDimension(ohB));
|
|
431978
|
+
}
|
|
431979
|
+
if (stations.length) {
|
|
431980
|
+
const rep = stations[Math.floor(stations.length / 2)].t;
|
|
431981
|
+
const span = Math.hypot(rep.bottom_right[0] - rep.bottom_left[0], rep.bottom_right[1] - rep.bottom_left[1]);
|
|
431982
|
+
const a = toSvg(rep.bottom_left);
|
|
431983
|
+
const b = toSvg(rep.bottom_right);
|
|
431984
|
+
const midY = Math.min(a[1], b[1]) - 12;
|
|
431985
|
+
svg += dimLineH(a[0], b[0], midY, `truss span ${formatDimension(span)}`);
|
|
431986
|
+
}
|
|
431987
|
+
const inLabel = (s) => s ? `${Math.round(s[0] * 10) / 10}"\xD7${Math.round(s[1] * 10) / 10}"` : null;
|
|
431988
|
+
const ringSec = inLabel(spec.framing?.ring_beam_size_in) ?? sectionFtLabel(ring[0]?.section_size);
|
|
431989
|
+
const ridgeSec = inLabel(spec.framing?.ridge_size_in) ?? sectionFtLabel(ridges[0]?.section_size);
|
|
431990
|
+
const footY = y0 + height - 12;
|
|
431991
|
+
const parts = [];
|
|
431992
|
+
parts.push(`ring beam ${formatDimension(spanX)} \xD7 ${formatDimension(spanY)} (wall centre)`);
|
|
431993
|
+
if (ringSec) parts.push(`ring ${ringSec}`);
|
|
431994
|
+
if (ridgeSec) parts.push(`ridge ${ridgeSec}`);
|
|
431995
|
+
parts.push(`${stations.length} truss station${stations.length === 1 ? "" : "s"}`);
|
|
431996
|
+
svg += `<text x="${(x0 + 12).toFixed(1)}" y="${footY.toFixed(1)}" text-anchor="start" font-size="10" fill="#475569">${escapeXml2(parts.join(" \xB7 "))}</text>
|
|
431997
|
+
`;
|
|
431998
|
+
svg += `<text x="${(x0 + width - 12).toFixed(1)}" y="${footY.toFixed(1)}" text-anchor="end" font-size="9" fill="#475569"><tspan fill="#16a34a">green = ring beam (wall centre)</tspan> \xB7 <tspan fill="#8a6a3f">brown = wall face</tspan> \xB7 grey = roof edge (eave)</text>
|
|
431999
|
+
`;
|
|
432000
|
+
svg += `</g>
|
|
432001
|
+
`;
|
|
432002
|
+
return svg;
|
|
432003
|
+
}
|
|
432004
|
+
|
|
432005
|
+
// ../editor/src/svg2d/roof/v2/eavePanel.ts
|
|
432006
|
+
init_format();
|
|
432007
|
+
var IN_TO_U2 = 10 / 12;
|
|
432008
|
+
var sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
432009
|
+
var cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
432010
|
+
var norm = (a) => Math.hypot(a[0], a[1], a[2]);
|
|
432011
|
+
var secInLabel = (s) => `${Math.round(s[0] * 10) / 10}"\xD7${Math.round(s[1] * 10) / 10}"`;
|
|
432012
|
+
function planePitch(vs) {
|
|
432013
|
+
if (vs.length < 3) return 0;
|
|
432014
|
+
const n3 = cross(sub(vs[1], vs[0]), sub(vs[2], vs[0]));
|
|
432015
|
+
const nl = norm(n3) || 1;
|
|
432016
|
+
const nz = Math.abs(n3[2] / nl);
|
|
432017
|
+
return Math.acos(Math.min(1, nz)) * 180 / Math.PI;
|
|
432018
|
+
}
|
|
432019
|
+
function lowEdgeMid(vs) {
|
|
432020
|
+
const minZ = Math.min(...vs.map((v) => v[2]));
|
|
432021
|
+
const low = vs.filter((v) => Math.abs(v[2] - minZ) < 1);
|
|
432022
|
+
if (low.length < 2) return null;
|
|
432023
|
+
return [(low[0][0] + low[1][0]) / 2, (low[0][1] + low[1][1]) / 2, minZ];
|
|
432024
|
+
}
|
|
432025
|
+
function groupEaves(spec) {
|
|
432026
|
+
const eaves = spec.members.filter((m) => m.role === "pani_patti");
|
|
432027
|
+
if (!eaves.length) return [];
|
|
432028
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
432029
|
+
for (const m of spec.members) if (m.role === "ring_beam") for (const p of [m.start, m.end]) {
|
|
432030
|
+
if (p[0] < minX) minX = p[0];
|
|
432031
|
+
if (p[0] > maxX) maxX = p[0];
|
|
432032
|
+
if (p[1] < minY) minY = p[1];
|
|
432033
|
+
if (p[1] > maxY) maxY = p[1];
|
|
432034
|
+
}
|
|
432035
|
+
if (!isFinite(minX)) return [];
|
|
432036
|
+
const halfWall = (spec.framing?.wall_thickness_u ?? 0) / 2;
|
|
432037
|
+
minX -= halfWall;
|
|
432038
|
+
maxX += halfWall;
|
|
432039
|
+
minY -= halfWall;
|
|
432040
|
+
maxY += halfWall;
|
|
432041
|
+
const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2;
|
|
432042
|
+
const facePlanes = spec.planes.filter((p) => p.role === "slope" || p.role === "hip_face");
|
|
432043
|
+
const faceInfo = facePlanes.map((p) => ({ role: p.role, pitch: planePitch(p.vertices), mid: lowEdgeMid(p.vertices) })).filter((f2) => f2.mid != null);
|
|
432044
|
+
const groups = /* @__PURE__ */ new Map();
|
|
432045
|
+
for (const e of eaves) {
|
|
432046
|
+
const emid = [(e.start[0] + e.end[0]) / 2, (e.start[1] + e.end[1]) / 2, (e.start[2] + e.end[2]) / 2];
|
|
432047
|
+
const vertical = Math.abs(e.start[0] - e.end[0]) < 1;
|
|
432048
|
+
let overhang, side2;
|
|
432049
|
+
if (vertical) {
|
|
432050
|
+
const X = e.start[0];
|
|
432051
|
+
const wallX = X > cx ? maxX : minX;
|
|
432052
|
+
overhang = Math.abs(X - wallX);
|
|
432053
|
+
side2 = X > cx ? "E" : "W";
|
|
432054
|
+
} else {
|
|
432055
|
+
const Y = e.start[1];
|
|
432056
|
+
const wallY = Y > cy ? maxY : minY;
|
|
432057
|
+
overhang = Math.abs(Y - wallY);
|
|
432058
|
+
side2 = Y > cy ? "S" : "N";
|
|
432059
|
+
}
|
|
432060
|
+
let best = null;
|
|
432061
|
+
let bestD = Infinity;
|
|
432062
|
+
for (const f2 of faceInfo) {
|
|
432063
|
+
const d = Math.hypot(f2.mid[0] - emid[0], f2.mid[1] - emid[1]);
|
|
432064
|
+
if (d < bestD) {
|
|
432065
|
+
bestD = d;
|
|
432066
|
+
best = { role: f2.role, pitch: f2.pitch };
|
|
432067
|
+
}
|
|
432068
|
+
}
|
|
432069
|
+
const roleLabel = best?.role === "hip_face" ? "Hip end" : best?.role === "slope" ? "Main slope" : "Eave";
|
|
432070
|
+
const pitchDeg = best?.pitch ?? 0;
|
|
432071
|
+
const sig = `${roleLabel}|${Math.round(pitchDeg)}|${Math.round(overhang)}`;
|
|
432072
|
+
const existing = groups.get(sig);
|
|
432073
|
+
if (existing) {
|
|
432074
|
+
existing.count += 1;
|
|
432075
|
+
if (!existing.sides.includes(side2)) existing.sides.push(side2);
|
|
432076
|
+
} else groups.set(sig, { signature: sig, roleLabel, pitchDeg, overhang, count: 1, sides: [side2] });
|
|
432077
|
+
}
|
|
432078
|
+
return [...groups.values()].sort((a, b) => a.roleLabel === b.roleLabel ? b.overhang - a.overhang : a.roleLabel < b.roleLabel ? -1 : 1);
|
|
432079
|
+
}
|
|
432080
|
+
function v2EavePanel(x0, y0, width, height, spec, group) {
|
|
432081
|
+
const titleH = 40;
|
|
432082
|
+
const sidesStr = group.sides.length ? ` (${group.sides.join(", ")})` : "";
|
|
432083
|
+
const title = `EAVE \u2014 ${group.roleLabel}${group.count > 1 ? ` \xD7 ${group.count}` : ""}${sidesStr}`;
|
|
432084
|
+
let svg = panelFrame(x0, y0, width, height, titleH, title, "v2-eave");
|
|
432085
|
+
group.sides.forEach((side2, i2) => {
|
|
432086
|
+
svg += keyPlanDisc(x0 + width - 24 - i2 * 30, y0 + 20, 12, side2, "#0e7490", "#ffffff", "#ffffff");
|
|
432087
|
+
});
|
|
432088
|
+
const ringIn = spec.framing?.ring_beam_size_in ?? [4, 2];
|
|
432089
|
+
const rafterIn = spec.framing?.rafter_size_in ?? [2, 4];
|
|
432090
|
+
const purlinIn = spec.framing?.purlin_size_in ?? [2, 1];
|
|
432091
|
+
const ringW = ringIn[0] * IN_TO_U2;
|
|
432092
|
+
const ringD = ringIn[1] * IN_TO_U2;
|
|
432093
|
+
const rafterD = rafterIn[1] * IN_TO_U2;
|
|
432094
|
+
const purlinD = purlinIn[1] * IN_TO_U2;
|
|
432095
|
+
const wallT = spec.framing?.wall_thickness_u ?? 7.5;
|
|
432096
|
+
const ringH0 = Math.max(0, (wallT - ringW) / 2);
|
|
432097
|
+
const pitchDeg = group.pitchDeg || 25;
|
|
432098
|
+
const th = pitchDeg * Math.PI / 180;
|
|
432099
|
+
const cos = Math.cos(th), sin = Math.sin(th), tan = Math.tan(th);
|
|
432100
|
+
const overhang = group.overhang || 2 * 10;
|
|
432101
|
+
const wallH = Math.max(ringD * 4, 20);
|
|
432102
|
+
const interiorRun = wallT + Math.max(wallT * 1.2, 12);
|
|
432103
|
+
const battenGap = Math.max(rafterD * 0.35, 2);
|
|
432104
|
+
const bottomAt = (h) => [h, ringD + (h - ringH0) * tan];
|
|
432105
|
+
const perpUp = [-sin, cos];
|
|
432106
|
+
const top = (p) => [p[0] + perpUp[0] * rafterD, p[1] + perpUp[1] * rafterD];
|
|
432107
|
+
const tailB = bottomAt(-overhang);
|
|
432108
|
+
const inB = bottomAt(interiorRun);
|
|
432109
|
+
const tailT = top(tailB), inT = top(inB);
|
|
432110
|
+
const tileOff = [perpUp[0] * battenGap, perpUp[1] * battenGap];
|
|
432111
|
+
const tileTail = [tailT[0] + tileOff[0] - cos * 2, tailT[1] + tileOff[1] - sin * 2];
|
|
432112
|
+
const tileIn = [inT[0] + tileOff[0], inT[1] + tileOff[1]];
|
|
432113
|
+
const pAlong = wallT + Math.max(wallT * 0.5, 8);
|
|
432114
|
+
const pOnTop = top(bottomAt(pAlong));
|
|
432115
|
+
const pts = [
|
|
432116
|
+
[0, 0],
|
|
432117
|
+
[wallT, 0],
|
|
432118
|
+
[0, -wallH],
|
|
432119
|
+
[wallT, -wallH],
|
|
432120
|
+
[ringH0, 0],
|
|
432121
|
+
[ringH0 + ringW, 0],
|
|
432122
|
+
[ringH0, ringD],
|
|
432123
|
+
[ringH0 + ringW, ringD],
|
|
432124
|
+
tailB,
|
|
432125
|
+
inB,
|
|
432126
|
+
tailT,
|
|
432127
|
+
inT,
|
|
432128
|
+
tileTail,
|
|
432129
|
+
tileIn,
|
|
432130
|
+
pOnTop
|
|
432131
|
+
];
|
|
432132
|
+
let minH = Infinity, maxH = -Infinity, minZ = Infinity, maxZ = -Infinity;
|
|
432133
|
+
for (const [h, z2] of pts) {
|
|
432134
|
+
if (h < minH) minH = h;
|
|
432135
|
+
if (h > maxH) maxH = h;
|
|
432136
|
+
if (z2 < minZ) minZ = z2;
|
|
432137
|
+
if (z2 > maxZ) maxZ = z2;
|
|
432138
|
+
}
|
|
432139
|
+
const padL = 60, padR = 74, padT = titleH + 26, padB = 54;
|
|
432140
|
+
const drawW = width - padL - padR, drawH = height - padT - padB;
|
|
432141
|
+
const spanH = maxH - minH || 1, spanZ = maxZ - minZ || 1;
|
|
432142
|
+
const scale = Math.min(drawW / spanH, drawH / spanZ);
|
|
432143
|
+
const offX = x0 + padL + (drawW - spanH * scale) / 2 - minH * scale;
|
|
432144
|
+
const offY = y0 + padT + (drawH - spanZ * scale) / 2 + maxZ * scale;
|
|
432145
|
+
const S = (p) => [offX + p[0] * scale, offY - p[1] * scale];
|
|
432146
|
+
const poly = (ps, fill, stroke, w) => `<polygon points="${ps.map((p) => {
|
|
432147
|
+
const q = S(p);
|
|
432148
|
+
return `${q[0].toFixed(1)},${q[1].toFixed(1)}`;
|
|
432149
|
+
}).join(" ")}" fill="${fill}" stroke="${stroke}" stroke-width="${w}"/>
|
|
432150
|
+
`;
|
|
432151
|
+
const seg = (a, b, stroke, w, dash) => {
|
|
432152
|
+
const p = S(a), q = S(b);
|
|
432153
|
+
return `<line x1="${p[0].toFixed(1)}" y1="${p[1].toFixed(1)}" x2="${q[0].toFixed(1)}" y2="${q[1].toFixed(1)}" stroke="${stroke}" stroke-width="${w}"${dash ? ` stroke-dasharray="${dash}"` : ""} stroke-linecap="round"/>
|
|
432154
|
+
`;
|
|
432155
|
+
};
|
|
432156
|
+
const label = (p, text, anchor = "start", fill = "#334155") => {
|
|
432157
|
+
const q = S(p);
|
|
432158
|
+
return `<text x="${q[0].toFixed(1)}" y="${q[1].toFixed(1)}" text-anchor="${anchor}" font-size="10" fill="${fill}">${escapeXml2(text)}</text>
|
|
432159
|
+
`;
|
|
432160
|
+
};
|
|
432161
|
+
svg += poly([[0, 0], [wallT, 0], [wallT, -wallH], [0, -wallH]], "#efe7db", "#8a6a3f", 1);
|
|
432162
|
+
for (let hh = 1; hh < 6; hh++) {
|
|
432163
|
+
const zc = -wallH * (hh / 6);
|
|
432164
|
+
svg += seg([0, zc], [wallT, zc], "#d9c9ad", 0.5);
|
|
432165
|
+
}
|
|
432166
|
+
svg += poly([[ringH0, 0], [ringH0 + ringW, 0], [ringH0 + ringW, ringD], [ringH0, ringD]], "#dcfce7", "#16a34a", 1.6);
|
|
432167
|
+
svg += seg([Math.min(minH, -overhang), 0], [maxH, 0], "#94a3b8", 0.6, "5 3");
|
|
432168
|
+
svg += poly([tailB, inB, inT, tailT], "#e2e8f0", "#475569", 1.4);
|
|
432169
|
+
svg += seg(tailB, [tailB[0], tailB[1] - rafterD * 1.1], "#475569", 1.4);
|
|
432170
|
+
const pHalf = purlinD / 2;
|
|
432171
|
+
svg += poly([
|
|
432172
|
+
[pOnTop[0] - pHalf * cos, pOnTop[1] - pHalf * sin],
|
|
432173
|
+
[pOnTop[0] + pHalf * cos, pOnTop[1] + pHalf * sin],
|
|
432174
|
+
[pOnTop[0] + pHalf * cos - perpUp[0] * purlinD, pOnTop[1] + pHalf * sin - perpUp[1] * purlinD],
|
|
432175
|
+
[pOnTop[0] - pHalf * cos - perpUp[0] * purlinD, pOnTop[1] - pHalf * sin - perpUp[1] * purlinD]
|
|
432176
|
+
], "#64748b", "#334155", 0.8);
|
|
432177
|
+
svg += seg(tileTail, tileIn, "#b45309", 2);
|
|
432178
|
+
svg += label([wallT / 2, -wallH * 0.66], "WALL", "middle", "#8a6a3f");
|
|
432179
|
+
svg += label([ringH0 + ringW + 1.5, ringD / 2], `RING BEAM ${secInLabel(ringIn)}`, "start", "#16a34a");
|
|
432180
|
+
svg += label(inT, "RAFTER", "start", "#475569");
|
|
432181
|
+
svg += label([pOnTop[0], pOnTop[1] + 1], "PURLIN", "middle", "#334155");
|
|
432182
|
+
svg += label(tileIn, "TILE", "start", "#b45309");
|
|
432183
|
+
const dimY = S([0, minZ])[1] + 22;
|
|
432184
|
+
svg += dimLineH(S(tailB)[0], S([0, 0])[0], dimY, `overhang ${formatDimension(overhang)}`);
|
|
432185
|
+
svg += seg(tailB, [tailB[0], minZ - overhang * 0.02], "#94a3b8", 0.4, "2 2");
|
|
432186
|
+
svg += dimLineH(S([0, -wallH])[0], S([wallT, -wallH])[0], S([0, -wallH])[1] + 16, `wall ${formatDimension(wallT)}`);
|
|
432187
|
+
const o = S([ringH0, ringD]);
|
|
432188
|
+
const r2 = 26;
|
|
432189
|
+
svg += `<path d="M ${(o[0] + r2).toFixed(1)} ${o[1].toFixed(1)} A ${r2} ${r2} 0 0 0 ${(o[0] + r2 * cos).toFixed(1)} ${(o[1] - r2 * sin).toFixed(1)}" fill="none" stroke="#8B4513" stroke-width="1"/>
|
|
432190
|
+
`;
|
|
432191
|
+
svg += `<text x="${(o[0] + r2 + 4).toFixed(1)}" y="${(o[1] - 6).toFixed(1)}" text-anchor="start" font-size="11" font-weight="600" fill="#8B4513">${pitchDeg.toFixed(1)}\xB0</text>
|
|
432192
|
+
`;
|
|
432193
|
+
const footY = y0 + height - 12;
|
|
432194
|
+
svg += `<text x="${(x0 + 12).toFixed(1)}" y="${footY.toFixed(1)}" text-anchor="start" font-size="9" fill="#475569">rafter ${secInLabel(rafterIn)} \xB7 purlin ${secInLabel(purlinIn)} \xB7 sizes from the roof framing</text>
|
|
432195
|
+
`;
|
|
432196
|
+
svg += `</g>
|
|
432197
|
+
`;
|
|
432198
|
+
return svg;
|
|
432199
|
+
}
|
|
432200
|
+
|
|
430881
432201
|
// ../editor/src/svg2d/roof/v2/compose.ts
|
|
430882
432202
|
var TILE_W = 900;
|
|
430883
432203
|
var TILE_H = 600;
|
|
@@ -430894,10 +432214,32 @@ function computeV2RoofSections(cfg) {
|
|
|
430894
432214
|
const cutY = (bounds.minY + bounds.maxY) / 2;
|
|
430895
432215
|
const zMax = roofMaxZ(spec);
|
|
430896
432216
|
const wallTopZ = inferWallTopZ(spec);
|
|
432217
|
+
const eaveGroups = groupEaves(spec);
|
|
432218
|
+
const faceGroups = groupFaces(spec);
|
|
432219
|
+
const markers = {
|
|
432220
|
+
sections: [
|
|
432221
|
+
{ label: "A", axis: "x", coord: cutX },
|
|
432222
|
+
{ label: "B", axis: "y", coord: cutY }
|
|
432223
|
+
],
|
|
432224
|
+
// one eave callout per distinct eave, on each compass side it occurs
|
|
432225
|
+
eaves: eaveGroups.map((g) => ({ sides: g.sides, label: g.roleLabel })),
|
|
432226
|
+
// roof faces labelled F1.. (matches the "Face N" fabrication panels below)
|
|
432227
|
+
faces: faceGroups.map((group, idx) => ({
|
|
432228
|
+
label: `F${idx + 1}`,
|
|
432229
|
+
planeIds: group.planes.map((p) => p.id)
|
|
432230
|
+
}))
|
|
432231
|
+
};
|
|
430897
432232
|
const defs = [
|
|
432233
|
+
// Isometric first — the at-a-glance frame — then the key plan that indexes
|
|
432234
|
+
// every other view.
|
|
432235
|
+
{
|
|
432236
|
+
id: "perspective",
|
|
432237
|
+
title: "Frame (isometric)",
|
|
432238
|
+
render: (x0, y0, w, h) => v2PerspectivePanel(x0, y0, w, h, spec, { title: "Frame (isometric)", dimensions: true })
|
|
432239
|
+
},
|
|
430898
432240
|
{
|
|
430899
432241
|
id: "top_view",
|
|
430900
|
-
title: "Top
|
|
432242
|
+
title: "Top view \u2014 key plan",
|
|
430901
432243
|
// renderTopViewPanel returns a full <svg> — nest it inside our
|
|
430902
432244
|
// master via a positioned <svg> wrapper. Inner viewBox handles
|
|
430903
432245
|
// its own scaling; outer x/y positions it in the master grid.
|
|
@@ -430906,17 +432248,13 @@ function computeV2RoofSections(cfg) {
|
|
|
430906
432248
|
width: w,
|
|
430907
432249
|
height: h,
|
|
430908
432250
|
padding: 20,
|
|
430909
|
-
title: "Top
|
|
432251
|
+
title: "Top view \u2014 key plan",
|
|
432252
|
+
markers
|
|
430910
432253
|
});
|
|
430911
432254
|
const stripped = inner.replace(/^\s*<svg[^>]*>/, "").replace(/<\/svg>\s*$/, "");
|
|
430912
432255
|
return `<svg x="${x0}" y="${y0}" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${stripped}</svg>`;
|
|
430913
432256
|
}
|
|
430914
432257
|
},
|
|
430915
|
-
{
|
|
430916
|
-
id: "perspective",
|
|
430917
|
-
title: "Frame (isometric)",
|
|
430918
|
-
render: (x0, y0, w, h) => v2PerspectivePanel(x0, y0, w, h, spec, { title: "Frame (isometric)" })
|
|
430919
|
-
},
|
|
430920
432258
|
{
|
|
430921
432259
|
id: "section_a_a",
|
|
430922
432260
|
title: `Section A-A (cut at X=${cutX.toFixed(0)})`,
|
|
@@ -430924,7 +432262,10 @@ function computeV2RoofSections(cfg) {
|
|
|
430924
432262
|
title: `Section A-A (cut at X=${cutX.toFixed(0)})`,
|
|
430925
432263
|
cutAxis: "x",
|
|
430926
432264
|
cutCoord: cutX,
|
|
430927
|
-
wallTopZ
|
|
432265
|
+
wallTopZ,
|
|
432266
|
+
overlayTruss: true,
|
|
432267
|
+
dimensions: true,
|
|
432268
|
+
keyLabel: "A"
|
|
430928
432269
|
})
|
|
430929
432270
|
},
|
|
430930
432271
|
{
|
|
@@ -430934,19 +432275,41 @@ function computeV2RoofSections(cfg) {
|
|
|
430934
432275
|
title: `Section B-B (cut at Y=${cutY.toFixed(0)})`,
|
|
430935
432276
|
cutAxis: "y",
|
|
430936
432277
|
cutCoord: cutY,
|
|
430937
|
-
wallTopZ
|
|
432278
|
+
wallTopZ,
|
|
432279
|
+
overlayTruss: true,
|
|
432280
|
+
dimensions: true,
|
|
432281
|
+
keyLabel: "B"
|
|
430938
432282
|
})
|
|
432283
|
+
},
|
|
432284
|
+
{
|
|
432285
|
+
id: "frame_dims",
|
|
432286
|
+
title: "Main frame \u2014 fabrication dimensions",
|
|
432287
|
+
render: (x0, y0, w, h) => v2FrameDimPanel(x0, y0, w, h, spec)
|
|
430939
432288
|
}
|
|
430940
432289
|
];
|
|
430941
|
-
|
|
430942
|
-
|
|
432290
|
+
eaveGroups.forEach((g, idx) => {
|
|
432291
|
+
const sides = g.sides.length ? ` (${g.sides.join("")})` : "";
|
|
432292
|
+
defs.push({
|
|
432293
|
+
id: `eave_${idx}`,
|
|
432294
|
+
title: `Eave \u2014 ${g.roleLabel}${g.count > 1 ? ` \xD7 ${g.count}` : ""}${sides}`,
|
|
432295
|
+
render: (x0, y0, w, h) => v2EavePanel(x0, y0, w, h, spec, g)
|
|
432296
|
+
});
|
|
432297
|
+
});
|
|
432298
|
+
faceGroups.forEach((group, idx) => {
|
|
430943
432299
|
const id = `face_${idx}`;
|
|
430944
432300
|
const shape = group.geom.ridgeLen > 0.5 ? "trapezoid" : "triangle";
|
|
430945
432301
|
const nice = group.planes.length === 1 ? `Face ${idx + 1} (${shape}) \u2014 ${group.planes[0].id}` : `Face ${idx + 1} (${shape}, ${group.planes.length} identical)`;
|
|
430946
432302
|
defs.push({
|
|
430947
432303
|
id,
|
|
430948
432304
|
title: nice,
|
|
430949
|
-
render: (x0, y0, w, h) => v2FacePanel(x0, y0, w, h, group)
|
|
432305
|
+
render: (x0, y0, w, h) => v2FacePanel(x0, y0, w, h, group, `F${idx + 1}`)
|
|
432306
|
+
});
|
|
432307
|
+
});
|
|
432308
|
+
groupTrusses(spec).forEach((group, idx) => {
|
|
432309
|
+
defs.push({
|
|
432310
|
+
id: `truss_${idx}`,
|
|
432311
|
+
title: `${group.geom.kind === "mono_pitch" ? "Mono-pitch" : "Fink"} truss ${idx + 1} (elevation${group.count > 1 ? `, \xD7 ${group.count}` : ""})`,
|
|
432312
|
+
render: (x0, y0, w, h) => v2TrussPanel(x0, y0, w, h, group)
|
|
430950
432313
|
});
|
|
430951
432314
|
});
|
|
430952
432315
|
const cols = 2;
|
|
@@ -431141,7 +432504,7 @@ var DOCS = {
|
|
|
431141
432504
|
},
|
|
431142
432505
|
"data-model": {
|
|
431143
432506
|
"title": "The underlying .wadi schema (generated from Zod)",
|
|
431144
|
-
"body": '# Wadi data model (`.wadi` / `house_config.json`)\n\n> **Generated from `editor/src/schema/houseConfig.ts` \u2014 do not edit by hand.**\n> Regenerate: `node scripts/gen-schema-doc.mjs <path/to/houseConfig.ts> reference/data-model.md`\n> Some primitives (beam, floor_slab, pillar, plinth, ground) are generated from their\n> `fields` (schema/fields/\\*) into generated/objects.generated.ts \u2014 run `npm run gen-primitives`\n> in editor/ first if you changed those, so the generated schemas (which this doc reads) are current.\n> The Zod schema is the single source of truth; this file mirrors it (structure + the\n> semantics carried in its comments) so it can\'t drift.\n\nA `.wadi` file is one JSON object matching **HouseConfig**. Geometry is in **project\nunits** (a unitless grid; by default `units.per_unit = 10` means 10 units = 1 ft).\nPlan coordinates are **Inkscape-style**: origin top-left, **X \u2192 right, Y \u2192 down**.\nSee `coordinate-system.md` for the coordinate/units detail and `parametric-conventions.md`\nfor variables/points/formulas.\n\n## Fields shared by (almost) every object\n\nThese appear on most object types; documented once here, marked *(cross-cutting)* below.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `room` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | |\n| `layer` | string | | |\n| `name` | string | **yes** | |\n| `material` | string | | |\n| `z_offset` | number | | Vertical position of the room (its floor + walls), as a lift above the FLOOR BASE (slabZ = plinth top for floor 0, else the floor below\'s top; project units, 10 = 1 ft). This is the UNIFIED z_offset convention: every object is placed at `slabZ + z_offset`. When OMITTED, on-slab objects (room, wall, staircase, kitchen_platform) default z_offset to the floor\'s resolved slab thickness (floor.slab_thickness \u2192 house.defaults.slab_thickness \u2192 code default), so by default they sit on top of the slab, exactly as before. Set it explicitly for split-level floors \u2014 e.g. a room raised onto a thicker slab uses the same value the raised slab\'s top sits at. |\n\n\n- `type` \u2014 the discriminated-union tag; selects the object shape (values below).\n- `formulas` \u2014 per-field `"= expression"` overrides; the resolver evaluates each into the\n matching numeric field. See `parametric-conventions.md`.\n- `z_offset` \u2014 vertical lift above the floor base (slab top). On-slab objects (room, wall,\n staircase, kitchen_platform) default it to the floor\'s slab thickness; slab/beam/pillar/\n roof default to 0.\n\n## Top level \u2014 HouseConfig\n\n| field | type | req | notes |\n|---|---|---|---|\n| `coord_convention` | enum: `outer` `center` | | How a rectangular object\'s x/y/width/length relate to its walls (plans/grid-convention.md). "center" (new/canonical): coordinates are wall CENTRELINES \u2014 adjacent rooms ABUT on a shared line (no overlap), walls are centred on the boundary, and expandRoomWalls grows each footprint by wall_thickness/2 to the outer face. "outer" / absent (legacy): coordinates are the OUTER wall face and adjacent rooms must overlap by wall_thickness. |\n| `plinth` | any (freeform) | | Legacy top-level plinth (pre-"Plinth floor"). Tolerated but IGNORED so an un-migrated file still loads (it just renders without a plinth/ground) instead of failing .strict() validation. New configs put the plinth on the Plinth floor as a `plinth` object. |\n| `defaults` | [houseDefaults](#housedefaults) | | |\n| `units` | [units](#units) | | |\n| `layers` | array of [LayerDef](#layerdef) | | Configurable 3D visibility layers (optional; defaults applied when absent). Objects opt in via their own `layer` field. |\n| `variables` | map: string \u2192 number, or `"= formula"` string | | Parametric layer (plans/object-relationships-plan.md). Named scalar variables (number or "= formula", may reference other variables) and named 2D points; object `formulas` maps reference these. Optional \u2014 absent = a plain non-parametric house, resolved as a no-op. |\n| `points` | map: string \u2192 inline object | | |\n| `components` | map: string \u2192 [ComponentDef](#componentdef) | | Reusable-component library (in-file). Map of id \u2192 ComponentDef. A `component` object instantiates one by `ref`. Stored once; referenced by many instances; edit here to update every instance. |\n| `grids` | map: string \u2192 `gridDef` | | First-class parametric grids (plans/grid-convention.md). Map of id \u2192 GridDef (named X/Y wall centrelines). Rooms/slabs bind via `grid`+`cell`, pillars via `grid`+`node`; the resolver derives their geometry from the centrelines + wall thickness. Optional; reusable across templates. |\n| `configurator` | [configurator](#configurator) | | Configurator metadata (Gharkul owner UI). Optional; see plans/configurator-plan.md. |\n| `thumbnails` | array of string | | Preview snapshots (data: URLs) captured by the architect editor and saved WITH the template so the owner gallery can show real previews \u2014 multiple angles + the floor plan. `thumbnails[0]` is the gallery cover. Optional; excluded from share links (a preview isn\'t model data \u2014 see io/shareLink.ts). `thumbnail` (singular) is the legacy one-image form, still read as a fallback so old template files keep working. |\n| `thumbnail` | string | | |\n| `template` | inline object | | Catalog metadata that makes a `.wadi` SELF-DESCRIBING: the editorial fields a gallery card needs that can\'t be derived from geometry (title, blurb, style/roof tags, min plot). With this block + `thumbnails[]`, a folder of `.wadi` files IS the catalog \u2014 the app lists the folder and indexes each file, with no separate index.json to maintain (see io/templateSource.ts). Non-strict so newer editorial fields don\'t break an older build. |\n| `floors` | array of [floor](#floor) | **yes** | |\n| `_walls_expanded` | boolean | | |\n\n\n## floor\n\n| field | type | req | notes |\n|---|---|---|---|\n| `floor_number` | integer \u2265 0 | **yes** | |\n| `name` | string | **yes** | |\n| `height` | number > 0 | | Per-floor overrides for the default heights in GlobalConfig. In project units (10 units = 1 ft). All three are INDEPENDENT \u2014 no relationship enforced between them: height \u2014 floor-to-floor rise (drives roof wallTop-Z stack) wall_height \u2014 standing wall height (floor top \u2192 ceiling) slab_thickness \u2014 RCC deck between this floor and the one above All fall back to GlobalConfig defaults when omitted. |\n| `wall_height` | number > 0 | | |\n| `slab_thickness` | number \u2265 0 | | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | |\n| `objects` | array of [Object types](#object-types) | **yes** | |\n\n\n## Object types (`floors[].objects[]`)\n\nEvery entry in a floor\'s `objects` array is one of these, tagged by `type`:\n\n### `plinth`\n\nThe plinth is now a normal object placed on the "Plinth" floor (the first floor, number 0), not a top-level config key. Its footprint + height match the old top-level plinth; the plinth floor\'s `height` drives the rise to the floor above (replacing the old hardcoded plinth_height seed).\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `plinth` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | Label |\n| `material` | string | | Material key |\n| `x` | number | **yes** | Top-left X (project units) |\n| `y` | number | **yes** | Top-left Y (project units) |\n| `width` | number > 0 | **yes** | X extent (project units) |\n| `length` | number > 0 | **yes** | Y extent (project units) |\n| `height` | number > 0 | **yes** | Plinth height (project units) |\n| `z_offset` | number | | Lift above ground (project units) |\n\n\n### `ground`\n\nThe ground plane, also on the Plinth floor. Extent defaults to the site plot when authored by the migration. `height` is an optional thickness (0 = a flat plane); slope fields are a later phase.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `ground` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | Label |\n| `material` | string | | Material key |\n| `x` | number | **yes** | Top-left X (project units) |\n| `y` | number | **yes** | Top-left Y (project units) |\n| `width` | number > 0 | **yes** | X extent (project units) |\n| `length` | number > 0 | **yes** | Y extent (project units) |\n| `height` | number \u2265 0 | | Thickness (0 = flat) (project units) |\n| `z_offset` | number | | Lift above origin (project units) |\n\n\n### `component`\n\nAn INSTANCE of a reusable component from the in-file `components` library. It references a component by id (`ref`), overrides the component\'s input variables via `params`, and places it at (x, y) with a `z_offset` lift on its parent floor. At render time `expandRoomWalls` flattens it into concrete objects (resolve component with param+origin overrides \u2192 recurse \u2192 offset), so no renderer needs to know about `component`.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `component` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | |\n| `ref` | string | **yes** | |\n| `params` | map: string \u2192 union \u2014 see notes | | Overrides for the component\'s declared input variables. A string starting with "=" is a formula evaluated in the HOST scope (so it can reference the host\'s variables/points); a number is used directly. |\n| `x` | number | **yes** | |\n| `y` | number | **yes** | |\n| `rotation` | number | | Standard placement: yaw\xB0 about the instance origin (clockwise, same sense as item rotation: 0=south, 90=east). Right angles (0/90/180/270) are exact for any component; a non-right angle is allowed only for furniture-only ones. |\n| `z_offset` | number | | |\n\n\n### `item`\n\nA free-standing GLB furniture / decor instance placed directly on a floor (for pieces that aren\'t inside an enclosed room \u2014 outdoor/site/verandah decor, a loft item, etc.). `x`/`y` are the item\'s plan CENTRE. It MAY instead anchor to a named room via `anchor_to` + `anchor` + `gap`, in which case `x`/`y` are DERIVED at expand time (same anchor model as room-nested items). `rotation` is yaw\xB0; `scale` is a uniform resize; `z_offset` lifts it above the floor base (default = slab thickness). (`itemAsset`, `itemAnchor`, `gapField` are defined above `room`.)\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `item` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | |\n| `asset` | [ItemAsset](#itemasset) | **yes** | |\n| `x` | number | **yes** | |\n| `y` | number | **yes** | |\n| `rotation` | number | | |\n| `scale` | number > 0 | | |\n| `z_offset` | number | | |\n| `anchor_to` | string | | Optional room-relative anchoring (for a free item that should follow a room). |\n| `anchor` | [ItemAnchor](#itemanchor) | | |\n| `gap_x` | number | | |\n| `gap_y` | number | | |\n\n\n### `model`\n\nA GLB placed at real scale and manipulated by a `rig` of named-node ops. Distinct from `item` (furniture, catalog + anchoring): `model` is a rigged structural asset. `asset.dimensions` is the real metre size, used for the 2D footprint and the scale.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `model` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | |\n| `asset` | [ItemAsset](#itemasset) | **yes** | |\n| `x` | number | **yes** | |\n| `y` | number | **yes** | |\n| `rotation` | number | | |\n| `scale` | number > 0 | | |\n| `z_offset` | number | | |\n| `rig` | array of `rigOp` | | |\n\n\n### `floor_slab`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `floor_slab` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | Label |\n| `x` | number | **yes** | Top-left X (project units) |\n| `y` | number | **yes** | Top-left Y (project units) |\n| `width` | number > 0 | **yes** | X extent (project units) |\n| `length` | number > 0 | **yes** | Y extent (project units) |\n| `thickness` | number \u2265 0 | | Slab thickness (defaults to floor\'s) (project units) |\n| `z_offset` | number | | Lift above floor base (project units) |\n\n\n### `pillar`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `pillar` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | **yes** | Label |\n| `x` | number | **yes** | Top-left corner X (project units) |\n| `y` | number | **yes** | Top-left corner Y (project units) |\n| `width` | number > 0 | | X extent (project units) |\n| `length` | number > 0 | | Y extent (project units) |\n| `height` | number > 0 | **yes** | Column height (project units) |\n| `z_offset` | number | | Lift above floor base (project units) |\n\n\n### `beam`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `beam` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | Label |\n| `x` | number | **yes** | Top-left X (project units) |\n| `y` | number | **yes** | Top-left Y (project units) |\n| `width` | number > 0 | **yes** | X extent (project units) |\n| `length` | number > 0 | **yes** | Y extent (project units) |\n| `height` | number > 0 | | Vertical thickness (project units) |\n| `z_offset` | number | | Lift above floor base (project units) |\n\n\n### `room`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `room` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | **yes** | |\n| `x` | number | **yes** | |\n| `y` | number | **yes** | |\n| `width` | number > 0 | **yes** | |\n| `length` | number > 0 | **yes** | |\n| `height` | number \u2265 0 | | 0 accepted \u2014 semantically the same as absent ("use floor default"). Old configs that accidentally saved height: 0 keep loading; the form treats 0 as "no override" and doesn\'t write it back. |\n| `material` | string | | |\n| `z_offset` | number | | Vertical position of the room (its floor + walls), as a lift above the FLOOR BASE (slabZ = plinth top for floor 0, else the floor below\'s top; project units, 10 = 1 ft). This is the UNIFIED z_offset convention: every object is placed at `slabZ + z_offset`. When OMITTED, on-slab objects (room, wall, staircase, kitchen_platform) default z_offset to the floor\'s resolved slab thickness (floor.slab_thickness \u2192 house.defaults.slab_thickness \u2192 code default), so by default they sit on top of the slab, exactly as before. Set it explicitly for split-level floors \u2014 e.g. a room raised onto a thicker slab uses the same value the raised slab\'s top sits at. |\n| `walls` | union \u2014 see notes | | |\n| `wall_heights` | map: string \u2192 [wall_heights entry](#wall-heights-entry) | | |\n| `items` | array of [RoomItem](#roomitem) | | Furniture nested in this room. Each piece is anchored to the room\'s inner footprint (see roomItem), so it reflows when the room resizes. Expanded into top-level `item` objects at render time. |\n\n\n### `wall`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `wall` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | **yes** | |\n| `start_x` | number | **yes** | |\n| `start_y` | number | **yes** | |\n| `end_x` | number | **yes** | |\n| `end_y` | number | **yes** | |\n| `height` | number > 0 | | |\n| `height_end` | number | | |\n| `material` | string | | |\n| `facing` | enum: `north` `south` `east` `west` | | |\n| `z_offset` | number | | Lift above the FLOOR BASE (slabZ), project units. Omitted \u2192 defaults to the floor\'s resolved slab thickness (sits on the slab, as before). Set it for a split-level wall. Same convention as `room`. |\n| `openings` | array of [Opening](#opening) | | |\n\n\n### `staircase`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `staircase` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | |\n| `climb` | enum: `up` `down` | | `climb` picks which end (start_x, start_y) is and which way the flight runs in z as it extends into `direction`: \u2022 "up" (recommended): BOTTOM-anchored. Put the stair on the LOWER floor it rises FROM; (start_x, start_y) is the bottom step\'s near corner on that floor and the flight ASCENDS into `direction`. `rise_height` defaults to THIS floor\'s height (climb to the next level). The intuitive way. \u2022 "down" (DEFAULT, kept for older configs): TOP-anchored. Put the stair on the upper DESTINATION floor; (start_x, start_y) is the top connection and the flight DESCENDS into `direction`. `rise_height` defaults to the floor immediately BELOW this one. Either way the body + landings fill the box [start, start + max_run] along `direction`, and `z_offset` is the ANCHORED end\'s height above the floor base (omitted \u2192 this floor\'s slab thickness, flush with the walking surface). |\n| `start_x` | number | **yes** | |\n| `start_y` | number | **yes** | |\n| `rise_height` | number > 0 | | Total height the stair covers, top \u2192 floor below. The step COUNT is derived: num_steps = round(rise_height / step_rise). Omitted \u2192 defaults to the height of the floor immediately below this one. Formula-capable (e.g. "= floor_height"). Replaces the old explicit `num_steps`. |\n| `step_rise` | number > 0 | **yes** | |\n| `step_tread` | number > 0 | **yes** | |\n| `step_width` | number > 0 | **yes** | |\n| `direction` | enum: `north` `south` `east` `west` | **yes** | The direction the stair EXTENDS from its top \u2014 the whole assembly fills the allocated box from (start_x,start_y) going this way for up to `max_run`. |\n| `max_run` | number > 0 | | ALLOCATED run: the length of space reserved for the stair along `direction`. The WHOLE assembly (flights + turn landings) is kept within [start, start+max_run]; when the run won\'t fit as one flight it auto-splits into switchback flights (more flights when tight), expanded in expandRoomWalls into plain staircases + floor_slab landings so every renderer is unchanged. Omit \u2192 one flight, no length limit. |\n| `landing_depth` | number > 0 | | Turn-landing depth (along the run). Omitted \u2192 equals step_width. |\n| `landing_thickness` | number \u2265 0 | | Turn-landing slab thickness. Omitted \u2192 equals step_rise. |\n| `turn` | enum: `clockwise` `anticlockwise` | | Switchback handedness, reckoned DESCENDING from the top. Omitted \u2192 "clockwise". Only affects split stairs. |\n| `flight_gap` | number > 0 | | Lateral gap between the two switchback flights (a stairwell void for a spine wall). Omitted/0 \u2192 flights are adjacent. The turn landings widen to bridge the gap. Only affects split stairs. |\n| `z_offset` | number | | Height of the stair\'s TOP above the floor base (slabZ; project units, 10 = 1 ft). Omitted \u2192 this floor\'s slab thickness, so the top is flush with the walking surface and the flights descend to the floor below. Raise it for an internal step whose top sits above the floor. |\n| `material` | string | | |\n\n\n### `spiral_staircase`\n\nA helical staircase: `steps` treads winding `turns` revolutions around a central pole, from the floor to `total_height`, within `radius`. Placed by its CENTRE (x, y). Optional fields fall back to sensible defaults at render time.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `spiral_staircase` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | Label |\n| `x` | number | **yes** | Centre X (project units) |\n| `y` | number | **yes** | Centre Y (project units) |\n| `radius` | number > 0 | **yes** | Outer radius (project units) |\n| `total_height` | number > 0 | **yes** | Total rise (floor to top step) (project units) |\n| `turns` | number > 0 | | Revolutions (default 1) |\n| `steps` | integer | | Number of treads (default ~12 per turn) |\n| `tread_thickness` | number > 0 | | Tread slab thickness (project units) |\n| `pole_radius` | number > 0 | | Central pole radius (project units) |\n| `z_offset` | number | | Lift above floor base (project units) |\n\n\n### `door`\n\nFlat door/window remain valid as a legacy schema \u2014 new configs nest them inside room.walls[side].openings or wall.openings.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `door` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | **yes** | |\n| `x` | number | **yes** | |\n| `y` | number | **yes** | |\n| `width` | number > 0 | **yes** | |\n| `height` | number > 0 | **yes** | |\n| `direction` | enum: `north` `south` `east` `west` | **yes** | |\n| `room` | string | | |\n| `wall` | string | | |\n| `open` | boolean | | |\n\n\n### `window`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `window` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | **yes** | |\n| `x` | number | **yes** | |\n| `y` | number | **yes** | |\n| `width` | number > 0 | **yes** | |\n| `height` | number > 0 | **yes** | |\n| `sill_height` | number \u2265 0 | | |\n| `direction` | enum: `north` `south` `east` `west` | **yes** | |\n| `room` | string | | |\n| `wall` | string | | |\n| `open` | boolean | | |\n\n\n### `kitchen_platform`\n\nKitchen platform \u2014 a polyline countertop / cooking slab that runs along the base of walls. Path is the wall-side edge; the platform extends `depth` units perpendicular to each segment on the given `side`. Renders as one box per path segment; corners meet at the shared point (no fancy mitering in v1).\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `kitchen_platform` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | |\n| `path` | array of tuple `[n,n]` | **yes** | |\n| `side` | enum: `left` `right` | **yes** | |\n| `depth` | number > 0 | **yes** | |\n| `height` | number > 0 | **yes** | |\n| `z_offset` | number | | Lift above the FLOOR BASE (slabZ), project units. Omitted \u2192 defaults to the floor\'s resolved slab thickness (sits on the slab top, as before). Same convention as `room`. |\n| `base_z` | number | | |\n| `material` | string | | |\n\n\n### `roof`\n\nv2 roof \u2014 unified segment-based type that replaces hip/gable/flat/shed. Schema is permissive; the v2 pipeline (svg2d/roof/v2/) validates segments + slope + endpoint style at derivation time.\n\n\n> **Freeform:** extra fields are allowed (`.catchall`) and validated at derivation time. See `roof-v2-guide.md`.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `roof` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n\n\n## Shared & nested schemas\n\n### site\n\nObjects don\'t bind to the grid with a special field \u2014 a grid line\'s position is published as a formula symbol (`<gridId>.x<name>` / `.y<name>`, see param/resolve.ts), so a room places itself with ordinary `formulas`, e.g. { x: "= main.x1", width: "= main.x5 - main.x1" }. With coord_convention:"center" those are wall centrelines and expandRoomWalls handles the wall extent.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `reference_x` | number | **yes** | |\n| `reference_y` | number | **yes** | |\n| `plot_length` | number > 0 | **yes** | |\n| `plot_width` | number > 0 | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | |\n\n\n### houseDefaults\n\nHouse-level overrides for the built-in GlobalConfig defaults. Every floor without its own value falls back to these; if these are absent too, the code defaults in DEFAULT_GLOBAL_CONFIG apply.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `floor_height` | number > 0 | | |\n| `wall_height` | number > 0 | | |\n| `slab_thickness` | number \u2265 0 | | |\n| `wall_thickness` | number > 0 | | House-wide wall thickness (project units). Per-object `wall_thickness`/`thickness` overrides still win. Falls back to the code default (DEFAULT_GLOBAL_CONFIG.wall_thickness = 8) when omitted. |\n| `formulas` | map: field name \u2192 `"= formula"` string | | |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | |\n\n\n### units\n\nHow dimensions are LABELLED on the drawings. Display-only \u2014 geometry always stays in project units; this just controls the text on the dimension lines. Omitted = the built-in default (feet & inches, 10 project units = 1 ft).\n\n| field | type | req | notes |\n|---|---|---|---|\n| `system` | enum: `feet_inches` `feet` `meters` `centimeters` `millimeters` | | feet_inches \u2192 12\' 6" ; the rest \u2192 decimal with a unit suffix. |\n| `per_unit` | number > 0 | | Project units that equal ONE display unit (10 \u2192 10 units = 1 ft; 100 \u2192 100 units = 1 m). Default 10. |\n| `precision` | integer \u2265 0 | | Decimal places for the non-feet_inches systems. |\n\n\n### Opening\n\nThe plinth is now a normal object placed on the "Plinth" floor (the first floor, number 0), not a top-level config key. Its footprint + height match the old top-level plinth; the plinth floor\'s `height` drives the rise to the floor above (replacing the old hardcoded plinth_height seed). `plinth` + `ground` are GENERATED from fields (schema/fields/{plinth,ground}.ts), imported above as plinthObject / groundObject. (P2b)\n\n| field | type | req | notes |\n|---|---|---|---|\n| `kind` | enum: `door` `window` | **yes** | |\n| `name` | string | | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | Numeric fields hold the RESOLVED value; a `= formula` for any of them lives in `formulas` (e.g. formulas.offset), evaluated by resolveParametric against the house variables/points \u2014 same pattern as every other object. |\n| `offset` | number | **yes** | Any number: `center` anchor takes a signed shift; the resolved placement is fit-checked against the wall in expand.ts, so a bad value errors there. |\n| `anchor` | enum: `start` `center` `end` | | Which end of the wall `offset` is measured from, so the opening holds its place when the wall/room scales \u2014 no formula needed. `start` (default, legacy): offset from the wall start to the near edge. `end`: offset from the wall end to the far edge (0 = flush to the end). `center`: signed shift of the opening centre from the wall midpoint (0 = centred; may be negative). Resolved to a start-based offset in expand.ts (openingAnchor.ts). |\n| `width` | number > 0 | **yes** | |\n| `height` | number > 0 | **yes** | |\n| `sill_height` | number | | |\n| `direction` | enum: `north` `south` `east` `west` | | |\n| `facing` | enum: `north` `south` `east` `west` | | |\n| `open` | boolean | | When true, the opening is left BARE (just a hole) \u2014 no glazing/frame for a window, no leaf for a door \u2014 e.g. an open doorway or unglazed vent. |\n\n\n### RoomWallSide\n| field | type | req | notes |\n|---|---|---|---|\n| `height` | number | | |\n| `height_end` | number | | |\n| `openings` | array of [Opening](#opening) | | |\n\n\n### RoomItem\n\nA furniture piece nested INSIDE a room (room.items[]). It has NO x/y \u2014 its plan position is DERIVED at expand time from the parent room\'s footprint + `anchor` + per-axis gap (+ its own `rotation`). Flattened into a top-level `item` for every renderer. `gap_x`/`gap_y` are the inset (project units) kept from the anchor into the room (edge/corner anchor \u2192 clears the wall; centre anchor \u2192 signed offset, +x east / +y south). `gap_x`/`gap_y`/`rotation`/`scale`/`z_offset` are all plain numeric fields so each can be driven by a `= formula` (via the `formulas` map).\n\n| field | type | req | notes |\n|---|---|---|---|\n| `name` | string | | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | |\n| `layer` | string | | |\n| `asset` | [ItemAsset](#itemasset) | **yes** | |\n| `anchor` | [ItemAnchor](#itemanchor) | | |\n| `gap_x` | number | | |\n| `gap_y` | number | | |\n| `rotation` | number | | |\n| `scale` | number > 0 | | |\n| `z_offset` | number | | |\n\n\n### ItemAsset\n\nFurniture (GLB `item`) \u2014 shared schema pieces Defined BEFORE `room` so a room can nest its own `items[]`. Asset distances are METRES (the GLB\'s native unit); the 3D/2D layers scale them into project units. See registry/nodes/item + three/units. The asset backing a furniture item \u2014 stored INLINE so a .wadi is self-contained (share links / web load the GLB from `src`). A catalog is just a picker convenience.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `id` | string | **yes** | |\n| `name` | string | | |\n| `src` | string | **yes** | |\n| `dimensions` | tuple `[n>0,n>0,n>0]` | **yes** | |\n| `thumbnail` | string | | |\n| `floorPlanUrl` | string | | |\n| `category` | string | | |\n| `tags` | array of string | | |\n| `offset` | tuple `[n,n,n]` | | |\n| `corrRotation` | tuple `[n,n,n]` | | |\n| `corrScale` | tuple `[n>0,n>0,n>0]` | | |\n\n\n### ComponentDef\n| field | type | req | notes |\n|---|---|---|---|\n| `name` | string | | |\n| `goal` | string | | A short natural-language description of what this component accomplishes (the discovery key for goal-based module lookup, e.g. "climb to the next floor"). Purely metadata \u2014 renderers ignore it. |\n| `params` | array of [ComponentParam](#componentparam) | | |\n| `variables` | map: string \u2192 number, or `"= formula"` string | | |\n| `points` | map: string \u2192 inline object | | |\n| `objects` | array of [Object types](#object-types) | **yes** | |\n| `expose` | inline object | | Promote this component to a typed primitive at load time (plans/declarative-plugins.md P0). When present, the component registers a NodeDefinition of type `expose.type` whose fields come from `params`; it can then be used like any core object type. `type` is namespaced (`pack.thing`). |\n\n\n### ComponentParam\n\nA reusable component DEFINITION in the in-file `components` library. It is a mini-house: its own `variables`/`points` and a flat `objects` body authored in LOCAL coords (origin 0,0). `params` names which variables are the public inputs (label/default for the instance form). A `component` instance overrides those variables and places the body at its (x,y,z_offset). Stored once; referenced by many instances.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `name` | string | **yes** | |\n| `label` | string | | |\n| `description` | string | | |\n| `default` | number | | |\n| `kind` | string | | Field-projection annotations, used only when the component is `expose`d as a typed primitive (plans/declarative-plugins.md). `kind` is a FieldKind preset (coord/extent/nonneg/int/text/flag/enum); when absent the kind is inferred from the default\'s type. `unit` is a doc-only unit hint. |\n| `unit` | string | | |\n\n\n### LayerDef\n\nA visibility layer for the 3D view. Each object may reference a layer by `id` (via its `layer` field); the layers menu toggles whole layers on/off. Display-only \u2014 never affects geometry. Optional: when absent, a built-in default layer set is used, and objects fall back to an automatic per-type/floor mapping.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `id` | string (non-empty) | **yes** | |\n| `label` | string | **yes** | |\n| `color` | string | | |\n| `group` | string | | Friendly group for the owner "Show/hide layers" menu (e.g. "Roof", "Walls"). Layers sharing a group toggle together. Optional. |\n\n\n### configurator\n| field | type | req | notes |\n|---|---|---|---|\n| `title` | string | | |\n| `description` | string | | |\n| `groups` | array of inline object | | |\n| `inputs` | array of [ConfiguratorInput](#configuratorinput) | **yes** | |\n\n\n### ConfiguratorInput\n\nConfigurator (Gharkul owner UI) Optional, author-supplied metadata: which `variables`/`points` a template exposes to end users, and how to present them. IGNORED by the resolver and every geometry consumer \u2014 read only by the owner-facing Configurator UI. `target` is a variable name (e.g. "floorH") or a point coordinate ("House.W" \u2192 points.House.x; W/L/X/Y/x/y are resolver synonyms). `min`/`max`/ `step` are in RAW project units; `unit` only affects display.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `target` | string (non-empty) | **yes** | |\n| `label` | string (non-empty) | **yes** | |\n| `description` | string | | |\n| `control` | enum: `slider` `number` `select` `toggle` | | |\n| `unit` | enum: `ft` `in` `m` `units` `percent` `count` `none` | | |\n| `min` | number | | |\n| `max` | number | | |\n| `step` | number > 0 | | |\n| `options` | array of inline object | | |\n| `group` | string | | |\n\n\n### ItemAnchor\n\n9-point anchor on a room\'s INNER footprint. First token = vertical (top = north \u2026 bottom = south), second = horizontal (left = west \u2026 right = east); "center" alone = both. The item aligns its matching edge/corner to this spot, held `gap` off it, into the room \u2014 so it reflows when the room is resized.\n\nEnum: `top-left` `top-center` `top-right` `center-left` `center` `center-right` `bottom-left` `bottom-center` `bottom-right`\n'
|
|
432507
|
+
"body": '# Wadi data model (`.wadi` / `house_config.json`)\n\n> **Generated from `editor/src/schema/houseConfig.ts` \u2014 do not edit by hand.**\n> Regenerate: `node scripts/gen-schema-doc.mjs <path/to/houseConfig.ts> reference/data-model.md`\n> Some primitives (beam, floor_slab, pillar, plinth, ground) are generated from their\n> `fields` (schema/fields/\\*) into generated/objects.generated.ts \u2014 run `npm run gen-primitives`\n> in editor/ first if you changed those, so the generated schemas (which this doc reads) are current.\n> The Zod schema is the single source of truth; this file mirrors it (structure + the\n> semantics carried in its comments) so it can\'t drift.\n\nA `.wadi` file is one JSON object matching **HouseConfig**. Geometry is in **project\nunits** (a unitless grid; by default `units.per_unit = 10` means 10 units = 1 ft).\nPlan coordinates are **Inkscape-style**: origin top-left, **X \u2192 right, Y \u2192 down**.\nSee `coordinate-system.md` for the coordinate/units detail and `parametric-conventions.md`\nfor variables/points/formulas.\n\n## Fields shared by (almost) every object\n\nThese appear on most object types; documented once here, marked *(cross-cutting)* below.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `room` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | |\n| `layer` | string | | |\n| `name` | string | **yes** | |\n| `material` | string | | |\n| `z_offset` | number | | Vertical position of the room (its floor + walls), as a lift above the FLOOR BASE (slabZ = plinth top for floor 0, else the floor below\'s top; project units, 10 = 1 ft). This is the UNIFIED z_offset convention: every object is placed at `slabZ + z_offset`. When OMITTED, on-slab objects (room, wall, staircase, kitchen_platform) default z_offset to the floor\'s resolved slab thickness (floor.slab_thickness \u2192 house.defaults.slab_thickness \u2192 code default), so by default they sit on top of the slab, exactly as before. Set it explicitly for split-level floors \u2014 e.g. a room raised onto a thicker slab uses the same value the raised slab\'s top sits at. |\n\n\n- `type` \u2014 the discriminated-union tag; selects the object shape (values below).\n- `formulas` \u2014 per-field `"= expression"` overrides; the resolver evaluates each into the\n matching numeric field. See `parametric-conventions.md`.\n- `z_offset` \u2014 vertical lift above the floor base (slab top). On-slab objects (room, wall,\n staircase, kitchen_platform) default it to the floor\'s slab thickness; slab/beam/pillar/\n roof default to 0.\n\n## Top level \u2014 HouseConfig\n\n| field | type | req | notes |\n|---|---|---|---|\n| `coord_convention` | enum: `outer` `center` | | How a rectangular object\'s x/y/width/length relate to its walls (plans/grid-convention.md). "center" (new/canonical): coordinates are wall CENTRELINES \u2014 adjacent rooms ABUT on a shared line (no overlap), walls are centred on the boundary, and expandRoomWalls grows each footprint by wall_thickness/2 to the outer face. "outer" / absent (legacy): coordinates are the OUTER wall face and adjacent rooms must overlap by wall_thickness. |\n| `plinth` | any (freeform) | | Legacy top-level plinth (pre-"Plinth floor"). Tolerated but IGNORED so an un-migrated file still loads (it just renders without a plinth/ground) instead of failing .strict() validation. New configs put the plinth on the Plinth floor as a `plinth` object. |\n| `defaults` | [houseDefaults](#housedefaults) | | |\n| `units` | [units](#units) | | |\n| `layers` | array of [LayerDef](#layerdef) | | Configurable 3D visibility layers (optional; defaults applied when absent). Objects opt in via their own `layer` field. |\n| `variables` | map: string \u2192 number, or `"= formula"` string | | Parametric layer (plans/object-relationships-plan.md). Named scalar variables (number or "= formula", may reference other variables) and named 2D points; object `formulas` maps reference these. Optional \u2014 absent = a plain non-parametric house, resolved as a no-op. |\n| `points` | map: string \u2192 inline object | | |\n| `components` | map: string \u2192 [ComponentDef](#componentdef) | | Reusable-component library (in-file). Map of id \u2192 ComponentDef. A `component` object instantiates one by `ref`. Stored once; referenced by many instances; edit here to update every instance. |\n| `grids` | map: string \u2192 `gridDef` | | First-class parametric grids (plans/grid-convention.md). Map of id \u2192 GridDef (named X/Y wall centrelines). Rooms/slabs bind via `grid`+`cell`, pillars via `grid`+`node`; the resolver derives their geometry from the centrelines + wall thickness. Optional; reusable across templates. |\n| `configurator` | [configurator](#configurator) | | Configurator metadata (Gharkul owner UI). Optional; see plans/configurator-plan.md. |\n| `thumbnails` | array of string | | Preview snapshots (data: URLs) captured by the architect editor and saved WITH the template so the owner gallery can show real previews \u2014 multiple angles + the floor plan. `thumbnails[0]` is the gallery cover. Optional; excluded from share links (a preview isn\'t model data \u2014 see io/shareLink.ts). `thumbnail` (singular) is the legacy one-image form, still read as a fallback so old template files keep working. |\n| `thumbnail` | string | | |\n| `template` | inline object | | Catalog metadata that makes a `.wadi` SELF-DESCRIBING: the editorial fields a gallery card needs that can\'t be derived from geometry (title, blurb, style/roof tags, min plot). With this block + `thumbnails[]`, a folder of `.wadi` files IS the catalog \u2014 the app lists the folder and indexes each file, with no separate index.json to maintain (see io/templateSource.ts). Non-strict so newer editorial fields don\'t break an older build. |\n| `floors` | array of [floor](#floor) | **yes** | |\n| `_walls_expanded` | boolean | | |\n\n\n## floor\n\n| field | type | req | notes |\n|---|---|---|---|\n| `floor_number` | integer \u2265 0 | **yes** | |\n| `name` | string | **yes** | |\n| `height` | number > 0 | | Per-floor overrides for the default heights in GlobalConfig. In project units (10 units = 1 ft). All three are INDEPENDENT \u2014 no relationship enforced between them: height \u2014 floor-to-floor rise (drives roof wallTop-Z stack) wall_height \u2014 standing wall height (floor top \u2192 ceiling) slab_thickness \u2014 RCC deck between this floor and the one above All fall back to GlobalConfig defaults when omitted. |\n| `wall_height` | number > 0 | | |\n| `slab_thickness` | number \u2265 0 | | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | |\n| `objects` | array of [Object types](#object-types) | **yes** | |\n\n\n## Object types (`floors[].objects[]`)\n\nEvery entry in a floor\'s `objects` array is one of these, tagged by `type`:\n\n### `plinth`\n\nThe plinth is now a normal object placed on the "Plinth" floor (the first floor, number 0), not a top-level config key. Its footprint + height match the old top-level plinth; the plinth floor\'s `height` drives the rise to the floor above (replacing the old hardcoded plinth_height seed).\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `plinth` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | Label |\n| `material` | string | | Material key |\n| `x` | number | **yes** | Top-left X (project units) |\n| `y` | number | **yes** | Top-left Y (project units) |\n| `width` | number > 0 | **yes** | X extent (project units) |\n| `length` | number > 0 | **yes** | Y extent (project units) |\n| `height` | number > 0 | **yes** | Plinth height (project units) |\n| `z_offset` | number | | Lift above ground (project units) |\n\n\n### `ground`\n\nThe ground plane, also on the Plinth floor. Extent defaults to the site plot when authored by the migration. `height` is an optional thickness (0 = a flat plane); slope fields are a later phase.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `ground` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | Label |\n| `material` | string | | Material key |\n| `x` | number | **yes** | Top-left X (project units) |\n| `y` | number | **yes** | Top-left Y (project units) |\n| `width` | number > 0 | **yes** | X extent (project units) |\n| `length` | number > 0 | **yes** | Y extent (project units) |\n| `height` | number \u2265 0 | | Thickness (0 = flat) (project units) |\n| `z_offset` | number | | Lift above origin (project units) |\n\n\n### `component`\n\nAn INSTANCE of a reusable component from the in-file `components` library. It references a component by id (`ref`), overrides the component\'s input variables via `params`, and places it at (x, y) with a `z_offset` lift on its parent floor. At render time `expandRoomWalls` flattens it into concrete objects (resolve component with param+origin overrides \u2192 recurse \u2192 offset), so no renderer needs to know about `component`.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `component` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | |\n| `ref` | string | **yes** | |\n| `params` | map: string \u2192 union \u2014 see notes | | Overrides for the component\'s declared input variables. A string starting with "=" is a formula evaluated in the HOST scope (so it can reference the host\'s variables/points); a number is used directly. |\n| `x` | number | **yes** | |\n| `y` | number | **yes** | |\n| `rotation` | number | | Standard placement: yaw\xB0 about the instance origin (clockwise, same sense as item rotation: 0=south, 90=east). Right angles (0/90/180/270) are exact for any component; a non-right angle is allowed only for furniture-only ones. |\n| `z_offset` | number | | |\n\n\n### `item`\n\nA free-standing GLB furniture / decor instance placed directly on a floor (for pieces that aren\'t inside an enclosed room \u2014 outdoor/site/verandah decor, a loft item, etc.). `x`/`y` are the item\'s plan CENTRE. It MAY instead anchor to a named room via `anchor_to` + `anchor` + `gap`, in which case `x`/`y` are DERIVED at expand time (same anchor model as room-nested items). `rotation` is yaw\xB0; `scale` is a uniform resize; `z_offset` lifts it above the floor base (default = slab thickness). (`itemAsset`, `itemAnchor`, `gapField` are defined above `room`.)\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `item` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | |\n| `asset` | [ItemAsset](#itemasset) | **yes** | |\n| `x` | number | **yes** | |\n| `y` | number | **yes** | |\n| `rotation` | number | | |\n| `scale` | number > 0 | | |\n| `z_offset` | number | | |\n| `anchor_to` | string | | Optional room-relative anchoring (for a free item that should follow a room). |\n| `anchor` | [ItemAnchor](#itemanchor) | | |\n| `gap_x` | number | | |\n| `gap_y` | number | | |\n\n\n### `model`\n\nA GLB placed at real scale and manipulated by a `rig` of named-node ops. Distinct from `item` (furniture, catalog + anchoring): `model` is a rigged structural asset. `asset.dimensions` is the real metre size, used for the 2D footprint and the scale.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `model` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | |\n| `asset` | [ItemAsset](#itemasset) | **yes** | |\n| `x` | number | **yes** | |\n| `y` | number | **yes** | |\n| `rotation` | number | | |\n| `scale` | number > 0 | | |\n| `z_offset` | number | | |\n| `rig` | array of `rigOp` | | |\n\n\n### `floor_slab`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `floor_slab` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | Label |\n| `x` | number | **yes** | Top-left X (project units) |\n| `y` | number | **yes** | Top-left Y (project units) |\n| `width` | number > 0 | **yes** | X extent (project units) |\n| `length` | number > 0 | **yes** | Y extent (project units) |\n| `thickness` | number \u2265 0 | | Slab thickness (defaults to floor\'s) (project units) |\n| `z_offset` | number | | Lift above floor base (project units) |\n\n\n### `pillar`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `pillar` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | **yes** | Label |\n| `x` | number | **yes** | Top-left corner X (project units) |\n| `y` | number | **yes** | Top-left corner Y (project units) |\n| `width` | number > 0 | | X extent (project units) |\n| `length` | number > 0 | | Y extent (project units) |\n| `height` | number > 0 | **yes** | Column height (project units) |\n| `z_offset` | number | | Lift above floor base (project units) |\n\n\n### `beam`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `beam` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | Label |\n| `x` | number | **yes** | Top-left X (project units) |\n| `y` | number | **yes** | Top-left Y (project units) |\n| `width` | number > 0 | **yes** | X extent (project units) |\n| `length` | number > 0 | **yes** | Y extent (project units) |\n| `height` | number > 0 | | Vertical thickness (project units) |\n| `z_offset` | number | | Lift above floor base (project units) |\n\n\n### `room`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `room` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | **yes** | |\n| `x` | number | **yes** | |\n| `y` | number | **yes** | |\n| `width` | number > 0 | **yes** | |\n| `length` | number > 0 | **yes** | |\n| `height` | number \u2265 0 | | 0 accepted \u2014 semantically the same as absent ("use floor default"). Old configs that accidentally saved height: 0 keep loading; the form treats 0 as "no override" and doesn\'t write it back. |\n| `material` | string | | |\n| `z_offset` | number | | Vertical position of the room (its floor + walls), as a lift above the FLOOR BASE (slabZ = plinth top for floor 0, else the floor below\'s top; project units, 10 = 1 ft). This is the UNIFIED z_offset convention: every object is placed at `slabZ + z_offset`. When OMITTED, on-slab objects (room, wall, staircase, kitchen_platform) default z_offset to the floor\'s resolved slab thickness (floor.slab_thickness \u2192 house.defaults.slab_thickness \u2192 code default), so by default they sit on top of the slab, exactly as before. Set it explicitly for split-level floors \u2014 e.g. a room raised onto a thicker slab uses the same value the raised slab\'s top sits at. |\n| `walls` | union \u2014 see notes | | |\n| `wall_heights` | map: string \u2192 [wall_heights entry](#wall-heights-entry) | | |\n| `items` | array of [RoomItem](#roomitem) | | Furniture nested in this room. Each piece is anchored to the room\'s inner footprint (see roomItem), so it reflows when the room resizes. Expanded into top-level `item` objects at render time. |\n\n\n### `wall`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `wall` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | **yes** | |\n| `start_x` | number | **yes** | |\n| `start_y` | number | **yes** | |\n| `end_x` | number | **yes** | |\n| `end_y` | number | **yes** | |\n| `height` | number > 0 | | |\n| `height_end` | number | | |\n| `material` | string | | |\n| `facing` | enum: `north` `south` `east` `west` | | |\n| `z_offset` | number | | Lift above the FLOOR BASE (slabZ), project units. Omitted \u2192 defaults to the floor\'s resolved slab thickness (sits on the slab, as before). Set it for a split-level wall. Same convention as `room`. |\n| `openings` | array of [Opening](#opening) | | |\n\n\n### `staircase`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `staircase` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | |\n| `climb` | enum: `up` `down` | | `climb` picks which end (start_x, start_y) is and which way the flight runs in z as it extends into `direction`: \u2022 "up" (recommended): BOTTOM-anchored. Put the stair on the LOWER floor it rises FROM; (start_x, start_y) is the bottom step\'s near corner on that floor and the flight ASCENDS into `direction`. `rise_height` defaults to THIS floor\'s height (climb to the next level). The intuitive way. \u2022 "down" (DEFAULT, kept for older configs): TOP-anchored. Put the stair on the upper DESTINATION floor; (start_x, start_y) is the top connection and the flight DESCENDS into `direction`. `rise_height` defaults to the floor immediately BELOW this one. Either way the body + landings fill the box [start, start + max_run] along `direction`, and `z_offset` is the ANCHORED end\'s height above the floor base (omitted \u2192 this floor\'s slab thickness, flush with the walking surface). |\n| `start_x` | number | **yes** | |\n| `start_y` | number | **yes** | |\n| `rise_height` | number > 0 | | Total height the stair covers, top \u2192 floor below. The step COUNT is derived: num_steps = round(rise_height / step_rise). Omitted \u2192 defaults to the height of the floor immediately below this one. Formula-capable (e.g. "= floor_height"). Replaces the old explicit `num_steps`. |\n| `step_rise` | number > 0 | **yes** | |\n| `step_tread` | number > 0 | **yes** | |\n| `width` | number > 0 | | BOX MODEL (preferred): give a `width` (X) \xD7 `length` (Y) rectangle and the WHOLE staircase \u2014 flights + turn landings \u2014 is packed to fit INSIDE it. (start_x,start_y) is then the box\'s min corner, NOT the first step, and each flight\'s width is DERIVED from the box: two switchback lanes = (lateral \u2212 flight_gap)/2, or the full box for a single flight. `direction` picks the run axis (N/S \u2192 run along length/Y; E/W \u2192 run along width/X). The model errors if the box is too small for even the tightest split. Omit both to use the legacy `step_width` + `max_run` form below. |\n| `length` | number > 0 | | |\n| `step_width` | number > 0 | | Legacy per-step width (required by the old form; derived from the box in the box model). The expanded single-flight staircases the renderer consumes always carry it. |\n| `direction` | enum: `north` `south` `east` `west` | **yes** | The direction the stair EXTENDS from its top \u2014 the whole assembly fills the allocated box from (start_x,start_y) going this way for up to `max_run`. |\n| `max_run` | number > 0 | | ALLOCATED run: the length of space reserved for the stair along `direction`. The WHOLE assembly (flights + turn landings) is kept within [start, start+max_run]; when the run won\'t fit as one flight it auto-splits into switchback flights (more flights when tight), expanded in expandRoomWalls into plain staircases + floor_slab landings so every renderer is unchanged. Omit \u2192 one flight, no length limit. |\n| `landing_depth` | number > 0 | | Turn-landing depth (along the run). Omitted \u2192 equals step_width. |\n| `landing_thickness` | number \u2265 0 | | Turn-landing slab thickness. Omitted \u2192 equals step_rise. |\n| `turn` | enum: `clockwise` `anticlockwise` | | Switchback handedness, reckoned DESCENDING from the top. Omitted \u2192 "clockwise". Only affects split stairs. |\n| `flight_gap` | number > 0 | | Lateral gap between the two switchback flights (a stairwell void for a spine wall). Omitted/0 \u2192 flights are adjacent. The turn landings widen to bridge the gap. Only affects split stairs. |\n| `z_offset` | number | | Height of the stair\'s TOP above the floor base (slabZ; project units, 10 = 1 ft). Omitted \u2192 this floor\'s slab thickness, so the top is flush with the walking surface and the flights descend to the floor below. Raise it for an internal step whose top sits above the floor. |\n| `material` | string | | |\n\n\n### `spiral_staircase`\n\nA helical staircase: `steps` treads winding `turns` revolutions around a central pole, from the floor to `total_height`, within `radius`. Placed by its CENTRE (x, y). Optional fields fall back to sensible defaults at render time.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `spiral_staircase` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | Label |\n| `x` | number | **yes** | Centre X (project units) |\n| `y` | number | **yes** | Centre Y (project units) |\n| `radius` | number > 0 | **yes** | Outer radius (project units) |\n| `total_height` | number > 0 | **yes** | Total rise (floor to top step) (project units) |\n| `turns` | number > 0 | | Revolutions (default 1) |\n| `steps` | integer | | Number of treads (default ~12 per turn) |\n| `tread_thickness` | number > 0 | | Tread slab thickness (project units) |\n| `pole_radius` | number > 0 | | Central pole radius (project units) |\n| `z_offset` | number | | Lift above floor base (project units) |\n\n\n### `door`\n\nFlat door/window remain valid as a legacy schema \u2014 new configs nest them inside room.walls[side].openings or wall.openings.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `door` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | **yes** | |\n| `x` | number | **yes** | |\n| `y` | number | **yes** | |\n| `width` | number > 0 | **yes** | |\n| `height` | number > 0 | **yes** | |\n| `direction` | enum: `north` `south` `east` `west` | **yes** | |\n| `room` | string | | |\n| `wall` | string | | |\n| `open` | boolean | | |\n\n\n### `window`\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `window` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | **yes** | |\n| `x` | number | **yes** | |\n| `y` | number | **yes** | |\n| `width` | number > 0 | **yes** | |\n| `height` | number > 0 | **yes** | |\n| `sill_height` | number \u2265 0 | | |\n| `direction` | enum: `north` `south` `east` `west` | **yes** | |\n| `room` | string | | |\n| `wall` | string | | |\n| `open` | boolean | | |\n\n\n### `kitchen_platform`\n\nKitchen platform \u2014 a polyline countertop / cooking slab that runs along the base of walls. Path is the wall-side edge; the platform extends `depth` units perpendicular to each segment on the given `side`. Renders as one box per path segment; corners meet at the shared point (no fancy mitering in v1).\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `kitchen_platform` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n| `layer` | string | | *(shared \u2014 see top)* |\n| `name` | string | | |\n| `path` | array of tuple `[n,n]` | **yes** | |\n| `side` | enum: `left` `right` | **yes** | |\n| `depth` | number > 0 | **yes** | |\n| `height` | number > 0 | **yes** | |\n| `z_offset` | number | | Lift above the FLOOR BASE (slabZ), project units. Omitted \u2192 defaults to the floor\'s resolved slab thickness (sits on the slab top, as before). Same convention as `room`. |\n| `base_z` | number | | |\n| `material` | string | | |\n\n\n### `roof`\n\nv2 roof \u2014 unified segment-based type that replaces hip/gable/flat/shed. Schema is permissive; the v2 pipeline (svg2d/roof/v2/) validates segments + slope + endpoint style at derivation time.\n\n\n> **Freeform:** extra fields are allowed (`.catchall`) and validated at derivation time. See `roof-v2-guide.md`.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `type` | literal `roof` | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | *(shared \u2014 see top)* |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | *(shared \u2014 see top)* |\n\n\n## Shared & nested schemas\n\n### site\n\nObjects don\'t bind to the grid with a special field \u2014 a grid line\'s position is published as a formula symbol (`<gridId>.x<name>` / `.y<name>`, see param/resolve.ts), so a room places itself with ordinary `formulas`, e.g. { x: "= main.x1", width: "= main.x5 - main.x1" }. With coord_convention:"center" those are wall centrelines and expandRoomWalls handles the wall extent.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `reference_x` | number | **yes** | |\n| `reference_y` | number | **yes** | |\n| `plot_length` | number > 0 | **yes** | |\n| `plot_width` | number > 0 | **yes** | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | |\n\n\n### houseDefaults\n\nHouse-level overrides for the built-in GlobalConfig defaults. Every floor without its own value falls back to these; if these are absent too, the code defaults in DEFAULT_GLOBAL_CONFIG apply.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `floor_height` | number > 0 | | |\n| `wall_height` | number > 0 | | |\n| `slab_thickness` | number \u2265 0 | | |\n| `wall_thickness` | number > 0 | | House-wide wall thickness (project units). Per-object `wall_thickness`/`thickness` overrides still win. Falls back to the code default (DEFAULT_GLOBAL_CONFIG.wall_thickness = 8) when omitted. |\n| `formulas` | map: field name \u2192 `"= formula"` string | | |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | |\n\n\n### units\n\nHow dimensions are LABELLED on the drawings. Display-only \u2014 geometry always stays in project units; this just controls the text on the dimension lines. Omitted = the built-in default (feet & inches, 10 project units = 1 ft).\n\n| field | type | req | notes |\n|---|---|---|---|\n| `system` | enum: `feet_inches` `feet` `meters` `centimeters` `millimeters` | | feet_inches \u2192 12\' 6" ; the rest \u2192 decimal with a unit suffix. |\n| `per_unit` | number > 0 | | Project units that equal ONE display unit (10 \u2192 10 units = 1 ft; 100 \u2192 100 units = 1 m). Default 10. |\n| `precision` | integer \u2265 0 | | Decimal places for the non-feet_inches systems. |\n\n\n### Opening\n\nThe plinth is now a normal object placed on the "Plinth" floor (the first floor, number 0), not a top-level config key. Its footprint + height match the old top-level plinth; the plinth floor\'s `height` drives the rise to the floor above (replacing the old hardcoded plinth_height seed). `plinth` + `ground` are GENERATED from fields (schema/fields/{plinth,ground}.ts), imported above as plinthObject / groundObject. (P2b)\n\n| field | type | req | notes |\n|---|---|---|---|\n| `kind` | enum: `door` `window` | **yes** | |\n| `name` | string | | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | Numeric fields hold the RESOLVED value; a `= formula` for any of them lives in `formulas` (e.g. formulas.offset), evaluated by resolveParametric against the house variables/points \u2014 same pattern as every other object. |\n| `offset` | number | **yes** | Any number: `center` anchor takes a signed shift; the resolved placement is fit-checked against the wall in expand.ts, so a bad value errors there. |\n| `anchor` | enum: `start` `center` `end` | | Which end of the wall `offset` is measured from, so the opening holds its place when the wall/room scales \u2014 no formula needed. `start` (default, legacy): offset from the wall start to the near edge. `end`: offset from the wall end to the far edge (0 = flush to the end). `center`: signed shift of the opening centre from the wall midpoint (0 = centred; may be negative). Resolved to a start-based offset in expand.ts (openingAnchor.ts). |\n| `width` | number > 0 | **yes** | |\n| `height` | number > 0 | **yes** | |\n| `sill_height` | number | | |\n| `direction` | enum: `north` `south` `east` `west` | | |\n| `facing` | enum: `north` `south` `east` `west` | | |\n| `open` | boolean | | When true, the opening is left BARE (just a hole) \u2014 no glazing/frame for a window, no leaf for a door \u2014 e.g. an open doorway or unglazed vent. |\n\n\n### RoomWallSide\n| field | type | req | notes |\n|---|---|---|---|\n| `height` | number | | |\n| `height_end` | number | | |\n| `openings` | array of [Opening](#opening) | | |\n\n\n### RoomItem\n\nA furniture piece nested INSIDE a room (room.items[]). It has NO x/y \u2014 its plan position is DERIVED at expand time from the parent room\'s footprint + `anchor` + per-axis gap (+ its own `rotation`). Flattened into a top-level `item` for every renderer. `gap_x`/`gap_y` are the inset (project units) kept from the anchor into the room (edge/corner anchor \u2192 clears the wall; centre anchor \u2192 signed offset, +x east / +y south). `gap_x`/`gap_y`/`rotation`/`scale`/`z_offset` are all plain numeric fields so each can be driven by a `= formula` (via the `formulas` map).\n\n| field | type | req | notes |\n|---|---|---|---|\n| `name` | string | | |\n| `formulas` | map: field name \u2192 `"= formula"` string | | |\n| `enabled` | boolean or number (`false`/`0` = hidden) | | |\n| `layer` | string | | |\n| `asset` | [ItemAsset](#itemasset) | **yes** | |\n| `anchor` | [ItemAnchor](#itemanchor) | | |\n| `gap_x` | number | | |\n| `gap_y` | number | | |\n| `rotation` | number | | |\n| `scale` | number > 0 | | |\n| `z_offset` | number | | |\n\n\n### ItemAsset\n\nFurniture (GLB `item`) \u2014 shared schema pieces Defined BEFORE `room` so a room can nest its own `items[]`. Asset distances are METRES (the GLB\'s native unit); the 3D/2D layers scale them into project units. See registry/nodes/item + three/units. The asset backing a furniture item \u2014 stored INLINE so a .wadi is self-contained (share links / web load the GLB from `src`). A catalog is just a picker convenience.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `id` | string | **yes** | |\n| `name` | string | | |\n| `src` | string | **yes** | |\n| `dimensions` | tuple `[n>0,n>0,n>0]` | **yes** | |\n| `thumbnail` | string | | |\n| `floorPlanUrl` | string | | |\n| `category` | string | | |\n| `tags` | array of string | | |\n| `offset` | tuple `[n,n,n]` | | |\n| `corrRotation` | tuple `[n,n,n]` | | |\n| `corrScale` | tuple `[n>0,n>0,n>0]` | | |\n\n\n### ComponentDef\n| field | type | req | notes |\n|---|---|---|---|\n| `name` | string | | |\n| `goal` | string | | A short natural-language description of what this component accomplishes (the discovery key for goal-based module lookup, e.g. "climb to the next floor"). Purely metadata \u2014 renderers ignore it. |\n| `params` | array of [ComponentParam](#componentparam) | | |\n| `variables` | map: string \u2192 number, or `"= formula"` string | | |\n| `points` | map: string \u2192 inline object | | |\n| `objects` | array of [Object types](#object-types) | **yes** | |\n| `expose` | inline object | | Promote this component to a typed primitive at load time (plans/declarative-plugins.md P0). When present, the component registers a NodeDefinition of type `expose.type` whose fields come from `params`; it can then be used like any core object type. `type` is namespaced (`pack.thing`). |\n\n\n### ComponentParam\n\nA reusable component DEFINITION in the in-file `components` library. It is a mini-house: its own `variables`/`points` and a flat `objects` body authored in LOCAL coords (origin 0,0). `params` names which variables are the public inputs (label/default for the instance form). A `component` instance overrides those variables and places the body at its (x,y,z_offset). Stored once; referenced by many instances.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `name` | string | **yes** | |\n| `label` | string | | |\n| `description` | string | | |\n| `default` | number | | |\n| `kind` | string | | Field-projection annotations, used only when the component is `expose`d as a typed primitive (plans/declarative-plugins.md). `kind` is a FieldKind preset (coord/extent/nonneg/int/text/flag/enum); when absent the kind is inferred from the default\'s type. `unit` is a doc-only unit hint. |\n| `unit` | string | | |\n\n\n### LayerDef\n\nA visibility layer for the 3D view. Each object may reference a layer by `id` (via its `layer` field); the layers menu toggles whole layers on/off. Display-only \u2014 never affects geometry. Optional: when absent, a built-in default layer set is used, and objects fall back to an automatic per-type/floor mapping.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `id` | string (non-empty) | **yes** | |\n| `label` | string | **yes** | |\n| `color` | string | | |\n| `group` | string | | Friendly group for the owner "Show/hide layers" menu (e.g. "Roof", "Walls"). Layers sharing a group toggle together. Optional. |\n\n\n### configurator\n| field | type | req | notes |\n|---|---|---|---|\n| `title` | string | | |\n| `description` | string | | |\n| `groups` | array of inline object | | |\n| `inputs` | array of [ConfiguratorInput](#configuratorinput) | **yes** | |\n\n\n### ConfiguratorInput\n\nConfigurator (Gharkul owner UI) Optional, author-supplied metadata: which `variables`/`points` a template exposes to end users, and how to present them. IGNORED by the resolver and every geometry consumer \u2014 read only by the owner-facing Configurator UI. `target` is a variable name (e.g. "floorH") or a point coordinate ("House.W" \u2192 points.House.x; W/L/X/Y/x/y are resolver synonyms). `min`/`max`/ `step` are in RAW project units; `unit` only affects display.\n\n| field | type | req | notes |\n|---|---|---|---|\n| `target` | string (non-empty) | **yes** | |\n| `label` | string (non-empty) | **yes** | |\n| `description` | string | | |\n| `control` | enum: `slider` `number` `select` `toggle` | | |\n| `unit` | enum: `ft` `in` `m` `units` `percent` `count` `none` | | |\n| `min` | number | | |\n| `max` | number | | |\n| `step` | number > 0 | | |\n| `options` | array of inline object | | |\n| `group` | string | | |\n\n\n### ItemAnchor\n\n9-point anchor on a room\'s INNER footprint. First token = vertical (top = north \u2026 bottom = south), second = horizontal (left = west \u2026 right = east); "center" alone = both. The item aligns its matching edge/corner to this spot, held `gap` off it, into the room \u2014 so it reflows when the room is resized.\n\nEnum: `top-left` `top-center` `top-right` `center-left` `center` `center-right` `bottom-left` `bottom-center` `bottom-right`\n'
|
|
431145
432508
|
}
|
|
431146
432509
|
};
|
|
431147
432510
|
var MODULES = {
|