1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2024-06-11 12:29:29 +00:00

scripting: updates, moved interact to ui pkg

This commit is contained in:
Steven Hugg 2021-08-22 15:49:38 -05:00
parent 65a16db7b7
commit 005adcc9ba
7 changed files with 165 additions and 113 deletions

View File

@ -713,7 +713,7 @@ div.asset_toolbar {
} }
.tree-header { .tree-header {
display: flex; display: flex;
border: 2px solid #555; border: 1px solid #555;
border-radius:8px; border-radius:8px;
color: #fff; color: #fff;
background-color:#666; background-color:#666;
@ -777,16 +777,18 @@ div.asset_toolbar {
.scripting-cell canvas { .scripting-cell canvas {
min-height: 20em; min-height: 20em;
max-width: 95%; max-width: 95%;
border: 2px solid #222; border: 0;
outline-color: #ccc; outline-color: #ccc;
background: #000; background: #000;
padding: 4px; padding: 0;
margin: 1px; margin: 0;
image-rendering: pixelated; image-rendering: pixelated;
image-rendering: crisp-edges; image-rendering: crisp-edges;
} }
.scripting-cell canvas:hover { .scripting-cell pre {
border-color:#aaa; background-color: #333;
border: 1px inset #666;
color: #99dd99;
} }
.scripting-flex canvas { .scripting-flex canvas {
min-height: 2vw; min-height: 2vw;
@ -803,6 +805,7 @@ div.scripting-color {
padding: 0.5em; padding: 0.5em;
min-width: 3em; min-width: 3em;
min-height: 3em; min-height: 3em;
border: 3px solid black;
} }
div.scripting-color span { div.scripting-color span {
visibility: hidden; visibility: hidden;
@ -818,12 +821,13 @@ div.scripting-grid {
div.scripting-flex { div.scripting-flex {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center;
} }
div.scripting-select > div { div.scripting-select > div {
border-radius: 2px; border-radius: 2px;
border-style: dotted; border-style: dotted;
border-color: transparent; border-color: transparent;
margin: 0.25em;
} }
div.scripting-select > div:hover { div.scripting-select > div:hover {
border-color: #eee; border-color: #eee;

View File

@ -8,6 +8,8 @@ import * as output from "./lib/output";
import * as color from "./lib/color"; import * as color from "./lib/color";
import * as scriptui from "./lib/scriptui"; import * as scriptui from "./lib/scriptui";
export const PROP_CONSTRUCTOR_NAME = "$$consname";
export interface Cell { export interface Cell {
id: string; id: string;
object?: any; object?: any;
@ -175,6 +177,7 @@ export class Environment {
if (o == null) return; if (o == null) return;
if (checked.has(o)) return; if (checked.has(o)) return;
if (typeof o === 'object') { if (typeof o === 'object') {
o[PROP_CONSTRUCTOR_NAME] = Object.getPrototypeOf(o).constructor.name;
if (o.length > 100) return; // big array, don't bother if (o.length > 100) return; // big array, don't bother
if (o.BYTES_PER_ELEMENT > 0) return; // typed 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 checked.add(o); // so we don't recurse if cycle
@ -261,8 +264,10 @@ export class Environment {
} }
getLoadableState() { getLoadableState() {
let updated = null; let updated = null;
for (let [key, value] of Object.entries(this.declvars)) { // TODO: use Loadable
// TODO: use Loadable // TODO: visit children?
// TODO: doesn't work
for (let [key, value] of Object.entries(this.obj)) {
if (typeof value['$$getstate'] === 'function') { if (typeof value['$$getstate'] === 'function') {
let loadable = <any>value as io.Loadable; let loadable = <any>value as io.Loadable;
if (updated == null) updated = {}; if (updated == null) updated = {};

View File

@ -10,6 +10,8 @@ export type PixelMapFunction = (x: number, y: number) => number;
export abstract class AbstractBitmap<T> { export abstract class AbstractBitmap<T> {
aspect? : number; // aspect ratio, null == default == 1:1 aspect? : number; // aspect ratio, null == default == 1:1
style? : {} = {}; // CSS styles (TODO: other elements?)
constructor( constructor(
public readonly width: number, public readonly width: number,
public readonly height: number, public readonly height: number,

View File

@ -9,8 +9,6 @@ var $$store: WorkingStore;
var $$data: {} = {}; var $$data: {} = {};
// events // events
var $$seq = 0; var $$seq = 0;
// if an event is specified, it goes here
export const EVENT_KEY = "$$event";
export function $$setupFS(store: WorkingStore) { export function $$setupFS(store: WorkingStore) {
$$store = store; $$store = store;
@ -49,6 +47,14 @@ export namespace data {
} }
return object; return object;
} }
export function get(key: string) {
return $$data && $$data[key];
}
export function set(key: string, value: object) {
if ($$data) {
$$data[key] = value;
}
}
} }
export class IOWaitError extends Error { export class IOWaitError extends Error {
@ -128,67 +134,10 @@ export function splitlines(text: string) : string[] {
} }
// an object that can become interactive, identified by ID
export interface Interactive {
$$interact: InteractionRecord;
}
export interface InteractEvent {
interactid : number;
type: string;
x?: number;
y?: number;
button?: boolean;
}
// 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 Loadable {
interactid : number;
lastevent : {} = null;
constructor(
public readonly interacttarget: Interactive,
private $$callback
) {
}
$$setstate(newstate: {interactid: number}) {
this.interactid = newstate.interactid;
this.interacttarget.$$interact = this;
let event : InteractEvent = $$data[EVENT_KEY];
if (event && event.interactid == this.interactid) {
if (this.$$callback) {
this.$$callback(event);
}
this.lastevent = event;
$$data[EVENT_KEY] = null;
}
this.$$callback = null;
}
$$getstate() {
//TODO: this isn't always cleared before we serialize (e.g. if exception or move element)
this.$$callback = null;
return this;
}
}
export function isInteractive(obj: object): obj is Interactive {
return !!((obj as Interactive).$$interact);
}
export function interact(object: any, callback) : InteractionRecord {
// TODO: limit to Bitmap, etc
if (typeof object === 'object') {
return new InteractionRecord(object, callback);
}
throw new Error(`This object is not capable of interaction.`);
}
// TODO: what if this isn't top level? // TODO: what if this isn't top level?
export class Mutable<T> implements Loadable { export class Mutable<T> implements Loadable {
value : T; value : T;
constructor(public readonly initial : T) { constructor(initial : T) {
this.value = initial; this.value = initial;
} }
$$setstate(newstate) { $$setstate(newstate) {
@ -199,3 +148,6 @@ export class Mutable<T> implements Loadable {
} }
} }
export function mutable<T>(obj: object) : object {
return new Mutable(obj);
}

View File

@ -1,6 +1,68 @@
import * as io from "./io"; import * as io from "./io";
// if an event is specified, it goes here
export const EVENT_KEY = "$$event";
// an object that can become interactive, identified by ID
export interface Interactive {
$$interact: InteractionRecord;
}
export interface InteractEvent {
interactid : number;
type: string;
x?: number;
y?: number;
button?: boolean;
}
// 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 {
interactid : number;
lastevent : {} = null;
constructor(
public readonly interacttarget: Interactive,
private $$callback
) {
}
$$setstate(newstate: {interactid: number}) {
this.interactid = newstate.interactid;
this.interacttarget.$$interact = this;
let event : InteractEvent = io.data.get(EVENT_KEY);
if (event && event.interactid == this.interactid) {
if (this.$$callback) {
this.$$callback(event);
}
this.lastevent = event;
io.data.set(EVENT_KEY, null);
}
this.$$callback = null;
}
$$getstate() {
//TODO: this isn't always cleared before we serialize (e.g. if exception or move element)
this.$$callback = null;
return this;
}
}
export function isInteractive(obj: object): obj is Interactive {
return !!((obj as Interactive).$$interact);
}
export function interact(object: any, callback) : InteractionRecord {
// TODO: limit to Bitmap, etc
if (typeof object === 'object') {
return new InteractionRecord(object, callback);
}
throw new Error(`This object is not capable of interaction.`);
}
///
export interface ScriptUIType { export interface ScriptUIType {
uitype : string; uitype : string;
} }
@ -18,7 +80,6 @@ export class ScriptUISliderType implements ScriptUIType {
} }
export class ScriptUISlider extends ScriptUISliderType implements io.Loadable { export class ScriptUISlider extends ScriptUISliderType implements io.Loadable {
initvalue: number;
initial(value: number) { initial(value: number) {
this.value = value; this.value = value;
return this; return this;
@ -41,8 +102,8 @@ export class ScriptUISelectType<T> implements ScriptUIType {
constructor( constructor(
readonly options: T[] readonly options: T[]
) { ) {
this.value = null; this.index = 0;
this.index = -1; this.value = this.options[this.index];
} }
} }
@ -60,3 +121,20 @@ export class ScriptUISelect<T> extends ScriptUISelectType<T> implements io.Loada
export function select(options: any[]) { export function select(options: any[]) {
return new ScriptUISelect(options); return new ScriptUISelect(options);
} }
///
export class ScriptUIButtonType implements ScriptUIType {
readonly uitype = 'button';
constructor(
readonly name: string
) {
}
}
export class ScriptUIButton extends ScriptUIButtonType {
}
export function button(name: string) {
return new ScriptUIButton(name);
}

View File

@ -1,14 +1,13 @@
import { BitmapType, IndexedBitmap, RGBABitmap } from "../lib/bitmap";
import { Component, render, h, createRef, VNode } from 'preact'; import { Component, render, h, createRef, VNode } from 'preact';
import { Cell } from "../env"; import { Cell, PROP_CONSTRUCTOR_NAME } from "../env";
import { findIntegerFactors, hex, isArray, rgb2bgr } from "../../util"; import { findIntegerFactors, hex, isArray, rgb2bgr } from "../../util";
import { dumpRAM } from "../../emu"; import { dumpRAM } from "../../emu";
// TODO: can't call methods from this end
import * as color from "../lib/color";
import { ScriptUISelectType, ScriptUISliderType, ScriptUIType } from "../lib/scriptui";
import { current_project } from "../../../ide/ui"; import { current_project } from "../../../ide/ui";
import { EVENT_KEY, InteractEvent, Interactive, isInteractive } from "../lib/io"; // TODO: can't call methods from this end (e.g. Palette, Bitmap)
import * as bitmap from "../lib/bitmap";
import * as color from "../lib/color";
import * as scriptui from "../lib/scriptui";
const MAX_STRING_LEN = 100; const MAX_STRING_LEN = 100;
const DEFAULT_ASPECT = 1; const DEFAULT_ASPECT = 1;
@ -57,9 +56,9 @@ class ColorComponent extends Component<ColorComponentProps> {
} }
} }
function sendInteraction(iobj: Interactive, type: string, event: Event, xtraprops: {}) { function sendInteraction(iobj: scriptui.Interactive, type: string, event: Event, xtraprops: {}) {
let irec = iobj.$$interact; let irec = iobj.$$interact;
let ievent : InteractEvent = {interactid: irec.interactid, type, ...xtraprops}; let ievent : scriptui.InteractEvent = {interactid: irec.interactid, type, ...xtraprops};
if (event instanceof PointerEvent) { if (event instanceof PointerEvent) {
const canvas = event.target as HTMLCanvasElement; const canvas = event.target as HTMLCanvasElement;
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
@ -73,13 +72,13 @@ function sendInteraction(iobj: Interactive, type: string, event: Event, xtraprop
} }
// TODO: add events to queue? // TODO: add events to queue?
current_project.updateDataItems([{ current_project.updateDataItems([{
key: EVENT_KEY, key: scriptui.EVENT_KEY,
value: ievent value: ievent
}]); }]);
} }
interface BitmapComponentProps { interface BitmapComponentProps {
bitmap: BitmapType; bitmap: bitmap.BitmapType;
width: number; width: number;
height: number; height: number;
} }
@ -101,9 +100,10 @@ class BitmapComponent extends Component<BitmapComponentProps> {
ref: this.ref, ref: this.ref,
width: this.props.width, width: this.props.width,
height: this.props.height, height: this.props.height,
style: this.props.bitmap.style
} }
let obj : any = this.props.bitmap; let obj : any = this.props.bitmap;
if (isInteractive(obj)) { if (scriptui.isInteractive(obj)) {
return h('canvas', { return h('canvas', {
onPointerMove: (e: PointerEvent) => { onPointerMove: (e: PointerEvent) => {
sendInteraction(obj, 'move', e, { pressed: this.pressed }); sendInteraction(obj, 'move', e, { pressed: this.pressed });
@ -111,15 +111,15 @@ class BitmapComponent extends Component<BitmapComponentProps> {
onPointerDown: (e: PointerEvent) => { onPointerDown: (e: PointerEvent) => {
this.pressed = true; this.pressed = true;
this.canvas.setPointerCapture(e.pointerId); this.canvas.setPointerCapture(e.pointerId);
sendInteraction(obj, 'down', e, { }); sendInteraction(obj, 'down', e, { pressed: true });
}, },
onPointerUp: (e: PointerEvent) => { onPointerUp: (e: PointerEvent) => {
this.pressed = false; this.pressed = false;
sendInteraction(obj, 'up', e, { }); sendInteraction(obj, 'up', e, { pressed: false });
}, },
onPointerOut: (e: PointerEvent) => { onPointerOut: (e: PointerEvent) => {
this.pressed = false; this.pressed = false;
sendInteraction(obj, 'out', e, { }); sendInteraction(obj, 'out', e, { pressed: false });
}, },
...props ...props
}); });
@ -154,18 +154,18 @@ class BitmapComponent extends Component<BitmapComponentProps> {
this.updateCanvas(this.datau32, this.props.bitmap); this.updateCanvas(this.datau32, this.props.bitmap);
this.ctx.putImageData(this.imageData, 0, 0); this.ctx.putImageData(this.imageData, 0, 0);
} }
updateCanvas(vdata: Uint32Array, bmp: BitmapType) { updateCanvas(vdata: Uint32Array, bmp: bitmap.BitmapType) {
if (bmp['palette']) { if (bmp['palette']) {
this.updateCanvasIndexed(vdata, bmp as IndexedBitmap); this.updateCanvasIndexed(vdata, bmp as bitmap.IndexedBitmap);
} }
if (bmp['rgba']) { if (bmp['rgba']) {
this.updateCanvasRGBA(vdata, bmp as RGBABitmap); this.updateCanvasRGBA(vdata, bmp as bitmap.RGBABitmap);
} }
} }
updateCanvasRGBA(vdata: Uint32Array, bmp: RGBABitmap) { updateCanvasRGBA(vdata: Uint32Array, bmp: bitmap.RGBABitmap) {
vdata.set(bmp.rgba); vdata.set(bmp.rgba);
} }
updateCanvasIndexed(vdata: Uint32Array, bmp: IndexedBitmap) { updateCanvasIndexed(vdata: Uint32Array, bmp: bitmap.IndexedBitmap) {
let pal = bmp.palette.colors; let pal = bmp.palette.colors;
for (var i = 0; i < bmp.pixels.length; i++) { for (var i = 0; i < bmp.pixels.length; i++) {
vdata[i] = pal[bmp.pixels[i]]; vdata[i] = pal[bmp.pixels[i]];
@ -190,7 +190,6 @@ class ObjectKeyValueComponent extends Component<ObjectTreeComponentProps, Object
if (expandable) if (expandable)
hdrclass = this.state.expanded ? 'tree-expanded' : 'tree-collapsed' hdrclass = this.state.expanded ? 'tree-expanded' : 'tree-collapsed'
let propName = this.props.name || null; let propName = this.props.name || null;
if (propName && propName.startsWith("$$")) propName = null;
return h('div', { return h('div', {
class: 'tree-content', class: 'tree-content',
key: `${this.props.objpath}__tree` key: `${this.props.objpath}__tree`
@ -199,8 +198,10 @@ class ObjectKeyValueComponent extends Component<ObjectTreeComponentProps, Object
class: 'tree-header ' + hdrclass, class: 'tree-header ' + hdrclass,
onClick: expandable ? () => this.toggleExpand() : null onClick: expandable ? () => this.toggleExpand() : null
}, [ }, [
h('span', { class: 'tree-key' }, [ propName, expandable ]), propName != null ? h('span', { class: 'tree-key' }, [ propName, expandable ]) : null,
h('span', { class: 'tree-value scripting-item' }, [ getShortName(this.props.object) ]) h('span', { class: 'tree-value scripting-item' }, [
getShortName(this.props.object)
])
]), ]),
this.state.expanded ? objectToContentsDiv(this.props.object, this.props.objpath) : null this.state.expanded ? objectToContentsDiv(this.props.object, this.props.objpath) : null
]); ]);
@ -213,7 +214,7 @@ class ObjectKeyValueComponent extends Component<ObjectTreeComponentProps, Object
function getShortName(object: any) { function getShortName(object: any) {
if (typeof object === 'object') { if (typeof object === 'object') {
try { try {
var s = Object.getPrototypeOf(object).constructor.name; var s = object[PROP_CONSTRUCTOR_NAME] || Object.getPrototypeOf(object).constructor.name;
if (object.length > 0) { if (object.length > 0) {
s += `[${object.length}]` s += `[${object.length}]`
} }
@ -241,18 +242,20 @@ function primitiveToString(obj) {
} else if (typeof obj == 'number') { } else if (typeof obj == 'number') {
if (obj != (obj | 0)) text = obj.toString(); // must be a float if (obj != (obj | 0)) text = obj.toString(); // must be a float
else text = obj + "\t($" + hex(obj) + ")"; else text = obj + "\t($" + hex(obj) + ")";
} else if (typeof obj == 'string') {
text = obj;
} else { } else {
text = JSON.stringify(obj); text = JSON.stringify(obj);
if (text.length > MAX_STRING_LEN)
text = text.substring(0, MAX_STRING_LEN) + "...";
} }
if (text.length > MAX_STRING_LEN)
text = text.substring(0, MAX_STRING_LEN) + "...";
return text; return text;
} }
function isIndexedBitmap(object): object is IndexedBitmap { function isIndexedBitmap(object): object is bitmap.IndexedBitmap {
return object['bitsPerPixel'] && object['pixels'] && object['palette']; return object['bitsPerPixel'] && object['pixels'] && object['palette'];
} }
function isRGBABitmap(object): object is RGBABitmap { function isRGBABitmap(object): object is bitmap.RGBABitmap {
return object['rgba'] instanceof Uint32Array; return object['rgba'] instanceof Uint32Array;
} }
@ -269,6 +272,12 @@ function objectToChildren(object: any) : any[] {
} }
function objectToDiv(object: any, name: string, objpath: string): VNode<any> { 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; }
name = null;
}
// TODO: limit # of items // TODO: limit # of items
// TODO: detect table // TODO: detect table
if (object == null) { if (object == null) {
@ -332,12 +341,12 @@ function objectToContentsDiv(object: {} | [], objpath: string) {
interface UIComponentProps { interface UIComponentProps {
iokey: string; iokey: string;
uiobject: ScriptUIType; uiobject: scriptui.ScriptUIType;
} }
class UISliderComponent extends Component<UIComponentProps> { class UISliderComponent extends Component<UIComponentProps> {
render(virtualDom, containerNode, replaceNode) { render(virtualDom, containerNode, replaceNode) {
let slider = this.props.uiobject as ScriptUISliderType; let slider = this.props.uiobject as scriptui.ScriptUISliderType;
return h('div', {}, [ return h('div', {}, [
this.props.iokey, this.props.iokey,
h('input', { h('input', {
@ -350,8 +359,8 @@ class UISliderComponent extends Component<UIComponentProps> {
this.setState(this.state); this.setState(this.state);
current_project.updateDataItems([{key: this.props.iokey, value: newUIValue}]); current_project.updateDataItems([{key: this.props.iokey, value: newUIValue}]);
} }
}, []), }),
getShortName(slider.value) h('span', { }, getShortName(slider.value)),
]); ]);
} }
} }
@ -359,24 +368,26 @@ class UISliderComponent extends Component<UIComponentProps> {
class UISelectComponent extends Component<UIComponentProps> { class UISelectComponent extends Component<UIComponentProps> {
ref = createRef(); ref = createRef();
render(virtualDom, containerNode, replaceNode) { render(virtualDom, containerNode, replaceNode) {
let select = this.props.uiobject as ScriptUISelectType<any>; let select = this.props.uiobject as scriptui.ScriptUISelectType<any>;
let children = objectToChildren(select.options); let children = objectToChildren(select.options);
return h('div', { return h('div', {
class: 'scripting-select scripting-flex', class: 'scripting-select scripting-flex',
ref: this.ref, ref: this.ref,
onClick: (e) => { onClick: (e) => {
// iterate parents until we find select div, then find index of child // select object -- iterate parents until we find select div, then find index of child
let target = e.target as HTMLElement; let target = e.target as HTMLElement;
while (target.parentElement && target.parentElement != this.ref.current) { while (target.parentElement && target.parentElement != this.ref.current) {
target = target.parentElement; target = target.parentElement;
} }
const selindex = Array.from(target.parentElement.children).indexOf(target); if (target.parentElement) {
if (selindex >= 0 && selindex < children.length) { const selindex = Array.from(target.parentElement.children).indexOf(target);
let newUIValue = { value: children[selindex], index: selindex }; if (selindex >= 0 && selindex < children.length) {
this.setState(this.state); let newUIValue = { value: children[selindex], index: selindex };
current_project.updateDataItems([{key: this.props.iokey, value: newUIValue}]); this.setState(this.state);
} else { current_project.updateDataItems([{key: this.props.iokey, value: newUIValue}]);
throw new Error(`Could not find click target of ${this.props.iokey}`); } else {
throw new Error(`Could not find click target of ${this.props.iokey}`);
}
} }
} }
}, },

View File

@ -23,7 +23,7 @@ export async function runJavascript(step: BuildStep): Promise<BuildStepResult> {
io.$$loadData(store.items); // TODO: load from file io.$$loadData(store.items); // TODO: load from file
await env.run(code); await env.run(code);
let cells = env.render(); let cells = env.render();
let state = env.getLoadableState(); let state = env.getLoadableState(); // TODO: doesn't work
let output : RunResult = { cells, state }; let output : RunResult = { cells, state };
return { output: output }; return { output: output };
} catch (e) { } catch (e) {