diff --git a/locenice/data/app-config.json b/locenice/data/app-config.json index c617ea1..a227129 100644 --- a/locenice/data/app-config.json +++ b/locenice/data/app-config.json @@ -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", diff --git a/locenice/models/dum.glb b/locenice/models/dum.glb index 6331557..443ea73 100644 Binary files a/locenice/models/dum.glb and b/locenice/models/dum.glb differ diff --git a/locenice/tools/fence.mjs b/locenice/tools/fence.mjs index 7c78ac4..1901618 100644 --- a/locenice/tools/fence.mjs +++ b/locenice/tools/fence.mjs @@ -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)`);