fix: update dimensions and descriptions in README and improve model generation logic in klabava.js
continuous-integration/drone/push Build is passing

This commit is contained in:
martin.fencl
2026-10-31 18:31:35 +01:00
parent 09eabdac26
commit 48d55d663f
3 changed files with 102 additions and 83 deletions
+95 -76
View File
@@ -138,6 +138,25 @@ function poly(parent, name, points, material) {
geometry.computeVertexNormals();
return mesh(geometry, material, parent, name);
}
function slopedBox(parent, name, x, z, sx, sz, lowerAt, upperAt, material) {
const x0 = x - sx / 2, x1 = x + sx / 2, z0 = z - sz / 2, z1 = z + sz / 2;
const vertices = [
[x0, lowerAt(x0, z0), z0], [x1, lowerAt(x1, z0), z0], [x1, lowerAt(x1, z1), z1], [x0, lowerAt(x0, z1), z1],
[x0, upperAt(x0, z0), z0], [x1, upperAt(x1, z0), z0], [x1, upperAt(x1, z1), z1], [x0, upperAt(x0, z1), z1]
];
const faces = [[0, 3, 2, 1], [4, 5, 6, 7], [0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]];
const positions = [], indices = [];
for (const face of faces) {
const base = positions.length / 3;
for (const index of face) positions.push(...vertices[index]);
for (let i = 1; i < face.length - 1; i++) indices.push(base, base + i, base + i + 1);
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geometry.setIndex(indices);
geometry.computeVertexNormals();
return mesh(geometry, material, parent, name);
}
function line(parent, name, points, color = 0x657361, opacity = 0.85) {
const geometry = new THREE.BufferGeometry().setFromPoints(points.map(p => new THREE.Vector3(...p)));
const material = new THREE.LineBasicMaterial({ color, transparent: opacity < 1, opacity });
@@ -154,7 +173,7 @@ function groundHeight(x, z) {
const dx = Math.max(Math.abs(x) - 9.5, 0);
const dz = z > 15.8 ? z - 15.8 : z < 0.4 ? 0.4 - z : 0;
const blend = smooth(0, 4.2, Math.hypot(dx, dz));
return -0.13 * (1 - blend) + rawGround(x, z) * blend;
return -0.30 * (1 - blend) + rawGround(x, z) * blend;
}
function buildGround() {
@@ -252,7 +271,7 @@ function makeWallEdge(p1, p2, openings = []) {
const dx = p2[0] - p1[0], dz = p2[1] - p1[1];
const length = Math.hypot(dx, dz);
const angle = Math.atan2(-dz, dx);
const thickness = 0.22, height = 3.046;
const thickness = 0.295, height = 3.046, wallBottom = -0.30;
const positionAt = (distance) => [p1[0] + dx * distance / length, p1[1] + dz * distance / length];
const wallSegment = (from, to, bottom, top, name = 'Obvodová stěna') => {
if (to - from < 0.02 || top - bottom < 0.02) return;
@@ -265,113 +284,113 @@ function makeWallEdge(p1, p2, openings = []) {
const left = clamp(opening.center - opening.width / 2, 0.05, length - 0.05);
const right = clamp(opening.center + opening.width / 2, 0.05, length - 0.05);
if (left <= cursor || right <= left) continue;
wallSegment(cursor, left, 0, height);
if (opening.bottom > 0) wallSegment(left, right, 0, opening.bottom, 'Parapet pod oknem');
wallSegment(cursor, left, wallBottom, height);
if (opening.bottom > wallBottom) wallSegment(left, right, wallBottom, opening.bottom, 'Parapet pod oknem');
const top = Math.min(height, opening.bottom + opening.height);
wallSegment(left, right, top, height, 'Nadpraží otvoru');
const [x, z] = positionAt((left + right) / 2);
const centerY = opening.bottom + (top - opening.bottom) / 2;
const paneMaterial = opening.kind === 'door' ? mats.door : mats.glass;
box(houseGroup, opening.kind === 'door' ? 'Vstupní dveře – zjednodušené' : 'Zasklení – zjednodušené', x, centerY, z, right - left - 0.10, Math.max(0.12, top - opening.bottom - 0.10), 0.055, paneMaterial, angle);
const paneName = opening.kind === 'door' ? 'Vstupní dveře – zjednodušené' : opening.kind === 'glazedDoor' ? 'Prosklené dveře – zjednodušené' : 'Okno – zjednodušené';
box(houseGroup, paneName, x, centerY, z, right - left - 0.10, Math.max(0.12, top - opening.bottom - 0.10), 0.055, paneMaterial, angle);
const frameWidth = 0.055;
const jambHeight = top - opening.bottom;
for (const offset of [-1, 1]) {
const [jx, jz] = positionAt((left + right) / 2 + offset * (right - left - frameWidth) / 2);
box(houseGroup, 'Rám otvoru', jx, opening.bottom + jambHeight / 2, jz, frameWidth, jambHeight, 0.09, mats.frame, angle);
}
for (let pane = 1; pane < (opening.panes || 1); pane++) {
const offset = (pane / opening.panes - 0.5) * (right - left);
const mullionX = x + dx / length * offset;
const mullionZ = z + dz / length * offset;
box(houseGroup, 'Svislý rám okna', mullionX, opening.bottom + jambHeight / 2, mullionZ, frameWidth, jambHeight, 0.09, mats.frame, angle);
}
box(houseGroup, 'Horní rám otvoru', x, top - frameWidth / 2, z, right - left, frameWidth, 0.09, mats.frame, angle);
if (opening.bottom > 0) box(houseGroup, 'Parapet okna', x, opening.bottom + frameWidth / 2, z, right - left, frameWidth, 0.09, mats.frame, angle);
cursor = right;
}
wallSegment(cursor, length, 0, height);
wallSegment(cursor, length, wallBottom, height);
}
function hipRoof(x0, x1, z0, z1, name, ridgeHeight) {
const overhang = 0.38, eave = 3.046;
x0 -= overhang; x1 += overhang; z0 -= overhang; z1 += overhang;
function hipPyramid(x0, x1, z0, z1, name, ridgeHeight, options = {}) {
const eave = 3.046;
const west = options.overhang?.west ?? 0.25, east = options.overhang?.east ?? 0.25;
const south = options.overhang?.south ?? 0.25, north = options.overhang?.north ?? 0.25;
x0 -= west; x1 += east; z0 -= south; z1 += north;
const xm = (x0 + x1) / 2, zm = (z0 + z1) / 2;
const width = x1 - x0, depth = z1 - z0;
const v = (x, y, z) => [x, y, z];
if (width >= depth) {
const inset = depth / 2;
const a = v(x0 + inset, ridgeHeight, zm), b = v(x1 - inset, ridgeHeight, zm);
poly(houseGroup, `${name} – severní střešní rovina`, [v(x0, eave, z1), v(x1, eave, z1), b, a], mats.roof);
poly(houseGroup, `${name} – jižní střešní rovina`, [v(x1, eave, z0), v(x0, eave, z0), a, b], mats.roof);
poly(houseGroup, `${name} – západní valba`, [v(x0, eave, z1), v(x0, eave, z0), a], mats.roof);
poly(houseGroup, `${name} – východní valba`, [v(x1, eave, z0), v(x1, eave, z1), b], mats.roof);
box(houseGroup, `${name} – hřeben`, (a[0] + b[0]) / 2, ridgeHeight, zm, Math.max(0.18, b[0] - a[0]), 0.12, 0.12, mats.roof);
} else {
const inset = width / 2;
const a = v(xm, ridgeHeight, z0 + inset), b = v(xm, ridgeHeight, z1 - inset);
poly(houseGroup, `${name} – východní střešní rovina`, [v(x1, eave, z0), v(x1, eave, z1), b, a], mats.roof);
poly(houseGroup, `${name} – západní střešní rovina`, [v(x0, eave, z1), v(x0, eave, z0), a, b], mats.roof);
poly(houseGroup, `${name} – severní valba`, [v(x0, eave, z1), v(x1, eave, z1), b], mats.roof);
poly(houseGroup, `${name} – jižní valba`, [v(x1, eave, z0), v(x0, eave, z0), a], mats.roof);
box(houseGroup, `${name} – hřeben`, xm, ridgeHeight, (a[2] + b[2]) / 2, 0.12, 0.12, Math.max(0.18, b[2] - a[2]), mats.roof);
}
// Eaves and downpipes make the roof silhouette readable at presentation scale.
box(houseGroup, `${name} – okap sever`, xm, eave - 0.04, z1, width, 0.12, 0.12, mats.frame);
box(houseGroup, `${name} – okap jih`, xm, eave - 0.04, z0, width, 0.12, 0.12, mats.frame);
box(houseGroup, `${name} – okap západ`, x0, eave - 0.04, zm, 0.12, 0.12, depth, mats.frame);
box(houseGroup, `${name} – okap východ`, x1, eave - 0.04, zm, 0.12, 0.12, depth, mats.frame);
const apex = v(xm, ridgeHeight, zm);
poly(houseGroup, `${name} – severní valba`, [v(x0, eave, z1), v(x1, eave, z1), apex], mats.roof);
poly(houseGroup, `${name} – jižní valba`, [v(x1, eave, z0), v(x0, eave, z0), apex], mats.roof);
poly(houseGroup, `${name} – západní valba`, [v(x0, eave, z0), v(x0, eave, z1), apex], mats.roof);
poly(houseGroup, `${name} – východní valba`, [v(x1, eave, z1), v(x1, eave, z0), apex], mats.roof);
const width = x1 - x0, depth = z1 - z0;
const edges = options.eaveEdges ?? { west: true, east: true, south: true, north: true };
if (edges.north) box(houseGroup, `${name} – okap sever`, xm, eave - 0.04, z1, width, 0.12, 0.12, mats.frame);
if (edges.south) box(houseGroup, `${name} – okap jih`, xm, eave - 0.04, z0, width, 0.12, 0.12, mats.frame);
if (edges.west) box(houseGroup, `${name} – okap západ`, x0, eave - 0.04, zm, 0.12, 0.12, depth, mats.frame);
if (edges.east) box(houseGroup, `${name} – okap východ`, x1, eave - 0.04, zm, 0.12, 0.12, depth, mats.frame);
return { x0, x1, z0, z1, ridgeX: xm, ridgeZ: zm, eave, ridgeHeight };
}
// The higher roof crosses the deeper wing. Its 13.05 m roof span and +5.832 m
// ridge follow the 23° section/elevation; the wing walls themselves are 11.67 m deep.
function hipRoofAcrossDepth(x0, x1, z0, z1, name, ridgeHeight) {
const eave = 3.046, overhangX = 0.38, overhangZ = 0.69;
x0 -= overhangX; x1 += overhangX; z0 -= overhangZ; z1 += overhangZ;
const zm = (z0 + z1) / 2;
const v = (x, y, z) => [x, y, z];
poly(houseGroup, `${name} – severní střešní rovina`, [
v(x0, eave, z1), v(x1, eave, z1), v(x1, ridgeHeight, zm), v(x0, ridgeHeight, zm)
], mats.roof);
poly(houseGroup, `${name} – jižní střešní rovina`, [
v(x1, eave, z0), v(x0, eave, z0), v(x0, ridgeHeight, zm), v(x1, ridgeHeight, zm)
], mats.roof);
poly(houseGroup, `${name} – západní valba`, [
v(x0, eave, z0), v(x0, eave, z1), v(x0, ridgeHeight, zm)
], mats.roof);
poly(houseGroup, `${name} – východní valba`, [
v(x1, eave, z1), v(x1, eave, z0), v(x1, ridgeHeight, zm)
], mats.roof);
box(houseGroup, `${name} – hřeben`, (x0 + x1) / 2, ridgeHeight, zm, x1 - x0, 0.12, 0.12, mats.roof);
box(houseGroup, `${name} – okap sever`, (x0 + x1) / 2, eave - 0.04, z1, x1 - x0, 0.12, 0.12, mats.frame);
box(houseGroup, `${name} – okap jih`, (x0 + x1) / 2, eave - 0.04, z0, x1 - x0, 0.12, 0.12, mats.frame);
box(houseGroup, `${name} – okap západ`, x0, eave - 0.04, zm, 0.12, 0.12, z1 - z0, mats.frame);
box(houseGroup, `${name} – okap východ`, x1, eave - 0.04, zm, 0.12, 0.12, z1 - z0, mats.frame);
function hipRoofHeight(roof, x, z) {
const fall = Math.max(
Math.abs(x - roof.ridgeX) / ((roof.x1 - roof.x0) / 2),
Math.abs(z - roof.ridgeZ) / ((roof.z1 - roof.z0) / 2)
);
return roof.eave + (roof.ridgeHeight - roof.eave) * (1 - fall);
}
function buildHouse() {
makeFootprintSlab();
const openings = [
[{ center: 2.0, width: 1.0, bottom: 0, height: 2.18, kind: 'door' }, { center: 6.2, width: 2.05, bottom: 0.85, height: 1.38 }],
[{ center: 2.5, width: 1.35, bottom: 0.9, height: 1.25 }, { center: 6.5, width: 1.1, bottom: 0.9, height: 1.25 }],
[{ center: 2.5, width: 1.65, bottom: 0.9, height: 1.35 }, { center: 7.4, width: 3.25, bottom: 0, height: 2.22, kind: 'door' }],
[{ center: 1.7, width: 1.55, bottom: 0.85, height: 1.4 }],
[{ center: 3.25, width: 3.4, bottom: 0, height: 2.2, kind: 'door' }],
[{ center: 2.2, width: 1.4, bottom: 0.9, height: 1.2 }, { center: 8.4, width: 1.7, bottom: 0.9, height: 1.2 }],
[{ center: 2.3, width: 1.45, bottom: 0.9, height: 1.25 }, { center: 5.4, width: 1.25, bottom: 0.9, height: 1.25 }],
// North long façade: three windows, positioned from the dimension chain in the floor plan.
[{ center: 1.66, width: 0.8, bottom: 1.3, height: 1.3 }, { center: 4.805, width: 1.3, bottom: 1.3, height: 1.3, panes: 2 }, { center: 7.525, width: 1.8, bottom: 1.3, height: 1.3, panes: 2 }],
// East end façade: two window groups.
[{ center: 1.8, width: 1.8, bottom: 1.3, height: 1.3, panes: 2 }, { center: 6.9, width: 1.8, bottom: 1.3, height: 1.3, panes: 2 }],
// South long façade: 1.8 m glazed door, 1.1 m door, and 1.8 m window from the dimension chain.
[{ center: 2.575, width: 1.8, bottom: 1.3, height: 1.3, panes: 2 }, { center: 4.925, width: 1.1, bottom: 0, height: 2.3, kind: 'door' }, { center: 8.475, width: 1.8, bottom: 0, height: 2.3, kind: 'glazedDoor', panes: 2 }],
[{ center: 1.8, width: 1.3, bottom: 1.3, height: 1.3 }],
// The garden-end door is 1.1 m wide in the plan.
[{ center: 1.125, width: 1.1, bottom: 0, height: 2.3, kind: 'door' }],
// West end: the entry door and two larger window groups shown in the elevations.
[{ center: 2.315, width: 1.8, bottom: 1.3, height: 1.3, panes: 2 }, { center: 6.221, width: 1.8, bottom: 1.3, height: 1.3, panes: 2 }, { center: 9.07, width: 1.1, bottom: 0, height: 2.3, kind: 'door' }],
[],
[]
];
for (let i = 0; i < footprint.length; i++) makeWallEdge(footprint[i], footprint[(i + 1) % footprint.length], openings[i]);
// The section shows the 23° lower roof ridge (+5.275 m); the elevations show the deeper wing ridge (+5.832 m).
hipRoof(-1.575, 8.7, -2.775, 6.525, 'Nižší valbová střecha', 5.275);
hipRoofAcrossDepth(-8.7, -1.575, -6.525, 5.145, 'Vyšší valbová střecha', 5.832);
// The section's 23° pitch and both elevations' ridge levels define two separate hipped masses.
// Their inner eaves meet at the common wall; neither roof projects through the other.
hipPyramid(-1.575, 8.7, -2.775, 6.525, 'Nižší valbová střecha', 5.275, {
overhang: { west: 0, east: 0.25, south: 0.38, north: 0.38 },
eaveEdges: { west: false, east: true, south: true, north: true }
});
const highRoof = hipPyramid(-8.7, -1.575, -6.525, 5.145, 'Vyšší valbová střecha', 5.832, {
overhang: { west: 0.25, east: 0, south: 0.69, north: 0.69 },
eaveEdges: { west: true, east: false, south: true, north: true }
});
// Chimney shown in the elevations, with a simple cap.
box(houseGroup, 'Komín', 5.7, 5.78, 1.8, 0.64, 1.25, 0.64, mats.foundation);
box(houseGroup, 'Hlavice komína', 5.7, 6.42, 1.8, 0.82, 0.12, 0.82, mats.frame);
// The elevations set the chimney top at +6.500 m and show 1.255 m above the roof plane.
// The base is cut to the sloping roof plane so the masonry cannot float above or pass through it.
const chimneyX = -5.14, chimneyZ = 0.69;
const chimneyRoofY = (x, z) => hipRoofHeight(highRoof, x, z);
slopedBox(houseGroup, 'Oplechování komína', chimneyX, chimneyZ, 0.82, 0.82,
chimneyRoofY, (x, z) => chimneyRoofY(x, z) + 0.08, mats.metal);
slopedBox(houseGroup, 'Komín', chimneyX, chimneyZ, 0.64, 0.64,
(x, z) => chimneyRoofY(x, z) + 0.05, () => 6.38, mats.foundation);
box(houseGroup, 'Hlavice komína', -5.14, 6.44, 0.69, 0.82, 0.12, 0.82, mats.frame);
// Covered terrace is included in the site sheets; its posts and canopy are deliberately simple.
// The covered terrace is 4.585 × 3.533 m and sits directly outside the south glazed door.
const deck = group(modelRoot, 'Krytá terasa – schematicky');
box(deck, 'Podlaha terasy', -4.0, -0.02, 0.05, 5.1, 0.18, 3.15, mats.terrace);
for (const [x, z] of [[-6.35, -1.38], [-1.65, -1.38], [-6.35, 1.48], [-1.65, 1.48]]) {
const ground = groundHeight(x, z);
box(deck, 'Sloupek kryté terasy', x, (2.92 + ground) / 2, z, 0.14, 2.92 - ground, 0.14, mats.wall);
deck.position.set(0.225, 0, houseGroup.position.z - 4.542);
box(deck, 'Podlaha terasy', 0, -0.15, 0, 4.585, 0.15, 3.533, mats.terrace);
for (const [x, z] of [[-2.12, -1.55], [2.12, -1.55], [-2.12, 1.55], [2.12, 1.55]]) {
const ground = groundHeight(deck.position.x + x, deck.position.z + z);
const postTop = 3.02;
box(deck, 'Sloupek kryté terasy', x, (postTop + ground) / 2, z, 0.14, postTop - ground, 0.14, mats.wall);
}
box(deck, 'Stříška terasy', -4.0, 3.02, 0.05, 5.35, 0.16, 3.35, mats.terrace);
box(deck, 'Schod z terasy', -4.0, -0.19, -1.7, 2.5, 0.18, 0.45, mats.path);
box(deck, 'Stříška terasy', 0, 3.1, 0, 5.05, 0.16, 4.0, mats.terrace);
box(deck, 'Schod z terasy', 0, -0.25, -2.0, 2.5, 0.18, 0.45, mats.path);
}
buildGround();