Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94a5215f82 | ||
|
|
373c2c2fb7 | ||
|
|
001206a108 | ||
|
|
9126588df7 | ||
|
|
9bd7faeaa6 | ||
|
|
f163f4deb1 | ||
|
|
448c2b1289 | ||
|
|
5ecf3e0c2a | ||
|
|
7ae76fe390 | ||
|
|
9ed9cee2e0 | ||
|
|
f8da490640 | ||
|
|
3a4b9608c0 | ||
|
|
3591fad220 |
@@ -1,237 +1,99 @@
|
|||||||
# Tectum — configurator Van Emous Mantelzorgwoningen (intern)
|
# Tectum — configurator Van Emous Mantelzorgwoningen (intern)
|
||||||
|
|
||||||
Losse, meerdere-bestanden-opzet van de eerdere single-file wand-tool, nu met
|
Configurator voor mantelzorgwoningen: wand, vloer en woning (gekoppelde units)
|
||||||
een menubalk om tussen objecttypen (muur/vloer) te schakelen.
|
met 2D, 3D, specificaties en PDF-tekenbladen.
|
||||||
|
|
||||||
Copyright © 2026 Machiel Heinen. Alle rechten
|
Copyright © 2026 Machiel Heinen. Alle rechten voorbehouden; gebruik
|
||||||
voorbehouden; gebruik uitsluitend met schriftelijke toestemming. Zie
|
uitsluitend met schriftelijke toestemming. Zie [LICENTIE.md](LICENTIE.md).
|
||||||
[LICENTIE.md](LICENTIE.md). Elk bronbestand begint met deze copyright-kop.
|
Elk bronbestand begint met deze copyright-kop.
|
||||||
|
|
||||||
## Draaien
|
## Draaien
|
||||||
|
|
||||||
**Optie 1 — direct openen (geen server nodig):**
|
|
||||||
Dubbelklik `index.html`. Alles werkt met gewone `<script src="...">`-tags
|
|
||||||
(geen ES-modules), dus dit werkt ook via `file://`.
|
|
||||||
|
|
||||||
**Optie 2 — lokale server (optioneel):**
|
|
||||||
```
|
```
|
||||||
cd project
|
./start.sh # of: python3 -m http.server 8000
|
||||||
python3 -m http.server 8000
|
|
||||||
```
|
```
|
||||||
en open `http://localhost:8000`.
|
Open `http://localhost:8000`. Elke statische webserver volstaat; geen
|
||||||
|
build-stap. Direct openen via `file://` werkt niet (texturen/lucht).
|
||||||
|
|
||||||
## Structuur
|
## Structuur
|
||||||
|
|
||||||
```
|
```
|
||||||
index.html app-shell: menubalk, splashscreen, over-deze-software
|
index.html app-shell: menubalk, splashscreen
|
||||||
css/
|
css/app.css alle stijl
|
||||||
app.css alle stijl, in één bestand
|
js/vendor/ three.js r128, eigen OrbitControls, jsPDF
|
||||||
js/
|
js/core/ gedeelde helpers: geometrie, UI-kit, PDF-bladen, labels,
|
||||||
vendor/
|
camerageheugen, weergavefilter, skybox, licentie, logo
|
||||||
three.min.js three.js r128, lokaal gevendord (geen CDN meer)
|
js/wall/ WallModel (logica) + wand-PDF + wand-editor
|
||||||
OrbitControls.js eigen, compacte OrbitControls-vervanging (zie
|
js/floor/ FloorModel (logica) + vloer-PDF + vloer-editor
|
||||||
"Bewuste keuzes" hieronder) — leunt op three.min.js
|
js/roof/ RoofModel: dak (plat/lessenaar/zadel) + dakrand (boeidelen/daktrim)
|
||||||
core/
|
js/lighting/ LightingModel: gevellampen, boeideel-spots, binnenspots
|
||||||
geometry.js gedeelde, pure functies (regelwerk-verdeling,
|
js/sandwich/ SandwichModel: sandwichpanelen rondom
|
||||||
polygon-oppervlaktes, clip-functies) — gebruikt
|
js/unit/ UnitModel: 1 vloer + 4 wanden, naden, wandstukken
|
||||||
door zowel wall.js als floor.js, niet gedupliceerd
|
js/house/ HouseModel (woning) + woning-PDF + woning-editor
|
||||||
canvas-labels.js gedeelde stijl voor maatlabels ("X: 6000mm") in de
|
js/app.js menubalk, versienummer, actieve editor
|
||||||
2D-canvassen van wand, vloer en unit
|
json/defaults/ referentie-JSON's (niet runtime ingeladen)
|
||||||
pdf-sheet.js gedeelde PDF-bouwstenen: titelblok, maatlijn, tabel
|
assets/ logo, texturen, skybox
|
||||||
camera-memory.js onthoudt de 3D-camerastand per tabblad + onderdeel
|
|
||||||
view-filter.js "Weergave"-dropdown met vinkjes voor de 3D-weergaven
|
|
||||||
license.js licentietekst voor "Over deze software" (kopie van
|
|
||||||
LICENTIE.md, dat bestand is leidend)
|
|
||||||
logo-data.js logo als PNG-data voor de PDF-titelblokken
|
|
||||||
(opnieuw genereren uit assets/logo.svg, zie kop)
|
|
||||||
wall/
|
|
||||||
wall-model.js WallModel: ENIGE bron van de wand-logica (anker-
|
|
||||||
resolutie openingen, regelwerk, 3D-opbouw, specs,
|
|
||||||
validatie) — geen DOM
|
|
||||||
wall-pdf.js WallPdf: het wand-tekenblad (aanzicht, boven- en
|
|
||||||
zijaanzicht, materiaallijst) — losse wand én unit
|
|
||||||
wall.js wand-editor (UI: invoer, 2D, interactie, JSON/PDF)
|
|
||||||
floor/
|
|
||||||
floor-model.js FloorModel: ENIGE bron van de vloer-logica (balken,
|
|
||||||
doorvoeren, chassis, 3D-opbouw, specs, validatie)
|
|
||||||
floor-pdf.js FloorPdf: het vloer-tekenblad — losse vloer én unit
|
|
||||||
floor.js vloer-editor (UI, uitsluitend cirkelvormige
|
|
||||||
openingen/doorvoeren)
|
|
||||||
roof/
|
|
||||||
roof-model.js RoofModel: plat dak = vloer (FloorModel) 180°
|
|
||||||
gespiegeld + dakleer; dakrand (boeidelen met spots
|
|
||||||
of daktrim) als één doorlopende rand om de woning
|
|
||||||
lighting/
|
|
||||||
lighting-model.js LightingModel: buitenverlichting per unit (geen /
|
|
||||||
boeideel-spots / gevellampen), gevellampen per
|
|
||||||
wandstuk met terugval naar spots, override per wand
|
|
||||||
unit/
|
|
||||||
unit-model.js UnitModel: 1 vloer + 4 wanden, hoekaansluiting,
|
|
||||||
naden (wand vervalt deels/geheel) en wandstukken
|
|
||||||
house/
|
|
||||||
house-model.js HouseModel: woning = gekoppelde units + binnenwanden;
|
|
||||||
posities (koppelen aan zijde + uitlijning), naden,
|
|
||||||
totalen
|
|
||||||
house-pdf.js HousePdf: samenhangende PDF (plattegrond, per unit
|
|
||||||
vloer + wandstukken, binnenwanden, totalen)
|
|
||||||
house.js woning-editor (UI): navigatieboom, plattegrond met
|
|
||||||
selecteren/binnenwand tekenen, eigenschappen, 3D;
|
|
||||||
mount de echte wand-/vloer-editor per onderdeel
|
|
||||||
app.js menubalk-logica, versienummer, mount/unmount van
|
|
||||||
de actieve editor
|
|
||||||
json/
|
|
||||||
defaults/ referentie-JSON's die tonen wat "Nieuw" oplevert
|
|
||||||
(worden NIET ingeladen via fetch(), zie hieronder)
|
|
||||||
premade/ bestemmingsmap voor opgeslagen/geëxporteerde designs
|
|
||||||
assets/ logo (preloader) + bestemmingsmap voor toekomstige
|
|
||||||
GLB's/texturen
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Bewuste keuzes / bekende beperkingen
|
## Uitgangspunten
|
||||||
|
|
||||||
- **Volledig CDN-vrij.** three.js (r128) staat lokaal in `js/vendor/three.min.js`.
|
- **Eén bron per logica.** Rekenwerk en 3D-opbouw zitten alleen in de
|
||||||
OrbitControls is een eigen, compacte implementatie (`js/vendor/OrbitControls.js`)
|
`*Model`/`*Pdf`-modules; editors en woning hergebruiken die.
|
||||||
— het originele addon-bestand was niet beschikbaar om te vendoren, dus is de
|
- **Geen ES-modules, geen CDN.** Gewone `<script>`-tags in vaste volgorde
|
||||||
gebruikte subset (roteren via slepen, zoomen via scrollwiel, pannen via
|
(zie `index.html`), alle libraries lokaal.
|
||||||
rechts-slepen, `target`/`update()`) opnieuw geschreven en losstaand getest
|
- **Assen/zijden.** Bovenaanzicht boven = noord: voor = noord, achter = zuid,
|
||||||
(rotatie/zoom-richting/grenzen/pannen/dispose). Functioneel gelijkwaardig
|
links = west, rechts = oost (interne sleutels blijven voor/achter/…).
|
||||||
voor wat deze tool nodig heeft, maar geen 1-op-1 kopie van alle opties van
|
- **Hoogtes** gelden vanaf bovenkant vloer (loopvlak).
|
||||||
de officiële addon (bijv. geen damping/inertia).
|
|
||||||
- **Geen ES-modules.** Alle JS-bestanden zijn gewone `<script>`-tags, geladen
|
## Functionaliteit
|
||||||
in een vaste volgorde (`geometry.js` → `canvas-labels.js` → jsPDF →
|
|
||||||
`pdf-sheet.js` → `wall-model.js` → `wall-pdf.js` → `floor-model.js` →
|
- **Wand:** openingen (raam/deur/voordeur) met ankers, ook boven elkaar
|
||||||
`floor-pdf.js` → `roof-model.js` → `lighting-model.js` → `wall.js` → `floor.js` → `unit-model.js` → `house-model.js`
|
(raam boven een deur; stijlen ertussen van dorpel tot dorpel), regelwerk
|
||||||
→ `house-pdf.js` → `house.js` → `app.js`).
|
38x235 met dubbele hoekstijlen en een dubbele stijl naast deuren, zaagplan
|
||||||
Reden: ES-module-imports (`import ... from`) breken bij rechtstreeks openen
|
op handelslengtes, spaanplaat binnen, vochtdoek (met kruislat en trasraam)
|
||||||
via `file://` door browser-CORS-beleid. Dit was expliciet een vereiste
|
of cementplaat buiten.
|
||||||
("gewoon via een index file"), dus is hier bewust voor gekozen.
|
- **Wandhoogte:** schuine bovenrand of middenpunt (nok) met goothoogtes. In
|
||||||
- **`json/defaults/*.json` worden niet gefetcht.** Om dezelfde reden (fetch()
|
een unit bepaalt de laatst gewijzigde wand de dakvorm: de tegenoverliggende
|
||||||
van lokale bestanden werkt ook niet betrouwbaar via `file://`): de
|
wand spiegelt mee, de haakse wanden worden vlak op de hoekhoogtes.
|
||||||
standaardwaarden van "Nieuw" staan hardcoded in `wall.js`/`floor.js` zelf,
|
- **Vloer:** balken, cirkelvormige doorvoeren, OSB, optioneel chassis.
|
||||||
en komen overeen met deze JSON-bestanden. De JSON's zijn dus referentie/
|
- **Woning:** units koppelen aan een zijde (uitlijning + verschuiving); op
|
||||||
documentatie, en (voor wanden) een bestand dat je via "Importeer JSON" kan
|
naden vervallen de buitenwanden en komt een naadwand met deur: midden op
|
||||||
inladen — geen runtime-configuratiebron.
|
de naad in twee delen (één per unit), of als één binnenwand geheel in unit
|
||||||
- **Alleen wall.js heeft Exporteer/Importeer JSON.** floor.js nog niet.
|
A of B. De hoogte volgt per deel de daklijn van zijn unit. Staat een unit
|
||||||
- **Floor.js heeft nu dezelfde soorten logica als wall.js.** 2D-bovenaanzicht
|
met een hogere daklijn tegen een lager dak, dan krijgt zijn buitenwand aan
|
||||||
+ doorsnede, regelwerk (vloerbalken), cirkelvormige doorvoeren, een
|
de naadzijde een opbouw vanaf de bovenkant van het lage dak (ramen daar
|
||||||
onderlaag van OSB-plaat (standaard 22mm, dikte + oppervlak + volume +
|
teken je op die buitenwand) en stopt de naadwand op die hoogte. Loopt het
|
||||||
gewicht), een isolatielaag (leegte tussen de balken, analytisch bepaald —
|
lage dak langs de naad schuin, dan loopt de naadwand door en krijgt het
|
||||||
net als bij de wand krijgt isolatie zelf bewust geen dichtheid/gewicht),
|
uitstekende stuk buitenafwerking (vochtdoek + latwerk).
|
||||||
een volwaardig 3D-perspectief (balken/isolatie/OSB met doorvoer-gaten/
|
Binnenwanden teken je in de plattegrond.
|
||||||
chassis-wielen, met aan/uit-toggles voor OSB en isolatie, plus fullscreen-
|
- **Dak:** per unit opgebouwd als gespiegelde vloer + dakleer, in de vorm
|
||||||
2D/3D) en een specificaties-paneel met een gewichtstotaal. Losse,
|
die de wanden aangeven: plat, lessenaar (schuine kopgevels) of zadel
|
||||||
optionele chassis-laag (4 transportwielen in een "dobbelsteen 4"-indeling,
|
(nok). Elk dakvlak is een eigen dek met de balken als sporen en een eigen
|
||||||
exact gecentreerd op de vloer — spoorbreedte/wielbasis/wieldiameter
|
PDF-blad; doorvoeren gaan naar het vlak waarin ze vallen. De dakrand
|
||||||
instelbaar, geen interactie met de balkposities).
|
(boeidelen/daktrim, spots) loopt als één rand door en volgt de helling.
|
||||||
- **Single point of truth wand/vloer/unit.** Alle reken- en 3D-logica zit in
|
Is een dak langs de naad minstens een boeideelhoogte hoger dan het
|
||||||
`WallModel`/`FloorModel`. De losse editors én de unit roepen die aan, dus
|
aangrenzende, dan krijgt het een eigen rand rondom (boven het lage dak
|
||||||
een wijziging in de wand- of vloerlogica geldt automatisch overal. De unit
|
zonder spots). De rand van het lage dak loopt dan door tot de gevel van
|
||||||
bewaart per onderdeel een gewone wand-/vloer-state; wandbreedtes worden
|
het hoge, of langs een zijgevel tot de volle overstek (onder de hoge
|
||||||
afgeleid uit vloermaat + wanddiktes + hoekaansluiting, openingen houden
|
rand). Boeideel en daktrim steken 80 mm boven het dakleer uit.
|
||||||
hun anker (links/midden/rechts). "Opslaan als" bij een unit geeft JSON +
|
- **Gevel:** latwerk en vochtdoek lopen per gevel door over unit-naden;
|
||||||
één samenhangende PDF: bovenaanzicht unit (U-1), vloer (V-1), elke wand
|
sandwichpanelen rondom, 600 mm de grond in.
|
||||||
(W-1…W-4, via WallPdf/FloorPdf) en een totalenblad (balken, zaaglijst,
|
- **Verlichting:** boeideel-spots of gevellampen (per gevel verdeeld,
|
||||||
glaswol, plaatwerk, gewichten). Projectgegevens van de unit staan in
|
override per wand) en binnenspots in een raster per unit.
|
||||||
`state.project` en gelden voor alle bladen.
|
- **3D:** weergavefilters, omgeving Ontwerp/Dag/Nacht, Render met lampen als
|
||||||
- **Standaarden (WallModel).** Muurbalk 38 x 235 mm (stijlen én regels),
|
echte lichtbronnen.
|
||||||
ingekocht in 3600/4200/4800/5400 mm; een zaagplan (`planStockCutting` in
|
- **Uitvoer:** JSON opslaan/openen en een samenhangende PDF (plattegrond,
|
||||||
geometry.js) kiest per staaf de lengte met het minste snijverlies — bij een
|
vloeren, wandstukken, binnenwanden, totalen).
|
||||||
unit over alle wanden samen. Stukken langer dan 5400 worden opgeknipt.
|
|
||||||
Elke wandkop heeft een dubbele hoekstijl (2 x 38 mm). De app start met een
|
## Bekende beperkingen
|
||||||
unit.
|
|
||||||
- **Woning (v0.2).** Bestand → Nieuw → Woning (1 unit) of Woning L-vorm. Een
|
- Units kunnen niet gedraaid worden.
|
||||||
unit koppel je aan een zijde van een andere unit (uitlijning start/midden/
|
- Getekende binnenwanden volgen de dakvorm (nog) niet.
|
||||||
eind + verschuiving). Op de naad vervalt de wand van beide units over het
|
- De dak-editor toont het dek in de plattegrond (niet per schuin dakvlak).
|
||||||
gedeelde stuk; een wand kan daardoor in meerdere stukken uiteenvallen
|
- Geen schaduw per lamp in de render (browserlimiet).
|
||||||
(bijv. A-W2a/b), haakse wanden lopen dan door tot de vloerrand. Binnen-
|
- Eigen OrbitControls: roteren/zoomen/pannen, geen damping.
|
||||||
wanden teken je in de plattegrond (standaard 100 mm, spaanplaat beide
|
|
||||||
zijden) en bewerk je met de gewone wand-editor. Een wandstuk dat aan een
|
## Versie
|
||||||
naad eindigt loopt de naad in door met de dikte van de haakse buurwand,
|
|
||||||
zodat binnenhoeken (L-vorm) dicht zijn. Zijden heten naar windrichting
|
`APP_VERSION` in `js/app.js`: laatste cijfer per iteratie, middelste per
|
||||||
t.o.v. het bovenaanzicht (boven = noord): voor = noord, achter = zuid,
|
mijlpaal (met git-tag), eerste bij live-gang. De changelog staat in git.
|
||||||
links = west, rechts = oost (intern blijven de oude sleutels). Oude unit-JSON's kunnen in
|
|
||||||
een woning worden geïmporteerd. Units kunnen (nog) niet gedraaid worden.
|
|
||||||
- **Mijlpaal v0.4 — gevelopbouw.** Samenvatting van de v0.3.11–0.3.19-iteraties:
|
|
||||||
kruislat-latwerk op vochtdoek (richting gevelbekleding globaal), lampen op de
|
|
||||||
afgewerkte gevel (+52 mm), logo-watermerk op het doek, trasraam (cement-
|
|
||||||
gebonden plaat, 350 mm vanaf het loopvlak), sandwichpanelen rondom (600 mm de
|
|
||||||
grond in), hoekdoorloop van de buitenlagen over de hoekstijlen, en latwerk/
|
|
||||||
doek per doorlopende gevel over unit-naden heen. Details per onderdeel hieronder.
|
|
||||||
- **Plat dak (v0.3).** Elke unit heeft een dak (tweede laag) dat standaard
|
|
||||||
de vloermaat volgt, maar een eigen maat/verschuiving mag krijgen. Opbouw =
|
|
||||||
vloer 180° gespiegeld (OSB als plafond onder, balken, dakleer bovenop); het
|
|
||||||
dak wordt bewerkt met de vloer-editor in dakmodus. De dakrand is woning-
|
|
||||||
breed: boeidelen (overstek 200–400 mm vanaf de dakrand = standaard de
|
|
||||||
gevel, 300 mm hoog, spots op 40% van de overstek, op elke (unit-)hoek en
|
|
||||||
daartussen verdeeld met min/max h.o.h.) of een daktrim van 80 mm. De rand
|
|
||||||
loopt als één geheel over de naden tussen units. Het dak ligt op de hoogste
|
|
||||||
wandbovenkant van de unit; bij units met verschillende hoogtes ligt de
|
|
||||||
dakrand op het hoogste dak.
|
|
||||||
- **Dakrand & buitenverlichting (v0.3.3).** Boeidelen aan/uit per unit
|
|
||||||
(anders daktrim); de rand wordt per ribbe opgebouwd (in verstek, geen
|
|
||||||
zichtbare naad). Verlichting is woning-breed: type (geen / boeideel-spots /
|
|
||||||
gevellampen), lichtrichting en alle afstanden. Gevellampen worden per
|
|
||||||
GEVEL verdeeld (aansluitende wandstukken in hetzelfde vlak, ook over een
|
|
||||||
naad tussen units), niet per wand: op lamphoogte (standaard 1950) alleen op
|
|
||||||
vrije stukken >= 2000 mm (openingen incl. raamwerk eraf), eerste/laatste
|
|
||||||
300 mm van hoek/opening, daartussen min/max h.o.h.; smallere stukken krijgen
|
|
||||||
geen lampen. Gevellampen en boeideel-spots sluiten elkaar uit. De min.
|
|
||||||
h.o.h. geldt over alle lampen van een gevel samen (ook bij afstand hoek/
|
|
||||||
opening en overrides): te dicht bij elkaar = samengevoegd tot één lamp in
|
|
||||||
het midden. Per wandzijde blijft een override: automatisch / 500 mm
|
|
||||||
naast elke opening / geen. Standaard unithoogte 2600 mm.
|
|
||||||
- **3D-weergave (v0.3.5).** Filters als dropdown met vinkjes. Woning:
|
|
||||||
Beplating (wandplaten, vloer- en plafondplaten, dakleer, boeideel/daktrim),
|
|
||||||
Isolatie, Dak, Lampen. "Volle render": avondscène met maaiveld, elke
|
|
||||||
gevellamp (per lichtrichting) en boeideel-spot is een echte SpotLight
|
|
||||||
(zonder schaduwen, om het licht te houden); de nep-lichtkegels zijn dan
|
|
||||||
verborgen.
|
|
||||||
- **Latwerk gevelbekleding (v0.3.11).** Bij vochtdoek komt er een kruislat
|
|
||||||
op (WallModel.computeBattens): latten 20x30 mm, handelslengte 4000, 400 mm
|
|
||||||
h.o.h. in beide richtingen, uitgespaard rond openingen en afgekapt op de
|
|
||||||
(schuine) bovenrand. De bovenste laag staat haaks op de gevelbekleding
|
|
||||||
(WEO/Novalit, 12 mm; richting per losse wand, in een woning voorlopig
|
|
||||||
globaal als woning-instelling). Gevellampen hangen op de afgewerkte gevel:
|
|
||||||
wandvlak + kruislat (40) + bekleding (12) = 52 mm naar buiten. Cement-gebonden
|
|
||||||
platen (steenstrips) krijgen geen latwerk. Latten tellen mee in gewicht,
|
|
||||||
specificaties, wand-PDF en woningtotalen; zichtbaar met "Beplating".
|
|
||||||
- **Vochtdoek-bedrukking (v0.3.13).** Net als op de echte doeken staat het logo
|
|
||||||
als medium-grijs watermerk (geen kleur) onder 45° op het vochtdoek, 1,5 m
|
|
||||||
breed, herhaald per tegel van 2 m (WallModel.createMaterials, CanvasTexture).
|
|
||||||
- **Trasraam (v0.3.14).** Woning-instelling (aan/uit + hoogte, standaard 350 mm
|
|
||||||
vanaf Y=0 van de vloer = het beloopbare vlak / bovenkant vloer). Onderaan
|
|
||||||
elke vochtdoek-gevel komt een strook cement-gebonden plaat (montage-
|
|
||||||
achterkant), vóór het doek; het doek loopt er ongewijzigd achter door en
|
|
||||||
het latwerk begint erboven. Zelfde plaat- en uitsparingslogica als de
|
|
||||||
buitenplaten, dus openingen blijven gerespecteerd (WallModel.trasraamFor,
|
|
||||||
build3D opts.trasraam, computeBattens). Telt mee in gewicht, wand-PDF en de
|
|
||||||
woningtotalen ("Cementplaat (trasraam)").
|
|
||||||
- **Hoekdoorloop buitenlagen (v0.3.18).** Een wand die tussen twee doorlopende
|
|
||||||
wanden past, bedekt met zijn buitenlagen ook de kopse kant (hoekstijlen) van
|
|
||||||
die wanden: UnitModel geeft per wand(stuk) skinExt { left, right } (= dikte
|
|
||||||
van de doorlopende wand, 0 bij naden/vrije einden), WallModel.cornerExt telt
|
|
||||||
per laag de eigen diepte erbij. Latwerk en trasraam lopen door; vochtdoek
|
|
||||||
en cementplaat (liggen in de beplatingsruimte) krijgen een strook net vóór
|
|
||||||
het wandvlak.
|
|
||||||
- **Latwerk en doek per gevel (v0.3.19).** HouseModel.assignFacades groepeert
|
|
||||||
collineaire, aansluitende buitenwandstukken over alle units tot gevels
|
|
||||||
(groupFacadeRuns in core/geometry.js, ook gebruikt door de buitenverlichting)
|
|
||||||
en geeft elk stuk p.facade { u0, extStart, isEnd }. Het verticale
|
|
||||||
latten-raster (400 h.o.h.) en het watermerk op het doek lopen daardoor over
|
|
||||||
unit-naden door; een eindlat komt alleen aan het einde van de gevel.
|
|
||||||
- **Sandwichpanelen (v0.3.16).** Woning-instelling aan/uit. SandwichModel
|
|
||||||
(js/sandwich/sandwich-model.js) legt één doorlopende rand (in verstek) om de
|
|
||||||
vereniging van alle unit-vloeren: 40 mm dik, van het loopvlak (bovenkant
|
|
||||||
vloer) 600 mm naar beneden, de grond in. Plaat 495 x 1000 x 40 (9 kg),
|
|
||||||
verticaal: één plaat per 495 mm gevel, 1000 afgezaagd op 600. Naden per
|
|
||||||
495 mm; zichtbaar met "Beplating"; m2, aantal platen en gewicht (hele
|
|
||||||
platen) in de woningtotalen.
|
|
||||||
- **Openingtypes.** Raam (500x1900), deur (900x2150) en voordeur (1150x2150);
|
|
||||||
type kiezen zet de standaardmaat, daarna vrij aan te passen. De app start
|
|
||||||
met een L-woning met een voordeur (noord, unit A) en 3 ramen (zuid, unit B).
|
|
||||||
- **Woning-instellingen.** Regelwerk-h.o.h. (min/max) geldt globaal voor alle
|
|
||||||
buitenwanden resp. alle binnenwanden; in de wand-editor zijn die velden
|
|
||||||
binnen een woning vergrendeld.
|
|
||||||
- **Versienummer** staat als enkele constante (`APP_VERSION`) in `app.js`.
|
|
||||||
Conventie: laatste cijfer per kleine iteratie, middelste per mijlpaal
|
|
||||||
(aangegeven in chat), eerste bij live-gang.
|
|
||||||
|
|||||||
+5
-2
@@ -1,2 +1,5 @@
|
|||||||
Bestemmingsmap voor toekomstige assets: GLB-modellen (ramen/deuren),
|
Assets voor de 3D-weergave en de PDF:
|
||||||
texturen/bumpmaps, e.d. Nog leeg.
|
- logo.svg / logo-animated.svg logo (splash, about); PDF-logo staat als data in js/core/logo-data.js
|
||||||
|
- skybox.webp equirectangulair panorama dag (4096x2048), "Lucht: Dag" (core/skybox.js)
|
||||||
|
- skybox-night.webp equirectangulair panorama nacht (4096x2048), "Lucht: Nacht"
|
||||||
|
- tiles.jpg tegeltextuur (1024x1024) voor de ronde vloer onder het object
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 508 KiB |
+97
@@ -544,3 +544,100 @@ html, body { height: 100%; }
|
|||||||
.vf-menu.open { display: block; }
|
.vf-menu.open { display: block; }
|
||||||
.vf-item { display: flex; align-items: center; gap: 8px; color: #eee; font-size: 13px; padding: 4px 0; cursor: pointer; user-select: none; }
|
.vf-item { display: flex; align-items: center; gap: 8px; color: #eee; font-size: 13px; padding: 4px 0; cursor: pointer; user-select: none; }
|
||||||
.vf-item input { accent-color: var(--primary-on-dark); width: 15px; height: 15px; cursor: pointer; }
|
.vf-item input { accent-color: var(--primary-on-dark); width: 15px; height: 15px; cursor: pointer; }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
UI-KIT (core/ui-kit.js): velden, schakelaars, secties, tabbladen,
|
||||||
|
specificatie-kaarten. Iconen: Bootstrap Icons (css/vendor).
|
||||||
|
============================================================ */
|
||||||
|
.bi { line-height: 1; vertical-align: -0.125em; }
|
||||||
|
.ui-section { margin-bottom: 14px; }
|
||||||
|
.ui-section h2, .ui-spec-card h2 {
|
||||||
|
display: flex; align-items: center; gap: 7px; margin: 0 0 8px; font-size: 12px; font-weight: 600;
|
||||||
|
color: var(--primary); text-transform: uppercase; letter-spacing: .6px;
|
||||||
|
}
|
||||||
|
.ui-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 28px; }
|
||||||
|
.ui-field { display: flex; justify-content: space-between; align-items: center; gap: 10px; min-height: 28px; }
|
||||||
|
.ui-field label { font-size: 12.5px; color: var(--text); display: flex; align-items: center; gap: 5px; }
|
||||||
|
.ui-field.is-disabled label { color: var(--text-faint); }
|
||||||
|
.ui-control { display: flex; align-items: center; flex: none; }
|
||||||
|
.ui-control input[type=number], .ui-control select {
|
||||||
|
width: 118px; background: var(--surface); border: 1px solid var(--border); color: var(--text);
|
||||||
|
padding: 4px 6px; border-radius: 4px; font-size: 12.5px; font-family: inherit;
|
||||||
|
}
|
||||||
|
.ui-control input[type=number] { text-align: right; }
|
||||||
|
.ui-control input:focus, .ui-control select:focus { border-color: var(--primary); outline: none; box-shadow: 0 0 0 2px var(--primary-soft); }
|
||||||
|
.ui-control input:disabled { opacity: .55; background: var(--border-soft); cursor: not-allowed; }
|
||||||
|
.ui-unit { width: 26px; margin-left: 5px; font-size: 11px; color: var(--text-faint); }
|
||||||
|
.ui-hint { color: var(--text-faint); font-size: 11px; cursor: help; }
|
||||||
|
.ui-hint:hover { color: var(--primary); }
|
||||||
|
.ui-switch { position: relative; display: inline-block; width: 36px; height: 20px; margin-right: 82px; }
|
||||||
|
.ui-switch input { opacity: 0; width: 0; height: 0; }
|
||||||
|
.ui-switch span { position: absolute; inset: 0; background: #c9d1db; border-radius: 20px; cursor: pointer; transition: background .15s; }
|
||||||
|
.ui-switch span::before { content: ''; position: absolute; width: 14px; height: 14px; left: 3px; top: 3px; background: #fff; border-radius: 50%; transition: transform .15s; }
|
||||||
|
.ui-switch input:checked + span { background: var(--primary); }
|
||||||
|
.ui-switch input:checked + span::before { transform: translateX(16px); }
|
||||||
|
.ui-switch input:focus-visible + span { box-shadow: 0 0 0 2px var(--primary-soft); }
|
||||||
|
.ui-info, .ui-warn { display: flex; gap: 8px; align-items: flex-start; font-size: 12px; line-height: 1.5; border-radius: 4px; padding: 7px 10px; margin: 6px 0 10px; }
|
||||||
|
.ui-info { color: var(--text-muted); background: var(--surface); border: 1px solid var(--border-soft); }
|
||||||
|
.ui-warn { color: var(--danger); background: var(--danger-soft); border: 1px solid var(--danger); }
|
||||||
|
.ui-info .bi, .ui-warn .bi { margin-top: 2px; }
|
||||||
|
.ui-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||||||
|
.ui-btn {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px; background: var(--surface); border: 1px solid var(--border);
|
||||||
|
color: var(--text); padding: 5px 11px; border-radius: 4px; font-size: 12px; cursor: pointer; font-family: inherit;
|
||||||
|
}
|
||||||
|
.ui-btn:hover { border-color: var(--primary); color: var(--primary); }
|
||||||
|
.ui-btn.ui-btn-danger { color: var(--danger); border-color: #e3b4b0; }
|
||||||
|
.ui-btn.ui-btn-danger:hover { background: var(--danger-soft); border-color: var(--danger); }
|
||||||
|
.ui-btn.ui-btn-primary { background: var(--primary); border-color: var(--primary); color: #fff; }
|
||||||
|
.ui-panel-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
|
||||||
|
.ui-panel-head h1 { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 14px; }
|
||||||
|
.ui-panel-head .ui-sub { font-size: 12px; color: var(--text-muted); }
|
||||||
|
|
||||||
|
.ui-tab-bar { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin: 0 0 12px; }
|
||||||
|
.ui-tab {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px; background: none; border: none; border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -1px; padding: 7px 12px; font-size: 12.5px; color: var(--text-muted); cursor: pointer; font-family: inherit;
|
||||||
|
}
|
||||||
|
.ui-tab:hover { color: var(--text); }
|
||||||
|
.ui-tab.active { color: var(--primary); border-bottom-color: var(--primary); font-weight: 600; }
|
||||||
|
.ui-tab-page { display: none; }
|
||||||
|
.ui-tab-page.active { display: block; }
|
||||||
|
|
||||||
|
.ui-spec-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
|
||||||
|
.ui-spec-card { background: var(--surface); border: 1px solid var(--border-soft); border-radius: 6px; padding: 10px 12px; }
|
||||||
|
.ui-spec-row { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; padding: 3px 0; border-bottom: 1px dashed var(--border-soft); }
|
||||||
|
.ui-spec-row:last-child { border-bottom: none; }
|
||||||
|
.ui-spec-val { font-weight: 600; color: var(--text); text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
|
.ui-spec-val small { font-weight: normal; color: var(--text-faint); margin-left: 3px; }
|
||||||
|
.ui-spec-total { border-top: 1px solid var(--border); margin-top: 4px; padding-top: 2px; }
|
||||||
|
.ui-spec-total .ui-spec-val { color: var(--primary); }
|
||||||
|
|
||||||
|
.btn-3d-toggle { display: inline-flex; align-items: center; gap: 6px; }
|
||||||
|
.hn-item .bi { margin-right: 6px; color: var(--text-muted); }
|
||||||
|
.hn-item.active .bi, .hn-item:hover .bi { color: var(--primary); }
|
||||||
|
.hn-label { display: inline-flex; align-items: center; }
|
||||||
|
|
||||||
|
/* Weergave-cyclus (T): render-only = ook het info-deel onder de 3D weg. */
|
||||||
|
.render-only #ho-specs, .render-only #specs-panel-3d, .render-only #floor-specs-panel-3d { display: none; }
|
||||||
|
|
||||||
|
/* Bestaande editor-panelen (wand/vloer) in dezelfde stijl als de UI-kit. */
|
||||||
|
.control-group { min-height: 28px; margin-bottom: 3px; gap: 10px; }
|
||||||
|
.control-group label { font-size: 12.5px; color: var(--text); }
|
||||||
|
.control-group input, .control-group select { width: 118px; font-size: 12.5px; padding: 4px 6px; font-family: inherit; }
|
||||||
|
h1 { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: 600; letter-spacing: .6px; }
|
||||||
|
.tab-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; text-transform: none; font-size: 12.5px; letter-spacing: 0; }
|
||||||
|
.result-row { padding: 3px 0; border-bottom: 1px dashed var(--border-soft); }
|
||||||
|
.result-row:last-child { border-bottom: none; }
|
||||||
|
.result-row .highlight { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||||
|
#specs-panel-3d .spec-section, #floor-specs-panel-3d .spec-section {
|
||||||
|
background: var(--surface); border: 1px solid var(--border-soft); border-radius: 6px; padding: 10px 12px; margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.add-opening-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; text-transform: none; font-size: 12.5px; }
|
||||||
|
.hn-actions .ui-btn { flex: 1; justify-content: center; }
|
||||||
|
.hn-title { display: flex; align-items: center; gap: 6px; }
|
||||||
|
.file-protocol-warning { position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 9000; max-width: 640px; box-shadow: 0 8px 20px rgba(33,33,33,.2); }
|
||||||
|
.file-protocol-warning code { background: rgba(0,0,0,.06); padding: 0 4px; border-radius: 3px; }
|
||||||
|
.vf-option { gap: 9px; }
|
||||||
|
.vf-option.active { color: var(--primary-on-dark); font-weight: 600; }
|
||||||
|
.vf-menu .vf-option:hover { color: var(--primary-on-dark); }
|
||||||
|
|||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2019-2024 The Bootstrap Authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
Vendored
+5
File diff suppressed because one or more lines are too long
+5
-2
@@ -10,6 +10,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Tectum — Van Emous Mantelzorgwoningen</title>
|
<title>Tectum — Van Emous Mantelzorgwoningen</title>
|
||||||
|
<link rel="stylesheet" href="css/vendor/bootstrap-icons.min.css">
|
||||||
<link rel="stylesheet" href="css/app.css">
|
<link rel="stylesheet" href="css/app.css">
|
||||||
<script src="js/vendor/three.min.js"></script>
|
<script src="js/vendor/three.min.js"></script>
|
||||||
<script src="js/vendor/OrbitControls.js"></script>
|
<script src="js/vendor/OrbitControls.js"></script>
|
||||||
@@ -52,8 +53,8 @@
|
|||||||
<button id="menu-beeld-btn" class="menu-btn">Beeld</button>
|
<button id="menu-beeld-btn" class="menu-btn">Beeld</button>
|
||||||
<div id="menu-beeld-dropdown" class="menu-dropdown">
|
<div id="menu-beeld-dropdown" class="menu-dropdown">
|
||||||
<div class="menu-item" id="menu-fullscreen"><span>Volledig scherm</span><span class="menu-hotkey">F</span></div>
|
<div class="menu-item" id="menu-fullscreen"><span>Volledig scherm</span><span class="menu-hotkey">F</span></div>
|
||||||
<div class="menu-item" id="menu-3d-fullscreen"><span>3D volledig scherm</span><span class="menu-hotkey">T</span></div>
|
<div class="menu-item" id="menu-3d-fullscreen"><span>3D groot / alleen render</span><span class="menu-hotkey">T</span></div>
|
||||||
<div class="menu-item" id="menu-2d-fullscreen"><span>2D volledig scherm</span><span class="menu-hotkey">R</span></div>
|
<div class="menu-item" id="menu-2d-fullscreen"><span>2D groot</span><span class="menu-hotkey">R</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -116,6 +117,8 @@
|
|||||||
<script src="js/core/canvas-labels.js"></script>
|
<script src="js/core/canvas-labels.js"></script>
|
||||||
<script src="js/core/camera-memory.js"></script>
|
<script src="js/core/camera-memory.js"></script>
|
||||||
<script src="js/core/view-filter.js"></script>
|
<script src="js/core/view-filter.js"></script>
|
||||||
|
<script src="js/core/ui-kit.js"></script>
|
||||||
|
<script src="js/core/skybox.js"></script>
|
||||||
<script src="js/vendor/jspdf.umd.min.js"></script>
|
<script src="js/vendor/jspdf.umd.min.js"></script>
|
||||||
<script src="js/core/pdf-sheet.js"></script>
|
<script src="js/core/pdf-sheet.js"></script>
|
||||||
<script src="js/wall/wall-model.js"></script>
|
<script src="js/wall/wall-model.js"></script>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||||
* Zie LICENTIE.md.
|
* Zie LICENTIE.md.
|
||||||
*/
|
*/
|
||||||
const APP_VERSION = '0.4.0';
|
const APP_VERSION = '0.4.13';
|
||||||
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
||||||
|
|
||||||
let tabs = [];
|
let tabs = [];
|
||||||
@@ -290,6 +290,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
wireNewModal();
|
wireNewModal();
|
||||||
wireHotkeys();
|
wireHotkeys();
|
||||||
wireTabBar();
|
wireTabBar();
|
||||||
|
// Tectum draait via een webserver (texturen, lucht); file:// werkt niet volledig.
|
||||||
|
if (location.protocol === 'file:') {
|
||||||
|
const note = document.createElement('div');
|
||||||
|
note.className = 'ui-warn file-protocol-warning';
|
||||||
|
note.innerHTML = '<i class="bi bi-exclamation-triangle"></i><span>Tectum is geopend als bestand. Start het via een webserver: <code>./start.sh</code> of <code>python3 -m http.server</code>, en open <code>http://localhost:8000</code>. De omgeving (Dag/Nacht) werkt anders niet.</span>';
|
||||||
|
document.body.appendChild(note);
|
||||||
|
}
|
||||||
openNew('woning', 'L');
|
openNew('woning', 'L');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
+2
-2
@@ -329,7 +329,7 @@ function offsetRectilinearLoop(pts, d) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Gevels: groepeert lijnstukken in de plattegrond tot doorlopende gevels
|
// Gevels: groepeert lijnstukken in de plattegrond tot doorlopende gevels
|
||||||
// (zelfde as + normaal + gevelvlak, aansluitend). segs: [{ axis ('x'|'z'),
|
// (zelfde as + normaal + gevelvlak, aansluitend of overlappend). segs: [{ axis ('x'|'z'),
|
||||||
// normal {x, z}, line (positie op de andere as), s0, s1 (s0 < s1) }, ...].
|
// normal {x, z}, line (positie op de andere as), s0, s1 (s0 < s1) }, ...].
|
||||||
// Geeft [{ axis, normal, line, s0, s1, segs (op s0 gesorteerd) }].
|
// Geeft [{ axis, normal, line, s0, s1, segs (op s0 gesorteerd) }].
|
||||||
// Gebruikt door de woning (latwerk/doek-patroon) en de buitenverlichting.
|
// Gebruikt door de woning (latwerk/doek-patroon) en de buitenverlichting.
|
||||||
@@ -338,7 +338,7 @@ function groupFacadeRuns(segs, opts) {
|
|||||||
const runs = [];
|
const runs = [];
|
||||||
segs.slice().sort((a, b) => a.s0 - b.s0).forEach(sg => {
|
segs.slice().sort((a, b) => a.s0 - b.s0).forEach(sg => {
|
||||||
const run = runs.find(r => r.axis === sg.axis && Math.abs(r.normal.x - sg.normal.x) < 1e-6 && Math.abs(r.normal.z - sg.normal.z) < 1e-6 &&
|
const run = runs.find(r => r.axis === sg.axis && Math.abs(r.normal.x - sg.normal.x) < 1e-6 && Math.abs(r.normal.z - sg.normal.z) < 1e-6 &&
|
||||||
Math.abs(r.line - sg.line) < lineTol && Math.abs(r.s1 - sg.s0) < gapTol);
|
Math.abs(r.line - sg.line) < lineTol && sg.s0 <= r.s1 + gapTol); // aansluitend of overlappend
|
||||||
if (run) { run.segs.push(sg); run.s1 = Math.max(run.s1, sg.s1); }
|
if (run) { run.segs.push(sg); run.s1 = Math.max(run.s1, sg.s1); }
|
||||||
else runs.push(Object.assign({}, sg, { s0: sg.s0, s1: sg.s1, segs: [sg] }));
|
else runs.push(Object.assign({}, sg, { s0: sg.s0, s1: sg.s1, segs: [sg] }));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
/*!
|
||||||
|
* Tectum
|
||||||
|
* Copyright © 2026 Machiel Heinen.
|
||||||
|
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||||
|
* Zie LICENTIE.md.
|
||||||
|
*/
|
||||||
|
// ============================================================
|
||||||
|
// core/skybox.js — omgeving voor de 3D-weergaven (Ontwerp / Dag / Nacht).
|
||||||
|
// Een panorama (assets/skybox.webp of skybox-night.webp) wordt op de grond
|
||||||
|
// geprojecteerd, zodat het object op de grond staat; onder het object ligt
|
||||||
|
// een ronde tegelvloer (assets/tiles.jpg) met een witte opstaande rand, groot
|
||||||
|
// genoeg voor het hele object. Vloer en rand reageren op het licht en vangen
|
||||||
|
// de schaduw op. Nacht: omgevingslicht op NIGHT_FACTOR (lampen blijven 100%).
|
||||||
|
//
|
||||||
|
// Grondprojectie: port van three.js GroundedSkybox (r157+, MIT; wij draaien
|
||||||
|
// r128): een binnenstebuiten bol waarvan de onderhelft plat gedrukt wordt op
|
||||||
|
// een vloer op CAMERA_HEIGHT_M onder het midden van de bol.
|
||||||
|
//
|
||||||
|
// Vereist een webserver (texturen via TextureLoader). Plain script.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
window.Skybox = (() => {
|
||||||
|
const PREF_KEY = 'tectum.sky';
|
||||||
|
const MODES = ['none', 'day', 'night'];
|
||||||
|
const SKY_URLS = { day: 'assets/skybox.webp', night: 'assets/skybox-night.webp' };
|
||||||
|
const TILES_URL = 'assets/tiles.jpg';
|
||||||
|
const CAMERA_HEIGHT_M = 1.7; // hoogte van de panoramacamera boven de grond
|
||||||
|
const RADIUS_M = 60; // straal van de omgevingsbol
|
||||||
|
const TILE_M = 2.4; // één tegeltextuur (1024 px) beslaat zoveel meter
|
||||||
|
const MARGIN_M = 1.5; // vrije rand tussen object en opstaande rand
|
||||||
|
const SIZE_FACTOR = 1.2; // tegelvloer 20% ruimer dan object + marge
|
||||||
|
const RIM_HEIGHT_M = 0.10, RIM_THICK_M = 0.025;
|
||||||
|
const NIGHT_FACTOR = 0.15; // omgevingslicht 's nachts
|
||||||
|
const SUN_OFFSET = { x: 8, y: 14, z: 6 };
|
||||||
|
|
||||||
|
const cache = {};
|
||||||
|
function loadTexture(url) {
|
||||||
|
if (!cache[url]) {
|
||||||
|
cache[url] = new Promise((resolve, reject) => new THREE.TextureLoader().load(url, resolve, undefined, reject));
|
||||||
|
cache[url].catch(() => { delete cache[url]; });
|
||||||
|
}
|
||||||
|
return cache[url];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSky(map) {
|
||||||
|
const geometry = new THREE.SphereGeometry(RADIUS_M, 256, 128);
|
||||||
|
geometry.scale(1, 1, -1); // binnenkant zichtbaar
|
||||||
|
const pos = geometry.getAttribute('position');
|
||||||
|
const v = new THREE.Vector3();
|
||||||
|
const h = CAMERA_HEIGHT_M, y1 = -h * 3 / 2;
|
||||||
|
for (let i = 0; i < pos.count; i++) {
|
||||||
|
v.fromBufferAttribute(pos, i);
|
||||||
|
if (v.y < 0) {
|
||||||
|
// Onderhelft plat op y = -h; vloeiende overgang naar de bol.
|
||||||
|
const f = v.y < y1 ? -h / v.y : (1 - v.y * v.y / (3 * y1 * y1));
|
||||||
|
v.multiplyScalar(f);
|
||||||
|
pos.setXYZ(i, v.x, v.y, v.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos.needsUpdate = true;
|
||||||
|
const mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ map, depthWrite: false }));
|
||||||
|
mesh.position.y = h - 0.01; // vlakke vloer net onder de tegelvloer
|
||||||
|
mesh.renderOrder = -1;
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ronde tegelvloer (straal r) + witte opstaande rand erlangs.
|
||||||
|
function buildTerrace(map, r) {
|
||||||
|
const group = new THREE.Group();
|
||||||
|
const tex = map.clone();
|
||||||
|
tex.needsUpdate = true;
|
||||||
|
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||||
|
tex.anisotropy = 8;
|
||||||
|
tex.repeat.set(2 * r / TILE_M, 2 * r / TILE_M);
|
||||||
|
const floor = new THREE.Mesh(new THREE.CircleGeometry(r, 128), new THREE.MeshStandardMaterial({ map: tex, roughness: 0.85 }));
|
||||||
|
floor.rotation.x = -Math.PI / 2;
|
||||||
|
floor.receiveShadow = true;
|
||||||
|
group.add(floor);
|
||||||
|
|
||||||
|
const ring = new THREE.Shape();
|
||||||
|
ring.absarc(0, 0, r + RIM_THICK_M, 0, Math.PI * 2, false);
|
||||||
|
const hole = new THREE.Path();
|
||||||
|
hole.absarc(0, 0, r, 0, Math.PI * 2, true);
|
||||||
|
ring.holes.push(hole);
|
||||||
|
const rim = new THREE.Mesh(
|
||||||
|
new THREE.ExtrudeGeometry(ring, { depth: RIM_HEIGHT_M, bevelEnabled: false, curveSegments: 128 }),
|
||||||
|
new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.5 })
|
||||||
|
);
|
||||||
|
rim.rotation.x = -Math.PI / 2; // extrusie omhoog
|
||||||
|
rim.castShadow = rim.receiveShadow = true;
|
||||||
|
group.add(rim);
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Omgeving voor één scene: { group, setMode(mode), fitTo(object3D, groundY) }.
|
||||||
|
function createEnvironment(scene) {
|
||||||
|
const group = new THREE.Group();
|
||||||
|
group.visible = false;
|
||||||
|
scene.add(group);
|
||||||
|
const skies = {};
|
||||||
|
let terrace = null, terraceR = 0, tilesTex = null, mode = 'none';
|
||||||
|
|
||||||
|
function showSky() {
|
||||||
|
Object.keys(skies).forEach(k => { skies[k].visible = k === mode; });
|
||||||
|
if (mode === 'none' || skies[mode]) return;
|
||||||
|
const wanted = mode;
|
||||||
|
loadTexture(SKY_URLS[wanted]).then(tex => {
|
||||||
|
if (!skies[wanted]) { skies[wanted] = buildSky(tex); group.add(skies[wanted]); }
|
||||||
|
Object.keys(skies).forEach(k => { skies[k].visible = k === mode; });
|
||||||
|
}).catch(() => console.warn('Tectum: lucht niet geladen (draait de app via een webserver?)'));
|
||||||
|
}
|
||||||
|
function setTerrace(r) {
|
||||||
|
if (terrace && Math.abs(r - terraceR) < 0.05) return;
|
||||||
|
terraceR = r;
|
||||||
|
const build = () => {
|
||||||
|
if (terrace) { group.remove(terrace); terrace.traverse(o => { if (o.geometry) o.geometry.dispose(); }); }
|
||||||
|
terrace = buildTerrace(tilesTex, terraceR);
|
||||||
|
group.add(terrace);
|
||||||
|
};
|
||||||
|
if (tilesTex) build();
|
||||||
|
else loadTexture(TILES_URL).then(t => { tilesTex = t; build(); }).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
group,
|
||||||
|
setMode(m) { mode = m; group.visible = m !== 'none'; showSky(); },
|
||||||
|
// Midden en straal uit de omhullende van het object (horizontaal), vloer op groundY (m).
|
||||||
|
fitTo(object3D, groundY) {
|
||||||
|
if (!object3D) return;
|
||||||
|
const box = new THREE.Box3().setFromObject(object3D);
|
||||||
|
if (box.isEmpty()) return;
|
||||||
|
const c = box.getCenter(new THREE.Vector3());
|
||||||
|
group.position.set(c.x, groundY, c.z);
|
||||||
|
setTerrace((Math.hypot(box.max.x - box.min.x, box.max.z - box.min.z) / 2 + MARGIN_M) * SIZE_FACTOR);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMode() {
|
||||||
|
try {
|
||||||
|
const v = localStorage.getItem(PREF_KEY);
|
||||||
|
if (MODES.includes(v)) return v;
|
||||||
|
return localStorage.getItem('tectum.skybox') === '1' ? 'day' : 'none'; // oude aan/uit-voorkeur
|
||||||
|
} catch (e) { return 'none'; }
|
||||||
|
}
|
||||||
|
function remember(m) {
|
||||||
|
try { localStorage.setItem(PREF_KEY, m); } catch (e) { /* geen opslag: alleen deze sessie */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
const lightFactor = (m) => m === 'night' ? NIGHT_FACTOR : 1;
|
||||||
|
|
||||||
|
// Omgeving toepassen: lucht/vloer, raster, camera niet onder de grond, zon
|
||||||
|
// met schaduw, en het licht (nacht: NIGHT_FACTOR).
|
||||||
|
// o: { env, grid, controls, sun, ambient, base: { ambient, sun }, shadowRoot }
|
||||||
|
function apply(m, o) {
|
||||||
|
const on = m !== 'none';
|
||||||
|
o.env.setMode(m);
|
||||||
|
if (o.grid) o.grid.visible = !on;
|
||||||
|
if (o.controls) o.controls.maxPolarAngle = on ? Math.PI / 2 - 0.03 : Math.PI - 0.001;
|
||||||
|
const f = lightFactor(m);
|
||||||
|
if (o.ambient && o.base) o.ambient.intensity = o.base.ambient * f;
|
||||||
|
if (o.sun && o.base) o.sun.intensity = o.base.sun * f;
|
||||||
|
if (o.sun) {
|
||||||
|
o.sun.castShadow = on;
|
||||||
|
if (on) {
|
||||||
|
const p = o.env.group.position, cam = o.sun.shadow.camera;
|
||||||
|
cam.left = cam.bottom = -20; cam.right = cam.top = 20;
|
||||||
|
cam.near = 0.5; cam.far = 80;
|
||||||
|
cam.updateProjectionMatrix();
|
||||||
|
o.sun.shadow.mapSize.set(2048, 2048);
|
||||||
|
o.sun.shadow.bias = -0.0005;
|
||||||
|
o.sun.position.set(p.x + SUN_OFFSET.x, p.y + SUN_OFFSET.y, p.z + SUN_OFFSET.z);
|
||||||
|
o.sun.target.position.copy(p);
|
||||||
|
o.sun.target.updateMatrixWorld();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (o.shadowRoot) o.shadowRoot.traverse(obj => { if (obj.isMesh) obj.castShadow = on; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keuzemenu in een 3D-werkbalk: Ontwerp (geen omgeving) / Dag / Nacht.
|
||||||
|
const LABELS = { none: ['pencil-square', 'Ontwerp'], day: ['sun', 'Dag'], night: ['moon-stars', 'Nacht'] };
|
||||||
|
function createToggle(toolbar, onChange) {
|
||||||
|
let m = getMode();
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.className = 'view-filter';
|
||||||
|
wrap.innerHTML = `<button type="button" class="btn-3d-toggle vf-btn" title="Omgeving"></button>
|
||||||
|
<div class="vf-menu">${MODES.map(k => `<div class="vf-item vf-option" data-mode="${k}"><i class="bi bi-${LABELS[k][0]}"></i>${LABELS[k][1]}</div>`).join('')}</div>`;
|
||||||
|
toolbar.appendChild(wrap);
|
||||||
|
const btn = wrap.querySelector('.vf-btn'), menu = wrap.querySelector('.vf-menu');
|
||||||
|
const sync = () => {
|
||||||
|
btn.innerHTML = `<i class="bi bi-${LABELS[m][0]}"></i>${LABELS[m][1]}<i class="bi bi-chevron-down"></i>`;
|
||||||
|
menu.querySelectorAll('.vf-option').forEach(o => o.classList.toggle('active', o.dataset.mode === m));
|
||||||
|
};
|
||||||
|
btn.addEventListener('click', (e) => { e.stopPropagation(); menu.classList.toggle('open'); });
|
||||||
|
menu.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const opt = e.target.closest('.vf-option');
|
||||||
|
if (!opt) return;
|
||||||
|
menu.classList.remove('open');
|
||||||
|
if (opt.dataset.mode === m) return;
|
||||||
|
m = opt.dataset.mode;
|
||||||
|
remember(m);
|
||||||
|
sync();
|
||||||
|
onChange(m);
|
||||||
|
});
|
||||||
|
const closeOnOutside = () => {
|
||||||
|
if (!document.body.contains(wrap)) { document.removeEventListener('click', closeOnOutside); return; }
|
||||||
|
menu.classList.remove('open');
|
||||||
|
};
|
||||||
|
document.addEventListener('click', closeOnOutside);
|
||||||
|
sync();
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { CAMERA_HEIGHT_M, NIGHT_FACTOR, createEnvironment, apply, getMode, lightFactor, createToggle };
|
||||||
|
})();
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/*!
|
||||||
|
* Tectum
|
||||||
|
* Copyright © 2026 Machiel Heinen.
|
||||||
|
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||||
|
* Zie LICENTIE.md.
|
||||||
|
*/
|
||||||
|
// ============================================================
|
||||||
|
// core/ui-kit.js — gedeelde UI-bouwstenen (window.UI): iconen (Bootstrap
|
||||||
|
// Icons, css/vendor), invoervelden, schakelaars, secties, tabbladen,
|
||||||
|
// specificatie-kaarten en de 3-staps weergave-cyclus (sneltoets T).
|
||||||
|
// Eén plek voor opmaak en woordgebruik, zodat alle panelen gelijk ogen.
|
||||||
|
//
|
||||||
|
// Woordgebruik labels: zelfstandig naamwoord met hoofdletter, geen dubbele
|
||||||
|
// punt, geen uitleg tussen haakjes (die gaat in een hint), eenheid rechts
|
||||||
|
// van het veld. Bijvoorbeeld "Afstand min." i.p.v. "Lampen min. h.o.h.".
|
||||||
|
// Plain script (geen ES-module).
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
window.UI = (() => {
|
||||||
|
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
const icon = (name, cls) => `<i class="bi bi-${name}${cls ? ' ' + cls : ''}" aria-hidden="true"></i>`;
|
||||||
|
const hint = (text) => text ? `<span class="ui-hint" title="${esc(text)}">${icon('info-circle')}</span>` : '';
|
||||||
|
|
||||||
|
function field(id, label, control, o) {
|
||||||
|
return `<div class="ui-field${o && o.disabled ? ' is-disabled' : ''}"><label for="${id}">${label}${hint(o && o.hint)}</label><div class="ui-control">${control}</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getal. o: { step, unit ('mm' standaard, '' = geen), hint, min, disabled }
|
||||||
|
function num(id, label, value, o) {
|
||||||
|
o = o || {};
|
||||||
|
const unit = o.unit === undefined ? 'mm' : o.unit;
|
||||||
|
return field(id, label,
|
||||||
|
`<input type="number" id="${id}" value="${value}" step="${o.step || 50}"${o.min !== undefined ? ` min="${o.min}"` : ''}${o.disabled ? ' disabled' : ''}>` +
|
||||||
|
`<span class="ui-unit">${unit}</span>`, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keuzelijst. options: { waarde: 'Label' }.
|
||||||
|
function sel(id, label, options, value, o) {
|
||||||
|
const opts = Object.entries(options).map(([k, v]) => `<option value="${k}"${k === value ? ' selected' : ''}>${v}</option>`).join('');
|
||||||
|
return field(id, label, `<select id="${id}">${opts}</select><span class="ui-unit"></span>`, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aan/uit-schakelaar.
|
||||||
|
function toggle(id, label, checked, o) {
|
||||||
|
return field(id, label,
|
||||||
|
`<label class="ui-switch"><input type="checkbox" id="${id}"${checked ? ' checked' : ''}><span></span></label><span class="ui-unit"></span>`, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
const section = (title, iconName, body) =>
|
||||||
|
`<section class="ui-section"><h2>${iconName ? icon(iconName) : ''}${title}</h2>${body}</section>`;
|
||||||
|
const info = (text) => `<div class="ui-info">${icon('info-circle')}<span>${text}</span></div>`;
|
||||||
|
const warn = (text) => `<div class="ui-warn">${icon('exclamation-triangle')}<span>${text}</span></div>`;
|
||||||
|
const grid = (...cols) => `<div class="ui-grid">${cols.map(c => `<div>${c}</div>`).join('')}</div>`;
|
||||||
|
const button = (label, iconName, attrs, cls) =>
|
||||||
|
`<button type="button" class="ui-btn${cls ? ' ' + cls : ''}" ${attrs || ''}>${iconName ? icon(iconName) : ''}<span>${label}</span></button>`;
|
||||||
|
const actions = (...btns) => `<div class="ui-actions">${btns.join('')}</div>`;
|
||||||
|
|
||||||
|
// Tabbladen. group = sleutel waaronder het actieve tabblad onthouden wordt
|
||||||
|
// (bijv. 'woning', 'unit'); tabs = [{ key, label, icon, html }].
|
||||||
|
const activeTabs = {};
|
||||||
|
function tabs(group, list) {
|
||||||
|
const active = list.some(t => t.key === activeTabs[group]) ? activeTabs[group] : list[0].key;
|
||||||
|
return `<div class="ui-tabs" data-tab-group="${group}">
|
||||||
|
<div class="ui-tab-bar">${list.map(t => `<button type="button" class="ui-tab${t.key === active ? ' active' : ''}" data-tab="${t.key}">${t.icon ? icon(t.icon) : ''}<span>${t.label}</span></button>`).join('')}</div>
|
||||||
|
${list.map(t => `<div class="ui-tab-page${t.key === active ? ' active' : ''}" data-tab-page="${t.key}">${t.html}</div>`).join('')}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
// Eenmalig per container: klik op een tab wisselt de pagina.
|
||||||
|
function wireTabs(container) {
|
||||||
|
if (container._uiTabsWired) return;
|
||||||
|
container._uiTabsWired = true;
|
||||||
|
container.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('.ui-tab');
|
||||||
|
if (!btn) return;
|
||||||
|
const wrap = btn.closest('.ui-tabs');
|
||||||
|
activeTabs[wrap.dataset.tabGroup] = btn.dataset.tab;
|
||||||
|
wrap.querySelectorAll('.ui-tab').forEach(b => b.classList.toggle('active', b === btn));
|
||||||
|
wrap.querySelectorAll('.ui-tab-page').forEach(p => p.classList.toggle('active', p.dataset.tabPage === btn.dataset.tab));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specificaties: kaart met regels [label, waarde, eenheid].
|
||||||
|
function specCard(title, iconName, rows, total) {
|
||||||
|
const row = ([label, value, unit]) =>
|
||||||
|
`<div class="ui-spec-row"><span>${label}</span><span class="ui-spec-val">${value}${unit ? `<small>${unit}</small>` : ''}</span></div>`;
|
||||||
|
return `<div class="ui-spec-card"><h2>${icon(iconName)}${title}</h2>${rows.map(row).join('')}${total ? `<div class="ui-spec-total">${row(total)}</div>` : ''}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoer koppelen: onNum/onSel/onToggle(root, id, fn, after).
|
||||||
|
function onNum(id, fn, after, o) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) return;
|
||||||
|
el.addEventListener('input', (e) => {
|
||||||
|
const v = parseFloat(e.target.value);
|
||||||
|
if (isFinite(v) && (!(o && o.positive) || v > 0)) { fn(v); after && after(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function onSel(id, fn, after) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.addEventListener('input', (e) => { fn(e.target.value); after && after(); });
|
||||||
|
}
|
||||||
|
function onToggle(id, fn, after) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.addEventListener('change', (e) => { fn(e.target.checked); after && after(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weergave-cyclus 3D (sneltoets T): standaard -> 3D groot -> alleen de
|
||||||
|
// render (ook het info-/specificatiedeel eronder weg) -> standaard.
|
||||||
|
function cycle3D(el) {
|
||||||
|
el.classList.remove('fullscreen-2d');
|
||||||
|
if (!el.classList.contains('fullscreen-3d')) el.classList.add('fullscreen-3d');
|
||||||
|
else if (!el.classList.contains('render-only')) el.classList.add('render-only');
|
||||||
|
else el.classList.remove('fullscreen-3d', 'render-only');
|
||||||
|
}
|
||||||
|
function toggle2D(el) {
|
||||||
|
el.classList.remove('fullscreen-3d', 'render-only');
|
||||||
|
el.classList.toggle('fullscreen-2d');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { esc, icon, hint, num, sel, toggle, section, info, warn, grid, button, actions, tabs, wireTabs, specCard, onNum, onSel, onToggle, cycle3D, toggle2D };
|
||||||
|
})();
|
||||||
@@ -21,7 +21,7 @@ function createViewFilter(toolbar, items, onChange) {
|
|||||||
const wrap = document.createElement('div');
|
const wrap = document.createElement('div');
|
||||||
wrap.className = 'view-filter';
|
wrap.className = 'view-filter';
|
||||||
wrap.innerHTML = `
|
wrap.innerHTML = `
|
||||||
<button type="button" class="btn-3d-toggle vf-btn">Weergave ▾</button>
|
<button type="button" class="btn-3d-toggle vf-btn"><i class="bi bi-eye"></i>Weergave<i class="bi bi-chevron-down"></i></button>
|
||||||
<div class="vf-menu">
|
<div class="vf-menu">
|
||||||
${items.map(it => `<label class="vf-item"><input type="checkbox" data-key="${it.key}" ${state[it.key] ? 'checked' : ''}> ${it.label}</label>`).join('')}
|
${items.map(it => `<label class="vf-item"><input type="checkbox" data-key="${it.key}" ${state[it.key] ? 'checked' : ''}> ${it.label}</label>`).join('')}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
+30
-27
@@ -114,8 +114,12 @@ window.FloorModel = (() => {
|
|||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// Balkverdeling: zelfde algoritme (calculateSegmentPositions) als de wand.
|
// Balkverdeling: zelfde algoritme (calculateSegmentPositions) als de wand.
|
||||||
// params = state.floor (of de ruwe invoerwaarden uit de editor).
|
// 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) {
|
function computeFrame(params, rims) {
|
||||||
const width = params.width || 2000;
|
const width = params.width || 2000;
|
||||||
const depth = params.depth || 2000;
|
const depth = params.depth || 2000;
|
||||||
const joistW = params.joistWidth || 70;
|
const joistW = params.joistWidth || 70;
|
||||||
@@ -131,7 +135,9 @@ window.FloorModel = (() => {
|
|||||||
calculateSegmentPositions(width - joistW, minSp, maxSp).forEach(p => joists.push(leftJoistX + p));
|
calculateSegmentPositions(width - joistW, minSp, maxSp).forEach(p => joists.push(leftJoistX + p));
|
||||||
joists = [...new Set(joists.map(x => Math.round(x)))].sort((a, b) => a - b);
|
joists = [...new Set(joists.map(x => Math.round(x)))].sort((a, b) => a - b);
|
||||||
|
|
||||||
return { joists, width, depth, joistW, joistH, underlayThick };
|
const r = { front: !!(rims && rims.front), back: !!(rims && rims.back) };
|
||||||
|
const joistLen = depth - (r.front ? joistW : 0) - (r.back ? joistW : 0);
|
||||||
|
return { joists, width, depth, joistW, joistH, underlayThick, rims: r, joistLen };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bovenkant vloer (mm) = waar de wanden van een unit op staan.
|
// Bovenkant vloer (mm) = waar de wanden van een unit op staan.
|
||||||
@@ -151,8 +157,10 @@ window.FloorModel = (() => {
|
|||||||
const { width, depth, joists, joistW, joistH, underlayThick } = calcData;
|
const { width, depth, joists, joistW, joistH, underlayThick } = calcData;
|
||||||
const widthM = width * toM, depthM = depth * toM;
|
const widthM = width * toM, depthM = depth * toM;
|
||||||
const joistWM = joistW * toM, joistHM = joistH * toM, underlayM = underlayThick * toM;
|
const joistWM = joistW * toM, joistHM = joistH * toM, underlayM = underlayThick * toM;
|
||||||
|
const rimCount = calcData.rims ? (calcData.rims.front ? 1 : 0) + (calcData.rims.back ? 1 : 0) : 0;
|
||||||
|
const joistLenM = (calcData.joistLen || depth) * toM;
|
||||||
|
|
||||||
const joistWoodM3 = joists.length * joistWM * joistHM * depthM;
|
const joistWoodM3 = joists.length * joistWM * joistHM * joistLenM + rimCount * joistWM * joistHM * widthM;
|
||||||
const joistWoodKg = joistWoodM3 * DENSITY_WOOD_KGM3;
|
const joistWoodKg = joistWoodM3 * DENSITY_WOOD_KGM3;
|
||||||
|
|
||||||
const grossAreaM2 = widthM * depthM;
|
const grossAreaM2 = widthM * depthM;
|
||||||
@@ -197,8 +205,9 @@ window.FloorModel = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Alles in één keer vanuit een vloer-state (handig voor de unit).
|
// Alles in één keer vanuit een vloer-state (handig voor de unit).
|
||||||
function computeFromState(state) {
|
// rims: zie computeFrame.
|
||||||
const calcData = computeFrame(state.floor);
|
function computeFromState(state, rims) {
|
||||||
|
const calcData = computeFrame(state.floor, rims);
|
||||||
const resolvedOpenings = resolveFloorOpenings(state.openings || [], calcData.width, calcData.depth, calcData.joists, calcData.joistW);
|
const resolvedOpenings = resolveFloorOpenings(state.openings || [], calcData.width, calcData.depth, calcData.joists, calcData.joistW);
|
||||||
const chassisData = computeChassisWheels(state.chassis, calcData.width, calcData.depth);
|
const chassisData = computeChassisWheels(state.chassis, calcData.width, calcData.depth);
|
||||||
return { calcData, resolvedOpenings, chassisData };
|
return { calcData, resolvedOpenings, chassisData };
|
||||||
@@ -208,9 +217,6 @@ window.FloorModel = (() => {
|
|||||||
return {
|
return {
|
||||||
woodMaterial: new THREE.MeshStandardMaterial({ color: 0xc29b62, roughness: 0.6 }),
|
woodMaterial: new THREE.MeshStandardMaterial({ color: 0xc29b62, roughness: 0.6 }),
|
||||||
osbMaterial: new THREE.MeshStandardMaterial({ color: 0xcda86a, roughness: 0.75, side: THREE.DoubleSide }),
|
osbMaterial: new THREE.MeshStandardMaterial({ color: 0xcda86a, roughness: 0.75, side: THREE.DoubleSide }),
|
||||||
isolatieMaterial: new THREE.MeshStandardMaterial({
|
|
||||||
color: 0xffe08c, roughness: 0.9, transparent: true, opacity: 0.5, side: THREE.DoubleSide
|
|
||||||
}),
|
|
||||||
wheelMaterial: new THREE.MeshStandardMaterial({ color: 0x151515, roughness: 0.5 })
|
wheelMaterial: new THREE.MeshStandardMaterial({ color: 0x151515, roughness: 0.5 })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -220,12 +226,11 @@ window.FloorModel = (() => {
|
|||||||
// Z = -diepte/2), Y = dikte (balkhoogte + OSB). Balken en isolatie zijn
|
// Z = -diepte/2), Y = dikte (balkhoogte + OSB). Balken en isolatie zijn
|
||||||
// simpele dozen; de OSB-onderlaag is een geëxtrudeerd Shape met ronde
|
// simpele dozen; de OSB-onderlaag is een geëxtrudeerd Shape met ronde
|
||||||
// uitsparingen voor de doorvoeren.
|
// uitsparingen voor de doorvoeren.
|
||||||
// opts: { materials, osbVisible, isolatieVisible }
|
// opts: { materials, osbVisible }
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
function build3D(calcData, resolvedOpenings, chassisData, opts) {
|
function build3D(calcData, resolvedOpenings, chassisData, opts) {
|
||||||
const { woodMaterial, osbMaterial, isolatieMaterial, wheelMaterial } = opts.materials;
|
const { woodMaterial, osbMaterial, wheelMaterial } = opts.materials;
|
||||||
const osbVisible = opts.osbVisible !== false;
|
const osbVisible = opts.osbVisible !== false;
|
||||||
const isolatieVisible = opts.isolatieVisible !== false;
|
|
||||||
const floorGroup3D = new THREE.Group();
|
const floorGroup3D = new THREE.Group();
|
||||||
|
|
||||||
const toM = 0.001;
|
const toM = 0.001;
|
||||||
@@ -233,28 +238,26 @@ window.FloorModel = (() => {
|
|||||||
const widthM = width * toM, depthM = depth * toM;
|
const widthM = width * toM, depthM = depth * toM;
|
||||||
const joistWM = joistW * toM, joistHM = joistH * toM, underlayM = underlayThick * toM;
|
const joistWM = joistW * toM, joistHM = joistH * toM, underlayM = underlayThick * toM;
|
||||||
|
|
||||||
// A. VLOERBALKEN — dozen, lopen over de volle diepte door.
|
// 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;
|
||||||
joists.forEach(xMM => {
|
joists.forEach(xMM => {
|
||||||
const geo = new THREE.BoxGeometry(joistWM, joistHM, depthM);
|
const geo = new THREE.BoxGeometry(joistWM, joistHM, joistLenM);
|
||||||
const mesh = new THREE.Mesh(geo, woodMaterial);
|
const mesh = new THREE.Mesh(geo, woodMaterial);
|
||||||
mesh.position.set(xMM * toM, joistHM / 2, 0);
|
mesh.position.set(xMM * toM, joistHM / 2, joistZ);
|
||||||
|
mesh.castShadow = true;
|
||||||
|
floorGroup3D.add(mesh);
|
||||||
|
});
|
||||||
|
[['front', -1], ['back', 1]].filter(([k]) => rims[k]).forEach(([, sign]) => {
|
||||||
|
const mesh = new THREE.Mesh(new THREE.BoxGeometry(widthM, joistHM, joistWM), woodMaterial);
|
||||||
|
mesh.position.set(0, joistHM / 2, sign * (depthM - joistWM) / 2);
|
||||||
mesh.castShadow = true;
|
mesh.castShadow = true;
|
||||||
floorGroup3D.add(mesh);
|
floorGroup3D.add(mesh);
|
||||||
});
|
});
|
||||||
|
|
||||||
// B. ISOLATIE — vult elk balkvak (tussen twee opeenvolgende balken) over
|
// (Isolatie wordt niet getekend; de leegte-inhoud zit in computeSpecs.)
|
||||||
// de volle balkhoogte/diepte. Zelfde "leegte tussen het regelwerk"-
|
|
||||||
// gedachte als bij wall.js, hier ook echt gevisualiseerd als laag.
|
|
||||||
const bays = computeBays(joists, joistW);
|
|
||||||
bays.forEach(bay => {
|
|
||||||
const bayWidthM = (bay.right - bay.left) * toM;
|
|
||||||
if (bayWidthM <= 0) return;
|
|
||||||
const geo = new THREE.BoxGeometry(bayWidthM, joistHM, depthM);
|
|
||||||
const mesh = new THREE.Mesh(geo, isolatieMaterial);
|
|
||||||
mesh.position.set(((bay.left + bay.right) / 2) * toM, joistHM / 2, 0);
|
|
||||||
mesh.visible = isolatieVisible;
|
|
||||||
floorGroup3D.add(mesh);
|
|
||||||
});
|
|
||||||
|
|
||||||
// C. OSB-ONDERLAAG — één geëxtrudeerd vlak over de volle vloer, met
|
// C. OSB-ONDERLAAG — één geëxtrudeerd vlak over de volle vloer, met
|
||||||
// ronde uitsparingen voor de doorvoeren. Shape wordt opgebouwd in
|
// ronde uitsparingen voor de doorvoeren. Shape wordt opgebouwd in
|
||||||
|
|||||||
+14
-2
@@ -46,8 +46,15 @@ window.FloorPdf = (() => {
|
|||||||
doc.setLineWidth(0.15);
|
doc.setLineWidth(0.15);
|
||||||
doc.line(tblX[0], ty, tblX[4] + 12, ty);
|
doc.line(tblX[0], ty, tblX[4] + 12, ty);
|
||||||
ty += 4;
|
ty += 4;
|
||||||
[String(1), String(joistW), String(joistH), String(Math.round(depth)), String(joists.length)]
|
const rims = calcData.rims || {}, joistLen = calcData.joistLen || depth;
|
||||||
|
const rimCount = (rims.front ? 1 : 0) + (rims.back ? 1 : 0);
|
||||||
|
[String(1), String(joistW), String(joistH), String(Math.round(joistLen)), String(joists.length)]
|
||||||
.forEach((v, i) => doc.text(v, tblX[i], ty));
|
.forEach((v, i) => doc.text(v, tblX[i], ty));
|
||||||
|
if (rimCount) {
|
||||||
|
ty += 4.3; // kopbalk(en) op een naad (FloorModel.computeFrame)
|
||||||
|
[String(2), String(joistW), String(joistH), String(Math.round(width)), String(rimCount)]
|
||||||
|
.forEach((v, i) => doc.text(v, tblX[i], ty));
|
||||||
|
}
|
||||||
|
|
||||||
// --- Plaatmateriaal + isolatie ---
|
// --- Plaatmateriaal + isolatie ---
|
||||||
let my = 240;
|
let my = 240;
|
||||||
@@ -89,11 +96,16 @@ window.FloorPdf = (() => {
|
|||||||
doc.setLineWidth(0.3);
|
doc.setLineWidth(0.3);
|
||||||
doc.setTextColor(...BLACK);
|
doc.setTextColor(...BLACK);
|
||||||
doc.setFontSize(5);
|
doc.setFontSize(5);
|
||||||
|
const jz0 = rims.front ? joistW : 0;
|
||||||
joists.forEach(x => {
|
joists.forEach(x => {
|
||||||
const xLeft = x + width / 2 - joistW / 2;
|
const xLeft = x + width / 2 - joistW / 2;
|
||||||
doc.rect(px(xLeft), py(0), joistW * scale, depth * scale, 'S');
|
doc.rect(px(xLeft), py(jz0), joistW * scale, joistLen * scale, 'S');
|
||||||
doc.text('1', px(xLeft + joistW / 2), py(depth / 2), { align: 'center' });
|
doc.text('1', px(xLeft + joistW / 2), py(depth / 2), { align: 'center' });
|
||||||
});
|
});
|
||||||
|
[['front', 0], ['back', depth - joistW]].filter(([k]) => rims[k]).forEach(([, z]) => {
|
||||||
|
doc.rect(px(0), py(z), width * scale, joistW * scale, 'S');
|
||||||
|
doc.text('2', px(width / 2), py(z + joistW / 2) + 1.2, { align: 'center' });
|
||||||
|
});
|
||||||
|
|
||||||
doc.setDrawColor(...RED);
|
doc.setDrawColor(...RED);
|
||||||
doc.setLineWidth(0.2);
|
doc.setLineWidth(0.2);
|
||||||
|
|||||||
+83
-81
@@ -32,11 +32,11 @@ const FLOOR_TEMPLATE = `
|
|||||||
<div id="floor-left-column">
|
<div id="floor-left-column">
|
||||||
<div id="floor-pane-2d-container">
|
<div id="floor-pane-2d-container">
|
||||||
<div class="pane-2d-sub" id="floor-pane-top">
|
<div class="pane-2d-sub" id="floor-pane-top">
|
||||||
<div class="pane-label">2D BOVENAANZICHT</div>
|
<div class="pane-label">BOVENAANZICHT</div>
|
||||||
<canvas id="floor-canvas-top"></canvas>
|
<canvas id="floor-canvas-top"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="pane-2d-sub" id="floor-pane-section">
|
<div class="pane-2d-sub" id="floor-pane-section">
|
||||||
<div class="pane-label">DOORSNEDE (BALKPROFIEL)</div>
|
<div class="pane-label">DOORSNEDE</div>
|
||||||
<canvas id="floor-canvas-section"></canvas>
|
<canvas id="floor-canvas-section"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,59 +46,59 @@ const FLOOR_TEMPLATE = `
|
|||||||
<span id="f-io-status" class="io-status"></span>
|
<span id="f-io-status" class="io-status"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-bar">
|
<div class="tab-bar">
|
||||||
<button class="tab-btn active" data-tab="floor-tab-main">1. Vloer Opbouw</button>
|
<button class="tab-btn active" data-tab="floor-tab-main"><i class="bi bi-rulers"></i> Opbouw</button>
|
||||||
<button class="tab-btn" data-tab="floor-tab-openings">2. Doorvoeren</button>
|
<button class="tab-btn" data-tab="floor-tab-openings"><i class="bi bi-bullseye"></i> Doorvoeren</button>
|
||||||
<button class="tab-btn" data-tab="floor-tab-chassis">3. Chassis</button>
|
<button class="tab-btn" data-tab="floor-tab-chassis"><i class="bi bi-truck"></i> Chassis</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="floor-tab-main" class="tab-content active">
|
<div id="floor-tab-main" class="tab-content active">
|
||||||
<div class="controls-grid">
|
<div class="controls-grid">
|
||||||
<div>
|
<div>
|
||||||
<h1>Vloer Dimensies (mm)</h1>
|
<h1><i class="bi bi-rulers"></i> Afmetingen</h1>
|
||||||
<div class="control-group"><label>Breedte:</label>
|
<div class="control-group"><label>Breedte</label>
|
||||||
<div><input type="number" id="f-inp-width" value="2000" step="50"><span class="unit">mm</span></div></div>
|
<div><input type="number" id="f-inp-width" value="2000" step="50"><span class="unit">mm</span></div></div>
|
||||||
<div class="control-group"><label>Diepte:</label>
|
<div class="control-group"><label>Diepte</label>
|
||||||
<div><input type="number" id="f-inp-depth" value="2000" step="50"><span class="unit">mm</span></div></div>
|
<div><input type="number" id="f-inp-depth" value="2000" step="50"><span class="unit">mm</span></div></div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<h1>Vloerbalken</h1>
|
<h1><i class="bi bi-columns-gap"></i> Vloerbalken</h1>
|
||||||
<div class="control-group"><label>Balk Breedte:</label>
|
<div class="control-group"><label>Breedte</label>
|
||||||
<div><input type="number" id="f-inp-joist-w" value="70" step="5"><span class="unit">mm</span></div></div>
|
<div><input type="number" id="f-inp-joist-w" value="70" step="5"><span class="unit">mm</span></div></div>
|
||||||
<div class="control-group"><label>Balk Hoogte:</label>
|
<div class="control-group"><label>Hoogte</label>
|
||||||
<div><input type="number" id="f-inp-joist-h" value="220" step="5"><span class="unit">mm</span></div></div>
|
<div><input type="number" id="f-inp-joist-h" value="220" step="5"><span class="unit">mm</span></div></div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<h1>Onderlaag (OSB-plaat)</h1>
|
<h1><i class="bi bi-layers"></i> Onderlaag OSB</h1>
|
||||||
<div class="control-group"><label>Dikte:</label>
|
<div class="control-group"><label>Dikte</label>
|
||||||
<div><input type="number" id="f-inp-underlay-t" value="22" step="1" min="3" max="40"><span class="unit">mm</span></div></div>
|
<div><input type="number" id="f-inp-underlay-t" value="22" step="1" min="3" max="40"><span class="unit">mm</span></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1>Regelwerk Afstand (h.o.h.)</h1>
|
<h1><i class="bi bi-arrows"></i> Regelwerk</h1>
|
||||||
<div class="control-group"><label>Min. h.o.h.:</label>
|
<div class="control-group"><label>Afstand min.</label>
|
||||||
<div><input type="number" id="f-inp-min-sp" value="300" step="10"><span class="unit">mm</span></div></div>
|
<div><input type="number" id="f-inp-min-sp" value="300" step="10"><span class="unit">mm</span></div></div>
|
||||||
<div class="control-group"><label>Max. h.o.h.:</label>
|
<div class="control-group"><label>Afstand max.</label>
|
||||||
<div><input type="number" id="f-inp-max-sp" value="600" step="10"><span class="unit">mm</span></div></div>
|
<div><input type="number" id="f-inp-max-sp" value="600" step="10"><span class="unit">mm</span></div></div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="result-row"><span>Totaal vloerbalken:</span><span data-spec="joist-count" class="highlight">-</span></div>
|
<div class="result-row"><span>Vloerbalken</span><span data-spec="joist-count" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Vloeroppervlak:</span><span data-spec="floor-area" class="highlight">-</span></div>
|
<div class="result-row"><span>Vloeroppervlak</span><span data-spec="floor-area" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Volume balkhout:</span><span data-spec="joist-m3" class="highlight">-</span></div>
|
<div class="result-row"><span>Volume balkhout</span><span data-spec="joist-m3" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Gewicht balkhout:</span><span data-spec="joist-kg" class="highlight">-</span></div>
|
<div class="result-row"><span>Gewicht balkhout</span><span data-spec="joist-kg" class="highlight">-</span></div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="result-row"><span>OSB-plaat oppervlak:</span><span data-spec="osb-m2" class="highlight">-</span></div>
|
<div class="result-row"><span>OSB oppervlak</span><span data-spec="osb-m2" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>OSB-plaat volume:</span><span data-spec="osb-m3" class="highlight">-</span></div>
|
<div class="result-row"><span>OSB volume</span><span data-spec="osb-m3" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>OSB-plaat gewicht:</span><span data-spec="osb-kg" class="highlight">-</span></div>
|
<div class="result-row"><span>OSB gewicht</span><span data-spec="osb-kg" class="highlight">-</span></div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="result-row"><span>Isolatie-inhoud (leegte):</span><span data-spec="void-m3" class="highlight">-</span></div>
|
<div class="result-row"><span>Isolatie</span><span data-spec="void-m3" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Totaal gewicht vloer:</span><span data-spec="total-kg" class="highlight">-</span></div>
|
<div class="result-row"><span>Totaal gewicht</span><span data-spec="total-kg" class="highlight">-</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="floor-tab-openings" class="tab-content">
|
<div id="floor-tab-openings" class="tab-content">
|
||||||
<button id="f-btn-add-opening" class="add-opening-btn">+ Doorvoer Toevoegen</button>
|
<button id="f-btn-add-opening" class="add-opening-btn"><i class="bi bi-plus-lg"></i> Doorvoer toevoegen</button>
|
||||||
<div id="f-openings-global-warn" class="global-warning" style="display:none;"></div>
|
<div id="f-openings-global-warn" class="global-warning" style="display:none;"></div>
|
||||||
<div id="f-openings-list"></div>
|
<div id="f-openings-list"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,13 +109,13 @@ const FLOOR_TEMPLATE = `
|
|||||||
</div>
|
</div>
|
||||||
<div class="controls-grid">
|
<div class="controls-grid">
|
||||||
<div>
|
<div>
|
||||||
<div class="control-group"><label>Wieldiameter:</label>
|
<div class="control-group"><label>Wieldiameter</label>
|
||||||
<div><input type="number" id="f-inp-wheel-d" value="400" step="10" min="50"><span class="unit">mm</span></div></div>
|
<div><input type="number" id="f-inp-wheel-d" value="400" step="10" min="50"><span class="unit">mm</span></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="control-group"><label>Spoorbreedte (X):</label>
|
<div class="control-group"><label>Spoorbreedte</label>
|
||||||
<div><input type="number" id="f-inp-track" value="1200" step="50" min="100"><span class="unit">mm</span></div></div>
|
<div><input type="number" id="f-inp-track" value="1200" step="50" min="100"><span class="unit">mm</span></div></div>
|
||||||
<div class="control-group"><label>Wielbasis (Y):</label>
|
<div class="control-group"><label>Wielbasis</label>
|
||||||
<div><input type="number" id="f-inp-wheelbase" value="1200" step="50" min="100"><span class="unit">mm</span></div></div>
|
<div><input type="number" id="f-inp-wheelbase" value="1200" step="50" min="100"><span class="unit">mm</span></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +126,7 @@ const FLOOR_TEMPLATE = `
|
|||||||
|
|
||||||
<div id="floor-right-column">
|
<div id="floor-right-column">
|
||||||
<div id="floor-pane-3d">
|
<div id="floor-pane-3d">
|
||||||
<div class="pane-label">3D PERSPECTIEF</div>
|
<div class="pane-label">3D</div>
|
||||||
<div class="toolbar-3d" id="floor-toolbar-3d"></div>
|
<div class="toolbar-3d" id="floor-toolbar-3d"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -134,27 +134,27 @@ const FLOOR_TEMPLATE = `
|
|||||||
<div class="controls-grid">
|
<div class="controls-grid">
|
||||||
<div>
|
<div>
|
||||||
<div class="spec-section">
|
<div class="spec-section">
|
||||||
<h1>Balken</h1>
|
<h1><i class="bi bi-columns-gap"></i> Balken</h1>
|
||||||
<div class="result-row"><span>Vloerbalken:</span><span data-spec="joist-count" class="highlight">-</span></div>
|
<div class="result-row"><span>Vloerbalken</span><span data-spec="joist-count" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Vloeroppervlak:</span><span data-spec="floor-area" class="highlight">-</span></div>
|
<div class="result-row"><span>Vloeroppervlak</span><span data-spec="floor-area" class="highlight">-</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="spec-section">
|
<div class="spec-section">
|
||||||
<h1>Houtwerk</h1>
|
<h1><i class="bi bi-tree"></i> Houtwerk</h1>
|
||||||
<div class="result-row"><span>Volume balkhout:</span><span data-spec="joist-m3" class="highlight">-</span></div>
|
<div class="result-row"><span>Volume balkhout</span><span data-spec="joist-m3" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Gewicht balkhout:</span><span data-spec="joist-kg" class="highlight">-</span></div>
|
<div class="result-row"><span>Gewicht balkhout</span><span data-spec="joist-kg" class="highlight">-</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="spec-section">
|
<div class="spec-section">
|
||||||
<h1>OSB-plaat (onderlaag, 22mm)</h1>
|
<h1><i class="bi bi-layers"></i> OSB onderlaag</h1>
|
||||||
<div class="result-row"><span>Oppervlak:</span><span data-spec="osb-m2" class="highlight">-</span></div>
|
<div class="result-row"><span>Oppervlak</span><span data-spec="osb-m2" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Volume:</span><span data-spec="osb-m3" class="highlight">-</span></div>
|
<div class="result-row"><span>Volume</span><span data-spec="osb-m3" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Gewicht:</span><span data-spec="osb-kg" class="highlight">-</span></div>
|
<div class="result-row"><span>Gewicht</span><span data-spec="osb-kg" class="highlight">-</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="spec-section">
|
<div class="spec-section">
|
||||||
<h1>Isolatie & totaal</h1>
|
<h1><i class="bi bi-speedometer2"></i> Isolatie en gewicht</h1>
|
||||||
<div class="result-row"><span>Inhoud leegtes:</span><span data-spec="void-m3" class="highlight">-</span></div>
|
<div class="result-row"><span>Isolatie</span><span data-spec="void-m3" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Totaal gewicht vloer:</span><span data-spec="total-kg" class="highlight">-</span></div>
|
<div class="result-row"><span>Totaal gewicht</span><span data-spec="total-kg" class="highlight">-</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,23 +170,23 @@ const FLOOR_TEMPLATE = `
|
|||||||
</div>
|
</div>
|
||||||
<div class="controls-grid">
|
<div class="controls-grid">
|
||||||
<div>
|
<div>
|
||||||
<div class="control-group"><label for="inp-proj-naam">Projectnaam:</label>
|
<div class="control-group"><label for="inp-proj-naam">Projectnaam</label>
|
||||||
<div><input type="text" id="inp-proj-naam" value=""></div></div>
|
<div><input type="text" id="inp-proj-naam" value=""></div></div>
|
||||||
<div class="control-group"><label for="inp-proj-plaats">Plaats:</label>
|
<div class="control-group"><label for="inp-proj-plaats">Plaats</label>
|
||||||
<div><input type="text" id="inp-proj-plaats" value=""></div></div>
|
<div><input type="text" id="inp-proj-plaats" value=""></div></div>
|
||||||
<div class="control-group"><label for="inp-proj-straat">Straat:</label>
|
<div class="control-group"><label for="inp-proj-straat">Straat</label>
|
||||||
<div><input type="text" id="inp-proj-straat" value=""></div></div>
|
<div><input type="text" id="inp-proj-straat" value=""></div></div>
|
||||||
<div class="control-group"><label for="inp-proj-nummer">Projectnummer:</label>
|
<div class="control-group"><label for="inp-proj-nummer">Projectnummer</label>
|
||||||
<div><input type="text" id="inp-proj-nummer" value=""></div></div>
|
<div><input type="text" id="inp-proj-nummer" value=""></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="control-group"><label for="inp-proj-tekenaar">Tekenaar:</label>
|
<div class="control-group"><label for="inp-proj-tekenaar">Tekenaar</label>
|
||||||
<div><input type="text" id="inp-proj-tekenaar" value=""></div></div>
|
<div><input type="text" id="inp-proj-tekenaar" value=""></div></div>
|
||||||
<div class="control-group"><label for="inp-proj-titel">Titel:</label>
|
<div class="control-group"><label for="inp-proj-titel">Titel</label>
|
||||||
<div><input type="text" id="inp-proj-titel" value="Vloer"></div></div>
|
<div><input type="text" id="inp-proj-titel" value="Vloer"></div></div>
|
||||||
<div class="control-group"><label for="inp-proj-element">Element nummer:</label>
|
<div class="control-group"><label for="inp-proj-element">Elementnummer</label>
|
||||||
<div><input type="text" id="inp-proj-element" value=""></div></div>
|
<div><input type="text" id="inp-proj-element" value=""></div></div>
|
||||||
<div class="control-group"><label for="inp-proj-opmerking">Opmerking:</label>
|
<div class="control-group"><label for="inp-proj-opmerking">Opmerking</label>
|
||||||
<div><input type="text" id="inp-proj-opmerking" value=""></div></div>
|
<div><input type="text" id="inp-proj-opmerking" value=""></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,18 +203,16 @@ const FLOOR_TEMPLATE = `
|
|||||||
// (RoofModel). Zelfde editor, andere teksten + gespiegelde doorsnede/3D.
|
// (RoofModel). Zelfde editor, andere teksten + gespiegelde doorsnede/3D.
|
||||||
function floorTemplateForRoof() {
|
function floorTemplateForRoof() {
|
||||||
const r = [
|
const r = [
|
||||||
['1. Vloer Opbouw', '1. Dak Opbouw'],
|
[' Vloerbalken</h1>', ' Dakbalken</h1>'],
|
||||||
['Vloer Dimensies (mm)', 'Dak Dimensies (mm)'],
|
['<span>Vloerbalken</span>', '<span>Dakbalken</span>'],
|
||||||
['<h1>Vloerbalken</h1>', '<h1>Dakbalken</h1>'],
|
[' Onderlaag OSB</h1>', ' Plafondplaat OSB</h1>'],
|
||||||
['Onderlaag (OSB-plaat)', 'Plafondplaat (OSB, onderzijde)'],
|
[' OSB onderlaag</h1>', ' OSB plafond</h1>'],
|
||||||
['Totaal vloerbalken:', 'Totaal dakbalken:'],
|
['<button class="tab-btn" data-tab="floor-tab-chassis"><i class="bi bi-truck"></i> Chassis</button>', ''],
|
||||||
['OSB-plaat (onderlaag, 22mm)', 'OSB-plaat (plafond)'],
|
[' Isolatie en gewicht</h1>', ' Dakleer, isolatie en gewicht</h1>\n <div class="result-row"><span>Dakleer oppervlak</span><span data-spec="dakleer-m2" class="highlight">-</span></div>\n <div class="result-row"><span>Dakleer gewicht</span><span data-spec="dakleer-kg" class="highlight">-</span></div>']
|
||||||
['<button class="tab-btn" data-tab="floor-tab-chassis">3. Chassis</button>', ''],
|
|
||||||
['<h1>Isolatie & totaal</h1>', '<h1>Dakleer, isolatie & totaal</h1>\n <div class="result-row"><span>Dakleer oppervlak:</span><span data-spec="dakleer-m2" class="highlight">-</span></div>\n <div class="result-row"><span>Dakleer gewicht:</span><span data-spec="dakleer-kg" class="highlight">-</span></div>']
|
|
||||||
];
|
];
|
||||||
let t = FLOOR_TEMPLATE;
|
let t = FLOOR_TEMPLATE;
|
||||||
r.forEach(([a, b]) => { t = t.split(a).join(b); });
|
r.forEach(([a, b]) => { t = t.split(a).join(b); });
|
||||||
return t.split('Vloeroppervlak:').join('Dakoppervlak:').split('Totaal gewicht vloer:').join('Totaal gewicht dak:');
|
return t.split('Vloeroppervlak').join('Dakoppervlak');
|
||||||
}
|
}
|
||||||
|
|
||||||
function mountFloorEditor(container, callbacks) {
|
function mountFloorEditor(container, callbacks) {
|
||||||
@@ -262,9 +260,9 @@ function mountFloorEditor(container, callbacks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function anchorLabel(a) {
|
function anchorLabel(a) {
|
||||||
if (a === 'right') return 'Afstand vanaf oostzijde (X)';
|
if (a === 'right') return 'Afstand tot oostzijde';
|
||||||
if (a === 'middle') return 'Verschuiving t.o.v. midden (X)';
|
if (a === 'middle') return 'Verschuiving t.o.v. midden';
|
||||||
return 'Afstand vanaf westzijde (X)';
|
return 'Afstand tot westzijde';
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderOpeningsList() {
|
function renderOpeningsList() {
|
||||||
@@ -277,13 +275,13 @@ function mountFloorEditor(container, callbacks) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="controls-grid">
|
<div class="controls-grid">
|
||||||
<div>
|
<div>
|
||||||
<div class="control-group"><label>Diameter:</label>
|
<div class="control-group"><label>Diameter</label>
|
||||||
<div><input type="number" data-field="diameter" data-id="${op.id}" value="${op.diameter}" step="5" min="10"><span class="unit">mm</span></div></div>
|
<div><input type="number" data-field="diameter" data-id="${op.id}" value="${op.diameter}" step="5" min="10"><span class="unit">mm</span></div></div>
|
||||||
<div class="control-group"><label>Afstand vanaf noordzijde (Y):</label>
|
<div class="control-group"><label>Afstand tot noordzijde</label>
|
||||||
<div><input type="number" data-field="offsetY" data-id="${op.id}" value="${op.offsetY}" step="50"><span class="unit">mm</span></div></div>
|
<div><input type="number" data-field="offsetY" data-id="${op.id}" value="${op.offsetY}" step="50"><span class="unit">mm</span></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="control-group"><label>Anker (X):</label>
|
<div class="control-group"><label>Anker</label>
|
||||||
<div>
|
<div>
|
||||||
<select data-field="anchorX" data-id="${op.id}">
|
<select data-field="anchorX" data-id="${op.id}">
|
||||||
<option value="left" ${op.anchorX === 'left' ? 'selected' : ''}>West</option>
|
<option value="left" ${op.anchorX === 'left' ? 'selected' : ''}>West</option>
|
||||||
@@ -292,7 +290,7 @@ function mountFloorEditor(container, callbacks) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group"><label>${anchorLabel(op.anchorX)}:</label>
|
<div class="control-group"><label>${anchorLabel(op.anchorX)}</label>
|
||||||
<div><input type="number" data-field="offsetX" data-id="${op.id}" value="${op.offsetX}" step="50"><span class="unit">mm</span></div></div>
|
<div><input type="number" data-field="offsetX" data-id="${op.id}" value="${op.offsetX}" step="50"><span class="unit">mm</span></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -702,18 +700,19 @@ function mountFloorEditor(container, callbacks) {
|
|||||||
controls.update();
|
controls.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
const floorAmbient = new THREE.AmbientLight(0xffffff, 0.6);
|
||||||
|
scene.add(floorAmbient);
|
||||||
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
dirLight.position.set(5, 8, 5);
|
dirLight.position.set(5, 8, 5);
|
||||||
dirLight.castShadow = true;
|
dirLight.castShadow = true;
|
||||||
scene.add(dirLight);
|
scene.add(dirLight);
|
||||||
|
|
||||||
scene.add(new THREE.GridHelper(10, 20, 0x444444, 0x222222));
|
const grid = new THREE.GridHelper(10, 20, 0x444444, 0x222222);
|
||||||
|
scene.add(grid);
|
||||||
|
|
||||||
const floorMaterials = roofMode ? RoofModel.createMaterials() : FloorModel.createMaterials();
|
const floorMaterials = roofMode ? RoofModel.createMaterials() : FloorModel.createMaterials();
|
||||||
|
|
||||||
let osbVisible = true;
|
let osbVisible = true;
|
||||||
let isolatieVisible = true;
|
|
||||||
let floorGroup3D = null;
|
let floorGroup3D = null;
|
||||||
let lastResolvedOpenings = [];
|
let lastResolvedOpenings = [];
|
||||||
let lastChassisData = { wheels: [], outOfBounds: false };
|
let lastChassisData = { wheels: [], outOfBounds: false };
|
||||||
@@ -721,20 +720,27 @@ function mountFloorEditor(container, callbacks) {
|
|||||||
// Weergave-filter (core/view-filter.js). Beplating = OSB-plaat (bij een
|
// Weergave-filter (core/view-filter.js). Beplating = OSB-plaat (bij een
|
||||||
// dak: plafondplaat + dakleer).
|
// dak: plafondplaat + dakleer).
|
||||||
createViewFilter(document.getElementById('floor-toolbar-3d'), [
|
createViewFilter(document.getElementById('floor-toolbar-3d'), [
|
||||||
{ key: 'beplating', label: roofMode ? 'Beplating (plafond + dakleer)' : 'Beplating (OSB-plaat)' },
|
{ key: 'beplating', label: roofMode ? 'Beplating (plafond + dakleer)' : 'Beplating (OSB-plaat)' }
|
||||||
{ key: 'isolatie', label: 'Isolatie' }
|
|
||||||
], (v) => {
|
], (v) => {
|
||||||
osbVisible = v.beplating;
|
osbVisible = v.beplating;
|
||||||
isolatieVisible = v.isolatie;
|
|
||||||
update3DScene();
|
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 = () => {
|
||||||
|
skyEnv.fitTo(floorGroup3D, 0);
|
||||||
|
Skybox.apply(skyMode, { env: skyEnv, grid, controls, sun: dirLight, ambient: floorAmbient, base: { ambient: 0.6, sun: 0.8 }, shadowRoot: floorGroup3D });
|
||||||
|
};
|
||||||
|
Skybox.createToggle(document.getElementById('floor-toolbar-3d'), (m) => { skyMode = m; applySky(); });
|
||||||
|
|
||||||
function update3DScene() {
|
function update3DScene() {
|
||||||
if (floorGroup3D) scene.remove(floorGroup3D);
|
if (floorGroup3D) scene.remove(floorGroup3D);
|
||||||
floorGroup3D = roofMode
|
floorGroup3D = roofMode
|
||||||
? RoofModel.build3D(calcData, lastResolvedOpenings, { materials: floorMaterials, osbVisible, isolatieVisible, dakleerVisible: osbVisible })
|
? RoofModel.build3D(calcData, lastResolvedOpenings, { materials: floorMaterials, osbVisible, dakleerVisible: osbVisible })
|
||||||
: FloorModel.build3D(calcData, lastResolvedOpenings, lastChassisData, { materials: floorMaterials, osbVisible, isolatieVisible });
|
: FloorModel.build3D(calcData, lastResolvedOpenings, lastChassisData, { materials: floorMaterials, osbVisible });
|
||||||
scene.add(floorGroup3D);
|
scene.add(floorGroup3D);
|
||||||
|
applySky(); // schaduw van de nieuwe vloer
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
@@ -778,15 +784,11 @@ function mountFloorEditor(container, callbacks) {
|
|||||||
window.addEventListener('resize', onWindowResize);
|
window.addEventListener('resize', onWindowResize);
|
||||||
|
|
||||||
function toggleFullscreen3D() {
|
function toggleFullscreen3D() {
|
||||||
const el = document.getElementById('floor-app');
|
UI.cycle3D(document.getElementById('floor-app')); // T: standaard -> 3D groot -> alleen render
|
||||||
el.classList.remove('fullscreen-2d');
|
|
||||||
el.classList.toggle('fullscreen-3d');
|
|
||||||
requestAnimationFrame(onWindowResize);
|
requestAnimationFrame(onWindowResize);
|
||||||
}
|
}
|
||||||
function toggleFullscreen2D() {
|
function toggleFullscreen2D() {
|
||||||
const el = document.getElementById('floor-app');
|
UI.toggle2D(document.getElementById('floor-app'));
|
||||||
el.classList.remove('fullscreen-3d');
|
|
||||||
el.classList.toggle('fullscreen-2d');
|
|
||||||
requestAnimationFrame(onWindowResize);
|
requestAnimationFrame(onWindowResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+355
-62
@@ -65,6 +65,7 @@ window.HouseModel = (() => {
|
|||||||
// vanaf het beloopbare vloervlak (WallModel.trasraamFor).
|
// vanaf het beloopbare vloervlak (WallModel.trasraamFor).
|
||||||
trasraam: { enabled: true, height: WallModel.TRASRAAM_DEFAULT_MM },
|
trasraam: { enabled: true, height: WallModel.TRASRAAM_DEFAULT_MM },
|
||||||
sandwich: true, // sandwichpanelen rondom (SandwichModel), vanaf loopvlak 600 mm de grond in
|
sandwich: true, // sandwichpanelen rondom (SandwichModel), vanaf loopvlak 600 mm de grond in
|
||||||
|
groundLevel: 350, // maaiveld: zoveel mm onder het loopvlak (bovenkant vloer); voor de 3D-omgeving
|
||||||
roof: Object.assign({}, RoofModel.DEFAULT_SETTINGS),
|
roof: Object.assign({}, RoofModel.DEFAULT_SETTINGS),
|
||||||
lighting: Object.assign({}, LightingModel.DEFAULT_SETTINGS)
|
lighting: Object.assign({}, LightingModel.DEFAULT_SETTINGS)
|
||||||
};
|
};
|
||||||
@@ -91,6 +92,7 @@ window.HouseModel = (() => {
|
|||||||
{ id: 3, type: 'raam', w: 500, h: 1900, y: 250, anchor: 'right', offset: 450 }
|
{ id: 3, type: 'raam', w: 500, h: 1900, y: 250, anchor: 'right', offset: 450 }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
ensureSeamWalls(state);
|
||||||
computeParts(state, { skip3D: true });
|
computeParts(state, { skip3D: true });
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -207,15 +209,55 @@ window.HouseModel = (() => {
|
|||||||
return wall;
|
return wall;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dikte = regelwerk + 2x beplating (WallModel.thicknessOf); de diepte van
|
// Dikte = regelwerk + beplating (2x, of 1x bij een naadwand-deel met
|
||||||
// stijlen en regels wordt daarop afgestemd.
|
// buitenzijde 'geen'); de diepte van stijlen en regels wordt daarop afgestemd.
|
||||||
|
const skinSides = (wallState) => ((wallState.materials || {}).exterior === 'geen' ? 1 : 2);
|
||||||
function setThickness(wallState, thickness) {
|
function setThickness(wallState, thickness) {
|
||||||
const depth = Math.max(20, Math.round(thickness - 2 * wallState.wall.sheathingThick));
|
const depth = Math.max(20, Math.round(thickness - skinSides(wallState) * wallState.wall.sheathingThick));
|
||||||
wallState.wall.studDepth = depth;
|
wallState.wall.studDepth = depth;
|
||||||
wallState.wall.plateDepth = depth;
|
wallState.wall.plateDepth = depth;
|
||||||
}
|
}
|
||||||
function thicknessOf(wallState) {
|
function thicknessOf(wallState) {
|
||||||
return Math.max(wallState.wall.studDepth, wallState.wall.plateDepth) + 2 * wallState.wall.sheathingThick;
|
return Math.max(wallState.wall.studDepth, wallState.wall.plateDepth) + skinSides(wallState) * wallState.wall.sheathingThick;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Naadwanden: op elke naad tussen twee units staat standaard een tussenwand
|
||||||
|
// met in het midden een deur. Hij bestaat uit twee delen, één per unit
|
||||||
|
// (de modules worden los vervoerd), die met hun regelwerk tegen elkaar aan
|
||||||
|
// staan: balken en isolatie in beide delen, beplating alleen aan de
|
||||||
|
// kamerzijde (buitenMateriaal 'geen' aan de naadzijde). Eén wand-state
|
||||||
|
// (hoogte, openingen) voor beide delen; de openingen gelden voor het deel
|
||||||
|
// in unit `seam.a` en worden in het andere deel gespiegeld, zodat de deur
|
||||||
|
// recht door beide delen gaat. De gebruiker kan hem aanpassen of
|
||||||
|
// verwijderen; alleen bij een nieuwe woning en bij "unit toevoegen" worden
|
||||||
|
// ontbrekende naadwanden aangemaakt (ensureSeamWalls).
|
||||||
|
// state.interiorWalls: { id, seam: { a, b }, wall }
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
const DEFAULT_SEAM_LEAF_MM = 100; // dikte per deel (regelwerk + 1x beplating)
|
||||||
|
function seamWallDefaults(height) {
|
||||||
|
const wall = WallEditor.getDefaultState();
|
||||||
|
wall.wall.heightAll = wall.wall.heightLeft = wall.wall.heightRight = height || wall.wall.heightAll;
|
||||||
|
wall.materials = { interior: 'spaanplaat', exterior: 'geen' };
|
||||||
|
const d = WallModel.OPENING_TYPES.deur;
|
||||||
|
wall.openings = [{ id: 1, type: 'deur', w: d.w, h: d.h, y: d.y, anchor: 'middle', offset: 0 }];
|
||||||
|
setThickness(wall, DEFAULT_SEAM_LEAF_MM);
|
||||||
|
return wall;
|
||||||
|
}
|
||||||
|
const sameSeam = (s, a, b) => (s.a === a && s.b === b) || (s.a === b && s.b === a);
|
||||||
|
// Maakt ontbrekende naadwanden aan (alleen naden met onlyUnitId, indien gegeven).
|
||||||
|
function ensureSeamWalls(state, onlyUnitId) {
|
||||||
|
const { seams } = computeSeams(unitRects(state));
|
||||||
|
seams.forEach(sm => {
|
||||||
|
if (onlyUnitId && sm.a !== onlyUnitId && sm.b !== onlyUnitId) return;
|
||||||
|
if (state.interiorWalls.some(iw => iw.seam && sameSeam(iw.seam, sm.a, sm.b))) return;
|
||||||
|
const id = state.interiorWalls.reduce((m, w) => Math.max(m, w.id), 0) + 1;
|
||||||
|
const height = Math.min(...[sm.a, sm.b].map(uid => {
|
||||||
|
const w = state.units.find(u => u.id === uid).unit.walls.voor.wall;
|
||||||
|
return Math.min(w.heightLeft, w.heightRight);
|
||||||
|
}));
|
||||||
|
state.interiorWalls.push({ id, seam: { a: sm.a, b: sm.b }, wall: seamWallDefaults(height) });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function interiorLayout(iw) {
|
function interiorLayout(iw) {
|
||||||
@@ -232,7 +274,7 @@ window.HouseModel = (() => {
|
|||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
// Alle onderdelen doorgerekend (+ 3D, tenzij opts.skip3D).
|
// Alle onderdelen doorgerekend (+ 3D, tenzij opts.skip3D).
|
||||||
// opts: { wallMaterials, floorMaterials, beplatingVisible, isolatieVisible, skip3D }
|
// opts: { wallMaterials, floorMaterials, beplatingVisible, skip3D }
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
// Past de woning-brede instellingen toe op de onderdeel-states (muteert
|
// Past de woning-brede instellingen toe op de onderdeel-states (muteert
|
||||||
// state): h.o.h. van alle wanden, en de dakmaat als die de vloer volgt.
|
// state): h.o.h. van alle wanden, en de dakmaat als die de vloer volgt.
|
||||||
@@ -261,22 +303,33 @@ window.HouseModel = (() => {
|
|||||||
// { u0, extStart, isEnd } (zie WallModel.computeBattens): u0 = afstand
|
// { u0, extStart, isEnd } (zie WallModel.computeBattens): u0 = afstand
|
||||||
// langs het wandvlak van het gevelbegin (links, van buitenaf gezien, incl.
|
// langs het wandvlak van het gevelbegin (links, van buitenaf gezien, incl.
|
||||||
// hoekdoorloop van het eerste stuk) tot de linkerrand van het stuk.
|
// hoekdoorloop van het eerste stuk) tot de linkerrand van het stuk.
|
||||||
function assignFacades(units) {
|
// pieces: buitenwandstukken + naadwanddelen met een blootliggend buitendeel.
|
||||||
|
function assignFacades(pieces) {
|
||||||
const segs = [];
|
const segs = [];
|
||||||
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => {
|
pieces.forEach(p => {
|
||||||
const W = p.place.width, T = p.place.thickness;
|
const W = p.place.width, T = p.place.thickness;
|
||||||
const normal = { x: p.place.normal.x, z: p.place.normal.y };
|
const normal = { x: p.place.normal.x, z: p.place.normal.y };
|
||||||
const axis = Math.abs(normal.z) > 0.5 ? 'x' : 'z', perp = axis === 'x' ? 'z' : 'x';
|
const axis = Math.abs(normal.z) > 0.5 ? 'x' : 'z', perp = axis === 'x' ? 'z' : 'x';
|
||||||
const q0 = UnitModel.localToWorld(p.place, -W / 2, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2, T / 2);
|
const q0 = UnitModel.localToWorld(p.place, -W / 2, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2, T / 2);
|
||||||
segs.push({ piece: p, axis, normal, line: q0[perp], sAt0: q0[axis], sAtW: q1[axis], s0: Math.min(q0[axis], q1[axis]), s1: Math.max(q0[axis], q1[axis]) });
|
segs.push({ piece: p, axis, normal, line: q0[perp], sAt0: q0[axis], sAtW: q1[axis], s0: Math.min(q0[axis], q1[axis]), s1: Math.max(q0[axis], q1[axis]) });
|
||||||
})));
|
});
|
||||||
groupFacadeRuns(segs).forEach(run => {
|
groupFacadeRuns(segs).forEach(run => {
|
||||||
const dir = Math.sign(run.segs[0].sAtW - run.segs[0].sAt0) || 1;
|
const dir = Math.sign(run.segs[0].sAtW - run.segs[0].sAt0) || 1;
|
||||||
const sLeft = dir > 0 ? run.s0 : run.s1, sRight = dir > 0 ? run.s1 : run.s0;
|
const sLeft = dir > 0 ? run.s0 : run.s1, sRight = dir > 0 ? run.s1 : run.s0;
|
||||||
const first = run.segs.reduce((best, sg) => Math.abs(sg.sAt0 - sLeft) < Math.abs(best.sAt0 - sLeft) ? sg : best);
|
const first = run.segs.reduce((best, sg) => Math.abs(sg.sAt0 - sLeft) < Math.abs(best.sAt0 - sLeft) ? sg : best);
|
||||||
const extLeft = (first.piece.skinExt && first.piece.skinExt.left) || 0;
|
const extLeft = (first.piece.skinExt && first.piece.skinExt.left) || 0;
|
||||||
|
// Horizontaal latten-raster: vanaf het trasraam van een gewoon stuk
|
||||||
|
// (absolute hoogte), zodat opbouw/naadwand-stukken erop aansluiten.
|
||||||
|
const ref = run.segs.find(sg => !sg.piece.place.baseY && sg.piece.skinFrom === undefined);
|
||||||
|
const gridAbs = ref ? ref.piece.place.floorTop + WallModel.skinStartY(ref.piece.trasraam) : undefined;
|
||||||
|
const floorAbs = ref ? ref.piece.place.floorTop : undefined;
|
||||||
run.segs.forEach(sg => {
|
run.segs.forEach(sg => {
|
||||||
sg.piece.facade = { u0: Math.abs(sg.sAt0 - sLeft) + extLeft, extStart: extLeft > 0, isEnd: Math.abs(sg.sAtW - sRight) < 5 };
|
const p = sg.piece;
|
||||||
|
sg.piece.facade = {
|
||||||
|
u0: Math.abs(sg.sAt0 - sLeft) + extLeft, extStart: extLeft > 0, isEnd: Math.abs(sg.sAtW - sRight) < 5,
|
||||||
|
gridY0: gridAbs === undefined ? undefined : gridAbs - p.place.floorTop,
|
||||||
|
yOff: floorAbs === undefined ? 0 : p.place.floorTop - floorAbs
|
||||||
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -291,59 +344,89 @@ window.HouseModel = (() => {
|
|||||||
const wallThickness = (id, side) => WallModel.thicknessOf(WallModel.configFromState(state.units.find(u => u.id === id).unit.walls[side]));
|
const wallThickness = (id, side) => WallModel.thicknessOf(WallModel.configFromState(state.units.find(u => u.id === id).unit.walls[side]));
|
||||||
const { cuts, seams } = computeSeams(rects, wallThickness);
|
const { cuts, seams } = computeSeams(rects, wallThickness);
|
||||||
|
|
||||||
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade) {
|
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow) {
|
||||||
if (!build) return { group: null, stats: null, specs: null };
|
if (!build) return { group: null, stats: null, specs: null };
|
||||||
const built = WallModel.build3D(config, frame, {
|
const built = WallModel.build3D(config, frame, {
|
||||||
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade,
|
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow,
|
||||||
spaanplaatVisible: o.beplatingVisible !== false, buitenVisible: o.beplatingVisible !== false,
|
spaanplaatVisible: o.beplatingVisible !== false, buitenVisible: o.beplatingVisible !== false
|
||||||
isolatieVisible: o.isolatieVisible !== false
|
|
||||||
});
|
});
|
||||||
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
|
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wandstukken van één zijde (UnitModel.wallPieces), met plaats in de
|
||||||
|
// woning. raised: opbouw-zones (zie raiseSeamWalls). Een opbouwstuk
|
||||||
|
// (baseY) staat op die hoogte boven het loopvlak.
|
||||||
|
function wallOf(u, rect, layout, wl, raised) {
|
||||||
|
const ws = u.unit.walls[wl.side];
|
||||||
|
const buitenMateriaal = (ws.materials || {}).exterior || 'vochtdoek';
|
||||||
|
const cladding = (ws.materials || {}).cladding || 'horizontaal';
|
||||||
|
const trasraam = buitenMateriaal === 'vochtdoek' ? ((ws.materials || {}).trasraam || null) : null;
|
||||||
|
const wp = UnitModel.wallPieces(wl, ws, raised);
|
||||||
|
const pieces = wp.pieces.map(p => {
|
||||||
|
const c = UnitModel.localToWorld(wl, p.lxCenter, 0);
|
||||||
|
const base = p.baseY || 0;
|
||||||
|
return Object.assign(p, {
|
||||||
|
element: `${u.id}-W${wl.nr}${p.suffix}`,
|
||||||
|
label: `Unit ${u.id} ${wl.label.toLowerCase()}` + (base ? ` opbouw (vanaf ${base} mm)` : p.suffix ? ` (${p.suffix})` : ''),
|
||||||
|
buitenMateriaal, cladding, trasraam: base ? null : trasraam,
|
||||||
|
place: { cx: rect.cx + c.x, cz: rect.cz + c.z, rotY: wl.rotY, width: p.config.wallWidth, thickness: wl.thickness, normal: wl.normal, floorTop: layout.floorTop + base, baseY: base }
|
||||||
|
}); // 3D volgt na assignFacades (latwerk/doek per gevel)
|
||||||
|
});
|
||||||
|
// Naad-zones met opbouw: in de wand-editor alleen gearceerd tot base.
|
||||||
|
// Naast de opbouw (haakse wand in de hoek) blijft het over de volle hoogte.
|
||||||
|
const cutsU = [];
|
||||||
|
wl.cutsU.forEach(([a, b]) => {
|
||||||
|
const r = pieces.find(p => p.baseY && p.u0 < b - EPS && p.u1 > a + EPS);
|
||||||
|
if (!r) { cutsU.push([a, b]); return; }
|
||||||
|
if (r.u0 > a + EPS) cutsU.push([a, r.u0]);
|
||||||
|
cutsU.push([Math.max(a, r.u0), Math.min(b, r.u1), r.baseY]);
|
||||||
|
if (r.u1 < b - EPS) cutsU.push([r.u1, b]);
|
||||||
|
});
|
||||||
|
return Object.assign({}, wl, { cutsU, pieces, dropped: wp.dropped, removed: pieces.length === 0 });
|
||||||
|
}
|
||||||
|
|
||||||
const units = state.units.map(u => {
|
const units = state.units.map(u => {
|
||||||
const rect = rectById[u.id];
|
const rect = rectById[u.id];
|
||||||
const layout = UnitModel.computeLayout(u.unit, cuts[u.id]);
|
const layout = UnitModel.computeLayout(u.unit, cuts[u.id]);
|
||||||
const floorData = FloorModel.computeFromState(u.unit.floor);
|
// Kopbalk op een naad langs voor-/achterrand (haaks op de balken): daar
|
||||||
|
// worden de modules weer aan elkaar gemonteerd. Zelfde voor het dak.
|
||||||
|
const rims = { front: cuts[u.id].voor.length > 0, back: cuts[u.id].achter.length > 0 };
|
||||||
|
const floorData = FloorModel.computeFromState(u.unit.floor, rims);
|
||||||
const floorSpecs = FloorModel.computeSpecs(floorData.calcData, floorData.resolvedOpenings);
|
const floorSpecs = FloorModel.computeSpecs(floorData.calcData, floorData.resolvedOpenings);
|
||||||
const floorGroup = build ? FloorModel.build3D(floorData.calcData, floorData.resolvedOpenings, floorData.chassisData, {
|
const floorGroup = build ? FloorModel.build3D(floorData.calcData, floorData.resolvedOpenings, floorData.chassisData, {
|
||||||
materials: o.floorMaterials, osbVisible: o.beplatingVisible !== false, isolatieVisible: o.isolatieVisible !== false
|
materials: o.floorMaterials, osbVisible: o.beplatingVisible !== false
|
||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
const walls = layout.walls.map(wl => {
|
const walls = layout.walls.map(wl => wallOf(u, rect, layout, wl));
|
||||||
const ws = u.unit.walls[wl.side];
|
|
||||||
const buitenMateriaal = (ws.materials || {}).exterior || 'vochtdoek';
|
|
||||||
const cladding = (ws.materials || {}).cladding || 'horizontaal';
|
|
||||||
const trasraam = buitenMateriaal === 'vochtdoek' ? ((ws.materials || {}).trasraam || null) : null;
|
|
||||||
const wp = UnitModel.wallPieces(wl, ws);
|
|
||||||
const pieces = wp.pieces.map(p => {
|
|
||||||
const c = UnitModel.localToWorld(wl, p.lxCenter, 0);
|
|
||||||
return Object.assign(p, {
|
|
||||||
element: `${u.id}-W${wl.nr}${p.suffix}`,
|
|
||||||
label: `Unit ${u.id} ${wl.label.toLowerCase()}${p.suffix ? ' (' + p.suffix + ')' : ''}`,
|
|
||||||
buitenMateriaal, cladding, trasraam,
|
|
||||||
place: { cx: rect.cx + c.x, cz: rect.cz + c.z, rotY: wl.rotY, width: p.config.wallWidth, thickness: wl.thickness, normal: wl.normal, floorTop: layout.floorTop }
|
|
||||||
}); // 3D volgt na assignFacades (latwerk/doek per gevel)
|
|
||||||
});
|
|
||||||
return Object.assign({}, wl, { pieces, dropped: wp.dropped, removed: pieces.length === 0 });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dak: dek = vloer-state (180° gespiegeld, RoofModel), op de hoogste
|
// Dak: dek = vloer-state (180° gespiegeld, RoofModel), in de vorm die
|
||||||
// wandbovenkant van deze unit.
|
// de wanden aangeven (plat, lessenaar of zadel; RoofModel.computeShape).
|
||||||
const roofData = FloorModel.computeFromState(u.roof.deck);
|
const shape = RoofModel.computeShape(u.unit, layout);
|
||||||
const roofSpecs = RoofModel.computeSpecs(roofData.calcData, roofData.resolvedOpenings);
|
const deckFrame = FloorModel.computeFrame(u.roof.deck.floor);
|
||||||
const wallTop = Math.max(...Object.values(u.unit.walls).map(ws => Math.max(ws.wall.heightLeft, ws.wall.heightRight)));
|
const roofW = deckFrame.width, roofD = deckFrame.depth;
|
||||||
const roofW = roofData.calcData.width, roofD = roofData.calcData.depth;
|
const ox = u.roof.offsetX || 0, oz = u.roof.offsetZ || 0;
|
||||||
const roofCx = rect.cx + (u.roof.offsetX || 0), roofCz = rect.cz + (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 roofCx = rect.cx + ox, roofCz = rect.cz + oz;
|
||||||
|
const shapeLabel = { plat: 'plat', lessenaar: 'lessenaar', zadel: 'zadel' }[shape.type];
|
||||||
|
const multi = ur.planes.length > 1;
|
||||||
|
ur.planes.forEach((pl, i) => {
|
||||||
|
pl.element = `${u.id}-D${i + 1}`;
|
||||||
|
const deg = Math.round(Math.abs(pl.theta) * 180 / Math.PI * 10) / 10;
|
||||||
|
pl.label = `Unit ${u.id} dak${multi ? ' deel ' + (i + 1) : ''} (${shapeLabel}${deg ? ', ' + String(deg).replace('.', ',') + '°' : ''})`;
|
||||||
|
});
|
||||||
const roof = {
|
const roof = {
|
||||||
element: `${u.id}-D1`, data: roofData, specs: roofSpecs,
|
element: `${u.id}-D1`, shape, planes: ur.planes, specs: ur.specs,
|
||||||
rect: { x0: roofCx - roofW / 2, x1: roofCx + roofW / 2, z0: roofCz - roofD / 2, z1: roofCz + roofD / 2, cx: roofCx, cz: roofCz, W: roofW, D: roofD },
|
rect: { x0: roofCx - roofW / 2, x1: roofCx + roofW / 2, z0: roofCz - roofD / 2, z1: roofCz + roofD / 2, cx: roofCx, cz: roofCz, W: roofW, D: roofD },
|
||||||
base: layout.floorTop + wallTop,
|
base: layout.floorTop, // 3D: group-Y = 0 op het loopvlak
|
||||||
top: layout.floorTop + wallTop + RoofModel.thickness(roofData.calcData),
|
// Onderkant plafond resp. bovenkant dakleer op (x, z) (woning-mm, absoluut).
|
||||||
|
ceilingAt: (x, z) => layout.floorTop + ur.undersideAt(x - rect.cx, z - rect.cz),
|
||||||
|
topAt: (x, z) => layout.floorTop + ur.topAt(x - rect.cx, z - rect.cz),
|
||||||
|
ridges: ur.ridges.map(r => ({ axis: r.axis, value: r.value + (r.axis === 'x' ? rect.cx : rect.cz) })),
|
||||||
|
top: layout.floorTop + ur.maxTop,
|
||||||
// Zichtbaarheid zoals wanden/vloeren: plafondplaat + dakleer (EPDM)
|
// Zichtbaarheid zoals wanden/vloeren: plafondplaat + dakleer (EPDM)
|
||||||
// horen bij "Beplating", de dakisolatie bij "Isolatie".
|
// horen bij "Beplating".
|
||||||
group: build ? RoofModel.build3D(roofData.calcData, roofData.resolvedOpenings, {
|
group: build ? RoofModel.buildUnit3D(ur, {
|
||||||
materials: o.roofMaterials, isolatieVisible: o.isolatieVisible !== false,
|
materials: o.roofMaterials,
|
||||||
osbVisible: o.beplatingVisible !== false,
|
osbVisible: o.beplatingVisible !== false,
|
||||||
dakleerVisible: o.beplatingVisible !== false
|
dakleerVisible: o.beplatingVisible !== false
|
||||||
}) : null
|
}) : null
|
||||||
@@ -355,38 +438,242 @@ window.HouseModel = (() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Doek en latwerk volgen de hele gevel (niet per unit/wandstuk).
|
// Opbouw op een naad: staat een unit met een hogere daklijn tegen een
|
||||||
assignFacades(units);
|
// unit met een lager dak, dan loopt zijn buitenwand aan de naadzijde door
|
||||||
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => {
|
// vanaf de bovenkant van het lage dak tot zijn eigen daklijn (zoals een
|
||||||
Object.assign(p, buildWall(p.config, p.frame, p.buitenMateriaal, p.cladding, p.trasraam, p.skinExt, p.facade));
|
// buitenwand die bij een schuin dak omhoog groeit). Ramen daar teken je
|
||||||
})));
|
// op die buitenwand. De naadwand eronder stopt op die hoogte (raisedCap).
|
||||||
|
// Alleen bij een rechte voet: het lage dak ligt langs de hele naad even
|
||||||
|
// hoog; anders blijft de naadwand doorlopen met buitenafwerking.
|
||||||
|
const raisedCap = {}; // `${uid}|${andere uid}` -> base (mm vanaf loopvlak uid)
|
||||||
|
(function raiseSeamWalls() {
|
||||||
|
const zones = {}; // uid -> side -> [{ u0, u1, base, other }]
|
||||||
|
seams.forEach(sm => {
|
||||||
|
const horiz = Math.abs(sm.z0 - sm.z1) < EPS;
|
||||||
|
const seamPerp = horiz ? sm.z0 : sm.x0;
|
||||||
|
const s0 = horiz ? sm.x0 : sm.z0, s1 = horiz ? sm.x1 : sm.z1;
|
||||||
|
[[sm.a, sm.b, horiz ? 'achter' : 'rechts'], [sm.b, sm.a, horiz ? 'voor' : 'links']].forEach(([uid, oid, side]) => {
|
||||||
|
const u = units.find(x => x.id === uid), other = units.find(x => x.id === oid);
|
||||||
|
const ts = [s0, s1].concat(other.roof.ridges.filter(r => r.axis === (horiz ? 'x' : 'z') && r.value > s0 && r.value < s1).map(r => r.value));
|
||||||
|
const tops = ts.map(t => horiz ? other.roof.topAt(t, seamPerp) : other.roof.topAt(seamPerp, t));
|
||||||
|
if (Math.max(...tops) - Math.min(...tops) > 2) return; // schuine voet: terugval
|
||||||
|
const base = Math.round(tops[0] - u.layout.floorTop);
|
||||||
|
const wl = u.walls.find(w => w.side === side);
|
||||||
|
const sd = UnitModel.WALL_SIDES.find(x => x.key === side);
|
||||||
|
const c0 = sd.axis === 'x' ? u.rect.cx : u.rect.cz;
|
||||||
|
const start = (sd.axis === 'x' ? wl.cx : wl.cz) - wl.width / 2;
|
||||||
|
const toU = (t) => sd.dir > 0 ? t - c0 - start : start + wl.width - (t - c0);
|
||||||
|
const z0 = Math.min(toU(s0), toU(s1)), z1 = Math.max(toU(s0), toU(s1));
|
||||||
|
wl.cutsU.forEach(([a, b]) => {
|
||||||
|
let lo = Math.max(a, z0), hi = Math.min(b, z1);
|
||||||
|
if (lo < EPS) lo = wl.endInset.left;
|
||||||
|
if (hi > wl.width - EPS) hi = wl.width - wl.endInset.right;
|
||||||
|
if (hi - lo > EPS) ((zones[uid] = zones[uid] || {})[side] = zones[uid][side] || []).push({ u0: lo, u1: hi, base, other: oid });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Object.keys(zones).forEach(uid => {
|
||||||
|
const u = units.find(x => x.id === uid);
|
||||||
|
const su = state.units.find(x => x.id === uid);
|
||||||
|
Object.keys(zones[uid]).forEach(side => {
|
||||||
|
const i = u.walls.findIndex(w => w.side === side);
|
||||||
|
const wl = u.layout.walls.find(w => w.side === side);
|
||||||
|
const next = wallOf(su, u.rect, u.layout, wl, zones[uid][side]);
|
||||||
|
u.walls[i] = next;
|
||||||
|
zones[uid][side].forEach(z => {
|
||||||
|
if (next.pieces.some(p => p.baseY && Math.abs(p.u0 - z.u0) < EPS)) raisedCap[`${uid}|${z.other}`] = z.base;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
// Binnenwanden staan op de vloer van de unit waarin hun midden valt.
|
// Binnenwanden staan op de vloer van de unit waarin hun midden valt.
|
||||||
const interior = state.interiorWalls.map((iw, i) => {
|
// Naadwanden (iw.seam) leveren twee delen op, één per unit (seamLeaves).
|
||||||
|
const interior = [];
|
||||||
|
let bwNr = 0, nwNr = 0;
|
||||||
|
state.interiorWalls.forEach(iw => {
|
||||||
|
if (iw.seam) { nwNr++; seamLeaves(iw, nwNr).forEach(l => interior.push(l)); return; }
|
||||||
|
bwNr++;
|
||||||
const lay = interiorLayout(iw);
|
const lay = interiorLayout(iw);
|
||||||
const host = units.find(u => lay.cx >= u.rect.x0 && lay.cx <= u.rect.x1 && lay.cz >= u.rect.z0 && lay.cz <= u.rect.z1);
|
const host = units.find(u => lay.cx >= u.rect.x0 && lay.cx <= u.rect.x1 && lay.cz >= u.rect.z0 && lay.cz <= u.rect.z1);
|
||||||
const floorTop = host ? host.layout.floorTop : Math.max(...units.map(u => u.layout.floorTop));
|
const floorTop = host ? host.layout.floorTop : Math.max(...units.map(u => u.layout.floorTop));
|
||||||
const config = WallModel.configFromState(iw.wall);
|
const config = WallModel.configFromState(iw.wall);
|
||||||
const frame = WallModel.computeFrame(config);
|
const frame = WallModel.computeFrame(config);
|
||||||
const buitenMateriaal = (iw.wall.materials || {}).exterior || 'spaanplaat';
|
const buitenMateriaal = (iw.wall.materials || {}).exterior || 'spaanplaat';
|
||||||
return Object.assign({
|
interior.push({
|
||||||
id: iw.id, element: `BW-${i + 1}`, label: `Binnenwand ${i + 1}`, config, frame, buitenMateriaal,
|
id: iw.id, element: `BW-${bwNr}`, label: `Binnenwand ${bwNr}`, config, frame, buitenMateriaal,
|
||||||
place: { cx: lay.cx, cz: lay.cz, rotY: lay.rotY, width: lay.width, thickness: WallModel.thicknessOf(config), floorTop }
|
place: { cx: lay.cx, cz: lay.cz, rotY: lay.rotY, width: lay.width, thickness: WallModel.thicknessOf(config), floorTop }
|
||||||
}, buildWall(config, frame, buitenMateriaal));
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Twee delen van een naadwand. Elk deel staat met zijn regelwerk op de
|
||||||
|
// naad (buitenzijde 'geen' = naadzijde) in zijn eigen unit. Een deel loopt van wand tot wand: waar een buitenwandstuk de hartlijn
|
||||||
|
// van het deel kruist, stopt het (voetafdrukken, zoals bij de
|
||||||
|
// gevellampen). De hoogte volgt de daklijn van de buitenwand van de unit
|
||||||
|
// aan de naadzijde (RoofModel.wallProfile), zodat het deel onder het dak
|
||||||
|
// van zijn eigen unit past. Steekt een deel boven het dak van de andere
|
||||||
|
// unit uit (bijv. hoge unit tegen een lage), dan krijgt dat stuk van de
|
||||||
|
// naadzijde buitenafwerking (vochtdoek + latwerk, skinFrom) die met de
|
||||||
|
// gevel van de woning meeloopt; het regelwerk blijft dat van de naadwand.
|
||||||
|
function seamLeaves(iw, nr) {
|
||||||
|
const sm = seams.find(x => sameSeam(x, iw.seam.a, iw.seam.b));
|
||||||
|
if (!sm) return []; // units niet (meer) tegen elkaar: naadwand vervalt
|
||||||
|
const horiz = Math.abs(sm.z0 - sm.z1) < EPS;
|
||||||
|
// Buitenwand van de unit aan de naadzijde (side +1: unit a, lage kant).
|
||||||
|
const seamSide = (side) => horiz ? (side > 0 ? 'achter' : 'voor') : (side > 0 ? 'rechts' : 'links');
|
||||||
|
const unitState = (uid) => state.units.find(u => u.id === uid).unit;
|
||||||
|
const baseConfig = WallModel.configFromState(iw.wall);
|
||||||
|
const maxD = Math.max(baseConfig.studDepth, baseConfig.plateDepth);
|
||||||
|
const T1 = WallModel.thicknessOf(baseConfig, 'geen');
|
||||||
|
// Plaats (iw.seam.in): leeg = midden op de naad (twee delen), 'a'/'b' =
|
||||||
|
// geheel in die unit: één binnenwand (beplating aan beide zijden) tegen
|
||||||
|
// de naad aan, met de hoogte van de daklijn van die unit.
|
||||||
|
const inUnit = iw.seam.in === 'a' || iw.seam.in === 'b' ? iw.seam[iw.seam.in] : null;
|
||||||
|
const T2 = WallModel.thicknessOf(baseConfig, 'spaanplaat');
|
||||||
|
// Opbouwstukken (baseY) staan boven de naadwand, niet ernaast.
|
||||||
|
const extPieces = [];
|
||||||
|
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => { if (!p.baseY) extPieces.push(p); })));
|
||||||
|
const along = (q) => horiz ? q.x : q.z, perpOf = (q) => horiz ? q.z : q.x;
|
||||||
|
const seamPerp = horiz ? sm.z0 : sm.x0;
|
||||||
|
const s0 = horiz ? sm.x0 : sm.z0, s1 = horiz ? sm.x1 : sm.z1;
|
||||||
|
|
||||||
|
const leaves = [];
|
||||||
|
[[sm.a, 1], [sm.b, -1]].forEach(([uid, side]) => {
|
||||||
|
// side +1: unit a ligt aan de lage kant, de naad aan zijn +kant.
|
||||||
|
if (inUnit && uid !== inUnit) return;
|
||||||
|
const T = inUnit ? T2 : T1;
|
||||||
|
const line = seamPerp - side * T / 2;
|
||||||
|
const blocked = [];
|
||||||
|
extPieces.forEach(p => {
|
||||||
|
const cs = [[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([i, j]) => UnitModel.localToWorld(p.place, i * p.place.width / 2, j * p.place.thickness / 2));
|
||||||
|
const pMin = Math.min(...cs.map(perpOf)), pMax = Math.max(...cs.map(perpOf));
|
||||||
|
if (line > pMin + 0.5 && line < pMax - 0.5) blocked.push([Math.min(...cs.map(along)), Math.max(...cs.map(along))]);
|
||||||
|
});
|
||||||
|
let cursor = s0, best = null;
|
||||||
|
blocked.sort((p, q) => p[0] - q[0]).concat([[s1, s1]]).forEach(([a, b]) => {
|
||||||
|
const hi = Math.min(a, s1);
|
||||||
|
if (hi - cursor > (best ? best[1] - best[0] : 0)) best = [cursor, hi];
|
||||||
|
cursor = Math.max(cursor, b);
|
||||||
|
});
|
||||||
|
if (!best || best[1] - best[0] < 300) return;
|
||||||
|
const width = Math.round(best[1] - best[0]), mid = (best[0] + best[1]) / 2;
|
||||||
|
const rotY = horiz ? (side > 0 ? 0 : Math.PI) : (side > 0 ? Math.PI / 2 : -Math.PI / 2);
|
||||||
|
const place = {
|
||||||
|
cx: horiz ? mid : line, cz: horiz ? line : mid, rotY, width, thickness: T,
|
||||||
|
normal: { x: Math.round(Math.sin(rotY)), y: Math.round(Math.cos(rotY)) }, // naadzijde
|
||||||
|
floorTop: units.find(u => u.id === uid).layout.floorTop
|
||||||
|
};
|
||||||
|
leaves.push({ uid, side, place });
|
||||||
|
});
|
||||||
|
if (!leaves.length) return [];
|
||||||
|
|
||||||
|
// Hoogtes van een deel: daklijn van de buitenwand aan de naadzijde, op
|
||||||
|
// de uiteinden van het deel (incl. nok als die erin valt).
|
||||||
|
const heightsOf = (lf) => {
|
||||||
|
const u = units.find(x => x.id === lf.uid);
|
||||||
|
const prof = RoofModel.wallProfile(unitState(lf.uid), u.layout, seamSide(lf.side));
|
||||||
|
const axisOf = (q) => horiz ? q.x - u.rect.cx : q.z - u.rect.cz;
|
||||||
|
const W = lf.place.width;
|
||||||
|
const pL = axisOf(UnitModel.localToWorld(lf.place, -W / 2, 0)), pR = axisOf(UnitModel.localToWorld(lf.place, W / 2, 0));
|
||||||
|
const peak = prof.length > 2 ? prof[1] : null;
|
||||||
|
// Opbouw erboven (raiseSeamWalls): het deel stopt op de voet daarvan.
|
||||||
|
const cap = raisedCap[`${lf.uid}|${lf.uid === sm.a ? sm.b : sm.a}`];
|
||||||
|
const lim = (h) => cap !== undefined ? Math.min(h, cap) : h;
|
||||||
|
const hL = lim(RoofModel.profileHeight(prof, pL)), hR = lim(RoofModel.profileHeight(prof, pR));
|
||||||
|
const hasPeak = !!peak && peak.p > Math.min(pL, pR) + 1 && peak.p < Math.max(pL, pR) - 1 && lim(peak.h) > Math.max(hL, hR) + 0.5;
|
||||||
|
return {
|
||||||
|
heightLeft: Math.round(hL), heightRight: Math.round(hR),
|
||||||
|
heightMid: hasPeak ? Math.round(lim(peak.h)) : null, peakX: hasPeak ? Math.abs(peak.p - pL) : undefined
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const paramsOf = (lf) => Object.assign({}, iw.wall.wall, { width: lf.place.width }, heightsOf(lf));
|
||||||
|
// Laagste bovenkant dak van de andere unit langs dit deel (mm vanaf het
|
||||||
|
// loopvlak van het deel), of null als het deel er niet bovenuit steekt.
|
||||||
|
const exposedFrom = (lf, config) => {
|
||||||
|
const other = units.find(u => u.id === (lf.uid === sm.a ? sm.b : sm.a));
|
||||||
|
const W = lf.place.width;
|
||||||
|
const ends = [-W / 2, W / 2].map(lx => along(UnitModel.localToWorld(lf.place, lx, 0)));
|
||||||
|
const lo = Math.min(...ends), hi = Math.max(...ends);
|
||||||
|
const ts = ends.concat(other.roof.ridges.filter(r => r.axis === (horiz ? 'x' : 'z') && r.value > lo && r.value < hi).map(r => r.value));
|
||||||
|
const from = Math.min(...ts.map(t => horiz ? other.roof.topAt(t, seamPerp) : other.roof.topAt(seamPerp, t))) - lf.place.floorTop;
|
||||||
|
return from < WallModel.topLine(config).maxY - 50 ? Math.max(0, Math.round(from)) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Openingen: opgeslagen voor het eerste deel; het andere deel krijgt
|
||||||
|
// dezelfde openingen op dezelfde plek in de wereld (gespiegeld).
|
||||||
|
const first = leaves[0];
|
||||||
|
const firstParams = paramsOf(first);
|
||||||
|
// Voor de wand-editor: breedte en daklijn van het eerste deel.
|
||||||
|
Object.assign(iw.wall.wall, { width: first.place.width, heightLeft: firstParams.heightLeft, heightRight: firstParams.heightRight, heightMid: firstParams.heightMid });
|
||||||
|
const firstConfig = WallModel.buildConfig(firstParams, iw.wall.openings || []);
|
||||||
|
const toLocalU = (pl, q) => {
|
||||||
|
const c = Math.cos(pl.rotY), sn = Math.sin(pl.rotY);
|
||||||
|
return (q.x - pl.cx) * c - (q.z - pl.cz) * sn + pl.width / 2;
|
||||||
|
};
|
||||||
|
return leaves.map((lf, k) => {
|
||||||
|
let config = firstConfig;
|
||||||
|
if (k > 0) {
|
||||||
|
const ops = firstConfig.openings.map(op => {
|
||||||
|
const wa = UnitModel.localToWorld(first.place, -first.place.width / 2 + op.x, 0);
|
||||||
|
const wb = UnitModel.localToWorld(first.place, -first.place.width / 2 + op.x + op.w, 0);
|
||||||
|
const u = Math.min(toLocalU(lf.place, wa), toLocalU(lf.place, wb));
|
||||||
|
return { id: op.id, type: op.type, w: op.w, h: op.h, y: op.y, anchor: 'left', offset: Math.round(u) };
|
||||||
|
});
|
||||||
|
config = WallModel.buildConfig(paramsOf(lf), ops);
|
||||||
|
}
|
||||||
|
const frame = WallModel.computeFrame(config);
|
||||||
|
const skinFrom = exposedFrom(lf, config);
|
||||||
|
return {
|
||||||
|
id: iw.id, seam: iw.seam, seamUnit: lf.uid,
|
||||||
|
element: `NW-${nr}${lf.uid}`, label: `Naadwand ${nr} (unit ${lf.uid})`,
|
||||||
|
config, frame, place: lf.place,
|
||||||
|
// Het regelwerk staat gecentreerd op lokaal z = 0, de beplating
|
||||||
|
// alleen aan de achterzijde: bij het bouwen schuiven zodat het
|
||||||
|
// midden van de totale dikte op place.cx/cz ligt (regelwerk op de naad).
|
||||||
|
// Geheel in één unit: beplating aan beide zijden, dus symmetrisch.
|
||||||
|
shiftZ: inUnit ? 0 : (T1 - maxD) / 2,
|
||||||
|
...(skinFrom === null
|
||||||
|
? { buitenMateriaal: inUnit ? 'spaanplaat' : 'geen' }
|
||||||
|
: { buitenMateriaal: 'vochtdoek', skinFrom, spaanplaatBelow: !!inUnit, cladding: state.settings.cladding || 'horizontaal', trasraam: null })
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doek en latwerk volgen de hele gevel (niet per unit/wandstuk), ook
|
||||||
|
// over het blootliggende deel van een naadwand. Daarna pas bouwen.
|
||||||
|
const extPieces = [];
|
||||||
|
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => extPieces.push(p))));
|
||||||
|
assignFacades(extPieces.concat(interior.filter(p => p.skinFrom !== undefined)));
|
||||||
|
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);
|
||||||
|
if (built.group && p.shiftZ) {
|
||||||
|
const outer = new THREE.Group();
|
||||||
|
built.group.position.z = p.shiftZ * 0.001;
|
||||||
|
outer.add(built.group);
|
||||||
|
built.group = outer;
|
||||||
|
}
|
||||||
|
Object.assign(p, built);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dakrand (boeidelen/daktrim) als één doorlopende rand om alle daken;
|
// Dakrand (boeidelen/daktrim) als één doorlopende rand om alle daken;
|
||||||
// boeidelen aan/uit per unit.
|
// boeidelen aan/uit per unit. De rand volgt de dakvorm van de unit waar
|
||||||
|
// de ribbe bij hoort; platte daken delen één randhoogte (de hoogste).
|
||||||
|
const flatTops = units.filter(u => u.roof.shape.type === 'plat').map(u => u.roof.top);
|
||||||
|
const flatTop = flatTops.length ? Math.max(...flatTops) : 0;
|
||||||
const roofEdge = units.length ? RoofModel.computeEdge(state.settings.roof,
|
const roofEdge = units.length ? RoofModel.computeEdge(state.settings.roof,
|
||||||
units.map(u => ({ unitId: u.id, rect: u.roof.rect, fascia: state.units.find(su => su.id === u.id).roof.fascia !== false })),
|
units.map(u => ({
|
||||||
Math.max(...units.map(u => u.roof.top))) : null;
|
unitId: u.id, rect: u.roof.rect, fascia: state.units.find(su => su.id === u.id).roof.fascia !== false,
|
||||||
|
topAt: u.roof.shape.type === 'plat' ? () => flatTop : u.roof.topAt, ridges: u.roof.ridges
|
||||||
|
}))) : null;
|
||||||
// Buitenverlichting: gevellampen per wandstuk + boeideel-spots (vult roofEdge.spots).
|
// Buitenverlichting: gevellampen per wandstuk + boeideel-spots (vult roofEdge.spots).
|
||||||
const lighting = LightingModel.compute(state.settings.lighting, state.settings.roof, units, state.units, roofEdge);
|
const lighting = LightingModel.compute(state.settings.lighting, state.settings.roof, units, state.units, roofEdge);
|
||||||
// Zichtbaarheid: boeideel/daktrim volgt "Beplating", spots en lampen volgen "Lampen".
|
// Zichtbaarheid: boeideel/daktrim volgt "Beplating", spots en lampen volgen "Lampen".
|
||||||
const roofEdgeGroup = build && roofEdge ? RoofModel.buildEdge3D(roofEdge, o.roofMaterials, {
|
const roofEdgeGroup = build && roofEdge ? RoofModel.buildEdge3D(roofEdge, o.roofMaterials, {
|
||||||
edgeVisible: o.beplatingVisible !== false, spotsVisible: o.lampenVisible !== false
|
edgeVisible: o.beplatingVisible !== false, spotsVisible: o.lampenVisible !== false
|
||||||
}) : null;
|
}) : null;
|
||||||
const lightingGroup = build ? LightingModel.build3D(lighting.lamps, o.lightingMaterials || LightingModel.createMaterials()) : null;
|
// Binnenspots in het plafond, per unit (LightingModel.computeIndoor).
|
||||||
|
lighting.indoor = LightingModel.computeIndoor(state.settings.lighting, units, interior);
|
||||||
|
const lightingGroup = build ? LightingModel.build3D(lighting.lamps, o.lightingMaterials || LightingModel.createMaterials(), lighting.indoor) : null;
|
||||||
if (lightingGroup) lightingGroup.visible = o.lampenVisible !== false;
|
if (lightingGroup) lightingGroup.visible = o.lampenVisible !== false;
|
||||||
|
|
||||||
// Sandwichpanelen rondom de vloeren, vanaf het loopvlak naar beneden.
|
// Sandwichpanelen rondom de vloeren, vanaf het loopvlak naar beneden.
|
||||||
@@ -425,9 +712,13 @@ window.HouseModel = (() => {
|
|||||||
}
|
}
|
||||||
parts.units.forEach(u => {
|
parts.units.forEach(u => {
|
||||||
const fc = u.floorData.calcData;
|
const fc = u.floorData.calcData;
|
||||||
fc.joists.forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.depth));
|
fc.joists.forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.joistLen));
|
||||||
const rc = u.roof.data.calcData;
|
['front', 'back'].filter(k => fc.rims[k]).forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.width));
|
||||||
rc.joists.forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.depth));
|
u.roof.planes.forEach(pl => {
|
||||||
|
const rc = pl.data.calcData;
|
||||||
|
rc.joists.forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.joistLen));
|
||||||
|
['front', 'back'].filter(k => rc.rims[k]).forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.width));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const wallBom = [];
|
const wallBom = [];
|
||||||
pieces.forEach(p => WallModel.computeBOM(p.config, p.frame).forEach(b => { wallBom.push(b); addPiece(b.kind, b.width, b.depth, b.length); }));
|
pieces.forEach(p => WallModel.computeBOM(p.config, p.frame).forEach(b => { wallBom.push(b); addPiece(b.kind, b.width, b.depth, b.length); }));
|
||||||
@@ -501,6 +792,7 @@ window.HouseModel = (() => {
|
|||||||
} : null,
|
} : null,
|
||||||
lamps: {
|
lamps: {
|
||||||
total: parts.lighting.lamps.length,
|
total: parts.lighting.lamps.length,
|
||||||
|
indoor: (parts.lighting.indoor || []).length,
|
||||||
both: parts.lighting.lamps.filter(l => l.direction === 'both').length,
|
both: parts.lighting.lamps.filter(l => l.direction === 'both').length,
|
||||||
down: parts.lighting.lamps.filter(l => l.direction === 'down').length,
|
down: parts.lighting.lamps.filter(l => l.direction === 'down').length,
|
||||||
up: parts.lighting.lamps.filter(l => l.direction === 'up').length
|
up: parts.lighting.lamps.filter(l => l.direction === 'up').length
|
||||||
@@ -543,6 +835,7 @@ window.HouseModel = (() => {
|
|||||||
state.settings.trasraam.enabled = state.settings.trasraam.enabled !== false;
|
state.settings.trasraam.enabled = state.settings.trasraam.enabled !== false;
|
||||||
if (!(state.settings.trasraam.height > 0)) state.settings.trasraam.height = WallModel.TRASRAAM_DEFAULT_MM;
|
if (!(state.settings.trasraam.height > 0)) state.settings.trasraam.height = WallModel.TRASRAAM_DEFAULT_MM;
|
||||||
state.settings.sandwich = state.settings.sandwich !== false;
|
state.settings.sandwich = state.settings.sandwich !== false;
|
||||||
|
if (!(state.settings.groundLevel >= 0)) state.settings.groundLevel = def.groundLevel;
|
||||||
const oldRoof = state.settings.roof || {}; // v0.3.0: edge/spots waren woning-breed
|
const oldRoof = state.settings.roof || {}; // v0.3.0: edge/spots waren woning-breed
|
||||||
state.settings.roof = RoofModel.normalizeSettings(oldRoof);
|
state.settings.roof = RoofModel.normalizeSettings(oldRoof);
|
||||||
// v0.3.1: type/richting van de verlichting stonden per unit; nu woning-breed.
|
// v0.3.1: type/richting van de verlichting stonden per unit; nu woning-breed.
|
||||||
@@ -563,7 +856,7 @@ window.HouseModel = (() => {
|
|||||||
return {
|
return {
|
||||||
ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS,
|
ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS,
|
||||||
clone, createEmptyProject, createDefaultSettings, createUnitEntry, applyGlobalSettings, nextUnitId, createDefaultState, unitRects, computeSeams, overlappingPairs,
|
clone, createEmptyProject, createDefaultSettings, createUnitEntry, applyGlobalSettings, nextUnitId, createDefaultState, unitRects, computeSeams, overlappingPairs,
|
||||||
interiorWallDefaults, setThickness, thicknessOf, interiorLayout,
|
interiorWallDefaults, setThickness, thicknessOf, interiorLayout, ensureSeamWalls,
|
||||||
computeParts, allWallPieces, computeTotals, validateState, fromUnitDesign, normalize
|
computeParts, allWallPieces, computeTotals, validateState, fromUnitDesign, normalize
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ window.HousePdf = (() => {
|
|||||||
parts.units.forEach(u => {
|
parts.units.forEach(u => {
|
||||||
list.push({ kind: 'floor', element: u.floorElement, label: `Unit ${u.id} vloer`, unit: u });
|
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.walls.forEach(w => w.pieces.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p })));
|
||||||
list.push({ kind: 'roof', element: u.roof.element, label: `Unit ${u.id} dak (plat)`, unit: u });
|
u.roof.planes.forEach(pl => list.push({ kind: 'roof', element: pl.element, label: pl.label, plane: pl }));
|
||||||
});
|
});
|
||||||
parts.interior.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p }));
|
parts.interior.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p }));
|
||||||
list.push({ kind: 'totals', element: 'TOT', label: 'Totalen' });
|
list.push({ kind: 'totals', element: 'TOT', label: 'Totalen' });
|
||||||
@@ -146,7 +146,7 @@ window.HousePdf = (() => {
|
|||||||
})));
|
})));
|
||||||
doc.setTextColor(...BLACK);
|
doc.setTextColor(...BLACK);
|
||||||
parts.interior.forEach(p => {
|
parts.interior.forEach(p => {
|
||||||
doc.text(`${p.element}: ${p.place.width}`, px(p.place.cx), py(p.place.cz) - 1.5, { align: 'center', angle: p.place.rotY === 0 ? 0 : 90 });
|
doc.text(`${p.element}: ${p.place.width}`, px(p.place.cx), py(p.place.cz) - 1.5, { align: 'center', angle: Math.abs(Math.sin(p.place.rotY)) < 0.5 ? 0 : 90 });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Buitenmaten woning
|
// Buitenmaten woning
|
||||||
@@ -224,7 +224,8 @@ window.HousePdf = (() => {
|
|||||||
if (e.trimLengthM > 0) rows.push(['Daktrim', `${fmt(e.trimLengthM, 2)} m, ${e.trimHeight} mm hoog`]);
|
if (e.trimLengthM > 0) rows.push(['Daktrim', `${fmt(e.trimLengthM, 2)} m, ${e.trimHeight} mm hoog`]);
|
||||||
rows.push(['Boeideel-spots', e.spots + ' stuks']);
|
rows.push(['Boeideel-spots', e.spots + ' stuks']);
|
||||||
rows.push(['Gevellampen', `${L.total} stuks (boven+onder ${L.both}, onder ${L.down}, boven ${L.up})`]);
|
rows.push(['Gevellampen', `${L.total} stuks (boven+onder ${L.both}, onder ${L.down}, boven ${L.up})`]);
|
||||||
PdfSheet.table(doc, 215, y, [0, 40], ['Onderdeel', 'Hoeveelheid'], rows, tableOpts('Dakrand & buitenverlichting'));
|
rows.push(['Binnenspots', `${L.indoor} stuks (plafond)`]);
|
||||||
|
PdfSheet.table(doc, 215, y, [0, 40], ['Onderdeel', 'Hoeveelheid'], rows, tableOpts('Dakrand & verlichting'));
|
||||||
}
|
}
|
||||||
|
|
||||||
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
|
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
|
||||||
@@ -244,8 +245,8 @@ window.HousePdf = (() => {
|
|||||||
const opts = { project, titel: sheet.label, element: sheet.element, blad: `${i + 1} / ${sheets.length}` };
|
const opts = { project, titel: sheet.label, element: sheet.element, blad: `${i + 1} / ${sheets.length}` };
|
||||||
if (sheet.kind === 'plan') drawPlanSheet(doc, parts, sheets, opts);
|
if (sheet.kind === 'plan') drawPlanSheet(doc, parts, sheets, opts);
|
||||||
else if (sheet.kind === 'floor') FloorPdf.drawSheet(doc, sheet.unit.floorData.calcData, sheet.unit.floorData.resolvedOpenings, opts);
|
else if (sheet.kind === 'floor') FloorPdf.drawSheet(doc, sheet.unit.floorData.calcData, sheet.unit.floorData.resolvedOpenings, opts);
|
||||||
else if (sheet.kind === 'roof') FloorPdf.drawSheet(doc, sheet.unit.roof.data.calcData, sheet.unit.roof.data.resolvedOpenings, Object.assign({ roof: true }, opts));
|
else if (sheet.kind === 'roof') FloorPdf.drawSheet(doc, sheet.plane.data.calcData, sheet.plane.data.resolvedOpenings, Object.assign({ roof: true }, opts));
|
||||||
else if (sheet.kind === 'wall') WallPdf.drawSheet(doc, sheet.piece.config, sheet.piece.frame, Object.assign({ buitenMateriaal: sheet.piece.buitenMateriaal, cladding: sheet.piece.cladding, trasraam: sheet.piece.trasraam, skinExt: sheet.piece.skinExt, facade: sheet.piece.facade, lamps: sheet.piece.lamps || [] }, opts));
|
else if (sheet.kind === 'wall') WallPdf.drawSheet(doc, sheet.piece.config, sheet.piece.frame, Object.assign({ buitenMateriaal: sheet.piece.buitenMateriaal, cladding: sheet.piece.cladding, trasraam: sheet.piece.trasraam, skinExt: sheet.piece.skinExt, facade: sheet.piece.facade, skinFrom: sheet.piece.skinFrom, lamps: sheet.piece.lamps || [] }, opts));
|
||||||
else drawTotalsSheet(doc, totals, opts);
|
else drawTotalsSheet(doc, totals, opts);
|
||||||
});
|
});
|
||||||
return doc;
|
return doc;
|
||||||
|
|||||||
+310
-293
@@ -23,11 +23,11 @@
|
|||||||
const HOUSE_TEMPLATE = `
|
const HOUSE_TEMPLATE = `
|
||||||
<div id="house-app">
|
<div id="house-app">
|
||||||
<aside id="house-nav">
|
<aside id="house-nav">
|
||||||
<div class="hn-title">Woning</div>
|
<div class="hn-title"><i class="bi bi-diagram-3"></i> Woning</div>
|
||||||
<div id="house-nav-tree"></div>
|
<div id="house-nav-tree"></div>
|
||||||
<div class="hn-actions">
|
<div class="hn-actions">
|
||||||
<button type="button" id="hn-add-unit" class="io-btn">+ Unit</button>
|
<button type="button" id="hn-add-unit" class="ui-btn"><i class="bi bi-plus-lg"></i><span>Unit</span></button>
|
||||||
<button type="button" id="hn-add-iw" class="io-btn">+ Binnenwand</button>
|
<button type="button" id="hn-add-iw" class="ui-btn"><i class="bi bi-plus-lg"></i><span>Binnenwand</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="hn-soon">
|
<div class="hn-soon">
|
||||||
<div class="hn-item disabled" title="Nog niet beschikbaar">Schuine daken</div>
|
<div class="hn-item disabled" title="Nog niet beschikbaar">Schuine daken</div>
|
||||||
@@ -47,14 +47,14 @@ const HOUSE_TEMPLATE = `
|
|||||||
</div>
|
</div>
|
||||||
<div class="controls-grid">
|
<div class="controls-grid">
|
||||||
<div>
|
<div>
|
||||||
<div class="control-group"><label for="h-proj-naam">Projectnaam:</label><div><input type="text" id="h-proj-naam"></div></div>
|
<div class="control-group"><label for="h-proj-naam">Projectnaam</label><div><input type="text" id="h-proj-naam"></div></div>
|
||||||
<div class="control-group"><label for="h-proj-plaats">Plaats:</label><div><input type="text" id="h-proj-plaats"></div></div>
|
<div class="control-group"><label for="h-proj-plaats">Plaats</label><div><input type="text" id="h-proj-plaats"></div></div>
|
||||||
<div class="control-group"><label for="h-proj-straat">Straat:</label><div><input type="text" id="h-proj-straat"></div></div>
|
<div class="control-group"><label for="h-proj-straat">Straat</label><div><input type="text" id="h-proj-straat"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="control-group"><label for="h-proj-nummer">Projectnummer:</label><div><input type="text" id="h-proj-nummer"></div></div>
|
<div class="control-group"><label for="h-proj-nummer">Projectnummer</label><div><input type="text" id="h-proj-nummer"></div></div>
|
||||||
<div class="control-group"><label for="h-proj-tekenaar">Tekenaar:</label><div><input type="text" id="h-proj-tekenaar"></div></div>
|
<div class="control-group"><label for="h-proj-tekenaar">Tekenaar</label><div><input type="text" id="h-proj-tekenaar"></div></div>
|
||||||
<div class="control-group"><label for="h-proj-opmerking">Opmerking:</label><div><input type="text" id="h-proj-opmerking"></div></div>
|
<div class="control-group"><label for="h-proj-opmerking">Opmerking</label><div><input type="text" id="h-proj-opmerking"></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="material-info">Geldt voor elk blad van de woning-PDF. Titel en elementnummer worden per blad automatisch ingevuld.</div>
|
<div class="material-info">Geldt voor elk blad van de woning-PDF. Titel en elementnummer worden per blad automatisch ingevuld.</div>
|
||||||
@@ -70,10 +70,10 @@ const HOUSE_OVERVIEW_TEMPLATE = `
|
|||||||
<div id="house-overview">
|
<div id="house-overview">
|
||||||
<div class="ho-col ho-left">
|
<div class="ho-col ho-left">
|
||||||
<div id="ho-pane-plan" class="ho-pane">
|
<div id="ho-pane-plan" class="ho-pane">
|
||||||
<div class="pane-label">2D PLATTEGROND</div>
|
<div class="pane-label">PLATTEGROND</div>
|
||||||
<div class="toolbar-3d">
|
<div class="toolbar-3d">
|
||||||
<button type="button" id="ho-tool-select" class="btn-3d-toggle active">Selecteren</button>
|
<button type="button" id="ho-tool-select" class="btn-3d-toggle active" title="Selecteren"><i class="bi bi-cursor"></i>Selecteren</button>
|
||||||
<button type="button" id="ho-tool-iw" class="btn-3d-toggle">Binnenwand tekenen</button>
|
<button type="button" id="ho-tool-iw" class="btn-3d-toggle" title="Binnenwand tekenen"><i class="bi bi-pencil"></i>Binnenwand</button>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="ho-canvas"></canvas>
|
<canvas id="ho-canvas"></canvas>
|
||||||
<div id="ho-hint" class="ho-hint"></div>
|
<div id="ho-hint" class="ho-hint"></div>
|
||||||
@@ -82,9 +82,9 @@ const HOUSE_OVERVIEW_TEMPLATE = `
|
|||||||
</div>
|
</div>
|
||||||
<div class="ho-col ho-right">
|
<div class="ho-col ho-right">
|
||||||
<div id="ho-pane-3d" class="ho-pane">
|
<div id="ho-pane-3d" class="ho-pane">
|
||||||
<div class="pane-label">3D PERSPECTIEF</div>
|
<div class="pane-label">3D</div>
|
||||||
<div class="toolbar-3d" id="ho-toolbar-3d">
|
<div class="toolbar-3d" id="ho-toolbar-3d">
|
||||||
<button type="button" id="ho-btn-render" class="btn-3d-toggle" title="Avondscène: elke lamp en spot is een echte lichtbron">Volle render</button>
|
<button type="button" id="ho-btn-render" class="btn-3d-toggle" title="Render: elke lamp en spot is een echte lichtbron"><i class="bi bi-camera"></i>Render</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="ho-specs"></div>
|
<div id="ho-specs"></div>
|
||||||
@@ -133,9 +133,21 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
if (!activeEditor || typeof activeEditor.getStateJson !== 'function' || !activePart) return;
|
if (!activeEditor || typeof activeEditor.getStateJson !== 'function' || !activePart) return;
|
||||||
const [kind, a, b] = activePart.split(':');
|
const [kind, a, b] = activePart.split(':');
|
||||||
if (kind === 'floor' && unitById(a)) unitById(a).unit.floor = activeEditor.getStateJson();
|
if (kind === 'floor' && unitById(a)) unitById(a).unit.floor = activeEditor.getStateJson();
|
||||||
else if (kind === 'wall' && unitById(a)) unitById(a).unit.walls[b] = activeEditor.getStateJson();
|
else if (kind === 'wall' && unitById(a)) {
|
||||||
|
// Hoogtes gewijzigd: tegenoverliggende wand spiegelen, haakse wanden
|
||||||
|
// vlak op de hoekhoogtes (UnitModel.syncWallHeights).
|
||||||
|
const unit = unitById(a).unit, before = unit.walls[b].wall;
|
||||||
|
const next = activeEditor.getStateJson();
|
||||||
|
unit.walls[b] = next;
|
||||||
|
const h = (w) => [w.heightLeft, w.heightRight, w.heightMid > 0 ? w.heightMid : null].join('|');
|
||||||
|
if (h(before) !== h(next.wall)) UnitModel.syncWallHeights(unit, b);
|
||||||
|
}
|
||||||
else if (kind === 'roof' && unitById(a)) unitById(a).roof.deck = activeEditor.getStateJson();
|
else if (kind === 'roof' && unitById(a)) unitById(a).roof.deck = activeEditor.getStateJson();
|
||||||
else if (kind === 'iw' && iwById(Number(a))) iwById(Number(a)).wall = activeEditor.getStateJson();
|
else if (kind === 'iw' && iwById(Number(a))) {
|
||||||
|
const iw = iwById(Number(a));
|
||||||
|
iw.wall = activeEditor.getStateJson();
|
||||||
|
if (iw.seam) iw.wall.materials = Object.assign({}, iw.wall.materials, { exterior: 'geen' }); // naadzijde blijft zonder plaat
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPart(part) {
|
function showPart(part) {
|
||||||
@@ -172,7 +184,8 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
activeEditor.loadStateJson(HouseModel.clone(u.roof.deck));
|
activeEditor.loadStateJson(HouseModel.clone(u.roof.deck));
|
||||||
} else if (kind === 'iw' && iwById(Number(a))) {
|
} else if (kind === 'iw' && iwById(Number(a))) {
|
||||||
HouseModel.applyGlobalSettings(state);
|
HouseModel.applyGlobalSettings(state);
|
||||||
HouseModel.interiorLayout(iwById(Number(a))); // breedte bijwerken
|
if (iwById(Number(a)).seam) HouseModel.computeParts(state, { skip3D: true }); // breedte naadwand-deel
|
||||||
|
else HouseModel.interiorLayout(iwById(Number(a))); // breedte bijwerken
|
||||||
activeEditor = WallEditor.mount(host, { cameraKey: camKey(part), lockWidth: 'De lengte van een binnenwand volgt uit de tekening (overzicht).', lockSpacing: SPACING_LOCK_MSG });
|
activeEditor = WallEditor.mount(host, { cameraKey: camKey(part), lockWidth: 'De lengte van een binnenwand volgt uit de tekening (overzicht).', lockSpacing: SPACING_LOCK_MSG });
|
||||||
activeEditor.loadStateJson(HouseModel.clone(iwById(Number(a)).wall));
|
activeEditor.loadStateJson(HouseModel.clone(iwById(Number(a)).wall));
|
||||||
} else {
|
} else {
|
||||||
@@ -187,25 +200,33 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
function renderNav() {
|
function renderNav() {
|
||||||
const parts = HouseModel.computeParts(state, { skip3D: true });
|
const parts = HouseModel.computeParts(state, { skip3D: true });
|
||||||
const item = (part, label, meta, cls) =>
|
const item = (part, label, meta, cls, iconName) =>
|
||||||
`<div class="hn-item ${cls || ''} ${activePart === part ? 'active' : ''}" data-part="${part}">
|
`<div class="hn-item ${cls || ''} ${activePart === part ? 'active' : ''}" data-part="${part}">
|
||||||
<span>${label}</span>${meta ? `<span class="hn-meta">${meta}</span>` : ''}</div>`;
|
<span class="hn-label">${iconName ? UI.icon(iconName) : ''}${label}</span>${meta ? `<span class="hn-meta">${meta}</span>` : ''}</div>`;
|
||||||
let html = item('overzicht', 'Overzicht', '', 'hn-top');
|
let html = item('overzicht', 'Overzicht', '', 'hn-top', 'grid-1x2');
|
||||||
parts.units.forEach(u => {
|
parts.units.forEach(u => {
|
||||||
const sel = selection && selection.type === 'unit' && selection.id === u.id && activePart === 'overzicht';
|
const sel = selection && selection.type === 'unit' && selection.id === u.id && activePart === 'overzicht';
|
||||||
html += `<div class="hn-group">`;
|
html += `<div class="hn-group">`;
|
||||||
html += `<div class="hn-item hn-unit ${sel ? 'active' : ''}" data-select-unit="${u.id}"><span>Unit ${u.id}</span><span class="hn-meta">${u.rect.W} × ${u.rect.D}</span></div>`;
|
html += `<div class="hn-item hn-unit ${sel ? 'active' : ''}" data-select-unit="${u.id}"><span class="hn-label">${UI.icon('box')}Unit ${u.id}</span><span class="hn-meta">${u.rect.W} × ${u.rect.D}</span></div>`;
|
||||||
html += item(`floor:${u.id}`, 'Vloer', u.floorElement, 'hn-sub');
|
html += item(`floor:${u.id}`, 'Vloer', u.floorElement, 'hn-sub', 'grid-3x3');
|
||||||
html += item(`roof:${u.id}`, 'Dak (plat)', u.roof.element, 'hn-sub');
|
html += item(`roof:${u.id}`, 'Dak', u.roof.element, 'hn-sub', 'house');
|
||||||
u.walls.forEach(w => {
|
u.walls.forEach(w => {
|
||||||
const meta = w.removed ? 'vervalt (naad)' : w.cutsU.length ? `deels naad · ${w.pieces.map(p => p.element).join(', ')}` : w.pieces[0].element;
|
const meta = w.removed ? 'vervalt' : w.pieces.map(p => p.element).join(', ');
|
||||||
html += item(`wall:${u.id}:${w.side}`, w.label, meta, 'hn-sub' + (w.removed ? ' hn-removed' : ''));
|
html += item(`wall:${u.id}:${w.side}`, w.label, meta, 'hn-sub' + (w.removed ? ' hn-removed' : ''), 'bricks');
|
||||||
});
|
});
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
});
|
});
|
||||||
if (parts.interior.length) {
|
if (parts.interior.length) {
|
||||||
html += `<div class="hn-group"><div class="hn-item hn-unit hn-static"><span>Binnenwanden</span></div>`;
|
html += `<div class="hn-group"><div class="hn-item hn-unit hn-static"><span class="hn-label">${UI.icon('distribute-horizontal')}Binnenwanden</span></div>`;
|
||||||
parts.interior.forEach(p => { html += item(`iw:${p.id}`, p.element, `${p.place.width} mm`, 'hn-sub'); });
|
const seen = new Set();
|
||||||
|
parts.interior.forEach(p => {
|
||||||
|
if (seen.has(p.id)) return; // naadwand: twee delen, één item
|
||||||
|
seen.add(p.id);
|
||||||
|
if (p.seam) {
|
||||||
|
const els = parts.interior.filter(x => x.id === p.id).map(x => x.element).join(', ');
|
||||||
|
html += item(`iw:${p.id}`, `Naadwand ${p.seam.a}|${p.seam.b}`, els, 'hn-sub', 'layout-split');
|
||||||
|
} else html += item(`iw:${p.id}`, p.element, `${p.place.width} mm`, 'hn-sub', 'distribute-horizontal');
|
||||||
|
});
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
}
|
}
|
||||||
document.getElementById('house-nav-tree').innerHTML = html;
|
document.getElementById('house-nav-tree').innerHTML = html;
|
||||||
@@ -234,6 +255,7 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
const side = ['rechts', 'achter', 'links', 'voor'].find(s => !taken.has(s)) || 'rechts';
|
const side = ['rechts', 'achter', 'links', 'voor'].find(s => !taken.has(s)) || 'rechts';
|
||||||
const id = HouseModel.nextUnitId(state);
|
const id = HouseModel.nextUnitId(state);
|
||||||
state.units.push(HouseModel.createUnitEntry(id, { to: parent.id, side, align: 'start', offset: 0 }, { width: 3000, depth: 3000, windowOn: [] }));
|
state.units.push(HouseModel.createUnitEntry(id, { to: parent.id, side, align: 'start', offset: 0 }, { width: 3000, depth: 3000, windowOn: [] }));
|
||||||
|
HouseModel.ensureSeamWalls(state, id); // tussenwand met deur op de nieuwe naad
|
||||||
selection = { type: 'unit', id };
|
selection = { type: 'unit', id };
|
||||||
showPart('overzicht');
|
showPart('overzicht');
|
||||||
}
|
}
|
||||||
@@ -299,42 +321,48 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
let dakVisible = true;
|
let dakVisible = true;
|
||||||
// Opgeslagen camerastand gaat voor; anders eenmalig passend maken.
|
// Opgeslagen camerastand gaat voor; anders eenmalig passend maken.
|
||||||
let cameraFitted = CameraMemory.restore(overviewCamKey, camera, controls);
|
let cameraFitted = CameraMemory.restore(overviewCamKey, camera, controls);
|
||||||
let beplatingVisible = true, isolatieVisible = true, lampenVisible = true;
|
let beplatingVisible = true, lampenVisible = true;
|
||||||
let fullRender = false;
|
let fullRender = false;
|
||||||
|
let skyMode = Skybox.getMode();
|
||||||
|
const skyEnv = Skybox.createEnvironment(scene);
|
||||||
|
|
||||||
// Weergave-filter (core/view-filter.js). Beplating = wandplaten, vloer-
|
// Weergave-filter (core/view-filter.js). Beplating = wandplaten, vloer-
|
||||||
// en plafondplaten, dakleer en boeideel/daktrim.
|
// en plafondplaten, dakleer en boeideel/daktrim.
|
||||||
createViewFilter(document.getElementById('ho-toolbar-3d'), [
|
createViewFilter(document.getElementById('ho-toolbar-3d'), [
|
||||||
{ key: 'beplating', label: 'Beplating' },
|
{ key: 'beplating', label: 'Beplating' },
|
||||||
{ key: 'isolatie', label: 'Isolatie' },
|
|
||||||
{ key: 'dak', label: 'Dak' },
|
{ key: 'dak', label: 'Dak' },
|
||||||
{ key: 'lampen', label: 'Lampen' }
|
{ key: 'lampen', label: 'Lampen' }
|
||||||
], (v) => {
|
], (v) => {
|
||||||
beplatingVisible = v.beplating;
|
beplatingVisible = v.beplating;
|
||||||
isolatieVisible = v.isolatie;
|
|
||||||
dakVisible = v.dak;
|
dakVisible = v.dak;
|
||||||
lampenVisible = v.lampen;
|
lampenVisible = v.lampen;
|
||||||
refresh({ keepProps: true });
|
refresh({ keepProps: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Volle render: avondscène, lampen en spots als echte lichtbronnen.
|
// Omgeving (core/skybox.js: geen/dag/nacht) en Render. Render = lampen en
|
||||||
|
// spots als echte lichtbronnen, in het licht van de gekozen omgeving;
|
||||||
|
// Nacht = omgevingslicht op 15% en de lampen altijd aan (100%). De
|
||||||
|
// tegelvloer ligt op het maaiveld, dus het ingegraven deel van vloer en
|
||||||
|
// sandwichpanelen verdwijnt eronder.
|
||||||
let lightsGroup = null;
|
let lightsGroup = null;
|
||||||
function applyRenderMode() {
|
function applyRenderMode() {
|
||||||
scene.background = new THREE.Color(fullRender ? 0x0b0f18 : 0x1a1a1a);
|
const lampsOn = fullRender || skyMode === 'night';
|
||||||
ambient.intensity = fullRender ? 0.1 : 0.6;
|
scene.background = new THREE.Color(0x1a1a1a);
|
||||||
dirLight.intensity = fullRender ? 0.12 : 0.8;
|
Skybox.apply(skyMode, { env: skyEnv, controls, sun: dirLight, ambient, base: { ambient: 0.6, sun: 0.8 }, shadowRoot: houseGroup });
|
||||||
ground.visible = fullRender;
|
ground.visible = fullRender && skyMode === 'none'; // zonder omgeving: vlak waarop het lamplicht valt
|
||||||
grid.visible = !fullRender;
|
grid.visible = skyMode === 'none' && !fullRender;
|
||||||
if (lightsGroup) { scene.remove(lightsGroup); lightsGroup = null; }
|
if (lightsGroup) { scene.remove(lightsGroup); lightsGroup = null; }
|
||||||
// De nep-lichtbundels (kegels) alleen in de snelle weergave; in de
|
// De nep-lichtbundels (kegels) alleen zonder echte lampen.
|
||||||
// volle render schijnt er echt licht.
|
if (parts && parts.lightingGroup) parts.lightingGroup.traverse(o => { if (o.userData.beam) o.visible = !lampsOn; });
|
||||||
if (parts && parts.lightingGroup) parts.lightingGroup.traverse(o => { if (o.userData.beam) o.visible = !fullRender; });
|
if (lampsOn && parts && lampenVisible) {
|
||||||
if (fullRender && parts && lampenVisible) {
|
|
||||||
const edge = dakVisible ? parts.roofEdge : null;
|
const edge = dakVisible ? parts.roofEdge : null;
|
||||||
lightsGroup = LightingModel.buildLights3D(parts.lighting.lamps, edge ? edge.spots : [], edge ? edge.top - edge.fasciaHeight : 0);
|
lightsGroup = LightingModel.buildLights3D(parts.lighting.lamps, edge ? edge.spots : [], edge ? edge.top - edge.fasciaHeight : 0, parts.lighting.indoor, {
|
||||||
|
outdoorFactor: skyMode === 'night' ? 1 : LightingModel.DAY_OUTDOOR_FACTOR
|
||||||
|
});
|
||||||
scene.add(lightsGroup);
|
scene.add(lightsGroup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Skybox.createToggle(document.getElementById('ho-toolbar-3d'), (m) => { skyMode = m; applyRenderMode(); });
|
||||||
document.getElementById('ho-btn-render').addEventListener('click', (e) => {
|
document.getElementById('ho-btn-render').addEventListener('click', (e) => {
|
||||||
fullRender = !fullRender;
|
fullRender = !fullRender;
|
||||||
e.currentTarget.classList.toggle('active', fullRender);
|
e.currentTarget.classList.toggle('active', fullRender);
|
||||||
@@ -578,6 +606,13 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
|
// Binnenspots (plafond): open cirkeltje
|
||||||
|
ctx.strokeStyle = '#ffe9a8'; ctx.lineWidth = 1.5;
|
||||||
|
(parts.lighting.indoor || []).forEach(sp => {
|
||||||
|
const q = toPx(sp.x, sp.z);
|
||||||
|
ctx.beginPath(); ctx.arc(q.x, q.y, 3.5, 0, Math.PI * 2); ctx.stroke();
|
||||||
|
});
|
||||||
|
|
||||||
// Gevellampen: blokje op het gevelvlak
|
// Gevellampen: blokje op het gevelvlak
|
||||||
ctx.fillStyle = '#ffd36b'; ctx.strokeStyle = '#1a1a1a'; ctx.lineWidth = 1;
|
ctx.fillStyle = '#ffd36b'; ctx.strokeStyle = '#1a1a1a'; ctx.lineWidth = 1;
|
||||||
parts.lighting.lamps.forEach(l => {
|
parts.lighting.lamps.forEach(l => {
|
||||||
@@ -617,227 +652,215 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Eigenschappenpaneel (contextafhankelijk) ---
|
// --- Eigenschappenpaneel (contextafhankelijk, opgebouwd met UI-kit) ---
|
||||||
const num = (id, label, value, step) =>
|
const { num, sel, toggle, section, info, button, actions } = UI;
|
||||||
`<div class="control-group"><label for="${id}">${label}:</label><div><input type="number" id="${id}" value="${value}" step="${step || 50}"><span class="unit">mm</span></div></div>`;
|
const cols = UI.grid; // (grid = de 3D-rasterhulp)
|
||||||
const sel = (id, label, options, value) =>
|
UI.wireTabs(propsEl);
|
||||||
`<div class="control-group"><label for="${id}">${label}:</label><div><select id="${id}">${Object.entries(options).map(([k, v]) => `<option value="${k}" ${k === value ? 'selected' : ''}>${v}</option>`).join('')}</select></div></div>`;
|
const head = (iconName, title, sub) =>
|
||||||
|
`<div class="ui-panel-head"><h1>${UI.icon(iconName)}${title}</h1>${sub ? `<span class="ui-sub">${sub}</span>` : ''}</div>`;
|
||||||
|
const after = () => refresh({ keepProps: true }); // getal: panelen blijven staan
|
||||||
|
const rerender = () => refresh(); // keuze: velden kunnen wijzigen
|
||||||
|
|
||||||
function renderProps() {
|
function renderProps() {
|
||||||
const warn = parts.overlaps.length
|
const warn = (parts.overlaps.length ? UI.warn(`Units overlappen elkaar: ${parts.overlaps.map(p => p.join(' + ')).join(', ')}.`) : '') +
|
||||||
? `<div class="global-warning">Let op: units overlappen elkaar (${parts.overlaps.map(p => p.join(' + ')).join(', ')}).</div>` : '';
|
parts.rects.filter(r => r.error).map(r => UI.warn(`Unit ${r.id}: ${r.error}`)).join('');
|
||||||
const errs = parts.rects.filter(r => r.error).map(r => `<div class="global-warning">Unit ${r.id}: ${r.error}</div>`).join('');
|
|
||||||
|
|
||||||
if (selection && selection.type === 'unit' && unitById(selection.id)) {
|
if (selection && selection.type === 'unit' && unitById(selection.id)) renderUnitProps(unitById(selection.id), warn);
|
||||||
const u = unitById(selection.id);
|
else if (selection && selection.type === 'wall') renderWallProps(warn);
|
||||||
const isRoot = state.units[0].id === u.id;
|
else if (selection && selection.type === 'iw' && iwById(selection.id)) renderIwProps(iwById(selection.id), warn);
|
||||||
const a = u.attach;
|
else renderHouseProps(warn);
|
||||||
const axis = a && (a.side === 'voor' || a.side === 'achter') ? 'x' : 'z';
|
|
||||||
propsEl.innerHTML = `${warn}${errs}
|
|
||||||
<div class="controls-grid">
|
|
||||||
<div>
|
|
||||||
<h1>Unit ${u.id}</h1>
|
|
||||||
${num('hp-width', 'Breedte (west-oost)', u.unit.floor.floor.width)}
|
|
||||||
${num('hp-depth', 'Diepte (noord-zuid)', u.unit.floor.floor.depth)}
|
|
||||||
${sel('hp-corners', 'Hoeken', UnitModel.CORNER_MODES, u.unit.layout.corners)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1>Koppeling</h1>
|
|
||||||
${isRoot ? '<div class="material-info">Basis-unit: noordwest-hoek op (0, 0). Andere units koppelen hieraan.</div>' : `
|
|
||||||
${sel('hp-att-to', 'Gekoppeld aan', Object.fromEntries(state.units.filter(o => o.id !== u.id).map(o => [o.id, 'Unit ' + o.id])), a.to)}
|
|
||||||
${sel('hp-att-side', 'Aan zijde', HouseModel.SIDE_LABELS, a.side)}
|
|
||||||
${sel('hp-att-align', 'Uitlijning', HouseModel.ALIGN_LABELS[axis], a.align)}
|
|
||||||
${num('hp-att-offset', 'Verschuiving', a.offset || 0)}`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="divider"></div>
|
|
||||||
<div class="controls-grid">
|
|
||||||
<div>
|
|
||||||
<h1>Dak (plat)</h1>
|
|
||||||
${sel('hp-roof-follow', 'Dak volgt vloer', { ja: 'Ja (standaard)', nee: 'Nee, eigen maat' }, u.roof.followFloor ? 'ja' : 'nee')}
|
|
||||||
${u.roof.followFloor ? '' : `
|
|
||||||
${num('hp-roof-w', 'Dakbreedte (west-oost)', u.roof.deck.floor.width)}
|
|
||||||
${num('hp-roof-d', 'Dakdiepte (noord-zuid)', u.roof.deck.floor.depth)}`}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
${u.roof.followFloor ? '<div class="material-info">Het dak heeft dezelfde maat als de vloer. Kies "Nee, eigen maat" om het dak langer/breder te maken of te verschuiven.</div>' : `
|
|
||||||
<h1>Positie dak</h1>
|
|
||||||
${num('hp-roof-ox', 'Verschuiving oost (+)', u.roof.offsetX || 0)}
|
|
||||||
${num('hp-roof-oz', 'Verschuiving zuid (+)', u.roof.offsetZ || 0)}`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="divider"></div>
|
|
||||||
<div class="controls-grid">
|
|
||||||
<div>
|
|
||||||
<h1>Dakrand</h1>
|
|
||||||
${sel('hp-fascia', 'Boeidelen', { aan: 'Aan', uit: 'Uit (daktrim 80 mm)' }, u.roof.fascia !== false ? 'aan' : 'uit')}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="material-info">Buitenverlichting (type, richting, afstanden) is woning-breed: Overzicht, niets geselecteerd.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ho-prop-actions">
|
|
||||||
<button type="button" class="io-btn" data-go="floor:${u.id}">Vloer bewerken</button>
|
|
||||||
<button type="button" class="io-btn" data-go="roof:${u.id}">Dak bewerken</button>
|
|
||||||
${isRoot ? '' : `<button type="button" class="io-btn io-btn-danger" data-remove-unit="${u.id}">Unit verwijderen</button>`}
|
|
||||||
</div>`;
|
|
||||||
const onNum = (id, fn) => document.getElementById(id) && document.getElementById(id).addEventListener('input', (e) => {
|
|
||||||
const v = parseFloat(e.target.value);
|
|
||||||
if (isFinite(v)) { fn(v); refresh({ keepProps: true }); }
|
|
||||||
});
|
|
||||||
onNum('hp-width', v => { if (v >= 1000) u.unit.floor.floor.width = v; });
|
|
||||||
onNum('hp-depth', v => { if (v >= 1000) u.unit.floor.floor.depth = v; });
|
|
||||||
onNum('hp-att-offset', v => { u.attach.offset = v; });
|
|
||||||
const onSel = (id, fn) => document.getElementById(id) && document.getElementById(id).addEventListener('input', (e) => { fn(e.target.value); refresh(); });
|
|
||||||
onSel('hp-corners', v => { u.unit.layout.corners = v; });
|
|
||||||
onSel('hp-att-to', v => { u.attach.to = v; });
|
|
||||||
onSel('hp-att-side', v => { u.attach.side = v; });
|
|
||||||
onSel('hp-att-align', v => { u.attach.align = v; });
|
|
||||||
onSel('hp-roof-follow', v => { u.roof.followFloor = v === 'ja'; });
|
|
||||||
onSel('hp-fascia', v => { u.roof.fascia = v === 'aan'; });
|
|
||||||
onNum('hp-roof-w', v => { if (v >= 500) u.roof.deck.floor.width = v; });
|
|
||||||
onNum('hp-roof-d', v => { if (v >= 500) u.roof.deck.floor.depth = v; });
|
|
||||||
onNum('hp-roof-ox', v => { u.roof.offsetX = v; });
|
|
||||||
onNum('hp-roof-oz', v => { u.roof.offsetZ = v; });
|
|
||||||
} else if (selection && selection.type === 'wall') {
|
|
||||||
const pu = parts.units.find(x => x.id === selection.unit);
|
|
||||||
const wl = pu && pu.walls.find(x => x.side === selection.side);
|
|
||||||
const piece = wl && wl.pieces.find(p => p.element === selection.element);
|
|
||||||
if (!piece) { selection = null; return renderProps(); }
|
|
||||||
propsEl.innerHTML = `${warn}
|
|
||||||
<h1>${piece.element} — ${piece.label}</h1>
|
|
||||||
<div class="result-row"><span>Lengte volle zijde:</span><span class="highlight">${wl.width} mm</span></div>
|
|
||||||
<div class="result-row"><span>Dit wandstuk:</span><span class="highlight">${piece.place.width} mm (${Math.round(piece.u0)}–${Math.round(piece.u1)} vanaf links, van buitenaf gezien)</span></div>
|
|
||||||
<div class="result-row"><span>Naad met andere unit:</span><span class="highlight">${wl.cutsU.length ? wl.cutsU.map(([a, b]) => `${Math.round(a)}–${Math.round(b)}`).join(', ') + ' mm vervalt' : 'nee'}</span></div>
|
|
||||||
<div class="result-row"><span>Openingen in dit stuk:</span><span class="highlight">${piece.config.openings.length}</span></div>
|
|
||||||
${state.settings.lighting.mode === 'gevel' ? `
|
|
||||||
<div class="divider"></div>
|
|
||||||
<h1>Gevellampen op deze wand</h1>
|
|
||||||
${sel('hw-light', 'Lampen', LightingModel.WALL_OVERRIDES, unitById(pu.id).lighting.walls[wl.side] || 'auto')}
|
|
||||||
<div class="result-row"><span>Lampen op dit wandstuk:</span><span class="highlight">${piece.lamps.length}</span></div>
|
|
||||||
<div class="material-info">Automatisch: vanaf ${state.settings.lighting.endOffset} mm van hoek/uiteinde, alleen op vrije stukken gevel ≥ ${state.settings.lighting.minWallWidth} mm op ${state.settings.lighting.lampHeight} mm hoogte (smallere stukken krijgen geen lampen). Override: ${state.settings.lighting.openingOffset} mm naast elke opening.</div>`
|
|
||||||
: '<div class="material-info">Gevellampen: kies in de woning-instellingen "Buitenverlichting: Gevellampen".</div>'}
|
|
||||||
${wl.dropped.length ? `<div class="global-warning">${wl.dropped.length} opening(en) van deze zijde vallen in een naad en vervallen.</div>` : ''}
|
|
||||||
<div class="ho-prop-actions"><button type="button" class="io-btn" data-go="wall:${pu.id}:${wl.side}">Wand bewerken</button></div>`;
|
|
||||||
const hw = document.getElementById('hw-light');
|
|
||||||
if (hw) hw.addEventListener('input', (e) => { unitById(pu.id).lighting.walls[wl.side] = e.target.value; refresh(); });
|
|
||||||
} else if (selection && selection.type === 'iw' && iwById(selection.id)) {
|
|
||||||
const iw = iwById(selection.id);
|
|
||||||
const p = parts.interior.find(x => x.id === iw.id);
|
|
||||||
propsEl.innerHTML = `${warn}
|
|
||||||
<div class="controls-grid">
|
|
||||||
<div>
|
|
||||||
<h1>${p.element} — binnenwand</h1>
|
|
||||||
${num('hi-x1', 'Begin X', iw.x1)}${num('hi-z1', 'Begin Y', iw.z1)}
|
|
||||||
${num('hi-x2', 'Eind X', iw.x2)}${num('hi-z2', 'Eind Y', iw.z2)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1>Opbouw</h1>
|
|
||||||
${num('hi-thick', 'Dikte', HouseModel.thicknessOf(iw.wall), 5)}
|
|
||||||
${num('hi-height', 'Hoogte', iw.wall.wall.heightAll)}
|
|
||||||
<div class="result-row"><span>Lengte:</span><span class="highlight">${p.place.width} mm</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ho-prop-actions">
|
|
||||||
<button type="button" class="io-btn" data-go="iw:${iw.id}">Wand bewerken (openingen)</button>
|
|
||||||
<button type="button" class="io-btn io-btn-danger" data-remove-iw="${iw.id}">Verwijderen</button>
|
|
||||||
</div>`;
|
|
||||||
const onNum = (id, fn) => document.getElementById(id).addEventListener('input', (e) => {
|
|
||||||
const v = parseFloat(e.target.value);
|
|
||||||
if (isFinite(v)) { fn(v); refresh({ keepProps: true }); }
|
|
||||||
});
|
|
||||||
['x1', 'z1', 'x2', 'z2'].forEach(f => onNum('hi-' + f, v => { iw[f] = v; }));
|
|
||||||
onNum('hi-thick', v => { if (v >= 40) HouseModel.setThickness(iw.wall, v); });
|
|
||||||
onNum('hi-height', v => { if (v >= 500) iw.wall.wall.heightAll = iw.wall.wall.heightLeft = iw.wall.wall.heightRight = v; });
|
|
||||||
} else {
|
|
||||||
propsEl.innerHTML = `${warn}${errs}
|
|
||||||
<h1>Woning</h1>
|
|
||||||
<div class="material-info">${state.units.length} unit(s), ${state.interiorWalls.length} binnenwand(en).
|
|
||||||
Klik in de plattegrond op een unit, wand of binnenwand om die te selecteren; dubbelklik om te bewerken.
|
|
||||||
Waar units tegen elkaar staan (oranje gestippeld) vervalt de wand.</div>
|
|
||||||
<div class="ho-prop-actions">
|
|
||||||
<button type="button" class="io-btn" data-action="add-unit">+ Unit toevoegen</button>
|
|
||||||
<button type="button" class="io-btn" data-action="draw-iw">Binnenwand tekenen</button>
|
|
||||||
<button type="button" class="io-btn" data-action="project">Project eigenschappen</button>
|
|
||||||
</div>
|
|
||||||
<div class="divider"></div>
|
|
||||||
${renderSettingsHtml()}`;
|
|
||||||
wireSettings();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Woning-instellingen: h.o.h. voor ALLE buiten-/binnenwanden + dakrand.
|
function renderUnitProps(u, warn) {
|
||||||
function renderSettingsHtml() {
|
const isRoot = state.units[0].id === u.id;
|
||||||
const sp = state.settings.spacing, r = state.settings.roof, lt = state.settings.lighting;
|
const a = u.attach;
|
||||||
return `
|
const axis = a && (a.side === 'voor' || a.side === 'achter') ? 'x' : 'z';
|
||||||
<div class="controls-grid">
|
const f = u.unit.floor.floor;
|
||||||
<div>
|
const koppeling = isRoot
|
||||||
<h1>Gevelbekleding (test, alle buitenwanden)</h1>
|
? info('Basis-unit: de noordwesthoek ligt op (0, 0). Andere units koppelen hieraan.')
|
||||||
${sel('hs-cladding', 'Richting WEO/Novalit', WallModel.CLADDING_DIRECTIONS, state.settings.cladding)}
|
: sel('hp-att-to', 'Gekoppeld aan', Object.fromEntries(state.units.filter(o => o.id !== u.id).map(o => [o.id, 'Unit ' + o.id])), a.to) +
|
||||||
${sel('hs-trasraam', 'Trasraam (cement-gebonden plaat)', { aan: 'Aan', uit: 'Uit' }, state.settings.trasraam.enabled ? 'aan' : 'uit')}
|
sel('hp-att-side', 'Zijde', HouseModel.SIDE_LABELS, a.side) +
|
||||||
${state.settings.trasraam.enabled ? num('hs-trasraam-h', 'Hoogte trasraam (vanaf bovenkant vloer)', state.settings.trasraam.height, 10) : ''}
|
sel('hp-att-align', 'Uitlijning', HouseModel.ALIGN_LABELS[axis], a.align) +
|
||||||
${sel('hs-sandwich', `Sandwichpanelen rondom (${SandwichModel.DEPTH_MM} mm de grond in)`, { aan: 'Aan', uit: 'Uit' }, state.settings.sandwich ? 'aan' : 'uit')}
|
num('hp-att-offset', 'Verschuiving', a.offset || 0);
|
||||||
<div class="divider"></div>
|
const dak = toggle('hp-roof-follow', 'Maat volgt vloer', u.roof.followFloor) +
|
||||||
<h1>Regelwerk h.o.h. (alle wanden)</h1>
|
(u.roof.followFloor ? '' :
|
||||||
${num('hs-ext-min', 'Buitenwanden min.', sp.exterior.min, 10)}
|
num('hp-roof-w', 'Breedte', u.roof.deck.floor.width, { hint: 'West–oost' }) +
|
||||||
${num('hs-ext-max', 'Buitenwanden max.', sp.exterior.max, 10)}
|
num('hp-roof-d', 'Diepte', u.roof.deck.floor.depth, { hint: 'Noord–zuid' }) +
|
||||||
${num('hs-int-min', 'Binnenwanden min.', sp.interior.min, 10)}
|
num('hp-roof-ox', 'Verschuiving oost', u.roof.offsetX || 0) +
|
||||||
${num('hs-int-max', 'Binnenwanden max.', sp.interior.max, 10)}
|
num('hp-roof-oz', 'Verschuiving zuid', u.roof.offsetZ || 0)) +
|
||||||
</div>
|
toggle('hp-fascia', 'Boeidelen', u.roof.fascia !== false, { hint: 'Uit = daktrim van 80 mm' });
|
||||||
<div>
|
|
||||||
<h1>Dakrand (maten, hele woning)</h1>
|
propsEl.innerHTML = warn + head('box', `Unit ${u.id}`, `${f.width} × ${f.depth} mm`) +
|
||||||
${num('hs-overhang', `Overstek boeideel (${RoofModel.OVERHANG_MIN}–${RoofModel.OVERHANG_MAX})`, r.overhang, 10)}
|
UI.tabs('unit', [
|
||||||
${num('hs-fascia-h', 'Hoogte boeideel', r.fasciaHeight, 10)}
|
{ key: 'maat', label: 'Afmetingen', icon: 'rulers', html: cols(
|
||||||
${num('hs-trim-h', 'Hoogte daktrim', r.trimHeight, 5)}
|
num('hp-width', 'Breedte', f.width, { hint: 'West–oost' }) + num('hp-depth', 'Diepte', f.depth, { hint: 'Noord–zuid' }),
|
||||||
${num('hs-spot-min', 'Spots min. h.o.h.', r.spotMin, 50)}
|
sel('hp-corners', 'Hoekaansluiting', UnitModel.CORNER_MODES, u.unit.layout.corners)) },
|
||||||
${num('hs-spot-max', 'Spots max. h.o.h.', r.spotMax, 50)}
|
{ key: 'koppeling', label: 'Koppeling', icon: 'link-45deg', html: cols(koppeling, '') },
|
||||||
</div>
|
{ key: 'dak', label: 'Dak', icon: 'house', html: cols(dak, u.roof.followFloor ? info('Het dak heeft de maat van de vloer. Zet "Maat volgt vloer" uit voor een eigen maat of verschuiving.') : '') }
|
||||||
</div>
|
]) +
|
||||||
<div class="divider"></div>
|
actions(
|
||||||
<div class="controls-grid">
|
button('Vloer bewerken', 'grid-3x3', `data-go="floor:${u.id}"`),
|
||||||
<div>
|
button('Dak bewerken', 'house', `data-go="roof:${u.id}"`),
|
||||||
<h1>Buitenverlichting (hele woning)</h1>
|
isRoot ? '' : button('Unit verwijderen', 'trash', `data-remove-unit="${u.id}"`, 'ui-btn-danger'));
|
||||||
${sel('hs-light-mode', 'Type', LightingModel.MODES, lt.mode)}
|
|
||||||
${lt.mode === 'gevel' ? sel('hs-light-dir', 'Lichtrichting', LightingModel.DIRECTIONS, lt.direction) : ''}
|
UI.onNum('hp-width', v => { if (v >= 1000) f.width = v; }, after);
|
||||||
${lt.mode === 'spots' ? '<div class="material-info">Boeideel-spots komen alleen bij units met boeidelen.</div>' : ''}
|
UI.onNum('hp-depth', v => { if (v >= 1000) f.depth = v; }, after);
|
||||||
${num('hs-lamp-h', 'Lamphoogte', lt.lampHeight, 50)}
|
UI.onNum('hp-att-offset', v => { u.attach.offset = v; }, after);
|
||||||
${num('hs-lamp-min', 'Lampen min. h.o.h.', lt.lampMin, 50)}
|
UI.onSel('hp-corners', v => { u.unit.layout.corners = v; }, rerender);
|
||||||
${num('hs-lamp-max', 'Lampen max. h.o.h.', lt.lampMax, 50)}
|
UI.onSel('hp-att-to', v => { u.attach.to = v; }, rerender);
|
||||||
</div>
|
UI.onSel('hp-att-side', v => { u.attach.side = v; }, rerender);
|
||||||
<div>
|
UI.onSel('hp-att-align', v => { u.attach.align = v; }, rerender);
|
||||||
<h1> </h1>
|
UI.onToggle('hp-roof-follow', v => { u.roof.followFloor = v; }, rerender);
|
||||||
${num('hs-lamp-w', 'Min. vrije gevelbreedte', lt.minWallWidth, 50)}
|
UI.onToggle('hp-fascia', v => { u.roof.fascia = v; }, rerender);
|
||||||
${num('hs-lamp-end', 'Vanaf hoek/uiteinde', lt.endOffset, 50)}
|
UI.onNum('hp-roof-w', v => { if (v >= 500) u.roof.deck.floor.width = v; }, after);
|
||||||
${num('hs-lamp-open', 'Override: naast opening', lt.openingOffset, 50)}
|
UI.onNum('hp-roof-d', v => { if (v >= 500) u.roof.deck.floor.depth = v; }, after);
|
||||||
</div>
|
UI.onNum('hp-roof-ox', v => { u.roof.offsetX = v; }, after);
|
||||||
</div>`;
|
UI.onNum('hp-roof-oz', v => { u.roof.offsetZ = v; }, after);
|
||||||
}
|
}
|
||||||
function wireSettings() {
|
|
||||||
const sp = state.settings.spacing, r = state.settings.roof;
|
function renderWallProps(warn) {
|
||||||
const onNum = (id, fn) => { const el = document.getElementById(id); if (el) el.addEventListener('input', (e) => { const v = parseFloat(e.target.value); if (isFinite(v) && v > 0) { fn(v); refresh({ keepProps: true }); } }); };
|
const pu = parts.units.find(x => x.id === selection.unit);
|
||||||
const onSel = (id, fn) => { const el = document.getElementById(id); if (el) el.addEventListener('input', (e) => { fn(e.target.value); refresh(); }); };
|
const wl = pu && pu.walls.find(x => x.side === selection.side);
|
||||||
onSel('hs-cladding', v => { state.settings.cladding = v; });
|
const piece = wl && wl.pieces.find(p => p.element === selection.element);
|
||||||
onSel('hs-trasraam', v => { state.settings.trasraam.enabled = v === 'aan'; });
|
if (!piece) { selection = null; return renderProps(); }
|
||||||
onSel('hs-sandwich', v => { state.settings.sandwich = v === 'aan'; });
|
|
||||||
onNum('hs-trasraam-h', v => { state.settings.trasraam.height = v; });
|
|
||||||
onNum('hs-ext-min', v => { sp.exterior.min = v; });
|
|
||||||
onNum('hs-ext-max', v => { sp.exterior.max = v; });
|
|
||||||
onNum('hs-int-min', v => { sp.interior.min = v; });
|
|
||||||
onNum('hs-int-max', v => { sp.interior.max = v; });
|
|
||||||
const lt = state.settings.lighting;
|
const lt = state.settings.lighting;
|
||||||
onNum('hs-overhang', v => { r.overhang = Math.min(RoofModel.OVERHANG_MAX, Math.max(RoofModel.OVERHANG_MIN, v)); });
|
const rows = [
|
||||||
onNum('hs-fascia-h', v => { r.fasciaHeight = v; });
|
['Lengte zijde', wl.width, 'mm'],
|
||||||
onNum('hs-spot-min', v => { r.spotMin = v; });
|
['Lengte wandstuk', piece.place.width, 'mm'],
|
||||||
onSel('hs-light-mode', v => { lt.mode = v; });
|
['Positie', `${Math.round(piece.u0)} – ${Math.round(piece.u1)}`, 'mm'],
|
||||||
onSel('hs-light-dir', v => { lt.direction = v; });
|
['Naad', wl.cutsU.length ? wl.cutsU.map(([a, b]) => `${Math.round(a)} – ${Math.round(b)}`).join(', ') : 'geen', wl.cutsU.length ? 'mm' : ''],
|
||||||
onNum('hs-lamp-h', v => { lt.lampHeight = v; });
|
['Openingen', piece.config.openings.length, '']
|
||||||
onNum('hs-lamp-min', v => { lt.lampMin = v; });
|
];
|
||||||
onNum('hs-lamp-max', v => { lt.lampMax = v; });
|
const lampen = lt.mode === 'gevel'
|
||||||
onNum('hs-lamp-w', v => { lt.minWallWidth = v; });
|
? sel('hw-light', 'Lampen', LightingModel.WALL_OVERRIDES, unitById(pu.id).lighting.walls[wl.side] || 'auto',
|
||||||
onNum('hs-lamp-end', v => { lt.endOffset = v; });
|
{ hint: `Automatisch: ${lt.endOffset} mm uit de hoek, alleen op vrije gevel van min. ${lt.minWallWidth} mm. Naast openingen: ${lt.openingOffset} mm naast elke opening.` }) +
|
||||||
onNum('hs-lamp-open', v => { lt.openingOffset = v; });
|
UI.specCard('Op dit wandstuk', 'lightbulb', [['Gevellampen', piece.lamps.length, 'st.']])
|
||||||
onNum('hs-spot-max', v => { r.spotMax = v; });
|
: info('Gevellampen staan uit. Kies bij Woning › Verlichting het type "Gevellampen".');
|
||||||
onNum('hs-trim-h', v => { r.trimHeight = v; });
|
propsEl.innerHTML = warn + head('bricks', piece.element, piece.label) +
|
||||||
|
cols(UI.specCard('Wandstuk', 'rulers', rows), section('Gevellampen', 'lightbulb', lampen)) +
|
||||||
|
(wl.dropped.length ? UI.warn(`${wl.dropped.length} opening(en) van deze zijde vallen in een naad en vervallen.`) : '') +
|
||||||
|
actions(button('Wand bewerken', 'pencil-square', `data-go="wall:${pu.id}:${wl.side}"`));
|
||||||
|
UI.onSel('hw-light', v => { unitById(pu.id).lighting.walls[wl.side] = v; }, rerender);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIwProps(iw, warn) {
|
||||||
|
const p = parts.interior.find(x => x.id === iw.id);
|
||||||
|
if (iw.seam) return renderSeamProps(iw, warn);
|
||||||
|
if (!p) { selection = null; return renderProps(); }
|
||||||
|
propsEl.innerHTML = warn + head('distribute-horizontal', p.element, `Binnenwand · ${p.place.width} mm`) +
|
||||||
|
cols(
|
||||||
|
section('Positie', 'crosshair', num('hi-x1', 'Begin X', iw.x1) + num('hi-z1', 'Begin Y', iw.z1) + num('hi-x2', 'Eind X', iw.x2) + num('hi-z2', 'Eind Y', iw.z2)),
|
||||||
|
section('Opbouw', 'layers', num('hi-thick', 'Dikte', HouseModel.thicknessOf(iw.wall), { step: 5 }) + num('hi-height', 'Hoogte', iw.wall.wall.heightAll))) +
|
||||||
|
actions(
|
||||||
|
button('Openingen bewerken', 'pencil-square', `data-go="iw:${iw.id}"`),
|
||||||
|
button('Verwijderen', 'trash', `data-remove-iw="${iw.id}"`, 'ui-btn-danger'));
|
||||||
|
['x1', 'z1', 'x2', 'z2'].forEach(k => UI.onNum('hi-' + k, v => { iw[k] = v; }, after));
|
||||||
|
UI.onNum('hi-thick', v => { if (v >= 40) HouseModel.setThickness(iw.wall, v); }, after);
|
||||||
|
UI.onNum('hi-height', v => { if (v >= 500) iw.wall.wall.heightAll = iw.wall.wall.heightLeft = iw.wall.wall.heightRight = v; }, after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Naadwand: midden op de naad (twee delen, één per unit) of geheel in
|
||||||
|
// unit A/B (één wand). Eén set openingen en maten; de hoogte volgt per
|
||||||
|
// deel de daklijn van zijn unit.
|
||||||
|
function renderSeamProps(iw, warn) {
|
||||||
|
const leaves = parts.interior.filter(x => x.id === iw.id);
|
||||||
|
const title = `Naadwand ${iw.seam.a} | ${iw.seam.b}`;
|
||||||
|
const exposed = leaves.filter(l => l.skinFrom !== undefined);
|
||||||
|
const inUnit = iw.seam.in === 'a' || iw.seam.in === 'b' ? iw.seam[iw.seam.in] : null;
|
||||||
|
const plaatsen = { midden: 'Midden op de naad (2 delen)', a: `Geheel in unit ${iw.seam.a}`, b: `Geheel in unit ${iw.seam.b}` };
|
||||||
|
const sheet = iw.wall.wall.sheathingThick;
|
||||||
|
const dikte = inUnit
|
||||||
|
? num('hi-thick', 'Dikte', HouseModel.thicknessOf(iw.wall) + sheet, { step: 5, hint: 'Regelwerk + beplating aan beide zijden' })
|
||||||
|
: num('hi-thick', 'Dikte per deel', HouseModel.thicknessOf(iw.wall), { step: 5, hint: 'Regelwerk + beplating aan de kamerzijde; naadzijde zonder plaat' });
|
||||||
|
const uitleg = inUnit
|
||||||
|
? `Binnenwand geheel in unit ${inUnit}, tegen de naad aan, met beplating aan beide zijden. De hoogte volgt de daklijn van unit ${inUnit}.`
|
||||||
|
: 'Tussenwand op de naad, per unit een eigen deel dat tegen het andere aan staat. Openingen gelden voor beide delen (de deur loopt door). De hoogte volgt per deel de daklijn van zijn unit.';
|
||||||
|
propsEl.innerHTML = warn + head('layout-split', title, leaves.length ? (leaves.length === 1 ? '1 deel' : `${leaves.length} delen`) : 'vervalt') +
|
||||||
|
(leaves.length ? '' : UI.warn(`Unit ${iw.seam.a} en ${iw.seam.b} staan niet (meer) tegen elkaar; deze naadwand wordt niet gebouwd.`)) +
|
||||||
|
cols(
|
||||||
|
section('Opbouw', 'layers', sel('hi-seam-in', 'Plaats', plaatsen, iw.seam.in || 'midden') + dikte),
|
||||||
|
UI.specCard('Delen', 'layout-split', leaves.map(l => [`${l.element} (unit ${l.seamUnit})`, l.place.width, 'mm']))) +
|
||||||
|
info(uitleg +
|
||||||
|
exposed.map(l => ` ${l.element} steekt vanaf ${l.skinFrom} mm boven het dak van de andere unit uit; dat stuk krijgt buitenafwerking die met de gevel meeloopt.`).join('')) +
|
||||||
|
actions(
|
||||||
|
button('Openingen bewerken', 'pencil-square', `data-go="iw:${iw.id}"`),
|
||||||
|
button('Verwijderen', 'trash', `data-remove-iw="${iw.id}"`, 'ui-btn-danger'));
|
||||||
|
UI.onSel('hi-seam-in', v => { if (v === 'midden') delete iw.seam.in; else iw.seam.in = v; }, rerender);
|
||||||
|
UI.onNum('hi-thick', v => { const t = inUnit ? v - sheet : v; if (t >= 40) HouseModel.setThickness(iw.wall, t); }, after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Woning-instellingen (niets geselecteerd): alles wat voor de hele woning
|
||||||
|
// geldt, in tabbladen.
|
||||||
|
function renderHouseProps(warn) {
|
||||||
|
const st = state.settings, sp = st.spacing, r = st.roof, lt = st.lighting;
|
||||||
|
const pos = { positive: true };
|
||||||
|
const verlichting = lt.mode === 'gevel'
|
||||||
|
? cols(
|
||||||
|
section('Gevellampen', 'lightbulb', sel('hs-light-dir', 'Lichtrichting', LightingModel.DIRECTIONS, lt.direction) +
|
||||||
|
num('hs-lamp-h', 'Hoogte', lt.lampHeight, { hint: 'Vanaf bovenkant vloer' }) +
|
||||||
|
num('hs-lamp-min', 'Afstand min.', lt.lampMin, { hint: 'Hart-op-hart' }) + num('hs-lamp-max', 'Afstand max.', lt.lampMax, { hint: 'Hart-op-hart' })),
|
||||||
|
section('Plaatsing', 'distribute-horizontal', num('hs-lamp-w', 'Min. gevelbreedte', lt.minWallWidth, { hint: 'Smallere vrije stukken gevel krijgen geen lamp' }) +
|
||||||
|
num('hs-lamp-end', 'Afstand tot hoek', lt.endOffset) +
|
||||||
|
num('hs-lamp-open', 'Afstand tot opening', lt.openingOffset, { hint: 'Voor wanden met de instelling "Naast openingen"' })))
|
||||||
|
: lt.mode === 'spots'
|
||||||
|
? cols(section('Boeideel-spots', 'lightbulb', num('hs-spot-min', 'Afstand min.', r.spotMin, { hint: 'Hart-op-hart' }) + num('hs-spot-max', 'Afstand max.', r.spotMax, { hint: 'Hart-op-hart' })),
|
||||||
|
info('Spots komen alleen onder units met boeidelen.'))
|
||||||
|
: '';
|
||||||
|
|
||||||
|
propsEl.innerHTML = warn + head('house-door', 'Woning', `${state.units.length} unit(s) · ${state.interiorWalls.length} binnenwand(en)`) +
|
||||||
|
UI.tabs('woning', [
|
||||||
|
{ key: 'algemeen', label: 'Algemeen', icon: 'house-door', html:
|
||||||
|
info('Klik in de plattegrond op een unit, wand of binnenwand om te selecteren; dubbelklik om te bewerken. Waar units tegen elkaar staan (oranje gestippeld) vervalt de wand.') +
|
||||||
|
actions(button('Unit toevoegen', 'plus-lg', 'data-action="add-unit"'), button('Binnenwand tekenen', 'pencil', 'data-action="draw-iw"'),
|
||||||
|
button('Projectgegevens', 'card-text', 'data-action="project"')) },
|
||||||
|
{ key: 'gevel', label: 'Gevel', icon: 'bricks', html: cols(
|
||||||
|
section('Gevelbekleding', 'grip-horizontal', sel('hs-cladding', 'Richting', WallModel.CLADDING_DIRECTIONS, st.cladding, { hint: 'WEO/Novalit, voorlopig voor alle buitenwanden' })) +
|
||||||
|
section('Trasraam', 'bricks', toggle('hs-trasraam', 'Trasraam', st.trasraam.enabled, { hint: 'Cement-gebonden plaat onderaan de gevel' }) +
|
||||||
|
(st.trasraam.enabled ? num('hs-trasraam-h', 'Hoogte', st.trasraam.height, { step: 10, hint: 'Vanaf bovenkant vloer' }) : '')),
|
||||||
|
section('Sandwichpanelen', 'bounding-box', toggle('hs-sandwich', 'Rondom woning', st.sandwich,
|
||||||
|
{ hint: `${SandwichModel.PANEL_WIDTH_MM} × ${SandwichModel.PANEL_LENGTH_MM} × ${SandwichModel.THICK_MM} mm, vanaf bovenkant vloer ${SandwichModel.DEPTH_MM} mm naar beneden` })) +
|
||||||
|
section('Maaiveld', 'tree', num('hs-ground', 'Onder bovenkant vloer', st.groundLevel, { step: 10, hint: 'Hoogte van de tegelvloer in de 3D-weergave bij Dag/Nacht' }))) },
|
||||||
|
{ key: 'constructie', label: 'Regelwerk', icon: 'columns-gap', html: cols(
|
||||||
|
section('Buitenwanden', 'bricks', num('hs-ext-min', 'Afstand min.', sp.exterior.min, { step: 10, hint: 'Hart-op-hart stijlen' }) + num('hs-ext-max', 'Afstand max.', sp.exterior.max, { step: 10, hint: 'Hart-op-hart stijlen' })),
|
||||||
|
section('Binnenwanden', 'distribute-horizontal', num('hs-int-min', 'Afstand min.', sp.interior.min, { step: 10, hint: 'Hart-op-hart stijlen' }) + num('hs-int-max', 'Afstand max.', sp.interior.max, { step: 10, hint: 'Hart-op-hart stijlen' }))) },
|
||||||
|
{ key: 'dak', label: 'Dakrand', icon: 'house', html: cols(
|
||||||
|
section('Boeideel', 'border-outer', num('hs-overhang', 'Overstek', r.overhang, { step: 10, hint: `${RoofModel.OVERHANG_MIN} – ${RoofModel.OVERHANG_MAX} mm` }) + num('hs-fascia-h', 'Hoogte', r.fasciaHeight, { step: 10 })),
|
||||||
|
section('Daktrim', 'border-top', num('hs-trim-h', 'Hoogte', r.trimHeight, { step: 5, hint: 'Bij units zonder boeidelen' }))) },
|
||||||
|
{ key: 'verlichting', label: 'Verlichting', icon: 'lightbulb', html:
|
||||||
|
cols(section('Buiten', 'sun', sel('hs-light-mode', 'Type', LightingModel.MODES, lt.mode)),
|
||||||
|
section('Binnenspots', 'circle', toggle('hs-indoor', 'Spots in plafond', lt.indoor, { hint: 'Per unit verdeeld in het plafond' }) +
|
||||||
|
(lt.indoor ? num('hs-indoor-min', 'Afstand min.', lt.indoorMin, { hint: 'Hart-op-hart, in beide richtingen' }) +
|
||||||
|
num('hs-indoor-max', 'Afstand max.', lt.indoorMax, { hint: 'Hart-op-hart, in beide richtingen' }) +
|
||||||
|
num('hs-indoor-off', 'Afstand tot wand', lt.indoorOffset, { hint: 'Tot de binnenzijde van een wand, of tot de unitrand bij een naad' }) : ''))) +
|
||||||
|
verlichting }
|
||||||
|
]);
|
||||||
|
|
||||||
|
UI.onSel('hs-cladding', v => { st.cladding = v; }, rerender);
|
||||||
|
UI.onToggle('hs-trasraam', v => { st.trasraam.enabled = v; }, rerender);
|
||||||
|
UI.onToggle('hs-sandwich', v => { st.sandwich = v; }, rerender);
|
||||||
|
UI.onNum('hs-trasraam-h', v => { st.trasraam.height = v; }, after, pos);
|
||||||
|
UI.onNum('hs-ground', v => { if (v >= 0) st.groundLevel = v; }, after);
|
||||||
|
UI.onNum('hs-ext-min', v => { sp.exterior.min = v; }, after, pos);
|
||||||
|
UI.onNum('hs-ext-max', v => { sp.exterior.max = v; }, after, pos);
|
||||||
|
UI.onNum('hs-int-min', v => { sp.interior.min = v; }, after, pos);
|
||||||
|
UI.onNum('hs-int-max', v => { sp.interior.max = v; }, after, pos);
|
||||||
|
UI.onNum('hs-overhang', v => { r.overhang = Math.min(RoofModel.OVERHANG_MAX, Math.max(RoofModel.OVERHANG_MIN, v)); }, after, pos);
|
||||||
|
UI.onNum('hs-fascia-h', v => { r.fasciaHeight = v; }, after, pos);
|
||||||
|
UI.onNum('hs-trim-h', v => { r.trimHeight = v; }, after, pos);
|
||||||
|
UI.onNum('hs-spot-min', v => { r.spotMin = v; }, after, pos);
|
||||||
|
UI.onNum('hs-spot-max', v => { r.spotMax = v; }, after, pos);
|
||||||
|
UI.onSel('hs-light-mode', v => { lt.mode = v; }, rerender);
|
||||||
|
UI.onSel('hs-light-dir', v => { lt.direction = v; }, rerender);
|
||||||
|
UI.onNum('hs-lamp-h', v => { lt.lampHeight = v; }, after, pos);
|
||||||
|
UI.onNum('hs-lamp-min', v => { lt.lampMin = v; }, after, pos);
|
||||||
|
UI.onNum('hs-lamp-max', v => { lt.lampMax = v; }, after, pos);
|
||||||
|
UI.onNum('hs-lamp-w', v => { lt.minWallWidth = v; }, after, pos);
|
||||||
|
UI.onNum('hs-lamp-end', v => { lt.endOffset = v; }, after, pos);
|
||||||
|
UI.onNum('hs-lamp-open', v => { lt.openingOffset = v; }, after, pos);
|
||||||
|
UI.onToggle('hs-indoor', v => { lt.indoor = v; }, rerender);
|
||||||
|
UI.onNum('hs-indoor-min', v => { lt.indoorMin = v; }, after, pos);
|
||||||
|
UI.onNum('hs-indoor-max', v => { lt.indoorMax = v; }, after, pos);
|
||||||
|
UI.onNum('hs-indoor-off', v => { lt.indoorOffset = v; }, after);
|
||||||
}
|
}
|
||||||
|
|
||||||
propsEl.addEventListener('click', (e) => {
|
propsEl.addEventListener('click', (e) => {
|
||||||
@@ -859,42 +882,34 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
else if (act.dataset.action === 'project') openProjectProperties();
|
else if (act.dataset.action === 'project') openProjectProperties();
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Specificaties/totalen ---
|
// --- Specificaties/totalen (onder de 3D) ---
|
||||||
function renderSpecs(totals) {
|
function renderSpecs(totals) {
|
||||||
const row = (label, value) => `<div class="result-row"><span>${label}:</span><span class="highlight">${value}</span></div>`;
|
const f = houseFmt;
|
||||||
document.getElementById('ho-specs').innerHTML = `
|
const cards = [
|
||||||
<div class="controls-grid">
|
UI.specCard('Gewicht', 'speedometer2', totals.weights.map(r => [r.label, f(r.kg), 'kg']), ['Totaal woning', f(totals.totalKg), 'kg']),
|
||||||
<div>
|
UI.specCard('Hout & isolatie', 'tree', [
|
||||||
<div class="spec-section"><h1>Gewicht</h1>
|
['Balken', totals.beamCount, 'st.'],
|
||||||
${totals.weights.map(r => row(r.label, houseFmt(r.kg) + ' kg')).join('')}
|
['Volume hout', f(totals.woodM3, 3), 'm³'],
|
||||||
<div class="divider"></div>${row('Totaal woning', houseFmt(totals.totalKg) + ' kg')}
|
['Inkoop wandbalken', WallModel.purchaseText(totals.purchase), ''],
|
||||||
</div>
|
['Snijverlies', `${f(totals.purchaseWasteMM / 1000, 2)} m · ${f(totals.purchaseStockMM ? totals.purchaseWasteMM / totals.purchaseStockMM * 100 : 0)}`, '%'],
|
||||||
</div>
|
[`Latwerk ${WallModel.BATTEN.thick}×${WallModel.BATTEN.width}`, `${totals.battens.count} × ${WallModel.BATTEN.length}`, 'mm'],
|
||||||
<div>
|
['Glaswol', f(totals.isolatieM3, 3), 'm³']
|
||||||
<div class="spec-section"><h1>Houtwerk & isolatie</h1>
|
]),
|
||||||
${row('Aantal balken', totals.beamCount + ' stuks')}
|
UI.specCard('Plaatwerk', 'layers', totals.plaatwerk.filter(p => p.m2 > 0).map(p => [p.naam, f(p.m2, 2), 'm²']))
|
||||||
${row('Volume hout', houseFmt(totals.woodM3, 3) + ' m3')}
|
];
|
||||||
${row('Latwerk 20x30 (kruislat)', totals.battens.count + ' st. à ' + WallModel.BATTEN.length + ' mm')}
|
if (totals.roofEdge) {
|
||||||
${row('Inkoop wandbalken', WallModel.purchaseText(totals.purchase))}
|
const e = totals.roofEdge, rows = [];
|
||||||
${row('Snijverlies', houseFmt(totals.purchaseWasteMM / 1000, 2) + ' m (' + houseFmt(totals.purchaseStockMM ? totals.purchaseWasteMM / totals.purchaseStockMM * 100 : 0) + '%)')}
|
if (e.fasciaLengthM > 0) rows.push([`Boeidelen · ${e.fasciaHeight} mm`, f(e.fasciaLengthM, 2), 'm'], ['Onderplaat boeideel', f(e.soffitM2, 2), 'm²']);
|
||||||
${row('Glaswol (isolatie)', houseFmt(totals.isolatieM3, 3) + ' m3')}
|
if (e.trimLengthM > 0) rows.push([`Daktrim · ${e.trimHeight} mm`, f(e.trimLengthM, 2), 'm']);
|
||||||
</div>
|
rows.push(['Boeideel-spots', e.spots, 'st.'], ['Gevellampen', totals.lamps.total, 'st.'], ['Binnenspots', totals.lamps.indoor, 'st.']);
|
||||||
<div class="spec-section"><h1>Plaatwerk</h1>
|
cards.push(UI.specCard('Dakrand & verlichting', 'lightbulb', rows));
|
||||||
${totals.plaatwerk.filter(p => p.m2 > 0).map(p => row(p.naam, houseFmt(p.m2, 2) + ' m2')).join('')}
|
}
|
||||||
</div>
|
document.getElementById('ho-specs').innerHTML = `<div class="ui-spec-grid">${cards.join('')}</div>`;
|
||||||
${totals.roofEdge ? `<div class="spec-section"><h1>Dakrand & buitenverlichting</h1>
|
|
||||||
${totals.roofEdge.fasciaLengthM > 0 ? row('Boeidelen', houseFmt(totals.roofEdge.fasciaLengthM, 2) + ' m · ' + totals.roofEdge.fasciaHeight + ' mm hoog') + row('Onderplaat boeideel', houseFmt(totals.roofEdge.soffitM2, 2) + ' m2') : ''}
|
|
||||||
${totals.roofEdge.trimLengthM > 0 ? row('Daktrim', houseFmt(totals.roofEdge.trimLengthM, 2) + ' m · ' + totals.roofEdge.trimHeight + ' mm hoog') : ''}
|
|
||||||
${row('Boeideel-spots', totals.roofEdge.spots + ' stuks')}
|
|
||||||
${row('Gevellampen', totals.lamps.total + ' stuks')}
|
|
||||||
</div>` : ''}
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Alles opnieuw doorrekenen ---
|
// --- Alles opnieuw doorrekenen ---
|
||||||
function refresh(opts) {
|
function refresh(opts) {
|
||||||
parts = HouseModel.computeParts(state, { wallMaterials, floorMaterials, roofMaterials, lightingMaterials, sandwichMaterials, beplatingVisible, isolatieVisible, lampenVisible });
|
parts = HouseModel.computeParts(state, { wallMaterials, floorMaterials, roofMaterials, lightingMaterials, sandwichMaterials, beplatingVisible, lampenVisible });
|
||||||
const totals = HouseModel.computeTotals(parts);
|
const totals = HouseModel.computeTotals(parts);
|
||||||
|
|
||||||
if (houseGroup) scene.remove(houseGroup);
|
if (houseGroup) scene.remove(houseGroup);
|
||||||
@@ -919,12 +934,16 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
if (parts.sandwichGroup) houseGroup.add(parts.sandwichGroup);
|
if (parts.sandwichGroup) houseGroup.add(parts.sandwichGroup);
|
||||||
if (parts.lightingGroup) houseGroup.add(parts.lightingGroup);
|
if (parts.lightingGroup) houseGroup.add(parts.lightingGroup);
|
||||||
scene.add(houseGroup);
|
scene.add(houseGroup);
|
||||||
applyRenderMode();
|
|
||||||
|
|
||||||
const bx0 = Math.min(...parts.rects.map(r => r.x0)), bx1 = Math.max(...parts.rects.map(r => r.x1));
|
const bx0 = Math.min(...parts.rects.map(r => r.x0)), bx1 = Math.max(...parts.rects.map(r => r.x1));
|
||||||
const bz0 = Math.min(...parts.rects.map(r => r.z0)), bz1 = Math.max(...parts.rects.map(r => r.z1));
|
const bz0 = Math.min(...parts.rects.map(r => r.z0)), bz1 = Math.max(...parts.rects.map(r => r.z1));
|
||||||
const cx = (bx0 + bx1) / 2 * toM, cz = (bz0 + bz1) / 2 * toM;
|
const cx = (bx0 + bx1) / 2 * toM, cz = (bz0 + bz1) / 2 * toM;
|
||||||
grid.position.set(cx, 0, cz);
|
grid.position.set(cx, 0, cz);
|
||||||
|
// Maaiveld: settings.groundLevel onder het loopvlak (bovenkant vloer).
|
||||||
|
const groundY = (Math.max(...parts.units.map(u => u.layout.floorTop)) - state.settings.groundLevel) * toM;
|
||||||
|
skyEnv.fitTo(houseGroup, groundY);
|
||||||
|
ground.position.set(cx, groundY - 0.002, cz);
|
||||||
|
applyRenderMode();
|
||||||
if (!cameraFitted) {
|
if (!cameraFitted) {
|
||||||
const size = Math.max(bx1 - bx0, bz1 - bz0) * toM;
|
const size = Math.max(bx1 - bx0, bz1 - bz0) * toM;
|
||||||
controls.target.set(cx, 1.2, cz);
|
controls.target.set(cx, 1.2, cz);
|
||||||
@@ -952,10 +971,8 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
}
|
}
|
||||||
window.addEventListener('resize', onResize);
|
window.addEventListener('resize', onResize);
|
||||||
|
|
||||||
function toggleFullscreen(cls, other) {
|
function toggleView(fn) {
|
||||||
const el = document.getElementById('house-overview');
|
fn(document.getElementById('house-overview'));
|
||||||
el.classList.remove(other);
|
|
||||||
el.classList.toggle(cls);
|
|
||||||
requestAnimationFrame(onResize);
|
requestAnimationFrame(onResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -974,8 +991,8 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
},
|
},
|
||||||
refresh,
|
refresh,
|
||||||
setTool,
|
setTool,
|
||||||
toggleFullscreen3D: () => toggleFullscreen('fullscreen-3d', 'fullscreen-2d'),
|
toggleFullscreen3D: () => toggleView(UI.cycle3D),
|
||||||
toggleFullscreen2D: () => toggleFullscreen('fullscreen-2d', 'fullscreen-3d')
|
toggleFullscreen2D: () => toggleView(UI.toggle2D)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+150
-18
@@ -27,6 +27,14 @@
|
|||||||
// (unit.lighting.walls[side]): 'auto' (standaard), 'openingen' (lampen op
|
// (unit.lighting.walls[side]): 'auto' (standaard), 'openingen' (lampen op
|
||||||
// openingOffset = 500 mm naast elke opening, bijv. slaapkamers) of 'geen'.
|
// openingOffset = 500 mm naast elke opening, bijv. slaapkamers) of 'geen'.
|
||||||
//
|
//
|
||||||
|
// Binnenspots (computeIndoor) worden PER UNIT berekend, net als de spots in
|
||||||
|
// het boeideel verdeeld tussen min/max h.o.h.: een raster in het plafond
|
||||||
|
// binnen de unit, minstens indoorOffset van de binnenzijde van elke wand (of
|
||||||
|
// van de unitrand waar een naad met een andere unit is), met in beide
|
||||||
|
// richtingen indoorMin..indoorMax h.o.h. Past er in een richting geen
|
||||||
|
// indoorMin tussen, dan één spot in het midden. Spots te dicht bij een
|
||||||
|
// binnenwand vervallen.
|
||||||
|
//
|
||||||
// Plain script, geladen na core/geometry.js en roof-model.js.
|
// Plain script, geladen na core/geometry.js en roof-model.js.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
@@ -39,7 +47,12 @@ window.LightingModel = (() => {
|
|||||||
lampMax: 3000,
|
lampMax: 3000,
|
||||||
minWallWidth: 2000, // kleinste vrije gevelbreedte voor gevellampen
|
minWallWidth: 2000, // kleinste vrije gevelbreedte voor gevellampen
|
||||||
endOffset: 300, // eerste/laatste lamp vanaf hoek/uiteinde
|
endOffset: 300, // eerste/laatste lamp vanaf hoek/uiteinde
|
||||||
openingOffset: 500 // override: lampen zoveel naast elke opening
|
openingOffset: 500, // override: lampen zoveel naast elke opening
|
||||||
|
// Binnenspots in het plafond, per unit (computeIndoor).
|
||||||
|
indoor: true,
|
||||||
|
indoorMin: 800, // mm h.o.h., in beide richtingen
|
||||||
|
indoorMax: 1500,
|
||||||
|
indoorOffset: 600 // min. afstand tot wand (binnenzijde) of unitrand (naad)
|
||||||
};
|
};
|
||||||
const MODES = { geen: 'Geen', spots: 'Boeideel-spots', gevel: 'Gevellampen' };
|
const MODES = { geen: 'Geen', spots: 'Boeideel-spots', gevel: 'Gevellampen' };
|
||||||
const DIRECTIONS = { both: 'Boven én onder', down: 'Alleen onder', up: 'Alleen boven' };
|
const DIRECTIONS = { both: 'Boven én onder', down: 'Alleen onder', up: 'Alleen boven' };
|
||||||
@@ -106,6 +119,7 @@ window.LightingModel = (() => {
|
|||||||
const segs = [];
|
const segs = [];
|
||||||
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => {
|
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => {
|
||||||
p.lamps = [];
|
p.lamps = [];
|
||||||
|
if (p.baseY) return; // opbouw boven een lager dak: geen gevellampen
|
||||||
const W = p.place.width;
|
const W = p.place.width;
|
||||||
// Lampen hangen op de afgewerkte gevel: wandvlak + kruislat + bekleding.
|
// Lampen hangen op de afgewerkte gevel: wandvlak + kruislat + bekleding.
|
||||||
const T = p.place.thickness + 2 * WallModel.claddingOffset(p.buitenMateriaal);
|
const T = p.place.thickness + 2 * WallModel.claddingOffset(p.buitenMateriaal);
|
||||||
@@ -113,9 +127,23 @@ window.LightingModel = (() => {
|
|||||||
const axis = Math.abs(normal.z) > 0.5 ? 'x' : 'z';
|
const axis = Math.abs(normal.z) > 0.5 ? 'x' : 'z';
|
||||||
const perp = axis === 'x' ? 'z' : 'x';
|
const perp = axis === 'x' ? 'z' : 'x';
|
||||||
const q0 = UnitModel.localToWorld(p.place, -W / 2, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2, T / 2);
|
const q0 = UnitModel.localToWorld(p.place, -W / 2, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2, T / 2);
|
||||||
|
// Gevel loopt tot de afgewerkte hoek, zodat endOffset altijd vanaf de
|
||||||
|
// zichtbare hoek telt: elk uiteinde + afwerking (latwerk + bekleding van
|
||||||
|
// de haakse wand die eroverheen loopt), en waar dit stuk de kopse kant
|
||||||
|
// van een doorlopende wand bedekt (skinExt) ook nog die wanddikte.
|
||||||
|
// Binnenhoeken en naden: zie de voetafdrukken in 3 en de overlap in 2.
|
||||||
|
const c = WallModel.claddingOffset(p.buitenMateriaal), e = p.skinExt || {};
|
||||||
|
const extL = (e.left > 0 ? e.left : 0) + c, extR = (e.right > 0 ? e.right : 0) + c;
|
||||||
|
const up = q1[axis] >= q0[axis];
|
||||||
|
// Voetafdruk van dit stuk incl. afwerking (om bedekte delen van andere
|
||||||
|
// gevels af te trekken, zie 3).
|
||||||
|
const corners = [[-W / 2, -p.place.thickness / 2], [W / 2, -p.place.thickness / 2], [-W / 2, T / 2], [W / 2, T / 2]]
|
||||||
|
.map(([lx, lz]) => UnitModel.localToWorld(p.place, lx, lz));
|
||||||
segs.push({
|
segs.push({
|
||||||
piece: p, normal, axis, perp, line: q0[perp],
|
piece: p, normal, axis, perp, line: q0[perp],
|
||||||
sAt0: q0[axis], sAtW: q1[axis], s0: Math.min(q0[axis], q1[axis]), s1: Math.max(q0[axis], q1[axis]),
|
sAt0: q0[axis], sAtW: q1[axis],
|
||||||
|
s0: up ? q0[axis] - extL : q1[axis] - extR, s1: up ? q1[axis] + extR : q0[axis] + extL,
|
||||||
|
foot: { x0: Math.min(...corners.map(q => q.x)), x1: Math.max(...corners.map(q => q.x)), z0: Math.min(...corners.map(q => q.z)), z1: Math.max(...corners.map(q => q.z)) },
|
||||||
override: overrides[u.id][w.side] || 'auto'
|
override: overrides[u.id][w.side] || 'auto'
|
||||||
});
|
});
|
||||||
})));
|
})));
|
||||||
@@ -140,6 +168,16 @@ window.LightingModel = (() => {
|
|||||||
runs.forEach(run => {
|
runs.forEach(run => {
|
||||||
const blocked = [];
|
const blocked = [];
|
||||||
const pos = []; // lampposities langs deze gevel (wereld-mm)
|
const pos = []; // lampposities langs deze gevel (wereld-mm)
|
||||||
|
// Delen van deze gevel die achter een andere wand liggen (bijv. het
|
||||||
|
// stuk van een wand dat de naad in loopt om een binnenhoek te sluiten)
|
||||||
|
// zijn geen zichtbare gevel: de gevel begint pas bij de afgewerkte
|
||||||
|
// buitenkant van die andere wand.
|
||||||
|
segs.filter(o => !run.segs.includes(o)).forEach(o => {
|
||||||
|
const f = o.foot;
|
||||||
|
const pMin = run.axis === 'x' ? f.z0 : f.x0, pMax = run.axis === 'x' ? f.z1 : f.x1;
|
||||||
|
const aMin = run.axis === 'x' ? f.x0 : f.z0, aMax = run.axis === 'x' ? f.x1 : f.z1;
|
||||||
|
if (run.line > pMin + 1 && run.line < pMax - 1 && aMax > run.s0 && aMin < run.s1) blocked.push([Math.max(run.s0, aMin), Math.min(run.s1, aMax)]);
|
||||||
|
});
|
||||||
run.segs.forEach(sg => {
|
run.segs.forEach(sg => {
|
||||||
const cfg = sg.piece.config, sW = cfg.studWidth;
|
const cfg = sg.piece.config, sW = cfg.studWidth;
|
||||||
const low = Math.min(cfg.wallHeightLeft, cfg.wallHeightRight) < H + 100;
|
const low = Math.min(cfg.wallHeightLeft, cfg.wallHeightRight) < H + 100;
|
||||||
@@ -175,23 +213,89 @@ window.LightingModel = (() => {
|
|||||||
const spots = [];
|
const spots = [];
|
||||||
if (s.mode === 'spots') {
|
if (s.mode === 'spots') {
|
||||||
const rs = RoofModel.normalizeSettings(roofSettings);
|
const rs = RoofModel.normalizeSettings(roofSettings);
|
||||||
edge.segments.filter(sg => sg.fascia).forEach(sg => spotsBetween(sg.spotA, sg.spotB, rs.spotMin, rs.spotMax, spots));
|
edge.segments.filter(sg => sg.fascia && !sg.overRoof).forEach(sg => { // niet boven een lager dak
|
||||||
|
const from = spots.length;
|
||||||
|
spotsBetween(sg.spotA, sg.spotB, rs.spotMin, rs.spotMax, spots);
|
||||||
|
for (let k = from; k < spots.length; k++) {
|
||||||
|
spots[k].normal = { x: sg.normal.x, z: sg.normal.z };
|
||||||
|
spots[k].y = sg.topAt(spots[k]) - edge.fasciaHeight; // onderkant boeideel (volgt de dakvorm)
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
edge.spots = spots.filter((p, i) => spots.findIndex(q => Math.hypot(q.x - p.x, q.z - p.z) < 5) === i);
|
// Dubbele spots (hoeken) samenvoegen; de buitenrichting wordt dan de
|
||||||
|
// som van beide gevels (diagonaal naar buiten).
|
||||||
|
edge.spots = [];
|
||||||
|
spots.forEach(p => {
|
||||||
|
const q = edge.spots.find(e => Math.hypot(e.x - p.x, e.z - p.z) < 5);
|
||||||
|
if (!q) edge.spots.push({ x: p.x, z: p.z, y: p.y, normal: p.normal });
|
||||||
|
else if (p.normal && q.normal) q.normal = { x: q.normal.x + p.normal.x, z: q.normal.z + p.normal.z };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return { lamps };
|
return { lamps };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Posities op [a, b]: beide uiteinden + verdeeld (calculateSegmentPositions);
|
||||||
|
// korter dan min (of negatief) = één positie in het midden.
|
||||||
|
function gridPositions(a, b, min, max) {
|
||||||
|
const L = b - a;
|
||||||
|
if (L < min) return [(a + b) / 2];
|
||||||
|
return [a, ...calculateSegmentPositions(L, min, max).map(p => a + p), b];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Afstand van punt p tot lijnstuk a-b (mm).
|
||||||
|
function distToSegment(p, a, b) {
|
||||||
|
const dx = b.x - a.x, dz = b.z - a.z, len2 = dx * dx + dz * dz || 1;
|
||||||
|
const t = Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.z - a.z) * dz) / len2));
|
||||||
|
return Math.hypot(p.x - (a.x + t * dx), p.z - (a.z + t * dz));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binnenspots per unit. units = HouseModel-parts.units (rect, walls, roof.ceilingAt),
|
||||||
|
// interior = parts.interior (binnenwanden met place). Geeft
|
||||||
|
// [{ unitId, x, z, y, floorY }] (mm, y = onderkant plafond, floorY = loopvlak).
|
||||||
|
function computeIndoor(settings, units, interior) {
|
||||||
|
const s = normalizeSettings(settings);
|
||||||
|
if (!s.indoor) return [];
|
||||||
|
const out = [];
|
||||||
|
units.forEach(u => {
|
||||||
|
const T = {};
|
||||||
|
u.walls.forEach(w => { T[w.side] = w.removed ? 0 : w.thickness; });
|
||||||
|
const r = u.rect, off = s.indoorOffset;
|
||||||
|
// Naadwanddelen van deze unit staan binnen de rect tegen de naad: het
|
||||||
|
// raster begint dan vanaf hun binnenzijde (net als bij een buitenwand).
|
||||||
|
const own = interior.filter(iw => !iw.seamUnit || iw.seamUnit === u.id);
|
||||||
|
own.filter(iw => iw.seamUnit).forEach(iw => {
|
||||||
|
const p = iw.place, horiz = Math.abs(Math.sin(p.rotY)) < 0.5;
|
||||||
|
const [side, d] = horiz
|
||||||
|
? (Math.abs(p.cz - r.z0) < Math.abs(p.cz - r.z1) ? ['voor', p.cz - r.z0] : ['achter', r.z1 - p.cz])
|
||||||
|
: (Math.abs(p.cx - r.x0) < Math.abs(p.cx - r.x1) ? ['links', p.cx - r.x0] : ['rechts', r.x1 - p.cx]);
|
||||||
|
T[side] = Math.max(T[side] || 0, d + p.thickness / 2);
|
||||||
|
});
|
||||||
|
const xs = gridPositions(r.x0 + (T.links || 0) + off, r.x1 - (T.rechts || 0) - off, s.indoorMin, s.indoorMax);
|
||||||
|
const zs = gridPositions(r.z0 + (T.voor || 0) + off, r.z1 - (T.achter || 0) - off, s.indoorMin, s.indoorMax);
|
||||||
|
xs.forEach(x => zs.forEach(z => {
|
||||||
|
const tooClose = own.some(iw => {
|
||||||
|
const half = iw.place.width / 2, c = Math.cos(iw.place.rotY), sn = Math.sin(iw.place.rotY);
|
||||||
|
const a = { x: iw.place.cx - half * c, z: iw.place.cz + half * sn }, b = { x: iw.place.cx + half * c, z: iw.place.cz - half * sn };
|
||||||
|
return distToSegment({ x, z }, a, b) < iw.place.thickness / 2 + off - 1;
|
||||||
|
});
|
||||||
|
if (!tooClose) out.push({ unitId: u.id, x, z, y: u.roof.ceilingAt(x, z), floorY: u.layout.floorTop });
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
function createMaterials() {
|
function createMaterials() {
|
||||||
return {
|
return {
|
||||||
housingMaterial: new THREE.MeshStandardMaterial({ color: 0x2b2f33, roughness: 0.5, metalness: 0.3 }),
|
housingMaterial: new THREE.MeshStandardMaterial({ color: 0x2b2f33, roughness: 0.5, metalness: 0.3 }),
|
||||||
glowMaterial: new THREE.MeshStandardMaterial({ color: 0xfff4d0, emissive: 0xffe2a0, emissiveIntensity: 1 }),
|
glowMaterial: new THREE.MeshStandardMaterial({ color: 0xfff4d0, emissive: 0xffe2a0, emissiveIntensity: 1 }),
|
||||||
|
indoorMaterial: new THREE.MeshStandardMaterial({ color: 0xfff4d0, emissive: 0xffe2a0, emissiveIntensity: 0.8 }),
|
||||||
beamMaterial: new THREE.MeshBasicMaterial({ color: 0xffe9a8, transparent: true, opacity: 0.16, depthWrite: false, side: THREE.DoubleSide })
|
beamMaterial: new THREE.MeshBasicMaterial({ color: 0xffe9a8, transparent: true, opacity: 0.16, depthWrite: false, side: THREE.DoubleSide })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3D: behuizing op het gevelvlak + lichtbundel(s) omhoog/omlaag.
|
// 3D: behuizing op het gevelvlak + lichtbundel(s) omhoog/omlaag, en de
|
||||||
function build3D(lamps, materials) {
|
// binnenspots (indoor, zie computeIndoor) als schijfje in het plafond.
|
||||||
|
function build3D(lamps, materials, indoor) {
|
||||||
const toM = 0.001;
|
const toM = 0.001;
|
||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
const w = 0.08, h = 0.16, d = 0.07, beamH = 0.7, beamR = 0.22;
|
const w = 0.08, h = 0.16, d = 0.07, beamH = 0.7, beamR = 0.22;
|
||||||
@@ -214,31 +318,59 @@ window.LightingModel = (() => {
|
|||||||
if (l.direction !== 'up') addGlow(-1);
|
if (l.direction !== 'up') addGlow(-1);
|
||||||
if (l.direction !== 'down') addGlow(1);
|
if (l.direction !== 'down') addGlow(1);
|
||||||
});
|
});
|
||||||
|
(indoor || []).forEach(p => {
|
||||||
|
const spot = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.01, 16), materials.indoorMaterial);
|
||||||
|
spot.position.set(p.x * toM, p.y * toM - 0.006, p.z * toM);
|
||||||
|
group.add(spot);
|
||||||
|
});
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Volle render: elke gevellamp (per lichtrichting) en elke boeideel-spot
|
// Render: elke gevellamp (per lichtrichting), boeideel-spot en binnenspot
|
||||||
// wordt een echte SpotLight. Gevellampen staan net buiten het gevelvlak en
|
// wordt een echte SpotLight. Echte schaduw per lamp is te zwaar (één
|
||||||
// schijnen langs de gevel omhoog/omlaag; spots schijnen recht naar beneden.
|
// schaduwkaart per lamp; de browser kan er maar ~16), dus licht door muren
|
||||||
// spots = [{ x, z }] (mm), spotY = hoogte onderkant boeideel (mm).
|
// wordt geometrisch voorkomen:
|
||||||
function buildLights3D(lamps, spots, spotY) {
|
// - buitenlampen en boeideel-spots zijn half hun bundelhoek naar buiten
|
||||||
|
// gekanteld: de binnenrand van de bundel loopt verticaal langs de gevel;
|
||||||
|
// - binnenspots hebben een harde reikwijdte net voorbij de vloer
|
||||||
|
// (INDOOR_REACH), zodat ze niet tot de grond buiten reiken.
|
||||||
|
// spots = [{ x, z, y }] (mm, boeideel; y = onderkant boeideel, anders spotY),
|
||||||
|
// indoor = [{ x, z, y, floorY }] (mm). o.outdoorFactor schaalt de buiten-
|
||||||
|
// lampen (overdag lager: dan vallen ze nauwelijks op).
|
||||||
|
const OUTDOOR = { lamp: 2.0, spot: 1.6, angle: 0.5 };
|
||||||
|
const INDOOR = { intensity: 4.0, angle: 1.0, reach: 1.12, decay: 0.6 };
|
||||||
|
const DAY_OUTDOOR_FACTOR = 0.35;
|
||||||
|
function buildLights3D(lamps, spots, spotY, indoor, o) {
|
||||||
const toM = 0.001;
|
const toM = 0.001;
|
||||||
|
const fo = (o && o.outdoorFactor !== undefined) ? o.outdoorFactor : 1;
|
||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
const add = (x, y, z, dirY, intensity, angle, distance) => {
|
// Spot op (x, y, z) die naar (x + tx, y + ty, z + tz) schijnt.
|
||||||
const light = new THREE.SpotLight(0xffd9a0, intensity, distance, angle, 0.6, 1.5);
|
const add = (x, y, z, tx, ty, tz, intensity, angle, distance, decay) => {
|
||||||
|
const light = new THREE.SpotLight(0xffd9a0, intensity, distance, angle, 0.6, decay);
|
||||||
light.position.set(x, y, z);
|
light.position.set(x, y, z);
|
||||||
light.target.position.set(x, y + dirY * 2, z);
|
light.target.position.set(x + tx, y + ty, z + tz);
|
||||||
group.add(light);
|
group.add(light);
|
||||||
group.add(light.target);
|
group.add(light.target);
|
||||||
};
|
};
|
||||||
|
const tilt = Math.tan(OUTDOOR.angle); // bundelas half de bundelhoek naar buiten
|
||||||
lamps.forEach(l => {
|
lamps.forEach(l => {
|
||||||
const x = l.x * toM + l.normal.x * 0.12, z = l.z * toM + l.normal.z * 0.12, y = l.y * toM;
|
const x = l.x * toM + l.normal.x * 0.12, z = l.z * toM + l.normal.z * 0.12, y = l.y * toM;
|
||||||
if (l.direction !== 'up') add(x, y - 0.09, z, -1, 3, 0.55, 4.5);
|
const nx = l.normal.x * tilt, nz = l.normal.z * tilt;
|
||||||
if (l.direction !== 'down') add(x, y + 0.09, z, 1, 3, 0.55, 4.5);
|
if (l.direction !== 'up') add(x, y - 0.09, z, nx, -1, nz, OUTDOOR.lamp * fo, OUTDOOR.angle, 4.5, 1.5);
|
||||||
|
if (l.direction !== 'down') add(x, y + 0.09, z, nx, 1, nz, OUTDOOR.lamp * fo, OUTDOOR.angle, 4.5, 1.5);
|
||||||
|
});
|
||||||
|
// Boeideel-spots: naar buiten gekanteld (normaal van de dakrand; op een
|
||||||
|
// hoek diagonaal).
|
||||||
|
(spots || []).forEach(p => {
|
||||||
|
const n = p.normal || { x: 0, z: 0 }, len = Math.hypot(n.x, n.z) || 1;
|
||||||
|
add(p.x * toM, (p.y !== undefined ? p.y : spotY) * toM - 0.02, p.z * toM, n.x / len * tilt, -1, n.z / len * tilt, OUTDOOR.spot * fo, OUTDOOR.angle, 5.5, 1.5);
|
||||||
|
});
|
||||||
|
(indoor || []).forEach(p => {
|
||||||
|
const drop = (p.y - p.floorY) * toM; // plafond -> loopvlak
|
||||||
|
add(p.x * toM, p.y * toM - 0.02, p.z * toM, 0, -1, 0, INDOOR.intensity, INDOOR.angle, drop * INDOOR.reach, INDOOR.decay);
|
||||||
});
|
});
|
||||||
(spots || []).forEach(p => add(p.x * toM, spotY * toM - 0.02, p.z * toM, -1, 2.5, 0.6, 5.5));
|
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { DEFAULT_SETTINGS, MODES, DIRECTIONS, WALL_OVERRIDES, createDefaultUnitLighting, normalizeSettings, compute, createMaterials, build3D, buildLights3D };
|
return { DEFAULT_SETTINGS, MODES, DIRECTIONS, WALL_OVERRIDES, createDefaultUnitLighting, DAY_OUTDOOR_FACTOR, normalizeSettings, compute, computeIndoor, createMaterials, build3D, buildLights3D };
|
||||||
})();
|
})();
|
||||||
|
|||||||
+297
-44
@@ -5,14 +5,20 @@
|
|||||||
* Zie LICENTIE.md.
|
* Zie LICENTIE.md.
|
||||||
*/
|
*/
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// roof/roof-model.js — RoofModel: ENIGE bron van de dak-logica (voorlopig
|
// roof/roof-model.js — RoofModel: ENIGE bron van de dak-logica. Geen DOM.
|
||||||
// alleen plat dak). Geen DOM.
|
|
||||||
//
|
//
|
||||||
// Een plat dak is opgebouwd als een vloer (FloorModel: balken, isolatie,
|
// Een dakvlak is opgebouwd als een vloer (FloorModel: balken, isolatie,
|
||||||
// doorvoeren, OSB-plaat), maar 180° gespiegeld: de OSB-plaat zit aan de
|
// doorvoeren, OSB-plaat), maar 180° gespiegeld: de OSB-plaat zit aan de
|
||||||
// onderzijde (plafond), de balken erboven, met dakleer als buitenlaag
|
// onderzijde (plafond), de balken erboven, met dakleer als buitenlaag
|
||||||
// bovenop. Het "dek" van een dak is dus gewoon een vloer-state.
|
// bovenop. Het "dek" van een dak is dus gewoon een vloer-state.
|
||||||
//
|
//
|
||||||
|
// Dakvorm volgt de wanden (computeShape): het paar tegenoverliggende wanden
|
||||||
|
// met een schuine bovenrand of middenpunt (nok) zijn de kopgevels, het dak
|
||||||
|
// volgt hun daklijn (plat, lessenaar of zadel). UnitModel.syncWallHeights
|
||||||
|
// houdt de wanden daarvoor consistent. Het dek (in de plattegrond) wordt per
|
||||||
|
// dakvlak omgerekend naar een schuin vlak met de balken als sporen (van
|
||||||
|
// laag naar hoog); doorvoeren gaan mee naar het vlak waarin ze vallen.
|
||||||
|
//
|
||||||
// Dakrand (over de hele woning, doorlopend over naden tussen units): de
|
// Dakrand (over de hele woning, doorlopend over naden tussen units): de
|
||||||
// omtrek van alle daken samen, opgeknipt per ribbe (en op elke unit-hoek).
|
// omtrek van alle daken samen, opgeknipt per ribbe (en op elke unit-hoek).
|
||||||
// Per ribbe bepaalt de unit waar die ribbe bij hoort of het een boeideel of
|
// Per ribbe bepaalt de unit waar die ribbe bij hoort of het een boeideel of
|
||||||
@@ -22,7 +28,9 @@
|
|||||||
// dakrand = standaard de buitenkant gevel), standaard 300 mm hoog.
|
// dakrand = standaard de buitenkant gevel), standaard 300 mm hoog.
|
||||||
// Spots in de onderplaat staan op 40% van de overstek (spotLine); welke
|
// Spots in de onderplaat staan op 40% van de overstek (spotLine); welke
|
||||||
// spots er komen bepaalt LightingModel.
|
// spots er komen bepaalt LightingModel.
|
||||||
// - zonder boeidelen: een daktrim van 80 mm hoog.
|
// - zonder boeidelen: een daktrim van 80 mm hoog (onder het dakleer).
|
||||||
|
// De bovenkant van boeideel en daktrim ligt EDGE_ABOVE_MM boven het dakleer
|
||||||
|
// (opstand rondom), dus het dakleer ligt altijd lager dan de rand.
|
||||||
//
|
//
|
||||||
// Plain script, geladen na floor-model.js en core/geometry.js.
|
// Plain script, geladen na floor-model.js en core/geometry.js.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -31,7 +39,7 @@ window.RoofModel = (() => {
|
|||||||
const DAKLEER_MM = 2; // visuele dikte van het dakleer
|
const DAKLEER_MM = 2; // visuele dikte van het dakleer
|
||||||
const DENSITY_DAKLEER_KGM2 = 1.4; // EPDM ~1,4 kg/m2
|
const DENSITY_DAKLEER_KGM2 = 1.4; // EPDM ~1,4 kg/m2
|
||||||
const TRIM_THICK_MM = 25; // daktrim-profiel, uitstekend buiten de dakrand
|
const TRIM_THICK_MM = 25; // daktrim-profiel, uitstekend buiten de dakrand
|
||||||
const TRIM_ABOVE_MM = 30; // daktrim steekt zoveel boven het dakleer uit
|
const EDGE_ABOVE_MM = 80; // bovenkant boeideel/daktrim ligt zoveel boven het dakleer
|
||||||
const SPOT_RATIO = 0.4; // spots op 40% van de overstek, gemeten vanaf de gevel
|
const SPOT_RATIO = 0.4; // spots op 40% van de overstek, gemeten vanaf de gevel
|
||||||
const OVERHANG_MIN = 200, OVERHANG_MAX = 400;
|
const OVERHANG_MIN = 200, OVERHANG_MAX = 400;
|
||||||
|
|
||||||
@@ -77,7 +85,7 @@ window.RoofModel = (() => {
|
|||||||
const toM = 0.001;
|
const toM = 0.001;
|
||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
const deck = FloorModel.build3D(calcData, resolvedOpenings, { wheels: [] }, {
|
const deck = FloorModel.build3D(calcData, resolvedOpenings, { wheels: [] }, {
|
||||||
materials: m, osbVisible: opts.osbVisible !== false, isolatieVisible: opts.isolatieVisible !== false
|
materials: m, osbVisible: opts.osbVisible !== false
|
||||||
});
|
});
|
||||||
const deckH = (calcData.joistH + calcData.underlayThick) * toM;
|
const deckH = (calcData.joistH + calcData.underlayThick) * toM;
|
||||||
deck.scale.y = -1;
|
deck.scale.y = -1;
|
||||||
@@ -114,6 +122,147 @@ window.RoofModel = (() => {
|
|||||||
return Object.assign({}, deck, { dakleerM2, dakleerKg, totalKg: deck.totalKg + dakleerKg });
|
return Object.assign({}, deck, { dakleerM2, dakleerKg, totalKg: deck.totalKg + dakleerKg });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Dakvorm per unit (volgt de wanden)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
// Daklijn van één wand als profiel langs zijn as (unit-lokaal mm):
|
||||||
|
// [{ p, h }] gesorteerd op p, h vanaf het loopvlak. Zelfde definitie als
|
||||||
|
// UnitModel.wallPieces: links/rechts op de uiteinden van de wand (van
|
||||||
|
// buitenaf gezien), het middenpunt (nok) midden op de wand.
|
||||||
|
function wallProfile(unitState, layout, side) {
|
||||||
|
const w = unitState.walls[side].wall;
|
||||||
|
const wl = layout.walls.find(x => x.side === side);
|
||||||
|
const s = UnitModel.WALL_SIDES.find(x => x.key === side);
|
||||||
|
const c = s.axis === 'x' ? wl.cx : wl.cz;
|
||||||
|
const pts = [{ p: c - s.dir * wl.width / 2, h: w.heightLeft }, { p: c + s.dir * wl.width / 2, h: w.heightRight }];
|
||||||
|
if (w.heightMid > 0) pts.push({ p: c, h: Math.max(w.heightMid, w.heightLeft, w.heightRight) });
|
||||||
|
return pts.sort((a, b) => a.p - b.p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dakvorm van een unit. layout = UnitModel.computeLayout-resultaat.
|
||||||
|
// Geeft { type: 'plat'|'lessenaar'|'zadel', axis: 'x'|'z', profile }:
|
||||||
|
// de hoogte van de dakonderkant (vanaf het loopvlak) hangt alleen af van
|
||||||
|
// de positie langs axis (profileHeight). Kopgevels: de eerste wand met
|
||||||
|
// een schuine daklijn (noord/zuid -> dak helt langs X, west/oost -> langs Z).
|
||||||
|
// Zonder schuine wanden: plat op de hoogste wand.
|
||||||
|
function computeShape(unitState, layout) {
|
||||||
|
for (const [side, axis] of [['achter', 'x'], ['voor', 'x'], ['links', 'z'], ['rechts', 'z']]) {
|
||||||
|
const profile = wallProfile(unitState, layout, side);
|
||||||
|
if (profile.some(q => Math.abs(q.h - profile[0].h) > 0.5)) {
|
||||||
|
return { type: profile.length > 2 ? 'zadel' : 'lessenaar', axis, profile };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const h = Math.max(...Object.values(unitState.walls).map(ws => UnitModel.wallTopOf(ws)));
|
||||||
|
return { type: 'plat', axis: 'z', profile: [{ p: 0, h }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hoogte van het profiel op positie p; buiten de wand loopt de laatste
|
||||||
|
// helling door (overstek, boeideel).
|
||||||
|
function profileHeight(profile, p) {
|
||||||
|
if (profile.length === 1) return profile[0].h;
|
||||||
|
let i = 0;
|
||||||
|
while (i < profile.length - 2 && p > profile[i + 1].p) i++;
|
||||||
|
const a = profile[i], b = profile[i + 1];
|
||||||
|
return a.h + (p - a.p) / (b.p - a.p) * (b.h - a.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tel alle getallen van een lijst specificaties op.
|
||||||
|
function sumSpecs(list) {
|
||||||
|
const out = Object.assign({}, list[0]);
|
||||||
|
list.slice(1).forEach(sp => Object.keys(sp).forEach(k => {
|
||||||
|
if (typeof sp[k] === 'number' && typeof out[k] === 'number') out[k] += sp[k];
|
||||||
|
}));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Het dak van één unit: de dakrechthoek (unit-lokaal mm { x0, x1, z0, z1 },
|
||||||
|
// = maat van roofState.deck) geknipt op de nok in dakvlakken. Per vlak een
|
||||||
|
// eigen dek (vloer-state) met breedte = maat dwars op de helling en diepte
|
||||||
|
// = lengte langs de helling, zodat de balken als sporen van laag naar hoog
|
||||||
|
// lopen. rims: zie FloorModel.computeFrame (alleen bij helling langs Z).
|
||||||
|
// Geeft { shape, planes, specs, undersideAt(x, z), topAt(x, z), ridges, maxTop }
|
||||||
|
// (unit-lokaal, hoogtes vanaf het loopvlak). Vlak-posities (cx, cz) zijn
|
||||||
|
// t.o.v. het midden van de dakrechthoek.
|
||||||
|
function computeUnitRoof(roofState, shape, rect, rims) {
|
||||||
|
const alongX = shape.axis === 'x';
|
||||||
|
const W = rect.x1 - rect.x0, D = rect.z1 - rect.z0;
|
||||||
|
const xc = (rect.x0 + rect.x1) / 2, zc = (rect.z0 + rect.z1) / 2;
|
||||||
|
const a0 = alongX ? rect.x0 : rect.z0, a1 = alongX ? rect.x1 : rect.z1;
|
||||||
|
const kinks = shape.profile.slice(1, -1).map(q => q.p).filter(p => p > a0 + 1 && p < a1 - 1);
|
||||||
|
const cuts = [a0, ...kinks, a1];
|
||||||
|
const flat = FloorModel.computeFromState(roofState.deck, rims);
|
||||||
|
const H = (p) => profileHeight(shape.profile, p);
|
||||||
|
|
||||||
|
// Doorvoeren: positie in de plattegrond (zoals in de dak-editor) -> vlak.
|
||||||
|
const holes = flat.resolvedOpenings.filter(o => !o.removed).map(o => {
|
||||||
|
const px = xc + o.x, pz = rect.z0 + o.y;
|
||||||
|
return { o, px, pz, q: alongX ? px : pz };
|
||||||
|
});
|
||||||
|
|
||||||
|
const planes = cuts.slice(0, -1).map((p0, i) => {
|
||||||
|
const p1 = cuts[i + 1], last = i === cuts.length - 2;
|
||||||
|
const h0 = H(p0), h1 = H(p1), run = p1 - p0;
|
||||||
|
const slopeLen = Math.hypot(run, h1 - h0), cos = run / slopeLen;
|
||||||
|
const plane = { p0, p1, h0, h1, theta: Math.atan2(h1 - h0, run), cos, yaw: alongX ? Math.PI / 2 : 0 };
|
||||||
|
let data;
|
||||||
|
if (cuts.length === 2 && !alongX && Math.abs(h1 - h0) < 0.5) {
|
||||||
|
data = flat; // plat dak: het dek zoals het is
|
||||||
|
} else {
|
||||||
|
const deck = clone(roofState.deck);
|
||||||
|
deck.floor.width = Math.round(alongX ? D : W);
|
||||||
|
deck.floor.depth = Math.round(slopeLen);
|
||||||
|
deck.openings = holes.filter(hl => hl.q >= p0 && (hl.q < p1 || last)).map(hl => ({
|
||||||
|
id: hl.o.id, diameter: hl.o.requestedDiameter, anchorX: 'left',
|
||||||
|
// Bij helling langs X is het dek een kwartslag gedraaid: lokaal x -> -Z.
|
||||||
|
offsetX: Math.round(alongX ? rect.z1 - hl.pz : hl.px - rect.x0),
|
||||||
|
offsetY: Math.round((hl.q - p0) / cos)
|
||||||
|
}));
|
||||||
|
const pr = alongX ? {} : { front: !!rims.front && Math.abs(p0 - rect.z0) < 1, back: !!rims.back && Math.abs(p1 - rect.z1) < 1 };
|
||||||
|
data = FloorModel.computeFromState(deck, pr);
|
||||||
|
}
|
||||||
|
const mid = (p0 + p1) / 2;
|
||||||
|
return Object.assign(plane, {
|
||||||
|
data, specs: computeSpecs(data.calcData, data.resolvedOpenings),
|
||||||
|
cx: alongX ? mid - xc : 0, cz: alongX ? 0 : mid - zc, y: (h0 + h1) / 2,
|
||||||
|
// Verticale dikte (dek + dakleer) van dit vlak.
|
||||||
|
vThick: thickness(data.calcData) / cos
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const planeAt = (q) => planes.find((pl, i) => q < pl.p1 || i === planes.length - 1);
|
||||||
|
const undersideAt = (x, z) => H(alongX ? x : z);
|
||||||
|
const topAt = (x, z) => {
|
||||||
|
const q = alongX ? x : z;
|
||||||
|
const onKink = planes.filter(pl => Math.abs(q - pl.p0) < 0.5 || Math.abs(q - pl.p1) < 0.5);
|
||||||
|
const t = onKink.length > 1 ? Math.max(...onKink.map(pl => pl.vThick)) : planeAt(q).vThick;
|
||||||
|
return H(q) + t;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
shape, planes, specs: sumSpecs(planes.map(pl => pl.specs)),
|
||||||
|
undersideAt, topAt,
|
||||||
|
ridges: kinks.map(value => ({ axis: shape.axis, value })),
|
||||||
|
maxTop: Math.max(...planes.map(pl => Math.max(pl.h0, pl.h1) + pl.vThick))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3D van het dak van één unit (meters, gecentreerd op de dakrechthoek,
|
||||||
|
// Y = 0 op het loopvlak): per dakvlak build3D, gekanteld om de onderkant
|
||||||
|
// van het plafond zodat die precies op de daklijn van de wanden ligt.
|
||||||
|
function buildUnit3D(unitRoof, opts) {
|
||||||
|
const toM = 0.001;
|
||||||
|
const group = new THREE.Group();
|
||||||
|
unitRoof.planes.forEach(pl => {
|
||||||
|
const g = new THREE.Group();
|
||||||
|
g.add(build3D(pl.data.calcData, pl.data.resolvedOpenings, opts));
|
||||||
|
g.position.set(pl.cx * toM, pl.y * toM, pl.cz * toM);
|
||||||
|
// Eerst kantelen om lokaal X (lokaal +Z = oplopende as), dan draaien.
|
||||||
|
g.rotation.set(-pl.theta, pl.yaw, 0, 'YXZ');
|
||||||
|
group.add(g);
|
||||||
|
});
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
// Dakrand over de hele woning
|
// Dakrand over de hele woning
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
@@ -148,37 +297,107 @@ window.RoofModel = (() => {
|
|||||||
return Math.abs(pts.reduce((s, p, k) => { const q = pts[(k + 1) % pts.length]; return s + p.x * q.z - q.x * p.z; }, 0) / 2);
|
return Math.abs(pts.reduce((s, p, k) => { const q = pts[(k + 1) % pts.length]; return s + p.x * q.z - q.x * p.z; }, 0) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// roofs: [{ unitId, rect: { x0, x1, z0, z1 }, fascia: bool }] (mm, woning-
|
// roofs: [{ unitId, rect: { x0, x1, z0, z1 }, fascia: bool, topAt(x, z),
|
||||||
// assen); topY = bovenkant dakleer (mm). Geeft per ribbe (segments) de
|
// ridges: [{ axis, value }] }] (mm, woning-assen); topAt = bovenkant
|
||||||
// geometrie terug; edge.spots wordt door LightingModel gevuld.
|
// dakleer (absoluut, loopt buiten het dak door met de helling). Geeft per
|
||||||
function computeEdge(settings, roofs, topY) {
|
// ribbe (segments) de geometrie terug; edge.spots wordt door LightingModel
|
||||||
|
// gevuld (met y = onderkant boeideel ter plaatse).
|
||||||
|
function computeEdge(settings, roofs) {
|
||||||
const s = normalizeSettings(settings);
|
const s = normalizeSettings(settings);
|
||||||
const rects = roofs.map(r => r.rect);
|
const rects = roofs.map(r => r.rect);
|
||||||
const loops = rectUnionLoops(rects);
|
if (!roofs.length) return null;
|
||||||
if (!loops.length) return null;
|
|
||||||
const base = splitAtCorners(loops[0].pts, rects);
|
|
||||||
const offBoei = offsetRectilinearLoop(base, s.overhang);
|
|
||||||
const offTrim = offsetRectilinearLoop(base, TRIM_THICK_MM);
|
|
||||||
const offSpot = offsetRectilinearLoop(base, s.overhang * SPOT_RATIO);
|
|
||||||
const n = base.length;
|
|
||||||
|
|
||||||
const segments = base.map((a, i) => {
|
// Hoogtegroepen: daken die tegen elkaar liggen horen bij één doorlopende
|
||||||
const j = (i + 1) % n, b = base[j];
|
// rand, tenzij het hoogteverschil langs de hele naad minstens de
|
||||||
const mid = { x: (a.x + b.x) / 2, z: (a.z + b.z) / 2 };
|
// boeideelhoogte is. Dan krijgt het hoge dak een eigen rand rondom (ook
|
||||||
const owner = roofs.find(r => onRectEdge(r.rect, mid)) || roofs[0];
|
// boven het lage dak), en loopt de rand van het lage dak tot de gevel
|
||||||
const fascia = owner.fascia !== false;
|
// van het hoge.
|
||||||
const off = fascia ? offBoei : offTrim;
|
const group = roofs.map((r, i) => i);
|
||||||
return {
|
const find = (i) => group[i] === i ? i : (group[i] = find(group[i]));
|
||||||
a, b, owner: owner.unitId, fascia, normal: rectilinearNormal(a, b),
|
const sharedEdge = (A, B) => {
|
||||||
outerA: off[i], outerB: off[j], spotA: offSpot[i], spotB: offSpot[j]
|
const e = 0.5;
|
||||||
};
|
for (const [ax, bx, lo0, lo1, hi0, hi1, axis] of [
|
||||||
|
[A.x1, B.x0, A.z0, A.z1, B.z0, B.z1, 'x'], [B.x1, A.x0, A.z0, A.z1, B.z0, B.z1, 'x'],
|
||||||
|
[A.z1, B.z0, A.x0, A.x1, B.x0, B.x1, 'z'], [B.z1, A.z0, A.x0, A.x1, B.x0, B.x1, 'z']]) {
|
||||||
|
const s0 = Math.max(lo0, hi0), s1 = Math.min(lo1, hi1);
|
||||||
|
if (Math.abs(ax - bx) < e && s1 - s0 > e) {
|
||||||
|
return [s0, (s0 + s1) / 2, s1].map(t => axis === 'x' ? { x: ax, z: t } : { x: t, z: ax });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
roofs.forEach((A, i) => roofs.forEach((B, j) => {
|
||||||
|
if (j <= i) return;
|
||||||
|
const pts = sharedEdge(A.rect, B.rect);
|
||||||
|
if (!pts) return;
|
||||||
|
const diff = Math.min(...pts.map(p => Math.abs(A.topAt(p.x, p.z) - B.topAt(p.x, p.z))));
|
||||||
|
if (diff < s.fasciaHeight) group[find(i)] = find(j);
|
||||||
|
}));
|
||||||
|
|
||||||
|
const segments = [];
|
||||||
|
[...new Set(roofs.map((r, i) => find(i)))].forEach(g => {
|
||||||
|
const members = roofs.filter((r, i) => find(i) === g);
|
||||||
|
const others = roofs.filter((r, i) => find(i) !== g);
|
||||||
|
const loops = rectUnionLoops(members.map(r => r.rect));
|
||||||
|
if (!loops.length) return;
|
||||||
|
const base = splitAtCorners(loops[0].pts, rects);
|
||||||
|
const offBoei = offsetRectilinearLoop(base, s.overhang);
|
||||||
|
const offTrim = offsetRectilinearLoop(base, TRIM_THICK_MM);
|
||||||
|
const offSpot = offsetRectilinearLoop(base, s.overhang * SPOT_RATIO);
|
||||||
|
const n = base.length;
|
||||||
|
const segs = base.map((a, i) => {
|
||||||
|
const j = (i + 1) % n, b = base[j];
|
||||||
|
const mid = { x: (a.x + b.x) / 2, z: (a.z + b.z) / 2 };
|
||||||
|
const owner = members.find(r => onRectEdge(r.rect, mid)) || members[0];
|
||||||
|
const fascia = owner.fascia !== false;
|
||||||
|
const off = fascia ? offBoei : offTrim;
|
||||||
|
// Ribbe tegen een dak van een andere groep: tegen een hoger dak
|
||||||
|
// vervalt hij (daar staat de gevel van het hoge), boven een lager
|
||||||
|
// dak blijft hij, maar zonder spots.
|
||||||
|
const nb = others.find(r => onRectEdge(r.rect, mid));
|
||||||
|
const dropped = !!nb && nb.topAt(mid.x, mid.z) > owner.topAt(mid.x, mid.z);
|
||||||
|
return {
|
||||||
|
a, b, owner: owner.unitId, fascia, normal: rectilinearNormal(a, b),
|
||||||
|
outerA: off[i], outerB: off[j], spotA: offSpot[i], spotB: offSpot[j],
|
||||||
|
// Bovenkant van de rand (dakleer + opstand).
|
||||||
|
topAt: (p) => owner.topAt(p.x, p.z) + EDGE_ABOVE_MM, ridges: owner.ridges || [],
|
||||||
|
overRoof: !!nb && !dropped, dropped
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// Naast een vervallen ribbe (tegen een hoger dak):
|
||||||
|
// - ligt het verlengde van de rand in het hoge deel, dan loopt hij
|
||||||
|
// recht door tot die gevel (spot op spotafstand van de gevel);
|
||||||
|
// - ligt het verlengde ernaast (de rand loopt langs de zijgevel van
|
||||||
|
// het hoge deel), dan loopt hij recht door tot de overstek, onder
|
||||||
|
// de rand van het hoge dak (van bovenaf overlappen ze).
|
||||||
|
const insideOther = (p) => others.some(r => p.x > r.rect.x0 + 0.5 && p.x < r.rect.x1 - 0.5 && p.z > r.rect.z0 + 0.5 && p.z < r.rect.z1 - 0.5);
|
||||||
|
segs.forEach((sg, i) => {
|
||||||
|
if (sg.dropped) return;
|
||||||
|
const prev = segs[(i - 1 + n) % n], next = segs[(i + 1) % n];
|
||||||
|
const alongZ = Math.abs(sg.a.x - sg.b.x) < 0.5;
|
||||||
|
const at = (q, v) => alongZ ? { x: q.x, z: v.z } : { x: v.x, z: q.z };
|
||||||
|
[['a', 'outerA', 'spotA', 'b', prev], ['b', 'outerB', 'spotB', 'a', next]].forEach(([k, outerK, spotK, otherK, nb]) => {
|
||||||
|
if (!nb.dropped) return;
|
||||||
|
const v = sg[k], corner = sg[outerK];
|
||||||
|
if (insideOther({ x: (v.x + corner.x) / 2, z: (v.z + corner.z) / 2 })) {
|
||||||
|
sg[outerK] = at(corner, v);
|
||||||
|
const dir = Math.sign(alongZ ? sg[otherK].z - v.z : sg[otherK].x - v.x);
|
||||||
|
const inset = dir * s.overhang * SPOT_RATIO;
|
||||||
|
sg[spotK] = alongZ ? { x: sg[spotK].x, z: v.z + inset } : { x: v.x + inset, z: sg[spotK].z };
|
||||||
|
} else {
|
||||||
|
sg[k] = at(v, corner);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
segs.filter(sg => !sg.dropped).forEach(sg => segments.push(sg));
|
||||||
});
|
});
|
||||||
|
|
||||||
const segLen = (sg) => Math.hypot(sg.outerB.x - sg.outerA.x, sg.outerB.z - sg.outerA.z);
|
const segLen = (sg) => Math.hypot(sg.outerB.x - sg.outerA.x, sg.outerB.z - sg.outerA.z);
|
||||||
|
const top = Math.max(...segments.map(sg => Math.max(sg.topAt(sg.a), sg.topAt(sg.b))));
|
||||||
return {
|
return {
|
||||||
base, segments, overhang: s.overhang, spotRatio: SPOT_RATIO,
|
segments, overhang: s.overhang, spotRatio: SPOT_RATIO,
|
||||||
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight,
|
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight,
|
||||||
top: topY, trimTop: topY + TRIM_ABOVE_MM,
|
top, edgeAbove: EDGE_ABOVE_MM,
|
||||||
fasciaLengthMM: segments.filter(sg => sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
|
fasciaLengthMM: segments.filter(sg => sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
|
||||||
trimLengthMM: segments.filter(sg => !sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
|
trimLengthMM: segments.filter(sg => !sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
|
||||||
soffitM2: segments.filter(sg => sg.fascia).reduce((t, sg) => t + quadArea([sg.a, sg.b, sg.outerB, sg.outerA]), 0) / 1e6,
|
soffitM2: segments.filter(sg => sg.fascia).reduce((t, sg) => t + quadArea([sg.a, sg.b, sg.outerB, sg.outerA]), 0) / 1e6,
|
||||||
@@ -186,9 +405,40 @@ window.RoofModel = (() => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3D van de dakrand: per ribbe een geëxtrudeerd vierhoek (dakrand ->
|
// Convexe veelhoek (plattegrond, mm) knippen met een halfvlak
|
||||||
// buitenrand, in verstek), naar beneden over de hoogte van boeideel resp.
|
// (Sutherland-Hodgman): houd de punten waar f(p) >= 0.
|
||||||
// daktrim. Plus de boeideel-spots (edge.spots) in de onderplaat.
|
function clipPoly(poly, f) {
|
||||||
|
const out = [];
|
||||||
|
poly.forEach((p, k) => {
|
||||||
|
const q = poly[(k + 1) % poly.length], fp = f(p), fq = f(q);
|
||||||
|
if (fp >= 0) out.push(p);
|
||||||
|
if ((fp >= 0) !== (fq >= 0)) {
|
||||||
|
const t = fp / (fp - fq);
|
||||||
|
out.push({ x: p.x + (q.x - p.x) * t, z: p.z + (q.z - p.z) * t });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prisma onder een convexe veelhoek: bovenkant per hoekpunt op topFn(p),
|
||||||
|
// onderkant h lager (mm -> meters).
|
||||||
|
function prismGeometry(poly, topFn, h) {
|
||||||
|
const toM = 0.001, n = poly.length, pos = [];
|
||||||
|
const T = poly.map(p => [p.x * toM, topFn(p) * toM, p.z * toM]);
|
||||||
|
const B = poly.map(p => [p.x * toM, (topFn(p) - h) * toM, p.z * toM]);
|
||||||
|
const tri = (a, b, c) => pos.push(...a, ...b, ...c);
|
||||||
|
for (let k = 1; k < n - 1; k++) { tri(T[0], T[k], T[k + 1]); tri(B[0], B[k + 1], B[k]); }
|
||||||
|
for (let k = 0; k < n; k++) { const j = (k + 1) % n; tri(T[k], B[k], B[j]); tri(T[k], B[j], T[j]); }
|
||||||
|
const geo = new THREE.BufferGeometry();
|
||||||
|
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
|
||||||
|
geo.computeVertexNormals();
|
||||||
|
return geo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3D van de dakrand: per ribbe een vierhoek (dakrand -> buitenrand, in
|
||||||
|
// verstek), naar beneden over de hoogte van boeideel resp. daktrim. De
|
||||||
|
// bovenkant volgt het dak (sg.topAt); op een nok wordt de ribbe geknipt.
|
||||||
|
// Plus de boeideel-spots (edge.spots) in de onderplaat.
|
||||||
// opts: { edgeVisible (volgt "Beplating"), spotsVisible (volgt "Lampen") }
|
// opts: { edgeVisible (volgt "Beplating"), spotsVisible (volgt "Lampen") }
|
||||||
function buildEdge3D(edge, materials, opts) {
|
function buildEdge3D(edge, materials, opts) {
|
||||||
const o = opts || {};
|
const o = opts || {};
|
||||||
@@ -196,23 +446,25 @@ window.RoofModel = (() => {
|
|||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
if (!edge) return group;
|
if (!edge) return group;
|
||||||
edge.segments.forEach(sg => {
|
edge.segments.forEach(sg => {
|
||||||
const pts = [sg.a, sg.b, sg.outerB, sg.outerA];
|
let polys = [[sg.a, sg.b, sg.outerB, sg.outerA]];
|
||||||
const shape = new THREE.Shape(pts.map(p => new THREE.Vector2(p.x * toM, p.z * toM)));
|
sg.ridges.forEach(r => {
|
||||||
const h = sg.fascia ? edge.fasciaHeight : edge.trimHeight;
|
const f = (p) => r.axis === 'x' ? p.x - r.value : p.z - r.value;
|
||||||
const mesh = new THREE.Mesh(
|
polys = polys.flatMap(pl => [clipPoly(pl, f), clipPoly(pl, p => -f(p))]).filter(pl => pl.length >= 3);
|
||||||
new THREE.ExtrudeGeometry(shape, { steps: 1, depth: h * toM, bevelEnabled: false }),
|
});
|
||||||
sg.fascia ? materials.fasciaMaterial : materials.trimMaterial
|
// Boeideel: totale hoogte fasciaHeight vanaf de bovenkant van de rand;
|
||||||
);
|
// daktrim: trimHeight onder het dakleer + de opstand erboven.
|
||||||
mesh.rotation.x = Math.PI / 2; // shape-y -> wereld-Z, extrusie naar beneden
|
const h = sg.fascia ? edge.fasciaHeight : edge.trimHeight + edge.edgeAbove;
|
||||||
mesh.position.y = (sg.fascia ? edge.top : edge.trimTop) * toM;
|
polys.forEach(pl => {
|
||||||
mesh.visible = o.edgeVisible !== false;
|
const mesh = new THREE.Mesh(prismGeometry(pl, sg.topAt, h), sg.fascia ? materials.fasciaMaterial : materials.trimMaterial);
|
||||||
group.add(mesh);
|
mesh.visible = o.edgeVisible !== false;
|
||||||
|
group.add(mesh);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const spotR = 0.035, spotH = 0.012;
|
const spotR = 0.035, spotH = 0.012;
|
||||||
edge.spots.forEach(p => {
|
edge.spots.forEach(p => {
|
||||||
const spot = new THREE.Mesh(new THREE.CylinderGeometry(spotR, spotR, spotH, 16), materials.spotOnMaterial);
|
const spot = new THREE.Mesh(new THREE.CylinderGeometry(spotR, spotR, spotH, 16), materials.spotOnMaterial);
|
||||||
spot.position.set(p.x * toM, (edge.top - edge.fasciaHeight) * toM - spotH / 2 + 0.002, p.z * toM);
|
spot.position.set(p.x * toM, p.y * toM - spotH / 2 + 0.002, p.z * toM);
|
||||||
spot.visible = o.spotsVisible !== false;
|
spot.visible = o.spotsVisible !== false;
|
||||||
group.add(spot);
|
group.add(spot);
|
||||||
});
|
});
|
||||||
@@ -229,6 +481,7 @@ window.RoofModel = (() => {
|
|||||||
return {
|
return {
|
||||||
DAKLEER_MM, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO,
|
DAKLEER_MM, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO,
|
||||||
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
|
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
|
||||||
|
computeShape, wallProfile, profileHeight, computeUnitRoof, buildUnit3D,
|
||||||
computeEdge, buildEdge3D, normalizeSettings
|
computeEdge, buildEdge3D, normalizeSettings
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
+88
-5
@@ -157,9 +157,18 @@ window.UnitModel = (() => {
|
|||||||
const full = s.axis === 'x' ? W / 2 : D / 2;
|
const full = s.axis === 'x' ? W / 2 : D / 2;
|
||||||
const ext0 = Math.max(0, Math.round(s0 + full)), ext1 = Math.max(0, Math.round(full - s1));
|
const ext0 = Math.max(0, Math.round(s0 + full)), ext1 = Math.max(0, Math.round(full - s1));
|
||||||
const skinExt = s.dir > 0 ? { left: ext0, right: ext1 } : { left: ext1, right: ext0 };
|
const skinExt = s.dir > 0 ? { left: ext0, right: ext1 } : { left: ext1, right: ext0 };
|
||||||
|
// Doorlopende wand die op een hoek vervalt: de haakse wand loopt daar
|
||||||
|
// tot de vloerrand door en staat dus op deze wandlijn (endInset, u-mm
|
||||||
|
// vanaf links/rechts). Een opbouw in die naad-zone begint erna.
|
||||||
|
const through = state.layout.corners === 'zijkanten' ? s.axis === 'z' : s.axis === 'x';
|
||||||
|
const pL = s.dir > 0 ? s0 : s1, pR = s.dir > 0 ? s1 : s0;
|
||||||
|
const endInset = {
|
||||||
|
left: through && cutAt(s.key, pL) ? T[WALL_ENDS[s.key].left] : 0,
|
||||||
|
right: through && cutAt(s.key, pR) ? T[WALL_ENDS[s.key].right] : 0
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
side: s.key, label: s.label, nr: s.nr, rotY: s.rotY, normal: s.normal, axis: s.axis,
|
side: s.key, label: s.label, nr: s.nr, rotY: s.rotY, normal: s.normal, axis: s.axis,
|
||||||
thickness: T[s.key], width, cx, cz, cutsU, pieces, skinExt
|
thickness: T[s.key], width, cx, cz, cutsU, pieces, skinExt, endInset
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -170,11 +179,22 @@ window.UnitModel = (() => {
|
|||||||
// op de volle zijde opgelost (ankers gelden voor de hele zijde) en daarna
|
// op de volle zijde opgelost (ankers gelden voor de hele zijde) en daarna
|
||||||
// aan het stuk toegewezen waar ze volledig in vallen; openingen die in een
|
// aan het stuk toegewezen waar ze volledig in vallen; openingen die in een
|
||||||
// naad vallen vervallen (dropped).
|
// naad vallen vervallen (dropped).
|
||||||
function wallPieces(wallLayout, wallState) {
|
// raised (optioneel): [{ u0, u1, base }] = opbouw in een naad-zone: daar
|
||||||
|
// staat de wand pas vanaf hoogte `base` (mm vanaf het loopvlak, bijv. boven
|
||||||
|
// het lagere dak van de buur-unit) tot de daklijn. Zo'n stuk heeft
|
||||||
|
// baseY = base; openingen die er volledig boven base in vallen horen erbij
|
||||||
|
// (y t.o.v. de onderkant van het stuk). Te lage opbouw vervalt.
|
||||||
|
const MIN_RAISE_MM = 150;
|
||||||
|
function wallPieces(wallLayout, wallState, raised) {
|
||||||
const fullConfig = WallModel.configFromState(wallState);
|
const fullConfig = WallModel.configFromState(wallState);
|
||||||
const W = wallLayout.width;
|
const W = wallLayout.width;
|
||||||
const hL = wallState.wall.heightLeft, hR = wallState.wall.heightRight;
|
const hL = wallState.wall.heightLeft, hR = wallState.wall.heightRight;
|
||||||
const heightAt = (u) => hL + (u / W) * (hR - hL);
|
// Middenpunt (nok) midden op de wand; een stuk krijgt de nok alleen als
|
||||||
|
// die erin valt, anders de daklijn ter plaatse als gewone helling.
|
||||||
|
const hM = wallState.wall.heightMid > 0 ? Math.max(wallState.wall.heightMid, hL, hR) : null;
|
||||||
|
const pU = W / 2;
|
||||||
|
const heightAt = (u) => hM === null ? hL + (u / W) * (hR - hL)
|
||||||
|
: u <= pU ? hL + (u / pU) * (hM - hL) : hM + ((u - pU) / (W - pU)) * (hR - hM);
|
||||||
const multi = wallLayout.pieces.length > 1;
|
const multi = wallLayout.pieces.length > 1;
|
||||||
const used = new Set();
|
const used = new Set();
|
||||||
|
|
||||||
@@ -182,8 +202,10 @@ window.UnitModel = (() => {
|
|||||||
const openings = fullConfig.openings
|
const openings = fullConfig.openings
|
||||||
.filter(op => op.x >= a - EPS && op.x + op.w <= b + EPS)
|
.filter(op => op.x >= a - EPS && op.x + op.w <= b + EPS)
|
||||||
.map(op => { used.add(op.id); return { id: op.id, type: op.type, w: op.w, h: op.h, y: op.y, anchor: 'left', offset: op.x - a }; });
|
.map(op => { used.add(op.id); return { id: op.id, type: op.type, w: op.w, h: op.h, y: op.y, anchor: 'left', offset: op.x - a }; });
|
||||||
|
const hasPeak = hM !== null && a < pU - 1 && b > pU + 1;
|
||||||
const params = Object.assign({}, wallState.wall, {
|
const params = Object.assign({}, wallState.wall, {
|
||||||
width: b - a, heightLeft: Math.round(heightAt(a)), heightRight: Math.round(heightAt(b))
|
width: b - a, heightLeft: Math.round(heightAt(a)), heightRight: Math.round(heightAt(b)),
|
||||||
|
heightMid: hasPeak ? hM : null, peakX: hasPeak ? pU - a : undefined
|
||||||
});
|
});
|
||||||
const config = WallModel.buildConfig(params, openings);
|
const config = WallModel.buildConfig(params, openings);
|
||||||
return {
|
return {
|
||||||
@@ -193,10 +215,71 @@ window.UnitModel = (() => {
|
|||||||
config, frame: WallModel.computeFrame(config)
|
config, frame: WallModel.computeFrame(config)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const raisedList = (raised || []).filter(r => r.u1 - r.u0 >= MIN_PIECE_MM);
|
||||||
|
const ins = wallLayout.endInset || { left: 0, right: 0 };
|
||||||
|
raisedList.forEach((r, i) => {
|
||||||
|
const { u0: a, u1: b, base } = r;
|
||||||
|
const hasPeak = hM !== null && a < pU - 1 && b > pU + 1;
|
||||||
|
const hA = heightAt(a) - base, hB = heightAt(b) - base;
|
||||||
|
if (Math.min(hA, hB) < MIN_RAISE_MM) return;
|
||||||
|
const openings = fullConfig.openings
|
||||||
|
.filter(op => op.x >= a - EPS && op.x + op.w <= b + EPS && op.y >= base - EPS)
|
||||||
|
.map(op => { used.add(op.id); return { id: op.id, type: op.type, w: op.w, h: op.h, y: op.y - base, anchor: 'left', offset: op.x - a }; });
|
||||||
|
const params = Object.assign({}, wallState.wall, {
|
||||||
|
width: b - a, heightLeft: Math.round(hA), heightRight: Math.round(hB),
|
||||||
|
heightMid: hasPeak ? hM - base : null, peakX: hasPeak ? pU - a : undefined
|
||||||
|
});
|
||||||
|
const config = WallModel.buildConfig(params, openings);
|
||||||
|
pieces.push({
|
||||||
|
suffix: 'o' + (raisedList.length > 1 ? i + 1 : ''), // opbouw
|
||||||
|
u0: a, u1: b, lxCenter: -W / 2 + (a + b) / 2, baseY: Math.round(base),
|
||||||
|
// Naast de haakse wand in de hoek (endInset) bedekken de buitenlagen
|
||||||
|
// ook diens kopse kant, die boven het lage dak bloot ligt.
|
||||||
|
skinExt: {
|
||||||
|
left: ins.left > 0 && a <= ins.left + EPS ? ins.left : 0,
|
||||||
|
right: ins.right > 0 && b >= W - ins.right - EPS ? ins.right : 0
|
||||||
|
},
|
||||||
|
config, frame: WallModel.computeFrame(config)
|
||||||
|
});
|
||||||
|
});
|
||||||
const dropped = fullConfig.openings.filter(op => !used.has(op.id));
|
const dropped = fullConfig.openings.filter(op => !used.has(op.id));
|
||||||
return { pieces, dropped, fullConfig };
|
return { pieces, dropped, fullConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wandhoogtes koppelen, zodat er altijd een logisch dak op past. De
|
||||||
|
// bewerkte wand (side) bepaalt de dakvorm:
|
||||||
|
// - de tegenoverliggende wand krijgt dezelfde hoogtes gespiegeld (van
|
||||||
|
// buitenaf gezien wisselen links en rechts), incl. middenpunt (nok);
|
||||||
|
// - de twee haakse wanden worden vlak op de hoogte van de hoek waar ze
|
||||||
|
// op aansluiten (goothoogte bij een lessenaar- of zadeldak).
|
||||||
|
// Voorbeeld: west zuid hoger dan noord -> oost ook, noordwand vlak op de
|
||||||
|
// lage, zuidwand vlak op de hoge hoogte. Muteert state.
|
||||||
|
// Welke haakse wand zit aan de linker/rechterkant (van buitenaf gezien).
|
||||||
|
const WALL_ENDS = {
|
||||||
|
voor: { left: 'rechts', right: 'links' },
|
||||||
|
achter: { left: 'links', right: 'rechts' },
|
||||||
|
links: { left: 'voor', right: 'achter' },
|
||||||
|
rechts: { left: 'achter', right: 'voor' }
|
||||||
|
};
|
||||||
|
const OPPOSITE = { voor: 'achter', achter: 'voor', links: 'rechts', rechts: 'links' };
|
||||||
|
function syncWallHeights(state, side) {
|
||||||
|
const w = state.walls[side].wall;
|
||||||
|
const mid = w.heightMid > 0 ? w.heightMid : null;
|
||||||
|
const set = (key, hL, hR, hM) => {
|
||||||
|
const t = state.walls[key].wall;
|
||||||
|
t.heightLeft = hL; t.heightRight = hR; t.heightMid = hM;
|
||||||
|
t.heightAll = hM === null && hL === hR ? hL : w.heightAll;
|
||||||
|
};
|
||||||
|
set(OPPOSITE[side], w.heightRight, w.heightLeft, mid);
|
||||||
|
set(WALL_ENDS[side].left, w.heightLeft, w.heightLeft, null);
|
||||||
|
set(WALL_ENDS[side].right, w.heightRight, w.heightRight, null);
|
||||||
|
}
|
||||||
|
// Hoogste punt van een wand (incl. nok).
|
||||||
|
function wallTopOf(wallState) {
|
||||||
|
const w = wallState.wall;
|
||||||
|
return Math.max(w.heightLeft, w.heightRight, w.heightMid > 0 ? w.heightMid : 0);
|
||||||
|
}
|
||||||
|
|
||||||
function validateState(design) {
|
function validateState(design) {
|
||||||
if (!design.floor) throw new Error('veld "floor" ontbreekt');
|
if (!design.floor) throw new Error('veld "floor" ontbreekt');
|
||||||
try { FloorModel.validateState(design.floor); } catch (err) { throw new Error('vloer: ' + err.message); }
|
try { FloorModel.validateState(design.floor); } catch (err) { throw new Error('vloer: ' + err.message); }
|
||||||
@@ -210,5 +293,5 @@ window.UnitModel = (() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { WALL_SIDES, CORNER_MODES, clone, createDefaultState, localToWorld, computeLayout, wallPieces, validateState };
|
return { WALL_SIDES, CORNER_MODES, clone, createDefaultState, localToWorld, computeLayout, wallPieces, syncWallHeights, wallTopOf, validateState };
|
||||||
})();
|
})();
|
||||||
|
|||||||
+333
-272
@@ -14,7 +14,7 @@
|
|||||||
// voor zowel de losse wand als de unit gelden.
|
// voor zowel de losse wand als de unit gelden.
|
||||||
//
|
//
|
||||||
// Werkt met een "wand-state" (zelfde formaat als getStateJson/JSON-export):
|
// Werkt met een "wand-state" (zelfde formaat als getStateJson/JSON-export):
|
||||||
// { wall: {width, heightAll, heightLeft, heightRight, studWidth, ...},
|
// { wall: {width, heightAll, heightLeft, heightRight, heightMid?, studWidth, ...},
|
||||||
// materials: {interior, exterior}, openings: [{id,w,h,y,anchor,offset}] }
|
// materials: {interior, exterior}, openings: [{id,w,h,y,anchor,offset}] }
|
||||||
//
|
//
|
||||||
// Plain script (geen ES-module), moet na core/geometry.js geladen worden.
|
// Plain script (geen ES-module), moet na core/geometry.js geladen worden.
|
||||||
@@ -95,10 +95,90 @@ window.WallModel = (() => {
|
|||||||
deur: { label: 'Deur', w: 900, h: 2150, y: 0 },
|
deur: { label: 'Deur', w: 900, h: 2150, y: 0 },
|
||||||
voordeur: { label: 'Voordeur', w: 1150, h: 2150, y: 0 }
|
voordeur: { label: 'Voordeur', w: 1150, h: 2150, y: 0 }
|
||||||
};
|
};
|
||||||
|
// Deuren krijgen aan weerszijden een tweede stijl direct tegen de
|
||||||
|
// koningsstijl (stevigheid, computeFrame).
|
||||||
|
const isDoor = (op) => op.type === 'deur' || op.type === 'voordeur';
|
||||||
|
|
||||||
// Buitenzijde-materialen (sleutel -> weergavenaam). 'spaanplaat' is voor
|
// Buitenzijde-materialen (sleutel -> weergavenaam). 'spaanplaat' is voor
|
||||||
// binnenwanden (aan beide zijden spaanplaat).
|
// binnenwanden (aan beide zijden spaanplaat).
|
||||||
const EXTERIOR_MATERIALS = { vochtdoek: 'Vochtdoek', cementplaat: 'Cement-gebonden platen', spaanplaat: 'Spaanplaat' };
|
// 'geen': geen beplating aan de buitenzijde — naadwand-deel dat tegen het
|
||||||
|
// deel van de andere unit aan staat (HouseModel, naadwanden).
|
||||||
|
const EXTERIOR_MATERIALS = { vochtdoek: 'Vochtdoek', cementplaat: 'Cement-gebonden platen', spaanplaat: 'Spaanplaat', geen: 'Geen (naadzijde)' };
|
||||||
|
|
||||||
|
// Bovenrand van de wand (daklijn): één recht stuk van links naar rechts,
|
||||||
|
// of met een middenpunt (nok, wallPeak) twee stukken die elkaar daar
|
||||||
|
// raken. De nok ligt nooit lager dan de goten (buildConfig), dus de
|
||||||
|
// bovenrand is overal het MINIMUM van de (doorgetrokken) stukken — dat
|
||||||
|
// houdt at/under/rangeAbove simpel en maakt "laagste punt boven een
|
||||||
|
// opening" gelijk aan het laagste van de twee uiteinden.
|
||||||
|
// x t.o.v. het wandmidden, alles in mm x k (k = 0.001: meters).
|
||||||
|
// at(x) bovenkant (daklijn)
|
||||||
|
// under(x) onderkant bovenregel(s); knee = hun snijpunt (verstek)
|
||||||
|
// rangeAbove(y) [x0, x1] waar de daklijn boven y ligt (of null)
|
||||||
|
// crossings(y) x-posities waar de daklijn y kruist (knikpunten)
|
||||||
|
// underPts(a, b) onderkant bovenregel van a naar b, incl. knie
|
||||||
|
function topLine(CONFIG, k) {
|
||||||
|
k = k || 1;
|
||||||
|
const halfW = CONFIG.wallWidth / 2 * k;
|
||||||
|
const L = { x: -halfW, y: CONFIG.wallHeightLeft * k }, R = { x: halfW, y: CONFIG.wallHeightRight * k };
|
||||||
|
const pk = CONFIG.wallPeak ? { x: (CONFIG.wallPeak.x - CONFIG.wallWidth / 2) * k, y: CONFIG.wallPeak.h * k } : null;
|
||||||
|
const pts = pk ? [L, pk, R] : [L, R];
|
||||||
|
const segs = [];
|
||||||
|
for (let i = 0; i < pts.length - 1; i++) {
|
||||||
|
const a = pts[i], b = pts[i + 1], slope = (b.y - a.y) / (b.x - a.x);
|
||||||
|
segs.push({ a, b, slope, vo: CONFIG.plateThick * k * Math.sqrt(1 + slope * slope) });
|
||||||
|
}
|
||||||
|
const lineY = (sg, x) => sg.a.y + (x - sg.a.x) * sg.slope;
|
||||||
|
const at = (x) => Math.min(...segs.map(sg => lineY(sg, x)));
|
||||||
|
const under = (x) => Math.min(...segs.map(sg => lineY(sg, x) - sg.vo));
|
||||||
|
let knee = null;
|
||||||
|
if (segs.length === 2) {
|
||||||
|
const [s0, s1] = segs, ds = s0.slope - s1.slope;
|
||||||
|
// s0.a.y + (x - s0.a.x) s0.slope - vo0 = s1.a.y + (x - s1.a.x) s1.slope - vo1
|
||||||
|
const x = Math.abs(ds) > 1e-12
|
||||||
|
? ((s1.a.y - s1.a.x * s1.slope - s1.vo) - (s0.a.y - s0.a.x * s0.slope - s0.vo)) / ds
|
||||||
|
: pk.x;
|
||||||
|
knee = { x, y: under(x) };
|
||||||
|
}
|
||||||
|
const rangeAbove = (y) => {
|
||||||
|
let x0 = -halfW, x1 = halfW;
|
||||||
|
segs.forEach(sg => {
|
||||||
|
if (Math.abs(sg.slope) < 1e-12) { if (sg.a.y <= y) x1 = x0 - 1; return; }
|
||||||
|
const xc = sg.a.x + (y - sg.a.y) / sg.slope;
|
||||||
|
if (sg.slope > 0) x0 = Math.max(x0, xc); else x1 = Math.min(x1, xc);
|
||||||
|
});
|
||||||
|
return x1 > x0 ? [x0, x1] : null;
|
||||||
|
};
|
||||||
|
const crossings = (y) => segs.filter(sg => Math.abs(sg.slope) > 1e-12)
|
||||||
|
.map(sg => ({ sg, x: sg.a.x + (y - sg.a.y) / sg.slope }))
|
||||||
|
.filter(c => c.x > c.sg.a.x && c.x < c.sg.b.x).map(c => c.x);
|
||||||
|
const underPts = (xa, xb) => {
|
||||||
|
const out = [{ x: xa, y: under(xa) }];
|
||||||
|
if (knee && knee.x > xa && knee.x < xb) out.push({ x: knee.x, y: knee.y });
|
||||||
|
out.push({ x: xb, y: under(xb) });
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
pts, segs, peak: pk, knee, at, under, rangeAbove, crossings, underPts,
|
||||||
|
maxY: Math.max(...pts.map(p => p.y)), minY: Math.min(L.y, R.y),
|
||||||
|
kinks: pk ? [pk.x] : []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Omtrek(ken) van de bovenregel(s) (zelfde eenheid als topLine): één
|
||||||
|
// parallellogram, of met nok twee stukken in verstek (nok boven, knie onder).
|
||||||
|
function topPlateOutlines(tl) {
|
||||||
|
const [L, R] = [tl.pts[0], tl.pts[tl.pts.length - 1]];
|
||||||
|
if (!tl.peak) {
|
||||||
|
const vo = tl.segs[0].vo;
|
||||||
|
return [[L, R, { x: R.x, y: R.y - vo }, { x: L.x, y: L.y - vo }]];
|
||||||
|
}
|
||||||
|
const P = tl.peak, K = tl.knee;
|
||||||
|
return [
|
||||||
|
[L, P, K, { x: L.x, y: L.y - tl.segs[0].vo }],
|
||||||
|
[P, R, { x: R.x, y: R.y - tl.segs[1].vo }, K]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Heuristiek (geen volwaardige constraint-solver): elke opening wordt eerst
|
// Heuristiek (geen volwaardige constraint-solver): elke opening wordt eerst
|
||||||
// individueel gepositioneerd op basis van zijn anker, dan van links naar
|
// individueel gepositioneerd op basis van zijn anker, dan van links naar
|
||||||
@@ -114,7 +194,8 @@ window.WallModel = (() => {
|
|||||||
// het laagste punt van de hele wand. Bij een hellende wand mag een opening
|
// het laagste punt van de hele wand. Bij een hellende wand mag een opening
|
||||||
// dicht bij de hoge kant dus hoger/groter zijn dan een opening dicht bij
|
// dicht bij de hoge kant dus hoger/groter zijn dan een opening dicht bij
|
||||||
// de lage kant.
|
// de lage kant.
|
||||||
function resolveOpenings(rawOpenings, wallWidth, studWidth, wallHeightLeft, wallHeightRight, plateThick) {
|
// roofAt(xLocal): daklijn, xLocal vanaf de linker wandrand (0..wallWidth).
|
||||||
|
function resolveOpenings(rawOpenings, wallWidth, studWidth, roofAt, plateThick) {
|
||||||
// 2850mm is de vaste referentie-bovengrens (bijv. max. sparingbreedte in
|
// 2850mm is de vaste referentie-bovengrens (bijv. max. sparingbreedte in
|
||||||
// dit constructiesysteem); daarnaast blijft de wand zelf ook een harde
|
// dit constructiesysteem); daarnaast blijft de wand zelf ook een harde
|
||||||
// grens (er moet nog stijlruimte overblijven).
|
// grens (er moet nog stijlruimte overblijven).
|
||||||
@@ -142,34 +223,44 @@ window.WallModel = (() => {
|
|||||||
o.x = Math.max(edgeMargin, Math.min(o.x, wallWidth - o.w - edgeMargin));
|
o.x = Math.max(edgeMargin, Math.min(o.x, wallWidth - o.w - edgeMargin));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Links-naar-rechts: duw overlappende openingen uit elkaar. 2x stijldikte
|
// Openingen die elkaar in de hoogte raken (incl. boven-/onderdorpel en
|
||||||
// i.p.v. 1x: de OSB-uitsparing rond elke opening steekt aan weerszijden
|
// 2x stijldikte ertussen) moeten naast elkaar; staan ze boven elkaar
|
||||||
// al 1x stijldikte uit (om de koningsstijl zichtbaar te laten), dus bij
|
// (bijv. een raam boven een deur), dan mogen ze in x overlappen.
|
||||||
// minder afstand tussen twee openingen zouden hun uitsparingen elkaar
|
// Hoogte hier op basis van de invoer; na fase 2 wordt het nog gecontroleerd.
|
||||||
// kunnen overlappen (gecontroleerd, zie eerdere test).
|
const vBand = (o, h, y) => [y > plateThick ? y - studWidth : 0, y + h + studWidth];
|
||||||
const minGap = studWidth * 2;
|
const stacked = (a, b, useFinal) => {
|
||||||
|
const [a0, a1] = useFinal ? vBand(a, a.h, a.y) : vBand(a, Math.max(400, a.rawH), a.rawY);
|
||||||
|
const [b0, b1] = useFinal ? vBand(b, b.h, b.y) : vBand(b, Math.max(400, b.rawH), b.rawY);
|
||||||
|
return a1 <= b0 + 0.5 || b1 <= a0 + 0.5;
|
||||||
|
};
|
||||||
|
// Tussenruimte naast elkaar: 2x stijldikte i.p.v. 1x: de OSB-uitsparing
|
||||||
|
// rond elke opening steekt aan weerszijden al 1x stijldikte uit (om de
|
||||||
|
// koningsstijl zichtbaar te laten), dus bij minder afstand zouden hun
|
||||||
|
// uitsparingen elkaar kunnen overlappen. Naast een deur 1x extra voor
|
||||||
|
// de dubbele stijl.
|
||||||
|
const gapOf = (a, b) => studWidth * (2 + (isDoor(a) ? 1 : 0) + (isDoor(b) ? 1 : 0));
|
||||||
|
|
||||||
|
// Links-naar-rechts: duw botsende openingen uit elkaar.
|
||||||
for (let i = 1; i < resolved.length; i++) {
|
for (let i = 1; i < resolved.length; i++) {
|
||||||
const prev = resolved[i - 1];
|
|
||||||
const cur = resolved[i];
|
const cur = resolved[i];
|
||||||
const minX = prev.x + prev.w + minGap;
|
for (let j = 0; j < i; j++) {
|
||||||
if (cur.x < minX) cur.x = minX;
|
const prev = resolved[j];
|
||||||
|
if (stacked(prev, cur)) continue;
|
||||||
|
cur.x = Math.max(cur.x, prev.x + prev.w + gapOf(prev, cur));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rechts-naar-links correctie als de laatste opening nu over de rand steekt
|
// Rechts-naar-links: wat over de rechterrand steekt terugtrekken, en de
|
||||||
|
// botsende openingen links ervan mee.
|
||||||
const rightLimit = wallWidth - edgeMargin;
|
const rightLimit = wallWidth - edgeMargin;
|
||||||
let overflow = 0;
|
for (let i = resolved.length - 1; i >= 0; i--) {
|
||||||
if (resolved.length) {
|
const cur = resolved[i];
|
||||||
const last = resolved[resolved.length - 1];
|
let maxX = rightLimit - cur.w;
|
||||||
overflow = Math.max(0, (last.x + last.w) - rightLimit);
|
for (let j = i + 1; j < resolved.length; j++) {
|
||||||
}
|
const next = resolved[j];
|
||||||
if (overflow > 0) {
|
if (!stacked(cur, next)) maxX = Math.min(maxX, next.x - gapOf(cur, next) - cur.w);
|
||||||
for (let i = resolved.length - 1; i >= 0; i--) {
|
|
||||||
resolved[i].x -= overflow;
|
|
||||||
if (i > 0) {
|
|
||||||
const maxX = resolved[i].x - minGap - resolved[i - 1].w;
|
|
||||||
if (resolved[i - 1].x > maxX) resolved[i - 1].x = maxX;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (cur.x > maxX) cur.x = maxX;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fase 2: hoogte (h) en verticale positie (y), per opening geklemd tegen
|
// Fase 2: hoogte (h) en verticale positie (y), per opening geklemd tegen
|
||||||
@@ -177,8 +268,9 @@ window.WallModel = (() => {
|
|||||||
// die opening — het punt waar een verticale lijn op die plek de
|
// die opening — het punt waar een verticale lijn op die plek de
|
||||||
// (eventueel hellende) horizontale topbalk zou snijden. Bij een
|
// (eventueel hellende) horizontale topbalk zou snijden. Bij een
|
||||||
// opening die zelf een breedte heeft, wordt het laagste van de twee
|
// opening die zelf een breedte heeft, wordt het laagste van de twee
|
||||||
// uiteinden aangehouden (de daklijn is lineair, dus dat is voldoende).
|
// uiteinden aangehouden (de daklijn is lineair of heeft een nok als
|
||||||
const localRoofHeightAt = (xLocal) => wallHeightLeft + (xLocal / wallWidth) * (wallHeightRight - wallHeightLeft);
|
// hoogste punt, dus dat is voldoende).
|
||||||
|
const localRoofHeightAt = roofAt;
|
||||||
|
|
||||||
resolved.forEach(o => {
|
resolved.forEach(o => {
|
||||||
const localCeiling = Math.min(localRoofHeightAt(o.x), localRoofHeightAt(o.x + o.w));
|
const localCeiling = Math.min(localRoofHeightAt(o.x), localRoofHeightAt(o.x + o.w));
|
||||||
@@ -191,14 +283,16 @@ window.WallModel = (() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Detecteer of het na dit alles nog steeds niet past (te veel/te brede
|
// Detecteer of het na dit alles nog steeds niet past (te veel/te brede
|
||||||
// openingen voor deze wandbreedte) zodat de UI dit kan tonen.
|
// openingen voor deze wandbreedte, of boven elkaar zonder ruimte
|
||||||
|
// ertussen) zodat de UI dit kan tonen.
|
||||||
resolved.forEach(o => {
|
resolved.forEach(o => {
|
||||||
o.overflowing = (o.x < edgeMargin - 0.5) || (o.x + o.w > wallWidth - edgeMargin + 0.5);
|
o.overflowing = (o.x < edgeMargin - 0.5) || (o.x + o.w > wallWidth - edgeMargin + 0.5);
|
||||||
});
|
});
|
||||||
for (let i = 1; i < resolved.length; i++) {
|
for (let i = 0; i < resolved.length; i++) {
|
||||||
if (resolved[i].x < resolved[i - 1].x + resolved[i - 1].w + minGap - 0.5) {
|
for (let j = i + 1; j < resolved.length; j++) {
|
||||||
resolved[i].overflowing = true;
|
const a = resolved[i], b = resolved[j];
|
||||||
resolved[i - 1].overflowing = true;
|
const [l, r] = a.x <= b.x ? [a, b] : [b, a];
|
||||||
|
if (!stacked(a, b, true) && r.x < l.x + l.w + gapOf(l, r) - 0.5) { a.overflowing = true; b.overflowing = true; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,11 +308,24 @@ window.WallModel = (() => {
|
|||||||
const hRight = Math.round(params.heightRight || 2900);
|
const hRight = Math.round(params.heightRight || 2900);
|
||||||
const studWidth = Math.round(params.studWidth || STANDARD_BEAM.width);
|
const studWidth = Math.round(params.studWidth || STANDARD_BEAM.width);
|
||||||
const plateThick = Math.round(params.plateThick || STANDARD_BEAM.width);
|
const plateThick = Math.round(params.plateThick || STANDARD_BEAM.width);
|
||||||
|
// Middenpunt (nok): standaard midden op de wand, of op peakX (mm vanaf
|
||||||
|
// links; een wandstuk uit een unit waar de nok niet in het midden
|
||||||
|
// valt). Nooit lager dan de hoogste goot; gelijk = geen nok.
|
||||||
|
let wallPeak = null;
|
||||||
|
if (params.heightMid > 0) {
|
||||||
|
const px = params.peakX !== undefined ? params.peakX : wallWidth / 2;
|
||||||
|
const h = Math.round(Math.max(params.heightMid, hLeft, hRight));
|
||||||
|
if (px > 1 && px < wallWidth - 1 && h > hLeft + (px / wallWidth) * (hRight - hLeft) + 0.5) wallPeak = { x: px, h };
|
||||||
|
}
|
||||||
|
const roofAt = (x) => wallPeak
|
||||||
|
? (x <= wallPeak.x ? hLeft + x / wallPeak.x * (wallPeak.h - hLeft) : wallPeak.h + (x - wallPeak.x) / (wallWidth - wallPeak.x) * (hRight - wallPeak.h))
|
||||||
|
: hLeft + (x / wallWidth) * (hRight - hLeft);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
wallWidth,
|
wallWidth,
|
||||||
wallHeightLeft: hLeft,
|
wallHeightLeft: hLeft,
|
||||||
wallHeightRight: hRight,
|
wallHeightRight: hRight,
|
||||||
|
wallPeak,
|
||||||
studWidth,
|
studWidth,
|
||||||
studDepth: Math.round(params.studDepth || STANDARD_BEAM.depth),
|
studDepth: Math.round(params.studDepth || STANDARD_BEAM.depth),
|
||||||
plateThick,
|
plateThick,
|
||||||
@@ -226,7 +333,7 @@ window.WallModel = (() => {
|
|||||||
sheathingThick: Math.round(params.sheathingThick || 10),
|
sheathingThick: Math.round(params.sheathingThick || 10),
|
||||||
minSpacing: Math.round(params.minSpacing || 240),
|
minSpacing: Math.round(params.minSpacing || 240),
|
||||||
maxSpacing: Math.round(params.maxSpacing || 480),
|
maxSpacing: Math.round(params.maxSpacing || 480),
|
||||||
openings: resolveOpenings(rawOpenings, wallWidth, studWidth, hLeft, hRight, plateThick)
|
openings: resolveOpenings(rawOpenings, wallWidth, studWidth, roofAt, plateThick)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,17 +341,23 @@ window.WallModel = (() => {
|
|||||||
return buildConfig(state.wall, state.openings || []);
|
return buildConfig(state.wall, state.openings || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Totale wanddikte (mm): regelwerk + beplating aan beide zijden. Gebruikt
|
// Totale wanddikte (mm): regelwerk + beplating aan beide zijden (bij
|
||||||
// door de unit om wanden met hun buitenkant op de vloerrand te zetten.
|
// buitenMateriaal 'geen' alleen aan de binnenzijde). Gebruikt door de unit
|
||||||
function thicknessOf(config) {
|
// om wanden met hun buitenkant op de vloerrand te zetten.
|
||||||
return Math.max(config.studDepth, config.plateDepth) + 2 * (config.sheathingThick + SKIN_GAP_MM);
|
function thicknessOf(config, buitenMateriaal) {
|
||||||
|
const sides = buitenMateriaal === 'geen' ? 1 : 2;
|
||||||
|
return Math.max(config.studDepth, config.plateDepth) + sides * (config.sheathingThick + SKIN_GAP_MM);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regelwerk: volledige stijlen (incl. koningsstijlen) + kreupele stijlen.
|
// Regelwerk: volledige stijlen (incl. koningsstijlen) + kreupele stijlen.
|
||||||
function computeFrame(CONFIG) {
|
function computeFrame(CONFIG) {
|
||||||
const startX = -CONFIG.wallWidth / 2;
|
const startX = -CONFIG.wallWidth / 2;
|
||||||
const slope = (CONFIG.wallHeightRight - CONFIG.wallHeightLeft) / CONFIG.wallWidth;
|
const top = topLine(CONFIG);
|
||||||
const angleDeg = Math.round(Math.atan(slope) * (180 / Math.PI));
|
const slope = top.segs[0].slope;
|
||||||
|
// Met nok: de steilste helling (beide kanten lopen dan op naar de nok).
|
||||||
|
const angleDeg = top.peak
|
||||||
|
? Math.round(Math.max(...top.segs.map(sg => Math.atan(Math.abs(sg.slope)))) * (180 / Math.PI))
|
||||||
|
: Math.round(Math.atan(slope) * (180 / Math.PI));
|
||||||
|
|
||||||
// Dubbele hoekstijlen: de buitenste staat op de wandrand, de tweede er
|
// Dubbele hoekstijlen: de buitenste staat op de wandrand, de tweede er
|
||||||
// direct tegenaan. De regelmatige verdeling begint bij de binnenste.
|
// direct tegenaan. De regelmatige verdeling begint bij de binnenste.
|
||||||
@@ -258,52 +371,88 @@ window.WallModel = (() => {
|
|||||||
cornerStuds.push(outerLeftStudX + i * CONFIG.studWidth, outerRightStudX - i * CONFIG.studWidth);
|
cornerStuds.push(outerLeftStudX + i * CONFIG.studWidth, outerRightStudX - i * CONFIG.studWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
let fullStuds = [];
|
// Stijlen als stukken { x, y0, y1, kind } (mm; y1 null = tot de
|
||||||
let jackStuds = [];
|
// daklijn). Eerst kandidaat-posities, daarna wordt elke kandidaat
|
||||||
|
// opgeknipt door de openingen (incl. boven-/onderdorpel) die hij kruist.
|
||||||
|
// Zo kunnen openingen ook boven elkaar staan (raam boven een deur): de
|
||||||
|
// stijlen ertussen lopen van dorpel tot dorpel.
|
||||||
|
// hoek dubbele hoekstijl
|
||||||
|
// koning koningsstijl direct naast een opening
|
||||||
|
// dubbel tweede stijl direct tegen de koningsstijl van een deur
|
||||||
|
// regel regelmatige verdeling in de vrije zones
|
||||||
|
// kreupel boven/onder een opening, binnen de openingbreedte
|
||||||
|
const sw = CONFIG.studWidth;
|
||||||
const ops = CONFIG.openings; // al gesorteerd op x door resolveOpenings
|
const ops = CONFIG.openings; // al gesorteerd op x door resolveOpenings
|
||||||
|
const bands = ops.map(op => ({
|
||||||
|
op, x0: startX + op.x, x1: startX + op.x + op.w,
|
||||||
|
y0: op.y > CONFIG.plateThick ? op.y - sw : 0, y1: op.y + op.h + sw
|
||||||
|
}));
|
||||||
|
const cands = [];
|
||||||
|
cornerStuds.forEach(x => cands.push({ x, kind: 'hoek' }));
|
||||||
|
ops.forEach(op => {
|
||||||
|
const x0 = startX + op.x, x1 = x0 + op.w;
|
||||||
|
cands.push({ x: x0 - sw / 2, kind: 'koning' }, { x: x1 + sw / 2, kind: 'koning' });
|
||||||
|
if (isDoor(op)) cands.push({ x: x0 - 1.5 * sw, kind: 'dubbel' }, { x: x1 + 1.5 * sw, kind: 'dubbel' });
|
||||||
|
});
|
||||||
|
|
||||||
if (ops.length === 0) {
|
// Kolommen: openingen (incl. hun stijlen) die elkaar in x overlappen.
|
||||||
fullStuds.push(...cornerStuds);
|
// Tussen de kolommen en de hoekstijlen: regelmatige verdeling.
|
||||||
const availWidth = wallRightStudX - wallLeftStudX;
|
const cols = [];
|
||||||
calculateSegmentPositions(availWidth, CONFIG.minSpacing, CONFIG.maxSpacing)
|
ops.map(op => {
|
||||||
.forEach(p => fullStuds.push(wallLeftStudX + p));
|
const extra = (isDoor(op) ? 2 : 1) * sw;
|
||||||
} else {
|
return [startX + op.x - extra + sw / 2, startX + op.x + op.w + extra - sw / 2]; // hartlijnen buitenste stijlen
|
||||||
// Hoekstijlen van de wand horen er, met of zonder opening, altijd bij.
|
}).sort((p, q) => p[0] - q[0]).forEach(r => {
|
||||||
fullStuds.push(...cornerStuds);
|
const last = cols[cols.length - 1];
|
||||||
|
if (last && r[0] < last[1] + sw) last[1] = Math.max(last[1], r[1]);
|
||||||
// Randen van de "vrije zones" tussen wand en openingen, per paar verwerkt
|
else cols.push(r.slice());
|
||||||
const edges = [wallLeftStudX];
|
});
|
||||||
|
const edges = [wallLeftStudX];
|
||||||
ops.forEach(op => {
|
cols.forEach(c => edges.push(c[0], c[1]));
|
||||||
const opStartX = startX + op.x;
|
edges.push(wallRightStudX);
|
||||||
const kingLeftX = opStartX - CONFIG.studWidth / 2;
|
for (let i = 0; i < edges.length; i += 2) {
|
||||||
const kingRightX = opStartX + op.w + CONFIG.studWidth / 2;
|
const a = edges[i], b = edges[i + 1];
|
||||||
|
if (b - a > sw) calculateSegmentPositions(b - a, CONFIG.minSpacing, CONFIG.maxSpacing).forEach(p => cands.push({ x: a + p, kind: 'regel' }));
|
||||||
fullStuds.push(kingLeftX, kingRightX);
|
|
||||||
edges.push(kingLeftX, kingRightX);
|
|
||||||
|
|
||||||
// Kreupele stijlen binnen de opening-breedte zelf
|
|
||||||
calculateSegmentPositions(op.w, CONFIG.minSpacing, CONFIG.maxSpacing)
|
|
||||||
.forEach(p => jackStuds.push({ x: opStartX + p, opening: op }));
|
|
||||||
});
|
|
||||||
|
|
||||||
edges.push(wallRightStudX);
|
|
||||||
|
|
||||||
for (let i = 0; i < edges.length; i += 2) {
|
|
||||||
const a = edges[i];
|
|
||||||
const b = edges[i + 1];
|
|
||||||
const span = (b - CONFIG.studWidth / 2) - (a + CONFIG.studWidth / 2);
|
|
||||||
if (span > 0) {
|
|
||||||
calculateSegmentPositions(span + CONFIG.studWidth, CONFIG.minSpacing, CONFIG.maxSpacing)
|
|
||||||
.forEach(p => fullStuds.push(a + p));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ops.forEach(op => calculateSegmentPositions(op.w, CONFIG.minSpacing, CONFIG.maxSpacing)
|
||||||
|
.forEach(p => cands.push({ x: startX + op.x + p, kind: 'kreupel', owner: op })));
|
||||||
|
|
||||||
fullStuds = [...new Set(fullStuds.map(x => Math.round(x)))].sort((a, b) => a - b);
|
// Knippen op de openingen.
|
||||||
jackStuds.sort((a, b) => a.x - b.x);
|
const PRIO = { hoek: 0, koning: 1, dubbel: 2, regel: 3, kreupel: 4 };
|
||||||
|
const roofMin = (x) => Math.min(...top.underPts(x - sw / 2, x + sw / 2).map(p => p.y));
|
||||||
|
const segs = [];
|
||||||
|
cands.forEach(c => {
|
||||||
|
const x = Math.round(c.x);
|
||||||
|
const hit = bands.filter(bd => Math.min(bd.x1, x + sw / 2) - Math.max(bd.x0, x - sw / 2) > 0.5).sort((p, q) => p.y0 - q.y0);
|
||||||
|
let cur = CONFIG.plateThick, below = null;
|
||||||
|
const push = (y0, y1, above) => {
|
||||||
|
if (c.kind === 'kreupel') {
|
||||||
|
// Alleen direct boven of onder de eigen opening; onder de opening
|
||||||
|
// niet als hij op een andere opening uitkomt (dat stuk vult de
|
||||||
|
// kreupele stijl van de onderste opening al).
|
||||||
|
const onTop = below && below.op === c.owner, underneath = above && above.op === c.owner;
|
||||||
|
if (!onTop && !(underneath && !below)) return;
|
||||||
|
}
|
||||||
|
segs.push({ x, y0, y1, kind: c.kind });
|
||||||
|
};
|
||||||
|
hit.forEach(bd => {
|
||||||
|
if (bd.y0 > cur + 1) push(cur, bd.y0, bd);
|
||||||
|
if (bd.y1 > cur) { cur = bd.y1; below = bd; }
|
||||||
|
});
|
||||||
|
if (roofMin(x) > cur + 1) push(cur, null, null);
|
||||||
|
});
|
||||||
|
// Dubbelingen (bijv. koningsstijl op de hoekstijl) weg: hoogste prioriteit wint.
|
||||||
|
const studs = [];
|
||||||
|
segs.sort((p, q) => PRIO[p.kind] - PRIO[q.kind]).forEach(sg => {
|
||||||
|
const top1 = sg.y1 === null ? Infinity : sg.y1;
|
||||||
|
const clash = studs.some(o => Math.abs(o.x - sg.x) < sw - 0.5 &&
|
||||||
|
Math.min(top1, o.y1 === null ? Infinity : o.y1) - Math.max(sg.y0, o.y0) > 1);
|
||||||
|
if (!clash) studs.push(sg);
|
||||||
|
});
|
||||||
|
studs.sort((p, q) => p.x - q.x || p.y0 - q.y0);
|
||||||
|
// Volle stijlen (onderregel tot daklijn), o.a. voor de maatvoering.
|
||||||
|
const fullStuds = [...new Set(studs.filter(sg => sg.y1 === null && sg.y0 <= CONFIG.plateThick).map(sg => sg.x))].sort((a, b) => a - b);
|
||||||
|
|
||||||
return { fullStuds, jackStuds, slope, angleDeg };
|
return { studs, fullStuds, slope, angleDeg };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uitsparing in de beplating/het vochtdoek per opening (mm; x t.o.v. het
|
// Uitsparing in de beplating/het vochtdoek per opening (mm; x t.o.v. het
|
||||||
@@ -315,8 +464,7 @@ window.WallModel = (() => {
|
|||||||
// Gebruikt door build3D (fill) én de 2D-tekening (naad-/baanlijnen).
|
// Gebruikt door build3D (fill) én de 2D-tekening (naad-/baanlijnen).
|
||||||
function sheathingHoles(CONFIG) {
|
function sheathingHoles(CONFIG) {
|
||||||
const halfW = CONFIG.wallWidth / 2;
|
const halfW = CONFIG.wallWidth / 2;
|
||||||
const slope = (CONFIG.wallHeightRight - CONFIG.wallHeightLeft) / CONFIG.wallWidth;
|
const topYAt = topLine(CONFIG).at;
|
||||||
const topYAt = (x) => CONFIG.wallHeightLeft + (x + halfW) * slope;
|
|
||||||
const roofSafetyMargin = 10;
|
const roofSafetyMargin = 10;
|
||||||
return CONFIG.openings.map(op => {
|
return CONFIG.openings.map(op => {
|
||||||
const hx0 = -halfW + op.x - CONFIG.studWidth;
|
const hx0 = -halfW + op.x - CONFIG.studWidth;
|
||||||
@@ -332,16 +480,11 @@ window.WallModel = (() => {
|
|||||||
// liggen: binnen de wand-omtrek (incl. schuine top) en buiten de
|
// liggen: binnen de wand-omtrek (incl. schuine top) en buiten de
|
||||||
// uitsparingen. Geeft [[x0, x1], ...] terug (mm, t.o.v. het wandmidden).
|
// uitsparingen. Geeft [[x0, x1], ...] terug (mm, t.o.v. het wandmidden).
|
||||||
function skinSegmentsAtY(CONFIG, y) {
|
function skinSegmentsAtY(CONFIG, y) {
|
||||||
const halfW = CONFIG.wallWidth / 2;
|
if (y <= 0) return [];
|
||||||
const hL = CONFIG.wallHeightLeft, hR = CONFIG.wallHeightRight;
|
|
||||||
if (y <= 0 || y >= Math.max(hL, hR)) return [];
|
|
||||||
// Deel van de breedte waar de daklijn boven y ligt.
|
// Deel van de breedte waar de daklijn boven y ligt.
|
||||||
let x0 = -halfW, x1 = halfW;
|
const range = topLine(CONFIG).rangeAbove(y);
|
||||||
if (hL !== hR) {
|
if (!range) return [];
|
||||||
const xAtY = -halfW + (y - hL) / (hR - hL) * CONFIG.wallWidth;
|
let segments = [range];
|
||||||
if (hR > hL) x0 = Math.max(x0, xAtY); else x1 = Math.min(x1, xAtY);
|
|
||||||
} else if (y >= hL) return [];
|
|
||||||
let segments = [[x0, x1]];
|
|
||||||
sheathingHoles(CONFIG).filter(h => y > h.hy0 && y < h.hy1).forEach(h => {
|
sheathingHoles(CONFIG).filter(h => y > h.hy0 && y < h.hy1).forEach(h => {
|
||||||
segments = segments.flatMap(([a, b]) => {
|
segments = segments.flatMap(([a, b]) => {
|
||||||
if (h.hx1 <= a || h.hx0 >= b) return [[a, b]];
|
if (h.hx1 <= a || h.hx0 >= b) return [[a, b]];
|
||||||
@@ -365,22 +508,31 @@ window.WallModel = (() => {
|
|||||||
// linkerrand van dit stuk. Het verticale latten-raster volgt dan de
|
// linkerrand van dit stuk. Het verticale latten-raster volgt dan de
|
||||||
// hele gevel i.p.v. per stuk opnieuw te beginnen; een eindlat komt
|
// hele gevel i.p.v. per stuk opnieuw te beginnen; een eindlat komt
|
||||||
// alleen aan het einde van de gevel.
|
// alleen aan het einde van de gevel.
|
||||||
function computeBattens(CONFIG, cladding, trasraam, skinExt, facade) {
|
// facade.gridY0 (optioneel, mm, wand-coördinaten): hoogte waarop het
|
||||||
const y0 = skinStartY(trasraam); // boven het trasraam
|
// horizontale latten-raster van de hele gevel begint. Een wandstuk dat
|
||||||
|
// hoger begint (opbouw, blootliggende naadwand) volgt zo hetzelfde raster.
|
||||||
|
// skinFrom (optioneel, mm): buitenafwerking pas vanaf deze hoogte (het
|
||||||
|
// blootliggende deel van een naadwand boven het dak van de andere unit).
|
||||||
|
function computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom) {
|
||||||
|
const y0 = Math.max(skinStartY(trasraam), skinFrom || 0); // boven het trasraam
|
||||||
const ext = cornerExt(skinExt, 2 * BATTEN.thick); // over de hoekstijlen van de doorlopende wand
|
const ext = cornerExt(skinExt, 2 * BATTEN.thick); // over de hoekstijlen van de doorlopende wand
|
||||||
const xL = -CONFIG.wallWidth / 2 - ext.left, xR = CONFIG.wallWidth / 2 + ext.right;
|
const xL = -CONFIG.wallWidth / 2 - ext.left, xR = CONFIG.wallWidth / 2 + ext.right;
|
||||||
const gridX0 = facade ? -CONFIG.wallWidth / 2 - facade.u0 - (facade.extStart ? 2 * BATTEN.thick : 0) : xL;
|
const gridX0 = facade ? -CONFIG.wallWidth / 2 - facade.u0 - (facade.extStart ? 2 * BATTEN.thick : 0) : xL;
|
||||||
const isEnd = facade ? facade.isEnd : true;
|
const isEnd = facade ? facade.isEnd : true;
|
||||||
const halfW = CONFIG.wallWidth / 2;
|
const halfW = CONFIG.wallWidth / 2;
|
||||||
const hL = CONFIG.wallHeightLeft, hR = CONFIG.wallHeightRight;
|
const tl = topLine(CONFIG), topYAt = tl.at;
|
||||||
const topYAt = (x) => hL + (x + halfW) * (hR - hL) / CONFIG.wallWidth;
|
|
||||||
const holes = sheathingHoles(CONFIG);
|
const holes = sheathingHoles(CONFIG);
|
||||||
const w = BATTEN.width, sp = BATTEN.spacing;
|
const w = BATTEN.width, sp = BATTEN.spacing;
|
||||||
const top = (cladding === 'verticaal') ? 'horizontaal' : 'verticaal';
|
const top = (cladding === 'verticaal') ? 'horizontaal' : 'verticaal';
|
||||||
|
|
||||||
// Horizontale latten: vanaf de onderkant elke 400 mm (hartmaat).
|
// Horizontale latten: vanaf de onderkant elke 400 mm (hartmaat).
|
||||||
const horizontaal = [];
|
const horizontaal = [];
|
||||||
for (let y = y0 + w / 2; y < Math.max(hL, hR) - w / 2; y += sp) {
|
let yStart = y0 + w / 2;
|
||||||
|
if (facade && facade.gridY0 !== undefined) {
|
||||||
|
const g = facade.gridY0 + w / 2;
|
||||||
|
yStart = g + Math.max(0, Math.ceil((yStart - g) / sp - 1e-9)) * sp;
|
||||||
|
}
|
||||||
|
for (let y = yStart; y < tl.maxY - w / 2; y += sp) {
|
||||||
skinSegmentsAtY(CONFIG, y).forEach(([x0, x1]) => {
|
skinSegmentsAtY(CONFIG, y).forEach(([x0, x1]) => {
|
||||||
if (x0 <= -halfW + 0.5) x0 = xL;
|
if (x0 <= -halfW + 0.5) x0 = xL;
|
||||||
if (x1 >= halfW - 0.5) x1 = xR;
|
if (x1 >= halfW - 0.5) x1 = xR;
|
||||||
@@ -475,24 +627,19 @@ window.WallModel = (() => {
|
|||||||
? new THREE.MeshStandardMaterial({ color: 0xffffff, map: doekMap, roughness: 0.7, side: THREE.DoubleSide })
|
? new THREE.MeshStandardMaterial({ color: 0xffffff, map: doekMap, roughness: 0.7, side: THREE.DoubleSide })
|
||||||
: new THREE.MeshStandardMaterial({ color: DOEK_COLOR, roughness: 0.7, side: THREE.DoubleSide }),
|
: new THREE.MeshStandardMaterial({ color: DOEK_COLOR, roughness: 0.7, side: THREE.DoubleSide }),
|
||||||
cementplaatMaterial: new THREE.MeshStandardMaterial({ color: 0x808285, roughness: 0.8, side: THREE.DoubleSide }),
|
cementplaatMaterial: new THREE.MeshStandardMaterial({ color: 0x808285, roughness: 0.8, side: THREE.DoubleSide }),
|
||||||
// Zelfde uitstraling als de vloer-isolatie (FloorModel).
|
battenMaterial: new THREE.MeshStandardMaterial({ color: 0xd9b77e, roughness: 0.7 })
|
||||||
battenMaterial: new THREE.MeshStandardMaterial({ color: 0xd9b77e, roughness: 0.7 }),
|
|
||||||
isolatieMaterial: new THREE.MeshStandardMaterial({
|
|
||||||
color: 0xffe08c, roughness: 0.9, transparent: true, opacity: 0.5, side: THREE.DoubleSide, depthWrite: false
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bouwt de volledige 3D-wand (in meters, lokaal assenstelsel: X = breedte
|
// Bouwt de volledige 3D-wand (in meters, lokaal assenstelsel: X = breedte
|
||||||
// gecentreerd, Y = hoogte vanaf onderkant onderregel, +Z = buitenzijde/zuid)
|
// gecentreerd, Y = hoogte vanaf onderkant onderregel, +Z = buitenzijde/zuid)
|
||||||
// en geeft daarnaast de volume-boekhouding terug voor de specificaties.
|
// en geeft daarnaast de volume-boekhouding terug voor de specificaties.
|
||||||
// opts: { materials, buitenMateriaal, cladding, trasraam, spaanplaatVisible, buitenVisible, isolatieVisible }
|
// opts: { materials, buitenMateriaal, cladding, trasraam, spaanplaatVisible, buitenVisible }
|
||||||
// trasraam: { bottom, top } in mm (wand-coördinaten, zie trasraamFor) of null.
|
// trasraam: { bottom, top } in mm (wand-coördinaten, zie trasraamFor) of null.
|
||||||
// skinExt: { left, right } in mm (zie cornerExt) of null.
|
// skinExt: { left, right } in mm (zie cornerExt) of null.
|
||||||
// facade: { u0, extStart, isEnd } (zie computeBattens) of null.
|
// facade: { u0, extStart, isEnd } (zie computeBattens) of null.
|
||||||
function build3D(CONFIG, calcData, opts) {
|
function build3D(CONFIG, calcData, opts) {
|
||||||
const { woodMaterial, spaanplaatMaterial, doekMaterial, cementplaatMaterial, isolatieMaterial, battenMaterial } = opts.materials;
|
const { woodMaterial, spaanplaatMaterial, doekMaterial, cementplaatMaterial, battenMaterial } = opts.materials;
|
||||||
const isolatieVisible = opts.isolatieVisible !== false;
|
|
||||||
const buitenMateriaal = opts.buitenMateriaal || 'vochtdoek';
|
const buitenMateriaal = opts.buitenMateriaal || 'vochtdoek';
|
||||||
const spaanplaatVisible = opts.spaanplaatVisible !== false;
|
const spaanplaatVisible = opts.spaanplaatVisible !== false;
|
||||||
const buitenVisible = opts.buitenVisible !== false;
|
const buitenVisible = opts.buitenVisible !== false;
|
||||||
@@ -500,13 +647,20 @@ window.WallModel = (() => {
|
|||||||
const trasraam = buitenMateriaal === 'vochtdoek' ? (opts.trasraam || null) : null;
|
const trasraam = buitenMateriaal === 'vochtdoek' ? (opts.trasraam || null) : null;
|
||||||
const skinExt = opts.skinExt || null;
|
const skinExt = opts.skinExt || null;
|
||||||
const facade = opts.facade || null;
|
const facade = opts.facade || null;
|
||||||
|
const skinFrom = opts.skinFrom > 0 ? opts.skinFrom : 0; // buitenafwerking vanaf (mm), zie computeBattens
|
||||||
|
// spaanplaatBelow: onder skinFrom aan de buitenzijde spaanplaat (naadwand
|
||||||
|
// geheel in één unit: onder het dak van de andere unit is het een binnenwand).
|
||||||
|
const spaanplaatBelow = !!opts.spaanplaatBelow && skinFrom > 0 && buitenMateriaal === 'vochtdoek';
|
||||||
// Watermerk-UV's (doek) in gevel-coördinaten, zodat het patroon over
|
// Watermerk-UV's (doek) in gevel-coördinaten, zodat het patroon over
|
||||||
// naden/wandstukken doorloopt (UV = vormcoördinaat in m + verschuiving).
|
// naden/wandstukken doorloopt (UV = vormcoördinaat in m + verschuiving).
|
||||||
const uvShiftM = facade ? (CONFIG.wallWidth / 2 + facade.u0) * 0.001 : 0;
|
const uvShiftM = facade ? (CONFIG.wallWidth / 2 + facade.u0) * 0.001 : 0;
|
||||||
|
// Verticaal: een hoger beginnend stuk (facade.yOff, mm boven het
|
||||||
|
// loopvlak van de gevel) schuift het watermerk mee omhoog.
|
||||||
|
const uvShiftYM = facade && facade.yOff ? facade.yOff * 0.001 : 0;
|
||||||
const shiftUV = (geo) => {
|
const shiftUV = (geo) => {
|
||||||
if (!uvShiftM) return geo;
|
if (!uvShiftM && !uvShiftYM) return geo;
|
||||||
const uv = geo.attributes.uv;
|
const uv = geo.attributes.uv;
|
||||||
for (let k = 0; k < uv.count; k++) uv.setX(k, uv.getX(k) + uvShiftM);
|
for (let k = 0; k < uv.count; k++) { uv.setX(k, uv.getX(k) + uvShiftM); uv.setY(k, uv.getY(k) + uvShiftYM); }
|
||||||
return geo;
|
return geo;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -552,10 +706,11 @@ window.WallModel = (() => {
|
|||||||
const hRightM = CONFIG.wallHeightRight * toM;
|
const hRightM = CONFIG.wallHeightRight * toM;
|
||||||
const halfW = wWidthM / 2;
|
const halfW = wWidthM / 2;
|
||||||
|
|
||||||
const slope = (hRightM - hLeftM) / wWidthM;
|
const tl = topLine(CONFIG, toM);
|
||||||
const vertOffset = pThickM * Math.sqrt(1 + slope * slope);
|
|
||||||
const startXM = -halfW;
|
const startXM = -halfW;
|
||||||
const topYAt = (x) => hLeftM + (x + halfW) * slope;
|
const topYAt = tl.at;
|
||||||
|
// Onderkant bovenregel van xLeft naar xRight (incl. verstek bij de nok).
|
||||||
|
const underPts = tl.underPts;
|
||||||
|
|
||||||
// Openingen naar meter-coordinaten. LET OP: xMin/xMax hier zijn al naar
|
// Openingen naar meter-coordinaten. LET OP: xMin/xMax hier zijn al naar
|
||||||
// wereld-coordinaten verschoven (dezelfde as als de rest van de 3D-opbouw,
|
// wereld-coordinaten verschoven (dezelfde as als de rest van de 3D-opbouw,
|
||||||
@@ -604,70 +759,27 @@ window.WallModel = (() => {
|
|||||||
|
|
||||||
// B. TOPBALK WAND (ongewijzigd door openingen) — dit is zelf ook een
|
// B. TOPBALK WAND (ongewijzigd door openingen) — dit is zelf ook een
|
||||||
// "horizontale balk" (liggend, ook al loopt hij mee met de dakhelling).
|
// "horizontale balk" (liggend, ook al loopt hij mee met de dakhelling).
|
||||||
const topPlateShape = new THREE.Shape();
|
// Met nok: twee bovenregels die in verstek op de nok samenkomen.
|
||||||
topPlateShape.moveTo(-halfW, hLeftM);
|
topPlateOutlines(tl).forEach(pts => {
|
||||||
topPlateShape.lineTo(halfW, hRightM);
|
wallGroup3D.add(createExtrudedBeam(new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y))), pDepthM, -pDepthM / 2));
|
||||||
topPlateShape.lineTo(halfW, hRightM - vertOffset);
|
horizBeamCount++;
|
||||||
topPlateShape.lineTo(-halfW, hLeftM - vertOffset);
|
|
||||||
topPlateShape.closePath();
|
|
||||||
wallGroup3D.add(createExtrudedBeam(topPlateShape, pDepthM, -pDepthM / 2));
|
|
||||||
horizBeamCount++;
|
|
||||||
|
|
||||||
// C. VOLLEDIGE VERTICALE STIJLEN (incl. de "koningsstijlen" naast elke opening)
|
|
||||||
calcData.fullStuds.forEach((xPosMM) => {
|
|
||||||
const xM = xPosMM * toM;
|
|
||||||
const xLeft = xM - (sWidthM / 2);
|
|
||||||
const xRight = xM + (sWidthM / 2);
|
|
||||||
|
|
||||||
const yTopLeft = (hLeftM + (xLeft + halfW) * slope) - vertOffset;
|
|
||||||
const yTopRight = (hLeftM + (xRight + halfW) * slope) - vertOffset;
|
|
||||||
|
|
||||||
const studShape = new THREE.Shape();
|
|
||||||
studShape.moveTo(xLeft, pThickM);
|
|
||||||
studShape.lineTo(xRight, pThickM);
|
|
||||||
studShape.lineTo(xRight, yTopRight);
|
|
||||||
studShape.lineTo(xLeft, yTopLeft);
|
|
||||||
studShape.closePath();
|
|
||||||
wallGroup3D.add(createExtrudedBeam(studShape, sDepthM, -sDepthM / 2));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// D. KREUPELE STIJLEN & DORPELS, per opening
|
// C. VERTICALE STIJLEN (vol, koning, dubbel bij deuren, kreupel): elk
|
||||||
|
// stuk van y0 tot y1, of tot onder de daklijn (y1 null), zie computeFrame.
|
||||||
|
calcData.studs.forEach(st => {
|
||||||
|
const xM = st.x * toM;
|
||||||
|
const xLeft = xM - (sWidthM / 2);
|
||||||
|
const xRight = xM + (sWidthM / 2);
|
||||||
|
const y0 = st.y0 * toM;
|
||||||
|
const pts = st.y1 === null
|
||||||
|
? [{ x: xLeft, y: y0 }, { x: xRight, y: y0 }, ...underPts(xLeft, xRight).reverse()]
|
||||||
|
: [{ x: xLeft, y: y0 }, { x: xRight, y: y0 }, { x: xRight, y: st.y1 * toM }, { x: xLeft, y: st.y1 * toM }];
|
||||||
|
wallGroup3D.add(createExtrudedBeam(new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y))), sDepthM, -sDepthM / 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
// D. DORPELS, per opening
|
||||||
opsM.forEach(op => {
|
opsM.forEach(op => {
|
||||||
calcData.jackStuds
|
|
||||||
.filter(j => j.opening.id === op.id)
|
|
||||||
.forEach(j => {
|
|
||||||
const xM = j.x * toM;
|
|
||||||
const xLeft = xM - (sWidthM / 2);
|
|
||||||
const xRight = xM + (sWidthM / 2);
|
|
||||||
|
|
||||||
const yTopLeft = (hLeftM + (xLeft + halfW) * slope) - vertOffset;
|
|
||||||
const yTopRight = (hLeftM + (xRight + halfW) * slope) - vertOffset;
|
|
||||||
|
|
||||||
// Onderste kreupele stijl mag niet door de onderdorpel heen lopen
|
|
||||||
// (die neemt zelf de band net onder op.yMin in beslag).
|
|
||||||
const sillBottomM = op.yMin - sWidthM;
|
|
||||||
if (sillBottomM > pThickM) {
|
|
||||||
const studBotShape = new THREE.Shape();
|
|
||||||
studBotShape.moveTo(xLeft, pThickM);
|
|
||||||
studBotShape.lineTo(xRight, pThickM);
|
|
||||||
studBotShape.lineTo(xRight, sillBottomM);
|
|
||||||
studBotShape.lineTo(xLeft, sillBottomM);
|
|
||||||
studBotShape.closePath();
|
|
||||||
wallGroup3D.add(createExtrudedBeam(studBotShape, sDepthM, -sDepthM / 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualOpTop = op.yMax + sWidthM;
|
|
||||||
if (actualOpTop < yTopLeft || actualOpTop < yTopRight) {
|
|
||||||
const studTopShape = new THREE.Shape();
|
|
||||||
studTopShape.moveTo(xLeft, actualOpTop);
|
|
||||||
studTopShape.lineTo(xRight, actualOpTop);
|
|
||||||
studTopShape.lineTo(xRight, yTopRight);
|
|
||||||
studTopShape.lineTo(xLeft, yTopLeft);
|
|
||||||
studTopShape.closePath();
|
|
||||||
wallGroup3D.add(createExtrudedBeam(studTopShape, sDepthM, -sDepthM / 2));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bovendorpel / linteel (horizontale balk)
|
// Bovendorpel / linteel (horizontale balk)
|
||||||
const opTopShape = new THREE.Shape();
|
const opTopShape = new THREE.Shape();
|
||||||
opTopShape.moveTo(op.xMin, op.yMax);
|
opTopShape.moveTo(op.xMin, op.yMax);
|
||||||
@@ -692,48 +804,7 @@ window.WallModel = (() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// D2. ISOLATIE — vult elk vak tussen twee opeenvolgende stijlen (vol of
|
// (Isolatie wordt niet getekend; de spouwinhoud zit in computeSpecs.)
|
||||||
// kreupel) over de stijldiepte, van bovenkant onderregel tot onderkant
|
|
||||||
// bovenregel (volgt de dakhelling). Binnen de breedte van een opening
|
|
||||||
// alleen onder de onderdorpel en boven de bovendorpel. Puur visueel,
|
|
||||||
// zelfde transparante laag als bij de vloer.
|
|
||||||
const members = calcData.fullStuds.map(x => x * toM)
|
|
||||||
.concat(calcData.jackStuds.map(j => j.x * toM))
|
|
||||||
.map(x => [x - sWidthM / 2, x + sWidthM / 2])
|
|
||||||
.sort((a, b) => a[0] - b[0]);
|
|
||||||
const merged = [];
|
|
||||||
members.forEach(m => {
|
|
||||||
const last = merged[merged.length - 1];
|
|
||||||
if (last && m[0] <= last[1] + 1e-9) last[1] = Math.max(last[1], m[1]);
|
|
||||||
else merged.push([m[0], m[1]]);
|
|
||||||
});
|
|
||||||
function addIsolatieBlock(x0, x1, yBottom, yTopLeft, yTopRight) {
|
|
||||||
if (x1 - x0 <= 1e-6 || Math.min(yTopLeft, yTopRight) - yBottom <= 1e-6) return;
|
|
||||||
const shape = new THREE.Shape();
|
|
||||||
shape.moveTo(x0, yBottom);
|
|
||||||
shape.lineTo(x1, yBottom);
|
|
||||||
shape.lineTo(x1, yTopRight);
|
|
||||||
shape.lineTo(x0, yTopLeft);
|
|
||||||
shape.closePath();
|
|
||||||
const mesh = new THREE.Mesh(new THREE.ExtrudeGeometry(shape, { steps: 1, depth: sDepthM, bevelEnabled: false }), isolatieMaterial);
|
|
||||||
mesh.position.set(0, 0, -sDepthM / 2);
|
|
||||||
mesh.visible = isolatieVisible;
|
|
||||||
wallGroup3D.add(mesh);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < merged.length - 1; i++) {
|
|
||||||
const x0 = merged[i][1], x1 = merged[i + 1][0];
|
|
||||||
const yTopL = topYAt(x0) - vertOffset, yTopR = topYAt(x1) - vertOffset;
|
|
||||||
const mid = (x0 + x1) / 2;
|
|
||||||
const op = opsM.find(o => mid > o.xMin && mid < o.xMax);
|
|
||||||
if (!op) {
|
|
||||||
addIsolatieBlock(x0, x1, pThickM, yTopL, yTopR);
|
|
||||||
} else {
|
|
||||||
const sillBottom = op.yMin - sWidthM;
|
|
||||||
if (sillBottom > pThickM) addIsolatieBlock(x0, x1, pThickM, sillBottom, sillBottom);
|
|
||||||
const headerTop = op.yMax + sWidthM;
|
|
||||||
addIsolatieBlock(x0, x1, headerTop, Math.max(headerTop, yTopL), Math.max(headerTop, yTopR));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// E. VERTICALE PLATEN — gedeelde opbouw voor spaanplaat (binnenzijde,
|
// E. VERTICALE PLATEN — gedeelde opbouw voor spaanplaat (binnenzijde,
|
||||||
// altijd) EN cement-gebonden platen (buitenzijde, als die gekozen is):
|
// altijd) EN cement-gebonden platen (buitenzijde, als die gekozen is):
|
||||||
@@ -823,7 +894,8 @@ window.WallModel = (() => {
|
|||||||
// over een plaatnaad wordt een inkeping in de plaatrand, zodat er
|
// over een plaatnaad wordt een inkeping in de plaatrand, zodat er
|
||||||
// geen reepje plaat (met randlijnen) door de opening blijft lopen.
|
// geen reepje plaat (met randlijnen) door de opening blijft lopen.
|
||||||
const pieces = columnMinusRects(ex0, ex1, yB, skinTopAt,
|
const pieces = columnMinusRects(ex0, ex1, yB, skinTopAt,
|
||||||
holeExtents.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })), range ? { yCap } : undefined);
|
holeExtents.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })),
|
||||||
|
{ yCap: range ? yCap : undefined, extraXs: range ? tl.kinks.concat(tl.crossings(yCap)) : tl.kinks });
|
||||||
let boardShapes;
|
let boardShapes;
|
||||||
if (pieces) {
|
if (pieces) {
|
||||||
boardShapes = pieces.map(piece => {
|
boardShapes = pieces.map(piece => {
|
||||||
@@ -838,6 +910,7 @@ window.WallModel = (() => {
|
|||||||
boardShape.moveTo(ex0, yB);
|
boardShape.moveTo(ex0, yB);
|
||||||
boardShape.lineTo(ex1, yB);
|
boardShape.lineTo(ex1, yB);
|
||||||
boardShape.lineTo(ex1, skinTopAt(ex1));
|
boardShape.lineTo(ex1, skinTopAt(ex1));
|
||||||
|
tl.kinks.filter(x => x > ex0 && x < ex1).forEach(x => boardShape.lineTo(x, skinTopAt(x)));
|
||||||
boardShape.lineTo(ex0, skinTopAt(ex0));
|
boardShape.lineTo(ex0, skinTopAt(ex0));
|
||||||
boardShape.closePath();
|
boardShape.closePath();
|
||||||
holeExtents.forEach(h => {
|
holeExtents.forEach(h => {
|
||||||
@@ -907,11 +980,18 @@ window.WallModel = (() => {
|
|||||||
|
|
||||||
// Buitenzijde (zuid): cement-gebonden platen (zelfde plaatlogica als
|
// Buitenzijde (zuid): cement-gebonden platen (zelfde plaatlogica als
|
||||||
// hierboven) OF vochtdoek (horizontale banen, zie onder) — nooit beide.
|
// hierboven) OF vochtdoek (horizontale banen, zie onder) — nooit beide.
|
||||||
|
if (spaanplaatBelow) {
|
||||||
|
const spaanOnder = buildVerticalBoardSkin(spaanplaatMaterial, 'front', buitenVisible, { y0: 0, y1: skinFrom * 0.001 });
|
||||||
|
spaanplaatBuitenAreaM2 = spaanOnder.areaM2;
|
||||||
|
spaanplaatBuitenVolumeM3 = spaanOnder.volumeM3;
|
||||||
|
}
|
||||||
if (buitenMateriaal === 'spaanplaat') {
|
if (buitenMateriaal === 'spaanplaat') {
|
||||||
// Binnenwand: aan beide zijden spaanplaat.
|
// Binnenwand: aan beide zijden spaanplaat.
|
||||||
const spaanBuiten = buildVerticalBoardSkin(spaanplaatMaterial, 'front', buitenVisible);
|
const spaanBuiten = buildVerticalBoardSkin(spaanplaatMaterial, 'front', buitenVisible);
|
||||||
spaanplaatBuitenAreaM2 = spaanBuiten.areaM2;
|
spaanplaatBuitenAreaM2 = spaanBuiten.areaM2;
|
||||||
spaanplaatBuitenVolumeM3 = spaanBuiten.volumeM3;
|
spaanplaatBuitenVolumeM3 = spaanBuiten.volumeM3;
|
||||||
|
} else if (buitenMateriaal === 'geen') {
|
||||||
|
// Naadzijde: geen beplating (het deel van de andere unit staat ertegen).
|
||||||
} else if (buitenMateriaal === 'cementplaat') {
|
} else if (buitenMateriaal === 'cementplaat') {
|
||||||
const cementResult = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible, null,
|
const cementResult = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible, null,
|
||||||
Object.assign(extM(0), { zOut: sThickM + 2 * skinGapM }));
|
Object.assign(extM(0), { zOut: sThickM + 2 * skinGapM }));
|
||||||
@@ -932,13 +1012,10 @@ window.WallModel = (() => {
|
|||||||
// (wat flikkering/z-fighting zou geven nu de platen niet meer
|
// (wat flikkering/z-fighting zou geven nu de platen niet meer
|
||||||
// transparant zijn).
|
// transparant zijn).
|
||||||
const doekZStepM = 0.0002; // 0.2 mm per baan, verwaarloosbaar t.o.v. de spouwdiepte
|
const doekZStepM = 0.0002; // 0.2 mm per baan, verwaarloosbaar t.o.v. de spouwdiepte
|
||||||
const wallOutlinePts = [
|
const wallOutlinePts = [{ x: -halfW, y: 0 }, { x: halfW, y: 0 }, ...tl.pts.slice().reverse()];
|
||||||
{ x: -halfW, y: 0 }, { x: halfW, y: 0 },
|
const maxRoofY = tl.maxY;
|
||||||
{ x: halfW, y: hRightM }, { x: -halfW, y: hLeftM }
|
|
||||||
];
|
|
||||||
const maxRoofY = Math.max(hLeftM, hRightM);
|
|
||||||
|
|
||||||
let rowY0 = 0;
|
let rowY0 = skinFrom * toM;
|
||||||
let rowIndex = 0;
|
let rowIndex = 0;
|
||||||
while (rowY0 < maxRoofY - 1e-9) {
|
while (rowY0 < maxRoofY - 1e-9) {
|
||||||
const rowY1 = rowY0 + doekHeightM;
|
const rowY1 = rowY0 + doekHeightM;
|
||||||
@@ -946,15 +1023,9 @@ window.WallModel = (() => {
|
|||||||
// Baan = strook [rowY0, min(rowY1, daklijn)] min uitsparingen
|
// Baan = strook [rowY0, min(rowY1, daklijn)] min uitsparingen
|
||||||
// (columnMinusRects, zelfde als de platen): een opening over de
|
// (columnMinusRects, zelfde als de platen): een opening over de
|
||||||
// baanrand wordt een inkeping, geen reepje doek door de opening.
|
// baanrand wordt een inkeping, geen reepje doek door de opening.
|
||||||
const roofAt = (x) => hLeftM + (x + halfW) * slope;
|
const roofAt = topYAt;
|
||||||
let rx0 = -halfW, rx1 = halfW;
|
const [rx0, rx1] = tl.rangeAbove(rowY0) || [0, 0];
|
||||||
const extraXs = [];
|
const extraXs = tl.crossings(rowY1).concat(tl.kinks);
|
||||||
if (Math.abs(slope) > 1e-12) {
|
|
||||||
const xAtRowBottom = -halfW + (rowY0 - hLeftM) / slope;
|
|
||||||
const xAtRowTop = -halfW + (rowY1 - hLeftM) / slope;
|
|
||||||
if (slope > 0) rx0 = Math.max(rx0, xAtRowBottom); else rx1 = Math.min(rx1, xAtRowBottom);
|
|
||||||
extraXs.push(xAtRowTop);
|
|
||||||
}
|
|
||||||
const pieces = rx1 - rx0 > edgeEps ? columnMinusRects(rx0, rx1, rowY0, (x) => Math.min(rowY1, roofAt(x)),
|
const pieces = rx1 - rx0 > edgeEps ? columnMinusRects(rx0, rx1, rowY0, (x) => Math.min(rowY1, roofAt(x)),
|
||||||
holeExtents.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })), { yCap: rowY1, extraXs }) : [];
|
holeExtents.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })), { yCap: rowY1, extraXs }) : [];
|
||||||
|
|
||||||
@@ -1040,7 +1111,7 @@ window.WallModel = (() => {
|
|||||||
|
|
||||||
// G. KRUISLAT op het vochtdoek (computeBattens): onderste laag direct op
|
// G. KRUISLAT op het vochtdoek (computeBattens): onderste laag direct op
|
||||||
// het doek, bovenste laag (haaks op de gevelbekleding) daaroverheen.
|
// het doek, bovenste laag (haaks op de gevelbekleding) daaroverheen.
|
||||||
const bat = computeBattens(CONFIG, cladding, trasraam, skinExt, facade);
|
const bat = computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom);
|
||||||
const bT = BATTEN.thick * toM, bW = BATTEN.width * toM;
|
const bT = BATTEN.thick * toM, bW = BATTEN.width * toM;
|
||||||
const zDoek = (maxDepth / 2) + skinGapM + sThickM + rowIndex * doekZStepM + skinGapM;
|
const zDoek = (maxDepth / 2) + skinGapM + sThickM + rowIndex * doekZStepM + skinGapM;
|
||||||
const addLayer = (orientation, z) => {
|
const addLayer = (orientation, z) => {
|
||||||
@@ -1082,10 +1153,7 @@ window.WallModel = (() => {
|
|||||||
// Omhullende van de wand (trapezium, incl. schuine top) als referentie
|
// Omhullende van de wand (trapezium, incl. schuine top) als referentie
|
||||||
// voor de leegte-berekening — spouwdiepte wordt gelijkgesteld aan de
|
// voor de leegte-berekening — spouwdiepte wordt gelijkgesteld aan de
|
||||||
// stijldiepte (vereenvoudiging, zie toelichting in de UI).
|
// stijldiepte (vereenvoudiging, zie toelichting in de UI).
|
||||||
const envelopePoints = [
|
const envelopePoints = [{ x: -halfW, y: 0 }, { x: halfW, y: 0 }, ...topLine(CONFIG, toM).pts.slice().reverse()];
|
||||||
{ x: -halfW, y: 0 }, { x: halfW, y: 0 },
|
|
||||||
{ x: halfW, y: hRightM }, { x: -halfW, y: hLeftM }
|
|
||||||
];
|
|
||||||
const envelopeAreaM2 = polygonArea(envelopePoints);
|
const envelopeAreaM2 = polygonArea(envelopePoints);
|
||||||
const envelopeVolumeM3 = envelopeAreaM2 * sDepthM;
|
const envelopeVolumeM3 = envelopeAreaM2 * sDepthM;
|
||||||
|
|
||||||
@@ -1104,6 +1172,8 @@ window.WallModel = (() => {
|
|||||||
buitenAreaM2 = stats.spaanplaatBuitenAreaM2;
|
buitenAreaM2 = stats.spaanplaatBuitenAreaM2;
|
||||||
buitenKg = stats.spaanplaatBuitenVolumeM3 * DENSITY_SPAANPLAAT_KGM3;
|
buitenKg = stats.spaanplaatBuitenVolumeM3 * DENSITY_SPAANPLAAT_KGM3;
|
||||||
buitenVolumeM3 = stats.spaanplaatBuitenVolumeM3;
|
buitenVolumeM3 = stats.spaanplaatBuitenVolumeM3;
|
||||||
|
} else if (buitenMateriaal === 'geen') {
|
||||||
|
buitenAreaM2 = 0; buitenKg = 0; buitenVolumeM3 = 0;
|
||||||
} else if (buitenMateriaal === 'cementplaat') {
|
} else if (buitenMateriaal === 'cementplaat') {
|
||||||
buitenAreaM2 = stats.cementplaatAreaM2;
|
buitenAreaM2 = stats.cementplaatAreaM2;
|
||||||
buitenKg = stats.cementplaatVolumeM3 * DENSITY_CEMENTPLAAT_KGM3;
|
buitenKg = stats.cementplaatVolumeM3 * DENSITY_CEMENTPLAAT_KGM3;
|
||||||
@@ -1126,9 +1196,7 @@ window.WallModel = (() => {
|
|||||||
function computeBOM(CONFIG, calcData) {
|
function computeBOM(CONFIG, calcData) {
|
||||||
const wallWidth = CONFIG.wallWidth, hLeft = CONFIG.wallHeightLeft, hRight = CONFIG.wallHeightRight;
|
const wallWidth = CONFIG.wallWidth, hLeft = CONFIG.wallHeightLeft, hRight = CONFIG.wallHeightRight;
|
||||||
const halfW = wallWidth / 2;
|
const halfW = wallWidth / 2;
|
||||||
const slope = (hRight - hLeft) / wallWidth;
|
const tl = topLine(CONFIG);
|
||||||
const topYAt = (xWorld) => hLeft + (xWorld + halfW) * slope;
|
|
||||||
const vertOffset = CONFIG.plateThick * Math.sqrt(1 + slope * slope);
|
|
||||||
const startX = -halfW;
|
const startX = -halfW;
|
||||||
|
|
||||||
const pieces = [];
|
const pieces = [];
|
||||||
@@ -1147,8 +1215,15 @@ window.WallModel = (() => {
|
|||||||
pieces.push({ length: halfW - cursor, width: CONFIG.plateThick, depth: CONFIG.plateDepth, kind: 'onderregel', x: (cursor + halfW) / 2, y: CONFIG.plateThick / 2 });
|
pieces.push({ length: halfW - cursor, width: CONFIG.plateThick, depth: CONFIG.plateDepth, kind: 'onderregel', x: (cursor + halfW) / 2, y: CONFIG.plateThick / 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const topLength = Math.sqrt(wallWidth * wallWidth + (hRight - hLeft) * (hRight - hLeft));
|
// Bovenregel(s): met nok twee stukken (verstek). outline = werkelijke
|
||||||
pieces.push({ length: topLength, width: CONFIG.plateThick, depth: CONFIG.plateDepth, kind: 'bovenregel', x: 0, y: (hLeft + hRight) / 2 });
|
// omtrek (PDF-tekening).
|
||||||
|
topPlateOutlines(tl).forEach((outline, i) => {
|
||||||
|
const sg = tl.segs[i];
|
||||||
|
pieces.push({
|
||||||
|
length: Math.hypot(sg.b.x - sg.a.x, sg.b.y - sg.a.y), width: CONFIG.plateThick, depth: CONFIG.plateDepth, kind: 'bovenregel',
|
||||||
|
x: (sg.a.x + sg.b.x) / 2, y: (sg.a.y + sg.b.y) / 2 - CONFIG.plateThick / 2, outline
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Verticale stukken krijgen, naast de bounding-box velden (length/y —
|
// Verticale stukken krijgen, naast de bounding-box velden (length/y —
|
||||||
// gebruikt voor de materiaallijst: de lengte van het rechte stuk hout
|
// gebruikt voor de materiaallijst: de lengte van het rechte stuk hout
|
||||||
@@ -1158,43 +1233,29 @@ window.WallModel = (() => {
|
|||||||
// i.p.v. als platte rechthoek tot het hoogste punt — anders komt alleen
|
// i.p.v. als platte rechthoek tot het hoogste punt — anders komt alleen
|
||||||
// de hoogste kant van de balk goed op de daklijn te staan, en blijft de
|
// de hoogste kant van de balk goed op de daklijn te staan, en blijft de
|
||||||
// laagste kant er (visueel) los van.
|
// laagste kant er (visueel) los van.
|
||||||
calcData.fullStuds.forEach(x => {
|
// (outline: ook de knik onder de nok, als de stijl daar staat.)
|
||||||
const xLeft = x - CONFIG.studWidth / 2, xRight = x + CONFIG.studWidth / 2;
|
calcData.studs.forEach(st => {
|
||||||
const yTopLeft = topYAt(xLeft) - vertOffset, yTopRight = topYAt(xRight) - vertOffset;
|
const x = st.x, xLeft = x - CONFIG.studWidth / 2, xRight = x + CONFIG.studWidth / 2;
|
||||||
const yBottom = CONFIG.plateThick;
|
const yBottom = st.y0;
|
||||||
const length = Math.max(yTopLeft, yTopRight) - yBottom;
|
const kind = st.kind === 'kreupel' ? 'kreupele stijl' : 'verticale balk';
|
||||||
|
if (st.y1 !== null) {
|
||||||
|
pieces.push({
|
||||||
|
length: st.y1 - yBottom, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind, x,
|
||||||
|
y: (yBottom + st.y1) / 2,
|
||||||
|
yBottomLeft: yBottom, yBottomRight: yBottom, yTopLeft: st.y1, yTopRight: st.y1
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const topPts = tl.underPts(xLeft, xRight);
|
||||||
|
const yTopLeft = topPts[0].y, yTopRight = topPts[topPts.length - 1].y;
|
||||||
pieces.push({
|
pieces.push({
|
||||||
length, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'verticale balk', x,
|
length: Math.max(...topPts.map(p => p.y)) - yBottom, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind, x,
|
||||||
y: (yBottom + yBottom + yTopLeft + yTopRight) / 4,
|
y: (yBottom + yBottom + yTopLeft + yTopRight) / 4,
|
||||||
yBottomLeft: yBottom, yBottomRight: yBottom, yTopLeft, yTopRight
|
yBottomLeft: yBottom, yBottomRight: yBottom, yTopLeft, yTopRight,
|
||||||
|
outline: [{ x: xLeft, y: yBottom }, { x: xRight, y: yBottom }, ...topPts.slice().reverse()]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
calcData.jackStuds.forEach(j => {
|
|
||||||
const op = j.opening;
|
|
||||||
const opYMin = op.y, opYMax = op.y + op.h;
|
|
||||||
const xLeft = j.x - CONFIG.studWidth / 2, xRight = j.x + CONFIG.studWidth / 2;
|
|
||||||
const yTopLeft = topYAt(xLeft) - vertOffset, yTopRight = topYAt(xRight) - vertOffset;
|
|
||||||
// De kreupele stijl mag niet door de onderdorpel heen lopen — die
|
|
||||||
// neemt zelf al de onderste stijldikte in vanaf opYMin in beslag.
|
|
||||||
const sillBottom = opYMin - CONFIG.studWidth;
|
|
||||||
if (sillBottom > CONFIG.plateThick) {
|
|
||||||
pieces.push({
|
|
||||||
length: sillBottom - CONFIG.plateThick, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'kreupele stijl', x: j.x,
|
|
||||||
y: (CONFIG.plateThick + sillBottom) / 2,
|
|
||||||
yBottomLeft: CONFIG.plateThick, yBottomRight: CONFIG.plateThick, yTopLeft: sillBottom, yTopRight: sillBottom
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const actualOpTop = opYMax + CONFIG.studWidth;
|
|
||||||
if (actualOpTop < yTopLeft || actualOpTop < yTopRight) {
|
|
||||||
pieces.push({
|
|
||||||
length: Math.max(yTopLeft, yTopRight) - actualOpTop, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'kreupele stijl', x: j.x,
|
|
||||||
y: (actualOpTop + actualOpTop + yTopLeft + yTopRight) / 4,
|
|
||||||
yBottomLeft: actualOpTop, yBottomRight: actualOpTop, yTopLeft, yTopRight
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
CONFIG.openings.forEach(op => {
|
CONFIG.openings.forEach(op => {
|
||||||
const opXWorld = startX + op.x;
|
const opXWorld = startX + op.x;
|
||||||
pieces.push({ length: op.w, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'bovendorpel', x: opXWorld + op.w / 2, y: op.y + op.h + CONFIG.studWidth / 2 });
|
pieces.push({ length: op.w, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'bovendorpel', x: opXWorld + op.w / 2, y: op.y + op.h + CONFIG.studWidth / 2 });
|
||||||
@@ -1285,8 +1346,8 @@ window.WallModel = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, trasraamFor, computeBattens, planPurchase, purchaseText,
|
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
|
||||||
sheathingHoles, skinSegmentsAtY,
|
sheathingHoles, skinSegmentsAtY, topLine, topPlateOutlines,
|
||||||
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
|
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
|
||||||
createMaterials, build3D, computeSpecs
|
createMaterials, build3D, computeSpecs
|
||||||
};
|
};
|
||||||
|
|||||||
+19
-24
@@ -67,7 +67,7 @@ window.WallPdf = (() => {
|
|||||||
|
|
||||||
// --- Materiaal binnen/buiten + isolatie ---
|
// --- Materiaal binnen/buiten + isolatie ---
|
||||||
let my = 240;
|
let my = 240;
|
||||||
const midH = Math.round((CONFIG.wallHeightLeft + CONFIG.wallHeightRight) / 2);
|
const midH = Math.round(WallModel.topLine(CONFIG).maxY);
|
||||||
doc.setFontSize(8.5);
|
doc.setFontSize(8.5);
|
||||||
doc.text('Materiaal binnen', tblX[0], my); my += 4.5;
|
doc.text('Materiaal binnen', tblX[0], my); my += 4.5;
|
||||||
doc.setFontSize(6.5);
|
doc.setFontSize(6.5);
|
||||||
@@ -80,7 +80,7 @@ window.WallPdf = (() => {
|
|||||||
doc.text('Isolatie: glaswol (spouwvullend)', tblX[0], my);
|
doc.text('Isolatie: glaswol (spouwvullend)', tblX[0], my);
|
||||||
if (buitenMateriaal === 'vochtdoek') {
|
if (buitenMateriaal === 'vochtdoek') {
|
||||||
// Kruislat op het vochtdoek (zelfde berekening als de 3D: WallModel.computeBattens).
|
// Kruislat op het vochtdoek (zelfde berekening als de 3D: WallModel.computeBattens).
|
||||||
const bat = WallModel.computeBattens(CONFIG, opts.cladding || 'horizontaal', opts.trasraam, opts.skinExt, opts.facade);
|
const bat = WallModel.computeBattens(CONFIG, opts.cladding || 'horizontaal', opts.trasraam, opts.skinExt, opts.facade, opts.skinFrom);
|
||||||
const B = WallModel.BATTEN;
|
const B = WallModel.BATTEN;
|
||||||
my += 4.5;
|
my += 4.5;
|
||||||
doc.text(`Latwerk ${B.thick}x${B.width}: ${bat.stock.count} st. \u00e0 ${B.length} mm, kruislat ${B.spacing} h.o.h.`, tblX[0], my);
|
doc.text(`Latwerk ${B.thick}x${B.width}: ${bat.stock.count} st. \u00e0 ${B.length} mm, kruislat ${B.spacing} h.o.h.`, tblX[0], my);
|
||||||
@@ -103,7 +103,8 @@ window.WallPdf = (() => {
|
|||||||
const availW = (drawX1 - drawX0) - marginSides * 2;
|
const availW = (drawX1 - drawX0) - marginSides * 2;
|
||||||
const availH = (drawY1 - drawY0) - marginTop - marginBottom;
|
const availH = (drawY1 - drawY0) - marginTop - marginBottom;
|
||||||
const wallWidth = CONFIG.wallWidth, hLeft = CONFIG.wallHeightLeft, hRight = CONFIG.wallHeightRight;
|
const wallWidth = CONFIG.wallWidth, hLeft = CONFIG.wallHeightLeft, hRight = CONFIG.wallHeightRight;
|
||||||
const maxH = Math.max(hLeft, hRight);
|
const tl = WallModel.topLine(CONFIG);
|
||||||
|
const maxH = tl.maxY;
|
||||||
const scale = Math.min(availW / wallWidth, availH / maxH);
|
const scale = Math.min(availW / wallWidth, availH / maxH);
|
||||||
|
|
||||||
const originX = drawX0 + marginSides;
|
const originX = drawX0 + marginSides;
|
||||||
@@ -115,15 +116,18 @@ window.WallPdf = (() => {
|
|||||||
doc.setLineWidth(0.25);
|
doc.setLineWidth(0.25);
|
||||||
doc.line(px(-wallWidth / 2), py(0), px(wallWidth / 2), py(0));
|
doc.line(px(-wallWidth / 2), py(0), px(wallWidth / 2), py(0));
|
||||||
doc.line(px(wallWidth / 2), py(0), px(wallWidth / 2), py(hRight));
|
doc.line(px(wallWidth / 2), py(0), px(wallWidth / 2), py(hRight));
|
||||||
doc.line(px(wallWidth / 2), py(hRight), px(-wallWidth / 2), py(hLeft));
|
for (let i = 0; i < tl.pts.length - 1; i++) doc.line(px(tl.pts[i].x), py(tl.pts[i].y), px(tl.pts[i + 1].x), py(tl.pts[i + 1].y));
|
||||||
doc.line(px(-wallWidth / 2), py(hLeft), px(-wallWidth / 2), py(0));
|
doc.line(px(-wallWidth / 2), py(hLeft), px(-wallWidth / 2), py(0));
|
||||||
|
|
||||||
doc.setDrawColor(...RED);
|
doc.setDrawColor(...RED);
|
||||||
doc.setLineWidth(0.3);
|
doc.setLineWidth(0.3);
|
||||||
pieces.forEach(p => {
|
pieces.forEach(p => {
|
||||||
if (p.kind === 'bovenregel') return; // apart getekend hieronder, volgt de helling
|
if (p.outline) {
|
||||||
const isVertical = p.kind === 'verticale balk' || p.kind === 'kreupele stijl';
|
// Werkelijke omtrek: schuine kop (volgt de daklijn, evt. met knik
|
||||||
if (isVertical) {
|
// onder de nok) resp. bovenregel in verstek.
|
||||||
|
const c = p.outline.map(q => [px(q.x), py(q.y)]);
|
||||||
|
c.forEach((q, i) => { const n = c[(i + 1) % c.length]; doc.line(q[0], q[1], n[0], n[1]); });
|
||||||
|
} else if (p.kind === 'verticale balk' || p.kind === 'kreupele stijl') {
|
||||||
// Viervlak i.p.v. rechthoek: de bovenkant loopt schuin mee met de
|
// Viervlak i.p.v. rechthoek: de bovenkant loopt schuin mee met de
|
||||||
// daklijn (yTopLeft/yTopRight kunnen verschillen), zodat zowel de
|
// daklijn (yTopLeft/yTopRight kunnen verschillen), zodat zowel de
|
||||||
// hoogste als de laagste kant van de balk op de juiste plek komt.
|
// hoogste als de laagste kant van de balk op de juiste plek komt.
|
||||||
@@ -143,22 +147,6 @@ window.WallPdf = (() => {
|
|||||||
doc.text(String(p.posnum), px(p.x), py(p.y), { align: 'center' });
|
doc.text(String(p.posnum), px(p.x), py(p.y), { align: 'center' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bovenregel: volgt de dakhelling, dus als schuin parallellogram
|
|
||||||
// getekend (niet als vlakke rechthoek zoals de rest hierboven).
|
|
||||||
const topPlatePiece = pieces.find(p => p.kind === 'bovenregel');
|
|
||||||
if (topPlatePiece) {
|
|
||||||
const slope = (hRight - hLeft) / wallWidth;
|
|
||||||
const vOff = CONFIG.plateThick * Math.sqrt(1 + slope * slope);
|
|
||||||
const tp = [
|
|
||||||
[px(-wallWidth / 2), py(hLeft)], [px(wallWidth / 2), py(hRight)],
|
|
||||||
[px(wallWidth / 2), py(hRight - vOff)], [px(-wallWidth / 2), py(hLeft - vOff)]
|
|
||||||
];
|
|
||||||
for (let i = 0; i < 4; i++) doc.line(tp[i][0], tp[i][1], tp[(i + 1) % 4][0], tp[(i + 1) % 4][1]);
|
|
||||||
doc.setTextColor(...BLACK);
|
|
||||||
doc.setFontSize(5);
|
|
||||||
doc.text(String(topPlatePiece.posnum), px(topPlatePiece.x), py(topPlatePiece.y), { align: 'center' });
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.setDrawColor(...BLACK);
|
doc.setDrawColor(...BLACK);
|
||||||
doc.setLineWidth(0.2);
|
doc.setLineWidth(0.2);
|
||||||
CONFIG.openings.forEach(op => {
|
CONFIG.openings.forEach(op => {
|
||||||
@@ -188,6 +176,13 @@ window.WallPdf = (() => {
|
|||||||
doc.text(String(Math.round(hLeft)), px(-wallWidth / 2) - 9, (py(0) + py(hLeft)) / 2, { align: 'center', angle: 90 });
|
doc.text(String(Math.round(hLeft)), px(-wallWidth / 2) - 9, (py(0) + py(hLeft)) / 2, { align: 'center', angle: 90 });
|
||||||
drawDimLine(px(wallWidth / 2) + 7, py(0), px(wallWidth / 2) + 7, py(hRight));
|
drawDimLine(px(wallWidth / 2) + 7, py(0), px(wallWidth / 2) + 7, py(hRight));
|
||||||
doc.text(String(Math.round(hRight)), px(wallWidth / 2) + 11, (py(0) + py(hRight)) / 2, { align: 'center', angle: 90 });
|
doc.text(String(Math.round(hRight)), px(wallWidth / 2) + 11, (py(0) + py(hRight)) / 2, { align: 'center', angle: 90 });
|
||||||
|
if (tl.peak) {
|
||||||
|
// Nokhoogte: stippellijn van vloer tot nok.
|
||||||
|
doc.setLineDashPattern([1, 1], 0);
|
||||||
|
doc.line(px(tl.peak.x), py(0), px(tl.peak.x), py(tl.peak.y));
|
||||||
|
doc.setLineDashPattern([], 0);
|
||||||
|
doc.text('nok ' + Math.round(tl.peak.y), px(tl.peak.x) + 1.5, py(tl.peak.y) + 4);
|
||||||
|
}
|
||||||
|
|
||||||
const studEdges = new Set([0, wallWidth]);
|
const studEdges = new Set([0, wallWidth]);
|
||||||
calcData.fullStuds.forEach(x => studEdges.add(Math.round(x - CONFIG.studWidth / 2 + wallWidth / 2)));
|
calcData.fullStuds.forEach(x => studEdges.add(Math.round(x - CONFIG.studWidth / 2 + wallWidth / 2)));
|
||||||
@@ -256,7 +251,7 @@ window.WallPdf = (() => {
|
|||||||
// Stijlen (vol + kreupel) als doorsneden: rechthoek met kruis.
|
// Stijlen (vol + kreupel) als doorsneden: rechthoek met kruis.
|
||||||
doc.setDrawColor(...RED);
|
doc.setDrawColor(...RED);
|
||||||
doc.setLineWidth(0.2);
|
doc.setLineWidth(0.2);
|
||||||
const studXs = calcData.fullStuds.concat(calcData.jackStuds.map(j => j.x));
|
const studXs = [...new Set(calcData.studs.map(st => st.x))];
|
||||||
studXs.forEach(x => {
|
studXs.forEach(x => {
|
||||||
const x0 = px(x - CONFIG.studWidth / 2), x1 = px(x + CONFIG.studWidth / 2);
|
const x0 = px(x - CONFIG.studWidth / 2), x1 = px(x + CONFIG.studWidth / 2);
|
||||||
doc.rect(x0, tvFrame0, x1 - x0, tvFrame1 - tvFrame0, 'S');
|
doc.rect(x0, tvFrame0, x1 - x0, tvFrame1 - tvFrame0, 'S');
|
||||||
|
|||||||
+145
-116
@@ -25,16 +25,16 @@ const WALL_TEMPLATE = `
|
|||||||
<div id="left-column">
|
<div id="left-column">
|
||||||
<div id="pane-2d-container">
|
<div id="pane-2d-container">
|
||||||
<div class="pane-2d-sub pane-2d-primary" id="pane-2d-front">
|
<div class="pane-2d-sub pane-2d-primary" id="pane-2d-front">
|
||||||
<div class="pane-label">2D BUITENAANZICHT</div>
|
<div class="pane-label">BUITENAANZICHT</div>
|
||||||
<canvas id="canvas-2d-front"></canvas>
|
<canvas id="canvas-2d-front"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="pane-2d-sub pane-2d-row" id="pane-2d-bottom-row">
|
<div class="pane-2d-sub pane-2d-row" id="pane-2d-bottom-row">
|
||||||
<div class="pane-2d-half" id="pane-2d-top">
|
<div class="pane-2d-half" id="pane-2d-top">
|
||||||
<div class="pane-label">2D BOVENAANZICHT</div>
|
<div class="pane-label">BOVENAANZICHT</div>
|
||||||
<canvas id="canvas-2d-top"></canvas>
|
<canvas id="canvas-2d-top"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="pane-2d-half" id="pane-2d-side">
|
<div class="pane-2d-half" id="pane-2d-side">
|
||||||
<div class="pane-label">2D ZIJAANZICHT</div>
|
<div class="pane-label">ZIJAANZICHT</div>
|
||||||
<canvas id="canvas-2d-side"></canvas>
|
<canvas id="canvas-2d-side"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,100 +46,105 @@ const WALL_TEMPLATE = `
|
|||||||
<span id="io-status" class="io-status"></span>
|
<span id="io-status" class="io-status"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-bar">
|
<div class="tab-bar">
|
||||||
<button class="tab-btn active" data-tab="tab-wall">1. Wand Opbouw</button>
|
<button class="tab-btn active" data-tab="tab-wall"><i class="bi bi-rulers"></i> Opbouw</button>
|
||||||
<button class="tab-btn" data-tab="tab-opening">2. Openingen</button>
|
<button class="tab-btn" data-tab="tab-opening"><i class="bi bi-door-open"></i> Openingen</button>
|
||||||
<button class="tab-btn" data-tab="tab-materials">3. Materiaalkeuze</button>
|
<button class="tab-btn" data-tab="tab-materials"><i class="bi bi-layers"></i> Materiaal</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TAB 1: WAND -->
|
<!-- TAB 1: WAND -->
|
||||||
<div id="tab-wall" class="tab-content active">
|
<div id="tab-wall" class="tab-content active">
|
||||||
<div class="controls-grid">
|
<div class="controls-grid">
|
||||||
<div>
|
<div>
|
||||||
<h1>Wand Dimensies (mm)</h1>
|
<h1><i class="bi bi-rulers"></i> Afmetingen</h1>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="inp-wall-w">Wand Breedte:</label>
|
<label for="inp-wall-w">Breedte</label>
|
||||||
<div><input type="number" id="inp-wall-w" value="6000" step="50" min="800" max="10000"><span class="unit">mm</span></div>
|
<div><input type="number" id="inp-wall-w" value="6000" step="50" min="800" max="10000"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="inp-wall-h-all">Totale Hoogte:</label>
|
<label for="inp-wall-h-all">Hoogte</label>
|
||||||
<div><input type="number" id="inp-wall-h-all" value="2400" step="50" min="500" max="5000"><span class="unit">mm</span></div>
|
<div><input type="number" id="inp-wall-h-all" value="2400" step="50" min="500" max="5000"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="inp-wall-h-left">Hoogte Links ($Y_1$):</label>
|
<label for="inp-wall-h-left" id="lbl-wall-h-left">Hoogte links</label>
|
||||||
<div><input type="number" id="inp-wall-h-left" value="2400" step="50" min="500" max="5000"><span class="unit">mm</span></div>
|
<div><input type="number" id="inp-wall-h-left" value="2400" step="50" min="500" max="5000"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="inp-wall-h-right">Hoogte Rechts ($Y_2$):</label>
|
<label for="inp-wall-h-right" id="lbl-wall-h-right">Hoogte rechts</label>
|
||||||
<div><input type="number" id="inp-wall-h-right" value="2400" step="50" min="500" max="5000"><span class="unit">mm</span></div>
|
<div><input type="number" id="inp-wall-h-right" value="2400" step="50" min="500" max="5000"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-group" title="Middenpunt van de bovenrand verhogen (nok): links en rechts worden dan goothoogtes.">
|
||||||
|
<label for="inp-wall-peak">Middenpunt (nok)</label>
|
||||||
|
<div><label class="ui-switch"><input type="checkbox" id="inp-wall-peak"><span></span></label></div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group" id="grp-wall-h-mid" style="display:none">
|
||||||
|
<label for="inp-wall-h-mid">Nokhoogte</label>
|
||||||
|
<div><input type="number" id="inp-wall-h-mid" value="3400" step="50" min="500" max="8000"><span class="unit">mm</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<h1>Verticale Balken & Dorpels</h1>
|
<h1><i class="bi bi-columns-gap"></i> Stijlen</h1>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="inp-stud-w">Verticale Balk Dikte/Breedte:</label>
|
<label for="inp-stud-w">Dikte</label>
|
||||||
<div><input type="number" id="inp-stud-w" value="38" step="5" min="20" max="300"><span class="unit">mm</span></div>
|
<div><input type="number" id="inp-stud-w" value="38" step="5" min="20" max="300"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="inp-stud-d">Verticale Balk Diepte:</label>
|
<label for="inp-stud-d">Diepte</label>
|
||||||
<div><input type="number" id="inp-stud-d" value="235" step="5" min="20" max="400"><span class="unit">mm</span></div>
|
<div><input type="number" id="inp-stud-d" value="235" step="5" min="20" max="400"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1>Muur Liggers (Top & Onder)</h1>
|
<h1><i class="bi bi-distribute-vertical"></i> Regels (boven en onder)</h1>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="inp-plate-w">Ligger Dikte:</label>
|
<label for="inp-plate-w">Dikte</label>
|
||||||
<div><input type="number" id="inp-plate-w" value="38" step="5" min="20" max="200"><span class="unit">mm</span></div>
|
<div><input type="number" id="inp-plate-w" value="38" step="5" min="20" max="200"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="inp-plate-d">Ligger Diepte:</label>
|
<label for="inp-plate-d">Diepte</label>
|
||||||
<div><input type="number" id="inp-plate-d" value="235" step="5" min="20" max="400"><span class="unit">mm</span></div>
|
<div><input type="number" id="inp-plate-d" value="235" step="5" min="20" max="400"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="inp-sheathing-t">Beplating Dikte:</label>
|
<label for="inp-sheathing-t">Dikte beplating</label>
|
||||||
<div><input type="number" id="inp-sheathing-t" value="10" step="1" min="3" max="30"><span class="unit">mm</span></div>
|
<div><input type="number" id="inp-sheathing-t" value="10" step="1" min="3" max="30"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<h1>Regelwerk Afstand (h.o.h.)</h1>
|
<h1><i class="bi bi-arrows"></i> Regelwerk</h1>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="inp-min-sp">Min. h.o.h.:</label>
|
<label for="inp-min-sp">Afstand min.</label>
|
||||||
<div><input type="number" id="inp-min-sp" value="240" step="10" min="100" max="1000"><span class="unit">mm</span></div>
|
<div><input type="number" id="inp-min-sp" value="240" step="10" min="100" max="1000"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="inp-max-sp">Max. h.o.h.:</label>
|
<label for="inp-max-sp">Afstand max.</label>
|
||||||
<div><input type="number" id="inp-max-sp" value="480" step="10" min="150" max="1500"><span class="unit">mm</span></div>
|
<div><input type="number" id="inp-max-sp" value="480" step="10" min="150" max="1500"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="result-row"><span>Totaal verticale balken:</span><span data-spec="vert-beams" class="highlight">-</span></div>
|
<div class="result-row"><span>Verticale balken</span><span data-spec="vert-beams" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Totaal horizontale balken:</span><span data-spec="horiz-beams" class="highlight">-</span></div>
|
<div class="result-row"><span>Horizontale balken</span><span data-spec="horiz-beams" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Dakhelling:</span><span data-spec="roof-angle" class="highlight">-</span></div>
|
<div class="result-row"><span>Dakhelling</span><span data-spec="roof-angle" class="highlight">-</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TAB 2: OPENINGEN (meerdere, ankerbaar) -->
|
<!-- TAB 2: OPENINGEN (meerdere, ankerbaar) -->
|
||||||
<div id="tab-opening" class="tab-content">
|
<div id="tab-opening" class="tab-content">
|
||||||
<button id="btn-add-opening" class="add-opening-btn">+ Opening Toevoegen</button>
|
<button id="btn-add-opening" class="add-opening-btn"><i class="bi bi-plus-lg"></i> Opening toevoegen</button>
|
||||||
<div id="openings-global-warning" class="global-warning" style="display:none;"></div>
|
<div id="openings-global-warning" class="global-warning" style="display:none;"></div>
|
||||||
<div id="openings-list"></div>
|
<div id="openings-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TAB 3: MATERIAALKEUZE -->
|
<!-- TAB 3: MATERIAALKEUZE -->
|
||||||
<div id="tab-materials" class="tab-content">
|
<div id="tab-materials" class="tab-content">
|
||||||
<h1>Binnenzijde</h1>
|
<h1><i class="bi bi-layers"></i> Buitenzijde</h1>
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
<h1>Buitenzijde</h1>
|
|
||||||
<div class="material-toggle-group">
|
<div class="material-toggle-group">
|
||||||
<button type="button" id="btn-mat-vochtdoek" class="material-toggle-btn active" data-material="vochtdoek">Vochtdoek</button>
|
<button type="button" id="btn-mat-vochtdoek" class="material-toggle-btn active" data-material="vochtdoek">Vochtdoek</button>
|
||||||
<button type="button" id="btn-mat-cementplaat" class="material-toggle-btn" data-material="cementplaat">Cement-gebonden platen</button>
|
<button type="button" id="btn-mat-cementplaat" class="material-toggle-btn" data-material="cementplaat">Cementplaat</button>
|
||||||
<button type="button" id="btn-mat-spaanplaat" class="material-toggle-btn" data-material="spaanplaat">Spaanplaat</button>
|
<button type="button" id="btn-mat-spaanplaat" class="material-toggle-btn" data-material="spaanplaat">Spaanplaat</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="cladding-section">
|
<div id="cladding-section">
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<h1>Gevelbekleding (WEO/Novalit) — richting</h1>
|
<h1><i class="bi bi-grip-horizontal"></i> Richting gevelbekleding</h1>
|
||||||
<div class="material-toggle-group">
|
<div class="material-toggle-group">
|
||||||
<button type="button" class="material-toggle-btn active" data-cladding="horizontaal">Horizontaal</button>
|
<button type="button" class="material-toggle-btn active" data-cladding="horizontaal">Horizontaal</button>
|
||||||
<button type="button" class="material-toggle-btn" data-cladding="verticaal">Verticaal</button>
|
<button type="button" class="material-toggle-btn" data-cladding="verticaal">Verticaal</button>
|
||||||
@@ -153,7 +158,7 @@ const WALL_TEMPLATE = `
|
|||||||
|
|
||||||
<div id="right-column">
|
<div id="right-column">
|
||||||
<div id="pane-3d">
|
<div id="pane-3d">
|
||||||
<div class="pane-label">3D PERSPECTIEF</div>
|
<div class="pane-label">3D</div>
|
||||||
<div class="toolbar-3d" id="wall-toolbar-3d"></div>
|
<div class="toolbar-3d" id="wall-toolbar-3d"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -161,37 +166,37 @@ const WALL_TEMPLATE = `
|
|||||||
<div class="controls-grid">
|
<div class="controls-grid">
|
||||||
<div>
|
<div>
|
||||||
<div class="spec-section">
|
<div class="spec-section">
|
||||||
<h1>Balken & Dak</h1>
|
<h1><i class="bi bi-columns-gap"></i> Balken</h1>
|
||||||
<div class="result-row"><span>Verticale balken:</span><span data-spec="vert-beams" class="highlight">-</span></div>
|
<div class="result-row"><span>Verticale balken</span><span data-spec="vert-beams" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Horizontale balken:</span><span data-spec="horiz-beams" class="highlight">-</span></div>
|
<div class="result-row"><span>Horizontale balken</span><span data-spec="horiz-beams" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Dakhelling:</span><span data-spec="roof-angle" class="highlight">-</span></div>
|
<div class="result-row"><span>Dakhelling</span><span data-spec="roof-angle" class="highlight">-</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="spec-section">
|
<div class="spec-section">
|
||||||
<h1>Houtwerk</h1>
|
<h1><i class="bi bi-tree"></i> Houtwerk</h1>
|
||||||
<div class="result-row"><span>Volume hout:</span><span data-spec="wood-m3" class="highlight">-</span></div>
|
<div class="result-row"><span>Volume hout</span><span data-spec="wood-m3" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Gewicht hout:</span><span data-spec="wood-kg" class="highlight">-</span></div>
|
<div class="result-row"><span>Gewicht hout</span><span data-spec="wood-kg" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Inkoop balken:</span><span data-spec="stock-list" class="highlight">-</span></div>
|
<div class="result-row"><span>Inkoop balken</span><span data-spec="stock-list" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Snijverlies:</span><span data-spec="stock-waste" class="highlight">-</span></div>
|
<div class="result-row"><span>Snijverlies</span><span data-spec="stock-waste" class="highlight">-</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="spec-section">
|
<div class="spec-section">
|
||||||
<h1>Spaanplaat (binnenzijde)</h1>
|
<h1><i class="bi bi-layers"></i> Binnenzijde: spaanplaat</h1>
|
||||||
<div class="result-row"><span>Oppervlak:</span><span data-spec="spaanplaat-m2" class="highlight">-</span></div>
|
<div class="result-row"><span>Oppervlak</span><span data-spec="spaanplaat-m2" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Volume:</span><span data-spec="spaanplaat-m3" class="highlight">-</span></div>
|
<div class="result-row"><span>Volume</span><span data-spec="spaanplaat-m3" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Gewicht:</span><span data-spec="spaanplaat-kg" class="highlight">-</span></div>
|
<div class="result-row"><span>Gewicht</span><span data-spec="spaanplaat-kg" class="highlight">-</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="spec-section">
|
<div class="spec-section">
|
||||||
<h1>Buitenzijde (<span data-spec="buiten-materiaalnaam">Vochtdoek</span>)</h1>
|
<h1><i class="bi bi-layers-half"></i> Buitenzijde: <span data-spec="buiten-materiaalnaam">Vochtdoek</span></h1>
|
||||||
<div class="result-row"><span>Oppervlak:</span><span data-spec="buiten-m2" class="highlight">-</span></div>
|
<div class="result-row"><span>Oppervlak</span><span data-spec="buiten-m2" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Volume:</span><span data-spec="buiten-m3" class="highlight">-</span></div>
|
<div class="result-row"><span>Volume</span><span data-spec="buiten-m3" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Gewicht:</span><span data-spec="buiten-kg" class="highlight">-</span></div>
|
<div class="result-row"><span>Gewicht</span><span data-spec="buiten-kg" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Latwerk 20x30 (kruislat):</span><span data-spec="latten" class="highlight">-</span></div>
|
<div class="result-row"><span>Latwerk 20×30</span><span data-spec="latten" class="highlight">-</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="spec-section">
|
<div class="spec-section">
|
||||||
<h1>Isolatie & totaal</h1>
|
<h1><i class="bi bi-speedometer2"></i> Isolatie en gewicht</h1>
|
||||||
<div class="result-row"><span>Inhoud leegtes:</span><span data-spec="void-m3" class="highlight">-</span></div>
|
<div class="result-row"><span>Isolatie</span><span data-spec="void-m3" class="highlight">-</span></div>
|
||||||
<div class="result-row"><span>Totaal gewicht wand:</span><span data-spec="total-kg" class="highlight">-</span></div>
|
<div class="result-row"><span>Totaal gewicht</span><span data-spec="total-kg" class="highlight">-</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,23 +212,23 @@ const WALL_TEMPLATE = `
|
|||||||
</div>
|
</div>
|
||||||
<div class="controls-grid">
|
<div class="controls-grid">
|
||||||
<div>
|
<div>
|
||||||
<div class="control-group"><label for="inp-proj-naam">Projectnaam:</label>
|
<div class="control-group"><label for="inp-proj-naam">Projectnaam</label>
|
||||||
<div><input type="text" id="inp-proj-naam" value=""></div></div>
|
<div><input type="text" id="inp-proj-naam" value=""></div></div>
|
||||||
<div class="control-group"><label for="inp-proj-plaats">Plaats:</label>
|
<div class="control-group"><label for="inp-proj-plaats">Plaats</label>
|
||||||
<div><input type="text" id="inp-proj-plaats" value=""></div></div>
|
<div><input type="text" id="inp-proj-plaats" value=""></div></div>
|
||||||
<div class="control-group"><label for="inp-proj-straat">Straat:</label>
|
<div class="control-group"><label for="inp-proj-straat">Straat</label>
|
||||||
<div><input type="text" id="inp-proj-straat" value=""></div></div>
|
<div><input type="text" id="inp-proj-straat" value=""></div></div>
|
||||||
<div class="control-group"><label for="inp-proj-nummer">Projectnummer:</label>
|
<div class="control-group"><label for="inp-proj-nummer">Projectnummer</label>
|
||||||
<div><input type="text" id="inp-proj-nummer" value=""></div></div>
|
<div><input type="text" id="inp-proj-nummer" value=""></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="control-group"><label for="inp-proj-tekenaar">Tekenaar:</label>
|
<div class="control-group"><label for="inp-proj-tekenaar">Tekenaar</label>
|
||||||
<div><input type="text" id="inp-proj-tekenaar" value=""></div></div>
|
<div><input type="text" id="inp-proj-tekenaar" value=""></div></div>
|
||||||
<div class="control-group"><label for="inp-proj-titel">Titel:</label>
|
<div class="control-group"><label for="inp-proj-titel">Titel</label>
|
||||||
<div><input type="text" id="inp-proj-titel" value="BuitenWand"></div></div>
|
<div><input type="text" id="inp-proj-titel" value="BuitenWand"></div></div>
|
||||||
<div class="control-group"><label for="inp-proj-element">Element nummer:</label>
|
<div class="control-group"><label for="inp-proj-element">Elementnummer</label>
|
||||||
<div><input type="text" id="inp-proj-element" value=""></div></div>
|
<div><input type="text" id="inp-proj-element" value=""></div></div>
|
||||||
<div class="control-group"><label for="inp-proj-opmerking">Opmerking:</label>
|
<div class="control-group"><label for="inp-proj-opmerking">Opmerking</label>
|
||||||
<div><input type="text" id="inp-proj-opmerking" value=""></div></div>
|
<div><input type="text" id="inp-proj-opmerking" value=""></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -269,7 +274,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
// Dichtheden/gewichten: zie WallModel (wall/wall-model.js).
|
// Dichtheden/gewichten: zie WallModel (wall/wall-model.js).
|
||||||
|
|
||||||
let CONFIG = {};
|
let CONFIG = {};
|
||||||
let calcData = { fullStuds: [], jackStuds: [] };
|
let calcData = { fullStuds: [], studs: [] };
|
||||||
let wallGroup3D = null;
|
let wallGroup3D = null;
|
||||||
let lastBaseHeight = 2400;
|
let lastBaseHeight = 2400;
|
||||||
|
|
||||||
@@ -338,9 +343,9 @@ function mountWallEditor(container, callbacks) {
|
|||||||
// OPENINGEN: UI (lijst met kaarten, ankerkeuze, toevoegen/verwijderen)
|
// OPENINGEN: UI (lijst met kaarten, ankerkeuze, toevoegen/verwijderen)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
function anchorOffsetLabel(anchor) {
|
function anchorOffsetLabel(anchor) {
|
||||||
if (anchor === 'right') return 'Afstand vanaf rechts:';
|
if (anchor === 'right') return 'Afstand tot rechts';
|
||||||
if (anchor === 'middle') return 'Verschuiving t.o.v. midden:';
|
if (anchor === 'middle') return 'Verschuiving t.o.v. midden';
|
||||||
return 'Afstand vanaf links:';
|
return 'Afstand tot links';
|
||||||
}
|
}
|
||||||
|
|
||||||
function addOpening() {
|
function addOpening() {
|
||||||
@@ -360,16 +365,12 @@ function mountWallEditor(container, callbacks) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleFullscreen3D() {
|
function toggleFullscreen3D() {
|
||||||
const el = document.getElementById('app-container');
|
UI.cycle3D(document.getElementById('app-container')); // T: standaard -> 3D groot -> alleen render
|
||||||
el.classList.remove('fullscreen-2d');
|
|
||||||
el.classList.toggle('fullscreen-3d');
|
|
||||||
requestAnimationFrame(onWindowResize);
|
requestAnimationFrame(onWindowResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFullscreen2D() {
|
function toggleFullscreen2D() {
|
||||||
const el = document.getElementById('app-container');
|
UI.toggle2D(document.getElementById('app-container'));
|
||||||
el.classList.remove('fullscreen-3d');
|
|
||||||
el.classList.toggle('fullscreen-2d');
|
|
||||||
requestAnimationFrame(onWindowResize);
|
requestAnimationFrame(onWindowResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,7 +449,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
<div class="controls-grid">
|
<div class="controls-grid">
|
||||||
<div>
|
<div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>Type:</label>
|
<label>Type</label>
|
||||||
<div>
|
<div>
|
||||||
<select data-field="type" data-id="${op.id}">
|
<select data-field="type" data-id="${op.id}">
|
||||||
${Object.entries(WallModel.OPENING_TYPES).map(([k, t]) => `<option value="${k}" ${(op.type || 'raam') === k ? 'selected' : ''}>${t.label}</option>`).join('')}
|
${Object.entries(WallModel.OPENING_TYPES).map(([k, t]) => `<option value="${k}" ${(op.type || 'raam') === k ? 'selected' : ''}>${t.label}</option>`).join('')}
|
||||||
@@ -456,21 +457,21 @@ function mountWallEditor(container, callbacks) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>Breedte:</label>
|
<label>Breedte</label>
|
||||||
<div><input type="number" data-field="w" data-id="${op.id}" value="${op.w}" step="50" min="400" max="2850"><span class="unit">mm</span></div>
|
<div><input type="number" data-field="w" data-id="${op.id}" value="${op.w}" step="50" min="400" max="2850"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>Hoogte:</label>
|
<label>Hoogte</label>
|
||||||
<div><input type="number" data-field="h" data-id="${op.id}" value="${op.h}" step="50" min="400"><span class="unit">mm</span></div>
|
<div><input type="number" data-field="h" data-id="${op.id}" value="${op.h}" step="50" min="400"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>Hoogte vloer (Y):</label>
|
<label>Afstand tot vloer</label>
|
||||||
<div><input type="number" data-field="y" data-id="${op.id}" value="${op.y}" step="50" min="0"><span class="unit">mm</span></div>
|
<div><input type="number" data-field="y" data-id="${op.id}" value="${op.y}" step="50" min="0"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>Ankerpunt:</label>
|
<label>Ankerpunt</label>
|
||||||
<div>
|
<div>
|
||||||
<select data-field="anchor" data-id="${op.id}">
|
<select data-field="anchor" data-id="${op.id}">
|
||||||
<option value="left" ${op.anchor === 'left' ? 'selected' : ''}>Links</option>
|
<option value="left" ${op.anchor === 'left' ? 'selected' : ''}>Links</option>
|
||||||
@@ -550,6 +551,8 @@ function mountWallEditor(container, callbacks) {
|
|||||||
heightAll: parseFloat(document.getElementById('inp-wall-h-all').value),
|
heightAll: parseFloat(document.getElementById('inp-wall-h-all').value),
|
||||||
heightLeft: parseFloat(document.getElementById('inp-wall-h-left').value),
|
heightLeft: parseFloat(document.getElementById('inp-wall-h-left').value),
|
||||||
heightRight: parseFloat(document.getElementById('inp-wall-h-right').value),
|
heightRight: parseFloat(document.getElementById('inp-wall-h-right').value),
|
||||||
|
// Middenpunt (nok); null = rechte bovenrand.
|
||||||
|
heightMid: document.getElementById('inp-wall-peak').checked ? parseFloat(document.getElementById('inp-wall-h-mid').value) : null,
|
||||||
studWidth: parseFloat(document.getElementById('inp-stud-w').value),
|
studWidth: parseFloat(document.getElementById('inp-stud-w').value),
|
||||||
studDepth: parseFloat(document.getElementById('inp-stud-d').value),
|
studDepth: parseFloat(document.getElementById('inp-stud-d').value),
|
||||||
plateThick: parseFloat(document.getElementById('inp-plate-w').value),
|
plateThick: parseFloat(document.getElementById('inp-plate-w').value),
|
||||||
@@ -573,6 +576,9 @@ function mountWallEditor(container, callbacks) {
|
|||||||
document.getElementById('inp-wall-h-all').value = state.wall.heightAll;
|
document.getElementById('inp-wall-h-all').value = state.wall.heightAll;
|
||||||
document.getElementById('inp-wall-h-left').value = state.wall.heightLeft;
|
document.getElementById('inp-wall-h-left').value = state.wall.heightLeft;
|
||||||
document.getElementById('inp-wall-h-right').value = state.wall.heightRight;
|
document.getElementById('inp-wall-h-right').value = state.wall.heightRight;
|
||||||
|
document.getElementById('inp-wall-peak').checked = state.wall.heightMid > 0;
|
||||||
|
document.getElementById('inp-wall-h-mid').value = state.wall.heightMid > 0 ? state.wall.heightMid
|
||||||
|
: Math.max(state.wall.heightLeft, state.wall.heightRight) + 800;
|
||||||
document.getElementById('inp-stud-w').value = state.wall.studWidth;
|
document.getElementById('inp-stud-w').value = state.wall.studWidth;
|
||||||
document.getElementById('inp-stud-d').value = state.wall.studDepth;
|
document.getElementById('inp-stud-d').value = state.wall.studDepth;
|
||||||
document.getElementById('inp-plate-w').value = state.wall.plateThick;
|
document.getElementById('inp-plate-w').value = state.wall.plateThick;
|
||||||
@@ -709,24 +715,35 @@ function mountWallEditor(container, callbacks) {
|
|||||||
const hLeftInput = document.getElementById('inp-wall-h-left');
|
const hLeftInput = document.getElementById('inp-wall-h-left');
|
||||||
const hRightInput = document.getElementById('inp-wall-h-right');
|
const hRightInput = document.getElementById('inp-wall-h-right');
|
||||||
|
|
||||||
|
const hMidInput = document.getElementById('inp-wall-h-mid');
|
||||||
|
const peakOn = document.getElementById('inp-wall-peak').checked;
|
||||||
|
|
||||||
let hAll = Math.round(parseFloat(hAllInput.value) || 2400);
|
let hAll = Math.round(parseFloat(hAllInput.value) || 2400);
|
||||||
let hLeft = Math.round(parseFloat(hLeftInput.value) || 2400);
|
let hLeft = Math.round(parseFloat(hLeftInput.value) || 2400);
|
||||||
let hRight = Math.round(parseFloat(hRightInput.value) || 2900);
|
let hRight = Math.round(parseFloat(hRightInput.value) || 2900);
|
||||||
|
let hMid = Math.round(parseFloat(hMidInput.value) || 0);
|
||||||
|
|
||||||
if (hAll !== lastBaseHeight) {
|
if (hAll !== lastBaseHeight) {
|
||||||
const delta = hAll - lastBaseHeight;
|
const delta = hAll - lastBaseHeight;
|
||||||
hLeft = Math.max(500, hLeft + delta);
|
hLeft = Math.max(500, hLeft + delta);
|
||||||
hRight = Math.max(500, hRight + delta);
|
hRight = Math.max(500, hRight + delta);
|
||||||
|
hMid = Math.max(500, hMid + delta);
|
||||||
hLeftInput.value = hLeft;
|
hLeftInput.value = hLeft;
|
||||||
hRightInput.value = hRight;
|
hRightInput.value = hRight;
|
||||||
|
hMidInput.value = hMid;
|
||||||
lastBaseHeight = hAll;
|
lastBaseHeight = hAll;
|
||||||
}
|
}
|
||||||
|
// Met nok: links/rechts zijn goothoogtes.
|
||||||
|
document.getElementById('grp-wall-h-mid').style.display = peakOn ? '' : 'none';
|
||||||
|
document.getElementById('lbl-wall-h-left').textContent = peakOn ? 'Goothoogte links' : 'Hoogte links';
|
||||||
|
document.getElementById('lbl-wall-h-right').textContent = peakOn ? 'Goothoogte rechts' : 'Hoogte rechts';
|
||||||
|
|
||||||
// Afronding/fallbacks + ankerresolutie van de openingen: WallModel.
|
// Afronding/fallbacks + ankerresolutie van de openingen: WallModel.
|
||||||
CONFIG = WallModel.buildConfig({
|
CONFIG = WallModel.buildConfig({
|
||||||
width: wallWidth,
|
width: wallWidth,
|
||||||
heightLeft: hLeft,
|
heightLeft: hLeft,
|
||||||
heightRight: hRight,
|
heightRight: hRight,
|
||||||
|
heightMid: peakOn ? hMid : null,
|
||||||
studWidth: parseFloat(document.getElementById('inp-stud-w').value),
|
studWidth: parseFloat(document.getElementById('inp-stud-w').value),
|
||||||
studDepth: parseFloat(document.getElementById('inp-stud-d').value),
|
studDepth: parseFloat(document.getElementById('inp-stud-d').value),
|
||||||
plateThick: parseFloat(document.getElementById('inp-plate-w').value),
|
plateThick: parseFloat(document.getElementById('inp-plate-w').value),
|
||||||
@@ -742,7 +759,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
readInputs();
|
readInputs();
|
||||||
calcData = WallModel.computeFrame(CONFIG);
|
calcData = WallModel.computeFrame(CONFIG);
|
||||||
|
|
||||||
setSpecAll('vert-beams', (calcData.fullStuds.length + calcData.jackStuds.length) + ' stuks');
|
setSpecAll('vert-beams', calcData.studs.length + ' stuks');
|
||||||
setSpecAll('roof-angle', calcData.angleDeg + '°');
|
setSpecAll('roof-angle', calcData.angleDeg + '°');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,8 +811,10 @@ function mountWallEditor(container, callbacks) {
|
|||||||
ctx.strokeRect(screenX, screenY, studWidthPx, studDepthPx);
|
ctx.strokeRect(screenX, screenY, studWidthPx, studDepthPx);
|
||||||
};
|
};
|
||||||
|
|
||||||
calcData.fullStuds.forEach(x => drawStud2D(x, false));
|
// Bovenaanzicht: één doorsnede per stijlpositie (kreupel alleen als er
|
||||||
calcData.jackStuds.forEach(j => drawStud2D(j.x, true));
|
// op die plek geen volle stijl staat).
|
||||||
|
const fullXs = new Set(calcData.studs.filter(st => st.kind !== 'kreupel').map(st => st.x));
|
||||||
|
[...new Set(calcData.studs.map(st => st.x))].forEach(x => drawStud2D(x, !fullXs.has(x)));
|
||||||
|
|
||||||
const startX = -CONFIG.wallWidth / 2;
|
const startX = -CONFIG.wallWidth / 2;
|
||||||
CONFIG.openings.forEach(op => {
|
CONFIG.openings.forEach(op => {
|
||||||
@@ -834,10 +853,8 @@ function mountWallEditor(container, callbacks) {
|
|||||||
const hLeftMM = CONFIG.wallHeightLeft;
|
const hLeftMM = CONFIG.wallHeightLeft;
|
||||||
const hRightMM = CONFIG.wallHeightRight;
|
const hRightMM = CONFIG.wallHeightRight;
|
||||||
const halfWMM = wallWidthMM / 2;
|
const halfWMM = wallWidthMM / 2;
|
||||||
const slopeMM = (hRightMM - hLeftMM) / wallWidthMM;
|
const tl = WallModel.topLine(CONFIG);
|
||||||
const topYAtMM = (x) => hLeftMM + (x + halfWMM) * slopeMM;
|
const maxRoofMM = tl.maxY;
|
||||||
const vertOffsetMM = CONFIG.plateThick * Math.sqrt(1 + slopeMM * slopeMM);
|
|
||||||
const maxRoofMM = Math.max(hLeftMM, hRightMM);
|
|
||||||
|
|
||||||
const scale = Math.min(w / (wallWidthMM * 1.3), h / (maxRoofMM * 1.3));
|
const scale = Math.min(w / (wallWidthMM * 1.3), h / (maxRoofMM * 1.3));
|
||||||
const centerX = w / 2;
|
const centerX = w / 2;
|
||||||
@@ -889,8 +906,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
ctxFront.beginPath();
|
ctxFront.beginPath();
|
||||||
ctxFront.moveTo(toCanvasX(-halfWMM), toCanvasY(0));
|
ctxFront.moveTo(toCanvasX(-halfWMM), toCanvasY(0));
|
||||||
ctxFront.lineTo(toCanvasX(halfWMM), toCanvasY(0));
|
ctxFront.lineTo(toCanvasX(halfWMM), toCanvasY(0));
|
||||||
ctxFront.lineTo(toCanvasX(halfWMM), toCanvasY(hRightMM));
|
tl.pts.slice().reverse().forEach(p => ctxFront.lineTo(toCanvasX(p.x), toCanvasY(p.y)));
|
||||||
ctxFront.lineTo(toCanvasX(-halfWMM), toCanvasY(hLeftMM));
|
|
||||||
ctxFront.closePath();
|
ctxFront.closePath();
|
||||||
ctxFront.stroke();
|
ctxFront.stroke();
|
||||||
|
|
||||||
@@ -905,14 +921,13 @@ function mountWallEditor(container, callbacks) {
|
|||||||
ctxFront.fill();
|
ctxFront.fill();
|
||||||
ctxFront.stroke();
|
ctxFront.stroke();
|
||||||
|
|
||||||
ctxFront.beginPath();
|
WallModel.topPlateOutlines(tl).forEach(pts => {
|
||||||
ctxFront.moveTo(toCanvasX(-halfWMM), toCanvasY(hLeftMM));
|
ctxFront.beginPath();
|
||||||
ctxFront.lineTo(toCanvasX(halfWMM), toCanvasY(hRightMM));
|
pts.forEach((p, i) => (i ? ctxFront.lineTo : ctxFront.moveTo).call(ctxFront, toCanvasX(p.x), toCanvasY(p.y)));
|
||||||
ctxFront.lineTo(toCanvasX(halfWMM), toCanvasY(hRightMM - vertOffsetMM));
|
ctxFront.closePath();
|
||||||
ctxFront.lineTo(toCanvasX(-halfWMM), toCanvasY(hLeftMM - vertOffsetMM));
|
ctxFront.fill();
|
||||||
ctxFront.closePath();
|
ctxFront.stroke();
|
||||||
ctxFront.fill();
|
});
|
||||||
ctxFront.stroke();
|
|
||||||
|
|
||||||
// X/Y-assen (lijn + label), altijd binnen het canvas gehouden zodat er
|
// X/Y-assen (lijn + label), altijd binnen het canvas gehouden zodat er
|
||||||
// niks buiten het zichtbare venster valt. Stijl: core/canvas-labels.js.
|
// niks buiten het zichtbare venster valt. Stijl: core/canvas-labels.js.
|
||||||
@@ -927,26 +942,31 @@ function mountWallEditor(container, callbacks) {
|
|||||||
const yAxisRightX = Math.min(w - 10, toCanvasX(halfWMM) + 14);
|
const yAxisRightX = Math.min(w - 10, toCanvasX(halfWMM) + 14);
|
||||||
drawCanvasDim(ctxFront, yAxisRightX, toCanvasY(0), yAxisRightX, toCanvasY(hRightMM),
|
drawCanvasDim(ctxFront, yAxisRightX, toCanvasY(0), yAxisRightX, toCanvasY(hRightMM),
|
||||||
'Y: ' + Math.round(hRightMM) + 'mm', Math.min(w - 4, yAxisRightX + 12), (toCanvasY(0) + toCanvasY(hRightMM)) / 2);
|
'Y: ' + Math.round(hRightMM) + 'mm', Math.min(w - 4, yAxisRightX + 12), (toCanvasY(0) + toCanvasY(hRightMM)) / 2);
|
||||||
|
if (tl.peak) {
|
||||||
|
const pxC = toCanvasX(tl.peak.x);
|
||||||
|
ctxFront.fillStyle = '#9ab';
|
||||||
|
ctxFront.font = '11px sans-serif';
|
||||||
|
ctxFront.textAlign = 'center';
|
||||||
|
ctxFront.fillText('nok ' + Math.round(tl.peak.y) + 'mm', pxC, Math.max(12, toCanvasY(tl.peak.y) - 6));
|
||||||
|
ctxFront.textAlign = 'start';
|
||||||
|
}
|
||||||
|
|
||||||
const drawStudFront = (xMM, isJack) => {
|
// Stijlstuk van y0 tot y1 (of tot de daklijn), zie WallModel.computeFrame.
|
||||||
const xLeft = xMM - CONFIG.studWidth / 2;
|
const drawStudFront = (st) => {
|
||||||
const xRight = xMM + CONFIG.studWidth / 2;
|
const xLeft = st.x - CONFIG.studWidth / 2;
|
||||||
const yTopLeft = topYAtMM(xLeft) - vertOffsetMM;
|
const xRight = st.x + CONFIG.studWidth / 2;
|
||||||
const yTopRight = topYAtMM(xRight) - vertOffsetMM;
|
ctxFront.fillStyle = st.kind === 'kreupel' ? '#a37c45' : '#c29b62';
|
||||||
const yBot = CONFIG.plateThick;
|
|
||||||
ctxFront.fillStyle = isJack ? '#a37c45' : '#c29b62';
|
|
||||||
ctxFront.strokeStyle = '#8a6836';
|
ctxFront.strokeStyle = '#8a6836';
|
||||||
ctxFront.beginPath();
|
ctxFront.beginPath();
|
||||||
ctxFront.moveTo(toCanvasX(xLeft), toCanvasY(yBot));
|
ctxFront.moveTo(toCanvasX(xLeft), toCanvasY(st.y0));
|
||||||
ctxFront.lineTo(toCanvasX(xRight), toCanvasY(yBot));
|
ctxFront.lineTo(toCanvasX(xRight), toCanvasY(st.y0));
|
||||||
ctxFront.lineTo(toCanvasX(xRight), toCanvasY(yTopRight));
|
if (st.y1 === null) tl.underPts(xLeft, xRight).reverse().forEach(p => ctxFront.lineTo(toCanvasX(p.x), toCanvasY(p.y)));
|
||||||
ctxFront.lineTo(toCanvasX(xLeft), toCanvasY(yTopLeft));
|
else { ctxFront.lineTo(toCanvasX(xRight), toCanvasY(st.y1)); ctxFront.lineTo(toCanvasX(xLeft), toCanvasY(st.y1)); }
|
||||||
ctxFront.closePath();
|
ctxFront.closePath();
|
||||||
ctxFront.fill();
|
ctxFront.fill();
|
||||||
ctxFront.stroke();
|
ctxFront.stroke();
|
||||||
};
|
};
|
||||||
calcData.fullStuds.forEach(x => drawStudFront(x, false));
|
calcData.studs.forEach(drawStudFront);
|
||||||
calcData.jackStuds.forEach(j => drawStudFront(j.x, true));
|
|
||||||
|
|
||||||
const startXMM = -halfWMM;
|
const startXMM = -halfWMM;
|
||||||
frontOpeningRects = [];
|
frontOpeningRects = [];
|
||||||
@@ -1008,10 +1028,11 @@ function mountWallEditor(container, callbacks) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Naad-zones (wand vervalt daar, zie woning) als gearceerde overlay.
|
// Naad-zones (wand vervalt daar, zie woning) als gearceerde overlay.
|
||||||
|
// Met upTo (mm): opbouw boven het lagere dak, alleen gearceerd tot daar.
|
||||||
function drawSeamOverlay(toCanvasX, toCanvasY, halfWMM, maxRoofMM) {
|
function drawSeamOverlay(toCanvasX, toCanvasY, halfWMM, maxRoofMM) {
|
||||||
seamCuts.forEach(([u0, u1]) => {
|
seamCuts.forEach(([u0, u1, upTo]) => {
|
||||||
const x0 = toCanvasX(-halfWMM + u0), x1 = toCanvasX(-halfWMM + u1);
|
const x0 = toCanvasX(-halfWMM + u0), x1 = toCanvasX(-halfWMM + u1);
|
||||||
const yTop = toCanvasY(maxRoofMM), yBot = toCanvasY(0);
|
const yTop = toCanvasY(upTo > 0 ? Math.min(upTo, maxRoofMM) : maxRoofMM), yBot = toCanvasY(0);
|
||||||
ctxFront.save();
|
ctxFront.save();
|
||||||
ctxFront.fillStyle = 'rgba(17, 17, 17, 0.8)';
|
ctxFront.fillStyle = 'rgba(17, 17, 17, 0.8)';
|
||||||
ctxFront.fillRect(x0, yTop, x1 - x0, yBot - yTop);
|
ctxFront.fillRect(x0, yTop, x1 - x0, yBot - yTop);
|
||||||
@@ -1135,7 +1156,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
const buitenThick = CONFIG.sheathingThick;
|
const buitenThick = CONFIG.sheathingThick;
|
||||||
const spaanThick = CONFIG.sheathingThick;
|
const spaanThick = CONFIG.sheathingThick;
|
||||||
const totalDepth = buitenThick + CONFIG.studDepth + spaanThick;
|
const totalDepth = buitenThick + CONFIG.studDepth + spaanThick;
|
||||||
const maxH = Math.max(CONFIG.wallHeightLeft, CONFIG.wallHeightRight);
|
const maxH = WallModel.topLine(CONFIG).maxY;
|
||||||
|
|
||||||
const scale = Math.min((w * 0.45) / totalDepth, (h * 0.8) / maxH);
|
const scale = Math.min((w * 0.45) / totalDepth, (h * 0.8) / maxH);
|
||||||
const originX = w / 2 - (totalDepth * scale) / 2;
|
const originX = w / 2 - (totalDepth * scale) / 2;
|
||||||
@@ -1204,13 +1225,15 @@ function mountWallEditor(container, callbacks) {
|
|||||||
controls.update();
|
controls.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
|
||||||
|
scene.add(ambient);
|
||||||
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
dirLight.position.set(5, 8, 5);
|
dirLight.position.set(5, 8, 5);
|
||||||
dirLight.castShadow = true;
|
dirLight.castShadow = true;
|
||||||
scene.add(dirLight);
|
scene.add(dirLight);
|
||||||
|
|
||||||
scene.add(new THREE.GridHelper(10, 20, 0x444444, 0x222222));
|
const grid = new THREE.GridHelper(10, 20, 0x444444, 0x222222);
|
||||||
|
scene.add(grid);
|
||||||
|
|
||||||
const wallMaterials = WallModel.createMaterials();
|
const wallMaterials = WallModel.createMaterials();
|
||||||
|
|
||||||
@@ -1236,18 +1259,23 @@ function mountWallEditor(container, callbacks) {
|
|||||||
|
|
||||||
let spaanplaatVisible = true;
|
let spaanplaatVisible = true;
|
||||||
let buitenVisible = true;
|
let buitenVisible = true;
|
||||||
let isolatieVisible = true;
|
|
||||||
// Weergave-filter (core/view-filter.js): beplating binnen/buiten + isolatie.
|
// Weergave-filter (core/view-filter.js): beplating binnen/buiten + isolatie.
|
||||||
createViewFilter(document.getElementById('wall-toolbar-3d'), [
|
createViewFilter(document.getElementById('wall-toolbar-3d'), [
|
||||||
{ key: 'binnen', label: 'Beplating binnenzijde' },
|
{ key: 'binnen', label: 'Beplating binnenzijde' },
|
||||||
{ key: 'buiten', label: 'Beplating buitenzijde' },
|
{ key: 'buiten', label: 'Beplating buitenzijde' }
|
||||||
{ key: 'isolatie', label: 'Isolatie' }
|
|
||||||
], (v) => {
|
], (v) => {
|
||||||
spaanplaatVisible = v.binnen;
|
spaanplaatVisible = v.binnen;
|
||||||
buitenVisible = v.buiten;
|
buitenVisible = v.buiten;
|
||||||
isolatieVisible = v.isolatie;
|
|
||||||
update3DScene();
|
update3DScene();
|
||||||
});
|
});
|
||||||
|
// Omgeving (core/skybox.js: geen/dag/nacht); de wand staat op de tegelvloer.
|
||||||
|
let skyMode = Skybox.getMode();
|
||||||
|
const skyEnv = Skybox.createEnvironment(scene);
|
||||||
|
const applySky = () => {
|
||||||
|
skyEnv.fitTo(wallGroup3D, 0);
|
||||||
|
Skybox.apply(skyMode, { env: skyEnv, grid, controls, sun: dirLight, ambient, base: { ambient: 0.6, sun: 0.8 }, shadowRoot: wallGroup3D });
|
||||||
|
};
|
||||||
|
Skybox.createToggle(document.getElementById('wall-toolbar-3d'), (m) => { skyMode = m; applySky(); });
|
||||||
|
|
||||||
// 3D-opbouw + volume-boekhouding komen uit WallModel.build3D (gedeeld
|
// 3D-opbouw + volume-boekhouding komen uit WallModel.build3D (gedeeld
|
||||||
// met de unit-editor).
|
// met de unit-editor).
|
||||||
@@ -1256,11 +1284,12 @@ function mountWallEditor(container, callbacks) {
|
|||||||
function update3DScene() {
|
function update3DScene() {
|
||||||
if (wallGroup3D) scene.remove(wallGroup3D);
|
if (wallGroup3D) scene.remove(wallGroup3D);
|
||||||
const result = WallModel.build3D(CONFIG, calcData, {
|
const result = WallModel.build3D(CONFIG, calcData, {
|
||||||
materials: wallMaterials, buitenMateriaal, cladding, trasraam, spaanplaatVisible, buitenVisible, isolatieVisible
|
materials: wallMaterials, buitenMateriaal, cladding, trasraam, spaanplaatVisible, buitenVisible
|
||||||
});
|
});
|
||||||
wallGroup3D = result.group;
|
wallGroup3D = result.group;
|
||||||
lastWallStats = result.stats;
|
lastWallStats = result.stats;
|
||||||
scene.add(wallGroup3D);
|
scene.add(wallGroup3D);
|
||||||
|
applySky(); // schaduw van de nieuwe wand
|
||||||
|
|
||||||
updateSpecificaties();
|
updateSpecificaties();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tectum lokaal starten: statische webserver in deze map (Python 3).
|
||||||
|
# Gebruik: ./start.sh [poort] (standaard 8000), daarna http://localhost:8000
|
||||||
|
cd "$(dirname "$0")" || exit 1
|
||||||
|
PORT="${1:-8000}"
|
||||||
|
echo "Tectum: http://localhost:$PORT (Ctrl+C om te stoppen)"
|
||||||
|
exec python3 -m http.server "$PORT" --bind 127.0.0.1
|
||||||
Reference in New Issue
Block a user