diff --git a/.DS_Store b/.DS_Store index ead1009..410b370 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/klabava/README_CZ.md b/klabava/README_CZ.md new file mode 100644 index 0000000..bd339d5 --- /dev/null +++ b/klabava/README_CZ.md @@ -0,0 +1,39 @@ +# Klabava · pracovní 3D web + +Tato složka obsahuje samostatný web s otočným modelem domu a pozemku, simulací slunce a odkazy na dodané PDF. Styl ovládání a časové chování simulace vychází z projektu Ločenice. Barvy a standardy domu zůstávají otevřené. + +## Spuštění na počítači + +Web používá ES moduly a Three.js z připnuté verze CDN jsDelivr. Spusťte místní server z této složky: + +```sh +python3 -m http.server 8000 +``` + +Potom otevřete . Prohlížeč potřebuje WebGL a připojení k CDN Three.js. Při nasazení na hosting nahrajte celý obsah složky `klabava/`; soubory `index.html`, `css/`, `js/`, `data/` a `references/` musí zůstat pohromadě. `index.php` je vstup pro PHP hosting. + +## Ovládání + +- Tažením myši model otáčíte, kolečkem přibližujete a pravým tlačítkem posouváte. +- Příjezd, Zahrada a Shora nastaví pohled; Terén, Parcela a Parkování přepínají vrstvy pozemku. +- Panel Slunce zachovává volbu data a času, sezónní dny, přehrávání dne, rychlost, východ/poledne/západ, azimut, výšku slunce, stíny, časové pásmo Europe/Prague a PNG. +- Zeměpisná poloha je nastavena přibližně na střed obce Klabava (49,7536° N, 13,5394° E). Natočení pozemku lze upravit v rozšířeném nastavení slunce. +- Stáhnout model GLB exportuje aktuálně viditelný 3D model domu a pozemku. Sluneční ovládání je součástí webu, nikoli souboru GLB. + +## Podklady a míra přesnosti + +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 výška hřebene +5,832 m. Situace uvádí parcelu 66/17 o výměře 1 037 m² a 206,54 m² včetně krytého vstupu a terasy. 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í roviny 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. Stromy, příjezd, parkovací plocha a materiály slouží k prostorové orientaci. 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é. + +## Soubory + +- `index.html` — webová aplikace. +- `index.php` — jednoduchý vstup pro PHP hosting. +- `js/klabava.js` — sestavení 3D modelu, kamera, ovládání a export GLB. +- `js/solar.js` — výpočet polohy slunce převzatý z podkladového webu. +- `css/app.css` a `css/web.css` — vizuální základ z původního webu; `css/klabava.css` obsahuje úpravy pro Klabavu. +- `data/model.json` — souhrn údajů z výkresů. +- `references/` — výškopis, situace, půdorys, pohledy a řez. diff --git a/klabava/THIRD_PARTY.txt b/klabava/THIRD_PARTY.txt new file mode 100644 index 0000000..e009fa4 --- /dev/null +++ b/klabava/THIRD_PARTY.txt @@ -0,0 +1,6 @@ +Three.js r180 / 0.180.0 is loaded from jsDelivr. It is not bundled here. +Source: https://github.com/mrdoob/three.js/tree/r180 +License: MIT, Copyright (c) 2010-2025 three.js authors. + +The architectural drawings and survey in references/ were supplied for this project. +The 3D geometry is a visual interpretation of those PDFs and does not claim survey or construction accuracy. diff --git a/klabava/css/app.css b/klabava/css/app.css new file mode 100644 index 0000000..27268f4 --- /dev/null +++ b/klabava/css/app.css @@ -0,0 +1,1728 @@ + + :root { + --ink: #26342c; + --muted: #737b72; + --paper: #fafbf6; + --line: #d8ded4; + --accent: #365944 + } + + * { + box-sizing: border-box + } + + html, + body { + margin: 0; + height: 100%; + overflow: hidden; + font-family: Arial, Helvetica, sans-serif; + color: var(--ink); + background: #e7ebe3 + } + + button, + input, + select { + font: inherit + } + + button { + cursor: pointer; + color: inherit + } + + button:focus-visible, + select:focus-visible, + input:focus-visible { + outline: 3px solid #91aa92; + outline-offset: 3px + } + + #view { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + display: block; + touch-action: none + } + + header { + position: fixed; + left: 32px; + top: 29px; + pointer-events: none + } + + .eyebrow { + font-size: 10px; + font-weight: 700; + letter-spacing: 2.6px; + color: #647669; + display: flex; + align-items: center; + gap: 9px + } + + .dot { + height: 6px; + width: 6px; + border-radius: 50%; + background: #536e57 + } + + h1 { + font-size: 31px; + font-weight: 500; + letter-spacing: -1.1px; + line-height: 1.13; + margin: 10px 0 7px + } + + .sub { + font-size: 12px; + color: var(--muted); + line-height: 1.5 + } + + .metrics { + display: flex; + gap: 24px; + margin-top: 20px + } + + .metric strong { + font-size: 18px; + font-weight: 500; + letter-spacing: -.4px + } + + .metric small { + display: block; + margin-top: 4px; + font-size: 9px; + text-transform: uppercase; + letter-spacing: 1.6px; + color: var(--muted) + } + + .topright { + position: fixed; + right: 27px; + top: 28px; + display: flex; + gap: 8px + } + + .btn { + border: 1px solid #d4dcd1; + background: #f9fbf5ed; + border-radius: 9px; + padding: 10px 14px; + font-size: 12px; + line-height: 1; + box-shadow: 0 2px 8px #243c2210 + } + + .btn:hover { + background: #fff + } + + .btn.primary { + background: var(--accent); + border-color: var(--accent); + color: white + } + + .btn.active { + background: #dfebdf; + border-color: #99af9a + } + + .icon { + width: 37px; + height: 37px; + padding: 0; + font-size: 16px + } + + .bottom { + position: fixed; + left: 50%; + bottom: 32px; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 11px; + max-width: calc(100% - 32px) + } + + .toolbar { + display: flex; + align-items: center; + gap: 4px; + padding: 7px; + border: 1px solid #ffffffb0; + background: #f9fbf6ed; + border-radius: 15px; + box-shadow: 0 10px 34px #24381e16; + white-space: nowrap + } + + .tool { + border: 0; + background: transparent; + padding: 10px 13px; + font-size: 12px; + border-radius: 8px + } + + .tool.active { + background: var(--accent); + color: white + } + + .tool:hover { + background: #e8eee5 + } + + .tool.active:hover { + background: #365944 + } + + .divider { + width: 1px; + height: 21px; + background: #dce2d8; + margin: 0 4px + } + + .help { + font-size: 10px; + letter-spacing: .25px; + color: #687567; + text-align: center; + background: #e7ebe3c0; + border-radius: 6px; + padding: 4px 9px + } + + .compass { + position: fixed; + right: 29px; + bottom: 38px; + width: 84px; + height: 84px + } + + .compass svg { + width: 84px; + height: 84px; + overflow: visible + } + + .compass text { + font: 9px Arial; + fill: #596959 + } + + .compass line { + stroke: #b4c0ae; + stroke-width: 1 + } + + #northArrow { + fill: #365944 + } + + #direction { + position: fixed; + left: 32px; + bottom: 37px; + font-size: 11px; + line-height: 1.6; + color: #576d5c + } + + .tag { + font-size: 8px; + letter-spacing: 1.8px; + color: #7d8979; + display: block + } + + .panel { + position: fixed; + right: 27px; + top: 77px; + max-height: calc(100% - 184px); + width: 337px; + overflow: auto; + background: #fafcf6f5; + border: 1px solid #fff; + border-radius: 15px; + box-shadow: 0 12px 45px #29392525; + padding: 23px; + display: none + } + + .panel.open { + display: block + } + + .panel h2 { + font-size: 19px; + letter-spacing: -.3px; + font-weight: 500; + margin: 0 0 10px + } + + .panel h3 { + font-size: 9px; + text-transform: uppercase; + letter-spacing: 1.8px; + color: #70816e; + margin: 23px 0 10px + } + + .panel p { + font-size: 12px; + line-height: 1.65; + color: #667261; + margin: 7px 0 + } + + .panel .row { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + border-bottom: 1px solid #e0e6db; + padding: 9px 0 + } + + .swatches { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 10px + } + + .swatches label { + display: flex; + flex-direction: column; + gap: 7px; + font-size: 10px; + color: #667261 + } + + input[type=color] { + width: 100%; + height: 30px; + border: 0; + background: none; + padding: 0; + cursor: pointer + } + + select { + width: 100%; + padding: 10px; + border: 1px solid #d6e0d0; + border-radius: 6px; + background: #f5f8f0; + color: #334931; + font-size: 12px + } + + .note { + border-left: 2px solid #a5b99d; + padding-left: 11px + } + + .panel .buttons { + display: flex; + gap: 8px; + margin-top: 15px; + flex-wrap: wrap + } + + .status { + display: flex; + align-items: center; + gap: 7px; + font-size: 10px; + color: #6c7f68; + margin-top: 16px + } + + #loading { + position: fixed; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: #e7ebe3; + z-index: 10; + gap: 20px + } + + .spinner { + width: 34px; + height: 34px; + border-radius: 50%; + border: 2px solid #bccabb; + border-top-color: #365944; + animation: spin 1s linear infinite + } + + @keyframes spin { + to { + transform: rotate(360deg) + } + } + + #loading .label { + font-size: 12px; + color: #597054 + } + + #loading.error { + padding: 30px; + text-align: center + } + + .errorDetail { + font-size: 12px; + max-width: 480px; + line-height: 1.7; + color: #51624d + } + + #measure { + position: fixed; + inset: 0; + pointer-events: none; + width: 100%; + height: 100%; + display: none + } + + #measure line { + stroke: #3f5b42; + stroke-width: 1 + } + + #measure circle { + fill: #456749 + } + + #measure text { + fill: #3a553d; + font: 12px Arial; + paint-order: stroke; + stroke: #eff3e9; + stroke-width: 5px; + stroke-linejoin: round + } + + #toast { + position: fixed; + left: 50%; + top: 28px; + transform: translateX(-50%); + font-size: 12px; + background: #2f4739; + color: #fff; + padding: 11px 17px; + border-radius: 9px; + opacity: 0; + transition: opacity .25s; + pointer-events: none + } + + .full { + display: none + } + + @media(max-width:850px) { + header { + left: 20px; + top: 22px + } + + .topright { + right: 18px; + top: 22px + } + + h1 { + font-size: 26px + } + + .metrics { + gap: 19px; + margin-top: 15px + } + + .metric strong { + font-size: 16px + } + + .bottom { + bottom: 21px + } + + .tool { + padding: 10px + } + + .compass { + display: none + } + + #direction { + bottom: 114px; + left: 20px + } + + .panel { + right: 18px; + top: 69px; + width: 320px + } + + .help { + font-size: 9px + } + + .optional { + display: none + } + } + + @media(max-width:520px) { + header { + left: 16px; + top: 19px + } + + .eyebrow { + font-size: 8px; + letter-spacing: 1.7px + } + + h1 { + font-size: 24px + } + + .sub { + font-size: 10px; + max-width: 210px + } + + .metrics { + gap: 16px; + margin-top: 13px + } + + .metric strong { + font-size: 15px + } + + .metric small { + font-size: 8px; + letter-spacing: 1px + } + + .topright { + right: 13px; + top: 18px + } + + .topright .btn.primary { + font-size: 10px; + padding: 11px 10px + } + + .topright .icon { + width: 33px + } + + .toolbar { + gap: 0; + padding: 5px; + border-radius: 12px + } + + .tool { + padding: 10px 9px; + font-size: 10px + } + + .divider { + margin: 0 1px + } + + .bottom { + bottom: 17px; + gap: 8px + } + + .help { + max-width: 320px; + font-size: 9px; + line-height: 1.5 + } + + #direction { + bottom: 100px; + left: 17px; + font-size: 10px + } + + .panel { + top: 62px; + right: 12px; + left: 12px; + width: auto; + max-height: calc(100% - 159px); + padding: 20px + } + + .btn { + font-size: 11px + } + + .measureWord { + display: none + } + + .topright .downloadText { + display: none + } + } + + :root { + --solar: #b8812d; + --solar-soft: #f4e9cf; + --sun-side: 376px; + --sun-sheet: 340px + } + + body.sun-open #view, + body.sun-open #measure, + body.sun-open #sunOverlay { + width: calc(100% - var(--sun-side)) + } + + #sunOverlay { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; + overflow: hidden + } + + body.sun-open .bottom { + left: calc((100% - var(--sun-side))/2) + } + + body.sun-open .compass { + right: calc(var(--sun-side) + 18px) + } + + header h1 { + font-size: 30px; + letter-spacing: -1px + } + + header .metrics { + margin-top: 18px; + gap: 22px + } + + .solar-mark { + color: var(--solar); + font-size: 16px; + line-height: 1 + } + + #sunToggle { + display: flex; + gap: 8px; + align-items: center; + font-weight: 600 + } + + #sunToggle.active { + background: #f3e8ce; + border-color: #d8bd85 + } + + #sunPanel { + display: none; + position: fixed; + right: 16px; + top: 79px; + bottom: 20px; + width: 348px; + padding: 22px; + background: #fafbf7f7; + border: 1px solid #fff; + border-radius: 18px; + box-shadow: 0 12px 45px #29392517; + overflow: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + scrollbar-color: #c1cabe transparent; + z-index: 3 + } + + #sunPanel.open { + display: block + } + + .sun-panel-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 15px; + margin-bottom: 17px + } + + .sun-kicker { + font-size: 9px; + font-weight: 700; + letter-spacing: 2px; + color: #8d753d; + margin-bottom: 6px + } + + #sunPanel h2 { + font-size: 23px; + letter-spacing: -.65px; + font-weight: 500; + margin: 0 + } + + #sunPanel .close-panel { + border: 0; + background: #eef1e9; + border-radius: 50%; + width: 29px; + height: 29px; + font-size: 20px; + line-height: 20px; + color: #6a7466 + } + + .sun-date-row { + display: grid; + grid-template-columns: 1fr 105px; + gap: 12px; + align-items: end + } + + .sun-field { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; + font-size: 10px; + color: #6f7969 + } + + .sun-field input:not([type=range]), + .sun-field select { + width: 100%; + min-width: 0; + padding: 9px 9px; + height: 39px; + border: 1px solid #d9e0d2; + border-radius: 8px; + background: #fff; + color: #354232; + font-size: 12px + } + + .sun-field #sunClock { + font-size: 20px; + letter-spacing: -.6px; + padding: 6px; + width: 105px + } + + .sun-seasons { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 5px; + margin-top: 11px + } + + .sun-seasons button { + border: 1px solid #e0e4d9; + border-radius: 7px; + background: #f4f6ef; + padding: 8px 2px; + font-size: 10px; + color: #53624d + } + + .sun-seasons button.active { + background: var(--solar-soft); + border-color: #dac08c; + color: #765923 + } + + .sun-status-line { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 10px; + color: #7b826f; + margin: 16px 0 5px; + gap: 8px + } + + #dayState { + display: flex; + align-items: center; + gap: 6px; + color: #856527 + } + + #dayState:before { + content: ''; + width: 5px; + height: 5px; + border-radius: 50%; + background: #c3933c + } + + .sun-graph { + display: block; + width: 100%; + height: 85px; + cursor: crosshair; + touch-action: none + } + + .sun-scrub { + width: 100%; + accent-color: var(--solar); + margin: 8px 0 3px; + cursor: ew-resize; + height: 22px + } + + .sun-ticks { + display: flex; + justify-content: space-between; + font-size: 9px; + color: #8b9283 + } + + .sun-play-row { + display: grid; + grid-template-columns: 1fr 90px 46px; + gap: 7px; + margin-top: 14px + } + + .sun-play-row button, + .sun-play-row select { + height: 36px; + padding: 7px 9px; + font-size: 11px; + border-radius: 8px; + border: 1px solid #d6ddce; + background: #f2f5ec; + white-space: nowrap + } + + .sun-play-row #sunPlay { + background: #3c5d48; + color: white; + border-color: #3c5d48; + font-weight: 600 + } + + .sun-play-row #sunSpeed { + font-size: 10px; + padding: 7px 4px + } + + .sun-readouts { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; + margin-top: 20px; + padding-top: 17px; + border-top: 1px solid #e1e5d9 + } + + .sun-stat { + font-size: 10px; + color: #7b8372 + } + + .sun-stat strong { + display: block; + font-weight: 500; + font-size: 24px; + letter-spacing: -.7px; + color: #324630; + margin: 4px 0 3px; + font-variant-numeric: tabular-nums + } + + .sun-stat small { + font-size: 10px; + color: #929883 + } + + .sun-events { + display: flex; + justify-content: space-between; + gap: 12px; + font-size: 10px; + color: #718066; + margin: 16px 0 2px; + padding: 12px 0; + border-top: 1px solid #e5e8df; + border-bottom: 1px solid #e5e8df + } + + .sun-events b { + font-weight: 600; + color: #3e563b; + margin-left: 4px + } + + .sun-options { + display: flex; + gap: 17px; + flex-wrap: wrap; + margin: 14px 0 12px + } + + .sun-options label { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: #52684d; + cursor: pointer + } + + .sun-options input { + accent-color: #597447; + margin: 0; + width: 14px; + height: 14px + } + + .sun-hint { + font-size: 10px; + line-height: 1.6; + color: #84907a; + margin: 8px 0 13px + } + + .sun-advanced { + margin-top: 15px; + border-top: 1px solid #e1e5d9; + padding-top: 13px + } + + .sun-advanced summary { + font-size: 11px; + cursor: pointer; + color: #4d6645; + padding: 3px 0 + } + + .sun-advanced .fields { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-top: 14px + } + + .sun-advanced .full-row { + grid-column: 1 / -1 + } + + .sun-advanced p { + font-size: 10px; + line-height: 1.65; + color: #7f8975; + margin: 10px 0 + } + + .sun-advanced a { + color: #536a4f + } + + .sun-advanced .mini-btn { + font-size: 10px; + border: 1px solid #d2dbcb; + background: #f5f8ef; + border-radius: 7px; + padding: 8px 10px + } + + .sun-export { + width: 100%; + padding: 10px; + border: 1px solid #ced8c6; + border-radius: 8px; + background: #eef3e8; + font-size: 11px; + color: #47603e; + margin-top: 13px + } + + .sun-footnote { + font-size: 9px; + line-height: 1.6; + color: #969d8b; + margin: 13px 0 0 + } + + #sunStamp { + position: fixed; + left: 32px; + top: 180px; + pointer-events: none; + color: #6c775f; + font-size: 11px; + line-height: 1.55 + } + + #sunStamp strong { + font-weight: 500; + color: #a07836; + font-variant-numeric: tabular-nums + } + + #solarMessage { + color: #977026; + font-size: 10px; + line-height: 1.5; + min-height: 0 + } + + #solarMessage:empty { + display: none + } + + #panel { + z-index: 4 + } + + #sunOverlay text { + font: 10px Arial; + paint-order: stroke; + stroke: #f3f4e9; + stroke-width: 3px; + stroke-linejoin: round; + fill: #9a702d + } + + #sunOverlay path, + #sunOverlay line { + vector-effect: non-scaling-stroke + } + + body.night header, + body.night header h1 { + color: #d3dec9 + } + + body.night header .sub, + body.night header .metric small, + body.night header .eyebrow, + body.night #direction, + body.night #sunStamp { + color: #aab9a5 + } + + body.night #sunStamp strong { + color: #d6ba76 + } + + #toast { + z-index: 12; + top: 20px + } + + @media(min-width:851px) { + .topright { + right: 20px; + top: 25px + } + + .help { + font-size: 9px + } + + .sun-open #direction { + bottom: 26px; + font-size: 10px + } + + .sun-open .bottom { + bottom: 28px + } + + .sun-open .compass { + bottom: 28px + } + + .sun-open .toolbar .tool { + font-size: 11px; + padding: 10px 10px + } + } + + @media(max-width:1100px) and (min-width:851px) { + header { + left: 22px; + top: 27px + } + + header h1 { + font-size: 25px + } + + .metrics { + display: none !important + } + + #sunStamp { + left: 22px; + top: 115px + } + + .sun-open .compass { + display: none + } + + .sun-open #direction { + display: none + } + + .sun-open .toolbar .tool { + padding: 10px 7px; + font-size: 10px + } + } + + @media(max-width:850px) { + :root { + --sun-sheet: min(42vh, 350px) + } + + body.sun-open #view, + body.sun-open #measure, + body.sun-open #sunOverlay { + width: 100%; + height: calc(100% - var(--sun-sheet)) + } + + body.sun-open .bottom { + left: 50%; + bottom: calc(var(--sun-sheet) + 8px); + gap: 0 + } + + body.sun-open .help { + display: none + } + + body.sun-open #direction { + display: none + } + + body.sun-open .compass { + display: none + } + + #sunPanel { + top: auto; + bottom: 0; + right: 0; + left: 0; + width: 100%; + height: var(--sun-sheet); + padding: 16px 21px 25px; + border-radius: 20px 20px 0 0; + box-shadow: 0 -8px 30px #26392514 + } + + .sun-panel-head { + display: none + } + + .sun-date-row { + grid-template-columns: 1fr 120px; + gap: 16px + } + + .sun-date-row .sun-field { + font-size: 9px + } + + .sun-field #sunClock { + width: 100%; + font-size: 23px; + height: 40px + } + + .sun-field input:not([type=range]) { + height: 40px + } + + .sun-seasons { + margin-top: 9px + } + + .sun-seasons button { + padding: 7px 0; + font-size: 10px + } + + .sun-status-line { + margin: 12px 0 0 + } + + .sun-graph { + height: 62px + } + + .sun-scrub { + margin: 4px 0 0 + } + + .sun-play-row { + margin-top: 10px; + grid-template-columns: 1fr 94px 51px + } + + .sun-readouts { + margin-top: 15px; + padding-top: 12px + } + + .sun-stat strong { + font-size: 23px + } + + .sun-events { + margin-top: 13px + } + + #sunStamp { + left: 18px; + top: 105px; + font-size: 10px + } + + header { + left: 18px; + top: 19px + } + + header h1 { + font-size: 25px; + margin-top: 9px + } + + .metrics { + display: none !important + } + + header .sub { + max-width: 230px; + font-size: 10px + } + + .topright { + right: 14px; + top: 19px; + gap: 6px + } + + #saveGlb { + display: none + } + + #sunToggle { + font-size: 10px; + padding: 10px 9px; + gap: 4px + } + + .topright .icon { + width: 33px; + height: 33px + } + + .sun-open .tool { + font-size: 10px; + padding: 9px 8px + } + + .sun-open .bottom { + max-width: calc(100% - 16px) + } + + #panel { + top: 66px; + bottom: 15px; + max-height: none + } + } + + @media(max-width:420px) { + header h1 { + font-size: 23px + } + + header .eyebrow { + font-size: 8px; + letter-spacing: 1.8px + } + + #sunToggle .toggle-word { + display: none + } + + #sunToggle { + width: 34px; + height: 34px; + justify-content: center; + padding: 0 + } + + .sun-open .tool { + padding: 9px 7px; + font-size: 10px + } + } + + @media(max-height:500px) and (max-width:850px) { + :root { + --sun-sheet: 170px + } + + header .sub, + #sunStamp { + display: none + } + + header h1 { + font-size: 21px + } + + .sun-graph { + display: none + } + + #sunPanel { + padding: 12px 20px + } + + .sun-readouts { + display: none + } + } + + /* Optional surroundings: independent switches, all on when the page opens. */ + .site-controls { + display: flex; + align-items: center; + gap: 5px; + padding: 6px; + border: 1px solid #fff; + border-radius: 13px; + background: #f9fbf6f2; + box-shadow: 0 6px 24px #24381e13; + max-width: 100%; + } + + .site-switch { + display: flex; + align-items: center; + gap: 7px; + padding: 9px 10px; + border: 1px solid transparent; + border-radius: 8px; + font-size: 11px; + color: #596b55; + cursor: pointer; + white-space: nowrap; + user-select: none; + } + + .site-switch:hover { + background: #eef2e8; + } + + .site-switch.selected { + background: #e1ecdc; + border-color: #b9cbb1; + color: #304d35; + } + + .site-switch input { + appearance: none; + width: 13px; + height: 13px; + margin: 0; + border: 1px solid #a7b29f; + border-radius: 4px; + background: #fff; + flex-shrink: 0; + cursor: pointer; + display: grid; + place-items: center; + } + + .site-switch input:checked { + background: #426448; + border-color: #426448; + } + + .site-switch input:checked:after { + content: '\2713'; + color: #fff; + font-size: 10px; + line-height: 1; + } + + .site-fit { + display: grid; + place-items: center; + width: 32px; + height: 32px; + flex-shrink: 0; + border: 0; + border-left: 1px solid #dce2d5; + border-radius: 5px; + background: transparent; + color: #59734e; + } + + .site-fit:hover { + background: #e7eee1; + } + + .site-fit svg { + width: 17px; + height: 17px; + } + + #siteMeta { + font-size: 9px; + letter-spacing: .1px; + color: #6e8066; + line-height: 1.2; + background: #f5f8eef0; + padding: 4px 8px; + border-radius: 5px; + pointer-events: none; + } + + #siteMeta[hidden] { + display: none; + } + + .bottom { + gap: 7px; + z-index: 2; + } + + .site-note { + margin-top: 14px; + border-top: 1px solid #dce4d5; + padding-top: 10px; + } + + .site-note summary { + cursor: pointer; + font-size: 12px; + color: #37563b; + padding: 6px 0; + } + + .site-note p { + font-size: 11px; + } + + @media(max-width:850px) { + .bottom { + max-width: calc(100% - 20px); + } + + #direction { + bottom: 161px; + } + + .site-controls { + gap: 2px; + padding: 5px; + } + + .site-switch { + font-size: 10px; + padding: 8px 8px; + gap: 5px; + } + + body.sun-open .bottom { + gap: 5px; + } + + #siteMeta { + font-size: 8px; + } + } + + @media(max-width:520px) { + .bottom { + gap: 5px; + bottom: 12px; + } + + .site-switch { + font-size: 9px; + padding: 8px 6px; + gap: 5px; + } + + .site-fit { + width: 27px; + height: 28px; + } + + .site-controls { + gap: 0; + padding: 4px; + } + + .site-switch input { + width: 12px; + height: 12px; + } + + .bottom .help { + display: none; + } + + #direction { + bottom: 111px; + } + + body.sun-open #siteMeta { + display: none; + } + } + + @media(max-width:360px) { + .site-switch { + font-size: 8.5px; + padding: 8px 4px; + gap: 4px; + } + + .site-fit { + width: 24px; + } + } + /* Fixed product palette, scoped to facade cladding only. */ + .swatches { grid-template-columns: 1fr 1fr; } + .hardie-card { margin: 15px 0 17px; padding: 14px; border: 1px solid #d8ded4; border-radius: 11px; background: #ffffffb8; } + .hardie-kicker { font-size: 9px; letter-spacing: 1.4px; text-transform: uppercase; color: #677b61; margin-bottom: 8px; } + .hardie-card h4 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.2px; } + .hardie-card .hardie-spec { font-size: 10px; line-height: 1.6; margin: 5px 0 10px; } + .hardie-preview { height: 48px; border-radius: 4px; background: #776e65; background-size: 400% 300%; background-position: 48% 48%; border: 1px solid #0002; box-shadow: inset 0 -3px 0 #0002, inset 0 1px 0 #fff2; } + .hardie-selection { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; padding: 9px 0 10px; color: #304835; } + .hardie-selection strong { font-size: 12px; font-weight: 600; } + .hardie-selection small { font-size: 9px; color: #778270; white-space: nowrap; } + .hardie-palette { border: 0; padding: 0; margin: 0; min-width: 0; } + .hardie-palette legend { font-size: 9px; color: #6d7b64; margin: 0 0 8px; padding: 0; } + .hardie-options { display: grid; grid-template-columns: 1fr; gap: 4px; } + .hardie-option { display: flex; align-items: center; gap: 8px; min-height: 33px; padding: 5px 8px; border: 1px solid #e1e6dd; border-radius: 6px; cursor: pointer; background: #fbfcf9; position: relative; } + .hardie-option:hover { border-color: #aabda1; background: #f4f7ef; } + .hardie-option.selected { background: #eaf1e5; border-color: #87a57c; } + .hardie-option:focus-within { outline: 2px solid #91aa92; outline-offset: 2px; } + .hardie-option input { position: absolute; width: 1px; height: 1px; opacity: 0; } + .hardie-chip { width: 22px; height: 21px; border-radius: 3px; flex-shrink: 0; border: 1px solid #0002; background: var(--chip); } + .hardie-option-name { font-size: 11px; flex: 1; line-height: 1.3; } + .hardie-option-code { font-size: 9px; color: #7d8577; font-variant-numeric: tabular-nums; } + .hardie-check { width: 11px; font-size: 12px; color: #365944; opacity: 0; } + .hardie-option.selected .hardie-check { opacity: 1; } + .hardie-card .hardie-scope { font-size: 10px; color: #6d7965; margin: 10px 0 7px; line-height: 1.5; } + .hardie-link { color: #365944; font-size: 11px; text-underline-offset: 3px; } + .hardie-detail-buttons { display: flex; gap: 6px; margin-top: 10px; } + .hardie-detail-buttons button { background: #f3f6ef; border: 1px solid #d6dfcf; border-radius: 6px; padding: 7px 8px; font-size: 10px; flex: 1; } + .hardie-detail-buttons button:hover { background: #e7eee0; } + .hardie-note { margin-top: 11px; border-top: 1px solid #e0e6da; padding-top: 8px; } + .hardie-note summary { font-size: 10px; cursor: pointer; color: #65765c; } + .hardie-card .hardie-note p { font-size: 10px; line-height: 1.55; } + .hardie-reference { width: 100%; height: auto; display: block; border-radius: 5px; margin: 8px 0; } + @media(max-width:360px) { .hardie-card { padding: 11px; } .hardie-option-code { font-size: 8px; } } + + .hardie-structure-button { width: 100%; margin-top: 6px; padding: 7px; border: 1px solid #d6dfcf; border-radius: 6px; background: #f3f6ef; font-size: 10px; } + .hardie-preview-caption { font-size: 9px; color: #7a8372; margin-top: 4px; } + + +/* Optional interior. No source exterior selector is changed outside this state. */ +#interiorLaunch{position:fixed;left:32px;top:225px;z-index:3;display:flex;align-items:center;gap:11px;padding:12px 17px;border:1px solid #b8c7b0;background:#fafcf6f5;border-radius:12px;box-shadow:0 5px 25px #263b2520;color:#365944;font-size:12px;font-weight:600} +#interiorLaunch svg{width:20px;height:20px}#interiorLaunch small{display:block;margin-top:4px;font-size:9px;font-weight:400;color:#7a8574;text-align:left} +#interiorLaunch:disabled{opacity:.6;cursor:wait} +.interior-heading,#interiorPanel,#interiorTools,#interiorLabels,#walkPad{display:none} +body.interior-active header,body.interior-active #interiorLaunch,body.interior-active .bottom,body.interior-active #direction,body.interior-active .compass,body.interior-active #measure,body.interior-active #sunStamp{display:none!important} +body.interior-active .interior-heading{display:flex;position:fixed;top:23px;left:25px;z-index:3;align-items:center;gap:17px;pointer-events:none} +.interior-heading button{pointer-events:auto;padding:11px 13px}.interior-heading .eyebrow{font-size:8px;letter-spacing:1.8px}.interior-heading h2{margin:5px 0 0;font-size:23px;font-weight:500;letter-spacing:-.65px;color:#2c4434} +body.interior-active:not(.interior-panel-closed):not(.sun-open) #view,body.interior-active:not(.interior-panel-closed):not(.sun-open) #sunOverlay{width:calc(100% - 340px)} +body.interior-active #interiorPanel{display:block;position:fixed;z-index:3;right:16px;top:78px;bottom:20px;width:308px;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin;padding:20px;background:#fafcf7f7;border:1px solid #fff;box-shadow:0 10px 35px #263b2520;border-radius:16px} +body.interior-panel-closed #interiorPanel,body.interior-active.sun-open #interiorPanel{display:none} +.interior-panel-title{display:flex;justify-content:space-between;align-items:center;gap:6px;margin:0 0 12px} +.interior-panel-title h3{margin:0;font-size:16px;font-weight:500;letter-spacing:-.3px}.interior-panel-title span{font-size:9px;color:#75816f} +.int-kicker{font-size:9px;letter-spacing:1.5px;text-transform:uppercase;color:#7c8675;margin:20px 0 10px} +#interiorMap{width:100%;height:auto;display:block;border-radius:10px;background:#eff1e9;border:1px solid #e0e5d8} +#interiorMap .map-room{cursor:pointer;fill:#dcd4c1;stroke:#f5f4eb;stroke-width:2;transition:fill .15s} +#interiorMap .map-room:hover,#interiorMap .map-room.active{fill:#aebda2} +#interiorMap text{font:9px Arial;fill:#465743;pointer-events:none} +#interiorMap .map-wall{fill:#8e9986;pointer-events:none} +.int-map-hint{font-size:9px!important;line-height:1.5!important;margin:7px 0 0!important;color:#85917d!important} +.int-views{display:grid;grid-template-columns:1fr 1fr;gap:6px} +.int-views button{padding:11px 8px;text-align:left;border:1px solid #d9e0d2;border-radius:8px;background:#f2f6ed;color:#38523a;font-size:10px;line-height:1.35} +.int-views button.active{background:#dfe9d8;border-color:#a6ba99}.int-views button:hover{background:#e5ecde} +#interiorRoom{margin-top:2px;font-size:11px;background:#fff} +.int-options{display:grid;grid-template-columns:1fr;gap:10px;padding-top:14px;margin-top:16px;border-top:1px solid #e0e6d9} +.int-options label{font-size:11px;display:flex;align-items:center;gap:8px;color:#51654a}.int-options input{accent-color:#426448} +#interiorPanel p{font-size:10px;line-height:1.6;color:#77836e;margin:10px 0} +.int-source{font-size:10px;border-top:1px solid #dce3d4;padding-top:12px;margin-top:16px}.int-source summary{cursor:pointer;line-height:1.5;color:#536b49} +.int-source-buttons{display:flex;gap:5px;flex-wrap:wrap;margin:10px 0} +.int-source-buttons button{border:1px solid #d4ddca;border-radius:7px;font-size:10px;background:#f5f7ef;padding:8px;color:#486140} +.int-export{width:100%;margin-top:12px;border:1px solid #d0dbc6;border-radius:8px;background:#eef3e7;padding:10px;font-size:10px;color:#49613f} +body.interior-active #interiorTools{display:flex;position:fixed;z-index:4;left:calc((100% - 340px)/2);bottom:22px;transform:translateX(-50%);flex-direction:column;align-items:center;gap:8px;max-width:calc(100% - 24px)} +body.interior-panel-closed #interiorTools{left:50%}body.interior-active.sun-open #interiorTools{left:calc((100% - var(--sun-side))/2)} +.int-mode-bar{display:flex;align-items:center;gap:4px;white-space:nowrap;padding:6px;background:#fafcf6f5;border:1px solid #fff;border-radius:12px;box-shadow:0 5px 26px #263b2525} +.int-mode-bar button{border:0;background:transparent;border-radius:7px;padding:10px 11px;font-size:11px;color:#52684a}.int-mode-bar button.active{background:#365944;color:#fff}.int-mode-bar button:hover:not(.active){background:#e8eee2} +.int-mode-bar .int-nav-button{background:#e9efe3;font-size:15px;line-height:14px;padding:9px 10px} +#interiorHint{font-size:9px;line-height:1.45;color:#71806a;text-align:center;background:#f0f3e8de;border-radius:6px;padding:4px 10px;max-width:460px} +body.interior-active #interiorLabels{display:block;position:fixed;inset:0;pointer-events:none;z-index:2} +.int-room-tag{position:absolute;transform:translate(-50%,-50%);border:1px solid #ffffffdc;border-radius:7px;background:#fafcf4ea;padding:5px 7px;box-shadow:0 2px 10px #253a2220;pointer-events:auto;white-space:nowrap;line-height:1.2;font-size:10px;color:#334c35} +.int-room-tag b{font-weight:600}.int-room-tag small{display:block;font-size:8px;color:#7c8874;margin-top:3px}.int-room-tag:hover{border-color:#739069;background:#edf4e5} +body.interior-walk #sunOverlay{display:none} +body.interior-walk #walkPad{display:grid;position:fixed;z-index:4;left:24px;bottom:94px;grid-template-columns:38px 38px 38px;grid-template-rows:34px 34px;gap:4px} +#walkPad button{touch-action:none;user-select:none;-webkit-user-select:none;border:1px solid #d5dfcc;background:#f8fbf1ee;border-radius:8px;color:#425e3c;font-size:17px;box-shadow:0 3px 15px #314a2520}#walkPad button:active,#walkPad button.held{background:#bfd0b2} +#walkPad [data-walk=forward]{grid-column:2;grid-row:1}#walkPad [data-walk=left]{grid-column:1;grid-row:2}#walkPad [data-walk=back]{grid-column:2;grid-row:2}#walkPad [data-walk=right]{grid-column:3;grid-row:2} +#interiorReferenceDialog{width:min(1120px,96vw);max-height:94vh;border:1px solid #e2e8da;border-radius:14px;background:#fafcf6;padding:16px;color:#36503c;box-shadow:0 25px 90px #14241666} +#interiorReferenceDialog::backdrop{background:#1e2b22b3;backdrop-filter:blur(3px)} +.int-ref-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}.int-ref-head h3{font-size:14px;font-weight:500;margin:0}.int-ref-head button{border:0;background:#e9eee3;border-radius:7px;padding:8px 12px} +#interiorReferenceImage{display:block;max-width:100%;max-height:76vh;object-fit:contain;margin:auto}.int-ref-caption{font-size:11px;line-height:1.5;margin:10px 0 0;color:#7b8574} +@media(max-width:1100px) and (min-width:851px){.interior-heading h2{font-size:19px}.interior-heading .eyebrow{display:none}.interior-heading{max-width:calc(100% - 370px)}} +@media(max-width:850px){ + #interiorLaunch{top:147px;left:18px;padding:10px 13px;font-size:11px}#interiorLaunch small{font-size:8px} + body.interior-active .interior-heading{left:16px;top:18px;gap:10px;max-width:calc(100% - 110px)}.interior-heading h2{font-size:17px;margin-top:3px}.interior-heading .eyebrow{font-size:7px;letter-spacing:1px}.interior-heading button{padding:9px 10px;font-size:10px} + body.interior-active:not(.interior-panel-closed):not(.sun-open) #view,body.interior-active:not(.interior-panel-closed):not(.sun-open) #sunOverlay{width:100%;height:calc(100% - 242px)} + body.interior-active #interiorPanel{top:auto;left:0;right:0;bottom:0;width:100%;height:242px;border-radius:18px 18px 0 0;padding:15px 18px 24px} + #interiorMap{width:150px;float:left;margin-right:14px;margin-bottom:9px}.interior-panel-title{margin-bottom:9px}.interior-panel-title h3{font-size:14px} + .int-views{grid-template-columns:1fr 1fr;gap:5px}.int-views button{font-size:9px;padding:9px 6px}.int-kicker{margin:10px 0 7px;font-size:8px}.int-map-hint{display:none}#interiorRoom{font-size:10px;padding:8px}.int-options{clear:both;grid-template-columns:1fr 1fr;margin-top:14px;gap:10px}.int-options label{font-size:10px} + body.interior-active #interiorTools{left:50%;bottom:250px;gap:5px;width:max-content;max-width:calc(100% - 16px)} + body.interior-active.sun-open #interiorTools{left:50%;bottom:calc(var(--sun-sheet) + 8px)}body.interior-panel-closed #interiorTools{bottom:17px} + .int-mode-bar{padding:5px;gap:2px}.int-mode-bar button{font-size:10px;padding:9px 9px}.int-mode-bar .int-nav-button{padding:9px 10px}#interiorHint{display:none} + body.interior-walk #walkPad{left:12px;bottom:304px;grid-template-columns:36px 36px 36px;grid-template-rows:32px 32px;gap:4px}body.interior-walk.interior-panel-closed #walkPad{bottom:80px}body.interior-walk.sun-open #walkPad{bottom:calc(var(--sun-sheet) + 62px)} + .int-room-tag{font-size:8px;padding:4px 5px}.int-room-tag small{font-size:7px}.int-room-tag b .room-word{display:none} + #interiorReferenceDialog{padding:12px}.int-ref-head h3{font-size:12px} +} +@media(max-width:420px){ + body.interior-active .interior-heading{max-width:calc(100% - 98px);gap:8px}.interior-heading h2{font-size:15px}.interior-heading button{font-size:9px;padding:9px 8px} + #interiorMap{width:132px;margin-right:10px}.int-views button{font-size:8.5px;padding:8px 5px}.int-mode-bar button{padding:9px 8px} +} +@media(max-height:550px) and (max-width:850px){ + body.interior-active:not(.interior-panel-closed):not(.sun-open) #view{height:100%;width:calc(100% - 260px)} + body.interior-active #interiorPanel{left:auto;right:10px;top:65px;bottom:10px;width:245px;height:auto;border-radius:13px;padding:12px}#interiorMap{float:none;width:100%;margin-right:0} + body.interior-active #interiorTools{left:calc((100% - 260px)/2);bottom:12px}body.interior-panel-closed #interiorTools{left:50%}body.interior-walk #walkPad{bottom:64px} +} + + + +/* Interior-only paint controls; facade palette and furniture are independent. */ +.int-paint{clear:both;margin:18px 0 0;padding:14px 0 0;border-top:1px solid #e0e6d9} +.int-paint-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px} +.int-paint-head h4{margin:0;font-size:12px;font-weight:600;color:#38523a} +.int-paint-head span{font-size:9px;color:#7c8675} +.int-paint-fields{display:grid;grid-template-columns:48px minmax(0,1fr);gap:10px;align-items:end} +.int-paint-picker{display:block;height:43px;padding:4px;border:1px solid #ccd6c6;border-radius:9px;background:#fff;overflow:hidden;cursor:pointer} +#interiorWallColor{display:block;width:100%;height:100%;padding:0;border:0;border-radius:5px;background:transparent;cursor:pointer} +#interiorWallColor::-webkit-color-swatch-wrapper{padding:0} +#interiorWallColor::-webkit-color-swatch{border:1px solid #0002;border-radius:4px} +#interiorWallColor::-moz-color-swatch{border:1px solid #0002;border-radius:4px} +.int-paint-hex{min-width:0;display:flex;flex-direction:column;gap:4px;font-size:9px;color:#75816f} +#interiorWallHex{display:block;width:100%;min-width:0;height:32px;border:1px solid #d6e0d0;border-radius:7px;padding:7px 9px;background:#fff;color:#334931;font:11px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:.7px;text-transform:uppercase} +.int-paint-presets{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px;margin-top:10px} +.int-paint-presets button{display:flex;align-items:center;gap:6px;min-width:0;min-height:34px;padding:6px;border:1px solid #d9e0d2;border-radius:7px;background:#f7f9f3;color:#51654a;text-align:left;font-size:9px;line-height:1.25} +.int-paint-presets button:hover{background:#eaf0e4;border-color:#a6ba99} +.int-paint-presets button[aria-pressed=true]{border-color:#78946c;background:#e5eedf;color:#2e4b35;box-shadow:inset 0 0 0 1px #78946c} +.int-paint-chip{display:block;width:19px;height:19px;flex:0 0 19px;border-radius:4px;background:var(--paint);border:1px solid #0002} +.int-paint-reset{display:block;margin:10px 0 0;padding:7px 9px;border:1px solid #d6dfcf;border-radius:7px;background:#f3f6ef;color:#49613f;font-size:10px} +.int-paint-reset:hover{background:#e7eee0} +#interiorPanel .int-paint-note{font-size:9px;line-height:1.55;margin:8px 0 0;color:#7c8675} +@media(max-width:850px){.int-paint{margin-top:14px;padding-top:12px}.int-paint-presets{grid-template-columns:repeat(3,minmax(0,1fr))}.int-paint-head h4{font-size:11px}.int-paint-presets button{min-height:36px}.int-paint-fields{grid-template-columns:48px minmax(0,1fr)}} +@media(max-width:360px){.int-paint-presets{grid-template-columns:repeat(2,minmax(0,1fr))}} +@media(max-height:550px) and (max-width:850px){.int-paint-presets{grid-template-columns:repeat(2,minmax(0,1fr))}} + + +/* ALLSTAV 2025 configuration panel. Independent from the retained sun and site tools. */ +:root{--std-side:424px;--std-sheet:min(48dvh,440px);--std-line:#dce2d7;--std-green:#365944} +#standardsToggle{display:flex;align-items:center;gap:7px;font-weight:600;background:#365944;color:#fff;border-color:#365944} +#standardsToggle svg{width:15px;height:15px}#standardsToggle.active{background:#244633} +#standardsPanel{display:none;position:fixed;z-index:5;right:16px;top:78px;bottom:17px;width:392px;background:#fafbf7;border:1px solid #fff;border-radius:18px;box-shadow:0 12px 44px #20311f22;overflow:hidden;flex-direction:column} +body.std-open #standardsPanel{display:flex}body.std-open #view,body.std-open #measure,body.std-open #sunOverlay{width:calc(100% - var(--std-side))!important;height:100%!important} +body.std-open .bottom,body.std-open #interiorTools{left:calc((100% - var(--std-side))/2)!important} +body.std-open #interiorPanel,body.std-open #panel{display:none!important}body.std-open .compass{right:calc(var(--std-side) + 18px)} +body.std-open .interior-heading{max-width:calc(100% - var(--std-side) - 30px)}body.std-open #direction{font-size:10px;bottom:24px} +.std-head{padding:20px 20px 14px}.std-kicker{font-size:9px;letter-spacing:2px;font-weight:700;color:#829077}.std-head-row{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:6px}.std-head h2{font-size:23px;letter-spacing:-.6px;font-weight:500;margin:0}.std-close{border:0;background:#edf1e7;border-radius:50%;width:29px;height:29px;flex-shrink:0;font-size:20px;line-height:20px}.std-head p{font-size:10px;line-height:1.5;color:#7c8774;margin:6px 0 0} +.std-tabs{margin:0 16px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;background:#ecf0e7;border:1px solid #e1e6db;padding:4px;border-radius:10px}.std-tabs button{border:0;border-radius:7px;padding:10px 5px;background:none;font-size:12px}.std-tabs button[aria-selected=true]{background:#365944;color:white;box-shadow:0 2px 5px #233a2520} +.std-scroll{overflow:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:#c1cbbb transparent;flex:1;min-height:0;padding:0 17px 18px}.std-scroll [hidden]{display:none!important}.std-shortcuts{display:flex;gap:5px;margin:13px 0 14px}.std-shortcuts button{flex:1;border:1px solid #dbe2d4;border-radius:7px;background:#f3f6ee;padding:8px 3px;font-size:10px}.std-shortcuts button:hover{background:#e4ecde} +.std-notice{font-size:10px;line-height:1.6;color:#777f6e;background:#eef2e9;border-radius:8px;padding:10px 11px;margin:10px 0}.std-notice strong{color:#53624b;font-weight:600}.std-notice.amber{background:#f6efdf;color:#8a744a}.std-notice.amber strong{color:#795f36}.std-notice.blue{background:#eaf0f2;color:#576e75} +.std-section{border-bottom:1px solid var(--std-line);padding:0}.std-section>summary{display:flex;align-items:center;gap:10px;list-style:none;cursor:pointer;padding:16px 0;line-height:1.3}.std-section>summary::-webkit-details-marker{display:none}.std-section>summary:after{content:'+';font-size:18px;color:#8d9985;margin-left:auto}.std-section[open]>summary:after{content:'\2212'}.std-section>summary .std-number{font-size:10px;width:22px;height:22px;border:1px solid #d6decd;background:#eef2e8;border-radius:7px;display:flex;align-items:center;justify-content:center;color:#738568;flex-shrink:0}.std-section>summary strong{display:block;font-size:12px;font-weight:600}.std-section>summary small{display:block;font-size:10px;font-weight:400;color:#849078;margin-top:4px;max-width:271px}.std-section .std-body{padding-bottom:17px} +.std-label{display:flex;justify-content:space-between;align-items:center;font-size:11px;font-weight:600;margin:13px 0 9px;color:#4a5c42;gap:7px}.std-source{border:0;padding:3px 0;background:none;color:#849375;font-size:9px;text-decoration:underline;text-underline-offset:3px;cursor:pointer}.std-body p{font-size:10px;line-height:1.6;color:#7c8673;margin:8px 0 10px} +.std-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}.std-grid.cols3{grid-template-columns:repeat(3,minmax(0,1fr))}.std-grid.cols5{grid-template-columns:repeat(5,minmax(0,1fr))}.std-grid.cols2{grid-template-columns:repeat(2,minmax(0,1fr))} +.std-card{position:relative;border:1px solid #dce2d5;background:white;border-radius:9px;padding:5px 5px 7px;min-width:0;min-height:65px;text-align:center;font-size:10px;line-height:1.35;transition:border-color .12s,box-shadow .12s}.std-card:hover{border-color:#8aa27c}.std-card[aria-pressed=true]{border-color:#55754a;box-shadow:0 0 0 1px #55754a;background:#f0f5e9}.std-card[aria-pressed=true]:after{content:'\2713';position:absolute;right:3px;top:3px;border-radius:50%;width:15px;height:15px;background:#365944;color:#fff;font-size:10px;line-height:15px}.std-card .sample{display:block;width:100%;height:32px;border-radius:5px;margin-bottom:6px;border:1px solid #00000008;background-size:cover;background-position:center}.std-card img{display:block;object-fit:cover;width:100%;height:48px;border-radius:4px;margin-bottom:6px}.std-card.door img{height:70px;object-fit:contain;background:#fff}.std-card.wood img{height:56px;object-fit:cover}.std-card strong{font-weight:500;display:block}.std-card small{display:block;margin-top:3px;font-size:8px;color:#87947c}.std-card .premium{color:#9e793e}.std-card .visual{color:#7b8990} +.std-swatch-inline{display:inline-block;width:11px;height:11px;border-radius:3px;margin-right:5px;vertical-align:-1px;border:1px solid #0001}.std-two{display:grid;grid-template-columns:1fr 1fr;gap:9px}.std-field{display:block;font-size:10px;color:#75816b;margin:10px 0;line-height:1.5}.std-field select,.std-field input:not([type=color]):not([type=range]):not([type=checkbox]){display:block;width:100%;padding:10px 9px;border:1px solid #d7dfcf;border-radius:7px;background:#fff;font-size:11px;color:#3e5338;margin-top:5px;min-height:36px}.std-field input[type=color]{display:block;width:100%;height:35px;border:1px solid #d7dfcf;border-radius:7px;padding:3px;margin-top:5px;background:white}.std-field input[type=range]{width:100%;accent-color:#365944}.std-range-head{display:flex;justify-content:space-between;font-size:10px;color:#718165;margin-top:11px}.std-range-head strong{font-size:12px;color:#365944;font-weight:600} +.std-action{border:1px solid #d3dec8;background:#eff4e8;border-radius:7px;padding:9px 11px;font-size:10px;color:#46603c}.std-action.wide{width:100%;margin-top:10px}.std-action:hover{background:#e2ebd8}.std-action.dark{background:#365944;border-color:#365944;color:white} +.std-toggle{display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12px;font-weight:600;cursor:pointer;margin:3px 0 10px;padding:10px;background:#eef3e8;border-radius:9px}.std-toggle input{appearance:none;width:35px;height:21px;border-radius:12px;background:#bdc6b6;position:relative;flex-shrink:0;cursor:pointer;transition:background .15s}.std-toggle input:before{content:'';position:absolute;width:15px;height:15px;border-radius:50%;background:white;left:3px;top:3px;box-shadow:0 1px 4px #0002;transition:transform .15s}.std-toggle input:checked{background:#365944}.std-toggle input:checked:before{transform:translateX(14px)} +.std-check{display:flex;gap:9px;align-items:center;font-size:11px;color:#63765a;margin:12px 0;line-height:1.5}.std-check input{accent-color:#365944}.std-disabled{opacity:.45;pointer-events:none}.std-inventory{margin:10px 0;border:1px solid #e0e5da;border-radius:9px;padding:5px 11px}.std-inventory>div{font-size:10px;line-height:1.5;padding:8px 0;border-bottom:1px solid #e6eadf}.std-inventory>div:last-child{border:0}.std-inventory b{font-weight:600;color:#49623f}.std-inventory small{display:block;color:#8a9480;font-size:9px;margin-top:3px} +.std-footer{padding:12px 16px;border-top:1px solid #dce2d6;background:#f5f8ee;flex-shrink:0}.std-saved{font-size:9px;color:#859378;display:flex;gap:6px;align-items:center;margin-bottom:8px}.std-saved:before{content:'';width:5px;height:5px;border-radius:50%;background:#648257}.std-footer-row{display:grid;grid-template-columns:1fr 1fr;gap:7px}.std-footer-row button{font-size:11px;padding:10px}.std-summary-title{font-size:18px;font-weight:500;letter-spacing:-.3px;margin:19px 0 7px}.std-summary-group{margin-top:16px}.std-summary-group h4{font-size:10px;text-transform:uppercase;letter-spacing:1px;font-weight:600;color:#728466;margin:16px 0 7px}.std-summary-row{display:grid;grid-template-columns:1fr 1.5fr;gap:9px;font-size:10px;line-height:1.6;padding:8px 0;border-bottom:1px solid #e2e7dc}.std-summary-row span{color:#7e8a72}.std-summary-row b{font-weight:500;text-align:right;color:#41583b}.std-summary-row small{display:block;font-size:8px;color:#9b7945} +.std-export-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:16px}.std-export-grid button{border:1px solid #d5dfcd;background:#f2f6ec;padding:11px 7px;border-radius:8px;font-size:10px}.std-reset{color:#9c6852!important;background:#f7f0e8!important}.std-fine{font-size:9px!important;line-height:1.6!important;color:#949e89!important;margin:12px 0 0!important} +#stdCatalogDialog{width:min(800px,94vw);max-height:94dvh;border:1px solid #fff;border-radius:15px;background:#fafbf7;padding:15px;box-shadow:0 20px 70px #13271066;color:#36523d}#stdCatalogDialog::backdrop,#stdResetDialog::backdrop{background:#1f2e23a8;backdrop-filter:blur(3px)}.std-dialog-head{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:9px}.std-dialog-head h3{margin:0;font-size:13px;font-weight:500}.std-dialog-head button{border:0;background:#e9efe1;padding:8px 11px;border-radius:7px;font-size:11px}#stdCatalogImage{display:block;max-width:100%;max-height:75dvh;object-fit:contain;margin:auto}#stdCatalogCaption{font-size:10px;color:#7c8871;line-height:1.5;margin:10px 0 0}.std-dialog-pager{display:flex;gap:7px;align-items:center;justify-content:space-between;margin-top:10px}.std-dialog-pager button{border:1px solid #d6dfcc;border-radius:7px;background:#edf3e5;padding:8px 11px;font-size:10px} +#stdResetDialog{width:min(390px,92vw);border:1px solid white;border-radius:16px;background:#fafbf7;padding:23px;color:#36523d}#stdResetDialog h3{font-size:19px;font-weight:500;margin:0 0 10px}#stdResetDialog p{font-size:12px;line-height:1.7;color:#7a866f}#stdResetDialog .std-two{margin-top:19px} +body.std-open #interiorLaunch{z-index:2} +@media(min-width:851px) and (max-width:1180px){body.std-open .compass,body.std-open #direction,body.std-open .help{display:none}body.std-open .tool{padding:9px 7px;font-size:10px}body.std-open .site-switch{font-size:9px}body.std-open .bottom{bottom:18px}body.std-open header .metrics{display:none!important}body.std-open #sunStamp{top:125px}body.std-open .interior-heading h2{font-size:17px}body.std-open .interior-heading .eyebrow{display:none}} +@media(max-width:850px){ + #standardsPanel{top:auto;left:0;right:0;bottom:0;width:100%;height:var(--std-sheet);border-radius:18px 18px 0 0} + body.std-open #view,body.std-open #measure,body.std-open #sunOverlay{width:100%!important;height:calc(100% - var(--std-sheet))!important} + body.std-open .bottom,body.std-open #interiorTools{left:50%!important;bottom:calc(var(--std-sheet) + 8px)!important;gap:0} + body.std-open .site-controls,body.std-open .help,body.std-open #direction,body.std-open .compass{display:none} + body.std-open .interior-heading{max-width:calc(100% - 126px)}body.std-open #walkPad{bottom:calc(var(--std-sheet) + 65px)!important} + #standardsToggle{padding:8px 9px;font-size:10px;gap:4px;min-height:34px}.std-head{padding:12px 17px 9px}.std-head .std-kicker{font-size:8px}.std-head h2{font-size:18px}.std-head p{display:none}.std-head-row{margin-top:3px}.std-close{width:25px;height:25px} + .std-tabs{margin:0 14px}.std-tabs button{padding:8px 4px;font-size:11px}.std-scroll{padding:0 16px 18px}.std-shortcuts{margin:9px 0}.std-footer{padding:8px 15px}.std-saved{margin-bottom:5px;font-size:8px}.std-footer-row button{padding:8px;font-size:10px}.std-section>summary{padding:13px 0} + .std-card.door img{height:62px}.std-notice{font-size:9px;padding:8px 10px}.std-card{font-size:9px}.std-section>summary small{max-width:calc(100vw - 115px)}body.std-open header h1{font-size:23px}body.std-open #interiorLaunch{top:139px;font-size:10px;padding:8px 10px} +} +@media(max-width:530px){#standardsToggle .std-toggle-word{display:none}#standardsToggle{width:34px;justify-content:center;padding:0}.topright{gap:5px!important}.topright #infoButton{display:none}.std-head .std-kicker{display:none}.std-head-row{margin:0}.std-head{padding:11px 16px 8px}.std-head h2{font-size:17px}body.std-open #sunStamp{top:98px;font-size:9px}body.std-open .interior-heading .eyebrow{font-size:6px;letter-spacing:.6px}} +@media(max-height:550px) and (max-width:850px){:root{--std-side:340px}#standardsPanel{left:auto;right:8px;top:60px;bottom:8px;width:324px;height:auto;border-radius:12px}body.std-open #view,body.std-open #sunOverlay{width:calc(100% - 340px)!important;height:100%!important}body.std-open .bottom,body.std-open #interiorTools{left:calc((100% - 340px)/2)!important;bottom:10px!important}.std-head{padding:9px 14px}.std-head h2{font-size:16px}.std-footer{display:none}body.std-open #walkPad{bottom:60px!important}body.std-open .interior-heading{max-width:calc(100% - 370px)}} + +@media(max-width:850px){body.std-open header .sub,body.std-open #sunStamp,body.std-open #interiorLaunch{display:none!important}body.std-open header h1{font-size:19px;line-height:1.2;max-width:calc(100vw - 116px);margin-top:9px}body.std-open header .eyebrow{font-size:7px;letter-spacing:1.3px}body.std-open #toast{top:73px;max-width:90%;font-size:10px}} + diff --git a/klabava/css/klabava.css b/klabava/css/klabava.css new file mode 100644 index 0000000..14859ae --- /dev/null +++ b/klabava/css/klabava.css @@ -0,0 +1,35 @@ +:root{--solar:#bd984f;--solar-soft:#f5eedc;--sun-side:380px} +header h1{font-size:32px;letter-spacing:-1px} +.topright{z-index:5} +.panel{z-index:6;max-height:calc(100dvh - 96px);overflow:auto;overscroll-behavior:contain;scrollbar-width:thin} +.panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px} +.panel-kicker{font-size:9px;font-weight:700;letter-spacing:1.7px;color:#8d753d;margin-bottom:6px} +.panel-close{flex:0 0 30px;border:0;border-radius:50%;width:30px;height:30px;background:#eef1e9;color:#657160;font-size:21px;line-height:1} +.panel .row{gap:12px} +.panel .row strong{text-align:right} +.panel .status{margin-top:18px} +.document-links{gap:7px} +.document-links a.btn{font-size:10px;padding:8px 10px} +.site-switch{font-family:Arial,Helvetica,sans-serif} +.site-controls{display:flex} +#siteParking{accent-color:#536f56} +#loading{position:fixed;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;gap:11px;background:#e7ebe3;color:#4f6252;font-size:12px;letter-spacing:.2px;transition:opacity .25s} +#loading.done{opacity:0;pointer-events:none} +#loading.error{inset:auto 50% 50% auto;transform:translate(50%,50%);width:min(420px,calc(100vw - 36px));padding:24px;background:#fafbf6;border:1px solid #d8ded4;border-radius:14px;box-shadow:0 12px 38px #26342c22;line-height:1.6} +.loader-dot{width:8px;height:8px;border-radius:50%;background:#607f63;box-shadow:0 0 0 5px #607f6320;animation:pulse 1s ease-in-out infinite alternate} +@keyframes pulse{to{transform:scale(.72);opacity:.5}} +#toast{position:fixed;left:50%;top:22px;z-index:10;transform:translateX(-50%);padding:9px 13px;border-radius:8px;background:#26342ce8;color:white;font-size:11px;opacity:0;pointer-events:none;transition:opacity .2s} +#sunOverlay{z-index:2} +#sunPanel{z-index:7} +#panel.open{display:block} +body.info-open #view,body.info-open #measure,body.info-open #sunOverlay{width:calc(100% - 390px);height:100%} +body.info-open .bottom{left:calc((100% - 390px)/2)} +body.info-open .compass{right:408px} +body.sun-open .bottom{left:calc((100% - var(--sun-side))/2)} +body.sun-open .compass{right:calc(var(--sun-side) + 18px)} +body.sun-open #view,body.sun-open #measure,body.sun-open #sunOverlay{width:calc(100% - var(--sun-side));height:100%} +#sunStamp{z-index:2} +@media(max-width:1100px) and (min-width:851px){header .metrics{gap:14px}.metric strong{font-size:16px}.topright{right:15px;gap:5px}.btn{padding:9px 11px}.toolbar .tool{padding:9px 8px;font-size:10px}.site-switch{font-size:9px;padding:8px 9px}.panel{width:320px}.sun-open #view,.sun-open #measure,.sun-open #sunOverlay{width:calc(100% - 360px)}#sunPanel{width:332px}.sun-open .bottom{left:calc((100% - 360px)/2)}.sun-open .compass{right:378px}} +@media(max-width:850px){header{left:18px;top:17px}header h1{font-size:23px;margin:6px 0 4px}.metrics{display:none!important}.sub{font-size:10px}.topright{top:15px;right:12px;gap:5px}.topright .btn{padding:8px 10px;font-size:10px}.topright .btn.primary{padding:8px 10px}.bottom{max-width:calc(100% - 14px);bottom:13px;gap:6px}.toolbar{gap:1px;max-width:100%;overflow-x:auto;padding:5px;border-radius:12px}.toolbar .tool{padding:8px 8px;font-size:10px}.toolbar .divider{margin:0 2px;height:17px}.site-switch{padding:8px 7px;font-size:9px}.help{font-size:8px}.compass{right:13px;bottom:12px;width:62px;height:62px}.panel{left:8px;right:8px;top:auto;bottom:8px;width:auto;max-height:min(72dvh,570px);border-radius:15px;padding:17px}.panel h2{font-size:20px}.panel p{font-size:11px}#sunPanel{left:8px;right:8px;top:auto;bottom:8px;width:auto;max-height:min(68dvh,570px);padding:15px;border-radius:15px}.sun-panel-head{margin-bottom:10px}.sun-date-row{grid-template-columns:1fr 95px;gap:8px}.sun-field #sunClock{width:95px;font-size:18px}.sun-graph{height:70px}.sun-play-row{margin-top:8px}.sun-readouts{margin-top:12px;padding-top:12px}.sun-stat strong{font-size:21px}.sun-events{margin:10px 0 2px;padding:9px 0}body.info-open #view,body.info-open #measure,body.info-open #sunOverlay{width:100%;height:calc(100dvh - min(72dvh,570px) - 16px)}body.sun-open #view,body.sun-open #measure,body.sun-open #sunOverlay{width:100%;height:calc(100dvh - min(68dvh,570px) - 16px)}body.info-open .bottom{left:50%;bottom:calc(min(72dvh,570px) + 13px)}body.sun-open .bottom{left:50%;bottom:calc(min(68dvh,570px) + 13px)}body.info-open .compass,body.sun-open .compass{display:none}body.info-open header .sub,body.sun-open header .sub{display:none}#sunStamp{left:18px;top:87px;font-size:9px}#webStatus{left:10px;bottom:5px;font-size:7px}body.info-open .topright,body.sun-open .topright{z-index:8}} +@media(max-width:520px){.topright .btn{padding:7px 8px}.topright #infoButton{font-size:0;width:33px;height:32px;position:relative}.topright #infoButton:after{content:'i';font-size:14px;font-weight:600}.topright #sunToggle .toggle-word{display:none}.topright #sunToggle{width:34px;justify-content:center;padding:0}.topright .btn.primary{font-size:9px}.bottom{max-width:calc(100% - 8px);bottom:8px}.toolbar .tool{padding:8px 6px;font-size:9px}.toolbar .site-switch{padding:7px 5px;font-size:8px}.toolbar #savePng{display:none}.toolbar{max-width:100vw}.help{display:none}#sunPanel{max-height:72dvh}.sun-hint{margin:5px 0 8px}.sun-advanced{margin-top:10px;padding-top:9px}} +@media(max-width:390px){.topright{gap:3px}.topright .btn.primary{padding:7px}.toolbar .tool{padding:8px 5px}.toolbar .divider{display:none}.toolbar .site-switch{padding:6px 4px}.toolbar{gap:0}} diff --git a/klabava/css/web.css b/klabava/css/web.css new file mode 100644 index 0000000..5dc6ffa --- /dev/null +++ b/klabava/css/web.css @@ -0,0 +1,24 @@ +/* Deployment-specific UI; the original configurator layout stays independent. */ +#webStatus{position:fixed;left:32px;bottom:13px;z-index:5;font-size:9px;color:#5e6b5b;pointer-events:none;background:#fafcf5d9;padding:3px 6px;border-radius:5px} +#projectToggle{position:fixed;left:32px;top:291px;z-index:5;font-size:11px;padding:9px 13px} +body.interior-active #projectToggle{top:76px;left:25px} +#projectDialog{width:min(920px,calc(100% - 32px));max-height:90dvh;padding:0;border:1px solid #dce4d5;border-radius:17px;color:#263b2c;background:#fafcf6;box-shadow:0 20px 85px #0a221740} +#projectDialog::backdrop{background:#13271988;backdrop-filter:blur(4px)} +.web-dialog-head{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:23px 27px;border-bottom:1px solid #dae2d5;position:sticky;top:0;background:#fafcf6;z-index:1} +.web-dialog-head small{font-size:9px;letter-spacing:2px;color:#6b7f64}.web-dialog-head h2{font-weight:500;font-size:25px;margin:7px 0 0} +.web-dialog-body{padding:8px 27px 25px;font-size:13px;line-height:1.65}.web-dialog-body h3{font-weight:600;font-size:16px;margin:24px 0 13px}.web-lead{font-size:14px;color:#657461} +.web-note{font-size:11px;color:#6c7865}.web-photo-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px} +.web-photo-grid button{padding:0;border:1px solid #d9e0d1;border-radius:10px;overflow:hidden;background:white;text-align:left;line-height:1.5}.web-photo-grid img{display:block;width:100%;height:210px;object-fit:cover}.web-photo-grid span{display:block;padding:10px 12px;font-size:12px} +.web-stat-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.web-stat-grid>div{background:#edf2e9;padding:14px;border-radius:9px}.web-stat-grid strong{display:block;font-size:19px;font-weight:500}.web-stat-grid span{display:block;font-size:10px;margin-top:4px;color:#6d7e65;line-height:1.5} +.web-actions{display:flex;flex-wrap:wrap;gap:9px;margin:15px 0}.web-actions a{display:inline-block;text-decoration:none}.web-actions .btn{line-height:1.4} +#surveyOverlay{position:fixed;top:0;left:0;pointer-events:none;z-index:2;overflow:hidden}#surveyOverlay circle{fill:#d97028;stroke:white;stroke-width:1}#surveyOverlay text{font:10px Arial;fill:#293e2b;stroke:#ffffffe6;stroke-width:3;paint-order:stroke;stroke-linejoin:round} +.web-fatal{position:fixed;inset:20%;z-index:1000;background:white;padding:40px;font:18px Arial;color:#26342c} +@media(max-width:850px){#projectToggle{top:206px;left:18px;font-size:10px;padding:8px 11px}body.interior-active #projectToggle{top:65px;left:16px}#webStatus{bottom:7px;left:12px;font-size:8px}.web-dialog-head{padding:17px}.web-dialog-body{padding:8px 17px 20px}.web-stat-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.web-photo-grid img{height:130px}.web-dialog-head h2{font-size:21px}#projectDialog{width:calc(100% - 18px);max-height:94dvh}body.std-open #projectToggle{display:none}} +@media(max-width:420px){.web-photo-grid{grid-template-columns:1fr}.web-photo-grid img{height:175px}.web-actions .btn{flex:1;min-width:130px;text-align:center}.web-stat-grid{gap:7px}.web-stat-grid>div{padding:11px}} +@media(max-height:550px){#projectToggle{top:auto;bottom:120px}} + +@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}} diff --git a/klabava/data/model.json b/klabava/data/model.json new file mode 100644 index 0000000..10c1a72 --- /dev/null +++ b/klabava/data/model.json @@ -0,0 +1,19 @@ +{ + "project": "Klabava", + "parcel": "66/17", + "parcelAreaM2": 1037, + "buildingFootprintM2": 178.75, + "coveredEntryAndTerraceM2": 206.54, + "usableFloorAreaM2": 153.74, + "footprintEnvelopeM": [17.4, 13.05], + "eaveM": 3.046, + "ridgeM": 5.832, + "chimneyM": 6.5, + "surveyGroundNearRoadBpv": 349.48, + "surveyGroundRearBpv": 347.2, + "latitude": 49.7536, + "longitude": 13.5394, + "latitudeLongitudeNote": "Přibližný střed obce Klabava; nikoli souřadnice parcely.", + "standards": null, + "modelNote": "Vizualizační model podle PDF podkladů; hranice a výšky terénu jsou převzaté a interpolované orientačně." +} diff --git a/klabava/index.html b/klabava/index.html new file mode 100644 index 0000000..01fa9c3 --- /dev/null +++ b/klabava/index.html @@ -0,0 +1,125 @@ + + + + + + + + Klabava | Rodinný dům + + + + + + + + +
+
RODINNÝ DŮM · KLABAVA
+

