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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-11 00:34:27 +02:00
co-authored by Claude Opus 5.5
parent b5510d56c7
commit ceda0b554d
45 changed files with 4117 additions and 3700 deletions
+44 -93
View File
@@ -4,33 +4,13 @@
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
// ============================================================
// floor/floor-model.js — ENIGE bron van waarheid voor de vloer-logica:
// balkverdeling, doorvoer-resolutie (tussen de balken), chassis-wielen,
// specificaties en de 3D-opbouw. Geen DOM. Wordt gebruikt door:
// - floor/floor.js (de losse vloer-editor)
// - unit/unit.js (unit = vloer + 4 wanden)
// Wijzigingen aan de vloer-logica horen dus HIER.
//
// Werkt met een "vloer-state" (zelfde formaat als getStateJson/JSON-export):
// { floor: {width, depth, joistWidth, joistHeight, minSpacing, maxSpacing, underlayThick},
// chassis: {enabled, wheelDiameter, trackWidth, wheelbase},
// openings: [{id, diameter, anchorX, offsetX, offsetY}] }
//
// Plain script (geen ES-module), moet na core/geometry.js geladen worden.
// ============================================================
window.FloorModel = (() => {
const DENSITY_WOOD_KGM3 = 450; // zelfde referentiewaarde als WallModel
const DENSITY_OSB_KGM3 = 650; // OSB-plaat, zelfde referentiewaarde als voorheen "onderlaag"
const BAY_MARGIN_MM = 15; // vrije marge tussen doorvoer-rand en balkflank
const MIN_DIAMETER_MM = 20; // kleinste zinnige doorvoer; past dit niet, dan vervalt de doorvoer
const DENSITY_WOOD_KGM3 = 450;
const DENSITY_OSB_KGM3 = 650;
const BAY_MARGIN_MM = 15;
const MIN_DIAMETER_MM = 20;
// ----------------------------------------------------------
// Balkvakken: de ruimte tussen twee opeenvolgende balken (in wereld-
// coordinaten, zelfde stelsel als calcData.joists). Een doorvoer mag
// nooit een balk kruisen, dus wordt altijd binnen zo'n vak geklemd.
// ----------------------------------------------------------
function computeBays(joists, joistW) {
const bays = [];
for (let i = 0; i < joists.length - 1; i++) {
@@ -52,19 +32,9 @@ window.FloorModel = (() => {
return nearest;
}
// Resolutie: X via hetzelfde anker+offset-patroon als de wand-openingen,
// maar het resultaat wordt altijd teruggeklemd tot het balkvak waar het in
// valt (nooit een balk kruisend). Past de gevraagde diameter niet — zelfs
// niet na verkleinen tot het minimum — dan vervalt de doorvoer (blijft wel
// gewoon in de invoerlijst staan, verdwijnt alleen uit de tekening/telling,
// zodat een latere wijziging aan de balkafstand 'm weer terug kan brengen).
//
// Y is een simpele afstand vanaf de voorrand — balken lopen over de volle
// diepte door, dus Y heeft geen balk-interactie zoals X die wel heeft.
//
// Overlap tussen twee doorvoeren IN HETZELFDE vak is een apart, 2D-
// probleem (afstand tussen middelpunten) — dat wordt gedetecteerd en
// gemeld, niet automatisch opgelost. Bewuste, kleinere scope, geen bug.
// Past de diameter niet (ook niet verkleind tot MIN_DIAMETER_MM), dan vervalt de doorvoer alleen uit
// tekening en telling: hij blijft in de invoer, zodat een andere balkafstand hem terugbrengt.
// Overlap van twee doorvoeren in hetzelfde vak wordt gemeld, niet opgelost (bewust).
function resolveFloorOpenings(rawOpenings, width, depth, joists, joistW) {
const bays = computeBays(joists, joistW);
@@ -111,14 +81,6 @@ window.FloorModel = (() => {
return resolved;
}
// ----------------------------------------------------------
// Balkverdeling: zelfde algoritme (calculateSegmentPositions) als de wand.
// params = state.floor (of de ruwe invoerwaarden uit de editor).
// rims (optioneel): { front, back } — kopbalk langs de voor-/achterrand
// (haaks op de balken), bijv. op een naad tussen twee units waar de
// modules weer aan elkaar gemonteerd worden. De langsbalken worden daar
// met één balkbreedte ingekort. Langs de zijranden ligt altijd al een balk.
// ----------------------------------------------------------
function computeFrame(params, rims) {
const width = params.width || 2000;
const depth = params.depth || 2000;
@@ -140,18 +102,11 @@ window.FloorModel = (() => {
return { joists, width, depth, joistW, joistH, underlayThick, rims: r, joistLen };
}
// Bovenkant vloer (mm) = waar de wanden van een unit op staan.
function topHeight(calcData) {
return calcData.joistH + calcData.underlayThick;
}
// ----------------------------------------------------------
// Specificaties: balkhout, OSB-onderlaag, isolatie-leegte (zelfde soort
// "envelope minus houtwerk minus openingen"-logica als wall.js) en het
// gewichtstotaal. Isolatie krijgt — net als bij de wand — bewust geen
// eigen dichtheid/gewicht toegekend (materiaalkeuze is aan de gebruiker),
// alleen de resterende inhoud wordt gerapporteerd.
// ----------------------------------------------------------
// Isolatie krijgt bewust geen gewicht: de materiaalkeuze is aan de gebruiker.
function computeSpecs(calcData, resolvedOpenings) {
const toM = 0.001;
const { width, depth, joists, joistW, joistH, underlayThick } = calcData;
@@ -183,13 +138,6 @@ window.FloorModel = (() => {
return { joistWoodM3, joistWoodKg, osbAreaM2, osbVolM3, osbKg, voidM3, totalKg };
}
// ----------------------------------------------------------
// Chassis-laag: 4 wielen in een "dobbelsteen 4"-indeling, exact
// gecentreerd op de vloer (X: midden van de breedte, Y: midden van de
// diepte). Losse laag t.o.v. het regelwerk — wordt niet tegen balken
// geklemd zoals de doorvoeren, alleen gewaarschuwd als een wiel buiten de
// vloerrand zou uitsteken.
// ----------------------------------------------------------
function computeChassisWheels(chassis, width, depth) {
if (!chassis || !chassis.enabled) return { wheels: [], outOfBounds: false };
const r = chassis.wheelDiameter / 2;
@@ -204,8 +152,6 @@ window.FloorModel = (() => {
return { wheels, outOfBounds };
}
// Alles in één keer vanuit een vloer-state (handig voor de unit).
// rims: zie computeFrame.
function computeFromState(state, rims) {
const calcData = computeFrame(state.floor, rims);
const resolvedOpenings = resolveFloorOpenings(state.openings || [], calcData.width, calcData.depth, calcData.joists, calcData.joistW);
@@ -213,13 +159,36 @@ window.FloorModel = (() => {
return { calcData, resolvedOpenings, chassisData };
}
// Vloerafwerking (bovenop de OSB). Texturen in assets/textures/vloer/,
// naadloos herhalend; tileMM = maat van één tegel van de textuur.
const FINISHES = {
geen: { label: 'Geen (OSB)', color: '#cda86a' },
visgraat_001: { label: 'Visgraat (Hongaarse punt)', color: '#a28670', file: 'assets/textures/vloer/visgraat_001.jpg', tileMM: 3000 }
visgraat_001: { label: 'Visgraat (Hongaarse punt)', color: '#a28670', file: 'assets/textures/vloer/visgraat_001.jpg', tileMM: 3000 },
tegel_wit: { label: 'Tegel wit 300×300', color: '#ecebe6', tile: { w: 300, h: 300, joint: 3 }, jointColor: '#b9b6ad' },
tegel_grijs: { label: 'Tegel grijs 600×600', color: '#a9a8a3', tile: { w: 600, h: 600, joint: 3 }, jointColor: '#7d7c78' },
tegel_antraciet: { label: 'Tegel antraciet 600×600', color: '#4a4b4d', tile: { w: 600, h: 600, joint: 3 }, jointColor: '#2c2d2e' }
};
// Voorvertoning (assets/textures/vloer/thumb/<px>/<naam>.png) of null.
function tileTexture(tile, color, jointColor) {
if (typeof document === 'undefined') return null;
const k = 512 / Math.max(tile.w, tile.h);
const cv = document.createElement('canvas');
cv.width = Math.max(8, Math.round(tile.w * k));
cv.height = Math.max(8, Math.round(tile.h * k));
const c = cv.getContext('2d');
c.fillStyle = color;
c.fillRect(0, 0, cv.width, cv.height);
for (let i = 0; i < 400; i++) {
c.fillStyle = `rgba(${Math.random() < 0.5 ? '255,255,255' : '0,0,0'},0.025)`;
c.fillRect(Math.random() * cv.width, Math.random() * cv.height, 6 + Math.random() * 20, 6 + Math.random() * 20);
}
const j = Math.max(1, Math.round(tile.joint * k));
c.fillStyle = jointColor;
c.fillRect(0, 0, cv.width, j);
c.fillRect(0, 0, j, cv.height);
const tex = new THREE.CanvasTexture(cv);
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(1000 / tile.w, 1000 / tile.h);
tex.anisotropy = 8;
return tex;
}
function finishThumbUrl(key, px) {
const f = FINISHES[key];
return f && f.file ? f.file.replace(/[^/]+$/, (n) => `thumb/${px || 64}/` + n.replace(/\.jpg$/, '.png')) : null;
@@ -227,8 +196,13 @@ window.FloorModel = (() => {
const finishMaterials = {};
function finishMaterial(key) {
const f = FINISHES[key];
if (!f || !f.file) return null;
if (!f || !(f.file || f.tile)) return null;
if (finishMaterials[key]) return finishMaterials[key];
if (f.tile) {
const tex = tileTexture(f.tile, f.color, f.jointColor);
finishMaterials[key] = new THREE.MeshStandardMaterial(tex ? { map: tex, roughness: 0.45 } : { color: f.color, roughness: 0.45 });
return finishMaterials[key];
}
const mat = new THREE.MeshStandardMaterial({ color: 0xb89a7c, roughness: 0.7 });
if (typeof document !== 'undefined' && THREE.TextureLoader) {
new THREE.TextureLoader().load(f.file, (tex) => {
@@ -252,18 +226,7 @@ window.FloorModel = (() => {
};
}
// ----------------------------------------------------------
// 3D-opbouw (meters): X = breedte, Z = diepte (y=0 "voor" ligt op
// Z = -diepte/2), Y = dikte (balkhoogte + OSB). Balken en isolatie zijn
// simpele dozen; de OSB-onderlaag is een geëxtrudeerd Shape met ronde
// uitsparingen voor de doorvoeren.
// opts: { materials, osbVisible, finish, outerJoistsOnly }
// outerJoistsOnly: alleen de buitenste balken en kopbalken (de rest is
// ingesloten, bijv. dakbalken tussen plafondplaat en dakbedekking).
// finish (optioneel): { key (FINISHES), originX, originZ } — afwerking als
// dun vlak op de OSB; UV's in meters vanaf origin (m, woning-coördinaten
// van het vloermidden), zodat het patroon over units doorloopt.
// ----------------------------------------------------------
// finish.originX/Z: UV's in woning-meters, zodat het patroon over units doorloopt.
function build3D(calcData, resolvedOpenings, chassisData, opts) {
const { woodMaterial, osbMaterial, wheelMaterial } = opts.materials;
const osbVisible = opts.osbVisible !== false;
@@ -274,8 +237,6 @@ window.FloorModel = (() => {
const widthM = width * toM, depthM = depth * toM;
const joistWM = joistW * toM, joistHM = joistH * toM, underlayM = underlayThick * toM;
// A. VLOERBALKEN — dozen over de diepte (ingekort bij een kopbalk), plus
// de kopbalken (rims) over de volle breedte.
const rims = calcData.rims || {};
const joistLenM = (calcData.joistLen || depth) * toM;
const joistZ = ((rims.front ? joistW : 0) - (rims.back ? joistW : 0)) / 2 * toM;
@@ -294,13 +255,8 @@ window.FloorModel = (() => {
floorGroup3D.add(mesh);
});
// (Isolatie wordt niet getekend; de leegte-inhoud zit in computeSpecs.)
// Isolatie wordt niet getekend; de leegte-inhoud zit in computeSpecs.
// C. OSB-ONDERLAAG — één geëxtrudeerd vlak over de volle vloer, met
// ronde uitsparingen voor de doorvoeren. Shape wordt opgebouwd in
// (worldX, worldZ)-vlak en vervolgens 90° gekanteld zodat de dikte
// (extrusierichting) verticaal (wereld-Y) komt te liggen, boven op de
// balken.
const osbShape = new THREE.Shape();
osbShape.moveTo(-widthM / 2, -depthM / 2);
osbShape.lineTo(widthM / 2, -depthM / 2);
@@ -335,8 +291,6 @@ window.FloorModel = (() => {
osbEdges.visible = osbVisible;
floorGroup3D.add(osbEdges);
// D. CHASSIS-WIELEN — schematische cilinders onder de vloer, alleen als
// de chassis-laag aan staat (zelfde brondata als de 2D-tekening).
(chassisData ? chassisData.wheels : []).forEach(wheel => {
const rM = wheel.r * toM;
const thicknessM = 0.08;
@@ -348,8 +302,6 @@ window.FloorModel = (() => {
floorGroup3D.add(mesh);
});
// Vloerafwerking: vlak net boven de OSB, over de hele vloer (onder de
// wanden verdwijnt het), met de doorvoeren uitgespaard.
const fin = opts.finish && finishMaterial(opts.finish.key);
if (fin) {
const shape = new THREE.Shape();
@@ -377,7 +329,6 @@ window.FloorModel = (() => {
return floorGroup3D;
}
// Structuurcontrole van een vloer-state (bijv. bij JSON-import).
function validateState(design) {
if (!design.floor || typeof design.floor !== 'object') throw new Error('veld "floor" ontbreekt');
const requiredFloorFields = ['width', 'depth', 'joistWidth', 'joistHeight', 'minSpacing', 'maxSpacing', 'underlayThick'];
@@ -401,7 +352,7 @@ window.FloorModel = (() => {
}
return {
FINISHES, finishMaterial, finishThumbUrl, validateState, computeFrame, topHeight, resolveOpenings: resolveFloorOpenings, computeChassisWheels,
FINISHES, finishMaterial, finishThumbUrl, tileTexture, validateState, computeFrame, topHeight, resolveOpenings: resolveFloorOpenings, computeChassisWheels,
computeSpecs, computeFromState, createMaterials, build3D
};
})();
+4 -31
View File
@@ -4,25 +4,10 @@
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
// ============================================================
// floor/floor-pdf.js — ENIGE bron van het vloer-tekenblad (A3 liggend):
// frame- en plaatmateriaal, bovenaanzicht met balken/doorvoeren en
// maatketens, doorsnede en titelblok. Gebruikt door de losse vloer-editor en
// door de unit-PDF.
// Plain script, geladen na floor-model.js en core/pdf-sheet.js.
// ============================================================
window.FloorPdf = (() => {
// ------------------------------------------------------------
// Zelfde opzet als WallPdf (frame-materiaallijst, plaatmateriaallijst,
// titelblok, hoofdaanzicht met maatketens). Layout/kolommen zijn
// afgestemd op de bestaande "Vloer^V-x"-tekeningen (zie examples/) —
// die voorbeelden splitsen een grote vloer in meerdere modules; hier
// wordt bewust altijd maar één, doorlopende module getekend.
// ------------------------------------------------------------
// Bewust altijd één doorlopende module (de voorbeelden in examples/ splitsen grote vloeren).
// Tekent één volledig vloerblad op de HUIDIGE pagina van `doc`.
// opts: { project, titel, element, blad, roof }
function drawSheet(doc, calcData, resolvedOpenings, opts) {
const { width, depth, joists, joistW, joistH, underlayThick } = calcData;
@@ -31,8 +16,7 @@ window.FloorPdf = (() => {
const { RED, BLUE, BLACK, GREY } = PdfSheet.COLORS;
doc.setFont('helvetica', 'normal');
// --- Frame materiaal-tabel (linksboven) — één groep: alle vloerbalken
// hebben in dit model dezelfde doorsnede/lengte. ---
// Eén groep: alle vloerbalken hebben dezelfde doorsnede en lengte.
const tblX = [8, 26, 42, 58, 76, 92];
let ty = 14;
doc.setTextColor(...BLACK);
@@ -51,12 +35,11 @@ window.FloorPdf = (() => {
[String(1), String(joistW), String(joistH), String(Math.round(joistLen)), String(joists.length)]
.forEach((v, i) => doc.text(v, tblX[i], ty));
if (rimCount) {
ty += 4.3; // kopbalk(en) op een naad (FloorModel.computeFrame)
ty += 4.3;
[String(2), String(joistW), String(joistH), String(Math.round(width)), String(rimCount)]
.forEach((v, i) => doc.text(v, tblX[i], ty));
}
// --- Plaatmateriaal + isolatie ---
let my = 240;
doc.setFontSize(8.5);
doc.text('Plaat materiaal', tblX[0], my); my += 4.5;
@@ -73,10 +56,8 @@ window.FloorPdf = (() => {
my += 6;
doc.text('Isolatie: leegte tussen de balken (spouwvullend)', tblX[0], my);
// --- Titelblok (rechtsonder) ---
PdfSheet.titleBlock(doc, opts.project, { titel: opts.titel, element: opts.element, blad: opts.blad });
// --- Hoofdaanzicht (bovenaanzicht) ---
const drawX0 = 95, drawX1 = 410, drawY0 = 15, drawY1 = 178;
const marginSides = 25, marginTop = 15, marginBottom = 30;
const availW = (drawX1 - drawX0) - marginSides * 2;
@@ -86,7 +67,7 @@ window.FloorPdf = (() => {
const originX = drawX0 + marginSides;
const topY = drawY0 + marginTop;
const px = (xmm) => originX + xmm * scale;
const py = (ymm) => topY + ymm * scale; // y=0 (voor) boven, groeit naar beneden
const py = (ymm) => topY + ymm * scale;
doc.setDrawColor(...GREY);
doc.setLineWidth(0.25);
@@ -117,8 +98,6 @@ window.FloorPdf = (() => {
doc.text('#' + o.id, cx, cy - o.r * scale - 2, { align: 'center' });
});
// Extra laag van de aanroeper (bijv. elektra, ElektraPdf): overlay(doc, px, py),
// px/py = mm vanaf linksvoor van het dek -> blad-mm.
if (opts.overlay) opts.overlay(doc, px, py);
const drawDimLine = (x1, y1, x2, y2) => PdfSheet.dimLine(doc, x1, y1, x2, y2);
@@ -135,8 +114,6 @@ window.FloorPdf = (() => {
drawDimLine(dimLeftX, py(0), dimLeftX, py(depth));
doc.text(String(Math.round(depth)), dimLeftX - 2, (py(0) + py(depth)) / 2, { align: 'center', angle: 90 });
// H.o.h.-segmenten + cumulatieve maten onder de tekening — zelfde
// opbouw als de stijl-maatketens in wall.js, nu op de balkposities.
const joistEdges = new Set([0, width]);
joists.forEach(x => joistEdges.add(Math.round(x + width / 2 - joistW / 2)));
const cumPoints = [...joistEdges].filter(v => v >= 0 && v <= width).sort((a, b) => a - b);
@@ -155,8 +132,6 @@ window.FloorPdf = (() => {
doc.line(px(v), cumY - 2.5, px(v), py(depth) + 2);
});
// --- Doorsnede (balkprofiel) — kleine profielweergave onderaan,
// analoog aan de bestaande 2D-doorsnede-canvas. ---
const secY0 = Math.min(PAGE_H - 20, py(depth) + 30);
const secScale = Math.min((drawX1 - drawX0 - 40) / width, 18 / (joistH + underlayThick));
const secOriginX = drawX0 + 20;
@@ -164,8 +139,6 @@ window.FloorPdf = (() => {
const spx = (xmm) => secOriginX + xmm * secScale;
const spy = (ymm) => secFloorY - ymm * secScale;
// Dak (opts.roof): 180° gespiegeld — OSB onder (plafond), balken
// erboven, dakleer bovenop (RoofModel).
const joistY0 = opts.roof ? underlayThick : 0;
const osbY0 = opts.roof ? 0 : joistH;
doc.setDrawColor(...RED);
+4 -96
View File
@@ -4,28 +4,6 @@
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
// ============================================================
// floor/floor.js — Vloer-editor. Hergebruikt calculateSegmentPositions uit
// core/geometry.js (moet voor dit bestand geladen worden) voor exact
// dezelfde h.o.h.-balkverdeling als de wand, nu toegepast op vloerbalken.
// Vloeren krijgen UITSLUITEND cirkelvormige openingen (leidingdoorvoer,
// bijv. riool/water) — en die mogen alleen TUSSEN twee balken vallen, nooit
// een balk kruisen: een doorvoer wordt daarom automatisch verkleind tot een
// maat die wél binnen zijn balkvak past, en pas verwijderd (uit de
// weergave, niet uit de invoer) als zelfs een minimale diameter nergens past.
//
// Zelfde soorten logica als wall.js: een isolatielaag (leegte tussen de
// balken, analytisch bepaald — geen gewicht toegekend, precies zoals bij de
// wand), een 3D-perspectief (balken, OSB-onderlaag met doorvoer-gaten,
// isolatie, chassis-wielen) en een specificaties-paneel met gewichten.
// Onderlaag is standaard OSB-plaat van 22mm.
//
// De rekenlogica (balken, doorvoeren, chassis, specs, 3D-opbouw) staat in
// floor/floor-model.js (FloorModel) — gedeeld met de unit-editor.
//
// Plain script (geen ES-module), zelfde mount()/unmount()-patroon als
// wall.js.
// ============================================================
const FLOOR_TEMPLATE = `
<div id="floor-app">
@@ -198,9 +176,6 @@ const FLOOR_TEMPLATE = `
</div>
`;
// Dakmodus: een plat dak is een vloer die 180° gespiegeld is (OSB-plaat aan
// de onderzijde als plafond, balken erboven) met dakleer als buitenlaag
// (RoofModel). Zelfde editor, andere teksten + gespiegelde doorsnede/3D.
function floorTemplateForRoof() {
const r = [
[' Vloerbalken</h1>', ' Dakbalken</h1>'],
@@ -220,8 +195,6 @@ function mountFloorEditor(container, callbacks) {
const roofMode = !!(callbacks && callbacks.mode === 'roof');
container.innerHTML = roofMode ? floorTemplateForRoof() : FLOOR_TEMPLATE;
// Binnen een woning kan de maat vastliggen (dak volgt de vloer);
// callbacks.lockSize is dan de uitleg-tekst voor de tooltip.
if (callbacks && callbacks.lockSize) {
['f-inp-width', 'f-inp-depth'].forEach(id => {
const el = document.getElementById(id);
@@ -230,25 +203,12 @@ function mountFloorEditor(container, callbacks) {
});
}
// ----------------------------------------------------------
// State
// ----------------------------------------------------------
// Dichtheden, balkvak-marge en minimale doorvoer-diameter: zie FloorModel.
// Geen residuele demo-doorvoer meer — leeg beginnen, gebruiker voegt zelf toe.
let openings = [];
let openingIdCounter = 1;
// Chassis-laag: een losse, schematische onderconstructie (transportwielen)
// die NIET met de balkpositie hoeft te overlappen — in tegenstelling tot de
// doorvoeren hierboven wordt hier niets geklemd/verkleind, alleen buiten de
// vloerrand gewaarschuwd (zie computeChassisWheels).
// Chassis wordt niet geklemd (anders dan doorvoeren), alleen gewaarschuwd buiten de vloerrand.
let chassis = { enabled: false, wheelDiameter: 400, trackWidth: 1200, wheelbase: 1200 };
// Tab-navigatie (1. Vloer Opbouw / 2. Doorvoeren / 3. Chassis) — dezelfde
// generieke .tab-btn/.tab-content-wiring als wall.js. Ontbrak hier eerder
// (residu van vóór de tab-bar werd toegevoegd), waardoor de knoppen wel
// getekend maar niet klikbaar waren.
function switchTab(tabId) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(ct => ct.classList.remove('active'));
@@ -342,11 +302,7 @@ function mountFloorEditor(container, callbacks) {
}));
syncChassisToggleLabel();
// ------------------------------------------------------------
// Project eigenschappen — zelfde modal/velden als wall.js (bewust
// dezelfde id's: er is nooit meer dan één editor gelijktijdig gemount,
// dus hergebruik hiervan is veilig en voorkomt dubbele CSS/HTML).
// ------------------------------------------------------------
// Bewust dezelfde id's als wall.js: er is nooit meer dan één editor tegelijk gemount.
const PROJECT_PROP_IDS = ['inp-proj-naam', 'inp-proj-plaats', 'inp-proj-straat', 'inp-proj-nummer',
'inp-proj-tekenaar', 'inp-proj-titel', 'inp-proj-element', 'inp-proj-opmerking'];
let projectPropsSnapshot = null;
@@ -375,12 +331,6 @@ function mountFloorEditor(container, callbacks) {
}
});
// ------------------------------------------------------------
// JSON export/import — zelfde patroon als wall.js: sla het volledige
// vloer-ontwerp op (parameters + chassis + ruwe doorvoeren) als JSON, en
// laad het weer terug. Bij "Opslaan als" wordt ook automatisch de
// bijbehorende PDF-tekening gegenereerd (zie generateFloorPdf).
// ------------------------------------------------------------
function setIoStatus(msg, cls) {
const el = document.getElementById('f-io-status');
el.textContent = msg;
@@ -456,12 +406,6 @@ function mountFloorEditor(container, callbacks) {
e.target.value = '';
});
// Balkvakken + doorvoer-resolutie (nooit een balk kruisen): zie
// FloorModel.resolveOpenings (floor/floor-model.js).
// ----------------------------------------------------------
// Balkverdeling: FloorModel.computeFrame (zelfde algoritme als de wand).
// ----------------------------------------------------------
let calcData = { joists: [] };
function fmt(n, decimals = 1) {
@@ -483,8 +427,6 @@ function mountFloorEditor(container, callbacks) {
});
}
// Specificaties: FloorModel.computeSpecs (balkhout, OSB, isolatie-leegte,
// gewichtstotaal).
function updateSpecificaties(resolvedOpenings) {
const specs = roofMode ? RoofModel.computeSpecs(calcData, resolvedOpenings) : FloorModel.computeSpecs(calcData, resolvedOpenings);
if (roofMode) {
@@ -508,17 +450,12 @@ function mountFloorEditor(container, callbacks) {
return specs;
}
// Chassis-laag (4 wielen, "dobbelsteen 4"): FloorModel.computeChassisWheels.
function updateChassisWarning(outOfBounds) {
const warnBox = document.getElementById('f-chassis-warning');
warnBox.style.display = outOfBounds ? 'block' : 'none';
warnBox.textContent = 'Let op: één of meer wielen steken buiten de vloerrand — verklein de spoorbreedte/wielbasis of vergroot de vloer.';
}
// ----------------------------------------------------------
// Canvas: bovenaanzicht
// ----------------------------------------------------------
const canvasTop = document.getElementById('floor-canvas-top');
const ctxTop = canvasTop.getContext('2d');
@@ -570,7 +507,6 @@ function mountFloorEditor(container, callbacks) {
ctxTop.fill(); ctxTop.stroke();
});
// Maatlabels (zelfde stijl als het wand-vooraanzicht, core/canvas-labels.js)
const xAxisY = Math.max(14, toY(0) - 14);
drawCanvasDim(ctxTop, toX(-width / 2), xAxisY, toX(width / 2), xAxisY,
'X: ' + Math.round(width) + 'mm', cx, Math.max(10, xAxisY - 4));
@@ -579,10 +515,6 @@ function mountFloorEditor(container, callbacks) {
'Y: ' + Math.round(depth) + 'mm', Math.max(10, yAxisX - 4), (toY(0) + toY(depth)) / 2);
}
// ----------------------------------------------------------
// Canvas: doorsnede (balkprofiel — breedte x hoogte, aanvullende kijkhoek
// naast het bovenaanzicht, analoog aan de vooraanzicht-doorsnede bij wall.js)
// ----------------------------------------------------------
const canvasSection = document.getElementById('floor-canvas-section');
const ctxSection = canvasSection.getContext('2d');
@@ -594,7 +526,6 @@ function mountFloorEditor(container, callbacks) {
const w = canvasSection.width, h = canvasSection.height;
ctxSection.clearRect(0, 0, w, h);
// Dakmodus: 180° gespiegeld — OSB onder, balken erboven, dakleer bovenop.
const leerT = roofMode ? RoofModel.DAKLEER_MM : 0;
const totalH = joistH + underlayThick + leerT;
const joistY0 = roofMode ? underlayThick : 0;
@@ -610,9 +541,6 @@ function mountFloorEditor(container, callbacks) {
ctxSection.lineTo(toX(width / 2), toY(0));
ctxSection.stroke();
// Isolatie (leegte tussen de balken) — geschematiseerd als een
// doorlopende, licht transparante band ter hoogte van de balken, zodat
// ook in de doorsnede duidelijk is wát er tussen de balken zit.
ctxSection.fillStyle = 'rgba(255, 224, 140, 0.25)';
ctxSection.fillRect(toX(-width / 2), toY(joistY0 + joistH), width * scale, toY(joistY0) - toY(joistY0 + joistH));
@@ -622,7 +550,6 @@ function mountFloorEditor(container, callbacks) {
ctxSection.strokeRect(toX(x - joistW / 2), toY(joistY0 + joistH), joistW * scale, joistH * scale);
});
// OSB-plaat: bij een vloer boven op de balken, bij een dak eronder (plafond).
ctxSection.fillStyle = '#cda86a'; ctxSection.strokeStyle = '#8a6836';
ctxSection.fillRect(toX(-width / 2), toY(osbY0 + underlayThick), width * scale, toY(osbY0) - toY(osbY0 + underlayThick));
ctxSection.strokeRect(toX(-width / 2), toY(osbY0 + underlayThick), width * scale, toY(osbY0) - toY(osbY0 + underlayThick));
@@ -642,7 +569,6 @@ function mountFloorEditor(container, callbacks) {
ctxSection.fillText(underlayThick + 'mm OSB', cx, toY(joistH + underlayThick) - 6);
}
// Maatlabels (zelfde stijl als het wand-vooraanzicht)
const xAxisY = Math.min(h - 12, toY(0) + 8);
drawCanvasDim(ctxSection, toX(-width / 2), xAxisY, toX(width / 2), xAxisY,
'X: ' + Math.round(width) + 'mm', cx, Math.min(h - 2, xAxisY + 11));
@@ -651,9 +577,6 @@ function mountFloorEditor(container, callbacks) {
'Z: ' + Math.round(totalH) + 'mm', Math.min(w - 4, zAxisX + 12), (toY(0) + toY(totalH)) / 2);
}
// ----------------------------------------------------------
// Waarschuwingen naar de openingen-kaarten
// ----------------------------------------------------------
function updateWarnings(resolved) {
const anyWarn = resolved.some(o => o.overflowing);
const warnBox = document.getElementById('f-openings-global-warn');
@@ -672,14 +595,6 @@ function mountFloorEditor(container, callbacks) {
});
}
// ----------------------------------------------------------
// THREE.JS 3D RENDERER — zelfde opzet als wall.js (scene/camera/renderer/
// OrbitControls/licht/grid), maar plat gelegd: X = breedte, Z = diepte,
// Y = dikte (balkhoogte + OSB), in plaats van wall.js' X/Y/Z (breedte/
// hoogte/diepte). Balken en isolatie zijn simpele dozen; de OSB-onderlaag
// is — net als de platen bij wall.js — een geëxtrudeerd Shape met ronde
// uitsparingen voor de doorvoeren.
// ----------------------------------------------------------
const pane3D = document.getElementById('floor-pane-3d');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);
@@ -717,15 +632,12 @@ function mountFloorEditor(container, callbacks) {
let lastResolvedOpenings = [];
let lastChassisData = { wheels: [], outOfBounds: false };
// Weergave-filter (core/view-filter.js). Beplating = OSB-plaat (bij een
// dak: plafondplaat + dakleer).
createViewFilter(document.getElementById('floor-toolbar-3d'), [
{ key: 'beplating', label: roofMode ? 'Beplating (plafond + dakleer)' : 'Beplating (OSB-plaat)' }
], (v) => {
osbVisible = v.beplating;
update3DScene();
});
// Omgeving (core/skybox.js: geen/dag/nacht); de vloer staat op de tegelvloer.
let skyMode = Skybox.getMode();
const skyEnv = Skybox.createEnvironment(scene);
const applySky = () => {
@@ -740,13 +652,9 @@ function mountFloorEditor(container, callbacks) {
? RoofModel.build3D(calcData, lastResolvedOpenings, { materials: floorMaterials, osbVisible, dakleerVisible: osbVisible })
: FloorModel.build3D(calcData, lastResolvedOpenings, lastChassisData, { materials: floorMaterials, osbVisible });
scene.add(floorGroup3D);
applySky(); // schaduw van de nieuwe vloer
applySky();
}
// ------------------------------------------------------------
// PDF-export: het tekenblad zelf komt uit FloorPdf (floor/floor-pdf.js),
// gedeeld met de unit-PDF.
// ------------------------------------------------------------
function generateFloorPdf() {
recalculate();
const resolvedOpenings = FloorModel.resolveOpenings(openings, calcData.width, calcData.depth, calcData.joists, calcData.joistW);
@@ -784,7 +692,7 @@ function mountFloorEditor(container, callbacks) {
window.addEventListener('resize', onWindowResize);
function toggleFullscreen3D() {
UI.cycle3D(document.getElementById('floor-app')); // T: standaard -> 3D groot -> alleen render
UI.cycle3D(document.getElementById('floor-app'));
requestAnimationFrame(onWindowResize);
}
function toggleFullscreen2D() {