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; background: #444;
color: #99cc99; color: #99cc99;
padding: 0.5em; padding: 0.5em;
margin: 0.5em; margin-right: 0.5em;
font-family: "Andale Mono", "Menlo", "Lucida Console", monospace; font-family: "Andale Mono", "Menlo", "Lucida Console", monospace;
word-wrap: break-word; word-wrap: break-word;
pointer-events: auto;
} }
.scripting-cell canvas { .scripting-cell canvas {
height: 15vw; height: 20vw;
max-width: 95%;
border: 2px solid #222; border: 2px solid #222;
outline-color: #ccc; outline-color: #ccc;
background: #000; background: #000;
padding: 6px; padding: 6px;
margin: 6px; margin: 6px;
pointer-events:auto;
} }
.scripting-cell canvas:focus { .scripting-cell canvas:hover {
outline:none; outline:none;
border-color:#888; border-color:#ccc;
}
.scripting-cell div {
display: inline;
} }
div.scripting-color { div.scripting-color {
padding:0.1em; padding:0.1em;
@ -800,4 +798,5 @@ div.scripting-color {
div.scripting-grid { div.scripting-grid {
display: grid; display: grid;
grid-template-columns: repeat( auto-fit, minmax(2em, 1fr) ); grid-template-columns: repeat( auto-fit, minmax(2em, 1fr) );
justify-items: center;
} }

View File

@ -67,7 +67,7 @@
"tsbuild": "tsc --build tsconfig.json", "tsbuild": "tsc --build tsconfig.json",
"esbuild": "npm run esbuild-worker && npm run esbuild-ui", "esbuild": "npm run esbuild-worker && npm run esbuild-ui",
"esbuild-clean": "rm -f ./gen/*.*", "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", "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-one": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000",
"test-node": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000 test/cli", "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 { hex, clamp, lpad } from "./util";
import { SourceLocation } from "./workertypes"; import { SourceLocation } from "./workertypes";
import Mousetrap = require('mousetrap');
import { VirtualList } from "./vlist" import { VirtualList } from "./vlist"
// external modules // external modules
@ -654,55 +653,6 @@ export function newAddressDecoder(table : AddressDecoderEntry[], options?:Addres
return new (AddressDecoder as any)(table, options); 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 // https://stackoverflow.com/questions/17130395/real-mouse-position-in-canvas
export function getMousePos(canvas : HTMLCanvasElement, evt) : {x:number,y:number} { export function getMousePos(canvas : HTMLCanvasElement, evt) : {x:number,y:number} {

View File

@ -1,23 +1,32 @@
import { WorkerError } from "../workertypes"; import { WorkerError } from "../workertypes";
import ErrorStackParser = require("error-stack-parser"); import ErrorStackParser = require("error-stack-parser");
import yufka from 'yufka'; import yufka from 'yufka';
import * as bitmap from "./lib/bitmap"; import * as bitmap from "./lib/bitmap";
import * as io from "./lib/io"; import * as io from "./lib/io";
import * as output from "./lib/output"; 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 { export interface Cell {
id: string; id: string;
object?: any; object?: any;
} }
export interface RunResult {
cells: Cell[];
state: {};
}
const IMPORTS = { const IMPORTS = {
'bitmap': bitmap, 'bitmap': bitmap,
'io': io, '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 = [ const GLOBAL_BADLIST = [
'eval' 'eval'
@ -34,10 +43,18 @@ const GLOBAL_GOODLIST = [
'Uint8Array', 'Uint16Array', 'Uint32Array', 'Uint8ClampedArray', 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Uint8ClampedArray',
] ]
class RuntimeError extends Error {
constructor(public loc: acorn.SourceLocation, msg: string) {
super(msg);
}
}
export class Environment { export class Environment {
preamble: string; preamble: string;
postamble: string; postamble: string;
obj: {}; obj: {};
seq: number;
declvars : {[name : string] : acorn.Node};
constructor( constructor(
public readonly globalenv: any, public readonly globalenv: any,
@ -51,45 +68,100 @@ export class Environment {
this.preamble += '{\n'; this.preamble += '{\n';
this.postamble = '\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 { preprocess(code: string): string {
var declvars = {}; this.declvars = {};
const result = yufka(code, (node, { update, source, parent }) => { 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']; let left = node['left'];
switch (node.type) { switch (node.type) {
// error on forbidden keywords
case 'Identifier': case 'Identifier':
if (GLOBAL_BADLIST.indexOf(source()) >= 0) { if (GLOBAL_BADLIST.indexOf(source()) >= 0) {
update(`__FORBIDDEN__KEYWORD__${source()}__`) // TODO? how to preserve line number? update(`__FORBIDDEN__KEYWORD__${source()}__`) // TODO? how to preserve line number?
} }
break; break;
// x = expr --> var x = expr (first use)
case 'AssignmentExpression': case 'AssignmentExpression':
// x = expr --> var x = expr (first use) if (isTopLevel()) {
if (parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program') { // TODO
if (left && left.type === 'Identifier') { if (left && left.type === 'Identifier') {
if (!declvars[left.name]) { if (!this.declvars[left.name]) {
update(`var ${left.name}=this.${source()}`) update(`var ${left.name}=io.data.get(this.${source()}, ${JSON.stringify(left.name)})`)
declvars[left.name] = true; this.declvars[left.name] = left;
} else { } else {
update(`${left.name}=this.${source()}`) update(`${left.name}=this.${source()}`)
} }
} }
} }
break; break;
// literal comments
case 'Literal':
if (isTopLevel() && typeof node['value'] === 'string') {
update(`this.$$doc__${this.seq++} = { literaltext: ${source()} };`);
}
break;
} }
}) });
return result.toString(); return result.toString();
} }
async run(code: string): Promise<void> { async run(code: string): Promise<void> {
// TODO: split into cells based on "--" linebreaks?
code = this.preprocess(code); code = this.preprocess(code);
this.obj = {}; this.obj = {};
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor; const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
const fn = new AsyncFunction('$$', this.preamble + code + this.postamble).bind(this.obj, IMPORTS); const fn = new AsyncFunction('$$', this.preamble + code + this.postamble).bind(this.obj, IMPORTS);
await fn.call(this); await fn.call(this);
this.checkResult(); this.checkResult(this.obj, new Set(), []);
} }
checkResult() { // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
for (var [key, value] of Object.entries(this.obj)) { // TODO: return initial location of thingie
if (value instanceof Promise) { checkResult(o, checked: Set<object>, fullkey: string[]) {
throw new Error(`'${key}' is unresolved. Use 'await' before expression.`) // TODO? 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; return cells;
} }
extractErrors(e: Error): WorkerError[] { extractErrors(e: Error): WorkerError[] {
if (e['loc'] != null) { let loc = e['loc'];
if (loc && loc.start && loc.end) {
return [{ return [{
path: this.path, path: this.path,
msg: e.message, msg: e.message,
line: e['loc'].line, line: loc.start.line,
start: e['loc'].column, 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 // TODO: Cannot parse given Error object
var frames = ErrorStackParser.parse(e); let frames = ErrorStackParser.parse(e);
var frame = frames.find(f => f.functionName === 'anonymous'); let frame = frames.findIndex(f => f.functionName === 'anonymous');
return [{ let errors = [];
path: this.path, while (frame >= 0) {
msg: e.message, console.log(frames[frame]);
line: frame ? frame.lineNumber - LINE_NUMBER_OFFSET : 0, if (frames[frame].fileName.endsWith('Function')) {
start: frame ? frame.columnNumber : 0, 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 * as fastpng from 'fast-png';
import { convertWordsToImages, PixelEditorImageFormat } from '../../../ide/pixeleditor'; import { Palette } from './color';
import { arrayCompare } from '../../util';
import * as io from './io' 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( constructor(
public readonly width: number, public readonly width: number,
public readonly height: 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 public readonly rgba: Uint32Array
constructor( constructor(
width: number, width: number,
height: number, height: number,
initial?: Uint32Array | PixelMapFunction
) { ) {
super(width, height); super(width, height);
this.rgba = new Uint32Array(this.width * this.height); this.rgba = new Uint32Array(this.width * this.height);
if (initial) this.assign(initial);
} }
setPixel(x: number, y: number, rgba: number): void { setarray(arr: ArrayLike<number>) {
this.rgba[y * this.width + x] = rgba; this.rgba.set(arr);
return this;
} }
getPixel(x: number, y: number): number { set(x: number, y: number, rgba: number) {
return this.rgba[y * this.width + x]; 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 public readonly pixels: Uint8Array
constructor( constructor(
width: number, width: number,
height: number, height: number,
public readonly bitsPerPixel: number, public readonly bitsPerPixel: number,
pixels?: Uint8Array initial?: Uint8Array | PixelMapFunction
) { ) {
super(width, height); 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);
} }
setarray(arr: ArrayLike<number>) {
setPixel(x: number, y: number, index: number): void { this.pixels.set(arr);
this.pixels[y * this.width + x] = index; return this;
} }
getPixel(x: number, y: number): number { set(x: number, y: number, index: number) {
return this.pixels[y * this.width + x]; 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; abstract getRGBAForIndex(index: number): number;
} }
export class Palette {
colors: Uint32Array;
constructor(numColors: number) {
this.colors = new Uint32Array(numColors);
}
}
export class IndexedBitmap extends MappedBitmap { export class IndexedBitmap extends MappedBitmap {
public palette: Palette; public palette: Palette;
@ -67,45 +115,46 @@ export class IndexedBitmap extends MappedBitmap {
width: number, width: number,
height: number, height: number,
bitsPerPixel: number, bitsPerPixel: number,
pixels?: Uint8Array initial?: Uint8Array | PixelMapFunction
) { ) {
super(width, height, bitsPerPixel, pixels); super(width, height, bitsPerPixel || 8, initial);
this.palette = getDefaultPalette(bitsPerPixel); this.palette = color.palette.colors(this.bitsPerPixel);
} }
getRGBAForIndex(index: number): number { getRGBAForIndex(index: number): number {
return this.palette.colors[index]; return this.palette.colors[index];
} }
} blank(width: number, height: number) : IndexedBitmap {
let bitmap = new IndexedBitmap(width, height, this.bitsPerPixel);
// TODO? bitmap.palette = this.palette;
function getDefaultPalette(bpp: number) { return bitmap;
var pal = new Palette(1 << bpp); }
for (var i=0; i<pal.colors.length; i++) { clone() : IndexedBitmap {
pal.colors[i] = 0xff000000 | (i * 7919); let bitmap = this.blank(this.width, this.height);
bitmap.pixels.set(this.pixels);
return bitmap;
} }
return pal;
} }
export function rgbaToUint32(rgba: number[]): number { export function rgba(width: number, height: number, initial?: Uint32Array | PixelMapFunction) {
let v = 0; return new RGBABitmap(width, height, initial);
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) { export function indexed(width: number, height: number, bpp: number, initial?: Uint8Array | PixelMapFunction) {
return new RGBABitmap(width, height); return new IndexedBitmap(width, height, bpp, initial);
}
export function indexed(width: number, height: number, bpp: number) {
return new IndexedBitmap(width, height, bpp);
} }
export type BitmapType = RGBABitmap | IndexedBitmap; 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 namespace png {
export function read(url: string): BitmapType { export function read(url: string): BitmapType {
return decode(io.readbin(url)); return decode(io.readbin(url));
@ -126,13 +175,22 @@ export namespace png {
var palette = new Palette(palarr.length); var palette = new Palette(palarr.length);
for (let i = 0; i < palarr.length; i++) { for (let i = 0; i < palarr.length; i++) {
// TODO: alpha? // 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); let bitmap = new IndexedBitmap(png.width, png.height, png.depth);
for (let i = 0; i < bitmap.pixels.length; i++) { if (png.depth == 8) {
bitmap.pixels[i] = png.data[i]; 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; bitmap.palette = palette;
// TODO: aspect etc
return bitmap; return bitmap;
} }
function convertRGBAToBitmap(png: fastpng.IDecodedPNG): RGBABitmap { function convertRGBAToBitmap(png: fastpng.IDecodedPNG): RGBABitmap {
@ -141,17 +199,172 @@ export namespace png {
for (let i = 0; i < bitmap.rgba.length; i++) { for (let i = 0; i < bitmap.rgba.length; i++) {
for (let j = 0; j < 4; j++) for (let j = 0; j < 4; j++)
rgba[j] = png.data[i * 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; return bitmap;
} }
} }
// TODO: check arguments export namespace font {
export function decode(arr: Uint8Array, fmt: PixelEditorImageFormat) { interface Font {
var pixels = convertWordsToImages(arr, fmt); maxheight: number;
// TODO: guess if missing w/h/count? glyphs: { [code: number]: Glyph };
// TODO: reverse mapping properties: {};
// TODO: maybe better composable functions }
return pixels.map(data => new IndexedBitmap(fmt.w, fmt.h, fmt.bpp|1, data)); 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 import { FileData, WorkingStore } from "../../workertypes";
var $$fs: ProjectFilesystem;
var $$cache: { [path: string]: FileData } = {};
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) { // object that can load state from backing store
$$fs = fs; export interface Loadable {
reset() : void;
$$getstate() : {};
} }
function getFS(): ProjectFilesystem { export namespace data {
if ($$fs == null) throw new Error(`Internal Error: The 'io' module has not been set up properly.`) export function get(object: Loadable, key: string): Loadable {
return $$fs; let override = $$data && $$data[key];
} if (override) Object.assign(object, override);
else if (object.reset) object.reset();
export function ___load(path: string): FileData { return object;
var data = $$cache[path]; }
if (data == null) { export function set(object: Loadable, key: string): Loadable {
getFS().getFileData(path).then((value) => { if ($$data && object.$$getstate) {
$$cache[path] = value; $$data[key] = object.$$getstate();
}) }
throw new IOWaitError(path); return object;
} else {
return data;
} }
} }
export class IOWaitError extends Error {
}
export function canonicalurl(url: string) : string { export function canonicalurl(url: string) : string {
// get raw resource URL for github // get raw resource URL for github
@ -42,24 +53,50 @@ export function canonicalurl(url: string) : string {
return url; return url;
} }
export function read(url: string, type?: 'binary' | 'text'): FileData { export function clearcache() {
url = canonicalurl(url); $$cache = new WeakMap();
}
export function fetchurl(url: string, type?: 'binary' | 'text'): FileData {
// TODO: only works in web worker // TODO: only works in web worker
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.responseType = type === 'text' ? 'text' : 'arraybuffer'; xhr.responseType = type === 'text' ? 'text' : 'arraybuffer';
xhr.open("GET", url, false); // synchronous request xhr.open("GET", url, false); // synchronous request
xhr.send(null); xhr.send(null);
if (xhr.response != null && xhr.status == 200) { if (xhr.response != null && xhr.status == 200) {
if (type !== 'text') { if (type === 'text') {
return new Uint8Array(xhr.response); return xhr.response as string;
} else { } else {
return xhr.response; return new Uint8Array(xhr.response);
} }
} else { } else {
throw new Error(`The resource at "${url}" responded with status code of ${xhr.status}.`) 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 { export function readbin(url: string): Uint8Array {
var data = read(url, 'binary'); var data = read(url, 'binary');
if (data instanceof Uint8Array) if (data instanceof Uint8Array)
@ -67,3 +104,11 @@ export function readbin(url: string): Uint8Array {
else else
throw new Error(`The resource at "${url}" is not a binary file.`); 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 { BitmapType, IndexedBitmap, RGBABitmap } from "../lib/bitmap";
import { Component, render, h, ComponentType } from 'preact'; import { Component, render, h, ComponentType } from 'preact';
import { Cell } from "../env"; import { Cell } from "../env";
import { rgb2bgr } from "../../util"; import { hex, rgb2bgr, RGBA } from "../../util";
import { dumpRAM } from "../../emu"; 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 { interface ColorComponentProps {
rgbavalue: number; rgbavalue: number;
@ -12,7 +18,7 @@ interface ColorComponentProps {
class ColorComponent extends Component<ColorComponentProps> { class ColorComponent extends Component<ColorComponentProps> {
render(virtualDom, containerNode, replaceNode) { render(virtualDom, containerNode, replaceNode) {
let rgb = this.props.rgbavalue & 0xffffff; let rgb = this.props.rgbavalue & 0xffffff;
var htmlcolor = `#${rgb2bgr(rgb).toString(16)}`; var htmlcolor = `#${hex(rgb2bgr(rgb),6)}`;
var textcol = (rgb & 0x008000) ? 'black' : 'white'; var textcol = (rgb & 0x008000) ? 'black' : 'white';
return h('div', { return h('div', {
class: 'scripting-color', class: 'scripting-color',
@ -41,12 +47,11 @@ class BitmapComponent extends Component<BitmapComponentProps> {
return h('canvas', { return h('canvas', {
class: 'pixelated', class: 'pixelated',
width: this.props.width, width: this.props.width,
height: this.props.height height: this.props.height,
...this.props
}); });
} }
componentDidMount() { componentDidMount() {
this.canvas = this.base as HTMLCanvasElement;
this.prepare();
this.refresh(); this.refresh();
} }
componentWillUnmount() { componentWillUnmount() {
@ -58,13 +63,16 @@ class BitmapComponent extends Component<BitmapComponentProps> {
this.refresh(); this.refresh();
} }
prepare() { prepare() {
this.canvas = this.base as HTMLCanvasElement;
this.ctx = this.canvas.getContext('2d'); this.ctx = this.canvas.getContext('2d');
this.imageData = this.ctx.createImageData(this.canvas.width, this.canvas.height); this.imageData = this.ctx.createImageData(this.canvas.width, this.canvas.height);
this.datau32 = new Uint32Array(this.imageData.data.buffer); this.datau32 = new Uint32Array(this.imageData.data.buffer);
} }
refresh() { refresh() {
// preact can reuse this component but it can change shape :^P // 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.prepare();
} }
this.updateCanvas(this.datau32, this.props.bitmap); 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 { interface ObjectTreeComponentProps {
name?: string;
object: {} | []; object: {} | [];
objpath: string;
} }
interface ObjectTreeComponentState { interface ObjectTreeComponentState {
expanded : boolean; expanded: boolean;
} }
class ObjectTreeComponent extends Component<ObjectTreeComponentProps, ObjectTreeComponentState> { class ObjectKeyValueComponent extends Component<ObjectTreeComponentProps, ObjectTreeComponentState> {
render(virtualDom, containerNode, replaceNode) { render(virtualDom, containerNode, replaceNode) {
if (this.state.expanded) { let expandable = typeof this.props.object === 'object';
var minus = h('span', { onClick: () => this.toggleExpand() }, [ '-' ]); let hdrclass = '';
return h('minus', { }, [ if (expandable)
minus, hdrclass = this.state.expanded ? 'tree-expanded' : 'tree-collapsed'
getShortName(this.props.object), return h('div', {
objectToContentsDiv(this.props.object) class: 'tree-content',
]); key: `${this.props.objpath}__tree`
} else { }, [
var plus = h('span', { onClick: () => this.toggleExpand() }, [ '+' ]); h('div', {
return h('div', { }, [ class: 'tree-header ' + hdrclass,
plus, onClick: expandable ? () => this.toggleExpand() : null
getShortName(this.props.object) }, [
]); this.props.name + "",
} h('span', { class: 'tree-value' }, [
getShortName(this.props.object)
])
]),
this.state.expanded ? objectToContentsDiv(this.props.object, this.props.objpath) : null
]);
} }
toggleExpand() { toggleExpand() {
this.setState({ expanded: !this.state.expanded }); this.setState({ expanded: !this.state.expanded });
@ -128,67 +193,133 @@ function getShortName(object: any) {
} }
return s; return s;
} catch (e) { } catch (e) {
return 'object'; console.log(e);
return e + "";
} }
} else { } else {
return object+""; return primitiveToString(object);
} }
} }
// TODO: need id? function primitiveToString(obj) {
function objectToDiv(object: any) { var text = "";
var props = { class: '' }; // 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 = []; var children = [];
// TODO: tile editor // TODO: tile editor
// TODO: limit # of items // TODO: limit # of items
// TODO: detect table // TODO: detect table
if (Array.isArray(object)) { //return objectToContentsDiv(object);
return objectToContentsDiv(object); if (object == null) {
} else if (object['bitsPerPixel'] && object['pixels'] && object['palette']) { return object + "";
addBitmapComponent(children, object as IndexedBitmap); } else if (object['uitype']) {
} else if (object['rgba'] instanceof Uint32Array) { children.push(h(UISliderComponent, { iokey: objpath, uiobject: object }));
addBitmapComponent(children, object as RGBABitmap); } else if (object['literaltext']) {
} else if (object['colors'] instanceof Uint32Array) { 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 // TODO: make sets of 2/4/8/16/etc
props.class += ' scripting-grid '; props.class += ' scripting-grid ';
object['colors'].forEach((val) => { object.colors.forEach((val) => {
children.push(h(ColorComponent, { rgbavalue: val })); children.push(h(ColorComponent, { rgbavalue: val }));
}) })
} else if (typeof object === 'object') {
children.push(h(ObjectTreeComponent, { object }));
} else { } else {
children.push(JSON.stringify(object)); return h(ObjectKeyValueComponent, { name, object, objpath }, []);
} }
let div = h('div', props, children); let div = h('div', props, children);
return div; 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? // is typed array?
let bpel = object['BYTES_PER_ELEMENT']; let bpel = object['BYTES_PER_ELEMENT'];
if (typeof bpel === 'number') { if (typeof bpel === 'number') {
const maxBytes = 0x100; return fixedArrayToDiv(object as Array<number>, bpel, objpath);
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);
}
} }
let objectEntries = Object.entries(object); let objectEntries = Object.entries(object);
// TODO: id? let objectDivs = objectEntries.map(entry => objectToDiv(entry[1], entry[0], `${objpath}.${entry[1]}`));
let objectDivs = objectEntries.map(entry => objectToDiv(entry[1])); return h('div', {}, objectDivs);
return h('div', { }, objectDivs);
} }
function addBitmapComponent(children, bitmap: BitmapType) { 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 { export class Notebook {
@ -201,9 +332,18 @@ export class Notebook {
} }
updateCells(cells: Cell[]) { updateCells(cells: Cell[]) {
let hTree = cells.map(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 '; cellDiv.props['class'] += ' scripting-cell ';
return cellDiv; return cellDiv;
*/
}); });
render(hTree, this.maindiv); 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 { export interface Dependency {
path:string, path:string
filename:string, filename:string
link:boolean, link:boolean
data:FileData // TODO: or binary? data:FileData // TODO: or binary?
} }
export interface WorkerFileUpdate { export interface WorkerFileUpdate {
path:string, path:string
data:FileData data:FileData
}; };
export interface WorkerBuildStep { export interface WorkerBuildStep {
path?:string path?:string
files?:string[]
platform:string platform:string
tool:string tool:string
mainfile?:boolean mainfile?:boolean
}; };
export interface WorkerItemUpdate {
key:string
value:object
};
export interface WorkerMessage extends WorkerBuildStep { // TODO: split into different msg types
preload:string, export interface WorkerMessage {
reset:boolean, preload?:string
code:string, platform?:string
updates:WorkerFileUpdate[], tool?:string
updates:WorkerFileUpdate[]
buildsteps:WorkerBuildStep[] buildsteps:WorkerBuildStep[]
reset?:boolean
code?:string
setitems?:WorkerItemUpdate[]
} }
export interface WorkerError extends SourceLocation { export interface WorkerError extends SourceLocation {
@ -87,10 +96,10 @@ export interface WorkerError extends SourceLocation {
} }
export interface CodeListing { export interface CodeListing {
lines:SourceLine[], lines:SourceLine[]
asmlines?:SourceLine[], asmlines?:SourceLine[]
text?:string, text?:string
sourcefile?:SourceFile, // not returned by worker sourcefile?:SourceFile // not returned by worker
assemblyfile?: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> { export function isOutputResult(result: WorkerResult) : result is WorkerOutputResult<any> {
return ('output' in result); 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 { hex, rgb2bgr, rle_unpack } from "../common/util";
import { ProjectWindows } from "./windows"; import { ProjectWindows } from "./windows";
import { Toolbar } from "../common/emu"; import { Toolbar } from "../common/toolbar";
import Mousetrap = require('mousetrap'); import Mousetrap = require('mousetrap');
export type UintArray = number[] | Uint8Array | Uint16Array | Uint32Array; //{[i:number]:number}; 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 { getFilenamePrefix, getFolderForPath, isProbablyBinary, getBasePlatform, getWithBinary } from "../common/util";
import { Platform } from "../common/baseplatform"; import { Platform } from "../common/baseplatform";
import localforage from "localforage"; import localforage from "localforage";
@ -100,6 +100,7 @@ export class CodeProject {
isCompiling : boolean = false; isCompiling : boolean = false;
filename2path = {}; // map stripped paths to full paths filename2path = {}; // map stripped paths to full paths
filesystem : ProjectFilesystem; filesystem : ProjectFilesystem;
dataItems : WorkerItemUpdate[];
callbackBuildResult : BuildResultCallback; callbackBuildResult : BuildResultCallback;
callbackBuildStatus : BuildStatusCallback; callbackBuildStatus : BuildStatusCallback;
@ -152,7 +153,13 @@ export class CodeProject {
parseIncludeDependencies(text:string):string[] { parseIncludeDependencies(text:string):string[] {
let files = []; let files = [];
let m; 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 // include verilog includes
let re1 = /^\s*(`include|[.]include)\s+"(.+?)"/gmi; let re1 = /^\s*(`include|[.]include)\s+"(.+?)"/gmi;
while (m = re1.exec(text)) { while (m = re1.exec(text)) {
@ -234,7 +241,7 @@ export class CodeProject {
} }
okToSend():boolean { okToSend():boolean {
return this.pendingWorkerMessages++ == 0; return this.pendingWorkerMessages++ == 0 && this.mainPath != null;
} }
updateFileInStore(path:string, text:FileData) { updateFileInStore(path:string, text:FileData) {
@ -242,9 +249,9 @@ export class CodeProject {
} }
// TODO: test duplicate files, local paths mixed with presets // TODO: test duplicate files, local paths mixed with presets
buildWorkerMessage(depends:Dependency[]) { buildWorkerMessage(depends:Dependency[]) : WorkerMessage {
this.preloadWorker(this.mainPath); this.preloadWorker(this.mainPath);
var msg = {updates:[], buildsteps:[]}; var msg : WorkerMessage = {updates:[], buildsteps:[]};
// TODO: add preproc directive for __MAINFILE__ // TODO: add preproc directive for __MAINFILE__
var mainfilename = this.stripLocalPath(this.mainPath); var mainfilename = this.stripLocalPath(this.mainPath);
var maintext = this.getFile(this.mainPath); var maintext = this.getFile(this.mainPath);
@ -275,6 +282,7 @@ export class CodeProject {
tool:this.platform.getToolForFilename(dep.path)}); tool:this.platform.getToolForFilename(dep.path)});
} }
} }
if (this.dataItems) msg.setitems = this.dataItems;
return msg; return msg;
} }
@ -350,7 +358,7 @@ export class CodeProject {
if (this.filedata[path] == text) return; // unchanged, don't update if (this.filedata[path] == text) return; // unchanged, don't update
this.updateFileInStore(path, text); // TODO: isBinary this.updateFileInStore(path, text); // TODO: isBinary
this.filedata[path] = text; this.filedata[path] = text;
if (this.okToSend() && this.mainPath) { if (this.okToSend()) {
if (this.callbackBuildStatus) this.callbackBuildStatus(true); if (this.callbackBuildStatus) this.callbackBuildStatus(true);
this.sendBuild(); this.sendBuild();
} }
@ -411,6 +419,13 @@ export class CodeProject {
return path; 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 { 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 { WorkerResult, WorkerOutputResult, WorkerError, FileData, WorkerErrorResult } from "../common/workertypes";
import { ProjectWindows } from "./windows"; import { ProjectWindows } from "./windows";
import { Platform, Preset, DebugSymbols, DebugEvalCondition, isDebuggable, EmuState } from "../common/baseplatform"; 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, import { getFilenameForPath, getFilenamePrefix, highlightDifferences, byteArrayToString, compressLZG, stringToByteArray,
byteArrayToUTF8, isProbablyBinary, getWithBinary, getBasePlatform, getRootBasePlatform, hex, loadScript, decodeQueryString, parseBool } from "../common/util"; byteArrayToUTF8, isProbablyBinary, getWithBinary, getBasePlatform, getRootBasePlatform, hex, loadScript, decodeQueryString, parseBool } from "../common/util";
import { StateRecorderImpl } from "../common/recorder"; 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"; import { VirtualList } from "../common/vlist";
const BUILTIN_INPUT_PORTS = [ const BUILTIN_INPUT_PORTS = [

View File

@ -1,7 +1,7 @@
import { PLATFORMS, RasterVideo } from "../common/emu"; import { PLATFORMS, RasterVideo } from "../common/emu";
import { Platform } from "../common/baseplatform"; import { Platform } from "../common/baseplatform";
import { Cell } from "../common/script/env"; import { RunResult } from "../common/script/env";
import { Notebook } from "../common/script/ui/notebook"; import { Notebook } from "../common/script/ui/notebook";
class ScriptingPlatform implements Platform { class ScriptingPlatform implements Platform {
@ -31,11 +31,12 @@ class ScriptingPlatform implements Platform {
} }
resume() { resume() {
} }
loadROM(title, cells: Cell[]) { loadROM(title, run: RunResult) {
this.notebook.updateCells(cells); this.notebook.updateCells(run.cells);
// TODO: save state file
} }
isRunning() { isRunning() {
return false; return true;
} }
isDebugging(): boolean { isDebugging(): boolean {
return false; return false;

View File

@ -1,11 +1,12 @@
import { Environment } from "../../common/script/env";
import { BuildStep, BuildStepResult, emglobal, store } from "../workermain"; import { BuildStep, BuildStepResult, emglobal, store } from "../workermain";
import { Environment, RunResult } from "../../common/script/env";
import * as io from "../../common/script/lib/io";
// cache environments // 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]; var env = environments[path];
if (!env) { if (!env) {
env = environments[path] = new Environment(emglobal, path); env = environments[path] = new Environment(emglobal, path);
@ -14,16 +15,20 @@ function getEnv(path: string) : Environment {
return env; return env;
} }
export async function runJavascript(step: BuildStep) : Promise<BuildStepResult> { export async function runJavascript(step: BuildStep): Promise<BuildStepResult> {
var env = getEnv(step.path); var env = getEnv(step.path);
var code = store.getFileAsString(step.path); var code = store.getFileAsString(step.path);
try { try {
io.$$setupFS(store);
io.$$loadData(store.items); // TODO: load from file
await env.run(code); await env.run(code);
let cells = env.render();
let state = env.getLoadableState();
let output : RunResult = { cells, state };
return { output: output };
} catch (e) { } 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 type { WorkerResult, WorkerBuildStep, WorkerMessage, WorkerError, SourceLine, WorkerErrorResult, WorkingStore } from "../common/workertypes";
import { getBasePlatform, getRootBasePlatform, hex } from "../common/util"; import { getBasePlatform, getRootBasePlatform } from "../common/util";
/// <reference types="emscripten" /> /// <reference types="emscripten" />
export interface EmscriptenModule { export interface EmscriptenModule {
@ -394,9 +394,10 @@ export interface BuildStep extends WorkerBuildStep {
/// ///
export class FileWorkingStore { export class FileWorkingStore implements WorkingStore {
workfs : {[path:string]:FileEntry} = {}; workfs : {[path:string]:FileEntry} = {};
workerseq : number = 0; workerseq : number = 0;
items : {} = {};
constructor() { constructor() {
this.reset(); this.reset();
@ -438,12 +439,19 @@ export class FileWorkingStore {
getFileEntry(path:string) : FileEntry { getFileEntry(path:string) : FileEntry {
return this.workfs[path]; return this.workfs[path];
} }
setItem(key: string, value: object) {
this.items[key] = value;
}
} }
export var store = new FileWorkingStore(); export var store = new FileWorkingStore();
/// ///
function errorResult(msg: string) : WorkerErrorResult {
return { errors:[{ line:0, msg:msg }]};
}
class Builder { class Builder {
steps : BuildStep[] = []; steps : BuildStep[] = [];
startseq : number = 0; startseq : number = 0;
@ -465,7 +473,7 @@ class Builder {
step.result = await toolfn(step); step.result = await toolfn(step);
} catch (e) { } catch (e) {
console.log("EXCEPTION", e, e.stack); 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) { if (step.result) {
(step.result as any).params = step.params; // TODO: type check (step.result as any).params = step.params; // TODO: type check
@ -513,10 +521,11 @@ class Builder {
this.steps = []; this.steps = [];
// file updates // file updates
if (data.updates) { if (data.updates) {
for (var i=0; i<data.updates.length; i++) { data.updates.forEach((u) => store.putFile(u.path, u.data));
var u = data.updates[i]; }
store.putFile(u.path, u.data); // object update
} if (data.setitems) {
data.setitems.forEach((i) => store.setItem(i.key, i.value));
} }
// build steps // build steps
if (data.buildsteps) { if (data.buildsteps) {
@ -524,7 +533,7 @@ class Builder {
} }
// single-file // single-file
if (data.code) { if (data.code) {
this.steps.push(data); this.steps.push(data as BuildStep); // TODO: remove cast
} }
// execute build steps // execute build steps
if (this.steps.length) { 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" // //main.c:2: error: Can't open include file "stdiosd.h"
var errors = extractErrors(/([^:]+):(\d+): (.+)/, errout.split("\n"), step.path, 2, 3, 1); var errors = extractErrors(/([^:]+):(\d+): (.+)/, errout.split("\n"), step.path, 2, 3, 1);
if (errors.length == 0) { if (errors.length == 0) {
errors = [{line:0, msg:errout}]; errors = errorResult(errout).errors;
} }
return {errors: errors}; return {errors: errors};
} }
@ -1124,7 +1133,12 @@ if (ENVIRONMENT_IS_WORKER) {
var result = await lastpromise; var result = await lastpromise;
lastpromise = null; lastpromise = null;
if (result) { 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 assert = require('assert');
var wtu = require('./workertestutils.js');
const dom = createTestDOM();
var pixed = require("gen/ide/pixeleditor.js"); var pixed = require("gen/ide/pixeleditor.js");
function dumbEqual(a,b) { function dumbEqual(a,b) {

View File

@ -2,7 +2,7 @@
var assert = require('assert'); var assert = require('assert');
var fs = require('fs'); var fs = require('fs');
var wtu = require('./workertestutils.js'); var wtu = require('./workertestutils.js');
var PNG = require('pngjs').PNG; var fastpng = require('fast-png');
const dom = createTestDOM(); const dom = createTestDOM();
includeInThisContext('gen/common/cpu/6809.js'); includeInThisContext('gen/common/cpu/6809.js');
@ -173,9 +173,7 @@ async function testPlatform(platid, romname, maxframes, callback) {
} }
// record video to file // record video to file
if (lastrastervideo) { if (lastrastervideo) {
var png = new PNG({width:lastrastervideo.width, height:lastrastervideo.height}); var pngbuffer = fastpng.encode(lastrastervideo.getImageData())
png.data = lastrastervideo.getImageData().data;
var pngbuffer = PNG.sync.write(png);
assert(pngbuffer.length > 500); // make sure PNG is big enough assert(pngbuffer.length > 500); // make sure PNG is big enough
try { fs.mkdirSync("./test"); } catch(e) { } try { fs.mkdirSync("./test"); } catch(e) { }
try { fs.mkdirSync("./test/output"); } 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 wtu = require('./workertestutils.js');
//var heapdump = require('heapdump'); //var heapdump = require('heapdump');
// TODO: await might be needed later
global.onmessage({data:{preload:'cc65', platform:'nes'}}); global.onmessage({data:{preload:'cc65', platform:'nes'}});
global.onmessage({data:{preload:'ca65', platform:'nes'}}); global.onmessage({data:{preload:'ca65', platform:'nes'}});
global.onmessage({data:{preload:'cc65', platform:'apple2'}}); 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); doBuild([msg], callback, outlen, nlines, nerrors, options);
} }
async function doBuild(msgs, callback, outlen, nlines, nerrors, options) {
function doBuild(msgs, callback, outlen, nlines, nerrors, options) {
var msgcount = msgs.length; var msgcount = msgs.length;
global.postMessage = function(msg) { global.postMessage = function(msg) {
if (!msg.unchanged) { if (!msg.unchanged) {
@ -74,9 +73,9 @@ function doBuild(msgs, callback, outlen, nlines, nerrors, options) {
} else } else
console.log(msgcount + ' msgs left'); console.log(msgcount + ' msgs left');
}; };
global.onmessage({data:{reset:true}}); await global.onmessage({data:{reset:true}});
for (var i=0; i<msgs.length; i++) { for (var i=0; i<msgs.length; i++) {
global.onmessage({data:msgs[i]}); await global.onmessage({data:msgs[i]});
} }
} }