v0.7.6: geen wolfsdak bij een overkapping (veranda, inpandig of uitpandig: het dak van een unit steekt buiten zijn vloer, HouseModel.hasCover): de keuze verdwijnt in configurator en light, een bestaand wolfsdak wordt een zadeldak met dezelfde goot en nok; afdekker op een stenen trasraam onder rabat, felsplaten of stuc (WallModel.TRAS_CAP): zetwerk achter de bekleding, 5° aflopend, neus 25 mm over de steen, onderbroken bij openingen, bij een buitenhoek door tot voorbij de buursteen, zwart/antraciet/wit (WallModel.TRIM_COLORS), in 3D, PDF-aanzichten, materialen, rapport en kosten; ventilatierooster van 40 mm op de gevel direct onder de onderplaat van alle boeidelen (RoofModel.VENT, niet boven een veranda of tegen een hogere gevel), sleuven in de textuur, aan/uit en kleur bij Dak › Boeideel, in 3D, PDF, rapport en kosten; light-versie: boeidelen of daktrim met kleur boeidelen (Dak) en één zetwerk-kleur voor afdekker en rooster (Gevel); 3D zijwaarts schuiven ook met de middelste muisknop, Shift/Ctrl/⌘ + slepen en de pijltjestoetsen met de muis boven de weergave (configurator en light, hints en sneltoetsenoverzicht bijgewerkt)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
432e0f494a
commit
118ccf9098
+58
-8
@@ -24,8 +24,35 @@ window.RoofModel = (() => {
|
||||
spotMin: 800,
|
||||
spotMax: 1200,
|
||||
trimHeight: 80,
|
||||
fasciaColor: 'antraciet'
|
||||
fasciaColor: 'antraciet',
|
||||
vent: true,
|
||||
ventColor: 'antraciet'
|
||||
};
|
||||
// Ventilatierooster onder de boeidelen: strook op de gevel, direct onder de onderplaat; sleuven in de textuur.
|
||||
// Kleuren als de afdekker van het trasraam (WallModel.TRIM_COLORS).
|
||||
const VENT = { HEIGHT_MM: 40, THICK_MM: 12, PITCH_MM: 12 };
|
||||
const ventMaterials = {};
|
||||
function ventMaterial(key) {
|
||||
const k = WallModel.isTrimColor(key) ? key : 'antraciet';
|
||||
if (ventMaterials[k]) return ventMaterials[k];
|
||||
const base = new THREE.Color(WallModel.trimHex(k));
|
||||
const canvas = typeof document !== 'undefined' && document.createElement('canvas');
|
||||
const ctx = canvas && canvas.getContext && canvas.getContext('2d');
|
||||
let mat;
|
||||
if (!ctx) mat = new THREE.MeshStandardMaterial({ color: base, roughness: 0.5, metalness: 0.3 });
|
||||
else {
|
||||
canvas.width = 32; canvas.height = 32;
|
||||
ctx.fillStyle = '#' + base.getHexString();
|
||||
ctx.fillRect(0, 0, 32, 32);
|
||||
ctx.fillStyle = '#' + base.clone().multiplyScalar(0.18).getHexString();
|
||||
ctx.fillRect(11, 6, 11, 20); // sleuf, 4 mm breed en 25 mm hoog
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
tex.wrapS = THREE.RepeatWrapping;
|
||||
tex.anisotropy = 8;
|
||||
mat = new THREE.MeshStandardMaterial({ color: 0xffffff, map: tex, roughness: 0.5, metalness: 0.3 });
|
||||
}
|
||||
return (ventMaterials[k] = mat);
|
||||
}
|
||||
|
||||
const FASCIA_COLORS = {
|
||||
wit: { label: 'Wit', hex: 0xf2f2f0 },
|
||||
@@ -988,7 +1015,8 @@ window.RoofModel = (() => {
|
||||
overRoof: !!nb && !dropped, dropped
|
||||
};
|
||||
});
|
||||
const depRaw = segs.map(sg => (facadeDepth ? facadeDepth(sg.a, sg.b, sg.normal) || 0 : 0));
|
||||
const depIn = segs.map(sg => (facadeDepth ? facadeDepth(sg.a, sg.b, sg.normal) : 0));
|
||||
const depRaw = depIn.map(d => d || 0);
|
||||
const dep = depRaw.map(d => Math.min(0, d));
|
||||
const spotD = depRaw.map(d => Math.max(0, d) + (s.overhang - Math.max(0, d)) * SPOT_RATIO);
|
||||
const perp = (m1, m2) => Math.abs(m1.x * m2.x + m1.z * m2.z) < 0.5;
|
||||
@@ -1002,7 +1030,8 @@ window.RoofModel = (() => {
|
||||
spotA: at(sg.a, sg.normal, spotD[i], pv.normal, spotD[pi]), spotB: at(sg.b, sg.normal, spotD[i], nx.normal, spotD[ni])
|
||||
};
|
||||
});
|
||||
segs.forEach((sg, i) => Object.assign(sg, shifted[i], { facadeDepth: depRaw[i] }));
|
||||
// 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.
|
||||
@@ -1048,8 +1077,10 @@ window.RoofModel = (() => {
|
||||
return {
|
||||
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,
|
||||
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.
|
||||
trimLengthMM: segments.filter(sg => !sg.fascia && !sg.fels).reduce((t, sg) => t + segLen(sg), 0),
|
||||
felsEdgeLengthMM: segments.filter(sg => sg.fels).reduce((t, sg) => t + segLen(sg), 0),
|
||||
@@ -1071,19 +1102,27 @@ window.RoofModel = (() => {
|
||||
return out;
|
||||
}
|
||||
|
||||
function prismGeometry(poly, topFn, h) {
|
||||
const toM = 0.001, n = poly.length, pos = [];
|
||||
// uAt(p): met textuur, u langs de rand; v = 0 onder, 1 boven.
|
||||
function prismGeometry(poly, topFn, h, uAt) {
|
||||
const toM = 0.001, n = poly.length, pos = [], uvs = [];
|
||||
const T = poly.map(p => [p.x * toM, topFn(p) * toM, p.z * toM]);
|
||||
const B = poly.map(p => [p.x * toM, (topFn(p) - h) * toM, p.z * toM]);
|
||||
const U = uAt ? poly.map(uAt) : null;
|
||||
const tri = (a, b, c) => pos.push(...a, ...b, ...c);
|
||||
for (let k = 1; k < n - 1; k++) { tri(T[0], T[k], T[k + 1]); tri(B[0], B[k + 1], B[k]); }
|
||||
for (let k = 0; k < n; k++) { const j = (k + 1) % n; tri(T[k], B[k], B[j]); tri(T[k], B[j], T[j]); }
|
||||
const uv = (...q) => { if (U) q.forEach(([k, v]) => uvs.push(U[k], v)); };
|
||||
for (let k = 1; k < n - 1; k++) { tri(T[0], T[k], T[k + 1]); uv([0, 1], [k, 1], [k + 1, 1]); tri(B[0], B[k + 1], B[k]); uv([0, 0], [k + 1, 0], [k, 0]); }
|
||||
for (let k = 0; k < n; k++) { const j = (k + 1) % n; tri(T[k], B[k], B[j]); uv([k, 1], [k, 0], [j, 0]); tri(T[k], B[j], T[j]); uv([k, 1], [j, 0], [j, 1]); }
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
|
||||
if (U) geo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
||||
geo.computeVertexNormals();
|
||||
return geo;
|
||||
}
|
||||
|
||||
// Gevelvlak onder een rand (afstand langs de normaal): de dakrand plus de bekleding die erbuiten steekt.
|
||||
function ventFace(sg) {
|
||||
return sg.a.x * sg.normal.x + sg.a.z * sg.normal.z + Math.max(0, sg.facadeDepth || 0);
|
||||
}
|
||||
// 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;
|
||||
@@ -1121,6 +1160,15 @@ window.RoofModel = (() => {
|
||||
mesh.visible = o.edgeVisible !== false;
|
||||
group.add(mesh);
|
||||
});
|
||||
// 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 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);
|
||||
});
|
||||
});
|
||||
|
||||
const spotR = 0.035, spotH = 0.012;
|
||||
@@ -1145,6 +1193,8 @@ window.RoofModel = (() => {
|
||||
s.house = { enabled: !!h.enabled, form: form && UnitModel.ROOF_TYPES[form.type] ? form : null, cover: !!h.cover };
|
||||
s.posts = PostModel.normalizeSettings(s.posts);
|
||||
if (!FASCIA_COLORS[s.fasciaColor]) s.fasciaColor = DEFAULT_SETTINGS.fasciaColor;
|
||||
s.vent = s.vent !== false;
|
||||
if (!WallModel.isTrimColor(s.ventColor)) s.ventColor = DEFAULT_SETTINGS.ventColor;
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -1152,6 +1202,6 @@ window.RoofModel = (() => {
|
||||
DAKLEER_MM, BESCHOT_MM, EDGE_ABOVE_MM, 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, computeEdge, buildEdge3D, edgeHeight, edgeColor, normalizeSettings
|
||||
FELS_EDGE, VENT, ventFace, computeEdge, buildEdge3D, edgeHeight, edgeColor, normalizeSettings
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user