diff --git a/README.md b/README.md index 7e7162d..18c02e8 100644 --- a/README.md +++ b/README.md @@ -350,7 +350,9 @@ assets/ logo, texturen, skybox Beperkte configurator voor de websitepagina van één model (later in de HUGO-build; nu los via `light.html?model=`). Inbedden: de stylesheets en scripts uit `light.html` plus `
` (mount vanzelf), of -`TectumLight.mount(el, { model, frames, url, modelViewer })`. +`TectumLight.mount(el, { model, frames, url, modelViewer, save })`. De engine hoeft niet naast de pagina te staan: +texturen, kleurstalen en `kosten/*.csv` laden via `tectumUrl()` (`js/core/geometry.js`) vanaf de map van de scripts +(bijv. `/configurator/` op een modelpagina van de site); paden in de data blijven relatief. - **Rekent als de configurator** (zelfde `HouseModel.computeParts`, totalen en `CostModel`), maar toont alleen: indicatieve prijs incl. btw (afgerond op € 100), buitenmaten, bouwhoogte, bruto/leefbaar m², @@ -393,6 +395,10 @@ plus `
` (mount vanzelf), of kosten) en als laatste het stappenplan van de site ("Hoe verloopt het traject?") met een blok over maatwerk met de eigen software (keuken op maat, indeling, ramen en deuren). Contact en stappen staan in `LightPdf.COMPANY` en `STEPS`. +- **Eigen opslaan** (`save: { label, icon, onSave }`): vervangt de PDF-knop, bijv. "Ontwerp aanvragen" op de site. + `onSave` krijgt `{ design, label, key, info, choices, url }`: `design` is de house-design-JSON zoals de + configurator die opslaat (de CRM maakt daar het gekozen model van), `info` de kerncijfers met prijs, `choices` de + keuzes in woorden. Zonder `save` (en via `el.tectumLight.design()`) hetzelfde ontwerp. - **AR / schermvullend:** knop rechtsboven in de weergave. Op een telefoon of tablet: het ontwerp als GLB (`GlbWriter`, afgewerkte woning zoals in 3D, één primitive per materiaal, texturen als JPEG/PNG tot 1024 px, lichtbundels weg; maaiveld onder het midden = oorsprong, `HouseModel.groundOrigin`) in model-viewer, over de diff --git a/js/app.js b/js/app.js index 5aa60bc..9503544 100644 --- a/js/app.js +++ b/js/app.js @@ -4,7 +4,7 @@ * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ -const APP_VERSION = '0.7.7'; +const APP_VERSION = '0.7.8'; // 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang. let tabs = []; diff --git a/js/core/geometry.js b/js/core/geometry.js index 14e201d..e0da138 100644 --- a/js/core/geometry.js +++ b/js/core/geometry.js @@ -6,6 +6,17 @@ */ // Plain script, geen ES-module: imports breken bij file:// (CORS). +// Map van de engine (met assets/ en kosten/), afgeleid van de plek van dit script. Texturen, kleurstalen en de +// kosten-CSV's laden via tectumUrl(), zodat ze ook werken als de engine in een andere pagina staat (light-versie op +// een modelpagina van de site). Paden in de data blijven relatief. +const TECTUM_BASE = (() => { + const s = typeof document !== 'undefined' && document.currentScript && document.currentScript.src; + return s ? new URL('../../', s).href : ''; +})(); +function tectumUrl(path) { + return path && !/^(?:[a-z][a-z0-9+.-]*:|\/)/i.test(path) ? TECTUM_BASE + path : path; +} + function calculateSegmentPositions(spanWidth, minSp, maxSp) { if (spanWidth <= 0) return []; let numBays = Math.ceil(spanWidth / maxSp); diff --git a/js/core/skybox.js b/js/core/skybox.js index b541501..d908c6e 100644 --- a/js/core/skybox.js +++ b/js/core/skybox.js @@ -23,7 +23,7 @@ window.Skybox = (() => { const cache = {}; function loadTexture(url) { if (!cache[url]) { - cache[url] = new Promise((resolve, reject) => new THREE.TextureLoader().load(url, resolve, undefined, reject)); + cache[url] = new Promise((resolve, reject) => new THREE.TextureLoader().load(tectumUrl(url), resolve, undefined, reject)); cache[url].catch(() => { delete cache[url]; }); } return cache[url]; diff --git a/js/cost/cost-csv.js b/js/cost/cost-csv.js index 5bc5750..e522985 100644 --- a/js/cost/cost-csv.js +++ b/js/cost/cost-csv.js @@ -114,7 +114,7 @@ window.CostCsv = (() => { } function loadList(list) { const url = LISTS[list].url; - return fetch(url, { cache: 'no-store' }) + return fetch(tectumUrl(url), { cache: 'no-store' }) .then(r => r.ok ? r.text().then(t => apply(list, t, 'server', url, r.headers.get('last-modified') || new Date().toISOString())) : null) .catch(() => null); } diff --git a/js/floor/floor-model.js b/js/floor/floor-model.js index 3eb7cb9..9239258 100644 --- a/js/floor/floor-model.js +++ b/js/floor/floor-model.js @@ -293,7 +293,7 @@ window.FloorModel = (() => { } function finishThumbUrl(key, px) { const f = FINISHES[key]; - return f && f.file ? f.file.replace(/[^/]+$/, (n) => `thumb/${px || 64}/` + n.replace(/\.jpg$/, '.png')) : null; + return f && f.file ? tectumUrl(f.file.replace(/[^/]+$/, (n) => `thumb/${px || 64}/` + n.replace(/\.jpg$/, '.png'))) : null; } const finishMaterials = {}; function finishMaterial(key) { @@ -307,7 +307,7 @@ window.FloorModel = (() => { } const mat = new THREE.MeshStandardMaterial({ color: 0xb89a7c, roughness: 0.7 }); if (typeof document !== 'undefined' && THREE.TextureLoader) { - new THREE.TextureLoader().load(f.file, (tex) => { + new THREE.TextureLoader().load(tectumUrl(f.file), (tex) => { tex.wrapS = tex.wrapT = THREE.RepeatWrapping; tex.repeat.set(1000 / f.tileMM, 1000 / f.tileMM); tex.anisotropy = 8; diff --git a/js/light/light.js b/js/light/light.js index ce98d87..9100309 100644 --- a/js/light/light.js +++ b/js/light/light.js @@ -8,8 +8,11 @@ // Light-versie voor de modelpagina's: één model in 3D, een paar keuzes en de kerncijfers; opslaan = PDF met // aanzichten en plattegrond. Alle logica in LightModel (en daarachter de gewone modellen). // Inbedden:
(vanzelf gemount), of -// TectumLight.mount(el, { model, frames, url, modelViewer }); url = adres voor de QR-code in de PDF (standaard de +// TectumLight.mount(el, { model, frames, url, modelViewer, save }); url = adres voor de QR-code in de PDF (standaard de // modelpagina), modelViewer = adres van model-viewer (standaard js/vendor naast dit script). +// save = { label, icon, onSave(ontwerp) } vervangt de PDF-knop (bijv. ontwerp aanvragen op de site); onSave krijgt +// { design, label, key, info, choices, url }: design = house-design-JSON zoals de configurator die opslaat, choices = de +// keuzes in woorden (zoals op het voorblad van de PDF). Zonder save blijft het de PDF. // Knop rechtsboven in de weergave: op een telefoon/tablet het ontwerp als GLB in model-viewer, met AR (WebXR, Scene // Viewer, Quick Look); op de desktop schermvullend. window.TectumLight = (() => { @@ -501,7 +504,8 @@ window.TectumLight = (() => { kv('Inhoud', `${nl(i.inhoudM3, 0)} m³`) + kv('Gewicht', `${nl(Math.round(i.kg / 10) * 10, 0)} kg`); $('.tl-buy').innerHTML = `
${CostModel.eur(Math.round(i.price / 100) * 100)}indicatief, incl. btw
- `; + ${o.save ? `` + : ''}`; } // Keuzes (knoppen en kleuren). @@ -542,6 +546,7 @@ window.TectumLight = (() => { const a = e.target.closest('[data-act]'); if (!a || !cur) return; if (a.dataset.act === 'pdf') savePdf(); + else if (a.dataset.act === 'save') o.save.onSave(design()); else if (a.dataset.act === 'reset') tryApply(clone(model.initial)); }); panel.addEventListener('keydown', (e) => { @@ -608,6 +613,18 @@ window.TectumLight = (() => { } const pageUrl = o.url || root.dataset.url || LightPdf.modelUrl(model.key); + // Het huidige ontwerp voor buiten de tool (save.onSave, el.tectumLight.design()). + function design() { + const st = clone(cur.st); + st.project.naam = model.label; + st.project.opmerking = 'Ontwerp uit de light-versie'; + return { + design: Object.assign({ type: 'house-design', version: 1, exportedAt: new Date().toISOString() }, st), + label: model.label, key: model.key, info: Object.assign({}, cur.info), + choices: LightModel.describe(model, ch, cur.st), url: pageUrl + }; + } + function savePdf() { const st = clone(cur.st); st.project.naam = model.label; @@ -768,6 +785,7 @@ window.TectumLight = (() => { root.innerHTML = ''; }, getChoices: () => clone(ch), + design: () => cur && design(), // GLB van het huidige ontwerp: { blob, url, stats } (zelfde als voor AR). glb: () => cur && makeGlb() }; diff --git a/js/roof/roof-model.js b/js/roof/roof-model.js index 83d4c2f..48a51ad 100644 --- a/js/roof/roof-model.js +++ b/js/roof/roof-model.js @@ -90,7 +90,7 @@ window.RoofModel = (() => { } function coveringThumbUrl(key, px) { const c = COVERINGS[key]; - return c && c.file ? c.file.replace(/[^/]+$/, (f) => `thumb/${px || 64}/` + f.replace(/\.jpg$/, '.png')) : null; + return c && c.file ? tectumUrl(c.file.replace(/[^/]+$/, (f) => `thumb/${px || 64}/` + f.replace(/\.jpg$/, '.png'))) : null; } function coveringFor(key, shapeType) { const c = COVERINGS[key]; @@ -108,7 +108,7 @@ window.RoofModel = (() => { if (!c.file) return null; const mat = new THREE.MeshStandardMaterial({ color: 0x9c8466, roughness: 0.95 }); if (typeof document !== 'undefined' && THREE.TextureLoader) { - new THREE.TextureLoader().load(c.file, (tex) => { + new THREE.TextureLoader().load(tectumUrl(c.file), (tex) => { tex.wrapS = tex.wrapT = THREE.RepeatWrapping; tex.repeat.set(1000 / c.tileMM, 1000 / c.tileMM); tex.anisotropy = 8; diff --git a/js/wall/wall-model.js b/js/wall/wall-model.js index b894b22..cd09372 100644 --- a/js/wall/wall-model.js +++ b/js/wall/wall-model.js @@ -234,8 +234,8 @@ window.WallModel = (() => { 'R961': '#c7b8a2', 'R970': '#ecdeb1', 'R980': '#ae9c88', 'R985': '#cd8b6d', 'R991': '#aa8274' }; const isStoneFinish = (finish) => TRASRAAM_STONES.includes(finish); - const stoneTextureUrl = (code) => STONE.DIR + encodeURIComponent(code + ' - Witte voeg.jpg'); - const stoneThumbUrl = (code, px) => STONE.DIR + 'thumb/' + (px || 64) + '/' + encodeURIComponent(code + ' - Witte voeg.png'); + const stoneTextureUrl = (code) => tectumUrl(STONE.DIR + encodeURIComponent(code + ' - Witte voeg.jpg')); + const stoneThumbUrl = (code, px) => tectumUrl(STONE.DIR + 'thumb/' + (px || 64) + '/' + encodeURIComponent(code + ' - Witte voeg.png')); // UV's in meters, dus repeat = 1/tegel. const stoneMaterials = {}; function stoneMaterial(code) {