HelloSun commited on
Commit
ebc1b3a
·
verified ·
1 Parent(s): 63dfab5

Initial WFreeCAD static space

Browse files
README.md CHANGED
@@ -1,10 +1,57 @@
1
  ---
2
  title: WFreeCAD
3
- emoji: 🐠
4
- colorFrom: indigo
5
- colorTo: pink
6
  sdk: static
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: WFreeCAD
3
+ emoji: ⚙️
4
+ colorFrom: yellow
5
+ colorTo: red
6
  sdk: static
7
  pinned: false
8
+ short_description: A tiny in-browser FreeCAD Part studio
9
  ---
10
 
11
+ # WFreeCAD
12
+
13
+ A tiny, static, in-browser FreeCAD Python playground. Pick a Part macro, orbit the result, edit the code, and run it again.
14
+
15
+ - Four instant examples: a spur gear, a desktop computer, a rocket, and a mug.
16
+ - Upload any `.py` macro that uses the supported `FreeCAD` / `Part` subset.
17
+ - Live execution happens locally in a Pyodide Web Worker; nothing is sent to a server.
18
+ - Results are rendered with Three.js and `three-bvh-csg`, with soft shadows and an optional edge overlay.
19
+
20
+ ## How it works
21
+
22
+ Example scenes are precomputed with `cadshim.py` under CPython and stored as `scenes/*.json`, so the examples render immediately without downloading Python. Uploaded macros are executed by Pyodide in `worker.js`, serialized into the same scene JSON format, and displayed by `viewer.js`.
23
+
24
+ The shim supports a practical subset of FreeCAD's Part API:
25
+
26
+ - `FreeCAD.Vector`, `Rotation`, `Placement`, and `ActiveDocument`
27
+ - `Part.makeBox`, `makeCylinder`, `makeSphere`, `makeCone`, `makeTorus`, `makePolygon`
28
+ - `Part.Face`, `Shape.extrude`, `Shape.cut`, `Shape.fuse`, `Shape.common`
29
+ - `Part.show`, `ViewObject.ShapeColor`, and `ViewObject.Transparency`
30
+
31
+ It is intentionally not a full FreeCAD distribution. Unsupported API calls return a clear error in the preview panel.
32
+
33
+ ## Local development
34
+
35
+ ```bash
36
+ cd wfreecad
37
+ python3 -m http.server 8123
38
+ ```
39
+
40
+ Open <http://127.0.0.1:8123/>.
41
+
42
+ Examples are generated by:
43
+
44
+ ```bash
45
+ python3 tools/build_scenes.py
46
+ ```
47
+
48
+ Thumbnails are rendered from `tools/thumb.html` through the same Three.js viewer, so their materials and lighting match the main preview.
49
+
50
+ ## Stack
51
+
52
+ - [Three.js](https://threejs.org/) + [three-bvh-csg](https://github.com/gkjohnson/three-bvh-csg)
53
+ - [Pyodide](https://pyodide.org/)
54
+ - [CodeMirror 5](https://codemirror.net/5/)
55
+ - Custom FreeCAD Part shim and static scene format
56
+
57
+ No backend, database, or build step is required at runtime.
app.js ADDED
@@ -0,0 +1,765 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createViewer } from './viewer.js';
2
+
3
+ const DEFAULT_VIEW = [0.86, -1.35, 0.98];
4
+ const MAX_FILE_BYTES = 512 * 1024;
5
+
6
+ const byId = (id) => document.getElementById(id);
7
+ const els = {
8
+ runtime: byId('runtime'),
9
+ runtimeText: byId('runtimeText'),
10
+ loadBtn: byId('loadBtn'),
11
+ fileInput: byId('fileInput'),
12
+ exampleList: byId('exampleList'),
13
+ exampleCount: byId('exampleCount'),
14
+ fileName: byId('fileName'),
15
+ fileState: byId('fileState'),
16
+ revertBtn: byId('revertBtn'),
17
+ runBtn: byId('runBtn'),
18
+ editorWrap: byId('editorWrap'),
19
+ cursorInfo: byId('cursorInfo'),
20
+ objectCount: byId('objectCount'),
21
+ autoBtn: byId('autoBtn'),
22
+ resetBtn: byId('resetBtn'),
23
+ canvasWrap: byId('canvasWrap'),
24
+ view: byId('view'),
25
+ canvasHint: byId('canvasHint'),
26
+ loadingState: byId('loadingState'),
27
+ loadingText: byId('loadingText'),
28
+ emptyState: byId('emptyState'),
29
+ emptyHint: byId('emptyHint'),
30
+ errorState: byId('errorState'),
31
+ errorText: byId('errorText'),
32
+ dismissError: byId('dismissError'),
33
+ toast: byId('toast'),
34
+ dropOverlay: byId('dropOverlay'),
35
+ };
36
+
37
+ const state = {
38
+ examples: [],
39
+ current: null,
40
+ mode: 'example',
41
+ source: '',
42
+ fileName: '未命名.py',
43
+ dirty: false,
44
+ scene: null,
45
+ sceneSource: '',
46
+ busy: false,
47
+ pendingRun: null,
48
+ uiSeq: 0,
49
+ cm: null,
50
+ viewer: null,
51
+ toastTimer: null,
52
+ hintTimer: null,
53
+ };
54
+
55
+ const RUNTIME_LABELS = {
56
+ idle: 'Python 待命',
57
+ loading: 'Python 載入中',
58
+ ready: 'Python 就緒',
59
+ busy: 'Python 執行中',
60
+ error: 'Python 離線',
61
+ };
62
+
63
+ class PythonRuntime {
64
+ constructor() {
65
+ this.worker = null;
66
+ this.ready = false;
67
+ this.failed = false;
68
+ this.shim = null;
69
+ this.initPromise = null;
70
+ this.seq = 0;
71
+ this.pending = new Map();
72
+ this.onState = () => {};
73
+ this.lastError = '';
74
+ this._readyResolve = null;
75
+ this._readyReject = null;
76
+ this._initTimer = null;
77
+ }
78
+
79
+ setState(name, detail = '') {
80
+ this.state = name;
81
+ this.onState(name, detail);
82
+ }
83
+
84
+ async loadShim() {
85
+ if (this.shim) return this.shim;
86
+ const response = await fetch('cadshim.py', { cache: 'no-cache' });
87
+ if (!response.ok) {
88
+ throw new Error(`無法載入 Python shim(HTTP ${response.status})`);
89
+ }
90
+ const text = await response.text();
91
+ if (!text.includes('__wfcad_runtime')) {
92
+ throw new Error('Python shim 內容不正確');
93
+ }
94
+ this.shim = text;
95
+ return text;
96
+ }
97
+
98
+ preload() {
99
+ this.ensure().catch(() => {});
100
+ }
101
+
102
+ async ensure() {
103
+ if (this.ready && this.worker) return;
104
+ if (this.initPromise) return this.initPromise;
105
+ this.initPromise = this.boot();
106
+ try {
107
+ await this.initPromise;
108
+ } catch (error) {
109
+ if (this.initPromise === null) throw error;
110
+ this.initPromise = null;
111
+ throw error;
112
+ }
113
+ }
114
+
115
+ async boot() {
116
+ this.setState('loading');
117
+ let shim;
118
+ try {
119
+ shim = await this.loadShim();
120
+ } catch (error) {
121
+ this.fail(error);
122
+ throw error;
123
+ }
124
+
125
+ let worker;
126
+ try {
127
+ worker = new Worker('worker.js');
128
+ } catch (error) {
129
+ this.fail(error);
130
+ throw error;
131
+ }
132
+ this.worker = worker;
133
+ worker.onmessage = (event) => this.handleMessage(event.data);
134
+ worker.onerror = (event) => {
135
+ this.fail(event.message || 'Python worker 發生錯誤');
136
+ };
137
+
138
+ const ready = new Promise((resolve, reject) => {
139
+ this._readyResolve = resolve;
140
+ this._readyReject = reject;
141
+ });
142
+
143
+ this._initTimer = setTimeout(() => {
144
+ this.fail('Python 執行環境載入逾時,請檢查網路後重試。');
145
+ }, 90000);
146
+
147
+ worker.postMessage({ type: 'init', shim });
148
+ await ready;
149
+ }
150
+
151
+ handleMessage(message) {
152
+ if (!message) return;
153
+ if (message.type === 'ready') {
154
+ if (this._initTimer) {
155
+ clearTimeout(this._initTimer);
156
+ this._initTimer = null;
157
+ }
158
+ this.ready = true;
159
+ this.failed = false;
160
+ this.setState('ready');
161
+ const resolve = this._readyResolve;
162
+ this._readyResolve = null;
163
+ this._readyReject = null;
164
+ if (resolve) resolve();
165
+ return;
166
+ }
167
+ if (message.type === 'fatal') {
168
+ this.fail(message.error || 'Python 執行環境發生錯誤');
169
+ return;
170
+ }
171
+ if (message.type === 'result' || message.type === 'error') {
172
+ const pending = this.pending.get(message.id);
173
+ if (!pending) return;
174
+ this.pending.delete(message.id);
175
+ if (message.type === 'result') {
176
+ pending.resolve(message.result);
177
+ } else {
178
+ pending.reject(new Error(message.error || 'Python 執行失敗'));
179
+ }
180
+ }
181
+ }
182
+
183
+ fail(error) {
184
+ const reason = error instanceof Error ? error.message : String(error || 'Python 執行環境發生錯誤');
185
+ this.lastError = reason;
186
+ this.ready = false;
187
+ this.failed = true;
188
+ if (this._initTimer) {
189
+ clearTimeout(this._initTimer);
190
+ this._initTimer = null;
191
+ }
192
+ if (this.worker) {
193
+ this.worker.terminate();
194
+ this.worker = null;
195
+ }
196
+ this.initPromise = null;
197
+ const rejectReady = this._readyReject;
198
+ this._readyResolve = null;
199
+ this._readyReject = null;
200
+ if (rejectReady) rejectReady(new Error(reason));
201
+ for (const pending of [...this.pending.values()]) {
202
+ pending.reject(new Error(reason));
203
+ }
204
+ this.pending.clear();
205
+ this.setState('error', reason);
206
+ }
207
+
208
+ async run(code) {
209
+ await this.ensure();
210
+ if (!this.worker) throw new Error('Python 執行環境不在線');
211
+ this.setState('busy');
212
+ const id = ++this.seq;
213
+ return new Promise((resolve, reject) => {
214
+ const timer = setTimeout(() => {
215
+ this.pending.delete(id);
216
+ this.restart();
217
+ reject(new Error('執行逾時。程式可能進入了無窮迴圈,已停止執行。'));
218
+ }, 20000);
219
+ this.pending.set(id, {
220
+ resolve: (value) => {
221
+ clearTimeout(timer);
222
+ this.finishRun();
223
+ resolve(value);
224
+ },
225
+ reject: (error) => {
226
+ clearTimeout(timer);
227
+ this.finishRun();
228
+ reject(error);
229
+ },
230
+ });
231
+ this.worker.postMessage({ type: 'run', id, code });
232
+ });
233
+ }
234
+
235
+ finishRun() {
236
+ if (this.ready) {
237
+ this.setState('ready');
238
+ } else if (this.failed) {
239
+ this.setState('error', this.lastError);
240
+ }
241
+ }
242
+
243
+ restart() {
244
+ if (this.worker) {
245
+ this.worker.terminate();
246
+ this.worker = null;
247
+ }
248
+ this.ready = false;
249
+ this.failed = false;
250
+ this.initPromise = null;
251
+ this.pending.clear();
252
+ this.setState('idle');
253
+ }
254
+ }
255
+
256
+ const runtime = new PythonRuntime();
257
+ runtime.onState = (name, detail) => {
258
+ els.runtime.dataset.state = name;
259
+ els.runtimeText.textContent = RUNTIME_LABELS[name] || 'Python';
260
+ if (name === 'error') {
261
+ els.runtime.title = detail || 'Python 執行環境無法載入';
262
+ } else {
263
+ els.runtime.title = 'Python 執行環境';
264
+ }
265
+ };
266
+
267
+ // --------------------------------------------------------------------------
268
+ // small helpers
269
+ // --------------------------------------------------------------------------
270
+
271
+ function escapeHtml(value) {
272
+ return String(value).replace(/[&<>"']/g, (char) => {
273
+ const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
274
+ return map[char];
275
+ });
276
+ }
277
+
278
+ function cleanError(value) {
279
+ let text = value instanceof Error ? value.stack || value.message : String(value || '未知錯誤');
280
+ text = text.replace(/\u001b\[[0-9;]*m/g, '').trim();
281
+ if (text.length > 7000) text = `${text.slice(0, 7000)}\n…`;
282
+ return text || '未知錯誤';
283
+ }
284
+
285
+ function toast(message, tone = 'info') {
286
+ els.toast.textContent = message;
287
+ els.toast.dataset.tone = tone;
288
+ els.toast.hidden = false;
289
+ if (state.toastTimer) clearTimeout(state.toastTimer);
290
+ state.toastTimer = setTimeout(() => {
291
+ els.toast.hidden = true;
292
+ state.toastTimer = null;
293
+ }, tone === 'error' ? 4200 : 2800);
294
+ }
295
+
296
+ function showLoading(text) {
297
+ if (text) els.loadingText.textContent = text;
298
+ els.loadingState.hidden = false;
299
+ els.emptyState.hidden = true;
300
+ els.errorState.hidden = true;
301
+ }
302
+
303
+ function showError(message) {
304
+ els.errorText.textContent = cleanError(message);
305
+ els.errorState.hidden = false;
306
+ els.loadingState.hidden = true;
307
+ els.emptyState.hidden = true;
308
+ els.canvasHint.classList.add('is-hidden');
309
+ els.fileState.textContent = '錯誤';
310
+ els.fileState.dataset.state = 'error';
311
+ }
312
+
313
+ function hideError() {
314
+ if (!els.errorState.hidden) {
315
+ els.errorState.hidden = true;
316
+ updateFileUI();
317
+ }
318
+ }
319
+
320
+ function updateCursor() {
321
+ if (!state.cm) return;
322
+ const cursor = state.cm.getCursor();
323
+ els.cursorInfo.textContent = `Ln ${cursor.line + 1}, Col ${cursor.ch + 1}`;
324
+ }
325
+
326
+ function updateRunButton() {
327
+ els.runBtn.disabled = state.busy;
328
+ els.runBtn.setAttribute('aria-busy', String(state.busy));
329
+ els.runBtn.classList.toggle('is-busy', state.busy);
330
+ }
331
+
332
+ function updateFileUI() {
333
+ els.fileName.textContent = state.fileName || '未命名.py';
334
+ document.title = `${state.fileName || 'WFreeCAD'} · WFreeCAD`;
335
+ els.revertBtn.disabled = !(state.mode === 'example' && state.current);
336
+ els.revertBtn.title = els.revertBtn.disabled ? '只有範例可以還原' : '回到範例程式';
337
+
338
+ let label = '範例';
339
+ let kind = 'example';
340
+ if (state.mode === 'upload') {
341
+ label = state.dirty ? '已修改' : '已載入';
342
+ kind = state.dirty ? 'modified' : 'upload';
343
+ } else if (state.dirty) {
344
+ label = '已修改';
345
+ kind = 'modified';
346
+ }
347
+ els.fileState.textContent = label;
348
+ els.fileState.dataset.state = kind;
349
+ }
350
+
351
+ function updateActive(id) {
352
+ const cards = els.exampleList.querySelectorAll('.example-card');
353
+ for (const card of cards) {
354
+ const active = card.dataset.id === id;
355
+ card.classList.toggle('is-active', active);
356
+ if (active) {
357
+ card.setAttribute('aria-current', 'true');
358
+ } else {
359
+ card.removeAttribute('aria-current');
360
+ }
361
+ }
362
+ }
363
+
364
+ // --------------------------------------------------------------------------
365
+ // editor
366
+ // --------------------------------------------------------------------------
367
+
368
+ function initEditor() {
369
+ state.cm = CodeMirror.fromTextArea(byId('editor'), {
370
+ mode: 'python',
371
+ lineNumbers: true,
372
+ indentUnit: 4,
373
+ tabSize: 4,
374
+ indentWithTabs: false,
375
+ lineWrapping: true,
376
+ matchBrackets: true,
377
+ autoCloseBrackets: true,
378
+ styleActiveLine: true,
379
+ viewportMargin: 40,
380
+ extraKeys: {
381
+ Tab: (editor) => {
382
+ if (editor.somethingSelected()) {
383
+ editor.indentSelection('add');
384
+ } else {
385
+ editor.replaceSelection(' ', 'end');
386
+ }
387
+ },
388
+ },
389
+ });
390
+ state.cm.setSize('100%', '100%');
391
+ state.cm.on('change', () => {
392
+ const value = state.cm.getValue();
393
+ state.dirty = value !== state.source;
394
+ updateFileUI();
395
+ updateCursor();
396
+ if (!els.errorState.hidden) hideError();
397
+ });
398
+ state.cm.on('cursorActivity', updateCursor);
399
+ state.cm.on('focus', () => runtime.preload());
400
+ requestAnimationFrame(() => state.cm.refresh());
401
+ updateCursor();
402
+ }
403
+
404
+ function setEditor(text, { mode, name } = {}) {
405
+ state.source = text;
406
+ if (mode) state.mode = mode;
407
+ if (name) state.fileName = name;
408
+ state.dirty = false;
409
+ state.cm.setValue(text || '');
410
+ state.cm.clearHistory();
411
+ state.cm.setCursor({ line: 0, ch: 0 });
412
+ updateFileUI();
413
+ updateCursor();
414
+ requestAnimationFrame(() => state.cm.refresh());
415
+ }
416
+
417
+ // --------------------------------------------------------------------------
418
+ // preview
419
+ // --------------------------------------------------------------------------
420
+
421
+ function showScene(scene, view) {
422
+ if (!scene) {
423
+ showError('沒有可顯示的場景');
424
+ return { objects: 0, skipped: 0 };
425
+ }
426
+ const stats = state.viewer.setScene(scene, view || DEFAULT_VIEW);
427
+ els.objectCount.textContent = `${stats.objects} 個物件`;
428
+ els.loadingState.hidden = true;
429
+ els.errorState.hidden = true;
430
+ if (stats.objects === 0) {
431
+ els.emptyState.hidden = false;
432
+ if (stats.skipped > 0) {
433
+ els.emptyHint.textContent = '場景中的幾何使用了尚未支援的 FreeCAD API';
434
+ } else {
435
+ els.emptyHint.innerHTML = '在程式裡呼叫 <code>Part.show(shape)</code>';
436
+ }
437
+ } else {
438
+ els.emptyState.hidden = true;
439
+ }
440
+ return stats;
441
+ }
442
+
443
+ function presentScene(scene, view, { notify = false } = {}) {
444
+ const stats = showScene(scene, view);
445
+ const errors = (scene && scene.errors) || [];
446
+ if (stats.objects === 0 && errors.length) {
447
+ showError(errors.join('\n'));
448
+ } else if (errors.length) {
449
+ toast(`有 ${errors.length} 個物件計算失敗`, 'error');
450
+ } else if (stats.objects === 0) {
451
+ // Empty state already visible.
452
+ } else if (stats.skipped > 0) {
453
+ toast(`已顯示 ${stats.objects} 個物件,${stats.skipped} 個未支援`, 'error');
454
+ } else if (notify) {
455
+ toast('執行完成');
456
+ }
457
+ return stats;
458
+ }
459
+
460
+ // --------------------------------------------------------------------------
461
+ // examples
462
+ // --------------------------------------------------------------------------
463
+
464
+ async function loadExamples() {
465
+ const indexResponse = await fetch('examples/index.json', { cache: 'no-cache' });
466
+ if (!indexResponse.ok) {
467
+ throw new Error(`無法載入範例索引(HTTP ${indexResponse.status})`);
468
+ }
469
+ const index = await indexResponse.json();
470
+ const examples = await Promise.all(
471
+ index.map(async (item) => {
472
+ const [codeResponse, sceneResponse] = await Promise.all([
473
+ fetch(`examples/${item.code}`, { cache: 'no-cache' }),
474
+ fetch(`scenes/${item.scene}`, { cache: 'no-cache' }),
475
+ ]);
476
+ if (!codeResponse.ok || !sceneResponse.ok) {
477
+ throw new Error(`無法載入 ${item.id} 的範例檔`);
478
+ }
479
+ return {
480
+ ...item,
481
+ codeText: await codeResponse.text(),
482
+ sceneData: await sceneResponse.json(),
483
+ };
484
+ }),
485
+ );
486
+ state.examples = examples;
487
+ els.exampleCount.textContent = String(examples.length).padStart(2, '0');
488
+ renderExamples();
489
+ return examples;
490
+ }
491
+
492
+ function renderExamples() {
493
+ els.exampleList.innerHTML = '';
494
+ const fragment = document.createDocumentFragment();
495
+ state.examples.forEach((item, index) => {
496
+ const button = document.createElement('button');
497
+ button.type = 'button';
498
+ button.className = 'example-card';
499
+ button.dataset.id = item.id;
500
+ button.innerHTML = `
501
+ <span class="example-thumb">
502
+ <img src="thumbs/${encodeURIComponent(item.thumb)}" alt="" loading="lazy" draggable="false">
503
+ </span>
504
+ <span class="example-meta">
505
+ <span class="example-title">${escapeHtml(item.title || item.id)}</span>
506
+ <span class="example-sub">${escapeHtml(item.subtitle || item.code)}</span>
507
+ </span>
508
+ <span class="example-index">${String(index + 1).padStart(2, '0')}</span>
509
+ <span class="example-chev" aria-hidden="true">
510
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
511
+ <path d="m9 6 6 6-6 6"/>
512
+ </svg>
513
+ </span>
514
+ `;
515
+ button.addEventListener('click', () => loadExample(item));
516
+ fragment.appendChild(button);
517
+ });
518
+ els.exampleList.appendChild(fragment);
519
+ }
520
+
521
+ function loadExample(item, { initial = false } = {}) {
522
+ if (!item) return;
523
+ state.uiSeq += 1;
524
+ state.pendingRun = null;
525
+ state.current = item;
526
+ state.mode = 'example';
527
+ state.fileName = item.code;
528
+ state.source = item.codeText;
529
+ state.dirty = false;
530
+ state.scene = item.sceneData;
531
+ state.sceneSource = item.codeText;
532
+ setEditor(item.codeText, { mode: 'example', name: item.code });
533
+ updateActive(item.id);
534
+ hideError();
535
+ presentScene(item.sceneData, item.view);
536
+ if (initial) {
537
+ els.canvasHint.classList.remove('is-hidden');
538
+ if (state.hintTimer) clearTimeout(state.hintTimer);
539
+ state.hintTimer = setTimeout(() => els.canvasHint.classList.add('is-hidden'), 9000);
540
+ }
541
+ }
542
+
543
+ // --------------------------------------------------------------------------
544
+ // running
545
+ // --------------------------------------------------------------------------
546
+
547
+ async function runCode(code) {
548
+ if (state.busy) {
549
+ state.pendingRun = { code };
550
+ toast('正在執行,完成後會接著執行');
551
+ return;
552
+ }
553
+ state.busy = true;
554
+ updateRunButton();
555
+ const uiSeq = state.uiSeq;
556
+ showLoading(runtime.ready ? '正在解析 Part 指令…' : 'Python 執行環境載入中…(首次約 10 MB)');
557
+ runtime.preload();
558
+ try {
559
+ const result = await runtime.run(code);
560
+ if (uiSeq !== state.uiSeq) return;
561
+ if (!result || !result.ok) {
562
+ showError((result && result.error) || '執行失敗');
563
+ return;
564
+ }
565
+ state.scene = result.scene;
566
+ state.sceneSource = code;
567
+ presentScene(result.scene, state.current ? state.current.view : DEFAULT_VIEW, { notify: true });
568
+ } catch (error) {
569
+ if (uiSeq !== state.uiSeq) return;
570
+ showError(error && error.message ? error.message : error);
571
+ } finally {
572
+ state.busy = false;
573
+ updateRunButton();
574
+ if (state.pendingRun) {
575
+ const pending = state.pendingRun;
576
+ state.pendingRun = null;
577
+ runCode(pending.code);
578
+ }
579
+ }
580
+ }
581
+
582
+ function requestRun(code) {
583
+ runtime.preload();
584
+ if (state.busy) {
585
+ state.pendingRun = { code };
586
+ toast('正在執行,完成後會接著執行');
587
+ return;
588
+ }
589
+ runCode(code);
590
+ }
591
+
592
+ function runCurrent() {
593
+ const code = state.cm.getValue();
594
+ if (state.mode === 'example' && state.current && !state.dirty && state.current.sceneData) {
595
+ state.uiSeq += 1;
596
+ presentScene(state.current.sceneData, state.current.view);
597
+ if (els.errorState.hidden) toast('已重新載入範例預覽');
598
+ return;
599
+ }
600
+ requestRun(code);
601
+ }
602
+
603
+ // --------------------------------------------------------------------------
604
+ // files
605
+ // --------------------------------------------------------------------------
606
+
607
+ async function loadFile(file) {
608
+ if (!file) return;
609
+ if (!/\.(py|txt)$/i.test(file.name)) {
610
+ toast('請選擇 .py 檔', 'error');
611
+ return;
612
+ }
613
+ if (file.size > MAX_FILE_BYTES) {
614
+ toast('檔案太大(上限 512 KB)', 'error');
615
+ return;
616
+ }
617
+ try {
618
+ const text = await file.text();
619
+ state.uiSeq += 1;
620
+ state.pendingRun = null;
621
+ state.current = null;
622
+ state.mode = 'upload';
623
+ state.fileName = file.name;
624
+ state.source = text;
625
+ state.dirty = false;
626
+ state.scene = null;
627
+ state.sceneSource = '';
628
+ setEditor(text, { mode: 'upload', name: file.name });
629
+ updateActive(null);
630
+ hideError();
631
+ requestRun(text);
632
+ } catch (error) {
633
+ showError(error && error.message ? error.message : error);
634
+ }
635
+ }
636
+
637
+ // --------------------------------------------------------------------------
638
+ // ui events
639
+ // --------------------------------------------------------------------------
640
+
641
+ function bindUI() {
642
+ els.loadBtn.addEventListener('click', () => {
643
+ runtime.preload();
644
+ els.fileInput.click();
645
+ });
646
+
647
+ els.fileInput.addEventListener('change', () => {
648
+ const file = els.fileInput.files && els.fileInput.files[0];
649
+ if (file) loadFile(file);
650
+ els.fileInput.value = '';
651
+ });
652
+
653
+ els.runBtn.addEventListener('click', runCurrent);
654
+
655
+ els.revertBtn.addEventListener('click', () => {
656
+ if (!state.current) return;
657
+ state.uiSeq += 1;
658
+ state.pendingRun = null;
659
+ loadExample(state.current);
660
+ toast('已回到範例程式');
661
+ });
662
+
663
+ els.dismissError.addEventListener('click', hideError);
664
+
665
+ els.autoBtn.addEventListener('click', () => {
666
+ const enabled = !state.viewer.isAutoRotate();
667
+ state.viewer.setAutoRotate(enabled);
668
+ els.autoBtn.classList.toggle('is-on', enabled);
669
+ els.autoBtn.setAttribute('aria-pressed', String(enabled));
670
+ els.autoBtn.title = enabled ? '自動旋轉' : '自動旋轉(已暫停)';
671
+ });
672
+
673
+ els.resetBtn.addEventListener('click', () => {
674
+ state.viewer.resetView();
675
+ els.canvasHint.classList.add('is-hidden');
676
+ });
677
+
678
+ els.view.addEventListener(
679
+ 'pointerdown',
680
+ () => {
681
+ els.canvasHint.classList.add('is-hidden');
682
+ },
683
+ { once: true },
684
+ );
685
+
686
+ window.addEventListener('keydown', (event) => {
687
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
688
+ event.preventDefault();
689
+ runCurrent();
690
+ } else if (event.key === 'Escape') {
691
+ hideError();
692
+ els.dropOverlay.hidden = true;
693
+ }
694
+ });
695
+
696
+ let dragDepth = 0;
697
+ const hasFiles = (event) => {
698
+ const types = event.dataTransfer && event.dataTransfer.types;
699
+ return Boolean(types && Array.from(types).includes('Files'));
700
+ };
701
+ window.addEventListener('dragenter', (event) => {
702
+ if (!hasFiles(event)) return;
703
+ event.preventDefault();
704
+ dragDepth += 1;
705
+ els.dropOverlay.hidden = false;
706
+ });
707
+ window.addEventListener('dragover', (event) => {
708
+ if (!hasFiles(event)) return;
709
+ event.preventDefault();
710
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
711
+ });
712
+ window.addEventListener('dragleave', (event) => {
713
+ if (!hasFiles(event)) return;
714
+ dragDepth = Math.max(0, dragDepth - 1);
715
+ if (dragDepth === 0) els.dropOverlay.hidden = true;
716
+ });
717
+ window.addEventListener('drop', (event) => {
718
+ event.preventDefault();
719
+ dragDepth = 0;
720
+ els.dropOverlay.hidden = true;
721
+ const file = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0];
722
+ if (file) loadFile(file);
723
+ });
724
+
725
+ let resizeTimer = null;
726
+ window.addEventListener('resize', () => {
727
+ if (resizeTimer) clearTimeout(resizeTimer);
728
+ resizeTimer = setTimeout(() => {
729
+ if (state.cm) state.cm.refresh();
730
+ }, 120);
731
+ });
732
+ }
733
+
734
+ function schedulePreload() {
735
+ const connection =
736
+ navigator.connection || navigator.mozConnection || navigator.webkitConnection;
737
+ if (connection && (connection.saveData || /2g/.test(connection.effectiveType || ''))) {
738
+ return;
739
+ }
740
+ setTimeout(() => runtime.preload(), 5000);
741
+ }
742
+
743
+ async function init() {
744
+ state.viewer = createViewer(els.view, { autoRotate: true });
745
+ initEditor();
746
+ bindUI();
747
+ updateFileUI();
748
+ showLoading('正在載入範例…');
749
+ try {
750
+ const examples = await loadExamples();
751
+ if (examples.length > 0) {
752
+ loadExample(examples[0], { initial: true });
753
+ } else {
754
+ showError('找不到任何範例。');
755
+ }
756
+ } catch (error) {
757
+ showError(error && error.message ? error.message : error);
758
+ }
759
+ schedulePreload();
760
+ }
761
+
762
+ init().catch((error) => {
763
+ console.error(error);
764
+ showError(error && error.message ? error.message : error);
765
+ });
cadshim.py ADDED
@@ -0,0 +1,903 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """A tiny, browser-friendly subset of the FreeCAD Python API.
2
+
3
+ The shim does not perform real boundary-representation modelling. It records
4
+ the operations that a macro asks for and emits a small JSON scene graph which
5
+ the Three.js front-end can build and display. It is intentionally forgiving
6
+ so that simple Part-based macros run without a FreeCAD installation.
7
+ """
8
+
9
+ import copy
10
+ import json
11
+ import math
12
+ import sys
13
+ import traceback
14
+ import types
15
+
16
+ PALETTE = [
17
+ (0.94, 0.68, 0.32),
18
+ (0.40, 0.64, 0.56),
19
+ (0.94, 0.49, 0.37),
20
+ (0.40, 0.50, 0.66),
21
+ (0.83, 0.78, 0.66),
22
+ (0.60, 0.47, 0.70),
23
+ ]
24
+
25
+
26
+ # --------------------------------------------------------------------------
27
+ # small vector / quaternion helpers
28
+ # --------------------------------------------------------------------------
29
+
30
+ def _num(value, default=0.0):
31
+ try:
32
+ return float(value)
33
+ except Exception:
34
+ return default
35
+
36
+
37
+ class Vector:
38
+ __slots__ = ("x", "y", "z")
39
+
40
+ def __init__(self, x=0.0, y=0.0, z=0.0):
41
+ if isinstance(x, Vector):
42
+ self.x, self.y, self.z = x.x, x.y, x.z
43
+ elif isinstance(x, (tuple, list)):
44
+ values = list(x) + [0.0, 0.0, 0.0]
45
+ self.x, self.y, self.z = _num(values[0]), _num(values[1]), _num(values[2])
46
+ else:
47
+ self.x, self.y, self.z = _num(x), _num(y), _num(z)
48
+
49
+ def __iter__(self):
50
+ yield self.x
51
+ yield self.y
52
+ yield self.z
53
+
54
+ def __getitem__(self, index):
55
+ return (self.x, self.y, self.z)[index]
56
+
57
+ def __len__(self):
58
+ return 3
59
+
60
+ def __add__(self, other):
61
+ other = Vector(other)
62
+ return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
63
+
64
+ def __sub__(self, other):
65
+ other = Vector(other)
66
+ return Vector(self.x - other.x, self.y - other.y, self.z - other.z)
67
+
68
+ def __mul__(self, other):
69
+ if isinstance(other, (Vector, tuple, list)):
70
+ return self.dot(Vector(other))
71
+ return Vector(self.x * other, self.y * other, self.z * other)
72
+
73
+ __rmul__ = __mul__
74
+
75
+ def __truediv__(self, other):
76
+ return Vector(self.x / other, self.y / other, self.z / other)
77
+
78
+ def __neg__(self):
79
+ return Vector(-self.x, -self.y, -self.z)
80
+
81
+ def __repr__(self):
82
+ return "Vector (%g, %g, %g)" % (self.x, self.y, self.z)
83
+
84
+ def clone(self):
85
+ return Vector(self)
86
+
87
+ copy = clone
88
+
89
+ def Length(self):
90
+ return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
91
+
92
+ def normalize(self):
93
+ length = self.Length()
94
+ if length > 1e-12:
95
+ self.x /= length
96
+ self.y /= length
97
+ self.z /= length
98
+ return self
99
+
100
+ normalized = normalize
101
+
102
+ def dot(self, other):
103
+ other = Vector(other)
104
+ return self.x * other.x + self.y * other.y + self.z * other.z
105
+
106
+ def cross(self, other):
107
+ other = Vector(other)
108
+ return Vector(
109
+ self.y * other.z - self.z * other.y,
110
+ self.z * other.x - self.x * other.z,
111
+ self.x * other.y - self.y * other.x,
112
+ )
113
+
114
+ def to_list(self):
115
+ return [self.x, self.y, self.z]
116
+
117
+
118
+ def _qmul(a, b):
119
+ x1, y1, z1, w1 = a
120
+ x2, y2, z2, w2 = b
121
+ return (
122
+ w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
123
+ w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2,
124
+ w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2,
125
+ w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
126
+ )
127
+
128
+
129
+ def _qaxis(axis, angle_deg):
130
+ axis = Vector(axis)
131
+ length = axis.Length()
132
+ if length < 1e-12:
133
+ return (0.0, 0.0, 0.0, 1.0)
134
+ axis = axis / length
135
+ half = math.radians(_num(angle_deg)) * 0.5
136
+ sine = math.sin(half)
137
+ return (axis.x * sine, axis.y * sine, axis.z * sine, math.cos(half))
138
+
139
+
140
+ def _qnorm(q):
141
+ length = math.sqrt(sum(v * v for v in q))
142
+ if length < 1e-12:
143
+ return (0.0, 0.0, 0.0, 1.0)
144
+ return tuple(v / length for v in q)
145
+
146
+
147
+ def _qconj(q):
148
+ return (-q[0], -q[1], -q[2], q[3])
149
+
150
+
151
+ def _qrotate(q, v):
152
+ v = Vector(v)
153
+ r = _qmul(_qmul(q, (v.x, v.y, v.z, 0.0)), _qconj(q))
154
+ return Vector(r[0], r[1], r[2])
155
+
156
+
157
+ def _qmatrix(q):
158
+ x, y, z, w = _qnorm(q)
159
+ xx, yy, zz = x * x, y * y, z * z
160
+ xy, xz, yz = x * y, x * z, y * z
161
+ wx, wy, wz = w * x, w * y, w * z
162
+ return [
163
+ [1 - 2 * (yy + zz), 2 * (xy - wz), 2 * (xz + wy)],
164
+ [2 * (xy + wz), 1 - 2 * (xx + zz), 2 * (yz - wx)],
165
+ [2 * (xz - wy), 2 * (yz + wx), 1 - 2 * (xx + yy)],
166
+ ]
167
+
168
+
169
+ def _matmul(a, b):
170
+ return [[sum(a[r][k] * b[k][c] for k in range(4)) for c in range(4)] for r in range(4)]
171
+
172
+
173
+ def _mat_identity():
174
+ return [
175
+ [1.0, 0.0, 0.0, 0.0],
176
+ [0.0, 1.0, 0.0, 0.0],
177
+ [0.0, 0.0, 1.0, 0.0],
178
+ [0.0, 0.0, 0.0, 1.0],
179
+ ]
180
+
181
+
182
+ def _mat_translate(v):
183
+ m = _mat_identity()
184
+ m[0][3], m[1][3], m[2][3] = v.x, v.y, v.z
185
+ return m
186
+
187
+
188
+ def _mat_rotate(q):
189
+ r = _qmatrix(q)
190
+ return [
191
+ [r[0][0], r[0][1], r[0][2], 0.0],
192
+ [r[1][0], r[1][1], r[1][2], 0.0],
193
+ [r[2][0], r[2][1], r[2][2], 0.0],
194
+ [0.0, 0.0, 0.0, 1.0],
195
+ ]
196
+
197
+
198
+ def _mat_to_column_major(m):
199
+ return [m[r][c] for c in range(4) for r in range(4)]
200
+
201
+
202
+ def _mat_is_identity(m):
203
+ for r in range(4):
204
+ for c in range(4):
205
+ expected = 1.0 if r == c else 0.0
206
+ if abs(m[r][c] - expected) > 1e-9:
207
+ return False
208
+ return True
209
+
210
+
211
+ class Rotation:
212
+ def __init__(self, *args):
213
+ if not args:
214
+ self.q = (0.0, 0.0, 0.0, 1.0)
215
+ elif len(args) == 1 and isinstance(args[0], Rotation):
216
+ self.q = tuple(args[0].q)
217
+ elif len(args) == 1 and isinstance(args[0], (tuple, list)) and len(args[0]) == 4:
218
+ self.q = _qnorm(tuple(_num(v) for v in args[0]))
219
+ elif len(args) == 2:
220
+ self.q = _qaxis(Vector(args[0]), args[1])
221
+ elif len(args) == 3:
222
+ yaw = _qaxis((0.0, 0.0, 1.0), args[0])
223
+ pitch = _qaxis((0.0, 1.0, 0.0), args[1])
224
+ roll = _qaxis((1.0, 0.0, 0.0), args[2])
225
+ self.q = _qnorm(_qmul(_qmul(yaw, pitch), roll))
226
+ elif len(args) == 4:
227
+ self.q = _qnorm(tuple(_num(v) for v in args))
228
+ else:
229
+ self.q = (0.0, 0.0, 0.0, 1.0)
230
+
231
+ def __repr__(self):
232
+ return "Rotation (%g, %g, %g, %g)" % self.q
233
+
234
+ def multiply(self, other):
235
+ return Rotation(_qmul(self.q, Rotation(other).q))
236
+
237
+ def __mul__(self, other):
238
+ if isinstance(other, Placement):
239
+ return Placement(other.Base, self).multiply(other.Rotation)
240
+ return self.multiply(other)
241
+
242
+ def toMatrix(self):
243
+ return _qmatrix(self.q)
244
+
245
+ @property
246
+ def Q(self):
247
+ return self.q
248
+
249
+ @property
250
+ def Angle(self):
251
+ w = max(-1.0, min(1.0, self.q[3]))
252
+ return math.degrees(2.0 * math.acos(w))
253
+
254
+ @property
255
+ def Axis(self):
256
+ x, y, z, w = self.q
257
+ s = math.sqrt(max(0.0, 1.0 - w * w))
258
+ if s < 1e-9:
259
+ return Vector(0.0, 0.0, 1.0)
260
+ return Vector(x / s, y / s, z / s)
261
+
262
+ def copy(self):
263
+ return Rotation(self)
264
+
265
+
266
+ class Placement:
267
+ def __init__(self, *args):
268
+ self.Base = Vector()
269
+ self.Rotation = Rotation()
270
+ if not args:
271
+ return
272
+ if len(args) == 1 and isinstance(args[0], Placement):
273
+ self.Base = Vector(args[0].Base)
274
+ self.Rotation = Rotation(args[0].Rotation)
275
+ elif len(args) == 1 and isinstance(args[0], (Vector, tuple, list)):
276
+ self.Base = Vector(args[0])
277
+ elif len(args) >= 2:
278
+ self.Base = Vector(args[0])
279
+ self.Rotation = Rotation(args[1])
280
+
281
+ def __repr__(self):
282
+ return "Placement [Pos=%s, Rot=%s]" % (self.Base, self.Rotation)
283
+
284
+ def copy(self):
285
+ return Placement(self)
286
+
287
+ def multiply(self, other):
288
+ other = Placement(other)
289
+ q = _qmul(self.Rotation.q, other.Rotation.q)
290
+ base = self.Base + _qrotate(self.Rotation.q, other.Base)
291
+ return Placement(base, Rotation(q))
292
+
293
+ def __mul__(self, other):
294
+ if isinstance(other, Vector):
295
+ return self.Base + _qrotate(self.Rotation.q, other)
296
+ if isinstance(other, Placement):
297
+ return self.multiply(other)
298
+ if isinstance(other, Shape):
299
+ shape = other.copy()
300
+ shape.Placement = self * shape.Placement
301
+ return shape
302
+ raise TypeError("unsupported Placement multiplication")
303
+
304
+ def __rmul__(self, other):
305
+ if isinstance(other, Vector):
306
+ return self.Base + _qrotate(self.Rotation.q, other)
307
+ return NotImplemented
308
+
309
+ def toMatrix(self):
310
+ return _mat_to_column_major(_matmul(_mat_translate(self.Base), _mat_rotate(self.Rotation.q)))
311
+
312
+
313
+ class Matrix:
314
+ def __init__(self, *args):
315
+ self._m = _mat_identity()
316
+ if len(args) == 16:
317
+ values = [_num(v) for v in args]
318
+ self._m = [[values[c * 4 + r] for c in range(4)] for r in range(4)]
319
+ elif len(args) == 1 and isinstance(args[0], Matrix):
320
+ self._m = copy.deepcopy(args[0]._m)
321
+
322
+ def move(self, vector):
323
+ self._m = _matmul(_mat_translate(Vector(vector)), self._m)
324
+
325
+ def multiply(self, other):
326
+ self._m = _matmul(self._m, other._m)
327
+
328
+ def A11(self):
329
+ return self._m[0][0]
330
+
331
+ def to_list(self):
332
+ return _mat_to_column_major(self._m)
333
+
334
+
335
+ # --------------------------------------------------------------------------
336
+ # shape tree
337
+ # --------------------------------------------------------------------------
338
+
339
+ class Shape:
340
+ def __init__(self, node=None, profile=None, placed=False):
341
+ self._node = node
342
+ self._profile = profile
343
+ self._placement = Placement()
344
+
345
+ def __repr__(self):
346
+ return "<Shape>"
347
+
348
+ @property
349
+ def Placement(self):
350
+ return self._placement
351
+
352
+ @Placement.setter
353
+ def Placement(self, value):
354
+ self._placement = Placement(value)
355
+
356
+ @property
357
+ def ShapeType(self):
358
+ return "Solid"
359
+
360
+ def copy(self):
361
+ shape = Shape(copy.deepcopy(self._node), copy.deepcopy(self._profile))
362
+ shape._placement = Placement(self._placement)
363
+ return shape
364
+
365
+ def render_node(self):
366
+ node = copy.deepcopy(self._node)
367
+ matrix = self._placement.toMatrix()
368
+ if not _mat_is_identity(_matmul(_mat_translate(self._placement.Base), _mat_rotate(self._placement.Rotation.q))):
369
+ node = {"op": "transform", "m": matrix, "child": node}
370
+ return node
371
+
372
+ def translate(self, vector, *args):
373
+ self._placement = Placement(Vector(vector)) * self._placement
374
+ return self
375
+
376
+ def rotate(self, center, axis, angle, *args):
377
+ center = Vector(center)
378
+ rot = Placement(Vector(0, 0, 0), Rotation(axis, angle))
379
+ self._placement = (
380
+ Placement(center) * rot * Placement(-center) * self._placement
381
+ )
382
+ return self
383
+
384
+ def scale(self, *args):
385
+ return self
386
+
387
+ def cut(self, tool):
388
+ return _boolean("cut", self, tool)
389
+
390
+ def fuse(self, tool):
391
+ return _boolean("fuse", self, tool)
392
+
393
+ def common(self, tool):
394
+ return _boolean("common", self, tool)
395
+
396
+ def multiFuse(self, tools):
397
+ return _multi_boolean("fuse", [self] + list(tools))
398
+
399
+ def multiCommon(self, tools):
400
+ return _multi_boolean("common", [self] + list(tools))
401
+
402
+ def extrude(self, vector):
403
+ if self._profile is None:
404
+ raise ValueError("extrude() needs a Face or closed wire")
405
+ node = {"op": "extrude", "profile": self._profile, "vector": Vector(vector).to_list()}
406
+ matrix = self._placement.toMatrix()
407
+ if not _mat_is_identity(
408
+ _matmul(_mat_translate(self._placement.Base), _mat_rotate(self._placement.Rotation.q))
409
+ ):
410
+ node = {"op": "transform", "m": matrix, "child": node}
411
+ return Shape(node)
412
+
413
+ def removeSplitter(self):
414
+ return self
415
+
416
+ def isClosed(self):
417
+ return self._profile is not None
418
+
419
+
420
+ def _boolean(kind, a, b):
421
+ if not isinstance(a, Shape) or not isinstance(b, Shape):
422
+ raise TypeError("boolean operations need Shape arguments")
423
+ node = {"op": "boolean", "kind": kind, "a": a.render_node(), "b": b.render_node()}
424
+ return Shape(node)
425
+
426
+
427
+ def _multi_boolean(kind, shapes):
428
+ shapes = [s for s in shapes if isinstance(s, Shape)]
429
+ if not shapes:
430
+ raise ValueError("no shapes supplied")
431
+ if len(shapes) == 1:
432
+ return shapes[0].copy()
433
+ node = {"op": "group", "children": [s.render_node() for s in shapes]}
434
+ return Shape(node)
435
+
436
+
437
+ def _points2d(points):
438
+ result = []
439
+ for point in points:
440
+ p = Vector(point)
441
+ result.append([p.x, p.y])
442
+ return result
443
+
444
+
445
+ def _make_face(profile, placement=None):
446
+ face = Shape({"op": "face", "profile": profile}, profile=profile)
447
+ if placement is not None:
448
+ face._placement = Placement(placement)
449
+ return face
450
+
451
+
452
+ # --------------------------------------------------------------------------
453
+ # Part module
454
+ # --------------------------------------------------------------------------
455
+
456
+ def _make_box(length, width, height, pnt=None, dir=None):
457
+ return Shape(
458
+ {
459
+ "op": "box",
460
+ "l": _num(length),
461
+ "w": _num(width),
462
+ "h": _num(height),
463
+ "pnt": Vector(pnt).to_list() if pnt is not None else [0, 0, 0],
464
+ "dir": Vector(dir).to_list() if dir is not None else [0, 0, 1],
465
+ }
466
+ )
467
+
468
+
469
+ def _make_cylinder(radius, height, pnt=None, dir=None, angle=360):
470
+ return Shape(
471
+ {
472
+ "op": "cylinder",
473
+ "r": _num(radius),
474
+ "h": _num(height),
475
+ "pnt": Vector(pnt).to_list() if pnt is not None else [0, 0, 0],
476
+ "dir": Vector(dir).to_list() if dir is not None else [0, 0, 1],
477
+ "angle": _num(angle, 360),
478
+ }
479
+ )
480
+
481
+
482
+ def _make_sphere(radius, pnt=None, *args):
483
+ return Shape(
484
+ {
485
+ "op": "sphere",
486
+ "r": _num(radius),
487
+ "pnt": Vector(pnt).to_list() if pnt is not None else [0, 0, 0],
488
+ }
489
+ )
490
+
491
+
492
+ def _make_cone(radius1, radius2, height, pnt=None, dir=None, angle=360):
493
+ return Shape(
494
+ {
495
+ "op": "cone",
496
+ "r1": _num(radius1),
497
+ "r2": _num(radius2),
498
+ "h": _num(height),
499
+ "pnt": Vector(pnt).to_list() if pnt is not None else [0, 0, 0],
500
+ "dir": Vector(dir).to_list() if dir is not None else [0, 0, 1],
501
+ "angle": _num(angle, 360),
502
+ }
503
+ )
504
+
505
+
506
+ def _make_torus(radius1, radius2, pnt=None, dir=None, *args):
507
+ return Shape(
508
+ {
509
+ "op": "torus",
510
+ "r1": _num(radius1),
511
+ "r2": _num(radius2),
512
+ "pnt": Vector(pnt).to_list() if pnt is not None else [0, 0, 0],
513
+ "dir": Vector(dir).to_list() if dir is not None else [0, 0, 1],
514
+ }
515
+ )
516
+
517
+
518
+ def _make_polygon(points, closed=False):
519
+ profile = _points2d(points)
520
+ return Shape({"op": "wire", "profile": profile, "closed": bool(closed)}, profile=profile)
521
+
522
+
523
+ def _make_line(point1, point2):
524
+ return _make_polygon([point1, point2], False)
525
+
526
+
527
+ def _make_face_from(value):
528
+ if isinstance(value, Shape):
529
+ if value._profile is None:
530
+ raise ValueError("Part.Face() needs a closed wire")
531
+ return _make_face(copy.deepcopy(value._profile), value._placement)
532
+ profile = _points2d(value)
533
+ if len(profile) > 2 and profile[0] != profile[-1]:
534
+ profile.append(profile[0])
535
+ return _make_face(profile)
536
+
537
+
538
+ def _make_compound(shapes):
539
+ shapes = [s for s in shapes if isinstance(s, Shape)]
540
+ return _multi_boolean("fuse", shapes)
541
+
542
+
543
+ def _show(shape, name=None):
544
+ document = _active_document()
545
+ if document is None:
546
+ document = newDocument("Unnamed")
547
+ return document._show(shape, name)
548
+
549
+
550
+ # --------------------------------------------------------------------------
551
+ # document
552
+ # --------------------------------------------------------------------------
553
+
554
+ class ViewObject:
555
+ def __init__(self, obj):
556
+ self.obj = obj
557
+ self.ShapeColor = None
558
+ self.LineColor = None
559
+ self.Visibility = True
560
+ self.Transparency = 0
561
+ self.DisplayMode = "Flat Lines"
562
+
563
+ def update(self):
564
+ pass
565
+
566
+
567
+ class DocObject:
568
+ def __init__(self, document, type_id, name):
569
+ object.__setattr__(self, "Document", document)
570
+ object.__setattr__(self, "Name", name)
571
+ object.__setattr__(self, "Label", name)
572
+ object.__setattr__(self, "TypeId", type_id)
573
+ object.__setattr__(self, "_shape", None)
574
+ object.__setattr__(self, "_feature_shape", None)
575
+ object.__setattr__(self, "_dirty", True)
576
+ object.__setattr__(self, "ViewObject", ViewObject(self))
577
+ object.__setattr__(self, "_props", self._defaults(type_id))
578
+ if "Placement" not in self._props:
579
+ self._props["Placement"] = Placement()
580
+
581
+ @staticmethod
582
+ def _defaults(type_id):
583
+ if type_id == "Part::Box":
584
+ return {"Length": 10.0, "Width": 10.0, "Height": 10.0, "Placement": Placement()}
585
+ if type_id == "Part::Cylinder":
586
+ return {"Radius": 2.0, "Height": 10.0, "Angle": 360.0, "Placement": Placement()}
587
+ if type_id == "Part::Sphere":
588
+ return {"Radius": 5.0, "Placement": Placement()}
589
+ if type_id == "Part::Cone":
590
+ return {
591
+ "Radius1": 2.0,
592
+ "Radius2": 4.0,
593
+ "Height": 10.0,
594
+ "Angle": 360.0,
595
+ "Placement": Placement(),
596
+ }
597
+ if type_id == "Part::Torus":
598
+ return {"Radius1": 10.0, "Radius2": 2.0, "Placement": Placement()}
599
+ if type_id in ("Part::Cut", "Part::Fuse"):
600
+ return {"Base": None, "Tool": None, "Placement": Placement()}
601
+ if type_id in ("Part::MultiFuse", "Part::MultiCommon"):
602
+ return {"Shapes": [], "Placement": Placement()}
603
+ return {"Placement": Placement()}
604
+
605
+ def __getattr__(self, name):
606
+ props = object.__getattribute__(self, "_props")
607
+ if name in props:
608
+ return props[name]
609
+ raise AttributeError("'%s' object has no attribute '%s'" % (self.TypeId, name))
610
+
611
+ def __setattr__(self, name, value):
612
+ if name in ("Document", "Name", "Label", "TypeId", "ViewObject"):
613
+ object.__setattr__(self, name, value)
614
+ elif name == "Shape":
615
+ object.__setattr__(self, "_feature_shape", value)
616
+ object.__setattr__(self, "_shape", value)
617
+ object.__setattr__(self, "_dirty", False)
618
+ elif name.startswith("_"):
619
+ object.__setattr__(self, name, value)
620
+ else:
621
+ self._props[name] = value
622
+ object.__setattr__(self, "_dirty", True)
623
+
624
+ @property
625
+ def Shape(self):
626
+ if self._dirty or self._shape is None:
627
+ self._shape = self._compute()
628
+ object.__setattr__(self, "_dirty", False)
629
+ return self._shape
630
+
631
+ @Shape.setter
632
+ def Shape(self, value):
633
+ object.__setattr__(self, "_feature_shape", value)
634
+ object.__setattr__(self, "_shape", value)
635
+ object.__setattr__(self, "_dirty", False)
636
+
637
+ def _compute(self):
638
+ type_id = self.TypeId
639
+ placement = self._props.get("Placement", Placement())
640
+ if type_id == "Part::Feature":
641
+ return self._feature_shape
642
+ if type_id == "Part::Box":
643
+ shape = _make_box(self.Length, self.Width, self.Height)
644
+ elif type_id == "Part::Cylinder":
645
+ shape = _make_cylinder(self.Radius, self.Height, angle=self.Angle)
646
+ elif type_id == "Part::Sphere":
647
+ shape = _make_sphere(self.Radius)
648
+ elif type_id == "Part::Cone":
649
+ shape = _make_cone(self.Radius1, self.Radius2, self.Height, angle=self.Angle)
650
+ elif type_id == "Part::Torus":
651
+ shape = _make_torus(self.Radius1, self.Radius2)
652
+ elif type_id == "Part::Cut":
653
+ if self.Base is None or self.Tool is None:
654
+ raise ValueError("%s needs Base and Tool" % self.Name)
655
+ shape = self.Base.Shape.copy().cut(self.Tool.Shape)
656
+ elif type_id == "Part::Fuse":
657
+ if self.Base is None or self.Tool is None:
658
+ raise ValueError("%s needs Base and Tool" % self.Name)
659
+ shape = self.Base.Shape.copy().fuse(self.Tool.Shape)
660
+ elif type_id == "Part::MultiFuse":
661
+ shape = _multi_boolean("fuse", [s.Shape for s in self.Shapes])
662
+ elif type_id == "Part::MultiCommon":
663
+ shape = _multi_boolean("common", [s.Shape for s in self.Shapes])
664
+ else:
665
+ return self._feature_shape
666
+ if type_id != "Part::Feature":
667
+ shape.Placement = placement
668
+ return shape
669
+
670
+
671
+ class Document:
672
+ def __init__(self, name="Unnamed"):
673
+ self.Name = name
674
+ self.Label = name
675
+ self._objects = []
676
+ self._counter = 0
677
+ self._errors = []
678
+
679
+ @property
680
+ def Objects(self):
681
+ return list(self._objects)
682
+
683
+ def addObject(self, type_id, name=None):
684
+ return self._add(type_id, name)
685
+
686
+ def _add(self, type_id, name=None):
687
+ if name is None:
688
+ name = type_id.split("::")[-1] + "%03d" % self._counter
689
+ base_name = name
690
+ while self.getObject(name) is not None:
691
+ self._counter += 1
692
+ name = base_name + "%03d" % self._counter
693
+ obj = DocObject(self, type_id, name)
694
+ self._objects.append(obj)
695
+ self._counter += 1
696
+ return obj
697
+
698
+ def _show(self, shape, name=None):
699
+ if not isinstance(shape, Shape):
700
+ raise TypeError("Part.show() needs a Shape")
701
+ obj = self._add("Part::Feature", name or "Shape")
702
+ obj.Shape = shape.copy()
703
+ return obj
704
+
705
+ def recompute(self):
706
+ self._errors = []
707
+ for obj in list(self._objects):
708
+ try:
709
+ _ = obj.Shape
710
+ except Exception as error:
711
+ self._errors.append("%s: %s" % (obj.Name, error))
712
+ return self
713
+
714
+ def getObject(self, name):
715
+ for obj in self._objects:
716
+ if obj.Name == name:
717
+ return obj
718
+ return None
719
+
720
+ def removeObject(self, name):
721
+ obj = self.getObject(name)
722
+ if obj is not None:
723
+ self._objects.remove(obj)
724
+
725
+ def __repr__(self):
726
+ return "<Document %s>" % self.Name
727
+
728
+
729
+ # --------------------------------------------------------------------------
730
+ # module installation
731
+ # --------------------------------------------------------------------------
732
+
733
+ _freecad = None
734
+ _part = None
735
+ _documents = {}
736
+ _active = None
737
+ _log = []
738
+
739
+
740
+ def _log_message(kind):
741
+ def inner(*args, **kwargs):
742
+ text = " ".join(str(a) for a in args)
743
+ _log.append("%s: %s" % (kind, text))
744
+
745
+ return inner
746
+
747
+
748
+ def newDocument(name="Unnamed", *args, **kwargs):
749
+ global _active
750
+ document = Document(name)
751
+ if name in _documents:
752
+ index = 1
753
+ while "%s%03d" % (name, index) in _documents:
754
+ index += 1
755
+ document.Name = "%s%03d" % (name, index)
756
+ document.Label = document.Name
757
+ _documents[document.Name] = document
758
+ _active = document
759
+ if _freecad is not None:
760
+ _freecad.ActiveDocument = document
761
+ return document
762
+
763
+
764
+ def activeDocument():
765
+ return _active
766
+
767
+
768
+ def _active_document():
769
+ return _active
770
+
771
+
772
+ def closeDocument(name):
773
+ global _active
774
+ document = _documents.pop(name, None)
775
+ if _active is document:
776
+ _active = None
777
+ if _freecad is not None:
778
+ _freecad.ActiveDocument = None
779
+ return document
780
+
781
+
782
+ def listDocuments():
783
+ return dict(_documents)
784
+
785
+
786
+ def _get_color(obj, index):
787
+ color = getattr(obj.ViewObject, "ShapeColor", None)
788
+ opacity = 1.0 - (max(0.0, min(100.0, _num(getattr(obj.ViewObject, "Transparency", 0)))) / 100.0)
789
+ if color is None:
790
+ rgb = PALETTE[index % len(PALETTE)]
791
+ elif isinstance(color, str):
792
+ value = color.strip()
793
+ if value.startswith("#") and len(value) >= 7:
794
+ rgb = (
795
+ int(value[1:3], 16) / 255.0,
796
+ int(value[3:5], 16) / 255.0,
797
+ int(value[5:7], 16) / 255.0,
798
+ )
799
+ else:
800
+ rgb = PALETTE[index % len(PALETTE)]
801
+ else:
802
+ values = list(color)
803
+ values += [0.0, 0.0, 0.0]
804
+ rgb = (_num(values[0], 0.8), _num(values[1], 0.8), _num(values[2], 0.8))
805
+ return [max(0.0, min(1.0, float(c))) for c in rgb], opacity
806
+
807
+
808
+ def build_scene():
809
+ document = _active
810
+ if document is None:
811
+ return {"objects": [], "errors": [], "log": list(_log)}
812
+ document.recompute()
813
+ objects = []
814
+ for obj in document.Objects:
815
+ if getattr(obj.ViewObject, "Visibility", True) is False:
816
+ continue
817
+ try:
818
+ shape = obj.Shape
819
+ except Exception:
820
+ continue
821
+ if shape is None:
822
+ continue
823
+ color, opacity = _get_color(obj, len(objects))
824
+ objects.append(
825
+ {
826
+ "name": obj.Name,
827
+ "color": color,
828
+ "opacity": opacity,
829
+ "node": shape.render_node(),
830
+ }
831
+ )
832
+ return {"objects": objects, "errors": list(document._errors), "log": list(_log)}
833
+
834
+
835
+ class Runtime:
836
+ def reset(self):
837
+ global _active, _log
838
+ _documents.clear()
839
+ _active = None
840
+ _log = []
841
+ if _freecad is not None:
842
+ _freecad.ActiveDocument = None
843
+
844
+ def run(self, code):
845
+ self.reset()
846
+ try:
847
+ namespace = {"__name__": "__main__", "__file__": "<wfreecad>"}
848
+ exec(compile(code, "<wfreecad>", "exec"), namespace, namespace)
849
+ scene = build_scene()
850
+ return {"ok": True, "scene": scene}
851
+ except Exception:
852
+ return {"ok": False, "error": traceback.format_exc(limit=8), "log": list(_log)}
853
+
854
+
855
+ _runtime = Runtime()
856
+
857
+
858
+ def install():
859
+ global _freecad, _part
860
+ if _freecad is not None:
861
+ return
862
+
863
+ _freecad = types.ModuleType("FreeCAD")
864
+ _freecad.Vector = Vector
865
+ _freecad.Rotation = Rotation
866
+ _freecad.Placement = Placement
867
+ _freecad.Matrix = Matrix
868
+ _freecad.newDocument = newDocument
869
+ _freecad.activeDocument = activeDocument
870
+ _freecad.closeDocument = closeDocument
871
+ _freecad.listDocuments = listDocuments
872
+ _freecad.ActiveDocument = None
873
+ _freecad.Console = types.SimpleNamespace(
874
+ PrintMessage=_log_message("Message"),
875
+ PrintWarning=_log_message("Warning"),
876
+ PrintError=_log_message("Error"),
877
+ )
878
+ sys.modules["FreeCAD"] = _freecad
879
+
880
+ _part = types.ModuleType("Part")
881
+ _part.makeBox = _make_box
882
+ _part.makeCylinder = _make_cylinder
883
+ _part.makeSphere = _make_sphere
884
+ _part.makeCone = _make_cone
885
+ _part.makeTorus = _make_torus
886
+ _part.makePolygon = _make_polygon
887
+ _part.makeLine = _make_line
888
+ _part.makeCompound = _make_compound
889
+ _part.Face = _make_face_from
890
+ _part.Wire = lambda *args, **kwargs: _make_polygon(args[0] if args else [], True)
891
+ _part.show = _show
892
+ _part.Shape = Shape
893
+ sys.modules["Part"] = _part
894
+
895
+ try:
896
+ sys.modules["FreeCADGui"] = types.ModuleType("FreeCADGui")
897
+ except Exception:
898
+ pass
899
+
900
+
901
+ install()
902
+
903
+ __wfcad_runtime = _runtime
examples/computer.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import FreeCAD as App
2
+ import Part
3
+
4
+ cream = (0.89, 0.85, 0.74)
5
+ sand = (0.78, 0.73, 0.62)
6
+ screen = (0.08, 0.11, 0.15)
7
+ slate = (0.24, 0.29, 0.32)
8
+ mint = (0.37, 0.63, 0.54)
9
+ coral = (0.93, 0.47, 0.34)
10
+
11
+ # Monitor shell.
12
+ monitor = Part.makeBox(26, 4, 17, App.Vector(-13, -2, 8))
13
+ monitor_obj = Part.show(monitor, "Monitor")
14
+ monitor_obj.ViewObject.ShapeColor = cream
15
+
16
+ # Screen panel and a tiny status line.
17
+ glass = Part.makeBox(23, 0.6, 14, App.Vector(-11.5, -2.4, 9.5))
18
+ glass_obj = Part.show(glass, "Screen")
19
+ glass_obj.ViewObject.ShapeColor = screen
20
+
21
+ status = Part.makeBox(20, 0.12, 0.42, App.Vector(-10, -2.52, 9.0))
22
+ status_obj = Part.show(status, "StatusLine")
23
+ status_obj.ViewObject.ShapeColor = mint
24
+
25
+ power = Part.makeCylinder(0.28, 0.35, App.Vector(9.9, -2.5, 8.6), App.Vector(0, 1, 0))
26
+ power_obj = Part.show(power, "PowerDot")
27
+ power_obj.ViewObject.ShapeColor = coral
28
+
29
+ # Tapered stand and round foot.
30
+ neck = Part.makeCone(2.6, 1.3, 6.5, App.Vector(0, 0, 1.5))
31
+ neck_obj = Part.show(neck, "Stand")
32
+ neck_obj.ViewObject.ShapeColor = sand
33
+
34
+ foot = Part.makeCylinder(6.2, 1.5, App.Vector(0, 0, 0))
35
+ foot_obj = Part.show(foot, "Foot")
36
+ foot_obj.ViewObject.ShapeColor = sand
37
+
38
+ # Keyboard slab.
39
+ keyboard = Part.makeBox(24, 8, 1, App.Vector(-12, -11.5, 0))
40
+ keyboard_obj = Part.show(keyboard, "Keyboard")
41
+ keyboard_obj.ViewObject.ShapeColor = cream
42
+
43
+ keywell = Part.makeBox(21.5, 6.6, 0.32, App.Vector(-10.75, -10.8, 1.0))
44
+ keywell_obj = Part.show(keywell, "KeyWell")
45
+ keywell_obj.ViewObject.ShapeColor = slate
46
+
47
+ for row in range(5):
48
+ keys = Part.makeBox(
49
+ 19,
50
+ 0.9,
51
+ 0.36,
52
+ App.Vector(-9.5, -10.1 + row * 1.35, 1.32),
53
+ )
54
+ keys_obj = Part.show(keys, "KeyRow")
55
+ keys_obj.ViewObject.ShapeColor = (0.42, 0.48, 0.48)
56
+
57
+ # A small tower on the right.
58
+ tower = Part.makeBox(6, 9, 16, App.Vector(14, -2, 0))
59
+ tower_obj = Part.show(tower, "Tower")
60
+ tower_obj.ViewObject.ShapeColor = cream
61
+
62
+ vent = Part.makeBox(1.2, 0.22, 8.5, App.Vector(15.4, -2.12, 3.2))
63
+ vent_obj = Part.show(vent, "Vent")
64
+ vent_obj.ViewObject.ShapeColor = slate
65
+
66
+ button = Part.makeCylinder(0.55, 0.3, App.Vector(18.1, -2.3, 12.6), App.Vector(0, 1, 0))
67
+ button_obj = Part.show(button, "TowerButton")
68
+ button_obj.ViewObject.ShapeColor = mint
examples/gear.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import FreeCAD as App
3
+ import Part
4
+
5
+ # A chunky spur gear, built from a closed 2D profile and extruded.
6
+ teeth = 18
7
+ module = 2.2
8
+ thickness = 6.0
9
+
10
+ pitch_r = module * teeth / 2.0
11
+ tip_r = pitch_r + module * 0.9
12
+ root_r = pitch_r - module * 1.1
13
+ step = 2.0 * math.pi / teeth
14
+
15
+ points = []
16
+ for i in range(teeth):
17
+ a = i * step
18
+ for frac, radius in (
19
+ (-0.46, root_r),
20
+ (-0.40, root_r),
21
+ (-0.35, root_r + (tip_r - root_r) * 0.30),
22
+ (-0.28, root_r + (tip_r - root_r) * 0.82),
23
+ (-0.20, tip_r),
24
+ (0.20, tip_r),
25
+ (0.28, root_r + (tip_r - root_r) * 0.82),
26
+ (0.35, root_r + (tip_r - root_r) * 0.30),
27
+ (0.40, root_r),
28
+ (0.46, root_r),
29
+ ):
30
+ angle = a + frac * step
31
+ points.append(
32
+ App.Vector(math.cos(angle) * radius, math.sin(angle) * radius, 0)
33
+ )
34
+ points.append(points[0])
35
+
36
+ profile = Part.Face(Part.makePolygon(points, True))
37
+ gear = profile.extrude(App.Vector(0, 0, thickness))
38
+
39
+ # Centre bore.
40
+ gear = gear.cut(
41
+ Part.makeCylinder(4.2, thickness + 2.0, App.Vector(0, 0, -1.0))
42
+ )
43
+
44
+ # Five lightening holes.
45
+ for i in range(5):
46
+ angle = 2.0 * math.pi * i / 5.0 + math.pi / 10.0
47
+ centre = App.Vector(
48
+ math.cos(angle) * pitch_r * 0.62,
49
+ math.sin(angle) * pitch_r * 0.62,
50
+ -1.0,
51
+ )
52
+ gear = gear.cut(Part.makeCylinder(2.8, thickness + 2.0, centre))
53
+
54
+ plate = Part.show(gear, "GearPlate")
55
+ plate.ViewObject.ShapeColor = (0.94, 0.68, 0.32)
56
+
57
+ hub = Part.makeCylinder(6.8, thickness + 3.0, App.Vector(0, 0, -1.5))
58
+ hub = hub.cut(Part.makeCylinder(4.2, thickness + 5.0, App.Vector(0, 0, -2.5)))
59
+ hub_obj = Part.show(hub, "Hub")
60
+ hub_obj.ViewObject.ShapeColor = (0.27, 0.38, 0.35)
examples/index.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "gear",
4
+ "title": "齒輪",
5
+ "subtitle": "Profile · boolean",
6
+ "tag": "Part",
7
+ "code": "gear.py",
8
+ "scene": "gear.json",
9
+ "thumb": "gear.png",
10
+ "view": [0.85, -1.25, 1.22]
11
+ },
12
+ {
13
+ "id": "computer",
14
+ "title": "桌上電腦",
15
+ "subtitle": "Boxes · cone",
16
+ "tag": "Part",
17
+ "code": "computer.py",
18
+ "scene": "computer.json",
19
+ "thumb": "computer.png",
20
+ "view": [0.86, -1.35, 0.98]
21
+ },
22
+ {
23
+ "id": "rocket",
24
+ "title": "火箭",
25
+ "subtitle": "Cones · torus",
26
+ "tag": "Part",
27
+ "code": "rocket.py",
28
+ "scene": "rocket.json",
29
+ "thumb": "rocket.png",
30
+ "view": [0.9, -1.25, 0.52]
31
+ },
32
+ {
33
+ "id": "mug",
34
+ "title": "馬克杯",
35
+ "subtitle": "Hollow · handle",
36
+ "tag": "Part",
37
+ "code": "mug.py",
38
+ "scene": "mug.json",
39
+ "thumb": "mug.png",
40
+ "view": [0.82, -1.25, 1.12]
41
+ }
42
+ ]
examples/mug.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import FreeCAD as App
2
+ import Part
3
+
4
+ mint = (0.42, 0.66, 0.58)
5
+ cream = (0.91, 0.87, 0.77)
6
+ coffee = (0.34, 0.21, 0.13)
7
+
8
+ # Outer mug body.
9
+ body = Part.makeCylinder(5.0, 9.0, App.Vector(0, 0, 0))
10
+
11
+ # Hollow it out from the top.
12
+ body = body.cut(
13
+ Part.makeCylinder(4.15, 8.5, App.Vector(0, 0, 0.9))
14
+ )
15
+
16
+ body_obj = Part.show(body, "MugBody")
17
+ body_obj.ViewObject.ShapeColor = mint
18
+
19
+ # Rounded rim.
20
+ rim = Part.makeTorus(4.6, 0.45, App.Vector(0, 0, 9.0))
21
+ rim_obj = Part.show(rim, "Rim")
22
+ rim_obj.ViewObject.ShapeColor = cream
23
+
24
+ # Coffee surface.
25
+ coffee_shape = Part.makeCylinder(4.02, 0.22, App.Vector(0, 0, 8.3))
26
+ coffee_obj = Part.show(coffee_shape, "Coffee")
27
+ coffee_obj.ViewObject.ShapeColor = coffee
28
+
29
+ # Handle: a torus turned to sit in the XZ plane.
30
+ handle = Part.makeTorus(3.4, 0.9, App.Vector(4.6, 0, 4.6), App.Vector(0, 1, 0))
31
+ handle_obj = Part.show(handle, "Handle")
32
+ handle_obj.ViewObject.ShapeColor = mint
examples/rocket.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import FreeCAD as App
2
+ import Part
3
+
4
+ cream = (0.90, 0.86, 0.76)
5
+ coral = (0.94, 0.47, 0.34)
6
+ mint = (0.37, 0.63, 0.54)
7
+ screen = (0.09, 0.12, 0.17)
8
+ flame = (1.00, 0.80, 0.32)
9
+
10
+ # Main body.
11
+ body = Part.makeCylinder(4.5, 16, App.Vector(0, 0, 3))
12
+ body_obj = Part.show(body, "Body")
13
+ body_obj.ViewObject.ShapeColor = cream
14
+
15
+ # Nose cone.
16
+ nose = Part.makeCone(4.5, 0.2, 7, App.Vector(0, 0, 19))
17
+ nose_obj = Part.show(nose, "Nose")
18
+ nose_obj.ViewObject.ShapeColor = coral
19
+
20
+ # Coral band around the body.
21
+ band = Part.makeCylinder(4.58, 0.7, App.Vector(0, 0, 6.2))
22
+ band_obj = Part.show(band, "Band")
23
+ band_obj.ViewObject.ShapeColor = coral
24
+
25
+ # Porthole and rim.
26
+ window = Part.makeCylinder(2.0, 0.9, App.Vector(0, -4.8, 12.4), App.Vector(0, 1, 0))
27
+ window_obj = Part.show(window, "Window")
28
+ window_obj.ViewObject.ShapeColor = screen
29
+
30
+ rim = Part.makeTorus(2.2, 0.32, App.Vector(0, -4.75, 12.4), App.Vector(0, 1, 0))
31
+ rim_obj = Part.show(rim, "WindowRim")
32
+ rim_obj.ViewObject.ShapeColor = cream
33
+
34
+ # Three swept fins. The profile is drawn in XY, extruded along Z and then
35
+ # tipped upright so it becomes a vertical triangle.
36
+ fin_profile = Part.Face(
37
+ Part.makePolygon(
38
+ [
39
+ App.Vector(4.0, 0, 0),
40
+ App.Vector(9.5, 0, 0),
41
+ App.Vector(4.0, 6.5, 0),
42
+ ],
43
+ True,
44
+ )
45
+ )
46
+ fin = fin_profile.extrude(App.Vector(0, 0, 0.8))
47
+ fin.rotate(App.Vector(0, 0, 0), App.Vector(1, 0, 0), 90)
48
+ fin.translate(App.Vector(0, -0.4, 3.0))
49
+
50
+ for i in range(3):
51
+ blade = fin.copy()
52
+ blade.rotate(App.Vector(0, 0, 0), App.Vector(0, 0, 1), i * 120)
53
+ blade_obj = Part.show(blade, "Fin")
54
+ blade_obj.ViewObject.ShapeColor = mint
55
+
56
+ # Exhaust flame.
57
+ outer = Part.makeCone(0.7, 3.6, 6.0, App.Vector(0, 0, -3.0))
58
+ outer_obj = Part.show(outer, "OuterFlame")
59
+ outer_obj.ViewObject.ShapeColor = coral
60
+
61
+ inner = Part.makeCone(0.0, 1.8, 5.2, App.Vector(0, 0, -4.2))
62
+ inner_obj = Part.show(inner, "InnerFlame")
63
+ inner_obj.ViewObject.ShapeColor = flame
favicon.svg ADDED
index.html CHANGED
@@ -1,19 +1,166 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="zh-Hant">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>WFreeCAD · tiny part studio</title>
7
+ <meta name="description" content="A tiny in-browser FreeCAD Python playground. Load a Part macro and orbit the result.">
8
+ <link rel="icon" href="favicon.svg" type="image/svg+xml">
9
+ <link rel="stylesheet" href="vendor/codemirror/codemirror.css">
10
+ <link rel="stylesheet" href="style.css">
11
+ </head>
12
+ <body>
13
+ <header class="topbar">
14
+ <a class="brand" href="./" aria-label="WFreeCAD 首頁">
15
+ <span class="brand-mark" aria-hidden="true">
16
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round">
17
+ <circle cx="12" cy="12" r="3.1"/>
18
+ <path d="M12 3v2.6M12 18.4V21M3 12h2.6M18.4 12H21M5.6 5.6l1.9 1.9M16.5 16.5l1.9 1.9M18.4 5.6l-1.9 1.9M7.5 16.5l-1.9 1.9"/>
19
+ </svg>
20
+ </span>
21
+ <span class="brand-name">WFreeCAD</span>
22
+ <span class="brand-sub">part studio</span>
23
+ </a>
24
+
25
+ <div class="topbar-actions">
26
+ <div class="runtime" id="runtime" data-state="idle" title="Python 執行環境">
27
+ <span class="runtime-dot" aria-hidden="true"></span>
28
+ <span class="runtime-text" id="runtimeText">Python 待命</span>
29
+ </div>
30
+ <button class="btn ghost" id="loadBtn" type="button">
31
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
32
+ <path d="M12 16V4m0 0L7.5 8.5M12 4l4.5 4.5M5 15.5v3A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5v-3"/>
33
+ </svg>
34
+ 載入 .py
35
+ </button>
36
+ <input type="file" id="fileInput" accept=".py,.txt,text/x-python" hidden>
37
+ </div>
38
+ </header>
39
+
40
+ <main class="workspace">
41
+ <aside class="panel examples" aria-label="範例">
42
+ <div class="panel-head">
43
+ <h2>範例</h2>
44
+ <span class="panel-count" id="exampleCount">04</span>
45
+ </div>
46
+ <div class="example-list" id="exampleList">
47
+ <div class="example-skeleton"></div>
48
+ <div class="example-skeleton"></div>
49
+ <div class="example-skeleton"></div>
50
+ </div>
51
+ <div class="examples-foot">
52
+ <span class="drop-dot"></span>
53
+ 拖放 .py 到畫布也可以載入
54
+ </div>
55
+ </aside>
56
+
57
+ <section class="panel editor-panel" aria-label="Python 編輯器">
58
+ <div class="panel-head">
59
+ <div class="file-meta">
60
+ <span class="file-dot" aria-hidden="true"></span>
61
+ <span class="file-name" id="fileName">未命名.py</span>
62
+ <span class="state-pill" id="fileState" data-state="example">範例</span>
63
+ </div>
64
+ <div class="editor-actions">
65
+ <button class="icon-btn" id="revertBtn" type="button" title="回到範例程式" aria-label="回到範例程式" disabled>
66
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
67
+ <path d="M4.5 10.5a7.5 7.5 0 1 1 2.1 5.3M4.5 10.5V5.5m0 5h5"/>
68
+ </svg>
69
+ </button>
70
+ <button class="btn run" id="runBtn" type="button" title="執行 (Ctrl / ⌘ + Enter)">
71
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
72
+ <path d="M8.5 5.4v13.2L19 12 8.5 5.4Z"/>
73
+ </svg>
74
+ 執行
75
+ <kbd>⌘↵</kbd>
76
+ </button>
77
+ </div>
78
+ </div>
79
+ <div class="editor-wrap" id="editorWrap">
80
+ <textarea id="editor" spellcheck="false" aria-label="Python 程式碼"></textarea>
81
+ </div>
82
+ <div class="editor-status">
83
+ <span class="status-left"><span class="lang-dot"></span>Python · FreeCAD Part API</span>
84
+ <span class="status-right" id="cursorInfo">Ln 1, Col 1</span>
85
+ </div>
86
+ </section>
87
+
88
+ <section class="panel preview-panel" aria-label="3D 預覽畫布">
89
+ <div class="panel-head">
90
+ <div class="preview-meta">
91
+ <span class="live-dot" aria-hidden="true"></span>
92
+ <h2>預覽</h2>
93
+ <span class="object-count" id="objectCount">0 個物件</span>
94
+ </div>
95
+ <div class="view-actions">
96
+ <button class="icon-btn is-on" id="autoBtn" type="button" title="自動旋轉" aria-label="自動旋轉" aria-pressed="true">
97
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
98
+ <path d="M20 10.5a8 8 0 0 0-14.6-3M4 13.5a8 8 0 0 0 14.6 3M4 4.5v4h4M20 19.5v-4h-4"/>
99
+ </svg>
100
+ </button>
101
+ <button class="icon-btn" id="resetBtn" type="button" title="重設視角" aria-label="重設視角">
102
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
103
+ <path d="m12 3 8 4.5v9L12 21l-8-4.5v-9L12 3Z"/>
104
+ <path d="m4 7.5 8 4.5 8-4.5M12 12v9"/>
105
+ </svg>
106
+ </button>
107
+ </div>
108
+ </div>
109
+
110
+ <div class="canvas-wrap" id="canvasWrap">
111
+ <canvas id="view" aria-label="3D 模型預覽"></canvas>
112
+ <div class="canvas-hint" id="canvasHint">
113
+ <span class="mouse-icon" aria-hidden="true"></span>
114
+ 拖曳旋轉 · 滾輪縮放
115
+ </div>
116
+
117
+ <div class="canvas-loading" id="loadingState" hidden>
118
+ <span class="spinner" aria-hidden="true"></span>
119
+ <div>
120
+ <strong>正在執行 Python</strong>
121
+ <span id="loadingText">首次執行會下載執行環境…</span>
122
+ </div>
123
+ </div>
124
+
125
+ <div class="canvas-empty" id="emptyState" hidden>
126
+ <span class="empty-mark" aria-hidden="true">∅</span>
127
+ <strong>沒有可顯示的物件</strong>
128
+ <span id="emptyHint">在程式裡呼叫 <code>Part.show(shape)</code></span>
129
+ </div>
130
+
131
+ <div class="canvas-error" id="errorState" hidden>
132
+ <div class="error-head">
133
+ <span>無法執行這段程式</span>
134
+ <button class="icon-btn tiny" id="dismissError" type="button" aria-label="關閉錯誤">
135
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true">
136
+ <path d="m6 6 12 12M18 6 6 18"/>
137
+ </svg>
138
+ </button>
139
+ </div>
140
+ <pre id="errorText"></pre>
141
+ <p>支援 FreeCAD / Part 的基本 Part API。</p>
142
+ </div>
143
+
144
+ <div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
145
+ </div>
146
+ </section>
147
+ </main>
148
+
149
+ <div class="drop-overlay" id="dropOverlay" hidden>
150
+ <div class="drop-card">
151
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
152
+ <path d="M12 16V4m0 0L7.5 8.5M12 4l4.5 4.5M5 15.5v3A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5v-3"/>
153
+ </svg>
154
+ <strong>放開以載入 .py</strong>
155
+ <span>檔案會直接開啟在編輯器</span>
156
+ </div>
157
+ </div>
158
+
159
+ <script src="vendor/codemirror/codemirror.js"></script>
160
+ <script src="vendor/codemirror/python.js"></script>
161
+ <script src="vendor/codemirror/closebrackets.js"></script>
162
+ <script src="vendor/codemirror/matchbrackets.js"></script>
163
+ <script src="vendor/codemirror/active-line.js"></script>
164
+ <script type="module" src="app.js"></script>
165
+ </body>
166
  </html>
