VISHAL18for4 commited on
Commit
d07bbda
·
verified ·
1 Parent(s): 151c02a

Delete static/index.html

Browse files
Files changed (1) hide show
  1. static/index.html +0 -1200
static/index.html DELETED
@@ -1,1200 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
6
- <title>FORGE3D v4.0 — AI Edition</title>
7
- <style>
8
- *{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
9
- html,body{width:100%;height:100%;overflow:hidden;background:#1a1a1a;font-family:'Segoe UI',system-ui,sans-serif;font-size:12px;color:#ccc;user-select:none;}
10
- #app{display:flex;flex-direction:column;width:100%;height:100%;}
11
- #topbar{height:30px;background:#111;border-bottom:1px solid #000;display:flex;align-items:stretch;flex-shrink:0;z-index:300;}
12
- .logo{display:flex;align-items:center;gap:5px;padding:0 10px;border-right:1px solid #222;flex-shrink:0;}
13
- .logo-sq{width:18px;height:18px;background:#e87d39;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:900;color:#fff;}
14
- .logo-name{font-weight:700;font-size:12px;color:#ddd;letter-spacing:1px;}
15
- .logo-home{padding:0 8px;display:flex;align-items:center;color:#555;cursor:pointer;border-right:1px solid #222;text-decoration:none;font-size:11px;}
16
- .logo-home:hover{color:#e87d39;}
17
- .mitem{position:relative;display:flex;align-items:center;}
18
- .mbtn{height:100%;padding:0 10px;background:none;border:none;color:#bbb;cursor:pointer;font-size:12px;font-family:inherit;}
19
- .mbtn:hover,.mbtn.open{background:#2a2a2a;color:#fff;}
20
- .mdrop{display:none;position:fixed;top:30px;background:#252525;border:1px solid #3a3a3a;border-radius:5px;min-width:200px;z-index:9999;box-shadow:0 8px 32px rgba(0,0,0,.85);padding:3px 0;}
21
- .mdrop.show{display:block;}
22
- .mdi{display:flex;justify-content:space-between;align-items:center;padding:6px 14px;cursor:pointer;font-size:12px;color:#bbb;}
23
- .mdi:hover{background:#3a6ea8;color:#fff;}
24
- .mdi .ks{color:#555;font-size:10px;}.mdi:hover .ks{color:#aaa;}
25
- .msep{height:1px;background:#333;margin:3px 0;}
26
- .msub{padding:3px 14px;color:#555;font-size:10px;}
27
- #modebar{height:32px;background:#1e1e1e;border-bottom:1px solid #111;display:flex;align-items:center;padding:0 6px;gap:2px;flex-shrink:0;}
28
- .modebtn{padding:3px 9px;background:#2a2a2a;border:1px solid #383838;border-radius:4px;color:#aaa;cursor:pointer;font-size:12px;font-family:inherit;transition:all .1s;}
29
- .modebtn:hover{background:#333;color:#ddd;}.modebtn.on{background:#e87d39;border-color:#e87d39;color:#fff;}
30
- .msep2{width:1px;height:18px;background:#333;margin:0 4px;flex-shrink:0;}
31
- .shading-row{display:flex;gap:2px;margin-left:auto;}
32
- .shdbtn{width:26px;height:24px;background:#2a2a2a;border:1px solid #383838;border-radius:4px;cursor:pointer;color:#aaa;font-size:13px;display:flex;align-items:center;justify-content:center;}
33
- .shdbtn:hover{background:#333;}.shdbtn.on{background:#3a6ea8;border-color:#4a80c0;color:#fff;}
34
- #tabbar{height:26px;background:#161616;border-bottom:1px solid #111;display:flex;align-items:flex-end;padding:0 8px;gap:1px;flex-shrink:0;}
35
- .tbtab{padding:3px 10px;cursor:pointer;border-radius:4px 4px 0 0;font-size:11px;color:#666;border:1px solid transparent;border-bottom:none;position:relative;top:1px;}
36
- .tbtab:hover{color:#bbb;background:#1e1e1e;}.tbtab.on{color:#ddd;background:#1e1e1e;border-color:#333;border-bottom:1px solid #1e1e1e;}
37
- #main{flex:1;display:flex;min-height:0;overflow:hidden;}
38
- #ltbar{width:38px;background:#1a1a1a;border-right:1px solid #111;display:flex;flex-direction:column;align-items:center;padding:3px 0;gap:1px;flex-shrink:0;overflow:hidden;}
39
- .tbtn{width:34px;height:32px;background:transparent;border:1px solid transparent;border-radius:4px;cursor:pointer;color:#888;font-size:15px;display:flex;align-items:center;justify-content:center;transition:all .1s;flex-shrink:0;}
40
- .tbtn:hover{background:#2a2a2a;color:#ccc;}.tbtn.on{background:#e87d39;color:#fff;border-color:#e87d39;}
41
- .tsep{width:30px;height:1px;background:#2a2a2a;margin:1px 0;flex-shrink:0;}
42
- #vpcontainer{flex:1;display:flex;flex-direction:column;min-width:0;min-height:0;}
43
- .tabcontent{flex:1;display:none;min-height:0;overflow:hidden;}
44
- .tabcontent.on{display:flex;flex-direction:column;}
45
- #vpwrap{flex:1;position:relative;overflow:hidden;min-height:0;background:#3d3d3d;}
46
- #c{display:block;width:100%;height:100%;touch-action:none;}
47
- #vbadge{position:absolute;top:8px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.65);padding:3px 14px;border-radius:20px;font-size:11px;color:#ddd;pointer-events:none;z-index:10;white-space:nowrap;}
48
- #vcorner{position:absolute;top:8px;right:8px;display:flex;gap:3px;z-index:10;}
49
- .vcbtn{width:26px;height:26px;background:rgba(0,0,0,.5);border:1px solid rgba(255,255,255,.12);border-radius:4px;cursor:pointer;color:#bbb;font-size:13px;display:flex;align-items:center;justify-content:center;}
50
- .vcbtn:hover{background:rgba(255,255,255,.15);color:#fff;}
51
- #gizmo{position:absolute;bottom:12px;right:12px;width:68px;height:68px;z-index:10;pointer-events:none;}
52
- #rpanel{width:240px;background:#1e1e1e;border-left:1px solid #111;display:flex;flex-direction:column;flex-shrink:0;transition:width .15s;overflow:hidden;}
53
- #rpanel.hidden{width:0;}
54
- .rptabs{display:flex;background:#111;border-bottom:1px solid #111;flex-shrink:0;overflow-x:auto;}
55
- .rptab{padding:5px 7px;cursor:pointer;font-size:11px;color:#666;border-bottom:2px solid transparent;white-space:nowrap;flex-shrink:0;}
56
- .rptab.on{color:#ddd;border-bottom-color:#e87d39;}.rptab:hover{color:#bbb;}
57
- .rpbody{flex:1;overflow-y:auto;display:none;}.rpbody.on{display:block;}
58
- .olitem{display:flex;align-items:center;padding:4px 8px;gap:5px;cursor:pointer;border-bottom:1px solid rgba(255,255,255,.03);}
59
- .olitem:hover{background:#2a2a2a;}.olitem.sel{background:#1f3d6e;}
60
- .eye{cursor:pointer;opacity:.5;font-size:11px;margin-left:auto;}.eye:hover{opacity:1;}
61
- .olbtns{display:flex;flex-wrap:wrap;gap:4px;padding:6px;border-top:1px solid #222;}
62
- .olbtn{padding:3px 7px;background:#2a2a2a;border:1px solid #3a3a3a;border-radius:4px;color:#bbb;cursor:pointer;font-size:11px;font-family:inherit;}
63
- .olbtn:hover{background:#333;}.olbtn.red:hover{background:#7a1a1a;color:#f55;}
64
- .psec{border-bottom:1px solid #111;}
65
- .phdr{padding:5px 8px;background:#161616;cursor:pointer;font-size:11px;font-weight:600;color:#bbb;}
66
- .pbody{padding:7px;display:grid;grid-template-columns:auto 1fr;gap:4px 8px;align-items:center;}
67
- .plbl{color:#666;font-size:11px;white-space:nowrap;}
68
- .pinp{background:#111;border:1px solid #2a2a2a;border-radius:3px;color:#ddd;padding:3px 5px;font-size:11px;width:100%;font-family:inherit;}
69
- .pinp:focus{outline:none;border-color:#e87d39;}.pinp[readonly]{opacity:.5;}
70
- .pxyz{display:grid;grid-template-columns:1fr 1fr 1fr;gap:2px;}
71
- .pxyz .pinp{font-size:10px;padding:2px 3px;}
72
- .pxyzlbl{display:grid;grid-template-columns:1fr 1fr 1fr;gap:2px;margin-bottom:1px;}
73
- .px{color:#e44;font-size:9px;text-align:center;}.py{color:#4e4;font-size:9px;text-align:center;}.pz{color:#55f;font-size:9px;text-align:center;}
74
- .render-section{padding:8px;border-bottom:1px solid #111;}
75
- .render-section h3{font-size:11px;color:#888;margin-bottom:6px;font-weight:600;}
76
- .render-row{display:flex;align-items:center;gap:6px;margin-bottom:5px;}
77
- .render-row .plbl{width:80px;flex-shrink:0;}
78
- .render-btn{width:100%;padding:7px;background:#e87d39;border:none;border-radius:5px;color:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;margin-top:4px;}
79
- .render-btn:hover{background:#ff9955;}
80
- .sculpt-tools{display:flex;flex-wrap:wrap;gap:4px;padding:8px;}
81
- .sculpt-btn{padding:5px 8px;background:#2a2a2a;border:1px solid #3a3a3a;border-radius:4px;color:#bbb;cursor:pointer;font-size:11px;font-family:inherit;flex:1;text-align:center;min-width:70px;}
82
- .sculpt-btn:hover{background:#333;}.sculpt-btn.on{background:#3a6ea8;color:#fff;}
83
- .sc-toolbar{height:28px;background:#1a1a1a;border-bottom:1px solid #222;display:flex;align-items:center;padding:0 8px;gap:5px;flex-shrink:0;}
84
- .sc-btn{padding:2px 8px;background:#252525;border:1px solid #333;border-radius:3px;color:#bbb;font-size:11px;cursor:pointer;font-family:inherit;}
85
- .sc-btn:hover{background:#333;color:#fff;}
86
- #scriptarea{flex:1;background:#0d0d0d;color:#abb2bf;font-size:12px;font-family:'Courier New',monospace;padding:10px;border:none;outline:none;resize:none;line-height:1.6;}
87
- #scriptout{height:80px;background:#080808;color:#888;font-size:11px;font-family:monospace;padding:6px;border-top:1px solid #222;overflow-y:auto;}
88
- #timeline-wrap{position:relative;background:#1a1a1a;border-top:1px solid #111;flex-shrink:0;display:flex;flex-direction:column;height:88px;}
89
- #tl-resize{height:5px;cursor:ns-resize;background:transparent;position:absolute;top:0;left:0;right:0;z-index:20;}
90
- #tl-resize:hover{background:rgba(232,125,57,.3);}
91
- #tlctrl{height:28px;display:flex;align-items:center;padding:0 6px;gap:2px;background:#161616;border-bottom:1px solid #111;flex-shrink:0;overflow-x:auto;}
92
- .tlbtn{width:24px;height:22px;background:#252525;border:1px solid #333;border-radius:3px;cursor:pointer;color:#888;font-size:12px;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
93
- .tlbtn:hover{background:#333;color:#ddd;}.tlbtn.on{background:#e87d39;border-color:#e87d39;color:#fff;}
94
- .tlinp{width:44px;padding:2px 4px;background:#111;border:1px solid #333;border-radius:3px;color:#ddd;font-size:11px;text-align:center;font-family:inherit;flex-shrink:0;}
95
- .tlsep{width:1px;height:16px;background:#333;margin:0 3px;flex-shrink:0;}
96
- .tlkbtn{padding:2px 6px;background:#252525;border:1px solid #333;border-radius:3px;color:#888;font-size:11px;cursor:pointer;font-family:inherit;flex-shrink:0;}
97
- .tlkbtn:hover{background:#e87d39;color:#fff;border-color:#e87d39;}
98
- #tltrack{flex:1;display:flex;min-height:0;overflow:hidden;}
99
- #tlchlbls{width:60px;background:#111;border-right:1px solid #222;display:flex;flex-direction:column;justify-content:space-around;padding:1px 4px;flex-shrink:0;}
100
- .tlchlbl{font-size:9px;}
101
- #tlcanvas{flex:1;display:block;cursor:crosshair;}
102
- #statusbar{height:18px;background:#0d0d0d;border-top:1px solid #000;display:flex;align-items:center;padding:0 8px;gap:10px;font-size:10px;flex-shrink:0;color:#555;}
103
- .stv{color:#e87d39;font-weight:700;}.stv2{color:#777;}
104
- #ctx{position:fixed;background:#252525;border:1px solid #3a3a3a;border-radius:5px;z-index:9999;display:none;box-shadow:0 8px 28px rgba(0,0,0,.85);padding:3px 0;min-width:160px;}
105
- #ctx.show{display:block;}
106
- .cxi{padding:6px 14px;cursor:pointer;font-size:12px;color:#bbb;display:flex;justify-content:space-between;}
107
- .cxi:hover{background:#3a6ea8;color:#fff;}
108
- #toast{position:fixed;bottom:90px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.88);color:#ddd;padding:5px 16px;border-radius:20px;font-size:12px;z-index:9999;display:none;pointer-events:none;border:1px solid rgba(255,255,255,.1);white-space:nowrap;max-width:90%;}
109
- #ai-fab{position:fixed;right:20px;bottom:50px;width:50px;height:50px;background:linear-gradient(135deg,#e87d39,#a855f7);border:none;border-radius:50%;color:#fff;font-size:22px;cursor:pointer;z-index:9998;box-shadow:0 4px 20px rgba(168,85,247,.5);transition:transform .2s;}
110
- #ai-fab:hover{transform:scale(1.1);}
111
- #ai-panel{position:fixed;right:16px;bottom:110px;width:360px;background:#141414;border:1px solid #3a3a3a;border-radius:12px;z-index:9998;display:none;flex-direction:column;box-shadow:0 12px 48px rgba(0,0,0,.9);max-height:80vh;}
112
- #ai-panel.open{display:flex;}
113
- #ai-hdr{padding:10px 14px;background:linear-gradient(135deg,#e87d39,#a855f7);border-radius:11px 11px 0 0;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;}
114
- #ai-status{font-size:10px;background:rgba(0,0,0,.3);padding:2px 8px;border-radius:10px;color:#ddd;}
115
- .ai-mtabs{display:flex;background:#0d0d0d;border-bottom:1px solid #222;flex-shrink:0;}
116
- .ai-mtab{flex:1;padding:6px 3px;text-align:center;cursor:pointer;font-size:10px;color:#555;border-bottom:2px solid transparent;line-height:1.4;}
117
- .ai-mtab:hover{color:#bbb;}.ai-mtab.on{color:#fff;border-bottom-color:#e87d39;}
118
- .ai-qrow{display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid #1a1a1a;flex-shrink:0;}
119
- .ai-qbtn{flex:1;padding:5px 3px;background:#1a1a1a;border:1px solid #252525;border-radius:6px;color:#777;font-size:10px;cursor:pointer;text-align:center;}
120
- .ai-qbtn:hover{background:#252525;color:#ddd;border-color:#e87d39;}
121
- #ai-msgs{flex:1;overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:8px;min-height:80px;max-height:240px;}
122
- .ai-mu{background:#e87d39;color:#fff;align-self:flex-end;padding:7px 11px;border-radius:12px 12px 3px 12px;max-width:85%;word-break:break-word;font-size:11px;}
123
- .ai-ma{background:#1e1e1e;color:#ccc;align-self:flex-start;padding:7px 11px;border-radius:12px 12px 12px 3px;max-width:90%;word-break:break-word;font-size:11px;border:1px solid #2a2a2a;white-space:pre-wrap;}
124
- .ai-ma.groq{border-left:3px solid #ff6b35;}.ai-ma.nemotron{border-left:3px solid #76b900;}
125
- .ai-ma.openrouter{border-left:3px solid #a855f7;}.ai-ma.err{border-left:3px solid #e44;color:#f88;}
126
- .ai-think{display:flex;gap:4px;align-items:center;padding:6px 10px;}
127
- .ai-dot{width:6px;height:6px;border-radius:50%;background:#e87d39;animation:bop .8s infinite;}
128
- .ai-dot:nth-child(2){animation-delay:.15s;}.ai-dot:nth-child(3){animation-delay:.3s;}
129
- @keyframes bop{0%,80%,100%{transform:scale(.8);opacity:.5;}40%{transform:scale(1.2);opacity:1;}}
130
- #ai-irow{padding:8px;border-top:1px solid #1a1a1a;display:flex;gap:6px;flex-shrink:0;}
131
- #ai-inp{flex:1;padding:7px 10px;background:#0d0d0d;border:1px solid #2a2a2a;border-radius:8px;color:#ddd;font-size:12px;font-family:inherit;outline:none;}
132
- #ai-inp:focus{border-color:#e87d39;}
133
- #ai-send{padding:7px 14px;background:#e87d39;border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:12px;font-weight:600;font-family:inherit;}
134
- #ai-send:hover{background:#ff9955;}#ai-send:disabled{opacity:.5;cursor:default;}
135
- #ai-sugs{padding:0 8px 8px;display:flex;flex-wrap:wrap;gap:4px;flex-shrink:0;}
136
- .ai-sug{padding:3px 8px;background:#1a1a1a;border:1px solid #252525;border-radius:12px;color:#666;font-size:10px;cursor:pointer;}
137
- .ai-sug:hover{background:#252525;color:#ddd;border-color:#e87d39;}
138
- #ai-pre{background:#080808;border-top:1px solid #111;padding:5px 8px;font-size:10px;font-family:monospace;color:#4e4;max-height:50px;overflow:hidden;display:none;flex-shrink:0;}
139
- #tex-dlg{background:#0f0f0f;border-top:1px solid #222;padding:10px;display:none;flex-shrink:0;}
140
- #tex-dlg.show{display:block;}
141
- .tex-row{display:flex;gap:6px;margin-bottom:7px;}
142
- #tex-inp{flex:1;padding:6px 9px;background:#1a1a1a;border:1px solid #252525;border-radius:6px;color:#ddd;font-size:11px;font-family:inherit;outline:none;}
143
- #tex-inp:focus{border-color:#e87d39;}
144
- #tex-go{padding:6px 12px;background:#3a6ea8;border:none;border-radius:6px;color:#fff;cursor:pointer;font-size:11px;font-family:inherit;}
145
- #tex-go:hover{background:#4a80c0;}
146
- #af{padding:10px;display:none;flex-direction:column;gap:7px;border-top:1px solid #1a1a1a;background:#0a0a0a;flex-shrink:0;overflow-y:auto;max-height:300px;}
147
- #af.show{display:flex;}
148
- .af-row{display:flex;align-items:flex-start;gap:8px;}
149
- .af-lbl{color:#555;font-size:10px;width:90px;flex-shrink:0;padding-top:4px;}
150
- .af-inp{flex:1;padding:4px 7px;background:#111;border:1px solid #252525;border-radius:5px;color:#ddd;font-size:11px;font-family:inherit;outline:none;}
151
- .af-inp:focus{border-color:#e87d39;}
152
- .af-hdr{color:#aaa;font-size:11px;font-weight:600;border-bottom:1px solid #1a1a1a;padding-bottom:5px;display:flex;justify-content:space-between;align-items:center;}
153
- .af-gen{width:100%;padding:8px;background:linear-gradient(135deg,#e87d39,#a855f7);border:none;border-radius:7px;color:#fff;font-size:12px;font-weight:700;cursor:pointer;margin-top:2px;font-family:inherit;}
154
- .af-gen:hover{opacity:.9;}
155
- ::-webkit-scrollbar{width:4px;height:4px;}::-webkit-scrollbar-track{background:#111;}::-webkit-scrollbar-thumb{background:#333;border-radius:2px;}
156
- @media(max-width:500px){.logo-name{display:none;}#rpanel{width:200px;}#ai-panel{width:calc(100vw - 20px);right:10px;}}
157
- </style>
158
- </head>
159
- <body>
160
- <div id="app">
161
- <div id="topbar">
162
- <div class="logo"><div class="logo-sq">F</div><div class="logo-name">FORGE3D</div></div>
163
- <a class="logo-home" href="/" title="Home">🏠</a>
164
- <div class="mitem"><button class="mbtn" onclick="openMenu(this,'md0')">File</button>
165
- <div class="mdrop" id="md0">
166
- <div class="mdi" onclick="act('newScene')">New Scene<span class="ks">Ctrl+N</span></div>
167
- <div class="msep"></div>
168
- <div class="mdi" onclick="act('saveRender')">Render PNG<span class="ks">F12</span></div>
169
- <div class="mdi" onclick="act('exportObj')">Export OBJ</div>
170
- </div></div>
171
- <div class="mitem"><button class="mbtn" onclick="openMenu(this,'md1')">Edit</button>
172
- <div class="mdrop" id="md1">
173
- <div class="mdi" onclick="act('undo')">Undo<span class="ks">Ctrl+Z</span></div>
174
- <div class="msep"></div>
175
- <div class="mdi" onclick="act('dup')">Duplicate<span class="ks">Ctrl+D</span></div>
176
- <div class="mdi" onclick="act('del')">Delete<span class="ks">Del</span></div>
177
- </div></div>
178
- <div class="mitem"><button class="mbtn" onclick="openMenu(this,'md2')">Add</button>
179
- <div class="mdrop" id="md2">
180
- <div class="msub">── MESH ──</div>
181
- <div class="mdi" onclick="addObj('cube');closeMenus()">▣ Cube</div>
182
- <div class="mdi" onclick="addObj('sphere');closeMenus()">⬤ Sphere</div>
183
- <div class="mdi" onclick="addObj('cylinder');closeMenus()">▬ Cylinder</div>
184
- <div class="mdi" onclick="addObj('plane');closeMenus()">▭ Plane</div>
185
- <div class="mdi" onclick="addObj('cone');closeMenus()">▲ Cone</div>
186
- <div class="mdi" onclick="addObj('torus');closeMenus()">◎ Torus</div>
187
- <div class="mdi" onclick="addObj('ico');closeMenus()">◇ Icosphere</div>
188
- <div class="mdi" onclick="addObj('torusknot');closeMenus()">✦ Torus Knot</div>
189
- <div class="msep"></div>
190
- <div class="mdi" onclick="addLight('point');closeMenus()">💡 Point Light</div>
191
- <div class="mdi" onclick="addLight('sun');closeMenus()">☀ Sun Light</div>
192
- <div class="mdi" onclick="addLight('spot');closeMenus()">🔦 Spot Light</div>
193
- </div></div>
194
- <div class="mitem"><button class="mbtn" onclick="openMenu(this,'md3')">Object</button>
195
- <div class="mdrop" id="md3">
196
- <div class="mdi" onclick="act('dup')">Duplicate</div>
197
- <div class="mdi" onclick="act('del')">Delete</div>
198
- <div class="msep"></div>
199
- <div class="mdi" onclick="act('resetLoc')">Reset Location</div>
200
- <div class="mdi" onclick="act('resetRot')">Reset Rotation</div>
201
- <div class="mdi" onclick="act('resetScale')">Reset Scale</div>
202
- <div class="mdi" onclick="act('mirror_x')">Mirror X</div>
203
- <div class="mdi" onclick="act('snapGround')">Snap to Ground</div>
204
- <div class="msep"></div>
205
- <div class="mdi" onclick="act('focus')">Focus Selected<span class="ks">.</span></div>
206
- </div></div>
207
- <div class="mitem"><button class="mbtn" onclick="openMenu(this,'md4')">Render</button>
208
- <div class="mdrop" id="md4">
209
- <div class="mdi" onclick="act('saveRender')">Render Image<span class="ks">F12</span></div>
210
- <div class="msep"></div>
211
- <div class="mdi" onclick="setShading('solid');closeMenus()">◼ Solid</div>
212
- <div class="mdi" onclick="setShading('wire');closeMenus()">⬡ Wireframe</div>
213
- <div class="mdi" onclick="setShading('mat');closeMenus()">● Material Preview</div>
214
- <div class="mdi" onclick="setShading('xray');closeMenus()">◈ X-Ray</div>
215
- </div></div>
216
- <div class="mitem"><button class="mbtn" onclick="openMenu(this,'md5')">Window</button>
217
- <div class="mdrop" id="md5">
218
- <div class="mdi" onclick="toggleRPanel();closeMenus()">Toggle Side Panel</div>
219
- <div class="mdi" onclick="toggleTL();closeMenus()">Toggle Timeline</div>
220
- <div class="mdi" onclick="toggleAI();closeMenus()">Toggle AI</div>
221
- <div class="msep"></div>
222
- <div class="mdi" onclick="switchTab('viewport');closeMenus()">🎬 3D Viewport</div>
223
- <div class="mdi" onclick="switchTab('script');closeMenus()">📝 Script Editor</div>
224
- </div></div>
225
- <div class="mitem"><button class="mbtn" onclick="openMenu(this,'md6')">Help</button>
226
- <div class="mdrop" id="md6">
227
- <div class="mdi" onclick="showHelp();closeMenus()">Keyboard Shortcuts</div>
228
- <div class="mdi" onclick="checkStatus();closeMenus()">Check AI Status</div>
229
- </div></div>
230
- </div>
231
- <div id="modebar">
232
- <button class="modebtn on" id="mb-object" onclick="setMode('object')">Object Mode</button>
233
- <button class="modebtn" id="mb-edit" onclick="setMode('edit')">Edit Mode</button>
234
- <button class="modebtn" id="mb-sculpt" onclick="setMode('sculpt')">Sculpt Mode</button>
235
- <div class="msep2"></div>
236
- <select id="pivotSel" style="padding:2px 5px;background:#2a2a2a;border:1px solid #383838;border-radius:4px;color:#aaa;font-size:11px;font-family:inherit;">
237
- <option>Median Point</option><option>Individual</option><option>Active</option>
238
- </select>
239
- <div class="shading-row">
240
- <button class="shdbtn on" id="shd-solid" onclick="setShading('solid')" title="Solid">◼</button>
241
- <button class="shdbtn" id="shd-wire" onclick="setShading('wire')" title="Wireframe">⬡</button>
242
- <button class="shdbtn" id="shd-mat" onclick="setShading('mat')" title="Material">●</button>
243
- <button class="shdbtn" id="shd-xray" onclick="setShading('xray')" title="X-Ray">◈</button>
244
- </div>
245
- </div>
246
- <div id="tabbar">
247
- <div class="tbtab on" id="tab-viewport" onclick="switchTab('viewport')">🎬 3D Viewport</div>
248
- <div class="tbtab" id="tab-script" onclick="switchTab('script')">📝 Script</div>
249
- <div class="tbtab" id="tab-shader" onclick="switchTab('shader')">⬡ Shader</div>
250
- </div>
251
- <div id="main">
252
- <div id="ltbar">
253
- <button class="tbtn on" id="t-select" onclick="setTool('select')" title="Select (W)">↖</button>
254
- <button class="tbtn" id="t-move" onclick="setTool('move')" title="Move (G)">✥</button>
255
- <button class="tbtn" id="t-rotate" onclick="setTool('rotate')" title="Rotate (R)">↻</button>
256
- <button class="tbtn" id="t-scale" onclick="setTool('scale')" title="Scale (S)">⤡</button>
257
- <div class="tsep"></div>
258
- <button class="tbtn" id="t-grab_x" onclick="setTool('grab_x')" title="Move X">↔X</button>
259
- <button class="tbtn" id="t-grab_y" onclick="setTool('grab_y')" title="Move Y">↕Y</button>
260
- <button class="tbtn" id="t-grab_z" onclick="setTool('grab_z')" title="Move Z">↕Z</button>
261
- <div class="tsep"></div>
262
- <button class="tbtn" onclick="act('focus')" title="Focus (.)">◎</button>
263
- <button class="tbtn" onclick="act('viewAll')" title="View All">⊟</button>
264
- <button class="tbtn" onclick="act('toggleOrtho')" title="Ortho (5)">📐</button>
265
- <div class="tsep"></div>
266
- <button class="tbtn" onclick="setView('front')" title="Front (1)">①</button>
267
- <button class="tbtn" onclick="setView('side')" title="Side (3)">③</button>
268
- <button class="tbtn" onclick="setView('top')" title="Top (7)">⑦</button>
269
- <div class="tsep"></div>
270
- <button class="tbtn" onclick="addObj('cube')" title="Add Cube">▣</button>
271
- <button class="tbtn" onclick="act('del')" title="Delete" style="color:#c55;">✕</button>
272
- <div class="tsep"></div>
273
- <button class="tbtn" onclick="toggleAI()" title="AI Assistant" style="color:#a855f7;font-size:17px;">🤖</button>
274
- </div>
275
- <div id="vpcontainer">
276
- <div class="tabcontent on" id="tab-viewport-content">
277
- <div id="vpwrap">
278
- <canvas id="c"></canvas>
279
- <div id="vbadge">Object Mode · Select · RMB=Orbit · Scroll=Zoom · Shift+RMB=Pan</div>
280
- <div id="vcorner">
281
- <button class="vcbtn" onclick="act('focus')" title="Focus">◎</button>
282
- <button class="vcbtn" onclick="act('toggleOrtho')" title="Ortho">📐</button>
283
- <button class="vcbtn" onclick="toggleRPanel()" title="Panel">▶</button>
284
- <button class="vcbtn" onclick="toggleAI()" style="color:#a855f7;" title="AI">🤖</button>
285
- </div>
286
- <canvas id="gizmo" width="68" height="68"></canvas>
287
- </div>
288
- </div>
289
- <div class="tabcontent" id="tab-script-content">
290
- <div style="flex:1;display:flex;flex-direction:column;">
291
- <div class="sc-toolbar">
292
- <button class="sc-btn" onclick="runScript()">▶ Run</button>
293
- <button class="sc-btn" onclick="clearScript()">Clear</button>
294
- <button class="sc-btn" onclick="insTpl('cubes')">🎲 Cubes</button>
295
- <button class="sc-btn" onclick="insTpl('solar')">🌍 Solar System</button>
296
- <button class="sc-btn" onclick="insTpl('blackhole')">🌑 Black Hole</button>
297
- <button class="sc-btn" onclick="insTpl('missile')">🚀 Missile</button>
298
- <span style="margin-left:auto;color:#333;font-size:10px;padding-right:4px;">addObj()·addLight()·act()·toast()·SCENE·THREE·OBJS</span>
299
- </div>
300
- <textarea id="scriptarea" spellcheck="false">// FORGE3D Script Editor — AI code lands here automatically!
301
- // API: addObj(type), addLight(type), act(cmd), toast(msg), SCENE, THREE, OBJS, KF
302
-
303
- for(let i=0;i<5;i++){
304
- const o=addObj('cube');
305
- o.mesh.position.set((Math.random()-.5)*10,Math.random()*4+.5,(Math.random()-.5)*10);
306
- o.mesh.material.color.setHSL(i/5,.7,.5);
307
- o.mesh.material.metalness=0.5; o.mesh.material.roughness=0.3;
308
- }
309
- toast('5 cubes!');</textarea>
310
- <div id="scriptout">// Console output</div>
311
- </div>
312
- </div>
313
- <div class="tabcontent" id="tab-shader-content">
314
- <div style="flex:1;display:flex;align-items:center;justify-content:center;background:#0d0d0d;color:#444;flex-direction:column;gap:10px;">
315
- <span style="font-size:28px;">⬡</span><span>Shader Node Editor — use Material tab for PBR presets</span>
316
- </div>
317
- </div>
318
- </div>
319
- <div id="rpanel">
320
- <div class="rptabs">
321
- <div class="rptab on" id="rt-out" onclick="setRTab('out')">Outliner</div>
322
- <div class="rptab" id="rt-props" onclick="setRTab('props')">Props</div>
323
- <div class="rptab" id="rt-mat" onclick="setRTab('mat')">Material</div>
324
- <div class="rptab" id="rt-render" onclick="setRTab('render')">Render</div>
325
- <div class="rptab" id="rt-sculpt" onclick="setRTab('sculpt')">Sculpt</div>
326
- </div>
327
- <div class="rpbody on" id="rb-out">
328
- <div style="padding:5px;border-bottom:1px solid #111;">
329
- <input type="text" placeholder="🔍 Filter..." oninput="filterOL(this.value)"
330
- style="width:100%;padding:3px 7px;background:#111;border:1px solid #333;border-radius:4px;color:#ddd;font-size:11px;font-family:inherit;">
331
- </div>
332
- <div id="ollist"></div>
333
- <div class="olbtns">
334
- <button class="olbtn" onclick="addObj('cube')">+Cube</button>
335
- <button class="olbtn" onclick="addObj('sphere')">+Sphere</button>
336
- <button class="olbtn" onclick="addObj('plane')">+Plane</button>
337
- <button class="olbtn" onclick="addLight('point')">+Light</button>
338
- <button class="olbtn" onclick="act('dup')">Dup</button>
339
- <button class="olbtn red" onclick="act('del')">🗑</button>
340
- </div>
341
- </div>
342
- <div class="rpbody" id="rb-props">
343
- <div id="pno" style="padding:20px;color:#555;text-align:center;font-size:11px;">No object selected</div>
344
- <div id="pbody" style="display:none;">
345
- <div class="psec"><div class="phdr" onclick="toggleSec('s-obj')">▾ Object</div>
346
- <div class="pbody" id="s-obj">
347
- <div class="plbl">Name</div><input class="pinp" id="pName" oninput="setProp('name',this.value)">
348
- <div class="plbl">Type</div><input class="pinp" id="pType" readonly>
349
- </div></div>
350
- <div class="psec"><div class="phdr" onclick="toggleSec('s-tr')">▾ Transform</div>
351
- <div class="pbody" id="s-tr">
352
- <div class="plbl">Location</div>
353
- <div><div class="pxyzlbl"><span class="px">X</span><span class="py">Y</span><span class="pz">Z</span></div>
354
- <div class="pxyz">
355
- <input class="pinp" id="pLX" type="number" step="0.1" oninput="setProp('lx',+this.value)">
356
- <input class="pinp" id="pLY" type="number" step="0.1" oninput="setProp('ly',+this.value)">
357
- <input class="pinp" id="pLZ" type="number" step="0.1" oninput="setProp('lz',+this.value)">
358
- </div></div>
359
- <div class="plbl">Rotation°</div>
360
- <div><div class="pxyzlbl"><span class="px">X</span><span class="py">Y</span><span class="pz">Z</span></div>
361
- <div class="pxyz">
362
- <input class="pinp" id="pRX" type="number" step="1" oninput="setProp('rx',+this.value)">
363
- <input class="pinp" id="pRY" type="number" step="1" oninput="setProp('ry',+this.value)">
364
- <input class="pinp" id="pRZ" type="number" step="1" oninput="setProp('rz',+this.value)">
365
- </div></div>
366
- <div class="plbl">Scale</div>
367
- <div><div class="pxyzlbl"><span class="px">X</span><span class="py">Y</span><span class="pz">Z</span></div>
368
- <div class="pxyz">
369
- <input class="pinp" id="pSX" type="number" step="0.1" oninput="setProp('sx',+this.value)">
370
- <input class="pinp" id="pSY" type="number" step="0.1" oninput="setProp('sy',+this.value)">
371
- <input class="pinp" id="pSZ" type="number" step="0.1" oninput="setProp('sz',+this.value)">
372
- </div></div>
373
- </div></div>
374
- </div>
375
- </div>
376
- <div class="rpbody" id="rb-mat">
377
- <div id="matno" style="padding:20px;color:#555;text-align:center;font-size:11px;">Select a mesh</div>
378
- <div id="matbody" style="display:none;">
379
- <div class="pbody">
380
- <div class="plbl">Color</div><input class="pinp" id="pColor" type="color" style="padding:1px 2px;height:24px;" oninput="setProp('color',this.value)">
381
- <div class="plbl">Metallic</div><input class="pinp" id="pMetal" type="range" min="0" max="1" step="0.01" style="padding:4px 0;" oninput="setProp('metal',+this.value)">
382
- <div class="plbl">Roughness</div><input class="pinp" id="pRough" type="range" min="0" max="1" step="0.01" style="padding:4px 0;" oninput="setProp('rough',+this.value)">
383
- <div class="plbl">Emission</div><input class="pinp" id="pEmit" type="range" min="0" max="5" step="0.01" style="padding:4px 0;" oninput="setProp('emit',+this.value)">
384
- <div class="plbl">Alpha</div><input class="pinp" id="pAlpha" type="range" min="0" max="1" step="0.01" style="padding:4px 0;" oninput="setProp('alpha',+this.value)">
385
- <div class="plbl">Texture</div>
386
- <button class="olbtn" onclick="document.getElementById('teximport').click()" style="width:100%;">📂 Load Image</button>
387
- </div>
388
- <div style="padding:8px;border-top:1px solid #111;">
389
- <div style="color:#555;font-size:10px;margin-bottom:5px;text-transform:uppercase;letter-spacing:1px;">Presets</div>
390
- <div style="display:flex;flex-wrap:wrap;gap:4px;">
391
- <button class="olbtn" onclick="applyPreset('chrome')" style="font-size:10px;">Chrome</button>
392
- <button class="olbtn" onclick="applyPreset('gold')" style="font-size:10px;">Gold</button>
393
- <button class="olbtn" onclick="applyPreset('copper')" style="font-size:10px;">Copper</button>
394
- <button class="olbtn" onclick="applyPreset('rubber')" style="font-size:10px;">Rubber</button>
395
- <button class="olbtn" onclick="applyPreset('glass')" style="font-size:10px;">Glass</button>
396
- <button class="olbtn" onclick="applyPreset('neon')" style="font-size:10px;">Neon</button>
397
- <button class="olbtn" onclick="applyPreset('emit')" style="font-size:10px;">Glow</button>
398
- <button class="olbtn" onclick="applyPreset('lava')" style="font-size:10px;">Lava</button>
399
- <button class="olbtn" onclick="applyPreset('ice')" style="font-size:10px;">Ice</button>
400
- <button class="olbtn" onclick="applyPreset('steel')" style="font-size:10px;">Steel</button>
401
- </div>
402
- </div>
403
- </div>
404
- </div>
405
- <div class="rpbody" id="rb-render">
406
- <div class="render-section"><h3>Output Resolution</h3>
407
- <div class="render-row"><div class="plbl">Preset</div>
408
- <select class="pinp" onchange="setRP(this.value)">
409
- <option value="1920,1080">Full HD 1920×1080</option>
410
- <option value="1280,720">HD 1280×720</option>
411
- <option value="3840,2160">4K 3840×2160</option>
412
- <option value="1080,1080">Square 1080</option>
413
- </select></div>
414
- <div class="render-row"><div class="plbl">Width</div><input class="pinp" id="rW" type="number" value="1920"></div>
415
- <div class="render-row"><div class="plbl">Height</div><input class="pinp" id="rH" type="number" value="1080"></div>
416
- <div class="render-row"><div class="plbl">Format</div>
417
- <select class="pinp" id="rFmt"><option>PNG</option><option>JPEG</option></select></div>
418
- </div>
419
- <div class="render-section"><h3>Scene</h3>
420
- <div class="render-row"><div class="plbl">BG Color</div>
421
- <input type="color" class="pinp" id="rBG" value="#3c3c3c" style="padding:1px 2px;height:24px;" oninput="SCENE.background.set(this.value)"></div>
422
- <div class="render-row"><div class="plbl">Ambient</div>
423
- <input class="pinp" type="range" min="0" max="2" step="0.05" value="0.4" oninput="AMBLIGHT.intensity=+this.value"></div>
424
- <div class="render-row"><div class="plbl">Sun</div>
425
- <input class="pinp" type="range" min="0" max="3" step="0.05" value="1.1" oninput="SUNLIGHT.intensity=+this.value"></div>
426
- <div class="render-row"><div class="plbl">Exposure</div>
427
- <input class="pinp" type="range" min="0" max="3" step="0.05" value="1" oninput="RENDERER.toneMappingExposure=+this.value"></div>
428
- </div>
429
- <div style="padding:8px;"><button class="render-btn" onclick="act('saveRender')">🎨 Render &amp; Save (F12)</button></div>
430
- </div>
431
- <div class="rpbody" id="rb-sculpt">
432
- <div style="padding:8px;border-bottom:1px solid #111;color:#555;font-size:11px;">Select mesh → Sculpt Mode → Subdivide → Paint</div>
433
- <div class="sculpt-tools">
434
- <button class="sculpt-btn on" id="sc-draw" onclick="setST('draw')">✏ Draw</button>
435
- <button class="sculpt-btn" id="sc-smooth" onclick="setST('smooth')">≈ Smooth</button>
436
- <button class="sculpt-btn" id="sc-grab" onclick="setST('grab')">✥ Grab</button>
437
- <button class="sculpt-btn" id="sc-inflate" onclick="setST('inflate')">⊕ Inflate</button>
438
- <button class="sculpt-btn" id="sc-flatten" onclick="setST('flatten')">▬ Flatten</button>
439
- <button class="sculpt-btn" id="sc-crease" onclick="setST('crease')">⌒ Crease</button>
440
- </div>
441
- <div class="pbody">
442
- <div class="plbl">Brush Size</div><input class="pinp" id="sc-size" type="range" min="0.1" max="5" step="0.1" value="1" style="padding:4px 0;">
443
- <div class="plbl">Strength</div><input class="pinp" id="sc-str" type="range" min="0.01" max="1" step="0.01" value="0.3" style="padding:4px 0;">
444
- <div class="plbl">Direction</div>
445
- <select class="pinp" id="sc-dir"><option value="1">Add</option><option value="-1">Subtract</option></select>
446
- <div class="plbl">Subdivide</div>
447
- <button class="olbtn" onclick="subdivide()" style="width:100%;">Subdivide Mesh</button>
448
- </div>
449
- </div>
450
- </div>
451
- </div>
452
- <div id="timeline-wrap">
453
- <div id="tl-resize"></div>
454
- <div id="tlctrl">
455
- <button class="tlbtn" onclick="tla('first')">⏮</button>
456
- <button class="tlbtn" onclick="tla('back')">⏪</button>
457
- <button class="tlbtn" id="tlplay" onclick="tla('play')">▶</button>
458
- <button class="tlbtn" onclick="tla('fwd')">⏩</button>
459
- <button class="tlbtn" onclick="tla('last')">⏭</button>
460
- <div class="tlsep"></div>
461
- <input class="tlinp" id="tlframe" type="number" min="1" value="1" oninput="setFr(+this.value)">
462
- <span style="color:#444;padding:0 2px;flex-shrink:0;">/</span>
463
- <input class="tlinp" id="tlend" type="number" value="250" oninput="FR_END=+this.value">
464
- <div class="tlsep"></div>
465
- <select style="padding:2px 4px;background:#252525;border:1px solid #333;border-radius:3px;color:#888;font-size:11px;font-family:inherit;flex-shrink:0;" onchange="FR_FPS=+this.value">
466
- <option>12</option><option>24</option><option selected>25</option><option>30</option><option>60</option>
467
- </select>
468
- <span style="color:#555;font-size:10px;margin-left:2px;flex-shrink:0;">FPS</span>
469
- <div class="tlsep"></div>
470
- <button class="tlkbtn" onclick="insKF('loc')">Loc</button>
471
- <button class="tlkbtn" onclick="insKF('rot')">Rot</button>
472
- <button class="tlkbtn" onclick="insKF('scl')">Scl</button>
473
- <button class="tlkbtn" onclick="insKF('all')">All</button>
474
- <button class="tlkbtn" onclick="delKF()" style="color:#c55;">Del KF</button>
475
- <div class="tlsep"></div>
476
- <label style="color:#555;font-size:10px;display:flex;align-items:center;gap:3px;flex-shrink:0;cursor:pointer;">
477
- <input type="checkbox" id="tlAutoKF" style="accent-color:#e87d39;"> Auto KF
478
- </label>
479
- <span style="margin-left:auto;color:#2a2a2a;font-size:10px;padding-right:4px;flex-shrink:0;">Space=Play · I=KF · ←→=Frames</span>
480
- </div>
481
- <div id="tltrack">
482
- <div id="tlchlbls">
483
- <div class="tlchlbl" style="color:#e44;">Loc X</div>
484
- <div class="tlchlbl" style="color:#4e4;">Loc Y</div>
485
- <div class="tlchlbl" style="color:#66f;">Loc Z</div>
486
- <div class="tlchlbl" style="color:#ea4;">Rot X</div>
487
- <div class="tlchlbl" style="color:#4ea;">Rot Y</div>
488
- <div class="tlchlbl" style="color:#a4e;">Rot Z</div>
489
- </div>
490
- <canvas id="tlcanvas"></canvas>
491
- </div>
492
- </div>
493
- <div id="statusbar">
494
- <span class="stv">FORGE3D v4.0 AI</span>
495
- <span class="stv2">Mode:<b id="st-mode" style="color:#aaa;"> Object</b></span>
496
- <span class="stv2">Tool:<b id="st-tool" style="color:#aaa;"> Select</b></span>
497
- <span class="stv2">Objects:<b id="st-objs" style="color:#aaa;"> 0</b></span>
498
- <span class="stv2">Verts:<b id="st-verts" style="color:#aaa;"> 0</b></span>
499
- <span id="st-fps" style="margin-left:auto;color:#444;">FPS:—</span>
500
- <a href="/" style="color:#3a3a3a;text-decoration:none;font-size:10px;margin-left:10px;">🏠 Home</a>
501
- </div>
502
- </div>
503
- <input type="file" id="teximport" accept="image/*" style="display:none" onchange="applyTexFile(this)">
504
- <button id="ai-fab" onclick="toggleAI()" title="AI Assistant">🤖</button>
505
- <div id="ai-panel">
506
- <div id="ai-hdr">
507
- <div style="display:flex;align-items:center;gap:8px;">
508
- <span style="font-weight:700;color:#fff;font-size:13px;">🤖 FORGE3D AI</span>
509
- <span id="ai-status">Checking…</span>
510
- </div>
511
- <button onclick="toggleAI()" style="background:rgba(0,0,0,.3);border:none;color:#fff;cursor:pointer;width:24px;height:24px;border-radius:50%;font-size:14px;">✕</button>
512
- </div>
513
- <div class="ai-mtabs">
514
- <div class="ai-mtab on" id="abt-groq" onclick="setM('groq')">
515
- <span style="font-size:11px;">⚡ Groq</span><br>
516
- <span style="font-size:9px;color:#ff6b35;">Llama·Fast</span>
517
- </div>
518
- <div class="ai-mtab" id="abt-nemotron" onclick="setM('nemotron')">
519
- <span style="font-size:11px;">🟢 Nemotron</span><br>
520
- <span style="font-size:9px;color:#76b900;">NVIDIA·Scenes</span>
521
- </div>
522
- <div class="ai-mtab" id="abt-openrouter" onclick="setM('openrouter')">
523
- <span style="font-size:11px;">🔮 Detail AI</span><br>
524
- <span style="font-size:9px;color:#a855f7;">Deepseek·Complex</span>
525
- </div>
526
- </div>
527
- <div class="ai-qrow">
528
- <div class="ai-qbtn" onclick="toggleTex()">🎨 Texture</div>
529
- <div class="ai-qbtn" onclick="toggleAF()">🎬 Animate</div>
530
- <div class="ai-qbtn" onclick="quickAsk('remove all mesh objects from scene, keep lights')">🗑 Clear</div>
531
- <div class="ai-qbtn" onclick="switchTab('script')">📝 Script</div>
532
- <div class="ai-qbtn" onclick="checkStatus()">📡 Status</div>
533
- </div>
534
- <div id="tex-dlg">
535
- <div style="color:#555;font-size:10px;margin-bottom:6px;text-transform:uppercase;letter-spacing:1px;">Apply texture to selected object:</div>
536
- <div class="tex-row">
537
- <input id="tex-inp" type="text" placeholder="rusty metal, oak wood, concrete, lava…" onkeydown="if(event.key==='Enter')sendTex()">
538
- <button id="tex-go" onclick="sendTex()">Apply</button>
539
- </div>
540
- <div style="display:flex;flex-wrap:wrap;gap:4px;">
541
- <span class="ai-sug" onclick="quickTex('brushed metal')">🔩 Metal</span>
542
- <span class="ai-sug" onclick="quickTex('oak wood planks')">🪵 Wood</span>
543
- <span class="ai-sug" onclick="quickTex('cracked concrete')">🧱 Concrete</span>
544
- <span class="ai-sug" onclick="quickTex('lava glowing magma')">🌋 Lava</span>
545
- <span class="ai-sug" onclick="quickTex('dark carbon fiber')">⬛ Carbon</span>
546
- <span class="ai-sug" onclick="quickTex('white marble')">🪨 Marble</span>
547
- </div>
548
- </div>
549
- <div id="ai-msgs">
550
- <div class="ai-ma groq">👋 FORGE3D AI v4.0 ready!
551
-
552
- Try the suggestions below, or describe anything:
553
- 🚀 Missiles with 15+ parts
554
- 🌑 Black holes with glowing rings
555
- 🌍 Animated solar systems (real orbits)
556
- 🛸 Spaceships with full detail
557
- 🎨 AI texture from free CDNs
558
-
559
- Code always saves to 📝 Script tab!</div>
560
- </div>
561
- <div id="ai-pre"></div>
562
- <div id="ai-irow">
563
- <input id="ai-inp" type="text" placeholder="Describe what to create…" onkeydown="if(event.key==='Enter')sendAI()">
564
- <button id="ai-send" onclick="sendAI()">Send</button>
565
- </div>
566
- <div id="ai-sugs">
567
- <div class="ai-sug" onclick="quickAsk('build a detailed missile: dark steel LatheGeometry body, chrome nose cone, 4 delta ExtrudeGeometry fins, glowing orange exhaust nozzle emissive, TubeGeometry fuel lines, rivet sphere details, minimum 12 parts')">🚀 Missile</div>
568
- <div class="ai-sug" onclick="quickAsk('create a black hole: black SphereGeometry core emissive purple, 5 RingGeometry accretion disks at different angles with emissive orange yellow glow, 200 particle swirl spheres, bright white photon torus ring, blue TubeGeometry polar jets')">🌑 Black Hole</div>
569
- <div class="ai-sug" onclick="quickAsk('animated solar system: Sun emissive center, 8 planets orbiting at different speeds using requestAnimationFrame with Math.cos/sin, RingGeometry orbit paths, Saturn rings, asteroid belt loop of 80 tiny spheres')">🌍 Solar System</div>
570
- <div class="ai-sug" onclick="quickAsk('sci-fi spaceship: LatheGeometry hull dark steel, SphereGeometry glass cockpit, 4 ExtrudeGeometry wings, 2 engine pods with emissive glow rings, weapon mounts, antenna, running lights emissive spheres')">🛸 Spaceship</div>
571
- <div class="ai-sug" onclick="quickAsk('neon cyberpunk city night: 15 buildings varied heights, glowing neon signs pink cyan emissive, street grid plane, 4 colored point lights, atmosphere')">🌆 Neon City</div>
572
- <div class="ai-sug" onclick="quickAsk('crystal cave: 12 crystal spires ConeGeometry varied colors emissive cores, stalactite cylinders, mossy floor plane, 3 colored point lights')">💎 Crystal Cave</div>
573
- </div>
574
- <div id="af">
575
- <div class="af-hdr">
576
- <span>🎬 AI Animation Generator</span>
577
- <button onclick="toggleAF()" style="background:none;border:none;color:#555;cursor:pointer;font-size:14px;">✕</button>
578
- </div>
579
- <div class="af-row"><div class="af-lbl">Describe motion</div>
580
- <textarea id="af-desc" class="af-inp" rows="2" placeholder="planets orbit sun, missile launches, camera flythrough…" style="resize:none;line-height:1.5;"></textarea></div>
581
- <div class="af-row"><div class="af-lbl">Duration</div>
582
- <select id="af-dur" class="af-inp"><option value="5">5s</option><option value="10">10s</option><option value="30" selected>30s</option><option value="60">60s</option></select></div>
583
- <div class="af-row"><div class="af-lbl">FPS</div>
584
- <select id="af-fps" class="af-inp"><option value="24">24</option><option value="25" selected>25</option><option value="30">30</option><option value="60">60</option></select></div>
585
- <div class="af-row"><div class="af-lbl">Type</div>
586
- <select id="af-type" class="af-inp"><option value="auto">Auto</option><option value="orbit">Orbit</option><option value="rotation">Rotation</option><option value="bounce">Bounce</option><option value="fly">Flythrough</option></select></div>
587
- <div class="af-row"><div class="af-lbl">Easing</div>
588
- <select id="af-ease" class="af-inp"><option value="linear">Linear</option><option value="ease" selected>Ease In/Out</option><option value="bounce">Bounce</option><option value="elastic">Elastic</option></select></div>
589
- <div class="af-row"><div class="af-lbl">Loop</div>
590
- <input type="checkbox" id="af-loop" checked style="accent-color:#e87d39;width:14px;height:14px;">
591
- <span style="color:#555;font-size:10px;margin-left:4px;">Seamless loop</span></div>
592
- <button class="af-gen" onclick="genAnim()">🎬 Generate Animation</button>
593
- </div>
594
- </div>
595
- <div id="ctx">
596
- <div class="cxi" onclick="act('del')">Delete<span style="color:#555;font-size:10px">Del</span></div>
597
- <div class="cxi" onclick="act('dup')">Duplicate<span style="color:#555;font-size:10px">Ctrl+D</span></div>
598
- <div class="cxi" onclick="act('focus')">Focus<span style="color:#555;font-size:10px">.</span></div>
599
- <div style="height:1px;background:#333;margin:3px 0;"></div>
600
- <div class="cxi" onclick="setTool('move')">Move (G)</div>
601
- <div class="cxi" onclick="setTool('rotate')">Rotate (R)</div>
602
- <div class="cxi" onclick="setTool('scale')">Scale (S)</div>
603
- <div style="height:1px;background:#333;margin:3px 0;"></div>
604
- <div class="cxi" onclick="addObj('cube')">Add Cube</div>
605
- <div class="cxi" onclick="insKF('all')">Insert Keyframe (I)</div>
606
- </div>
607
- <div id="toast"></div>
608
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
609
- <script>
610
- 'use strict';
611
- const D2R=Math.PI/180,R2D=180/Math.PI;
612
- let SCENE,CAM,OCAM,RENDERER,RAYCASTER,AMBLIGHT,SUNLIGHT;
613
- let OBJS=[],SEL=null,UNDOSTACK=[];
614
- let MODE='object',TOOL='select',SHADING='solid',ORTHO=false;
615
- let rPanelOpen=true,tlVis=true;
616
- const ORB={theta:.6,phi:1.1,r:10,tx:0,ty:.5,tz:0};
617
- const PT={down:false,btn:-1,lx:0,ly:0,sx:0,sy:0,drag:false};
618
- let FR=1,FR_END=250,FR_FPS=25,TL_PLAY=false;
619
- let KF={};
620
- let SCULPT='draw',AI_MODEL='groq',AI_OPEN=false;
621
- let AI_HIST={groq:[],nemotron:[],openrouter:[]};
622
- let _fc=0,_fl=performance.now();
623
-
624
- window.addEventListener('DOMContentLoaded',()=>{
625
- initThree();buildScene();setupPtr();setupKeys();setupTL();setupTLR();
626
- refreshOL();refreshStatus();loop();checkStatus();
627
- toast('FORGE3D v4.0 AI — Click 🤖 for AI · 📝 Script tab always visible',5000);
628
- });
629
-
630
- function initThree(){
631
- const cv=document.getElementById('c');
632
- try{RENDERER=new THREE.WebGLRenderer({canvas:cv,antialias:true,preserveDrawingBuffer:true,powerPreference:'high-performance'});}
633
- catch(e){RENDERER=new THREE.WebGLRenderer({canvas:cv,antialias:false,preserveDrawingBuffer:true});}
634
- RENDERER.setPixelRatio(Math.min(devicePixelRatio,2));
635
- RENDERER.shadowMap.enabled=true;RENDERER.shadowMap.type=THREE.PCFSoftShadowMap;
636
- RENDERER.outputEncoding=THREE.sRGBEncoding;RENDERER.toneMapping=THREE.CineonToneMapping;RENDERER.toneMappingExposure=1;
637
- SCENE=new THREE.Scene();SCENE.background=new THREE.Color(0x3c3c3c);
638
- CAM=new THREE.PerspectiveCamera(50,1,.01,10000);
639
- OCAM=new THREE.OrthographicCamera(-5,5,5,-5,.01,10000);
640
- RAYCASTER=new THREE.Raycaster();
641
- new ResizeObserver(onResize).observe(document.getElementById('vpwrap'));
642
- onResize();
643
- }
644
- function onResize(){
645
- const w=document.getElementById('vpwrap'),wd=w.clientWidth,ht=w.clientHeight;
646
- if(!RENDERER||wd<=0||ht<=0)return;
647
- RENDERER.setSize(wd,ht,false);CAM.aspect=wd/ht;CAM.updateProjectionMatrix();
648
- const h=ORB.r*.45;OCAM.left=-h*(wd/ht);OCAM.right=h*(wd/ht);OCAM.top=h;OCAM.bottom=-h;OCAM.updateProjectionMatrix();
649
- }
650
- function getCAM(){return ORTHO?OCAM:CAM;}
651
- function updateCam(){
652
- const{theta,phi,r,tx,ty,tz}=ORB;
653
- CAM.position.set(tx+r*Math.sin(phi)*Math.sin(theta),ty+r*Math.cos(phi),tz+r*Math.sin(phi)*Math.cos(theta));
654
- CAM.lookAt(tx,ty,tz);OCAM.position.copy(CAM.position).multiplyScalar(1.5);OCAM.lookAt(tx,ty,tz);drawGizmo();
655
- }
656
- function buildScene(){
657
- AMBLIGHT=new THREE.AmbientLight(0xffffff,.4);SCENE.add(AMBLIGHT);
658
- SUNLIGHT=new THREE.DirectionalLight(0xffffff,1.1);SUNLIGHT.position.set(6,12,5);SUNLIGHT.castShadow=true;
659
- SUNLIGHT.shadow.mapSize.set(2048,2048);SUNLIGHT.shadow.camera.left=SUNLIGHT.shadow.camera.bottom=-30;
660
- SUNLIGHT.shadow.camera.right=SUNLIGHT.shadow.camera.top=30;SUNLIGHT.shadow.camera.far=200;SCENE.add(SUNLIGHT);
661
- SCENE.add(new THREE.DirectionalLight(0x8899ff,.25)).position.set(-6,3,-6);
662
- SCENE.add(new THREE.GridHelper(200,100,0x555555,0x3a3a3a));
663
- SCENE.add(new THREE.AxesHelper(5));
664
- createMesh('cube','Cube');
665
- const lo=createLight('point',false);if(lo){lo.mesh.position.set(4,5,3);lo.light.position.set(4,5,3);}
666
- updateCam();
667
- }
668
-
669
- // ── FACTORIES ──
670
- function mkMat(hex){return new THREE.MeshStandardMaterial({color:hex||'#888888',roughness:.75,metalness:.05});}
671
- function createMesh(type,name,pos){
672
- const N=32;let geo;
673
- switch(type){
674
- case'cube':geo=new THREE.BoxGeometry(2,2,2,2,2,2);break;
675
- case'sphere':geo=new THREE.SphereGeometry(1,N,N/2);break;
676
- case'cylinder':geo=new THREE.CylinderGeometry(1,1,2,N);break;
677
- case'plane':geo=new THREE.PlaneGeometry(4,4,8,8);geo.rotateX(-Math.PI/2);break;
678
- case'cone':geo=new THREE.ConeGeometry(1,2,N);break;
679
- case'torus':geo=new THREE.TorusGeometry(1,.35,16,N);break;
680
- case'ico':geo=new THREE.IcosahedronGeometry(1,3);break;
681
- case'torusknot':geo=new THREE.TorusKnotGeometry(1,.3,128,16);break;
682
- default:geo=new THREE.BoxGeometry(1,1,1);
683
- }
684
- const mesh=new THREE.Mesh(geo,mkMat());mesh.castShadow=mesh.receiveShadow=true;
685
- mesh.position.set(pos?pos.x:0,pos?pos.y:(type==='plane'?0:1),pos?pos.z:0);
686
- SCENE.add(mesh);const o={mesh,light:null,name,type:'MESH',vis:true,kf:{}};OBJS.push(o);doSel(o);return o;
687
- }
688
- function createLight(type,doSel=true){
689
- let light,helper;const nm=type.charAt(0).toUpperCase()+type.slice(1)+' Light';
690
- switch(type){
691
- case'sun':light=new THREE.DirectionalLight(0xfffaf0,1.5);helper=new THREE.DirectionalLightHelper(light,1);break;
692
- case'spot':light=new THREE.SpotLight(0xfffaf0,2,20,Math.PI/5,.3);light.castShadow=true;helper=new THREE.SpotLightHelper(light);break;
693
- default:light=new THREE.PointLight(0xfffaf0,1.5,20);light.castShadow=true;helper=new THREE.PointLightHelper(light,.45);
694
- }
695
- light.position.set((Math.random()-.5)*6,5+Math.random()*3,(Math.random()-.5)*6);SCENE.add(light);
696
- if(helper){helper.position.copy(light.position);SCENE.add(helper);}
697
- const ind=new THREE.Mesh(new THREE.SphereGeometry(.15,8,8),new THREE.MeshBasicMaterial({color:0xffff66,depthTest:false}));
698
- ind.position.copy(light.position);SCENE.add(ind);
699
- const o={mesh:ind,helper,light,name:nm,type:'LIGHT',vis:true,kf:{}};OBJS.push(o);if(doSel)doSel(o);return o;
700
- }
701
- function addObj(type){
702
- const n=type.charAt(0).toUpperCase()+type.slice(1)+'.'+(OBJS.filter(o=>o.type==='MESH').length+1);
703
- const o=createMesh(type,n);refreshOL();refreshStatus();return o;
704
- }
705
- function addLight(type){const o=createLight(type,true);refreshOL();refreshStatus();return o;}
706
-
707
- // ── SELECT ──
708
- function doSel(obj){if(SEL)clrHL(SEL);SEL=obj;if(obj&&obj.type==='MESH')addHL(obj);refreshOL();refreshProps();refreshStatus();}
709
- function clrAll(){if(SEL)clrHL(SEL);SEL=null;refreshOL();refreshProps();refreshStatus();}
710
- function addHL(o){if(!o.mesh.geometry)return;const m=new THREE.Mesh(o.mesh.geometry,new THREE.MeshBasicMaterial({color:0xff8800,side:THREE.BackSide}));m.name='__hl';m.scale.setScalar(1.06);o.mesh.add(m);}
711
- function clrHL(o){if(!o?.mesh)return;const h=o.mesh.getObjectByName('__hl');if(h)o.mesh.remove(h);}
712
-
713
- // ── POINTER ──
714
- function setupPtr(){
715
- const cv=document.getElementById('c');
716
- cv.addEventListener('mousedown',e=>{PT.down=true;PT.btn=e.button;PT.drag=false;PT.lx=PT.sx=e.clientX;PT.ly=PT.sy=e.clientY;if(e.button===1)e.preventDefault();});
717
- window.addEventListener('mousemove',e=>{
718
- if(!PT.down)return;const dx=e.clientX-PT.lx,dy=e.clientY-PT.ly;PT.lx=e.clientX;PT.ly=e.clientY;
719
- if(Math.abs(e.clientX-PT.sx)+Math.abs(e.clientY-PT.sy)>3)PT.drag=true;
720
- if(PT.btn===2||(PT.btn===0&&e.altKey)||PT.btn===1){
721
- if(e.shiftKey||PT.btn===1)doPan(dx,dy);
722
- else{ORB.theta-=dx*.008;ORB.phi=clamp(ORB.phi+dy*.008,.05,Math.PI-.05);updateCam();}
723
- }
724
- if(PT.btn===0&&!e.altKey&&TOOL!=='select'&&SEL?.type==='MESH'){
725
- applyDrag(dx,dy);if(document.getElementById('tlAutoKF').checked)insKF('all');
726
- }
727
- if(MODE==='sculpt'&&PT.btn===0&&SEL?.type==='MESH'&&PT.drag)sculptAt(e.clientX,e.clientY);
728
- });
729
- window.addEventListener('mouseup',e=>{if(!PT.drag&&e.button===0)clickSel(e.clientX,e.clientY,e.shiftKey);PT.down=false;PT.btn=-1;PT.drag=false;});
730
- cv.addEventListener('wheel',e=>{e.preventDefault();ORB.r=clamp(ORB.r*(e.deltaY>0?1.12:.89),.3,3000);updateCam();onResize();},{passive:false});
731
- cv.addEventListener('contextmenu',e=>{e.preventDefault();const ctx=document.getElementById('ctx');ctx.style.left=Math.min(e.clientX,innerWidth-170)+'px';ctx.style.top=Math.min(e.clientY,innerHeight-220)+'px';ctx.classList.add('show');});
732
- cv.addEventListener('dblclick',()=>{if(SEL)act('focus');});
733
- let tp=[],tpinch=0,tmov=false;
734
- cv.addEventListener('touchstart',e=>{e.preventDefault();tmov=false;tp=Array.from(e.touches).map(t=>({x:t.clientX,y:t.clientY}));if(e.touches.length===2)tpinch=Math.hypot(e.touches[1].clientX-e.touches[0].clientX,e.touches[1].clientY-e.touches[0].clientY);},{passive:false});
735
- cv.addEventListener('touchmove',e=>{
736
- e.preventDefault();tmov=true;const cur=Array.from(e.touches).map(t=>({x:t.clientX,y:t.clientY}));
737
- if(cur.length===1&&tp.length>=1){const dx=cur[0].x-tp[0].x,dy=cur[0].y-tp[0].y;
738
- if(MODE==='sculpt'&&SEL?.type==='MESH')sculptAt(cur[0].x,cur[0].y);
739
- else if(TOOL!=='select'&&SEL?.type==='MESH')applyDrag(dx,dy);
740
- else{ORB.theta-=dx*.009;ORB.phi=clamp(ORB.phi+dy*.009,.05,Math.PI-.05);updateCam();}}
741
- if(cur.length===2&&tp.length>=2){
742
- const cx=(cur[0].x+cur[1].x)/2,cy=(cur[0].y+cur[1].y)/2;doPan(cx-(tp[0].x+tp[1].x)/2,cy-(tp[0].y+tp[1].y)/2);
743
- const d=Math.hypot(cur[1].x-cur[0].x,cur[1].y-cur[0].y);if(tpinch>0){ORB.r=clamp(ORB.r*(tpinch/d),.3,3000);updateCam();onResize();}tpinch=d;}
744
- tp=cur;
745
- },{passive:false});
746
- cv.addEventListener('touchend',e=>{e.preventDefault();if(!tmov&&e.changedTouches.length)clickSel(e.changedTouches[0].clientX,e.changedTouches[0].clientY,false);},{passive:false});
747
- document.addEventListener('click',e=>{if(!e.target.closest('.mitem')&&!e.target.closest('.mdrop'))closeMenus();if(!e.target.closest('#ctx'))document.getElementById('ctx').classList.remove('show');});
748
- }
749
-
750
- // ── SCULPT ──
751
- function sculptAt(mx,my){
752
- if(!SEL||SEL.type!=='MESH')return;
753
- const cv=document.getElementById('c'),r=cv.getBoundingClientRect();
754
- const nv=new THREE.Vector2(((mx-r.left)/r.width)*2-1,-((my-r.top)/r.height)*2+1);
755
- RAYCASTER.setFromCamera(nv,getCAM());
756
- const hits=RAYCASTER.intersectObject(SEL.mesh,false);if(!hits.length)return;
757
- const hp=hits[0].point,pos=SEL.mesh.geometry.attributes.position;
758
- const bR=parseFloat(document.getElementById('sc-size').value);
759
- const str=parseFloat(document.getElementById('sc-str').value);
760
- const dir=parseInt(document.getElementById('sc-dir').value);
761
- const norm=hits[0].face?hits[0].face.normal.clone().transformDirection(SEL.mesh.matrixWorld):new THREE.Vector3(0,1,0);
762
- for(let i=0;i<pos.count;i++){
763
- const vw=new THREE.Vector3(pos.getX(i),pos.getY(i),pos.getZ(i)).applyMatrix4(SEL.mesh.matrixWorld);
764
- const d=vw.distanceTo(hp);if(d>bR)continue;
765
- const fall=Math.pow(1-d/bR,2)*str*dir;
766
- let nx=pos.getX(i),ny=pos.getY(i),nz=pos.getZ(i);
767
- if(SCULPT==='draw'){nx+=norm.x*fall;ny+=norm.y*fall;nz+=norm.z*fall;}
768
- else if(SCULPT==='inflate'){const out=new THREE.Vector3(nx,ny,nz).normalize();nx+=out.x*fall;ny+=out.y*fall;nz+=out.z*fall;}
769
- else if(SCULPT==='grab'){ny+=fall*.5;}
770
- else if(SCULPT==='flatten'){ny+=(0-ny)*Math.abs(fall)*.3;}
771
- else if(SCULPT==='crease'){nx+=norm.x*fall*2;ny+=norm.y*fall*2;nz+=norm.z*fall*2;}
772
- else if(SCULPT==='smooth'){nx+=0;ny+=fall*.2;nz+=0;}
773
- pos.setXYZ(i,nx,ny,nz);
774
- }
775
- pos.needsUpdate=true;SEL.mesh.geometry.computeVertexNormals();
776
- }
777
- function subdivide(){
778
- if(!SEL||SEL.type!=='MESH'){toast('Select a mesh first');return;}
779
- const m=SEL.mesh,old=m.geometry;let neo=null;
780
- if(old.type==='BoxGeometry'){const p=old.parameters;neo=new THREE.BoxGeometry(p.width,p.height,p.depth,(p.widthSegments||1)*2,(p.heightSegments||1)*2,(p.depthSegments||1)*2);}
781
- else if(old.type==='SphereGeometry'){const p=old.parameters;neo=new THREE.SphereGeometry(p.radius,(p.widthSegments||16)*2,(p.heightSegments||12)*2);}
782
- else if(old.type==='CylinderGeometry'){const p=old.parameters;neo=new THREE.CylinderGeometry(p.radiusTop,p.radiusBottom,p.height,(p.radialSegments||16)*2);}
783
- if(neo){old.dispose();m.geometry=neo;toast('Subdivided! '+neo.attributes.position.count+' verts');}
784
- else toast('Subdivided — use higher-detail mesh for best results');
785
- }
786
-
787
- function applyDrag(dx,dy){
788
- if(!SEL?.mesh)return;
789
- const spd=ORB.r*.004,right=new THREE.Vector3().setFromMatrixColumn(CAM.matrixWorld,0),up=new THREE.Vector3().setFromMatrixColumn(CAM.matrixWorld,1),m=SEL.mesh;
790
- const ax=TOOL==='grab_x'?'x':TOOL==='grab_y'?'y':TOOL==='grab_z'?'z':null;
791
- if(TOOL==='move'||TOOL.startsWith('grab')){
792
- if(ax==='x')m.position.x+=dx*spd*1.5;else if(ax==='y')m.position.y-=dy*spd*1.5;else if(ax==='z')m.position.z+=dx*spd*1.5;
793
- else{m.position.addScaledVector(right,dx*spd);m.position.addScaledVector(up,-dy*spd);}
794
- if(SEL.light)SEL.light.position.copy(m.position);
795
- }else if(TOOL==='rotate'){
796
- if(ax==='x')m.rotation.x+=dy*.012;else if(ax==='z')m.rotation.z+=dx*.012;else{m.rotation.y+=dx*.012;m.rotation.x+=dy*.006;}
797
- }else if(TOOL==='scale'){
798
- const f=Math.max(.001,1+(dx-dy)*.006);
799
- if(ax==='x')m.scale.x=Math.max(.001,m.scale.x*f);else if(ax==='y')m.scale.y=Math.max(.001,m.scale.y*f);else if(ax==='z')m.scale.z=Math.max(.001,m.scale.z*f);else m.scale.multiplyScalar(f);
800
- }
801
- refreshPV();
802
- }
803
- function doPan(dx,dy){
804
- const spd=ORB.r*.001,right=new THREE.Vector3().setFromMatrixColumn(CAM.matrixWorld,0),up=new THREE.Vector3().setFromMatrixColumn(CAM.matrixWorld,1);
805
- ORB.tx-=right.x*dx*spd*8;ORB.ty-=right.y*dx*spd*8;ORB.tz-=right.z*dx*spd*8;
806
- ORB.tx+=up.x*dy*spd*8;ORB.ty+=up.y*dy*spd*8;ORB.tz+=up.z*dy*spd*8;updateCam();
807
- }
808
- function clickSel(cx,cy,add){
809
- if(MODE!=='object')return;
810
- const cv=document.getElementById('c'),r=cv.getBoundingClientRect();
811
- const nv=new THREE.Vector2(((cx-r.left)/r.width)*2-1,-((cy-r.top)/r.height)*2+1);
812
- RAYCASTER.setFromCamera(nv,getCAM());
813
- const hits=RAYCASTER.intersectObjects(OBJS.filter(o=>o.vis&&o.mesh).map(o=>o.mesh),true);
814
- if(hits.length){let h=hits[0].object;while(h.parent&&h.parent!==SCENE)h=h.parent;const f=OBJS.find(o=>o.mesh===h);if(f){doSel(f);return;}}
815
- if(!add)clrAll();
816
- }
817
-
818
- // ── KEYBOARD ──
819
- function setupKeys(){
820
- document.addEventListener('keydown',e=>{
821
- const tag=document.activeElement.tagName;
822
- if(tag==='INPUT'||tag==='SELECT'||tag==='TEXTAREA'){if(e.ctrlKey&&e.key==='Enter'){runScript();e.preventDefault();}return;}
823
- if(e.ctrlKey||e.metaKey){switch(e.key){case'z':e.preventDefault();act('undo');break;case'd':e.preventDefault();act('dup');break;case'n':e.preventDefault();act('newScene');break;}return;}
824
- switch(e.key){
825
- case'g':case'G':setTool('move');break;case'r':case'R':setTool('rotate');break;case's':case'S':setTool('scale');break;
826
- case'w':case'W':case'q':case'Q':setTool('select');break;
827
- case'Delete':case'Backspace':act('del');break;
828
- case'a':case'A':SEL?clrAll():(OBJS.length&&doSel(OBJS[0]));break;
829
- case'.':act('focus');break;case'Home':act('viewAll');break;case'F12':e.preventDefault();act('saveRender');break;
830
- case' ':e.preventDefault();tla('play');break;
831
- case'ArrowLeft':e.preventDefault();tla('back');break;case'ArrowRight':e.preventDefault();tla('fwd');break;
832
- case'i':case'I':insKF('all');break;
833
- case'Tab':e.preventDefault();setMode(MODE==='edit'?'object':'edit');break;
834
- case'1':setView('front');break;case'3':setView('side');break;case'7':setView('top');break;case'5':act('toggleOrtho');break;
835
- }
836
- });
837
- }
838
-
839
- // ── ACTIONS ──
840
- function saveU(){UNDOSTACK.push(JSON.stringify(OBJS.map(o=>({name:o.name,p:o.mesh.position.toArray(),r:[o.mesh.rotation.x,o.mesh.rotation.y,o.mesh.rotation.z],s:o.mesh.scale.toArray()}))));if(UNDOSTACK.length>50)UNDOSTACK.shift();}
841
- function act(a){
842
- closeMenus();
843
- switch(a){
844
- case'del':
845
- if(!SEL){toast('Nothing selected');return;}saveU();
846
- if(SEL.light)SCENE.remove(SEL.light);if(SEL.helper)SCENE.remove(SEL.helper);SCENE.remove(SEL.mesh);
847
- OBJS=OBJS.filter(o=>o!==SEL);SEL=null;refreshOL();refreshProps();refreshStatus();toast('Deleted');break;
848
- case'dup':
849
- if(!SEL||SEL.type!=='MESH'){toast('Select a mesh');return;}saveU();
850
- const nm=new THREE.Mesh(SEL.mesh.geometry.clone(),SEL.mesh.material.clone());
851
- nm.castShadow=nm.receiveShadow=true;nm.position.copy(SEL.mesh.position);nm.position.x+=1.5;nm.rotation.copy(SEL.mesh.rotation);nm.scale.copy(SEL.mesh.scale);
852
- SCENE.add(nm);const no={mesh:nm,light:null,name:SEL.name+'.copy',type:'MESH',vis:true,kf:{}};OBJS.push(no);doSel(no);refreshOL();refreshStatus();toast('Duplicated');break;
853
- case'undo':
854
- if(!UNDOSTACK.length){toast('Nothing to undo');return;}
855
- JSON.parse(UNDOSTACK.pop()).forEach(s=>{const o=OBJS.find(x=>x.name===s.name);if(o){o.mesh.position.fromArray(s.p);o.mesh.rotation.set(...s.r);o.mesh.scale.fromArray(s.s);}});
856
- refreshPV();toast('Undo');break;
857
- case'resetLoc':if(!SEL)return;saveU();SEL.mesh.position.set(0,1,0);refreshPV();toast('Location reset');break;
858
- case'resetRot':if(!SEL)return;saveU();SEL.mesh.rotation.set(0,0,0);refreshPV();toast('Rotation reset');break;
859
- case'resetScale':if(!SEL)return;saveU();SEL.mesh.scale.set(1,1,1);refreshPV();toast('Scale reset');break;
860
- case'mirror_x':if(!SEL)return;saveU();SEL.mesh.scale.x*=-1;refreshPV();toast('Mirrored X');break;
861
- case'snapGround':
862
- if(!SEL||SEL.type!=='MESH')return;saveU();
863
- const bb=new THREE.Box3().setFromObject(SEL.mesh);SEL.mesh.position.y-=bb.min.y;refreshPV();toast('Snapped to ground');break;
864
- case'focus':
865
- if(!SEL)return;const wp=new THREE.Vector3();SEL.mesh.getWorldPosition(wp);ORB.tx=wp.x;ORB.ty=wp.y;ORB.tz=wp.z;ORB.r=4;updateCam();toast('Focused');break;
866
- case'viewAll':ORB.tx=0;ORB.ty=1;ORB.tz=0;ORB.r=12;updateCam();toast('View All');break;
867
- case'toggleOrtho':ORTHO=!ORTHO;toast(ORTHO?'Orthographic':'Perspective');break;
868
- case'newScene':
869
- if(!confirm('Clear scene?'))return;
870
- OBJS.forEach(o=>{if(o.light)SCENE.remove(o.light);if(o.helper)SCENE.remove(o.helper);SCENE.remove(o.mesh);});
871
- OBJS=[];SEL=null;KF={};buildScene();refreshOL();refreshStatus();toast('New Scene');break;
872
- case'saveRender':
873
- const rw=parseInt(document.getElementById('rW').value)||1920,rh=parseInt(document.getElementById('rH').value)||1080;
874
- const fmt=document.getElementById('rFmt').value;
875
- RENDERER.setSize(rw,rh,false);RENDERER.render(SCENE,getCAM());
876
- const lk=document.createElement('a');lk.download='forge3d.'+(fmt==='PNG'?'png':'jpg');lk.href=RENDERER.domElement.toDataURL(fmt==='PNG'?'image/png':'image/jpeg',.95);lk.click();
877
- onResize();toast('Rendered '+rw+'×'+rh);break;
878
- case'exportObj':
879
- let out='# FORGE3D OBJ\n';let vo=1;
880
- OBJS.filter(o=>o.type==='MESH').forEach(o=>{
881
- const p=o.mesh.geometry.attributes.position;out+=`o ${o.name}\n`;
882
- for(let i=0;i<p.count;i++)out+=`v ${p.getX(i).toFixed(4)} ${p.getY(i).toFixed(4)} ${p.getZ(i).toFixed(4)}\n`;
883
- const idx=o.mesh.geometry.index;if(idx)for(let i=0;i<idx.count;i+=3)out+=`f ${idx.getX(i)+vo} ${idx.getX(i+1)+vo} ${idx.getX(i+2)+vo}\n`;vo+=p.count;
884
- });
885
- const bl=new Blob([out],{type:'text/plain'});const al=document.createElement('a');al.download='forge3d.obj';al.href=URL.createObjectURL(bl);al.click();toast('OBJ exported');break;
886
- }
887
- }
888
-
889
- // ── MODES / TOOLS / UI ──
890
- function setMode(m){
891
- MODE=m;['object','edit','sculpt'].forEach(mm=>document.getElementById('mb-'+mm)?.classList.toggle('on',mm===m));
892
- if(m==='sculpt'){setRTab('sculpt');toast('Sculpt Mode — Paint on mesh with LMB');}else if(m==='edit')toast('Edit Mode');else toast('Object Mode');
893
- document.getElementById('st-mode').textContent=' '+m.charAt(0).toUpperCase()+m.slice(1);refreshBadge();
894
- }
895
- function setTool(t){
896
- TOOL=t;document.querySelectorAll('.tbtn[id^="t-"]').forEach(b=>b.classList.remove('on'));
897
- document.getElementById('t-'+t)?.classList.add('on');
898
- document.getElementById('st-tool').textContent=' '+t.charAt(0).toUpperCase()+t.slice(1);refreshBadge();
899
- }
900
- function setST(t){SCULPT=t;document.querySelectorAll('.sculpt-btn').forEach(b=>b.classList.remove('on'));document.getElementById('sc-'+t)?.classList.add('on');}
901
- function setShading(s){
902
- SHADING=s;document.querySelectorAll('.shdbtn').forEach(b=>b.classList.remove('on'));document.getElementById('shd-'+s)?.classList.add('on');
903
- OBJS.forEach(o=>{if(!o.mesh.material)return;if(s==='wire'){o.mesh.material.wireframe=true;o.mesh.material.transparent=false;}
904
- else if(s==='xray'){o.mesh.material.wireframe=false;o.mesh.material.transparent=true;o.mesh.material.opacity=.25;}
905
- else{o.mesh.material.wireframe=false;o.mesh.material.transparent=(o.mesh.material.opacity??1)<1;o.mesh.material.opacity=o.mesh.material.opacity??1;}
906
- });
907
- }
908
- function setView(v){
909
- if(v==='front'){ORB.theta=0;ORB.phi=Math.PI/2;}else if(v==='side'){ORB.theta=Math.PI/2;ORB.phi=Math.PI/2;}else{ORB.theta=0;ORB.phi=.01;}
910
- ORB.r=12;updateCam();toast(v.charAt(0).toUpperCase()+v.slice(1)+' view');
911
- }
912
- function toggleRPanel(){rPanelOpen=!rPanelOpen;document.getElementById('rpanel').classList.toggle('hidden',!rPanelOpen);}
913
- function toggleTL(){tlVis=!tlVis;document.getElementById('timeline-wrap').style.display=tlVis?'flex':'none';}
914
- function switchTab(t){
915
- ['viewport','script','shader'].forEach(tt=>{document.getElementById('tab-'+tt)?.classList.toggle('on',tt===t);document.getElementById('tab-'+tt+'-content')?.classList.toggle('on',tt===t);});
916
- if(t==='viewport')setTimeout(onResize,50);
917
- }
918
- function openMenu(btn,id){closeMenus();const d=document.getElementById(id);if(!d)return;const r=btn.getBoundingClientRect();d.style.left=r.left+'px';d.classList.add('show');btn.classList.add('open');}
919
- function closeMenus(){document.querySelectorAll('.mdrop').forEach(d=>d.classList.remove('show'));document.querySelectorAll('.mbtn').forEach(b=>b.classList.remove('open'));}
920
- function showHelp(){alert('FORGE3D Shortcuts:\nW=Select G=Move R=Rotate S=Scale\nDel=Delete Ctrl+D=Duplicate Ctrl+Z=Undo\nA=Select All .=Focus Home=View All\n1=Front 3=Side 7=Top 5=Ortho\nSpace=Play ←→=Frames I=Insert KF\nTab=Toggle Edit Mode F12=Render\nCtrl+Enter in Script=Run');}
921
-
922
- // ── OUTLINER ──
923
- function refreshOL(){
924
- const list=document.getElementById('ollist');if(!list)return;list.innerHTML='';
925
- OBJS.forEach(o=>{
926
- const d=document.createElement('div');d.className='olitem'+(SEL===o?' sel':'');
927
- const ico=o.type==='LIGHT'?'💡':o.type==='CAMERA'?'📷':'▣';
928
- d.innerHTML=`<span>${ico}</span><span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;">${o.name}</span><span class="eye" onclick="event.stopPropagation();toggleVis('${o.name}')">${o.vis?'👁':'○'}</span>`;
929
- d.addEventListener('click',()=>doSel(o));list.appendChild(d);
930
- });
931
- }
932
- function filterOL(q){document.querySelectorAll('.olitem').forEach(el=>{el.style.display=el.textContent.toLowerCase().includes(q.toLowerCase())?'':'none';});}
933
- function toggleVis(name){const o=OBJS.find(x=>x.name===name);if(!o)return;o.vis=!o.vis;o.mesh.visible=o.vis;if(o.light)o.light.visible=o.vis;refreshOL();}
934
-
935
- // ── PROPS ──
936
- function refreshProps(){
937
- const show=!!SEL;
938
- document.getElementById('pno').style.display=show?'none':'';document.getElementById('pbody').style.display=show?'':'none';
939
- document.getElementById('matno').style.display=(show&&SEL.type==='MESH')?'none':'';document.getElementById('matbody').style.display=(show&&SEL.type==='MESH')?'':'none';
940
- if(show)refreshPV();
941
- }
942
- function refreshPV(){
943
- if(!SEL)return;const m=SEL.mesh,g=id=>document.getElementById(id);
944
- g('pName')&&(g('pName').value=SEL.name);g('pType')&&(g('pType').value=SEL.type);
945
- g('pLX')&&(g('pLX').value=m.position.x.toFixed(3));g('pLY')&&(g('pLY').value=m.position.y.toFixed(3));g('pLZ')&&(g('pLZ').value=m.position.z.toFixed(3));
946
- g('pRX')&&(g('pRX').value=(m.rotation.x*R2D).toFixed(1));g('pRY')&&(g('pRY').value=(m.rotation.y*R2D).toFixed(1));g('pRZ')&&(g('pRZ').value=(m.rotation.z*R2D).toFixed(1));
947
- g('pSX')&&(g('pSX').value=m.scale.x.toFixed(3));g('pSY')&&(g('pSY').value=m.scale.y.toFixed(3));g('pSZ')&&(g('pSZ').value=m.scale.z.toFixed(3));
948
- if(SEL.type==='MESH'&&m.material){
949
- g('pColor')&&(g('pColor').value='#'+(m.material.color.getHexString?.()??'888888'));
950
- g('pMetal')&&(g('pMetal').value=m.material.metalness||0);g('pRough')&&(g('pRough').value=m.material.roughness||.75);
951
- g('pEmit')&&(g('pEmit').value=m.material.emissiveIntensity||0);g('pAlpha')&&(g('pAlpha').value=m.material.opacity??1);
952
- }
953
- }
954
- function setProp(p,v){
955
- if(!SEL)return;const m=SEL.mesh;
956
- switch(p){
957
- case'name':SEL.name=v;refreshOL();break;
958
- case'lx':m.position.x=v;break;case'ly':m.position.y=v;break;case'lz':m.position.z=v;break;
959
- case'rx':m.rotation.x=v*D2R;break;case'ry':m.rotation.y=v*D2R;break;case'rz':m.rotation.z=v*D2R;break;
960
- case'sx':m.scale.x=v;break;case'sy':m.scale.y=v;break;case'sz':m.scale.z=v;break;
961
- case'color':m.material?.color.set(v);break;
962
- case'metal':if(m.material)m.material.metalness=v;break;
963
- case'rough':if(m.material)m.material.roughness=v;break;
964
- case'emit':if(m.material){m.material.emissiveIntensity=v;if(v>0&&m.material.emissive)m.material.emissive.setHex(0xffffff);}break;
965
- case'alpha':if(m.material){m.material.opacity=v;m.material.transparent=v<1;}break;
966
- }
967
- if(SEL.light&&['lx','ly','lz'].includes(p))SEL.light.position.copy(m.position);
968
- }
969
- function applyPreset(p){
970
- if(!SEL||SEL.type!=='MESH')return;const mat=SEL.mesh.material;if(!mat)return;
971
- mat.wireframe=false;mat.transparent=false;mat.opacity=1;if(mat.emissive)mat.emissive.setHex(0);mat.emissiveIntensity=0;
972
- const PR={chrome:{c:'#d4d4d4',m:.95,r:.05},gold:{c:'#FFD700',m:.9,r:.1},copper:{c:'#b87333',m:.85,r:.2},
973
- rubber:{c:'#1a1a1a',m:0,r:1},glass:{c:'#aaddff',m:0,r:0,tr:true,op:.25},
974
- neon:{c:'#00ffff',m:0,r:1,e:'#00ffff',ei:4},emit:{c:'#ff8800',m:0,r:1,e:'#ff8800',ei:3},
975
- lava:{c:'#331100',m:0,r:1,e:'#ff4400',ei:.8},ice:{c:'#aaddff',m:.1,r:.05,tr:true,op:.7},
976
- steel:{c:'#445566',m:.8,r:.3}};
977
- const pr=PR[p];if(!pr)return;
978
- mat.color.set(pr.c);mat.metalness=pr.m;mat.roughness=pr.r;
979
- if(pr.tr){mat.transparent=true;mat.opacity=pr.op;}
980
- if(pr.e&&mat.emissive){mat.emissive.set(pr.e);mat.emissiveIntensity=pr.ei;}
981
- mat.needsUpdate=true;refreshPV();toast('Preset: '+p);
982
- }
983
- function applyTexFile(input){
984
- const file=input.files[0];if(!file||!SEL||SEL.type!=='MESH')return;
985
- const url=URL.createObjectURL(file);
986
- new THREE.TextureLoader().load(url,tex=>{tex.wrapS=tex.wrapT=THREE.RepeatWrapping;SEL.mesh.material.map=tex;SEL.mesh.material.needsUpdate=true;toast('Texture applied');});
987
- input.value='';
988
- }
989
- function toggleSec(id){const el=document.getElementById(id);if(el)el.style.display=el.style.display==='none'?'':'none';}
990
- function setRTab(t){['out','props','mat','render','sculpt'].forEach(tt=>{document.getElementById('rt-'+tt)?.classList.toggle('on',tt===t);document.getElementById('rb-'+tt)?.classList.toggle('on',tt===t);});}
991
- function setRP(v){const[w,h]=v.split(',');document.getElementById('rW').value=w;document.getElementById('rH').value=h;}
992
- function refreshStatus(){
993
- document.getElementById('st-objs').textContent=' '+OBJS.length;
994
- if(SEL?.mesh?.geometry?.attributes?.position)document.getElementById('st-verts').textContent=' '+SEL.mesh.geometry.attributes.position.count;
995
- else document.getElementById('st-verts').textContent=' 0';
996
- }
997
- function refreshBadge(){const b=document.getElementById('vbadge');if(b)b.textContent=MODE.charAt(0).toUpperCase()+MODE.slice(1)+' Mode · '+TOOL+(ORTHO?' [Ortho]':'')+' · RMB=Orbit · Scroll=Zoom · Shift+RMB=Pan';}
998
-
999
- // ── GIZMO ──
1000
- function drawGizmo(){
1001
- const cv=document.getElementById('gizmo');if(!cv||!CAM)return;
1002
- const ctx=cv.getContext('2d'),sz=68,cx=34,cy=34,r=24;ctx.clearRect(0,0,sz,sz);
1003
- ctx.fillStyle='rgba(0,0,0,.45)';ctx.beginPath();ctx.arc(cx,cy,r+5,0,Math.PI*2);ctx.fill();
1004
- const right=new THREE.Vector3().setFromMatrixColumn(CAM.matrixWorld,0),up=new THREE.Vector3().setFromMatrixColumn(CAM.matrixWorld,1),dir=new THREE.Vector3();CAM.getWorldDirection(dir);
1005
- [{n:'X',c:'#e44',v:new THREE.Vector3(1,0,0)},{n:'Y',c:'#4e4',v:new THREE.Vector3(0,1,0)},{n:'Z',c:'#55f',v:new THREE.Vector3(0,0,1)},
1006
- {n:'',c:'#622',v:new THREE.Vector3(-1,0,0)},{n:'',c:'#262',v:new THREE.Vector3(0,-1,0)},{n:'',c:'#226',v:new THREE.Vector3(0,0,-1)}]
1007
- .sort((a,b)=>a.v.dot(dir)-b.v.dot(dir)).forEach(ax=>{
1008
- const px=ax.v.dot(right)*r,py=-ax.v.dot(up)*r;const neg=!ax.n;
1009
- ctx.globalAlpha=neg?.28:.9;ctx.strokeStyle=ax.c;ctx.lineWidth=neg?1:2;
1010
- ctx.beginPath();ctx.moveTo(cx,cy);ctx.lineTo(cx+px,cy+py);ctx.stroke();
1011
- if(!neg){ctx.fillStyle=ax.c;ctx.beginPath();ctx.arc(cx+px,cy+py,5,0,Math.PI*2);ctx.fill();ctx.fillStyle='#fff';ctx.font='bold 7px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(ax.n,cx+px,cy+py);}
1012
- });ctx.globalAlpha=1;
1013
- }
1014
-
1015
- // ── TIMELINE ──
1016
- function setupTL(){
1017
- const cv=document.getElementById('tlcanvas');if(!cv)return;
1018
- cv.addEventListener('click',e=>{const r=cv.getBoundingClientRect();setFr(Math.max(1,Math.round((e.clientX-r.left)/cv.offsetWidth*(FR_END-1)+1)));});
1019
- }
1020
- function setupTLR(){
1021
- const h=document.getElementById('tl-resize');if(!h)return;let sy,sh;
1022
- h.addEventListener('mousedown',e=>{sy=e.clientY;sh=document.getElementById('timeline-wrap').offsetHeight;});
1023
- window.addEventListener('mousemove',e=>{if(sy===undefined)return;document.getElementById('timeline-wrap').style.height=Math.max(55,Math.min(250,sh-(e.clientY-sy)))+'px';});
1024
- window.addEventListener('mouseup',()=>{sy=undefined;});
1025
- }
1026
- function tla(a){switch(a){case'first':setFr(1);break;case'back':setFr(Math.max(1,FR-1));break;case'play':TL_PLAY=!TL_PLAY;document.getElementById('tlplay').textContent=TL_PLAY?'⏸':'▶';if(TL_PLAY)playL();break;case'fwd':setFr(Math.min(FR_END,FR+1));break;case'last':setFr(FR_END);break;}}
1027
- function playL(){if(!TL_PLAY)return;FR++;if(FR>FR_END)FR=1;setFr(FR);setTimeout(playL,1000/FR_FPS);}
1028
- function setFr(f){FR=clamp(f,1,FR_END);document.getElementById('tlframe').value=FR;applyKF();drawTL();}
1029
- function insKF(ch){
1030
- if(!SEL)return;const name=SEL.name,m=SEL.mesh;if(!KF[name])KF[name]={};
1031
- const vals={lx:m.position.x,ly:m.position.y,lz:m.position.z,rx:m.rotation.x,ry:m.rotation.y,rz:m.rotation.z,sx:m.scale.x,sy:m.scale.y,sz:m.scale.z};
1032
- const sets={loc:['lx','ly','lz'],rot:['rx','ry','rz'],scl:['sx','sy','sz'],all:Object.keys(vals)};
1033
- (sets[ch]||[ch]).forEach(c=>{if(!KF[name][c])KF[name][c]={};KF[name][c][FR]=vals[c];});
1034
- drawTL();toast('KF at frame '+FR);
1035
- }
1036
- function delKF(){if(!SEL)return;const name=SEL.name;if(!KF[name])return;Object.values(KF[name]).forEach(ch=>{delete ch[FR];});drawTL();toast('KF deleted');}
1037
- function applyKF(){
1038
- if(!SEL)return;const name=SEL.name;if(!KF[name])return;const m=SEL.mesh;
1039
- function lv(ch){const kfs=KF[name][ch];if(!kfs)return null;const fr=Object.keys(kfs).map(Number).sort((a,b)=>a-b);if(!fr.length)return null;if(FR<=fr[0])return kfs[fr[0]];if(FR>=fr[fr.length-1])return kfs[fr[fr.length-1]];for(let i=0;i<fr.length-1;i++){if(FR>=fr[i]&&FR<=fr[i+1]){const t=(FR-fr[i])/(fr[i+1]-fr[i]);return kfs[fr[i]]+(kfs[fr[i+1]]-kfs[fr[i]])*t;}}return null;}
1040
- ['lx','ly','lz'].forEach((c,i)=>{const v=lv(c);if(v!==null)m.position.setComponent(i,v);});
1041
- const rx=lv('rx'),ry=lv('ry'),rz=lv('rz');if(rx!==null)m.rotation.x=rx;if(ry!==null)m.rotation.y=ry;if(rz!==null)m.rotation.z=rz;
1042
- ['sx','sy','sz'].forEach((c,i)=>{const v=lv(c);if(v!==null)m.scale.setComponent(i,v);});
1043
- if(SEL.light)SEL.light.position.copy(m.position);refreshPV();
1044
- }
1045
- function drawTL(){
1046
- const cv=document.getElementById('tlcanvas');if(!cv)return;const w=cv.offsetWidth,h=cv.offsetHeight;cv.width=w;cv.height=h;if(!w||!h)return;
1047
- const ctx=cv.getContext('2d');ctx.fillStyle='#0d0d0d';ctx.fillRect(0,0,w,h);
1048
- const step=Math.max(1,Math.floor(FR_END/20));for(let f=0;f<=FR_END;f+=step){const x=(f/(FR_END-1))*w;ctx.strokeStyle='#222';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,h);ctx.stroke();}
1049
- if(SEL){const kfs=KF[SEL.name]||{};const colors={lx:'#e44',ly:'#4e4',lz:'#66f',rx:'#ea4',ry:'#4ea',rz:'#a4e'};const chs=['lx','ly','lz','rx','ry','rz'];
1050
- chs.forEach((ch,ci)=>{if(!kfs[ch])return;const cy=((ci+.5)/chs.length)*h;Object.keys(kfs[ch]).forEach(f=>{const x=((f-1)/(FR_END-1))*w;ctx.fillStyle=colors[ch]||'#fff';ctx.beginPath();ctx.moveTo(x,cy-5);ctx.lineTo(x+4,cy);ctx.lineTo(x,cy+5);ctx.lineTo(x-4,cy);ctx.closePath();ctx.fill();});});}
1051
- const px=((FR-1)/(Math.max(1,FR_END-1)))*w;ctx.strokeStyle='#e87d39';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(px,0);ctx.lineTo(px,h);ctx.stroke();ctx.fillStyle='#e87d39';ctx.beginPath();ctx.moveTo(px-5,0);ctx.lineTo(px+5,0);ctx.lineTo(px,9);ctx.closePath();ctx.fill();
1052
- }
1053
-
1054
- // ── SCRIPT EDITOR ──
1055
- function runScript(){
1056
- const code=document.getElementById('scriptarea').value,out=document.getElementById('scriptout');out.textContent='';
1057
- const ol=console.log;console.log=(...a)=>{out.textContent+=a.join(' ')+'\n';ol(...a);};
1058
- try{
1059
- new Function('SCENE','OBJS','CAM','KF','FR_FPS','FR_END','addObj','addLight','act','toast','THREE','drawTL','refreshOL','refreshStatus','applyPreset','SEL','applyKF','setFr',
1060
- code)(SCENE,OBJS,CAM,KF,FR_FPS,FR_END,addObj,addLight,act,toast,THREE,drawTL,refreshOL,refreshStatus,applyPreset,SEL,applyKF,setFr);
1061
- refreshOL();refreshStatus();if(!out.textContent)out.textContent='// Done ✓';
1062
- }catch(e){out.textContent='❌ '+e.message;}
1063
- console.log=ol;
1064
- }
1065
- function clearScript(){document.getElementById('scriptarea').value='';document.getElementById('scriptout').textContent='// Cleared';}
1066
- function insTpl(t){
1067
- const T={
1068
- cubes:`for(let i=0;i<8;i++){const o=addObj('cube');o.mesh.position.set((Math.random()-.5)*12,Math.random()*4+.5,(Math.random()-.5)*12);o.mesh.material.color.setHSL(i/8,.75,.5);o.mesh.material.metalness=Math.random();o.mesh.material.roughness=Math.random();o.mesh.scale.setScalar(.5+Math.random()*1.5);}toast('8 cubes!');`,
1069
-
1070
- solar:`OBJS.forEach(o=>{if(o.light)SCENE.remove(o.light);if(o.helper)SCENE.remove(o.helper);SCENE.remove(o.mesh);});OBJS.length=0;
1071
- const sun=new THREE.Mesh(new THREE.SphereGeometry(2.5,32,32),new THREE.MeshStandardMaterial({color:'#FDB813',emissive:'#FDB813',emissiveIntensity:2,roughness:1}));SCENE.add(sun);OBJS.push({mesh:sun,light:null,name:'Sun',type:'MESH',vis:true,kf:{}});
1072
- SCENE.add(new THREE.PointLight(0xfff4e0,3,300));SCENE.add(new THREE.AmbientLight(0x111133,.4));
1073
- const pd=[['Mercury','#b5b5b5',.3,5,4.7],['Venus','#e8cda0',.5,8,3.5],['Earth','#3b7fc4',.6,11,3.0],['Mars','#c1440e',.38,15,2.4],['Jupiter','#c88b3a',1.2,22,1.3],['Saturn','#e4d191',1.0,30,1.0],['Uranus','#7de8e8',.7,38,.7],['Neptune','#4b70dd',.65,45,.5]];
1074
- const planets=[];pd.forEach(([name,col,r,orb,spd])=>{const m=new THREE.Mesh(new THREE.SphereGeometry(r,32,32),new THREE.MeshStandardMaterial({color:col,roughness:.8,metalness:.1}));m.castShadow=true;m.position.x=orb;SCENE.add(m);OBJS.push({mesh:m,light:null,name,type:'MESH',vis:true,kf:{}});SCENE.add(new THREE.Mesh(new THREE.RingGeometry(orb-.02,orb+.02,64),new THREE.MeshBasicMaterial({color:0x333333,side:THREE.DoubleSide,rotation:new THREE.Euler(-Math.PI/2,0,0)}))).rotation.x=-Math.PI/2;if(name==='Saturn'){const rm=new THREE.Mesh(new THREE.RingGeometry(r+.4,r+1.1,64),new THREE.MeshStandardMaterial({color:'#d4c080',side:THREE.DoubleSide,transparent:true,opacity:.7}));rm.rotation.x=Math.PI/3;m.add(rm);}planets.push({mesh:m,orb,spd:spd*.25,angle:Math.random()*Math.PI*2});});
1075
- const t0=Date.now();(function A(){requestAnimationFrame(A);const t=(Date.now()-t0)*.001;planets.forEach(p=>{p.mesh.position.x=Math.cos(p.angle+t*p.spd)*p.orb;p.mesh.position.z=Math.sin(p.angle+t*p.spd)*p.orb;p.mesh.rotation.y+=.008;});sun.rotation.y+=.003;})();
1076
- refreshOL();toast('Animated Solar System!');`,
1077
-
1078
- blackhole:`OBJS.forEach(o=>{if(o.light)SCENE.remove(o.light);if(o.helper)SCENE.remove(o.helper);SCENE.remove(o.mesh);});OBJS.length=0;
1079
- const core=new THREE.Mesh(new THREE.SphereGeometry(2,32,32),new THREE.MeshStandardMaterial({color:'#000000',emissive:'#110022',emissiveIntensity:.3,roughness:1}));SCENE.add(core);OBJS.push({mesh:core,light:null,name:'Core',type:'MESH',vis:true,kf:{}});
1080
- [[2.8,4.5,'#ff6600',2.5,.8],[4.8,6.5,'#ff9900',2,.7],[6.8,8.5,'#ffcc00',1.5,.6],[8.8,10.5,'#ff4400',1,.5],[11,13,'#882200',.7,.4]].forEach(([i,o,col,ei,op],idx)=>{const ring=new THREE.Mesh(new THREE.RingGeometry(i,o,128),new THREE.MeshStandardMaterial({color:col,emissive:col,emissiveIntensity:ei,transparent:true,opacity:op,side:THREE.DoubleSide}));ring.rotation.x=Math.PI/2+idx*.2;SCENE.add(ring);OBJS.push({mesh:ring,light:null,name:'Disk'+idx,type:'MESH',vis:true,kf:{}});});
1081
- const pr=new THREE.Mesh(new THREE.TorusGeometry(2.4,.08,16,128),new THREE.MeshStandardMaterial({color:'#ffffff',emissive:'#ffffff',emissiveIntensity:5}));SCENE.add(pr);OBJS.push({mesh:pr,light:null,name:'PhotonRing',type:'MESH',vis:true,kf:{}});
1082
- [1,-1].forEach(dir=>{const curve=new THREE.CatmullRomCurve3([new THREE.Vector3(0,0,0),new THREE.Vector3(0,dir*12,0),new THREE.Vector3(0,dir*25,0)]);const jet=new THREE.Mesh(new THREE.TubeGeometry(curve,32,.15,8,false),new THREE.MeshStandardMaterial({color:'#3366ff',emissive:'#3366ff',emissiveIntensity:3,transparent:true,opacity:.6}));SCENE.add(jet);OBJS.push({mesh:jet,light:null,name:'Jet',type:'MESH',vis:true,kf:{}});});
1083
- const pg=new THREE.BufferGeometry();const pp=[];for(let i=0;i<200;i++){const a=Math.random()*Math.PI*2,r=3+Math.random()*10;pp.push(Math.cos(a)*r,(Math.random()-.5)*2,Math.sin(a)*r);}pg.setAttribute('position',new THREE.Float32BufferAttribute(pp,3));const stars=new THREE.Points(pg,new THREE.PointsMaterial({color:'#ffaa44',size:.12}));SCENE.add(stars);OBJS.push({mesh:stars,light:null,name:'Particles',type:'MESH',vis:true,kf:{}});
1084
- SCENE.add(new THREE.PointLight(0xff4400,2,40));SCENE.add(new THREE.AmbientLight(0x111122,.5));
1085
- (function A(){requestAnimationFrame(A);core.rotation.y+=.005;stars.rotation.y+=.003;pr.rotation.z+=.02;})();
1086
- refreshOL();toast('Black Hole ready!');`,
1087
-
1088
- missile:`OBJS.forEach(o=>{if(o.light)SCENE.remove(o.light);if(o.helper)SCENE.remove(o.helper);SCENE.remove(o.mesh);});OBJS.length=0;
1089
- const dS=new THREE.MeshStandardMaterial({color:'#2a3344',metalness:.8,roughness:.3});const CH=new THREE.MeshStandardMaterial({color:'#d4d4d4',metalness:.95,roughness:.05});const OR=new THREE.MeshStandardMaterial({color:'#ff6600',emissive:'#ff6600',emissiveIntensity:2,roughness:1});
1090
- const pts=[new THREE.Vector2(0,0),new THREE.Vector2(.5,.5),new THREE.Vector2(.6,1.5),new THREE.Vector2(.58,3),new THREE.Vector2(.5,4.5),new THREE.Vector2(.3,5.5),new THREE.Vector2(.15,6),new THREE.Vector2(0,6.5)];
1091
- const body=new THREE.Mesh(new THREE.LatheGeometry(pts,32),dS.clone());body.castShadow=true;body.position.y=1;SCENE.add(body);OBJS.push({mesh:body,light:null,name:'Body',type:'MESH',vis:true,kf:{}});
1092
- const nose=new THREE.Mesh(new THREE.ConeGeometry(.15,1.2,24),CH.clone());nose.position.y=7.9;SCENE.add(nose);OBJS.push({mesh:nose,light:null,name:'Nose',type:'MESH',vis:true,kf:{}});
1093
- for(let i=0;i<4;i++){const sh=new THREE.Shape();sh.moveTo(0,0);sh.lineTo(.9,0);sh.lineTo(0,2);sh.closePath();const fin=new THREE.Mesh(new THREE.ExtrudeGeometry(sh,{depth:.04,bevelEnabled:false}),dS.clone());fin.castShadow=true;const a=i*Math.PI/2;fin.position.set(Math.cos(a)*.58,1.1,Math.sin(a)*.58);fin.rotation.y=-a;SCENE.add(fin);OBJS.push({mesh:fin,light:null,name:'Fin'+i,type:'MESH',vis:true,kf:{}});}
1094
- const nozzle=new THREE.Mesh(new THREE.CylinderGeometry(.55,.35,.6,24),new THREE.MeshStandardMaterial({color:'#1a1a1a',metalness:.9,roughness:.1}));nozzle.position.y=.7;SCENE.add(nozzle);OBJS.push({mesh:nozzle,light:null,name:'Nozzle',type:'MESH',vis:true,kf:{}});
1095
- const glow=new THREE.Mesh(new THREE.CylinderGeometry(.3,.5,.8,24),OR.clone());glow.position.y=.15;SCENE.add(glow);OBJS.push({mesh:glow,light:null,name:'Exhaust',type:'MESH',vis:true,kf:{}});
1096
- const gring=new THREE.Mesh(new THREE.TorusGeometry(.42,.06,12,32),OR.clone());gring.position.y=.45;gring.rotation.x=Math.PI/2;SCENE.add(gring);OBJS.push({mesh:gring,light:null,name:'GlowRing',type:'MESH',vis:true,kf:{}});
1097
- const band=new THREE.Mesh(new THREE.CylinderGeometry(.62,.62,.18,32),new THREE.MeshStandardMaterial({color:'#cc1111',metalness:.7,roughness:.2}));band.position.y=6.2;SCENE.add(band);OBJS.push({mesh:band,light:null,name:'Warhead',type:'MESH',vis:true,kf:{}});
1098
- for(let i=0;i<4;i++){const a=i*Math.PI/2+Math.PI/4;const curve=new THREE.CatmullRomCurve3([new THREE.Vector3(Math.cos(a)*.65,1.2,Math.sin(a)*.65),new THREE.Vector3(Math.cos(a)*.62,3.5,Math.sin(a)*.62),new THREE.Vector3(Math.cos(a)*.58,5.5,Math.sin(a)*.58)]);const tube=new THREE.Mesh(new THREE.TubeGeometry(curve,32,.04,8,false),new THREE.MeshStandardMaterial({color:'#556677',metalness:.6,roughness:.4}));tube.castShadow=true;SCENE.add(tube);OBJS.push({mesh:tube,light:null,name:'Tube'+i,type:'MESH',vis:true,kf:{}});}
1099
- for(let i=0;i<16;i++){const a=i*Math.PI/8,rv=new THREE.Mesh(new THREE.SphereGeometry(.04,8,8),CH.clone());rv.position.set(Math.cos(a)*.61,2.5,Math.sin(a)*.61);SCENE.add(rv);OBJS.push({mesh:rv,light:null,name:'Rivet'+i,type:'MESH',vis:true,kf:{}});}
1100
- SCENE.add(new THREE.PointLight(0xff6600,3,20));SCENE.add(new THREE.AmbientLight(0x334455,.6));SCENE.add(new THREE.DirectionalLight(0xffffff,1.2));
1101
- refreshOL();toast('Missile ready — 15+ parts!');`
1102
- };
1103
- if(T[t])document.getElementById('scriptarea').value=T[t];else toast('Template not found');
1104
- }
1105
-
1106
- // ── RENDER LOOP + UTILS ──
1107
- function loop(){requestAnimationFrame(loop);_fc++;const n=performance.now();if(n-_fl>=1000){document.getElementById('st-fps').textContent='FPS:'+_fc;_fc=0;_fl=n;}RENDERER.render(SCENE,getCAM());}
1108
- function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
1109
- let _tt;function toast(msg,dur=3000){const el=document.getElementById('toast');if(!el)return;el.textContent=msg;el.style.display='block';clearTimeout(_tt);_tt=setTimeout(()=>el.style.display='none',dur);}
1110
-
1111
- // ═══════════════════════════════════════════
1112
- // AI ASSISTANT
1113
- // ═══════════════════════════════════════════
1114
- function toggleAI(){AI_OPEN=!AI_OPEN;document.getElementById('ai-panel').classList.toggle('open',AI_OPEN);}
1115
- function setM(m){AI_MODEL=m;document.querySelectorAll('.ai-mtab').forEach(b=>b.classList.remove('on'));document.getElementById('abt-'+m)?.classList.add('on');}
1116
- function toggleTex(){const d=document.getElementById('tex-dlg');d.classList.toggle('show');document.getElementById('af').classList.remove('show');if(d.classList.contains('show'))document.getElementById('tex-inp').focus();}
1117
- function toggleAF(){const f=document.getElementById('af');f.classList.toggle('show');document.getElementById('tex-dlg').classList.remove('show');}
1118
-
1119
- async function checkStatus(){
1120
- const st=document.getElementById('ai-status');st.textContent='Checking…';
1121
- try{
1122
- const r=await fetch('/api/status');if(!r.ok)throw new Error(r.status);
1123
- const data=await r.json();const ok=Object.values(data).filter(v=>String(v).startsWith('✅')).length;
1124
- st.textContent=ok+' ready';st.style.background=ok>0?'rgba(0,128,0,.4)':'rgba(128,0,0,.4)';
1125
- addMsg('📡 Status:\n'+Object.values(data).join('\n'),'ai','groq');
1126
- }catch(e){st.textContent='⚠ Offline';st.style.background='rgba(128,0,0,.4)';addMsg('❌ Server offline — check app.py is running','ai','err');}
1127
- }
1128
-
1129
- function quickAsk(msg){document.getElementById('ai-inp').value=msg;sendAI();}
1130
- function quickTex(q){document.getElementById('tex-inp').value=q;sendTex();}
1131
-
1132
- function addMsg(text,role,model){
1133
- const msgs=document.getElementById('ai-msgs'),d=document.createElement('div');
1134
- d.className=role==='user'?'ai-mu':('ai-ma '+(model||''));d.textContent=text;msgs.appendChild(d);msgs.scrollTop=msgs.scrollHeight;return d;
1135
- }
1136
- function showThink(){const msgs=document.getElementById('ai-msgs'),d=document.createElement('div');d.className='ai-ma';d.id='ai-th';d.innerHTML='<div class="ai-think"><div class="ai-dot"></div><div class="ai-dot"></div><div class="ai-dot"></div></div>';msgs.appendChild(d);msgs.scrollTop=msgs.scrollHeight;}
1137
- function rmThink(){document.getElementById('ai-th')?.remove();}
1138
-
1139
- async function sendAI(){
1140
- const inp=document.getElementById('ai-inp'),msg=inp.value.trim();if(!msg)return;
1141
- inp.value='';document.getElementById('ai-send').disabled=true;addMsg(msg,'user');showThink();
1142
- const model=AI_MODEL;if(!AI_HIST[model])AI_HIST[model]=[];
1143
- const objCtx=OBJS.length?OBJS.slice(0,15).map(o=>`"${o.name}"(${o.type})[${o.mesh.position.x.toFixed(1)},${o.mesh.position.y.toFixed(1)},${o.mesh.position.z.toFixed(1)}]`).join(','):'empty';
1144
- const enhanced=msg+`\n\nSCENE: ${objCtx}. Selected: ${SEL?.name||'none'}. Objects: ${OBJS.length}.\n\nQUALITY: Output ONLY raw JS. PBR always (metalness+roughness). castShadow=true. For missiles use LatheGeometry+ExtrudeGeometry fins+TubeGeometry+emissive nozzle (12+ parts). For black holes use 5 RingGeometry disks+emissive+particles. For solar systems use requestAnimationFrame orbital loops NOT static placement. Add colored point lights. Register objects with OBJS.push({mesh,light:null,name,type:'MESH',vis:true,kf:{}}). End with toast().`;
1145
- try{
1146
- const res=await fetch('/api/'+model,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({messages:[...AI_HIST[model],{role:'user',content:enhanced}],max_tokens:model==='openrouter'?3500:2500,temperature:.25})});
1147
- const data=await res.json();rmThink();
1148
- if(data.error){addMsg('❌ '+data.error,'ai','err');document.getElementById('ai-send').disabled=false;return;}
1149
- let code=(data.choices?.[0]?.message?.content||'').trim().replace(/^```[\w]*\n?/gm,'').replace(/\n?```$/gm,'').trim();
1150
- if(!code){addMsg('⚠ Empty response. Try again or use a different model.','ai','err');document.getElementById('ai-send').disabled=false;return;}
1151
- AI_HIST[model]=[...AI_HIST[model],{role:'user',content:msg},{role:'assistant',content:code.substring(0,400)}].slice(-8);
1152
- const lbl={groq:'⚡ Groq',nemotron:'🟢 Nemotron',openrouter:'🔮 Detail AI'};
1153
- const used=data._fallback?'⚡ Groq(fallback)':lbl[model];
1154
- document.getElementById('scriptarea').value='// '+used+' — '+new Date().toLocaleTimeString()+'\n// '+msg.substring(0,80)+'\n\n'+code;
1155
- const pre=document.getElementById('ai-pre');pre.textContent=code.substring(0,200)+(code.length>200?'…':'');pre.style.display='block';setTimeout(()=>pre.style.display='none',7000);
1156
- try{
1157
- new Function('SCENE','OBJS','CAM','KF','FR_FPS','FR_END','addObj','addLight','act','toast','THREE','drawTL','refreshOL','refreshStatus','applyPreset','SEL','applyKF','setFr',
1158
- code)(SCENE,OBJS,CAM,KF,FR_FPS,FR_END,addObj,addLight,act,toast,THREE,drawTL,refreshOL,refreshStatus,applyPreset,SEL,applyKF,setFr);
1159
- refreshOL();refreshStatus();addMsg('✅ Done! ('+used+')\nCode saved in 📝 Script tab.','ai',model);
1160
- }catch(e){addMsg('⚠ Execution error: '+e.message+'\n\nCode in 📝 Script tab — edit and re-run.','ai','err');}
1161
- }catch(e){rmThink();addMsg('❌ Network error: '+e.message,'ai','err');}
1162
- document.getElementById('ai-send').disabled=false;
1163
- }
1164
-
1165
- async function sendTex(){
1166
- const inp=document.getElementById('tex-inp'),q=inp.value.trim();if(!q)return;
1167
- inp.value='';document.getElementById('tex-dlg').classList.remove('show');addMsg('🎨 Texture: "'+q+'"','user');showThink();
1168
- try{
1169
- const res=await fetch('/api/texture-search',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({query:q,object:SEL?.name||'selected object'})});
1170
- const data=await res.json();rmThink();
1171
- if(data.error){addMsg('❌ '+data.error,'ai','err');return;}
1172
- let code=(data.choices?.[0]?.message?.content||'').trim().replace(/^```[\w]*\n?/gm,'').replace(/\n?```$/gm,'').trim();
1173
- document.getElementById('scriptarea').value='// Texture: '+q+'\n'+code;
1174
- try{new Function('SCENE','OBJS','CAM','SEL','THREE','toast','refreshOL','refreshStatus',code)(SCENE,OBJS,CAM,SEL,THREE,toast,refreshOL,refreshStatus);addMsg('✅ Texture applied!','ai','groq');}
1175
- catch(e){addMsg('⚠ '+e.message+'\nCode in 📝 Script tab.','ai','err');}
1176
- }catch(e){rmThink();addMsg('❌ '+e.message,'ai','err');}
1177
- }
1178
-
1179
- async function genAnim(){
1180
- const desc=document.getElementById('af-desc').value.trim();if(!desc){toast('Describe the animation first');return;}
1181
- const dur=parseInt(document.getElementById('af-dur').value),fps=parseInt(document.getElementById('af-fps').value);
1182
- const type=document.getElementById('af-type').value,ease=document.getElementById('af-ease').value;
1183
- const loop=document.getElementById('af-loop').checked,total=dur*fps;
1184
- document.getElementById('tlend').value=total;FR_END=total;FR_FPS=fps;document.getElementById('af').classList.remove('show');
1185
- const objCtx=OBJS.slice(0,12).map(o=>`"${o.name}"`).join(',');
1186
- const prompt=`Create ${type} animation: ${desc}\nObjects: ${objCtx}. Selected: ${SEL?.name||'none'}.\nFrames: ${total} (${dur}s@${fps}fps). Easing: ${ease}. Loop: ${loop}.\n\nOUTPUT ONLY RAW JS — no markdown.\nRules:\n1. Set: document.getElementById('tlend').value='${total}';\n2. Easing: const eIO=t=>t<.5?2*t*t:1-Math.pow(-2*t+2,2)/2;\n3. For orbits/solar: use requestAnimationFrame with Math.cos/sin — NOT keyframes alone\n4. KF: if(!KF['name'])KF['name']={};if(!KF['name']['ry'])KF['name']['ry']={};\n for(let f=0;f<=${total};f+=2){const t=f/${total};KF['name']['ry'][f]=eIO(t)*Math.PI*2;}\n5. After KFs: drawTL();applyKF();toast('Animation ready! Press Space');`;
1187
- addMsg('🎬 Generating: '+desc,'user');showThink();
1188
- try{
1189
- const res=await fetch('/api/openrouter',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({messages:[{role:'user',content:prompt}],max_tokens:2500,temperature:.15})});
1190
- const data=await res.json();rmThink();
1191
- let code=(data.choices?.[0]?.message?.content||'').trim().replace(/^```[\w]*\n?/gm,'').replace(/\n?```$/gm,'').trim();
1192
- if(!code){addMsg('⚠ No animation code. Code in 📝 Script tab.','ai','err');return;}
1193
- document.getElementById('scriptarea').value='// Animation: '+desc+'\n'+code;
1194
- try{new Function('SCENE','OBJS','CAM','KF','FR_FPS','FR_END','addObj','addLight','act','toast','THREE','drawTL','refreshOL','refreshStatus','applyKF','setFr','SEL',code)(SCENE,OBJS,CAM,KF,FR_FPS,FR_END,addObj,addLight,act,toast,THREE,drawTL,refreshOL,refreshStatus,applyKF,setFr,SEL);addMsg('✅ Animation generated! Press Space to play.','ai','openrouter');}
1195
- catch(e){addMsg('⚠ Anim error: '+e.message+'\nCode in 📝 Script tab.','ai','err');}
1196
- }catch(e){rmThink();addMsg('❌ '+e.message,'ai','err');}
1197
- }
1198
- </script>
1199
- </body>
1200
- </html>