v0.7.0: exports voor CAD, BIM en renders (Bestand › Exporteren, los van Opslaan/Ctrl+S): CAD (DXF R12, plattegrond met wanden/sparingen/gevelbekleding/kozijnen met draaicirkel, ruimtes, inrichting, dakrand, elektra en echte maatvoering, plus vier gevels als lijntekening met verborgen lijnen weg; lagen PL-/AZ-, 1:1 mm, afgestemd op 1:50; in CAD op te slaan als DWG), BIM (IFC4 met alleen de bouwconstructie: stijlen, regels, vloer- en dakbalken, stalen nokbalk, palen, spaanplaat/OSB/cementplaat en elektra per leiding, gegroepeerd per unit en element met Pset_Tectum en kleur per soort) en Render (OBJ + MTL + texturen in een ZIP voor Blender, volledige afgewerkte woning, kleuren lineair, PBR); schoorsteen tegen een buitenwand (ChimneyModel: sparing in wand en dak met raveelwerk, steenstrip/stuc, verslepen langs de gevel); kozijnen koppelen met een eigen schakelaar; kozijnindeling (deurblad, zij-/bovenlicht, schuifbladen) gedeeld tussen 3D, plattegrond en aanzicht
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
86fcd57b96
commit
adb3c74f85
+172
-17
@@ -328,6 +328,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
const lightingMaterials = LightingModel.createMaterials();
|
||||
const sandwichMaterials = SandwichModel.createMaterials();
|
||||
const postMaterials = PostModel.createMaterials();
|
||||
const chimneyMaterials = ChimneyModel.createMaterials();
|
||||
const elektraMaterials = ElektraModel.createMaterials();
|
||||
let houseGroup = null;
|
||||
let cameraFitted = CameraMemory.restore(overviewCamKey, camera, controls);
|
||||
@@ -462,9 +463,17 @@ function mountHouseEditor(container, callbacks) {
|
||||
}
|
||||
const elektraItem = (key) => parts && parts.elektra && parts.elektra.items.find(it => it.key === key);
|
||||
|
||||
// Klik op een schoorsteen: de wand waar hij aan hangt (daar staat het schoorsteenpaneel).
|
||||
function chimneyWallHit(ch) {
|
||||
const u = parts.units.find(q => q.id === ch.unitId), w = u && u.walls.find(q => q.side === ch.side);
|
||||
const pc = w && w.pieces.find(p => !p.baseY);
|
||||
return pc ? { type: 'wall', unit: u.id, side: ch.side, element: pc.element } : null;
|
||||
}
|
||||
function hitTest(x, z) {
|
||||
const eh = elektraHit(x, z);
|
||||
if (eh) return { type: 'elektra', id: eh.key };
|
||||
const ch = ChimneyModel.hit(parts.chimneys || [], x, z, 0);
|
||||
if (ch && chimneyWallHit(ch)) return chimneyWallHit(ch);
|
||||
const iw = parts.interior.find(p => pieceHit(p.place, x, z, 4));
|
||||
if (iw) return { type: 'iw', id: iw.id };
|
||||
for (const u of parts.units) {
|
||||
@@ -562,6 +571,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
let roomHover = null;
|
||||
let fxHover = null;
|
||||
let fxDrag = null;
|
||||
let chDrag = null;
|
||||
|
||||
function setTool(t) {
|
||||
tool = t;
|
||||
@@ -681,6 +691,38 @@ function mountHouseEditor(container, callbacks) {
|
||||
refresh();
|
||||
}
|
||||
window.addEventListener('mouseup', onFixtureDrop);
|
||||
// Schoorsteen slepen: alleen langs de buitengevel.
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
if (tool !== 'select' || !parts || e.button !== 0 || elDrag || fxDrag) return;
|
||||
const r = canvas.getBoundingClientRect();
|
||||
const w = toWorld(e.clientX - r.left, e.clientY - r.top);
|
||||
const ch = ChimneyModel.hit(parts.chimneys || [], w.x, w.z, 0);
|
||||
if (ch) chDrag = { id: ch.id, sx: e.clientX, sy: e.clientY, moved: false, snap: null };
|
||||
});
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (!chDrag) return;
|
||||
if (!chDrag.moved && Math.hypot(e.clientX - chDrag.sx, e.clientY - chDrag.sy) < 4) return;
|
||||
chDrag.moved = true;
|
||||
canvas.style.cursor = 'grabbing';
|
||||
const r = canvas.getBoundingClientRect();
|
||||
const w = toWorld(e.clientX - r.left, e.clientY - r.top);
|
||||
const ch = (parts.chimneys || []).find(c => c.id === chDrag.id);
|
||||
if (ch) chDrag.snap = ChimneyModel.snapToFacade(parts.rects, w.x, w.z, ch.w);
|
||||
drawPlan();
|
||||
});
|
||||
window.addEventListener('mouseup', () => {
|
||||
if (!chDrag) return;
|
||||
const d = chDrag;
|
||||
chDrag = null;
|
||||
canvas.style.cursor = 'default';
|
||||
if (!d.moved || !d.snap) return;
|
||||
suppressClick = true;
|
||||
const cs = (state.chimneys || []).find(c => c.id === d.id);
|
||||
if (cs) Object.assign(cs, { unitId: d.snap.unitId, side: d.snap.side, u: d.snap.u });
|
||||
const u = parts.units.find(q => q.id === d.snap.unitId), wl = u && u.walls.find(q => q.side === d.snap.side), pc = wl && wl.pieces.find(p => !p.baseY);
|
||||
if (pc) selection = { type: 'wall', unit: u.id, side: d.snap.side, element: pc.element };
|
||||
refresh();
|
||||
});
|
||||
const newFixtureItem = (cx, cz, rot) => {
|
||||
const T = FixtureModel.TYPES[fixtureType];
|
||||
return { type: fixtureType, w: T.w, d: T.d, h: T.h, opts: Object.assign({}, T.opts || {}), rot: rot || 0, cx, cz };
|
||||
@@ -844,7 +886,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#8a6836'; ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
p.config.openings.forEach(op => {
|
||||
p.config.openings.filter(op => op.type !== 'schoorsteen').forEach(op => {
|
||||
polyPath(pieceCorners(p.place, -p.place.width / 2 + op.x, -p.place.width / 2 + op.x + op.w));
|
||||
ctx.fillStyle = op.overflowing ? 'rgba(255, 60, 60, 0.85)' : (op.y < p.config.plateThick ? '#1d5f99' : '#9fd0ff');
|
||||
ctx.fill();
|
||||
@@ -982,6 +1024,22 @@ function mountHouseEditor(container, callbacks) {
|
||||
const a = toPx(pp.x - pp.size / 2, pp.z - pp.size / 2), sz = pp.size * scale;
|
||||
ctx.fillRect(a.x, a.y, sz, sz); ctx.strokeRect(a.x, a.y, sz, sz);
|
||||
});
|
||||
(parts.chimneys || []).forEach(ch => {
|
||||
const path = (pts) => { ctx.beginPath(); pts.forEach((p, i) => { const q = toPx(p.x, p.z); if (i) ctx.lineTo(q.x, q.y); else ctx.moveTo(q.x, q.y); }); ctx.closePath(); };
|
||||
const sn = chDrag && chDrag.moved && chDrag.snap && chDrag.id === ch.id ? chDrag.snap : null;
|
||||
if (sn) {
|
||||
// Voorbeeld tijdens het slepen: dezelfde diepte, op de nieuwe plek.
|
||||
const S = ChimneyModel.SIDES[sn.side], W = (t, s) => S.axis === 'z' ? { x: sn.plane + S.n.x * s, z: t } : { x: t, z: sn.plane + S.n.z * s };
|
||||
path([W(sn.t - ch.w / 2, ch.sIn), W(sn.t + ch.w / 2, ch.sIn), W(sn.t + ch.w / 2, ch.sFront), W(sn.t - ch.w / 2, ch.sFront)]);
|
||||
ctx.fillStyle = 'rgba(77, 166, 255, 0.45)'; ctx.fill(); ctx.strokeStyle = '#4da6ff'; ctx.stroke();
|
||||
return;
|
||||
}
|
||||
path(ch.footprint);
|
||||
ctx.fillStyle = ch.outside.type === 'steen' ? (WallModel.STONE_COLORS[ch.outside.stone] || '#8a6a5c') : WallModel.stucHex(ch.outside.stuc);
|
||||
ctx.fill(); ctx.strokeStyle = '#2b2b2b'; ctx.stroke();
|
||||
path(ch.flueFootprint);
|
||||
ctx.fillStyle = '#1b1a19'; ctx.fill();
|
||||
});
|
||||
ctx.restore();
|
||||
|
||||
ctx.strokeStyle = '#ffe9a8'; ctx.lineWidth = 1.5;
|
||||
@@ -1130,6 +1188,9 @@ function mountHouseEditor(container, callbacks) {
|
||||
if (a === 'add-unit') addUnit();
|
||||
else if (a === 'project') openProjectProperties();
|
||||
else if (a === 'save') exportDesign();
|
||||
else if (a === 'dxf') exportDxf();
|
||||
else if (a === 'ifc') exportIfc();
|
||||
else if (a === 'obj') exportObj();
|
||||
else if (a === 'costs') CostSettings.open();
|
||||
else if (a === 'add-seams') { HouseModel.ensureSeamWalls(state, b.dataset.unit); refresh(); }
|
||||
else if (a === 'remove-unit') removeUnit(b.dataset.unit);
|
||||
@@ -1235,7 +1296,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
toggle('hs-roof-house', 'Over hele woning', hr.enabled, { hint: 'Eén dakvorm over alle units (bijv. een grote wolfskap); uit = dak per unit. De daken per unit blijven bewaard.' }) +
|
||||
|
||||
(houseRoofUi ? sel('hs-roof-cover', 'Bedekking', covOpts, RoofModel.coveringFor(state.units[0].roof.covering, shapeType), { hint: 'Voor alle units (per unit aan te passen)' }) : '') +
|
||||
toggle('hs-steel-ridge', 'Stalen nokbalk', state.units.some(u => u.roof.steelRidge !== false), { hint: 'IPE onder de nok van zadel- en wolfsdaken (alle units); uit = verwijderen' }) }
|
||||
toggle('hs-steel-ridge', 'Stalen nokbalk', state.units.some(u => u.roof.steelRidge !== false), { hint: 'IPE onder de nok van zadel- en wolfsdaken en van een dubbel lessenaardak (gelijke goot en nok, samen een zadel); alle units; uit = verwijderen' }) }
|
||||
];
|
||||
if (houseRoofUi) groups.push(...houseRoofUi.groups());
|
||||
else groups.push({ label: 'Dakvorm', html: note('Vorm, hoogtes, helling en bedekking zijn per unit: selecteer een unit.') });
|
||||
@@ -1487,7 +1548,10 @@ function mountHouseEditor(container, callbacks) {
|
||||
key: 'rapport', label: 'Rapport', order: 60,
|
||||
groups: () => [
|
||||
{ label: 'Rapport', html: note('Transport per unit, gewicht, afmetingen, kosten, hout en plaatwerk. Staat in plaats van de plattegrond zolang dit tabblad open is en loopt mee met elke wijziging.') },
|
||||
{ label: 'Uitvoer', html: big('Opslaan + PDF', 'file-earmark-pdf', 'data-ho-act="save" title="JSON en de volledige PDF (Ctrl+S)"') },
|
||||
{ label: 'Uitvoer', html: big('Opslaan + PDF', 'file-earmark-pdf', 'data-ho-act="save" title="JSON en de volledige PDF (Ctrl+S)"') +
|
||||
big('CAD (DXF)', 'vector-pen', 'data-ho-act="dxf" title="Plattegrond en gevels als DXF voor CAD (AutoCAD, BricsCAD, …), 1:1 in mm; daar op te slaan als DWG"') +
|
||||
big('BIM (IFC)', 'boxes', 'data-ho-act="ifc" title="Bouwconstructie als IFC4 (balken, platen, elektra) per element, zonder afwerking en inrichting"') +
|
||||
big('Render (OBJ)', 'badge-3d', 'data-ho-act="obj" title="Volledige afgewerkte woning als OBJ + MTL + texturen in een ZIP, voor renders in Blender"') },
|
||||
{ label: 'Instellingen', html: big('Kosten & tijden', 'calculator', 'data-ho-act="costs"') }
|
||||
],
|
||||
wire: (body) => { live = []; wireButtons(body); }
|
||||
@@ -1877,7 +1941,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
pair(sel('hp-roof-cover', 'Bedekking', covOpts, covNow, { hint: shapeNow === 'plat' ? 'Riet en felsplaten alleen bij een hellend dak' : `${c.thickMM} mm, ${String(c.kgM2).replace('.', ',')} kg/m²` }),
|
||||
thumb(RoofModel.coveringThumbUrl(covNow, 64), c.label)) +
|
||||
toggle('hp-fascia', 'Boeidelen', u.roof.fascia !== false, { hint: 'Uit = daktrim' }) +
|
||||
(pu && pu.roof.unitRoof.ridgeBeam || u.roof.steelRidge === false && ['zadel', 'wolf'].includes(shapeNow)
|
||||
(pu && pu.roof.unitRoof.ridgeBeam || u.roof.steelRidge === false && ['zadel', 'wolf', 'lessenaar'].includes(shapeNow)
|
||||
? toggle('hp-steel-ridge', 'Stalen nokbalk', u.roof.steelRidge !== false, { hint: pu && pu.roof.unitRoof.ridgeBeam ? `${pu.roof.unitRoof.ridgeBeam.profile.name}, ${Math.round(pu.roof.unitRoof.ridgeBeam.steelKg)} kg; uit = verwijderen` : 'Uit = verwijderd' }) : '') +
|
||||
toggle('hp-roof-follow', 'Maat volgt vloer', u.roof.followFloor) });
|
||||
if (u.roof.followFloor) groups.push({ label: 'Overkapping', html:
|
||||
@@ -1931,6 +1995,52 @@ function mountHouseEditor(container, callbacks) {
|
||||
};
|
||||
}
|
||||
|
||||
// Schoorstenen in het vlak van deze wand (ook als ze aan een andere unit hangen, bijv. over een naad).
|
||||
function chimneysOnWall(pu, side) {
|
||||
const r = pu.rect, plane = { voor: r.z0, achter: r.z1, links: r.x0, rechts: r.x1 }[side];
|
||||
const ids = (parts.chimneys || []).filter(c => c.side === side && Math.abs(c.plane - plane) < 1).map(c => c.id);
|
||||
return (state.chimneys || []).filter(c => ids.includes(c.id));
|
||||
}
|
||||
function chimneyGroup(pu, wl, piece, e0) {
|
||||
const list = chimneysOnWall(pu, wl.side);
|
||||
const fin = (pre, label, f, types) => sel(pre, label, types, f.type) +
|
||||
(f.type === 'steen' ? pair(sel(pre + '-stone', 'Steen', stoneOptions, f.stone), thumb(WallModel.stoneThumbUrl(f.stone, 64), f.stone)) : '') +
|
||||
(f.type === 'stuc' ? pair(sel(pre + '-stuc', 'Kleur', stucColors, f.stuc), swatchOf(WallModel.stucHex(f.stuc))) : '');
|
||||
const html = list.map(c => {
|
||||
const r = (parts.chimneys || []).find(x => x.id === c.id);
|
||||
const id = 'hw-ch' + c.id;
|
||||
return kv('Schoorsteen', `${c.id} (unit ${c.unitId})`) +
|
||||
num(id + '-u', 'Positie', c.u, { step: 10, hint: `Hart vanaf de linker vloerhoek van unit ${c.unitId}, van buitenaf gezien` }) +
|
||||
num(id + '-w', 'Breedte', c.w, { step: 10, hint: 'Buitenwerks; de wand krijgt een sparing zoals een raam' }) +
|
||||
num(id + '-out', 'Voor de gevel', c.out, { step: 10, hint: r ? `Minstens tot voorbij de dakoverstek: nu ${r.protrudes} mm voor de gevel` : '' }) +
|
||||
num(id + '-above', 'Boven het dak', c.above, { step: 50, hint: r ? `Bovenkant ${r.yTop - r.yFloor} mm boven de vloer (hoogste dakpunt + dit)` : '' }) +
|
||||
fin(id + '-out-type', 'Buiten', c.outside, ChimneyModel.OUTSIDE_TYPES) +
|
||||
fin(id + '-in-type', 'Binnen', c.inside, ChimneyModel.INSIDE_TYPES) +
|
||||
small('Schoorsteen weg', 'trash', `data-hw-ch="del" data-ch="${c.id}"`, 'rb-danger');
|
||||
}).join('');
|
||||
return { label: 'Schoorsteen', html: html + small('Schoorsteen toevoegen', 'plus-lg', `data-hw-ch="add" data-u="${Math.round(e0 + (piece.u0 + piece.u1) / 2)}"`) };
|
||||
}
|
||||
function wireChimneys(body, pu, wl) {
|
||||
chimneysOnWall(pu, wl.side).forEach(c => {
|
||||
const id = 'hw-ch' + c.id;
|
||||
UI.onNum(id + '-u', v => { c.u = Math.round(v); }, after);
|
||||
UI.onNum(id + '-w', v => { if (v >= 400) c.w = Math.round(v); }, after);
|
||||
UI.onNum(id + '-out', v => { if (v >= 0) c.out = Math.round(v); }, after);
|
||||
UI.onNum(id + '-above', v => { if (v >= 0) c.above = Math.round(v); }, after);
|
||||
[['out', c.outside], ['in', c.inside]].forEach(([k, f]) => {
|
||||
UI.onSel(`${id}-${k}-type`, v => { f.type = v; }, rerender);
|
||||
UI.onSel(`${id}-${k}-type-stone`, v => { f.stone = v; }, rerender);
|
||||
UI.onSel(`${id}-${k}-type-stuc`, v => { f.stuc = v; }, rerender);
|
||||
});
|
||||
});
|
||||
body.querySelectorAll('[data-hw-ch]').forEach(b => b.addEventListener('click', () => {
|
||||
state.chimneys = state.chimneys || [];
|
||||
if (b.dataset.hwCh === 'add') state.chimneys.push(ChimneyModel.create(state.chimneys, pu.id, wl.side, Number(b.dataset.u)));
|
||||
else state.chimneys = state.chimneys.filter(c => c.id !== Number(b.dataset.ch));
|
||||
refresh();
|
||||
}));
|
||||
}
|
||||
|
||||
function wallTab() {
|
||||
const pu = parts.units.find(x => x.id === selection.unit);
|
||||
const wl = pu && pu.walls.find(x => x.side === selection.side);
|
||||
@@ -1970,6 +2080,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
(own.facade && own.facade.type === 'steen' ? sel('hw-piece-stone', 'Steen', stoneOptions, own.facade.stone || fc.stone) : '') +
|
||||
(own.facade && own.facade.type === 'stuc' ? sel('hw-piece-stuc', 'Kleur', stucColors, own.facade.stuc || fc.stuc) : '') +
|
||||
small('Splitsing weg', 'x-lg', 'data-hw="unsplit"') : '') },
|
||||
chimneyGroup(pu, wl, piece, e0),
|
||||
{ label: 'Gevellampen', html: lt.mode === 'gevel'
|
||||
? sel('hw-light', 'Lampen', LightingModel.WALL_OVERRIDES, su.lighting.walls[wl.side] || 'auto',
|
||||
{ hint: `Automatisch: ${lt.endOffset} mm uit de hoek, op vrije gevel van min. ${lt.minWallWidth} mm. Naast openingen: ${lt.openingOffset} mm ernaast.` }) +
|
||||
@@ -1984,6 +2095,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
wireButtons(body);
|
||||
const fc = HouseModel.facadeFor(state, su, wl.side);
|
||||
UI.onSel('hw-light', v => { su.lighting.walls[wl.side] = v; }, rerender);
|
||||
wireChimneys(body, pu, wl);
|
||||
UI.onSel('hw-facade', v => {
|
||||
if (v === 'woning') delete su.facade.walls[wl.side];
|
||||
else su.facade.walls[wl.side] = v === 'steen' ? { type: v, stone: fc.stone } : v === 'stuc' ? { type: v, stuc: fc.stuc } : { type: v };
|
||||
@@ -2198,23 +2310,19 @@ function mountHouseEditor(container, callbacks) {
|
||||
const painted = state.settings.interiorPaint && BuildStages.shows(view3D.stage, 'afwerking');
|
||||
wallMaterials.spaanplaatMaterial.color.set(painted ? state.settings.interiorColor : SPAANPLAAT_COLOR);
|
||||
roofMaterials.ceilingMaterial.color.set(state.settings.ceilingColor || '#f4f3ef');
|
||||
parts = HouseModel.computeParts(state, { wallMaterials, floorMaterials, roofMaterials, lightingMaterials, sandwichMaterials, postMaterials, elektraMaterials, stage: view3D.stage, lampenVisible: view3D.lampen, elektraVisible: view3D.elektra });
|
||||
parts = HouseModel.computeParts(state, { wallMaterials, floorMaterials, roofMaterials, lightingMaterials, sandwichMaterials, postMaterials, chimneyMaterials, elektraMaterials, stage: view3D.stage, lampenVisible: view3D.lampen, elektraVisible: view3D.elektra });
|
||||
reportDirty = true;
|
||||
|
||||
if (houseGroup) { scene.remove(houseGroup); disposeGroup(houseGroup); }
|
||||
houseGroup = new THREE.Group();
|
||||
planCaps = null;
|
||||
const toM = 0.001;
|
||||
parts.units.forEach(u => {
|
||||
u.floorGroup.position.set(u.rect.cx * toM, 0, u.rect.cz * toM);
|
||||
houseGroup.add(u.floorGroup);
|
||||
});
|
||||
HouseModel.placeGroups(parts);
|
||||
parts.units.forEach(u => houseGroup.add(u.floorGroup));
|
||||
// Doorsnede: de wandmaterialen worden gedeeld, dus altijd zetten (ook weer uit).
|
||||
const section = view3D.section;
|
||||
const clip = section ? [cutPlane] : [];
|
||||
HouseModel.allWallPieces(parts).forEach(p => {
|
||||
p.group.rotation.y = p.place.rotY;
|
||||
p.group.position.set(p.place.cx * toM, p.place.floorTop * toM, p.place.cz * toM);
|
||||
p.group.traverse(o => {
|
||||
if (!o.material) return;
|
||||
(Array.isArray(o.material) ? o.material : [o.material]).forEach(m => {
|
||||
@@ -2227,12 +2335,10 @@ function mountHouseEditor(container, callbacks) {
|
||||
});
|
||||
placeSection();
|
||||
if (view3D.dak && !section) {
|
||||
parts.units.forEach(u => {
|
||||
u.roof.group.position.set(u.roof.rect.cx * toM, u.roof.base * toM, u.roof.rect.cz * toM);
|
||||
houseGroup.add(u.roof.group);
|
||||
});
|
||||
parts.units.forEach(u => houseGroup.add(u.roof.group));
|
||||
if (parts.roofEdgeGroup) houseGroup.add(parts.roofEdgeGroup);
|
||||
if (parts.postsGroup) houseGroup.add(parts.postsGroup);
|
||||
if (parts.chimneysGroup) houseGroup.add(parts.chimneysGroup);
|
||||
}
|
||||
if (parts.sandwichGroup) houseGroup.add(parts.sandwichGroup);
|
||||
if (parts.lightingGroup && !section) houseGroup.add(parts.lightingGroup);
|
||||
@@ -2344,12 +2450,13 @@ function mountHouseEditor(container, callbacks) {
|
||||
}
|
||||
|
||||
const WALK_RADIUS = 250;
|
||||
const isWalkDoor = (op) => op.type === 'deur' || op.type === 'voordeur' ||
|
||||
(op.fill && (op.fill.type === 'deur' || op.fill.type === 'schuifdeur')) || op.y < 50;
|
||||
const isWalkDoor = (op) => op.type !== 'schoorsteen' && (op.type === 'deur' || op.type === 'voordeur' ||
|
||||
(op.fill && (op.fill.type === 'deur' || op.fill.type === 'schuifdeur')) || op.y < 50);
|
||||
function wallBlocked(xm, zm) {
|
||||
if (!parts) return false;
|
||||
const x = xm * 1000, z = zm * 1000;
|
||||
if (signPos && Math.hypot(x - signPos.x, z - signPos.z) < SIGN.poleW / 2 + WALK_RADIUS) return true;
|
||||
if ((parts.chimneys || []).length && ChimneyModel.covers(parts.chimneys, x, z, WALK_RADIUS)) return true;
|
||||
return HouseModel.allWallPieces(parts).some(p => {
|
||||
if (p.place.baseY) return false; // opbouw boven een lager dak
|
||||
const c = Math.cos(p.place.rotY), s = Math.sin(p.place.rotY);
|
||||
@@ -2634,6 +2741,51 @@ function mountHouseEditor(container, callbacks) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
function exportAs(ext, title, run, done) {
|
||||
flushActivePart();
|
||||
const naam = String((state.project && state.project.naam) || '').replace(/[\\/:*?"<>|]+/g, '-').trim();
|
||||
const suggested = (naam || 'woning') + '-' + new Date().toISOString().slice(0, 10);
|
||||
let filename = window.prompt(`Bestandsnaam voor de ${title}:`, suggested);
|
||||
if (filename === null) { setIoStatus('Export geannuleerd.', ''); return null; }
|
||||
filename = filename.trim() || suggested;
|
||||
if (!new RegExp(`\\.${ext}$`, 'i').test(filename)) filename += '.' + ext;
|
||||
try {
|
||||
const res = run(filename);
|
||||
setIoStatus(`Geëxporteerd als "${filename}" (${typeof done === 'function' ? done(res) : done}).`, 'ok');
|
||||
} catch (err) {
|
||||
setIoStatus(`${ext.toUpperCase()}-export mislukt: ${err.message}`, 'err');
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
const exportDxf = () => exportAs('dxf', 'CAD-export (DXF)', (f) => HouseDxf.generate(state).save(f), 'DXF, in CAD op te slaan als DWG');
|
||||
const exportIfc = () => exportAs('ifc', 'BIM-export (IFC)', (f) => HouseIfc.generate(state).save(f), 'IFC4: balken, platen en elektra');
|
||||
|
||||
// Render-export: de volledige afgewerkte woning, los van de gekozen bouwfase, doorsnede en weergave-opties.
|
||||
// Eigen materiaalsets (steen, riet, rabat en tegels komen uit gedeelde caches); maaiveld onder het midden = oorsprong.
|
||||
const exportObj = () => exportAs('zip', 'render-export (OBJ + texturen, ZIP)', (filename) => {
|
||||
const wallMaterials = WallModel.createMaterials(), roofMaterials = RoofModel.createMaterials();
|
||||
if (state.settings.interiorPaint) wallMaterials.spaanplaatMaterial.color.set(state.settings.interiorColor);
|
||||
roofMaterials.ceilingMaterial.color.set(state.settings.ceilingColor || '#f4f3ef');
|
||||
const full = HouseModel.computeParts(state, {
|
||||
wallMaterials, floorMaterials: FloorModel.createMaterials(), roofMaterials, lightingMaterials: LightingModel.createMaterials(),
|
||||
sandwichMaterials: SandwichModel.createMaterials(), postMaterials: PostModel.createMaterials(), chimneyMaterials: ChimneyModel.createMaterials(),
|
||||
elektraMaterials: ElektraModel.createMaterials(), stage: BuildStages.FINAL, lampenVisible: true, elektraVisible: true
|
||||
});
|
||||
HouseModel.placeGroups(full);
|
||||
if (full.roomsGroup && full.roomsGroup.userData.ceilings) full.roomsGroup.userData.ceilings.visible = true;
|
||||
const roots = full.units.map(u => u.floorGroup).concat(HouseModel.allWallPieces(full).map(p => p.group), full.units.map(u => u.roof.group),
|
||||
[full.roofEdgeGroup, full.postsGroup, full.chimneysGroup, full.sandwichGroup, full.lightingGroup, full.elektraGroup, full.roomsGroup, full.fixturesGroup]).filter(Boolean);
|
||||
const bx0 = Math.min(...full.rects.map(r => r.x0)), bx1 = Math.max(...full.rects.map(r => r.x1));
|
||||
const bz0 = Math.min(...full.rects.map(r => r.z0)), bz1 = Math.max(...full.rects.map(r => r.z1));
|
||||
const ground = Math.max(...full.units.map(u => u.layout.floorTop)) - (state.settings.groundLevel || 0);
|
||||
const out = ObjWriter.build(roots, {
|
||||
name: filename.replace(/\.zip$/i, '').replace(/[^A-Za-z0-9_-]+/g, '_') || 'woning', app: APP_VERSION,
|
||||
origin: { x: (bx0 + bx1) / 2000, y: ground / 1000, z: (bz0 + bz1) / 2000 }
|
||||
});
|
||||
ZipWriter.save(out.files, filename);
|
||||
return out.stats;
|
||||
}, (s) => `${s.triangles.toLocaleString('nl-NL')} driehoeken, ${s.materials} materialen, ${s.textures} texturen; uitpakken en in Blender importeren`);
|
||||
|
||||
function triggerImport() {
|
||||
document.getElementById('h-file-import-json').click();
|
||||
}
|
||||
@@ -2679,6 +2831,9 @@ function mountHouseEditor(container, callbacks) {
|
||||
activeEditor = null;
|
||||
},
|
||||
exportDesign,
|
||||
exportDxf,
|
||||
exportIfc,
|
||||
exportObj,
|
||||
triggerImport,
|
||||
mirrorHorizontal: forward('mirrorHorizontal'),
|
||||
toggleFullscreen3D: forward('toggleFullscreen3D'),
|
||||
|
||||
Reference in New Issue
Block a user