diff --git a/.gitignore b/.gitignore index c17671e0..891b895c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ tmp/ web/ release/ gen/ +config.js +chromedriver.log +nightwatch.conf.js diff --git a/css/ui.css b/css/ui.css index c4cc6d68..9268e548 100644 --- a/css/ui.css +++ b/css/ui.css @@ -793,3 +793,11 @@ div.asset_toolbar { .scripting-cell div { display: inline; } +div.scripting-color { + padding:0.1em; + margin:0.1em; +} +div.scripting-grid { + display: grid; + grid-template-columns: repeat( auto-fit, minmax(2em, 1fr) ); +} \ No newline at end of file diff --git a/meta/icons/8bitworkshop-icon-1024.jpg b/meta/icons/8bitworkshop-icon-1024.jpg new file mode 100644 index 00000000..1b8656c2 Binary files /dev/null and b/meta/icons/8bitworkshop-icon-1024.jpg differ diff --git a/package-lock.json b/package-lock.json index 2934eb50..5938777a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "localforage": "^1.9.0", "mousetrap": "^1.6.5", "octokat": "^0.10.0", + "preact": "^10.5.14", "split.js": "^1.6.2", "yufka": "^2.0.1" }, @@ -7742,6 +7743,15 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, + "node_modules/preact": { + "version": "10.5.14", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.5.14.tgz", + "integrity": "sha512-KojoltCrshZ099ksUZ2OQKfbH66uquFoxHSbnwKbTJHeQNvx42EmC7wQVWNuDt6vC5s3nudRHFtKbpY4ijKlaQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -16281,6 +16291,11 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, + "preact": { + "version": "10.5.14", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.5.14.tgz", + "integrity": "sha512-KojoltCrshZ099ksUZ2OQKfbH66uquFoxHSbnwKbTJHeQNvx42EmC7wQVWNuDt6vC5s3nudRHFtKbpY4ijKlaQ==" + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", diff --git a/package.json b/package.json index f66a641a..87b42a1b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "localforage": "^1.9.0", "mousetrap": "^1.6.5", "octokat": "^0.10.0", + "preact": "^10.5.14", "split.js": "^1.6.2", "yufka": "^2.0.1" }, diff --git a/src/common/script/env.ts b/src/common/script/env.ts index dcad95f8..464f9ce6 100644 --- a/src/common/script/env.ts +++ b/src/common/script/env.ts @@ -1,9 +1,9 @@ import { WorkerError } from "../workertypes"; import ErrorStackParser = require("error-stack-parser"); import yufka from 'yufka'; -import * as bitmap from "./bitmap"; -import * as io from "./io"; -import * as output from "./output"; +import * as bitmap from "./lib/bitmap"; +import * as io from "./lib/io"; +import * as output from "./lib/output"; import { escapeHTML } from "../util"; export interface Cell { @@ -54,6 +54,7 @@ export class Environment { preprocess(code: string): string { var declvars = {}; const result = yufka(code, (node, { update, source, parent }) => { + let left = node['left']; switch (node.type) { case 'Identifier': if (GLOBAL_BADLIST.indexOf(source()) >= 0) { @@ -61,16 +62,17 @@ export class Environment { } break; case 'AssignmentExpression': - /* - // x = expr --> var x = expr + // x = expr --> var x = expr (first use) if (parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program') { // TODO - let left = node['left']; - if (left && left.type === 'Identifier' && declvars[left.name] == null) { - update(`var ${source()}`) - declvars[left.name] = true; + if (left && left.type === 'Identifier') { + if (!declvars[left.name]) { + update(`var ${left.name}=this.${source()}`) + declvars[left.name] = true; + } else { + update(`${left.name}=this.${source()}`) + } } } - */ break; } }) diff --git a/src/common/script/bitmap.ts b/src/common/script/lib/bitmap.ts similarity index 87% rename from src/common/script/bitmap.ts rename to src/common/script/lib/bitmap.ts index e8a1a0ef..850e2b73 100644 --- a/src/common/script/bitmap.ts +++ b/src/common/script/lib/bitmap.ts @@ -1,7 +1,7 @@ import * as fastpng from 'fast-png'; -import { convertWordsToImages, PixelEditorImageFormat } from '../../ide/pixeleditor'; -import { arrayCompare } from '../util'; +import { convertWordsToImages, PixelEditorImageFormat } from '../../../ide/pixeleditor'; +import { arrayCompare } from '../../util'; import * as io from './io' export abstract class AbstractBitmap { @@ -107,8 +107,8 @@ export function indexed(width: number, height: number, bpp: number) { export type BitmapType = RGBABitmap | IndexedBitmap; export namespace png { - export function load(url: string): BitmapType { - return decode(io.loadbin(url)); + export function read(url: string): BitmapType { + return decode(io.readbin(url)); } export function decode(data: Uint8Array): BitmapType { let png = fastpng.decode(data); @@ -147,13 +147,11 @@ export namespace png { } } -export namespace from { - // TODO: check arguments - export function bytes(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)); - } +// 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)); } diff --git a/src/common/script/io.ts b/src/common/script/lib/io.ts similarity index 82% rename from src/common/script/io.ts rename to src/common/script/lib/io.ts index 0f0df744..ba661a9c 100644 --- a/src/common/script/io.ts +++ b/src/common/script/lib/io.ts @@ -1,8 +1,8 @@ -import { ProjectFilesystem } from "../../ide/project"; -import { FileData } from "../workertypes"; +import { ProjectFilesystem } from "../../../ide/project"; +import { FileData } from "../../workertypes"; +import * as output from "./output"; // TODO - var $$fs: ProjectFilesystem; var $$cache: { [path: string]: FileData } = {}; @@ -18,6 +18,19 @@ function getFS(): ProjectFilesystem { 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 function canonicalurl(url: string) : string { // get raw resource URL for github if (url.startsWith('https://github.com/')) { @@ -29,7 +42,7 @@ export function canonicalurl(url: string) : string { return url; } -export function load(url: string, type?: 'binary' | 'text'): FileData { +export function read(url: string, type?: 'binary' | 'text'): FileData { url = canonicalurl(url); // TODO: only works in web worker var xhr = new XMLHttpRequest(); @@ -47,23 +60,10 @@ export function load(url: string, type?: 'binary' | 'text'): FileData { } } -export function loadbin(url: string): Uint8Array { - var data = load(url, 'binary'); +export function readbin(url: string): Uint8Array { + var data = read(url, 'binary'); if (data instanceof Uint8Array) return data; else throw new Error(`The resource at "${url}" is not a binary file.`); } - -export function xload(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; - } -} - diff --git a/src/common/script/output.ts b/src/common/script/lib/output.ts similarity index 100% rename from src/common/script/output.ts rename to src/common/script/lib/output.ts diff --git a/src/common/script/node.ts b/src/common/script/node.ts deleted file mode 100644 index ccf8502c..00000000 --- a/src/common/script/node.ts +++ /dev/null @@ -1,165 +0,0 @@ - -var lastTimestamp = 0; - -function newTimestamp() { - return ++lastTimestamp; -} - -export type DependencySet = {[id:string] : ComputeNode | any} - -export abstract class ComputeNode { - private src_ts: number = newTimestamp(); - private result_ts: number = 0; - private depends: DependencySet = {}; - private busy: Promise = null; - - modified() { - this.src_ts = newTimestamp(); - } - - isStale(ts: number) { - return this.result_ts < ts; - } - - setDependencies(depends: DependencySet) { - this.depends = depends; - // compute latest timestamp of all dependencies - var ts = 0; - for (let [key, dep] of Object.entries(this.depends)) { - if (dep instanceof ComputeNode && dep.result_ts) { - ts = Math.max(ts, dep.result_ts); - } else { - ts = newTimestamp(); - } - } - this.src_ts = ts; - } - - getDependencies() { - return this.depends; - } - - async update() : Promise { - let maxts = 0; - let dependsComputes = [] - for (let [key, dep] of Object.entries(this.depends)) { - if (dep instanceof ComputeNode && dep.isStale(this.src_ts)) { - dependsComputes.push(dep.compute()); - } - } - if (dependsComputes.length) { - await Promise.all(dependsComputes); - this.recompute(maxts); - } - } - - async recompute(ts: number) : Promise { - // are we currently waiting for a computation to finish? - if (this.busy == null || ts > this.result_ts) { - // wait for previous operation to finish (no-op if null) - await this.busy; - this.result_ts = ts; - this.busy = this.compute(); - } - await this.busy; - this.busy = null; - } - - abstract compute(): Promise; -} - -class ValueNode extends ComputeNode { - private value : T; - - constructor(value : T) { - super(); - this.set(value); - } - - get() : T { - return this.value; - } - - set(newValue : T) { - this.value = newValue; - this.modified(); - } - - async compute() { } -} - -class ArrayNode extends ValueNode { -} - -class IntegerNode extends ValueNode { -} - -abstract class BitmapNode extends ComputeNode { - width: number; - height: number; -} - -class RGBABitmapNode extends BitmapNode { - rgba: ArrayNode; - - compute(): Promise { - throw new Error("Method not implemented."); - } -} - -class IndexedBitmapNode extends BitmapNode { - indices: ArrayNode; - - compute(): Promise { - throw new Error("Method not implemented."); - } -} - -class PaletteNode { - colors: ArrayNode; - - compute(): Promise { - throw new Error("Method not implemented."); - } -} - -class PaletteMapNode extends ComputeNode { - palette: PaletteNode; - indices: ArrayNode; - - compute(): Promise { - throw new Error("Method not implemented."); - } -} - -function valueOf(node : ValueNode) : T { - return node.get(); -} - -class TestNode extends ComputeNode { - value : string; - - async compute() { - await new Promise(r => setTimeout(r, 100)); - this.value = Object.values(this.getDependencies()).map(valueOf).join(''); - } - -} - -/// - -async function test() { - var val1 = new ValueNode(1234); - var arr1 = new ValueNode([1,2,3]); - var join = new TestNode(); - join.setDependencies({a:val1, b:arr1}); - await join.update(); - console.log(join); - val1.set(9999); - join.update(); - val1.set(9989) - await join.update(); - console.log(join); -} - -test(); diff --git a/src/common/script/test.ts b/src/common/script/test.ts deleted file mode 100644 index 7d518ec0..00000000 --- a/src/common/script/test.ts +++ /dev/null @@ -1,13 +0,0 @@ - -import 'fs'; -import * as bitmap from './bitmap' - -const fs = require('fs') - -var data = fs.readFileSync('images/book_a2600.png'); -//var data = fs.readFileSync('images/print-head.png'); -console.log(data); - -var png = bitmap.png.decode(data); -console.log(png) - diff --git a/src/common/script/ui/notebook.ts b/src/common/script/ui/notebook.ts new file mode 100644 index 00000000..4db3906b --- /dev/null +++ b/src/common/script/ui/notebook.ts @@ -0,0 +1,210 @@ + +import { BitmapType, IndexedBitmap, RGBABitmap } from "../lib/bitmap"; +import { Component, render, h, ComponentType } from 'preact'; +import { Cell } from "../env"; +import { rgb2bgr } from "../../util"; +import { dumpRAM } from "../../emu"; + +interface ColorComponentProps { + rgbavalue: number; +} + +class ColorComponent extends Component { + render(virtualDom, containerNode, replaceNode) { + let rgb = this.props.rgbavalue & 0xffffff; + var htmlcolor = `#${rgb2bgr(rgb).toString(16)}`; + var textcol = (rgb & 0x008000) ? 'black' : 'white'; + return h('div', { + class: 'scripting-color', + style: `background-color: ${htmlcolor}; color: ${textcol}`, + alt: htmlcolor, // TODO + }, '\u00a0'); + } +} + +interface BitmapComponentProps { + bitmap: BitmapType; + width: number; + height: number; +} + +class BitmapComponent extends Component { + canvas: HTMLCanvasElement; + ctx: CanvasRenderingContext2D; + imageData: ImageData; + datau32: Uint32Array; + + constructor(props: BitmapComponentProps) { + super(props); + } + render(virtualDom, containerNode, replaceNode) { + return h('canvas', { + class: 'pixelated', + width: this.props.width, + height: this.props.height + }); + } + componentDidMount() { + this.canvas = this.base as HTMLCanvasElement; + this.prepare(); + this.refresh(); + } + componentWillUnmount() { + this.canvas = null; + this.imageData = null; + this.datau32 = null; + } + componentDidUpdate(prevProps, prevState, snapshot) { + this.refresh(); + } + prepare() { + 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) { + this.prepare(); + } + this.updateCanvas(this.datau32, this.props.bitmap); + this.ctx.putImageData(this.imageData, 0, 0); + } + updateCanvas(vdata: Uint32Array, bmp: BitmapType) { + if (bmp['palette']) { + this.updateCanvasIndexed(vdata, bmp as IndexedBitmap); + } + if (bmp['rgba']) { + this.updateCanvasRGBA(vdata, bmp as RGBABitmap); + } + } + updateCanvasRGBA(vdata: Uint32Array, bmp: RGBABitmap) { + vdata.set(bmp.rgba); + } + updateCanvasIndexed(vdata: Uint32Array, bmp: IndexedBitmap) { + let pal = bmp.palette.colors; + for (var i = 0; i < bmp.pixels.length; i++) { + vdata[i] = pal[bmp.pixels[i]]; + } + } +} + +interface ObjectTreeComponentProps { + object: {} | []; +} + +interface ObjectTreeComponentState { + expanded : boolean; +} + +class ObjectTreeComponent extends Component { + render(virtualDom, containerNode, replaceNode) { + if (this.state.expanded) { + var minus = h('span', { onClick: () => this.toggleExpand() }, [ '-' ]); + return h('minus', { }, [ + minus, + getShortName(this.props.object), + objectToContentsDiv(this.props.object) + ]); + } else { + var plus = h('span', { onClick: () => this.toggleExpand() }, [ '+' ]); + return h('div', { }, [ + plus, + getShortName(this.props.object) + ]); + } + } + toggleExpand() { + this.setState({ expanded: !this.state.expanded }); + } +} + +function getShortName(object: any) { + if (typeof object === 'object') { + try { + var s = Object.getPrototypeOf(object).constructor.name; + if (object.length > 0) { + s += `[${object.length}]` + } + return s; + } catch (e) { + return 'object'; + } + } else { + return object+""; + } +} + +// TODO: need id? +function objectToDiv(object: any) { + var props = { class: '' }; + 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) { + // TODO: make sets of 2/4/8/16/etc + props.class += ' scripting-grid '; + 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)); + } + let div = h('div', props, children); + return div; +} + +function objectToContentsDiv(object: {} | []) { + // 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 objectToDiv(entry[1])); + return h('div', { }, objectDivs); +} + +function addBitmapComponent(children, bitmap: BitmapType) { + children.push(h(BitmapComponent, { bitmap: bitmap, width: bitmap.width, height: bitmap.height})); +} + +export class Notebook { + constructor( + public readonly maindoc: HTMLDocument, + public readonly maindiv: HTMLElement + ) { + maindiv.classList.add('vertical-scroll'); + //maindiv.classList.add('container') + } + updateCells(cells: Cell[]) { + let hTree = cells.map(cell => { + let cellDiv = objectToDiv(cell.object); + cellDiv.props['class'] += ' scripting-cell '; + return cellDiv; + }); + render(hTree, this.maindiv); + } +} diff --git a/src/platform/script.ts b/src/platform/script.ts index 9461be34..aed7ff11 100644 --- a/src/platform/script.ts +++ b/src/platform/script.ts @@ -2,111 +2,7 @@ import { PLATFORMS, RasterVideo } from "../common/emu"; import { Platform } from "../common/baseplatform"; import { Cell } from "../common/script/env"; -import { escapeHTML } from "../common/util"; -import { BitmapType, IndexedBitmap, RGBABitmap } from "../common/script/bitmap"; - -abstract class TileEditor { - video: RasterVideo; - - constructor( - public readonly tileWidth: number, - public readonly tileHeight: number, - public readonly numColumns: number, - public readonly numRows: number, - ) { - } - getPixelWidth() { return this.tileWidth * this.numColumns } - getPixelHeight() { return this.tileHeight * this.numRows } - attach(div: HTMLElement) { - this.video = new RasterVideo(div, this.getPixelWidth(), this.getPixelHeight()); - this.video.create(); - } - detach() { - this.video = null; - } - update() { - if (this.video) this.video.updateFrame(); - } - abstract getTile(x: number, y: number): T; - abstract renderTile(x: number, y: number): void; -} - -class RGBABitmapEditor extends TileEditor { - constructor( - public readonly bitmap: RGBABitmap - ) { - super(1, 1, bitmap.width, bitmap.height); - } - getTile(x: number, y: number) { - return this.bitmap.getPixel(x, y); - } - renderTile(x: number, y: number): void { - //TODO - } -} - -function bitmap2image(doc: HTMLDocument, div: HTMLElement, bitmap: BitmapType): HTMLCanvasElement { - var video = new RasterVideo(div, bitmap.width, bitmap.height); - video.create(doc); - video.canvas.className = 'pixelated'; - let vdata = video.getFrameData(); - if (bitmap['palette'] != null) { - let bmp = bitmap as IndexedBitmap; - let pal = bmp.palette.colors; - for (var i = 0; i < bmp.pixels.length; i++) { - vdata[i] = pal[bmp.pixels[i]]; - } - } else { - let bmp = bitmap as RGBABitmap; - vdata.set(bmp.rgba); - } - video.updateFrame(); - return video.canvas; -} - -class Notebook { - constructor( - public readonly maindoc: HTMLDocument, - public readonly maindiv: HTMLElement - ) { - maindiv.classList.add('vertical-scroll'); - //maindiv.classList.add('container') - } - updateCells(cells: Cell[]) { - let body = this.maindiv; - body.innerHTML = ''; - //var body = $(this.iframe).contents().find('body'); - //body.empty(); - for (let cell of cells) { - if (cell.object != null) { - let div = this.objectToDiv(cell.object); - div.id = cell.id; - div.classList.add('scripting-cell') - //div.classList.add('row') - body.append(div); - } - } - } - objectToDiv(object: any) { - let div = document.createElement('div'); - //div.classList.add('col-auto') - //grid-template-columns: repeat(auto-fit, minmax(50px, 1fr)); - // TODO: tile editor - if (Array.isArray(object) || object.BYTES_PER_ELEMENT) { - object.forEach((obj) => { - div.appendChild(this.objectToDiv(obj)); - }); - // TODO - } else if (object['bitsPerPixel'] && object['pixels'] && object['palette']) { - bitmap2image(this.maindoc, div, object as IndexedBitmap); - } else if (object['rgba']) { - bitmap2image(this.maindoc, div, object as RGBABitmap); - } else if (object != null) { - div.innerHTML = escapeHTML(JSON.stringify(object)); - } - return div; - } -} +import { Notebook } from "../common/script/ui/notebook"; class ScriptingPlatform implements Platform { mainElement: HTMLElement; @@ -125,7 +21,7 @@ class ScriptingPlatform implements Platform { head.appendChild($(``)[0]); }; */ - this.notebook = new Notebook(document, mainElement); + this.notebook = new Notebook(document, mainElement); } start() { } diff --git a/src/worker/workermain.ts b/src/worker/workermain.ts index e04f9db2..b8ff78a6 100644 --- a/src/worker/workermain.ts +++ b/src/worker/workermain.ts @@ -1094,7 +1094,7 @@ var TOOL_PRELOADFS = { 'wiz': 'wiz', } -const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay)); +//const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay)); // for testing async function handleMessage(data : WorkerMessage) : Promise { // preload file system @@ -1128,4 +1128,3 @@ if (ENVIRONMENT_IS_WORKER) { } } } -