diff --git a/.DS_Store b/.DS_Store index 8d43b8d..ead1009 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/locenice/MANIFEST.json b/locenice/MANIFEST.json new file mode 100644 index 0000000..7a62c08 --- /dev/null +++ b/locenice/MANIFEST.json @@ -0,0 +1,334 @@ +{ + "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" + } +} diff --git a/locenice/README_CZ.md b/locenice/README_CZ.md new file mode 100644 index 0000000..59c08a4 --- /dev/null +++ b/locenice/README_CZ.md @@ -0,0 +1,148 @@ +# 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 + + + + + + + + Ločenice | Model domu / web + + + + + + + + + + + + +
+
RODINNÝ DŮM - Ločenice
+

Model domu / web – Ločenice (Novohradské podhůří)

+
Ločenice · 3D model + simulace slunce
+
+
15,340 mDélka
+
9,390 mŠířka
+
23°Střecha
+
+
+
+
+

Model podle projektu

+

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í.

+

Parametry

+
Obálka domu15,340 × 9,390 m
+
Hřeben nad podlahou 1. NP5,137 m
+
Vrchol komína5,837 m
+
Krytá terasa / navazující ochoz−0,03 / −0,21 + m
+

Barevnost

+
+
+
Fasádní obklad · vstup a terasa
+

HardiePlank struktura dřeva

+

James Hardie · vláknocement
Rozměr výrobku: 3 600 × 180 × 8 mm

+ +
ZVĚTŠENÝ DETAIL STRUKTURY · ORIENTAČNĚ
+
Oříškově hnědá5771220 · výchozí
+
Standardní barvy podle dodaného vzorníku
+

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í.

+ HardiePlank na webu James Hardie ↗ +
+ +
Odstíny, struktura a přesnost modelu +

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.

+ Produktová fotografie struktury dřeva z dodaného vzorníku +

Ve 3D je doplněn jemný simulovaný reliéf inspirovaný touto fotografií. Nejde o originální výrobní texturu nebo 3D sken.

+

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ů.

+
+
+

Zvolené barvy a textura fasádního obkladu se uloží i do staženého GLB.

+

Vzorník barev StoColor System (PDF)

+

Kolmé pohledy

+ +
+ +
+ Okolí, výškopis a přesnost +

Tři spodní přepínače jsou nezávislé a při otevření zapnuté. 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.

+

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.

+

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: 591,840 m Bpv. U vstupu −0,480 m, u + horní podesty schodů z parkování +0,390 m, na konci zahradních schodů dle C.3 −1,395 m. +

+

Povrch kombinuje původní zaměřené body s upravenými úrovněmi C.3. Mezi body je interpolovaný; + 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.

+

