You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

129 lines
6.2 KiB
JavaScript

// Generates src/3d/massing.glb — a conceptual massing model of a Kerala
// residence (two gabled volumes on a plinth + ember "section cut" fin),
// echoing the homepage hero elevation drawing. Pure Node, no deps.
// Usage: node tools/make-glb.js (rerun only if you change the geometry)
const fs = require('fs');
const path = require('path');
const positions = [], normals = [], groups = { walls: [], roof: [], ember: [] };
function tri(mat, a, b, c) {
// flat normal
const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
const v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
const l = Math.hypot(n[0], n[1], n[2]) || 1;
n = n.map(x => x / l);
for (const p of [a, b, c]) {
groups[mat].push(positions.length / 3);
positions.push(...p);
normals.push(...n);
}
}
function quad(mat, a, b, c, d) { tri(mat, a, b, c); tri(mat, a, c, d); }
// axis-aligned box
function box(mat, [x0, y0, z0], [x1, y1, z1]) {
quad(mat, [x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1]); // front +z
quad(mat, [x1, y0, z0], [x0, y0, z0], [x0, y1, z0], [x1, y1, z0]); // back
quad(mat, [x0, y0, z0], [x0, y0, z1], [x0, y1, z1], [x0, y1, z0]); // left
quad(mat, [x1, y0, z1], [x1, y0, z0], [x1, y1, z0], [x1, y1, z1]); // right
quad(mat, [x0, y1, z1], [x1, y1, z1], [x1, y1, z0], [x0, y1, z0]); // top
quad(mat, [x0, y0, z0], [x1, y0, z0], [x1, y0, z1], [x0, y0, z1]); // bottom
}
// gabled volume: walls to eave height, triangular gable ends, roof planes with overhang
function gabled(matW, matR, [x0, z0], [x1, z1], yBase, yEave, yRidge, over) {
box(matW, [x0, yBase, z0], [x1, yEave, z1]);
const zm = (z0 + z1) / 2;
// gable end triangles (on x0 and x1 faces)
tri(matW, [x0, yEave, z1], [x0, yRidge, zm], [x0, yEave, z0]);
tri(matW, [x1, yEave, z0], [x1, yRidge, zm], [x1, yEave, z1]);
// roof planes (slightly overhanging slabs)
const t = 0.12; // roof slab thickness
const ox0 = x0 - over, ox1 = x1 + over, oz0 = z0 - over, oz1 = z1 + over;
// south plane (z1 side): from eave edge to ridge
quad(matR, [ox0, yEave, oz1], [ox1, yEave, oz1], [ox1, yRidge, zm], [ox0, yRidge, zm]);
quad(matR, [ox0, yEave + t, oz1], [ox0, yRidge + t, zm], [ox1, yRidge + t, zm], [ox1, yEave + t, oz1]);
// north plane
quad(matR, [ox1, yEave, oz0], [ox0, yEave, oz0], [ox0, yRidge, zm], [ox1, yRidge, zm]);
quad(matR, [ox1, yEave + t, oz0], [ox1, yRidge + t, zm], [ox0, yRidge + t, zm], [ox0, yEave + t, oz0]);
}
// ---- compose the massing (meters; y up; x along ridge) ----
box('walls', [-7.2, 0, -4.2], [7.2, 0.3, 4.2]); // plinth
gabled('walls', 'roof', [-6.4, -3.4], [0.6, 3.4], 0.3, 3.4, 5.1, 0.5); // main volume
gabled('walls', 'roof', [1.2, -2.6], [6.4, 2.6], 0.3, 2.9, 4.2, 0.4); // secondary volume
box('walls', [0.6, 0.3, -1.0], [1.2, 2.9, 1.0]); // link block
box('walls', [-6.9, 0.3, 3.6], [-2.9, 0.45, 4.9]); // veranda deck
box('roof', [-6.9, 3.05, 3.6], [-2.9, 3.2, 5.0]); // veranda canopy
for (const px of [-6.7, -4.9, -3.1]) box('walls', [px, 0.45, 4.55], [px + 0.14, 3.05, 4.69]); // posts
box('ember', [-2.95, 0.05, -4.6], [-2.83, 5.6, 4.6]); // section-cut fin
// ---- write GLB ----
function toBuf(arr, Ctor) { return Buffer.from(new Ctor(arr).buffer); }
const posBuf = toBuf(positions, Float32Array);
const nrmBuf = toBuf(normals, Float32Array);
const idxBufs = ['walls', 'roof', 'ember'].map(m => toBuf(groups[m], Uint32Array));
const bin = Buffer.concat([posBuf, nrmBuf, ...idxBufs]);
const pad4 = n => (4 - (n % 4)) % 4;
let mins = [Infinity, Infinity, Infinity], maxs = [-Infinity, -Infinity, -Infinity];
for (let i = 0; i < positions.length; i += 3)
for (let k = 0; k < 3; k++) { mins[k] = Math.min(mins[k], positions[i + k]); maxs[k] = Math.max(maxs[k], positions[i + k]); }
const bufferViews = [
{ buffer: 0, byteOffset: 0, byteLength: posBuf.length, target: 34962 },
{ buffer: 0, byteOffset: posBuf.length, byteLength: nrmBuf.length, target: 34962 },
];
let off = posBuf.length + nrmBuf.length;
for (const b of idxBufs) { bufferViews.push({ buffer: 0, byteOffset: off, byteLength: b.length, target: 34963 }); off += b.length; }
const accessors = [
{ bufferView: 0, componentType: 5126, count: positions.length / 3, type: 'VEC3', min: mins, max: maxs },
{ bufferView: 1, componentType: 5126, count: normals.length / 3, type: 'VEC3' },
];
['walls', 'roof', 'ember'].forEach((m, i) => accessors.push({ bufferView: 2 + i, componentType: 5125, count: groups[m].length, type: 'SCALAR' }));
const materials = [
{ name: 'walls', pbrMetallicRoughness: { baseColorFactor: [0.87, 0.845, 0.79, 1], metallicFactor: 0, roughnessFactor: 0.92 } },
{ name: 'roof', pbrMetallicRoughness: { baseColorFactor: [0.16, 0.15, 0.14, 1], metallicFactor: 0, roughnessFactor: 0.85 } },
{ name: 'ember', pbrMetallicRoughness: { baseColorFactor: [0.34, 0.16, 0.09, 1], metallicFactor: 0, roughnessFactor: 0.7 }, emissiveFactor: [0.05, 0.02, 0.01] },
];
const json = {
asset: { version: '2.0', generator: 'artn-massing' },
scene: 0,
scenes: [{ nodes: [0] }],
nodes: [{ mesh: 0, name: 'massing' }],
meshes: [{
primitives: ['walls', 'roof', 'ember'].map((m, i) => ({
attributes: { POSITION: 0, NORMAL: 1 }, indices: 2 + i, material: i
}))
}],
materials, accessors, bufferViews,
buffers: [{ byteLength: bin.length }],
};
let jsonStr = JSON.stringify(json);
jsonStr += ' '.repeat(pad4(jsonStr.length));
const jsonChunk = Buffer.from(jsonStr);
const binPadded = Buffer.concat([bin, Buffer.alloc(pad4(bin.length))]);
const total = 12 + 8 + jsonChunk.length + 8 + binPadded.length;
const glb = Buffer.alloc(total);
glb.writeUInt32LE(0x46546C67, 0); // magic 'glTF'
glb.writeUInt32LE(2, 4);
glb.writeUInt32LE(total, 8);
glb.writeUInt32LE(jsonChunk.length, 12);
glb.writeUInt32LE(0x4E4F534A, 16); // 'JSON'
jsonChunk.copy(glb, 20);
glb.writeUInt32LE(binPadded.length, 20 + jsonChunk.length);
glb.writeUInt32LE(0x004E4942, 24 + jsonChunk.length); // 'BIN'
binPadded.copy(glb, 28 + jsonChunk.length);
const dest = path.join(__dirname, '..', 'src', '3d', 'massing.glb');
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.writeFileSync(dest, glb);
console.log(`massing.glb written: ${(glb.length / 1024).toFixed(1)} KB, ${positions.length / 3} verts`);