2 Commits
13 changed files with 114 additions and 47 deletions
+7 -1
View File
@@ -350,7 +350,9 @@ assets/ logo, texturen, skybox
Beperkte configurator voor de websitepagina van één model (later in de HUGO-build; nu los via
`light.html?model=<sleutel uit HouseModel.MODELS>`). Inbedden: de stylesheets en scripts uit `light.html`
plus `<div class="tectum-light" data-model="amstel"></div>` (mount vanzelf), of
`TectumLight.mount(el, { model, frames, url, modelViewer })`.
`TectumLight.mount(el, { model, frames, url, modelViewer, save })`. De engine hoeft niet naast de pagina te staan:
texturen, kleurstalen en `kosten/*.csv` laden via `tectumUrl()` (`js/core/geometry.js`) vanaf de map van de scripts
(bijv. `/configurator/` op een modelpagina van de site); paden in de data blijven relatief.
- **Rekent als de configurator** (zelfde `HouseModel.computeParts`, totalen en `CostModel`), maar toont
alleen: indicatieve prijs incl. btw (afgerond op € 100), buitenmaten, bouwhoogte, bruto/leefbaar m²,
@@ -393,6 +395,10 @@ plus `<div class="tectum-light" data-model="amstel"></div>` (mount vanzelf), of
kosten) en als laatste het stappenplan van de site ("Hoe verloopt het traject?") met een blok over maatwerk
met de eigen software (keuken op maat, indeling, ramen en deuren). Contact en stappen staan
in `LightPdf.COMPANY` en `STEPS`.
- **Eigen opslaan** (`save: { label, icon, onSave }`): vervangt de PDF-knop, bijv. "Ontwerp aanvragen" op de site.
`onSave` krijgt `{ design, label, key, info, choices, url }`: `design` is de house-design-JSON zoals de
configurator die opslaat (de CRM maakt daar het gekozen model van), `info` de kerncijfers met prijs, `choices` de
keuzes in woorden. Zonder `save` (en via `el.tectumLight.design()`) hetzelfde ontwerp.
- **AR / schermvullend:** knop rechtsboven in de weergave. Op een telefoon of tablet: het ontwerp als GLB
(`GlbWriter`, afgewerkte woning zoals in 3D, één primitive per materiaal, texturen als JPEG/PNG tot 1024 px,
lichtbundels weg; maaiveld onder het midden = oorsprong, `HouseModel.groundOrigin`) in model-viewer, over de
+1 -1
View File
@@ -4,7 +4,7 @@
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
const APP_VERSION = '0.7.6';
const APP_VERSION = '0.7.8';
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
let tabs = [];
+11
View File
@@ -6,6 +6,17 @@
*/
// Plain script, geen ES-module: imports breken bij file:// (CORS).
// Map van de engine (met assets/ en kosten/), afgeleid van de plek van dit script. Texturen, kleurstalen en de
// kosten-CSV's laden via tectumUrl(), zodat ze ook werken als de engine in een andere pagina staat (light-versie op
// een modelpagina van de site). Paden in de data blijven relatief.
const TECTUM_BASE = (() => {
const s = typeof document !== 'undefined' && document.currentScript && document.currentScript.src;
return s ? new URL('../../', s).href : '';
})();
function tectumUrl(path) {
return path && !/^(?:[a-z][a-z0-9+.-]*:|\/)/i.test(path) ? TECTUM_BASE + path : path;
}
function calculateSegmentPositions(spanWidth, minSp, maxSp) {
if (spanWidth <= 0) return [];
let numBays = Math.ceil(spanWidth / maxSp);
+1 -1
View File
@@ -23,7 +23,7 @@ window.Skybox = (() => {
const cache = {};
function loadTexture(url) {
if (!cache[url]) {
cache[url] = new Promise((resolve, reject) => new THREE.TextureLoader().load(url, resolve, undefined, reject));
cache[url] = new Promise((resolve, reject) => new THREE.TextureLoader().load(tectumUrl(url), resolve, undefined, reject));
cache[url].catch(() => { delete cache[url]; });
}
return cache[url];
+1 -1
View File
@@ -114,7 +114,7 @@ window.CostCsv = (() => {
}
function loadList(list) {
const url = LISTS[list].url;
return fetch(url, { cache: 'no-store' })
return fetch(tectumUrl(url), { cache: 'no-store' })
.then(r => r.ok ? r.text().then(t => apply(list, t, 'server', url, r.headers.get('last-modified') || new Date().toISOString())) : null)
.catch(() => null);
}
+2 -2
View File
@@ -293,7 +293,7 @@ window.FloorModel = (() => {
}
function finishThumbUrl(key, px) {
const f = FINISHES[key];
return f && f.file ? f.file.replace(/[^/]+$/, (n) => `thumb/${px || 64}/` + n.replace(/\.jpg$/, '.png')) : null;
return f && f.file ? tectumUrl(f.file.replace(/[^/]+$/, (n) => `thumb/${px || 64}/` + n.replace(/\.jpg$/, '.png'))) : null;
}
const finishMaterials = {};
function finishMaterial(key) {
@@ -307,7 +307,7 @@ window.FloorModel = (() => {
}
const mat = new THREE.MeshStandardMaterial({ color: 0xb89a7c, roughness: 0.7 });
if (typeof document !== 'undefined' && THREE.TextureLoader) {
new THREE.TextureLoader().load(f.file, (tex) => {
new THREE.TextureLoader().load(tectumUrl(f.file), (tex) => {
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(1000 / f.tileMM, 1000 / f.tileMM);
tex.anisotropy = 8;
+14 -6
View File
@@ -1500,10 +1500,10 @@ window.HouseModel = (() => {
});
const ridgePairs = houseShape ? {} : doubleLessenaarPairs(state, rects, cuts);
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop, fels) {
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop, fels, endCaps) {
if (!build) return { group: null, stats: null, specs: null };
const built = WallModel.build3D(config, frame, {
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop, fels,
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop, fels, endCaps,
painted: state.settings.interiorPaint && shows('afwerking'), layers, rabatColor: state.settings.facade.rabatColor
});
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
@@ -1918,14 +1918,22 @@ window.HouseModel = (() => {
// De rand begint op de dakrand, die op of iets vóór het wandvlak kan liggen.
const sg = [10, 60, 150].map(d => ({ x: m.x + nrm.x * d, z: m.z + nrm.z * d }))
.map(q => roofEdge.segments.find(e => pointInPoly(q, [e.a, e.b, e.outerB, e.outerA]))).find(Boolean);
if (!sg) return;
const above = sg.fels ? RoofModel.FELS_EDGE.aboveMM : roofEdge.edgeAbove;
p.battenAbove = (p.skinAbove || 0) + above - RoofModel.edgeHeight(roofEdge, sg);
// Daktrim: de bekleding loopt door tot het dak, de trim valt eroverheen.
if (!sg || (!sg.fascia && !sg.fels)) return;
p.battenAbove = (p.skinAbove || 0) + sg.above - RoofModel.edgeHeight(roofEdge, sg);
});
// Vrije uiteinden van binnenwanden: vlak voorbij het eind geen andere wand.
const allPieces = extPieces.concat(interior);
interior.forEach(p => {
if (p.buitenMateriaal !== 'spaanplaat') return;
const W = p.place.width / 2, against = (q) => allPieces.some(o => o !== p && pointInPoly(q,
[[-1, -1], [1, -1], [1, 1], [-1, 1]].map(([i, j]) => UnitModel.localToWorld(o.place, i * o.place.width / 2, j * o.place.thickness / 2))));
p.endCaps = { left: !against(UnitModel.localToWorld(p.place, -W - 15, 0)), right: !against(UnitModel.localToWorld(p.place, W + 15, 0)) };
});
assignFacades(extPieces.concat(interior.filter(p => p.skinFrom !== undefined)));
const rooms = RoomModel.compute(state.rooms || [], units, extPieces.concat(interior));
extPieces.concat(interior).forEach(p => {
const built = buildWall(p.config, p.frame, p.buitenMateriaal, p.cladding, p.trasraam, p.skinExt, p.facade, p.skinFrom, p.spaanplaatBelow, p.stone, p.skinAbove, p.trimTop, p.battenAbove, p.stuc, p.skinTop, p.fels);
const built = buildWall(p.config, p.frame, p.buitenMateriaal, p.cladding, p.trasraam, p.skinExt, p.facade, p.skinFrom, p.spaanplaatBelow, p.stone, p.skinAbove, p.trimTop, p.battenAbove, p.stuc, p.skinTop, p.fels, p.endCaps);
if (built.group && p.shiftZ) {
const outer = new THREE.Group();
built.group.position.z = p.shiftZ * 0.001;
+1 -1
View File
@@ -1413,7 +1413,7 @@ function mountHouseEditor(container, callbacks) {
sel('hs-fascia-color', 'Kleur', Object.fromEntries(Object.entries(RoofModel.FASCIA_COLORS).map(([k, c]) => [k, c.label])), r.fasciaColor) +
toggle('hs-vent', 'Ventilatierooster', r.vent !== false, { hint: `${RoofModel.VENT.HEIGHT_MM} mm hoog op de gevel, direct onder alle boeidelen` }) +
(r.vent !== false ? pair(sel('hs-vent-color', 'Kleur rooster', TRIM_OPTS, r.ventColor || 'antraciet'), swatchOf(WallModel.trimHex(r.ventColor))) : '') },
{ label: 'Daktrim', html: num('hs-trim-h', 'Hoogte', r.trimHeight, { step: 5, hint: 'Bij units zonder boeidelen' }) },
{ label: 'Daktrim', html: num('hs-trim-h', 'Hoogte', r.trimHeight, { step: 5, hint: 'Over de gevel; steekt 30 mm boven het dak uit (units zonder boeidelen)' }) },
{ label: 'Palen overkapping', html:
toggle('hs-posts', 'Palen', r.posts.enabled, { hint: `Onder een dak dat minstens de min. uitkraging buiten de vloer steekt (veranda); per unit: Overkapping. Nu ${parts ? parts.posts.count : 0} st.` }) +
(r.posts.enabled ? toggle('hs-posts-side', 'Zijwand', r.posts.sideWall, { hint: `Vrije hoek dicht met een gestuukte wand (${PostModel.SIDE_MM} mm) terug tot de gevel, in plaats van een hoekpaal` }) +
+11 -8
View File
@@ -135,20 +135,23 @@ window.LightPdf = (() => {
], 'Kerncijfers');
y = keyValues(doc, M, y + 6, 150, d.choices, 'Uw keuzes');
// Prijs
// Prijs: rustig, als de rest van de pagina; alleen het bedrag groter en vet.
const py = Math.max(y + 4, 196);
doc.setFillColor(...ORANGE);
doc.roundedRect(M, py, 150, 30, 3, 3, 'F');
doc.setTextColor(255, 255, 255);
doc.setDrawColor(...LINE);
doc.setLineWidth(0.3);
doc.line(M, py, M + 150, py);
doc.setTextColor(...MUTED);
doc.setFont('helvetica', 'normal');
doc.setFontSize(10);
doc.text('Indicatieve prijs', M + 7, py + 9);
doc.text('Indicatieve prijs', M, py + 9);
doc.setTextColor(...INK);
doc.setFont('helvetica', 'bold');
doc.setFontSize(24);
doc.text(CostModel.eur(Math.round(i.price / 100) * 100), M + 7, py + 21);
doc.setFontSize(20);
doc.text(CostModel.eur(Math.round(i.price / 100) * 100), M, py + 19);
doc.setFont('helvetica', 'normal');
doc.setFontSize(9);
doc.text('incl. btw, onder voorbehoud', M + 7, py + 27);
doc.setTextColor(...MUTED);
doc.text('incl. btw, onder voorbehoud', M, py + 25);
// Beeld van de woning
const bx = 190, by = 18, bw = PW - M - bx, bh = 206;
+20 -2
View File
@@ -8,8 +8,11 @@
// Light-versie voor de modelpagina's: één model in 3D, een paar keuzes en de kerncijfers; opslaan = PDF met
// aanzichten en plattegrond. Alle logica in LightModel (en daarachter de gewone modellen).
// Inbedden: <div class="tectum-light" data-model="amstel" data-url="…"></div> (vanzelf gemount), of
// TectumLight.mount(el, { model, frames, url, modelViewer }); url = adres voor de QR-code in de PDF (standaard de
// TectumLight.mount(el, { model, frames, url, modelViewer, save }); url = adres voor de QR-code in de PDF (standaard de
// modelpagina), modelViewer = adres van model-viewer (standaard js/vendor naast dit script).
// save = { label, icon, onSave(ontwerp) } vervangt de PDF-knop (bijv. ontwerp aanvragen op de site); onSave krijgt
// { design, label, key, info, choices, url }: design = house-design-JSON zoals de configurator die opslaat, choices = de
// keuzes in woorden (zoals op het voorblad van de PDF). Zonder save blijft het de PDF.
// Knop rechtsboven in de weergave: op een telefoon/tablet het ontwerp als GLB in model-viewer, met AR (WebXR, Scene
// Viewer, Quick Look); op de desktop schermvullend.
window.TectumLight = (() => {
@@ -501,7 +504,8 @@ window.TectumLight = (() => {
kv('Inhoud', `${nl(i.inhoudM3, 0)} m³`) + kv('Gewicht', `${nl(Math.round(i.kg / 10) * 10, 0)} kg`);
$('.tl-buy').innerHTML = `<div class="tl-price"><span class="tl-price-val">${CostModel.eur(Math.round(i.price / 100) * 100)}</span><span class="tl-price-note">indicatief, incl. btw</span></div>
<button type="button" class="tl-btn tl-icon" data-act="reset" title="Alle keuzes terug naar het model" aria-label="Terug naar het model"><i class="bi bi-arrow-counterclockwise"></i></button>
<button type="button" class="tl-btn tl-primary" data-act="pdf"><i class="bi bi-file-earmark-pdf"></i>PDF</button>`;
${o.save ? `<button type="button" class="tl-btn tl-primary" data-act="save"><i class="bi bi-${esc(o.save.icon || 'send')}"></i>${esc(o.save.label || 'Opslaan')}</button>`
: '<button type="button" class="tl-btn tl-primary" data-act="pdf"><i class="bi bi-file-earmark-pdf"></i>PDF</button>'}`;
}
// Keuzes (knoppen en kleuren).
@@ -542,6 +546,7 @@ window.TectumLight = (() => {
const a = e.target.closest('[data-act]');
if (!a || !cur) return;
if (a.dataset.act === 'pdf') savePdf();
else if (a.dataset.act === 'save') o.save.onSave(design());
else if (a.dataset.act === 'reset') tryApply(clone(model.initial));
});
panel.addEventListener('keydown', (e) => {
@@ -608,6 +613,18 @@ window.TectumLight = (() => {
}
const pageUrl = o.url || root.dataset.url || LightPdf.modelUrl(model.key);
// Het huidige ontwerp voor buiten de tool (save.onSave, el.tectumLight.design()).
function design() {
const st = clone(cur.st);
st.project.naam = model.label;
st.project.opmerking = 'Ontwerp uit de light-versie';
return {
design: Object.assign({ type: 'house-design', version: 1, exportedAt: new Date().toISOString() }, st),
label: model.label, key: model.key, info: Object.assign({}, cur.info),
choices: LightModel.describe(model, ch, cur.st), url: pageUrl
};
}
function savePdf() {
const st = clone(cur.st);
st.project.naam = model.label;
@@ -768,6 +785,7 @@ window.TectumLight = (() => {
root.innerHTML = '';
},
getChoices: () => clone(ch),
design: () => cur && design(),
// GLB van het huidige ontwerp: { blob, url, stats } (zelfde als voor AR).
glb: () => cur && makeGlb()
};
+31 -21
View File
@@ -12,6 +12,9 @@ window.RoofModel = (() => {
const DENSITY_DAKLEER_KGM2 = 1.4;
const TRIM_THICK_MM = 25;
const EDGE_ABOVE_MM = 80;
// Daktrim: voorkant 8 mm vóór de gevel, over de bovenste trimHeight van de gevel en 30 mm boven het dak; de opstand
// is 20 mm diep, daarachter ligt een flens van 3 mm op het dak over de bovenkant van de bekleding.
const TRIM = { OUT_MM: 8, ABOVE_MM: 30, DEPTH_MM: 20, FLANGE_MM: 3 };
// Felsplaat op dak én gevel: geen boeideel; de dakplaat zet om over de rand (net vóór de gevelbekleding), zonder
// opstand (de waterkant van een lessenaar), tot over de bovenkant van de gevelplaten.
const FELS_EDGE = { lipMM: 2, aboveMM: 2, heightMM: 120 };
@@ -23,7 +26,7 @@ window.RoofModel = (() => {
fasciaHeight: 300,
spotMin: 800,
spotMax: 1200,
trimHeight: 80,
trimHeight: 30,
fasciaColor: 'antraciet',
vent: true,
ventColor: 'antraciet'
@@ -87,7 +90,7 @@ window.RoofModel = (() => {
}
function coveringThumbUrl(key, px) {
const c = COVERINGS[key];
return c && c.file ? c.file.replace(/[^/]+$/, (f) => `thumb/${px || 64}/` + f.replace(/\.jpg$/, '.png')) : null;
return c && c.file ? tectumUrl(c.file.replace(/[^/]+$/, (f) => `thumb/${px || 64}/` + f.replace(/\.jpg$/, '.png'))) : null;
}
function coveringFor(key, shapeType) {
const c = COVERINGS[key];
@@ -105,7 +108,7 @@ window.RoofModel = (() => {
if (!c.file) return null;
const mat = new THREE.MeshStandardMaterial({ color: 0x9c8466, roughness: 0.95 });
if (typeof document !== 'undefined' && THREE.TextureLoader) {
new THREE.TextureLoader().load(c.file, (tex) => {
new THREE.TextureLoader().load(tectumUrl(c.file), (tex) => {
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(1000 / c.tileMM, 1000 / c.tileMM);
tex.anisotropy = 8;
@@ -1007,9 +1010,9 @@ window.RoofModel = (() => {
// Tegen een hoger dak vervalt de ribbe (daar staat de gevel), boven een lager dak blijft hij zonder spots.
const nb = others.find(r => onRectEdge(r.rect, mid));
const dropped = !!nb && nb.topAt(mid.x, mid.z) > owner.topAt(mid.x, mid.z);
const above = fels ? FELS_EDGE.aboveMM : EDGE_ABOVE_MM;
const above = fels ? FELS_EDGE.aboveMM : fascia ? EDGE_ABOVE_MM : TRIM.ABOVE_MM;
return {
a, b, owner: owner.unitId, fascia, fels, covering: owner.covering, normal,
a, b, owner: owner.unitId, fascia, fels, covering: owner.covering, normal, above,
outerA: off[i], outerB: off[j], spotA: offSpot[i], spotB: offSpot[j],
topAt: (p) => owner.topAt(p.x, p.z) + above, ridges: owner.ridges || [],
overRoof: !!nb && !dropped, dropped
@@ -1033,13 +1036,13 @@ window.RoofModel = (() => {
// facadeUnder: een eigen gevel onder de rand (niet bij een veranda of een hogere gevel ertegenaan).
segs.forEach((sg, i) => Object.assign(sg, shifted[i], { facadeDepth: depRaw[i], facadeUnder: depIn[i] !== null && depIn[i] >= 0 }));
// Rand zonder overstek (daktrim, felsrand): de felsrand ligt net vóór de gevelbekleding en loopt bij een hoek
// door tot de buitenkant van de buurrand; een boeideel ernaast stopt op de buitenkant van zo'n rand.
const edgeOff = (k) => segs[k].fels ? Math.max(TRIM_THICK_MM, depRaw[k] + FELS_EDGE.lipMM) : TRIM_THICK_MM;
// Rand zonder overstek (daktrim, felsrand): ligt net vóór de gevelbekleding en loopt bij een hoek door tot de
// buitenkant van de buurrand; een boeideel ernaast stopt op de buitenkant van zo'n rand.
const edgeOff = (k) => segs[k].fels ? Math.max(TRIM_THICK_MM, depRaw[k] + FELS_EDGE.lipMM) : Math.max(0, depRaw[k]) + TRIM.OUT_MM;
const mv = (p, nrm, d) => ({ x: p.x + nrm.x * d, z: p.z + nrm.z * d });
segs.forEach((sg, i) => {
const ends = [['a', 'outerA', (i - 1 + n) % n], ['b', 'outerB', (i + 1) % n]];
if (sg.fels) ends.forEach(([k, ok, j]) => {
if (!sg.fascia) ends.forEach(([k, ok, j]) => {
const nb = segs[j], q = mv(sg[k], sg.normal, edgeOff(i));
sg[ok] = perp(sg.normal, nb.normal) && !nb.fascia && !nb.dropped ? mv(q, nb.normal, edgeOff(j)) : q;
});
@@ -1078,7 +1081,7 @@ window.RoofModel = (() => {
segments, overhang: s.overhang, spotRatio: SPOT_RATIO,
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight, fasciaColor: s.fasciaColor,
vent: s.vent, ventColor: s.ventColor,
top, edgeAbove: EDGE_ABOVE_MM,
top,
fasciaLengthMM: segments.filter(sg => sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
ventLengthMM: s.vent ? segments.filter(sg => sg.fascia && sg.facadeUnder).reduce((t, sg) => t + Math.hypot(sg.b.x - sg.a.x, sg.b.z - sg.a.z), 0) : 0,
// Felsrand: zetwerk van de dakplaten, zit in de prijs van de felsplaten.
@@ -1125,7 +1128,7 @@ window.RoofModel = (() => {
}
// Hoogte van de rand vanaf sg.topAt naar beneden: boeideel, felsrand of daktrim (met opstand boven het dak).
function edgeHeight(edge, sg) {
return sg.fascia ? edge.fasciaHeight : sg.fels ? FELS_EDGE.heightMM : edge.trimHeight + edge.edgeAbove;
return sg.fascia ? edge.fasciaHeight : sg.fels ? FELS_EDGE.heightMM : edge.trimHeight + TRIM.ABOVE_MM;
}
// Kleur van de rand in tekeningen (hex-getal).
function edgeColor(edge, sg) {
@@ -1155,20 +1158,27 @@ window.RoofModel = (() => {
});
const h = edgeHeight(edge, sg);
const mat = sg.fascia ? materials.fasciaMaterial : sg.fels ? coveringMaterial(sg.covering) || materials.trimMaterial : materials.trimMaterial;
polys.forEach(pl => {
const mesh = new THREE.Mesh(prismGeometry(pl, sg.topAt, h), mat);
const nd = (p) => p.x * sg.normal.x + p.z * sg.normal.z;
const add = (pl, topFn, hh, m, u) => {
const mesh = new THREE.Mesh(prismGeometry(pl, topFn, hh, u), m);
mesh.visible = o.edgeVisible !== false;
group.add(mesh);
});
};
if (!sg.fascia && !sg.fels) {
// Daktrim: opstand aan de voorkant, flens erachter op het dak.
const inner = nd(sg.outerA) - TRIM.DEPTH_MM;
polys.forEach(pl => {
const up = clipPoly(pl, p => nd(p) - inner), fl = clipPoly(pl, p => inner - nd(p));
if (up.length >= 3) add(up, sg.topAt, h, mat);
if (fl.length >= 3) add(fl, p => sg.topAt(p) - TRIM.ABOVE_MM + TRIM.FLANGE_MM, TRIM.FLANGE_MM, mat);
});
} else polys.forEach(pl => add(pl, sg.topAt, h, mat));
// Rooster: op de gevel (voorkant bekleding), onder de onderplaat van het boeideel.
if (!sg.fascia || !sg.facadeUnder || !edge.vent) return;
const nd = (p) => p.x * sg.normal.x + p.z * sg.normal.z, face = ventFace(sg);
const face = ventFace(sg);
const u = (p) => (alongZ ? p.z : p.x) / VENT.PITCH_MM;
polys.map(pl => clipPoly(clipPoly(pl, p => nd(p) - face), p => face + VENT.THICK_MM - nd(p))).filter(pl => pl.length >= 3).forEach(pl => {
const mesh = new THREE.Mesh(prismGeometry(pl, (p) => sg.topAt(p) - h, VENT.HEIGHT_MM, u), ventMaterial(edge.ventColor));
mesh.visible = o.edgeVisible !== false;
group.add(mesh);
});
polys.map(pl => clipPoly(clipPoly(pl, p => nd(p) - face), p => face + VENT.THICK_MM - nd(p))).filter(pl => pl.length >= 3)
.forEach(pl => add(pl, (p) => sg.topAt(p) - h, VENT.HEIGHT_MM, ventMaterial(edge.ventColor), u));
});
const spotR = 0.035, spotH = 0.012;
@@ -1199,7 +1209,7 @@ window.RoofModel = (() => {
}
return {
DAKLEER_MM, BESCHOT_MM, EDGE_ABOVE_MM, RIET, FELS, coverKind, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO, FASCIA_COLORS, COVERINGS, coveringFor, coveringThumbUrl,
DAKLEER_MM, BESCHOT_MM, EDGE_ABOVE_MM, TRIM, RIET, FELS, coverKind, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO, FASCIA_COLORS, COVERINGS, coveringFor, coveringThumbUrl,
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
computeShape, shapeFromProfile, shapeLines, wallProfile, profileHeight, computeUnitRoof, computeJoinedRoof, sharedRidgeBeam, joinValue, buildUnit3D, capRegions,
FELS_EDGE, VENT, ventFace, computeEdge, buildEdge3D, edgeHeight, edgeColor, normalizeSettings
+13 -2
View File
@@ -234,8 +234,8 @@ window.WallModel = (() => {
'R961': '#c7b8a2', 'R970': '#ecdeb1', 'R980': '#ae9c88', 'R985': '#cd8b6d', 'R991': '#aa8274'
};
const isStoneFinish = (finish) => TRASRAAM_STONES.includes(finish);
const stoneTextureUrl = (code) => STONE.DIR + encodeURIComponent(code + ' - Witte voeg.jpg');
const stoneThumbUrl = (code, px) => STONE.DIR + 'thumb/' + (px || 64) + '/' + encodeURIComponent(code + ' - Witte voeg.png');
const stoneTextureUrl = (code) => tectumUrl(STONE.DIR + encodeURIComponent(code + ' - Witte voeg.jpg'));
const stoneThumbUrl = (code, px) => tectumUrl(STONE.DIR + 'thumb/' + (px || 64) + '/' + encodeURIComponent(code + ' - Witte voeg.png'));
// UV's in meters, dus repeat = 1/tegel.
const stoneMaterials = {};
function stoneMaterial(code) {
@@ -1257,6 +1257,17 @@ window.WallModel = (() => {
cap.position.set(x + dir * 0.0003, h / 2, 0);
if (spaanplaatVisible) wallGroup3D.add(cap);
});
// Vrij uiteinde van een binnenwand (endCaps): afgewerkt zoals de wand, geen stijl in zicht.
const endCaps = opts.endCaps || {};
if (buitenMateriaal === 'spaanplaat' && spaanplaatVisible) [[endCaps.left, -halfW, -1], [endCaps.right, halfW, 1]].forEach(([on, x, dir]) => {
if (!on) return;
const h = topYAt(x), depth = maxDepth + 2 * (skinGapM + sThickM);
if (!(h > 0)) return;
const cap = new THREE.Mesh(new THREE.PlaneGeometry(depth, h), spaanplaatMaterial);
cap.rotation.y = dir * Math.PI / 2;
cap.position.set(x + dir * 0.0003, h / 2, 0);
wallGroup3D.add(cap);
});
spaanplaatAreaM2 = spaanplaatResult.areaM2;
spaanplaatVolumeM3 = spaanplaatResult.volumeM3;
+1 -1
View File
@@ -41,7 +41,7 @@
"fasciaHeight": 300,
"spotMin": 800,
"spotMax": 1200,
"trimHeight": 80,
"trimHeight": 30,
"fasciaColor": "antraciet"
},
"lighting": {