VISHAL18for4 commited on
Commit
064ada4
Β·
verified Β·
1 Parent(s): 489b57d

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +1290 -0
index.html ADDED
@@ -0,0 +1,1290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
6
+ <title>DroneLab Pro</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/dracula.min.css">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/eclipse.min.css">
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/python/python.min.js"></script>
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/edit/closebrackets.min.js"></script>
13
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/edit/matchbrackets.min.js"></script>
14
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
16
+ <script src="https://skulpt.org/js/skulpt.min.js"></script>
17
+ <script src="https://skulpt.org/js/skulpt-stdlib.js"></script>
18
+ <style>
19
+ [data-theme=dark]{--bg0:#07090f;--bg1:#0d1117;--bg2:#161b22;--bg3:#21262d;--bdr:#30363d;--txt:#f0f6fc;--sub:#8b949e;--acc:#00e5ff;--acc2:#7c3aed;--grn:#3fb950;--red:#f85149;--yel:#e3b341;--cbg:#0d1117;--cso:#060a0f;--hdr:#010409}
20
+ [data-theme=light]{--bg0:#f6f8fa;--bg1:#fff;--bg2:#eaecef;--bg3:#d8dadd;--bdr:#d0d7de;--txt:#1f2328;--sub:#57606a;--acc:#0969da;--acc2:#8250df;--grn:#1a7f37;--red:#cf222e;--yel:#9a6700;--cbg:#fff;--cso:#f6f8fa;--hdr:#fff}
21
+ *{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
22
+ html,body{height:100%;overflow:hidden;background:var(--bg0);color:var(--txt);font-family:'SF Mono','JetBrains Mono','Fira Code',Consolas,monospace;font-size:13px;transition:background .25s,color .25s}
23
+ #H{height:44px;display:flex;align-items:center;padding:0 10px;gap:7px;background:var(--hdr);border-bottom:1px solid var(--bdr);flex-shrink:0;z-index:100}
24
+ #logo{display:flex;align-items:center;gap:7px;text-decoration:none}
25
+ #logo-i{width:26px;height:26px;border-radius:6px;background:linear-gradient(135deg,#00e5ff,#7c3aed);display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0}
26
+ #logo-t{font-size:14px;font-weight:800}
27
+ #logo-t b{color:var(--acc)}
28
+ .tag{font-size:9px;padding:1px 5px;border-radius:6px;background:linear-gradient(90deg,var(--acc),var(--acc2));color:#fff;font-weight:700;letter-spacing:.8px}
29
+ #HTABS{flex:1;display:flex;align-items:center;justify-content:center;gap:2px}
30
+ .ht{height:32px;padding:0 12px;border:none;border-radius:5px;background:none;color:var(--sub);font-family:inherit;font-size:12px;cursor:pointer;border-bottom:2px solid transparent;transition:all .2s}
31
+ .ht.on{color:var(--txt);border-bottom-color:var(--acc)}
32
+ .ht:hover:not(.on){color:var(--txt)}
33
+ #HR{display:flex;align-items:center;gap:5px}
34
+ .hb{height:28px;padding:0 9px;border-radius:5px;border:1px solid var(--bdr);background:var(--bg3);color:var(--txt);font-family:inherit;font-size:11px;font-weight:600;cursor:pointer;transition:all .18s;white-space:nowrap;display:flex;align-items:center;gap:4px}
35
+ .hb:hover{border-color:var(--acc);color:var(--acc)}
36
+ #RUN{background:var(--grn);border-color:var(--grn);color:#000;height:30px;padding:0 13px;font-size:12px}
37
+ #RUN:hover{filter:brightness(1.12)}
38
+ #STP{background:var(--red);border-color:var(--red);color:#fff;height:30px;display:none}
39
+ #DOT{width:7px;height:7px;border-radius:50%;background:var(--bg3);flex-shrink:0;transition:all .3s}
40
+ #DOT.live{background:var(--grn);box-shadow:0 0 8px var(--grn)}
41
+ #DOT.err{background:var(--red);box-shadow:0 0 8px var(--red)}
42
+ #MAIN{height:calc(100vh - 44px);display:flex;overflow:hidden}
43
+ #SP{flex:1;min-width:0;display:flex;flex-direction:column;background:var(--bg0)}
44
+ #STB{display:flex;align-items:center;padding:4px 8px;gap:6px;background:var(--bg2);border-bottom:1px solid var(--bdr);flex-shrink:0;overflow-x:auto}
45
+ #STB::-webkit-scrollbar{display:none}
46
+ #STB label{font-size:9px;color:var(--sub);letter-spacing:.5px;text-transform:uppercase;white-space:nowrap;flex-shrink:0}
47
+ .ss{height:24px;padding:0 6px;border-radius:4px;border:1px solid var(--bdr);background:var(--bg1);color:var(--txt);font-family:inherit;font-size:11px;cursor:pointer;flex-shrink:0}
48
+ .ss:hover,.sb:hover{border-color:var(--acc)}
49
+ .sb{height:24px;padding:0 8px;border-radius:4px;border:1px solid var(--bdr);background:var(--bg1);color:var(--txt);font-family:inherit;font-size:11px;cursor:pointer;flex-shrink:0}
50
+ .sb:hover{color:var(--acc)}
51
+ #CVW{flex:1;position:relative;overflow:hidden}
52
+ canvas#C{width:100%;height:100%;display:block;cursor:grab;touch-action:none}
53
+ canvas#C:active{cursor:grabbing}
54
+ #HUD{position:absolute;top:8px;left:8px;pointer-events:none;display:flex;flex-direction:column;gap:4px}
55
+ .hr{display:flex;gap:4px}
56
+ .hc{display:flex;align-items:center;gap:4px;background:rgba(7,9,15,.85);border:1px solid rgba(48,54,61,.8);border-radius:5px;padding:3px 8px;backdrop-filter:blur(6px)}
57
+ [data-theme=light] .hc{background:rgba(246,248,250,.88);border-color:var(--bdr)}
58
+ .hl{font-size:8px;color:var(--sub);letter-spacing:.8px;text-transform:uppercase}
59
+ .hv{font-size:11px;font-weight:700;color:var(--acc);min-width:28px}
60
+ #HINT{position:absolute;bottom:8px;left:50%;transform:translateX(-50%);font-size:9px;color:rgba(139,148,158,.55);pointer-events:none;background:rgba(7,9,15,.55);padding:3px 10px;border-radius:16px;white-space:nowrap}
61
+ #SPL{width:4px;background:var(--bdr);cursor:col-resize;flex-shrink:0;transition:background .2s}
62
+ #SPL:hover,#SPL.drag{background:var(--acc)}
63
+ #CP{width:46%;min-width:200px;display:flex;flex-direction:column;background:var(--cbg);border-left:1px solid var(--bdr)}
64
+ #CTB{display:flex;align-items:center;padding:0 8px;height:32px;background:var(--bg2);border-bottom:1px solid var(--bdr);flex-shrink:0;gap:5px}
65
+ .ct{height:26px;padding:0 10px;font-size:11px;color:var(--sub);border-radius:4px;cursor:pointer;display:flex;align-items:center;gap:4px;border-bottom:2px solid transparent;transition:all .15s}
66
+ .ct.on{color:var(--acc);border-bottom-color:var(--acc)}
67
+ .cd{width:5px;height:5px;border-radius:50%;background:var(--acc)}
68
+ .ct-r{margin-left:auto;display:flex;gap:4px}
69
+ .ct-r .hb{height:20px;font-size:10px;padding:0 6px}
70
+ #CMW{flex:1;overflow:hidden;min-height:0}
71
+ .CodeMirror{height:100%!important;font-size:13px!important;line-height:1.72!important;font-family:inherit!important}
72
+ .CodeMirror-scroll{height:100%!important}
73
+ [data-theme=dark] .CodeMirror{background:#0d1117!important}
74
+ [data-theme=light] .CodeMirror{background:#fff!important}
75
+ .CodeMirror-gutters{border-right:1px solid var(--bdr)!important}
76
+ [data-theme=dark] .CodeMirror-gutters{background:#0d1117!important}
77
+ [data-theme=light] .CodeMirror-gutters{background:#f6f8fa!important}
78
+ #CON{height:148px;display:flex;flex-direction:column;border-top:1px solid var(--bdr);flex-shrink:0}
79
+ #CONH{display:flex;align-items:center;padding:0 10px;height:26px;background:var(--bg2);border-bottom:1px solid var(--bdr);flex-shrink:0;gap:8px}
80
+ #CONH span{font-size:10px;color:var(--sub);letter-spacing:.8px;text-transform:uppercase}
81
+ #CONH button{margin-left:auto;background:none;border:1px solid var(--bdr);color:var(--sub);padding:1px 7px;font-size:10px;border-radius:4px;cursor:pointer;font-family:inherit}
82
+ #CONH button:hover{border-color:var(--acc);color:var(--acc)}
83
+ #OUT{flex:1;overflow-y:auto;padding:5px 10px;font-size:12px;line-height:1.75;background:var(--cso)}
84
+ #OUT::-webkit-scrollbar{width:3px}
85
+ #OUT::-webkit-scrollbar-thumb{background:var(--bdr);border-radius:2px}
86
+ .lo{color:var(--txt)}.lc{color:var(--acc)}.ls{color:var(--grn)}.le{color:var(--red)}.lw{color:var(--yel)}.li{color:#79c0ff}
87
+ #MN{display:none;height:52px;background:var(--hdr);border-top:1px solid var(--bdr);flex-shrink:0}
88
+ .mb{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:none;border:none;color:var(--sub);font-family:inherit;cursor:pointer;transition:color .2s;position:relative}
89
+ .mb.on{color:var(--acc)}
90
+ .mb.on::before{content:'';position:absolute;top:0;left:18%;right:18%;height:2px;background:var(--acc);border-radius:0 0 3px 3px}
91
+ .mi{font-size:19px}.ml{font-size:9px;font-weight:700;letter-spacing:.4px}
92
+ @media(max-width:760px){
93
+ #HTABS,.dsk{display:none!important}
94
+ #H{padding:0 8px;gap:5px}
95
+ #logo-t,.tag{display:none}
96
+ #RUN,#STP{padding:0 10px;height:28px;font-size:11px}
97
+ #MAIN{flex-direction:column;height:calc(100vh - 44px - 52px)}
98
+ #SP,#CP{width:100%!important;flex:1;border-left:none;display:none!important}
99
+ #SP.ma,#CP.ma{display:flex!important}
100
+ #SPL{display:none!important}
101
+ #MN{display:flex!important}
102
+ #STB{padding:3px 6px;gap:4px}
103
+ #CON{height:120px}
104
+ }
105
+ @media(min-width:761px){
106
+ #SP{display:flex!important}
107
+ #CP{display:flex!important}
108
+ #MN{display:none!important}
109
+ }
110
+ #SPL2{position:fixed;inset:0;z-index:9999;background:#07090f;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:14px;animation:splOut .5s 1.6s ease forwards}
111
+ @keyframes splOut{to{opacity:0;visibility:hidden;pointer-events:none}}
112
+ .si{font-size:50px;animation:hov 1.4s ease-in-out infinite}
113
+ @keyframes hov{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
114
+ .st{font-size:21px;font-weight:900;letter-spacing:6px;color:#00e5ff}
115
+ .ss2{font-size:9px;color:#8b949e;letter-spacing:3px;animation:blk 1s infinite}
116
+ @keyframes blk{0%,100%{opacity:1}50%{opacity:.25}}
117
+ #DRP{position:fixed;inset:0;z-index:500;background:rgba(0,229,255,.08);border:3px dashed var(--acc);display:none;align-items:center;justify-content:center;flex-direction:column;gap:10px;color:var(--acc);font-size:18px}
118
+ #DRP.show{display:flex}
119
+ </style>
120
+ </head>
121
+ <body>
122
+
123
+ <div id="SPL2">
124
+ <div class="si">&#x1F681;</div>
125
+ <div class="st">DRONELAB PRO</div>
126
+ <div class="ss2">LOADING...</div>
127
+ </div>
128
+ <div id="DRP"><div>&#x1F4E6;</div><div style="font-size:13px">Drop .glb / .gltf to load drone</div></div>
129
+
130
+ <div id="H">
131
+ <a id="logo" href="#"><div id="logo-i">&#x1F681;</div><div id="logo-t">Drone<b>Lab</b></div></a>
132
+ <div class="tag">PRO</div>
133
+ <div id="HTABS">
134
+ <button class="ht on" data-v="both">&#x2B1B; Split</button>
135
+ <button class="ht" data-v="sim">&#x1F310; Sim</button>
136
+ <button class="ht" data-v="code">&#x1F4DD; Code</button>
137
+ </div>
138
+ <div id="HR">
139
+ <div id="DOT"></div>
140
+ <button class="hb dsk" id="SAVBTN">&#x1F4BE; Save</button>
141
+ <button class="hb dsk" id="LOADBTN">&#x1F4C2; Load</button>
142
+ <button class="hb dsk" id="UPBTN">&#x1F4E6; Model</button>
143
+ <button class="hb" id="THBTN">&#x1F319;</button>
144
+ <button class="hb" id="RUN">&#x25B6; Run</button>
145
+ <button class="hb" id="STP">&#x26D4; Stop</button>
146
+ </div>
147
+ </div>
148
+
149
+ <input type="file" id="FPY" accept=".py" style="display:none">
150
+ <input type="file" id="FGLB" accept=".glb,.gltf" style="display:none">
151
+
152
+ <div id="MAIN">
153
+ <div id="SP" class="ma">
154
+ <div id="STB">
155
+ <label>ENV</label>
156
+ <select class="ss" id="ENVS">
157
+ <option value="night" selected>Night</option>
158
+ <option value="day">Day</option>
159
+ <option value="afternoon">Afternoon</option>
160
+ <option value="evening">Evening</option>
161
+ <option value="indoor">Indoor</option>
162
+ <option value="grass">Grass</option>
163
+ <option value="road">Road</option>
164
+ <option value="mountain">Mountain</option>
165
+ </select>
166
+ <label>DRONE</label>
167
+ <select class="ss" id="DSEL">
168
+ <option value="mavic">DJI Mavic</option>
169
+ <option value="racing">Racing</option>
170
+ <option value="delivery">Delivery</option>
171
+ <option value="custom">Custom GLB</option>
172
+ </select>
173
+ <button class="sb" id="RCAM">Reset View</button>
174
+ </div>
175
+ <div id="CVW">
176
+ <canvas id="C"></canvas>
177
+ <div id="HUD">
178
+ <div class="hr">
179
+ <div class="hc"><span class="hl">X</span><span class="hv" id="HX">0.0</span></div>
180
+ <div class="hc"><span class="hl">Y</span><span class="hv" id="HY">0.0</span></div>
181
+ <div class="hc"><span class="hl">ALT</span><span class="hv" id="HZ">0.0m</span></div>
182
+ </div>
183
+ <div class="hr">
184
+ <div class="hc"><span class="hl">YAW</span><span class="hv" id="HYAW">0</span></div>
185
+ <div class="hc"><span class="hl">SPD</span><span class="hv" id="HSPD">-</span></div>
186
+ <div class="hc"><span class="hl">BAT</span><span class="hv" id="HBAT" style="color:var(--grn)">100%</span></div>
187
+ </div>
188
+ </div>
189
+ <div id="HINT">Drag: orbit | Scroll: zoom | Right-drag: pan | Pinch: zoom</div>
190
+ </div>
191
+ </div>
192
+
193
+ <div id="SPL"></div>
194
+
195
+ <div id="CP">
196
+ <div id="CTB">
197
+ <div class="ct on"><div class="cd"></div>mission.py</div>
198
+ <div class="ct-r">
199
+ <button class="hb" id="MSAV">&#x1F4BE;</button>
200
+ <button class="hb" id="MLOD">&#x1F4C2;</button>
201
+ <button class="hb" id="MMDL">&#x1F4E6;</button>
202
+ </div>
203
+ </div>
204
+ <div id="CMW"><textarea id="CODE"></textarea></div>
205
+ <div id="CON">
206
+ <div id="CONH">
207
+ <span>Console</span>
208
+ <button onclick="document.getElementById('OUT').innerHTML=''">Clear</button>
209
+ </div>
210
+ <div id="OUT"></div>
211
+ </div>
212
+ </div>
213
+ </div>
214
+
215
+ <div id="MN">
216
+ <button class="mb on" id="MB_SIM"><span class="mi">&#x1F310;</span><span class="ml">SIM</span></button>
217
+ <button class="mb" id="MB_CODE"><span class="mi">&#x1F4DD;</span><span class="ml">CODE</span></button>
218
+ <button class="mb" id="MB_RUN"><span class="mi" id="RICO">&#x25B6;</span><span class="ml">RUN</span></button>
219
+ <button class="mb" id="MB_MDL"><span class="mi">&#x1F4E6;</span><span class="ml">MODEL</span></button>
220
+ </div>
221
+
222
+ <script>
223
+ 'use strict';
224
+
225
+ // ─────────────────────────────────────────────────────────────────
226
+ // DEFAULT PYTHON CODE
227
+ // ─────────────────────────────────────────────────────────────────
228
+ var DEFAULT_CODE = [
229
+ "# DroneLab Pro - PySimverse API",
230
+ "# Same as FreeCodeCamp tutorial by Murtaza Hassan",
231
+ "from pysimverse import Drone",
232
+ "import math",
233
+ "",
234
+ "drone = Drone()",
235
+ "drone.connect()",
236
+ "",
237
+ "# Basic Square Flight",
238
+ "drone.takeoff(height=5)",
239
+ "",
240
+ "for i in range(4):",
241
+ " drone.move_forward(distance=3, speed=1.5)",
242
+ " drone.rotate_cw(degrees=90)",
243
+ "",
244
+ "# Sensor Readings",
245
+ "pos = drone.get_position()",
246
+ "print('Position: x=' + str(pos[0]) + ' y=' + str(pos[1]) + ' z=' + str(pos[2]))",
247
+ "print('Battery: ' + str(drone.get_battery()) + '%')",
248
+ "",
249
+ "# Spiral",
250
+ "drone.move_up(distance=2)",
251
+ "drone.spiral(radius=2, height=3, turns=2)",
252
+ "",
253
+ "# Orbit",
254
+ "drone.go_to(x=0, y=0, z=6)",
255
+ "drone.orbit(cx=0, cy=0, radius=3, laps=1)",
256
+ "",
257
+ "# Flip stunt",
258
+ "drone.go_to(x=0, y=0, z=5)",
259
+ "drone.flip('forward')",
260
+ "drone.hover(seconds=1)",
261
+ "",
262
+ "# CV Mission (FreeCodeCamp advanced)",
263
+ "face = drone.detect_face()",
264
+ "print('Face: ' + str(face['detected']))",
265
+ "",
266
+ "gesture = drone.detect_gesture()",
267
+ "print('Gesture: ' + str(gesture))",
268
+ "",
269
+ "drone.follow_line(path=[[0,0],[2,0],[4,2],[6,2]], height=2)",
270
+ "",
271
+ "# Return home",
272
+ "drone.return_home()",
273
+ "drone.land()",
274
+ "print('Mission complete!')"
275
+ ].join('\n');
276
+
277
+ // ─────────────────────────────────────────────────────────────────
278
+ // PYSIMVERSE MODULE - returned as Python source to Skulpt read()
279
+ // This is the KEY FIX: Skulpt calls read("pysimverse.py")
280
+ // ─────────────────────────────────────────────────────────────────
281
+ var PYSIMVERSE_PY = [
282
+ "class Drone:",
283
+ " def __init__(self): pass",
284
+ " def connect(self): __d_connect()",
285
+ " def disconnect(self): __d_disconnect()",
286
+ " def takeoff(self, height=3): __d_takeoff(height)",
287
+ " def land(self): __d_land()",
288
+ " def hover(self, seconds=1): __d_hover(seconds)",
289
+ " def return_home(self): __d_return_home()",
290
+ " def emergency_land(self): __d_emergency_land()",
291
+ " def move_forward(self, distance=1, speed=1): __d_move_forward(distance, speed)",
292
+ " def move_backward(self, distance=1, speed=1): __d_move_backward(distance, speed)",
293
+ " def move_left(self, distance=1, speed=1): __d_move_left(distance, speed)",
294
+ " def move_right(self, distance=1, speed=1): __d_move_right(distance, speed)",
295
+ " def move_up(self, distance=1): __d_move_up(distance)",
296
+ " def move_down(self, distance=1): __d_move_down(distance)",
297
+ " def go_to(self, x=0, y=0, z=0, speed=1): __d_go_to(x, y, z, speed)",
298
+ " def set_position(self, x=0, y=0, z=0, speed=1): __d_go_to(x, y, z, speed)",
299
+ " def rotate_cw(self, degrees=90): __d_rotate_cw(degrees)",
300
+ " def rotate_ccw(self, degrees=90): __d_rotate_ccw(degrees)",
301
+ " def set_yaw(self, angle=0): __d_set_yaw(angle)",
302
+ " def face(self, direction='north'): __d_face(direction)",
303
+ " def set_speed(self, speed=1): __d_set_speed(speed)",
304
+ " def flip(self, direction='forward'): __d_flip(direction)",
305
+ " def flip_forward(self): __d_flip('forward')",
306
+ " def flip_backward(self): __d_flip('backward')",
307
+ " def flip_left(self): __d_flip('left')",
308
+ " def flip_right(self): __d_flip('right')",
309
+ " def orbit(self, cx=0, cy=0, radius=2, laps=1, direction='cw'): __d_orbit(cx, cy, radius, laps, direction)",
310
+ " def circle(self, radius=2, laps=1): __d_circle(radius, laps)",
311
+ " def spiral(self, radius=2, height=4, turns=2): __d_spiral(radius, height, turns)",
312
+ " def figure_8(self, size=3): __d_figure_8(size)",
313
+ " def square(self, size=2): __d_square(size)",
314
+ " def triangle(self, size=2): __d_triangle(size)",
315
+ " def patrol(self, waypoints): __d_patrol(waypoints)",
316
+ " def lawnmower(self, width=8, length=6, rows=4): __d_lawnmower(width, length, rows)",
317
+ " def zigzag(self, width=2, distance=5, steps=6): __d_zigzag(width, distance, steps)",
318
+ " def take_photo(self): __d_take_photo()",
319
+ " def set_camera_angle(self, pitch=0, yaw=0): __d_camera_angle(pitch, yaw)",
320
+ " def detect_face(self): return __d_detect_face()",
321
+ " def detect_gesture(self): return __d_detect_gesture()",
322
+ " def detect_object(self, label='object'): return __d_detect_object(label)",
323
+ " def follow_line(self, path, height=0): __d_follow_line(path, height)",
324
+ " def follow_body(self): __d_follow_body()",
325
+ " def detect_color(self, color='red'): return __d_detect_color(color)",
326
+ " def detect_qr(self): return __d_detect_qr()",
327
+ " def navigate_to_room(self, room='garage'): __d_navigate_to_room(room)",
328
+ " def get_position(self): return __d_get_position()",
329
+ " def get_altitude(self): return __d_get_altitude()",
330
+ " def get_x(self): return __d_get_x()",
331
+ " def get_y(self): return __d_get_y()",
332
+ " def get_yaw(self): return __d_get_yaw()",
333
+ " def get_speed(self): return __d_get_speed()",
334
+ " def get_battery(self): return __d_get_battery()",
335
+ " def is_flying(self): return __d_is_flying()",
336
+ " def distance_to(self, x=0, y=0, z=0): return __d_distance_to(x, y, z)",
337
+ " def print_status(self): __d_print_status()",
338
+ " def log(self, msg): print(str(msg))"
339
+ ].join('\n');
340
+
341
+ // ─────────────────────────────────────────────────────────────────
342
+ // THREE.JS SETUP
343
+ // ─────────────────────────────────────────────────────────────────
344
+ var cvs = document.getElementById('C');
345
+ var cvw = document.getElementById('CVW');
346
+ var rndr = new THREE.WebGLRenderer({canvas: cvs, antialias: true});
347
+ rndr.setPixelRatio(Math.min(window.devicePixelRatio, 2));
348
+ rndr.shadowMap.enabled = true;
349
+ rndr.shadowMap.type = THREE.PCFSoftShadowMap;
350
+ rndr.toneMapping = THREE.ACESFilmicToneMapping;
351
+ rndr.toneMappingExposure = 1.0;
352
+
353
+ var scene = new THREE.Scene();
354
+ var cam = new THREE.PerspectiveCamera(50, 1, 0.05, 600);
355
+ var camR = 20, camTh = 42, camPh = 30;
356
+ var camTgt = new THREE.Vector3(0, 2, 0);
357
+ var panOff = new THREE.Vector3();
358
+
359
+ function updateCam() {
360
+ var ph = camPh * Math.PI / 180;
361
+ var th = camTh * Math.PI / 180;
362
+ cam.position.set(
363
+ camTgt.x + panOff.x + camR * Math.cos(ph) * Math.sin(th),
364
+ camTgt.y + panOff.y + camR * Math.sin(ph),
365
+ camTgt.z + panOff.z + camR * Math.cos(ph) * Math.cos(th)
366
+ );
367
+ cam.lookAt(camTgt.x + panOff.x, camTgt.y + panOff.y, camTgt.z + panOff.z);
368
+ }
369
+
370
+ function resz() {
371
+ var W = cvw.clientWidth || 1;
372
+ var H = cvw.clientHeight || 1;
373
+ rndr.setSize(W, H);
374
+ cam.aspect = W / H;
375
+ cam.updateProjectionMatrix();
376
+ }
377
+ window.addEventListener('resize', resz);
378
+ new ResizeObserver(resz).observe(cvw);
379
+
380
+ // Orbit controls
381
+ var dn = false, lx = 0, ly = 0, rmb = false, pd = 0;
382
+ cvs.addEventListener('mousedown', function(e) { dn = true; lx = e.clientX; ly = e.clientY; rmb = e.button === 2; e.preventDefault(); });
383
+ cvs.addEventListener('contextmenu', function(e) { e.preventDefault(); });
384
+ document.addEventListener('mousemove', function(e) {
385
+ if (!dn) return;
386
+ var dx = e.clientX - lx, dy = e.clientY - ly;
387
+ lx = e.clientX; ly = e.clientY;
388
+ if (rmb) { panOff.x -= dx * .022; panOff.z -= dy * .022; }
389
+ else { camTh -= dx * .38; camPh = Math.max(3, Math.min(88, camPh + dy * .30)); }
390
+ });
391
+ document.addEventListener('mouseup', function() { dn = false; });
392
+ cvs.addEventListener('wheel', function(e) { camR = Math.max(3, Math.min(80, camR + e.deltaY * .045)); }, {passive: true});
393
+ cvs.addEventListener('touchstart', function(e) {
394
+ e.preventDefault();
395
+ if (e.touches.length === 1) { dn = true; lx = e.touches[0].clientX; ly = e.touches[0].clientY; }
396
+ if (e.touches.length === 2) { dn = false; pd = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY); }
397
+ }, {passive: false});
398
+ cvs.addEventListener('touchmove', function(e) {
399
+ e.preventDefault();
400
+ if (e.touches.length === 1 && dn) {
401
+ camTh -= (e.touches[0].clientX - lx) * .42;
402
+ camPh = Math.max(3, Math.min(88, camPh + (e.touches[0].clientY - ly) * .32));
403
+ lx = e.touches[0].clientX; ly = e.touches[0].clientY;
404
+ }
405
+ if (e.touches.length === 2) {
406
+ var d = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
407
+ camR = Math.max(3, Math.min(80, camR - (d - pd) * .05)); pd = d;
408
+ }
409
+ }, {passive: false});
410
+ cvs.addEventListener('touchend', function() { dn = false; });
411
+ document.getElementById('RCAM').onclick = function() { camR = 20; camTh = 42; camPh = 30; panOff.set(0, 0, 0); camTgt.set(0, 2, 0); };
412
+
413
+ // ─────────────────────────────────────────────────────────────────
414
+ // ENVIRONMENT BUILDER
415
+ // ─────────────────────────────────────────────────────────────────
416
+ var envObjs = [], sunL, fillL, ambL;
417
+
418
+ var ENVS = {
419
+ night: {sky: 0x020508, fog: 0x060a12, fd: .022, si: .35, sc: 0x5577aa, sp: [12, 22, 8], ai: .18, ac: 0x1a2244},
420
+ day: {sky: 0x5bb8e8, fog: 0x99d4ef, fd: .010, si: 1.2, sc: 0xfffbe0, sp: [22, 44, 18], ai: .65, ac: 0xfff5dd},
421
+ afternoon:{sky: 0x5a9ec8, fog: 0x88c8e0, fd: .011, si: 1.0, sc: 0xffd888, sp: [10, 28, 22], ai: .55, ac: 0xffeedd},
422
+ evening: {sky: 0x16082e, fog: 0x2e1222, fd: .020, si: .65, sc: 0xff5522, sp: [-4, 9, 22], ai: .30, ac: 0xff8844},
423
+ indoor: {sky: 0x181824, fog: 0x181824, fd: .050, si: .85, sc: 0xfff5cc, sp: [0, 12, 0], ai: .55, ac: 0xfff2cc},
424
+ grass: {sky: 0x5bbee2, fog: 0x88d8ee, fd: .010, si: 1.1, sc: 0xffffff, sp: [16, 38, 12], ai: .60, ac: 0xddfadd},
425
+ road: {sky: 0x78b0cc, fog: 0x99c8de, fd: .011, si: 1.0, sc: 0xffffff, sp: [22, 32, 12], ai: .52, ac: 0xddeeff},
426
+ mountain: {sky: 0x325a74, fog: 0x224466, fd: .016, si: .92, sc: 0xffeebb, sp: [22, 44, 6], ai: .42, ac: 0xddeeff}
427
+ };
428
+
429
+ function addMesh(geo, color, rx, ry, rz, x, y, z) {
430
+ rx = rx || 0; ry = ry || 0; rz = rz || 0; x = x || 0; y = y || 0; z = z || 0;
431
+ var m = new THREE.Mesh(geo, new THREE.MeshLambertMaterial({color: color}));
432
+ m.rotation.set(rx, ry, rz); m.position.set(x, y, z);
433
+ m.castShadow = true; m.receiveShadow = true;
434
+ return m;
435
+ }
436
+
437
+ function makeStars(n, range, ymin, ymax) {
438
+ var sv = [];
439
+ for (var i = 0; i < n; i++) {
440
+ sv.push((Math.random() - .5) * range, Math.random() * (ymax - ymin) + ymin, (Math.random() - .5) * range);
441
+ }
442
+ var sg = new THREE.BufferGeometry();
443
+ sg.setAttribute('position', new THREE.Float32BufferAttribute(sv, 3));
444
+ return new THREE.Points(sg, new THREE.PointsMaterial({color: 0xffffff, size: .22, transparent: true, opacity: .92}));
445
+ }
446
+
447
+ function setEnv(key) {
448
+ envObjs.forEach(function(o) { scene.remove(o); }); envObjs = [];
449
+ var E = ENVS[key] || ENVS.night;
450
+ rndr.setClearColor(E.sky);
451
+ scene.background = new THREE.Color(E.sky);
452
+ scene.fog = new THREE.FogExp2(E.fog, E.fd);
453
+ if (sunL) scene.remove(sunL);
454
+ if (fillL) scene.remove(fillL);
455
+ if (ambL) scene.remove(ambL);
456
+ ambL = new THREE.AmbientLight(E.ac, E.ai);
457
+ sunL = new THREE.DirectionalLight(E.sc, E.si);
458
+ sunL.position.set(E.sp[0], E.sp[1], E.sp[2]); sunL.castShadow = true;
459
+ sunL.shadow.mapSize.set(2048, 2048);
460
+ sunL.shadow.camera.left = sunL.shadow.camera.bottom = -45;
461
+ sunL.shadow.camera.right = sunL.shadow.camera.top = 45;
462
+ sunL.shadow.camera.near = .5; sunL.shadow.camera.far = 130;
463
+ fillL = new THREE.PointLight(0x00e5ff, .4, 40); fillL.position.set(-8, 10, -8);
464
+ scene.add(ambL, sunL, fillL);
465
+
466
+ function add(o) { scene.add(o); envObjs.push(o); }
467
+ function floor(color, s) { s = s || 120; return addMesh(new THREE.PlaneGeometry(s, s), color, -Math.PI/2, 0, 0, 0, 0, 0); }
468
+
469
+ if (key === 'night') {
470
+ add(floor(0x060810));
471
+ add(new THREE.GridHelper(100, 50, 0x0d2030, 0x0a1820));
472
+ add(makeStars(800, 300, 15, 90));
473
+ var moon = new THREE.Mesh(new THREE.SphereGeometry(3.5, 14, 10), new THREE.MeshBasicMaterial({color: 0xeeeedd}));
474
+ moon.position.set(-50, 56, -65); add(moon);
475
+ for (var i = -2; i <= 2; i++) {
476
+ add(addMesh(new THREE.CylinderGeometry(.1, .1, 5.5, 6), 0x334455, 0, 0, 0, i * 10, 2.75, 16));
477
+ var bulb = new THREE.Mesh(new THREE.SphereGeometry(.3, 7, 7), new THREE.MeshBasicMaterial({color: 0xffee88}));
478
+ bulb.position.set(i * 10, 5.65, 16); add(bulb);
479
+ var pl = new THREE.PointLight(0xffee88, .9, 14); pl.position.set(i * 10, 5.3, 16); add(pl);
480
+ }
481
+ } else if (key === 'day' || key === 'afternoon') {
482
+ add(floor(0x4a9038));
483
+ var g = new THREE.GridHelper(100, 50, 0x2a7020, 0x2a7020);
484
+ g.material.opacity = .28; g.material.transparent = true; add(g);
485
+ for (var t = 0; t < 10; t++) {
486
+ var tx = (Math.random() - .5) * 80, tz = (Math.random() - .5) * 80;
487
+ if (Math.abs(tx) < 10 && Math.abs(tz) < 10) continue;
488
+ add(addMesh(new THREE.CylinderGeometry(.3, .5, 3, 7), 0x5a3200, 0, 0, 0, tx, 1.5, tz));
489
+ add(addMesh(new THREE.ConeGeometry(1.8, 3.5, 8), 0x267a26, 0, 0, 0, tx, 4.5, tz));
490
+ }
491
+ for (var c = 0; c < 8; c++) {
492
+ add(addMesh(new THREE.SphereGeometry(2 + Math.random() * 3, 8, 6), 0xffffff, 0, 0, 0, (Math.random() - .5) * 80, 25 + Math.random() * 15, (Math.random() - .5) * 80));
493
+ }
494
+ } else if (key === 'evening') {
495
+ add(floor(0x2a1a10));
496
+ add(makeStars(600, 300, 18, 90));
497
+ var gh = new THREE.Mesh(new THREE.PlaneGeometry(140, 20), new THREE.MeshBasicMaterial({color: 0xff4400, transparent: true, opacity: .18, side: THREE.DoubleSide}));
498
+ gh.rotation.y = Math.PI / 2; gh.position.set(-50, 6, 0); add(gh);
499
+ } else if (key === 'indoor') {
500
+ add(floor(0x8a7a68, 22));
501
+ var rm = new THREE.Mesh(new THREE.BoxGeometry(24, 14, 24), new THREE.MeshLambertMaterial({color: 0xc8b898, side: THREE.BackSide}));
502
+ rm.position.y = 7; add(rm);
503
+ var cf = new THREE.Mesh(new THREE.BoxGeometry(4, .08, 4), new THREE.MeshBasicMaterial({color: 0xfff6ee}));
504
+ cf.position.set(0, 13.94, 0); add(cf);
505
+ var cl = new THREE.PointLight(0xfff4cc, 1.6, 28); cl.position.set(0, 12, 0); add(cl);
506
+ add(addMesh(new THREE.BoxGeometry(3, 1.4, 5), 0x4a3218, 0, 0, 0, 8.5, .7, 0));
507
+ add(new THREE.GridHelper(20, 20, 0x9a8a78, 0x9a8a78));
508
+ } else if (key === 'grass') {
509
+ add(floor(0x3a8a22));
510
+ for (var f = 0; f < 30; f++) {
511
+ var fx = (Math.random() - .5) * 80, fz = (Math.random() - .5) * 80;
512
+ var col = Math.random() > .5 ? 0xffdd44 : 0xff6699;
513
+ add(addMesh(new THREE.SphereGeometry(.35, 6, 5), col, 0, 0, 0, fx, .35, fz));
514
+ }
515
+ add(makeStars(200, 200, 60, 120));
516
+ } else if (key === 'road') {
517
+ add(floor(0x557766));
518
+ add(addMesh(new THREE.PlaneGeometry(9, 100), 0x3a3a3a, -Math.PI/2, 0, 0, 0, .01, 0));
519
+ for (var d = -9; d <= 9; d++) {
520
+ add(addMesh(new THREE.PlaneGeometry(.28, 3.2), 0xffff44, -Math.PI/2, 0, 0, 0, .02, d * 6));
521
+ }
522
+ var carColors = [0xcc3333, 0x3366cc, 0xcccc33, 0x33cc66, 0xcc6633];
523
+ var carZ = [-22, -8, 6, 20, 35];
524
+ for (var ci = 0; ci < carZ.length; ci++) {
525
+ var cs = ci % 2 === 0 ? 3 : -3;
526
+ add(addMesh(new THREE.BoxGeometry(2.2, 1.1, 3.8), carColors[ci], 0, 0, 0, cs, .6, carZ[ci]));
527
+ add(addMesh(new THREE.BoxGeometry(1.7, .85, 2.2), carColors[ci], 0, 0, 0, cs, 1.6, carZ[ci]));
528
+ }
529
+ add(new THREE.GridHelper(120, 60, 0x446655, 0x446655));
530
+ } else if (key === 'mountain') {
531
+ add(floor(0x4a6240, 200));
532
+ add(new THREE.GridHelper(120, 60, 0x3a5230, 0x3a5230));
533
+ var peaks = [[40, -42, 9, 26], [36, 48, 7, 20], [58, 22, 6, 18], [-52, -28, 11, 30], [-38, 42, 8, 22]];
534
+ for (var pi = 0; pi < peaks.length; pi++) {
535
+ var pk = peaks[pi];
536
+ add(addMesh(new THREE.ConeGeometry(pk[2], pk[3], 9), 0x5a6458, 0, 0, 0, pk[0], pk[3]/2, pk[1]));
537
+ add(addMesh(new THREE.ConeGeometry(pk[2]*.28, pk[3]*.3, 9), 0xeeeeff, 0, 0, 0, pk[0], pk[3]*.86, pk[1]));
538
+ }
539
+ for (var mt = 0; mt < 16; mt++) {
540
+ var mx = (Math.random() - .5) * 48, mz = (Math.random() - .5) * 48;
541
+ add(addMesh(new THREE.ConeGeometry(1.2 + Math.random() * .8, 4 + Math.random() * 3, 7), 0x2a6a2a, 0, 0, 0, mx, 2.5, mz));
542
+ }
543
+ add(makeStars(400, 300, 30, 100));
544
+ }
545
+
546
+ // Landing pad (always)
547
+ var lp = new THREE.Mesh(new THREE.CircleGeometry(1.7, 32), new THREE.MeshBasicMaterial({color: 0x00e5ff, transparent: true, opacity: .16}));
548
+ lp.rotation.x = -Math.PI/2; lp.position.y = .01; add(lp);
549
+ var lr = new THREE.Mesh(new THREE.RingGeometry(1.6, 1.82, 32), new THREE.MeshBasicMaterial({color: 0x00e5ff, transparent: true, opacity: .55, side: THREE.DoubleSide}));
550
+ lr.rotation.x = -Math.PI/2; lr.position.y = .015; add(lr);
551
+ // XYZ axes
552
+ var axPts = [
553
+ [new THREE.Vector3(0,.01,0), new THREE.Vector3(3.5,.01,0), 0xf85149],
554
+ [new THREE.Vector3(0,.01,0), new THREE.Vector3(0,3.5,0), 0x3fb950],
555
+ [new THREE.Vector3(0,.01,0), new THREE.Vector3(0,.01,3.5), 0x00e5ff]
556
+ ];
557
+ for (var ai = 0; ai < axPts.length; ai++) {
558
+ var axLine = new THREE.Line(new THREE.BufferGeometry().setFromPoints([axPts[ai][0], axPts[ai][1]]), new THREE.LineBasicMaterial({color: axPts[ai][2]}));
559
+ add(axLine);
560
+ }
561
+ }
562
+
563
+ // ─────────────────────────────────────────────────────────────────
564
+ // DRONE MODELS
565
+ // ─────────────────────────────────────────────────────────────────
566
+ var droneRoot = null, propGs = [], droneGlow = null, customMdl = null;
567
+
568
+ function mkm(geo, mat, rx, ry, rz, x, y, z) {
569
+ rx = rx||0; ry = ry||0; rz = rz||0; x = x||0; y = y||0; z = z||0;
570
+ var m = new THREE.Mesh(geo, mat); m.rotation.set(rx,ry,rz); m.position.set(x,y,z);
571
+ m.castShadow = true; return m;
572
+ }
573
+ function mp(color, shin, spec, transp, opa, side) {
574
+ return new THREE.MeshPhongMaterial({
575
+ color: color, shininess: shin||100, specular: spec||0,
576
+ transparent: transp||false, opacity: opa!==undefined?opa:1, side: side||THREE.FrontSide
577
+ });
578
+ }
579
+
580
+ function buildMavic() {
581
+ var G = new THREE.Group();
582
+ var mBody = mp(0x1e2a38, 160, 0x5588bb);
583
+ var mDark = mp(0x0e1520, 80);
584
+ var mArm = mp(0x16202e, 60);
585
+ var mMot = mp(0x080e18, 220, 0x336699);
586
+ var mProp = mp(0x2a3848, 30, 0, true, .82, THREE.DoubleSide);
587
+ var mGlass= mp(0x001828, 280, 0x88ccff, true, .65);
588
+ var mLED = new THREE.MeshBasicMaterial({color: 0x00e5ff});
589
+ var mRed = new THREE.MeshBasicMaterial({color: 0xff2222});
590
+ var mGrn = new THREE.MeshBasicMaterial({color: 0x22ff55});
591
+
592
+ G.add(mkm(new THREE.BoxGeometry(2.3,.3,1.1), mBody));
593
+ G.add(mkm(new THREE.BoxGeometry(1.1,.2,.68), mBody, 0,0,0, -.1,.25,0));
594
+ G.add(mkm(new THREE.BoxGeometry(.25,.2,.88), mDark, 0,0,0, .97,-.04,0));
595
+ G.add(mkm(new THREE.BoxGeometry(.52,.04,.05), mLED, 0,0,0, .72,.18,0));
596
+
597
+ var mPosArr = [[1.85,0,-1.85],[1.85,0,1.85],[-1.85,0,-1.85],[-1.85,0,1.85]];
598
+ propGs = [];
599
+ for (var i = 0; i < mPosArr.length; i++) {
600
+ var mp2 = mPosArr[i];
601
+ var mx = mp2[0], mz = mp2[2];
602
+ var al = Math.sqrt(mx*mx + mz*mz) * .88;
603
+ var aYaw = Math.atan2(mx, mz);
604
+ G.add(mkm(new THREE.BoxGeometry(al,.09,.09), mArm, 0,aYaw,0, mx*.5,0,mz*.5));
605
+ G.add(mkm(new THREE.CylinderGeometry(.24,.24,.14,14), mMot, 0,0,0, mx,.07,mz));
606
+ G.add(mkm(new THREE.CylinderGeometry(.15,.2,.1,12), mMot, 0,0,0, mx,.17,mz));
607
+ G.add(mkm(new THREE.CylinderGeometry(.04,.04,.12,8), mMot, 0,0,0, mx,.26,mz));
608
+ var pg = new THREE.Group(); pg.position.set(mx,.3,mz);
609
+ for (var b = 0; b < 2; b++) {
610
+ var blade = mkm(new THREE.BoxGeometry(1.72,.02,.14), mProp, 0, b*Math.PI/2, 0.065);
611
+ pg.add(blade);
612
+ }
613
+ var cap = new THREE.Mesh(new THREE.CylinderGeometry(.06,.09,.06,8), mMot); pg.add(cap);
614
+ G.add(pg); propGs.push(pg);
615
+ var tipMat = i < 2 ? mGrn : mRed;
616
+ G.add(mkm(new THREE.SphereGeometry(.07,6,6), tipMat, 0,0,0, mx,.31,mz));
617
+ }
618
+
619
+ // Landing gear
620
+ var lzArr = [-0.58, 0.58];
621
+ for (var li = 0; li < lzArr.length; li++) {
622
+ var lz = lzArr[li];
623
+ G.add(mkm(new THREE.BoxGeometry(1.85,.05,.07), mArm, 0,0,0, 0,-.28,lz));
624
+ var lxArr = [-0.68, 0.68];
625
+ for (var lxi = 0; lxi < lxArr.length; lxi++) {
626
+ G.add(mkm(new THREE.CylinderGeometry(.055,.055,.3,6), mArm, 0,0,0, lxArr[lxi],-.14,lz));
627
+ }
628
+ }
629
+
630
+ // Camera gimbal
631
+ G.add(mkm(new THREE.SphereGeometry(.17,10,8), mDark, 0,0,0, .9,-.23,0));
632
+ G.add(mkm(new THREE.SphereGeometry(.11,10,8), mGlass, 0,0,0, 1.0,-.23,0));
633
+
634
+ droneGlow = new THREE.PointLight(0x00e5ff, 0, 6);
635
+ droneGlow.position.set(0,-1.2,0); G.add(droneGlow);
636
+ G.traverse(function(c) { if (c.isMesh) c.castShadow = true; });
637
+ return G;
638
+ }
639
+
640
+ function buildRacing() {
641
+ var G = new THREE.Group();
642
+ var mF = mp(0x1a0806,120,0xff3300);
643
+ var mA = mp(0x0a0808);
644
+ var mP = mp(0xff2222,30,0,true,.82,THREE.DoubleSide);
645
+ G.add(mkm(new THREE.CylinderGeometry(.35,.35,.1,5), mF));
646
+ propGs = [];
647
+ var rp = [[1,0,-1],[1,0,1],[-1,0,-1],[-1,0,1]];
648
+ for (var ri = 0; ri < rp.length; ri++) {
649
+ var rmx = rp[ri][0], rmz = rp[ri][2];
650
+ G.add(mkm(new THREE.BoxGeometry(Math.SQRT2*.9,.07,.07), mA, 0,Math.atan2(rmx,rmz),0, rmx*.5,0,rmz*.5));
651
+ var pg = new THREE.Group(); pg.position.set(rmx,.12,rmz);
652
+ for (var b = 0; b < 2; b++) pg.add(mkm(new THREE.BoxGeometry(1.3,.02,.11), mP, 0,b*Math.PI/2,0));
653
+ G.add(pg); propGs.push(pg);
654
+ }
655
+ droneGlow = new THREE.PointLight(0xff3300,0,4); droneGlow.position.set(0,-.5,0); G.add(droneGlow);
656
+ G.traverse(function(c) { if (c.isMesh) c.castShadow = true; });
657
+ return G;
658
+ }
659
+
660
+ function buildDelivery() {
661
+ var G = new THREE.Group();
662
+ var mB = mp(0xf0eee0,110,0x4488aa);
663
+ var mA = mp(0xc8c2b8);
664
+ var mP = mp(0x8899aa,30,0,true,.8,THREE.DoubleSide);
665
+ var mBox = mp(0x2266cc,80,0x88aacc);
666
+ G.add(mkm(new THREE.CylinderGeometry(.62,.62,.22,7), mB));
667
+ G.add(mkm(new THREE.BoxGeometry(1.1,.7,1.1), mBox, 0,0,0, 0,-.46,0));
668
+ propGs = [];
669
+ var hexAngles = [0,60,120,180,240,300];
670
+ for (var hi = 0; hi < hexAngles.length; hi++) {
671
+ var ha = hexAngles[hi] * Math.PI / 180;
672
+ var hmx = 2.1*Math.cos(ha), hmz = 2.1*Math.sin(ha);
673
+ G.add(mkm(new THREE.BoxGeometry(2.1,.07,.07), mA, 0,Math.atan2(hmx,hmz),0, hmx*.5,0,hmz*.5));
674
+ var pg = new THREE.Group(); pg.position.set(hmx,.14,hmz);
675
+ for (var b = 0; b < 2; b++) pg.add(mkm(new THREE.BoxGeometry(1.4,.02,.12), mP, 0,b*Math.PI/2,0));
676
+ G.add(pg); propGs.push(pg);
677
+ }
678
+ droneGlow = new THREE.PointLight(0xffffff,0,7); droneGlow.position.set(0,-1,0); G.add(droneGlow);
679
+ G.traverse(function(c) { if (c.isMesh) c.castShadow = true; });
680
+ return G;
681
+ }
682
+
683
+ function setDrone(type) {
684
+ if (droneRoot) scene.remove(droneRoot);
685
+ propGs = []; droneGlow = null;
686
+ if (type === 'custom' && customMdl) { droneRoot = customMdl; scene.add(droneRoot); return; }
687
+ if (type === 'racing') droneRoot = buildRacing();
688
+ else if (type === 'delivery') droneRoot = buildDelivery();
689
+ else droneRoot = buildMavic();
690
+ scene.add(droneRoot);
691
+ }
692
+
693
+ // ─────────────────────────────────────────────────────────────────
694
+ // TRAIL
695
+ // ─────────────────────────────────────────────────────────────────
696
+ var trailPts = [];
697
+ var tGeo = new THREE.BufferGeometry();
698
+ var tLine = new THREE.Line(tGeo, new THREE.LineBasicMaterial({color: 0x00e5ff, transparent: true, opacity: .5}));
699
+ scene.add(tLine);
700
+
701
+ function addTrail(x, y, z) {
702
+ trailPts.push(new THREE.Vector3(x, y, z));
703
+ if (trailPts.length > 600) trailPts.shift();
704
+ if (trailPts.length >= 2) tGeo.setFromPoints(trailPts);
705
+ }
706
+
707
+ // ─────────────────────────────────────────────────────────────────
708
+ // AUDIO
709
+ // ─────────────────────────────────────────────────────────────────
710
+ var aC = null, pO = null, pG = null, aOK = false;
711
+ function initAudio() {
712
+ if (aOK) return;
713
+ try {
714
+ aC = new (window.AudioContext || window.webkitAudioContext)();
715
+ pO = aC.createOscillator();
716
+ var f = aC.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 190;
717
+ pG = aC.createGain(); pG.gain.value = 0;
718
+ pO.type = 'sawtooth'; pO.frequency.value = 80;
719
+ pO.connect(f); f.connect(pG); pG.connect(aC.destination);
720
+ pO.start(); aOK = true;
721
+ } catch(e) {}
722
+ }
723
+ function setHum(v, f) { if (!aOK) return; pG.gain.setTargetAtTime(v, aC.currentTime, .28); pO.frequency.setTargetAtTime(f, aC.currentTime, .45); }
724
+ function beep(f, d, v) { f=f||880; d=d||.09; v=v||.1; if (!aOK) return; try { var o=aC.createOscillator(),g=aC.createGain(); o.frequency.value=f; g.gain.setValueAtTime(v,aC.currentTime); g.gain.exponentialRampToValueAtTime(.0001,aC.currentTime+d); o.connect(g); g.connect(aC.destination); o.start(); o.stop(aC.currentTime+d); } catch(e) {} }
725
+ function tsound() { if (!aOK) return; pO.frequency.setValueAtTime(62,aC.currentTime); pO.frequency.linearRampToValueAtTime(135,aC.currentTime+1.1); pG.gain.setValueAtTime(0,aC.currentTime); pG.gain.linearRampToValueAtTime(.08,aC.currentTime+1.1); }
726
+ function lsound() { if (!aOK) return; pO.frequency.linearRampToValueAtTime(62,aC.currentTime+1); pG.gain.linearRampToValueAtTime(0,aC.currentTime+1.6); }
727
+ ['click','touchstart'].forEach(function(ev) { document.addEventListener(ev, initAudio, {once: true}); });
728
+
729
+ // ────────────────���────────────────────────────────────────────────
730
+ // MISSION STATE
731
+ // ─────────────────────────────────────────────────────────────────
732
+ var P = {x:0, y:0, z:0, yaw:0, spd:1, flying:false, battery:100};
733
+ var C = {x:0, y:0, z:0, yaw:0, flying:false, battery:100};
734
+ var cmdQ = [], curC = null, cFrom = null, cT0 = 0;
735
+ var running = false, shouldStop = false, mDone = true, bDrain = 0, fAngle = 0, frameN = 0;
736
+
737
+ function ease(t) { return t < .5 ? 2*t*t : -1+(4-2*t)*t; }
738
+ function qm(tx,ty,tz,tyaw,ms,log,fast) { fast=fast||false; cmdQ.push({t:'move',tx:tx,ty:ty,tz:tz,tyaw:tyaw,dur:Math.max(fast?90:300,ms||500),log:log,fast:fast}); P.x=tx; P.y=ty; P.z=tz; P.yaw=tyaw; }
739
+ function qh(ms,log) { cmdQ.push({t:'hover',dur:ms,log:log}); }
740
+ function qfl(dir) { cmdQ.push({t:'flip',dir:dir,dur:700,log:"flip('"+dir+"')"}); }
741
+ function qfn(fn) { cmdQ.push({t:'fn',fn:fn}); }
742
+
743
+ // ─────────────────────────────────────────────────────────────────
744
+ // SKULPT BUILTINS SETUP
745
+ // Called BEFORE runPython β€” registers all __d_* JS functions
746
+ // ─────────────────────────────────────────────────────────────────
747
+ function setupBuiltins() {
748
+ var b = Sk.builtins;
749
+
750
+ function sk(fn) { return new Sk.builtin.func(fn); }
751
+ function N(v, d) { d = (d !== undefined) ? d : 0; return (v != null && v !== Sk.builtin.none.none$) ? Sk.ffi.remapToJs(v) : d; }
752
+ function S(v, d) { d = (d !== undefined) ? d : ''; return (v != null && v !== Sk.builtin.none.none$) ? Sk.ffi.remapToJs(v) : d; }
753
+ var NONE = Sk.builtin.none.none$;
754
+
755
+ b.__d_connect = sk(function() { qfn(function() { clog('drone.connect()', 'ls'); beep(440,.1); }); return NONE; });
756
+ b.__d_disconnect = sk(function() { qfn(function() { clog('drone.disconnect()', 'li'); }); return NONE; });
757
+
758
+ b.__d_takeoff = sk(function(h) {
759
+ var ht = N(h, 3);
760
+ if (P.flying) { clog('Already flying!', 'lw'); return NONE; }
761
+ P.flying = true;
762
+ qm(P.x, P.y, ht, P.yaw, Math.max(900, ht*400/P.spd), 'takeoff(height='+ht+')');
763
+ qfn(function() { tsound(); });
764
+ return NONE;
765
+ });
766
+
767
+ b.__d_land = sk(function() {
768
+ P.flying = false;
769
+ qm(P.x, P.y, 0, P.yaw, Math.max(900, P.z*400/P.spd), 'land()');
770
+ qfn(function() { lsound(); });
771
+ return NONE;
772
+ });
773
+
774
+ b.__d_hover = sk(function(s) { var sec = N(s, 1); qh(sec*1000, 'hover('+sec+'s)'); return NONE; });
775
+
776
+ b.__d_return_home = sk(function() {
777
+ var d = Math.hypot(P.x, P.y);
778
+ qm(0, 0, P.z, P.yaw, Math.max(600, d*380/P.spd), 'return_home()');
779
+ return NONE;
780
+ });
781
+
782
+ b.__d_emergency_land = sk(function() { P.flying=false; qm(P.x,P.y,0,P.yaw,400,'emergency_land()'); return NONE; });
783
+
784
+ function mkMover(name, xs, zs) {
785
+ return sk(function(d, sp) {
786
+ var dist = N(d, 1), spd = N(sp, -1);
787
+ if (spd > 0) P.spd = Math.max(.1, Math.min(5, spd));
788
+ var r = P.yaw * Math.PI / 180;
789
+ qm(P.x + xs(r)*dist, P.y + zs(r)*dist, P.z, P.yaw, Math.max(300, dist*380/P.spd), name+'('+dist+')');
790
+ return NONE;
791
+ });
792
+ }
793
+ b.__d_move_forward = mkMover('move_forward', function(r){return Math.sin(r);}, function(r){return Math.cos(r);});
794
+ b.__d_move_backward = mkMover('move_backward', function(r){return -Math.sin(r);}, function(r){return -Math.cos(r);});
795
+ b.__d_move_left = mkMover('move_left', function(r){return -Math.cos(r);}, function(r){return Math.sin(r);});
796
+ b.__d_move_right = mkMover('move_right', function(r){return Math.cos(r);}, function(r){return -Math.sin(r);});
797
+
798
+ b.__d_move_up = sk(function(d) { var dist=N(d,1); qm(P.x,P.y,P.z+dist,P.yaw,Math.max(300,dist*360/P.spd),'move_up('+dist+')'); return NONE; });
799
+ b.__d_move_down = sk(function(d) { var dist=N(d,1); qm(P.x,P.y,Math.max(0,P.z-dist),P.yaw,Math.max(300,dist*360/P.spd),'move_down('+dist+')'); return NONE; });
800
+
801
+ b.__d_go_to = sk(function(x, y, z, sp) {
802
+ var tx=N(x,P.x), ty=N(y,P.y), tz=N(z,P.z), spd=N(sp,-1);
803
+ if (spd > 0) P.spd = Math.max(.1, Math.min(5, spd));
804
+ var d = Math.hypot(tx-P.x, ty-P.y, tz-P.z);
805
+ qm(tx, ty, tz, P.yaw, Math.max(400, d*380/P.spd), 'go_to('+tx.toFixed(1)+','+ty.toFixed(1)+','+tz.toFixed(1)+')');
806
+ return NONE;
807
+ });
808
+
809
+ b.__d_rotate_cw = sk(function(deg) { var d=N(deg,90); qm(P.x,P.y,P.z,P.yaw+d,Math.max(250,d*10/P.spd),'rotate_cw('+d+')'); return NONE; });
810
+ b.__d_rotate_ccw = sk(function(deg) { var d=N(deg,90); qm(P.x,P.y,P.z,P.yaw-d,Math.max(250,d*10/P.spd),'rotate_ccw('+d+')'); return NONE; });
811
+ b.__d_set_yaw = sk(function(a) { var ang=N(a,0); qm(P.x,P.y,P.z,ang,350,'set_yaw('+ang+')'); return NONE; });
812
+ b.__d_face = sk(function(d) { var dirs={north:0,east:90,south:180,west:270,n:0,e:90,s:180,w:270}; var a=dirs[S(d,'north').toLowerCase()]||0; qm(P.x,P.y,P.z,a,350,"face('"+S(d)+"')"); return NONE; });
813
+ b.__d_set_speed = sk(function(s) { P.spd=Math.max(.1,Math.min(5,N(s,1))); qfn(function(){clog('set_speed('+P.spd.toFixed(1)+')','li');beep(660,.06);}); return NONE; });
814
+ b.__d_flip = sk(function(d) { qfl(S(d,'forward')); return NONE; });
815
+
816
+ // Patterns
817
+ b.__d_orbit = sk(function(cx, cy, r, laps, dir) {
818
+ var ocx=N(cx,0), ocy=N(cy,0), rad=N(r,2), l=N(laps,1), dd=S(dir,'cw')==='ccw'?-1:1;
819
+ clog('orbit(r='+rad+',laps='+l+')', 'li');
820
+ var steps = Math.ceil(44*l);
821
+ for (var i=1; i<=steps; i++) { var a=dd*(i/44)*Math.PI*2; qm(ocx+rad*Math.cos(a),ocy+rad*Math.sin(a),P.z,P.yaw,90,null,true); }
822
+ return NONE;
823
+ });
824
+
825
+ b.__d_circle = sk(function(r, laps) {
826
+ var rad=N(r,2), l=N(laps,1), sx=P.x, sy=P.y;
827
+ clog('circle(r='+rad+')', 'li');
828
+ var steps = Math.ceil(44*l);
829
+ for (var i=1; i<=steps; i++) { var a=(i/44)*Math.PI*2; qm(sx+rad*Math.sin(a),sy+rad-rad*Math.cos(a),P.z,P.yaw,90,null,true); }
830
+ return NONE;
831
+ });
832
+
833
+ b.__d_spiral = sk(function(r, h, t) {
834
+ var rad=N(r,2), ht=N(h,4), turns=N(t,2), sx=P.x, sy=P.y, sz=P.z;
835
+ clog('spiral(r='+rad+',h='+ht+')', 'li');
836
+ var steps = Math.ceil(44*turns);
837
+ for (var i=1; i<=steps; i++) { var a=(i/44)*Math.PI*2; qm(sx+rad*Math.cos(a),sy+rad*Math.sin(a),sz+ht*(i/steps),P.yaw,90,null,true); }
838
+ return NONE;
839
+ });
840
+
841
+ b.__d_figure_8 = sk(function(size) {
842
+ var s=N(size,3), cx=P.x, cy=P.y;
843
+ clog('figure_8(size='+s+')', 'li');
844
+ for (var i=1;i<=36;i++){var a=(i/36)*Math.PI*2;qm(cx+s*Math.sin(a),cy+s*Math.sin(a)*Math.cos(a),P.z,P.yaw,90,null,true);}
845
+ for (var i=1;i<=36;i++){var a=(i/36)*Math.PI*2;qm(cx-s*Math.sin(a),cy+s*Math.sin(a)*Math.cos(a),P.z,P.yaw,90,null,true);}
846
+ return NONE;
847
+ });
848
+
849
+ b.__d_square = sk(function(size) {
850
+ var s=N(size,2); clog('square('+s+')', 'li');
851
+ for (var i=0;i<4;i++){var r2=P.yaw*Math.PI/180;qm(P.x+Math.sin(r2)*s,P.y+Math.cos(r2)*s,P.z,P.yaw,Math.max(350,s*380),'side '+(i+1));qm(P.x,P.y,P.z,P.yaw+90,280,null);}
852
+ return NONE;
853
+ });
854
+
855
+ b.__d_triangle = sk(function(size) {
856
+ var s=N(size,2); clog('triangle('+s+')', 'li');
857
+ for (var i=0;i<3;i++){var r2=P.yaw*Math.PI/180;qm(P.x+Math.sin(r2)*s,P.y+Math.cos(r2)*s,P.z,P.yaw,Math.max(350,s*380),'side '+(i+1));qm(P.x,P.y,P.z,P.yaw+120,320,null);}
858
+ return NONE;
859
+ });
860
+
861
+ b.__d_patrol = sk(function(wps) {
862
+ var list = Sk.ffi.remapToJs(wps);
863
+ clog('patrol('+list.length+' pts)', 'li');
864
+ for (var pi=0; pi<list.length; pi++) {
865
+ var wp = list[pi], a = Array.isArray(wp) ? wp : Object.values(wp);
866
+ var tx=+a[0]||0, ty=+a[1]||0, tz=a[2]!=null?+a[2]:P.z;
867
+ qm(tx,ty,tz,P.yaw,Math.max(400,Math.hypot(tx-P.x,ty-P.y)*380),'-> ('+tx+','+ty+','+tz+')');
868
+ }
869
+ return NONE;
870
+ });
871
+
872
+ b.__d_lawnmower = sk(function(w, l, rows) {
873
+ var ww=N(w,8),ll=N(l,6),rr=N(rows,4); clog('lawnmower', 'li');
874
+ var sp=ww/rr;
875
+ for (var i=0;i<=rr;i++){var x=-ww/2+i*sp;qm(x,P.y,P.z,P.yaw,300,null);qm(x,P.y+(i%2===0?ll:-ll),P.z,P.yaw,Math.max(400,ll*320),null);}
876
+ return NONE;
877
+ });
878
+
879
+ b.__d_zigzag = sk(function(w, d, n) {
880
+ var ww=N(w,2),dd=N(d,5),nn=N(n,6); clog('zigzag', 'li');
881
+ var step=dd/nn;
882
+ for (var i=0;i<nn;i++){var r2=P.yaw*Math.PI/180;var side=i%2===0?ww:-ww;qm(P.x+Math.sin(r2)*step+Math.cos(r2)*side,P.y+Math.cos(r2)*step-Math.sin(r2)*side,P.z,P.yaw,Math.max(200,step*300),null);}
883
+ return NONE;
884
+ });
885
+
886
+ b.__d_take_photo = sk(function() { qfn(function(){clog('take_photo()','li');beep(1200,.08);}); return NONE; });
887
+ b.__d_camera_angle = sk(function(p,y) { qfn(function(){clog('camera_angle('+N(p)+','+N(y)+')','li');}); return NONE; });
888
+
889
+ // CV Missions (simulated)
890
+ b.__d_detect_face = sk(function() {
891
+ var x=+(Math.random()*.4-.2).toFixed(3), y=+(Math.random()*.4-.2).toFixed(3);
892
+ clog('detect_face() -> face at ('+x+','+y+')', 'li');
893
+ return Sk.ffi.remapToPy({detected:true, x:x, y:y, confidence:0.92, label:'human_face'});
894
+ });
895
+
896
+ b.__d_detect_gesture = sk(function() {
897
+ var gs = ['pointing','peace','thumbs_up','open_hand','fist','okay','pinky'];
898
+ var g = gs[Math.floor(Math.random()*gs.length)];
899
+ clog('detect_gesture() -> "'+g+'"', 'li');
900
+ return Sk.ffi.remapToPy(g);
901
+ });
902
+
903
+ b.__d_detect_object = sk(function(label) {
904
+ var lbl = S(label, 'object');
905
+ clog('detect_object("'+lbl+'")', 'li');
906
+ return Sk.ffi.remapToPy({detected:Math.random()>.3, label:lbl, x:Math.random()-.5, y:Math.random()-.5, confidence:+(Math.random()*.3+.7).toFixed(2)});
907
+ });
908
+
909
+ b.__d_follow_line = sk(function(path, height) {
910
+ var pts = Sk.ffi.remapToJs(path), ht = N(height, P.z);
911
+ clog('follow_line('+pts.length+' pts)', 'li');
912
+ for (var pi=0; pi<pts.length; pi++) {
913
+ var pt = pts[pi], a = Array.isArray(pt) ? pt : Object.values(pt);
914
+ qm(+a[0]||0, +a[1]||0, ht, P.yaw, 300, '->('+a[0]+','+a[1]+')', true);
915
+ }
916
+ return NONE;
917
+ });
918
+
919
+ b.__d_follow_body = sk(function() { clog('follow_body() -> tracking','li'); qm(P.x+2,P.y,P.z,P.yaw,1200,'tracking...'); return NONE; });
920
+ b.__d_detect_color = sk(function(c) { var cl=S(c,'red'); clog('detect_color("'+cl+'")','li'); return Sk.ffi.remapToPy({detected:Math.random()>.3,color:cl,x:Math.random()-.5,y:Math.random()-.5}); });
921
+ b.__d_detect_qr = sk(function() { clog('detect_qr() -> DRONELAB-1','li'); return Sk.ffi.remapToPy({detected:true,data:'DRONELAB-1',x:.01,y:-.04}); });
922
+ b.__d_navigate_to_room = sk(function(room) { var r=S(room,'garage'); clog('navigate_to_room("'+r+'")','li'); qm(P.x+3,P.y+3,P.z,P.yaw,1800,'-> '+r); return NONE; });
923
+
924
+ // Sensors
925
+ b.__d_get_position = sk(function() { return Sk.ffi.remapToPy([+P.x.toFixed(3), +P.y.toFixed(3), +P.z.toFixed(3)]); });
926
+ b.__d_get_altitude = sk(function() { return Sk.ffi.remapToPy(+P.z.toFixed(3)); });
927
+ b.__d_get_x = sk(function() { return Sk.ffi.remapToPy(+P.x.toFixed(3)); });
928
+ b.__d_get_y = sk(function() { return Sk.ffi.remapToPy(+P.y.toFixed(3)); });
929
+ b.__d_get_yaw = sk(function() { return Sk.ffi.remapToPy(+P.yaw.toFixed(1)); });
930
+ b.__d_get_speed = sk(function() { return Sk.ffi.remapToPy(P.spd); });
931
+ b.__d_get_battery = sk(function() { return Sk.ffi.remapToPy(Math.round(P.battery)); });
932
+ b.__d_is_flying = sk(function() { return P.flying ? Sk.builtin.bool.true$ : Sk.builtin.bool.false$; });
933
+ b.__d_distance_to = sk(function(x,y,z) { return Sk.ffi.remapToPy(Math.hypot(N(x)-P.x,N(y)-P.y,N(z)-P.z)); });
934
+ b.__d_print_status = sk(function() { clog('x='+P.x.toFixed(1)+' y='+P.y.toFixed(1)+' z='+P.z.toFixed(1)+' yaw='+P.yaw.toFixed(0)+' bat='+Math.round(P.battery)+'%','li'); return NONE; });
935
+ }
936
+
937
+ // ─────────────────────────────────────────────────────────────────
938
+ // THE CRITICAL FIX: builtinRead returns pysimverse.py source
939
+ // Skulpt calls read("pysimverse.py") when Python does:
940
+ // from pysimverse import Drone
941
+ // ─────────────────────────────────────────────────────────────────
942
+ function builtinRead(x) {
943
+ // Handle ALL possible ways Skulpt may request pysimverse
944
+ if (x === 'pysimverse.py' || x === 'pysimverse' ||
945
+ x.indexOf('pysimverse') !== -1) {
946
+ return PYSIMVERSE_PY;
947
+ }
948
+ if (x.endsWith('/time.js') || x === 'time.py') {
949
+ return 'def sleep(s):\n pass\n';
950
+ }
951
+ if (Sk.builtinFiles && Sk.builtinFiles['files'][x] !== undefined) {
952
+ return Sk.builtinFiles['files'][x];
953
+ }
954
+ throw new Error("File not found: '" + x + "'");
955
+ }
956
+
957
+ // ─────────────────────────────────────────────────────────────────
958
+ // MAIN RUN FUNCTION
959
+ // ─────────────────────────────────────────────────────────────────
960
+ function runPython(code) {
961
+ // 1. Reset mission state
962
+ P = {x:0, y:0, z:0, yaw:0, spd:1, flying:false, battery:100};
963
+ cmdQ = []; curC = null;
964
+
965
+ // 2. Configure Skulpt with the fixed read function
966
+ Sk.configure({
967
+ output: function(txt) {
968
+ txt.split('\n').forEach(function(l) { if (l.trim()) clog(l, 'lo'); });
969
+ },
970
+ read: builtinRead,
971
+ __future__: Sk.python3,
972
+ execLimit: 150000
973
+ });
974
+
975
+ // 3. Register all __d_* builtins BEFORE execution
976
+ setupBuiltins();
977
+
978
+ // 4. Execute Python code
979
+ Sk.misceval.asyncToPromise(function() {
980
+ return Sk.importMainWithBody('<stdin>', false, code, true);
981
+ }).then(function() {
982
+ clog('', 'lo');
983
+ clog(cmdQ.length + ' commands queued β€” simulating...', 'ls');
984
+ beginSim();
985
+ }).catch(function(err) {
986
+ var msg = err.tp$str ? err.tp$str().v : String(err);
987
+ clog('', 'le');
988
+ clog('ERROR: ' + msg, 'le');
989
+ document.getElementById('DOT').className = 'err';
990
+ setRunning(false);
991
+ });
992
+ }
993
+
994
+ // ─────────────────────────────────────────────────────────────────
995
+ // SIMULATION ENGINE
996
+ // ─────────────────────────────────────────────────────────────────
997
+ function beginSim() {
998
+ trailPts = [];
999
+ if (tGeo.attributes && tGeo.attributes.position) tGeo.deleteAttribute('position');
1000
+ C = {x:0, y:0, z:0, yaw:0, flying:false, battery:100};
1001
+ bDrain = 0; mDone = false; fAngle = 0; panOff.set(0,0,0);
1002
+ }
1003
+
1004
+ function simTick(ts) {
1005
+ if (mDone || shouldStop) return;
1006
+
1007
+ // Flush fn commands
1008
+ while (cmdQ.length && cmdQ[0].t === 'fn') cmdQ.shift().fn();
1009
+
1010
+ if (!cmdQ.length && !curC) {
1011
+ if (!mDone) {
1012
+ mDone = true;
1013
+ clog('Mission complete!', 'ls');
1014
+ beep(660,.2); setTimeout(function(){beep(880,.2);},240); setTimeout(function(){beep(1100,.3);},480);
1015
+ setRunning(false);
1016
+ }
1017
+ return;
1018
+ }
1019
+
1020
+ if (!curC && cmdQ.length) {
1021
+ curC = cmdQ.shift(); cFrom = {x:C.x,y:C.y,z:C.z,yaw:C.yaw}; cT0 = ts;
1022
+ if (curC.log) clog(curC.log, 'lc');
1023
+ if (curC.t === 'move') {
1024
+ if (curC.tz > .1 && cFrom.z < .1) tsound();
1025
+ else if (curC.tz < .1 && cFrom.z > .1) lsound();
1026
+ else if (!curC.fast) beep(520,.05,.06);
1027
+ }
1028
+ }
1029
+ if (!curC) return;
1030
+
1031
+ var t = Math.min((ts - cT0) / Math.max(1, curC.dur), 1);
1032
+ var e = curC.fast ? t : ease(t);
1033
+
1034
+ if (curC.t === 'move') {
1035
+ C.x = cFrom.x + (curC.tx - cFrom.x) * e;
1036
+ C.y = cFrom.y + (curC.ty - cFrom.y) * e;
1037
+ C.z = cFrom.z + (curC.tz - cFrom.z) * e;
1038
+ C.yaw = cFrom.yaw + (curC.tyaw - cFrom.yaw) * e;
1039
+ } else if (curC.t === 'flip') {
1040
+ fAngle = e * 360;
1041
+ }
1042
+ C.flying = C.z > .06;
1043
+
1044
+ if (t >= 1) {
1045
+ if (curC.t === 'move') { C.x=curC.tx; C.y=curC.ty; C.z=curC.tz; C.yaw=curC.tyaw; }
1046
+ if (curC.t === 'move') { bDrain+=.28; C.battery=Math.max(0,100-bDrain); P.battery=C.battery; }
1047
+ fAngle = 0; curC = null;
1048
+ }
1049
+
1050
+ if (C.z > .06) addTrail(C.x, C.z, C.y); // Three.js: Y = up
1051
+ }
1052
+
1053
+ // ─────────────────────────────────────────────────────────────────
1054
+ // RENDER LOOP
1055
+ // ─────────────────────────────────────────────────────────────────
1056
+ var propSpin = 0;
1057
+
1058
+ function loop(ts) {
1059
+ frameN++;
1060
+ simTick(ts);
1061
+
1062
+ if (droneRoot) {
1063
+ droneRoot.position.set(C.x, C.z, C.y);
1064
+ droneRoot.rotation.y = -C.yaw * Math.PI / 180;
1065
+ droneRoot.rotation.x = fAngle * Math.PI / 180;
1066
+ droneRoot.rotation.z = 0;
1067
+ }
1068
+
1069
+ var tSpd = C.flying ? (0.30 + P.spd * .05) : 0;
1070
+ propSpin += (tSpd - propSpin) * .08;
1071
+ for (var pi = 0; pi < propGs.length; pi++) {
1072
+ propGs[pi].rotation.y += propSpin * (pi % 2 === 0 ? 1 : -1);
1073
+ }
1074
+
1075
+ if (droneGlow) droneGlow.intensity = C.flying ? .55 : 0;
1076
+
1077
+ if (frameN % 30 === 0 && aOK) {
1078
+ if (C.flying) setHum(.07, 82 + C.z*3 + propSpin*155);
1079
+ else if (!running) setHum(0, 82);
1080
+ }
1081
+
1082
+ if (droneRoot) camTgt.lerp(droneRoot.position, .028);
1083
+ updateCam();
1084
+
1085
+ if (frameN % 8 === 0) {
1086
+ document.getElementById('HX').textContent = C.x.toFixed(1);
1087
+ document.getElementById('HY').textContent = C.y.toFixed(1);
1088
+ document.getElementById('HZ').textContent = C.z.toFixed(1) + 'm';
1089
+ document.getElementById('HYAW').textContent = ((C.yaw%360+360)%360).toFixed(0) + 'deg';
1090
+ document.getElementById('HSPD').textContent = running ? P.spd.toFixed(1) : '-';
1091
+ var bat = Math.round(C.battery);
1092
+ var bel = document.getElementById('HBAT');
1093
+ bel.textContent = bat + '%';
1094
+ bel.style.color = bat > 50 ? 'var(--grn)' : bat > 20 ? 'var(--yel)' : 'var(--red)';
1095
+ }
1096
+
1097
+ rndr.render(scene, cam);
1098
+ requestAnimationFrame(loop);
1099
+ }
1100
+
1101
+ // ─────────────────────────────────────────────────────────────────
1102
+ // CONSOLE
1103
+ // ─────────────────────────────────────────────────────────────────
1104
+ var out = document.getElementById('OUT');
1105
+ function clog(msg, cls) {
1106
+ cls = cls || 'lo';
1107
+ var d = document.createElement('div'); d.className = cls; d.textContent = msg;
1108
+ out.appendChild(d); out.scrollTop = 9999;
1109
+ }
1110
+
1111
+ // ─────────────────────────────────────────────────────────────────
1112
+ // UI ACTIONS
1113
+ // ─────────────────────────────────────────────────────────────────
1114
+ function setRunning(r) {
1115
+ running = r;
1116
+ document.getElementById('RUN').style.display = r ? 'none' : '';
1117
+ document.getElementById('STP').style.display = r ? '' : 'none';
1118
+ document.getElementById('DOT').className = r ? 'live' : '';
1119
+ document.getElementById('RICO').textContent = r ? 'stop' : 'play';
1120
+ }
1121
+
1122
+ function doRun() {
1123
+ if (running) return;
1124
+ initAudio();
1125
+ shouldStop = false; mDone = true; bDrain = 0;
1126
+ trailPts = [];
1127
+ if (tGeo.attributes && tGeo.attributes.position) tGeo.deleteAttribute('position');
1128
+ if (droneRoot) { droneRoot.position.set(0,0,0); droneRoot.rotation.set(0,0,0); }
1129
+ C = {x:0, y:0, z:0, yaw:0, flying:false, battery:100};
1130
+ out.innerHTML = '';
1131
+ clog('Running mission.py...', 'li');
1132
+ setRunning(true);
1133
+ mobSwitch('sim');
1134
+ setTimeout(function() { runPython(editor.getValue()); }, 50);
1135
+ }
1136
+
1137
+ function doStop() {
1138
+ shouldStop = true; curC = null; cmdQ = [];
1139
+ clog('Stopped.', 'lw'); setRunning(false);
1140
+ if (aOK) setHum(0, 82);
1141
+ }
1142
+
1143
+ document.getElementById('RUN').onclick = doRun;
1144
+ document.getElementById('STP').onclick = doStop;
1145
+ document.getElementById('MB_RUN').onclick = doRun;
1146
+
1147
+ // Desktop view tabs
1148
+ document.querySelectorAll('[data-v]').forEach(function(btn) {
1149
+ btn.addEventListener('click', function() {
1150
+ document.querySelectorAll('[data-v]').forEach(function(b) { b.classList.remove('on'); });
1151
+ btn.classList.add('on');
1152
+ var v = btn.dataset.v;
1153
+ var sp = document.getElementById('SP'), cp = document.getElementById('CP'), spl = document.getElementById('SPL');
1154
+ if (v === 'sim') { sp.style.cssText='flex:1;display:flex'; cp.style.display='none'; spl.style.display='none'; }
1155
+ else if (v === 'code') { sp.style.display='none'; cp.style.cssText='width:100%;display:flex'; spl.style.display='none'; }
1156
+ else { sp.style.cssText='flex:1;display:flex'; cp.style.cssText='width:46%;display:flex'; spl.style.display='block'; }
1157
+ setTimeout(function() { resz(); editor.refresh(); }, 20);
1158
+ });
1159
+ });
1160
+
1161
+ // Mobile switch
1162
+ function mobSwitch(pane) {
1163
+ var sp = document.getElementById('SP'), cp = document.getElementById('CP');
1164
+ sp.classList.remove('ma'); cp.classList.remove('ma');
1165
+ document.getElementById('MB_SIM').classList.remove('on');
1166
+ document.getElementById('MB_CODE').classList.remove('on');
1167
+ if (pane === 'sim') { sp.classList.add('ma'); document.getElementById('MB_SIM').classList.add('on'); setTimeout(resz, 20); }
1168
+ else { cp.classList.add('ma'); document.getElementById('MB_CODE').classList.add('on'); setTimeout(function(){editor.refresh();}, 20); }
1169
+ }
1170
+ document.getElementById('MB_SIM').onclick = function() { mobSwitch('sim'); };
1171
+ document.getElementById('MB_CODE').onclick = function() { mobSwitch('code'); };
1172
+ document.getElementById('MB_MDL').onclick = function() { document.getElementById('FGLB').click(); };
1173
+
1174
+ // Save / Load Python
1175
+ function saveCode() {
1176
+ var b = new Blob([editor.getValue()], {type:'text/plain'});
1177
+ var a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = 'mission.py'; a.click();
1178
+ clog('Saved mission.py', 'ls');
1179
+ }
1180
+ function loadCode() { document.getElementById('FPY').click(); }
1181
+ document.getElementById('SAVBTN').onclick = saveCode;
1182
+ document.getElementById('MSAV').onclick = saveCode;
1183
+ document.getElementById('LOADBTN').onclick = loadCode;
1184
+ document.getElementById('MLOD').onclick = loadCode;
1185
+ document.getElementById('FPY').addEventListener('change', function() {
1186
+ if (!this.files[0]) return;
1187
+ var r = new FileReader();
1188
+ r.onload = function(e) { editor.setValue(e.target.result); clog('Loaded: ' + this.files[0].name, 'ls'); }.bind(this);
1189
+ r.readAsText(this.files[0]); this.value = '';
1190
+ });
1191
+
1192
+ // Upload 3D Model
1193
+ function loadGLB(file) {
1194
+ if (!file) return;
1195
+ clog('Loading ' + file.name + '...', 'li');
1196
+ var loader = new THREE.GLTFLoader();
1197
+ loader.load(URL.createObjectURL(file), function(gltf) {
1198
+ customMdl = gltf.scene;
1199
+ var box = new THREE.Box3().setFromObject(customMdl);
1200
+ var sz = box.getSize(new THREE.Vector3());
1201
+ customMdl.scale.setScalar(4 / Math.max(sz.x, sz.y, sz.z, 0.01));
1202
+ customMdl.traverse(function(c) { if (c.isMesh) c.castShadow = true; });
1203
+ document.getElementById('DSEL').value = 'custom';
1204
+ setDrone('custom');
1205
+ clog('Model loaded: ' + file.name, 'ls'); beep(880,.1);
1206
+ }, null, function(err) { clog('Model error: ' + err.message, 'le'); });
1207
+ }
1208
+ document.getElementById('UPBTN').onclick = function() { document.getElementById('FGLB').click(); };
1209
+ document.getElementById('MMDL').onclick = function() { document.getElementById('FGLB').click(); };
1210
+ document.getElementById('FGLB').addEventListener('change', function() { loadGLB(this.files[0]); this.value = ''; });
1211
+ document.getElementById('DSEL').onchange = function() {
1212
+ if (this.value === 'custom') { document.getElementById('FGLB').click(); return; }
1213
+ setDrone(this.value);
1214
+ };
1215
+
1216
+ // Drag-drop model
1217
+ document.body.addEventListener('dragover', function(e) { e.preventDefault(); document.getElementById('DRP').classList.add('show'); });
1218
+ document.body.addEventListener('dragleave', function(e) { if (!e.relatedTarget) document.getElementById('DRP').classList.remove('show'); });
1219
+ document.body.addEventListener('drop', function(e) {
1220
+ e.preventDefault(); document.getElementById('DRP').classList.remove('show');
1221
+ var f = e.dataTransfer.files[0];
1222
+ if (f && (f.name.endsWith('.glb') || f.name.endsWith('.gltf'))) loadGLB(f);
1223
+ else clog('Drop a .glb or .gltf file', 'lw');
1224
+ });
1225
+
1226
+ // Environment
1227
+ document.getElementById('ENVS').onchange = function() { setEnv(this.value); };
1228
+
1229
+ // Theme
1230
+ var dark = true;
1231
+ document.getElementById('THBTN').onclick = function() {
1232
+ dark = !dark;
1233
+ document.documentElement.dataset.theme = dark ? 'dark' : 'light';
1234
+ document.getElementById('THBTN').textContent = dark ? 'Dark' : 'Light';
1235
+ editor.setOption('theme', dark ? 'dracula' : 'eclipse');
1236
+ };
1237
+
1238
+ // Splitter
1239
+ var splEl = document.getElementById('SPL'); var splDrag = false;
1240
+ splEl.addEventListener('mousedown', function(e) { splDrag = true; splEl.classList.add('drag'); e.preventDefault(); });
1241
+ document.addEventListener('mousemove', function(e) {
1242
+ if (!splDrag) return;
1243
+ var rect = document.getElementById('MAIN').getBoundingClientRect();
1244
+ var pct = Math.min(78, Math.max(22, (e.clientX - rect.left) / rect.width * 100));
1245
+ document.getElementById('SP').style.cssText = 'flex:0 0 ' + pct + '%;display:flex';
1246
+ resz();
1247
+ });
1248
+ document.addEventListener('mouseup', function() { splDrag = false; splEl.classList.remove('drag'); });
1249
+
1250
+ // Keyboard shortcuts
1251
+ document.addEventListener('keydown', function(e) {
1252
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); doRun(); }
1253
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); saveCode(); }
1254
+ });
1255
+
1256
+ // ─────────────────────────────────────────────────────────────────
1257
+ // CODEMIRROR EDITOR
1258
+ // ─────────────────────────────────────────────────────────────────
1259
+ var editor = CodeMirror.fromTextArea(document.getElementById('CODE'), {
1260
+ mode: 'python', theme: 'dracula', lineNumbers: true,
1261
+ autoCloseBrackets: true, matchBrackets: true,
1262
+ indentUnit: 4, tabSize: 4, lineWrapping: false,
1263
+ extraKeys: {
1264
+ 'Tab': function(cm) { cm.execCommand('indentMore'); },
1265
+ 'Shift-Tab': function(cm) { cm.execCommand('indentLess'); },
1266
+ 'Ctrl-Enter': doRun, 'Cmd-Enter': doRun
1267
+ }
1268
+ });
1269
+ editor.setValue(DEFAULT_CODE);
1270
+ editor.setSize('100%', '100%');
1271
+
1272
+ // ─────────────────────────────────────────────────────────────────
1273
+ // INITIALIZE
1274
+ // ─────────────────────────────────────────────────────────────────
1275
+ setEnv('night');
1276
+ setDrone('mavic');
1277
+ resz();
1278
+ updateCam();
1279
+
1280
+ // Mobile: start on code tab
1281
+ if (window.innerWidth <= 760) mobSwitch('code');
1282
+
1283
+ clog('DroneLab Pro ready!', 'ls');
1284
+ clog('Type your code and press Run', 'li');
1285
+ clog('Ctrl+Enter: Run | Ctrl+S: Save', 'li');
1286
+
1287
+ requestAnimationFrame(loop);
1288
+ </script>
1289
+ </body>
1290
+ </html>