scripting: fixed cache, io.module(), return values, button, blank()

This commit is contained in:
Steven Hugg 2021-08-23 13:12:02 -05:00
parent 6343c75953
commit 5542555193
9 changed files with 211 additions and 94 deletions

View File

@ -802,9 +802,8 @@ div.scripting-editor {
max-height: 50vw;
}
div.scripting-color {
padding: 0.5em;
min-width: 3em;
min-height: 3em;
min-width: 2em;
min-height: 2em;
border: 3px solid black;
}
div.scripting-color span {
@ -836,3 +835,14 @@ div.scripting-select > .scripting-selected {
border-style: solid;
border-color: #eee;
}
div.scripting-cell button {
background-color: #333;
color: #fff;
padding: 0.5em;
}
div.scripting-cell button:hover {
border-color: #fff;
}
div.scripting-cell button.scripting-enabled {
background-color: #339999;
}

View File

@ -192,7 +192,7 @@ export class Environment {
function prkey() { return fullkey.join('.') }
// go through all object properties recursively
for (var [key, value] of Object.entries(o)) {
if (value == null && fullkey.length == 0 && !key.startsWith("$$")) {
if (value == null && fullkey.length == 0 && !key.startsWith("$")) {
this.error(key, `"${key}" has no value.`)
}
fullkey.push(key);
@ -244,10 +244,22 @@ export class Environment {
start: loc.column,
}]
}
// TODO: Cannot parse given Error object
// TODO: Cannot parse given Error object?
let frames = ErrorStackParser.parse(e);
let frame = frames.findIndex(f => f.functionName === 'anonymous');
let errors = [];
// if ErrorStackParser fails, resort to regex
if (frame < 0 && e.stack != null) {
let m = /.anonymous.:(\d+):(\d+)/g.exec(e.stack);
if (m != null) {
errors.push( {
path: this.path,
msg: e.message,
line: parseInt(m[1]) - LINE_NUMBER_OFFSET,
});
}
}
// otherwise iterate thru all the frames
while (frame >= 0) {
console.log(frames[frame]);
if (frames[frame].fileName.endsWith('Function')) {
@ -261,6 +273,7 @@ export class Environment {
}
--frame;
}
// if no stack frames parsed, last resort error msg
if (errors.length == 0) {
errors.push( {
path: this.path,

View File

@ -4,7 +4,7 @@ import * as fastpng from 'fast-png';
import { Palette } from './color';
import * as io from './io'
import * as color from './color'
import { findIntegerFactors, RGBA } from '../../util';
import { coerceToArray, findIntegerFactors, RGBA } from '../../util';
export type PixelMapFunction = (x: number, y: number) => number;
@ -17,17 +17,16 @@ export abstract class AbstractBitmap<T> {
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 setarray(arr: ArrayLike<number>) : void;
abstract set(x: number, y: number, val: number) : void;
abstract get(x: number, y: number): number;
abstract getrgba(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) {
assign(fn: ArrayLike<number> | PixelMapFunction) : void {
if (typeof fn === 'function') {
for (let y=0; y<this.height; y++) {
for (let x=0; x<this.width; x++) {
@ -39,10 +38,11 @@ export abstract class AbstractBitmap<T> {
} 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));
let bmp = this.blank(this.width, this.height);
bmp.assign((x,y) => this.get(x,y));
return bmp;
}
crop(srcx: number, srcy: number, width: number, height: number) {
let dest = this.blank(width, height);
@ -64,6 +64,13 @@ export abstract class AbstractBitmap<T> {
}
}
}
fill(destx: number, desty: number, width:number, height:number, value:number) {
for (var y=0; y<height; y++) {
for (var x=0; x<width; x++) {
this.set(x+destx, y+desty, value);
}
}
}
}
export class RGBABitmap extends AbstractBitmap<RGBABitmap> {
@ -80,11 +87,9 @@ export class RGBABitmap extends AbstractBitmap<RGBABitmap> {
}
setarray(arr: ArrayLike<number>) {
this.rgba.set(arr);
return this;
}
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;
@ -92,8 +97,8 @@ export class RGBABitmap extends AbstractBitmap<RGBABitmap> {
getrgba(x: number, y: number): number {
return this.get(x, y);
}
blank(width: number, height: number) : RGBABitmap {
return new RGBABitmap(width, height);
blank(width?: number, height?: number) : RGBABitmap {
return new RGBABitmap(width || this.width, height || this.height);
}
clone() : RGBABitmap {
let bitmap = this.blank(this.width, this.height);
@ -108,47 +113,55 @@ export abstract class MappedBitmap extends AbstractBitmap<MappedBitmap> {
constructor(
width: number,
height: number,
public readonly bitsPerPixel: number,
public readonly bpp: number,
initial?: Uint8Array | PixelMapFunction
) {
super(width, height);
if (bitsPerPixel != 1 && bitsPerPixel != 2 && bitsPerPixel != 4 && bitsPerPixel != 8)
throw new Error(`Invalid bits per pixel: ${bitsPerPixel}`);
if (bpp != 1 && bpp != 2 && bpp != 4 && bpp != 8)
throw new Error(`Invalid bits per pixel: ${bpp}`);
this.pixels = new Uint8Array(this.width * this.height);
if (initial) this.assign(initial);
}
setarray(arr: ArrayLike<number>) {
this.pixels.set(arr);
return this;
}
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;
}
}
function getbpp(x : number | Palette) : number {
if (typeof x === 'number') return x;
if (x instanceof Palette) {
if (x.colors.length <= 2) return 1;
else if (x.colors.length <= 4) return 2;
else if (x.colors.length <= 16) return 4;
}
return 8;
}
export class IndexedBitmap extends MappedBitmap {
public palette: Palette;
constructor(
width: number,
height: number,
bitsPerPixel: number,
bppOrPalette: number | Palette,
initial?: Uint8Array | PixelMapFunction
) {
super(width, height, bitsPerPixel || 8, initial);
this.palette = color.palette.colors(1 << this.bitsPerPixel);
super(width, height, getbpp(bppOrPalette), initial);
this.palette = bppOrPalette instanceof Palette
? bppOrPalette
: color.palette.colors(1 << this.bpp);
}
getrgba(x: number, y: number): number {
return this.palette && this.palette.colors[this.get(x, y)];
}
blank(width: number, height: number) : IndexedBitmap {
let bitmap = new IndexedBitmap(width, height, this.bitsPerPixel);
bitmap.palette = this.palette;
blank(width?: number, height?: number, newPalette?: Palette) : IndexedBitmap {
let bitmap = new IndexedBitmap(width || this.width, height || this.height, newPalette || this.palette);
return bitmap;
}
clone() : IndexedBitmap {
@ -184,6 +197,7 @@ export interface BitmapAnalysis {
}
export function analyze(bitmaps: BitmapType[]) {
bitmaps = coerceToArray(bitmaps);
let r = {min:{w:0,h:0}, max:{w:0,h:0}};
for (let bmp of bitmaps) {
if (!(bmp instanceof AbstractBitmap)) return null;
@ -202,6 +216,7 @@ export interface MontageOptions {
}
export function montage(bitmaps: BitmapType[], options?: MontageOptions) {
bitmaps = coerceToArray(bitmaps);
let minmax = (options && options.analysis) || analyze(bitmaps);
if (minmax == null) throw new Error(`Expected an array of bitmaps`);
let hitrects = [];

View File

@ -1,12 +1,15 @@
import { FileDataCache } from "../../util";
import { FileData, WorkingStore } from "../../workertypes";
// remote resource cache
var $$cache: WeakMap<object,FileData> = new WeakMap();
var $$cache = new FileDataCache(); // TODO: better cache?
// file read/write interface
var $$store: WorkingStore;
// backing store for data
var $$data: {} = {};
// module cache
var $$modules: Map<string,{}> = new Map();
export function $$setupFS(store: WorkingStore) {
$$store = store;
@ -38,7 +41,7 @@ export namespace data {
return object;
}
export function save(object: Loadable, key: string): Loadable {
if ($$data && object.$$getstate) {
if ($$data && object && object.$$getstate) {
$$data[key] = object.$$getstate();
}
return object;
@ -67,10 +70,6 @@ export function canonicalurl(url: string) : string {
return 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();
@ -99,17 +98,19 @@ export function readnocache(url: string, type?: 'binary' | 'text'): FileData {
// TODO: read files too
export function read(url: string, type?: 'binary' | 'text'): FileData {
// canonical-ize url
url = canonicalurl(url);
// check cache
let cachekey = {url: url};
if ($$cache.has(cachekey)) {
return $$cache.get(cachekey);
}
let data = readnocache(url, type);
// check cache first
let cachekey = url;
let data = $$cache.get(cachekey);
if (data != null) return data;
// not in cache, read it
data = readnocache(url, type);
if (data == null) throw new Error(`Cannot find resource "${url}"`);
if (type === 'text' && typeof data !== 'string') throw new Error(`Resource "${url}" is not a string`);
if (type === 'binary' && !(data instanceof Uint8Array)) throw new Error(`Resource "${url}" is not a binary file`);
$$cache.set(cachekey, data);
// store in cache
$$cache.put(cachekey, data);
return data;
}
@ -129,6 +130,22 @@ export function splitlines(text: string) : string[] {
return text.split(/\n|\r\n/g);
}
export function module(url: string) {
// find module in cache?
let key = `${url}::${url.length}`;
let exports = $$modules.get(key);
if (exports == null) {
let code = readnocache(url, 'text') as string;
let func = new Function('exports', 'module', code);
let module = {}; // TODO?
exports = {};
func(exports, module);
$$modules.set(key, exports);
}
return exports;
}
///
// TODO: what if this isn't top level?
export class Mutable<T> implements Loadable {

View File

@ -1,6 +1,10 @@
import { coerceToArray } from "../../util";
import * as io from "./io";
// sequence counter
var $$seq : number = 0;
// if an event is specified, it goes here
export const EVENT_KEY = "$$event";
@ -17,17 +21,22 @@ export interface InteractEvent {
button?: boolean;
}
export type InteractCallback = (event: InteractEvent) => void;
// InteractionRecord maps a target object to an interaction ID
// the $$callback is used once per script eval, then gets nulled
// whether or not it's invoked
// event comes from $$data.$$event
export class InteractionRecord implements io.Loadable {
readonly interacttarget: Interactive;
interactid : number;
lastevent : {} = null;
constructor(
public readonly interacttarget: Interactive,
private $$callback
interacttarget: Interactive,
private $$callback: InteractCallback
) {
this.interacttarget = interacttarget || (<any>this as Interactive);
this.interactid = ++$$seq;
}
$$setstate(newstate: {interactid: number}) {
this.interactid = newstate.interactid;
@ -46,7 +55,7 @@ export class InteractionRecord implements io.Loadable {
//TODO: this isn't always cleared before we serialize (e.g. if exception or move element)
//and we do it in checkResult() too
this.$$callback = null;
return this;
return {interactid: this.interactid};
}
}
@ -125,17 +134,42 @@ export function select(options: any[]) {
///
export class ScriptUIButtonType implements ScriptUIType {
export class ScriptUIButtonType extends InteractionRecord implements ScriptUIType, Interactive {
readonly uitype = 'button';
$$interact: InteractionRecord;
enabled?: boolean;
constructor(
readonly name: string
readonly label: string,
callback: InteractCallback
) {
super(null, callback);
this.$$interact = this;
}
}
export class ScriptUIButton extends ScriptUIButtonType {
}
export function button(name: string) {
return new ScriptUIButton(name);
export function button(name: string, callback: InteractCallback) {
return new ScriptUIButton(name, callback);
}
export class ScriptUIToggle extends ScriptUIButton implements io.Loadable {
// share with InteractionRecord
$$getstate() {
let state = super.$$getstate() as any;
state.enabled = this.enabled;
return state;
}
$$setstate(newstate: any) {
this.enabled = newstate.enabled;
super.$$setstate(newstate);
}
}
export function toggle(name: string) {
return new ScriptUIToggle(name, function(e) {
this.enabled = !this.enabled;
});
}

View File

@ -12,27 +12,27 @@ import * as scriptui from "../lib/scriptui";
const MAX_STRING_LEN = 100;
const DEFAULT_ASPECT = 1;
interface ObjectStats {
type: 'prim' | 'complex' | 'bitmap'
width: number
height: number
units: 'em' | 'px'
}
// TODO
class ObjectAnalyzer {
recurse(obj: any) : ObjectStats {
if (typeof obj === 'string') {
return { type: 'prim', width: obj.length, height: 1, units: 'em' }
} else if (obj instanceof Uint8Array) {
return { type: 'complex', width: 60, height: Math.ceil(obj.length / 16), units: 'em' }
} else if (typeof obj === 'object') {
let stats : ObjectStats = { type: 'complex', width: 0, height: 0, units: 'em'};
return stats; // TODO
} else {
return { type: 'prim', width: 12, height: 1, units: 'em' }
}
function sendInteraction(iobj: scriptui.Interactive, type: string, event: Event, xtraprops: {}) {
let irec = iobj.$$interact;
let ievent : scriptui.InteractEvent = {interactid: irec.interactid, type, ...xtraprops};
if (event instanceof PointerEvent) {
const canvas = event.target as HTMLCanvasElement;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (event.clientX - rect.left) * scaleX;
const y = (event.clientY - rect.top) * scaleY;
ievent.x = Math.floor(x);
ievent.y = Math.floor(y);
// TODO: pressure, etc.
} else {
console.log("Unknown event type", event);
}
// TODO: add events to queue?
current_project.updateDataItems([{
key: scriptui.EVENT_KEY,
value: ievent
}]);
}
interface ColorComponentProps {
@ -56,27 +56,6 @@ class ColorComponent extends Component<ColorComponentProps> {
}
}
function sendInteraction(iobj: scriptui.Interactive, type: string, event: Event, xtraprops: {}) {
let irec = iobj.$$interact;
let ievent : scriptui.InteractEvent = {interactid: irec.interactid, type, ...xtraprops};
if (event instanceof PointerEvent) {
const canvas = event.target as HTMLCanvasElement;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (event.clientX - rect.left) * scaleX;
const y = (event.clientY - rect.top) * scaleY;
ievent.x = Math.round(x);
ievent.y = Math.round(y);
// TODO: pressure, etc.
}
// TODO: add events to queue?
current_project.updateDataItems([{
key: scriptui.EVENT_KEY,
value: ievent
}]);
}
interface BitmapComponentProps {
bitmap: bitmap.BitmapType;
width: number;
@ -253,7 +232,7 @@ function primitiveToString(obj) {
}
function isIndexedBitmap(object): object is bitmap.IndexedBitmap {
return object['bitsPerPixel'] && object['pixels'] && object['palette'];
return object['bpp'] && object['pixels'] && object['palette'];
}
function isRGBABitmap(object): object is bitmap.RGBABitmap {
return object['rgba'] instanceof Uint32Array;
@ -275,7 +254,10 @@ function objectToDiv(object: any, name: string, objpath: string): VNode<any> {
// don't view any keys that start with "$"
if (name && name.startsWith("$")) {
// don't view any values that start with "$$"
if (name.startsWith("$$")) { return; }
if (name.startsWith("$$")) return;
// don't print if null or undefined
if (object == null) return;
// don't print key in any case
name = null;
}
// TODO: limit # of items
@ -400,9 +382,24 @@ class UISelectComponent extends Component<UIComponentProps> {
}
}
class UIButtonComponent extends Component<UIComponentProps> {
render(virtualDom, containerNode, replaceNode) {
let button = this.props.uiobject as scriptui.ScriptUIButtonType;
return h('button', {
class: button.enabled ? 'scripting-button scripting-enabled' : 'scripting-button',
onClick: (e: MouseEvent) => {
sendInteraction(button, 'click', e, { });
},
}, [
button.label
])
}
}
const UI_COMPONENTS = {
'slider': UISliderComponent,
'select': UISelectComponent,
'button': UIButtonComponent,
}
///

View File

@ -1,4 +1,3 @@
import { UintArray } from "../ide/pixeleditor";
export function lpad(s:string, n:number):string {
s += ''; // convert to string
@ -56,7 +55,7 @@ export function toradix(v:number, nd:number, radix:number) {
}
}
export function arrayCompare(a:any[]|UintArray, b:any[]|UintArray):boolean {
export function arrayCompare(a:ArrayLike<any>, b:ArrayLike<any>):boolean {
if (a == null && b == null) return true;
if (a == null) return false;
if (b == null) return false;
@ -662,3 +661,34 @@ export function findIntegerFactors(x: number, mina: number, minb: number, aspect
}
return {a, b};
}
export class FileDataCache {
maxSize : number = 8000000;
size : number;
cache : Map<string, string|Uint8Array>;
constructor() {
this.reset();
}
get(key : string) : string|Uint8Array {
return this.cache.get(key);
}
put(key : string, value : string|Uint8Array) {
this.cache.set(key, value);
this.size += value.length;
if (this.size > this.maxSize) {
console.log('cache reset', this);
this.reset();
}
}
reset() {
this.cache = new Map();
this.size = 0;
}
}
export function coerceToArray<T>(arrobj: any) : T[] {
if (Array.isArray(arrobj)) return arrobj;
else if (arrobj != null && typeof arrobj[Symbol.iterator] === 'function') return Array.from(arrobj);
else if (typeof arrobj === 'object') return Array.from(Object.values(arrobj))
else throw new Error(`Expected array or object, got "${arrobj}"`);
}

View File

@ -147,6 +147,7 @@ function fatalError(s:string) {
}
function newWorker() : Worker {
// TODO: return new Worker("https://8bitworkshop.com.s3-website-us-east-1.amazonaws.com/dev/gen/worker/bundle.js");
return new Worker("./gen/worker/bundle.js");
}

View File

@ -1,5 +1,5 @@
import { PLATFORMS, RasterVideo } from "../common/emu";
import { PLATFORMS } from "../common/emu";
import { Platform } from "../common/baseplatform";
import { RunResult } from "../common/script/env";
import { Notebook } from "../common/script/ui/notebook";