From 6134a8c89ca1b87198f161516ba5419c181d4b7b Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Sun, 15 Aug 2021 10:10:01 -0500 Subject: [PATCH] scripting: working on notebook, functions, files, setItem(), fixed tests --- css/ui.css | 15 +- package.json | 2 +- presets/script/mandel.js | 47 +++++ src/common/emu.ts | 50 ----- src/common/script/env.ts | 162 ++++++++++++--- src/common/script/lib/bitmap.ts | 331 ++++++++++++++++++++++++------ src/common/script/lib/color.ts | 87 ++++++++ src/common/script/lib/io.ts | 101 ++++++--- src/common/script/lib/scriptui.ts | 30 +++ src/common/script/ui/notebook.ts | 254 ++++++++++++++++++----- src/common/toolbar.ts | 53 +++++ src/common/workertypes.ts | 39 ++-- src/ide/pixeleditor.ts | 2 +- src/ide/project.ts | 27 ++- src/ide/ui.ts | 3 +- src/ide/waveform.ts | 2 +- src/platform/script.ts | 9 +- src/worker/tools/script.ts | 23 ++- src/worker/workermain.ts | 36 +++- test/cli/testpixelconvert.js | 2 + test/cli/testplatforms.js | 6 +- test/cli/testworker.js | 9 +- 22 files changed, 1005 insertions(+), 285 deletions(-) create mode 100644 presets/script/mandel.js create mode 100644 src/common/script/lib/color.ts create mode 100644 src/common/script/lib/scriptui.ts create mode 100644 src/common/toolbar.ts diff --git a/css/ui.css b/css/ui.css index 9268e548..8fd934d5 100644 --- a/css/ui.css +++ b/css/ui.css @@ -773,25 +773,23 @@ div.asset_toolbar { background: #444; color: #99cc99; padding: 0.5em; - margin: 0.5em; + margin-right: 0.5em; font-family: "Andale Mono", "Menlo", "Lucida Console", monospace; word-wrap: break-word; + pointer-events: auto; } .scripting-cell canvas { - height: 15vw; + height: 20vw; + max-width: 95%; border: 2px solid #222; outline-color: #ccc; background: #000; padding: 6px; margin: 6px; - pointer-events:auto; } -.scripting-cell canvas:focus { +.scripting-cell canvas:hover { outline:none; - border-color:#888; -} -.scripting-cell div { - display: inline; + border-color:#ccc; } div.scripting-color { padding:0.1em; @@ -800,4 +798,5 @@ div.scripting-color { div.scripting-grid { display: grid; grid-template-columns: repeat( auto-fit, minmax(2em, 1fr) ); + justify-items: center; } \ No newline at end of file diff --git a/package.json b/package.json index 87b42a1b..47a9543a 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "tsbuild": "tsc --build tsconfig.json", "esbuild": "npm run esbuild-worker && npm run esbuild-ui", "esbuild-clean": "rm -f ./gen/*.*", - "esbuild-worker": "esbuild src/worker/workermain.ts --bundle --minify --sourcemap --target=es2017 --outfile=./gen/worker/bundle.js", + "esbuild-worker": "esbuild src/worker/workermain.ts --bundle --sourcemap --target=es2017 --outfile=./gen/worker/bundle.js", "esbuild-ui": "esbuild src/ide/ui.ts src/ide/embedui.ts --splitting --format=esm --bundle --minify --sourcemap --target=es2017 --outdir=./gen/ --external:path --external:fs", "test-one": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000", "test-node": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000 test/cli", diff --git a/presets/script/mandel.js b/presets/script/mandel.js new file mode 100644 index 00000000..c155894f --- /dev/null +++ b/presets/script/mandel.js @@ -0,0 +1,47 @@ + +// slider UI objects +mx = ui.slider(0,2000).initial(1028) +my = ui.slider(0,2000).initial(1409) +ms = ui.slider(0,2000).initial(615) + +// compute slider-derived values +xofs = (mx.value-1000)/500 +yofs = (my.value-1000)/500 +zoom = Math.pow(0.99, ms.value) + +// create bitmap +fract = bitmap.indexed(512,256,8) + +// compute fractal using assign() +fract.assign(mandel) + +// mandelbrot pixel function +function mandel(x,y) { + return iterateEquation( + (x-fract.width/2)*zoom-xofs, + (y-fract.height/2)*zoom-yofs, 10, 256)[0]; +} + +// mandelbrot compute function +function iterateEquation(Cr, Ci, escapeRadius, iterations) +{ + var Zr = 0; + var Zi = 0; + var Tr = 0; + var Ti = 0; + var n = 0; + for ( ; n void) { - var btn = null; - if (icon) { - btn = $(document.createElement("button")).addClass("btn"); - if (icon.startsWith('glyphicon')) { - icon = ''; - } - btn.html(icon); - btn.prop("title", key ? (alttext+" ("+key+")") : alttext); - btn.click(fn); - this.grp.append(btn).show(); - } - if (key) { - this.mousetrap.bind(key, fn); - this.boundkeys.push(key); - } - return btn; - } - -} // https://stackoverflow.com/questions/17130395/real-mouse-position-in-canvas export function getMousePos(canvas : HTMLCanvasElement, evt) : {x:number,y:number} { diff --git a/src/common/script/env.ts b/src/common/script/env.ts index 464f9ce6..277034bf 100644 --- a/src/common/script/env.ts +++ b/src/common/script/env.ts @@ -1,23 +1,32 @@ + import { WorkerError } from "../workertypes"; import ErrorStackParser = require("error-stack-parser"); import yufka from 'yufka'; import * as bitmap from "./lib/bitmap"; import * as io from "./lib/io"; import * as output from "./lib/output"; -import { escapeHTML } from "../util"; +import * as color from "./lib/color"; +import * as scriptui from "./lib/scriptui"; export interface Cell { id: string; object?: any; } +export interface RunResult { + cells: Cell[]; + state: {}; +} + const IMPORTS = { 'bitmap': bitmap, 'io': io, - 'output': output + 'output': output, + 'color': color, + 'ui': scriptui, } -const LINE_NUMBER_OFFSET = 3; +const LINE_NUMBER_OFFSET = 3; // TODO: shouldnt need? const GLOBAL_BADLIST = [ 'eval' @@ -34,10 +43,18 @@ const GLOBAL_GOODLIST = [ 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Uint8ClampedArray', ] +class RuntimeError extends Error { + constructor(public loc: acorn.SourceLocation, msg: string) { + super(msg); + } +} + export class Environment { preamble: string; postamble: string; obj: {}; + seq: number; + declvars : {[name : string] : acorn.Node}; constructor( public readonly globalenv: any, @@ -51,45 +68,100 @@ export class Environment { this.preamble += '{\n'; this.postamble = '\n}'; } + error(varname: string, msg: string) { + console.log(varname, this.declvars[varname]); + throw new RuntimeError(this.declvars && this.declvars[varname].loc, msg); + } preprocess(code: string): string { - var declvars = {}; - const result = yufka(code, (node, { update, source, parent }) => { + this.declvars = {}; + this.seq = 0; + let options = { + // https://www.npmjs.com/package/magic-string#sgeneratemap-options- + sourceMap: { + file: this.path, + source: this.path, + hires: false, + includeContent: false + }, + // https://github.com/acornjs/acorn/blob/master/acorn/README.md + acorn: { + ecmaVersion: 6 as any, + locations: true, + allowAwaitOutsideFunction: true, + allowReturnOutsideFunction: true, + allowReserved: true, + } + }; + const result = yufka(code, options, (node, { update, source, parent }) => { + function isTopLevel() { + return parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program'; + } let left = node['left']; switch (node.type) { + // error on forbidden keywords case 'Identifier': if (GLOBAL_BADLIST.indexOf(source()) >= 0) { update(`__FORBIDDEN__KEYWORD__${source()}__`) // TODO? how to preserve line number? } break; + // x = expr --> var x = expr (first use) case 'AssignmentExpression': - // x = expr --> var x = expr (first use) - if (parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program') { // TODO + if (isTopLevel()) { if (left && left.type === 'Identifier') { - if (!declvars[left.name]) { - update(`var ${left.name}=this.${source()}`) - declvars[left.name] = true; + if (!this.declvars[left.name]) { + update(`var ${left.name}=io.data.get(this.${source()}, ${JSON.stringify(left.name)})`) + this.declvars[left.name] = left; } else { update(`${left.name}=this.${source()}`) } } } break; + // literal comments + case 'Literal': + if (isTopLevel() && typeof node['value'] === 'string') { + update(`this.$$doc__${this.seq++} = { literaltext: ${source()} };`); + } + break; } - }) + }); return result.toString(); } async run(code: string): Promise { + // TODO: split into cells based on "--" linebreaks? code = this.preprocess(code); this.obj = {}; const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor; const fn = new AsyncFunction('$$', this.preamble + code + this.postamble).bind(this.obj, IMPORTS); await fn.call(this); - this.checkResult(); + this.checkResult(this.obj, new Set(), []); } - checkResult() { - for (var [key, value] of Object.entries(this.obj)) { - if (value instanceof Promise) { - throw new Error(`'${key}' is unresolved. Use 'await' before expression.`) // TODO? + // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm + // TODO: return initial location of thingie + checkResult(o, checked: Set, fullkey: string[]) { + if (o == null) return; + if (checked.has(o)) return; + if (typeof o === 'object') { + if (o.length > 100) return; // big array, don't bother + if (o.BYTES_PER_ELEMENT > 0) return; // typed array, don't bother + checked.add(o); // so we don't recurse if cycle + function prkey() { return fullkey.join('.') } + for (var [key, value] of Object.entries(o)) { + if (value == null && fullkey.length == 0) { + this.error(key, `"${key}" has no value.`) + } + fullkey.push(key); + if (typeof value === 'function') { + this.error(fullkey[0], `"${prkey()}" is a function. Did you forget to pass parameters?`); // TODO? did you mean (needs to see entire expr) + } + if (typeof value === 'symbol') { + this.error(fullkey[0], `"${prkey()}" is a Symbol, and can't be used.`) // TODO? + } + if (value instanceof Promise) { + this.error(fullkey[0], `"${prkey()}" is unresolved. Use "await" before expression.`) // TODO? + } + this.checkResult(value, checked, fullkey); + fullkey.pop(); } } } @@ -106,22 +178,58 @@ export class Environment { return cells; } extractErrors(e: Error): WorkerError[] { - if (e['loc'] != null) { + let loc = e['loc']; + if (loc && loc.start && loc.end) { return [{ path: this.path, msg: e.message, - line: e['loc'].line, - start: e['loc'].column, + line: loc.start.line, + start: loc.start.column, + end: loc.end.line, + }] + } + if (loc && loc.line != null) { + return [{ + path: this.path, + msg: e.message, + line: loc.line, + start: loc.column, }] } // TODO: Cannot parse given Error object - var frames = ErrorStackParser.parse(e); - var frame = frames.find(f => f.functionName === 'anonymous'); - return [{ - path: this.path, - msg: e.message, - line: frame ? frame.lineNumber - LINE_NUMBER_OFFSET : 0, - start: frame ? frame.columnNumber : 0, - }]; + let frames = ErrorStackParser.parse(e); + let frame = frames.findIndex(f => f.functionName === 'anonymous'); + let errors = []; + while (frame >= 0) { + console.log(frames[frame]); + if (frames[frame].fileName.endsWith('Function')) { + errors.push( { + path: this.path, + msg: e.message, + line: frames[frame].lineNumber - LINE_NUMBER_OFFSET, + start: frames[frame].columnNumber, + } ); + } + --frame; + } + if (errors.length == 0) { + errors.push( { + path: this.path, + msg: e.message, + line: 0 + } ); + } + return errors; + } + getLoadableState() { + let updated = null; + for (let [key, value] of Object.entries(this.declvars)) { + if (typeof value['$$getstate'] === 'function') { + let loadable = value as io.Loadable; + if (updated == null) updated = {}; + updated[key] = loadable.$$getstate(); + } + } + return updated; } } diff --git a/src/common/script/lib/bitmap.ts b/src/common/script/lib/bitmap.ts index 850e2b73..8596d9c4 100644 --- a/src/common/script/lib/bitmap.ts +++ b/src/common/script/lib/bitmap.ts @@ -1,65 +1,113 @@ import * as fastpng from 'fast-png'; -import { convertWordsToImages, PixelEditorImageFormat } from '../../../ide/pixeleditor'; -import { arrayCompare } from '../../util'; +import { Palette } from './color'; import * as io from './io' +import * as color from './color' -export abstract class AbstractBitmap { +export type PixelMapFunction = (x: number, y: number) => number; + +export abstract class AbstractBitmap { + aspect? : number; // aspect ratio, null == default == 1:1 constructor( public readonly width: number, public readonly height: number, ) { } + + abstract blank(width: number, height: number) : AbstractBitmap; + abstract setarray(arr: ArrayLike) : AbstractBitmap; + abstract set(x: number, y: number, val: number) : AbstractBitmap; + abstract get(x: number, y: number): number; + + inbounds(x: number, y: number): boolean { + return (x >= 0 && x < this.width && y >= 0 && y < this.height); + } + assign(fn: ArrayLike | PixelMapFunction) { + if (typeof fn === 'function') { + for (let y=0; y { + return this.blank(this.width, this.height).assign((x,y) => this.get(x,y)); + } + crop(srcx: number, srcy: number, width: number, height: number) { + let dest = this.blank(width, height); + dest.assign((x, y) => this.get(x + srcx, y + srcy)); + return dest; + } } -export class RGBABitmap extends AbstractBitmap { +export class RGBABitmap extends AbstractBitmap { public readonly rgba: Uint32Array constructor( width: number, height: number, + initial?: Uint32Array | PixelMapFunction ) { super(width, height); this.rgba = new Uint32Array(this.width * this.height); + if (initial) this.assign(initial); } - setPixel(x: number, y: number, rgba: number): void { - this.rgba[y * this.width + x] = rgba; + setarray(arr: ArrayLike) { + this.rgba.set(arr); + return this; } - getPixel(x: number, y: number): number { - return this.rgba[y * this.width + x]; + set(x: number, y: number, rgba: number) { + if (this.inbounds(x,y)) this.rgba[y * this.width + x] = rgba; + return this; + } + get(x: number, y: number): number { + return this.inbounds(x,y) ? this.rgba[y * this.width + x] : 0; + } + blank(width: number, height: number) : RGBABitmap { + return new RGBABitmap(width, height); + } + clone() : RGBABitmap { + let bitmap = this.blank(this.width, this.height); + bitmap.rgba.set(this.rgba); + return bitmap; } } -export abstract class MappedBitmap extends AbstractBitmap { +export abstract class MappedBitmap extends AbstractBitmap { public readonly pixels: Uint8Array constructor( width: number, height: number, public readonly bitsPerPixel: number, - pixels?: Uint8Array + initial?: Uint8Array | PixelMapFunction ) { super(width, height); - this.pixels = pixels || new Uint8Array(this.width * this.height); + if (bitsPerPixel != 1 && bitsPerPixel != 2 && bitsPerPixel != 4 && bitsPerPixel != 8) + throw new Error(`Invalid bits per pixel: ${bitsPerPixel}`); + this.pixels = new Uint8Array(this.width * this.height); + if (initial) this.assign(initial); } - - setPixel(x: number, y: number, index: number): void { - this.pixels[y * this.width + x] = index; + setarray(arr: ArrayLike) { + this.pixels.set(arr); + return this; } - getPixel(x: number, y: number): number { - return this.pixels[y * this.width + x]; + set(x: number, y: number, index: number) { + if (this.inbounds(x,y)) this.pixels[y * this.width + x] = index; + return this; + } + get(x: number, y: number): number { + return this.inbounds(x,y) ? this.pixels[y * this.width + x] : 0; } abstract getRGBAForIndex(index: number): number; } -export class Palette { - colors: Uint32Array; - - constructor(numColors: number) { - this.colors = new Uint32Array(numColors); - } -} - export class IndexedBitmap extends MappedBitmap { public palette: Palette; @@ -67,45 +115,46 @@ export class IndexedBitmap extends MappedBitmap { width: number, height: number, bitsPerPixel: number, - pixels?: Uint8Array + initial?: Uint8Array | PixelMapFunction ) { - super(width, height, bitsPerPixel, pixels); - this.palette = getDefaultPalette(bitsPerPixel); + super(width, height, bitsPerPixel || 8, initial); + this.palette = color.palette.colors(this.bitsPerPixel); } getRGBAForIndex(index: number): number { return this.palette.colors[index]; } -} - -// TODO? -function getDefaultPalette(bpp: number) { - var pal = new Palette(1 << bpp); - for (var i=0; i new IndexedBitmap(fmt.w, fmt.h, fmt.bpp | 1, data)); +} + export namespace png { export function read(url: string): BitmapType { return decode(io.readbin(url)); @@ -126,13 +175,22 @@ export namespace png { var palette = new Palette(palarr.length); for (let i = 0; i < palarr.length; i++) { // TODO: alpha? - palette.colors[i] = rgbaToUint32(palarr[i]) | 0xff000000; + palette.colors[i] = color.arr2rgba(palarr[i]) | 0xff000000; } let bitmap = new IndexedBitmap(png.width, png.height, png.depth); - for (let i = 0; i < bitmap.pixels.length; i++) { - bitmap.pixels[i] = png.data[i]; + if (png.depth == 8) { + bitmap.pixels.set(png.data); + } else { + let pixperbyte = Math.floor(8 / png.depth); + let mask = (1 << png.depth) - 1; + for (let i = 0; i < bitmap.pixels.length; i++) { + var bofs = (i % pixperbyte) * png.depth; + let val = png.data[Math.floor(i / pixperbyte)]; + bitmap.pixels[i] = (val >> bofs) & mask; + } } bitmap.palette = palette; + // TODO: aspect etc return bitmap; } function convertRGBAToBitmap(png: fastpng.IDecodedPNG): RGBABitmap { @@ -141,17 +199,172 @@ export namespace png { for (let i = 0; i < bitmap.rgba.length; i++) { for (let j = 0; j < 4; j++) rgba[j] = png.data[i * 4 + j]; - bitmap.rgba[i] = rgbaToUint32(rgba); + bitmap.rgba[i] = color.arr2rgba(rgba); } + // TODO: aspect etc return bitmap; } } -// TODO: check arguments -export function decode(arr: Uint8Array, fmt: PixelEditorImageFormat) { - var pixels = convertWordsToImages(arr, fmt); - // TODO: guess if missing w/h/count? - // TODO: reverse mapping - // TODO: maybe better composable functions - return pixels.map(data => new IndexedBitmap(fmt.w, fmt.h, fmt.bpp|1, data)); +export namespace font { + interface Font { + maxheight: number; + glyphs: { [code: number]: Glyph }; + properties: {}; + } + class Glyph extends IndexedBitmap { + constructor(width: number, height: number, bpp: number, + public readonly code: number, + public readonly yoffset: number) { + super(width, height, bpp); + } + } + export function read(url: string) { + if (url.endsWith('.yaff')) return decodeyafflines(io.readlines(url)); + if (url.endsWith('.draw')) return decodedrawlines(io.readlines(url)); + throw new Error(`Can't figure out font format for "${url}"`); + } + export function decodeglyph(glines: string[], curcode: number, yoffset: number): Glyph { + let width = 0; + for (var gline of glines) width = Math.max(width, gline.length); + let g = new Glyph(width, glines.length, 1, curcode, yoffset); + for (var y = 0; y < glines.length; y++) { + let gline = glines[y]; + for (var x = 0; x < gline.length; x++) { + let ch = gline[x]; + g.set(x, y, ch==='@' || ch==='#' ? 1 : 0); // TODO: provide mapping + } + } + return g; + } + // https://github.com/robhagemans/monobit + export function decodeyafflines(lines: string[]): Font { + let maxheight = 0; + let properties = {}; + let glyphs = {}; + let yoffset = 0; + let curcode = -1; + let curglyph: string[] = []; + const re_prop = /^([\w-]+):\s+(.+)/i; + const re_label = /^0x([0-9a-f]+):|u[+]([0-9a-f]+):|(\w+):/i; + const re_gline = /^\s+([.@]+)/ + function addfont() { + if (curcode >= 0 && curglyph.length) { + glyphs[curcode] = decodeglyph(curglyph, curcode, yoffset); + curcode = -1; + curglyph = []; + } + } + for (let line of lines) { + let m: RegExpExecArray; + if (m = re_prop.exec(line)) { + properties[m[1]] = m[2]; + if (m[1] === 'bottom') yoffset = parseInt(m[2]); + if (m[1] === 'size') maxheight = parseInt(m[2]); + } else if (m = re_label.exec(line)) { + addfont(); + if (m[1] != null) curcode = parseInt(m[1], 16); + else if (m[2] != null) curcode = parseInt(m[2], 16); + else if (m[3] != null) curcode = null; // text labels not supported + } else if (m = re_gline.exec(line)) { + curglyph.push(m[1]); + } + if (isNaN(curcode + yoffset + maxheight)) + throw new Error(`couldn't decode .yaff: ${JSON.stringify(line)}`) + } + addfont(); + return { maxheight, properties, glyphs }; + } + // https://github.com/robhagemans/monobit + export function decodedrawlines(lines: string[]): Font { + let maxheight = 0; + let properties = {}; + let glyphs = {}; + let curcode = -1; + let curglyph: string[] = []; + const re_gline = /^([0-9a-f]+)?[:]?\s*([-#]+)/i; + function addfont() { + if (curcode >= 0 && curglyph.length) { + glyphs[curcode] = decodeglyph(curglyph, curcode, 0); + maxheight = Math.max(maxheight, curglyph.length); + curcode = -1; + curglyph = []; + } + } + for (let line of lines) { + let m: RegExpExecArray; + if (m = re_gline.exec(line)) { + if (m[1] != null) { + addfont(); + curcode = parseInt(m[1], 16); + if (isNaN(curcode)) + throw new Error(`couldn't decode .draw: ${JSON.stringify(line)}`) + } + curglyph.push(m[2]); + } + } + addfont(); + return { maxheight, properties, glyphs }; + } } + +// TODO: merge w/ pixeleditor + +export type PixelEditorImageFormat = { + w:number + h:number + count?:number + bpp?:number + np?:number + bpw?:number + sl?:number + pofs?:number + remap?:number[] + reindex?:number[] + brev?:boolean + flip?:boolean + destfmt?:PixelEditorImageFormat + xform?:string + skip?:number + aspect?:number + }; + + export function convertWordsToImages(words:ArrayLike, fmt:PixelEditorImageFormat) : Uint8Array[] { + var width = fmt.w; + var height = fmt.h; + var count = fmt.count || 1; + var bpp = fmt.bpp || 1; + var nplanes = fmt.np || 1; + var bitsperword = fmt.bpw || 8; + var wordsperline = fmt.sl || Math.ceil(width * bpp / bitsperword); + var mask = (1 << bpp)-1; + var pofs = fmt.pofs || wordsperline*height*count; + var skip = fmt.skip || 0; + var images = []; + for (var n=0; n>(bitsperword-shift-bpp) : byte>>shift) & mask) << (p*bpp); + } + imgdata.push(color); + shift += bpp; + if (shift >= bitsperword && !fmt.reindex) { + ofs0 += 1; + shift = 0; + } + } + } + images.push(new Uint8Array(imgdata)); + } + return images; + } + \ No newline at end of file diff --git a/src/common/script/lib/color.ts b/src/common/script/lib/color.ts new file mode 100644 index 00000000..e9e12a03 --- /dev/null +++ b/src/common/script/lib/color.ts @@ -0,0 +1,87 @@ + +export class Palette { + colors: Uint32Array; + + constructor(arg: number | number[] | Uint32Array) { + if (typeof arg === 'number') { + if (!(arg >= 1 && arg <= 65536)) throw new Error('Invalid palette size ' + arg); + this.colors = new Uint32Array(arg); + } else { + this.colors = new Uint32Array(arg); + } + } +} + +export function rgb(r: number, g: number, b: number): number { + return ((r & 0xff) << 0) | ((g & 0xff) << 8) | ((b & 0xff) << 16) | 0xff000000; +} + +export function arr2rgba(arr: number[] | Uint8Array): number { + let v = 0; + v |= (arr[0] & 0xff) << 0; + v |= (arr[1] & 0xff) << 8; + v |= (arr[2] & 0xff) << 16; + v |= (arr[3] & 0xff) << 24; + return v; +} + +export function rgba2arr(v: number): number[] { + return [ + (v >> 0) & 0xff, + (v >> 8) & 0xff, + (v >> 16) & 0xff, + (v >> 24) & 0xff, + ] +} + +export namespace palette { + export function generate(bpp: number, func: (index: number) => number) { + var pal = new Palette(1 << bpp); + for (var i = 0; i < pal.colors.length; i++) { + pal.colors[i] = 0xff000000 | func(i); + } + return pal; + } + export function mono() { + return greys(1); + } + export function rgb2() { + return new Palette([ + rgb(0, 0, 0), + rgb(0, 0, 255), + rgb(255, 0, 0), + rgb(0, 255, 0), + ]); + } + export function rgb3() { + return new Palette([ + rgb(0, 0, 0), + rgb(0, 0, 255), + rgb(255, 0, 0), + rgb(255, 0, 255), + rgb(0, 255, 0), + rgb(0, 255, 255), + rgb(255, 255, 0), + rgb(255, 255, 255), + ]); + } + export function greys(bpp: number) { + return generate(bpp, (i) => { + let v = 255 * i / ((1 << bpp) - 1); + return rgb(v,v,v); + }); + } + export function colors(bpp: number) { + switch (bpp) { + case 1: return mono(); + case 2: return rgb2(); + case 3: return rgb3(); + default: return factors(bpp); // TODO + } + } + export function factors(bpp: number, mult?: number) { + mult = mult || 0x031f0f; + return generate(bpp, (i) => i * mult); + } + // TODO: https://www.iquilezles.org/www/articles/palettes/palettes.htm +} diff --git a/src/common/script/lib/io.ts b/src/common/script/lib/io.ts index ba661a9c..d0676ba0 100644 --- a/src/common/script/lib/io.ts +++ b/src/common/script/lib/io.ts @@ -1,35 +1,46 @@ -import { ProjectFilesystem } from "../../../ide/project"; -import { FileData } from "../../workertypes"; -import * as output from "./output"; -// TODO -var $$fs: ProjectFilesystem; -var $$cache: { [path: string]: FileData } = {}; +import { FileData, WorkingStore } from "../../workertypes"; -export class IOWaitError extends Error { +// remote resource cache +var $$cache: WeakMap = new WeakMap(); +// file read/write interface +var $$store: WorkingStore; +// backing store for data +var $$data: {} = {}; + +export function $$setupFS(store: WorkingStore) { + $$store = store; +} +export function $$getData() { + return $$data; +} +export function $$loadData(data: {}) { + Object.assign($$data, data); } -export function $$setupFS(fs: ProjectFilesystem) { - $$fs = fs; +// object that can load state from backing store +export interface Loadable { + reset() : void; + $$getstate() : {}; } -function getFS(): ProjectFilesystem { - if ($$fs == null) throw new Error(`Internal Error: The 'io' module has not been set up properly.`) - return $$fs; -} - -export function ___load(path: string): FileData { - var data = $$cache[path]; - if (data == null) { - getFS().getFileData(path).then((value) => { - $$cache[path] = value; - }) - throw new IOWaitError(path); - } else { - return data; +export namespace data { + export function get(object: Loadable, key: string): Loadable { + let override = $$data && $$data[key]; + if (override) Object.assign(object, override); + else if (object.reset) object.reset(); + return object; + } + export function set(object: Loadable, key: string): Loadable { + if ($$data && object.$$getstate) { + $$data[key] = object.$$getstate(); + } + return object; } } +export class IOWaitError extends Error { +} export function canonicalurl(url: string) : string { // get raw resource URL for github @@ -42,24 +53,50 @@ export function canonicalurl(url: string) : string { return url; } -export function read(url: string, type?: 'binary' | 'text'): FileData { - url = canonicalurl(url); +export function clearcache() { + $$cache = new WeakMap(); +} + +export function fetchurl(url: string, type?: 'binary' | 'text'): FileData { // TODO: only works in web worker var xhr = new XMLHttpRequest(); xhr.responseType = type === 'text' ? 'text' : 'arraybuffer'; xhr.open("GET", url, false); // synchronous request xhr.send(null); if (xhr.response != null && xhr.status == 200) { - if (type !== 'text') { - return new Uint8Array(xhr.response); + if (type === 'text') { + return xhr.response as string; } else { - return xhr.response; + return new Uint8Array(xhr.response); } } else { throw new Error(`The resource at "${url}" responded with status code of ${xhr.status}.`) } } +export function readnocache(url: string, type?: 'binary' | 'text'): FileData { + if (url.startsWith('http:') || url.startsWith('https:')) { + return fetchurl(url); + } + if ($$store) { + return $$store.getFileData(url); + } +} + +// TODO: read files too +export function read(url: string, type?: 'binary' | 'text'): FileData { + url = canonicalurl(url); + // check cache + let cachekey = {url: url}; + if ($$cache.has(cachekey)) { + return $$cache.get(cachekey); + } + let data = readnocache(url, type); + if (data == null) throw new Error(`Cannot find resource "${url}"`); + $$cache.set(cachekey, data); + return data; +} + export function readbin(url: string): Uint8Array { var data = read(url, 'binary'); if (data instanceof Uint8Array) @@ -67,3 +104,11 @@ export function readbin(url: string): Uint8Array { else throw new Error(`The resource at "${url}" is not a binary file.`); } + +export function readlines(url: string) : string[] { + return (read(url, 'text') as string).split('\n'); +} + +export function splitlines(text: string) : string[] { + return text.split(/\n|\r\n/g); +} diff --git a/src/common/script/lib/scriptui.ts b/src/common/script/lib/scriptui.ts new file mode 100644 index 00000000..c311125b --- /dev/null +++ b/src/common/script/lib/scriptui.ts @@ -0,0 +1,30 @@ + +import * as io from "./io"; + +export class ScriptUISliderType { + readonly uitype = 'slider'; + value: number; + constructor( + readonly min: number, + readonly max: number + ) { + } +} + +export class ScriptUISlider extends ScriptUISliderType implements io.Loadable { + initvalue: number; + initial(value: number) { + this.initvalue = value; + return this; + } + reset() { + this.value = this.initvalue != null ? this.initvalue : this.min; + } + $$getstate() { + return { value: this.value }; + } +} + +export function slider(min: number, max: number) { + return new ScriptUISlider(min, max); +} diff --git a/src/common/script/ui/notebook.ts b/src/common/script/ui/notebook.ts index 4db3906b..e15faa43 100644 --- a/src/common/script/ui/notebook.ts +++ b/src/common/script/ui/notebook.ts @@ -2,8 +2,14 @@ import { BitmapType, IndexedBitmap, RGBABitmap } from "../lib/bitmap"; import { Component, render, h, ComponentType } from 'preact'; import { Cell } from "../env"; -import { rgb2bgr } from "../../util"; +import { hex, rgb2bgr, RGBA } from "../../util"; import { dumpRAM } from "../../emu"; +// TODO: can't call methods from this end +import { Palette } from "../lib/color"; +import { ScriptUISlider, ScriptUISliderType } from "../lib/scriptui"; +import { current_project } from "../../../ide/ui"; + +const MAX_STRING_LEN = 100; interface ColorComponentProps { rgbavalue: number; @@ -12,7 +18,7 @@ interface ColorComponentProps { class ColorComponent extends Component { render(virtualDom, containerNode, replaceNode) { let rgb = this.props.rgbavalue & 0xffffff; - var htmlcolor = `#${rgb2bgr(rgb).toString(16)}`; + var htmlcolor = `#${hex(rgb2bgr(rgb),6)}`; var textcol = (rgb & 0x008000) ? 'black' : 'white'; return h('div', { class: 'scripting-color', @@ -41,12 +47,11 @@ class BitmapComponent extends Component { return h('canvas', { class: 'pixelated', width: this.props.width, - height: this.props.height + height: this.props.height, + ...this.props }); } componentDidMount() { - this.canvas = this.base as HTMLCanvasElement; - this.prepare(); this.refresh(); } componentWillUnmount() { @@ -58,13 +63,16 @@ class BitmapComponent extends Component { this.refresh(); } prepare() { + this.canvas = this.base as HTMLCanvasElement; this.ctx = this.canvas.getContext('2d'); this.imageData = this.ctx.createImageData(this.canvas.width, this.canvas.height); this.datau32 = new Uint32Array(this.imageData.data.buffer); } refresh() { // preact can reuse this component but it can change shape :^P - if (this.imageData.width != this.props.width || this.imageData.height != this.props.height) { + if (this.canvas !== this.base + || this.imageData.width != this.props.width + || this.imageData.height != this.props.height) { this.prepare(); } this.updateCanvas(this.datau32, this.props.bitmap); @@ -89,30 +97,87 @@ class BitmapComponent extends Component { } } +interface BitmapEditorState { + isEditing: boolean; +} + +class BitmapEditor extends Component { + render(virtualDom, containerNode, replaceNode) { + // TODO: focusable? + let bitmapProps = { + onClick: (event) => { + if (!this.state.isEditing) { + this.setState({ isEditing: true }); + } else { + // TODO: process click + } + }, + ...this.props + } + let bitmapRender = h(BitmapComponent, bitmapProps, []); + let okCancel = null; + if (this.state.isEditing) { + okCancel = h('div', {}, [ + button('Ok', () => this.commitChanges()), + button('Cancel', () => this.abandonChanges()), + ]); + } + return h('div', { + tabIndex: 0, + class: this.state.isEditing ? 'scripting-cell' : '' // TODO + }, [ + bitmapRender, + okCancel, + ]) + } + commitChanges() { + this.cancelEditing(); + } + abandonChanges() { + this.cancelEditing(); + } + cancelEditing() { + this.setState({ isEditing: false }); + } +} + +function button(label: string, action: () => void) { + return h('button', { + onClick: action + }, [label]); +} + interface ObjectTreeComponentProps { + name?: string; object: {} | []; + objpath: string; } interface ObjectTreeComponentState { - expanded : boolean; + expanded: boolean; } -class ObjectTreeComponent extends Component { +class ObjectKeyValueComponent extends Component { render(virtualDom, containerNode, replaceNode) { - if (this.state.expanded) { - var minus = h('span', { onClick: () => this.toggleExpand() }, [ '-' ]); - return h('minus', { }, [ - minus, - getShortName(this.props.object), - objectToContentsDiv(this.props.object) - ]); - } else { - var plus = h('span', { onClick: () => this.toggleExpand() }, [ '+' ]); - return h('div', { }, [ - plus, - getShortName(this.props.object) - ]); - } + let expandable = typeof this.props.object === 'object'; + let hdrclass = ''; + if (expandable) + hdrclass = this.state.expanded ? 'tree-expanded' : 'tree-collapsed' + return h('div', { + class: 'tree-content', + key: `${this.props.objpath}__tree` + }, [ + h('div', { + class: 'tree-header ' + hdrclass, + onClick: expandable ? () => this.toggleExpand() : null + }, [ + this.props.name + "", + h('span', { class: 'tree-value' }, [ + getShortName(this.props.object) + ]) + ]), + this.state.expanded ? objectToContentsDiv(this.props.object, this.props.objpath) : null + ]); } toggleExpand() { this.setState({ expanded: !this.state.expanded }); @@ -128,67 +193,133 @@ function getShortName(object: any) { } return s; } catch (e) { - return 'object'; + console.log(e); + return e + ""; } } else { - return object+""; + return primitiveToString(object); } } -// TODO: need id? -function objectToDiv(object: any) { - var props = { class: '' }; +function primitiveToString(obj) { + var text = ""; + // is it a function? call it first, if we are expanded + // TODO: only call functions w/ signature + if (obj && obj.$$ && typeof obj.$$ == 'function' && this._content != null) { + obj = obj.$$(); + } + // check null first + if (obj == null) { + text = obj + ""; + // primitive types + } else if (typeof obj == 'number') { + if (obj != (obj | 0)) text = obj.toString(); // must be a float + else text = obj + "\t($" + hex(obj) + ")"; + } else { + text = JSON.stringify(obj); + if (text.length > MAX_STRING_LEN) + text = text.substring(0, MAX_STRING_LEN) + "..."; + } + return text; +} + +function isIndexedBitmap(object): object is IndexedBitmap { + return object['bitsPerPixel'] && object['pixels'] && object['palette']; +} +function isRGBABitmap(object): object is RGBABitmap { + return object['rgba'] instanceof Uint32Array; +} +function isPalette(object): object is Palette { + return object['colors'] instanceof Uint32Array; +} + +function objectToDiv(object: any, name: string, objpath: string) { + var props = { class: '', key: `${objpath}__obj` }; var children = []; // TODO: tile editor // TODO: limit # of items // TODO: detect table - if (Array.isArray(object)) { - return objectToContentsDiv(object); - } else if (object['bitsPerPixel'] && object['pixels'] && object['palette']) { - addBitmapComponent(children, object as IndexedBitmap); - } else if (object['rgba'] instanceof Uint32Array) { - addBitmapComponent(children, object as RGBABitmap); - } else if (object['colors'] instanceof Uint32Array) { + //return objectToContentsDiv(object); + if (object == null) { + return object + ""; + } else if (object['uitype']) { + children.push(h(UISliderComponent, { iokey: objpath, uiobject: object })); + } else if (object['literaltext']) { + children.push(h("pre", { }, [ object['literaltext'] ])); // TODO + } else if (isIndexedBitmap(object)) { + //Object.setPrototypeOf(object, IndexedBitmap.prototype); // TODO: use Object.create()? + addBitmapComponent(children, object); + } else if (isRGBABitmap(object)) { + //Object.setPrototypeOf(object, RGBABitmap.prototype); // TODO: use Object.create()? + addBitmapComponent(children, object); + } else if (isPalette(object)) { // TODO: make sets of 2/4/8/16/etc props.class += ' scripting-grid '; - object['colors'].forEach((val) => { + object.colors.forEach((val) => { children.push(h(ColorComponent, { rgbavalue: val })); }) - } else if (typeof object === 'object') { - children.push(h(ObjectTreeComponent, { object })); } else { - children.push(JSON.stringify(object)); + return h(ObjectKeyValueComponent, { name, object, objpath }, []); } let div = h('div', props, children); return div; } -function objectToContentsDiv(object: {} | []) { +function fixedArrayToDiv(tyarr: Array, bpel: number, objpath: string) { + const maxBytes = 0x100; + if (tyarr.length <= maxBytes) { + // TODO + let dumptext = dumpRAM(tyarr, 0, tyarr.length); + return h('pre', {}, dumptext); + } else { + let children = []; + for (var ofs = 0; ofs < tyarr.length; ofs += maxBytes) { + children.push(objectToDiv(tyarr.slice(ofs, ofs + maxBytes), '$' + hex(ofs), `${objpath}.${ofs}`)); + } + return h('div', {}, children); + } +} + +function objectToContentsDiv(object: {} | [], objpath: string) { // is typed array? let bpel = object['BYTES_PER_ELEMENT']; if (typeof bpel === 'number') { - const maxBytes = 0x100; - let tyarr = object as Uint8Array; - if (tyarr.length <= maxBytes) { - // TODO - let dumptext = dumpRAM(tyarr, 0, tyarr.length); - return h('pre', { }, dumptext); - } else { - let children = []; - for (var ofs=0; ofs, bpel, objpath); } let objectEntries = Object.entries(object); - // TODO: id? - let objectDivs = objectEntries.map(entry => objectToDiv(entry[1])); - return h('div', { }, objectDivs); + let objectDivs = objectEntries.map(entry => objectToDiv(entry[1], entry[0], `${objpath}.${entry[1]}`)); + return h('div', {}, objectDivs); } function addBitmapComponent(children, bitmap: BitmapType) { - children.push(h(BitmapComponent, { bitmap: bitmap, width: bitmap.width, height: bitmap.height})); + children.push(h(BitmapEditor, { bitmap: bitmap, width: bitmap.width, height: bitmap.height })); +} + +interface UISliderComponentProps { + iokey: string; + uiobject: ScriptUISliderType; +} + +class UISliderComponent extends Component { + render(virtualDom, containerNode, replaceNode) { + let slider = this.props.uiobject; + return h('div', {}, [ + this.props.iokey, + h('input', { + type: 'range', + min: slider.min, + max: slider.max, + value: this.props.uiobject.value, + onInput: (ev) => { + let newValue = { value: ev.target.value }; + slider.value = parseFloat(ev.target.value); + this.setState(this.state); + current_project.updateDataItems([{key: this.props.iokey, value: newValue}]); + } + }, []), + slider.value + ]); + } } export class Notebook { @@ -201,9 +332,18 @@ export class Notebook { } updateCells(cells: Cell[]) { let hTree = cells.map(cell => { - let cellDiv = objectToDiv(cell.object); + //return objectToDiv(cell.object, cell.id) + return h('div', { + class: 'scripting-cell', + key: `${cell.id}__cell` + }, [ + objectToDiv(cell.object, cell.id, cell.id) + ]) + /* + let cellDiv = objectToDiv(cell.object, cell.id); cellDiv.props['class'] += ' scripting-cell '; return cellDiv; + */ }); render(hTree, this.maindiv); } diff --git a/src/common/toolbar.ts b/src/common/toolbar.ts new file mode 100644 index 00000000..6de0195b --- /dev/null +++ b/src/common/toolbar.ts @@ -0,0 +1,53 @@ + +import Mousetrap = require('mousetrap'); + +/// TOOLBAR + +export class Toolbar { + span : JQuery; + grp : JQuery; + mousetrap; + boundkeys = []; + + constructor(parentDiv:HTMLElement, focusDiv:HTMLElement) { + this.mousetrap = focusDiv ? new Mousetrap(focusDiv) : Mousetrap; + this.span = $(document.createElement("span")).addClass("btn_toolbar"); + parentDiv.appendChild(this.span[0]); + this.newGroup(); + } + destroy() { + if (this.span) { + this.span.remove(); + this.span = null; + } + if (this.mousetrap) { + for (var key of this.boundkeys) { + this.mousetrap.unbind(key); + } + this.mousetrap = null; + } + } + newGroup() { + return this.grp = $(document.createElement("span")).addClass("btn_group").appendTo(this.span).hide(); + } + add(key:string, alttext:string, icon:string, fn:(e,combo) => void) { + var btn = null; + if (icon) { + btn = $(document.createElement("button")).addClass("btn"); + if (icon.startsWith('glyphicon')) { + icon = ''; + } + btn.html(icon); + btn.prop("title", key ? (alttext+" ("+key+")") : alttext); + btn.click(fn); + this.grp.append(btn).show(); + } + if (key) { + this.mousetrap.bind(key, fn); + this.boundkeys.push(key); + } + return btn; + } + + } + \ No newline at end of file diff --git a/src/common/workertypes.ts b/src/common/workertypes.ts index 2d364e9c..07fbc2ff 100644 --- a/src/common/workertypes.ts +++ b/src/common/workertypes.ts @@ -57,29 +57,38 @@ export class SourceFile { } export interface Dependency { - path:string, - filename:string, - link:boolean, + path:string + filename:string + link:boolean data:FileData // TODO: or binary? } export interface WorkerFileUpdate { - path:string, + path:string data:FileData }; export interface WorkerBuildStep { path?:string + files?:string[] platform:string tool:string mainfile?:boolean }; +export interface WorkerItemUpdate { + key:string + value:object +}; -export interface WorkerMessage extends WorkerBuildStep { - preload:string, - reset:boolean, - code:string, - updates:WorkerFileUpdate[], +// TODO: split into different msg types +export interface WorkerMessage { + preload?:string + platform?:string + tool?:string + updates:WorkerFileUpdate[] buildsteps:WorkerBuildStep[] + reset?:boolean + code?:string + setitems?:WorkerItemUpdate[] } export interface WorkerError extends SourceLocation { @@ -87,10 +96,10 @@ export interface WorkerError extends SourceLocation { } export interface CodeListing { - lines:SourceLine[], - asmlines?:SourceLine[], - text?:string, - sourcefile?:SourceFile, // not returned by worker + lines:SourceLine[] + asmlines?:SourceLine[] + text?:string + sourcefile?:SourceFile // not returned by worker assemblyfile?:SourceFile // not returned by worker } @@ -133,3 +142,7 @@ export function isErrorResult(result: WorkerResult) : result is WorkerErrorResul export function isOutputResult(result: WorkerResult) : result is WorkerOutputResult { return ('output' in result); } + +export interface WorkingStore { + getFileData(path:string) : FileData; +} diff --git a/src/ide/pixeleditor.ts b/src/ide/pixeleditor.ts index 0837c9c5..d8bc15dc 100644 --- a/src/ide/pixeleditor.ts +++ b/src/ide/pixeleditor.ts @@ -1,7 +1,7 @@ import { hex, rgb2bgr, rle_unpack } from "../common/util"; import { ProjectWindows } from "./windows"; -import { Toolbar } from "../common/emu"; +import { Toolbar } from "../common/toolbar"; import Mousetrap = require('mousetrap'); export type UintArray = number[] | Uint8Array | Uint16Array | Uint32Array; //{[i:number]:number}; diff --git a/src/ide/project.ts b/src/ide/project.ts index d70ff6a9..a399ef33 100644 --- a/src/ide/project.ts +++ b/src/ide/project.ts @@ -1,5 +1,5 @@ -import { FileData, Dependency, SourceLine, SourceFile, CodeListing, CodeListingMap, WorkerError, Segment, WorkerResult, WorkerOutputResult, isUnchanged, isOutputResult } from "../common/workertypes"; +import { FileData, Dependency, SourceLine, SourceFile, CodeListing, CodeListingMap, WorkerError, Segment, WorkerResult, WorkerOutputResult, isUnchanged, isOutputResult, WorkerMessage, WorkerItemUpdate } from "../common/workertypes"; import { getFilenamePrefix, getFolderForPath, isProbablyBinary, getBasePlatform, getWithBinary } from "../common/util"; import { Platform } from "../common/baseplatform"; import localforage from "localforage"; @@ -100,6 +100,7 @@ export class CodeProject { isCompiling : boolean = false; filename2path = {}; // map stripped paths to full paths filesystem : ProjectFilesystem; + dataItems : WorkerItemUpdate[]; callbackBuildResult : BuildResultCallback; callbackBuildStatus : BuildStatusCallback; @@ -152,7 +153,13 @@ export class CodeProject { parseIncludeDependencies(text:string):string[] { let files = []; let m; - if (this.platform_id.startsWith('verilog')) { + if (this.platform_id.startsWith('script')) { // TODO + let re1 = /\b\w+[.]read\(["'](.+?)["']/gmi; + while (m = re1.exec(text)) { + if (m[1] && m[1].indexOf(':/') < 0) // TODO: ignore URLs + this.pushAllFiles(files, m[1]); + } + } else if (this.platform_id.startsWith('verilog')) { // include verilog includes let re1 = /^\s*(`include|[.]include)\s+"(.+?)"/gmi; while (m = re1.exec(text)) { @@ -234,7 +241,7 @@ export class CodeProject { } okToSend():boolean { - return this.pendingWorkerMessages++ == 0; + return this.pendingWorkerMessages++ == 0 && this.mainPath != null; } updateFileInStore(path:string, text:FileData) { @@ -242,9 +249,9 @@ export class CodeProject { } // TODO: test duplicate files, local paths mixed with presets - buildWorkerMessage(depends:Dependency[]) { + buildWorkerMessage(depends:Dependency[]) : WorkerMessage { this.preloadWorker(this.mainPath); - var msg = {updates:[], buildsteps:[]}; + var msg : WorkerMessage = {updates:[], buildsteps:[]}; // TODO: add preproc directive for __MAINFILE__ var mainfilename = this.stripLocalPath(this.mainPath); var maintext = this.getFile(this.mainPath); @@ -275,6 +282,7 @@ export class CodeProject { tool:this.platform.getToolForFilename(dep.path)}); } } + if (this.dataItems) msg.setitems = this.dataItems; return msg; } @@ -350,7 +358,7 @@ export class CodeProject { if (this.filedata[path] == text) return; // unchanged, don't update this.updateFileInStore(path, text); // TODO: isBinary this.filedata[path] = text; - if (this.okToSend() && this.mainPath) { + if (this.okToSend()) { if (this.callbackBuildStatus) this.callbackBuildStatus(true); this.sendBuild(); } @@ -411,6 +419,13 @@ export class CodeProject { return path; } + updateDataItems(items: WorkerItemUpdate[]) { + this.dataItems = items; + if (this.okToSend()) { // TODO? mainpath == null? + this.sendBuild(); // TODO: don't need entire build? + } + } + } export function createNewPersistentStore(storeid:string) : LocalForage { diff --git a/src/ide/ui.ts b/src/ide/ui.ts index 581b4c95..4b301843 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -6,7 +6,8 @@ import { CodeProject, createNewPersistentStore, LocalForageFilesystem, OverlayFi import { WorkerResult, WorkerOutputResult, WorkerError, FileData, WorkerErrorResult } from "../common/workertypes"; import { ProjectWindows } from "./windows"; import { Platform, Preset, DebugSymbols, DebugEvalCondition, isDebuggable, EmuState } from "../common/baseplatform"; -import { PLATFORMS, EmuHalt, Toolbar } from "../common/emu"; +import { PLATFORMS, EmuHalt } from "../common/emu"; +import { Toolbar } from "../common/toolbar"; import { getFilenameForPath, getFilenamePrefix, highlightDifferences, byteArrayToString, compressLZG, stringToByteArray, byteArrayToUTF8, isProbablyBinary, getWithBinary, getBasePlatform, getRootBasePlatform, hex, loadScript, decodeQueryString, parseBool } from "../common/util"; import { StateRecorderImpl } from "../common/recorder"; diff --git a/src/ide/waveform.ts b/src/ide/waveform.ts index 1edad322..3a21d00d 100644 --- a/src/ide/waveform.ts +++ b/src/ide/waveform.ts @@ -1,5 +1,5 @@ -import { Toolbar } from "../common/emu"; +import { Toolbar } from "../common/toolbar"; import { VirtualList } from "../common/vlist"; const BUILTIN_INPUT_PORTS = [ diff --git a/src/platform/script.ts b/src/platform/script.ts index aed7ff11..f8fadbf3 100644 --- a/src/platform/script.ts +++ b/src/platform/script.ts @@ -1,7 +1,7 @@ import { PLATFORMS, RasterVideo } from "../common/emu"; import { Platform } from "../common/baseplatform"; -import { Cell } from "../common/script/env"; +import { RunResult } from "../common/script/env"; import { Notebook } from "../common/script/ui/notebook"; class ScriptingPlatform implements Platform { @@ -31,11 +31,12 @@ class ScriptingPlatform implements Platform { } resume() { } - loadROM(title, cells: Cell[]) { - this.notebook.updateCells(cells); + loadROM(title, run: RunResult) { + this.notebook.updateCells(run.cells); + // TODO: save state file } isRunning() { - return false; + return true; } isDebugging(): boolean { return false; diff --git a/src/worker/tools/script.ts b/src/worker/tools/script.ts index 4df6bbb6..bca3454c 100644 --- a/src/worker/tools/script.ts +++ b/src/worker/tools/script.ts @@ -1,11 +1,12 @@ -import { Environment } from "../../common/script/env"; import { BuildStep, BuildStepResult, emglobal, store } from "../workermain"; +import { Environment, RunResult } from "../../common/script/env"; +import * as io from "../../common/script/lib/io"; // cache environments -var environments : {[path:string] : Environment} = {}; +var environments: { [path: string]: Environment } = {}; -function getEnv(path: string) : Environment { +function getEnv(path: string): Environment { var env = environments[path]; if (!env) { env = environments[path] = new Environment(emglobal, path); @@ -14,16 +15,20 @@ function getEnv(path: string) : Environment { return env; } -export async function runJavascript(step: BuildStep) : Promise { +export async function runJavascript(step: BuildStep): Promise { var env = getEnv(step.path); var code = store.getFileAsString(step.path); try { + io.$$setupFS(store); + io.$$loadData(store.items); // TODO: load from file await env.run(code); + let cells = env.render(); + let state = env.getLoadableState(); + let output : RunResult = { cells, state }; + return { output: output }; } catch (e) { - return {errors: env.extractErrors(e)}; + return { errors: env.extractErrors(e) }; + } finally { + io.$$setupFS(null); } - var output = env.render(); - return { - output - }; } diff --git a/src/worker/workermain.ts b/src/worker/workermain.ts index b8ff78a6..70c52af3 100644 --- a/src/worker/workermain.ts +++ b/src/worker/workermain.ts @@ -1,6 +1,6 @@ -import type { WorkerResult, WorkerBuildStep, WorkerMessage, WorkerError, SourceLine, CodeListingMap, Segment, SourceLocation } from "../common/workertypes"; -import { getBasePlatform, getRootBasePlatform, hex } from "../common/util"; +import type { WorkerResult, WorkerBuildStep, WorkerMessage, WorkerError, SourceLine, WorkerErrorResult, WorkingStore } from "../common/workertypes"; +import { getBasePlatform, getRootBasePlatform } from "../common/util"; /// export interface EmscriptenModule { @@ -394,9 +394,10 @@ export interface BuildStep extends WorkerBuildStep { /// -export class FileWorkingStore { +export class FileWorkingStore implements WorkingStore { workfs : {[path:string]:FileEntry} = {}; workerseq : number = 0; + items : {} = {}; constructor() { this.reset(); @@ -438,12 +439,19 @@ export class FileWorkingStore { getFileEntry(path:string) : FileEntry { return this.workfs[path]; } + setItem(key: string, value: object) { + this.items[key] = value; + } } export var store = new FileWorkingStore(); /// +function errorResult(msg: string) : WorkerErrorResult { + return { errors:[{ line:0, msg:msg }]}; +} + class Builder { steps : BuildStep[] = []; startseq : number = 0; @@ -465,7 +473,7 @@ class Builder { step.result = await toolfn(step); } catch (e) { console.log("EXCEPTION", e, e.stack); - return {errors:[{line:0, msg:e+""}]}; // TODO: catch errors already generated? + return errorResult(e+""); // TODO: catch errors already generated? } if (step.result) { (step.result as any).params = step.params; // TODO: type check @@ -513,10 +521,11 @@ class Builder { this.steps = []; // file updates if (data.updates) { - for (var i=0; i store.putFile(u.path, u.data)); + } + // object update + if (data.setitems) { + data.setitems.forEach((i) => store.setItem(i.key, i.value)); } // build steps if (data.buildsteps) { @@ -524,7 +533,7 @@ class Builder { } // single-file if (data.code) { - this.steps.push(data); + this.steps.push(data as BuildStep); // TODO: remove cast } // execute build steps if (this.steps.length) { @@ -994,7 +1003,7 @@ export function preprocessMCPP(step:BuildStep, filesys:string) { // //main.c:2: error: Can't open include file "stdiosd.h" var errors = extractErrors(/([^:]+):(\d+): (.+)/, errout.split("\n"), step.path, 2, 3, 1); if (errors.length == 0) { - errors = [{line:0, msg:errout}]; + errors = errorResult(errout).errors; } return {errors: errors}; } @@ -1124,7 +1133,12 @@ if (ENVIRONMENT_IS_WORKER) { var result = await lastpromise; lastpromise = null; if (result) { - postMessage(result); + try { + postMessage(result); + } catch (e) { + console.log(e); + postMessage(errorResult(`${e}`)); + } } } } diff --git a/test/cli/testpixelconvert.js b/test/cli/testpixelconvert.js index 6b064290..f104c5d4 100644 --- a/test/cli/testpixelconvert.js +++ b/test/cli/testpixelconvert.js @@ -1,5 +1,7 @@ var assert = require('assert'); +var wtu = require('./workertestutils.js'); +const dom = createTestDOM(); var pixed = require("gen/ide/pixeleditor.js"); function dumbEqual(a,b) { diff --git a/test/cli/testplatforms.js b/test/cli/testplatforms.js index fcc5ee7a..8889f072 100644 --- a/test/cli/testplatforms.js +++ b/test/cli/testplatforms.js @@ -2,7 +2,7 @@ var assert = require('assert'); var fs = require('fs'); var wtu = require('./workertestutils.js'); -var PNG = require('pngjs').PNG; +var fastpng = require('fast-png'); const dom = createTestDOM(); includeInThisContext('gen/common/cpu/6809.js'); @@ -173,9 +173,7 @@ async function testPlatform(platid, romname, maxframes, callback) { } // record video to file if (lastrastervideo) { - var png = new PNG({width:lastrastervideo.width, height:lastrastervideo.height}); - png.data = lastrastervideo.getImageData().data; - var pngbuffer = PNG.sync.write(png); + var pngbuffer = fastpng.encode(lastrastervideo.getImageData()) assert(pngbuffer.length > 500); // make sure PNG is big enough try { fs.mkdirSync("./test"); } catch(e) { } try { fs.mkdirSync("./test/output"); } catch(e) { } diff --git a/test/cli/testworker.js b/test/cli/testworker.js index 49992b63..f9ea16d4 100644 --- a/test/cli/testworker.js +++ b/test/cli/testworker.js @@ -4,6 +4,7 @@ var fs = require('fs'); var wtu = require('./workertestutils.js'); //var heapdump = require('heapdump'); +// TODO: await might be needed later global.onmessage({data:{preload:'cc65', platform:'nes'}}); global.onmessage({data:{preload:'ca65', platform:'nes'}}); global.onmessage({data:{preload:'cc65', platform:'apple2'}}); @@ -30,9 +31,7 @@ function compileFiles(tool, files, platform, callback, outlen, nlines, nerrors, doBuild([msg], callback, outlen, nlines, nerrors, options); } - - -function doBuild(msgs, callback, outlen, nlines, nerrors, options) { +async function doBuild(msgs, callback, outlen, nlines, nerrors, options) { var msgcount = msgs.length; global.postMessage = function(msg) { if (!msg.unchanged) { @@ -74,9 +73,9 @@ function doBuild(msgs, callback, outlen, nlines, nerrors, options) { } else console.log(msgcount + ' msgs left'); }; - global.onmessage({data:{reset:true}}); + await global.onmessage({data:{reset:true}}); for (var i=0; i