Compare commits

2 Commits
Author SHA1 Message Date
fencl 21cdd94c2c Merge pull request 'add phpver' (#2) from main into deployment
Build Docker Image / build (push) Successful in 1m36s
Reviewed-on: #2
2025-01-20 14:21:26 +00:00
fencl 1f7e9c19bc Merge pull request 'upgrade apache version' (#1) from main into deployment
Build Docker Image / build (push) Successful in 1m37s
Reviewed-on: #1
2025-01-20 13:52:29 +00:00
121 changed files with 149 additions and 103424 deletions
Vendored
BIN
View File
Binary file not shown.
-31
View File
@@ -1,31 +0,0 @@
kind: pipeline
type: docker
name: default
trigger:
branch:
- main
event:
- push
steps:
- name: build-and-push-to-gitea-registry
image: plugins/docker:20.16
settings:
registry: git.internet-master.cz
repo: git.internet-master.cz/fencl/martinfencl.eu
context: .
dockerfile: Dockerfile
tags:
- latest
username: fencl
password:
from_secret: REGISTRY_PASSWORD
- name: call-portainer-webhook
image: curlimages/curl
environment:
PORTAINER_WEBHOOK:
from_secret: PORTAINER_WEBHOOK
commands:
- curl -fsS -X POST "$PORTAINER_WEBHOOK"
+106 -120
View File
@@ -1,207 +1,193 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<meta charset=utf-8 /> <meta charset=utf-8 />
<title> Martin Fencl's CV | CV</title> <title> Martin Fencl's CV | CV</title>
<link href="media/kjhealy-screen.css" type="text/css" rel="stylesheet" media="screen"> <link href="media/kjhealy-screen.css" type="text/css" rel="stylesheet" media="screen">
<link href="media/kjhealy-print.css" type="text/css" rel="stylesheet" media="print"> <link href="media/kjhealy-print.css" type="text/css" rel="stylesheet" media="print">
</head> </head>
<body> <body>
<div id="main"> <div id="main">
<div id="content"> <div id="content">
<h1 id="martin-fencl">Martin Fencl</h1> <h1 id="martin-fencl">Martin Fencl</h1>
<p>Software test developer, Python engineer, HW tester.</p> <p>Software test developer, Python engineer, HW tester.</p>
<p>&nbsp;</p> <p>&nbsp;</p>
<p></p> <p></p>
<div id="webaddress"> <div id="webaddress">
<a href="martin@martinfencl.eu">martin@martinfencl.eu</a> <a href="martin@martinfencl.eu">martin@martinfencl.eu</a>
| <a href="https://www.linkedin.com/in/martin-fencl-6a9795189/">My linkedin page</a> | <a href="https://www.linkedin.com/in/martin-fencl-6a9795189/">My linkedin page</a>
</div> </div>
<div style="position:absolute;top: 15px; left:960px;"> <div style="position:absolute;top: 15px; left:960px;">
<p align="center"> <p align="center">
<img <img src="https://media-exp1.licdn.com/dms/image/C4D03AQHf42s9PdItiQ/profile-displayphoto-shrink_800_800/0/1649094514688?e=2147483647&amp;v=beta&amp;t=dzRGhT5vQ_64piZjueEWZvFbEpyOczqQ6bkrHRQvDWs" width="200" height="200" />
src="https://media.licdn.com/dms/image/v2/C4D03AQHf42s9PdItiQ/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1649094515161?e=1793232000&v=beta&t=ml6vURyZO6DQWUznlYFdbNokfGww5YA6-Qf41oxSiN4"
width="200" height="200" />
</p> </p>
</div> </div>
<p><br /></p> <p><br /></p>
<p> <p>
</p> </p>
<p><br /></p> <p><br /></p>
<p> <p>
</p> </p>
<p><br /></p> <p><br /></p>
<h2 id="currently">Currently</h2> <h2 id="currently">Currently</h2>
<p><strong>I work as a freelancer and I’m searching for interesting projects that I would enjoy.</strong></p> <p><strong>I work as a freelancer and I’m searching for interesting projects that I would enjoy.</strong></p>
<h3 id="interest-in">Interest in</h3> <h3 id="interest-in">Interest in</h3>
<ul> <ul>
<li>Robotics <br /> <li>Robotics <br />
– autonomous projects <br /> – autonomous projects <br />
– DYI projects (modeling drones, aircraft …) <br /> – DYI projects (modeling drones, aircraft …) <br />
– automation (home security system, DYI projects) <br /></li> – automation (home security system, DYI projects) <br /></li>
<li>Linux-based systems (preferring debian)</li> <li>Linux-based systems (preferring debian)</li>
<li>Dockerizing <br /> <li>Dockerizing <br />
– docker, docker-compose <br /> – docker, docker-compose <br />
– kubernetes <br /></li> – kubernetes <br /></li>
<li>Cryptography (YubiKey) <br /> <li>Cryptography (YubiKey) <br />
– OATH | HOTP <br /> – OATH | HOTP <br />
– OATH | TOTP <br /> – OATH | TOTP <br />
– GPG | PGP <br /> – GPG | PGP <br />
– FIDO U2F, FIDO2 (WebAuthn)<br /></li> – FIDO U2F, FIDO2 (WebAuthn)<br /></li>
</ul> </ul>
<h2 id="education">Education</h2> <h2 id="education">Education</h2>
<p><code class="language-plaintext highlighter-rouge">2008-2012</code> <p><code class="language-plaintext highlighter-rouge">2008-2012</code>
<strong>Completion of the study program in the field of Electronics Mechanics</strong> <strong>Completion of the study program in the field of Electronics Mechanics</strong></p>
</p> <ul>
<ul>
<li>High School of Electrical Engineering, Hluboká nad Vltavou (Czechia)</li> <li>High School of Electrical Engineering, Hluboká nad Vltavou (Czechia)</li>
<li>Level EQF 4</li> <li>Level EQF 4</li>
</ul> </ul>
<p><code class="language-plaintext highlighter-rouge">2013-2018</code> <p><code class="language-plaintext highlighter-rouge">2013-2018</code>
<strong>Completion of the study program in the field of Applied Electrical Engineering</strong> <strong>Completion of the study program in the field of Applied Electrical Engineering</strong></p>
</p> <ul>
<ul>
<li>University of West Bohemia, Faculty of Electrical Engineering, Pilsen (Czechia)</li> <li>University of West Bohemia, Faculty of Electrical Engineering, Pilsen (Czechia)</li>
<li>Level EQF 6</li> <li>Level EQF 6</li>
</ul> </ul>
<p><code class="language-plaintext highlighter-rouge">2018-2020</code> <p><code class="language-plaintext highlighter-rouge">2018-2020</code>
<strong>Completion of the study program in the field of commercial electrical engineering</strong> <strong>Completion of the study program in the field of commercial electrical engineering</strong></p>
</p> <ul>
<ul>
<li>University of West Bohemia, Faculty of Electrical Engineering, Pilsen (Czechia)</li> <li>University of West Bohemia, Faculty of Electrical Engineering, Pilsen (Czechia)</li>
<li>Level EQF 7</li> <li>Level EQF 7</li>
</ul> </ul>
<h2 id="work-experience">Work Experience</h2> <h2 id="work-experience">Work Experience</h2>
<p><code class="language-plaintext highlighter-rouge">2013-2013</code> <p><code class="language-plaintext highlighter-rouge">2013-2013</code>
<strong>IT service worker / support</strong> <strong>IT service worker / support</strong></p>
</p> <ul>
<ul>
<li><em>Karel Peisker</em>, Budweis (Czechia)</li> <li><em>Karel Peisker</em>, Budweis (Czechia)</li>
<li>Network service, user interface security</li> <li>Network service, user interface security</li>
</ul> </ul>
<p><code class="language-plaintext highlighter-rouge">2016-2017</code> <p><code class="language-plaintext highlighter-rouge">2016-2017</code>
<strong>Development programmer - development department (internship)</strong> <strong>Development programmer - development department (internship)</strong></p>
</p> <ul>
<ul>
<li><em>Robert Bosch, spol. s.r.o.</em>, Budweis (Czechia)</li> <li><em>Robert Bosch, spol. s.r.o.</em>, Budweis (Czechia)</li>
<li>Programming of laboratory instruments</li> <li>Programming of laboratory instruments</li>
<li>Development department assistance</li> <li>Development department assistance</li>
</ul> </ul>
<p><em>technologies / tools used:</em> <br /> <p><em>technologies / tools used:</em> <br />
– LabView <br /> – LabView <br />
– Python <br /></p> – Python <br /></p>
<p><code class="language-plaintext highlighter-rouge">2017-2020</code> <p><code class="language-plaintext highlighter-rouge">2017-2020</code>
<strong>Development programmer - technology department (internship)</strong> <strong>Development programmer - technology department (internship)</strong></p>
</p> <ul>
<ul>
<li><em>Robert Bosch, spol. s.r.o.</em>, Budweis (Czechia)</li> <li><em>Robert Bosch, spol. s.r.o.</em>, Budweis (Czechia)</li>
<li>Programming of laboratory measuring devices</li> <li>Programming of laboratory measuring devices</li>
<li>Testing of laboratory equipment, proposal for improvement and calibration</li> <li>Testing of laboratory equipment, proposal for improvement and calibration</li>
<li>Analysis and work with data</li> <li>Analysis and work with data</li>
</ul> </ul>
<p><em>technologies / tools used:</em> <br /> <p><em>technologies / tools used:</em> <br />
– Git <br /> – Git <br />
– LabVIEW <br /> – LabVIEW <br />
– Python <br /> – Python <br />
– Visual Basic for Applications (VBA) <br /> – Visual Basic for Applications (VBA) <br />
– Structured Text (ST) <br /> – Structured Text (ST) <br />
– 3D printing (SLA, DMLS) <br /> – 3D printing (SLA, DMLS) <br />
– Measuring and analysis technology <br /></p> – Measuring and analysis technology <br /></p>
<p><code class="language-plaintext highlighter-rouge">2017-2020</code> <p><code class="language-plaintext highlighter-rouge">2017-2020</code>
<strong>Software test engineer – DITET5 (Schweinfurth team for EMF projects)</strong> <strong>Software test engineer – DITET5 (Schweinfurth team for EMF projects)</strong></p>
</p> <ul>
<ul>
<li><em>ZF Engineering Pilsen, spol. s.r.o.</em>, Pilsen (Czechia)</li> <li><em>ZF Engineering Pilsen, spol. s.r.o.</em>, Pilsen (Czechia)</li>
<li>Testing of hybrid automotive units (8P4G project)</li> <li>Testing of hybrid automotive units (8P4G project)</li>
<li>Creating automation tests, regression tests</li> <li>Creating automation tests, regression tests</li>
<li>Manual testing of internal software (EMF)</li> <li>Manual testing of internal software (EMF)</li>
<li>Problem analysis, consulting of status SW</li> <li>Problem analysis, consulting of status SW</li>
</ul> </ul>
<p><em>technologies / tools used:</em> <br /> <p><em>technologies / tools used:</em> <br />
– Dspace SCALEXIO (rapid control prototyping (RCP) applications) <br /> – Dspace SCALEXIO (rapid control prototyping (RCP) applications) <br />
– Exam (test extension/orchestrator) <br /> – Exam (test extension/orchestrator) <br />
– Python <br /></p> – Python <br /></p>
<p><code class="language-plaintext highlighter-rouge">2020-</code> <p><code class="language-plaintext highlighter-rouge">2020-</code>
<strong>Software test developer | Python developer </strong> <strong>Software test developer | Python developer </strong></p>
</p> <ul>
<ul>
<li><em>W.A.G. payment solutions, a.s., Princip, a.s.</em>, Prague (Czechia)</li> <li><em>W.A.G. payment solutions, a.s., Princip, a.s.</em>, Prague (Czechia)</li>
<li>Creating, develop and maintain software unit project for testing (SwMU)</li> <li>Creating, develop and maintain software unit project for testing (SwMU)</li>
<li>Modified existing software to detect and correct errors, improve performance, and upgrade interfaces <li>Modified existing software to detect and correct errors, improve performance, and upgrade interfaces (cooperating with sw-team)</li>
(cooperating with sw-team)</li>
<li>Prepared reports on specifications and activities for each project.</li> <li>Prepared reports on specifications and activities for each project.</li>
<li>Designed web applications (interface for support SwMU project)</li> <li>Designed web applications (interface for support SwMU project)</li>
<li>Creating DevOps pipelines, testing applications (Python)</li> <li>Creating DevOps pipelines, testing applications (Python)</li>
<li>Development of resources for deploying production software to Azure cloud (Helm, Azure DevOps)</li> <li>Development of resources for deploying production software to Azure cloud (Helm, Azure DevOps)</li>
</ul> </ul>
<p><em>technologies / tools used:</em> <br /> <p><em>technologies / tools used:</em> <br />
– Apache Kafka (event streaming platform) <br /> – Apache Kafka (event streaming platform) <br />
– MySQL <br /> – MySQL <br />
– Python <br /> – Python <br />
– Git <br /> – Git <br />
– Flask, Rest API (Flask-RESTful) <br /> – Flask, Rest API (Flask-RESTful) <br />
– Django <br /> – Django <br />
– Docker, Docker-compose <br /> – Docker, Docker-compose <br />
– Helm, Skaffold <br /> – Helm, Skaffold <br />
– Azure DevOps <br /> – Azure DevOps <br />
– Kubernetes (and local minikube) <br /></p> – Kubernetes (and local minikube) <br /></p>
<p><code class="language-plaintext highlighter-rouge">2022-2024</code> <p><code class="language-plaintext highlighter-rouge">2022-2024</code>
<strong>System engineer | Software test developer</strong> <strong>System engineer | Software test developer</strong></p>
</p> <ul>
<ul>
<li><em>Deutsche Telekom AG</em></li> <li><em>Deutsche Telekom AG</em></li>
<li>Developing an application for controlling and testing mobile phones (MDA project, Python)</li> <li>Developing an application for controlling and testing mobile phones (MDA project, Python)</li>
<li>Participation in the development of the analysis program (FST project)</li> <li>Participation in the development of the analysis program (FST project)</li>
<li>Creating test applications in the Cypress framework</li> <li>Creating test applications in the Cypress framework</li>
</ul> </ul>
<p><em>technologies / tools used:</em> <br /> <p><em>technologies / tools used:</em> <br />
– Python, JS, TS <br /> – Python, JS, TS <br />
– Cypress framework <br /> – Cypress framework <br />
– Git <br /> – Git <br />
– Docker, Docker-compose <br /> – Docker, Docker-compose <br />
– Azure DevOps <br /> – Azure DevOps <br />
– Behave (use Gherkin Syntax with Python) <br /> – Behave (use Gherkin Syntax with Python) <br />
– pyqt6 (cross-platform Python GUI) <br /></p> – pyqt6 (cross-platform Python GUI) <br /></p>
<div style="position:static;bottom: 0px; left:0px;"> <div style="position:static;bottom: 0px; left:0px;">
<button onclick="history.back()">Go Back</button> <button onclick="history.back()">Go Back</button>
</div> </div>
<!-- ### Footer <!-- ### Footer
Last updated: May 2013 --> Last updated: May 2013 -->
</div> </div>
</div> </div>
</body> </body>
</html> </html>
-1
View File
@@ -29,7 +29,6 @@ class Global_conf {
array("FAwesome","linkedinBtn", "https://www.linkedin.com/in/martin-fencl-6a9795189"), array("FAwesome","linkedinBtn", "https://www.linkedin.com/in/martin-fencl-6a9795189"),
//array("FAwesome","foursquareBtn", "https://foursquare.com/zacekjakub"), //array("FAwesome","foursquareBtn", "https://foursquare.com/zacekjakub"),
array("FAwesome","mailBtn", "mailto:martin@martinfencl.eu"), array("FAwesome","mailBtn", "mailto:martin@martinfencl.eu"),
array("FAwesome","dashboardBtn", "https://dashboard.martinfencl.eu/"),
//array("FAwesome", "telegramBtn", "https://t.me/zacekjakub"), //array("FAwesome", "telegramBtn", "https://t.me/zacekjakub"),
//array("Blog","blog.png", "https://blog.zacekj.cz"), //array("Blog","blog.png", "https://blog.zacekj.cz"),
//array("FAwesome","gravBtn", "https://blog.zacekj.cz"), //array("FAwesome","gravBtn", "https://blog.zacekj.cz"),
-19
View File
@@ -460,25 +460,6 @@ jakub
background: #333; background: #333;
} }
/* homarr dashboard button class*/
.dashboardBtn {
background: black;
-webkit-transition: all 0.3s linear;
-moz-transition: all 0.3s ease;
-ms-transition: all 0.3s ease;
-o-transition: all 0.3s ease;
transition: all 0.3s ease;
}
.dashboardBtn:before {
font-family: "FontAwesome";
content: "\f0e4"; /* add dashboard icon */
}
.dashboardBtn:hover {
background: #333;
}
.GlobalBtn { .GlobalBtn {
display: inline-block; display: inline-block;
-67
View File
@@ -1,67 +0,0 @@
# 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 přibalené Three.js 0.180.0 ve složce `vendor/`. Spusťte místní server z této složky:
```sh
python3 -m http.server 8000
```
Potom otevřete <http://localhost:8000>. Prohlížeč potřebuje WebGL. Knihovny se načítají lokálně, takže připojení k CDN není potřeba. Samotný soubor `index.html` neotvírejte přes `file://`; použijte místní server nebo hosting. Při nasazení na hosting nahrajte celý obsah složky `klabava/`; soubory `index.html`, `css/`, `js/`, `data/`, `vendor/` 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 střed parcely 66/17 (49,7520° N, 13,5440° E) podle souřadnicové sítě S-JTSK ve výškopisu. Severka situace ukazuje sever sítě S-JTSK, který je tady o 8,5° západněji než zeměpisný sever; uliční strana domu proto míří 1,5° východně od zeměpisného severu. 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 úrovně hřebenů +5,275 a +5,832 m. Řez uvádí sklon střechy 23°, přesah 0,6 m, podbití +2,766 a okap +3,046 m. Vyšší valbová střecha kryje kuchyňské křídlo, krytý vstup a krytou terasu (13,05 × 11,925 m, hřeben +5,832 m), nižší křídlo s ložnicemi (9,3 m, hřeben +5,275 m); na uliční straně mají obě střechy společnou rovinu. Situace uvádí parcelu 66/17 o výměře 1 037 m² a 206,54 m² včetně krytého vstupu (1,5 × 6,525 m) a terasy (3,75 × 4,8 m). Dvě parkovací stání 6,625 × 5,0 m jsou mezi komunikací a krytým vstupem před kuchyňským křídlem, tedy vpravo při pohledu z ulice. Dům je 5,0 m od komunikace, 2,0 m od východní a asi 3,3 m od západní hranice. Bodové výšky ve výškopisu klesají přibližně z 349,2 m n. m. u komunikace na 347,1 m n. m. u zadní hranice; východní strana je asi o 0,4 m výš.
Obrys domu, polohy a rozměry otvorů, střechy, komín a sloupky terasy a vstupu vycházejí z kót a vektorů půdorysu, pohledů a řezu. Terén je rovina proložená 27 bodovými výškami parcely (střední odchylka 4 cm); podlaha ±0,000 je 0,30 m nad terénem uprostřed domu. Rámy otvorů, nosná konstrukce terasy a vstupu, auta a materiály slouží k prostorové orientaci. Hranice parcely je převzatá graficky ze situačního PDF a výškopisu, nikoli z vytyčovacích souřadnic. 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/` jako podklad modelu; panel interiéru odkazuje na půdorys a řez. 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.
## Nový interiér
Klikněte na **Prohlédnout interiér** (nebo klávesu I). Ovládání navazuje na Ločenici, dispozice je nově sestavená podle Klabavy.
- **Půdorys:** ortografický pohled shora s plochami místností.
- **3D bez střechy:** otočný pohled se skutečným řezem stěn ve výšce 1,12 m.
- **Procházet:** stěny a rovný podhled mají plnou výšku 2,60 m. Klikněte do modelu a použijte W/A/S/D, šipky nebo tlačítka na obrazovce. Tažením se rozhlížíte, Shift zrychlí chůzi. Kolečko mění zorný úhel.
- Mapa, výběr místnosti a připravené pohledy přenesou kameru dovnitř. Průchody jsou volné; zárubně jsou zobrazené bez dveřních křídel.
- Vybavení, popisky a přisvětlení lze přepnout. Barva mění vnitřní příčky; obvodový plášť a strop používají původní materiály.
- Tlačítko ☷ skryje panel, ⌖ zarámuje dům nebo obnoví pohled. **Exteriér / Esc** vrací původní kameru, vrstvy okolí a otevřený panel. Simulace slunce se v interiéru pozastaví a při návratu obnoví původní stav přehrávání.
- **PNG** ukládá aktuální pohled včetně popisků a stručné poznámky. **GLB** obsahuje právě viditelné vrstvy: v řezu skutečně zkrácené stěny, při procházení plnou výšku. Z exteriéru se exportuje celý dům včetně interiéru a viditelných vrstev pozemku. Světla a ovládání webu nejsou součástí GLB.
Dispozice obsahuje 11 místností o součtu ploch z výkresu 153,74 m²: obývací pokoj s kuchyní, místnost bez určení, šatnu, zádveří, chodbu, technickou místnost, společnou koupelnu, dva pokoje, ložnici se šatnou a koupelnu u ložnice. Názvy jsou odvozené ze zakresleného vybavení. Místnost 7,05 m² je ponechaná prázdná a přístupná přes šatnu 6,15 m².
Poloha a tloušťky příček, dveřních otvorů a uspořádání vybavení vycházejí z půdorysu. Světlá výška 2,60 m vychází z řezu; výška vnitřních otvorů 2,10 m je vizualizační předpoklad. Kuchyň do U s poloostrovem, sedačka, jídelní stůl, postele, skříně, sanita a technické vybavení jsou zjednodušené. Rozměry výrobků, jejich přesné provedení a materiály nejsou závazně navržené. Podhled zůstává rovný, podle dokumentace Klabavy.
Přisvětlení slouží ke čtení interiéru. Pohled bez střechy ani přisvětlený interiér nejsou výpočtem denního osvětlení.
### Nové soubory
- `js/interior-model.js`: místnosti, podlahy, příčky, dveřní otvory, strop a kolizní hranice.
- `js/interior-furniture.js`: vybavení a jeho kolizní hranice.
- `js/interior-view.js`: přepínání režimů, procházení, mapa, popisky a PNG.
- `vendor/three/`: lokální Three.js, OrbitControls, GLTFExporter a licence MIT.
- `tests/interior-checks.mjs`: geometrická kontrola a průchodnost ze zádveří do všech místností s vybavením i bez něj. Spuštění: `node tests/interior-checks.mjs` (Node.js 22+).
Kontrola: všechny místnosti jsou dostupné z chodby/zádveří, součet popsaných ploch odpovídá výkresu, geometrie má konečné souřadnice. V prohlížeči ověřeny režimy půdorys, 3D a chůze, mobilní rozložení, návrat do exteriéru a export PNG/GLB.
-8
View File
@@ -1,8 +0,0 @@
Three.js r180 / 0.180.0 is bundled in vendor/three/.
The MIT license is included at vendor/three/LICENSE.
Source files were obtained from the version-pinned official npm distribution on jsDelivr.
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.
-1728
View File
File diff suppressed because it is too large Load Diff
-68
View File
@@ -1,68 +0,0 @@
: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}
.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}}
/* Interior shares the Ločenice controls; dimensions are specific to this plan. */
body.interior-active #infoButton,body.interior-active #sunToggle{display:none}
body.interior-active .topright{right:18px}
body.interior-active #sunOverlay{display:none}
body.interior-active #interiorPanel{z-index:5;width:308px;box-sizing:border-box}
#interiorRoom{width:100%;border:1px solid #d6dfcf;border-radius:7px;padding:9px;color:#365944}
#interiorPanel .wall-paint{justify-content:space-between}
#interiorWallColor{width:42px;height:29px;border:1px solid #ccd6c6;border-radius:5px;padding:2px}
.int-source-buttons a{font-size:10px;color:#365944;padding:6px;border:1px solid #d4ddca;border-radius:6px;text-decoration:none}
#interiorMap .map-room:focus{outline:none;fill:#b5c7a5;stroke:#365944;stroke-width:2}
body.interior-active #webStatus{display:none}
body.interior-active #interiorLabels{overflow:hidden}
@media(max-width:850px){
#interiorLaunch{top:118px}
body.interior-active .interior-heading{top:17px;left:14px;max-width:calc(100% - 130px)}
body.interior-active .interior-heading .eyebrow{display:none}
body.interior-active .interior-heading h2{font-size:14px;max-width:170px;line-height:1.2}
body.interior-active #interiorPanel{left:0;right:0;top:auto;bottom:0;width:100%;height:242px;padding:15px 18px 24px;border-radius:18px 18px 0 0}
body.interior-active:not(.interior-panel-closed) #view{width:100%;height:calc(100% - 242px)}
body.interior-active.interior-panel-closed #view{width:100%;height:100%}
body.interior-active #interiorTools{left:50%;bottom:250px}
body.interior-active.interior-panel-closed #interiorTools{left:50%;bottom:17px}
body.interior-walk #walkPad{bottom:304px}
body.interior-walk.interior-panel-closed #walkPad{bottom:80px}
#interiorMap{width:145px;float:left;margin-right:14px}
.int-mode-bar button{padding:9px 8px}
}
@media(max-width:420px){
body.interior-active .interior-heading h2{font-size:12px;max-width:112px}
body.interior-active .interior-heading{gap:7px}
body.interior-active .topright{right:10px}
#interiorMap{width:130px}
.int-room-tag{font-size:8px;padding:3px 4px}
}
-24
View File
@@ -1,24 +0,0 @@
/* 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}}
-36
View File
@@ -1,36 +0,0 @@
{
"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,
"secondaryRidgeM": 5.275,
"chimneyM": 6.5,
"surveyGroundNearRoadBpv": 349.2,
"surveyGroundRearBpv": 347.1,
"latitude": 49.752,
"longitude": 13.544,
"latitudeLongitudeNote": "Střed parcely 66/17 ze souřadnicové sítě S-JTSK ve výškopisu, převedený do WGS84.",
"siteBearingDeg": 1.5,
"siteBearingNote": "Uliční strana domu míří 10,0° od severu sítě S-JTSK (severka situace); meridiánová konvergence je -8,5°, tedy 1,5° od zeměpisného severu.",
"standards": null,
"modelNote": "Vizualizační model podle PDF podkladů; vyšší valbová střecha kryje kuchyňské křídlo, terasu a krytý vstup, nižší křídlo s ložnicemi. Terén je rovina proložená bodovými výškami výškopisu.",
"interior": {
"rooms": 11,
"areaM2": 153.74,
"ceilingHeightM": 2.6,
"cutHeightM": 1.12,
"doorHeadAssumedM": 2.1,
"layoutSource": "references/Pudorys prizemi (1).pdf",
"heightSource": "references/Rez.pdf",
"furnishing": "Orientační podle rozmístění ve výkresu; materiály dočasné.",
"roomNames": "Odvozené podle zakresleného vybavení. Místnost 7,05 m² bez určení."
}
}
-151
View File
@@ -1,151 +0,0 @@
<!doctype html>
<html lang="cs">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="robots" content="noindex,nofollow">
<meta name="theme-color" content="#e7ebe3">
<title>Klabava | Rodinný dům</title>
<link rel="stylesheet" href="./css/app.css?v=20261102-interior">
<link rel="stylesheet" href="./css/web.css?v=20261102-interior">
<link rel="stylesheet" href="./css/klabava.css?v=20261102-interior">
<script type="importmap">
{"imports":{"three":"./vendor/three/build/three.module.js","three/addons/":"./vendor/three/examples/jsm/"}}
</script>
</head>
<body>
<canvas id="view" aria-label="Interaktivní trojrozměrný model rodinného domu v Klabavě"></canvas>
<svg id="measure" aria-hidden="true"></svg>
<header>
<div class="eyebrow"><span class="dot"></span>RODINNÝ DŮM · KLABAVA</div>
<h1>Klabava</h1>
<div class="sub">Rodinný dům · 3D studie a simulace slunce</div>
<div class="metrics">
<div class="metric"><strong>178,75 m²</strong><small>Zastavěná plocha</small></div>
<div class="metric"><strong>153,74 m²</strong><small>Užitná plocha</small></div>
<div class="metric"><strong>5,28 / 5,83 m</strong><small>Hřebeny střechy</small></div>
</div>
</header>
<div class="topright">
<button class="btn" id="infoButton" type="button" aria-expanded="false" title="Projekt a podklady">Projekt</button>
<button class="btn" id="sunToggle" type="button" aria-controls="sunPanel" aria-expanded="false" title="Ovládání slunce"><span class="solar-mark">☼</span><span class="toggle-word">Slunce</span></button>
<button class="btn primary" id="saveGlb" type="button">Stáhnout <span class="downloadText">model</span> GLB</button>
</div>
<aside class="panel" id="panel" aria-label="Informace o projektu">
<div class="panel-head">
<div><div class="panel-kicker">RODINNÝ DŮM · KLABAVA</div><h2>Dům a pozemek</h2></div>
<button class="panel-close" id="infoClose" type="button" aria-label="Zavřít panel">×</button>
</div>
<p>Pracovní 3D vizualizace podle dodaného půdorysu, pohledů, řezu, koordinační situace a výškopisu.</p>
<h3>Základní údaje</h3>
<div class="row"><span>Parcela</span><strong>66/17 · 1 037 m²</strong></div>
<div class="row"><span>Zastavěná plocha domu</span><strong>178,75 m²</strong></div>
<div class="row"><span>Včetně krytého vstupu a terasy</span><strong>206,54 m²</strong></div>
<div class="row"><span>Užitná plocha</span><strong>153,74 m²</strong></div>
<div class="row"><span>Rozměry obálky v půdorysu</span><strong>17,40 × 13,05 m</strong></div>
<div class="row"><span>Hřebeny / komín</span><strong>+5,275 a +5,832 / +6,500 m</strong></div>
<h3>Vzhled a vybavení</h3>
<p>Barvy fasády, krytiny, oken a další standardy zůstávají otevřené. Model používá neutrální dočasné materiály. Interiér nabízí dispozici a orientační vybavení podle půdorysu, pohled bez střechy i procházení.</p>
<details class="site-note" open>
<summary>Rozsah modelu a přesnost</summary>
<p>Půdorysná hmota, poloha a rozměry otvorů a výšky střech vycházejí z kót půdorysu, pohledů a řezu. Střechu tvoří dvě valbové střechy se sklonem 23° a přesahem 0,6 m; vyšší kryje i terasu a krytý vstup, nižší křídlo s ložnicemi. Terén je rovina proložená zaměřenými body výškopisu, přibližně 349,2 m n. m. u komunikace a 347,1 m n. m. u zadní hranice.</p>
<p>Hranice parcely je převzatá graficky z PDF situace a výškopisu, ne ze souřadnic pro vytyčení. Rámy oken a dveří, nosná konstrukce terasy a vstupu, auta a materiály jsou zjednodušené pro prostorovou orientaci. Model slouží k prezentaci a studii oslunění, nikoli jako BIM, geodetický nebo stavební podklad.</p>
</details>
<div class="status"><span class="dot"></span>Pracovní vizualizace · standardy doplníme po jejich výběru</div>
</aside>
<div id="direction"><span class="tag">AKTUÁLNÍ POHLED</span><span id="directionText">Dům / příjezd</span></div>
<div class="bottom">
<nav class="toolbar" aria-label="Ovládání modelu">
<button class="tool active" data-preset="entry" type="button">Příjezd</button>
<button class="tool" data-preset="garden" type="button">Zahrada</button>
<button class="tool" data-preset="top" type="button">Shora</button>
<span class="divider"></span>
<label class="site-switch"><input type="checkbox" id="siteTerrain" checked><span>Terén</span></label>
<label class="site-switch"><input type="checkbox" id="siteBoundary" checked><span>Parcela</span></label>
<label class="site-switch"><input type="checkbox" id="siteParking" checked><span>Parkování</span></label>
<span class="divider"></span>
<button class="tool" id="savePng" type="button">Uložit PNG</button>
<button class="tool" id="resetView" type="button">Reset</button>
</nav>
<div class="help">Tažením otáčej · kolečkem přibliž · pravým tlačítkem posuň</div>
</div>
<div class="compass" aria-label="Severka">
<svg viewBox="0 0 84 84">
<circle cx="42" cy="42" r="27" fill="none" stroke="#bec8b7" />
<g id="compassGroup"><line x1="42" y1="13" x2="42" y2="71"/><line x1="13" y1="42" x2="71" y2="42"/>
<path id="northArrow" d="M42 18 L37 32 L42 29 L47 32 Z"/><text x="42" y="8" text-anchor="middle">S</text><text x="42" y="81" text-anchor="middle">J</text><text x="3" y="45" text-anchor="middle">Z</text><text x="80" y="45" text-anchor="middle">V</text>
</g>
</svg>
</div>
<svg id="sunOverlay" aria-hidden="true"></svg>
<div id="sunStamp"><span>SLUNCE NAD DOMEM</span><br><strong id="sceneTime">21. 6. 2026 · 10:00</strong></div>
<section id="sunPanel" aria-label="Simulace slunce">
<div class="sun-panel-head"><div><div class="sun-kicker">STUDIE OSVĚTLENÍ</div><h2>Slunce a stíny</h2></div><button class="close-panel" id="sunClose" type="button" aria-label="Skrýt panel slunce">×</button></div>
<div class="sun-date-row"><label class="sun-field">Datum<input id="sunDate" type="date" min="2000-01-01" max="2100-12-31" value="2026-06-21"></label><label class="sun-field">Místní čas<input id="sunClock" type="text" inputmode="numeric" value="10:00" maxlength="5" placeholder="HH:MM" aria-label="Místní čas ve formátu HH:MM"></label></div>
<div class="sun-seasons" aria-label="Typické dny roku"><button type="button" data-season="03-20">Jaro</button><button type="button" data-season="06-21" class="active">Léto</button><button type="button" data-season="09-23">Podzim</button><button type="button" data-season="12-21">Zima</button></div>
<div class="sun-status-line"><span id="dayState">Slunce nad obzorem</span><span id="zoneLabel">SELČ · UTC+2</span></div>
<svg id="sunGraph" class="sun-graph" viewBox="0 0 304 86" role="img" aria-label="Výška slunce během dne. Kliknutím změníš čas."></svg>
<label class="sr-only" for="sunTime">Posun času</label><input class="sun-scrub" id="sunTime" type="range" min="0" max="1439" value="600" step="1" aria-valuetext="10:00">
<div class="sun-ticks"><span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>24:00</span></div>
<div class="sun-play-row"><button id="sunPlay" type="button">▶ Přehrát den</button><select id="sunSpeed" aria-label="Rychlost simulace"><option value="15">15 min / s</option><option value="30" selected>30 min / s</option><option value="60">60 min / s</option></select><button id="sunNow" type="button" title="Aktuální datum a čas v Praze">Teď</button></div>
<div id="solarMessage" role="status"></div>
<div class="sun-readouts"><div class="sun-stat">Výška nad obzorem<strong id="sunElevation">—</strong><small>Střed slunečního kotouče</small></div><div class="sun-stat">Azimut slunce<strong id="sunAzimuth">—</strong><small id="sunDirection">Od severu po směru hodin</small></div></div>
<div class="sun-events"><span>Východ <b id="sunRise">—</b></span><span>Poledne <b id="sunNoon">—</b></span><span>Západ <b id="sunSet">—</b></span></div>
<div class="sun-options"><label><input type="checkbox" id="sunShadows" checked>Vržené stíny</label><label><input type="checkbox" id="sunPath">Dráha ve 3D</label></div>
<p class="sun-hint">Posouvej čas a sleduj, kam dopadá slunce na fasády a zahradu. Sever modelu je zeměpisný sever přepočtený ze souřadnic S-JTSK.</p>
<button class="sun-export" id="sunSavePng" type="button">Uložit obrázek s datem a časem</button>
<details class="sun-advanced"><summary>Poloha a orientace domu</summary>
<div class="fields"><label class="sun-field">Zeměpisná šířka (° N)<input id="sunLat" type="number" min="-89" max="89" step="0.0001" value="49.7520"></label><label class="sun-field">Zeměpisná délka (° E)<input id="sunLon" type="number" min="-180" max="180" step="0.0001" value="13.5440"></label><label class="sun-field full-row">Natočení pozemku vůči severu (°)<input id="siteBearing" type="number" min="0" max="359.9" step="0.1" value="1.5"></label></div>
<p>Výchozí poloha je střed parcely 66/17 podle souřadnicové sítě S-JTSK ve výškopisu. Severka v situaci ukazuje sever sítě S-JTSK, který je tady o 8,5° západněji než zeměpisný sever. Uliční strana domu proto míří 1,5° východně od zeměpisného severu; kladné natočení otáčí pozemek po směru hodinových ručiček.</p>
<p>Časové pásmo zůstává <b>Europe/Prague</b>. Letní čas se přepočítává podle zvoleného data a pravidel prohlížeče.</p>
<button class="mini-btn" id="sunResetSite" type="button">Obnovit polohu Klabavy</button>
</details>
<details class="sun-advanced"><summary>Metoda výpočtu a přesnost</summary><p>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í.</p></details>
</section>
<button id="interiorLaunch" type="button" aria-controls="interiorPanel" aria-expanded="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M3 11 12 4 21 11M5 10v10h14V10M10 20v-7h5v7"/></svg>
<span>Prohlédnout interiér<small>Půdorys · 3D bez střechy · procházení</small></span>
</button>
<div class="interior-heading"><button class="btn" id="interiorExit" type="button">← Exteriér</button><div><div class="eyebrow">KLABAVA / INTERIÉR PŘÍZEMÍ</div><h2 id="interiorViewTitle">Dům bez střechy</h2></div></div>
<section id="interiorPanel" aria-label="Pohledy a nastavení interiéru">
<div class="interior-panel-title"><h3>Uvnitř domu</h3><span>11 místností · 153,74 m²</span></div>
<svg id="interiorMap" viewBox="0 0 348 270" role="group" aria-label="Mapa místností; výběrem vstoupíte dovnitř"></svg>
<p class="int-map-hint">Vyberte místnost v mapě nebo připravený pohled.</p>
<div class="int-kicker">Připravené pohledy</div>
<div class="int-views"><button type="button" data-interior-view="kitchen">Kuchyň</button><button type="button" data-interior-view="living">Obývací pokoj</button><button type="button" data-interior-view="entry">Od vstupu</button><button type="button" data-interior-view="master">Ložnice</button></div>
<div class="int-kicker">Přejít do místnosti</div>
<select id="interiorRoom" aria-label="Přejít do vybrané místnosti"><option value="">Vyberte místnost…</option></select>
<div class="int-options">
<label><input type="checkbox" id="interiorFurniture" checked>Vybavení podle půdorysu</label>
<label><input type="checkbox" id="interiorRoomLabels" checked>Názvy a plochy místností</label>
<label><input type="checkbox" id="interiorFill" checked>Přisvětlení interiéru</label>
<label class="wall-paint">Barva vnitřních příček <input id="interiorWallColor" type="color" value="#eeeae1" aria-label="Barva vnitřních příček"></label>
</div>
<p>V půdorysu a 3D pohledu je střecha skrytá a stěny zkrácené na 1,12 m. Při procházení mají stěny i rovný podhled výšku 2,60 m.</p>
<details class="int-source"><summary>Podklady a přesnost</summary>
<p>Dispozice, plochy, poloha příček a průchody vycházejí z dodaného půdorysu přízemí. Světlá výška 2,60 m je převzatá z řezu. Názvy místností jsou odvozené ze zakresleného vybavení.</p>
<p>Místnost 7,05 m² nemá ve výkresu uvedený účel a zůstává prázdná. Vstupuje se do ní přes šatnu. Ložnice 22,35 m² zahrnuje vlastní šatnovou část.</p>
<p>Nábytek, sanita, spotřebiče a materiály jsou orientační. Uspořádání kuchyně do U, lůžka, skříně a technické vybavení odpovídají schématu v půdorysu. Vnitřní dveře jsou zobrazené jako volné průchody se zárubněmi; výška otvoru 2,10 m je předpoklad.</p>
<p>Přisvětlení pomáhá čitelnosti interiéru. Pohled bez střechy ani přisvětlený interiér neslouží k posouzení skutečného denního osvětlení.</p>
<div class="int-source-buttons"><a href="./references/Pudorys%20prizemi%20(1).pdf" target="_blank" rel="noopener">Původní půdorys ↗</a><a href="./references/Rez.pdf" target="_blank" rel="noopener">Řez ↗</a></div>
</details>
<button class="int-export" id="interiorSavePng" type="button">Uložit aktuální pohled PNG</button>
<button class="int-export" id="interiorSaveGlb" type="button">Stáhnout zobrazený model GLB</button>
</section>
<div id="interiorLabels" aria-label="Místnosti v půdorysu"></div>
<div id="interiorTools"><div id="interiorHint">Tažením otáčejte · kolečkem přibližujte · výběrem místnosti vstupte dovnitř</div><nav class="int-mode-bar" aria-label="Režim interiéru"><button type="button" data-interior-mode="plan">Půdorys</button><button type="button" data-interior-mode="axon" class="active">3D bez střechy</button><button type="button" data-interior-mode="walk">Procházet</button><button id="interiorFit" type="button" class="int-nav-button" title="Zarámovat dům nebo obnovit pohled">⌖</button><button id="interiorPanelToggle" type="button" class="int-nav-button" title="Skrýt nebo zobrazit panel" aria-expanded="true" aria-controls="interiorPanel">☷</button></nav></div>
<div id="walkPad" aria-label="Ovládání chůze"><button type="button" data-walk="forward" aria-label="Jít vpřed">↑</button><button type="button" data-walk="left" aria-label="Krok vlevo">←</button><button type="button" data-walk="back" aria-label="Jít vzad">↓</button><button type="button" data-walk="right" aria-label="Krok vpravo">→</button></div>
<div id="loading" role="status"><span class="loader-dot"></span><span>Načítám 3D model…</span></div>
<div id="toast" role="status" aria-live="polite"></div>
<div id="webStatus">Pracovní model · Klabava 66/17</div>
<script type="module" src="./js/klabava.js?v=20261102-interior"></script>
</body>
</html>
-10
View File
@@ -1,10 +0,0 @@
<?php
header('Content-Type: text/html; charset=utf-8');
header('X-Content-Type-Options: nosniff');
$page = __DIR__ . '/index.html';
if (!is_file($page)) {
http_response_code(500);
echo 'Web Klabava není kompletní.';
exit;
}
readfile($page);
-145
View File
@@ -1,145 +0,0 @@
// Schematic furnishing positions follow the supplied floor plan. Finishes are provisional.
export function furnishInterior({THREE,parent,planPoint,box,mesh,group}) {
const material=(color,roughness=.8)=>new THREE.MeshStandardMaterial({color,roughness});
const m={oak:material(0xb99b70),oakLight:material(0xd2ba95),white:material(0xf0eee5),stone:material(0xe2ded2),sage:material(0x9ca894),cloth:material(0xd3c7b5),linen:material(0xeae5d9),pillow:material(0xb9bca9),dark:material(0x323a38),metal:new THREE.MeshStandardMaterial({color:0x8c9390,metalness:.65,roughness:.25}),water:material(0xc8d8d7),glass:new THREE.MeshStandardMaterial({color:0xb6d4d3,transparent:true,opacity:.23,roughness:.15,side:THREE.DoubleSide}),mirror:new THREE.MeshStandardMaterial({color:0xbdcaca,metalness:.65,roughness:.08})};
const colliders=[];
function block(p,name,px,py,w,d,h,y,mat){const [x,z]=planPoint(px,py);return box(p,name,x,y+h/2,z,d,h,w,mat);}
function collide(px,py,w,d){const [x,z]=planPoint(px,py);colliders.push({minX:x-d/2,maxX:x+d/2,minZ:z-w/2,maxZ:z+w/2});}
function cylinder(p,name,px,py,r,h,y,mat){const [x,z]=planPoint(px,py);const o=mesh(new THREE.CylinderGeometry(r,r,h,24),mat,p,name);o.position.set(x,y+h/2,z);return o;}
function oval(p,name,px,py,w,d,h,y,mat){const [x,z]=planPoint(px,py);const o=mesh(new THREE.SphereGeometry(1,24,12),mat,p,name);o.scale.set(d/2,h/2,w/2);o.position.set(x,y+h/2,z);return o;}
function cabinet(p,name,px,py,w,d,h=2.35,axis='py',mat=m.white){
const g=group(p,name);block(g,'Korpus',px,py,w,d,h,.02,mat);block(g,'Sokl',px,py,w-.06,d-.06,.09,.02,m.dark);
const span=axis==='py'?w:d,n=Math.max(1,Math.round(span/.55));
for(let i=1;i<n;i++){
const a=-span/2+i*span/n;
block(g,'Spára dvířek',px+(axis==='py'?a:0),py+(axis==='px'?a:0),axis==='py'?.008:w+.002,axis==='px'?.008:d+.002,h-.16,.12,m.oakLight);
}
collide(px,py,w,d);return g;
}
function legs(p,px,py,w,d,h){for(const a of [-1,1])for(const b of [-1,1])block(p,'Noha',px+a*(w/2-.08),py+b*(d/2-.08),.045,.045,h,.02,m.oak);}
function table(p,name,px,py,w,d,h=.75){const g=group(p,name);block(g,'Deska',px,py,w,d,.05,h-.05,m.oakLight);legs(g,px,py,w,d,h-.05);collide(px,py,w,d);return g;}
function chair(p,px,py,rotation=0){
const g=group(p,'Židle');const [x,z]=planPoint(px,py);g.position.set(x,0,z);g.rotation.y=rotation;
box(g,'Sedák',0,.47,0,.43,.08,.43,m.cloth);box(g,'Opěradlo',0,.71,.19,.43,.45,.06,m.cloth);
for(const a of [-1,1])for(const b of [-1,1])box(g,'Noha',a*.16,.22,b*.16,.035,.44,.035,m.oak);
}
function bed(p,name,px,py,w,d,head='py'){
const g=group(p,name);block(g,'Rám postele',px,py,w,d,.29,.08,m.oak);block(g,'Matrace',px,py,w-.06,d-.06,.2,.37,m.linen);
const across=head==='py'?w:d,along=head==='py'?d:w;
block(g,'Čelo',px+(head==='px'?w/2-.04:0),py+(head==='py'?d/2-.04:0),head==='px'?.08:w,head==='py'?.08:d,.9,.07,m.oakLight);
block(g,'Přikrývka',px-(head==='px'?.25:0),py-(head==='py'?.25:0),head==='px'?w-.55:w-.03,head==='py'?d-.55:d-.03,.055,.58,m.pillow);
const count=across>1.3?2:1;
for(let i=0;i<count;i++){
const side=(i-(count-1)/2)*.8;
oval(g,'Polštář',px+(head==='px'?along/2-.32:side),py+(head==='py'?along/2-.32:side),head==='py'?.64:.40,head==='py'?.40:.64,.16,.58,m.linen);
}
collide(px,py,w,d);return g;
}
function sink(p,name,px,py,w,d){
const g=group(p,name);block(g,'Umyvadlová skříňka',px,py,w,d,.52,.3,m.oakLight);block(g,'Umyvadlová deska',px,py,w+.03,d+.03,.06,.82,m.white);
const alongPx=w>d;const n=Math.max(w,d)>1.1?2:1;
for(let i=0;i<n;i++){
const off=(i-(n-1)/2)*.62,cx=px+(alongPx?off:0),cy=py+(alongPx?0:off);
oval(g,'Umyvadlo',cx,cy,alongPx?.5:.36,alongPx?.36:.5,.05,.88,m.white);
oval(g,'Dno mísy',cx,cy,alongPx?.37:.25,alongPx?.25:.37,.014,.912,m.water);
cylinder(g,'Baterie',cx+(alongPx?0:.18),cy+(alongPx?.18:0),.018,.20,.86,m.metal);
}
collide(px,py,w,d);return g;
}
function wc(p,px,py,axis='px'){
const g=group(p,'Závěsné WC');const w=axis==='px'?.65:.38,d=axis==='px'?.38:.65;
oval(g,'Keramická mísa',px,py,w,d,.34,.15,m.white);oval(g,'Sedátko',px,py,w,d,.045,.49,m.white);
oval(g,'Vnitřek mísy',px-.035,py, w*.59,d*.57,.016,.526,m.water);collide(px,py,w,d);
}
function shower(p,px,py,w,d){
const g=group(p,'Sprchový kout');block(g,'Vanička v úrovni podlahy',px,py,w,d,.025,.016,m.stone);
block(g,'Odtok',px+w/2-.1,py,.035,d*.55,.005,.043,m.metal);
const [x,z]=planPoint(px+w/2-.08,py);const o=mesh(new THREE.CylinderGeometry(.014,.014,1.35,10),m.metal,g,'Sprchová tyč');o.position.set(x,1.45,z);
cylinder(g,'Sprchová hlavice',px+w/2-.2,py,.11,.018,2.08,m.metal);
// Open entry to the shower; only the side screen is solid.
block(g,'Skleněná zástěna',px,py+d/2,w,.014,2,.03,m.glass);
}
const kitchen=group(parent,'Kuchyň do U s poloostrovem');
// Run at garden wall, then west side and peninsula; clear aisle 1.30 m.
for(let i=0;i<6;i++){
const py=.625+i*.60;cabinet(kitchen,'Spodní kuchyňská skříňka',.605,py,.60,.59,.82,'px',m.sage);
block(kitchen,'Pracovní deska',.605,py,.64,.60,.045,.84,m.stone);
}
cabinet(kitchen,'Dřezová část',1.545,.605,1.30,.60,.82,'py',m.sage);
block(kitchen,'Pracovní deska',1.545,.605,1.30,.64,.045,.84,m.stone);
cabinet(kitchen,'Poloostrov',2.645,1.745,.90,2.30,.82,'px',m.sage);
block(kitchen,'Deska poloostrova',2.645,1.745,.96,2.34,.045,.84,m.oakLight);
block(kitchen,'Dřez',1.45,.61,.60,.44,.012,.89,m.metal);block(kitchen,'Dřez – vnitřek',1.45,.61,.50,.34,.013,.90,m.dark);
cylinder(kitchen,'Dřezová baterie',1.45,.39,.018,.30,.89,m.metal);
block(kitchen,'Varná deska',.61,2.45,.49,.60,.015,.89,m.dark);
for(const a of [-.13,.13])for(const b of [-.16,.16])cylinder(kitchen,'Varná zóna',.61+a,2.45+b,.083,.003,.908,m.metal);
block(kitchen,'Vestavná trouba',.91,3.04,.016,.48,.42,.27,m.dark);block(kitchen,'Madlo trouby',.93,3.04,.025,.36,.025,.65,m.metal);
cabinet(kitchen,'Vysoká potravinová skříň',.62,3.92,.64,.60,2.30,'px',m.sage);
cabinet(kitchen,'Americká lednice',.66,4.78,.72,.92,1.88,'px',m.metal);
block(kitchen,'Lednice – madla',1.035,4.78,.025,.045,.62,.78,m.dark);
const living=group(parent,'Obývací pokoj a jídelna');
block(living,'Koberec',5.55,2.1,2.6,3.1,.012,.016,m.linen);
block(living,'Pohovka – sedací část',5.0,1.86,.95,2.80,.40,.12,m.cloth);
block(living,'Pohovka – lenoška',5.65,.91,1.7,.90,.40,.12,m.cloth);
block(living,'Pohovka – opěradlo',4.51,1.86,.18,2.80,.76,.12,m.cloth);
block(living,'Pohovka – bočnice',5.0,3.23,1.0,.15,.60,.12,m.cloth);
for(let py=.8;py<3;py+=.75)block(living,'Sedací polštář',5.05,py,.78,.68,.12,.51,m.linen);
collide(5.0,1.86,1.1,2.8);collide(5.65,.91,1.7,.9);
table(living,'Konferenční stolek',6.25,2.25,.70,1.15,.40);
cabinet(living,'Nízká TV skříňka',7.65,1.95,.45,2.30,.42,'px',m.oak);
block(living,'Televize',7.89,1.95,.05,1.45,.82,1.05,m.dark);
cylinder(living,'Kamna',7.582,4.763,.24,1.02,.02,m.dark);cylinder(living,'Kouřovod',7.582,4.763,.075,1.56,1.04,m.dark);
collide(7.582,4.763,.52,.52);
table(living,'Jídelní stůl pro šest',3.95,5.30,2.1,.86);
for(const px of [3.25,3.95,4.65]){chair(living,px,4.57,Math.PI/2);chair(living,px,6.03,-Math.PI/2);}
const storage=group(parent,'Šatny a zádveří');
for(const py of [2.956,4.306])cabinet(storage,'Šatní skříň',10.26,py,1.96,.60,2.35,'py');
cabinet(storage,'Šatní skříň v zádveří',10.2,7.13,1.90,.60,2.35,'py');
cabinet(storage,'Botník',10.20,4.98,1.90,.44,.48,'py',m.oakLight);
block(storage,'Zrcadlo v zádveří',10.20,4.76,1.20,.012,1.25,.92,m.mirror);
for(const py of [11.406,13.125])cabinet(storage,'Šatní skříň u ložnice',6.68,py,2.55,.60,2.35,'py');
const bedrooms=group(parent,'Pokoje a ložnice');
bed(bedrooms,'Lůžko pokoje u zahrady',6.96,7.53,1.95,1.0,'px');
cabinet(bedrooms,'Šatní skříň pokoje',6.65,10.65,2.50,.65,2.35,'py');
table(bedrooms,'Pracovní stůl u zahrady',4.7,10.64,1.15,.62);chair(bedrooms,4.7,9.85,Math.PI/2);
bed(bedrooms,'Lůžko pokoje u příjezdu',12.25,16.08,.92,2.0,'py');
cabinet(bedrooms,'Šatní skříň pokoje u příjezdu',9.85,15.8,.60,2.50,2.35,'px');
table(bedrooms,'Pracovní stůl u příjezdu',11.7,12.75,1.45,.65);chair(bedrooms,11.7,13.55,-Math.PI/2);
bed(bedrooms,'Manželská postel',5.95,16.06,1.80,2.05,'py');
for(const px of [4.77,7.13])cabinet(bedrooms,'Noční stolek',px,16.75,.48,.50,.46,'py',m.oakLight);
const bath=group(parent,'Sanita – společná koupelna');
shower(bath,12.005,10.08,1.50,1.0);
// Quarter-round corner tub, bounded by the 1600 x 1600 footprint in the PDF.
const tubShape=new THREE.Shape();tubShape.moveTo(0,0);tubShape.lineTo(1.6,0);tubShape.absarc(0,0,1.6,0,Math.PI/2,false);tubShape.lineTo(0,0);
const tubGeometry=new THREE.ExtrudeGeometry(tubShape,{depth:.53,bevelEnabled:true,bevelSize:.025,bevelThickness:.025,bevelSegments:2,steps:1,curveSegments:24});
tubGeometry.rotateX(-Math.PI/2);
const tub=mesh(tubGeometry,m.white,bath,'Rohová vana 1600');const [tx,tz]=planPoint(12.73,12.26);tub.position.set(tx,.055,tz);
// Local x increases towards room, local -z towards garden; oval basin is schematic.
oval(bath,'Vana – vnitřní mísa',12.05,11.53,1.15,.83,.035,.62,m.water);
collide(12.0,11.5,1.55,1.55);
sink(bath,'Dvojumyvadlo',10.65,12.01,1.25,.50);
block(bath,'Zrcadlo',10.65,12.27,1.28,.012,.82,1.08,m.mirror);wc(bath,10.0,9.91,'py');
const ensuite=group(parent,'Sanita – koupelna u ložnice');
shower(ensuite,8.60,16.60,1.55,1.0);wc(ensuite,9.06,13.95,'px');
sink(ensuite,'Dvojumyvadlo u ložnice',9.15,15.30,.48,1.35);
block(ensuite,'Zrcadlo',9.41,15.30,.012,1.40,.80,1.1,m.mirror);
for(let i=0;i<7;i++)block(ensuite,'Topný žebřík',7.83,15.35,.04,.55,.025,.60+i*.10,m.white);
const technical=group(parent,'Technické vybavení – orientační');
cabinet(technical,'Rekuperace / tepelné čerpadlo',12.10,7.15,1.0,.60,1.9,'py',m.white);
for(let i=0;i<2;i++){
block(technical,i?'Sušička':'Pračka',12.39,9.13,.62,.62,.82,.04+i*.84,m.white);
const o=cylinder(technical,'Kruhová dvířka spotřebiče',12.39,8.805,.20,.025,.44+i*.84,m.dark);o.rotation.x=Math.PI/2;o.position.y=.44+i*.84;
const [x,z]=planPoint(12.39,8.80);o.position.x=x;o.position.z=z;
}
collide(12.39,9.13,.62,.62);
cabinet(technical,'Mrazák',11.65,9.13,.63,.62,1.45,'py',m.white);
block(technical,'Elektroměnič a baterie',10.20,7.72,.65,.30,1.1,.6,m.white);
collide(10.20,7.72,.65,.30);
return {colliders,materials:m};
}
-127
View File
@@ -1,127 +0,0 @@
// Plan coordinates in metres: px garden -> road; py west -> east.
// Areas are the printed PDF values; names (except zádveří) follow the drawn fixtures.
export const rooms = [
{id:'living',name:'Obývací pokoj + kuchyň',short:'Obývák + kuchyň',area:49.88,polygon:[[.295,.295],[7.955,.295],[7.955,6.791],[.295,6.791]],label:[4.0,3.7],spawn:[3.7,3.7],look:[1.2,1.8],floor:'wood'},
{id:'spare',name:'Místnost bez určení',short:'Místnost',area:7.05,polygon:[[8.1,.295],[11.255,.295],[11.255,2.531],[8.1,2.531]],label:[9.7,1.4],spawn:[9.7,1.4],look:[8.5,2.3],floor:'tile'},
{id:'wardrobe',name:'Šatna',short:'Šatna',area:6.15,polygon:[[8.1,2.656],[11.255,2.656],[11.255,4.605],[8.1,4.605]],label:[9.7,3.63],spawn:[8.8,3.65],look:[10.5,3.65],floor:'wood'},
{id:'entry',name:'Zádveří',short:'Zádveří',area:8.58,polygon:[[8.1,4.75],[11.255,4.75],[11.255,7.432],[8.1,7.432]],label:[9.8,6.1],spawn:[10.3,6.1],look:[8.3,5.8],floor:'tile'},
{id:'hall',name:'Chodba',short:'Chodba',area:7.59,polygon:[[8.1,7.557],[9.421,7.557],[9.421,13.406],[8.1,13.406]],label:[8.76,10.5],spawn:[8.76,8.15],look:[8.76,12.6],floor:'wood'},
{id:'technical',name:'Technická místnost',short:'Technická',area:7.10,polygon:[[9.546,7.557],[11.38,7.557],[11.38,6.821],[12.755,6.821],[12.755,9.456],[9.546,9.456]],label:[11.15,8.35],spawn:[10.5,8.4],look:[12.3,8.1],floor:'tile'},
{id:'bath',name:'Koupelna',short:'Koupelna',area:8.66,polygon:[[9.546,9.581],[12.755,9.581],[12.755,12.282],[9.546,12.282]],label:[10.6,10.9],spawn:[10.25,10.95],look:[12.15,11.0],floor:'tile'},
{id:'front',name:'Pokoj u příjezdu',short:'Pokoj',area:15.08,polygon:[[9.546,12.407],[12.755,12.407],[12.755,17.105],[9.546,17.105]],label:[11.0,14.6],spawn:[10.9,14.3],look:[12.0,16.0],floor:'wood'},
{id:'garden',name:'Pokoj u zahrady',short:'Pokoj',area:15.62,polygon:[[4.045,7.015],[7.955,7.015],[7.955,10.981],[4.045,10.981]],label:[5.7,8.8],spawn:[5.4,8.7],look:[7.0,7.5],floor:'wood'},
{id:'master',name:'Ložnice se šatnou',short:'Ložnice + šatna',area:22.35,polygon:[[4.045,11.106],[7.955,11.106],[7.955,13.425],[5.344,13.425],[5.344,13.55],[7.673,13.55],[7.673,17.105],[4.045,17.105]],label:[4.65,14.2],spawn:[4.6,14.5],look:[5.95,16.2],floor:'wood'},
{id:'ensuite',name:'Koupelna u ložnice',short:'Koupelna',area:5.68,polygon:[[7.798,13.55],[9.421,13.55],[9.421,17.105],[7.798,17.105]],label:[8.6,15.2],spawn:[8.35,14.55],look:[8.6,16.65],floor:'tile'}
];
// axis=px: fixed px, runs along py; axis=py: fixed py, runs along px.
export const partitions = [
['px',8.0275,.295,13.425,.145,[[5.19,6.09],[9.257,10.156],[11.805,12.708]]],
['py',2.5935,8.1,11.255,.125,[[8.349,9.248]]],
['py',4.6775,8.1,11.255,.145,[[8.349,9.248]]],
['py',7.4945,8.1,11.255,.125,[[8.349,9.248]]],
['py',6.903,3.775,7.955,.224,[]],
['py',11.0435,4.045,7.955,.125,[]],
['px',9.4835,7.557,13.406,.125,[[8.006,8.905],[10.656,11.457],[12.457,13.357]]],
['py',9.5185,9.546,12.755,.125,[]],
['py',12.3445,9.546,12.755,.125,[]],
['py',13.4785,8.1,9.421,.145,[]],
['py',13.4875,5.344,7.955,.125,[]],
['px',7.7355,13.55,17.105,.125,[[14.152,14.953]]],
['px',9.4835,13.406,17.105,.125,[]],
['py',10.63,11.255,12.755,.10,[]]
];
export function buildInterior({THREE,houseGroup,planPoint,box,mesh,group}) {
const root=group(houseGroup,'Interiér podle půdorysu');
const floors=group(root,'Podlahy místností'),fullWalls=group(root,'Příčky – plná výška'),cutWalls=group(root,'Příčky – řez 1,12 m');
const ceiling=group(root,'Podhled +2,600 m'),furniture=group(root,'Orientační vybavení');
const height=2.6,cutHeight=1.12,doorHead=2.1;
const wallMaterial=new THREE.MeshStandardMaterial({color:0xeeeae1,roughness:.92});
const wood=new THREE.MeshStandardMaterial({color:0xc7ac87,roughness:.85});
const tile=new THREE.MeshStandardMaterial({color:0xc8c6be,roughness:.83});
const trim=new THREE.MeshStandardMaterial({color:0xe4d5bb,roughness:.78});
const grout=new THREE.LineBasicMaterial({color:0x8c8373,transparent:true,opacity:.16});
const wallColliders=[];
function horizontal(parent,name,polygon,y,material,down=false) {
const points=polygon.map(p=>planPoint(...p));
const triangles=THREE.ShapeUtils.triangulateShape(points.map(([x,z])=>new THREE.Vector2(x,z)),[]);
const geom=new THREE.BufferGeometry();
geom.setAttribute('position',new THREE.Float32BufferAttribute(points.flatMap(([x,z])=>[x,y,z]),3));
const indices=triangles.flatMap(([a,b,c])=>{
const A=points[a],B=points[b],C=points[c];
const normalY=(B[1]-A[1])*(C[0]-A[0])-(B[0]-A[0])*(C[1]-A[1]);
return (normalY>0)!==down?[a,b,c]:[c,b,a];
});
geom.setIndex(indices);geom.computeVertexNormals();
const m=mesh(geom,material,parent,name);m.castShadow=false;return m;
}
// A continuous surface also fills thresholds, so doors have no visible cracks.
const outline=[[0,0],[11.55,0],[11.55,6.525],[13.05,6.525],[13.05,17.4],[3.75,17.4],[3.75,7.125],[0,7.125]];
horizontal(floors,'Podlaha a prahy',outline,.004,tile);
horizontal(ceiling,'Souvislý rovný podhled',outline,height,new THREE.MeshStandardMaterial({color:0xf5f3eb,roughness:1,side:THREE.DoubleSide}),true);
for(const room of rooms) {
const m=horizontal(floors,`${room.name} · ${room.area.toFixed(2)} m²`,room.polygon,.01,room.floor==='wood'?wood:tile);
m.userData.roomId=room.id;
// Thin joints: plank rhythm in dry rooms, large tiles in service rooms.
const loX=Math.min(...room.polygon.map(p=>p[0])),hiX=Math.max(...room.polygon.map(p=>p[0]));
const loY=Math.min(...room.polygon.map(p=>p[1])),hiY=Math.max(...room.polygon.map(p=>p[1]));
const pts=[];
const inside=(px,py)=>pointInPolygon(room.polygon,px,py);
function joint(a,b) { const [x,z]=planPoint(...a),[xx,zz]=planPoint(...b);pts.push(x,.012,z,xx,.012,zz); }
for(let py=Math.ceil(loY/(room.floor==='wood'?.20:.6))*(room.floor==='wood'?.20:.6);py<hiY;py+=room.floor==='wood'?.20:.6) {
let start=null;
for(let px=loX+.002;px<=hiX+.03;px+=.025) {
const ok=px<hiX&&inside(px,py);
if(ok&&start===null)start=px;
if(!ok&&start!==null){joint([start,py],[Math.min(px,hiX-.002),py]);start=null;}
}
}
if(room.floor==='tile')for(let px=Math.ceil(loX/.6)*.6;px<hiX;px+=.6){
let start=null;for(let py=loY+.002;py<=hiY+.03;py+=.025){
const ok=py<hiY&&inside(px,py);if(ok&&start===null)start=py;
if(!ok&&start!==null){joint([px,start],[px,Math.min(py,hiY-.002)]);start=null;}
}
}
const geometry=new THREE.BufferGeometry();geometry.setAttribute('position',new THREE.Float32BufferAttribute(pts,3));
const joints=new THREE.LineSegments(geometry,grout);joints.name='Spáry podlahy';floors.add(joints);
}
function wallBox(parent,axis,fixed,start,end,thickness,bottom,top,material,name) {
const px=axis==='px'?fixed:(start+end)/2,py=axis==='py'?fixed:(start+end)/2;
const [x,z]=planPoint(px,py);
return box(parent,name,x,(bottom+top)/2,z,axis==='px'?end-start:thickness,top-bottom,axis==='px'?thickness:end-start,material);
}
for(const [axis,fixed,start,end,thickness,holes] of partitions) {
let cursor=start;
const solid=(a,b)=>{
if(b-a<.001)return;
const m=wallBox(fullWalls,axis,fixed,a,b,thickness,0,height,wallMaterial,'Vnitřní příčka');
wallBox(cutWalls,axis,fixed,a,b,thickness,0,cutHeight,wallMaterial,'Vnitřní příčka v řezu');
const sx=axis==='px'?b-a:thickness,sz=axis==='px'?thickness:b-a;
wallColliders.push({minX:m.position.x-sx/2,maxX:m.position.x+sx/2,minZ:m.position.z-sz/2,maxZ:m.position.z+sz/2});
for(const parent of [fullWalls,cutWalls])wallBox(parent,axis,fixed,a,b,thickness+.016,.015,.09,trim,'Soklová lišta');
};
for(const [a,b] of holes){
solid(cursor,a);
wallBox(fullWalls,axis,fixed,a,b,thickness,doorHead,height,wallMaterial,'Nadpraží vnitřních dveří');
for(const parent of [fullWalls,cutWalls])for(const edge of [a,b])wallBox(parent,axis,fixed,edge-.018,edge+.018,thickness+.035,.01,parent===fullWalls?doorHead:cutHeight,trim,'Obložková zárubeň');
wallBox(fullWalls,axis,fixed,a-.018,b+.018,thickness+.035,doorHead-.04,doorHead,trim,'Horní zárubeň');
cursor=b;
}
solid(cursor,end);
}
// Attic access as drawn in vestibule; a flush hatch, not a freestanding object.
const [hx,hz]=planPoint(8.9,6.3);
box(ceiling,'Půdní výlez 1000 × 700',hx,2.586,hz,.70,.018,1.0,trim);
fullWalls.visible=false;ceiling.visible=false;root.visible=false;
return {root,floors,fullWalls,cutWalls,ceiling,furniture,rooms,wallColliders,wallMaterial,cutHeight,height};
}
export function pointInPolygon(polygon,x,z) {
let inside=false;
for(let i=0,j=polygon.length-1;i<polygon.length;j=i++){
const a=polygon[i],b=polygon[j];
if((a[1]>z)!==(b[1]>z)&&x<(b[0]-a[0])*(z-a[1])/(b[1]-a[1])+a[0])inside=!inside;
}
return inside;
}
-203
View File
@@ -1,203 +0,0 @@
import {buildInterior,pointInPolygon} from './interior-model.js';
import {furnishInterior} from './interior-furniture.js';
export function installInterior(ctx) {
const {THREE,houseGroup,modelRoot,shellGroup,roofGroup,roofTimber,controls,scene,renderer,planPoint,box,mesh,group,groundGroup,parkingGroup,boundaryGroup,resize,saveBlob,exportGlb,solar,setSolarPlaying,toggleInfo,toggleSun}=ctx;
const $=s=>document.querySelector(s),canvas=renderer.domElement;
const data=buildInterior(ctx),furnishing=furnishInterior({...ctx,parent:data.furniture});
const perspective=ctx.getCamera();
const ortho=new THREE.OrthographicCamera(-12,12,10,-10,.035,220);
const state={enabled:false,mode:'axon',panel:true,furniture:true,labels:true,fill:true,room:'living',position:new THREE.Vector3(),yaw:0,pitch:0,keys:new Set(),pad:new Set(),pointer:null,saved:null,userCamera:false};
const cutShell=group(houseGroup,'Obvodové stěny – řez 1,12 m');
shellGroup.children.forEach(original=>{
if(!original.isMesh||!original.geometry.parameters)return;
const {width,height,depth}=original.geometry.parameters;
if(!width||!height||!depth)return;
const bottom=original.position.y-height/2,top=Math.min(data.cutHeight,original.position.y+height/2);
if(top<=bottom)return;
const copy=original.clone();copy.geometry=new THREE.BoxGeometry(width,top-bottom,depth);copy.position.y=(bottom+top)/2;copy.name+=' – řez';cutShell.add(copy);
});
cutShell.visible=false;
data.root.visible=true;data.fullWalls.visible=true;data.cutWalls.visible=false;data.ceiling.visible=true;
modelRoot.userData.interior={source:'Pudorys prizemi (1).pdf + Rez.pdf',heightM:2.6,cutHeightM:1.12,areasFromDrawing:true,furnishing:'schematic',rooms:data.rooms.map(({id,name,area})=>({id,name,area}))};
const fillLight=new THREE.AmbientLight(0xfff5df,1.25);fillLight.visible=false;scene.add(fillLight);
const innerLight=new THREE.HemisphereLight(0xfff9ed,0xb7a98e,.7);innerLight.visible=false;scene.add(innerLight);
const glassMaterials=new Set();shellGroup.traverse(o=>{if(o.isMesh&&o.material.transparent)glassMaterials.add(o.material);});
const glassOpacity=new Map([...glassMaterials].map(m=>[m,m.opacity]));
const localFootprint=[[0,0],[11.55,0],[11.55,6.525],[13.05,6.525],[13.05,17.4],[3.75,17.4],[3.75,7.125],[0,7.125]].map(p=>planPoint(...p));
const labels=[];
const toWorld=(p,y=0)=>{houseGroup.updateWorldMatrix(true,false);const [x,z]=planPoint(...p);return houseGroup.localToWorld(new THREE.Vector3(x,y,z));};
const localToWorld=v=>{houseGroup.updateWorldMatrix(true,false);return houseGroup.localToWorld(v.clone());};
const formatArea=v=>v.toFixed(2).replace('.',',')+' m²';
function selectRoom(id){
const room=data.rooms.find(r=>r.id===id)||data.rooms[0];state.room=room.id;
const [x,z]=planPoint(...room.spawn),[lx,lz]=planPoint(...room.look);
state.position.set(x,1.62,z);state.yaw=Math.atan2(lx-x,lz-z);state.pitch=-.04;
mode('walk');$('#interiorRoom').value=room.id;
document.querySelectorAll('#interiorMap .map-room').forEach(el=>el.classList.toggle('active',el.dataset.room===id));
$('#interiorViewTitle').textContent=room.name;
updateWalkCamera();
}
for(const room of data.rooms){
const option=document.createElement('option');option.value=room.id;option.textContent=room.name+' · '+formatArea(room.area);$('#interiorRoom').append(option);
const ns='http://www.w3.org/2000/svg',g=document.createElementNS(ns,'g'),p=document.createElementNS(ns,'polygon');
p.setAttribute('points',room.polygon.map(([px,py])=>`${10+py*18.8},${10+(13.05-px)*18.8}`).join(' '));
p.setAttribute('class','map-room');p.dataset.room=room.id;p.setAttribute('tabindex','0');p.setAttribute('role','button');p.setAttribute('aria-label',option.textContent);
const title=document.createElementNS(ns,'title');title.textContent=option.textContent;p.append(title);
p.addEventListener('click',()=>selectRoom(room.id));p.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();selectRoom(room.id);}});g.append(p);
const text=document.createElementNS(ns,'text');text.setAttribute('x',10+room.label[1]*18.8);text.setAttribute('y',13+(13.05-room.label[0])*18.8);text.setAttribute('text-anchor','middle');text.textContent=room.area.toFixed(2).replace('.',',');g.append(text);$('#interiorMap').append(g);
const tag=document.createElement('button');tag.type='button';tag.className='int-room-tag';tag.innerHTML=`<b>${room.short}</b><small>${formatArea(room.area)}</small>`;tag.setAttribute('aria-label','Vstoupit: '+option.textContent);tag.addEventListener('click',()=>selectRoom(room.id));$('#interiorLabels').append(tag);labels.push({room,tag});
}
function switchCamera(camera){ctx.setCamera(camera);controls.object=camera;}
function sync(){
const walking=state.enabled&&state.mode==='walk',cut=state.enabled&&!walking;
shellGroup.visible=!cut;cutShell.visible=cut;roofGroup.visible=!cut;roofTimber.visible=!cut;
data.fullWalls.visible=!cut;data.cutWalls.visible=cut;data.ceiling.visible=!cut;data.furniture.visible=state.furniture;
fillLight.visible=state.enabled&&state.fill;innerLight.visible=fillLight.visible;
for(const [m,opacity]of glassOpacity)m.opacity=walking?.13:opacity;
document.body.classList.toggle('interior-active',state.enabled);document.body.classList.toggle('interior-walk',walking);document.body.classList.toggle('interior-panel-closed',!state.panel);
$('#interiorLaunch').setAttribute('aria-expanded',String(state.enabled));$('#interiorPanelToggle').setAttribute('aria-expanded',String(state.panel));
$('#interiorRoomLabels').disabled=walking;
document.querySelectorAll('[data-interior-mode]').forEach(b=>{const active=b.dataset.interiorMode===state.mode;b.classList.toggle('active',active);b.setAttribute('aria-pressed',String(active));});
$('#interiorHint').textContent=walking?'W A S D: chůze · šipky: chůze / otáčení · tažením se rozhlížejte · Esc: exteriér':'Tažením otáčejte · kolečkem přibližujte · výběrem místnosti vstupte dovnitř';
if(!walking)$('#interiorViewTitle').textContent=state.mode==='plan'?'Půdorys přízemí':'Dům bez střechy';
if(state.enabled){groundGroup.visible=walking&&state.saved.ground;parkingGroup.visible=walking&&state.saved.parking;boundaryGroup.visible=walking&&state.saved.boundary;}
labels.forEach(({tag})=>tag.hidden=!state.enabled||walking||!state.labels);
}
function enter(){
if(state.enabled)return;
state.saved={position:perspective.position.clone(),quaternion:perspective.quaternion.clone(),target:controls.target.clone(),fov:perspective.fov,near:perspective.near,minDistance:controls.minDistance,maxDistance:controls.maxDistance,minPolar:controls.minPolarAngle,maxPolar:controls.maxPolarAngle,ground:groundGroup.visible,parking:parkingGroup.visible,boundary:boundaryGroup.visible,playing:solar.playing,info:document.body.classList.contains('info-open'),sun:document.body.classList.contains('sun-open')};
setSolarPlaying(false);toggleInfo(false);toggleSun(false);
state.enabled=true;state.panel=true;state.mode='axon';state.userCamera=false;controls.minDistance=3;controls.maxDistance=100;
sync();resize();fit();$('#interiorExit').focus({preventScroll:true});
}
function exit(){
if(!state.enabled)return;
state.enabled=false;clearKeys();state.pointer=null;state.panel=true;sync();
const s=state.saved;switchCamera(perspective);controls.enabled=true;controls.enableRotate=true;controls.minDistance=s.minDistance;controls.maxDistance=s.maxDistance;controls.minPolarAngle=s.minPolar;controls.maxPolarAngle=s.maxPolar;
perspective.fov=s.fov;perspective.near=s.near;perspective.position.copy(s.position);perspective.quaternion.copy(s.quaternion);controls.target.copy(s.target);
groundGroup.visible=s.ground;parkingGroup.visible=s.parking;boundaryGroup.visible=s.boundary;
controls.update();resize();if(s.info)toggleInfo(true);if(s.sun)toggleSun(true);setSolarPlaying(s.playing);$('#interiorLaunch').focus({preventScroll:true});
}
function mode(value){
if(!state.enabled)enter();
state.mode=value;state.userCamera=false;clearKeys();state.pointer=null;
controls.enabled=value!=='walk';controls.enableRotate=value!=='plan';controls.minPolarAngle=.001;controls.maxPolarAngle=1.52;
switchCamera(value==='plan'?ortho:perspective);
perspective.near=value==='walk'?.035:.1;perspective.fov=value==='walk'?65:36;perspective.updateProjectionMatrix();
sync();resize();
if(value==='walk'){
if(state.position.lengthSq()===0)selectRoom('living');else updateWalkCamera();
}else fit();
}
function fit(){
if(!state.enabled)return;
state.userCamera=false;
if(state.mode==='walk'){selectRoom(state.room);return;}
const camera=ctx.getCamera(),rect=canvas.getBoundingClientRect(),aspect=rect.width/rect.height;
const target=toWorld([6.525,8.7],.45);controls.target.copy(target);
if(state.mode==='plan'){
const h=Math.max(18,22/aspect);ortho.left=-h*aspect/2;ortho.right=h*aspect/2;ortho.top=h/2;ortho.bottom=-h/2;ortho.zoom=1;
// Garden at the bottom, matching the supplied plan, with the road at the top.
const up=houseGroup.localToWorld(new THREE.Vector3(0,0,1)).sub(houseGroup.localToWorld(new THREE.Vector3())).normalize();
ortho.up.copy(up);ortho.position.copy(target).add(new THREE.Vector3(0,50,0));ortho.lookAt(target);ortho.updateProjectionMatrix();
}else{
perspective.up.set(0,1,0);const distance=Math.max(35,33/aspect);
const offset=new THREE.Vector3(9,18,-23).normalize().multiplyScalar(distance).applyQuaternion(modelRoot.quaternion);
perspective.position.copy(target).add(offset);perspective.lookAt(target);
}
controls.update();
}
function resizeView(){
if(!state.enabled)return;
const rect=canvas.getBoundingClientRect();
if(state.mode==='plan'){
const h=Math.max(18,22/(rect.width/rect.height));ortho.left=-h*rect.width/rect.height/2;ortho.right=-ortho.left;ortho.top=h/2;ortho.bottom=-h/2;ortho.updateProjectionMatrix();
}else if(state.mode==='axon'&&!state.userCamera)fit();
}
function distanceToEdge(x,z,a,b){const dx=b[0]-a[0],dz=b[1]-a[1],t=Math.max(0,Math.min(1,((x-a[0])*dx+(z-a[1])*dz)/(dx*dx+dz*dz)));return Math.hypot(x-a[0]-t*dx,z-a[1]-t*dz);}
function canStand(x,z){
const r=.16;
if(!pointInPolygon(localFootprint,x,z))return false;
for(let i=0;i<localFootprint.length;i++)if(distanceToEdge(x,z,localFootprint[i],localFootprint[(i+1)%localFootprint.length])<.295+r)return false;
for(const b of data.wallColliders)if(x>b.minX-r&&x<b.maxX+r&&z>b.minZ-r&&z<b.maxZ+r)return false;
if(state.furniture)for(const b of furnishing.colliders)if(x>b.minX-r&&x<b.maxX+r&&z>b.minZ-r&&z<b.maxZ+r)return false;
return true;
}
function move(dx,dz){const n=Math.max(1,Math.ceil(Math.hypot(dx,dz)/.045));for(let i=0;i<n;i++){if(canStand(state.position.x+dx/n,state.position.z))state.position.x+=dx/n;if(canStand(state.position.x,state.position.z+dz/n))state.position.z+=dz/n;}}
function updateWalkCamera(){
const direction=new THREE.Vector3(Math.sin(state.yaw)*Math.cos(state.pitch),Math.sin(state.pitch),Math.cos(state.yaw)*Math.cos(state.pitch));
perspective.position.copy(localToWorld(state.position));perspective.up.set(0,1,0);perspective.lookAt(localToWorld(state.position.clone().add(direction)));
}
function updateLabels(){
if(!state.enabled||state.mode==='walk'||!state.labels)return;
const camera=ctx.getCamera(),r=canvas.getBoundingClientRect();camera.updateMatrixWorld();
const occupied=[];
for(const {room,tag}of labels){
const p=toWorld(room.label,.08).project(camera),x=(p.x+1)*r.width/2,y=(1-p.y)*r.height/2;
tag.style.left=x+'px';tag.style.top=y+'px';tag.hidden=p.z<-1||p.z>1||x<20||x>r.width-20||y<78||y>r.height-70;
if(tag.hidden)continue;
// Keep small screens readable; all rooms remain in the map and selector.
const w=tag.offsetWidth,h=tag.offsetHeight,b={x:x-w/2,y:y-h/2,w,h};
if(occupied.some(a=>b.x<a.x+a.w+3&&b.x+b.w+3>a.x&&b.y<a.y+a.h+3&&b.y+b.h+3>a.y))tag.hidden=true;else occupied.push(b);
}
}
function frame(dt){
if(!state.enabled)return;
if(state.mode==='walk'){
const k=state.keys,p=state.pad;
const forward=Number(k.has('KeyW')||k.has('ArrowUp')||p.has('forward'))-Number(k.has('KeyS')||k.has('ArrowDown')||p.has('back'));
const side=Number(k.has('KeyD')||p.has('right'))-Number(k.has('KeyA')||p.has('left'));
const turn=Number(k.has('ArrowLeft'))-Number(k.has('ArrowRight'));state.yaw+=turn*dt*1.4;
const distance=dt*(k.has('ShiftLeft')||k.has('ShiftRight')?2.8:1.45)/Math.max(1,Math.hypot(forward,side));
move((Math.sin(state.yaw)*forward-Math.cos(state.yaw)*side)*distance,(Math.cos(state.yaw)*forward+Math.sin(state.yaw)*side)*distance);updateWalkCamera();
}
updateLabels();
}
function keydown(e){
if(e.key==='Escape'){e.preventDefault();exit();return;}
if(/^(INPUT|SELECT|TEXTAREA|BUTTON|SUMMARY)$/.test(e.target.tagName))return;
if(state.mode==='walk'&&['KeyW','KeyA','KeyS','KeyD','ArrowUp','ArrowDown','ArrowLeft','ArrowRight','ShiftLeft','ShiftRight'].includes(e.code)){e.preventDefault();state.keys.add(e.code);}
}
function clearKeys(){state.keys.clear();state.pad.clear();document.querySelectorAll('#walkPad .held').forEach(b=>b.classList.remove('held'));}
window.addEventListener('keyup',e=>state.keys.delete(e.code));window.addEventListener('blur',clearKeys);document.addEventListener('visibilitychange',clearKeys);
canvas.addEventListener('pointerdown',e=>{
if(!state.enabled)return;state.userCamera=true;
if(state.mode==='walk'){e.preventDefault();document.activeElement?.blur();canvas.setPointerCapture(e.pointerId);state.pointer={id:e.pointerId,x:e.clientX,y:e.clientY};}
});
canvas.addEventListener('pointermove',e=>{const p=state.pointer;if(!p||p.id!==e.pointerId||state.mode!=='walk')return;state.yaw-=(e.clientX-p.x)*.004;state.pitch=Math.max(-1.25,Math.min(1.25,state.pitch-(e.clientY-p.y)*.003));p.x=e.clientX;p.y=e.clientY;});
for(const event of ['pointerup','pointercancel','lostpointercapture'])canvas.addEventListener(event,e=>{if(state.pointer?.id===e.pointerId)state.pointer=null;});
canvas.addEventListener('wheel',e=>{if(state.enabled&&state.mode==='walk'){e.preventDefault();perspective.fov=Math.max(45,Math.min(85,perspective.fov+e.deltaY*.025));perspective.updateProjectionMatrix();}},{passive:false});
for(const button of document.querySelectorAll('[data-walk]')){
button.addEventListener('pointerdown',e=>{e.preventDefault();button.setPointerCapture(e.pointerId);state.pad.add(button.dataset.walk);button.classList.add('held');});
for(const event of ['pointerup','pointercancel','lostpointercapture'])button.addEventListener(event,()=>{state.pad.delete(button.dataset.walk);button.classList.remove('held');});
}
$('#interiorLaunch').addEventListener('click',enter);$('#interiorExit').addEventListener('click',exit);
$('#interiorRoom').addEventListener('change',e=>{if(e.target.value)selectRoom(e.target.value);});
document.querySelectorAll('[data-interior-mode]').forEach(b=>b.addEventListener('click',()=>{mode(b.dataset.interiorMode);b.blur();}));
document.querySelectorAll('[data-interior-view]').forEach(b=>b.addEventListener('click',()=>{
const id=b.dataset.interiorView;selectRoom(id==='kitchen'?'living':id);
if(id==='living'){const [x,z]=planPoint(3.7,4.15),[lx,lz]=planPoint(6.2,1.9);state.position.set(x,1.62,z);state.yaw=Math.atan2(lx-x,lz-z);state.pitch=-.07;updateWalkCamera();}
b.blur();
}));
$('#interiorFit').addEventListener('click',fit);
$('#interiorPanelToggle').addEventListener('click',()=>{state.panel=!state.panel;sync();resize();});
$('#interiorFurniture').addEventListener('change',e=>{state.furniture=e.target.checked;sync();if(state.mode==='walk'&&!canStand(state.position.x,state.position.z))selectRoom(state.room);});
$('#interiorRoomLabels').addEventListener('change',e=>{state.labels=e.target.checked;sync();});
$('#interiorFill').addEventListener('change',e=>{state.fill=e.target.checked;sync();});
$('#interiorWallColor').addEventListener('input',e=>data.wallMaterial.color.set(e.target.value));
$('#interiorSaveGlb').addEventListener('click',exportGlb);
async function savePng(){
renderer.render(scene,ctx.getCamera());updateLabels();
const out=document.createElement('canvas'),scale=canvas.width/canvas.clientWidth;out.width=canvas.width;out.height=canvas.height+Math.round(62*scale);
const c=out.getContext('2d');c.fillStyle='#f8f9f3';c.fillRect(0,0,out.width,out.height);c.drawImage(canvas,0,0);
if(state.labels&&state.mode!=='walk')for(const {room,tag}of labels){if(tag.hidden)continue;const x=parseFloat(tag.style.left)*scale,y=parseFloat(tag.style.top)*scale;c.font=`${10*scale}px Arial`;const text=room.short+' · '+formatArea(room.area),w=c.measureText(text).width;c.fillStyle='#fafcf4eb';c.fillRect(x-w/2-6*scale,y-10*scale,w+12*scale,20*scale);c.fillStyle='#334c35';c.textAlign='center';c.fillText(text,x,y+3*scale);}
c.textAlign='left';c.fillStyle='#2d3c31';c.font=`${12*scale}px Arial`;c.fillText('Klabava · '+$('#interiorViewTitle').textContent,22*scale,canvas.height+26*scale);
c.fillStyle='#7b8372';c.font=`${9*scale}px Arial`;c.fillText('Dispozice podle půdorysu · vybavení a materiály orientační',22*scale,canvas.height+44*scale);
out.toBlob(blob=>{if(blob)saveBlob(blob,'klabava-interier-'+state.mode+'.png');});
}
$('#interiorSavePng').addEventListener('click',savePng);
// Small inspection API also useful when embedding this viewer in another page.
window.KLABAVA_INTERIOR={enter,exit,setMode:mode,selectRoom,getState:()=>({enabled:state.enabled,mode:state.mode,room:state.room,furniture:state.furniture,panel:state.panel,position:state.position.toArray(),canStand:canStand(state.position.x,state.position.z),rooms:data.rooms.map(({id,name,area,spawn})=>({id,name,area,spawn})),wallColliders:data.wallColliders.length}),canStand:(px,py)=>canStand(...planPoint(px,py))};
return {get enabled(){return state.enabled;},get walking(){return state.enabled&&state.mode==='walk';},frame,resize:resizeView,keydown,exit,enter,savePng};
}
-901
View File
@@ -1,901 +0,0 @@
import { installInterior } from './interior-view.js';
let THREE, OrbitControls, GLTFExporter, solarPosition;
try {
const modules = await Promise.all([
import('three'),
import('three/addons/controls/OrbitControls.js'),
import('three/addons/exporters/GLTFExporter.js'),
import('./solar.js')
]);
THREE = modules[0];
OrbitControls = modules[1].OrbitControls;
GLTFExporter = modules[2].GLTFExporter;
solarPosition = modules[3].solarPosition;
} catch (error) {
const loading = document.querySelector('#loading');
if (loading) {
loading.classList.add('error');
loading.innerHTML = `3D model se nepodařilo načíst.<br>${error.message}<br><small>Spusť web přes místní server a ověř přítomnost složky vendor/three.</small>`;
}
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');
// Centre of parcel 66/17 from the S-JTSK grid in the survey. The site plan's north arrow is S-JTSK grid north,
// 8.5° west of true north here, so the road side of the house faces 1.5° east of true north.
const SITE_DEFAULT = { latitude: 49.7520, longitude: 13.5440, bearing: 1.5 };
const site = { ...SITE_DEFAULT };
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');
let 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.<br>${error.message}<br><small>Zkontroluj podporu WebGL v prohlížeči.</small>`;
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';
// The scene uses true north = +z and east = −x; the site bearing turns the plot clockwise as seen from above.
modelRoot.rotation.y = -site.bearing * RAD;
scene.add(modelRoot);
const groundGroup = new THREE.Group(); groundGroup.name = 'Interpolovaný terén'; 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 shellGroup = new THREE.Group(); shellGroup.name = 'Obvodové stěny a otvory'; houseGroup.add(shellGroup);
const roofGroup = new THREE.Group(); roofGroup.name = 'Střecha, podbití a komín'; houseGroup.add(roofGroup);
const roofTimber = new THREE.Group(); roofTimber.name = 'Trámy krytých ploch'; houseGroup.add(roofTimber);
let interior = null;
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 }),
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 }),
soffit: new THREE.MeshStandardMaterial({ color: 0xd6d2c7, roughness: 0.9, side: THREE.DoubleSide }),
timber: new THREE.MeshStandardMaterial({ color: 0x8c7a63, roughness: 0.85 }),
metal: new THREE.MeshStandardMaterial({ color: 0x545b58, roughness: 0.6, metalness: 0.18 }),
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 })
};
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 slopedBox(parent, name, x, z, sx, sz, lowerAt, upperAt, material) {
const x0 = x - sx / 2, x1 = x + sx / 2, z0 = z - sz / 2, z1 = z + sz / 2;
const vertices = [
[x0, lowerAt(x0, z0), z0], [x1, lowerAt(x1, z0), z0], [x1, lowerAt(x1, z1), z1], [x0, lowerAt(x0, z1), z1],
[x0, upperAt(x0, z0), z0], [x1, upperAt(x1, z0), z0], [x1, upperAt(x1, z1), z1], [x0, upperAt(x0, z1), z1]
];
const faces = [[0, 3, 2, 1], [4, 5, 6, 7], [0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]];
const positions = [], indices = [];
for (const face of faces) {
const base = positions.length / 3;
for (const index of face) positions.push(...vertices[index]);
for (let i = 1; i < face.length - 1; i++) indices.push(base, base + i, base + 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; }
// Floor-plan frame used for the dimensions below (Půdorys přízemí, Situace): px runs from the garden façade (0)
// to the road side (13.05 m), py from the west façade (0) to the east gable (17.4 m). The road side faces north,
// so px maps to +z and py to −x (east); houseGroup sits 8.4 m north of the site origin.
const planPoint = (px, py) => [8.7 - py, px - 6.525];
const sitePoint = (px, py) => [8.7 - py, px - 6.525 + houseGroup.position.z];
// Plane fitted to the 27 spot heights inside parcel 66/17 (rms 4 cm): about 349.2 m Bpv at the road boundary,
// 347.1 m Bpv at the rear and 0.4 m higher on the east side. Floor ±0.000 sits 0.30 m above the surveyed ground
// at the centre of the house, matching the −0.300 terrain line in the elevations.
function rawGround(x, z) { return -0.694 + 0.0468 * z - 0.0178 * x; }
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.30 * (1 - blend) + rawGround(x, z) * blend;
}
function buildGround() {
// Parcel corners from the site plan in floor-plan metres; they agree with the S-JTSK survey within 0.1 m
// (5.0 m to the road, 3.18–3.37 m to the west boundary, 2.0 m to the east boundary, 27.44 m to the rear).
const roadEast = sitePoint(18.04, 19.40), roadWest = sitePoint(18.05, -3.11);
const rearEast = sitePoint(-27.19, 19.39), rearWest = sitePoint(-27.50, -3.78);
const nx = 30, nz = 60;
const positions = [], indices = [];
for (let j = 0; j <= nz; j++) {
const v = j / nz;
const leftX = roadEast[0] * (1 - v) + rearEast[0] * v, leftZ = roadEast[1] * (1 - v) + rearEast[1] * v;
const rightX = roadWest[0] * (1 - v) + rearWest[0] * v, rightZ = roadWest[1] * (1 - v) + rearWest[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, 'Terén parcely 66/17 podle výškopisu');
// The local road (parcel 66/8) runs along the northern boundary and follows the surveyed fall.
const roadY = (x, z) => rawGround(x, z) + 0.01;
const roadPoints = [[-18, 26], [20, 26], [20, 19.95], [-18, 19.95]].map(([x, z]) => [x, roadY(x, z), z]);
poly(parkingGroup, 'Místní komunikace', roadPoints, mats.road);
line(parkingGroup, 'Okraj komunikace', [[-18, roadY(-18, 20.05) + 0.02, 20.05], [20, roadY(20, 20.05) + 0.02, 20.05]], 0xbfc4bc, 0.8);
// Parcel outline is intentionally thin and diagrammatic; the supplied survey has no parcel staking coordinates.
const border = [roadWest, roadEast, rearEast, rearWest].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();
}
function buildParking() {
const y = (x, z) => groundHeight(x, z) + 0.035;
// Situace: two parking spaces 6.625 × 5.0 m between the road boundary and the covered entry, in front of the
// recessed kitchen wing. The covered entry strip under the roof is paved as well.
const pad = [[11.55, -0.1], [11.55, 6.525], [18.05, 6.525], [18.05, -0.1]].map(([px, py]) => {
const [x, z] = sitePoint(px, py);
return [x, y(x, z), z];
});
poly(parkingGroup, 'Parkovací stání a krytý vstup', pad, mats.gravel);
function addCar(px, py, material, label) {
const [cx, cz] = sitePoint(px, py);
const car = group(parkingGroup, label);
car.position.set(cx, groundHeight(cx, cz) + 0.035, cz);
// Parked nose-in towards the house, as drawn in the floor plan.
car.rotation.y = Math.PI;
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(14.4, 1.45, mats.car1, 'Orientační automobil 1');
addCar(14.4, 3.97, mats.car2, 'Orientační automobil 2');
}
// Section and elevations: soffit +2.766, eave +3.046, 23° roof planes with 0.6 m overhangs; all openings end at +2.300.
const SOFFIT = 2.766, EAVE = 3.046, HEAD = 2.3, OVERHANG = 0.6, ROOF_SLOPE = Math.tan(23 * RAD);
// Outer face of the external walls: kitchen/living wing 11.55 × 7.125 m with the covered entry recess on the road
// side, bedroom wing 9.3 × 10.875 m reaching the road line, terrace notch 3.75 m deep. Area 178.75 m².
const footprint = [[0, 0], [11.55, 0], [11.55, 6.525], [13.05, 6.525], [13.05, 17.4], [3.75, 17.4], [3.75, 7.125], [0, 7.125]]
.map(([px, py]) => planPoint(px, py));
const footprintSign = Math.sign(footprint.reduce((sum, [x, z], i) => {
const [x2, z2] = footprint[(i + 1) % footprint.length];
return sum + x * z2 - x2 * z;
}, 0));
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.295, height = SOFFIT, wallBottom = -0.30;
// Dimension chains are measured on the outer face, so the wall body sits inside the footprint line.
const inX = -dz / length * footprintSign * thickness / 2, inZ = dx / length * footprintSign * thickness / 2;
const positionAt = (distance) => [p1[0] + dx * distance / length + inX, p1[1] + dz * distance / length + inZ];
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(shellGroup, 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, wallBottom, height);
if (opening.bottom > wallBottom) wallSegment(left, right, wallBottom, 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;
const paneName = opening.kind === 'door' ? 'Vstupní dveře – zjednodušené' : opening.kind === 'glazedDoor' ? 'Prosklené dveře – zjednodušené' : 'Okno – zjednodušené';
box(shellGroup, paneName, 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(shellGroup, 'Rám otvoru', jx, opening.bottom + jambHeight / 2, jz, frameWidth, jambHeight, 0.09, mats.frame, angle);
}
for (let pane = 1; pane < (opening.panes || 1); pane++) {
const offset = (pane / opening.panes - 0.5) * (right - left);
const mullionX = x + dx / length * offset;
const mullionZ = z + dz / length * offset;
box(shellGroup, 'Svislý rám okna', mullionX, opening.bottom + jambHeight / 2, mullionZ, frameWidth, jambHeight, 0.09, mats.frame, angle);
}
box(shellGroup, 'Horní rám otvoru', x, top - frameWidth / 2, z, right - left, frameWidth, 0.09, mats.frame, angle);
if (opening.bottom > 0) box(shellGroup, 'Parapet okna', x, opening.bottom + frameWidth / 2, z, right - left, frameWidth, 0.09, mats.frame, angle);
cursor = right;
}
wallSegment(cursor, length, wallBottom, height);
}
// Eave lines in floor-plan metres. The higher roof spans the kitchen wing, covered entry and covered terrace
// (13.05 × 11.925 m → ridge +5.832); the lower roof spans the bedroom wing (9.3 m → ridge +5.275) and shares the
// road-side plane with the higher roof.
const roofPlan = {
south: -OVERHANG, north: 13.05 + OVERHANG, west: -OVERHANG,
highEast: 11.925 + OVERHANG, lowSouth: 3.75 - OVERHANG, east: 17.4 + OVERHANG
};
function roofHeight(px, py) {
const r = roofPlan, hip = (distance) => EAVE + distance * ROOF_SLOPE;
const high = px >= r.south && px <= r.north && py >= r.west && py <= r.highEast
? hip(Math.min(px - r.south, r.north - px, py - r.west, r.highEast - py)) : -Infinity;
const low = px >= r.lowSouth && px <= r.north && py >= r.west && py <= r.east
? hip(Math.min(px - r.lowSouth, r.north - px, py - r.west, r.east - py)) : -Infinity;
return Math.max(high, low);
}
function roofFace(name, outline, distance) {
const triangles = THREE.ShapeUtils.triangulateShape(outline.map(([px, py]) => new THREE.Vector2(px, py)), []);
const positions = [];
for (const [px, py] of outline) {
const [x, z] = planPoint(px, py);
positions.push(x, EAVE + distance(px, py) * ROOF_SLOPE, z);
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geometry.setIndex(triangles.flat());
geometry.computeVertexNormals();
return mesh(geometry, mats.roof, roofGroup, name);
}
function buildRoof() {
const r = roofPlan;
const highHalf = (r.highEast - r.west) / 2, lowHalf = (r.north - r.lowSouth) / 2;
const ridgePy = r.west + highHalf, ridgeSouth = r.south + highHalf, ridgeNorth = r.north - highHalf;
const lowRidgePx = r.lowSouth + lowHalf, lowRidgeEast = r.east - lowHalf, lowRidgeWest = r.highEast - lowHalf;
roofFace('Střecha – severní rovina nad vstupem a ložnicemi',
[[r.north, r.west], [r.north, r.east], [lowRidgePx, lowRidgeEast], [lowRidgePx, lowRidgeWest], [ridgeNorth, ridgePy]], (px) => r.north - px);
roofFace('Vyšší střecha – západní rovina',
[[r.south, r.west], [r.north, r.west], [ridgeNorth, ridgePy], [ridgeSouth, ridgePy]], (px, py) => py - r.west);
roofFace('Vyšší střecha – jižní valba nad terasou',
[[r.south, r.highEast], [r.south, r.west], [ridgeSouth, ridgePy]], (px) => px - r.south);
roofFace('Vyšší střecha – východní rovina nad terasou',
[[r.south, r.highEast], [r.lowSouth, r.highEast], [lowRidgePx, lowRidgeWest], [ridgeNorth, ridgePy], [ridgeSouth, ridgePy]], (px, py) => r.highEast - py);
roofFace('Nižší střecha – jižní rovina',
[[r.lowSouth, r.highEast], [r.lowSouth, r.east], [lowRidgePx, lowRidgeEast], [lowRidgePx, lowRidgeWest]], (px) => px - r.lowSouth);
roofFace('Nižší střecha – východní valba',
[[r.north, r.east], [r.lowSouth, r.east], [lowRidgePx, lowRidgeEast]], (px, py) => r.east - py);
const eaves = [[r.south, r.west], [r.north, r.west], [r.north, r.east], [r.lowSouth, r.east], [r.lowSouth, r.highEast], [r.south, r.highEast]]
.map(([px, py]) => planPoint(px, py));
const soffitGeometry = new THREE.ShapeGeometry(new THREE.Shape(eaves.map(([x, z]) => new THREE.Vector2(x, z))));
soffitGeometry.rotateX(Math.PI / 2);
const soffit = mesh(soffitGeometry, mats.soffit, roofGroup, 'Podbití střechy');
soffit.position.y = SOFFIT;
soffit.castShadow = false;
for (let i = 0; i < eaves.length; i++) {
const [ax, az] = eaves[i], [bx, bz] = eaves[(i + 1) % eaves.length];
box(roofGroup, 'Čelní prkno a okap', (ax + bx) / 2, (SOFFIT + EAVE) / 2, (az + bz) / 2,
Math.hypot(bx - ax, bz - az) + 0.06, EAVE - SOFFIT + 0.04, 0.06, mats.frame, Math.atan2(-(bz - az), bx - ax));
}
// The chimney stands over the stove flue (plan 7.58 / 4.76); its top at +6.500 is 1.255 m above the roof on the low side.
const [chimneyX, chimneyZ] = planPoint(7.582, 4.763);
const roofAt = (x, z) => roofHeight(z + 6.525, 8.7 - x);
slopedBox(roofGroup, 'Oplechování komína', chimneyX, chimneyZ, 0.5, 0.5, roofAt, (x, z) => roofAt(x, z) + 0.06, mats.metal);
slopedBox(roofGroup, 'Komín', chimneyX, chimneyZ, 0.36, 0.36, (x, z) => roofAt(x, z) + 0.04, () => 6.38, mats.foundation);
box(roofGroup, 'Hlavice komína', chimneyX, 6.44, chimneyZ, 0.46, 0.12, 0.46, mats.frame);
}
function timberPost(parent, px, py, base) {
const [x, z] = planPoint(px, py);
box(parent, 'Sloupek', x, (base + SOFFIT) / 2, z, 0.14, SOFFIT - base, 0.14, mats.timber);
}
function timberBeam(parent, from, to) {
const [ax, az] = planPoint(...from), [bx, bz] = planPoint(...to);
box(roofTimber, 'Trám', (ax + bx) / 2, SOFFIT - 0.1, (az + bz) / 2, Math.hypot(bx - ax, bz - az), 0.2, 0.14, mats.timber, Math.atan2(-(bz - az), bx - ax));
}
function buildHouse() {
makeFootprintSlab();
const win = (center, width, sill, panes = 1) => ({ center, width, bottom: sill, height: HEAD - sill, panes });
const door = (center, width, kind = 'glazedDoor', panes = 1) => ({ center, width, bottom: 0, height: HEAD, kind, panes });
// Openings per footprint edge, centred from the first corner of the edge along the outer face (plan dimension chains).
// "1300 (1000)" windows are 1.3 m high on a 1.0 m sill, "1000 (1300)" windows 1.0 m high on a 1.3 m sill.
const openings = [
// West façade: 1295 | 1800 window | 2105 | 1800 window | 1399 | 1100 door | 2050.
[win(2.195, 1.8, 1.0, 2), win(6.1, 1.8, 1.0, 2), door(8.949, 1.1)],
// Recessed road side under the covered entry: 5050 wall, then the 1500 entrance with a sidelight.
[win(5.26, 0.42, 0), door(5.9975, 0.955, 'door')],
[],
// Road side of the bedroom wing: 1260 | 800 | 2095 | 1300 | 1170 | 1800 | 2450, all on 1.3 m sills.
[win(1.66, 0.8, 1.3), win(4.805, 1.3, 1.3), win(7.525, 1.8, 1.3, 2)],
// East gable: 900 | 1800 window | 6600.
[win(1.8, 1.8, 1.0, 2)],
// Garden side of the bedroom wing: 1675 | 1800 door | 900 | 1100 door | 2100 | 1800 door | 900.
[door(2.575, 1.8, 'glazedDoor', 2), door(4.925, 1.1), door(8.475, 1.8, 'glazedDoor', 2)],
// Living room to the terrace: 2700 sliding door from the inner corner, then 1075 wall.
[door(1.375, 2.65, 'glazedDoor', 2)],
// South wall of the kitchen: 575 | 1100 door | 5450.
[door(1.125, 1.1)]
];
for (let i = 0; i < footprint.length; i++) makeWallEdge(footprint[i], footprint[(i + 1) % footprint.length], openings[i]);
buildRoof();
// Covered terrace 3.75 × 4.8 m under the higher roof, with one corner post (plan 0.14 / 11.78) and beams under the soffit.
const terrace = group(houseGroup, 'Krytá terasa');
const [tx, tz] = planPoint(3.75 / 2, (7.125 + 11.925) / 2);
box(terrace, 'Podlaha terasy', tx, -0.175, tz, 11.925 - 7.125, 0.25, 3.75, mats.terrace);
timberPost(terrace, 0.144, 11.778, -0.05);
timberBeam(terrace, [0.144, 7.125], [0.144, 11.85]);
timberBeam(terrace, [0.07, 11.778], [3.75, 11.778]);
// Covered entry 1.5 × 6.525 m in front of the recessed road side, with three posts from the floor plan.
const entry = group(houseGroup, 'Krytý vstup');
for (const [px, py] of [[11.617, 0.147], [12.901, 0.147], [12.901, 6.455]]) timberPost(entry, px, py, -0.27);
timberBeam(entry, [12.901, 0.07], [12.901, 6.525]);
timberBeam(entry, [11.55, 0.147], [12.97, 0.147]);
}
buildGround();
buildHouse();
function viewportSize() {
const rect = canvas.getBoundingClientRect();
return [Math.max(1, rect.width), Math.max(1, rect.height)];
}
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);
interior?.resize();
}
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.8, 90, -1], target: [0.8, 0.1, -1], label: 'Pozemek / shora' }
};
function setPreset(name) {
if (interior?.enabled) interior.exit();
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 = `<defs><linearGradient id="solarFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#d4b465" stop-opacity=".34"/><stop offset="1" stop-color="#d4b465" stop-opacity=".04"/></linearGradient></defs><line x1="8" y1="70" x2="296" y2="70" stroke="#dce2d3"/><line x1="8" y1="40.75" x2="296" y2="40.75" stroke="#e4e8dd" stroke-dasharray="3 4"/><text x="294" y="37" text-anchor="end" fill="#a1a891" font-size="8">45°</text><path d="${curve} L296,70 L8,70 Z" fill="url(#solarFill)"/><path d="${curve}" fill="none" stroke="#c29b4b" stroke-width="1.5"/>`;
}
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 + `<line x1="${x}" y1="10" x2="${x}" y2="75" stroke="#a97731" stroke-width="1" stroke-dasharray="2 3"/><circle cx="${x}" cy="${y}" r="4" fill="#bc8933" stroke="#fafbf7" stroke-width="2"/>`;
}
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', () => {
Object.assign(site, SITE_DEFAULT);
$('#sunLat').value = site.latitude; $('#sunLon').value = site.longitude; $('#siteBearing').value = site.bearing;
modelRoot.rotation.y = -site.bearing * RAD;
rebuildSolarDay(true);
});
$('#compassGroup').insertAdjacentHTML('beforeend', '<circle id="compassSun" cx="42" cy="16" r="3" fill="#c89439" stroke="#fff7d7" stroke-width="1"/>');
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="${viewWidth}" height="${source.height / ratio}" viewBox="0 0 ${viewWidth} ${source.height / ratio}"><style>text{font:10px Arial;fill:#96712e;paint-order:stroke;stroke:#f3f4e9;stroke-width:3px;stroke-linejoin:round}</style>${$('#sunOverlay').innerHTML}</svg>`;
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í · parcela 66/17, 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);
let exporting = false;
function exportGlb() {
if (exporting) return;
exporting = true;
const button = $('#saveGlb'); button.disabled = true;
const exporter = new GLTFExporter();
modelRoot.updateMatrixWorld(true);
exporter.parse(modelRoot, (result) => {
saveBlob(new Blob([result], { type: 'model/gltf-binary' }), interior?.enabled ? 'klabava-interier.glb' : 'klabava-pracovni-model.glb');
toast('Model Klabava GLB je připravený ke stažení.');
exporting = false; button.disabled = false;
}, (error) => { console.error(error); toast('Model se nepodařilo exportovat.'); exporting = false; button.disabled = false; }, { binary: true, onlyVisible: true, trs: false });
}
$('#saveGlb').addEventListener('click', exportGlb);
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 = `<path d="${path}" fill="none" stroke="#b28b40" stroke-width="1.5" stroke-dasharray="5 5" opacity=".8"/>`;
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 += `<circle cx="${x}" cy="${y}" r="2.5" fill="#bd984f"/><text x="${x + 6}" y="${y - 7}">${hour}:00</text>`;
}
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 += `<circle cx="${x}" cy="${y}" r="18" fill="#e2bc5c" opacity=".13"/><circle cx="${x}" cy="${y}" r="6.5" fill="#e7bd59" stroke="#fff5cf" stroke-width="2"/>`;
}
}
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);
if (!interior?.walking) controls.update();
interior?.frame(dt);
const cameraHeading = Math.atan2(camera.position.x - controls.target.x, camera.position.z - controls.target.z) / RAD;
// North is +z, so a camera on the north side looking south shows north at the bottom of the compass.
$('#compassGroup').setAttribute('transform', `rotate(${cameraHeading + 180},42,42)`);
renderer.render(scene, camera);
drawSunPath();
}
window.addEventListener('keydown', event => {
if (interior?.enabled) { interior.keydown(event); return; }
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.toLowerCase() === 'i') interior?.enter();
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 {
interior = installInterior({THREE,houseGroup,modelRoot,shellGroup,roofGroup,roofTimber,controls,scene,renderer,planPoint,box,mesh,group,groundGroup,parkingGroup,boundaryGroup,resize,saveBlob,exportGlb,solar,setSolarPlaying,toggleInfo,toggleSun,getCamera:()=>camera,setCamera:value=>{camera=value;}});
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.<br>${error.message}<br><small>Spusť web přes místní server a ověř přítomnost složky vendor/three.</small>`;
$('#webStatus').textContent = '3D není dostupné';
}
-23
View File
@@ -1,23 +0,0 @@
// 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 };
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-35
View File
@@ -1,35 +0,0 @@
import assert from 'node:assert/strict';
import * as THREE from '../vendor/three/build/three.module.js';
import {buildInterior,pointInPolygon} from '../js/interior-model.js';
import {furnishInterior} from '../js/interior-furniture.js';
const houseGroup=new THREE.Group();
const planPoint=(px,py)=>[8.7-py,px-6.525];
const group=(parent,name)=>{const g=new THREE.Group();g.name=name;parent.add(g);return g;};
const mesh=(geometry,material,parent,name)=>{const m=new THREE.Mesh(geometry,material);m.name=name;parent.add(m);return m;};
const box=(parent,name,x,y,z,sx,sy,sz,material,rotationY=0)=>{const m=mesh(new THREE.BoxGeometry(sx,sy,sz),material,parent,name);m.position.set(x,y,z);m.rotation.y=rotationY;return m;};
const ctx={THREE,houseGroup,planPoint,group,mesh,box};
const data=buildInterior(ctx),furniture=furnishInterior({...ctx,parent:data.furniture});
assert.equal(data.rooms.length,11);assert.ok(Math.abs(data.rooms.reduce((a,r)=>a+r.area,0)-153.74)<1e-6);
const footprint=[[0,0],[11.55,0],[11.55,6.525],[13.05,6.525],[13.05,17.4],[3.75,17.4],[3.75,7.125],[0,7.125]].map(p=>planPoint(...p));
const distance=(x,z,a,b)=>{const dx=b[0]-a[0],dz=b[1]-a[1],t=Math.max(0,Math.min(1,((x-a[0])*dx+(z-a[1])*dz)/(dx*dx+dz*dz)));return Math.hypot(x-a[0]-t*dx,z-a[1]-t*dz);};
function canStand(x,z,withFurniture){
if(!pointInPolygon(footprint,x,z))return false;
for(let i=0;i<footprint.length;i++)if(distance(x,z,footprint[i],footprint[(i+1)%footprint.length])<.455)return false;
for(const b of [...data.wallColliders,...(withFurniture?furniture.colliders:[])])if(x>b.minX-.16&&x<b.maxX+.16&&z>b.minZ-.16&&z<b.maxZ+.16)return false;
return true;
}
// Flood fill uses a person of radius 160 mm, not a point; all room spawns must be reachable from the foyer.
for(const withFurniture of [false,true]){
const step=.08,w=221,h=170,x0=-8.8,z0=-6.6,walkable=new Uint8Array(w*h),visited=new Uint8Array(w*h);
for(let j=0;j<h;j++)for(let i=0;i<w;i++)walkable[j*w+i]=canStand(x0+i*step,z0+j*step,withFurniture)?1:0;
const nearest=([x,z])=>Math.round((z-z0)/step)*w+Math.round((x-x0)/step);
const first=nearest(planPoint(...data.rooms.find(r=>r.id==='entry').spawn));assert.equal(walkable[first],1);
const q=[first];visited[first]=1;
for(let n=0;n<q.length;n++){const v=q[n];for(const next of [v-1,v+1,v-w,v+w])if(next>=0&&next<w*h&&!visited[next]&&walkable[next]){visited[next]=1;q.push(next);}}
for(const room of data.rooms){const p=planPoint(...room.spawn);assert.ok(canStand(...p,withFurniture),'Spawn collides: '+room.id);assert.equal(visited[nearest(p)],1,'Room unreachable from entry: '+room.id+' furniture='+withFurniture);}
console.log('All 11 rooms reachable with furniture '+withFurniture+'; walkable cells '+q.length);
}
let count=0,triangles=0;houseGroup.traverse(o=>{if(!o.isMesh&&!o.isLineSegments)return;count++;const p=o.geometry.getAttribute('position');for(const n of p.array)assert.ok(Number.isFinite(n));if(o.isMesh)triangles+=(o.geometry.index?.count??p.count)/3;});
const floor=data.floors.children.find(o=>o.isMesh);assert.ok(floor.geometry.getAttribute('normal').getY(0)>.99,'Floor must face up');
const report={rooms:data.rooms.length,areaM2:153.74,objects:count,triangles,wallColliders:data.wallColliders.length,furnitureColliders:furniture.colliders.length,connectivity:'PASS in furnished and empty modes',geometry:'PASS finite coordinates and upward floor normals'};
console.log(JSON.stringify(report,null,2));
-21
View File
@@ -1,21 +0,0 @@
The MIT License
Copyright © 2010-2025 three.js authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-334
View File
@@ -1,334 +0,0 @@
{
"README_CZ.md": {
"bytes": 8392,
"sha256": "19e19ce4c58e8f5d052d930c78e0442dfa7bf62ed22dd483750c8a422a483ef7"
},
"TEST_REPORT.json": {
"bytes": 2154,
"sha256": "38d04a56f405d48a916cf8ec456d34d0fe997fd52b21d0d4f296cd1d9c25d436"
},
"THIRD_PARTY.txt": {
"bytes": 496,
"sha256": "0dab04f18aaefe62937d313eb9ff128cf279687da956ac1593e3d8387c196142"
},
"blender/prepare_model.py": {
"bytes": 3342,
"sha256": "0a461d270effa128a3c44a58bd96e7cf52b771b396a7793bd496f596c9854d1b"
},
"locenice/app.html": {
"bytes": 36841,
"sha256": "6e347d9245e3e072d4bb6bab7212464957fbc44ebb338827b8ce6a72c652cc15"
},
"locenice/assets/catalog/door0.jpg": {
"bytes": 1828,
"sha256": "9161b2f44a4a5118fa7256332efe8e393a8e6f964b03f5cf923ca15e2829d18e"
},
"locenice/assets/catalog/door1.jpg": {
"bytes": 1910,
"sha256": "036bce257db9690e0ae6d870c962d59d22854c585cb3849ace3bc3ea2e292cdb"
},
"locenice/assets/catalog/door2.jpg": {
"bytes": 1859,
"sha256": "b800bde543e2e3a658ad39b225fb2ab87e104c03a11cc24bef1b1febe9c3412c"
},
"locenice/assets/catalog/door3.jpg": {
"bytes": 2131,
"sha256": "6fb178efd1f827c39c44fa074fc13aadca327271b03539142a4345aee6a5447c"
},
"locenice/assets/catalog/door4.jpg": {
"bytes": 2209,
"sha256": "8168c67a52d57d6e30af0f9211ebbb5bd36330a810bfb6d9965c3526f951668e"
},
"locenice/assets/catalog/door5.jpg": {
"bytes": 2091,
"sha256": "6bb82a9208873c21e47d1df04391990c9678cbbd620679dad855cab43947224c"
},
"locenice/assets/catalog/door6.jpg": {
"bytes": 2183,
"sha256": "e23d18692d5a02173d24172c6928fe29fafd1e42d0b219fe283dbf2bd9eed5a1"
},
"locenice/assets/catalog/door7.jpg": {
"bytes": 2142,
"sha256": "27a3f4392ee3aef6584bd0074399088c99a1ac706b6965caec1b61f362875561"
},
"locenice/assets/catalog/intsill0.jpg": {
"bytes": 3997,
"sha256": "4d07213542ee0f2a53a10d8f22e00f0b874fdfca548a8f6d995ccb7060e513c1"
},
"locenice/assets/catalog/intsill1.jpg": {
"bytes": 3818,
"sha256": "f12e3283459b853d5418d96a151bb49681bda4d4e00ced1951c04eea22d34a5c"
},
"locenice/assets/catalog/intsill2.jpg": {
"bytes": 3721,
"sha256": "4308a8db2270f71c057ca4501cc1b08f71d47490e4ba7c37984012d5ff50689f"
},
"locenice/assets/catalog/lam1.jpg": {
"bytes": 2820,
"sha256": "e01b9ee523b114b7f02df037c828445d1afbff1313322fa60577a68bef710d24"
},
"locenice/assets/catalog/lam2.jpg": {
"bytes": 3011,
"sha256": "7f37991b35cc44e7eb8ce1f86b7679df8b2ac22f24deed7b110afd8f57ee1654"
},
"locenice/assets/catalog/lam3.jpg": {
"bytes": 2676,
"sha256": "d91daf17cf54063ce90bc54a809b273c36c8e14768b035454f561aef2b591d78"
},
"locenice/assets/catalog/lam4.jpg": {
"bytes": 2678,
"sha256": "8eab4cecb5083c1135b4f5186f34e19ebc54cb7aa2d5a1366475ba190287c703"
},
"locenice/assets/catalog/lam5.jpg": {
"bytes": 1817,
"sha256": "2d12cd34fa4c3e02c66b0db5bc7a11f35470f6f65e4a194dfbf4c48cf45e6a1d"
},
"locenice/assets/catalog/lam6.jpg": {
"bytes": 2468,
"sha256": "0c68c1a8273cb5545c195e20394b211ac48d4f77c12a9393492ec4a7b8af7167"
},
"locenice/assets/catalog/lam7.jpg": {
"bytes": 2496,
"sha256": "b080bf3ece148b3f210a61af8e8b7c5156bda5e8d4c1ee204122c0791dd91793"
},
"locenice/assets/catalog/lam8.jpg": {
"bytes": 3304,
"sha256": "13de01ed496dffec443527b174c91ba2b70682aa57f4f71a6abc8d93dea949bd"
},
"locenice/assets/catalog/lam9.jpg": {
"bytes": 2219,
"sha256": "0d0921cbd7c8d4ddd53c5ad60f9ce1d576588f6646feea0d3b3fd56d6b03d219"
},
"locenice/assets/catalog/page12.jpg": {
"bytes": 148801,
"sha256": "063672ea2626ee0e3935fe59ba3e790c8df71393766e042182192f7f029f2f56"
},
"locenice/assets/catalog/page13.jpg": {
"bytes": 127287,
"sha256": "108ac1321f652d4280f8855ac3c8cddc70d2533066730aca0eb6f4a20c458cce"
},
"locenice/assets/catalog/page14.jpg": {
"bytes": 146019,
"sha256": "cdcacfd61673e20f3b8c5de2225991d68b00aefac63b912d79f5d2819401d48e"
},
"locenice/assets/catalog/page15.jpg": {
"bytes": 117331,
"sha256": "c8f8f3874a0d4c21744299931cf2317cf2b0aabf9ac762060251e05dc3fe6da0"
},
"locenice/assets/catalog/page16.jpg": {
"bytes": 128511,
"sha256": "e1d4f27404554729a2d1ad718244b4941cf38e223b023648b4a573cfe10a5cbd"
},
"locenice/assets/catalog/page17.jpg": {
"bytes": 151989,
"sha256": "3dc907b795c22abf260398b9816acc11ab47b11e7b0f503df592ed0b66a5ba1a"
},
"locenice/assets/catalog/page18.jpg": {
"bytes": 125562,
"sha256": "c3a2304c8daa3a0f76417a4622ba8f565bad99950323cd74dff45b3dffa9e28d"
},
"locenice/assets/catalog/page19.jpg": {
"bytes": 119551,
"sha256": "6440a8c153f03e2270a8ffbc98edb54aa4636ab6e6bcc2e259cf1e44455d095c"
},
"locenice/assets/catalog/page20.jpg": {
"bytes": 106137,
"sha256": "b1aeaa729e4fb2d6ba784a861d6d101f9e675e77901d49ba86d7eb010b1960dc"
},
"locenice/assets/catalog/page21.jpg": {
"bytes": 91231,
"sha256": "3385abbcbfebc59edf42a631f671e5af7be9901b8944797ad4bb0b9a4ac0d634"
},
"locenice/assets/catalog/page22.jpg": {
"bytes": 94318,
"sha256": "206988c563e47b2263271878a7ac7efcd4b2bbd4a9615f1b7454a4fac9e4a449"
},
"locenice/assets/catalog/page23.jpg": {
"bytes": 129965,
"sha256": "e7d82787f8af877884c92e637cd829ba85eb8c508a6268054c12ac87bec3eea2"
},
"locenice/assets/catalog/page24.jpg": {
"bytes": 157070,
"sha256": "bff6ac7315c114fa69944696fec3df7057868f1ddf9a294ae55e13514833139c"
},
"locenice/assets/catalog/page25.jpg": {
"bytes": 137049,
"sha256": "00df86ed78e0aa8cc2389733d33bffd3fb5f2bf6cddacf95381d630ecc81f0ae"
},
"locenice/assets/catalog/prum.jpg": {
"bytes": 2656,
"sha256": "cc47e7ea76e63aeee82ad3d6c4fdd8c048f84539361afaf55473af914e146409"
},
"locenice/assets/catalog/requestedDoor.jpg": {
"bytes": 3790,
"sha256": "ba8ca3f723e3a0593615aa2a888df44ad872f7a9c6cc9f3eb6a4641865e3d43f"
},
"locenice/assets/catalog/roof0.jpg": {
"bytes": 4599,
"sha256": "984511ec7fd693452a9118e3b4bf6d7a9735c55762ca962cc0bc9e54c69d549f"
},
"locenice/assets/catalog/roof1.jpg": {
"bytes": 3969,
"sha256": "d27467601eba1ce2190ce8707f2371cc99372cfbe643747021b1830808c4ad5c"
},
"locenice/assets/catalog/roof2.jpg": {
"bytes": 4326,
"sha256": "b09ec26315ace228c62b978e708f4a461d68deb4a2f8dac405ce3b8af498e29e"
},
"locenice/assets/catalog/roof3.jpg": {
"bytes": 4106,
"sha256": "f743e6bcf4a0510986192e55a807cf13e70c5b93cdb5352dc50a00c5b8e3ad6b"
},
"locenice/assets/catalog/roof4.jpg": {
"bytes": 3961,
"sha256": "b0c034d4072c19696e4198963123e347a038b5f8346969f947f0dcec39b1cd9d"
},
"locenice/assets/catalog/tile1.jpg": {
"bytes": 1968,
"sha256": "5c786000fe2027ea5f5cf42a6976562a506dbc47099eb428865d38736e4006f2"
},
"locenice/assets/catalog/tile2.jpg": {
"bytes": 2829,
"sha256": "ff310d5fd6279233ba4bd21e17f16dcfd7bb5038bfb2f09971e3bed386b9ac64"
},
"locenice/assets/catalog/tile3.jpg": {
"bytes": 2566,
"sha256": "49f57228671e28141e96f3f02b43f420c5a42d6e930e42ff21e3baa08f4ca246"
},
"locenice/assets/catalog/tile4.jpg": {
"bytes": 2536,
"sha256": "54cbdfe7031c6da5b9a55d223f31c98fcbd23e5302a30804bc9352cb6969ff1c"
},
"locenice/assets/catalog/tile5.jpg": {
"bytes": 2832,
"sha256": "ee225992e87178f635010bb90c9ccff0557394e29c915987878e038fb8e5c99a"
},
"locenice/assets/catalog/tile6.jpg": {
"bytes": 3423,
"sha256": "d18ba495c6e6e06459ded48a6f87bfa824dc1fa83315db926fda263bac2856ed"
},
"locenice/assets/catalog/tile7.jpg": {
"bytes": 2282,
"sha256": "3b0f42f66264ec5dfc11467dc4905042cb01448bcaee725f49cf4632f75a56bd"
},
"locenice/assets/catalog/tile8.jpg": {
"bytes": 2338,
"sha256": "81f972783c5abc5c6e4d895f0778a1650df5c7d1df8a3bd960c5ff73680a0f5f"
},
"locenice/assets/catalog/tile9.jpg": {
"bytes": 2632,
"sha256": "9bb6ddaf21bb0e7ede49888d365b3f37c6ffc8045040c0a70f0336e21b4ffdbb"
},
"locenice/assets/references/kitchen1.jpg": {
"bytes": 100673,
"sha256": "90e8b220093eae30564f8cd979a5bb1b454d58032abbe431e747cdc50804fc13"
},
"locenice/assets/references/kitchen2.jpg": {
"bytes": 332927,
"sha256": "4617eb7ab1b967adb9e54daf4abf55207fb6819d29fb4973cd9126262ae50c0d"
},
"locenice/assets/references/plan.jpg": {
"bytes": 588857,
"sha256": "27f84372a48c41c645ef751ab78ebde43b2fac22cc7b46e1393038967a1d311a"
},
"locenice/assets/references/survey.png": {
"bytes": 207493,
"sha256": "e6fb9c57df1d53241846ff99362ca93899509cdb5564089f93993ed2646c1b86"
},
"locenice/assets/references/view1.jpg": {
"bytes": 221160,
"sha256": "2ae4fa95cca2ec52716a821e28956d48f0752aa148fcfb9a8c3b69956a55d6bc"
},
"locenice/assets/references/view2.jpg": {
"bytes": 159227,
"sha256": "4fef97b2d48568e7538e1c88084cf46341b843e4c2ebabe11ed9354040178bb8"
},
"locenice/assets/ui/image-1.jpg": {
"bytes": 10404,
"sha256": "db0fef3bc718c8feb15612d250384d189f8d311eed64c3837681c852c74e0c65"
},
"locenice/css/app.css": {
"bytes": 63876,
"sha256": "453197f4a027cd1585074fc3d8da5f4259c15b38fa755358daf7315d973b4728"
},
"locenice/css/web.css": {
"bytes": 3260,
"sha256": "4218b686835e38ed44b6a9d58cba4c284dc95e4c7f4e450d9b0f28b1733fe350"
},
"locenice/data/app-config.json": {
"bytes": 528,
"sha256": "7378244aea1cdce3c24dfb3e04241d7908326ca85ded6ac6b672aa29b81a0916"
},
"locenice/data/assets.json": {
"bytes": 2094,
"sha256": "c2d7822874607330c2dd256c4b7e9e56ac29147b7f8f49bf7c97aba449bd3af5"
},
"locenice/data/catalog.json": {
"bytes": 7798,
"sha256": "719958ad36182df4dd0f19307fb5e083c3ab0c8e83250289e8fa3cf208e14f95"
},
"locenice/data/references.json": {
"bytes": 1328,
"sha256": "9e3fd1b5e3c8db02cf63065b443cb140043896af3dd6dc1aaad87b7247c54b61"
},
"locenice/data/scene.json": {
"bytes": 31475,
"sha256": "a8f5d8bf68be9b73e04b27347b3dca6d3d954a4f0e8f2b35fa2db55a9d2c9089"
},
"locenice/data/site.json": {
"bytes": 5897,
"sha256": "b5222c0fe12d4192f51f74a233e89b97b462ec2dc259718c8b7a0260adfa85b3"
},
"locenice/index.php": {
"bytes": 348,
"sha256": "c473e40e59838adc34902a4d223f592ac6f79ecadb12ddc1374b76bdb64128db"
},
"locenice/js/app.js": {
"bytes": 118457,
"sha256": "cc89e78969e9cdf1672661e1b501722da53f4969ca2db04ae2e2b2b2dddb6625"
},
"locenice/js/bootstrap.js": {
"bytes": 2186,
"sha256": "daef518d18425f86d9daaa9fb01dceb5671ca17afcee8c37658886b8d2e5934c"
},
"locenice/js/legacy-renderer.js": {
"bytes": 12513,
"sha256": "04b7fe36cbc2af577849abf6cdaf497ce35c8203f3982faf81e62c40c7a39063"
},
"locenice/js/legacy-shaders.js": {
"bytes": 3481,
"sha256": "65b346b36da103b89343e5445ff0959cba90f2c55227d6851391bad72d1481c6"
},
"locenice/js/model-io.js": {
"bytes": 1374,
"sha256": "25b03a405d0c6d7eba6b49352f7dfb11e26d7c8eb2e953075829be19f4209d53"
},
"locenice/js/project-panel.js": {
"bytes": 3795,
"sha256": "241742a4ebb764af21596fe6cfe6fcf45082dfeb38add1a76bf1c5950896654c"
},
"locenice/js/solar.js": {
"bytes": 1894,
"sha256": "14ae60b6740d645febf593773b321a6d3e61abf133b4956297c695518c41eb12"
},
"locenice/js/three-renderer.js": {
"bytes": 8428,
"sha256": "dad3161a5e809a7a467e96853bf65b4bb1dcc3c9a2109fdf2e1ea835e0898a67"
},
"locenice/models/dum.glb": {
"bytes": 17414380,
"sha256": "0a0bf5ccceb5cddc11a7770531990b6fecdd57f82d9cd84158ad64b5e7548324"
},
"tests/data_checks.mjs": {
"bytes": 2012,
"sha256": "bf01da3b9aab0c7517aae1219866999015ec2c2110e0736d8f7d879ca3fc01a5"
},
"tools/prepare_export.py": {
"bytes": 6619,
"sha256": "22e5b6151cac9a8c687f8f3a0007ea116fa9208662164297a611ae6bfdc366cd"
},
"tools/vendor_three.py": {
"bytes": 2017,
"sha256": "197501bc7f0ef169a43265b19ad4bb8441ea5b90db9d6ade1b535df9a3cf6f90"
}
}
-148
View File
@@ -1,148 +0,0 @@
# Ločenice / webový model pro PHP hosting
## Nahrání na web
1. Zálohujte dosavadní složku `locenice`.
2. Nahrajte **celý obsah přiložené složky `locenice/`** do stejné složky na hostingu.
3. Otevřete `/locenice/` nebo `/locenice/index.php` a obnovte stránku bez cache.
PHP jen vrací HTML. Pro samotný provoz nepotřebujete Node.js, npm, databázi ani sestavování aplikace.
Složky `blender`, `tools` a `tests` na hosting nenahrávejte; jsou určené pro úpravy na počítači.
Základ `index.php` zůstává stejný jako dosud:
```php
<?php
header('Content-Type: text/html; charset=utf-8');
readfile(__DIR__ . '/app.html');
```
Dodaný `index.php` navíc kontroluje existenci souboru a posílá hlavičku `nosniff`.
Relativní cesty počítají s tím, že `index.php` i `app.html` jsou ve stejné složce.
Nenahrávejte pouze PHP nebo pouze HTML. Stránka potřebuje i `js`, `css`, `data`, `models` a `assets`.
Na běžném Apache není nutné přidávat `.htaccess` ani přesměrování.
Server musí vydávat `.js` jako JavaScript, `.json` jako JSON a umožnit stažení `.glb`.
## Co je kde
| Soubor / složka | Účel |
| --- | --- |
| `locenice/index.php` | Vstup na dosavadním PHP hostingu. |
| `locenice/app.html` | Struktura stránky; už bez vloženého modelu. |
| `locenice/models/dum.glb` | Celý zdrojový model, včetně alternativ a textur. |
| `locenice/data/catalog.json` | Možnosti standardů, názvy, barvy a výchozí volby. |
| `locenice/data/scene.json` | Vazby materiálů, místnosti, kamery, kolizní obrysy a metadata. |
| `locenice/data/site.json` | 27 bodů výškopisu a zvlášť souřadnice z geometrického plánu. |
| `locenice/data/app-config.json` | Model, renderer, cesty a maximální hustota vykreslování. |
| `locenice/assets/` | Samostatné obrázky katalogu a referencí. |
| `locenice/js/app.js` | Ovládání, pohledy, stav a pravidla konfigurátoru. |
| `locenice/js/three-renderer.js` | Nový renderer nad Three.js. |
| `locenice/js/legacy-renderer.js` | Lokální záloha nad původním WebGL. |
| `locenice/js/solar.js` | Oddělený výpočet polohy slunce. |
| `locenice/js/model-io.js` | Načtení a zápis kontejneru GLB. |
| `locenice/css/` | Vzhled aplikace. |
## Three.js a záložní renderer
Výchozí `renderer: "auto"` nejprve zkusí Three.js **0.180.0 / r180**, přesně připnutou verzi z CDN jsDelivr.
Model, katalog ani fotografie se na CDN neposílají. Prohlížeč odtud stahuje knihovnu, takže jde o externí síťový požadavek.
Knihovna Three.js není fyzicky součástí tohoto ZIPu. Při nedostupnosti CDN nastoupí lokální renderer.
Nejde tedy o úplné odstranění původního vykreslování: je zachované jako oddělená záloha.
Obě cesty vyžadují WebGL 2; zablokovanou grafiku prohlížeče záloha neobejde.
V levém dolním rohu nebo v dialogu **Projekt a podklady** je uveden aktivní renderer.
Vynucené režimy pro diagnostiku: `index.php?renderer=three` a `index.php?renderer=legacy`.
### Volitelně vše z vlastního hostingu
Na počítači s Pythonem a připojením k internetu spusťte z kořene balíčku:
```sh
python tools/vendor_three.py
```
Skript stáhne připnuté soubory a jejich relativní závislosti z oficiálního repozitáře Three.js, včetně licence.
Potom nahrajte vytvořenou složku `locenice/vendor` a upravený `locenice/data/app-config.json`.
Stahovací skript se nespouští na webu a neslouží jako veřejný instalátor.
Jeho síťovou část zde kvůli blokovanému připojení nebylo možné ověřit.
## Model, projekt a dodané fotografie
Stavební geometrie i svažitý terén jsou převzaté z dodaného modelu ALLSTAV v2.
Neproběhlo nové zaměření ani kompletní přemodelování domu z PDF.
Kuchyň a obývací prostor dosavadního modelu zůstávají včetně zelených čel, desky, vinotéky, vybavení a trámů.
Čtyři nově dodané obrázky jsou dostupné v dialogu pro porovnání.
Obrázky nejsou zaměřené ani mezi sebou zcela shodné; jejich odlišné rámy a dispozice neřídí přesun oken.
Nejde o fotorealistickou rekonstrukci, BIM, výrobní návrh kuchyně nebo dokumentaci pro stavbu.
Výškopis je dostupný jako podklad a lze zapnout jeho body nad 3D modelem.
Výšková nula je 591,840 m Bpv. Grafické x/z polohy 27 bodů jsou převzaté z dosavadního modelu.
Souřadnice S-JTSK z geometrického plánu jsou uložené odděleně, nepředstírají souřadnice vytyčení domu.
Doklad uvádí parcelu 3480/37 o výměře 1 010 m²; dosavadní grafický obrys modelu má 987,89 m².
Tento rozdíl je uvedený, nikoli skrytě odstraněný deformací modelu.
Výšky upraveného terénu u domu a původní zaměření nejsou zaměnitelné.
## Standardy a sdílení
Zachované volby: dveře, parapety, okapy, krytina, žaluzie, podlahy po místnostech, obklady,
malba, vnitřní dveře, kuchyň, sprcha a další specifikace.
Žaluzie se týkají devíti obdélníkových otvorů, nikdy trojúhelníku nebo vstupních dveří.
Výchozí barvy jsou RAL 7016 / antracit / CERN 015 / tmavě šedivá.
Na webu je export samostatného HTML nahrazen **odkazem s výběrem**.
Odkaz otevře tento web s vloženým nastavením. Obsahuje i poznámku pro dodavatele; nevkládejte do ní citlivé údaje.
Nastavení je ve fragmentu URL za `#`; neukládá se do serverové databáze.
Zachovaný je export/import JSON, TXT a export viditelného modelu GLB; PNG zachycuje 3D canvas.
Soubor `models/dum.glb` je kompletní editovatelný zdroj. Export **viditelného výběru** ho nenahrazuje.
Dosavadní JSON výběry lze importovat. Lokální uložení je pouze v daném prohlížeči.
Změna výchozích hodnot v katalogu nepřepíše již uložený výběr; použijte tlačítko obnovy výchozích voleb.
## Úpravy v Blenderu
Soubor `.blend` v balíčku není. Přiložený skript jej vytvoří ve vašem Blenderu:
```sh
blender --background --python blender/prepare_model.py
```
Výsledek: `blender/dum_locenice.blend`. Skript vytvoří pojmenované kolekce a skryje neaktivní varianty.
Skript zde nebyl spuštěn, protože Blender není v prostředí nainstalovaný.
Alternativně importujte `locenice/models/dum.glb` do Blenderu ručně.
Neslučujte vše do jednoho objektu ani materiálu. Zachovejte vlastnosti `loceniceId`, `interiorLayer`, `stdWhen`, `stdBlind` a `windowNode`.
Při exportu do GLB zapněte vlastní vlastnosti / **Custom Properties (extras)**.
Exportujte všechny varianty, nikoli jen viditelné objekty; vypněte filtr **Visible / Renderable / Selected objects only**.
Pak spusťte:
```sh
python tools/prepare_export.py upraveny_model.glb --out pripraveno
```
Nástroj znovu naváže čísla objektů a materiálů na stabilní ID a doplní celý katalog textur.
Po záloze webu vyměňte **oba** soubory `models/dum.glb` a `data/scene.json` za připravené výsledky.
Nástroj byl ověřen na testu s přeházeným pořadím objektů a materiálů, ne na skutečném exportu z Blenderu.
Transformované objekty po novém exportu vyžadují Three.js; původní záložný renderer je pro takový model odmítne namísto chybného zobrazení.
Změna dispozice navíc vyžaduje kontrolu kolizních obrysů, poloh žaluzií, podlah a připravených kamer v `data/scene.json`.
Samotné přejmenování nebo výměna GLB tyto pravidla automaticky nepřeprojektuje.
## Soukromí a provoz
Balíček obsahuje referenční obrázky a výřezy podkladů v `assets`. Na veřejném webu jsou přímo dostupné.
Pro neveřejný projekt chraňte **celou složku**, nikoli pouze `index.php`.
Meta `noindex` není zabezpečení. Aplikace sama nemá přihlášení ani serverové ukládání.
## Ověření a omezení
Ověřena je syntaxe PHP a JavaScriptu, vracení stránky přes PHP, HTTP cesty a typy souborů,
struktura GLB, katalog a jeho vazby, změny výběrů a rozložení rozhraní na desktopu i mobilu.
Testy ovládání použily simulované grafické API. **Nejde o ověření skutečného 3D vykreslení.**
Zdejší prohlížeč blokuje WebGL i externí knihovny, proto nebyl renderer Three.js plně spuštěn.
Barevné podání a výkon je potřeba zkontrolovat po nasazení v běžném prohlížeči.
Opakovatelná kontrola dat na počítači s Node.js:
```sh
node tests/data_checks.mjs
```
Node.js je zde pouze pro dobrovolný test, nikoli pro hosting nebo běh webu.
-93
View File
@@ -1,93 +0,0 @@
{
"data_checks_passed": 96,
"ui_checks_passed": 14,
"ui_page_errors": [],
"ui_graphics": "Mock WebGL API for controller/layout tests. No real 3D rendering.",
"syntax_checks": [
"PHP index.php lint",
"JavaScript modules node --check",
"Python helpers compilation"
],
"http_checks": "Local PHP server returned HTTP 200 for page, scripts, JSON, CSS and model.",
"export_mapping_test": "Synthetic reversed node/material order: passed. Not tested with a real Blender export.",
"not_verified": [
"Live WebGL 2 rendering",
"Three.js CDN imports in an unrestricted browser",
"Blender helper execution",
"Network part of the optional vendoring helper",
"User hosting deployment"
],
"environment_limitation": "Browser policy blocks WebGL and navigations; outbound container network blocked.",
"ui_checks": [
{
"name": "Defaults",
"ok": true,
"value": true
},
{
"name": "Blinds all nine rectangular openings",
"ok": true,
"value": true
},
{
"name": "Blinds off",
"ok": true,
"value": true
},
{
"name": "Nine floor rooms",
"ok": true,
"value": true
},
{
"name": "Kitchen independent colours",
"ok": true,
"value": true
},
{
"name": "Invalid colour rejected",
"ok": true,
"value": true
},
{
"name": "Walkin alternative",
"ok": true,
"value": true
},
{
"name": "Source binary roundtrip",
"ok": true,
"value": true
},
{
"name": "Summary export schema",
"ok": true,
"value": true
},
{
"name": "Pure astronomical function",
"ok": true,
"value": true
},
{
"name": "Reference photos loaded",
"ok": true,
"value": true
},
{
"name": "Survey overlay 27 points maximum",
"ok": true,
"value": true
},
{
"name": "No horizontal page overflow on mobile",
"ok": true,
"value": true
},
{
"name": "Mobile interior panel visible",
"ok": true,
"value": true
}
]
}
-8
View File
@@ -1,8 +0,0 @@
Three.js r180 / 0.180.0 is loaded from jsDelivr by default. It is not bundled here.
Source: https://github.com/mrdoob/three.js/tree/r180
License: MIT, Copyright (c) 2010-2025 three.js authors.
The optional vendor downloader fetches and preserves the upstream LICENSE.
User supplied model, drawings, catalogue samples and reference photographs:
provided for this project. No third-party asset ownership is claimed.
Do not treat these files as a separately licensed general-purpose asset library.
-362
View File
@@ -1,362 +0,0 @@
<!doctype html>
<html lang="cs">
<head>
<meta charset="utf-8">
<meta name="allstav-config-version" content="2025.2">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>Lo&#269;enice | Slune&#269;n&#253; vrch</title>
<link rel="stylesheet" href="./css/app.css">
<link rel="stylesheet" href="./css/web.css?v=20261026">
<meta name="robots" content="noindex,nofollow">
</head>
<body>
<canvas id="view" aria-label="Interaktivn&#237; trojrozm&#283;rn&#253; model rodinn&#233;ho domu"></canvas>
<svg id="measure" aria-hidden="true"></svg>
<header>
<div class="eyebrow"><span class="dot"></span>RODINN&#221; D&#366;M - Lo&#269;enice</div>
<h1>Slune&#269;n&#253; vrch &ndash; Lo&#269;enice (Novohradsk&#233; podh&#367;&#345;&#237;)</h1>
<div class="sub">Lo&#269;enice &middot; 3D model + simulace slunce</div>
<div class="metrics">
<div class="metric"><strong>15,340 m</strong><small>D&#233;lka</small></div>
<div class="metric"><strong>9,390 m</strong><small>&#352;&#237;&#345;ka</small></div>
<div class="metric"><strong>23&deg;</strong><small>St&#345;echa</small></div>
</div>
</header>
<div class="topright"><button id="standardsToggle" class="btn" type="button" aria-controls="standardsPanel" aria-expanded="false" title="Standardy a vybaveni domu"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M3 5h14M3 10h14M3 15h14"/><circle cx="7" cy="5" r="2" fill="currentColor"/><circle cx="13" cy="10" r="2" fill="currentColor"/><circle cx="7" cy="15" r="2" fill="currentColor"/></svg><span class="std-toggle-word">Standardy</span></button><button class="btn" id="sunToggle" aria-controls="sunPanel" aria-expanded="false"
title="Ovl&#225;d&#225;n&#237; slunce"><span class="solar-mark">&#9728;</span><span
class="toggle-word">Slunce</span></button><button class="btn icon" id="infoButton"
aria-label="Nastaven&#237; a informace o modelu" title="Nastaven&#237;">&#9881;&#xFE0E;</button><button
class="btn primary" id="saveGlb">St&#225;hnout <span
class="downloadText">model
</span>GLB</button></div>
<div class="panel" id="panel">
<h2>Model podle projektu</h2>
<p>Ot&#225;&#269;ec&#237; exteri&#233;rov&#253; model v metrech. Hlavn&#237; rozm&#283;ry a polohy otvor&#367;
vych&#225;zej&#237; z k&#243;t. Fas&#225;dn&#237; obklad: HardiePlank podle dodan&#233;ho vzorn&#237;ku; ostatn&#237; barevnost z p&#345;ilo&#382;en&#253;ch vizualizac&#237;.</p>
<h3>Parametry</h3>
<div class="row"><span>Ob&#225;lka domu</span><strong>15,340 &times; 9,390 m</strong></div>
<div class="row"><span>H&#345;eben nad podlahou 1. NP</span><strong>5,137 m</strong></div>
<div class="row"><span>Vrchol kom&#237;na</span><strong>5,837 m</strong></div>
<div class="row"><span>Kryt&#225; terasa / navazuj&#237;c&#237; ochoz</span><strong>&minus;0,03 / &minus;0,21
m</strong></div>
<h3>Barevnost</h3>
<div class="swatches"><label>Fas&#225;da<input id="wallColor" type="color" value="#c9c2ad"></label><label>Krytina<input id="roofColor" type="color" value="#414542"></label></div>
<section class="hardie-card" aria-labelledby="hardieTitle">
<div class="hardie-kicker">Fas&#225;dn&#237; obklad &#183; vstup a terasa</div>
<h4 id="hardieTitle">HardiePlank struktura d&#345;eva</h4>
<p class="hardie-spec">James Hardie &#183; vl&#225;knocement<br>Rozm&#283;r v&#253;robku: 3 600 &#215; 180 &#215; 8 mm</p>
<div id="hardiePreview" class="hardie-preview" role="img" aria-label="Orienta&#269;n&#237; n&#225;hled obkladu"></div>
<div class="hardie-preview-caption">ZV&#282;T&#352;EN&#221; DETAIL STRUKTURY &#183; ORIENTA&#268;N&#282;</div>
<div class="hardie-selection" aria-live="polite"><strong id="hardieColorName">O&#345;&#237;&#353;kov&#283; hn&#283;d&#225;</strong><small id="hardieProductCode">5771220 &#183; v&#253;choz&#237;</small></div>
<fieldset class="hardie-palette"><legend>Standardn&#237; barvy podle dodan&#233;ho vzorn&#237;ku</legend><div class="hardie-options"><label class="hardie-option" style="--chip:#ffffff"><input type="radio" name="hardieColor" value="snehove-bila" autocomplete="off"><span class="hardie-chip" aria-hidden="true"></span><span class="hardie-option-name">Sn&#283;hov&#283; b&#237;l&#225;</span><span class="hardie-option-code">5671220</span><span class="hardie-check" aria-hidden="true">&#10003;</span></label><label class="hardie-option" style="--chip:#d0c8bb"><input type="radio" name="hardieColor" value="sterkove-seda" autocomplete="off"><span class="hardie-chip" aria-hidden="true"></span><span class="hardie-option-name">&#352;t&#283;rkov&#283; &#353;ed&#225;</span><span class="hardie-option-code">5751220</span><span class="hardie-check" aria-hidden="true">&#10003;</span></label><label class="hardie-option" style="--chip:#b9c1c3"><input type="radio" name="hardieColor" value="mlhove-seda" autocomplete="off"><span class="hardie-chip" aria-hidden="true"></span><span class="hardie-option-name">Mlhov&#283; &#353;ed&#225;</span><span class="hardie-option-code">5841220</span><span class="hardie-check" aria-hidden="true">&#10003;</span></label><label class="hardie-option" style="--chip:#758896"><input type="radio" name="hardieColor" value="ocelove-seda" autocomplete="off"><span class="hardie-chip" aria-hidden="true"></span><span class="hardie-option-name">Ocelov&#283; &#353;ed&#225;</span><span class="hardie-option-code">5851220</span><span class="hardie-check" aria-hidden="true">&#10003;</span></label><label class="hardie-option selected" style="--chip:#776e65"><input type="radio" name="hardieColor" value="oriskove-hneda" checked autocomplete="off"><span class="hardie-chip" aria-hidden="true"></span><span class="hardie-option-name">O&#345;&#237;&#353;kov&#283; hn&#283;d&#225;</span><span class="hardie-option-code">5771220</span><span class="hardie-check" aria-hidden="true">&#10003;</span></label><label class="hardie-option" style="--chip:#241510"><input type="radio" name="hardieColor" value="espresso" autocomplete="off"><span class="hardie-chip" aria-hidden="true"></span><span class="hardie-option-name">Espresso</span><span class="hardie-option-code">5460122</span><span class="hardie-check" aria-hidden="true">&#10003;</span></label><label class="hardie-option" style="--chip:#4c5053"><input type="radio" name="hardieColor" value="antracitove-seda" autocomplete="off"><span class="hardie-chip" aria-hidden="true"></span><span class="hardie-option-name">Antracitov&#283; &#353;ed&#225;</span><span class="hardie-option-code">5420122</span><span class="hardie-check" aria-hidden="true">&#10003;</span></label></div></fieldset>
<p class="hardie-scope">M&#283;n&#237; se pouze svisl&#253; fas&#225;dn&#237; obklad u vstupu a na terase, v&#269;etn&#283; bo&#269;n&#237;ch st&#283;n. Podlaha a &#269;ela terasy ani schody se nem&#283;n&#237;.</p>
<a class="hardie-link" href="https://www.jameshardie.cz/cz/hardie-plank" target="_blank" rel="noopener noreferrer">HardiePlank na webu James Hardie &#8599;</a>
<div class="hardie-detail-buttons"><button type="button" data-cladding-view="entry">Detail vstupu</button><button type="button" data-cladding-view="terrace">Detail terasy</button></div>
<button class="hardie-structure-button" type="button" data-cladding-view="structure">P&#345;ibl&#237;&#382;it strukturu ve 3D</button>
<details class="hardie-note"><summary>Odst&#237;ny, struktura a p&#345;esnost modelu</summary>
<p>Barvy jsou ode&#269;ten&#233; z va&#353;eho obr&#225;zku vzorn&#237;ku, nikoli z kalibrovan&#253;ch dat v&#253;robce. Odst&#237;n ve sc&#233;n&#283; ovliv&#328;uje simulovan&#233; slunce, st&#237;n i monitor. Shodu s dodan&#253;m v&#253;robkem ov&#283;&#345;te na fyzick&#233;m vzorku.</p>
<img class="hardie-reference" src="./assets/ui/image-1.jpg" alt="Produktov&#225; fotografie struktury d&#345;eva z dodan&#233;ho vzorn&#237;ku">
<p>Ve 3D je dopln&#283;n jemn&#253; simulovan&#253; reli&#233;f inspirovan&#253; touto fotografi&#237;. Nejde o origin&#225;ln&#237; v&#253;robn&#237; texturu nebo 3D sken.</p>
<p>Vodorovn&#233; &#269;len&#283;n&#237;, geometrie a sp&#225;ry jsou zachov&#225;ny z p&#367;vodn&#237;ho modelu. Uveden&#253; rozm&#283;r desky je specifikace v&#253;robku, nikoli ov&#283;&#345;en&#237; klade&#269;sk&#233;ho pl&#225;nu nebo mont&#225;&#382;n&#237;ch detail&#367;.</p>
</details>
</section>
<p style="font-size:10px">Zvolen&#233; barvy a textura fas&#225;dn&#237;ho obkladu se ulo&#382;&#237; i do sta&#382;en&#233;ho GLB.</p>
<p style="font-size:10px"><a
href="https://www.sto.cz/cepcom/cz/documents/download_colour_ranges_1/StoColor_System_2022_CZ.pdf"
target="_blank" rel="noopener">Vzorn&#237;k barev StoColor System (PDF)</a></p>
<h3>Kolm&#233; pohledy</h3>
<select id="elevation">
<option value="">Vyber pohled...</option>
<option value="north">Severn&#237; pohled</option>
<option value="east">V&#253;chodn&#237; pohled</option>
<option value="south">Ji&#382;n&#237; pohled</option>
<option value="west">Z&#225;padn&#237; pohled</option>
</select>
<div class="buttons"><button class="btn" onclick="document.getElementById('saveGlb').click()">St&#225;hnout
model GLB</button></div>
<details class="site-note">
<summary>Okolí, výškopis a přesnost</summary>
<p><b>Tři spodní přepínače jsou nezávislé a při otevření zapnuté.</b> Zapnutí zároveň zaměří příslušný
pohled. Ikona vedle přepínačů zarámuje zapnuté okolí; Vstup, Terasa a Reset vrátí pohled k domu. Vypnutí
poslední vrstvy obnoví předchozí pohled.</p>
<p>Geometrie, měřítko a poloha původního domu, otvorů, střechy, teras a jejich schodů jsou zachované.
Nahrazen je pouze původní schematický terén, štěrk a vstupní zpevněná plocha.</p>
<p>Podklady okolí: C.2 a C.3 (umístění a upravené úrovně), C.4 (celý pozemek), C.5 (komunikace) a „Ločenice
p.č. 3480/37 – polohopis a výškopis“. Výšková nula: <b>591,840 m Bpv</b>. U vstupu <b>−0,480 m</b>, u
horní podesty schodů z parkování <b>+0,390 m</b>, na konci zahradních schodů dle C.3 <b>−1,395 m</b>.
</p>
<p>Povrch kombinuje původní zaměřené body s upravenými úrovněmi C.3. Mezi body je <b>interpolovaný</b>;
navázání okrajů, skladba dlažby a dvě auta jsou vizualizační zjednodušení. Parkovací plocha směrem k
ulici stoupá; není zobrazena jako jediná vodorovná deska.</p>
<p><b>Rozdíl v podkladech:</b> projekt uvádí výměru <b>1 010 m²</b>, obrys převzatý z vektorové kresby PDF v
měřítku domu vychází přibližně <b>988 m²</b> (o 2,2 % méně). Obrys není uměle zvětšen na deklarovanou
výměru. Přesnou hranici je nutné ověřit ze souřadnic nebo DWG; tato vizualizace není podklad pro
vytyčení ani zemní práce.</p>
<p>Výpočet slunce a převod světových stran do os modelu jsou zachované. Původní azimut severního štítu
<b>351,5°</b> a přibližná poloha obce nejsou tímto geodeticky ověřeny.
</p>
<p>PNG zachycuje právě viditelné vrstvy. GLB exportuje scénu s právě zapnutým okolím a zvolenými barvami;
ovládání slunce zůstává jen v HTML.</p>
</details>
<h3>Úpravy modelu</h3>
<p>Doplněn rohový sloup kryté terasy podle jižního a východního pohledu D.1.1.2.3.1. Průřez 200 × 200 mm a odsazení od hrany jsou odečtené orientačně z kresby, nikoli staticky ověřené. Vinotéka je u stěny a nad celou kuchyní pokračuje vysoký strop obýváku.</p>
<h3>Rozsah a omezen&#237;</h3>
<p class="note">Pracovn&#237; vizualiza&#269;n&#237; model, nikoli BIM nebo podklad pro stavbu. S volitelnou orientační vrstvou interiéru, bez skrytých konstrukcí. Terén okolí je doplněn podle C.3 a výškopisu; mezi body je
interpolovaný. Hranice jsou převzaté graficky z PDF, nikoli ze souřadnic pro vytyčení.</p>
<p>Profily oken, ta&#353;ky, oplechov&#225;n&#237;, technick&#233; vybaven&#237; a spoje jsou
zjednodu&#353;en&#233;. Z&#225;bradl&#237; je kovov&#233; podle pohled&#367; a technick&#233; zpr&#225;vy. V
p&#345;&#237;pad&#283; rozd&#237;lu maj&#237; v&#253;kresy p&#345;ednost p&#345;ed vizualizacemi.</p>
<h3>Pou&#382;it&#233; podklady</h3>
<p style="font-size:10px">D.1.1.2.1.1 P&#367;dorys 1NP<br>D.1.1.2.1.3 P&#367;dorys st&#345;echy<br>D.1.1.2.3.1
Pohledy<br>D.1.1.2.2.1&ndash;3 &#344;ezy A&ndash;A, B&ndash;B, C&ndash;C<br>D.1.1.1 Technick&#225;
zpr&#225;va<br>Obr&#225;zky 6_1.jpeg a 6_2.jpeg</p>
<div class="buttons"><button class="btn" id="savePng">Ulo&#382;it pohled PNG</button><button class="btn"
id="resetColors">P&#367;vodn&#237; barvy</button></div>
<div class="status"><span class="dot"></span>Samostatn&#253; soubor &middot; funguje bez internetu &middot;
verze 05 · sloup terasy, vinotéka a vysoký strop</div>
</div>
<div id="direction"><span class="tag">AKTU&#193;LN&#205; POHLED</span><span id="directionText">Terasa /
jihov&#253;chod</span></div>
<div class="bottom">
<nav class="toolbar" aria-label="Ovl&#225;d&#225;n&#237; modelu"><button class="tool"
data-preset="entry">Vstup</button><button class="tool"
data-preset="terrace">Terasa</button><button class="tool" data-preset="top">Shora</button><span
class="divider"></span><button class="tool" id="terrainButton"
title="Zobrazit nebo skr&#253;t ter&#233;n">Ter&#233;n</button><button class="tool"
id="measureButton">K&#243;ty</button><button class="tool optional active"
id="rotateButton">Ot&#225;&#269;en&#237;</button><span class="divider"></span><button class="tool"
id="resetView" title="Obnovit v&#253;choz&#237; pohled">Reset</button></nav>
<div class="site-controls" role="group" aria-label="Volitelné okolí domu; výchozí stav zapnuto">
<label class="site-switch" title="Komunikace a navazující terén mezi ulicí a domem"><input type="checkbox"
id="siteRoad" autocomplete="off" checked><span>Cesta a přístup</span></label>
<label class="site-switch" title="Celá zahrada a graficky převzatá hranice parcely"><input type="checkbox"
id="siteParcel" autocomplete="off" checked><span>Celý pozemek</span></label>
<label class="site-switch" title="Parkovací plocha, napojení a schody; dvě orientační auta"><input
type="checkbox" id="siteParking" autocomplete="off" checked><span>Parkování</span></label>
<label class="site-switch" title="Plot z 3D panelů na podhrabových deskách; u domu branka a brána pro auta"><input
type="checkbox" id="siteFence" autocomplete="off"><span>Plot</span></label>
<button id="siteFit" class="site-fit" type="button" title="Zarámovat zapnuté okolí"
aria-label="Zarámovat zapnuté okolí"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" aria-hidden="true">
<path d="M9 4H4v5M15 4h5v5M20 15v5h-5M9 20H4v-5" />
<rect x="9" y="9" width="6" height="6" rx="1" />
</svg></button>
</div>
<div id="fenceHeights" class="site-controls fence-heights" role="group" aria-label="Výška plotu" hidden>
<span class="fence-heights-label">Výška plotu</span>
<button type="button" class="site-switch" data-fence-height="0" title="Panel 830 mm na podhrabové desce 200 mm">1,0 m</button>
<button type="button" class="site-switch" data-fence-height="1" title="Panel 1030 mm na podhrabové desce 200 mm">1,2 m</button>
<button type="button" class="site-switch" data-fence-height="2" title="Panel 1230 mm na podhrabové desce 200 mm">1,4 m</button>
<button type="button" class="site-switch" data-fence-height="3" title="Panel 1530 mm na podhrabové desce 200 mm">1,7 m</button>
<button type="button" class="site-switch" data-fence-height="4" title="Panel 1730 mm na podhrabové desce 200 mm; nejvyšší vyráběná kombinace do 2 m">1,9 m</button>
</div>
<div id="siteMeta" hidden aria-live="polite"></div>
<div class="help">Ta&#382;en&#237;m ot&#225;&#269;ej &nbsp;&middot;&nbsp; Kole&#269;kem p&#345;ibli&#382;
&nbsp;&middot;&nbsp; Prav&#253;m tla&#269;&#237;tkem posu&#328;</div>
</div>
<div class="compass"><svg viewBox="0 0 84 84">
<circle cx="42" cy="42" r="27" fill="none" stroke="#bec8b7" />
<g id="compassGroup">
<line x1="42" y1="13" x2="42" y2="71" />
<line x1="13" y1="42" x2="71" y2="42" />
<path id="northArrow" d="M42 18 L37 32 L42 29 L47 32 Z" /><text x="42" y="8"
text-anchor="middle">S</text><text x="42" y="81" text-anchor="middle">J</text><text x="3" y="45"
text-anchor="middle">Z</text><text x="80" y="45" text-anchor="middle">V</text>
</g>
</svg></div>
<svg id="sunOverlay" aria-hidden="true"></svg>
<div id="sunStamp"><span>SLUNCE NAD DOMEM</span><br><strong id="sceneTime">21. 6. 2026 · 10:00</strong></div>
<section id="sunPanel" aria-label="Simulace slunce">
<div class="sun-panel-head">
<div>
<div class="sun-kicker">STUDIE OSVĚTLENÍ</div>
<h2>Slunce a stíny</h2>
</div><button class="close-panel" id="sunClose" aria-label="Skrýt panel slunce">×</button>
</div>
<div class="sun-date-row"><label class="sun-field">Datum<input id="sunDate" type="date" min="2000-01-01"
max="2100-12-31" value="2026-06-21"></label><label class="sun-field">Místní čas<input id="sunClock"
type="text" inputmode="numeric" value="10:00" maxlength="5" placeholder="HH:MM"
aria-label="Místní čas ve formátu HH:MM"></label></div>
<div class="sun-seasons" aria-label="Typické dny roku"><button data-season="03-20">Jaro</button><button
data-season="06-21" class="active">Léto</button><button data-season="09-23">Podzim</button><button
data-season="12-21">Zima</button></div>
<div class="sun-status-line"><span id="dayState">Slunce nad obzorem</span><span id="zoneLabel">SELČ ·
UTC+2</span></div>
<svg id="sunGraph" class="sun-graph" viewBox="0 0 304 86" role="img"
aria-label="Výška slunce během dne. Kliknutím změníš čas."></svg>
<label class="sr-only" for="sunTime"
style="position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)">Posun času</label>
<input class="sun-scrub" id="sunTime" type="range" min="0" max="1439" value="600" step="1"
aria-valuetext="10:00">
<div class="sun-ticks">
<span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>24:00</span>
</div>
<div class="sun-play-row"><button id="sunPlay" type="button">▶ Přehrát den</button><select id="sunSpeed"
aria-label="Rychlost simulace">
<option value="15">15 min / s</option>
<option value="30" selected>30 min / s</option>
<option value="60">60 min / s</option>
</select><button id="sunNow" title="Aktuální datum a čas v Praze">Teď</button></div>
<div id="solarMessage" role="status"></div>
<div class="sun-readouts">
<div class="sun-stat">Výška nad obzorem<strong id="sunElevation">—</strong><small>Střed slunečního
kotouče</small></div>
<div class="sun-stat">Azimut slunce<strong id="sunAzimuth">—</strong><small id="sunDirection">Od severu po
směru hodin</small></div>
</div>
<div class="sun-events"><span>Východ <b id="sunRise">—</b></span><span>Poledne <b
id="sunNoon">—</b></span><span>Západ <b id="sunSet">—</b></span></div>
<div class="sun-options"><label><input type="checkbox" id="sunShadows" checked>Vržené stíny</label><label><input
type="checkbox" id="sunPath">Dráha ve 3D</label></div>
<p class="sun-hint" id="pathHint">Posouvej čas a sleduj, kam dopadá slunce na terasu a fasády. Natočení pohledu
polohu slunce nemění.</p>
<button class="sun-export" id="sunSavePng">Uložit obrázek s datem a časem</button>
<details class="sun-advanced">
<summary>Poloha a orientace domu</summary>
<div class="fields"><label class="sun-field">Zeměpisná šířka (° N)<input id="sunLat" type="number" min="-89"
max="89" step="0.0001" value="48.8242"></label><label class="sun-field">Zeměpisná délka (°
E)<input id="sunLon" type="number" min="-180" max="180" step="0.0001" value="14.5275"></label><label
class="sun-field full-row">Azimut severního štítu domu (°, od severu)<input id="houseBearing"
type="number" min="0" max="359.9" step="0.1" value="351.5"></label></div>
<p>Výchozí poloha: <b>Ločenice, přibližný bod obce</b>, nikoli geodeticky určený bod parcely. Ponecháno
původní nastavení <b>351,5°</b>, v původním HTML označené jako odečet severky D.1.1.2.1.1. Přiložené
situace a výškopis bez číselných souřadnic neumožňují toto natočení nezávisle potvrdit. Nejde o ověřený
azimut stavby.</p>
<p id="bearingNote">Jižní štít: 171,5° · vstupní fasáda: 261,5°.</p>
<button class="mini-btn" id="sunResetSite">Obnovit Ločenice a sever z projektu</button>
<p>Časové pásmo zůstává <b>Europe/Prague</b>. Letní čas se přepočítává podle zvoleného data a pravidel
prohlížeče.</p>
</details>
<details class="sun-advanced">
<summary>Co simulace počítá a co ne</summary>
<p>Sluneční poloha se počítá z data, času a zeměpisných souřadnic podle astronomických rovnic Meeus / NOAA.
Směr přímého světla i stínová mapa se mění spolu s časem. Zobrazená výška slunce je geometrická. Východ
a západ jsou určeny pro výšku středu −0,833°, bez místního horizontu.</p>
<p>Stíny vytváří geometrie tohoto modelu. <b>Okolní domy, stromy, ploty a vzdálený horizont nejsou
modelovány.</b> Terén navazuje na body výškopisu a úpravy C.3; mezi body je interpolovaný. Nejde o
výpočet vnitřního denního osvětlení, tepelných
zisků ani výkonu fotovoltaiky. Oblačnost není zahrnuta; jas a barva jsou vizualizační.</p>
<p>Dráha ve 3D je vysvětlující překryv v symbolickém měřítku; slunce není ve skutečné vzdálenosti. Noční
pohled je mírně přisvětlen pro orientaci. Velmi nízké slunce může mít stín mimo rozsah terénu.</p>
<p>Podklady: D.1.1.2.1.1 (severka), D.1.1.2.3.1 (fasády), D.1.1.1 a B (orientace a terasa); situační výkresy
C.1–C.5 pro umístění a „Ločenice p.č. 3480/37 – polohopis a výškopis“ pro terén.</p>
<p>Zdroje výpočtu a polohy: <a href="https://gml.noaa.gov/grad/solcalc/calcdetails.html" target="_blank"
rel="noopener">NOAA / Meeus</a> · <a href="https://www.mistopisy.cz/pruvodce/obec/8006/locenice/"
target="_blank" rel="noopener">Ločenice – souřadnice</a>. Připojení je potřeba jen pro otevření
těchto zdrojů, ne pro simulaci.</p>
</details>
<p class="sun-footnote">Pracovní studie stínů · bez internetu<br>Geometrie domu zůstává zachována. Export GLB
neobsahuje ovládání slunce.</p>
</section>
<button id="interiorLaunch" type="button" aria-controls="interiorPanel" aria-expanded="false" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M3 11 12 4 21 11M5 10v10h14V10M10 20v-7h5v7"/></svg>
<span>Zapnout interiér<small>Půdorys · 3D bez střechy · procházení</small></span>
</button>
<div class="interior-heading"><button class="btn" id="interiorExit" title="Zpět na původní exteriér (Esc)">← Exteriér</button><div><div class="eyebrow">INTERIÉR / 1. NADZEMNÍ PODLAŽÍ</div><h2 id="interiorViewTitle">Dům bez střechy</h2></div></div>
<section id="interiorPanel" aria-label="Pohledy a nastavení interiéru">
<div class="interior-panel-title"><h3>Uvnitř domu</h3><span>9 místností</span></div>
<svg id="interiorMap" viewBox="0 0 330 205" role="group" aria-label="Kliknutím na místnost přejdete dovnitř"></svg>
<p class="int-map-hint">Klikněte na místnost v mapě nebo vyberte připravený pohled.</p>
<div class="int-kicker">Kuchyň a obytný prostor</div>
<div class="int-views">
<button data-interior-view="kitchen">Kuchyně</button><button data-interior-view="outlook">Z kuchyně ven</button>
<button data-interior-view="living">Obývací prostor</button><button data-interior-view="vault">Trámy a vysoké okno</button>
<button data-interior-view="kitchenVault" style="grid-column:1/-1">Vysoký strop nad kuchyní</button>
</div>
<div class="int-kicker">Přejít do místnosti</div>
<select id="interiorRoom" aria-label="Pohled z vybrané místnosti"><option value="">Vyberte místnost…</option></select>
<section class="int-paint" aria-labelledby="interiorPaintTitle">
<div class="int-paint-head"><h4 id="interiorPaintTitle">Barva zd&#237;</h4><span>Vnit&#345;n&#237; om&#237;tka</span></div>
<div class="int-paint-fields">
<label class="int-paint-picker" title="Vybrat vlastn&#237; barvu zd&#237;"><input id="interiorWallColor" type="color" value="#eeeae1" aria-label="Barva vnit&#345;n&#237;ch zd&#237;" aria-describedby="interiorPaintScope"></label>
<label class="int-paint-hex" for="interiorWallHex">Vlastn&#237; odst&#237;n / HEX<input id="interiorWallHex" type="text" value="#EEEAE1" maxlength="7" spellcheck="false" autocapitalize="characters" autocomplete="off" aria-label="HEX k&#243;d barvy vnit&#345;n&#237;ch zd&#237;"></label>
</div>
<div class="int-paint-presets" role="group" aria-label="P&#345;ipraven&#233; barvy zd&#237;">
<button type="button" data-interior-paint="#eeeae1" aria-pressed="true"><span class="int-paint-chip" style="--paint:#eeeae1" aria-hidden="true"></span>Tepl&#225; b&#237;l&#225;</button>
<button type="button" data-interior-paint="#ffffff" aria-pressed="false"><span class="int-paint-chip" style="--paint:#ffffff" aria-hidden="true"></span>&#268;ist&#225; b&#237;l&#225;</button>
<button type="button" data-interior-paint="#dfceb3" aria-pressed="false"><span class="int-paint-chip" style="--paint:#dfceb3" aria-hidden="true"></span>P&#237;skov&#225;</button>
<button type="button" data-interior-paint="#d9d9d4" aria-pressed="false"><span class="int-paint-chip" style="--paint:#d9d9d4" aria-hidden="true"></span>Sv&#283;tle &#353;ed&#225;</button>
<button type="button" data-interior-paint="#c4cebc" aria-pressed="false"><span class="int-paint-chip" style="--paint:#c4cebc" aria-hidden="true"></span>&#352;alv&#283;jov&#225;</button>
<button type="button" data-interior-paint="#d5c7b9" aria-pressed="false"><span class="int-paint-chip" style="--paint:#d5c7b9" aria-hidden="true"></span>Jemn&#283; b&#233;&#382;ov&#225;</button>
</div>
<button class="int-paint-reset" id="interiorWallReset" type="button">Obnovit p&#367;vodn&#237; barvu zd&#237;</button>
<p class="int-paint-note" id="interiorPaintScope">M&#283;n&#237; v&#353;echny vnit&#345;n&#237; om&#237;tnut&#233; st&#283;ny. Strop, tr&#225;my, obklady a fas&#225;da z&#367;st&#225;vaj&#237; beze zm&#283;ny. Zvolen&#253; odst&#237;n se prom&#237;tne i do PNG a GLB.</p>
</section>
<div class="int-options">
<label><input type="checkbox" id="interiorFurniture" checked>Vybavení</label>
<label><input type="checkbox" id="interiorRoomLabels" checked>Názvy místností</label>
<label><input type="checkbox" id="interiorFill" checked>Přisvětlení interiéru</label>
</div>
<p>V pohledech shora je střecha skrytá a stěny zkrácené do výšky 1,12 m. Procházení obnoví plnou výšku stěn, podhledy a trámy. Dveře mezi místnostmi jsou zobrazené otevřené.</p>
<details class="int-source"><summary>Podklady a přesnost modelu</summary>
<p><b>Dispozice:</b> půdorys D.1.1.2.1.1 ze dne 21. 9. 2026. Čísla místností a popisky ploch jsou převzaté z výkresu. Obvod domu, poloha oken a vnějších dveří jsou beze změny z původního modelu.</p>
<p><b>Kuchyň:</b> zelená dvířka, dubové plochy, vysoké skříně, otevřené police a poloostrov podle dodaných obrázků. Vinotéka je na konci poloostrova u stěny. Nad celou kuchyní a obývacím prostorem je souvislý vysoký šikmý strop s přiznanými trámy. Uspořádání do U je přizpůsobené místnosti 108 a průchodům; přesné výrobní rozměry skříněk ani typy spotřebičů nejsou doložené.</p>
<p><b>Trámy:</b> vzhled upraven hlavně podle pohled_ven.jpeg: vodorovné trámy, svislé podpěry, krátké podélné vzpěry a navazující horní trám. Světlé dřevo má kresbu podél jednotlivých prvků. Přesné průřezy a spoje nejsou z obrázků ověřené; jde o vzhledovou studii, ne statický návrh.</p>
<p><b>Orientační:</b> rozměry nábytku, světlá výška ostatních místností 2,60 m, vysoký strop obytného prostoru podle stávajícího sklonu střechy, průřezy a detaily trámů, sanita a technické vybavení. Jde o prostorovou studii, nikoli výrobní dokumentaci nebo statický návrh.</p>
<p>Rámy oken zůstaly v původní barvě, i když mají referenční obrázky různé odstíny. Výhled používá stávající 3D okolí; fotografie nejsou přesným panoramatem okolní krajiny. V procházení jsou okenní skla opticky skrytá pro volný výhled.</p>
<p>Přisvětlení slouží k čitelnosti interiéru. Simulace slunce není výpočtem vnitřního denního osvětlení.</p>
<div class="int-source-buttons"><button data-int-ref="plan">Původní půdorys</button><button data-int-ref="kitchen1">Kuchyň · 1</button><button data-int-ref="kitchen2">Kuchyň · 2</button><button data-int-ref="view1">Výhled · 1</button><button data-int-ref="view2">Výhled · 2</button></div>
</details>
<button class="int-export" id="interiorSavePng">Uložit aktuální pohled PNG</button>
<button class="int-export" id="interiorSaveGlb">Stáhnout zobrazený model GLB</button>
</section>
<div id="interiorLabels" aria-label="Místnosti v půdorysu"></div>
<div id="interiorTools"><div id="interiorHint">Tažením otáčejte · kolečkem přibližujte · kliknutím na místnost vstupte dovnitř</div>
<nav class="int-mode-bar" aria-label="Režim interiéru"><button data-interior-mode="plan">Půdorys</button><button data-interior-mode="axon" class="active">3D bez střechy</button><button data-interior-mode="walk">Procházet</button><button id="interiorFit" class="int-nav-button" title="Zarámovat dům / obnovit pohled">⌖</button><button id="interiorPanelToggle" class="int-nav-button" title="Skrýt nebo zobrazit navigaci" aria-expanded="true">☷</button></nav>
</div>
<div id="walkPad" aria-label="Ovládání chůze"><button data-walk="forward" aria-label="Jít vpřed">↑</button><button data-walk="left" aria-label="Krok vlevo">←</button><button data-walk="back" aria-label="Jít vzad">↓</button><button data-walk="right" aria-label="Krok vpravo">→</button></div>
<dialog id="interiorReferenceDialog"><div class="int-ref-head"><h3 id="interiorReferenceTitle">Podklad</h3><button id="interiorReferenceClose" aria-label="Zavřít podklad">Zavřít ×</button></div><img id="interiorReferenceImage" alt="Dodaný podklad interiéru"><p class="int-ref-caption" id="interiorReferenceCaption"></p></dialog>
<div id="toast"></div>
<div id="loading">
<div class="spinner"></div>
<div class="label">Na&#269;&#237;t&#225;m 3D model...</div>
</div>
<aside id="standardsPanel" aria-label="Výběr standardů ALLSTAV">
<div class="std-head"><div class="std-kicker">ALLSTAV / STANDARDY 2025</div><div class="std-head-row"><h2>Váš dům, váš výběr</h2><button id="stdClose" class="std-close" aria-label="Zavřít standardy" type="button">×</button></div><p>Katalogové varianty a materiály přímo v modelu.</p></div>
<div class="std-tabs" role="tablist" aria-label="Kategorie standardů"><button role="tab" id="std-tab-exterior" data-std-tab="exterior" aria-selected="true" aria-controls="std-exterior">Exteriér</button><button role="tab" id="std-tab-interior" data-std-tab="interior" aria-selected="false" aria-controls="std-interior">Interiér</button><button role="tab" id="std-tab-summary" data-std-tab="summary" aria-selected="false" aria-controls="std-summary">Můj výběr</button></div>
<div class="std-scroll"><div id="std-exterior" role="tabpanel" aria-labelledby="std-tab-exterior"></div><div id="std-interior" role="tabpanel" aria-labelledby="std-tab-interior" hidden></div><div id="std-summary" role="tabpanel" aria-labelledby="std-tab-summary" hidden></div></div>
<div class="std-footer"><div id="stdSaved" class="std-saved" role="status">Připraveno</div><div class="std-footer-row"><button class="std-action dark" data-std-action="summary">Přehled výběru</button><button class="std-action" data-std-action="exportJson">Zálohovat výběr</button></div></div></aside>
<dialog id="stdCatalogDialog" aria-labelledby="stdCatalogTitle"><div class="std-dialog-head"><h3 id="stdCatalogTitle">Katalog ALLSTAV</h3><button id="stdCatalogClose">Zavřít ×</button></div><img id="stdCatalogImage" alt="Katalog ALLSTAV"><p id="stdCatalogCaption"></p><div class="std-dialog-pager"><button id="stdCatalogPrev">← Předchozí</button><button id="stdCatalogNext">Další →</button></div></dialog>
<dialog id="stdResetDialog" aria-labelledby="stdResetTitle"><h3 id="stdResetTitle">Obnovit výchozí výběr?</h3><p>Vaše volby standardů budou nahrazeny výchozím nastavením. Před obnovením je můžete zálohovat jako JSON. Slunce a okolí domu se nemění.</p><div class="std-two"><button id="stdResetCancel" class="std-action">Zrušit</button><button id="stdResetConfirm" class="std-action dark">Obnovit</button></div></dialog>
<input id="stdImport" type="file" accept=".json,application/json" hidden>
<script id="stdSavedState" type="application/json">{}</script>
<div id="webStatus" role="status">Načítání modelu…</div>
<button id="projectToggle" class="btn" type="button">Projekt a podklady</button>
<dialog id="projectDialog" aria-labelledby="projectTitle">
<div class="web-dialog-head"><div><small>LOČENICE / ZDROJOVÝ MODEL</small><h2 id="projectTitle">Projekt a podklady</h2></div><button id="projectClose" class="btn" aria-label="Zavřít">Zavřít</button></div>
<div class="web-dialog-body">
<p class="web-lead">Dům, katalog a ovládání jsou samostatné soubory. Stavební geometrie vychází z dosavadního modelu, vybavení z referenčních obrázků.</p>
<h3>Kuchyň a obývací pokoj</h3><div id="projectPhotos" class="web-photo-grid"></div>
<p class="web-note">Fotografie se liší v rámech oken a detailech nábytku. Jde o vzhledové reference, nikoli o výrobní výkres kuchyně nebo zaměřené panorama.</p>
<h3>Pozemek a výšky</h3><div id="surveyStats" class="web-stat-grid"></div>
<p class="web-note">Body výškopisu jsou v Bpv; poloha v 3D je převzatá z grafického odečtu PDF. Upravený terén u domu je jiná vrstva než původní zaměření. Rozpor plochy není opraven natažením modelu.</p>
<div class="web-actions"><button class="btn" id="surveyShow">Zobrazit body v modelu</button><button class="btn" data-int-ref="survey">Otevřít výškopis</button><a class="btn" href="./data/site.json" download="locenice_pozemek.json">Data pozemku JSON</a></div>
<h3>Editace modelu</h3><p>Soubor <code>models/dum.glb</code> lze importovat do Blenderu. Kompletní zdroj obsahuje i skryté varianty dveří, žaluzií a interiéru. Export „viditelný výběr“ je naopak pouze konkrétní varianta.</p>
<div class="web-actions"><a class="btn primary" href="./models/dum.glb" download="dum_zdrojovy.glb">Stáhnout celý zdrojový GLB</a><a class="btn" href="./data/catalog.json" download="allstav_katalog.json">Katalog JSON</a></div>
<p class="web-note" id="backendNote"></p>
</div>
</dialog>
<svg id="surveyOverlay" aria-label="Body výškopisu" style="display:none"></svg>
<noscript><div class="web-fatal">Pro 3D model povolte JavaScript. <a href="./models/dum.glb">Stáhnout GLB</a></div></noscript>
<script>
// Firefox and Safari accept an import map only before the first module starts loading, so the map
// for Three.js is added here and bootstrap.js is requested only afterwards.
fetch('./data/app-config.json',{cache:'no-cache'}).then(r=>r.json()).then(c=>{const m=document.createElement('script');m.type='importmap';m.textContent=JSON.stringify({imports:{three:new URL(c.threeModule,location.href).href}});document.head.appendChild(m);}).catch(()=>{}).finally(()=>{const s=document.createElement('script');s.type='module';s.src='./js/bootstrap.js?v=20261026e';document.body.appendChild(s);});
</script>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

-65
View File
@@ -1,65 +0,0 @@
"""Run in Blender, not system Python:
blender --background --python blender/prepare_model.py
Creates an editable .blend next to this script. Existing scene is replaced.
Keeps every configuration variant; helper collections are hidden in viewport.
"""
from pathlib import Path
import json
import bpy
ROOT=Path(__file__).resolve().parent.parent
SOURCE=ROOT/'models'/'dum.glb'
TARGET=Path(__file__).resolve().parent/'dum_locenice.blend'
if not SOURCE.is_file():
raise FileNotFoundError(SOURCE)
if TARGET.exists():
raise FileExistsError(f'Rename or remove {TARGET} before running again.')
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.gltf(filepath=str(SOURCE))
defaults=json.loads((ROOT/'data'/'catalog.json').read_text())['defaults']
scene=bpy.context.scene
scene.unit_settings.system='METRIC'
scene.unit_settings.scale_length=1.0
collections={}
for name in ['01_Exterior','02_Interior_walls','03_Kitchen_and_furniture','04_Site','05_Cutaway_alternatives','06_Standards_alternatives']:
col=bpy.data.collections.new(name);scene.collection.children.link(col);collections[name]=col
for obj in list(scene.objects):
if obj.type!='MESH':continue
props=obj
if props.get('stdHidden') or props.get('stdWhen') or props.get('stdBlind'):
key='06_Standards_alternatives'
elif props.get('interiorLayer') in ['cut','cutShell']:
key='05_Cutaway_alternatives'
elif props.get('interiorLayer')=='furniture':
key='03_Kitchen_and_furniture'
elif props.get('interiorLayer'):
key='02_Interior_walls'
elif props.get('siteLayer') or props.get('legacySite'):
key='04_Site'
else:key='01_Exterior'
for c in list(obj.users_collection):c.objects.unlink(obj)
collections[key].objects.link(obj)
# Stable ID comes from the GLB extras. Do not delete it during future editing.
obj['locenice_edit_note']='Keep loceniceId and configuration properties.'
condition=props.get('stdWhen',{})
hidden=bool(props.get('stdHidden')) or bool(props.get('stdBlind') and not defaults['blinds'])
if hasattr(condition,'items'):
hidden=hidden or any(defaults.get(k)!=v for k,v in condition.items())
obj.hide_set(hidden)
obj.hide_render=hidden
collections['05_Cutaway_alternatives'].hide_viewport=True
# Keep matching default variants visible; all alternatives remain in the file.
note=bpy.data.texts.new('READ_ME_Locenice')
note.write('''This is a visualisation mesh, not BIM. Units: metres.
The source includes multiple, mutually exclusive variants in the same location.
Use named collections to inspect them. Do not join all meshes or all materials.
loceniceId, stdWhen, stdBlind, windowNode and interiorLayer must stay on objects.
Detailed construction dimensions come from the project, not reference photos.
After exporting all variants to GLB, run tools/prepare_export.py.
Blender reorders glTF nodes, materials and textures. The included web manifest
is indexed for the supplied GLB; the preparation tool rebinds stable IDs.
A changed wall or cabinet also requires checking web collision volumes / camera presets.
''')
scene['locenice_metadata_json']=json.dumps(json.loads((ROOT/'data'/'scene.json').read_text()),ensure_ascii=False)
bpy.ops.wm.save_as_mainfile(filepath=str(TARGET))
print('Saved:',TARGET)
-1728
View File
File diff suppressed because it is too large Load Diff
-24
View File
@@ -1,24 +0,0 @@
/* 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}}
-16
View File
@@ -1,16 +0,0 @@
{
"version": "locenice-web-01",
"model": "./models/dum.glb?v=20261029",
"renderer": "auto",
"threeModule": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
"threeLoader": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/loaders/GLTFLoader.js",
"cdnTimeoutMs": 6500,
"maxPixelRatio": 1.5,
"startAutoRotate": true,
"startStandardsOpen": false,
"catalog": "./data/catalog.json",
"assets": "./data/assets.json",
"references": "./data/references.json",
"sceneMetadata": "./data/scene.json",
"site": "./data/site.json"
}
-52
View File
@@ -1,52 +0,0 @@
{
"page12": "./assets/catalog/page12.jpg",
"page13": "./assets/catalog/page13.jpg",
"page14": "./assets/catalog/page14.jpg",
"page15": "./assets/catalog/page15.jpg",
"page16": "./assets/catalog/page16.jpg",
"page17": "./assets/catalog/page17.jpg",
"page18": "./assets/catalog/page18.jpg",
"page19": "./assets/catalog/page19.jpg",
"page20": "./assets/catalog/page20.jpg",
"page21": "./assets/catalog/page21.jpg",
"page22": "./assets/catalog/page22.jpg",
"page23": "./assets/catalog/page23.jpg",
"page24": "./assets/catalog/page24.jpg",
"page25": "./assets/catalog/page25.jpg",
"lam1": "./assets/catalog/lam1.jpg",
"lam2": "./assets/catalog/lam2.jpg",
"lam3": "./assets/catalog/lam3.jpg",
"lam4": "./assets/catalog/lam4.jpg",
"lam5": "./assets/catalog/lam5.jpg",
"lam6": "./assets/catalog/lam6.jpg",
"lam7": "./assets/catalog/lam7.jpg",
"lam8": "./assets/catalog/lam8.jpg",
"lam9": "./assets/catalog/lam9.jpg",
"tile1": "./assets/catalog/tile1.jpg",
"tile2": "./assets/catalog/tile2.jpg",
"tile3": "./assets/catalog/tile3.jpg",
"tile4": "./assets/catalog/tile4.jpg",
"tile5": "./assets/catalog/tile5.jpg",
"tile6": "./assets/catalog/tile6.jpg",
"tile7": "./assets/catalog/tile7.jpg",
"tile8": "./assets/catalog/tile8.jpg",
"tile9": "./assets/catalog/tile9.jpg",
"door0": "./assets/catalog/door0.jpg",
"door1": "./assets/catalog/door1.jpg",
"door2": "./assets/catalog/door2.jpg",
"door3": "./assets/catalog/door3.jpg",
"door4": "./assets/catalog/door4.jpg",
"door5": "./assets/catalog/door5.jpg",
"door6": "./assets/catalog/door6.jpg",
"door7": "./assets/catalog/door7.jpg",
"requestedDoor": "./assets/catalog/requestedDoor.jpg",
"roof0": "./assets/catalog/roof0.jpg",
"roof1": "./assets/catalog/roof1.jpg",
"roof2": "./assets/catalog/roof2.jpg",
"roof3": "./assets/catalog/roof3.jpg",
"roof4": "./assets/catalog/roof4.jpg",
"intsill0": "./assets/catalog/intsill0.jpg",
"intsill1": "./assets/catalog/intsill1.jpg",
"intsill2": "./assets/catalog/intsill2.jpg",
"prum": "./assets/catalog/prum.jpg"
}
-534
View File
@@ -1,534 +0,0 @@
{
"rooms": [
[
"101",
"Zádveří",
"5,20",
"lam"
],
[
"102",
"Chodba",
"6,36",
"lam"
],
[
"103",
"Pokoj 1",
"10,62",
"lam"
],
[
"104",
"Pokoj 2",
"12,35",
"lam"
],
[
"105",
"Koupelna",
"7,72",
"tile"
],
[
"106",
"WC",
"1,40",
"tile"
],
[
"107",
"Ložnice",
"15,86",
"lam"
],
[
"108",
"Obývací pokoj + KK",
"37,69",
"lam"
],
[
"109",
"Technická místnost",
"3,09",
"tile"
]
],
"options": {
"entryModel": [
[
"solid",
"Plné",
"",
false,
"door0"
],
[
"line1",
"Jemná linka",
"",
true,
"door1"
],
[
"line2",
"Dvě linky",
"",
true,
"door2"
],
[
"slit",
"Svislé sklo",
"",
true,
"door3"
],
[
"three",
"3 skla",
"",
true,
"requestedDoor"
],
[
"four",
"4 skla",
"",
true,
"door5"
],
[
"five",
"5 skel",
"",
true,
"door6"
],
[
"wide",
"Velké sklo",
"",
true,
"door7"
]
],
"entryColor": [
[
"3003",
"RAL 3003",
"#8d1d2c"
],
[
"7012",
"RAL 7012",
"#575d5e"
],
[
"7016",
"RAL 7016",
"#383e42"
],
[
"9016",
"RAL 9016",
"#f1f0ea"
]
],
"extSill": [
[
"anthracite",
"Antracit",
"#383e42"
],
[
"white",
"Bílá",
"#eeefeb"
],
[
"elox",
"Elox",
"#bfc3c4"
],
[
"silver",
"Stříbrná",
"#b2b5b5"
],
[
"brown",
"Hnědá",
"#5c4338",
true
]
],
"gutter": [
[
"015",
"015 CERN",
"#262b2b"
],
[
"087",
"087 TMSE",
"#444f52"
],
[
"035",
"035 BRSE",
"#394c55"
],
[
"434",
"434 HNED",
"#5a4441"
],
[
"758",
"758 TMCE",
"#82282d",
true
],
[
"742",
"742 CICE",
"#a34832",
true
],
[
"045",
"045 SMET",
"#aaaeb2",
true
],
[
"044",
"044 SEME",
"#82878b",
true
]
],
"roof": [
[
"chestnut",
"Kaštanová",
"#6e3930",
false,
"roof0"
],
[
"brick",
"Cihlová",
"#934837",
false,
"roof1"
],
[
"terracotta",
"Terakota",
"#94402e",
false,
"roof2"
],
[
"black",
"Černá",
"#252a2b",
false,
"roof3"
],
[
"darkgray",
"Tmavě šedivá",
"#464a48",
false,
"roof4"
]
],
"blindColor": [
[
"140H",
"140 H",
"#e6e7e5"
],
[
"9007",
"9007",
"#969996"
],
[
"7012",
"7012",
"#575d5e"
],
[
"7022",
"7022",
"#4c4a44"
],
[
"7016",
"7016",
"#383e42"
]
],
"intSill": [
[
"white",
"Bílá",
"#edecea",
false,
"intsill0"
],
[
"gray",
"Žíhaná šedá",
"#b9b9b5",
false,
"intsill1"
],
[
"brown",
"Žíhaná hnědá",
"#b4a692",
false,
"intsill2"
]
],
"intHandle": [
[
"square",
"Hranatá"
],
[
"round",
"Oblá"
]
],
"shower": [
[
"tray",
"S vaničkou"
],
[
"walkin",
"Walk-in"
]
],
"kitchenColor": [
[
"#4b5449",
"Zelená",
"#4b5449"
],
[
"#eeeae1",
"Krémová",
"#eeeae1"
],
[
"#e9e9e5",
"Bílá",
"#e9e9e5"
],
[
"#b3aaa0",
"Kašmír",
"#b3aaa0"
],
[
"#686d69",
"Šedá",
"#686d69"
],
[
"#343a3d",
"Antracit",
"#343a3d"
]
],
"worktop": [
[
"oak",
"Dřevo",
"#a97a50"
],
[
"white",
"Bílá",
"#eeeeea"
],
[
"graphite",
"Grafit",
"#444946"
],
[
"stone",
"Kámen",
"#c7c5b9"
]
],
"backsplash": [
[
"none",
"Bez obkladu",
"#efede8"
],
[
"stone",
"Světlý kámen",
"#c7c5b9"
],
[
"white",
"Bílá",
"#eaeae5"
],
[
"worktop",
"Jako deska",
"#a97a50"
]
],
"faucet": [
[
"chrome",
"Chrom",
"#bec1c1"
],
[
"black",
"Černá",
"#303434"
]
],
"wallPaint": [
[
"#ffffff",
"HET bílá",
"#ffffff"
],
[
"#eeeae1",
"Teplá bílá",
"#eeeae1"
],
[
"#ded4c7",
"Písková",
"#ded4c7"
],
[
"#ccd2c5",
"Šalvěj",
"#ccd2c5"
]
],
"kitchenBodyColor": [
[
"#4b5449",
"Zelená",
"#4b5449"
],
[
"#eeeae1",
"Krémová",
"#eeeae1"
],
[
"#e9e9e5",
"Bílá",
"#e9e9e5"
],
[
"#b3aaa0",
"Kašmír",
"#b3aaa0"
],
[
"#686d69",
"Šedá",
"#686d69"
],
[
"#343a3d",
"Antracit",
"#343a3d"
]
]
},
"defaults": {
"entryModel": "three",
"entryColor": "7016",
"extSill": "anthracite",
"gutter": "015",
"roof": "darkgray",
"blinds": false,
"blindColor": "7016",
"blindDrop": 100,
"floors": {
"101": {
"type": "lam",
"sample": 6
},
"102": {
"type": "lam",
"sample": 6
},
"103": {
"type": "lam",
"sample": 6
},
"104": {
"type": "lam",
"sample": 6
},
"105": {
"type": "tile",
"sample": 1
},
"106": {
"type": "tile",
"sample": 1
},
"107": {
"type": "lam",
"sample": 6
},
"108": {
"type": "lam",
"sample": 6
},
"109": {
"type": "tile",
"sample": 1
}
},
"wallTile105": 1,
"wallTile106": 1,
"tileCorner": "profile",
"intSill": "white",
"wallPaint": "#ffffff",
"intDoor": "sample",
"intDoorColor": "#c3aa85",
"intDoorCode": "",
"intDoorType": "hinged",
"intHandle": "square",
"kitchenColor": "#4b5449",
"kitchenBodyColor": "#4b5449",
"worktop": "oak",
"backsplash": "none",
"shower": "tray",
"faucet": "chrome",
"hood": "facade",
"fans": true,
"aircon": true,
"ventilation": "local",
"hotWaterCirculation": false,
"radiator": "electric",
"note": ""
}
}
-32
View File
@@ -1,32 +0,0 @@
{
"plan": {
"title": "D.1.1.2.1.1 · Půdorys 1. NP",
"caption": "Dodaný projektový podklad. Dispozice a popisky místností vycházejí z tohoto výkresu.",
"src": "./assets/references/plan.jpg"
},
"kitchen1": {
"title": "Kuchyň · mrvz1_uprava.jpg",
"caption": "Dodaný referenční obrázek kuchyně. V modelu jsou moduly přizpůsobené půdorysu; jejich výrobní rozměry nejsou doložené.",
"src": "./assets/references/kitchen1.jpg"
},
"kitchen2": {
"title": "Kuchyň · mrvz2.jpeg",
"caption": "Dodaný referenční obrázek kuchyně.",
"src": "./assets/references/kitchen2.jpg"
},
"view1": {
"title": "Výhled · pohled_ven.jpeg",
"caption": "Dodaný referenční obrázek obytného prostoru a výhledu. Není použit jako georeferencované panorama.",
"src": "./assets/references/view1.jpg"
},
"view2": {
"title": "Obytný prostor · PHOTO-2026-05-26-20-38-11.jpg",
"caption": "Dodaný referenční obrázek materiálů, trámů a obytného prostoru.",
"src": "./assets/references/view2.jpg"
},
"survey": {
"title": "Polohopis a vyskopis - dodany podklad",
"caption": "Vysky jsou v Bpv. Body v modelu maji polohu graficky odectenou z PDF; nejde o zamerene souradnice domu.",
"src": "./assets/references/survey.png"
}
}

Some files were not shown because too many files have changed in this diff Show More