187 lines
9.8 KiB
JavaScript
187 lines
9.8 KiB
JavaScript
/*!
|
||
* 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, layers) {
|
||
const M = materialsSet();
|
||
const parts = HouseModel.computeParts(state, {
|
||
wallMaterials: M.wall, floorMaterials: M.floor, roofMaterials: M.roof, elektraMaterials: M.elektra, postMaterials: M.post,
|
||
layers: Object.assign(layers, { lampen: false })
|
||
});
|
||
HouseModel.placeGroups(parts);
|
||
return { parts, classify: classifier(M) };
|
||
}
|
||
|
||
function generate(state) {
|
||
const frame = build(state, ViewLayers.upTo(1));
|
||
const sheet = build(state, ViewLayers.upTo(2));
|
||
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 };
|
||
})();
|