Spaces:
Sleeping
Sleeping
Delete static/index.html
Browse files- 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 & 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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|