Rozdíl v podkladech: projekt uvádí výměru 1 010 m², obrys převzatý z vektorové kresby PDF v + měřítku domu vychází přibližně 988 m² (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.

+

Výpočet slunce a převod světových stran do os modelu jsou zachované. Původní azimut severního štítu + 351,5° a přibližná poloha obce nejsou tímto geodeticky ověřeny. +

+

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.

+
+

Úpravy modelu

+

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.

+

Rozsah a omezení

+

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í.

+

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.

+

Použité podklady

+

D.1.1.2.1.1 Půdorys 1NP
D.1.1.2.1.3 Půdorys střechy
D.1.1.2.3.1 + Pohledy
D.1.1.2.2.1–3 Řezy A–A, B–B, C–C
D.1.1.1 Technická + zpráva
Obrázky 6_1.jpeg a 6_2.jpeg

+
+
Samostatný soubor · funguje bez internetu · + verze 05 · sloup terasy, vinotéka a vysoký strop
+
+
AKTUÁLNÍ POHLEDTerasa / + jihovýchod
+
+ + +
+ + + + +
+ +
Tažením otáčej  ·  Kolečkem přibliž +  ·  Pravým tlačítkem posuň
+
+
+ + + + + SJZV + +
+ +
SLUNCE NAD DOMEM
21. 6. 2026 · 10:00
+
+
+
+
STUDIE OSVĚTLENÍ
+

Slunce a stíny

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

Posouvej čas a sleduj, kam dopadá slunce na terasu a fasády. Natočení pohledu + polohu slunce nemění.

+ +
+ Poloha a orientace domu +
+

Výchozí poloha: Ločenice, přibližný bod obce, nikoli geodeticky určený bod parcely. Ponecháno + původní nastavení 351,5°, 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.

+

Jižní štít: 171,5° · vstupní fasáda: 261,5°.

+ +

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

+
+
+ Co simulace počítá a co ne +

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.

+

Stíny vytváří geometrie tohoto modelu. Okolní domy, stromy, ploty a vzdálený horizont nejsou + modelovány. 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í.

+

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.

+

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.

+

Zdroje výpočtu a polohy: NOAA / Meeus · Ločenice – souřadnice. Připojení je potřeba jen pro otevření + těchto zdrojů, ne pro simulaci.

+
+

Pracovní studie stínů · bez internetu
Geometrie domu zůstává zachována. Export GLB + neobsahuje ovládání slunce.

+
+ + +
INTERIÉR / 1. NADZEMNÍ PODLAŽÍ

Dům bez střechy

+
+

Uvnitř domu

9 místností
+ +

Klikněte na místnost v mapě nebo vyberte připravený pohled.

+
Kuchyň a obytný prostor
+
+ + + +
+
Přejít do místnosti
+ +
+

Barva zdí

Vnitřní omítka
+
+ + +
+
+ + + + + + +
+ +

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.

+
+ +
+ + + +
+

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é.

+
Podklady a přesnost modelu +

Dispozice: 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.

+

Kuchyň: 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é.

+

Trámy: 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.

+

Orientační: 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.

+

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řisvětlení slouží k čitelnosti interiéru. Simulace slunce není výpočtem vnitřního denního osvětlení.

+
+
+ + +
+
+
Tažením otáčejte · kolečkem přibližujte · kliknutím na místnost vstupte dovnitř
+ +
+
+

Podklad

Dodaný podklad interiéru

+ +
+
+
+
Načítám 3D model...
+
+ +

Katalog ALLSTAV

Katalog ALLSTAV

+

Obnovit výchozí výběr?

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í.

+ + + + + + +
Načítání modelu…
+ + +
LOČENICE / ZDROJOVÝ MODEL

Projekt a podklady

+
+

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ů.

+

Kuchyň a obývací pokoj

+

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.

+

Pozemek a výšky

+

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.

+
Data pozemku JSON
+

Editace modelu

Soubor models/dum.glb 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.

+ +

+
+
+ + + + + \ No newline at end of file diff --git a/locenice/locenice/assets/catalog/door0.jpg b/locenice/locenice/assets/catalog/door0.jpg new file mode 100644 index 0000000..028e59d Binary files /dev/null and b/locenice/locenice/assets/catalog/door0.jpg differ diff --git a/locenice/locenice/assets/catalog/door1.jpg b/locenice/locenice/assets/catalog/door1.jpg new file mode 100644 index 0000000..f952617 Binary files /dev/null and b/locenice/locenice/assets/catalog/door1.jpg differ diff --git a/locenice/locenice/assets/catalog/door2.jpg b/locenice/locenice/assets/catalog/door2.jpg new file mode 100644 index 0000000..1265019 Binary files /dev/null and b/locenice/locenice/assets/catalog/door2.jpg differ diff --git a/locenice/locenice/assets/catalog/door3.jpg b/locenice/locenice/assets/catalog/door3.jpg new file mode 100644 index 0000000..6e826ef Binary files /dev/null and b/locenice/locenice/assets/catalog/door3.jpg differ diff --git a/locenice/locenice/assets/catalog/door4.jpg b/locenice/locenice/assets/catalog/door4.jpg new file mode 100644 index 0000000..176dbe0 Binary files /dev/null and b/locenice/locenice/assets/catalog/door4.jpg differ diff --git a/locenice/locenice/assets/catalog/door5.jpg b/locenice/locenice/assets/catalog/door5.jpg new file mode 100644 index 0000000..45712c3 Binary files /dev/null and b/locenice/locenice/assets/catalog/door5.jpg differ diff --git a/locenice/locenice/assets/catalog/door6.jpg b/locenice/locenice/assets/catalog/door6.jpg new file mode 100644 index 0000000..08e5e36 Binary files /dev/null and b/locenice/locenice/assets/catalog/door6.jpg differ diff --git a/locenice/locenice/assets/catalog/door7.jpg b/locenice/locenice/assets/catalog/door7.jpg new file mode 100644 index 0000000..df8ebae Binary files /dev/null and b/locenice/locenice/assets/catalog/door7.jpg differ diff --git a/locenice/locenice/assets/catalog/intsill0.jpg b/locenice/locenice/assets/catalog/intsill0.jpg new file mode 100644 index 0000000..52f947e Binary files /dev/null and b/locenice/locenice/assets/catalog/intsill0.jpg differ diff --git a/locenice/locenice/assets/catalog/intsill1.jpg b/locenice/locenice/assets/catalog/intsill1.jpg new file mode 100644 index 0000000..015d027 Binary files /dev/null and b/locenice/locenice/assets/catalog/intsill1.jpg differ diff --git a/locenice/locenice/assets/catalog/intsill2.jpg b/locenice/locenice/assets/catalog/intsill2.jpg new file mode 100644 index 0000000..b191da0 Binary files /dev/null and b/locenice/locenice/assets/catalog/intsill2.jpg differ diff --git a/locenice/locenice/assets/catalog/lam1.jpg b/locenice/locenice/assets/catalog/lam1.jpg new file mode 100644 index 0000000..277dc51 Binary files /dev/null and b/locenice/locenice/assets/catalog/lam1.jpg differ diff --git a/locenice/locenice/assets/catalog/lam2.jpg b/locenice/locenice/assets/catalog/lam2.jpg new file mode 100644 index 0000000..f816215 Binary files /dev/null and b/locenice/locenice/assets/catalog/lam2.jpg differ diff --git a/locenice/locenice/assets/catalog/lam3.jpg b/locenice/locenice/assets/catalog/lam3.jpg new file mode 100644 index 0000000..e5aff8b Binary files /dev/null and b/locenice/locenice/assets/catalog/lam3.jpg differ diff --git a/locenice/locenice/assets/catalog/lam4.jpg b/locenice/locenice/assets/catalog/lam4.jpg new file mode 100644 index 0000000..84541e9 Binary files /dev/null and b/locenice/locenice/assets/catalog/lam4.jpg differ diff --git a/locenice/locenice/assets/catalog/lam5.jpg b/locenice/locenice/assets/catalog/lam5.jpg new file mode 100644 index 0000000..2db462e Binary files /dev/null and b/locenice/locenice/assets/catalog/lam5.jpg differ diff --git a/locenice/locenice/assets/catalog/lam6.jpg b/locenice/locenice/assets/catalog/lam6.jpg new file mode 100644 index 0000000..52f3159 Binary files /dev/null and b/locenice/locenice/assets/catalog/lam6.jpg differ diff --git a/locenice/locenice/assets/catalog/lam7.jpg b/locenice/locenice/assets/catalog/lam7.jpg new file mode 100644 index 0000000..b5eeb0d Binary files /dev/null and b/locenice/locenice/assets/catalog/lam7.jpg differ diff --git a/locenice/locenice/assets/catalog/lam8.jpg b/locenice/locenice/assets/catalog/lam8.jpg new file mode 100644 index 0000000..cbb72c7 Binary files /dev/null and b/locenice/locenice/assets/catalog/lam8.jpg differ diff --git a/locenice/locenice/assets/catalog/lam9.jpg b/locenice/locenice/assets/catalog/lam9.jpg new file mode 100644 index 0000000..b21cfe6 Binary files /dev/null and b/locenice/locenice/assets/catalog/lam9.jpg differ diff --git a/locenice/locenice/assets/catalog/page12.jpg b/locenice/locenice/assets/catalog/page12.jpg new file mode 100644 index 0000000..bbd7bd8 Binary files /dev/null and b/locenice/locenice/assets/catalog/page12.jpg differ diff --git a/locenice/locenice/assets/catalog/page13.jpg b/locenice/locenice/assets/catalog/page13.jpg new file mode 100644 index 0000000..5aaecf0 Binary files /dev/null and b/locenice/locenice/assets/catalog/page13.jpg differ diff --git a/locenice/locenice/assets/catalog/page14.jpg b/locenice/locenice/assets/catalog/page14.jpg new file mode 100644 index 0000000..584f78e Binary files /dev/null and b/locenice/locenice/assets/catalog/page14.jpg differ diff --git a/locenice/locenice/assets/catalog/page15.jpg b/locenice/locenice/assets/catalog/page15.jpg new file mode 100644 index 0000000..f88ab3a Binary files /dev/null and b/locenice/locenice/assets/catalog/page15.jpg differ diff --git a/locenice/locenice/assets/catalog/page16.jpg b/locenice/locenice/assets/catalog/page16.jpg new file mode 100644 index 0000000..c48af6a Binary files /dev/null and b/locenice/locenice/assets/catalog/page16.jpg differ diff --git a/locenice/locenice/assets/catalog/page17.jpg b/locenice/locenice/assets/catalog/page17.jpg new file mode 100644 index 0000000..f28dcb5 Binary files /dev/null and b/locenice/locenice/assets/catalog/page17.jpg differ diff --git a/locenice/locenice/assets/catalog/page18.jpg b/locenice/locenice/assets/catalog/page18.jpg new file mode 100644 index 0000000..3be68a4 Binary files /dev/null and b/locenice/locenice/assets/catalog/page18.jpg differ diff --git a/locenice/locenice/assets/catalog/page19.jpg b/locenice/locenice/assets/catalog/page19.jpg new file mode 100644 index 0000000..f997138 Binary files /dev/null and b/locenice/locenice/assets/catalog/page19.jpg differ diff --git a/locenice/locenice/assets/catalog/page20.jpg b/locenice/locenice/assets/catalog/page20.jpg new file mode 100644 index 0000000..fb085c5 Binary files /dev/null and b/locenice/locenice/assets/catalog/page20.jpg differ diff --git a/locenice/locenice/assets/catalog/page21.jpg b/locenice/locenice/assets/catalog/page21.jpg new file mode 100644 index 0000000..3e90881 Binary files /dev/null and b/locenice/locenice/assets/catalog/page21.jpg differ diff --git a/locenice/locenice/assets/catalog/page22.jpg b/locenice/locenice/assets/catalog/page22.jpg new file mode 100644 index 0000000..74d4a2e Binary files /dev/null and b/locenice/locenice/assets/catalog/page22.jpg differ diff --git a/locenice/locenice/assets/catalog/page23.jpg b/locenice/locenice/assets/catalog/page23.jpg new file mode 100644 index 0000000..560dc7c Binary files /dev/null and b/locenice/locenice/assets/catalog/page23.jpg differ diff --git a/locenice/locenice/assets/catalog/page24.jpg b/locenice/locenice/assets/catalog/page24.jpg new file mode 100644 index 0000000..3c4dc16 Binary files /dev/null and b/locenice/locenice/assets/catalog/page24.jpg differ diff --git a/locenice/locenice/assets/catalog/page25.jpg b/locenice/locenice/assets/catalog/page25.jpg new file mode 100644 index 0000000..18caccd Binary files /dev/null and b/locenice/locenice/assets/catalog/page25.jpg differ diff --git a/locenice/locenice/assets/catalog/prum.jpg b/locenice/locenice/assets/catalog/prum.jpg new file mode 100644 index 0000000..d8cdc1a Binary files /dev/null and b/locenice/locenice/assets/catalog/prum.jpg differ diff --git a/locenice/locenice/assets/catalog/requestedDoor.jpg b/locenice/locenice/assets/catalog/requestedDoor.jpg new file mode 100644 index 0000000..b06fabe Binary files /dev/null and b/locenice/locenice/assets/catalog/requestedDoor.jpg differ diff --git a/locenice/locenice/assets/catalog/roof0.jpg b/locenice/locenice/assets/catalog/roof0.jpg new file mode 100644 index 0000000..5087fa8 Binary files /dev/null and b/locenice/locenice/assets/catalog/roof0.jpg differ diff --git a/locenice/locenice/assets/catalog/roof1.jpg b/locenice/locenice/assets/catalog/roof1.jpg new file mode 100644 index 0000000..e16bbb5 Binary files /dev/null and b/locenice/locenice/assets/catalog/roof1.jpg differ diff --git a/locenice/locenice/assets/catalog/roof2.jpg b/locenice/locenice/assets/catalog/roof2.jpg new file mode 100644 index 0000000..c271e33 Binary files /dev/null and b/locenice/locenice/assets/catalog/roof2.jpg differ diff --git a/locenice/locenice/assets/catalog/roof3.jpg b/locenice/locenice/assets/catalog/roof3.jpg new file mode 100644 index 0000000..5f59976 Binary files /dev/null and b/locenice/locenice/assets/catalog/roof3.jpg differ diff --git a/locenice/locenice/assets/catalog/roof4.jpg b/locenice/locenice/assets/catalog/roof4.jpg new file mode 100644 index 0000000..e791808 Binary files /dev/null and b/locenice/locenice/assets/catalog/roof4.jpg differ diff --git a/locenice/locenice/assets/catalog/tile1.jpg b/locenice/locenice/assets/catalog/tile1.jpg new file mode 100644 index 0000000..e397599 Binary files /dev/null and b/locenice/locenice/assets/catalog/tile1.jpg differ diff --git a/locenice/locenice/assets/catalog/tile2.jpg b/locenice/locenice/assets/catalog/tile2.jpg new file mode 100644 index 0000000..2b806b2 Binary files /dev/null and b/locenice/locenice/assets/catalog/tile2.jpg differ diff --git a/locenice/locenice/assets/catalog/tile3.jpg b/locenice/locenice/assets/catalog/tile3.jpg new file mode 100644 index 0000000..03d5c79 Binary files /dev/null and b/locenice/locenice/assets/catalog/tile3.jpg differ diff --git a/locenice/locenice/assets/catalog/tile4.jpg b/locenice/locenice/assets/catalog/tile4.jpg new file mode 100644 index 0000000..d9d9965 Binary files /dev/null and b/locenice/locenice/assets/catalog/tile4.jpg differ diff --git a/locenice/locenice/assets/catalog/tile5.jpg b/locenice/locenice/assets/catalog/tile5.jpg new file mode 100644 index 0000000..3acfd19 Binary files /dev/null and b/locenice/locenice/assets/catalog/tile5.jpg differ diff --git a/locenice/locenice/assets/catalog/tile6.jpg b/locenice/locenice/assets/catalog/tile6.jpg new file mode 100644 index 0000000..4b01a60 Binary files /dev/null and b/locenice/locenice/assets/catalog/tile6.jpg differ diff --git a/locenice/locenice/assets/catalog/tile7.jpg b/locenice/locenice/assets/catalog/tile7.jpg new file mode 100644 index 0000000..a48bb17 Binary files /dev/null and b/locenice/locenice/assets/catalog/tile7.jpg differ diff --git a/locenice/locenice/assets/catalog/tile8.jpg b/locenice/locenice/assets/catalog/tile8.jpg new file mode 100644 index 0000000..7dd6387 Binary files /dev/null and b/locenice/locenice/assets/catalog/tile8.jpg differ diff --git a/locenice/locenice/assets/catalog/tile9.jpg b/locenice/locenice/assets/catalog/tile9.jpg new file mode 100644 index 0000000..83393e2 Binary files /dev/null and b/locenice/locenice/assets/catalog/tile9.jpg differ diff --git a/locenice/locenice/assets/references/kitchen1.jpg b/locenice/locenice/assets/references/kitchen1.jpg new file mode 100644 index 0000000..c304b70 Binary files /dev/null and b/locenice/locenice/assets/references/kitchen1.jpg differ diff --git a/locenice/locenice/assets/references/kitchen2.jpg b/locenice/locenice/assets/references/kitchen2.jpg new file mode 100644 index 0000000..240b136 Binary files /dev/null and b/locenice/locenice/assets/references/kitchen2.jpg differ diff --git a/locenice/locenice/assets/references/plan.jpg b/locenice/locenice/assets/references/plan.jpg new file mode 100644 index 0000000..c835196 Binary files /dev/null and b/locenice/locenice/assets/references/plan.jpg differ diff --git a/locenice/locenice/assets/references/survey.png b/locenice/locenice/assets/references/survey.png new file mode 100644 index 0000000..8eb90e7 Binary files /dev/null and b/locenice/locenice/assets/references/survey.png differ diff --git a/locenice/locenice/assets/references/view1.jpg b/locenice/locenice/assets/references/view1.jpg new file mode 100644 index 0000000..288432e Binary files /dev/null and b/locenice/locenice/assets/references/view1.jpg differ diff --git a/locenice/locenice/assets/references/view2.jpg b/locenice/locenice/assets/references/view2.jpg new file mode 100644 index 0000000..cb45038 Binary files /dev/null and b/locenice/locenice/assets/references/view2.jpg differ diff --git a/locenice/locenice/assets/ui/image-1.jpg b/locenice/locenice/assets/ui/image-1.jpg new file mode 100644 index 0000000..b5455c7 Binary files /dev/null and b/locenice/locenice/assets/ui/image-1.jpg differ diff --git a/locenice/locenice/css/app.css b/locenice/locenice/css/app.css new file mode 100644 index 0000000..27268f4 --- /dev/null +++ b/locenice/locenice/css/app.css @@ -0,0 +1,1728 @@ + + :root { + --ink: #26342c; + --muted: #737b72; + --paper: #fafbf6; + --line: #d8ded4; + --accent: #365944 + } + + * { + box-sizing: border-box + } + + html, + body { + margin: 0; + height: 100%; + overflow: hidden; + font-family: Arial, Helvetica, sans-serif; + color: var(--ink); + background: #e7ebe3 + } + + button, + input, + select { + font: inherit + } + + button { + cursor: pointer; + color: inherit + } + + button:focus-visible, + select:focus-visible, + input:focus-visible { + outline: 3px solid #91aa92; + outline-offset: 3px + } + + #view { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + display: block; + touch-action: none + } + + header { + position: fixed; + left: 32px; + top: 29px; + pointer-events: none + } + + .eyebrow { + font-size: 10px; + font-weight: 700; + letter-spacing: 2.6px; + color: #647669; + display: flex; + align-items: center; + gap: 9px + } + + .dot { + height: 6px; + width: 6px; + border-radius: 50%; + background: #536e57 + } + + h1 { + font-size: 31px; + font-weight: 500; + letter-spacing: -1.1px; + line-height: 1.13; + margin: 10px 0 7px + } + + .sub { + font-size: 12px; + color: var(--muted); + line-height: 1.5 + } + + .metrics { + display: flex; + gap: 24px; + margin-top: 20px + } + + .metric strong { + font-size: 18px; + font-weight: 500; + letter-spacing: -.4px + } + + .metric small { + display: block; + margin-top: 4px; + font-size: 9px; + text-transform: uppercase; + letter-spacing: 1.6px; + color: var(--muted) + } + + .topright { + position: fixed; + right: 27px; + top: 28px; + display: flex; + gap: 8px + } + + .btn { + border: 1px solid #d4dcd1; + background: #f9fbf5ed; + border-radius: 9px; + padding: 10px 14px; + font-size: 12px; + line-height: 1; + box-shadow: 0 2px 8px #243c2210 + } + + .btn:hover { + background: #fff + } + + .btn.primary { + background: var(--accent); + border-color: var(--accent); + color: white + } + + .btn.active { + background: #dfebdf; + border-color: #99af9a + } + + .icon { + width: 37px; + height: 37px; + padding: 0; + font-size: 16px + } + + .bottom { + position: fixed; + left: 50%; + bottom: 32px; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 11px; + max-width: calc(100% - 32px) + } + + .toolbar { + display: flex; + align-items: center; + gap: 4px; + padding: 7px; + border: 1px solid #ffffffb0; + background: #f9fbf6ed; + border-radius: 15px; + box-shadow: 0 10px 34px #24381e16; + white-space: nowrap + } + + .tool { + border: 0; + background: transparent; + padding: 10px 13px; + font-size: 12px; + border-radius: 8px + } + + .tool.active { + background: var(--accent); + color: white + } + + .tool:hover { + background: #e8eee5 + } + + .tool.active:hover { + background: #365944 + } + + .divider { + width: 1px; + height: 21px; + background: #dce2d8; + margin: 0 4px + } + + .help { + font-size: 10px; + letter-spacing: .25px; + color: #687567; + text-align: center; + background: #e7ebe3c0; + border-radius: 6px; + padding: 4px 9px + } + + .compass { + position: fixed; + right: 29px; + bottom: 38px; + width: 84px; + height: 84px + } + + .compass svg { + width: 84px; + height: 84px; + overflow: visible + } + + .compass text { + font: 9px Arial; + fill: #596959 + } + + .compass line { + stroke: #b4c0ae; + stroke-width: 1 + } + + #northArrow { + fill: #365944 + } + + #direction { + position: fixed; + left: 32px; + bottom: 37px; + font-size: 11px; + line-height: 1.6; + color: #576d5c + } + + .tag { + font-size: 8px; + letter-spacing: 1.8px; + color: #7d8979; + display: block + } + + .panel { + position: fixed; + right: 27px; + top: 77px; + max-height: calc(100% - 184px); + width: 337px; + overflow: auto; + background: #fafcf6f5; + border: 1px solid #fff; + border-radius: 15px; + box-shadow: 0 12px 45px #29392525; + padding: 23px; + display: none + } + + .panel.open { + display: block + } + + .panel h2 { + font-size: 19px; + letter-spacing: -.3px; + font-weight: 500; + margin: 0 0 10px + } + + .panel h3 { + font-size: 9px; + text-transform: uppercase; + letter-spacing: 1.8px; + color: #70816e; + margin: 23px 0 10px + } + + .panel p { + font-size: 12px; + line-height: 1.65; + color: #667261; + margin: 7px 0 + } + + .panel .row { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + border-bottom: 1px solid #e0e6db; + padding: 9px 0 + } + + .swatches { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 10px + } + + .swatches label { + display: flex; + flex-direction: column; + gap: 7px; + font-size: 10px; + color: #667261 + } + + input[type=color] { + width: 100%; + height: 30px; + border: 0; + background: none; + padding: 0; + cursor: pointer + } + + select { + width: 100%; + padding: 10px; + border: 1px solid #d6e0d0; + border-radius: 6px; + background: #f5f8f0; + color: #334931; + font-size: 12px + } + + .note { + border-left: 2px solid #a5b99d; + padding-left: 11px + } + + .panel .buttons { + display: flex; + gap: 8px; + margin-top: 15px; + flex-wrap: wrap + } + + .status { + display: flex; + align-items: center; + gap: 7px; + font-size: 10px; + color: #6c7f68; + margin-top: 16px + } + + #loading { + position: fixed; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: #e7ebe3; + z-index: 10; + gap: 20px + } + + .spinner { + width: 34px; + height: 34px; + border-radius: 50%; + border: 2px solid #bccabb; + border-top-color: #365944; + animation: spin 1s linear infinite + } + + @keyframes spin { + to { + transform: rotate(360deg) + } + } + + #loading .label { + font-size: 12px; + color: #597054 + } + + #loading.error { + padding: 30px; + text-align: center + } + + .errorDetail { + font-size: 12px; + max-width: 480px; + line-height: 1.7; + color: #51624d + } + + #measure { + position: fixed; + inset: 0; + pointer-events: none; + width: 100%; + height: 100%; + display: none + } + + #measure line { + stroke: #3f5b42; + stroke-width: 1 + } + + #measure circle { + fill: #456749 + } + + #measure text { + fill: #3a553d; + font: 12px Arial; + paint-order: stroke; + stroke: #eff3e9; + stroke-width: 5px; + stroke-linejoin: round + } + + #toast { + position: fixed; + left: 50%; + top: 28px; + transform: translateX(-50%); + font-size: 12px; + background: #2f4739; + color: #fff; + padding: 11px 17px; + border-radius: 9px; + opacity: 0; + transition: opacity .25s; + pointer-events: none + } + + .full { + display: none + } + + @media(max-width:850px) { + header { + left: 20px; + top: 22px + } + + .topright { + right: 18px; + top: 22px + } + + h1 { + font-size: 26px + } + + .metrics { + gap: 19px; + margin-top: 15px + } + + .metric strong { + font-size: 16px + } + + .bottom { + bottom: 21px + } + + .tool { + padding: 10px + } + + .compass { + display: none + } + + #direction { + bottom: 114px; + left: 20px + } + + .panel { + right: 18px; + top: 69px; + width: 320px + } + + .help { + font-size: 9px + } + + .optional { + display: none + } + } + + @media(max-width:520px) { + header { + left: 16px; + top: 19px + } + + .eyebrow { + font-size: 8px; + letter-spacing: 1.7px + } + + h1 { + font-size: 24px + } + + .sub { + font-size: 10px; + max-width: 210px + } + + .metrics { + gap: 16px; + margin-top: 13px + } + + .metric strong { + font-size: 15px + } + + .metric small { + font-size: 8px; + letter-spacing: 1px + } + + .topright { + right: 13px; + top: 18px + } + + .topright .btn.primary { + font-size: 10px; + padding: 11px 10px + } + + .topright .icon { + width: 33px + } + + .toolbar { + gap: 0; + padding: 5px; + border-radius: 12px + } + + .tool { + padding: 10px 9px; + font-size: 10px + } + + .divider { + margin: 0 1px + } + + .bottom { + bottom: 17px; + gap: 8px + } + + .help { + max-width: 320px; + font-size: 9px; + line-height: 1.5 + } + + #direction { + bottom: 100px; + left: 17px; + font-size: 10px + } + + .panel { + top: 62px; + right: 12px; + left: 12px; + width: auto; + max-height: calc(100% - 159px); + padding: 20px + } + + .btn { + font-size: 11px + } + + .measureWord { + display: none + } + + .topright .downloadText { + display: none + } + } + + :root { + --solar: #b8812d; + --solar-soft: #f4e9cf; + --sun-side: 376px; + --sun-sheet: 340px + } + + body.sun-open #view, + body.sun-open #measure, + body.sun-open #sunOverlay { + width: calc(100% - var(--sun-side)) + } + + #sunOverlay { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; + overflow: hidden + } + + body.sun-open .bottom { + left: calc((100% - var(--sun-side))/2) + } + + body.sun-open .compass { + right: calc(var(--sun-side) + 18px) + } + + header h1 { + font-size: 30px; + letter-spacing: -1px + } + + header .metrics { + margin-top: 18px; + gap: 22px + } + + .solar-mark { + color: var(--solar); + font-size: 16px; + line-height: 1 + } + + #sunToggle { + display: flex; + gap: 8px; + align-items: center; + font-weight: 600 + } + + #sunToggle.active { + background: #f3e8ce; + border-color: #d8bd85 + } + + #sunPanel { + display: none; + position: fixed; + right: 16px; + top: 79px; + bottom: 20px; + width: 348px; + padding: 22px; + background: #fafbf7f7; + border: 1px solid #fff; + border-radius: 18px; + box-shadow: 0 12px 45px #29392517; + overflow: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + scrollbar-color: #c1cabe transparent; + z-index: 3 + } + + #sunPanel.open { + display: block + } + + .sun-panel-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 15px; + margin-bottom: 17px + } + + .sun-kicker { + font-size: 9px; + font-weight: 700; + letter-spacing: 2px; + color: #8d753d; + margin-bottom: 6px + } + + #sunPanel h2 { + font-size: 23px; + letter-spacing: -.65px; + font-weight: 500; + margin: 0 + } + + #sunPanel .close-panel { + border: 0; + background: #eef1e9; + border-radius: 50%; + width: 29px; + height: 29px; + font-size: 20px; + line-height: 20px; + color: #6a7466 + } + + .sun-date-row { + display: grid; + grid-template-columns: 1fr 105px; + gap: 12px; + align-items: end + } + + .sun-field { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; + font-size: 10px; + color: #6f7969 + } + + .sun-field input:not([type=range]), + .sun-field select { + width: 100%; + min-width: 0; + padding: 9px 9px; + height: 39px; + border: 1px solid #d9e0d2; + border-radius: 8px; + background: #fff; + color: #354232; + font-size: 12px + } + + .sun-field #sunClock { + font-size: 20px; + letter-spacing: -.6px; + padding: 6px; + width: 105px + } + + .sun-seasons { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 5px; + margin-top: 11px + } + + .sun-seasons button { + border: 1px solid #e0e4d9; + border-radius: 7px; + background: #f4f6ef; + padding: 8px 2px; + font-size: 10px; + color: #53624d + } + + .sun-seasons button.active { + background: var(--solar-soft); + border-color: #dac08c; + color: #765923 + } + + .sun-status-line { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 10px; + color: #7b826f; + margin: 16px 0 5px; + gap: 8px + } + + #dayState { + display: flex; + align-items: center; + gap: 6px; + color: #856527 + } + + #dayState:before { + content: ''; + width: 5px; + height: 5px; + border-radius: 50%; + background: #c3933c + } + + .sun-graph { + display: block; + width: 100%; + height: 85px; + cursor: crosshair; + touch-action: none + } + + .sun-scrub { + width: 100%; + accent-color: var(--solar); + margin: 8px 0 3px; + cursor: ew-resize; + height: 22px + } + + .sun-ticks { + display: flex; + justify-content: space-between; + font-size: 9px; + color: #8b9283 + } + + .sun-play-row { + display: grid; + grid-template-columns: 1fr 90px 46px; + gap: 7px; + margin-top: 14px + } + + .sun-play-row button, + .sun-play-row select { + height: 36px; + padding: 7px 9px; + font-size: 11px; + border-radius: 8px; + border: 1px solid #d6ddce; + background: #f2f5ec; + white-space: nowrap + } + + .sun-play-row #sunPlay { + background: #3c5d48; + color: white; + border-color: #3c5d48; + font-weight: 600 + } + + .sun-play-row #sunSpeed { + font-size: 10px; + padding: 7px 4px + } + + .sun-readouts { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; + margin-top: 20px; + padding-top: 17px; + border-top: 1px solid #e1e5d9 + } + + .sun-stat { + font-size: 10px; + color: #7b8372 + } + + .sun-stat strong { + display: block; + font-weight: 500; + font-size: 24px; + letter-spacing: -.7px; + color: #324630; + margin: 4px 0 3px; + font-variant-numeric: tabular-nums + } + + .sun-stat small { + font-size: 10px; + color: #929883 + } + + .sun-events { + display: flex; + justify-content: space-between; + gap: 12px; + font-size: 10px; + color: #718066; + margin: 16px 0 2px; + padding: 12px 0; + border-top: 1px solid #e5e8df; + border-bottom: 1px solid #e5e8df + } + + .sun-events b { + font-weight: 600; + color: #3e563b; + margin-left: 4px + } + + .sun-options { + display: flex; + gap: 17px; + flex-wrap: wrap; + margin: 14px 0 12px + } + + .sun-options label { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: #52684d; + cursor: pointer + } + + .sun-options input { + accent-color: #597447; + margin: 0; + width: 14px; + height: 14px + } + + .sun-hint { + font-size: 10px; + line-height: 1.6; + color: #84907a; + margin: 8px 0 13px + } + + .sun-advanced { + margin-top: 15px; + border-top: 1px solid #e1e5d9; + padding-top: 13px + } + + .sun-advanced summary { + font-size: 11px; + cursor: pointer; + color: #4d6645; + padding: 3px 0 + } + + .sun-advanced .fields { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-top: 14px + } + + .sun-advanced .full-row { + grid-column: 1 / -1 + } + + .sun-advanced p { + font-size: 10px; + line-height: 1.65; + color: #7f8975; + margin: 10px 0 + } + + .sun-advanced a { + color: #536a4f + } + + .sun-advanced .mini-btn { + font-size: 10px; + border: 1px solid #d2dbcb; + background: #f5f8ef; + border-radius: 7px; + padding: 8px 10px + } + + .sun-export { + width: 100%; + padding: 10px; + border: 1px solid #ced8c6; + border-radius: 8px; + background: #eef3e8; + font-size: 11px; + color: #47603e; + margin-top: 13px + } + + .sun-footnote { + font-size: 9px; + line-height: 1.6; + color: #969d8b; + margin: 13px 0 0 + } + + #sunStamp { + position: fixed; + left: 32px; + top: 180px; + pointer-events: none; + color: #6c775f; + font-size: 11px; + line-height: 1.55 + } + + #sunStamp strong { + font-weight: 500; + color: #a07836; + font-variant-numeric: tabular-nums + } + + #solarMessage { + color: #977026; + font-size: 10px; + line-height: 1.5; + min-height: 0 + } + + #solarMessage:empty { + display: none + } + + #panel { + z-index: 4 + } + + #sunOverlay text { + font: 10px Arial; + paint-order: stroke; + stroke: #f3f4e9; + stroke-width: 3px; + stroke-linejoin: round; + fill: #9a702d + } + + #sunOverlay path, + #sunOverlay line { + vector-effect: non-scaling-stroke + } + + body.night header, + body.night header h1 { + color: #d3dec9 + } + + body.night header .sub, + body.night header .metric small, + body.night header .eyebrow, + body.night #direction, + body.night #sunStamp { + color: #aab9a5 + } + + body.night #sunStamp strong { + color: #d6ba76 + } + + #toast { + z-index: 12; + top: 20px + } + + @media(min-width:851px) { + .topright { + right: 20px; + top: 25px + } + + .help { + font-size: 9px + } + + .sun-open #direction { + bottom: 26px; + font-size: 10px + } + + .sun-open .bottom { + bottom: 28px + } + + .sun-open .compass { + bottom: 28px + } + + .sun-open .toolbar .tool { + font-size: 11px; + padding: 10px 10px + } + } + + @media(max-width:1100px) and (min-width:851px) { + header { + left: 22px; + top: 27px + } + + header h1 { + font-size: 25px + } + + .metrics { + display: none !important + } + + #sunStamp { + left: 22px; + top: 115px + } + + .sun-open .compass { + display: none + } + + .sun-open #direction { + display: none + } + + .sun-open .toolbar .tool { + padding: 10px 7px; + font-size: 10px + } + } + + @media(max-width:850px) { + :root { + --sun-sheet: min(42vh, 350px) + } + + body.sun-open #view, + body.sun-open #measure, + body.sun-open #sunOverlay { + width: 100%; + height: calc(100% - var(--sun-sheet)) + } + + body.sun-open .bottom { + left: 50%; + bottom: calc(var(--sun-sheet) + 8px); + gap: 0 + } + + body.sun-open .help { + display: none + } + + body.sun-open #direction { + display: none + } + + body.sun-open .compass { + display: none + } + + #sunPanel { + top: auto; + bottom: 0; + right: 0; + left: 0; + width: 100%; + height: var(--sun-sheet); + padding: 16px 21px 25px; + border-radius: 20px 20px 0 0; + box-shadow: 0 -8px 30px #26392514 + } + + .sun-panel-head { + display: none + } + + .sun-date-row { + grid-template-columns: 1fr 120px; + gap: 16px + } + + .sun-date-row .sun-field { + font-size: 9px + } + + .sun-field #sunClock { + width: 100%; + font-size: 23px; + height: 40px + } + + .sun-field input:not([type=range]) { + height: 40px + } + + .sun-seasons { + margin-top: 9px + } + + .sun-seasons button { + padding: 7px 0; + font-size: 10px + } + + .sun-status-line { + margin: 12px 0 0 + } + + .sun-graph { + height: 62px + } + + .sun-scrub { + margin: 4px 0 0 + } + + .sun-play-row { + margin-top: 10px; + grid-template-columns: 1fr 94px 51px + } + + .sun-readouts { + margin-top: 15px; + padding-top: 12px + } + + .sun-stat strong { + font-size: 23px + } + + .sun-events { + margin-top: 13px + } + + #sunStamp { + left: 18px; + top: 105px; + font-size: 10px + } + + header { + left: 18px; + top: 19px + } + + header h1 { + font-size: 25px; + margin-top: 9px + } + + .metrics { + display: none !important + } + + header .sub { + max-width: 230px; + font-size: 10px + } + + .topright { + right: 14px; + top: 19px; + gap: 6px + } + + #saveGlb { + display: none + } + + #sunToggle { + font-size: 10px; + padding: 10px 9px; + gap: 4px + } + + .topright .icon { + width: 33px; + height: 33px + } + + .sun-open .tool { + font-size: 10px; + padding: 9px 8px + } + + .sun-open .bottom { + max-width: calc(100% - 16px) + } + + #panel { + top: 66px; + bottom: 15px; + max-height: none + } + } + + @media(max-width:420px) { + header h1 { + font-size: 23px + } + + header .eyebrow { + font-size: 8px; + letter-spacing: 1.8px + } + + #sunToggle .toggle-word { + display: none + } + + #sunToggle { + width: 34px; + height: 34px; + justify-content: center; + padding: 0 + } + + .sun-open .tool { + padding: 9px 7px; + font-size: 10px + } + } + + @media(max-height:500px) and (max-width:850px) { + :root { + --sun-sheet: 170px + } + + header .sub, + #sunStamp { + display: none + } + + header h1 { + font-size: 21px + } + + .sun-graph { + display: none + } + + #sunPanel { + padding: 12px 20px + } + + .sun-readouts { + display: none + } + } + + /* Optional surroundings: independent switches, all on when the page opens. */ + .site-controls { + display: flex; + align-items: center; + gap: 5px; + padding: 6px; + border: 1px solid #fff; + border-radius: 13px; + background: #f9fbf6f2; + box-shadow: 0 6px 24px #24381e13; + max-width: 100%; + } + + .site-switch { + display: flex; + align-items: center; + gap: 7px; + padding: 9px 10px; + border: 1px solid transparent; + border-radius: 8px; + font-size: 11px; + color: #596b55; + cursor: pointer; + white-space: nowrap; + user-select: none; + } + + .site-switch:hover { + background: #eef2e8; + } + + .site-switch.selected { + background: #e1ecdc; + border-color: #b9cbb1; + color: #304d35; + } + + .site-switch input { + appearance: none; + width: 13px; + height: 13px; + margin: 0; + border: 1px solid #a7b29f; + border-radius: 4px; + background: #fff; + flex-shrink: 0; + cursor: pointer; + display: grid; + place-items: center; + } + + .site-switch input:checked { + background: #426448; + border-color: #426448; + } + + .site-switch input:checked:after { + content: '\2713'; + color: #fff; + font-size: 10px; + line-height: 1; + } + + .site-fit { + display: grid; + place-items: center; + width: 32px; + height: 32px; + flex-shrink: 0; + border: 0; + border-left: 1px solid #dce2d5; + border-radius: 5px; + background: transparent; + color: #59734e; + } + + .site-fit:hover { + background: #e7eee1; + } + + .site-fit svg { + width: 17px; + height: 17px; + } + + #siteMeta { + font-size: 9px; + letter-spacing: .1px; + color: #6e8066; + line-height: 1.2; + background: #f5f8eef0; + padding: 4px 8px; + border-radius: 5px; + pointer-events: none; + } + + #siteMeta[hidden] { + display: none; + } + + .bottom { + gap: 7px; + z-index: 2; + } + + .site-note { + margin-top: 14px; + border-top: 1px solid #dce4d5; + padding-top: 10px; + } + + .site-note summary { + cursor: pointer; + font-size: 12px; + color: #37563b; + padding: 6px 0; + } + + .site-note p { + font-size: 11px; + } + + @media(max-width:850px) { + .bottom { + max-width: calc(100% - 20px); + } + + #direction { + bottom: 161px; + } + + .site-controls { + gap: 2px; + padding: 5px; + } + + .site-switch { + font-size: 10px; + padding: 8px 8px; + gap: 5px; + } + + body.sun-open .bottom { + gap: 5px; + } + + #siteMeta { + font-size: 8px; + } + } + + @media(max-width:520px) { + .bottom { + gap: 5px; + bottom: 12px; + } + + .site-switch { + font-size: 9px; + padding: 8px 6px; + gap: 5px; + } + + .site-fit { + width: 27px; + height: 28px; + } + + .site-controls { + gap: 0; + padding: 4px; + } + + .site-switch input { + width: 12px; + height: 12px; + } + + .bottom .help { + display: none; + } + + #direction { + bottom: 111px; + } + + body.sun-open #siteMeta { + display: none; + } + } + + @media(max-width:360px) { + .site-switch { + font-size: 8.5px; + padding: 8px 4px; + gap: 4px; + } + + .site-fit { + width: 24px; + } + } + /* Fixed product palette, scoped to facade cladding only. */ + .swatches { grid-template-columns: 1fr 1fr; } + .hardie-card { margin: 15px 0 17px; padding: 14px; border: 1px solid #d8ded4; border-radius: 11px; background: #ffffffb8; } + .hardie-kicker { font-size: 9px; letter-spacing: 1.4px; text-transform: uppercase; color: #677b61; margin-bottom: 8px; } + .hardie-card h4 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.2px; } + .hardie-card .hardie-spec { font-size: 10px; line-height: 1.6; margin: 5px 0 10px; } + .hardie-preview { height: 48px; border-radius: 4px; background: #776e65; background-size: 400% 300%; background-position: 48% 48%; border: 1px solid #0002; box-shadow: inset 0 -3px 0 #0002, inset 0 1px 0 #fff2; } + .hardie-selection { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; padding: 9px 0 10px; color: #304835; } + .hardie-selection strong { font-size: 12px; font-weight: 600; } + .hardie-selection small { font-size: 9px; color: #778270; white-space: nowrap; } + .hardie-palette { border: 0; padding: 0; margin: 0; min-width: 0; } + .hardie-palette legend { font-size: 9px; color: #6d7b64; margin: 0 0 8px; padding: 0; } + .hardie-options { display: grid; grid-template-columns: 1fr; gap: 4px; } + .hardie-option { display: flex; align-items: center; gap: 8px; min-height: 33px; padding: 5px 8px; border: 1px solid #e1e6dd; border-radius: 6px; cursor: pointer; background: #fbfcf9; position: relative; } + .hardie-option:hover { border-color: #aabda1; background: #f4f7ef; } + .hardie-option.selected { background: #eaf1e5; border-color: #87a57c; } + .hardie-option:focus-within { outline: 2px solid #91aa92; outline-offset: 2px; } + .hardie-option input { position: absolute; width: 1px; height: 1px; opacity: 0; } + .hardie-chip { width: 22px; height: 21px; border-radius: 3px; flex-shrink: 0; border: 1px solid #0002; background: var(--chip); } + .hardie-option-name { font-size: 11px; flex: 1; line-height: 1.3; } + .hardie-option-code { font-size: 9px; color: #7d8577; font-variant-numeric: tabular-nums; } + .hardie-check { width: 11px; font-size: 12px; color: #365944; opacity: 0; } + .hardie-option.selected .hardie-check { opacity: 1; } + .hardie-card .hardie-scope { font-size: 10px; color: #6d7965; margin: 10px 0 7px; line-height: 1.5; } + .hardie-link { color: #365944; font-size: 11px; text-underline-offset: 3px; } + .hardie-detail-buttons { display: flex; gap: 6px; margin-top: 10px; } + .hardie-detail-buttons button { background: #f3f6ef; border: 1px solid #d6dfcf; border-radius: 6px; padding: 7px 8px; font-size: 10px; flex: 1; } + .hardie-detail-buttons button:hover { background: #e7eee0; } + .hardie-note { margin-top: 11px; border-top: 1px solid #e0e6da; padding-top: 8px; } + .hardie-note summary { font-size: 10px; cursor: pointer; color: #65765c; } + .hardie-card .hardie-note p { font-size: 10px; line-height: 1.55; } + .hardie-reference { width: 100%; height: auto; display: block; border-radius: 5px; margin: 8px 0; } + @media(max-width:360px) { .hardie-card { padding: 11px; } .hardie-option-code { font-size: 8px; } } + + .hardie-structure-button { width: 100%; margin-top: 6px; padding: 7px; border: 1px solid #d6dfcf; border-radius: 6px; background: #f3f6ef; font-size: 10px; } + .hardie-preview-caption { font-size: 9px; color: #7a8372; margin-top: 4px; } + + +/* Optional interior. No source exterior selector is changed outside this state. */ +#interiorLaunch{position:fixed;left:32px;top:225px;z-index:3;display:flex;align-items:center;gap:11px;padding:12px 17px;border:1px solid #b8c7b0;background:#fafcf6f5;border-radius:12px;box-shadow:0 5px 25px #263b2520;color:#365944;font-size:12px;font-weight:600} +#interiorLaunch svg{width:20px;height:20px}#interiorLaunch small{display:block;margin-top:4px;font-size:9px;font-weight:400;color:#7a8574;text-align:left} +#interiorLaunch:disabled{opacity:.6;cursor:wait} +.interior-heading,#interiorPanel,#interiorTools,#interiorLabels,#walkPad{display:none} +body.interior-active header,body.interior-active #interiorLaunch,body.interior-active .bottom,body.interior-active #direction,body.interior-active .compass,body.interior-active #measure,body.interior-active #sunStamp{display:none!important} +body.interior-active .interior-heading{display:flex;position:fixed;top:23px;left:25px;z-index:3;align-items:center;gap:17px;pointer-events:none} +.interior-heading button{pointer-events:auto;padding:11px 13px}.interior-heading .eyebrow{font-size:8px;letter-spacing:1.8px}.interior-heading h2{margin:5px 0 0;font-size:23px;font-weight:500;letter-spacing:-.65px;color:#2c4434} +body.interior-active:not(.interior-panel-closed):not(.sun-open) #view,body.interior-active:not(.interior-panel-closed):not(.sun-open) #sunOverlay{width:calc(100% - 340px)} +body.interior-active #interiorPanel{display:block;position:fixed;z-index:3;right:16px;top:78px;bottom:20px;width:308px;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin;padding:20px;background:#fafcf7f7;border:1px solid #fff;box-shadow:0 10px 35px #263b2520;border-radius:16px} +body.interior-panel-closed #interiorPanel,body.interior-active.sun-open #interiorPanel{display:none} +.interior-panel-title{display:flex;justify-content:space-between;align-items:center;gap:6px;margin:0 0 12px} +.interior-panel-title h3{margin:0;font-size:16px;font-weight:500;letter-spacing:-.3px}.interior-panel-title span{font-size:9px;color:#75816f} +.int-kicker{font-size:9px;letter-spacing:1.5px;text-transform:uppercase;color:#7c8675;margin:20px 0 10px} +#interiorMap{width:100%;height:auto;display:block;border-radius:10px;background:#eff1e9;border:1px solid #e0e5d8} +#interiorMap .map-room{cursor:pointer;fill:#dcd4c1;stroke:#f5f4eb;stroke-width:2;transition:fill .15s} +#interiorMap .map-room:hover,#interiorMap .map-room.active{fill:#aebda2} +#interiorMap text{font:9px Arial;fill:#465743;pointer-events:none} +#interiorMap .map-wall{fill:#8e9986;pointer-events:none} +.int-map-hint{font-size:9px!important;line-height:1.5!important;margin:7px 0 0!important;color:#85917d!important} +.int-views{display:grid;grid-template-columns:1fr 1fr;gap:6px} +.int-views button{padding:11px 8px;text-align:left;border:1px solid #d9e0d2;border-radius:8px;background:#f2f6ed;color:#38523a;font-size:10px;line-height:1.35} +.int-views button.active{background:#dfe9d8;border-color:#a6ba99}.int-views button:hover{background:#e5ecde} +#interiorRoom{margin-top:2px;font-size:11px;background:#fff} +.int-options{display:grid;grid-template-columns:1fr;gap:10px;padding-top:14px;margin-top:16px;border-top:1px solid #e0e6d9} +.int-options label{font-size:11px;display:flex;align-items:center;gap:8px;color:#51654a}.int-options input{accent-color:#426448} +#interiorPanel p{font-size:10px;line-height:1.6;color:#77836e;margin:10px 0} +.int-source{font-size:10px;border-top:1px solid #dce3d4;padding-top:12px;margin-top:16px}.int-source summary{cursor:pointer;line-height:1.5;color:#536b49} +.int-source-buttons{display:flex;gap:5px;flex-wrap:wrap;margin:10px 0} +.int-source-buttons button{border:1px solid #d4ddca;border-radius:7px;font-size:10px;background:#f5f7ef;padding:8px;color:#486140} +.int-export{width:100%;margin-top:12px;border:1px solid #d0dbc6;border-radius:8px;background:#eef3e7;padding:10px;font-size:10px;color:#49613f} +body.interior-active #interiorTools{display:flex;position:fixed;z-index:4;left:calc((100% - 340px)/2);bottom:22px;transform:translateX(-50%);flex-direction:column;align-items:center;gap:8px;max-width:calc(100% - 24px)} +body.interior-panel-closed #interiorTools{left:50%}body.interior-active.sun-open #interiorTools{left:calc((100% - var(--sun-side))/2)} +.int-mode-bar{display:flex;align-items:center;gap:4px;white-space:nowrap;padding:6px;background:#fafcf6f5;border:1px solid #fff;border-radius:12px;box-shadow:0 5px 26px #263b2525} +.int-mode-bar button{border:0;background:transparent;border-radius:7px;padding:10px 11px;font-size:11px;color:#52684a}.int-mode-bar button.active{background:#365944;color:#fff}.int-mode-bar button:hover:not(.active){background:#e8eee2} +.int-mode-bar .int-nav-button{background:#e9efe3;font-size:15px;line-height:14px;padding:9px 10px} +#interiorHint{font-size:9px;line-height:1.45;color:#71806a;text-align:center;background:#f0f3e8de;border-radius:6px;padding:4px 10px;max-width:460px} +body.interior-active #interiorLabels{display:block;position:fixed;inset:0;pointer-events:none;z-index:2} +.int-room-tag{position:absolute;transform:translate(-50%,-50%);border:1px solid #ffffffdc;border-radius:7px;background:#fafcf4ea;padding:5px 7px;box-shadow:0 2px 10px #253a2220;pointer-events:auto;white-space:nowrap;line-height:1.2;font-size:10px;color:#334c35} +.int-room-tag b{font-weight:600}.int-room-tag small{display:block;font-size:8px;color:#7c8874;margin-top:3px}.int-room-tag:hover{border-color:#739069;background:#edf4e5} +body.interior-walk #sunOverlay{display:none} +body.interior-walk #walkPad{display:grid;position:fixed;z-index:4;left:24px;bottom:94px;grid-template-columns:38px 38px 38px;grid-template-rows:34px 34px;gap:4px} +#walkPad button{touch-action:none;user-select:none;-webkit-user-select:none;border:1px solid #d5dfcc;background:#f8fbf1ee;border-radius:8px;color:#425e3c;font-size:17px;box-shadow:0 3px 15px #314a2520}#walkPad button:active,#walkPad button.held{background:#bfd0b2} +#walkPad [data-walk=forward]{grid-column:2;grid-row:1}#walkPad [data-walk=left]{grid-column:1;grid-row:2}#walkPad [data-walk=back]{grid-column:2;grid-row:2}#walkPad [data-walk=right]{grid-column:3;grid-row:2} +#interiorReferenceDialog{width:min(1120px,96vw);max-height:94vh;border:1px solid #e2e8da;border-radius:14px;background:#fafcf6;padding:16px;color:#36503c;box-shadow:0 25px 90px #14241666} +#interiorReferenceDialog::backdrop{background:#1e2b22b3;backdrop-filter:blur(3px)} +.int-ref-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}.int-ref-head h3{font-size:14px;font-weight:500;margin:0}.int-ref-head button{border:0;background:#e9eee3;border-radius:7px;padding:8px 12px} +#interiorReferenceImage{display:block;max-width:100%;max-height:76vh;object-fit:contain;margin:auto}.int-ref-caption{font-size:11px;line-height:1.5;margin:10px 0 0;color:#7b8574} +@media(max-width:1100px) and (min-width:851px){.interior-heading h2{font-size:19px}.interior-heading .eyebrow{display:none}.interior-heading{max-width:calc(100% - 370px)}} +@media(max-width:850px){ + #interiorLaunch{top:147px;left:18px;padding:10px 13px;font-size:11px}#interiorLaunch small{font-size:8px} + body.interior-active .interior-heading{left:16px;top:18px;gap:10px;max-width:calc(100% - 110px)}.interior-heading h2{font-size:17px;margin-top:3px}.interior-heading .eyebrow{font-size:7px;letter-spacing:1px}.interior-heading button{padding:9px 10px;font-size:10px} + body.interior-active:not(.interior-panel-closed):not(.sun-open) #view,body.interior-active:not(.interior-panel-closed):not(.sun-open) #sunOverlay{width:100%;height:calc(100% - 242px)} + body.interior-active #interiorPanel{top:auto;left:0;right:0;bottom:0;width:100%;height:242px;border-radius:18px 18px 0 0;padding:15px 18px 24px} + #interiorMap{width:150px;float:left;margin-right:14px;margin-bottom:9px}.interior-panel-title{margin-bottom:9px}.interior-panel-title h3{font-size:14px} + .int-views{grid-template-columns:1fr 1fr;gap:5px}.int-views button{font-size:9px;padding:9px 6px}.int-kicker{margin:10px 0 7px;font-size:8px}.int-map-hint{display:none}#interiorRoom{font-size:10px;padding:8px}.int-options{clear:both;grid-template-columns:1fr 1fr;margin-top:14px;gap:10px}.int-options label{font-size:10px} + body.interior-active #interiorTools{left:50%;bottom:250px;gap:5px;width:max-content;max-width:calc(100% - 16px)} + body.interior-active.sun-open #interiorTools{left:50%;bottom:calc(var(--sun-sheet) + 8px)}body.interior-panel-closed #interiorTools{bottom:17px} + .int-mode-bar{padding:5px;gap:2px}.int-mode-bar button{font-size:10px;padding:9px 9px}.int-mode-bar .int-nav-button{padding:9px 10px}#interiorHint{display:none} + body.interior-walk #walkPad{left:12px;bottom:304px;grid-template-columns:36px 36px 36px;grid-template-rows:32px 32px;gap:4px}body.interior-walk.interior-panel-closed #walkPad{bottom:80px}body.interior-walk.sun-open #walkPad{bottom:calc(var(--sun-sheet) + 62px)} + .int-room-tag{font-size:8px;padding:4px 5px}.int-room-tag small{font-size:7px}.int-room-tag b .room-word{display:none} + #interiorReferenceDialog{padding:12px}.int-ref-head h3{font-size:12px} +} +@media(max-width:420px){ + body.interior-active .interior-heading{max-width:calc(100% - 98px);gap:8px}.interior-heading h2{font-size:15px}.interior-heading button{font-size:9px;padding:9px 8px} + #interiorMap{width:132px;margin-right:10px}.int-views button{font-size:8.5px;padding:8px 5px}.int-mode-bar button{padding:9px 8px} +} +@media(max-height:550px) and (max-width:850px){ + body.interior-active:not(.interior-panel-closed):not(.sun-open) #view{height:100%;width:calc(100% - 260px)} + body.interior-active #interiorPanel{left:auto;right:10px;top:65px;bottom:10px;width:245px;height:auto;border-radius:13px;padding:12px}#interiorMap{float:none;width:100%;margin-right:0} + body.interior-active #interiorTools{left:calc((100% - 260px)/2);bottom:12px}body.interior-panel-closed #interiorTools{left:50%}body.interior-walk #walkPad{bottom:64px} +} + + + +/* Interior-only paint controls; facade palette and furniture are independent. */ +.int-paint{clear:both;margin:18px 0 0;padding:14px 0 0;border-top:1px solid #e0e6d9} +.int-paint-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px} +.int-paint-head h4{margin:0;font-size:12px;font-weight:600;color:#38523a} +.int-paint-head span{font-size:9px;color:#7c8675} +.int-paint-fields{display:grid;grid-template-columns:48px minmax(0,1fr);gap:10px;align-items:end} +.int-paint-picker{display:block;height:43px;padding:4px;border:1px solid #ccd6c6;border-radius:9px;background:#fff;overflow:hidden;cursor:pointer} +#interiorWallColor{display:block;width:100%;height:100%;padding:0;border:0;border-radius:5px;background:transparent;cursor:pointer} +#interiorWallColor::-webkit-color-swatch-wrapper{padding:0} +#interiorWallColor::-webkit-color-swatch{border:1px solid #0002;border-radius:4px} +#interiorWallColor::-moz-color-swatch{border:1px solid #0002;border-radius:4px} +.int-paint-hex{min-width:0;display:flex;flex-direction:column;gap:4px;font-size:9px;color:#75816f} +#interiorWallHex{display:block;width:100%;min-width:0;height:32px;border:1px solid #d6e0d0;border-radius:7px;padding:7px 9px;background:#fff;color:#334931;font:11px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:.7px;text-transform:uppercase} +.int-paint-presets{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px;margin-top:10px} +.int-paint-presets button{display:flex;align-items:center;gap:6px;min-width:0;min-height:34px;padding:6px;border:1px solid #d9e0d2;border-radius:7px;background:#f7f9f3;color:#51654a;text-align:left;font-size:9px;line-height:1.25} +.int-paint-presets button:hover{background:#eaf0e4;border-color:#a6ba99} +.int-paint-presets button[aria-pressed=true]{border-color:#78946c;background:#e5eedf;color:#2e4b35;box-shadow:inset 0 0 0 1px #78946c} +.int-paint-chip{display:block;width:19px;height:19px;flex:0 0 19px;border-radius:4px;background:var(--paint);border:1px solid #0002} +.int-paint-reset{display:block;margin:10px 0 0;padding:7px 9px;border:1px solid #d6dfcf;border-radius:7px;background:#f3f6ef;color:#49613f;font-size:10px} +.int-paint-reset:hover{background:#e7eee0} +#interiorPanel .int-paint-note{font-size:9px;line-height:1.55;margin:8px 0 0;color:#7c8675} +@media(max-width:850px){.int-paint{margin-top:14px;padding-top:12px}.int-paint-presets{grid-template-columns:repeat(3,minmax(0,1fr))}.int-paint-head h4{font-size:11px}.int-paint-presets button{min-height:36px}.int-paint-fields{grid-template-columns:48px minmax(0,1fr)}} +@media(max-width:360px){.int-paint-presets{grid-template-columns:repeat(2,minmax(0,1fr))}} +@media(max-height:550px) and (max-width:850px){.int-paint-presets{grid-template-columns:repeat(2,minmax(0,1fr))}} + + +/* ALLSTAV 2025 configuration panel. Independent from the retained sun and site tools. */ +:root{--std-side:424px;--std-sheet:min(48dvh,440px);--std-line:#dce2d7;--std-green:#365944} +#standardsToggle{display:flex;align-items:center;gap:7px;font-weight:600;background:#365944;color:#fff;border-color:#365944} +#standardsToggle svg{width:15px;height:15px}#standardsToggle.active{background:#244633} +#standardsPanel{display:none;position:fixed;z-index:5;right:16px;top:78px;bottom:17px;width:392px;background:#fafbf7;border:1px solid #fff;border-radius:18px;box-shadow:0 12px 44px #20311f22;overflow:hidden;flex-direction:column} +body.std-open #standardsPanel{display:flex}body.std-open #view,body.std-open #measure,body.std-open #sunOverlay{width:calc(100% - var(--std-side))!important;height:100%!important} +body.std-open .bottom,body.std-open #interiorTools{left:calc((100% - var(--std-side))/2)!important} +body.std-open #interiorPanel,body.std-open #panel{display:none!important}body.std-open .compass{right:calc(var(--std-side) + 18px)} +body.std-open .interior-heading{max-width:calc(100% - var(--std-side) - 30px)}body.std-open #direction{font-size:10px;bottom:24px} +.std-head{padding:20px 20px 14px}.std-kicker{font-size:9px;letter-spacing:2px;font-weight:700;color:#829077}.std-head-row{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:6px}.std-head h2{font-size:23px;letter-spacing:-.6px;font-weight:500;margin:0}.std-close{border:0;background:#edf1e7;border-radius:50%;width:29px;height:29px;flex-shrink:0;font-size:20px;line-height:20px}.std-head p{font-size:10px;line-height:1.5;color:#7c8774;margin:6px 0 0} +.std-tabs{margin:0 16px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;background:#ecf0e7;border:1px solid #e1e6db;padding:4px;border-radius:10px}.std-tabs button{border:0;border-radius:7px;padding:10px 5px;background:none;font-size:12px}.std-tabs button[aria-selected=true]{background:#365944;color:white;box-shadow:0 2px 5px #233a2520} +.std-scroll{overflow:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:#c1cbbb transparent;flex:1;min-height:0;padding:0 17px 18px}.std-scroll [hidden]{display:none!important}.std-shortcuts{display:flex;gap:5px;margin:13px 0 14px}.std-shortcuts button{flex:1;border:1px solid #dbe2d4;border-radius:7px;background:#f3f6ee;padding:8px 3px;font-size:10px}.std-shortcuts button:hover{background:#e4ecde} +.std-notice{font-size:10px;line-height:1.6;color:#777f6e;background:#eef2e9;border-radius:8px;padding:10px 11px;margin:10px 0}.std-notice strong{color:#53624b;font-weight:600}.std-notice.amber{background:#f6efdf;color:#8a744a}.std-notice.amber strong{color:#795f36}.std-notice.blue{background:#eaf0f2;color:#576e75} +.std-section{border-bottom:1px solid var(--std-line);padding:0}.std-section>summary{display:flex;align-items:center;gap:10px;list-style:none;cursor:pointer;padding:16px 0;line-height:1.3}.std-section>summary::-webkit-details-marker{display:none}.std-section>summary:after{content:'+';font-size:18px;color:#8d9985;margin-left:auto}.std-section[open]>summary:after{content:'\2212'}.std-section>summary .std-number{font-size:10px;width:22px;height:22px;border:1px solid #d6decd;background:#eef2e8;border-radius:7px;display:flex;align-items:center;justify-content:center;color:#738568;flex-shrink:0}.std-section>summary strong{display:block;font-size:12px;font-weight:600}.std-section>summary small{display:block;font-size:10px;font-weight:400;color:#849078;margin-top:4px;max-width:271px}.std-section .std-body{padding-bottom:17px} +.std-label{display:flex;justify-content:space-between;align-items:center;font-size:11px;font-weight:600;margin:13px 0 9px;color:#4a5c42;gap:7px}.std-source{border:0;padding:3px 0;background:none;color:#849375;font-size:9px;text-decoration:underline;text-underline-offset:3px;cursor:pointer}.std-body p{font-size:10px;line-height:1.6;color:#7c8673;margin:8px 0 10px} +.std-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}.std-grid.cols3{grid-template-columns:repeat(3,minmax(0,1fr))}.std-grid.cols5{grid-template-columns:repeat(5,minmax(0,1fr))}.std-grid.cols2{grid-template-columns:repeat(2,minmax(0,1fr))} +.std-card{position:relative;border:1px solid #dce2d5;background:white;border-radius:9px;padding:5px 5px 7px;min-width:0;min-height:65px;text-align:center;font-size:10px;line-height:1.35;transition:border-color .12s,box-shadow .12s}.std-card:hover{border-color:#8aa27c}.std-card[aria-pressed=true]{border-color:#55754a;box-shadow:0 0 0 1px #55754a;background:#f0f5e9}.std-card[aria-pressed=true]:after{content:'\2713';position:absolute;right:3px;top:3px;border-radius:50%;width:15px;height:15px;background:#365944;color:#fff;font-size:10px;line-height:15px}.std-card .sample{display:block;width:100%;height:32px;border-radius:5px;margin-bottom:6px;border:1px solid #00000008;background-size:cover;background-position:center}.std-card img{display:block;object-fit:cover;width:100%;height:48px;border-radius:4px;margin-bottom:6px}.std-card.door img{height:70px;object-fit:contain;background:#fff}.std-card.wood img{height:56px;object-fit:cover}.std-card strong{font-weight:500;display:block}.std-card small{display:block;margin-top:3px;font-size:8px;color:#87947c}.std-card .premium{color:#9e793e}.std-card .visual{color:#7b8990} +.std-swatch-inline{display:inline-block;width:11px;height:11px;border-radius:3px;margin-right:5px;vertical-align:-1px;border:1px solid #0001}.std-two{display:grid;grid-template-columns:1fr 1fr;gap:9px}.std-field{display:block;font-size:10px;color:#75816b;margin:10px 0;line-height:1.5}.std-field select,.std-field input:not([type=color]):not([type=range]):not([type=checkbox]){display:block;width:100%;padding:10px 9px;border:1px solid #d7dfcf;border-radius:7px;background:#fff;font-size:11px;color:#3e5338;margin-top:5px;min-height:36px}.std-field input[type=color]{display:block;width:100%;height:35px;border:1px solid #d7dfcf;border-radius:7px;padding:3px;margin-top:5px;background:white}.std-field input[type=range]{width:100%;accent-color:#365944}.std-range-head{display:flex;justify-content:space-between;font-size:10px;color:#718165;margin-top:11px}.std-range-head strong{font-size:12px;color:#365944;font-weight:600} +.std-action{border:1px solid #d3dec8;background:#eff4e8;border-radius:7px;padding:9px 11px;font-size:10px;color:#46603c}.std-action.wide{width:100%;margin-top:10px}.std-action:hover{background:#e2ebd8}.std-action.dark{background:#365944;border-color:#365944;color:white} +.std-toggle{display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12px;font-weight:600;cursor:pointer;margin:3px 0 10px;padding:10px;background:#eef3e8;border-radius:9px}.std-toggle input{appearance:none;width:35px;height:21px;border-radius:12px;background:#bdc6b6;position:relative;flex-shrink:0;cursor:pointer;transition:background .15s}.std-toggle input:before{content:'';position:absolute;width:15px;height:15px;border-radius:50%;background:white;left:3px;top:3px;box-shadow:0 1px 4px #0002;transition:transform .15s}.std-toggle input:checked{background:#365944}.std-toggle input:checked:before{transform:translateX(14px)} +.std-check{display:flex;gap:9px;align-items:center;font-size:11px;color:#63765a;margin:12px 0;line-height:1.5}.std-check input{accent-color:#365944}.std-disabled{opacity:.45;pointer-events:none}.std-inventory{margin:10px 0;border:1px solid #e0e5da;border-radius:9px;padding:5px 11px}.std-inventory>div{font-size:10px;line-height:1.5;padding:8px 0;border-bottom:1px solid #e6eadf}.std-inventory>div:last-child{border:0}.std-inventory b{font-weight:600;color:#49623f}.std-inventory small{display:block;color:#8a9480;font-size:9px;margin-top:3px} +.std-footer{padding:12px 16px;border-top:1px solid #dce2d6;background:#f5f8ee;flex-shrink:0}.std-saved{font-size:9px;color:#859378;display:flex;gap:6px;align-items:center;margin-bottom:8px}.std-saved:before{content:'';width:5px;height:5px;border-radius:50%;background:#648257}.std-footer-row{display:grid;grid-template-columns:1fr 1fr;gap:7px}.std-footer-row button{font-size:11px;padding:10px}.std-summary-title{font-size:18px;font-weight:500;letter-spacing:-.3px;margin:19px 0 7px}.std-summary-group{margin-top:16px}.std-summary-group h4{font-size:10px;text-transform:uppercase;letter-spacing:1px;font-weight:600;color:#728466;margin:16px 0 7px}.std-summary-row{display:grid;grid-template-columns:1fr 1.5fr;gap:9px;font-size:10px;line-height:1.6;padding:8px 0;border-bottom:1px solid #e2e7dc}.std-summary-row span{color:#7e8a72}.std-summary-row b{font-weight:500;text-align:right;color:#41583b}.std-summary-row small{display:block;font-size:8px;color:#9b7945} +.std-export-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:16px}.std-export-grid button{border:1px solid #d5dfcd;background:#f2f6ec;padding:11px 7px;border-radius:8px;font-size:10px}.std-reset{color:#9c6852!important;background:#f7f0e8!important}.std-fine{font-size:9px!important;line-height:1.6!important;color:#949e89!important;margin:12px 0 0!important} +#stdCatalogDialog{width:min(800px,94vw);max-height:94dvh;border:1px solid #fff;border-radius:15px;background:#fafbf7;padding:15px;box-shadow:0 20px 70px #13271066;color:#36523d}#stdCatalogDialog::backdrop,#stdResetDialog::backdrop{background:#1f2e23a8;backdrop-filter:blur(3px)}.std-dialog-head{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:9px}.std-dialog-head h3{margin:0;font-size:13px;font-weight:500}.std-dialog-head button{border:0;background:#e9efe1;padding:8px 11px;border-radius:7px;font-size:11px}#stdCatalogImage{display:block;max-width:100%;max-height:75dvh;object-fit:contain;margin:auto}#stdCatalogCaption{font-size:10px;color:#7c8871;line-height:1.5;margin:10px 0 0}.std-dialog-pager{display:flex;gap:7px;align-items:center;justify-content:space-between;margin-top:10px}.std-dialog-pager button{border:1px solid #d6dfcc;border-radius:7px;background:#edf3e5;padding:8px 11px;font-size:10px} +#stdResetDialog{width:min(390px,92vw);border:1px solid white;border-radius:16px;background:#fafbf7;padding:23px;color:#36523d}#stdResetDialog h3{font-size:19px;font-weight:500;margin:0 0 10px}#stdResetDialog p{font-size:12px;line-height:1.7;color:#7a866f}#stdResetDialog .std-two{margin-top:19px} +body.std-open #interiorLaunch{z-index:2} +@media(min-width:851px) and (max-width:1180px){body.std-open .compass,body.std-open #direction,body.std-open .help{display:none}body.std-open .tool{padding:9px 7px;font-size:10px}body.std-open .site-switch{font-size:9px}body.std-open .bottom{bottom:18px}body.std-open header .metrics{display:none!important}body.std-open #sunStamp{top:125px}body.std-open .interior-heading h2{font-size:17px}body.std-open .interior-heading .eyebrow{display:none}} +@media(max-width:850px){ + #standardsPanel{top:auto;left:0;right:0;bottom:0;width:100%;height:var(--std-sheet);border-radius:18px 18px 0 0} + body.std-open #view,body.std-open #measure,body.std-open #sunOverlay{width:100%!important;height:calc(100% - var(--std-sheet))!important} + body.std-open .bottom,body.std-open #interiorTools{left:50%!important;bottom:calc(var(--std-sheet) + 8px)!important;gap:0} + body.std-open .site-controls,body.std-open .help,body.std-open #direction,body.std-open .compass{display:none} + body.std-open .interior-heading{max-width:calc(100% - 126px)}body.std-open #walkPad{bottom:calc(var(--std-sheet) + 65px)!important} + #standardsToggle{padding:8px 9px;font-size:10px;gap:4px;min-height:34px}.std-head{padding:12px 17px 9px}.std-head .std-kicker{font-size:8px}.std-head h2{font-size:18px}.std-head p{display:none}.std-head-row{margin-top:3px}.std-close{width:25px;height:25px} + .std-tabs{margin:0 14px}.std-tabs button{padding:8px 4px;font-size:11px}.std-scroll{padding:0 16px 18px}.std-shortcuts{margin:9px 0}.std-footer{padding:8px 15px}.std-saved{margin-bottom:5px;font-size:8px}.std-footer-row button{padding:8px;font-size:10px}.std-section>summary{padding:13px 0} + .std-card.door img{height:62px}.std-notice{font-size:9px;padding:8px 10px}.std-card{font-size:9px}.std-section>summary small{max-width:calc(100vw - 115px)}body.std-open header h1{font-size:23px}body.std-open #interiorLaunch{top:139px;font-size:10px;padding:8px 10px} +} +@media(max-width:530px){#standardsToggle .std-toggle-word{display:none}#standardsToggle{width:34px;justify-content:center;padding:0}.topright{gap:5px!important}.topright #infoButton{display:none}.std-head .std-kicker{display:none}.std-head-row{margin:0}.std-head{padding:11px 16px 8px}.std-head h2{font-size:17px}body.std-open #sunStamp{top:98px;font-size:9px}body.std-open .interior-heading .eyebrow{font-size:6px;letter-spacing:.6px}} +@media(max-height:550px) and (max-width:850px){:root{--std-side:340px}#standardsPanel{left:auto;right:8px;top:60px;bottom:8px;width:324px;height:auto;border-radius:12px}body.std-open #view,body.std-open #sunOverlay{width:calc(100% - 340px)!important;height:100%!important}body.std-open .bottom,body.std-open #interiorTools{left:calc((100% - 340px)/2)!important;bottom:10px!important}.std-head{padding:9px 14px}.std-head h2{font-size:16px}.std-footer{display:none}body.std-open #walkPad{bottom:60px!important}body.std-open .interior-heading{max-width:calc(100% - 370px)}} + +@media(max-width:850px){body.std-open header .sub,body.std-open #sunStamp,body.std-open #interiorLaunch{display:none!important}body.std-open header h1{font-size:19px;line-height:1.2;max-width:calc(100vw - 116px);margin-top:9px}body.std-open header .eyebrow{font-size:7px;letter-spacing:1.3px}body.std-open #toast{top:73px;max-width:90%;font-size:10px}} + diff --git a/locenice/locenice/css/web.css b/locenice/locenice/css/web.css new file mode 100644 index 0000000..5dddcfb --- /dev/null +++ b/locenice/locenice/css/web.css @@ -0,0 +1,20 @@ +/* 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}} diff --git a/locenice/locenice/data/app-config.json b/locenice/locenice/data/app-config.json new file mode 100644 index 0000000..5642b4c --- /dev/null +++ b/locenice/locenice/data/app-config.json @@ -0,0 +1,15 @@ +{ + "version": "locenice-web-01", + "model": "./models/dum.glb", + "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": false, + "catalog": "./data/catalog.json", + "assets": "./data/assets.json", + "references": "./data/references.json", + "sceneMetadata": "./data/scene.json", + "site": "./data/site.json" +} \ No newline at end of file diff --git a/locenice/locenice/data/assets.json b/locenice/locenice/data/assets.json new file mode 100644 index 0000000..9ebe011 --- /dev/null +++ b/locenice/locenice/data/assets.json @@ -0,0 +1,52 @@ +{ + "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" +} \ No newline at end of file diff --git a/locenice/locenice/data/catalog.json b/locenice/locenice/data/catalog.json new file mode 100644 index 0000000..ecd04de --- /dev/null +++ b/locenice/locenice/data/catalog.json @@ -0,0 +1,529 @@ +{ + "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": [ + [ + "#35463e", + "Zelená", + "#35463e" + ], + [ + "#eeeae1", + "Krémová", + "#eeeae1" + ], + [ + "#e9e9e5", + "Bílá", + "#e9e9e5" + ], + [ + "#b3aaa0", + "Kašmír", + "#b3aaa0" + ], + [ + "#686d69", + "Šedá", + "#686d69" + ], + [ + "#343a3d", + "Antracit", + "#343a3d" + ] + ], + "worktop": [ + [ + "oak", + "Dřevo", + "#b59369" + ], + [ + "white", + "Bílá", + "#eeeeea" + ], + [ + "graphite", + "Grafit", + "#444946" + ], + [ + "stone", + "Kámen", + "#c7c5b9" + ] + ], + "backsplash": [ + [ + "stone", + "Světlý kámen", + "#c7c5b9" + ], + [ + "white", + "Bílá", + "#eaeae5" + ], + [ + "worktop", + "Jako deska", + "#b59369" + ] + ], + "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": [ + [ + "#35463e", + "Zelená", + "#35463e" + ], + [ + "#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": "#35463e", + "kitchenBodyColor": "#35463e", + "worktop": "oak", + "backsplash": "stone", + "shower": "tray", + "faucet": "chrome", + "hood": "facade", + "fans": true, + "aircon": true, + "ventilation": "local", + "hotWaterCirculation": false, + "radiator": "electric", + "note": "" + } +} diff --git a/locenice/locenice/data/references.json b/locenice/locenice/data/references.json new file mode 100644 index 0000000..6bd870f --- /dev/null +++ b/locenice/locenice/data/references.json @@ -0,0 +1,32 @@ +{ + "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" + } +} \ No newline at end of file diff --git a/locenice/locenice/data/scene.json b/locenice/locenice/data/scene.json new file mode 100644 index 0000000..4a3c1ec --- /dev/null +++ b/locenice/locenice/data/scene.json @@ -0,0 +1,1727 @@ +{ + "siteStudy": { + "version": "2026-10-site-01", + "referenceElevation": 591.84, + "source": "C.2-C.5 and Locenice p.c.3480/37 polohopis a vyskopis; digitized PDF vectors, not survey coordinates.", + "cadastralAreaInProject": 1010, + "digitizedOutlineArea": 987.89, + "note": "The graphical outline is about 988 m2, versus 1010 m2 in the project. It is NOT scaled to force area equality; house dimensions are unchanged.", + "finishedGroundC3": [ + [ + -7.67, + 4.695, + -0.48 + ], + [ + -7.67, + -4.695, + -0.48 + ], + [ + 3.713, + -4.695, + -0.992 + ], + [ + 3.713, + -0.688, + -0.871 + ], + [ + 7.67, + -0.688, + -1.14 + ], + [ + 7.67, + 4.695, + -0.971 + ], + [ + -1.113, + 4.695, + -0.48 + ] + ], + "surveyPoints": [ + { + "id": 1, + "xz": [ + -22.86387745468233, + 6.9162902718421835 + ], + "altitude": 593.13 + }, + { + "id": 2, + "xz": [ + -26.00649803769423, + 5.740604360018231 + ], + "altitude": 593.19 + }, + { + "id": 3, + "xz": [ + -22.733664786123356, + -2.9186632403462283 + ], + "altitude": 592.64 + }, + { + "id": 4, + "xz": [ + -19.66526683318922, + -1.6042685750232388 + ], + "altitude": 592.54 + }, + { + "id": 5, + "xz": [ + -16.849177040431528, + -9.283203935982339 + ], + "altitude": 592.08 + }, + { + "id": 6, + "xz": [ + -19.809586816163616, + -10.38887051378567 + ], + "altitude": 592.09 + }, + { + "id": 7, + "xz": [ + -19.100913252951532, + 0.5664836927799971 + ], + "altitude": 592.84 + }, + { + "id": 8, + "xz": [ + -21.249447968471955, + 7.071889756172209 + ], + "altitude": 592.85 + }, + { + "id": 9, + "xz": [ + -19.62233327368599, + 2.5536226040233165 + ], + "altitude": 592.59 + }, + { + "id": 10, + "xz": [ + -9.809681750990507, + 7.518430876054446 + ], + "altitude": 592.14 + }, + { + "id": 11, + "xz": [ + -8.73082925476737, + 0.6499201351270848 + ], + "altitude": 591.85 + }, + { + "id": 12, + "xz": [ + 7.4440611266407775, + 0.4236299245776415 + ], + "altitude": 590.74 + }, + { + "id": 13, + "xz": [ + 8.057613164117942, + 7.427291898661351 + ], + "altitude": 590.96 + }, + { + "id": 14, + "xz": [ + 22.42368569156818, + 7.487149207097606 + ], + "altitude": 589.96 + }, + { + "id": 15, + "xz": [ + 22.980738978199824, + 0.027095859342831083 + ], + "altitude": 589.65 + }, + { + "id": 16, + "xz": [ + 25.962873515635593, + 7.010131955908398 + ], + "altitude": 589.82 + }, + { + "id": 17, + "xz": [ + 36.80267751573004, + 5.954683418127402 + ], + "altitude": 589.03 + }, + { + "id": 18, + "xz": [ + 37.69887332216715, + -0.19213899494552378 + ], + "altitude": 588.82 + }, + { + "id": 19, + "xz": [ + 47.409464441441116, + 4.094040845154811 + ], + "altitude": 588.54 + }, + { + "id": 20, + "xz": [ + 51.16376748811935, + -2.0537658393134657 + ], + "altitude": 588.06 + }, + { + "id": 21, + "xz": [ + 54.12316842449226, + -9.187005561860445 + ], + "altitude": 587.79 + }, + { + "id": 22, + "xz": [ + 41.075319538836055, + -8.900127720728307 + ], + "altitude": 588.45 + }, + { + "id": 23, + "xz": [ + 23.482474278537843, + -7.205827671852879 + ], + "altitude": 589.47 + }, + { + "id": 24, + "xz": [ + 6.9940948033230175, + -7.604159411778007 + ], + "altitude": 590.56 + }, + { + "id": 25, + "xz": [ + -5.631153944681365, + -8.570201470985344 + ], + "altitude": 591.41 + }, + { + "id": 26, + "xz": [ + -14.560744944331192, + -8.695220715590304 + ], + "altitude": 591.9 + }, + { + "id": 27, + "xz": [ + -17.020111569954853, + -2.760537937058604 + ], + "altitude": 592.24 + } + ], + "parkingSpot": 0.39, + "entranceSpot": -0.48, + "gardenStairSpot": -1.395, + "originalNodeCount": 111, + "originalBinaryBytes": null, + "terrainClosure": { + "version": "2", + "description": "Continuous boundary walls and underside added to the three terrain slabs. Original grass surfaces and house geometry unchanged.", + "thickness_m": 0.6 + } + }, + "vespaProp": { + "description": "Original stylized classic Vespa-style scooter in matte black; approximate visualization prop, not manufacturer CAD.", + "positionMetres": [ + -8.72, + -0.48, + -3.15 + ], + "yawDegrees": -94, + "surface": "Site_Entrance_paving", + "surfaceElevation": -0.48, + "bounds": { + "min": [ + -9.109809535077924, + -0.48, + -4.111515026914823 + ], + "max": [ + -8.365626753584094, + 0.907, + -2.2210983876797625 + ] + }, + "visibleWith": "parking", + "houseAndTerrainUnchanged": true + }, + "hardiePlank": { + "product": "HardiePlank struktura dřeva", + "manufacturer": "James Hardie", + "defaultColor": "oriskove-hneda", + "selectedColor": { + "id": "oriskove-hneda", + "name": "Oříškově hnědá", + "code": "5771220", + "hex": "#776e65", + "texture": 4 + }, + "palette": [ + { + "id": "snehove-bila", + "name": "Sněhově bílá", + "code": "5671220", + "hex": "#ffffff", + "texture": 0 + }, + { + "id": "sterkove-seda", + "name": "Štěrkově šedá", + "code": "5751220", + "hex": "#d0c8bb", + "texture": 1 + }, + { + "id": "mlhove-seda", + "name": "Mlhově šedá", + "code": "5841220", + "hex": "#b9c1c3", + "texture": 2 + }, + { + "id": "ocelove-seda", + "name": "Ocelově šedá", + "code": "5851220", + "hex": "#758896", + "texture": 3 + }, + { + "id": "oriskove-hneda", + "name": "Oříškově hnědá", + "code": "5771220", + "hex": "#776e65", + "texture": 4 + }, + { + "id": "espresso", + "name": "Espresso", + "code": "5460122", + "hex": "#241510", + "texture": 5 + }, + { + "id": "antracitove-seda", + "name": "Antracitově šedá", + "code": "5420122", + "hex": "#4c5053", + "texture": 6 + } + ], + "boardMaterial": 56, + "backingMaterial": 57, + "boardNodes": [ + 14, + 15, + 34, + 35, + 37, + 38, + 39, + 41, + 42, + 43 + ], + "backingNodes": [ + 13, + 33, + 36, + 40 + ], + "sourceProductURL": "https://www.jameshardie.cz/cz/hardie-plank", + "paletteSource": "User supplied swatch image, chip medians; not calibrated.", + "geometryStatus": "Original geometry and horizontal board layout preserved. Nominal product size is a specification, not verification of the source geometry or installation.", + "textureStatus": "Illustrative embossed painted-cedar color and normal maps, guided by the supplied photo; not a manufacturer scan.", + "scope": "Vertical facade cladding and associated backing at entrance and terrace only. Deck fascia, deck surface, stairs and all other materials unchanged." + }, + "interiorStudy": { + "version": 2, + "defaultEnabled": false, + "originalNodeCount": 157, + "originalMeshCount": 156, + "originalMaterialCount": 58, + "sourceBinarySHA256": "cbfbfd1282bb6451f5e4a4b616902b176c6c8c216f2c416234bb822c4a4394fb", + "originalBinaryLength": 8803052, + "originalGeometryUnchanged": true, + "primarySource": "D.1.1.2.1.1 Pudorys 1NP_pdfA.PDF, sheet dated 21.09.2026", + "secondarySources": [ + "D.1.1.2.3.1 Pohledy_pdfA.PDF", + "pohled_ven.jpeg", + "PHOTO-2026-05-26-20-38-11.jpg", + "mrvz1_uprava.jpg", + "mrvz2.jpeg" + ], + "coordinateSystem": "Existing model: +X south gable, +Z entrance/west facade, Y vertical; finished floor Y=0.", + "sourcePriority": "Existing exterior dimensions and openings retained; internal partition layout from dimensioned plan. Kitchen U-shape and finishes adapted from user images to room 108 without relocating exterior openings.", + "limitations": [ + "Illustrative furniture dimensions, exact cabinet modules and appliance models not provided.", + "Flat ceiling height 2.60 m applies only to the other rooms. Room 108 has a continuous pitched ceiling following the original 23-degree roof and 4.82 m interior ridge. Timber sections are illustrative; not structural design.", + "Room area labels copied from plan, not independently measured as-built floor areas.", + "Open interior doors are for navigation; no construction changes implied.", + "Existing site model outside windows, not a surveyed distant landscape or stitched photo panorama.", + "Interior lighting is an illustrative fill light, not a daylight calculation." + ], + "cutHeight": 1.12, + "footprint": [ + [ + -7.31, + -4.335 + ], + [ + 3.362, + -4.335 + ], + [ + 3.362, + -0.337 + ], + [ + 7.31, + -0.337 + ], + [ + 7.31, + 4.335 + ], + [ + -0.762, + 4.335 + ], + [ + -0.762, + 3.535 + ], + [ + -7.31, + 3.535 + ] + ], + "rooms": [ + { + "id": "101", + "name": "Zadveri", + "label": "Zádveří", + "area": "5,20", + "poly": [ + [ + -4.41, + 0.725 + ], + [ + -2.56, + 0.725 + ], + [ + -2.56, + 3.535 + ], + [ + -4.41, + 3.535 + ] + ], + "anchor": [ + -3.4, + 2.12 + ], + "eye": [ + -3.25, + 1.62, + 2.72 + ], + "look": [ + -3.05, + 1.5, + 0.1 + ] + }, + { + "id": "102", + "name": "Chodba", + "label": "Chodba", + "area": "6,36", + "poly": [ + [ + -4.41, + -0.335 + ], + [ + -4.11, + -0.335 + ], + [ + -4.11, + -0.485 + ], + [ + -3.96, + -0.485 + ], + [ + -3.96, + -1.555 + ], + [ + -2.56, + -1.555 + ], + [ + -2.56, + -0.335 + ], + [ + -0.91, + -0.335 + ], + [ + -0.91, + 0.725 + ], + [ + -4.41, + 0.725 + ] + ], + "anchor": [ + -3.1, + 0.13 + ], + "eye": [ + -3.1, + 1.62, + 0.25 + ], + "look": [ + -0.4, + 1.5, + 0.22 + ] + }, + { + "id": "103", + "name": "Pokoj 103", + "label": "Pokoj 103", + "area": "10,62", + "poly": [ + [ + -7.31, + -0.335 + ], + [ + -4.56, + -0.335 + ], + [ + -4.56, + 3.535 + ], + [ + -7.31, + 3.535 + ] + ], + "anchor": [ + -5.6, + 1.64 + ], + "eye": [ + -5.64, + 1.62, + 0.95 + ], + "look": [ + -5.1, + 1.25, + 3.1 + ] + }, + { + "id": "104", + "name": "Pokoj 104", + "label": "Pokoj 104", + "area": "12,35", + "poly": [ + [ + -7.31, + -4.335 + ], + [ + -4.11, + -4.335 + ], + [ + -4.11, + -0.485 + ], + [ + -7.31, + -0.485 + ] + ], + "anchor": [ + -5.56, + -2.0 + ], + "eye": [ + -5.5, + 1.62, + -1.3 + ], + "look": [ + -5.2, + 1.2, + -3.9 + ] + }, + { + "id": "105", + "name": "Koupelna", + "label": "Koupelna", + "area": "7,72", + "poly": [ + [ + -3.96, + -4.335 + ], + [ + -0.91, + -4.335 + ], + [ + -0.91, + -1.705 + ], + [ + -3.96, + -1.705 + ] + ], + "anchor": [ + -2.68, + -2.78 + ], + "eye": [ + -2.96, + 1.62, + -2.47 + ], + "look": [ + -1.4, + 1.05, + -2.9 + ] + }, + { + "id": "106", + "name": "WC", + "label": "WC", + "area": "1,40", + "poly": [ + [ + -2.41, + -1.555 + ], + [ + -1.06, + -1.555 + ], + [ + -1.06, + -0.485 + ], + [ + -2.41, + -0.485 + ] + ], + "anchor": [ + -1.85, + -1.0 + ], + "eye": [ + -2.04, + 1.62, + -1.015 + ], + "look": [ + -1.2, + 0.9, + -1.01 + ] + }, + { + "id": "107", + "name": "Loznice", + "label": "Ložnice", + "area": "15,86", + "poly": [ + [ + -0.76, + -4.335 + ], + [ + 3.362, + -4.335 + ], + [ + 3.362, + -0.485 + ], + [ + -0.76, + -0.485 + ] + ], + "anchor": [ + 1.05, + -3.5 + ], + "eye": [ + 0.85, + 1.62, + -1.2 + ], + "look": [ + 2.6, + 1.0, + -2.6 + ] + }, + { + "id": "108", + "name": "Obyvaci pokoj + KK", + "label": "Obývací pokoj + KK", + "area": "37,69", + "poly": [ + [ + -0.76, + -0.335 + ], + [ + 7.31, + -0.335 + ], + [ + 7.31, + 4.335 + ], + [ + -0.76, + 4.335 + ] + ], + "anchor": [ + 3.3, + 0.5 + ], + "eye": [ + 3.8, + 1.62, + 1.05 + ], + "look": [ + 0.5, + 1.0, + 3.1 + ] + }, + { + "id": "109", + "name": "Technicka mistnost", + "label": "Technická místnost", + "area": "3,09", + "poly": [ + [ + -2.41, + 1.475 + ], + [ + -0.91, + 1.475 + ], + [ + -0.91, + 3.535 + ], + [ + -2.41, + 3.535 + ] + ], + "anchor": [ + -1.75, + 2.65 + ], + "eye": [ + -1.85, + 1.62, + 2.25 + ], + "look": [ + -1.35, + 1.0, + 3.15 + ] + } + ], + "wallColliders": [ + { + "lo": [ + -4.11, + -4.335 + ], + "hi": [ + -3.96, + -1.4 + ] + }, + { + "lo": [ + -4.11, + -0.6 + ], + "hi": [ + -3.96, + -0.485 + ] + }, + { + "lo": [ + -7.31, + -0.485 + ], + "hi": [ + -4.035, + -0.33499999999999996 + ] + }, + { + "lo": [ + -3.96, + -1.7049999999999998 + ], + "hi": [ + -3.82, + -1.555 + ] + }, + { + "lo": [ + -3.12, + -1.7049999999999998 + ], + "hi": [ + -0.91, + -1.555 + ] + }, + { + "lo": [ + -2.56, + -1.555 + ], + "hi": [ + -2.4099999999999997, + -1.5 + ] + }, + { + "lo": [ + -2.56, + -0.8 + ], + "hi": [ + -2.4099999999999997, + -0.485 + ] + }, + { + "lo": [ + -2.56, + -0.485 + ], + "hi": [ + -0.91, + -0.33499999999999996 + ] + }, + { + "lo": [ + -0.9099999999999999, + -4.335 + ], + "hi": [ + -0.76, + -0.225 + ] + }, + { + "lo": [ + -0.9099999999999999, + 0.575 + ], + "hi": [ + -0.76, + 3.535 + ] + }, + { + "lo": [ + -0.76, + -0.485 + ], + "hi": [ + 0.29, + -0.33499999999999996 + ] + }, + { + "lo": [ + 1.09, + -0.485 + ], + "hi": [ + 3.362, + -0.33499999999999996 + ] + }, + { + "lo": [ + -4.5600000000000005, + -0.335 + ], + "hi": [ + -4.41, + -0.13 + ] + }, + { + "lo": [ + -4.5600000000000005, + 0.67 + ], + "hi": [ + -4.41, + 3.535 + ] + }, + { + "lo": [ + -2.56, + 0.725 + ], + "hi": [ + -2.4099999999999997, + 1.965 + ] + }, + { + "lo": [ + -2.56, + 2.665 + ], + "hi": [ + -2.4099999999999997, + 3.535 + ] + }, + { + "lo": [ + -2.41, + 1.325 + ], + "hi": [ + -0.91, + 1.4749999999999999 + ] + } + ], + "furnitureColliders": [ + { + "lo": [ + -0.76, + 1.15 + ], + "hi": [ + -0.075, + 4.33 + ], + "room": "108" + }, + { + "lo": [ + -0.075, + 3.66 + ], + "hi": [ + 2.05, + 4.33 + ], + "room": "108" + }, + { + "lo": [ + 1.22, + 1.37 + ], + "hi": [ + 2.05, + 3.68 + ], + "room": "108" + }, + { + "lo": [ + 2.65, + 1.62 + ], + "hi": [ + 4.28, + 2.53 + ], + "room": "108" + }, + { + "lo": [ + 6.3, + 1.29 + ], + "hi": [ + 7.1, + 2.09 + ], + "room": "108" + }, + { + "lo": [ + 6.3, + 2.45 + ], + "hi": [ + 7.1, + 3.25 + ], + "room": "108" + }, + { + "lo": [ + 4.45, + 1.15 + ], + "hi": [ + 5.45, + 3.42 + ], + "room": "108" + }, + { + "lo": [ + 5.2, + 2.5 + ], + "hi": [ + 6.0, + 3.35 + ], + "room": "108" + }, + { + "lo": [ + 5.06, + 3.92 + ], + "hi": [ + 7.14, + 4.3 + ], + "room": "108" + }, + { + "lo": [ + 6.68, + -0.18 + ], + "hi": [ + 7.24, + 0.42 + ], + "room": "108" + }, + { + "lo": [ + 1.38, + -3.8 + ], + "hi": [ + 3.32, + -1.97 + ], + "room": "107" + }, + { + "lo": [ + -0.71, + -3.74 + ], + "hi": [ + -0.05, + -1.76 + ], + "room": "107" + }, + { + "lo": [ + -7.2, + 0.0 + ], + "hi": [ + -6.29, + 1.99 + ], + "room": "103" + }, + { + "lo": [ + -7.2, + -2.58 + ], + "hi": [ + -6.29, + -0.5900000000000001 + ], + "room": "104" + }, + { + "lo": [ + -7.21, + 2.29 + ], + "hi": [ + -6.6, + 3.48 + ], + "room": "103" + }, + { + "lo": [ + -5.18, + 2.1 + ], + "hi": [ + -4.64, + 3.48 + ], + "room": "103" + }, + { + "lo": [ + -7.21, + -4.27 + ], + "hi": [ + -6.6, + -2.97 + ], + "room": "104" + }, + { + "lo": [ + -4.75, + -4.25 + ], + "hi": [ + -4.16, + -2.92 + ], + "room": "104" + }, + { + "lo": [ + -4.4, + 0.86 + ], + "hi": [ + -3.73, + 2.56 + ], + "room": "101" + }, + { + "lo": [ + -2.4, + 0.72 + ], + "hi": [ + -0.92, + 1.33 + ], + "room": "102" + }, + { + "lo": [ + -1.9, + -3.48 + ], + "hi": [ + -1.07, + -1.73 + ], + "room": "105" + }, + { + "lo": [ + -2.9, + -2.16 + ], + "hi": [ + -2.05, + -1.72 + ], + "room": "105" + }, + { + "lo": [ + -3.94, + -4.3 + ], + "hi": [ + -3.0, + -3.36 + ], + "room": "105" + }, + { + "lo": [ + -1.61, + 2.75 + ], + "hi": [ + -0.94, + 3.43 + ], + "room": "109" + }, + { + "lo": [ + -1.59, + 1.56 + ], + "hi": [ + -0.97, + 2.2 + ], + "room": "109" + } + ], + "cutOriginalNodes": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 315 + ], + "roofNodes": [ + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76 + ], + "presets": [ + { + "id": "kitchen", + "label": "Kuchyně", + "eye": [ + 3.78, + 1.62, + 0.9 + ], + "look": [ + 0.45, + 1.05, + 3.0 + ] + }, + { + "id": "outlook", + "label": "Z kuchyně ven", + "eye": [ + 0.62, + 1.62, + 2.7 + ], + "look": [ + 7.26, + 1.53, + 1.05 + ] + }, + { + "id": "living", + "label": "Obývací prostor", + "eye": [ + 3.7, + 1.62, + 0.54 + ], + "look": [ + 6.3, + 1.2, + 2.55 + ] + }, + { + "id": "vault", + "label": "Trámy a vysoké okno", + "eye": [ + 1.0, + 1.7, + 3.2 + ], + "look": [ + 6.5, + 2.85, + 0.3 + ], + "fov": 70 + }, + { + "id": "kitchenVault", + "label": "Vysoký strop nad kuchyní", + "eye": [ + 3.8, + 1.62, + 1.05 + ], + "look": [ + 0.05, + 3.05, + 2.7 + ] + } + ], + "userCorrections": [ + "Wine fridge at the wall-side end of the peninsula.", + "Continuous high ceiling over the entire kitchen/living room; no low kitchen bulkhead.", + "Interior wall-paint picker independent of exterior stucco and ceilings.", + "Timber frames revised mainly from pohled_ven.jpeg: stronger ties and posts, longitudinal knee braces and purlin; separate natural-oak map with lengthwise UVs." + ], + "wallPaint": { + "materialIndices": [ + 58 + ], + "defaultHex": "#eeeae1", + "selectedHex": "#eeeae1", + "scope": "All interior plaster walls and reveals only; not ceilings, timber, furniture, tiles or facade." + }, + "timberReference": { + "source": "pohled_ven.jpeg", + "secondarySource": "PHOTO-2026-05-26-20-38-11.jpg", + "node": 226, + "framePositionsX": [ + 0.95, + 3.25, + 5.55 + ], + "exactStructuralDimensionsVerified": false, + "note": "Visual proportions and joint arrangement are inferred from the supplied images; not construction or structural documentation." + } + }, + "modelRevision": "06 - ALLSTAV 2025 configurator", + "allstavStandards": { + "version": 1, + "catalog": "allstav_standardy_2025.pdf", + "catalogDate": "2025-09", + "materials": { + "entry": [ + 84 + ], + "extSill": [ + 85 + ], + "gutter": [ + 86 + ], + "roof": [ + 14, + 15, + 16, + 17, + 18, + 19 + ], + "entryGlass": [ + 87 + ], + "hardware": [ + 88 + ], + "blinds": [ + 89 + ], + "floor101": [ + 90 + ], + "floor102": [ + 91 + ], + "floor103": [ + 92 + ], + "floor104": [ + 93 + ], + "floor105": [ + 94 + ], + "floor106": [ + 95 + ], + "floor107": [ + 96 + ], + "floor108": [ + 97 + ], + "floor109": [ + 98 + ], + "intSill": [ + 99 + ], + "intDoor": [ + 100 + ], + "worktop": [ + 101 + ], + "kitchenFront": [ + 64 + ], + "kitchenBody": [ + 65 + ], + "backsplash": [ + 102 + ], + "wallTile105": [ + 103 + ], + "wallTile106": [ + 104 + ], + "ceramic": [ + 105 + ], + "recess": [ + 106 + ], + "showerGlass": [ + 107 + ], + "faucet": [ + 108 + ], + "whiteTechnical": [ + 109 + ] + }, + "textures": { + "lam1": 13, + "lam2": 14, + "lam3": 15, + "lam4": 16, + "lam5": 17, + "lam6": 18, + "lam7": 19, + "lam8": 20, + "lam9": 21, + "tile1": 22, + "tile2": 23, + "tile3": 24, + "tile4": 25, + "tile5": 26, + "tile6": 27, + "tile7": 28, + "tile8": 29, + "tile9": 30, + "prum": 31 + }, + "windows": [ + { + "node": 4, + "name": "05_Frames_East_1__frame", + "axis": 2, + "sign": -1, + "low": [ + -6.570000171661377, + 0.949999988079071, + -4.639999866485596 + ], + "high": [ + -4.730000019073486, + 2.069999933242798, + -4.53000020980835 + ] + }, + { + "node": 8, + "name": "05_Frames_East_2__frame", + "axis": 2, + "sign": -1, + "low": [ + -2.630000114440918, + 0.949999988079071, + -4.639999866485596 + ], + "high": [ + -1.8600000143051147, + 2.049999952316284, + -4.53000020980835 + ] + }, + { + "node": 10, + "name": "05_Frames_East_3__frame", + "axis": 2, + "sign": -1, + "low": [ + 0.7799999713897705, + 0.949999988079071, + -4.639999866485596 + ], + "high": [ + 2.619999885559082, + 2.069999933242798, + -4.53000020980835 + ] + }, + { + "node": 16, + "name": "05_Frames_West_entrance_1__frame", + "axis": 2, + "sign": 1, + "low": [ + -6.550000190734863, + 1.149999976158142, + 3.7230000495910645 + ], + "high": [ + -4.730000019073486, + 2.059999942779541, + 3.8329999446868896 + ] + }, + { + "node": 23, + "name": "05_Frames_West_entrance_3__frame", + "axis": 2, + "sign": 1, + "low": [ + -2.3450000286102295, + 1.149999976158142, + 3.7230000495910645 + ], + "high": [ + -1.5750000476837158, + 2.059999942779541, + 3.8329999446868896 + ] + }, + { + "node": 27, + "name": "05_Frames_West_main_1__frame", + "axis": 2, + "sign": 1, + "low": [ + -0.3149999976158142, + 1.2999999523162842, + 4.53000020980835 + ], + "high": [ + 1.4249999523162842, + 2.069999933242798, + 4.639999866485596 + ] + }, + { + "node": 29, + "name": "05_Frames_West_main_2__frame", + "axis": 2, + "sign": 1, + "low": [ + 2.3350000381469727, + -0.009999999776482582, + 4.53000020980835 + ], + "high": [ + 3.115000009536743, + 2.065000057220459, + 4.639999866485596 + ] + }, + { + "node": 44, + "name": "05_Frames_Terrace_back_1__frame", + "axis": 2, + "sign": -1, + "low": [ + 4.619999885559082, + -0.009999999776482582, + -0.6349999904632568 + ], + "high": [ + 6.460000038146973, + 2.069999933242798, + -0.5249999761581421 + ] + }, + { + "node": 48, + "name": "05_Frames_South_1__frame", + "axis": 0, + "sign": 1, + "low": [ + 7.505000114440918, + -0.009999999776482582, + 0.6819999814033508 + ], + "high": [ + 7.614999771118164, + 2.069999933242798, + 3.322000026702881 + ] + } + ], + "triangleExcludedNode": 51, + "sourceHierarchy": [ + "explicit user choices", + "project documentation", + "supplied ALLSTAV 2025 brochure" + ], + "scopeNotes": [ + "Kitchen selections are visualization only, not brochure standards.", + "Unlabelled floor/tile samples retain numerical visual indices, not invented product codes.", + "Sliding doors, ventilation and heating variants are specifications only; hidden services are not redesigned.", + "Door and sanitary geometry is illustrative, project opening/footprint is retained." + ] + }, + "webRevision": "locenice-web-01", + "webTextureLibrarySize": 32 +} \ No newline at end of file diff --git a/locenice/locenice/data/site.json b/locenice/locenice/data/site.json new file mode 100644 index 0000000..50cc380 --- /dev/null +++ b/locenice/locenice/data/site.json @@ -0,0 +1,326 @@ +{ + "version": "2026-10-site-01", + "referenceElevation": 591.84, + "source": "C.2-C.5 and Locenice p.c.3480/37 polohopis a vyskopis; digitized PDF vectors, not survey coordinates.", + "cadastralAreaInProject": 1010, + "digitizedOutlineArea": 987.89, + "note": "The graphical outline is about 988 m2, versus 1010 m2 in the project. It is NOT scaled to force area equality; house dimensions are unchanged.", + "finishedGroundC3": [ + [ + -7.67, + 4.695, + -0.48 + ], + [ + -7.67, + -4.695, + -0.48 + ], + [ + 3.713, + -4.695, + -0.992 + ], + [ + 3.713, + -0.688, + -0.871 + ], + [ + 7.67, + -0.688, + -1.14 + ], + [ + 7.67, + 4.695, + -0.971 + ], + [ + -1.113, + 4.695, + -0.48 + ] + ], + "surveyPoints": [ + { + "id": 1, + "xz": [ + -22.86387745468233, + 6.9162902718421835 + ], + "altitude": 593.13 + }, + { + "id": 2, + "xz": [ + -26.00649803769423, + 5.740604360018231 + ], + "altitude": 593.19 + }, + { + "id": 3, + "xz": [ + -22.733664786123356, + -2.9186632403462283 + ], + "altitude": 592.64 + }, + { + "id": 4, + "xz": [ + -19.66526683318922, + -1.6042685750232388 + ], + "altitude": 592.54 + }, + { + "id": 5, + "xz": [ + -16.849177040431528, + -9.283203935982339 + ], + "altitude": 592.08 + }, + { + "id": 6, + "xz": [ + -19.809586816163616, + -10.38887051378567 + ], + "altitude": 592.09 + }, + { + "id": 7, + "xz": [ + -19.100913252951532, + 0.5664836927799971 + ], + "altitude": 592.84 + }, + { + "id": 8, + "xz": [ + -21.249447968471955, + 7.071889756172209 + ], + "altitude": 592.85 + }, + { + "id": 9, + "xz": [ + -19.62233327368599, + 2.5536226040233165 + ], + "altitude": 592.59 + }, + { + "id": 10, + "xz": [ + -9.809681750990507, + 7.518430876054446 + ], + "altitude": 592.14 + }, + { + "id": 11, + "xz": [ + -8.73082925476737, + 0.6499201351270848 + ], + "altitude": 591.85 + }, + { + "id": 12, + "xz": [ + 7.4440611266407775, + 0.4236299245776415 + ], + "altitude": 590.74 + }, + { + "id": 13, + "xz": [ + 8.057613164117942, + 7.427291898661351 + ], + "altitude": 590.96 + }, + { + "id": 14, + "xz": [ + 22.42368569156818, + 7.487149207097606 + ], + "altitude": 589.96 + }, + { + "id": 15, + "xz": [ + 22.980738978199824, + 0.027095859342831083 + ], + "altitude": 589.65 + }, + { + "id": 16, + "xz": [ + 25.962873515635593, + 7.010131955908398 + ], + "altitude": 589.82 + }, + { + "id": 17, + "xz": [ + 36.80267751573004, + 5.954683418127402 + ], + "altitude": 589.03 + }, + { + "id": 18, + "xz": [ + 37.69887332216715, + -0.19213899494552378 + ], + "altitude": 588.82 + }, + { + "id": 19, + "xz": [ + 47.409464441441116, + 4.094040845154811 + ], + "altitude": 588.54 + }, + { + "id": 20, + "xz": [ + 51.16376748811935, + -2.0537658393134657 + ], + "altitude": 588.06 + }, + { + "id": 21, + "xz": [ + 54.12316842449226, + -9.187005561860445 + ], + "altitude": 587.79 + }, + { + "id": 22, + "xz": [ + 41.075319538836055, + -8.900127720728307 + ], + "altitude": 588.45 + }, + { + "id": 23, + "xz": [ + 23.482474278537843, + -7.205827671852879 + ], + "altitude": 589.47 + }, + { + "id": 24, + "xz": [ + 6.9940948033230175, + -7.604159411778007 + ], + "altitude": 590.56 + }, + { + "id": 25, + "xz": [ + -5.631153944681365, + -8.570201470985344 + ], + "altitude": 591.41 + }, + { + "id": 26, + "xz": [ + -14.560744944331192, + -8.695220715590304 + ], + "altitude": 591.9 + }, + { + "id": 27, + "xz": [ + -17.020111569954853, + -2.760537937058604 + ], + "altitude": 592.24 + } + ], + "parkingSpot": 0.39, + "entranceSpot": -0.48, + "gardenStairSpot": -1.395, + "originalNodeCount": 111, + "originalBinaryBytes": null, + "terrainClosure": { + "version": "2", + "description": "Continuous boundary walls and underside added to the three terrain slabs. Original grass surfaces and house geometry unchanged.", + "thickness_m": 0.6 + }, + "cadastralSource": "VYKAZ DOSAVADNIHO A NOVEHO STAVU UDAJU KN, plan 738-33/2023, 1.8.2023", + "cadastralCoordinates": [ + { + "id": "461-1364", + "Y": 754888.21, + "X": 1183134.81, + "qualityCode": 8 + }, + { + "id": "461-1508", + "Y": 754858.48, + "X": 1183140.31, + "qualityCode": 8 + }, + { + "id": "559-1", + "Y": 754878.61, + "X": 1183200.41, + "qualityCode": 8 + }, + { + "id": "559-2", + "Y": 754846.53, + "X": 1183210.96, + "qualityCode": 8 + }, + { + "id": "1", + "Y": 754873.34, + "X": 1183137.56, + "qualityCode": 8 + }, + { + "id": "2", + "Y": 754860.49, + "X": 1183206.37, + "qualityCode": 8 + }, + { + "id": "3", + "Y": 754866.57, + "X": 1183185.6, + "qualityCode": 3 + } + ], + "cadastralBoundary3480_37": [ + "1", + "461-1508", + "559-2", + "2", + "3" + ], + "sourceDistinction": "Cadastral coordinates are S-JTSK plan coordinates. Survey heights are Bpv. Survey xz is a graphical registration in the existing model, not a geodetic transformation. No house setting-out coordinates were provided." +} \ No newline at end of file diff --git a/locenice/locenice/index.php b/locenice/locenice/index.php new file mode 100644 index 0000000..b9a2a8c --- /dev/null +++ b/locenice/locenice/index.php @@ -0,0 +1,10 @@ + document.querySelector(s), canvas = $('#view'); + + let originalBytes = resources.modelBytes, gltf = null, binary = null, gl = null, meshes = [], shadowProgram, program, shadowFbo, shadowTex; + let showTerrain = true, showMeasure = false, autoRotate = false, orthographic = false, dirty = true, lastTime = 0; + let target = [.8, 1.15, 0], theta = 2.25, phi = .30, distance = 35.0, aspect = 1, vp, lightVP, eye = [0, 0, 0]; + let animation = null, pointerMap = new Map(), pinch = null, wheelTimer = 0; + const initialCamera = { theta: 2.25, phi: .30, distance: 35.0, target: [.8, 1.15, 0], ortho: false }; + // Opening composition follows the supplied reference; house geometry is unchanged. + // Keep this separate from the existing Entry / Terrace / Top view presets. + const startupCamera = { theta: .63, phi: .30, distance: 41.35, target: [-2.8, 2.3, 0], ortho: false }; + function applyStartupView() { + document.querySelectorAll('[data-preset]').forEach(b => b.classList.remove('active')); + $('#elevation').value = ''; + $('#panel').classList.remove('open'); + $('#infoButton').classList.remove('active'); + // Match the existing portrait resize rule, without fitting the entire long parcel. + setCamera({ ...startupCamera, distance: startupCamera.distance * Math.max(1, 1 / aspect) }, 'Voln\u00fd pohled', true); + autoRotate = true; + $('#rotateButton').classList.add('active'); + dirty = true; + } + const originalColors = {}; + const HARDIE_DEFAULT = 'oriskove-hneda'; + const hardieTextures = [], hardieImages = [], hardieObjectURLs = []; + let hardieSelected = HARDIE_DEFAULT; + async function loadHardieTextures(){backend.images.forEach((im,i)=>hardieImages[i]=im);} + function hardieLinearRGB(hex) { return hexToRgb(hex).map(c => c <= .04045 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4); } + function setHardieColor(id) { + const data = gltf?.extras?.hardiePlank; + const color = data?.palette.find(c => c.id === id || c.code === id || c.name === id); + if (!color) return false; + hardieSelected = color.id; + const boards = gltf.materials[data.boardMaterial], backing = gltf.materials[data.backingMaterial]; + boards.name = data.product + ' - ' + color.name; + boards.pbrMetallicRoughness.baseColorFactor = [1, 1, 1, 1]; + boards.pbrMetallicRoughness.baseColorTexture.index = color.texture; + boards.extras.color = color.name; boards.extras.productCode = color.code; + backing.pbrMetallicRoughness.baseColorFactor = [...hardieLinearRGB(color.hex).map(c => c * .72), 1]; + data.selectedColor = { ...color }; + $('#hardieColorName').textContent = color.name; + $('#hardieProductCode').textContent = color.code + (color.id === HARDIE_DEFAULT ? ' \u00b7 v\u00fdchoz\u00ed' : ''); + const preview = $('#hardiePreview'); preview.style.backgroundColor = color.hex; + if (hardieImages[color.texture]) preview.style.backgroundImage = 'url("' + hardieImages[color.texture].src + '")'; + preview.setAttribute('aria-label', data.product + ', ' + color.name + ' - orienta\u010dn\u00ed n\u00e1hled'); + document.querySelectorAll('input[name="hardieColor"]').forEach(input => { + input.checked = input.value === color.id; input.closest('label').classList.toggle('selected', input.checked); + }); + dirty = true; return true; + } + function focusHardieDetail(key) { if(interior.enabled)toggleInterior(false); + const portraitScale = Math.max(1, 1 / aspect); + const poses = { + entry: { theta: -.07, phi: .075, distance: 13.1 * portraitScale, target: [-4.05, 1.33, 3.90], ortho: false }, + terrace: { theta: 2.43, phi: .10, distance: 12.5 * portraitScale, target: [5.30, 1.35, -2.20], ortho: false }, + structure: { theta: Math.PI / 2, phi: 0, distance: 1.6 * portraitScale, target: [3.729, 1.32, -4.03], ortho: false } + }; + if (!poses[key]) return; + $('#panel').classList.remove('open'); $('#infoButton').classList.remove('active'); + document.querySelectorAll('[data-preset]').forEach(b => b.classList.remove('active')); + $('#elevation').value = ''; + setCamera(poses[key], key === 'entry' ? 'HardiePlank \u00b7 vstup' : key === 'terrace' ? 'HardiePlank \u00b7 terasa' : 'HardiePlank \u00b7 detail struktury'); + } + function initHardiePalette() { + document.querySelectorAll('input[name="hardieColor"]').forEach(input => { + input.addEventListener('change', () => { if (input.checked) setHardieColor(input.value); }); + }); + document.querySelectorAll('[data-cladding-view]').forEach(button => button.onclick = () => focusHardieDetail(button.dataset.claddingView)); + setHardieColor(HARDIE_DEFAULT); + window.HARDIE_PLANK = { + getState: () => ({ product: gltf.extras.hardiePlank.product, selected: { ...gltf.extras.hardiePlank.selectedColor }, defaultColor: HARDIE_DEFAULT, + palette: gltf.extras.hardiePlank.palette.map(c => ({ ...c })), boardNodes: [...gltf.extras.hardiePlank.boardNodes], + backingNodes: [...gltf.extras.hardiePlank.backingNodes], scope: 'facade_only', originalGeometryPreserved: true, textureIsIllustrative: true }), + setColor: id => setHardieColor(id), reset: () => setHardieColor(HARDIE_DEFAULT), detail: key => focusHardieDetail(key) + }; + } + + const vadd = (a, b) => a.map((x, i) => x + b[i]), vsub = (a, b) => a.map((x, i) => x - b[i]), scale = (a, s) => a.map(x => x * s), dot = (a, b) => a.reduce((r, x, i) => r + x * b[i], 0), norm = a => scale(a, 1 / Math.hypot(...a)), cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; + function mul(a, b) { const c = new Float32Array(16); for (let j = 0; j < 4; j++)for (let i = 0; i < 4; i++)for (let k = 0; k < 4; k++)c[j * 4 + i] += a[k * 4 + i] * b[j * 4 + k]; return c; } + function lookAt(e, t, up = [0, 1, 0]) { const z = norm(vsub(e, t)), x = norm(cross(up, z)), y = cross(z, x); return new Float32Array([x[0], y[0], z[0], 0, x[1], y[1], z[1], 0, x[2], y[2], z[2], 0, -dot(x, e), -dot(y, e), -dot(z, e), 1]); } + function perspective(fovy, asp, n, f) { const q = 1 / Math.tan(fovy / 2); return new Float32Array([q / asp, 0, 0, 0, 0, q, 0, 0, 0, 0, (f + n) / (n - f), -1, 0, 0, 2 * f * n / (n - f), 0]); } + function ortho(l, r, b, t, n, f) { return new Float32Array([2 / (r - l), 0, 0, 0, 0, 2 / (t - b), 0, 0, 0, 0, -2 / (f - n), 0, -(r + l) / (r - l), -(t + b) / (t - b), -(f + n) / (f - n), 1]); } + function point(m, p) { const a = [...p, 1], o = [0, 0, 0, 0]; for (let i = 0; i < 4; i++)for (let j = 0; j < 4; j++)o[i] += m[j * 4 + i] * a[j]; return o; } + + function getAccessor(i) { const a = gltf.accessors[i], v = gltf.bufferViews[a.bufferView]; return { a, v, offset: (v.byteOffset || 0) + (a.byteOffset || 0) }; } + function visible(m) { return standardsVisible(m) && interiorVisibility(m) && meshVisible(m); } + async function initGL(){ + const requested=new URLSearchParams(location.search).get('renderer')||config.renderer; + let primaryError=null; + if(requested!=='legacy')try{ + const task=(async()=>{const {createThreeRenderer}=await import('./three-renderer.js');return await createThreeRenderer({canvas,gltf,binary,bytes:originalBytes,config});})(); + // Do not fall back after WebGL initialization begins: that would double-bind a canvas. + backend=await task; + }catch(error){primaryError=error;console.warn('Three.js unavailable, using local renderer:',error);} + if(!backend){if(requested==='three')throw primaryError||Error('Three.js unavailable');backend=await createLegacyRenderer({canvas,gltf,binary});} + gl=backend;meshes=backend.meshes; + gltf.materials.forEach((m,i)=>originalColors[i]=[...(m.pbrMetallicRoughness?.baseColorFactor||[.7,.7,.7,1])]); + window.MODEL_INFO={objects:meshes.length,triangles:meshes.reduce((n,m)=>n+m.count/3,0),loaded:true,backend:backend.backend}; + document.getElementById('webStatus').textContent=backend.label+' / GLB'; + resize(); +} + function resize() { if (!gl) return; const r = canvas.getBoundingClientRect(), w = r.width, h = r.height, dpr = Math.min(devicePixelRatio || 1, 1.7); const newAspect = w / h; if (window.SUN_SIM && !interior.enabled) { distance *= Math.max(1, 1 / newAspect) / Math.max(1, 1 / aspect); animation = null; initialCamera.distance = Math.max(41, 38 / newAspect); presetDefs.entry.distance = Math.max(39, 38 / newAspect); presetDefs.top.distance = Math.max(46, 25 / newAspect); presetDefs.top.theta = newAspect < 1 ? Math.PI / 2 : 0; } viewW = w; viewH = h; backend.resize(w,h,Math.min(dpr,config.maxPixelRatio||1.5)); aspect = newAspect; if(interior.enabled && interior.mode !== 'walk' && !interior.userCamera)interiorFitCamera(); dirty = true; } + function camera() { if(interior.enabled && interior.mode === 'walk'){interiorCamera();return;} eye = vadd(target, [Math.sin(theta) * Math.cos(phi) * distance, Math.sin(phi) * distance, Math.cos(theta) * Math.cos(phi) * distance]); const view = lookAt(eye, target); const span = distance * .28; const projection = orthographic ? ortho(-span * aspect, span * aspect, -span, span, .1, 1000) : perspective(34 * Math.PI / 180, aspect, .1, 1000); vp = mul(projection, view); } + function render(){ + if(!backend)return; + camera(); + const lookTarget=interior.enabled&&interior.mode==='walk'?vadd(eye,[Math.sin(interior.yaw)*Math.cos(interior.pitch),Math.sin(interior.pitch),Math.cos(interior.yaw)*Math.cos(interior.pitch)]):target; + backend.draw({solar,interior,eye,vp,lightVP,lookTarget,orthographic:orthographic&&!(interior.enabled&&interior.mode==='walk'),visible}); + solar.shadowDirty=false; + $('#compassGroup').setAttribute('transform',`rotate(${(theta-Math.PI/2)*180/Math.PI-solar.bearing},42,42)`); + if(showMeasure)drawMeasures();if(solar.pos)solarOverlay();drawInteriorLabels(); + projectPanel?.drawSurvey(project,p=>point(vp,p),viewW,viewH); + dirty=false; +} + function project(p) { let q = point(vp, p); return [(q[0] / q[3] * .5 + .5) * viewW, (-q[1] / q[3] * .5 + .5) * viewH]; } + function drawMeasures() { + const dims = [{ a: [-7.67, .12, 5.54], b: [7.67, .12, 5.54], t: '15,340 m' }, { a: [-8.48, .12, -4.695], b: [-8.48, .12, 4.695], t: '9,390 m' }, { a: [7.9, 0, 0], b: [7.9, 5.137, 0], t: '5,137 m' }]; + let h = ''; for (const d of dims) { let a = project(d.a), b = project(d.b), x = (a[0] + b[0]) / 2, y = (a[1] + b[1]) / 2 - 10; h += `${d.t}`; } $('#measure').innerHTML = h; + } + function animate(time) { requestAnimationFrame(animate); const dt = Math.min(.05, (time - lastTime) / 1000 || .016); lastTime = time; solarFrame(dt); stepInterior(dt); if (animation) { const a = animation, t = Math.min(1, (time - a.start) / 650), k = t * t * (3 - 2 * t); theta = a.from.theta + (a.to.theta - a.from.theta) * k; phi = a.from.phi + (a.to.phi - a.from.phi) * k; distance = a.from.distance + (a.to.distance - a.from.distance) * k; target = a.from.target.map((v, i) => v + (a.to.target[i] - v) * k); dirty = true; if (t >= 1) { orthographic = a.to.ortho; animation = null; } } if (autoRotate && !pointerMap.size) { theta += dt * .11; dirty = true; } if (dirty && gl) render(); } + function setCamera(to, label, instant = false) { autoRotate = false; $('#rotateButton').classList.remove('active'); if (instant) { theta = to.theta; phi = to.phi; distance = to.distance; target = [...to.target]; orthographic = to.ortho; animation = null; } else { let tt = to.theta; while (tt - theta > Math.PI) tt -= 2 * Math.PI; while (tt - theta < -Math.PI) tt += 2 * Math.PI; animation = { start: performance.now(), from: { theta, phi, distance, target: [...target] }, to: { ...to, theta: tt } }; orthographic = to.ortho; } $('#directionText').textContent = label; dirty = true; } + const presetDefs = { entry: { theta: -.59, phi: .24, distance: 33.4, target: [-.25, 1.20, 0], ortho: false }, terrace: initialCamera, top: { theta: 0, phi: 1.5706, distance: 35, target: [1, .1, 0], ortho: true } }; + const presetNames = { entry: 'Vstup / severoz\u00e1pad', terrace: 'Terasa / jihov\u00fdchod', top: 'Pohled shora' }; + function setPreset(key, instant = false) { if(interior.enabled)toggleInterior(false); document.querySelectorAll('[data-preset]').forEach(b => b.classList.toggle('active', b.dataset.preset === key)); $('#elevation').value = ''; setCamera(presetDefs[key], presetNames[key], instant); } + window.setPreset = setPreset; + function pan(dx, dy) { const right = [Math.cos(theta), 0, -Math.sin(theta)]; const up = norm(cross(norm(vsub(eye, target)), right)); const k = distance * .0008; target = vadd(target, vadd(scale(right, -dx * k), scale(up, dy * k))); } + canvas.addEventListener('contextmenu', e => e.preventDefault()); + canvas.addEventListener('pointerdown', e => { if(interior.enabled && interior.mode==='walk'){e.preventDefault();canvas.setPointerCapture(e.pointerId);interior.lookPointer={id:e.pointerId,x:e.clientX,y:e.clientY};interior.keys.clear();return;} if(interior.enabled)interior.userCamera=true; canvas.setPointerCapture(e.pointerId); pointerMap.set(e.pointerId, { x: e.clientX, y: e.clientY, button: e.button }); animation = null; autoRotate = false; $('#rotateButton').classList.remove('active'); if (pointerMap.size === 2) { const p = [...pointerMap.values()]; pinch = { d: Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y), x: (p[0].x + p[1].x) / 2, y: (p[0].y + p[1].y) / 2 }; } }); + canvas.addEventListener('pointermove', e => { if(interior.enabled && interior.mode==='walk'){const p=interior.lookPointer;if(p && p.id===e.pointerId){interior.yaw-=(e.clientX-p.x)*.004;interior.pitch=clamp(interior.pitch-(e.clientY-p.y)*.003,-1.3,1.3);p.x=e.clientX;p.y=e.clientY;dirty=true;}return;} const p = pointerMap.get(e.pointerId); if (!p) return; let dx = e.clientX - p.x, dy = e.clientY - p.y; pointerMap.set(e.pointerId, { ...p, x: e.clientX, y: e.clientY }); if (pointerMap.size === 2) { const ps = [...pointerMap.values()], d = Math.hypot(ps[0].x - ps[1].x, ps[0].y - ps[1].y), x = (ps[0].x + ps[1].x) / 2, y = (ps[0].y + ps[1].y) / 2; if (pinch) { distance = Math.max(1.2, Math.min(500, distance * pinch.d / Math.max(1, d))); pan(x - pinch.x, y - pinch.y); } pinch = { d, x, y }; } else if (p.button === 2 || e.shiftKey) pan(dx, dy); else { theta -= dx * .006; phi = Math.max(.03, Math.min(1.51, phi + dy * .006)); orthographic = false; } document.querySelectorAll('[data-preset]').forEach(b => b.classList.remove('active')); $('#directionText').textContent = 'Voln\u00fd pohled'; dirty = true; }); + function pointerEnd(e) { if(interior.lookPointer?.id===e.pointerId)interior.lookPointer=null; pointerMap.delete(e.pointerId); pinch = null; } + canvas.addEventListener('pointerup', pointerEnd); canvas.addEventListener('pointercancel', pointerEnd); + canvas.addEventListener('wheel', e => { e.preventDefault(); if(interior.enabled && interior.mode==='walk'){interior.fov=clamp(interior.fov+e.deltaY*.025,45,88);dirty=true;return;} if(interior.enabled)interior.userCamera=true; distance = Math.max(1.2, Math.min(500, distance * Math.exp(e.deltaY * .001))); animation = null; dirty = true; }, { passive: false }); + document.querySelectorAll('[data-preset]').forEach(b => b.onclick = () => setPreset(b.dataset.preset)); + $('#resetView').onclick = () => setPreset('terrace'); + $('#terrainButton').onclick = () => { showTerrain = !showTerrain; $('#terrainButton').classList.toggle('active', !showTerrain); updateSolar(); dirty = true; }; + $('#measureButton').onclick = () => { showMeasure = !showMeasure; $('#measure').style.display = showMeasure ? 'block' : 'none'; $('#measureButton').classList.toggle('active', showMeasure); dirty = true; }; + $('#rotateButton').onclick = () => { autoRotate = !autoRotate; $('#rotateButton').classList.toggle('active', autoRotate); orthographic = false; dirty = true; }; + $('#infoButton').onclick = () => { $('#panel').classList.toggle('open'); $('#infoButton').classList.toggle('active'); }; + $('#elevation').onchange = e => { const d = { north: [-Math.PI / 2, 'Severn\u00ed pohled'], east: [Math.PI, 'V\u00fdchodn\u00ed pohled'], south: [Math.PI / 2, 'Ji\u017en\u00ed pohled'], west: [0, 'Z\u00e1padn\u00ed pohled'] }[e.target.value]; if (!d) return; document.querySelectorAll('[data-preset]').forEach(b => b.classList.remove('active')); setCamera({ theta: d[0], phi: 0, distance: aspect < .8 ? 70 : 29, target: [0, 1.8, 0], ortho: true }, d[1]); }; + function hexToRgb(s) { return [1, 3, 5].map(i => parseInt(s.slice(i, i + 2), 16) / 255); } + function recolor(type, hex) { const c = hexToRgb(hex); gltf.materials.forEach((m, i) => { let ratio = 1, match = false; if (type === 'wall') match = m.name === 'stucco'; if (type === 'roof') { match = m.name.startsWith('tile'); ratio = m.name === 'tile_1' ? 1.06 : m.name === 'tile_2' ? .94 : m.name === 'tile_3' ? 1.10 : m.name === 'tile_4' ? .88 : m.name === 'tile_edge' ? .90 : 1; } if (match) m.pbrMetallicRoughness.baseColorFactor = [...c.map(v => Math.min(1, v * ratio)), 1]; }); dirty = true; } + $('#wallColor').oninput = e => recolor('wall', e.target.value); $('#roofColor').oninput = e => recolor('roof', e.target.value); + $('#resetColors').onclick = () => { for (const [i, c] of Object.entries(originalColors)) { if (!gltf.materials[+i].extras?.interiorMaterial) gltf.materials[+i].pbrMetallicRoughness.baseColorFactor = [...c]; } $('#wallColor').value = '#c9c2ad'; $('#roofColor').value = '#414542'; setHardieColor(HARDIE_DEFAULT); dirty = true; }; + function toast(s) { $('#toast').textContent = s; $('#toast').style.opacity = 1; setTimeout(() => $('#toast').style.opacity = 0, 2600); } + function saveBlob(blob, name) { const a = document.createElement('a'), u = URL.createObjectURL(blob); a.href = u; a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(u), 5000); } + function currentGlb() { if (!gltf) return originalBytes; const j = new TextEncoder().encode(JSON.stringify(exportVisibleGltf())), jl = Math.ceil(j.length / 4) * 4, bl = Math.ceil(binary.byteLength / 4) * 4, n = 12 + 8 + jl + 8 + bl, out = new Uint8Array(n), v = new DataView(out.buffer); v.setUint32(0, 0x46546c67, true); v.setUint32(4, 2, true); v.setUint32(8, n, true); v.setUint32(12, jl, true); v.setUint32(16, 0x4e4f534a, true); out.fill(32, 20, 20 + jl); out.set(j, 20); v.setUint32(20 + jl, bl, true); v.setUint32(24 + jl, 0x004e4942, true); out.set(binary, 28 + jl); return out; } + $('#saveGlb').onclick = () => { saveBlob(new Blob([currentGlb()], { type: 'model/gltf-binary' }), 'dum_3D_HardiePlank_' + hardieSelected + '.glb'); toast('Model GLB je p\u0159ipraven\u00fd ke sta\u017een\u00ed.'); }; + $('#savePng').onclick = () => { render(); canvas.toBlob(b => saveBlob(b, 'dum_pohled.png'), 'image/png'); }; + window.addEventListener('resize', resize); + window.addEventListener('keydown', e => { if(interior.enabled){handleInteriorKey(e);return;} if(e.code==='KeyI' && !/^(INPUT|SELECT|TEXTAREA)$/.test(e.target.tagName)){toggleInterior(true);return;} if (e.key === 'Escape') { $('#panel').classList.remove('open'); $('#infoButton').classList.remove('active'); } if (/^(INPUT|SELECT|TEXTAREA)$/.test(e.target.tagName)) return; if (e.key === '1') setPreset('entry'); if (e.key === '2') setPreset('terrace'); if (e.key === '3') setPreset('top'); if (e.key === 'r') setPreset('terrace'); }); + async function start(){ + try{ + ({gltf,binary}=parseGLB(originalBytes)); + gltf.extras={...gltf.extras,...structuredClone(resources.scene)}; + await initGL();await loadHardieTextures();initHardiePalette();initInterior();initSolar(); + distance=Math.max(41,38/aspect);initialCamera.distance=distance;presetDefs.entry.distance=Math.max(39,38/aspect);presetDefs.top.distance=Math.max(46,25/aspect); + if(aspect<1)presetDefs.top.theta=Math.PI/2;initSiteControls();applyStartupView();initStandards(); + projectPanel=installProjectPanel({resources,backend,getMode:()=>interior,project,focusSite:()=>{stdClose();if(interior.enabled)toggleInterior(false);focusSite('parcel');},invalidate:()=>{dirty=true;}}); + autoRotate=!!config.startAutoRotate;$('#rotateButton').classList.toggle('active',autoRotate); + $('#loading').style.display='none';requestAnimationFrame(animate); + window.LOCENICE_APP={backend:backend.backend,exportSource:()=>encodeGLB(gltf,binary),getMeshes:()=>meshes.map(m=>({name:m.name,nodeIndex:m.nodeIndex,visible:visible(m),material:m.material,std:m.std})),render}; + if('ontouchstart'in window)$('.help').innerHTML='Jednim prstem otacejte; dvema prsty priblizte a posunte.'; + }catch(error){console.error(error);$('#loading').className='error';$('#loading').innerHTML='
Stahnout zdrojovy GLBZkusit lokalni renderer'; + $('#loading .errorDetail').textContent='Model se nepodarilo spustit: '+error.message;$('#webStatus').textContent='3D neni dostupne';} +} + + // Solar geometry: Meeus / NOAA. UTC is the only time basis for the ephemeris. + // Model axes: +X = south gable, +Z = entrance (west) side, +Y = up. + const RAD = Math.PI / 180, DAY_MS = 86400000; + const solar = { + date: '2026-06-21', minute: 600, lat: 48.8242, lon: 14.5275, bearing: 351.5, + playing: false, speed: 30, shadows: true, path: false, start: 0, end: 0, length: 1440, + rise: null, set: null, noon: null, samples: [], pos: null, light: [1, 1, 1], sky: [.906, .922, .890], + sunColor: [1, 1, .95], ambient: [.40, .43, .46], direct: .85, dayFactor: 1, shadowDirty: true, + accumulator: 0, clock: 0 + }; + let viewW = innerWidth, viewH = innerHeight; + 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) => { let t = clamp((x - a) / (b - a), 0, 1); return t * t * (3 - 2 * t); }; + const mix3 = (a, b, t) => a.map((v, i) => v + (b[i] - v) * t); + const czNum = (v, d = 1) => v.toFixed(d).replace('.', ','); + 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 o = {}; for (const p of localFormatter.formatToParts(new Date(ms))) if (p.type !== 'literal') o[p.type] = +p.value; return o; } + 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(date) { const [y, m, d] = date.split('-').map(Number), naive = Date.UTC(y, m - 1, d); let u = naive; for (let i = 0; i < 4; i++)u = naive - utcOffset(u) * 60000; return u; } + function isoDate(ms) { const p = localParts(ms); return `${p.year}-${String(p.month).padStart(2, '0')}-${String(p.day).padStart(2, '0')}`; } + function clockText(ms) { if (ms === null || !Number.isFinite(ms)) return '\u2014'; 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 o = utcOffset(ms); return (o === 120 ? 'SEL\u010c' : o === 60 ? 'SE\u010c' : 'Praha') + ' \u00b7 UTC' + (o >= 0 ? '+' : '') + o / 60; } + + function solarVector(azimuth, elevation) { const a = (azimuth - solar.bearing) * RAD, e = elevation * RAD; return [-Math.cos(a) * Math.cos(e), Math.sin(e), -Math.sin(a) * Math.cos(e)]; } + function timeForClock(text) { + const [h, m] = text.split(':').map(Number); if (!/^\d{1,2}:\d{2}$/.test(text) || !Number.isFinite(h) || !Number.isFinite(m) || h < 0 || h > 23 || m < 0 || m > 59) return null; text = String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0'); + const [y, mo, d] = solar.date.split('-').map(Number), 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(o => naive - o * 60000).filter(u => u >= solar.start && u < solar.end && clockText(u) === text).sort((a, b) => a - b); + if (candidates.length) return candidates[0]; + // The missing spring-forward hour advances to the first valid following clock time. + for (let i = 0; i < solar.length; i++) { const u = solar.start + i * 60000, p = localParts(u); if (p.hour * 60 + p.minute >= h * 60 + m) return u; } + return solar.end - 60000; + } + function rootElevation(a, b, threshold) { let fa = solarPosition(a, solar.lat, solar.lon).elevation - threshold; for (let i = 0; i < 24; i++) { const m = (a + b) / 2, fm = solarPosition(m, solar.lat, solar.lon).elevation - threshold; if ((fa < 0) === (fm < 0)) { a = m; fa = fm; } else b = m; } return (a + b) / 2; } + function rebuildSolarDay(preserveClock = true) { + const clock = 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), next = new Date(Date.UTC(y, m - 1, d + 1)).toISOString().slice(0, 10); + solar.end = localMidnight(next); solar.length = Math.round((solar.end - solar.start) / 60000); solar.rise = null; solar.set = null; solar.samples = []; + let prev = null, max = { alt: -999, ms: solar.start }; + for (let u = solar.start; u <= solar.end; u += 600000) { + const p = solarPosition(u, solar.lat, solar.lon); solar.samples.push({ ms: u, ...p }); if (p.elevation > max.alt) max = { alt: p.elevation, ms: u }; + if (prev) { if (prev.elevation < -.833 && p.elevation >= -.833) solar.rise = rootElevation(prev.ms, u, -.833); if (prev.elevation >= -.833 && p.elevation < -.833) solar.set = rootElevation(prev.ms, u, -.833); } prev = { ms: u, ...p }; + } + let lo = Math.max(solar.start, max.ms - 600000), hi = Math.min(solar.end, max.ms + 600000); for (let i = 0; i < 36; i++) { let a = lo + (hi - lo) / 3, b = hi - (hi - lo) / 3; if (solarPosition(a, solar.lat, solar.lon).elevation < solarPosition(b, solar.lat, solar.lon).elevation) lo = a; else hi = b; } solar.noon = (lo + hi) / 2; + solar.minute = clamp((timeForClock(clock) - 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(b => b.classList.toggle('active', solar.date.endsWith(b.dataset.season))); + const tickEl = $('.sun-ticks'); tickEl.style.position = 'relative'; tickEl.style.height = '12px'; + [...tickEl.children].forEach((el, i) => { const mm = i === 4 ? solar.length : (timeForClock(el.textContent) - solar.start) / 60000; el.style.cssText = `position:absolute;left:${100 * mm / solar.length}%;transform:translateX(${i === 0 ? 0 : i === 4 ? -100 : -50}%)`; }); + $('#solarMessage').textContent = solar.length === 1440 ? '' : `Tento den m\u00e1 ${solar.length / 60} hodin. \u010casov\u00e1 osa respektuje zm\u011bnu letn\u00edho \u010dasu.`; + drawSolarGraphBase(); updateSolar(); + } + let graphBase = ''; + function drawSolarGraphBase() { + const y = alt => 70 - clamp(alt, 0, 90) * .65; + const pts = solar.samples.map(p => `${8 + 288 * (p.ms - solar.start) / (solar.end - solar.start)},${y(p.elevation)}`); + const curve = 'M' + pts.join(' L'), max = solarPosition(solar.noon, solar.lat, solar.lon).elevation; + graphBase = `45\u00b0`; + } + function updateSolarGraph() { const p = solar.pos, x = 8 + 288 * solar.minute / solar.length, y = 70 - clamp(p.elevation, 0, 90) * .65; $('#sunGraph').innerHTML = graphBase + ``; } + function updateSolar() { + solar.clock = solar.start + solar.minute * 60000; solar.pos = solarPosition(solar.clock, solar.lat, solar.lon); + const a = solar.pos.elevation; solar.light = solarVector(solar.pos.azimuth, a); + solar.dayFactor = smooth(-10, 4, a); const daylight = solar.dayFactor; + const warm = 1 - smooth(0, 24, a); + solar.sunColor = mix3([1.07, 1.03, .95], [1.30, .78, .38], warm); + solar.direct = a > 0 ? (1.02 - .32 * Math.exp(-Math.max(a, 0) / 9)) * smooth(0, 1.2, a) : 0; + solar.ambient = mix3([.017, .027, .047], [.38, .415, .44], daylight); + solar.sky = mix3([.075, .112, .157], [.906, .922, .890], daylight); + if (a > -7 && a < 9) solar.sky = mix3(solar.sky, [.89, .83, .74], .16 * Math.sin((a + 7) / 16 * Math.PI)); + const lt = [0, 1, 0], lp = vadd(lt, scale(solar.light, 70)); + lightVP = mul(ortho(-22, 22, -21, 21, 1, 145), lookAt(lp, lt, Math.abs(solar.light[1]) > .999 ? [0, 0, 1] : [0, 1, 0])); + updateSiteLight(); + solar.shadowDirty = true; dirty = true; + $('#sunClock').value = clockText(solar.clock); $('#sunTime').value = Math.floor(solar.minute); $('#sunTime').setAttribute('aria-valuetext', clockText(solar.clock)); + $('#zoneLabel').textContent = zoneText(solar.clock); + $('#sunElevation').textContent = czNum(a) + '\u00b0'; $('#sunAzimuth').textContent = czNum(solar.pos.azimuth) + '\u00b0'; + const dirs = ['sever', 'severov\u00fdchod', 'v\u00fdchod', 'jihov\u00fdchod', 'jih', 'jihoz\u00e1pad', 'z\u00e1pad', 'severoz\u00e1pad']; + $('#sunDirection').textContent = dirs[Math.round(solar.pos.azimuth / 45) % 8] + ' \u00b7 od severu'; + $('#dayState').textContent = a > 0 ? 'Slunce nad obzorem' : a > -.833 ? 'Slunce na obzoru' : a > -6 ? 'Soumrak' : 'Noc \u00b7 bez p\u0159\u00edm\u00e9ho slunce'; + $('#sceneTime').textContent = czDate(solar.date) + ' \u00b7 ' + clockText(solar.clock) + ' ' + (utcOffset(solar.clock) === 120 ? 'SEL\u010c' : 'SE\u010c'); + $('#bearingNote').textContent = `Ji\u017en\u00ed \u0161t\u00edt: ${czNum(mod(solar.bearing + 180, 360))}\u00b0 \u00b7 vstupn\u00ed fas\u00e1da: ${czNum(mod(solar.bearing + 270, 360))}\u00b0.`; + document.body.classList.toggle('night', a < -4); document.body.style.backgroundColor = `rgb(${solar.sky.map(v => Math.round(v * 255)).join(',')})`; + updateSolarGraph(); + } + 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 >= .075) { solar.accumulator = 0; updateSolar(); } } + function setSolarPlaying(on) { solar.playing = on; $('#sunPlay').innerHTML = on ? '❚❚ Pozastavit' : '▶ P\u0159ehr\u00e1t den'; $('#sunPlay').setAttribute('aria-pressed', String(on)); } + function toggleSunPanel(on) { const yes = on === undefined ? !$('#sunPanel').classList.contains('open') : on; $('#sunPanel').classList.toggle('open', yes); document.body.classList.toggle('sun-open', yes); $('#sunToggle').classList.toggle('active', yes); $('#sunToggle').setAttribute('aria-expanded', String(yes)); if (yes) { $('#panel').classList.remove('open'); $('#infoButton').classList.remove('active'); } resize(); } + function solarOverlay() { + const svg = $('#sunOverlay'); svg.setAttribute('viewBox', `0 0 ${viewW} ${viewH}`); + const compassSun = $('#compassSun'); if (compassSun) { let a = solar.pos.azimuth * RAD; compassSun.setAttribute('cx', 42 + 26 * Math.sin(a)); compassSun.setAttribute('cy', 42 - 26 * Math.cos(a)); compassSun.style.display = solar.pos.elevation > 0 ? 'block' : 'none'; } + if (!solar.path) { svg.innerHTML = ''; return; } + const center = [0, .8, 0], radius = 12.8; + const pathPoint = (az, al) => vadd(center, scale(solarVector(az, al), radius)); + let d = '', started = false; for (const p of solar.samples) { if (p.elevation < 0) { started = false; continue; } const v = pathPoint(p.azimuth, p.elevation), q = point(vp, v); if (q[3] <= 0) { started = false; continue; } const a = project(v); d += (started ? 'L' : 'M') + a.map(x => x.toFixed(1)).join(','); started = true; } + let out = ``; + for (let hour = 6; hour <= 21; hour += 3) { const ms = timeForClock(String(hour).padStart(2, '0') + ':00'), p = solarPosition(ms, solar.lat, solar.lon); if (p.elevation < 0) continue; const v = pathPoint(p.azimuth, p.elevation); if (point(vp, v)[3] <= 0) continue; const q = project(v); out += `${hour}:00`; } + if (solar.pos.elevation >= 0) { const v = pathPoint(solar.pos.azimuth, solar.pos.elevation), q = project(v); if (point(vp, v)[3] > 0) { const base = project([v[0], center[1], v[2]]); out += ``; } } + svg.innerHTML = out; + } + async function saveSolarImage() { + setSolarPlaying(false); updateSolar(); render(); + const out = document.createElement('canvas'), ratio = canvas.width / viewW, footer = Math.round(77 * ratio); out.width = canvas.width; out.height = canvas.height + footer; const ctx = out.getContext('2d'); ctx.drawImage(canvas, 0, 0); + if (solar.path) { try { const xml = `${$('#sunOverlay').innerHTML}`, u = URL.createObjectURL(new Blob([xml], { type: 'image/svg+xml' })); const im = new Image(); await new Promise((resolve, reject) => { im.onload = resolve; im.onerror = reject; im.src = u; }); ctx.drawImage(im, 0, 0, canvas.width, canvas.height); URL.revokeObjectURL(u); } catch (e) { console.warn('Solar overlay export', e); } } + ctx.fillStyle = '#fafbf6'; ctx.fillRect(0, canvas.height, canvas.width, footer); ctx.fillStyle = '#3e5138'; ctx.font = `${Math.round(12 * ratio)}px Arial`; + let primary = `Lo\u010denice | ${czDate(solar.date)} | ${clockText(solar.clock)} ${zoneText(solar.clock)} | v\u00fd\u0161ka ${czNum(solar.pos.elevation)}\u00b0 | azimut ${czNum(solar.pos.azimuth)}\u00b0`; + let secondary = `Terén: C.3 + interpolace výškopisu. Bez okolní zástavby, stromů a místního horizontu.`; + const max = canvas.width - 32 * ratio; while (ctx.measureText(primary).width > max) { const px = parseFloat(ctx.font); if (px <= 8 * ratio) break; ctx.font = `${px - 1}px Arial`; } + ctx.fillText(primary, 16 * ratio, canvas.height + 26 * ratio); ctx.font = `${Math.round(10 * ratio)}px Arial`; ctx.fillStyle = '#7d8976'; + if (ctx.measureText(secondary).width > max) secondary = 'Orienta\u010dn\u00ed studie. Bez okoln\u00ed z\u00e1stavby a strom\u016f.'; + ctx.fillText(secondary, 16 * ratio, canvas.height + 47 * ratio); ctx.fillText(`GPS ${solar.lat.toFixed(4)}, ${solar.lon.toFixed(4)} | severn\u00ed \u0161t\u00edt ${czNum(solar.bearing)}\u00b0`, 16 * ratio, canvas.height + 64 * ratio); + out.toBlob(b => { if (b) saveBlob(b, `dum_slunce_${solar.date}_${clockText(solar.clock).replace(':', '-')}.png`); }, 'image/png'); + } + function initSolar() { + $('#sunDate').onchange = e => { if (!e.target.value || !e.target.checkValidity()) { toast('Zadej datum v letech 2000 a\u017e 2100.'); e.target.value = solar.date; return; } setSolarPlaying(false); solar.date = e.target.value; rebuildSolarDay(); }; + $('#sunClock').onchange = e => { if (!e.target.value) return; setSolarPlaying(false); const u = timeForClock(e.target.value); if (u === null) { toast('Zadej \u010das HH:MM, nap\u0159. 09:30.'); e.target.value = clockText(solar.clock); return; } solar.minute = (u - solar.start) / 60000; updateSolar(); }; + $('#sunTime').oninput = e => { setSolarPlaying(false); solar.minute = +e.target.value; updateSolar(); }; + $('#sunPlay').onclick = () => setSolarPlaying(!solar.playing); $('#sunSpeed').onchange = e => solar.speed = +e.target.value; + $('#sunNow').onclick = () => { const now = Date.now(); setSolarPlaying(false); solar.date = isoDate(now); rebuildSolarDay(false); solar.minute = clamp((now - solar.start) / 60000, 0, solar.length - 1); updateSolar(); }; + document.querySelectorAll('[data-season]').forEach(b => b.onclick = () => { setSolarPlaying(false); solar.date = solar.date.slice(0, 4) + '-' + b.dataset.season; rebuildSolarDay(); }); + $('#sunShadows').onchange = e => { solar.shadows = e.target.checked; solar.shadowDirty = true; dirty = true; }; + $('#sunPath').onchange = e => { solar.path = e.target.checked; dirty = true; $('#pathHint').textContent = solar.path ? 'Dr\u00e1ha je symbolick\u00e1, v mal\u00e9m m\u011b\u0159\u00edtku. Pro celou dr\u00e1hu oddal pohled kole\u010dkem nebo dv\u011bma prsty.' : 'Posouvej \u010das a sleduj, kam dopad\u00e1 slunce na terasu a fas\u00e1dy. Nato\u010den\u00ed pohledu polohu slunce nem\u011bn\u00ed.'; }; + for (const [id, key] of [['sunLat', 'lat'], ['sunLon', 'lon'], ['houseBearing', 'bearing']]) $('#' + id).onchange = e => { const val = +e.target.value; if (!e.target.value || !Number.isFinite(val) || !e.target.checkValidity()) { toast('Zadej hodnotu v povolen\u00e9m rozsahu.'); e.target.value = solar[key]; return; } solar[key] = val; rebuildSolarDay(); }; + $('#sunResetSite').onclick = () => { solar.lat = 48.8242; solar.lon = 14.5275; solar.bearing = 351.5; $('#sunLat').value = solar.lat; $('#sunLon').value = solar.lon; $('#houseBearing').value = solar.bearing; rebuildSolarDay(); }; + $('#sunToggle').onclick = () => toggleSunPanel(); $('#sunClose').onclick = () => toggleSunPanel(false); $('#sunSavePng').onclick = saveSolarImage; $('#savePng').onclick = saveSolarImage; + $('#infoButton').onclick = () => { const open = !$('#panel').classList.contains('open'); $('#panel').classList.toggle('open', open); $('#infoButton').classList.toggle('active', open); if (open) toggleSunPanel(false); }; + const graph = $('#sunGraph'); function scrubGraph(e) { const r = graph.getBoundingClientRect(); solar.minute = clamp(((e.clientX - r.left) / r.width * 304 - 8) / 288 * solar.length, 0, solar.length - 1); setSolarPlaying(false); updateSolar(); } + graph.addEventListener('pointerdown', e => { graph.setPointerCapture(e.pointerId); scrubGraph(e); }); graph.addEventListener('pointermove', e => { if (graph.hasPointerCapture(e.pointerId)) scrubGraph(e); }); graph.addEventListener('pointerup', e => { if (graph.hasPointerCapture(e.pointerId)) graph.releasePointerCapture(e.pointerId); }); + window.addEventListener('keydown', e => { if (['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON', 'SUMMARY'].includes(e.target.tagName)) return; if (e.code === 'Space') { e.preventDefault(); setSolarPlaying(!solar.playing); } if (e.key.toLowerCase() === 's') toggleSunPanel(); }); + document.addEventListener('visibilitychange', () => { if (document.hidden) setSolarPlaying(false); }); + const cp = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); cp.id = 'compassSun'; cp.setAttribute('r', '3'); cp.setAttribute('fill', '#c89439'); cp.setAttribute('stroke', '#fff7d7'); cp.setAttribute('stroke-width', '1'); $('#compassGroup').appendChild(cp); + toggleSunPanel(false); rebuildSolarDay(false); + // Read-only diagnostics and small public control API for verification and reuse. + window.SUN_SIM = { position: solarPosition, getState: () => ({ date: solar.date, time: clockText(solar.clock), utc: new Date(solar.clock).toISOString(), dayMinutes: solar.length, bearing: solar.bearing, lat: solar.lat, lon: solar.lon, azimuth: solar.pos.azimuth, elevation: solar.pos.elevation, light: solar.light.slice(), rise: clockText(solar.rise), set: clockText(solar.set), noon: clockText(solar.noon), shadows: solar.shadows, playing: solar.playing }), setDate: d => { solar.date = d; rebuildSolarDay(); }, setTime: t => { solar.minute = (timeForClock(t) - solar.start) / 60000; updateSolar(); }, setBearing: b => { solar.bearing = mod(b, 360); $('#houseBearing').value = solar.bearing; updateSolar(); }, pause: () => setSolarPlaying(false) }; + } + + // Independent site layers: initSiteControls enables all three on each fresh open. + const site = { road: false, parcel: false, parking: false, savedCamera: null, lastFocus: null }; + const siteActive = () => site.road || site.parcel || site.parking; + function meshVisible(m) { + if (m.legacySite) return false; + switch (m.siteLayer) { + case 'local': return showTerrain; + case 'north': return showTerrain && siteActive(); + case 'parcel': case 'boundary': return showTerrain && site.parcel; + case 'road': return site.road; + case 'parking': return site.parking; + default: return showTerrain || !m.context; + } + } + function visibleBounds() { + const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity]; + for (const m of meshes) if (visible(m) && m.bounds) { + for (let i = 0; i < 3; i++) { lo[i] = Math.min(lo[i], m.bounds.min[i]); hi[i] = Math.max(hi[i], m.bounds.max[i]); } + } + return { lo, hi }; + } + function boxCorners(lo, hi) { + const out = []; for (let k = 0; k < 8; k++) out.push([0, 1, 2].map(i => k & (1 << i) ? hi[i] : lo[i])); return out; + } + function fittedSiteCamera(mode) { + let bounds = visibleBounds(); + if (mode === 'parking') bounds = { lo: [-22.2, -.8, -8.0], hi: [-6.3, 5.8, 7.1] }; + const mobile = aspect < 1; + const th = mode === 'parcel' ? (mobile ? Math.PI / 2 + .12 : .24) : (mobile ? -.94 : -.44); + const ph = mode === 'parcel' ? (mobile ? 1.02 : .88) : .72; + const center = bounds.lo.map((v, i) => (v + bounds.hi[i]) / 2); + const forward = [Math.sin(th) * Math.cos(ph), Math.sin(ph), Math.cos(th) * Math.cos(ph)]; + const right = [Math.cos(th), 0, -Math.sin(th)], up = cross(forward, right); + const tangent = Math.tan(17 * Math.PI / 180); + const usableY = mobile ? .58 : .69, usableX = .82; + let dist = 16; + for (const p of boxCorners(bounds.lo, bounds.hi)) { + const d = vsub(p, center), depth = dot(d, forward); + dist = Math.max(dist, depth + Math.abs(dot(d, right)) / (tangent * aspect * usableX), depth + Math.abs(dot(d, up)) / (tangent * usableY)); + } + return { theta: th, phi: ph, target: center, distance: dist + 2, ortho: false }; + } + function focusSite(mode, instant = false) { + if (!siteActive()) { setPreset('terrace', instant); return; } + mode = mode || (site.parcel ? 'parcel' : site.road ? 'road' : 'parking'); + site.lastFocus = mode; + document.querySelectorAll('[data-preset]').forEach(b => b.classList.remove('active')); + $('#elevation').value = ''; + const label = { road: 'Cesta a přístup k domu', parcel: 'Celý pozemek', parking: 'Parkování u příjezdu' }[mode]; + setCamera(fittedSiteCamera(mode), label, instant); + } + function setSiteLayer(key, on, focus = true) { + if (!['road', 'parcel', 'parking'].includes(key)) return; + const was = siteActive(); + if (!was && on) site.savedCamera = { theta, phi, distance, target: [...target], ortho: orthographic, label: $('#directionText').textContent }; + site[key] = !!on; + const ids = { road: 'siteRoad', parcel: 'siteParcel', parking: 'siteParking' }; + const input = $('#' + ids[key]); input.checked = site[key]; + input.closest('label').classList.toggle('selected', site[key]); + document.body.classList.toggle('site-active', siteActive()); + $('#siteMeta').hidden = !siteActive(); + $('#siteMeta').textContent = 'C.3 + výškopis · mezi body interpolováno'; + updateSolar(); + if (focus) { + if (siteActive()) focusSite(on ? key : null); + else if (site.savedCamera) { + setCamera(site.savedCamera, site.savedCamera.label || 'Dům'); + site.savedCamera = null; site.lastFocus = null; + } + } + dirty = true; + } + function updateSiteLight() { + if(interior.enabled){ + const center=[1,2,0],lightEye=vadd(center,scale(solar.light,45)),view=lookAt(lightEye,center,Math.abs(solar.light[1])>.999?[0,0,1]:[0,1,0]); + const cs=boxCorners([-8,-.6,-5],[10,5.9,5]).map(p=>point(view,p)),xs=cs.map(p=>p[0]),ys=cs.map(p=>p[1]); + lightVP=mul(ortho(Math.min(...xs)-2,Math.max(...xs)+2,Math.min(...ys)-2,Math.max(...ys)+2,1,100),view);return; + } + if (!siteActive()) return; + const { lo, hi } = visibleBounds(), center = lo.map((v, i) => (v + hi[i]) / 2); + const lightEye = vadd(center, scale(solar.light, 115)); + const view = lookAt(lightEye, center, Math.abs(solar.light[1]) > .999 ? [0, 0, 1] : [0, 1, 0]); + const cs = boxCorners(lo, hi).map(p => point(view, p)); + const xs = cs.map(p => p[0]), ys = cs.map(p => p[1]); + lightVP = mul(ortho(Math.min(...xs) - 3, Math.max(...xs) + 3, Math.min(...ys) - 3, Math.max(...ys) + 3, 1, 240), view); + } + function exportVisibleGltf(){ + const selected=new Set(meshes.filter(visible).map(m=>m.nodeIndex)); + const keep=new Set();function retain(i){const n=gltf.nodes[i];const children=(n.children||[]).filter(retain);const yes=selected.has(i)||children.length>0;if(yes)keep.add(i);return yes;} + for(const sc of gltf.scenes||[])for(const i of sc.nodes||[])retain(i); + const nodes=gltf.nodes.map((n,i)=>({...n,...(n.children?{children:n.children.filter(c=>keep.has(c))}:{})})); + const scenes=(gltf.scenes||[]).map(sc=>({...sc,nodes:(sc.nodes||[]).filter(i=>keep.has(i))})); + return {...gltf,nodes,scenes,extras:{...gltf.extras,visibleSiteLayers:{road:site.road,parcel:site.parcel,parking:site.parking,terrain:showTerrain},allstavSelection:stdClone(stdState),interiorView:{enabled:interior.enabled,mode:interior.mode}}}; +} + + function initSiteControls() { + for (const [id, key] of [['siteRoad', 'road'], ['siteParcel', 'parcel'], ['siteParking', 'parking']]) { + const input = $('#' + id); input.onchange = () => setSiteLayer(key, input.checked); + setSiteLayer(key, true, false); + } + $('#siteFit').onclick = () => focusSite(); + window.SITE_SIM = { + getState: () => ({ + road: site.road, parcel: site.parcel, parking: site.parking, terrain: showTerrain, + visibleObjects: meshes.filter(visible).length, referenceElevation: 591.840, + projectArea: 1010, digitizedArea: 987.89, houseGeometryUnchanged: true + }), + setLayer: (key, on, focus = true) => setSiteLayer(key, on, focus), + focus: mode => focusSite(mode), + reset: () => { for (const key of ['road', 'parcel', 'parking']) setSiteLayer(key, false, false); setPreset('terrace'); }, + getCamera: () => ({ theta, phi, distance, target: [...target], aspect }), + checkGround: () => gltf.extras.siteStudy.finishedGroundC3 + }; + } + + // Optional additive interior, independent of the retained exterior and solar state. + const interior = { + enabled: false, mode: 'axon', furniture: true, labels: true, fill: true, + position: [3.78, 1.62, .90], yaw: 0, pitch: 0, fov: 64, + saved: null, data: null, lookPointer: null, keys: new Set(), pad: new Set(), + panel: true, userCamera: false, lastView: 'kitchen', currentRoom: '', + roof: new Set(), cutOriginals: new Set(), ready: false + }; + function interiorVisibility(m) { + const layer = m.interiorLayer; + if (!interior.enabled) return !layer; + const walking = interior.mode === 'walk'; + if (layer) { + if (layer === 'cut' || layer === 'cutShell') return !walking; + if (['wall', 'liner', 'door', 'ceiling', 'beams'].includes(layer)) return walking; + if (layer === 'furniture') return interior.furniture; + return true; + } + if (!walking) { + if (interior.roof.has(m.nodeIndex) || interior.cutOriginals.has(m.nodeIndex)) return false; + if (m.siteLayer || m.context || /^(25_|26_|27_)/.test(m.name)) return false; + } else if (m.glass) return false; // Optical opening only; source glazing is untouched. + return true; + } + function interiorPointIn(poly, x, z) { + let inside = false; + for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { + const a = poly[i], b = poly[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; + } + function interiorCanStand(x, z) { + const d = interior.data, r = .16; + if (!d || ![[x-r,z],[x+r,z],[x,z-r],[x,z+r]].every(p => interiorPointIn(d.footprint,p[0],p[1]))) return false; + for (const b of d.wallColliders) if (x > b.lo[0]-r && x < b.hi[0]+r && z > b.lo[1]-r && z < b.hi[1]+r) return false; + if (interior.furniture) for (const b of d.furnitureColliders) if (x > b.lo[0]-r && x < b.hi[0]+r && z > b.lo[1]-r && z < b.hi[1]+r) return false; + return true; + } + function moveInterior(dx, dz) { + // Small substeps and axis sliding keep narrow 700 mm doorways usable. + const steps = Math.max(1, Math.ceil(Math.hypot(dx,dz)/.055)); + for (let i = 0; i < steps; i++) { + let x = interior.position[0], z = interior.position[2]; + if (interiorCanStand(x+dx/steps,z)) interior.position[0] = x+dx/steps; + x = interior.position[0]; + if (interiorCanStand(x,z+dz/steps)) interior.position[2] = z+dz/steps; + } + dirty = true; + } + function stepInterior(dt) { + if (!interior.enabled || interior.mode !== 'walk') return; + const k = interior.keys, p = interior.pad; + let f = (k.has('KeyW')||k.has('ArrowUp')||p.has('forward')?1:0)-(k.has('KeyS')||k.has('ArrowDown')||p.has('back')?1:0); + let side = (k.has('KeyD')||p.has('right')?1:0)-(k.has('KeyA')||p.has('left')?1:0); + const turn = (k.has('ArrowLeft')||k.has('KeyQ')?1:0)-(k.has('ArrowRight')||k.has('KeyE')?1:0); + if (turn) {interior.yaw += turn*dt*1.25;dirty=true;} + if (f || side) { + const len = Math.hypot(f,side), speed = (k.has('ShiftLeft')||k.has('ShiftRight')?2.15:1.2)*dt; + f/=len;side/=len; + moveInterior((Math.sin(interior.yaw)*f-Math.cos(interior.yaw)*side)*speed,(Math.cos(interior.yaw)*f+Math.sin(interior.yaw)*side)*speed); + } + } + function interiorCamera() { + eye = [...interior.position]; + const dir = [Math.sin(interior.yaw)*Math.cos(interior.pitch),Math.sin(interior.pitch),Math.cos(interior.yaw)*Math.cos(interior.pitch)]; + vp = mul(perspective(interior.fov*Math.PI/180,aspect,.035,400),lookAt(eye,vadd(eye,dir))); + } + function interiorFitCamera() { + if (!interior.enabled || interior.mode === 'walk' || !interior.data) return; + const mobile = aspect < 1; + theta = interior.mode === 'plan' ? (mobile ? Math.PI/2 : 0) : .68; + phi = interior.mode === 'plan' ? 1.5706 : .93; + target = [1.6,.4,0]; orthographic = true;animation=null;autoRotate=false; + const forward = [Math.sin(theta)*Math.cos(phi),Math.sin(phi),Math.cos(theta)*Math.cos(phi)]; + const right = [Math.cos(theta),0,-Math.sin(theta)], up = cross(forward,right); + distance = 10; + for (const p of boxCorners([-8.05,-.2,-5.0],[11.68,2.3,5.05])) { + const q = vsub(p,target); + distance = Math.max(distance,Math.abs(dot(q,right))/(.28*aspect*.89),Math.abs(dot(q,up))/(.28*.76)); + } + dirty = true; + } + function updateInteriorInterface() { + document.body.classList.toggle('interior-active',interior.enabled); + document.body.classList.toggle('interior-walk',interior.enabled&&interior.mode==='walk'); + document.body.classList.toggle('interior-panel-closed',interior.enabled&&!interior.panel); + $('#interiorLaunch').setAttribute('aria-expanded',String(interior.enabled)); + $('#interiorPanelToggle').setAttribute('aria-expanded',String(interior.panel)); + document.querySelectorAll('[data-interior-mode]').forEach(b=>b.classList.toggle('active',b.dataset.interiorMode===interior.mode)); + const walking=interior.mode==='walk'; + $('#interiorRoomLabels').disabled=walking; + $('#interiorHint').textContent=walking?'W / A / S / D nebo šipky · tažením se rozhlížejte · Shift = rychleji · Esc = exteriér':'Tažením otáčejte · kolečkem přibližujte · kliknutím na místnost vstupte dovnitř'; + if(!walking)$('#interiorViewTitle').textContent=interior.mode==='plan'?'Půdorys 1. NP':'Dům bez střechy'; + if(!interior.enabled)$('#interiorLabels').replaceChildren(); + } + function toggleInterior(on) { + if (!interior.ready) return; + on = on === undefined ? !interior.enabled : !!on; + if (on === interior.enabled) return; + if (on) { + interior.saved = {theta,phi,distance,target:[...target],ortho:orthographic,autoRotate,aspect, + label:$('#directionText').textContent,sunOpen:$('#sunPanel').classList.contains('open'),panelOpen:$('#panel').classList.contains('open'), + presets:[...document.querySelectorAll('[data-preset].active')].map(b=>b.dataset.preset),elevation:$('#elevation').value}; + interior.enabled=true;interior.mode='axon';interior.panel=true;interior.userCamera=false; + toggleSunPanel(false);$('#panel').classList.remove('open');$('#infoButton').classList.remove('active'); + autoRotate=false;animation=null;pointerMap.clear();$('#rotateButton').classList.remove('active'); + updateInteriorInterface();resize();interiorFitCamera(); + toast('Interiér zapnutý. Klikněte na místnost nebo zvolte Procházet.'); + } else { + const s=interior.saved; + interior.enabled=false;interior.keys.clear();interior.pad.clear();interior.lookPointer=null; + updateInteriorInterface();toggleSunPanel(s?.sunOpen||false);resize(); + if(s){ + setCamera({...s,distance:s.distance*Math.max(1,1/aspect)/Math.max(1,1/s.aspect)},s.label,true); + autoRotate=s.autoRotate;$('#rotateButton').classList.toggle('active',autoRotate); + $('#panel').classList.toggle('open',s.panelOpen);$('#infoButton').classList.toggle('active',s.panelOpen); + $('#elevation').value=s.elevation; + document.querySelectorAll('[data-preset]').forEach(b=>b.classList.toggle('active',s.presets.includes(b.dataset.preset))); + } + } + updateSolar();solar.shadowDirty=true;dirty=true; + } + function setInteriorMode(mode) { + if(!['plan','axon','walk'].includes(mode)) return; + if(!interior.enabled)toggleInterior(true); + interior.mode=mode;interior.userCamera=false;interior.keys.clear();interior.pad.clear();animation=null;autoRotate=false;pointerMap.clear(); + if(mode==='walk'){ + const preset=interior.data.presets.find(p=>p.id===interior.lastView)||interior.data.presets[0]; + enterInteriorView(preset.id);return; + } + document.querySelectorAll('[data-interior-view]').forEach(b=>b.classList.remove('active')); + $('#interiorRoom').value='';updateInteriorInterface();interiorFitCamera();updateSolar();dirty=true; + } + function enterInteriorView(id) { + if(!interior.ready)return false; + if(!interior.enabled)toggleInterior(true); + const v=interior.data.presets.find(p=>p.id===id)||interior.data.rooms.find(r=>r.id===id); + if(!v)return false; + interior.mode='walk';interior.lastView=id;interior.position=[...v.eye]; + const dir=vsub(v.look,v.eye);interior.yaw=Math.atan2(dir[0],dir[2]);interior.pitch=Math.atan2(dir[1],Math.hypot(dir[0],dir[2])); + interior.fov=v.fov||64;interior.keys.clear();interior.pad.clear();interior.lookPointer=null;pointerMap.clear();autoRotate=false;animation=null; + document.querySelectorAll('[data-interior-view]').forEach(b=>b.classList.toggle('active',b.dataset.interiorView===id)); + $('#interiorRoom').value=interior.data.rooms.some(r=>r.id===id)?id:''; + $('#interiorViewTitle').textContent=v.label; + updateInteriorInterface();updateSolar();solar.shadowDirty=true;dirty=true;return true; + } + function handleInteriorKey(e) { + if($('#interiorReferenceDialog').open||$('#stdCatalogDialog').open||$('#stdResetDialog').open)return; + if(e.key==='Escape'){e.preventDefault();toggleInterior(false);return;} + if(/^(INPUT|SELECT|TEXTAREA)$/.test(e.target.tagName))return; + const modes={Digit1:'plan',Digit2:'axon',Digit3:'walk'}; + if(modes[e.code]){e.preventDefault();setInteriorMode(modes[e.code]);return;} + if(e.code==='KeyR'){e.preventDefault();if(interior.mode==='walk')enterInteriorView(interior.lastView);else{interior.userCamera=false;interiorFitCamera();}return;} + if(interior.mode==='walk'&&['KeyW','KeyA','KeyS','KeyD','KeyQ','KeyE','ArrowUp','ArrowDown','ArrowLeft','ArrowRight','ShiftLeft','ShiftRight'].includes(e.code)){ + e.preventDefault();interior.keys.add(e.code); + } + } + function interiorMapPoint(x,z){return [17+(x+7.67)*19.2,12+(z+4.695)*19.2];} + function buildInteriorMap() { + let svg=''; + svg+=''; + svg+='Terasa'; + for(const r of interior.data.rooms){ + const path=r.poly.map(p=>interiorMapPoint(...p).join(',')).join(' '),a=interiorMapPoint(...r.anchor); + svg+=`${r.id}`; + } + for(const b of interior.data.wallColliders){let a=interiorMapPoint(...b.lo),c=interiorMapPoint(...b.hi);svg+=``;} + svg+=''; + svg+='1. NP · orientace podle půdorysu'; + $('#interiorMap').innerHTML=svg; + document.querySelectorAll('[data-map-room]').forEach(el=>{ + el.onclick=()=>enterInteriorView(el.dataset.mapRoom); + el.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();enterInteriorView(el.dataset.mapRoom);}}; + }); + } + function drawInteriorLabels() { + if(!interior.enabled)return; + const el=$('#interiorLabels'),walking=interior.mode==='walk'; + if(walking||!interior.labels){el.replaceChildren();} + else{ + if(!el.children.length)for(const r of interior.data.rooms){ + const b=document.createElement('button');b.className='int-room-tag';b.dataset.room=r.id; + b.innerHTML=`${r.id} ${r.label}${r.area} m²`; + b.title='Vstoupit: '+r.label;b.onclick=()=>enterInteriorView(r.id);el.appendChild(b); + } + const rect=canvas.getBoundingClientRect(); + for(const b of el.children){const r=interior.data.rooms.find(r=>r.id===b.dataset.room),p=project([r.anchor[0],.20,r.anchor[1]]); + b.style.left=(p[0]+rect.left)+'px';b.style.top=(p[1]+rect.top)+'px'; + b.style.display=p[0]<20||p[0]>viewW-20||p[1]<80||p[1]>viewH-65?'none':'block';} + } + const marker=$('#interiorMapMarker');if(marker){marker.style.display=walking?'block':'none';if(walking){const p=interiorMapPoint(interior.position[0],interior.position[2]);marker.setAttribute('transform',`translate(${p[0]},${p[1]}) rotate(${-interior.yaw*180/Math.PI})`);}} + if(walking){const room=interior.data.rooms.find(r=>interiorPointIn(r.poly,interior.position[0],interior.position[2])); + const id=room?.id||'';if(id!==interior.currentRoom){interior.currentRoom=id;document.querySelectorAll('[data-map-room] .map-room').forEach(p=>p.classList.toggle('active',p.parentElement.dataset.mapRoom===id));}} + } + // Interior paint uses linear RGB, like the other interior PBR materials. + // Only explicitly tagged plaster is edited; no name-based facade match. + const INTERIOR_PAINT_STORAGE = 'locenice.interior.wallPaint.v1'; + function normalizeInteriorHex(value) { + if (typeof value !== 'string') return null; + let hex = value.trim().replace(/^#/, ''); + if (/^[0-9a-f]{3}$/i.test(hex)) hex = hex.split('').map(c=>c+c).join(''); + return /^[0-9a-f]{6}$/i.test(hex) ? '#'+hex.toLowerCase() : null; + } + function setInteriorWallColor(value, persist = true) { + const hex=normalizeInteriorHex(value), paint=interior.data?.wallPaint; + if (!hex || !gltf || !paint) return false; + const rgb=hardieLinearRGB(hex); + for (const index of paint.materialIndices) { + const material=gltf.materials[index]; + if (material?.extras?.interiorRole !== 'wallPaint') continue; + material.pbrMetallicRoughness.baseColorFactor=[...rgb,1]; + } + interior.wallColor=hex; paint.selectedHex=hex; + $('#interiorWallColor').value=hex; + $('#interiorWallHex').value=hex.toUpperCase(); + $('#interiorWallHex').removeAttribute('aria-invalid'); + document.querySelectorAll('[data-interior-paint]').forEach(button=>{ + button.setAttribute('aria-pressed',String(button.dataset.interiorPaint===hex)); + }); + // file://, private mode and disabled storage must never break rendering. + if (persist) try { + if (hex===paint.defaultHex) localStorage.removeItem(INTERIOR_PAINT_STORAGE); + else localStorage.setItem(INTERIOR_PAINT_STORAGE,hex); + } catch (_) { /* The live view and export still keep the selected color. */ } + dirty=true; + return true; + } + function resetInteriorWallColor() { + return setInteriorWallColor(interior.data?.wallPaint?.defaultHex || '#eeeae1'); + } + function initInteriorWallPaint() { + const paint=interior.data.wallPaint; + let color=paint.selectedHex || paint.defaultHex; + try { color=normalizeInteriorHex(localStorage.getItem(INTERIOR_PAINT_STORAGE)) || color; } catch (_) {} + setInteriorWallColor(color,false); + $('#interiorWallColor').addEventListener('input',e=>setInteriorWallColor(e.target.value)); + $('#interiorWallColor').addEventListener('change',e=>setInteriorWallColor(e.target.value)); + const hexInput=$('#interiorWallHex'); + const commitHex=()=>{ + if (!setInteriorWallColor(hexInput.value)) { + hexInput.value=interior.wallColor.toUpperCase(); + toast('Zadejte platnou barvu, nap\u0159\u00edklad #EEEAE1.'); + } + }; + hexInput.addEventListener('change',commitHex); + hexInput.addEventListener('keydown',e=>{ + if (e.key==='Enter') { e.preventDefault();commitHex();hexInput.blur(); } + }); + document.querySelectorAll('[data-interior-paint]').forEach(button=>{ + button.addEventListener('click',()=>setInteriorWallColor(button.dataset.interiorPaint)); + }); + $('#interiorWallReset').addEventListener('click',resetInteriorWallColor); + } + + function initInterior() { + interior.data=gltf.extras.interiorStudy;interior.roof=new Set(interior.data.roofNodes);interior.cutOriginals=new Set(interior.data.cutOriginalNodes); + for(const r of interior.data.rooms){const op=document.createElement('option');op.value=r.id;op.textContent=`${r.id} · ${r.label} · ${r.area} m²`;$('#interiorRoom').appendChild(op);} + buildInteriorMap(); + initInteriorWallPaint(); + $('#interiorLaunch').onclick=()=>toggleInterior(true);$('#interiorExit').onclick=()=>toggleInterior(false); + document.querySelectorAll('[data-interior-mode]').forEach(b=>b.onclick=()=>setInteriorMode(b.dataset.interiorMode)); + document.querySelectorAll('[data-interior-view]').forEach(b=>b.onclick=()=>enterInteriorView(b.dataset.interiorView)); + $('#interiorRoom').onchange=e=>{if(e.target.value)enterInteriorView(e.target.value);}; + $('#interiorFurniture').onchange=e=>{interior.furniture=e.target.checked;solar.shadowDirty=true;dirty=true;}; + $('#interiorRoomLabels').onchange=e=>{interior.labels=e.target.checked;dirty=true;}; + $('#interiorFill').onchange=e=>{interior.fill=e.target.checked;dirty=true;}; + $('#interiorFit').onclick=()=>{if(interior.mode==='walk')enterInteriorView(interior.lastView);else{interior.userCamera=false;interiorFitCamera();}}; + $('#interiorPanelToggle').onclick=()=>{if($('#sunPanel').classList.contains('open'))toggleSunPanel(false);else interior.panel=!interior.panel;updateInteriorInterface();resize();}; + $('#interiorSavePng').onclick=()=>{render();canvas.toBlob(b=>{if(b)saveBlob(b,'dum_interier_'+interior.mode+'.png');},'image/png');}; + $('#interiorSaveGlb').onclick=()=>{saveBlob(new Blob([currentGlb()],{type:'model/gltf-binary'}),'dum_interier_'+interior.mode+'_'+hardieSelected+'.glb');}; + const refs=resources.references; + document.querySelectorAll('[data-int-ref]').forEach(b=>b.onclick=()=>{const r=refs[b.dataset.intRef];$('#interiorReferenceTitle').textContent=r.title;$('#interiorReferenceImage').src=r.src;$('#interiorReferenceImage').alt=r.title;$('#interiorReferenceCaption').textContent=r.caption;$('#interiorReferenceDialog').showModal();}); + $('#interiorReferenceClose').onclick=()=>$('#interiorReferenceDialog').close(); + $('#interiorReferenceDialog').addEventListener('click',e=>{if(e.target.id==='interiorReferenceDialog'){const r=e.target.getBoundingClientRect();if(e.clientXr.right||e.clientYr.bottom)e.target.close();}}); + document.querySelectorAll('[data-walk]').forEach(b=>{ + const release=()=>{interior.pad.delete(b.dataset.walk);b.classList.remove('held');}; + b.onpointerdown=e=>{e.preventDefault();b.setPointerCapture(e.pointerId);interior.pad.add(b.dataset.walk);b.classList.add('held');}; + b.onpointerup=release;b.onpointercancel=release;b.onlostpointercapture=release; + }); + window.addEventListener('keyup',e=>interior.keys.delete(e.code)); + window.addEventListener('blur',()=>{interior.keys.clear();interior.pad.clear();interior.lookPointer=null;}); + document.addEventListener('visibilitychange',()=>{if(document.hidden){interior.keys.clear();interior.pad.clear();}}); + interior.ready=true;$('#interiorLaunch').disabled=false; + window.INTERIOR = { + toggle:toggleInterior,mode:setInteriorMode,view:enterInteriorView, + setWallColor:setInteriorWallColor, resetWallColor:resetInteriorWallColor, + getState:()=>({enabled:interior.enabled,mode:interior.mode,position:[...interior.position],yaw:interior.yaw,pitch:interior.pitch, + furniture:interior.furniture,labels:interior.labels,fill:interior.fill,currentRoom:interior.currentRoom, + wallColor:interior.wallColor,defaultWallColor:interior.data.wallPaint.defaultHex,timberReference:interior.data.timberReference, + rooms:interior.data.rooms.map(r=>({id:r.id,name:r.label,area:r.area})),originalGeometryUnchanged:false,standardsRevision:1,userCorrections:interior.data.userCorrections, + originalNodes:interior.data.originalNodeCount,sourceBinarySHA256:interior.data.sourceBinarySHA256}), + checkPosition:(x,z)=>interiorCanStand(x,z),move:(dx,dz)=>moveInterior(dx,dz), + setLook:(yaw,pitch)=>{interior.yaw=yaw;interior.pitch=clamp(pitch,-1.3,1.3);dirty=true;} + }; + } + + + // ALLSTAV configurator. The PDF is the source; unlabeled samples are not product codes. + const STD_ASSETS=resources.assets; + const STD_STORAGE = 'locenice.allstav.2025.v1'; + let stdStorageKey = STD_STORAGE; + const STD_ROOMS=resources.catalog.rooms,STD_OPTIONS=resources.catalog.options,STD_DEFAULTS=resources.catalog.defaults; + const stdClone = x=>JSON.parse(JSON.stringify(x)); + let stdState=stdClone(STD_DEFAULTS),stdReady=false,stdSwitching=false,stdTab='exterior',stdFloorRoom='108',stdTileRoom='105',stdCatalogPage=12,stdSaveTimer=0; + const stdEsc = s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + const stdOption=(key,val=stdState[key])=>(STD_OPTIONS[key]||[]).find(o=>o[0]===val); + const stdLabel=(key,val=stdState[key])=>stdOption(key,val)?.[1]||String(val); + const stdSource=p=>``; + function stdCards(key,options=STD_OPTIONS[key],cols='',kind='',tier='standard'){ + return `
${options.map(o=>``).join('')}
`; + } + function stdSampleCards(key,type){ + return stdCards(key,Array.from({length:9},(_,i)=>[String(i+1),`Vzorek ${String(i+1).padStart(2,'0')}`,'',false,`${type}${i+1}`]),'cols3','wood','catalog'); + } + const stdSection=(id,n,title,body,open=false)=>`
${n}${title}
${body}
`; + const stdSelect=(key,label,options)=>``; + const stdCheck=(key,label)=>``; + const stdColor=(key,label)=>``; + const stdNotice=(s,t='')=>`
${s}
`; + function standardsVisible(m){ + const e=m.std||{}; + if(e.stdHidden)return false; + if(e.stdFullOnly && interior.enabled && interior.mode!=='walk')return false; + if(e.stdBlind && (!stdState.blinds || (e.stdBlindFraction!==undefined && e.stdBlindFraction>stdState.blindDrop/100)))return false; + if(e.stdWhen && Object.entries(e.stdWhen).some(([k,v])=>stdState[k]!==v))return false; + return true; + } + function stdSetMaterial(role,color,texture){ + const meta=gltf.extras.allstavStandards; + for(const idx of meta.materials[role]||[]){ + const mat=gltf.materials[idx],p=mat.pbrMetallicRoughness; + if(color)p.baseColorFactor=[...hardieLinearRGB(color),p.baseColorFactor?.[3]??1]; + if(texture!==undefined){if(texture===null)delete p.baseColorTexture;else p.baseColorTexture={index:typeof texture==='number'?texture:meta.textures[texture]};} + for(const m of meshes){if(m.material!==idx)continue;m.surfaceKind=p.baseColorTexture?3:2;} + } + } + function stdApply(save=true){ + if(!gltf)return; + stdSetMaterial('entry',stdOption('entryColor')[2]);stdSetMaterial('extSill',stdOption('extSill')[2]);stdSetMaterial('gutter',stdOption('gutter')[2]); + recolor('roof',stdOption('roof')[2]);for(const i of gltf.extras.allstavStandards.materials.roof)gltf.materials[i].pbrMetallicRoughness.roughnessFactor=.32;if($('#roofColor'))$('#roofColor').value=stdOption('roof')[2]; + stdSetMaterial('blinds',stdOption('blindColor')[2]); + for(const r of STD_ROOMS){const f=stdState.floors[r[0]];stdSetMaterial('floor'+r[0],'#ffffff',f.type+f.sample);for(const i of gltf.extras.allstavStandards.materials['floor'+r[0]]){gltf.materials[i].pbrMetallicRoughness.baseColorTexture.extensions={KHR_texture_transform:{scale:f.type==='tile'?[.22/.6,1.28/.6]:[1,1]}};}} + gltf.extensionsUsed=[...new Set([...(gltf.extensionsUsed||[]),'KHR_texture_transform'])]; + stdSetMaterial('wallTile105','#ffffff','tile'+stdState.wallTile105);stdSetMaterial('wallTile106','#ffffff','tile'+stdState.wallTile106); + stdSetMaterial('intSill',stdOption('intSill')[2]); + stdSetMaterial('intDoor',stdState.intDoor==='sample'?'#ffffff':stdState.intDoorColor,stdState.intDoor==='sample'?'prum':null); + setInteriorWallColor(stdState.wallPaint,false); + stdSetMaterial('kitchenFront',stdState.kitchenColor);stdSetMaterial('kitchenBody',stdState.kitchenBodyColor); + const work=stdOption('worktop'),tex=stdState.worktop==='oak'?9:stdState.worktop==='stone'?'tile1':null; + stdSetMaterial('worktop',tex!==null?'#ffffff':work[2],tex); + const back=stdState.backsplash==='worktop'?stdState.worktop:stdState.backsplash; + stdSetMaterial('backsplash',back==='stone'||back==='oak'?'#ffffff':'#eeeeda',back==='stone'?'tile1':back==='oak'?9:null); + if(stdState.backsplash==='worktop'&&back==='graphite')stdSetMaterial('backsplash','#444946',null); + stdSetMaterial('faucet',stdOption('faucet')[2]); + for(const i of gltf.extras.allstavStandards.materials.faucet){gltf.materials[i].pbrMetallicRoughness.metallicFactor=stdState.faucet==='chrome'?.8:.05;} + gltf.extras.allstavSelection=stdClone(stdState); + solar.shadowDirty=true;dirty=true; + if(stdReady){stdSync();if(save){clearTimeout(stdSaveTimer);stdSaveTimer=setTimeout(stdPersist,220);}} + } + function stdNormalize(input){ + if(!input||typeof input!=='object'||Array.isArray(input))throw Error('Chyb\u00ed platn\u00e9 nastaven\u00ed.'); + const d=stdClone(STD_DEFAULTS),enums={intDoor:['sample','custom'],intDoorType:['hinged','sliding','pocket'],tileCorner:['profile','miter'],hood:['facade','recirculation'],ventilation:['local','central','none'],radiator:['electric','combined']}; + for(const k of Object.keys(d)){ + if(input[k]===undefined)continue; + const v=input[k]; + if(k==='floors'){ + if(!v||typeof v!=='object')throw Error('Neplatn\u00e9 podlahy.'); + for(const r of STD_ROOMS){if(v[r[0]]===undefined)continue;const f=v[r[0]];if(!f||!['lam','tile'].includes(f.type)||!Number.isInteger(f.sample)||f.sample<1||f.sample>9)throw Error('Neplatn\u00e1 podlaha m\u00edstnosti '+r[0]);d.floors[r[0]]={type:f.type,sample:f.sample};} + } else if(['wallPaint','kitchenColor','kitchenBodyColor','intDoorColor'].includes(k)){ + if(typeof v!=='string'||!/^#[0-9a-f]{6}$/i.test(v))throw Error('Neplatn\u00e1 barva: '+k);d[k]=v.toLowerCase(); + } else if(STD_OPTIONS[k]){if(!STD_OPTIONS[k].some(o=>o[0]===v))throw Error('Nezn\u00e1m\u00e1 varianta: '+k);d[k]=v; + } else if(enums[k]){if(!enums[k].includes(v))throw Error('Nezn\u00e1m\u00e1 varianta: '+k);d[k]=v; + } else if(typeof d[k]==='boolean'){if(typeof v!=='boolean')throw Error('Neplatn\u00fd p\u0159ep\u00edna\u010d: '+k);d[k]=v; + } else if(typeof d[k]==='number'){const max=k==='blindDrop'?100:9,min=k==='blindDrop'?0:1;if(!Number.isInteger(v)||vmax)throw Error('Hodnota mimo rozsah: '+k);d[k]=v; + } else if(typeof d[k]==='string'){if(typeof v!=='string'||v.length>(k==='note'?2000:120))throw Error('Neplatn\u00fd text: '+k);d[k]=v;} + } + // Backwards compatibility: older exports used one colour for both. + if(input.kitchenBodyColor===undefined)d.kitchenBodyColor=d.kitchenColor; + return d; + } + function stdPersist(){try{localStorage.setItem(stdStorageKey,JSON.stringify(stdState));$('#stdSaved').textContent='Ulo\u017eeno v tomto prohl\u00ed\u017ee\u010di \u00b7 z\u00e1lohu st\u00e1hnete jako JSON';}catch(e){$('#stdSaved').textContent='Prohl\u00ed\u017ee\u010d nepovolil ulo\u017een\u00ed. St\u00e1hn\u011bte JSON.';}} + function stdBuildPanel(){ + let ext='
'; + ext+=stdSection('entry','01','Vchodov\u00e9 dve\u0159e',`
Tvar k\u0159\u00eddla ${stdSource(12)}
${stdCards('entryModel',undefined,'','door')}
Odst\u00edn dve\u0159\u00ed
${stdCards('entryColor')}${stdNotice('V\u00fdchoz\u00ed: 3 skla, RAL 7016. Prosklen\u00e9 proveden\u00ed je v katalogu p\u0159\u00edplatkov\u00e9. N\u00e1zvy tvar\u016f jsou popisn\u00e9; modelov\u00e9 k\u00f3dy katalog neuv\u00e1d\u00ed.','amber')}

Masivn\u00ed d\u0159ev\u011bn\u00e9 dve\u0159e KeraT\u00fcr. 3D tvar je ilustrativn\u00ed; stavebn\u00ed otvor a bo\u010dn\u00ed sv\u011btl\u00edk p\u016fvodn\u00edho modelu z\u016fst\u00e1vaj\u00ed.

`,true); + ext+=stdSection('extSill','02','Venkovn\u00ed parapety',`
Ta\u017een\u00fd hlin\u00edk ${stdSource(13)}
${stdCards('extSill',undefined,'cols3')}

Volba m\u011bn\u00ed parapety. N\u00e1\u0161lapn\u00e9 prahy u terasov\u00fdch dve\u0159\u00ed z\u016fst\u00e1vaj\u00ed samostatn\u00e9.

`); + ext+=stdSection('gutter','03','Okapy a svody',`
Lindab ${stdSource(13)}
${stdCards('gutter')}${stdNotice('M\u011bn\u00ed se jen okapov\u00fd syst\u00e9m, nikoli lemov\u00e1n\u00ed st\u0159echy, kom\u00edn nebo z\u00e1bradl\u00ed.')}

Zkratky a k\u00f3dy jsou p\u0159evzat\u00e9 z katalogu. CERN 015 = v\u00fdchoz\u00ed volba.

`); + ext+=stdSection('roof','04','St\u0159e\u0161n\u00ed ta\u0161ky',`
Benders Palema Candor ${stdSource(14)}
${stdCards('roof',undefined,'cols3')}

Betonov\u00e1 ta\u0161ka, vysoce leskl\u00e1 povrchov\u00e1 \u00faprava. N\u00e1hled zachov\u00e1v\u00e1 geometrii krytiny p\u016fvodn\u00edho modelu.

`); + ext+=stdSection('blinds','05','Venkovn\u00ed \u017ealuzie',`
Climax Zetta 70 \u00b7 p\u0159\u00edplatek ${stdSource(15)}
${stdCards('blindColor',undefined,'cols5','', 'catalog')}
Sta\u017een\u00ed lamel100 %
${stdNotice('9 obd\u00e9ln\u00edkov\u00fdch otvor\u016f. Troj\u00faheln\u00edkov\u00e9 okno ani vchodov\u00e9 dve\u0159e \u017ealuzie nemaj\u00ed. Vodic\u00ed li\u0161ty maj\u00ed shodnou barvu; \u0161irok\u00e9 ji\u017en\u00ed zasklen\u00ed je rozd\u011blen\u00e9.')}

Podom\u00edtkov\u00fd kastl\u00edk, elektrick\u00e9 ovl\u00e1d\u00e1n\u00ed. Sta\u017een\u00ed je spole\u010dn\u00fd ilustrativn\u00ed n\u00e1hled, ne n\u00e1vrh pohonu.

`); + ext+=stdNotice('Fas\u00e1da James Hardie Plank a okol\u00ed domu z\u016fst\u00e1vaj\u00ed v p\u016fvodn\u00edch ovl\u00e1dac\u00edch prvc\u00edch. '); + $('#std-exterior').innerHTML=ext; + let int='
'; + int+=stdSection('floors','01','Podlahy po m\u00edstnostech',`
KronoSwiss / keramick\u00e1 dla\u017eba ${stdSource(18)}

Vzorky 01\u201309 odpov\u00eddaj\u00ed obr\u00e1zk\u016fm na str. 18, nikoli objednac\u00edm k\u00f3d\u016fm. M\u011b\u0159\u00edtko kresby a sp\u00e1ry jsou pouze n\u00e1hledov\u00e9.

`,true); + int+=stdSection('tiles','02','Obklady koupelny a WC',`
Rektifikovan\u00e9 obklady ${stdSource(18)}
${stdSampleCards('wallTile','tile')}${stdSelect('tileCorner','Zakon\u010den\u00ed hran \u00b7 specifikace',[['profile','Hlin\u00edkov\u00e1 eloxov\u00e1 li\u0161ta \u00b7 standard'],['miter','Kamenick\u00fd roh \u00b7 p\u0159\u00edplatek']])}

Obklad do v\u00fd\u0161ky 2,1 m dle technick\u00e9 zpr\u00e1vy. Ve 3D \u0159ezu je zobrazen pouze po v\u00fd\u0161ku \u0159ezu; ukon\u010den\u00ed hran se eviduje v soupisu.

`); + int+=stdSection('doors','03','Interi\u00e9rov\u00e9 dve\u0159e a kov\u00e1n\u00ed',`
Pr\u00fcm CPL ${stdSource(19)}
${stdCards('intDoor',[['sample','Fotografie z katalogu','',false,'prum'],['custom','Vlastn\u00ed odst\u00edn','#c3aa85']], 'cols2','wood','catalog')}
${stdColor('intDoorColor','Vlastn\u00ed barva \u00b7 pouze vizualizace')}
${stdNotice('Katalog uv\u00e1d\u00ed v\u00edce ne\u017e 20 CPL dekor\u016f, ale jejich vzorn\u00edk a k\u00f3dy neobsahuje. Fotografie dve\u0159\u00ed je referen\u010dn\u00ed, nikoli p\u0159esn\u00fd dekor.','amber')}${stdSelect('intDoorType','Proveden\u00ed \u00b7 specifikace',[['hinged','Oto\u010dn\u00e9 pln\u00e9 \u00b7 standard'],['sliding','Posuvn\u00e9 po zdi \u00b7 p\u0159\u00edplatek'],['pocket','Do pouzdra Eclisse \u00b7 p\u0159\u00edplatek']])}
Brou\u0161en\u00fd nerez \u00b7 rozetov\u00e9 kov\u00e1n\u00ed
${stdCards('intHandle',undefined,'cols2')}

Koupelna: Klima 2. Koupelna a WC: WC z\u00e1mek. Posuvn\u00e9 varianty se zaznamenaj\u00ed do soupisu; 3D otvory ani kolize p\u0159\u00ed\u010dek se nep\u0159eprojektov\u00e1vaj\u00ed.

`); + int+=stdSection('finishes','04','Parapety a malba',`
Helopal \u00b7 lit\u00fd mramor ${stdSource(16)}
${stdCards('intSill',undefined,'cols3')}

Fotografie ukazuj\u00ed katalogov\u00fd vzorek. V modelu se m\u011bn\u00ed odst\u00edn; \u017e\u00edh\u00e1n\u00ed je na fotografii.

Malba st\u011bn
${stdCards('wallPaint',STD_OPTIONS.wallPaint.map((o,i)=>[...o]),'','', 'visual')}${stdColor('wallPaint','Vlastn\u00ed odst\u00edn st\u011bn')}${stdNotice('Standard: HET b\u00edl\u00e1, \u0161pachtlov\u00e1n\u00ed Q2. Ostatn\u00ed barvy jsou vlastn\u00ed n\u00e1hled, nikoli katalogov\u00fd vzorn\u00edk. Barva st\u011bn nem\u011bn\u00ed tr\u00e1my ani strop.')}`); + int+=stdSection('kitchen','05','Kuchy\u0148 \u00b7 vlastn\u00ed n\u00e1vrh',`${stdNotice('Kuchy\u0148 nen\u00ed v katalogu barevn\u011b ani typov\u011b specifikovan\u00e1. Projekt ponech\u00e1v\u00e1 jej\u00ed dod\u00e1vku na realizaci. N\u00e1sleduj\u00edc\u00ed volby slou\u017e\u00ed pouze pro slad\u011bn\u00ed interi\u00e9ru.','blue')}
\u010cela sk\u0159\u00edn\u011bk
${stdCards('kitchenColor',undefined,'cols3','', 'visual')}${stdColor('kitchenColor','Vlastn\u00ed odst\u00edn \u010del')}
Korpusy sk\u0159\u00edn\u011bk
${stdCards('kitchenBodyColor',undefined,'cols3','', 'visual')}${stdColor('kitchenBodyColor','Vlastn\u00ed odst\u00edn korpus\u016f')}
Pracovn\u00ed deska
${stdCards('worktop',undefined,'','', 'visual')}
Obklad za linkou
${stdCards('backsplash',undefined,'cols3','', 'visual')}

Rozvr\u017een\u00ed linky vych\u00e1z\u00ed z p\u016fvodn\u00edho HTML. Typy spot\u0159ebi\u010d\u016f, rozm\u011bry modul\u016f a cena nejsou t\u00edmto n\u00e1hledem ur\u010deny.

`); + int+=stdSection('bath','06','Koupelna a sanita',`
Proveden\u00ed sprchy ${stdSource(21)}
${stdCards('shower',undefined,'cols2','', 'catalog')}
Baterie
${stdCards('faucet',undefined,'cols2','', 'visual')}

Standardem jsou chromov\u00e9 Ravak Classic. \u010cern\u00e1 je vlastn\u00ed vizualiza\u010dn\u00ed alternativa, ne potvrzen\u00fd produkt z katalogu.

Pevn\u00e9 vybaven\u00ed dle projektu ${stdSource(20)}
Vana Ravak Chrome \u00b7 1700 \u00d7 750 mmRavak Classic, click-clack, hlavice s hadic\u00ed 1,5 m.
Umyvadlo Laufen Pro \u00b7 600 \u00d7 420 mmRavak Classic, chromov\u00fd sifon. Mal\u00e9 umyvadlo na WC dle p\u016fdorysu: konkr\u00e9tn\u00ed typ potvrdit.
2\u00d7 WC \u00b7 koupelna a samostatn\u00e9 WCGeberit 112, Ravak Uni Chrome Rimoff, softclose, b\u00edl\u00e9 tla\u010d\u00edtko Sigma.
Topn\u00fd \u017eeb\u0159\u00edk Koralux \u00b7 500 \u00d7 1500 mmVolba p\u0159ipojen\u00ed v technick\u00e9 specifikaci.

Geometrie vybaven\u00ed je zjednodu\u0161en\u00e1. Rozm\u00edst\u011bn\u00ed vych\u00e1z\u00ed z p\u016fdorysu; nenahrazuje d\u00edlensk\u00fd n\u00e1vrh koupelny.

`); + int+=stdSection('tech','07','Technika a dal\u0161\u00ed specifikace',`${stdNotice('Volby v t\u00e9to sekci se ukl\u00e1daj\u00ed do soupisu. Model nep\u0159epo\u010d\u00edt\u00e1v\u00e1 ani nem\u011bn\u00ed rozvody, prostupy, v\u00fdkony a polohy jednotek.','blue')}
V\u011btr\u00e1n\u00ed a chlazen\u00ed ${stdSource(25)}
${stdSelect('ventilation','Rekuperace',[['local','Lok\u00e1ln\u00ed Dalap Zephir \u00b7 dle projektu'],['central','Centr\u00e1ln\u00ed Brink Flair / Air Excellent'],['none','Bez rekuperace \u00b7 zm\u011bna proti projektu']])}${stdCheck('aircon','Klimatizace Panasonic \u00b7 voliteln\u00e9 vybaven\u00ed')}${stdCheck('fans','Nucen\u00e9 v\u011btr\u00e1n\u00ed koupelny / WC \u00b7 Airflow ECO 15')}${stdSelect('hood','Digesto\u0159',[['facade','V\u00fdvod na fas\u00e1du \u00d8 150 mm \u00b7 p\u0159\u00edplatek'],['recirculation','Cirkula\u010dn\u00ed \u00b7 standard']])}

V\u00fdchoz\u00ed technick\u00e9 volby sleduj\u00ed popisy v p\u016fdorysu; nacen\u011bn\u00ed a rozsah potvrzuje dodavatel. ${stdSource(16)}

Voda a vyt\u00e1p\u011bn\u00ed ${stdSource(24)}
${stdCheck('hotWaterCirculation','Cirkulace tepl\u00e9 vody \u00b7 na p\u0159\u00e1n\u00ed')}${stdSelect('radiator','Topn\u00fd \u017eeb\u0159\u00edk',[['electric','Elektropatrona \u00b7 standard'],['combined','Nav\u00edc samostatn\u00fd topn\u00fd okruh \u00b7 p\u0159\u00edplatek']])}
Teplovodn\u00ed podlahov\u00e9 topen\u00edPanasonic vzduch\u2013voda, integrovan\u00fd z\u00e1sobn\u00edk T\u00daV. Katalog uv\u00e1d\u00ed do 5 kW, skute\u010dn\u00fd n\u00e1vrh dle projektu TZB.
ABB Levit \u00b7 b\u00edl\u00e1 / b\u00edl\u00e1Standard z\u00e1suvek a vyp\u00edna\u010d\u016f. ${stdSource(17)}
Tepeln\u011bizola\u010dn\u00ed p\u016fdn\u00ed sch\u016fdky Fakro LWTV dom\u011b nen\u00ed b\u011b\u017en\u00e9 schodi\u0161t\u011b. Rozm\u011br otvoru potvrdit podle projektu. ${stdSource(22)}
`); + $('#std-interior').innerHTML=int; + $('#std-summary').innerHTML=`

V\u00e1\u0161 v\u00fdb\u011br

${stdNotice('P\u0159ehled variant, nikoli cenov\u00e1 nab\u00eddka. P\u0159\u00edplatky ani dostupnost katalog 2025 neoce\u0148uje. P\u0159ed objedn\u00e1n\u00edm potvr\u010fte aktu\u00e1ln\u00ed vzorn\u00edky s ALLSTAV.','amber')}

Zdroj: dodan\u00fd ALLSTAV Standardy, z\u00e1\u0159\u00ed 2025, a projekt RD. Fotografie a vzorky jsou v samostatn\u00e9 slo\u017ece assets. RGB n\u00e1hled nenahrazuje fyzick\u00fd vzorek RAL ani materi\u00e1lu. 3D n\u00e1hled nen\u00ed prov\u00e1d\u011bc\u00ed dokumentace.

`; + } + function stdSummary(){ + const s=stdState,premium='P\u0159\u00edplatek',vis='Vlastn\u00ed vizualizace',spec='Specifikace; bez zm\u011bny geometrie'; + const groups=[['Exteri\u00e9r',[ + ['Vchodov\u00e9 dve\u0159e',`KeraT\u00fcr \u00b7 ${stdLabel('entryModel')} \u00b7 ${stdLabel('entryColor')}`,stdOption('entryModel')[3]?premium:'Standard'], + ['Venkovn\u00ed parapety',stdLabel('extSill'),stdOption('extSill')[3]?premium:'Standard'],['Okapy / svody',`Lindab ${stdLabel('gutter')}`,stdOption('gutter')[3]?premium:'Standard'], + ['St\u0159e\u0161n\u00ed ta\u0161ky',`Benders Palema Candor \u00b7 ${stdLabel('roof')}`,'Standard'], + ['Venkovn\u00ed \u017ealuzie',s.blinds?`Zetta 70 \u00b7 ${stdLabel('blindColor')} \u00b7 ${s.blindDrop} %`:'Vypnuto',s.blinds?premium+'; 9 otvor\u016f, bez troj\u00faheln\u00edku':'Voliteln\u00e9; projekt s nimi po\u010d\u00edt\u00e1']]], + ['Podlahy',STD_ROOMS.map(r=>{const f=s.floors[r[0]];return[`${r[0]} ${r[1]} (${r[2]} m\u00b2)`,`${f.type==='lam'?'KronoSwiss lamin\u00e1t':'Keramick\u00e1 dla\u017eba'} \u00b7 vzorek ${String(f.sample).padStart(2,'0')}`,f.type===r[3]?'K\u00f3d dekoru potvrdit':'Zm\u011bna druhu proti projektu'];})], + ['Povrchy a dve\u0159e',[ + ['Obklad koupelny',`Vzorek ${s.wallTile105} \u00b7 do 2,1 m`,'K\u00f3d dekoru potvrdit'],['Obklad WC',`Vzorek ${s.wallTile106} \u00b7 do 2,1 m`,'K\u00f3d dekoru potvrdit'], + ['Ukon\u010den\u00ed obkladu',s.tileCorner==='profile'?'Eloxov\u00e1 li\u0161ta':'Kamenick\u00fd roh',s.tileCorner==='miter'?premium+'; '+spec:spec],['Vnit\u0159n\u00ed parapety','Helopal \u00b7 '+stdLabel('intSill'),'Standard'], + ['Malba',s.wallPaint==='#ffffff'?'HET b\u00edl\u00e1 \u00b7 Q2':s.wallPaint,s.wallPaint==='#ffffff'?'Standard':vis], + ['Interi\u00e9rov\u00e9 dve\u0159e','Pr\u00fcm CPL \u00b7 '+(s.intDoorCode||'dekor neur\u010den'),s.intDoor==='sample'?'Referen\u010dn\u00ed fotografie':vis+' '+s.intDoorColor], + ['Proveden\u00ed dve\u0159\u00ed',({hinged:'Oto\u010dn\u00e9 pln\u00e9',sliding:'Posuvn\u00e9 po zdi',pocket:'Do pouzdra Eclisse'})[s.intDoorType],s.intDoorType==='hinged'?'Standard':premium+'; '+spec], + ['Kov\u00e1n\u00ed',stdLabel('intHandle')+' rozeta \u00b7 brou\u0161en\u00fd nerez','Standard; WC z\u00e1mky, koupelna Klima 2']]], + ['Kuchy\u0148 \u00b7 vlastn\u00ed n\u00e1vrh',[ + ['\u010cela sk\u0159\u00edn\u011bk',stdLabel('kitchenColor')+' '+s.kitchenColor,vis],['Korpusy sk\u0159\u00edn\u011bk',stdLabel('kitchenBodyColor')+' '+s.kitchenBodyColor,vis],['Pracovn\u00ed deska',stdLabel('worktop'),vis],['Obklad linky',stdLabel('backsplash'),vis]]], + ['Koupelna a sanita',[ + ['Sprcha',s.shower==='tray'?'Ravak Chrome 900 \u00d7 900 mm, vani\u010dka + z\u00e1st\u011bna':'Walk-in, z\u00e1st\u011bna 800 mm, \u017el\u00e1bek 750 mm','Katalog str. 21; variantu a cenu potvrdit'], + ['Baterie',s.faucet==='chrome'?'Ravak Classic \u00b7 chrom':'\u010cern\u00e9 proveden\u00ed \u00b7 typ neur\u010den',s.faucet==='chrome'?'Standard':vis], + ['Vana','Ravak Chrome 1700 \u00d7 750 mm','Dle projektu'],['Umyvadlo koupelny','Laufen Pro 600 \u00d7 420 mm','Mal\u00e9 umyvadlo na WC: typ potvrdit'],['WC','2\u00d7 Geberit 112 / Ravak Uni Chrome Rimoff','Softclose; b\u00edl\u00e1 Sigma; dle p\u016fdorysu']]], + ['Technika \u00b7 specifikace',[ + ['Rekuperace',({local:'Lok\u00e1ln\u00ed Dalap Zephir',central:'Brink Flair / Air Excellent',none:'Bez rekuperace'})[s.ventilation],spec],['Klimatizace',s.aircon?'Panasonic':'Bez klimatizace',spec],['Nucen\u00e9 v\u011btr\u00e1n\u00ed',s.fans?'Airflow ECO 15':'Bez dopl\u0148kov\u00fdch ventil\u00e1tor\u016f',s.fans?premium+'; '+spec:spec], + ['Digesto\u0159',s.hood==='facade'?'V\u00fdvod na fas\u00e1du \u00d8 150 mm':'Cirkula\u010dn\u00ed',s.hood==='facade'?premium+'; '+spec:'Standard; '+spec],['Cirkulace T\u00daV',s.hotWaterCirculation?'Ano':'Ne',spec],['Koralux 500 \u00d7 1500',s.radiator==='electric'?'Elektropatrona':'Elektropatrona + samostatn\u00fd okruh',s.radiator==='combined'?premium+'; '+spec:'Standard'],['Z\u00e1suvky / vyp\u00edna\u010de','ABB Levit b\u00edl\u00e1 / b\u00edl\u00e1','Standard'],['Vyt\u00e1p\u011bn\u00ed','Teplovodn\u00ed podlahov\u00e9 / Panasonic vzduch-voda','V\u00fdkon a rozvody dle projektu TZB'],['P\u016fdn\u00ed sch\u016fdky','Fakro LWT','Rozm\u011br potvrdit dle projektu']]]]; + return groups; + } + function stdSync(){ + if(!stdReady)return; + for(const el of document.querySelectorAll('[data-std-input]')){const v=stdState[el.dataset.stdInput];if(el.type==='checkbox')el.checked=v;else if(document.activeElement!==el)el.value=v;} + for(const b of document.querySelectorAll('[data-std-key]')){ + const k=b.dataset.stdKey;const v=k==='floorSample'?stdState.floors[stdFloorRoom].sample:k==='wallTile'?stdState['wallTile'+stdTileRoom]:stdState[k];b.setAttribute('aria-pressed',String(String(v)===b.dataset.stdValue)); + } + const currents={entry:stdLabel('entryModel')+' \u00b7 '+stdLabel('entryColor'),extSill:stdLabel('extSill'),gutter:'Lindab '+stdLabel('gutter'),roof:stdLabel('roof'),blinds:stdState.blinds?stdLabel('blindColor')+' \u00b7 '+stdState.blindDrop+' %':'Vypnuto \u00b7 voliteln\u00e9',floors:'9 m\u00edstnost\u00ed \u00b7 samostatn\u00fd v\u00fdb\u011br dekoru',tiles:'Koupelna '+stdState.wallTile105+' \u00b7 WC '+stdState.wallTile106,doors:'Pr\u00fcm CPL \u00b7 '+stdLabel('intHandle')+' rozeta',finishes:'Helopal '+stdLabel('intSill')+' \u00b7 '+(stdState.wallPaint==='#ffffff'?'HET b\u00edl\u00e1':stdState.wallPaint),kitchen:stdLabel('kitchenColor')+' \u00b7 '+stdLabel('worktop'),bath:stdLabel('shower')+' \u00b7 '+stdLabel('faucet'),tech:'V\u011btr\u00e1n\u00ed, topen\u00ed, elektro'}; + for(const e of document.querySelectorAll('[data-std-current]'))e.textContent=currents[e.dataset.stdCurrent]||''; + $('#stdBlindsControls').classList.toggle('std-disabled',!stdState.blinds);$('#stdBlindsControls').querySelectorAll('button,input').forEach(e=>e.disabled=!stdState.blinds);$('#stdBlindDropLabel').textContent=stdState.blindDrop+' %'; + $('#stdDoorCustom').hidden=stdState.intDoor!=='custom'; + $('#stdFloorRoom').value=stdFloorRoom;$('#stdFloorType').value=stdState.floors[stdFloorRoom].type;$('#stdTileRoom').value=stdTileRoom; + const f=stdState.floors[stdFloorRoom],r=STD_ROOMS.find(r=>r[0]===stdFloorRoom); + if($('#stdFloorSamples').dataset.type!==f.type){$('#stdFloorSamples').innerHTML=stdSampleCards('floorSample',f.type);$('#stdFloorSamples').dataset.type=f.type;$('#stdFloorSamples').querySelectorAll('button').forEach(b=>b.setAttribute('aria-pressed',String(+b.dataset.stdValue===f.sample)));} + $('#stdFloorProjectNote').textContent='Projekt: '+(r[3]==='lam'?'lamin\u00e1t':'keramick\u00e1 dla\u017eba')+'. '+(f.type===r[3]?'Zvolen\u00fd druh krytiny odpov\u00edd\u00e1 projektu.':'Zvolen\u00fd druh se od projektu li\u0161\u00ed. Nutn\u00e9 potvrdit skladbu a v\u00fd\u0161ku podlahy.'); + $('#stdFloorProjectNote').classList.toggle('amber',f.type!==r[3]); + $('#stdShowerSpec').textContent=stdState.shower==='tray'?'Ravak Chrome 900 \u00d7 900 mm, vani\u010dka a z\u00e1st\u011bna. Ravak Classic a sprchov\u00fd set 922 se 3 funkcemi.':'Walk-in: nerezov\u00fd odtokov\u00fd \u017el\u00e1bek 750 mm, pevn\u00e1 z\u00e1st\u011bna 800 mm, sp\u00e1dov\u00fd profil. Potvrdit skladbu podlahy, hydroizolaci a cenu.'; + $('#stdSummaryContent').innerHTML=stdSummary().map(([heading,rows])=>`

${stdEsc(heading)}

${rows.map(([a,b,c])=>`
${stdEsc(a)}${stdEsc(b)}${stdEsc(c)}
`).join('')}
`).join(''); + } + function stdSet(key,value){ + const next=stdClone(stdState); + if(key==='floorSample')next.floors[stdFloorRoom].sample=+value;else if(key==='wallTile')next['wallTile'+stdTileRoom]=+value;else next[key]=value; + try{stdState=stdNormalize(next);if(['note','intDoorCode','intDoorType','tileCorner','hood','fans','aircon','ventilation','hotWaterCirculation','radiator'].includes(key)){gltf.extras.allstavSelection=stdClone(stdState);stdSync();clearTimeout(stdSaveTimer);stdSaveTimer=setTimeout(stdPersist,220);}else stdApply();}catch(e){toast(e.message);} + } + function stdClose(){document.body.classList.remove('std-open');$('#standardsToggle').classList.remove('active');$('#standardsToggle').setAttribute('aria-expanded','false');resize();} + function stdOpen(tab=stdTab){ + stdSwitching=true; + if($('#sunPanel').classList.contains('open'))toggleSunPanel(false); + $('#panel').classList.remove('open');$('#infoButton').classList.remove('active'); + if(tab==='interior'&&!interior.enabled)toggleInterior(true);if(tab==='exterior'&&interior.enabled)toggleInterior(false); + if($('#sunPanel').classList.contains('open'))toggleSunPanel(false); + document.body.classList.add('std-open');stdTab=tab; + $('#standardsToggle').classList.add('active');$('#standardsToggle').setAttribute('aria-expanded','true'); + document.querySelectorAll('[data-std-tab]').forEach(b=>b.setAttribute('aria-selected',String(b.dataset.stdTab===tab))); + ['exterior','interior','summary'].forEach(k=>$('#std-'+k).hidden=k!==tab); + autoRotate=false;$('#rotateButton')?.classList.remove('active');resize();stdSync();stdSwitching=false; + } + function stdView(id){ + if(['entry','terrace','site'].includes(id)){ + stdOpen('exterior');if(id==='site'){setPreset('entry');return;} + setCamera(id==='entry'?{theta:-.12,phi:.09,distance:Math.max(8.5,8.5/aspect),target:[-3.65,1.22,3.84],ortho:false}:{theta:1.05,phi:.34,distance:Math.max(37,34/aspect),target:[1.0,1.0,0],ortho:false},id==='entry'?'Vstup \u00b7 standardy':'Terasa \u00b7 standardy');return; + } + stdOpen('interior');interior.furniture=true;$('#interiorFurniture').checked=true; + if(id==='plan'||id==='axon'){setInteriorMode(id);return;} + if(id==='shower'){ + enterInteriorView('105');interior.position=[-2.20,1.55,-2.10];const look=[-3.48,.93,-3.82],dir=vsub(look,interior.position);interior.yaw=Math.atan2(dir[0],dir[2]);interior.pitch=Math.atan2(dir[1],Math.hypot(dir[0],dir[2]));interior.fov=72;dirty=true;return; + } + if(id==='kitchen'){const v=interior.data.presets.find(p=>/kitchen/i.test(p.id));enterInteriorView(v?.id||'108');return;} + enterInteriorView(id); + } + function stdShowCatalog(page){ + stdCatalogPage=page==='requestedDoor'?'requestedDoor':Math.min(25,Math.max(12,+page||12)); + const key=stdCatalogPage==='requestedDoor'?'requestedDoor':'page'+stdCatalogPage; + $('#stdCatalogImage').src=STD_ASSETS[key];$('#stdCatalogImage').alt=stdCatalogPage==='requestedDoor'?'V\u00e1\u0161 referen\u010dn\u00ed obr\u00e1zek dve\u0159\u00ed':'ALLSTAV Standardy 2025, strana '+stdCatalogPage; + $('#stdCatalogTitle').textContent=stdCatalogPage==='requestedDoor'?'V\u00fdchoz\u00ed dve\u0159e \u00b7 v\u00e1\u0161 obr\u00e1zek':'ALLSTAV Standardy 2025 \u00b7 strana '+stdCatalogPage; + $('#stdCatalogCaption').textContent='P\u016fvodn\u00ed podklad. Odst\u00edny na displeji a fotografie jsou pouze orienta\u010dn\u00ed; konkr\u00e9tn\u00ed v\u00fdrobek a dostupnost ov\u011b\u0159te u dodavatele.'; + if(!$('#stdCatalogDialog').open)$('#stdCatalogDialog').showModal(); + interior.keys.clear();interior.pad.clear(); + } + function stdExportData(){return{schema:'allstav-rd-config',version:1,catalog:'ALLSTAV Standardy, zari 2025',project:'RD Locenice',exportedAt:new Date().toISOString(),selection:stdClone(stdState),summary:stdSummary(),scope:'Illustrative geometry. Kitchen is a custom visualization. Technical alternatives are specification-only.'};} + function stdText(){return 'RD LOCENICE | VYBER STANDARDU\nALLSTAV, katalog zari 2025\n'+new Date().toLocaleDateString('cs-CZ')+'\n\n'+stdSummary().map(([title,rows])=>title.toUpperCase()+'\n'+rows.map(([a,b,c])=>a+': '+b+' ['+c+']').join('\n')).join('\n\n')+'\n\nPOZNAMKA\n'+(stdState.note||'-')+'\n\nSoupis neni cenovou nabidkou. Kody neoznacenych vzorku, ceny, dostupnost a projektove zmeny potvrdit s dodavatelem. Vlastni kuchynske a barevne alternativy nejsou katalogovym standardem. 3D je ilustrativni, nikoli provadeci dokumentace.\n';} + function stdSaveHtml(){ + const payload=stdExportData();const bytes=new TextEncoder().encode(JSON.stringify(payload)); + let binaryText='';bytes.forEach(b=>binaryText+=String.fromCharCode(b)); + const encoded=btoa(binaryText).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,''); + const url=new URL(location.href);url.searchParams.delete('renderer');url.hash='vyber='+encoded; + // Hash stays in the browser, not a server-side database. It contains the note too. + if(navigator.clipboard&&window.isSecureContext)navigator.clipboard.writeText(url.href).then(()=>toast('Odkaz s vyberem byl zkopirovan. Obsahuje i poznamku.'),()=>prompt('Zkopirujte odkaz; obsahuje i poznamku:',url.href)); + else prompt('Zkopirujte odkaz; obsahuje i poznamku:',url.href); +} + + async function stdImport(file){ + if(!file)return; + try{if(file.size>2*1024*1024)throw Error('Soubor je p\u0159\u00edli\u0161 velk\u00fd. Limit je 2 MB.');const data=JSON.parse(await file.text());if(data.schema!=='allstav-rd-config'||data.version!==1)throw Error('Nezn\u00e1m\u00fd form\u00e1t nebo verze JSON.');const next=stdNormalize(data.selection);stdState=next;stdApply();toast('Nastaven\u00ed bylo na\u010dteno.');}catch(e){toast('Import se nezda\u0159il: '+e.message);} + $('#stdImport').value=''; + } + function stdAction(action){ + if(action==='floorView')return stdView(stdFloorRoom); + if(action==='matchKitchenBody')return stdSet('kitchenBodyColor',stdState.kitchenColor); + if(action==='copyFloor'){const f=stdState.floors[stdFloorRoom];for(const r of STD_ROOMS)if(stdState.floors[r[0]].type===f.type)stdState.floors[r[0]].sample=f.sample;stdApply();toast('Vzorek p\u0159enesen do m\u00edstnost\u00ed se stejnou krytinou.');return;} + if(action==='exportJson')return saveBlob(new Blob([JSON.stringify(stdExportData(),null,2)],{type:'application/json'}),'allstav_muj_vyber.json'); + if(action==='exportText')return saveBlob(new Blob(['\ufeff'+stdText()],{type:'text/plain;charset=utf-8'}),'allstav_soupis_standardu.txt'); + if(action==='importJson')return $('#stdImport').click(); + if(action==='saveHtml')return stdSaveHtml(); + if(action==='summary')return stdOpen('summary'); + if(action==='png'){render();canvas.toBlob(b=>{if(b)saveBlob(b,'dum_ALLSTAV_nahled.png');});return;} + if(action==='glb')return saveBlob(new Blob([currentGlb()],{type:'model/gltf-binary'}),'dum_ALLSTAV_viditelny_vyber.glb'); + if(action==='reset')return $('#stdResetDialog').showModal(); + if(action==='original'){stdClose();$('#panel').classList.add('open');$('#infoButton').classList.add('active');} + } + function initStandards(){ + let loaded=false,embedded={}; + stdState=stdClone(STD_DEFAULTS); + try{embedded=JSON.parse($('#stdSavedState').textContent||'{}');}catch(e){console.info('ALLSTAV: invalid embedded settings ignored.');} + if(embedded.selection){ + try{stdState=stdNormalize(embedded.selection);loaded=true;}catch(e){console.info('ALLSTAV: invalid embedded selection ignored.');} + let id=embedded.storageId; + if(typeof id!=='string'||!/^[a-z0-9_-]{1,100}$/i.test(id)){ + // Stable fallback for HTML exports made by the previous version. + let hash=2166136261;for(const ch of JSON.stringify(embedded.selection)){hash=Math.imul(hash^ch.charCodeAt(0),16777619);} + id='legacy-'+(hash>>>0).toString(36); + } + stdStorageKey=STD_STORAGE+'.copy.'+id; + }else stdStorageKey=STD_STORAGE; + // Edits made after opening an exported copy take precedence on reload. + // A new copy has its own key and opens with its embedded selection. + try{const stored=localStorage.getItem(stdStorageKey);if(stored){stdState=stdNormalize(JSON.parse(stored));loaded=true;}}catch(e){console.info('ALLSTAV: invalid or inaccessible local settings ignored.');} + stdBuildPanel();stdReady=true;stdApply(false); + $('#standardsToggle').onclick=()=>document.body.classList.contains('std-open')?stdClose():stdOpen(interior.enabled?'interior':stdTab==='interior'?'exterior':stdTab); + $('#stdClose').onclick=stdClose; + $('#standardsPanel').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;if(b.dataset.stdKey){const k=b.dataset.stdKey,v=b.dataset.stdValue;stdSet(k,v);}else if(b.dataset.stdTab){stdOpen(b.dataset.stdTab);}else if(b.dataset.stdPage){stdShowCatalog(b.dataset.stdPage);}else if(b.dataset.stdView){stdView(b.dataset.stdView);}else if(b.dataset.stdAction){stdAction(b.dataset.stdAction);}}); + $('#standardsPanel').addEventListener('input',e=>{const el=e.target,k=el.dataset.stdInput;if(!k)return;let v=el.type==='checkbox'?el.checked:el.type==='range'?+el.value:el.value;stdSet(k,v);}); + $('#stdFloorRoom').onchange=e=>{stdFloorRoom=e.target.value;stdSync();}; + $('#stdFloorType').onchange=e=>{stdState.floors[stdFloorRoom]={type:e.target.value,sample:1};stdApply();}; + $('#stdTileRoom').onchange=e=>{stdTileRoom=e.target.value;stdSync();}; + $('#stdCatalogClose').onclick=()=>$('#stdCatalogDialog').close();$('#stdCatalogPrev').onclick=()=>stdShowCatalog(typeof stdCatalogPage==='number'?Math.max(12,stdCatalogPage-1):12);$('#stdCatalogNext').onclick=()=>stdShowCatalog(typeof stdCatalogPage==='number'?Math.min(25,stdCatalogPage+1):12); + $('#stdResetCancel').onclick=()=>$('#stdResetDialog').close();$('#stdResetConfirm').onclick=()=>{stdState=stdClone(STD_DEFAULTS);stdApply();$('#stdResetDialog').close();toast('V\u00fdchoz\u00ed v\u00fdb\u011br byl obnoven.');}; + $('#stdImport').onchange=e=>stdImport(e.target.files[0]); + for(const id of ['stdCatalogDialog','stdResetDialog'])$('#'+id).addEventListener('click',e=>{if(e.target.id===id){const r=e.target.getBoundingClientRect();if(e.clientXr.right||e.clientYr.bottom)e.target.close();}}); + const oldSun=toggleSunPanel;toggleSunPanel=function(on){if(on&&!stdSwitching)stdClose();return oldSun(on);}; + const oldToggle=toggleInterior;toggleInterior=function(on){if(!stdSwitching&&document.body.classList.contains('std-open'))stdClose();return oldToggle(on);}; + window.INTERIOR.toggle=toggleInterior; + const oldPaint=setInteriorWallColor;setInteriorWallColor=function(hex,persist=true){const result=oldPaint(hex,persist);if(persist&&stdReady){stdState.wallPaint=interior.wallColor;stdSync();stdPersist();}return result;};window.INTERIOR.setWallColor=setInteriorWallColor; + const oldInfo=$('#infoButton').onclick;$('#infoButton').onclick=e=>{stdClose();oldInfo?.(e);}; + const oldPanel=$('#interiorPanelToggle').onclick;$('#interiorPanelToggle').onclick=e=>{stdClose();oldPanel?.(e);}; + // Keep legacy wall colour controls synchronized with the new selection. + document.querySelectorAll('[data-wall-color]').forEach(b=>b.addEventListener('click',()=>{stdState.wallPaint=interior.wallColor;stdSync();stdPersist();})); + $('#roofColor').disabled=true;$('#roofColor').title='Barvu krytiny vyberte v sekci Standardy / Exteri\u00e9r.'; + $('#resetColors')?.addEventListener('click',()=>stdApply()); + window.addEventListener('keydown',e=>{if($('#stdCatalogDialog').open||$('#stdResetDialog').open){e.stopImmediatePropagation();return;}if(e.key==='Escape'&&document.body.classList.contains('std-open')){e.preventDefault();e.stopImmediatePropagation();stdClose();}},true); + window.ALLSTAV_CONFIG={getState:()=>stdClone(stdState),defaults:()=>stdClone(STD_DEFAULTS),set:(patch)=>{stdState=stdNormalize({...stdClone(stdState),...patch});stdApply();return stdClone(stdState);},open:stdOpen,close:stdClose,view:stdView,summary:stdSummary,exportData:stdExportData,normalize:stdNormalize,visibleWindows:()=>[...new Set(meshes.filter(m=>m.std?.stdBlind&&visible(m)).map(m=>m.std.windowNode))],sourcePage:stdShowCatalog,saveHtml:stdSaveHtml,validate:()=>({windowCount:gltf.extras.allstavStandards.windows.length,triangleExcluded:!meshes.some(m=>m.std?.stdBlind&&m.std.windowNode===gltf.extras.allstavStandards.triangleExcludedNode),rooms:STD_ROOMS.length,materials:gltf.extras.allstavStandards.materials})}; + if(loaded)$('#stdSaved').textContent='Na\u010dten v\u00e1\u0161 ulo\u017een\u00fd v\u00fdb\u011br';else $('#stdSaved').textContent='V\u00fdchoz\u00ed v\u00fdb\u011br \u00b7 zm\u011bny se ukl\u00e1daj\u00ed automaticky'; + window.addEventListener('pagehide',()=>{if(stdSaveTimer){clearTimeout(stdSaveTimer);stdPersist();}}); + stdOpen('exterior');stdView('terrace'); + } + + start(); \ No newline at end of file diff --git a/locenice/locenice/js/bootstrap.js b/locenice/locenice/js/bootstrap.js new file mode 100644 index 0000000..fe25ba7 --- /dev/null +++ b/locenice/locenice/js/bootstrap.js @@ -0,0 +1,23 @@ +/** Only application startup. All paths are relative to /locenice/index.php. */ +async function json(path){const response=await fetch(path,{cache:'no-cache'});if(!response.ok)throw Error(path+' / HTTP '+response.status);return response.json();} +try{ + const config=await json('./data/app-config.json'); + const [catalog,assets,references,scene,site]=await Promise.all([config.catalog,config.assets,config.references,config.sceneMetadata,config.site].map(json)); + const response=await fetch(config.model);if(!response.ok)throw Error('Model / HTTP '+response.status); + const modelBytes=new Uint8Array(await response.arrayBuffer()); + config.threeModule=new URL(config.threeModule,location.href).href;config.threeLoader=new URL(config.threeLoader,location.href).href; + // The standard GLTFLoader imports the same pinned Three.js copy. + const map=document.createElement('script');map.type='importmap';map.textContent=JSON.stringify({imports:{three:config.threeModule}});document.head.appendChild(map); + const hash=location.hash; + if(hash.startsWith('#vyber='))try{ + let text=hash.slice(7).replace(/-/g,'+').replace(/_/g,'/');text+='='.repeat((4-text.length%4)%4); + if(text.length>32000)throw Error('Too large');const bytes=Uint8Array.from(atob(text),ch=>ch.charCodeAt(0));const payload=JSON.parse(new TextDecoder().decode(bytes)); + if(payload.schema!=='allstav-rd-config'||payload.version!==1||!payload.selection)throw Error('Invalid selection'); + document.getElementById('stdSavedState').textContent=JSON.stringify({selection:payload.selection}); + }catch(error){console.warn('Invalid shared selection:',error);} + window.LOCENICE_RESOURCES={config,catalog,assets,references,scene,site,modelBytes}; + await import('./app.js'); +}catch(error){ + console.error(error);const box=document.getElementById('loading');box.className='error';box.innerHTML='

Nahrajte celou slozku vcetne js, css, data, models a assets. Otevrete ji pres HTTP(S), nikoli dvojklikem na HTML.

Stahnout model GLB'; + box.querySelector('.errorDetail').textContent=error.message;document.getElementById('webStatus').textContent='Chyba nacitani'; +} diff --git a/locenice/locenice/js/legacy-renderer.js b/locenice/locenice/js/legacy-renderer.js new file mode 100644 index 0000000..4c97a4a --- /dev/null +++ b/locenice/locenice/js/legacy-renderer.js @@ -0,0 +1,89 @@ +import {vs,fs,depthvs,depthfs} from './legacy-shaders.js'; +/** Self-contained fallback. No external dependencies. Keeps the original shaders. */ +export async function createLegacyRenderer({canvas,gltf,binary}) { + let frame=null,solar,interior,eye,vp,lightVP,gl,program,shadowProgram,shadowFbo,shadowTex; + const meshes=[],originalColors={},hardieTextures=[],hardieImages=[],hardieObjectURLs=[]; + const mul=(a,b)=>{const c=new Float32Array(16);for(let j=0;j<4;j++)for(let i=0;i<4;i++)for(let k=0;k<4;k++)c[j*4+i]+=a[k*4+i]*b[j*4+k];return c;}; + const lookAt=()=>new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]); + const ortho=lookAt; + function compile(type,src){const sh=gl.createShader(type);gl.shaderSource(sh,src);gl.compileShader(sh);if(!gl.getShaderParameter(sh,gl.COMPILE_STATUS))throw Error(gl.getShaderInfoLog(sh));return sh;} + function link(v,f){const pr=gl.createProgram();gl.attachShader(pr,compile(gl.VERTEX_SHADER,v));gl.attachShader(pr,compile(gl.FRAGMENT_SHADER,f));gl.linkProgram(pr);if(!gl.getProgramParameter(pr,gl.LINK_STATUS))throw Error(gl.getProgramInfoLog(pr));return pr;} +async function loadHardieTextures() { + const anisotropy = gl.getExtension('EXT_texture_filter_anisotropic'); + for (const [index, texture] of (gltf.textures || []).entries()) { + const imageSpec = gltf.images[texture.source], view = gltf.bufferViews[imageSpec.bufferView]; + const data = binary.subarray(view.byteOffset || 0, (view.byteOffset || 0) + view.byteLength); + const url = URL.createObjectURL(new Blob([data], { type: imageSpec.mimeType })); hardieObjectURLs.push(url); + const image = new Image(); + await new Promise((resolve, reject) => { image.onload = resolve; image.onerror = () => reject(Error('Nelze na\u010d\u00edst vlo\u017eenou texturu HardiePlank.')); image.src = url; }); + const gpuTexture = gl.createTexture(); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, gpuTexture); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); + gl.generateMipmap(gl.TEXTURE_2D); + if (anisotropy) gl.texParameterf(gl.TEXTURE_2D, anisotropy.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(8, gl.getParameter(anisotropy.MAX_TEXTURE_MAX_ANISOTROPY_EXT))); + hardieTextures[index] = gpuTexture; hardieImages[index] = image; + } + gl.activeTexture(gl.TEXTURE0); + } +function getAccessor(i) { const a = gltf.accessors[i], v = gltf.bufferViews[a.bufferView]; return { a, v, offset: (v.byteOffset || 0) + (a.byteOffset || 0) }; } + function visible(m) { return frame.visible(m); } + function initGL() { + gl = canvas.getContext('webgl2', { antialias: true, alpha: false, preserveDrawingBuffer: true, powerPreference: 'high-performance' }); + if (!gl) throw Error('Prohl\u00ed\u017ee\u010d nepodporuje WebGL 2. Soubor GLB si m\u016f\u017ee\u0161 st\u00e1hnout a otev\u0159\u00edt v 3D programu.'); + program = link(vs, fs); shadowProgram = link(depthvs, depthfs); + const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, binary, gl.STATIC_DRAW); + const ibuf = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuf); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, binary, gl.STATIC_DRAW); + for (const [nodeIndex, node] of gltf.nodes.entries()) { + if (node.mesh === undefined) continue; const mesh = gltf.meshes[node.mesh]; for (const p of mesh.primitives) { + const pos = getAccessor(p.attributes.POSITION), nor = getAccessor(p.attributes.NORMAL), ind = getAccessor(p.indices), mat = gltf.materials[p.material], name = node.name || mesh.name || ''; + if (node.matrix && node.matrix.some((v, i) => Math.abs(v - (i % 5 === 0 ? 1 : 0)) > 1e-6)) throw Error('Unexpected non-identity model transform'); + const vao = gl.createVertexArray(); gl.bindVertexArray(vao); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, pos.a.componentType, false, pos.v.byteStride || 0, pos.offset); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 3, nor.a.componentType, false, nor.v.byteStride || 0, nor.offset); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuf); + for (const [attribute, location, size] of [['TEXCOORD_0', 2, 2], ['TANGENT', 3, 4]]) { + if (p.attributes[attribute] !== undefined) { const a = getAccessor(p.attributes[attribute]); gl.enableVertexAttribArray(location); gl.vertexAttribPointer(location, size, a.a.componentType, false, a.v.byteStride || 0, a.offset); } + else { gl.disableVertexAttribArray(location); if (location === 2) gl.vertexAttrib2f(location, 0, 0); else gl.vertexAttrib4f(location, 1, 0, 0, 1); } + } + const pbr = mat.pbrMetallicRoughness || {}; originalColors[p.material] = [...(pbr.baseColorFactor || [.7, .7, .7, 1])]; + meshes.push({ std: node.extras || {}, transparent: mat.alphaMode === 'BLEND', vao, count: ind.a.count, type: ind.a.componentType, offset: ind.offset, material: p.material, name, context: /^(90_|91_|28_|23_)/.test(name), nodeIndex, legacySite: !!node.extras?.legacySite, siteLayer: node.extras?.siteLayer || null, castShadow: node.extras?.castShadow !== false, bounds: { min: pos.a.min, max: pos.a.max }, glass: (mat.name || '').startsWith('glass'), interiorLayer: node.extras?.interiorLayer || null, interiorMaterial: !!mat.extras?.interiorMaterial, interiorStyle: mat.extras?.interiorStyle || 0, surfaceKind: mat.extras?.hardieRole === 'boards' ? (node.extras?.interiorLayer === 'cutShell' ? 3 : 1) : mat.extras?.hardieRole === 'backing' ? 2 : (mat.extras?.interiorMaterial || mat.extras?.linearColor) ? (pbr.baseColorTexture ? 3 : 2) : 0, pbr }); + } + } + gl.bindVertexArray(null); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.disable(gl.CULL_FACE); + shadowTex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, shadowTex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT24, 2048, 2048, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + shadowFbo = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, shadowTex, 0); gl.drawBuffers([gl.NONE]); gl.readBuffer(gl.NONE); if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) throw Error('Shadow framebuffer incomplete'); gl.bindFramebuffer(gl.FRAMEBUFFER, null); + const lightPos = [-15, 29, 12], lightTarget = [.5, 0, 0]; lightVP = mul(ortho(-20, 20, -19, 19, 1, 65), lookAt(lightPos, lightTarget)); + + } +function renderShadow() { gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFbo); gl.viewport(0, 0, 2048, 2048); gl.clear(gl.DEPTH_BUFFER_BIT); gl.useProgram(shadowProgram); gl.uniformMatrix4fv(gl.getUniformLocation(shadowProgram, 'uVP'), false, lightVP); gl.enable(gl.POLYGON_OFFSET_FILL); gl.polygonOffset(2.0, 3.0); for (const m of meshes) { if (!visible(m) || m.glass || m.transparent || !m.castShadow) continue; gl.bindVertexArray(m.vao); gl.drawElements(gl.TRIANGLES, m.count, m.type, m.offset); } gl.disable(gl.POLYGON_OFFSET_FILL); gl.bindFramebuffer(gl.FRAMEBUFFER, null); } +function render() { + if (solar.shadowDirty && solar.shadows && solar.direct > 0) { renderShadow(); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, canvas.width, canvas.height); gl.clearColor(...solar.sky, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.useProgram(program); gl.uniformMatrix4fv(gl.getUniformLocation(program, 'uVP'), false, vp); gl.uniformMatrix4fv(gl.getUniformLocation(program, 'uLightVP'), false, lightVP); gl.uniform3fv(gl.getUniformLocation(program, 'uEye'), eye); gl.uniform3fv(gl.getUniformLocation(program, 'uLight'), solar.light); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, shadowTex); gl.uniform1i(gl.getUniformLocation(program, 'uShadow'), 0); gl.uniform1i(gl.getUniformLocation(program, 'uBaseMap'), 1); gl.uniform1i(gl.getUniformLocation(program, 'uNormalMap'), 2); + gl.uniform3fv(gl.getUniformLocation(program, 'uAmbient'), solar.ambient); gl.uniform3fv(gl.getUniformLocation(program, 'uSunColor'), solar.sunColor); gl.uniform1f(gl.getUniformLocation(program, 'uDirect'), solar.direct); gl.uniform1f(gl.getUniformLocation(program, 'uSkyDay'), solar.dayFactor); gl.uniform1f(gl.getUniformLocation(program, 'uShadows'), solar.shadows ? 1 : 0); + const transparentMeshes=meshes.filter(m=>m.transparent&&visible(m)).sort((a,b)=>{const dist=m=>{const c=m.bounds.min.map((v,i)=>(v+m.bounds.max[i])/2);return c.reduce((d,v,i)=>d+(v-eye[i])**2,0)};return dist(b)-dist(a);}); + for (const m of meshes.filter(m=>!m.transparent).concat(transparentMeshes)) { if (!visible(m)) continue; const p = gltf.materials[m.material].pbrMetallicRoughness; + if(m.transparent){gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);gl.depthMask(false);}else{gl.disable(gl.BLEND);gl.depthMask(true);} + gl.uniform2fv(gl.getUniformLocation(program,'uUvScale'),p.baseColorTexture?.extensions?.KHR_texture_transform?.scale||[1,1]); gl.uniform4fv(gl.getUniformLocation(program, 'uColor'), p.baseColorFactor || [.7, .7, .7, 1]); gl.uniform1f(gl.getUniformLocation(program, 'uRoughness'), p.roughnessFactor || .8); gl.uniform1f(gl.getUniformLocation(program, 'uMetal'), p.metallicFactor || 0); gl.uniform1f(gl.getUniformLocation(program, 'uGlass'), m.glass ? 1 : 0); + gl.uniform1f(gl.getUniformLocation(program, 'uSurfaceKind'), m.surfaceKind); + gl.uniform1f(gl.getUniformLocation(program, 'uInteriorMaterial'), m.interiorMaterial ? 1 : 0); + gl.uniform1f(gl.getUniformLocation(program, 'uInteriorFill'), interior.enabled && interior.fill ? .30 : 0); + gl.uniform1f(gl.getUniformLocation(program, 'uInteriorStyle'), m.interiorStyle); + if (m.surfaceKind === 3) {gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,hardieTextures[p.baseColorTexture.index]);} + if (m.surfaceKind === 1) { + const mat = gltf.materials[m.material]; + gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, hardieTextures[p.baseColorTexture.index]); gl.uniform1i(gl.getUniformLocation(program, 'uBaseMap'), 1); + gl.activeTexture(gl.TEXTURE2); gl.bindTexture(gl.TEXTURE_2D, hardieTextures[mat.normalTexture.index]); gl.uniform1i(gl.getUniformLocation(program, 'uNormalMap'), 2); + gl.uniform1f(gl.getUniformLocation(program, 'uNormalScale'), mat.normalTexture.scale ?? 1); + } + gl.bindVertexArray(m.vao); gl.drawElements(gl.TRIANGLES, m.count, m.type, m.offset); } + gl.depthMask(true);gl.disable(gl.BLEND);gl.bindVertexArray(null); +} + for(const node of gltf.nodes){ + const different=(values,base)=>values&&values.some((v,i)=>Math.abs(v-base[i])>1e-6); + if(different(node.translation,[0,0,0])||different(node.rotation,[0,0,0,1])||different(node.scale,[1,1,1])||different(node.matrix,[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]))throw Error('Model has edited transforms; enable the Three.js renderer.'); + } + initGL(); await loadHardieTextures(); + return {backend:'local-webgl',label:'Lokalni WebGL / zaloha',meshes,images:hardieImages, + resize(w,h,dpr){canvas.width=Math.round(w*dpr);canvas.height=Math.round(h*dpr);}, + draw(f){frame=f;({solar,interior,eye,vp,lightVP}=f);render();}, + dispose(){hardieObjectURLs.forEach(URL.revokeObjectURL);} + }; +} diff --git a/locenice/locenice/js/legacy-shaders.js b/locenice/locenice/js/legacy-shaders.js new file mode 100644 index 0000000..fe79a9f --- /dev/null +++ b/locenice/locenice/js/legacy-shaders.js @@ -0,0 +1,33 @@ +// Retained offline fallback shaders from the supplied prototype. +const vs = `#version 300 es +precision highp float; +layout(location=0) in vec3 position; +layout(location=1) in vec3 normal; +layout(location=2) in vec2 texCoord; +layout(location=3) in vec4 tangent; +uniform mat4 uVP; uniform mat4 uLightVP; uniform vec2 uUvScale; +out vec3 vNormal; out vec3 vWorld; out vec4 vShadow; out vec2 vUv; out vec4 vTangent; +void main(){vUv=texCoord*uUvScale;vTangent=tangent;vWorld=position;vNormal=normal;vShadow=uLightVP*vec4(position,1.);gl_Position=uVP*vec4(position,1.);}`; + const fs = `#version 300 es +precision highp float; +in vec3 vNormal; in vec3 vWorld; in vec4 vShadow; in vec2 vUv; in vec4 vTangent; +uniform vec4 uColor; uniform vec3 uEye; uniform vec3 uLight; +uniform float uInteriorMaterial; uniform float uInteriorFill; uniform float uInteriorStyle; +uniform float uSurfaceKind; uniform sampler2D uBaseMap; uniform sampler2D uNormalMap; uniform float uNormalScale; +uniform sampler2D uShadow; uniform float uGlass; uniform float uMetal; uniform float uRoughness; +uniform vec3 uAmbient; uniform vec3 uSunColor; uniform float uDirect; uniform float uSkyDay; uniform float uShadows; +out vec4 outColor; +vec3 srgbToLinear(vec3 c){return mix(c/12.92,pow((c+.055)/1.055,vec3(2.4)),step(vec3(.04045),c));} +float shade(vec3 n){if(uShadows<.5||uDirect<.001)return 1.;vec3 p=vShadow.xyz/vShadow.w*.5+.5;if(p.x<0.||p.x>1.||p.y<0.||p.y>1.||p.z>1.||p.z<0.)return 1.;float bias=max(.00045*(1.-max(dot(n,uLight),0.)),.00014);vec2 grad=vec2(0.);if(uSurfaceKind>.5){vec3 dx=dFdx(p),dy=dFdy(p);float det=dx.x*dy.y-dx.y*dy.x;if(abs(det)>1e-12)grad=vec2(dx.z*dy.y-dy.z*dx.y,dx.x*dy.z-dy.x*dx.z)/det;}float s=0.;for(int x=-1;x<=1;x++)for(int y=-1;y<=1;y++){vec2 uv=p.xy+vec2(float(x),float(y))/2048.;float d=texture(uShadow,uv).r;vec2 center=(floor(uv*2048.)+.5)/2048.;float receiver=p.z+dot(grad,center-p.xy);s+=receiver-bias<=d?1.:0.;}return s/9.;} +void main(){vec3 n=normalize(vNormal);if(!gl_FrontFacing)n=-n;vec3 gn=n; +if(uSurfaceKind>.5&&uSurfaceKind<1.5){vec3 t=normalize(vTangent.xyz-n*dot(n,vTangent.xyz));vec3 bt=normalize(cross(n,t))*vTangent.w;vec3 mn=texture(uNormalMap,vUv).xyz*2.-1.;mn.xy*=uNormalScale;n=normalize(mat3(t,bt,n)*mn);} +vec3 view=normalize(uEye-vWorld);float ndl=max(dot(n,uLight),0.);float sh=shade(gn);vec3 base=uSurfaceKind>.5?uColor.rgb:pow(uColor.rgb,vec3(2.2)); +if((uSurfaceKind>.5&&uSurfaceKind<1.5)||uSurfaceKind>2.5)base*=srgbToLinear(texture(uBaseMap,vUv).rgb);vec3 ambient=uAmbient*(.84+.16*n.y)+vec3(uInteriorMaterial*uInteriorFill);vec3 col=base*(ambient+uSunColor*ndl*sh*uDirect);vec3 halfway=normalize(uLight+view);float spec=pow(max(dot(n,halfway),0.),mix(90.,8.,uRoughness));col+=uSunColor*spec*sh*(.018+uMetal*.15)*uDirect; +if(uGlass>.5){vec3 reflected=reflect(-view,n);float fres=pow(1.-max(dot(n,view),0.),4.);vec3 skycol=mix(vec3(.19,.26,.26),vec3(.50,.60,.64),smoothstep(-.25,.8,reflected.y));skycol*=mix(.035,1.,uSkyDay);col=mix(base*(ambient*.75),skycol,.32+fres*.48);col+=uSunColor*.14*pow(max(dot(n,halfway),0.),100.)*sh*uDirect;} +if(uInteriorStyle>.5)col=max(col,base*.85); +col=pow(max(col,vec3(0.)),vec3(1./2.2));outColor=vec4(clamp(col,0.,1.),uColor.a);}`; + const depthvs = `#version 300 es +precision highp float;layout(location=0) in vec3 position;uniform mat4 uVP;void main(){gl_Position=uVP*vec4(position,1.);}`; + const depthfs = `#version 300 es +precision highp float;void main(){}`; +export {vs, fs, depthvs, depthfs}; diff --git a/locenice/locenice/js/model-io.js b/locenice/locenice/js/model-io.js new file mode 100644 index 0000000..f21c0b6 --- /dev/null +++ b/locenice/locenice/js/model-io.js @@ -0,0 +1,17 @@ +/** Minimal glTF 2.0 container I/O, independent of either rendering backend. */ +export function parseGLB(bytes){ + if(!(bytes instanceof Uint8Array)||bytes.byteLength<28)throw Error('Invalid GLB length'); + const v=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength); + if(v.getUint32(0,true)!==0x46546c67||v.getUint32(4,true)!==2)throw Error('Expected glTF 2.0'); + if(v.getUint32(8,true)!==bytes.byteLength)throw Error('GLB length mismatch'); + let gltf=null,binary=null,off=12; + while(offbytes.length)throw Error('Invalid GLB chunk');const size=v.getUint32(off,true),type=v.getUint32(off+4,true);if(size%4||off+8+size>bytes.length)throw Error('Invalid GLB chunk bounds'); + const part=bytes.subarray(off+8,off+8+size);if(type===0x4e4f534a)gltf=JSON.parse(new TextDecoder().decode(part));else if(type===0x004e4942)binary=part;off+=8+size;} + if(!gltf||!binary)throw Error('Missing GLB data');return {gltf,binary}; +} +export function encodeGLB(gltf,binary){ + const text=new TextEncoder().encode(JSON.stringify(gltf)),jl=(text.length+3)&~3,bl=(binary.length+3)&~3; + const out=new Uint8Array(28+jl+bl),v=new DataView(out.buffer); + [0x46546c67,2,out.length,jl,0x4e4f534a].forEach((n,i)=>v.setUint32(4*i,n,true));out.fill(32,20,20+jl);out.set(text,20); + v.setUint32(20+jl,bl,true);v.setUint32(24+jl,0x004e4942,true);out.set(binary,28+jl);return out; +} diff --git a/locenice/locenice/js/project-panel.js b/locenice/locenice/js/project-panel.js new file mode 100644 index 0000000..3f0a307 --- /dev/null +++ b/locenice/locenice/js/project-panel.js @@ -0,0 +1,20 @@ +/** Source references and non-geodetic survey overlay. Does not alter house dimensions. */ +export function installProjectPanel({resources,backend,getMode,focusSite,invalidate}){ + const $=s=>document.querySelector(s),dialog=$('#projectDialog'),overlay=$('#surveyOverlay');let survey=false; + const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + $('#projectToggle').onclick=()=>dialog.showModal();$('#projectClose').onclick=()=>dialog.close(); + dialog.addEventListener('click',e=>{if(e.target===dialog){const r=dialog.getBoundingClientRect();if(e.clientXr.right||e.clientYr.bottom)dialog.close();}}); + const photos=[['kitchen1','Kuchy\u0148 / zelen\u00e1 a dub'],['kitchen2','Kuchy\u0148 / vnit\u0159n\u00ed strana'],['view1','Ob\u00fdvac\u00ed pokoj / tr\u00e1my'],['view2','Ob\u00fdvac\u00ed pokoj / vybaven\u00ed']]; + $('#projectPhotos').innerHTML=photos.map(([key,title])=>``).join(''); + $('#projectPhotos').onclick=e=>{const key=e.target.closest('[data-photo]')?.dataset.photo;if(!key)return;const r=resources.references[key];dialog.close();$('#interiorReferenceTitle').textContent=r.title;$('#interiorReferenceImage').src=r.src;$('#interiorReferenceImage').alt=r.title;$('#interiorReferenceCaption').textContent=r.caption;$('#interiorReferenceDialog').showModal();}; + const site=resources.site; + $('#surveyStats').innerHTML=`
${site.cadastralAreaInProject.toLocaleString('cs-CZ')} m\u00b2V\u00fdm\u011bra dle geometrick\u00e9ho pl\u00e1nu
${site.digitizedOutlineArea.toLocaleString('cs-CZ')} m\u00b2Obrys dosavadn\u00edho 3D modelu
${site.referenceElevation.toLocaleString('cs-CZ',{minimumFractionDigits:3})} mV\u00fd\u0161kov\u00e1 nula / Bpv
${site.surveyPoints.length} bod\u016fV\u00fd\u0161kopis / grafick\u00fd ode\u010det polohy
`; + $('#surveyShow').onclick=()=>{survey=!survey;$('#surveyShow').textContent=survey?'Skr\u00fdt body v\u00fd\u0161kopisu':'Zobrazit body v modelu';dialog.close();if(survey)focusSite();invalidate();}; + $('#backendNote').textContent=backend.backend==='three'?'Aktivn\u00ed renderer: Three.js r180. Knihovna se na\u010d\u00edt\u00e1 z CDN; model i podklady z va\u0161eho hostingu.':'Aktivn\u00ed renderer: lok\u00e1ln\u00ed WebGL. Three.js se nepoda\u0159ilo na\u010d\u00edst nebo byl zvolen z\u00e1lo\u017en\u00ed re\u017eim. Model a konfigur\u00e1tor funguj\u00ed bez CDN.'; + return {drawSurvey(project,clip,width,height){ + if(!survey||getMode().enabled){overlay.style.display='none';return;} + overlay.style.display='block';overlay.setAttribute('viewBox',`0 0 ${width} ${height}`);overlay.style.width=width+'px';overlay.style.height=height+'px'; + const occupied=[]; + overlay.innerHTML=site.surveyPoints.map(p=>{const xyz=[p.xz[0],p.altitude-site.referenceElevation+.10,p.xz[1]],q=clip(xyz);if(q[3]<=0)return '';const [x,y]=project(xyz);if(x<0||y<0||x>width||y>height)return '';const value=p.altitude.toFixed(2).replace('.',',');let label='';for(const [dx,dy]of [[6,-6],[6,17],[-78,-6]]){const box={x:x+dx,y:y+dy-11,w:77,h:15};if(box.x<4||box.y<4||box.x+box.w>width-4||box.y+box.h>height-4)continue;if(occupied.some(b=>box.xb.x&&box.yb.y))continue;occupied.push(box);label=`${p.id} / ${value}`;break;}return `${p.id} / ${value} m Bpv${label}`;}).join(''); + }}; +} diff --git a/locenice/locenice/js/solar.js b/locenice/locenice/js/solar.js new file mode 100644 index 0000000..cd8bd7e --- /dev/null +++ b/locenice/locenice/js/solar.js @@ -0,0 +1,23 @@ +// Pure function retained from the supplied Meeus / NOAA implementation. +// Arguments: UTC Unix milliseconds, degrees latitude, degrees longitude. +const RAD=Math.PI/180,DAY_MS=86400000; +const mod=(x,n)=>((x%n)+n)%n,clamp=(x,a,b)=>Math.max(a,Math.min(b,x)); +export function solarPosition(ms, lat, lon) { + const jd = ms / DAY_MS + 2440587.5, t = (jd - 2451545) / 36525; + const L = mod(280.46646 + t * (36000.76983 + t * .0003032), 360) * RAD; + const M = (357.52911 + t * (35999.05029 - .0001537 * t)) * RAD; + const e = .016708634 - t * (.000042037 + .0000001267 * t); + const C = Math.sin(M) * (1.914602 - t * (.004817 + .000014 * t)) + Math.sin(2 * M) * (.019993 - .000101 * t) + Math.sin(3 * M) * .000289; + const omega = (125.04 - 1934.136 * t) * RAD; + const lambda = L + (C - .00569 - .00478 * Math.sin(omega)) * RAD; + const mean = 23 + (26 + (21.448 - t * (46.815 + t * (.00059 - t * .001813))) / 60) / 60; + const obliq = (mean + .00256 * Math.cos(omega)) * RAD; + const dec = Math.asin(Math.sin(obliq) * Math.sin(lambda)); + const y = Math.tan(obliq / 2) ** 2; + const eq = 4 / RAD * (y * Math.sin(2 * L) - 2 * e * Math.sin(M) + 4 * e * y * Math.sin(M) * Math.cos(2 * L) - .5 * y * y * Math.sin(4 * L) - 1.25 * e * e * Math.sin(2 * M)); + const solarMinutes = mod(mod(ms, DAY_MS) / 60000 + eq + 4 * lon, 1440); + const H = (solarMinutes / 4 - 180) * RAD, phi = lat * RAD; + const elevation = Math.asin(clamp(Math.sin(phi) * Math.sin(dec) + Math.cos(phi) * Math.cos(dec) * Math.cos(H), -1, 1)) / RAD; + const azimuth = mod(Math.atan2(Math.sin(H), Math.cos(H) * Math.sin(phi) - Math.tan(dec) * Math.cos(phi)) / RAD + 180, 360); + return { elevation, azimuth, declination: dec / RAD, equationOfTime: eq }; + } diff --git a/locenice/locenice/js/three-renderer.js b/locenice/locenice/js/three-renderer.js new file mode 100644 index 0000000..a307cc2 --- /dev/null +++ b/locenice/locenice/js/three-renderer.js @@ -0,0 +1,114 @@ +/** Standard Three.js backend. UI and catalogue never depend on THREE internals. */ +export async function createThreeRenderer({canvas,gltf,binary,bytes,config}) { + let timer; + const modules=Promise.all([import(config.threeModule),import(config.threeLoader)]); + const [T,{GLTFLoader}]=await Promise.race([modules,new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error('Three.js CDN timeout')),config.cdnTimeoutMs||6500);})]).finally(()=>clearTimeout(timer)); + const loader=new GLTFLoader(); + const documentModel=await loader.parseAsync(bytes.buffer.slice(bytes.byteOffset,bytes.byteOffset+bytes.byteLength),''); + const scene=new T.Scene();scene.add(documentModel.scene); + const renderer=new T.WebGLRenderer({canvas,antialias:true,alpha:false,preserveDrawingBuffer:true,powerPreference:'high-performance'}); + renderer.outputColorSpace=T.SRGBColorSpace; + renderer.toneMapping=T.ACESFilmicToneMapping;renderer.toneMappingExposure=1.05; + renderer.shadowMap.enabled=true;renderer.shadowMap.type=T.PCFSoftShadowMap;renderer.shadowMap.autoUpdate=false; + const ambient=new T.AmbientLight(0xffffff,.7); + const skyLight=new T.HemisphereLight(0xf5f8ff,0x67604e,1.7); + const sun=new T.DirectionalLight(0xffffff,3.0); + const fill=new T.HemisphereLight(0xfff5df,0x77716a,0); + scene.add(ambient,skyLight,sun,sun.target,fill); + sun.castShadow=true;sun.shadow.mapSize.set(2048,2048);sun.shadow.bias=-.00008;sun.shadow.normalBias=.025; + const persp=new T.PerspectiveCamera(),ortho=new T.OrthographicCamera(); + const textures=await Promise.all((gltf.textures||[]).map((_,i)=>documentModel.parser.getDependency('texture',i))); + const images=[]; + for (let i=0;i{ + if(!obj.isMesh)return; + const association=associations.get(obj)||{}; + let parent=obj,nodeIndex=association.nodes; + while(nodeIndex===undefined&&parent){const a=associations.get(parent);if(a?.nodes!==undefined)nodeIndex=a.nodes;parent=parent.parent;} + if(nodeIndex===undefined)return; + const node=gltf.nodes[nodeIndex],meshSpec=gltf.meshes[node.mesh]; + const primitiveIndex=association.primitives||0; + const primitive=meshSpec.primitives[primitiveIndex]||meshSpec.primitives[0]; + const materialIndex=primitive.material; + if(materialIndex===undefined)return; + const spec=gltf.materials[materialIndex],name=node.name||meshSpec.name||obj.name; + // Separate copies prevent a material variant from changing unrelated objects. + let material=matCopies.get(materialIndex); + if(!material){material=obj.material.clone();material.side=T.DoubleSide;matCopies.set(materialIndex,material);} + obj.material=material; + const box=new T.Box3().setFromObject(obj); + const pbr=spec.pbrMetallicRoughness||{}; + const desc={object:obj,std:node.extras||{},transparent:spec.alphaMode==='BLEND',material:materialIndex,name,nodeIndex, + count:obj.geometry.index?.count||obj.geometry.attributes.position.count,context:/^(90_|91_|28_|23_)/.test(name), + legacySite:!!node.extras?.legacySite,siteLayer:node.extras?.siteLayer||null,castShadow:node.extras?.castShadow!==false, + bounds:{min:box.min.toArray(),max:box.max.toArray()},glass:(spec.name||'').startsWith('glass'), + interiorLayer:node.extras?.interiorLayer||null,interiorMaterial:!!spec.extras?.interiorMaterial, + interiorStyle:spec.extras?.interiorStyle||0,pbr}; + obj.castShadow=desc.castShadow&&!desc.glass&&!desc.transparent;obj.receiveShadow=!desc.transparent; + if(desc.transparent){material.transparent=true;material.depthWrite=false;} + entries.push(desc); + }); + const stamps=new Map(),textureCopies=new Map(); + function localTexture(index,role,materialIndex,scale) { + const key=materialIndex+':'+role+':'+index+':'+scale.join(','); + if(!textureCopies.has(key)){ + const tx=textures[index]?.clone();if(!tx)return null; + tx.colorSpace=role==='map'?T.SRGBColorSpace:T.NoColorSpace; + tx.repeat.set(...scale);tx.needsUpdate=true;textureCopies.set(key,tx); + } + return textureCopies.get(key); + } + function syncMaterials(){ + for(const [index,mat] of matCopies){ + const spec=gltf.materials[index],p=spec.pbrMetallicRoughness||{}; + const stamp=JSON.stringify([p,spec.normalTexture,spec.emissiveFactor,spec.name]);if(stamps.get(index)===stamp)continue;stamps.set(index,stamp); + const rgba=p.baseColorFactor||[.7,.7,.7,1];mat.color.setRGB(...rgba.slice(0,3)); + // Legacy exterior factors were sRGB; tagged PBR / interior factors are already linear. + if(!spec.extras?.interiorMaterial&&!spec.extras?.linearColor&&!spec.extras?.hardieRole)mat.color.convertSRGBToLinear(); + mat.opacity=rgba[3]??1;mat.roughness=p.roughnessFactor??.8;mat.metalness=p.metallicFactor??0; + const tex=p.baseColorTexture; + mat.map=tex?localTexture(tex.index,'map',index,tex.extensions?.KHR_texture_transform?.scale||[1,1]):null; + mat.normalMap=spec.normalTexture?localTexture(spec.normalTexture.index,'normalMap',index,[1,1]):null; + if(mat.normalScale&&spec.normalTexture)mat.normalScale.setScalar(spec.normalTexture.scale??1); + if(spec.extras?.interiorStyle===1){mat.emissive.copy(mat.color);mat.emissiveIntensity=.75;} + mat.needsUpdate=true; + } + } + const temp=new T.Vector3();let oldVisible='',oldInterior=''; + function shadowFit(frame){ + const low=[Infinity,Infinity,Infinity],high=[-Infinity,-Infinity,-Infinity]; + for(const m of entries){if(!m.object.visible||m.glass)continue;for(let a=0;a<3;a++){low[a]=Math.min(low[a],m.bounds.min[a]);high[a]=Math.max(high[a],m.bounds.max[a]);}} + if(!Number.isFinite(low[0]))return; + if(frame.interior.enabled){low.splice(0,3,-9,-1,-6);high.splice(0,3,11,6,6);} + const center=low.map((v,i)=>(v+high[i])/2); + sun.target.position.fromArray(center);sun.position.fromArray(center).addScaledVector(temp.fromArray(frame.solar.light),120); + sun.target.updateMatrixWorld();sun.updateMatrixWorld(); + const c=sun.shadow.camera;c.near=.1;c.far=320;c.position.copy(sun.position);c.up.set(0,1,0);c.lookAt(sun.target.position);c.updateMatrixWorld(); + let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity; + for(let a=0;a<8;a++){temp.set(a&1?high[0]:low[0],a&2?high[1]:low[1],a&4?high[2]:low[2]).applyMatrix4(c.matrixWorldInverse);minX=Math.min(minX,temp.x);maxX=Math.max(maxX,temp.x);minY=Math.min(minY,temp.y);maxY=Math.max(maxY,temp.y);} + c.left=minX-2;c.right=maxX+2;c.bottom=minY-2;c.top=maxY+2;c.updateProjectionMatrix();renderer.shadowMap.needsUpdate=true; + } + return {backend:'three',label:'Three.js r'+T.REVISION,meshes:entries,images, + resize(w,h,dpr){renderer.setPixelRatio(dpr);renderer.setSize(w,h,false);}, + draw(frame){ + syncMaterials(); + let key='';for(const m of entries){m.object.visible=!!frame.visible(m);key+=m.object.visible?'1':'0';} + if(frame.solar.shadowDirty||key!==oldVisible||oldInterior!==frame.interior.mode){shadowFit(frame);oldVisible=key;oldInterior=frame.interior.mode;} + sun.intensity=Math.max(0,frame.solar.direct)*3.0;sun.color.fromArray(frame.solar.sunColor);sun.castShadow=frame.solar.shadows; + const day=frame.solar.dayFactor;skyLight.intensity=.18+1.5*day;ambient.intensity=.12+.45*day; + fill.intensity=frame.interior.enabled&&frame.interior.fill?1.3:0; + scene.background=new T.Color().setRGB(...frame.solar.sky).convertSRGBToLinear(); + const camera=frame.orthographic?ortho:persp; + camera.position.fromArray(frame.eye);camera.up.set(0,1,0);camera.lookAt(temp.fromArray(frame.lookTarget));camera.updateMatrixWorld(); + camera.projectionMatrix.fromArray(frame.vp).multiply(camera.matrixWorld);camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert(); + renderer.render(scene,camera); + }, + dispose(){renderer.dispose();images.forEach(i=>i?.src&&URL.revokeObjectURL(i.src));textureCopies.forEach(t=>t.dispose());} + }; +} diff --git a/locenice/locenice/models/dum.glb b/locenice/locenice/models/dum.glb new file mode 100644 index 0000000..7ef6d9f Binary files /dev/null and b/locenice/locenice/models/dum.glb differ diff --git a/locenice/tests/data_checks.mjs b/locenice/tests/data_checks.mjs new file mode 100644 index 0000000..b1ae2d8 --- /dev/null +++ b/locenice/tests/data_checks.mjs @@ -0,0 +1,27 @@ +import fs from 'node:fs'; +import assert from 'node:assert/strict'; +import {parseGLB,encodeGLB} from '../locenice/js/model-io.js'; +import {solarPosition} from '../locenice/js/solar.js'; +const root=new URL('../locenice/',import.meta.url); +const bytes=new Uint8Array(fs.readFileSync(new URL('models/dum.glb',root))); +const {gltf,binary}=parseGLB(bytes); +const catalog=JSON.parse(fs.readFileSync(new URL('data/catalog.json',root),'utf8')); +let count=0;function check(name,test){assert.ok(test,name);count++;console.log('PASS',name)} +check('GLB roundtrip',parseGLB(encodeGLB(gltf,binary)).gltf.nodes.length===gltf.nodes.length); +check('source model has 9 configurable rectangular windows',gltf.extras.allstavStandards.windows.length===9); +check('source model has 9 rooms',gltf.extras.interiorStudy.rooms.length===9); +check('default entry',catalog.defaults.entryModel==='three'&&catalog.defaults.entryColor==='7016'); +check('default gutters',catalog.defaults.gutter==='015'); +check('default roof',catalog.defaults.roof==='darkgray'); +check('default sills',catalog.defaults.extSill==='anthracite'); +check('blinds off at startup',catalog.defaults.blinds===false); +check('stable IDs unique',new Set(gltf.nodes.map(n=>n.extras.loceniceId)).size===gltf.nodes.length); +const excluded=gltf.extras.allstavStandards.triangleExcludedNode; +check('triangle has no blinds',!gltf.nodes.some(n=>n.extras?.stdBlind&&n.extras.windowNode===excluded)); +for(const [role,indices]of Object.entries(gltf.extras.allstavStandards.materials))check('material role '+role,indices.length>0&&indices.every(i=>gltf.materials[i])); +for(const file of ['assets.json','references.json']){ + const assets=JSON.parse(fs.readFileSync(new URL('data/'+file,root),'utf8')); + for(const [id,value]of Object.entries(assets)){const uri=typeof value==='string'?value:value.src;check('asset '+id,fs.existsSync(new URL(uri,root)));} +} +check('June noon sun',solarPosition(Date.UTC(2026,5,21,11),48.8242,14.5275).elevation>60); +console.log('All checks passed:',count); diff --git a/locenice/tools/prepare_export.py b/locenice/tools/prepare_export.py new file mode 100644 index 0000000..e39605f --- /dev/null +++ b/locenice/tools/prepare_export.py @@ -0,0 +1,109 @@ +"""Prepare a Blender GLB export for the configurator. + +python tools/prepare_export.py path/to/edited.glb --out prepared +Produces prepared/dum.glb and prepared/scene.json. Keeps the original complete +catalogue texture library, rebinds material/node indices using loceniceId. +It does not update floor polygons, collision volumes, windows or structural design. +""" +from pathlib import Path +from copy import deepcopy +import argparse, json, struct +ROOT=Path(__file__).resolve().parent.parent + +def read_glb(path): + data=Path(path).read_bytes() + if len(data)<28 or struct.unpack_from('len(data):raise ValueError('Truncated GLB') + chunk=data[offset+8:offset+8+size] + if kind==0x4e4f534a:g=json.loads(chunk) + elif kind==0x004e4942:b=chunk + offset+=8+size + if g is None or b is None:raise ValueError('Missing JSON or binary chunk') + return g,b + +def write_glb(path,g,b): + j=json.dumps(g,ensure_ascii=False,separators=(',',':')).encode();j+=b' '*((-len(j))%4) + b=bytes(b);b+=b'\0'*((-len(b))%4) + Path(path).write_bytes(struct.pack('