Files
smarthome/Wohnung 3D Komplett.html
T
2026-09-25 12:23:27 +02:00

14 lines
215 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover">
<title>Wohnung – 3D Miniatur</title>
<style>html,body{margin:0;background:#efece6}</style></head><body>
<script>
(function(){
var DESKTOP="<!DOCTYPE html>\n<html lang=\"de\">\n<head>\n<meta charset=\"utf-8\">\n<title>Wohnung – 3D Miniatur<\/title>\n<script type=\"importmap\">\n{\n \"imports\": {\n \"three\": \"https://unpkg.com/three@0.184.0/build/three.module.js\",\n \"three/addons/controls/OrbitControls.js\": \"https://unpkg.com/three@0.184.0/examples/jsm/controls/OrbitControls.js\",\n \"three/addons/exporters/OBJExporter.js\": \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/OBJExporter.js\",\n \"three/addons/exporters/GLTFExporter.js\": \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/GLTFExporter.js\",\n \"three/examples/jsm/\": \"https://unpkg.com/three@0.184.0/examples/jsm/\",\n \"three/addons/\": \"https://unpkg.com/three@0.184.0/examples/jsm/\",\n \"three-mesh-bvh\": \"https://cdn.jsdelivr.net/npm/three-mesh-bvh@0.9.5/build/index.module.js\",\n \"three-gpu-pathtracer\": \"https://cdn.jsdelivr.net/npm/three-gpu-pathtracer@0.0.24/build/index.module.js\",\n \"n8ao\": \"https://cdn.jsdelivr.net/npm/n8ao@1/dist/N8AO.js\",\n \"postprocessing\": \"https://cdn.jsdelivr.net/npm/postprocessing@6/build/index.js\"\n },\n \"integrity\": {\n \"https://unpkg.com/three@0.184.0/build/three.module.js\": \"sha384-8FCZ1eVO6it4+pbec2aDtnTrwjWXZLJRC+MAGCIPDgsYnUrl/E0A2YlF8ioMKI/J\",\n \"https://unpkg.com/three@0.184.0/build/three.core.js\": \"sha384-dw2ooPewaEIrAgl6oFDBmmBWCE9oW9LxRGcfwZ0hLvEprzo202wXl7vCYHRlSnOT\",\n \"https://unpkg.com/three@0.184.0/examples/jsm/controls/OrbitControls.js\": \"sha384-4rziNxOBZKQ69i+w+f89KJ55TCYquwchVbByQwmaOeIOXdOU2PLDn3kOfXHwIJC9\",\n \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/OBJExporter.js\": \"sha384-nbwtoZENJD3Vq+ACK0CuGQdPMuDWHkamC2KJD70EV5nfg6jQjfppKOea07YJN+N3\",\n \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/GLTFExporter.js\": \"sha384-VofkvpG6HERhFCYbsUOHeNXBCqID2nfqkQqnVzE1jc/oPcz+qJ13ADdXH08hE+cQ\"\n }\n}\n<\/script>\n<style>\n html,body{margin:0;height:100%;background:#efece6;font-family:Helvetica,Arial,sans-serif}\n three-d-stage:not(:defined){visibility:hidden}\n #rooms{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);display:flex;gap:6px;padding:6px;background:rgba(250,248,244,.82);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-radius:999px;box-shadow:0 6px 24px rgba(30,28,24,.14)}\n #rooms button{appearance:none;border:0;background:transparent;color:#2f2d2a;font:500 14px/1 Helvetica,Arial,sans-serif;padding:11px 18px;border-radius:999px;cursor:pointer;transition:background .2s,color .2s}\n #rooms button:hover{background:rgba(47,45,42,.08)}\n #rooms button.on{background:#2f2d2a;color:#f7f5f0}\n #pov{position:fixed;left:50%;top:18px;transform:translate(-50%,-8px);opacity:0;pointer-events:none;transition:opacity .3s,transform .3s;display:flex;gap:4px;padding:4px;background:rgba(250,248,244,.82);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-radius:999px;box-shadow:0 1px 2px rgba(0,0,0,.06),0 6px 20px rgba(0,0,0,.08)}\n #pov.show{opacity:1;pointer-events:auto;transform:translate(-50%,0)}\n #pov button{appearance:none;border:0;background:transparent;color:#2f2d2a;font:500 13px/1 Helvetica,Arial,sans-serif;padding:9px 14px;border-radius:999px;cursor:pointer;transition:background .2s,color .2s}\n #pov button:hover{background:rgba(47,45,42,.08)}\n #pov button.on{background:#2f2d2a;color:#f7f5f0}\n #lamp{position:fixed;left:50%;bottom:82px;transform:translate(-50%,10px);opacity:0;pointer-events:none;transition:opacity .35s,transform .35s;display:flex;align-items:center;gap:4px;padding:4px;border-radius:999px;background:rgba(250,248,244,.82);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:#2f2d2a;font:500 14px/1 Helvetica,Arial,sans-serif;box-shadow:0 4px 18px rgba(0,0,0,.08)}\n #lampT{display:flex;align-items:center;gap:10px;appearance:none;border:0;background:transparent;color:inherit;font:inherit;padding:7px 14px 7px 10px;border-radius:999px;cursor:pointer;white-space:nowrap}\n #lampT:hover{background:rgba(47,45,42,.08)}\n #lampCtl{display:flex;align-items:center;gap:14px;max-width:0;overflow:hidden;opacity:0;transition:max-width .4s,opacity .3s,padding .4s;padding:0}\n #lamp.on #lampCtl{max-width:420px;opacity:1;padding:0 12px 0 6px}\n #lampSw{display:flex;gap:6px}\n #lampSw button{appearance:none;width:22px;height:22px;border-radius:50%;border:2px solid rgba(250,248,244,.9);box-shadow:0 0 0 1px rgba(47,45,42,.18);cursor:pointer;padding:0}\n #lampSw button.sel{box-shadow:0 0 0 2px #2f2d2a}\n #lampSw{align-items:center;gap:8px;font-size:12px;color:#6b665e}\n #lampSw input[type=color]{appearance:none;-webkit-appearance:none;width:26px;height:26px;border-radius:50%;border:2px solid rgba(250,248,244,.9);box-shadow:0 0 0 1px rgba(47,45,42,.25);padding:0;cursor:pointer;background:none}\n #lampSw input[type=color]::-webkit-color-swatch-wrapper{padding:0}\n #lampSw input[type=color]::-webkit-color-swatch{border:0;border-radius:50%}\n #lampSw input[type=color]::-moz-color-swatch{border:0;border-radius:50%}\n #lampDim{display:flex;align-items:center;gap:8px;font-size:12px;color:#6b665e}\n #lampDim input{width:110px;accent-color:#2f2d2a}\n #lampDim output{width:34px;text-align:right;font-variant-numeric:tabular-nums}\n #lamp.show{opacity:1;transform:translate(-50%,0);pointer-events:auto}\n #lamp i{width:12px;height:12px;border-radius:50%;background:#bdb8ae;transition:background .3s,box-shadow .3s}\n #pt{position:fixed;top:18px;right:18px;padding:8px 12px;border-radius:999px;background:rgba(250,248,244,.82);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:#2f2d2a;font:500 12px/1 ui-monospace,Menlo,monospace;opacity:0;transition:opacity .3s;pointer-events:none}\n #pt.show{opacity:1}\n #pt{top:62px !important}\n #rt{position:fixed;top:18px;right:18px;display:flex;align-items:center;gap:8px;appearance:none;border:0;padding:9px 14px 9px 11px;border-radius:999px;background:rgba(250,248,244,.82);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:#2f2d2a;font:500 13px/1 Helvetica,Arial,sans-serif;cursor:pointer;box-shadow:0 1px 2px rgba(0,0,0,.06),0 6px 20px rgba(0,0,0,.08);transition:background .2s,color .2s}\n #rt i{width:10px;height:10px;border-radius:50%;background:#bdb8ae;transition:background .2s}\n #rt.on{background:#2f2d2a;color:#f7f5f0}\n #rt.on i{background:#7fd08a}\n #lamp.on i{background:#ffc86b;box-shadow:0 0 10px 3px rgba(255,190,90,.6)}\n #lampD{display:flex;align-items:center;gap:10px;appearance:none;border:0;background:transparent;color:inherit;font:inherit;padding:7px 14px 7px 10px;border-radius:999px;cursor:pointer;white-space:nowrap}\n #lampD:hover{background:rgba(47,45,42,.08)}\n #lampD b{width:12px;height:12px;border-radius:50%;background:#bdb8ae;transition:background .3s,box-shadow .3s}\n #lampD.on b{background:#ff9a3c;box-shadow:0 0 10px 3px rgba(255,150,60,.6)}\n #lamp2{position:fixed;left:50%;bottom:82px;transform:translate(-50%,10px);opacity:0;pointer-events:none;transition:opacity .35s,transform .35s;display:flex;align-items:center;gap:4px;padding:4px;border-radius:999px;background:rgba(250,248,244,.82);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:#2f2d2a;font:500 14px/1 Helvetica,Arial,sans-serif;box-shadow:0 4px 18px rgba(0,0,0,.08)}\n #lamp2.show{opacity:1;transform:translate(-50%,0);pointer-events:auto}\n #tripT{display:flex;align-items:center;gap:10px;appearance:none;border:0;background:transparent;color:inherit;font:inherit;padding:7px 14px 7px 10px;border-radius:999px;cursor:pointer;white-space:nowrap}\n #tripT:hover{background:rgba(47,45,42,.08)}\n #tripT i{width:12px;height:12px;border-radius:50%;background:#bdb8ae;transition:background .3s,box-shadow .3s}\n #tripCtl{display:flex;align-items:center;gap:14px;max-width:0;overflow:hidden;opacity:0;transition:max-width .4s,opacity .3s,padding .4s;padding:0}\n #lamp2.on #tripCtl{max-width:560px;opacity:1;padding:0 12px 0 6px}\n #tripCtl label{display:flex;align-items:center;gap:8px;font-size:12px;color:#6b665e}\n #tripCtl input[type=range]{width:100px;accent-color:#2f2d2a}\n #tripCtl output{width:34px;text-align:right;font-variant-numeric:tabular-nums}\n #tripCtl input[type=color]{appearance:none;-webkit-appearance:none;width:26px;height:26px;border-radius:50%;border:2px solid rgba(250,248,244,.9);box-shadow:0 0 0 1px rgba(47,45,42,.25);padding:0;cursor:pointer;background:none}\n #tripCtl input[type=color]::-webkit-color-swatch-wrapper{padding:0}\n #tripCtl input[type=color]::-webkit-color-swatch{border:0;border-radius:50%}\n #tripCtl input[type=color]::-moz-color-swatch{border:0;border-radius:50%}\n #rooms,#pov,#rt,#pt,#lamp,#lamp2,three-d-stage::part(toolbar){transition:opacity .6s}\n body.idle #rooms,body.idle #pov,body.idle #rt,body.idle #pt,body.idle #lamp,body.idle #lamp2{opacity:0 !important;pointer-events:none}\n body.idle{cursor:none}\n\n /* muavin-Gradient für GUI-Leisten */\n #rooms,#pov,#lamp,#lamp2,#pt,#rt{background-color:#000 !important;background-image:radial-gradient(circle at 65.34% 44.62%, rgba(95,95,95,1) 0%, rgba(95,95,95,.844) 8.53%, rgba(95,95,95,.5) 17.05%, rgba(95,95,95,.156) 25.58%, rgba(95,95,95,0) 34.1%),radial-gradient(circle at 28.07% 74.48%, rgba(53,54,48,1) 0%, rgba(53,54,48,.844) 11.41%, rgba(53,54,48,.5) 22.83%, rgba(53,54,48,.156) 34.24%, rgba(53,54,48,0) 45.65%),radial-gradient(circle at 52.42% 19.94%, rgba(56,122,56,1) 0%, rgba(56,122,56,.844) 14.39%, rgba(56,122,56,.5) 28.77%, rgba(56,122,56,.156) 43.16%, rgba(56,122,56,0) 57.55%),radial-gradient(circle at 80.31% 84.47%, rgba(0,0,0,1) 0%, rgba(0,0,0,.844) 17.27%, rgba(0,0,0,.5) 34.55%, rgba(0,0,0,.156) 51.82%, rgba(0,0,0,0) 69.1%) !important;background-size:100% 100% !important;color:#f2f1ec !important;box-shadow:0 6px 24px rgba(0,0,0,.25),inset 0 0 0 1px rgba(255,255,255,.08)}\n #rooms button,#pov button{color:#f2f1ec}\n #rooms button:hover,#pov button:hover,#lampT:hover,#lampD:hover,#tripT:hover{background:rgba(255,255,255,.1)}\n #rooms button.on,#pov button.on,#rt.on{background:#f2f1ec !important;background-image:none !important;color:#141412 !important}\n #lampSw,#lampDim,#tripCtl{color:#d9d7d0 !important}\n #lampSw button.sel{box-shadow:0 0 0 2px #f2f1ec}\n #lampDim input[type=range],#tripCtl input[type=range]{accent-color:#f2f1ec}\n .lp-sw{appearance:none;width:26px;height:26px;border-radius:50%;border:2px solid rgba(250,248,244,.9);box-shadow:0 0 0 1px rgba(47,45,42,.25);padding:0;cursor:pointer;flex:none}\n .lpick{position:fixed;z-index:50;width:236px;padding:14px;border-radius:18px;background:rgba(26,27,24,.38);backdrop-filter:blur(18px) saturate(1.2);-webkit-backdrop-filter:blur(18px) saturate(1.2);color:#f2f1ec;font:500 12px/1.2 Helvetica,Arial,sans-serif;box-shadow:0 10px 36px rgba(0,0,0,.35),inset 0 0 0 1px rgba(255,255,255,.08);display:none;flex-direction:column;gap:14px;transition:opacity .6s}\n .lpick.open{display:flex}\n body.lpfocus #rooms,body.lpfocus #pov,body.lpfocus #rt,body.lpfocus #pt,body.lpfocus #lamp,body.lpfocus #lamp2,body.lpfocus three-d-stage::part(toolbar){opacity:0 !important;pointer-events:none !important}\n .lp-done{appearance:none;border:0;border-radius:999px;padding:9px 0;background:rgba(255,255,255,.1);color:#f2f1ec;font:inherit;cursor:pointer}\n .lp-done:hover{background:rgba(255,255,255,.18)}\n body.idle .lpick{opacity:0;pointer-events:none}\n .lp-seg{display:grid;grid-template-columns:1fr 1fr;gap:3px;padding:3px;border-radius:999px;background:rgba(255,255,255,.08)}\n .lp-seg button{appearance:none;border:0;border-radius:999px;padding:7px 0;background:transparent;color:#d9d7d0;font:inherit;cursor:pointer}\n .lp-seg button.on{background:#f2f1ec;color:#141412}\n .lp-row{display:flex;flex-direction:column;gap:7px}\n .lp-row>div{display:flex;justify-content:space-between;color:#d9d7d0}\n .lp-row output{font-variant-numeric:tabular-nums;color:#f2f1ec}\n .lp-row small{display:flex;justify-content:space-between;color:#a9a69e;font-size:11px}\n .lpick input[type=range]{width:100%;margin:0;accent-color:#f2f1ec}\n .lpick input.lp-temp{appearance:none;-webkit-appearance:none;height:10px;border-radius:5px;outline:none}\n .lp-temp::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;border-radius:50%;background:#fff;border:2px solid #1a1b18;box-shadow:0 0 0 1px rgba(255,255,255,.6);cursor:pointer}\n .lp-temp::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:#fff;border:2px solid #1a1b18;cursor:pointer}\n .lp-wheel{position:relative;width:180px;height:180px;margin:0 auto;touch-action:none;cursor:pointer}\n .lp-wheel canvas{width:180px;height:180px;border-radius:50%;display:block}\n .lp-wheel i{position:absolute;width:18px;height:18px;margin:-9px 0 0 -9px;border-radius:50%;border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.4),0 2px 6px rgba(0,0,0,.4);pointer-events:none}\n<\/style>\n<\/head>\n<body>\n<three-d-stage name=\"wohnung\" background=\"#efece6\"><\/three-d-stage>\n<div id=\"rooms\"><\/div>\n<div id=\"pov\"><button data-m=\"bird\" class=\"on\">Vogelperspektive<\/button><button data-m=\"fp\">Frei umsehen<\/button><\/div>\n<button id=\"rt\" aria-pressed=\"false\"><i><\/i><span>Render aus<\/span><\/button>\n<div id=\"pt\"><\/div>\n<div id=\"lamp\"><button id=\"lampT\"><i><\/i><span>Bogenlampe an<\/span><\/button><div id=\"lampCtl\"><div id=\"lampSw\"><\/div><label id=\"lampDim\" style=\"display:none\"><span>Stärke<\/span><input type=\"range\" min=\"10\" max=\"200\" step=\"5\" value=\"100\"><output>100%<\/output><\/label><\/div><button id=\"lampD\" aria-pressed=\"false\"><b><\/b><span>Donut-Lampe an<\/span><\/button><\/div>\n<div id=\"lamp2\"><button id=\"tripT\"><i><\/i><span>Tripod-Lampe an<\/span><\/button><div id=\"tripCtl\"><label><button id=\"tripCol\" class=\"lp-sw\" title=\"Licht einstellen\"><\/button><\/label><label style=\"display:none\"><span>Stärke<\/span><input id=\"tripDim\" type=\"range\" min=\"10\" max=\"200\" step=\"5\" value=\"100\"><output>100%<\/output><\/label><label><span>Kegel<\/span><input id=\"tripCone\" type=\"range\" min=\"15\" max=\"80\" step=\"1\" value=\"32\"><output>32°<\/output><\/label><\/div><\/div>\n<script>// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design)\n// Copied omelette starter. Re-running copy_starter_component with this kind overwrites this file with the latest version (page content is unaffected).\n/* BEGIN USAGE */\n/**\n * <three-d-stage> — 3D object viewer + exporter shell (three.js).\n *\n * The stage owns the whole scene: WebGL renderer, neutral studio lighting\n * with a soft ground shadow, orbit controls (drag to orbit, wheel to zoom,\n * right-drag to pan), a camera auto-framed to the object's bounds, resize\n * handling, and a download toolbar that exports the current object as\n * OBJ + MTL or GLB (binary glTF). FBX cannot be exported in the browser;\n * GLB is the interchange format every modern 3D tool imports.\n *\n * three.js loads through the page's import map. Include this EXACT pinned\n * map in <head>, before any module runs — versions and integrity hashes\n * stay together (same map the \"3D object\" skill mandates):\n *\n * <script type=\"importmap\">\n * {\n * \"imports\": {\n * \"three\": \"https://unpkg.com/three@0.184.0/build/three.module.js\",\n * \"three/addons/controls/OrbitControls.js\": \"https://unpkg.com/three@0.184.0/examples/jsm/controls/OrbitControls.js\",\n * \"three/addons/exporters/OBJExporter.js\": \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/OBJExporter.js\",\n * \"three/addons/exporters/GLTFExporter.js\": \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/GLTFExporter.js\"\n * },\n * \"integrity\": {\n * \"https://unpkg.com/three@0.184.0/build/three.module.js\": \"sha384-8FCZ1eVO6it4+pbec2aDtnTrwjWXZLJRC+MAGCIPDgsYnUrl/E0A2YlF8ioMKI/J\",\n * \"https://unpkg.com/three@0.184.0/build/three.core.js\": \"sha384-dw2ooPewaEIrAgl6oFDBmmBWCE9oW9LxRGcfwZ0hLvEprzo202wXl7vCYHRlSnOT\",\n * \"https://unpkg.com/three@0.184.0/examples/jsm/controls/OrbitControls.js\": \"sha384-4rziNxOBZKQ69i+w+f89KJ55TCYquwchVbByQwmaOeIOXdOU2PLDn3kOfXHwIJC9\",\n * \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/OBJExporter.js\": \"sha384-nbwtoZENJD3Vq+ACK0CuGQdPMuDWHkamC2KJD70EV5nfg6jQjfppKOea07YJN+N3\",\n * \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/GLTFExporter.js\": \"sha384-VofkvpG6HERhFCYbsUOHeNXBCqID2nfqkQqnVzE1jc/oPcz+qJ13ADdXH08hE+cQ\"\n * }\n * }\n * <\\/script>\n *\n * Usage:\n * <style>three-d-stage:not(:defined){visibility:hidden}<\/style>\n * <three-d-stage name=\"rocket\"><\/three-d-stage>\n * <script src=\"three-d-stage.js\"><\\/script>\n * <script type=\"module\">\n * const stage = document.querySelector('three-d-stage');\n * const { THREE } = await stage.ready;\n * const model = new THREE.Group();\n * // …build the model out of named meshes with named materials —\n * // the names become the o / usemtl entries in the exported OBJ…\n * stage.setObject(model);\n * <\\/script>\n *\n * Attributes:\n * name — export file basename (default \"model\")\n * background — CSS color behind the scene (default a warm paper tone)\n * autorotate — when present, a slow turntable until the user interacts\n *\n * Model in real-world meters, centered on the origin, y-up — exports\n * inherit the scene's units and orientation. The stage fills its own box;\n * size it with ordinary CSS (default 100vw/100vh page hero).\n *\n * Default setup: neutral studio lighting (hemisphere + key + fill), a\n * soft ground shadow, and NO environment map — so high metalness has\n * nothing to reflect and renders near-black. Cap metalness around\n * 0.3–0.4 and carry a metal look with a brighter base color. The copied\n * file is yours: adjust the lights, shadow, or background in _boot()\n * when the object needs a different look.\n */\n/* END USAGE */\n\n(() => {\n const stylesheet = `\n :host {\n position: relative;\n display: block;\n width: 100%;\n height: 100vh;\n background: var(--stage-bg, #f0eee6);\n overflow: hidden;\n }\n canvas { display: block; outline: none; }\n .toolbar {\n position: absolute;\n right: 16px;\n bottom: 16px;\n display: flex;\n gap: 8px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n }\n .toolbar button {\n appearance: none;\n border: 1px solid rgba(20, 20, 19, 0.18);\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.92);\n color: #1a1915;\n font-family: inherit;\n font-size: 12.5px;\n font-weight: 500;\n line-height: 1;\n padding: 9px 12px;\n cursor: default;\n }\n .toolbar button:hover { background: #fff; }\n .toolbar button:active { transform: translateY(1px); }\n .toolbar button[disabled] { opacity: 0.5; pointer-events: none; }\n .note {\n position: absolute;\n left: 16px;\n bottom: 16px;\n max-width: 60%;\n font: 400 12px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n color: #1a1915;\n background: rgba(255, 255, 255, 0.85);\n padding: 4px 10px;\n border-radius: 6px;\n user-select: none;\n }\n .err {\n position: absolute;\n inset: 0;\n display: none;\n align-items: center;\n justify-content: center;\n padding: 24px;\n font: 500 14px/1.6 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n color: #8a2f20;\n text-align: center;\n white-space: pre-line;\n }\n `;\n\n function download(blob, filename) {\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n a.remove();\n setTimeout(() => URL.revokeObjectURL(url), 4000);\n }\n\n /** Tell the host an export attempt settled — telemetry only. The host\n * (HTMLViewer) verifies the source and re-reads these fields defensively\n * before counting; nothing else crosses the frame boundary. Guarded so\n * telemetry can never break the download path. */\n function notifyExport(format, ok) {\n try {\n window.parent.postMessage(\n { type: 'omelette:notify-3d-export', format: format, ok: ok === true },\n '*'\n );\n } catch (e) {}\n }\n\n class ThreeDStage extends HTMLElement {\n constructor() {\n super();\n const root = this.attachShadow({ mode: 'open' });\n const style = document.createElement('style');\n style.textContent = stylesheet;\n root.appendChild(style);\n this._err = document.createElement('div');\n this._err.className = 'err';\n root.appendChild(this._err);\n const note = document.createElement('div');\n note.className = 'note';\n note.textContent = 'Drag to orbit · scroll to zoom · right-drag to pan';\n root.appendChild(note);\n this._toolbar = document.createElement('div');\n this._toolbar.className = 'toolbar';\n this._objBtn = document.createElement('button');\n this._objBtn.type = 'button';\n this._objBtn.textContent = 'Download OBJ + MTL';\n this._objBtn.addEventListener('click', () => this._runExport('obj'));\n this._glbBtn = document.createElement('button');\n this._glbBtn.type = 'button';\n this._glbBtn.textContent = 'Download GLB';\n this._glbBtn.addEventListener('click', () => this._runExport('glb'));\n this._toolbar.appendChild(this._objBtn);\n this._toolbar.appendChild(this._glbBtn);\n root.appendChild(this._toolbar);\n this._setButtonsEnabled(false);\n /** Resolves with { THREE } once the scene is live — build the model\n * in `await stage.ready` so nothing races the library load. */\n this.ready = new Promise((resolve, reject) => {\n this._readyResolve = resolve;\n this._readyReject = reject;\n });\n }\n\n connectedCallback() {\n if (this._booted) {\n // Re-attached after a removal — resume what disconnected stopped.\n if (this._renderer) {\n this._renderer.setAnimationLoop(this._loop);\n this._ro && this._ro.observe(this);\n }\n return;\n }\n this._booted = true;\n this._boot().catch((err) => {\n this._err.style.display = 'flex';\n this._err.textContent =\n 'three.js failed to load.\\n' +\n 'Check that the pinned <script type=\"importmap\"> from the usage ' +\n 'notes is in <head> before any module script.\\n\\n' +\n String(err && err.message ? err.message : err);\n this._readyReject(err);\n });\n }\n\n async _boot() {\n const bg = this.getAttribute('background');\n if (bg) this.style.setProperty('--stage-bg', bg);\n const [THREE, controlsMod] = await Promise.all([\n import('three'),\n import('three/addons/controls/OrbitControls.js'),\n ]);\n this._THREE = THREE;\n // preserveDrawingBuffer keeps the last frame readable after\n // compositing (toDataURL / drawImage) — it's what lets the\n // screenshot tools capture the scene instead of a blank canvas.\n const renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: true,\n preserveDrawingBuffer: true,\n });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.shadowMap.enabled = true;\n renderer.shadowMap.type = THREE.PCFSoftShadowMap;\n this._renderer = renderer;\n this.shadowRoot.insertBefore(renderer.domElement, this._err);\n\n const scene = new THREE.Scene();\n this._scene = scene;\n\n const camera = new THREE.PerspectiveCamera(45, 1, 0.01, 500);\n camera.position.set(3, 2.2, 4);\n this._camera = camera;\n\n const controls = new controlsMod.OrbitControls(camera, renderer.domElement);\n controls.enableDamping = true;\n controls.dampingFactor = 0.08;\n this._controls = controls;\n\n // Neutral studio: soft sky/ground wash, a shadow-casting key light,\n // and a dim fill from behind so silhouettes never go black.\n scene.add(new THREE.HemisphereLight(0xffffff, 0xd8d2c4, 1.0));\n const key = new THREE.DirectionalLight(0xffffff, 2.2);\n key.position.set(4, 7, 5);\n key.castShadow = true;\n key.shadow.mapSize.set(2048, 2048);\n key.shadow.bias = -0.0002;\n this._key = key;\n scene.add(key);\n const fill = new THREE.DirectionalLight(0xfff4e6, 0.5);\n fill.position.set(-5, 3, -4);\n scene.add(fill);\n\n const ground = new THREE.Mesh(\n new THREE.PlaneGeometry(200, 200),\n new THREE.ShadowMaterial({ opacity: 0.18 })\n );\n ground.rotation.x = -Math.PI / 2;\n ground.receiveShadow = true;\n this._ground = ground;\n scene.add(ground);\n\n this._autorotate = this.hasAttribute('autorotate');\n controls.autoRotate = this._autorotate;\n controls.autoRotateSpeed = 1.2;\n controls.addEventListener('start', () => {\n controls.autoRotate = false;\n });\n\n const fit = () => {\n const w = this.clientWidth || 1;\n const h = this.clientHeight || 1;\n renderer.setSize(w, h);\n camera.aspect = w / h;\n camera.updateProjectionMatrix();\n };\n fit();\n this._ro = new ResizeObserver(fit);\n this._loop = () => {\n controls.update();\n renderer.render(scene, camera);\n };\n // Detached while three.js was fetching? Stay idle — the\n // connectedCallback resume starts the loop and observer on\n // reattach.\n if (this.isConnected) {\n this._ro.observe(this);\n renderer.setAnimationLoop(this._loop);\n }\n\n this._readyResolve({ THREE });\n }\n\n disconnectedCallback() {\n // Stop rendering and observing while detached; connectedCallback\n // resumes both. (The renderer itself is kept — a move within the\n // document must not rebuild the scene.)\n if (this._renderer) this._renderer.setAnimationLoop(null);\n if (this._ro) this._ro.disconnect();\n }\n\n /** Show (and own) the object. Replaces any previous object, enables\n * shadows on every mesh, rests it on the ground plane, and frames\n * the camera to its bounds. */\n setObject(object) {\n const THREE = this._THREE;\n if (!THREE) throw new Error('three-d-stage: not ready — await stage.ready first');\n if (this._object) this._scene.remove(this._object);\n this._object = object;\n object.traverse((o) => {\n if (o.isMesh) {\n o.castShadow = true;\n o.receiveShadow = true;\n }\n });\n const box = new THREE.Box3().setFromObject(object);\n if (!box.isEmpty()) {\n // Rest the object on the ground without moving its origin.\n this._ground.position.y = box.min.y;\n const sphere = box.getBoundingSphere(new THREE.Sphere());\n const dist =\n (sphere.radius / Math.tan((this._camera.fov * Math.PI) / 360)) * 1.35;\n const dir = new THREE.Vector3(1, 0.55, 1.25).normalize();\n this._camera.position\n .copy(sphere.center)\n .add(dir.multiplyScalar(dist));\n this._camera.near = Math.max(dist / 100, 0.01);\n this._camera.far = dist * 100;\n this._camera.updateProjectionMatrix();\n this._controls.target.copy(sphere.center);\n this._controls.update();\n const span = sphere.radius * 3;\n this._key.shadow.camera.left = -span;\n this._key.shadow.camera.right = span;\n this._key.shadow.camera.top = span;\n this._key.shadow.camera.bottom = -span;\n this._key.shadow.camera.updateProjectionMatrix();\n }\n this._scene.add(object);\n this._setButtonsEnabled(true);\n }\n\n get _basename() {\n return (this.getAttribute('name') || 'model').replace(/[^\\w.-]+/g, '_');\n }\n\n _setButtonsEnabled(on) {\n this._objBtn.disabled = !on;\n this._glbBtn.disabled = !on;\n }\n\n /** Every mesh and material needs a unique name for o/usemtl lines —\n * fill in stable fallbacks, and return the unique material list. */\n _nameParts() {\n const mats = [];\n const seen = new Set();\n let meshI = 0;\n let matI = 0;\n this._object.traverse((o) => {\n if (!o.isMesh) return;\n if (!o.name) o.name = 'part_' + meshI;\n meshI += 1;\n const list = Array.isArray(o.material) ? o.material : [o.material];\n for (const m of list) {\n if (!m || mats.includes(m)) continue;\n if (!m.name) {\n m.name = 'mat_' + matI;\n matI += 1;\n }\n while (seen.has(m.name)) {\n m.name = m.name + '_' + matI;\n matI += 1;\n }\n seen.add(m.name);\n mats.push(m);\n }\n });\n return mats;\n }\n\n /** One export attempt, reported to the host however it settles.\n * Rethrows so a failure stays visible on the guest console exactly as\n * before. The no-object early return is not an attempt (the toolbar is\n * disabled until the model loads) and reports nothing. */\n async _runExport(format) {\n if (!this._object) return;\n try {\n await (format === 'obj' ? this._exportObj() : this._exportGlb());\n notifyExport(format, true);\n } catch (err) {\n notifyExport(format, false);\n throw err;\n }\n }\n\n async _exportObj() {\n if (!this._object) return;\n const mod = await import('three/addons/exporters/OBJExporter.js');\n const mats = this._nameParts();\n const base = this._basename;\n const obj =\n 'mtllib ' + base + '.mtl\\n' + new mod.OBJExporter().parse(this._object);\n let mtl = '# Exported by three-d-stage\\n';\n for (const m of mats) {\n const c = m.color || { r: 0.8, g: 0.8, b: 0.8 };\n const rough = typeof m.roughness === 'number' ? m.roughness : 0.5;\n const opacity = typeof m.opacity === 'number' ? m.opacity : 1;\n mtl += 'newmtl ' + m.name + '\\n';\n mtl +=\n 'Kd ' + c.r.toFixed(4) + ' ' + c.g.toFixed(4) + ' ' + c.b.toFixed(4) + '\\n';\n mtl += 'Ks 0.2000 0.2000 0.2000\\n';\n mtl += 'Ns ' + Math.round((1 - rough) * 200) + '\\n';\n mtl += 'd ' + opacity.toFixed(4) + '\\n\\n';\n }\n download(new Blob([obj], { type: 'text/plain' }), base + '.obj');\n download(new Blob([mtl], { type: 'text/plain' }), base + '.mtl');\n }\n\n async _exportGlb() {\n if (!this._object) return;\n const mod = await import('three/addons/exporters/GLTFExporter.js');\n this._nameParts();\n const base = this._basename;\n const buf = await new mod.GLTFExporter().parseAsync(this._object, {\n binary: true,\n });\n download(\n new Blob([buf], { type: 'model/gltf-binary' }),\n base + '.glb'\n );\n }\n }\n\n customElements.define('three-d-stage', ThreeDStage);\n})();\n<\/script>\n<script type=\"module\">\nimport { buildSofa, buildRegal, regalMaterials, buildLampe, buildBoxen, buildTripod } from 'data:text/javascript;base64,Ly8gTcO2YmVsLUJ1aWxkZXIgKGF1cyBTb2ZhIDNEIC8gUmVnYWwgM0QgLyBCb2dlbmxhbXBlIDNEIMO8YmVybm9tbWVuKQpjb25zdCBtayA9IChUSFJFRSwgbmFtZSwgZ2VvLCBtYXQsIHg9MCwgeT0wLCB6PTApID0+IHsgY29uc3QgbSA9IG5ldyBUSFJFRS5NZXNoKGdlbywgbWF0KTsgbS5uYW1lPW5hbWU7IG0ucG9zaXRpb24uc2V0KHgseSx6KTsgcmV0dXJuIG07IH07CgpleHBvcnQgZnVuY3Rpb24gYnVpbGRTb2ZhKFRIUkVFKXsKICBjb25zdCBNID0gewogICAgZmFicmljOiBuZXcgVEhSRUUuTWVzaFN0YW5kYXJkTWF0ZXJpYWwoeyBuYW1lOidwb2x5X3dvb2xfaGVycmluZ2JvbmUnLCBjb2xvcjoweDhiODc4MCwgcm91Z2huZXNzOjAuOTUgfSksCiAgICBtZXRhbDogIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J2JsYWNrX3Bvd2RlcmNvYXQnLCBjb2xvcjoweDFkMWQxZiwgcm91Z2huZXNzOjAuNDUsIG1ldGFsbmVzczowLjYgfSksCiAgICBjdXA6ICAgIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J2N1cGhvbGRlcl9ibGFjaycsIGNvbG9yOjB4MTUxNTE1LCByb3VnaG5lc3M6MC44IH0pLAogIH07CiAgY29uc3QgVEVYID0gMC41OwogIGZ1bmN0aW9uIHJib3godywgaCwgZCwgciwgayA9IDcsIGJ1bGdlID0gMCl7CiAgICBjb25zdCBoYWxmID0gW3cvMiwgaC8yLCBkLzJdOwogICAgY29uc3QgUyA9IGhhbGYubWFwKGhoID0+IHsgY29uc3QgYT1bXTsgZm9yKGxldCBpPWs7aT49MDtpLS0pIGEucHVzaCgtKGhoLXIpIC0gcipNYXRoLnNpbihpL2sqTWF0aC5QSS8yKSk7IGZvcihsZXQgaT0wO2k8PWs7aSsrKSBhLnB1c2goKGhoLXIpICsgcipNYXRoLnNpbihpL2sqTWF0aC5QSS8yKSk7IHJldHVybiBhOyB9KTsKICAgIGNvbnN0IHBvcz1bXSwgbm9yPVtdLCB1dj1bXSwgaWR4PVtdOwogICAgZm9yIChsZXQgYT0wO2E8MzthKyspIGZvciAoY29uc3QgcyBvZiBbMSwtMV0pewogICAgICBjb25zdCB1PShhKzEpJTMsIHY9KGErMiklMywgc3U9U1t1XSwgc3Y9U1t2XSwgYmFzZT1wb3MubGVuZ3RoLzM7CiAgICAgIGZvciAobGV0IGo9MDtqPHN2Lmxlbmd0aDtqKyspIGZvciAobGV0IGk9MDtpPHN1Lmxlbmd0aDtpKyspewogICAgICAgIGNvbnN0IHA9WzAsMCwwXTsgcFthXT1zKmhhbGZbYV07IHBbdV09c3VbaV07IHBbdl09c3Zbal07CiAgICAgICAgY29uc3QgYz1wLm1hcCgoeCxtKT0+TWF0aC5tYXgoLShoYWxmW21dLXIpLE1hdGgubWluKGhhbGZbbV0tcix4KSkpOwogICAgICAgIGxldCBvPXAubWFwKCh4LG0pPT54LWNbbV0pOyBjb25zdCBMPU1hdGguaHlwb3QoLi4ubyl8fDE7IG89by5tYXAoeD0+eC9MKTsKICAgICAgICBjb25zdCBxPWMubWFwKCh4LG0pPT54K29bbV0qcik7CiAgICAgICAgcG9zLnB1c2goLi4ucSk7IG5vci5wdXNoKC4uLm8pOwogICAgICAgIGNvbnN0IGF4PU1hdGguYWJzKG9bMF0pLGF5PU1hdGguYWJzKG9bMV0pLGF6PU1hdGguYWJzKG9bMl0pOwogICAgICAgIGlmIChheT49YXgmJmF5Pj1heikgdXYucHVzaChxWzBdL1RFWCxxWzJdL1RFWCk7IGVsc2UgaWYgKGF4Pj1heikgdXYucHVzaChxWzJdL1RFWCxxWzFdL1RFWCk7IGVsc2UgdXYucHVzaChxWzBdL1RFWCxxWzFdL1RFWCk7CiAgICAgIH0KICAgICAgY29uc3QgbnU9c3UubGVuZ3RoOwogICAgICBmb3IgKGxldCBqPTA7ajxzdi5sZW5ndGgtMTtqKyspIGZvciAobGV0IGk9MDtpPG51LTE7aSsrKXsKICAgICAgICBjb25zdCBBPWJhc2UraipudStpLCBCPUErMSwgQz1BK251KzEsIEQ9QStudTsKICAgICAgICBpZiAocz4wKSBpZHgucHVzaChBLEIsQywgQSxDLEQpOyBlbHNlIGlkeC5wdXNoKEEsQyxCLCBBLEQsQyk7CiAgICAgIH0KICAgIH0KICAgIGNvbnN0IGcgPSBuZXcgVEhSRUUuQnVmZmVyR2VvbWV0cnkoKTsKICAgIGcuc2V0QXR0cmlidXRlKCdwb3NpdGlvbicsIG5ldyBUSFJFRS5GbG9hdDMyQnVmZmVyQXR0cmlidXRlKHBvcywzKSk7CiAgICBnLnNldEF0dHJpYnV0ZSgnbm9ybWFsJywgbmV3IFRIUkVFLkZsb2F0MzJCdWZmZXJBdHRyaWJ1dGUobm9yLDMpKTsKICAgIGcuc2V0QXR0cmlidXRlKCd1dicsIG5ldyBUSFJFRS5GbG9hdDMyQnVmZmVyQXR0cmlidXRlKHV2LDIpKTsKICAgIGcuc2V0SW5kZXgoaWR4KTsKICAgIGlmIChidWxnZSl7CiAgICAgIGNvbnN0IHA9Zy5hdHRyaWJ1dGVzLnBvc2l0aW9uOwogICAgICBmb3IgKGxldCBpPTA7aTxwLmNvdW50O2krKyl7IGNvbnN0IHg9cC5nZXRYKGkpL2hhbGZbMF0sIHk9cC5nZXRZKGkpL2hhbGZbMV07IHAuc2V0WihpLCBwLmdldFooaSkqKDErYnVsZ2UqKDEteCp4KSooMS15KnkpKSk7IH0KICAgICAgZy5jb21wdXRlVmVydGV4Tm9ybWFscygpOwogICAgfQogICAgcmV0dXJuIGc7CiAgfQogIGNvbnN0IG1lc2ggPSAoLi4uYSkgPT4gbWsoVEhSRUUsIC4uLmEpOwogIGNvbnN0IFc9MS44NSwgRD0wLjkyNSwgTD0wLjIwLCBBUk09MC4yNCwgVE9QPTAuMjEsIEc9MC4wMDQ7CiAgY29uc3Qgc29mYSA9IG5ldyBUSFJFRS5Hcm91cCgpOyBzb2ZhLm5hbWUgPSAnc29mYSc7CiAgZm9yIChjb25zdCBbaSxzeF0gb2YgW1sxLC0xXSxbMiwxXV0pCiAgICBzb2ZhLmFkZChtZXNoKCdsYWdlX3VudGVuXycraSwgcmJveChXLzItRywgTC1HLCBELCAwLjAzNSksIE0uZmFicmljLCBzeCpXLzQsIEwvMiwgMCkpOwogIGNvbnN0IGhpbmdlID0gbmV3IFRIUkVFLkdyb3VwKCk7IGhpbmdlLm5hbWU9J2tsYXBwdGVpbCc7IGhpbmdlLnBvc2l0aW9uLnNldCgwLCBMLCBELzIpOwogIGZvciAoY29uc3QgW2ksc3hdIG9mIFtbMSwtMV0sWzIsMV1dKQogICAgaGluZ2UuYWRkKG1lc2goJ2xhZ2VfbWl0dGVfJytpLCByYm94KFcvMi1HLCBMLUcsIEQsIDAuMDM1KSwgTS5mYWJyaWMsIHN4KlcvNCwgTC8yLCAtRC8yKSk7CiAgc29mYS5hZGQoaGluZ2UpOwogIGNvbnN0IHRvcCA9IG5ldyBUSFJFRS5Hcm91cCgpOyB0b3AubmFtZT0nYXVmYmF1JzsgdG9wLnBvc2l0aW9uLnkgPSAyKkw7CiAgZm9yIChjb25zdCBbbixzeF0gb2YgW1snbGlua3MnLC0xXSxbJ3JlY2h0cycsMV1dKXsKICAgIGNvbnN0IHggPSBzeCooVy8yLUFSTS8yKTsKICAgIHRvcC5hZGQobWVzaCgnYXJtbGVobmVfJytuLCByYm94KEFSTSwgVE9QLCBELCAwLjA1KSwgTS5mYWJyaWMsIHgsIFRPUC8yLCAwKSk7CiAgICB0b3AuYWRkKG1lc2goJ2JlY2hlcmhhbHRlcl8nK24sIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KDAuMDQsMC4wNCwwLjAxMiw0MCksIE0uY3VwLCB4LCBUT1ArMC4wMDEsIEQvMi0wLjEpKTsKICAgIGNvbnN0IHJpbmcgPSBtZXNoKCdiZWNoZXJoYWx0ZXJfcmluZ18nK24sIG5ldyBUSFJFRS5Ub3J1c0dlb21ldHJ5KDAuMDQyLDAuMDA1LDEyLDQwKSwgTS5tZXRhbCwgeCwgVE9QKzAuMDA0LCBELzItMC4xKTsKICAgIHJpbmcucm90YXRpb24ueCA9IE1hdGguUEkvMjsgdG9wLmFkZChyaW5nKTsKICB9CiAgY29uc3QgYmFja1cgPSAoVy0yKkFSTSkvMiAtIEc7CiAgZm9yIChjb25zdCBbaSxzeF0gb2YgW1sxLC0xXSxbMiwxXV0pewogICAgdG9wLmFkZChtZXNoKCdydWVja2VudGVpbF8nK2ksIHJib3goYmFja1csIFRPUCwgQVJNLCAwLjA1KSwgTS5mYWJyaWMsIHN4KihiYWNrVy8yK0cvMiksIFRPUC8yLCAtRC8yK0FSTS8yKSk7CiAgICBjb25zdCBwID0gbWVzaCgna2lzc2VuXycraSwgcmJveCgwLjQ1LCAwLjQ1LCAwLjEwLCAwLjA0LCA3LCAwLjYpLCBNLmZhYnJpYywgc3gqMC4zMywgMC4yMiwgLUQvMitBUk0rMC4wOCk7CiAgICBwLnJvdGF0aW9uLnggPSAtMC4yODsgcC5yb3RhdGlvbi56ID0gc3gqMC4wNjsgdG9wLmFkZChwKTsKICB9CiAgc29mYS5hZGQodG9wKTsKICBjb25zdCBmejAgPSAtMC4wNSwgZnoxID0gMC4zMCwgZmggPSAwLjM0LCByciA9IDAuMDA4LCBSID0gMC4wNDUsIHkwID0gMC4wMiwgeXQgPSB5MCArIGZoOwogIGZvciAoY29uc3QgW24sc3hdIG9mIFtbJ2xpbmtzJywtMV0sWydyZWNodHMnLDFdXSl7CiAgICBjb25zdCBmcmFtZSA9IG5ldyBUSFJFRS5Hcm91cCgpOyBmcmFtZS5uYW1lPSdzZWl0ZW5nZXN0ZWxsXycrbjsKICAgIGNvbnN0IGZ4ID0gc3gqKFcvMiArIDAuMDEyKTsKICAgIGZvciAoY29uc3QgeiBvZiBbZnowLCBmejFdKSBmcmFtZS5hZGQobWVzaCgnZ2VzdGVsbF9zdGFiXycrbiwgbmV3IFRIUkVFLkN5bGluZGVyR2VvbWV0cnkocnIscnIsZmgtUiwyMCksIE0ubWV0YWwsIGZ4LCB5MCsoZmgtUikvMiwgeikpOwogICAgY29uc3QgYmFyID0gbWVzaCgnZ2VzdGVsbF9xdWVyXycrbiwgbmV3IFRIUkVFLkN5bGluZGVyR2VvbWV0cnkocnIscnIsZnoxLWZ6MC0yKlIsMjApLCBNLm1ldGFsLCBmeCwgeXQsIChmejArZnoxKS8yKTsgYmFyLnJvdGF0aW9uLng9TWF0aC5QSS8yOyBmcmFtZS5hZGQoYmFyKTsKICAgIGZvciAoY29uc3QgW3oscnldIG9mIFtbZnowK1IsIE1hdGguUEkvMl0sW2Z6MS1SLCAtTWF0aC5QSS8yXV0pewogICAgICBjb25zdCBjID0gbWVzaCgnZ2VzdGVsbF9ib2dlbl8nK24sIG5ldyBUSFJFRS5Ub3J1c0dlb21ldHJ5KFIsIHJyLCAxNiwgMjQsIE1hdGguUEkvMiksIE0ubWV0YWwsIGZ4LCB5dC1SLCB6KTsgYy5yb3RhdGlvbi55ID0gcnk7IGZyYW1lLmFkZChjKTsKICAgIH0KICAgIHNvZmEuYWRkKGZyYW1lKTsKICB9CiAgcmV0dXJuIHsgZ3JvdXA6IHNvZmEsIE0sIEQgfTsKfQoKZXhwb3J0IGZ1bmN0aW9uIGJ1aWxkUmVnYWwoVEhSRUUsIE0pewogIGNvbnN0IGJveCA9ICh3LCBoLCBkKSA9PiB7CiAgICBjb25zdCBnID0gbmV3IFRIUkVFLkJveEdlb21ldHJ5KHcsIGgsIGQpLCB1diA9IGcuYXR0cmlidXRlcy51djsKICAgIGNvbnN0IGZhY2VzID0gW1tkLGhdLFtkLGhdLFt3LGRdLFt3LGRdLFt3LGhdLFt3LGhdXTsKICAgIGZvciAobGV0IGY9MDsgZjw2OyBmKyspIGZvciAobGV0IGk9MDtpPDQ7aSsrKXsgY29uc3Qgaz1mKjQraTsgdXYuc2V0WFkoaywgdXYuZ2V0WChrKSpmYWNlc1tmXVswXSwgdXYuZ2V0WShrKSpmYWNlc1tmXVsxXSk7IH0KICAgIHJldHVybiBnOwogIH07CiAgY29uc3QgbWVzaCA9ICguLi5hKSA9PiBtayhUSFJFRSwgLi4uYSk7CiAgY29uc3QgVz0wLjYwLCBEPTAuMjcsIEg9MC43NCwgQT0wLjAzLCBUPTAuMDAxNSwgTElQPTAuMDIsIEZPT1Q9MC4wMTI7CiAgY29uc3Qgc2hlbGZUb3BzID0gWzAuMTAsIDAuNDIsIEhdOwogIGNvbnN0IHJlZ2FsID0gbmV3IFRIUkVFLkdyb3VwKCk7IHJlZ2FsLm5hbWU9J3JlZ2FsJzsKICBjb25zdCBwb3N0SCA9IEggLSBGT09UOwogIGZvciAoY29uc3QgW24sc3gsc3pdIG9mIFtbJ3ZsJywtMSwxXSxbJ3ZyJywxLDFdLFsnaGwnLC0xLC0xXSxbJ2hyJywxLC0xXV0pewogICAgY29uc3QgcCA9IG5ldyBUSFJFRS5Hcm91cCgpOyBwLm5hbWU9J3Bmb3N0ZW5fJytuOwogICAgY29uc3QgY3ggPSBzeCooVy8yIC0gQS8yKSwgY3ogPSBzeiooRC8yIC0gQS8yKSwgeSA9IEZPT1QgKyBwb3N0SC8yOwogICAgcC5hZGQobWVzaCgncGZvc3Rlbl8nK24rJ19hJywgYm94KEEsIHBvc3RILCBUKSwgTS5jb25jcmV0ZSwgY3gsIHksIHN6KihELzIgLSBULzIpKSk7CiAgICBwLmFkZChtZXNoKCdwZm9zdGVuXycrbisnX2InLCBib3goVCwgcG9zdEgsIEEpLCBNLmNvbmNyZXRlLCBzeCooVy8yIC0gVC8yKSwgeSwgY3opKTsKICAgIHAuYWRkKG1lc2goJ2Z1c3NfJytuLCBib3goQSswLjAwMiwgRk9PVCwgQSswLjAwMiksIE0uZm9vdCwgY3gsIEZPT1QvMiwgY3opKTsKICAgIGZvciAoY29uc3QgdG9wIG9mIHNoZWxmVG9wcyl7CiAgICAgIGNvbnN0IHliID0gdG9wIC0gTElQLzI7CiAgICAgIGNvbnN0IGIxID0gbWVzaCgnc2NocmF1YmVfJytuLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeSgwLjAwNCwwLjAwNCwwLjAwMywxNiksIE0uYm9sdCwgY3gsIHliLCBzeiooRC8yKzAuMDAxKSk7IGIxLnJvdGF0aW9uLnggPSBNYXRoLlBJLzI7IHAuYWRkKGIxKTsKICAgICAgY29uc3QgYjIgPSBtZXNoKCdzY2hyYXViZV8nK24sIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KDAuMDA0LDAuMDA0LDAuMDAzLDE2KSwgTS5ib2x0LCBzeCooVy8yKzAuMDAxKSwgeWIsIGN6KTsgYjIucm90YXRpb24ueiA9IE1hdGguUEkvMjsgcC5hZGQoYjIpOwogICAgfQogICAgcmVnYWwuYWRkKHApOwogIH0KICBzaGVsZlRvcHMuZm9yRWFjaCgodG9wLCBpKSA9PiB7CiAgICBjb25zdCBzID0gbmV3IFRIUkVFLkdyb3VwKCk7IHMubmFtZT0nYm9kZW5fJysoaSsxKTsKICAgIGNvbnN0IGl3ID0gVyAtIDIqVCwgaWQgPSBEIC0gMipUOwogICAgcy5hZGQobWVzaCgnYm9kZW5fJysoaSsxKSsnX3BsYXR0ZScsIGJveChpdywgVCwgaWQpLCBNLmNvbmNyZXRlLCAwLCB0b3AgLSBULzIsIDApKTsKICAgIGZvciAoY29uc3Qgc3ogb2YgWzEsLTFdKSBzLmFkZChtZXNoKCdib2Rlbl8nKyhpKzEpKydfa2FudGVfeicsIGJveChpdywgTElQLCBUKSwgTS5jb25jcmV0ZSwgMCwgdG9wIC0gTElQLzIsIHN6KihpZC8yIC0gVC8yKSkpOwogICAgZm9yIChjb25zdCBzeCBvZiBbMSwtMV0pIHMuYWRkKG1lc2goJ2JvZGVuXycrKGkrMSkrJ19rYW50ZV94JywgYm94KFQsIExJUCwgaWQpLCBNLmNvbmNyZXRlLCBzeCooaXcvMiAtIFQvMiksIHRvcCAtIExJUC8yLCAwKSk7CiAgICByZWdhbC5hZGQocyk7CiAgfSk7CiAgcmV0dXJuIHJlZ2FsOwp9CmV4cG9ydCBmdW5jdGlvbiByZWdhbE1hdGVyaWFscyhUSFJFRSl7CiAgcmV0dXJuIHsKICAgIGNvbmNyZXRlOiBnYWx2YW5pemVkTWF0ZXJpYWwoVEhSRUUsIDE4KSwgLy8gdmVyemlua3RlciBTdGFobCwgVGV4dHVyIOKAnkZlaW7igJwKICAgIGZvb3Q6ICAgICBuZXcgVEhSRUUuTWVzaFN0YW5kYXJkTWF0ZXJpYWwoeyBuYW1lOidrdW5zdHN0b2ZmX2Z1c3MnLCBjb2xvcjoweDJhMmEyYSwgcm91Z2huZXNzOjAuNyB9KSwKICAgIGJvbHQ6ICAgICBuZXcgVEhSRUUuTWVzaFN0YW5kYXJkTWF0ZXJpYWwoeyBuYW1lOidzY2hyYXViZScsIGNvbG9yOjB4OGM4YzhjLCByb3VnaG5lc3M6MC4zNSwgbWV0YWxuZXNzOjAuOSB9KSwKICB9Owp9CgpleHBvcnQgZnVuY3Rpb24gYnVpbGRMYW1wZShUSFJFRSl7CiAgY29uc3QgViA9ICh4LHksej0wKSA9PiBuZXcgVEhSRUUuVmVjdG9yMyh4LHkseik7CiAgY29uc3QgTSA9IHsKICAgIGNocm9tOiAgbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTonY2hyb20nLCBjb2xvcjoweGQ4ZGFkZCwgbWV0YWxuZXNzOjAuNSwgcm91Z2huZXNzOjAuMjUgfSksCiAgICBzY2hpcm06IG5ldyBUSFJFRS5NZXNoUGh5c2ljYWxNYXRlcmlhbCh7IG5hbWU6J2FjcnlsX3dlaXNzJywgY29sb3I6MHhmN2Y3ZjUsIHJvdWdobmVzczowLjI4LCBjbGVhcmNvYXQ6MC44LCBjbGVhcmNvYXRSb3VnaG5lc3M6MC4xLCBzaWRlOlRIUkVFLkRvdWJsZVNpZGUgfSksCiAgICBiaXJuZTogIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J2xldWNodG1pdHRlbCcsIGNvbG9yOjB4ZmZmZmZmLCBlbWlzc2l2ZToweGZmZjFkNiwgZW1pc3NpdmVJbnRlbnNpdHk6MC4yIH0pLAogIH07CiAgY29uc3QgYWRkID0gKHBhcmVudCwgbmFtZSwgZ2VvLCBtYXQsIHA9WzAsMCwwXSwgcj1bMCwwLDBdKSA9PiB7IGNvbnN0IG0gPSBuZXcgVEhSRUUuTWVzaChnZW8sIG1hdCk7IG0ubmFtZSA9IG5hbWU7IG0ucG9zaXRpb24uc2V0KC4uLnApOyBtLnJvdGF0aW9uLnNldCguLi5yKTsgcGFyZW50LmFkZChtKTsgcmV0dXJuIG07IH07CiAgY29uc3QgbGFtcGUgPSBuZXcgVEhSRUUuR3JvdXAoKTsgbGFtcGUubmFtZSA9ICdib2dlbmxhbXBlJzsKICBjb25zdCBIID0gMS42NiwgUkVBQ0ggPSAxLjIyLCBTSF9SID0gMC4xNSwgQkFTRV9SID0gMC4xMywgQkFTRV9IID0gMC4wMjgsIFBPTEVfUiA9IDAuMDEyNSwgQVJDX1IgPSAwLjAwODUsIFBPTEVfVE9QID0gMC43NDsKICBjb25zdCBwcm9mID0gW25ldyBUSFJFRS5WZWN0b3IyKDAsIDApLCBuZXcgVEhSRUUuVmVjdG9yMihCQVNFX1IgLSAwLjAwNCwgMCldOwogIGZvciAobGV0IGkgPSAwOyBpIDw9IDEwOyBpKyspeyBjb25zdCBhID0gLU1hdGguUEkvMiArIGkvMTAqTWF0aC5QSS8yOyBwcm9mLnB1c2gobmV3IFRIUkVFLlZlY3RvcjIoQkFTRV9SIC0gMC4wMDggKyBNYXRoLmNvcyhhKSowLjAwOCwgMC4wMDggKyBNYXRoLnNpbihhKSowLjAwOCkpOyB9CiAgZm9yIChsZXQgaSA9IDA7IGkgPD0gMTA7IGkrKyl7IGNvbnN0IGEgPSBpLzEwKk1hdGguUEkvMjsgcHJvZi5wdXNoKG5ldyBUSFJFRS5WZWN0b3IyKEJBU0VfUiAtIDAuMDEyICsgTWF0aC5jb3MoYSkqMC4wMTIsIEJBU0VfSCAtIDAuMDEyICsgTWF0aC5zaW4oYSkqMC4wMTIpKTsgfQogIHByb2YucHVzaChuZXcgVEhSRUUuVmVjdG9yMigwLCBCQVNFX0gpKTsKICBhZGQobGFtcGUsICdmdXNzJywgbmV3IFRIUkVFLkxhdGhlR2VvbWV0cnkocHJvZiwgOTYpLCBNLmNocm9tKTsKICBhZGQobGFtcGUsICdmdXNzX3Jvc2V0dGUnLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeSgwLjAyMiwgMC4wMjYsIDAuMDEyLCA0OCksIE0uY2hyb20sIFswLCBCQVNFX0ggKyAwLjAwNiwgMF0pOwogIGFkZChsYW1wZSwgJ3N0YW5kcm9ocicsIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KFBPTEVfUiwgUE9MRV9SLCBQT0xFX1RPUCAtIEJBU0VfSCwgNDgpLCBNLmNocm9tLCBbMCwgKFBPTEVfVE9QICsgQkFTRV9IKS8yLCAwXSk7CiAgYWRkKGxhbXBlLCAnbXVmZmVfdW50ZW4nLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeShQT0xFX1IgKyAwLjAwMTUsIFBPTEVfUiArIDAuMDAxNSwgMC4wMywgNDgpLCBNLmNocm9tLCBbMCwgUE9MRV9UT1AgLSAwLjAxNSwgMF0pOwogIGNvbnN0IEFSQ19FTkQgPSBWKFJFQUNIIC0gMC4wNCwgSCAtIEFSQ19SKTsKICBjb25zdCBhcmMgPSBuZXcgVEhSRUUuQ3ViaWNCZXppZXJDdXJ2ZTMoVigwLCBQT0xFX1RPUCAtIDAuMDIpLCBWKDAsIDEuMzgpLCBWKDAuNDIsIEggLSBBUkNfUiksIEFSQ19FTkQpOwogIGFkZChsYW1wZSwgJ2JvZ2VuJywgbmV3IFRIUkVFLlR1YmVHZW9tZXRyeShhcmMsIDI0MCwgQVJDX1IsIDMyKSwgTS5jaHJvbSk7CiAgY29uc3QgcE0gPSBhcmMuZ2V0UG9pbnRBdCgwLjM2KSwgZE0gPSBhcmMuZ2V0VGFuZ2VudEF0KDAuMzYpOwogIGNvbnN0IG11ZmZlID0gYWRkKGxhbXBlLCAnbXVmZmVfYm9nZW4nLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeShBUkNfUiArIDAuMDAxOCwgQVJDX1IgKyAwLjAwMTgsIDAuMDM1LCAzMiksIE0uY2hyb20sIFtwTS54LCBwTS55LCAwXSk7CiAgbXVmZmUucXVhdGVybmlvbi5zZXRGcm9tVW5pdFZlY3RvcnMoVigwLDEsMCksIGRNKTsKICBhZGQobGFtcGUsICdlbmRrYXBwZScsIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KEFSQ19SICsgMC4wMDIsIEFSQ19SICsgMC4wMDIsIDAuMDcsIDMyKSwgTS5jaHJvbSwgW0FSQ19FTkQueCAtIDAuMDIsIEFSQ19FTkQueSwgMF0sIFswLCAwLCBNYXRoLlBJLzJdKTsKICBjb25zdCBrb3BmID0gbmV3IFRIUkVFLkdyb3VwKCk7IGtvcGYubmFtZSA9ICdzY2hpcm1fZ3J1cHBlJzsKICBjb25zdCBUSUxUID0gMC4zOwogIGtvcGYucG9zaXRpb24uc2V0KFJFQUNIIC0gU0hfUipNYXRoLnNpbihUSUxUKSwgSCAtIEFSQ19SIC0gMC4wMzUsIDApOwogIGtvcGYucm90YXRpb24ueiA9IFRJTFQ7CiAgYWRkKGxhbXBlLCAnZ2VsZW5rJywgbmV3IFRIUkVFLkN5bGluZGVyR2VvbWV0cnkoMC4wMDYsIDAuMDA2LCAwLjAzLCAyMCksIE0uY2hyb20sIFtrb3BmLnBvc2l0aW9uLngsIEggLSBBUkNfUiAtIDAuMDE4LCAwXSk7CiAgbGFtcGUuYWRkKGtvcGYpOwogIGFkZChrb3BmLCAnc2NoaXJtJywgbmV3IFRIUkVFLlNwaGVyZUdlb21ldHJ5KFNIX1IsIDk2LCA0OCwgMCwgTWF0aC5QSSoyLCAwLCAxLjk1KSwgTS5zY2hpcm0sIFswLCAtU0hfUiwgMF0pOwogIGFkZChrb3BmLCAnc2NoaXJtX25hYmUnLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeSgwLjAxOCwgMC4wMjgsIDAuMDIsIDQwKSwgTS5jaHJvbSwgWzAsIC0wLjAwNCwgMF0pOwogIGFkZChrb3BmLCAnZmFzc3VuZycsIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KDAuMDE2LCAwLjAxNiwgMC4wNSwgMzIpLCBNLmNocm9tLCBbMCwgLTAuMDQsIDBdKTsKICBhZGQoa29wZiwgJ2xldWNodG1pdHRlbCcsIG5ldyBUSFJFRS5TcGhlcmVHZW9tZXRyeSgwLjAzLCAzMiwgMTYpLCBNLmJpcm5lLCBbMCwgLTAuMDksIDBdKTsKICByZXR1cm4geyBncm91cDogbGFtcGUsIFJFQUNIIH07Cn0KCi8vIFZlcnppbmt0ZXIgU3RhaGwgKFppbmstU3BhbmdsZSwgcHJvemVkdXJhbCkg4oCTIGF1cyBSZWdhbCAzRApleHBvcnQgZnVuY3Rpb24gZ2FsdmFuaXplZChzaXplPTEwMjQsIGNlbGxzPTI0LCBzZWVkPTcpewogIGxldCBzID0gc2VlZDsgY29uc3Qgcm5kID0gKCkgPT4gKHMgPSAocyoxNjgwNykgJSAyMTQ3NDgzNjQ3KSAvIDIxNDc0ODM2NDc7CiAgY29uc3QgZyA9IGNlbGxzLCBwdHMgPSBbXTsKICBmb3IgKGxldCBqPTA7ajxnO2orKykgZm9yIChsZXQgaT0wO2k8ZztpKyspewogICAgY29uc3QgYSA9IHJuZCgpKk1hdGguUEkqMjsKICAgIHB0cy5wdXNoKHsgeDooaStybmQoKSkvZywgeTooaitybmQoKSkvZywgYmFzZTogMC43MiArIHJuZCgpKjAuMTgsIGd4Ok1hdGguY29zKGEpLCBneTpNYXRoLnNpbihhKSwgazogMC4wMyArIHJuZCgpKjAuMDgsIHI6IHJuZCgpIH0pOwogIH0KICBjb25zdCBjb2wgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdjYW52YXMnKTsgY29sLndpZHRoID0gY29sLmhlaWdodCA9IHNpemU7CiAgY29uc3Qgcm91ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnY2FudmFzJyk7IHJvdS53aWR0aCA9IHJvdS5oZWlnaHQgPSBzaXplOwogIGNvbnN0IGNjID0gY29sLmdldENvbnRleHQoJzJkJyksIHJjID0gcm91LmdldENvbnRleHQoJzJkJyk7CiAgY29uc3QgY2kgPSBjYy5jcmVhdGVJbWFnZURhdGEoc2l6ZSxzaXplKSwgcmkgPSByYy5jcmVhdGVJbWFnZURhdGEoc2l6ZSxzaXplKTsKICBmb3IgKGxldCB5PTA7eTxzaXplO3krKykgZm9yIChsZXQgeD0wO3g8c2l6ZTt4KyspewogICAgY29uc3QgdSA9IHgvc2l6ZSwgdiA9IHkvc2l6ZSwgY3ggPSBNYXRoLmZsb29yKHUqZyksIGN5ID0gTWF0aC5mbG9vcih2KmcpOwogICAgbGV0IGJlc3QgPSA5LCBzZWNvbmQgPSA5LCBicCA9IG51bGwsIGJkeD0wLCBiZHk9MDsKICAgIGZvciAobGV0IG95PS0xO295PD0xO295KyspIGZvciAobGV0IG94PS0xO294PD0xO294KyspewogICAgICBjb25zdCBpeCA9IChjeCtveCtnKSVnLCBpeSA9IChjeStveStnKSVnLCBwID0gcHRzW2l5KmcraXhdOwogICAgICBjb25zdCBkeCA9IHUgLSAocC54ICsgTWF0aC5mbG9vcigoY3grb3gpL2cpICogMSArICgoY3grb3gpPDA/MDowKSkgLCBkeSA9IHYgLSAocC55ICsgTWF0aC5mbG9vcigoY3krb3kpL2cpKTsKICAgICAgY29uc3QgZCA9IGR4KmR4ICsgZHkqZHk7CiAgICAgIGlmIChkIDwgYmVzdCl7IHNlY29uZCA9IGJlc3Q7IGJlc3QgPSBkOyBicCA9IHA7IGJkeCA9IGR4OyBiZHkgPSBkeTsgfSBlbHNlIGlmIChkIDwgc2Vjb25kKSBzZWNvbmQgPSBkOwogICAgfQogICAgY29uc3QgZWRnZSA9IE1hdGguc3FydChzZWNvbmQpIC0gTWF0aC5zcXJ0KGJlc3QpOyAgICAgICAgICAgIC8vIEFic3RhbmQgenVyIEtvcm5ncmVuemUKICAgIGxldCBsID0gYnAuYmFzZSArIChiZHgqYnAuZ3ggKyBiZHkqYnAuZ3kpICogZyAqIGJwLms7ICAgICAgICAvLyBGYWNldHRlbi1WZXJsYXVmIHBybyBLb3JuCiAgICBsICs9IChNYXRoLnJhbmRvbSgpLTAuNSkqMC4wNTsgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLy8gZmVpbmVzIEtvcm4KICAgIGlmIChlZGdlIDwgMC4wMDE4KSBsIC09IDAuMDM1OyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLy8gZHVua2xlIEtvcm5ncmVuemVuCiAgICBpZiAoTWF0aC5yYW5kb20oKSA8IDAuMDAzKSBsIC09IDAuMTI7ICAgICAgICAgICAgICAgICAgICAgICAgLy8gZHVua2xlIFNwcmVua2VsCiAgICBsID0gTWF0aC5tYXgoMC4yNSwgTWF0aC5taW4oMC45NywgbCkpOwogICAgY29uc3QgbyA9ICh5KnNpemUreCkqNCwgYyA9IGwqMjU1OwogICAgY2kuZGF0YVtvXT1jKjAuOTg7IGNpLmRhdGFbbysxXT1jOyBjaS5kYXRhW28rMl09YyoxLjAyOyBjaS5kYXRhW28rM109MjU1OwogICAgY29uc3QgciA9ICgwLjI4ICsgKDEtbCkqMC4zNSArIGJwLnIqMC4xMikgKiAyNTU7ICAgICAgICAgICAgIC8vIGhlbGxlIEvDtnJuZXIgZ2zDpG56ZW5kZXIKICAgIHJpLmRhdGFbb109cmkuZGF0YVtvKzFdPXJpLmRhdGFbbysyXT1yOyByaS5kYXRhW28rM109MjU1OwogIH0KICBjYy5wdXRJbWFnZURhdGEoY2ksMCwwKTsgcmMucHV0SW1hZ2VEYXRhKHJpLDAsMCk7CiAgcmV0dXJuIHsgY29sLCByb3UgfTsKfQpleHBvcnQgZnVuY3Rpb24gZ2FsdmFuaXplZE1hdGVyaWFsKFRIUkVFLCByZXBlYXQgPSAxOCl7CiAgY29uc3QgeyBjb2wsIHJvdSB9ID0gZ2FsdmFuaXplZCgpOwogIGNvbnN0IG1rID0gKGMsIHNyZ2IpID0+IHsgY29uc3QgdCA9IG5ldyBUSFJFRS5DYW52YXNUZXh0dXJlKGMpOyB0LndyYXBTID0gdC53cmFwVCA9IFRIUkVFLlJlcGVhdFdyYXBwaW5nOyB0LnJlcGVhdC5zZXQocmVwZWF0LCByZXBlYXQpOwogICAgaWYgKHNyZ2IpIHQuY29sb3JTcGFjZSA9IFRIUkVFLlNSR0JDb2xvclNwYWNlOyB0LmFuaXNvdHJvcHkgPSA4OyByZXR1cm4gdDsgfTsKICByZXR1cm4gbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTonc3RhaGxfdmVyemlua3QnLCBtYXA6IG1rKGNvbCwgdHJ1ZSksIHJvdWdobmVzc01hcDogbWsocm91KSwgYnVtcE1hcDogbWsoY29sKSwgYnVtcFNjYWxlOiAwLjI1LAogICAgY29sb3I6IDB4ZWNlZWYwLCByb3VnaG5lc3M6IDEsIG1ldGFsbmVzczogMC4zIH0pOwp9CgovLyBSZWdhbGxhdXRzcHJlY2hlci1QYWFyIChhdXMgQm94ZW4gM0QpOiBwYXNzaXYgKyBha3RpdiAoQmVkaWVuZmVsZCBhbiBsb2thbGVyICt4LVNlaXRlKQpleHBvcnQgZnVuY3Rpb24gYnVpbGRCb3hlbihUSFJFRSl7CiAgY29uc3QgTSA9IHsKICAgIHdvb2Q6ICAgbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTonZWljaGUnLCBjb2xvcjoweGI3N2EzZSwgcm91Z2huZXNzOjAuNTUgfSksCiAgICBmYWJyaWM6IG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J3N0b2ZmX2dyYXUnLCBjb2xvcjoweDZjNmM2YSwgcm91Z2huZXNzOjAuOTcgfSksCiAgICBzdHJpcDogIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J3ppZXJsZWlzdGVfc2lsYmVyJywgY29sb3I6MHhjOWM5YzksIHJvdWdobmVzczowLjMsIG1ldGFsbmVzczowLjkgfSksCiAgICBibGFjazogIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J2t1bnN0c3RvZmZfc2Nod2FyeicsIGNvbG9yOjB4MWMxYzFjLCByb3VnaG5lc3M6MC42IH0pLAogICAga25vYjogICBuZXcgVEhSRUUuTWVzaFN0YW5kYXJkTWF0ZXJpYWwoeyBuYW1lOidkcmVocmVnbGVyJywgY29sb3I6MHgxNDE0MTQsIHJvdWdobmVzczowLjQsIG1ldGFsbmVzczowLjIgfSksCiAgICBhbHU6ICAgIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J2hvY2h0b2VuZXJfYWx1JywgY29sb3I6MHhhYWFhYWEsIHJvdWdobmVzczowLjM1LCBtZXRhbG5lc3M6MC44IH0pLAogICAgbGVkOiAgICBuZXcgVEhSRUUuTWVzaFN0YW5kYXJkTWF0ZXJpYWwoeyBuYW1lOidsZWQnLCBjb2xvcjoweDBhMmExMCwgZW1pc3NpdmU6MHgyYmQ0NWEsIGVtaXNzaXZlSW50ZW5zaXR5OjEuMiB9KSwKICB9OwogIGNvbnN0IFc9MC4xNDYsIEQ9MC4xOTYsIEg9MC4yMzQsIFI9MC4wMTQsIEJWPTAuMDAyOwogIGNvbnN0IG1lc2ggPSAobmFtZSwgZ2VvLCBtYXQsIHg9MCwgeT0wLCB6PTApID0+IG1rKFRIUkVFLCBuYW1lLCBnZW8sIG1hdCwgeCwgeSwgeik7CiAgY29uc3QgcnJlY3QgPSAodywgaCwgcikgPT4geyBjb25zdCBzID0gbmV3IFRIUkVFLlNoYXBlKCksIHg9LXcvMiwgeT0taC8yOwogICAgcy5tb3ZlVG8oeCtyLHkpOyBzLmxpbmVUbyh4K3ctcix5KTsgcy5xdWFkcmF0aWNDdXJ2ZVRvKHgrdyx5LHgrdyx5K3IpOyBzLmxpbmVUbyh4K3cseStoLXIpOyBzLnF1YWRyYXRpY0N1cnZlVG8oeCt3LHkraCx4K3ctcix5K2gpOwogICAgcy5saW5lVG8oeCtyLHkraCk7IHMucXVhZHJhdGljQ3VydmVUbyh4LHkraCx4LHkraC1yKTsgcy5saW5lVG8oeCx5K3IpOyBzLnF1YWRyYXRpY0N1cnZlVG8oeCx5LHgrcix5KTsgcmV0dXJuIHM7IH07CiAgY29uc3QgY2FiR2VvID0gbmV3IFRIUkVFLkV4dHJ1ZGVHZW9tZXRyeShycmVjdChXLTIqQlYsIEQtMipCViwgUiksIHsgZGVwdGg6SC0yKkJWLCBiZXZlbEVuYWJsZWQ6dHJ1ZSwgYmV2ZWxTaXplOkJWLCBiZXZlbFRoaWNrbmVzczpCViwgYmV2ZWxTZWdtZW50czoyLCBjdXJ2ZVNlZ21lbnRzOjEwIH0pOwogIGNhYkdlby5yb3RhdGVYKC1NYXRoLlBJLzIpOyBjYWJHZW8udHJhbnNsYXRlKDAsIEJWLCAwKTsKICB7IGNvbnN0IHAgPSBjYWJHZW8uYXR0cmlidXRlcy5wb3NpdGlvbiwgbiA9IGNhYkdlby5hdHRyaWJ1dGVzLm5vcm1hbCwgdXYgPSBjYWJHZW8uYXR0cmlidXRlcy51djsKICAgIGZvciAobGV0IGk9MDtpPHAuY291bnQ7aSsrKXsgY29uc3QgeD1wLmdldFgoaSksIHk9cC5nZXRZKGkpLCB6PXAuZ2V0WihpKTsKICAgICAgaWYgKE1hdGguYWJzKG4uZ2V0WShpKSkgPiAwLjcpIHV2LnNldFhZKGksIHgsIHopOyBlbHNlIHV2LnNldFhZKGksIE1hdGguYWJzKG4uZ2V0WChpKSkgPiBNYXRoLmFicyhuLmdldFooaSkpID8geiA6IHgsIHkpOyB9IH0KICBjb25zdCBHVz1XLTAuMDA2LCBHSD1ILTAuMDA2LCBHVD0wLjAwODsKICBjb25zdCBncmlsbGVHZW8gPSBuZXcgVEhSRUUuRXh0cnVkZUdlb21ldHJ5KHJyZWN0KEdXLTAuMDA0LCBHSC0wLjAwNCwgMC4wMDkpLCB7IGRlcHRoOkdULTAuMDAzLCBiZXZlbEVuYWJsZWQ6dHJ1ZSwgYmV2ZWxTaXplOjAuMDAyLCBiZXZlbFRoaWNrbmVzczowLjAwMTUsIGJldmVsU2VnbWVudHM6MywgY3VydmVTZWdtZW50czo4IH0pOwogIGZ1bmN0aW9uIHNwZWFrZXIobmFtZSwgYWN0aXZlKXsKICAgIGNvbnN0IHMgPSBuZXcgVEhSRUUuR3JvdXAoKTsgcy5uYW1lID0gbmFtZTsKICAgIHMuYWRkKG1lc2gobmFtZSsnX2dlaGFldXNlJywgY2FiR2VvLCBNLndvb2QpKTsKICAgIHMuYWRkKG1lc2gobmFtZSsnX3N0b2ZmJywgZ3JpbGxlR2VvLCBNLmZhYnJpYywgMCwgSC8yLCBELzIgKyAwLjAwMTUpKTsKICAgIHMuYWRkKG1lc2gobmFtZSsnX3ppZXJsZWlzdGUnLCBuZXcgVEhSRUUuQm94R2VvbWV0cnkoR1cqMC43NCwgMC4wMDI2LCAwLjAwMTIpLCBNLnN0cmlwLCAwLCBIKjAuNTg1LCBELzIgKyBHVCArIDAuMDAwNSkpOwogICAgcy5hZGQobWVzaChuYW1lKydfbG9nbycsIG5ldyBUSFJFRS5Cb3hHZW9tZXRyeSgwLjAyNiwgMC4wMDYsIDAuMDAwOCksIE0uc3RyaXAsIDAsIDAuMDI2LCBELzIgKyBHVCArIDAuMDAwMykpOwogICAgY29uc3QgcG9ydCA9IG1lc2gobmFtZSsnX2Jhc3NyZWZsZXgnLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeSgwLjAxNCwgMC4wMTQsIDAuMDA0LCAzMiksIE0uYmxhY2ssIDAsIDAuMTcsIC1ELzIgLSAwLjAwMSk7CiAgICBwb3J0LnJvdGF0aW9uLnggPSBNYXRoLlBJLzI7IHMuYWRkKHBvcnQpOwogICAgZm9yIChjb25zdCBbZngsZnpdIG9mIFtbLTEsLTFdLFsxLC0xXSxbLTEsMV0sWzEsMV1dKQogICAgICBzLmFkZChtZXNoKG5hbWUrJ19mdXNzJywgbmV3IFRIUkVFLkN5bGluZGVyR2VvbWV0cnkoMC4wMDYsMC4wMDYsMC4wMDIsMjApLCBNLmJsYWNrLCBmeCooVy8yLTAuMDIpLCAwLjAwMSwgZnoqKEQvMi0wLjAyNSkpKTsKICAgIGlmIChhY3RpdmUpewogICAgICBjb25zdCBweCA9IFcvMiArIDAuMDAwNCwgY3ogPSAtMC4wMTI7CiAgICAgIHMuYWRkKG1lc2gobmFtZSsnX2JlZGllbmZlbGQnLCBuZXcgVEhSRUUuQm94R2VvbWV0cnkoMC4wMDIsIDAuMTA4LCAwLjA1NSksIE0uYmxhY2ssIHB4LCAwLjE0LCBjeikpOwogICAgICBbMC4xNzYsIDAuMTQzLCAwLjExXS5mb3JFYWNoKCh5LCBpKSA9PiB7CiAgICAgICAgY29uc3QgayA9IG1lc2gobmFtZSsnX3JlZ2xlcl8nK1snaG9laGVuJywnYmFzcycsJ2xhdXRzdGFlcmtlJ11baV0sIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KDAuMDA4NSwgMC4wMDg1LCAwLjAxMSwgMjgpLCBNLmtub2IsIHB4KzAuMDA1NSwgeSwgY3otMC4wMDQpOwogICAgICAgIGsucm90YXRpb24ueiA9IE1hdGguUEkvMjsgcy5hZGQoayk7CiAgICAgIH0pOwogICAgICBzLmFkZChtZXNoKG5hbWUrJ19sZWQnLCBuZXcgVEhSRUUuU3BoZXJlR2VvbWV0cnkoMC4wMDE1LCAxMiwgOCksIE0ubGVkLCBweCswLjAwMSwgMC4wOTMsIGN6LTAuMDA0KSk7CiAgICB9CiAgICBzLnRyYXZlcnNlKG8gPT4geyBpZiAoby5pc01lc2gpIG8ucG9zaXRpb24ueSArPSAwLjAwMjsgfSk7CiAgICByZXR1cm4gczsKICB9CiAgcmV0dXJuIHsgVywgRCwgSCwgbGlua3M6IHNwZWFrZXIoJ2JveF9saW5rcycsIGZhbHNlKSwgcmVjaHRzOiBzcGVha2VyKCdib3hfcmVjaHRzX2FrdGl2JywgdHJ1ZSkgfTsKfQoKLy8gVHJpcG9kLUxhbXBlIChhdXMgVHJpcG9kbGFtcGUgM0QpOiBFaWNoZW4tTGV1Y2h0a29wZiBtaXQgTEVELVNwb3QgYXVmIHNjaHdhcnplbSBEcmVpYmVpbjsgRnJvbnQgPSBsb2thbGUgK3oKZXhwb3J0IGZ1bmN0aW9uIGJ1aWxkVHJpcG9kKFRIUkVFKXsKICBjb25zdCBWID0gKHgseSx6KSA9PiBuZXcgVEhSRUUuVmVjdG9yMyh4LHkseik7CiAgY29uc3QgYyA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2NhbnZhcycpOyBjLndpZHRoID0gYy5oZWlnaHQgPSA1MTI7IGNvbnN0IGcgPSBjLmdldENvbnRleHQoJzJkJyk7CiAgZy5maWxsU3R5bGUgPSAnI2M0OWE2Nic7IGcuZmlsbFJlY3QoMCwwLDUxMiw1MTIpOwogIGxldCBzID0gMTE7IGNvbnN0IHIgPSAoKSA9PiAocyA9IChzKjE2ODA3KSAlIDIxNDc0ODM2NDcpIC8gMjE0NzQ4MzY0NzsKICBmb3IgKGxldCBpID0gMDsgaSA8IDI2MDsgaSsrKXsKICAgIGNvbnN0IHkgPSByKCkqNTEyLCBhID0gMC4wNCArIHIoKSowLjEyOyBnLnN0cm9rZVN0eWxlID0gcigpID4gMC41ID8gYHJnYmEoMTIwLDgwLDQwLCR7YX0pYCA6IGByZ2JhKDIzNSwyMDAsMTUwLCR7YX0pYDsgZy5saW5lV2lkdGggPSAwLjUgKyByKCkqMi4yOwogICAgZy5iZWdpblBhdGgoKTsgZy5tb3ZlVG8oMCx5KTsgZm9yIChsZXQgeCA9IDA7IHggPD0gNTEyOyB4ICs9IDMyKSBnLmxpbmVUbyh4LCB5ICsgTWF0aC5zaW4oeCowLjAxMiArIGkpKjQgKyAocigpLTAuNSkqMS41KTsgZy5zdHJva2UoKTsKICB9CiAgY29uc3QgdGV4ID0gbmV3IFRIUkVFLkNhbnZhc1RleHR1cmUoYyk7IHRleC5jb2xvclNwYWNlID0gVEhSRUUuU1JHQkNvbG9yU3BhY2U7CiAgY29uc3QgTSA9IHsKICAgIGhvbHo6ICAgbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTondHJpcG9kX2VpY2hlJywgbWFwOiB0ZXgsIHJvdWdobmVzczowLjYgfSksCiAgICBzdGFobDogIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J3RyaXBvZF9zdGFobF9zY2h3YXJ6JywgY29sb3I6MHgxYjFiMWMsIHJvdWdobmVzczowLjQ1LCBtZXRhbG5lc3M6MC42IH0pLAogICAgZ3VtbWk6ICBuZXcgVEhSRUUuTWVzaFN0YW5kYXJkTWF0ZXJpYWwoeyBuYW1lOid0cmlwb2RfZ3VtbWknLCBjb2xvcjoweDExMTExMSwgcm91Z2huZXNzOjAuOSB9KSwKICAgIGJsZW5kZTogbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTondHJpcG9kX3JlZmxla3RvcicsIGNvbG9yOjB4OWE5YTlhLCByb3VnaG5lc3M6MC4yNSwgbWV0YWxuZXNzOjAuOSB9KSwKICAgIGxlZDogICAgbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTondHJpcG9kX2xlZCcsIGNvbG9yOjB4ZDhkYWRkLCBlbWlzc2l2ZToweGY0ZjdmZiwgZW1pc3NpdmVJbnRlbnNpdHk6MCwgcm91Z2huZXNzOjAuMiB9KSwKICAgIGthYmVsOiAgbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTondHJpcG9kX2thYmVsJywgY29sb3I6MHgxNTE1MTUsIHJvdWdobmVzczowLjcgfSksCiAgfTsKICBjb25zdCBhZGQgPSAocCwgbiwgZ2VvLCBtYXQsIHBvcz1bMCwwLDBdKSA9PiB7IGNvbnN0IG8gPSBtayhUSFJFRSwgbiwgZ2VvLCBtYXQsIC4uLnBvcyk7IHAuYWRkKG8pOyByZXR1cm4gbzsgfTsKICBjb25zdCByb2QgPSAocCwgbiwgYSwgYiwgcnIsIG1hdCkgPT4geyBjb25zdCBkID0gYi5jbG9uZSgpLnN1YihhKSwgbyA9IGFkZChwLCBuLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeShyciwgcnIsIGQubGVuZ3RoKCksIDIwKSwgbWF0KTsKICAgIG8ucG9zaXRpb24uY29weShhKS5hZGRTY2FsZWRWZWN0b3IoZCwgMC41KTsgby5xdWF0ZXJuaW9uLnNldEZyb21Vbml0VmVjdG9ycyhWKDAsMSwwKSwgZC5ub3JtYWxpemUoKSk7IHJldHVybiBvOyB9OwogIGNvbnN0IGdyb3VwID0gbmV3IFRIUkVFLkdyb3VwKCk7IGdyb3VwLm5hbWUgPSAndHJpcG9kbGFtcGUnOwogIGNvbnN0IEhVQiA9IFYoMCwgMC4zNiwgMCksIEZSID0gMC4xNzsKICBmb3IgKGxldCBpID0gMDsgaSA8IDM7IGkrKyl7CiAgICBjb25zdCBhID0gTWF0aC5QSS8yICsgaSoyKk1hdGguUEkvMywgZm9vdCA9IFYoTWF0aC5jb3MoYSkqRlIsIDAuMDA2LCBNYXRoLnNpbihhKSpGUik7CiAgICByb2QoZ3JvdXAsICd0cmlwb2RfYmVpbl8nKyhpKzEpLCBmb290LCBIVUIuY2xvbmUoKS5hZGQoVihNYXRoLmNvcyhhKSowLjAxMiwgMCwgTWF0aC5zaW4oYSkqMC4wMTIpKSwgMC4wMDUsIE0uc3RhaGwpOwogICAgYWRkKGdyb3VwLCAndHJpcG9kX2Z1c3NfJysoaSsxKSwgbmV3IFRIUkVFLkN5bGluZGVyR2VvbWV0cnkoMC4wMDU4LCAwLjAwNTgsIDAuMDEyLCAxNiksIE0uZ3VtbWksIFtmb290LngsIDAuMDA2LCBmb290LnpdKTsKICB9CiAgYWRkKGdyb3VwLCAndHJpcG9kX2tub3RlbicsIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KDAuMDE2LCAwLjAxOCwgMC4wMjgsIDI4KSwgTS5zdGFobCwgWzAsIDAuMzYsIDBdKTsKICBjb25zdCBIVyA9IDAuMTQsIEhIID0gMC4xMiwgSEQgPSAwLjExOwogIGNvbnN0IGFybSA9IG5ldyBUSFJFRS5DYXRtdWxsUm9tQ3VydmUzKFtWKDAsMC4zNzQsMCksIFYoLTAuMDEsMC40MSwwKSwgVigtMC4wNDUsMC40NSwwLjAwNSksIFYoLTAuMDksMC40ODUsMC4wMSksIFYoLTAuMDkyLDAuNDk1LDAuMDEpLCBWKC1IVy8yLTAuMDA4LDAuNDksMC4wMSldKTsKICBhZGQoZ3JvdXAsICd0cmlwb2RfYnVlZ2VsJywgbmV3IFRIUkVFLlR1YmVHZW9tZXRyeShhcm0sIDQ4LCAwLjAwNTUsIDEyKSwgTS5zdGFobCk7CiAgY29uc3Qga29wZiA9IG5ldyBUSFJFRS5Hcm91cCgpOyBrb3BmLm5hbWUgPSAndHJpcG9kX2tvcGYnOyBrb3BmLnBvc2l0aW9uLnNldCgwLCAwLjQ5LCAwLjAxKTsga29wZi5yb3RhdGlvbi5zZXQoMC4xMiwgMC4xNSwgMCk7IGdyb3VwLmFkZChrb3BmKTsKICBhZGQoa29wZiwgJ3RyaXBvZF9ob2x6a29lcnBlcicsIG5ldyBUSFJFRS5Cb3hHZW9tZXRyeShIVywgSEgsIEhEKSwgTS5ob2x6KTsKICBhZGQoa29wZiwgJ3RyaXBvZF9nZWxlbmsnLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeSgwLjAxMywgMC4wMTMsIDAuMDEsIDI4KSwgTS5zdGFobCwgWy1IVy8yLTAuMDA1LCAwLCAwXSkucm90YXRpb24ueiA9IE1hdGguUEkvMjsKICBhZGQoa29wZiwgJ3RyaXBvZF9ibGVuZGUnLCBuZXcgVEhSRUUuVG9ydXNHZW9tZXRyeSgwLjAzNiwgMC4wMDUsIDEyLCA0OCksIE0uYmxlbmRlLCBbMC4wMSwgMCwgSEQvMiswLjAwMV0pOwogIGFkZChrb3BmLCAndHJpcG9kX3JlZmxla3RvcicsIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KDAuMDM2LCAwLjAzLCAwLjAxMiwgNDgsIDEsIHRydWUpLCBNLmJsZW5kZSwgWzAuMDEsIDAsIEhELzItMC4wMDVdKS5yb3RhdGlvbi54ID0gTWF0aC5QSS8yOwogIGNvbnN0IGxpbnNlID0gYWRkKGtvcGYsICd0cmlwb2RfbGVkX2xpbnNlJywgbmV3IFRIUkVFLkNpcmNsZUdlb21ldHJ5KDAuMDM0LCA0OCksIE0ubGVkLCBbMC4wMSwgMCwgSEQvMi0wLjAwOF0pOwogIGNvbnN0IGthYmVsID0gbmV3IFRIUkVFLkNhdG11bGxSb21DdXJ2ZTMoW1YoLTAuMDA1LDAuMzUsLTAuMDEyKSwgVigtMC4wMywwLjI4LC0wLjA0KSwgVigtMC4wNywwLjE2LC0wLjA4KSwgVigtMC4xMSwwLjA1LC0wLjEyKSwgVigtMC4xNCwwLjAwNCwtMC4xNiksIFYoLTAuMjgsMC4wMDQsLTAuMjQpLCBWKC0wLjQ1LDAuMDA0LC0wLjIpXSk7CiAgYWRkKGdyb3VwLCAndHJpcG9kX2thYmVsJywgbmV3IFRIUkVFLlR1YmVHZW9tZXRyeShrYWJlbCwgNjQsIDAuMDAyMiwgOCksIE0ua2FiZWwpOwogIGdyb3VwLnRyYXZlcnNlKG8gPT4geyBpZiAoby5pc01lc2gpeyBvLmNhc3RTaGFkb3cgPSB0cnVlOyBvLnJlY2VpdmVTaGFkb3cgPSB0cnVlOyB9IH0pOwogIGxpbnNlLmNhc3RTaGFkb3cgPSBmYWxzZTsKICAvLyBMaWNodHF1ZWxsZW4gYW0gS29wZiAoSW50ZW5zaXTDpHQgMCA9IGF1cykKICBjb25zdCBzcG90ID0gbmV3IFRIUkVFLlNwb3RMaWdodCgweGYyZjVmZiwgMCwgMCwgMC41NSwgMC41LCAyKTsKICBzcG90LnBvc2l0aW9uLnNldCgwLjAxLCAwLCBIRC8yICsgMC4wMDUpOyBzcG90LnRhcmdldC5wb3NpdGlvbi5zZXQoMC4wMSwgMCwgSEQvMiArIDEpOwogIHNwb3QuY2FzdFNoYWRvdyA9IHRydWU7IHNwb3Quc2hhZG93Lm1hcFNpemUuc2V0KDEwMjQsIDEwMjQpOyBzcG90LnNoYWRvdy5iaWFzID0gLTAuMDAwNTsgc3BvdC5zaGFkb3cubm9ybWFsQmlhcyA9IDAuMDI7CiAgc3BvdC5zaGFkb3cuY2FtZXJhLm5lYXIgPSAwLjAzOyBzcG90LnNoYWRvdy5jYW1lcmEuZmFyID0gODsKICBjb25zdCBnbG93ID0gbmV3IFRIUkVFLlBvaW50TGlnaHQoMHhmMmY1ZmYsIDAsIDMsIDIpOyBnbG93LnBvc2l0aW9uLnNldCgwLjAxLCAwLCBIRC8yICsgMC4wNSk7CiAga29wZi5hZGQoc3BvdCwgc3BvdC50YXJnZXQsIGdsb3cpOwogIHJldHVybiB7IGdyb3VwLCBrb3BmLCBNLCBzcG90LCBnbG93IH07Cn0K';\nconst stage = document.querySelector('three-d-stage');\nconst { THREE } = await stage.ready;\nconst statusEl = { set textContent(v){} };\nfor (const sel of ['.toolbar','.note']) { const el = stage.shadowRoot.querySelector(sel); if (el) el.style.display = 'none'; }\n\n// ---------- Maße: Grundriss in Bild-Pixeln → Meter ----------\nconst S = 0.009; // 1 px ≈ 0.9 cm\nconst OX = 610, OZ = 725; // Ursprung ~ Mitte\nconst mx = px => (px - OX) * S, mz = py => (py - OZ) * S;\nconst H = 2.5, TE = 0.2, TI = 0.1; // Wandhöhe, Außen-/Innenwand\nconst DOOR_W = 0.9, DOOR_H = 2.1, WIN_B = 0.9, WIN_T = 2.2;\n\n// ---------- Materialien ----------\nconst M = {\n floor: new THREE.MeshStandardMaterial({ name:'rubber_tiles', color:0x8a8a86, roughness:0.9 }),\n wall: new THREE.MeshStandardMaterial({ name:'white_stucco', color:0xf2efe9, roughness:0.95 }),\n brick: new THREE.MeshStandardMaterial({ name:'brick_wall_04', color:0x9a5a44, roughness:0.9 }),\n cap: new THREE.MeshStandardMaterial({ name:'rough_plaster_brick_04', color:0xb8a898, roughness:0.95 }),\n plinth: new THREE.MeshStandardMaterial({ name:'plinth', color:0x2f2d2a, roughness:0.7 }),\n wood: new THREE.MeshStandardMaterial({ name:'door_white_lacquer', color:0xf7f5f0, roughness:0.35 }),\n metal: new THREE.MeshStandardMaterial({ name:'brushed_steel', color:0xb9bcc0, roughness:0.3, metalness:0.35 }),\n glass: new THREE.MeshStandardMaterial({ name:'glass', color:0xbfd8e4, roughness:0.05, transparent:true, opacity:0.35 }),\n balcony:new THREE.MeshStandardMaterial({ name:'balcony_concrete', color:0xa9a59d, roughness:0.9 }),\n};\n\n// Welt-UVs (Box-Projektion), damit Texturen nicht gestreckt werden\nfunction worldUV(geo, scale){\n const p = geo.attributes.position, n = geo.attributes.normal, uv = geo.attributes.uv;\n for (let i = 0; i < p.count; i++){\n const ax = Math.abs(n.getX(i)), ay = Math.abs(n.getY(i)), az = Math.abs(n.getZ(i));\n let u, v;\n if (ay >= ax && ay >= az){ u = p.getX(i); v = p.getZ(i); }\n else if (ax >= az){ u = p.getZ(i); v = p.getY(i); }\n else { u = p.getX(i); v = p.getY(i); }\n uv.setXY(i, u/scale, v/scale);\n }\n uv.needsUpdate = true;\n return geo;\n}\n// Box in Weltkoordinaten (min/max)\nfunction box(name, mat, x0,y0,z0, x1,y1,z1, uvScale){\n const g = new THREE.BoxGeometry(x1-x0, y1-y0, z1-z0);\n g.translate((x0+x1)/2, (y0+y1)/2, (z0+z1)/2);\n if (uvScale) worldUV(g, uvScale);\n const m = new THREE.Mesh(g, mat); m.name = name; return m;\n}\n\nconst home = new THREE.Group(); home.name = 'wohnung';\nconst walls = new THREE.Group(); walls.name = 'waende';\nconst doors = new THREE.Group(); doors.name = 'tueren';\nconst windows = new THREE.Group(); windows.name = 'fenster';\nhome.add(walls, doors, windows);\n\n// ---------- Tür (prozedural, Kassettentür) ----------\nfunction makeDoor(name, t, openDeg, glassDoor=false){\n const g = new THREE.Group(); g.name = name;\n const w = DOOR_W, d = t + 0.03;\n g.add(box(name+'_zarge_l', M.wood, -w/2, 0, -d/2, -w/2+0.04, DOOR_H, d/2));\n g.add(box(name+'_zarge_r', M.wood, w/2-0.04, 0, -d/2, w/2, DOOR_H, d/2));\n g.add(box(name+'_zarge_oben', M.wood, -w/2, DOOR_H-0.04, -d/2, w/2, DOOR_H, d/2));\n const lw = w - 0.08, lh = DOOR_H - 0.05, lt = 0.04;\n const pivot = new THREE.Group(); pivot.name = name+'_blatt';\n pivot.position.set(-w/2 + 0.04, 0.005, 0);\n pivot.rotation.y = THREE.MathUtils.degToRad(openDeg);\n pivot.add(box(name+'_tuerblatt', M.wood, 0, 0, -lt/2, lw, lh, lt/2));\n for (const s of [1,-1]){\n const z0 = s*0.014, z1 = s*0.026, zz=[Math.min(z0,z1),Math.max(z0,z1)];\n if (glassDoor){\n pivot.add(box(name+'_glas', M.glass, 0.1, 0.35, -0.006, lw-0.1, lh-0.12, 0.006));\n } else {\n pivot.add(box(name+'_kassette_o'+s, M.wood, 0.11, 1.05, zz[0], lw-0.11, lh-0.14, zz[1]));\n pivot.add(box(name+'_kassette_u'+s, M.wood, 0.11, 0.14, zz[0], lw-0.11, 0.9, zz[1]));\n }\n const rose = new THREE.Mesh(new THREE.CylinderGeometry(0.026,0.026,0.012,32), M.metal);\n rose.name = name+'_rosette'+s; rose.rotation.x = Math.PI/2; rose.position.set(lw-0.07, 1.05, s*(lt/2+0.006)); pivot.add(rose);\n const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.009,0.009,0.05,20), M.metal);\n neck.name = name+'_hals'+s; neck.rotation.x = Math.PI/2; neck.position.set(lw-0.07, 1.05, s*(lt/2+0.03)); pivot.add(neck);\n const lever = new THREE.Mesh(new THREE.CapsuleGeometry(0.009, 0.12, 6, 16), M.metal);\n lever.name = name+'_druecker'+s; lever.rotation.z = Math.PI/2; lever.position.set(lw-0.13, 1.05, s*(lt/2+0.055)); pivot.add(lever);\n }\n for (const y of [0.25, lh-0.25]){\n const h = new THREE.Mesh(new THREE.CylinderGeometry(0.008,0.008,0.1,16), M.metal);\n h.name = name+'_band'; h.position.set(-0.004, y, lt/2); pivot.add(h);\n }\n g.add(pivot);\n return g;\n}\n\n// ---------- Fenster ----------\nfunction makeWindow(name, w, t, bottom, top){\n const g = new THREE.Group(); g.name = name;\n const f = 0.05, d = 0.07;\n g.add(box(name+'_rahmen_l', M.wood, -w/2, bottom, -d/2, -w/2+f, top, d/2));\n g.add(box(name+'_rahmen_r', M.wood, w/2-f, bottom, -d/2, w/2, top, d/2));\n g.add(box(name+'_rahmen_o', M.wood, -w/2+f, top-f, -d/2, w/2-f, top, d/2));\n g.add(box(name+'_rahmen_u', M.wood, -w/2+f, bottom, -d/2, w/2-f, bottom+f, d/2));\n if (w > 1.1) g.add(box(name+'_pfosten', M.wood, -0.025, bottom+f, -d/2+0.005, 0.025, top-f, d/2-0.005));\n g.add(box(name+'_glas', M.glass, -w/2+f, bottom+f, -0.005, w/2-f, top-f, 0.005));\n if (bottom > 0) g.add(box(name+'_bank', M.wood, -w/2-0.05, bottom-0.027, -t/2-0.04, w/2+0.05, bottom+0.012, t/2+0.04));\n return g;\n}\n\n// ---------- Wand mit Öffnungen (achsparallel) ----------\n// ops: {c: Mittelpunkt in px, w: Breite m, type:'door'|'window'|'glassdoor', open, name}\n// outside: Box-Face-Index der Außenseite (0:+x 1:-x 4:+z 5:-z) → Klinker außen, Putz innen\nfunction wall(name, a, b, fixed, horizontal, t, h, ops=[], ext=0, outside=-1){\n const A = (horizontal ? mx(a) : mz(a)) - ext, B = (horizontal ? mx(b) : mz(b)) + ext;\n const F = horizontal ? mz(fixed) : mx(fixed);\n const seg = (n, s0, s1, y0, y1, ends=[]) => {\n if (s1 - s0 < 0.001) return;\n const mat = Array(6).fill(M.wall);\n if (outside >= 0){ mat[outside] = M.brick; ends.forEach(e => mat[e] = M.brick); }\n if (y1 >= h - 0.0001) mat[2] = M.cap; // Schnittfläche oben\n walls.add(horizontal ? box(n, mat, s0, y0, F-t/2, s1, y1, F+t/2, 1.5)\n : box(n, mat, F-t/2, y0, s0, F+t/2, y1, s1, 1.5));\n };\n const endA = ext > 0 ? [horizontal ? 1 : 5] : [], endB = ext > 0 ? [horizontal ? 0 : 4] : [];\n const list = ops.map(o => ({...o, c: horizontal ? mx(o.c) : mz(o.c)})).sort((p,q)=>p.c-q.c);\n let cur = A;\n list.forEach((o, i) => {\n const s0 = o.c - o.w/2, s1 = o.c + o.w/2;\n seg(`${name}_${i}`, cur, s0, 0, h, i === 0 ? endA : []);\n const top = o.type === 'window' ? WIN_T : (o.type === 'glassdoor' ? WIN_T : DOOR_H);\n seg(`${name}_sturz_${i}`, s0, s1, top, h);\n if (o.type === 'window') seg(`${name}_bruestung_${i}`, s0, s1, 0, WIN_B);\n let el;\n if (o.type === 'door') { el = makeDoor(o.name, t, o.open ?? 0); doors.add(el); }\n else if (o.type === 'glassdoor') { el = makeWindow(o.name, o.w, t, 0, WIN_T); windows.add(el); }\n else { el = makeWindow(o.name, o.w, t, WIN_B, WIN_T); windows.add(el); }\n if (horizontal) el.position.set(o.c, 0, F);\n else { el.position.set(F, 0, o.c); el.rotation.y = Math.PI/2; }\n if (o.flip) el.rotation.y += Math.PI;\n cur = s1;\n });\n seg(`${name}_${list.length}`, cur, B, 0, h, list.length === 0 ? [...endA, ...endB] : endB);\n}\n\n// Grundriss-Koordinaten (px): außen x 140–1080, y 230–1225; Mittelwand x 610\nconst L=140, R=1080, T=230, Bt=1225, MID=610, BAD=690, FLUR=815;\nconst HI = H - 0.003; // Innenwände minimal niedriger → kein Z-Fighting auf den Oberkanten\n\n// Außenwände\nwall('aussen_nord', L, R, T, true, TE, H, [\n { c: 415, w: 1.4, type:'window', name:'fenster_bad' },\n { c: 800, w: 1.0, type:'glassdoor', name:'balkontuer' },\n { c: 960, w: 1.0, type:'window', name:'fenster_kueche_nord' },\n], TE/2, 5);\nwall('aussen_sued', L, R, Bt, true, TE, H, [\n { c: 375, w: 1.5, type:'window', name:'fenster_wohnzimmer' },\n { c: 845, w: 1.5, type:'window', name:'fenster_schlafzimmer' },\n], TE/2, 4);\nwall('aussen_west', T, Bt, L, false, TE, H, [\n { c: 752, w: DOOR_W, type:'door', open: -35, name:'wohnungstuer' },\n], 0, 1);\nwall('aussen_ost', T, Bt, R, false, TE, H, [\n { c: 530, w: 1.3, type:'window', name:'fenster_kueche' },\n { c: 1020, w: 1.3, type:'window', name:'fenster_schlafzimmer_ost' },\n], 0, 0);\n// Innenwände\nwall('innen_mitte', T, Bt, MID, false, TI, HI, [\n { c: 752, w: DOOR_W, type:'door', open: 70, name:'tuer_kueche' },\n], -TE/2 + 0.0);\nwall('innen_bad_flur', L, MID, BAD, true, TI, HI, [\n { c: 245, w: DOOR_W, type:'door', open: -60, flip:true, name:'tuer_bad' },\n]);\nwall('innen_flur_wohnen', L, MID, FLUR, true, TI, HI, [\n { c: 245, w: DOOR_W, type:'door', open: 55, name:'tuer_wohnzimmer' },\n]);\nwall('innen_kueche_schlafen', MID, R, FLUR, true, TI, HI, [\n { c: 830, w: DOOR_W, type:'door', open: 65, name:'tuer_schlafzimmer' },\n]);\n\n// ---------- Boden, Sockel, Balkon ----------\nconst BL=670, BR=1030, BT=150;\nconst floorGeo = new THREE.PlaneGeometry(mx(R)-mx(L), mz(Bt)-mz(T));\nfloorGeo.rotateX(-Math.PI/2);\nfloorGeo.translate((mx(L)+mx(R))/2, 0.002, (mz(T)+mz(Bt))/2);\nworldUV(floorGeo, 1.0);\nconst floor = new THREE.Mesh(floorGeo, M.floor); floor.name = 'boden_rubber_tiles';\nhome.add(floor);\nhome.add(box('sockel', M.plinth, mx(L)-TE/2-0.05, -0.12, mz(T)-TE/2-0.05, mx(R)+TE/2+0.05, 0, mz(Bt)+TE/2+0.05));\nhome.add(box('balkon_platte', M.balcony, mx(BL), -0.12, mz(BT), mx(BR), 0.02, mz(T)-TE/2));\nconst PH = 1.0, PT = 0.08;\nhome.add(box('balkon_bruestung_w', M.wall, mx(BL), 0.02, mz(BT), mx(BL)+PT, PH, mz(T)-TE/2, 1.5));\nhome.add(box('balkon_bruestung_o', M.wall, mx(BR)-PT, 0.02, mz(BT), mx(BR), PH, mz(T)-TE/2, 1.5));\nhome.add(box('balkon_bruestung_n', M.wall, mx(BL)+PT, 0.02, mz(BT), mx(BR)-PT, PH-0.002, mz(BT)+PT, 1.5));\n\n// ---------- Wohnzimmer (Raum Süd-Ost) ----------\nconst wohn = new THREE.Group(); wohn.name = 'wohnzimmer';\nconst IN_O = mx(R) - TE/2, IN_S = mz(Bt) - TE/2; // Innenkanten Ost-/Südwand\n// Sofa unter dem Ostfenster (links beim Betreten), Rücken zur Wand\nconst sofa = buildSofa(THREE);\nsofa.group.position.set(IN_O - sofa.D/2 - 0.03, 0.002, mz(1020));\nsofa.group.rotation.y = -Math.PI/2;\nwohn.add(sofa.group);\n// 3 Regale nebeneinander unter dem Südfenster, Front zum Raum\nconst RM = regalMaterials(THREE);\nfor (const [i, dx] of [[1,-0.603],[2,0],[3,0.603]]){\n const r = buildRegal(THREE, RM); r.name = 'regal_'+i;\n r.position.set(mx(845) + dx, 0.002, IN_S - 0.135 - 0.02);\n r.rotation.y = Math.PI;\n wohn.add(r);\n}\n// Boxen oben auf den äußeren Regalen, leicht nach innen gedreht\n{\n const bx = buildBoxen(THREE), top = new THREE.Box3().setFromObject(wohn.getObjectByName('regal_1')).max.y;\n const TURN = THREE.MathUtils.degToRad(15), X = 0.603 + 0.30 - 0.105, Z = IN_S - 0.135 - 0.02;\n // Aktivbox links (–x), Bedienfeld zeigt nach außen\n bx.rechts.position.set(mx(845) - X, top, Z); bx.rechts.rotation.y = Math.PI - TURN;\n bx.links.position.set(mx(845) + X, top, Z); bx.links.rotation.y = Math.PI + TURN;\n wohn.add(bx.links, bx.rechts);\n}\n// Bogenlampe in der Fensterecke, Schirm über dem Sofa – zur Raummitte versetzt\nconst lampe = buildLampe(THREE);\nconst LB = new THREE.Vector2(IN_O - 0.22, IN_S - 0.25);\nconst LT = new THREE.Vector2(IN_O - 0.95, mz(1020) + 0.25);\nlampe.group.position.set(LB.x, 0.002, LB.y);\nlampe.group.rotation.y = Math.atan2(-(LT.y - LB.y), LT.x - LB.x);\nwohn.add(lampe.group);\n// Donut-Lampe (Ø 30 cm, H 11 cm) im linken Regal, mittleres Fach, mittig\nconst DM = {\n koerper: new THREE.MeshPhysicalMaterial({ name:'donut_opal_orange', color:0xffa04a, roughness:0.6, clearcoat:0.2, clearcoatRoughness:0.5, emissive:0xff8a2a, emissiveIntensity:0 }),\n sockel: new THREE.MeshStandardMaterial({ name:'donut_sockel', color:0xee6a3e, roughness:0.5, emissive:0xd04a20, emissiveIntensity:0 }),\n knopf: new THREE.MeshStandardMaterial({ name:'donut_knopf', color:0x9a6a58, roughness:0.45 }),\n};\nconst donut = new THREE.Group(); donut.name = 'donutlampe';\n{\n const V2 = (x,y) => new THREE.Vector2(x,y), dm = (n,g,m,y=0) => { const o = new THREE.Mesh(g,m); o.name = n; o.position.y = y; donut.add(o); };\n const SB_R = 0.12, SB_H = 0.018, sp = [V2(0,0), V2(SB_R-0.006,0)];\n for (let i = 0; i <= 12; i++){ const a = -Math.PI/2 + i/12*Math.PI; sp.push(V2(SB_R-0.006+Math.cos(a)*0.006, 0.006+Math.sin(a)*0.006+(i>6?(SB_H-0.012)*(i-6)/6:0))); }\n sp.push(V2(0, SB_H));\n dm('donut_sockel', new THREE.LatheGeometry(sp, 96), DM.sockel);\n const R = 0.085, A = 0.065, B = 0.0475, CY = 0.0625, sg = v => Math.sign(v)*Math.pow(Math.abs(v), 0.8), kp = [V2(0,0.015)];\n for (let i = 0; i <= 60; i++){ const t = -Math.PI/2 + i/60*1.35*Math.PI; kp.push(V2(R+A*sg(Math.cos(t)), CY+B*sg(Math.sin(t)))); }\n kp.push(V2(0.018,0.078), V2(0.008,0.0755), V2(0,0.075));\n dm('donut_koerper', new THREE.LatheGeometry(kp, 128), DM.koerper);\n dm('donut_knopf', new THREE.CylinderGeometry(0.02, 0.022, 0.004, 40), DM.knopf, 0.077);\n}\ndonut.position.set(mx(845) - 0.603, 0.002 + 0.42, IN_S - 0.135 - 0.02);\nwohn.add(donut);\nwohn.traverse(o => { if (o.isMesh){ o.castShadow = true; o.receiveShadow = true; } });\nhome.add(wohn);\n// ---------- Schlafzimmer: Tripod-Lampe auf dem Boden, Südwest-Ecke, Spot zur Raummitte ----------\nconst trip = buildTripod(THREE);\n{\n const IN_W = mx(L) + TE/2, px = IN_W + 0.35, pz = IN_S - 0.35;\n const cx = (mx(L) + mx(MID))/2, cz = (mz(FLUR) + mz(Bt))/2;\n trip.group.position.set(px, 0.002, pz);\n trip.group.rotation.y = Math.atan2(cx - px, cz - pz);\n home.add(trip.group);\n}\n\nstage.setObject(home);\n\n// ---------- Schatten: enger Frustum, 4096er Map, harter PCF → scharf & ohne Flackern ----------\nconst renderer = stage._renderer, scene = stage._scene, key = stage._key;\nrenderer.shadowMap.type = THREE.PCFShadowMap;\nconst hb = new THREE.Box3().setFromObject(home), hc = hb.getCenter(new THREE.Vector3()), hs = hb.getSize(new THREE.Vector3());\nconst sunDir = new THREE.Vector3(4, 7, 5).normalize();\nkey.position.copy(hc).addScaledVector(sunDir, 30);\nkey.target.position.copy(hc); scene.add(key.target);\nconst half = Math.max(hs.x, hs.z) * 0.62;\nObject.assign(key.shadow.camera, { left:-half, right:half, top:half, bottom:-half, near:1, far:60 });\nkey.shadow.camera.updateProjectionMatrix();\nkey.shadow.mapSize.set(2048, 2048);\nkey.shadow.bias = -0.0004; key.shadow.normalBias = 0.015;\nif (key.shadow.map){ key.shadow.map.dispose(); key.shadow.map = null; }\nrenderer.toneMapping = THREE.ACESFilmicToneMapping;\nif (stage._ground) stage._ground.visible = false;\nconst hemi = scene.children.find(o => o.isHemisphereLight), hemiBase = hemi ? hemi.intensity : 0;\nconst fillL = scene.children.find(o => o.isDirectionalLight && o !== key), fillBase = fillL ? fillL.intensity : 0;\nconst ENV = 0.9;\nlet envK = 1, hemiK = 1, fillK = 1, expK = 1;\n// Innenraum: Himmels-/Fülllicht dringt nicht durch die Decke → nur noch Sonne + Umgebung durch die Fenster\nfunction applyInterior(o){\n if (hemi) hemi.intensity = hemiBase * hemiK * (1 - 0.85*o);\n if (fillL) fillL.intensity = fillBase * fillK * (1 - o);\n scene.environmentIntensity = ENV * envK * (1 - 0.7*o);\n renderer.toneMappingExposure = (1 + 0.45*o) * expK;\n}\n\n// ---------- Decke (blendet ein, sobald man in einem Raum steht) ----------\nconst ceilMat = new THREE.MeshStandardMaterial({ name:'decke', color:0xf4f2ee, roughness:0.95, transparent:true, opacity:0, depthWrite:false });\nconst ceiling = box('decke', ceilMat, mx(L)-TE/2, H, mz(T)-TE/2, mx(R)+TE/2, H+0.04, mz(Bt)+TE/2);\nceiling.receiveShadow = true; ceiling.castShadow = true; ceiling.visible = false;\nhome.add(ceiling);\nfunction setCeil(o){\n ceilMat.opacity = o;\n if (ceiling.visible !== (o > 0.001)){ ceiling.visible = o > 0.001; ptDirty = true; }\n const solid = o >= 0.999;\n if (ceilMat.transparent === solid){ ceilMat.transparent = !solid; ceilMat.depthWrite = solid; ceilMat.needsUpdate = true; }\n if (!ptOn) applyInterior(o);\n}\n\n// ---------- Bogenlampe: Licht an/aus ----------\nhome.updateMatrixWorld(true);\nconst bulb = lampe.group.getObjectByName('leuchtmittel');\nconst bp = bulb.getWorldPosition(new THREE.Vector3());\nconst lampSpot = new THREE.SpotLight(0xffd6a0, 0, 0, 1.0, 0.6, 2);\nlampSpot.position.copy(bp).add(new THREE.Vector3(0, -0.035, 0));\nlampSpot.target.position.set(bp.x, 0, bp.z);\nlampSpot.castShadow = true;\nlampSpot.shadow.mapSize.set(1024, 1024);\nlampSpot.shadow.bias = -0.0005; lampSpot.shadow.normalBias = 0.02;\nlampSpot.shadow.camera.near = 0.05; lampSpot.shadow.camera.far = 8;\nconst lampFill = new THREE.PointLight(0xffd6a0, 0, 7, 2);\nlampFill.position.copy(bp);\nscene.add(lampSpot, lampSpot.target, lampFill);\nconst schirm = lampe.group.getObjectByName('schirm').material;\nschirm.emissive = new THREE.Color(0xffe2b8);\nconst lampBtn = document.getElementById('lamp');\nlet lampOn = false, lampLvl = 0, lampAnim = 0;\nlet rasterEnv = null, ptEnv = null;\nlet tracer = null, ptOn = false, ptDirty = true, inRoom = false, stillSince = 0;\nconst LAMP_KEY = 'wohnung-bogenlampe-farbe';\nlet lampK = 1;\nconst lampSw = document.getElementById('lampSw');\nfunction setLampColor(hex){\n const c = new THREE.Color(hex), s = c.clone().lerp(new THREE.Color(0xffffff), 0.3);\n lampSpot.color.copy(c); lampFill.color.copy(c);\n if (bulb.material.emissive) bulb.material.emissive.copy(c);\n schirm.emissive.copy(s);\n try { localStorage.setItem(LAMP_KEY, hex); } catch(e){}\n document.querySelector('#lamp i').style.cssText = lampOn ? `background:${hex};box-shadow:0 0 10px 3px ${hex}99` : '';\n if (ptOn){ tracer.updateLights(); tracer.updateMaterials(); }\n}\n// ---------- Licht-Picker: Hell (Lichtton + Helligkeit) oder Farbe (Farbkreis) ----------\nconst K_COLD = 8000, K_WARM = 2000;\nfunction kelvinHex(k){\n const t = k/100; let r, g, b;\n if (t <= 66){ r = 255; g = 99.4708025861*Math.log(t) - 161.1195681661; b = t <= 19 ? 0 : 138.5177312231*Math.log(t-10) - 305.0447927307; }\n else { r = 329.698727446*Math.pow(t-60, -0.1332047592); g = 288.1221695283*Math.pow(t-60, -0.0755148492); b = 255; }\n const c = v => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0');\n return '#' + c(r) + c(g) + c(b);\n}\nconst warmK = w => Math.round(K_COLD - w/100*(K_COLD - K_WARM));\nfunction hsvRGB(h, s, v){ const f = n => { const k = (n + h/60) % 6; return v - v*s*Math.max(0, Math.min(k, 4-k, 1)); }; return [f(5), f(3), f(1)]; }\nconst rgbHex = a => '#' + a.map(x => Math.round(x*255).toString(16).padStart(2, '0')).join('');\nfunction makePicker(sw, key, def, dimIn, onChange){\n let st = { mode: 'white', warm: 70, h: 30, s: 0.6, ...def };\n try { Object.assign(st, JSON.parse(localStorage.getItem(key) || 'null') || {}); } catch(e){}\n const pop = document.createElement('div'); pop.className = 'lpick';\n pop.innerHTML = '<div class=\"lp-seg\"><button data-m=\"white\">Hell<\/button><button data-m=\"color\">Farbe<\/button><\/div>'\n + '<div class=\"lp-row lp-w\"><div><span>Lichtton<\/span><output class=\"lp-kv\"><\/output><\/div><input class=\"lp-temp\" type=\"range\" min=\"0\" max=\"100\" step=\"1\"><small><span>Steril<\/span><span>Warm<\/span><\/small><\/div>'\n + '<div class=\"lp-c\"><div class=\"lp-wheel\"><canvas width=\"360\" height=\"360\"><\/canvas><i><\/i><\/div><\/div>'\n + '<div class=\"lp-row\"><div><span>Helligkeit<\/span><output class=\"lp-bv\"><\/output><\/div><input class=\"lp-dim\" type=\"range\" min=\"10\" max=\"200\" step=\"5\"><\/div><button class=\"lp-done\">Fertig<\/button>';\n document.body.appendChild(pop);\n const $ = s => pop.querySelector(s), temp = $('.lp-temp'), bri = $('.lp-dim'), bv = $('.lp-bv'), dot = $('.lp-wheel i'), wheel = $('.lp-wheel');\n temp.style.background = `linear-gradient(90deg,${kelvinHex(K_COLD)},${kelvinHex(4500)},${kelvinHex(3000)},${kelvinHex(K_WARM)})`;\n const cv = $('canvas'), ctx = cv.getContext('2d'), img = ctx.createImageData(360, 360), R = 180;\n for (let y = 0; y < 360; y++) for (let x = 0; x < 360; x++){\n const dx = x + .5 - R, dy = y + .5 - R, d = Math.hypot(dx, dy) / R, i = (y*360 + x)*4;\n const rgb = hsvRGB((Math.atan2(dy, dx)*180/Math.PI + 360) % 360, 1, 1);\n img.data[i] = rgb[0]*255; img.data[i+1] = rgb[1]*255; img.data[i+2] = rgb[2]*255;\n img.data[i+3] = 255 * Math.max(0, Math.min(1, (1 - d) * R, (d - 0.72) * R));\n }\n ctx.putImageData(img, 0, 0);\n const hex = () => st.mode === 'white' ? kelvinHex(warmK(st.warm)) : rgbHex(hsvRGB(st.h, 1, 1));\n function update(fire = true){\n pop.querySelectorAll('.lp-seg button').forEach(b => b.classList.toggle('on', b.dataset.m === st.mode));\n $('.lp-w').style.display = st.mode === 'white' ? '' : 'none';\n $('.lp-c').style.display = st.mode === 'color' ? '' : 'none';\n temp.value = st.warm; $('.lp-kv').textContent = warmK(st.warm) + ' K';\n const a = st.h*Math.PI/180;\n dot.style.left = (90 + Math.cos(a)*77) + 'px'; dot.style.top = (90 + Math.sin(a)*77) + 'px';\n const h = hex(); dot.style.background = h; sw.style.background = h;\n try { localStorage.setItem(key, JSON.stringify(st)); } catch(e){}\n if (fire) onChange(h);\n }\n pop.querySelectorAll('.lp-seg button').forEach(b => b.onclick = () => { st.mode = b.dataset.m; update(); });\n temp.oninput = () => { st.warm = +temp.value; update(); };\n const pick = e => { const r = wheel.getBoundingClientRect(), dx = e.clientX - r.left - 90, dy = e.clientY - r.top - 90;\n st.h = (Math.atan2(dy, dx)*180/Math.PI + 360) % 360; update(); };\n wheel.onpointerdown = e => { const r = wheel.getBoundingClientRect(), d = Math.hypot(e.clientX - r.left - 90, e.clientY - r.top - 90);\n if (d < 58 || d > 94) return; wheel.setPointerCapture(e.pointerId); pick(e); };\n wheel.onpointermove = e => { if (wheel.hasPointerCapture(e.pointerId)) pick(e); };\n const syncBri = () => { bri.value = dimIn.value; bv.textContent = dimIn.value + '%'; };\n bri.oninput = () => { dimIn.value = bri.value; dimIn.oninput(); syncBri(); };\n dimIn.addEventListener('input', syncBri);\n const place = () => { pop.style.left = '18px'; pop.style.top = '50%'; pop.style.transform = 'translateY(-50%)'; };\n sw.onclick = e => { e.preventDefault(); e.stopPropagation(); const o = !pop.classList.contains('open');\n document.querySelectorAll('.lpick.open').forEach(p => p.classList.remove('open'));\n if (o){ syncBri(); place(); pop.classList.add('open'); }\n document.body.classList.toggle('lpfocus', o);\n dispatchEvent(new CustomEvent('lpick', { detail: { key, open: o } })); };\n $('.lp-done').onclick = () => { pop.classList.remove('open'); document.body.classList.remove('lpfocus'); dispatchEvent(new CustomEvent('lpick', { detail: { key, open: false } })); };\n document.addEventListener('pointerdown', e => { if (pop.classList.contains('open') && !pop.contains(e.target) && !sw.contains(e.target)){ pop.classList.remove('open'); document.body.classList.remove('lpfocus'); dispatchEvent(new CustomEvent('lpick', { detail: { key, open: false } })); } });\n addEventListener('resize', () => pop.classList.contains('open') && place());\n update();\n return pop;\n}\n\nlet lampCol = localStorage.getItem(LAMP_KEY) || '#ffd6a0';\nlampSw.innerHTML = '';\nconst colIn = document.createElement('button'); colIn.className = 'lp-sw'; colIn.title = 'Licht einstellen';\nlampSw.appendChild(colIn);\nconst dimIn = document.querySelector('#lampDim input'), dimOut = document.querySelector('#lampDim output');\ndimIn.oninput = () => { lampK = dimIn.value / 100; dimOut.textContent = dimIn.value + '%'; applyLamp(lampLvl); if (ptOn){ tracer.updateLights(); tracer.updateMaterials(); } };\nfunction applyLamp(v){\n v *= lampK;\n lampSpot.intensity = 9 * v;\n lampFill.intensity = 2.2 * v;\n bulb.material.emissiveIntensity = 0.2 + 5 * v;\n schirm.emissiveIntensity = 0.55 * v;\n}\napplyLamp(0);\nmakePicker(colIn, LAMP_KEY + '-cfg', { mode: 'white', warm: 75 }, dimIn, h => { lampCol = h; setLampColor(h); });\nsetLampColor(lampCol);\ndocument.getElementById('lampT').onclick = () => {\n lampOn = !lampOn;\n lampBtn.classList.toggle('on', lampOn);\n lampBtn.querySelector('span').textContent = lampOn ? 'Bogenlampe aus' : 'Bogenlampe an';\n setLampColor(lampCol);\n const id = ++lampAnim, from = lampLvl, to = lampOn ? 1 : 0, t0 = performance.now();\n (function step(now){\n if (id !== lampAnim) return;\n const t = Math.min(1, (now - t0) / 450);\n lampLvl = from + (to - from) * (1 - Math.pow(1 - t, 3));\n applyLamp(lampLvl);\n if (ptOn){ tracer.updateLights(); tracer.updateMaterials(); }\n if (t < 1) requestAnimationFrame(step);\n })(t0);\n};\n\n// ---------- Donut-Lampe: nur an/aus ----------\nconst donutLight = new THREE.PointLight(0xffa050, 0, 4.5, 2);\ndonutLight.position.copy(donut.getWorldPosition(new THREE.Vector3())).add(new THREE.Vector3(0, 0.13, 0));\nscene.add(donutLight);\nconst donutBtn = document.getElementById('lampD');\ndonutBtn.onclick = () => {\n const on = donutBtn.getAttribute('aria-pressed') !== 'true';\n donutBtn.setAttribute('aria-pressed', on); donutBtn.classList.toggle('on', on);\n donutBtn.querySelector('span').textContent = on ? 'Donut-Lampe aus' : 'Donut-Lampe an';\n DM.koerper.emissiveIntensity = on ? 2.2 : 0;\n DM.sockel.emissiveIntensity = on ? 0.6 : 0;\n donutLight.intensity = on ? 2.6 : 0;\n if (ptOn){ tracer.updateLights(); tracer.updateMaterials(); }\n};\n\n// ---------- Tripod-Lampe: an/aus, Farbe, Stärke, Kegel ----------\nconst tripBox = document.getElementById('lamp2'), tripDot = document.querySelector('#tripT i');\nconst TRIP_KEY = 'wohnung-tripod-farbe';\nconst tripColIn = document.getElementById('tripCol'), tripDimIn = document.getElementById('tripDim'), tripConeIn = document.getElementById('tripCone');\nlet tripOn = false, tripLvl = 0, tripAnim = 0, tripK = 1;\nlet tripCol = localStorage.getItem(TRIP_KEY) || '#f2f5ff';\nconst tripSync = () => { if (ptOn){ tracer.updateLights(); tracer.updateMaterials(); } };\nfunction applyTrip(){\n const v = tripLvl * tripK, c = new THREE.Color(tripCol);\n trip.spot.color.copy(c); trip.glow.color.copy(c); trip.M.led.emissive.copy(c);\n trip.M.led.color.set(tripLvl > 0.01 ? 0xffffff : 0xd8dadd);\n trip.spot.intensity = 12 * v; trip.glow.intensity = 0.35 * v;\n trip.M.led.emissiveIntensity = 4 * v;\n tripDot.style.cssText = tripOn ? `background:${tripCol};box-shadow:0 0 10px 3px ${tripCol}99` : '';\n}\ntripDimIn.oninput = () => { tripK = tripDimIn.value/100; tripDimIn.nextElementSibling.textContent = tripDimIn.value + '%'; applyTrip(); tripSync(); };\nmakePicker(tripColIn, TRIP_KEY + '-cfg', { mode: 'white', warm: 5 }, tripDimIn, h => { tripCol = h; try { localStorage.setItem(TRIP_KEY, h); } catch(e){} applyTrip(); tripSync(); });\ntripConeIn.oninput = () => { trip.spot.angle = THREE.MathUtils.degToRad(tripConeIn.value); tripConeIn.nextElementSibling.textContent = tripConeIn.value + '°'; tripSync(); };\ntripConeIn.oninput();\napplyTrip();\ndocument.getElementById('tripT').onclick = () => {\n tripOn = !tripOn;\n tripBox.classList.toggle('on', tripOn);\n tripBox.querySelector('#tripT span').textContent = tripOn ? 'Tripod-Lampe aus' : 'Tripod-Lampe an';\n const id = ++tripAnim, from = tripLvl, to = tripOn ? 1 : 0, t0 = performance.now();\n (function step(now){\n if (id !== tripAnim) return;\n const t = Math.min(1, (now - t0) / 450);\n tripLvl = from + (to - from) * (1 - Math.pow(1 - t, 3));\n applyTrip(); tripSync();\n if (t < 1) requestAnimationFrame(step);\n })(t0);\n};\n\n// ---------- Pathtracing (three-gpu-pathtracer): echte Lichtbündel mit Mehrfach-Reflexion, sobald die Kamera stillsteht ----------\n\nconst ptLabel = document.getElementById('pt');\nlet renderOn = false;\n// Sparsamer Standard-Renderer: N8AO (Screen-Space Ambient Occlusion) + Rendern nur bei Aktivität\nlet composer = null, aoPass = null, lastAct = performance.now(), lastInput = lastAct;\nconst act = () => { lastAct = lastInput = performance.now(); };\nfor (const ev of ['pointerdown','pointermove','wheel','keydown','input','click','resize','touchstart']) window.addEventListener(ev, act, true);\nPromise.all([import('three/examples/jsm/postprocessing/EffectComposer.js'), import('three/examples/jsm/postprocessing/RenderPass.js'), import('three/examples/jsm/postprocessing/OutputPass.js'), import('n8ao')]).then(([{EffectComposer},{RenderPass},{OutputPass},{N8AOPass}]) => {\n const s = renderer.getSize(new THREE.Vector2());\n composer = new EffectComposer(renderer);\n aoPass = new N8AOPass(scene, cam, s.x, s.y);\n Object.assign(aoPass.configuration, { aoRadius: 0.6, distanceFalloff: 0.5, intensity: 2.2, aoSamples: 12, denoiseSamples: 6, denoiseRadius: 10, halfRes: true, gammaCorrection: false });\n aoPass.setQualityMode && aoPass.setQualityMode('Low');\n composer.addPass(aoPass); composer.addPass(new OutputPass());\n act();\n}).catch(e => console.warn('N8AO nicht verfügbar – einfacher Raster', e));\nconst _cs = new THREE.Vector2();\nconst rtBtn = document.getElementById('rt');\nrtBtn.onclick = () => {\n renderOn = !renderOn;\n rtBtn.classList.toggle('on', renderOn); rtBtn.setAttribute('aria-pressed', renderOn);\n rtBtn.querySelector('span').textContent = renderOn ? 'Render an' : 'Render aus';\n if (!renderOn && ptOn) stopPT();\n stillSince = performance.now();\n};\nimport('three-gpu-pathtracer').then(({ WebGLPathTracer }) => {\n tracer = new WebGLPathTracer(renderer);\n tracer.bounces = 4;\n tracer.renderScale = 0.5;\n tracer.dynamicLowRes = false;\n tracer.filterGlossyFactor = 0.5;\n tracer.tiles.set(4, 4);\n tracer.minSamples = 2;\n tracer.fadeDuration = 350;\n tracer.renderDelay = 0;\n}).catch(e => console.warn('Pathtracer nicht verfügbar – Raster-Fallback', e));\nfunction startPT(){\n scene.environment = ptEnv; scene.background = ptEnv;\n scene.environmentIntensity = ENV;\n renderer.toneMappingExposure = 2.4;\n if (ptDirty){ tracer.setScene(scene, cam); ptDirty = false; }\n else { tracer.updateCamera(); tracer.updateMaterials(); tracer.updateLights(); tracer.updateEnvironment(); }\n tracer.reset();\n ptOn = true; ptLabel.classList.add('show');\n}\nfunction stopPT(){ ptOn = false; scene.environment = rasterEnv; scene.background = cubeRT.texture; applyInterior(ceilMat.opacity); ptLabel.classList.remove('show'); }\nconst MAX_SAMPLES = 256; let ptFrame = 0;\nlet lastUser = -1e9;\nconst interrupt = () => { stillSince = lastUser = performance.now(); if (ptOn) stopPT(); };\nfor (const ev of ['pointerdown','wheel','keydown']) window.addEventListener(ev, interrupt, true);\nrenderer.domElement.addEventListener('pointerdown', interrupt, true);\nconst lastM = new THREE.Matrix4(); let lastAspect = 0;\nfunction camMoved(){\n const a = cam.matrixWorld.elements, b = lastM.elements; let d = 0;\n for (let i = 0; i < 16; i++) d += Math.abs(a[i] - b[i]);\n const m = d > 1e-5 || cam.aspect !== lastAspect;\n lastM.copy(cam.matrixWorld); lastAspect = cam.aspect;\n return m;\n}\nconst loop = () => {\n ctl.autoRotate = !inRoom && !lampFocus && ctl.enabled && performance.now() - lastUser > 6000;\n ctl.autoRotateSpeed = 0.4;\n document.body.classList.toggle('idle', performance.now() - lastInput > 8000);\n ctl.update(); cam.updateMatrixWorld();\n const now = performance.now();\n if (camMoved()){ stillSince = now; lastAct = now; if (ptOn) stopPT(); }\n if (tracer && renderOn && !ptOn && now - stillSince > 350 && (ceilMat.opacity >= 0.999 || ceilMat.opacity <= 0.001)){\n try { startPT(); } catch(e){ console.warn('Pathtracer-Fehler: ' + (e && e.message), e); tracer = null; ptOn = false; scene.environment = rasterEnv; scene.background = cubeRT.texture; ptLabel.classList.remove('show'); }\n }\n if (ptOn && tracer.samples >= MAX_SAMPLES){ ptLabel.textContent = 'Raytracing · fertig'; return; }\n if (ptOn){ ptFrame ^= 1; if (ptFrame) tracer.renderSample(); ptLabel.textContent = 'Raytracing · ' + Math.floor(tracer.samples) + ' Samples'; }\n else if (now - lastAct < 1500) {\n if (composer){ renderer.getSize(_cs); if (composer._w !== _cs.x || composer._h !== _cs.y){ composer.setSize(_cs.x, _cs.y); composer._w = _cs.x; composer._h = _cs.y; } composer.render(); }\n else renderer.render(scene, cam);\n }\n};\nstage._loop = loop;\nrenderer.setAnimationLoop(loop);\n\n// ---------- Raum-Navigation ----------\nconst cam = stage._camera, ctl = stage._controls;\nlet lampFocus = false;\nconst overview = { pos: cam.position.clone(), tgt: ctl.target.clone() };\nconst EYE = 1.6;\n// Standpunkt in der Raummitte, Blick in Richtung (dx,dz); Target minimal vor der Kamera → Umsehen statt Umkreisen\nconst spot = (x, z, dx, dz, y = EYE) => {\n const d = new THREE.Vector3(dx, -0.12, dz).normalize().multiplyScalar(0.05);\n const pos = new THREE.Vector3(x, y, z);\n return { pos, tgt: pos.clone().add(d), fp: true };\n};\n// Vogelperspektive: senkrecht über dem Raum, leicht geneigt, Decke ausgeblendet – ganzer Raum im Bild\nconst bird = (x0,z0,x1,z1) => {\n const cx = (x0+x1)/2, cz = (z0+z1)/2, w = Math.abs(x1-x0), d = Math.abs(z1-z0);\n const fov = (cam.fov || 50) * Math.PI/180, asp = Math.max(0.6, innerWidth/innerHeight);\n const need = Math.max(d, w/asp) * 0.5 * 1.25; // halbe Ausdehnung + Rand\n const hgt = need / Math.tan(fov/2) + 1.0;\n return { pos: new THREE.Vector3(cx, hgt, cz + hgt*0.22), tgt: new THREE.Vector3(cx, 0.3, cz), fp: false };\n};\nconst room = (x0,y0,x1,y1, dx, dz) => ({\n fp: spot((mx(x0)+mx(x1))/2, (mz(y0)+mz(y1))/2, dx, dz),\n bird: bird(mx(x0), mz(y0), mx(x1), mz(y1)),\n});\nconst ROOMS = [\n ['Übersicht', overview],\n ['Wohnzimmer', room(MID, FLUR, R, Bt, 1, 0.4)],\n ['Küche', room(MID, T, R, FLUR, 1, -0.3)],\n ['Schlafzimmer', room(L, FLUR, MID, Bt, -0.3, 1)],\n ['Bad', room(L, T, MID, BAD, 0, -1)],\n ['Flur', room(L, BAD, MID, FLUR, -1, 0)],\n ['Balkon', { fp: spot((mx(BL)+mx(BR))/2, (mz(BT)+mz(T))/2, 0, -1), bird: bird(mx(BL), mz(BT), mx(BR), mz(T)) }],\n];\nlet anim = 0;\nfunction flyTo(view){\n const id = ++anim;\n const p0 = cam.position.clone(), t0 = ctl.target.clone();\n const c0 = ceilMat.opacity, c1 = view.fp ? 1 : 0;\n inRoom = !!view.fp;\n const clamp01 = x => Math.max(0, Math.min(1, x));\n const dist = p0.distanceTo(view.pos);\n const dur = Math.min(2200, 1100 + dist*90);\n const lift = Math.min(3, dist*0.25);\n const start = performance.now();\n ctl.enabled = false;\n ctl.enableZoom = ctl.enablePan = !view.fp;\n ctl.rotateSpeed = view.fp ? -0.35 : 1;\n ctl.minDistance = 0;\n const ease = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;\n (function step(now){\n if (id !== anim) return;\n const t = Math.min(1, (now - start)/dur), e = ease(t);\n cam.position.lerpVectors(p0, view.pos, e);\n cam.position.y += Math.sin(Math.PI*e) * lift;\n ctl.target.lerpVectors(t0, view.tgt, e);\n cam.lookAt(ctl.target);\n // Einblenden erst am Ende der Anflugs (Kamera schon unter Deckenhöhe), Ausblenden sofort\n const k = c1 > c0 ? clamp01((e - 0.6) / 0.4) : clamp01(e / 0.3);\n setCeil(c0 + (c1 - c0) * k);\n if (t < 1) requestAnimationFrame(step); else ctl.enabled = true;\n })(start);\n}\nconst bar = document.getElementById('rooms');\nconst povBar = document.getElementById('pov');\nlet curView = overview, povMode = 'bird';\nconst show = () => flyTo(curView.bird ? curView[povMode] : curView);\nROOMS.forEach(([label, view], i) => {\n const b = document.createElement('button');\n b.textContent = label;\n if (i === 0) b.classList.add('on');\n b.onclick = () => {\n bar.querySelectorAll('button').forEach(x => x.classList.toggle('on', x === b));\n lampBtn.classList.toggle('show', label === 'Wohnzimmer');\n tripBox.classList.toggle('show', label === 'Schlafzimmer');\n povBar.classList.toggle('show', !!view.bird);\n curView = view; povMode = 'bird';\n povBar.querySelectorAll('button').forEach(x => x.classList.toggle('on', x.dataset.m === povMode));\n show();\n };\n bar.appendChild(b);\n});\n// Lampe konfigurieren → Kamera fliegt an die Lampe, Drehen stoppt; beim Schließen zurück\nconst LAMP_FOCUS = { [LAMP_KEY + '-cfg']: [bp.clone(), 1], [TRIP_KEY + '-cfg']: [trip.spot.getWorldPosition(new THREE.Vector3()), 3] };\naddEventListener('lpick', e => {\n const f = LAMP_FOCUS[e.detail.key]; if (!f) return;\n if (e.detail.open){\n lampFocus = true;\n const p = f[0], c = ROOMS[f[1]][1].fp.pos, dir = new THREE.Vector3(c.x - p.x, 0, c.z - p.z);\n const dist = Math.min(1.8, dir.length() * 0.85); dir.normalize();\n const pos = p.clone().addScaledVector(dir, dist).add(new THREE.Vector3(0, 0.35, 0));\n const right = new THREE.Vector3(-dir.z, 0, dir.x).multiplyScalar(dist * 0.3); // Lampe ins rechte Bilddrittel, Picker links\n flyTo({ pos, tgt: p.clone().add(new THREE.Vector3(0, -0.2, 0)).add(right), fp: false });\n } else { lampFocus = false; show(); }\n});\npovBar.addEventListener('click', e => {\n const b = e.target.closest('button'); if (!b || b.dataset.m === povMode) return;\n povMode = b.dataset.m;\n povBar.querySelectorAll('button').forEach(x => x.classList.toggle('on', x === b));\n show();\n});\n\n// ---------- Poly Haven Texturen ----------\nasync function polyHaven(assetName){\n const all = await (await fetch('https://api.polyhaven.com/assets?t=textures')).json();\n const want = assetName.toLowerCase();\n const id = Object.keys(all).find(k => (all[k].name||'').toLowerCase() === want)\n || Object.keys(all).find(k => (all[k].name||'').toLowerCase().includes(want))\n || want.replace(/\\s+/g,'_');\n const files = await (await fetch(`https://api.polyhaven.com/files/${id}`)).json();\n const pick = key => files[key]?.['4k']?.jpg?.url || files[key]?.['4k']?.png?.url || files[key]?.['2k']?.jpg?.url || files[key]?.['1k']?.jpg?.url;\n const dim = all[id]?.dimensions; // mm [w,h]\n return { id, dim, diff: pick('Diffuse'), nor: pick('nor_gl'), rough: pick('Rough') };\n}\nconst loader = new THREE.TextureLoader();\nloader.setCrossOrigin('anonymous');\nconst loadTex = (url, srgb) => new Promise((res, rej) => loader.load(url, t => {\n t.wrapS = t.wrapT = THREE.RepeatWrapping;\n if (srgb) t.colorSpace = THREE.SRGBColorSpace;\n t.anisotropy = 8; res(t);\n}, undefined, rej));\nasync function apply(mat, assetName, uvScale){\n const f = await polyHaven(assetName);\n const [map, normalMap, roughnessMap] = await Promise.all([\n loadTex(f.diff, true), f.nor ? loadTex(f.nor) : null, f.rough ? loadTex(f.rough) : null\n ]);\n if (uvScale){ // echte Größe: UVs sind in 1/uvScale m, Textur deckt dim mm ab\n const w = (f.dim?.[0] || 2000) / 1000, hh = (f.dim?.[1] || f.dim?.[0] || 2000) / 1000;\n for (const t of [map, normalMap, roughnessMap]) if (t) t.repeat.set(uvScale / w, uvScale / hh);\n }\n mat.map = map; mat.color.set(0xffffff);\n if (normalMap) mat.normalMap = normalMap;\n if (roughnessMap) { mat.roughnessMap = roughnessMap; mat.roughness = 1; }\n mat.needsUpdate = true;\n ptDirty = true; if (ptOn) stopPT();\n return f.id;\n}\n// ---------- Tageszeit: prozeduraler Himmel (three.js Sky, Preetham-Modell) ----------\nconst { Sky } = await import('three/addons/objects/Sky.js');\nconst sky = new Sky(); sky.scale.setScalar(1000);\nconst skyScene = new THREE.Scene(); skyScene.add(sky);\nconst SU = sky.material.uniforms;\nSU.mieCoefficient.value = 0.005; SU.mieDirectionalG.value = 0.8;\n// Nacht-/Dämmerungs-Minimum: dunkles Blau, zum Horizont heller\nsky.material.uniforms.nightFloor = { value: new THREE.Color(0,0,0) };\nsky.material.fragmentShader = 'uniform vec3 nightFloor;\\n' + sky.material.fragmentShader.replace('#include <tonemapping_fragment>',\n 'float hzF = 1.0 - clamp(normalize(vWorldPosition - cameraPosition).y, 0.0, 1.0);\\n gl_FragColor.rgb = max(gl_FragColor.rgb, nightFloor * (0.55 + 0.9*hzF*hzF));\\n#include <tonemapping_fragment>');\nsky.material.needsUpdate = true;\nconst cubeRT = new THREE.WebGLCubeRenderTarget(512, { type: THREE.HalfFloatType });\nconst cubeCam = new THREE.CubeCamera(1, 5000, cubeRT); skyScene.add(cubeCam);\nconst pmrem = new THREE.PMREMGenerator(renderer);\nlet envRT = null;\nconst keyBase = key.intensity, hemiCol0 = hemi ? hemi.color.clone() : null, hemiGnd0 = hemi ? hemi.groundColor.clone() : null;\nconst SS = THREE.MathUtils.smoothstep, col = (r,g,b) => new THREE.Color(r,g,b);\nfunction makePTEnv(d, day, golden){\n const w = 256, h = 128, arr = new Float32Array(w*h*4);\n const zen = col(.03,.045,.1).lerp(col(.16,.32,.75), day);\n const hor = col(.05,.07,.13).lerp(col(.75,.82,.92).lerp(col(1,.55,.3), golden*.8), day);\n const gnd = hor.clone().multiplyScalar(.25);\n const sunC = col(1,.95,.85).lerp(col(1,.5,.2), golden).multiplyScalar(40*day);\n for (let j = 0; j < h; j++){\n const el = ((j+.5)/h - .5)*Math.PI, sy = Math.sin(el), cy = Math.cos(el);\n for (let i = 0; i < w; i++){\n const az = ((i+.5)/w - .5)*2*Math.PI, x = Math.cos(az)*cy, z = Math.sin(az)*cy;\n let r, g, bb;\n if (sy >= 0){ const t = Math.sqrt(sy); r = hor.r+(zen.r-hor.r)*t; g = hor.g+(zen.g-hor.g)*t; bb = hor.b+(zen.b-hor.b)*t; }\n else { const t = Math.min(1, -sy*4); r = hor.r+(gnd.r-hor.r)*t; g = hor.g+(gnd.g-hor.g)*t; bb = hor.b+(gnd.b-hor.b)*t; }\n const dot = x*d.x + sy*d.y + z*d.z;\n if (dot > 0){ const gl = Math.pow(dot, 48)*day*(1+golden); r += gl; g += gl*(.85-.3*golden); bb += gl*(.7-.45*golden); }\n if (dot > .999 && sy > -0.02){ r = sunC.r; g = sunC.g; bb = sunC.b; }\n const k = (j*w + i)*4; arr[k] = r; arr[k+1] = g; arr[k+2] = bb; arr[k+3] = 1;\n }\n }\n const dt = new THREE.DataTexture(arr, w, h, THREE.RGBAFormat, THREE.FloatType);\n dt.mapping = THREE.EquirectangularReflectionMapping;\n dt.minFilter = dt.magFilter = THREE.LinearFilter; dt.generateMipmaps = false; dt.needsUpdate = true;\n return dt;\n}\nconst sunV = new THREE.Vector3();\nfunction setTime(hr){\n // Sonnenbahn (vereinfacht): 7 Uhr Ost (+x) → 13 Uhr Süd (+z) → 19 Uhr West, max. 50° Höhe\n const phi = Math.PI*(hr - 7)/12, e = THREE.MathUtils.degToRad(50)*Math.sin(phi);\n sunV.set(Math.cos(phi)*Math.cos(e), Math.sin(e), Math.sin(phi)*Math.cos(e)).normalize();\n const s = sunV.y, day = SS(s, -0.14, 0.18), golden = 1 - SS(s, 0, 0.4), dusk = SS(s, -0.22, -0.02) * (1 - SS(s, 0.02, 0.2));\n SU.sunPosition.value.copy(sunV);\n SU.rayleigh.value = 2.5 + 1.2*golden;\n SU.turbidity.value = 3 + 5*golden;\n // Nachtblau, zur blauen Stunde etwas heller/violetter\n SU.nightFloor.value.setRGB(0.010 + 0.03*dusk, 0.016 + 0.03*dusk, 0.045 + 0.06*dusk);\n cubeCam.update(renderer, skyScene);\n envRT = pmrem.fromCubemap(cubeRT.texture, envRT);\n rasterEnv = envRT.texture;\n if (s > -0.03){\n key.position.copy(hc).addScaledVector(sunV, 30);\n key.color.set(0xff9a52).lerp(new THREE.Color(0xfff3e2), SS(s, 0, 0.5));\n key.intensity = keyBase * 1.3 * Math.max(day, 0.15);\n } else { // Mondlicht, gegenüber der Sonne\n const m = new THREE.Vector3(-sunV.x, Math.abs(sunV.y)*0.8 + 0.2, -sunV.z).normalize();\n key.position.copy(hc).addScaledVector(m, 30);\n key.color.set(0x9fb4ff); key.intensity = keyBase * 0.25;\n }\n envK = 0.2 + 0.1*day + 0.15*dusk; hemiK = 0.3 + 0.4*day; fillK = 0.1 + 0.5*day; expK = 1 - 0.45*day;\n if (hemi){ hemi.color.copy(hemiCol0).lerp(new THREE.Color(0x34426e), 1 - day); hemi.groundColor.copy(hemiGnd0).multiplyScalar(0.3 + 0.7*day); }\n ptEnv = makePTEnv(sunV, day, golden);\n scene.background = cubeRT.texture;\n scene.backgroundIntensity = 0.2 + 1.1*(1 - day);\n if (ptOn) stopPT(); else { scene.environment = rasterEnv; applyInterior(ceilMat.opacity); }\n act();\n}\n// Himmel & Licht folgen der echten Uhrzeit\nconst nowH = () => { const n = new Date(); return n.getHours() + n.getMinutes()/60; };\nsetTime(nowH());\nsetInterval(() => setTime(nowH()), 60000);\nlet texMsg = 'Texturen werden geladen…';\nconst upd = () => statusEl.textContent = texMsg;\nupd();\nsofa.M.fabric.color.set('#3a3a3c');\napply(sofa.M.fabric, 'Poly Wool Herringbone').then(() => sofa.M.fabric.color.set('#3a3a3c')).catch(e => console.warn(e));\nPromise.all([apply(M.floor, 'Rubber Tiles'), apply(M.wall, 'White Stucco'), apply(M.brick, 'Brick Wall 04', 1.5), apply(M.cap, 'Rough Plaster Brick 04', 1.5)])\n .then(ids => { texMsg = 'Texturen: ' + ids.join(', '); upd(); })\n .catch(e => { console.warn(e); texMsg = 'Texturen nicht erreichbar – Farb-Fallback'; upd(); });\n<\/script>\n<\/body>\n<\/html>\n";
var HANDY="<!DOCTYPE html>\n<html lang=\"de\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover\">\n<title>Wohnung – 3D Miniatur (Handy)<\/title>\n<script type=\"importmap\">\n{\n \"imports\": {\n \"three\": \"https://unpkg.com/three@0.184.0/build/three.module.js\",\n \"three/addons/controls/OrbitControls.js\": \"https://unpkg.com/three@0.184.0/examples/jsm/controls/OrbitControls.js\",\n \"three/addons/exporters/OBJExporter.js\": \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/OBJExporter.js\",\n \"three/addons/exporters/GLTFExporter.js\": \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/GLTFExporter.js\",\n \"three/examples/jsm/\": \"https://unpkg.com/three@0.184.0/examples/jsm/\",\n \"three/addons/\": \"https://unpkg.com/three@0.184.0/examples/jsm/\",\n \"three-mesh-bvh\": \"https://cdn.jsdelivr.net/npm/three-mesh-bvh@0.9.5/build/index.module.js\",\n \"three-gpu-pathtracer\": \"https://cdn.jsdelivr.net/npm/three-gpu-pathtracer@0.0.24/build/index.module.js\",\n \"n8ao\": \"https://cdn.jsdelivr.net/npm/n8ao@1/dist/N8AO.js\",\n \"postprocessing\": \"https://cdn.jsdelivr.net/npm/postprocessing@6/build/index.js\"\n },\n \"integrity\": {\n \"https://unpkg.com/three@0.184.0/build/three.module.js\": \"sha384-8FCZ1eVO6it4+pbec2aDtnTrwjWXZLJRC+MAGCIPDgsYnUrl/E0A2YlF8ioMKI/J\",\n \"https://unpkg.com/three@0.184.0/build/three.core.js\": \"sha384-dw2ooPewaEIrAgl6oFDBmmBWCE9oW9LxRGcfwZ0hLvEprzo202wXl7vCYHRlSnOT\",\n \"https://unpkg.com/three@0.184.0/examples/jsm/controls/OrbitControls.js\": \"sha384-4rziNxOBZKQ69i+w+f89KJ55TCYquwchVbByQwmaOeIOXdOU2PLDn3kOfXHwIJC9\",\n \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/OBJExporter.js\": \"sha384-nbwtoZENJD3Vq+ACK0CuGQdPMuDWHkamC2KJD70EV5nfg6jQjfppKOea07YJN+N3\",\n \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/GLTFExporter.js\": \"sha384-VofkvpG6HERhFCYbsUOHeNXBCqID2nfqkQqnVzE1jc/oPcz+qJ13ADdXH08hE+cQ\"\n }\n}\n<\/script>\n<style>\n html,body{margin:0;height:100%;background:#efece6;font-family:Helvetica,Arial,sans-serif}\n three-d-stage:not(:defined){visibility:hidden}\n #rooms{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);display:flex;gap:6px;padding:6px;background:rgba(250,248,244,.82);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-radius:999px;box-shadow:0 6px 24px rgba(30,28,24,.14)}\n #rooms button{appearance:none;border:0;background:transparent;color:#2f2d2a;font:500 14px/1 Helvetica,Arial,sans-serif;padding:11px 18px;border-radius:999px;cursor:pointer;transition:background .2s,color .2s}\n #rooms button:hover{background:rgba(47,45,42,.08)}\n #rooms button.on{background:#2f2d2a;color:#f7f5f0}\n #pov{position:fixed;left:50%;top:18px;transform:translate(-50%,-8px);opacity:0;pointer-events:none;transition:opacity .3s,transform .3s;display:flex;gap:4px;padding:4px;background:rgba(250,248,244,.82);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-radius:999px;box-shadow:0 1px 2px rgba(0,0,0,.06),0 6px 20px rgba(0,0,0,.08)}\n #pov.show{opacity:1;pointer-events:auto;transform:translate(-50%,0)}\n #pov button{appearance:none;border:0;background:transparent;color:#2f2d2a;font:500 13px/1 Helvetica,Arial,sans-serif;padding:9px 14px;border-radius:999px;cursor:pointer;transition:background .2s,color .2s}\n #pov button:hover{background:rgba(47,45,42,.08)}\n #pov button.on{background:#2f2d2a;color:#f7f5f0}\n #lamp{position:fixed;left:50%;bottom:82px;transform:translate(-50%,10px);opacity:0;pointer-events:none;transition:opacity .35s,transform .35s;display:flex;align-items:center;gap:4px;padding:4px;border-radius:999px;background:rgba(250,248,244,.82);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:#2f2d2a;font:500 14px/1 Helvetica,Arial,sans-serif;box-shadow:0 4px 18px rgba(0,0,0,.08)}\n #lampT{display:flex;align-items:center;gap:10px;appearance:none;border:0;background:transparent;color:inherit;font:inherit;padding:7px 14px 7px 10px;border-radius:999px;cursor:pointer;white-space:nowrap}\n #lampT:hover{background:rgba(47,45,42,.08)}\n #lampCtl{display:flex;align-items:center;gap:14px;max-width:0;overflow:hidden;opacity:0;transition:max-width .4s,opacity .3s,padding .4s;padding:0}\n #lamp.on #lampCtl{max-width:420px;opacity:1;padding:0 12px 0 6px}\n #lampSw{display:flex;gap:6px}\n #lampSw button{appearance:none;width:22px;height:22px;border-radius:50%;border:2px solid rgba(250,248,244,.9);box-shadow:0 0 0 1px rgba(47,45,42,.18);cursor:pointer;padding:0}\n #lampSw button.sel{box-shadow:0 0 0 2px #2f2d2a}\n #lampSw{align-items:center;gap:8px;font-size:12px;color:#6b665e}\n #lampSw input[type=color]{appearance:none;-webkit-appearance:none;width:26px;height:26px;border-radius:50%;border:2px solid rgba(250,248,244,.9);box-shadow:0 0 0 1px rgba(47,45,42,.25);padding:0;cursor:pointer;background:none}\n #lampSw input[type=color]::-webkit-color-swatch-wrapper{padding:0}\n #lampSw input[type=color]::-webkit-color-swatch{border:0;border-radius:50%}\n #lampSw input[type=color]::-moz-color-swatch{border:0;border-radius:50%}\n #lampDim{display:flex;align-items:center;gap:8px;font-size:12px;color:#6b665e}\n #lampDim input{width:110px;accent-color:#2f2d2a}\n #lampDim output{width:34px;text-align:right;font-variant-numeric:tabular-nums}\n #lamp.show{opacity:1;transform:translate(-50%,0);pointer-events:auto}\n #lamp i{width:12px;height:12px;border-radius:50%;background:#bdb8ae;transition:background .3s,box-shadow .3s}\n #pt{position:fixed;top:18px;right:18px;padding:8px 12px;border-radius:999px;background:rgba(250,248,244,.82);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:#2f2d2a;font:500 12px/1 ui-monospace,Menlo,monospace;opacity:0;transition:opacity .3s;pointer-events:none}\n #pt.show{opacity:1}\n #pt{top:62px !important}\n #rt{position:fixed;top:18px;right:18px;display:flex;align-items:center;gap:8px;appearance:none;border:0;padding:9px 14px 9px 11px;border-radius:999px;background:rgba(250,248,244,.82);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:#2f2d2a;font:500 13px/1 Helvetica,Arial,sans-serif;cursor:pointer;box-shadow:0 1px 2px rgba(0,0,0,.06),0 6px 20px rgba(0,0,0,.08);transition:background .2s,color .2s}\n #rt i{width:10px;height:10px;border-radius:50%;background:#bdb8ae;transition:background .2s}\n #rt.on{background:#2f2d2a;color:#f7f5f0}\n #rt.on i{background:#7fd08a}\n #lamp.on i{background:#ffc86b;box-shadow:0 0 10px 3px rgba(255,190,90,.6)}\n #lampD{display:flex;align-items:center;gap:10px;appearance:none;border:0;background:transparent;color:inherit;font:inherit;padding:7px 14px 7px 10px;border-radius:999px;cursor:pointer;white-space:nowrap}\n #lampD:hover{background:rgba(47,45,42,.08)}\n #lampD b{width:12px;height:12px;border-radius:50%;background:#bdb8ae;transition:background .3s,box-shadow .3s}\n #lampD.on b{background:#ff9a3c;box-shadow:0 0 10px 3px rgba(255,150,60,.6)}\n #lamp2{position:fixed;left:50%;bottom:82px;transform:translate(-50%,10px);opacity:0;pointer-events:none;transition:opacity .35s,transform .35s;display:flex;align-items:center;gap:4px;padding:4px;border-radius:999px;background:rgba(250,248,244,.82);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:#2f2d2a;font:500 14px/1 Helvetica,Arial,sans-serif;box-shadow:0 4px 18px rgba(0,0,0,.08)}\n #lamp2.show{opacity:1;transform:translate(-50%,0);pointer-events:auto}\n #tripT{display:flex;align-items:center;gap:10px;appearance:none;border:0;background:transparent;color:inherit;font:inherit;padding:7px 14px 7px 10px;border-radius:999px;cursor:pointer;white-space:nowrap}\n #tripT:hover{background:rgba(47,45,42,.08)}\n #tripT i{width:12px;height:12px;border-radius:50%;background:#bdb8ae;transition:background .3s,box-shadow .3s}\n #tripCtl{display:flex;align-items:center;gap:14px;max-width:0;overflow:hidden;opacity:0;transition:max-width .4s,opacity .3s,padding .4s;padding:0}\n #lamp2.on #tripCtl{max-width:560px;opacity:1;padding:0 12px 0 6px}\n #tripCtl label{display:flex;align-items:center;gap:8px;font-size:12px;color:#6b665e}\n #tripCtl input[type=range]{width:100px;accent-color:#2f2d2a}\n #tripCtl output{width:34px;text-align:right;font-variant-numeric:tabular-nums}\n #tripCtl input[type=color]{appearance:none;-webkit-appearance:none;width:26px;height:26px;border-radius:50%;border:2px solid rgba(250,248,244,.9);box-shadow:0 0 0 1px rgba(47,45,42,.25);padding:0;cursor:pointer;background:none}\n #tripCtl input[type=color]::-webkit-color-swatch-wrapper{padding:0}\n #tripCtl input[type=color]::-webkit-color-swatch{border:0;border-radius:50%}\n #tripCtl input[type=color]::-moz-color-swatch{border:0;border-radius:50%}\n #rooms,#pov,#rt,#pt,#lamp,#lamp2,three-d-stage::part(toolbar){transition:opacity .6s}\n body.idle #rooms,body.idle #pov,body.idle #rt,body.idle #pt,body.idle #lamp,body.idle #lamp2{opacity:0 !important;pointer-events:none}\n body.idle{cursor:none}\n\n /* muavin-Gradient für GUI-Leisten */\n #rooms,#pov,#lamp,#lamp2,#pt,#rt{background-color:#000 !important;background-image:radial-gradient(circle at 65.34% 44.62%, rgba(95,95,95,1) 0%, rgba(95,95,95,.844) 8.53%, rgba(95,95,95,.5) 17.05%, rgba(95,95,95,.156) 25.58%, rgba(95,95,95,0) 34.1%),radial-gradient(circle at 28.07% 74.48%, rgba(53,54,48,1) 0%, rgba(53,54,48,.844) 11.41%, rgba(53,54,48,.5) 22.83%, rgba(53,54,48,.156) 34.24%, rgba(53,54,48,0) 45.65%),radial-gradient(circle at 52.42% 19.94%, rgba(56,122,56,1) 0%, rgba(56,122,56,.844) 14.39%, rgba(56,122,56,.5) 28.77%, rgba(56,122,56,.156) 43.16%, rgba(56,122,56,0) 57.55%),radial-gradient(circle at 80.31% 84.47%, rgba(0,0,0,1) 0%, rgba(0,0,0,.844) 17.27%, rgba(0,0,0,.5) 34.55%, rgba(0,0,0,.156) 51.82%, rgba(0,0,0,0) 69.1%) !important;background-size:100% 100% !important;color:#f2f1ec !important;box-shadow:0 6px 24px rgba(0,0,0,.25),inset 0 0 0 1px rgba(255,255,255,.08)}\n #rooms button,#pov button{color:#f2f1ec}\n #rooms button:hover,#pov button:hover,#lampT:hover,#lampD:hover,#tripT:hover{background:rgba(255,255,255,.1)}\n #rooms button.on,#pov button.on,#rt.on{background:#f2f1ec !important;background-image:none !important;color:#141412 !important}\n #lampSw,#lampDim,#tripCtl{color:#d9d7d0 !important}\n #lampSw button.sel{box-shadow:0 0 0 2px #f2f1ec}\n #lampDim input[type=range],#tripCtl input[type=range]{accent-color:#f2f1ec}\n .lp-sw{appearance:none;width:26px;height:26px;border-radius:50%;border:2px solid rgba(250,248,244,.9);box-shadow:0 0 0 1px rgba(47,45,42,.25);padding:0;cursor:pointer;flex:none}\n .lpick{position:fixed;z-index:50;width:236px;padding:14px;border-radius:18px;background:rgba(26,27,24,.38);backdrop-filter:blur(18px) saturate(1.2);-webkit-backdrop-filter:blur(18px) saturate(1.2);color:#f2f1ec;font:500 12px/1.2 Helvetica,Arial,sans-serif;box-shadow:0 10px 36px rgba(0,0,0,.35),inset 0 0 0 1px rgba(255,255,255,.08);display:none;flex-direction:column;gap:14px;transition:opacity .6s}\n .lpick.open{display:flex}\n body.lpfocus #rooms,body.lpfocus #pov,body.lpfocus #rt,body.lpfocus #pt,body.lpfocus #lamp,body.lpfocus #lamp2,body.lpfocus three-d-stage::part(toolbar){opacity:0 !important;pointer-events:none !important}\n .lp-done{appearance:none;border:0;border-radius:999px;padding:9px 0;background:rgba(255,255,255,.1);color:#f2f1ec;font:inherit;cursor:pointer}\n .lp-done:hover{background:rgba(255,255,255,.18)}\n body.idle .lpick{opacity:0;pointer-events:none}\n .lp-seg{display:grid;grid-template-columns:1fr 1fr;gap:3px;padding:3px;border-radius:999px;background:rgba(255,255,255,.08)}\n .lp-seg button{appearance:none;border:0;border-radius:999px;padding:7px 0;background:transparent;color:#d9d7d0;font:inherit;cursor:pointer}\n .lp-seg button.on{background:#f2f1ec;color:#141412}\n .lp-row{display:flex;flex-direction:column;gap:7px}\n .lp-row>div{display:flex;justify-content:space-between;color:#d9d7d0}\n .lp-row output{font-variant-numeric:tabular-nums;color:#f2f1ec}\n .lp-row small{display:flex;justify-content:space-between;color:#a9a69e;font-size:11px}\n .lpick input[type=range]{width:100%;margin:0;accent-color:#f2f1ec}\n .lpick input.lp-temp{appearance:none;-webkit-appearance:none;height:10px;border-radius:5px;outline:none}\n .lp-temp::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;border-radius:50%;background:#fff;border:2px solid #1a1b18;box-shadow:0 0 0 1px rgba(255,255,255,.6);cursor:pointer}\n .lp-temp::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:#fff;border:2px solid #1a1b18;cursor:pointer}\n .lp-wheel{position:relative;width:180px;height:180px;margin:0 auto;touch-action:none;cursor:pointer}\n .lp-wheel canvas{width:180px;height:180px;border-radius:50%;display:block}\n .lp-wheel i{position:absolute;width:18px;height:18px;margin:-9px 0 0 -9px;border-radius:50%;border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.4),0 2px 6px rgba(0,0,0,.4);pointer-events:none}\n /* ---------- Handy-Ansicht ---------- */\n html,body{overscroll-behavior:none;-webkit-tap-highlight-color:transparent;touch-action:none}\n @media (max-width:700px){\n #rooms{left:12px;right:12px;transform:none;bottom:calc(14px + max(env(safe-area-inset-bottom), 18px));overflow-x:auto;scrollbar-width:none;justify-content:flex-start;gap:2px;padding:5px;touch-action:pan-x}\n #rooms::-webkit-scrollbar{display:none}\n #rooms button{flex:none;padding:13px 15px;font-size:14px}\n #pov{left:12px;top:calc(12px + max(env(safe-area-inset-top), 46px));transform:translateY(-8px)}\n #pov.show{transform:none}\n #pov button{padding:11px 12px;font-size:12px}\n #rt{top:calc(12px + max(env(safe-area-inset-top), 46px));right:12px;padding:11px 12px 11px 10px;font-size:12px}\n #pt{top:calc(62px + max(env(safe-area-inset-top), 46px)) !important;right:12px}\n #lamp,#lamp2{left:12px;right:12px;bottom:calc(78px + max(env(safe-area-inset-bottom), 18px));transform:translateY(10px);flex-wrap:wrap;justify-content:center;border-radius:24px;row-gap:2px}\n #lamp.show,#lamp2.show{transform:none}\n #lampT,#lampD,#tripT{padding:11px 14px 11px 10px;font-size:13px}\n #lamp.on #lampCtl,#lamp2.on #tripCtl{max-width:100%;padding:4px 10px 8px}\n #tripCtl{flex-wrap:wrap;justify-content:center;gap:12px}\n #tripCtl input[type=range]{width:120px}\n .lp-sw{width:30px;height:30px}\n .lpick{left:0 !important;right:0;top:auto !important;bottom:0;transform:none !important;width:auto;border-radius:24px 24px 0 0;padding:16px 20px calc(20px + max(env(safe-area-inset-bottom), 18px));gap:16px;font-size:13px}\n .lp-seg button,.lp-done{padding:12px 0}\n .lp-wheel,.lp-wheel canvas{width:180px;height:180px}\n }\n /* ---------- Handy: 3D oben, Fernbedienung unten ---------- */\n #rooms,#pov,#rt,#pt,#lamp,#lamp2{display:none !important}\n body{display:flex;flex-direction:column;height:100vh;height:100dvh;overflow:hidden;cursor:auto !important}\n three-d-stage{flex:none;height:36%;min-height:0}\n #remote{position:relative;z-index:5;flex:1;min-height:0;margin-top:-22px;overflow-y:auto;touch-action:pan-y;display:flex;flex-direction:column;gap:12px;padding:10px 16px max(env(safe-area-inset-bottom), 22px);border-radius:28px 28px 0 0;background:linear-gradient(180deg,#262624,#171716 60%);color:#f2f1ec;box-shadow:0 -10px 30px rgba(0,0,0,.18),inset 0 1px 0 rgba(255,255,255,.08);font:500 14px/1.2 Helvetica,Arial,sans-serif}\n #remote button{appearance:none;border:0;font:inherit;color:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent}\n .r-grip{width:40px;height:4px;border-radius:2px;background:rgba(255,255,255,.18);margin:0 auto 2px;flex:none}\n .r-head{display:flex;align-items:center;justify-content:space-between;gap:12px}\n .r-stat{display:flex;flex-direction:column;gap:4px}\n .r-stat small{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:#9d9a92}\n .r-stat b{font-size:17px;font-weight:600}\n #rPower{display:flex;align-items:center;gap:9px;padding:0 16px 0 12px;height:48px;border-radius:24px;background:#b8402f !important;color:#fff !important;box-shadow:0 4px 14px rgba(184,64,47,.35),inset 0 1px 0 rgba(255,255,255,.2);font-size:13px !important;font-weight:600 !important;flex:none}\n #rPower:active{transform:scale(.96)}\n .r-label{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:#9d9a92;margin:4px 2px -4px}\n #rRooms{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}\n three-d-stage{transition:height .5s cubic-bezier(.3,.8,.2,1)}\n #remote{flex:none !important;height:calc(64% + 22px)}\n body.lpfocus three-d-stage{height:calc(100% + 22px)}\n .lpick.open{animation:lpUp .5s cubic-bezier(.3,.8,.2,1) .1s both}\n @keyframes lpUp{from{translate:0 110%}to{translate:0 0}}\n .r-key{height:48px;border-radius:14px;background:#2f2f2c !important;box-shadow:inset 0 1px 0 rgba(255,255,255,.07),0 2px 0 #0e0e0d;font-size:12px !important;padding:0 4px;transition:background .15s,color .15s,transform .1s}\n .r-key.wide{grid-column:span 2}\n .r-key:active{transform:translateY(1px)}\n .r-key.on{background:#f2f1ec !important;color:#141412 !important}\n #rPov{display:grid;grid-template-columns:1fr 1fr;gap:3px;padding:3px;border-radius:14px;background:#0f0f0e;transition:opacity .2s}\n #rPov button{height:40px;border-radius:11px;font-size:13px !important;color:#bdbab2 !important}\n #rPov button.on{background:#3a3a37;color:#f2f1ec !important}\n #rPov.off{opacity:.35;pointer-events:none}\n #rLights{display:flex;flex-direction:column;gap:7px}\n .r-lamp{display:flex;gap:7px}\n .r-tog{flex:1;display:flex;align-items:center;gap:12px;height:56px;padding:0 14px;border-radius:16px;background:#2f2f2c !important;box-shadow:inset 0 1px 0 rgba(255,255,255,.07),0 2px 0 #0e0e0d;text-align:left}\n .r-tog i{width:12px;height:12px;border-radius:50%;background:#55534e;flex:none;transition:background .3s,box-shadow .3s}\n .r-tog span{flex:1;display:flex;flex-direction:column;gap:3px;font-size:14px}\n .r-tog em{font-style:normal;font-size:11px;color:#9d9a92}\n .r-tog b{width:38px;height:22px;border-radius:11px;background:#4a4945;position:relative;flex:none;transition:background .2s}\n .r-tog b::after{content:\"\";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#f2f1ec;transition:transform .2s}\n .r-lamp.on .r-tog b{background:#e9e6de}\n .r-lamp.on .r-tog b::after{transform:translateX(16px);background:#1b1b19}\n .r-col{width:56px;height:56px;border-radius:16px;background:#2f2f2c !important;box-shadow:inset 0 1px 0 rgba(255,255,255,.07),0 2px 0 #0e0e0d;display:grid;place-items:center;flex:none}\n .r-col::after{content:\"\";width:24px;height:24px;border-radius:50%;background:var(--c,#ffd6a0);box-shadow:0 0 0 2px #1b1b19,0 0 0 3px rgba(255,255,255,.25)}\n .r-two{display:grid;grid-template-columns:1fr 1fr;gap:7px}\n .r-two button{height:48px;border-radius:14px;background:#2f2f2c !important;box-shadow:inset 0 1px 0 rgba(255,255,255,.07),0 2px 0 #0e0e0d;font-size:14px !important}\n .r-two button:active,.r-tog:active,.r-col:active{transform:translateY(1px)}\n<\/style>\n<\/head>\n<body>\n<three-d-stage name=\"wohnung\" background=\"#efece6\"><\/three-d-stage>\n<div id=\"remote\">\n <div class=\"r-grip\"><\/div>\n <div class=\"r-head\">\n <div class=\"r-stat\"><small>Wohnung<\/small><b id=\"rStat\">Alle Lichter aus<\/b><\/div>\n <button id=\"rPower\" aria-label=\"Wohnung verlassen\"><svg viewBox=\"0 0 24 24\" width=\"22\" height=\"22\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\"><path d=\"M12 3v8\"/><path d=\"M6.3 6.8a8 8 0 1 0 11.4 0\"/><\/svg><span>Wohnung verlassen<\/span><\/button>\n <\/div>\n <div class=\"r-label\">Räume<\/div>\n <div id=\"rRooms\"><\/div>\n <div class=\"r-label\" id=\"rLightsLbl\">Licht<\/div>\n <div id=\"rLights\">\n <div class=\"r-lamp\" data-l=\"bogen\" data-room=\"1\"><button class=\"r-tog\"><i><\/i><span>Bogenlampe<em>Wohnzimmer<\/em><\/span><b><\/b><\/button><button class=\"r-col\" aria-label=\"Farbe\"><\/button><\/div>\n <div class=\"r-lamp\" data-l=\"donut\" data-room=\"1\"><button class=\"r-tog\"><i><\/i><span>Donut-Lampe<em>Wohnzimmer · Regal<\/em><\/span><b><\/b><\/button><\/div>\n <div class=\"r-lamp\" data-l=\"trip\" data-room=\"3\"><button class=\"r-tog\"><i><\/i><span>Tripod-Lampe<em>Schlafzimmer<\/em><\/span><b><\/b><\/button><button class=\"r-col\" aria-label=\"Farbe\"><\/button><\/div>\n <\/div>\n <div class=\"r-two\"><button id=\"rAllOn\">Alle an<\/button><button id=\"rAllOff\">Alles aus<\/button><\/div>\n<\/div>\n<div id=\"rooms\"><\/div>\n<div id=\"pov\"><button data-m=\"bird\" class=\"on\">Vogelperspektive<\/button><button data-m=\"fp\">Frei umsehen<\/button><\/div>\n<button id=\"rt\" aria-pressed=\"false\"><i><\/i><span>Render aus<\/span><\/button>\n<div id=\"pt\"><\/div>\n<div id=\"lamp\"><button id=\"lampT\"><i><\/i><span>Bogenlampe an<\/span><\/button><div id=\"lampCtl\"><div id=\"lampSw\"><\/div><label id=\"lampDim\" style=\"display:none\"><span>Stärke<\/span><input type=\"range\" min=\"10\" max=\"200\" step=\"5\" value=\"100\"><output>100%<\/output><\/label><\/div><button id=\"lampD\" aria-pressed=\"false\"><b><\/b><span>Donut-Lampe an<\/span><\/button><\/div>\n<div id=\"lamp2\"><button id=\"tripT\"><i><\/i><span>Tripod-Lampe an<\/span><\/button><div id=\"tripCtl\"><label><button id=\"tripCol\" class=\"lp-sw\" title=\"Licht einstellen\"><\/button><\/label><label style=\"display:none\"><span>Stärke<\/span><input id=\"tripDim\" type=\"range\" min=\"10\" max=\"200\" step=\"5\" value=\"100\"><output>100%<\/output><\/label><label><span>Kegel<\/span><input id=\"tripCone\" type=\"range\" min=\"15\" max=\"80\" step=\"1\" value=\"32\"><output>32°<\/output><\/label><\/div><\/div>\n<script>// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design)\n// Copied omelette starter. Re-running copy_starter_component with this kind overwrites this file with the latest version (page content is unaffected).\n/* BEGIN USAGE */\n/**\n * <three-d-stage> — 3D object viewer + exporter shell (three.js).\n *\n * The stage owns the whole scene: WebGL renderer, neutral studio lighting\n * with a soft ground shadow, orbit controls (drag to orbit, wheel to zoom,\n * right-drag to pan), a camera auto-framed to the object's bounds, resize\n * handling, and a download toolbar that exports the current object as\n * OBJ + MTL or GLB (binary glTF). FBX cannot be exported in the browser;\n * GLB is the interchange format every modern 3D tool imports.\n *\n * three.js loads through the page's import map. Include this EXACT pinned\n * map in <head>, before any module runs — versions and integrity hashes\n * stay together (same map the \"3D object\" skill mandates):\n *\n * <script type=\"importmap\">\n * {\n * \"imports\": {\n * \"three\": \"https://unpkg.com/three@0.184.0/build/three.module.js\",\n * \"three/addons/controls/OrbitControls.js\": \"https://unpkg.com/three@0.184.0/examples/jsm/controls/OrbitControls.js\",\n * \"three/addons/exporters/OBJExporter.js\": \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/OBJExporter.js\",\n * \"three/addons/exporters/GLTFExporter.js\": \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/GLTFExporter.js\"\n * },\n * \"integrity\": {\n * \"https://unpkg.com/three@0.184.0/build/three.module.js\": \"sha384-8FCZ1eVO6it4+pbec2aDtnTrwjWXZLJRC+MAGCIPDgsYnUrl/E0A2YlF8ioMKI/J\",\n * \"https://unpkg.com/three@0.184.0/build/three.core.js\": \"sha384-dw2ooPewaEIrAgl6oFDBmmBWCE9oW9LxRGcfwZ0hLvEprzo202wXl7vCYHRlSnOT\",\n * \"https://unpkg.com/three@0.184.0/examples/jsm/controls/OrbitControls.js\": \"sha384-4rziNxOBZKQ69i+w+f89KJ55TCYquwchVbByQwmaOeIOXdOU2PLDn3kOfXHwIJC9\",\n * \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/OBJExporter.js\": \"sha384-nbwtoZENJD3Vq+ACK0CuGQdPMuDWHkamC2KJD70EV5nfg6jQjfppKOea07YJN+N3\",\n * \"https://unpkg.com/three@0.184.0/examples/jsm/exporters/GLTFExporter.js\": \"sha384-VofkvpG6HERhFCYbsUOHeNXBCqID2nfqkQqnVzE1jc/oPcz+qJ13ADdXH08hE+cQ\"\n * }\n * }\n * <\\/script>\n *\n * Usage:\n * <style>three-d-stage:not(:defined){visibility:hidden}<\/style>\n * <three-d-stage name=\"rocket\"><\/three-d-stage>\n * <script src=\"three-d-stage.js\"><\\/script>\n * <script type=\"module\">\n * const stage = document.querySelector('three-d-stage');\n * const { THREE } = await stage.ready;\n * const model = new THREE.Group();\n * // …build the model out of named meshes with named materials —\n * // the names become the o / usemtl entries in the exported OBJ…\n * stage.setObject(model);\n * <\\/script>\n *\n * Attributes:\n * name — export file basename (default \"model\")\n * background — CSS color behind the scene (default a warm paper tone)\n * autorotate — when present, a slow turntable until the user interacts\n *\n * Model in real-world meters, centered on the origin, y-up — exports\n * inherit the scene's units and orientation. The stage fills its own box;\n * size it with ordinary CSS (default 100vw/100vh page hero).\n *\n * Default setup: neutral studio lighting (hemisphere + key + fill), a\n * soft ground shadow, and NO environment map — so high metalness has\n * nothing to reflect and renders near-black. Cap metalness around\n * 0.3–0.4 and carry a metal look with a brighter base color. The copied\n * file is yours: adjust the lights, shadow, or background in _boot()\n * when the object needs a different look.\n */\n/* END USAGE */\n\n(() => {\n const stylesheet = `\n :host {\n position: relative;\n display: block;\n width: 100%;\n height: 100vh;\n background: var(--stage-bg, #f0eee6);\n overflow: hidden;\n }\n canvas { display: block; outline: none; }\n .toolbar {\n position: absolute;\n right: 16px;\n bottom: 16px;\n display: flex;\n gap: 8px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n }\n .toolbar button {\n appearance: none;\n border: 1px solid rgba(20, 20, 19, 0.18);\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.92);\n color: #1a1915;\n font-family: inherit;\n font-size: 12.5px;\n font-weight: 500;\n line-height: 1;\n padding: 9px 12px;\n cursor: default;\n }\n .toolbar button:hover { background: #fff; }\n .toolbar button:active { transform: translateY(1px); }\n .toolbar button[disabled] { opacity: 0.5; pointer-events: none; }\n .note {\n position: absolute;\n left: 16px;\n bottom: 16px;\n max-width: 60%;\n font: 400 12px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n color: #1a1915;\n background: rgba(255, 255, 255, 0.85);\n padding: 4px 10px;\n border-radius: 6px;\n user-select: none;\n }\n .err {\n position: absolute;\n inset: 0;\n display: none;\n align-items: center;\n justify-content: center;\n padding: 24px;\n font: 500 14px/1.6 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n color: #8a2f20;\n text-align: center;\n white-space: pre-line;\n }\n `;\n\n function download(blob, filename) {\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n a.remove();\n setTimeout(() => URL.revokeObjectURL(url), 4000);\n }\n\n /** Tell the host an export attempt settled — telemetry only. The host\n * (HTMLViewer) verifies the source and re-reads these fields defensively\n * before counting; nothing else crosses the frame boundary. Guarded so\n * telemetry can never break the download path. */\n function notifyExport(format, ok) {\n try {\n window.parent.postMessage(\n { type: 'omelette:notify-3d-export', format: format, ok: ok === true },\n '*'\n );\n } catch (e) {}\n }\n\n class ThreeDStage extends HTMLElement {\n constructor() {\n super();\n const root = this.attachShadow({ mode: 'open' });\n const style = document.createElement('style');\n style.textContent = stylesheet;\n root.appendChild(style);\n this._err = document.createElement('div');\n this._err.className = 'err';\n root.appendChild(this._err);\n const note = document.createElement('div');\n note.className = 'note';\n note.textContent = 'Drag to orbit · scroll to zoom · right-drag to pan';\n root.appendChild(note);\n this._toolbar = document.createElement('div');\n this._toolbar.className = 'toolbar';\n this._objBtn = document.createElement('button');\n this._objBtn.type = 'button';\n this._objBtn.textContent = 'Download OBJ + MTL';\n this._objBtn.addEventListener('click', () => this._runExport('obj'));\n this._glbBtn = document.createElement('button');\n this._glbBtn.type = 'button';\n this._glbBtn.textContent = 'Download GLB';\n this._glbBtn.addEventListener('click', () => this._runExport('glb'));\n this._toolbar.appendChild(this._objBtn);\n this._toolbar.appendChild(this._glbBtn);\n root.appendChild(this._toolbar);\n this._setButtonsEnabled(false);\n /** Resolves with { THREE } once the scene is live — build the model\n * in `await stage.ready` so nothing races the library load. */\n this.ready = new Promise((resolve, reject) => {\n this._readyResolve = resolve;\n this._readyReject = reject;\n });\n }\n\n connectedCallback() {\n if (this._booted) {\n // Re-attached after a removal — resume what disconnected stopped.\n if (this._renderer) {\n this._renderer.setAnimationLoop(this._loop);\n this._ro && this._ro.observe(this);\n }\n return;\n }\n this._booted = true;\n this._boot().catch((err) => {\n this._err.style.display = 'flex';\n this._err.textContent =\n 'three.js failed to load.\\n' +\n 'Check that the pinned <script type=\"importmap\"> from the usage ' +\n 'notes is in <head> before any module script.\\n\\n' +\n String(err && err.message ? err.message : err);\n this._readyReject(err);\n });\n }\n\n async _boot() {\n const bg = this.getAttribute('background');\n if (bg) this.style.setProperty('--stage-bg', bg);\n const [THREE, controlsMod] = await Promise.all([\n import('three'),\n import('three/addons/controls/OrbitControls.js'),\n ]);\n this._THREE = THREE;\n // preserveDrawingBuffer keeps the last frame readable after\n // compositing (toDataURL / drawImage) — it's what lets the\n // screenshot tools capture the scene instead of a blank canvas.\n const renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: true,\n preserveDrawingBuffer: true,\n });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.shadowMap.enabled = true;\n renderer.shadowMap.type = THREE.PCFSoftShadowMap;\n this._renderer = renderer;\n this.shadowRoot.insertBefore(renderer.domElement, this._err);\n\n const scene = new THREE.Scene();\n this._scene = scene;\n\n const camera = new THREE.PerspectiveCamera(45, 1, 0.01, 500);\n camera.position.set(3, 2.2, 4);\n this._camera = camera;\n\n const controls = new controlsMod.OrbitControls(camera, renderer.domElement);\n controls.enableDamping = true;\n controls.dampingFactor = 0.08;\n this._controls = controls;\n\n // Neutral studio: soft sky/ground wash, a shadow-casting key light,\n // and a dim fill from behind so silhouettes never go black.\n scene.add(new THREE.HemisphereLight(0xffffff, 0xd8d2c4, 1.0));\n const key = new THREE.DirectionalLight(0xffffff, 2.2);\n key.position.set(4, 7, 5);\n key.castShadow = true;\n key.shadow.mapSize.set(2048, 2048);\n key.shadow.bias = -0.0002;\n this._key = key;\n scene.add(key);\n const fill = new THREE.DirectionalLight(0xfff4e6, 0.5);\n fill.position.set(-5, 3, -4);\n scene.add(fill);\n\n const ground = new THREE.Mesh(\n new THREE.PlaneGeometry(200, 200),\n new THREE.ShadowMaterial({ opacity: 0.18 })\n );\n ground.rotation.x = -Math.PI / 2;\n ground.receiveShadow = true;\n this._ground = ground;\n scene.add(ground);\n\n this._autorotate = this.hasAttribute('autorotate');\n controls.autoRotate = this._autorotate;\n controls.autoRotateSpeed = 1.2;\n controls.addEventListener('start', () => {\n controls.autoRotate = false;\n });\n\n const fit = () => {\n const w = this.clientWidth || 1;\n const h = this.clientHeight || 1;\n renderer.setSize(w, h);\n camera.aspect = w / h;\n camera.updateProjectionMatrix();\n };\n fit();\n this._ro = new ResizeObserver(fit);\n this._loop = () => {\n controls.update();\n renderer.render(scene, camera);\n };\n // Detached while three.js was fetching? Stay idle — the\n // connectedCallback resume starts the loop and observer on\n // reattach.\n if (this.isConnected) {\n this._ro.observe(this);\n renderer.setAnimationLoop(this._loop);\n }\n\n this._readyResolve({ THREE });\n }\n\n disconnectedCallback() {\n // Stop rendering and observing while detached; connectedCallback\n // resumes both. (The renderer itself is kept — a move within the\n // document must not rebuild the scene.)\n if (this._renderer) this._renderer.setAnimationLoop(null);\n if (this._ro) this._ro.disconnect();\n }\n\n /** Show (and own) the object. Replaces any previous object, enables\n * shadows on every mesh, rests it on the ground plane, and frames\n * the camera to its bounds. */\n setObject(object) {\n const THREE = this._THREE;\n if (!THREE) throw new Error('three-d-stage: not ready — await stage.ready first');\n if (this._object) this._scene.remove(this._object);\n this._object = object;\n object.traverse((o) => {\n if (o.isMesh) {\n o.castShadow = true;\n o.receiveShadow = true;\n }\n });\n const box = new THREE.Box3().setFromObject(object);\n if (!box.isEmpty()) {\n // Rest the object on the ground without moving its origin.\n this._ground.position.y = box.min.y;\n const sphere = box.getBoundingSphere(new THREE.Sphere());\n const dist =\n (sphere.radius / Math.tan((this._camera.fov * Math.PI) / 360)) * 1.35;\n const dir = new THREE.Vector3(1, 0.55, 1.25).normalize();\n this._camera.position\n .copy(sphere.center)\n .add(dir.multiplyScalar(dist));\n this._camera.near = Math.max(dist / 100, 0.01);\n this._camera.far = dist * 100;\n this._camera.updateProjectionMatrix();\n this._controls.target.copy(sphere.center);\n this._controls.update();\n const span = sphere.radius * 3;\n this._key.shadow.camera.left = -span;\n this._key.shadow.camera.right = span;\n this._key.shadow.camera.top = span;\n this._key.shadow.camera.bottom = -span;\n this._key.shadow.camera.updateProjectionMatrix();\n }\n this._scene.add(object);\n this._setButtonsEnabled(true);\n }\n\n get _basename() {\n return (this.getAttribute('name') || 'model').replace(/[^\\w.-]+/g, '_');\n }\n\n _setButtonsEnabled(on) {\n this._objBtn.disabled = !on;\n this._glbBtn.disabled = !on;\n }\n\n /** Every mesh and material needs a unique name for o/usemtl lines —\n * fill in stable fallbacks, and return the unique material list. */\n _nameParts() {\n const mats = [];\n const seen = new Set();\n let meshI = 0;\n let matI = 0;\n this._object.traverse((o) => {\n if (!o.isMesh) return;\n if (!o.name) o.name = 'part_' + meshI;\n meshI += 1;\n const list = Array.isArray(o.material) ? o.material : [o.material];\n for (const m of list) {\n if (!m || mats.includes(m)) continue;\n if (!m.name) {\n m.name = 'mat_' + matI;\n matI += 1;\n }\n while (seen.has(m.name)) {\n m.name = m.name + '_' + matI;\n matI += 1;\n }\n seen.add(m.name);\n mats.push(m);\n }\n });\n return mats;\n }\n\n /** One export attempt, reported to the host however it settles.\n * Rethrows so a failure stays visible on the guest console exactly as\n * before. The no-object early return is not an attempt (the toolbar is\n * disabled until the model loads) and reports nothing. */\n async _runExport(format) {\n if (!this._object) return;\n try {\n await (format === 'obj' ? this._exportObj() : this._exportGlb());\n notifyExport(format, true);\n } catch (err) {\n notifyExport(format, false);\n throw err;\n }\n }\n\n async _exportObj() {\n if (!this._object) return;\n const mod = await import('three/addons/exporters/OBJExporter.js');\n const mats = this._nameParts();\n const base = this._basename;\n const obj =\n 'mtllib ' + base + '.mtl\\n' + new mod.OBJExporter().parse(this._object);\n let mtl = '# Exported by three-d-stage\\n';\n for (const m of mats) {\n const c = m.color || { r: 0.8, g: 0.8, b: 0.8 };\n const rough = typeof m.roughness === 'number' ? m.roughness : 0.5;\n const opacity = typeof m.opacity === 'number' ? m.opacity : 1;\n mtl += 'newmtl ' + m.name + '\\n';\n mtl +=\n 'Kd ' + c.r.toFixed(4) + ' ' + c.g.toFixed(4) + ' ' + c.b.toFixed(4) + '\\n';\n mtl += 'Ks 0.2000 0.2000 0.2000\\n';\n mtl += 'Ns ' + Math.round((1 - rough) * 200) + '\\n';\n mtl += 'd ' + opacity.toFixed(4) + '\\n\\n';\n }\n download(new Blob([obj], { type: 'text/plain' }), base + '.obj');\n download(new Blob([mtl], { type: 'text/plain' }), base + '.mtl');\n }\n\n async _exportGlb() {\n if (!this._object) return;\n const mod = await import('three/addons/exporters/GLTFExporter.js');\n this._nameParts();\n const base = this._basename;\n const buf = await new mod.GLTFExporter().parseAsync(this._object, {\n binary: true,\n });\n download(\n new Blob([buf], { type: 'model/gltf-binary' }),\n base + '.glb'\n );\n }\n }\n\n customElements.define('three-d-stage', ThreeDStage);\n})();\n<\/script>\n<script type=\"module\">\nimport { buildSofa, buildRegal, regalMaterials, buildLampe, buildBoxen, buildTripod } from 'data:text/javascript;base64,Ly8gTcO2YmVsLUJ1aWxkZXIgKGF1cyBTb2ZhIDNEIC8gUmVnYWwgM0QgLyBCb2dlbmxhbXBlIDNEIMO8YmVybm9tbWVuKQpjb25zdCBtayA9IChUSFJFRSwgbmFtZSwgZ2VvLCBtYXQsIHg9MCwgeT0wLCB6PTApID0+IHsgY29uc3QgbSA9IG5ldyBUSFJFRS5NZXNoKGdlbywgbWF0KTsgbS5uYW1lPW5hbWU7IG0ucG9zaXRpb24uc2V0KHgseSx6KTsgcmV0dXJuIG07IH07CgpleHBvcnQgZnVuY3Rpb24gYnVpbGRTb2ZhKFRIUkVFKXsKICBjb25zdCBNID0gewogICAgZmFicmljOiBuZXcgVEhSRUUuTWVzaFN0YW5kYXJkTWF0ZXJpYWwoeyBuYW1lOidwb2x5X3dvb2xfaGVycmluZ2JvbmUnLCBjb2xvcjoweDhiODc4MCwgcm91Z2huZXNzOjAuOTUgfSksCiAgICBtZXRhbDogIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J2JsYWNrX3Bvd2RlcmNvYXQnLCBjb2xvcjoweDFkMWQxZiwgcm91Z2huZXNzOjAuNDUsIG1ldGFsbmVzczowLjYgfSksCiAgICBjdXA6ICAgIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J2N1cGhvbGRlcl9ibGFjaycsIGNvbG9yOjB4MTUxNTE1LCByb3VnaG5lc3M6MC44IH0pLAogIH07CiAgY29uc3QgVEVYID0gMC41OwogIGZ1bmN0aW9uIHJib3godywgaCwgZCwgciwgayA9IDcsIGJ1bGdlID0gMCl7CiAgICBjb25zdCBoYWxmID0gW3cvMiwgaC8yLCBkLzJdOwogICAgY29uc3QgUyA9IGhhbGYubWFwKGhoID0+IHsgY29uc3QgYT1bXTsgZm9yKGxldCBpPWs7aT49MDtpLS0pIGEucHVzaCgtKGhoLXIpIC0gcipNYXRoLnNpbihpL2sqTWF0aC5QSS8yKSk7IGZvcihsZXQgaT0wO2k8PWs7aSsrKSBhLnB1c2goKGhoLXIpICsgcipNYXRoLnNpbihpL2sqTWF0aC5QSS8yKSk7IHJldHVybiBhOyB9KTsKICAgIGNvbnN0IHBvcz1bXSwgbm9yPVtdLCB1dj1bXSwgaWR4PVtdOwogICAgZm9yIChsZXQgYT0wO2E8MzthKyspIGZvciAoY29uc3QgcyBvZiBbMSwtMV0pewogICAgICBjb25zdCB1PShhKzEpJTMsIHY9KGErMiklMywgc3U9U1t1XSwgc3Y9U1t2XSwgYmFzZT1wb3MubGVuZ3RoLzM7CiAgICAgIGZvciAobGV0IGo9MDtqPHN2Lmxlbmd0aDtqKyspIGZvciAobGV0IGk9MDtpPHN1Lmxlbmd0aDtpKyspewogICAgICAgIGNvbnN0IHA9WzAsMCwwXTsgcFthXT1zKmhhbGZbYV07IHBbdV09c3VbaV07IHBbdl09c3Zbal07CiAgICAgICAgY29uc3QgYz1wLm1hcCgoeCxtKT0+TWF0aC5tYXgoLShoYWxmW21dLXIpLE1hdGgubWluKGhhbGZbbV0tcix4KSkpOwogICAgICAgIGxldCBvPXAubWFwKCh4LG0pPT54LWNbbV0pOyBjb25zdCBMPU1hdGguaHlwb3QoLi4ubyl8fDE7IG89by5tYXAoeD0+eC9MKTsKICAgICAgICBjb25zdCBxPWMubWFwKCh4LG0pPT54K29bbV0qcik7CiAgICAgICAgcG9zLnB1c2goLi4ucSk7IG5vci5wdXNoKC4uLm8pOwogICAgICAgIGNvbnN0IGF4PU1hdGguYWJzKG9bMF0pLGF5PU1hdGguYWJzKG9bMV0pLGF6PU1hdGguYWJzKG9bMl0pOwogICAgICAgIGlmIChheT49YXgmJmF5Pj1heikgdXYucHVzaChxWzBdL1RFWCxxWzJdL1RFWCk7IGVsc2UgaWYgKGF4Pj1heikgdXYucHVzaChxWzJdL1RFWCxxWzFdL1RFWCk7IGVsc2UgdXYucHVzaChxWzBdL1RFWCxxWzFdL1RFWCk7CiAgICAgIH0KICAgICAgY29uc3QgbnU9c3UubGVuZ3RoOwogICAgICBmb3IgKGxldCBqPTA7ajxzdi5sZW5ndGgtMTtqKyspIGZvciAobGV0IGk9MDtpPG51LTE7aSsrKXsKICAgICAgICBjb25zdCBBPWJhc2UraipudStpLCBCPUErMSwgQz1BK251KzEsIEQ9QStudTsKICAgICAgICBpZiAocz4wKSBpZHgucHVzaChBLEIsQywgQSxDLEQpOyBlbHNlIGlkeC5wdXNoKEEsQyxCLCBBLEQsQyk7CiAgICAgIH0KICAgIH0KICAgIGNvbnN0IGcgPSBuZXcgVEhSRUUuQnVmZmVyR2VvbWV0cnkoKTsKICAgIGcuc2V0QXR0cmlidXRlKCdwb3NpdGlvbicsIG5ldyBUSFJFRS5GbG9hdDMyQnVmZmVyQXR0cmlidXRlKHBvcywzKSk7CiAgICBnLnNldEF0dHJpYnV0ZSgnbm9ybWFsJywgbmV3IFRIUkVFLkZsb2F0MzJCdWZmZXJBdHRyaWJ1dGUobm9yLDMpKTsKICAgIGcuc2V0QXR0cmlidXRlKCd1dicsIG5ldyBUSFJFRS5GbG9hdDMyQnVmZmVyQXR0cmlidXRlKHV2LDIpKTsKICAgIGcuc2V0SW5kZXgoaWR4KTsKICAgIGlmIChidWxnZSl7CiAgICAgIGNvbnN0IHA9Zy5hdHRyaWJ1dGVzLnBvc2l0aW9uOwogICAgICBmb3IgKGxldCBpPTA7aTxwLmNvdW50O2krKyl7IGNvbnN0IHg9cC5nZXRYKGkpL2hhbGZbMF0sIHk9cC5nZXRZKGkpL2hhbGZbMV07IHAuc2V0WihpLCBwLmdldFooaSkqKDErYnVsZ2UqKDEteCp4KSooMS15KnkpKSk7IH0KICAgICAgZy5jb21wdXRlVmVydGV4Tm9ybWFscygpOwogICAgfQogICAgcmV0dXJuIGc7CiAgfQogIGNvbnN0IG1lc2ggPSAoLi4uYSkgPT4gbWsoVEhSRUUsIC4uLmEpOwogIGNvbnN0IFc9MS44NSwgRD0wLjkyNSwgTD0wLjIwLCBBUk09MC4yNCwgVE9QPTAuMjEsIEc9MC4wMDQ7CiAgY29uc3Qgc29mYSA9IG5ldyBUSFJFRS5Hcm91cCgpOyBzb2ZhLm5hbWUgPSAnc29mYSc7CiAgZm9yIChjb25zdCBbaSxzeF0gb2YgW1sxLC0xXSxbMiwxXV0pCiAgICBzb2ZhLmFkZChtZXNoKCdsYWdlX3VudGVuXycraSwgcmJveChXLzItRywgTC1HLCBELCAwLjAzNSksIE0uZmFicmljLCBzeCpXLzQsIEwvMiwgMCkpOwogIGNvbnN0IGhpbmdlID0gbmV3IFRIUkVFLkdyb3VwKCk7IGhpbmdlLm5hbWU9J2tsYXBwdGVpbCc7IGhpbmdlLnBvc2l0aW9uLnNldCgwLCBMLCBELzIpOwogIGZvciAoY29uc3QgW2ksc3hdIG9mIFtbMSwtMV0sWzIsMV1dKQogICAgaGluZ2UuYWRkKG1lc2goJ2xhZ2VfbWl0dGVfJytpLCByYm94KFcvMi1HLCBMLUcsIEQsIDAuMDM1KSwgTS5mYWJyaWMsIHN4KlcvNCwgTC8yLCAtRC8yKSk7CiAgc29mYS5hZGQoaGluZ2UpOwogIGNvbnN0IHRvcCA9IG5ldyBUSFJFRS5Hcm91cCgpOyB0b3AubmFtZT0nYXVmYmF1JzsgdG9wLnBvc2l0aW9uLnkgPSAyKkw7CiAgZm9yIChjb25zdCBbbixzeF0gb2YgW1snbGlua3MnLC0xXSxbJ3JlY2h0cycsMV1dKXsKICAgIGNvbnN0IHggPSBzeCooVy8yLUFSTS8yKTsKICAgIHRvcC5hZGQobWVzaCgnYXJtbGVobmVfJytuLCByYm94KEFSTSwgVE9QLCBELCAwLjA1KSwgTS5mYWJyaWMsIHgsIFRPUC8yLCAwKSk7CiAgICB0b3AuYWRkKG1lc2goJ2JlY2hlcmhhbHRlcl8nK24sIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KDAuMDQsMC4wNCwwLjAxMiw0MCksIE0uY3VwLCB4LCBUT1ArMC4wMDEsIEQvMi0wLjEpKTsKICAgIGNvbnN0IHJpbmcgPSBtZXNoKCdiZWNoZXJoYWx0ZXJfcmluZ18nK24sIG5ldyBUSFJFRS5Ub3J1c0dlb21ldHJ5KDAuMDQyLDAuMDA1LDEyLDQwKSwgTS5tZXRhbCwgeCwgVE9QKzAuMDA0LCBELzItMC4xKTsKICAgIHJpbmcucm90YXRpb24ueCA9IE1hdGguUEkvMjsgdG9wLmFkZChyaW5nKTsKICB9CiAgY29uc3QgYmFja1cgPSAoVy0yKkFSTSkvMiAtIEc7CiAgZm9yIChjb25zdCBbaSxzeF0gb2YgW1sxLC0xXSxbMiwxXV0pewogICAgdG9wLmFkZChtZXNoKCdydWVja2VudGVpbF8nK2ksIHJib3goYmFja1csIFRPUCwgQVJNLCAwLjA1KSwgTS5mYWJyaWMsIHN4KihiYWNrVy8yK0cvMiksIFRPUC8yLCAtRC8yK0FSTS8yKSk7CiAgICBjb25zdCBwID0gbWVzaCgna2lzc2VuXycraSwgcmJveCgwLjQ1LCAwLjQ1LCAwLjEwLCAwLjA0LCA3LCAwLjYpLCBNLmZhYnJpYywgc3gqMC4zMywgMC4yMiwgLUQvMitBUk0rMC4wOCk7CiAgICBwLnJvdGF0aW9uLnggPSAtMC4yODsgcC5yb3RhdGlvbi56ID0gc3gqMC4wNjsgdG9wLmFkZChwKTsKICB9CiAgc29mYS5hZGQodG9wKTsKICBjb25zdCBmejAgPSAtMC4wNSwgZnoxID0gMC4zMCwgZmggPSAwLjM0LCByciA9IDAuMDA4LCBSID0gMC4wNDUsIHkwID0gMC4wMiwgeXQgPSB5MCArIGZoOwogIGZvciAoY29uc3QgW24sc3hdIG9mIFtbJ2xpbmtzJywtMV0sWydyZWNodHMnLDFdXSl7CiAgICBjb25zdCBmcmFtZSA9IG5ldyBUSFJFRS5Hcm91cCgpOyBmcmFtZS5uYW1lPSdzZWl0ZW5nZXN0ZWxsXycrbjsKICAgIGNvbnN0IGZ4ID0gc3gqKFcvMiArIDAuMDEyKTsKICAgIGZvciAoY29uc3QgeiBvZiBbZnowLCBmejFdKSBmcmFtZS5hZGQobWVzaCgnZ2VzdGVsbF9zdGFiXycrbiwgbmV3IFRIUkVFLkN5bGluZGVyR2VvbWV0cnkocnIscnIsZmgtUiwyMCksIE0ubWV0YWwsIGZ4LCB5MCsoZmgtUikvMiwgeikpOwogICAgY29uc3QgYmFyID0gbWVzaCgnZ2VzdGVsbF9xdWVyXycrbiwgbmV3IFRIUkVFLkN5bGluZGVyR2VvbWV0cnkocnIscnIsZnoxLWZ6MC0yKlIsMjApLCBNLm1ldGFsLCBmeCwgeXQsIChmejArZnoxKS8yKTsgYmFyLnJvdGF0aW9uLng9TWF0aC5QSS8yOyBmcmFtZS5hZGQoYmFyKTsKICAgIGZvciAoY29uc3QgW3oscnldIG9mIFtbZnowK1IsIE1hdGguUEkvMl0sW2Z6MS1SLCAtTWF0aC5QSS8yXV0pewogICAgICBjb25zdCBjID0gbWVzaCgnZ2VzdGVsbF9ib2dlbl8nK24sIG5ldyBUSFJFRS5Ub3J1c0dlb21ldHJ5KFIsIHJyLCAxNiwgMjQsIE1hdGguUEkvMiksIE0ubWV0YWwsIGZ4LCB5dC1SLCB6KTsgYy5yb3RhdGlvbi55ID0gcnk7IGZyYW1lLmFkZChjKTsKICAgIH0KICAgIHNvZmEuYWRkKGZyYW1lKTsKICB9CiAgcmV0dXJuIHsgZ3JvdXA6IHNvZmEsIE0sIEQgfTsKfQoKZXhwb3J0IGZ1bmN0aW9uIGJ1aWxkUmVnYWwoVEhSRUUsIE0pewogIGNvbnN0IGJveCA9ICh3LCBoLCBkKSA9PiB7CiAgICBjb25zdCBnID0gbmV3IFRIUkVFLkJveEdlb21ldHJ5KHcsIGgsIGQpLCB1diA9IGcuYXR0cmlidXRlcy51djsKICAgIGNvbnN0IGZhY2VzID0gW1tkLGhdLFtkLGhdLFt3LGRdLFt3LGRdLFt3LGhdLFt3LGhdXTsKICAgIGZvciAobGV0IGY9MDsgZjw2OyBmKyspIGZvciAobGV0IGk9MDtpPDQ7aSsrKXsgY29uc3Qgaz1mKjQraTsgdXYuc2V0WFkoaywgdXYuZ2V0WChrKSpmYWNlc1tmXVswXSwgdXYuZ2V0WShrKSpmYWNlc1tmXVsxXSk7IH0KICAgIHJldHVybiBnOwogIH07CiAgY29uc3QgbWVzaCA9ICguLi5hKSA9PiBtayhUSFJFRSwgLi4uYSk7CiAgY29uc3QgVz0wLjYwLCBEPTAuMjcsIEg9MC43NCwgQT0wLjAzLCBUPTAuMDAxNSwgTElQPTAuMDIsIEZPT1Q9MC4wMTI7CiAgY29uc3Qgc2hlbGZUb3BzID0gWzAuMTAsIDAuNDIsIEhdOwogIGNvbnN0IHJlZ2FsID0gbmV3IFRIUkVFLkdyb3VwKCk7IHJlZ2FsLm5hbWU9J3JlZ2FsJzsKICBjb25zdCBwb3N0SCA9IEggLSBGT09UOwogIGZvciAoY29uc3QgW24sc3gsc3pdIG9mIFtbJ3ZsJywtMSwxXSxbJ3ZyJywxLDFdLFsnaGwnLC0xLC0xXSxbJ2hyJywxLC0xXV0pewogICAgY29uc3QgcCA9IG5ldyBUSFJFRS5Hcm91cCgpOyBwLm5hbWU9J3Bmb3N0ZW5fJytuOwogICAgY29uc3QgY3ggPSBzeCooVy8yIC0gQS8yKSwgY3ogPSBzeiooRC8yIC0gQS8yKSwgeSA9IEZPT1QgKyBwb3N0SC8yOwogICAgcC5hZGQobWVzaCgncGZvc3Rlbl8nK24rJ19hJywgYm94KEEsIHBvc3RILCBUKSwgTS5jb25jcmV0ZSwgY3gsIHksIHN6KihELzIgLSBULzIpKSk7CiAgICBwLmFkZChtZXNoKCdwZm9zdGVuXycrbisnX2InLCBib3goVCwgcG9zdEgsIEEpLCBNLmNvbmNyZXRlLCBzeCooVy8yIC0gVC8yKSwgeSwgY3opKTsKICAgIHAuYWRkKG1lc2goJ2Z1c3NfJytuLCBib3goQSswLjAwMiwgRk9PVCwgQSswLjAwMiksIE0uZm9vdCwgY3gsIEZPT1QvMiwgY3opKTsKICAgIGZvciAoY29uc3QgdG9wIG9mIHNoZWxmVG9wcyl7CiAgICAgIGNvbnN0IHliID0gdG9wIC0gTElQLzI7CiAgICAgIGNvbnN0IGIxID0gbWVzaCgnc2NocmF1YmVfJytuLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeSgwLjAwNCwwLjAwNCwwLjAwMywxNiksIE0uYm9sdCwgY3gsIHliLCBzeiooRC8yKzAuMDAxKSk7IGIxLnJvdGF0aW9uLnggPSBNYXRoLlBJLzI7IHAuYWRkKGIxKTsKICAgICAgY29uc3QgYjIgPSBtZXNoKCdzY2hyYXViZV8nK24sIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KDAuMDA0LDAuMDA0LDAuMDAzLDE2KSwgTS5ib2x0LCBzeCooVy8yKzAuMDAxKSwgeWIsIGN6KTsgYjIucm90YXRpb24ueiA9IE1hdGguUEkvMjsgcC5hZGQoYjIpOwogICAgfQogICAgcmVnYWwuYWRkKHApOwogIH0KICBzaGVsZlRvcHMuZm9yRWFjaCgodG9wLCBpKSA9PiB7CiAgICBjb25zdCBzID0gbmV3IFRIUkVFLkdyb3VwKCk7IHMubmFtZT0nYm9kZW5fJysoaSsxKTsKICAgIGNvbnN0IGl3ID0gVyAtIDIqVCwgaWQgPSBEIC0gMipUOwogICAgcy5hZGQobWVzaCgnYm9kZW5fJysoaSsxKSsnX3BsYXR0ZScsIGJveChpdywgVCwgaWQpLCBNLmNvbmNyZXRlLCAwLCB0b3AgLSBULzIsIDApKTsKICAgIGZvciAoY29uc3Qgc3ogb2YgWzEsLTFdKSBzLmFkZChtZXNoKCdib2Rlbl8nKyhpKzEpKydfa2FudGVfeicsIGJveChpdywgTElQLCBUKSwgTS5jb25jcmV0ZSwgMCwgdG9wIC0gTElQLzIsIHN6KihpZC8yIC0gVC8yKSkpOwogICAgZm9yIChjb25zdCBzeCBvZiBbMSwtMV0pIHMuYWRkKG1lc2goJ2JvZGVuXycrKGkrMSkrJ19rYW50ZV94JywgYm94KFQsIExJUCwgaWQpLCBNLmNvbmNyZXRlLCBzeCooaXcvMiAtIFQvMiksIHRvcCAtIExJUC8yLCAwKSk7CiAgICByZWdhbC5hZGQocyk7CiAgfSk7CiAgcmV0dXJuIHJlZ2FsOwp9CmV4cG9ydCBmdW5jdGlvbiByZWdhbE1hdGVyaWFscyhUSFJFRSl7CiAgcmV0dXJuIHsKICAgIGNvbmNyZXRlOiBnYWx2YW5pemVkTWF0ZXJpYWwoVEhSRUUsIDE4KSwgLy8gdmVyemlua3RlciBTdGFobCwgVGV4dHVyIOKAnkZlaW7igJwKICAgIGZvb3Q6ICAgICBuZXcgVEhSRUUuTWVzaFN0YW5kYXJkTWF0ZXJpYWwoeyBuYW1lOidrdW5zdHN0b2ZmX2Z1c3MnLCBjb2xvcjoweDJhMmEyYSwgcm91Z2huZXNzOjAuNyB9KSwKICAgIGJvbHQ6ICAgICBuZXcgVEhSRUUuTWVzaFN0YW5kYXJkTWF0ZXJpYWwoeyBuYW1lOidzY2hyYXViZScsIGNvbG9yOjB4OGM4YzhjLCByb3VnaG5lc3M6MC4zNSwgbWV0YWxuZXNzOjAuOSB9KSwKICB9Owp9CgpleHBvcnQgZnVuY3Rpb24gYnVpbGRMYW1wZShUSFJFRSl7CiAgY29uc3QgViA9ICh4LHksej0wKSA9PiBuZXcgVEhSRUUuVmVjdG9yMyh4LHkseik7CiAgY29uc3QgTSA9IHsKICAgIGNocm9tOiAgbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTonY2hyb20nLCBjb2xvcjoweGQ4ZGFkZCwgbWV0YWxuZXNzOjAuNSwgcm91Z2huZXNzOjAuMjUgfSksCiAgICBzY2hpcm06IG5ldyBUSFJFRS5NZXNoUGh5c2ljYWxNYXRlcmlhbCh7IG5hbWU6J2FjcnlsX3dlaXNzJywgY29sb3I6MHhmN2Y3ZjUsIHJvdWdobmVzczowLjI4LCBjbGVhcmNvYXQ6MC44LCBjbGVhcmNvYXRSb3VnaG5lc3M6MC4xLCBzaWRlOlRIUkVFLkRvdWJsZVNpZGUgfSksCiAgICBiaXJuZTogIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J2xldWNodG1pdHRlbCcsIGNvbG9yOjB4ZmZmZmZmLCBlbWlzc2l2ZToweGZmZjFkNiwgZW1pc3NpdmVJbnRlbnNpdHk6MC4yIH0pLAogIH07CiAgY29uc3QgYWRkID0gKHBhcmVudCwgbmFtZSwgZ2VvLCBtYXQsIHA9WzAsMCwwXSwgcj1bMCwwLDBdKSA9PiB7IGNvbnN0IG0gPSBuZXcgVEhSRUUuTWVzaChnZW8sIG1hdCk7IG0ubmFtZSA9IG5hbWU7IG0ucG9zaXRpb24uc2V0KC4uLnApOyBtLnJvdGF0aW9uLnNldCguLi5yKTsgcGFyZW50LmFkZChtKTsgcmV0dXJuIG07IH07CiAgY29uc3QgbGFtcGUgPSBuZXcgVEhSRUUuR3JvdXAoKTsgbGFtcGUubmFtZSA9ICdib2dlbmxhbXBlJzsKICBjb25zdCBIID0gMS42NiwgUkVBQ0ggPSAxLjIyLCBTSF9SID0gMC4xNSwgQkFTRV9SID0gMC4xMywgQkFTRV9IID0gMC4wMjgsIFBPTEVfUiA9IDAuMDEyNSwgQVJDX1IgPSAwLjAwODUsIFBPTEVfVE9QID0gMC43NDsKICBjb25zdCBwcm9mID0gW25ldyBUSFJFRS5WZWN0b3IyKDAsIDApLCBuZXcgVEhSRUUuVmVjdG9yMihCQVNFX1IgLSAwLjAwNCwgMCldOwogIGZvciAobGV0IGkgPSAwOyBpIDw9IDEwOyBpKyspeyBjb25zdCBhID0gLU1hdGguUEkvMiArIGkvMTAqTWF0aC5QSS8yOyBwcm9mLnB1c2gobmV3IFRIUkVFLlZlY3RvcjIoQkFTRV9SIC0gMC4wMDggKyBNYXRoLmNvcyhhKSowLjAwOCwgMC4wMDggKyBNYXRoLnNpbihhKSowLjAwOCkpOyB9CiAgZm9yIChsZXQgaSA9IDA7IGkgPD0gMTA7IGkrKyl7IGNvbnN0IGEgPSBpLzEwKk1hdGguUEkvMjsgcHJvZi5wdXNoKG5ldyBUSFJFRS5WZWN0b3IyKEJBU0VfUiAtIDAuMDEyICsgTWF0aC5jb3MoYSkqMC4wMTIsIEJBU0VfSCAtIDAuMDEyICsgTWF0aC5zaW4oYSkqMC4wMTIpKTsgfQogIHByb2YucHVzaChuZXcgVEhSRUUuVmVjdG9yMigwLCBCQVNFX0gpKTsKICBhZGQobGFtcGUsICdmdXNzJywgbmV3IFRIUkVFLkxhdGhlR2VvbWV0cnkocHJvZiwgOTYpLCBNLmNocm9tKTsKICBhZGQobGFtcGUsICdmdXNzX3Jvc2V0dGUnLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeSgwLjAyMiwgMC4wMjYsIDAuMDEyLCA0OCksIE0uY2hyb20sIFswLCBCQVNFX0ggKyAwLjAwNiwgMF0pOwogIGFkZChsYW1wZSwgJ3N0YW5kcm9ocicsIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KFBPTEVfUiwgUE9MRV9SLCBQT0xFX1RPUCAtIEJBU0VfSCwgNDgpLCBNLmNocm9tLCBbMCwgKFBPTEVfVE9QICsgQkFTRV9IKS8yLCAwXSk7CiAgYWRkKGxhbXBlLCAnbXVmZmVfdW50ZW4nLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeShQT0xFX1IgKyAwLjAwMTUsIFBPTEVfUiArIDAuMDAxNSwgMC4wMywgNDgpLCBNLmNocm9tLCBbMCwgUE9MRV9UT1AgLSAwLjAxNSwgMF0pOwogIGNvbnN0IEFSQ19FTkQgPSBWKFJFQUNIIC0gMC4wNCwgSCAtIEFSQ19SKTsKICBjb25zdCBhcmMgPSBuZXcgVEhSRUUuQ3ViaWNCZXppZXJDdXJ2ZTMoVigwLCBQT0xFX1RPUCAtIDAuMDIpLCBWKDAsIDEuMzgpLCBWKDAuNDIsIEggLSBBUkNfUiksIEFSQ19FTkQpOwogIGFkZChsYW1wZSwgJ2JvZ2VuJywgbmV3IFRIUkVFLlR1YmVHZW9tZXRyeShhcmMsIDI0MCwgQVJDX1IsIDMyKSwgTS5jaHJvbSk7CiAgY29uc3QgcE0gPSBhcmMuZ2V0UG9pbnRBdCgwLjM2KSwgZE0gPSBhcmMuZ2V0VGFuZ2VudEF0KDAuMzYpOwogIGNvbnN0IG11ZmZlID0gYWRkKGxhbXBlLCAnbXVmZmVfYm9nZW4nLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeShBUkNfUiArIDAuMDAxOCwgQVJDX1IgKyAwLjAwMTgsIDAuMDM1LCAzMiksIE0uY2hyb20sIFtwTS54LCBwTS55LCAwXSk7CiAgbXVmZmUucXVhdGVybmlvbi5zZXRGcm9tVW5pdFZlY3RvcnMoVigwLDEsMCksIGRNKTsKICBhZGQobGFtcGUsICdlbmRrYXBwZScsIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KEFSQ19SICsgMC4wMDIsIEFSQ19SICsgMC4wMDIsIDAuMDcsIDMyKSwgTS5jaHJvbSwgW0FSQ19FTkQueCAtIDAuMDIsIEFSQ19FTkQueSwgMF0sIFswLCAwLCBNYXRoLlBJLzJdKTsKICBjb25zdCBrb3BmID0gbmV3IFRIUkVFLkdyb3VwKCk7IGtvcGYubmFtZSA9ICdzY2hpcm1fZ3J1cHBlJzsKICBjb25zdCBUSUxUID0gMC4zOwogIGtvcGYucG9zaXRpb24uc2V0KFJFQUNIIC0gU0hfUipNYXRoLnNpbihUSUxUKSwgSCAtIEFSQ19SIC0gMC4wMzUsIDApOwogIGtvcGYucm90YXRpb24ueiA9IFRJTFQ7CiAgYWRkKGxhbXBlLCAnZ2VsZW5rJywgbmV3IFRIUkVFLkN5bGluZGVyR2VvbWV0cnkoMC4wMDYsIDAuMDA2LCAwLjAzLCAyMCksIE0uY2hyb20sIFtrb3BmLnBvc2l0aW9uLngsIEggLSBBUkNfUiAtIDAuMDE4LCAwXSk7CiAgbGFtcGUuYWRkKGtvcGYpOwogIGFkZChrb3BmLCAnc2NoaXJtJywgbmV3IFRIUkVFLlNwaGVyZUdlb21ldHJ5KFNIX1IsIDk2LCA0OCwgMCwgTWF0aC5QSSoyLCAwLCAxLjk1KSwgTS5zY2hpcm0sIFswLCAtU0hfUiwgMF0pOwogIGFkZChrb3BmLCAnc2NoaXJtX25hYmUnLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeSgwLjAxOCwgMC4wMjgsIDAuMDIsIDQwKSwgTS5jaHJvbSwgWzAsIC0wLjAwNCwgMF0pOwogIGFkZChrb3BmLCAnZmFzc3VuZycsIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KDAuMDE2LCAwLjAxNiwgMC4wNSwgMzIpLCBNLmNocm9tLCBbMCwgLTAuMDQsIDBdKTsKICBhZGQoa29wZiwgJ2xldWNodG1pdHRlbCcsIG5ldyBUSFJFRS5TcGhlcmVHZW9tZXRyeSgwLjAzLCAzMiwgMTYpLCBNLmJpcm5lLCBbMCwgLTAuMDksIDBdKTsKICByZXR1cm4geyBncm91cDogbGFtcGUsIFJFQUNIIH07Cn0KCi8vIFZlcnppbmt0ZXIgU3RhaGwgKFppbmstU3BhbmdsZSwgcHJvemVkdXJhbCkg4oCTIGF1cyBSZWdhbCAzRApleHBvcnQgZnVuY3Rpb24gZ2FsdmFuaXplZChzaXplPTEwMjQsIGNlbGxzPTI0LCBzZWVkPTcpewogIGxldCBzID0gc2VlZDsgY29uc3Qgcm5kID0gKCkgPT4gKHMgPSAocyoxNjgwNykgJSAyMTQ3NDgzNjQ3KSAvIDIxNDc0ODM2NDc7CiAgY29uc3QgZyA9IGNlbGxzLCBwdHMgPSBbXTsKICBmb3IgKGxldCBqPTA7ajxnO2orKykgZm9yIChsZXQgaT0wO2k8ZztpKyspewogICAgY29uc3QgYSA9IHJuZCgpKk1hdGguUEkqMjsKICAgIHB0cy5wdXNoKHsgeDooaStybmQoKSkvZywgeTooaitybmQoKSkvZywgYmFzZTogMC43MiArIHJuZCgpKjAuMTgsIGd4Ok1hdGguY29zKGEpLCBneTpNYXRoLnNpbihhKSwgazogMC4wMyArIHJuZCgpKjAuMDgsIHI6IHJuZCgpIH0pOwogIH0KICBjb25zdCBjb2wgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdjYW52YXMnKTsgY29sLndpZHRoID0gY29sLmhlaWdodCA9IHNpemU7CiAgY29uc3Qgcm91ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnY2FudmFzJyk7IHJvdS53aWR0aCA9IHJvdS5oZWlnaHQgPSBzaXplOwogIGNvbnN0IGNjID0gY29sLmdldENvbnRleHQoJzJkJyksIHJjID0gcm91LmdldENvbnRleHQoJzJkJyk7CiAgY29uc3QgY2kgPSBjYy5jcmVhdGVJbWFnZURhdGEoc2l6ZSxzaXplKSwgcmkgPSByYy5jcmVhdGVJbWFnZURhdGEoc2l6ZSxzaXplKTsKICBmb3IgKGxldCB5PTA7eTxzaXplO3krKykgZm9yIChsZXQgeD0wO3g8c2l6ZTt4KyspewogICAgY29uc3QgdSA9IHgvc2l6ZSwgdiA9IHkvc2l6ZSwgY3ggPSBNYXRoLmZsb29yKHUqZyksIGN5ID0gTWF0aC5mbG9vcih2KmcpOwogICAgbGV0IGJlc3QgPSA5LCBzZWNvbmQgPSA5LCBicCA9IG51bGwsIGJkeD0wLCBiZHk9MDsKICAgIGZvciAobGV0IG95PS0xO295PD0xO295KyspIGZvciAobGV0IG94PS0xO294PD0xO294KyspewogICAgICBjb25zdCBpeCA9IChjeCtveCtnKSVnLCBpeSA9IChjeStveStnKSVnLCBwID0gcHRzW2l5KmcraXhdOwogICAgICBjb25zdCBkeCA9IHUgLSAocC54ICsgTWF0aC5mbG9vcigoY3grb3gpL2cpICogMSArICgoY3grb3gpPDA/MDowKSkgLCBkeSA9IHYgLSAocC55ICsgTWF0aC5mbG9vcigoY3krb3kpL2cpKTsKICAgICAgY29uc3QgZCA9IGR4KmR4ICsgZHkqZHk7CiAgICAgIGlmIChkIDwgYmVzdCl7IHNlY29uZCA9IGJlc3Q7IGJlc3QgPSBkOyBicCA9IHA7IGJkeCA9IGR4OyBiZHkgPSBkeTsgfSBlbHNlIGlmIChkIDwgc2Vjb25kKSBzZWNvbmQgPSBkOwogICAgfQogICAgY29uc3QgZWRnZSA9IE1hdGguc3FydChzZWNvbmQpIC0gTWF0aC5zcXJ0KGJlc3QpOyAgICAgICAgICAgIC8vIEFic3RhbmQgenVyIEtvcm5ncmVuemUKICAgIGxldCBsID0gYnAuYmFzZSArIChiZHgqYnAuZ3ggKyBiZHkqYnAuZ3kpICogZyAqIGJwLms7ICAgICAgICAvLyBGYWNldHRlbi1WZXJsYXVmIHBybyBLb3JuCiAgICBsICs9IChNYXRoLnJhbmRvbSgpLTAuNSkqMC4wNTsgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLy8gZmVpbmVzIEtvcm4KICAgIGlmIChlZGdlIDwgMC4wMDE4KSBsIC09IDAuMDM1OyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLy8gZHVua2xlIEtvcm5ncmVuemVuCiAgICBpZiAoTWF0aC5yYW5kb20oKSA8IDAuMDAzKSBsIC09IDAuMTI7ICAgICAgICAgICAgICAgICAgICAgICAgLy8gZHVua2xlIFNwcmVua2VsCiAgICBsID0gTWF0aC5tYXgoMC4yNSwgTWF0aC5taW4oMC45NywgbCkpOwogICAgY29uc3QgbyA9ICh5KnNpemUreCkqNCwgYyA9IGwqMjU1OwogICAgY2kuZGF0YVtvXT1jKjAuOTg7IGNpLmRhdGFbbysxXT1jOyBjaS5kYXRhW28rMl09YyoxLjAyOyBjaS5kYXRhW28rM109MjU1OwogICAgY29uc3QgciA9ICgwLjI4ICsgKDEtbCkqMC4zNSArIGJwLnIqMC4xMikgKiAyNTU7ICAgICAgICAgICAgIC8vIGhlbGxlIEvDtnJuZXIgZ2zDpG56ZW5kZXIKICAgIHJpLmRhdGFbb109cmkuZGF0YVtvKzFdPXJpLmRhdGFbbysyXT1yOyByaS5kYXRhW28rM109MjU1OwogIH0KICBjYy5wdXRJbWFnZURhdGEoY2ksMCwwKTsgcmMucHV0SW1hZ2VEYXRhKHJpLDAsMCk7CiAgcmV0dXJuIHsgY29sLCByb3UgfTsKfQpleHBvcnQgZnVuY3Rpb24gZ2FsdmFuaXplZE1hdGVyaWFsKFRIUkVFLCByZXBlYXQgPSAxOCl7CiAgY29uc3QgeyBjb2wsIHJvdSB9ID0gZ2FsdmFuaXplZCgpOwogIGNvbnN0IG1rID0gKGMsIHNyZ2IpID0+IHsgY29uc3QgdCA9IG5ldyBUSFJFRS5DYW52YXNUZXh0dXJlKGMpOyB0LndyYXBTID0gdC53cmFwVCA9IFRIUkVFLlJlcGVhdFdyYXBwaW5nOyB0LnJlcGVhdC5zZXQocmVwZWF0LCByZXBlYXQpOwogICAgaWYgKHNyZ2IpIHQuY29sb3JTcGFjZSA9IFRIUkVFLlNSR0JDb2xvclNwYWNlOyB0LmFuaXNvdHJvcHkgPSA4OyByZXR1cm4gdDsgfTsKICByZXR1cm4gbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTonc3RhaGxfdmVyemlua3QnLCBtYXA6IG1rKGNvbCwgdHJ1ZSksIHJvdWdobmVzc01hcDogbWsocm91KSwgYnVtcE1hcDogbWsoY29sKSwgYnVtcFNjYWxlOiAwLjI1LAogICAgY29sb3I6IDB4ZWNlZWYwLCByb3VnaG5lc3M6IDEsIG1ldGFsbmVzczogMC4zIH0pOwp9CgovLyBSZWdhbGxhdXRzcHJlY2hlci1QYWFyIChhdXMgQm94ZW4gM0QpOiBwYXNzaXYgKyBha3RpdiAoQmVkaWVuZmVsZCBhbiBsb2thbGVyICt4LVNlaXRlKQpleHBvcnQgZnVuY3Rpb24gYnVpbGRCb3hlbihUSFJFRSl7CiAgY29uc3QgTSA9IHsKICAgIHdvb2Q6ICAgbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTonZWljaGUnLCBjb2xvcjoweGI3N2EzZSwgcm91Z2huZXNzOjAuNTUgfSksCiAgICBmYWJyaWM6IG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J3N0b2ZmX2dyYXUnLCBjb2xvcjoweDZjNmM2YSwgcm91Z2huZXNzOjAuOTcgfSksCiAgICBzdHJpcDogIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J3ppZXJsZWlzdGVfc2lsYmVyJywgY29sb3I6MHhjOWM5YzksIHJvdWdobmVzczowLjMsIG1ldGFsbmVzczowLjkgfSksCiAgICBibGFjazogIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J2t1bnN0c3RvZmZfc2Nod2FyeicsIGNvbG9yOjB4MWMxYzFjLCByb3VnaG5lc3M6MC42IH0pLAogICAga25vYjogICBuZXcgVEhSRUUuTWVzaFN0YW5kYXJkTWF0ZXJpYWwoeyBuYW1lOidkcmVocmVnbGVyJywgY29sb3I6MHgxNDE0MTQsIHJvdWdobmVzczowLjQsIG1ldGFsbmVzczowLjIgfSksCiAgICBhbHU6ICAgIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J2hvY2h0b2VuZXJfYWx1JywgY29sb3I6MHhhYWFhYWEsIHJvdWdobmVzczowLjM1LCBtZXRhbG5lc3M6MC44IH0pLAogICAgbGVkOiAgICBuZXcgVEhSRUUuTWVzaFN0YW5kYXJkTWF0ZXJpYWwoeyBuYW1lOidsZWQnLCBjb2xvcjoweDBhMmExMCwgZW1pc3NpdmU6MHgyYmQ0NWEsIGVtaXNzaXZlSW50ZW5zaXR5OjEuMiB9KSwKICB9OwogIGNvbnN0IFc9MC4xNDYsIEQ9MC4xOTYsIEg9MC4yMzQsIFI9MC4wMTQsIEJWPTAuMDAyOwogIGNvbnN0IG1lc2ggPSAobmFtZSwgZ2VvLCBtYXQsIHg9MCwgeT0wLCB6PTApID0+IG1rKFRIUkVFLCBuYW1lLCBnZW8sIG1hdCwgeCwgeSwgeik7CiAgY29uc3QgcnJlY3QgPSAodywgaCwgcikgPT4geyBjb25zdCBzID0gbmV3IFRIUkVFLlNoYXBlKCksIHg9LXcvMiwgeT0taC8yOwogICAgcy5tb3ZlVG8oeCtyLHkpOyBzLmxpbmVUbyh4K3ctcix5KTsgcy5xdWFkcmF0aWNDdXJ2ZVRvKHgrdyx5LHgrdyx5K3IpOyBzLmxpbmVUbyh4K3cseStoLXIpOyBzLnF1YWRyYXRpY0N1cnZlVG8oeCt3LHkraCx4K3ctcix5K2gpOwogICAgcy5saW5lVG8oeCtyLHkraCk7IHMucXVhZHJhdGljQ3VydmVUbyh4LHkraCx4LHkraC1yKTsgcy5saW5lVG8oeCx5K3IpOyBzLnF1YWRyYXRpY0N1cnZlVG8oeCx5LHgrcix5KTsgcmV0dXJuIHM7IH07CiAgY29uc3QgY2FiR2VvID0gbmV3IFRIUkVFLkV4dHJ1ZGVHZW9tZXRyeShycmVjdChXLTIqQlYsIEQtMipCViwgUiksIHsgZGVwdGg6SC0yKkJWLCBiZXZlbEVuYWJsZWQ6dHJ1ZSwgYmV2ZWxTaXplOkJWLCBiZXZlbFRoaWNrbmVzczpCViwgYmV2ZWxTZWdtZW50czoyLCBjdXJ2ZVNlZ21lbnRzOjEwIH0pOwogIGNhYkdlby5yb3RhdGVYKC1NYXRoLlBJLzIpOyBjYWJHZW8udHJhbnNsYXRlKDAsIEJWLCAwKTsKICB7IGNvbnN0IHAgPSBjYWJHZW8uYXR0cmlidXRlcy5wb3NpdGlvbiwgbiA9IGNhYkdlby5hdHRyaWJ1dGVzLm5vcm1hbCwgdXYgPSBjYWJHZW8uYXR0cmlidXRlcy51djsKICAgIGZvciAobGV0IGk9MDtpPHAuY291bnQ7aSsrKXsgY29uc3QgeD1wLmdldFgoaSksIHk9cC5nZXRZKGkpLCB6PXAuZ2V0WihpKTsKICAgICAgaWYgKE1hdGguYWJzKG4uZ2V0WShpKSkgPiAwLjcpIHV2LnNldFhZKGksIHgsIHopOyBlbHNlIHV2LnNldFhZKGksIE1hdGguYWJzKG4uZ2V0WChpKSkgPiBNYXRoLmFicyhuLmdldFooaSkpID8geiA6IHgsIHkpOyB9IH0KICBjb25zdCBHVz1XLTAuMDA2LCBHSD1ILTAuMDA2LCBHVD0wLjAwODsKICBjb25zdCBncmlsbGVHZW8gPSBuZXcgVEhSRUUuRXh0cnVkZUdlb21ldHJ5KHJyZWN0KEdXLTAuMDA0LCBHSC0wLjAwNCwgMC4wMDkpLCB7IGRlcHRoOkdULTAuMDAzLCBiZXZlbEVuYWJsZWQ6dHJ1ZSwgYmV2ZWxTaXplOjAuMDAyLCBiZXZlbFRoaWNrbmVzczowLjAwMTUsIGJldmVsU2VnbWVudHM6MywgY3VydmVTZWdtZW50czo4IH0pOwogIGZ1bmN0aW9uIHNwZWFrZXIobmFtZSwgYWN0aXZlKXsKICAgIGNvbnN0IHMgPSBuZXcgVEhSRUUuR3JvdXAoKTsgcy5uYW1lID0gbmFtZTsKICAgIHMuYWRkKG1lc2gobmFtZSsnX2dlaGFldXNlJywgY2FiR2VvLCBNLndvb2QpKTsKICAgIHMuYWRkKG1lc2gobmFtZSsnX3N0b2ZmJywgZ3JpbGxlR2VvLCBNLmZhYnJpYywgMCwgSC8yLCBELzIgKyAwLjAwMTUpKTsKICAgIHMuYWRkKG1lc2gobmFtZSsnX3ppZXJsZWlzdGUnLCBuZXcgVEhSRUUuQm94R2VvbWV0cnkoR1cqMC43NCwgMC4wMDI2LCAwLjAwMTIpLCBNLnN0cmlwLCAwLCBIKjAuNTg1LCBELzIgKyBHVCArIDAuMDAwNSkpOwogICAgcy5hZGQobWVzaChuYW1lKydfbG9nbycsIG5ldyBUSFJFRS5Cb3hHZW9tZXRyeSgwLjAyNiwgMC4wMDYsIDAuMDAwOCksIE0uc3RyaXAsIDAsIDAuMDI2LCBELzIgKyBHVCArIDAuMDAwMykpOwogICAgY29uc3QgcG9ydCA9IG1lc2gobmFtZSsnX2Jhc3NyZWZsZXgnLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeSgwLjAxNCwgMC4wMTQsIDAuMDA0LCAzMiksIE0uYmxhY2ssIDAsIDAuMTcsIC1ELzIgLSAwLjAwMSk7CiAgICBwb3J0LnJvdGF0aW9uLnggPSBNYXRoLlBJLzI7IHMuYWRkKHBvcnQpOwogICAgZm9yIChjb25zdCBbZngsZnpdIG9mIFtbLTEsLTFdLFsxLC0xXSxbLTEsMV0sWzEsMV1dKQogICAgICBzLmFkZChtZXNoKG5hbWUrJ19mdXNzJywgbmV3IFRIUkVFLkN5bGluZGVyR2VvbWV0cnkoMC4wMDYsMC4wMDYsMC4wMDIsMjApLCBNLmJsYWNrLCBmeCooVy8yLTAuMDIpLCAwLjAwMSwgZnoqKEQvMi0wLjAyNSkpKTsKICAgIGlmIChhY3RpdmUpewogICAgICBjb25zdCBweCA9IFcvMiArIDAuMDAwNCwgY3ogPSAtMC4wMTI7CiAgICAgIHMuYWRkKG1lc2gobmFtZSsnX2JlZGllbmZlbGQnLCBuZXcgVEhSRUUuQm94R2VvbWV0cnkoMC4wMDIsIDAuMTA4LCAwLjA1NSksIE0uYmxhY2ssIHB4LCAwLjE0LCBjeikpOwogICAgICBbMC4xNzYsIDAuMTQzLCAwLjExXS5mb3JFYWNoKCh5LCBpKSA9PiB7CiAgICAgICAgY29uc3QgayA9IG1lc2gobmFtZSsnX3JlZ2xlcl8nK1snaG9laGVuJywnYmFzcycsJ2xhdXRzdGFlcmtlJ11baV0sIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KDAuMDA4NSwgMC4wMDg1LCAwLjAxMSwgMjgpLCBNLmtub2IsIHB4KzAuMDA1NSwgeSwgY3otMC4wMDQpOwogICAgICAgIGsucm90YXRpb24ueiA9IE1hdGguUEkvMjsgcy5hZGQoayk7CiAgICAgIH0pOwogICAgICBzLmFkZChtZXNoKG5hbWUrJ19sZWQnLCBuZXcgVEhSRUUuU3BoZXJlR2VvbWV0cnkoMC4wMDE1LCAxMiwgOCksIE0ubGVkLCBweCswLjAwMSwgMC4wOTMsIGN6LTAuMDA0KSk7CiAgICB9CiAgICBzLnRyYXZlcnNlKG8gPT4geyBpZiAoby5pc01lc2gpIG8ucG9zaXRpb24ueSArPSAwLjAwMjsgfSk7CiAgICByZXR1cm4gczsKICB9CiAgcmV0dXJuIHsgVywgRCwgSCwgbGlua3M6IHNwZWFrZXIoJ2JveF9saW5rcycsIGZhbHNlKSwgcmVjaHRzOiBzcGVha2VyKCdib3hfcmVjaHRzX2FrdGl2JywgdHJ1ZSkgfTsKfQoKLy8gVHJpcG9kLUxhbXBlIChhdXMgVHJpcG9kbGFtcGUgM0QpOiBFaWNoZW4tTGV1Y2h0a29wZiBtaXQgTEVELVNwb3QgYXVmIHNjaHdhcnplbSBEcmVpYmVpbjsgRnJvbnQgPSBsb2thbGUgK3oKZXhwb3J0IGZ1bmN0aW9uIGJ1aWxkVHJpcG9kKFRIUkVFKXsKICBjb25zdCBWID0gKHgseSx6KSA9PiBuZXcgVEhSRUUuVmVjdG9yMyh4LHkseik7CiAgY29uc3QgYyA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2NhbnZhcycpOyBjLndpZHRoID0gYy5oZWlnaHQgPSA1MTI7IGNvbnN0IGcgPSBjLmdldENvbnRleHQoJzJkJyk7CiAgZy5maWxsU3R5bGUgPSAnI2M0OWE2Nic7IGcuZmlsbFJlY3QoMCwwLDUxMiw1MTIpOwogIGxldCBzID0gMTE7IGNvbnN0IHIgPSAoKSA9PiAocyA9IChzKjE2ODA3KSAlIDIxNDc0ODM2NDcpIC8gMjE0NzQ4MzY0NzsKICBmb3IgKGxldCBpID0gMDsgaSA8IDI2MDsgaSsrKXsKICAgIGNvbnN0IHkgPSByKCkqNTEyLCBhID0gMC4wNCArIHIoKSowLjEyOyBnLnN0cm9rZVN0eWxlID0gcigpID4gMC41ID8gYHJnYmEoMTIwLDgwLDQwLCR7YX0pYCA6IGByZ2JhKDIzNSwyMDAsMTUwLCR7YX0pYDsgZy5saW5lV2lkdGggPSAwLjUgKyByKCkqMi4yOwogICAgZy5iZWdpblBhdGgoKTsgZy5tb3ZlVG8oMCx5KTsgZm9yIChsZXQgeCA9IDA7IHggPD0gNTEyOyB4ICs9IDMyKSBnLmxpbmVUbyh4LCB5ICsgTWF0aC5zaW4oeCowLjAxMiArIGkpKjQgKyAocigpLTAuNSkqMS41KTsgZy5zdHJva2UoKTsKICB9CiAgY29uc3QgdGV4ID0gbmV3IFRIUkVFLkNhbnZhc1RleHR1cmUoYyk7IHRleC5jb2xvclNwYWNlID0gVEhSRUUuU1JHQkNvbG9yU3BhY2U7CiAgY29uc3QgTSA9IHsKICAgIGhvbHo6ICAgbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTondHJpcG9kX2VpY2hlJywgbWFwOiB0ZXgsIHJvdWdobmVzczowLjYgfSksCiAgICBzdGFobDogIG5ldyBUSFJFRS5NZXNoU3RhbmRhcmRNYXRlcmlhbCh7IG5hbWU6J3RyaXBvZF9zdGFobF9zY2h3YXJ6JywgY29sb3I6MHgxYjFiMWMsIHJvdWdobmVzczowLjQ1LCBtZXRhbG5lc3M6MC42IH0pLAogICAgZ3VtbWk6ICBuZXcgVEhSRUUuTWVzaFN0YW5kYXJkTWF0ZXJpYWwoeyBuYW1lOid0cmlwb2RfZ3VtbWknLCBjb2xvcjoweDExMTExMSwgcm91Z2huZXNzOjAuOSB9KSwKICAgIGJsZW5kZTogbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTondHJpcG9kX3JlZmxla3RvcicsIGNvbG9yOjB4OWE5YTlhLCByb3VnaG5lc3M6MC4yNSwgbWV0YWxuZXNzOjAuOSB9KSwKICAgIGxlZDogICAgbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTondHJpcG9kX2xlZCcsIGNvbG9yOjB4ZDhkYWRkLCBlbWlzc2l2ZToweGY0ZjdmZiwgZW1pc3NpdmVJbnRlbnNpdHk6MCwgcm91Z2huZXNzOjAuMiB9KSwKICAgIGthYmVsOiAgbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsgbmFtZTondHJpcG9kX2thYmVsJywgY29sb3I6MHgxNTE1MTUsIHJvdWdobmVzczowLjcgfSksCiAgfTsKICBjb25zdCBhZGQgPSAocCwgbiwgZ2VvLCBtYXQsIHBvcz1bMCwwLDBdKSA9PiB7IGNvbnN0IG8gPSBtayhUSFJFRSwgbiwgZ2VvLCBtYXQsIC4uLnBvcyk7IHAuYWRkKG8pOyByZXR1cm4gbzsgfTsKICBjb25zdCByb2QgPSAocCwgbiwgYSwgYiwgcnIsIG1hdCkgPT4geyBjb25zdCBkID0gYi5jbG9uZSgpLnN1YihhKSwgbyA9IGFkZChwLCBuLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeShyciwgcnIsIGQubGVuZ3RoKCksIDIwKSwgbWF0KTsKICAgIG8ucG9zaXRpb24uY29weShhKS5hZGRTY2FsZWRWZWN0b3IoZCwgMC41KTsgby5xdWF0ZXJuaW9uLnNldEZyb21Vbml0VmVjdG9ycyhWKDAsMSwwKSwgZC5ub3JtYWxpemUoKSk7IHJldHVybiBvOyB9OwogIGNvbnN0IGdyb3VwID0gbmV3IFRIUkVFLkdyb3VwKCk7IGdyb3VwLm5hbWUgPSAndHJpcG9kbGFtcGUnOwogIGNvbnN0IEhVQiA9IFYoMCwgMC4zNiwgMCksIEZSID0gMC4xNzsKICBmb3IgKGxldCBpID0gMDsgaSA8IDM7IGkrKyl7CiAgICBjb25zdCBhID0gTWF0aC5QSS8yICsgaSoyKk1hdGguUEkvMywgZm9vdCA9IFYoTWF0aC5jb3MoYSkqRlIsIDAuMDA2LCBNYXRoLnNpbihhKSpGUik7CiAgICByb2QoZ3JvdXAsICd0cmlwb2RfYmVpbl8nKyhpKzEpLCBmb290LCBIVUIuY2xvbmUoKS5hZGQoVihNYXRoLmNvcyhhKSowLjAxMiwgMCwgTWF0aC5zaW4oYSkqMC4wMTIpKSwgMC4wMDUsIE0uc3RhaGwpOwogICAgYWRkKGdyb3VwLCAndHJpcG9kX2Z1c3NfJysoaSsxKSwgbmV3IFRIUkVFLkN5bGluZGVyR2VvbWV0cnkoMC4wMDU4LCAwLjAwNTgsIDAuMDEyLCAxNiksIE0uZ3VtbWksIFtmb290LngsIDAuMDA2LCBmb290LnpdKTsKICB9CiAgYWRkKGdyb3VwLCAndHJpcG9kX2tub3RlbicsIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KDAuMDE2LCAwLjAxOCwgMC4wMjgsIDI4KSwgTS5zdGFobCwgWzAsIDAuMzYsIDBdKTsKICBjb25zdCBIVyA9IDAuMTQsIEhIID0gMC4xMiwgSEQgPSAwLjExOwogIGNvbnN0IGFybSA9IG5ldyBUSFJFRS5DYXRtdWxsUm9tQ3VydmUzKFtWKDAsMC4zNzQsMCksIFYoLTAuMDEsMC40MSwwKSwgVigtMC4wNDUsMC40NSwwLjAwNSksIFYoLTAuMDksMC40ODUsMC4wMSksIFYoLTAuMDkyLDAuNDk1LDAuMDEpLCBWKC1IVy8yLTAuMDA4LDAuNDksMC4wMSldKTsKICBhZGQoZ3JvdXAsICd0cmlwb2RfYnVlZ2VsJywgbmV3IFRIUkVFLlR1YmVHZW9tZXRyeShhcm0sIDQ4LCAwLjAwNTUsIDEyKSwgTS5zdGFobCk7CiAgY29uc3Qga29wZiA9IG5ldyBUSFJFRS5Hcm91cCgpOyBrb3BmLm5hbWUgPSAndHJpcG9kX2tvcGYnOyBrb3BmLnBvc2l0aW9uLnNldCgwLCAwLjQ5LCAwLjAxKTsga29wZi5yb3RhdGlvbi5zZXQoMC4xMiwgMC4xNSwgMCk7IGdyb3VwLmFkZChrb3BmKTsKICBhZGQoa29wZiwgJ3RyaXBvZF9ob2x6a29lcnBlcicsIG5ldyBUSFJFRS5Cb3hHZW9tZXRyeShIVywgSEgsIEhEKSwgTS5ob2x6KTsKICBhZGQoa29wZiwgJ3RyaXBvZF9nZWxlbmsnLCBuZXcgVEhSRUUuQ3lsaW5kZXJHZW9tZXRyeSgwLjAxMywgMC4wMTMsIDAuMDEsIDI4KSwgTS5zdGFobCwgWy1IVy8yLTAuMDA1LCAwLCAwXSkucm90YXRpb24ueiA9IE1hdGguUEkvMjsKICBhZGQoa29wZiwgJ3RyaXBvZF9ibGVuZGUnLCBuZXcgVEhSRUUuVG9ydXNHZW9tZXRyeSgwLjAzNiwgMC4wMDUsIDEyLCA0OCksIE0uYmxlbmRlLCBbMC4wMSwgMCwgSEQvMiswLjAwMV0pOwogIGFkZChrb3BmLCAndHJpcG9kX3JlZmxla3RvcicsIG5ldyBUSFJFRS5DeWxpbmRlckdlb21ldHJ5KDAuMDM2LCAwLjAzLCAwLjAxMiwgNDgsIDEsIHRydWUpLCBNLmJsZW5kZSwgWzAuMDEsIDAsIEhELzItMC4wMDVdKS5yb3RhdGlvbi54ID0gTWF0aC5QSS8yOwogIGNvbnN0IGxpbnNlID0gYWRkKGtvcGYsICd0cmlwb2RfbGVkX2xpbnNlJywgbmV3IFRIUkVFLkNpcmNsZUdlb21ldHJ5KDAuMDM0LCA0OCksIE0ubGVkLCBbMC4wMSwgMCwgSEQvMi0wLjAwOF0pOwogIGNvbnN0IGthYmVsID0gbmV3IFRIUkVFLkNhdG11bGxSb21DdXJ2ZTMoW1YoLTAuMDA1LDAuMzUsLTAuMDEyKSwgVigtMC4wMywwLjI4LC0wLjA0KSwgVigtMC4wNywwLjE2LC0wLjA4KSwgVigtMC4xMSwwLjA1LC0wLjEyKSwgVigtMC4xNCwwLjAwNCwtMC4xNiksIFYoLTAuMjgsMC4wMDQsLTAuMjQpLCBWKC0wLjQ1LDAuMDA0LC0wLjIpXSk7CiAgYWRkKGdyb3VwLCAndHJpcG9kX2thYmVsJywgbmV3IFRIUkVFLlR1YmVHZW9tZXRyeShrYWJlbCwgNjQsIDAuMDAyMiwgOCksIE0ua2FiZWwpOwogIGdyb3VwLnRyYXZlcnNlKG8gPT4geyBpZiAoby5pc01lc2gpeyBvLmNhc3RTaGFkb3cgPSB0cnVlOyBvLnJlY2VpdmVTaGFkb3cgPSB0cnVlOyB9IH0pOwogIGxpbnNlLmNhc3RTaGFkb3cgPSBmYWxzZTsKICAvLyBMaWNodHF1ZWxsZW4gYW0gS29wZiAoSW50ZW5zaXTDpHQgMCA9IGF1cykKICBjb25zdCBzcG90ID0gbmV3IFRIUkVFLlNwb3RMaWdodCgweGYyZjVmZiwgMCwgMCwgMC41NSwgMC41LCAyKTsKICBzcG90LnBvc2l0aW9uLnNldCgwLjAxLCAwLCBIRC8yICsgMC4wMDUpOyBzcG90LnRhcmdldC5wb3NpdGlvbi5zZXQoMC4wMSwgMCwgSEQvMiArIDEpOwogIHNwb3QuY2FzdFNoYWRvdyA9IHRydWU7IHNwb3Quc2hhZG93Lm1hcFNpemUuc2V0KDEwMjQsIDEwMjQpOyBzcG90LnNoYWRvdy5iaWFzID0gLTAuMDAwNTsgc3BvdC5zaGFkb3cubm9ybWFsQmlhcyA9IDAuMDI7CiAgc3BvdC5zaGFkb3cuY2FtZXJhLm5lYXIgPSAwLjAzOyBzcG90LnNoYWRvdy5jYW1lcmEuZmFyID0gODsKICBjb25zdCBnbG93ID0gbmV3IFRIUkVFLlBvaW50TGlnaHQoMHhmMmY1ZmYsIDAsIDMsIDIpOyBnbG93LnBvc2l0aW9uLnNldCgwLjAxLCAwLCBIRC8yICsgMC4wNSk7CiAga29wZi5hZGQoc3BvdCwgc3BvdC50YXJnZXQsIGdsb3cpOwogIHJldHVybiB7IGdyb3VwLCBrb3BmLCBNLCBzcG90LCBnbG93IH07Cn0K';\nconst stage = document.querySelector('three-d-stage');\nconst { THREE } = await stage.ready;\nconst statusEl = { set textContent(v){} };\nfor (const sel of ['.toolbar','.note']) { const el = stage.shadowRoot.querySelector(sel); if (el) el.style.display = 'none'; }\n\n// ---------- Maße: Grundriss in Bild-Pixeln → Meter ----------\nconst S = 0.009; // 1 px ≈ 0.9 cm\nconst OX = 610, OZ = 725; // Ursprung ~ Mitte\nconst mx = px => (px - OX) * S, mz = py => (py - OZ) * S;\nconst H = 2.5, TE = 0.2, TI = 0.1; // Wandhöhe, Außen-/Innenwand\nconst DOOR_W = 0.9, DOOR_H = 2.1, WIN_B = 0.9, WIN_T = 2.2;\n\n// ---------- Materialien ----------\nconst M = {\n floor: new THREE.MeshStandardMaterial({ name:'rubber_tiles', color:0x8a8a86, roughness:0.9 }),\n wall: new THREE.MeshStandardMaterial({ name:'white_stucco', color:0xf2efe9, roughness:0.95 }),\n brick: new THREE.MeshStandardMaterial({ name:'brick_wall_04', color:0x9a5a44, roughness:0.9 }),\n cap: new THREE.MeshStandardMaterial({ name:'rough_plaster_brick_04', color:0xb8a898, roughness:0.95 }),\n plinth: new THREE.MeshStandardMaterial({ name:'plinth', color:0x2f2d2a, roughness:0.7 }),\n wood: new THREE.MeshStandardMaterial({ name:'door_white_lacquer', color:0xf7f5f0, roughness:0.35 }),\n metal: new THREE.MeshStandardMaterial({ name:'brushed_steel', color:0xb9bcc0, roughness:0.3, metalness:0.35 }),\n glass: new THREE.MeshStandardMaterial({ name:'glass', color:0xbfd8e4, roughness:0.05, transparent:true, opacity:0.35 }),\n balcony:new THREE.MeshStandardMaterial({ name:'balcony_concrete', color:0xa9a59d, roughness:0.9 }),\n};\n\n// Welt-UVs (Box-Projektion), damit Texturen nicht gestreckt werden\nfunction worldUV(geo, scale){\n const p = geo.attributes.position, n = geo.attributes.normal, uv = geo.attributes.uv;\n for (let i = 0; i < p.count; i++){\n const ax = Math.abs(n.getX(i)), ay = Math.abs(n.getY(i)), az = Math.abs(n.getZ(i));\n let u, v;\n if (ay >= ax && ay >= az){ u = p.getX(i); v = p.getZ(i); }\n else if (ax >= az){ u = p.getZ(i); v = p.getY(i); }\n else { u = p.getX(i); v = p.getY(i); }\n uv.setXY(i, u/scale, v/scale);\n }\n uv.needsUpdate = true;\n return geo;\n}\n// Box in Weltkoordinaten (min/max)\nfunction box(name, mat, x0,y0,z0, x1,y1,z1, uvScale){\n const g = new THREE.BoxGeometry(x1-x0, y1-y0, z1-z0);\n g.translate((x0+x1)/2, (y0+y1)/2, (z0+z1)/2);\n if (uvScale) worldUV(g, uvScale);\n const m = new THREE.Mesh(g, mat); m.name = name; return m;\n}\n\nconst home = new THREE.Group(); home.name = 'wohnung';\nconst walls = new THREE.Group(); walls.name = 'waende';\nconst doors = new THREE.Group(); doors.name = 'tueren';\nconst windows = new THREE.Group(); windows.name = 'fenster';\nhome.add(walls, doors, windows);\n\n// ---------- Tür (prozedural, Kassettentür) ----------\nfunction makeDoor(name, t, openDeg, glassDoor=false){\n const g = new THREE.Group(); g.name = name;\n const w = DOOR_W, d = t + 0.03;\n g.add(box(name+'_zarge_l', M.wood, -w/2, 0, -d/2, -w/2+0.04, DOOR_H, d/2));\n g.add(box(name+'_zarge_r', M.wood, w/2-0.04, 0, -d/2, w/2, DOOR_H, d/2));\n g.add(box(name+'_zarge_oben', M.wood, -w/2, DOOR_H-0.04, -d/2, w/2, DOOR_H, d/2));\n const lw = w - 0.08, lh = DOOR_H - 0.05, lt = 0.04;\n const pivot = new THREE.Group(); pivot.name = name+'_blatt';\n pivot.position.set(-w/2 + 0.04, 0.005, 0);\n pivot.rotation.y = THREE.MathUtils.degToRad(openDeg);\n pivot.add(box(name+'_tuerblatt', M.wood, 0, 0, -lt/2, lw, lh, lt/2));\n for (const s of [1,-1]){\n const z0 = s*0.014, z1 = s*0.026, zz=[Math.min(z0,z1),Math.max(z0,z1)];\n if (glassDoor){\n pivot.add(box(name+'_glas', M.glass, 0.1, 0.35, -0.006, lw-0.1, lh-0.12, 0.006));\n } else {\n pivot.add(box(name+'_kassette_o'+s, M.wood, 0.11, 1.05, zz[0], lw-0.11, lh-0.14, zz[1]));\n pivot.add(box(name+'_kassette_u'+s, M.wood, 0.11, 0.14, zz[0], lw-0.11, 0.9, zz[1]));\n }\n const rose = new THREE.Mesh(new THREE.CylinderGeometry(0.026,0.026,0.012,32), M.metal);\n rose.name = name+'_rosette'+s; rose.rotation.x = Math.PI/2; rose.position.set(lw-0.07, 1.05, s*(lt/2+0.006)); pivot.add(rose);\n const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.009,0.009,0.05,20), M.metal);\n neck.name = name+'_hals'+s; neck.rotation.x = Math.PI/2; neck.position.set(lw-0.07, 1.05, s*(lt/2+0.03)); pivot.add(neck);\n const lever = new THREE.Mesh(new THREE.CapsuleGeometry(0.009, 0.12, 6, 16), M.metal);\n lever.name = name+'_druecker'+s; lever.rotation.z = Math.PI/2; lever.position.set(lw-0.13, 1.05, s*(lt/2+0.055)); pivot.add(lever);\n }\n for (const y of [0.25, lh-0.25]){\n const h = new THREE.Mesh(new THREE.CylinderGeometry(0.008,0.008,0.1,16), M.metal);\n h.name = name+'_band'; h.position.set(-0.004, y, lt/2); pivot.add(h);\n }\n g.add(pivot);\n return g;\n}\n\n// ---------- Fenster ----------\nfunction makeWindow(name, w, t, bottom, top){\n const g = new THREE.Group(); g.name = name;\n const f = 0.05, d = 0.07;\n g.add(box(name+'_rahmen_l', M.wood, -w/2, bottom, -d/2, -w/2+f, top, d/2));\n g.add(box(name+'_rahmen_r', M.wood, w/2-f, bottom, -d/2, w/2, top, d/2));\n g.add(box(name+'_rahmen_o', M.wood, -w/2+f, top-f, -d/2, w/2-f, top, d/2));\n g.add(box(name+'_rahmen_u', M.wood, -w/2+f, bottom, -d/2, w/2-f, bottom+f, d/2));\n if (w > 1.1) g.add(box(name+'_pfosten', M.wood, -0.025, bottom+f, -d/2+0.005, 0.025, top-f, d/2-0.005));\n g.add(box(name+'_glas', M.glass, -w/2+f, bottom+f, -0.005, w/2-f, top-f, 0.005));\n if (bottom > 0) g.add(box(name+'_bank', M.wood, -w/2-0.05, bottom-0.027, -t/2-0.04, w/2+0.05, bottom+0.012, t/2+0.04));\n return g;\n}\n\n// ---------- Wand mit Öffnungen (achsparallel) ----------\n// ops: {c: Mittelpunkt in px, w: Breite m, type:'door'|'window'|'glassdoor', open, name}\n// outside: Box-Face-Index der Außenseite (0:+x 1:-x 4:+z 5:-z) → Klinker außen, Putz innen\nfunction wall(name, a, b, fixed, horizontal, t, h, ops=[], ext=0, outside=-1){\n const A = (horizontal ? mx(a) : mz(a)) - ext, B = (horizontal ? mx(b) : mz(b)) + ext;\n const F = horizontal ? mz(fixed) : mx(fixed);\n const seg = (n, s0, s1, y0, y1, ends=[]) => {\n if (s1 - s0 < 0.001) return;\n const mat = Array(6).fill(M.wall);\n if (outside >= 0){ mat[outside] = M.brick; ends.forEach(e => mat[e] = M.brick); }\n if (y1 >= h - 0.0001) mat[2] = M.cap; // Schnittfläche oben\n walls.add(horizontal ? box(n, mat, s0, y0, F-t/2, s1, y1, F+t/2, 1.5)\n : box(n, mat, F-t/2, y0, s0, F+t/2, y1, s1, 1.5));\n };\n const endA = ext > 0 ? [horizontal ? 1 : 5] : [], endB = ext > 0 ? [horizontal ? 0 : 4] : [];\n const list = ops.map(o => ({...o, c: horizontal ? mx(o.c) : mz(o.c)})).sort((p,q)=>p.c-q.c);\n let cur = A;\n list.forEach((o, i) => {\n const s0 = o.c - o.w/2, s1 = o.c + o.w/2;\n seg(`${name}_${i}`, cur, s0, 0, h, i === 0 ? endA : []);\n const top = o.type === 'window' ? WIN_T : (o.type === 'glassdoor' ? WIN_T : DOOR_H);\n seg(`${name}_sturz_${i}`, s0, s1, top, h);\n if (o.type === 'window') seg(`${name}_bruestung_${i}`, s0, s1, 0, WIN_B);\n let el;\n if (o.type === 'door') { el = makeDoor(o.name, t, o.open ?? 0); doors.add(el); }\n else if (o.type === 'glassdoor') { el = makeWindow(o.name, o.w, t, 0, WIN_T); windows.add(el); }\n else { el = makeWindow(o.name, o.w, t, WIN_B, WIN_T); windows.add(el); }\n if (horizontal) el.position.set(o.c, 0, F);\n else { el.position.set(F, 0, o.c); el.rotation.y = Math.PI/2; }\n if (o.flip) el.rotation.y += Math.PI;\n cur = s1;\n });\n seg(`${name}_${list.length}`, cur, B, 0, h, list.length === 0 ? [...endA, ...endB] : endB);\n}\n\n// Grundriss-Koordinaten (px): außen x 140–1080, y 230–1225; Mittelwand x 610\nconst L=140, R=1080, T=230, Bt=1225, MID=610, BAD=690, FLUR=815;\nconst HI = H - 0.003; // Innenwände minimal niedriger → kein Z-Fighting auf den Oberkanten\n\n// Außenwände\nwall('aussen_nord', L, R, T, true, TE, H, [\n { c: 415, w: 1.4, type:'window', name:'fenster_bad' },\n { c: 800, w: 1.0, type:'glassdoor', name:'balkontuer' },\n { c: 960, w: 1.0, type:'window', name:'fenster_kueche_nord' },\n], TE/2, 5);\nwall('aussen_sued', L, R, Bt, true, TE, H, [\n { c: 375, w: 1.5, type:'window', name:'fenster_wohnzimmer' },\n { c: 845, w: 1.5, type:'window', name:'fenster_schlafzimmer' },\n], TE/2, 4);\nwall('aussen_west', T, Bt, L, false, TE, H, [\n { c: 752, w: DOOR_W, type:'door', open: -35, name:'wohnungstuer' },\n], 0, 1);\nwall('aussen_ost', T, Bt, R, false, TE, H, [\n { c: 530, w: 1.3, type:'window', name:'fenster_kueche' },\n { c: 1020, w: 1.3, type:'window', name:'fenster_schlafzimmer_ost' },\n], 0, 0);\n// Innenwände\nwall('innen_mitte', T, Bt, MID, false, TI, HI, [\n { c: 752, w: DOOR_W, type:'door', open: 70, name:'tuer_kueche' },\n], -TE/2 + 0.0);\nwall('innen_bad_flur', L, MID, BAD, true, TI, HI, [\n { c: 245, w: DOOR_W, type:'door', open: -60, flip:true, name:'tuer_bad' },\n]);\nwall('innen_flur_wohnen', L, MID, FLUR, true, TI, HI, [\n { c: 245, w: DOOR_W, type:'door', open: 55, name:'tuer_wohnzimmer' },\n]);\nwall('innen_kueche_schlafen', MID, R, FLUR, true, TI, HI, [\n { c: 830, w: DOOR_W, type:'door', open: 65, name:'tuer_schlafzimmer' },\n]);\n\n// ---------- Boden, Sockel, Balkon ----------\nconst BL=670, BR=1030, BT=150;\nconst floorGeo = new THREE.PlaneGeometry(mx(R)-mx(L), mz(Bt)-mz(T));\nfloorGeo.rotateX(-Math.PI/2);\nfloorGeo.translate((mx(L)+mx(R))/2, 0.002, (mz(T)+mz(Bt))/2);\nworldUV(floorGeo, 1.0);\nconst floor = new THREE.Mesh(floorGeo, M.floor); floor.name = 'boden_rubber_tiles';\nhome.add(floor);\nhome.add(box('sockel', M.plinth, mx(L)-TE/2-0.05, -0.12, mz(T)-TE/2-0.05, mx(R)+TE/2+0.05, 0, mz(Bt)+TE/2+0.05));\nhome.add(box('balkon_platte', M.balcony, mx(BL), -0.12, mz(BT), mx(BR), 0.02, mz(T)-TE/2));\nconst PH = 1.0, PT = 0.08;\nhome.add(box('balkon_bruestung_w', M.wall, mx(BL), 0.02, mz(BT), mx(BL)+PT, PH, mz(T)-TE/2, 1.5));\nhome.add(box('balkon_bruestung_o', M.wall, mx(BR)-PT, 0.02, mz(BT), mx(BR), PH, mz(T)-TE/2, 1.5));\nhome.add(box('balkon_bruestung_n', M.wall, mx(BL)+PT, 0.02, mz(BT), mx(BR)-PT, PH-0.002, mz(BT)+PT, 1.5));\n\n// ---------- Wohnzimmer (Raum Süd-Ost) ----------\nconst wohn = new THREE.Group(); wohn.name = 'wohnzimmer';\nconst IN_O = mx(R) - TE/2, IN_S = mz(Bt) - TE/2; // Innenkanten Ost-/Südwand\n// Sofa unter dem Ostfenster (links beim Betreten), Rücken zur Wand\nconst sofa = buildSofa(THREE);\nsofa.group.position.set(IN_O - sofa.D/2 - 0.03, 0.002, mz(1020));\nsofa.group.rotation.y = -Math.PI/2;\nwohn.add(sofa.group);\n// 3 Regale nebeneinander unter dem Südfenster, Front zum Raum\nconst RM = regalMaterials(THREE);\nfor (const [i, dx] of [[1,-0.603],[2,0],[3,0.603]]){\n const r = buildRegal(THREE, RM); r.name = 'regal_'+i;\n r.position.set(mx(845) + dx, 0.002, IN_S - 0.135 - 0.02);\n r.rotation.y = Math.PI;\n wohn.add(r);\n}\n// Boxen oben auf den äußeren Regalen, leicht nach innen gedreht\n{\n const bx = buildBoxen(THREE), top = new THREE.Box3().setFromObject(wohn.getObjectByName('regal_1')).max.y;\n const TURN = THREE.MathUtils.degToRad(15), X = 0.603 + 0.30 - 0.105, Z = IN_S - 0.135 - 0.02;\n // Aktivbox links (–x), Bedienfeld zeigt nach außen\n bx.rechts.position.set(mx(845) - X, top, Z); bx.rechts.rotation.y = Math.PI - TURN;\n bx.links.position.set(mx(845) + X, top, Z); bx.links.rotation.y = Math.PI + TURN;\n wohn.add(bx.links, bx.rechts);\n}\n// Bogenlampe in der Fensterecke, Schirm über dem Sofa – zur Raummitte versetzt\nconst lampe = buildLampe(THREE);\nconst LB = new THREE.Vector2(IN_O - 0.22, IN_S - 0.25);\nconst LT = new THREE.Vector2(IN_O - 0.95, mz(1020) + 0.25);\nlampe.group.position.set(LB.x, 0.002, LB.y);\nlampe.group.rotation.y = Math.atan2(-(LT.y - LB.y), LT.x - LB.x);\nwohn.add(lampe.group);\n// Donut-Lampe (Ø 30 cm, H 11 cm) im linken Regal, mittleres Fach, mittig\nconst DM = {\n koerper: new THREE.MeshPhysicalMaterial({ name:'donut_opal_orange', color:0xffa04a, roughness:0.6, clearcoat:0.2, clearcoatRoughness:0.5, emissive:0xff8a2a, emissiveIntensity:0 }),\n sockel: new THREE.MeshStandardMaterial({ name:'donut_sockel', color:0xee6a3e, roughness:0.5, emissive:0xd04a20, emissiveIntensity:0 }),\n knopf: new THREE.MeshStandardMaterial({ name:'donut_knopf', color:0x9a6a58, roughness:0.45 }),\n};\nconst donut = new THREE.Group(); donut.name = 'donutlampe';\n{\n const V2 = (x,y) => new THREE.Vector2(x,y), dm = (n,g,m,y=0) => { const o = new THREE.Mesh(g,m); o.name = n; o.position.y = y; donut.add(o); };\n const SB_R = 0.12, SB_H = 0.018, sp = [V2(0,0), V2(SB_R-0.006,0)];\n for (let i = 0; i <= 12; i++){ const a = -Math.PI/2 + i/12*Math.PI; sp.push(V2(SB_R-0.006+Math.cos(a)*0.006, 0.006+Math.sin(a)*0.006+(i>6?(SB_H-0.012)*(i-6)/6:0))); }\n sp.push(V2(0, SB_H));\n dm('donut_sockel', new THREE.LatheGeometry(sp, 96), DM.sockel);\n const R = 0.085, A = 0.065, B = 0.0475, CY = 0.0625, sg = v => Math.sign(v)*Math.pow(Math.abs(v), 0.8), kp = [V2(0,0.015)];\n for (let i = 0; i <= 60; i++){ const t = -Math.PI/2 + i/60*1.35*Math.PI; kp.push(V2(R+A*sg(Math.cos(t)), CY+B*sg(Math.sin(t)))); }\n kp.push(V2(0.018,0.078), V2(0.008,0.0755), V2(0,0.075));\n dm('donut_koerper', new THREE.LatheGeometry(kp, 128), DM.koerper);\n dm('donut_knopf', new THREE.CylinderGeometry(0.02, 0.022, 0.004, 40), DM.knopf, 0.077);\n}\ndonut.position.set(mx(845) - 0.603, 0.002 + 0.42, IN_S - 0.135 - 0.02);\nwohn.add(donut);\nwohn.traverse(o => { if (o.isMesh){ o.castShadow = true; o.receiveShadow = true; } });\nhome.add(wohn);\n// ---------- Schlafzimmer: Tripod-Lampe auf dem Boden, Südwest-Ecke, Spot zur Raummitte ----------\nconst trip = buildTripod(THREE);\n{\n const IN_W = mx(L) + TE/2, px = IN_W + 0.35, pz = IN_S - 0.35;\n const cx = (mx(L) + mx(MID))/2, cz = (mz(FLUR) + mz(Bt))/2;\n trip.group.position.set(px, 0.002, pz);\n trip.group.rotation.y = Math.atan2(cx - px, cz - pz);\n home.add(trip.group);\n}\n\nstage.setObject(home);\n\n// ---------- Schatten: enger Frustum, 4096er Map, harter PCF → scharf & ohne Flackern ----------\nconst renderer = stage._renderer, scene = stage._scene, key = stage._key;\nrenderer.shadowMap.type = THREE.PCFShadowMap;\nconst hb = new THREE.Box3().setFromObject(home), hc = hb.getCenter(new THREE.Vector3()), hs = hb.getSize(new THREE.Vector3());\nconst sunDir = new THREE.Vector3(4, 7, 5).normalize();\nkey.position.copy(hc).addScaledVector(sunDir, 30);\nkey.target.position.copy(hc); scene.add(key.target);\nconst half = Math.max(hs.x, hs.z) * 0.62;\nObject.assign(key.shadow.camera, { left:-half, right:half, top:half, bottom:-half, near:1, far:60 });\nkey.shadow.camera.updateProjectionMatrix();\nkey.shadow.mapSize.set(2048, 2048);\nkey.shadow.bias = -0.0004; key.shadow.normalBias = 0.015;\nif (key.shadow.map){ key.shadow.map.dispose(); key.shadow.map = null; }\nrenderer.toneMapping = THREE.ACESFilmicToneMapping;\nif (stage._ground) stage._ground.visible = false;\nconst hemi = scene.children.find(o => o.isHemisphereLight), hemiBase = hemi ? hemi.intensity : 0;\nconst fillL = scene.children.find(o => o.isDirectionalLight && o !== key), fillBase = fillL ? fillL.intensity : 0;\nconst ENV = 0.9;\nlet envK = 1, hemiK = 1, fillK = 1, expK = 1;\n// Innenraum: Himmels-/Fülllicht dringt nicht durch die Decke → nur noch Sonne + Umgebung durch die Fenster\nfunction applyInterior(o){\n if (hemi) hemi.intensity = hemiBase * hemiK * (1 - 0.85*o);\n if (fillL) fillL.intensity = fillBase * fillK * (1 - o);\n scene.environmentIntensity = ENV * envK * (1 - 0.7*o);\n renderer.toneMappingExposure = (1 + 0.45*o) * expK;\n}\n\n// ---------- Decke (blendet ein, sobald man in einem Raum steht) ----------\nconst ceilMat = new THREE.MeshStandardMaterial({ name:'decke', color:0xf4f2ee, roughness:0.95, transparent:true, opacity:0, depthWrite:false });\nconst ceiling = box('decke', ceilMat, mx(L)-TE/2, H, mz(T)-TE/2, mx(R)+TE/2, H+0.04, mz(Bt)+TE/2);\nceiling.receiveShadow = true; ceiling.castShadow = true; ceiling.visible = false;\nhome.add(ceiling);\nfunction setCeil(o){\n ceilMat.opacity = o;\n if (ceiling.visible !== (o > 0.001)){ ceiling.visible = o > 0.001; ptDirty = true; }\n const solid = o >= 0.999;\n if (ceilMat.transparent === solid){ ceilMat.transparent = !solid; ceilMat.depthWrite = solid; ceilMat.needsUpdate = true; }\n if (!ptOn) applyInterior(o);\n}\n\n// ---------- Bogenlampe: Licht an/aus ----------\nhome.updateMatrixWorld(true);\nconst bulb = lampe.group.getObjectByName('leuchtmittel');\nconst bp = bulb.getWorldPosition(new THREE.Vector3());\nconst lampSpot = new THREE.SpotLight(0xffd6a0, 0, 0, 1.0, 0.6, 2);\nlampSpot.position.copy(bp).add(new THREE.Vector3(0, -0.035, 0));\nlampSpot.target.position.set(bp.x, 0, bp.z);\nlampSpot.castShadow = true;\nlampSpot.shadow.mapSize.set(1024, 1024);\nlampSpot.shadow.bias = -0.0005; lampSpot.shadow.normalBias = 0.02;\nlampSpot.shadow.camera.near = 0.05; lampSpot.shadow.camera.far = 8;\nconst lampFill = new THREE.PointLight(0xffd6a0, 0, 7, 2);\nlampFill.position.copy(bp);\nscene.add(lampSpot, lampSpot.target, lampFill);\nconst schirm = lampe.group.getObjectByName('schirm').material;\nschirm.emissive = new THREE.Color(0xffe2b8);\nconst lampBtn = document.getElementById('lamp');\nlet lampOn = false, lampLvl = 0, lampAnim = 0;\nlet rasterEnv = null, ptEnv = null;\nlet tracer = null, ptOn = false, ptDirty = true, inRoom = false, stillSince = 0;\nconst LAMP_KEY = 'wohnung-bogenlampe-farbe';\nlet lampK = 1;\nconst lampSw = document.getElementById('lampSw');\nfunction setLampColor(hex){\n const c = new THREE.Color(hex), s = c.clone().lerp(new THREE.Color(0xffffff), 0.3);\n lampSpot.color.copy(c); lampFill.color.copy(c);\n if (bulb.material.emissive) bulb.material.emissive.copy(c);\n schirm.emissive.copy(s);\n try { localStorage.setItem(LAMP_KEY, hex); } catch(e){}\n document.querySelector('#lamp i').style.cssText = lampOn ? `background:${hex};box-shadow:0 0 10px 3px ${hex}99` : '';\n if (ptOn){ tracer.updateLights(); tracer.updateMaterials(); }\n}\n// ---------- Licht-Picker: Hell (Lichtton + Helligkeit) oder Farbe (Farbkreis) ----------\nconst K_COLD = 8000, K_WARM = 2000;\nfunction kelvinHex(k){\n const t = k/100; let r, g, b;\n if (t <= 66){ r = 255; g = 99.4708025861*Math.log(t) - 161.1195681661; b = t <= 19 ? 0 : 138.5177312231*Math.log(t-10) - 305.0447927307; }\n else { r = 329.698727446*Math.pow(t-60, -0.1332047592); g = 288.1221695283*Math.pow(t-60, -0.0755148492); b = 255; }\n const c = v => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0');\n return '#' + c(r) + c(g) + c(b);\n}\nconst warmK = w => Math.round(K_COLD - w/100*(K_COLD - K_WARM));\nfunction hsvRGB(h, s, v){ const f = n => { const k = (n + h/60) % 6; return v - v*s*Math.max(0, Math.min(k, 4-k, 1)); }; return [f(5), f(3), f(1)]; }\nconst rgbHex = a => '#' + a.map(x => Math.round(x*255).toString(16).padStart(2, '0')).join('');\nfunction makePicker(sw, key, def, dimIn, onChange){\n let st = { mode: 'white', warm: 70, h: 30, s: 0.6, ...def };\n try { Object.assign(st, JSON.parse(localStorage.getItem(key) || 'null') || {}); } catch(e){}\n const pop = document.createElement('div'); pop.className = 'lpick';\n pop.innerHTML = '<div class=\"lp-seg\"><button data-m=\"white\">Hell<\/button><button data-m=\"color\">Farbe<\/button><\/div>'\n + '<div class=\"lp-row lp-w\"><div><span>Lichtton<\/span><output class=\"lp-kv\"><\/output><\/div><input class=\"lp-temp\" type=\"range\" min=\"0\" max=\"100\" step=\"1\"><small><span>Steril<\/span><span>Warm<\/span><\/small><\/div>'\n + '<div class=\"lp-c\"><div class=\"lp-wheel\"><canvas width=\"360\" height=\"360\"><\/canvas><i><\/i><\/div><\/div>'\n + '<div class=\"lp-row\"><div><span>Helligkeit<\/span><output class=\"lp-bv\"><\/output><\/div><input class=\"lp-dim\" type=\"range\" min=\"10\" max=\"200\" step=\"5\"><\/div><button class=\"lp-done\">Fertig<\/button>';\n document.body.appendChild(pop);\n const $ = s => pop.querySelector(s), temp = $('.lp-temp'), bri = $('.lp-dim'), bv = $('.lp-bv'), dot = $('.lp-wheel i'), wheel = $('.lp-wheel');\n temp.style.background = `linear-gradient(90deg,${kelvinHex(K_COLD)},${kelvinHex(4500)},${kelvinHex(3000)},${kelvinHex(K_WARM)})`;\n const cv = $('canvas'), ctx = cv.getContext('2d'), img = ctx.createImageData(360, 360), R = 180;\n for (let y = 0; y < 360; y++) for (let x = 0; x < 360; x++){\n const dx = x + .5 - R, dy = y + .5 - R, d = Math.hypot(dx, dy) / R, i = (y*360 + x)*4;\n const rgb = hsvRGB((Math.atan2(dy, dx)*180/Math.PI + 360) % 360, 1, 1);\n img.data[i] = rgb[0]*255; img.data[i+1] = rgb[1]*255; img.data[i+2] = rgb[2]*255;\n img.data[i+3] = 255 * Math.max(0, Math.min(1, (1 - d) * R, (d - 0.72) * R));\n }\n ctx.putImageData(img, 0, 0);\n const hex = () => st.mode === 'white' ? kelvinHex(warmK(st.warm)) : rgbHex(hsvRGB(st.h, 1, 1));\n function update(fire = true){\n pop.querySelectorAll('.lp-seg button').forEach(b => b.classList.toggle('on', b.dataset.m === st.mode));\n $('.lp-w').style.display = st.mode === 'white' ? '' : 'none';\n $('.lp-c').style.display = st.mode === 'color' ? '' : 'none';\n temp.value = st.warm; $('.lp-kv').textContent = warmK(st.warm) + ' K';\n const a = st.h*Math.PI/180;\n dot.style.left = (90 + Math.cos(a)*77) + 'px'; dot.style.top = (90 + Math.sin(a)*77) + 'px';\n const h = hex(); dot.style.background = h; sw.style.background = h;\n try { localStorage.setItem(key, JSON.stringify(st)); } catch(e){}\n if (fire) onChange(h);\n }\n pop.querySelectorAll('.lp-seg button').forEach(b => b.onclick = () => { st.mode = b.dataset.m; update(); });\n temp.oninput = () => { st.warm = +temp.value; update(); };\n const pick = e => { const r = wheel.getBoundingClientRect(), dx = e.clientX - r.left - 90, dy = e.clientY - r.top - 90;\n st.h = (Math.atan2(dy, dx)*180/Math.PI + 360) % 360; update(); };\n wheel.onpointerdown = e => { const r = wheel.getBoundingClientRect(), d = Math.hypot(e.clientX - r.left - 90, e.clientY - r.top - 90);\n if (d < 58 || d > 94) return; wheel.setPointerCapture(e.pointerId); pick(e); };\n wheel.onpointermove = e => { if (wheel.hasPointerCapture(e.pointerId)) pick(e); };\n const syncBri = () => { bri.value = dimIn.value; bv.textContent = dimIn.value + '%'; };\n bri.oninput = () => { dimIn.value = bri.value; dimIn.oninput(); syncBri(); };\n dimIn.addEventListener('input', syncBri);\n const place = () => { pop.style.left = '18px'; pop.style.top = '50%'; pop.style.transform = 'translateY(-50%)'; };\n sw.onclick = e => { e.preventDefault(); e.stopPropagation(); const o = !pop.classList.contains('open');\n document.querySelectorAll('.lpick.open').forEach(p => p.classList.remove('open'));\n if (o){ syncBri(); place(); pop.classList.add('open'); }\n document.body.classList.toggle('lpfocus', o);\n dispatchEvent(new CustomEvent('lpick', { detail: { key, open: o } })); };\n $('.lp-done').onclick = () => { pop.classList.remove('open'); document.body.classList.remove('lpfocus'); dispatchEvent(new CustomEvent('lpick', { detail: { key, open: false } })); };\n document.addEventListener('pointerdown', e => { if (pop.classList.contains('open') && !pop.contains(e.target) && !sw.contains(e.target)){ pop.classList.remove('open'); document.body.classList.remove('lpfocus'); dispatchEvent(new CustomEvent('lpick', { detail: { key, open: false } })); } });\n addEventListener('resize', () => pop.classList.contains('open') && place());\n update();\n return pop;\n}\n\nlet lampCol = localStorage.getItem(LAMP_KEY) || '#ffd6a0';\nlampSw.innerHTML = '';\nconst colIn = document.createElement('button'); colIn.className = 'lp-sw'; colIn.title = 'Licht einstellen';\nlampSw.appendChild(colIn);\nconst dimIn = document.querySelector('#lampDim input'), dimOut = document.querySelector('#lampDim output');\ndimIn.oninput = () => { lampK = dimIn.value / 100; dimOut.textContent = dimIn.value + '%'; applyLamp(lampLvl); if (ptOn){ tracer.updateLights(); tracer.updateMaterials(); } };\nfunction applyLamp(v){\n v *= lampK;\n lampSpot.intensity = 9 * v;\n lampFill.intensity = 2.2 * v;\n bulb.material.emissiveIntensity = 0.2 + 5 * v;\n schirm.emissiveIntensity = 0.55 * v;\n}\napplyLamp(0);\nmakePicker(colIn, LAMP_KEY + '-cfg', { mode: 'white', warm: 75 }, dimIn, h => { lampCol = h; setLampColor(h); });\nsetLampColor(lampCol);\ndocument.getElementById('lampT').onclick = () => {\n lampOn = !lampOn;\n lampBtn.classList.toggle('on', lampOn);\n lampBtn.querySelector('span').textContent = lampOn ? 'Bogenlampe aus' : 'Bogenlampe an';\n setLampColor(lampCol);\n const id = ++lampAnim, from = lampLvl, to = lampOn ? 1 : 0, t0 = performance.now();\n (function step(now){\n if (id !== lampAnim) return;\n const t = Math.min(1, (now - t0) / 450);\n lampLvl = from + (to - from) * (1 - Math.pow(1 - t, 3));\n applyLamp(lampLvl);\n if (ptOn){ tracer.updateLights(); tracer.updateMaterials(); }\n if (t < 1) requestAnimationFrame(step);\n })(t0);\n};\n\n// ---------- Donut-Lampe: nur an/aus ----------\nconst donutLight = new THREE.PointLight(0xffa050, 0, 4.5, 2);\ndonutLight.position.copy(donut.getWorldPosition(new THREE.Vector3())).add(new THREE.Vector3(0, 0.13, 0));\nscene.add(donutLight);\nconst donutBtn = document.getElementById('lampD');\ndonutBtn.onclick = () => {\n const on = donutBtn.getAttribute('aria-pressed') !== 'true';\n donutBtn.setAttribute('aria-pressed', on); donutBtn.classList.toggle('on', on);\n donutBtn.querySelector('span').textContent = on ? 'Donut-Lampe aus' : 'Donut-Lampe an';\n DM.koerper.emissiveIntensity = on ? 2.2 : 0;\n DM.sockel.emissiveIntensity = on ? 0.6 : 0;\n donutLight.intensity = on ? 2.6 : 0;\n if (ptOn){ tracer.updateLights(); tracer.updateMaterials(); }\n};\n\n// ---------- Tripod-Lampe: an/aus, Farbe, Stärke, Kegel ----------\nconst tripBox = document.getElementById('lamp2'), tripDot = document.querySelector('#tripT i');\nconst TRIP_KEY = 'wohnung-tripod-farbe';\nconst tripColIn = document.getElementById('tripCol'), tripDimIn = document.getElementById('tripDim'), tripConeIn = document.getElementById('tripCone');\nlet tripOn = false, tripLvl = 0, tripAnim = 0, tripK = 1;\nlet tripCol = localStorage.getItem(TRIP_KEY) || '#f2f5ff';\nconst tripSync = () => { if (ptOn){ tracer.updateLights(); tracer.updateMaterials(); } };\nfunction applyTrip(){\n const v = tripLvl * tripK, c = new THREE.Color(tripCol);\n trip.spot.color.copy(c); trip.glow.color.copy(c); trip.M.led.emissive.copy(c);\n trip.M.led.color.set(tripLvl > 0.01 ? 0xffffff : 0xd8dadd);\n trip.spot.intensity = 12 * v; trip.glow.intensity = 0.35 * v;\n trip.M.led.emissiveIntensity = 4 * v;\n tripDot.style.cssText = tripOn ? `background:${tripCol};box-shadow:0 0 10px 3px ${tripCol}99` : '';\n}\ntripDimIn.oninput = () => { tripK = tripDimIn.value/100; tripDimIn.nextElementSibling.textContent = tripDimIn.value + '%'; applyTrip(); tripSync(); };\nmakePicker(tripColIn, TRIP_KEY + '-cfg', { mode: 'white', warm: 5 }, tripDimIn, h => { tripCol = h; try { localStorage.setItem(TRIP_KEY, h); } catch(e){} applyTrip(); tripSync(); });\ntripConeIn.oninput = () => { trip.spot.angle = THREE.MathUtils.degToRad(tripConeIn.value); tripConeIn.nextElementSibling.textContent = tripConeIn.value + '°'; tripSync(); };\ntripConeIn.oninput();\napplyTrip();\ndocument.getElementById('tripT').onclick = () => {\n tripOn = !tripOn;\n tripBox.classList.toggle('on', tripOn);\n tripBox.querySelector('#tripT span').textContent = tripOn ? 'Tripod-Lampe aus' : 'Tripod-Lampe an';\n const id = ++tripAnim, from = tripLvl, to = tripOn ? 1 : 0, t0 = performance.now();\n (function step(now){\n if (id !== tripAnim) return;\n const t = Math.min(1, (now - t0) / 450);\n tripLvl = from + (to - from) * (1 - Math.pow(1 - t, 3));\n applyTrip(); tripSync();\n if (t < 1) requestAnimationFrame(step);\n })(t0);\n};\n\n// ---------- Pathtracing (three-gpu-pathtracer): echte Lichtbündel mit Mehrfach-Reflexion, sobald die Kamera stillsteht ----------\n\nconst ptLabel = document.getElementById('pt');\nlet renderOn = false;\n// Sparsamer Standard-Renderer: N8AO (Screen-Space Ambient Occlusion) + Rendern nur bei Aktivität\nlet composer = null, aoPass = null, lastAct = performance.now(), lastInput = lastAct;\nconst act = () => { lastAct = lastInput = performance.now(); };\nfor (const ev of ['pointerdown','pointermove','wheel','keydown','input','click','resize','touchstart']) window.addEventListener(ev, act, true);\nPromise.all([import('three/examples/jsm/postprocessing/EffectComposer.js'), import('three/examples/jsm/postprocessing/RenderPass.js'), import('three/examples/jsm/postprocessing/OutputPass.js'), import('n8ao')]).then(([{EffectComposer},{RenderPass},{OutputPass},{N8AOPass}]) => {\n const s = renderer.getSize(new THREE.Vector2());\n composer = new EffectComposer(renderer);\n aoPass = new N8AOPass(scene, cam, s.x, s.y);\n Object.assign(aoPass.configuration, { aoRadius: 0.6, distanceFalloff: 0.5, intensity: 2.2, aoSamples: 12, denoiseSamples: 6, denoiseRadius: 10, halfRes: true, gammaCorrection: false });\n aoPass.setQualityMode && aoPass.setQualityMode('Low');\n composer.addPass(aoPass); composer.addPass(new OutputPass());\n act();\n}).catch(e => console.warn('N8AO nicht verfügbar – einfacher Raster', e));\nconst _cs = new THREE.Vector2();\nconst rtBtn = document.getElementById('rt');\nrtBtn.onclick = () => {\n renderOn = !renderOn;\n rtBtn.classList.toggle('on', renderOn); rtBtn.setAttribute('aria-pressed', renderOn);\n rtBtn.querySelector('span').textContent = renderOn ? 'Render an' : 'Render aus';\n if (!renderOn && ptOn) stopPT();\n stillSince = performance.now();\n};\nnew Promise(() => {}).then(({ WebGLPathTracer }) => {\n tracer = new WebGLPathTracer(renderer);\n tracer.bounces = 4;\n tracer.renderScale = 0.5;\n tracer.dynamicLowRes = false;\n tracer.filterGlossyFactor = 0.5;\n tracer.tiles.set(4, 4);\n tracer.minSamples = 2;\n tracer.fadeDuration = 350;\n tracer.renderDelay = 0;\n}).catch(e => console.warn('Pathtracer nicht verfügbar – Raster-Fallback', e));\nfunction startPT(){\n scene.environment = ptEnv; scene.background = ptEnv;\n scene.environmentIntensity = ENV;\n renderer.toneMappingExposure = 2.4;\n if (ptDirty){ tracer.setScene(scene, cam); ptDirty = false; }\n else { tracer.updateCamera(); tracer.updateMaterials(); tracer.updateLights(); tracer.updateEnvironment(); }\n tracer.reset();\n ptOn = true; ptLabel.classList.add('show');\n}\nfunction stopPT(){ ptOn = false; scene.environment = rasterEnv; scene.background = cubeRT.texture; applyInterior(ceilMat.opacity); ptLabel.classList.remove('show'); }\nconst MAX_SAMPLES = 256; let ptFrame = 0;\nlet lastUser = -1e9;\nconst interrupt = () => { stillSince = lastUser = performance.now(); if (ptOn) stopPT(); };\nfor (const ev of ['pointerdown','wheel','keydown']) window.addEventListener(ev, interrupt, true);\nrenderer.domElement.addEventListener('pointerdown', interrupt, true);\nconst lastM = new THREE.Matrix4(); let lastAspect = 0;\nfunction camMoved(){\n const a = cam.matrixWorld.elements, b = lastM.elements; let d = 0;\n for (let i = 0; i < 16; i++) d += Math.abs(a[i] - b[i]);\n const m = d > 1e-5 || cam.aspect !== lastAspect;\n lastM.copy(cam.matrixWorld); lastAspect = cam.aspect;\n return m;\n}\nconst loop = () => {\n ctl.autoRotate = !inRoom && !lampFocus && ctl.enabled && performance.now() - lastUser > 6000;\n ctl.autoRotateSpeed = 0.4;\n document.body.classList.toggle('idle', performance.now() - lastInput > 8000);\n ctl.update(); cam.updateMatrixWorld();\n const now = performance.now();\n if (camMoved()){ stillSince = now; lastAct = now; if (ptOn) stopPT(); }\n if (tracer && renderOn && !ptOn && now - stillSince > 350 && (ceilMat.opacity >= 0.999 || ceilMat.opacity <= 0.001)){\n try { startPT(); } catch(e){ console.warn('Pathtracer-Fehler: ' + (e && e.message), e); tracer = null; ptOn = false; scene.environment = rasterEnv; scene.background = cubeRT.texture; ptLabel.classList.remove('show'); }\n }\n if (ptOn && tracer.samples >= MAX_SAMPLES){ ptLabel.textContent = 'Raytracing · fertig'; return; }\n if (ptOn){ ptFrame ^= 1; if (ptFrame) tracer.renderSample(); ptLabel.textContent = 'Raytracing · ' + Math.floor(tracer.samples) + ' Samples'; }\n else if (now - lastAct < 1500) {\n if (composer){ renderer.getSize(_cs); if (composer._w !== _cs.x || composer._h !== _cs.y){ composer.setSize(_cs.x, _cs.y); composer._w = _cs.x; composer._h = _cs.y; } composer.render(); }\n else renderer.render(scene, cam);\n }\n};\nstage._loop = loop;\nrenderer.setAnimationLoop(loop);\n// Beim Größenwechsel (Menü-Animation) sofort neu zeichnen → kein heller Blitz\nnew ResizeObserver(() => { const n = performance.now(); lastAct = n; stillSince = n; if (ptOn) stopPT(); loop(); }).observe(stage);\n\n// ---------- Raum-Navigation ----------\nconst cam = stage._camera, ctl = stage._controls;\nlet lampFocus = false;\ncam.position.sub(ctl.target).multiplyScalar(0.6).add(ctl.target);\nconst overview = { pos: cam.position.clone(), tgt: ctl.target.clone() };\nconst EYE = 1.6;\n// Standpunkt in der Raummitte, Blick in Richtung (dx,dz); Target minimal vor der Kamera → Umsehen statt Umkreisen\nconst spot = (x, z, dx, dz, y = EYE) => {\n const d = new THREE.Vector3(dx, -0.12, dz).normalize().multiplyScalar(0.05);\n const pos = new THREE.Vector3(x, y, z);\n return { pos, tgt: pos.clone().add(d), fp: true };\n};\n// Vogelperspektive: senkrecht über dem Raum, leicht geneigt, Decke ausgeblendet – ganzer Raum im Bild\nconst bird = (x0,z0,x1,z1) => {\n const cx = (x0+x1)/2, cz = (z0+z1)/2, w = Math.abs(x1-x0), d = Math.abs(z1-z0);\n const fov = (cam.fov || 50) * Math.PI/180, asp = Math.max(0.35, (stage.clientWidth||1)/(stage.clientHeight||1));\n const need = Math.max(d, w/asp) * 0.5 * 1.25; // halbe Ausdehnung + Rand\n const hgt = need / Math.tan(fov/2) + 1.0;\n return { pos: new THREE.Vector3(cx, hgt, cz + hgt*0.22), tgt: new THREE.Vector3(cx, 0.3, cz), fp: false };\n};\nconst room = (x0,y0,x1,y1, dx, dz) => ({\n fp: spot((mx(x0)+mx(x1))/2, (mz(y0)+mz(y1))/2, dx, dz),\n bird: bird(mx(x0), mz(y0), mx(x1), mz(y1)),\n});\nconst ROOMS = [\n ['Übersicht', overview],\n ['Wohnzimmer', room(MID, FLUR, R, Bt, 1, 0.4)],\n ['Küche', room(MID, T, R, FLUR, 1, -0.3)],\n ['Schlafzimmer', room(L, FLUR, MID, Bt, -0.3, 1)],\n ['Bad', room(L, T, MID, BAD, 0, -1)],\n ['Flur', room(L, BAD, MID, FLUR, -1, 0)],\n ['Balkon', { fp: spot((mx(BL)+mx(BR))/2, (mz(BT)+mz(T))/2, 0, -1), bird: bird(mx(BL), mz(BT), mx(BR), mz(T)) }],\n];\nlet anim = 0;\nfunction flyTo(view){\n const id = ++anim;\n const p0 = cam.position.clone(), t0 = ctl.target.clone();\n const c0 = ceilMat.opacity, c1 = view.fp ? 1 : 0;\n inRoom = !!view.fp;\n const clamp01 = x => Math.max(0, Math.min(1, x));\n const dist = p0.distanceTo(view.pos);\n const dur = Math.min(2200, 1100 + dist*90);\n const lift = Math.min(3, dist*0.25);\n const start = performance.now();\n ctl.enabled = false;\n ctl.enableZoom = ctl.enablePan = !view.fp;\n ctl.rotateSpeed = view.fp ? -0.35 : 1;\n ctl.minDistance = 0;\n const ease = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;\n (function step(now){\n if (id !== anim) return;\n const t = Math.min(1, (now - start)/dur), e = ease(t);\n cam.position.lerpVectors(p0, view.pos, e);\n cam.position.y += Math.sin(Math.PI*e) * lift;\n ctl.target.lerpVectors(t0, view.tgt, e);\n cam.lookAt(ctl.target);\n // Einblenden erst am Ende der Anflugs (Kamera schon unter Deckenhöhe), Ausblenden sofort\n const k = c1 > c0 ? clamp01((e - 0.6) / 0.4) : clamp01(e / 0.3);\n setCeil(c0 + (c1 - c0) * k);\n if (t < 1) requestAnimationFrame(step); else ctl.enabled = true;\n })(start);\n}\nconst bar = document.getElementById('rooms');\nconst povBar = document.getElementById('pov');\nlet curView = overview, povMode = 'bird';\nconst show = () => flyTo(curView.bird ? curView[povMode] : curView);\nROOMS.forEach(([label, view], i) => {\n const b = document.createElement('button');\n b.textContent = label;\n if (i === 0) b.classList.add('on');\n b.onclick = () => {\n bar.querySelectorAll('button').forEach(x => x.classList.toggle('on', x === b));\n lampBtn.classList.toggle('show', label === 'Wohnzimmer');\n tripBox.classList.toggle('show', label === 'Schlafzimmer');\n povBar.classList.toggle('show', !!view.bird);\n curView = view; povMode = 'bird';\n povBar.querySelectorAll('button').forEach(x => x.classList.toggle('on', x.dataset.m === povMode));\n show();\n };\n bar.appendChild(b);\n});\n// Lampe konfigurieren → Kamera fliegt an die Lampe, Drehen stoppt; beim Schließen zurück\nconst LAMP_FOCUS = { [LAMP_KEY + '-cfg']: [bp.clone(), 1], [TRIP_KEY + '-cfg']: [trip.spot.getWorldPosition(new THREE.Vector3()), 3] };\naddEventListener('lpick', e => {\n const f = LAMP_FOCUS[e.detail.key]; if (!f) return;\n if (e.detail.open){\n lampFocus = true;\n const p = f[0], c = ROOMS[f[1]][1].fp.pos, dir = new THREE.Vector3(c.x - p.x, 0, c.z - p.z);\n const dist = Math.min(1.8, dir.length() * 0.85); dir.normalize();\n // Handy: Lampe mittig im oberen Bildteil, Menü-Sheet unten\n const pos = p.clone().addScaledVector(dir, dist * 1.4).add(new THREE.Vector3(0, -0.3, 0));\n flyTo({ pos, tgt: p.clone().add(new THREE.Vector3(0, -0.35, 0)), fp: false });\n } else { lampFocus = false; show(); }\n});\npovBar.addEventListener('click', e => {\n const b = e.target.closest('button'); if (!b || b.dataset.m === povMode) return;\n povMode = b.dataset.m;\n povBar.querySelectorAll('button').forEach(x => x.classList.toggle('on', x === b));\n show();\n});\n\n// ---------- Poly Haven Texturen ----------\nasync function polyHaven(assetName){\n const all = await (await fetch('https://api.polyhaven.com/assets?t=textures')).json();\n const want = assetName.toLowerCase();\n const id = Object.keys(all).find(k => (all[k].name||'').toLowerCase() === want)\n || Object.keys(all).find(k => (all[k].name||'').toLowerCase().includes(want))\n || want.replace(/\\s+/g,'_');\n const files = await (await fetch(`https://api.polyhaven.com/files/${id}`)).json();\n const pick = key => files[key]?.['4k']?.jpg?.url || files[key]?.['4k']?.png?.url || files[key]?.['2k']?.jpg?.url || files[key]?.['1k']?.jpg?.url;\n const dim = all[id]?.dimensions; // mm [w,h]\n return { id, dim, diff: pick('Diffuse'), nor: pick('nor_gl'), rough: pick('Rough') };\n}\nconst loader = new THREE.TextureLoader();\nloader.setCrossOrigin('anonymous');\nconst loadTex = (url, srgb) => new Promise((res, rej) => loader.load(url, t => {\n t.wrapS = t.wrapT = THREE.RepeatWrapping;\n if (srgb) t.colorSpace = THREE.SRGBColorSpace;\n t.anisotropy = 8; res(t);\n}, undefined, rej));\nasync function apply(mat, assetName, uvScale){\n const f = await polyHaven(assetName);\n const [map, normalMap, roughnessMap] = await Promise.all([\n loadTex(f.diff, true), f.nor ? loadTex(f.nor) : null, f.rough ? loadTex(f.rough) : null\n ]);\n if (uvScale){ // echte Größe: UVs sind in 1/uvScale m, Textur deckt dim mm ab\n const w = (f.dim?.[0] || 2000) / 1000, hh = (f.dim?.[1] || f.dim?.[0] || 2000) / 1000;\n for (const t of [map, normalMap, roughnessMap]) if (t) t.repeat.set(uvScale / w, uvScale / hh);\n }\n mat.map = map; mat.color.set(0xffffff);\n if (normalMap) mat.normalMap = normalMap;\n if (roughnessMap) { mat.roughnessMap = roughnessMap; mat.roughness = 1; }\n mat.needsUpdate = true;\n ptDirty = true; if (ptOn) stopPT();\n return f.id;\n}\n// ---------- Tageszeit: prozeduraler Himmel (three.js Sky, Preetham-Modell) ----------\nconst { Sky } = await import('three/addons/objects/Sky.js');\nconst sky = new Sky(); sky.scale.setScalar(1000);\nconst skyScene = new THREE.Scene(); skyScene.add(sky);\nconst SU = sky.material.uniforms;\nSU.mieCoefficient.value = 0.005; SU.mieDirectionalG.value = 0.8;\n// Nacht-/Dämmerungs-Minimum: dunkles Blau, zum Horizont heller\nsky.material.uniforms.nightFloor = { value: new THREE.Color(0,0,0) };\nsky.material.fragmentShader = 'uniform vec3 nightFloor;\\n' + sky.material.fragmentShader.replace('#include <tonemapping_fragment>',\n 'float hzF = 1.0 - clamp(normalize(vWorldPosition - cameraPosition).y, 0.0, 1.0);\\n gl_FragColor.rgb = max(gl_FragColor.rgb, nightFloor * (0.55 + 0.9*hzF*hzF));\\n#include <tonemapping_fragment>');\nsky.material.needsUpdate = true;\nconst cubeRT = new THREE.WebGLCubeRenderTarget(512, { type: THREE.HalfFloatType });\nconst cubeCam = new THREE.CubeCamera(1, 5000, cubeRT); skyScene.add(cubeCam);\nconst pmrem = new THREE.PMREMGenerator(renderer);\nlet envRT = null;\nconst keyBase = key.intensity, hemiCol0 = hemi ? hemi.color.clone() : null, hemiGnd0 = hemi ? hemi.groundColor.clone() : null;\nconst SS = THREE.MathUtils.smoothstep, col = (r,g,b) => new THREE.Color(r,g,b);\nfunction makePTEnv(d, day, golden){\n const w = 256, h = 128, arr = new Float32Array(w*h*4);\n const zen = col(.03,.045,.1).lerp(col(.16,.32,.75), day);\n const hor = col(.05,.07,.13).lerp(col(.75,.82,.92).lerp(col(1,.55,.3), golden*.8), day);\n const gnd = hor.clone().multiplyScalar(.25);\n const sunC = col(1,.95,.85).lerp(col(1,.5,.2), golden).multiplyScalar(40*day);\n for (let j = 0; j < h; j++){\n const el = ((j+.5)/h - .5)*Math.PI, sy = Math.sin(el), cy = Math.cos(el);\n for (let i = 0; i < w; i++){\n const az = ((i+.5)/w - .5)*2*Math.PI, x = Math.cos(az)*cy, z = Math.sin(az)*cy;\n let r, g, bb;\n if (sy >= 0){ const t = Math.sqrt(sy); r = hor.r+(zen.r-hor.r)*t; g = hor.g+(zen.g-hor.g)*t; bb = hor.b+(zen.b-hor.b)*t; }\n else { const t = Math.min(1, -sy*4); r = hor.r+(gnd.r-hor.r)*t; g = hor.g+(gnd.g-hor.g)*t; bb = hor.b+(gnd.b-hor.b)*t; }\n const dot = x*d.x + sy*d.y + z*d.z;\n if (dot > 0){ const gl = Math.pow(dot, 48)*day*(1+golden); r += gl; g += gl*(.85-.3*golden); bb += gl*(.7-.45*golden); }\n if (dot > .999 && sy > -0.02){ r = sunC.r; g = sunC.g; bb = sunC.b; }\n const k = (j*w + i)*4; arr[k] = r; arr[k+1] = g; arr[k+2] = bb; arr[k+3] = 1;\n }\n }\n const dt = new THREE.DataTexture(arr, w, h, THREE.RGBAFormat, THREE.FloatType);\n dt.mapping = THREE.EquirectangularReflectionMapping;\n dt.minFilter = dt.magFilter = THREE.LinearFilter; dt.generateMipmaps = false; dt.needsUpdate = true;\n return dt;\n}\nconst sunV = new THREE.Vector3();\nfunction setTime(hr){\n // Sonnenbahn (vereinfacht): 7 Uhr Ost (+x) → 13 Uhr Süd (+z) → 19 Uhr West, max. 50° Höhe\n const phi = Math.PI*(hr - 7)/12, e = THREE.MathUtils.degToRad(50)*Math.sin(phi);\n sunV.set(Math.cos(phi)*Math.cos(e), Math.sin(e), Math.sin(phi)*Math.cos(e)).normalize();\n const s = sunV.y, day = SS(s, -0.14, 0.18), golden = 1 - SS(s, 0, 0.4), dusk = SS(s, -0.22, -0.02) * (1 - SS(s, 0.02, 0.2));\n SU.sunPosition.value.copy(sunV);\n SU.rayleigh.value = 2.5 + 1.2*golden;\n SU.turbidity.value = 3 + 5*golden;\n // Nachtblau, zur blauen Stunde etwas heller/violetter\n SU.nightFloor.value.setRGB(0.010 + 0.03*dusk, 0.016 + 0.03*dusk, 0.045 + 0.06*dusk);\n cubeCam.update(renderer, skyScene);\n envRT = pmrem.fromCubemap(cubeRT.texture, envRT);\n rasterEnv = envRT.texture;\n if (s > -0.03){\n key.position.copy(hc).addScaledVector(sunV, 30);\n key.color.set(0xff9a52).lerp(new THREE.Color(0xfff3e2), SS(s, 0, 0.5));\n key.intensity = keyBase * 1.3 * Math.max(day, 0.15);\n } else { // Mondlicht, gegenüber der Sonne\n const m = new THREE.Vector3(-sunV.x, Math.abs(sunV.y)*0.8 + 0.2, -sunV.z).normalize();\n key.position.copy(hc).addScaledVector(m, 30);\n key.color.set(0x9fb4ff); key.intensity = keyBase * 0.25;\n }\n envK = 0.2 + 0.1*day + 0.15*dusk; hemiK = 0.3 + 0.4*day; fillK = 0.1 + 0.5*day; expK = 1 - 0.45*day;\n if (hemi){ hemi.color.copy(hemiCol0).lerp(new THREE.Color(0x34426e), 1 - day); hemi.groundColor.copy(hemiGnd0).multiplyScalar(0.3 + 0.7*day); }\n ptEnv = makePTEnv(sunV, day, golden);\n scene.background = cubeRT.texture;\n scene.backgroundIntensity = 0.2 + 1.1*(1 - day);\n if (ptOn) stopPT(); else { scene.environment = rasterEnv; applyInterior(ceilMat.opacity); }\n act();\n}\n// Himmel & Licht folgen der echten Uhrzeit\nconst nowH = () => { const n = new Date(); return n.getHours() + n.getMinutes()/60; };\nsetTime(nowH());\nsetInterval(() => setTime(nowH()), 60000);\nlet texMsg = 'Texturen werden geladen…';\nconst upd = () => statusEl.textContent = texMsg;\nupd();\nsofa.M.fabric.color.set('#3a3a3c');\napply(sofa.M.fabric, 'Poly Wool Herringbone').then(() => sofa.M.fabric.color.set('#3a3a3c')).catch(e => console.warn(e));\nPromise.all([apply(M.floor, 'Rubber Tiles'), apply(M.wall, 'White Stucco'), apply(M.brick, 'Brick Wall 04', 1.5), apply(M.cap, 'Rough Plaster Brick 04', 1.5)])\n .then(ids => { texMsg = 'Texturen: ' + ids.join(', '); upd(); })\n .catch(e => { console.warn(e); texMsg = 'Texturen nicht erreichbar – Farb-Fallback'; upd(); });\n\n// ---------- Fernbedienung ----------\n{\n const $ = s => document.querySelector(s);\n const keys = [];\n [1,2,3,4,5,6,0].forEach(i => {\n const b = document.createElement('button');\n b.className = 'r-key' + (i === 0 ? ' wide' : ''); b.textContent = ROOMS[i][0];\n b.onclick = () => { bar.children[i].click(); sync(); };\n keys.push([i, b]); $('#rRooms').appendChild(b);\n });\n const LAMPS = {\n bogen: { on: () => lampOn, t: () => $('#lampT').click(), col: () => lampCol, sw: colIn },\n donut: { on: () => donutBtn.getAttribute('aria-pressed') === 'true', t: () => donutBtn.click(), col: () => '#ff9a3c' },\n trip: { on: () => tripOn, t: () => $('#tripT').click(), col: () => tripCol, sw: tripColIn },\n };\n let msg = '';\n document.querySelectorAll('.r-lamp').forEach(row => {\n const L = LAMPS[row.dataset.l];\n row.querySelector('.r-tog').onclick = () => { msg = ''; L.t(); sync(); };\n const c = row.querySelector('.r-col'); if (c) c.onclick = e => { e.stopPropagation(); L.sw.click(); };\n });\n const setAll = on => { Object.values(LAMPS).forEach(L => { if (L.on() !== on) L.t(); }); };\n const closePick = () => document.querySelectorAll('.lpick.open').forEach(p => p.querySelector('.lp-done').click());\n $('#rAllOn').onclick = () => { msg = ''; setAll(true); sync(); };\n $('#rAllOff').onclick = () => { msg = ''; closePick(); setAll(false); sync(); };\n $('#rPower').onclick = () => { closePick(); setAll(false); bar.children[0].click(); msg = 'Tschüss – alles aus'; sync(); };\n function sync(){\n const cur = [...bar.children].findIndex(b => b.classList.contains('on'));\n keys.forEach(([i, b]) => b.classList.toggle('on', i === cur));\n let n = 0, shown = 0;\n document.querySelectorAll('.r-lamp').forEach(row => {\n const L = LAMPS[row.dataset.l], on = L.on(), c = L.col(); if (on) n++;\n row.classList.toggle('on', on);\n const vis = +row.dataset.room === cur; row.style.display = vis ? '' : 'none'; if (vis) shown++;\n row.querySelector('i').style.cssText = on ? 'background:' + c + ';box-shadow:0 0 10px 3px ' + c + '99' : '';\n const cb = row.querySelector('.r-col'); if (cb) cb.style.setProperty('--c', c);\n });\n $('#rLightsLbl').style.display = $('#rLights').style.display = shown ? '' : 'none';\n if (n) msg = '';\n $('#rStat').textContent = msg || (n ? n + (n === 1 ? ' Licht an' : ' Lichter an') : 'Alle Lichter aus');\n }\n addEventListener('pointerup', () => setTimeout(sync, 0));\n addEventListener('input', sync);\n addEventListener('lpick', () => setTimeout(sync, 0));\n sync();\n}\n<\/script>\n<\/body>\n<\/html>\n";
var q=new URLSearchParams(location.search).get('view');
var mobile=q?q==='handy':(matchMedia('(pointer:coarse)').matches&&Math.min(screen.width,screen.height)<820)||innerWidth<700;
document.open();document.write(mobile?HANDY:DESKTOP);document.close();
})();
</script></body></html>