scripting: working on notebook, functions, files, setItem(), fixed tests

This commit is contained in:
Steven Hugg 2021-08-15 10:10:01 -05:00
parent 7f86ed0cb6
commit 6134a8c89c
22 changed files with 1005 additions and 285 deletions

View File

@ -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;
}

View File

@ -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",

47
presets/script/mandel.js Normal file
View File

@ -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<iterations && (Tr+Ti)<=escapeRadius; ++n ) {
Zi = 2 * Zr * Zi + Ci;
Zr = Tr - Ti + Cr;
Tr = Zr * Zr;
Ti = Zi * Zi;
}
for ( var e=0; e<4; ++e ) {
Zi = 2 * Zr * Zi + Ci;
Zr = Tr - Ti + Cr;
Tr = Zr * Zr;
Ti = Zi * Zi;
}
return [n, Tr, Ti];
}

View File

@ -1,7 +1,6 @@
import { hex, clamp, lpad } from "./util";
import { SourceLocation } from "./workertypes";
import Mousetrap = require('mousetrap');
import { VirtualList } from "./vlist"
// external modules
@ -654,55 +653,6 @@ export function newAddressDecoder(table : AddressDecoderEntry[], options?:Addres
return new (AddressDecoder as any)(table, options);
}
/// 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 = '<span class="glyphicon ' + icon + '" aria-hidden="true"></span>';
}
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} {

View File

@ -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<void> {
// 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<object>, 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 = <any>value as io.Loadable;
if (updated == null) updated = {};
updated[key] = loadable.$$getstate();
}
}
return updated;
}
}

View File

@ -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<T> {
aspect? : number; // aspect ratio, null == default == 1:1
constructor(
public readonly width: number,
public readonly height: number,
) {
}
abstract blank(width: number, height: number) : AbstractBitmap<T>;
abstract setarray(arr: ArrayLike<number>) : AbstractBitmap<T>;
abstract set(x: number, y: number, val: number) : AbstractBitmap<T>;
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<number> | PixelMapFunction) {
if (typeof fn === 'function') {
for (let y=0; y<this.height; y++) {
for (let x=0; x<this.width; x++) {
this.set(x, y, fn(x, y));
}
}
} else if (fn && fn['length'] != null) {
this.setarray(fn);
} else {
throw new Error(`Illegal argument to assign(): ${fn}`)
}
return this;
}
clone() : AbstractBitmap<T> {
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<RGBABitmap> {
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<number>) {
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<MappedBitmap> {
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<number>) {
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<pal.colors.length; i++) {
pal.colors[i] = 0xff000000 | (i * 7919);
blank(width: number, height: number) : IndexedBitmap {
let bitmap = new IndexedBitmap(width, height, this.bitsPerPixel);
bitmap.palette = this.palette;
return bitmap;
}
clone() : IndexedBitmap {
let bitmap = this.blank(this.width, this.height);
bitmap.pixels.set(this.pixels);
return bitmap;
}
return pal;
}
export function rgbaToUint32(rgba: number[]): number {
let v = 0;
v |= rgba[0] << 0;
v |= rgba[1] << 8;
v |= rgba[2] << 16;
v |= rgba[3] << 24;
return v;
export function rgba(width: number, height: number, initial?: Uint32Array | PixelMapFunction) {
return new RGBABitmap(width, height, initial);
}
export function rgba(width: number, height: number) {
return new RGBABitmap(width, height);
}
export function indexed(width: number, height: number, bpp: number) {
return new IndexedBitmap(width, height, bpp);
export function indexed(width: number, height: number, bpp: number, initial?: Uint8Array | PixelMapFunction) {
return new IndexedBitmap(width, height, bpp, initial);
}
export type BitmapType = RGBABitmap | IndexedBitmap;
// 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 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<number>, 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<count; n++) {
var imgdata = [];
for (var y=0; y<height; y++) {
var yp = fmt.flip ? height-1-y : y;
var ofs0 = n*wordsperline*height + yp*wordsperline;
var shift = 0;
for (var x=0; x<width; x++) {
var color = 0;
var ofs = ofs0; // TODO: remapBits(ofs0, fmt.remap);
// TODO: if (fmt.reindex) { [ofs, shift] = reindexMask(x, fmt.reindex); ofs += ofs0; }
for (var p=0; p<nplanes; p++) {
var byte = words[ofs + p*pofs + skip];
color |= ((fmt.brev ? byte>>(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;
}

View File

@ -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
}

View File

@ -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<object,FileData> = 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);
}

View File

@ -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);
}

View File

@ -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<ColorComponentProps> {
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<BitmapComponentProps> {
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<BitmapComponentProps> {
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<BitmapComponentProps> {
}
}
interface BitmapEditorState {
isEditing: boolean;
}
class BitmapEditor extends Component<BitmapComponentProps, BitmapEditorState> {
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<ObjectTreeComponentProps, ObjectTreeComponentState> {
class ObjectKeyValueComponent extends Component<ObjectTreeComponentProps, ObjectTreeComponentState> {
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<number>, 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<tyarr.length; ofs+=maxBytes) {
children.push(objectToDiv(tyarr.slice(ofs, ofs+maxBytes)));
}
return h('div', { }, children);
}
return fixedArrayToDiv(object as Array<number>, 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<UISliderComponentProps> {
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);
}

53
src/common/toolbar.ts Normal file
View File

@ -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 = '<span class="glyphicon ' + icon + '" aria-hidden="true"></span>';
}
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;
}
}

View File

@ -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<any> {
return ('output' in result);
}
export interface WorkingStore {
getFileData(path:string) : FileData;
}

View File

@ -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};

View File

@ -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 {

View File

@ -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";

View File

@ -1,5 +1,5 @@
import { Toolbar } from "../common/emu";
import { Toolbar } from "../common/toolbar";
import { VirtualList } from "../common/vlist";
const BUILTIN_INPUT_PORTS = [

View File

@ -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;

View File

@ -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<BuildStepResult> {
export async function runJavascript(step: BuildStep): Promise<BuildStepResult> {
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
};
}

View File

@ -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";
/// <reference types="emscripten" />
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<data.updates.length; i++) {
var u = data.updates[i];
store.putFile(u.path, u.data);
}
data.updates.forEach((u) => 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}`));
}
}
}
}

View File

@ -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) {

View File

@ -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) { }

View File

@ -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<msgs.length; i++) {
global.onmessage({data:msgs[i]});
await global.onmessage({data:msgs[i]});
}
}