mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2024-10-03 18:54:44 +00:00
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;
|
||||
color: #99cc99;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
font-family: "Andale Mono", "Menlo", "Lucida Console", monospace;
|
||||
word-wrap: break-word;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.scripting-cell canvas {
|
||||
height: 15vw;
|
||||
height: 20vw;
|
||||
max-width: 95%;
|
||||
border: 2px solid #222;
|
||||
outline-color: #ccc;
|
||||
background: #000;
|
||||
padding: 6px;
|
||||
margin: 6px;
|
||||
pointer-events:auto;
|
||||
}
|
||||
.scripting-cell canvas:focus {
|
||||
.scripting-cell canvas:hover {
|
||||
outline:none;
|
||||
border-color:#888;
|
||||
}
|
||||
.scripting-cell div {
|
||||
display: inline;
|
||||
border-color:#ccc;
|
||||
}
|
||||
div.scripting-color {
|
||||
padding:0.1em;
|
||||
@ -800,4 +798,5 @@ div.scripting-color {
|
||||
div.scripting-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat( auto-fit, minmax(2em, 1fr) );
|
||||
justify-items: center;
|
||||
}
|
@ -67,7 +67,7 @@
|
||||
"tsbuild": "tsc --build tsconfig.json",
|
||||
"esbuild": "npm run esbuild-worker && npm run esbuild-ui",
|
||||
"esbuild-clean": "rm -f ./gen/*.*",
|
||||
"esbuild-worker": "esbuild src/worker/workermain.ts --bundle --minify --sourcemap --target=es2017 --outfile=./gen/worker/bundle.js",
|
||||
"esbuild-worker": "esbuild src/worker/workermain.ts --bundle --sourcemap --target=es2017 --outfile=./gen/worker/bundle.js",
|
||||
"esbuild-ui": "esbuild src/ide/ui.ts src/ide/embedui.ts --splitting --format=esm --bundle --minify --sourcemap --target=es2017 --outdir=./gen/ --external:path --external:fs",
|
||||
"test-one": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000",
|
||||
"test-node": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000 test/cli",
|
||||
|
47
presets/script/mandel.js
Normal file
47
presets/script/mandel.js
Normal 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];
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
|
||||
import { hex, clamp, lpad } from "./util";
|
||||
import { SourceLocation } from "./workertypes";
|
||||
import Mousetrap = require('mousetrap');
|
||||
import { VirtualList } from "./vlist"
|
||||
|
||||
// external modules
|
||||
@ -654,55 +653,6 @@ export function newAddressDecoder(table : AddressDecoderEntry[], options?:Addres
|
||||
return new (AddressDecoder as any)(table, options);
|
||||
}
|
||||
|
||||
/// TOOLBAR
|
||||
|
||||
export class Toolbar {
|
||||
span : JQuery;
|
||||
grp : JQuery;
|
||||
mousetrap;
|
||||
boundkeys = [];
|
||||
|
||||
constructor(parentDiv:HTMLElement, focusDiv:HTMLElement) {
|
||||
this.mousetrap = focusDiv ? new Mousetrap(focusDiv) : Mousetrap;
|
||||
this.span = $(document.createElement("span")).addClass("btn_toolbar");
|
||||
parentDiv.appendChild(this.span[0]);
|
||||
this.newGroup();
|
||||
}
|
||||
destroy() {
|
||||
if (this.span) {
|
||||
this.span.remove();
|
||||
this.span = null;
|
||||
}
|
||||
if (this.mousetrap) {
|
||||
for (var key of this.boundkeys) {
|
||||
this.mousetrap.unbind(key);
|
||||
}
|
||||
this.mousetrap = null;
|
||||
}
|
||||
}
|
||||
newGroup() {
|
||||
return this.grp = $(document.createElement("span")).addClass("btn_group").appendTo(this.span).hide();
|
||||
}
|
||||
add(key:string, alttext:string, icon:string, fn:(e,combo) => void) {
|
||||
var btn = null;
|
||||
if (icon) {
|
||||
btn = $(document.createElement("button")).addClass("btn");
|
||||
if (icon.startsWith('glyphicon')) {
|
||||
icon = '<span class="glyphicon ' + icon + '" aria-hidden="true"></span>';
|
||||
}
|
||||
btn.html(icon);
|
||||
btn.prop("title", key ? (alttext+" ("+key+")") : alttext);
|
||||
btn.click(fn);
|
||||
this.grp.append(btn).show();
|
||||
}
|
||||
if (key) {
|
||||
this.mousetrap.bind(key, fn);
|
||||
this.boundkeys.push(key);
|
||||
}
|
||||
return btn;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/17130395/real-mouse-position-in-canvas
|
||||
export function getMousePos(canvas : HTMLCanvasElement, evt) : {x:number,y:number} {
|
||||
|
@ -1,23 +1,32 @@
|
||||
|
||||
import { WorkerError } from "../workertypes";
|
||||
import ErrorStackParser = require("error-stack-parser");
|
||||
import yufka from 'yufka';
|
||||
import * as bitmap from "./lib/bitmap";
|
||||
import * as io from "./lib/io";
|
||||
import * as output from "./lib/output";
|
||||
import { escapeHTML } from "../util";
|
||||
import * as color from "./lib/color";
|
||||
import * as scriptui from "./lib/scriptui";
|
||||
|
||||
export interface Cell {
|
||||
id: string;
|
||||
object?: any;
|
||||
}
|
||||
|
||||
export interface RunResult {
|
||||
cells: Cell[];
|
||||
state: {};
|
||||
}
|
||||
|
||||
const IMPORTS = {
|
||||
'bitmap': bitmap,
|
||||
'io': io,
|
||||
'output': output
|
||||
'output': output,
|
||||
'color': color,
|
||||
'ui': scriptui,
|
||||
}
|
||||
|
||||
const LINE_NUMBER_OFFSET = 3;
|
||||
const LINE_NUMBER_OFFSET = 3; // TODO: shouldnt need?
|
||||
|
||||
const GLOBAL_BADLIST = [
|
||||
'eval'
|
||||
@ -34,10 +43,18 @@ const GLOBAL_GOODLIST = [
|
||||
'Uint8Array', 'Uint16Array', 'Uint32Array', 'Uint8ClampedArray',
|
||||
]
|
||||
|
||||
class RuntimeError extends Error {
|
||||
constructor(public loc: acorn.SourceLocation, msg: string) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export class Environment {
|
||||
preamble: string;
|
||||
postamble: string;
|
||||
obj: {};
|
||||
seq: number;
|
||||
declvars : {[name : string] : acorn.Node};
|
||||
|
||||
constructor(
|
||||
public readonly globalenv: any,
|
||||
@ -51,45 +68,100 @@ export class Environment {
|
||||
this.preamble += '{\n';
|
||||
this.postamble = '\n}';
|
||||
}
|
||||
error(varname: string, msg: string) {
|
||||
console.log(varname, this.declvars[varname]);
|
||||
throw new RuntimeError(this.declvars && this.declvars[varname].loc, msg);
|
||||
}
|
||||
preprocess(code: string): string {
|
||||
var declvars = {};
|
||||
const result = yufka(code, (node, { update, source, parent }) => {
|
||||
this.declvars = {};
|
||||
this.seq = 0;
|
||||
let options = {
|
||||
// https://www.npmjs.com/package/magic-string#sgeneratemap-options-
|
||||
sourceMap: {
|
||||
file: this.path,
|
||||
source: this.path,
|
||||
hires: false,
|
||||
includeContent: false
|
||||
},
|
||||
// https://github.com/acornjs/acorn/blob/master/acorn/README.md
|
||||
acorn: {
|
||||
ecmaVersion: 6 as any,
|
||||
locations: true,
|
||||
allowAwaitOutsideFunction: true,
|
||||
allowReturnOutsideFunction: true,
|
||||
allowReserved: true,
|
||||
}
|
||||
};
|
||||
const result = yufka(code, options, (node, { update, source, parent }) => {
|
||||
function isTopLevel() {
|
||||
return parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program';
|
||||
}
|
||||
let left = node['left'];
|
||||
switch (node.type) {
|
||||
// error on forbidden keywords
|
||||
case 'Identifier':
|
||||
if (GLOBAL_BADLIST.indexOf(source()) >= 0) {
|
||||
update(`__FORBIDDEN__KEYWORD__${source()}__`) // TODO? how to preserve line number?
|
||||
}
|
||||
break;
|
||||
// x = expr --> var x = expr (first use)
|
||||
case 'AssignmentExpression':
|
||||
// x = expr --> var x = expr (first use)
|
||||
if (parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program') { // TODO
|
||||
if (isTopLevel()) {
|
||||
if (left && left.type === 'Identifier') {
|
||||
if (!declvars[left.name]) {
|
||||
update(`var ${left.name}=this.${source()}`)
|
||||
declvars[left.name] = true;
|
||||
if (!this.declvars[left.name]) {
|
||||
update(`var ${left.name}=io.data.get(this.${source()}, ${JSON.stringify(left.name)})`)
|
||||
this.declvars[left.name] = left;
|
||||
} else {
|
||||
update(`${left.name}=this.${source()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
// literal comments
|
||||
case 'Literal':
|
||||
if (isTopLevel() && typeof node['value'] === 'string') {
|
||||
update(`this.$$doc__${this.seq++} = { literaltext: ${source()} };`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
});
|
||||
return result.toString();
|
||||
}
|
||||
async run(code: string): Promise<void> {
|
||||
// TODO: split into cells based on "--" linebreaks?
|
||||
code = this.preprocess(code);
|
||||
this.obj = {};
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
||||
const fn = new AsyncFunction('$$', this.preamble + code + this.postamble).bind(this.obj, IMPORTS);
|
||||
await fn.call(this);
|
||||
this.checkResult();
|
||||
this.checkResult(this.obj, new Set(), []);
|
||||
}
|
||||
checkResult() {
|
||||
for (var [key, value] of Object.entries(this.obj)) {
|
||||
if (value instanceof Promise) {
|
||||
throw new Error(`'${key}' is unresolved. Use 'await' before expression.`) // TODO?
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
|
||||
// TODO: return initial location of thingie
|
||||
checkResult(o, checked: Set<object>, fullkey: string[]) {
|
||||
if (o == null) return;
|
||||
if (checked.has(o)) return;
|
||||
if (typeof o === 'object') {
|
||||
if (o.length > 100) return; // big array, don't bother
|
||||
if (o.BYTES_PER_ELEMENT > 0) return; // typed array, don't bother
|
||||
checked.add(o); // so we don't recurse if cycle
|
||||
function prkey() { return fullkey.join('.') }
|
||||
for (var [key, value] of Object.entries(o)) {
|
||||
if (value == null && fullkey.length == 0) {
|
||||
this.error(key, `"${key}" has no value.`)
|
||||
}
|
||||
fullkey.push(key);
|
||||
if (typeof value === 'function') {
|
||||
this.error(fullkey[0], `"${prkey()}" is a function. Did you forget to pass parameters?`); // TODO? did you mean (needs to see entire expr)
|
||||
}
|
||||
if (typeof value === 'symbol') {
|
||||
this.error(fullkey[0], `"${prkey()}" is a Symbol, and can't be used.`) // TODO?
|
||||
}
|
||||
if (value instanceof Promise) {
|
||||
this.error(fullkey[0], `"${prkey()}" is unresolved. Use "await" before expression.`) // TODO?
|
||||
}
|
||||
this.checkResult(value, checked, fullkey);
|
||||
fullkey.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -106,22 +178,58 @@ export class Environment {
|
||||
return cells;
|
||||
}
|
||||
extractErrors(e: Error): WorkerError[] {
|
||||
if (e['loc'] != null) {
|
||||
let loc = e['loc'];
|
||||
if (loc && loc.start && loc.end) {
|
||||
return [{
|
||||
path: this.path,
|
||||
msg: e.message,
|
||||
line: e['loc'].line,
|
||||
start: e['loc'].column,
|
||||
line: loc.start.line,
|
||||
start: loc.start.column,
|
||||
end: loc.end.line,
|
||||
}]
|
||||
}
|
||||
if (loc && loc.line != null) {
|
||||
return [{
|
||||
path: this.path,
|
||||
msg: e.message,
|
||||
line: loc.line,
|
||||
start: loc.column,
|
||||
}]
|
||||
}
|
||||
// TODO: Cannot parse given Error object
|
||||
var frames = ErrorStackParser.parse(e);
|
||||
var frame = frames.find(f => f.functionName === 'anonymous');
|
||||
return [{
|
||||
path: this.path,
|
||||
msg: e.message,
|
||||
line: frame ? frame.lineNumber - LINE_NUMBER_OFFSET : 0,
|
||||
start: frame ? frame.columnNumber : 0,
|
||||
}];
|
||||
let frames = ErrorStackParser.parse(e);
|
||||
let frame = frames.findIndex(f => f.functionName === 'anonymous');
|
||||
let errors = [];
|
||||
while (frame >= 0) {
|
||||
console.log(frames[frame]);
|
||||
if (frames[frame].fileName.endsWith('Function')) {
|
||||
errors.push( {
|
||||
path: this.path,
|
||||
msg: e.message,
|
||||
line: frames[frame].lineNumber - LINE_NUMBER_OFFSET,
|
||||
start: frames[frame].columnNumber,
|
||||
} );
|
||||
}
|
||||
--frame;
|
||||
}
|
||||
if (errors.length == 0) {
|
||||
errors.push( {
|
||||
path: this.path,
|
||||
msg: e.message,
|
||||
line: 0
|
||||
} );
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
getLoadableState() {
|
||||
let updated = null;
|
||||
for (let [key, value] of Object.entries(this.declvars)) {
|
||||
if (typeof value['$$getstate'] === 'function') {
|
||||
let loadable = <any>value as io.Loadable;
|
||||
if (updated == null) updated = {};
|
||||
updated[key] = loadable.$$getstate();
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
|
@ -1,65 +1,113 @@
|
||||
|
||||
import * as fastpng from 'fast-png';
|
||||
import { convertWordsToImages, PixelEditorImageFormat } from '../../../ide/pixeleditor';
|
||||
import { arrayCompare } from '../../util';
|
||||
import { Palette } from './color';
|
||||
import * as io from './io'
|
||||
import * as color from './color'
|
||||
|
||||
export abstract class AbstractBitmap {
|
||||
export type PixelMapFunction = (x: number, y: number) => number;
|
||||
|
||||
export abstract class AbstractBitmap<T> {
|
||||
aspect? : number; // aspect ratio, null == default == 1:1
|
||||
constructor(
|
||||
public readonly width: number,
|
||||
public readonly height: number,
|
||||
) {
|
||||
}
|
||||
|
||||
abstract blank(width: number, height: number) : AbstractBitmap<T>;
|
||||
abstract setarray(arr: ArrayLike<number>) : AbstractBitmap<T>;
|
||||
abstract set(x: number, y: number, val: number) : AbstractBitmap<T>;
|
||||
abstract get(x: number, y: number): number;
|
||||
|
||||
inbounds(x: number, y: number): boolean {
|
||||
return (x >= 0 && x < this.width && y >= 0 && y < this.height);
|
||||
}
|
||||
assign(fn: ArrayLike<number> | PixelMapFunction) {
|
||||
if (typeof fn === 'function') {
|
||||
for (let y=0; y<this.height; y++) {
|
||||
for (let x=0; x<this.width; x++) {
|
||||
this.set(x, y, fn(x, y));
|
||||
}
|
||||
}
|
||||
} else if (fn && fn['length'] != null) {
|
||||
this.setarray(fn);
|
||||
} else {
|
||||
throw new Error(`Illegal argument to assign(): ${fn}`)
|
||||
}
|
||||
return this;
|
||||
}
|
||||
clone() : AbstractBitmap<T> {
|
||||
return this.blank(this.width, this.height).assign((x,y) => this.get(x,y));
|
||||
}
|
||||
crop(srcx: number, srcy: number, width: number, height: number) {
|
||||
let dest = this.blank(width, height);
|
||||
dest.assign((x, y) => this.get(x + srcx, y + srcy));
|
||||
return dest;
|
||||
}
|
||||
}
|
||||
|
||||
export class RGBABitmap extends AbstractBitmap {
|
||||
export class RGBABitmap extends AbstractBitmap<RGBABitmap> {
|
||||
public readonly rgba: Uint32Array
|
||||
|
||||
constructor(
|
||||
width: number,
|
||||
height: number,
|
||||
initial?: Uint32Array | PixelMapFunction
|
||||
) {
|
||||
super(width, height);
|
||||
this.rgba = new Uint32Array(this.width * this.height);
|
||||
if (initial) this.assign(initial);
|
||||
}
|
||||
setPixel(x: number, y: number, rgba: number): void {
|
||||
this.rgba[y * this.width + x] = rgba;
|
||||
setarray(arr: ArrayLike<number>) {
|
||||
this.rgba.set(arr);
|
||||
return this;
|
||||
}
|
||||
getPixel(x: number, y: number): number {
|
||||
return this.rgba[y * this.width + x];
|
||||
set(x: number, y: number, rgba: number) {
|
||||
if (this.inbounds(x,y)) this.rgba[y * this.width + x] = rgba;
|
||||
return this;
|
||||
}
|
||||
get(x: number, y: number): number {
|
||||
return this.inbounds(x,y) ? this.rgba[y * this.width + x] : 0;
|
||||
}
|
||||
blank(width: number, height: number) : RGBABitmap {
|
||||
return new RGBABitmap(width, height);
|
||||
}
|
||||
clone() : RGBABitmap {
|
||||
let bitmap = this.blank(this.width, this.height);
|
||||
bitmap.rgba.set(this.rgba);
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class MappedBitmap extends AbstractBitmap {
|
||||
export abstract class MappedBitmap extends AbstractBitmap<MappedBitmap> {
|
||||
public readonly pixels: Uint8Array
|
||||
|
||||
constructor(
|
||||
width: number,
|
||||
height: number,
|
||||
public readonly bitsPerPixel: number,
|
||||
pixels?: Uint8Array
|
||||
initial?: Uint8Array | PixelMapFunction
|
||||
) {
|
||||
super(width, height);
|
||||
this.pixels = pixels || new Uint8Array(this.width * this.height);
|
||||
if (bitsPerPixel != 1 && bitsPerPixel != 2 && bitsPerPixel != 4 && bitsPerPixel != 8)
|
||||
throw new Error(`Invalid bits per pixel: ${bitsPerPixel}`);
|
||||
this.pixels = new Uint8Array(this.width * this.height);
|
||||
if (initial) this.assign(initial);
|
||||
}
|
||||
|
||||
setPixel(x: number, y: number, index: number): void {
|
||||
this.pixels[y * this.width + x] = index;
|
||||
setarray(arr: ArrayLike<number>) {
|
||||
this.pixels.set(arr);
|
||||
return this;
|
||||
}
|
||||
getPixel(x: number, y: number): number {
|
||||
return this.pixels[y * this.width + x];
|
||||
set(x: number, y: number, index: number) {
|
||||
if (this.inbounds(x,y)) this.pixels[y * this.width + x] = index;
|
||||
return this;
|
||||
}
|
||||
get(x: number, y: number): number {
|
||||
return this.inbounds(x,y) ? this.pixels[y * this.width + x] : 0;
|
||||
}
|
||||
abstract getRGBAForIndex(index: number): number;
|
||||
}
|
||||
|
||||
export class Palette {
|
||||
colors: Uint32Array;
|
||||
|
||||
constructor(numColors: number) {
|
||||
this.colors = new Uint32Array(numColors);
|
||||
}
|
||||
}
|
||||
|
||||
export class IndexedBitmap extends MappedBitmap {
|
||||
public palette: Palette;
|
||||
|
||||
@ -67,45 +115,46 @@ export class IndexedBitmap extends MappedBitmap {
|
||||
width: number,
|
||||
height: number,
|
||||
bitsPerPixel: number,
|
||||
pixels?: Uint8Array
|
||||
initial?: Uint8Array | PixelMapFunction
|
||||
) {
|
||||
super(width, height, bitsPerPixel, pixels);
|
||||
this.palette = getDefaultPalette(bitsPerPixel);
|
||||
super(width, height, bitsPerPixel || 8, initial);
|
||||
this.palette = color.palette.colors(this.bitsPerPixel);
|
||||
}
|
||||
|
||||
getRGBAForIndex(index: number): number {
|
||||
return this.palette.colors[index];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO?
|
||||
function getDefaultPalette(bpp: number) {
|
||||
var pal = new Palette(1 << bpp);
|
||||
for (var i=0; i<pal.colors.length; i++) {
|
||||
pal.colors[i] = 0xff000000 | (i * 7919);
|
||||
blank(width: number, height: number) : IndexedBitmap {
|
||||
let bitmap = new IndexedBitmap(width, height, this.bitsPerPixel);
|
||||
bitmap.palette = this.palette;
|
||||
return bitmap;
|
||||
}
|
||||
clone() : IndexedBitmap {
|
||||
let bitmap = this.blank(this.width, this.height);
|
||||
bitmap.pixels.set(this.pixels);
|
||||
return bitmap;
|
||||
}
|
||||
return pal;
|
||||
}
|
||||
|
||||
export function rgbaToUint32(rgba: number[]): number {
|
||||
let v = 0;
|
||||
v |= rgba[0] << 0;
|
||||
v |= rgba[1] << 8;
|
||||
v |= rgba[2] << 16;
|
||||
v |= rgba[3] << 24;
|
||||
return v;
|
||||
export function rgba(width: number, height: number, initial?: Uint32Array | PixelMapFunction) {
|
||||
return new RGBABitmap(width, height, initial);
|
||||
}
|
||||
|
||||
export function rgba(width: number, height: number) {
|
||||
return new RGBABitmap(width, height);
|
||||
}
|
||||
|
||||
export function indexed(width: number, height: number, bpp: number) {
|
||||
return new IndexedBitmap(width, height, bpp);
|
||||
export function indexed(width: number, height: number, bpp: number, initial?: Uint8Array | PixelMapFunction) {
|
||||
return new IndexedBitmap(width, height, bpp, initial);
|
||||
}
|
||||
|
||||
export type BitmapType = RGBABitmap | IndexedBitmap;
|
||||
|
||||
// TODO: check arguments
|
||||
export function decode(arr: Uint8Array, fmt: PixelEditorImageFormat) {
|
||||
var pixels = convertWordsToImages(arr, fmt);
|
||||
// TODO: guess if missing w/h/count?
|
||||
// TODO: reverse mapping
|
||||
// TODO: maybe better composable functions
|
||||
return pixels.map(data => new IndexedBitmap(fmt.w, fmt.h, fmt.bpp | 1, data));
|
||||
}
|
||||
|
||||
export namespace png {
|
||||
export function read(url: string): BitmapType {
|
||||
return decode(io.readbin(url));
|
||||
@ -126,13 +175,22 @@ export namespace png {
|
||||
var palette = new Palette(palarr.length);
|
||||
for (let i = 0; i < palarr.length; i++) {
|
||||
// TODO: alpha?
|
||||
palette.colors[i] = rgbaToUint32(palarr[i]) | 0xff000000;
|
||||
palette.colors[i] = color.arr2rgba(palarr[i]) | 0xff000000;
|
||||
}
|
||||
let bitmap = new IndexedBitmap(png.width, png.height, png.depth);
|
||||
for (let i = 0; i < bitmap.pixels.length; i++) {
|
||||
bitmap.pixels[i] = png.data[i];
|
||||
if (png.depth == 8) {
|
||||
bitmap.pixels.set(png.data);
|
||||
} else {
|
||||
let pixperbyte = Math.floor(8 / png.depth);
|
||||
let mask = (1 << png.depth) - 1;
|
||||
for (let i = 0; i < bitmap.pixels.length; i++) {
|
||||
var bofs = (i % pixperbyte) * png.depth;
|
||||
let val = png.data[Math.floor(i / pixperbyte)];
|
||||
bitmap.pixels[i] = (val >> bofs) & mask;
|
||||
}
|
||||
}
|
||||
bitmap.palette = palette;
|
||||
// TODO: aspect etc
|
||||
return bitmap;
|
||||
}
|
||||
function convertRGBAToBitmap(png: fastpng.IDecodedPNG): RGBABitmap {
|
||||
@ -141,17 +199,172 @@ export namespace png {
|
||||
for (let i = 0; i < bitmap.rgba.length; i++) {
|
||||
for (let j = 0; j < 4; j++)
|
||||
rgba[j] = png.data[i * 4 + j];
|
||||
bitmap.rgba[i] = rgbaToUint32(rgba);
|
||||
bitmap.rgba[i] = color.arr2rgba(rgba);
|
||||
}
|
||||
// TODO: aspect etc
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check arguments
|
||||
export function decode(arr: Uint8Array, fmt: PixelEditorImageFormat) {
|
||||
var pixels = convertWordsToImages(arr, fmt);
|
||||
// TODO: guess if missing w/h/count?
|
||||
// TODO: reverse mapping
|
||||
// TODO: maybe better composable functions
|
||||
return pixels.map(data => new IndexedBitmap(fmt.w, fmt.h, fmt.bpp|1, data));
|
||||
export namespace font {
|
||||
interface Font {
|
||||
maxheight: number;
|
||||
glyphs: { [code: number]: Glyph };
|
||||
properties: {};
|
||||
}
|
||||
class Glyph extends IndexedBitmap {
|
||||
constructor(width: number, height: number, bpp: number,
|
||||
public readonly code: number,
|
||||
public readonly yoffset: number) {
|
||||
super(width, height, bpp);
|
||||
}
|
||||
}
|
||||
export function read(url: string) {
|
||||
if (url.endsWith('.yaff')) return decodeyafflines(io.readlines(url));
|
||||
if (url.endsWith('.draw')) return decodedrawlines(io.readlines(url));
|
||||
throw new Error(`Can't figure out font format for "${url}"`);
|
||||
}
|
||||
export function decodeglyph(glines: string[], curcode: number, yoffset: number): Glyph {
|
||||
let width = 0;
|
||||
for (var gline of glines) width = Math.max(width, gline.length);
|
||||
let g = new Glyph(width, glines.length, 1, curcode, yoffset);
|
||||
for (var y = 0; y < glines.length; y++) {
|
||||
let gline = glines[y];
|
||||
for (var x = 0; x < gline.length; x++) {
|
||||
let ch = gline[x];
|
||||
g.set(x, y, ch==='@' || ch==='#' ? 1 : 0); // TODO: provide mapping
|
||||
}
|
||||
}
|
||||
return g;
|
||||
}
|
||||
// https://github.com/robhagemans/monobit
|
||||
export function decodeyafflines(lines: string[]): Font {
|
||||
let maxheight = 0;
|
||||
let properties = {};
|
||||
let glyphs = {};
|
||||
let yoffset = 0;
|
||||
let curcode = -1;
|
||||
let curglyph: string[] = [];
|
||||
const re_prop = /^([\w-]+):\s+(.+)/i;
|
||||
const re_label = /^0x([0-9a-f]+):|u[+]([0-9a-f]+):|(\w+):/i;
|
||||
const re_gline = /^\s+([.@]+)/
|
||||
function addfont() {
|
||||
if (curcode >= 0 && curglyph.length) {
|
||||
glyphs[curcode] = decodeglyph(curglyph, curcode, yoffset);
|
||||
curcode = -1;
|
||||
curglyph = [];
|
||||
}
|
||||
}
|
||||
for (let line of lines) {
|
||||
let m: RegExpExecArray;
|
||||
if (m = re_prop.exec(line)) {
|
||||
properties[m[1]] = m[2];
|
||||
if (m[1] === 'bottom') yoffset = parseInt(m[2]);
|
||||
if (m[1] === 'size') maxheight = parseInt(m[2]);
|
||||
} else if (m = re_label.exec(line)) {
|
||||
addfont();
|
||||
if (m[1] != null) curcode = parseInt(m[1], 16);
|
||||
else if (m[2] != null) curcode = parseInt(m[2], 16);
|
||||
else if (m[3] != null) curcode = null; // text labels not supported
|
||||
} else if (m = re_gline.exec(line)) {
|
||||
curglyph.push(m[1]);
|
||||
}
|
||||
if (isNaN(curcode + yoffset + maxheight))
|
||||
throw new Error(`couldn't decode .yaff: ${JSON.stringify(line)}`)
|
||||
}
|
||||
addfont();
|
||||
return { maxheight, properties, glyphs };
|
||||
}
|
||||
// https://github.com/robhagemans/monobit
|
||||
export function decodedrawlines(lines: string[]): Font {
|
||||
let maxheight = 0;
|
||||
let properties = {};
|
||||
let glyphs = {};
|
||||
let curcode = -1;
|
||||
let curglyph: string[] = [];
|
||||
const re_gline = /^([0-9a-f]+)?[:]?\s*([-#]+)/i;
|
||||
function addfont() {
|
||||
if (curcode >= 0 && curglyph.length) {
|
||||
glyphs[curcode] = decodeglyph(curglyph, curcode, 0);
|
||||
maxheight = Math.max(maxheight, curglyph.length);
|
||||
curcode = -1;
|
||||
curglyph = [];
|
||||
}
|
||||
}
|
||||
for (let line of lines) {
|
||||
let m: RegExpExecArray;
|
||||
if (m = re_gline.exec(line)) {
|
||||
if (m[1] != null) {
|
||||
addfont();
|
||||
curcode = parseInt(m[1], 16);
|
||||
if (isNaN(curcode))
|
||||
throw new Error(`couldn't decode .draw: ${JSON.stringify(line)}`)
|
||||
}
|
||||
curglyph.push(m[2]);
|
||||
}
|
||||
}
|
||||
addfont();
|
||||
return { maxheight, properties, glyphs };
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: merge w/ pixeleditor
|
||||
|
||||
export type PixelEditorImageFormat = {
|
||||
w:number
|
||||
h:number
|
||||
count?:number
|
||||
bpp?:number
|
||||
np?:number
|
||||
bpw?:number
|
||||
sl?:number
|
||||
pofs?:number
|
||||
remap?:number[]
|
||||
reindex?:number[]
|
||||
brev?:boolean
|
||||
flip?:boolean
|
||||
destfmt?:PixelEditorImageFormat
|
||||
xform?:string
|
||||
skip?:number
|
||||
aspect?:number
|
||||
};
|
||||
|
||||
export function convertWordsToImages(words:ArrayLike<number>, fmt:PixelEditorImageFormat) : Uint8Array[] {
|
||||
var width = fmt.w;
|
||||
var height = fmt.h;
|
||||
var count = fmt.count || 1;
|
||||
var bpp = fmt.bpp || 1;
|
||||
var nplanes = fmt.np || 1;
|
||||
var bitsperword = fmt.bpw || 8;
|
||||
var wordsperline = fmt.sl || Math.ceil(width * bpp / bitsperword);
|
||||
var mask = (1 << bpp)-1;
|
||||
var pofs = fmt.pofs || wordsperline*height*count;
|
||||
var skip = fmt.skip || 0;
|
||||
var images = [];
|
||||
for (var n=0; n<count; n++) {
|
||||
var imgdata = [];
|
||||
for (var y=0; y<height; y++) {
|
||||
var yp = fmt.flip ? height-1-y : y;
|
||||
var ofs0 = n*wordsperline*height + yp*wordsperline;
|
||||
var shift = 0;
|
||||
for (var x=0; x<width; x++) {
|
||||
var color = 0;
|
||||
var ofs = ofs0; // TODO: remapBits(ofs0, fmt.remap);
|
||||
// TODO: if (fmt.reindex) { [ofs, shift] = reindexMask(x, fmt.reindex); ofs += ofs0; }
|
||||
for (var p=0; p<nplanes; p++) {
|
||||
var byte = words[ofs + p*pofs + skip];
|
||||
color |= ((fmt.brev ? byte>>(bitsperword-shift-bpp) : byte>>shift) & mask) << (p*bpp);
|
||||
}
|
||||
imgdata.push(color);
|
||||
shift += bpp;
|
||||
if (shift >= bitsperword && !fmt.reindex) {
|
||||
ofs0 += 1;
|
||||
shift = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
images.push(new Uint8Array(imgdata));
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
87
src/common/script/lib/color.ts
Normal file
87
src/common/script/lib/color.ts
Normal 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
|
||||
}
|
@ -1,35 +1,46 @@
|
||||
import { ProjectFilesystem } from "../../../ide/project";
|
||||
import { FileData } from "../../workertypes";
|
||||
import * as output from "./output";
|
||||
|
||||
// TODO
|
||||
var $$fs: ProjectFilesystem;
|
||||
var $$cache: { [path: string]: FileData } = {};
|
||||
import { FileData, WorkingStore } from "../../workertypes";
|
||||
|
||||
export class IOWaitError extends Error {
|
||||
// remote resource cache
|
||||
var $$cache: WeakMap<object,FileData> = new WeakMap();
|
||||
// file read/write interface
|
||||
var $$store: WorkingStore;
|
||||
// backing store for data
|
||||
var $$data: {} = {};
|
||||
|
||||
export function $$setupFS(store: WorkingStore) {
|
||||
$$store = store;
|
||||
}
|
||||
export function $$getData() {
|
||||
return $$data;
|
||||
}
|
||||
export function $$loadData(data: {}) {
|
||||
Object.assign($$data, data);
|
||||
}
|
||||
|
||||
export function $$setupFS(fs: ProjectFilesystem) {
|
||||
$$fs = fs;
|
||||
// object that can load state from backing store
|
||||
export interface Loadable {
|
||||
reset() : void;
|
||||
$$getstate() : {};
|
||||
}
|
||||
|
||||
function getFS(): ProjectFilesystem {
|
||||
if ($$fs == null) throw new Error(`Internal Error: The 'io' module has not been set up properly.`)
|
||||
return $$fs;
|
||||
}
|
||||
|
||||
export function ___load(path: string): FileData {
|
||||
var data = $$cache[path];
|
||||
if (data == null) {
|
||||
getFS().getFileData(path).then((value) => {
|
||||
$$cache[path] = value;
|
||||
})
|
||||
throw new IOWaitError(path);
|
||||
} else {
|
||||
return data;
|
||||
export namespace data {
|
||||
export function get(object: Loadable, key: string): Loadable {
|
||||
let override = $$data && $$data[key];
|
||||
if (override) Object.assign(object, override);
|
||||
else if (object.reset) object.reset();
|
||||
return object;
|
||||
}
|
||||
export function set(object: Loadable, key: string): Loadable {
|
||||
if ($$data && object.$$getstate) {
|
||||
$$data[key] = object.$$getstate();
|
||||
}
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
export class IOWaitError extends Error {
|
||||
}
|
||||
|
||||
export function canonicalurl(url: string) : string {
|
||||
// get raw resource URL for github
|
||||
@ -42,24 +53,50 @@ export function canonicalurl(url: string) : string {
|
||||
return url;
|
||||
}
|
||||
|
||||
export function read(url: string, type?: 'binary' | 'text'): FileData {
|
||||
url = canonicalurl(url);
|
||||
export function clearcache() {
|
||||
$$cache = new WeakMap();
|
||||
}
|
||||
|
||||
export function fetchurl(url: string, type?: 'binary' | 'text'): FileData {
|
||||
// TODO: only works in web worker
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = type === 'text' ? 'text' : 'arraybuffer';
|
||||
xhr.open("GET", url, false); // synchronous request
|
||||
xhr.send(null);
|
||||
if (xhr.response != null && xhr.status == 200) {
|
||||
if (type !== 'text') {
|
||||
return new Uint8Array(xhr.response);
|
||||
if (type === 'text') {
|
||||
return xhr.response as string;
|
||||
} else {
|
||||
return xhr.response;
|
||||
return new Uint8Array(xhr.response);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`The resource at "${url}" responded with status code of ${xhr.status}.`)
|
||||
}
|
||||
}
|
||||
|
||||
export function readnocache(url: string, type?: 'binary' | 'text'): FileData {
|
||||
if (url.startsWith('http:') || url.startsWith('https:')) {
|
||||
return fetchurl(url);
|
||||
}
|
||||
if ($$store) {
|
||||
return $$store.getFileData(url);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: read files too
|
||||
export function read(url: string, type?: 'binary' | 'text'): FileData {
|
||||
url = canonicalurl(url);
|
||||
// check cache
|
||||
let cachekey = {url: url};
|
||||
if ($$cache.has(cachekey)) {
|
||||
return $$cache.get(cachekey);
|
||||
}
|
||||
let data = readnocache(url, type);
|
||||
if (data == null) throw new Error(`Cannot find resource "${url}"`);
|
||||
$$cache.set(cachekey, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function readbin(url: string): Uint8Array {
|
||||
var data = read(url, 'binary');
|
||||
if (data instanceof Uint8Array)
|
||||
@ -67,3 +104,11 @@ export function readbin(url: string): Uint8Array {
|
||||
else
|
||||
throw new Error(`The resource at "${url}" is not a binary file.`);
|
||||
}
|
||||
|
||||
export function readlines(url: string) : string[] {
|
||||
return (read(url, 'text') as string).split('\n');
|
||||
}
|
||||
|
||||
export function splitlines(text: string) : string[] {
|
||||
return text.split(/\n|\r\n/g);
|
||||
}
|
||||
|
30
src/common/script/lib/scriptui.ts
Normal file
30
src/common/script/lib/scriptui.ts
Normal 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);
|
||||
}
|
@ -2,8 +2,14 @@
|
||||
import { BitmapType, IndexedBitmap, RGBABitmap } from "../lib/bitmap";
|
||||
import { Component, render, h, ComponentType } from 'preact';
|
||||
import { Cell } from "../env";
|
||||
import { rgb2bgr } from "../../util";
|
||||
import { hex, rgb2bgr, RGBA } from "../../util";
|
||||
import { dumpRAM } from "../../emu";
|
||||
// TODO: can't call methods from this end
|
||||
import { Palette } from "../lib/color";
|
||||
import { ScriptUISlider, ScriptUISliderType } from "../lib/scriptui";
|
||||
import { current_project } from "../../../ide/ui";
|
||||
|
||||
const MAX_STRING_LEN = 100;
|
||||
|
||||
interface ColorComponentProps {
|
||||
rgbavalue: number;
|
||||
@ -12,7 +18,7 @@ interface ColorComponentProps {
|
||||
class ColorComponent extends Component<ColorComponentProps> {
|
||||
render(virtualDom, containerNode, replaceNode) {
|
||||
let rgb = this.props.rgbavalue & 0xffffff;
|
||||
var htmlcolor = `#${rgb2bgr(rgb).toString(16)}`;
|
||||
var htmlcolor = `#${hex(rgb2bgr(rgb),6)}`;
|
||||
var textcol = (rgb & 0x008000) ? 'black' : 'white';
|
||||
return h('div', {
|
||||
class: 'scripting-color',
|
||||
@ -41,12 +47,11 @@ class BitmapComponent extends Component<BitmapComponentProps> {
|
||||
return h('canvas', {
|
||||
class: 'pixelated',
|
||||
width: this.props.width,
|
||||
height: this.props.height
|
||||
height: this.props.height,
|
||||
...this.props
|
||||
});
|
||||
}
|
||||
componentDidMount() {
|
||||
this.canvas = this.base as HTMLCanvasElement;
|
||||
this.prepare();
|
||||
this.refresh();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
@ -58,13 +63,16 @@ class BitmapComponent extends Component<BitmapComponentProps> {
|
||||
this.refresh();
|
||||
}
|
||||
prepare() {
|
||||
this.canvas = this.base as HTMLCanvasElement;
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.imageData = this.ctx.createImageData(this.canvas.width, this.canvas.height);
|
||||
this.datau32 = new Uint32Array(this.imageData.data.buffer);
|
||||
}
|
||||
refresh() {
|
||||
// preact can reuse this component but it can change shape :^P
|
||||
if (this.imageData.width != this.props.width || this.imageData.height != this.props.height) {
|
||||
if (this.canvas !== this.base
|
||||
|| this.imageData.width != this.props.width
|
||||
|| this.imageData.height != this.props.height) {
|
||||
this.prepare();
|
||||
}
|
||||
this.updateCanvas(this.datau32, this.props.bitmap);
|
||||
@ -89,30 +97,87 @@ class BitmapComponent extends Component<BitmapComponentProps> {
|
||||
}
|
||||
}
|
||||
|
||||
interface BitmapEditorState {
|
||||
isEditing: boolean;
|
||||
}
|
||||
|
||||
class BitmapEditor extends Component<BitmapComponentProps, BitmapEditorState> {
|
||||
render(virtualDom, containerNode, replaceNode) {
|
||||
// TODO: focusable?
|
||||
let bitmapProps = {
|
||||
onClick: (event) => {
|
||||
if (!this.state.isEditing) {
|
||||
this.setState({ isEditing: true });
|
||||
} else {
|
||||
// TODO: process click
|
||||
}
|
||||
},
|
||||
...this.props
|
||||
}
|
||||
let bitmapRender = h(BitmapComponent, bitmapProps, []);
|
||||
let okCancel = null;
|
||||
if (this.state.isEditing) {
|
||||
okCancel = h('div', {}, [
|
||||
button('Ok', () => this.commitChanges()),
|
||||
button('Cancel', () => this.abandonChanges()),
|
||||
]);
|
||||
}
|
||||
return h('div', {
|
||||
tabIndex: 0,
|
||||
class: this.state.isEditing ? 'scripting-cell' : '' // TODO
|
||||
}, [
|
||||
bitmapRender,
|
||||
okCancel,
|
||||
])
|
||||
}
|
||||
commitChanges() {
|
||||
this.cancelEditing();
|
||||
}
|
||||
abandonChanges() {
|
||||
this.cancelEditing();
|
||||
}
|
||||
cancelEditing() {
|
||||
this.setState({ isEditing: false });
|
||||
}
|
||||
}
|
||||
|
||||
function button(label: string, action: () => void) {
|
||||
return h('button', {
|
||||
onClick: action
|
||||
}, [label]);
|
||||
}
|
||||
|
||||
interface ObjectTreeComponentProps {
|
||||
name?: string;
|
||||
object: {} | [];
|
||||
objpath: string;
|
||||
}
|
||||
|
||||
interface ObjectTreeComponentState {
|
||||
expanded : boolean;
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
class ObjectTreeComponent extends Component<ObjectTreeComponentProps, ObjectTreeComponentState> {
|
||||
class ObjectKeyValueComponent extends Component<ObjectTreeComponentProps, ObjectTreeComponentState> {
|
||||
render(virtualDom, containerNode, replaceNode) {
|
||||
if (this.state.expanded) {
|
||||
var minus = h('span', { onClick: () => this.toggleExpand() }, [ '-' ]);
|
||||
return h('minus', { }, [
|
||||
minus,
|
||||
getShortName(this.props.object),
|
||||
objectToContentsDiv(this.props.object)
|
||||
]);
|
||||
} else {
|
||||
var plus = h('span', { onClick: () => this.toggleExpand() }, [ '+' ]);
|
||||
return h('div', { }, [
|
||||
plus,
|
||||
getShortName(this.props.object)
|
||||
]);
|
||||
}
|
||||
let expandable = typeof this.props.object === 'object';
|
||||
let hdrclass = '';
|
||||
if (expandable)
|
||||
hdrclass = this.state.expanded ? 'tree-expanded' : 'tree-collapsed'
|
||||
return h('div', {
|
||||
class: 'tree-content',
|
||||
key: `${this.props.objpath}__tree`
|
||||
}, [
|
||||
h('div', {
|
||||
class: 'tree-header ' + hdrclass,
|
||||
onClick: expandable ? () => this.toggleExpand() : null
|
||||
}, [
|
||||
this.props.name + "",
|
||||
h('span', { class: 'tree-value' }, [
|
||||
getShortName(this.props.object)
|
||||
])
|
||||
]),
|
||||
this.state.expanded ? objectToContentsDiv(this.props.object, this.props.objpath) : null
|
||||
]);
|
||||
}
|
||||
toggleExpand() {
|
||||
this.setState({ expanded: !this.state.expanded });
|
||||
@ -128,67 +193,133 @@ function getShortName(object: any) {
|
||||
}
|
||||
return s;
|
||||
} catch (e) {
|
||||
return 'object';
|
||||
console.log(e);
|
||||
return e + "";
|
||||
}
|
||||
} else {
|
||||
return object+"";
|
||||
return primitiveToString(object);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: need id?
|
||||
function objectToDiv(object: any) {
|
||||
var props = { class: '' };
|
||||
function primitiveToString(obj) {
|
||||
var text = "";
|
||||
// is it a function? call it first, if we are expanded
|
||||
// TODO: only call functions w/ signature
|
||||
if (obj && obj.$$ && typeof obj.$$ == 'function' && this._content != null) {
|
||||
obj = obj.$$();
|
||||
}
|
||||
// check null first
|
||||
if (obj == null) {
|
||||
text = obj + "";
|
||||
// primitive types
|
||||
} else if (typeof obj == 'number') {
|
||||
if (obj != (obj | 0)) text = obj.toString(); // must be a float
|
||||
else text = obj + "\t($" + hex(obj) + ")";
|
||||
} else {
|
||||
text = JSON.stringify(obj);
|
||||
if (text.length > MAX_STRING_LEN)
|
||||
text = text.substring(0, MAX_STRING_LEN) + "...";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function isIndexedBitmap(object): object is IndexedBitmap {
|
||||
return object['bitsPerPixel'] && object['pixels'] && object['palette'];
|
||||
}
|
||||
function isRGBABitmap(object): object is RGBABitmap {
|
||||
return object['rgba'] instanceof Uint32Array;
|
||||
}
|
||||
function isPalette(object): object is Palette {
|
||||
return object['colors'] instanceof Uint32Array;
|
||||
}
|
||||
|
||||
function objectToDiv(object: any, name: string, objpath: string) {
|
||||
var props = { class: '', key: `${objpath}__obj` };
|
||||
var children = [];
|
||||
// TODO: tile editor
|
||||
// TODO: limit # of items
|
||||
// TODO: detect table
|
||||
if (Array.isArray(object)) {
|
||||
return objectToContentsDiv(object);
|
||||
} else if (object['bitsPerPixel'] && object['pixels'] && object['palette']) {
|
||||
addBitmapComponent(children, object as IndexedBitmap);
|
||||
} else if (object['rgba'] instanceof Uint32Array) {
|
||||
addBitmapComponent(children, object as RGBABitmap);
|
||||
} else if (object['colors'] instanceof Uint32Array) {
|
||||
//return objectToContentsDiv(object);
|
||||
if (object == null) {
|
||||
return object + "";
|
||||
} else if (object['uitype']) {
|
||||
children.push(h(UISliderComponent, { iokey: objpath, uiobject: object }));
|
||||
} else if (object['literaltext']) {
|
||||
children.push(h("pre", { }, [ object['literaltext'] ])); // TODO
|
||||
} else if (isIndexedBitmap(object)) {
|
||||
//Object.setPrototypeOf(object, IndexedBitmap.prototype); // TODO: use Object.create()?
|
||||
addBitmapComponent(children, object);
|
||||
} else if (isRGBABitmap(object)) {
|
||||
//Object.setPrototypeOf(object, RGBABitmap.prototype); // TODO: use Object.create()?
|
||||
addBitmapComponent(children, object);
|
||||
} else if (isPalette(object)) {
|
||||
// TODO: make sets of 2/4/8/16/etc
|
||||
props.class += ' scripting-grid ';
|
||||
object['colors'].forEach((val) => {
|
||||
object.colors.forEach((val) => {
|
||||
children.push(h(ColorComponent, { rgbavalue: val }));
|
||||
})
|
||||
} else if (typeof object === 'object') {
|
||||
children.push(h(ObjectTreeComponent, { object }));
|
||||
} else {
|
||||
children.push(JSON.stringify(object));
|
||||
return h(ObjectKeyValueComponent, { name, object, objpath }, []);
|
||||
}
|
||||
let div = h('div', props, children);
|
||||
return div;
|
||||
}
|
||||
|
||||
function objectToContentsDiv(object: {} | []) {
|
||||
function fixedArrayToDiv(tyarr: Array<number>, bpel: number, objpath: string) {
|
||||
const maxBytes = 0x100;
|
||||
if (tyarr.length <= maxBytes) {
|
||||
// TODO
|
||||
let dumptext = dumpRAM(tyarr, 0, tyarr.length);
|
||||
return h('pre', {}, dumptext);
|
||||
} else {
|
||||
let children = [];
|
||||
for (var ofs = 0; ofs < tyarr.length; ofs += maxBytes) {
|
||||
children.push(objectToDiv(tyarr.slice(ofs, ofs + maxBytes), '$' + hex(ofs), `${objpath}.${ofs}`));
|
||||
}
|
||||
return h('div', {}, children);
|
||||
}
|
||||
}
|
||||
|
||||
function objectToContentsDiv(object: {} | [], objpath: string) {
|
||||
// is typed array?
|
||||
let bpel = object['BYTES_PER_ELEMENT'];
|
||||
if (typeof bpel === 'number') {
|
||||
const maxBytes = 0x100;
|
||||
let tyarr = object as Uint8Array;
|
||||
if (tyarr.length <= maxBytes) {
|
||||
// TODO
|
||||
let dumptext = dumpRAM(tyarr, 0, tyarr.length);
|
||||
return h('pre', { }, dumptext);
|
||||
} else {
|
||||
let children = [];
|
||||
for (var ofs=0; ofs<tyarr.length; ofs+=maxBytes) {
|
||||
children.push(objectToDiv(tyarr.slice(ofs, ofs+maxBytes)));
|
||||
}
|
||||
return h('div', { }, children);
|
||||
}
|
||||
return fixedArrayToDiv(object as Array<number>, bpel, objpath);
|
||||
}
|
||||
let objectEntries = Object.entries(object);
|
||||
// TODO: id?
|
||||
let objectDivs = objectEntries.map(entry => objectToDiv(entry[1]));
|
||||
return h('div', { }, objectDivs);
|
||||
|