scenes/computer.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"objects":[{"name":"Monitor","color":[0.89,0.85,0.74],"opacity":1.0,"node":{"op":"box","l":26.0,"w":4.0,"h":17.0,"pnt":[-13.0,-2.0,8.0],"dir":[0,0,1]}},{"name":"Screen","color":[0.08,0.11,0.15],"opacity":1.0,"node":{"op":"box","l":23.0,"w":0.6,"h":14.0,"pnt":[-11.5,-2.4,9.5],"dir":[0,0,1]}},{"name":"StatusLine","color":[0.37,0.63,0.54],"opacity":1.0,"node":{"op":"box","l":20.0,"w":0.12,"h":0.42,"pnt":[-10.0,-2.52,9.0],"dir":[0,0,1]}},{"name":"PowerDot","color":[0.93,0.47,0.34],"opacity":1.0,"node":{"op":"cylinder","r":0.28,"h":0.35,"pnt":[9.9,-2.5,8.6],"dir":[0.0,1.0,0.0],"angle":360.0}},{"name":"Stand","color":[0.78,0.73,0.62],"opacity":1.0,"node":{"op":"cone","r1":2.6,"r2":1.3,"h":6.5,"pnt":[0.0,0.0,1.5],"dir":[0,0,1],"angle":360.0}},{"name":"Foot","color":[0.78,0.73,0.62],"opacity":1.0,"node":{"op":"cylinder","r":6.2,"h":1.5,"pnt":[0.0,0.0,0.0],"dir":[0,0,1],"angle":360.0}},{"name":"Keyboard","color":[0.89,0.85,0.74],"opacity":1.0,"node":{"op":"box","l":24.0,"w":8.0,"h":1.0,"pnt":[-12.0,-11.5,0.0],"dir":[0,0,1]}},{"name":"KeyWell","color":[0.24,0.29,0.32],"opacity":1.0,"node":{"op":"box","l":21.5,"w":6.6,"h":0.32,"pnt":[-10.75,-10.8,1.0],"dir":[0,0,1]}},{"name":"KeyRow","color":[0.42,0.48,0.48],"opacity":1.0,"node":{"op":"box","l":19.0,"w":0.9,"h":0.36,"pnt":[-9.5,-10.1,1.32],"dir":[0,0,1]}},{"name":"KeyRow010","color":[0.42,0.48,0.48],"opacity":1.0,"node":{"op":"box","l":19.0,"w":0.9,"h":0.36,"pnt":[-9.5,-8.75,1.32],"dir":[0,0,1]}},{"name":"KeyRow012","color":[0.42,0.48,0.48],"opacity":1.0,"node":{"op":"box","l":19.0,"w":0.9,"h":0.36,"pnt":[-9.5,-7.3999999999999995,1.32],"dir":[0,0,1]}},{"name":"KeyRow014","color":[0.42,0.48,0.48],"opacity":1.0,"node":{"op":"box","l":19.0,"w":0.9,"h":0.36,"pnt":[-9.5,-6.049999999999999,1.32],"dir":[0,0,1]}},{"name":"KeyRow016","color":[0.42,0.48,0.48],"opacity":1.0,"node":{"op":"box","l":19.0,"w":0.9,"h":0.36,"pnt":[-9.5,-4.699999999999999,1.32],"dir":[0,0,1]}},{"name":"Tower","color":[0.89,0.85,0.74],"opacity":1.0,"node":{"op":"box","l":6.0,"w":9.0,"h":16.0,"pnt":[14.0,-2.0,0.0],"dir":[0,0,1]}},{"name":"Vent","color":[0.24,0.29,0.32],"opacity":1.0,"node":{"op":"box","l":1.2,"w":0.22,"h":8.5,"pnt":[15.4,-2.12,3.2],"dir":[0,0,1]}},{"name":"TowerButton","color":[0.37,0.63,0.54],"opacity":1.0,"node":{"op":"cylinder","r":0.55,"h":0.3,"pnt":[18.1,-2.3,12.6],"dir":[0.0,1.0,0.0],"angle":360.0}}],"errors":[],"log":[]}
scenes/gear.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"objects":[{"name":"GearPlate","color":[0.94,0.68,0.32],"opacity":1.0,"node":{"op":"boolean","kind":"cut","a":{"op":"boolean","kind":"cut","a":{"op":"boolean","kind":"cut","a":{"op":"boolean","kind":"cut","a":{"op":"boolean","kind":"cut","a":{"op":"boolean","kind":"cut","a":{"op":"extrude","profile":[[17.156428286968527,-2.7787350420840893],[17.210859034728493,-2.418828494685937],[18.56061303569272,-2.2789567216762574],[20.88783267081867,-2.048069900145029],[21.726945014658973,-1.519295998147049],[21.726945014658973,1.519295998147049],[20.88783267081867,2.048069900145029],[18.56061303569272,2.2789567216762574],[17.210859034728493,2.418828494685937],[17.156428286968527,2.7787350420840893],[17.072152417664608,3.2566872474998934],[17.000205300753542,3.613505186412657],[18.220720211483897,4.2065847162302745],[20.32862338576771,5.2195033518159715],[20.936279737536587,6.003381609694242],[19.89702006745585,8.858724086190929],[18.927661064190527,9.068615695856005],[16.66182200232248,8.489622345129524],[15.34562916388815,8.159415761218781],[15.17138570294929,8.479000875831174],[14.928723008654947,8.899304991450531],[14.739075911198682,9.2099968123731],[15.683139620579428,10.184749954781006],[17.317482101864858,11.857527467882596],[17.620390137486357,12.801962794930066],[15.667220851375824,15.129659308596962],[14.684534190694514,14.995352600133277],[12.753369333168724,13.676314220278487],[11.629489938516956,12.915857066797109],[11.356450097348032,13.156574067227526],[10.984669280315108,13.46853521367864],[10.70019644115994,13.695626897684827],[11.253940932943305,14.934484037884376],[12.217596897661192,17.065358772855014],[12.179221437592869,18.056438330248806],[9.547723577066108,19.57573432839586],[8.670235773157486,19.113428673000044],[7.306672102749422,17.213440759560633],[6.510662593568553,16.11445539237076],[6.171759006653423,16.24727025576273],[5.715702320316577,16.413261314727418],[5.370715362236586,16.52936225320977],[5.467350878315178,17.882898936508763],[5.644089195073197,20.21485595194925],[5.269058886160767,21.133040918291204],[2.2766299299694923,21.660686881120995],[1.6101789623256733,20.9261431637386],[0.9786823817430521,18.67437229991053],[0.6065532526894687,17.369412573591884],[0.2426626942943462,17.378305867281707],[-0.2426626942943441,17.378305867281707],[-0.6065532526894666,17.369412573591884],[-0.9786823817430498,18.67437229991053],[-1.6101789623256706,20.9261431637386],[-2.2766299299694897,21.660686881120995],[-5.269058886160765,21.133040918291204],[-5.644089195073194,20.21485595194925],[-5.467350878315175,17.882898936508763],[-5.370715362236584,16.52936225320977],[-5.715702320316575,16.41326131472742],[-6.171759006653418,16.247270255762732],[-6.510662593568547,16.114455392370765],[-7.306672102749416,17.213440759560633],[-8.67023577315748,19.113428673000044],[-9.547723577066101,19.57573432839586],[-12.179221437592863,18.05643833024881],[-12.217596897661188,17.065358772855017],[-11.253940932943298,14.93448403788438],[-10.70019644115994,13.695626897684829],[-10.984669280315108,13.468535213678642],[-11.356450097348032,13.156574067227526],[-11.629489938516949,12.915857066797114],[-12.753369333168719,13.676314220278488],[-14.684534190694508,14.995352600133282],[-15.667220851375818,15.129659308596965],[-17.620390137486353,12.801962794930068],[-17.317482101864858,11.857527467882598],[-15.683139620579427,10.18474995478101],[-14.739075911198682,9.2099968123731],[-14.928723008654943,8.89930499145054],[-15.171385702949289,8.47900087583118],[-15.345629163888146,8.159415761218787],[-16.661822002322477,8.489622345129526],[-18.927661064190527,9.06861569585601],[-19.897020067455845,8.858724086190934],[-20.936279737536587,6.003381609694244],[-20.32862338576771,5.219503351815973],[-18.220720211483897,4.206584716230279],[-17.000205300753542,3.613505186412657],[-17.072152417664608,3.256687247499893],[-17.156428286968527,2.778735042084088],[-17.21085903472849,2.4188284946859424],[-18.56061303569272,2.278956721676259],[-20.88783267081867,2.0480699001450344],[-21.726945014658973,1.519295998147054],[-21.726945014658973,-1.5192959981470486],[-20.88783267081867,-2.048069900145029],[-18.56061303569272,-2.2789567216762547],[-17.21085903472849,-2.4188284946859384],[-17.15642828696853,-2.7787350420840835],[-17.072152417664608,-3.2566872474998885],[-17.000205300753542,-3.6135051864126524],[-18.220720211483897,-4.2065847162302745],[-20.32862338576771,-5.219503351815968],[-20.936279737536587,-6.003381609694238],[-19.89702006745585,-8.858724086190929],[-18.927661064190527,-9.068615695856005],[-16.66182200232248,-8.489622345129522],[-15.345629163888148,-8.159415761218783],[-15.17138570294929,-8.479000875831176],[-14.928723008654945,-8.899304991450537],[-14.739075911198684,-9.209996812373099],[-15.683139620579428,-10.184749954781006],[-17.31748210186486,-11.857527467882594],[-17.62039013748636,-12.801962794930063],[-15.667220851375822,-15.129659308596963],[-14.68453419069451,-14.995352600133279],[-12.753369333168724,-13.676314220278487],[-11.629489938516954,-12.915857066797113],[-11.356450097348034,-13.156574067227524],[-10.98466928031511,-13.468535213678638],[-10.70019644115995,-13.69562689768482],[-11.253940932943314,-14.934484037884367],[-12.217596897661199,-17.06535877285501],[-12.179221437592876,-18.056438330248803],[-9.547723577066114,-19.575734328395853],[-8.670235773157488,-19.113428673000044],[-7.30667210274942,-17.213440759560633],[-6.510662593568555,-16.11445539237076],[-6.171759006653433,-16.247270255762725],[-5.715702320316573,-16.41326131472742],[-5.370715362236588,-16.52936225320977],[-5.467350878315184,-17.88289893650876],[-5.644089195073199,-20.214855951949247],[-5.269058886160765,-21.133040918291204],[-2.27662992996949,-21.660686881120995],[-1.6101789623256666,-20.9261431637386],[-0.978682381743042,-18.674372299910534],[-0.6065532526894631,-17.369412573591884],[-0.24266269429434834,-17.378305867281707],[0.24266269429434195,-17.378305867281707],[0.6065532526894568,-17.369412573591884],[0.9786823817430351,-18.674372299910534],[1.6101789623256586,-20.9261431637386],[2.276629929969482,-21.660686881120995],[5.2690588861607575,-21.133040918291208],[5.6440891950731915,-20.21485595194925],[5.467350878315177,-17.882898936508763],[5.370715362236583,-16.52936225320977],[5.715702320316566,-16.41326131472742],[6.171759006653426,-16.24727025576273],[6.51066259356855,-16.114455392370765],[7.306672102749414,-17.213440759560637],[8.670235773157481,-19.113428673000044],[9.547723577066106,-19.57573432839586],[12.17922143759287,-18.056438330248806],[12.217596897661195,-17.065358772855014],[11.25394093294331,-14.93448403788437],[10.700196441159944,-13.695626897684823],[10.984669280315106,-13.468535213678642],[11.35645009734803,-13.156574067227528],[11.629489938516947,-12.915857066797116],[12.753369333168713,-13.676314220278497],[14.684534190694507,-14.995352600133284],[15.667220851375816,-15.129659308596969],[17.620390137486353,-12.80196279493007],[17.317482101864858,-11.857527467882601],[15.683139620579428,-10.184749954781005],[14.739075911198682,-9.209996812373102],[14.928723008654941,-8.899304991450542],[15.171385702949287,-8.479000875831181],[15.345629163888143,-8.159415761218796],[16.66182200232247,-8.489622345129543],[18.92766106419052,-9.068615695856021],[19.89702006745584,-8.858724086190945],[20.93627973753658,-6.003381609694255],[20.328623385767706,-5.219503351815984],[18.220720211483897,-4.206584716230282],[17.00020530075354,-3.613505186412666],[17.072152417664604,-3.2566872474999102],[17.156428286968527,-2.7787350420840893]],"vector":[0.0,0.0,6.0]},"b":{"op":"cylinder","r":4.2,"h":8.0,"pnt":[0.0,0.0,-1.0],"dir":[0,0,1],"angle":360.0}},"b":{"op":"cylinder","r":2.8,"h":8.0,"pnt":[11.675169794039304,3.7934926229468546,-1.0],"dir":[0,0,1],"angle":360.0}},"b":{"op":"cylinder","r":2.8,"h":8.0,"pnt":[7.516882053166454e-16,12.276,-1.0],"dir":[0,0,1],"angle":360.0}},"b":{"op":"cylinder","r":2.8,"h":8.0,"pnt":[-11.675169794039304,3.7934926229468555,-1.0],"dir":[0,0,1],"angle":360.0}},"b":{"op":"cylinder","r":2.8,"h":8.0,"pnt":[-7.215651757142402,-9.931492622946854,-1.0],"dir":[0,0,1],"angle":360.0}},"b":{"op":"cylinder","r":2.8,"h":8.0,"pnt":[7.215651757142398,-9.931492622946855,-1.0],"dir":[0,0,1],"angle":360.0}}},{"name":"Hub","color":[0.27,0.38,0.35],"opacity":1.0,"node":{"op":"boolean","kind":"cut","a":{"op":"cylinder","r":6.8,"h":9.0,"pnt":[0.0,0.0,-1.5],"dir":[0,0,1],"angle":360.0},"b":{"op":"cylinder","r":4.2,"h":11.0,"pnt":[0.0,0.0,-2.5],"dir":[0,0,1],"angle":360.0}}}],"errors":[],"log":[]}
scenes/mug.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"objects":[{"name":"MugBody","color":[0.42,0.66,0.58],"opacity":1.0,"node":{"op":"boolean","kind":"cut","a":{"op":"cylinder","r":5.0,"h":9.0,"pnt":[0.0,0.0,0.0],"dir":[0,0,1],"angle":360.0},"b":{"op":"cylinder","r":4.15,"h":8.5,"pnt":[0.0,0.0,0.9],"dir":[0,0,1],"angle":360.0}}},{"name":"Rim","color":[0.91,0.87,0.77],"opacity":1.0,"node":{"op":"torus","r1":4.6,"r2":0.45,"pnt":[0.0,0.0,9.0],"dir":[0,0,1]}},{"name":"Coffee","color":[0.34,0.21,0.13],"opacity":1.0,"node":{"op":"cylinder","r":4.02,"h":0.22,"pnt":[0.0,0.0,8.3],"dir":[0,0,1],"angle":360.0}},{"name":"Handle","color":[0.42,0.66,0.58],"opacity":1.0,"node":{"op":"torus","r1":3.4,"r2":0.9,"pnt":[4.6,0.0,4.6],"dir":[0.0,1.0,0.0]}}],"errors":[],"log":[]}
scenes/rocket.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"objects":[{"name":"Body","color":[0.9,0.86,0.76],"opacity":1.0,"node":{"op":"cylinder","r":4.5,"h":16.0,"pnt":[0.0,0.0,3.0],"dir":[0,0,1],"angle":360.0}},{"name":"Nose","color":[0.94,0.47,0.34],"opacity":1.0,"node":{"op":"cone","r1":4.5,"r2":0.2,"h":7.0,"pnt":[0.0,0.0,19.0],"dir":[0,0,1],"angle":360.0}},{"name":"Band","color":[0.94,0.47,0.34],"opacity":1.0,"node":{"op":"cylinder","r":4.58,"h":0.7,"pnt":[0.0,0.0,6.2],"dir":[0,0,1],"angle":360.0}},{"name":"Window","color":[0.09,0.12,0.17],"opacity":1.0,"node":{"op":"cylinder","r":2.0,"h":0.9,"pnt":[0.0,-4.8,12.4],"dir":[0.0,1.0,0.0],"angle":360.0}},{"name":"WindowRim","color":[0.9,0.86,0.76],"opacity":1.0,"node":{"op":"torus","r1":2.2,"r2":0.32,"pnt":[0.0,-4.75,12.4],"dir":[0.0,1.0,0.0]}},{"name":"Fin","color":[0.37,0.63,0.54],"opacity":1.0,"node":{"op":"transform","m":[1.0,0.0,0.0,0.0,0.0,2.220446049250313e-16,1.0,0.0,0.0,-1.0,2.220446049250313e-16,0.0,0.0,-0.4,3.0,1.0],"child":{"op":"extrude","profile":[[4.0,0.0],[9.5,0.0],[4.0,6.5]],"vector":[0.0,0.0,0.8]}}},{"name":"Fin007","color":[0.37,0.63,0.54],"opacity":1.0,"node":{"op":"transform","m":[-0.5,0.8660254037844388,0.0,0.0,-1.6653345369377348e-16,-2.220446049250313e-16,1.0,0.0,0.8660254037844388,0.49999999999999994,0.0,0.0,0.34641016151377557,0.19999999999999993,3.0000000000000004,1.0],"child":{"op":"extrude","profile":[[4.0,0.0],[9.5,0.0],[4.0,6.5]],"vector":[0.0,0.0,0.8]}}},{"name":"Fin009","color":[0.37,0.63,0.54],"opacity":1.0,"node":{"op":"transform","m":[-0.5000000000000004,-0.8660254037844386,0.0,0.0,1.6653345369377348e-16,-2.220446049250313e-16,1.0,0.0,-0.8660254037844385,0.5000000000000004,1.1102230246251565e-16,0.0,-0.34641016151377546,0.2000000000000002,3.0000000000000004,1.0],"child":{"op":"extrude","profile":[[4.0,0.0],[9.5,0.0],[4.0,6.5]],"vector":[0.0,0.0,0.8]}}},{"name":"OuterFlame","color":[0.94,0.47,0.34],"opacity":1.0,"node":{"op":"cone","r1":0.7,"r2":3.6,"h":6.0,"pnt":[0.0,0.0,-3.0],"dir":[0,0,1],"angle":360.0}},{"name":"InnerFlame","color":[1.0,0.8,0.32],"opacity":1.0,"node":{"op":"cone","r1":0.0,"r2":1.8,"h":5.2,"pnt":[0.0,0.0,-4.2],"dir":[0,0,1],"angle":360.0}}],"errors":[],"log":[]}
style.css CHANGED
@@ -1,28 +1,1404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
 
 
 
 
 
 
 
 
 
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
 
