Full-featured .scad text editor w/ autocomplete
This commit is contained in:
parent
6486d698d4
commit
186049b215
14 changed files with 930 additions and 13 deletions
|
|
@ -3,6 +3,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@monaco-editor/loader": "^1.3.2",
|
||||||
"@monaco-editor/react": "^4.4.6",
|
"@monaco-editor/react": "^4.4.6",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import logo from '../public/logo.png';
|
import SCADEditor from './Editor';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
|
|
@ -18,6 +20,8 @@ export function App() {
|
||||||
>
|
>
|
||||||
Learn OpenSCAD
|
Learn OpenSCAD
|
||||||
</a>
|
</a>
|
||||||
|
<SCADEditor height="50vh" value="if (true) sphere(10); else cube();"/>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
18
src/BrowserFS.ts
Normal file
18
src/BrowserFS.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
|
||||||
|
import { FS } from "./filesystem";
|
||||||
|
|
||||||
|
interface EmscriptenFS extends FS {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export let BrowserFS = (window as any)['BrowserFS'] as {
|
||||||
|
BFSRequire: (name: string) => any,
|
||||||
|
|
||||||
|
install: (windowOrSelf: Window) => void,
|
||||||
|
configure: (options: any, callback: (e?: any) => void) => void,
|
||||||
|
|
||||||
|
EmscriptenFS: {
|
||||||
|
new(fs: FS, path: string, errnoCodes: object): EmscriptenFS
|
||||||
|
}
|
||||||
|
};
|
||||||
36
src/Editor.tsx
Normal file
36
src/Editor.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Editor from '@monaco-editor/react';
|
||||||
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
|
const options: monaco.editor.IStandaloneEditorConstructionOptions = {
|
||||||
|
lineNumbers: 'on',
|
||||||
|
automaticLayout: true,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
fontSize: 12,
|
||||||
|
language: 'openscad',
|
||||||
|
wordWrap: 'on',
|
||||||
|
wrappingStrategy: 'advanced',
|
||||||
|
suggest: {
|
||||||
|
// snippetsPreventQuickSuggestions: false,
|
||||||
|
showStatusBar: true,
|
||||||
|
preview: true,
|
||||||
|
},
|
||||||
|
codeLens: true,
|
||||||
|
function SCADEditor({value, ...props}: {value: string, height: string}) {
|
||||||
|
|
||||||
|
// let editor: monaco.editor.IStandaloneCodeEditor;
|
||||||
|
|
||||||
|
// function editorDidMount(e) {
|
||||||
|
// editor = e;
|
||||||
|
// console.log('editorDidMount', monaco.languages.getLanguages(), editor);
|
||||||
|
<Editor {...props} className="openscad-editor" defaultLanguage="openscad" value={value} options={options} />
|
||||||
|
// onMount={editorDidMount} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// export function createEditor() {
|
||||||
|
// monaco.editor.create(document.getElementById('openscad-editor') as any, options);
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default SCADEditor;
|
||||||
104
src/filesystem.ts
Normal file
104
src/filesystem.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
// import FS from "browserfs/dist/node/core/FS";
|
||||||
|
import { BrowserFS } from "./BrowserFS";
|
||||||
|
import { zipArchives } from "./zip-archives";
|
||||||
|
|
||||||
|
export interface FS {
|
||||||
|
readdir(path: string, cb: (err: any, files: string[]) => void): void;
|
||||||
|
symlink(target: string, source: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FSMounts = {
|
||||||
|
[n: string]: {fs: string, options: {zipData: Buffer}}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Symlinks = {[alias: string]: string};
|
||||||
|
|
||||||
|
export function readDirAsArray(fs: FS, path: string): Promise<string[] | undefined> {
|
||||||
|
return new Promise((res, rej) => fs.readdir(path, (err, files) => err ? rej(err) : res(files)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBrowserFSLibrariesMounts(archiveNames: string[]) {
|
||||||
|
const Buffer = BrowserFS.BFSRequire('buffer').Buffer;
|
||||||
|
const fetchData = async (url: string) => (await fetch(url)).arrayBuffer();
|
||||||
|
const results: [string, ArrayBuffer][] =
|
||||||
|
await Promise.all(archiveNames.map(async (n: string) => [n, await fetchData(`./libraries/${n}.zip`)]));
|
||||||
|
|
||||||
|
const zipMounts: FSMounts = {};
|
||||||
|
for (const [n, zipData] of results) {
|
||||||
|
zipMounts[n] = {
|
||||||
|
fs: "ZipFS",
|
||||||
|
options: {
|
||||||
|
zipData: Buffer.from(zipData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return zipMounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function symlinkLibraries(archiveNames: string[], fs: FS, prefix='/libraries', cwd='/tmp') {
|
||||||
|
const createSymlink = async (target: string, source: string) => {
|
||||||
|
// console.log('symlink', target, source);
|
||||||
|
await fs.symlink(target, source);
|
||||||
|
// await symlink(target, source);
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(archiveNames.map(n => (async () => {
|
||||||
|
if (!(n in zipArchives)) throw `Archive named ${n} invalid (valid ones: ${Object.keys(zipArchives).join(', ')})`;
|
||||||
|
const {symlinks} = (zipArchives)[n];
|
||||||
|
if (symlinks) {
|
||||||
|
for (const from in symlinks) {
|
||||||
|
const to = symlinks[from];
|
||||||
|
const target = to == '.' ? `${prefix}/${n}` : `${prefix}/${n}/${to}`;
|
||||||
|
const source = from.startsWith('/') ? from : `${cwd}/${from}`;
|
||||||
|
await createSymlink(target, source);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await createSymlink(`${prefix}/${n}`, `${cwd}/${n}`);
|
||||||
|
}
|
||||||
|
})()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureAndInstallFS(windowOrSelf: Window, options: any) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
BrowserFS.install(windowOrSelf);
|
||||||
|
try {
|
||||||
|
BrowserFS.configure(options, function (e: any) {
|
||||||
|
if (e) reject(e);
|
||||||
|
else resolve(null);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createEditorFS(workingDir='/home') {
|
||||||
|
const archiveNames = Object.keys(zipArchives);
|
||||||
|
const librariesMounts = await getBrowserFSLibrariesMounts(archiveNames);
|
||||||
|
const allMounts: FSMounts = {};
|
||||||
|
for (const n in librariesMounts) {
|
||||||
|
allMounts[`${workingDir}/${n}`] = librariesMounts[n];
|
||||||
|
}
|
||||||
|
|
||||||
|
await configureAndInstallFS(window, {
|
||||||
|
fs: "OverlayFS",
|
||||||
|
options: {
|
||||||
|
readable: {
|
||||||
|
fs: "MountableFileSystem",
|
||||||
|
options: {
|
||||||
|
...allMounts,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
writable: {
|
||||||
|
fs: "InMemory"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var fs = BrowserFS.BFSRequire('fs');
|
||||||
|
// const symlink = (target, source) => new Promise((res, rej) => fs.symlink(target, source, (err) => err ? rej(err) : res()));
|
||||||
|
|
||||||
|
// await setupLibraries(archiveNames, symlink, '/libraries', workingDir);
|
||||||
|
return fs;
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,31 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
import {App} from './App';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
import { createEditorFS } from './filesystem';
|
||||||
|
import { registerOpenSCADLanguage } from './language/openscad-register-language';
|
||||||
|
import { zipArchives } from './zip-archives';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
|
||||||
|
const workingDir = '/home';
|
||||||
|
const fs = await createEditorFS(workingDir);
|
||||||
|
await registerOpenSCADLanguage(fs, workingDir, zipArchives);
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(
|
||||||
|
document.getElementById('root') as HTMLElement
|
||||||
|
);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
|
||||||
document.getElementById('root') as HTMLElement
|
|
||||||
);
|
|
||||||
root.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
|
|
|
||||||
106
src/language/openscad-builtins.ts
Normal file
106
src/language/openscad-builtins.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
export default `
|
||||||
|
$fa=undef;
|
||||||
|
$fs=undef;
|
||||||
|
$fn=undef;
|
||||||
|
$t=undef;
|
||||||
|
|
||||||
|
$preview=undef;
|
||||||
|
|
||||||
|
// shows rotation
|
||||||
|
$vpr=undef;
|
||||||
|
// shows translation (i.e. won't be affected by rotate and zoom)
|
||||||
|
$vpt=undef;
|
||||||
|
// shows the FOV of the view [Note: Requires version 2021.01]
|
||||||
|
$vpf=undef;
|
||||||
|
// shows the camera distance [Note: Requires version 2015.03]
|
||||||
|
$vpd=undef;
|
||||||
|
|
||||||
|
PI=undef;
|
||||||
|
|
||||||
|
function abs(x) = x;
|
||||||
|
function acos(x) = x;
|
||||||
|
function asin(x) = x;
|
||||||
|
function atan(x) = x;
|
||||||
|
function atan2(y, x) = x;
|
||||||
|
function ceil(x) = x;
|
||||||
|
function chr(x) = 0;
|
||||||
|
function len(assignments) = $children;
|
||||||
|
function let(x) = x;
|
||||||
|
function ln(x) = x;
|
||||||
|
function log(x) = x;
|
||||||
|
function lookup(key, array) = x;
|
||||||
|
function max(values) = x;
|
||||||
|
function min(values) = x;
|
||||||
|
function sqrt(x) = x;
|
||||||
|
function tan(degrees) = x;
|
||||||
|
function rands(min_value, max_value, value_count, seed_value=undef) = x;
|
||||||
|
function search(match_value, string_or_vector, num_returns_per_match=1, index_col_num=0) = x;
|
||||||
|
function ord(x) = x;
|
||||||
|
function round(x) = x;
|
||||||
|
function sign(x) = x;
|
||||||
|
function sin(degrees) = x;
|
||||||
|
function str(values) = x;
|
||||||
|
function norm(x) = x;
|
||||||
|
function pow(base, exponent) = x;
|
||||||
|
function concat(values) = x;
|
||||||
|
function cos(degrees) = x;
|
||||||
|
function cross(a, b) = x;
|
||||||
|
function floor(x) = x;
|
||||||
|
function exp(x) = x;
|
||||||
|
function chr(x) = x;
|
||||||
|
function is_undef(x) = x;
|
||||||
|
function is_list(x) = x;
|
||||||
|
function is_num(x) = x;
|
||||||
|
function is_bool(x) = x;
|
||||||
|
function is_string(x) = x;
|
||||||
|
function is_function(x) = x;
|
||||||
|
|
||||||
|
function version() = '';
|
||||||
|
function version_num() = 0;
|
||||||
|
|
||||||
|
$parent_modules=0;
|
||||||
|
module parent_module(n) {}
|
||||||
|
|
||||||
|
module children() {}
|
||||||
|
|
||||||
|
module render(convexity=undef) {}
|
||||||
|
module surface(file, center=false, invert=false, convexity=undef) {}
|
||||||
|
|
||||||
|
function assert(condition, message=undef) = children();
|
||||||
|
module assert(condition, message=undef) children();
|
||||||
|
|
||||||
|
module cube(size, center=false) {}
|
||||||
|
module sphere(r, d=undef, $fa, $fs, $fn) {}
|
||||||
|
module cylinder(h, r, r1=undef, r2, d, d1, d2, center=false, $fa, $fs, $fn) {}
|
||||||
|
module polyhedron(points, faces, convexity=undef) {}
|
||||||
|
|
||||||
|
module square(size, center=false) {}
|
||||||
|
module circle(r, d=undef, $fa, $fs, $fn) {}
|
||||||
|
module polygon(points, paths, convexity=undef) {}
|
||||||
|
module linear_extrude(height, center=false, twist=undef, slices=undef, scale=undef, convexity=undef) children();
|
||||||
|
module rotate_extrude(degrees, convexity=undef, $fa, $fs, $fn) children();
|
||||||
|
|
||||||
|
module scale(v) children();
|
||||||
|
module resize(newsize, auto=false) children();
|
||||||
|
module rotate(a, v=undef) children();
|
||||||
|
module translate(v) children();
|
||||||
|
module mirror(v) children();
|
||||||
|
module multmatrix(m) children();
|
||||||
|
|
||||||
|
module color(c, alpha) children();
|
||||||
|
|
||||||
|
module offset(r, delta=undef, chamfer) children();
|
||||||
|
|
||||||
|
module minkowski() children();
|
||||||
|
module union() children();
|
||||||
|
module difference() children();
|
||||||
|
module intersection() children();
|
||||||
|
module hull() children();
|
||||||
|
|
||||||
|
module children() {}
|
||||||
|
|
||||||
|
// module for(i=undef) children();
|
||||||
|
|
||||||
|
module import(file, convexity=undef, $fn, $fa, $fs) {}
|
||||||
|
`;
|
||||||
288
src/language/openscad-completions.ts
Normal file
288
src/language/openscad-completions.ts
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
import FS from 'browserfs/dist/node/core/FS';
|
||||||
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
|
import { FSMounts, readDirAsArray, Symlinks } from '../filesystem';
|
||||||
|
import { ParsedFile, ParsedFunctionoidDef, parseOpenSCAD, stripComments } from './openscad-pseudoparser';
|
||||||
|
import builtinSignatures from './openscad-builtins'
|
||||||
|
import { mapObject } from '../utils';
|
||||||
|
import { ZipArchives } from '../zip-archives';
|
||||||
|
import openscadLanguage from './openscad-language';
|
||||||
|
|
||||||
|
function makeFunctionoidSuggestion(name: string, mod: ParsedFunctionoidDef) {
|
||||||
|
const argSnippets: string[] = [];
|
||||||
|
const namedArgs: string[] = [];
|
||||||
|
let collectingPosArgs = true;
|
||||||
|
let i = 0;
|
||||||
|
for (const param of mod.params ?? []) {
|
||||||
|
if (collectingPosArgs) {
|
||||||
|
if (param.defaultValue != null) {
|
||||||
|
collectingPosArgs = false;
|
||||||
|
} else {
|
||||||
|
//argSnippets.push(`${param.name}=${'${' + (i + 1) + ':' + param.name + '}'}`);
|
||||||
|
argSnippets.push(`${param.name.replaceAll('$', '\\$')}=${'${' + (++i) + ':' + param.name + '}'}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
namedArgs.push(param.name);
|
||||||
|
}
|
||||||
|
if (namedArgs.length) {
|
||||||
|
argSnippets.push(`${'${' + (++i) + ':' + namedArgs.join('|') + '=}'}`);
|
||||||
|
}
|
||||||
|
let insertText = `${name.replaceAll('$', '\\$')}(${argSnippets.join(', ')})`;
|
||||||
|
if (mod.referencesChildren !== null) {
|
||||||
|
insertText += mod.referencesChildren ? ' ${' + (++i) + ':children}' : ';';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: mod.signature,//`${name}(${(mod.params ?? []).join(', ')})`,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Function,
|
||||||
|
insertText,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const builtinCompletions = [
|
||||||
|
...[true, false].map(v => ({
|
||||||
|
label: `${v}`,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Value,
|
||||||
|
insertText: `${v}`,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
|
||||||
|
})),
|
||||||
|
...openscadLanguage.language.keywords.map(v => ({
|
||||||
|
label: v,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Function,
|
||||||
|
insertText: v,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
const keywordSnippets = [
|
||||||
|
'for(${1:variable}=[${2:start}:${3:end}) ${4:body}',
|
||||||
|
'for(${1:variable}=[${2:start}:${3:increment}:${4:end}) ${5:body}',
|
||||||
|
'if (${1:condition}) {\n\t$0\n} else {\n\t\n}'
|
||||||
|
];
|
||||||
|
|
||||||
|
function cleanupVariables(snippet: string) {
|
||||||
|
return snippet
|
||||||
|
.replaceAll(/\$\{\d+:([$\w]+)\}/g, '$1')
|
||||||
|
.replaceAll(/\$\d+/g, '')
|
||||||
|
.replaceAll(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-custom-languages
|
||||||
|
export async function buildOpenSCADCompletionItemProvider(fs: FS, workingDir: string, zipArchives: ZipArchives) {
|
||||||
|
|
||||||
|
const parsedFiles: {[path: string]: Promise<ParsedFile>} = {};
|
||||||
|
const toAbsolutePath = (path: string) => path.startsWith('/') ? path : `${workingDir}/${path}`;
|
||||||
|
|
||||||
|
const allSymlinks: Symlinks = {};
|
||||||
|
for (const n of Object.keys(zipArchives)) {
|
||||||
|
if (n == 'fonts') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const { symlinks } = zipArchives[n];
|
||||||
|
for (const s in symlinks) {
|
||||||
|
allSymlinks[s] = `${n}/${symlinks[s]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function readFile(path: string) {
|
||||||
|
if (path in allSymlinks) {
|
||||||
|
path = allSymlinks[path];
|
||||||
|
}
|
||||||
|
path = toAbsolutePath(path);
|
||||||
|
try {
|
||||||
|
const bytes = await fs.readFileSync(path);
|
||||||
|
const src = new TextDecoder("utf-8").decode(bytes);
|
||||||
|
return src;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const builtinsPath = '<builtins>';
|
||||||
|
let builtinsDefs: ParsedFile;
|
||||||
|
|
||||||
|
function getParsed(path: string, src: string, {skipPrivates, addBuiltins}: {skipPrivates: boolean, addBuiltins: boolean}) {
|
||||||
|
return parsedFiles[path] ??= new Promise(async (res, rej) => {
|
||||||
|
if (src == null) {
|
||||||
|
src = await readFile(path);
|
||||||
|
}
|
||||||
|
const result: ParsedFile = {
|
||||||
|
functions: {},
|
||||||
|
modules: {},
|
||||||
|
vars: [],
|
||||||
|
includes: [],
|
||||||
|
uses: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeDefinitions = (isUse: boolean, defs: ParsedFile) => {
|
||||||
|
result.functions = {...result.functions, ...defs.functions }
|
||||||
|
result.modules = {...result.modules, ...defs.modules }
|
||||||
|
if (!isUse) {
|
||||||
|
result.vars = [...result.vars, ...defs.vars]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const dir = (path.split('/').slice(0, -1).join('/') || '.') + '/';
|
||||||
|
|
||||||
|
const handleInclude = async (isUse: boolean, otherPath: string) => {
|
||||||
|
for (const path of [`${dir}/${otherPath}`, otherPath]) {
|
||||||
|
try {
|
||||||
|
const otherSrc = await readFile(otherPath);
|
||||||
|
const sub = await getParsed(otherPath, otherSrc, {skipPrivates: true, addBuiltins: false});
|
||||||
|
mergeDefinitions(isUse, sub);
|
||||||
|
} catch (e) {
|
||||||
|
// console.warn(path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error('Failed to find ', otherPath, '(context imported in ', path, ')');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (addBuiltins && path != builtinsPath) {
|
||||||
|
mergeDefinitions(false, builtinsDefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownDefs = parseOpenSCAD(path, src, skipPrivates);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
...(ownDefs.uses ?? []).map(p => [p, true] as [string, boolean]),
|
||||||
|
...(ownDefs.includes ?? []).map(p => [p, false] as [string, boolean])
|
||||||
|
].map(([otherPath, isUse]) => handleInclude(isUse, otherPath)));
|
||||||
|
|
||||||
|
mergeDefinitions(false, ownDefs);
|
||||||
|
|
||||||
|
res(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
builtinsDefs = await getParsed(builtinsPath, builtinSignatures, {skipPrivates: false, addBuiltins: false});
|
||||||
|
|
||||||
|
return {
|
||||||
|
triggerCharacters: ["<", "/"], //, "\n"],
|
||||||
|
//provideCompletionItems: (async (model, position, context, token) => {
|
||||||
|
provideCompletionItems: ((async (model: monaco.editor.ITextModel, position: monaco.Position, context: monaco.languages.CompletionContext, token: monaco.CancellationToken) => {
|
||||||
|
try {
|
||||||
|
const {word} = model.getWordUntilPosition(position);
|
||||||
|
const offset = model.getOffsetAt(position);
|
||||||
|
const text = model.getValue();
|
||||||
|
let previous = text.substring(0, offset);
|
||||||
|
let i = previous.lastIndexOf('\n');
|
||||||
|
previous = previous.substring(i + 1);
|
||||||
|
|
||||||
|
const includeMatch = /\b(include|use)\s*<([^<>\n"]*)$/.exec(previous);
|
||||||
|
if (includeMatch) {
|
||||||
|
const prefix = includeMatch[2];
|
||||||
|
let folder, filePrefix, folderPrefix;
|
||||||
|
const i = prefix.lastIndexOf('/');
|
||||||
|
if (i < 0) {
|
||||||
|
folderPrefix = '';
|
||||||
|
filePrefix = prefix;
|
||||||
|
} else {
|
||||||
|
folderPrefix = prefix.substring(0, i);
|
||||||
|
filePrefix = prefix.substring(i + 1);
|
||||||
|
}
|
||||||
|
folder = workingDir + (folderPrefix == '' ? '' : '/' + folderPrefix);
|
||||||
|
let files = folderPrefix == '' ? [...Object.keys(allSymlinks)] : [];
|
||||||
|
try {
|
||||||
|
files = [...(await readDirAsArray(fs, folder) ?? []), ...files];
|
||||||
|
// console.log('readDir', folder, files);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestions = [];
|
||||||
|
for (const file of files) {
|
||||||
|
if (filePrefix != '' && !file.startsWith(filePrefix)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^(LICENSE.*|fonts)$/.test(file)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (folderPrefix == '' && (file in zipArchives) && zipArchives[file].symlinks) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const isFolder = !file.endsWith('.scad');
|
||||||
|
const completion = file + (isFolder ? '' : '>\n'); // don't append '/' as it's a useful trigger char
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
prefix,
|
||||||
|
folder,
|
||||||
|
filePrefix,
|
||||||
|
folderPrefix,
|
||||||
|
// files,
|
||||||
|
completion,
|
||||||
|
file,
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
|
suggestions.push({
|
||||||
|
label: file,
|
||||||
|
kind: isFolder ? monaco.languages.CompletionItemKind.Folder : monaco.languages.CompletionItemKind.File,
|
||||||
|
insertText: completion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
suggestions.sort();
|
||||||
|
|
||||||
|
return { suggestions };
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputFile = workingDir + "/foo.scad";
|
||||||
|
delete parsedFiles[inputFile];
|
||||||
|
const parsed = await getParsed(inputFile, text, {skipPrivates: false, addBuiltins: true});
|
||||||
|
// console.log("PARSED", JSON.stringify(parsed, null, 2));
|
||||||
|
|
||||||
|
type CompletionItem = monaco.languages.CompletionItem & {
|
||||||
|
range?: monaco.IRange
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousWithoutComments = stripComments(previous);
|
||||||
|
// console.log('previousWithoutComments', previousWithoutComments);
|
||||||
|
const statementMatch = /(^|.*?[{});]|>\s*\n)\s*([$\w]*)$/m.exec(previousWithoutComments);
|
||||||
|
if (statementMatch) {
|
||||||
|
const start = statementMatch[1];
|
||||||
|
const suggestions: CompletionItem[] = [
|
||||||
|
...builtinCompletions,
|
||||||
|
...mapObject(
|
||||||
|
parsed.modules ?? {},
|
||||||
|
(name, mod) => makeFunctionoidSuggestion(name, mod),
|
||||||
|
name => name.indexOf(word) >= 0),
|
||||||
|
...((parsed.vars ?? []).filter(name => name.indexOf(word) >= 0).map(name => ({
|
||||||
|
label: name,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Variable,
|
||||||
|
insertText: name.replaceAll('$', '\\$'),
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
|
||||||
|
}))),
|
||||||
|
...keywordSnippets.map(snippet => ({
|
||||||
|
label: cleanupVariables(snippet).replaceAll(/ body/g, ''),
|
||||||
|
kind: monaco.languages.CompletionItemKind.Keyword,
|
||||||
|
insertText: snippet,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
|
||||||
|
})),
|
||||||
|
// ...getStatementSuggestions().filter(s => start == '' || s.insertText.indexOf(start) >= 0)
|
||||||
|
];
|
||||||
|
suggestions.sort((a, b) => a.insertText.indexOf(start) - b.insertText.indexOf(start));
|
||||||
|
return { suggestions };
|
||||||
|
}
|
||||||
|
|
||||||
|
const allWithoutComments = stripComments(text);
|
||||||
|
|
||||||
|
const named: [string, CompletionItem][] = [
|
||||||
|
...mapObject(parsed.functions ?? {},
|
||||||
|
(name, mod) => [name, makeFunctionoidSuggestion(name, mod)],
|
||||||
|
name => name.indexOf(word) >= 0)
|
||||||
|
];
|
||||||
|
named.sort(([a], [b]) => a.indexOf(word) - b.indexOf(word));
|
||||||
|
// const suggestions = names.map(name => ({
|
||||||
|
// label: name,
|
||||||
|
// kind: monaco.languages.CompletionItemKind.Constant,
|
||||||
|
// insertText: name
|
||||||
|
// }));
|
||||||
|
|
||||||
|
const suggestions = named.map(([n, s]) => s as any as monaco.languages.CompletionItem);
|
||||||
|
return { suggestions };
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);//, (e as any).stackTrace);
|
||||||
|
return { suggestions: [] };
|
||||||
|
}
|
||||||
|
}) as any),
|
||||||
|
} as monaco.languages.CompletionItemProvider;
|
||||||
|
}
|
||||||
186
src/language/openscad-language.ts
Normal file
186
src/language/openscad-language.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
import FS from 'browserfs/dist/node/core/FS';
|
||||||
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
|
|
||||||
|
const builtInFunctionNames = [
|
||||||
|
'abs',
|
||||||
|
'acos', 'asin', 'atan', 'atan2', 'ceil',
|
||||||
|
'len', 'let', 'ln', 'log',
|
||||||
|
'lookup', 'max', 'min', 'sqrt', 'tan', 'rands',
|
||||||
|
'search', 'sign', 'sin', 'str', 'norm', 'pow',
|
||||||
|
'concat', 'cos', 'cross', 'floor', 'exp',
|
||||||
|
'chr',
|
||||||
|
];
|
||||||
|
const builtInModuleNames = [
|
||||||
|
'children',
|
||||||
|
'circle', 'color', 'cube', 'cylinder',
|
||||||
|
'diameter', 'difference', 'echo', 'extrude',
|
||||||
|
'for', 'function', 'hull', 'if', 'include',
|
||||||
|
'intersection_for', 'intersection', 'linear', 'minkowski', 'mirror', 'module', 'multmatrix',
|
||||||
|
'offset', 'polyhedron', 'projection', 'radius',
|
||||||
|
'render', 'resize', 'rotate', 'round', 'scale',
|
||||||
|
'sphere', 'square', 'surface', 'translate',
|
||||||
|
'union', 'use', 'value', 'version',
|
||||||
|
// 'center', 'width', 'height',
|
||||||
|
];
|
||||||
|
const builtInVarNames = [
|
||||||
|
'false', 'true', 'PI', 'undef', '$children',
|
||||||
|
'$fa', '$fn', '$fs', '$t', '$vpd', '$vpr', '$vpt',
|
||||||
|
]
|
||||||
|
|
||||||
|
var conf: monaco.languages.LanguageConfiguration = {
|
||||||
|
|
||||||
|
colorizedBracketPairs: [['{', '}'], ['(', ')'], ['[', ']']],
|
||||||
|
|
||||||
|
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
|
||||||
|
comments: {
|
||||||
|
lineComment: "//",
|
||||||
|
blockComment: ["/*", "*/"]
|
||||||
|
},
|
||||||
|
brackets: [
|
||||||
|
["{", "}"],
|
||||||
|
["[", "]"],
|
||||||
|
["(", ")"]
|
||||||
|
],
|
||||||
|
onEnterRules: [
|
||||||
|
{
|
||||||
|
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
|
||||||
|
afterText: /^\s*\*\/$/,
|
||||||
|
action: {
|
||||||
|
indentAction: monaco.languages.IndentAction.IndentOutdent,
|
||||||
|
appendText: " * "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
|
||||||
|
action: {
|
||||||
|
indentAction: monaco.languages.IndentAction.None,
|
||||||
|
appendText: " * "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
beforeText: /^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/,
|
||||||
|
action: {
|
||||||
|
indentAction: monaco.languages.IndentAction.None,
|
||||||
|
appendText: "* "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
beforeText: /^(\t|(\ \ ))*\ \*\/\s*$/,
|
||||||
|
action: {
|
||||||
|
indentAction: monaco.languages.IndentAction.None,
|
||||||
|
removeText: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
autoClosingPairs: [
|
||||||
|
{ open: "{", close: "}" },
|
||||||
|
{ open: "[", close: "]" },
|
||||||
|
{ open: "(", close: ")" },
|
||||||
|
{ open: '"', close: '"', notIn: ["string"] },
|
||||||
|
{ open: "'", close: "'", notIn: ["string", "comment"] },
|
||||||
|
{ open: "`", close: "`", notIn: ["string", "comment"] },
|
||||||
|
{ open: "/**", close: " */", notIn: ["string"] }
|
||||||
|
],
|
||||||
|
folding: {
|
||||||
|
markers: {
|
||||||
|
start: new RegExp("^\\s*//\\s*#?region\\b"),
|
||||||
|
end: new RegExp("^\\s*//\\s*#?endregion\\b")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var language: monaco.languages.IMonarchLanguage = {
|
||||||
|
defaultToken: "invalid",
|
||||||
|
tokenPostfix: ".js",
|
||||||
|
keywords: [...builtInFunctionNames, ...builtInModuleNames, ...builtInVarNames, 'each'],
|
||||||
|
typeKeywords: [],
|
||||||
|
operators: [
|
||||||
|
"<=",
|
||||||
|
">=",
|
||||||
|
"==",
|
||||||
|
"!=",
|
||||||
|
"=>",
|
||||||
|
"+",
|
||||||
|
"-",
|
||||||
|
"*",
|
||||||
|
"/",
|
||||||
|
"%",
|
||||||
|
"<<",
|
||||||
|
">>",
|
||||||
|
">>>",
|
||||||
|
"&",
|
||||||
|
"|",
|
||||||
|
"^",
|
||||||
|
"!",
|
||||||
|
"&&",
|
||||||
|
"||",
|
||||||
|
"?",
|
||||||
|
":",
|
||||||
|
"=",
|
||||||
|
],
|
||||||
|
symbols: /[=><!~?:&|+\-*\/\^%]+/,
|
||||||
|
escapes: /\\[abfnrtv\\"']/,
|
||||||
|
digits: /\d+/,
|
||||||
|
tokenizer: {
|
||||||
|
root: [[/[{}]/, "delimiter.bracket"], { include: "common" }],
|
||||||
|
common: [
|
||||||
|
[
|
||||||
|
/[a-z_$][\w$]*/,
|
||||||
|
{
|
||||||
|
cases: {
|
||||||
|
"@keywords": "keyword",
|
||||||
|
"@default": "identifier"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[/[A-Z][\w\$]*/, "type.identifier"],
|
||||||
|
{ include: "@whitespace" },
|
||||||
|
[/[()\[\]]/, "@brackets"],
|
||||||
|
[/[<>](?!@symbols)/, "@brackets"],
|
||||||
|
[/!(?=([^=]|$))/, "delimiter"],
|
||||||
|
[
|
||||||
|
/@symbols/,
|
||||||
|
{
|
||||||
|
cases: {
|
||||||
|
"@operators": "delimiter",
|
||||||
|
"@default": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[/(@digits)[eE]([\-+]?(@digits))?/, "number.float"],
|
||||||
|
[/(@digits)\.(@digits)([eE][\-+]?(@digits))?/, "number.float"],
|
||||||
|
[/(@digits)n?/, "number"],
|
||||||
|
[/[;,.]/, "delimiter"],
|
||||||
|
[/"([^"\\]|\\.)*$/, "string.invalid"],
|
||||||
|
[/'([^'\\]|\\.)*$/, "string.invalid"],
|
||||||
|
[/"/, "string", "@string_double"],
|
||||||
|
],
|
||||||
|
whitespace: [
|
||||||
|
[/[ \t\r\n]+/, ""],
|
||||||
|
[/\/\*/, "comment", "@comment"],
|
||||||
|
[/\/\/.*$/, "comment"]
|
||||||
|
],
|
||||||
|
comment: [
|
||||||
|
[/[^\/*]+/, "comment"],
|
||||||
|
[/\*\//, "comment", "@pop"],
|
||||||
|
[/[\/*]/, "comment"]
|
||||||
|
],
|
||||||
|
string_double: [
|
||||||
|
[/[^\\"]+/, "string"],
|
||||||
|
[/@escapes/, "string.escape"],
|
||||||
|
[/\\./, "string.escape.invalid"],
|
||||||
|
[/"/, "string", "@pop"]
|
||||||
|
],
|
||||||
|
bracketCounting: [
|
||||||
|
[/\{/, "delimiter.bracket", "@bracketCounting"],
|
||||||
|
[/\}/, "delimiter.bracket", "@pop"],
|
||||||
|
{ include: "common" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
conf,
|
||||||
|
language,
|
||||||
|
}
|
||||||
68
src/language/openscad-pseudoparser.ts
Normal file
68
src/language/openscad-pseudoparser.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
export type ParsedFunctionoidDef = {
|
||||||
|
path: string,
|
||||||
|
name: string,
|
||||||
|
params?: {
|
||||||
|
name: string,
|
||||||
|
defaultValue: string,
|
||||||
|
}[],
|
||||||
|
signature: string,
|
||||||
|
referencesChildren: boolean | null,
|
||||||
|
};
|
||||||
|
export type ParsedFunctionoidDefs = {[name: string]: ParsedFunctionoidDef};
|
||||||
|
|
||||||
|
export type ParsedFile = {
|
||||||
|
functions: ParsedFunctionoidDefs,
|
||||||
|
modules: ParsedFunctionoidDefs,
|
||||||
|
vars: string[],
|
||||||
|
includes: string[],
|
||||||
|
uses: string[],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stripComments = (src: string) => src.replaceAll(/\/\*(.|[\s\S])*?\*\/|\/\/.*?$/gm, '');
|
||||||
|
|
||||||
|
export function parseOpenSCAD(path: string, src: string, skipPrivates: boolean): ParsedFile {
|
||||||
|
const withoutComments = stripComments(src);
|
||||||
|
const vars = [];
|
||||||
|
const functions: ParsedFunctionoidDefs = {};
|
||||||
|
const modules: ParsedFunctionoidDefs = {};
|
||||||
|
const includes: string[] = [];
|
||||||
|
const uses: string[] = [];
|
||||||
|
for (const m of withoutComments.matchAll(/(use|include)\s*<([^>]+)>/g)) {
|
||||||
|
(m[1] == 'use' ? uses : includes).push(m[2]);
|
||||||
|
}
|
||||||
|
for (const m of withoutComments.matchAll(/(?:^|[{};])\s*([$\w]+)\s*=/g)) {
|
||||||
|
vars.push(m[1]);
|
||||||
|
}
|
||||||
|
for (const m of withoutComments.matchAll(/(function|module)\s+([$\w]+)\s*\(([^)]*)\)(?:\s*(?:=\s*)?(\{\}|[^{}]+?;))?/gm)) {
|
||||||
|
const type = m[1];
|
||||||
|
const name = m[2];
|
||||||
|
if (skipPrivates && name.startsWith('_')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const paramsStr = m[3];
|
||||||
|
const optBody = m[4];
|
||||||
|
const params = [];
|
||||||
|
if (/^(\s*([$\w]+(\s*=[^,()[]+)?(\s*,\s*[$\w]+(\s*=[^,()[]+)?)*)?\s*)$/m.test(paramsStr)) {
|
||||||
|
for (const paramStr of paramsStr.split(',')) {
|
||||||
|
const am = /^\s*([$\w]+)(?:\s*=([^,()[]+))?\s*$/.exec(paramStr);
|
||||||
|
if (am) {
|
||||||
|
const paramName = am[1];
|
||||||
|
const defaultValue = am[2];
|
||||||
|
params.push({
|
||||||
|
name: paramName,
|
||||||
|
defaultValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(type == 'function' ? functions : modules)[name] = {
|
||||||
|
path,
|
||||||
|
name,
|
||||||
|
signature: `${name}(${paramsStr.replaceAll(/[\s]+/gm, ' ').replaceAll(/\b | \b/g, '')})`,
|
||||||
|
params,
|
||||||
|
referencesChildren: optBody != null ? (optBody.indexOf('children()') >= 0) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {vars, functions, modules, includes, uses};
|
||||||
|
}
|
||||||
26
src/language/openscad-register-language.ts
Normal file
26
src/language/openscad-register-language.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
|
||||||
|
import loader from '@monaco-editor/loader';
|
||||||
|
import { ZipArchives } from '../zip-archives';
|
||||||
|
import { buildOpenSCADCompletionItemProvider } from './openscad-completions';
|
||||||
|
import openscadLanguage from './openscad-language';
|
||||||
|
// import jsLanguage from 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution'
|
||||||
|
|
||||||
|
// https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-custom-languages
|
||||||
|
export async function registerOpenSCADLanguage(fs: any, workingDir: string, zipArchives: ZipArchives) {
|
||||||
|
const monaco = await loader.init();
|
||||||
|
|
||||||
|
monaco.languages.register({
|
||||||
|
id: 'openscad',
|
||||||
|
extensions: ['.scad'],
|
||||||
|
mimetypes: ["text/openscad"],
|
||||||
|
});
|
||||||
|
|
||||||
|
//const [inheritLanguage] = monaco.languages.getLanguages().filter(l => l.id === 'javascript');
|
||||||
|
const { conf, language } = openscadLanguage;
|
||||||
|
monaco.languages.setLanguageConfiguration('openscad', conf);
|
||||||
|
monaco.languages.setMonarchTokensProvider('openscad', language);
|
||||||
|
|
||||||
|
monaco.languages.registerCompletionItemProvider('openscad',
|
||||||
|
await buildOpenSCADCompletionItemProvider(fs, workingDir, zipArchives));
|
||||||
|
}
|
||||||
12
src/utils.ts
Normal file
12
src/utils.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
|
||||||
|
export function mapObject(o: any, f: (key: string, value: any) => any, ifPred: (key: string) => boolean) {
|
||||||
|
const ret = [];
|
||||||
|
for (const key of Object.keys(o)) {
|
||||||
|
if (ifPred && !ifPred(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ret.push(f(key, o[key]));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
50
src/zip-archives.ts
Normal file
50
src/zip-archives.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
|
||||||
|
|
||||||
|
import { Symlinks } from "./filesystem";
|
||||||
|
|
||||||
|
export type ZipArchives = {
|
||||||
|
[name: string]: {
|
||||||
|
symlinks?: Symlinks
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const zipArchives: ZipArchives = {
|
||||||
|
'fonts': {},
|
||||||
|
// @openscad
|
||||||
|
'MCAD': {},
|
||||||
|
// @revarbat
|
||||||
|
'BOSL': {},
|
||||||
|
'BOSL2': {
|
||||||
|
// "includes": {
|
||||||
|
// "BOSL2/std.scad": "The Belfry OpenScad Library, v2.0. An OpenSCAD library of shapes, masks, and manipulators to make working with OpenSCAD easier. BETA"
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
// @nophead
|
||||||
|
'NopSCADlib': {},
|
||||||
|
// @thehans
|
||||||
|
'FunctionalOpenSCAD': {},
|
||||||
|
'funcutils': {},
|
||||||
|
// @colyer
|
||||||
|
'smooth-prim': {
|
||||||
|
symlinks: {'smooth_prim.scad': 'smooth_prim.scad'},
|
||||||
|
},
|
||||||
|
'closepoints': {
|
||||||
|
symlinks: {'closepoints.scad': 'closepoints.scad'},
|
||||||
|
},
|
||||||
|
'plot-function': {
|
||||||
|
symlinks: {'plot_function.scad': 'plot_function.scad'},
|
||||||
|
},
|
||||||
|
// 'threads': {},
|
||||||
|
// @sofian
|
||||||
|
'openscad-tray': {
|
||||||
|
symlinks: {'tray.scad': 'tray.scad'},
|
||||||
|
},
|
||||||
|
// @mrWheel
|
||||||
|
'YAPP_Box': {},
|
||||||
|
// @Cantareus
|
||||||
|
'Stemfie_OpenSCAD': {},
|
||||||
|
// @UBaer21
|
||||||
|
'UB.scad': {
|
||||||
|
symlinks: {"ub.scad": "libraries/ub.scad"},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2021",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"esnext",
|
"ES2021",
|
||||||
"WebWorker",
|
"WebWorker",
|
||||||
],
|
],
|
||||||
"outDir": "js",
|
"outDir": "js",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue