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
+1 -1
View File
@@ -24,7 +24,7 @@ Potom otevřete <http://localhost:8000>. Prohlížeč potřebuje WebGL a připoj
Z půdorysu a pohledů vychází obrys domu 17,40 × 13,05 m, zastavěná plocha 178,75 m², užitná plocha 153,74 m² a úrovně hřebenů +5,275 a +5,832 m. Řez uvádí sklon střechy 23°. Hmota je modelována ve dvou navazujících křídlech; vyšší střecha je omezena na hlubší křídlo a její rozpon vychází z řezu a pohledů. Situace uvádí parcelu 66/17 o výměře 1 037 m² a 206,54 m² včetně krytého vstupu a terasy. Dvě parkovací stání jsou u komunikace vpravo od domu; plocha je zakreslená přibližně 6,625 × 5,0 m. Ve výškopisu jsou zaměřené hodnoty přibližně 349,5 m n. m. u komunikace a 347,2 m n. m. u zadní části parcely.
Objem domu a terén jsou zjednodušené z PDF. Rozmístění otvorů a střešní napojení jsou schematické. Hranice parcely je převzatá graficky ze situačního PDF, nikoli z vytyčovacích souřadnic. Výšky terénu jsou interpolované mezi zaměřenými body. Příjezd, parkovací plocha a materiály slouží k prostorové orientaci. Model není BIM ani podklad pro stavbu, zemní práce nebo vytyčení.
Objem domu, polohy a rozměry hlavních otvorů a výšky střech vycházejí z kót půdorysu, pohledů a řezu. Sklon střechy odpovídá řezu 23°; vyšší a nižší valbová hmota jsou v místě napojení zjednodušené a komín je usazen na šikmé rovině střechy. Rámy otvorů, terén, příjezd a parkovací plocha slouží k prostorové orientaci. Hranice parcely je převzatá graficky ze situačního PDF, nikoli z vytyčovacích souřadnic; výšky terénu jsou interpolované mezi zaměřenými body. Model není BIM ani podklad pro stavbu, zemní práce nebo vytyčení.
Soubor `data/model.json` uchovává hlavní údaje projektu. Dodané PDF jsou zkopírované do `references/` a dostupné z panelu Projekt. Barvy fasády, střešní krytiny, oken a vybavení nejsou zatím vybrané.
+6 -6
View File
@@ -6,9 +6,9 @@
<meta name="robots" content="noindex,nofollow">
<meta name="theme-color" content="#e7ebe3">
<title>Klabava | Rodinný dům</title>
<link rel="stylesheet" href="./css/app.css?v=20261031b">
<link rel="stylesheet" href="./css/web.css?v=20261031b">
<link rel="stylesheet" href="./css/klabava.css?v=20261031b">
<link rel="stylesheet" href="./css/app.css?v=20261031c">
<link rel="stylesheet" href="./css/web.css?v=20261031c">
<link rel="stylesheet" href="./css/klabava.css?v=20261031c">
<script type="importmap">
{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/"}}
</script>
@@ -59,8 +59,8 @@
</div>
<details class="site-note" open>
<summary>Rozsah modelu a přesnost</summary>
<p>Půdorysná hmota, hlavní otvory a výšky vycházejí z dodaných výkresů. Střecha je schematicky složená z valbových ploch. Terén navazuje na zaměřené body přibližně od 349,5 m n. m. u komunikace k 347,2 m n. m. v zadní části parcely; mezi body je interpolovaný.</p>
<p>Hranice parcely je převzatá graficky z PDF situace, ne ze souřadnic pro vytyčení. Příjezd, parkovací plocha, okna a materiály jsou zjednodušené pro prostorovou orientaci. Model slouží k prezentaci a studii oslunění, nikoli jako BIM, geodetický nebo stavební podklad.</p>
<p>Půdorysná hmota, poloha a rozměry hlavních otvorů a výšky střech vycházejí z půdorysu, pohledů a řezu. Střecha je složená ze dvou valbových hmot se sklony podle řezu; komín dosedá na šikmou rovinu. Terén navazuje na zaměřené body přibližně od 349,5 m n. m. u komunikace k 347,2 m n. m. v zadní části parcely; mezi body je interpolovaný.</p>
<p>Hranice parcely je převzatá graficky z PDF situace, ne ze souřadnic pro vytyčení. Napojení střešních hmot, rámy oken a dveří, příjezd, parkovací plocha a materiály jsou zjednodušené pro prostorovou orientaci. Model slouží k prezentaci a studii oslunění, nikoli jako BIM, geodetický nebo stavební podklad.</p>
</details>
<div class="status"><span class="dot"></span>Pracovní vizualizace · standardy doplníme po jejich výběru</div>
</aside>
@@ -120,6 +120,6 @@
<div id="toast" role="status" aria-live="polite"></div>
<div id="webStatus">Pracovní model · Klabava 66/17</div>
<script type="module" src="./js/klabava.js?v=20261031b"></script>
<script type="module" src="./js/klabava.js?v=20261031c"></script>
</body>
</html>
+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();