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
+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() {