1
+ :root {
2
+ --bg: #f1ede4;
3
+ --bg-deep: #e9e3d6;
4
+ --panel: #fbf9f3;
5
+ --panel-2: #f6f2e9;
6
+ --code-bg: #f8f4eb;
7
+ --ink: #211e18;
8
+ --ink-2: #4d473c;
9
+ --muted: #8d8474;
10
+ --line: #ded7c8;
11
+ --line-strong: #cec4b2;
12
+ --accent: #e4663b;
13
+ --accent-deep: #c94f29;
14
+ --accent-ink: #fff8f2;
15
+ --accent-soft: #f8ddd1;
16
+ --sage: #3f6d5e;
17
+ --sage-soft: #e2ece6;
18
+ --danger: #b5442e;
19
+ --radius: 14px;
20
+ --radius-sm: 9px;
21
+ --mono: ui-monospace, "SF Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
22
+ --sans: "Avenir Next", "Segoe UI", "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif;
23
+ }
24
+
25
+ * {
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ html,
30
  body {
31
+ height: 100%;
32
+ }
33
+
34
+ body {
35
+ margin: 0;
36
+ color: var(--ink);
37
+ background: var(--bg);
38
+ font-family: var(--sans);
39
+ font-size: 14px;
40
+ -webkit-font-smoothing: antialiased;
41
+ text-rendering: optimizeLegibility;
42
+ overflow: hidden;
43
+ }
44
+
45
+ body::before {
46
+ content: "";
47
+ position: fixed;
48
+ inset: 0;
49
+ z-index: 0;
50
+ pointer-events: none;
51
+ background-image: radial-gradient(rgba(60, 48, 30, 0.035) 0.6px, transparent 0.7px);
52
+ background-size: 5px 5px;
53
+ opacity: 0.55;
54
+ }
55
+
56
+ button,
57
+ input,
58
+ textarea {
59
+ font: inherit;
60
+ color: inherit;
61
+ }
62
+
63
+ button {
64
+ -webkit-tap-highlight-color: transparent;
65
+ }
66
+
67
+ svg {
68
+ display: block;
69
+ }
70
+
71
+ :focus-visible {
72
+ outline: 2px solid rgba(228, 102, 59, 0.58);
73
+ outline-offset: 2px;
74
+ }
75
+
76
+ * {
77
+ scrollbar-width: thin;
78
+ scrollbar-color: #d0c7b7 transparent;
79
+ }
80
+
81
+ ::-webkit-scrollbar {
82
+ width: 9px;
83
+ height: 9px;
84
+ }
85
+
86
+ ::-webkit-scrollbar-thumb {
87
+ background: #d0c7b7;
88
+ border: 2px solid transparent;
89
+ border-radius: 999px;
90
+ background-clip: content-box;
91
+ }
92
+
93
+ ::-webkit-scrollbar-track {
94
+ background: transparent;
95
+ }
96
+
97
+ /* ------------------------------------------------------------------ */
98
+ /* top bar */
99
+ /* ------------------------------------------------------------------ */
100
+
101
+ .topbar {
102
+ position: relative;
103
+ z-index: 2;
104
+ height: 66px;
105
+ padding: 0 22px;
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: space-between;
109
+ gap: 16px;
110
+ border-bottom: 1px solid rgba(206, 196, 178, 0.55);
111
+ background: linear-gradient(180deg, rgba(251, 249, 243, 0.72), rgba(241, 237, 228, 0.42));
112
+ }
113
+
114
+ .brand {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: 10px;
118
+ min-width: 0;
119
+ color: var(--ink);
120
+ text-decoration: none;
121
+ }
122
+
123
+ .brand-mark {
124
+ width: 32px;
125
+ height: 32px;
126
+ flex: none;
127
+ display: grid;
128
+ place-items: center;
129
+ border-radius: 9px;
130
+ color: #fff7ef;
131
+ background: var(--accent);
132
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28), 0 5px 14px -8px rgba(180, 70, 35, 0.7);
133
+ }
134
+
135
+ .brand-mark svg {
136
+ width: 20px;
137
+ height: 20px;
138
+ }
139
+
140
+ .brand-name {
141
+ font-size: 19px;
142
+ font-weight: 720;
143
+ letter-spacing: -0.045em;
144
+ line-height: 1;
145
+ white-space: nowrap;
146
+ }
147
+
148
+ .brand-sub {
149
+ margin-left: 2px;
150
+ padding-left: 11px;
151
+ border-left: 1px solid var(--line-strong);
152
+ font-size: 9px;
153
+ font-weight: 650;
154
+ letter-spacing: 0.19em;
155
+ text-transform: uppercase;
156
+ color: var(--muted);
157
+ white-space: nowrap;
158
+ }
159
+
160
+ .topbar-actions {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 9px;
164
+ min-width: 0;
165
+ }
166
+
167
+ .runtime {
168
+ display: inline-flex;
169
+ align-items: center;
170
+ gap: 7px;
171
+ height: 30px;
172
+ padding: 0 10px;
173
+ border: 1px solid var(--line);
174
+ border-radius: 999px;
175
+ background: rgba(255, 255, 255, 0.38);
176
+ color: var(--muted);
177
+ font-size: 10.5px;
178
+ font-weight: 600;
179
+ letter-spacing: 0.01em;
180
+ white-space: nowrap;
181
+ transition: color 0.2s, border-color 0.2s, background 0.2s;
182
+ }
183
+
184
+ .runtime-dot {
185
+ width: 7px;
186
+ height: 7px;
187
+ flex: none;
188
+ border-radius: 50%;
189
+ background: #b6ad9c;
190
+ box-shadow: 0 0 0 3px rgba(182, 173, 156, 0.14);
191
+ transition: background 0.2s, box-shadow 0.2s;
192
+ }
193
+
194
+ .runtime[data-state="loading"] {
195
+ color: #9b6b1f;
196
+ border-color: #e8d6ab;
197
+ background: #fbf2dd;
198
+ }
199
+
200
+ .runtime[data-state="loading"] .runtime-dot {
201
+ background: #d99a32;
202
+ box-shadow: 0 0 0 3px rgba(217, 154, 50, 0.16);
203
+ animation: pulse 1.1s ease-in-out infinite;
204
+ }
205
+
206
+ .runtime[data-state="ready"] {
207
+ color: var(--sage);
208
+ border-color: #c9ddd2;
209
+ background: #eef6f1;
210
+ }
211
+
212
+ .runtime[data-state="ready"] .runtime-dot {
213
+ background: #4f8a72;
214
+ box-shadow: 0 0 0 3px rgba(79, 138, 114, 0.15);
215
+ }
216
+
217
+ .runtime[data-state="busy"] {
218
+ color: var(--accent-deep);
219
+ border-color: #ecc7b6;
220
+ background: #fdf0e8;
221
+ }
222
+
223
+ .runtime[data-state="busy"] .runtime-dot {
224
+ background: var(--accent);
225
+ box-shadow: 0 0 0 3px rgba(228, 102, 59, 0.14);
226
+ animation: pulse 1s ease-in-out infinite;
227
+ }
228
+
229
+ .runtime[data-state="error"] {
230
+ color: var(--danger);
231
+ border-color: #e8c2b7;
232
+ background: #fbebe6;
233
+ }
234
+
235
+ .runtime[data-state="error"] .runtime-dot {
236
+ background: #c8553d;
237
+ box-shadow: 0 0 0 3px rgba(200, 85, 61, 0.14);
238
+ }
239
+
240
+ /* ------------------------------------------------------------------ */
241
+ /* generic controls */
242
+ /* ------------------------------------------------------------------ */
243
+
244
+ .btn {
245
+ display: inline-flex;
246
+ align-items: center;
247
+ justify-content: center;
248
+ gap: 8px;
249
+ height: 36px;
250
+ padding: 0 14px;
251
+ border: 1px solid transparent;
252
+ border-radius: var(--radius-sm);
253
+ background: transparent;
254
+ font-size: 12px;
255
+ font-weight: 650;
256
+ letter-spacing: 0.005em;
257
+ white-space: nowrap;
258
+ cursor: pointer;
259
+ transition: background 0.16s, border-color 0.16s, color 0.16s, transform 0.16s;
260
+ }
261
+
262
+ .btn svg {
263
+ width: 15px;
264
+ height: 15px;
265
+ flex: none;
266
+ }
267
+
268
+ .btn:active {
269
+ transform: translateY(1px);
270
+ }
271
+
272
+ .btn:disabled {
273
+ cursor: default;
274
+ opacity: 0.55;
275
+ transform: none;
276
+ }
277
+
278
+ .btn.ghost {
279
+ border-color: var(--line-strong);
280
+ background: rgba(255, 255, 255, 0.42);
281
+ color: var(--ink-2);
282
+ }
283
+
284
+ .btn.ghost:hover {
285
+ background: #fffdf8;
286
+ border-color: #c3b8a4;
287
+ color: var(--ink);
288
+ }
289
+
290
+ .btn.run {
291
+ height: 34px;
292
+ padding: 0 11px 0 13px;
293
+ background: var(--accent);
294
+ border-color: var(--accent);
295
+ color: var(--accent-ink);
296
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.24), 0 6px 16px -10px rgba(180, 70, 35, 0.9);
297
+ }
298
+
299
+ .btn.run:hover {
300
+ background: var(--accent-deep);
301
+ border-color: var(--accent-deep);
302
+ }
303
+
304
+ .btn.run svg {
305
+ width: 13px;
306
+ height: 13px;
307
+ }
308
+
309
+ .btn.run.is-busy svg {
310
+ display: none;
311
+ }
312
+
313
+ .btn.run.is-busy::before {
314
+ content: "";
315
+ width: 11px;
316
+ height: 11px;
317
+ border: 1.5px solid rgba(255, 255, 255, 0.34);
318
+ border-top-color: #fff;
319
+ border-radius: 50%;
320
+ animation: spin 0.8s linear infinite;
321
+ }
322
+
323
+ .btn.run kbd {
324
+ margin-left: 1px;
325
+ padding: 2px 5px;
326
+ border-radius: 4px;
327
+ background: rgba(255, 255, 255, 0.18);
328
+ font-family: var(--sans);
329
+ font-size: 9px;
330
+ font-weight: 600;
331
+ letter-spacing: 0.02em;
332
+ opacity: 0.82;
333
  }
