14 lines
215 KiB
HTML
14 lines
215 KiB
HTML
<!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> |