Files
Tectum/js/house/house-ifc.js
T

187 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*!
* Tectum
* Copyright © 2026 Machiel Heinen.
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
// BIM-export van de bouwconstructie (IFC4): balken, platen en elektra, geen afwerking of inrichting.
// De geometrie komt uit de 3D-opbouw (dezelfde als in de app), in twee fasen: 'elektra' (alle balken en
// leidingen, nog zonder platen, dus niets weggelaten) en 'beplating' (alleen de platen).
// Assen: X = oost, Y = noord, Z = omhoog; 0,0 = zuidwesthoek van de units, Z = 0 op peil (bovenkant vloer).
// Opbouw: unit (samenstelling) > element A-W1 / A-V / A-D (samenstelling) > onderdelen; binnenwanden los.
window.HouseIfc = (() => {
const WOOD = 'Hout', ELEKTRA_KIND = { kast: 1, doos: 1, buis: 1 };
// Productiekleuren per soort (elektra houdt de kleur uit de app).
const COLORS = {
Stijl: [0.93, 0.74, 0.42], Regel: [0.85, 0.48, 0.18], Vloerbalk: [0.62, 0.40, 0.20], Dakbalk: [0.78, 0.30, 0.22],
Paal: [0.45, 0.28, 0.15], 'Stalen nokbalk': [0.35, 0.42, 0.55],
Spaanplaat: [0.40, 0.68, 0.42], OSB: [0.95, 0.86, 0.50], Cementplaat: [0.62, 0.64, 0.68]
};
const rgbOf = (mesh) => { const m = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material; return m && m.color ? [m.color.r, m.color.g, m.color.b] : null; };
function materialsSet() {
return {
wall: WallModel.createMaterials(), floor: FloorModel.createMaterials(), roof: RoofModel.createMaterials(),
elektra: ElektraModel.createMaterials(), post: PostModel.createMaterials()
};
}
// Materiaal -> soort onderdeel (alles wat hier niet in staat, gaat niet mee).
function classifier(M) {
const map = new Map();
map.set(M.wall.woodMaterial, { kind: 'hout', where: 'wand' });
map.set(M.floor.woodMaterial, { kind: 'hout', where: 'vloer' });
map.set(M.roof.woodMaterial, { kind: 'hout', where: 'dak' });
map.set(M.roof.steelMaterial, { kind: 'staal' });
map.set(M.wall.spaanplaatMaterial, { kind: 'plaat', label: 'Spaanplaat' });
map.set(M.wall.cementplaatMaterial, { kind: 'plaat', label: 'Cementplaat' });
map.set(M.floor.osbMaterial, { kind: 'plaat', label: 'OSB' });
map.set(M.roof.osbMaterial, { kind: 'plaat', label: 'OSB' });
Object.entries(M.elektra.conduit).forEach(([type, m]) => map.set(m, { kind: 'buis', type }));
map.set(M.elektra.boxMaterial, { kind: 'doos' });
map.set(M.elektra.kastMaterial, { kind: 'kast' });
map.set(M.elektra.deurMaterial, { kind: 'kast' });
Object.values(M.post.wood).forEach(m => map.set(m, { kind: 'paal' }));
return (mesh) => {
const m0 = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
return m0 ? map.get((m0.userData && m0.userData.base) || m0) || null : null;
};
}
function build(state, stage) {
const M = materialsSet();
const parts = HouseModel.computeParts(state, {
wallMaterials: M.wall, floorMaterials: M.floor, roofMaterials: M.roof, elektraMaterials: M.elektra, postMaterials: M.post,
stage, lampenVisible: false
});
HouseModel.placeGroups(parts);
return { parts, classify: classifier(M) };
}
function generate(state) {
const frame = build(state, BuildStages.INDEX.elektra);
const sheet = build(state, BuildStages.INDEX.beplating);
const parts = frame.parts;
if (!parts.units.length) throw new Error('geen units in het ontwerp');
const bx0 = Math.min(...parts.rects.map(r => r.x0)), bz1 = Math.max(...parts.rects.map(r => r.z1));
const peil = Math.max(...parts.units.map(u => u.layout.floorTop));
const v = new THREE.Vector3(), ax = new THREE.Vector3(), box = new THREE.Box3(), size = new THREE.Vector3();
function triangles(mesh, out) {
const g = mesh.geometry, pos = g.attributes.position, idx = g.index;
const n = idx ? idx.count : pos.count;
for (let i = 0; i < n; i++) {
v.fromBufferAttribute(pos, idx ? idx.getX(i) : i).applyMatrix4(mesh.matrixWorld);
out.push(v.x * 1000 - bx0, bz1 - v.z * 1000, v.y * 1000 - peil);
}
return out;
}
// Maat in de eigen assen (afgeschuinde sporen houden zo lengte en dikte), oplopend in mm.
function dims(mesh) {
if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox();
box.copy(mesh.geometry.boundingBox).getSize(size);
const s = [size.x * Math.abs(mesh.scale.x), size.y * Math.abs(mesh.scale.y), size.z * Math.abs(mesh.scale.z)].map(d => Math.round(d * 1000));
const longest = s.indexOf(Math.max(...s));
ax.set(longest === 0 ? 1 : 0, longest === 1 ? 1 : 0, longest === 2 ? 1 : 0).transformDirection(mesh.matrixWorld);
return { sorted: s.slice().sort((a, b) => a - b), vertical: Math.abs(ax.y) > 0.7 };
}
const label = (d) => d.sorted.join(' × ');
const groups = new Map(); // sleutel -> { name, tag, objectType, unit, members: [] }
const loose = [];
const merged = new Map(); // leidingen per run, meterkast
const groupOf = (key, info) => { if (!groups.has(key)) groups.set(key, Object.assign({ members: [] }, info)); return groups.get(key); };
function member(mesh, c, tag) {
const d = dims(mesh);
if (d.sorted[0] < 1) return null; // vlakken zonder dikte (kopse afdekking)
const tri = triangles(mesh, []);
const props = (name, material) => ({ Element: tag || null, Onderdeel: name, Afmeting: label(d), Lengte: d.sorted[2], Materiaal: material });
if (c.kind === 'plaat') {
const name = `${c.label} ${d.sorted[0]} mm`;
return { type: 'IFCPLATE', predefined: 'SHEET', name, material: c.label, tri, color: COLORS[c.label], props: props(name, c.label) };
}
const el = (type, predefined, name, material) => ({ type, predefined, name, material, tri, color: COLORS[name], props: props(name, material) });
if (c.kind === 'staal') return el('IFCBEAM', 'BEAM', 'Stalen nokbalk', 'Staal');
if (c.kind === 'paal') return el('IFCCOLUMN', 'COLUMN', 'Paal', WOOD);
if (c.where === 'vloer') return el('IFCBEAM', 'JOIST', 'Vloerbalk', WOOD);
if (c.where === 'dak') return el('IFCMEMBER', 'RAFTER', 'Dakbalk', WOOD);
return d.vertical ? el('IFCMEMBER', 'STUD', 'Stijl', WOOD) : el('IFCMEMBER', 'PLATE', 'Regel', WOOD);
}
function collect(src, group, plates, target) {
if (!group) return;
group.updateMatrixWorld(true);
group.traverseVisible(o => {
if (!o.isMesh) return;
const c = src.classify(o);
if (!c || (c.kind === 'plaat') !== plates) return;
if (ELEKTRA_KIND[c.kind]) {
if (c.kind === 'doos') {
loose.push({ type: 'IFCJUNCTIONBOX', predefined: 'POWER', name: 'Inbouwdoos', material: 'Kunststof', tri: triangles(o, []), color: rgbOf(o), props: { Onderdeel: 'Inbouwdoos', Materiaal: 'Kunststof' } });
return;
}
const key = c.kind === 'kast' ? 'kast' : `buis:${o.userData.run}`;
if (!merged.has(key)) {
merged.set(key, c.kind === 'kast'
? { type: 'IFCELECTRICDISTRIBUTIONBOARD', predefined: 'DISTRIBUTIONBOARD', name: 'Meterkast', material: 'Kunststof', tri: [], color: rgbOf(o), props: { Onderdeel: 'Meterkast' } }
: { type: 'IFCCABLECARRIERSEGMENT', predefined: 'CONDUITSEGMENT', name: `Installatiebuis ${c.type}`, material: `PVC ${c.type}`, tri: [], len: 0, color: rgbOf(o), props: { Onderdeel: `Installatiebuis ${c.type}`, Materiaal: 'PVC' } });
}
const e = merged.get(key);
triangles(o, e.tri);
if (e.len !== undefined && o.geometry.type === 'CylinderGeometry') e.len += o.geometry.parameters.height * 1000;
return;
}
const m = member(o, c, target && target.tag);
if (m) (target ? target.members : loose).push(m);
});
}
[[frame, false], [sheet, true]].forEach(([src, plates]) => {
const P = src.parts;
P.units.forEach(u => {
collect(src, u.floorGroup, plates, groupOf(u.floorElement, { name: `Unit ${u.id} vloer`, tag: u.floorElement, objectType: 'Vloerelement', unit: u.id }));
u.walls.forEach(w => w.pieces.forEach(p => {
collect(src, p.group, plates, groupOf(p.element, { name: p.label, tag: p.element, objectType: 'Wandelement', unit: u.id }));
}));
collect(src, u.roof.group, plates, groupOf(`${u.id}-D`, { name: `Unit ${u.id} dak`, tag: `${u.id}-D`, objectType: 'Dakelement', unit: u.id }));
});
P.interior.forEach(p => collect(src, p.group, plates, groupOf(p.element, { name: p.label, tag: p.element, objectType: 'Binnenwand', unit: null })));
collect(src, P.postsGroup, plates, null);
collect(src, P.elektraGroup, plates, null);
});
merged.forEach(e => {
if (e.len) e.props.Lengte = Math.round(e.len);
delete e.len;
loose.push(e);
});
const pr = state.project || {};
const ifc = IfcWriter.create({
project: pr.naam || 'Woning', building: pr.naam || 'Woning', author: pr.tekenaar || '',
app: `Tectum${typeof APP_VERSION !== 'undefined' ? ' ' + APP_VERSION : ''}`,
address: { lines: [pr.straat, pr.nummer].filter(Boolean).join(' '), town: pr.plaats || '' }
});
const unitParts = new Map(parts.units.map(u => [u.id, []]));
groups.forEach(g => {
const ids = g.members.map(m => ifc.element(m)).filter(Boolean);
if (!ids.length) return;
const asm = ifc.element({ type: 'IFCELEMENTASSEMBLY', name: g.name, tag: g.tag, objectType: g.objectType, props: { Element: g.tag } });
ifc.aggregate(asm, ids);
if (g.unit && unitParts.has(g.unit)) unitParts.get(g.unit).push(asm);
else ifc.contain(asm);
});
unitParts.forEach((ids, id) => {
if (!ids.length) return;
const asm = ifc.element({ type: 'IFCELEMENTASSEMBLY', name: `Unit ${id}`, tag: id, objectType: 'Unit' });
ifc.aggregate(asm, ids);
ifc.contain(asm);
});
loose.forEach(e => ifc.contain(ifc.element(e)));
return ifc;
}
return { generate };
})();