334
 
335
+ .icon-btn {
336
+ width: 32px;
337
+ height: 32px;
338
+ flex: none;
339
+ display: grid;
340
+ place-items: center;
341
+ padding: 0;
342
+ border: 1px solid transparent;
343
+ border-radius: 8px;
344
+ background: transparent;
345
+ color: var(--muted);
346
+ cursor: pointer;
347
+ transition: background 0.16s, color 0.16s, border-color 0.16s;
348
  }
349
 
350
+ .icon-btn svg {
351
+ width: 17px;
352
+ height: 17px;
 
 
353
  }
354
 
355
+ .icon-btn:hover {
356
+ background: rgba(59, 48, 32, 0.05);
357
+ color: var(--ink-2);
 
 
 
358
  }
359
 
360
+ .icon-btn.is-on {
361
+ color: var(--sage);
362
+ background: var(--sage-soft);
363
+ }
364
+
365
+ .icon-btn.tiny {
366
+ width: 26px;
367
+ height: 26px;
368
+ border-radius: 7px;
369
+ }
370
+
371
+ .icon-btn.tiny svg {
372
+ width: 14px;
373
+ height: 14px;
374
+ }
375
+
376
+ .icon-btn:disabled {
377
+ opacity: 0.32;
378
+ cursor: default;
379
+ }
380
+
381
+ .icon-btn:disabled:hover {
382
+ background: transparent;
383
+ color: var(--muted);
384
+ }
385
+
386
+ /* ------------------------------------------------------------------ */
387
+ /* workspace panels */
388
+ /* ------------------------------------------------------------------ */
389
+
390
+ .workspace {
391
+ position: relative;
392
+ z-index: 1;
393
+ width: 100%;
394
+ max-width: 1720px;
395
+ height: calc(100vh - 66px);
396
+ margin: 0 auto;
397
+ padding: 14px 18px 18px;
398
+ display: grid;
399
+ grid-template-columns: 248px minmax(330px, 0.92fr) minmax(380px, 1.08fr);
400
+ gap: 14px;
401
+ min-height: 0;
402
+ }
403
+
404
+ .panel {
405
+ min-width: 0;
406
+ min-height: 0;
407
+ display: flex;
408
+ flex-direction: column;
409
+ overflow: hidden;
410
+ border: 1px solid var(--line);
411
+ border-radius: var(--radius);
412
+ background: var(--panel);
413
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65), 0 16px 34px -32px rgba(52, 42, 27, 0.4);
414
+ }
415
+
416
+ .panel-head {
417
+ height: 50px;
418
+ flex: none;
419
+ display: flex;
420
+ align-items: center;
421
+ justify-content: space-between;
422
+ gap: 10px;
423
+ padding: 0 13px 0 16px;
424
+ border-bottom: 1px solid var(--line);
425
+ }
426
+
427
+ .panel-head h2 {
428
+ margin: 0;
429
+ font-size: 12px;
430
+ font-weight: 680;
431
+ letter-spacing: 0.03em;
432
+ color: var(--ink-2);
433
+ }
434
+
435
+ .panel-count {
436
+ min-width: 27px;
437
+ padding: 2px 7px;
438
+ border: 1px solid var(--line);
439
+ border-radius: 999px;
440
+ color: var(--muted);
441
+ font-size: 10px;
442
+ font-weight: 600;
443
+ font-variant-numeric: tabular-nums;
444
+ text-align: center;
445
+ }
446
+
447
+ /* ------------------------------------------------------------------ */
448
+ /* examples */
449
+ /* ------------------------------------------------------------------ */
450
+
451
+ .examples {
452
+ background: var(--panel-2);
453
+ }
454
+
455
+ .example-list {
456
+ flex: 1;
457
+ min-height: 0;
458
+ display: flex;
459
+ flex-direction: column;
460
+ gap: 6px;
461
+ padding: 10px;
462
+ overflow-y: auto;
463
+ overscroll-behavior: contain;
464
+ }
465
+
466
+ .example-card {
467
+ position: relative;
468
+ display: flex;
469
+ align-items: center;
470
+ gap: 10px;
471
+ width: 100%;
472
+ padding: 7px;
473
+ border: 1px solid transparent;
474
+ border-radius: 11px;
475
+ background: transparent;
476
+ color: var(--ink);
477
+ text-align: left;
478
+ cursor: pointer;
479
+ transition: background 0.16s, border-color 0.16s, box-shadow 0.16s;
480
+ }
481
+
482
+ .example-card:hover {
483
+ background: rgba(255, 255, 255, 0.55);
484
+ border-color: var(--line);
485
+ }
486
+
487
+ .example-card.is-active {
488
+ background: #fffdf8;
489
+ border-color: var(--line-strong);
490
+ box-shadow: 0 8px 18px -18px rgba(52, 42, 27, 0.6);
491
+ }
492
+
493
+ .example-card.is-active::before {
494
+ content: "";
495
+ position: absolute;
496
+ left: -1px;
497
+ top: 17px;
498
+ bottom: 17px;
499
+ width: 2px;
500
+ border-radius: 0 2px 2px 0;
501
+ background: var(--accent);
502
+ }
503
+
504
+ .example-thumb {
505
+ width: 58px;
506
+ height: 58px;
507
+ flex: none;
508
+ overflow: hidden;
509
+ border: 1px solid var(--line);
510
+ border-radius: 9px;
511
+ background: #f3eee4;
512
+ }
513
+
514
+ .example-thumb img {
515
+ width: 100%;
516
+ height: 100%;
517
+ display: block;
518
+ object-fit: cover;
519
+ }
520
+
521
+ .example-meta {
522
+ min-width: 0;
523
+ flex: 1;
524
+ display: flex;
525
+ flex-direction: column;
526
+ gap: 2px;
527
+ }
528
+
529
+ .example-title {
530
+ font-size: 13px;
531
+ font-weight: 680;
532
+ letter-spacing: -0.01em;
533
+ line-height: 1.25;
534
+ }
535
+
536
+ .example-sub {
537
+ overflow: hidden;
538
+ color: var(--muted);
539
+ font-size: 9.5px;
540
+ font-weight: 500;
541
+ letter-spacing: 0.01em;
542
+ text-overflow: ellipsis;
543
+ white-space: nowrap;
544
+ }
545
+
546
+ .example-index {
547
+ align-self: flex-start;
548
+ margin-top: 1px;
549
+ color: #b8ae9d;
550
+ font-family: var(--mono);
551
+ font-size: 8.5px;
552
+ letter-spacing: 0.06em;
553
+ }
554
+
555
+ .example-chev {
556
+ width: 14px;
557
+ height: 14px;
558
+ flex: none;
559
+ margin-right: 2px;
560
+ color: var(--line-strong);
561
+ opacity: 0;
562
+ transform: translateX(-3px);
563
+ transition: opacity 0.16s, transform 0.16s, color 0.16s;
564
+ }
565
+
566
+ .example-chev svg {
567
+ width: 14px;
568
+ height: 14px;
569
+ }
570
+
571
+ .example-card:hover .example-chev,
572
+ .example-card.is-active .example-chev {
573
+ opacity: 1;
574
+ transform: translateX(0);
575
+ }
576
+
577
+ .example-card.is-active .example-chev {
578
+ color: var(--accent);
579
+ }
580
+
581
+ .example-skeleton {
582
+ height: 74px;
583
+ flex: none;
584
+ border-radius: 11px;
585
+ background: linear-gradient(100deg, rgba(255, 255, 255, 0.2) 15%, rgba(255, 255, 255, 0.65) 40%, rgba(255, 255, 255, 0.2) 65%);
586
+ background-size: 200% 100%;
587
+ border: 1px solid rgba(222, 215, 200, 0.55);
588
+ animation: shimmer 1.5s linear infinite;
589
+ }
590
+
591
+ .examples-foot {
592
+ flex: none;
593
+ display: flex;
594
+ align-items: center;
595
+ gap: 7px;
596
+ padding: 11px 16px;
597
+ border-top: 1px solid var(--line);
598
+ color: var(--muted);
599
+ font-size: 9.5px;
600
+ letter-spacing: 0.01em;
601
+ }
602
+
603
+ .drop-dot {
604
+ width: 5px;
605
+ height: 5px;
606
+ flex: none;
607
+ border-radius: 50%;
608
+ background: var(--accent);
609
+ }
610
+
611
+ /* ------------------------------------------------------------------ */
612
+ /* editor */
613
+ /* ------------------------------------------------------------------ */
614
+
615
+ .file-meta {
616
+ display: flex;
617
+ align-items: center;
618
+ gap: 8px;
619
+ min-width: 0;
620
+ }
621
+
622
+ .file-dot {
623
+ width: 7px;
624
+ height: 7px;
625
+ flex: none;
626
+ border-radius: 50%;
627
+ background: var(--accent);
628
+ }
629
+
630
+ .file-name {
631
+ overflow: hidden;
632
+ color: var(--ink);
633
+ font-family: var(--mono);
634
+ font-size: 11.5px;
635
+ font-weight: 600;
636
+ text-overflow: ellipsis;
637
+ white-space: nowrap;
638
+ }
639
+
640
+ .state-pill {
641
+ flex: none;
642
+ padding: 2px 7px;
643
+ border: 1px solid var(--line);
644
+ border-radius: 999px;
645
+ background: var(--panel-2);
646
+ color: var(--muted);
647
+ font-size: 9px;
648
+ font-weight: 650;
649
+ letter-spacing: 0.02em;
650
+ }
651
+
652
+ .state-pill[data-state="modified"] {
653
+ border-color: #eac6b7;
654
+ background: var(--accent-soft);
655
+ color: var(--accent-deep);
656
+ }
657
+
658
+ .state-pill[data-state="upload"] {
659
+ border-color: #c8ddd2;
660
+ background: var(--sage-soft);
661
+ color: var(--sage);
662
+ }
663
+
664
+ .state-pill[data-state="error"] {
665
+ border-color: #e8c2b7;
666
+ background: #fbebe6;
667
+ color: var(--danger);
668
+ }
669
+
670
+ .editor-actions {
671
+ display: flex;
672
+ align-items: center;
673
+ gap: 5px;
674
+ }
675
+
676
+ .editor-wrap {
677
+ position: relative;
678
+ flex: 1;
679
+ min-height: 0;
680
+ overflow: hidden;
681
+ background: var(--code-bg);
682
+ }
683
+
684
+ .editor-wrap .CodeMirror {
685
+ height: 100%;
686
+ background: transparent;
687
+ color: var(--ink);
688
+ font-family: var(--mono);
689
+ font-size: 12.5px;
690
+ line-height: 1.68;
691
+ }
692
+
693
+ .CodeMirror-scroll {
694
+ padding-bottom: 26px;
695
+ }
696
+
697
+ .CodeMirror-gutters {
698
+ border-right: 1px solid rgba(222, 215, 200, 0.72);
699
+ background: transparent;
700
+ }
701
+
702
+ .CodeMirror-linenumber {
703
+ padding: 0 9px 0 12px;
704
+ color: #b4aa98;
705
+ font-size: 10px;
706
+ }
707
+
708
+ .CodeMirror-cursor {
709
+ border-left: 1.5px solid var(--accent);
710
+ }
711
+
712
+ .CodeMirror-selected {
713
+ background: rgba(228, 102, 59, 0.13) !important;
714
+ }
715
+
716
+ .CodeMirror-focused .CodeMirror-selected {
717
+ background: rgba(228, 102, 59, 0.17) !important;
718
+ }
719
+
720
+ .CodeMirror-activeline-background {
721
+ background: rgba(228, 102, 59, 0.032);
722
+ }
723
+
724
+ .CodeMirror-matchingbracket {
725
+ color: var(--accent-deep) !important;
726
+ text-decoration: underline;
727
+ }
728
+
729
+ .cm-s-default .cm-keyword {
730
+ color: #b34e2d;
731
+ font-weight: 600;
732
+ }
733
+
734
+ .cm-s-default .cm-atom {
735
+ color: #8b5e2f;
736
+ }
737
+
738
+ .cm-s-default .cm-number {
739
+ color: #a34b2c;
740
+ }
741
+
742
+ .cm-s-default .cm-def {
743
+ color: var(--sage);
744
+ }
745
+
746
+ .cm-s-default .cm-variable {
747
+ color: var(--ink);
748
+ }
749
+
750
+ .cm-s-default .cm-variable-2 {
751
+ color: #47647b;
752
+ }
753
+
754
+ .cm-s-default .cm-property {
755
+ color: var(--sage);
756
+ }
757
+
758
+ .cm-s-default .cm-operator {
759
+ color: #6c6152;
760
+ }
761
+
762
+ .cm-s-default .cm-comment {
763
+ color: #9c9282;
764
+ font-style: italic;
765
+ }
766
+
767
+ .cm-s-default .cm-string,
768
+ .cm-s-default .cm-string-2 {
769
+ color: #8a6a2a;
770
+ }
771
+
772
+ .cm-s-default .cm-builtin {
773
+ color: var(--sage);
774
+ }
775
+
776
+ .cm-s-default .cm-meta {
777
+ color: #8b5e2f;
778
+ }
779
+
780
+ .editor-status {
781
+ height: 31px;
782
+ flex: none;
783
+ display: flex;
784
+ align-items: center;
785
+ justify-content: space-between;
786
+ gap: 10px;
787
+ padding: 0 13px;
788
+ border-top: 1px solid var(--line);
789
+ background: rgba(255, 255, 255, 0.34);
790
+ color: var(--muted);
791
+ font-size: 9.5px;
792
+ letter-spacing: 0.02em;
793
+ }
794
+
795
+ .status-left {
796
+ display: inline-flex;
797
+ align-items: center;
798
+ min-width: 0;
799
+ }
800
+
801
+ .lang-dot {
802
+ width: 6px;
803
+ height: 6px;
804
+ flex: none;
805
+ margin-right: 6px;
806
+ border-radius: 50%;
807
+ background: var(--sage);
808
+ }
809
+
810
+ .status-right {
811
+ font-family: var(--mono);
812
+ font-size: 9px;
813
+ white-space: nowrap;
814
+ }
815
+
816
+ /* ------------------------------------------------------------------ */
817
+ /* preview */
818
+ /* ------------------------------------------------------------------ */
819
+
820
+ .preview-meta {
821
+ display: flex;
822
+ align-items: center;
823
+ gap: 8px;
824
+ min-width: 0;
825
+ }
826
+
827
+ .live-dot {
828
+ width: 7px;
829
+ height: 7px;
830
+ flex: none;
831
+ border-radius: 50%;
832
+ background: #5b947b;
833
+ box-shadow: 0 0 0 3px rgba(91, 148, 123, 0.14);
834
+ }
835
+
836
+ .object-count {
837
+ padding: 2px 7px;
838
+ border: 1px solid var(--line);
839
+ border-radius: 999px;
840
+ color: var(--muted);
841
+ font-size: 9.5px;
842
+ font-weight: 600;
843
+ font-variant-numeric: tabular-nums;
844
+ white-space: nowrap;
845
+ }
846
+
847
+ .view-actions {
848
+ display: flex;
849
+ align-items: center;
850
+ gap: 3px;
851
+ }
852
+
853
+ .canvas-wrap {
854
+ position: relative;
855
+ flex: 1;
856
+ min-height: 0;
857
+ overflow: hidden;
858
+ border-radius: 0 0 var(--radius) var(--radius);
859
+ background:
860
+ radial-gradient(90% 90% at 64% 22%, rgba(255, 255, 255, 0.82) 0%, rgba(255, 255, 255, 0) 58%),
861
+ radial-gradient(100% 100% at 50% 110%, #e6dfd0 0%, #f3eee3 52%, #fbf8f0 100%);
862
+ }
863
+
864
+ .canvas-wrap::after {
865
+ content: "";
866
+ position: absolute;
867
+ inset: 0;
868
+ z-index: 1;
869
+ pointer-events: none;
870
+ border-radius: inherit;
871
+ box-shadow: inset 0 0 90px rgba(79, 63, 39, 0.07);
872
+ }
873
+
874
+ #view {
875
+ display: block;
876
+ width: 100%;
877
+ height: 100%;
878
+ touch-action: none;
879
+ outline: none;
880
+ }
881
+
882
+ .canvas-hint {
883
+ position: absolute;
884
+ left: 50%;
885
+ bottom: 14px;
886
+ z-index: 2;
887
+ display: inline-flex;
888
+ align-items: center;
889
+ gap: 7px;
890
+ padding: 6px 11px;
891
+ border: 1px solid rgba(206, 196, 178, 0.7);
892
+ border-radius: 999px;
893
+ background: rgba(251, 249, 243, 0.64);
894
+ backdrop-filter: blur(7px);
895
+ color: #8c8272;
896
+ font-size: 9.5px;
897
+ letter-spacing: 0.02em;
898
+ pointer-events: none;
899
+ transform: translateX(-50%);
900
+ transition: opacity 0.5s ease, transform 0.5s ease;
901
+ white-space: nowrap;
902
+ }
903
+
904
+ .canvas-hint.is-hidden {
905
+ opacity: 0;
906
+ transform: translateX(-50%) translateY(4px);
907
+ }
908
+
909
+ .mouse-icon {
910
+ position: relative;
911
+ width: 9px;
912
+ height: 13px;
913
+ border: 1.2px solid currentColor;
914
+ border-radius: 5px;
915
+ }
916
+
917
+ .mouse-icon::before {
918
+ content: "";
919
+ position: absolute;
920
+ top: 2px;
921
+ left: 50%;
922
+ width: 1px;
923
+ height: 3px;
924
+ border-radius: 1px;
925
+ background: currentColor;
926
+ transform: translateX(-50%);
927
+ }
928
+
929
+ .canvas-loading,
930
+ .canvas-empty {
931
+ position: absolute;
932
+ inset: 0;
933
+ z-index: 3;
934
+ display: flex;
935
+ align-items: center;
936
+ justify-content: center;
937
+ gap: 12px;
938
+ background: rgba(243, 238, 227, 0.68);
939
+ backdrop-filter: blur(9px);
940
+ text-align: left;
941
+ }
942
+
943
+ .canvas-loading[hidden],
944
+ .canvas-empty[hidden],
945
+ .canvas-error[hidden] {
946
+ display: none;
947
+ }
948
+
949
+ .canvas-loading > div,
950
+ .canvas-empty {
951
+ flex-direction: column;
952
+ }
953
+
954
+ .canvas-loading > div {
955
+ display: flex;
956
+ flex-direction: column;
957
+ gap: 2px;
958
+ }
959
+
960
+ .canvas-loading strong,
961
+ .canvas-empty strong {
962
+ color: var(--ink);
963
+ font-size: 12px;
964
+ font-weight: 680;
965
+ letter-spacing: 0.01em;
966
+ }
967
+
968
+ .canvas-loading span:not(.spinner),
969
+ .canvas-empty span:not(.empty-mark) {
970
+ color: var(--muted);
971
+ font-size: 10px;
972
+ }
973
+
974
+ .canvas-empty code {
975
+ padding: 1px 4px;
976
+ border-radius: 4px;
977
+ background: rgba(255, 255, 255, 0.62);
978
+ font-family: var(--mono);
979
+ font-size: 9.5px;
980
+ color: var(--ink-2);
981
+ }
982
+
983
+ .empty-mark {
984
+ display: grid;
985
+ place-items: center;
986
+ width: 38px;
987
+ height: 38px;
988
+ margin-bottom: 2px;
989
+ border: 1px solid var(--line-strong);
990
+ border-radius: 50%;
991
+ color: var(--line-strong);
992
+ font-family: var(--mono);
993
+ font-size: 17px;
994
+ }
995
+
996
+ .spinner {
997
+ width: 27px;
998
+ height: 27px;
999
+ flex: none;
1000
+ border: 2px solid rgba(228, 102, 59, 0.18);
1001
+ border-top-color: var(--accent);
1002
+ border-radius: 50%;
1003
+ animation: spin 0.9s linear infinite;
1004
+ }
1005
+
1006
+ .canvas-error {
1007
+ position: absolute;
1008
+ inset: 14px;
1009
+ z-index: 4;
1010
+ display: flex;
1011
+ flex-direction: column;
1012
+ min-height: 0;
1013
+ padding: 11px 13px 10px;
1014
+ border: 1px solid #e6c7ba;
1015
+ border-radius: 12px;
1016
+ background: rgba(251, 249, 243, 0.975);
1017
+ box-shadow: 0 26px 60px -36px rgba(74, 45, 26, 0.55);
1018
+ backdrop-filter: blur(8px);
1019
+ }
1020
+
1021
+ .error-head {
1022
+ display: flex;
1023
+ align-items: center;
1024
+ justify-content: space-between;
1025
+ gap: 10px;
1026
+ color: #a33f2a;
1027
+ font-size: 11.5px;
1028
+ font-weight: 680;
1029
+ letter-spacing: 0.01em;
1030
+ }
1031
+
1032
+ .canvas-error pre {
1033
+ flex: 1;
1034
+ min-height: 0;
1035
+ margin: 9px 0;
1036
+ padding: 10px;
1037
+ overflow: auto;
1038
+ border: 1px solid #e8dccb;
1039
+ border-radius: 8px;
1040
+ background: #f6efe4;
1041
+ color: #6e3a2c;
1042
+ font-family: var(--mono);
1043
+ font-size: 10px;
1044
+ line-height: 1.55;
1045
+ white-space: pre-wrap;
1046
+ word-break: break-word;
1047
+ }
1048
+
1049
+ .canvas-error p {
1050
+ margin: 0;
1051
+ color: var(--muted);
1052
+ font-size: 9.5px;
1053
+ }
1054
+
1055
+ .toast {
1056
+ position: absolute;
1057
+ left: 50%;
1058
+ bottom: 16px;
1059
+ z-index: 6;
1060
+ max-width: calc(100% - 32px);
1061
+ padding: 7px 14px;
1062
+ border-radius: 999px;
1063
+ background: #2c2821;
1064
+ color: #fbf8f1;
1065
+ font-size: 10.5px;
1066
+ letter-spacing: 0.01em;
1067
+ text-align: center;
1068
+ box-shadow: 0 12px 30px -18px rgba(24, 19, 12, 0.8);
1069
+ transform: translateX(-50%);
1070
+ animation: toast-in 0.22s ease both;
1071
+ }
1072
+
1073
+ .toast[data-tone="error"] {
1074
+ background: #8e3a27;
1075
+ }
1076
+
1077
+ /* ------------------------------------------------------------------ */
1078
+ /* drop overlay */
1079
+ /* ------------------------------------------------------------------ */
1080
+
1081
+ .drop-overlay {
1082
+ position: fixed;
1083
+ inset: 0;
1084
+ z-index: 50;
1085
+ display: grid;
1086
+ place-items: center;
1087
+ background: rgba(241, 237, 228, 0.76);
1088
+ backdrop-filter: blur(7px);
1089
+ }
1090
+
1091
+ .drop-overlay[hidden] {
1092
+ display: none;
1093
+ }
1094
+
1095
+ .drop-card {
1096
+ width: min(420px, 80vw);
1097
+ padding: 36px 30px;
1098
+ display: flex;
1099
+ flex-direction: column;
1100
+ align-items: center;
1101
+ gap: 7px;
1102
+ border: 1.5px dashed var(--accent);
1103
+ border-radius: 18px;
1104
+ background: #fbf8f0;
1105
+ box-shadow: 0 30px 60px -40px rgba(74, 45, 26, 0.5);
1106
+ text-align: center;
1107
+ }
1108
+
1109
+ .drop-card svg {
1110
+ width: 33px;
1111
+ height: 33px;
1112
+ margin-bottom: 4px;
1113
+ color: var(--accent);
1114
+ }
1115
+
1116
+ .drop-card strong {
1117
+ font-size: 14px;
1118
+ font-weight: 680;
1119
+ }
1120
+
1121
+ .drop-card span {
1122
+ color: var(--muted);
1123
+ font-size: 10.5px;
1124
+ }
1125
+
1126
+ /* ------------------------------------------------------------------ */
1127
+ /* motion */
1128
+ /* ------------------------------------------------------------------ */
1129
+
1130
+ @keyframes spin {
1131
+ to {
1132
+ transform: rotate(360deg);
1133
+ }
1134
+ }
1135
+
1136
+ @keyframes pulse {
1137
+ 0%,
1138
+ 100% {
1139
+ opacity: 1;
1140
+ transform: scale(1);
1141
+ }
1142
+ 50% {
1143
+ opacity: 0.55;
1144
+ transform: scale(0.82);
1145
+ }
1146
+ }
1147
+
1148
+ @keyframes shimmer {
1149
+ from {
1150
+ background-position: 150% 0;
1151
+ }
1152
+ to {
1153
+ background-position: -50% 0;
1154
+ }
1155
+ }
1156
+
1157
+ @keyframes toast-in {
1158
+ from {
1159
+ opacity: 0;
1160
+ transform: translateX(-50%) translateY(5px);
1161
+ }
1162
+ to {
1163
+ opacity: 1;
1164
+ transform: translateX(-50%) translateY(0);
1165
+ }
1166
+ }
1167
+
1168
+ @media (prefers-reduced-motion: reduce) {
1169
+ *,
1170
+ *::before,
1171
+ *::after {
1172
+ animation-duration: 0.001ms !important;
1173
+ animation-iteration-count: 1 !important;
1174
+ transition-duration: 0.001ms !important;
1175
+ }
1176
+ }
1177
+
1178
+ /* ------------------------------------------------------------------ */
1179
+ /* responsive */
1180
+ /* ------------------------------------------------------------------ */
1181
+
1182
+ @media (max-width: 1280px) {
1183
+ .workspace {
1184
+ grid-template-columns: 222px minmax(310px, 0.9fr) minmax(350px, 1.1fr);
1185
+ gap: 11px;
1186
+ padding: 12px 14px 14px;
1187
+ }
1188
+
1189
+ .brand-sub {
1190
+ display: none;
1191
+ }
1192
+ }
1193
+
1194
+ @media (max-width: 1020px) {
1195
+ .workspace {
1196
+ grid-template-columns: 206px minmax(0, 0.92fr) minmax(0, 1.08fr);
1197
+ gap: 9px;
1198
+ padding: 10px 12px 12px;
1199
+ }
1200
+
1201
+ .example-thumb {
1202
+ width: 50px;
1203
+ height: 50px;
1204
+ }
1205
+
1206
+ .example-card {
1207
+ gap: 8px;
1208
+ padding: 6px;
1209
+ }
1210
+
1211
+ .example-sub {
1212
+ display: none;
1213
+ }
1214
+
1215
+ .runtime-text {
1216
+ display: none;
1217
+ }
1218
+
1219
+ .runtime {
1220
+ width: 30px;
1221
+ padding: 0;
1222
+ justify-content: center;
1223
+ }
1224
+
1225
+ .btn.run kbd {
1226
+ display: none;
1227
+ }
1228
+ }
1229
+
1230
+ @media (max-width: 900px) {
1231
+ body {
1232
+ overflow: auto;
1233
+ }
1234
+
1235
+ .topbar {
1236
+ height: 58px;
1237
+ padding: 0 14px;
1238
+ }
1239
+
1240
+ .workspace {
1241
+ height: auto;
1242
+ display: flex;
1243
+ flex-direction: column;
1244
+ gap: 10px;
1245
+ padding: 10px 12px 16px;
1246
+ }
1247
+
1248
+ .examples {
1249
+ flex: none;
1250
+ order: 1;
1251
+ }
1252
+
1253
+ .preview-panel {
1254
+ order: 2;
1255
+ flex: none;
1256
+ height: 52vh;
1257
+ min-height: 320px;
1258
+ }
1259
+
1260
+ .editor-panel {
1261
+ order: 3;
1262
+ flex: none;
1263
+ height: 58vh;
1264
+ min-height: 390px;
1265
+ }
1266
+
1267
+ .examples .panel-head {
1268
+ height: 42px;
1269
+ }
1270
+
1271
+ .example-list {
1272
+ flex-direction: row;
1273
+ overflow-x: auto;
1274
+ overflow-y: hidden;
1275
+ padding: 8px;
1276
+ gap: 6px;
1277
+ }
1278
+
1279
+ .example-card {
1280
+ width: 196px;
1281
+ flex: 0 0 196px;
1282
+ padding: 6px;
1283
+ }
1284
+
1285
+ .example-card.is-active::before {
1286
+ top: auto;
1287
+ left: 16px;
1288
+ right: 16px;
1289
+ bottom: -1px;
1290
+ width: auto;
1291
+ height: 2px;
1292
+ border-radius: 2px 2px 0 0;
1293
+ }
1294
+
1295
+ .example-thumb {
1296
+ width: 44px;
1297
+ height: 44px;
1298
+ }
1299
+
1300
+ .example-chev {
1301
+ display: none;
1302
+ }
1303
+
1304
+ .examples-foot {
1305
+ display: none;
1306
+ }
1307
+
1308
+ .panel-head {
1309
+ height: 46px;
1310
+ }
1311
+
1312
+ .canvas-hint {
1313
+ display: none;
1314
+ }
1315
+ }
1316
+
1317
+ @media (max-width: 560px) {
1318
+ .topbar {
1319
+ gap: 8px;
1320
+ padding: 0 10px;
1321
+ }
1322
+
1323
+ .brand {
1324
+ gap: 8px;
1325
+ }
1326
+
1327
+ .brand-mark {
1328
+ width: 29px;
1329
+ height: 29px;
1330
+ border-radius: 8px;
1331
+ }
1332
+
1333
+ .brand-name {
1334
+ font-size: 17px;
1335
+ }
1336
+
1337
+ .topbar-actions {
1338
+ gap: 6px;
1339
+ }
1340
+
1341
+ .btn.ghost {
1342
+ height: 32px;
1343
+ padding: 0 10px;
1344
+ font-size: 11px;
1345
+ }
1346
+
1347
+ .runtime {
1348
+ display: none;
1349
+ }
1350
+
1351
+ .workspace {
1352
+ padding: 8px 8px 14px;
1353
+ gap: 8px;
1354
+ }
1355
+
1356
+ .preview-panel {
1357
+ height: 46vh;
1358
+ min-height: 280px;
1359
+ }
1360
+
1361
+ .editor-panel {
1362
+ height: 60vh;
1363
+ min-height: 380px;
1364
+ }
1365
+
1366
+ .file-name {
1367
+ max-width: 118px;
1368
+ }
1369
+
1370
+ .state-pill {
1371
+ display: none;
1372
+ }
1373
+
1374
+ .editor-actions .icon-btn {
1375
+ width: 29px;
1376
+ height: 29px;
1377
+ }
1378
+
1379
+ .btn.run {
1380
+ padding: 0 10px;
1381
+ }
1382
+
1383
+ .example-card {
1384
+ width: 166px;
1385
+ flex-basis: 166px;
1386
+ }
1387
+
1388
+ .example-title {
1389
+ font-size: 12px;
1390
+ }
1391
+
1392
+ .example-thumb {
1393
+ width: 40px;
1394
+ height: 40px;
1395
+ }
1396
+
1397
+ .object-count {
1398
+ display: none;
1399
+ }
1400
+
1401
+ .canvas-error {
1402
+ inset: 9px;
1403
+ }
1404
  }
