fix: Update model version in app-config.json and adjust fence script for gate dimensions
continuous-integration/drone/push Build is running

This commit is contained in:
martin.fencl
2026-10-26 14:39:41 +01:00
parent 9defe3ac98
commit ae85455dc3
3 changed files with 76 additions and 39 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"version": "locenice-web-01",
"model": "./models/dum.glb?v=20261026b",
"model": "./models/dum.glb?v=20261026c",
"renderer": "auto",
"threeModule": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
"threeLoader": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/loaders/GLTFLoader.js",
Binary file not shown.
+75 -38
View File
@@ -1,12 +1,13 @@
/**
* Plot kolem parcely 3480/37: 3D svařované panely na betonových podhrabových deskách,
* u příjezdu dvoukřídlá brána pro auto a branka pro lidi. Na webu ho zapíná přepínač „Plot“.
* Plot kolem parcely 3480/37: 3D svařované panely na betonových podhrabových deskách. Ke silnici
* je pozemek otevřený; zahradu uzavírají brány mezi domem a bočními ploty u severní fasády:
* vpravo od silnice (u vchodu) branka pro lidi, vlevo brána pro auta. Na webu přepínač „Plot“.
*
* node tools/fence.mjs [models/dum.glb]
*
* Skript přepíše uzly Site_Fence_* v modelu (opakované spuštění plot nahradí, nepřidá druhý).
* Linie plotu = obrys Site_Cadastral_outline posunutý o FENCE.inset dovnitř; výška se bere
* z viditelného terénu. Pozice bran jsou souřadnice z na hraně parcely u silnice (GATES).
* Linie plotu = obrys Site_Cadastral_outline posunutý o FENCE.inset dovnitř, bez hrany
* u silnice; výška se bere z viditelného terénu. Polohu a šířky bran určuje GATES.
* Po změně modelu zvyšte ?v= u "model" v data/app-config.json.
*/
import fs from 'node:fs';
@@ -27,9 +28,10 @@ const FENCE = {
bendDepth: 0.035,
bendHalf: 0.05,
};
const GATES = { // z na hraně u silnice; branka naproti schodům, brána přes obě stání
pedestrian: [0.40, -0.85],
car: [-0.85, -5.05],
const GATES = { // čára bran kousek za severní fasádou (x = -7.67), sloupek tak mine svod v SV rohu
lineX: -7.45,
car: { houseZ: -4.695 }, // východní stěna; dvoukřídlá brána přes celou mezeru k plotu
pedestrian: { houseZ: 3.902, width: 1.10 }, // obklad u vstupu; branka na dlažbě u domu, zbytek pevné pole
height: 1.50,
post: 0.08,
};
@@ -180,43 +182,78 @@ function gateLeaf(f, u0, u1, y0, y1, reg, latch) {
const corners = inset(outline(), FENCE.inset);
outwardSign = signedArea(corners) > 0 ? 1 : -1; // right-hand normal of a CCW ring points out
const streetIndex = corners.reduce((best, c, i) => (c[0] + corners[(i + 1) % corners.length][0]) < (corners[best][0] + corners[(best + 1) % corners.length][0]) ? i : best, 0);
const S0 = corners[streetIndex], S1 = corners[(streetIndex + 1) % corners.length];
const onStreet = z => { const t = (z - S0[1]) / (S1[1] - S0[1]); if (t < 0 || t > 1) throw Error('gate outside street edge: ' + z); return [S0[0] + (S1[0] - S0[0]) * t, z]; };
const [pedA, mid] = GATES.pedestrian.map(onStreet), carEnd = onStreet(GATES.car[1]);
assert(Math.abs(GATES.pedestrian[1] - GATES.car[0]) < 1e-9, 'gates must share the middle post');
function assert(ok, msg) { if (!ok) throw Error(msg); }
// fence: from the car gate around the parcel back to the pedestrian gate
const ring = [carEnd];
// fence: from one street corner around the garden to the other; the street side stays open
const ring = [];
for (let i = 1; i <= corners.length; i++) ring.push(corners[(streetIndex + i) % corners.length]);
ring.push(pedA);
let posts = 0, panels = 0;
const edges = [];
// where the gate line meets the side fences (a post is forced there)
const junctions = [];
for (let e = 0; e < ring.length - 1; e++) {
const f = edgeFrame(ring[e], ring[e + 1]), n = Math.ceil(f.L / FENCE.postSpacing - 1e-6), tops = [];
for (let i = 0; i < n; i++) { tops.push(panel(f, f.L * i / n, f.L * (i + 1) / n)); panels++; }
edges.push({ f, n, tops });
const [a, b] = [ring[e], ring[e + 1]];
if ((a[0] - GATES.lineX) * (b[0] - GATES.lineX) > 0) continue;
const t = (GATES.lineX - a[0]) / (b[0] - a[0]);
junctions.push({ e, t, z: a[1] + t * (b[1] - a[1]) });
}
edges.forEach(({ f, n, tops }, e) => {
for (let i = 0; i <= n; i++) {
if (e > 0 && i === 0) continue; // roh má sloupek z konce předchozí hrany
const near = [tops[Math.max(0, i - 1)], tops[Math.min(n - 1, i)]];
if (i === n && e < edges.length - 1) near.push(edges[e + 1].tops[0]);
const atGate = (e === 0 && i === 0) || (e === edges.length - 1 && i === n);
const u = f.L * i / n, reg = region(at(f, u, 0, 0)[0]);
if (!atGate) { post(f, u, Math.max(...near) + 0.04, reg); posts++; }
clamps(f, u, Math.min(...near) - FENCE.panelHeight, reg);
const east = junctions.find(j => j.z < 0), west = junctions.find(j => j.z > 0);
assert(junctions.length === 2 && east && west, 'gate line must cross both side fences');
let posts = 0, panels = 0;
const edges = ring.slice(0, -1).map((a, e) => {
const f = edgeFrame(a, ring[e + 1]), cuts = [0, ...junctions.filter(j => j.e === e).map(j => j.t * f.L), f.L], us = [0];
for (let k = 0; k < cuts.length - 1; k++) {
const n = Math.ceil((cuts[k + 1] - cuts[k]) / FENCE.postSpacing - 1e-6);
for (let i = 1; i <= n; i++) us.push(cuts[k] + (cuts[k + 1] - cuts[k]) * i / n);
}
const tops = us.slice(1).map((u, i) => panel(f, us[i], u));
panels += tops.length;
return { f, us, tops };
});
// gates along the street edge (pedA -> mid -> carEnd)
const gf = edgeFrame(pedA, carEnd), gp = GATES.post, uMid = Math.hypot(mid[0] - pedA[0], mid[1] - pedA[1]), uEnd = gf.L;
const gy = (u0, u1) => Math.max(...sampleGround(gf, u0, u1, 8)) + 0.05;
const pedY = gy(0, uMid), carY = gy(uMid, uEnd), top = Math.max(pedY, carY) + GATES.height + 0.06;
for (const u of [0, uMid, uEnd]) post(gf, u, top, 'north', [gp, gp]);
gateLeaf(gf, gp / 2 + 0.02, uMid - gp / 2 - 0.02, pedY, pedY + GATES.height, 'north', uMid - gp / 2 - 0.02);
const half = (uMid + uEnd) / 2;
gateLeaf(gf, uMid + gp / 2 + 0.02, half - 0.01, carY, carY + GATES.height, 'north', half - 0.01);
gateLeaf(gf, half + 0.01, uEnd - gp / 2 - 0.02, carY, carY + GATES.height, 'north');
edges.forEach(({ f, us, tops }, e) => {
us.forEach((u, i) => {
if (e > 0 && i === 0) return; // roh má sloupek z konce předchozí hrany
const near = [tops[Math.max(0, i - 1)], tops[Math.min(tops.length - 1, i)]];
if (i === us.length - 1 && e < edges.length - 1) near.push(edges[e + 1].tops[0]);
const reg = region(at(f, u, 0, 0)[0]);
post(f, u, Math.max(...near) + 0.04, reg); posts++;
clamps(f, u, Math.min(...near) - FENCE.panelHeight, reg);
for (const j of junctions) if (j.e === e && Math.abs(j.t * f.L - u) < 1e-6) j.fenceTop = Math.max(...near);
});
});
// gates: a line from the house wall to the side fence; d = distance from the house
const gp = GATES.post, gap = 0.02;
function gateLine(houseZ, junction) {
const dir = Math.sign(junction.z - houseZ), h = [GATES.lineX, houseZ + dir * (gp / 2 + 0.005)], j = [GATES.lineX, junction.z];
let f = edgeFrame(h, j);
const flip = f.w[0] > 0; // outer side (w) faces the street
if (flip) f = edgeFrame(j, h);
const g = { f, L: f.L, reg: region(GATES.lineX), fenceTop: junction.fenceTop, s: d => flip ? f.L - d : d };
g.span = (d0, d1) => [Math.min(g.s(d0), g.s(d1)), Math.max(g.s(d0), g.s(d1))];
g.bottom = (d0, d1) => Math.max(...sampleGround(f, ...g.span(d0, d1), 8)) + 0.05;
g.leaf = (d0, d1, y0, y1, latch) => gateLeaf(f, ...g.span(d0, d1), y0, y1, g.reg, latch === undefined ? undefined : g.s(latch));
return g;
}
const gateTop = (bottom, neighbourTop) => Math.min(bottom + 1.8, Math.max(bottom + GATES.height, neighbourTop));
// vlevo od silnice: dvoukřídlá brána pro auta přes celou mezeru mezi domem a plotem
const car = gateLine(GATES.car.houseZ, east);
{
const y0 = car.bottom(0, car.L), y1 = gateTop(y0, car.fenceTop), half = car.L / 2;
post(car.f, car.s(0), y1 + 0.06, car.reg, [gp, gp]);
post(car.f, car.s(car.L), Math.max(y1, car.fenceTop) + 0.06, car.reg, [gp, gp]);
car.leaf(gp / 2 + gap, half - 0.01, y0, y1, half - 0.01);
car.leaf(half + 0.01, car.L - gp / 2 - gap, y0, y1);
}
// vpravo od silnice (u vchodu): branka pro lidi na dlažbě u domu, zbytek mezery pevné pole
const ped = gateLine(GATES.pedestrian.houseZ, west), dMid = gp + GATES.pedestrian.width;
{
const fixedTop = panel(ped.f, ...ped.span(dMid, ped.L));
const y0 = ped.bottom(0, dMid), y1 = gateTop(y0, fixedTop);
post(ped.f, ped.s(0), y1 + 0.06, ped.reg, [gp, gp]);
post(ped.f, ped.s(dMid), Math.max(y1, fixedTop) + 0.06, ped.reg, [gp, gp]);
post(ped.f, ped.s(ped.L), Math.max(fixedTop, ped.fenceTop) + 0.06, ped.reg, [gp, gp]);
for (const d of [dMid, ped.L]) clamps(ped.f, ped.s(d), fixedTop - FENCE.panelHeight, ped.reg);
ped.leaf(gp / 2 + gap, dMid - gp / 2 - gap, y0, y1, dMid - gp / 2 - gap);
panels++;
}
// --- write into the glTF ----------------------------------------------------------------------
const srgbToLinear = h => [1, 3, 5].map(i => { const c = parseInt(h.slice(i, i + 2), 16) / 255; return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; });
@@ -269,6 +306,6 @@ gltf.buffers[0].byteLength = bin.length;
fs.writeFileSync(file, encodeGLB(gltf, bin));
const tris = wanted.reduce((s, m) => s + m.idx.length / 3, 0);
console.log(`plot: ${panels} polí, ${posts} sloupků, brána ${(uEnd - uMid - gp).toFixed(2)} m, branka ${(uMid - gp).toFixed(2)} m, ${tris} trojúhelníků`);
console.log(`plot: ${panels} polí, ${posts} sloupků, brána ${(car.L - gp).toFixed(2)} m, branka ${GATES.pedestrian.width.toFixed(2)} m + pole ${(ped.L - dMid).toFixed(2)} m, ${tris} trojúhelníků`);
console.log('uzly:', wanted.map(m => `Site_Fence_${m.reg}_${m.part} (${m.idx.length / 3})`).join(', '));
console.log(`zapsáno ${file} (${fs.statSync(file).size} B)`);