Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b137a458d4 | ||
|
|
bd084e4a96 |
@@ -97,26 +97,39 @@ assets/ logo, texturen, skybox
|
||||
geschilderd in de plafondkleur (Binnen › Wanden › Plafond), zolang
|
||||
"Geschilderd" aan staat; anders kale OSB.
|
||||
- **Dakvorm/wandhoogte:** per unit een dakvorm (Unit › Dakvorm: plat,
|
||||
lessenaar, zadel, mansarde of wolfsdak, met schuiven en hellingshoek).
|
||||
Daaruit volgen de wanden: de kopgevels krijgen de daklijn (bij mansarde
|
||||
met twee knikken, bij een wolfsdak afgeknot op het wolfseind, met
|
||||
lessenaar, zadel of wolfsdak, met schuiven en hellingshoek).
|
||||
Daaruit volgen de wanden: de kopgevels krijgen de daklijn (bij een
|
||||
wolfsdak afgeknot op het wolfseind, met
|
||||
daarboven driehoekige wolfseinden met de dakhelling tot de nok, met een
|
||||
rechte nok ertussen), de haakse wanden staan vlak op de goothoogte. Het
|
||||
bovenste deel van een wolfsdak is gewoon houtwerk: sporen, hoekkepers,
|
||||
nokbalk en kopbalken (ook in zaaglijst, specificaties en kosten).
|
||||
Goot en nok gelden over de volle vloermaat (goot boven de buitenkant van
|
||||
de gevel); staat de kopgevel tussen de haakse wanden, dan loopt zijn
|
||||
daklijn daar precies op aan. Knik en wolfseind schalen mee met goot en
|
||||
nok (verhouding). Bij een knik (mansarde) sluiten de dakvlakken in
|
||||
verstek aan. De PDF heeft voor een wolfsdak een eigen blad met het
|
||||
daklijn daar precies op aan. Het wolfseind schaalt mee met goot en
|
||||
nok (verhouding). De PDF heeft voor een wolfsdak een eigen blad met het
|
||||
bovenste deel (bovenaanzicht houtwerk + stuklijst).
|
||||
Grenzen (`UnitModel.LIMITS`): nok max 5 m, goot / plat dak max 3 m (vanaf
|
||||
bovenkant vloer tot onderkant dak), unit max 4,3 x 14 m. Platte daken van verschillende hoogte krijgen elk hun eigen
|
||||
bovenkant vloer tot onderkant dak), unit max 4,7 x 14 m. Platte daken van verschillende hoogte krijgen elk hun eigen
|
||||
boeideel (versprongen, het lage loopt door tot onder het hoge). Wijzig je een
|
||||
wandhoogte in de wand-editor, dan geldt dezelfde regel vanuit die wand.
|
||||
De daklijn van een wand hoort bij de wand zonder naden: loopt een wand
|
||||
door een naad langer door (bijv. de kopgevel naast een gekoppelde unit),
|
||||
dan blijven nok en knikken op hun plaats (`UnitModel.topLineOn`).
|
||||
- **Dakaansluiting (L/T):** staat een unit met nokdak (zadel of
|
||||
wolfsdak) met zijn nok haaks op de langszijde van een buur met nokdak
|
||||
(vleugel tegen hoofdvolume), dan loopt de vleugel het hoofddak in tot
|
||||
zijn nok, met kilkepers in de binnenhoeken (bij gelijke goot, nok en
|
||||
overspanning onder 45°); het
|
||||
wolfseind van de vleugel aan de naad vervalt. Ligt de vleugel gelijk met
|
||||
een kop van het hoofdvolume, dan wordt die kop een schild in de vlakken
|
||||
van de vleugel (hoekkeper, de goot loopt door; de kopgevel staat dan op
|
||||
goothoogte, een wolfseind daar vervalt). Een vleugel die in een wolfseind
|
||||
van het hoofdvolume zou steken sluit niet aan. Elke unit houdt zijn eigen
|
||||
dakdeel (sporen, kreupele sporen, kepers, knik- en nokbalken; stalen
|
||||
nokbalk alleen over de eigen nok) met een eigen PDF-blad
|
||||
(`HouseModel.roofJoins`, `RoofModel.computeJoinedRoof`). Niet bij dak
|
||||
over de hele woning; dakramen in zo'n dakdeel doen nog niet mee.
|
||||
- **Overkapping:** bij een dak over de hele woning met `cover` loopt het
|
||||
dak door tot de omhullende rechthoek waar geen unit staat (bijv. een
|
||||
terras naast een kortere module), met een afgewerkte onderplaat in de
|
||||
@@ -271,8 +284,19 @@ hout.balk.38x235;Constructiehout vuren C24 38×235;€/m;4,91;Houthandel Y;;2026
|
||||
stucwerk wit met verticaal hout aan de slaapkamerkant, binnenwanden,
|
||||
dubbele schuifdeur tussen woonkamer en hal, en complete inrichting.
|
||||
Langer maken: beide units breder (alles is vanaf de westgevel gemaat).
|
||||
Ook via Bestand › Woning Amstel. De L-woning (8 × 3,6 m + 4,5 × 3,6 m)
|
||||
blijft beschikbaar als "Woning L-vorm".
|
||||
Model **Wetering** (`HouseModel.createWetering`, naar
|
||||
`examples/ontwerp_wetering.pdf`): 10,5 × 4,8 m, twee units van 2,6 en
|
||||
2,2 m zonder naadwand (open woonkamer over de volle diepte), verticaal
|
||||
rabat grijs, plat dak met zwart boeideel, hoekkeuken met kookplaat,
|
||||
badkamer, slaapkamer met kledingkast. Model **IJssel**
|
||||
(`HouseModel.createIJssel`, naar `examples/ontwerp_ijssel.pdf`): L-vorm
|
||||
14 × 4,6 m met een vleugel van 4,6 × 3 m, twee units met zadeldak en
|
||||
riet die met een kil en een hoekkeper op elkaar aansluiten, stucwerk wit,
|
||||
openslaande deuren in de westgevel. De standaardmodellen staan in
|
||||
`HouseModel.MODELS`; Bestand › Snel nieuw en het venster Nieuw worden
|
||||
daaruit opgebouwd (nieuw model = één regel plus een `create`-functie die
|
||||
`modelKit` gebruikt). De L-woning (8 × 3,6 m + 4,5 × 3,6 m) blijft
|
||||
beschikbaar als "Woning L-vorm".
|
||||
- Netto m² per ruimte telt de hele vloer, ook onder een laag schuin dak
|
||||
(geen NEN 2580-correctie voor hoogte < 1,5 m).
|
||||
- Units kunnen niet gedraaid worden.
|
||||
|
||||
@@ -428,6 +428,7 @@ body { display: flex; flex-direction: column; }
|
||||
color: var(--nav-link-color); cursor: pointer; width: 110px;
|
||||
}
|
||||
.new-option svg { width: 40px; height: 40px; }
|
||||
.new-option .bi { font-size: 36px; line-height: 40px; }
|
||||
.new-option span { font-size: 13px; }
|
||||
.new-option:hover:not(.disabled) { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); }
|
||||
.new-option.disabled { opacity: .4; cursor: not-allowed; }
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
<div id="new-modal">
|
||||
<h2>Nieuw</h2>
|
||||
<div class="new-options">
|
||||
<button class="new-option" data-kind="woning" data-template="amstel">
|
||||
<svg viewBox="0 0 48 48"><rect x="6" y="10" width="36" height="14" fill="none" stroke="currentColor" stroke-width="2"/><path d="M6 24 L6 38 L38 38 L38 24" fill="none" stroke="currentColor" stroke-width="2"/><rect x="38" y="24" width="4" height="14" fill="none" stroke="currentColor" stroke-width="1" stroke-dasharray="2 2"/><line x1="22" y1="24" x2="22" y2="38" stroke="currentColor" stroke-width="1"/></svg>
|
||||
<span>Woning Amstel</span>
|
||||
</button>
|
||||
<button class="new-option" data-kind="woning" data-template="single">
|
||||
<svg viewBox="0 0 48 48"><rect x="8" y="14" width="32" height="20" fill="none" stroke="currentColor" stroke-width="2"/><line x1="8" y1="24" x2="40" y2="24" stroke="currentColor" stroke-width="1" stroke-dasharray="2 2"/></svg>
|
||||
<span>Woning</span>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||
* Zie LICENTIE.md.
|
||||
*/
|
||||
const APP_VERSION = '0.6.0';
|
||||
const APP_VERSION = '0.6.2';
|
||||
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
||||
|
||||
let tabs = [];
|
||||
@@ -58,7 +58,7 @@ function appRibbonTabs() {
|
||||
big('Openen', 'folder2-open', `data-app="triggerImport" title="JSON-ontwerp openen" ${disabledUnless('triggerImport')}`) +
|
||||
big('Opslaan', 'save', `data-app="exportDesign" title="Opslaan als JSON + PDF (Ctrl+S)" ${disabledUnless('exportDesign')}`) },
|
||||
{ label: 'Snel nieuw', html:
|
||||
small('Woning Amstel', 'house-heart', 'data-app="new-kind" data-kind="woning" data-template="amstel"') +
|
||||
Object.entries(HouseModel.MODELS).map(([key, m]) => small(m.label, m.icon, `data-app="new-kind" data-kind="woning" data-template="${key}"`)).join('') +
|
||||
small('Woning', 'house', 'data-app="new-kind" data-kind="woning" data-template="single"') +
|
||||
small('Woning L-vorm', 'bounding-box-circles', 'data-app="new-kind" data-kind="woning" data-template="L"') +
|
||||
small('Los: muur', 'bricks', 'data-app="new-kind" data-kind="muur"') +
|
||||
@@ -213,6 +213,8 @@ function wireAbout() {
|
||||
|
||||
function wireNewModal() {
|
||||
const overlay = document.getElementById('new-overlay');
|
||||
overlay.querySelector('.new-options').insertAdjacentHTML('afterbegin', Object.entries(HouseModel.MODELS).map(([key, m]) =>
|
||||
`<button class="new-option" data-kind="woning" data-template="${key}"><i class="bi bi-${m.icon}"></i><span>${m.label}</span></button>`).join(''));
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.classList.remove('open'); });
|
||||
document.querySelectorAll('.new-option:not(.disabled)').forEach(opt => {
|
||||
opt.addEventListener('click', () => {
|
||||
|
||||
@@ -206,13 +206,13 @@ window.CostModel = (() => {
|
||||
|
||||
const rf = newEl(`${u.id}-D`, `Unit ${u.id} dak`, u.id);
|
||||
const rs = u.roof.specs, D = S.dak;
|
||||
add(rf, 'Hout', 'Zagen', 'Dakbalken', rs.joistWoodM3, 'm³', joistWoodCost(rs.joistWoodM3, u.roof.planes[0] && u.roof.planes[0].data.calcData), 0);
|
||||
u.roof.planes.forEach((pl, i) => joistLayer(rf, pl.data.calcData, Math.round(Math.abs(pl.theta || 0) * 180 / Math.PI * 10) / 10, u.roof.planes.length > 1 ? `Dak ${i + 1} ` : 'Dak', true));
|
||||
const cap = u.roof.unitRoof && u.roof.unitRoof.cap;
|
||||
add(rf, 'Hout', 'Zagen', 'Dakbalken', rs.joistWoodM3, 'm³', joistWoodCost(rs.joistWoodM3, u.roof.planes[0] ? u.roof.planes[0].data.calcData : cap), 0);
|
||||
u.roof.planes.forEach((pl, i) => joistLayer(rf, pl.data.calcData, Math.round(Math.abs(pl.theta || 0) * 180 / Math.PI * 10) / 10, u.roof.planes.length > 1 ? `Dak ${i + 1} ` : 'Dak', true));
|
||||
if (cap) {
|
||||
const n = cap.beams.length;
|
||||
add(rf, 'Hout', 'Zagen', `${sawLabel}: wolfseinden ${n} balken`, 2 * n, 'sneden', 0, cap.beams.reduce((t, bm) => t + sawSec([bm.angle, bm.angle], 1), 0));
|
||||
addJoints(rf, n * V.verbindingenPerVloerbalk, 'wolfseindbalken', true);
|
||||
const n = cap.beams.length, join = cap.kind === 'join';
|
||||
add(rf, 'Hout', 'Zagen', `${sawLabel}: ${join ? 'dak (sporen, kepers)' : 'wolfseinden'} ${n} balken`, 2 * n, 'sneden', 0, cap.beams.reduce((t, bm) => t + sawSec([bm.angle, bm.angle], 1), 0));
|
||||
addJoints(rf, n * V.verbindingenPerVloerbalk, join ? 'dakbalken' : 'wolfseindbalken', true);
|
||||
}
|
||||
addSheet(rf, 'Plaatwerk', 'OSB plafond', rs.osbAreaM2, P.osbM2, P.osbMontage);
|
||||
addSheet(rf, 'Plaatwerk', `Dakbeschot OSB ${RoofModel.BESCHOT_MM} mm`, rs.beschotM2 || 0, D.beschotM2, D.beschotMontage);
|
||||
@@ -324,11 +324,10 @@ window.CostModel = (() => {
|
||||
const m = f.w / 1000, crew = IN.personeel;
|
||||
const perM = (what, price, sec) => add(el, 'Inrichting', 'Inrichting', `${what} ${f.w} mm`, m, 'm', m * (price || 0), m * (sec || 0), crew);
|
||||
if (f.cost === 'keuken') perM('Keukenblok', IN.keukenM, IN.keukenMontage);
|
||||
else if (f.cost === 'kookeiland') {
|
||||
perM('Kookeiland', IN.kookeilandM, IN.kookeilandMontage);
|
||||
if (f.opts && f.opts.kookplaat) add(el, 'Inrichting', 'Inrichting', 'Inductiekookplaat', 1, 'st.', IN.kookplaat || 0, 0);
|
||||
} else if (f.cost === 'kast') perM(f.label, IN.kastM, IN.kastMontage);
|
||||
else if (f.cost === 'kookeiland') perM('Kookeiland', IN.kookeilandM, IN.kookeilandMontage);
|
||||
else if (f.cost === 'kast') perM(f.label, IN.kastM, IN.kastMontage);
|
||||
else add(el, 'Inrichting', 'Inrichting', f.label, 1, 'st.', IN[f.cost] || 0, IN[f.cost + 'Montage'] || 0, crew);
|
||||
if ((f.cost === 'keuken' || f.cost === 'kookeiland') && f.opts && f.opts.kookplaat) add(el, 'Inrichting', 'Inrichting', 'Inductiekookplaat', 1, 'st.', IN.kookplaat || 0, 0);
|
||||
});
|
||||
|
||||
const sumLines = (lines) => lines.reduce((a, l) => ({ mat: a.mat + l.mat, sec: a.sec + l.sec, clock: a.clock + l.clock }), { mat: 0, sec: 0, clock: 0 });
|
||||
|
||||
+342
-43
@@ -73,12 +73,17 @@ window.HouseModel = (() => {
|
||||
return { exterior: 'vochtdoek', stone: null, stuc: null, cladding, trasraam, rabatColor: state.settings.facade.rabatColor };
|
||||
}
|
||||
|
||||
function newModelState(naam) {
|
||||
const st = { project: createEmptyProject(), settings: createDefaultSettings(), units: [], interiorWalls: [], rooms: [], fixtures: [] };
|
||||
st.project.naam = naam || '';
|
||||
return st;
|
||||
}
|
||||
|
||||
// TIJDELIJK (testen): nieuwe woning = voorbeeldontwerp (json/premade/vlinderwoning.json); false = één unit.
|
||||
const TEST_DEFAULT = true;
|
||||
|
||||
function createVlinderwoning() {
|
||||
const st = { project: createEmptyProject(), settings: createDefaultSettings(), units: [], interiorWalls: [], rooms: [], fixtures: [] };
|
||||
st.project.naam = 'Vlinderwoning';
|
||||
const st = newModelState('Vlinderwoning');
|
||||
st.settings.trasraam.finish = 'K490';
|
||||
const A = createUnitEntry('A', null, { width: 10000, depth: 4000, windowOn: [] });
|
||||
const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'start', offset: 0 }, { width: 10000, depth: 3600, windowOn: [] });
|
||||
@@ -112,10 +117,35 @@ window.HouseModel = (() => {
|
||||
return st;
|
||||
}
|
||||
|
||||
// Bouwstenen voor de standaardmodellen. Gevelopeningen: [type, w, h, y, anchor, offset, fill];
|
||||
// binnenwanden: [type, w, h, anchor, offset, fill] vanaf de vloer.
|
||||
function modelKit(st, H, T) {
|
||||
const K = (type, extra) => Object.assign(KozijnModel.defaultFill(type === 'raam' ? 'raam' : 'deur'), { out: 'zwart', in: 'zwart' }, extra || {});
|
||||
const binnen = (extra) => Object.assign(KozijnModel.defaultFill('deur'), { out: 'wit', in: 'wit', glas: false }, extra || {});
|
||||
const ops = (ws, list) => { ws.openings = list.map(([type, w, h, y, anchor, offset, fill], i) => ({ id: i + 1, type, w, h, y, anchor, offset, fill })); };
|
||||
const iw = (x1, z1, x2, z2, openings, t) => {
|
||||
const wall = interiorWallDefaults(t || T, H);
|
||||
wall.openings = (openings || []).map(([type, w, h, anchor, offset, fill], i) => ({ id: i + 1, type, w, h, y: 0, anchor, offset, fill }));
|
||||
st.interiorWalls.push({ id: st.interiorWalls.length + 1, x1, z1, x2, z2, wall });
|
||||
};
|
||||
// zaadpunt t.o.v. de noordwesthoek van de unit
|
||||
const room = (unitId, x, z, type, name, extra) =>
|
||||
st.rooms.push(Object.assign(RoomModel.applyType({ id: st.rooms.length + 1, unitId, x, z, name: '' }, type), { name: name || '' }, extra || {}));
|
||||
// Midden voetafdruk t.o.v. de noordwesthoek van de unit; rot = richting van de rug (0 = noord, 90 = oost).
|
||||
// anchor 'oost': x opgeven vanaf de westgevel, het meubel schuift mee met de oostgevel.
|
||||
const fx = (type, unitId, x, z, rot, size, opts) => {
|
||||
const f = FixtureModel.create(st, type, unitId, x, z, rot);
|
||||
if (size && size.anchor === 'oost') f.x = Math.round(st.units.find(u => u.id === unitId).unit.floor.floor.width - x);
|
||||
Object.assign(f, size || {});
|
||||
Object.assign(f.opts, opts || {});
|
||||
st.fixtures.push(f);
|
||||
};
|
||||
return { K, binnen, ops, iw, room, fx };
|
||||
}
|
||||
|
||||
// Model Amstel (examples/ontwerp_amstel.pdf). Alles vanaf de westgevel gemaat, zodat de woning in de lengte kan groeien.
|
||||
function createAmstel() {
|
||||
const st = { project: createEmptyProject(), settings: createDefaultSettings(), units: [], interiorWalls: [], rooms: [], fixtures: [] };
|
||||
st.project.naam = 'Amstel';
|
||||
const st = newModelState('Amstel');
|
||||
const S = st.settings, H = 2800;
|
||||
Object.assign(S.facade, { type: 'stuc', stucColor: 'wit', rabatColor: 'naturel' });
|
||||
S.cladding = 'verticaal';
|
||||
@@ -131,9 +161,7 @@ window.HouseModel = (() => {
|
||||
setHouseRoof(st, true);
|
||||
Object.assign(S.roof.house, { form: { type: 'plat', low: H, high: H }, cover: true });
|
||||
|
||||
const K = (type, extra) => Object.assign(KozijnModel.defaultFill(type === 'raam' ? 'raam' : 'deur'), { out: 'zwart', in: 'zwart' }, extra || {});
|
||||
const binnen = (extra) => Object.assign(KozijnModel.defaultFill('deur'), { out: 'wit', in: 'wit', glas: false }, extra || {});
|
||||
const ops = (ws, list) => { ws.openings = list.map(([type, w, h, y, anchor, offset, fill], i) => ({ id: i + 1, type, w, h, y, anchor, offset, fill })); };
|
||||
const { K, binnen, ops, iw, room, fx } = modelKit(st, H, 90);
|
||||
// Noordgevel: 'right' = vanaf de westhoek.
|
||||
ops(A.unit.walls.voor, [
|
||||
['deur', 1474, 2650, 0, 'right', 150, K('deur', { leaf: 'links', hinge: 'rechts', swing: 'buiten', volglas: true })],
|
||||
@@ -152,13 +180,6 @@ window.HouseModel = (() => {
|
||||
B.facade.walls.rechts = { type: 'rabat' };
|
||||
B.lighting.walls.achter = 'openingen';
|
||||
|
||||
const T = 90;
|
||||
let iwId = 0;
|
||||
const iw = (x1, z1, x2, z2, openings) => {
|
||||
const wall = interiorWallDefaults(T, H);
|
||||
wall.openings = (openings || []).map(([type, w, h, anchor, offset, fill], i) => ({ id: i + 1, type, w, h, y: 0, anchor, offset, fill }));
|
||||
st.interiorWalls.push({ id: ++iwId, x1, z1, x2, z2, wall });
|
||||
};
|
||||
const D = 2115;
|
||||
iw(2555, 3700, 2555, 4800, [['deur', 850, D, 'left', 80, binnen({ hinge: 'links', swing: 'buiten' })]]); // hal | logeerkamer
|
||||
iw(1980, 4845, 3070, 4845); // boven boilerkast en garderobe
|
||||
@@ -180,25 +201,14 @@ window.HouseModel = (() => {
|
||||
fill: Object.assign(KozijnModel.defaultFill('deur'), { type: 'schuifdeur', leaves: 2, mount: 'opbouw', side: 'binnen', glas: true, out: 'zwart', in: 'zwart' }) }];
|
||||
}
|
||||
|
||||
// zaadpunt t.o.v. de noordwesthoek van de unit
|
||||
const room = (id, unitId, x, z, type, name) => st.rooms.push(Object.assign(RoomModel.applyType({ id, unitId, x, z, name: '' }, type), { name }));
|
||||
room(1, 'A', 5600, 1200, 'woonkeuken', 'Woonkamer/keuken');
|
||||
room(2, 'B', 1000, 2000, 'slaapkamer', 'Logeerkamer');
|
||||
room(3, 'B', 3600, 2600, 'hal', 'Entree');
|
||||
room(4, 'B', 2480, 2800, 'toilet', '');
|
||||
room(5, 'B', 5300, 2700, 'badkamer', '');
|
||||
room(6, 'B', 8300, 1800, 'slaapkamer', 'Slaapkamer');
|
||||
room(7, 'B', 2245, 1650, 'techniek', 'Boilerkast');
|
||||
room('A', 5600, 1200, 'woonkeuken', 'Woonkamer/keuken');
|
||||
room('B', 1000, 2000, 'slaapkamer', 'Logeerkamer');
|
||||
room('B', 3600, 2600, 'hal', 'Entree');
|
||||
room('B', 2480, 2800, 'toilet');
|
||||
room('B', 5300, 2700, 'badkamer');
|
||||
room('B', 8300, 1800, 'slaapkamer', 'Slaapkamer');
|
||||
room('B', 2245, 1650, 'techniek', 'Boilerkast');
|
||||
|
||||
// Midden voetafdruk t.o.v. de noordwesthoek van de unit; rot = richting van de rug (0 = noord, 90 = oost).
|
||||
const xA = 11240, xB = 10600;
|
||||
const fx = (type, unitId, x, z, rot, size, opts) => {
|
||||
const f = FixtureModel.create(st, type, unitId, x, z, rot);
|
||||
if (size && size.anchor === 'oost') f.x = Math.round((unitId === 'A' ? xA : xB) - x);
|
||||
Object.assign(f, size || {});
|
||||
Object.assign(f.opts, opts || {});
|
||||
st.fixtures.push(f);
|
||||
};
|
||||
fx('keuken', 'A', 4746, 556, 0, { w: 2660 }, { front: 'antraciet', hogeKast: 'links', spoelbak: 0.49 });
|
||||
fx('kookeiland', 'A', 4746, 2000, 0, { w: 2660, d: 600 }, { front: 'antraciet', kookplaat: true });
|
||||
fx('eettafel', 'A', 1600, 1820, 90, { w: 2000, d: 1100 }, { vorm: 'ovaal', stoelen: 6 });
|
||||
@@ -220,10 +230,155 @@ window.HouseModel = (() => {
|
||||
return st;
|
||||
}
|
||||
|
||||
// Model Wetering (examples/ontwerp_wetering.pdf): 10,5 × 4,8 m, één rechthoek zonder naadwand (open woonkamer
|
||||
// over de volle diepte). 4,8 m is te diep voor één unit, dus twee units (2,6 + 2,2 m). Alles vanaf de westgevel en
|
||||
// de noordgevel gemaat (buitenwerks); de entreegevel met openslaande deuren en voordeur is het zuiden.
|
||||
function createWetering() {
|
||||
const st = newModelState('Wetering');
|
||||
const S = st.settings, H = 2800, W = 10500, DA = 2600, DB = 2200;
|
||||
Object.assign(S.facade, { type: 'rabat', rabatColor: 'grijs' });
|
||||
S.cladding = 'verticaal';
|
||||
S.trasraam.enabled = false;
|
||||
S.roof.fasciaColor = 'zwart';
|
||||
S.lighting.mode = 'gevel';
|
||||
S.lighting.direction = 'both';
|
||||
if (S.elektra && S.elektra.apparaten) Object.assign(S.elektra.apparaten, { koken: true, oven: true, vaatwasser: true });
|
||||
|
||||
const A = createUnitEntry('A', null, { width: W, depth: DA, height: H, windowOn: [] });
|
||||
const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'start', offset: 0 }, { width: W, depth: DB, height: H, windowOn: [] });
|
||||
st.units.push(A, B);
|
||||
setHouseRoof(st, true);
|
||||
Object.assign(S.roof.house, { form: { type: 'plat', low: H, high: H } });
|
||||
|
||||
const { K, binnen, ops, iw, room, fx } = modelKit(st, H, 90);
|
||||
// Noord 'right' en zuid 'left' = vanaf de westhoek; west 'left' en oost 'right' = vanaf de noordkant van de wand.
|
||||
ops(A.unit.walls.voor, [['raam', 500, 700, 1400, 'right', 5670, K('raam')]]);
|
||||
ops(A.unit.walls.links, [['raam', 700, 2100, 0, 'left', 300, K('raam')], ['raam', 700, 2100, 0, 'left', 1500, K('raam')]]);
|
||||
ops(A.unit.walls.rechts, [['raam', 500, 1400, 700, 'right', 270, K('raam')]]);
|
||||
ops(B.unit.walls.rechts, [['raam', 500, 1400, 700, 'right', 540, K('raam')]]);
|
||||
ops(B.unit.walls.achter, [
|
||||
['deur', 824, 2100, 0, 'left', 1200, K('deur', { hinge: 'links', swing: 'buiten', volglas: true })],
|
||||
['deur', 824, 2100, 0, 'left', 2176, K('deur', { hinge: 'rechts', swing: 'buiten', volglas: true })],
|
||||
['voordeur', 1100, 2100, 0, 'left', 5350, K('deur', { hinge: 'rechts', swing: 'buiten', volglas: true })]]);
|
||||
B.lighting.walls.achter = 'openingen';
|
||||
|
||||
const D = 2115;
|
||||
iw(4675, 190, 4675, 2210); // keuken | badkamer
|
||||
iw(4630, 2255, 7120, 2255, [['deur', 930, D, 'left', 1390, binnen({ hinge: 'rechts', swing: 'buiten' })]]); // badkamer | entree
|
||||
iw(7165, 190, 7165, 4610, [ // slaapkamer
|
||||
['deur', 980, 2130, 'left', 360, Object.assign(binnen(), { type: 'schuifdeur', leaves: 1, mount: 'opbouw', side: 'binnen', dir: 'rechts' })],
|
||||
['deur', 930, D, 'left', 2277, binnen({ hinge: 'links', swing: 'buiten' })]]);
|
||||
iw(6455, 3765, 7120, 3765); // kapstoknis entree
|
||||
|
||||
// Geen naadwand: woonkamer en slaapkamer lopen over beide units door (Unit › Naadwand toevoegen zet hem terug).
|
||||
|
||||
room('A', 2000, 1500, 'woonkeuken', 'Woonkamer/keuken');
|
||||
room('A', 5900, 1500, 'badkamer');
|
||||
room('A', 8700, 1500, 'slaapkamer', 'Slaapkamer');
|
||||
|
||||
const zB = (z) => z - DA;
|
||||
fx('keuken', 'A', 4330, 1245, 90, { w: 2110 }, { front: 'antraciet', hogeKast: 'geen', spoelbak: 0.85, kookplaat: true, kookplaatPlek: 0.2 });
|
||||
fx('keuken', 'A', 4630, 2600, 0, { w: 1200 }, { front: 'antraciet', hogeKast: 'rechts', spoel: false, vaatwasser: false });
|
||||
fx('boiler', 'A', 5515, 2525, 0);
|
||||
fx('eettafel', 'A', 1620, 1160, 0, { w: 1000, d: 1000 }, { vorm: 'ovaal', stoelen: 4 });
|
||||
fx('bank', 'B', 615, zB(3640), 270, { w: 1600, d: 850 }, { chaise: 'geen' });
|
||||
fx('salontafel', 'B', 1770, zB(3510), 0, { w: 600, d: 600 }, { vorm: 'rond' });
|
||||
fx('fauteuil', 'B', 2760, zB(4060), 150);
|
||||
fx('douche', 'A', 5245, 640, 270, { w: 900, d: 1050 }, { glas: 'links', glasLengte: 800 });
|
||||
fx('toilet', 'A', 5065, 1760, 270, { w: 900 });
|
||||
fx('wastafel', 'A', 6150, 420, 0, { w: 600 }, { spiegel: false });
|
||||
fx('bed', 'A', 9260, 2050, 90, { w: 1600, d: 2100, anchor: 'oost' }, { nachtkastjes: false });
|
||||
fx('kledingkast', 'B', 8760, zB(4310), 180, { w: 3100 });
|
||||
|
||||
computeParts(st, { skip3D: true });
|
||||
return st;
|
||||
}
|
||||
|
||||
// Model IJssel (examples/ontwerp_ijssel.pdf): L-vorm 14 × 4,6 m met een vleugel van 4,6 × 3 m aan de zuidoostkant,
|
||||
// samen 14 × 7,6 m. Twee units: hoofdvolume met zadeldak west–oost, vleugel met zadeldak noord–zuid. Gelijke
|
||||
// goot, nok en overspanning, dus de daken sluiten onder 45° aan (dakaansluiting, zie roofJoins): kil in de
|
||||
// binnenhoek en, omdat de vleugel gelijk ligt met de oostgevel, een hoekkeper (de oostkop wordt een schild).
|
||||
// Geen naadwanden; de wand tussen hal/badkamer en slaapkamer is een gewone binnenwand. Binnenwanden volgen het
|
||||
// dak nog niet, dus alle ruimtes krijgen een vlak plafond op wandhoogte. Alles buitenwerks vanaf de noordwesthoek.
|
||||
function createIJssel() {
|
||||
const st = newModelState('IJssel');
|
||||
const S = st.settings, LO = 3000, HI = 4600, H = 2900, D = 4600, ROOF = 'riet_001';
|
||||
Object.assign(S.facade, { type: 'stuc', stucColor: 'wit' });
|
||||
Object.assign(S.trasraam, { enabled: true, height: 600, finish: 'R510' });
|
||||
S.roof.fasciaColor = 'zwart';
|
||||
S.lighting.mode = 'gevel';
|
||||
S.lighting.direction = 'both';
|
||||
if (S.elektra && S.elektra.apparaten) Object.assign(S.elektra.apparaten, { koken: true, oven: true, vaatwasser: true });
|
||||
|
||||
const A = createUnitEntry('A', null, { width: 14000, depth: D, height: LO, windowOn: [] });
|
||||
const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'end', offset: 0 }, { width: 4600, depth: 3000, height: LO, windowOn: [] });
|
||||
st.units.push(A, B);
|
||||
UnitModel.applyRoofForm(A.unit, { type: 'zadel', ridge: 'x', low: LO, high: HI });
|
||||
UnitModel.applyRoofForm(B.unit, { type: 'zadel', ridge: 'z', low: LO, high: HI });
|
||||
[A, B].forEach(u => { u.roof.covering = ROOF; });
|
||||
|
||||
const { K, binnen, ops, iw, room, fx } = modelKit(st, H, 90);
|
||||
// Noord 'right' en zuid 'left' = vanaf de westhoek; west 'left' en oost 'right' = vanaf de noordkant van de wand.
|
||||
ops(A.unit.walls.voor, [['raam', 1500, 500, 1600, 'right', 5445, K('raam')], ['raam', 500, 700, 1400, 'right', 9950, K('raam')]]);
|
||||
ops(A.unit.walls.links, [
|
||||
['deur', 1424, 2100, 0, 'left', 610, K('deur', { leaf: 'rechts', hinge: 'links', swing: 'buiten', volglas: true })],
|
||||
['deur', 1424, 2100, 0, 'left', 2376, K('deur', { leaf: 'links', hinge: 'rechts', swing: 'buiten', volglas: true })]]);
|
||||
ops(A.unit.walls.rechts, [['raam', 1400, 1100, 1000, 'right', 608, K('raam')], ['raam', 1200, 500, 1600, 'right', 3101, K('raam')]]);
|
||||
ops(A.unit.walls.achter, [
|
||||
['raam', 1800, 2800, 0, 'left', 3215, K('raam')],
|
||||
['voordeur', 1100, 2100, 0, 'left', 8240, K('deur', { hinge: 'rechts', swing: 'buiten', glas: true })]]);
|
||||
ops(B.unit.walls.achter, [['raam', 500, 1400, 700, 'left', 410, K('raam')], ['raam', 500, 1400, 700, 'right', 1100, K('raam')]]);
|
||||
A.lighting.walls.achter = 'openingen';
|
||||
|
||||
const DH = 2115;
|
||||
iw(9580, 190, 9580, 3220, [], 160); // keuken | toilet, hal
|
||||
iw(8060, 3265, 9660, 3265); // woonkamer | hal
|
||||
iw(8105, 3310, 8105, 4410, [['deur', 930, DH, 'left', 86, binnen({ hinge: 'rechts', swing: 'buiten' })]]); // hal → woonkamer
|
||||
iw(9660, 1633, 10760, 1633, [['deur', 930, DH, 'left', 85, binnen({ hinge: 'links', swing: 'buiten' })]]); // toilet | hal
|
||||
iw(10805, 190, 10805, 4410, [['deur', 930, DH, 'left', 1725, binnen({ hinge: 'rechts', swing: 'binnen' })]]); // hal | berging, badkamer
|
||||
iw(10850, 2965, 13810, 2965); // berging | badkamer
|
||||
iw(9590, 4455, 13810, 4455, [ // hal, badkamer | slaapkamer
|
||||
['deur', 930, DH, 'left', 115, binnen({ hinge: 'links', swing: 'buiten' })],
|
||||
['deur', 980, DH, 'left', 2350, Object.assign(binnen(), { type: 'schuifdeur', leaves: 1, mount: 'opbouw', side: 'buiten', dir: 'links' })]]);
|
||||
|
||||
const plafond = { ceiling: H };
|
||||
room('A', 3000, 1500, 'woonkeuken', 'Woonkamer/keuken', plafond);
|
||||
room('A', 10210, 900, 'toilet');
|
||||
room('A', 12300, 1500, 'berging', 'Berging', plafond);
|
||||
room('A', 9000, 3800, 'hal', 'Hal', plafond);
|
||||
room('A', 12300, 3700, 'badkamer');
|
||||
room('B', 2000, 1400, 'slaapkamer', 'Slaapkamer', plafond);
|
||||
|
||||
const xB = (x) => x - 9400, zB = (z) => z - D;
|
||||
fx('bank', 'A', 2030, 690, 0, { w: 3600, d: 1000 }, { chaise: 'links', chaiseLengte: 1700 });
|
||||
fx('salontafel', 'A', 2410, 1980, 0, { w: 800, d: 800 }, { vorm: 'rond' });
|
||||
fx('fauteuil', 'A', 3120, 2950, 150);
|
||||
fx('tvmeubel', 'A', 1060, 4210, 180, { w: 1200 });
|
||||
fx('eettafel', 'A', 6100, 1115, 0, { w: 2000, d: 1000 }, { vorm: 'ovaal', stoelen: 6 });
|
||||
fx('keuken', 'A', 9200, 1705, 90, { w: 3030 }, { front: 'antraciet', hogeKast: 'rechts', spoelbak: 0.16, kookplaat: true, kookplaatPlek: 0.8 });
|
||||
fx('toilet', 'A', 10210, 535, 0, { w: 1100 });
|
||||
fx('boiler', 'A', 11040, 415, 0);
|
||||
fx('douche', 'A', 11300, 3710, 0, { w: 900, d: 1400 }, { glas: 'rechts', glasLengte: 1400 });
|
||||
fx('wastafel', 'A', 12300, 3240, 0, { w: 800 });
|
||||
fx('toilet', 'A', 13355, 3355, 0, { w: 900 });
|
||||
fx('bed', 'B', xB(11400), zB(6360), 180, { w: 1600, d: 2100 }, { nachtkastjes: false });
|
||||
fx('kledingkast', 'B', xB(13510), zB(5955), 90, { w: 2910, anchor: 'oost' });
|
||||
|
||||
computeParts(st, { skip3D: true });
|
||||
return st;
|
||||
}
|
||||
|
||||
// Standaardmodellen: Bestand › Snel nieuw en het venster Nieuw. De sleutel is de template.
|
||||
const MODELS = {
|
||||
amstel: { label: 'Woning Amstel', icon: 'house-heart', create: createAmstel },
|
||||
wetering: { label: 'Woning Wetering', icon: 'house-door', create: createWetering },
|
||||
ijssel: { label: 'Woning IJssel', icon: 'houses', create: createIJssel }
|
||||
};
|
||||
|
||||
function createDefaultState(template) {
|
||||
if (template === 'amstel') return createAmstel();
|
||||
if (MODELS[template]) return MODELS[template].create();
|
||||
if (TEST_DEFAULT && (!template || template === 'single')) return createVlinderwoning();
|
||||
const state = { project: createEmptyProject(), settings: createDefaultSettings(), units: [], interiorWalls: [], rooms: [], fixtures: [] };
|
||||
const state = newModelState();
|
||||
state.units.push(createUnitEntry('A', null, template === 'L' ? { width: 8000, depth: 3600 } : { width: 6000, depth: 3000 }));
|
||||
if (template === 'L') {
|
||||
state.units.push(createUnitEntry('B', { to: 'A', side: 'achter', align: 'start', offset: 0 }, { width: 4500, depth: 3600, windowOn: [] }));
|
||||
@@ -452,6 +607,7 @@ window.HouseModel = (() => {
|
||||
});
|
||||
state.interiorWalls.forEach(iw => { iw.wall.wall.minSpacing = sp.interior.min; iw.wall.wall.maxSpacing = sp.interior.max; });
|
||||
applyHouseRoof(state);
|
||||
applyRoofJoins(state);
|
||||
}
|
||||
|
||||
const houseRoofOn = (state) => !!(state.settings.roof.house && state.settings.roof.house.enabled && state.settings.roof.house.form);
|
||||
@@ -527,7 +683,6 @@ window.HouseModel = (() => {
|
||||
const shape = houseRoofShape(state, rects);
|
||||
if (!shape) return;
|
||||
const form = state.settings.roof.house.form, ln = shape.line;
|
||||
if (form.type === 'mansarde' && ln.length === 5) Object.assign(form, { kneeIn: Math.round(ln[1].u), kneeH: Math.round(ln[1].h) });
|
||||
if (form.type === 'wolf' && ln.length === 4) form.cut = Math.round(ln[1].h);
|
||||
const planes = RoofModel.shapeLines(shape);
|
||||
state.units.forEach((u, i) => {
|
||||
@@ -556,13 +711,131 @@ window.HouseModel = (() => {
|
||||
const a = pts[k], b = pts[k + 2];
|
||||
return q.h > a.h + (q.x - a.x) / (b.x - a.x) * (b.h - a.h) + 0.5;
|
||||
});
|
||||
const wall = u.unit.walls[wl.side].wall;
|
||||
const hL = Math.round(pts[0].h), hR = Math.round(pts[pts.length - 1].h);
|
||||
setWallTop(u.unit.walls[wl.side].wall, W, pts[0].h, pts[pts.length - 1].h, inner);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setWallTop(wall, W, h0, h1, inner) {
|
||||
const hL = Math.round(h0), hR = Math.round(h1);
|
||||
const mid = inner.length === 1 && Math.abs(inner[0].x - W / 2) < 0.5;
|
||||
Object.assign(wall, { heightLeft: hL, heightRight: hR, heightAll: Math.min(hL, hR), heightMid: mid ? Math.round(inner[0].h) : null });
|
||||
if (inner.length && !mid) wall.topPts = inner.map(q => ({ f: q.x / W, h: Math.round(q.h) }));
|
||||
else delete wall.topPts;
|
||||
}
|
||||
|
||||
// Dakaansluiting (L/T): een vleugel met nokdak (zadel, wolfsdak), nok haaks op de naad, tegen de
|
||||
// langszijde van een hoofdvolume met nokdak loopt het hoofddak in tot zijn nok (kilkepers). Het wolfseind van de
|
||||
// vleugel aan de naad vervalt. Ligt de vleugel gelijk met een kop van het hoofdvolume, dan wordt die kop een
|
||||
// schild in de vlakken van de vleugel (hoekkeper, goot loopt door; een wolfseind daar vervalt); hooguit één kop,
|
||||
// anders heeft het hoofdvolume geen kopgevel meer om zijn eigen dakvorm uit af te leiden. Een vleugel die in een
|
||||
// wolfseind van het hoofdvolume zou steken (niet gelijk met de kop) sluit niet aan.
|
||||
// Vlakken in wereldmaten; h = min(verre vlakken, wolfseinden, schildvlakken, max(min(nabije vlakken), vleugels)),
|
||||
// zie RoofModel.computeJoinedRoof.
|
||||
const JOIN_TYPES = ['zadel', 'wolf'];
|
||||
function roofJoins(state, rects) {
|
||||
const out = {};
|
||||
if (houseRoofOn(state)) return out;
|
||||
const rs = rects || unitRects(state);
|
||||
if (rs.some(r => r.error) || overlappingPairs(rs).length) return out;
|
||||
const at = (pl, [x, z]) => pl.a + pl.bx * x + pl.bz * z;
|
||||
const info = state.units.map((u, i) => {
|
||||
const r = rs[i], shape = RoofModel.computeShape(u.unit, UnitModel.computeLayout(u.unit, {}));
|
||||
const planes = [];
|
||||
if (!JOIN_TYPES.includes(shape.type)) return { u, r, shape, planes };
|
||||
const sx = shape.axis === 'x';
|
||||
const s = shape.type === 'wolf' && shape.hip
|
||||
? Object.assign({}, shape, { hip: Object.assign({}, shape.hip, sx ? { q0: -r.D / 2, q1: r.D / 2 } : { q0: -r.W / 2, q1: r.W / 2 }) })
|
||||
: shape;
|
||||
const corners = [[r.x0, r.z0], [r.x1, r.z0], [r.x0, r.z1], [r.x1, r.z1]];
|
||||
RoofModel.shapeLines(s).forEach(l => {
|
||||
const [bx, bz] = sx ? [l.bp, l.bq] : [l.bq, l.bp];
|
||||
const pl = { a: l.a - bx * r.cx - bz * r.cz, bx, bz };
|
||||
// Wolfsdak: onder- en bovenstuk liggen (op afronding na) in één vlak.
|
||||
if (!planes.some(q => corners.every(c => Math.abs(at(q, c) - at(pl, c)) < 2))) planes.push(pl);
|
||||
});
|
||||
return { u, r, shape, planes };
|
||||
});
|
||||
const groups = new Map(), role = {};
|
||||
info.forEach(M => info.forEach(W => {
|
||||
if (M === W || !M.planes.length || !W.planes.length || M.shape.axis === W.shape.axis) return;
|
||||
if (role[M.u.id] === 'vleugel' || role[W.u.id]) return;
|
||||
const seamX = M.shape.axis === 'z'; // naad evenwijdig aan de nok van M
|
||||
const [m0, m1, w0, w1] = seamX ? [M.r.x0, M.r.x1, W.r.x0, W.r.x1] : [M.r.z0, M.r.z1, W.r.z0, W.r.z1];
|
||||
const [a0, a1, b0, b1] = seamX ? [M.r.z0, M.r.z1, W.r.z0, W.r.z1] : [M.r.x0, M.r.x1, W.r.x0, W.r.x1];
|
||||
const seam = Math.abs(a1 - b0) < EPS ? a1 : Math.abs(b1 - a0) < EPS ? a0 : null;
|
||||
if (seam === null || w0 < m0 - EPS || w1 > m1 + EPS || (w0 < m0 + EPS && w1 > m1 - EPS)) return;
|
||||
const xz = (t) => seamX ? [t, seam] : [seam, t];
|
||||
const dir = seam === a1 ? 1 : -1; // vleugel aan de +kant (dwars op de naad) van M
|
||||
const across = (pl) => seamX ? pl.bz : pl.bx, along = (pl) => seamX ? pl.bx : pl.bz;
|
||||
const g = groups.get(M) || { M, seamX, wings: [], flush: null, drop: [] };
|
||||
const end = Math.abs(w1 - m1) < EPS ? 1 : Math.abs(w0 - m0) < EPS ? 0 : -1;
|
||||
const toEnd = (pl) => end ? along(pl) < 0 : along(pl) > 0; // daalt naar die kop
|
||||
const flush = end >= 0 && !g.flush ? {
|
||||
planes: W.planes.filter(pl => Math.abs(along(pl)) > 1e-9 && toEnd(pl)),
|
||||
drop: M.planes.filter(pl => Math.abs(across(pl)) <= 1e-9 && toEnd(pl)),
|
||||
side: seamX ? (end ? 'rechts' : 'links') : (end ? 'achter' : 'voor')
|
||||
} : null;
|
||||
const wingTop = Math.max(...W.shape.profile.map(q => q.h));
|
||||
const hips = M.planes.filter(pl => Math.abs(across(pl)) <= 1e-9 && !g.drop.includes(pl) && !(flush && flush.drop.includes(pl)));
|
||||
if (hips.some(pl => Math.min(at(pl, xz(w0)), at(pl, xz(w1))) < wingTop - 1)) return;
|
||||
if (flush) { g.flush = flush; g.drop.push(...flush.drop); }
|
||||
g.wings.push({ W, sign: -dir, roof: W.planes.filter(pl => Math.abs(along(pl)) > 1e-9 || across(pl) * dir < 0) });
|
||||
groups.set(M, g);
|
||||
role[M.u.id] = 'hoofd';
|
||||
role[W.u.id] = 'vleugel';
|
||||
}));
|
||||
groups.forEach(g => {
|
||||
const planes = [], idx = (pl) => planes.includes(pl) ? planes.indexOf(pl) : planes.push(pl) - 1;
|
||||
const cross = (pl) => g.seamX ? pl.bz : pl.bx, terms = [];
|
||||
[-1, 1].forEach(sg => {
|
||||
const side = g.M.planes.filter(pl => Math.abs(cross(pl)) > 1e-9 && Math.sign(cross(pl)) === sg);
|
||||
const ws = g.wings.filter(w => w.sign === sg);
|
||||
if (!ws.length) side.forEach(pl => terms.push(idx(pl)));
|
||||
else terms.push(['max', ['min', ...side.map(idx)], ...ws.map(w => ['min', ...w.roof.map(idx)])]);
|
||||
});
|
||||
g.M.planes.filter(pl => Math.abs(cross(pl)) <= 1e-9 && !g.drop.includes(pl)).forEach(pl => terms.push(idx(pl)));
|
||||
if (g.flush) g.flush.planes.forEach(pl => terms.push(idx(pl)));
|
||||
const expr = ['min', ...terms];
|
||||
[g.M, ...g.wings.map(w => w.W)].forEach(X => { out[X.u.id] = { planes, expr, flush: X === g.M && g.flush ? g.flush.side : null }; });
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// De kop die een schild wordt volgt het dak; vervalt de aansluiting, dan krijgt de unit zijn eigen dakvorm terug.
|
||||
function applyRoofJoins(state) {
|
||||
const rects = unitRects(state);
|
||||
const joins = roofJoins(state, rects);
|
||||
state.units.forEach((u, i) => {
|
||||
const join = joins[u.id], side = join && join.flush;
|
||||
if (Object.entries(u.unit.walls).some(([k, ws]) => ws.wall.joinTop && k !== side)) {
|
||||
Object.values(u.unit.walls).forEach(ws => { delete ws.wall.joinTop; });
|
||||
UnitModel.applyRoofForm(u.unit, UnitModel.roofForm(u.unit));
|
||||
}
|
||||
if (!side) return;
|
||||
const r = rects[i];
|
||||
const wl = UnitModel.computeLayout(u.unit, {}).walls.find(w => w.side === side);
|
||||
const s = UnitModel.WALL_SIDES.find(x => x.key === side), alongX = s.axis === 'x';
|
||||
const start = (alongX ? wl.cx + r.cx : wl.cz + r.cz) - s.dir * wl.width / 2;
|
||||
const perp = alongX ? wl.cz + r.cz : wl.cx + r.cx, out = alongX ? wl.normal.y : wl.normal.x;
|
||||
const faces = [perp + out * wl.thickness / 2, perp - out * wl.thickness / 2];
|
||||
const pos = (x, f) => alongX ? { x: start + s.dir * x, z: f } : { x: f, z: start + s.dir * x };
|
||||
const W = wl.width, xs = [0, W];
|
||||
// Knikken alleen waar twee vlakken langs een wandvlak even hoog zijn.
|
||||
faces.forEach(f => join.planes.forEach((p1, j) => join.planes.slice(j + 1).forEach(p2 => {
|
||||
const h = (pl, x) => { const q = pos(x, f); return pl.a + pl.bx * q.x + pl.bz * q.z; };
|
||||
const d0 = h(p1, 0) - h(p2, 0), d1 = h(p1, W) - h(p2, W);
|
||||
if (Math.abs(d1 - d0) > 1e-9) { const x = -d0 / (d1 - d0) * W; if (x > 1 && x < W - 1) xs.push(x); }
|
||||
})));
|
||||
const hAt = (x) => Math.min(...faces.map(f => { const q = pos(x, f); return RoofModel.joinValue(join, q); }));
|
||||
const pts = [...new Set(xs.map(x => Math.round(x * 10) / 10))].sort((a, b) => a - b).map(x => ({ x, h: hAt(x) }));
|
||||
const inner = pts.slice(1, -1).filter((q, k) => {
|
||||
const a = pts[k], b = pts[k + 2];
|
||||
return Math.abs(q.h - (a.h + (q.x - a.x) / (b.x - a.x) * (b.h - a.h))) > 0.5;
|
||||
});
|
||||
const wall = u.unit.walls[side].wall;
|
||||
setWallTop(wall, W, pts[0].h, pts[pts.length - 1].h, inner);
|
||||
wall.joinTop = true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -638,11 +911,12 @@ window.HouseModel = (() => {
|
||||
const { cuts, seams } = computeSeams(rects, wallThickness);
|
||||
const houseShape = houseRoofShape(state, rects);
|
||||
const coverExt = coverExtensions(state, rects);
|
||||
const joins = roofJoins(state, rects);
|
||||
|
||||
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc) {
|
||||
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop) {
|
||||
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,
|
||||
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop,
|
||||
painted: state.settings.interiorPaint && shows('afwerking'), stage, rabatColor: state.settings.facade.rabatColor
|
||||
});
|
||||
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
|
||||
@@ -696,9 +970,14 @@ window.HouseModel = (() => {
|
||||
const deckFrame = FloorModel.computeFrame(u.roof.deck.floor);
|
||||
const roofW = deckFrame.width, roofD = deckFrame.depth;
|
||||
const ox = u.roof.offsetX || 0, oz = u.roof.offsetZ || 0;
|
||||
const ur = RoofModel.computeUnitRoof(u.roof, shape, { x0: ox - roofW / 2, x1: ox + roofW / 2, z0: oz - roofD / 2, z1: oz + roofD / 2 }, rims);
|
||||
const roofRect = { x0: ox - roofW / 2, x1: ox + roofW / 2, z0: oz - roofD / 2, z1: oz + roofD / 2 };
|
||||
const join = joins[u.id] && {
|
||||
planes: joins[u.id].planes.map(pl => ({ a: pl.a + pl.bx * rect.cx + pl.bz * rect.cz, bx: pl.bx, bz: pl.bz })),
|
||||
expr: joins[u.id].expr
|
||||
};
|
||||
const ur = join ? RoofModel.computeJoinedRoof(u.roof, join, roofRect, shape) : RoofModel.computeUnitRoof(u.roof, shape, roofRect, rims);
|
||||
const roofCx = rect.cx + ox, roofCz = rect.cz + oz;
|
||||
const shapeLabel = { plat: 'plat', lessenaar: 'lessenaar', zadel: 'zadel', mansarde: 'mansarde', wolf: 'wolfsdak' }[shape.type];
|
||||
const shapeLabel = { plat: 'plat', lessenaar: 'lessenaar', zadel: 'zadel', wolf: 'wolfsdak' }[shape.type];
|
||||
const multi = ur.planes.length > 1;
|
||||
ur.planes.forEach((pl, i) => {
|
||||
pl.element = `${u.id}-D${i + 1}`;
|
||||
@@ -923,6 +1202,24 @@ window.HouseModel = (() => {
|
||||
});
|
||||
}
|
||||
|
||||
// Buitenlagen tot de bovenkant van het dak, per punt: een steil vlak (wolfseind) is verticaal dikker.
|
||||
function skinTopLine(p, roof) {
|
||||
const cfg = p.config, W = cfg.wallWidth, tl = WallModel.topLine(cfg), zF = p.place.thickness / 2;
|
||||
const xs = new Set([0, W].concat((cfg.wallTop || []).map(q => q.x)));
|
||||
for (let x = 100; x < W; x += 100) xs.add(x);
|
||||
const pts = [...xs].sort((a, b) => a - b).map(x => {
|
||||
const top = tl.at(x - W / 2), w = UnitModel.localToWorld(p.place, x - W / 2, zF), wallTop = p.place.floorTop + top;
|
||||
return { x, h: top + Math.max(0, roof.topAt(w.x, w.z) - Math.min(wallTop, roof.ceilingAt(w.x, w.z))) };
|
||||
});
|
||||
const L = pts[0], R = pts[pts.length - 1];
|
||||
const inner = WallModel.wallTopPoints({ topPts: pts.slice(1, -1).map(q => ({ f: q.x / W, h: q.h })) }, W, Math.round(L.h), Math.round(R.h));
|
||||
const line = [L, ...inner, R];
|
||||
const at = (x) => { let i = 0; while (i < line.length - 2 && x > line[i + 1].x) i++; const a = line[i], b = line[i + 1]; return a.h + (x - a.x) / (b.x - a.x) * (b.h - a.h); };
|
||||
// Alleen een bolle daklijn: een kil of sprong (binnenhoek L-vorm) zou de lijn erboven trekken; dan vast skinAbove.
|
||||
if (pts.some(q => at(q.x) > q.h + 5)) return null;
|
||||
return { hL: Math.round(L.h), hR: Math.round(R.h), pts: inner };
|
||||
}
|
||||
|
||||
function topWedge(p, roof) {
|
||||
const cfg = p.config, half = cfg.wallWidth / 2, zF = cfg.studDepth / 2, tl = WallModel.topLine(cfg);
|
||||
const xs = new Set([-half, half].concat(tl.kinks));
|
||||
@@ -961,6 +1258,7 @@ window.HouseModel = (() => {
|
||||
p.skinAbove = covered !== null
|
||||
? Math.max(0, Math.round(covered - 3 - wallTop))
|
||||
: Math.max(0, Math.round(u.roof.topAt(m.x, m.z) - Math.min(wallTop, u.roof.ceilingAt(m.x, m.z))));
|
||||
p.skinTop = covered !== null ? null : skinTopLine(p, u.roof);
|
||||
p.underRoof = covered !== null;
|
||||
p.config.topWedge = topWedge(p, u.roof);
|
||||
// Binnenhoek-doorloop: tot de bovenkant van het dak van de buur geen buitenlagen (trimTop).
|
||||
@@ -1017,7 +1315,7 @@ window.HouseModel = (() => {
|
||||
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);
|
||||
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);
|
||||
if (built.group && p.shiftZ) {
|
||||
const outer = new THREE.Group();
|
||||
built.group.position.z = p.shiftZ * 0.001;
|
||||
@@ -1357,6 +1655,7 @@ window.HouseModel = (() => {
|
||||
state.settings.elektra = ElektraModel.normalizeSettings(state.settings.elektra);
|
||||
state.units.forEach(u => {
|
||||
if (!u.unit.layout) u.unit.layout = { corners: 'voor-achter' };
|
||||
UnitModel.dropMansarde(u.unit);
|
||||
if (!u.roof) { u.roof = RoofModel.createDefaultRoof(u.unit.floor); delete u.roof.fascia; }
|
||||
if (u.roof.fascia === undefined) u.roof.fascia = oldRoof.edge !== 'daktrim';
|
||||
if (!RoofModel.COVERINGS[u.roof.covering]) u.roof.covering = 'dakleer';
|
||||
@@ -1371,7 +1670,7 @@ window.HouseModel = (() => {
|
||||
|
||||
return {
|
||||
ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS, FACADE_TYPES, facadeFor, facadeMaterials,
|
||||
clone, createEmptyProject, createDefaultSettings, createUnitEntry, applyGlobalSettings, nextUnitId, createDefaultState, unitRects, computeSeams, overlappingPairs,
|
||||
clone, createEmptyProject, createDefaultSettings, createUnitEntry, applyGlobalSettings, nextUnitId, MODELS, createDefaultState, unitRects, computeSeams, overlappingPairs,
|
||||
interiorWallDefaults, setThickness, thicknessOf, interiorLayout, ensureSeamWalls, syncSeamWalls, missingSeamWalls,
|
||||
houseRoofShape, houseRoofFits, setHouseRoof, coverExtensions,
|
||||
computeParts, allWallPieces, computeTotals, computeDimensions, dimensionRows, TRANSPORT, TRANSPORT_CLASSES, computeTransport, validateState, fromUnitDesign, normalize
|
||||
|
||||
+18
-11
@@ -32,7 +32,8 @@ window.HousePdf = (() => {
|
||||
list.push({ kind: 'floor', element: u.floorElement, label: `Unit ${u.id} vloer`, unit: u });
|
||||
u.walls.forEach(w => w.pieces.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p })));
|
||||
u.roof.planes.forEach(pl => list.push({ kind: 'roof', element: pl.element, label: pl.label, plane: pl }));
|
||||
if (u.roof.unitRoof && u.roof.unitRoof.cap) list.push({ kind: 'cap', element: `${u.id}-D${u.roof.planes.length + 1}`, label: `Unit ${u.id} dak bovenste deel (wolfsdak)`, unit: u });
|
||||
const cap = u.roof.unitRoof && u.roof.unitRoof.cap;
|
||||
if (cap) list.push({ kind: 'cap', element: `${u.id}-D${u.roof.planes.length + 1}`, label: cap.kind === 'join' ? `Unit ${u.id} dak (aansluiting)` : `Unit ${u.id} dak bovenste deel (wolfsdak)`, unit: u });
|
||||
});
|
||||
parts.interior.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p }));
|
||||
list.push({ kind: 'totals', element: 'TOT', label: 'Totalen' });
|
||||
@@ -235,10 +236,10 @@ window.HousePdf = (() => {
|
||||
const W = p.place.width, T = p.place.thickness;
|
||||
const c = WallModel.claddingOffset(p.buitenMateriaal, p.stone, p.stuc), e = p.skinExt || {};
|
||||
const xl = -W / 2 - (e.left > 0 ? e.left + c : 0), xr = W / 2 + (e.right > 0 ? e.right + c : 0);
|
||||
const tl = WallModel.topLine(p.config), up = p.skinAbove || 0;
|
||||
const top = (x) => tl.at(Math.max(-W / 2, Math.min(W / 2, x))) + up;
|
||||
const tl = WallModel.topLine(WallModel.skinConfig(p.config, p.skinAbove || 0, p.skinTop));
|
||||
const top = (x) => Math.min(tl.at(x), tl.at(Math.max(-W / 2, Math.min(W / 2, x))));
|
||||
const loc = [{ x: xl, y: y0 }, { x: xr, y: y0 }, { x: xr, y: top(xr) }]
|
||||
.concat(tl.pts.slice().reverse().filter(q => q.x > xl + 1 && q.x < xr - 1).map(q => ({ x: q.x, y: q.y + up })))
|
||||
.concat(tl.pts.slice().reverse().filter(q => q.x > xl + 1 && q.x < xr - 1).map(q => ({ x: q.x, y: q.y })))
|
||||
.concat([{ x: xl, y: top(xl) }]);
|
||||
return loc;
|
||||
};
|
||||
@@ -282,7 +283,7 @@ window.HousePdf = (() => {
|
||||
};
|
||||
RoofModel.capRegions(r.unitRoof).forEach(rg => {
|
||||
pushRoof(rg.poly.map(pt => {
|
||||
const x = u.rect.cx + (sh.axis === 'x' ? pt.x : pt.z), z = u.rect.cz + (sh.axis === 'x' ? pt.z : pt.x);
|
||||
const ax = r.unitRoof.alongX, x = u.rect.cx + (ax ? pt.x : pt.z), z = u.rect.cz + (ax ? pt.z : pt.x);
|
||||
return { x, y: u.layout.floorTop + rg.under(pt) + rg.vThick, z };
|
||||
}));
|
||||
});
|
||||
@@ -457,7 +458,7 @@ window.HousePdf = (() => {
|
||||
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
|
||||
}
|
||||
|
||||
const CAP_KINDS = { spoor: 'Spoor', hoekkeper: 'Hoekkeper', nokbalk: 'Nokbalk', kopbalk: 'Kopbalk' };
|
||||
const CAP_KINDS = { spoor: 'Spoor', hoekkeper: 'Hoekkeper', kilkeper: 'Kilkeper', nokbalk: 'Nokbalk', knikbalk: 'Knikbalk', kopbalk: 'Kopbalk' };
|
||||
function drawCapSheet(doc, u, sheetOpts) {
|
||||
const { RED, BLUE, BLACK, GREY } = PdfSheet.COLORS;
|
||||
const ur = u.roof.unitRoof, c = ur.cap;
|
||||
@@ -474,7 +475,8 @@ window.HousePdf = (() => {
|
||||
};
|
||||
doc.setTextColor(...BLACK);
|
||||
doc.setFontSize(11);
|
||||
doc.text('Bovenaanzicht bovenste deel (wolfseinden en nok)', X0, 20);
|
||||
const join = c.kind === 'join';
|
||||
doc.text(join ? 'Bovenaanzicht dak (aansluiting: kil- en hoekkepers, kreupele sporen)' : 'Bovenaanzicht bovenste deel (wolfseinden en nok)', X0, 20);
|
||||
doc.setDrawColor(...GREY);
|
||||
doc.setLineWidth(0.15);
|
||||
c.regions.forEach(rg => poly(rg.poly.map(xz), 'S'));
|
||||
@@ -505,17 +507,22 @@ window.HousePdf = (() => {
|
||||
const H = (p) => RoofModel.profileHeight(ur.shape.profile, p);
|
||||
const deg = (t) => String(Math.round(Math.atan(t) * 1800 / Math.PI) / 10).replace('.', ',');
|
||||
const hip = c.regions.find(rg => rg.key === 'h1');
|
||||
const facts = [
|
||||
const facts = (join ? [
|
||||
['Goot (onderkant dak)', `${Math.round(c.eave)} mm`],
|
||||
['Nok (onderkant dak)', `${Math.round(c.ridgeH)} mm`],
|
||||
['Helling dakvlakken', `${deg(c.slope)}°`]
|
||||
] : [
|
||||
['Wolfseind (onderkant dak)', `${Math.round(c.hipH)} mm`],
|
||||
['Nok (onderkant dak)', `${Math.round(H(c.pR))} mm`],
|
||||
['Helling dakvlakken', `${deg(c.slope)}°`],
|
||||
['Helling wolfseinden', hip ? `${deg(Math.tan(Math.acos(hip.cos)))}°` : '-'],
|
||||
['Helling wolfseinden', hip ? `${deg(Math.tan(Math.acos(hip.cos)))}°` : '-']
|
||||
]).concat([
|
||||
['Balken', `${c.joistW} x ${c.joistH} mm`],
|
||||
['Oppervlak (schuin)', `${fmt(c.specs.osbAreaM2, 2)} m²`],
|
||||
['Hout', `${fmt(c.specs.joistWoodM3, 3)} m³`]
|
||||
];
|
||||
]);
|
||||
doc.setTextColor(...BLACK);
|
||||
let y = PdfSheet.table(doc, 290, 20, [0, 52], ['Gegeven', 'Waarde'], facts, { title: 'Wolfsdak, bovenste deel', fontSize: 7, rowH: 4.6, lastColW: 40 });
|
||||
let y = PdfSheet.table(doc, 290, 20, [0, 52], ['Gegeven', 'Waarde'], facts, { title: join ? 'Dak met aansluiting' : 'Wolfsdak, bovenste deel', fontSize: 7, rowH: 4.6, lastColW: 40 });
|
||||
PdfSheet.table(doc, 290, y + 6, [0, 32, 56, 78], ['Soort', 'Lengte (mm)', 'Hoek (°)', 'Aantal'], rows, { title: 'Stuklijst balken', fontSize: 7, rowH: 4.6, lastColW: 14, maxY: 240 });
|
||||
doc.setFontSize(7);
|
||||
doc.setTextColor(...GREY);
|
||||
|
||||
+9
-27
@@ -1228,7 +1228,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
toggle('hs-roof-house', 'Over hele woning', hr.enabled, { hint: 'Eén dakvorm over alle units (bijv. een grote wolfskap); uit = dak per unit. De daken per unit blijven bewaard.' }) +
|
||||
|
||||
(houseRoofUi ? sel('hs-roof-cover', 'Bedekking', covOpts, RoofModel.coveringFor(state.units[0].roof.covering, shapeType), { hint: 'Voor alle units (per unit aan te passen)' }) : '') +
|
||||
toggle('hs-steel-ridge', 'Stalen nokbalk', state.units.some(u => u.roof.steelRidge !== false), { hint: 'IPE onder de nok van zadel-, mansarde- en wolfsdaken (alle units); uit = verwijderen' }) }
|
||||
toggle('hs-steel-ridge', 'Stalen nokbalk', state.units.some(u => u.roof.steelRidge !== false), { hint: 'IPE onder de nok van zadel- en wolfsdaken (alle units); uit = verwijderen' }) }
|
||||
];
|
||||
if (houseRoofUi) groups.push(...houseRoofUi.groups());
|
||||
else groups.push({ label: 'Dakvorm', html: note('Vorm, hoogtes, helling en bedekking zijn per unit: selecteer een unit.') });
|
||||
@@ -1256,7 +1256,8 @@ function mountHouseEditor(container, callbacks) {
|
||||
const el = body.querySelector('[data-live="roof-list"]');
|
||||
if (!el || !parts) return;
|
||||
el.innerHTML = parts.units.map(u => {
|
||||
const pl = u.roof.planes[0], deg = Math.abs(pl ? pl.theta : 0) * 180 / Math.PI;
|
||||
const pl = u.roof.planes[0], cap = u.roof.unitRoof.cap;
|
||||
const deg = (pl ? Math.abs(pl.theta) : cap ? Math.atan(cap.slope) : 0) * 180 / Math.PI;
|
||||
const cov = (RoofModel.COVERINGS[u.roof.covering] || {}).label || '';
|
||||
return kv(`Unit ${u.id}`, `${(UnitModel.ROOF_TYPES[u.roof.shape.type] || u.roof.shape.type).toLowerCase()}${deg > 0.05 ? ' ' + fmtDeg(deg) + '°' : ''} · ${cov.toLowerCase()}`);
|
||||
}).join('');
|
||||
@@ -1522,11 +1523,13 @@ function mountHouseEditor(container, callbacks) {
|
||||
}
|
||||
const FX_OPT_UI = {
|
||||
front: { label: 'Front', kind: 'sel', options: () => Object.fromEntries(Object.entries(FixtureModel.FRONTS).map(([k, f]) => [k, f.label])) },
|
||||
spoelbak: { label: 'Spoelbak', kind: 'range', min: 0, max: 1, step: 0.01, unit: '', hint: '0 = links, 1 = rechts (van voren gezien)' },
|
||||
spoel: { label: 'Spoelbak', kind: 'toggle' },
|
||||
spoelbak: { label: 'Plek spoelbak', kind: 'range', min: 0, max: 1, step: 0.01, unit: '', hint: '0 = links, 1 = rechts (van voren gezien)' },
|
||||
hogeKast: { label: 'Hoge kast', kind: 'sel', options: () => ({ geen: 'Geen', links: 'Links', rechts: 'Rechts' }), hint: 'Inbouwkoelkast + oven, 600 mm' },
|
||||
vaatwasser: { label: 'Vaatwasser', kind: 'toggle' },
|
||||
bovenkasten: { label: 'Bovenkasten', kind: 'toggle' },
|
||||
kookplaat: { label: 'Kookplaat', kind: 'toggle', hint: 'Inductie, 4 zones' },
|
||||
kookplaatPlek: { label: 'Plek kookplaat', kind: 'range', min: 0, max: 1, step: 0.01, unit: '', hint: '0 = links, 1 = rechts (van voren gezien)' },
|
||||
glas: { label: 'Glaswand', kind: 'sel', options: () => ({ links: 'Links', rechts: 'Rechts', geen: 'Geen' }), hint: 'Van voren gezien' },
|
||||
glasLengte: { label: 'Lengte glas', kind: 'num', step: 50 },
|
||||
spiegel: { label: 'Spiegel', kind: 'toggle' },
|
||||
@@ -1752,10 +1755,6 @@ function mountHouseEditor(container, callbacks) {
|
||||
const LIM = UnitModel.LIMITS;
|
||||
const runOf = () => UnitModel.hasRidge(form.type) ? o.span() / 2 : o.span();
|
||||
const degOf = () => sloped() ? Math.atan(Math.max(0, form.high - form.low) / runOf()) * 180 / Math.PI : 0;
|
||||
const mansardeDeg = () => {
|
||||
const kIn = Math.max(1, form.kneeIn || 1), rest = Math.max(1, runOf() - kIn);
|
||||
return [Math.atan(Math.max(0, form.kneeH - form.low) / kIn), Math.atan(Math.max(0, form.high - form.kneeH) / rest)].map(r => r * 180 / Math.PI);
|
||||
};
|
||||
const f = (k) => `${id}-roof-${k}`;
|
||||
const groups = () => {
|
||||
const g = [
|
||||
@@ -1766,12 +1765,8 @@ function mountHouseEditor(container, callbacks) {
|
||||
];
|
||||
if (sloped()) g.push({ label: 'Helling', html:
|
||||
UI.range(f('high'), UnitModel.hasRidge(form.type) ? 'Nok' : 'Hoog', form.high, { min: 2100, max: LIM.ridge, step: 10, hint: `Max ${LIM.ridge} mm` }) +
|
||||
(form.type === 'mansarde' ? '' : UI.range(f('deg'), 'Helling', Math.round(degOf() * 10) / 10, { min: 1, max: 60, step: 0.5, unit: '°', hint: 'Zet de hoge zijde/nok bij de huidige goothoogte' })) +
|
||||
UI.range(f('deg'), 'Helling', Math.round(degOf() * 10) / 10, { min: 1, max: 60, step: 0.5, unit: '°', hint: 'Zet de hoge zijde/nok bij de huidige goothoogte' }) +
|
||||
(o.gable ? `<div data-live="roof-gable">${kv('Kopgevel', `wand ${SIDE_SHORT[UnitModel.gableOf(form)].toLowerCase()} + overstaande`)}</div>` : '') });
|
||||
if (form.type === 'mansarde') g.push({ label: 'Mansarde', html:
|
||||
UI.range(f('knee-h'), 'Knik', form.kneeH, { min: 2100, max: LIM.ridge, step: 10, hint: 'Hoogte van de knik tussen het steile en het flauwe dakvlak' }) +
|
||||
UI.range(f('knee-in'), 'Knik in', form.kneeIn, { min: 100, max: 3000, step: 10, hint: 'Horizontale afstand van de gevel tot de knik' }) +
|
||||
`<div data-live="roof-mansarde">${kv('Hellingen', mansardeDeg().map(d => fmtDeg(d) + '°').join(' / '))}</div>` });
|
||||
if (form.type === 'wolf') g.push({ label: 'Wolfsdak', html:
|
||||
UI.range(f('cut'), 'Wolfseind', form.cut, { min: 2100, max: LIM.ridge, step: 10, hint: 'Hoogte waarop de kopgevels stoppen; daarboven driehoekige wolfseinden met de dakhelling tot de nok' }) });
|
||||
return g;
|
||||
@@ -1787,13 +1782,6 @@ function mountHouseEditor(container, callbacks) {
|
||||
if (v !== 'plat' && !(form.high > form.low)) form.high = form.low + 700;
|
||||
if (v === 'lessenaar' && !form.dir) form.dir = 'voor';
|
||||
if (UnitModel.hasRidge(v) && !form.ridge) form.ridge = 'z';
|
||||
if (v === 'mansarde') {
|
||||
const run = runOf();
|
||||
form.kneeIn = Math.round(run * 0.35);
|
||||
form.kneeH = Math.round(form.low + form.kneeIn * 2.1);
|
||||
form.high = Math.min(LIM.ridge, Math.max(form.high, Math.round(form.kneeH + (run - form.kneeIn) * 0.5)));
|
||||
form.kneeRatio = Math.min(0.9, (form.kneeH - form.low) / (form.high - form.low));
|
||||
}
|
||||
if (v === 'wolf') {
|
||||
if (form.high - form.low < 1000) form.high = form.low + 1500;
|
||||
form.cutRatio = 0.6;
|
||||
@@ -1804,13 +1792,9 @@ function mountHouseEditor(container, callbacks) {
|
||||
UI.onSel(f('ridge'), v => { form.ridge = v; apply(); }, rerender);
|
||||
const syncDeg = () => {
|
||||
const now = o.readBack();
|
||||
if (now && now.type === form.type) ['kneeH', 'kneeIn', 'cut'].forEach(k => { if (now[k] !== undefined) form[k] = now[k]; });
|
||||
if (now && now.type === form.type) ['cut'].forEach(k => { if (now[k] !== undefined) form[k] = now[k]; });
|
||||
UI.setRange(f('deg'), Math.round(degOf() * 10) / 10);
|
||||
UI.setRange(f('knee-h'), form.kneeH);
|
||||
UI.setRange(f('knee-in'), form.kneeIn);
|
||||
UI.setRange(f('cut'), form.cut);
|
||||
const md = body.querySelector('[data-live="roof-mansarde"]');
|
||||
if (md && form.type === 'mansarde') md.innerHTML = kv('Hellingen', mansardeDeg().map(d => fmtDeg(d) + '°').join(' / '));
|
||||
};
|
||||
UI.onRange(f('low'), v => {
|
||||
if (!(v >= 500)) return;
|
||||
@@ -1830,8 +1814,6 @@ function mountHouseEditor(container, callbacks) {
|
||||
apply(); syncDeg();
|
||||
}, after);
|
||||
const ratioOf = (v) => Math.max(0.05, Math.min(0.95, (v - form.low) / Math.max(1, form.high - form.low)));
|
||||
UI.onRange(f('knee-h'), v => { if (v >= 500) { form.kneeRatio = ratioOf(v); apply(); syncDeg(); } }, after);
|
||||
UI.onRange(f('knee-in'), v => { if (v >= 50) { form.kneeIn = v; apply(); syncDeg(); } }, after);
|
||||
UI.onRange(f('cut'), v => { if (v >= 500) { form.cutRatio = ratioOf(v); apply(); syncDeg(); } }, after);
|
||||
};
|
||||
return { groups, wire };
|
||||
@@ -1875,7 +1857,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
pair(sel('hp-roof-cover', 'Bedekking', covOpts, covNow, { hint: shapeNow === 'plat' ? 'Riet en felsplaten alleen bij een hellend dak' : `${c.thickMM} mm, ${String(c.kgM2).replace('.', ',')} kg/m²` }),
|
||||
thumb(RoofModel.coveringThumbUrl(covNow, 64), c.label)) +
|
||||
toggle('hp-fascia', 'Boeidelen', u.roof.fascia !== false, { hint: 'Uit = daktrim' }) +
|
||||
(pu && pu.roof.unitRoof.ridgeBeam || u.roof.steelRidge === false && ['zadel', 'mansarde', 'wolf'].includes(shapeNow)
|
||||
(pu && pu.roof.unitRoof.ridgeBeam || u.roof.steelRidge === false && ['zadel', 'wolf'].includes(shapeNow)
|
||||
? toggle('hp-steel-ridge', 'Stalen nokbalk', u.roof.steelRidge !== false, { hint: pu && pu.roof.unitRoof.ridgeBeam ? `${pu.roof.unitRoof.ridgeBeam.profile.name}, ${Math.round(pu.roof.unitRoof.ridgeBeam.steelKg)} kg; uit = verwijderen` : 'Uit = verwijderd' }) : '') +
|
||||
toggle('hp-roof-follow', 'Maat volgt vloer', u.roof.followFloor) });
|
||||
if (!u.roof.followFloor) groups.push({ label: 'Dakmaat', html:
|
||||
|
||||
@@ -162,8 +162,8 @@ window.FixtureModel = (() => {
|
||||
},
|
||||
keuken: {
|
||||
label: 'Keukenblok', cat: 'keuken', w: 2660, d: 600, h: 910, resize: ['w', 'd', 'h'], minW: 600, wall: true, cost: 'keuken',
|
||||
opts: { front: 'antraciet', spoelbak: 0.6, hogeKast: 'links', vaatwasser: true, bovenkasten: false },
|
||||
hint: 'Onderkasten met werkblad en spoelbak (plek: 0 = links, 1 = rechts, van voren gezien); hoge kast 600 mm met inbouwkoelkast en oven',
|
||||
opts: { front: 'antraciet', spoel: true, spoelbak: 0.6, kookplaat: false, kookplaatPlek: 0.2, hogeKast: 'links', vaatwasser: true, bovenkasten: false },
|
||||
hint: 'Onderkasten met werkblad, spoelbak en eventueel kookplaat (plek: 0 = links, 1 = rechts, van voren gezien); hoge kast 600 mm met inbouwkoelkast en oven',
|
||||
build(f, g) { kitchenRun(g, f, false); },
|
||||
plan(f) { return kitchenPlan(f, false); }
|
||||
},
|
||||
@@ -386,25 +386,32 @@ window.FixtureModel = (() => {
|
||||
boxAt(g, front, a + 2, b - 2, 1790, 2195, zf - 22, zf - 2);
|
||||
}
|
||||
if (!island) {
|
||||
// Spoelbak + kraan
|
||||
if (f.opts.spoel !== false) {
|
||||
const sx = spoelX(f, K);
|
||||
boxAt(g, MAT.chroom(), sx - 380, sx + 380, top - 2, top + 0.5, zb + 90, zf - 70);
|
||||
boxAt(g, MAT.rubber(), sx - 360, sx + 360, top - 1, top + 1, zb + 105, zf - 85);
|
||||
ecyl(g, MAT.zwartMetaal(), sx + 200, zb + 55, 22, 22, top, top + 300);
|
||||
boxAt(g, MAT.zwartMetaal(), sx + 190, sx + 210, top + 280, top + 300, zb + 55, zb + 280);
|
||||
}
|
||||
if (f.opts.bovenkasten) boxAt(g, front, K.x0, K.x1, 1450, 2150, zb, zb + 350);
|
||||
} else if (f.opts.kookplaat) {
|
||||
const hw = Math.min(780, K.x1 - K.x0 - 100), hd = Math.min(520, f.d - 80);
|
||||
boxAt(g, MAT.kookplaat(), -hw / 2, hw / 2, top, top + 4, -hd / 2, hd / 2);
|
||||
hobZones(hw, hd).forEach(z => ecyl(g, MAT.ring(), z.x, z.z, z.r, z.r, top + 4, top + 5));
|
||||
}
|
||||
if (f.opts.kookplaat) {
|
||||
const { hx, hw, hd } = hobOf(f, K, island);
|
||||
boxAt(g, MAT.kookplaat(), hx - hw / 2, hx + hw / 2, top, top + 4, -hd / 2, hd / 2);
|
||||
hobZones(hw, hd).forEach(z => ecyl(g, MAT.ring(), hx + z.x, z.z, z.r, z.r, top + 4, top + 5));
|
||||
}
|
||||
}
|
||||
const hobZones = (hw, hd) => [[-0.25, -0.25, 0.19], [0.25, -0.25, 0.14], [-0.25, 0.25, 0.14], [0.25, 0.25, 0.19]]
|
||||
.map(([a, b, r]) => ({ x: a * hw, z: b * hd, r: r * Math.min(hw, hd * 1.4) }));
|
||||
function spoelX(f, K) {
|
||||
const t = Math.max(0, Math.min(1, f.opts.spoelbak === undefined ? 0.6 : f.opts.spoelbak));
|
||||
function plekX(K, v, def) {
|
||||
const t = Math.max(0, Math.min(1, v === undefined ? def : v));
|
||||
return Math.max(K.x0 + 400, Math.min(K.x1 - 400, K.x0 + t * (K.x1 - K.x0)));
|
||||
}
|
||||
const spoelX = (f, K) => plekX(K, f.opts.spoelbak, 0.6);
|
||||
// Kookeiland: kookplaat in het midden; keukenblok: op kookplaatPlek.
|
||||
const hobOf = (f, K, island) => ({
|
||||
hx: island ? 0 : plekX(K, f.opts.kookplaatPlek, 0.2), hw: Math.min(780, K.x1 - K.x0 - 100), hd: Math.min(520, f.d - 80)
|
||||
});
|
||||
function kitchenPlan(f, island) {
|
||||
const zb = -f.d / 2, zf = f.d / 2, K = kitchenModules(f, island), out = [];
|
||||
out.push(rectP(K.x0, K.x1, zb, zf));
|
||||
@@ -413,13 +420,14 @@ window.FixtureModel = (() => {
|
||||
const a = K.tall === 'links' ? -f.w / 2 : f.w / 2 - 600;
|
||||
out.push(rectP(a, a + 600, zb, zf), lineP(a, zb, a + 600, zf, 'thin'), lineP(a, zf, a + 600, zb, 'thin'));
|
||||
}
|
||||
if (!island) {
|
||||
if (!island && f.opts.spoel !== false) {
|
||||
const sx = spoelX(f, K);
|
||||
out.push(rrectP(sx - 360, sx + 360, zb + 105, zf - 85, 40, 'thin'), circP(sx + 200, zb + 55, 22, 'thin'));
|
||||
} else if (f.opts.kookplaat) {
|
||||
const hw = Math.min(780, K.x1 - K.x0 - 100), hd = Math.min(520, f.d - 80);
|
||||
out.push(rectP(-hw / 2, hw / 2, -hd / 2, hd / 2, 'thin'));
|
||||
hobZones(hw, hd).forEach(z => out.push(circP(z.x, z.z, z.r, 'thin')));
|
||||
}
|
||||
if (f.opts.kookplaat) {
|
||||
const { hx, hw, hd } = hobOf(f, K, island);
|
||||
out.push(rectP(hx - hw / 2, hx + hw / 2, -hd / 2, hd / 2, 'thin'));
|
||||
hobZones(hw, hd).forEach(z => out.push(circP(hx + z.x, z.z, z.r, 'thin')));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ window.LightingModel = (() => {
|
||||
mode: 'spots',
|
||||
direction: 'both',
|
||||
lampHeight: 1950,
|
||||
lampMin: 1500,
|
||||
lampMin: 500,
|
||||
lampMax: 3000,
|
||||
minWallWidth: 2000,
|
||||
endOffset: 300,
|
||||
|
||||
+264
-70
@@ -212,7 +212,7 @@ window.RoofModel = (() => {
|
||||
return { type: 'wolf', axis, profile: [L, A, ridge, B, R], hip: { h: A.h } };
|
||||
}
|
||||
}
|
||||
const type = profile.length === 2 ? 'lessenaar' : profile.length === 5 ? 'mansarde' : 'zadel';
|
||||
const type = profile.length === 2 ? 'lessenaar' : 'zadel';
|
||||
return { type, axis, profile };
|
||||
}
|
||||
|
||||
@@ -267,8 +267,9 @@ window.RoofModel = (() => {
|
||||
finishKNm2: 0.3, // afwerking/installaties onder het dak
|
||||
gammaG: 1.2, gammaQ: 1.5, deflection: 250
|
||||
};
|
||||
const ridgeTypes = ['zadel', 'mansarde', 'wolf'];
|
||||
function computeRidgeBeam(roofState, shape, rect, alongX, cd, kgM2) {
|
||||
const ridgeTypes = ['zadel', 'wolf'];
|
||||
// span: [q0, q1] waar de nok echt loopt (dakaansluiting: tot de hoekkeper).
|
||||
function computeRidgeBeam(roofState, shape, rect, alongX, cd, kgM2, span) {
|
||||
if (roofState.steelRidge === false || !ridgeTypes.includes(shape.type)) return null;
|
||||
const pr = shape.profile, inner = pr.slice(1, -1);
|
||||
if (!inner.length) return null;
|
||||
@@ -283,6 +284,7 @@ window.RoofModel = (() => {
|
||||
q0 = Math.max(uq0, Math.min(g.qM, g.q0 + g.inset));
|
||||
q1 = Math.min(uq1, Math.max(g.qM, g.q1 - g.inset));
|
||||
}
|
||||
if (span) { q0 = Math.max(q0, span[0]); q1 = Math.min(q1, span[1]); }
|
||||
const L = q1 - q0;
|
||||
if (L < 100) return null;
|
||||
// q in N/mm = kN/m.
|
||||
@@ -393,7 +395,7 @@ window.RoofModel = (() => {
|
||||
});
|
||||
});
|
||||
|
||||
// Mansardeknik: het steilere vlak is verticaal dikker, dus verstek: onderkant op de knik,
|
||||
// Knik in de daklijn: het steilere vlak is verticaal dikker, dus verstek: onderkant op de knik,
|
||||
// bovenkant op het snijpunt van de bovenvlakken.
|
||||
planes.forEach((pl, i) => { pl.miter0 = 0; pl.miter1 = 0; });
|
||||
planes.slice(0, -1).forEach((pi, i) => {
|
||||
@@ -418,11 +420,7 @@ window.RoofModel = (() => {
|
||||
}
|
||||
const specs = sumSpecs(outPlanes.map(pl => pl.specs).concat(cap ? [cap.specs] : []));
|
||||
const ridgeBeam = computeRidgeBeam(roofState, shape, rect, alongX, flat.calcData, specs.totalKg / Math.max(0.1, W * D / 1e6));
|
||||
Object.assign(specs, {
|
||||
nokStaalM: ridgeBeam ? ridgeBeam.length / 1000 : 0, nokStaalKg: ridgeBeam ? ridgeBeam.steelKg : 0,
|
||||
nokVulhoutM3: ridgeBeam ? ridgeBeam.fillM3 : 0, nokVulhoutKg: ridgeBeam ? ridgeBeam.fillKg : 0
|
||||
});
|
||||
specs.totalKg += specs.nokStaalKg + specs.nokVulhoutKg;
|
||||
addRidgeSpecs(specs, ridgeBeam);
|
||||
return {
|
||||
shape, covering, planes: outPlanes, specs, ridgeBeam,
|
||||
undersideAt, topAt, cap, rect, alongX,
|
||||
@@ -431,6 +429,185 @@ window.RoofModel = (() => {
|
||||
};
|
||||
}
|
||||
|
||||
function addRidgeSpecs(specs, ridgeBeam) {
|
||||
Object.assign(specs, {
|
||||
nokStaalM: ridgeBeam ? ridgeBeam.length / 1000 : 0, nokStaalKg: ridgeBeam ? ridgeBeam.steelKg : 0,
|
||||
nokVulhoutM3: ridgeBeam ? ridgeBeam.fillM3 : 0, nokVulhoutKg: ridgeBeam ? ridgeBeam.fillKg : 0
|
||||
});
|
||||
specs.totalKg += specs.nokStaalKg + specs.nokVulhoutKg;
|
||||
}
|
||||
|
||||
// Dakaansluiting (HouseModel.roofJoins): vlakken h = a + bx·x + bz·z, samengevoegd volgens expr
|
||||
// (vlaknummer of ['min' | 'max', ...]). extra(k): ophoging per vlak (bovenkant dak).
|
||||
const planeAt = (pl, p) => pl.a + pl.bx * p.x + pl.bz * p.z;
|
||||
function joinValue(join, p, extra) {
|
||||
const at = (e) => Array.isArray(e) ? Math[e[0]](...e.slice(1).map(at)) : planeAt(join.planes[e], p) + (extra ? extra(e) : 0);
|
||||
return at(join.expr);
|
||||
}
|
||||
function convexHull(pts) {
|
||||
const sorted = pts.slice().sort((a, b) => a.x - b.x || a.z - b.z);
|
||||
const cross = (o, a, b) => (a.x - o.x) * (b.z - o.z) - (a.z - o.z) * (b.x - o.x);
|
||||
const half = (list) => list.reduce((h, q) => {
|
||||
while (h.length >= 2 && cross(h[h.length - 2], h[h.length - 1], q) <= 0.01 * Math.hypot(q.x - h[h.length - 2].x, q.z - h[h.length - 2].z)) h.pop();
|
||||
h.push(q);
|
||||
return h;
|
||||
}, []);
|
||||
const lo = half(sorted), up = half(sorted.slice().reverse());
|
||||
const out = lo.slice(0, -1).concat(up.slice(0, -1));
|
||||
return out.filter((p, k) => { const q = out[(k + 1) % out.length]; return Math.hypot(q.x - p.x, q.z - p.z) > 0.5; });
|
||||
}
|
||||
|
||||
// Dak van één unit in een dakaansluiting (join in unit-lokale maten): per vlak de convexe stukken binnen het dek,
|
||||
// sporen haaks op de goot (kreupel tegen kil- en hoekkepers), kepers en nokken op de grenzen tussen de vlakken.
|
||||
// Alles als houtwerk zoals het bovenste deel van een wolfsdak (cap); de eigen nok wordt de stalen nokbalk.
|
||||
function computeJoinedRoof(roofState, join, rect, shape) {
|
||||
const fp = roofState.deck.floor;
|
||||
const joistW = fp.joistWidth || 70, joistH = fp.joistHeight || 220, underlay = fp.underlayThick || 22;
|
||||
const covering = coveringFor(roofState.covering, shape.type);
|
||||
const cov = COVERINGS[covering] || COVERINGS.dakleer;
|
||||
const pls = join.planes.map((pl, k) => {
|
||||
const slope = Math.hypot(pl.bx, pl.bz), cos = 1 / Math.sqrt(1 + slope * slope);
|
||||
const tV = underlay / cos, jV = joistH / cos, bV = BESCHOT_MM / cos, covV = cov.thickMM / cos;
|
||||
const under = (p) => planeAt(pl, p);
|
||||
return { key: k, pl, slope, cos, tV, jV, bV, covV, vThick: tV + jV + bV + covV, under, f: under, along: Math.abs(pl.bx) >= Math.abs(pl.bz) ? 'p' : 'q' };
|
||||
});
|
||||
const activeAt = (p) => {
|
||||
const h = joinValue(join, p);
|
||||
return pls.reduce((a, b) => Math.abs(b.under(p) - h) < Math.abs(a.under(p) - h) ? b : a);
|
||||
};
|
||||
|
||||
const box = [{ x: rect.x0, z: rect.z0 }, { x: rect.x1, z: rect.z0 }, { x: rect.x1, z: rect.z1 }, { x: rect.x0, z: rect.z1 }];
|
||||
const clipBox = (poly) => [(p) => p.x - rect.x0, (p) => rect.x1 - p.x, (p) => p.z - rect.z0, (p) => rect.z1 - p.z]
|
||||
.reduce((pl, f) => pl.length >= 3 ? clipPoly(pl, f) : pl, poly);
|
||||
const pairs = [];
|
||||
pls.forEach((A, i) => pls.slice(i + 1).forEach(B => {
|
||||
if (Math.hypot(A.pl.bx - B.pl.bx, A.pl.bz - B.pl.bz) > 1e-9) pairs.push({ A, B, f: (p) => A.under(p) - B.under(p) });
|
||||
}));
|
||||
// Afgeronde wandhoogtes maken bijna samenvallende lijnen: splinters (gemiddeld < 0,5 mm breed) vallen weg.
|
||||
const perim = (poly) => poly.reduce((t, p, k) => { const q = poly[(k + 1) % poly.length]; return t + Math.hypot(q.x - p.x, q.z - p.z); }, 0);
|
||||
const sliver = (poly) => quadArea(poly) < Math.max(100, 0.25 * perim(poly));
|
||||
let cells = [box];
|
||||
pairs.forEach(({ f }) => {
|
||||
cells = cells.flatMap(c => [clipPoly(c, f), clipPoly(c, p => -f(p))]).filter(c => c.length >= 3 && !sliver(c));
|
||||
});
|
||||
const centroid = (poly) => ({ x: poly.reduce((t, p) => t + p.x, 0) / poly.length, z: poly.reduce((t, p) => t + p.z, 0) / poly.length });
|
||||
const regions = [];
|
||||
pls.forEach(pl => {
|
||||
const list = cells.filter(c => activeAt(centroid(c)) === pl).map(convexHull);
|
||||
// Aangrenzende cellen samenvoegen zolang het geheel convex blijft.
|
||||
let merged = true;
|
||||
while (merged) {
|
||||
merged = false;
|
||||
for (let i = 0; i < list.length && !merged; i++) {
|
||||
for (let j = i + 1; j < list.length && !merged; j++) {
|
||||
const h = convexHull(list[i].concat(list[j]));
|
||||
if (quadArea(h) - quadArea(list[i]) - quadArea(list[j]) < 0.5 * perim(h)) { list[i] = h; list.splice(j, 1); merged = true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
list.filter(poly => poly.length >= 3).forEach(poly => regions.push(Object.assign({}, pl, { poly })));
|
||||
});
|
||||
|
||||
// Grenzen tussen twee vlakken: bol = nok (waterpas) of hoekkeper, hol = kilkeper.
|
||||
const members = [];
|
||||
pairs.forEach(({ A, B, f }) => {
|
||||
const gx = A.pl.bx - B.pl.bx, gz = A.pl.bz - B.pl.bz, g = Math.hypot(gx, gz), c0 = f({ x: 0, z: 0 });
|
||||
const o = { x: -c0 * gx / (g * g), z: -c0 * gz / (g * g) }, d = { x: -gz / g, z: gx / g }, n = { x: gx / g, z: gz / g };
|
||||
let t0 = -Infinity, t1 = Infinity;
|
||||
[['x', rect.x0, rect.x1], ['z', rect.z0, rect.z1]].forEach(([k, lo, hi]) => {
|
||||
if (Math.abs(d[k]) < 1e-9) { if (o[k] < lo || o[k] > hi) t1 = -Infinity; return; }
|
||||
const a = (lo - o[k]) / d[k], b = (hi - o[k]) / d[k];
|
||||
t0 = Math.max(t0, Math.min(a, b));
|
||||
t1 = Math.min(t1, Math.max(a, b));
|
||||
});
|
||||
if (!(t1 - t0 >= 100)) return;
|
||||
const pt = (t) => ({ x: o.x + d.x * t, z: o.z + d.z * t });
|
||||
const ts = [t0, t1];
|
||||
pairs.forEach(q => {
|
||||
const dn = q.f(pt(1)) - q.f(pt(0));
|
||||
if (Math.abs(dn) < 1e-9) return;
|
||||
const t = -q.f(pt(0)) / dn;
|
||||
if (t > t0 && t < t1) ts.push(t);
|
||||
});
|
||||
ts.sort((a, b) => a - b);
|
||||
const segs = [];
|
||||
let cur = null;
|
||||
ts.slice(0, -1).forEach((ta, k) => {
|
||||
const tb = ts[k + 1];
|
||||
if (tb - ta < 1) return;
|
||||
const m = pt((ta + tb) / 2), off = (s) => ({ x: m.x + n.x * s, z: m.z + n.z * s });
|
||||
const s1 = activeAt(off(2)), s2 = activeAt(off(-2));
|
||||
if (Math.abs(joinValue(join, m) - A.under(m)) > 0.5 || !((s1 === A && s2 === B) || (s1 === B && s2 === A))) { cur = null; return; }
|
||||
const onA = off(s1 === A ? 2 : -2), convex = B.under(onA) > A.under(onA);
|
||||
if (cur && cur.convex === convex) { cur.tb = tb; return; }
|
||||
cur = { ta, tb, convex };
|
||||
segs.push(cur);
|
||||
});
|
||||
// Waterpas en bol: nok als de vlakken van elkaar af aflopen, anders een knik.
|
||||
const ridge = A.pl.bx * B.pl.bx + A.pl.bz * B.pl.bz < 0;
|
||||
segs.forEach(sg => {
|
||||
const P = pt(sg.ta), Q = pt(sg.tb), level = Math.abs(A.under(P) - A.under(Q)) < 1;
|
||||
members.push({ kind: !sg.convex ? 'kilkeper' : !level ? 'hoekkeper' : ridge ? 'nokbalk' : 'knikbalk', A: P, B: Q, rg: A });
|
||||
});
|
||||
});
|
||||
|
||||
const alongX = shape.axis === 'x';
|
||||
const own = members.find(m => m.kind === 'nokbalk' && (alongX ? Math.abs(m.A.x - m.B.x) < 1 : Math.abs(m.A.z - m.B.z) < 1));
|
||||
const steel = own && roofState.steelRidge !== false;
|
||||
const { hw, beams, awayFrom, addBeam } = beamTools(joistW);
|
||||
members.forEach(m => { if (!(steel && m === own)) addBeam(m.kind, m.rg, m.A, m.B, [], true); });
|
||||
pls.forEach(pl => {
|
||||
const polys = regions.filter(rg => rg.key === pl.key).map(rg => rg.poly);
|
||||
if (!polys.length) return;
|
||||
const fixed = pl.along === 'p' ? 'z' : 'x';
|
||||
const [lo, hi] = fixed === 'z' ? [rect.z0, rect.z1] : [rect.x0, rect.x1];
|
||||
joistSpread(fp, hi - lo).map(v => lo + v).forEach(v => {
|
||||
const spans = polys.map(poly => cutLine(poly, fixed, v)).filter(Boolean).sort((a, b) => a[0] - b[0]);
|
||||
const runs = [];
|
||||
spans.forEach(sp => {
|
||||
const last = runs[runs.length - 1];
|
||||
if (last && sp[0] <= last[1] + 1) last[1] = Math.max(last[1], sp[1]);
|
||||
else runs.push(sp.slice());
|
||||
});
|
||||
runs.forEach(span => {
|
||||
const pt = (t) => fixed === 'z' ? { x: t, z: v } : { x: v, z: t };
|
||||
const A = pt(span[0]), B = pt(span[1]), mid = pt((span[0] + span[1]) / 2);
|
||||
const clips = members.filter(m => distToSeg(A, m) < hw + 1 || distToSeg(B, m) < hw + 1).map(m => awayFrom(m, mid));
|
||||
addBeam('spoor', pl, A, B, clips);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const ownBeams = clipBeams(beams, clipBox);
|
||||
const capSpecs = regionSpecs(ownBeams, regions, joistW, joistH, underlay, covering);
|
||||
const kgM2 = capSpecs.totalKg / Math.max(0.1, (rect.x1 - rect.x0) * (rect.z1 - rect.z0) / 1e6);
|
||||
const q = own ? (alongX ? [own.A.z, own.B.z] : [own.A.x, own.B.x]) : null;
|
||||
// Wolfsdak: de nok loopt hier tot de aansluiting, de inkorting van de wolfseinden volgt uit span.
|
||||
const ridgeShape = shape.type === 'wolf' ? Object.assign({}, shape, { type: 'zadel' }) : shape;
|
||||
const ridgeBeam = steel ? computeRidgeBeam(roofState, ridgeShape, rect, alongX, { underlayThick: underlay, joistH }, kgM2, [Math.min(...q), Math.max(...q)]) : null;
|
||||
if (ridgeBeam) ridgeBeam.alongX = alongX;
|
||||
const specs = Object.assign({}, capSpecs);
|
||||
addRidgeSpecs(specs, ridgeBeam);
|
||||
const heights = regions.flatMap(rg => rg.poly.map(p => rg.under(p)));
|
||||
const maxTop = Math.max(...regions.map(rg => Math.max(...rg.poly.map(p => rg.under(p) + rg.vThick))));
|
||||
const cap = {
|
||||
kind: 'join', pA: rect.x0, pB: rect.x1, q0: rect.z0, q1: rect.z1, eave: Math.min(...heights), ridgeH: Math.max(...heights),
|
||||
slope: Math.max(...regions.map(rg => rg.slope)), joistW, joistH, regions, beams: ownBeams, specs: capSpecs, maxTop
|
||||
};
|
||||
// Knikpunten van de daklijn op de randen (nok, knik, wolfseind, kepers): daar knikt ook het boeideel.
|
||||
const ridges = [];
|
||||
members.forEach(m => [m.A, m.B].forEach(p => {
|
||||
const onX = Math.abs(p.x - rect.x0) < 1 || Math.abs(p.x - rect.x1) < 1, onZ = Math.abs(p.z - rect.z0) < 1 || Math.abs(p.z - rect.z1) < 1;
|
||||
const r = onX && !onZ ? { axis: 'z', value: p.z } : onZ && !onX ? { axis: 'x', value: p.x } : null;
|
||||
if (r && !ridges.some(q => q.axis === r.axis && Math.abs(q.value - r.value) < 1)) ridges.push(r);
|
||||
}));
|
||||
return {
|
||||
shape, covering, planes: [], specs, ridgeBeam, cap, rect, alongX: true, ridges, maxTop,
|
||||
undersideAt: (x, z) => joinValue(join, { x, z }),
|
||||
topAt: (x, z) => joinValue(join, { x, z }, k => pls[k].vThick)
|
||||
};
|
||||
}
|
||||
|
||||
const DENSITY_WOOD_KGM3 = 450, DENSITY_OSB_KGM3 = 650; // zelfde als FloorModel
|
||||
function computeCap(roofState, shape, rect, alongX, covering) {
|
||||
const H = (p) => profileHeight(shape.profile, p);
|
||||
@@ -465,53 +642,7 @@ window.RoofModel = (() => {
|
||||
.filter(rg => rg.poly.length >= 3 && quadArea(rg.poly) > 100);
|
||||
if (!clipped.some(rg => rg.along === 'q')) return null;
|
||||
|
||||
const cutLine = (poly, fixed, v) => {
|
||||
const ts = [];
|
||||
poly.forEach((a, k) => {
|
||||
const b = poly[(k + 1) % poly.length], av = a[fixed], bv = b[fixed];
|
||||
if ((av - v) * (bv - v) > 0 || Math.abs(bv - av) < 1e-9) return;
|
||||
const t = (v - av) / (bv - av), o = fixed === 'x' ? 'z' : 'x';
|
||||
ts.push(a[o] + (b[o] - a[o]) * t);
|
||||
});
|
||||
return ts.length >= 2 ? [Math.min(...ts), Math.max(...ts)] : null;
|
||||
};
|
||||
const hw = joistW / 2;
|
||||
const rectAround = (A, B) => {
|
||||
const L = Math.hypot(B.x - A.x, B.z - A.z), nx = -(B.z - A.z) / L * hw, nz = (B.x - A.x) / L * hw;
|
||||
return [{ x: A.x - nx, z: A.z - nz }, { x: B.x - nx, z: B.z - nz }, { x: B.x + nx, z: B.z + nz }, { x: A.x + nx, z: A.z + nz }];
|
||||
};
|
||||
const awayFrom = (m, from) => {
|
||||
const L = Math.hypot(m.B.x - m.A.x, m.B.z - m.A.z), nx = -(m.B.z - m.A.z) / L, nz = (m.B.x - m.A.x) / L;
|
||||
const d = (p) => (p.x - m.A.x) * nx + (p.z - m.A.z) * nz;
|
||||
const sgn = d(from) >= 0 ? 1 : -1;
|
||||
return (p) => sgn * d(p) - hw;
|
||||
};
|
||||
const distToSeg = (p, m) => {
|
||||
const dx = m.B.x - m.A.x, dz = m.B.z - m.A.z, L2 = dx * dx + dz * dz;
|
||||
const t = Math.max(0, Math.min(1, ((p.x - m.A.x) * dx + (p.z - m.A.z) * dz) / L2));
|
||||
return Math.hypot(p.x - (m.A.x + dx * t), p.z - (m.A.z + dz * t));
|
||||
};
|
||||
const beams = [];
|
||||
const addBeam = (kind, rg, A, B, clips, lineHeight) => {
|
||||
if (Math.hypot(B.x - A.x, B.z - A.z) < 100) return null;
|
||||
let poly = rectAround(A, B);
|
||||
(clips || []).forEach(f => { if (poly.length >= 3) poly = clipPoly(poly, f); });
|
||||
if (poly.length < 3) return null;
|
||||
const dx = B.x - A.x, dz = B.z - A.z, L = Math.hypot(dx, dz);
|
||||
const yA = rg.under(A) + rg.tV, yB = rg.under(B) + rg.tV;
|
||||
const yAt = lineHeight
|
||||
? (p) => yA + (yB - yA) * Math.max(0, Math.min(1, ((p.x - A.x) * dx + (p.z - A.z) * dz) / (L * L)))
|
||||
: (p) => rg.under(p) + rg.tV;
|
||||
const ts = poly.map(p => ((p.x - A.x) * dx + (p.z - A.z) * dz) / L);
|
||||
const plan = Math.max(...ts) - Math.min(...ts), slope = Math.abs(yB - yA) / L;
|
||||
if (plan < 50) return null;
|
||||
const bm = {
|
||||
kind, A, B, poly, yAt, jV: rg.jV, slope,
|
||||
length: Math.round(plan * Math.sqrt(1 + slope * slope)), angle: Math.round(Math.atan(slope) * 1800 / Math.PI) / 10
|
||||
};
|
||||
beams.push(bm);
|
||||
return bm;
|
||||
};
|
||||
const { hw, beams, awayFrom, addBeam } = beamTools(joistW);
|
||||
const mOf = (x) => regions.find(rg => rg.key === (x < pR ? 'm1' : 'm2')) || regions[0];
|
||||
const hOf = (z) => regions.find(rg => rg.key === (z < qM ? 'h1' : 'h2')) || regions[0];
|
||||
const ends = [{ x: pR, z: Math.min(qM, q0 + inset) }, { x: pR, z: Math.max(qM, q1 - inset) }];
|
||||
@@ -534,8 +665,7 @@ window.RoofModel = (() => {
|
||||
if (ridgeBeam && roofState.steelRidge !== false) beams.splice(beams.indexOf(ridgeBeam), 1);
|
||||
}
|
||||
const members = kops.concat(hips, ridgeBeam ? [ridgeBeam] : []);
|
||||
const spread = (len) => FloorModel.computeFrame({ width: len, depth: 1000, joistWidth: joistW, joistHeight: joistH, minSpacing: fp.minSpacing, maxSpacing: fp.maxSpacing, underlayThick: underlay }).joists.map(x => x + len / 2);
|
||||
const qs = spread(q1 - q0).map(v => q0 + v), ps = spread(pB - pA).map(v => pA + v);
|
||||
const qs = joistSpread(fp, q1 - q0).map(v => q0 + v), ps = joistSpread(fp, pB - pA).map(v => pA + v);
|
||||
regions.forEach(rg => {
|
||||
const fixed = rg.along === 'p' ? 'z' : 'x';
|
||||
(rg.along === 'p' ? qs : ps).forEach(v => {
|
||||
@@ -548,8 +678,69 @@ window.RoofModel = (() => {
|
||||
});
|
||||
});
|
||||
|
||||
const ownBeams = beams.map(bm => {
|
||||
const poly = clipBox(bm.poly);
|
||||
const ownBeams = clipBeams(beams, clipBox);
|
||||
const specs = regionSpecs(ownBeams, clipped, joistW, joistH, underlay, covering);
|
||||
const maxTop = Math.max(...clipped.map(rg => Math.max(...rg.poly.map(pt => rg.under(pt) + rg.vThick))));
|
||||
return { pA: cp0, pR, pB: cp1, q0: cq0, q1: cq1, hipH, slope: sA, joistW, joistH, regions: clipped, beams: ownBeams, specs, maxTop };
|
||||
}
|
||||
|
||||
// Houtwerk op dakvlakken met een eigen omtrek (wolfseinden, dakaansluiting); balken als rechthoek in bovenaanzicht.
|
||||
function cutLine(poly, fixed, v) {
|
||||
const ts = [];
|
||||
poly.forEach((a, k) => {
|
||||
const b = poly[(k + 1) % poly.length], av = a[fixed], bv = b[fixed];
|
||||
if ((av - v) * (bv - v) > 0 || Math.abs(bv - av) < 1e-9) return;
|
||||
const t = (v - av) / (bv - av), o = fixed === 'x' ? 'z' : 'x';
|
||||
ts.push(a[o] + (b[o] - a[o]) * t);
|
||||
});
|
||||
return ts.length >= 2 ? [Math.min(...ts), Math.max(...ts)] : null;
|
||||
}
|
||||
function distToSeg(p, m) {
|
||||
const dx = m.B.x - m.A.x, dz = m.B.z - m.A.z, L2 = dx * dx + dz * dz;
|
||||
const t = Math.max(0, Math.min(1, ((p.x - m.A.x) * dx + (p.z - m.A.z) * dz) / L2));
|
||||
return Math.hypot(p.x - (m.A.x + dx * t), p.z - (m.A.z + dz * t));
|
||||
}
|
||||
function joistSpread(fp, len) {
|
||||
return FloorModel.computeFrame({ width: len, depth: 1000, joistWidth: fp.joistWidth || 70, joistHeight: fp.joistHeight || 220, minSpacing: fp.minSpacing, maxSpacing: fp.maxSpacing, underlayThick: fp.underlayThick || 22 })
|
||||
.joists.map(x => x + len / 2);
|
||||
}
|
||||
function beamTools(joistW) {
|
||||
const hw = joistW / 2, beams = [];
|
||||
const rectAround = (A, B) => {
|
||||
const L = Math.hypot(B.x - A.x, B.z - A.z), nx = -(B.z - A.z) / L * hw, nz = (B.x - A.x) / L * hw;
|
||||
return [{ x: A.x - nx, z: A.z - nz }, { x: B.x - nx, z: B.z - nz }, { x: B.x + nx, z: B.z + nz }, { x: A.x + nx, z: A.z + nz }];
|
||||
};
|
||||
const awayFrom = (m, from) => {
|
||||
const L = Math.hypot(m.B.x - m.A.x, m.B.z - m.A.z), nx = -(m.B.z - m.A.z) / L, nz = (m.B.x - m.A.x) / L;
|
||||
const d = (p) => (p.x - m.A.x) * nx + (p.z - m.A.z) * nz;
|
||||
const sgn = d(from) >= 0 ? 1 : -1;
|
||||
return (p) => sgn * d(p) - hw;
|
||||
};
|
||||
const addBeam = (kind, rg, A, B, clips, lineHeight) => {
|
||||
if (Math.hypot(B.x - A.x, B.z - A.z) < 100) return null;
|
||||
let poly = rectAround(A, B);
|
||||
(clips || []).forEach(f => { if (poly.length >= 3) poly = clipPoly(poly, f); });
|
||||
if (poly.length < 3) return null;
|
||||
const dx = B.x - A.x, dz = B.z - A.z, L = Math.hypot(dx, dz);
|
||||
const yA = rg.under(A) + rg.tV, yB = rg.under(B) + rg.tV;
|
||||
const yAt = lineHeight
|
||||
? (p) => yA + (yB - yA) * Math.max(0, Math.min(1, ((p.x - A.x) * dx + (p.z - A.z) * dz) / (L * L)))
|
||||
: (p) => rg.under(p) + rg.tV;
|
||||
const ts = poly.map(p => ((p.x - A.x) * dx + (p.z - A.z) * dz) / L);
|
||||
const plan = Math.max(...ts) - Math.min(...ts), slope = Math.abs(yB - yA) / L;
|
||||
if (plan < 50) return null;
|
||||
const bm = {
|
||||
kind, A, B, poly, yAt, jV: rg.jV, slope,
|
||||
length: Math.round(plan * Math.sqrt(1 + slope * slope)), angle: Math.round(Math.atan(slope) * 1800 / Math.PI) / 10
|
||||
};
|
||||
beams.push(bm);
|
||||
return bm;
|
||||
};
|
||||
return { hw, beams, awayFrom, addBeam };
|
||||
}
|
||||
function clipBeams(beams, clip) {
|
||||
return beams.map(bm => {
|
||||
const poly = clip(bm.poly);
|
||||
if (poly.length < 3 || quadArea(poly) < 100) return null;
|
||||
const dx = bm.B.x - bm.A.x, dz = bm.B.z - bm.A.z, L = Math.hypot(dx, dz);
|
||||
const ts = poly.map(p => ((p.x - bm.A.x) * dx + (p.z - bm.A.z) * dz) / L);
|
||||
@@ -557,20 +748,19 @@ window.RoofModel = (() => {
|
||||
if (plan < 50) return null;
|
||||
return Object.assign({}, bm, { poly, length: Math.round(plan * Math.sqrt(1 + slope * slope)) });
|
||||
}).filter(Boolean);
|
||||
|
||||
}
|
||||
function regionSpecs(beams, regions, joistW, joistH, underlay, covering) {
|
||||
const W = joistW * 1e-3, Hh = joistH * 1e-3;
|
||||
const woodM3 = ownBeams.reduce((t, bm) => t + bm.length * 1e-3 * W * Hh, 0);
|
||||
const areaM2 = clipped.reduce((t, rg) => t + quadArea(rg.poly) / rg.cos, 0) / 1e6;
|
||||
const woodM3 = beams.reduce((t, bm) => t + bm.length * 1e-3 * W * Hh, 0);
|
||||
const areaM2 = regions.reduce((t, rg) => t + quadArea(rg.poly) / rg.cos, 0) / 1e6;
|
||||
const cs = coverSpecs(covering, areaM2), osbVol = areaM2 * underlay * 1e-3, bVol = areaM2 * BESCHOT_MM * 1e-3;
|
||||
const specs = Object.assign({
|
||||
return Object.assign({
|
||||
joistWoodM3: woodM3, joistWoodKg: woodM3 * DENSITY_WOOD_KGM3,
|
||||
osbAreaM2: areaM2, osbVolM3: osbVol, osbKg: osbVol * DENSITY_OSB_KGM3,
|
||||
beschotM2: areaM2, beschotVolM3: bVol, beschotKg: bVol * DENSITY_OSB_KGM3,
|
||||
voidM3: Math.max(0, areaM2 * Hh - woodM3),
|
||||
totalKg: woodM3 * DENSITY_WOOD_KGM3 + (osbVol + bVol) * DENSITY_OSB_KGM3 + cs.coverKg
|
||||
}, cs);
|
||||
const maxTop = Math.max(...clipped.map(rg => Math.max(...rg.poly.map(pt => rg.under(pt) + rg.vThick))));
|
||||
return { pA: cp0, pR, pB: cp1, q0: cq0, q1: cq1, hipH, slope: sA, joistW, joistH, regions: clipped, beams: ownBeams, specs, maxTop };
|
||||
}
|
||||
|
||||
// Afschuiving i.p.v. kanteling (lokaal +Z = oplopende as): plan = z·cos θ, Y = y/cos θ + z·sin θ.
|
||||
@@ -581,7 +771,8 @@ window.RoofModel = (() => {
|
||||
if (unitRoof.cap) group.add(buildCap3D(unitRoof, opts));
|
||||
if (unitRoof.ridgeBeam) {
|
||||
const r = unitRoof.rect;
|
||||
group.add(buildRidgeBeam3D(unitRoof.ridgeBeam, (r.x0 + r.x1) / 2, (r.z0 + r.z1) / 2, unitRoof.alongX, opts.materials));
|
||||
const rb = unitRoof.ridgeBeam;
|
||||
group.add(buildRidgeBeam3D(rb, (r.x0 + r.x1) / 2, (r.z0 + r.z1) / 2, rb.alongX !== undefined ? rb.alongX : unitRoof.alongX, opts.materials));
|
||||
}
|
||||
unitRoof.planes.forEach(pl => {
|
||||
const g = new THREE.Group();
|
||||
@@ -647,7 +838,7 @@ window.RoofModel = (() => {
|
||||
const T = poly.map(pt => P(pt, bottom(pt) + h)), B = poly.map(pt => P(pt, bottom(pt)));
|
||||
const UV = poly.map(pt => uvOf ? uvOf(pt) : [0, 0]), z2 = [0, 0];
|
||||
const tri = (a, b, cc, ua, ub, uc) => { pos.push(...a, ...b, ...cc); uv.push(...ua, ...ub, ...uc); };
|
||||
const up = (T[1][0] - T[0][0]) * (T[2][2] - T[0][2]) - (T[1][2] - T[0][2]) * (T[2][0] - T[0][0]) < 0;
|
||||
const up = T.reduce((t, a, k) => { const b = T[(k + 1) % n]; return t + a[0] * b[2] - b[0] * a[2]; }, 0) < 0;
|
||||
for (let k = 1; k < n - 1; k++) up ? tri(T[0], T[k], T[k + 1], UV[0], UV[k], UV[k + 1]) : tri(T[0], T[k + 1], T[k], UV[0], UV[k + 1], UV[k]);
|
||||
const topCount = pos.length / 3;
|
||||
for (let k = 1; k < n - 1; k++) up ? tri(B[0], B[k + 1], B[k], z2, z2, z2) : tri(B[0], B[k], B[k + 1], z2, z2, z2);
|
||||
@@ -879,7 +1070,10 @@ window.RoofModel = (() => {
|
||||
s.overhang = Math.min(OVERHANG_MAX, Math.max(OVERHANG_MIN, s.overhang));
|
||||
delete s.edge; delete s.spots; // oud (v0.3.0): nu per unit
|
||||
const h = s.house || {};
|
||||
s.house = { enabled: !!h.enabled, form: h.form && UnitModel.ROOF_TYPES[h.form.type] ? Object.assign({}, h.form) : null, cover: !!h.cover };
|
||||
const form = h.form ? Object.assign({}, h.form) : null;
|
||||
// v0.6.2: mansarde vervallen, wordt een zadeldak met dezelfde nok.
|
||||
if (form && form.type === 'mansarde') { form.type = 'zadel'; delete form.kneeH; delete form.kneeIn; delete form.kneeRatio; }
|
||||
s.house = { enabled: !!h.enabled, form: form && UnitModel.ROOF_TYPES[form.type] ? form : null, cover: !!h.cover };
|
||||
if (!FASCIA_COLORS[s.fasciaColor]) s.fasciaColor = DEFAULT_SETTINGS.fasciaColor;
|
||||
return s;
|
||||
}
|
||||
@@ -887,7 +1081,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,
|
||||
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
|
||||
computeShape, shapeFromProfile, shapeLines, wallProfile, profileHeight, computeUnitRoof, buildUnit3D, capRegions,
|
||||
computeShape, shapeFromProfile, shapeLines, wallProfile, profileHeight, computeUnitRoof, computeJoinedRoof, joinValue, buildUnit3D, capRegions,
|
||||
computeEdge, buildEdge3D, normalizeSettings
|
||||
};
|
||||
})();
|
||||
|
||||
+12
-19
@@ -306,8 +306,8 @@ window.UnitModel = (() => {
|
||||
return Math.max(w.heightLeft, w.heightRight, w.heightMid > 0 ? w.heightMid : 0, ...(w.topPts || []).map(q => q.h));
|
||||
}
|
||||
|
||||
// kneeRatio / cutRatio gaan voor kneeH / cut, zodat ze meeschalen als goot of nok verandert.
|
||||
const LIMITS = { ridge: 5000, eave: 3000, short: 4300, long: 14000 };
|
||||
// cutRatio gaat voor cut, zodat ze meeschalen als goot of nok verandert.
|
||||
const LIMITS = { ridge: 5000, eave: 3000, short: 4700, long: 14000 };
|
||||
function clampSize(floor, keep) {
|
||||
floor.width = Math.min(LIMITS.long, floor.width);
|
||||
floor.depth = Math.min(LIMITS.long, floor.depth);
|
||||
@@ -317,9 +317,9 @@ window.UnitModel = (() => {
|
||||
}
|
||||
return floor;
|
||||
}
|
||||
const ROOF_TYPES = { plat: 'Plat', lessenaar: 'Lessenaar', zadel: 'Zadel', mansarde: 'Mansarde', wolf: 'Wolfsdak' };
|
||||
const ROOF_TYPES = { plat: 'Plat', lessenaar: 'Lessenaar', zadel: 'Zadel', wolf: 'Wolfsdak' };
|
||||
const RIDGES = { z: 'Noord–zuid', x: 'West–oost' };
|
||||
const hasRidge = (type) => type === 'zadel' || type === 'mansarde' || type === 'wolf';
|
||||
const hasRidge = (type) => type === 'zadel' || type === 'wolf';
|
||||
function gableOf(form) {
|
||||
if (hasRidge(form.type)) return form.ridge === 'x' ? 'links' : 'achter';
|
||||
if (form.type === 'lessenaar') return form.dir === 'links' || form.dir === 'rechts' ? 'achter' : 'links';
|
||||
@@ -333,10 +333,8 @@ window.UnitModel = (() => {
|
||||
const full = fullTopLine(w, gableSpan(unitState, side));
|
||||
const F = full[full.length - 1].u, inner = full.slice(1, -1);
|
||||
const L = full[0], R = full[full.length - 1], low = Math.round(Math.min(L.h, R.h));
|
||||
if (inner.length === 3) {
|
||||
const high = Math.round(inner[1].h);
|
||||
return { type: 'mansarde', ridge, low, high, kneeH: Math.round(inner[0].h), kneeIn: Math.round(inner[0].u), kneeRatio: (inner[0].h - low) / Math.max(1, high - low) };
|
||||
}
|
||||
// v0.6.2: mansarde vervallen; een opgeslagen mansarde wordt een zadeldak met dezelfde nok.
|
||||
if (inner.length === 3) return { type: 'zadel', ridge, low, high: Math.round(inner[1].h) };
|
||||
if (inner.length === 2 && Math.abs(inner[0].h - inner[1].h) < 0.5) {
|
||||
const slope = (inner[0].h - L.h) / Math.max(1, inner[0].u);
|
||||
const high = Math.round(L.h + slope * F / 2);
|
||||
@@ -351,6 +349,10 @@ window.UnitModel = (() => {
|
||||
const h = Math.max(...Object.values(W).map(wallTopOf));
|
||||
return { type: 'plat', low: h, high: h };
|
||||
}
|
||||
function dropMansarde(unitState) {
|
||||
const f = roofForm(unitState), side = f.type === 'zadel' ? gableOf(f) : null;
|
||||
if (side && fullTopLine(unitState.walls[side].wall, gableSpan(unitState, side)).length === 5) applyRoofForm(unitState, f);
|
||||
}
|
||||
function applyRoofForm(unitState, form) {
|
||||
const side = form.type !== 'plat' ? gableOf(form) : 'achter';
|
||||
const g = gableSpan(unitState, side);
|
||||
@@ -365,16 +367,7 @@ window.UnitModel = (() => {
|
||||
const half = F / 2;
|
||||
let line = [{ u: 0, h: low }, { u: F, h: low }];
|
||||
if (sloped && form.type === 'zadel') line = [{ u: 0, h: low }, { u: half, h: high }, { u: F, h: low }];
|
||||
else if (sloped && form.type === 'mansarde') {
|
||||
// Knik altijd boven de rechte lijn goot–nok, anders wordt het een hol dak.
|
||||
const kIn = Math.max(eMin + 100, Math.min(half - 100, Math.round(form.kneeIn || F * 0.18)));
|
||||
const minK = low + (high - low) * kIn / half + 20;
|
||||
const want = form.kneeRatio > 0 ? low + form.kneeRatio * (high - low) : form.kneeH;
|
||||
const kH = Math.round(Math.min(high - 20, Math.max(minK, want || minK)));
|
||||
line = kH > minK - 1 && high - kH >= 20
|
||||
? [{ u: 0, h: low }, { u: kIn, h: kH }, { u: half, h: high }, { u: F - kIn, h: kH }, { u: F, h: low }]
|
||||
: [{ u: 0, h: low }, { u: half, h: high }, { u: F, h: low }];
|
||||
} else if (sloped && form.type === 'wolf') {
|
||||
else if (sloped && form.type === 'wolf') {
|
||||
const minR = (eMin + 50) / half;
|
||||
const ratio = form.cutRatio > 0 ? form.cutRatio : form.cut ? (form.cut - low) / (high - low) : 0.6;
|
||||
const cut = Math.round(Math.max(low + 50, Math.min(high - 50, low + Math.max(0.05, minR, Math.min(0.95, ratio)) * (high - low))));
|
||||
@@ -401,5 +394,5 @@ window.UnitModel = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
return { WALL_SIDES, CORNER_MODES, ROOF_TYPES, RIDGES, LIMITS, clampSize, hasRidge, clone, createDefaultState, localToWorld, computeLayout, normalizeSplits, splitFacadeAt, topLineOn, topParamsOn, topParamsFromLayout, wallPieces, syncWallHeights, wallTopOf, gableOf, roofForm, applyRoofForm, formLine, validateState };
|
||||
return { WALL_SIDES, CORNER_MODES, ROOF_TYPES, RIDGES, LIMITS, clampSize, hasRidge, clone, createDefaultState, localToWorld, computeLayout, normalizeSplits, splitFacadeAt, topLineOn, topParamsOn, topParamsFromLayout, wallPieces, syncWallHeights, wallTopOf, gableOf, roofForm, dropMansarde, applyRoofForm, formLine, validateState };
|
||||
})();
|
||||
|
||||
+19
-12
@@ -269,6 +269,17 @@ window.WallModel = (() => {
|
||||
};
|
||||
}
|
||||
|
||||
function raiseTop(CONFIG, d) {
|
||||
return d ? Object.assign({}, CONFIG, {
|
||||
wallHeightLeft: CONFIG.wallHeightLeft + d, wallHeightRight: CONFIG.wallHeightRight + d,
|
||||
wallTop: CONFIG.wallTop.map(q => ({ x: q.x, h: q.h + d }))
|
||||
}) : CONFIG;
|
||||
}
|
||||
// Bovenrand van de buitenlagen: per punt op het dak (skinTop) of de wandrand + skinAbove.
|
||||
function skinConfig(CONFIG, skinAbove, skinTop) {
|
||||
return skinTop ? Object.assign({}, CONFIG, { wallHeightLeft: skinTop.hL, wallHeightRight: skinTop.hR, wallTop: skinTop.pts }) : raiseTop(CONFIG, skinAbove);
|
||||
}
|
||||
|
||||
function topPlateOutlines(tl) {
|
||||
const n = tl.segs.length, L = tl.pts[0], R = tl.pts[n];
|
||||
return tl.segs.map((sg, i) => [
|
||||
@@ -769,16 +780,10 @@ window.WallModel = (() => {
|
||||
const startXM = -halfW;
|
||||
const topYAt = tl.at;
|
||||
const skinAbove = opts.skinAbove > 0 ? opts.skinAbove : 0;
|
||||
const CONFIG_OUT = skinAbove ? Object.assign({}, CONFIG, {
|
||||
wallHeightLeft: CONFIG.wallHeightLeft + skinAbove, wallHeightRight: CONFIG.wallHeightRight + skinAbove,
|
||||
wallTop: CONFIG.wallTop.map(q => ({ x: q.x, h: q.h + skinAbove }))
|
||||
}) : CONFIG;
|
||||
const tlOut = skinAbove ? topLine(CONFIG_OUT, toM) : tl;
|
||||
const CONFIG_OUT = skinConfig(CONFIG, skinAbove, opts.skinTop);
|
||||
const tlOut = CONFIG_OUT === CONFIG ? tl : topLine(CONFIG_OUT, toM);
|
||||
const battenAbove = typeof opts.battenAbove === 'number' ? opts.battenAbove : skinAbove;
|
||||
const CONFIG_BAT = battenAbove ? Object.assign({}, CONFIG, {
|
||||
wallHeightLeft: CONFIG.wallHeightLeft + battenAbove, wallHeightRight: CONFIG.wallHeightRight + battenAbove,
|
||||
wallTop: CONFIG.wallTop.map(q => ({ x: q.x, h: q.h + battenAbove }))
|
||||
}) : CONFIG;
|
||||
const CONFIG_BAT = raiseTop(CONFIG_OUT, battenAbove - skinAbove);
|
||||
const underPts = tl.underPts;
|
||||
|
||||
// LET OP: xMin/xMax zijn wereld-coördinaten (-halfW..+halfW), niet lokaal (0..wallWidth).
|
||||
@@ -940,7 +945,9 @@ window.WallModel = (() => {
|
||||
uv.needsUpdate = true;
|
||||
return geo;
|
||||
};
|
||||
const topClampedOut = (x) => tlOut.at(Math.max(-halfW, Math.min(halfW, x)));
|
||||
// Voorbij het wandeinde (hoekdoorloop) de daklijn volgen: vlak doorgetrokken steekt hij boven een steil dakvlak uit.
|
||||
const beyondEnd = (T) => (x) => Math.min(T.at(x), T.at(Math.max(-halfW, Math.min(halfW, x))));
|
||||
const topClampedOut = beyondEnd(tlOut);
|
||||
const layerShapes = (a, b, y0, yCap) => {
|
||||
if (b - a <= edgeEps) return [];
|
||||
const capped = yCap === Infinity;
|
||||
@@ -1216,7 +1223,7 @@ window.WallModel = (() => {
|
||||
const rExt = extM(rabatDepthMM);
|
||||
const rxL = -halfW - rExt.left, rxR = halfW + rExt.right;
|
||||
const rY0 = Math.max(skinStartY(trasraam), skinFrom) * toM;
|
||||
const rTop = (x) => tlBat.at(Math.max(-halfW, Math.min(halfW, x)));
|
||||
const rTop = beyondEnd(tlBat);
|
||||
const rabatShapes = (rxR - rxL > edgeEps && tlBat.maxY > rY0 + edgeEps)
|
||||
? columnMinusRects(rxL, rxR, rY0, rTop, outerHoles.map(holeRect), { extraXs: tlBat.kinks.concat([-halfW, halfW]) }).map(piece => {
|
||||
const shape = new THREE.Shape(piece.outer.map(q => new THREE.Vector2(q.x, q.y)));
|
||||
@@ -1546,7 +1553,7 @@ window.WallModel = (() => {
|
||||
|
||||
return {
|
||||
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, RABAT, RABAT_COLORS, rabatHex, STUC, STUC_COLORS, isStucColor, stucHex, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, STONE, STONE_COLORS, TRASRAAM_STONES, isStoneFinish, stoneThumbUrl, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
|
||||
sheathingHoles, skinSegmentsAtY, topLine, wallTopPoints, topPlateOutlines, openingTopAt, openingTopMax, openingArea, openingSpanAtY,
|
||||
sheathingHoles, skinSegmentsAtY, topLine, skinConfig, wallTopPoints, topPlateOutlines, openingTopAt, openingTopMax, openingArea, openingSpanAtY,
|
||||
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
|
||||
createMaterials, build3D, computeSpecs
|
||||
};
|
||||
|
||||
+1
-1
@@ -1380,7 +1380,7 @@ function mountWallEditor(container, callbacks) {
|
||||
|
||||
const inputs = document.querySelectorAll('#tab-wall input');
|
||||
inputs.forEach(input => {
|
||||
// Hoogte met de hand gewijzigd: de knikken van een mansarde-/wolfsdak vervallen.
|
||||
// Hoogte met de hand gewijzigd: de knikken van een wolfsdak vervallen.
|
||||
if (/^inp-wall-(h-|peak)/.test(input.id)) input.addEventListener('input', () => { wallTopPts = null; });
|
||||
input.addEventListener('input', updateAll);
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"mode": "spots",
|
||||
"direction": "both",
|
||||
"lampHeight": 1950,
|
||||
"lampMin": 1500,
|
||||
"lampMin": 500,
|
||||
"lampMax": 3000,
|
||||
"minWallWidth": 2000,
|
||||
"endOffset": 300,
|
||||
|
||||
Reference in New Issue
Block a user