thumbs/computer.png ADDED
thumbs/gear.png ADDED
thumbs/mug.png ADDED
thumbs/rocket.png ADDED
vendor/codemirror/active-line.js ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
2
+ // Distributed under an MIT license: https://codemirror.net/5/LICENSE
3
+
4
+ (function(mod) {
5
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
6
+ mod(require("../../lib/codemirror"));
7
+ else if (typeof define == "function" && define.amd) // AMD
8
+ define(["../../lib/codemirror"], mod);
9
+ else // Plain browser env
10
+ mod(CodeMirror);
11
+ })(function(CodeMirror) {
12
+ "use strict";
13
+ var WRAP_CLASS = "CodeMirror-activeline";
14
+ var BACK_CLASS = "CodeMirror-activeline-background";
15
+ var GUTT_CLASS = "CodeMirror-activeline-gutter";
16
+
17
+ CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
18
+ var prev = old == CodeMirror.Init ? false : old;
19
+ if (val == prev) return
20
+ if (prev) {
21
+ cm.off("beforeSelectionChange", selectionChange);
22
+ clearActiveLines(cm);
23
+ delete cm.state.activeLines;
24
+ }
25
+ if (val) {
26
+ cm.state.activeLines = [];
27
+ updateActiveLines(cm, cm.listSelections());
28
+ cm.on("beforeSelectionChange", selectionChange);
29
+ }
30
+ });
31
+
32
+ function clearActiveLines(cm) {
33
+ for (var i = 0; i < cm.state.activeLines.length; i++) {
34
+ cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS);
35
+ cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS);
36
+ cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS);
37
+ }
38
+ }
39
+
40
+ function sameArray(a, b) {
41
+ if (a.length != b.length) return false;
42
+ for (var i = 0; i < a.length; i++)
43
+ if (a[i] != b[i]) return false;
44
+ return true;
45
+ }
46
+
47
+ function updateActiveLines(cm, ranges) {
48
+ var active = [];
49
+ for (var i = 0; i < ranges.length; i++) {
50
+ var range = ranges[i];
51
+ var option = cm.getOption("styleActiveLine");
52
+ if (typeof option == "object" && option.nonEmpty ? range.anchor.line != range.head.line : !range.empty())
53
+ continue
54
+ var line = cm.getLineHandleVisualStart(range.head.line);
55
+ if (active[active.length - 1] != line) active.push(line);
56
+ }
57
+ if (sameArray(cm.state.activeLines, active)) return;
58
+ cm.operation(function() {
59
+ clearActiveLines(cm);
60
+ for (var i = 0; i < active.length; i++) {
61
+ cm.addLineClass(active[i], "wrap", WRAP_CLASS);
62
+ cm.addLineClass(active[i], "background", BACK_CLASS);
63
+ cm.addLineClass(active[i], "gutter", GUTT_CLASS);
64
+ }
65
+ cm.state.activeLines = active;
66
+ });
67
+ }
68
+
69
+ function selectionChange(cm, sel) {
70
+ updateActiveLines(cm, sel.ranges);
71
+ }
72
+ });
vendor/codemirror/closebrackets.js ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
2
+ // Distributed under an MIT license: https://codemirror.net/5/LICENSE
3
+
4
+ (function(mod) {
5
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
6
+ mod(require("../../lib/codemirror"));
7
+ else if (typeof define == "function" && define.amd) // AMD
8
+ define(["../../lib/codemirror"], mod);
9
+ else // Plain browser env
10
+ mod(CodeMirror);
11
+ })(function(CodeMirror) {
12
+ var defaults = {
13
+ pairs: "()[]{}''\"\"",
14
+ closeBefore: ")]}'\":;>",
15
+ triples: "",
16
+ explode: "[]{}"
17
+ };
18
+
19
+ var Pos = CodeMirror.Pos;
20
+
21
+ CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
22
+ if (old && old != CodeMirror.Init) {
23
+ cm.removeKeyMap(keyMap);
24
+ cm.state.closeBrackets = null;
25
+ }
26
+ if (val) {
27
+ ensureBound(getOption(val, "pairs"))
28
+ cm.state.closeBrackets = val;
29
+ cm.addKeyMap(keyMap);
30
+ }
31
+ });
32
+
33
+ function getOption(conf, name) {
34
+ if (name == "pairs" && typeof conf == "string") return conf;
35
+ if (typeof conf == "object" && conf[name] != null) return conf[name];
36
+ return defaults[name];
37
+ }
38
+
39
+ var keyMap = {Backspace: handleBackspace, Enter: handleEnter};
40
+ function ensureBound(chars) {
41
+ for (var i = 0; i < chars.length; i++) {
42
+ var ch = chars.charAt(i), key = "'" + ch + "'"
43
+ if (!keyMap[key]) keyMap[key] = handler(ch)
44
+ }
45
+ }
46
+ ensureBound(defaults.pairs + "`")
47
+
48
+ function handler(ch) {
49
+ return function(cm) { return handleChar(cm, ch); };
50
+ }
51
+
52
+ function getConfig(cm) {
53
+ var deflt = cm.state.closeBrackets;
54
+ if (!deflt || deflt.override) return deflt;
55
+ var mode = cm.getModeAt(cm.getCursor());
56
+ return mode.closeBrackets || deflt;
57
+ }
58
+
59
+ function handleBackspace(cm) {
60
+ var conf = getConfig(cm);
61
+ if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
62
+
63
+ var pairs = getOption(conf, "pairs");
64
+ var ranges = cm.listSelections();
65
+ for (var i = 0; i < ranges.length; i++) {
66
+ if (!ranges[i].empty()) return CodeMirror.Pass;
67
+ var around = charsAround(cm, ranges[i].head);
68
+ if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
69
+ }
70
+ for (var i = ranges.length - 1; i >= 0; i--) {
71
+ var cur = ranges[i].head;
72
+ cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
73
+ }
74
+ }
75
+
76
+ function handleEnter(cm) {
77
+ var conf = getConfig(cm);
78
+ var explode = conf && getOption(conf, "explode");
79
+ if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass;
80
+
81
+ var ranges = cm.listSelections();
82
+ for (var i = 0; i < ranges.length; i++) {
83
+ if (!ranges[i].empty()) return CodeMirror.Pass;
84
+ var around = charsAround(cm, ranges[i].head);
85
+ if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
86
+ }
87
+ cm.operation(function() {
88
+ var linesep = cm.lineSeparator() || "\n";
89
+ cm.replaceSelection(linesep + linesep, null);
90
+ moveSel(cm, -1)
91
+ ranges = cm.listSelections();
92
+ for (var i = 0; i < ranges.length; i++) {
93
+ var line = ranges[i].head.line;
94
+ cm.indentLine(line, null, true);
95
+ cm.indentLine(line + 1, null, true);
96
+ }
97
+ });
98
+ }
99
+
100
+ function moveSel(cm, dir) {
101
+ var newRanges = [], ranges = cm.listSelections(), primary = 0
102
+ for (var i = 0; i < ranges.length; i++) {
103
+ var range = ranges[i]
104
+ if (range.head == cm.getCursor()) primary = i
105
+ var pos = range.head.ch || dir > 0 ? {line: range.head.line, ch: range.head.ch + dir} : {line: range.head.line - 1}
106
+ newRanges.push({anchor: pos, head: pos})
107
+ }
108
+ cm.setSelections(newRanges, primary)
109
+ }
110
+
111
+ function contractSelection(sel) {
112
+ var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
113
+ return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
114
+ head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))};
115
+ }
116
+
117
+ function handleChar(cm, ch) {
118
+ var conf = getConfig(cm);
119
+ if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
120
+
121
+ var pairs = getOption(conf, "pairs");
122
+ var pos = pairs.indexOf(ch);
123
+ if (pos == -1) return CodeMirror.Pass;
124
+
125
+ var closeBefore = getOption(conf,"closeBefore");
126
+
127
+ var triples = getOption(conf, "triples");
128
+
129
+ var identical = pairs.charAt(pos + 1) == ch;
130
+ var ranges = cm.listSelections();
131
+ var opening = pos % 2 == 0;
132
+
133
+ var type;
134
+ for (var i = 0; i < ranges.length; i++) {
135
+ var range = ranges[i], cur = range.head, curType;
136
+ var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
137
+ if (opening && !range.empty()) {
138
+ curType = "surround";
139
+ } else if ((identical || !opening) && next == ch) {
140
+ if (identical && stringStartsAfter(cm, cur))
141
+ curType = "both";
142
+ else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
143
+ curType = "skipThree";
144
+ else
145
+ curType = "skip";
146
+ } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 &&
147
+ cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) {
148
+ if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass;
149
+ curType = "addFour";
150
+ } else if (identical) {
151
+ var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur)
152
+ if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both";
153
+ else return CodeMirror.Pass;
154
+ } else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) {
155
+ curType = "both";
156
+ } else {
157
+ return CodeMirror.Pass;
158
+ }
159
+ if (!type) type = curType;
160
+ else if (type != curType) return CodeMirror.Pass;
161
+ }
162
+
163
+ var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
164
+ var right = pos % 2 ? ch : pairs.charAt(pos + 1);
165
+ cm.operation(function() {
166
+ if (type == "skip") {
167
+ moveSel(cm, 1)
168
+ } else if (type == "skipThree") {
169
+ moveSel(cm, 3)
170
+ } else if (type == "surround") {
171
+ var sels = cm.getSelections();
172
+ for (var i = 0; i < sels.length; i++)
173
+ sels[i] = left + sels[i] + right;
174
+ cm.replaceSelections(sels, "around");
175
+ sels = cm.listSelections().slice();
176
+ for (var i = 0; i < sels.length; i++)
177
+ sels[i] = contractSelection(sels[i]);
178
+ cm.setSelections(sels);
179
+ } else if (type == "both") {
180
+ cm.replaceSelection(left + right, null);
181
+ cm.triggerElectric(left + right);
182
+ moveSel(cm, -1)
183
+ } else if (type == "addFour") {
184
+ cm.replaceSelection(left + left + left + left, "before");
185
+ moveSel(cm, 1)
186
+ }
187
+ });
188
+ }
189
+
190
+ function charsAround(cm, pos) {
191
+ var str = cm.getRange(Pos(pos.line, pos.ch - 1),
192
+ Pos(pos.line, pos.ch + 1));
193
+ return str.length == 2 ? str : null;
194
+ }
195
+
196
+ function stringStartsAfter(cm, pos) {
197
+ var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1))
198
+ return /\bstring/.test(token.type) && token.start == pos.ch &&
199
+ (pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos)))
200
+ }
201
+ });
vendor/codemirror/codemirror.css ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* BASICS */
2
+
3
+ .CodeMirror {
4
+ /* Set height, width, borders, and global font properties here */
5
+ font-family: monospace;
6
+ height: 300px;
7
+ color: black;
8
+ direction: ltr;
9
+ }
10
+
11
+ /* PADDING */
12
+
13
+ .CodeMirror-lines {
14
+ padding: 4px 0; /* Vertical padding around content */
15
+ }
16
+ .CodeMirror pre.CodeMirror-line,
17
+ .CodeMirror pre.CodeMirror-line-like {
18
+ padding: 0 4px; /* Horizontal padding of content */
19
+ }
20
+
21
+ .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
22
+ background-color: white; /* The little square between H and V scrollbars */
23
+ }
24
+
25
+ /* GUTTER */
26
+
27
+ .CodeMirror-gutters {
28
+ border-right: 1px solid #ddd;
29
+ background-color: #f7f7f7;
30
+ white-space: nowrap;
31
+ }
32
+ .CodeMirror-linenumbers {}
33
+ .CodeMirror-linenumber {
34
+ padding: 0 3px 0 5px;
35
+ min-width: 20px;
36
+ text-align: right;
37
+ color: #999;
38
+ white-space: nowrap;
39
+ }
40
+
41
+ .CodeMirror-guttermarker { color: black; }
42
+ .CodeMirror-guttermarker-subtle { color: #999; }
43
+
44
+ /* CURSOR */
45
+
46
+ .CodeMirror-cursor {
47
+ border-left: 1px solid black;
48
+ border-right: none;
49
+ width: 0;
50
+ }
51
+ /* Shown when moving in bi-directional text */
52
+ .CodeMirror div.CodeMirror-secondarycursor {
53
+ border-left: 1px solid silver;
54
+ }
55
+ .cm-fat-cursor .CodeMirror-cursor {
56
+ width: auto;
57
+ border: 0 !important;
58
+ background: #7e7;
59
+ }
60
+ .cm-fat-cursor div.CodeMirror-cursors {
61
+ z-index: 1;
62
+ }
63
+ .cm-fat-cursor .CodeMirror-line::selection,
64
+ .cm-fat-cursor .CodeMirror-line > span::selection,
65
+ .cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; }
66
+ .cm-fat-cursor .CodeMirror-line::-moz-selection,
67
+ .cm-fat-cursor .CodeMirror-line > span::-moz-selection,
68
+ .cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; }
69
+ .cm-fat-cursor { caret-color: transparent; }
70
+ @-moz-keyframes blink {
71
+ 0% {}
72
+ 50% { background-color: transparent; }
73
+ 100% {}
74
+ }
75
+ @-webkit-keyframes blink {
76
+ 0% {}
77
+ 50% { background-color: transparent; }
78
+ 100% {}
79
+ }
80
+ @keyframes blink {
81
+ 0% {}
82
+ 50% { background-color: transparent; }
83
+ 100% {}
84
+ }
85
+
86
+ /* Can style cursor different in overwrite (non-insert) mode */
87
+ .CodeMirror-overwrite .CodeMirror-cursor {}
88
+
89
+ .cm-tab { display: inline-block; text-decoration: inherit; }
90
+
91
+ .CodeMirror-rulers {
92
+ position: absolute;
93
+ left: 0; right: 0; top: -50px; bottom: 0;
94
+ overflow: hidden;
95
+ }
96
+ .CodeMirror-ruler {
97
+ border-left: 1px solid #ccc;
98
+ top: 0; bottom: 0;
99
+ position: absolute;
100
+ }
101
+
102
+ /* DEFAULT THEME */
103
+
104
+ .cm-s-default .cm-header {color: blue;}
105
+ .cm-s-default .cm-quote {color: #090;}
106
+ .cm-negative {color: #d44;}
107
+ .cm-positive {color: #292;}
108
+ .cm-header, .cm-strong {font-weight: bold;}
109
+ .cm-em {font-style: italic;}
110
+ .cm-link {text-decoration: underline;}
111
+ .cm-strikethrough {text-decoration: line-through;}
112
+
113
+ .cm-s-default .cm-keyword {color: #708;}
114
+ .cm-s-default .cm-atom {color: #219;}
115
+ .cm-s-default .cm-number {color: #164;}
116
+ .cm-s-default .cm-def {color: #00f;}
117
+ .cm-s-default .cm-variable,
118
+ .cm-s-default .cm-punctuation,
119
+ .cm-s-default .cm-property,
120
+ .cm-s-default .cm-operator {}
121
+ .cm-s-default .cm-variable-2 {color: #05a;}
122
+ .cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
123
+ .cm-s-default .cm-comment {color: #a50;}
124
+ .cm-s-default .cm-string {color: #a11;}
125
+ .cm-s-default .cm-string-2 {color: #f50;}
126
+ .cm-s-default .cm-meta {color: #555;}
127
+ .cm-s-default .cm-qualifier {color: #555;}
128
+ .cm-s-default .cm-builtin {color: #30a;}
129
+ .cm-s-default .cm-bracket {color: #997;}
130
+ .cm-s-default .cm-tag {color: #170;}
131
+ .cm-s-default .cm-attribute {color: #00c;}
132
+ .cm-s-default .cm-hr {color: #999;}
133
+ .cm-s-default .cm-link {color: #00c;}
134
+
135
+ .cm-s-default .cm-error {color: #f00;}
136
+ .cm-invalidchar {color: #f00;}
137
+
138
+ .CodeMirror-composing { border-bottom: 2px solid; }
139
+
140
+ /* Default styles for common addons */
141
+
142
+ div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
143
+ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
144
+ .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
145
+ .CodeMirror-activeline-background {background: #e8f2ff;}
146
+
147
+ /* STOP */
148
+
149
+ /* The rest of this file contains styles related to the mechanics of
150
+ the editor. You probably shouldn't touch them. */
151
+
152
+ .CodeMirror {
153
+ position: relative;
154
+ overflow: hidden;
155
+ background: white;
156
+ }
157
+
158
+ .CodeMirror-scroll {
159
+ overflow: scroll !important; /* Things will break if this is overridden */
160
+ /* 50px is the magic margin used to hide the element's real scrollbars */
161
+ /* See overflow: hidden in .CodeMirror */
162
+ margin-bottom: -50px; margin-right: -50px;
163
+ padding-bottom: 50px;
164
+ height: 100%;
165
+ outline: none; /* Prevent dragging from highlighting the element */
166
+ position: relative;
167
+ z-index: 0;
168
+ }
169
+ .CodeMirror-sizer {
170
+ position: relative;
171
+ border-right: 50px solid transparent;
172
+ }
173
+
174
+ /* The fake, visible scrollbars. Used to force redraw during scrolling
175
+ before actual scrolling happens, thus preventing shaking and
176
+ flickering artifacts. */
177
+ .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
178
+ position: absolute;
179
+ z-index: 6;
180
+ display: none;
181
+ outline: none;
182
+ }
183
+ .CodeMirror-vscrollbar {
184
+ right: 0; top: 0;
185
+ overflow-x: hidden;
186
+ overflow-y: scroll;
187
+ }
188
+ .CodeMirror-hscrollbar {
189
+ bottom: 0; left: 0;
190
+ overflow-y: hidden;
191
+ overflow-x: scroll;
192
+ }
193
+ .CodeMirror-scrollbar-filler {
194
+ right: 0; bottom: 0;
195
+ }
196
+ .CodeMirror-gutter-filler {
197
+ left: 0; bottom: 0;
198
+ }
199
+
200
+ .CodeMirror-gutters {
201
+ position: absolute; left: 0; top: 0;
202
+ min-height: 100%;
203
+ z-index: 3;
204
+ }
205
+ .CodeMirror-gutter {
206
+ white-space: normal;
207
+ height: 100%;
208
+ display: inline-block;
209
+ vertical-align: top;
210
+ margin-bottom: -50px;
211
+ }
212
+ .CodeMirror-gutter-wrapper {
213
+ position: absolute;
214
+ z-index: 4;
215
+ background: none !important;
216
+ border: none !important;
217
+ }
218
+ .CodeMirror-gutter-background {
219
+ position: absolute;
220
+ top: 0; bottom: 0;
221
+ z-index: 4;
222
+ }
223
+ .CodeMirror-gutter-elt {
224
+ position: absolute;
225
+ cursor: default;
226
+ z-index: 4;
227
+ }
228
+ .CodeMirror-gutter-wrapper ::selection { background-color: transparent }
229
+ .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
230
+
231
+ .CodeMirror-lines {
232
+ cursor: text;
233
+ min-height: 1px; /* prevents collapsing before first draw */
234
+ }
235
+ .CodeMirror pre.CodeMirror-line,
236
+ .CodeMirror pre.CodeMirror-line-like {
237
+ /* Reset some styles that the rest of the page might have set */
238
+ -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
239
+ border-width: 0;
240
+ background: transparent;
241
+ font-family: inherit;
242
+ font-size: inherit;
243
+ margin: 0;
244
+ white-space: pre;
245
+ word-wrap: normal;
246
+ line-height: inherit;
247
+ color: inherit;
248
+ z-index: 2;
249
+ position: relative;
250
+ overflow: visible;
251
+ -webkit-tap-highlight-color: transparent;
252
+ -webkit-font-variant-ligatures: contextual;
253
+ font-variant-ligatures: contextual;
254
+ }
255
+ .CodeMirror-wrap pre.CodeMirror-line,
256
+ .CodeMirror-wrap pre.CodeMirror-line-like {
257
+ word-wrap: break-word;
258
+ white-space: pre-wrap;
259
+ word-break: normal;
260
+ }
261
+
262
+ .CodeMirror-linebackground {
263
+ position: absolute;
264
+ left: 0; right: 0; top: 0; bottom: 0;
265
+ z-index: 0;
266
+ }
267
+
268
+ .CodeMirror-linewidget {
269
+ position: relative;
270
+ z-index: 2;
271
+ padding: 0.1px; /* Force widget margins to stay inside of the container */
272
+ }
273
+
274
+ .CodeMirror-widget {}
275
+
276
+ .CodeMirror-rtl pre { direction: rtl; }
277
+
278
+ .CodeMirror-code {
279
+ outline: none;
280
+ }
281
+
282
+ /* Force content-box sizing for the elements where we expect it */
283
+ .CodeMirror-scroll,
284
+ .CodeMirror-sizer,
285
+ .CodeMirror-gutter,
286
+ .CodeMirror-gutters,
287
+ .CodeMirror-linenumber {
288
+ -moz-box-sizing: content-box;
289
+ box-sizing: content-box;
290
+ }
291
+
292
+ .CodeMirror-measure {
293
+ position: absolute;
294
+ width: 100%;
295
+ height: 0;
296
+ overflow: hidden;
297
+ visibility: hidden;
298
+ }
299
+
300
+ .CodeMirror-cursor {
301
+ position: absolute;
302
+ pointer-events: none;
303
+ }
304
+ .CodeMirror-measure pre { position: static; }
305
+
306
+ div.CodeMirror-cursors {
307
+ visibility: hidden;
308
+ position: relative;
309
+ z-index: 3;
310
+ }
311
+ div.CodeMirror-dragcursors {
312
+ visibility: visible;
313
+ }
314
+
315
+ .CodeMirror-focused div.CodeMirror-cursors {
316
+ visibility: visible;
317
+ }
318
+
319
+ .CodeMirror-selected { background: #d9d9d9; }
320
+ .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
321
+ .CodeMirror-crosshair { cursor: crosshair; }
322
+ .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
323
+ .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
324
+
325
+ .cm-searching {
326
+ background-color: #ffa;
327
+ background-color: rgba(255, 255, 0, .4);
328
+ }
329
+
330
+ /* Used to force a border model for a node */
331
+ .cm-force-border { padding-right: .1px; }
332
+
333
+ @media print {
334
+ /* Hide the cursor when printing */
335
+ .CodeMirror div.CodeMirror-cursors {
336
+ visibility: hidden;
337
+ }
338
+ }
339
+
340
+ /* See issue #2901 */
341
+ .cm-tab-wrap-hack:after { content: ''; }
342
+
343
+ /* Help users use markselection to safely style text background */
344
+ span.CodeMirror-selectedtext { background: none; }
vendor/codemirror/codemirror.js ADDED
The diff for this file is too large to render. See raw diff
 
vendor/codemirror/matchbrackets.js ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
2
+ // Distributed under an MIT license: https://codemirror.net/5/LICENSE
3
+
4
+ (function(mod) {
5
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
6
+ mod(require("../../lib/codemirror"));
7
+ else if (typeof define == "function" && define.amd) // AMD
8
+ define(["../../lib/codemirror"], mod);
9
+ else // Plain browser env
10
+ mod(CodeMirror);
11
+ })(function(CodeMirror) {
12
+ var ie_lt8 = /MSIE \d/.test(navigator.userAgent) &&
13
+ (document.documentMode == null || document.documentMode < 8);
14
+
15
+ var Pos = CodeMirror.Pos;
16
+
17
+ var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<", "<": ">>", ">": "<<"};
18
+
19
+ function bracketRegex(config) {
20
+ return config && config.bracketRegex || /[(){}[\]]/
21
+ }
22
+
23
+ function findMatchingBracket(cm, where, config) {
24
+ var line = cm.getLineHandle(where.line), pos = where.ch - 1;
25
+ var afterCursor = config && config.afterCursor
26
+ if (afterCursor == null)
27
+ afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className)
28
+ var re = bracketRegex(config)
29
+
30
+ // A cursor is defined as between two characters, but in vim command mode
31
+ // (i.e. not insert mode), the cursor is visually represented as a
32
+ // highlighted box on top of the 2nd character. Otherwise, we allow matches
33
+ // from before or after the cursor.
34
+ var match = (!afterCursor && pos >= 0 && re.test(line.text.charAt(pos)) && matching[line.text.charAt(pos)]) ||
35
+ re.test(line.text.charAt(pos + 1)) && matching[line.text.charAt(++pos)];
36
+ if (!match) return null;
37
+ var dir = match.charAt(1) == ">" ? 1 : -1;
38
+ if (config && config.strict && (dir > 0) != (pos == where.ch)) return null;
39
+ var style = cm.getTokenTypeAt(Pos(where.line, pos + 1));
40
+
41
+ var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style, config);
42
+ if (found == null) return null;
43
+ return {from: Pos(where.line, pos), to: found && found.pos,
44
+ match: found && found.ch == match.charAt(0), forward: dir > 0};
45
+ }
46
+
47
+ // bracketRegex is used to specify which type of bracket to scan
48
+ // should be a regexp, e.g. /[[\]]/
49
+ //
50
+ // Note: If "where" is on an open bracket, then this bracket is ignored.
51
+ //
52
+ // Returns false when no bracket was found, null when it reached
53
+ // maxScanLines and gave up
54
+ function scanForBracket(cm, where, dir, style, config) {
55
+ var maxScanLen = (config && config.maxScanLineLength) || 10000;
56
+ var maxScanLines = (config && config.maxScanLines) || 1000;
57
+
58
+ var stack = [];
59
+ var re = bracketRegex(config)
60
+ var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1)
61
+ : Math.max(cm.firstLine() - 1, where.line - maxScanLines);
62
+ for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) {
63
+ var line = cm.getLine(lineNo);
64
+ if (!line) continue;
65
+ var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1;
66
+ if (line.length > maxScanLen) continue;
67
+ if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0);
68
+ for (; pos != end; pos += dir) {
69
+ var ch = line.charAt(pos);
70
+ if (re.test(ch) && (style === undefined ||
71
+ (cm.getTokenTypeAt(Pos(lineNo, pos + 1)) || "") == (style || ""))) {
72
+ var match = matching[ch];
73
+ if (match && (match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
74
+ else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch};
75
+ else stack.pop();
76
+ }
77
+ }
78
+ }
79
+ return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null;
80
+ }
81
+
82
+ function matchBrackets(cm, autoclear, config) {
83
+ // Disable brace matching in long lines, since it'll cause hugely slow updates
84
+ var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000,
85
+ highlightNonMatching = config && config.highlightNonMatching;
86
+ var marks = [], ranges = cm.listSelections();
87
+ for (var i = 0; i < ranges.length; i++) {
88
+ var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config);
89
+ if (match && (match.match || highlightNonMatching !== false) && cm.getLine(match.from.line).length <= maxHighlightLen) {
90
+ var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
91
+ marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style}));
92
+ if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
93
+ marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style}));
94
+ }
95
+ }
96
+
97
+ if (marks.length) {
98
+ // Kludge to work around the IE bug from issue #1193, where text
99
+ // input stops going to the textarea whenever this fires.
100
+ if (ie_lt8 && cm.state.focused) cm.focus();
101
+
102
+ var clear = function() {
103
+ cm.operation(function() {
104
+ for (var i = 0; i < marks.length; i++) marks[i].clear();
105
+ });
106
+ };
107
+ if (autoclear) setTimeout(clear, 800);
108
+ else return clear;
109
+ }
110
+ }
111
+
112
+ function doMatchBrackets(cm) {
113
+ cm.operation(function() {
114
+ if (cm.state.matchBrackets.currentlyHighlighted) {
115
+ cm.state.matchBrackets.currentlyHighlighted();
116
+ cm.state.matchBrackets.currentlyHighlighted = null;
117
+ }
118
+ cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
119
+ });
120
+ }
121
+
122
+ function clearHighlighted(cm) {
123
+ if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) {
124
+ cm.state.matchBrackets.currentlyHighlighted();
125
+ cm.state.matchBrackets.currentlyHighlighted = null;
126
+ }
127
+ }
128
+
129
+ CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
130
+ if (old && old != CodeMirror.Init) {
131
+ cm.off("cursorActivity", doMatchBrackets);
132
+ cm.off("focus", doMatchBrackets)
133
+ cm.off("blur", clearHighlighted)
134
+ clearHighlighted(cm);
135
+ }
136
+ if (val) {
137
+ cm.state.matchBrackets = typeof val == "object" ? val : {};
138
+ cm.on("cursorActivity", doMatchBrackets);
139
+ cm.on("focus", doMatchBrackets)
140
+ cm.on("blur", clearHighlighted)
141
+ }
142
+ });
143
+
144
+ CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
145
+ CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){
146
+ // Backwards-compatibility kludge
147
+ if (oldConfig || typeof config == "boolean") {
148
+ if (!oldConfig) {
149
+ config = config ? {strict: true} : null
150
+ } else {
151
+ oldConfig.strict = config
152
+ config = oldConfig
153
+ }
154
+ }
155
+ return findMatchingBracket(this, pos, config)
156
+ });
157
+ CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
158
+ return scanForBracket(this, pos, dir, style, config);
159
+ });
160
+ });
vendor/codemirror/python.js ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // CodeMirror, copyright (c) by Marijn Haverbeke and others
2
+ // Distributed under an MIT license: https://codemirror.net/5/LICENSE
3
+
4
+ (function(mod) {
5
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
6
+ mod(require("../../lib/codemirror"));
7
+ else if (typeof define == "function" && define.amd) // AMD
8
+ define(["../../lib/codemirror"], mod);
9
+ else // Plain browser env
10
+ mod(CodeMirror);
11
+ })(function(CodeMirror) {
12
+ "use strict";
13
+
14
+ function wordRegexp(words) {
15
+ return new RegExp("^((" + words.join(")|(") + "))\\b");
16
+ }
17
+
18
+ var wordOperators = wordRegexp(["and", "or", "not", "is"]);
19
+ var commonKeywords = ["as", "assert", "break", "class", "continue",
20
+ "def", "del", "elif", "else", "except", "finally",
21
+ "for", "from", "global", "if", "import",
22
+ "lambda", "pass", "raise", "return",
23
+ "try", "while", "with", "yield", "in", "False", "True"];
24
+ var commonBuiltins = ["abs", "all", "any", "bin", "bool", "bytearray", "callable", "chr",
25
+ "classmethod", "compile", "complex", "delattr", "dict", "dir", "divmod",
26
+ "enumerate", "eval", "filter", "float", "format", "frozenset",
27
+ "getattr", "globals", "hasattr", "hash", "help", "hex", "id",
28
+ "input", "int", "isinstance", "issubclass", "iter", "len",
29
+ "list", "locals", "map", "max", "memoryview", "min", "next",
30
+ "object", "oct", "open", "ord", "pow", "property", "range",
31
+ "repr", "reversed", "round", "set", "setattr", "slice",
32
+ "sorted", "staticmethod", "str", "sum", "super", "tuple",
33
+ "type", "vars", "zip", "__import__", "NotImplemented",
34
+ "Ellipsis", "__debug__"];
35
+ CodeMirror.registerHelper("hintWords", "python", commonKeywords.concat(commonBuiltins).concat(["exec", "print"]));
36
+
37
+ function top(state) {
38
+ return state.scopes[state.scopes.length - 1];
39
+ }
40
+
41
+ CodeMirror.defineMode("python", function(conf, parserConf) {
42
+ var ERRORCLASS = "error";
43
+
44
+ var delimiters = parserConf.delimiters || parserConf.singleDelimiters || /^[\(\)\[\]\{\}@,:`=;\.\\]/;
45
+ // (Backwards-compatibility with old, cumbersome config system)
46
+ var operators = [parserConf.singleOperators, parserConf.doubleOperators, parserConf.doubleDelimiters, parserConf.tripleDelimiters,
47
+ parserConf.operators || /^([-+*/%\/&|^]=?|[<>=]+|\/\/=?|\*\*=?|!=|[~!@]|\.\.\.)/]
48
+ for (var i = 0; i < operators.length; i++) if (!operators[i]) operators.splice(i--, 1)
49
+
50
+ var hangingIndent = parserConf.hangingIndent || conf.indentUnit;
51
+
52
+ var myKeywords = commonKeywords, myBuiltins = commonBuiltins;
53
+ if (parserConf.extra_keywords != undefined)
54
+ myKeywords = myKeywords.concat(parserConf.extra_keywords);
55
+
56
+ if (parserConf.extra_builtins != undefined)
57
+ myBuiltins = myBuiltins.concat(parserConf.extra_builtins);
58
+
59
+ var py3 = !(parserConf.version && Number(parserConf.version) < 3)
60
+ if (py3) {
61
+ // since http://legacy.python.org/dev/peps/pep-0465/ @ is also an operator
62
+ var identifiers = parserConf.identifiers|| /^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*/;
63
+ myKeywords = myKeywords.concat(["nonlocal", "None", "aiter", "anext", "async", "await", "breakpoint", "match", "case"]);
64
+ myBuiltins = myBuiltins.concat(["ascii", "bytes", "exec", "print"]);
65
+ var stringPrefixes = new RegExp("^(([rbuf]|(br)|(rb)|(fr)|(rf))?('{3}|\"{3}|['\"]))", "i");
66
+ } else {
67
+ var identifiers = parserConf.identifiers|| /^[_A-Za-z][_A-Za-z0-9]*/;
68
+ myKeywords = myKeywords.concat(["exec", "print"]);
69
+ myBuiltins = myBuiltins.concat(["apply", "basestring", "buffer", "cmp", "coerce", "execfile",
70
+ "file", "intern", "long", "raw_input", "reduce", "reload",
71
+ "unichr", "unicode", "xrange", "None"]);
72
+ var stringPrefixes = new RegExp("^(([rubf]|(ur)|(br))?('{3}|\"{3}|['\"]))", "i");
73
+ }
74
+ var keywords = wordRegexp(myKeywords);
75
+ var builtins = wordRegexp(myBuiltins);
76
+
77
+ // tokenizers
78
+ function tokenBase(stream, state) {
79
+ var sol = stream.sol() && state.lastToken != "\\"
80
+ if (sol) state.indent = stream.indentation()
81
+ // Handle scope changes
82
+ if (sol && top(state).type == "py") {
83
+ var scopeOffset = top(state).offset;
84
+ if (stream.eatSpace()) {
85
+ var lineOffset = stream.indentation();
86
+ if (lineOffset > scopeOffset)
87
+ pushPyScope(state);
88
+ else if (lineOffset < scopeOffset && dedent(stream, state) && stream.peek() != "#")
89
+ state.errorToken = true;
90
+ return null;
91
+ } else {
92
+ var style = tokenBaseInner(stream, state);
93
+ if (scopeOffset > 0 && dedent(stream, state))
94
+ style += " " + ERRORCLASS;
95
+ return style;
96
+ }
97
+ }
98
+ return tokenBaseInner(stream, state);
99
+ }
100
+
101
+ function tokenBaseInner(stream, state, inFormat) {
102
+ if (stream.eatSpace()) return null;
103
+
104
+ // Handle Comments
105
+ if (!inFormat && stream.match(/^#.*/)) return "comment";
106
+
107
+ // Handle Number Literals
108
+ if (stream.match(/^[0-9\.]/, false)) {
109
+ var floatLiteral = false;
110
+ // Floats
111
+ if (stream.match(/^[\d_]*\.\d+(e[\+\-]?\d+)?/i)) { floatLiteral = true; }
112
+ if (stream.match(/^[\d_]+\.\d*/)) { floatLiteral = true; }
113
+ if (stream.match(/^\.\d+/)) { floatLiteral = true; }
114
+ if (floatLiteral) {
115
+ // Float literals may be "imaginary"
116
+ stream.eat(/J/i);
117
+ return "number";
118
+ }
119
+ // Integers
120
+ var intLiteral = false;
121
+ // Hex
122
+ if (stream.match(/^0x[0-9a-f_]+/i)) intLiteral = true;
123
+ // Binary
124
+ if (stream.match(/^0b[01_]+/i)) intLiteral = true;
125
+ // Octal
126
+ if (stream.match(/^0o[0-7_]+/i)) intLiteral = true;
127
+ // Decimal
128
+ if (stream.match(/^[1-9][\d_]*(e[\+\-]?[\d_]+)?/)) {
129
+ // Decimal literals may be "imaginary"
130
+ stream.eat(/J/i);
131
+ // TODO - Can you have imaginary longs?
132
+ intLiteral = true;
133
+ }
134
+ // Zero by itself with no other piece of number.
135
+ if (stream.match(/^0(?![\dx])/i)) intLiteral = true;
136
+ if (intLiteral) {
137
+ // Integer literals may be "long"
138
+ stream.eat(/L/i);
139
+ return "number";
140
+ }
141
+ }
142
+
143
+ // Handle Strings
144
+ if (stream.match(stringPrefixes)) {
145
+ var isFmtString = stream.current().toLowerCase().indexOf('f') !== -1;
146
+ if (!isFmtString) {
147
+ state.tokenize = tokenStringFactory(stream.current(), state.tokenize);
148
+ return state.tokenize(stream, state);
149
+ } else {
150
+ state.tokenize = formatStringFactory(stream.current(), state.tokenize);
151
+ return state.tokenize(stream, state);
152
+ }
153
+ }
154
+
155
+ for (var i = 0; i < operators.length; i++)
156
+ if (stream.match(operators[i])) return "operator"
157
+
158
+ if (stream.match(delimiters)) return "punctuation";
159
+
160
+ if (state.lastToken == "." && stream.match(identifiers))
161
+ return "property";
162
+
163
+ if (stream.match(keywords) || stream.match(wordOperators))
164
+ return "keyword";
165
+
166
+ if (stream.match(builtins))
167
+ return "builtin";
168
+
169
+ if (stream.match(/^(self|cls)\b/))
170
+ return "variable-2";
171
+
172
+ if (stream.match(identifiers)) {
173
+ if (state.lastToken == "def" || state.lastToken == "class")
174
+ return "def";
175
+ return "variable";
176
+ }
177
+
178
+ // Handle non-detected items
179
+ stream.next();
180
+ return inFormat ? null :ERRORCLASS;
181
+ }
182
+
183
+ function formatStringFactory(delimiter, tokenOuter) {
184
+ while ("rubf".indexOf(delimiter.charAt(0).toLowerCase()) >= 0)
185
+ delimiter = delimiter.substr(1);
186
+
187
+ var singleline = delimiter.length == 1;
188
+ var OUTCLASS = "string";
189
+
190
+ function tokenNestedExpr(depth) {
191
+ return function(stream, state) {
192
+ var inner = tokenBaseInner(stream, state, true)
193
+ if (inner == "punctuation") {
194
+ if (stream.current() == "{") {
195
+ state.tokenize = tokenNestedExpr(depth + 1)
196
+ } else if (stream.current() == "}") {
197
+ if (depth > 1) state.tokenize = tokenNestedExpr(depth - 1)
198
+ else state.tokenize = tokenString
199
+ }
200
+ }
201
+ return inner
202
+ }
203
+ }
204
+
205
+ function tokenString(stream, state) {
206
+ while (!stream.eol()) {
207
+ stream.eatWhile(/[^'"\{\}\\]/);
208
+ if (stream.eat("\\")) {
209
+ stream.next();
210
+ if (singleline && stream.eol())
211
+ return OUTCLASS;
212
+ } else if (stream.match(delimiter)) {
213
+ state.tokenize = tokenOuter;
214
+ return OUTCLASS;
215
+ } else if (stream.match('{{')) {
216
+ // ignore {{ in f-str
217
+ return OUTCLASS;
218
+ } else if (stream.match('{', false)) {
219
+ // switch to nested mode
220
+ state.tokenize = tokenNestedExpr(0)
221
+ if (stream.current()) return OUTCLASS;
222
+ else return state.tokenize(stream, state)
223
+ } else if (stream.match('}}')) {
224
+ return OUTCLASS;
225
+ } else if (stream.match('}')) {
226
+ // single } in f-string is an error
227
+ return ERRORCLASS;
228
+ } else {
229
+ stream.eat(/['"]/);
230
+ }
231
+ }
232
+ if (singleline) {
233
+ if (parserConf.singleLineStringErrors)
234
+ return ERRORCLASS;
235
+ else
236
+ state.tokenize = tokenOuter;
237
+ }
238
+ return OUTCLASS;
239
+ }
240
+ tokenString.isString = true;
241
+ return tokenString;
242
+ }
243
+
244
+ function tokenStringFactory(delimiter, tokenOuter) {
245
+ while ("rubf".indexOf(delimiter.charAt(0).toLowerCase()) >= 0)
246
+ delimiter = delimiter.substr(1);
247
+
248
+ var singleline = delimiter.length == 1;
249
+ var OUTCLASS = "string";
250
+
251
+ function tokenString(stream, state) {
252
+ while (!stream.eol()) {
253
+ stream.eatWhile(/[^'"\\]/);
254
+ if (stream.eat("\\")) {
255
+ stream.next();
256
+ if (singleline && stream.eol())
257
+ return OUTCLASS;
258
+ } else if (stream.match(delimiter)) {
259
+ state.tokenize = tokenOuter;
260
+ return OUTCLASS;
261
+ } else {
262
+ stream.eat(/['"]/);
263
+ }
264
+ }
265
+ if (singleline) {
266
+ if (parserConf.singleLineStringErrors)
267
+ return ERRORCLASS;
268
+ else
269
+ state.tokenize = tokenOuter;
270
+ }
271
+ return OUTCLASS;
272
+ }
273
+ tokenString.isString = true;
274
+ return tokenString;
275
+ }
276
+
277
+ function pushPyScope(state) {
278
+ while (top(state).type != "py") state.scopes.pop()
279
+ state.scopes.push({offset: top(state).offset + conf.indentUnit,
280
+ type: "py",
281
+ align: null})
282
+ }
283
+
284
+ function pushBracketScope(stream, state, type) {
285
+ var align = stream.match(/^[\s\[\{\(]*(?:#|$)/, false) ? null : stream.column() + 1
286
+ state.scopes.push({offset: state.indent + hangingIndent,
287
+ type: type,
288
+ align: align})
289
+ }
290
+
291
+ function dedent(stream, state) {
292
+ var indented = stream.indentation();
293
+ while (state.scopes.length > 1 && top(state).offset > indented) {
294
+ if (top(state).type != "py") return true;
295
+ state.scopes.pop();
296
+ }
297
+ return top(state).offset != indented;
298
+ }
299
+
300
+ function tokenLexer(stream, state) {
301
+ if (stream.sol()) {
302
+ state.beginningOfLine = true;
303
+ state.dedent = false;
304
+ }
305
+
306
+ var style = state.tokenize(stream, state);
307
+ var current = stream.current();
308
+
309
+ // Handle decorators
310
+ if (state.beginningOfLine && current == "@")
311
+ return stream.match(identifiers, false) ? "meta" : py3 ? "operator" : ERRORCLASS;
312
+
313
+ if (/\S/.test(current)) state.beginningOfLine = false;
314
+
315
+ if ((style == "variable" || style == "builtin")
316
+ && state.lastToken == "meta")
317
+ style = "meta";
318
+
319
+ // Handle scope changes.
320
+ if (current == "pass" || current == "return")
321
+ state.dedent = true;
322
+
323
+ if (current == "lambda") state.lambda = true;
324
+ if (current == ":" && !state.lambda && top(state).type == "py" && stream.match(/^\s*(?:#|$)/, false))
325
+ pushPyScope(state);
326
+
327
+ if (current.length == 1 && !/string|comment/.test(style)) {
328
+ var delimiter_index = "[({".indexOf(current);
329
+ if (delimiter_index != -1)
330
+ pushBracketScope(stream, state, "])}".slice(delimiter_index, delimiter_index+1));
331
+
332
+ delimiter_index = "])}".indexOf(current);
333
+ if (delimiter_index != -1) {
334
+ if (top(state).type == current) state.indent = state.scopes.pop().offset - hangingIndent
335
+ else return ERRORCLASS;
336
+ }
337
+ }
338
+ if (state.dedent && stream.eol() && top(state).type == "py" && state.scopes.length > 1)
339
+ state.scopes.pop();
340
+
341
+ return style;
342
+ }
343
+
344
+ var external = {
345
+ startState: function(basecolumn) {
346
+ return {
347
+ tokenize: tokenBase,
348
+ scopes: [{offset: basecolumn || 0, type: "py", align: null}],
349
+ indent: basecolumn || 0,
350
+ lastToken: null,
351
+ lambda: false,
352
+ dedent: 0
353
+ };
354
+ },
355
+
356
+ token: function(stream, state) {
357
+ var addErr = state.errorToken;
358
+ if (addErr) state.errorToken = false;
359
+ var style = tokenLexer(stream, state);
360
+
361
+ if (style && style != "comment")
362
+ state.lastToken = (style == "keyword" || style == "punctuation") ? stream.current() : style;
363
+ if (style == "punctuation") style = null;
364
+
365
+ if (stream.eol() && state.lambda)
366
+ state.lambda = false;
367
+ return addErr ? style + " " + ERRORCLASS : style;
368
+ },
369
+
370
+ indent: function(state, textAfter) {
371
+ if (state.tokenize != tokenBase)
372
+ return state.tokenize.isString ? CodeMirror.Pass : 0;
373
+
374
+ var scope = top(state)
375
+ var closing = scope.type == textAfter.charAt(0) ||
376
+ scope.type == "py" && !state.dedent && /^(else:|elif |except |finally:)/.test(textAfter)
377
+ if (scope.align != null)
378
+ return scope.align - (closing ? 1 : 0)
379
+ else
380
+ return scope.offset - (closing ? hangingIndent : 0)
381
+ },
382
+
383
+ electricInput: /^\s*([\}\]\)]|else:|elif |except |finally:)$/,
384
+ closeBrackets: {triples: "'\""},
385
+ lineComment: "#",
386
+ fold: "indent"
387
+ };
388
+ return external;
389
+ });
390
+
391
+ CodeMirror.defineMIME("text/x-python", "python");
392
+
393
+ var words = function(str) { return str.split(" "); };
394
+
395
+ CodeMirror.defineMIME("text/x-cython", {
396
+ name: "python",
397
+ extra_keywords: words("by cdef cimport cpdef ctypedef enum except "+
398
+ "extern gil include nogil property public "+
399
+ "readonly struct union DEF IF ELIF ELSE")
400
+ });
401
+
402
+ });
vendor/engine.js ADDED
The diff for this file is too large to render. See raw diff
 
viewer.js ADDED
@@ -0,0 +1,559 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ THREE,
3
+ OrbitControls,
4
+ mergeGeometries,
5
+ Brush,
6
+ Evaluator,
7
+ SUBTRACTION,
8
+ INTERSECTION,
9
+ } from './vendor/engine.js';
10
+
11
+ const Z = new THREE.Vector3(0, 0, 1);
12
+
13
+ function disposeObject(object) {
14
+ object.traverse((child) => {
15
+ if (child.geometry) child.geometry.dispose();
16
+ if (child.material) {
17
+ const materials = Array.isArray(child.material) ? child.material : [child.material];
18
+ for (const material of materials) material.dispose();
19
+ }
20
+ });
21
+ }
22
+
23
+ function cleanPoints(raw) {
24
+ const points = [];
25
+ for (const p of raw || []) {
26
+ const v = new THREE.Vector2(Number(p[0]) || 0, Number(p[1]) || 0);
27
+ if (points.length === 0 || points[points.length - 1].distanceToSquared(v) > 1e-12) {
28
+ points.push(v);
29
+ }
30
+ }
31
+ if (points.length > 1 && points[0].distanceToSquared(points[points.length - 1]) < 1e-12) {
32
+ points.pop();
33
+ }
34
+ let area = 0;
35
+ for (let i = 0; i < points.length; i += 1) {
36
+ const a = points[i];
37
+ const b = points[(i + 1) % points.length];
38
+ area += a.x * b.y - b.x * a.y;
39
+ }
40
+ if (area < 0) points.reverse();
41
+ return points;
42
+ }
43
+
44
+ function cleanGeometry(geometry) {
45
+ let g = geometry.index ? geometry.toNonIndexed() : geometry;
46
+ for (const name of Object.keys(g.attributes)) {
47
+ if (name !== 'position' && name !== 'normal') g.deleteAttribute(name);
48
+ }
49
+ if (!g.attributes.normal) g.computeVertexNormals();
50
+ return g;
51
+ }
52
+
53
+ function orientAlong(geometry, dirArray, pointArray) {
54
+ const dir = new THREE.Vector3().fromArray(dirArray || [0, 0, 1]);
55
+ if (dir.lengthSq() < 1e-12) dir.set(0, 0, 1);
56
+ dir.normalize();
57
+ if (Math.abs(dir.z - 1) > 1e-8) {
58
+ const q = new THREE.Quaternion().setFromUnitVectors(Z, dir);
59
+ geometry.applyQuaternion(q);
60
+ }
61
+ const p = pointArray || [0, 0, 0];
62
+ geometry.translate(p[0] || 0, p[1] || 0, p[2] || 0);
63
+ }
64
+
65
+ function primitive(node) {
66
+ let geometry;
67
+ const segments = 48;
68
+ switch (node.op) {
69
+ case 'box': {
70
+ const l = Math.max(1e-4, Number(node.l) || 0);
71
+ const w = Math.max(1e-4, Number(node.w) || 0);
72
+ const h = Math.max(1e-4, Number(node.h) || 0);
73
+ geometry = new THREE.BoxGeometry(l, w, h, 1, 1, 1);
74
+ geometry.translate(l / 2, w / 2, h / 2);
75
+ orientAlong(geometry, node.dir, node.pnt);
76
+ return geometry;
77
+ }
78
+ case 'cylinder': {
79
+ const r = Math.max(1e-4, Number(node.r) || 0);
80
+ const h = Math.max(1e-4, Number(node.h) || 0);
81
+ const angle = Math.min(360, Math.max(1, Number(node.angle) || 360));
82
+ geometry = new THREE.CylinderGeometry(
83
+ r,
84
+ r,
85
+ h,
86
+ segments,
87
+ 1,
88
+ false,
89
+ 0,
90
+ (angle / 360) * Math.PI * 2,
91
+ );
92
+ geometry.rotateX(Math.PI / 2);
93
+ geometry.translate(0, 0, h / 2);
94
+ orientAlong(geometry, node.dir, node.pnt);
95
+ return geometry;
96
+ }
97
+ case 'cone': {
98
+ const r1 = Math.max(0, Number(node.r1) || 0);
99
+ const r2 = Math.max(0, Number(node.r2) || 0);
100
+ const h = Math.max(1e-4, Number(node.h) || 0);
101
+ const angle = Math.min(360, Math.max(1, Number(node.angle) || 360));
102
+ geometry = new THREE.CylinderGeometry(
103
+ r2,
104
+ r1,
105
+ h,
106
+ segments,
107
+ 1,
108
+ false,
109
+ 0,
110
+ (angle / 360) * Math.PI * 2,
111
+ );
112
+ geometry.rotateX(Math.PI / 2);
113
+ geometry.translate(0, 0, h / 2);
114
+ orientAlong(geometry, node.dir, node.pnt);
115
+ return geometry;
116
+ }
117
+ case 'sphere': {
118
+ const r = Math.max(1e-4, Number(node.r) || 0);
119
+ geometry = new THREE.SphereGeometry(r, segments, 32);
120
+ const p = node.pnt || [0, 0, 0];
121
+ geometry.translate(p[0] || 0, p[1] || 0, p[2] || 0);
122
+ return geometry;
123
+ }
124
+ case 'torus': {
125
+ const r1 = Math.max(1e-4, Number(node.r1) || 0);
126
+ const r2 = Math.max(1e-4, Number(node.r2) || 0);
127
+ geometry = new THREE.TorusGeometry(r1, Math.min(r2, r1 * 0.95), 20, 56);
128
+ orientAlong(geometry, node.dir, node.pnt);
129
+ return geometry;
130
+ }
131
+ case 'extrude': {
132
+ const points = cleanPoints(node.profile);
133
+ if (points.length < 3) return null;
134
+ const vec = new THREE.Vector3().fromArray(node.vector || [0, 0, 1]);
135
+ const depth = Math.max(1e-4, vec.length());
136
+ const shape = new THREE.Shape(points);
137
+ geometry = new THREE.ExtrudeGeometry(shape, {
138
+ depth,
139
+ bevelEnabled: false,
140
+ steps: 1,
141
+ curveSegments: 24,
142
+ });
143
+ if (depth > 1e-9) {
144
+ const q = new THREE.Quaternion().setFromUnitVectors(Z, vec.clone().normalize());
145
+ geometry.applyQuaternion(q);
146
+ }
147
+ return geometry;
148
+ }
149
+ case 'face': {
150
+ const points = cleanPoints(node.profile);
151
+ if (points.length < 3) return null;
152
+ geometry = new THREE.ShapeGeometry(new THREE.Shape(points), 24);
153
+ return geometry;
154
+ }
155
+ case 'wire': {
156
+ const points = cleanPoints(node.profile);
157
+ if (points.length < 2) return null;
158
+ const curve = new THREE.CatmullRomCurve3(
159
+ points.map((p) => new THREE.Vector3(p.x, p.y, 0)),
160
+ Boolean(node.closed),
161
+ 'catmullrom',
162
+ 0.08,
163
+ );
164
+ geometry = new THREE.TubeGeometry(
165
+ curve,
166
+ Math.max(12, points.length * 3),
167
+ 0.16,
168
+ 8,
169
+ Boolean(node.closed),
170
+ );
171
+ return geometry;
172
+ }
173
+ default:
174
+ return null;
175
+ }
176
+ }
177
+
178
+ function makeBytes(geometry) {
179
+ return cleanGeometry(geometry);
180
+ }
181
+
182
+ function unionGeometries(a, b) {
183
+ const merged = mergeGeometries([a, b], false);
184
+ return merged || a;
185
+ }
186
+
187
+ function csgGeometry(a, b, operation) {
188
+ try {
189
+ const evaluator = new Evaluator();
190
+ evaluator.attributes = ['position', 'normal'];
191
+ evaluator.useGroups = false;
192
+ const brushA = new Brush(a);
193
+ const brushB = new Brush(b);
194
+ brushA.updateMatrixWorld();
195
+ brushB.updateMatrixWorld();
196
+ const result = evaluator.evaluate(brushA, brushB, operation);
197
+ const out = result.geometry;
198
+ if (!out || !out.attributes.position || out.attributes.position.count === 0) return a;
199
+ return cleanGeometry(out.clone());
200
+ } catch (error) {
201
+ console.warn('CSG failed', error);
202
+ return a;
203
+ }
204
+ }
205
+
206
+ function nodeHasBoolean(node) {
207
+ if (!node) return false;
208
+ if (node.op === 'boolean') return true;
209
+ if (node.op === 'transform') return nodeHasBoolean(node.child);
210
+ if (node.op === 'group') return (node.children || []).some(nodeHasBoolean);
211
+ return false;
212
+ }
213
+
214
+ function buildNode(node) {
215
+ if (!node) return null;
216
+ switch (node.op) {
217
+ case 'boolean': {
218
+ const a = buildNode(node.a);
219
+ const b = buildNode(node.b);
220
+ if (!a) return b;
221
+ if (!b) return a;
222
+ if (node.kind === 'cut') return csgGeometry(a, b, SUBTRACTION);
223
+ if (node.kind === 'common') return csgGeometry(a, b, INTERSECTION);
224
+ return unionGeometries(a, b);
225
+ }
226
+ case 'group': {
227
+ const parts = (node.children || []).map(buildNode).filter(Boolean);
228
+ if (parts.length === 0) return null;
229
+ if (parts.length === 1) return parts[0];
230
+ return mergeGeometries(parts, false) || parts[0];
231
+ }
232
+ case 'transform': {
233
+ const child = buildNode(node.child);
234
+ if (!child) return null;
235
+ const matrix = new THREE.Matrix4().fromArray(node.m || []);
236
+ child.applyMatrix4(matrix);
237
+ return child;
238
+ }
239
+ default:
240
+ return primitive(node);
241
+ }
242
+ }
243
+
244
+ function makeMaterial(item) {
245
+ const [r, g, b] = item.color || [0.8, 0.8, 0.8];
246
+ const color = new THREE.Color().setRGB(r, g, b, THREE.SRGBColorSpace);
247
+ const luminance = r * 0.2126 + g * 0.7152 + b * 0.0722;
248
+ const dark = luminance < 0.2;
249
+ const opacity = typeof item.opacity === 'number' ? item.opacity : 1;
250
+ return new THREE.MeshStandardMaterial({
251
+ color,
252
+ roughness: dark ? 0.36 : 0.82,
253
+ metalness: dark ? 0.12 : 0.02,
254
+ transparent: opacity < 0.999,
255
+ opacity,
256
+ depthWrite: opacity > 0.999,
257
+ side: THREE.FrontSide,
258
+ });
259
+ }
260
+
261
+ export function createViewer(canvas, options = {}) {
262
+ const renderer = new THREE.WebGLRenderer({
263
+ canvas,
264
+ antialias: true,
265
+ alpha: true,
266
+ powerPreference: 'high-performance',
267
+ });
268
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
269
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
270
+ renderer.toneMappingExposure = 1.05;
271
+ renderer.shadowMap.enabled = true;
272
+ renderer.shadowMap.type = THREE.PCFShadowMap;
273
+ renderer.setClearColor(0x000000, 0);
274
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
275
+
276
+ const scene = new THREE.Scene();
277
+ const camera = new THREE.PerspectiveCamera(32, 1, 0.1, 10000);
278
+ camera.up.set(0, 0, 1);
279
+
280
+ let viewDirection = new THREE.Vector3().fromArray(options.direction || [0.86, -1.35, 0.98]).normalize();
281
+ const showEdges = options.edges !== false;
282
+
283
+ const controls = new OrbitControls(camera, canvas);
284
+ controls.enableDamping = true;
285
+ controls.dampingFactor = 0.075;
286
+ controls.enablePan = false;
287
+ controls.rotateSpeed = 0.6;
288
+ controls.zoomSpeed = 0.8;
289
+ controls.autoRotate = options.autoRotate !== false;
290
+ controls.autoRotateSpeed = 0.75;
291
+
292
+ scene.add(new THREE.HemisphereLight(0xfff8ec, 0xb2a690, 1.35));
293
+
294
+ const key = new THREE.DirectionalLight(0xfff2df, 2.7);
295
+ key.castShadow = true;
296
+ key.shadow.mapSize.set(2048, 2048);
297
+ key.shadow.bias = -0.0004;
298
+ key.shadow.radius = 3;
299
+ scene.add(key);
300
+ scene.add(key.target);
301
+
302
+ const fill = new THREE.DirectionalLight(0xdfe9ff, 0.85);
303
+ fill.position.set(-30, 45, 30);
304
+ scene.add(fill);
305
+
306
+ const rim = new THREE.DirectionalLight(0xffffff, 0.7);
307
+ rim.position.set(20, 50, -35);
308
+ scene.add(rim);
309
+
310
+ const root = new THREE.Group();
311
+ scene.add(root);
312
+
313
+ const floor = new THREE.Mesh(
314
+ new THREE.PlaneGeometry(400, 400),
315
+ new THREE.ShadowMaterial({ color: 0x2a241c, opacity: 0.16 }),
316
+ );
317
+ floor.rotation.x = Math.PI / 2;
318
+ floor.receiveShadow = true;
319
+ floor.renderOrder = -2;
320
+ scene.add(floor);
321
+
322
+ let grid = null;
323
+ let size = 10;
324
+ let center = new THREE.Vector3();
325
+ let radius = 5;
326
+ let fitted = false;
327
+ let autoRotateEnabled = options.autoRotate !== false;
328
+ let resumeAt = 0;
329
+
330
+ function resize() {
331
+ const parent = canvas.parentElement;
332
+ if (!parent) return;
333
+ const width = parent.clientWidth;
334
+ const height = parent.clientHeight;
335
+ if (width < 2 || height < 2) return;
336
+ renderer.setSize(width, height, false);
337
+ camera.aspect = width / height;
338
+ camera.updateProjectionMatrix();
339
+ if (fitted) fit(false);
340
+ }
341
+
342
+ function fit(resetDirection) {
343
+ if (!fitted) return;
344
+ const aspect = camera.aspect || 1;
345
+ const dir = viewDirection.clone();
346
+ if (!resetDirection) {
347
+ const current = camera.position.clone().sub(controls.target).normalize();
348
+ if (current.lengthSq() > 0.5) dir.copy(current);
349
+ }
350
+ const forward = dir.clone().negate();
351
+ let up = camera.up.clone();
352
+ let right = new THREE.Vector3().crossVectors(forward, up);
353
+ if (right.lengthSq() < 1e-8) {
354
+ up.set(0, 1, 0);
355
+ right = new THREE.Vector3().crossVectors(forward, up);
356
+ }
357
+ right.normalize();
358
+ const camUp = new THREE.Vector3().crossVectors(right, forward).normalize();
359
+ const tanV = Math.tan(THREE.MathUtils.degToRad(camera.fov * 0.5));
360
+ const tanH = tanV * aspect;
361
+ const max = new THREE.Vector3();
362
+ const box = new THREE.Box3().setFromObject(root);
363
+ if (box.isEmpty()) {
364
+ box.setFromCenterAndSize(center, new THREE.Vector3(radius * 2, radius * 2, radius * 2));
365
+ }
366
+ box.getSize(max);
367
+ const half = max.multiplyScalar(0.5);
368
+ const corners = [];
369
+ for (const x of [-1, 1]) {
370
+ for (const y of [-1, 1]) {
371
+ for (const z of [-1, 1]) {
372
+ corners.push(
373
+ new THREE.Vector3(x * half.x, y * half.y, z * half.z),
374
+ );
375
+ }
376
+ }
377
+ }
378
+ let distance = 0;
379
+ for (const corner of corners) {
380
+ const x = corner.dot(right);
381
+ const y = corner.dot(camUp);
382
+ const z = corner.dot(forward);
383
+ distance = Math.max(distance, Math.abs(x) / tanH - z, Math.abs(y) / tanV - z);
384
+ }
385
+ distance = Math.max(distance * 1.08, radius * 0.5);
386
+ camera.position.copy(center).addScaledVector(dir, distance);
387
+ camera.near = Math.max(0.01, distance - radius * 4);
388
+ camera.far = distance + radius * 12;
389
+ camera.updateProjectionMatrix();
390
+ controls.target.copy(center);
391
+ controls.minDistance = radius * 0.25;
392
+ controls.maxDistance = radius * 8;
393
+ controls.update();
394
+
395
+ key.position.copy(center).add(new THREE.Vector3(0.7, -0.9, 1.5).normalize().multiplyScalar(radius * 3.6));
396
+ key.target.position.copy(center);
397
+ key.target.updateMatrixWorld();
398
+ const extent = radius * 1.55;
399
+ const shadow = key.shadow.camera;
400
+ shadow.left = -extent;
401
+ shadow.right = extent;
402
+ shadow.top = extent;
403
+ shadow.bottom = -extent;
404
+ shadow.near = 0.1;
405
+ shadow.far = radius * 12;
406
+ shadow.updateProjectionMatrix();
407
+ }
408
+
409
+ function setScene(sceneData, direction) {
410
+ if (direction && direction.length === 3) {
411
+ viewDirection = new THREE.Vector3().fromArray(direction).normalize();
412
+ }
413
+ for (const child of [...root.children]) {
414
+ root.remove(child);
415
+ disposeObject(child);
416
+ }
417
+ if (grid) {
418
+ scene.remove(grid);
419
+ grid.geometry.dispose();
420
+ grid.material.dispose();
421
+ grid = null;
422
+ }
423
+ const objects = (sceneData && sceneData.objects) || [];
424
+ let built = 0;
425
+ let skipped = 0;
426
+ for (const item of objects) {
427
+ const geometry = buildNode(item.node);
428
+ if (!geometry) {
429
+ skipped += 1;
430
+ continue;
431
+ }
432
+ const clean = makeBytes(geometry);
433
+ const material = makeMaterial(item);
434
+ const mesh = new THREE.Mesh(clean, material);
435
+ mesh.castShadow = true;
436
+ mesh.receiveShadow = true;
437
+ mesh.name = item.name || 'Part';
438
+ if (showEdges && !nodeHasBoolean(item.node) && clean.attributes.position && clean.attributes.position.count < 180000) {
439
+ const edges = new THREE.EdgesGeometry(clean, 48);
440
+ const line = new THREE.LineSegments(
441
+ edges,
442
+ new THREE.LineBasicMaterial({
443
+ color: 0x2f2a22,
444
+ transparent: true,
445
+ opacity: 0.1,
446
+ depthWrite: false,
447
+ }),
448
+ );
449
+ line.renderOrder = 3;
450
+ mesh.add(line);
451
+ }
452
+ root.add(mesh);
453
+ built += 1;
454
+ }
455
+
456
+ const box = new THREE.Box3().setFromObject(root);
457
+ if (box.isEmpty()) {
458
+ size = 10;
459
+ center.set(0, 0, 0);
460
+ radius = 5;
461
+ fitted = false;
462
+ floor.visible = false;
463
+ controls.target.set(0, 0, 0);
464
+ camera.position.set(18, -28, 22);
465
+ camera.near = 0.1;
466
+ camera.far = 1000;
467
+ camera.updateProjectionMatrix();
468
+ controls.update();
469
+ return { objects: 0, skipped, visible: [] };
470
+ }
471
+
472
+ box.getCenter(center);
473
+ const dims = box.getSize(new THREE.Vector3());
474
+ radius = Math.max(1, dims.length() * 0.5);
475
+ size = Math.max(20, Math.max(dims.x, dims.y, dims.z) * 3.2);
476
+ fitted = true;
477
+
478
+ floor.visible = true;
479
+ floor.position.set(center.x, center.y, box.min.z - 0.005);
480
+ floor.updateMatrixWorld();
481
+
482
+ grid = new THREE.GridHelper(size, 18, 0xb9ae9b, 0xd0c7b7);
483
+ grid.rotation.x = Math.PI / 2;
484
+ grid.position.set(center.x, center.y, box.min.z + 0.003);
485
+ grid.material.transparent = true;
486
+ grid.material.opacity = 0.24;
487
+ grid.material.depthWrite = false;
488
+ scene.add(grid);
489
+
490
+ fit(true);
491
+ return {
492
+ objects: built,
493
+ skipped,
494
+ visible: root.children.map((o) => o.name),
495
+ bounds: { min: box.min.toArray(), max: box.max.toArray() },
496
+ };
497
+ }
498
+
499
+ function resetView() {
500
+ if (!fitted) return;
501
+ const box = new THREE.Box3().setFromObject(root);
502
+ if (box.isEmpty()) return;
503
+ box.getCenter(center);
504
+ fit(true);
505
+ resumeAt = performance.now() + 3500;
506
+ }
507
+
508
+ function setAutoRotate(enabled) {
509
+ autoRotateEnabled = Boolean(enabled);
510
+ controls.autoRotate = autoRotateEnabled;
511
+ resumeAt = performance.now() + 3500;
512
+ }
513
+
514
+ function onPointerDown() {
515
+ controls.autoRotate = false;
516
+ resumeAt = performance.now() + 4500;
517
+ }
518
+
519
+ function onPointerUp() {
520
+ if (autoRotateEnabled) {
521
+ resumeAt = performance.now() + 900;
522
+ }
523
+ }
524
+
525
+ canvas.addEventListener('pointerdown', onPointerDown);
526
+ window.addEventListener('pointerup', onPointerUp);
527
+
528
+ const observer = new ResizeObserver(resize);
529
+ if (canvas.parentElement) observer.observe(canvas.parentElement);
530
+ resize();
531
+
532
+ renderer.setAnimationLoop(() => {
533
+ if (autoRotateEnabled && resumeAt && performance.now() > resumeAt) {
534
+ controls.autoRotate = true;
535
+ resumeAt = 0;
536
+ }
537
+ controls.update();
538
+ renderer.render(scene, camera);
539
+ });
540
+
541
+ return {
542
+ setScene,
543
+ resetView,
544
+ setAutoRotate,
545
+ resize,
546
+ isAutoRotate: () => autoRotateEnabled,
547
+ dispose() {
548
+ renderer.setAnimationLoop(null);
549
+ observer.disconnect();
550
+ window.removeEventListener('pointerup', onPointerUp);
551
+ for (const child of [...root.children]) {
552
+ root.remove(child);
553
+ disposeObject(child);
554
+ }
555
+ controls.dispose();
556
+ renderer.dispose();
557
+ },
558
+ };
559
+ }
worker.js ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const PYODIDE_URL = 'https://cdn.jsdelivr.net/pyodide/v0.26.4/full/';
2
+
3
+ let pyodide = null;
4
+ let shimSource = '';
5
+ let initPromise = null;
6
+
7
+ function post(message) {
8
+ self.postMessage(message);
9
+ }
10
+
11
+ async function init() {
12
+ if (pyodide) return pyodide;
13
+ if (initPromise) return initPromise;
14
+ initPromise = (async () => {
15
+ if (!shimSource) throw new Error('No Python shim was supplied.');
16
+ importScripts(`${PYODIDE_URL}pyodide.js`);
17
+ if (typeof loadPyodide !== 'function') {
18
+ throw new Error('Pyodide did not initialise.');
19
+ }
20
+ pyodide = await loadPyodide({ indexURL: PYODIDE_URL });
21
+ pyodide.runPython(shimSource);
22
+ post({ type: 'ready' });
23
+ return pyodide;
24
+ })();
25
+ return initPromise;
26
+ }
27
+
28
+ self.onmessage = async (event) => {
29
+ const message = event.data || {};
30
+ if (message.type === 'init') {
31
+ shimSource = message.shim || '';
32
+ try {
33
+ await init();
34
+ } catch (error) {
35
+ post({ type: 'fatal', error: String((error && error.stack) || error) });
36
+ }
37
+ return;
38
+ }
39
+ if (message.type === 'run') {
40
+ try {
41
+ await init();
42
+ pyodide.globals.set('__wfcad_code', message.code || '');
43
+ const raw = pyodide.runPython(
44
+ 'import json; json.dumps(__wfcad_runtime.run(__wfcad_code))',
45
+ );
46
+ post({ type: 'result', id: message.id, result: JSON.parse(raw) });
47
+ } catch (error) {
48
+ post({
49
+ type: 'error',
50
+ id: message.id,
51
+ error: String((error && error.message) || error),
52
+ });
53
+ }
54
+ }
55
+ };