Files
Tectum/js/core/obj-writer.js
T
MachielandClaude Opus 5.5 ab0dca9bb5 v0.7.1: UI-overhaul, ribbon vervangen door de indeling van CAD/BIM-pakketten met elke functie op één plek: bovenbalk met menu Bestand (nieuw, openen, opslaan, exporteren, projectgegevens, kosten & tijden) · Beeld · Help, de ontwerpen als tabs en de knop Opslaan; links de structuur met inklapbare groepen (onthouden, groep met de selectie klapt vanzelf open); werkbalk boven de viewports met gereedschap (Selecteren V, Binnenwand W, Ruimte R, Inrichting I, opties van het gereedschap ernaast, Unit toevoegen) en weergave (één Lagen-menu met bouwfase voor 2D en 3D, Omgeving, Doorsnede X, Render, Lopen L, Rapport 4, 2D + 3D / 2D / 3D met 1/2/3); rechts het eigenschappenpaneel (Inspector: selectie met kruimelpad, of de woning met kerncijfers en inklapbare secties Gevel, Dak, Binnen, Verlichting, Elektra, Constructie); statusbalk met hint en meldingen; wand- en vloer/dak-editor in dezelfde indeling (paneel rechts met tab Resultaat, terug met ← Woning of Esc); sneltoetsenoverzicht (?), Ctrl+O, Ctrl+\ panelen verbergen; rapport als masonry; dubbele functies weg (lagenbalk, Beeld-tab en Weergave-filter, export en kosten op twee plekken, Snel nieuw); oude onderdelen gelijkgetrokken (dialoogknoppen, materiaalknoppen, chassis als schakelaar, eenheden); weergavelagen (ViewLayers) i.p.v. bouwfasen, elk los aan/uit; render-export met Blender-script voor de lampen; elektraleidingen boven een vals plafond en haaks binnen de units bij een L/T-vorm
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:16:43 +02:00

199 lines
9.5 KiB
JavaScript

