forge3d / static /index.html
VISHAL18for4's picture
Upload index.html
c0eb200 verified
Raw
History Blame Contribute Delete
75.1 kB
<!DOCTYPE html>
<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>
'use strict';
// ── 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>