/*! * Tectum * Copyright © 2026 Machiel Heinen. * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ // 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 = { wit: { label: 'Wit', ral: 'RAL 9016', hex: 0xf1f0ea }, grijs: { label: 'Grijs', ral: 'RAL 7038', hex: 0xaeb0aa }, 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', 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'; const P = { gap: 5, kader: 70, depth: 120, dorpel: 30, tussen: 70, // tussenstijl/-regel (deur naast zijlicht, onder bovenlicht) 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, minLight: 200 }; const colorKey = (k, def) => COLORS[k] ? k : def; const pick = (v, opts, def) => opts[v] ? v : def; function normalize(fill) { if (!fill || !FILL_TYPES[fill.type] || fill.type === 'geen') return null; 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; } 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 }; } function insetPoly(pts, d) { const n = pts.length; const lines = pts.map((p, i) => { const q = pts[(i + 1) % n]; const len = Math.hypot(q.x - p.x, q.y - p.y) || 1; const dx = (q.x - p.x) / len, dy = (q.y - p.y) / len; return { x: p.x - dy * d[i], y: p.y + dx * d[i], dx, dy }; }); return pts.map((p, i) => { const a = lines[(i - 1 + n) % n], b = lines[i]; const den = a.dx * b.dy - a.dy * b.dx; if (Math.abs(den) < 1e-9) return { x: b.x, y: b.y }; const t = ((b.x - a.x) * b.dy - (b.y - a.y) * b.dx) / den; return { x: a.x + a.dx * t, y: a.y + a.dy * t }; }); } function clipPoly(pts, f) { const out = []; pts.forEach((p, i) => { const q = pts[(i + 1) % pts.length]; const fp = f(p), fq = f(q); if (fp >= 0) out.push(p); if ((fp >= 0) !== (fq >= 0)) { const t = fp / (fp - fq); out.push({ x: p.x + (q.x - p.x) * t, y: p.y + (q.y - p.y) * t }); } }); return out; } // >= 0 onder de bovenrand q[3]-q[2] van een vierhoek. 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); }; 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; const mats = {}; function colorMat(key) { const k = 'c:' + colorKey(key, DEFAULT_OUT); return mats[k] || (mats[k] = new THREE.MeshStandardMaterial({ color: COLORS[colorKey(key, DEFAULT_OUT)].hex, roughness: 0.45 })); } function mat(key) { 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 }); return mats[key]; } const toM = 0.001; const toShape = (pts, holes) => { const s = new THREE.Shape(pts.map(p => new THREE.Vector2(p.x * toM, p.y * toM))); (holes || []).forEach(h => s.holes.push(new THREE.Path(h.map(p => new THREE.Vector2(p.x * toM, p.y * toM))))); return s; }; 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; const add = (a, b, m) => { const mesh = new THREE.Mesh(new THREE.ExtrudeGeometry(toShape(pts, holes), { depth: (b - a) * toM, bevelEnabled: false }), m); mesh.position.z = a * toM; mesh.castShadow = shadow !== false; mesh.receiveShadow = true; g.add(mesh); }; add(z0, z1 - FOIL, matIn); if (FOIL) add(z1 - FOIL, z1, matOut); } const box = (g, m, x, y, z, w, h, d) => { const mesh = new THREE.Mesh(new THREE.BoxGeometry(w * toM, h * toM, d * toM), m); mesh.position.set(x * toM, y * toM, z * toM); mesh.castShadow = true; g.add(mesh); }; // Gekoppelde zijde (op.joinL/joinR, zie WallModel.resolveOpenings): geen speling en geen kaderstijl; // 'stijl' = de helft van een tussenstijl die het kozijn ernaast afmaakt, 'stolp' = niets (bladen tegen elkaar). const sideGap = (join) => join ? 0 : P.gap; const sideKader = (join) => join === 'stolp' ? 0 : join ? P.tussen / 2 : P.kader; // 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; const g = new THREE.Group(); const mIn = colorMat(f.in), mOut = colorMat(f.out); const depth = Math.min(P.depth, place.depth || P.depth); const z1 = place.zOut, z0 = z1 - depth; const hR = op.hR !== undefined ? op.hR : op.h; const kL = sideKader(op.joinL), kR = sideKader(op.joinR); const outer = insetPoly([{ x: 0, y: 0 }, { x: op.w, y: 0 }, { x: op.w, y: hR }, { x: 0, y: op.h }], [P.gap, sideGap(op.joinR), P.gap, sideGap(op.joinL)]); const glass = (pts) => slab(g, pts, null, z1 - 50 - P.glass / 2, z1 - 50 + P.glass / 2, mat('glass'), null, false); if (f.type === 'raam') { const inner = insetPoly(outer, [P.kader, kR, P.kader, kL]); slab(g, outer, [inner], z0, z1, mIn, mOut); let pane = inner; if (f.rooster && Math.min(inner[3].y, inner[2].y) - inner[0].y > P.rooster + 200) { 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); [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]]; slab(g, slot, null, z1 - 8, z1 - 6, mat('rubber')); slab(g, slot, null, z1 - 107, z1 - 105, mat('rubber')); }); } glass(pane); return g; } const sill = place.exterior !== false ? P.dorpel : 0; const frameOuter = insetPoly(outer, [sill, 0, 0, 0]); const inner = insetPoly(frameOuter, [0, kR, P.kader, kL]); const u = [...(kL ? [frameOuter[0], inner[0]] : []), inner[3], inner[2], ...(kR ? [inner[1], frameOuter[1]] : []), frameOuter[2], frameOuter[3]]; slab(g, u, null, z0, z1, mIn, mOut); if (sill) box(g, mat('rubber'), (outer[0].x + outer[1].x) / 2, outer[0].y + sill / 2, z1 - depth / 2 + 15, outer[1].x - outer[0].x, sill, depth + 30); 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); 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'; const d = doorSplit(f, ix0, ix1, iy0, Math.min(topAt(ix0), topAt(ix1))); const { side, leafR, la, lb, transom, ly1 } = d; if (side) { 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)); glass(region(ix0, ix1, ly1 + P.tussen, Infinity)); } const L = doorLeaf(f, d, iy0, sill, top, topAt); if (!L) return g; const { leaf, lx0, lxR, lb0, lt, lw, holes } = L; const lzOut = z1 - 15, lzIn = lzOut - P.leafT; 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 kx = hingeR ? lx0 + 75 : lxR - 75, gd = hingeR ? 60 : -60; [[lzOut, 1], [lzIn, -1]].forEach(([z, s]) => { 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; } const SLIDE = { overlap: 50, gap: 12, railH: 40, railD: 45, leafT: 40, frame: 70 }; // Deurblad en zij-/bovenlicht binnen de kaderopening ix0..ix1 vanaf iy0 (lowTop = laagste bovenkant). function doorSplit(f, ix0, ix1, iy0, lowTop) { const 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 transom = lowTop - iy0 - P.leafH - P.tussen >= P.minLight; return { side, leafR, la, lb, transom, ly1: transom ? iy0 + P.leafH : Infinity }; } // Deurblad (3 mm speling, onderaan 10 mm boven dorpel/vloer) met het glas erin. function doorLeaf(f, d, iy0, sill, top, topAt) { const leaf = clipPoly(rect(d.la + 3, d.lb - 3, iy0 + (sill ? 3 : 10), Math.min(d.ly1 - 3, 1e6)), (p) => top({ x: p.x, y: p.y + 3 })); if (leaf.length < 3) return null; const xs = leaf.map(p => p.x), ys = leaf.map(p => p.y); const lx0 = Math.min(...xs), lxR = Math.max(...xs), lb0 = Math.min(...ys), lt = Math.min(topAt(lx0), topAt(lxR), d.ly1) - 3; const lw = lxR - lx0; let holes = null; 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 = f.hinge === 'rechts' ? 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)]; } return { leaf, lx0, lxR, lb0, lt, lw, holes }; } function slideLeaves(f, ix0, ix1, iy0, sill, topAt) { const n = f.leaves, opbouw = f.mount === 'opbouw'; const ov = opbouw ? SLIDE.overlap : -3; const xa = ix0 - ov, xb = ix1 + ov; const yb = iy0 + (sill ? 3 : 10); const yt = Math.min(topAt(ix0), topAt(ix1)) + (opbouw ? SLIDE.overlap : -3); const lw = (xb - xa) / n; const dirs = n === 2 ? [-1, 1] : [f.dir === 'rechts' ? 1 : -1]; const leaves = dirs.map((dir, i) => ({ dir, x0: xa + i * lw + (n === 2 ? (i ? 1 : 0) : 0), x1: xa + (i + 1) * lw - (n === 2 && !i ? 1 : 0) })); const pane = (l) => f.glas && l.x1 - l.x0 > 2 * SLIDE.frame + 100 ? rect(l.x0 + SLIDE.frame, l.x1 - SLIDE.frame, yb + SLIDE.frame + 30, yt - SLIDE.frame) : null; return { n, opbouw, xa, xb, yb, yt, lw, dirs, leaves, pane }; } function buildSlideLeaves(g, f, o) { const S = slideLeaves(f, o.ix0, o.ix1, o.iy0, o.sill, o.topAt); const { n, opbouw, xa, xb, yb, yt, lw, dirs } = S; 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 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]; S.leaves.forEach(l => { const { dir, x0, x1 } = l; const lg = new THREE.Group(); g.add(lg); const pts = rect(x0, x1, yb, yt); const pane = S.pane(l); if (pane) { 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 = sideGap(op.joinL) + sideKader(op.joinL), ix1 = op.w - sideGap(op.joinR) - sideKader(op.joinR); const out = []; if (f.type === 'deur') { const hingeR = f.hinge === 'rechts'; const { la, lb } = doorSplit(f, ix0, ix1, 0, 0); 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; } // Aanzicht van buiten, lokaal als build3D (x vanaf links, y vanaf de onderkant van de opening): gesloten omtrekken. function viewLines(op, fill, exterior) { const f = normalize(fill); if (!f) return []; const hR = op.hR !== undefined ? op.hR : op.h; const kL = sideKader(op.joinL), kR = sideKader(op.joinR); const outer = insetPoly([{ x: 0, y: 0 }, { x: op.w, y: 0 }, { x: op.w, y: hR }, { x: 0, y: op.h }], [P.gap, sideGap(op.joinR), P.gap, sideGap(op.joinL)]); const out = [outer]; const add = (pts) => { if (pts && pts.length > 2 && areaOf(pts) > 1) out.push(pts); }; if (f.type === 'raam') { const inner = insetPoly(outer, [P.kader, kR, P.kader, kL]); add(inner); if (f.rooster && Math.min(inner[3].y, inner[2].y) - inner[0].y > P.rooster + 200) add(insetPoly(inner, [0, 0, P.rooster, 0])); return out; } const sill = exterior !== false ? P.dorpel : 0; const frameOuter = insetPoly(outer, [sill, 0, 0, 0]); const inner = insetPoly(frameOuter, [0, kR, P.kader, kL]); if (sill) add(frameOuter); add(inner); 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); const region = (x0, x1, y0, y1) => clipPoly(rect(x0, x1, y0, Math.min(y1, 1e6)), top); if (f.type === 'schuifdeur') { const S = slideLeaves(f, ix0, ix1, iy0, sill, topAt); if (!S.opbouw || f.side === 'buiten') S.leaves.forEach(l => { add(region(l.x0, l.x1, S.yb, S.yt)); add(S.pane(l)); }); return out; } const d = doorSplit(f, ix0, ix1, iy0, Math.min(topAt(ix0), topAt(ix1))); if (d.side) { const t0 = d.leafR ? d.la - P.tussen : d.lb; add(region(t0, t0 + P.tussen, iy0, d.ly1)); const [g0, g1] = d.leafR ? [ix0, d.la - P.tussen] : [d.lb + P.tussen, ix1]; add(region(g0, g1, iy0, iy0 + P.kader)); } if (d.transom) add(region(ix0, ix1, d.ly1, d.ly1 + P.tussen)); const L = doorLeaf(f, d, iy0, sill, top, topAt); if (L) { add(L.leaf); (L.holes || []).forEach(add); } return out; } return { COLORS, FILL_TYPES, HINGES, SWINGS, SLIDE_MOUNTS, SLIDE_SIDES, SLIDE_DIRS, PROFILE: P, normalize, defaultFill, build3D, planSymbol, viewLines, colorMaterial: colorMat }; })();