v0.7.8: light-versie in te bedden op elke pagina en eigen opslaan: texturen, kleurstalen en kosten/*.csv laden via tectumUrl() (js/core/geometry.js) vanaf de map van de scripts, zodat de engine niet naast de pagina hoeft te staan (modelpagina's van de site met de engine op /configurator/); paden in de data blijven relatief; TectumLight.mount(el, { save: { label, icon, onSave } }) vervangt de PDF-knop, onSave krijgt het ontwerp als house-design-JSON met kerncijfers, prijs en keuzes in woorden (ook el.tectumLight.design())
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b8cc39275b
commit
73248a69c9
@@ -350,7 +350,9 @@ assets/ logo, texturen, skybox
|
|||||||
Beperkte configurator voor de websitepagina van één model (later in de HUGO-build; nu los via
|
Beperkte configurator voor de websitepagina van één model (later in de HUGO-build; nu los via
|
||||||
`light.html?model=<sleutel uit HouseModel.MODELS>`). Inbedden: de stylesheets en scripts uit `light.html`
|
`light.html?model=<sleutel uit HouseModel.MODELS>`). Inbedden: de stylesheets en scripts uit `light.html`
|
||||||
plus `<div class="tectum-light" data-model="amstel"></div>` (mount vanzelf), of
|
plus `<div class="tectum-light" data-model="amstel"></div>` (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
|
- **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²,
|
alleen: indicatieve prijs incl. btw (afgerond op € 100), buitenmaten, bouwhoogte, bruto/leefbaar m²,
|
||||||
@@ -393,6 +395,10 @@ plus `<div class="tectum-light" data-model="amstel"></div>` (mount vanzelf), of
|
|||||||
kosten) en als laatste het stappenplan van de site ("Hoe verloopt het traject?") met een blok over maatwerk
|
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
|
met de eigen software (keuken op maat, indeling, ramen en deuren). Contact en stappen staan
|
||||||
in `LightPdf.COMPANY` en `STEPS`.
|
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
|
- **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,
|
(`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
|
lichtbundels weg; maaiveld onder het midden = oorsprong, `HouseModel.groundOrigin`) in model-viewer, over de
|
||||||
|
|||||||
@@ -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.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.
|
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
||||||
|
|
||||||
let tabs = [];
|
let tabs = [];
|
||||||
|
|||||||
@@ -6,6 +6,17 @@
|
|||||||
*/
|
*/
|
||||||
// Plain script, geen ES-module: imports breken bij file:// (CORS).
|
// 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) {
|
function calculateSegmentPositions(spanWidth, minSp, maxSp) {
|
||||||
if (spanWidth <= 0) return [];
|
if (spanWidth <= 0) return [];
|
||||||
let numBays = Math.ceil(spanWidth / maxSp);
|
let numBays = Math.ceil(spanWidth / maxSp);
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ window.Skybox = (() => {
|
|||||||
const cache = {};
|
const cache = {};
|
||||||
function loadTexture(url) {
|
function loadTexture(url) {
|
||||||
if (!cache[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]; });
|
cache[url].catch(() => { delete cache[url]; });
|
||||||
}
|
}
|
||||||
return cache[url];
|
return cache[url];
|
||||||
|
|||||||
+1
-1
@@ -114,7 +114,7 @@ window.CostCsv = (() => {
|
|||||||
}
|
}
|
||||||
function loadList(list) {
|
function loadList(list) {
|
||||||
const url = LISTS[list].url;
|
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)
|
.then(r => r.ok ? r.text().then(t => apply(list, t, 'server', url, r.headers.get('last-modified') || new Date().toISOString())) : null)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ window.FloorModel = (() => {
|
|||||||
}
|
}
|
||||||
function finishThumbUrl(key, px) {
|
function finishThumbUrl(key, px) {
|
||||||
const f = FINISHES[key];
|
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 = {};
|
const finishMaterials = {};
|
||||||
function finishMaterial(key) {
|
function finishMaterial(key) {
|
||||||
@@ -307,7 +307,7 @@ window.FloorModel = (() => {
|
|||||||
}
|
}
|
||||||
const mat = new THREE.MeshStandardMaterial({ color: 0xb89a7c, roughness: 0.7 });
|
const mat = new THREE.MeshStandardMaterial({ color: 0xb89a7c, roughness: 0.7 });
|
||||||
if (typeof document !== 'undefined' && THREE.TextureLoader) {
|
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.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||||
tex.repeat.set(1000 / f.tileMM, 1000 / f.tileMM);
|
tex.repeat.set(1000 / f.tileMM, 1000 / f.tileMM);
|
||||||
tex.anisotropy = 8;
|
tex.anisotropy = 8;
|
||||||
|
|||||||
+20
-2
@@ -8,8 +8,11 @@
|
|||||||
// Light-versie voor de modelpagina's: één model in 3D, een paar keuzes en de kerncijfers; opslaan = PDF met
|
// 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).
|
// aanzichten en plattegrond. Alle logica in LightModel (en daarachter de gewone modellen).
|
||||||
// Inbedden: <div class="tectum-light" data-model="amstel" data-url="…"></div> (vanzelf gemount), of
|
// Inbedden: <div class="tectum-light" data-model="amstel" data-url="…"></div> (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).
|
// 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
|
// 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.
|
// Viewer, Quick Look); op de desktop schermvullend.
|
||||||
window.TectumLight = (() => {
|
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`);
|
kv('Inhoud', `${nl(i.inhoudM3, 0)} m³`) + kv('Gewicht', `${nl(Math.round(i.kg / 10) * 10, 0)} kg`);
|
||||||
$('.tl-buy').innerHTML = `<div class="tl-price"><span class="tl-price-val">${CostModel.eur(Math.round(i.price / 100) * 100)}</span><span class="tl-price-note">indicatief, incl. btw</span></div>
|
$('.tl-buy').innerHTML = `<div class="tl-price"><span class="tl-price-val">${CostModel.eur(Math.round(i.price / 100) * 100)}</span><span class="tl-price-note">indicatief, incl. btw</span></div>
|
||||||
<button type="button" class="tl-btn tl-icon" data-act="reset" title="Alle keuzes terug naar het model" aria-label="Terug naar het model"><i class="bi bi-arrow-counterclockwise"></i></button>
|
<button type="button" class="tl-btn tl-icon" data-act="reset" title="Alle keuzes terug naar het model" aria-label="Terug naar het model"><i class="bi bi-arrow-counterclockwise"></i></button>
|
||||||
<button type="button" class="tl-btn tl-primary" data-act="pdf"><i class="bi bi-file-earmark-pdf"></i>PDF</button>`;
|
${o.save ? `<button type="button" class="tl-btn tl-primary" data-act="save"><i class="bi bi-${esc(o.save.icon || 'send')}"></i>${esc(o.save.label || 'Opslaan')}</button>`
|
||||||
|
: '<button type="button" class="tl-btn tl-primary" data-act="pdf"><i class="bi bi-file-earmark-pdf"></i>PDF</button>'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keuzes (knoppen en kleuren).
|
// Keuzes (knoppen en kleuren).
|
||||||
@@ -542,6 +546,7 @@ window.TectumLight = (() => {
|
|||||||
const a = e.target.closest('[data-act]');
|
const a = e.target.closest('[data-act]');
|
||||||
if (!a || !cur) return;
|
if (!a || !cur) return;
|
||||||
if (a.dataset.act === 'pdf') savePdf();
|
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));
|
else if (a.dataset.act === 'reset') tryApply(clone(model.initial));
|
||||||
});
|
});
|
||||||
panel.addEventListener('keydown', (e) => {
|
panel.addEventListener('keydown', (e) => {
|
||||||
@@ -608,6 +613,18 @@ window.TectumLight = (() => {
|
|||||||
}
|
}
|
||||||
const pageUrl = o.url || root.dataset.url || LightPdf.modelUrl(model.key);
|
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() {
|
function savePdf() {
|
||||||
const st = clone(cur.st);
|
const st = clone(cur.st);
|
||||||
st.project.naam = model.label;
|
st.project.naam = model.label;
|
||||||
@@ -768,6 +785,7 @@ window.TectumLight = (() => {
|
|||||||
root.innerHTML = '';
|
root.innerHTML = '';
|
||||||
},
|
},
|
||||||
getChoices: () => clone(ch),
|
getChoices: () => clone(ch),
|
||||||
|
design: () => cur && design(),
|
||||||
// GLB van het huidige ontwerp: { blob, url, stats } (zelfde als voor AR).
|
// GLB van het huidige ontwerp: { blob, url, stats } (zelfde als voor AR).
|
||||||
glb: () => cur && makeGlb()
|
glb: () => cur && makeGlb()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ window.RoofModel = (() => {
|
|||||||
}
|
}
|
||||||
function coveringThumbUrl(key, px) {
|
function coveringThumbUrl(key, px) {
|
||||||
const c = COVERINGS[key];
|
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) {
|
function coveringFor(key, shapeType) {
|
||||||
const c = COVERINGS[key];
|
const c = COVERINGS[key];
|
||||||
@@ -108,7 +108,7 @@ window.RoofModel = (() => {
|
|||||||
if (!c.file) return null;
|
if (!c.file) return null;
|
||||||
const mat = new THREE.MeshStandardMaterial({ color: 0x9c8466, roughness: 0.95 });
|
const mat = new THREE.MeshStandardMaterial({ color: 0x9c8466, roughness: 0.95 });
|
||||||
if (typeof document !== 'undefined' && THREE.TextureLoader) {
|
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.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||||
tex.repeat.set(1000 / c.tileMM, 1000 / c.tileMM);
|
tex.repeat.set(1000 / c.tileMM, 1000 / c.tileMM);
|
||||||
tex.anisotropy = 8;
|
tex.anisotropy = 8;
|
||||||
|
|||||||
@@ -234,8 +234,8 @@ window.WallModel = (() => {
|
|||||||
'R961': '#c7b8a2', 'R970': '#ecdeb1', 'R980': '#ae9c88', 'R985': '#cd8b6d', 'R991': '#aa8274'
|
'R961': '#c7b8a2', 'R970': '#ecdeb1', 'R980': '#ae9c88', 'R985': '#cd8b6d', 'R991': '#aa8274'
|
||||||
};
|
};
|
||||||
const isStoneFinish = (finish) => TRASRAAM_STONES.includes(finish);
|
const isStoneFinish = (finish) => TRASRAAM_STONES.includes(finish);
|
||||||
const stoneTextureUrl = (code) => STONE.DIR + encodeURIComponent(code + ' - Witte voeg.jpg');
|
const stoneTextureUrl = (code) => tectumUrl(STONE.DIR + encodeURIComponent(code + ' - Witte voeg.jpg'));
|
||||||
const stoneThumbUrl = (code, px) => STONE.DIR + 'thumb/' + (px || 64) + '/' + encodeURIComponent(code + ' - Witte voeg.png');
|
const stoneThumbUrl = (code, px) => tectumUrl(STONE.DIR + 'thumb/' + (px || 64) + '/' + encodeURIComponent(code + ' - Witte voeg.png'));
|
||||||
// UV's in meters, dus repeat = 1/tegel.
|
// UV's in meters, dus repeat = 1/tegel.
|
||||||
const stoneMaterials = {};
|
const stoneMaterials = {};
|
||||||
function stoneMaterial(code) {
|
function stoneMaterial(code) {
|
||||||
|
|||||||
Reference in New Issue
Block a user