Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21cdd94c2c | ||
|
|
1f7e9c19bc |
@@ -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"
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
|
|
||||||
<!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">
|
||||||
@@ -24,9 +23,7 @@
|
|||||||
|
|
||||||
<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&v=beta&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>
|
||||||
|
|
||||||
@@ -67,24 +64,21 @@
|
|||||||
<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>
|
||||||
@@ -92,16 +86,14 @@
|
|||||||
|
|
||||||
<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>
|
||||||
@@ -113,8 +105,7 @@
|
|||||||
– 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>
|
||||||
@@ -132,8 +123,7 @@
|
|||||||
– 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>
|
||||||
@@ -148,13 +138,11 @@
|
|||||||
– 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>
|
||||||
@@ -174,8 +162,7 @@
|
|||||||
– 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>
|
||||||
@@ -203,5 +190,4 @@ Last updated: May 2013 -->
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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}
|
|
||||||
}
|
|
||||||
@@ -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}}
|
|
||||||
@@ -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í."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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);
|
|
||||||
@@ -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};
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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};
|
|
||||||
}
|
|
||||||
@@ -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é';
|
|
||||||
}
|
|
||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
@@ -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));
|
|
||||||
@@ -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.
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
@@ -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čenice | Slunečný 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í trojrozměrný model rodinného domu"></canvas>
|
|
||||||
<svg id="measure" aria-hidden="true"></svg>
|
|
||||||
<header>
|
|
||||||
<div class="eyebrow"><span class="dot"></span>RODINNÝ DŮM - Ločenice</div>
|
|
||||||
<h1>Slunečný vrch – Ločenice (Novohradské podhůří)</h1>
|
|
||||||
<div class="sub">Ločenice · 3D model + simulace slunce</div>
|
|
||||||
<div class="metrics">
|
|
||||||
<div class="metric"><strong>15,340 m</strong><small>Délka</small></div>
|
|
||||||
<div class="metric"><strong>9,390 m</strong><small>Šířka</small></div>
|
|
||||||
<div class="metric"><strong>23°</strong><small>Stř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ádání slunce"><span class="solar-mark">☀</span><span
|
|
||||||
class="toggle-word">Slunce</span></button><button class="btn icon" id="infoButton"
|
|
||||||
aria-label="Nastavení a informace o modelu" title="Nastavení">⚙︎</button><button
|
|
||||||
class="btn primary" id="saveGlb">Stáhnout <span
|
|
||||||
class="downloadText">model
|
|
||||||
</span>GLB</button></div>
|
|
||||||
<div class="panel" id="panel">
|
|
||||||
<h2>Model podle projektu</h2>
|
|
||||||
<p>Otáčecí exteriérový model v metrech. Hlavní rozměry a polohy otvorů
|
|
||||||
vycházejí z kót. Fasádní obklad: HardiePlank podle dodaného vzorníku; ostatní barevnost z přiložených vizualizací.</p>
|
|
||||||
<h3>Parametry</h3>
|
|
||||||
<div class="row"><span>Obálka domu</span><strong>15,340 × 9,390 m</strong></div>
|
|
||||||
<div class="row"><span>Hřeben nad podlahou 1. NP</span><strong>5,137 m</strong></div>
|
|
||||||
<div class="row"><span>Vrchol komína</span><strong>5,837 m</strong></div>
|
|
||||||
<div class="row"><span>Krytá terasa / navazující ochoz</span><strong>−0,03 / −0,21
|
|
||||||
m</strong></div>
|
|
||||||
<h3>Barevnost</h3>
|
|
||||||
<div class="swatches"><label>Fasá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ádní obklad · vstup a terasa</div>
|
|
||||||
<h4 id="hardieTitle">HardiePlank struktura dřeva</h4>
|
|
||||||
<p class="hardie-spec">James Hardie · vláknocement<br>Rozměr výrobku: 3 600 × 180 × 8 mm</p>
|
|
||||||
<div id="hardiePreview" class="hardie-preview" role="img" aria-label="Orientační náhled obkladu"></div>
|
|
||||||
<div class="hardie-preview-caption">ZVĚTŠENÝ DETAIL STRUKTURY · ORIENTAČNĚ</div>
|
|
||||||
<div class="hardie-selection" aria-live="polite"><strong id="hardieColorName">Oříškově hnědá</strong><small id="hardieProductCode">5771220 · výchozí</small></div>
|
|
||||||
<fieldset class="hardie-palette"><legend>Standardní barvy podle dodaného vzorní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ěhově bílá</span><span class="hardie-option-code">5671220</span><span class="hardie-check" aria-hidden="true">✓</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">Štěrkově šedá</span><span class="hardie-option-code">5751220</span><span class="hardie-check" aria-hidden="true">✓</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ě šedá</span><span class="hardie-option-code">5841220</span><span class="hardie-check" aria-hidden="true">✓</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ě šedá</span><span class="hardie-option-code">5851220</span><span class="hardie-check" aria-hidden="true">✓</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říškově hnědá</span><span class="hardie-option-code">5771220</span><span class="hardie-check" aria-hidden="true">✓</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">✓</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ě šedá</span><span class="hardie-option-code">5420122</span><span class="hardie-check" aria-hidden="true">✓</span></label></div></fieldset>
|
|
||||||
<p class="hardie-scope">Mění se pouze svislý fasádní obklad u vstupu a na terase, včetně bočních stěn. Podlaha a čela terasy ani schody se nemění.</p>
|
|
||||||
<a class="hardie-link" href="https://www.jameshardie.cz/cz/hardie-plank" target="_blank" rel="noopener noreferrer">HardiePlank na webu James Hardie ↗</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řiblížit strukturu ve 3D</button>
|
|
||||||
<details class="hardie-note"><summary>Odstíny, struktura a přesnost modelu</summary>
|
|
||||||
<p>Barvy jsou odečtené z vašeho obrázku vzorníku, nikoli z kalibrovaných dat výrobce. Odstín ve scéně ovlivňuje simulované slunce, stín i monitor. Shodu s dodaným výrobkem ověřte na fyzickém vzorku.</p>
|
|
||||||
<img class="hardie-reference" src="./assets/ui/image-1.jpg" alt="Produktová fotografie struktury dřeva z dodaného vzorníku">
|
|
||||||
<p>Ve 3D je doplněn jemný simulovaný reliéf inspirovaný touto fotografií. Nejde o originální výrobní texturu nebo 3D sken.</p>
|
|
||||||
<p>Vodorovné členění, geometrie a spáry jsou zachovány z původního modelu. Uvedený rozměr desky je specifikace výrobku, nikoli ověření kladečského plánu nebo montážních detailů.</p>
|
|
||||||
</details>
|
|
||||||
</section>
|
|
||||||
<p style="font-size:10px">Zvolené barvy a textura fasádního obkladu se uloží i do stažené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ík barev StoColor System (PDF)</a></p>
|
|
||||||
<h3>Kolmé pohledy</h3>
|
|
||||||
<select id="elevation">
|
|
||||||
<option value="">Vyber pohled...</option>
|
|
||||||
<option value="north">Severní pohled</option>
|
|
||||||
<option value="east">Východní pohled</option>
|
|
||||||
<option value="south">Jižní pohled</option>
|
|
||||||
<option value="west">Západní pohled</option>
|
|
||||||
</select>
|
|
||||||
<div class="buttons"><button class="btn" onclick="document.getElementById('saveGlb').click()">Stá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í</h3>
|
|
||||||
<p class="note">Pracovní vizualizační 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šky, oplechování, technické vybavení a spoje jsou
|
|
||||||
zjednodušené. Zábradlí je kovové podle pohledů a technické zprávy. V
|
|
||||||
případě rozdílu mají výkresy přednost před vizualizacemi.</p>
|
|
||||||
<h3>Použité podklady</h3>
|
|
||||||
<p style="font-size:10px">D.1.1.2.1.1 Půdorys 1NP<br>D.1.1.2.1.3 Půdorys střechy<br>D.1.1.2.3.1
|
|
||||||
Pohledy<br>D.1.1.2.2.1–3 Řezy A–A, B–B, C–C<br>D.1.1.1 Technická
|
|
||||||
zpráva<br>Obrázky 6_1.jpeg a 6_2.jpeg</p>
|
|
||||||
<div class="buttons"><button class="btn" id="savePng">Uložit pohled PNG</button><button class="btn"
|
|
||||||
id="resetColors">Původní barvy</button></div>
|
|
||||||
<div class="status"><span class="dot"></span>Samostatný soubor · funguje bez internetu ·
|
|
||||||
verze 05 · sloup terasy, vinotéka a vysoký strop</div>
|
|
||||||
</div>
|
|
||||||
<div id="direction"><span class="tag">AKTUÁLNÍ POHLED</span><span id="directionText">Terasa /
|
|
||||||
jihovýchod</span></div>
|
|
||||||
<div class="bottom">
|
|
||||||
<nav class="toolbar" aria-label="Ovládání modelu"><button class="tool"
|
|
||||||
data-preset="entry">Vstup</button><button class="tool"
|
|
||||||
data-preset="terrace">Terasa</button><button class="tool" data-preset="top">Shora</button><span
|
|
||||||
class="divider"></span><button class="tool" id="terrainButton"
|
|
||||||
title="Zobrazit nebo skrýt terén">Terén</button><button class="tool"
|
|
||||||
id="measureButton">Kóty</button><button class="tool optional active"
|
|
||||||
id="rotateButton">Otáčení</button><span class="divider"></span><button class="tool"
|
|
||||||
id="resetView" title="Obnovit výchozí pohled">Reset</button></nav>
|
|
||||||
|
|
||||||
<div class="site-controls" role="group" aria-label="Volitelné okolí domu; výchozí stav 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žením otáčej · Kolečkem přibliž
|
|
||||||
· Pravým tlačítkem posuň</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í</h4><span>Vnitřní omítka</span></div>
|
|
||||||
<div class="int-paint-fields">
|
|
||||||
<label class="int-paint-picker" title="Vybrat vlastní barvu zdí"><input id="interiorWallColor" type="color" value="#eeeae1" aria-label="Barva vnitřních zdí" aria-describedby="interiorPaintScope"></label>
|
|
||||||
<label class="int-paint-hex" for="interiorWallHex">Vlastní odstín / HEX<input id="interiorWallHex" type="text" value="#EEEAE1" maxlength="7" spellcheck="false" autocapitalize="characters" autocomplete="off" aria-label="HEX kód barvy vnitřních zdí"></label>
|
|
||||||
</div>
|
|
||||||
<div class="int-paint-presets" role="group" aria-label="Připravené barvy zdí">
|
|
||||||
<button type="button" data-interior-paint="#eeeae1" aria-pressed="true"><span class="int-paint-chip" style="--paint:#eeeae1" aria-hidden="true"></span>Teplá bílá</button>
|
|
||||||
<button type="button" data-interior-paint="#ffffff" aria-pressed="false"><span class="int-paint-chip" style="--paint:#ffffff" aria-hidden="true"></span>Čistá bílá</button>
|
|
||||||
<button type="button" data-interior-paint="#dfceb3" aria-pressed="false"><span class="int-paint-chip" style="--paint:#dfceb3" aria-hidden="true"></span>Písková</button>
|
|
||||||
<button type="button" data-interior-paint="#d9d9d4" aria-pressed="false"><span class="int-paint-chip" style="--paint:#d9d9d4" aria-hidden="true"></span>Světle šedá</button>
|
|
||||||
<button type="button" data-interior-paint="#c4cebc" aria-pressed="false"><span class="int-paint-chip" style="--paint:#c4cebc" aria-hidden="true"></span>Šalvějová</button>
|
|
||||||
<button type="button" data-interior-paint="#d5c7b9" aria-pressed="false"><span class="int-paint-chip" style="--paint:#d5c7b9" aria-hidden="true"></span>Jemně béžová</button>
|
|
||||||
</div>
|
|
||||||
<button class="int-paint-reset" id="interiorWallReset" type="button">Obnovit původní barvu zdí</button>
|
|
||||||
<p class="int-paint-note" id="interiorPaintScope">Mění všechny vnitřní omítnuté stěny. Strop, trámy, obklady a fasáda zůstávají beze změny. Zvolený odstín se promí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čítá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>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 575 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -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)
|
|
||||||
@@ -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}}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||