wadi-mcp 0.1.36 → 0.1.37
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 +854 -212
- package/package.json +1 -1
package/dist/server.mjs
CHANGED
|
@@ -13618,7 +13618,7 @@ function formulaDeps(src) {
|
|
|
13618
13618
|
return [];
|
|
13619
13619
|
}
|
|
13620
13620
|
}
|
|
13621
|
-
function evalFormula(src, scope) {
|
|
13621
|
+
function evalFormula(src, scope, fn) {
|
|
13622
13622
|
let ast;
|
|
13623
13623
|
const deps = [];
|
|
13624
13624
|
try {
|
|
@@ -13650,8 +13650,11 @@ function evalFormula(src, scope) {
|
|
|
13650
13650
|
return args.length === 1 ? Math.ceil(args[0]) : setErr("ceil(x) needs 1 arg");
|
|
13651
13651
|
case "abs":
|
|
13652
13652
|
return args.length === 1 ? Math.abs(args[0]) : setErr("abs(x) needs 1 arg");
|
|
13653
|
-
default:
|
|
13653
|
+
default: {
|
|
13654
|
+
const custom4 = fn?.(name, args);
|
|
13655
|
+
if (typeof custom4 === "number" && Number.isFinite(custom4)) return custom4;
|
|
13654
13656
|
return setErr(`unknown function '${name}'`);
|
|
13657
|
+
}
|
|
13655
13658
|
}
|
|
13656
13659
|
};
|
|
13657
13660
|
const evalNode = (n3) => {
|
|
@@ -13660,11 +13663,16 @@ function evalFormula(src, scope) {
|
|
|
13660
13663
|
return n3.v;
|
|
13661
13664
|
case "ref": {
|
|
13662
13665
|
const v = scope[n3.name];
|
|
13663
|
-
if (typeof v
|
|
13664
|
-
|
|
13665
|
-
|
|
13666
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
13667
|
+
if (fn) {
|
|
13668
|
+
const m = /^(.+\.[xy])(\d+)$/.exec(n3.name);
|
|
13669
|
+
if (m) {
|
|
13670
|
+
const g = fn(m[1], [Number(m[2])]);
|
|
13671
|
+
if (typeof g === "number" && Number.isFinite(g)) return g;
|
|
13672
|
+
}
|
|
13666
13673
|
}
|
|
13667
|
-
|
|
13674
|
+
if (!error53) error53 = `unknown or unresolved '${n3.name}'`;
|
|
13675
|
+
return NaN;
|
|
13668
13676
|
}
|
|
13669
13677
|
case "neg":
|
|
13670
13678
|
return -evalNode(n3.a);
|
|
@@ -13799,7 +13807,7 @@ function hasRoofNestedFormulas(obj) {
|
|
|
13799
13807
|
if (Array.isArray(trusses) && trusses.some(hasFormulas)) return true;
|
|
13800
13808
|
return hasFormulas(o.slope) || hasFormulas(o.slope_left) || hasFormulas(o.slope_right);
|
|
13801
13809
|
}
|
|
13802
|
-
function resolveSegment(seg, scope, warnings, where) {
|
|
13810
|
+
function resolveSegment(seg, scope, warnings, where, fn) {
|
|
13803
13811
|
const s = seg;
|
|
13804
13812
|
const fm = s?.formulas;
|
|
13805
13813
|
if (!s || !fm || Object.keys(fm).length === 0) return { value: seg, changed: false };
|
|
@@ -13808,7 +13816,7 @@ function resolveSegment(seg, scope, warnings, where) {
|
|
|
13808
13816
|
const start = Array.isArray(s.start) ? [...s.start] : void 0;
|
|
13809
13817
|
const end = Array.isArray(s.end) ? [...s.end] : void 0;
|
|
13810
13818
|
for (const [field, src] of Object.entries(fm)) {
|
|
13811
|
-
const r2 = evalFormula(src, scope);
|
|
13819
|
+
const r2 = evalFormula(src, scope, fn);
|
|
13812
13820
|
if (r2.value === null) {
|
|
13813
13821
|
warnings.push({ where: `${where}/${field}`, formula: src, message: r2.error ?? "invalid formula", severity: "error" });
|
|
13814
13822
|
continue;
|
|
@@ -13833,14 +13841,14 @@ function resolveSegment(seg, scope, warnings, where) {
|
|
|
13833
13841
|
if (end) next.end = end;
|
|
13834
13842
|
return { value: next, changed: true };
|
|
13835
13843
|
}
|
|
13836
|
-
function resolveTruss(truss, scope, warnings, where) {
|
|
13844
|
+
function resolveTruss(truss, scope, warnings, where, fn) {
|
|
13837
13845
|
const t = truss;
|
|
13838
13846
|
const fm = t?.formulas;
|
|
13839
13847
|
if (!t || !fm || Object.keys(fm).length === 0) return { value: truss, changed: false };
|
|
13840
13848
|
const positions = Array.isArray(t.positions_along) ? [...t.positions_along] : [];
|
|
13841
13849
|
let changed = false;
|
|
13842
13850
|
for (const [field, src] of Object.entries(fm)) {
|
|
13843
|
-
const r2 = evalFormula(src, scope);
|
|
13851
|
+
const r2 = evalFormula(src, scope, fn);
|
|
13844
13852
|
if (r2.value === null) {
|
|
13845
13853
|
warnings.push({ where: `${where}/${field}`, formula: src, message: r2.error ?? "invalid formula", severity: "error" });
|
|
13846
13854
|
continue;
|
|
@@ -13856,7 +13864,7 @@ function resolveTruss(truss, scope, warnings, where) {
|
|
|
13856
13864
|
}
|
|
13857
13865
|
return changed ? { value: { ...t, positions_along: positions }, changed: true } : { value: truss, changed: false };
|
|
13858
13866
|
}
|
|
13859
|
-
function resolveRoofNested(obj, scope, warnings, where) {
|
|
13867
|
+
function resolveRoofNested(obj, scope, warnings, where, fn) {
|
|
13860
13868
|
const o = obj;
|
|
13861
13869
|
if (o?.type !== "roof") return { value: obj, changed: false };
|
|
13862
13870
|
let changed = false;
|
|
@@ -13864,7 +13872,7 @@ function resolveRoofNested(obj, scope, warnings, where) {
|
|
|
13864
13872
|
if (Array.isArray(o.segments)) {
|
|
13865
13873
|
let segChanged = false;
|
|
13866
13874
|
const next = o.segments.map((s, i2) => {
|
|
13867
|
-
const r2 = resolveSegment(s, scope, warnings, `${where}/seg${i2}
|
|
13875
|
+
const r2 = resolveSegment(s, scope, warnings, `${where}/seg${i2}`, fn);
|
|
13868
13876
|
if (r2.changed) segChanged = true;
|
|
13869
13877
|
return r2.value;
|
|
13870
13878
|
});
|
|
@@ -13876,7 +13884,7 @@ function resolveRoofNested(obj, scope, warnings, where) {
|
|
|
13876
13884
|
if (Array.isArray(o.trusses)) {
|
|
13877
13885
|
let tChanged = false;
|
|
13878
13886
|
const next = o.trusses.map((t, i2) => {
|
|
13879
|
-
const r2 = resolveTruss(t, scope, warnings, `${where}/truss${i2}
|
|
13887
|
+
const r2 = resolveTruss(t, scope, warnings, `${where}/truss${i2}`, fn);
|
|
13880
13888
|
if (r2.changed) tChanged = true;
|
|
13881
13889
|
return r2.value;
|
|
13882
13890
|
});
|
|
@@ -13887,7 +13895,7 @@ function resolveRoofNested(obj, scope, warnings, where) {
|
|
|
13887
13895
|
}
|
|
13888
13896
|
for (const key of ["slope", "slope_left", "slope_right"]) {
|
|
13889
13897
|
if (o[key] && typeof o[key] === "object") {
|
|
13890
|
-
const r2 = applyContainerFormulas(o[key], scope, warnings, `${where}/${key}
|
|
13898
|
+
const r2 = applyContainerFormulas(o[key], scope, warnings, `${where}/${key}`, fn);
|
|
13891
13899
|
if (r2.changed) {
|
|
13892
13900
|
patch[key] = r2.value;
|
|
13893
13901
|
changed = true;
|
|
@@ -13896,12 +13904,12 @@ function resolveRoofNested(obj, scope, warnings, where) {
|
|
|
13896
13904
|
}
|
|
13897
13905
|
return changed ? { value: { ...o, ...patch }, changed: true } : { value: obj, changed: false };
|
|
13898
13906
|
}
|
|
13899
|
-
function resolveOpenings(obj, scope, warnings, where) {
|
|
13907
|
+
function resolveOpenings(obj, scope, warnings, where, fn) {
|
|
13900
13908
|
const o = obj;
|
|
13901
13909
|
if (o?.type === "wall" && Array.isArray(o.openings)) {
|
|
13902
13910
|
let changed = false;
|
|
13903
13911
|
const next = o.openings.map((op, i2) => {
|
|
13904
|
-
const r2 = applyContainerFormulas(op, scope, warnings, `${where}/opening${i2}
|
|
13912
|
+
const r2 = applyContainerFormulas(op, scope, warnings, `${where}/opening${i2}`, fn);
|
|
13905
13913
|
if (r2.changed) changed = true;
|
|
13906
13914
|
return r2.value;
|
|
13907
13915
|
});
|
|
@@ -13920,7 +13928,7 @@ function resolveOpenings(obj, scope, warnings, where) {
|
|
|
13920
13928
|
}
|
|
13921
13929
|
let sideChanged = false;
|
|
13922
13930
|
const nextOps = ops.map((op, i2) => {
|
|
13923
|
-
const r2 = applyContainerFormulas(op, scope, warnings, `${where}/${side2}/opening${i2}
|
|
13931
|
+
const r2 = applyContainerFormulas(op, scope, warnings, `${where}/${side2}/opening${i2}`, fn);
|
|
13924
13932
|
if (r2.changed) sideChanged = true;
|
|
13925
13933
|
return r2.value;
|
|
13926
13934
|
});
|
|
@@ -13931,14 +13939,14 @@ function resolveOpenings(obj, scope, warnings, where) {
|
|
|
13931
13939
|
}
|
|
13932
13940
|
return { value: obj, changed: false };
|
|
13933
13941
|
}
|
|
13934
|
-
function applyContainerFormulas(container, scope, warnings, where) {
|
|
13942
|
+
function applyContainerFormulas(container, scope, warnings, where, fn) {
|
|
13935
13943
|
const c = container;
|
|
13936
13944
|
const fm = c?.formulas;
|
|
13937
13945
|
if (!c || !fm || Object.keys(fm).length === 0) return { value: container, changed: false };
|
|
13938
13946
|
const next = { ...c };
|
|
13939
13947
|
let changed = false;
|
|
13940
13948
|
for (const [field, src] of Object.entries(fm)) {
|
|
13941
|
-
const r2 = evalFormula(src, scope);
|
|
13949
|
+
const r2 = evalFormula(src, scope, fn);
|
|
13942
13950
|
if (r2.value === null) {
|
|
13943
13951
|
warnings.push({ where: `${where}/${field}`, formula: src, message: r2.error ?? "invalid formula", severity: "error" });
|
|
13944
13952
|
continue;
|
|
@@ -13973,9 +13981,10 @@ function buildScope(config3) {
|
|
|
13973
13981
|
scope[s.key] = r2.value;
|
|
13974
13982
|
}
|
|
13975
13983
|
}
|
|
13976
|
-
|
|
13984
|
+
const fn = buildGuideAccessor(config3, scope, warnings);
|
|
13985
|
+
if (Object.keys(mergedGuides(config3)).length) {
|
|
13977
13986
|
const defaultT = config3.defaults?.wall_thickness ?? 8;
|
|
13978
|
-
const grids = resolveGrids(config3, scope, warnings, defaultT);
|
|
13987
|
+
const grids = resolveGrids(config3, scope, warnings, defaultT, fn);
|
|
13979
13988
|
for (const [id, g] of grids) {
|
|
13980
13989
|
for (const [name, pos] of g.x) scope[`${id}.x${name}`] = pos;
|
|
13981
13990
|
for (const [name, pos] of g.y) scope[`${id}.y${name}`] = pos;
|
|
@@ -13988,7 +13997,7 @@ function buildScope(config3) {
|
|
|
13988
13997
|
seen.add(k);
|
|
13989
13998
|
return true;
|
|
13990
13999
|
});
|
|
13991
|
-
return { scope, warnings: deduped };
|
|
14000
|
+
return { scope, warnings: deduped, fn };
|
|
13992
14001
|
}
|
|
13993
14002
|
function buildScopeCached(config3) {
|
|
13994
14003
|
if (!config3 || typeof config3 !== "object") return { scope: {}, warnings: [] };
|
|
@@ -14002,24 +14011,93 @@ function scopeForConfig(config3) {
|
|
|
14002
14011
|
return buildScopeCached(config3).scope;
|
|
14003
14012
|
}
|
|
14004
14013
|
function resolvedGridsForConfig(config3) {
|
|
14005
|
-
if (!config3 || typeof config3 !== "object" || !config3.
|
|
14006
|
-
const { scope, warnings } = buildScopeCached(config3);
|
|
14014
|
+
if (!config3 || typeof config3 !== "object" || !Object.keys(mergedGuides(config3)).length) return /* @__PURE__ */ new Map();
|
|
14015
|
+
const { scope, warnings, fn } = buildScopeCached(config3);
|
|
14007
14016
|
const defaultT = config3.defaults?.wall_thickness ?? 8;
|
|
14008
|
-
return resolveGrids(config3, scope, warnings.slice(), defaultT);
|
|
14017
|
+
return resolveGrids(config3, scope, warnings.slice(), defaultT, fn);
|
|
14018
|
+
}
|
|
14019
|
+
function resolvedGeneratedGuidesForConfig(config3) {
|
|
14020
|
+
const out = /* @__PURE__ */ new Map();
|
|
14021
|
+
if (!config3 || typeof config3 !== "object" || !Object.keys(mergedGuides(config3)).length) return out;
|
|
14022
|
+
const { scope } = buildScopeCached(config3);
|
|
14023
|
+
const num5 = (v, dflt) => {
|
|
14024
|
+
if (typeof v === "number") return v;
|
|
14025
|
+
if (isFormula(v)) {
|
|
14026
|
+
const r2 = evalFormula(v, scope);
|
|
14027
|
+
return r2.value === null ? dflt : r2.value;
|
|
14028
|
+
}
|
|
14029
|
+
return dflt;
|
|
14030
|
+
};
|
|
14031
|
+
for (const [id, g] of Object.entries(mergedGuides(config3))) {
|
|
14032
|
+
if (!isGeneratedGuide(g)) continue;
|
|
14033
|
+
const gg = g;
|
|
14034
|
+
const dx = num5(gg.spacing?.[0], 0);
|
|
14035
|
+
const dy = num5(gg.spacing?.[1], 0);
|
|
14036
|
+
if (!dx && !dy) continue;
|
|
14037
|
+
out.set(id, {
|
|
14038
|
+
ox: num5(gg.origin?.[0], 0),
|
|
14039
|
+
oy: num5(gg.origin?.[1], 0),
|
|
14040
|
+
dx,
|
|
14041
|
+
dy,
|
|
14042
|
+
ex: Math.max(0, Math.round(num5(gg.extent?.[0], 0))),
|
|
14043
|
+
ey: Math.max(0, Math.round(num5(gg.extent?.[1], 0)))
|
|
14044
|
+
});
|
|
14045
|
+
}
|
|
14046
|
+
return out;
|
|
14009
14047
|
}
|
|
14010
14048
|
function formulaFieldError(config3, src) {
|
|
14011
14049
|
if (!src) return null;
|
|
14012
|
-
const
|
|
14050
|
+
const { scope, fn } = buildScopeCached(config3);
|
|
14051
|
+
const r2 = evalFormula(src, scope, fn);
|
|
14013
14052
|
return r2.value === null ? r2.error ?? "invalid formula" : null;
|
|
14014
14053
|
}
|
|
14015
|
-
function
|
|
14054
|
+
function mergedGuides(config3) {
|
|
14055
|
+
const c = config3;
|
|
14056
|
+
return { ...c.grids ?? {}, ...c.guides ?? {} };
|
|
14057
|
+
}
|
|
14058
|
+
function isGeneratedGuide(g) {
|
|
14059
|
+
return !!g && typeof g === "object" && "spacing" in g && !("x" in g);
|
|
14060
|
+
}
|
|
14061
|
+
function buildGuideAccessor(config3, scope, warnings) {
|
|
14062
|
+
const acc = /* @__PURE__ */ new Map();
|
|
14063
|
+
const numAt = (v, dflt, where) => {
|
|
14064
|
+
if (v === void 0) return dflt;
|
|
14065
|
+
if (typeof v === "number") return v;
|
|
14066
|
+
if (isFormula(v)) {
|
|
14067
|
+
const r2 = evalFormula(v, scope);
|
|
14068
|
+
if (r2.value === null) {
|
|
14069
|
+
warnings.push({ where, formula: v, message: r2.error ?? "invalid formula", severity: "error" });
|
|
14070
|
+
return dflt;
|
|
14071
|
+
}
|
|
14072
|
+
return r2.value;
|
|
14073
|
+
}
|
|
14074
|
+
return dflt;
|
|
14075
|
+
};
|
|
14076
|
+
for (const [id, g] of Object.entries(mergedGuides(config3))) {
|
|
14077
|
+
if (!isGeneratedGuide(g)) continue;
|
|
14078
|
+
const gg = g;
|
|
14079
|
+
const ox = numAt(gg.origin?.[0], 0, `guides/${id}/origin/x`) ?? 0;
|
|
14080
|
+
const oy = numAt(gg.origin?.[1], 0, `guides/${id}/origin/y`) ?? 0;
|
|
14081
|
+
const dx = numAt(gg.spacing?.[0], null, `guides/${id}/spacing/x`);
|
|
14082
|
+
const dy = numAt(gg.spacing?.[1], null, `guides/${id}/spacing/y`);
|
|
14083
|
+
if (dx !== null) acc.set(`${id}.x`, { origin: ox, spacing: dx });
|
|
14084
|
+
if (dy !== null) acc.set(`${id}.y`, { origin: oy, spacing: dy });
|
|
14085
|
+
}
|
|
14086
|
+
if (acc.size === 0) return void 0;
|
|
14087
|
+
return (name, args) => {
|
|
14088
|
+
const a = acc.get(name);
|
|
14089
|
+
if (!a || args.length !== 1 || !Number.isFinite(args[0])) return void 0;
|
|
14090
|
+
return a.origin + args[0] * a.spacing;
|
|
14091
|
+
};
|
|
14092
|
+
}
|
|
14093
|
+
function resolveGrids(config3, scope, warnings, defaultT, fn) {
|
|
14016
14094
|
const out = /* @__PURE__ */ new Map();
|
|
14017
|
-
const grids = config3
|
|
14018
|
-
if (!grids) return out;
|
|
14095
|
+
const grids = mergedGuides(config3);
|
|
14096
|
+
if (!Object.keys(grids).length) return out;
|
|
14019
14097
|
const num5 = (v, where) => {
|
|
14020
14098
|
if (typeof v === "number") return v;
|
|
14021
14099
|
if (isFormula(v)) {
|
|
14022
|
-
const r2 = evalFormula(v, scope);
|
|
14100
|
+
const r2 = evalFormula(v, scope, fn);
|
|
14023
14101
|
if (r2.value === null) {
|
|
14024
14102
|
warnings.push({ where, formula: v, message: r2.error ?? "invalid formula", severity: "error" });
|
|
14025
14103
|
return null;
|
|
@@ -14029,6 +14107,7 @@ function resolveGrids(config3, scope, warnings, defaultT) {
|
|
|
14029
14107
|
return null;
|
|
14030
14108
|
};
|
|
14031
14109
|
for (const [id, gRaw] of Object.entries(grids)) {
|
|
14110
|
+
if (isGeneratedGuide(gRaw)) continue;
|
|
14032
14111
|
const g = gRaw;
|
|
14033
14112
|
const rg = { x: /* @__PURE__ */ new Map(), y: /* @__PURE__ */ new Map(), xt: /* @__PURE__ */ new Map(), yt: /* @__PURE__ */ new Map() };
|
|
14034
14113
|
for (const axis of ["x", "y"]) {
|
|
@@ -14061,30 +14140,31 @@ function resolveParametric(config3) {
|
|
|
14061
14140
|
if (!hasVars && !hasPts && !hasContainerFormulas) {
|
|
14062
14141
|
return { config: config3, warnings: [] };
|
|
14063
14142
|
}
|
|
14064
|
-
const { scope, warnings } = buildScope(config3);
|
|
14143
|
+
const { scope, warnings, fn } = buildScope(config3);
|
|
14065
14144
|
let anyFloorChanged = false;
|
|
14066
14145
|
const mappedFloors = config3.floors.map((f2, fi) => {
|
|
14067
14146
|
let objectsChanged = false;
|
|
14068
14147
|
const objects = f2.objects.map((o, oi) => {
|
|
14069
|
-
const res2 = applyContainerFormulas(o, scope, warnings, `floor${fi}/obj${oi}
|
|
14070
|
-
const opRes = resolveOpenings(res2.value, scope, warnings, `floor${fi}/obj${oi}
|
|
14071
|
-
const roofRes = resolveRoofNested(opRes.value, scope, warnings, `floor${fi}/obj${oi}
|
|
14148
|
+
const res2 = applyContainerFormulas(o, scope, warnings, `floor${fi}/obj${oi}`, fn);
|
|
14149
|
+
const opRes = resolveOpenings(res2.value, scope, warnings, `floor${fi}/obj${oi}`, fn);
|
|
14150
|
+
const roofRes = resolveRoofNested(opRes.value, scope, warnings, `floor${fi}/obj${oi}`, fn);
|
|
14072
14151
|
if (res2.changed || opRes.changed || roofRes.changed) objectsChanged = true;
|
|
14073
14152
|
return roofRes.value;
|
|
14074
14153
|
});
|
|
14075
14154
|
const base = objectsChanged ? { ...f2, objects } : f2;
|
|
14076
|
-
const res = applyContainerFormulas(base, scope, warnings, `floor${fi}
|
|
14155
|
+
const res = applyContainerFormulas(base, scope, warnings, `floor${fi}`, fn);
|
|
14077
14156
|
const finalFloor = res.value;
|
|
14078
14157
|
if (finalFloor !== f2) anyFloorChanged = true;
|
|
14079
14158
|
return finalFloor;
|
|
14080
14159
|
});
|
|
14081
14160
|
const floors = anyFloorChanged ? mappedFloors : config3.floors;
|
|
14082
|
-
const siteRes = applyContainerFormulas(config3.site, scope, warnings, "site");
|
|
14161
|
+
const siteRes = applyContainerFormulas(config3.site, scope, warnings, "site", fn);
|
|
14083
14162
|
const defaultsRes = applyContainerFormulas(
|
|
14084
14163
|
config3.defaults,
|
|
14085
14164
|
scope,
|
|
14086
14165
|
warnings,
|
|
14087
|
-
"defaults"
|
|
14166
|
+
"defaults",
|
|
14167
|
+
fn
|
|
14088
14168
|
);
|
|
14089
14169
|
const changed = anyFloorChanged || siteRes.changed || defaultsRes.changed;
|
|
14090
14170
|
if (!changed) return { config: config3, warnings };
|
|
@@ -29421,7 +29501,7 @@ function validate(data, opts) {
|
|
|
29421
29501
|
}))
|
|
29422
29502
|
};
|
|
29423
29503
|
}
|
|
29424
|
-
var side, positive, nonNegative, numOrFormula, formulaMap, enabledField, gridLine, gridDef, site, opening2, roomWallSide, wallHeightsEntry, itemAsset, itemAnchor, roomItem2, room2, wall2, staircase2, door, windowObj, roofV2, kitchenPlatform, componentObject, itemObject, vec3, rigOp2, modelObject, object4, REGISTERED_OBJECT_SCHEMAS, registeredObjectFallback, objectSchema, floor, componentParam, componentDef2, houseDefaults, houseUnits, layerDef, configuratorInput, configuratorSection, HouseConfig;
|
|
29504
|
+
var side, positive, nonNegative, numOrFormula, formulaMap, enabledField, gridLine, gridDef, generatedGuides, guidesDef, site, opening2, roomWallSide, wallHeightsEntry, itemAsset, itemAnchor, roomItem2, room2, wall2, staircase2, door, windowObj, roofV2, kitchenPlatform, componentObject, itemObject, vec3, rigOp2, modelObject, object4, REGISTERED_OBJECT_SCHEMAS, registeredObjectFallback, objectSchema, floor, componentParam, componentDef2, houseDefaults, houseUnits, layerDef, configuratorInput, configuratorSection, HouseConfig;
|
|
29425
29505
|
var init_houseConfig = __esm({
|
|
29426
29506
|
"../editor/src/schema/houseConfig.ts"() {
|
|
29427
29507
|
init_zod();
|
|
@@ -29444,6 +29524,14 @@ var init_houseConfig = __esm({
|
|
|
29444
29524
|
y: external_exports2.array(gridLine).min(2)
|
|
29445
29525
|
// horizontal centrelines (north → south)
|
|
29446
29526
|
}).strict();
|
|
29527
|
+
generatedGuides = external_exports2.object({
|
|
29528
|
+
origin: external_exports2.tuple([numOrFormula, numOrFormula]).optional(),
|
|
29529
|
+
// default (0, 0)
|
|
29530
|
+
spacing: external_exports2.tuple([numOrFormula, numOrFormula]),
|
|
29531
|
+
// (dx, dy)
|
|
29532
|
+
extent: external_exports2.tuple([external_exports2.number().int().positive(), external_exports2.number().int().positive()]).optional()
|
|
29533
|
+
}).strict();
|
|
29534
|
+
guidesDef = external_exports2.union([gridDef, generatedGuides]);
|
|
29447
29535
|
site = external_exports2.object({
|
|
29448
29536
|
reference_x: external_exports2.number(),
|
|
29449
29537
|
reference_y: external_exports2.number(),
|
|
@@ -29549,6 +29637,11 @@ var init_houseConfig = __esm({
|
|
|
29549
29637
|
// form treats 0 as "no override" and doesn't write it back.
|
|
29550
29638
|
height: nonNegative().optional(),
|
|
29551
29639
|
material: external_exports2.string().optional(),
|
|
29640
|
+
// Rooms this room connects to, by name (same floor). Design intent + a
|
|
29641
|
+
// functional test (constraint C11): a declared connection must be adjacent
|
|
29642
|
+
// AND joined by a door. Symmetric and deduped; NOT geometry — it never moves
|
|
29643
|
+
// or sizes anything, and the renderer ignores it.
|
|
29644
|
+
connections: external_exports2.array(external_exports2.string()).optional(),
|
|
29552
29645
|
// Vertical position of the room (its floor + walls), as a lift above the
|
|
29553
29646
|
// FLOOR BASE (slabZ = plinth top for floor 0, else the floor below's
|
|
29554
29647
|
// top; project units, 10 = 1 ft). This is the UNIFIED z_offset
|
|
@@ -29945,11 +30038,13 @@ var init_houseConfig = __esm({
|
|
|
29945
30038
|
// `component` object instantiates one by `ref`. Stored once; referenced by
|
|
29946
30039
|
// many instances; edit here to update every instance.
|
|
29947
30040
|
components: external_exports2.record(external_exports2.string(), componentDef2).optional(),
|
|
29948
|
-
// First-class parametric
|
|
29949
|
-
//
|
|
29950
|
-
//
|
|
29951
|
-
//
|
|
29952
|
-
|
|
30041
|
+
// First-class parametric GUIDES (plans/floor-planner-graph-integration.md).
|
|
30042
|
+
// Map of id → a named XOR generated guides object; objects place themselves by
|
|
30043
|
+
// referencing a guide in ordinary formulas (`main.x2`, `module.x8`). Optional;
|
|
30044
|
+
// reusable across templates. `grids` is the deprecated former name — still
|
|
30045
|
+
// read for backward-compat; the resolver merges both (guides wins on collision).
|
|
30046
|
+
guides: external_exports2.record(external_exports2.string(), guidesDef).optional(),
|
|
30047
|
+
grids: external_exports2.record(external_exports2.string(), guidesDef).optional(),
|
|
29953
30048
|
// Configurator metadata (Gharkul owner UI). Optional; see plans/configurator-plan.md.
|
|
29954
30049
|
configurator: configuratorSection.optional(),
|
|
29955
30050
|
// Preview snapshots (data: URLs) captured by the architect editor and saved
|
|
@@ -343269,7 +343364,7 @@ var require_detect_gpu_umd = __commonJS({
|
|
|
343269
343364
|
function e3(e4) {
|
|
343270
343365
|
var o2;
|
|
343271
343366
|
return n3(this, void 0, void 0, function() {
|
|
343272
|
-
var n4, a2, i5, c2, u2, l2, d3, v2, p2, g3, m2, b2, y2, x3, P2, A2,
|
|
343367
|
+
var n4, a2, i5, c2, u2, l2, d3, v2, p2, g3, m2, b2, y2, x3, P2, A2, C12, L2, M2, k2, j2, B2, R2, T2, U2, I2;
|
|
343273
343368
|
return t(this, function(t2) {
|
|
343274
343369
|
switch (t2.label) {
|
|
343275
343370
|
case 0:
|
|
@@ -343297,7 +343392,7 @@ var require_detect_gpu_umd = __commonJS({
|
|
|
343297
343392
|
return [e5, h(p2, e5[2])];
|
|
343298
343393
|
}).sort(function(e5, r5) {
|
|
343299
343394
|
return e5[1] - r5[1];
|
|
343300
|
-
})[0][0] : d3[0], m2 = g3[0], b2 = g3[4], y2 = Number.MAX_VALUE, P2 = window.devicePixelRatio, A2 = S.width * P2 * S.height * P2,
|
|
343395
|
+
})[0][0] : d3[0], m2 = g3[0], b2 = g3[4], y2 = Number.MAX_VALUE, P2 = window.devicePixelRatio, A2 = S.width * P2 * S.height * P2, C12 = 0, L2 = b2; C12 < L2.length; C12++) M2 = L2[C12], k2 = M2[0], j2 = M2[1], B2 = k2 * j2, (R2 = Math.abs(A2 - B2)) < y2 && (y2 = R2, x3 = M2);
|
|
343301
343396
|
return x3 ? (U2 = (T2 = x3)[2], I2 = T2[3], [2, [y2, U2, m2, I2]]) : [2];
|
|
343302
343397
|
}
|
|
343303
343398
|
});
|
|
@@ -354941,15 +355036,15 @@ var require_index_cjs3 = __commonJS({
|
|
|
354941
355036
|
}, [r3, z3]), S.createElement("primitive", M.default({ ref: d2, object: z3 }, u2));
|
|
354942
355037
|
});
|
|
354943
355038
|
var ft = S.forwardRef(({ children: e2, domElement: t2, onChange: r3, onMouseDown: n4, onMouseUp: o2, onObjectChange: i3, object: s2, makeDefault: l2, camera: u2, enabled: d2, axis: m2, mode: f3, translationSnap: p2, rotationSnap: h2, scaleSnap: x2, space: y2, size: v2, showX: g2, showY: w2, showZ: z3, ...b2 }, E2) => {
|
|
354944
|
-
const
|
|
355039
|
+
const C12 = a.useThree((e3) => e3.controls), P2 = a.useThree((e3) => e3.gl), R2 = a.useThree((e3) => e3.events), D2 = a.useThree((e3) => e3.camera), F2 = a.useThree((e3) => e3.invalidate), k2 = a.useThree((e3) => e3.get), _2 = a.useThree((e3) => e3.set), A2 = u2 || D2, L2 = t2 || R2.connected || P2.domElement, I2 = S.useMemo(() => new c.TransformControls(A2, L2), [A2, L2]), B2 = S.useRef(null);
|
|
354945
355040
|
S.useLayoutEffect(() => (s2 ? I2.attach(s2 instanceof T.Object3D ? s2 : s2.current) : B2.current instanceof T.Object3D && I2.attach(B2.current), () => {
|
|
354946
355041
|
I2.detach();
|
|
354947
355042
|
}), [s2, e2, I2]), S.useEffect(() => {
|
|
354948
|
-
if (
|
|
354949
|
-
const e3 = (e4) =>
|
|
355043
|
+
if (C12) {
|
|
355044
|
+
const e3 = (e4) => C12.enabled = !e4.value;
|
|
354950
355045
|
return I2.addEventListener("dragging-changed", e3), () => I2.removeEventListener("dragging-changed", e3);
|
|
354951
355046
|
}
|
|
354952
|
-
}, [I2,
|
|
355047
|
+
}, [I2, C12]);
|
|
354953
355048
|
const U2 = S.useRef(void 0), V3 = S.useRef(void 0), O2 = S.useRef(void 0), N2 = S.useRef(void 0);
|
|
354954
355049
|
return S.useLayoutEffect(() => {
|
|
354955
355050
|
U2.current = r3;
|
|
@@ -355019,7 +355114,7 @@ var require_index_cjs3 = __commonJS({
|
|
|
355019
355114
|
const e3 = { Box3: n3.Box3, MathUtils: { clamp: n3.MathUtils.clamp }, Matrix4: n3.Matrix4, Quaternion: n3.Quaternion, Raycaster: n3.Raycaster, Sphere: n3.Sphere, Spherical: n3.Spherical, Vector2: n3.Vector2, Vector3: n3.Vector3, Vector4: n3.Vector4 };
|
|
355020
355115
|
b2.install({ THREE: e3 }), a.extend({ CameraControlsImpl: b2 });
|
|
355021
355116
|
}, [b2]);
|
|
355022
|
-
const E2 = a.useThree((e3) => e3.camera),
|
|
355117
|
+
const E2 = a.useThree((e3) => e3.camera), C12 = a.useThree((e3) => e3.gl), T2 = a.useThree((e3) => e3.invalidate), R2 = a.useThree((e3) => e3.events), D2 = a.useThree((e3) => e3.setEvents), F2 = a.useThree((e3) => e3.set), k2 = a.useThree((e3) => e3.get), _2 = a.useThree((e3) => e3.performance), A2 = i3 || E2, L2 = s2 || R2.connected || C12.domElement, I2 = t.useMemo(() => new b2(A2), [b2, A2]);
|
|
355023
355118
|
return a.useFrame((e3, t2) => {
|
|
355024
355119
|
I2.update(t2);
|
|
355025
355120
|
}, -1), t.useEffect(() => (I2.connect(L2), () => {
|
|
@@ -355335,7 +355430,7 @@ var require_index_cjs3 = __commonJS({
|
|
|
355335
355430
|
for (const n4 in e3) r4[n4] = t2(e3[n4]);
|
|
355336
355431
|
return r4;
|
|
355337
355432
|
}
|
|
355338
|
-
}, []),
|
|
355433
|
+
}, []), C12 = S.useCallback(() => {
|
|
355339
355434
|
const e3 = {}, t2 = u2.current, r4 = p2.current;
|
|
355340
355435
|
if (t2) {
|
|
355341
355436
|
if (r4 && Array.isArray(t2.frames)) {
|
|
@@ -355386,12 +355481,12 @@ var require_index_cjs3 = __commonJS({
|
|
|
355386
355481
|
}
|
|
355387
355482
|
u2.current && u2.current.frames && (u2.current.frames = M2(u2.current.frames));
|
|
355388
355483
|
} else if (t2) {
|
|
355389
|
-
u2.current = e3, u2.current.frames =
|
|
355484
|
+
u2.current = e3, u2.current.frames = C12(), d2.current = Array.isArray(e3.frames) ? e3.frames.length : Object.keys(e3.frames).length;
|
|
355390
355485
|
const { w: t3, h: n4 } = or(e3.frames).sourceSize;
|
|
355391
355486
|
r4 = b2(t3, n4, 0.1);
|
|
355392
355487
|
}
|
|
355393
355488
|
x2(u2.current), "encoding" in t2 ? t2.encoding = 3001 : "colorSpace" in t2 && (t2.colorSpace = T.SRGBColorSpace), v2(t2), z3({ spriteTexture: t2, spriteData: u2.current, aspect: r4 });
|
|
355394
|
-
}, [E2, i3,
|
|
355489
|
+
}, [E2, i3, C12, b2, M2]), R2 = S.useCallback((e3, t2, r4) => {
|
|
355395
355490
|
const n4 = fetch(e3).then((e4) => e4.json()), a2 = new Promise((e4) => {
|
|
355396
355491
|
g2.load(t2, e4);
|
|
355397
355492
|
});
|
|
@@ -355664,7 +355759,7 @@ var require_index_cjs3 = __commonJS({
|
|
|
355664
355759
|
});
|
|
355665
355760
|
var Or = S.createContext(null);
|
|
355666
355761
|
var Nr = new T.PlaneGeometry(1, 1);
|
|
355667
|
-
var jr = S.forwardRef(({ startFrame: e2 = 0, endFrame: t2, fps: r3 = 30, frameName: n4 = "", textureDataURL: o2, textureImageURL: i3, loop: s2 = false, numberOfFrames: l2 = 1, autoPlay: c2 = true, animationNames: u2, onStart: d2, onEnd: m2, onLoopEnd: f3, onFrame: p2, play: h2, pause: x2 = false, flipX: y2 = false, alphaTest: v2 = 0, children: g2, asSprite: w2 = false, offset: z3, playBackwards: b2 = false, resetOnEnd: E2 = false, maxItems:
|
|
355762
|
+
var jr = S.forwardRef(({ startFrame: e2 = 0, endFrame: t2, fps: r3 = 30, frameName: n4 = "", textureDataURL: o2, textureImageURL: i3, loop: s2 = false, numberOfFrames: l2 = 1, autoPlay: c2 = true, animationNames: u2, onStart: d2, onEnd: m2, onLoopEnd: f3, onFrame: p2, play: h2, pause: x2 = false, flipX: y2 = false, alphaTest: v2 = 0, children: g2, asSprite: w2 = false, offset: z3, playBackwards: b2 = false, resetOnEnd: E2 = false, maxItems: C12 = 1, instanceItems: P2 = [[0, 0, 0]], spriteDataset: R2, canvasRenderingContext2DSettings: D2, roundFramePosition: F2 = false, meshProps: k2 = {}, ..._2 }, A2) => {
|
|
355668
355763
|
const L2 = S.useRef(new T.Group()), I2 = S.useRef(null), B2 = S.useRef(null), U2 = S.useRef(null), V3 = S.useRef(window.performance.now()), O2 = S.useRef(e2), N2 = S.useRef(n4), j2 = r3 > 0 ? 1e3 / r3 : 0, [W2, G2] = S.useState(new T.Texture()), H2 = S.useRef(0), [$2, q2] = S.useState(new T.Vector3(1, 1, 1)), X2 = y2 ? -1 : 1, Z2 = S.useRef(x2), Y2 = S.useRef(z3), Q2 = S.useRef(false), { spriteObj: K2, loadJsonAndTexture: J2 } = sr(null, null, u2, l2, void 0, D2), te2 = S.useRef(n4), re2 = S.useCallback((e3, t3) => {
|
|
355669
355764
|
if (null === t3) l2 && (H2.current = l2, b2 && (O2.current = l2 - 1), I2.current = t3);
|
|
355670
355765
|
else {
|
|
@@ -355740,7 +355835,7 @@ var require_index_cjs3 = __commonJS({
|
|
|
355740
355835
|
})(), null == p2 || p2({ currentFrameName: N2.current, currentFrame: O2.current })));
|
|
355741
355836
|
}), S.createElement("group", M.default({}, _2, { ref: L2, scale: function(e3 = new T.Vector3(1, 1, 1), t3 = 1) {
|
|
355742
355837
|
return "number" == typeof t3 ? e3.multiplyScalar(t3) : Array.isArray(t3) ? e3.multiply(new T.Vector3(...t3)) : t3 instanceof T.Vector3 ? e3.multiply(t3) : void 0;
|
|
355743
|
-
}($2, _2.scale) }), S.createElement(Or.Provider, { value: ae2 }, w2 && S.createElement(ee, null, S.createElement("mesh", M.default({ ref: U2, scale: 1, geometry: Nr }, k2), S.createElement("meshBasicMaterial", { premultipliedAlpha: false, toneMapped: false, side: T.DoubleSide, ref: B2, map: W2, transparent: true, alphaTest: null != v2 ? v2 : 0 }))), !w2 && S.createElement(Ir, M.default({ geometry: Nr, limit: null !=
|
|
355838
|
+
}($2, _2.scale) }), S.createElement(Or.Provider, { value: ae2 }, w2 && S.createElement(ee, null, S.createElement("mesh", M.default({ ref: U2, scale: 1, geometry: Nr }, k2), S.createElement("meshBasicMaterial", { premultipliedAlpha: false, toneMapped: false, side: T.DoubleSide, ref: B2, map: W2, transparent: true, alphaTest: null != v2 ? v2 : 0 }))), !w2 && S.createElement(Ir, M.default({ geometry: Nr, limit: null != C12 ? C12 : 1 }, k2), S.createElement("meshBasicMaterial", { premultipliedAlpha: false, toneMapped: false, side: T.DoubleSide, ref: B2, map: W2, transparent: true, alphaTest: null != v2 ? v2 : 0 }), (null != P2 ? P2 : [0]).map((e3, t3) => S.createElement(Lr, M.default({ key: t3, ref: 1 === (null == P2 ? void 0 : P2.length) ? U2 : null, position: e3, scale: 1 }, k2)))), g2));
|
|
355744
355839
|
});
|
|
355745
355840
|
var Wr = S.forwardRef(({ children: e2, curve: t2 }, r3) => {
|
|
355746
355841
|
const [n4] = S.useState(() => new T.Scene()), [o2, i3] = S.useState(), s2 = S.useRef(null);
|
|
@@ -356002,12 +356097,12 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
356002
356097
|
a.extend({ MeshReflectorMaterialImpl: Yr });
|
|
356003
356098
|
const y2 = a.useThree(({ gl: e3 }) => e3), v2 = a.useThree(({ camera: e3 }) => e3), g2 = a.useThree(({ scene: e3 }) => e3), w2 = (o2 = Array.isArray(o2) ? o2 : [o2, o2])[0] + o2[1] > 0, z3 = o2[0], b2 = o2[1], E2 = S.useRef(null);
|
|
356004
356099
|
S.useImperativeHandle(x2, () => E2.current, []);
|
|
356005
|
-
const [
|
|
356100
|
+
const [C12] = S.useState(() => new n3.Plane()), [T2] = S.useState(() => new n3.Vector3()), [P2] = S.useState(() => new n3.Vector3()), [R2] = S.useState(() => new n3.Vector3()), [D2] = S.useState(() => new n3.Matrix4()), [F2] = S.useState(() => new n3.Vector3(0, 0, -1)), [k2] = S.useState(() => new n3.Vector4()), [_2] = S.useState(() => new n3.Vector3()), [A2] = S.useState(() => new n3.Vector3()), [L2] = S.useState(() => new n3.Vector4()), [I2] = S.useState(() => new n3.Matrix4()), [B2] = S.useState(() => new n3.PerspectiveCamera()), U2 = S.useCallback(() => {
|
|
356006
356101
|
var e3;
|
|
356007
356102
|
const t3 = E2.current.parent || (null == (e3 = E2.current) || null == (e3 = e3.__r3f.parent) ? void 0 : e3.object);
|
|
356008
356103
|
if (!t3) return;
|
|
356009
356104
|
if (P2.setFromMatrixPosition(t3.matrixWorld), R2.setFromMatrixPosition(v2.matrixWorld), D2.extractRotation(t3.matrixWorld), T2.set(0, 0, 1), T2.applyMatrix4(D2), P2.addScaledVector(T2, p2), _2.subVectors(P2, R2), _2.dot(T2) > 0) return;
|
|
356010
|
-
_2.reflect(T2).negate(), _2.add(P2), D2.extractRotation(v2.matrixWorld), F2.set(0, 0, -1), F2.applyMatrix4(D2), F2.add(R2), A2.subVectors(P2, F2), A2.reflect(T2).negate(), A2.add(P2), B2.position.copy(_2), B2.up.set(0, 1, 0), B2.up.applyMatrix4(D2), B2.up.reflect(T2), B2.lookAt(A2), B2.far = v2.far, B2.updateMatrixWorld(), B2.projectionMatrix.copy(v2.projectionMatrix), I2.set(0.5, 0, 0, 0.5, 0, 0.5, 0, 0.5, 0, 0, 0.5, 0.5, 0, 0, 0, 1), I2.multiply(B2.projectionMatrix), I2.multiply(B2.matrixWorldInverse), I2.multiply(t3.matrixWorld),
|
|
356105
|
+
_2.reflect(T2).negate(), _2.add(P2), D2.extractRotation(v2.matrixWorld), F2.set(0, 0, -1), F2.applyMatrix4(D2), F2.add(R2), A2.subVectors(P2, F2), A2.reflect(T2).negate(), A2.add(P2), B2.position.copy(_2), B2.up.set(0, 1, 0), B2.up.applyMatrix4(D2), B2.up.reflect(T2), B2.lookAt(A2), B2.far = v2.far, B2.updateMatrixWorld(), B2.projectionMatrix.copy(v2.projectionMatrix), I2.set(0.5, 0, 0, 0.5, 0, 0.5, 0, 0.5, 0, 0, 0.5, 0.5, 0, 0, 0, 1), I2.multiply(B2.projectionMatrix), I2.multiply(B2.matrixWorldInverse), I2.multiply(t3.matrixWorld), C12.setFromNormalAndCoplanarPoint(T2, P2), C12.applyMatrix4(B2.matrixWorldInverse), k2.set(C12.normal.x, C12.normal.y, C12.normal.z, C12.constant);
|
|
356011
356106
|
const r4 = B2.projectionMatrix;
|
|
356012
356107
|
L2.x = (Math.sign(k2.x) + r4.elements[8]) / r4.elements[0], L2.y = (Math.sign(k2.y) + r4.elements[9]) / r4.elements[5], L2.z = -1, L2.w = (1 + r4.elements[10]) / r4.elements[14], k2.multiplyScalar(2 / k2.dot(L2)), r4.elements[2] = k2.x, r4.elements[6] = k2.y, r4.elements[10] = k2.z + 1, r4.elements[14] = k2.w;
|
|
356013
356108
|
}, [v2, p2]), [V3, O2, N2, j2] = S.useMemo(() => {
|
|
@@ -356200,10 +356295,10 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
356200
356295
|
var tn = S.forwardRef(({ buffer: e2, transmissionSampler: t2 = false, backside: r3 = false, side: n4 = T.FrontSide, transmission: o2 = 1, thickness: i3 = 0, backsideThickness: s2 = 0, backsideEnvMapIntensity: l2 = 1, samples: c2 = 10, resolution: u2, backsideResolution: d2, background: m2, anisotropy: f3, anisotropicBlur: p2, ...h2 }, x2) => {
|
|
356201
356296
|
a.extend({ MeshTransmissionMaterial: en });
|
|
356202
356297
|
const y2 = S.useRef(null), [v2] = S.useState(() => new Jr()), g2 = rt(d2 || u2), w2 = rt(u2);
|
|
356203
|
-
let z3, b2, E2,
|
|
356298
|
+
let z3, b2, E2, C12;
|
|
356204
356299
|
return a.useFrame((e3) => {
|
|
356205
356300
|
var a2;
|
|
356206
|
-
(y2.current.time = e3.clock.elapsedTime, y2.current.buffer !== w2.texture || t2) || (
|
|
356301
|
+
(y2.current.time = e3.clock.elapsedTime, y2.current.buffer !== w2.texture || t2) || (C12 = null == (a2 = y2.current.__r3f.parent) ? void 0 : a2.object, C12 && (E2 = e3.gl.toneMapping, z3 = e3.scene.background, b2 = y2.current.envMapIntensity, e3.gl.toneMapping = T.NoToneMapping, m2 && (e3.scene.background = m2), C12.material = v2, r3 && (e3.gl.setRenderTarget(g2), e3.gl.render(e3.scene, e3.camera), C12.material = y2.current, C12.material.buffer = g2.texture, C12.material.thickness = s2, C12.material.side = T.BackSide, C12.material.envMapIntensity = l2), e3.gl.setRenderTarget(w2), e3.gl.render(e3.scene, e3.camera), C12.material = y2.current, C12.material.thickness = i3, C12.material.side = n4, C12.material.buffer = w2.texture, C12.material.envMapIntensity = b2, e3.scene.background = z3, e3.gl.setRenderTarget(null), e3.gl.toneMapping = E2));
|
|
356207
356302
|
}), S.useImperativeHandle(x2, () => y2.current, []), S.createElement("meshTransmissionMaterial", M.default({ args: [c2, t2], ref: y2 }, h2, { buffer: e2 || w2.texture, _transmission: o2, anisotropicBlur: null != p2 ? p2 : f3, transmission: t2 ? o2 : 0, thickness: i3, side: n4 }));
|
|
356208
356303
|
});
|
|
356209
356304
|
var rn = S.forwardRef((e2, t2) => (a.extend({ DiscardMaterialImpl: Jr }), S.createElement("discardMaterialImpl", M.default({ ref: t2 }, e2))));
|
|
@@ -356512,11 +356607,11 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
356512
356607
|
}
|
|
356513
356608
|
return Kn(l2, h2, z3, M2.texture, { backgroundBlurriness: null != c2 ? c2 : u2, backgroundIntensity: d2, backgroundRotation: m2, environmentIntensity: f3, environmentRotation: p2 });
|
|
356514
356609
|
}, [e2, E2, M2.texture, h2, z3, l2, i3, w2]);
|
|
356515
|
-
let
|
|
356610
|
+
let C12 = 1;
|
|
356516
356611
|
return a.useFrame(() => {
|
|
356517
|
-
if (i3 === 1 / 0 ||
|
|
356612
|
+
if (i3 === 1 / 0 || C12 < i3) {
|
|
356518
356613
|
const e3 = w2.autoClear;
|
|
356519
|
-
w2.autoClear = true, b2.current.update(w2, E2), w2.autoClear = e3,
|
|
356614
|
+
w2.autoClear = true, b2.current.update(w2, E2), w2.autoClear = e3, C12++;
|
|
356520
356615
|
}
|
|
356521
356616
|
}), S.createElement(S.Fragment, null, a.createPortal(S.createElement(S.Fragment, null, e2, S.createElement("cubeCamera", { ref: b2, args: [t2, r3, M2] }), x2 || v2 ? S.createElement(ea, { background: true, files: x2, preset: v2, path: y2, extensions: g2 }) : s2 ? S.createElement(Jn, { background: true, map: s2, extensions: g2 }) : null), E2));
|
|
356522
356617
|
}
|
|
@@ -356535,7 +356630,7 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
356535
356630
|
var aa = S.forwardRef(({ scale: e2 = 10, frames: t2 = 1 / 0, opacity: r3 = 1, width: n4 = 1, height: o2 = 1, blur: i3 = 1, near: s2 = 0, far: l2 = 10, resolution: u2 = 512, smooth: d2 = true, color: m2 = "#000000", depthWrite: f3 = false, renderOrder: p2, ...h2 }, x2) => {
|
|
356536
356631
|
const y2 = S.useRef(null), v2 = a.useThree((e3) => e3.scene), g2 = a.useThree((e3) => e3.gl), w2 = S.useRef(null);
|
|
356537
356632
|
n4 *= Array.isArray(e2) ? e2[0] : e2 || 1, o2 *= Array.isArray(e2) ? e2[1] : e2 || 1;
|
|
356538
|
-
const [z3, b2, E2,
|
|
356633
|
+
const [z3, b2, E2, C12, P2, R2, D2] = S.useMemo(() => {
|
|
356539
356634
|
const e3 = new T.WebGLRenderTarget(u2, u2), t3 = new T.WebGLRenderTarget(u2, u2);
|
|
356540
356635
|
t3.texture.generateMipmaps = e3.texture.generateMipmaps = false;
|
|
356541
356636
|
const r4 = new T.PlaneGeometry(n4, o2).rotateX(Math.PI / 2), a2 = new T.Mesh(r4), i4 = new T.MeshDepthMaterial();
|
|
@@ -356545,7 +356640,7 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
356545
356640
|
const s3 = new T.ShaderMaterial(c.HorizontalBlurShader), l3 = new T.ShaderMaterial(c.VerticalBlurShader);
|
|
356546
356641
|
return l3.depthTest = s3.depthTest = false, [e3, r4, i4, a2, s3, l3, t3];
|
|
356547
356642
|
}, [u2, n4, o2, e2, m2]), F2 = (e3) => {
|
|
356548
|
-
|
|
356643
|
+
C12.visible = true, C12.material = P2, P2.uniforms.tDiffuse.value = z3.texture, P2.uniforms.h.value = 1 * e3 / 256, g2.setRenderTarget(D2), g2.render(C12, w2.current), C12.material = R2, R2.uniforms.tDiffuse.value = D2.texture, R2.uniforms.v.value = 1 * e3 / 256, g2.setRenderTarget(z3), g2.render(C12, w2.current), C12.visible = false;
|
|
356549
356644
|
};
|
|
356550
356645
|
let k2, _2, A2 = 0;
|
|
356551
356646
|
return a.useFrame(() => {
|
|
@@ -356685,7 +356780,7 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
356685
356780
|
var va = { minFilter: T.LinearMipmapLinearFilter, magFilter: T.LinearFilter, type: T.FloatType, generateMipmaps: true };
|
|
356686
356781
|
var ga = S.forwardRef(({ debug: e2, children: t2, frames: r3 = 1, ior: n4 = 1.1, color: o2 = "white", causticsOnly: i3 = false, backside: s2 = false, backsideIOR: l2 = 1.1, worldRadius: u2 = 0.3125, intensity: d2 = 0.05, resolution: m2 = 2024, lightSource: f3 = [5, 5, 5], ...p2 }, h2) => {
|
|
356687
356782
|
a.extend({ CausticsProjectionMaterial: ha });
|
|
356688
|
-
const x2 = S.useRef(null), y2 = S.useRef(null), v2 = S.useRef(null), g2 = S.useRef(null), w2 = a.useThree((e3) => e3.gl), z3 = lr(e2 && y2, T.CameraHelper), b2 = rt(m2, m2, ya), E2 = rt(m2, m2, ya),
|
|
356783
|
+
const x2 = S.useRef(null), y2 = S.useRef(null), v2 = S.useRef(null), g2 = S.useRef(null), w2 = a.useThree((e3) => e3.gl), z3 = lr(e2 && y2, T.CameraHelper), b2 = rt(m2, m2, ya), E2 = rt(m2, m2, ya), C12 = rt(m2, m2, va), P2 = rt(m2, m2, va), [R2] = S.useState(() => pa()), [D2] = S.useState(() => pa(T.BackSide)), [F2] = S.useState(() => new xa()), [k2] = S.useState(() => new c.FullScreenQuad(F2));
|
|
356689
356784
|
S.useLayoutEffect(() => {
|
|
356690
356785
|
x2.current.updateWorldMatrix(false, true);
|
|
356691
356786
|
});
|
|
@@ -356709,9 +356804,9 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
356709
356804
|
const T2 = W2.map((e3, t4) => e3.add(H2[t4].copy(U2).multiplyScalar(-e3.y / U2.y))), _3 = T2.reduce((e3, t4) => e3.add(t4), A2.set(0, 0, 0)).divideScalar(T2.length), q2 = 2 * T2.map((e3) => Math.hypot(e3.x - _3.x, e3.z - _3.z)).reduce((e3, t4) => Math.max(e3, t4));
|
|
356710
356805
|
g2.current.scale.setScalar(q2), g2.current.position.copy(_3), e2 && (null == (a2 = z3.current) || a2.update()), D2.viewMatrix.value = R2.viewMatrix.value = y2.current.matrixWorldInverse;
|
|
356711
356806
|
const X2 = L2.setFromProjectionMatrix(I2.multiplyMatrices(y2.current.projectionMatrix, y2.current.matrixWorldInverse)).planes[4];
|
|
356712
|
-
F2.cameraMatrixWorld = y2.current.matrixWorld, F2.cameraProjectionMatrixInv = y2.current.projectionMatrixInverse, F2.lightDir = V3, F2.lightPlaneNormal = X2.normal, F2.lightPlaneConstant = X2.constant, F2.near = y2.current.near, F2.far = y2.current.far, F2.resolution = m2, F2.size = c2, F2.intensity = d2, F2.worldRadius = u2, v2.current.visible = true, w2.setRenderTarget(b2), w2.clear(), v2.current.overrideMaterial = R2, w2.render(v2.current, y2.current), w2.setRenderTarget(E2), w2.clear(), s2 && (v2.current.overrideMaterial = D2, w2.render(v2.current, y2.current)), v2.current.overrideMaterial = null, F2.ior = n4, g2.current.material.lightProjMatrix = y2.current.projectionMatrix, g2.current.material.lightViewMatrix = y2.current.matrixWorldInverse, F2.normalTexture = b2.texture, F2.depthTexture = b2.depthTexture, w2.setRenderTarget(
|
|
356807
|
+
F2.cameraMatrixWorld = y2.current.matrixWorld, F2.cameraProjectionMatrixInv = y2.current.projectionMatrixInverse, F2.lightDir = V3, F2.lightPlaneNormal = X2.normal, F2.lightPlaneConstant = X2.constant, F2.near = y2.current.near, F2.far = y2.current.far, F2.resolution = m2, F2.size = c2, F2.intensity = d2, F2.worldRadius = u2, v2.current.visible = true, w2.setRenderTarget(b2), w2.clear(), v2.current.overrideMaterial = R2, w2.render(v2.current, y2.current), w2.setRenderTarget(E2), w2.clear(), s2 && (v2.current.overrideMaterial = D2, w2.render(v2.current, y2.current)), v2.current.overrideMaterial = null, F2.ior = n4, g2.current.material.lightProjMatrix = y2.current.projectionMatrix, g2.current.material.lightViewMatrix = y2.current.matrixWorldInverse, F2.normalTexture = b2.texture, F2.depthTexture = b2.depthTexture, w2.setRenderTarget(C12), w2.clear(), k2.render(w2), F2.ior = l2, F2.normalTexture = E2.texture, F2.depthTexture = E2.depthTexture, w2.setRenderTarget(P2), w2.clear(), s2 && k2.render(w2), w2.setRenderTarget(null), i3 && (v2.current.visible = false);
|
|
356713
356808
|
}
|
|
356714
|
-
}), S.useImperativeHandle(h2, () => x2.current, []), S.createElement("group", M.default({ ref: x2 }, p2), S.createElement("scene", { ref: v2 }, S.createElement("orthographicCamera", { ref: y2, up: [0, 1, 0] }), t2), S.createElement("mesh", { renderOrder: 2, ref: g2, "rotation-x": -Math.PI / 2 }, S.createElement("planeGeometry", null), S.createElement("causticsProjectionMaterial", { transparent: true, color: o2, causticsTexture:
|
|
356809
|
+
}), S.useImperativeHandle(h2, () => x2.current, []), S.createElement("group", M.default({ ref: x2 }, p2), S.createElement("scene", { ref: v2 }, S.createElement("orthographicCamera", { ref: y2, up: [0, 1, 0] }), t2), S.createElement("mesh", { renderOrder: 2, ref: g2, "rotation-x": -Math.PI / 2 }, S.createElement("planeGeometry", null), S.createElement("causticsProjectionMaterial", { transparent: true, color: o2, causticsTexture: C12.texture, causticsTextureB: P2.texture, blending: T.CustomBlending, blendSrc: T.OneFactor, blendDst: T.SrcAlphaFactor, depthWrite: false }), e2 && S.createElement(ke, null, S.createElement("lineBasicMaterial", { color: "#ffff00", toneMapped: false }))));
|
|
356715
356810
|
});
|
|
356716
356811
|
var wa = class extends T.ShaderMaterial {
|
|
356717
356812
|
constructor() {
|
|
@@ -356893,9 +356988,9 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
356893
356988
|
const e3 = Math.min(i3, void 0 !== o2 ? o2 : i3, p2.current.length);
|
|
356894
356989
|
f3.current.count = e3, br(f3.current.instanceMatrix, { start: 0, count: 16 * e3 }), f3.current.instanceColor && br(f3.current.instanceColor, { start: 0, count: 3 * e3 }), br(f3.current.geometry.attributes.cloudOpacity, { start: 0, count: e3 });
|
|
356895
356990
|
});
|
|
356896
|
-
let
|
|
356897
|
-
const T2 = Math.max(
|
|
356898
|
-
return
|
|
356991
|
+
let C12 = [null !== (u2 = y2.image.width) && void 0 !== u2 ? u2 : 1, null !== (d2 = y2.image.height) && void 0 !== d2 ? d2 : 1];
|
|
356992
|
+
const T2 = Math.max(C12[0], C12[1]);
|
|
356993
|
+
return C12 = [C12[0] / T2, C12[1] / T2], S.createElement("group", M.default({ ref: c2 }, l2), S.createElement(Ua.Provider, { value: p2 }, e2, S.createElement("instancedMesh", { matrixAutoUpdate: false, ref: f3, args: [null, null, i3], frustumCulled: s2 }, S.createElement("instancedBufferAttribute", { usage: n3.DynamicDrawUsage, attach: "instanceColor", args: [x2, 3] }), S.createElement("planeGeometry", { args: [...C12] }, S.createElement("instancedBufferAttribute", { usage: n3.DynamicDrawUsage, attach: "attributes-cloudOpacity", args: [h2, 1] })), S.createElement("cloudMaterial", { key: t2.name, map: y2, transparent: true, depthWrite: false }))));
|
|
356899
356994
|
});
|
|
356900
356995
|
var Oa = S.forwardRef(({ opacity: e2 = 1, speed: t2 = 0, bounds: r3 = [5, 1, 1], segments: o2 = 20, color: i3 = "#ffffff", fade: s2 = 10, volume: l2 = 6, smallestVolume: c2 = 0.25, distribute: u2 = null, growth: d2 = 4, concentrate: m2 = "inside", seed: f3 = Math.random(), ...p2 }, h2) => {
|
|
356901
356996
|
function x2() {
|
|
@@ -357248,13 +357343,13 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357248
357343
|
}, t2), S.createElement(S.Fragment, null, r3);
|
|
357249
357344
|
}
|
|
357250
357345
|
var ko = S.forwardRef(({ children: e2, compute: t2, renderPriority: r3 = -1, eventPriority: n4 = 0, frames: o2 = 1 / 0, stencilBuffer: i3 = false, depthBuffer: s2 = true, generateMipmaps: l2 = false, resolution: c2 = 896, near: u2 = 0.1, far: d2 = 1e3, flip: m2 = false, position: f3, rotation: p2, scale: h2, quaternion: x2, matrix: y2, matrixAutoUpdate: v2, ...g2 }, w2) => {
|
|
357251
|
-
const { size: z3, viewport: b2 } = a.useThree(), E2 = S.useRef(null),
|
|
357346
|
+
const { size: z3, viewport: b2 } = a.useThree(), E2 = S.useRef(null), C12 = S.useMemo(() => {
|
|
357252
357347
|
const e3 = new T.WebGLCubeRenderTarget(Math.max((c2 || z3.width) * b2.dpr, (c2 || z3.height) * b2.dpr), { stencilBuffer: i3, depthBuffer: s2, generateMipmaps: l2 });
|
|
357253
357348
|
return e3.texture.isRenderTargetTexture = !m2, e3.texture.flipY = true, e3.texture.type = T.HalfFloatType, e3;
|
|
357254
357349
|
}, [c2, m2]);
|
|
357255
|
-
S.useEffect(() => () =>
|
|
357350
|
+
S.useEffect(() => () => C12.dispose(), [C12]);
|
|
357256
357351
|
const [P2] = S.useState(() => new T.Scene());
|
|
357257
|
-
return S.useImperativeHandle(w2, () => ({ scene: P2, fbo:
|
|
357352
|
+
return S.useImperativeHandle(w2, () => ({ scene: P2, fbo: C12, camera: E2.current }), [C12]), S.createElement(S.Fragment, null, a.createPortal(S.createElement(_o, { renderPriority: r3, frames: o2, camera: E2 }, e2, S.createElement("group", { onPointerOver: () => null })), P2, { events: { compute: t2, priority: n4 } }), S.createElement("primitive", M.default({ object: C12.texture }, g2)), S.createElement("cubeCamera", { ref: E2, args: [u2, d2, C12], position: f3, rotation: p2, scale: h2, quaternion: x2, matrix: y2, matrixAutoUpdate: v2 }));
|
|
357258
357353
|
});
|
|
357259
357354
|
function _o({ frames: e2, renderPriority: t2, children: r3, camera: n4 }) {
|
|
357260
357355
|
let o2 = 0;
|
|
@@ -357479,12 +357574,12 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357479
357574
|
var Jo = new T.Vector3(0, 1, 0);
|
|
357480
357575
|
var ei = new T.Matrix4();
|
|
357481
357576
|
var ti = ({ direction: e2, axis: t2 }) => {
|
|
357482
|
-
const { translation: r3, translationLimits: n4, annotations: o2, annotationsClass: i3, depthTest: s2, scale: l2, lineWidth: c2, fixed: u2, axisColors: d2, hoveredColor: m2, opacity: f3, renderOrder: p2, onDragStart: h2, onDrag: x2, onDragEnd: y2, userData: v2 } = S.useContext(Yo), g2 = a.useThree((e3) => e3.controls), w2 = S.useRef(null), z3 = S.useRef(null), b2 = S.useRef(null), E2 = S.useRef(0), [M2,
|
|
357577
|
+
const { translation: r3, translationLimits: n4, annotations: o2, annotationsClass: i3, depthTest: s2, scale: l2, lineWidth: c2, fixed: u2, axisColors: d2, hoveredColor: m2, opacity: f3, renderOrder: p2, onDragStart: h2, onDrag: x2, onDragEnd: y2, userData: v2 } = S.useContext(Yo), g2 = a.useThree((e3) => e3.controls), w2 = S.useRef(null), z3 = S.useRef(null), b2 = S.useRef(null), E2 = S.useRef(0), [M2, C12] = S.useState(false), P2 = S.useCallback((n5) => {
|
|
357483
357578
|
o2 && (w2.current.innerText = `${r3.current[t2].toFixed(2)}`, w2.current.style.display = "block"), n5.stopPropagation();
|
|
357484
357579
|
const a2 = new T.Matrix4().extractRotation(z3.current.matrixWorld), i4 = n5.point.clone(), s3 = new T.Vector3().setFromMatrixPosition(z3.current.matrixWorld), l3 = e2.clone().applyMatrix4(a2).normalize();
|
|
357485
357580
|
b2.current = { clickPoint: i4, dir: l3 }, E2.current = r3.current[t2], h2({ component: "Arrow", axis: t2, origin: s3, directions: [l3] }), g2 && (g2.enabled = false), n5.target.setPointerCapture(n5.pointerId);
|
|
357486
357581
|
}, [o2, e2, g2, h2, r3, t2]), R2 = S.useCallback((e3) => {
|
|
357487
|
-
if (e3.stopPropagation(), M2 ||
|
|
357582
|
+
if (e3.stopPropagation(), M2 || C12(true), b2.current) {
|
|
357488
357583
|
const { clickPoint: a2, dir: i4 } = b2.current, [s3, l3] = (null == n4 ? void 0 : n4[t2]) || [void 0, void 0];
|
|
357489
357584
|
let c3 = ((e4, t3, r4, n5) => {
|
|
357490
357585
|
const a3 = t3.dot(t3), o3 = t3.dot(e4) - t3.dot(r4), i5 = t3.dot(n5);
|
|
@@ -357495,7 +357590,7 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357495
357590
|
}, [o2, x2, M2, r3, n4, t2]), D2 = S.useCallback((e3) => {
|
|
357496
357591
|
o2 && (w2.current.style.display = "none"), e3.stopPropagation(), b2.current = null, y2(), g2 && (g2.enabled = true), e3.target.releasePointerCapture(e3.pointerId);
|
|
357497
357592
|
}, [o2, g2, y2]), F2 = S.useCallback((e3) => {
|
|
357498
|
-
e3.stopPropagation(),
|
|
357593
|
+
e3.stopPropagation(), C12(false);
|
|
357499
357594
|
}, []), { cylinderLength: k2, coneWidth: _2, coneLength: A2, matrixL: L2 } = S.useMemo(() => {
|
|
357500
357595
|
const t3 = u2 ? c2 / l2 * 1.6 : l2 / 20, r4 = u2 ? 0.2 : l2 / 5, n5 = u2 ? 1 - r4 : l2 - r4, a2 = new T.Quaternion().setFromUnitVectors(Jo, e2.clone().normalize());
|
|
357501
357596
|
return { cylinderLength: n5, coneWidth: t3, coneLength: r4, matrixL: new T.Matrix4().makeRotationFromQuaternion(a2) };
|
|
@@ -357517,12 +357612,12 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357517
357612
|
var li = new T.Ray();
|
|
357518
357613
|
var ci = new T.Vector3();
|
|
357519
357614
|
var ui = ({ dir1: e2, dir2: t2, axis: r3 }) => {
|
|
357520
|
-
const { rotationLimits: n4, annotations: o2, annotationsClass: i3, depthTest: s2, scale: l2, lineWidth: c2, fixed: u2, axisColors: d2, hoveredColor: m2, renderOrder: f3, opacity: p2, onDragStart: h2, onDrag: x2, onDragEnd: y2, userData: v2 } = S.useContext(Yo), g2 = a.useThree((e3) => e3.controls), w2 = S.useRef(null), z3 = S.useRef(null), b2 = S.useRef(0), E2 = S.useRef(0), M2 = S.useRef(null), [
|
|
357615
|
+
const { rotationLimits: n4, annotations: o2, annotationsClass: i3, depthTest: s2, scale: l2, lineWidth: c2, fixed: u2, axisColors: d2, hoveredColor: m2, renderOrder: f3, opacity: p2, onDragStart: h2, onDrag: x2, onDragEnd: y2, userData: v2 } = S.useContext(Yo), g2 = a.useThree((e3) => e3.controls), w2 = S.useRef(null), z3 = S.useRef(null), b2 = S.useRef(0), E2 = S.useRef(0), M2 = S.useRef(null), [C12, P2] = S.useState(false), R2 = S.useCallback((e3) => {
|
|
357521
357616
|
o2 && (w2.current.innerText = `${ai(E2.current).toFixed(0)}\xBA`, w2.current.style.display = "block"), e3.stopPropagation();
|
|
357522
357617
|
const t3 = e3.point.clone(), n5 = new T.Vector3().setFromMatrixPosition(z3.current.matrixWorld), a2 = new T.Vector3().setFromMatrixColumn(z3.current.matrixWorld, 0).normalize(), i4 = new T.Vector3().setFromMatrixColumn(z3.current.matrixWorld, 1).normalize(), s3 = new T.Vector3().setFromMatrixColumn(z3.current.matrixWorld, 2).normalize(), l3 = new T.Plane().setFromNormalAndCoplanarPoint(s3, n5);
|
|
357523
357618
|
M2.current = { clickPoint: t3, origin: n5, e1: a2, e2: i4, normal: s3, plane: l3 }, h2({ component: "Rotator", axis: r3, origin: n5, directions: [a2, i4, s3] }), g2 && (g2.enabled = false), e3.target.setPointerCapture(e3.pointerId);
|
|
357524
357619
|
}, [o2, g2, h2, r3]), D2 = S.useCallback((e3) => {
|
|
357525
|
-
if (e3.stopPropagation(),
|
|
357620
|
+
if (e3.stopPropagation(), C12 || P2(true), M2.current) {
|
|
357526
357621
|
const { clickPoint: t3, origin: a2, e1: i4, e2: s3, normal: l3, plane: c3 } = M2.current, [u3, d3] = (null == n4 ? void 0 : n4[r3]) || [void 0, void 0];
|
|
357527
357622
|
li.copy(e3.ray), li.intersectPlane(c3, ci), li.direction.negate(), li.intersectPlane(c3, ci);
|
|
357528
357623
|
let m3 = ((e4, t4, r4, n5, a3) => {
|
|
@@ -357532,7 +357627,7 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357532
357627
|
})(t3, ci, a2, i4, s3), f4 = ai(m3);
|
|
357533
357628
|
e3.shiftKey && (f4 = 10 * Math.round(f4 / 10), m3 = ((e4) => e4 * Math.PI / 180)(f4)), void 0 !== u3 && void 0 !== d3 && d3 - u3 < 2 * Math.PI ? (m3 = oi(m3), m3 = m3 > Math.PI ? m3 - 2 * Math.PI : m3, m3 = T.MathUtils.clamp(m3, u3 - b2.current, d3 - b2.current), E2.current = b2.current + m3) : (E2.current = oi(b2.current + m3), E2.current = E2.current > Math.PI ? E2.current - 2 * Math.PI : E2.current), o2 && (f4 = ai(E2.current), w2.current.innerText = `${f4.toFixed(0)}\xBA`), ii.makeRotationAxis(l3, m3), si.copy(a2).applyMatrix4(ii).sub(a2).negate(), ii.setPosition(si), x2(ii);
|
|
357534
357629
|
}
|
|
357535
|
-
}, [o2, x2,
|
|
357630
|
+
}, [o2, x2, C12, n4, r3]), F2 = S.useCallback((e3) => {
|
|
357536
357631
|
o2 && (w2.current.style.display = "none"), e3.stopPropagation(), b2.current = E2.current, M2.current = null, y2(), g2 && (g2.enabled = true), e3.target.releasePointerCapture(e3.pointerId);
|
|
357537
357632
|
}, [o2, g2, y2]), k2 = S.useCallback((e3) => {
|
|
357538
357633
|
e3.stopPropagation(), P2(false);
|
|
@@ -357547,16 +357642,16 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357547
357642
|
}
|
|
357548
357643
|
return e3;
|
|
357549
357644
|
}, [A2]);
|
|
357550
|
-
return S.createElement("group", { ref: z3, onPointerDown: R2, onPointerMove: D2, onPointerUp: F2, onPointerOut: k2, matrix: _2, matrixAutoUpdate: false }, o2 && S.createElement(j, { position: [A2, A2, 0] }, S.createElement("div", { style: { display: "none", background: "#151520", color: "white", padding: "6px 8px", borderRadius: 7, whiteSpace: "nowrap" }, className: i3, ref: w2 })), S.createElement(ce, { points: L2, lineWidth: 4 * c2, visible: false, userData: v2 }), S.createElement(ce, { transparent: true, raycast: () => null, depthTest: s2, points: L2, lineWidth: c2, side: T.DoubleSide, color:
|
|
357645
|
+
return S.createElement("group", { ref: z3, onPointerDown: R2, onPointerMove: D2, onPointerUp: F2, onPointerOut: k2, matrix: _2, matrixAutoUpdate: false }, o2 && S.createElement(j, { position: [A2, A2, 0] }, S.createElement("div", { style: { display: "none", background: "#151520", color: "white", padding: "6px 8px", borderRadius: 7, whiteSpace: "nowrap" }, className: i3, ref: w2 })), S.createElement(ce, { points: L2, lineWidth: 4 * c2, visible: false, userData: v2 }), S.createElement(ce, { transparent: true, raycast: () => null, depthTest: s2, points: L2, lineWidth: c2, side: T.DoubleSide, color: C12 ? m2 : d2[r3], opacity: p2, polygonOffset: true, polygonOffsetFactor: -10, renderOrder: f3, fog: false }));
|
|
357551
357646
|
};
|
|
357552
357647
|
var di = new T.Ray();
|
|
357553
357648
|
var mi = new T.Vector3();
|
|
357554
357649
|
var fi = new T.Matrix4();
|
|
357555
357650
|
var pi = ({ dir1: e2, dir2: t2, axis: r3 }) => {
|
|
357556
|
-
const { translation: n4, translationLimits: o2, annotations: i3, annotationsClass: s2, depthTest: l2, scale: c2, lineWidth: u2, fixed: d2, axisColors: m2, hoveredColor: f3, opacity: p2, renderOrder: h2, onDragStart: x2, onDrag: y2, onDragEnd: v2, userData: g2 } = S.useContext(Yo), w2 = a.useThree((e3) => e3.controls), z3 = S.useRef(null), b2 = S.useRef(null), E2 = S.useRef(null), M2 = S.useRef(0),
|
|
357651
|
+
const { translation: n4, translationLimits: o2, annotations: i3, annotationsClass: s2, depthTest: l2, scale: c2, lineWidth: u2, fixed: d2, axisColors: m2, hoveredColor: f3, opacity: p2, renderOrder: h2, onDragStart: x2, onDrag: y2, onDragEnd: v2, userData: g2 } = S.useContext(Yo), w2 = a.useThree((e3) => e3.controls), z3 = S.useRef(null), b2 = S.useRef(null), E2 = S.useRef(null), M2 = S.useRef(0), C12 = S.useRef(0), [P2, R2] = S.useState(false), D2 = S.useCallback((e3) => {
|
|
357557
357652
|
i3 && (z3.current.innerText = `${n4.current[(r3 + 1) % 3].toFixed(2)}, ${n4.current[(r3 + 2) % 3].toFixed(2)}`, z3.current.style.display = "block"), e3.stopPropagation();
|
|
357558
357653
|
const t3 = e3.point.clone(), a2 = new T.Vector3().setFromMatrixPosition(b2.current.matrixWorld), o3 = new T.Vector3().setFromMatrixColumn(b2.current.matrixWorld, 0).normalize(), s3 = new T.Vector3().setFromMatrixColumn(b2.current.matrixWorld, 1).normalize(), l3 = new T.Vector3().setFromMatrixColumn(b2.current.matrixWorld, 2).normalize(), c3 = new T.Plane().setFromNormalAndCoplanarPoint(l3, a2);
|
|
357559
|
-
E2.current = { clickPoint: t3, e1: o3, e2: s3, plane: c3 }, M2.current = n4.current[(r3 + 1) % 3],
|
|
357654
|
+
E2.current = { clickPoint: t3, e1: o3, e2: s3, plane: c3 }, M2.current = n4.current[(r3 + 1) % 3], C12.current = n4.current[(r3 + 2) % 3], x2({ component: "Slider", axis: r3, origin: a2, directions: [o3, s3, l3] }), w2 && (w2.enabled = false), e3.target.setPointerCapture(e3.pointerId);
|
|
357560
357655
|
}, [i3, w2, x2, r3]), F2 = S.useCallback((e3) => {
|
|
357561
357656
|
if (e3.stopPropagation(), P2 || R2(true), E2.current) {
|
|
357562
357657
|
const { clickPoint: t3, e1: a2, e2: s3, plane: l3 } = E2.current, [c3, u3] = (null == o2 ? void 0 : o2[(r3 + 1) % 3]) || [void 0, void 0], [d3, m3] = (null == o2 ? void 0 : o2[(r3 + 2) % 3]) || [void 0, void 0];
|
|
@@ -357565,7 +357660,7 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357565
357660
|
const n5 = Math.abs(e4.x) >= Math.abs(e4.y) && Math.abs(e4.x) >= Math.abs(e4.z) ? 0 : Math.abs(e4.y) >= Math.abs(e4.x) && Math.abs(e4.y) >= Math.abs(e4.z) ? 1 : 2, a3 = [0, 1, 2].sort((e5, r5) => Math.abs(t4.getComponent(r5)) - Math.abs(t4.getComponent(e5))), o3 = n5 === a3[0] ? a3[1] : a3[0], i4 = e4.getComponent(n5), s4 = e4.getComponent(o3), l4 = t4.getComponent(n5), c4 = t4.getComponent(o3), u4 = r4.getComponent(n5), d4 = (r4.getComponent(o3) - u4 * (s4 / i4)) / (c4 - l4 * (s4 / i4));
|
|
357566
357661
|
return [(u4 - d4 * l4) / i4, d4];
|
|
357567
357662
|
})(a2, s3, mi);
|
|
357568
|
-
void 0 !== c3 && (f4 = Math.max(f4, c3 - M2.current)), void 0 !== u3 && (f4 = Math.min(f4, u3 - M2.current)), void 0 !== d3 && (p3 = Math.max(p3, d3 -
|
|
357663
|
+
void 0 !== c3 && (f4 = Math.max(f4, c3 - M2.current)), void 0 !== u3 && (f4 = Math.min(f4, u3 - M2.current)), void 0 !== d3 && (p3 = Math.max(p3, d3 - C12.current)), void 0 !== m3 && (p3 = Math.min(p3, m3 - C12.current)), n4.current[(r3 + 1) % 3] = M2.current + f4, n4.current[(r3 + 2) % 3] = C12.current + p3, i3 && (z3.current.innerText = `${n4.current[(r3 + 1) % 3].toFixed(2)}, ${n4.current[(r3 + 2) % 3].toFixed(2)}`), fi.makeTranslation(f4 * a2.x + p3 * s3.x, f4 * a2.y + p3 * s3.y, f4 * a2.z + p3 * s3.z), y2(fi);
|
|
357569
357664
|
}
|
|
357570
357665
|
}, [i3, y2, P2, n4, o2, r3]), k2 = S.useCallback((e3) => {
|
|
357571
357666
|
i3 && (z3.current.style.display = "none"), e3.stopPropagation(), E2.current = null, v2(), w2 && (w2.enabled = true), e3.target.releasePointerCapture(e3.pointerId);
|
|
@@ -357583,13 +357678,13 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357583
357678
|
var vi = new T.Vector3();
|
|
357584
357679
|
var gi = new T.Matrix4();
|
|
357585
357680
|
var wi = ({ direction: e2, axis: t2 }) => {
|
|
357586
|
-
const { scaleLimits: r3, annotations: n4, annotationsClass: o2, depthTest: i3, scale: s2, lineWidth: l2, fixed: c2, axisColors: u2, hoveredColor: d2, opacity: m2, renderOrder: f3, onDragStart: p2, onDrag: h2, onDragEnd: x2, userData: y2 } = S.useContext(Yo), v2 = a.useThree((e3) => e3.size), g2 = a.useThree((e3) => e3.controls), w2 = S.useRef(null), z3 = S.useRef(null), b2 = S.useRef(null), E2 = S.useRef(1), M2 = S.useRef(1),
|
|
357681
|
+
const { scaleLimits: r3, annotations: n4, annotationsClass: o2, depthTest: i3, scale: s2, lineWidth: l2, fixed: c2, axisColors: u2, hoveredColor: d2, opacity: m2, renderOrder: f3, onDragStart: p2, onDrag: h2, onDragEnd: x2, userData: y2 } = S.useContext(Yo), v2 = a.useThree((e3) => e3.size), g2 = a.useThree((e3) => e3.controls), w2 = S.useRef(null), z3 = S.useRef(null), b2 = S.useRef(null), E2 = S.useRef(1), M2 = S.useRef(1), C12 = S.useRef(null), [P2, R2] = S.useState(false), D2 = c2 ? 1.2 : 1.2 * s2, F2 = S.useCallback((r4) => {
|
|
357587
357682
|
n4 && (w2.current.innerText = `${M2.current.toFixed(2)}`, w2.current.style.display = "block"), r4.stopPropagation();
|
|
357588
357683
|
const a2 = new T.Matrix4().extractRotation(z3.current.matrixWorld), o3 = r4.point.clone(), i4 = new T.Vector3().setFromMatrixPosition(z3.current.matrixWorld), l3 = e2.clone().applyMatrix4(a2).normalize(), u3 = z3.current.matrixWorld.clone(), d3 = u3.clone().invert(), m3 = c2 ? 1 / ie(z3.current.getWorldPosition(hi), s2, r4.camera, v2) : 1;
|
|
357589
|
-
|
|
357684
|
+
C12.current = { clickPoint: o3, dir: l3, mPLG: u3, mPLGInv: d3, offsetMultiplier: m3 }, p2({ component: "Sphere", axis: t2, origin: i4, directions: [l3] }), g2 && (g2.enabled = false), r4.target.setPointerCapture(r4.pointerId);
|
|
357590
357685
|
}, [n4, g2, e2, p2, t2, c2, s2, v2]), k2 = S.useCallback((e3) => {
|
|
357591
|
-
if (e3.stopPropagation(), P2 || R2(true),
|
|
357592
|
-
const { clickPoint: a2, dir: o3, mPLG: i4, mPLGInv: l3, offsetMultiplier: u3 } =
|
|
357686
|
+
if (e3.stopPropagation(), P2 || R2(true), C12.current) {
|
|
357687
|
+
const { clickPoint: a2, dir: o3, mPLG: i4, mPLGInv: l3, offsetMultiplier: u3 } = C12.current, [d3, m3] = (null == r3 ? void 0 : r3[t2]) || [1e-5, void 0], f4 = ((e4, t3, r4, n5) => {
|
|
357593
357688
|
const a3 = t3.dot(t3), o4 = t3.dot(e4) - t3.dot(r4), i5 = t3.dot(n5);
|
|
357594
357689
|
return 0 === i5 ? -o4 / a3 : (hi.copy(n5).multiplyScalar(a3 / i5).sub(t3), xi.copy(n5).multiplyScalar(o4 / i5).add(r4).sub(e4), -hi.dot(xi) / hi.dot(hi));
|
|
357595
357690
|
})(a2, o3, e3.ray.origin, e3.ray.direction), p3 = f4 * u3, x3 = c2 ? p3 : p3 / s2;
|
|
@@ -357597,7 +357692,7 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357597
357692
|
e3.shiftKey && (y3 = Math.round(10 * y3) / 10), y3 = Math.max(y3, d3 / E2.current), void 0 !== m3 && (y3 = Math.min(y3, m3 / E2.current)), M2.current = E2.current * y3, b2.current.position.set(0, D2 + p3, 0), n4 && (w2.current.innerText = `${M2.current.toFixed(2)}`), vi.set(1, 1, 1), vi.setComponent(t2, y3), gi.makeScale(vi.x, vi.y, vi.z).premultiply(i4).multiply(l3), h2(gi);
|
|
357598
357693
|
}
|
|
357599
357694
|
}, [n4, D2, h2, P2, r3, t2]), _2 = S.useCallback((e3) => {
|
|
357600
|
-
n4 && (w2.current.style.display = "none"), e3.stopPropagation(), E2.current = M2.current,
|
|
357695
|
+
n4 && (w2.current.style.display = "none"), e3.stopPropagation(), E2.current = M2.current, C12.current = null, b2.current.position.set(0, D2, 0), x2(), g2 && (g2.enabled = true), e3.target.releasePointerCapture(e3.pointerId);
|
|
357601
357696
|
}, [n4, g2, x2, D2]), A2 = S.useCallback((e3) => {
|
|
357602
357697
|
e3.stopPropagation(), R2(false);
|
|
357603
357698
|
}, []), { radius: L2, matrixL: I2 } = S.useMemo(() => {
|
|
@@ -357625,7 +357720,7 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357625
357720
|
var Bi = new T.Vector3(1, 0, 0);
|
|
357626
357721
|
var Ui = new T.Vector3(0, 1, 0);
|
|
357627
357722
|
var Vi = new T.Vector3(0, 0, 1);
|
|
357628
|
-
var Oi = S.forwardRef(({ enabled: e2 = true, matrix: t2, onDragStart: r3, onDrag: n4, onDragEnd: o2, autoTransform: i3 = true, anchor: s2, disableAxes: l2 = false, disableSliders: c2 = false, disableRotations: u2 = false, disableScaling: d2 = false, activeAxes: m2 = [true, true, true], offset: f3 = [0, 0, 0], rotation: p2 = [0, 0, 0], scale: h2 = 1, lineWidth: x2 = 4, fixed: y2 = false, translationLimits: v2, rotationLimits: g2, scaleLimits: w2, depthTest: z3 = true, renderOrder: b2 = 500, axisColors: E2 = ["#ff2060", "#20df80", "#2080ff"], hoveredColor:
|
|
357723
|
+
var Oi = S.forwardRef(({ enabled: e2 = true, matrix: t2, onDragStart: r3, onDrag: n4, onDragEnd: o2, autoTransform: i3 = true, anchor: s2, disableAxes: l2 = false, disableSliders: c2 = false, disableRotations: u2 = false, disableScaling: d2 = false, activeAxes: m2 = [true, true, true], offset: f3 = [0, 0, 0], rotation: p2 = [0, 0, 0], scale: h2 = 1, lineWidth: x2 = 4, fixed: y2 = false, translationLimits: v2, rotationLimits: g2, scaleLimits: w2, depthTest: z3 = true, renderOrder: b2 = 500, axisColors: E2 = ["#ff2060", "#20df80", "#2080ff"], hoveredColor: C12 = "#ffff40", annotations: P2 = false, annotationsClass: R2, opacity: D2 = 1, visible: F2 = true, userData: k2, children: _2, ...A2 }, L2) => {
|
|
357629
357724
|
const I2 = a.useThree((e3) => e3.invalidate), B2 = S.useRef(null), U2 = S.useRef(null), V3 = S.useRef(null), O2 = S.useRef(null), N2 = S.useRef([0, 0, 0]), j2 = S.useRef(new T.Vector3(1, 1, 1)), W2 = S.useRef(new T.Vector3(1, 1, 1));
|
|
357630
357725
|
S.useLayoutEffect(() => {
|
|
357631
357726
|
s2 && (O2.current.updateWorldMatrix(true, true), Mi.copy(O2.current.matrixWorld).invert(), Di.makeEmpty(), O2.current.traverse((e3) => {
|
|
@@ -357638,7 +357733,7 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357638
357733
|
Ei.copy(B2.current.matrixWorld), Mi.copy(Ei).invert(), Si.copy(bi).premultiply(e3), Ci.copy(Si).premultiply(Mi), Ti.copy(zi).invert(), Pi.copy(Ci).multiply(Ti), i3 && U2.current.matrix.copy(Ci), n4 && n4(Ci, Pi, Si, e3), I2();
|
|
357639
357734
|
}, onDragEnd: () => {
|
|
357640
357735
|
o2 && o2(), I2();
|
|
357641
|
-
}, translation: N2, translationLimits: v2, rotationLimits: g2, axisColors: E2, hoveredColor:
|
|
357736
|
+
}, translation: N2, translationLimits: v2, rotationLimits: g2, axisColors: E2, hoveredColor: C12, opacity: D2, scale: h2, lineWidth: x2, fixed: y2, depthTest: z3, renderOrder: b2, userData: k2, annotations: P2, annotationsClass: R2 }), [r3, n4, o2, N2, v2, g2, w2, z3, h2, x2, y2, ...E2, C12, D2, k2, i3, P2, R2]), H2 = new T.Vector3();
|
|
357642
357737
|
return a.useFrame((e3) => {
|
|
357643
357738
|
if (y2) {
|
|
357644
357739
|
const t3 = ie(V3.current.getWorldPosition(H2), h2, e3.camera, e3.size);
|
|
@@ -357669,7 +357764,7 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357669
357764
|
var Hi = S.forwardRef(({ points: e2 = Xi.SAMPLE_FACELANDMARKER_RESULT.faceLandmarks[0], face: t2, facialTransformationMatrix: r3, faceBlendshapes: n4, offset: o2, offsetScalar: i3 = 80, width: s2, height: l2, depth: c2 = 1, verticalTri: u2 = [159, 386, 152], origin: d2, eyes: m2 = true, eyesAsOrigin: f3 = false, debug: p2 = false, children: h2, ...x2 }, y2) => {
|
|
357670
357765
|
var v2;
|
|
357671
357766
|
t2 && (e2 = t2.keypoints, console.warn("Facemesh `face` prop is deprecated: use `points` instead"));
|
|
357672
|
-
const g2 = S.useRef(null), w2 = S.useRef(null), z3 = S.useRef(null), b2 = S.useRef(null), E2 = S.useRef(null), M2 = S.useRef(null),
|
|
357767
|
+
const g2 = S.useRef(null), w2 = S.useRef(null), z3 = S.useRef(null), b2 = S.useRef(null), E2 = S.useRef(null), M2 = S.useRef(null), C12 = S.useRef(null), [P2] = S.useState(() => new T.Vector3()), [R2] = S.useState(() => new T.Object3D()), [D2] = S.useState(() => new T.Quaternion()), [F2] = S.useState(() => new T.Vector3()), { invalidate: k2 } = a.useThree();
|
|
357673
357768
|
S.useEffect(() => {
|
|
357674
357769
|
var e3;
|
|
357675
357770
|
null == (e3 = E2.current) || e3.geometry.setIndex(Xi.TRIANGULATION);
|
|
@@ -357683,12 +357778,12 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357683
357778
|
(h3.setFromPoints(e2), h3.setDrawRange(0, Xi.TRIANGULATION.length), r3) ? (R2.matrix.fromArray(r3.data), R2.matrix.decompose(R2.position, R2.quaternion, R2.scale), R2.rotation.y *= -1, R2.rotation.z *= -1, D2.setFromEuler(R2.rotation), o2 ? (R2.position.y *= -1, R2.position.z *= -1, null == (x3 = g2.current) || x3.position.copy(R2.position.divideScalar(i3))) : null == (y3 = g2.current) || y3.position.set(0, 0, 0)) : (Gi(e2[u2[0]], e2[u2[1]], e2[u2[2]], P2), D2.setFromUnitVectors(Wi, P2));
|
|
357684
357779
|
const v3 = D2.clone().invert();
|
|
357685
357780
|
if (h3.computeBoundingBox(), p2 && k2(), h3.center(), h3.applyQuaternion(v3), null == (a2 = b2.current) || a2.setRotationFromQuaternion(D2), m2) if (n4) {
|
|
357686
|
-
if (M2.current &&
|
|
357687
|
-
const e3 = M2.current._computeSphere(h3), t4 =
|
|
357781
|
+
if (M2.current && C12.current && z3.current) if (f3) {
|
|
357782
|
+
const e3 = M2.current._computeSphere(h3), t4 = C12.current._computeSphere(h3), r4 = function(e4, t5) {
|
|
357688
357783
|
return e4.clone().add(t5).multiplyScalar(0.5);
|
|
357689
357784
|
}(e3.center, t4.center);
|
|
357690
|
-
d2 = r4.negate(), M2.current._update(h3, n4, e3),
|
|
357691
|
-
} else M2.current._update(h3, n4),
|
|
357785
|
+
d2 = r4.negate(), M2.current._update(h3, n4, e3), C12.current._update(h3, n4, t4);
|
|
357786
|
+
} else M2.current._update(h3, n4), C12.current._update(h3, n4);
|
|
357692
357787
|
} else console.warn("Facemesh `eyes` option only works if `faceBlendshapes` is provided: skipping.");
|
|
357693
357788
|
if (z3.current) {
|
|
357694
357789
|
if (void 0 !== d2) if ("number" == typeof d2) {
|
|
@@ -357704,10 +357799,10 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357704
357799
|
}
|
|
357705
357800
|
h3.computeVertexNormals(), h3.attributes.position.needsUpdate = true;
|
|
357706
357801
|
}, [e2, r3, n4, R2, o2, i3, s2, l2, c2, u2, d2, m2, p2, k2, P2, D2, _2, F2]);
|
|
357707
|
-
const A2 = S.useMemo(() => ({ outerRef: b2, meshRef: E2, eyeRightRef: M2, eyeLeftRef:
|
|
357802
|
+
const A2 = S.useMemo(() => ({ outerRef: b2, meshRef: E2, eyeRightRef: M2, eyeLeftRef: C12 }), []);
|
|
357708
357803
|
S.useImperativeHandle(y2, () => A2, [A2]);
|
|
357709
357804
|
const [L2] = S.useState(() => new T.Vector3()), I2 = null == (v2 = E2.current) ? void 0 : v2.geometry.boundingBox, B2 = (null == I2 ? void 0 : I2.getSize(L2).z) || 1;
|
|
357710
|
-
return S.createElement("group", x2, S.createElement("group", { ref: g2 }, S.createElement("group", { ref: b2 }, S.createElement("group", { ref: w2 }, p2 ? S.createElement(S.Fragment, null, S.createElement("axesHelper", { args: [B2] }), S.createElement(ce, { points: [[0, 0, 0], [0, 0, -B2]], color: 65535 })) : null, S.createElement("group", { ref: z3 }, m2 && n4 && S.createElement("group", { name: "eyes" }, S.createElement(qi, { side: "left", ref: M2, debug: p2 }), S.createElement(qi, { side: "right", ref:
|
|
357805
|
+
return S.createElement("group", x2, S.createElement("group", { ref: g2 }, S.createElement("group", { ref: b2 }, S.createElement("group", { ref: w2 }, p2 ? S.createElement(S.Fragment, null, S.createElement("axesHelper", { args: [B2] }), S.createElement(ce, { points: [[0, 0, 0], [0, 0, -B2]], color: 65535 })) : null, S.createElement("group", { ref: z3 }, m2 && n4 && S.createElement("group", { name: "eyes" }, S.createElement(qi, { side: "left", ref: M2, debug: p2 }), S.createElement(qi, { side: "right", ref: C12, debug: p2 })), S.createElement("mesh", { ref: E2, name: "face" }, h2, p2 ? S.createElement(S.Fragment, null, I2 && S.createElement("box3Helper", { args: [I2] })) : null))))));
|
|
357711
357806
|
});
|
|
357712
357807
|
var $i = { contourLandmarks: { right: [33, 133, 159, 145, 153], left: [263, 362, 386, 374, 380] }, blendshapes: { right: [14, 16, 18, 12], left: [13, 15, 17, 11] }, color: { right: "red", left: "#00ff00" }, fov: { horizontal: 100, vertical: 90 } };
|
|
357713
357808
|
var qi = S.forwardRef(({ side: e2, debug: t2 = true }, r3) => {
|
|
@@ -357753,27 +357848,27 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
357753
357848
|
var ts = t.createContext({});
|
|
357754
357849
|
var rs = t.forwardRef(({ camera: e2, videoTexture: r3 = { start: true }, manualDetect: n4 = false, faceLandmarkerResult: o2, manualUpdate: i3 = false, makeDefault: l2, smoothTime: c2 = 0.25, offset: u2 = true, offsetScalar: d2 = 80, eyes: m2 = false, eyesAsOrigin: f3 = true, depth: p2 = 0.15, debug: h2 = false, facemesh: x2 }, y2) => {
|
|
357755
357850
|
var v2, g2;
|
|
357756
|
-
const w2 = a.useThree((e3) => e3.scene), z3 = a.useThree((e3) => e3.camera), b2 = a.useThree((e3) => e3.set), E2 = a.useThree((e3) => e3.get),
|
|
357757
|
-
R2.parent =
|
|
357851
|
+
const w2 = a.useThree((e3) => e3.scene), z3 = a.useThree((e3) => e3.camera), b2 = a.useThree((e3) => e3.set), E2 = a.useThree((e3) => e3.get), C12 = e2 || z3, P2 = t.useRef(null), [R2] = t.useState(() => new T.Object3D()), [D2] = t.useState(() => new T.Vector3()), [F2] = t.useState(() => new T.Vector3()), [k2] = t.useState(() => new T.Vector3()), [_2] = t.useState(() => new T.Vector3()), A2 = t.useCallback(() => {
|
|
357852
|
+
R2.parent = C12.parent;
|
|
357758
357853
|
const e3 = P2.current;
|
|
357759
357854
|
if (e3) {
|
|
357760
357855
|
const { outerRef: t2, eyeRightRef: r4, eyeLeftRef: n5 } = e3;
|
|
357761
357856
|
if (r4.current && n5.current) {
|
|
357762
357857
|
const { irisDirRef: e4 } = r4.current, { irisDirRef: a2 } = n5.current;
|
|
357763
|
-
e4.current && a2.current && t2.current && (D2.copy(es(e4.current, new T.Vector3(0, 0, 0), t2.current)), F2.copy(es(a2.current, new T.Vector3(0, 0, 0), t2.current)), R2.position.copy(es(t2.current, Ji(D2, F2),
|
|
357764
|
-
} else t2.current && (R2.position.copy(es(t2.current, new T.Vector3(0, 0, 0),
|
|
357858
|
+
e4.current && a2.current && t2.current && (D2.copy(es(e4.current, new T.Vector3(0, 0, 0), t2.current)), F2.copy(es(a2.current, new T.Vector3(0, 0, 0), t2.current)), R2.position.copy(es(t2.current, Ji(D2, F2), C12.parent || w2)), k2.copy(es(e4.current, new T.Vector3(0, 0, 1), t2.current)), _2.copy(es(a2.current, new T.Vector3(0, 0, 1), t2.current)), R2.lookAt(t2.current.localToWorld(Ji(k2, _2))));
|
|
357859
|
+
} else t2.current && (R2.position.copy(es(t2.current, new T.Vector3(0, 0, 0), C12.parent || w2)), R2.lookAt(t2.current.localToWorld(new T.Vector3(0, 0, 1))));
|
|
357765
357860
|
}
|
|
357766
357861
|
return R2;
|
|
357767
|
-
}, [
|
|
357768
|
-
if (
|
|
357862
|
+
}, [C12, F2, _2, D2, k2, w2, R2]), [L2] = t.useState(() => new T.Object3D()), I2 = t.useCallback(function(e3, t2) {
|
|
357863
|
+
if (C12) {
|
|
357769
357864
|
var r4;
|
|
357770
357865
|
if (null !== (r4 = t2) && void 0 !== r4 || (t2 = A2()), c2 > 0) {
|
|
357771
357866
|
const r5 = 1e-9;
|
|
357772
357867
|
s.easing.damp3(L2.position, t2.position, c2, e3, void 0, void 0, r5), s.easing.dampE(L2.rotation, t2.rotation, c2, e3, void 0, void 0, r5);
|
|
357773
357868
|
} else L2.position.copy(t2.position), L2.rotation.copy(t2.rotation);
|
|
357774
|
-
|
|
357869
|
+
C12.position.copy(L2.position), C12.rotation.copy(L2.rotation);
|
|
357775
357870
|
}
|
|
357776
|
-
}, [
|
|
357871
|
+
}, [C12, A2, c2, L2.position, L2.rotation]);
|
|
357777
357872
|
a.useFrame((e3, t2) => {
|
|
357778
357873
|
i3 || I2(t2);
|
|
357779
357874
|
});
|
|
@@ -358138,14 +358233,14 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
358138
358233
|
v2.style.cursor = "default", y2.domElement.style.cursor = "default";
|
|
358139
358234
|
};
|
|
358140
358235
|
}, [r3, l2, v2, e2]);
|
|
358141
|
-
const [E2] = S.useState({ scale: 1, rotation: b2, damping: h2 }),
|
|
358236
|
+
const [E2] = S.useState({ scale: 1, rotation: b2, damping: h2 }), C12 = S.useRef(null);
|
|
358142
358237
|
a.useFrame((e3, t3) => {
|
|
358143
|
-
s.easing.damp3(
|
|
358238
|
+
s.easing.damp3(C12.current.scale, E2.scale, E2.damping, t3), s.easing.dampE(C12.current.rotation, E2.rotation, E2.damping, t3);
|
|
358144
358239
|
});
|
|
358145
358240
|
const T2 = i2.useGesture({ onHover: ({ last: t3 }) => {
|
|
358146
358241
|
l2 && !r3 && e2 && (v2.style.cursor = t3 ? "auto" : "grab");
|
|
358147
358242
|
}, onDrag: ({ down: r4, delta: [a2, o3], memo: [i3, s2] = E2.rotation || b2 }) => e2 ? (l2 && (v2.style.cursor = r4 ? "grabbing" : "grab"), a2 = n3.MathUtils.clamp(s2 + a2 / g2.width * Math.PI * u2, ...z3), o3 = n3.MathUtils.clamp(i3 + o3 / g2.height * Math.PI * u2, ...w2), E2.scale = r4 && o3 > w2[1] / 2 ? m2 : 1, E2.rotation = t2 && !r4 ? b2 : [o3, a2, 0], E2.damping = t2 && !r4 && "boolean" != typeof t2 ? t2 : h2, [o3, a2]) : [o3, a2] }, { target: r3 ? v2 : void 0 });
|
|
358148
|
-
return S.createElement("group", M.default({ ref:
|
|
358243
|
+
return S.createElement("group", M.default({ ref: C12 }, null == T2 ? void 0 : T2()), c2);
|
|
358149
358244
|
}, exports2.Progress = function({ children: e2 }) {
|
|
358150
358245
|
const t2 = G();
|
|
358151
358246
|
return S.createElement(S.Fragment, null, null == e2 ? void 0 : e2(t2));
|
|
@@ -358206,18 +358301,18 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
358206
358301
|
};
|
|
358207
358302
|
}
|
|
358208
358303
|
}, [g2, v2, x2, r3, M2, y2, n4, t2]);
|
|
358209
|
-
let
|
|
358304
|
+
let C12 = 0;
|
|
358210
358305
|
return a.useFrame((t3, r4) => {
|
|
358211
|
-
|
|
358306
|
+
C12 = M2.offset, s.easing.damp(M2, "offset", E2.current, l2, r4, c2, void 0, e2), s.easing.damp(M2, "delta", Math.abs(C12 - M2.offset), l2, r4, c2, void 0, e2), M2.delta > e2 && y2();
|
|
358212
358307
|
}), S.createElement(q.Provider, { value: M2 }, m2);
|
|
358213
358308
|
}, exports2.Segment = Eo, exports2.SegmentObject = zo, exports2.Segments = wo, exports2.Select = function({ box: e2, multiple: t2, children: r3, onChange: n4, onChangePointerUp: o2, border: i3 = "1px solid #55aaff", backgroundColor: s2 = "rgba(75, 160, 255, 0.1)", filter: l2 = (e3) => e3, ...d2 }) {
|
|
358214
|
-
const [m2, f3] = S.useState(false), { setEvents: p2, camera: h2, raycaster: x2, gl: y2, controls: v2, size: g2, get: w2 } = a.useThree(), [z3, b2] = S.useState(false), [E2,
|
|
358309
|
+
const [m2, f3] = S.useState(false), { setEvents: p2, camera: h2, raycaster: x2, gl: y2, controls: v2, size: g2, get: w2 } = a.useThree(), [z3, b2] = S.useState(false), [E2, C12] = S.useReducer((e3, { object: t3, shift: r4 }) => void 0 === t3 ? [] : Array.isArray(t3) ? t3 : r4 ? e3.includes(t3) ? e3.filter((e4) => e4 !== t3) : [t3, ...e3] : e3[0] === t3 ? [] : [t3], []);
|
|
358215
358310
|
S.useEffect(() => {
|
|
358216
358311
|
m2 ? null == n4 || n4(E2) : null == o2 || o2(E2);
|
|
358217
358312
|
}, [E2, m2]);
|
|
358218
358313
|
const P2 = S.useCallback((e3) => {
|
|
358219
|
-
e3.stopPropagation(),
|
|
358220
|
-
}, []), R2 = S.useCallback((e3) => !z3 &&
|
|
358314
|
+
e3.stopPropagation(), C12({ object: l2([e3.object])[0], shift: t2 && e3.shiftKey });
|
|
358315
|
+
}, []), R2 = S.useCallback((e3) => !z3 && C12({}), [z3]), D2 = S.useRef(null);
|
|
358221
358316
|
return S.useEffect(() => {
|
|
358222
358317
|
if (!e2 || !t2) return;
|
|
358223
358318
|
const r4 = new c.SelectionBox(h2, D2.current), n5 = document.createElement("div");
|
|
@@ -358241,7 +358336,7 @@ vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}vec4 mod289(vec4 x){ret
|
|
|
358241
358336
|
d3.x = Math.max(a2.x, e4.clientX), d3.y = Math.max(a2.y, e4.clientY), o3.x = Math.min(a2.x, e4.clientX), o3.y = Math.min(a2.y, e4.clientY), n5.style.left = `${o3.x}px`, n5.style.top = `${o3.y}px`, n5.style.width = d3.x - o3.x + "px", n5.style.height = d3.y - o3.y + "px";
|
|
358242
358337
|
}(e3), b3(e3, r4.endPoint);
|
|
358243
358338
|
const t3 = r4.select().sort((e4) => e4.uuid).filter((e4) => e4.isMesh);
|
|
358244
|
-
u.shallow(t3, M2) || (M2 = t3,
|
|
358339
|
+
u.shallow(t3, M2) || (M2 = t3, C12({ object: l2(t3) }));
|
|
358245
358340
|
}
|
|
358246
358341
|
}
|
|
358247
358342
|
function P3(e3) {
|
|
@@ -358387,11 +358482,11 @@ float PCSS (sampler2D shadowMap, vec4 coords) {
|
|
|
358387
358482
|
return e2.shader ? S.createElement(Ea, e2) : S.createElement(Ma, e2);
|
|
358388
358483
|
}, exports2.SpriteAnimator = jr, exports2.Stage = function({ children: e2, center: t2, adjustCamera: r3 = true, intensity: n4 = 0.5, shadows: a2 = "contact", environment: o2 = "city", preset: i3 = "rembrandt", ...s2 }) {
|
|
358389
358484
|
var l2, c2, u2, d2, m2, f3, p2, h2;
|
|
358390
|
-
const x2 = "string" == typeof i3 ? ua[i3] : i3, [{ radius: y2, height: v2 }, g2] = S.useState({ radius: 0, width: 0, height: 0, depth: 0 }), w2 = null !== (l2 = null == a2 ? void 0 : a2.bias) && void 0 !== l2 ? l2 : -1e-4, z3 = null !== (c2 = null == a2 ? void 0 : a2.normalBias) && void 0 !== c2 ? c2 : 0, b2 = null !== (u2 = null == a2 ? void 0 : a2.size) && void 0 !== u2 ? u2 : 1024, E2 = null !== (d2 = null == a2 ? void 0 : a2.offset) && void 0 !== d2 ? d2 : 0,
|
|
358485
|
+
const x2 = "string" == typeof i3 ? ua[i3] : i3, [{ radius: y2, height: v2 }, g2] = S.useState({ radius: 0, width: 0, height: 0, depth: 0 }), w2 = null !== (l2 = null == a2 ? void 0 : a2.bias) && void 0 !== l2 ? l2 : -1e-4, z3 = null !== (c2 = null == a2 ? void 0 : a2.normalBias) && void 0 !== c2 ? c2 : 0, b2 = null !== (u2 = null == a2 ? void 0 : a2.size) && void 0 !== u2 ? u2 : 1024, E2 = null !== (d2 = null == a2 ? void 0 : a2.offset) && void 0 !== d2 ? d2 : 0, C12 = "contact" === a2 || "contact" === (null == a2 ? void 0 : a2.type), T2 = "accumulative" === a2 || "accumulative" === (null == a2 ? void 0 : a2.type), P2 = { ..."object" == typeof a2 ? a2 : {} }, R2 = o2 ? "string" == typeof o2 ? { preset: o2 } : o2 : null, D2 = S.useCallback((e3) => {
|
|
358391
358486
|
const { width: r4, height: n5, depth: a3, boundingSphere: o3 } = e3;
|
|
358392
358487
|
g2({ radius: o3.radius, width: r4, height: n5, depth: a3 }), null != t2 && t2.onCentered && t2.onCentered(e3);
|
|
358393
358488
|
}, []);
|
|
358394
|
-
return S.createElement(S.Fragment, null, S.createElement("ambientLight", { intensity: n4 / 3 }), S.createElement("spotLight", { penumbra: 1, position: [x2.main[0] * y2, x2.main[1] * y2, x2.main[2] * y2], intensity: 2 * n4, castShadow: !!a2, "shadow-bias": w2, "shadow-normalBias": z3, "shadow-mapSize": b2 }), S.createElement("pointLight", { position: [x2.fill[0] * y2, x2.fill[1] * y2, x2.fill[2] * y2], intensity: n4 }), S.createElement(Un, M.default({ fit: !!r3, clip: !!r3, margin: Number(r3), observe: true }, s2), S.createElement(da, { radius: y2, adjustCamera: r3 }), S.createElement(wr, M.default({}, t2, { position: [0, E2 / 2, 0], onCentered: D2 }), e2)), S.createElement("group", { position: [0, -v2 / 2 - E2 / 2, 0] },
|
|
358489
|
+
return S.createElement(S.Fragment, null, S.createElement("ambientLight", { intensity: n4 / 3 }), S.createElement("spotLight", { penumbra: 1, position: [x2.main[0] * y2, x2.main[1] * y2, x2.main[2] * y2], intensity: 2 * n4, castShadow: !!a2, "shadow-bias": w2, "shadow-normalBias": z3, "shadow-mapSize": b2 }), S.createElement("pointLight", { position: [x2.fill[0] * y2, x2.fill[1] * y2, x2.fill[2] * y2], intensity: n4 }), S.createElement(Un, M.default({ fit: !!r3, clip: !!r3, margin: Number(r3), observe: true }, s2), S.createElement(da, { radius: y2, adjustCamera: r3 }), S.createElement(wr, M.default({}, t2, { position: [0, E2 / 2, 0], onCentered: D2 }), e2)), S.createElement("group", { position: [0, -v2 / 2 - E2 / 2, 0] }, C12 && S.createElement(aa, M.default({ scale: 4 * y2, far: y2, blur: 2 }, P2)), T2 && S.createElement(sa, M.default({ temporal: true, frames: 100, alphaTest: 0.9, toneMapped: true, scale: 4 * y2 }, P2), S.createElement(la, { amount: null !== (m2 = P2.amount) && void 0 !== m2 ? m2 : 8, radius: null !== (f3 = P2.radius) && void 0 !== f3 ? f3 : y2, ambient: null !== (p2 = P2.ambient) && void 0 !== p2 ? p2 : 0.5, intensity: null !== (h2 = P2.intensity) && void 0 !== h2 ? h2 : 1, position: [x2.main[0] * y2, x2.main[1] * y2, x2.main[2] * y2], size: 4 * y2, bias: -w2, mapSize: b2 }))), o2 && S.createElement(na, R2));
|
|
358395
358490
|
}, exports2.Stars = Fa, exports2.Stats = function({ showPanel: e2 = 0, className: t2, parent: r3 }) {
|
|
358396
358491
|
const n4 = function(e3, t3 = [], r4) {
|
|
358397
358492
|
const [n5, a2] = S.useState();
|
|
@@ -371314,6 +371409,7 @@ var init_configStore = __esm({
|
|
|
371314
371409
|
selection: null,
|
|
371315
371410
|
siteEditorOpen: false,
|
|
371316
371411
|
floorEditorIdx: null,
|
|
371412
|
+
activeFloorIdx: 0,
|
|
371317
371413
|
validationErrors: [],
|
|
371318
371414
|
dirty: false,
|
|
371319
371415
|
loadConfig: (config3, filename, filePath) => {
|
|
@@ -371330,6 +371426,7 @@ var init_configStore = __esm({
|
|
|
371330
371426
|
selection: null,
|
|
371331
371427
|
siteEditorOpen: false,
|
|
371332
371428
|
floorEditorIdx: null,
|
|
371429
|
+
activeFloorIdx: 0,
|
|
371333
371430
|
validationErrors: pluginError ? [{ path: "components", message: pluginError }] : [],
|
|
371334
371431
|
dirty: false
|
|
371335
371432
|
});
|
|
@@ -371345,6 +371442,7 @@ var init_configStore = __esm({
|
|
|
371345
371442
|
selection: null,
|
|
371346
371443
|
siteEditorOpen: false,
|
|
371347
371444
|
floorEditorIdx: null,
|
|
371445
|
+
activeFloorIdx: 0,
|
|
371348
371446
|
validationErrors: [],
|
|
371349
371447
|
dirty: false
|
|
371350
371448
|
});
|
|
@@ -371368,6 +371466,11 @@ var init_configStore = __esm({
|
|
|
371368
371466
|
selection: idx !== null ? null : state2.selection,
|
|
371369
371467
|
siteEditorOpen: idx !== null ? false : state2.siteEditorOpen
|
|
371370
371468
|
})),
|
|
371469
|
+
// Switch the floor the editing surfaces operate on. A plain UI cursor
|
|
371470
|
+
// (no config change ⇒ the wrapped set skips re-resolution); selection is
|
|
371471
|
+
// left as-is (surfaces already guard selection.floor), matching how the
|
|
371472
|
+
// Sidebar floor tabs behaved before this was lifted into the store.
|
|
371473
|
+
setActiveFloor: (idx) => set2({ activeFloorIdx: idx }),
|
|
371371
371474
|
updateSite: (patch) => set2((state2) => {
|
|
371372
371475
|
if (!state2.config) return state2;
|
|
371373
371476
|
return {
|
|
@@ -371495,6 +371598,11 @@ var init_configStore = __esm({
|
|
|
371495
371598
|
dirty: true
|
|
371496
371599
|
};
|
|
371497
371600
|
}),
|
|
371601
|
+
setCoordConvention: (conv) => set2((state2) => {
|
|
371602
|
+
if (!state2.config) return state2;
|
|
371603
|
+
if (state2.config.coord_convention === conv) return state2;
|
|
371604
|
+
return { config: { ...state2.config, coord_convention: conv }, dirty: true };
|
|
371605
|
+
}),
|
|
371498
371606
|
updateFloor: (floorIdx, patch) => set2((state2) => {
|
|
371499
371607
|
if (!state2.config) return state2;
|
|
371500
371608
|
const floors = state2.config.floors.map(
|
|
@@ -405001,7 +405109,13 @@ var GlbModel = {
|
|
|
405001
405109
|
};
|
|
405002
405110
|
var Grid = {
|
|
405003
405111
|
$type: "Grid",
|
|
405112
|
+
extent_x: "extent_x",
|
|
405113
|
+
extent_y: "extent_y",
|
|
405004
405114
|
name: "name",
|
|
405115
|
+
origin_x: "origin_x",
|
|
405116
|
+
origin_y: "origin_y",
|
|
405117
|
+
spacing_x: "spacing_x",
|
|
405118
|
+
spacing_y: "spacing_y",
|
|
405005
405119
|
xlines: "xlines",
|
|
405006
405120
|
ylines: "ylines"
|
|
405007
405121
|
};
|
|
@@ -405294,6 +405408,7 @@ var Roof = {
|
|
|
405294
405408
|
};
|
|
405295
405409
|
var Room = {
|
|
405296
405410
|
$type: "Room",
|
|
405411
|
+
connections: "connections",
|
|
405297
405412
|
enabled: "enabled",
|
|
405298
405413
|
height: "height",
|
|
405299
405414
|
items: "items",
|
|
@@ -405952,16 +406067,42 @@ var WadiAstReflection = class extends AbstractAstReflection {
|
|
|
405952
406067
|
Grid: {
|
|
405953
406068
|
name: Grid.$type,
|
|
405954
406069
|
properties: {
|
|
406070
|
+
extent_x: {
|
|
406071
|
+
name: Grid.extent_x,
|
|
406072
|
+
optional: true
|
|
406073
|
+
},
|
|
406074
|
+
extent_y: {
|
|
406075
|
+
name: Grid.extent_y,
|
|
406076
|
+
optional: true
|
|
406077
|
+
},
|
|
405955
406078
|
name: {
|
|
405956
406079
|
name: Grid.name
|
|
405957
406080
|
},
|
|
406081
|
+
origin_x: {
|
|
406082
|
+
name: Grid.origin_x,
|
|
406083
|
+
optional: true
|
|
406084
|
+
},
|
|
406085
|
+
origin_y: {
|
|
406086
|
+
name: Grid.origin_y,
|
|
406087
|
+
optional: true
|
|
406088
|
+
},
|
|
406089
|
+
spacing_x: {
|
|
406090
|
+
name: Grid.spacing_x,
|
|
406091
|
+
optional: true
|
|
406092
|
+
},
|
|
406093
|
+
spacing_y: {
|
|
406094
|
+
name: Grid.spacing_y,
|
|
406095
|
+
optional: true
|
|
406096
|
+
},
|
|
405958
406097
|
xlines: {
|
|
405959
406098
|
name: Grid.xlines,
|
|
405960
|
-
defaultValue: []
|
|
406099
|
+
defaultValue: [],
|
|
406100
|
+
optional: true
|
|
405961
406101
|
},
|
|
405962
406102
|
ylines: {
|
|
405963
406103
|
name: Grid.ylines,
|
|
405964
|
-
defaultValue: []
|
|
406104
|
+
defaultValue: [],
|
|
406105
|
+
optional: true
|
|
405965
406106
|
}
|
|
405966
406107
|
},
|
|
405967
406108
|
superTypes: []
|
|
@@ -406733,6 +406874,11 @@ var WadiAstReflection = class extends AbstractAstReflection {
|
|
|
406733
406874
|
Room: {
|
|
406734
406875
|
name: Room.$type,
|
|
406735
406876
|
properties: {
|
|
406877
|
+
connections: {
|
|
406878
|
+
name: Room.connections,
|
|
406879
|
+
defaultValue: [],
|
|
406880
|
+
optional: true
|
|
406881
|
+
},
|
|
406736
406882
|
enabled: {
|
|
406737
406883
|
name: Room.enabled,
|
|
406738
406884
|
optional: true
|
|
@@ -408031,8 +408177,17 @@ var WadiGrammar = () => loadedWadiGrammar ?? (loadedWadiGrammar = loadGrammarFro
|
|
|
408031
408177
|
"$type": "Group",
|
|
408032
408178
|
"elements": [
|
|
408033
408179
|
{
|
|
408034
|
-
"$type": "
|
|
408035
|
-
"
|
|
408180
|
+
"$type": "Alternatives",
|
|
408181
|
+
"elements": [
|
|
408182
|
+
{
|
|
408183
|
+
"$type": "Keyword",
|
|
408184
|
+
"value": "guides"
|
|
408185
|
+
},
|
|
408186
|
+
{
|
|
408187
|
+
"$type": "Keyword",
|
|
408188
|
+
"value": "grid"
|
|
408189
|
+
}
|
|
408190
|
+
]
|
|
408036
408191
|
},
|
|
408037
408192
|
{
|
|
408038
408193
|
"$type": "Assignment",
|
|
@@ -408051,88 +408206,235 @@ var WadiGrammar = () => loadedWadiGrammar ?? (loadedWadiGrammar = loadGrammarFro
|
|
|
408051
408206
|
"value": "{"
|
|
408052
408207
|
},
|
|
408053
408208
|
{
|
|
408054
|
-
"$type": "
|
|
408055
|
-
"value": "x"
|
|
408056
|
-
},
|
|
408057
|
-
{
|
|
408058
|
-
"$type": "Keyword",
|
|
408059
|
-
"value": ":"
|
|
408060
|
-
},
|
|
408061
|
-
{
|
|
408062
|
-
"$type": "Assignment",
|
|
408063
|
-
"feature": "xlines",
|
|
408064
|
-
"operator": "+=",
|
|
408065
|
-
"terminal": {
|
|
408066
|
-
"$type": "RuleCall",
|
|
408067
|
-
"rule": {
|
|
408068
|
-
"$ref": "#/rules@6"
|
|
408069
|
-
},
|
|
408070
|
-
"arguments": []
|
|
408071
|
-
}
|
|
408072
|
-
},
|
|
408073
|
-
{
|
|
408074
|
-
"$type": "Group",
|
|
408209
|
+
"$type": "Alternatives",
|
|
408075
408210
|
"elements": [
|
|
408076
408211
|
{
|
|
408077
|
-
"$type": "
|
|
408078
|
-
"
|
|
408079
|
-
|
|
408080
|
-
|
|
408081
|
-
|
|
408082
|
-
"feature": "xlines",
|
|
408083
|
-
"operator": "+=",
|
|
408084
|
-
"terminal": {
|
|
408085
|
-
"$type": "RuleCall",
|
|
408086
|
-
"rule": {
|
|
408087
|
-
"$ref": "#/rules@6"
|
|
408212
|
+
"$type": "Group",
|
|
408213
|
+
"elements": [
|
|
408214
|
+
{
|
|
408215
|
+
"$type": "Keyword",
|
|
408216
|
+
"value": "x"
|
|
408088
408217
|
},
|
|
408089
|
-
|
|
408090
|
-
|
|
408091
|
-
|
|
408092
|
-
|
|
408093
|
-
|
|
408094
|
-
|
|
408095
|
-
|
|
408096
|
-
|
|
408097
|
-
|
|
408098
|
-
|
|
408099
|
-
|
|
408100
|
-
|
|
408101
|
-
|
|
408102
|
-
|
|
408103
|
-
|
|
408104
|
-
|
|
408105
|
-
|
|
408106
|
-
|
|
408107
|
-
|
|
408108
|
-
|
|
408109
|
-
|
|
408110
|
-
|
|
408111
|
-
|
|
408112
|
-
|
|
408113
|
-
|
|
408114
|
-
|
|
408115
|
-
|
|
408116
|
-
|
|
408117
|
-
|
|
408118
|
-
|
|
408119
|
-
|
|
408120
|
-
|
|
408218
|
+
{
|
|
408219
|
+
"$type": "Keyword",
|
|
408220
|
+
"value": ":"
|
|
408221
|
+
},
|
|
408222
|
+
{
|
|
408223
|
+
"$type": "Assignment",
|
|
408224
|
+
"feature": "xlines",
|
|
408225
|
+
"operator": "+=",
|
|
408226
|
+
"terminal": {
|
|
408227
|
+
"$type": "RuleCall",
|
|
408228
|
+
"rule": {
|
|
408229
|
+
"$ref": "#/rules@6"
|
|
408230
|
+
},
|
|
408231
|
+
"arguments": []
|
|
408232
|
+
}
|
|
408233
|
+
},
|
|
408234
|
+
{
|
|
408235
|
+
"$type": "Group",
|
|
408236
|
+
"elements": [
|
|
408237
|
+
{
|
|
408238
|
+
"$type": "Keyword",
|
|
408239
|
+
"value": ","
|
|
408240
|
+
},
|
|
408241
|
+
{
|
|
408242
|
+
"$type": "Assignment",
|
|
408243
|
+
"feature": "xlines",
|
|
408244
|
+
"operator": "+=",
|
|
408245
|
+
"terminal": {
|
|
408246
|
+
"$type": "RuleCall",
|
|
408247
|
+
"rule": {
|
|
408248
|
+
"$ref": "#/rules@6"
|
|
408249
|
+
},
|
|
408250
|
+
"arguments": []
|
|
408251
|
+
}
|
|
408252
|
+
}
|
|
408253
|
+
],
|
|
408254
|
+
"cardinality": "*"
|
|
408255
|
+
},
|
|
408256
|
+
{
|
|
408257
|
+
"$type": "Keyword",
|
|
408258
|
+
"value": "y"
|
|
408259
|
+
},
|
|
408260
|
+
{
|
|
408261
|
+
"$type": "Keyword",
|
|
408262
|
+
"value": ":"
|
|
408263
|
+
},
|
|
408264
|
+
{
|
|
408265
|
+
"$type": "Assignment",
|
|
408266
|
+
"feature": "ylines",
|
|
408267
|
+
"operator": "+=",
|
|
408268
|
+
"terminal": {
|
|
408269
|
+
"$type": "RuleCall",
|
|
408270
|
+
"rule": {
|
|
408271
|
+
"$ref": "#/rules@6"
|
|
408272
|
+
},
|
|
408273
|
+
"arguments": []
|
|
408274
|
+
}
|
|
408275
|
+
},
|
|
408276
|
+
{
|
|
408277
|
+
"$type": "Group",
|
|
408278
|
+
"elements": [
|
|
408279
|
+
{
|
|
408280
|
+
"$type": "Keyword",
|
|
408281
|
+
"value": ","
|
|
408282
|
+
},
|
|
408283
|
+
{
|
|
408284
|
+
"$type": "Assignment",
|
|
408285
|
+
"feature": "ylines",
|
|
408286
|
+
"operator": "+=",
|
|
408287
|
+
"terminal": {
|
|
408288
|
+
"$type": "RuleCall",
|
|
408289
|
+
"rule": {
|
|
408290
|
+
"$ref": "#/rules@6"
|
|
408291
|
+
},
|
|
408292
|
+
"arguments": []
|
|
408293
|
+
}
|
|
408294
|
+
}
|
|
408295
|
+
],
|
|
408296
|
+
"cardinality": "*"
|
|
408297
|
+
}
|
|
408298
|
+
]
|
|
408121
408299
|
},
|
|
408122
408300
|
{
|
|
408123
|
-
"$type": "
|
|
408124
|
-
"
|
|
408125
|
-
|
|
408126
|
-
|
|
408127
|
-
|
|
408128
|
-
|
|
408129
|
-
|
|
408301
|
+
"$type": "Group",
|
|
408302
|
+
"elements": [
|
|
408303
|
+
{
|
|
408304
|
+
"$type": "Group",
|
|
408305
|
+
"elements": [
|
|
408306
|
+
{
|
|
408307
|
+
"$type": "Keyword",
|
|
408308
|
+
"value": "origin"
|
|
408309
|
+
},
|
|
408310
|
+
{
|
|
408311
|
+
"$type": "Keyword",
|
|
408312
|
+
"value": "("
|
|
408313
|
+
},
|
|
408314
|
+
{
|
|
408315
|
+
"$type": "Assignment",
|
|
408316
|
+
"feature": "origin_x",
|
|
408317
|
+
"operator": "=",
|
|
408318
|
+
"terminal": {
|
|
408319
|
+
"$type": "RuleCall",
|
|
408320
|
+
"rule": {
|
|
408321
|
+
"$ref": "#/rules@17"
|
|
408322
|
+
},
|
|
408323
|
+
"arguments": []
|
|
408324
|
+
}
|
|
408325
|
+
},
|
|
408326
|
+
{
|
|
408327
|
+
"$type": "Keyword",
|
|
408328
|
+
"value": ","
|
|
408329
|
+
},
|
|
408330
|
+
{
|
|
408331
|
+
"$type": "Assignment",
|
|
408332
|
+
"feature": "origin_y",
|
|
408333
|
+
"operator": "=",
|
|
408334
|
+
"terminal": {
|
|
408335
|
+
"$type": "RuleCall",
|
|
408336
|
+
"rule": {
|
|
408337
|
+
"$ref": "#/rules@17"
|
|
408338
|
+
},
|
|
408339
|
+
"arguments": []
|
|
408340
|
+
}
|
|
408341
|
+
},
|
|
408342
|
+
{
|
|
408343
|
+
"$type": "Keyword",
|
|
408344
|
+
"value": ")"
|
|
408345
|
+
}
|
|
408346
|
+
],
|
|
408347
|
+
"cardinality": "?"
|
|
408130
408348
|
},
|
|
408131
|
-
|
|
408132
|
-
|
|
408349
|
+
{
|
|
408350
|
+
"$type": "Keyword",
|
|
408351
|
+
"value": "spacing"
|
|
408352
|
+
},
|
|
408353
|
+
{
|
|
408354
|
+
"$type": "Keyword",
|
|
408355
|
+
"value": "("
|
|
408356
|
+
},
|
|
408357
|
+
{
|
|
408358
|
+
"$type": "Assignment",
|
|
408359
|
+
"feature": "spacing_x",
|
|
408360
|
+
"operator": "=",
|
|
408361
|
+
"terminal": {
|
|
408362
|
+
"$type": "RuleCall",
|
|
408363
|
+
"rule": {
|
|
408364
|
+
"$ref": "#/rules@17"
|
|
408365
|
+
},
|
|
408366
|
+
"arguments": []
|
|
408367
|
+
}
|
|
408368
|
+
},
|
|
408369
|
+
{
|
|
408370
|
+
"$type": "Keyword",
|
|
408371
|
+
"value": ","
|
|
408372
|
+
},
|
|
408373
|
+
{
|
|
408374
|
+
"$type": "Assignment",
|
|
408375
|
+
"feature": "spacing_y",
|
|
408376
|
+
"operator": "=",
|
|
408377
|
+
"terminal": {
|
|
408378
|
+
"$type": "RuleCall",
|
|
408379
|
+
"rule": {
|
|
408380
|
+
"$ref": "#/rules@17"
|
|
408381
|
+
},
|
|
408382
|
+
"arguments": []
|
|
408383
|
+
}
|
|
408384
|
+
},
|
|
408385
|
+
{
|
|
408386
|
+
"$type": "Keyword",
|
|
408387
|
+
"value": ")"
|
|
408388
|
+
},
|
|
408389
|
+
{
|
|
408390
|
+
"$type": "Group",
|
|
408391
|
+
"elements": [
|
|
408392
|
+
{
|
|
408393
|
+
"$type": "Keyword",
|
|
408394
|
+
"value": "extent"
|
|
408395
|
+
},
|
|
408396
|
+
{
|
|
408397
|
+
"$type": "Keyword",
|
|
408398
|
+
"value": "("
|
|
408399
|
+
},
|
|
408400
|
+
{
|
|
408401
|
+
"$type": "Assignment",
|
|
408402
|
+
"feature": "extent_x",
|
|
408403
|
+
"operator": "=",
|
|
408404
|
+
"terminal": {
|
|
408405
|
+
"$type": "RuleCall",
|
|
408406
|
+
"rule": {
|
|
408407
|
+
"$ref": "#/rules@79"
|
|
408408
|
+
},
|
|
408409
|
+
"arguments": []
|
|
408410
|
+
}
|
|
408411
|
+
},
|
|
408412
|
+
{
|
|
408413
|
+
"$type": "Keyword",
|
|
408414
|
+
"value": ","
|
|
408415
|
+
},
|
|
408416
|
+
{
|
|
408417
|
+
"$type": "Assignment",
|
|
408418
|
+
"feature": "extent_y",
|
|
408419
|
+
"operator": "=",
|
|
408420
|
+
"terminal": {
|
|
408421
|
+
"$type": "RuleCall",
|
|
408422
|
+
"rule": {
|
|
408423
|
+
"$ref": "#/rules@79"
|
|
408424
|
+
},
|
|
408425
|
+
"arguments": []
|
|
408426
|
+
}
|
|
408427
|
+
},
|
|
408428
|
+
{
|
|
408429
|
+
"$type": "Keyword",
|
|
408430
|
+
"value": ")"
|
|
408431
|
+
}
|
|
408432
|
+
],
|
|
408433
|
+
"cardinality": "?"
|
|
408434
|
+
}
|
|
408435
|
+
]
|
|
408133
408436
|
}
|
|
408134
|
-
]
|
|
408135
|
-
"cardinality": "*"
|
|
408437
|
+
]
|
|
408136
408438
|
},
|
|
408137
408439
|
{
|
|
408138
408440
|
"$type": "Keyword",
|
|
@@ -411917,6 +412219,40 @@ var WadiGrammar = () => loadedWadiGrammar ?? (loadedWadiGrammar = loadGrammarFro
|
|
|
411917
412219
|
{
|
|
411918
412220
|
"$type": "Alternatives",
|
|
411919
412221
|
"elements": [
|
|
412222
|
+
{
|
|
412223
|
+
"$type": "Group",
|
|
412224
|
+
"elements": [
|
|
412225
|
+
{
|
|
412226
|
+
"$type": "Keyword",
|
|
412227
|
+
"value": "connect"
|
|
412228
|
+
},
|
|
412229
|
+
{
|
|
412230
|
+
"$type": "Assignment",
|
|
412231
|
+
"feature": "connections",
|
|
412232
|
+
"operator": "+=",
|
|
412233
|
+
"terminal": {
|
|
412234
|
+
"$type": "RuleCall",
|
|
412235
|
+
"rule": {
|
|
412236
|
+
"$ref": "#/rules@47"
|
|
412237
|
+
},
|
|
412238
|
+
"arguments": []
|
|
412239
|
+
}
|
|
412240
|
+
},
|
|
412241
|
+
{
|
|
412242
|
+
"$type": "Assignment",
|
|
412243
|
+
"feature": "connections",
|
|
412244
|
+
"operator": "+=",
|
|
412245
|
+
"terminal": {
|
|
412246
|
+
"$type": "RuleCall",
|
|
412247
|
+
"rule": {
|
|
412248
|
+
"$ref": "#/rules@47"
|
|
412249
|
+
},
|
|
412250
|
+
"arguments": []
|
|
412251
|
+
},
|
|
412252
|
+
"cardinality": "*"
|
|
412253
|
+
}
|
|
412254
|
+
]
|
|
412255
|
+
},
|
|
411920
412256
|
{
|
|
411921
412257
|
"$type": "Assignment",
|
|
411922
412258
|
"feature": "walls",
|
|
@@ -416489,6 +416825,9 @@ var HARD_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
416489
416825
|
"raw",
|
|
416490
416826
|
"door",
|
|
416491
416827
|
"window",
|
|
416828
|
+
// room-block statement leader — hard so the space-separated `connect A B` name
|
|
416829
|
+
// list stops at the next `connect` (repeatable) instead of swallowing it.
|
|
416830
|
+
"connect",
|
|
416492
416831
|
// top-level / house-body leaders
|
|
416493
416832
|
"house",
|
|
416494
416833
|
"import",
|
|
@@ -416502,6 +416841,7 @@ var HARD_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
416502
416841
|
"var",
|
|
416503
416842
|
"point",
|
|
416504
416843
|
"grid",
|
|
416844
|
+
"guides",
|
|
416505
416845
|
"configurator",
|
|
416506
416846
|
"group",
|
|
416507
416847
|
// configurator / grid internals (rule leaders)
|
|
@@ -416680,6 +417020,9 @@ function room(r2) {
|
|
|
416680
417020
|
if (h !== void 0) o.height = h;
|
|
416681
417021
|
if (Object.keys(walls).length) o.walls = walls;
|
|
416682
417022
|
if (items.length) o.items = items;
|
|
417023
|
+
if (r2.connections.length) {
|
|
417024
|
+
o.connections = [...new Set(r2.connections.map(unquote2))];
|
|
417025
|
+
}
|
|
416683
417026
|
applyCommon(o, formulas, r2);
|
|
416684
417027
|
return done(o, formulas);
|
|
416685
417028
|
}
|
|
@@ -417351,13 +417694,22 @@ function modelToHouseConfig(model, project) {
|
|
|
417351
417694
|
if (model.grids.length) {
|
|
417352
417695
|
const grids = {};
|
|
417353
417696
|
for (const g of model.grids) {
|
|
417354
|
-
|
|
417355
|
-
const
|
|
417356
|
-
|
|
417357
|
-
|
|
417358
|
-
|
|
417359
|
-
|
|
417360
|
-
|
|
417697
|
+
if (g.spacing_x !== void 0) {
|
|
417698
|
+
const gen = {
|
|
417699
|
+
spacing: [exprToValue(g.spacing_x), exprToValue(g.spacing_y)]
|
|
417700
|
+
};
|
|
417701
|
+
if (g.origin_x !== void 0) gen.origin = [exprToValue(g.origin_x), exprToValue(g.origin_y)];
|
|
417702
|
+
if (g.extent_x !== void 0) gen.extent = [Number(g.extent_x), Number(g.extent_y)];
|
|
417703
|
+
grids[g.name] = gen;
|
|
417704
|
+
} else {
|
|
417705
|
+
const line2 = (l) => {
|
|
417706
|
+
const o = { name: String(l.name), at: exprToValue(l.at) };
|
|
417707
|
+
if (l.thickness) o.thickness = exprToValue(l.thickness);
|
|
417708
|
+
if (l.role) o.role = l.role;
|
|
417709
|
+
return o;
|
|
417710
|
+
};
|
|
417711
|
+
grids[g.name] = { x: g.xlines.map(line2), y: g.ylines.map(line2) };
|
|
417712
|
+
}
|
|
417361
417713
|
}
|
|
417362
417714
|
cfg.grids = grids;
|
|
417363
417715
|
}
|
|
@@ -417549,6 +417901,13 @@ function buildRefsView(config3) {
|
|
|
417549
417901
|
const g = grids.get(id);
|
|
417550
417902
|
(axis === "x" ? g.xLines : g.yLines).push({ name: line2, value: num(val) });
|
|
417551
417903
|
}
|
|
417904
|
+
for (const [id, gg] of resolvedGeneratedGuidesForConfig(config3)) {
|
|
417905
|
+
const xLines = [];
|
|
417906
|
+
const yLines = [];
|
|
417907
|
+
for (let i2 = 0; i2 < gg.ex; i2++) xLines.push({ name: `${i2}`, value: num(gg.ox + i2 * gg.dx) });
|
|
417908
|
+
for (let j = 0; j < gg.ey; j++) yLines.push({ name: `${j}`, value: num(gg.oy + j * gg.dy) });
|
|
417909
|
+
grids.set(id, { xLines, yLines, generated: true });
|
|
417910
|
+
}
|
|
417552
417911
|
return {
|
|
417553
417912
|
variables,
|
|
417554
417913
|
points,
|
|
@@ -419542,6 +419901,7 @@ function roomSideOpenToWeather(rects, rx, ry, rw, rl, side2, wallT) {
|
|
|
419542
419901
|
}
|
|
419543
419902
|
|
|
419544
419903
|
// ../editor/src/lint/constraints/geometry.ts
|
|
419904
|
+
init_openingAnchor();
|
|
419545
419905
|
init_vocab();
|
|
419546
419906
|
var ALL_SIDES = ["north", "south", "east", "west"];
|
|
419547
419907
|
function declaredSides(walls) {
|
|
@@ -419597,8 +419957,10 @@ function collectOpeningSegs(objs) {
|
|
|
419597
419957
|
for (const side2 of ALL_SIDES) {
|
|
419598
419958
|
const ops = walls[side2]?.openings;
|
|
419599
419959
|
if (!Array.isArray(ops)) continue;
|
|
419960
|
+
const wallLen = side2 === "north" || side2 === "south" ? rw : rl;
|
|
419600
419961
|
for (const op of ops) {
|
|
419601
|
-
const
|
|
419962
|
+
const w = num2(op.width);
|
|
419963
|
+
const off = openingStartOffset(op.anchor, num2(op.offset), w, wallLen);
|
|
419602
419964
|
const owner = `room ${objLabel(o)} ${side2}`;
|
|
419603
419965
|
const label = openLabel(op);
|
|
419604
419966
|
if (side2 === "north") out.push({ horiz: true, at: ry, lo: rx + off, hi: rx + off + w, owner, label });
|
|
@@ -419614,8 +419976,10 @@ function collectOpeningSegs(objs) {
|
|
|
419614
419976
|
const horiz = Math.abs(sy - ey) < 1, vert = Math.abs(sx - ex) < 1;
|
|
419615
419977
|
if (!horiz && !vert) continue;
|
|
419616
419978
|
const start = horiz ? Math.min(sx, ex) : Math.min(sy, ey);
|
|
419979
|
+
const wallLen = horiz ? Math.abs(ex - sx) : Math.abs(ey - sy);
|
|
419617
419980
|
for (const op of ops) {
|
|
419618
|
-
const
|
|
419981
|
+
const w = num2(op.width);
|
|
419982
|
+
const off = openingStartOffset(op.anchor, num2(op.offset), w, wallLen);
|
|
419619
419983
|
out.push({ horiz, at: horiz ? sy : sx, lo: start + off, hi: start + off + w, owner: `wall ${objLabel(o)}`, label: openLabel(op) });
|
|
419620
419984
|
}
|
|
419621
419985
|
}
|
|
@@ -420007,7 +420371,7 @@ var C6 = {
|
|
|
420007
420371
|
doc: {
|
|
420008
420372
|
statement: "Two openings (doors/windows) cut into the **same physical wall** must not overlap along it. This includes openings that belong to **two different rooms sharing a boundary wall**.",
|
|
420009
420373
|
rationale: "Each opening is a boolean-subtract from the wall. Overlapping spans merge into one ragged hole (or fight over the same brick), which is never what you meant \u2014 and on a shared wall it silently punches a bigger gap than either room's plan shows.",
|
|
420010
|
-
fix: "Offset or narrow one opening so the spans are disjoint.
|
|
420374
|
+
fix: "Offset or narrow one opening so the spans are disjoint. An opening's span is its resolved `[offset, offset+width]` along the wall \u2014 the `from start|center|end` anchor is honoured (its offset is converted to a start-based position first, exactly as the renderer does)."
|
|
420011
420375
|
},
|
|
420012
420376
|
check(ctx) {
|
|
420013
420377
|
const { findings, report } = makeReport("C6", "error");
|
|
@@ -420046,6 +420410,20 @@ var C6 = {
|
|
|
420046
420410
|
] }
|
|
420047
420411
|
})
|
|
420048
420412
|
])
|
|
420413
|
+
},
|
|
420414
|
+
{
|
|
420415
|
+
name: "anchored openings that only READ as disjoint once anchors resolve",
|
|
420416
|
+
// Hall south wall is 300 wide. D1 = start [40,80]; W1 = `from end` offset 40
|
|
420417
|
+
// → start-offset 300-40-40=220 → [220,260]. Disjoint. A start-only reader
|
|
420418
|
+
// would place W1 at [40,80] and wrongly flag an overlap.
|
|
420419
|
+
config: house5([
|
|
420420
|
+
roomWith({
|
|
420421
|
+
south: { openings: [
|
|
420422
|
+
{ kind: "door", name: "D1", offset: 40, width: 40, anchor: "start" },
|
|
420423
|
+
{ kind: "window", name: "W1", offset: 40, width: 40, anchor: "end" }
|
|
420424
|
+
] }
|
|
420425
|
+
})
|
|
420426
|
+
])
|
|
420049
420427
|
}
|
|
420050
420428
|
],
|
|
420051
420429
|
fail: [
|
|
@@ -420064,6 +420442,24 @@ var C6 = {
|
|
|
420064
420442
|
]),
|
|
420065
420443
|
expect: { count: 1, level: "error", messageIncludes: "overlap" }
|
|
420066
420444
|
},
|
|
420445
|
+
{
|
|
420446
|
+
name: "anchored openings that only COLLIDE once anchors resolve",
|
|
420447
|
+
// D1 = start [40,80]; W1 = `from end` offset 230 → start-offset
|
|
420448
|
+
// 300-40-230=30 → [30,70], overlapping D1. A start-only reader would place
|
|
420449
|
+
// W1 at [230,270] and miss the collision entirely.
|
|
420450
|
+
config: house5([
|
|
420451
|
+
roomWith({
|
|
420452
|
+
north: {},
|
|
420453
|
+
east: {},
|
|
420454
|
+
west: {},
|
|
420455
|
+
south: { openings: [
|
|
420456
|
+
{ kind: "door", name: "D1", offset: 40, width: 40, anchor: "start" },
|
|
420457
|
+
{ kind: "window", name: "W1", offset: 230, width: 40, anchor: "end" }
|
|
420458
|
+
] }
|
|
420459
|
+
})
|
|
420460
|
+
]),
|
|
420461
|
+
expect: { count: 1, level: "error", messageIncludes: "overlap" }
|
|
420462
|
+
},
|
|
420067
420463
|
{
|
|
420068
420464
|
name: "openings on a SHARED wall between two rooms overlap",
|
|
420069
420465
|
config: house5([
|
|
@@ -427706,8 +428102,246 @@ function roofSeg(start, end, width) {
|
|
|
427706
428102
|
return { type: "roof", name: "Roof", segments: [{ id: "seg0", start, end, width }] };
|
|
427707
428103
|
}
|
|
427708
428104
|
|
|
428105
|
+
// ../editor/src/lint/constraints/c11_declared_connection.ts
|
|
428106
|
+
init_vocab();
|
|
428107
|
+
var OPP2 = { north: "south", south: "north", east: "west", west: "east" };
|
|
428108
|
+
var rectOf = (o) => ({ x: num2(o.x), y: num2(o.y), w: num2(o.width), l: num2(o.length) });
|
|
428109
|
+
function sharedWall(a, b, tol) {
|
|
428110
|
+
const ax1 = a.x + a.w, ay1 = a.y + a.l, bx1 = b.x + b.w, by1 = b.y + b.l;
|
|
428111
|
+
const yLo = Math.max(a.y, b.y), yHi = Math.min(ay1, by1);
|
|
428112
|
+
const xLo = Math.max(a.x, b.x), xHi = Math.min(ax1, bx1);
|
|
428113
|
+
if (yHi - yLo > 0) {
|
|
428114
|
+
if (Math.abs(ax1 - b.x) <= tol) return { side: "east", lo: yLo, hi: yHi };
|
|
428115
|
+
if (Math.abs(bx1 - a.x) <= tol) return { side: "west", lo: yLo, hi: yHi };
|
|
428116
|
+
}
|
|
428117
|
+
if (xHi - xLo > 0) {
|
|
428118
|
+
if (Math.abs(ay1 - b.y) <= tol) return { side: "south", lo: xLo, hi: xHi };
|
|
428119
|
+
if (Math.abs(by1 - a.y) <= tol) return { side: "north", lo: xLo, hi: xHi };
|
|
428120
|
+
}
|
|
428121
|
+
return null;
|
|
428122
|
+
}
|
|
428123
|
+
function doorOnShared(room5, side2, lo, hi) {
|
|
428124
|
+
const walls = room5.walls;
|
|
428125
|
+
if (!walls || Array.isArray(walls) || typeof walls !== "object") return false;
|
|
428126
|
+
const ops = walls[side2]?.openings;
|
|
428127
|
+
if (!Array.isArray(ops)) return false;
|
|
428128
|
+
const base = side2 === "east" || side2 === "west" ? num2(room5.y) : num2(room5.x);
|
|
428129
|
+
for (const op of ops) {
|
|
428130
|
+
if (op?.kind !== "door") continue;
|
|
428131
|
+
const dLo = base + num2(op.offset), dHi = dLo + num2(op.width);
|
|
428132
|
+
if (Math.min(dHi, hi) - Math.max(dLo, lo) > 0) return true;
|
|
428133
|
+
}
|
|
428134
|
+
return false;
|
|
428135
|
+
}
|
|
428136
|
+
function wallPresent(room5, side2) {
|
|
428137
|
+
const walls = room5.walls;
|
|
428138
|
+
if (walls == null) return true;
|
|
428139
|
+
if (Array.isArray(walls)) return walls.includes(side2);
|
|
428140
|
+
if (typeof walls === "object") return side2 in walls;
|
|
428141
|
+
return true;
|
|
428142
|
+
}
|
|
428143
|
+
var C11 = {
|
|
428144
|
+
id: "C11",
|
|
428145
|
+
title: "A declared connection must overlap on a wall and be passable (door or open)",
|
|
428146
|
+
level: "error",
|
|
428147
|
+
doc: {
|
|
428148
|
+
statement: "For every `connect`ion a room declares, the two rooms must **overlap on a wall** (not necessarily the whole wall), and that overlap must be **passable**: either a **door** lies in it, or the wall is **left off both rooms** (an open passage).",
|
|
428149
|
+
rationale: "A connection is a FUNCTIONAL requirement \u2014 `Living` opens into `Kitchen`. It is design intent, not geometry (the renderer never draws it), so this constraint is what verifies the intent is physically realized. It fails two ways: the rooms' walls don't overlap at all, or they overlap but a solid wall (present on either room, no door in the overlap) blocks the way. No door is ever generated \u2014 a room authors its own openings, or omits the shared wall to leave the rooms open to each other.",
|
|
428150
|
+
fix: "Overlap the two rooms on a wall, then EITHER put a door in the overlap (on either room), OR omit that wall on both:\n\n```wdl\n// door in the shared wall\nroom Living at (\u2026) size (\u2026) { connect Kitchen wall east { door D at 80 size (40,210) } }\nroom Kitchen at (\u2026) size (\u2026)\n\n// open passage \u2014 neither room walls the shared side\nroom Living at (\u2026) size (\u2026) { connect Kitchen wall north south west }\nroom Kitchen at (\u2026) size (\u2026) { wall north south east }\n```"
|
|
428151
|
+
},
|
|
428152
|
+
check(ctx) {
|
|
428153
|
+
const { findings, report } = makeReport("C11", "error");
|
|
428154
|
+
const config3 = ctx.resolved;
|
|
428155
|
+
const floors = config3.floors ?? [];
|
|
428156
|
+
const tol = ctx.defaults.wall_thickness + 1;
|
|
428157
|
+
for (const fl of floors) {
|
|
428158
|
+
const fnum = num2(fl.floor_number);
|
|
428159
|
+
const rooms = activeObjects2(fl).filter((o) => o.type === "room");
|
|
428160
|
+
const byName = new Map(rooms.map((r2) => [String(r2.name), r2]));
|
|
428161
|
+
const seen = /* @__PURE__ */ new Set();
|
|
428162
|
+
for (const a of rooms) {
|
|
428163
|
+
const conns = Array.isArray(a.connections) ? a.connections : [];
|
|
428164
|
+
for (const raw of conns) {
|
|
428165
|
+
const name = String(raw);
|
|
428166
|
+
const key = [String(a.name), name].sort().join(" \u2194 ");
|
|
428167
|
+
if (seen.has(key)) continue;
|
|
428168
|
+
seen.add(key);
|
|
428169
|
+
const b = byName.get(name);
|
|
428170
|
+
if (!b) {
|
|
428171
|
+
report(
|
|
428172
|
+
`${cap(floorLabel(fl))}: room ${objLabel(a)} declares a connection to "${name}", which is not an active room on this floor.`,
|
|
428173
|
+
{ floor: fnum, where: objLabel(a) }
|
|
428174
|
+
);
|
|
428175
|
+
continue;
|
|
428176
|
+
}
|
|
428177
|
+
const sw = sharedWall(rectOf(a), rectOf(b), tol);
|
|
428178
|
+
if (!sw) {
|
|
428179
|
+
report(
|
|
428180
|
+
`${cap(floorLabel(fl))}: ${objLabel(a)} and ${objLabel(b)} are connected but their walls do not overlap (not adjacent) \u2014 no wall they could pass through.`,
|
|
428181
|
+
{ floor: fnum, where: objLabel(a) }
|
|
428182
|
+
);
|
|
428183
|
+
continue;
|
|
428184
|
+
}
|
|
428185
|
+
const bSide = OPP2[sw.side];
|
|
428186
|
+
const wallThere = wallPresent(a, sw.side) || wallPresent(b, bSide);
|
|
428187
|
+
const doorThere = doorOnShared(a, sw.side, sw.lo, sw.hi) || doorOnShared(b, bSide, sw.lo, sw.hi);
|
|
428188
|
+
if (wallThere && !doorThere) {
|
|
428189
|
+
report(
|
|
428190
|
+
`${cap(floorLabel(fl))}: ${objLabel(a)} and ${objLabel(b)} are connected and their walls overlap, but no door lies in the overlap \u2014 add a door there, or omit that wall on BOTH rooms for an open passage.`,
|
|
428191
|
+
{ floor: fnum, where: objLabel(a) }
|
|
428192
|
+
);
|
|
428193
|
+
}
|
|
428194
|
+
}
|
|
428195
|
+
}
|
|
428196
|
+
}
|
|
428197
|
+
return findings;
|
|
428198
|
+
},
|
|
428199
|
+
fixtures: {
|
|
428200
|
+
pass: [
|
|
428201
|
+
{
|
|
428202
|
+
name: "adjacent rooms with a door on the shared wall",
|
|
428203
|
+
config: house10([
|
|
428204
|
+
{
|
|
428205
|
+
type: "room",
|
|
428206
|
+
name: "Living",
|
|
428207
|
+
x: 0,
|
|
428208
|
+
y: 0,
|
|
428209
|
+
width: 200,
|
|
428210
|
+
length: 200,
|
|
428211
|
+
connections: ["Kitchen"],
|
|
428212
|
+
walls: { east: { openings: [{ kind: "door", name: "D1", offset: 80, width: 40 }] } }
|
|
428213
|
+
},
|
|
428214
|
+
{ type: "room", name: "Kitchen", x: 200, y: 0, width: 200, length: 200 }
|
|
428215
|
+
])
|
|
428216
|
+
},
|
|
428217
|
+
{
|
|
428218
|
+
name: "door authored on the neighbour's side of the shared wall",
|
|
428219
|
+
config: house10([
|
|
428220
|
+
{ type: "room", name: "Living", x: 0, y: 0, width: 200, length: 200, connections: ["Kitchen"] },
|
|
428221
|
+
{
|
|
428222
|
+
type: "room",
|
|
428223
|
+
name: "Kitchen",
|
|
428224
|
+
x: 200,
|
|
428225
|
+
y: 0,
|
|
428226
|
+
width: 200,
|
|
428227
|
+
length: 200,
|
|
428228
|
+
walls: { west: { openings: [{ kind: "door", name: "D1", offset: 80, width: 40 }] } }
|
|
428229
|
+
}
|
|
428230
|
+
])
|
|
428231
|
+
},
|
|
428232
|
+
{
|
|
428233
|
+
name: "open passage \u2014 neither room walls the shared side",
|
|
428234
|
+
config: house10([
|
|
428235
|
+
{
|
|
428236
|
+
type: "room",
|
|
428237
|
+
name: "Living",
|
|
428238
|
+
x: 0,
|
|
428239
|
+
y: 0,
|
|
428240
|
+
width: 200,
|
|
428241
|
+
length: 200,
|
|
428242
|
+
connections: ["Kitchen"],
|
|
428243
|
+
walls: { north: {}, south: {}, west: {} }
|
|
428244
|
+
// no east wall (open toward Kitchen)
|
|
428245
|
+
},
|
|
428246
|
+
{
|
|
428247
|
+
type: "room",
|
|
428248
|
+
name: "Kitchen",
|
|
428249
|
+
x: 200,
|
|
428250
|
+
y: 0,
|
|
428251
|
+
width: 200,
|
|
428252
|
+
length: 200,
|
|
428253
|
+
walls: { north: {}, south: {}, east: {} }
|
|
428254
|
+
// no west wall (open toward Living)
|
|
428255
|
+
}
|
|
428256
|
+
])
|
|
428257
|
+
},
|
|
428258
|
+
{
|
|
428259
|
+
name: "partial wall overlap with a door in the overlap",
|
|
428260
|
+
config: house10([
|
|
428261
|
+
{
|
|
428262
|
+
type: "room",
|
|
428263
|
+
name: "Living",
|
|
428264
|
+
x: 0,
|
|
428265
|
+
y: 0,
|
|
428266
|
+
width: 200,
|
|
428267
|
+
length: 100,
|
|
428268
|
+
connections: ["Kitchen"],
|
|
428269
|
+
// overlap with Kitchen is y 0..100; door at y 30..70 sits inside it.
|
|
428270
|
+
walls: { east: { openings: [{ kind: "door", name: "D1", offset: 30, width: 40 }] } }
|
|
428271
|
+
},
|
|
428272
|
+
{ type: "room", name: "Kitchen", x: 200, y: 0, width: 200, length: 300 }
|
|
428273
|
+
])
|
|
428274
|
+
},
|
|
428275
|
+
{
|
|
428276
|
+
name: "rooms with no declared connections are not checked",
|
|
428277
|
+
config: house10([
|
|
428278
|
+
{ type: "room", name: "A", x: 0, y: 0, width: 100, length: 100 },
|
|
428279
|
+
{ type: "room", name: "B", x: 300, y: 0, width: 100, length: 100 }
|
|
428280
|
+
])
|
|
428281
|
+
}
|
|
428282
|
+
],
|
|
428283
|
+
fail: [
|
|
428284
|
+
{
|
|
428285
|
+
name: "connected rooms are not adjacent",
|
|
428286
|
+
config: house10([
|
|
428287
|
+
{ type: "room", name: "Living", x: 0, y: 0, width: 100, length: 100, connections: ["Kitchen"] },
|
|
428288
|
+
{ type: "room", name: "Kitchen", x: 300, y: 0, width: 100, length: 100 }
|
|
428289
|
+
]),
|
|
428290
|
+
expect: { count: 1, level: "error", messageIncludes: "not adjacent" }
|
|
428291
|
+
},
|
|
428292
|
+
{
|
|
428293
|
+
name: "adjacent but no door on the shared wall",
|
|
428294
|
+
config: house10([
|
|
428295
|
+
{ type: "room", name: "Living", x: 0, y: 0, width: 200, length: 200, connections: ["Kitchen"] },
|
|
428296
|
+
{ type: "room", name: "Kitchen", x: 200, y: 0, width: 200, length: 200 }
|
|
428297
|
+
]),
|
|
428298
|
+
expect: { count: 1, level: "error", messageIncludes: "no door" }
|
|
428299
|
+
},
|
|
428300
|
+
{
|
|
428301
|
+
name: "door on the shared wall is outside the shared span",
|
|
428302
|
+
config: house10([
|
|
428303
|
+
{
|
|
428304
|
+
type: "room",
|
|
428305
|
+
name: "Living",
|
|
428306
|
+
x: 0,
|
|
428307
|
+
y: 0,
|
|
428308
|
+
width: 200,
|
|
428309
|
+
length: 100,
|
|
428310
|
+
connections: ["Kitchen"],
|
|
428311
|
+
// Living's east wall runs y 0..100 and the shared span with Kitchen is
|
|
428312
|
+
// y 0..100 — but the door sits at y 120..160, off the shared segment.
|
|
428313
|
+
walls: { east: { openings: [{ kind: "door", name: "D1", offset: 120, width: 40 }] } }
|
|
428314
|
+
},
|
|
428315
|
+
{ type: "room", name: "Kitchen", x: 200, y: 0, width: 200, length: 300 }
|
|
428316
|
+
]),
|
|
428317
|
+
expect: { count: 1, level: "error", messageIncludes: "no door" }
|
|
428318
|
+
},
|
|
428319
|
+
{
|
|
428320
|
+
name: "one room walls the shared side (no door) while the other leaves it open \u2014 still blocked",
|
|
428321
|
+
config: house10([
|
|
428322
|
+
// Living has all four walls by default (east present, no door).
|
|
428323
|
+
{ type: "room", name: "Living", x: 0, y: 0, width: 200, length: 200, connections: ["Kitchen"] },
|
|
428324
|
+
// Kitchen omits its west wall, but Living's solid east wall still blocks.
|
|
428325
|
+
{ type: "room", name: "Kitchen", x: 200, y: 0, width: 200, length: 200, walls: { north: {}, south: {}, east: {} } }
|
|
428326
|
+
]),
|
|
428327
|
+
expect: { count: 1, level: "error", messageIncludes: "no door" }
|
|
428328
|
+
},
|
|
428329
|
+
{
|
|
428330
|
+
name: "connection to a room that does not exist",
|
|
428331
|
+
config: house10([
|
|
428332
|
+
{ type: "room", name: "Living", x: 0, y: 0, width: 100, length: 100, connections: ["Ghost"] }
|
|
428333
|
+
]),
|
|
428334
|
+
expect: { count: 1, level: "error", messageIncludes: "not an active room" }
|
|
428335
|
+
}
|
|
428336
|
+
]
|
|
428337
|
+
}
|
|
428338
|
+
};
|
|
428339
|
+
function house10(objects) {
|
|
428340
|
+
return { floors: [{ floor_number: 1, name: "Ground", slab_thickness: 0, objects }] };
|
|
428341
|
+
}
|
|
428342
|
+
|
|
427709
428343
|
// ../editor/src/lint/constraints/index.ts
|
|
427710
|
-
var CONSTRAINTS = [C1, C2, C3, C4, C5, C6, C7, C8, C9, C10];
|
|
428344
|
+
var CONSTRAINTS = [C1, C2, C3, C4, C5, C6, C7, C8, C9, C10, C11];
|
|
427711
428345
|
function allConstraints() {
|
|
427712
428346
|
const primitive = allNodes().flatMap((n3) => n3.constraints ?? []);
|
|
427713
428347
|
return [...CONSTRAINTS, ...primitive];
|
|
@@ -429335,15 +429969,23 @@ init_expand();
|
|
|
429335
429969
|
init_resolve();
|
|
429336
429970
|
init_config();
|
|
429337
429971
|
function buildGridOverlay(config3) {
|
|
429338
|
-
const
|
|
429339
|
-
|
|
429972
|
+
const cfg = config3;
|
|
429973
|
+
const grids = resolvedGridsForConfig(cfg);
|
|
429974
|
+
const generated = resolvedGeneratedGuidesForConfig(cfg);
|
|
429975
|
+
if (grids.size === 0 && generated.size === 0) return void 0;
|
|
429340
429976
|
const x = [];
|
|
429341
429977
|
const y = [];
|
|
429978
|
+
const multi = grids.size + generated.size > 1;
|
|
429342
429979
|
for (const [id, g] of grids) {
|
|
429343
|
-
const prefix =
|
|
429980
|
+
const prefix = multi ? `${id}.` : "";
|
|
429344
429981
|
for (const [name, pos] of g.x) x.push({ name: prefix + name, pos });
|
|
429345
429982
|
for (const [name, pos] of g.y) y.push({ name: prefix + name, pos });
|
|
429346
429983
|
}
|
|
429984
|
+
for (const [id, gg] of generated) {
|
|
429985
|
+
const prefix = multi ? `${id}.` : "";
|
|
429986
|
+
for (let i2 = 0; i2 < gg.ex; i2++) x.push({ name: `${prefix}x${i2}`, pos: gg.ox + i2 * gg.dx });
|
|
429987
|
+
for (let j = 0; j < gg.ey; j++) y.push({ name: `${prefix}y${j}`, pos: gg.oy + j * gg.dy });
|
|
429988
|
+
}
|
|
429347
429989
|
return x.length || y.length ? { x, y } : void 0;
|
|
429348
429990
|
}
|
|
429349
429991
|
|
|
@@ -432475,11 +433117,11 @@ var DOCS = {
|
|
|
432475
433117
|
},
|
|
432476
433118
|
"dsl": {
|
|
432477
433119
|
"title": "The Wadi DSL (.wdl) \u2014 syntax reference",
|
|
432478
|
-
"body": '# The Wadi DSL (`.wdl`) \u2014 authoring reference\n\nYou author houses in the **Wadi DSL** \u2014 a small, formal language (`.wdl`) that\ncompiles to a resolved `.wadi` (`house_config.json`). The DSL is **complete**:\nevery object type in the model has first-class syntax, so you rarely need the\n`raw` escape. Authoring the DSL is more direct and less error-prone than writing\nJSON \u2014 the grammar enforces structure, and `check.sh` reports parse errors with\nline:col.\n\n**This file is the SYNTAX reference.** The *semantics* live in the other\nreferences and apply unchanged \u2014 read them:\n\n- `coordinate-system.md` \u2014 X\u2192right, **Y\u2192DOWN**, Z\u2192up; **10 units = 1 ft**;\n the **centreline** convention. The #1 source of mistakes.\n- `conventions.md` \u2014 the **structural coding conventions** (`check.sh` enforces\n them): plinth-floor height must match the plinth block, rooms must wall every\n exterior side, a no-slab floor must set `slab_thickness 0`.\n- `parametric-conventions.md` \u2014 the grid-first recipe for reusable templates.\n- `roof-v2-guide.md` \u2014 roof segments, hip vs gable, trusses, joints.\n- `data-model.md` \u2014 the underlying `.wadi` schema (what the DSL compiles to; also\n the field reference for the `raw` escape).\n\n## The loop\n\n1. Write / edit `house.wdl` \u2014 the **single shared source** (you and the human\n co-edit it; the app\'s DSL previewer renders it live). You never produce a `.wadi`.\n2. `wadi-skill/architect/scripts/check.sh house.wdl` \u2014 runs the DSL compiler +\n validator (schema + wall/roof geometry) against a **throwaway temp** just for\n feedback; fix any reported error and re-run.\n3. `preview.sh house.wdl` \u2192 read the PNGs (plans / elevations / roof) to check your\n work. (It also compiles to a throwaway temp \u2014 no persistent `.wadi`.)\n\n## Skeleton\n\n```wdl\nhouse MyHouse {\n convention center // ALWAYS use center (wall-centreline coords)\n units feet_inches per_unit 10 // 10 project units = 1 ft\n site { plot (WIDTH, LENGTH) ref (0, 0) }\n defaults { floor_height 120 wall_height 108 slab_thickness 8 wall_thickness 8 }\n\n // parametric core (optional): var, point, grid, configurator\n // component / layer declarations (optional)\n floor 0 "Plinth" { \u2026 } // floors stack in source order (0 = plinth)\n floor 1 "Ground Floor" { \u2026 }\n floor 2 "Loft" { roof \u2026 } // roof lives ALONE on its own top floor\n}\n```\n\nNumbers are **project units** (feet \xD7 10 by default). Names after `house`,\n`room`, `pillar`, `var`, `point`, `grid`, and `use`/`component` are bare\nidentifiers (no spaces); names introduced with the `name` keyword are quoted\nstrings.\n\n## Parametric core (domain-neutral)\n\n```wdl\nvar wallT = 8 // a knob; may reference other vars\nvar pilInset = (pillarW - wallT) / 2\n\npoint House { x = 420, y = 470 } // reference as House.x / House.W / House.L\n // (.W = x, .L = y \u2014 a point doubles as a size)\n\ngrid main { // named wall centrelines; publishes main.x1 / main.yA\n x: 1 @ wallT / 2, 2 @ House.W / 2, 3 @ House.W - wallT / 2\n y: A @ wallT / 2, B @ House.L / 2, C @ House.L - wallT / 2\n}\n// each line may add: \u2026 @ <expr> thick <expr> role structural|planning\n\nconfigurator { // the owner-facing template you author\n title "Configure your home" // panel heading (optional)\n note "Everything re-flows to fit." // panel subtitle (optional)\n\n slider pillarW "Column size" ft [8 .. 14 step 1] note "help text" // trailing note optional\n number ceiling "Ceiling height" ft\n toggle has_loft "Add a loft"\n select roof_style "Roof style" { Flat = 0, Shed = 1, Gable = 2, Hip = 3 }\n select floorH "Ceiling height" { "9 ft" = 90, "10 ft (std)" = 100 } // labels can be quoted strings\n\n group "Plot" note "about this section" { // sections the panel; label only\n slider W "Plot width" ft [340 .. 520 step 10]\n }\n}\n```\n\nEvery knob binds to a `var` by name (`target`). To expose a plot dimension, model\nit as a `var` and reference it from the point (`point House { x = W }`), then bind\nthe knob to `W` \u2014 knobs target vars, never a point field like `House.W`.\n\n**Formulas are automatic.** Any geometry number can be a formula \u2014 just write the\nexpression instead of a literal (`at (main.x1, main.yA)`, `size (House.W/2, 200)`).\nOperators: `+ - * /`, unary `-`, parentheses, and the functions\n`min max clamp round floor ceil abs`. References: a `var`, a `point`\n(`House.W`), or a grid line (`main.x3 - main.x1`). No comparison operators \u2014 gate\nthings with the `min/abs` idiom (see `enabled` below).\n\n## Common attribute tail (every object)\n\nAfter an object\'s geometry, in THIS order, any of:\n\n```\n\u2026 z_offset <expr> enabled <expr> layer "id" [material "id"]\n```\n\n- `enabled <expr>` \u2014 the on/off switch. A `0`/`false` value hides the object. To\n gate on a configurator variable, use a 0/1 formula:\n `enabled 1 - min(1, abs(roof_style - 3))` renders the object only when\n `roof_style == 3`. (This is how one template carries several roofs and shows\n only the chosen one.)\n- `z_offset <expr>` \u2014 lift above the floor base (split levels).\n- `material "id"` \u2014 only on plinth / ground / room / wall / staircase / kitchen /\n roof.\n\n## Objects \u2014 structure & envelope\n\n```wdl\nslab [name "N"] at (x,y) size (w,l) [thickness <t>] // floor_slab\nbeam [name "N"] at (x,y) size (w,l) [height <h>]\nplinth [name "N"] at (x,y) size (w,l) height <h> // raised base (Plinth floor)\nground [name "N"] at (x,y) size (w,l) [height <h>] // terrain plane\npillar Name at (x,y) size (w,l) [height <h>] // (x,y) = TOP-LEFT corner\n```\n\n`at (x,y)` is the **TOP-LEFT CORNER** \u2014 **not the centre** \u2014 for every one of these\n(same as rooms/slabs/beams); `size (w,l)` is width \xD7 length. All accept the common tail.\n\n**Pillars catch people out here.** A column reads as "placed at a point," but `at` is\nstill its corner. To **centre a column on a point** `(cx, cy)` \u2014 a grid node, a room\ncorner \u2014 place it at **`at (cx - w/2, cy - l/2)`**, never at `(cx, cy)`. On a grid, the\n`pilInset` idiom (see `parametric-conventions.md`) does exactly this so columns sit flush.\n\n## Objects \u2014 rooms, walls & openings\n\nA room shows exactly the walls you declare. A **bare room (no `wall` lines) is\nenclosed on all four sides.** List plain walls compactly; give a wall its own line\nonly when it carries a door/window; omit a side to leave it open (verandah).\n\n```wdl\nroom Name at (x,y) size (w,l) [height <h>] [material "\u2026"] {\n wall east west north // plain walls \u2014 several in one statement\n wall south { door Main at <offset> [from start|center|end] size (w,h) [open] } // wall WITH openings\n wall west { window W at <offset> [from start|center|end] size (w,h) [sill <s>] [open] }\n item asset { \u2026 } anchor center [gap (gx,gy)] // furniture anchored inside the room\n}\n```\n\n- `wall <side>\u2026` sides are `north|south|east|west`. A `wall <side>` line may also\n add `height <h>` / `height_end <h>` (sloped).\n- `door`/`window` `at <offset>` is measured along the wall; `size (width, height)`;\n `window \u2026 sill <s>` sets the sill height; `open` = a bare hole (no leaf/glazing).\n- `from start|center|end` (default `start`) picks which end `<offset>` is measured\n from, so the opening keeps its place when the wall or room scales \u2014 no formula\n needed. `start`: offset from the wall start to the near edge (the legacy default).\n `end`: offset from the wall end to the far edge (`at 0 from end` = flush to the end).\n `center`: signed shift of the opening centre from the wall midpoint (`at 0 from\n center` = centred; the offset may be negative).\n\nA **free-standing wall** (not a room side):\n\n```wdl\nwall Name from (x1,y1) to (x2,y2) [height <h>] [height_end <h>] [facing north|\u2026] {\n \u2026 door/window openings \u2026\n}\n```\n\n- `from`/`to` are the wall\'s **centreline** endpoints; the wall is drawn as a rectangle\n `wall_thickness` wide, centred on that line.\n- **Overlap walls at corners \u2014 they do NOT auto-mitre.** Two free-standing walls that\n merely *touch* at a shared endpoint leave an unfilled square notch (\xBD\xB7`wall_thickness`)\n at the corner, because each is just a rectangle capped at its endpoint. To fill the\n corner, **extend the endpoints so the wall bodies OVERLAP** \u2014 run at least one wall\'s\n end **half the wall thickness past** the shared point (overlapping by the full thickness\n is fine and simplest). For an L of thickness 8 meeting at `(160,40)`:\n\n ```wdl\n wall H from (40, 40) to (164, 40) height 108 // ends 4 (\xBD\xB78) PAST the corner\n wall V from (160, 40) to (160, 160) height 108 // butts into H\'s overlapped body\n ```\n\n (Room walls handle their own corners; this only applies to `wall \u2026 from \u2026 to \u2026`.)\n\n## Objects \u2014 circulation & fittings\n\n```wdl\nstaircase [name "N"] at (start_x, start_y) step (rise, tread, width)\n direction north|south|east|west [climb up|down]\n [total_height <h>] [max_run <r>] [landing_depth <d>]\n [landing_thickness <t>] [turn clockwise|anticlockwise] [flight_gap <g>]\n\nkitchen [name "N"] path ((x,y), (x,y), \u2026) side left|right\n depth <d> height <h> [base_z <z>] // path points are literal numbers\n```\n\n**`climb` picks the anchor + z direction.** Prefer **`climb up`** \u2014 the intuitive way:\n\n- Put the stair on the **LOWER** floor it rises FROM. `at (x,y)` is the **bottom** step\'s\n near corner on that floor; `direction` is the **ascent** direction; the flight climbs UP.\n- `total_height` is the **rise** to the next level (omit \u2192 this floor\'s own height).\n- `max_run` caps a flight\'s run; exceed it and the stair auto-splits into switchback\n flights with turn landings (`landing_depth`/`turn`/`flight_gap` tune the switchback).\n\n```wdl\nfloor 1 "Ground Floor" height 116 {\n slab at (\u2026) size (\u2026)\n staircase name "Stair" at (212, 64) step (7, 11, 44) // bottom = this floor\n direction south climb up // ascends south to the floor above\n}\n```\n\n`climb down` is the legacy mode (**DEFAULT** for older configs): put the stair on the\n**UPPER** destination floor; `at (x,y)` is the **top** connection, `direction` is the\ndescent, and `total_height` defaults to the floor-below\'s height. New designs should use\n`climb up`.\n\n// three ways to name the GLB, in order of preference:\nitem [name "N"] f."sofa" // 1. from an imported module (see Imports)\nitem [name "N"] "sofa" // 2. a same-file / bare-imported `asset` id\nitem [name "N"] asset { id "sofa" src "\u2026/sofa.glb" dims (w,h,d) [category "\u2026"] } // 3. inline one-off\n at (x,y) [rotation <deg>] [scale <s>]\n [anchor_to "RoomName" anchor center gap (gx,gy)]\n```\n\nPrefer the module form (`item f."bed_double"`) \u2014 `import "std-furniture" as f`\nonce and every piece is a short id, no URLs. The bare form (`item "sofa"`) needs\na matching top-level `asset "sofa" \u2026` in the file (or a bare `import`). The inline\n`asset { \u2026 }` block is only for a one-off GLB not in any pack. All three produce\nthe identical `{id,src,dims}` downstream. Furniture `dims` are the real-world size\nin **metres** `(width, height, depth)`; `src` is a GLB URL (an unreachable GLB\nshows a placeholder box, never a blank). `anchor` is one of `top-left top-center\ntop-right center-left center center-right bottom-left bottom-center bottom-right`.\n\n**Orientation \u2014 this is how you point furniture the right way.** A piece\'s FRONT\n(the side you sit at / the doors / the open side) faces a known compass direction\nper its `rotation` (degrees):\n\n| `rotation` | front faces |\n|---|---|\n| `0` | **South** (the plot front / entrance side, +Y) |\n| `90` | East |\n| `180` | North |\n| `270` | West |\n\nSo a sofa against the NORTH wall (facing into the room, i.e. south) is `rotation\n0`; against the SOUTH wall (facing north) it\'s `rotation 180`; against the WEST\nwall (facing east) `rotation 90`. The floor plan (`wadi_preview plans`) draws a\nsmall triangle on each piece\'s front edge so you can verify the way it points;\nfor a definitive 3D check use `wadi_capture_3d({ room: "\u2026" })` (first-person from\ninside the room).\n\n**Anchoring auto-orients.** When you `anchor` a piece to a wall and DON\'T give a\n`rotation`, it automatically faces away from that wall, into the room \u2014 `anchor\ntop-center` \u2192 faces south, `bottom-center` \u2192 north, `center-left` \u2192 east,\n`center-right` \u2192 west (a corner uses its north/south edge). So `item f."bed_double"\nanchor top-center` needs no rotation. An explicit `rotation` always overrides,\nand the derived value is written into the resolved model, so the plan notch and\nthe 3D view show it \u2014 anchoring never changes facing silently.\n\n## Objects \u2014 GLB models (`model`)\n\nA `model` places a **GLB at real metre scale** and manipulates it through a **rig**\nof named-node operations. It is distinct from `item` (catalog furniture): `model`\ntargets the GLB\'s internal node graph, so you can hide, move, recolour, or array\nsub-parts of one asset.\n\n```wdl\nmodel [name "N"] <asset> at (x,y) [rotation <deg>] [scale <s>] {\n translate "nodeName" (x,y,z) // move a named node\n rotate "nodeName" (x,y,z) // rotate it (degrees)\n scale "nodeName" (x,y,z) // scale it\n visible "nodeName" false // hide a node\n material "nodeName" color "#rrggbb" // recolour a node\n array "nodeName" count N step (dx,dy,dz) { \u2026 nested rig ops \u2026 }\n}\n```\n\n`<asset>` names the GLB the same three ways as `item` (module `ns."id"`, bare\n`"id"`, or an inline `asset { \u2026 }` block). The rig ops run against the GLB\'s named\nnodes; `array` replicates a node `count` times along `step`, and its nested block\napplies further ops per copy. Use `model` for a rigged mechanism (a fan, a louvre\nbank, a spiral of balusters); use `item` for a plain piece of furniture.\n\n## Imports & modules (reusable `.wdl` libraries)\n\nA `.wdl` file can be a **module** \u2014 top-level declarations (no `house` needed) \u2014\nthat another file `import`s. Two bundled ones: `std-furniture` (asset pack \u2192\n`item ns."id"`) and `konkan/base` (goal-tagged component pack \u2192 `use ns.Comp`;\nStairwell, Verandah, Otla, Bathroom, Kitchen, TulsiVrindavan, Parapet). The\n`konkan_cottage` example (`wadi_examples`) assembles a whole house from both.\n\n```wdl\nhouse Home {\n import "std-furniture" as f // aliased: refer to its assets as f."<id>"\n // import "std-furniture" // bare: its ids drop into scope for item "<id>"\n floor 1 "G" slab_thickness 0 {\n room Bed at (20,20) size (160,200) { wall north east south west\n item f."bed_double" anchor center }\n }\n}\n```\n\nA module file itself is just top-level `asset` and `component` decls:\n\n```wdl\n// my-furniture.wdl \u2014 a house-less module (a reusable library)\nasset "daybed" src "https://\u2026/daybed.glb" dims (1.8, 0.4, 0.9) name "Daybed" category "Living"\n```\n\nOver MCP, `wadi_modules` lists importable modules and `wadi_module "<name>"`\nshows a module\'s asset ids + dimensions (filter with a `query`). Import refs\nresolve by name against the bundled `std-*` packs (a local `modules/` search\npath and git refs come later).\n\n## Objects \u2014 roof (one object; flat / shed / gable / hip)\n\nThe roof lives ALONE on its own top floor and you never set its Z (see\n`roof-v2-guide.md`). `endpoint`: `closed` = hip triangle, `open` = gable end-wall.\n\n```wdl\nroof [name "N"] pitched|shed|flat\n [endpoint open|closed]\n [slope angle <deg> | slope height <ridge_h>] // symmetric pitch (one value)\n [slope angle (<left>, <right>)] // asymmetric (saltbox) gable \u2014 angle pair\n [overhang <o>] [slab_thickness <t>] [parapet <h> x <t>] [gable_wall_thickness <t>] {\n segment "id" from (x,y) to (x,y) width <w>\n [high_side left|right] // shed only\n [start_endpoint open|closed] [end_endpoint open|closed]\n [hip_setback (a,b)] [gable_overhang (a,b)] [hip_ridge_extension (a,b)]\n [overhang <o>] // uniform eave, all four sides\n [overhang_start <o>] [overhang_end <o>] // per-side along the axis (shed;\n // on a gable end = gable_overhang)\n [overhang_low <o>] [overhang_high <o>] // SHED eaves (down-slope / up-slope)\n [overhang_left <o>] [overhang_right <o>] // PITCHED eaves (left / right of ridge)\n [tie_beams N]\n truss "segId" fink|mono_pitch at (pos, pos, \u2026)\n }\n```\n\nSegment `from`/`to`/`width` and the `hip_setback`/\u2026 values accept formulas, so a\nroof scales with the plot (e.g. `width House.W`, `hip_setback (Verandah.L, Padvi.L)`).\n\n**Per-side overhang (cantilever one edge).** `overhang <o>` sets a uniform eave on\nall four sides. Any sloping roof can override a side independently \u2014 each defaults to\n`overhang`. **Along the axis:** `overhang_start` / `overhang_end` (on a shed, or a\ngable open end \u2014 there they\'re the same as `gable_overhang`; a hip end is geometric,\ntuned via `hip_setback`). **Eaves:** `overhang_low` / `overhang_high` on a **shed**\n(down-slope / up-slope); `overhang_left` / `overhang_right` on a **pitched** roof\n(the two eaves either side of the ridge). A bigger eave overhang also drops that\neave\'s edge along the same pitch, so the slope stays planar. (Per-eave on a *pitched*\nroof is single-segment only \u2014 on a multi-segment roof the eaves share one height so\njoints line up.)\n\n**Asymmetric gable (saltbox) \u2014 an angle pair.** A pitched roof\'s `slope` gives both\nfaces the same pitch (a symmetric gable). To make the two sides different, give\n`slope angle` a **pair** instead of one value \u2014 `slope angle (45, 25)`. A single value\nis symmetric; a pair is asymmetric, and that\'s the whole distinction (no separate\nkeywords, nothing half-settable). The two eaves stay put (footprint unchanged) and the\nridge shifts across the width so each face takes its angle; the gable-end triangle and\ntrusses follow. The pair is `(left, right)` by the segment\'s left normal \u2014 the **same\nsides as** `overhang_left`/`overhang_right`. Angles are measured to the wall-top eave\nline. `height` is single-value only (both faces share one ridge line). Intended for a\nsingle-segment gable (`endpoint open`); on a hip end the ridge shift skews the hip.\n\n```wdl\nroof pitched endpoint open slope angle (45, 25) {\n segment "s0" from (House.W/2, 0) to (House.W/2, House.L) width House.W\n}\n```\nIdiom: keep the roof FOOTPRINT (its supported edges) on the main room, then cantilever\none eave to cover an entry landing / stair \u2014 end the axis on the room wall and set a big\n`overhang_end`:\n```wdl\n// footprint ends on the main room\'s east wall (x204 centreline \u2192 x208 outer);\n// the east eave reaches 258, covering a landing that sticks out to x256.\nsegment "seg0" from (4,124) to (204,124) width 240 high_side right overhang 25 overhang_end 50\n```\n\n**Roof coordinates are wall centrelines (under `convention center`), same as\nrooms.** `from`/`to` is the segment\'s ridge/axis and `width` its span *centred on\nthat axis*. Author them on the **same centreline grid as the walls** \u2014 a segment\nwhose axis + width match the rooms\' centrelines auto-grows to the **outer wall\nface** on every side (the compiler extends the axis by \xBD\xB7wall_thickness at each end\nand widens by wall_thickness, exactly the grow a room gets). `overhang` then\nextends *beyond* the outer face. So to cover a footprint spanning wall centrelines\n`x1..x2` (E\u2013W) and `yA..yB` (N\u2013S), write `from (x1, (yA+yB)/2) to (x2, (yA+yB)/2)\nwidth (yB - yA)` \u2014 do **not** add \xBD-wall fudge factors; the convention handles it.\n(Before this, a roof drawn on the grid sat half a wall-thickness *inside* the walls.)\n\n## Components & layers\n\n```wdl\ncomponent Bench { // a reusable mini-house in LOCAL coords (origin 0,0)\n param blen = 60 label "Bench length"\n beam name "Top" at (0,0) size (blen, 18) height 6\n}\nuse Bench as "B1" at (x,y) [rotation <deg>] with { blen = 80 } // stamp onto a floor\n\nlayer "structure" "Structure" [color "#rrggbb"] [group "Frame"] // per-house layer registry\n```\n\nA component may carry a **`goal`** \u2014 a short description of what it accomplishes,\nthe discovery key for module lookup (`wadi_module` / a `wadi_modules` query):\n\n```wdl\ncomponent Stairwell goal "climb to the next floor" {\n param rise = 116\n staircase name "Stair" at (0,0) step (7,11,44) direction south total_height rise\n}\n```\n\nComponents can also come from an **imported module** (see *Imports & modules*),\nstamped with a namespaced `use ns.Comp`:\n\n```wdl\nhouse Home {\n import "konkan/base" as kb // Stairwell, Verandah, Otla (goal-tagged)\n floor 1 "G" slab_thickness 0 {\n room Hall at (20,20) size (200,200) { wall north east south west }\n use kb.Stairwell at (60,60) with { rise = 116 } // param args use `=`, not `:`\n }\n}\n```\n\n`use ns.Comp` expands byte-identical to an inline `component`. Components **nest\nfreely**: a library component may `use` a sibling, `use` a component from a\nlibrary it itself `import`s, and place `item ns."id"` furniture from its own\nimports \u2014 imports resolve **transitively** (cycles are a compile error).\nUn-overridden `param`s fall back to their declared defaults.\n\n`rotation <deg>` (optional, yaw\xB0: 0=south, 90=east) turns the whole stamped\nassembly about its origin. **Right angles (0/90/180/270) are exact for any\ncomponent** (rooms/pillars/beams/slabs swap dims + remap wall sides; furniture\nturns with them). A **non-right angle** is allowed **only for a furniture-only\ncomponent** (items/free walls rotate to any angle); a free angle on a component\nthat contains a room/pillar/beam/slab/staircase is a compile error (arbitrary\nstructural rotation is a future feature).\n\n### Promote a component to a primitive (`expose as`)\n\nA component can be **promoted to a runtime typed primitive** with `expose as`, so it\nreads and behaves like a built-in object type (`pack.type`) rather than a `use`\ninstance:\n\n```wdl\ncomponent Bench goal "a place to sit" expose as garden.bench [layer "id"] [label "\u2026"] {\n param length = 60\n beam name "Seat" at (0,0) size (length, 18) height 6\n}\n```\n\n`expose as <pack>.<type>` names the promoted primitive (a dotted `pack.type` id);\noptional `layer "id"` and `label "\u2026"` set its default layer and menu label. Once\nexposed, the component is available as a first-class object type named `pack.type`\nthroughout the model, its `param`s becoming that type\'s fields.\n\n## Template metadata (`template`)\n\nA `template { \u2026 }` block **self-describes a template** on its `.wdl` / `.wadi`, so a\ngallery can index the file without a separate catalog. It carries display metadata\nonly (no geometry):\n\n```wdl\ntemplate {\n title "Coastal Cottage"\n description "A compact single-storey Konkan home."\n style "konkan"\n roof "gable"\n tags ("coastal", "1BHK", "compact")\n thumbnails ("thumb-iso.png", "thumb-plan.png")\n min_plot (340, 400)\n}\n```\n\nFields: `title`, `description`, `style`, `roof`, `tags` (a list), `thumbnails` (a\nlist of image paths), `min_plot` (the minimum plot the template needs). The folder\nof self-describing files IS the catalog.\n\n## The `raw` escape (rarely needed)\n\nAnything the first-class syntax doesn\'t cover can be written as literal JSON per\nthe `.wadi` schema (`data-model.md`):\n\n```wdl\nraw "type" { "field": 1, "formulas": { "field": "= expr" } }\n```\n\n## DSL-specific pitfalls\n\n- **`convention center` and `units \u2026 per_unit 10`** belong at the top of every\n `house` \u2014 same as the JSON path. All the `coordinate-system.md` rules (Y-down,\n units, centreline abutment) apply identically; the DSL just writes them shorter.\n- **Formulas are bare expressions**, not `"= \u2026"` strings \u2014 the compiler emits the\n `= \u2026` form for you. Write `at (main.x1, main.yA)`, not `at ("= main.x1", \u2026)`.\n- **`name "\u2026"` is quoted; `room`/`pillar`/`var`/grid-line names are bare** ids\n (no spaces, and not a reserved word like `width`, `height`, `size`, `at`).\n- **Roof alone on the top floor**; segment widths/positions come from the walls\n they sit on. See `roof-v2-guide.md`.\n- **A pillar\'s `at` is its TOP-LEFT corner, not its centre.** To centre a column on\n `(cx,cy)`, author `at (cx - w/2, cy - l/2)`.\n- **Free-standing walls don\'t auto-mitre at corners** \u2014 extend endpoints so the wall\n bodies overlap (\u2265 \xBD\xB7`wall_thickness` past the shared point), or the corner is left\n as a gap.\n- **Prefer `climb up` \u2014 it\'s bottom-anchored.** Put the stair on the LOWER floor it\n rises FROM; `at` is the bottom step and the flight ascends into `direction`.\n Top-anchored is the legacy `climb down` (default only for older configs): the stair\n sits on the UPPER floor and descends. `check.sh` C5 flags a stair that lands below\n ground. See the staircase note.\n- **Structural conventions are enforced** \u2014 `check.sh` fails on floating floors\n (plinth-floor `height` \u2260 plinth block height; a no-slab floor with nonzero\n `slab_thickness`) and warns on exterior room sides left open. See\n `conventions.md`; the DSL editor shows the same findings in its status pill.\n- Compile after **every** edit; a parse error means the `.wadi` wasn\'t updated, so\n the live model just won\'t change \u2014 never silently wrong.\n'
|
|
433120
|
+
"body": '# The Wadi DSL (`.wdl`) \u2014 authoring reference\n\nYou author houses in the **Wadi DSL** \u2014 a small, formal language (`.wdl`) that\ncompiles to a resolved `.wadi` (`house_config.json`). The DSL is **complete**:\nevery object type in the model has first-class syntax, so you rarely need the\n`raw` escape. Authoring the DSL is more direct and less error-prone than writing\nJSON \u2014 the grammar enforces structure, and `check.sh` reports parse errors with\nline:col.\n\n**This file is the SYNTAX reference.** The *semantics* live in the other\nreferences and apply unchanged \u2014 read them:\n\n- `coordinate-system.md` \u2014 X\u2192right, **Y\u2192DOWN**, Z\u2192up; **10 units = 1 ft**;\n the **centreline** convention. The #1 source of mistakes.\n- `conventions.md` \u2014 the **structural coding conventions** (`check.sh` enforces\n them): plinth-floor height must match the plinth block, rooms must wall every\n exterior side, a no-slab floor must set `slab_thickness 0`.\n- `parametric-conventions.md` \u2014 the grid-first recipe for reusable templates.\n- `roof-v2-guide.md` \u2014 roof segments, hip vs gable, trusses, joints.\n- `data-model.md` \u2014 the underlying `.wadi` schema (what the DSL compiles to; also\n the field reference for the `raw` escape).\n\n## The loop\n\n1. Write / edit `house.wdl` \u2014 the **single shared source** (you and the human\n co-edit it; the app\'s DSL previewer renders it live). You never produce a `.wadi`.\n2. `wadi-skill/architect/scripts/check.sh house.wdl` \u2014 runs the DSL compiler +\n validator (schema + wall/roof geometry) against a **throwaway temp** just for\n feedback; fix any reported error and re-run.\n3. `preview.sh house.wdl` \u2192 read the PNGs (plans / elevations / roof) to check your\n work. (It also compiles to a throwaway temp \u2014 no persistent `.wadi`.)\n\n## Skeleton\n\n```wdl\nhouse MyHouse {\n convention center // ALWAYS use center (wall-centreline coords)\n units feet_inches per_unit 10 // 10 project units = 1 ft\n site { plot (WIDTH, LENGTH) ref (0, 0) }\n defaults { floor_height 120 wall_height 108 slab_thickness 8 wall_thickness 8 }\n\n // parametric core (optional): var, point, grid, configurator\n // component / layer declarations (optional)\n floor 0 "Plinth" { \u2026 } // floors stack in source order (0 = plinth)\n floor 1 "Ground Floor" { \u2026 }\n floor 2 "Loft" { roof \u2026 } // roof lives ALONE on its own top floor\n}\n```\n\nNumbers are **project units** (feet \xD7 10 by default). Names after `house`,\n`room`, `pillar`, `var`, `point`, `grid`, and `use`/`component` are bare\nidentifiers (no spaces); names introduced with the `name` keyword are quoted\nstrings.\n\n## Parametric core (domain-neutral)\n\n```wdl\nvar wallT = 8 // a knob; may reference other vars\nvar pilInset = (pillarW - wallT) / 2\n\npoint House { x = 420, y = 470 } // reference as House.x / House.W / House.L\n // (.W = x, .L = y \u2014 a point doubles as a size)\n\nguides main { // NAMED wall centrelines; publishes main.x1 / main.yA\n x: 1 @ wallT / 2, 2 @ House.W / 2, 3 @ House.W - wallT / 2\n y: A @ wallT / 2, B @ House.L / 2, C @ House.L - wallT / 2\n}\n// each line may add: \u2026 @ <expr> thick <expr> role structural|planning\n// (`grid` is a deprecated alias for a named `guides` object \u2014 still parses.)\n\nguides module { // GENERATED \u2014 a uniform family from origin+spacing\n origin (0, 0) spacing (30, 30) [extent (40, 30)] // origin default (0,0); extent optional\n}\n// Reference a generated line by INDEX: module.x8 (integer) or module.x(<expr>)\n// (call form \u2014 required for fractional/negative/computed indices). `extent`\n// (line counts per axis) only bounds where the lines are DRAWN. A `guides` object\n// is EITHER named (x/y lists) OR generated (spacing) \u2014 never both.\n\nconfigurator { // the owner-facing template you author\n title "Configure your home" // panel heading (optional)\n note "Everything re-flows to fit." // panel subtitle (optional)\n\n slider pillarW "Column size" ft [8 .. 14 step 1] note "help text" // trailing note optional\n number ceiling "Ceiling height" ft\n toggle has_loft "Add a loft"\n select roof_style "Roof style" { Flat = 0, Shed = 1, Gable = 2, Hip = 3 }\n select floorH "Ceiling height" { "9 ft" = 90, "10 ft (std)" = 100 } // labels can be quoted strings\n\n group "Plot" note "about this section" { // sections the panel; label only\n slider W "Plot width" ft [340 .. 520 step 10]\n }\n}\n```\n\nEvery knob binds to a `var` by name (`target`). To expose a plot dimension, model\nit as a `var` and reference it from the point (`point House { x = W }`), then bind\nthe knob to `W` \u2014 knobs target vars, never a point field like `House.W`.\n\n**Formulas are automatic.** Any geometry number can be a formula \u2014 just write the\nexpression instead of a literal (`at (main.x1, main.yA)`, `size (House.W/2, 200)`).\nOperators: `+ - * /`, unary `-`, parentheses, and the functions\n`min max clamp round floor ceil abs`. References: a `var`, a `point`\n(`House.W`), a named guide (`main.x3 - main.x1`), or a generated guide\n(`module.x8`, `module.x(n+1)`). No comparison operators \u2014 gate\nthings with the `min/abs` idiom (see `enabled` below).\n\n## Common attribute tail (every object)\n\nAfter an object\'s geometry, in THIS order, any of:\n\n```\n\u2026 z_offset <expr> enabled <expr> layer "id" [material "id"]\n```\n\n- `enabled <expr>` \u2014 the on/off switch. A `0`/`false` value hides the object. To\n gate on a configurator variable, use a 0/1 formula:\n `enabled 1 - min(1, abs(roof_style - 3))` renders the object only when\n `roof_style == 3`. (This is how one template carries several roofs and shows\n only the chosen one.)\n- `z_offset <expr>` \u2014 lift above the floor base (split levels).\n- `material "id"` \u2014 only on plinth / ground / room / wall / staircase / kitchen /\n roof.\n\n## Objects \u2014 structure & envelope\n\n```wdl\nslab [name "N"] at (x,y) size (w,l) [thickness <t>] // floor_slab\nbeam [name "N"] at (x,y) size (w,l) [height <h>]\nplinth [name "N"] at (x,y) size (w,l) height <h> // raised base (Plinth floor)\nground [name "N"] at (x,y) size (w,l) [height <h>] // terrain plane\npillar Name at (x,y) size (w,l) [height <h>] // (x,y) = TOP-LEFT corner\n```\n\n`at (x,y)` is the **TOP-LEFT CORNER** \u2014 **not the centre** \u2014 for every one of these\n(same as rooms/slabs/beams); `size (w,l)` is width \xD7 length. All accept the common tail.\n\n**Pillars catch people out here.** A column reads as "placed at a point," but `at` is\nstill its corner. To **centre a column on a point** `(cx, cy)` \u2014 a grid node, a room\ncorner \u2014 place it at **`at (cx - w/2, cy - l/2)`**, never at `(cx, cy)`. On a grid, the\n`pilInset` idiom (see `parametric-conventions.md`) does exactly this so columns sit flush.\n\n## Objects \u2014 rooms, walls & openings\n\nA room shows exactly the walls you declare. A **bare room (no `wall` lines) is\nenclosed on all four sides.** List plain walls compactly; give a wall its own line\nonly when it carries a door/window; omit a side to leave it open (verandah).\n\n```wdl\nroom Name at (x,y) size (w,l) [height <h>] [material "\u2026"] {\n connect Kitchen Hall // rooms this room opens into (same floor)\n wall east west north // plain walls \u2014 several in one statement\n wall south { door Main at <offset> [from start|center|end] size (w,h) [open] } // wall WITH openings\n wall west { window W at <offset> [from start|center|end] size (w,h) [sill <s>] [open] }\n item asset { \u2026 } anchor center [gap (gx,gy)] // furniture anchored inside the room\n}\n```\n\n- `connect A B \u2026` records that this room adjoins the named room(s) \u2014 space-separated,\n quote a name with spaces (`connect "Guest Room"`). It\'s design intent + a functional\n test, **not geometry** (the renderer ignores it). May go anywhere in the block and\n repeat. Validated by C11: connected rooms must OVERLAP on a wall AND be passable \u2014\n a door in the overlap, or the shared wall left off BOTH rooms (an open passage).\n- `wall <side>\u2026` sides are `north|south|east|west`. A `wall <side>` line may also\n add `height <h>` / `height_end <h>` (sloped).\n- `door`/`window` `at <offset>` is measured along the wall; `size (width, height)`;\n `window \u2026 sill <s>` sets the sill height; `open` = a bare hole (no leaf/glazing).\n- `from start|center|end` (default `start`) picks which end `<offset>` is measured\n from, so the opening keeps its place when the wall or room scales \u2014 no formula\n needed. `start`: offset from the wall start to the near edge (the legacy default).\n `end`: offset from the wall end to the far edge (`at 0 from end` = flush to the end).\n `center`: signed shift of the opening centre from the wall midpoint (`at 0 from\n center` = centred; the offset may be negative).\n\nA **free-standing wall** (not a room side):\n\n```wdl\nwall Name from (x1,y1) to (x2,y2) [height <h>] [height_end <h>] [facing north|\u2026] {\n \u2026 door/window openings \u2026\n}\n```\n\n- `from`/`to` are the wall\'s **centreline** endpoints; the wall is drawn as a rectangle\n `wall_thickness` wide, centred on that line.\n- **Overlap walls at corners \u2014 they do NOT auto-mitre.** Two free-standing walls that\n merely *touch* at a shared endpoint leave an unfilled square notch (\xBD\xB7`wall_thickness`)\n at the corner, because each is just a rectangle capped at its endpoint. To fill the\n corner, **extend the endpoints so the wall bodies OVERLAP** \u2014 run at least one wall\'s\n end **half the wall thickness past** the shared point (overlapping by the full thickness\n is fine and simplest). For an L of thickness 8 meeting at `(160,40)`:\n\n ```wdl\n wall H from (40, 40) to (164, 40) height 108 // ends 4 (\xBD\xB78) PAST the corner\n wall V from (160, 40) to (160, 160) height 108 // butts into H\'s overlapped body\n ```\n\n (Room walls handle their own corners; this only applies to `wall \u2026 from \u2026 to \u2026`.)\n\n## Objects \u2014 circulation & fittings\n\n```wdl\nstaircase [name "N"] at (start_x, start_y) step (rise, tread, width)\n direction north|south|east|west [climb up|down]\n [total_height <h>] [max_run <r>] [landing_depth <d>]\n [landing_thickness <t>] [turn clockwise|anticlockwise] [flight_gap <g>]\n\nkitchen [name "N"] path ((x,y), (x,y), \u2026) side left|right\n depth <d> height <h> [base_z <z>] // path points are literal numbers\n```\n\n**`climb` picks the anchor + z direction.** Prefer **`climb up`** \u2014 the intuitive way:\n\n- Put the stair on the **LOWER** floor it rises FROM. `at (x,y)` is the **bottom** step\'s\n near corner on that floor; `direction` is the **ascent** direction; the flight climbs UP.\n- `total_height` is the **rise** to the next level (omit \u2192 this floor\'s own height).\n- `max_run` caps a flight\'s run; exceed it and the stair auto-splits into switchback\n flights with turn landings (`landing_depth`/`turn`/`flight_gap` tune the switchback).\n\n```wdl\nfloor 1 "Ground Floor" height 116 {\n slab at (\u2026) size (\u2026)\n staircase name "Stair" at (212, 64) step (7, 11, 44) // bottom = this floor\n direction south climb up // ascends south to the floor above\n}\n```\n\n`climb down` is the legacy mode (**DEFAULT** for older configs): put the stair on the\n**UPPER** destination floor; `at (x,y)` is the **top** connection, `direction` is the\ndescent, and `total_height` defaults to the floor-below\'s height. New designs should use\n`climb up`.\n\n// three ways to name the GLB, in order of preference:\nitem [name "N"] f."sofa" // 1. from an imported module (see Imports)\nitem [name "N"] "sofa" // 2. a same-file / bare-imported `asset` id\nitem [name "N"] asset { id "sofa" src "\u2026/sofa.glb" dims (w,h,d) [category "\u2026"] } // 3. inline one-off\n at (x,y) [rotation <deg>] [scale <s>]\n [anchor_to "RoomName" anchor center gap (gx,gy)]\n```\n\nPrefer the module form (`item f."bed_double"`) \u2014 `import "std-furniture" as f`\nonce and every piece is a short id, no URLs. The bare form (`item "sofa"`) needs\na matching top-level `asset "sofa" \u2026` in the file (or a bare `import`). The inline\n`asset { \u2026 }` block is only for a one-off GLB not in any pack. All three produce\nthe identical `{id,src,dims}` downstream. Furniture `dims` are the real-world size\nin **metres** `(width, height, depth)`; `src` is a GLB URL (an unreachable GLB\nshows a placeholder box, never a blank). `anchor` is one of `top-left top-center\ntop-right center-left center center-right bottom-left bottom-center bottom-right`.\n\n**Orientation \u2014 this is how you point furniture the right way.** A piece\'s FRONT\n(the side you sit at / the doors / the open side) faces a known compass direction\nper its `rotation` (degrees):\n\n| `rotation` | front faces |\n|---|---|\n| `0` | **South** (the plot front / entrance side, +Y) |\n| `90` | East |\n| `180` | North |\n| `270` | West |\n\nSo a sofa against the NORTH wall (facing into the room, i.e. south) is `rotation\n0`; against the SOUTH wall (facing north) it\'s `rotation 180`; against the WEST\nwall (facing east) `rotation 90`. The floor plan (`wadi_preview plans`) draws a\nsmall triangle on each piece\'s front edge so you can verify the way it points;\nfor a definitive 3D check use `wadi_capture_3d({ room: "\u2026" })` (first-person from\ninside the room).\n\n**Anchoring auto-orients.** When you `anchor` a piece to a wall and DON\'T give a\n`rotation`, it automatically faces away from that wall, into the room \u2014 `anchor\ntop-center` \u2192 faces south, `bottom-center` \u2192 north, `center-left` \u2192 east,\n`center-right` \u2192 west (a corner uses its north/south edge). So `item f."bed_double"\nanchor top-center` needs no rotation. An explicit `rotation` always overrides,\nand the derived value is written into the resolved model, so the plan notch and\nthe 3D view show it \u2014 anchoring never changes facing silently.\n\n## Objects \u2014 GLB models (`model`)\n\nA `model` places a **GLB at real metre scale** and manipulates it through a **rig**\nof named-node operations. It is distinct from `item` (catalog furniture): `model`\ntargets the GLB\'s internal node graph, so you can hide, move, recolour, or array\nsub-parts of one asset.\n\n```wdl\nmodel [name "N"] <asset> at (x,y) [rotation <deg>] [scale <s>] {\n translate "nodeName" (x,y,z) // move a named node\n rotate "nodeName" (x,y,z) // rotate it (degrees)\n scale "nodeName" (x,y,z) // scale it\n visible "nodeName" false // hide a node\n material "nodeName" color "#rrggbb" // recolour a node\n array "nodeName" count N step (dx,dy,dz) { \u2026 nested rig ops \u2026 }\n}\n```\n\n`<asset>` names the GLB the same three ways as `item` (module `ns."id"`, bare\n`"id"`, or an inline `asset { \u2026 }` block). The rig ops run against the GLB\'s named\nnodes; `array` replicates a node `count` times along `step`, and its nested block\napplies further ops per copy. Use `model` for a rigged mechanism (a fan, a louvre\nbank, a spiral of balusters); use `item` for a plain piece of furniture.\n\n## Imports & modules (reusable `.wdl` libraries)\n\nA `.wdl` file can be a **module** \u2014 top-level declarations (no `house` needed) \u2014\nthat another file `import`s. Two bundled ones: `std-furniture` (asset pack \u2192\n`item ns."id"`) and `konkan/base` (goal-tagged component pack \u2192 `use ns.Comp`;\nStairwell, Verandah, Otla, Bathroom, Kitchen, TulsiVrindavan, Parapet). The\n`konkan_cottage` example (`wadi_examples`) assembles a whole house from both.\n\n```wdl\nhouse Home {\n import "std-furniture" as f // aliased: refer to its assets as f."<id>"\n // import "std-furniture" // bare: its ids drop into scope for item "<id>"\n floor 1 "G" slab_thickness 0 {\n room Bed at (20,20) size (160,200) { wall north east south west\n item f."bed_double" anchor center }\n }\n}\n```\n\nA module file itself is just top-level `asset` and `component` decls:\n\n```wdl\n// my-furniture.wdl \u2014 a house-less module (a reusable library)\nasset "daybed" src "https://\u2026/daybed.glb" dims (1.8, 0.4, 0.9) name "Daybed" category "Living"\n```\n\nOver MCP, `wadi_modules` lists importable modules and `wadi_module "<name>"`\nshows a module\'s asset ids + dimensions (filter with a `query`). Import refs\nresolve by name against the bundled `std-*` packs (a local `modules/` search\npath and git refs come later).\n\n## Objects \u2014 roof (one object; flat / shed / gable / hip)\n\nThe roof lives ALONE on its own top floor and you never set its Z (see\n`roof-v2-guide.md`). `endpoint`: `closed` = hip triangle, `open` = gable end-wall.\n\n```wdl\nroof [name "N"] pitched|shed|flat\n [endpoint open|closed]\n [slope angle <deg> | slope height <ridge_h>] // symmetric pitch (one value)\n [slope angle (<left>, <right>)] // asymmetric (saltbox) gable \u2014 angle pair\n [overhang <o>] [slab_thickness <t>] [parapet <h> x <t>] [gable_wall_thickness <t>] {\n segment "id" from (x,y) to (x,y) width <w>\n [high_side left|right] // shed only\n [start_endpoint open|closed] [end_endpoint open|closed]\n [hip_setback (a,b)] [gable_overhang (a,b)] [hip_ridge_extension (a,b)]\n [overhang <o>] // uniform eave, all four sides\n [overhang_start <o>] [overhang_end <o>] // per-side along the axis (shed;\n // on a gable end = gable_overhang)\n [overhang_low <o>] [overhang_high <o>] // SHED eaves (down-slope / up-slope)\n [overhang_left <o>] [overhang_right <o>] // PITCHED eaves (left / right of ridge)\n [tie_beams N]\n truss "segId" fink|mono_pitch at (pos, pos, \u2026)\n }\n```\n\nSegment `from`/`to`/`width` and the `hip_setback`/\u2026 values accept formulas, so a\nroof scales with the plot (e.g. `width House.W`, `hip_setback (Verandah.L, Padvi.L)`).\n\n**Per-side overhang (cantilever one edge).** `overhang <o>` sets a uniform eave on\nall four sides. Any sloping roof can override a side independently \u2014 each defaults to\n`overhang`. **Along the axis:** `overhang_start` / `overhang_end` (on a shed, or a\ngable open end \u2014 there they\'re the same as `gable_overhang`; a hip end is geometric,\ntuned via `hip_setback`). **Eaves:** `overhang_low` / `overhang_high` on a **shed**\n(down-slope / up-slope); `overhang_left` / `overhang_right` on a **pitched** roof\n(the two eaves either side of the ridge). A bigger eave overhang also drops that\neave\'s edge along the same pitch, so the slope stays planar. (Per-eave on a *pitched*\nroof is single-segment only \u2014 on a multi-segment roof the eaves share one height so\njoints line up.)\n\n**Asymmetric gable (saltbox) \u2014 an angle pair.** A pitched roof\'s `slope` gives both\nfaces the same pitch (a symmetric gable). To make the two sides different, give\n`slope angle` a **pair** instead of one value \u2014 `slope angle (45, 25)`. A single value\nis symmetric; a pair is asymmetric, and that\'s the whole distinction (no separate\nkeywords, nothing half-settable). The two eaves stay put (footprint unchanged) and the\nridge shifts across the width so each face takes its angle; the gable-end triangle and\ntrusses follow. The pair is `(left, right)` by the segment\'s left normal \u2014 the **same\nsides as** `overhang_left`/`overhang_right`. Angles are measured to the wall-top eave\nline. `height` is single-value only (both faces share one ridge line). Intended for a\nsingle-segment gable (`endpoint open`); on a hip end the ridge shift skews the hip.\n\n```wdl\nroof pitched endpoint open slope angle (45, 25) {\n segment "s0" from (House.W/2, 0) to (House.W/2, House.L) width House.W\n}\n```\nIdiom: keep the roof FOOTPRINT (its supported edges) on the main room, then cantilever\none eave to cover an entry landing / stair \u2014 end the axis on the room wall and set a big\n`overhang_end`:\n```wdl\n// footprint ends on the main room\'s east wall (x204 centreline \u2192 x208 outer);\n// the east eave reaches 258, covering a landing that sticks out to x256.\nsegment "seg0" from (4,124) to (204,124) width 240 high_side right overhang 25 overhang_end 50\n```\n\n**Roof coordinates are wall centrelines (under `convention center`), same as\nrooms.** `from`/`to` is the segment\'s ridge/axis and `width` its span *centred on\nthat axis*. Author them on the **same centreline grid as the walls** \u2014 a segment\nwhose axis + width match the rooms\' centrelines auto-grows to the **outer wall\nface** on every side (the compiler extends the axis by \xBD\xB7wall_thickness at each end\nand widens by wall_thickness, exactly the grow a room gets). `overhang` then\nextends *beyond* the outer face. So to cover a footprint spanning wall centrelines\n`x1..x2` (E\u2013W) and `yA..yB` (N\u2013S), write `from (x1, (yA+yB)/2) to (x2, (yA+yB)/2)\nwidth (yB - yA)` \u2014 do **not** add \xBD-wall fudge factors; the convention handles it.\n(Before this, a roof drawn on the grid sat half a wall-thickness *inside* the walls.)\n\n## Components & layers\n\n```wdl\ncomponent Bench { // a reusable mini-house in LOCAL coords (origin 0,0)\n param blen = 60 label "Bench length"\n beam name "Top" at (0,0) size (blen, 18) height 6\n}\nuse Bench as "B1" at (x,y) [rotation <deg>] with { blen = 80 } // stamp onto a floor\n\nlayer "structure" "Structure" [color "#rrggbb"] [group "Frame"] // per-house layer registry\n```\n\nA component may carry a **`goal`** \u2014 a short description of what it accomplishes,\nthe discovery key for module lookup (`wadi_module` / a `wadi_modules` query):\n\n```wdl\ncomponent Stairwell goal "climb to the next floor" {\n param rise = 116\n staircase name "Stair" at (0,0) step (7,11,44) direction south total_height rise\n}\n```\n\nComponents can also come from an **imported module** (see *Imports & modules*),\nstamped with a namespaced `use ns.Comp`:\n\n```wdl\nhouse Home {\n import "konkan/base" as kb // Stairwell, Verandah, Otla (goal-tagged)\n floor 1 "G" slab_thickness 0 {\n room Hall at (20,20) size (200,200) { wall north east south west }\n use kb.Stairwell at (60,60) with { rise = 116 } // param args use `=`, not `:`\n }\n}\n```\n\n`use ns.Comp` expands byte-identical to an inline `component`. Components **nest\nfreely**: a library component may `use` a sibling, `use` a component from a\nlibrary it itself `import`s, and place `item ns."id"` furniture from its own\nimports \u2014 imports resolve **transitively** (cycles are a compile error).\nUn-overridden `param`s fall back to their declared defaults.\n\n`rotation <deg>` (optional, yaw\xB0: 0=south, 90=east) turns the whole stamped\nassembly about its origin. **Right angles (0/90/180/270) are exact for any\ncomponent** (rooms/pillars/beams/slabs swap dims + remap wall sides; furniture\nturns with them). A **non-right angle** is allowed **only for a furniture-only\ncomponent** (items/free walls rotate to any angle); a free angle on a component\nthat contains a room/pillar/beam/slab/staircase is a compile error (arbitrary\nstructural rotation is a future feature).\n\n### Promote a component to a primitive (`expose as`)\n\nA component can be **promoted to a runtime typed primitive** with `expose as`, so it\nreads and behaves like a built-in object type (`pack.type`) rather than a `use`\ninstance:\n\n```wdl\ncomponent Bench goal "a place to sit" expose as garden.bench [layer "id"] [label "\u2026"] {\n param length = 60\n beam name "Seat" at (0,0) size (length, 18) height 6\n}\n```\n\n`expose as <pack>.<type>` names the promoted primitive (a dotted `pack.type` id);\noptional `layer "id"` and `label "\u2026"` set its default layer and menu label. Once\nexposed, the component is available as a first-class object type named `pack.type`\nthroughout the model, its `param`s becoming that type\'s fields.\n\n## Template metadata (`template`)\n\nA `template { \u2026 }` block **self-describes a template** on its `.wdl` / `.wadi`, so a\ngallery can index the file without a separate catalog. It carries display metadata\nonly (no geometry):\n\n```wdl\ntemplate {\n title "Coastal Cottage"\n description "A compact single-storey Konkan home."\n style "konkan"\n roof "gable"\n tags ("coastal", "1BHK", "compact")\n thumbnails ("thumb-iso.png", "thumb-plan.png")\n min_plot (340, 400)\n}\n```\n\nFields: `title`, `description`, `style`, `roof`, `tags` (a list), `thumbnails` (a\nlist of image paths), `min_plot` (the minimum plot the template needs). The folder\nof self-describing files IS the catalog.\n\n## The `raw` escape (rarely needed)\n\nAnything the first-class syntax doesn\'t cover can be written as literal JSON per\nthe `.wadi` schema (`data-model.md`):\n\n```wdl\nraw "type" { "field": 1, "formulas": { "field": "= expr" } }\n```\n\n## DSL-specific pitfalls\n\n- **`convention center` and `units \u2026 per_unit 10`** belong at the top of every\n `house` \u2014 same as the JSON path. All the `coordinate-system.md` rules (Y-down,\n units, centreline abutment) apply identically; the DSL just writes them shorter.\n- **Formulas are bare expressions**, not `"= \u2026"` strings \u2014 the compiler emits the\n `= \u2026` form for you. Write `at (main.x1, main.yA)`, not `at ("= main.x1", \u2026)`.\n- **`name "\u2026"` is quoted; `room`/`pillar`/`var`/grid-line names are bare** ids\n (no spaces, and not a reserved word like `width`, `height`, `size`, `at`).\n- **Roof alone on the top floor**; segment widths/positions come from the walls\n they sit on. See `roof-v2-guide.md`.\n- **A pillar\'s `at` is its TOP-LEFT corner, not its centre.** To centre a column on\n `(cx,cy)`, author `at (cx - w/2, cy - l/2)`.\n- **Free-standing walls don\'t auto-mitre at corners** \u2014 extend endpoints so the wall\n bodies overlap (\u2265 \xBD\xB7`wall_thickness` past the shared point), or the corner is left\n as a gap.\n- **Prefer `climb up` \u2014 it\'s bottom-anchored.** Put the stair on the LOWER floor it\n rises FROM; `at` is the bottom step and the flight ascends into `direction`.\n Top-anchored is the legacy `climb down` (default only for older configs): the stair\n sits on the UPPER floor and descends. `check.sh` C5 flags a stair that lands below\n ground. See the staircase note.\n- **Structural conventions are enforced** \u2014 `check.sh` fails on floating floors\n (plinth-floor `height` \u2260 plinth block height; a no-slab floor with nonzero\n `slab_thickness`) and warns on exterior room sides left open. See\n `conventions.md`; the DSL editor shows the same findings in its status pill.\n- Compile after **every** edit; a parse error means the `.wadi` wasn\'t updated, so\n the live model just won\'t change \u2014 never silently wrong.\n'
|
|
432479
433121
|
},
|
|
432480
433122
|
"conventions": {
|
|
432481
433123
|
"title": "Structural coding conventions (C1/C2/C3\u2026)",
|
|
432482
|
-
"body": "# Wadi structural conventions (coding guidelines)\n\n<!-- GENERATED FILE \u2014 do not edit conventions.md by hand. It is built from\n conventions.preamble.md + the constraint modules in editor/src/lint/constraints.\n Regenerate with `npm --prefix editor run gen-conventions-doc`. -->\n\nA house can be **well-formed but structurally unsound**: it passes the schema and\nthe wall/roof geometry check, yet the building would not stand up \u2014 a floor floats\nin mid-air, a room is open to the weather, walls hover above a phantom slab. These\nare the *coding conventions* every Wadi house must follow.\n\nThey are **formally defined here** and **enforced in code** by the structural\nlinter (`editor/src/lint/structural.ts`), which runs automatically:\n\n- in **`check.sh`** (and `validate.mjs`) \u2014 **errors fail** the check, **warnings**\n are printed but advisory;\n- in the **DSL editor** \u2014 the status pill shows the count and lists every finding\n in its hover tooltip, while still rendering the model so you can *see* the\n unsound part.\n\nEach finding carries its convention id (`C1`, `C2`, \u2026). Each convention is a\nself-contained module under `editor/src/lint/constraints/` (its check + this doc +\nits example fixtures), and **this file is generated from those modules**\n(`editor/scripts/gen-conventions-doc.mjs`) \u2014 so the doc and the linter cannot\ndrift. Add a rule by adding a constraint module and regenerating.\n\n---\n\n## The vertical model (why C1 and C3 exist)\n\nFloors stack in source order (floor 0 = the Plinth floor). The renderer places\nthem like this (`editor/src/three/coords.ts`):\n\n- **A floor's base elevation = the running sum of the previous floors' `height`\n only.** `wall_height` and `slab_thickness` do **not** raise the next floor.\n- The **plinth block** is drawn to its *own* `height`. So the floor above sits at\n `plinth-floor.height`, while the plinth top is at `plinth.height` \u2014 they must be\n equal or the floor above floats/sinks by the difference. \u2192 **C1**\n- **`slab_thickness` lifts a floor's walls within its band** (`wallZ = base +\n slab_thickness`) \u2014 it is the deck the walls stand on. With no slab object there\n is no deck, so the walls float by that amount. \u2192 **C3**\n\n`height`, `wall_height`, and `slab_thickness` are otherwise **independent** \u2014 the\nmodel enforces no relationship between them. These conventions add the few\nrelationships that structural soundness *does* require.\n\n---\n\n## C1 \u2014 The plinth floor's height must match the plinth block height \xB7 **error**\n\n**Statement.** A floor that carries a `plinth` object (the Plinth floor) must set an explicit `height`, and that height must equal the plinth block's `height`.\n\n**Rationale.** The floor above is stacked at `plinth-floor.height`; the plinth block rises to `plinth.height`. If they differ, the floor above floats above the plinth (`floor.height > plinth.height`) or sinks into it (`<`). If the floor `height` is omitted it silently defaults to `100`, almost never the plinth height.\n\n**Fix.**\n\n```wdl\nfloor 0 \"Plinth\" height 40 { // == the plinth block height below\n ground name \"Ground\" at (0,0) size (500,500)\n plinth name \"Plinth\" at (\u2026) size (\u2026) height 40\n}\n```\n\n(If the plinth block omits its own `height`, it follows the floor height and is consistent by construction \u2014 but set the floor `height` explicitly anyway, so the stack is not left to the default.)\n\n---\n\n## C2 \u2014 A room must wall every exterior side \xB7 **warning**\n\n**Statement.** A room shown with a **partial** `walls` list must still wall every side that faces **outside** (no room beyond it). Interior (shared) sides may be omitted \u2014 the neighbour's wall stands on the shared centreline.\n\n**Rationale.** A room shows exactly the walls it declares; a **bare room (no `wall` lines) is enclosed on all four sides**. But the moment you add a `wall` line to hang a door or window, the room switches to a *whitelist* \u2014 every side you don't list is now a hole. An exterior hole leaves the room open to the weather. It is a **warning**, not an error, because an open exterior side is sometimes intentional (a verandah / open padvi).\n\n**Fix.**\n\n```wdl\nroom Living at (x,y) size (w,l) {\n wall east west // plain exterior sides \u2014 enclosed\n wall south { door Main at 120 size (36,84) }\n wall north { window N1 at 100 size (60,50) sill 35 }\n}\n```\n\n---\n\n## C3 \u2014 A floor with no slab must set slab_thickness to 0 \xB7 **error**\n\n**Statement.** A floor that has wall/room objects but **no `floor_slab` object** must set `slab_thickness 0`.\n\n**Rationale.** `slab_thickness` is the deck the floor's walls stand on (`wallZ = base + slab_thickness`). Its default is `8`. With no slab object there is no deck, so every wall on the floor floats `slab_thickness` units above the floor base. Setting it to `0` puts the walls on the floor base; alternatively, model the deck by adding a `slab`.\n\n**Fix.**\n\n```wdl\nfloor 1 \"Ground\" slab_thickness 0 { // no slab modelled \u2192 walls sit on the base\n room Studio at (\u2026) size (\u2026) { \u2026 }\n}\n```\n\n*(This does not fire on a floor that carries no walls/rooms \u2014 e.g. a Plinth floor of just `ground` + `plinth`, or a roof-only top floor \u2014 where `slab_thickness` is harmless.)*\n\n---\n\n## C4 \u2014 A stacked floor's height should equal wall_height + slab_thickness \xB7 **warning**\n\n**Statement.** A floor that carries a floor above it (and has walls/rooms) should set `height` = `wall_height` + `slab_thickness`.\n\n**Rationale.** The next floor sits at `base + height`; this floor's walls stand on the deck and reach `base + slab_thickness + wall_height`. When `height` is larger, the floor above leaves a gap over the walls; when smaller, the walls poke through it. It is a **warning** \u2014 a deliberate gap is legitimate (a service plenum, a deep transfer beam) \u2014 but usually they should match.\n\n**Fix.**\n\n```wdl\ndefaults { floor_height 116 wall_height 108 slab_thickness 8 } // 108 + 8 = 116\n```\n\n*(Skipped for the plinth floor \u2014 governed by C1 \u2014 and for the topmost floor, since nothing stacks on its walls.)*\n\n---\n\n## C5 \u2014 A staircase must land on a floor, not below ground \xB7 **warning**\n\n**Statement.** A staircase's descent must not carry it below the ground plane (z < 0).\n\n**Rationale.** Only a `climb down` (top-anchored) stair can fall below ground: you place it on the **upper** floor and it **descends**. Put it on the wrong floor, or give it too large a `total_height`, and the expanded flight lands **below ground** \u2014 it still draws in the 2D plans (which ignore Z) but is **buried and invisible in 3D**, with no other error. A `climb up` stair is anchored on its own floor and ascends, so it never trips this.\n\n**Fix.**\n\nPrefer **`climb up`**: put the stair on the **lower** floor it rises FROM and let it ascend.\n\n```wdl\nfloor 1 \"Ground Floor\" height 116 {\n slab at (\u2026) size (\u2026)\n staircase name \"Stair\" at (212, 64) step (7, 11, 44) // `at` = the BOTTOM (this floor)\n direction south climb up // ascends to the floor above\n}\n```\n\n(Or, if you must keep it `climb down`, move it **up one floor** or reduce `total_height`.)\n\n---\n\n## C6 \u2014 Openings on the same wall must not overlap \xB7 **error**\n\n**Statement.** Two openings (doors/windows) cut into the **same physical wall** must not overlap along it. This includes openings that belong to **two different rooms sharing a boundary wall**.\n\n**Rationale.** Each opening is a boolean-subtract from the wall. Overlapping spans merge into one ragged hole (or fight over the same brick), which is never what you meant \u2014 and on a shared wall it silently punches a bigger gap than either room's plan shows.\n\n**Fix.**\n\nOffset or narrow one opening so the spans are disjoint. Openings are measured from the wall's start corner (`offset` = near edge; the opening occupies `[offset, offset+width]`).\n\n---\n\n## C7 \u2014 Furniture items should not overlap \xB7 **warning**\n\n**Statement.** Two furniture `item`s whose plan footprints overlap are flagged \u2014 as a **warning**, because it is sometimes intentional (a rug under a table, a lamp on a desk, deliberately stacked pieces).\n\n**Rationale.** More often it's a placement slip \u2014 two beds dropped on the same spot, or an anchored piece that reflowed into another when a room was resized. The footprint used is the item's rotated bounding box (yaw-aware), so it matches what the plan draws.\n\n**Fix.**\n\nReposition one item, or ignore the warning if the overlap is deliberate.\n\n---\n\n## C8 \u2014 Two abutting rooms need a partition between them \xB7 **warning**\n\n**Statement.** Where two rooms share a boundary line and **neither** declares a wall on it, there is no partition between them.\n\n**Rationale.** A bare room (no `wall` lines) is enclosed on all four sides, so two bare neighbours have two walls on their shared line. But once **both** rooms switch to partial `walls` lists and both omit the shared side, the centreline is left open \u2014 the rooms merge into one space with no divider. C2 only guards *exterior* sides; this is its interior counterpart. It is a **warning** because an intentional open-plan link (kitchen into living) is legitimate.\n\n**Fix.**\n\nDeclare the wall on **one** of the two rooms (the neighbour's wall stands on the shared centreline, so one is enough):\n\n```wdl\nroom Kitchen at (\u2026) size (\u2026) { wall north south east } // east = the shared line\nroom Living at (\u2026) size (\u2026) { wall north south west }\n```\n\n---\n\n## C9 \u2014 A floor's slab_thickness should match its slab object's thickness \xB7 **warning**\n\n**Statement.** When a floor carries a `floor_slab` object with an explicit `thickness`, that thickness should equal the floor's `slab_thickness`.\n\n**Rationale.** The floor's `slab_thickness` is the deck the walls stand on (`wallZ = base + slab_thickness`); the slab object's own `thickness` is how thick the slab MESH is drawn. If they differ, the walls sit at the floor's `slab_thickness` while the slab top is at the object's `thickness`, so the walls float above or sink into the drawn deck. (A slab with no explicit `thickness` follows the floor's `slab_thickness` and is consistent by construction \u2014 this only fires when both are set and disagree.)\n\n**Fix.**\n\nMake them equal \u2014 most simply, drop the slab's explicit `thickness` so it follows the floor:\n\n```wdl\nfloor 1 \"Ground\" slab_thickness 8 {\n slab name \"Deck\" at (\u2026) size (\u2026) // no thickness \u2192 uses 8\n}\n```\n\n---\n\n## C10 \u2014 The roof should cover the rooms of the top occupied floor \xB7 **warning**\n\n**Statement.** Every room on the top occupied floor should sit under a roof segment \u2014 no room left entirely uncovered.\n\n**Rationale.** The roof's segments span a plan area (each segment's ridge line \xB1 its `width`). A room on the top floor whose footprint does not overlap **any** roof segment has open sky above it \u2014 usually a roof that was sized to the wrong footprint, or a room added after the roof. (Only a *completely* uncovered room is flagged, so eave overhangs and partial coverage never false-warn; a house with no roof at all \u2014 a terrace \u2014 is not flagged.)\n\n**Fix.**\n\nExtend or add a roof segment to span the room, or reduce the room. Roof segments cover `start \u2192 end` along the ridge, `width` across it, so grow `width`/`end` (or the plot variables they derive from) until the room is under it.\n\n---\n\n## SP1 \u2014 A spiral staircase's central pole must be smaller than its radius \xB7 **error**\n\n**Statement.** A `spiral_staircase`'s `pole_radius` must be less than its outer `radius`.\n\n**Rationale.** The treads run from the central pole out to the outer radius. If the pole is as wide as (or wider than) the stair, there is no tread left to stand on \u2014 the geometry collapses.\n\n**Fix.**\n\nReduce `pole_radius` below `radius` (a pole is typically a small fraction of the radius).\n\n---\n\n## Running the checks\n\n```bash\nwadi-skill/architect/scripts/check.sh house.wdl\n```\n\n- **`\u2716 [C\u2026]`** \u2014 a structural **error**; the check exits non-zero. Fix before you\n save/share.\n- **`\u26A0 [C\u2026]`** \u2014 a structural **warning**; advisory. Fix, or keep it if the open\n side is intentional.\n\nIn the DSL editor the same findings appear in the status pill (hover for the full\nlist); the model still renders so you can see the problem.\n\n---\n\n## Planned conventions (not yet enforced)\n\nDocumented so authors know they matter; not linted yet:\n\n- **Interior partition gaps** \u2014 where two rooms share a centreline and *neither*\n declares that wall, there is no partition between them. (C2 only covers\n *exterior* sides.)\n- **Slab thickness \u2194 slab object** \u2014 when a floor *does* carry a `floor_slab`,\n its `slab_thickness` should match the slab's own thickness so walls sit on the\n real deck.\n- **Roof footprint coverage** \u2014 the roof segments should span the top occupied\n floor's footprint (no uncovered rooms).\n"
|
|
433124
|
+
"body": "# Wadi structural conventions (coding guidelines)\n\n<!-- GENERATED FILE \u2014 do not edit conventions.md by hand. It is built from\n conventions.preamble.md + the constraint modules in editor/src/lint/constraints.\n Regenerate with `npm --prefix editor run gen-conventions-doc`. -->\n\nA house can be **well-formed but structurally unsound**: it passes the schema and\nthe wall/roof geometry check, yet the building would not stand up \u2014 a floor floats\nin mid-air, a room is open to the weather, walls hover above a phantom slab. These\nare the *coding conventions* every Wadi house must follow.\n\nThey are **formally defined here** and **enforced in code** by the structural\nlinter (`editor/src/lint/structural.ts`), which runs automatically:\n\n- in **`check.sh`** (and `validate.mjs`) \u2014 **errors fail** the check, **warnings**\n are printed but advisory;\n- in the **DSL editor** \u2014 the status pill shows the count and lists every finding\n in its hover tooltip, while still rendering the model so you can *see* the\n unsound part.\n\nEach finding carries its convention id (`C1`, `C2`, \u2026). Each convention is a\nself-contained module under `editor/src/lint/constraints/` (its check + this doc +\nits example fixtures), and **this file is generated from those modules**\n(`editor/scripts/gen-conventions-doc.mjs`) \u2014 so the doc and the linter cannot\ndrift. Add a rule by adding a constraint module and regenerating.\n\n---\n\n## The vertical model (why C1 and C3 exist)\n\nFloors stack in source order (floor 0 = the Plinth floor). The renderer places\nthem like this (`editor/src/three/coords.ts`):\n\n- **A floor's base elevation = the running sum of the previous floors' `height`\n only.** `wall_height` and `slab_thickness` do **not** raise the next floor.\n- The **plinth block** is drawn to its *own* `height`. So the floor above sits at\n `plinth-floor.height`, while the plinth top is at `plinth.height` \u2014 they must be\n equal or the floor above floats/sinks by the difference. \u2192 **C1**\n- **`slab_thickness` lifts a floor's walls within its band** (`wallZ = base +\n slab_thickness`) \u2014 it is the deck the walls stand on. With no slab object there\n is no deck, so the walls float by that amount. \u2192 **C3**\n\n`height`, `wall_height`, and `slab_thickness` are otherwise **independent** \u2014 the\nmodel enforces no relationship between them. These conventions add the few\nrelationships that structural soundness *does* require.\n\n---\n\n## C1 \u2014 The plinth floor's height must match the plinth block height \xB7 **error**\n\n**Statement.** A floor that carries a `plinth` object (the Plinth floor) must set an explicit `height`, and that height must equal the plinth block's `height`.\n\n**Rationale.** The floor above is stacked at `plinth-floor.height`; the plinth block rises to `plinth.height`. If they differ, the floor above floats above the plinth (`floor.height > plinth.height`) or sinks into it (`<`). If the floor `height` is omitted it silently defaults to `100`, almost never the plinth height.\n\n**Fix.**\n\n```wdl\nfloor 0 \"Plinth\" height 40 { // == the plinth block height below\n ground name \"Ground\" at (0,0) size (500,500)\n plinth name \"Plinth\" at (\u2026) size (\u2026) height 40\n}\n```\n\n(If the plinth block omits its own `height`, it follows the floor height and is consistent by construction \u2014 but set the floor `height` explicitly anyway, so the stack is not left to the default.)\n\n---\n\n## C2 \u2014 A room must wall every exterior side \xB7 **warning**\n\n**Statement.** A room shown with a **partial** `walls` list must still wall every side that faces **outside** (no room beyond it). Interior (shared) sides may be omitted \u2014 the neighbour's wall stands on the shared centreline.\n\n**Rationale.** A room shows exactly the walls it declares; a **bare room (no `wall` lines) is enclosed on all four sides**. But the moment you add a `wall` line to hang a door or window, the room switches to a *whitelist* \u2014 every side you don't list is now a hole. An exterior hole leaves the room open to the weather. It is a **warning**, not an error, because an open exterior side is sometimes intentional (a verandah / open padvi).\n\n**Fix.**\n\n```wdl\nroom Living at (x,y) size (w,l) {\n wall east west // plain exterior sides \u2014 enclosed\n wall south { door Main at 120 size (36,84) }\n wall north { window N1 at 100 size (60,50) sill 35 }\n}\n```\n\n---\n\n## C3 \u2014 A floor with no slab must set slab_thickness to 0 \xB7 **error**\n\n**Statement.** A floor that has wall/room objects but **no `floor_slab` object** must set `slab_thickness 0`.\n\n**Rationale.** `slab_thickness` is the deck the floor's walls stand on (`wallZ = base + slab_thickness`). Its default is `8`. With no slab object there is no deck, so every wall on the floor floats `slab_thickness` units above the floor base. Setting it to `0` puts the walls on the floor base; alternatively, model the deck by adding a `slab`.\n\n**Fix.**\n\n```wdl\nfloor 1 \"Ground\" slab_thickness 0 { // no slab modelled \u2192 walls sit on the base\n room Studio at (\u2026) size (\u2026) { \u2026 }\n}\n```\n\n*(This does not fire on a floor that carries no walls/rooms \u2014 e.g. a Plinth floor of just `ground` + `plinth`, or a roof-only top floor \u2014 where `slab_thickness` is harmless.)*\n\n---\n\n## C4 \u2014 A stacked floor's height should equal wall_height + slab_thickness \xB7 **warning**\n\n**Statement.** A floor that carries a floor above it (and has walls/rooms) should set `height` = `wall_height` + `slab_thickness`.\n\n**Rationale.** The next floor sits at `base + height`; this floor's walls stand on the deck and reach `base + slab_thickness + wall_height`. When `height` is larger, the floor above leaves a gap over the walls; when smaller, the walls poke through it. It is a **warning** \u2014 a deliberate gap is legitimate (a service plenum, a deep transfer beam) \u2014 but usually they should match.\n\n**Fix.**\n\n```wdl\ndefaults { floor_height 116 wall_height 108 slab_thickness 8 } // 108 + 8 = 116\n```\n\n*(Skipped for the plinth floor \u2014 governed by C1 \u2014 and for the topmost floor, since nothing stacks on its walls.)*\n\n---\n\n## C5 \u2014 A staircase must land on a floor, not below ground \xB7 **warning**\n\n**Statement.** A staircase's descent must not carry it below the ground plane (z < 0).\n\n**Rationale.** Only a `climb down` (top-anchored) stair can fall below ground: you place it on the **upper** floor and it **descends**. Put it on the wrong floor, or give it too large a `total_height`, and the expanded flight lands **below ground** \u2014 it still draws in the 2D plans (which ignore Z) but is **buried and invisible in 3D**, with no other error. A `climb up` stair is anchored on its own floor and ascends, so it never trips this.\n\n**Fix.**\n\nPrefer **`climb up`**: put the stair on the **lower** floor it rises FROM and let it ascend.\n\n```wdl\nfloor 1 \"Ground Floor\" height 116 {\n slab at (\u2026) size (\u2026)\n staircase name \"Stair\" at (212, 64) step (7, 11, 44) // `at` = the BOTTOM (this floor)\n direction south climb up // ascends to the floor above\n}\n```\n\n(Or, if you must keep it `climb down`, move it **up one floor** or reduce `total_height`.)\n\n---\n\n## C6 \u2014 Openings on the same wall must not overlap \xB7 **error**\n\n**Statement.** Two openings (doors/windows) cut into the **same physical wall** must not overlap along it. This includes openings that belong to **two different rooms sharing a boundary wall**.\n\n**Rationale.** Each opening is a boolean-subtract from the wall. Overlapping spans merge into one ragged hole (or fight over the same brick), which is never what you meant \u2014 and on a shared wall it silently punches a bigger gap than either room's plan shows.\n\n**Fix.**\n\nOffset or narrow one opening so the spans are disjoint. An opening's span is its resolved `[offset, offset+width]` along the wall \u2014 the `from start|center|end` anchor is honoured (its offset is converted to a start-based position first, exactly as the renderer does).\n\n---\n\n## C7 \u2014 Furniture items should not overlap \xB7 **warning**\n\n**Statement.** Two furniture `item`s whose plan footprints overlap are flagged \u2014 as a **warning**, because it is sometimes intentional (a rug under a table, a lamp on a desk, deliberately stacked pieces).\n\n**Rationale.** More often it's a placement slip \u2014 two beds dropped on the same spot, or an anchored piece that reflowed into another when a room was resized. The footprint used is the item's rotated bounding box (yaw-aware), so it matches what the plan draws.\n\n**Fix.**\n\nReposition one item, or ignore the warning if the overlap is deliberate.\n\n---\n\n## C8 \u2014 Two abutting rooms need a partition between them \xB7 **warning**\n\n**Statement.** Where two rooms share a boundary line and **neither** declares a wall on it, there is no partition between them.\n\n**Rationale.** A bare room (no `wall` lines) is enclosed on all four sides, so two bare neighbours have two walls on their shared line. But once **both** rooms switch to partial `walls` lists and both omit the shared side, the centreline is left open \u2014 the rooms merge into one space with no divider. C2 only guards *exterior* sides; this is its interior counterpart. It is a **warning** because an intentional open-plan link (kitchen into living) is legitimate.\n\n**Fix.**\n\nDeclare the wall on **one** of the two rooms (the neighbour's wall stands on the shared centreline, so one is enough):\n\n```wdl\nroom Kitchen at (\u2026) size (\u2026) { wall north south east } // east = the shared line\nroom Living at (\u2026) size (\u2026) { wall north south west }\n```\n\n---\n\n## C9 \u2014 A floor's slab_thickness should match its slab object's thickness \xB7 **warning**\n\n**Statement.** When a floor carries a `floor_slab` object with an explicit `thickness`, that thickness should equal the floor's `slab_thickness`.\n\n**Rationale.** The floor's `slab_thickness` is the deck the walls stand on (`wallZ = base + slab_thickness`); the slab object's own `thickness` is how thick the slab MESH is drawn. If they differ, the walls sit at the floor's `slab_thickness` while the slab top is at the object's `thickness`, so the walls float above or sink into the drawn deck. (A slab with no explicit `thickness` follows the floor's `slab_thickness` and is consistent by construction \u2014 this only fires when both are set and disagree.)\n\n**Fix.**\n\nMake them equal \u2014 most simply, drop the slab's explicit `thickness` so it follows the floor:\n\n```wdl\nfloor 1 \"Ground\" slab_thickness 8 {\n slab name \"Deck\" at (\u2026) size (\u2026) // no thickness \u2192 uses 8\n}\n```\n\n---\n\n## C10 \u2014 The roof should cover the rooms of the top occupied floor \xB7 **warning**\n\n**Statement.** Every room on the top occupied floor should sit under a roof segment \u2014 no room left entirely uncovered.\n\n**Rationale.** The roof's segments span a plan area (each segment's ridge line \xB1 its `width`). A room on the top floor whose footprint does not overlap **any** roof segment has open sky above it \u2014 usually a roof that was sized to the wrong footprint, or a room added after the roof. (Only a *completely* uncovered room is flagged, so eave overhangs and partial coverage never false-warn; a house with no roof at all \u2014 a terrace \u2014 is not flagged.)\n\n**Fix.**\n\nExtend or add a roof segment to span the room, or reduce the room. Roof segments cover `start \u2192 end` along the ridge, `width` across it, so grow `width`/`end` (or the plot variables they derive from) until the room is under it.\n\n---\n\n## C11 \u2014 A declared connection must overlap on a wall and be passable (door or open) \xB7 **error**\n\n**Statement.** For every `connect`ion a room declares, the two rooms must **overlap on a wall** (not necessarily the whole wall), and that overlap must be **passable**: either a **door** lies in it, or the wall is **left off both rooms** (an open passage).\n\n**Rationale.** A connection is a FUNCTIONAL requirement \u2014 `Living` opens into `Kitchen`. It is design intent, not geometry (the renderer never draws it), so this constraint is what verifies the intent is physically realized. It fails two ways: the rooms' walls don't overlap at all, or they overlap but a solid wall (present on either room, no door in the overlap) blocks the way. No door is ever generated \u2014 a room authors its own openings, or omits the shared wall to leave the rooms open to each other.\n\n**Fix.**\n\nOverlap the two rooms on a wall, then EITHER put a door in the overlap (on either room), OR omit that wall on both:\n\n```wdl\n// door in the shared wall\nroom Living at (\u2026) size (\u2026) { connect Kitchen wall east { door D at 80 size (40,210) } }\nroom Kitchen at (\u2026) size (\u2026)\n\n// open passage \u2014 neither room walls the shared side\nroom Living at (\u2026) size (\u2026) { connect Kitchen wall north south west }\nroom Kitchen at (\u2026) size (\u2026) { wall north south east }\n```\n\n---\n\n## SP1 \u2014 A spiral staircase's central pole must be smaller than its radius \xB7 **error**\n\n**Statement.** A `spiral_staircase`'s `pole_radius` must be less than its outer `radius`.\n\n**Rationale.** The treads run from the central pole out to the outer radius. If the pole is as wide as (or wider than) the stair, there is no tread left to stand on \u2014 the geometry collapses.\n\n**Fix.**\n\nReduce `pole_radius` below `radius` (a pole is typically a small fraction of the radius).\n\n---\n\n## Running the checks\n\n```bash\nwadi-skill/architect/scripts/check.sh house.wdl\n```\n\n- **`\u2716 [C\u2026]`** \u2014 a structural **error**; the check exits non-zero. Fix before you\n save/share.\n- **`\u26A0 [C\u2026]`** \u2014 a structural **warning**; advisory. Fix, or keep it if the open\n side is intentional.\n\nIn the DSL editor the same findings appear in the status pill (hover for the full\nlist); the model still renders so you can see the problem.\n\n---\n\n## Planned conventions (not yet enforced)\n\nDocumented so authors know they matter; not linted yet:\n\n- **Interior partition gaps** \u2014 where two rooms share a centreline and *neither*\n declares that wall, there is no partition between them. (C2 only covers\n *exterior* sides.)\n- **Slab thickness \u2194 slab object** \u2014 when a floor *does* carry a `floor_slab`,\n its `slab_thickness` should match the slab's own thickness so walls sit on the\n real deck.\n- **Roof footprint coverage** \u2014 the roof segments should span the top occupied\n floor's footprint (no uncovered rooms).\n"
|
|
432483
433125
|
},
|
|
432484
433126
|
"coordinate-system": {
|
|
432485
433127
|
"title": "Coordinates, units & the centreline convention",
|
|
@@ -432495,7 +433137,7 @@ var DOCS = {
|
|
|
432495
433137
|
},
|
|
432496
433138
|
"data-model": {
|
|
432497
433139
|
"title": "The underlying .wadi schema (generated from Zod)",
|
|
432498
|
-
"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'
|
|
433140
|
+
"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| `guides` | map: string \u2192 `guidesDef` | | First-class parametric GUIDES (plans/floor-planner-graph-integration.md). Map of id \u2192 a named XOR generated guides object; objects place themselves by referencing a guide in ordinary formulas (`main.x2`, `module.x8`). Optional; reusable across templates. `grids` is the deprecated former name \u2014 still read for backward-compat; the resolver merges both (guides wins on collision). |\n| `grids` | map: string \u2192 `guidesDef` | | |\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| `connections` | array of string | | Rooms this room connects to, by name (same floor). Design intent + a functional test (constraint C11): a declared connection must be adjacent AND joined by a door. Symmetric and deduped; NOT geometry \u2014 it never moves or sizes anything, and the renderer ignores it. |\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'
|
|
432499
433141
|
}
|
|
432500
433142
|
};
|
|
432501
433143
|
var MODULES = {
|