v0.6.0: ruimtes (verfemmer: functie, netto m², vloerafwerking, FIBO-wandpanelen, eigen verfkleur, vals plafond, ruimtestaat op de PDF), inrichting (sanitair, keuken, kookeiland, kasten, bed, zithoek, boiler; slepen met snappen op het wandvlak, draaien, kosten), kozijnen met deurrichting, glasdeur en schuifdeuren (inbouw/opbouw, deuren open in de loopmodus) plus dagkanten, gevel stucwerk en wandstukken splitsen met eigen gevel, dak over de hele woning met overkapping, geschilderd plafond, model Amstel als nieuw ontwerp, kosten en prijzen uit kosten/intern.csv en kosten/leveranciers.csv (CostCsv); easter egg verwijderd; overbodige comments opgeschoond (alleen copyright, uitzonderingen en valkuilen blijven)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-11 00:34:27 +02:00
co-authored by Claude Opus 5.5
parent b5510d56c7
commit ceda0b554d
45 changed files with 4117 additions and 3700 deletions
+155 -78
View File
@@ -4,24 +4,9 @@
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
// ============================================================
// wall/kozijn-model.js — Vulling van een opening (window.KozijnModel):
// kunststof kozijnen naar het voorbeeld van de Aluplast Ideal 7000
// (examples/Buiten kozijnen.pdf): kader 70 mm in het aanzicht, 120 mm
// diep, buitenzijde in een andere kleur dan de binnenzijde (folie).
//
// Een opening krijgt een (optionele) vulling, opgeslagen bij de opening:
// op.fill = { type: 'raam'|'deur', rooster, glas, out, in }
// raam vast glas in het kader; rooster = ventilatierooster bovenin
// deur deurblad (draait links) met dorpel; is de opening breder of
// hoger dan één deur, dan vult vast glas de rest (zijlicht,
// bovenlicht). glas = glasstrook in het deurblad
// out/in kleur buiten/binnen (COLORS)
// Zonder fill blijft de opening open (zoals voorheen). Het formaat volgt
// altijd de opening (ook een schuine bovenzijde).
//
// Plain script (geen ES-module), geen DOM. Gebruikt door WallModel.build3D.
// ============================================================
// Kunststof kozijnen naar de Aluplast Ideal 7000 (examples/Buiten kozijnen.pdf).
// links/rechts (leaf, hinge, dir) zijn van buitenaf gezien = vanaf +z.
// userData.door: open/target in radialen (draaien) of meters (schuiven); de loopmodus (house.js) zet target.
window.KozijnModel = (() => {
const COLORS = {
@@ -30,47 +15,54 @@ window.KozijnModel = (() => {
antraciet: { label: 'Antraciet', ral: 'RAL 7016', hex: 0x383e42 },
zwart: { label: 'Zwart', ral: 'RAL 9005', hex: 0x151515 }
};
const FILL_TYPES = { geen: 'Geen (open)', raam: 'Raam (vast glas)', deur: 'Deur' };
const FILL_TYPES = { geen: 'Geen (open)', raam: 'Raam (vast glas)', deur: 'Deur', schuifdeur: 'Schuifdeur' };
const HINGES = { links: 'Links', rechts: 'Rechts' };
const SWINGS = { binnen: 'Naar binnen', buiten: 'Naar buiten' };
const SLIDE_MOUNTS = { inbouw: 'In de wand', opbouw: 'Op de wand (rail)' };
const SLIDE_SIDES = { binnen: 'Binnenzijde', buiten: 'Buitenzijde' };
const SLIDE_DIRS = { links: 'Naar links', rechts: 'Naar rechts' };
const DEFAULT_OUT = 'antraciet', DEFAULT_IN = 'wit';
// Maten (mm).
const P = {
gap: 5, // stelruimte rondom
kader: 70, // kader in het aanzicht
depth: 120, // kaderdiepte
dorpel: 30, // deurdorpel (hoogte)
gap: 5,
kader: 70,
depth: 120,
dorpel: 30,
tussen: 70, // tussenstijl/-regel (deur naast zijlicht, onder bovenlicht)
rooster: 80, // ventilatierooster (hoogte)
glass: 24, // glasdikte
rooster: 80,
glass: 24,
leafW: 930, // deurblad (binnenmaat kader) als er een zijlicht naast past
leafH: 2200, // deurblad als er een bovenlicht boven past
leafT: 68, // deurbladdikte
minLight: 250 // kleinste zijlicht/bovenlicht
leafT: 68,
minLight: 200
};
const colorKey = (k, def) => COLORS[k] ? k : def;
// Volledige vulling (defaults ingevuld) of null (geen vulling).
const pick = (v, opts, def) => opts[v] ? v : def;
function normalize(fill) {
if (!fill || !FILL_TYPES[fill.type] || fill.type === 'geen') return null;
return {
const f = {
type: fill.type, rooster: !!fill.rooster, glas: fill.glas !== false,
out: colorKey(fill.out, DEFAULT_OUT), in: colorKey(fill.in, DEFAULT_IN)
};
if (f.type === 'deur') {
f.hinge = pick(fill.hinge, HINGES, 'links');
f.leaf = pick(fill.leaf, HINGES, f.hinge);
f.swing = pick(fill.swing, SWINGS, 'binnen');
f.volglas = !!fill.volglas;
}
if (f.type === 'schuifdeur') Object.assign(f, {
leaves: Number(fill.leaves) === 2 ? 2 : 1, mount: pick(fill.mount, SLIDE_MOUNTS, 'inbouw'),
side: pick(fill.side, SLIDE_SIDES, 'binnen'), dir: pick(fill.dir, SLIDE_DIRS, 'links')
});
return f;
}
// Standaardvulling bij een openingtype (WallModel.OPENING_TYPES); kleuren
// van `like` (bijv. de vorige opening) overnemen.
function defaultFill(openingType, like) {
const l = normalize(like) || {};
const type = openingType === 'deur' || openingType === 'voordeur' ? 'deur' : 'raam';
return { type, rooster: false, glas: openingType === 'voordeur', out: l.out || DEFAULT_OUT, in: l.in || DEFAULT_IN };
}
// ------------------------------------------------------------
// 2D-veelhoeken (mm, tegen de klok in, x vanaf de linkerkant van de
// opening, y vanaf de onderkant).
// ------------------------------------------------------------
// Convexe veelhoek naar binnen verschoven, per zijde (zijde i = punt i ->
// punt i+1) een eigen afstand d[i].
function insetPoly(pts, d) {
const n = pts.length;
const lines = pts.map((p, i) => {
@@ -87,8 +79,6 @@ window.KozijnModel = (() => {
return { x: a.x + a.dx * t, y: a.y + a.dy * t };
});
}
// Veelhoek afgekapt op een halfvlak (Sutherland-Hodgman): f(p) >= 0
// blijft, f is lineair.
function clipPoly(pts, f) {
const out = [];
pts.forEach((p, i) => {
@@ -105,9 +95,6 @@ window.KozijnModel = (() => {
const rect = (x0, x1, y0, y1) => [{ x: x0, y: y0 }, { x: x1, y: y0 }, { x: x1, y: y1 }, { x: x0, y: y1 }];
const areaOf = (pts) => Math.abs(pts.reduce((a, p, i) => { const q = pts[(i + 1) % pts.length]; return a + p.x * q.y - q.x * p.y; }, 0)) / 2;
// ------------------------------------------------------------
// 3D
// ------------------------------------------------------------
const mats = {};
function colorMat(key) {
const k = 'c:' + colorKey(key, DEFAULT_OUT);
@@ -117,7 +104,7 @@ window.KozijnModel = (() => {
if (mats[key]) return mats[key];
if (key === 'glass') mats[key] = new THREE.MeshStandardMaterial({ color: 0x9fbfcc, roughness: 0.05, metalness: 0.2, transparent: true, opacity: 0.28, depthWrite: false, side: THREE.DoubleSide });
else if (key === 'rubber') mats[key] = new THREE.MeshStandardMaterial({ color: 0x1b1b1b, roughness: 0.8 });
else mats[key] = new THREE.MeshStandardMaterial({ color: 0x9a9da0, roughness: 0.3, metalness: 0.7 }); // beslag
else mats[key] = new THREE.MeshStandardMaterial({ color: 0x9a9da0, roughness: 0.3, metalness: 0.7 });
return mats[key];
}
@@ -127,8 +114,6 @@ window.KozijnModel = (() => {
(holes || []).forEach(h => s.holes.push(new THREE.Path(h.map(p => new THREE.Vector2(p.x * toM, p.y * toM)))));
return s;
};
// Plaat van z0 tot z1 (mm); buitenvlak (z1) in matOut als dat afwijkt
// (folie aan de buitenzijde, de rest in de binnenkleur).
function slab(g, pts, holes, z0, z1, matIn, matOut, shadow) {
if (pts.length < 3 || areaOf(pts) < 1) return;
const FOIL = matOut && matOut !== matIn ? 1 : 0;
@@ -149,10 +134,7 @@ window.KozijnModel = (() => {
g.add(mesh);
};
// Kozijn in een opening (opgelost: w, h links, hR rechts). Geeft een
// groep in mm-lokale coördinaten van de opening (x vanaf links, y vanaf
// de onderkant, +z = buiten) in meters, of null.
// place: { zOut (mm, buitenvlak kozijn), depth (mm), exterior (dorpel) }
// Lokaal: x vanaf links, y vanaf de onderkant, +z = buiten (mm in, meters uit).
function build3D(op, fill, place) {
const f = normalize(fill);
if (!f) return null;
@@ -162,8 +144,6 @@ window.KozijnModel = (() => {
const z1 = place.zOut, z0 = z1 - depth;
const hR = op.hR !== undefined ? op.hR : op.h;
const outer = insetPoly([{ x: 0, y: 0 }, { x: op.w, y: 0 }, { x: op.w, y: hR }, { x: 0, y: op.h }], [P.gap, P.gap, P.gap, P.gap]);
// Bovenkant (schuin) van een veelhoek binnen het kader: >= 0 onder de
// lijn door TL-TR van `q` (lineair, voor clipPoly).
const below = (q) => (p) => {
const a = q[3], b = q[2];
return (b.y - a.y) * (p.x - a.x) - (b.x - a.x) * (p.y - a.y);
@@ -178,7 +158,6 @@ window.KozijnModel = (() => {
pane = insetPoly(inner, [0, 0, P.rooster, 0]);
const band = [pane[3], pane[2], inner[2], inner[3]];
slab(g, [band[0], band[1], band[2], band[3]], null, z1 - 105, z1 - 8, mIn, mOut);
// Lamellen: donkere sleuven op beide vlakken.
[0.3, 0.55, 0.8].forEach(t => {
const a = insetPoly(inner, [0, 20, P.rooster * t - 4, 20]), b = insetPoly(inner, [0, 20, P.rooster * t + 4, 20]);
const slot = [b[3], b[2], a[2], a[3]];
@@ -190,8 +169,6 @@ window.KozijnModel = (() => {
return g;
}
// Deur: kader als U (zonder onderregel) op een dorpel (buitenwand) of
// tot de vloer (binnenwand).
const sill = place.exterior !== false ? P.dorpel : 0;
const frameOuter = insetPoly(outer, [sill, 0, 0, 0]);
const inner = insetPoly(frameOuter, [0, P.kader, P.kader, P.kader]);
@@ -202,20 +179,25 @@ window.KozijnModel = (() => {
const ix0 = inner[0].x, ix1 = inner[1].x, iy0 = inner[0].y;
const top = below(inner);
const topAt = (x) => inner[3].y + (x - inner[3].x) / (inner[2].x - inner[3].x) * (inner[2].y - inner[3].y);
// Zijlicht rechts als er naast een deurblad nog genoeg ruimte is.
const region = (x0, x1, y0, y1) => clipPoly(rect(x0, x1, y0, Math.min(y1, 1e6)), top);
const bar = (pts) => slab(g, pts, null, z0 + 10, z1 - 10, mIn, mOut);
const link = `${Math.round(op.x || 0)}|${Math.round(op.y || 0)}|${op.w}`;
if (f.type === 'schuifdeur') {
buildSlideLeaves(g, f, { ix0, ix1, iy0, topAt, z0, z1, mIn, mOut, sill, place, link, op });
return g;
}
const hingeR = f.hinge === 'rechts', leafR = f.leaf === 'rechts';
const side = ix1 - ix0 - P.leafW - P.tussen >= P.minLight;
const lx1 = side ? ix0 + P.leafW : ix1;
// Bovenlicht als er boven een deurblad nog genoeg ruimte is (overal).
const la = side && leafR ? ix1 - P.leafW : ix0, lb = side && !leafR ? ix0 + P.leafW : ix1;
const lowTop = Math.min(topAt(ix0), topAt(ix1));
const transom = lowTop - iy0 - P.leafH - P.tussen >= P.minLight;
const ly1 = transom ? iy0 + P.leafH : Infinity;
const region = (x0, x1, y0, y1) => clipPoly(rect(x0, x1, y0, Math.min(y1, 1e6)), top);
const bar = (pts) => slab(g, pts, null, z0 + 10, z1 - 10, mIn, mOut);
if (side) {
bar(region(lx1, lx1 + P.tussen, iy0, ly1));
// Zijlicht: onderregel + glas.
bar(region(lx1 + P.tussen, ix1, iy0, iy0 + P.kader));
glass(region(lx1 + P.tussen, ix1, iy0 + P.kader, ly1));
const [t0, g0, g1] = leafR ? [la - P.tussen, ix0, la - P.tussen] : [lb, lb + P.tussen, ix1];
bar(region(t0, t0 + P.tussen, iy0, ly1));
bar(region(g0, g1, iy0, iy0 + P.kader));
glass(region(g0, g1, iy0 + P.kader, ly1));
}
if (transom) {
bar(region(ix0, ix1, ly1, ly1 + P.tussen));
@@ -223,31 +205,126 @@ window.KozijnModel = (() => {
}
// Deurblad (3 mm speling, onderaan 10 mm boven dorpel/vloer).
const leaf = clipPoly(rect(ix0 + 3, lx1 - 3, iy0 + (sill ? 3 : 10), Math.min(ly1 - 3, 1e6)), (p) => top({ x: p.x, y: p.y + 3 }));
const leaf = clipPoly(rect(la + 3, lb - 3, iy0 + (sill ? 3 : 10), Math.min(ly1 - 3, 1e6)), (p) => top({ x: p.x, y: p.y + 3 }));
if (leaf.length < 3) return g;
const xs = leaf.map(p => p.x), ys = leaf.map(p => p.y);
const lx0 = Math.min(...xs), lxR = Math.max(...xs), lb = Math.min(...ys), lt = Math.min(topAt(lx0), topAt(lxR), ly1) - 3;
const lx0 = Math.min(...xs), lxR = Math.max(...xs), lb0 = Math.min(...ys), lt = Math.min(topAt(lx0), topAt(lxR), ly1) - 3;
const lzOut = z1 - 15, lzIn = lzOut - P.leafT;
let holes = null;
const lw = lxR - lx0;
if (f.glas && lw >= 500 && lt - lb >= 1400) {
const gx = lx0 + lw * 0.3, gw = Math.min(160, lw * 0.18);
const strip = rect(gx - gw / 2, gx + gw / 2, lb + 350, lt - 300);
holes = [strip];
slab(g, strip, null, (lzIn + lzOut) / 2 - P.glass / 2, (lzIn + lzOut) / 2 + P.glass / 2, mat('glass'), null, false);
if (f.volglas && lw >= 400 && lt - lb0 >= 800) {
holes = [rect(lx0 + 110, lxR - 110, lb0 + 140, lt - 110)]; // glasdeur: smal kader rondom
} else if (f.glas && lw >= 500 && lt - lb0 >= 1400) {
const gx = hingeR ? lxR - lw * 0.3 : lx0 + lw * 0.3, gw = Math.min(160, lw * 0.18);
holes = [rect(gx - gw / 2, gx + gw / 2, lb0 + 350, lt - 300)]; // glas: in het deurblad (hieronder)
}
slab(g, leaf, holes, lzIn, lzOut, mIn, mOut);
// Kruk aan beide zijden (scharnieren links, slot rechts).
const outward = f.swing === 'buiten';
const hx = hingeR ? lxR : lx0, hz = outward ? lzOut : lzIn;
const pivot = new THREE.Group(), lg = new THREE.Group();
pivot.position.set(hx * toM, 0, hz * toM);
lg.position.set(-hx * toM, 0, -hz * toM);
pivot.add(lg);
g.add(pivot);
if (holes) slab(lg, holes[0], null, (lzIn + lzOut) / 2 - P.glass / 2, (lzIn + lzOut) / 2 + P.glass / 2, mat('glass'), null, false);
slab(lg, leaf, holes, lzIn, lzOut, mIn, mOut);
const hy = Math.min(lb0 + 1050, (lb0 + lt) / 2);
if (lw >= 400) {
const hx = lxR - 75, hy = Math.min(lb + 1050, (lb + lt) / 2);
const kx = hingeR ? lx0 + 75 : lxR - 75, gd = hingeR ? 60 : -60;
[[lzOut, 1], [lzIn, -1]].forEach(([z, s]) => {
box(g, mat('beslag'), hx, hy - 30, z + s * 5, 34, 210, 10); // schild
box(g, mat('beslag'), hx, hy, z + s * 30, 20, 20, 50); // nek
box(g, mat('beslag'), hx - 60, hy, z + s * 55, 140, 20, 20); // greep
box(lg, mat('beslag'), kx, hy - 30, z + s * 5, 34, 210, 10); // schild
box(lg, mat('beslag'), kx, hy, z + s * 30, 20, 20, 50); // nek
box(lg, mat('beslag'), kx + gd, hy, z + s * 55, 140, 20, 20); // greep
});
}
const dir = (hingeR ? -1 : 1) * (outward ? -1 : 1);
pivot.userData.door = {
kind: 'swing', dir, max: Math.PI / 2, open: 0, target: 0, link,
base: pivot.position.x, center: new THREE.Vector3((lx0 + lxR) / 2 * toM, hy * toM, (lzIn + lzOut) / 2 * toM)
};
return g;
}
return { COLORS, FILL_TYPES, PROFILE: P, normalize, defaultFill, build3D };
const SLIDE = { overlap: 50, gap: 12, railH: 40, railD: 45, leafT: 40, frame: 70 };
function buildSlideLeaves(g, f, o) {
const n = f.leaves, opbouw = f.mount === 'opbouw';
const out = f.side === 'buiten';
const faceIn = o.place.faceIn !== undefined ? o.place.faceIn : o.z0;
const faceOut = o.place.faceOut !== undefined ? o.place.faceOut : o.z1;
const ov = opbouw ? SLIDE.overlap : -3;
const xa = o.ix0 - ov, xb = o.ix1 + ov;
const yb = o.iy0 + (o.sill ? 3 : 10);
const yt = Math.min(o.topAt(o.ix0), o.topAt(o.ix1)) + (opbouw ? SLIDE.overlap : -3);
const lt = opbouw ? SLIDE.leafT : P.leafT;
const [lz0, lz1] = opbouw
? (out ? [faceOut + SLIDE.gap, faceOut + SLIDE.gap + lt] : [faceIn - SLIDE.gap - lt, faceIn - SLIDE.gap])
: [(faceIn + faceOut) / 2 - lt / 2, (faceIn + faceOut) / 2 + lt / 2];
const lw = (xb - xa) / n;
const dirs = n === 2 ? [-1, 1] : [f.dir === 'rechts' ? 1 : -1];
dirs.forEach((dir, i) => {
const x0 = xa + i * lw + (n === 2 ? (i ? 1 : 0) : 0), x1 = xa + (i + 1) * lw - (n === 2 && !i ? 1 : 0);
const lg = new THREE.Group();
g.add(lg);
const pts = rect(x0, x1, yb, yt);
if (f.glas && x1 - x0 > 2 * SLIDE.frame + 100) {
const pane = rect(x0 + SLIDE.frame, x1 - SLIDE.frame, yb + SLIDE.frame + 30, yt - SLIDE.frame);
slab(lg, pts, [pane], lz0, lz1, colorMat(f.in), opbouw ? null : colorMat(f.out));
slab(lg, pane, null, (lz0 + lz1) / 2 - 4, (lz0 + lz1) / 2 + 4, mat('glass'), null, false);
} else {
slab(lg, pts, null, lz0, lz1, colorMat(f.in), opbouw ? null : colorMat(f.out));
}
const hx = dir < 0 ? x1 - 60 : x0 + 60, hy = Math.min(yb + 1050, (yb + yt) / 2);
if (opbouw) [lz0 - 18, lz1 + 18].forEach(z => box(lg, mat('beslag'), hx, hy, z, 20, 400, 20));
else [lz0 - 1, lz1 + 1].forEach(z => box(lg, mat('beslag'), hx, hy, z, 24, 160, 2));
lg.userData.door = {
kind: 'slide', dir, max: (x1 - x0 - (opbouw ? 2 * SLIDE.overlap : 60)) * toM, open: 0, target: 0, link: o.link,
base: 0, center: new THREE.Vector3((x0 + x1) / 2 * toM, hy * toM, (lz0 + lz1) / 2 * toM)
};
});
if (opbouw) {
const ra = n === 2 ? xa - lw : (dirs[0] < 0 ? xa - lw : xa), rb = n === 2 ? xb + lw : (dirs[0] < 0 ? xb : xb + lw);
const rz = out ? faceOut + SLIDE.railD / 2 : faceIn - SLIDE.railD / 2;
box(g, mat('beslag'), (ra + rb) / 2, yt + SLIDE.railH / 2 + 5, rz, rb - ra, SLIDE.railH, SLIDE.railD);
}
}
function planSymbol(op, fill, faces) {
const f = normalize(fill);
if (!f || f.type === 'raam') return [];
const zIn = faces.zIn, zOut = faces.zOut;
const ix0 = P.gap + P.kader, ix1 = op.w - P.gap - P.kader;
const out = [];
if (f.type === 'deur') {
const hingeR = f.hinge === 'rechts', leafR = f.leaf === 'rechts';
const side = ix1 - ix0 - P.leafW - P.tussen >= P.minLight;
const la = side && leafR ? ix1 - P.leafW : ix0, lb = side && !leafR ? ix0 + P.leafW : ix1;
const L = lb - la, hx = hingeR ? lb : la, s = hingeR ? -1 : 1;
const outward = f.swing === 'buiten', hz = outward ? zOut : zIn, dz = outward ? 1 : -1;
out.push({ kind: 'leaf', pts: [{ x: hx, z: hz }, { x: hx, z: hz + dz * L }] });
const arc = [];
for (let k = 0; k <= 16; k++) { const t = k / 16 * Math.PI / 2; arc.push({ x: hx + s * L * Math.cos(t), z: hz + dz * L * Math.sin(t) }); }
out.push({ kind: 'arc', pts: arc });
return out;
}
const n = f.leaves, opbouw = f.mount === 'opbouw', sideOut = f.side === 'buiten';
const ov = opbouw ? SLIDE.overlap : -3;
const xa = ix0 - ov, xb = ix1 + ov, lw = (xb - xa) / n;
const lt = opbouw ? SLIDE.leafT : P.leafT;
const [z0, z1] = opbouw
? (sideOut ? [zOut + SLIDE.gap, zOut + SLIDE.gap + lt] : [zIn - SLIDE.gap - lt, zIn - SLIDE.gap])
: [(zIn + zOut) / 2 - lt / 2, (zIn + zOut) / 2 + lt / 2];
const dirs = n === 2 ? [-1, 1] : [f.dir === 'rechts' ? 1 : -1];
const za = sideOut ? Math.max(zOut, z1) + 140 : Math.min(zIn, z0) - 140;
dirs.forEach((dir, i) => {
const x0 = xa + i * lw, x1 = x0 + lw;
out.push({ kind: 'leaf', closed: true, pts: [{ x: x0, z: z0 }, { x: x1, z: z0 }, { x: x1, z: z1 }, { x: x0, z: z1 }] });
const c = (x0 + x1) / 2, len = Math.min(500, lw * 0.6);
const a = { x: c - dir * len / 2, z: za }, b = { x: c + dir * len / 2, z: za };
out.push({ kind: 'arrow', pts: [a, b] });
out.push({ kind: 'arrow', pts: [{ x: b.x - dir * 90, z: za - 45 }, b, { x: b.x - dir * 90, z: za + 45 }] });
out.push({ kind: 'rail', closed: true, pts: [{ x: x0 + dir * lw, z: z0 }, { x: x1 + dir * lw, z: z0 }, { x: x1 + dir * lw, z: z1 }, { x: x0 + dir * lw, z: z1 }] });
});
return out;
}
return { COLORS, FILL_TYPES, HINGES, SWINGS, SLIDE_MOUNTS, SLIDE_SIDES, SLIDE_DIRS, PROFILE: P, normalize, defaultFill, build3D, planSymbol, colorMaterial: colorMat };
})();