Klabava

+
Rodinný dům · 3D studie a simulace slunce
+
+
178,75 m²Zastavěná plocha
+
153,74 m²Užitná plocha
+
5,83 mHřeben střechy
+
+
+ +
+ + + +
+ + + +
AKTUÁLNÍ POHLEDDům / příjezd
+
+ +
Tažením otáčej · kolečkem přibliž · pravým tlačítkem posuň
+
+ +
+ + + + SJZV + + +
+ +
SLUNCE NAD DOMEM
21. 6. 2026 · 10:00
+ +
+
STUDIE OSVĚTLENÍ

Slunce a stíny

+
+
+
Slunce nad obzoremSELČ · UTC+2
+ + +
00:0006:0012:0018:0024:00
+
+
+
Výška nad obzorem—Střed slunečního kotouče
Azimut slunce—Od severu po směru hodin
+
Východ —Poledne —Západ —
+
+

Posouvej čas a sleduj, kam dopadá slunce na fasády a zahradu. Sever modelu vychází ze severky v situaci.

+ +
Poloha a orientace domu +
+

Výchozí poloha odpovídá přibližnému středu obce Klabava. Natočení je odečtené orientačně ze severky v situačním výkresu. Přesné souřadnice parcely a azimut stavby nebyly ve vstupních podkladech uvedeny.

