diff --git a/README.md b/README.md index e4a7a5a..7acd10d 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,14 @@ js/roof/ RoofModel: dak (plat/lessenaar/zadel) + dakrand (boeidelen/d js/lighting/ LightingModel: gevellampen, boeideel-spots, binnenspots js/elektra/ ElektraModel + ElektraPdf: meterkast, schakelaars (letters), wcd, groepen, leidingen js/sandwich/ SandwichModel: sandwichpanelen rondom +js/room/ RoomModel: ruimtes (verfemmer), functie, netto m², afwerking, vals plafond +js/interior/ FixtureModel: inrichting (sanitair, keuken, kasten, meubels, boiler) js/unit/ UnitModel: 1 vloer + 4 wanden, naden, wandstukken js/house/ HouseModel (woning) + woning-PDF + woning-editor +js/cost/ CostModel (kostenberekening), posten + noodwaarden, CostCsv (CSV's), venster Kosten & tijden js/app.js menubalk, versienummer, actieve editor json/defaults/ referentie-JSON's (niet runtime ingeladen) +kosten/ intern.csv (eigen waarden) + leveranciers.csv (live prijzen), bij het starten ingeladen assets/ logo, texturen, skybox ``` @@ -77,9 +81,21 @@ assets/ logo, texturen, skybox afmonteren, groepenkast en keuring. - **Kozijnen** (`wall/kozijn-model.js`): per opening een vulling (geen, raam met vast glas en optioneel ventilatierooster, of deur met dorpel en - optionele glasstrook; zij- en bovenlicht vullen de rest van een grote + optionele glasstrook of als glasdeur; zij- en bovenlicht vullen de rest van een grote opening), kleur buiten en binnen (wit, grijs, antraciet, zwart). Volgt de - maat van de opening, ook een schuine bovenzijde. + maat van de opening, ook een schuine bovenzijde. Deur: kant van het blad, + scharnierzijde en draairichting (naar binnen/buiten); openslaande deuren + met zijlichten = twee deuren naast elkaar. Schuifdeur: 1 of 2 bladen + (uit elkaar), in de wand of op de wand aan een rail (binnen- of + buitenzijde), glazen of dicht blad. In de plattegrond (app en PDF) een + draaicirkel per deur en de schuifrichting per schuifdeur; in de loopmodus + gaan deuren open (dubbele schuifdeur samen). Dagkanten (afgewerkte + fase): binnen een geschilderde omranding, dagkantplaten tot het kozijn en + een vensterbank onder ramen; buiten een omranding in de kozijnkleur en + een waterslag onder ramen (`WallModel.build3D`). +- **Plafond:** in de afgewerkte fase is de onderkant van het dak + geschilderd in de plafondkleur (Binnen › Wanden › Plafond), zolang + "Geschilderd" aan staat; anders kale OSB. - **Dakvorm/wandhoogte:** per unit een dakvorm (Unit › Dakvorm: plat, lessenaar, zadel, mansarde of wolfsdak, met schuiven en hellingshoek). Daaruit volgen de wanden: de kopgevels krijgen de daklijn (bij mansarde @@ -98,6 +114,22 @@ assets/ logo, texturen, skybox bovenkant vloer tot onderkant dak), unit max 4,3 x 14 m. Platte daken van verschillende hoogte krijgen elk hun eigen boeideel (versprongen, het lage loopt door tot onder het hoge). Wijzig je een wandhoogte in de wand-editor, dan geldt dezelfde regel vanuit die wand. + De daklijn van een wand hoort bij de wand zonder naden: loopt een wand + door een naad langer door (bijv. de kopgevel naast een gekoppelde unit), + dan blijven nok en knikken op hun plaats (`UnitModel.topLineOn`). +- **Overkapping:** bij een dak over de hele woning met `cover` loopt het + dak door tot de omhullende rechthoek waar geen unit staat (bijv. een + terras naast een kortere module), met een afgewerkte onderplaat in de + kleur van het boeideel (`HouseModel.coverExtensions`). +- **Dak over de hele woning:** tabblad Dak › "Over hele woning": één + dakvorm (zelfde invoer als per unit) over de omhullende rechthoek van alle + units; een wolfsdak alleen bij een rechthoek (bij een L-/T-vorm zouden + de wolfseinden kilkepers vragen, dan wordt het een zadeldak), bijv. een + grote wolfskap over twee units naast elkaar. Elke unit + houdt zijn eigen dakdeel (dekken, bovenste deel van het wolfsdak geknipt + op de unit, PDF-bladen, kosten); de wanden van alle units volgen het + woningdak. De daken per unit blijven bewaard (`roof.unitWalls`) en komen + terug als je het weer uitzet. - **Vloer:** balken, cirkelvormige doorvoeren, OSB, optioneel chassis; in de woning een vloerafwerking (textuur, bijv. visgraat). - **Woning:** units koppelen aan een zijde (uitlijning + verschuiving); op @@ -124,13 +156,44 @@ assets/ logo, texturen, skybox zonder spots). De rand van het lage dak loopt dan door tot de gevel van het hoge, of langs een zijgevel tot de volle overstek (onder de hoge rand). Boeideel en daktrim steken 80 mm boven het dakleer uit. -- **Gevel:** gevelbekleding per woning, met override per wand: rabat - (vochtdoek + kruislat + rabat 12 mm, kleur per woning, getekend als - plankentextuur die over naden doorloopt) of steen (181 texturen). Steen is overal een +- **Gevel:** gevelbekleding per woning, met override per wand en per + wandstuk: rabat (vochtdoek + kruislat + rabat 12 mm, kleur per woning, + getekend als plankentextuur die over naden doorloopt), steen (181 + texturen) of stucwerk (pleisterlaag 15 mm op cement-gebonden plaat, kleur + per woning, `WallModel.STUC`). Een zijde kan gesplitst worden (wand › + Wandstukken › Splitsen op, gemeten vanaf de linker vloerhoek van buitenaf): + elk stuk wordt een eigen wand-element en kan een eigen gevel krijgen + (bijv. stuc links, hout rechts). Steen is overal een steenstrip op een wit isolatieblok, samen 110 mm (`WallModel.STONE`). Het trasraam (cementplaat of een eigen steen) komt onder beide, dus twee stenen kan. Latwerk, doek en metselwerk lopen per gevel door over unit-naden. Sandwichpanelen rondom, 600 mm de grond in. +- **Ruimtes** (`room/room-model.js`): elk door wanden afgesloten gebied is + een ruimte, bepaald als een verfemmer: alles wat bereikbaar is zonder door + een wand te gaan (deuren en ramen tellen als wand). Exact op het raster van + de wandvlakken, dus de netto vloer tot op de mm; kiertjes tot 30 mm tussen + wanden worden dichtgezet. Gereedschap *Ruimte vullen* (Start/Binnen): kies + een functie (woonkamer, keuken, slaapkamer, badkamer, toilet, hal, ...) en + klik in de plattegrond; of selecteer een gebied en kies daar de functie + (nog eens klikken = de unit eronder). Per ruimte: naam, vloerafwerking + (o.a. tegels), FIBO-wandpanelen (decor, tot een hoogte of tot het plafond), + eigen verfkleur en een vals plafond op een vaste hoogte (alleen waar het + dak hoger ligt; wandafwerking en binnenspots volgen). Laag in de + plattegrond (Beeld › Tonen: Ruimtes), label met netto m², in de structuur, + het rapport, de kosten (vals plafond, FIBO) en op de PDF-plattegrond met + een ruimtestaat. Het zaadpunt hoort bij een unit en schuift mee. +- **Inrichting** (`interior/fixture-model.js`): zwevend toilet (wandcloset + aan een voorzetwand), fontein, badmeubel met spiegel, inloopdouche + (douchegoot, glaswand, regendouche), keukenblok (spoelbak, hoge kast met + koelkast en oven, vaatwasser, bovenkasten), kookeiland met inductieplaat, + kledingkast, bed, eettafel met stoelen, bank met chaise longue, fauteuil, + salontafel, tv-meubel, boiler en wasmachine. Binnen › Inrichting: kies + een stuk en klik in de plattegrond; slepen, bij een wand snapt de rug op + het wandvlak (Alt = vrij), D = draaien, Del = verwijderen. Per stuk maten + en opties (front, plek spoelbak, glaswand links/rechts, ...). In 2D, 3D + (afgewerkte fase), de PDF-plattegrond, de structuur en de kosten (groep + Inrichting; los meubilair telt niet mee). Het stuk hoort bij een unit en + schuift mee. - **Verlichting:** boeideel-spots of gevellampen (per gevel verdeeld, override per wand) en binnenspots in een raster per unit. - **Ribbon** (`core/ribbon.js`, vervangt de menubalk): Bestand · Start · @@ -152,6 +215,49 @@ assets/ logo, texturen, skybox noord/oost/zuid/west als vlakke render met legenda afwerking, plattegrond, vloeren, wandstukken, binnenwanden, totalen). +## Kosten en prijzen (CSV) + +Alle waarden van het kostenmodel staan in twee CSV-bestanden in `kosten/`, die +de app bij het starten ophaalt. Elke waarde staat op één plek; de CSV's zijn +leidend en de app bewaart niets in de browser. + +- `kosten/intern.csv`: eigen waarden: tijden, uurloon, bezetting, kosten per + uur, opslagen, marge (alles behalve prijzen). +- `kosten/leveranciers.csv`: alle inkoopprijzen (bijv. uit API-scripts van + leveranciers), ook constructiehout per balkmaat. +- `js/cost/cost-defaults.js`: welke posten er zijn (labels, eenheden, keuzes) + en noodwaarden voor wat een CSV mist. + +``` +# intern.csv +code;omschrijving;eenheid;waarde +arbeid.uurloon;Uurloon productie (all-in);€/u;45 +zagen.methode;Zaagmethode;;robot + +# leveranciers.csv +code;omschrijving;eenheid;prijs;leverancier;artikelnummer;bijgewerkt +elektra.wcdEnkel;Wandcontactdoos enkel met randaarde;€/st.;6,00;Groothandel X;123456;2026-10-09 +hout.balk.38x235;Constructiehout vuren C24 38×235;€/m;4,91;Houthandel Y;;2026-10-09 +``` + +- Verplicht: `code` en `waarde` (of `prijs`), excl. btw; `6,00`, `6.00`, + `1.234,50` en `€ 6` mogen; bij een keuze de sleutel of de tekst. + Scheidingsteken `;`, `,` of tab; regels met `#` worden overgeslagen. +- `code` = `.` uit `cost-defaults.js`, of `hout.balk.x` + voor constructiehout per balkmaat in €/m (zonder balkmaat: m³-prijs × volume). +- `omschrijving` is de naam van de post in de app (leeg = die uit `cost-defaults.js`). +- `eenheid` is optioneel; ingevuld moet die gelijk zijn aan die van de post, + anders wordt de regel geweigerd. +- Een prijs in intern.csv of een tijd in leveranciers.csv wordt geweigerd + ("hoort in …"). Meerdere regels met dezelfde code in leveranciers.csv + (leveranciers): de laagste prijs telt. +- Onbekende codes (bijv. onderdelen die de app nog niet rekent) en fouten staan + bovenaan in Kosten & tijden. +- Kosten & tijden: **Toepassen** geldt tot herladen (om te proberen); + **intern.csv downloaden** om het bestand in `kosten/` te vervangen. + Prijsvelden staan vast. Bij Leveranciers: **Sjabloon** (alle prijscodes), + **Tijdelijk laden** (tot herladen of **Terug naar server**). + ## Bekende beperkingen - Tijdelijk (verwijderen voor productie): zaagmachine-overzicht (zaagmeters bij @@ -159,7 +265,16 @@ assets/ logo, texturen, skybox - Tijdelijk: een nieuwe woning is het voorbeeldontwerp "Vlinderwoning" (92 m², drie units, vlinderdak met riet + platte tuinkamer, ook als `json/premade/vlinderwoning.json`); uit te zetten met `TEST_DEFAULT` in - `js/house/house-model.js`. + `js/house/house-model.js`. Bij het starten opent model **Amstel** + (`HouseModel.createAmstel`, naar `examples/ontwerp_amstel.pdf`): 11,24 × + 7,2 m, twee modules onder één plat dak dat over het terras doorloopt, + stucwerk wit met verticaal hout aan de slaapkamerkant, binnenwanden, + dubbele schuifdeur tussen woonkamer en hal, en complete inrichting. + Langer maken: beide units breder (alles is vanaf de westgevel gemaat). + Ook via Bestand › Woning Amstel. De L-woning (8 × 3,6 m + 4,5 × 3,6 m) + blijft beschikbaar als "Woning L-vorm". +- Netto m² per ruimte telt de hele vloer, ook onder een laag schuin dak + (geen NEN 2580-correctie voor hoogte < 1,5 m). - Units kunnen niet gedraaid worden. - Getekende binnenwanden volgen de dakvorm (nog) niet. - De dak-editor toont het dek in de plattegrond (niet per schuin dakvlak). diff --git a/assets/README.txt b/assets/README.txt index ca5abe9..0adc399 100644 --- a/assets/README.txt +++ b/assets/README.txt @@ -7,6 +7,3 @@ Assets voor de 3D-weergave en de PDF: naadloos), voorvertoning in thumb/32|64|128/*.png (WallModel.TRASRAAM_STONES) - textures/vloer/ vloerafwerking (FloorModel.FINISHES), bijv. visgraat_001.jpg (1500x1500, tegel 3 m); thumb/32|64|128/*.png - textures/dak/ dakbedekking: riet_.jpg (1024x1024, tegel 1 m); nummers in RoofModel RIET.nrs; thumb/32|64|128/*.png -- audio/five-armies.mp3 soundtrack easter egg (Ctrl+Shift+Alt+L shooter; core/granny-kit.js): - "Five Armies" Kevin MacLeod (incompetech.com), licensed under - Creative Commons: By Attribution 4.0 — https://creativecommons.org/licenses/by/4.0/ diff --git a/assets/audio/five-armies.mp3 b/assets/audio/five-armies.mp3 deleted file mode 100644 index a732acb..0000000 Binary files a/assets/audio/five-armies.mp3 and /dev/null differ diff --git a/css/app.css b/css/app.css index 3f89b97..2b5b86c 100644 --- a/css/app.css +++ b/css/app.css @@ -4,13 +4,7 @@ * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ -/* ============================================================ - HUISSTIJL Van Emous — kleuren als CSS-variabelen (bron: SCSS-variabelen - $van-emous-blue / -orange / -ultra-light, $nav-link-color, $primary). - De bediening (menu's, panelen, navigatie) is licht; de 2D-/3D-weergaven - houden een donkere achtergrond (--viewport-bg), omdat de canvas-tekeningen - daar lichte lijnen/teksten op gebruiken. - ============================================================ */ +/* De 2D-/3D-weergaven blijven donker (--viewport-bg): de canvas-tekeningen gebruiken lichte lijnen. */ :root { --van-emous-blue: #dce4ee; --van-emous-orange: #b95913; @@ -20,7 +14,7 @@ --primary-hover: #9c4a0f; --primary-soft: rgba(185, 89, 19, 0.10); - --primary-on-dark: #e8883d; /* oranje dat leesbaar blijft op de donkere weergaven */ + --primary-on-dark: #e8883d; --text: var(--nav-link-color); --text-muted: #5b6673; --text-faint: #8a94a0; @@ -85,7 +79,7 @@ background: var(--viewport-bg); } .pane-2d-sub + .pane-2d-sub { border-top: 1px solid var(--border); } - .pane-2d-primary { flex: 2; } /* primair aanzicht (bijv. vooraanzicht bij een wand) krijgt meer ruimte */ + .pane-2d-primary { flex: 2; } .pane-2d-sub canvas.interactive { cursor: default; } .pane-2d-row { display: flex; flex-direction: row; padding: 0; } .pane-2d-half { flex: 1; position: relative; min-width: 0; min-height: 0; background: var(--viewport-bg); } @@ -226,7 +220,6 @@ .highlight { color: var(--primary); font-weight: bold; } canvas { display: block; width: 100%; height: 100%; } - /* --- Openingen tab (nieuw) --- */ .opening-card { background: var(--surface); border: 1px solid var(--border); @@ -286,7 +279,6 @@ margin-bottom: 10px; } - /* --- Specificaties tab (nieuw) --- */ .spec-section { margin-bottom: 14px; } .material-info { font-size: 12px; color: var(--text-muted); line-height: 1.5; margin: 4px 0 10px; } @@ -298,9 +290,6 @@ } .material-toggle-btn.active { background: var(--primary); border-color: var(--primary); color: #fff; } .material-toggle-btn:hover:not(.active) { border-color: var(--primary); color: var(--primary); } -/* ============================================================ - APP SHELL: ribbon, workspace-wrapper, splashscreen, about-modal - ============================================================ */ html, body { height: 100%; } #tab-bar { @@ -331,7 +320,7 @@ body { display: flex; flex-direction: column; } color: var(--text-faint); font-size: 14px; flex-direction: column; gap: 10px; } -/* --- Splashscreen (blijft donker: het logo bevat lichte delen) --- */ +/* Splashscreen blijft donker: het logo bevat lichte delen. */ #splash-screen { position: fixed; inset: 0; background: #0d0d0d; z-index: 9999; display: flex; flex-direction: column; align-items: center; justify-content: center; @@ -342,7 +331,6 @@ body { display: flex; flex-direction: column; } #splash-screen .splash-name { color: #eee; font-weight: bold; letter-spacing: 1px; } @keyframes splashFadeOut { 0% { opacity: 1; } 100% { opacity: 0; visibility: hidden; pointer-events: none; } } -/* --- "Over deze software"-modal --- */ #about-overlay { position: fixed; inset: 0; background: rgba(33, 33, 33, .45); z-index: 500; display: none; align-items: center; justify-content: center; @@ -392,7 +380,6 @@ body { display: flex; flex-direction: column; } #wall-project-modal .modal-footer, #house-project-modal .modal-footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 14px; } #wall-project-modal .control-group input, #house-project-modal .control-group input { width: 150px; text-align: left; } -/* --- Kosten & tijden (CostSettings) --- */ #cost-settings-overlay { position: fixed; inset: 0; background: rgba(33, 33, 33, .45); z-index: 500; display: none; align-items: center; justify-content: center; @@ -416,6 +403,13 @@ body { display: flex; flex-direction: column; } #cost-settings-modal .cs-row select { text-align: left; } #cost-settings-modal .cs-unit { color: var(--text-faint); font-size: 11px; } #cost-settings-modal .cs-row.changed .cs-label { color: var(--primary); font-weight: bold; } +#cost-settings-modal .cs-row.from-list input { color: var(--primary); font-weight: bold; } +#cost-settings-modal .cs-row.from-list .cs-unit::after { content: ' CSV'; color: var(--primary); font-size: 9px; } +#cost-settings-modal .cs-pricelist { font-size: 12px; margin-bottom: 10px; padding: 6px 8px; border: 1px solid var(--border); border-radius: 4px; } +#cost-settings-modal .cs-pricelist .io-btn { padding: 1px 8px; margin-left: 6px; } +#cost-settings-modal .cs-pricelist .cs-list + .cs-list { margin-top: 4px; } +#cost-settings-modal .cs-pricelist ul { margin: 4px 0 0; padding-left: 18px; max-height: 120px; overflow-y: auto; } +#cost-settings-modal .cs-pricelist li.err { color: var(--danger); } #new-overlay { position: fixed; inset: 0; background: rgba(33, 33, 33, .45); z-index: 500; @@ -438,12 +432,6 @@ body { display: flex; flex-direction: column; } .new-option:hover:not(.disabled) { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); } .new-option.disabled { opacity: .4; cursor: not-allowed; } -/* ============================================================ - VLOER-EDITOR (floor.js) — hergebruikt bestaande klassen als - .opening-card/.add-opening-btn/.global-warning/.controls-grid/.control-group/ - .tab-bar/.tab-btn/.tab-content/.result-row/.highlight/.divider (identiek - aan de wand-editor, dus geen dubbele stijl-definities nodig). - ============================================================ */ #floor-app { display: flex; width: 100%; height: 100%; } #floor-left-column { flex: 1; display: flex; flex-direction: column; height: 100%; min-height: 0; min-width: 0; @@ -462,10 +450,6 @@ body { display: flex; flex-direction: column; } } #floor-specs-panel-3d .controls-grid { grid-template-columns: 1fr 1fr; } -/* ============================================================ - WONING-EDITOR (house/house.js) — navigatieboom links, actief onderdeel - rechts (overzicht of de ECHTE wand-/vloer-editor in #house-part-host). - ============================================================ */ #house-app { display: flex; width: 100%; height: 100%; } #house-nav { width: 240px; flex: none; display: flex; flex-direction: column; gap: 6px; @@ -497,8 +481,6 @@ body { display: flex; flex-direction: column; } #house-part-host > #floor-app, #house-part-host > #house-overview { flex: 1; min-height: 0; height: auto; } -/* Overzicht: plattegrond en 3D over de volle breedte (smal: onder elkaar); - bediening in de ribbon. Tabblad Rapport: rapport i.p.v. plattegrond. */ #house-overview { display: flex; width: 100%; } .ho-views { flex: 1; display: flex; min-width: 0; min-height: 0; } .ho-pane { flex: 1; position: relative; min-height: 0; min-width: 0; background: var(--viewport-bg); } @@ -527,7 +509,6 @@ body { display: flex; flex-direction: column; } .io-btn-danger:hover { border-color: var(--danger); color: var(--danger); background: var(--danger-soft); } .control-group input:disabled { opacity: .5; cursor: not-allowed; background: var(--border-soft); } -/* --- Weergave-filter (core/view-filter.js): dropdown met vinkjes op de 3D-weergave --- */ .view-filter { position: relative; } .vf-menu { position: absolute; right: 0; top: 100%; margin-top: 6px; min-width: 210px; display: none; @@ -537,10 +518,6 @@ body { display: flex; flex-direction: column; } .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; } -/* ============================================================ - 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 { @@ -552,7 +529,7 @@ body { display: flex; flex-direction: column; } .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 { +.ui-control input[type=number], .ui-control input[type=text], .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; } @@ -610,10 +587,8 @@ body { display: flex; flex-direction: column; } .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 #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; } @@ -634,44 +609,21 @@ h1 { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: .vf-option.active { color: var(--primary-on-dark); font-weight: 600; } .vf-menu .vf-option:hover { color: var(--primary-on-dark); } -/* Voorvertoning steen trasraam (woning > Gevel) */ .hs-stone-preview { display: block; width: 128px; height: auto; margin: 4px 0 8px; border: 1px solid var(--border, #ccc); border-radius: 3px; } -/* Lopen (first person, derde weergavestand): uitleg onderin de 3D. */ -.walk-hint { display: none; position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%); padding: 6px 12px; border-radius: 4px; +.walk-hint { display: none; position: absolute; left: 50%; top: 14px; transform: translateX(-50%); padding: 6px 12px; border-radius: 4px; background: rgba(0, 0, 0, 0.6); color: #fff; font-size: 12px; pointer-events: none; white-space: nowrap; z-index: 5; } .walk-hint.show { display: block; } +.walk-prompt { display: none; position: absolute; left: 50%; top: 58%; transform: translateX(-50%); padding: 5px 11px; border-radius: 4px; + background: rgba(0, 0, 0, 0.65); color: #fff; font-size: 13px; font-weight: 600; pointer-events: none; white-space: nowrap; z-index: 5; } +.walk-prompt.show { display: block; } -/* Easter egg: schieten (Ctrl+L) */ -.shooter-hud { display: none; position: absolute; inset: 0; pointer-events: none; z-index: 6; font-family: monospace; color: #ffd27a; } -.shooter-hud.show { display: block; } -.shooter-cross { position: absolute; left: 50%; top: 50%; width: 18px; height: 18px; margin: -9px 0 0 -9px; - background: linear-gradient(#ffd27a, #ffd27a) center/2px 100% no-repeat, linear-gradient(#ffd27a, #ffd27a) center/100% 2px no-repeat; opacity: .85; } -.shooter-weapon { position: absolute; right: 18px; bottom: 16px; font-size: 18px; font-weight: 700; text-shadow: 0 0 4px #000; } -.shooter-credit { position: absolute; left: 14px; top: 12px; font-size: 11px; color: #fff; opacity: .7; text-shadow: 0 0 3px #000; } -.shooter-kills { position: absolute; right: 18px; top: 12px; font-size: 18px; font-weight: 700; text-shadow: 0 0 4px #000; } -.shooter-shout { position: absolute; left: 50%; top: 22%; transform: translateX(-50%); font-size: 20px; font-weight: 700; color: #fff; - text-shadow: 0 0 4px #000, 2px 2px 0 #8040a0; opacity: 0; transition: opacity .2s; white-space: nowrap; } -.shooter-shout.show { opacity: 1; } -.shooter-ouch { position: absolute; inset: 0; background: rgba(255, 0, 0, 0.45); opacity: 0; } -.shooter-ouch.show { animation: shooter-ouch .4s ease-out; } -@keyframes shooter-ouch { from { opacity: 1; } to { opacity: 0; } } -.shooter-announce { position: absolute; left: 50%; top: 34%; transform: translateX(-50%); font: italic 900 42px Impact, 'Arial Black', sans-serif; - color: #ff2a1a; text-shadow: 0 0 8px #000, 3px 3px 0 #400; letter-spacing: 2px; white-space: nowrap; opacity: 0; } -.shooter-announce.show { opacity: 1; animation: shooter-announce .35s ease-out; } -@keyframes shooter-announce { from { transform: translateX(-50%) scale(2.2); opacity: 0; } to { transform: translateX(-50%) scale(1); opacity: 1; } } -.shooter-help { position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%); padding: 6px 12px; border-radius: 4px; - background: rgba(0, 0, 0, 0.6); color: #fff; font-size: 12px; white-space: nowrap; } - - -/* Openingen: "Volg dakhelling" (schuine bovenzijde) */ .opening-roof-btn { display: inline-flex; align-items: center; gap: 5px; margin-top: 2px; padding: 4px 8px; font-size: 11.5px; background: var(--surface); border: 1px solid var(--border); color: var(--primary); border-radius: 4px; cursor: pointer; } .opening-roof-btn:hover { background: var(--primary-soft); border-color: var(--primary); } -/* Inspector: kruimelpad, schuiven, kleurstaal. */ .ui-crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; font-size: 11px; color: var(--text-faint); margin-bottom: 4px; } .ui-crumbs .bi { font-size: 9px; } .ui-crumbs span:last-child { color: var(--text-muted); } @@ -682,7 +634,6 @@ h1 { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: .ui-range input[type=number] { width: 84px; } .ui-swatch { height: 14px; border-radius: 3px; border: 1px solid var(--border); margin: 2px 0 6px; } -/* Bouwfase (core/build-stages.js): schuif linksonder in de 3D. */ .stage-bar { position: absolute; left: 15px; bottom: 14px; z-index: 10; min-width: 300px; max-width: calc(100% - 30px); background: rgba(0, 0, 0, 0.75); border: 1px solid #444; border-radius: 6px; padding: 7px 12px 6px; color: #eee; font-size: 12px; @@ -698,10 +649,7 @@ h1 { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: .stage-ticks span.on { color: var(--primary-on-dark); } .stage-extra { display: none; margin-top: 6px; padding-top: 6px; border-top: 1px solid #444; } .stage-extra.show { display: flex; } -.walking .stage-bar { display: none; } -/* Grafieken in het rapport (staafdiagrammen, HTML). Categorische kleuren - in vaste volgorde: vloer, wanden, dak. */ .viz { --series-1: #2a78d6; --series-2: #eb6834; --series-3: #1baf7a; --track: #eef1f5; } .viz-legend { display: flex; flex-wrap: wrap; gap: 12px; font-size: 11px; color: var(--text-muted); margin: -2px 0 8px; } .viz-legend span { display: inline-flex; align-items: center; gap: 5px; } @@ -722,9 +670,6 @@ h1 { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: .viz-val { font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text); text-align: right; white-space: nowrap; } .viz-note { font-size: 11px; color: var(--text-faint); line-height: 1.45; margin-top: 6px; } -/* ============================================================ - RIBBON (core/ribbon.js): tabbladen + groepen van 3 rijen hoog. - ============================================================ */ #ribbon { flex: none; background: var(--van-emous-blue); border-bottom: 1px solid var(--border); position: relative; z-index: 100; user-select: none; } .rb-top { display: flex; align-items: flex-end; height: 34px; padding: 0 8px; } .rb-brand { display: flex; align-items: center; height: 100%; padding: 0 14px 0 4px; font-weight: 700; color: var(--primary); letter-spacing: .5px; } @@ -769,11 +714,10 @@ h1 { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: .rb-kv small { font-weight: normal; color: var(--text-faint); margin-left: 2px; } .rb-note { grid-row: span 3; max-width: 260px; font-size: 11px; line-height: 1.4; color: var(--text-muted); white-space: normal; align-self: center; } .rb-note.rb-warn { color: var(--danger); } -/* Velden (UI-kit) compact in de ribbon: label links, invoer rechts, één rij. */ .rb-group .ui-field { min-height: 0; height: 26px; gap: 8px; } .rb-group .ui-field label { font-size: 11.5px; color: var(--text-muted); white-space: nowrap; } .rb-group .ui-control input[type=number] { width: 70px; padding: 2px 5px; font-size: 12px; } -.rb-group .ui-control select { width: 132px; padding: 2px 4px; font-size: 12px; } +.rb-group .ui-control select, .rb-group .ui-control input[type=text] { width: 132px; padding: 2px 4px; font-size: 12px; } .rb-group .ui-control input[type=color] { width: 46px; height: 22px; padding: 0; border: 1px solid var(--border); border-radius: 3px; background: none; } .rb-group .ui-unit { width: 20px; margin-left: 3px; font-size: 10.5px; } .rb-group .ui-switch { margin-right: 0; } @@ -792,7 +736,6 @@ h1 { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: .rb-check { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text); cursor: pointer; white-space: nowrap; } .rb-check input { accent-color: var(--primary); } -/* Ribbon: hulpklassen voor de woning-tabbladen. */ .rb-pair { display: flex; align-items: center; gap: 6px; } .rb-cols { grid-row: span 3; display: grid; grid-auto-flow: column; grid-template-rows: repeat(3, 26px); column-gap: 18px; align-items: center; } .rb-cols:empty { display: none; } diff --git a/index.html b/index.html index 157726a..93e0a0c 100644 --- a/index.html +++ b/index.html @@ -51,6 +51,10 @@

Nieuw

+
`; } -// Ribbon (core/ribbon.js): app-tabbladen Bestand, Beeld en Extra; de -// editor voegt er zijn eigen tabbladen aan toe. Knoppen die de huidige -// editor niet ondersteunt staan uit. function updateEditMenuState() { Ribbon.refresh(); } @@ -61,6 +58,7 @@ function appRibbonTabs() { big('Openen', 'folder2-open', `data-app="triggerImport" title="JSON-ontwerp openen" ${disabledUnless('triggerImport')}`) + big('Opslaan', 'save', `data-app="exportDesign" title="Opslaan als JSON + PDF (Ctrl+S)" ${disabledUnless('exportDesign')}`) }, { label: 'Snel nieuw', html: + small('Woning Amstel', 'house-heart', 'data-app="new-kind" data-kind="woning" data-template="amstel"') + small('Woning', 'house', 'data-app="new-kind" data-kind="woning" data-template="single"') + small('Woning L-vorm', 'bounding-box-circles', 'data-app="new-kind" data-kind="woning" data-template="L"') + small('Los: muur', 'bricks', 'data-app="new-kind" data-kind="muur"') + @@ -127,7 +125,7 @@ function switchToTab(tabId) { } function openNew(kind, template) { - if (!editorModuleFor(kind)) return; // 'dak' nog niet beschikbaar + if (!editorModuleFor(kind)) return; const oldTab = tabs.find(t => t.id === activeTabId); if (oldTab && currentEditor && typeof currentEditor.getStateJson === 'function') { @@ -196,7 +194,6 @@ function wireTabBar() { }); } -// Minimale markdown-weergave voor de licentietekst (koppen + alinea's). function renderLicenseHtml(md) { const esc = (t) => t.replace(/&/g, '&').replace(//g, '>'); return md.trim().split(/\n\s*\n/).map(block => { @@ -248,12 +245,6 @@ function wireHotkeys() { openNewModal(); return; } - // Easter egg (shooter); e.code omdat Alt/Shift e.key per toetsenbord verandert. - if (e.ctrlKey && e.shiftKey && e.altKey && e.code === 'KeyL') { - e.preventDefault(); - if (currentEditor && typeof currentEditor.toggleShooter === 'function') currentEditor.toggleShooter(); - return; - } if (inField || e.ctrlKey || e.metaKey || e.altKey) return; if (e.key.toLowerCase() === 'f') { @@ -279,14 +270,13 @@ document.addEventListener('DOMContentLoaded', () => { wireNewModal(); wireHotkeys(); 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 = 'Tectum is geopend als bestand. Start het via een webserver: ./start.sh of python3 -m http.server, en open http://localhost:8000. De omgeving (Dag/Nacht) werkt anders niet.'; document.body.appendChild(note); } - openNew('woning', 'L'); + openNew('woning', 'amstel'); setTimeout(() => { const splash = document.getElementById('splash-screen'); diff --git a/js/core/build-stages.js b/js/core/build-stages.js index 54b68ca..c224db6 100644 --- a/js/core/build-stages.js +++ b/js/core/build-stages.js @@ -4,26 +4,6 @@ * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ -// ============================================================ -// core/build-stages.js — Bouwfasen (window.BuildStages): de volgorde waarin -// een unit in de hal wordt opgebouwd. Eén begrip voor zowel de weergave -// (schuif "Bouwfase" in de 3D) als het weglaten van verborgen delen: -// -// 0 regelwerk vloer-, wand- en dakbalken -// 1 elektra installatiebuizen, dozen en meterkast (ElektraModel) -// 2 beplating spaanplaat binnen, OSB vloer/plafond/dakbeschot, cementplaat buiten -// 3 doek vochtdoek -// 4 latwerk kruislat, trasraamplaat -// 5 gevel rabat, steen, kozijnen, dakbedekking (dakleer, riet, felsplaten), sandwichpanelen -// 6 afwerking dakrand, verlichting, vloerafwerking, verf binnen -// -// Fasen zijn cumulatief: fase n toont alles t/m n. Een laag die in de -// gekozen fase aan beide zijden is ingesloten door latere lagen wordt niet -// gebouwd (bijv. stijlen tussen spaanplaat en doek, latten achter de -// rabat). De specificaties rekenen altijd met alle lagen. -// -// Plain script (geen ES-module); het model-deel heeft geen DOM nodig. -// ============================================================ window.BuildStages = (() => { const LIST = [ @@ -38,14 +18,9 @@ window.BuildStages = (() => { const FINAL = LIST.length - 1; const INDEX = Object.fromEntries(LIST.map((s, i) => [s.key, i])); - // Fase (getal of undefined = afgewerkt) normaliseren. const clamp = (stage) => (typeof stage === 'number' && isFinite(stage)) ? Math.max(0, Math.min(FINAL, Math.round(stage))) : FINAL; - // Is laag `key` zichtbaar in fase `stage`? const shows = (stage, key) => clamp(stage) >= INDEX[key]; - // Schuif onderin een 3D-weergave (DOM). pane = de weergave (position: - // relative); onChange(stage) bij elke stap. Geeft { set(stage), el, - // extra } terug; extra = vrije rij onder de schuif (bijv. doorsnede). function createBar(pane, stage, onChange) { const el = document.createElement('div'); el.className = 'stage-bar'; @@ -75,8 +50,10 @@ window.BuildStages = (() => { sync(); onChange(clamp(Number(range.value))); }); - // Toetsen in de weergave mogen de schuif niet bedienen (T, R, ...). + // T, R, ... zijn sneltoetsen van de weergave, niet van de schuif. range.addEventListener('keydown', (e) => { if (!/^Arrow|Home|End/.test(e.key)) range.blur(); }); + // Na slepen focus loslaten, zodat de pijltoetsen weer naar de weergave (lopen) gaan. + el.addEventListener('pointerup', (e) => { if (e.target.matches('input[type=range]')) setTimeout(() => e.target.blur(), 0); }); sync(); return { el, extra: el.querySelector('.stage-extra'), set: (s) => { range.value = clamp(s); sync(); } }; } diff --git a/js/core/camera-memory.js b/js/core/camera-memory.js index a0d378d..dd119e1 100644 --- a/js/core/camera-memory.js +++ b/js/core/camera-memory.js @@ -4,14 +4,6 @@ * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ -// ============================================================ -// core/camera-memory.js — onthoudt de 3D-camerastand (positie + draaipunt) -// per weergave, zodat wisselen tussen onderdelen/tabbladen of een her-mount -// na een wijziging niet terugspringt naar de standaardcamera. -// Sleutels komen van de aanroeper (bijv. "tab3:wall:A:voor"). Alleen in het -// geheugen van deze sessie. -// Plain script (geen ES-module). -// ============================================================ window.CameraMemory = (() => { const store = new Map(); @@ -21,7 +13,6 @@ window.CameraMemory = (() => { store.set(key, { position: camera.position.toArray(), target: controls.target.toArray() }); } - // true als er een opgeslagen stand was (en die is toegepast). function restore(key, camera, controls) { const m = key && store.get(key); if (!m) return false; diff --git a/js/core/canvas-labels.js b/js/core/canvas-labels.js index 4d7255f..420a4bb 100644 --- a/js/core/canvas-labels.js +++ b/js/core/canvas-labels.js @@ -4,17 +4,9 @@ * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ -// ============================================================ -// core/canvas-labels.js — gedeelde stijl voor maat-labels in de 2D-canvassen -// (blauwe maatlijn + "X: 6000mm"-tekst, zoals het wand-vooraanzicht). Gebruikt -// door wand, vloer en unit, zodat alle labels er hetzelfde uitzien. -// Plain script (geen ES-module). -// ============================================================ const CANVAS_LABEL_COLOR = '#4da6ff'; -// Tekent een maatlijn van (x1,y1) naar (x2,y2) plus tekst op (textX,textY). -// Een verticale lijn (x1 === x2) krijgt de tekst 90° gedraaid. function drawCanvasDim(ctx, x1, y1, x2, y2, text, textX, textY) { ctx.save(); ctx.strokeStyle = CANVAS_LABEL_COLOR; diff --git a/js/core/geometry.js b/js/core/geometry.js index 1ce935f..14e201d 100644 --- a/js/core/geometry.js +++ b/js/core/geometry.js @@ -4,20 +4,8 @@ * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ -// ============================================================ -// core/geometry.js — gedeelde, pure math. Geen DOM, geen THREE-afhankelijkheid -// behalve shapeAreaWithHoles (die werkt met elk object dat .getPoints()/.holes -// heeft, dus ook met THREE.Shape). Plain