Add react-stl-viewer + fix preview vs. render flow

This commit is contained in:
ochafik 2023-03-25 15:23:54 +00:00
commit 42d0d0a5d2
17 changed files with 313 additions and 109 deletions

View file

@ -15,6 +15,7 @@ The WASM build was made possible by https://github.com/DSchroer/openscad-wasm.
* [Monaco Editor](#monaco-editor) * [Monaco Editor](#monaco-editor)
* [Viewstl Plugin](#viewstl-plugin) * [Viewstl Plugin](#viewstl-plugin)
* [React Stl Viewer](#react-stl-viewer)
* [Three.js](#threejs) * [Three.js](#threejs)
* [Boost](#boost) * [Boost](#boost)
* [GNU MPFR](#gnu-mpfr) * [GNU MPFR](#gnu-mpfr)
@ -106,6 +107,22 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
``` ```
## React STL Viewer
https://github.com/gabotechs/react-stl-viewer
```
MIT License
Copyright (c) 2022 Gabriel Musat Mestre
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
## Three.js ## Three.js
Used for the 3D visualization Used for the 3D visualization

View file

@ -15,6 +15,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-stl-viewer": "^2.2.5",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
@ -49,7 +50,6 @@
"rollup": "^2.79.1", "rollup": "^2.79.1",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
"tslib": "^2.5.0", "tslib": "^2.5.0",
"typescript": "^5.0.2",
"webpack-cli": "^5.0.1" "webpack-cli": "^5.0.1"
} }
} }

8
public/browserfs.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -3,8 +3,8 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/BrowserFS/2.0.0/browserfs.min.js"></script> <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/BrowserFS/2.0.0/browserfs.min.js" defer></script> -->
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/BrowserFS/2.0.0/browserfs.js"></script> --> <script src="browserfs.min.js"></script>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
@ -27,6 +27,17 @@
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>OpenSCAD Playground</title> <title>OpenSCAD Playground</title>
<style>
#root,
body {
display: flex;
flex-direction: column;
flex: 1;
margin: 0;
width: 100vw;
height: 100vh;
}
</style>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run the OpenSCAD Playground.</noscript> <noscript>You need to enable JavaScript to run the OpenSCAD Playground.</noscript>

View file

@ -36,3 +36,10 @@
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.logs-container {
overflow-y: scroll;
height: calc(min(200px, 30vh));
position: relative;
}

View file

@ -1,55 +1,103 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING. // Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import React, { useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import {ModelContext, State} from './app-state' import {ModelContext, State} from './app-state'
import Editor, { loader, Monaco } from '@monaco-editor/react'; import Editor, { loader, Monaco } from '@monaco-editor/react';
import './App.css'; import './App.css';
import openscadEditorOptions from './language/openscad-editor-options'; import openscadEditorOptions from './language/openscad-editor-options';
import { Model } from './model'; import { Model } from './model';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import {StlViewer} from "react-stl-viewer";
let monacoInstance: Monaco let monacoInstance: Monaco
loader.init().then(mi => monacoInstance = mi); loader.init().then(mi => monacoInstance = mi);
function EditorPanel() {
const model = useContext(ModelContext);
const [editor, setEditor] = useState(null as monaco.editor.IStandaloneCodeEditor | null)
if (editor) {
const checkerRun = model.state.lastCheckerRun;
const editorModel = editor.getModel();
if (editorModel && checkerRun) {
monacoInstance.editor.setModelMarkers(editorModel, 'openscad', checkerRun.markers);
}
}
const onMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
editor.addAction({
id: "openscad-render",
label: "Render OpenSCAD",
keybindings: [
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
monaco.KeyCode.F6,
],
run: () => model.render()
});
setEditor(editor)
}
return (
<div className="editor-panel" style={{
display: 'flex',
flexDirection: 'column',
position: 'relative'
}}>
<Editor
className="openscad-editor"
defaultLanguage="openscad"
value={model.state.params.source}
onChange={s => model.source = s ?? ''}
onMount={onMount} // TODO: This looks a bit silly, does it trigger a re-render??
options={openscadEditorOptions}
height="50vh"/>
<div className="logs-container">
<pre><code id="logs">{model.state.lastCheckerRun?.logText ?? 'No log yet!'}</code></pre>
</div>
</div>
)
}
export function App({initialState}: {initialState: State}) { export function App({initialState}: {initialState: State}) {
const [state, setState] = useState(initialState); const [state, setState] = useState(initialState);
const [editor, setEditor] = useState(null as monaco.editor.IStandaloneCodeEditor | null) const [editor, setEditor] = useState(null as monaco.editor.IStandaloneCodeEditor | null)
const model = new Model(state, setState);
if (editor) { if (editor) {
const editorModel = editor.getModel(); const editorModel = editor.getModel();
if (editorModel && state.checkerRun) { if (editorModel && state.lastCheckerRun) {
monacoInstance.editor.setModelMarkers(editorModel, 'openscad', state.checkerRun.markers); monacoInstance.editor.setModelMarkers(editorModel, 'openscad', state.lastCheckerRun.markers);
} }
} }
const source = model.source; const model = new Model(state, setState);
useEffect(() => model.init());
return ( return (
<ModelContext.Provider value={model}> <ModelContext.Provider value={model}>
<div className="App"> <div style={{display: 'flex', flexDirection: 'column', flex: 1}}>
<header className="App-header"> <div style={{display: 'flex', flexDirection: 'row', flex: 1}}>
<img src="logo.png" className="App-logo" alt="logo" /> <div style={{width: "50vw"}}>
<p> <EditorPanel/>
Edit <code>src/App.tsx</code> and save to reload. </div>
</p> {state.output?.stlFileURL &&
<a <StlViewer
className="App-link" style={{
href="https://openscad.org" flex: 1
target="_blank" }}
rel="noopener noreferrer" showAxes={true}
> orbitControls
Learn OpenSCAD shadows
</a> url={state.output?.stlFileURL ?? ''}
<Editor />
className="openscad-editor" }
defaultLanguage="openscad" </div>
value={source} <div style={{display: 'flex', flexDirection: 'row'}}>
onChange={s => model.source = s ?? ''} <button onClick={() => model.render()}>Render</button>
onMount={e => setEditor(e)} // TODO: This looks a bit silly, does it trigger a re-render?? {model.state.previewing && 'previewing... '}
options={openscadEditorOptions} {model.state.rendering && 'rendering... '}
height="50vh"/> {model.state.checkingSyntax && 'checking syntax... '}
</header> </div>
</div> </div>
</ModelContext.Provider> </ModelContext.Provider>
); );

View file

@ -1,12 +1,15 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING. // Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import { FS } from "./filesystem"; // import { FS } from "./filesystem";
interface EmscriptenFS extends FS { declare interface FS {
readdir(path: string, cb: (err: any, files: string[]) => void): void;
symlink(target: string, source: string): void;
}
}; declare interface EmscriptenFS extends FS {}
export let BrowserFS = (window as any)['BrowserFS'] as { declare type BrowserFSInterface = {
BFSRequire: (name: string) => any, BFSRequire: (name: string) => any,
install: (windowOrSelf: Window) => void, install: (windowOrSelf: Window) => void,

View file

@ -2,19 +2,22 @@
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { spawnOpenSCAD } from "./openscad-runner"; import { spawnOpenSCAD } from "./openscad-runner";
import { joinMergedOutputs, parseMergedOutputs } from "./output-parser"; import { processMergedOutputs } from "./output-parser";
import { AbortablePromise, turnIntoDelayableExecution } from './utils'; import { AbortablePromise, turnIntoDelayableExecution } from './utils';
const syntaxDelay = 300; const syntaxDelay = 300;
type SyntaxCheckOutput = {logText: string, markers: monaco.editor.IMarkerData[]}; type SyntaxCheckOutput = {logText: string, markers: monaco.editor.IMarkerData[]};
export const checkSyntax = (source: string, callback: (out: SyntaxCheckOutput) => void) => export const checkSyntax =
turnIntoDelayableExecution(syntaxDelay, () => { turnIntoDelayableExecution(syntaxDelay, (source: string) => {
// const timestamp = Date.now(); // const timestamp = Date.now();
source = '$preview=true;\n' + source;
const sourceFile = 'input.scad';
const job = spawnOpenSCAD({ const job = spawnOpenSCAD({
inputs: [['input.scad', source + '\n']], inputs: [[sourceFile, source + '\n']],
args: ["input.scad", "-o", "out.ast"], args: [sourceFile, "-o", "out.ast"],
}); });
return AbortablePromise<SyntaxCheckOutput>((res, rej) => { return AbortablePromise<SyntaxCheckOutput>((res, rej) => {
@ -22,9 +25,7 @@ export const checkSyntax = (source: string, callback: (out: SyntaxCheckOutput) =
try { try {
const result = await job; const result = await job;
// console.log(result); // console.log(result);
const logText = joinMergedOutputs(result.mergedOutputs); res(processMergedOutputs(result.mergedOutputs, {shiftSourceLines: {[sourceFile]: 1}}));
const markers = parseMergedOutputs(result.mergedOutputs);
res({logText, markers});
} catch (e) { } catch (e) {
console.error(e); console.error(e);
rej(e); rej(e);
@ -32,25 +33,34 @@ export const checkSyntax = (source: string, callback: (out: SyntaxCheckOutput) =
})() })()
return () => job.kill(); return () => job.kill();
}); });
}, callback); });
var sourceFileName;
// var editor;
var renderDelay = 1000; var renderDelay = 1000;
type RenderOutput = {stlFile: File, logText: string, markers: monaco.editor.IMarkerData[], elapsedMillis?: number} export type RenderOutput = {stlFile: File, logText: string, markers: monaco.editor.IMarkerData[], elapsedMillis?: number}
export const render = (source: string, features: string[], callback: (result: RenderOutput) => void) => export type RenderArgs = {
turnIntoDelayableExecution(renderDelay, () => { source: string,
features?: string[],
extraArgs?: string[],
isPreview?: boolean
}
export const render =
turnIntoDelayableExecution(renderDelay, (args: RenderArgs) => {
const prefixLines: string[] = [];
if (args.isPreview) prefixLines.push('$preview=false;');
const source = args.isPreview ? [...prefixLines, args.source].join('\n') : args.source
const inputFile = 'input.scad';
const job = spawnOpenSCAD({ const job = spawnOpenSCAD({
// wasmMemory, // wasmMemory,
inputs: [['input.scad', source]], inputs: [['input.scad', source]],
args: [ args: [
"input.scad", inputFile,
"-o", "out.stl", "-o", "out.stl",
"--export-format=binstl", "--export-format=binstl",
...features.map(f => `--enable=${f}`), ...(args.features ?? []).map(f => `--enable=${f}`),
...(args.extraArgs ?? [])
], ],
outputPaths: ['out.stl'] outputPaths: ['out.stl']
}); });
@ -61,8 +71,9 @@ export const render = (source: string, features: string[], callback: (result: Re
const result = await job; const result = await job;
console.log(result); console.log(result);
const logText = joinMergedOutputs(result.mergedOutputs); const {logText, markers} = processMergedOutputs(result.mergedOutputs, {
const markers = parseMergedOutputs(result.mergedOutputs); shiftSourceLines: {[inputFile]: prefixLines.length}
});
if (result.error) { if (result.error) {
reject(result.error); reject(result.error);
@ -87,4 +98,4 @@ export const render = (source: string, features: string[], callback: (result: Re
return () => job.kill() return () => job.kill()
}); });
}, callback); });

View file

@ -8,23 +8,27 @@ import { Model } from "./model"
export interface State { export interface State {
params: { params: {
source: string, source: string,
features: string[],
}, },
checkerRun?: { lastCheckerRun?: {
logText: string, logText: string,
markers: monaco.editor.IMarkerData[] markers: monaco.editor.IMarkerData[]
} }
rendering?: boolean,
previewing?: boolean,
checkingSyntax?: boolean,
output?: { output?: {
path: string, stlFile: File,
timestamp: number, stlFileURL: string,
sizeBytes: number,
formattedSize: string,
}, },
}; };
export const ModelContext = createContext(new Model( export const ModelContext = createContext(new Model(
{ {
params: { params: {
source: '' source: '',
features: ['manifold', 'lazy-union'],
} }
}, },
() => { throw new Error('Not implemented'); } () => { throw new Error('Not implemented'); }

View file

@ -1,18 +1,19 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING. // Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import { State } from "./app-state"; import { State } from "./app-state";
import { validateString } from "./utils"; import { validateArray, validateString } from "./utils";
export function writeStateInFragment(state: State) { export function writeStateInFragment(state: State) {
window.location.hash = encodeURIComponent(JSON.stringify(state)); window.location.hash = encodeURIComponent(JSON.stringify(state.params));
} }
export function readStateFromFragment(): State | null { export function readStateFromFragment(): State | null {
if (window.location.hash.startsWith('#') && window.location.hash.length > 1) { if (window.location.hash.startsWith('#') && window.location.hash.length > 1) {
try { try {
const state = JSON.parse(decodeURIComponent(window.location.hash.substring(1))); const params = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
return { return {
params: { params: {
source: validateString(state.params?.source), source: validateString(params?.source),
features: validateArray(params?.features, validateString),
}, },
}; };
} catch (e) { } catch (e) {

View file

@ -2,7 +2,6 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import './index.css';
import {App} from './App'; import {App} from './App';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import { createEditorFS } from './filesystem'; import { createEditorFS } from './filesystem';
@ -10,6 +9,7 @@ import { registerOpenSCADLanguage } from './language/openscad-register-language'
import { zipArchives } from './zip-archives'; import { zipArchives } from './zip-archives';
import {readStateFromFragment} from './fragment-state' import {readStateFromFragment} from './fragment-state'
import { State } from './app-state'; import { State } from './app-state';
import './index.css';
(async () => { (async () => {
@ -23,6 +23,12 @@ import { State } from './app-state';
} }
} as State; } as State;
const defaultFeatures = ['manifold', 'fast-csg', 'lazy-union'];
defaultFeatures.forEach(f => {
if (initialState.params.features.indexOf(f) < 0)
initialState.params.features.push(f);
});
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
); );

View file

@ -1,4 +1,5 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING. // Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
export default ` export default `
$fa=undef; $fa=undef;
$fs=undef; $fs=undef;

View file

@ -1,7 +1,7 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING. // Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { FS, readDirAsArray, Symlinks } from '../filesystem'; import { readDirAsArray, Symlinks } from '../filesystem';
import { ParsedFile, ParsedFunctionoidDef, parseOpenSCAD, stripComments } from './openscad-pseudoparser'; import { ParsedFile, ParsedFunctionoidDef, parseOpenSCAD, stripComments } from './openscad-pseudoparser';
import builtinSignatures from './openscad-builtins' import builtinSignatures from './openscad-builtins'
import { mapObject } from '../utils'; import { mapObject } from '../utils';

View file

@ -1,25 +1,32 @@
// Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING. // Portions of this file are Copyright 2021 Google LLC, and licensed under GPL2+. See COPYING.
import { checkSyntax } from "./actions"; import { checkSyntax, render, RenderArgs, RenderOutput } from "./actions";
import { State } from "./app-state"; import { State } from "./app-state";
import { bubbleUpDeepMutations } from "./deep-mutate"; import { bubbleUpDeepMutations } from "./deep-mutate";
import { writeStateInFragment } from "./fragment-state"; import { writeStateInFragment } from "./fragment-state";
export class Model { export class Model {
constructor(private state: State, private setState_: (state: State) => void) {} constructor(public state: State, private setStateCallback?: (state: State) => void) {
}
init() {
if (!this.state.output && !this.state.lastCheckerRun && !this.state.previewing && !this.state.checkingSyntax && !this.state.rendering &&
this.state.params.source.trim() != '') {
this.processSource();
}
}
private setState(state: State) { private setState(state: State) {
this.state = state; this.state = state;
this.setState_(state);
writeStateInFragment(state); writeStateInFragment(state);
this.setStateCallback && this.setStateCallback(state);
} }
mutate(f: (state: State) => void) { mutate(f: (state: State) => void) {
const state = this.state; const mutated = bubbleUpDeepMutations(this.state, f);
const mutated = bubbleUpDeepMutations(state, f);
// No matter how deep the mutation happened, the top-level object's identity // No matter how deep the mutation happened, the top-level object's identity
// will have changed iff the mutated values are different. // will have changed iff the mutated values are different.
if (mutated !== state) { if (mutated !== this.state) {
this.setState(mutated); this.setState(mutated);
return true; return true;
} }
@ -27,16 +34,73 @@ export class Model {
return false; return false;
} }
get source(): string { // get features() { return this.state_.params.features; }
return this.state.params.source;//.source, this.state.editor;//params.source.content;
}
// get source(): string { return this.state_.params.source; }
set source(source) { set source(source: string) {
if (this.mutate(s => s.params.source = source)) { if (this.mutate(s => { s.params.source = source; })) {
checkSyntax(source, checkerRun => this.mutate(s => s.checkerRun = checkerRun))({now: false}); this.processSource();
} }
} }
private processSource() {
this.mutate(s => {
s.previewing = true;
s.checkingSyntax = true;
});
checkSyntax(this.state.params.source)({now: false, callback: checkerRun => this.mutate(s => {
s.lastCheckerRun = checkerRun;
s.checkingSyntax = false;
})});
render({...this.renderArgs, isPreview: true})({now: false, callback: output => this.handleRenderOutput(output, s => {
s.previewing = false;
})});
}
// checkSyntax() {
// this.mutate(s => s.checkingSyntax = true);
// checkSyntax(this.state.params.source)({now: false, callback: checkerRun => this.mutate(s => {
// s.lastCheckerRun = checkerRun;
// s.checkingSyntax = false;
// })});
// }
// preview() {
// this.mutate(s => s.previewing = true);
// render({...this.renderArgs, isPreview: true})({now: false, callback: output => this.handleRenderOutput(output, s => {
// s.previewing = false;
// })});
// }
private handleRenderOutput(output: RenderOutput, extraMutations: (s: State) => void) {
this.mutate(s => {
s.lastCheckerRun = {
logText: output.logText,
markers: output.markers,
}
if (s.output?.stlFileURL) {
URL.revokeObjectURL(s.output.stlFileURL);
}
s.output = {
stlFile: output.stlFile,
stlFileURL: URL.createObjectURL(output.stlFile),
};
extraMutations(s);
});
}
private get renderArgs(): RenderArgs {
const source = this.state.params.source;
const features = this.state.params.features;
return {source, features, extraArgs: ['-D$preview=true']};
}
render() {
this.mutate(s => s.rendering = true);
render(this.renderArgs)({now: true, callback: output => this.handleRenderOutput(output, s => {
s.rendering = false;
})})
}
} }

View file

@ -1,16 +1,32 @@
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { MergedOutputs } from "./openscad-worker"; import { MergedOutputs } from "./openscad-worker";
export function joinMergedOutputs(mergedOutputs: MergedOutputs) { const ignoredLogs = new Set([
'Could not initialize localization.'
]);
type MergedOutputsOptions = {
shiftSourceLines: {[path: string]: number}
}
export const processMergedOutputs = (outputs: MergedOutputs, opts: MergedOutputsOptions) => ({
logText: joinMergedOutputs(outputs, opts),
markers: parseMergedOutputs(outputs, opts)
});
export function joinMergedOutputs(mergedOutputs: MergedOutputs, opts: MergedOutputsOptions) {
let allLines = []; let allLines = [];
for (const {stderr, stdout, error} of mergedOutputs){ for (const {stderr, stdout, error} of mergedOutputs){
allLines.push(stderr ?? stdout ?? `EXCEPTION: ${error}`); const line = stderr ?? stdout ?? `EXCEPTION: ${error}`;
if (ignoredLogs.has(line)) {
continue;
}
allLines.push(line);
} }
return allLines.join("\n"); return allLines.join("\n");
} }
export function parseMergedOutputs(mergedOutputs: MergedOutputs): monaco.editor.IMarkerData[] { export function parseMergedOutputs(mergedOutputs: MergedOutputs, opts: MergedOutputsOptions): monaco.editor.IMarkerData[] {
let unmatchedLines = []; let unmatchedLines = [];
const markers = []; const markers = [];
@ -25,6 +41,10 @@ export function parseMergedOutputs(mergedOutputs: MergedOutputs): monaco.editor.
severity: monaco.MarkerSeverity.Error severity: monaco.MarkerSeverity.Error
}) })
} }
const getLine = (path: string, lineStr: string) => {
const shift = opts.shiftSourceLines[path] ?? 0;
return Number(lineStr) - shift;
}
for (const {stderr, stdout, error} of mergedOutputs){ for (const {stderr, stdout, error} of mergedOutputs){
if (stderr) { if (stderr) {
if (stderr.startsWith('ERROR:')) errorCount++; if (stderr.startsWith('ERROR:')) errorCount++;
@ -33,14 +53,14 @@ export function parseMergedOutputs(mergedOutputs: MergedOutputs): monaco.editor.
let m = /^ERROR: Parser error in file "([^"]+)", line (\d+): (.*)$/.exec(stderr) let m = /^ERROR: Parser error in file "([^"]+)", line (\d+): (.*)$/.exec(stderr)
if (m) { if (m) {
const [_, file, line, error] = m const [_, file, line, error] = m
addError(error, file, Number(line)); addError(error, file, getLine(file, line));
continue; continue;
} }
m = /^ERROR: Parser error: (.*?) in file ([^",]+), line (\d+)$/.exec(stderr) m = /^ERROR: Parser error: (.*?) in file ([^",]+), line (\d+)$/.exec(stderr)
if (m) { if (m) {
const [_, error, file, line] = m const [_, error, file, line] = m
addError(error, file, Number(line)); addError(error, file, getLine(file, line));
continue; continue;
} }
@ -48,9 +68,9 @@ export function parseMergedOutputs(mergedOutputs: MergedOutputs): monaco.editor.
if (m) { if (m) {
const [_, warning, file, line] = m const [_, warning, file, line] = m
markers.push({ markers.push({
startLineNumber: Number(line), startLineNumber: getLine(file, line),
startColumn: 1, startColumn: 1,
endLineNumber: Number(line), endLineNumber: getLine(file, line),
endColumn: 1000, endColumn: 1000,
message: warning, message: warning,
severity: monaco.MarkerSeverity.Warning severity: monaco.MarkerSeverity.Warning

View file

@ -22,25 +22,27 @@ export function AbortablePromise<T>(f: (resolve: (result: T) => void, reject: (e
return Object.assign(promise, {kill: kill!}); return Object.assign(promise, {kill: kill!});
} }
// <T extends any[]>(...args: T)
export function turnIntoDelayableExecution<T>(delay: number, job: () => AbortablePromise<T>, callback: (result: T) => void) { export function turnIntoDelayableExecution<T extends any[], R>(
delay: number,
job: (...args: T) => AbortablePromise<R>) {
let pendingId: number | null; let pendingId: number | null;
let runningJobKillSignal: (() => void) | null; let runningJobKillSignal: (() => void) | null;
const doExecute = async () => { return (...args: T) => async ({now, callback}: {now: boolean, callback: (result: R) => void}) => {
if (runningJobKillSignal) { const doExecute = async () => {
runningJobKillSignal(); if (runningJobKillSignal) {
runningJobKillSignal = null; runningJobKillSignal();
runningJobKillSignal = null;
}
const abortablePromise = job(...args);
runningJobKillSignal = abortablePromise.kill;
try {
callback(await abortablePromise);
} finally {
runningJobKillSignal = null;
}
} }
const abortablePromise = job();
runningJobKillSignal = abortablePromise.kill;
try {
callback(await abortablePromise);
} finally {
runningJobKillSignal = null;
}
}
return async ({now}: {now: boolean}) => {
if (pendingId) { if (pendingId) {
clearTimeout(pendingId); clearTimeout(pendingId);
pendingId = null; pendingId = null;
@ -54,7 +56,7 @@ export function turnIntoDelayableExecution<T>(delay: number, job: () => Abortabl
} }
export const validateString = (s: string, orElse: () => string = () => '') => s != null && typeof s === 'string' ? s : orElse(); export const validateString = (s: string, orElse: () => string = () => '') => s != null && typeof s === 'string' ? s : orElse();
export const validateArray = <T>(a: Array<T>, validateElement: (e: T) => T, orElse: () => T[]) => { export const validateArray = <T>(a: Array<T>, validateElement: (e: T) => T, orElse: () => T[] = () => []) => {
if (!(a instanceof Array)) return orElse(); if (!(a instanceof Array)) return orElse();
return a.map(validateElement); return a.map(validateElement);
} }

View file

@ -7,6 +7,7 @@
"ES2021", "ES2021",
"WebWorker", "WebWorker",
], ],
"rootDir": "src",
"outDir": "js", "outDir": "js",
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
@ -19,7 +20,7 @@
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": false,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": [ "include": [