scripting: working on notebook, functions, files, setItem(), fixed tests
This commit is contained in:
parent
7f86ed0cb6
commit
6134a8c89c
15
css/ui.css
15
css/ui.css
|
@ -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;
|
||||||
}
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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];
|
||||||
|
}
|
||||||
|
|
|
@ -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} {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) { }
|
||||||
|
|
|
@ -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]});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue