Initial WFreeCAD static space
Browse files- README.md +51 -4
- app.js +765 -0
- cadshim.py +903 -0
- examples/computer.py +68 -0
- examples/gear.py +60 -0
- examples/index.json +42 -0
- examples/mug.py +32 -0
- examples/rocket.py +63 -0
- favicon.svg +7 -0
- index.html +164 -17
- scenes/computer.json +1 -0
- scenes/gear.json +1 -0
- scenes/mug.json +1 -0
- scenes/rocket.json +1 -0
- style.css +1394 -18
- thumbs/computer.png +0 -0
- thumbs/gear.png +0 -0
- thumbs/mug.png +0 -0
- thumbs/rocket.png +0 -0
- vendor/codemirror/active-line.js +72 -0
- vendor/codemirror/closebrackets.js +201 -0
- vendor/codemirror/codemirror.css +344 -0
- vendor/codemirror/codemirror.js +0 -0
- vendor/codemirror/matchbrackets.js +160 -0
- vendor/codemirror/python.js +402 -0
- vendor/engine.js +0 -0
- viewer.js +559 -0
- worker.js +55 -0
README.md
CHANGED
|
@@ -1,10 +1,57 @@
|
|
| 1 |
---
|
| 2 |
title: WFreeCAD
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
| 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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
}
|
| 17 |
|
| 18 |
-
.
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
}
|
| 25 |
|
| 26 |
-
.
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
};
|