Update optional surroundings behavior and improve site controls initialization
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
+30
-15
@@ -1350,7 +1350,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Optional surroundings: independent switches, all off on every fresh open. */
|
||||
/* Optional surroundings: independent switches, all on when the page opens. */
|
||||
.site-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1555,7 +1555,7 @@
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="sun-open">
|
||||
<body>
|
||||
<canvas id="view" aria-label="Interaktivní trojrozměrný model rodinného domu"></canvas>
|
||||
<svg id="measure" aria-hidden="true"></svg>
|
||||
<header>
|
||||
@@ -1568,7 +1568,7 @@
|
||||
<div class="metric"><strong>23°</strong><small>Střecha</small></div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="topright"><button class="btn active" id="sunToggle" aria-controls="sunPanel" aria-expanded="true"
|
||||
<div class="topright"><button class="btn" id="sunToggle" aria-controls="sunPanel" aria-expanded="false"
|
||||
title="Ovládání slunce"><span class="solar-mark">☀</span><span
|
||||
class="toggle-word">Slunce</span></button><button class="btn icon" id="infoButton"
|
||||
aria-label="Nastavení a informace o modelu" title="Nastavení">⚙︎</button><button
|
||||
@@ -1607,7 +1607,7 @@
|
||||
|
||||
<details class="site-note">
|
||||
<summary>Okolí, výškopis a přesnost</summary>
|
||||
<p><b>Tři spodní přepínače jsou nezávislé a při otevření vypnuté.</b> Zapnutí zároveň zaměří příslušný
|
||||
<p><b>Tři spodní přepínače jsou nezávislé a při otevření zapnuté.</b> Zapnutí zároveň zaměří příslušný
|
||||
pohled. Ikona vedle přepínačů zarámuje zapnuté okolí; Vstup, Terasa a Reset vrátí pohled k domu. Vypnutí
|
||||
poslední vrstvy obnoví předchozí pohled.</p>
|
||||
<p>Geometrie, měřítko a poloha původního domu, otvorů, střechy, teras a jejich schodů jsou zachované.
|
||||
@@ -1649,21 +1649,21 @@
|
||||
jihovýchod</span></div>
|
||||
<div class="bottom">
|
||||
<nav class="toolbar" aria-label="Ovládání modelu"><button class="tool"
|
||||
data-preset="entry">Vstup</button><button class="tool active"
|
||||
data-preset="entry">Vstup</button><button class="tool"
|
||||
data-preset="terrace">Terasa</button><button class="tool" data-preset="top">Shora</button><span
|
||||
class="divider"></span><button class="tool" id="terrainButton"
|
||||
title="Zobrazit nebo skrýt terén">Terén</button><button class="tool"
|
||||
id="measureButton">Kóty</button><button class="tool optional"
|
||||
id="measureButton">Kóty</button><button class="tool optional active"
|
||||
id="rotateButton">Otáčení</button><span class="divider"></span><button class="tool"
|
||||
id="resetView" title="Obnovit výchozí pohled">Reset</button></nav>
|
||||
|
||||
<div class="site-controls" role="group" aria-label="Volitelné okolí domu; výchozí stav vypnuto">
|
||||
<div class="site-controls" role="group" aria-label="Volitelné okolí domu; výchozí stav zapnuto">
|
||||
<label class="site-switch" title="Komunikace a navazující terén mezi ulicí a domem"><input type="checkbox"
|
||||
id="siteRoad" autocomplete="off"><span>Cesta a přístup</span></label>
|
||||
id="siteRoad" autocomplete="off" checked><span>Cesta a přístup</span></label>
|
||||
<label class="site-switch" title="Celá zahrada a graficky převzatá hranice parcely"><input type="checkbox"
|
||||
id="siteParcel" autocomplete="off"><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
|
||||
type="checkbox" id="siteParking" autocomplete="off"><span>Parkování</span></label>
|
||||
type="checkbox" id="siteParking" autocomplete="off" checked><span>Parkování</span></label>
|
||||
<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"
|
||||
stroke-width="1.5" aria-hidden="true">
|
||||
@@ -1687,7 +1687,7 @@
|
||||
</svg></div>
|
||||
<svg id="sunOverlay" aria-hidden="true"></svg>
|
||||
<div id="sunStamp"><span>SLUNCE NAD DOMEM</span><br><strong id="sceneTime">21. 6. 2026 · 10:00</strong></div>
|
||||
<section id="sunPanel" class="open" aria-label="Simulace slunce">
|
||||
<section id="sunPanel" aria-label="Simulace slunce">
|
||||
<div class="sun-panel-head">
|
||||
<div>
|
||||
<div class="sun-kicker">STUDIE OSVĚTLENÍ</div>
|
||||
@@ -1787,6 +1787,20 @@
|
||||
let target = [.8, 1.15, 0], theta = 2.25, phi = .30, distance = 35.0, aspect = 1, vp, lightVP, eye = [0, 0, 0];
|
||||
let animation = null, pointerMap = new Map(), pinch = null, wheelTimer = 0;
|
||||
const initialCamera = { theta: 2.25, phi: .30, distance: 35.0, target: [.8, 1.15, 0], ortho: false };
|
||||
// Opening composition follows the supplied reference; house geometry is unchanged.
|
||||
// Keep this separate from the existing Entry / Terrace / Top view presets.
|
||||
const startupCamera = { theta: .63, phi: .30, distance: 41.35, target: [-2.8, 2.3, 0], ortho: false };
|
||||
function applyStartupView() {
|
||||
document.querySelectorAll('[data-preset]').forEach(b => b.classList.remove('active'));
|
||||
$('#elevation').value = '';
|
||||
$('#panel').classList.remove('open');
|
||||
$('#infoButton').classList.remove('active');
|
||||
// Match the existing portrait resize rule, without fitting the entire long parcel.
|
||||
setCamera({ ...startupCamera, distance: startupCamera.distance * Math.max(1, 1 / aspect) }, 'Voln\u00fd pohled', true);
|
||||
autoRotate = true;
|
||||
$('#rotateButton').classList.add('active');
|
||||
dirty = true;
|
||||
}
|
||||
const originalColors = {};
|
||||
const vadd = (a, b) => a.map((x, i) => x + b[i]), vsub = (a, b) => a.map((x, i) => x - b[i]), scale = (a, s) => a.map(x => x * s), dot = (a, b) => a.reduce((r, x, i) => r + x * b[i], 0), norm = a => scale(a, 1 / Math.hypot(...a)), 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]];
|
||||
function mul(a, b) { const c = new Float32Array(16); for (let j = 0; j < 4; j++)for (let i = 0; i < 4; i++)for (let k = 0; k < 4; k++)c[j * 4 + i] += a[k * 4 + i] * b[j * 4 + k]; return c; }
|
||||
@@ -1887,7 +1901,7 @@ precision highp float;void main(){}`;
|
||||
$('#savePng').onclick = () => { render(); canvas.toBlob(b => saveBlob(b, 'dum_pohled.png'), 'image/png'); };
|
||||
window.addEventListener('resize', resize);
|
||||
window.addEventListener('keydown', e => { if (e.key === 'Escape') { $('#panel').classList.remove('open'); $('#infoButton').classList.remove('active'); } if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return; if (e.key === '1') setPreset('entry'); if (e.key === '2') setPreset('terrace'); if (e.key === '3') setPreset('top'); if (e.key === 'r') setPreset('terrace'); });
|
||||
async function start() { try { await new Promise(r => setTimeout(r, 40)); const s = atob(b64); originalBytes = new Uint8Array(s.length); for (let i = 0; i < s.length; i++)originalBytes[i] = s.charCodeAt(i); const dv = new DataView(originalBytes.buffer); if (dv.getUint32(0, true) !== 0x46546c67) throw Error('Invalid GLB'); let off = 12; while (off < originalBytes.length) { const len = dv.getUint32(off, true), type = dv.getUint32(off + 4, true); if (type === 0x4e4f534a) gltf = JSON.parse(new TextDecoder().decode(originalBytes.subarray(off + 8, off + 8 + len))); if (type === 0x004e4942) binary = originalBytes.subarray(off + 8, off + 8 + len); off += 8 + len; } initGL(); initSolar(); initSiteControls(); distance = Math.max(41, 38 / aspect); initialCamera.distance = distance; presetDefs.entry.distance = Math.max(39, 38 / aspect); presetDefs.top.distance = Math.max(46, 25 / aspect); if (aspect < 1) presetDefs.top.theta = Math.PI / 2; $('#loading').style.display = 'none'; requestAnimationFrame(animate); if ('ontouchstart' in window) $('.help').innerHTML = 'Jedn\u00edm prstem ot\u00e1\u010dej · Dv\u011bma prsty p\u0159ibli\u017e a posu\u0148'; } catch (e) { console.error(e); $('#loading').className = 'error'; $('#loading').innerHTML = '<div class="errorDetail">' + String(e.message).replace(/</g, '<') + '</div><button class="btn primary" id="fallbackSave">St\u00e1hnout GLB</button>'; $('#fallbackSave').onclick = () => saveBlob(new Blob([originalBytes], { type: 'model/gltf-binary' }), 'dum_3D_pozemek.glb'); } }
|
||||
async function start() { try { await new Promise(r => setTimeout(r, 40)); const s = atob(b64); originalBytes = new Uint8Array(s.length); for (let i = 0; i < s.length; i++)originalBytes[i] = s.charCodeAt(i); const dv = new DataView(originalBytes.buffer); if (dv.getUint32(0, true) !== 0x46546c67) throw Error('Invalid GLB'); let off = 12; while (off < originalBytes.length) { const len = dv.getUint32(off, true), type = dv.getUint32(off + 4, true); if (type === 0x4e4f534a) gltf = JSON.parse(new TextDecoder().decode(originalBytes.subarray(off + 8, off + 8 + len))); if (type === 0x004e4942) binary = originalBytes.subarray(off + 8, off + 8 + len); off += 8 + len; } initGL(); initSolar(); distance = Math.max(41, 38 / aspect); initialCamera.distance = distance; presetDefs.entry.distance = Math.max(39, 38 / aspect); presetDefs.top.distance = Math.max(46, 25 / aspect); if (aspect < 1) presetDefs.top.theta = Math.PI / 2; initSiteControls(); applyStartupView(); $('#loading').style.display = 'none'; requestAnimationFrame(animate); if ('ontouchstart' in window) $('.help').innerHTML = 'Jedn\u00edm prstem ot\u00e1\u010dej · Dv\u011bma prsty p\u0159ibli\u017e a posu\u0148'; } catch (e) { console.error(e); $('#loading').className = 'error'; $('#loading').innerHTML = '<div class="errorDetail">' + String(e.message).replace(/</g, '<') + '</div><button class="btn primary" id="fallbackSave">St\u00e1hnout GLB</button>'; $('#fallbackSave').onclick = () => saveBlob(new Blob([originalBytes], { type: 'model/gltf-binary' }), 'dum_3D_pozemek.glb'); } }
|
||||
// Solar geometry: Meeus / NOAA. UTC is the only time basis for the ephemeris.
|
||||
// Model axes: +X = south gable, +Z = entrance (west) side, +Y = up.
|
||||
const RAD = Math.PI / 180, DAY_MS = 86400000;
|
||||
@@ -2042,12 +2056,12 @@ precision highp float;void main(){}`;
|
||||
window.addEventListener('keydown', e => { if (['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON', 'SUMMARY'].includes(e.target.tagName)) return; if (e.code === 'Space') { e.preventDefault(); setSolarPlaying(!solar.playing); } if (e.key.toLowerCase() === 's') toggleSunPanel(); });
|
||||
document.addEventListener('visibilitychange', () => { if (document.hidden) setSolarPlaying(false); });
|
||||
const cp = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); cp.id = 'compassSun'; cp.setAttribute('r', '3'); cp.setAttribute('fill', '#c89439'); cp.setAttribute('stroke', '#fff7d7'); cp.setAttribute('stroke-width', '1'); $('#compassGroup').appendChild(cp);
|
||||
toggleSunPanel(true); rebuildSolarDay(false);
|
||||
toggleSunPanel(false); rebuildSolarDay(false);
|
||||
// Read-only diagnostics and small public control API for verification and reuse.
|
||||
window.SUN_SIM = { position: solarPosition, getState: () => ({ date: solar.date, time: clockText(solar.clock), utc: new Date(solar.clock).toISOString(), dayMinutes: solar.length, bearing: solar.bearing, lat: solar.lat, lon: solar.lon, azimuth: solar.pos.azimuth, elevation: solar.pos.elevation, light: solar.light.slice(), rise: clockText(solar.rise), set: clockText(solar.set), noon: clockText(solar.noon), shadows: solar.shadows, playing: solar.playing }), setDate: d => { solar.date = d; rebuildSolarDay(); }, setTime: t => { solar.minute = (timeForClock(t) - solar.start) / 60000; updateSolar(); }, setBearing: b => { solar.bearing = mod(b, 360); $('#houseBearing').value = solar.bearing; updateSolar(); }, pause: () => setSolarPlaying(false) };
|
||||
}
|
||||
|
||||
// Independent optional site layers. State is deliberately never persisted.
|
||||
// Independent site layers: initSiteControls enables all three on each fresh open.
|
||||
const site = { road: false, parcel: false, parking: false, savedCamera: null, lastFocus: null };
|
||||
const siteActive = () => site.road || site.parcel || site.parking;
|
||||
function meshVisible(m) {
|
||||
@@ -2135,7 +2149,8 @@ precision highp float;void main(){}`;
|
||||
}
|
||||
function initSiteControls() {
|
||||
for (const [id, key] of [['siteRoad', 'road'], ['siteParcel', 'parcel'], ['siteParking', 'parking']]) {
|
||||
const input = $('#' + id); input.checked = false; input.onchange = () => setSiteLayer(key, input.checked);
|
||||
const input = $('#' + id); input.onchange = () => setSiteLayer(key, input.checked);
|
||||
setSiteLayer(key, true, false);
|
||||
}
|
||||
$('#siteFit').onclick = () => focusSite();
|
||||
window.SITE_SIM = {
|
||||
|
||||
Reference in New Issue
Block a user