+

Časové pásmo zůstává Europe/Prague. Letní čas se přepočítává podle zvoleného data a pravidel prohlížeče.

+ +
+
Metoda výpočtu a přesnost

Sluneční poloha se počítá z data, času a zeměpisných souřadnic podle astronomických rovnic Meeus / NOAA. Model zobrazuje přímé sluneční světlo a geometrické stíny. Nezahrnuje oblačnost, odrazy okolních budov, profil horizontu ani skutečné vlastnosti zasklení.

+
+
Načítám 3D model…
+
+
Pracovní model · Klabava 66/17
+ + + + diff --git a/klabava/index.php b/klabava/index.php new file mode 100644 index 0000000..7847b1f --- /dev/null +++ b/klabava/index.php @@ -0,0 +1,10 @@ +${error.message}
Spusť web přes místní server a zkontroluj připojení k CDN Three.js.`; + } + throw error; +} + +const $ = (selector) => document.querySelector(selector); +const RAD = Math.PI / 180; +const DAY_MS = 86400000; +const mod = (x, n) => ((x % n) + n) % n; +const clamp = (x, a, b) => Math.max(a, Math.min(b, x)); +const smooth = (a, b, x) => { const t = clamp((x - a) / (b - a), 0, 1); return t * t * (3 - 2 * t); }; +const mix = (a, b, t) => a.map((v, i) => v + (b[i] - v) * t); +const czNum = (v, n = 1) => Number(v).toFixed(n).replace('.', ','); +const canvas = $('#view'); +const loading = $('#loading'); +const toastEl = $('#toast'); + +const site = { latitude: 49.7536, longitude: 13.5394, bearing: 0 }; +const solar = { + date: '2026-06-21', minute: 600, playing: false, speed: 30, shadows: true, path: false, + start: 0, end: 0, length: 1440, rise: null, set: null, noon: null, samples: [], + position: null, direct: 0.8, dayFactor: 1, sky: [0.89, 0.91, 0.87], accumulator: 0, lastTick: 0 +}; + +const scene = new THREE.Scene(); +scene.background = new THREE.Color('#e7ebe3'); +const camera = new THREE.PerspectiveCamera(36, 1, 0.1, 220); +camera.position.set(28, 21, 36); + +let renderer; +try { + renderer = new THREE.WebGLRenderer({ + canvas, antialias: true, alpha: false, preserveDrawingBuffer: true, + powerPreference: 'high-performance' + }); +} catch (error) { + loading.classList.add('error'); + loading.innerHTML = `3D grafiku nelze spustit.
${error.message}
Zkontroluj podporu WebGL v prohlížeči.`; + throw error; +} +renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); +renderer.outputColorSpace = THREE.SRGBColorSpace; +renderer.toneMapping = THREE.ACESFilmicToneMapping; +renderer.toneMappingExposure = 1.05; +renderer.shadowMap.enabled = true; +renderer.shadowMap.type = THREE.PCFSoftShadowMap; +const controls = new OrbitControls(camera, renderer.domElement); +controls.enableDamping = true; +controls.dampingFactor = 0.075; +controls.minDistance = 15; +controls.maxDistance = 95; +controls.minPolarAngle = 0.08; +controls.maxPolarAngle = 1.52; +controls.target.set(0, 1.2, 8.2); + +scene.add(new THREE.HemisphereLight(0xf7f8ee, 0x6c705f, 1.55)); +scene.add(new THREE.AmbientLight(0xffffff, 0.35)); +const sunLight = new THREE.DirectionalLight(0xfff5df, 2.25); +sunLight.castShadow = true; +sunLight.shadow.mapSize.set(1536, 1536); +sunLight.shadow.camera.left = -32; +sunLight.shadow.camera.right = 32; +sunLight.shadow.camera.top = 36; +sunLight.shadow.camera.bottom = -36; +sunLight.shadow.camera.near = 0.1; +sunLight.shadow.camera.far = 110; +sunLight.shadow.bias = -0.00015; +sunLight.shadow.normalBias = 0.025; +const sunTarget = new THREE.Object3D(); +sunTarget.position.set(0, 1.2, 8.2); +scene.add(sunLight, sunTarget); +sunLight.target = sunTarget; + +const modelRoot = new THREE.Group(); +modelRoot.name = 'Klabava – dům a pozemek'; +scene.add(modelRoot); +const groundGroup = new THREE.Group(); groundGroup.name = 'Terén a zahrada'; modelRoot.add(groundGroup); +const boundaryGroup = new THREE.Group(); boundaryGroup.name = 'Orientační hranice parcely'; modelRoot.add(boundaryGroup); +const parkingGroup = new THREE.Group(); parkingGroup.name = 'Příjezd a parkování'; modelRoot.add(parkingGroup); +const houseGroup = new THREE.Group(); houseGroup.name = 'Rodinný dům – pracovní hmota'; houseGroup.position.set(0, 0, 8.4); modelRoot.add(houseGroup); + +const mats = { + grass: new THREE.MeshStandardMaterial({ color: 0x9ba78f, roughness: 1 }), + road: new THREE.MeshStandardMaterial({ color: 0x676d6b, roughness: 0.98 }), + gravel: new THREE.MeshStandardMaterial({ color: 0xc5c1b4, roughness: 1 }), + path: new THREE.MeshStandardMaterial({ color: 0xb8b7a9, roughness: 0.9 }), + wall: new THREE.MeshStandardMaterial({ color: 0xdedbd2, roughness: 0.86 }), + foundation: new THREE.MeshStandardMaterial({ color: 0xb9b9b1, roughness: 0.9, side: THREE.DoubleSide }), + roof: new THREE.MeshStandardMaterial({ color: 0x555c59, roughness: 0.82, metalness: 0.02, side: THREE.DoubleSide }), + glass: new THREE.MeshStandardMaterial({ color: 0x9aafb2, roughness: 0.2, metalness: 0.05, transparent: true, opacity: 0.78, side: THREE.DoubleSide }), + frame: new THREE.MeshStandardMaterial({ color: 0x626966, roughness: 0.7 }), + door: new THREE.MeshStandardMaterial({ color: 0x958d7d, roughness: 0.8 }), + terrace: new THREE.MeshStandardMaterial({ color: 0xaaa492, roughness: 0.88 }), + metal: new THREE.MeshStandardMaterial({ color: 0x545b58, roughness: 0.6, metalness: 0.18 }), + trunk: new THREE.MeshStandardMaterial({ color: 0x735d46, roughness: 1 }), + leaf: new THREE.MeshStandardMaterial({ color: 0x657c5c, roughness: 1 }), + leaf2: new THREE.MeshStandardMaterial({ color: 0x839273, roughness: 1 }), + car1: new THREE.MeshStandardMaterial({ color: 0x777e7b, roughness: 0.45, metalness: 0.08 }), + car2: new THREE.MeshStandardMaterial({ color: 0xa59f91, roughness: 0.45, metalness: 0.08 }), + tire: new THREE.MeshStandardMaterial({ color: 0x333635, roughness: 0.95 }), + pavingLine: new THREE.MeshStandardMaterial({ color: 0xe4e0d2, roughness: 0.9 }) +}; + +function mesh(geometry, material, parent, name = '') { + const object = new THREE.Mesh(geometry, material); + object.name = name; + object.castShadow = true; + object.receiveShadow = true; + parent.add(object); + return object; +} +function box(parent, name, x, y, z, sx, sy, sz, material, rotationY = 0) { + const object = mesh(new THREE.BoxGeometry(sx, sy, sz), material, parent, name); + object.position.set(x, y, z); + object.rotation.y = rotationY; + return object; +} +function poly(parent, name, points, material) { + const positions = []; + for (const p of points) positions.push(p[0], p[1], p[2]); + const indices = []; + for (let i = 1; i < points.length - 1; i++) indices.push(0, i, 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 }); + const object = new THREE.Line(geometry, material); + object.name = name; + parent.add(object); + return object; +} +function group(parent, name) { const g = new THREE.Group(); g.name = name; parent.add(g); return g; } + +// Survey-led ground slope: about 349.5 m Bpv by the road and 347.2 m Bpv at the rear. +function rawGround(x, z) { return 0.12 - (20 - z) * (2.25 / 40) + x * 0.004; } +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; +} + +function buildGround() { + const nw = [-13, 20], ne = [13, 20], se = [13.5, -19.5], sw = [-12.5, -20]; + const nx = 34, nz = 54; + const positions = [], indices = []; + for (let j = 0; j <= nz; j++) { + const v = j / nz; + const leftX = nw[0] * (1 - v) + sw[0] * v, leftZ = nw[1] * (1 - v) + sw[1] * v; + const rightX = ne[0] * (1 - v) + se[0] * v, rightZ = ne[1] * (1 - v) + se[1] * v; + for (let i = 0; i <= nx; i++) { + const u = i / nx; + const x = leftX * (1 - u) + rightX * u; + const z = leftZ * (1 - u) + rightZ * u; + positions.push(x, groundHeight(x, z), z); + if (i < nx && j < nz) { + const a = j * (nx + 1) + i, b = a + 1, c = a + (nx + 1), d = c + 1; + indices.push(a, b, c, b, d, c); + } + } + } + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + geometry.setIndex(indices); + geometry.computeVertexNormals(); + mesh(geometry, mats.grass, groundGroup, 'Interpolovaný terén parcely 66/17'); + + // The northern road and its edge follow the topographic fall across the site. + const roadY = (z) => rawGround(0, z) + 0.01; + const roadPoints = [[-17, roadY(26), 26], [17, roadY(26), 26], [17, roadY(20.4), 20.4], [-17, roadY(20.4), 20.4]]; + poly(parkingGroup, 'Místní komunikace', roadPoints, mats.road); + line(parkingGroup, 'Okraj komunikace', [[-17, roadY(20.3) + 0.02, 20.3], [17, roadY(20.3) + 0.02, 20.3]], 0xbfc4bc, 0.8); + + // Parcel outline is intentionally thin and diagrammatic; the supplied survey has no parcel staking coordinates. + const border = [nw, ne, se, sw].map(([x, z]) => [x, groundHeight(x, z) + 0.045, z]); + line(boundaryGroup, 'Grafická hranice parcely 66/17', [...border, border[0]], 0x4f694f, 0.95); + for (let i = 0; i < border.length; i++) { + const [x, y, z] = border[i]; + const post = mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.12, 6), mats.frame, boundaryGroup, `Orientační lom hranice ${i + 1}`); + post.position.set(x, y + 0.05, z); + post.castShadow = false; + } + + buildParking(roadY); + buildGarden(); +} + +function buildParking(roadY) { + const y = (x, z) => groundHeight(x, z) + 0.035; + const drivewayPts = [[-9.2, y(-9.2, 20.2), 20.2], [-5.3, y(-5.3, 20.2), 20.2], [-5.3, y(-5.3, 17.8), 17.8], [-9.2, y(-9.2, 17.8), 17.8]]; + poly(parkingGroup, 'Příjezd na pozemek', drivewayPts, mats.gravel); + const pad = [[-10.3, y(-10.3, 17.7), 17.7], [-3.7, y(-3.7, 17.7), 17.7], [-3.7, y(-3.7, 14.9), 14.9], [-10.3, y(-10.3, 14.9), 14.9]]; + poly(parkingGroup, 'Orientační parkovací plocha', pad, mats.gravel); + line(parkingGroup, 'Osa příjezdu', [[-7.25, roadY(20) + 0.03, 20], [-7.25, y(-7.25, 17.8) + 0.04, 17.8]], 0x8f958b, 0.65); + + function addCar(cx, cz, material, label) { + const car = group(parkingGroup, label); + car.position.set(cx, groundHeight(cx, cz) + 0.12, cz); + box(car, `${label} karoserie`, 0, 0.42, 0, 2.05, 0.72, 4.35, material); + box(car, `${label} kabina`, 0, 0.93, -0.1, 1.55, 0.62, 2.45, mats.glass); + for (const x of [-1.02, 1.02]) for (const z of [-1.42, 1.42]) { + const wheel = mesh(new THREE.CylinderGeometry(0.36, 0.36, 0.19, 12), mats.tire, car, `${label} kolo`); + wheel.rotation.z = Math.PI / 2; + wheel.position.set(x, 0.35, z); + } + box(car, `${label} přední sklo`, 0, 0.95, 1.12, 1.38, 0.43, 0.08, mats.glass, 0.18); + } + addCar(-8.15, 16.35, mats.car1, 'Orientační automobil 1'); + addCar(-5.75, 16.35, mats.car2, 'Orientační automobil 2'); + // A short walk connects the front door and the gravel parking area. + const walkway = [[-0.9, y(-0.9, 16.5), 16.5], [0.55, y(0.55, 16.5), 16.5], [0.55, y(0.55, 14.7), 14.7], [-0.9, y(-0.9, 14.7), 14.7]]; + poly(parkingGroup, 'Pěší vstup', walkway, mats.path); +} + +function addTree(x, z, height, variant = 0) { + const g = group(groundGroup, 'Orientační strom'); + g.position.set(x, groundHeight(x, z), z); + const trunkHeight = height * 0.42; + const trunk = mesh(new THREE.CylinderGeometry(0.11, 0.17, trunkHeight, 7), mats.trunk, g, 'Kmen'); + trunk.position.y = trunkHeight / 2; + const material = variant % 2 ? mats.leaf2 : mats.leaf; + for (let i = 0; i < 3; i++) { + const radius = height * (0.27 - i * 0.04); + const cone = mesh(new THREE.ConeGeometry(radius, height * 0.42, 8), material, g, 'Koruna'); + cone.position.set(0, trunkHeight + height * (0.17 + i * 0.18), 0); + } +} +function buildGarden() { + const trees = [[-10.7, -3.7, 3.7], [10.7, -3.4, 3.4], [-10.1, -10.2, 3.6], [11.0, -11.3, 4.1], [-7.4, -16.9, 3.4], [3.5, -17.1, 3.8], [9.6, 4.3, 3.1]]; + trees.forEach(([x, z, h], i) => addTree(x, z, h, i)); + for (const [x, z] of [[-9.5, -6.5], [10.3, -6.4], [-5.9, -18.1], [6.1, -18.2]]) { + const shrub = mesh(new THREE.SphereGeometry(0.65, 10, 8), mats.leaf2, groundGroup, 'Orientační keř'); + shrub.scale.set(1.2, 0.8, 0.9); + shrub.position.set(x, groundHeight(x, z) + 0.44, z); + } +} + +const footprint = [ + [-1.575, 6.525], [8.7, 6.525], [8.7, -2.775], [-1.575, -2.775], + [-1.575, -6.525], [-8.7, -6.525], [-8.7, 5.145], [-1.575, 5.145] +]; + +function makeFootprintSlab() { + const shape = new THREE.Shape(); + shape.moveTo(footprint[0][0], footprint[0][1]); + footprint.slice(1).forEach(([x, z]) => shape.lineTo(x, z)); + shape.closePath(); + const geometry = new THREE.ShapeGeometry(shape); + geometry.rotateX(Math.PI / 2); + const slab = mesh(geometry, mats.foundation, houseGroup, 'Základová deska domu'); + slab.position.y = -0.19; + return slab; +} + +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 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; + const [x, z] = positionAt((from + to) / 2); + box(houseGroup, name, x, (bottom + top) / 2, z, to - from, top - bottom, thickness, mats.wall, angle); + }; + const ordered = [...openings].sort((a, b) => a.center - b.center); + let cursor = 0; + for (const opening of ordered) { + 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'); + 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 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); + } + 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); +} + +function hipRoof(x0, x1, z0, z1, name, ridgeHeight = 5.832) { + const overhang = 0.38, eave = 3.06; + x0 -= overhang; x1 += overhang; z0 -= overhang; z1 += overhang; + 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); +} + +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 }], + [] + ]; + for (let i = 0; i < footprint.length; i++) makeWallEdge(footprint[i], footprint[(i + 1) % footprint.length], openings[i]); + hipRoof(-1.575, 8.7, -2.775, 6.525, 'Hlavní valbová střecha'); + hipRoof(-8.7, -1.575, -6.525, 5.145, 'Střecha západního křídla'); + + // 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); + + // Covered terrace is included in the site sheets; its posts and canopy are deliberately simple. + 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); + } + 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); +} + +buildGround(); +buildHouse(); + +function viewportSize() { + let w = window.innerWidth, h = window.innerHeight; + const infoOpen = document.body.classList.contains('info-open'); + const sunOpen = document.body.classList.contains('sun-open'); + if (w <= 850 && (infoOpen || sunOpen)) { + const panelHeight = Math.min((infoOpen ? 0.72 : 0.68) * h, 570); + h = Math.max(120, h - panelHeight - 16); + } else if (sunOpen) { + w = Math.max(240, w - 380); + } else if (infoOpen) { + w = Math.max(240, w - 390); + } + return [w, h]; +} +function resize() { + const [w, h] = viewportSize(); + camera.aspect = w / h; + camera.updateProjectionMatrix(); + renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); + renderer.setSize(Math.round(w), Math.round(h), false); +} +window.addEventListener('resize', resize); +resize(); + +function toast(message) { + toastEl.textContent = message; + toastEl.style.opacity = '1'; + clearTimeout(toast.timer); + toast.timer = setTimeout(() => { toastEl.style.opacity = '0'; }, 2600); +} +function saveBlob(blob, filename) { + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = filename; + a.click(); + setTimeout(() => URL.revokeObjectURL(a.href), 5000); +} + +const presets = { + entry: { position: [24, 18, 31], target: [0, 1.15, 9.3], label: 'Dům / příjezd' }, + garden: { position: [-2, 15, -22], target: [0, 1.25, 7], label: 'Dům / zahrada' }, + top: { position: [0, 77, 8], target: [0, 0.1, 8], label: 'Pozemek / shora' } +}; +function setPreset(name) { + const p = presets[name] || presets.entry; + camera.position.fromArray(p.position); + controls.target.fromArray(p.target); + controls.update(); + $('#directionText').textContent = p.label; + document.querySelectorAll('[data-preset]').forEach(button => button.classList.toggle('active', button.dataset.preset === name)); +} +document.querySelectorAll('[data-preset]').forEach(button => button.addEventListener('click', () => setPreset(button.dataset.preset))); +$('#resetView').addEventListener('click', () => setPreset('entry')); +setPreset('entry'); + +$('#siteTerrain').addEventListener('change', (event) => { groundGroup.visible = event.target.checked; }); +$('#siteBoundary').addEventListener('change', (event) => { boundaryGroup.visible = event.target.checked; }); +$('#siteParking').addEventListener('change', (event) => { parkingGroup.visible = event.target.checked; }); + +function toggleInfo(force) { + const on = force ?? !$('#panel').classList.contains('open'); + $('#panel').classList.toggle('open', on); + document.body.classList.toggle('info-open', on); + $('#infoButton').classList.toggle('active', on); + $('#infoButton').setAttribute('aria-expanded', String(on)); + if (on) toggleSun(false); + resize(); +} +function toggleSun(force) { + const on = force ?? !$('#sunPanel').classList.contains('open'); + $('#sunPanel').classList.toggle('open', on); + document.body.classList.toggle('sun-open', on); + $('#sunToggle').classList.toggle('active', on); + $('#sunToggle').setAttribute('aria-expanded', String(on)); + if (on) toggleInfo(false); + resize(); +} +$('#infoButton').addEventListener('click', () => toggleInfo()); +$('#infoClose').addEventListener('click', () => toggleInfo(false)); +$('#sunToggle').addEventListener('click', () => toggleSun()); +$('#sunClose').addEventListener('click', () => toggleSun(false)); + +// Local civil-time helpers keep the sun controls aligned with Europe/Prague and DST. +const localFormatter = new Intl.DateTimeFormat('en-GB', { + timeZone: 'Europe/Prague', year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23' +}); +function localParts(ms) { + const out = {}; + for (const part of localFormatter.formatToParts(new Date(ms))) if (part.type !== 'literal') out[part.type] = +part.value; + return out; +} +function utcOffset(ms) { + const p = localParts(ms); + return Math.round((Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second) - Math.floor(ms / 1000) * 1000) / 60000); +} +function localMidnight(iso) { + const [y, m, d] = iso.split('-').map(Number); + const naive = Date.UTC(y, m - 1, d); + let utc = naive; + for (let i = 0; i < 4; i++) utc = naive - utcOffset(utc) * 60000; + return utc; +} +function clockText(ms) { + if (ms === null || !Number.isFinite(ms)) return '—'; + const p = localParts(ms); + return `${String(p.hour).padStart(2, '0')}:${String(p.minute).padStart(2, '0')}`; +} +function czDate(iso) { + const [y, m, d] = iso.split('-').map(Number); + return `${d}. ${m}. ${y}`; +} +function zoneText(ms) { + const offset = utcOffset(ms); + return `${offset === 120 ? 'SELČ' : offset === 60 ? 'SEČ' : 'Praha'} · UTC${offset >= 0 ? '+' : ''}${offset / 60}`; +} +function timeForClock(text) { + if (!/^\d{1,2}:\d{2}$/.test(text)) return null; + const [h, m] = text.split(':').map(Number); + if (!Number.isFinite(h) || !Number.isFinite(m) || h < 0 || h > 23 || m < 0 || m > 59) return null; + const normalized = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; + const [y, mo, d] = solar.date.split('-').map(Number); + const naive = Date.UTC(y, mo - 1, d, h, m); + const offsets = [...new Set([utcOffset(solar.start - 12 * 3600000), utcOffset(solar.start), utcOffset(solar.end - 1), utcOffset(solar.end + 12 * 3600000)])]; + const candidates = offsets.map(offset => naive - offset * 60000) + .filter(utc => utc >= solar.start && utc < solar.end && clockText(utc) === normalized).sort((a, b) => a - b); + if (candidates.length) return candidates[0]; + for (let i = 0; i < solar.length; i++) { + const utc = solar.start + i * 60000, p = localParts(utc); + if (p.hour * 60 + p.minute >= h * 60 + m) return utc; + } + return solar.end - 60000; +} +function refineCrossing(a, b, threshold) { + let fa = solarPosition(a, site.latitude, site.longitude).elevation - threshold; + for (let i = 0; i < 25; i++) { + const mid = (a + b) / 2; + const fm = solarPosition(mid, site.latitude, site.longitude).elevation - threshold; + if ((fa < 0) === (fm < 0)) { a = mid; fa = fm; } else b = mid; + } + return (a + b) / 2; +} + +let graphBase = ''; +function rebuildSolarDay(preserveClock = true) { + const previousClock = preserveClock && solar.start ? clockText(solar.start + solar.minute * 60000) : ($('#sunClock').value || '10:00'); + solar.start = localMidnight(solar.date); + const [y, m, d] = solar.date.split('-').map(Number); + const nextDate = new Date(Date.UTC(y, m - 1, d + 1)).toISOString().slice(0, 10); + solar.end = localMidnight(nextDate); + solar.length = Math.round((solar.end - solar.start) / 60000); + solar.rise = null; solar.set = null; solar.samples = []; + let previous = null, max = { altitude: -999, ms: solar.start }; + for (let utc = solar.start; utc <= solar.end; utc += 600000) { + const pos = solarPosition(utc, site.latitude, site.longitude); + solar.samples.push({ ms: utc, ...pos }); + if (pos.elevation > max.altitude) max = { altitude: pos.elevation, ms: utc }; + if (previous) { + if (previous.elevation < -0.833 && pos.elevation >= -0.833) solar.rise = refineCrossing(previous.ms, utc, -0.833); + if (previous.elevation >= -0.833 && pos.elevation < -0.833) solar.set = refineCrossing(previous.ms, utc, -0.833); + } + previous = { ms: utc, ...pos }; + } + let lo = Math.max(solar.start, max.ms - 600000), hi = Math.min(solar.end, max.ms + 600000); + for (let i = 0; i < 36; i++) { + const a = lo + (hi - lo) / 3, b = hi - (hi - lo) / 3; + if (solarPosition(a, site.latitude, site.longitude).elevation < solarPosition(b, site.latitude, site.longitude).elevation) lo = a; else hi = b; + } + solar.noon = (lo + hi) / 2; + solar.minute = clamp((timeForClock(previousClock) - solar.start) / 60000, 0, solar.length - 1); + $('#sunDate').value = solar.date; + $('#sunTime').max = solar.length - 1; + $('#sunRise').textContent = clockText(solar.rise === null ? null : solar.rise + 30000); + $('#sunSet').textContent = clockText(solar.set === null ? null : solar.set + 30000); + $('#sunNoon').textContent = clockText(solar.noon + 30000); + document.querySelectorAll('[data-season]').forEach(button => button.classList.toggle('active', solar.date.endsWith(button.dataset.season))); + const ticks = $('.sun-ticks'); + ticks.style.position = 'relative'; ticks.style.height = '12px'; + [...ticks.children].forEach((el, i) => { + const minutes = i === 4 ? solar.length : (timeForClock(el.textContent) - solar.start) / 60000; + el.style.cssText = `position:absolute;left:${100 * minutes / solar.length}%;transform:translateX(${i === 0 ? 0 : i === 4 ? -100 : -50}%)`; + }); + $('#solarMessage').textContent = solar.length === 1440 ? '' : `Tento den má ${solar.length / 60} hodin. Časová osa respektuje změnu letního času.`; + drawSolarGraphBase(); + updateSolar(); +} +function drawSolarGraphBase() { + const y = altitude => 70 - clamp(altitude, 0, 90) * 0.65; + const points = solar.samples.map(p => `${8 + 288 * (p.ms - solar.start) / (solar.end - solar.start)},${y(p.elevation)}`); + const curve = `M${points.join(' L')}`; + graphBase = `45°`; +} +function drawSolarGraph() { + if (!solar.position) return; + const x = 8 + 288 * solar.minute / solar.length; + const y = 70 - clamp(solar.position.elevation, 0, 90) * 0.65; + $('#sunGraph').innerHTML = graphBase + ``; +} +function updateSolar() { + solar.minute = clamp(solar.minute, 0, solar.length - 1); + const utc = solar.start + solar.minute * 60000; + solar.position = solarPosition(utc, site.latitude, site.longitude); + const { elevation, azimuth } = solar.position; + solar.dayFactor = smooth(-10, 4, elevation); + solar.direct = elevation > 0 ? (1.02 - 0.32 * Math.exp(-Math.max(elevation, 0) / 9)) * smooth(0, 1.2, elevation) : 0; + const daylight = solar.dayFactor; + solar.sky = mix([0.075, 0.112, 0.157], [0.906, 0.922, 0.890], daylight); + if (elevation > -7 && elevation < 9) solar.sky = mix(solar.sky, [0.89, 0.83, 0.74], 0.16 * Math.sin((elevation + 7) / 16 * Math.PI)); + scene.background.setRGB(...solar.sky); + const az = azimuth * RAD, alt = elevation * RAD; + const distance = 65; + modelRoot.updateMatrixWorld(true); + sunTarget.position.set(0, 1.2, houseGroup.position.z + 1).applyMatrix4(modelRoot.matrixWorld); + sunLight.position.set(Math.sin(az) * Math.cos(alt), Math.sin(alt), Math.cos(az) * Math.cos(alt)).multiplyScalar(distance).add(sunTarget.position); + sunLight.intensity = Math.max(0, solar.direct) * 2.55; + sunLight.color.setRGB(1, elevation < 12 ? 0.87 + elevation / 100 : 0.96, elevation < 12 ? 0.68 + elevation / 55 : 0.88); + sunLight.castShadow = solar.shadows && elevation > -0.5; + solar.lastTick = utc; + $('#sunClock').value = clockText(utc); + $('#sunTime').value = Math.floor(solar.minute); + $('#sunTime').setAttribute('aria-valuetext', clockText(utc)); + $('#zoneLabel').textContent = zoneText(utc); + $('#sunElevation').textContent = `${czNum(elevation)}°`; + $('#sunAzimuth').textContent = `${czNum(azimuth)}°`; + const dirs = ['sever', 'severovýchod', 'východ', 'jihovýchod', 'jih', 'jihozápad', 'západ', 'severozápad']; + $('#sunDirection').textContent = `${dirs[Math.round(azimuth / 45) % 8]} · od severu`; + $('#dayState').textContent = elevation > 0 ? 'Slunce nad obzorem' : elevation > -0.833 ? 'Slunce na obzoru' : elevation > -6 ? 'Soumrak' : 'Noc · bez přímého slunce'; + $('#sceneTime').textContent = `${czDate(solar.date)} · ${clockText(utc)} ${utcOffset(utc) === 120 ? 'SELČ' : 'SEČ'}`; + document.body.classList.toggle('night', elevation < -4); + document.body.style.backgroundColor = `rgb(${solar.sky.map(v => Math.round(v * 255)).join(',')})`; + drawSolarGraph(); +} + +function solarFrame(dt) { + if (!solar.playing) return; + solar.minute += dt * solar.speed; + if (solar.minute >= solar.length) solar.minute = 0; + solar.accumulator += dt; + if (solar.accumulator >= 0.075) { solar.accumulator = 0; updateSolar(); } +} +function setSolarPlaying(on) { + solar.playing = on; + $('#sunPlay').innerHTML = on ? 'Ⅱ Pozastavit' : '▶ Přehrát den'; + $('#sunPlay').setAttribute('aria-pressed', String(on)); +} +$('#sunDate').addEventListener('change', (event) => { + if (!event.target.value) return; + setSolarPlaying(false); solar.date = event.target.value; rebuildSolarDay(true); +}); +$('#sunClock').addEventListener('change', (event) => { + const utc = timeForClock(event.target.value); + if (utc === null) { event.target.value = clockText(solar.start + solar.minute * 60000); return; } + setSolarPlaying(false); solar.minute = (utc - solar.start) / 60000; updateSolar(); +}); +$('#sunTime').addEventListener('input', (event) => { setSolarPlaying(false); solar.minute = +event.target.value; updateSolar(); }); +document.querySelectorAll('[data-season]').forEach(button => button.addEventListener('click', () => { + solar.date = `${solar.date.slice(0, 4)}-${button.dataset.season}`; setSolarPlaying(false); rebuildSolarDay(true); +})); +$('#sunPlay').addEventListener('click', () => setSolarPlaying(!solar.playing)); +$('#sunSpeed').addEventListener('change', (event) => { solar.speed = +event.target.value; }); +$('#sunNow').addEventListener('click', () => { + const now = new Date(); + solar.date = `${localParts(now.getTime()).year}-${String(localParts(now.getTime()).month).padStart(2, '0')}-${String(localParts(now.getTime()).day).padStart(2, '0')}`; + rebuildSolarDay(false); + solar.minute = (now.getTime() - solar.start) / 60000; + updateSolar(); +}); +$('#sunShadows').addEventListener('change', (event) => { solar.shadows = event.target.checked; updateSolar(); }); +$('#sunPath').addEventListener('change', (event) => { solar.path = event.target.checked; }); +function readSiteNumbers() { + const lat = +$('#sunLat').value, lon = +$('#sunLon').value, bearing = +$('#siteBearing').value; + if (!Number.isFinite(lat) || !Number.isFinite(lon) || !Number.isFinite(bearing) || !$('#sunLat').checkValidity() || !$('#sunLon').checkValidity() || !$('#siteBearing').checkValidity()) { + toast('Zadej hodnoty v povoleném rozsahu.'); + $('#sunLat').value = site.latitude; $('#sunLon').value = site.longitude; $('#siteBearing').value = site.bearing; + return; + } + site.latitude = lat; site.longitude = lon; site.bearing = bearing; + modelRoot.rotation.y = bearing * RAD; + rebuildSolarDay(true); +} +['sunLat', 'sunLon', 'siteBearing'].forEach(id => $(`#${id}`).addEventListener('change', readSiteNumbers)); +$('#sunResetSite').addEventListener('click', () => { + site.latitude = 49.7536; site.longitude = 13.5394; site.bearing = 0; + $('#sunLat').value = site.latitude; $('#sunLon').value = site.longitude; $('#siteBearing').value = site.bearing; + modelRoot.rotation.y = 0; + rebuildSolarDay(true); +}); +$('#compassGroup').insertAdjacentHTML('beforeend', ''); + +const sunGraph = $('#sunGraph'); +function scrubGraph(event) { + const rect = sunGraph.getBoundingClientRect(); + solar.minute = clamp(((event.clientX - rect.left) / rect.width * 304 - 8) / 288 * solar.length, 0, solar.length - 1); + setSolarPlaying(false); updateSolar(); +} +sunGraph.addEventListener('pointerdown', event => { sunGraph.setPointerCapture(event.pointerId); scrubGraph(event); }); +sunGraph.addEventListener('pointermove', event => { if (sunGraph.hasPointerCapture(event.pointerId)) scrubGraph(event); }); +sunGraph.addEventListener('pointerup', event => { if (sunGraph.hasPointerCapture(event.pointerId)) sunGraph.releasePointerCapture(event.pointerId); }); + +async function saveSunImage() { + setSolarPlaying(false); updateSolar(); + controls.update(); + drawSunPath(); + renderer.render(scene, camera); + const source = renderer.domElement; + const out = document.createElement('canvas'); + const [viewWidth] = viewportSize(); + const ratio = source.width / viewWidth; + const footer = Math.round(66 * ratio); + out.width = source.width; out.height = source.height + footer; + const ctx = out.getContext('2d'); + ctx.fillStyle = '#f8f9f3'; ctx.fillRect(0, 0, out.width, out.height); + ctx.drawImage(source, 0, 0); + const scale = out.width / viewWidth; + if (solar.path && $('#sunOverlay').innerHTML) { + const svgText = `${$('#sunOverlay').innerHTML}`; + const url = URL.createObjectURL(new Blob([svgText], { type: 'image/svg+xml' })); + const overlay = new Image(); + await new Promise((resolve, reject) => { overlay.onload = resolve; overlay.onerror = reject; overlay.src = url; }); + ctx.drawImage(overlay, 0, 0, source.width, source.height); + URL.revokeObjectURL(url); + } + ctx.fillStyle = '#2d3c31'; ctx.font = `${12 * scale}px Arial`; + ctx.fillText(`Klabava · ${czDate(solar.date)} · ${clockText(solar.start + solar.minute * 60000)} · ${zoneText(solar.start + solar.minute * 60000)}`, 24 * scale, source.height + 28 * scale); + ctx.fillStyle = '#7b8372'; ctx.font = `${9 * scale}px Arial`; + ctx.fillText('Orientační studie oslunění · souřadnice obce Klabava', 24 * scale, source.height + 46 * scale); + out.toBlob(blob => { if (blob) saveBlob(blob, 'klabava-slunce.png'); }); +} +$('#sunSavePng').addEventListener('click', saveSunImage); +$('#savePng').addEventListener('click', saveSunImage); + +$('#saveGlb').addEventListener('click', () => { + const exporter = new GLTFExporter(); + exporter.parse(modelRoot, (result) => { + saveBlob(new Blob([result], { type: 'model/gltf-binary' }), 'klabava-pracovni-model.glb'); + toast('Model Klabava GLB je připravený ke stažení.'); + }, (error) => { console.error(error); toast('Model se nepodařilo exportovat.'); }, { binary: true, onlyVisible: true, trs: false }); +}); + +function drawSunPath() { + const svg = $('#sunOverlay'); + const [width, height] = viewportSize(); + svg.setAttribute('viewBox', `0 0 ${width} ${height}`); + const dot = $('#compassSun'); + const azimuth = solar.position?.azimuth ?? 0; + dot.setAttribute('cx', 42 + 26 * Math.sin(azimuth * RAD)); + dot.setAttribute('cy', 42 - 26 * Math.cos(azimuth * RAD)); + dot.style.display = solar.position?.elevation > 0 ? 'block' : 'none'; + if (!solar.path) { svg.innerHTML = ''; return; } + modelRoot.updateMatrixWorld(true); + const center = new THREE.Vector3(0, 1.2, houseGroup.position.z + 1).applyMatrix4(modelRoot.matrixWorld); + const pointAt = (az, alt, distance = 15.5) => new THREE.Vector3( + center.x + Math.sin(az * RAD) * Math.cos(alt * RAD) * distance, + center.y + Math.sin(alt * RAD) * distance, + center.z + Math.cos(az * RAD) * Math.cos(alt * RAD) * distance + ); + let path = '', started = false; + for (const sample of solar.samples) { + if (sample.elevation < 0) { started = false; continue; } + const v = pointAt(sample.azimuth, sample.elevation).project(camera); + if (v.z < -1 || v.z > 1) { started = false; continue; } + const x = (v.x + 1) * width / 2, y = (1 - v.y) * height / 2; + path += `${started ? 'L' : 'M'}${x.toFixed(1)},${y.toFixed(1)} `; + started = true; + } + let out = ``; + for (const hour of [6, 9, 12, 15, 18, 21]) { + const utc = timeForClock(`${String(hour).padStart(2, '0')}:00`); + if (utc === null) continue; + const pos = solarPosition(utc, site.latitude, site.longitude); + if (pos.elevation < 0) continue; + const v = pointAt(pos.azimuth, pos.elevation).project(camera); + if (v.z < -1 || v.z > 1) continue; + const x = (v.x + 1) * width / 2, y = (1 - v.y) * height / 2; + out += `${hour}:00`; + } + if (solar.position?.elevation >= 0) { + const v = pointAt(solar.position.azimuth, solar.position.elevation).project(camera); + if (v.z >= -1 && v.z <= 1) { + const x = (v.x + 1) * width / 2, y = (1 - v.y) * height / 2; + out += ``; + } + } + svg.innerHTML = out; +} + +function animate(now = 0) { + requestAnimationFrame(animate); + const dt = Math.min(0.1, Math.max(0, (now - (animate.last || now)) / 1000)); + animate.last = now; + solarFrame(dt); + controls.update(); + const cameraHeading = Math.atan2(camera.position.x - controls.target.x, camera.position.z - controls.target.z) / RAD; + $('#compassGroup').setAttribute('transform', `rotate(${cameraHeading},42,42)`); + renderer.render(scene, camera); + drawSunPath(); +} + +window.addEventListener('keydown', event => { + if (event.key === 'Escape') { toggleInfo(false); toggleSun(false); } + if (['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON', 'SUMMARY'].includes(event.target.tagName)) return; + if (event.code === 'Space') { event.preventDefault(); setSolarPlaying(!solar.playing); } + if (event.key.toLowerCase() === 's') toggleSun(); + if (event.key === '1') setPreset('entry'); + if (event.key === '2') setPreset('garden'); + if (event.key === '3') setPreset('top'); +}); +document.addEventListener('visibilitychange', () => { if (document.hidden) setSolarPlaying(false); }); + +window.SUN_SIM = { + position: solarPosition, + getState: () => ({ date: solar.date, time: clockText(solar.start + solar.minute * 60000), utc: new Date(solar.start + solar.minute * 60000).toISOString(), lat: site.latitude, lon: site.longitude, bearing: site.bearing, azimuth: solar.position.azimuth, elevation: solar.position.elevation, rise: clockText(solar.rise), set: clockText(solar.set), noon: clockText(solar.noon), shadows: solar.shadows, playing: solar.playing }), + setDate: date => { solar.date = date; rebuildSolarDay(true); }, + setTime: time => { solar.minute = (timeForClock(time) - solar.start) / 60000; updateSolar(); }, + pause: () => setSolarPlaying(false) +}; + +try { + rebuildSolarDay(false); + $('#sunLat').value = site.latitude; $('#sunLon').value = site.longitude; $('#siteBearing').value = site.bearing; + toggleSun(false); toggleInfo(false); + loading.classList.add('done'); + setTimeout(() => { loading.style.display = 'none'; }, 300); + requestAnimationFrame(animate); +} catch (error) { + console.error(error); + loading.classList.add('error'); + loading.innerHTML = `3D model se nepodařilo načíst.
${error.message}
Spusť web přes místní server a zkontroluj připojení k CDN Three.js.`; + $('#webStatus').textContent = '3D není dostupné'; +} diff --git a/klabava/js/solar.js b/klabava/js/solar.js new file mode 100644 index 0000000..cd8bd7e --- /dev/null +++ b/klabava/js/solar.js @@ -0,0 +1,23 @@ +// Pure function retained from the supplied Meeus / NOAA implementation. +// Arguments: UTC Unix milliseconds, degrees latitude, degrees longitude. +const RAD=Math.PI/180,DAY_MS=86400000; +const mod=(x,n)=>((x%n)+n)%n,clamp=(x,a,b)=>Math.max(a,Math.min(b,x)); +export function solarPosition(ms, lat, lon) { + const jd = ms / DAY_MS + 2440587.5, t = (jd - 2451545) / 36525; + const L = mod(280.46646 + t * (36000.76983 + t * .0003032), 360) * RAD; + const M = (357.52911 + t * (35999.05029 - .0001537 * t)) * RAD; + const e = .016708634 - t * (.000042037 + .0000001267 * t); + const C = Math.sin(M) * (1.914602 - t * (.004817 + .000014 * t)) + Math.sin(2 * M) * (.019993 - .000101 * t) + Math.sin(3 * M) * .000289; + const omega = (125.04 - 1934.136 * t) * RAD; + const lambda = L + (C - .00569 - .00478 * Math.sin(omega)) * RAD; + const mean = 23 + (26 + (21.448 - t * (46.815 + t * (.00059 - t * .001813))) / 60) / 60; + const obliq = (mean + .00256 * Math.cos(omega)) * RAD; + const dec = Math.asin(Math.sin(obliq) * Math.sin(lambda)); + const y = Math.tan(obliq / 2) ** 2; + const eq = 4 / RAD * (y * Math.sin(2 * L) - 2 * e * Math.sin(M) + 4 * e * y * Math.sin(M) * Math.cos(2 * L) - .5 * y * y * Math.sin(4 * L) - 1.25 * e * e * Math.sin(2 * M)); + const solarMinutes = mod(mod(ms, DAY_MS) / 60000 + eq + 4 * lon, 1440); + const H = (solarMinutes / 4 - 180) * RAD, phi = lat * RAD; + const elevation = Math.asin(clamp(Math.sin(phi) * Math.sin(dec) + Math.cos(phi) * Math.cos(dec) * Math.cos(H), -1, 1)) / RAD; + const azimuth = mod(Math.atan2(Math.sin(H), Math.cos(H) * Math.sin(phi) - Math.tan(dec) * Math.cos(phi)) / RAD + 180, 360); + return { elevation, azimuth, declination: dec / RAD, equationOfTime: eq }; + } diff --git a/klabava/references/KLABAVA_66_17.pdf b/klabava/references/KLABAVA_66_17.pdf new file mode 100644 index 0000000..d8407c2 Binary files /dev/null and b/klabava/references/KLABAVA_66_17.pdf differ diff --git a/klabava/references/Pohled JV a JZ.pdf b/klabava/references/Pohled JV a JZ.pdf new file mode 100644 index 0000000..18c1aea Binary files /dev/null and b/klabava/references/Pohled JV a JZ.pdf differ diff --git a/klabava/references/Pohled SZ a SV.pdf b/klabava/references/Pohled SZ a SV.pdf new file mode 100644 index 0000000..fde03ca Binary files /dev/null and b/klabava/references/Pohled SZ a SV.pdf differ diff --git a/klabava/references/Pudorys prizemi (1).pdf b/klabava/references/Pudorys prizemi (1).pdf new file mode 100644 index 0000000..58404a5 Binary files /dev/null and b/klabava/references/Pudorys prizemi (1).pdf differ diff --git a/klabava/references/Rez.pdf b/klabava/references/Rez.pdf new file mode 100644 index 0000000..f7021c0 Binary files /dev/null and b/klabava/references/Rez.pdf differ diff --git a/klabava/references/Situace.pdf b/klabava/references/Situace.pdf new file mode 100644 index 0000000..ae06891 Binary files /dev/null and b/klabava/references/Situace.pdf differ diff --git a/locenice.zip b/locenice.zip new file mode 100644 index 0000000..39cd545 Binary files /dev/null and b/locenice.zip differ