2016-04-07 20:30:22 +02:00
|
|
|
/// <reference path="../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
|
|
|
|
/// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
2016-04-02 00:44:04 +02:00
|
|
|
/// <reference path="../libs/microbit/dal.d.ts"/>
|
2016-03-11 01:24:11 +01:00
|
|
|
|
2016-04-07 21:52:02 +02:00
|
|
|
namespace pxsim {
|
2016-08-30 20:55:00 +02:00
|
|
|
export type BBRowCol = [/*row*/string, /*column*/string];
|
|
|
|
export type BoardPin = string;
|
|
|
|
export interface BBLoc {type: "breadboard", rowCol: BBRowCol};
|
|
|
|
export interface BoardLoc {type: "dalboard", pin: BoardPin};
|
|
|
|
export type Loc = BBLoc | BoardLoc;
|
2016-04-14 16:39:24 +02:00
|
|
|
|
2016-08-30 20:55:00 +02:00
|
|
|
export function initRuntimeWithDalBoard() {
|
|
|
|
U.assert(!runtime.board);
|
|
|
|
let b = new DalBoard();
|
|
|
|
runtime.board = b;
|
2016-06-25 20:40:21 +02:00
|
|
|
}
|
2016-08-30 20:55:00 +02:00
|
|
|
if (!pxsim.initCurrentRuntime) {
|
|
|
|
pxsim.initCurrentRuntime = initRuntimeWithDalBoard;
|
2016-06-25 20:40:21 +02:00
|
|
|
}
|
|
|
|
|
2016-08-30 20:55:00 +02:00
|
|
|
export function board() {
|
|
|
|
return runtime.board as DalBoard;
|
2016-04-03 05:44:29 +02:00
|
|
|
}
|
|
|
|
|
2016-08-30 20:55:00 +02:00
|
|
|
export function mkRange(a: number, b: number): number[] {
|
|
|
|
let res: number[] = [];
|
|
|
|
for (; a < b; a++)
|
|
|
|
res.push(a);
|
|
|
|
return res;
|
2016-04-03 05:44:29 +02:00
|
|
|
}
|
|
|
|
|
2016-08-30 20:55:00 +02:00
|
|
|
export function parseQueryString(): (key: string) => string {
|
|
|
|
let qs = window.location.search.substring(1);
|
2016-09-03 06:38:22 +02:00
|
|
|
let getQsVal = (key: string) => decodeURIComponent((qs.split(`${key}=`)[1] || "").split("&")[0] || ""); //.replace(/\+/g, " ");
|
2016-08-30 20:55:00 +02:00
|
|
|
return getQsVal;
|
2016-06-14 22:53:05 +02:00
|
|
|
}
|
2016-06-14 22:24:55 +02:00
|
|
|
}
|
|
|
|
|
2016-08-30 20:55:00 +02:00
|
|
|
namespace pxsim.visuals {
|
|
|
|
export interface IPointerEvents {
|
|
|
|
up: string,
|
|
|
|
down: string,
|
|
|
|
move: string,
|
|
|
|
leave: string
|
|
|
|
}
|
|
|
|
|
|
|
|
export const pointerEvents: IPointerEvents = !!(window as any).PointerEvent ? {
|
|
|
|
up: "pointerup",
|
|
|
|
down: "pointerdown",
|
|
|
|
move: "pointermove",
|
|
|
|
leave: "pointerleave"
|
|
|
|
} : {
|
|
|
|
up: "mouseup",
|
|
|
|
down: "mousedown",
|
|
|
|
move: "mousemove",
|
|
|
|
leave: "mouseleave"
|
|
|
|
};
|
2016-05-05 08:31:55 +02:00
|
|
|
|
2016-08-30 20:55:00 +02:00
|
|
|
export function translateEl(el: SVGElement, xy: [number, number]) {
|
|
|
|
//TODO append translation instead of replacing the full transform
|
|
|
|
svg.hydrate(el, {transform: `translate(${xy[0]} ${xy[1]})`});
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ComposeOpts {
|
|
|
|
el1: SVGAndSize<SVGSVGElement>,
|
|
|
|
scaleUnit1: number,
|
|
|
|
el2: SVGAndSize<SVGSVGElement>,
|
|
|
|
scaleUnit2: number,
|
|
|
|
margin: [number, number, number, number],
|
|
|
|
middleMargin: number,
|
2016-08-31 00:33:57 +02:00
|
|
|
maxWidth?: string,
|
|
|
|
maxHeight?: string,
|
2016-08-30 20:55:00 +02:00
|
|
|
}
|
|
|
|
export interface ComposeResult {
|
|
|
|
host: SVGSVGElement,
|
|
|
|
scaleUnit: number,
|
|
|
|
under: SVGGElement,
|
|
|
|
over: SVGGElement,
|
|
|
|
edges: number[],
|
|
|
|
toHostCoord1: (xy: Coord) => Coord,
|
|
|
|
toHostCoord2: (xy: Coord) => Coord,
|
|
|
|
}
|
|
|
|
export function composeSVG(opts: ComposeOpts): ComposeResult {
|
|
|
|
let [a, b] = [opts.el1, opts.el2];
|
|
|
|
U.assert(a.x == 0 && a.y == 0 && b.x == 0 && b.y == 0, "el1 and el2 x,y offsets not supported");
|
|
|
|
let setXY = (e: SVGSVGElement, x: number, y: number) => svg.hydrate(e, {x: x, y: y});
|
2016-08-31 20:34:49 +02:00
|
|
|
let setWH = (e: SVGSVGElement, w: string, h: string) => {
|
|
|
|
if (w)
|
|
|
|
svg.hydrate(e, {width: w});
|
|
|
|
if (h)
|
|
|
|
svg.hydrate(e, {height: h});
|
|
|
|
}
|
2016-08-31 00:33:57 +02:00
|
|
|
let setWHpx = (e: SVGSVGElement, w: number, h: number) => svg.hydrate(e, {width: `${w}px`, height: `${h}px`});
|
2016-08-30 20:55:00 +02:00
|
|
|
let scaleUnit = opts.scaleUnit2;
|
|
|
|
let aScalar = opts.scaleUnit2 / opts.scaleUnit1;
|
|
|
|
let bScalar = 1.0;
|
|
|
|
let aw = a.w * aScalar;
|
|
|
|
let ah = a.h * aScalar;
|
2016-08-31 00:33:57 +02:00
|
|
|
setWHpx(a.el, aw, ah);
|
2016-08-30 20:55:00 +02:00
|
|
|
let bw = b.w * bScalar;
|
|
|
|
let bh = b.h * bScalar;
|
2016-08-31 00:33:57 +02:00
|
|
|
setWHpx(b.el, bw, bh);
|
2016-08-30 20:55:00 +02:00
|
|
|
let [mt, mr, mb, ml] = opts.margin;
|
|
|
|
let mm = opts.middleMargin;
|
|
|
|
let innerW = Math.max(aw, bw);
|
|
|
|
let ax = mr + (innerW - aw) / 2.0;
|
|
|
|
let ay = mt;
|
|
|
|
setXY(a.el, ax, ay);
|
|
|
|
let bx = mr + (innerW - bw) / 2.0;
|
|
|
|
let by = ay + ah + mm;
|
|
|
|
setXY(b.el, bx, by);
|
|
|
|
let edges = [ay, ay + ah, by, by + bh];
|
|
|
|
let w = mr + innerW + ml;
|
|
|
|
let h = mt + ah + mm + bh + mb;
|
|
|
|
let host = <SVGSVGElement>svg.elt("svg", {
|
|
|
|
"version": "1.0",
|
|
|
|
"viewBox": `0 0 ${w} ${h}`,
|
|
|
|
"class": `sim-bb`,
|
|
|
|
});
|
2016-08-31 20:34:49 +02:00
|
|
|
setWH(host, opts.maxWidth, opts.maxHeight);
|
2016-08-30 20:55:00 +02:00
|
|
|
setXY(host, 0, 0);
|
|
|
|
let under = <SVGGElement>svg.child(host, "g");
|
|
|
|
host.appendChild(a.el);
|
|
|
|
host.appendChild(b.el);
|
|
|
|
let over = <SVGGElement>svg.child(host, "g");
|
|
|
|
let toHostCoord1 = (xy: Coord): Coord => {
|
|
|
|
let [x, y] = xy;
|
|
|
|
return [x * aScalar + ax, y * aScalar + ay];
|
|
|
|
};
|
|
|
|
let toHostCoord2 = (xy: Coord): Coord => {
|
|
|
|
let [x, y] = xy;
|
|
|
|
return [x * bScalar + bx, y * bScalar + by];
|
|
|
|
};
|
|
|
|
return {
|
|
|
|
under: under,
|
|
|
|
over: over,
|
|
|
|
host: host,
|
|
|
|
edges: edges,
|
|
|
|
scaleUnit: scaleUnit,
|
|
|
|
toHostCoord1: toHostCoord1,
|
|
|
|
toHostCoord2: toHostCoord2,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2016-09-01 03:03:34 +02:00
|
|
|
export function mkScaleFn(originUnit: number, targetUnit: number): (n: number) => number {
|
|
|
|
return (n: number) => n * (targetUnit / originUnit);
|
|
|
|
}
|
|
|
|
export interface MkImageOpts {
|
|
|
|
image: string,
|
|
|
|
width: number,
|
|
|
|
height: number,
|
|
|
|
imageUnitDist: number,
|
|
|
|
targetUnitDist: number
|
|
|
|
}
|
|
|
|
export function mkImageSVG(opts: MkImageOpts): SVGAndSize<SVGImageElement> {
|
|
|
|
let scaleFn = mkScaleFn(opts.imageUnitDist, opts.targetUnitDist);
|
|
|
|
let w = scaleFn(opts.width);
|
|
|
|
let h = scaleFn(opts.height);
|
|
|
|
let img = <SVGImageElement>svg.elt("image", {
|
|
|
|
width: w,
|
|
|
|
height: h,
|
|
|
|
"href": `${opts.image}`
|
|
|
|
});
|
|
|
|
return {el: img, w: w, h: h, x: 0, y: 0};
|
|
|
|
}
|
|
|
|
|
2016-08-30 20:55:00 +02:00
|
|
|
export type Coord = [number, number];
|
|
|
|
export function findDistSqrd(a: Coord, b: Coord): number {
|
|
|
|
let x = a[0] - b[0];
|
|
|
|
let y = a[1] - b[1];
|
|
|
|
return x * x + y * y;
|
|
|
|
}
|
|
|
|
export function findClosestCoordIdx(a: Coord, bs: Coord[]): number {
|
|
|
|
let dists = bs.map(b => findDistSqrd(a, b));
|
|
|
|
let minIdx = dists.reduce((prevIdx, currDist, currIdx, arr) => {
|
|
|
|
return currDist < arr[prevIdx] ? currIdx : prevIdx;
|
|
|
|
}, 0);
|
|
|
|
return minIdx;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface IBoardComponent<T> {
|
|
|
|
style: string,
|
|
|
|
element: SVGElement,
|
|
|
|
defs: SVGElement[],
|
|
|
|
init(bus: EventBus, state: T, svgEl: SVGSVGElement, gpioPins: string[], otherArgs: string[]): void, //NOTE: constructors not supported in interfaces
|
|
|
|
moveToCoord(xy: Coord): void,
|
|
|
|
updateState(): void,
|
|
|
|
updateTheme(): void,
|
|
|
|
}
|
|
|
|
|
|
|
|
export function mkTxt(cx: number, cy: number, size: number, rot: number, txt: string, txtXOffFactor?: number, txtYOffFactor?: number): SVGTextElement {
|
|
|
|
let el = <SVGTextElement>svg.elt("text")
|
|
|
|
//HACK: these constants (txtXOffFactor, txtYOffFactor) tweak the way this algorithm knows how to center the text
|
|
|
|
txtXOffFactor = txtXOffFactor || -0.33333;
|
|
|
|
txtYOffFactor = txtYOffFactor || 0.3;
|
|
|
|
const xOff = txtXOffFactor * size * txt.length;
|
|
|
|
const yOff = txtYOffFactor * size;
|
|
|
|
svg.hydrate(el, {style: `font-size:${size}px;`,
|
|
|
|
transform: `translate(${cx} ${cy}) rotate(${rot}) translate(${xOff} ${yOff})` });
|
|
|
|
svg.addClass(el, "noselect");
|
|
|
|
el.textContent = txt;
|
|
|
|
return el;
|
|
|
|
}
|
|
|
|
|
|
|
|
export type WireColor =
|
|
|
|
"black" | "white" | "gray" | "purple" | "blue" | "green" | "yellow" | "orange" | "red" | "brown";
|
|
|
|
export const WIRE_COLOR_MAP: Map<string> = {
|
|
|
|
black: "#514f4d",
|
|
|
|
white: "#fcfdfc",
|
|
|
|
gray: "#acabab",
|
|
|
|
purple: "#a772a1",
|
|
|
|
blue: "#01a6e8",
|
|
|
|
green: "#3cce73",
|
|
|
|
yellow: "#ece600",
|
|
|
|
orange: "#fdb262",
|
|
|
|
red: "#f44f43",
|
|
|
|
brown: "#c89764",
|
|
|
|
}
|
|
|
|
export function mapWireColor(clr: WireColor | string): string {
|
|
|
|
return WIRE_COLOR_MAP[clr] || clr;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface SVGAndSize<T extends SVGElement> {
|
|
|
|
el: T,
|
|
|
|
y: number,
|
|
|
|
x: number,
|
|
|
|
w: number,
|
|
|
|
h: number
|
|
|
|
};
|
|
|
|
export type SVGElAndSize = SVGAndSize<SVGElement>;
|
2016-04-12 17:55:20 +02:00
|
|
|
|
2016-08-30 20:55:00 +02:00
|
|
|
export const PIN_DIST = 15;
|
2016-08-30 23:13:44 +02:00
|
|
|
|
|
|
|
export interface BoardView {
|
|
|
|
getView(): SVGAndSize<SVGSVGElement>;
|
|
|
|
getCoord(pinNm: string): Coord;
|
|
|
|
getPinDist(): number;
|
2016-08-31 20:14:16 +02:00
|
|
|
highlightPin(pinNm: string): void;
|
2016-08-30 23:13:44 +02:00
|
|
|
}
|
2016-08-30 20:55:00 +02:00
|
|
|
}
|