/*!
* Tectum
* Copyright © 2026 Machiel Heinen.
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
// Wavefront OBJ + MTL + texturen (PNG) uit three.js-objecten, in meters, Y omhoog (Blender: standaard import).
// Texturen: herhaling/verschuiving (texture.matrix) zit in de UV's, de MTL verwijst naar textures/*.png.
// PBR via de MTL-uitbreiding (Pr ruwheid, Pm metaal, d doorzichtigheid, Ke gloed), die Blender inleest.
window.ObjWriter = (() => {
const f4 = (v) => { const r = Math.round(v * 10000) / 10000; return Object.is(r, -0) ? '0' : String(r); };
const f3 = (v) => { const r = Math.round(v * 1000) / 1000; return Object.is(r, -0) ? '0' : String(r); };
const safe = (s) => String(s || '').replace(/[^A-Za-z0-9_-]+/g, '_').slice(0, 40);
function pngBytes(image) {
try {
const w = image.naturalWidth || image.videoWidth || image.width, h = image.naturalHeight || image.videoHeight || image.height;
if (!w || !h) return null;
const c = document.createElement('canvas');
c.width = w; c.height = h;
c.getContext('2d').drawImage(image, 0, 0);
const bin = atob(c.toDataURL('image/png').split(',')[1]);
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
} catch (e) { return null; }
}
// objects: three.js-objecten (zichtbaar deel); origin: verschuiving in meters; name: basisnaam van de bestanden.
function build(objects, opts) {
const o = opts || {};
const name = o.name || 'woning', origin = o.origin || { x: 0, y: 0, z: 0 };
const enc = new TextEncoder();
const obj = [`# Tectum${o.app ? ' ' + o.app : ''}\n# meters, Y omhoog\nmtllib ${name}.mtl\n`], chunks = [];
const mats = new Map(), textures = new Map(), files = [];
let vBase = 1, tBase = 1, nBase = 1, meshNr = 0, triCount = 0;
const flush = () => { if (obj.length > 2000) { chunks.push(enc.encode(obj.join(''))); obj.length = 0; } };
function textureFile(tex) {
if (!tex || !tex.image) return null;
const key = tex.image;
if (!textures.has(key)) {
const bytes = pngBytes(tex.image);
const file = bytes ? `textures/tex_${textures.size + 1}.png` : null;
if (bytes) files.push({ name: file, data: bytes });
textures.set(key, file);
}
return textures.get(key);
}
function matName(m) {
if (!mats.has(m)) mats.set(m, { name: `m${mats.size + 1}_${safe(m.name || m.type)}`, mat: m, map: textureFile(m.map) });
return mats.get(m);
}
const v = new THREE.Vector3(), nm = new THREE.Matrix3(), uv = new THREE.Vector2();
// Eerst de hele boom bijwerken: een mesh alleen gebruikt de (verouderde) matrix van zijn groep.
const visit = (root) => root && (root.updateMatrixWorld(true), root.traverseVisible(mesh => {
if (!mesh.isMesh || !mesh.geometry || !mesh.geometry.attributes.position) return;
const g = mesh.geometry, pos = g.attributes.position, nrm = g.attributes.normal, uvA = g.attributes.uv, idx = g.index;
nm.getNormalMatrix(mesh.matrixWorld);
const flip = mesh.matrixWorld.determinant() < 0;
const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
const groups = g.groups && g.groups.length ? g.groups : [{ start: 0, count: idx ? idx.count : pos.count, materialIndex: 0 }];
const out = [`o ${safe(mesh.parent && mesh.parent.name) || 'deel'}_${++meshNr}\n`];
for (let i = 0; i < pos.count; i++) {
v.fromBufferAttribute(pos, i).applyMatrix4(mesh.matrixWorld);
out.push(`v ${f4(v.x - origin.x)} ${f4(v.y - origin.y)} ${f4(v.z - origin.z)}\n`);
}
if (nrm) for (let i = 0; i < nrm.count; i++) {
v.fromBufferAttribute(nrm, i).applyMatrix3(nm).normalize();
out.push(`vn ${f3(v.x)} ${f3(v.y)} ${f3(v.z)}\n`);
}
let tCount = 0;
groups.forEach(gr => {
const m = list[gr.materialIndex] || list[0];
if (!m || m.visible === false) return;
const mi = matName(m);
out.push(`usemtl ${mi.name}\n`);
const map = mi.map && m.map ? m.map : null;
if (map) map.updateMatrix();
const end = Math.min(gr.start + gr.count, idx ? idx.count : pos.count);
for (let k = gr.start; k + 2 < end; k += 3) {
let c = [0, 1, 2].map(j => idx ? idx.getX(k + j) : k + j);
if (flip) c = [c[0], c[2], c[1]];
const corner = c.map(ci => {
let s = `${vBase + ci}`;
if (uvA && map) {
uv.fromBufferAttribute(uvA, ci).applyMatrix3(map.matrix);
out.push(`vt ${f4(uv.x)} ${f4(uv.y)}\n`);
s += `/${tBase + tCount++}`;
} else if (nrm) s += '/';
if (nrm) s += `/${nBase + ci}`;
return s;
});
out.push(`f ${corner.join(' ')}\n`);
triCount++;
}
});
vBase += pos.count; tBase += tCount; if (nrm) nBase += nrm.count;
obj.push(out.join(''));
flush();
}));
objects.forEach(visit);
chunks.push(enc.encode(obj.join('')));
// De app toont kleurwaarden zoals ze zijn (geen kleurbeheer); Blender rekent lineair: sRGB -> lineair.
const lin = (x) => x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
const col = (c) => c ? `${f3(lin(c.r))} ${f3(lin(c.g))} ${f3(lin(c.b))}` : '0.6 0.6 0.6';
const mtl = [`# Tectum materialen\n`];
mats.forEach(({ name: n, mat: m, map }) => {
mtl.push(`\nnewmtl ${n}\n`);
mtl.push(`Kd ${col(m.color)}\n`);
mtl.push('Ks 0.5 0.5 0.5\n');
if (m.emissive && (m.emissive.r || m.emissive.g || m.emissive.b)) {
const k = m.emissiveIntensity !== undefined ? m.emissiveIntensity : 1;
mtl.push(`Ke ${f3(lin(m.emissive.r) * k)} ${f3(lin(m.emissive.g) * k)} ${f3(lin(m.emissive.b) * k)}\n`);
}
if (m.roughness !== undefined) mtl.push(`Pr ${f3(m.roughness)}\n`);
if (m.metalness !== undefined) mtl.push(`Pm ${f3(m.metalness)}\n`);
const d = m.transparent ? (m.opacity !== undefined ? m.opacity : 1) : 1;
mtl.push(`d ${f3(d)}\nillum 2\n`);
if (map) mtl.push(`map_Kd ${map}\n`);
});
return {
files: [{ name: `${name}.obj`, data: chunks }, { name: `${name}.mtl`, data: enc.encode(mtl.join('')) }].concat(files),
stats: { meshes: meshNr, triangles: triCount, materials: mats.size, textures: files.length }
};
}
// OBJ kent geen lampen: een Blender-script dat de OBJ importeert (als die er nog niet is) en de lampen als echte
// spotlights plaatst. sources: LightingModel.lightSources (m, Y omhoog); Blender is Z omhoog: (x, -z, y).
const BLENDER_WATT = { wandlamp: 10, dakrandspot: 8, binnenspot: 15 };
function blenderLights(sources, opts) {
const o = opts || {}, origin = o.origin || { x: 0, y: 0, z: 0 }, name = o.name || 'woning';
const c = new THREE.Color(o.color !== undefined ? o.color : 0xffd9a0);
const lin = (x) => x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
const t = (v) => `(${v.map(f4).join(', ')})`;
const rows = sources.map(s => ` (${JSON.stringify(s.kind)}, ${t([s.x - origin.x, -(s.z - origin.z), s.y - origin.y])}, ${t([s.dir.x, -s.dir.z, s.dir.y])}, ` +
`${f3(Math.min(Math.PI, s.angle * 2))}, ${f3(s.penumbra)}, ${BLENDER_WATT[s.kind] || 10}),\n`).join('');
return `# Tectum${o.app ? ' ' + o.app : ''}: lampen voor ${name}.obj
# Blender: Scripting > Open > dit bestand > Run Script. Importeert ${name}.obj uit dezelfde map
# (IMPORTEER = False als je de OBJ al zelf hebt geïmporteerd) en zet elke lamp als spotlight in de
# collectie "Tectum lampen"; opnieuw uitvoeren vervangt de lampen.
# HELDERHEID schaalt alle lampen; vermogens in watt per soort staan in de lijst (soort, plek, richting,
# kegel, zachte rand, watt).
import bpy, os
from mathutils import Vector
IMPORTEER = True
HELDERHEID = 1.0
KLEUR = (${f3(lin(c.r))}, ${f3(lin(c.g))}, ${f3(lin(c.b))})
OBJ = "${name}.obj"
LAMPEN = [
${rows}]
def map_van_script():
for area in bpy.context.screen.areas if bpy.context.screen else []:
if area.type == 'TEXT_EDITOR' and area.spaces.active.text and area.spaces.active.text.filepath:
return os.path.dirname(bpy.path.abspath(area.spaces.active.text.filepath))
return os.path.dirname(bpy.data.filepath) if bpy.data.filepath else ""
pad = os.path.join(map_van_script(), OBJ)
if IMPORTEER and not any(ob.get("tectum") == OBJ for ob in bpy.data.objects):
if not os.path.exists(pad):
raise Exception(f"{OBJ} niet gevonden naast dit script; zet IMPORTEER = False na een eigen import")
if hasattr(bpy.ops.wm, "obj_import"):
bpy.ops.wm.obj_import(filepath=pad)
else:
bpy.ops.import_scene.obj(filepath=pad)
for ob in bpy.context.selected_objects:
ob["tectum"] = OBJ
oud = bpy.data.collections.get("Tectum lampen")
if oud:
for ob in list(oud.objects):
bpy.data.objects.remove(ob, do_unlink=True)
bpy.data.collections.remove(oud)
col = bpy.data.collections.new("Tectum lampen")
bpy.context.scene.collection.children.link(col)
for i, (soort, plek, richting, kegel, rand, watt) in enumerate(LAMPEN):
licht = bpy.data.lights.new(f"{soort}_{i + 1}", 'SPOT')
licht.energy = watt * HELDERHEID
licht.color = KLEUR
licht.spot_size = kegel
licht.spot_blend = rand
licht.shadow_soft_size = 0.03
ob = bpy.data.objects.new(licht.name, licht)
ob.location = plek
ob.rotation_euler = Vector(richting).to_track_quat('-Z', 'Y').to_euler()
col.objects.link(ob)
print(f"Tectum: {len(LAMPEN)} lampen geplaatst")
`;
}
return { build, blenderLights };
})();