Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> | |
| <title>FORGE3D</title> | |
| <style> | |
| *{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;user-select:none;} | |
| :root{ | |
| --bg:#0d0d0f;--bg2:#141416;--bg3:#1a1a1e;--bg4:#222228; | |
| --br:rgba(255,255,255,.06);--br2:rgba(255,255,255,.1); | |
| --acc:#e87d39;--acc2:#c94c0a;--pur:#a855f7; | |
| --tx:#e0e0e0;--tx2:#888;--tx3:#444; | |
| --red:#e05555;--grn:#55cc77; | |
| } | |
| html,body{width:100%;height:100%;overflow:hidden;background:var(--bg);font-family:-apple-system,system-ui,sans-serif;font-size:12px;color:var(--tx);} | |
| button{font-family:inherit;cursor:pointer;} | |
| input{font-family:inherit;} | |
| /* ββ TOPBAR ββ */ | |
| #topbar{ | |
| position:fixed;top:0;left:0;right:0;height:48px; | |
| background:rgba(13,13,15,.96);backdrop-filter:blur(16px); | |
| border-bottom:1px solid var(--br); | |
| display:flex;align-items:center;padding:0 10px;gap:6px;z-index:1000; | |
| } | |
| #topbar-title{flex:1;font-size:13px;font-weight:700;color:#bbb;letter-spacing:.5px;} | |
| .tb-btn{width:36px;height:36px;background:none;border:none;color:var(--tx2);font-size:16px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background .1s;} | |
| .tb-btn:active{background:var(--br2);} | |
| .tb-pill{padding:4px 10px;background:var(--bg3);border:1px solid var(--br);border-radius:16px;color:var(--tx2);font-size:10px;display:flex;align-items:center;gap:4px;} | |
| .tb-pill.on{background:rgba(232,125,57,.15);border-color:rgba(232,125,57,.4);color:var(--acc);} | |
| /* ββ MAIN LAYOUT ββ */ | |
| #app{position:fixed;top:48px;left:0;right:0;bottom:0;display:flex;} | |
| /* ββ LEFT: OBJECT LIST ββ */ | |
| #objlist{ | |
| width:52px;background:var(--bg2);border-right:1px solid var(--br); | |
| display:flex;flex-direction:column;align-items:center;padding:6px 0;gap:3px; | |
| overflow-y:auto;overflow-x:hidden;z-index:100;flex-shrink:0; | |
| } | |
| #objlist::-webkit-scrollbar{width:0;} | |
| .ol-item{ | |
| width:40px;height:40px;background:var(--bg3);border:1.5px solid transparent; | |
| border-radius:9px;cursor:pointer;display:flex;align-items:center;justify-content:center; | |
| font-size:16px;transition:all .12s;flex-shrink:0;position:relative; | |
| } | |
| .ol-item:active{background:var(--bg4);} | |
| .ol-item.active{background:#1a170a;border-color:var(--acc);box-shadow:0 0 8px rgba(232,125,57,.25);} | |
| .ol-item.hidden{opacity:.35;} | |
| .ol-sep{width:28px;height:1px;background:var(--br);flex-shrink:0;} | |
| .ol-add{background:#101a10;border-color:#1a2a1a !important;color:#4a7;font-size:20px;} | |
| /* ββ VIEWPORT ββ */ | |
| #vpw{flex:1;position:relative;overflow:hidden;background:#1a1a1a;} | |
| #c{display:block;width:100%;height:100%;touch-action:none;} | |
| /* ββ VIEWPORT OVERLAY BUTTONS ββ */ | |
| #vp-shading{position:absolute;top:8px;right:8px;display:flex;gap:3px;z-index:20;} | |
| .vp-shd{width:30px;height:30px;background:rgba(0,0,0,.7);backdrop-filter:blur(8px);border:1px solid var(--br);border-radius:6px;color:var(--tx2);font-size:12px;display:flex;align-items:center;justify-content:center;} | |
| .vp-shd.on{background:rgba(232,125,57,.25);border-color:rgba(232,125,57,.5);color:var(--acc);} | |
| #vp-fab{position:absolute;right:10px;bottom:80px;display:flex;flex-direction:column;gap:8px;z-index:60;} | |
| .fab{width:46px;height:46px;border:none;border-radius:50%;color:#fff;display:flex;align-items:center;justify-content:center;font-size:20px;box-shadow:0 4px 20px rgba(0,0,0,.5);transition:transform .1s;} | |
| .fab:active{transform:scale(.9);} | |
| .fab-add{background:linear-gradient(135deg,var(--acc),var(--acc2));} | |
| .fab-ai{background:linear-gradient(135deg,#7c3aed,var(--pur));} | |
| #gizmo-cv{position:absolute;bottom:8px;right:8px;width:60px;height:60px;z-index:20;pointer-events:none;} | |
| /* ββ RIGHT PANEL: OBJECT PROPERTIES ββ */ | |
| #rpanel{ | |
| width:0;background:var(--bg2);border-left:1px solid var(--br); | |
| overflow:hidden;transition:width .2s ease;z-index:100;flex-shrink:0; | |
| display:flex;flex-direction:column; | |
| } | |
| #rpanel.open{width:220px;} | |
| @media(max-width:520px){#rpanel.open{width:190px;}} | |
| #rp-head{ | |
| padding:10px 12px;border-bottom:1px solid var(--br); | |
| display:flex;align-items:center;gap:8px;flex-shrink:0; | |
| } | |
| #rp-name{flex:1;font-size:12px;font-weight:700;color:var(--acc);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} | |
| #rp-close{width:26px;height:26px;background:none;border:none;color:var(--tx3);font-size:16px;border-radius:6px;} | |
| #rp-close:active{color:var(--tx);} | |
| #rp-body{flex:1;overflow-y:auto;padding:10px;} | |
| #rp-body::-webkit-scrollbar{width:3px;} | |
| #rp-body::-webkit-scrollbar-thumb{background:var(--bg4);} | |
| /* Right panel sections */ | |
| .rp-sec{margin-bottom:14px;} | |
| .rp-sec-title{font-size:9px;font-weight:700;color:var(--tx3);text-transform:uppercase;letter-spacing:.8px;margin-bottom:7px;} | |
| .rp-row{display:grid;grid-template-columns:30px 1fr 1fr 1fr;gap:3px;margin-bottom:4px;align-items:center;} | |
| .rp-label{font-size:9px;color:var(--tx3);text-align:right;padding-right:4px;} | |
| .rp-input{background:var(--bg3);border:1px solid var(--br);border-radius:5px;color:var(--tx);font-size:10px;padding:4px 5px;width:100%;text-align:center;outline:none;} | |
| .rp-input:focus{border-color:var(--acc);} | |
| .rp-btn{width:100%;padding:8px;background:var(--bg3);border:1px solid var(--br);border-radius:8px;color:var(--tx);font-size:10px;text-align:center;transition:all .1s;margin-bottom:4px;} | |
| .rp-btn:active{background:var(--bg4);border-color:var(--br2);} | |
| .rp-btn.danger{background:rgba(224,85,85,.12);border-color:rgba(224,85,85,.3);color:var(--red);} | |
| .rp-btn.danger:active{background:rgba(224,85,85,.25);} | |
| .rp-btn.acc{background:rgba(232,125,57,.12);border-color:rgba(232,125,57,.3);color:var(--acc);} | |
| .rp-color{width:100%;height:30px;border-radius:6px;border:1px solid var(--br);cursor:pointer;padding:2px;} | |
| .rp-slider{width:100%;accent-color:var(--acc);} | |
| .rp-toggle{display:flex;align-items:center;justify-content:space-between;padding:4px 0;} | |
| .rp-chk{accent-color:var(--acc);width:14px;height:14px;} | |
| /* ββ BOTTOM TOOLBAR ββ */ | |
| #btbar{ | |
| position:absolute;bottom:0;left:0;right:0;height:56px; | |
| background:rgba(10,10,12,.97);backdrop-filter:blur(16px); | |
| border-top:1px solid var(--br); | |
| display:none;align-items:center;padding:0 4px;gap:0;overflow-x:auto;z-index:50; | |
| } | |
| #btbar.show{display:flex;} | |
| #btbar::-webkit-scrollbar{height:0;} | |
| .bt-btn{min-width:48px;height:50px;background:none;border:none;color:var(--tx3);font-size:9px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;border-radius:7px;flex-shrink:0;transition:all .1s;} | |
| .bt-btn:active,.bt-btn.on{color:var(--acc);} | |
| .bt-btn .bi{font-size:16px;line-height:1;} | |
| .bt-sep{width:1px;height:30px;background:var(--br);margin:0 3px;flex-shrink:0;} | |
| /* ββ AI PANEL ββ */ | |
| #aip{ | |
| position:fixed;bottom:0;left:0;right:0; | |
| background:var(--bg);border-top:1px solid var(--br2); | |
| z-index:800;display:none;flex-direction:column; | |
| max-height:72vh;border-radius:20px 20px 0 0; | |
| box-shadow:0 -8px 40px rgba(0,0,0,.8); | |
| } | |
| #aip.show{display:flex;} | |
| #ai-handle{width:36px;height:4px;background:var(--bg4);border-radius:2px;margin:10px auto 0;} | |
| #ai-head{ | |
| padding:8px 14px 10px; | |
| background:linear-gradient(135deg,var(--acc),var(--pur)); | |
| display:flex;align-items:center;justify-content:space-between;flex-shrink:0; | |
| } | |
| #ai-head-title{font-size:13px;font-weight:700;color:#fff;} | |
| #ai-status-badge{font-size:10px;background:rgba(0,0,0,.3);padding:2px 8px;border-radius:10px;color:#fff;} | |
| #ai-close{background:none;border:none;color:rgba(255,255,255,.7);font-size:20px;} | |
| #ai-msgs{flex:1;overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:6px;min-height:0;} | |
| #ai-msgs::-webkit-scrollbar{width:3px;} | |
| #ai-msgs::-webkit-scrollbar-thumb{background:var(--bg4);} | |
| .msg-u{background:var(--acc);color:#fff;align-self:flex-end;padding:7px 12px;border-radius:16px 16px 3px 16px;max-width:88%;word-break:break-word;font-size:11px;line-height:1.5;} | |
| .msg-a{background:var(--bg3);color:var(--tx);align-self:flex-start;padding:7px 12px;border-radius:16px 16px 16px 3px;max-width:96%;word-break:break-word;font-size:11px;border:1px solid var(--br);white-space:pre-wrap;line-height:1.5;} | |
| .msg-a.ok{border-left:3px solid var(--grn);} | |
| .msg-a.err{border-left:3px solid var(--red);color:#f88;} | |
| .msg-a.plan{border-left:3px solid #60a5fa;color:#aac4ee;} | |
| .ai-thinking{display:flex;gap:4px;padding:8px 12px;} | |
| .ai-dot{width:6px;height:6px;border-radius:50%;background:var(--acc);animation:bop .8s infinite;} | |
| .ai-dot:nth-child(2){animation-delay:.15s;}.ai-dot:nth-child(3){animation-delay:.3s;} | |
| @keyframes bop{0%,80%,100%{transform:scale(.7);opacity:.3;}40%{transform:scale(1.2);opacity:1;}} | |
| #ai-suggestions{padding:0 8px 6px;display:flex;flex-wrap:wrap;gap:4px;flex-shrink:0;} | |
| .ai-sug{padding:4px 10px;background:var(--bg3);border:1px solid var(--br);border-radius:12px;color:var(--tx3);font-size:10px;cursor:pointer;transition:all .1s;} | |
| .ai-sug:active{background:var(--bg4);color:var(--tx);border-color:var(--acc);} | |
| #ai-input-row{padding:8px;border-top:1px solid var(--br);display:flex;gap:6px;flex-shrink:0;} | |
| #ai-input{flex:1;padding:9px 14px;background:var(--bg3);border:1px solid var(--br);border-radius:12px;color:var(--tx);font-size:12px;outline:none;} | |
| #ai-input:focus{border-color:var(--acc);} | |
| #ai-send{padding:9px 16px;background:var(--acc);border:none;border-radius:12px;color:#fff;font-size:12px;font-weight:700;} | |
| #ai-send:disabled{opacity:.4;} | |
| /* ββ ADD MODAL ββ */ | |
| #add-modal{ | |
| position:fixed;inset:0;background:var(--bg);z-index:900; | |
| display:none;flex-direction:column; | |
| } | |
| #add-modal.show{display:flex;} | |
| #add-modal-head{padding:12px 14px;display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--br);flex-shrink:0;} | |
| #add-modal-head input{flex:1;padding:7px 12px;background:var(--bg3);border:1px solid var(--br);border-radius:10px;color:var(--tx);font-size:13px;outline:none;} | |
| #add-modal-head input:focus{border-color:var(--acc);} | |
| #add-modal-head button{width:36px;height:36px;background:none;border:none;color:var(--tx2);font-size:20px;} | |
| #add-modal-body{flex:1;overflow-y:auto;padding:12px;} | |
| .add-sec-title{font-size:10px;font-weight:700;color:var(--tx3);text-transform:uppercase;letter-spacing:.8px;margin:12px 0 8px;} | |
| .add-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(76px,1fr));gap:6px;margin-bottom:12px;} | |
| .add-item{background:var(--bg3);border:1px solid var(--br);border-radius:10px;padding:10px 4px 8px;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:4px;transition:all .1s;} | |
| .add-item:active{background:var(--bg4);border-color:var(--br2);transform:scale(.95);} | |
| .add-item .ico{font-size:22px;}.add-item .lbl{font-size:9px;color:#aaa;} | |
| /* ββ SHEETS (bottom modal) ββ */ | |
| .ov{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:700;display:none;backdrop-filter:blur(3px);} | |
| .ov.show{display:block;} | |
| .sheet{position:fixed;bottom:0;left:0;right:0;background:var(--bg2);border-radius:18px 18px 0 0;z-index:710;display:none;flex-direction:column;max-height:80vh;border-top:1px solid var(--br);box-shadow:0 -6px 30px rgba(0,0,0,.7);} | |
| .sheet.show{display:flex;} | |
| .sh-handle{width:32px;height:4px;background:var(--bg4);border-radius:2px;margin:10px auto 4px;} | |
| .sh-title{padding:10px 16px;font-size:14px;font-weight:700;border-bottom:1px solid var(--br);display:flex;justify-content:space-between;align-items:center;flex-shrink:0;} | |
| .sh-title button{background:none;border:none;color:var(--tx3);font-size:18px;} | |
| .sh-body{flex:1;overflow-y:auto;padding:14px;} | |
| .sh-body::-webkit-scrollbar{width:3px;} | |
| /* ββ SHARED FORM ββ */ | |
| .pi{background:var(--bg3);border:1px solid var(--br);border-radius:6px;color:var(--tx);padding:5px 8px;font-size:11px;width:100%;outline:none;} | |
| .pi:focus{border-color:var(--acc);} | |
| .gbtn{padding:9px 6px;background:var(--bg3);border:1px solid var(--br);border-radius:8px;color:#bbb;cursor:pointer;font-family:inherit;text-align:center;transition:all .1s;font-size:10px;width:100%;} | |
| .gbtn:active{border-color:var(--acc);background:var(--bg4);} | |
| .rbtn{width:100%;padding:11px;background:linear-gradient(135deg,var(--acc),var(--acc2));border:none;border-radius:10px;color:#fff;font-size:12px;font-weight:700;cursor:pointer;font-family:inherit;margin-top:8px;} | |
| .pl{color:var(--tx3);font-size:10px;white-space:nowrap;} | |
| .xyz{display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;} | |
| .xyzl{display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;margin-bottom:3px;} | |
| .cx{color:#b44;font-size:9px;text-align:center;}.cy{color:#4b4;font-size:9px;text-align:center;}.cz{color:#44b;font-size:9px;text-align:center;} | |
| .prgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:4px;} | |
| .prb{padding:7px 2px;background:var(--bg3);border:1px solid var(--br);border-radius:7px;color:var(--tx2);cursor:pointer;font-size:9px;font-family:inherit;text-align:center;transition:all .1s;} | |
| .prb:active{border-color:var(--acc);color:var(--acc);} | |
| /* ββ TOAST ββ */ | |
| #toast{position:fixed;top:56px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.92);color:var(--tx);padding:7px 18px;border-radius:20px;font-size:11px;z-index:9999;display:none;pointer-events:none;border:1px solid var(--br2);white-space:nowrap;max-width:92%;} | |
| /* ββ SCRIPT EDITOR ββ */ | |
| #sa{background:#050505;color:#abb2bf;font-size:11px;font-family:monospace;padding:10px;border:none;outline:none;resize:none;line-height:1.7;width:100%;height:180px;border-bottom:1px solid #0c0c0c;} | |
| #so{background:#020202;color:#3a7a3a;font-size:10px;font-family:monospace;padding:6px;height:44px;overflow-y:auto;} | |
| </style> | |
| </head> | |
| <body> | |
| <!-- TOPBAR --> | |
| <div id="topbar"> | |
| <button class="tb-btn" onclick="location.href='/'">π </button> | |
| <div id="topbar-title">FORGE3D</div> | |
| <button class="tb-btn" onclick="toggleStats()" id="stat-btn" style="font-size:10px;">π</button> | |
| <button class="tb-btn" onclick="showAddModal()" style="color:var(--acc);font-size:22px;">+</button> | |
| <button class="tb-btn" onclick="showAI()" style="color:var(--pur);font-size:17px;">π€</button> | |
| <button class="tb-pill on" id="anim-pill" onclick="toggleAnim()"><span style="width:5px;height:5px;background:var(--acc);border-radius:50%;display:inline-block;"></span> Anim</button> | |
| <button class="tb-btn" onclick="openSheet('menu-sheet')">β°</button> | |
| </div> | |
| <!-- APP --> | |
| <div id="app"> | |
| <!-- LEFT OBJECT LIST --> | |
| <div id="objlist"> | |
| <div style="font-size:7px;color:var(--tx3);text-transform:uppercase;letter-spacing:.5px;margin-bottom:2px;">OBJ</div> | |
| <div id="ol-items"></div> | |
| <div class="ol-sep"></div> | |
| <div class="ol-item ol-add" onclick="showAddModal()">+</div> | |
| </div> | |
| <!-- VIEWPORT --> | |
| <div id="vpw"> | |
| <canvas id="c"></canvas> | |
| <!-- Shading buttons --> | |
| <div id="vp-shading"> | |
| <button class="vp-shd on" id="shd-solid" onclick="setShd('solid')">βΌ</button> | |
| <button class="vp-shd" id="shd-wire" onclick="setShd('wire')">⬑</button> | |
| <button class="vp-shd" id="shd-xray" onclick="setShd('xray')">β</button> | |
| <button class="vp-shd" id="shd-mat" onclick="setShd('mat')">π¨</button> | |
| <button class="vp-shd" onclick="togGrid()">β</button> | |
| </div> | |
| <!-- FABs --> | |
| <div id="vp-fab"> | |
| <button class="fab" style="background:#181820;border:1px solid var(--br);font-size:16px;" onclick="act('undo')">β©</button> | |
| <button class="fab fab-add" onclick="showAddModal()">+</button> | |
| <button class="fab fab-ai" onclick="showAI()">π€</button> | |
| </div> | |
| <!-- Gizmo --> | |
| <canvas id="gizmo-cv" width="60" height="60"></canvas> | |
| <!-- Stats overlay --> | |
| <div id="stats-hud" style="display:none;position:absolute;top:8px;left:8px;background:rgba(0,0,0,.7);padding:4px 8px;border-radius:6px;font-size:10px;color:var(--tx2);pointer-events:none;line-height:1.8;"> | |
| Objs:<span id="st-o">0</span> FPS:<span id="st-f">β</span> | |
| </div> | |
| <!-- Bottom toolbar (when object selected) --> | |
| <div id="btbar"> | |
| <button class="bt-btn on" id="bt-sel" onclick="setTool('select')"><span class="bi">β</span>Select</button> | |
| <button class="bt-btn" id="bt-mov" onclick="setTool('move')"><span class="bi">β</span>Move</button> | |
| <button class="bt-btn" id="bt-rot" onclick="setTool('rotate')"><span class="bi">β»</span>Rotate</button> | |
| <button class="bt-btn" id="bt-scl" onclick="setTool('scale')"><span class="bi">‑</span>Scale</button> | |
| <div class="bt-sep"></div> | |
| <button class="bt-btn" onclick="openSheet('mat-sheet')"><span class="bi">β</span>Mat</button> | |
| <button class="bt-btn" onclick="openSheet('ptc-sheet')"><span class="bi">β¨</span>FX</button> | |
| <button class="bt-btn" onclick="openSheet('phy-sheet')"><span class="bi">β½</span>Phys</button> | |
| <div class="bt-sep"></div> | |
| <button class="bt-btn" onclick="act('dup')"><span class="bi">β</span>Copy</button> | |
| <button class="bt-btn" onclick="act('ground')"><span class="bi">β¬</span>Ground</button> | |
| <button class="bt-btn" onclick="act('focus')"><span class="bi">β</span>Focus</button> | |
| <button class="bt-btn" style="color:var(--red);" onclick="act('del')"><span class="bi">β</span>Delete</button> | |
| </div> | |
| </div> | |
| <!-- RIGHT PANEL --> | |
| <div id="rpanel"> | |
| <div id="rp-head"> | |
| <div id="rp-name">β</div> | |
| <button id="rp-close" onclick="closeRPanel()">β</button> | |
| </div> | |
| <div id="rp-body"> | |
| <!-- Visibility & Lock --> | |
| <div class="rp-sec"> | |
| <div class="rp-toggle"> | |
| <span style="font-size:10px;color:var(--tx2);">π Visible</span> | |
| <input type="checkbox" class="rp-chk" id="rp-vis" checked onchange="setPropVis(this.checked)"> | |
| </div> | |
| </div> | |
| <!-- Transform --> | |
| <div class="rp-sec"> | |
| <div class="rp-sec-title">π Location</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;margin-bottom:3px;"> | |
| <div class="cx" style="font-size:9px;text-align:center;">X</div> | |
| <div class="cy" style="font-size:9px;text-align:center;">Y</div> | |
| <div class="cz" style="font-size:9px;text-align:center;">Z</div> | |
| </div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;margin-bottom:8px;"> | |
| <input class="rp-input" id="rp-lx" type="number" step="0.1" oninput="applyTransform()"> | |
| <input class="rp-input" id="rp-ly" type="number" step="0.1" oninput="applyTransform()"> | |
| <input class="rp-input" id="rp-lz" type="number" step="0.1" oninput="applyTransform()"> | |
| </div> | |
| <div class="rp-sec-title">π Rotation Β°</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;margin-bottom:3px;"> | |
| <div class="cx" style="font-size:9px;text-align:center;">X</div> | |
| <div class="cy" style="font-size:9px;text-align:center;">Y</div> | |
| <div class="cz" style="font-size:9px;text-align:center;">Z</div> | |
| </div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;margin-bottom:8px;"> | |
| <input class="rp-input" id="rp-rx" type="number" step="1" oninput="applyTransform()"> | |
| <input class="rp-input" id="rp-ry" type="number" step="1" oninput="applyTransform()"> | |
| <input class="rp-input" id="rp-rz" type="number" step="1" oninput="applyTransform()"> | |
| </div> | |
| <div class="rp-sec-title">‑ Scale</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:3px;margin-bottom:8px;"> | |
| <input class="rp-input" id="rp-sx" type="number" step="0.1" oninput="applyTransform()"> | |
| <input class="rp-input" id="rp-sy" type="number" step="0.1" oninput="applyTransform()"> | |
| <input class="rp-input" id="rp-sz" type="number" step="0.1" oninput="applyTransform()"> | |
| </div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;"> | |
| <button class="gbtn" onclick="act('resetLoc')">β© Loc</button> | |
| <button class="gbtn" onclick="act('resetRot')">β© Rot</button> | |
| <button class="gbtn" onclick="act('resetScale')">β© Scale</button> | |
| <button class="gbtn" onclick="act('ground')">β¬ Ground</button> | |
| </div> | |
| </div> | |
| <!-- Material --> | |
| <div class="rp-sec" id="rp-mat-sec"> | |
| <div class="rp-sec-title">π¨ Material</div> | |
| <div style="margin-bottom:6px;"> | |
| <div style="font-size:9px;color:var(--tx3);margin-bottom:3px;">Color</div> | |
| <input type="color" class="rp-color" id="rp-color" oninput="applyMat()"> | |
| </div> | |
| <div style="margin-bottom:6px;"> | |
| <div style="font-size:9px;color:var(--tx3);margin-bottom:3px;">Metalness <span id="rp-met-val">0</span></div> | |
| <input type="range" class="rp-slider" min="0" max="1" step="0.01" value="0" id="rp-met" oninput="document.getElementById('rp-met-val').textContent=this.value;applyMat()"> | |
| </div> | |
| <div style="margin-bottom:6px;"> | |
| <div style="font-size:9px;color:var(--tx3);margin-bottom:3px;">Roughness <span id="rp-rou-val">0.5</span></div> | |
| <input type="range" class="rp-slider" min="0" max="1" step="0.01" value="0.5" id="rp-rou" oninput="document.getElementById('rp-rou-val').textContent=this.value;applyMat()"> | |
| </div> | |
| <div class="rp-toggle" style="margin-bottom:6px;"> | |
| <span style="font-size:10px;color:var(--tx2);">Wireframe</span> | |
| <input type="checkbox" class="rp-chk" id="rp-wire" onchange="applyMat()"> | |
| </div> | |
| <div style="font-size:9px;color:var(--tx3);margin-bottom:4px;">Quick presets</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;"> | |
| <button class="gbtn" onclick="applyPreset('chrome')">Chrome</button> | |
| <button class="gbtn" onclick="applyPreset('gold')">Gold</button> | |
| <button class="gbtn" onclick="applyPreset('rubber')">Rubber</button> | |
| <button class="gbtn" onclick="applyPreset('glass')">Glass</button> | |
| <button class="gbtn" onclick="applyPreset('neon')">Neon</button> | |
| <button class="gbtn" onclick="applyPreset('carbon')">Carbon</button> | |
| </div> | |
| </div> | |
| <!-- Actions --> | |
| <div class="rp-sec"> | |
| <div class="rp-sec-title">β‘ Actions</div> | |
| <button class="rp-btn" onclick="act('dup')">β Duplicate</button> | |
| <button class="rp-btn" onclick="act('focus')">β Focus Camera</button> | |
| <button class="rp-btn" onclick="act('ground')">β¬ Snap to Ground</button> | |
| <button class="rp-btn danger" onclick="act('del')">β Delete Object</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- AI PANEL --> | |
| <div id="aip"> | |
| <div id="ai-handle"></div> | |
| <div id="ai-head"> | |
| <div id="ai-head-title">π€ FORGE3D AI</div> | |
| <div id="ai-status-badge">β¦</div> | |
| <button id="ai-close" onclick="hideAI()">β</button> | |
| </div> | |
| <div id="ai-msgs"></div> | |
| <div id="ai-suggestions"> | |
| <div class="ai-sug" onclick="quickAsk('solar system 8 planets with rings orbiting animation')">π Solar</div> | |
| <div class="ai-sug" onclick="quickAsk('black hole accretion disk photon ring particle cloud animation')">π Black Hole</div> | |
| <div class="ai-sug" onclick="quickAsk('detailed sports car with wheels headlights exhaust')">π Car</div> | |
| <div class="ai-sug" onclick="quickAsk('humanoid robot with joints rigging idle animation')">π€ Robot</div> | |
| <div class="ai-sug" onclick="quickAsk('sci-fi spaceship with engines glowing')">πΈ Ship</div> | |
| <div class="ai-sug" onclick="quickAsk('mechanical gear system rotating')">β Gears</div> | |
| <div class="ai-sug" onclick="quickAsk('medieval castle towers walls gate')">π° Castle</div> | |
| <div class="ai-sug" onclick="quickAsk('explosion vfx with particles fire shockwave')">π₯ VFX</div> | |
| <div class="ai-sug" onclick="quickAsk('cyberpunk neon city night scene')">π City</div> | |
| </div> | |
| <div id="ai-input-row"> | |
| <input id="ai-input" type="text" placeholder="Describe what to buildβ¦" onkeydown="if(event.key==='Enter')sendAI()"> | |
| <button id="ai-send" onclick="sendAI()">Send</button> | |
| </div> | |
| </div> | |
| <!-- ADD MODAL --> | |
| <div id="add-modal"> | |
| <div id="add-modal-head"> | |
| <input type="text" placeholder="Search shapesβ¦" oninput="filterAdd(this.value)"> | |
| <button onclick="hideAddModal()">β</button> | |
| </div> | |
| <div id="add-modal-body"> | |
| <div class="add-sec-title">Meshes</div> | |
| <div class="add-grid" id="add-mesh-grid"> | |
| <div class="add-item" onclick="addObj('cube')"><span class="ico">β¬</span><span class="lbl">Cube</span></div> | |
| <div class="add-item" onclick="addObj('sphere')"><span class="ico">βͺ</span><span class="lbl">Sphere</span></div> | |
| <div class="add-item" onclick="addObj('cylinder')"><span class="ico">π₯«</span><span class="lbl">Cylinder</span></div> | |
| <div class="add-item" onclick="addObj('plane')"><span class="ico">β¬</span><span class="lbl">Plane</span></div> | |
| <div class="add-item" onclick="addObj('cone')"><span class="ico">πΊ</span><span class="lbl">Cone</span></div> | |
| <div class="add-item" onclick="addObj('torus')"><span class="ico">π©</span><span class="lbl">Torus</span></div> | |
| <div class="add-item" onclick="addObj('ico')"><span class="ico">π</span><span class="lbl">Ico</span></div> | |
| <div class="add-item" onclick="addObj('torusknot')"><span class="ico">πͺ’</span><span class="lbl">Knot</span></div> | |
| <div class="add-item" onclick="addObj('ring')"><span class="ico">π</span><span class="lbl">Ring</span></div> | |
| <div class="add-item" onclick="addObj('capsule')"><span class="ico">π</span><span class="lbl">Capsule</span></div> | |
| <div class="add-item" onclick="addObj('helix')"><span class="ico">π</span><span class="lbl">Helix</span></div> | |
| <div class="add-item" onclick="addObj('wedge')"><span class="ico">π·</span><span class="lbl">Wedge</span></div> | |
| </div> | |
| <div class="add-sec-title">Lights</div> | |
| <div class="add-grid"> | |
| <div class="add-item" onclick="addLight('point')"><span class="ico">π‘</span><span class="lbl">Point</span></div> | |
| <div class="add-item" onclick="addLight('spot')"><span class="ico">π¦</span><span class="lbl">Spot</span></div> | |
| <div class="add-item" onclick="addLight('sun')"><span class="ico">βοΈ</span><span class="lbl">Sun</span></div> | |
| </div> | |
| <div class="add-sec-title">Particles</div> | |
| <div class="add-grid"> | |
| <div class="add-item" onclick="addParticles('fire');hideAddModal()"><span class="ico">π₯</span><span class="lbl">Fire</span></div> | |
| <div class="add-item" onclick="addParticles('smoke');hideAddModal()"><span class="ico">π¨</span><span class="lbl">Smoke</span></div> | |
| <div class="add-item" onclick="addParticles('sparks');hideAddModal()"><span class="ico">β¨</span><span class="lbl">Sparks</span></div> | |
| <div class="add-item" onclick="addParticles('rain');hideAddModal()"><span class="ico">π§</span><span class="lbl">Rain</span></div> | |
| <div class="add-item" onclick="addParticles('snow');hideAddModal()"><span class="ico">β</span><span class="lbl">Snow</span></div> | |
| <div class="add-item" onclick="addParticles('magic');hideAddModal()"><span class="ico">β</span><span class="lbl">Magic</span></div> | |
| <div class="add-item" onclick="addParticles('galaxy');hideAddModal()"><span class="ico">π</span><span class="lbl">Galaxy</span></div> | |
| <div class="add-item" onclick="addParticles('explosion');hideAddModal()"><span class="ico">π₯</span><span class="lbl">Blast</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- OVERLAY for sheets --> | |
| <div class="ov" id="sheet-ov" onclick="closeSheets()"></div> | |
| <!-- MATERIAL SHEET --> | |
| <div class="sheet" id="mat-sheet"> | |
| <div class="sh-handle"></div> | |
| <div class="sh-title">π¨ Material<button onclick="closeSheets()">β</button></div> | |
| <div class="sh-body"> | |
| <div style="display:grid;grid-template-columns:60px 1fr;gap:6px 8px;align-items:center;margin-bottom:14px;"> | |
| <div class="pl">Color</div><input type="color" class="pi" id="mat-col" style="height:28px;padding:1px 2px;" oninput="applyMatSheet()"> | |
| <div class="pl">Metalness</div><input class="pi" type="range" min="0" max="1" step="0.01" value="0.05" id="mat-met" oninput="applyMatSheet()"> | |
| <div class="pl">Roughness</div><input class="pi" type="range" min="0" max="1" step="0.01" value="0.75" id="mat-rou" oninput="applyMatSheet()"> | |
| <div class="pl">Emissive</div><input type="color" class="pi" id="mat-emi" style="height:28px;padding:1px 2px;" oninput="applyMatSheet()"> | |
| <div class="pl">Emit Int.</div><input class="pi" type="range" min="0" max="5" step="0.1" value="0" id="mat-emi-int" oninput="applyMatSheet()"> | |
| <div class="pl">Opacity</div><input class="pi" type="range" min="0" max="1" step="0.01" value="1" id="mat-opa" oninput="applyMatSheet()"> | |
| <div class="pl">Wireframe</div><input type="checkbox" id="mat-wire" style="accent-color:var(--acc);width:15px;height:15px;" onchange="applyMatSheet()"> | |
| </div> | |
| <div class="prgrid" style="margin-bottom:10px;"> | |
| <button class="prb" onclick="applyPreset('chrome')">Chrome</button> | |
| <button class="prb" onclick="applyPreset('gold')">Gold</button> | |
| <button class="prb" onclick="applyPreset('rubber')">Rubber</button> | |
| <button class="prb" onclick="applyPreset('glass')">Glass</button> | |
| <button class="prb" onclick="applyPreset('neon')">Neon</button> | |
| <button class="prb" onclick="applyPreset('carbon')">Carbon</button> | |
| <button class="prb" onclick="applyPreset('lava')">Lava</button> | |
| <button class="prb" onclick="applyPreset('ice')">Ice</button> | |
| </div> | |
| <button class="rbtn" onclick="quickAsk('Apply AI texture to '+SEL?.name+': canvas procedural 512x512, apply as material.map. Toast done.')">π€ AI Texture</button> | |
| </div> | |
| </div> | |
| <!-- PARTICLES SHEET --> | |
| <div class="sheet" id="ptc-sheet"> | |
| <div class="sh-handle"></div> | |
| <div class="sh-title">β¨ Particles & VFX<button onclick="closeSheets()">β</button></div> | |
| <div class="sh-body"> | |
| <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:5px;margin-bottom:12px;"> | |
| <button class="gbtn" onclick="addParticles('fire')">π₯Fire</button> | |
| <button class="gbtn" onclick="addParticles('smoke')">π¨Smoke</button> | |
| <button class="gbtn" onclick="addParticles('sparks')">β¨Sparks</button> | |
| <button class="gbtn" onclick="addParticles('explosion')">π₯Blast</button> | |
| <button class="gbtn" onclick="addParticles('rain')">π§Rain</button> | |
| <button class="gbtn" onclick="addParticles('snow')">βSnow</button> | |
| <button class="gbtn" onclick="addParticles('magic')">βMagic</button> | |
| <button class="gbtn" onclick="addParticles('galaxy')">πGalaxy</button> | |
| </div> | |
| <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:5px;"> | |
| <button class="gbtn" onclick="createVFX('explosion')">π₯ Explode</button> | |
| <button class="gbtn" onclick="createVFX('lightning')">β‘ Lightning</button> | |
| <button class="gbtn" onclick="createVFX('portal')">π Portal</button> | |
| <button class="gbtn" onclick="objVFX('ghost')">π» Ghost</button> | |
| <button class="gbtn" onclick="objVFX('hologram')">π΅ Holo</button> | |
| <button class="gbtn" onclick="objVFX('dissolve')">β‘ Dissolve</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- PHYSICS SHEET --> | |
| <div class="sheet" id="phy-sheet"> | |
| <div class="sh-handle"></div> | |
| <div class="sh-title">β½ Physics<button onclick="closeSheets()">β</button></div> | |
| <div class="sh-body"> | |
| <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:5px;margin-bottom:12px;"> | |
| <button class="gbtn" onclick="addPhysics('gravity')">π Fall</button> | |
| <button class="gbtn" onclick="addPhysics('bounce')">β½ Bounce</button> | |
| <button class="gbtn" onclick="addPhysics('spin')">π Spin</button> | |
| <button class="gbtn" onclick="addPhysics('float')">π Float</button> | |
| <button class="gbtn" onclick="addPhysics('orbit')">πͺ Orbit</button> | |
| <button class="gbtn" onclick="addPhysics('wave')">γ Wave</button> | |
| </div> | |
| <button onclick="clearPhysics()" class="rbtn" style="background:linear-gradient(135deg,#555,#333);">β Clear Physics</button> | |
| </div> | |
| </div> | |
| <!-- WORLD SHEET --> | |
| <div class="sheet" id="world-sheet"> | |
| <div class="sh-handle"></div> | |
| <div class="sh-title">π World<button onclick="closeSheets()">β</button></div> | |
| <div class="sh-body"> | |
| <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:5px;margin-bottom:14px;"> | |
| <button class="gbtn" onclick="setWorld('studio')">π¬ Studio</button> | |
| <button class="gbtn" onclick="setWorld('sunset')">π Sunset</button> | |
| <button class="gbtn" onclick="setWorld('night')">π Night</button> | |
| <button class="gbtn" onclick="setWorld('space')">π Space</button> | |
| <button class="gbtn" onclick="setWorld('neon')">π Neon</button> | |
| <button class="gbtn" onclick="setWorld('void')">β¬ Void</button> | |
| </div> | |
| <div style="display:grid;grid-template-columns:70px 1fr;gap:6px 8px;align-items:center;"> | |
| <div class="pl">BG Color</div><input type="color" class="pi" value="#1a1a1a" style="height:26px;padding:1px 2px;" oninput="SCENE.background.set(this.value)"> | |
| <div class="pl">Ambient</div><input class="pi" type="range" min="0" max="3" step="0.05" value="0.45" oninput="AMBLIGHT.intensity=+this.value"> | |
| <div class="pl">Sun</div><input class="pi" type="range" min="0" max="6" step="0.05" value="1.2" oninput="SUNLIGHT.intensity=+this.value"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SCRIPT SHEET --> | |
| <div class="sheet" id="script-sheet" style="max-height:90vh;"> | |
| <div class="sh-handle"></div> | |
| <div class="sh-title">π Script<button onclick="closeSheets()">β</button></div> | |
| <div class="sh-body" style="padding:0;"> | |
| <textarea id="sa" placeholder="// Paste or write Three.js code here and run it"></textarea> | |
| <div id="so">// Output</div> | |
| <div style="display:flex;gap:6px;padding:8px;"> | |
| <button class="gbtn" style="flex:1;" onclick="runSc()">βΆ Run</button> | |
| <button class="gbtn" style="flex:1;" onclick="document.getElementById('sa').value=''">Clear</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- MENU SHEET --> | |
| <div class="sheet" id="menu-sheet"> | |
| <div class="sh-handle"></div> | |
| <div class="sh-title">β° Menu<button onclick="closeSheets()">β</button></div> | |
| <div class="sh-body"> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;"> | |
| <button class="gbtn" onclick="openSheet('world-sheet')">π World</button> | |
| <button class="gbtn" onclick="openSheet('script-sheet')">π Script</button> | |
| <button class="gbtn" onclick="act('clearAll')">π Clear All</button> | |
| <button class="gbtn" onclick="act('saveRender')">π· Screenshot</button> | |
| <button class="gbtn" onclick="chkStatus()">π‘ API Status</button> | |
| <button class="gbtn" onclick="closeSheets();showAI()">π€ AI Panel</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="toast"></div> | |
| <input type="file" id="texi" accept="image/*" style="display:none" onchange="applyTexFile(this)"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script> | |
| ; | |
| // ββ GLOBALS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let SCENE,CAM,RENDERER,RAYCASTER,AMBLIGHT,SUNLIGHT,GRID; | |
| let OBJS=[],SEL=null,TOOL='select',SHADING='solid'; | |
| let ANIM_ON=true,AI_MODEL='groq'; | |
| const ORB={th:.6,ph:1.1,r:12,tx:0,ty:.5,tz:0}; | |
| const PT={dn:false,btn:-1,lx:0,ly:0,sx:0,sy:0,drag:false,ax:null}; | |
| let OVX=0,OVY=0; | |
| let PHYS_OBJS=[],GRAVITY=-0.098; | |
| const WIND=new THREE.Vector3(0,0,0); | |
| let PTCL_SYS=[]; | |
| const SPRITE_CACHE={}; | |
| let _fc=0,_fl=performance.now(),_fps=0; | |
| const MOB=/Android|iPhone|iPad/i.test(navigator.userAgent); | |
| let GIZMO=null; | |
| // ββ INIT βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| window.addEventListener('load',()=>{ | |
| const cv=document.getElementById('c'); | |
| try{RENDERER=new THREE.WebGLRenderer({canvas:cv,antialias:!MOB,preserveDrawingBuffer:true,powerPreference:'high-performance'});} | |
| catch(e){RENDERER=new THREE.WebGLRenderer({canvas:cv,antialias:false,preserveDrawingBuffer:true});} | |
| RENDERER.setPixelRatio(Math.min(devicePixelRatio,MOB?1.5:2)); | |
| RENDERER.shadowMap.enabled=true; | |
| RENDERER.shadowMap.type=THREE.PCFSoftShadowMap; | |
| RENDERER.outputEncoding=THREE.sRGBEncoding; | |
| RENDERER.toneMapping=THREE.CineonToneMapping; | |
| RENDERER.toneMappingExposure=1.1; | |
| SCENE=new THREE.Scene();SCENE.background=new THREE.Color(0x1a1a1e); | |
| CAM=new THREE.PerspectiveCamera(50,1,.01,5000); | |
| RAYCASTER=new THREE.Raycaster(); | |
| new ResizeObserver(onResize).observe(document.getElementById('vpw')); | |
| onResize(); | |
| buildScene();setupGizmo();setupPointer();setupKeys(); | |
| loop(); | |
| setTimeout(chkStatus,2000); | |
| toast('FORGE3D ready β tap π€ to build!',3000); | |
| }); | |
| function onResize(){ | |
| const vp=document.getElementById('vpw');if(!vp||!RENDERER)return; | |
| const w=vp.clientWidth,h=vp.clientHeight;if(!w||!h)return; | |
| RENDERER.setSize(w,h,false);CAM.aspect=w/h;CAM.updateProjectionMatrix(); | |
| } | |
| function buildScene(){ | |
| AMBLIGHT=new THREE.AmbientLight(0xffffff,.45);SCENE.add(AMBLIGHT); | |
| SUNLIGHT=new THREE.DirectionalLight(0xfffaf0,1.2);SUNLIGHT.position.set(8,14,6); | |
| SUNLIGHT.castShadow=true;SUNLIGHT.shadow.mapSize.set(1024,1024); | |
| SUNLIGHT.shadow.camera.left=SUNLIGHT.shadow.camera.bottom=-25; | |
| SUNLIGHT.shadow.camera.right=SUNLIGHT.shadow.camera.top=25; | |
| SCENE.add(SUNLIGHT); | |
| SCENE.add(new THREE.DirectionalLight(0x8899ff,.25)).position.set(-6,4,-6); | |
| GRID=new THREE.GridHelper(80,32,0x3a3a3a,0x2a2a2a);SCENE.add(GRID); | |
| SCENE.add(new THREE.AxesHelper(2)); | |
| addObj('cube'); | |
| updCam(); | |
| } | |
| // ββ GIZMO βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setupGizmo(){ | |
| GIZMO=new THREE.Group(); | |
| const mkA=(dir,col,ax)=>{const a=new THREE.ArrowHelper(dir.normalize(),new THREE.Vector3(),1.4,col,.32,.15);a.line.material.depthTest=false;a.cone.material.depthTest=false;a.line.renderOrder=999;a.cone.renderOrder=999;a.userData.axis=ax;a.cone.userData.axis=ax;a.line.userData.axis=ax;return a;}; | |
| GIZMO.add(mkA(new THREE.Vector3(1,0,0),0xff2222,'x')); | |
| GIZMO.add(mkA(new THREE.Vector3(0,1,0),0x22cc22,'y')); | |
| GIZMO.add(mkA(new THREE.Vector3(0,0,1),0x2244ff,'z')); | |
| GIZMO.visible=false;SCENE.add(GIZMO); | |
| } | |
| function updateGizmo(){ | |
| if(!GIZMO)return; | |
| const vis=!!SEL&&SEL.type==='MESH'&&(TOOL==='move'||TOOL==='rotate'||TOOL==='scale'); | |
| GIZMO.visible=vis; | |
| if(vis){const wp=new THREE.Vector3();SEL.mesh.getWorldPosition(wp);GIZMO.position.copy(wp);GIZMO.scale.setScalar(CAM.position.distanceTo(wp)*.12);} | |
| } | |
| function checkGizmo(cx,cy){ | |
| if(!GIZMO?.visible)return null; | |
| RAYCASTER.setFromCamera(ndcPos(cx,cy),CAM); | |
| const obs=[];GIZMO.traverse(o=>{if(o.isMesh||o.isLine)obs.push(o);}); | |
| const hits=RAYCASTER.intersectObjects(obs,false);if(!hits.length)return null; | |
| let o=hits[0].object;while(o&&!o.userData.axis)o=o.parent;return o?.userData.axis||null; | |
| } | |
| // ββ CAMERA βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updCam(){ | |
| const{th,ph,r,tx,ty,tz}=ORB; | |
| CAM.position.set(tx+r*Math.sin(ph)*Math.sin(th),ty+r*Math.cos(ph),tz+r*Math.sin(ph)*Math.cos(th)); | |
| CAM.lookAt(tx,ty,tz);drawGizmo(); | |
| } | |
| function doPan(dx,dy){ | |
| const right=new THREE.Vector3(),up=new THREE.Vector3(); | |
| CAM.getWorldDirection(new THREE.Vector3()); | |
| right.crossVectors(CAM.position.clone().sub(new THREE.Vector3(ORB.tx,ORB.ty,ORB.tz)).normalize(),new THREE.Vector3(0,1,0)).normalize(); | |
| up.set(0,1,0); | |
| const s=ORB.r*.001; | |
| ORB.tx-=right.x*dx*s;ORB.tz-=right.z*dx*s; | |
| ORB.ty+=dy*s;updCam(); | |
| } | |
| function clamp(v,lo,hi){return Math.max(lo,Math.min(hi,v));} | |
| function ndcPos(cx,cy){ | |
| const vp=document.getElementById('vpw').getBoundingClientRect(); | |
| return new THREE.Vector2(((cx-vp.left)/vp.width)*2-1,-((cy-vp.top)/vp.height)*2+1); | |
| } | |
| // ββ POINTER βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setupPointer(){ | |
| const cv=document.getElementById('c'); | |
| cv.addEventListener('mousedown',e=>{PT.dn=true;PT.btn=e.button;PT.drag=false;PT.lx=PT.sx=e.clientX;PT.ly=PT.sy=e.clientY;PT.ax=null;OVX=0;OVY=0;if(e.button===0&&GIZMO?.visible)PT.ax=checkGizmo(e.clientX,e.clientY);if(e.button===1)e.preventDefault();}); | |
| window.addEventListener('mousemove',e=>{ | |
| if(!PT.dn)return;const dx=e.clientX-PT.lx,dy=e.clientY-PT.ly;PT.lx=e.clientX;PT.ly=e.clientY; | |
| if(Math.abs(e.clientX-PT.sx)+Math.abs(e.clientY-PT.sy)>3)PT.drag=true; | |
| if(PT.btn===2||(PT.btn===0&&e.altKey)||PT.btn===1){ | |
| if(e.shiftKey||PT.btn===1)doPan(dx,dy); | |
| else{OVX=-dx*.007;OVY=dy*.007;ORB.th+=OVX;ORB.ph=clamp(ORB.ph+OVY,.05,Math.PI-.05);updCam();} | |
| } | |
| if(PT.btn===0&&!e.altKey&&SEL?.type==='MESH'){ | |
| if(PT.ax)dragGizmo(dx,dy,PT.ax); | |
| else if(TOOL!=='select')applyDrag(dx,dy); | |
| } | |
| }); | |
| window.addEventListener('mouseup',e=>{if(!PT.drag&&e.button===0){if(!PT.ax)clickSel(e.clientX,e.clientY);}PT.dn=false;PT.btn=-1;PT.drag=false;PT.ax=null;}); | |
| cv.addEventListener('wheel',e=>{e.preventDefault();ORB.r=clamp(ORB.r*(e.deltaY>0?1.09:.92),.3,2000);updCam();},{passive:false}); | |
| cv.addEventListener('contextmenu',e=>{e.preventDefault();if(SEL)openRPanel();}); | |
| // TOUCH | |
| let tp=[],tpin=0,tvx=0,tvy=0,tmov=false; | |
| cv.addEventListener('touchstart',e=>{e.preventDefault();tmov=false;tvx=0;tvy=0;OVX=0;OVY=0;tp=Array.from(e.touches).map(t=>({x:t.clientX,y:t.clientY}));if(e.touches.length===2)tpin=Math.hypot(e.touches[1].clientX-e.touches[0].clientX,e.touches[1].clientY-e.touches[0].clientY);},{passive:false}); | |
| cv.addEventListener('touchmove',e=>{ | |
| e.preventDefault(); | |
| const tc=Array.from(e.touches).map(t=>({x:t.clientX,y:t.clientY})); | |
| if(tc.length===1&&tp.length===1){ | |
| const dx=tc[0].x-tp[0].x,dy=tc[0].y-tp[0].y; | |
| if(Math.abs(dx)+Math.abs(dy)>2)tmov=true; | |
| if(SEL&&TOOL!=='select'){applyDrag(dx,dy);} | |
| else{OVX=-dx*.007;OVY=dy*.007;ORB.th+=OVX;ORB.ph=clamp(ORB.ph+OVY,.05,Math.PI-.05);updCam();} | |
| }else if(tc.length===2&&tp.length===2){ | |
| tmov=true; | |
| const pin=Math.hypot(tc[1].x-tc[0].x,tc[1].y-tc[0].y); | |
| ORB.r=clamp(ORB.r*(tpin/Math.max(pin,.1)),.3,2000);tpin=pin; | |
| const mdx=((tc[0].x+tc[1].x)-(tp[0].x+tp[1].x))*.5; | |
| const mdy=((tc[0].y+tc[1].y)-(tp[0].y+tp[1].y))*.5; | |
| doPan(mdx,mdy);updCam(); | |
| } | |
| tp=tc; | |
| },{passive:false}); | |
| cv.addEventListener('touchend',e=>{ | |
| if(!tmov&&e.changedTouches.length===1){ | |
| clickSel(e.changedTouches[0].clientX,e.changedTouches[0].clientY); | |
| } | |
| tp=[]; | |
| },{passive:false}); | |
| } | |
| function setupKeys(){ | |
| window.addEventListener('keydown',e=>{ | |
| if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA')return; | |
| if(e.key==='Delete'||e.key==='Backspace')act('del'); | |
| if(e.key==='g')setTool('move'); | |
| if(e.key==='r')setTool('rotate'); | |
| if(e.key==='s')setTool('scale'); | |
| if(e.key==='Escape'){setTool('select');clrSel();} | |
| if(e.ctrlKey&&e.key==='z')act('undo'); | |
| if(e.ctrlKey&&e.key==='d')act('dup'); | |
| if(e.key==='f')act('focus'); | |
| }); | |
| } | |
| // ββ DRAG TOOLS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function applyDrag(dx,dy){ | |
| if(!SEL)return;const m=SEL.mesh;const s=ORB.r*.004; | |
| if(TOOL==='move'){m.position.x+=dx*s;m.position.y-=dy*s;} | |
| if(TOOL==='rotate'){m.rotation.y+=dx*.01;m.rotation.x+=dy*.01;} | |
| if(TOOL==='scale'){const f=1+dx*.005;m.scale.x*=f;m.scale.y*=f;m.scale.z*=f;} | |
| refreshRPanel(); | |
| } | |
| function dragGizmo(dx,dy,ax){ | |
| if(!SEL)return;const m=SEL.mesh;const s=ORB.r*.005; | |
| if(TOOL==='move'){if(ax==='x')m.position.x+=dx*s;else if(ax==='y')m.position.y-=dy*s;else m.position.z+=dx*s;} | |
| if(TOOL==='rotate'){if(ax==='x')m.rotation.x+=dy*.01;else if(ax==='y')m.rotation.y+=dx*.01;else m.rotation.z+=dx*.01;} | |
| if(TOOL==='scale'){const f=1+(dx-dy)*.004;if(ax==='x')m.scale.x*=f;else if(ax==='y')m.scale.y*=f;else m.scale.z*=f;} | |
| refreshRPanel(); | |
| } | |
| // ββ SELECTION ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function clickSel(cx,cy){ | |
| RAYCASTER.setFromCamera(ndcPos(cx,cy),CAM); | |
| const meshes=[];OBJS.forEach(o=>{if(o.vis&&o.mesh)o.mesh.traverse(c=>{if(c.isMesh&&!c.name.startsWith('__'))meshes.push(c);});}); | |
| const hits=RAYCASTER.intersectObjects(meshes,false); | |
| if(!hits.length){clrSel();return;} | |
| let hit=hits[0].object;while(hit.parent&&!OBJS.find(o=>o.mesh===hit))hit=hit.parent; | |
| const found=OBJS.find(o=>o.mesh===hit); | |
| if(found)doSel(found);else clrSel(); | |
| } | |
| function doSel(obj){ | |
| if(SEL)clrHL(SEL); | |
| SEL=obj; | |
| if(obj?.type==='MESH')addHL(obj); | |
| refreshOL();refreshRPanel(); | |
| document.getElementById('btbar').classList.toggle('show',!!obj); | |
| // Auto-open right panel on selection | |
| if(obj)openRPanel(); | |
| } | |
| function clrSel(){ | |
| if(SEL)clrHL(SEL);SEL=null; | |
| refreshOL();refreshRPanel(); | |
| document.getElementById('btbar').classList.remove('show'); | |
| closeRPanel(); | |
| } | |
| function addHL(o){if(!o.mesh?.geometry)return;const m=new THREE.Mesh(o.mesh.geometry,new THREE.MeshBasicMaterial({color:0xff8800,side:THREE.BackSide,depthTest:false}));m.name='__hl';m.scale.setScalar(1.06);o.mesh.add(m);} | |
| function clrHL(o){if(!o?.mesh)return;const h=o.mesh.getObjectByName('__hl');if(h)o.mesh.remove(h);} | |
| // ββ RIGHT PANEL βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function openRPanel(){ | |
| document.getElementById('rpanel').classList.add('open'); | |
| refreshRPanel(); | |
| } | |
| function closeRPanel(){ | |
| document.getElementById('rpanel').classList.remove('open'); | |
| } | |
| function refreshRPanel(){ | |
| if(!SEL){document.getElementById('rp-name').textContent='β';return;} | |
| document.getElementById('rp-name').textContent=SEL.name; | |
| const m=SEL.mesh; | |
| const R2D=180/Math.PI; | |
| document.getElementById('rp-lx').value=m.position.x.toFixed(2); | |
| document.getElementById('rp-ly').value=m.position.y.toFixed(2); | |
| document.getElementById('rp-lz').value=m.position.z.toFixed(2); | |
| document.getElementById('rp-rx').value=(m.rotation.x*R2D).toFixed(1); | |
| document.getElementById('rp-ry').value=(m.rotation.y*R2D).toFixed(1); | |
| document.getElementById('rp-rz').value=(m.rotation.z*R2D).toFixed(1); | |
| document.getElementById('rp-sx').value=m.scale.x.toFixed(2); | |
| document.getElementById('rp-sy').value=m.scale.y.toFixed(2); | |
| document.getElementById('rp-sz').value=m.scale.z.toFixed(2); | |
| document.getElementById('rp-vis').checked=SEL.vis; | |
| // Material | |
| const mat=getMat(); | |
| if(mat){ | |
| document.getElementById('rp-mat-sec').style.display=''; | |
| try{ | |
| const c='#'+mat.color.getHexString(); | |
| document.getElementById('rp-color').value=c; | |
| document.getElementById('rp-met').value=mat.metalness||0; | |
| document.getElementById('rp-met-val').textContent=(mat.metalness||0).toFixed(2); | |
| document.getElementById('rp-rou').value=mat.roughness||0.5; | |
| document.getElementById('rp-rou-val').textContent=(mat.roughness||0.5).toFixed(2); | |
| document.getElementById('rp-wire').checked=mat.wireframe||false; | |
| }catch(e){} | |
| } else { | |
| document.getElementById('rp-mat-sec').style.display='none'; | |
| } | |
| } | |
| function getMat(){ | |
| if(!SEL||SEL.type!=='MESH')return null; | |
| try{return Array.isArray(SEL.mesh.material)?SEL.mesh.material[0]:SEL.mesh.material;}catch(e){return null;} | |
| } | |
| function applyTransform(){ | |
| if(!SEL)return;const m=SEL.mesh;const D2R=Math.PI/180; | |
| m.position.set(+document.getElementById('rp-lx').value||0,+document.getElementById('rp-ly').value||0,+document.getElementById('rp-lz').value||0); | |
| m.rotation.set((+document.getElementById('rp-rx').value||0)*D2R,(+document.getElementById('rp-ry').value||0)*D2R,(+document.getElementById('rp-rz').value||0)*D2R); | |
| m.scale.set(+document.getElementById('rp-sx').value||1,+document.getElementById('rp-sy').value||1,+document.getElementById('rp-sz').value||1); | |
| } | |
| function applyMat(){ | |
| const mat=getMat();if(!mat)return; | |
| mat.color.set(document.getElementById('rp-color').value); | |
| mat.metalness=+document.getElementById('rp-met').value; | |
| mat.roughness=+document.getElementById('rp-rou').value; | |
| mat.wireframe=document.getElementById('rp-wire').checked; | |
| mat.needsUpdate=true; | |
| } | |
| function applyMatSheet(){ | |
| const mat=getMat();if(!mat)return; | |
| mat.color.set(document.getElementById('mat-col').value); | |
| mat.metalness=+document.getElementById('mat-met').value; | |
| mat.roughness=+document.getElementById('mat-rou').value; | |
| mat.emissive?.set(document.getElementById('mat-emi').value); | |
| mat.emissiveIntensity=+document.getElementById('mat-emi-int').value; | |
| const opa=+document.getElementById('mat-opa').value; | |
| if(opa<1){mat.transparent=true;mat.opacity=opa;}else{mat.transparent=false;mat.opacity=1;} | |
| mat.wireframe=document.getElementById('mat-wire').checked; | |
| mat.needsUpdate=true; | |
| } | |
| function applyPreset(p){ | |
| const mat=getMat();if(!mat)return; | |
| const P={ | |
| chrome:{color:'#d4d4d4',metalness:.98,roughness:.02}, | |
| gold:{color:'#FFD700',metalness:.92,roughness:.08}, | |
| rubber:{color:'#0a0a0a',metalness:.05,roughness:.95}, | |
| glass:{color:'#aaddff',metalness:0,roughness:0,transparent:true,opacity:.15}, | |
| neon:{color:'#00ffff',emissive:'#00ffff',emissiveIntensity:4}, | |
| carbon:{color:'#1a1a1a',metalness:.6,roughness:.4}, | |
| lava:{color:'#331100',emissive:'#ff4400',emissiveIntensity:1.5}, | |
| ice:{color:'#ccf0ff',metalness:.1,roughness:.05,transparent:true,opacity:.7}, | |
| }; | |
| const pr=P[p];if(!pr)return; | |
| if(pr.color)mat.color.set(pr.color); | |
| if(pr.metalness!==undefined)mat.metalness=pr.metalness; | |
| if(pr.roughness!==undefined)mat.roughness=pr.roughness; | |
| if(pr.transparent!==undefined){mat.transparent=pr.transparent;mat.opacity=pr.opacity||1;} | |
| if(pr.emissive)mat.emissive?.set(pr.emissive); | |
| if(pr.emissiveIntensity!==undefined)mat.emissiveIntensity=pr.emissiveIntensity; | |
| mat.needsUpdate=true; | |
| refreshRPanel(); | |
| toast('Preset: '+p); | |
| } | |
| function setPropVis(v){if(SEL){SEL.vis=v;SEL.mesh.visible=v;refreshOL();}} | |
| // ββ OBJECT FACTORIES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function mkMat(c){return new THREE.MeshStandardMaterial({color:c||'#888',roughness:.75,metalness:.05});} | |
| function addObj(type,name){ | |
| const N=28;let geo; | |
| switch(type){ | |
| case'cube':geo=new THREE.BoxGeometry(2,2,2,2,2,2);break; | |
| case'sphere':geo=new THREE.SphereGeometry(1,N,N/2);break; | |
| case'cylinder':geo=new THREE.CylinderGeometry(1,1,2,N);break; | |
| case'plane':geo=new THREE.PlaneGeometry(4,4,6,6);geo.rotateX(-Math.PI/2);break; | |
| case'cone':geo=new THREE.ConeGeometry(1,2,N);break; | |
| case'torus':geo=new THREE.TorusGeometry(1,.35,12,N);break; | |
| case'ico':geo=new THREE.IcosahedronGeometry(1,2);break; | |
| case'torusknot':geo=new THREE.TorusKnotGeometry(1,.3,100,12);break; | |
| case'ring':geo=new THREE.RingGeometry(.5,1,N);geo.rotateX(-Math.PI/2);break; | |
| case'wedge':{const sh=new THREE.Shape();sh.moveTo(-1,0);sh.lineTo(1,0);sh.lineTo(0,1.8);sh.closePath();geo=new THREE.ExtrudeGeometry(sh,{depth:2,bevelEnabled:false});geo.center();break;} | |
| case'helix':{const pts=[];for(let i=0;i<=150;i++){const t=i/150*4*Math.PI;pts.push(new THREE.Vector3(Math.cos(t),t*.25-1.5,Math.sin(t)));}geo=new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts),150,.08,6,false);break;} | |
| case'capsule':{const cpts=[];for(let i=0;i<=12;i++){const a=i/12*Math.PI/2;cpts.push(new THREE.Vector2(Math.sin(a)*.75,Math.cos(a)*.75+.8));}cpts.push(new THREE.Vector2(.75,-.8));for(let i=12;i>=0;i--){const a=i/12*Math.PI/2;cpts.push(new THREE.Vector2(Math.sin(a)*.75,-.8-Math.cos(a)*.75));}geo=new THREE.LatheGeometry(cpts,N);break;} | |
| default:geo=new THREE.BoxGeometry(1,1,1); | |
| } | |
| const mesh=new THREE.Mesh(geo,mkMat()); | |
| mesh.castShadow=true;mesh.receiveShadow=true; | |
| mesh.position.y=type==='plane'?0:1; | |
| SCENE.add(mesh); | |
| const n=name||(type.charAt(0).toUpperCase()+type.slice(1)+'.'+(OBJS.length+1).toString().padStart(3,'0')); | |
| const o={mesh,light:null,name:n,type:'MESH',vis:true,kf:{},phys:null}; | |
| OBJS.push(o);doSel(o);refreshOL();refreshSt(); | |
| hideAddModal(); | |
| return o; | |
| } | |
| function addLight(type){ | |
| let light,ind; | |
| if(type==='sun'){light=new THREE.DirectionalLight(0xfffaf0,1.5);} | |
| else if(type==='spot'){light=new THREE.SpotLight(0xfffaf0,2,20,Math.PI/5,.3);light.castShadow=true;} | |
| else{light=new THREE.PointLight(0xfffaf0,1.5,20);light.castShadow=true;} | |
| light.position.set((Math.random()-.5)*6,5+Math.random()*2,(Math.random()-.5)*6);SCENE.add(light); | |
| ind=new THREE.Mesh(new THREE.SphereGeometry(.12,8,8),new THREE.MeshBasicMaterial({color:0xffffaa,depthTest:false})); | |
| ind.position.copy(light.position);SCENE.add(ind); | |
| const nm=type.charAt(0).toUpperCase()+type.slice(1)+' Light'; | |
| const o={mesh:ind,light,name:nm,type:'LIGHT',vis:true,kf:{},phys:null}; | |
| OBJS.push(o);doSel(o);refreshOL();refreshSt();hideAddModal(); | |
| return o; | |
| } | |
| // ββ OBJECT LIST βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function refreshOL(){ | |
| const el=document.getElementById('ol-items');if(!el)return; | |
| const icons={'MESH':'β¬','LIGHT':'π‘','GROUP':'π¦'}; | |
| el.innerHTML=''; | |
| OBJS.forEach((o,i)=>{ | |
| const d=document.createElement('div'); | |
| d.className='ol-item'+(SEL===o?' active':'')+(o.vis?'':' hidden'); | |
| d.title=o.name; | |
| d.textContent=icons[o.type]||'π¦'; | |
| d.onclick=()=>doSel(o); | |
| el.appendChild(d); | |
| if((i+1)%5===0){const sep=document.createElement('div');sep.className='ol-sep';el.appendChild(sep);} | |
| }); | |
| } | |
| function refreshSt(){ | |
| document.getElementById('st-o').textContent=OBJS.length; | |
| } | |
| // ββ ACTIONS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function act(cmd){ | |
| switch(cmd){ | |
| case'del':if(SEL){SCENE.remove(SEL.mesh);if(SEL.light)SCENE.remove(SEL.light);OBJS=OBJS.filter(o=>o!==SEL);clrSel();refreshOL();refreshSt();toast('Deleted');}break; | |
| case'dup':if(SEL&&SEL.type==='MESH'){const c=SEL.mesh.clone();c.position.x+=1.5;SCENE.add(c);const o={mesh:c,light:null,name:SEL.name+'.copy',type:'MESH',vis:true,kf:{},phys:null};OBJS.push(o);doSel(o);refreshOL();refreshSt();toast('Duplicated');}break; | |
| case'focus':if(SEL){const wp=new THREE.Vector3();SEL.mesh.getWorldPosition(wp);ORB.tx=wp.x;ORB.ty=wp.y;ORB.tz=wp.z;ORB.r=5;updCam();toast('Focused');}break; | |
| case'ground':if(SEL){const b=new THREE.Box3().setFromObject(SEL.mesh);SEL.mesh.position.y-=b.min.y;refreshRPanel();toast('Grounded');}break; | |
| case'clearAll':OBJS.forEach(o=>{SCENE.remove(o.mesh);if(o.light)SCENE.remove(o.light);});OBJS=[];clrSel();refreshOL();refreshSt();toast('Scene cleared');break; | |
| case'resetLoc':if(SEL){SEL.mesh.position.set(0,1,0);refreshRPanel();}break; | |
| case'resetRot':if(SEL){SEL.mesh.rotation.set(0,0,0);refreshRPanel();}break; | |
| case'resetScale':if(SEL){SEL.mesh.scale.set(1,1,1);refreshRPanel();}break; | |
| case'saveRender':{const a=document.createElement('a');a.href=RENDERER.domElement.toDataURL('image/png');a.download='forge3d_render.png';a.click();toast('Screenshot saved');}break; | |
| case'undo':toast('Undo coming soon');break; | |
| } | |
| } | |
| function setTool(t){ | |
| TOOL=t; | |
| ['sel','mov','rot','scl'].forEach(id=>{document.getElementById('bt-'+id)?.classList.remove('on');}); | |
| const map={select:'sel',move:'mov',rotate:'rot',scale:'scl'}; | |
| document.getElementById('bt-'+map[t])?.classList.add('on'); | |
| toast('Tool: '+t); | |
| } | |
| // ββ SHADING βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setShd(mode){ | |
| SHADING=mode; | |
| ['solid','wire','xray','mat'].forEach(m=>document.getElementById('shd-'+m)?.classList.remove('on')); | |
| document.getElementById('shd-'+mode)?.classList.add('on'); | |
| OBJS.forEach(o=>{ | |
| if(!o.mesh||o.type!=='MESH')return; | |
| o.mesh.traverse(c=>{ | |
| if(!c.isMesh||c.name.startsWith('__'))return; | |
| const mat=Array.isArray(c.material)?c.material[0]:c.material; | |
| if(!mat)return; | |
| if(mode==='wire'){mat.wireframe=true;mat.transparent=false;} | |
| else if(mode==='xray'){mat.wireframe=false;mat.transparent=true;mat.opacity=.25;} | |
| else if(mode==='solid'){mat.wireframe=false;mat.transparent=false;mat.opacity=1;} | |
| else if(mode==='mat'){mat.wireframe=false;mat.transparent=mat.opacity<1;} | |
| mat.needsUpdate=true; | |
| }); | |
| }); | |
| } | |
| function togGrid(){GRID.visible=!GRID.visible;toast('Grid: '+(GRID.visible?'on':'off'));} | |
| function toggleStats(){const h=document.getElementById('stats-hud');h.style.display=h.style.display==='none'?'block':'none';} | |
| function toggleAnim(){ANIM_ON=!ANIM_ON;const p=document.getElementById('anim-pill');p.classList.toggle('on',ANIM_ON);toast('Anim: '+(ANIM_ON?'on':'off'));} | |
| // ββ WORLD PRESETS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setWorld(w){ | |
| const W={ | |
| studio:{bg:'#111114',amb:0.5,sun:1.2}, | |
| sunset:{bg:'#1a0808',amb:0.3,sun:2.0}, | |
| night:{bg:'#05060a',amb:0.1,sun:0.2}, | |
| space:{bg:'#000005',amb:0.05,sun:0.3}, | |
| neon:{bg:'#050510',amb:0.3,sun:0.2}, | |
| void:{bg:'#000000',amb:0,sun:0}, | |
| }; | |
| const p=W[w]||W.studio; | |
| SCENE.background.set(p.bg);AMBLIGHT.intensity=p.amb;SUNLIGHT.intensity=p.sun; | |
| if(w==='space')SCENE.fog=null; | |
| toast('World: '+w);closeSheets(); | |
| } | |
| // ββ PARTICLES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function getSprite(type){ | |
| if(SPRITE_CACHE[type])return SPRITE_CACHE[type]; | |
| const cv=document.createElement('canvas');cv.width=cv.height=64;const ctx=cv.getContext('2d'); | |
| const C={fire:['rgba(255,200,0,1)','rgba(255,100,0,.6)','rgba(255,0,0,0)'],smoke:['rgba(180,180,180,.6)','rgba(100,100,100,.3)','rgba(50,50,50,0)'],sparks:['rgba(255,255,200,1)','rgba(255,180,0,.5)','rgba(255,100,0,0)'],explosion:['rgba(255,200,100,1)','rgba(255,80,0,.7)','rgba(0,0,0,0)'],rain:['rgba(150,180,255,.8)','rgba(100,140,255,.3)','rgba(0,0,50,0)'],snow:['rgba(255,255,255,1)','rgba(200,220,255,.5)','rgba(255,255,255,0)'],magic:['rgba(255,150,255,1)','rgba(100,50,255,.6)','rgba(0,0,100,0)'],galaxy:['rgba(255,240,200,1)','rgba(200,180,255,.4)','rgba(0,0,20,0)']}; | |
| const c=C[type]||C.fire;const g=ctx.createRadialGradient(32,32,0,32,32,32);g.addColorStop(0,c[0]);g.addColorStop(.4,c[1]);g.addColorStop(1,c[2]);ctx.fillStyle=g;ctx.fillRect(0,0,64,64); | |
| const tex=new THREE.CanvasTexture(cv);SPRITE_CACHE[type]=tex;return tex; | |
| } | |
| function addParticles(type,cnt,posOvr){ | |
| cnt=Math.min(cnt||1200,MOB?600:2500); | |
| const pos=posOvr||new THREE.Vector3(0,0,0); | |
| const C={fire:{vy:.06,spread:.4,grav:.003,blend:THREE.AdditiveBlending},smoke:{vy:.02,spread:.5,grav:-.001,blend:THREE.NormalBlending},sparks:{vy:.1,spread:1,grav:.004,blend:THREE.AdditiveBlending},explosion:{vy:.15,spread:2,grav:.005,blend:THREE.AdditiveBlending},rain:{vy:-.15,spread:1,grav:.008,blend:THREE.NormalBlending},snow:{vy:-.02,spread:.7,grav:.001,blend:THREE.NormalBlending},magic:{vy:.06,spread:.8,grav:-.001,blend:THREE.AdditiveBlending},galaxy:{vy:.005,spread:10,grav:0,blend:THREE.AdditiveBlending}}; | |
| const cfg=C[type]||C.fire; | |
| const geo=new THREE.BufferGeometry();const pa=new Float32Array(cnt*3);const vels=[],lives=[]; | |
| for(let i=0;i<cnt;i++){pa[i*3]=(Math.random()-.5)*cfg.spread+pos.x;pa[i*3+1]=(Math.random()-.5)*cfg.spread*.5+pos.y;pa[i*3+2]=(Math.random()-.5)*cfg.spread+pos.z;vels.push(new THREE.Vector3((Math.random()-.5)*.4,(Math.random()*.5+.5)*cfg.vy,(Math.random()-.5)*.4));lives.push(Math.floor(Math.random()*100)+1);} | |
| geo.setAttribute('position',new THREE.BufferAttribute(pa,3)); | |
| const mat=new THREE.PointsMaterial({size:.18,map:getSprite(type),transparent:true,opacity:.9,blending:cfg.blend,depthWrite:false,sizeAttenuation:true}); | |
| const pts=new THREE.Points(geo,mat);pts.frustumCulled=false;SCENE.add(pts); | |
| const sys={pts,vels,lives,cfg,cnt,pos:pos.clone(),type};PTCL_SYS.push(sys); | |
| OBJS.push({mesh:pts,light:null,name:'Particles.'+type,type:'MESH',vis:true,kf:{},phys:null}); | |
| refreshOL();refreshSt();toast('Particles: '+type);return sys; | |
| } | |
| let _pf=0; | |
| function updateParticles(){ | |
| if(!PTCL_SYS.length)return;_pf++;if(_pf%2)return; | |
| PTCL_SYS.forEach(s=>{ | |
| const p=s.pts.geometry.attributes.position; | |
| for(let i=0;i<s.cnt;i++){ | |
| s.lives[i]--;if(s.lives[i]<=0){p.setXYZ(i,(Math.random()-.5)*s.cfg.spread+s.pos.x,(Math.random()-.5)*s.cfg.spread*.3+s.pos.y,(Math.random()-.5)*s.cfg.spread+s.pos.z);s.vels[i].set((Math.random()-.5)*.4,(Math.random()*.5+.5)*s.cfg.vy,(Math.random()-.5)*.4);s.lives[i]=Math.floor(Math.random()*100)+1;continue;} | |
| s.vels[i].y+=s.cfg.grav;p.setXYZ(i,p.getX(i)+s.vels[i].x,p.getY(i)+s.vels[i].y,p.getZ(i)+s.vels[i].z); | |
| } | |
| p.needsUpdate=true; | |
| }); | |
| } | |
| // ββ PHYSICS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function addPhysics(type){ | |
| if(!SEL||SEL.type!=='MESH'){toast('Select a mesh first');return;} | |
| const m=SEL.mesh;const init=m.position.clone();const vel=new THREE.Vector3();let fr=0; | |
| const U={gravity:()=>{vel.y+=GRAVITY*.016;m.position.y+=vel.y;if(m.position.y<.5){m.position.y=.5;vel.y*=-.55;}},bounce:()=>{vel.y+=GRAVITY*.016;m.position.add(vel);if(m.position.y<.5){m.position.y=.5;vel.y=Math.abs(vel.y)*.7;}},spin:()=>{m.rotation.y+=.025;},float:()=>{vel.y+=.0018;m.position.y+=vel.y;if(m.position.y>init.y+4){vel.y*=-.8;}},wave:()=>{fr++;m.position.y=init.y+Math.sin(fr*.04)*.9;},orbit:()=>{fr++;m.position.x=init.x+Math.cos(fr*.02)*4;m.position.z=init.z+Math.sin(fr*.02)*4;}}; | |
| SEL.phys={type,update:U[type]||U.spin,vel,init};PHYS_OBJS.push({obj:SEL,phys:SEL.phys}); | |
| toast('Physics: '+type);closeSheets(); | |
| } | |
| function clearPhysics(){PHYS_OBJS.forEach(p=>{p.obj.phys=null;});PHYS_OBJS=[];toast('Physics cleared');closeSheets();} | |
| function updatePhysics(){PHYS_OBJS.forEach(p=>{try{if(p.obj.vis&&p.obj.phys)p.phys.update();}catch(e){}});} | |
| // ββ VFX βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function createVFX(type){ | |
| if(type==='explosion'){addParticles('explosion',1500);addParticles('sparks',600);const fl=new THREE.PointLight(0xff6600,8,15);SCENE.add(fl);let f=0;const a=()=>{f++;fl.intensity=Math.max(0,8-f*.4);if(f<25)requestAnimationFrame(a);else SCENE.remove(fl);};a();toast('π₯ Explosion!');} | |
| else if(type==='lightning'){for(let b=0;b<3;b++){const pts=[];let x=0,y=6;while(y>-2){x+=(Math.random()-.5)*1.2;y-=.5+Math.random()*.5;pts.push(new THREE.Vector3(x+(b-1)*2,y,0));}const ln=new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts),new THREE.LineBasicMaterial({color:0xaaaaff,transparent:true}));SCENE.add(ln);let f=0;const a=()=>{f++;ln.material.opacity=Math.max(0,.9-f*.08);if(ln.material.opacity>0)requestAnimationFrame(a);else SCENE.remove(ln);};a();}toast('β‘ Lightning!');} | |
| else if(type==='portal'){const pg=new THREE.Group();pg.add(new THREE.Mesh(new THREE.TorusGeometry(2,.15,8,60),new THREE.MeshStandardMaterial({color:'#220044',emissive:'#8800ff',emissiveIntensity:3})));pg.position.y=1.5;SCENE.add(pg);addParticles('magic',400,pg.position);const _t=Date.now();(function an(){requestAnimationFrame(an);pg.children[0].rotation.z=(Date.now()-_t)*.001*.8;})();OBJS.push({mesh:pg,light:null,name:'Portal',type:'MESH',vis:true,kf:{}});refreshOL();toast('π Portal!');} | |
| } | |
| function objVFX(type){ | |
| if(!SEL||SEL.type!=='MESH'){toast('Select object first');return;} | |
| const mat=Array.isArray(SEL.mesh.material)?SEL.mesh.material[0]:SEL.mesh.material; | |
| if(type==='ghost'){mat.transparent=true;mat.opacity=.3;mat.color.set('#88aaff');mat.needsUpdate=true;toast('π» Ghost');} | |
| else if(type==='hologram'){mat.color.set('#00ffcc');mat.emissive?.set('#00ffcc');mat.emissiveIntensity=2;mat.transparent=true;mat.opacity=.6;mat.wireframe=true;mat.needsUpdate=true;toast('π΅ Hologram');} | |
| else if(type==='dissolve'){let f=0;const a=()=>{f++;mat.opacity=Math.max(0,1-f*.02);mat.transparent=true;mat.needsUpdate=true;if(mat.opacity>0)requestAnimationFrame(a);else SEL.mesh.visible=false;};a();toast('β‘ Dissolve');} | |
| } | |
| // ββ GIZMO DRAW ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function drawGizmo(){ | |
| const cv=document.getElementById('gizmo-cv');if(!cv)return; | |
| const ctx=cv.getContext('2d');const w=cv.width,h=cv.height; | |
| ctx.clearRect(0,0,w,h); | |
| const dir=CAM.position.clone().sub(new THREE.Vector3(ORB.tx,ORB.ty,ORB.tz)).normalize(); | |
| const axes=[{v:new THREE.Vector3(1,0,0),c:'#ff4444',l:'X'},{v:new THREE.Vector3(0,1,0),c:'#44cc44',l:'Y'},{v:new THREE.Vector3(0,0,1),c:'#4466ff',l:'Z'}]; | |
| const right=new THREE.Vector3();const up=new THREE.Vector3(); | |
| right.crossVectors(new THREE.Vector3(0,1,0),dir).normalize();up.crossVectors(dir,right).normalize(); | |
| axes.forEach(a=>{const d=a.v.dot(dir);const x2=w/2+a.v.dot(right)*22;const y2=h/2-a.v.dot(up)*22;ctx.beginPath();ctx.moveTo(w/2,h/2);ctx.lineTo(x2,y2);ctx.strokeStyle=a.c;ctx.lineWidth=2;ctx.globalAlpha=d<0?.35:1;ctx.stroke();ctx.fillStyle=a.c;ctx.font='9px sans-serif';ctx.fillText(a.l,x2-4,y2+4);ctx.globalAlpha=1;}); | |
| } | |
| // ββ MAIN LOOP βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function loop(){ | |
| requestAnimationFrame(loop); | |
| if(OVX||OVY){OVX*=.88;OVY*=.88;if(Math.abs(OVX)<.0001)OVX=0;if(Math.abs(OVY)<.0001)OVY=0;ORB.th+=OVX;ORB.ph=clamp(ORB.ph+OVY,.05,Math.PI-.05);updCam();} | |
| if(ANIM_ON){updateParticles();updatePhysics();} | |
| updateGizmo(); | |
| RENDERER.render(SCENE,CAM); | |
| _fc++;const now=performance.now();if(now-_fl>=1000){_fps=_fc;_fc=0;_fl=now;document.getElementById('st-f').textContent=_fps;} | |
| } | |
| // ββ SCRIPT RUNNER βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function runSc(){ | |
| const code=document.getElementById('sa').value; | |
| const out=document.getElementById('so');out.style.color='#3a7a3a';out.textContent='Runningβ¦'; | |
| try{ | |
| new Function('SCENE','OBJS','CAM','THREE','toast','addObj','addLight','addParticles','createVFX','addPhysics','objVFX','setWorld','refreshOL','refreshSt','SEL','AMBLIGHT','SUNLIGHT','RENDERER',code)(SCENE,OBJS,CAM,THREE,toast,addObj,addLight,addParticles,createVFX,addPhysics,objVFX,setWorld,refreshOL,refreshSt,SEL,AMBLIGHT,SUNLIGHT,RENDERER); | |
| refreshOL();refreshSt();out.textContent='// β OK '+new Date().toLocaleTimeString(); | |
| }catch(e){out.textContent='// β '+e.message;out.style.color='#c44';} | |
| } | |
| // ββ MODAL / SHEET HELPERS βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function showAddModal(){document.getElementById('add-modal').classList.add('show');} | |
| function hideAddModal(){document.getElementById('add-modal').classList.remove('show');} | |
| function filterAdd(q){ | |
| document.querySelectorAll('#add-mesh-grid .add-item').forEach(el=>{ | |
| el.style.display=!q||el.querySelector('.lbl').textContent.toLowerCase().includes(q.toLowerCase())?'':'none'; | |
| }); | |
| } | |
| function openSheet(id){document.getElementById('sheet-ov').classList.add('show');document.getElementById(id).classList.add('show');} | |
| function closeSheets(){document.getElementById('sheet-ov').classList.remove('show');document.querySelectorAll('.sheet').forEach(s=>s.classList.remove('show'));} | |
| function showAI(){document.getElementById('aip').classList.add('show');setTimeout(()=>document.getElementById('ai-input').focus(),100);} | |
| function hideAI(){document.getElementById('aip').classList.remove('show');} | |
| // ββ AI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function addMsg(txt,type,cls){ | |
| const d=document.createElement('div'); | |
| d.className=type==='user'?'msg-u':'msg-a '+(cls||''); | |
| d.textContent=txt; | |
| document.getElementById('ai-msgs').appendChild(d); | |
| document.getElementById('ai-msgs').scrollTop=99999; | |
| return d; | |
| } | |
| function showThinking(){ | |
| const d=document.createElement('div');d.className='ai-thinking';d.id='ai-think'; | |
| d.innerHTML='<div class="ai-dot"></div><div class="ai-dot"></div><div class="ai-dot"></div>'; | |
| document.getElementById('ai-msgs').appendChild(d); | |
| document.getElementById('ai-msgs').scrollTop=99999; | |
| } | |
| function hideThinking(){const d=document.getElementById('ai-think');if(d)d.remove();} | |
| async function fetchAI(endpoint,messages,tokens){ | |
| try{ | |
| const res=await fetch(endpoint,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({messages,max_tokens:tokens||4096})}); | |
| const data=await res.json(); | |
| if(data.error)return{error:data.error}; | |
| let code=(data.choices?.[0]?.message?.content||'').trim(); | |
| code=code.replace(/^```[\w]*\n?/gm,'').replace(/\n?```$/gm,'').trim(); | |
| return{code,model:data._model_used||endpoint,len:code.length}; | |
| }catch(e){return{error:e.message};} | |
| } | |
| async function sendAI(){ | |
| const inp=document.getElementById('ai-input'); | |
| const msg=inp.value.trim();if(!msg)return; | |
| inp.value='';document.getElementById('ai-send').disabled=true; | |
| addMsg(msg,'user');showThinking(); | |
| let finalCode='',modelUsed=''; | |
| // Always try smart-build first (plan β code) | |
| try{ | |
| addMsg('π Planningβ¦','ai','plan'); | |
| const res=await fetch('/api/smart-build',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt:msg,max_tokens:4096})}); | |
| const data=await res.json(); | |
| hideThinking(); | |
| if(!data.error&&data.choices?.[0]?.message?.content){ | |
| // Show plan preview | |
| if(data._plan){ | |
| const planPreview=data._plan.split('\n').slice(0,6).join('\n'); | |
| addMsg('π Blueprint:\n'+planPreview+'β¦','ai','plan'); | |
| } | |
| finalCode=data.choices[0].message.content.trim().replace(/^```[\w]*\n?/gm,'').replace(/\n?```$/gm,'').trim(); | |
| modelUsed=data._model_used||'smart-build'; | |
| } else { | |
| // Fallback: direct Groq call | |
| addMsg('β© Falling back to direct modeβ¦','ai'); | |
| showThinking(); | |
| const fb=await fetchAI('/api/groq',[{role:'user',content:`Build detailed 3D model of: ${msg}. Use 20+ parts minimum. Output ONLY raw JavaScript.`}]); | |
| hideThinking(); | |
| if(fb.error){addMsg('β '+fb.error,'ai','err');document.getElementById('ai-send').disabled=false;return;} | |
| finalCode=fb.code;modelUsed=fb.model||'groq'; | |
| } | |
| }catch(e){ | |
| hideThinking(); | |
| addMsg('β Network error: '+e.message,'ai','err'); | |
| document.getElementById('ai-send').disabled=false;return; | |
| } | |
| if(!finalCode||finalCode.length<30){ | |
| addMsg('β οΈ Empty response β try again','ai','err'); | |
| document.getElementById('ai-send').disabled=false;return; | |
| } | |
| // Strip re-declarations of globals | |
| finalCode=finalCode.replace(/\b(const|let|var)\s+(SCENE|THREE|OBJS|CAM|AMBLIGHT|SUNLIGHT|RENDERER)\b\s*=/g,'/* exists */ '); | |
| // Save to script editor | |
| document.getElementById('sa').value='// '+modelUsed+' β '+new Date().toLocaleTimeString()+'\n'+finalCode; | |
| // Execute | |
| try{ | |
| new Function('SCENE','OBJS','CAM','THREE','toast','addObj','addLight','addParticles','createVFX','addPhysics','objVFX','setWorld','refreshOL','refreshSt','SEL','AMBLIGHT','SUNLIGHT','RENDERER',finalCode)(SCENE,OBJS,CAM,THREE,toast,addObj,addLight,addParticles,createVFX,addPhysics,objVFX,setWorld,refreshOL,refreshSt,SEL,AMBLIGHT,SUNLIGHT,RENDERER); | |
| refreshOL();refreshSt(); | |
| addMsg('β Built! ('+modelUsed+')','ai','ok'); | |
| }catch(e){ | |
| addMsg('β οΈ JS Error: '+e.message,'ai','err'); | |
| } | |
| document.getElementById('ai-send').disabled=false; | |
| } | |
| function quickAsk(q){showAI();document.getElementById('ai-input').value=q;sendAI();} | |
| async function chkStatus(){ | |
| document.getElementById('ai-status-badge').textContent='β¦'; | |
| try{ | |
| const r=await fetch('/api/status');const d=await r.json(); | |
| const ok=Object.values(d).filter(v=>v.startsWith('β ')).length; | |
| document.getElementById('ai-status-badge').textContent=ok+'/'+Object.keys(d).length+' β '; | |
| addMsg(Object.values(d).join('\n'),'ai','ok'); | |
| }catch(e){document.getElementById('ai-status-badge').textContent='β offline';} | |
| } | |
| function applyTexFile(inp){ | |
| const file=inp.files[0];if(!file||!SEL)return; | |
| const url=URL.createObjectURL(file); | |
| const t=new THREE.TextureLoader().load(url,()=>{t.wrapS=t.wrapT=THREE.RepeatWrapping;t.repeat.set(3,3);const mat=getMat();if(mat){mat.map=t;mat.needsUpdate=true;}toast('Texture applied');}); | |
| } | |
| // ββ HELPERS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function toast(msg,dur=3000){const t=document.getElementById('toast');t.textContent=msg;t.style.display='block';clearTimeout(t._tid);t._tid=setTimeout(()=>t.style.display='none',dur);} | |
| function addBoneRig(s){toast('Rig: '+s+' (visual)');} | |
| function drawTL(){}function applyKF(){}function applyPre(){}function setFr(){} | |
| const FR_FPS=25,FR_END=250;const KF={}; | |
| </script> | |
| </body> | |
| </html> | |