use preact for scripting notebook, moved files

This commit is contained in:
Steven Hugg 2021-08-14 11:06:49 -05:00
parent a8b2b7c043
commit 7f86ed0cb6
14 changed files with 282 additions and 328 deletions

3
.gitignore vendored
View File

@ -8,3 +8,6 @@ tmp/
web/
release/
gen/
config.js
chromedriver.log
nightwatch.conf.js

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

15
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<void> = 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<void> {
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<void> {
// 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<void>;
}
class ValueNode<T> 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<T> extends ValueNode<T> {
}
class IntegerNode extends ValueNode<number> {
}
abstract class BitmapNode extends ComputeNode {
width: number;
height: number;
}
class RGBABitmapNode extends BitmapNode {
rgba: ArrayNode<Uint32Array>;
compute(): Promise<void> {
throw new Error("Method not implemented.");
}
}
class IndexedBitmapNode extends BitmapNode {
indices: ArrayNode<Uint8Array>;
compute(): Promise<void> {
throw new Error("Method not implemented.");
}
}
class PaletteNode {
colors: ArrayNode<Uint32Array>;
compute(): Promise<void> {
throw new Error("Method not implemented.");
}
}
class PaletteMapNode extends ComputeNode {
palette: PaletteNode;
indices: ArrayNode<Uint8Array>;
compute(): Promise<void> {
throw new Error("Method not implemented.");
}
}
function valueOf<T>(node : ValueNode<T>) : 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<number>(1234);
var arr1 = new ValueNode<number[]>([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();

View File

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

View File

@ -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<ColorComponentProps> {
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<BitmapComponentProps> {
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<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)
]);
}
}
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<tyarr.length; ofs+=maxBytes) {
children.push(objectToDiv(tyarr.slice(ofs, ofs+maxBytes)));
}
return h('div', { }, children);
}
}
let objectEntries = Object.entries(object);
// TODO: id?
let objectDivs = objectEntries.map(entry => 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);
}
}

View File

@ -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<T> {
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<number> {
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($(`<link rel="stylesheet" href="css/script.css">`)[0]);
};
*/
this.notebook = new Notebook(document, mainElement);
this.notebook = new Notebook(document, mainElement);
}
start() {
}

View File

@ -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<WorkerResult> {
// preload file system
@ -1128,4 +1128,3 @@ if (ENVIRONMENT_IS_WORKER) {
}
}
}