feat(fence): add height selection for fence panels and update related logic
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Introduced a new section in the UI for selecting fence heights, visible only when the fence option is enabled. - Updated the fence label to clarify the presence of gates. - Enhanced the JavaScript logic to manage the visibility of fence heights based on user selection. - Modified the fence rendering logic to accommodate different heights and ensure proper visibility based on the selected height. - Updated CSS to style the new fence height controls appropriately. - Adjusted the model configuration to reflect changes in the fence height options.
This commit is contained in:
+11
-3
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="./css/app.css">
|
<link rel="stylesheet" href="./css/app.css">
|
||||||
<link rel="stylesheet" href="./css/web.css">
|
<link rel="stylesheet" href="./css/web.css?v=20261026">
|
||||||
<meta name="robots" content="noindex,nofollow">
|
<meta name="robots" content="noindex,nofollow">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
id="siteParcel" autocomplete="off" checked><span>Celý pozemek</span></label>
|
id="siteParcel" autocomplete="off" checked><span>Celý pozemek</span></label>
|
||||||
<label class="site-switch" title="Parkovací plocha, napojení a schody; dvě orientační auta"><input
|
<label class="site-switch" title="Parkovací plocha, napojení a schody; dvě orientační auta"><input
|
||||||
type="checkbox" id="siteParking" autocomplete="off" checked><span>Parkování</span></label>
|
type="checkbox" id="siteParking" autocomplete="off" checked><span>Parkování</span></label>
|
||||||
<label class="site-switch" title="Plot z 3D panelů na podhrabových deskách; u příjezdu brána a branka"><input
|
<label class="site-switch" title="Plot z 3D panelů na podhrabových deskách; u domu branka a brána pro auta"><input
|
||||||
type="checkbox" id="siteFence" autocomplete="off"><span>Plot</span></label>
|
type="checkbox" id="siteFence" autocomplete="off"><span>Plot</span></label>
|
||||||
<button id="siteFit" class="site-fit" type="button" title="Zarámovat zapnuté okolí"
|
<button id="siteFit" class="site-fit" type="button" title="Zarámovat zapnuté okolí"
|
||||||
aria-label="Zarámovat zapnuté okolí"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
aria-label="Zarámovat zapnuté okolí"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
@@ -150,6 +150,14 @@
|
|||||||
<rect x="9" y="9" width="6" height="6" rx="1" />
|
<rect x="9" y="9" width="6" height="6" rx="1" />
|
||||||
</svg></button>
|
</svg></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="fenceHeights" class="site-controls fence-heights" role="group" aria-label="Výška plotu" hidden>
|
||||||
|
<span class="fence-heights-label">Výška plotu</span>
|
||||||
|
<button type="button" class="site-switch" data-fence-height="0" title="Panel 830 mm na podhrabové desce 200 mm">1,0 m</button>
|
||||||
|
<button type="button" class="site-switch" data-fence-height="1" title="Panel 1030 mm na podhrabové desce 200 mm">1,2 m</button>
|
||||||
|
<button type="button" class="site-switch" data-fence-height="2" title="Panel 1230 mm na podhrabové desce 200 mm">1,4 m</button>
|
||||||
|
<button type="button" class="site-switch" data-fence-height="3" title="Panel 1530 mm na podhrabové desce 200 mm">1,7 m</button>
|
||||||
|
<button type="button" class="site-switch" data-fence-height="4" title="Panel 1730 mm na podhrabové desce 200 mm; nejvyšší vyráběná kombinace do 2 m">1,9 m</button>
|
||||||
|
</div>
|
||||||
<div id="siteMeta" hidden aria-live="polite"></div>
|
<div id="siteMeta" hidden aria-live="polite"></div>
|
||||||
<div class="help">Tažením otáčej · Kolečkem přibliž
|
<div class="help">Tažením otáčej · Kolečkem přibliž
|
||||||
· Pravým tlačítkem posuň</div>
|
· Pravým tlačítkem posuň</div>
|
||||||
@@ -347,7 +355,7 @@
|
|||||||
<script>
|
<script>
|
||||||
// Firefox and Safari accept an import map only before the first module starts loading, so the map
|
// Firefox and Safari accept an import map only before the first module starts loading, so the map
|
||||||
// for Three.js is added here and bootstrap.js is requested only afterwards.
|
// for Three.js is added here and bootstrap.js is requested only afterwards.
|
||||||
fetch('./data/app-config.json',{cache:'no-cache'}).then(r=>r.json()).then(c=>{const m=document.createElement('script');m.type='importmap';m.textContent=JSON.stringify({imports:{three:new URL(c.threeModule,location.href).href}});document.head.appendChild(m);}).catch(()=>{}).finally(()=>{const s=document.createElement('script');s.type='module';s.src='./js/bootstrap.js?v=20261026';document.body.appendChild(s);});
|
fetch('./data/app-config.json',{cache:'no-cache'}).then(r=>r.json()).then(c=>{const m=document.createElement('script');m.type='importmap';m.textContent=JSON.stringify({imports:{three:new URL(c.threeModule,location.href).href}});document.head.appendChild(m);}).catch(()=>{}).finally(()=>{const s=document.createElement('script');s.type='module';s.src='./js/bootstrap.js?v=20261026e';document.body.appendChild(s);});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -18,3 +18,7 @@ body.interior-active #projectToggle{top:76px;left:25px}
|
|||||||
@media(max-height:550px){#projectToggle{top:auto;bottom:120px}}
|
@media(max-height:550px){#projectToggle{top:auto;bottom:120px}}
|
||||||
|
|
||||||
@media(max-width:850px){body.std-open #webStatus{display:none}}
|
@media(max-width:850px){body.std-open #webStatus{display:none}}
|
||||||
|
|
||||||
|
/* Výška plotu: druhá řada pod přepínači okolí, jen při zapnutém plotu. */
|
||||||
|
.fence-heights[hidden]{display:none}.fence-heights{gap:3px}.fence-heights-label{font-size:10px;color:#6e8066;padding:0 6px 0 5px;white-space:nowrap}.fence-heights button.site-switch{background:transparent;font-family:inherit;line-height:1.1}.fence-heights button.site-switch.selected{background:#e1ecdc}
|
||||||
|
@media(max-width:520px){.fence-heights-label{font-size:9px;padding:0 3px}.fence-heights button.site-switch{padding:8px 5px}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "locenice-web-01",
|
"version": "locenice-web-01",
|
||||||
"model": "./models/dum.glb?v=20261026c",
|
"model": "./models/dum.glb?v=20261026e",
|
||||||
"renderer": "auto",
|
"renderer": "auto",
|
||||||
"threeModule": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
|
"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",
|
"threeLoader": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/loaders/GLTFLoader.js",
|
||||||
|
|||||||
+16
-3
@@ -312,8 +312,10 @@ let backend=null,projectPanel=null;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Independent site layers: initSiteControls enables all three on each fresh open.
|
// Independent site layers: initSiteControls enables all three on each fresh open.
|
||||||
const site = { road: false, parcel: false, parking: false, fence: false, savedCamera: null, lastFocus: null };
|
// fenceHeight: index of the manufactured height (tools/fence.mjs FENCE.heights), default the tallest under 2 m
|
||||||
|
const site = { road: false, parcel: false, parking: false, fence: false, fenceHeight: 4, savedCamera: null, lastFocus: null };
|
||||||
const siteActive = () => site.road || site.parcel || site.parking;
|
const siteActive = () => site.road || site.parcel || site.parking;
|
||||||
|
const fenceHeightVisible = e => e.fenceOnly !== undefined ? e.fenceOnly === site.fenceHeight : e.fenceBand === undefined || e.fenceBand <= site.fenceHeight;
|
||||||
function meshVisible(m) {
|
function meshVisible(m) {
|
||||||
if (m.legacySite) return false;
|
if (m.legacySite) return false;
|
||||||
switch (m.siteLayer) {
|
switch (m.siteLayer) {
|
||||||
@@ -323,7 +325,7 @@ let backend=null,projectPanel=null;
|
|||||||
case 'road': return site.road;
|
case 'road': return site.road;
|
||||||
case 'parking': return site.parking;
|
case 'parking': return site.parking;
|
||||||
// Each fence part stands on one terrain slab and is shown only together with it.
|
// Each fence part stands on one terrain slab and is shown only together with it.
|
||||||
case 'fence': return showTerrain && site.fence && (m.std?.fenceRegion === 'parcel' ? site.parcel : m.std?.fenceRegion === 'north' ? siteActive() : true);
|
case 'fence': return showTerrain && site.fence && fenceHeightVisible(m.std || {}) && (m.std?.fenceRegion === 'parcel' ? site.parcel : m.std?.fenceRegion === 'north' ? siteActive() : true);
|
||||||
default: return showTerrain || !m.context;
|
default: return showTerrain || !m.context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,6 +376,7 @@ let backend=null,projectPanel=null;
|
|||||||
const ids = { road: 'siteRoad', parcel: 'siteParcel', parking: 'siteParking', fence: 'siteFence' };
|
const ids = { road: 'siteRoad', parcel: 'siteParcel', parking: 'siteParking', fence: 'siteFence' };
|
||||||
const input = $('#' + ids[key]); input.checked = site[key];
|
const input = $('#' + ids[key]); input.checked = site[key];
|
||||||
input.closest('label').classList.toggle('selected', site[key]);
|
input.closest('label').classList.toggle('selected', site[key]);
|
||||||
|
if (key === 'fence') $('#fenceHeights').hidden = !site.fence;
|
||||||
document.body.classList.toggle('site-active', siteActive());
|
document.body.classList.toggle('site-active', siteActive());
|
||||||
$('#siteMeta').hidden = !siteActive();
|
$('#siteMeta').hidden = !siteActive();
|
||||||
$('#siteMeta').textContent = 'C.3 + výškopis · mezi body interpolováno';
|
$('#siteMeta').textContent = 'C.3 + výškopis · mezi body interpolováno';
|
||||||
@@ -387,6 +390,13 @@ let backend=null,projectPanel=null;
|
|||||||
}
|
}
|
||||||
dirty = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
|
function setFenceHeight(index) {
|
||||||
|
const buttons = [...document.querySelectorAll('[data-fence-height]')];
|
||||||
|
if (!buttons.some(b => +b.dataset.fenceHeight === index)) return;
|
||||||
|
site.fenceHeight = index;
|
||||||
|
for (const b of buttons) { const on = +b.dataset.fenceHeight === index; b.classList.toggle('selected', on); b.setAttribute('aria-pressed', on); }
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
function updateSiteLight() {
|
function updateSiteLight() {
|
||||||
if(interior.enabled){
|
if(interior.enabled){
|
||||||
const center=[1,2,0],lightEye=vadd(center,scale(solar.light,45)),view=lookAt(lightEye,center,Math.abs(solar.light[1])>.999?[0,0,1]:[0,1,0]);
|
const center=[1,2,0],lightEye=vadd(center,scale(solar.light,45)),view=lookAt(lightEye,center,Math.abs(solar.light[1])>.999?[0,0,1]:[0,1,0]);
|
||||||
@@ -415,14 +425,17 @@ let backend=null,projectPanel=null;
|
|||||||
const input = $('#' + id); input.onchange = () => setSiteLayer(key, input.checked);
|
const input = $('#' + id); input.onchange = () => setSiteLayer(key, input.checked);
|
||||||
setSiteLayer(key, key !== 'fence', false);
|
setSiteLayer(key, key !== 'fence', false);
|
||||||
}
|
}
|
||||||
|
document.querySelectorAll('[data-fence-height]').forEach(b => b.onclick = () => setFenceHeight(+b.dataset.fenceHeight));
|
||||||
|
setFenceHeight(site.fenceHeight);
|
||||||
$('#siteFit').onclick = () => focusSite();
|
$('#siteFit').onclick = () => focusSite();
|
||||||
window.SITE_SIM = {
|
window.SITE_SIM = {
|
||||||
getState: () => ({
|
getState: () => ({
|
||||||
road: site.road, parcel: site.parcel, parking: site.parking, fence: site.fence, terrain: showTerrain,
|
road: site.road, parcel: site.parcel, parking: site.parking, fence: site.fence, fenceHeight: site.fenceHeight, terrain: showTerrain,
|
||||||
visibleObjects: meshes.filter(visible).length, referenceElevation: 591.840,
|
visibleObjects: meshes.filter(visible).length, referenceElevation: 591.840,
|
||||||
projectArea: 1010, digitizedArea: 987.89, houseGeometryUnchanged: true
|
projectArea: 1010, digitizedArea: 987.89, houseGeometryUnchanged: true
|
||||||
}),
|
}),
|
||||||
setLayer: (key, on, focus = true) => setSiteLayer(key, on, focus),
|
setLayer: (key, on, focus = true) => setSiteLayer(key, on, focus),
|
||||||
|
setFenceHeight: index => setFenceHeight(index),
|
||||||
focus: mode => focusSite(mode),
|
focus: mode => focusSite(mode),
|
||||||
reset: () => { for (const key of ['road', 'parcel', 'parking', 'fence']) setSiteLayer(key, false, false); setPreset('terrace'); },
|
reset: () => { for (const key of ['road', 'parcel', 'parking', 'fence']) setSiteLayer(key, false, false); setPreset('terrace'); },
|
||||||
getCamera: () => ({ theta, phi, distance, target: [...target], aspect }),
|
getCamera: () => ({ theta, phi, distance, target: [...target], aspect }),
|
||||||
|
|||||||
Vendored
+1
-1
@@ -17,7 +17,7 @@ try{
|
|||||||
document.getElementById('stdSavedState').textContent=JSON.stringify({selection:payload.selection});
|
document.getElementById('stdSavedState').textContent=JSON.stringify({selection:payload.selection});
|
||||||
}catch(error){console.warn('Invalid shared selection:',error);}
|
}catch(error){console.warn('Invalid shared selection:',error);}
|
||||||
window.LOCENICE_RESOURCES={config,catalog,assets,references,scene,site,modelBytes};
|
window.LOCENICE_RESOURCES={config,catalog,assets,references,scene,site,modelBytes};
|
||||||
await import('./app.js?v=20261026');
|
await import('./app.js?v=20261026e');
|
||||||
}catch(error){
|
}catch(error){
|
||||||
console.error(error);const box=document.getElementById('loading');box.className='error';box.innerHTML='<div class="errorDetail"></div><p>Nahrajte celou slozku vcetne js, css, data, models a assets. Otevrete ji pres HTTP(S), nikoli dvojklikem na HTML.</p><a href="./models/dum.glb" download>Stahnout model GLB</a>';
|
console.error(error);const box=document.getElementById('loading');box.className='error';box.innerHTML='<div class="errorDetail"></div><p>Nahrajte celou slozku vcetne js, css, data, models a assets. Otevrete ji pres HTTP(S), nikoli dvojklikem na HTML.</p><a href="./models/dum.glb" download>Stahnout model GLB</a>';
|
||||||
box.querySelector('.errorDetail').textContent=error.message;document.getElementById('webStatus').textContent='Chyba nacitani';
|
box.querySelector('.errorDetail').textContent=error.message;document.getElementById('webStatus').textContent='Chyba nacitani';
|
||||||
|
|||||||
Binary file not shown.
+131
-69
@@ -5,6 +5,10 @@
|
|||||||
*
|
*
|
||||||
* node tools/fence.mjs [models/dum.glb]
|
* node tools/fence.mjs [models/dum.glb]
|
||||||
*
|
*
|
||||||
|
* Výšku plotu volí web (řada „Výška plotu“): panel je složený z vodorovných pásů po vyráběných
|
||||||
|
* výškách (FENCE.heights) a nižší varianta jen skryje horní pásy; krytky sloupků a brány jsou
|
||||||
|
* pro každou výšku zvlášť. Uzly mají v extras fenceBand (vidět od varianty) nebo fenceOnly.
|
||||||
|
*
|
||||||
* Skript přepíše uzly Site_Fence_* v modelu (opakované spuštění plot nahradí, nepřidá druhý).
|
* 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ř, bez hrany
|
* 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.
|
* u silnice; výška se bere z viditelného terénu. Polohu a šířky bran určuje GATES.
|
||||||
@@ -18,13 +22,19 @@ import { parseGLB, encodeGLB } from '../js/model-io.js';
|
|||||||
const FENCE = {
|
const FENCE = {
|
||||||
inset: 0.06, // osa sloupků od hranice parcely dovnitř [m]
|
inset: 0.06, // osa sloupků od hranice parcely dovnitř [m]
|
||||||
postSpacing: 2.5, // max. rozteč sloupků = šířka panelu
|
postSpacing: 2.5, // max. rozteč sloupků = šířka panelu
|
||||||
panelHeight: 1.53,
|
// Vyráběné 3D panely (830–2030 mm) na podhrabové desce 200 mm (50 mm v zemi) do 2 m celkem:
|
||||||
boardHeight: 0.20, // nejmenší viditelná výška podhrabové desky nad nejvyšším terénem pole
|
// panel + 0,16 m = 1,0 / 1,2 / 1,4 / 1,7 / 1,9 m. Panel 2030 mm by sám přesáhl 2 m.
|
||||||
|
heights: [0.83, 1.03, 1.23, 1.53, 1.73],
|
||||||
|
maxHeight: 2.0, // kontrola: nikde nad terénem výš
|
||||||
|
rakeTolerance: 0.04, // max. odchylka terénu od přímky pole, jinak se pole zkrátí
|
||||||
|
minField: 0.6,
|
||||||
|
boardVisible: 0.15, // deska 200 mm, nad nejvyšším terénem pole z ní kouká 150 mm
|
||||||
boardThickness: 0.05,
|
boardThickness: 0.05,
|
||||||
post: [0.06, 0.04], // sloupek 60 × 40 mm
|
post: [0.06, 0.04], // sloupek 60 × 40 mm
|
||||||
wire: 0.005,
|
wire: 0.005,
|
||||||
wirePitch: 0.05, // oko 50 mm
|
wirePitch: 0.05, // oko 50 × 200 mm
|
||||||
bends: [0.35, 0.80, 1.25],
|
bends: [0.30, 0.62, 1.13, 1.40], // každý prolis celý uvnitř jednoho pásu
|
||||||
|
wires: [0, 0.18, 0.45], // + vodorovný drát na horní hraně každého pásu
|
||||||
bendDepth: 0.035,
|
bendDepth: 0.035,
|
||||||
bendHalf: 0.05,
|
bendHalf: 0.05,
|
||||||
};
|
};
|
||||||
@@ -32,9 +42,12 @@ const GATES = { // čára bran kousek za severní fasádou (x = -7.67)
|
|||||||
lineX: -7.45,
|
lineX: -7.45,
|
||||||
car: { houseZ: -4.695 }, // východní stěna; dvoukřídlá brána přes celou mezeru k plotu
|
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
|
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, // křídla jsou stejně vysoká jako panel zvolené výšky
|
||||||
post: 0.08,
|
|
||||||
};
|
};
|
||||||
|
const H = FENCE.heights, TOP = H.length - 1;
|
||||||
|
const bandOf = v => H.findIndex(h => v <= h + 1e-9);
|
||||||
|
let tallest = { h: 0 }; // nejvyšší místo plotu nad terénem (kontrola FENCE.maxHeight)
|
||||||
|
const track = (top, groundMin, where) => { if (top - groundMin > tallest.h) tallest = { h: top - groundMin, where }; };
|
||||||
const RAL7016 = '#383e42', CONCRETE = '#c2c1bb';
|
const RAL7016 = '#383e42', CONCRETE = '#c2c1bb';
|
||||||
|
|
||||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||||
@@ -106,14 +119,18 @@ const region = x => x < -11.17 ? 'north' : x > 13.53 ? 'parcel' : 'local'; //
|
|||||||
|
|
||||||
// --- geometry ---------------------------------------------------------------------------------
|
// --- geometry ---------------------------------------------------------------------------------
|
||||||
const meshes = {};
|
const meshes = {};
|
||||||
const bucket = (reg, part) => (meshes[reg + '_' + part] ||= { reg, part, pos: [], nor: [], idx: [] });
|
// vis: { band: j } = vidět od výšky j výš, { only: k } = jen u výšky k, {} = vždy
|
||||||
|
const bucket = (reg, part, vis = {}) => {
|
||||||
|
const key = reg + '_' + part + (vis.band !== undefined ? '_b' + vis.band : vis.only !== undefined ? '_h' + vis.only : '');
|
||||||
|
return (meshes[key] ||= { key, reg, part, vis, pos: [], nor: [], idx: [] });
|
||||||
|
};
|
||||||
const add = (a, b) => a.map((v, i) => v + b[i]), mul = (a, s) => a.map(v => v * s);
|
const add = (a, b) => a.map((v, i) => v + b[i]), mul = (a, s) => a.map(v => v * s);
|
||||||
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
||||||
const unit = a => mul(a, 1 / Math.hypot(...a));
|
const unit = a => mul(a, 1 / Math.hypot(...a));
|
||||||
// frame: origin o (x,0,z), u along the line, w outward, y up; local (u, y, w) -> world
|
// frame: origin o (x,0,z), u along the line, w outward, y up; local (u, y, w) -> world
|
||||||
const at = (f, u, y, w) => [f.o[0] + f.u[0] * u + f.w[0] * w, y, f.o[2] + f.u[2] * u + f.w[2] * w];
|
const at = (f, u, y, w) => [f.o[0] + f.u[0] * u + f.w[0] * w, y, f.o[2] + f.u[2] * u + f.w[2] * w];
|
||||||
function box(m, f, u0, u1, y0, y1, w0, w1) {
|
function box(m, f, u0, u1, y0, y1, w0, w1, rise = 0) { // rise: shear along u (pole ve svahu)
|
||||||
const P = (u, y, w) => at(f, u, y, w), faces = [
|
const P = (u, y, w) => at(f, u, y + rise * (u - u0) / (u1 - u0), w), faces = [
|
||||||
[[u0, y0, w1], [u1, y0, w1], [u1, y1, w1], [u0, y1, w1], f.w], [[u1, y0, w0], [u0, y0, w0], [u0, y1, w0], [u1, y1, w0], mul(f.w, -1)],
|
[[u0, y0, w1], [u1, y0, w1], [u1, y1, w1], [u0, y1, w1], f.w], [[u1, y0, w0], [u0, y0, w0], [u0, y1, w0], [u1, y1, w0], mul(f.w, -1)],
|
||||||
[[u1, y0, w1], [u1, y0, w0], [u1, y1, w0], [u1, y1, w1], f.u], [[u0, y0, w0], [u0, y0, w1], [u0, y1, w1], [u0, y1, w0], mul(f.u, -1)],
|
[[u1, y0, w1], [u1, y0, w0], [u1, y1, w0], [u1, y1, w1], f.u], [[u0, y0, w0], [u0, y0, w1], [u0, y1, w1], [u0, y1, w0], mul(f.u, -1)],
|
||||||
[[u0, y1, w1], [u1, y1, w1], [u1, y1, w0], [u0, y1, w0], [0, 1, 0]], [[u0, y0, w0], [u1, y0, w0], [u1, y0, w1], [u0, y0, w1], [0, -1, 0]],
|
[[u0, y1, w1], [u1, y1, w1], [u1, y1, w0], [u0, y1, w0], [0, 1, 0]], [[u0, y0, w0], [u1, y0, w0], [u1, y0, w1], [u0, y0, w1], [0, -1, 0]],
|
||||||
@@ -138,37 +155,69 @@ let outwardSign = 1; // set from t
|
|||||||
const edgeFrame = (a, b) => { const L = Math.hypot(b[0] - a[0], b[1] - a[1]), u = [(b[0] - a[0]) / L, 0, (b[1] - a[1]) / L]; return { o: [a[0], 0, a[1]], u, w: [outwardSign * u[2], 0, -outwardSign * u[0]], L }; };
|
const edgeFrame = (a, b) => { const L = Math.hypot(b[0] - a[0], b[1] - a[1]), u = [(b[0] - a[0]) / L, 0, (b[1] - a[1]) / L]; return { o: [a[0], 0, a[1]], u, w: [outwardSign * u[2], 0, -outwardSign * u[0]], L }; };
|
||||||
const sampleGround = (f, u0, u1, n = 6) => Array.from({ length: n }, (_, i) => { const p = at(f, u0 + (u1 - u0) * i / (n - 1), 0, 0); return groundY(p[0], p[2]); });
|
const sampleGround = (f, u0, u1, n = 6) => Array.from({ length: n }, (_, i) => { const p = at(f, u0 + (u1 - u0) * i / (n - 1), 0, 0); return groundY(p[0], p[2]); });
|
||||||
|
|
||||||
|
// Pole kopíruje svah: deska i panel jdou rovnoběžně s přímkou terénu mezi sloupky, takže plot má
|
||||||
|
// všude stejnou výšku nad zemí. Kde terén od přímky víc uhýbá, rozdělí se pole kratšími (refine).
|
||||||
|
const groundProfile = (f, u0, u1) => {
|
||||||
|
const n = Math.max(6, Math.ceil((u1 - u0) / 0.25) + 1), g = sampleGround(f, u0, u1, n);
|
||||||
|
const dev = g.map((v, i) => v - (g[0] + (g[n - 1] - g[0]) * i / (n - 1)));
|
||||||
|
return { g, up: Math.max(0, ...dev), down: Math.min(0, ...dev), rise: g[n - 1] - g[0] };
|
||||||
|
};
|
||||||
|
function refine(f, a, b, out) {
|
||||||
|
const p = groundProfile(f, a, b);
|
||||||
|
if (b - a > FENCE.minField && p.up - p.down > FENCE.rakeTolerance) { refine(f, a, (a + b) / 2, out); refine(f, (a + b) / 2, b, out); }
|
||||||
|
else out.push(b);
|
||||||
|
}
|
||||||
|
function fields(f, a, b) { // post positions in [a, b], a included
|
||||||
|
const n = Math.ceil((b - a) / FENCE.postSpacing - 1e-6), us = [a];
|
||||||
|
for (let i = 1; i <= n; i++) refine(f, a + (b - a) * (i - 1) / n, a + (b - a) * i / n, us);
|
||||||
|
return us;
|
||||||
|
}
|
||||||
function panel(f, u0, u1) {
|
function panel(f, u0, u1) {
|
||||||
const mid = at(f, (u0 + u1) / 2, 0, 0), reg = region(mid[0]), g = sampleGround(f, u0, u1);
|
const mid = at(f, (u0 + u1) / 2, 0, 0), reg = region(mid[0]), { g, up, down, rise } = groundProfile(f, u0, u1);
|
||||||
const boardTop = Math.max(...g) + FENCE.boardHeight * 0.75, boardBottom = Math.min(...g) - 0.05;
|
const slope = rise / (u1 - u0), base = u => g[0] + up + slope * (u - u0); // terén zvednutý nad hrboly
|
||||||
const [pw, pd] = FENCE.post, t = FENCE.boardThickness / 2, plane = pd / 2 + 0.006, y0 = boardTop + 0.01, H = FENCE.panelHeight;
|
const [pw, pd] = FENCE.post, t = FENCE.boardThickness / 2, plane = pd / 2 + 0.006;
|
||||||
box(bucket(reg, 'boards'), f, u0 + pw / 2, u1 - pw / 2, boardBottom, boardTop, -t, t);
|
const b0 = u0 + pw / 2, b1 = u1 - pw / 2;
|
||||||
const mesh = bucket(reg, 'mesh'), profile = [[0, 0]];
|
box(bucket(reg, 'boards'), f, b0, b1, base(b0) - up + down - 0.05, base(b0) + FENCE.boardVisible, -t, t, slope * (b1 - b0));
|
||||||
for (const b of FENCE.bends) profile.push([b - FENCE.bendHalf, 0], [b, FENCE.bendDepth], [b + FENCE.bendHalf, 0]);
|
const y0 = u => base(u) + FENCE.boardVisible + 0.01;
|
||||||
profile.push([H + 0.03, 0]); // trny nad horním drátem
|
|
||||||
const count = Math.floor((u1 - u0 - 0.07) / FENCE.wirePitch);
|
const count = Math.floor((u1 - u0 - 0.07) / FENCE.wirePitch);
|
||||||
const start = (u0 + u1) / 2 - count * FENCE.wirePitch / 2;
|
const start = (u0 + u1) / 2 - count * FENCE.wirePitch / 2, end = start + count * FENCE.wirePitch;
|
||||||
for (let i = 0; i <= count; i++) { const u = start + i * FENCE.wirePitch; ribbon(mesh, f, profile.map(([v, w]) => [u, y0 + v, plane + w]), [1, 0], FENCE.wire); }
|
const levels = [...FENCE.wires, ...H].map(v => [v, 0]).concat(FENCE.bends.map(b => [b, FENCE.bendDepth]));
|
||||||
const levels = [0, 0.2, 0.6, 1.0, 1.4, H - 0.03].map(v => [v, 0]).concat(FENCE.bends.map(b => [b, FENCE.bendDepth]));
|
H.forEach((hi, j) => { // pás j: od horní hrany pásu j-1 po hi
|
||||||
for (const [v, w] of levels) ribbon(mesh, f, [[start - 0.01, y0 + v, plane + w - 0.003], [start + count * FENCE.wirePitch + 0.01, y0 + v, plane + w - 0.003]], [0, 1], FENCE.wire);
|
const lo = j ? H[j - 1] : 0, mesh = bucket(reg, 'mesh', { band: j }), profile = [[lo, 0]];
|
||||||
return y0 + H;
|
for (const b of FENCE.bends) if (b > lo && b < hi) profile.push([b - FENCE.bendHalf, 0], [b, FENCE.bendDepth], [b + FENCE.bendHalf, 0]);
|
||||||
|
profile.push([hi, 0]);
|
||||||
|
for (let i = 0; i <= count; i++) { const u = start + i * FENCE.wirePitch; ribbon(mesh, f, profile.map(([v, w]) => [u, y0(u) + v, plane + w]), [1, 0], FENCE.wire); }
|
||||||
|
// vodorovné dráty pásu, v prolisu jeden na vrcholu
|
||||||
|
for (const [v, w] of levels) if (bandOf(v) === j) ribbon(mesh, f, [[start - 0.01, y0(start - 0.01) + v, plane + w - 0.003], [end + 0.01, y0(end + 0.01) + v, plane + w - 0.003]], [0, 1], FENCE.wire);
|
||||||
|
});
|
||||||
|
g.forEach((gy, i) => track(y0(u0 + (u1 - u0) * i / (g.length - 1)) + H[TOP], gy, `pole x ${mid[0].toFixed(1)} z ${mid[2].toFixed(1)}`));
|
||||||
|
return { start: y0(u0), end: y0(u1) }; // spodní hrana panelu u obou sloupků
|
||||||
}
|
}
|
||||||
function post(f, u, top, reg, size = FENCE.post) {
|
// sloupek z dílů po pásech + krytka pro každou výšku zvlášť; bottom = nejvyšší spodek sousedních panelů
|
||||||
const p = at(f, u, 0, 0), m = bucket(reg, 'posts'), [pw, pd] = size, g = groundY(p[0], p[2]);
|
function fencePost(f, u, bottom, bottomMin, reg) {
|
||||||
box(m, f, u - pw / 2, u + pw / 2, g - 0.3, top, -pd / 2, pd / 2);
|
const p = at(f, u, 0, 0), [pw, pd] = FENCE.post, g = groundY(p[0], p[2]);
|
||||||
box(m, f, u - pw / 2 - 0.002, u + pw / 2 + 0.002, top, top + 0.012, -pd / 2 - 0.002, pd / 2 + 0.002);
|
const top = k => Math.min(bottom + H[k] + 0.04, g + FENCE.maxHeight - 0.013); // s krytkou nikdy nad limitem
|
||||||
return g;
|
H.forEach((_, j) => {
|
||||||
|
const m = bucket(reg, 'posts', { band: j });
|
||||||
|
box(m, f, u - pw / 2, u + pw / 2, j ? top(j - 1) : g - 0.3, top(j), -pd / 2, pd / 2);
|
||||||
|
box(bucket(reg, 'caps', { only: j }), f, u - pw / 2 - 0.002, u + pw / 2 + 0.002, top(j), top(j) + 0.012, -pd / 2 - 0.002, pd / 2 + 0.002);
|
||||||
|
for (const v of j ? [H[j] - 0.12] : [0.25, 0.65]) box(m, f, u - 0.045, u + 0.045, bottomMin + v - 0.018, bottomMin + v + 0.018, pd / 2, pd / 2 + 0.03);
|
||||||
|
});
|
||||||
|
track(top(TOP) + 0.012, g, `sloupek x ${p[0].toFixed(1)} z ${p[2].toFixed(1)}`);
|
||||||
}
|
}
|
||||||
function clamps(f, u, y0, reg) {
|
function gatePost(f, u, top, reg, k) {
|
||||||
const m = bucket(reg, 'posts'), pd = FENCE.post[1] / 2;
|
const p = at(f, u, 0, 0), m = bucket(reg, 'gates', { only: k }), s = GATES.post, g = groundY(p[0], p[2]);
|
||||||
for (const v of [0.25, 0.75, 1.3]) box(m, f, u - 0.045, u + 0.045, y0 + v - 0.018, y0 + v + 0.018, pd, pd + 0.03);
|
top = Math.min(top, g + FENCE.maxHeight - 0.013);
|
||||||
|
box(m, f, u - s / 2, u + s / 2, g - 0.3, top, -s / 2, s / 2);
|
||||||
|
box(m, f, u - s / 2 - 0.002, u + s / 2 + 0.002, top, top + 0.012, -s / 2 - 0.002, s / 2 + 0.002);
|
||||||
|
track(top + 0.012, g, `sloupek brány x ${p[0].toFixed(1)} z ${p[2].toFixed(1)}`);
|
||||||
}
|
}
|
||||||
function gateLeaf(f, u0, u1, y0, y1, reg, latch) {
|
function gateLeaf(f, u0, u1, y0, y1, reg, latch, k) {
|
||||||
const m = bucket(reg, 'gates'), fr = 0.04, d = 0.02;
|
const m = bucket(reg, 'gates', { only: k }), fr = 0.04, d = 0.02;
|
||||||
box(m, f, u0, u0 + fr, y0, y1, -d, d); box(m, f, u1 - fr, u1, y0, y1, -d, d);
|
box(m, f, u0, u0 + fr, y0, y1, -d, d); box(m, f, u1 - fr, u1, y0, y1, -d, d);
|
||||||
box(m, f, u0 + fr, u1 - fr, y0, y0 + fr, -d, d); box(m, f, u0 + fr, u1 - fr, y1 - fr, y1, -d, d);
|
box(m, f, u0 + fr, u1 - fr, y0, y0 + fr, -d, d); box(m, f, u0 + fr, u1 - fr, y1 - fr, y1, -d, d);
|
||||||
if (u1 - u0 > 1.5) box(m, f, u0 + fr, u1 - fr, (y0 + y1) / 2 - 0.02, (y0 + y1) / 2 + 0.02, -d, d);
|
if (u1 - u0 > 1.5) box(m, f, u0 + fr, u1 - fr, (y0 + y1) / 2 - 0.02, (y0 + y1) / 2 + 0.02, -d, d);
|
||||||
const mesh = bucket(reg, 'mesh'), n = Math.floor((u1 - u0 - 2 * fr) / FENCE.wirePitch);
|
const mesh = bucket(reg, 'mesh', { only: k }), n = Math.floor((u1 - u0 - 2 * fr) / FENCE.wirePitch);
|
||||||
for (let i = 1; i < n; i++) { const u = u0 + fr + i * (u1 - u0 - 2 * fr) / n; ribbon(mesh, f, [[u, y0 + fr, 0], [u, y1 - fr, 0]], [1, 0], FENCE.wire); }
|
for (let i = 1; i < n; i++) { const u = u0 + fr + i * (u1 - u0 - 2 * fr) / n; ribbon(mesh, f, [[u, y0 + fr, 0], [u, y1 - fr, 0]], [1, 0], FENCE.wire); }
|
||||||
if (latch !== undefined) { // klika + zámek
|
if (latch !== undefined) { // klika + zámek
|
||||||
const s = Math.sign(latch - (u0 + u1) / 2), uL = latch - s * 0.07;
|
const s = Math.sign(latch - (u0 + u1) / 2), uL = latch - s * 0.07;
|
||||||
@@ -200,23 +249,20 @@ assert(junctions.length === 2 && east && west, 'gate line must cross both side f
|
|||||||
let posts = 0, panels = 0;
|
let posts = 0, panels = 0;
|
||||||
const edges = ring.slice(0, -1).map((a, e) => {
|
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];
|
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++) {
|
for (let k = 0; k < cuts.length - 1; k++) us.push(...fields(f, cuts[k], cuts[k + 1]).slice(1));
|
||||||
const n = Math.ceil((cuts[k + 1] - cuts[k]) / FENCE.postSpacing - 1e-6);
|
const bottoms = us.slice(1).map((u, i) => panel(f, us[i], u));
|
||||||
for (let i = 1; i <= n; i++) us.push(cuts[k] + (cuts[k + 1] - cuts[k]) * i / n);
|
panels += bottoms.length;
|
||||||
}
|
return { f, us, bottoms };
|
||||||
const tops = us.slice(1).map((u, i) => panel(f, us[i], u));
|
|
||||||
panels += tops.length;
|
|
||||||
return { f, us, tops };
|
|
||||||
});
|
});
|
||||||
edges.forEach(({ f, us, tops }, e) => {
|
// spodní hrany panelů u sloupku i (z obou stran)
|
||||||
|
const nearBottoms = (bottoms, i) => [i > 0 ? bottoms[i - 1].end : bottoms[0].start, i < bottoms.length ? bottoms[i].start : bottoms[i - 1].end];
|
||||||
|
edges.forEach(({ f, us, bottoms }, e) => {
|
||||||
us.forEach((u, i) => {
|
us.forEach((u, i) => {
|
||||||
if (e > 0 && i === 0) return; // roh má sloupek z konce předchozí hrany
|
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)]];
|
const near = nearBottoms(bottoms, i);
|
||||||
if (i === us.length - 1 && e < edges.length - 1) near.push(edges[e + 1].tops[0]);
|
if (i === us.length - 1 && e < edges.length - 1) near.push(edges[e + 1].bottoms[0].start);
|
||||||
const reg = region(at(f, u, 0, 0)[0]);
|
fencePost(f, u, Math.max(...near), Math.min(...near), region(at(f, u, 0, 0)[0])); posts++;
|
||||||
post(f, u, Math.max(...near) + 0.04, reg); posts++;
|
for (const j of junctions) if (j.e === e && Math.abs(j.t * f.L - u) < 1e-6) j.fenceBottom = Math.max(...near);
|
||||||
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
|
// gates: a line from the house wall to the side fence; d = distance from the house
|
||||||
@@ -226,33 +272,44 @@ function gateLine(houseZ, junction) {
|
|||||||
let f = edgeFrame(h, j);
|
let f = edgeFrame(h, j);
|
||||||
const flip = f.w[0] > 0; // outer side (w) faces the street
|
const flip = f.w[0] > 0; // outer side (w) faces the street
|
||||||
if (flip) f = edgeFrame(j, h);
|
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 };
|
const g = { f, L: f.L, reg: region(GATES.lineX), fenceBottom: junction.fenceBottom, 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.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;
|
const ground = (d0, d1) => sampleGround(f, ...g.span(d0, d1), 8);
|
||||||
g.leaf = (d0, d1, y0, y1, latch) => gateLeaf(f, ...g.span(d0, d1), y0, y1, g.reg, latch === undefined ? undefined : g.s(latch));
|
g.bottom = (d0, d1) => Math.max(...ground(d0, d1)) + 0.05;
|
||||||
|
// křídla vysoká jako panel výšky k, k sousednímu poli dorovnaná, ale nikde nad limitem
|
||||||
|
g.top = (d0, d1, bottom, neighbourTop, k) => Math.min(Math.min(...ground(d0, d1)) + FENCE.maxHeight - 0.02, Math.max(bottom + H[k], neighbourTop));
|
||||||
|
g.leaf = (d0, d1, y0, y1, latch, k) => {
|
||||||
|
track(y1, Math.min(...ground(d0, d1)), 'brána');
|
||||||
|
gateLeaf(f, ...g.span(d0, d1), y0, y1, g.reg, latch === undefined ? undefined : g.s(latch), k);
|
||||||
|
};
|
||||||
return g;
|
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
|
// 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 car = gateLine(GATES.car.houseZ, east);
|
||||||
{
|
H.forEach((h, k) => {
|
||||||
const y0 = car.bottom(0, car.L), y1 = gateTop(y0, car.fenceTop), half = car.L / 2;
|
const y0 = car.bottom(0, car.L), y1 = car.top(0, car.L, y0, car.fenceBottom + h, k), half = car.L / 2;
|
||||||
post(car.f, car.s(0), y1 + 0.06, car.reg, [gp, gp]);
|
gatePost(car.f, car.s(0), y1 + 0.06, car.reg, k);
|
||||||
post(car.f, car.s(car.L), Math.max(y1, car.fenceTop) + 0.06, car.reg, [gp, gp]);
|
gatePost(car.f, car.s(car.L), Math.max(y1, car.fenceBottom + h) + 0.06, car.reg, k);
|
||||||
car.leaf(gp / 2 + gap, half - 0.01, y0, y1, half - 0.01);
|
car.leaf(gp / 2 + gap, half - 0.01, y0, y1, half - 0.01, k);
|
||||||
car.leaf(half + 0.01, car.L - gp / 2 - gap, y0, y1);
|
car.leaf(half + 0.01, car.L - gp / 2 - gap, y0, y1, undefined, k);
|
||||||
}
|
});
|
||||||
// vpravo od silnice (u vchodu): branka pro lidi na dlažbě u domu, zbytek mezery pevné pole
|
// vpravo od silnice (u vchodu): branka pro lidi na dlažbě u domu, zbytek mezery pevné pole(e) ve svahu
|
||||||
const ped = gateLine(GATES.pedestrian.houseZ, west), dMid = gp + GATES.pedestrian.width;
|
const ped = gateLine(GATES.pedestrian.houseZ, west), dMid = gp + GATES.pedestrian.width;
|
||||||
{
|
{
|
||||||
const fixedTop = panel(ped.f, ...ped.span(dMid, ped.L));
|
const [a, b] = ped.span(dMid, ped.L), us = fields(ped.f, a, b), bottoms = us.slice(1).map((u, i) => panel(ped.f, us[i], u));
|
||||||
const y0 = ped.bottom(0, dMid), y1 = gateTop(y0, fixedTop);
|
const bottomAt = d => ped.s(d) === a ? bottoms[0].start : bottoms[bottoms.length - 1].end; // pole u branky / u plotu
|
||||||
post(ped.f, ped.s(0), y1 + 0.06, ped.reg, [gp, gp]);
|
us.forEach((u, i) => {
|
||||||
post(ped.f, ped.s(dMid), Math.max(y1, fixedTop) + 0.06, ped.reg, [gp, gp]);
|
const near = nearBottoms(bottoms, i);
|
||||||
post(ped.f, ped.s(ped.L), Math.max(fixedTop, ped.fenceTop) + 0.06, ped.reg, [gp, gp]);
|
if (i > 0 && i < us.length - 1) { fencePost(ped.f, u, Math.max(...near), Math.min(...near), ped.reg); posts++; }
|
||||||
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);
|
H.forEach((h, k) => {
|
||||||
panels++;
|
const y0 = ped.bottom(0, dMid), y1 = ped.top(0, dMid, y0, bottomAt(dMid) + h, k);
|
||||||
|
gatePost(ped.f, ped.s(0), y1 + 0.06, ped.reg, k);
|
||||||
|
gatePost(ped.f, ped.s(dMid), Math.max(y1, bottomAt(dMid) + h) + 0.06, ped.reg, k);
|
||||||
|
gatePost(ped.f, ped.s(ped.L), Math.max(bottomAt(ped.L), ped.fenceBottom) + h + 0.06, ped.reg, k);
|
||||||
|
ped.leaf(gp / 2 + gap, dMid - gp / 2 - gap, y0, y1, dMid - gp / 2 - gap, k);
|
||||||
|
});
|
||||||
|
panels += bottoms.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- write into the glTF ----------------------------------------------------------------------
|
// --- write into the glTF ----------------------------------------------------------------------
|
||||||
@@ -279,12 +336,15 @@ wanted.forEach((m, k) => {
|
|||||||
attributes: { POSITION: accessor(m.pos, 'VEC3', 5126, 34962), NORMAL: accessor(m.nor, 'VEC3', 5126, 34962) },
|
attributes: { POSITION: accessor(m.pos, 'VEC3', 5126, 34962), NORMAL: accessor(m.nor, 'VEC3', 5126, 34962) },
|
||||||
indices: accessor(m.idx, 'SCALAR', m.pos.length < 65536 ? 5123 : 5125, 34963), material: m.part === 'boards' ? MAT.concrete : MAT.metal,
|
indices: accessor(m.idx, 'SCALAR', m.pos.length < 65536 ? 5123 : 5125, 34963), material: m.part === 'boards' ? MAT.concrete : MAT.metal,
|
||||||
};
|
};
|
||||||
const name = `Site_Fence_${m.reg}_${m.part}`;
|
const name = `Site_Fence_${m.key}`;
|
||||||
let ni = existing[k];
|
let ni = existing[k];
|
||||||
if (ni === undefined) { gltf.meshes.push({ name, primitives: [prim] }); gltf.nodes.push({ mesh: gltf.meshes.length - 1 }); ni = gltf.nodes.length - 1; root.push(ni); }
|
if (ni === undefined) { gltf.meshes.push({ name, primitives: [prim] }); gltf.nodes.push({ mesh: gltf.meshes.length - 1 }); ni = gltf.nodes.length - 1; root.push(ni); }
|
||||||
else { gltf.meshes[gltf.nodes[ni].mesh] = { name, primitives: [prim] }; if (!root.includes(ni)) root.push(ni); }
|
else { gltf.meshes[gltf.nodes[ni].mesh] = { name, primitives: [prim] }; if (!root.includes(ni)) root.push(ni); }
|
||||||
gltf.nodes[ni].name = name;
|
gltf.nodes[ni].name = name;
|
||||||
gltf.nodes[ni].extras = { siteLayer: 'fence', fenceRegion: m.reg, fencePart: m.part, castShadow: m.part !== 'mesh', loceniceId: 'node-' + ni };
|
gltf.nodes[ni].extras = {
|
||||||
|
siteLayer: 'fence', fenceRegion: m.reg, fencePart: m.part, castShadow: m.part !== 'mesh',
|
||||||
|
...(m.vis.band !== undefined ? { fenceBand: m.vis.band } : {}), ...(m.vis.only !== undefined ? { fenceOnly: m.vis.only } : {}), loceniceId: 'node-' + ni,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
for (const ni of existing.slice(wanted.length)) { // nepotřebné staré uzly odpojit
|
for (const ni of existing.slice(wanted.length)) { // nepotřebné staré uzly odpojit
|
||||||
delete gltf.nodes[ni].mesh; gltf.nodes[ni].name += '_unused';
|
delete gltf.nodes[ni].mesh; gltf.nodes[ni].name += '_unused';
|
||||||
@@ -303,9 +363,11 @@ let total = 0; const place = order.map(i => { total = (total + 3) & ~3; const of
|
|||||||
const bin = new Uint8Array((total + 3) & ~3);
|
const bin = new Uint8Array((total + 3) & ~3);
|
||||||
gltf.bufferViews = place.map(([i, off]) => { bin.set(viewBytes[i], off); const v = { ...views[i], byteOffset: off, byteLength: viewBytes[i].length }; return v; });
|
gltf.bufferViews = place.map(([i, off]) => { bin.set(viewBytes[i], off); const v = { ...views[i], byteOffset: off, byteLength: viewBytes[i].length }; return v; });
|
||||||
gltf.buffers[0].byteLength = bin.length;
|
gltf.buffers[0].byteLength = bin.length;
|
||||||
|
assert(tallest.h <= FENCE.maxHeight, `plot přesahuje ${FENCE.maxHeight} m: ${tallest.h.toFixed(3)} m (${tallest.where}); model nezapsán`);
|
||||||
fs.writeFileSync(file, encodeGLB(gltf, bin));
|
fs.writeFileSync(file, encodeGLB(gltf, bin));
|
||||||
|
|
||||||
const tris = wanted.reduce((s, m) => s + m.idx.length / 3, 0);
|
const tris = wanted.reduce((s, m) => s + m.idx.length / 3, 0);
|
||||||
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(`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(`nejvyšší místo nad terénem: ${tallest.h.toFixed(3)} m (${tallest.where}), limit ${FENCE.maxHeight} m`);
|
||||||
|
console.log(`uzly: ${wanted.length} (pásy, krytky a brány pro výšky ${H.map(h => (h + FENCE.boardVisible + 0.01).toFixed(2)).join(' / ')} m)`);
|
||||||
console.log(`zapsáno ${file} (${fs.statSync(file).size} B)`);
|
console.log(`zapsáno ${file} (${fs.statSync(file).size} B)`);
|
||||||
|
|||||||
Reference in New Issue
Block a user