Merge pull request #225 from Microsoft/generic-part

Adds support for generic parts; fixes generic boards to utilize BoardHost
This commit is contained in:
Peli de Halleux 2016-08-31 20:49:03 -07:00 committed by GitHub
commit c6c133ef9e
9 changed files with 234 additions and 378 deletions

View File

@ -1,4 +1,4 @@
# don't check in until OSS request is approved # don't check in until OSS request is approved
neopixel-black-60-vert.svg
sparkfun-* sparkfun-*
raspberrypi-* raspberrypi-*
arduino-*

View File

@ -71,11 +71,7 @@ namespace pxsim {
initAsync(msg: SimulatorRunMessage): Promise<void> { initAsync(msg: SimulatorRunMessage): Promise<void> {
let options = (msg.options || {}) as RuntimeOptions; let options = (msg.options || {}) as RuntimeOptions;
//TODO: read from pxt.json/pxttarget.json let boardDef = CURRENT_BOARD; //TODO: read from pxt.json/pxttarget.json
let boardDef = MICROBIT_DEF;
// let boardDef = ARDUINO_ZERO;
// let boardDef = SPARKFUN_PHOTON;
// let boardDef = RASPBERRYPI_MODELB;
let cmpsList = msg.parts; let cmpsList = msg.parts;
let cmpDefs = PART_DEFINITIONS; //TODO: read from pxt.json/pxttarget.json let cmpDefs = PART_DEFINITIONS; //TODO: read from pxt.json/pxttarget.json

View File

@ -7,10 +7,11 @@ namespace pxsim {
export interface PinBlockDefinition { export interface PinBlockDefinition {
x: number, x: number,
y: number, y: number,
labelPosition: "above" | "below";
labels: string[] labels: string[]
} }
export interface BoardImageDefinition { export interface BoardImageDefinition {
image?: string, image: string,
outlineImage?: string, outlineImage?: string,
width: number, width: number,
height: number, height: number,
@ -25,6 +26,8 @@ namespace pxsim {
threeVoltPins: string[], threeVoltPins: string[],
attachPowerOnRight?: boolean, attachPowerOnRight?: boolean,
onboardComponents?: string[] onboardComponents?: string[]
useCrocClips?: boolean,
marginWhenBreadboarding?: [number, number, number, number],
} }
export interface FactoryFunctionPinAlloc { export interface FactoryFunctionPinAlloc {
type: "factoryfunction", type: "factoryfunction",
@ -44,9 +47,9 @@ namespace pxsim {
image: string, image: string,
width: number, width: number,
height: number, height: number,
left: number,
top: number,
pinDist: number, pinDist: number,
extraColumnOffset?: number,
firstPin: [number, number],
} }
export interface PartDefinition { export interface PartDefinition {
visual: string | PartVisualDefinition, visual: string | PartVisualDefinition,
@ -99,8 +102,11 @@ namespace pxsim {
groundPins: ["GND"], groundPins: ["GND"],
threeVoltPins: ["+3v3"], threeVoltPins: ["+3v3"],
attachPowerOnRight: true, attachPowerOnRight: true,
onboardComponents: ["buttonpair", "ledmatrix"], onboardComponents: ["buttonpair", "ledmatrix", "speaker"],
useCrocClips: true,
marginWhenBreadboarding: [0, 0, 80, 0],
} }
export const PART_DEFINITIONS: Map<PartDefinition> = { export const PART_DEFINITIONS: Map<PartDefinition> = {
"ledmatrix": { "ledmatrix": {
visual: "ledmatrix", visual: "ledmatrix",
@ -162,9 +168,9 @@ namespace pxsim {
image: "/static/hardware/speaker.svg", image: "/static/hardware/speaker.svg",
width: 500, width: 500,
height: 500, height: 500,
left: -180, firstPin: [180, 135],
top: -135,
pinDist: 70, pinDist: 70,
extraColumnOffset: 1,
}, },
breadboardColumnsNeeded: 5, breadboardColumnsNeeded: 5,
breadboardStartRow: "f", breadboardStartRow: "f",
@ -174,8 +180,8 @@ namespace pxsim {
}, },
assemblyStep: 0, assemblyStep: 0,
wires: [ wires: [
{start: ["breadboard", "j", 1], end: ["GPIO", 0], color: "white", assemblyStep: 1}, {start: ["breadboard", "j", 1], end: ["GPIO", 0], color: "#ff80fa", assemblyStep: 1},
{start: ["breadboard", "j", 3], end: "ground", color: "white", assemblyStep: 1}, {start: ["breadboard", "j", 3], end: "ground", color: "blue", assemblyStep: 1},
], ],
}, },
} }
@ -202,4 +208,7 @@ namespace pxsim {
"ledmatrix": (xy: visuals.Coord) => visuals.mkLedMatrixSvg(xy, 8, 8), "ledmatrix": (xy: visuals.Coord) => visuals.mkLedMatrixSvg(xy, 8, 8),
"neopixel": (xy: visuals.Coord) => visuals.mkNeoPixelPart(xy), "neopixel": (xy: visuals.Coord) => visuals.mkNeoPixelPart(xy),
}; };
//TODO: add multiple board support
export const CURRENT_BOARD = MICROBIT_DEF;
} }

View File

@ -128,10 +128,18 @@ namespace pxsim.instructions {
cmpHeight?: number, cmpHeight?: number,
cmpScale?: number cmpScale?: number
}; };
function mkBoardImgSvg(def: BoardImageDefinition): visuals.SVGElAndSize { function mkBoardImgSvg(def: string | BoardImageDefinition): visuals.SVGElAndSize {
return new visuals.MicrobitBoardSvg({ let boardView: visuals.BoardView;
if (def === "microbit") {
boardView = new visuals.MicrobitBoardSvg({
theme: visuals.randomTheme() theme: visuals.randomTheme()
}).getView(); })
} else {
boardView = new visuals.GenericBoardSvg({
visualDef: <BoardImageDefinition>def
})
}
return boardView.getView();
} }
function mkBBSvg(): visuals.SVGElAndSize { function mkBBSvg(): visuals.SVGElAndSize {
let bb = new visuals.Breadboard({}); let bb = new visuals.Breadboard({});
@ -270,14 +278,18 @@ namespace pxsim.instructions {
div.appendChild(svgEl); div.appendChild(svgEl);
return div; return div;
} }
function mkCmpDiv(type: "wire" | string, opts: mkCmpDivOpts): HTMLElement { function mkCmpDiv(cmp: "wire" | string | PartVisualDefinition, opts: mkCmpDivOpts): HTMLElement {
let el: visuals.SVGElAndSize; let el: visuals.SVGElAndSize;
if (type == "wire") { if (cmp == "wire") {
//TODO: support non-croc wire parts //TODO: support non-croc wire parts
el = visuals.mkWirePart([0, 0], opts.wireClr || "red", true); el = visuals.mkWirePart([0, 0], opts.wireClr || "red", true);
} else { } else if (typeof cmp == "string") {
let cnstr = builtinComponentPartVisual[type]; let builtinVis = <string>cmp;
let cnstr = builtinComponentPartVisual[builtinVis];
el = cnstr([0, 0]); el = cnstr([0, 0]);
} else {
let partVis = <PartVisualDefinition> cmp;
el = visuals.mkGenericPartSVG(partVis);
} }
return wrapSvg(el, opts); return wrapSvg(el, opts);
} }
@ -406,7 +418,8 @@ namespace pxsim.instructions {
if (cmps) { if (cmps) {
cmps.forEach(cmpInst => { cmps.forEach(cmpInst => {
let cmp = board.addComponent(cmpInst) let cmp = board.addComponent(cmpInst)
let rowCol: BBRowCol = [`${cmpInst.breadboardStartRow}`, `${cmpInst.breadboardStartColumn}`]; let colOffset = (<any>cmpInst.visual).breadboardStartColIdx || 0;
let rowCol: BBRowCol = [`${cmpInst.breadboardStartRow}`, `${colOffset + cmpInst.breadboardStartColumn}`];
//last step //last step
if (i === step) { if (i === step) {
board.highlightBreadboardPin(rowCol); board.highlightBreadboardPin(rowCol);
@ -432,7 +445,7 @@ namespace pxsim.instructions {
let panel = mkPanel(); let panel = mkPanel();
// board and breadboard // board and breadboard
let boardImg = mkBoardImgSvg(<BoardImageDefinition>props.boardDef.visual); let boardImg = mkBoardImgSvg(props.boardDef.visual);
let board = wrapSvg(boardImg, {left: QUANT_LBL(1), leftSize: QUANT_LBL_SIZE, cmpScale: PARTS_BOARD_SCALE}); let board = wrapSvg(boardImg, {left: QUANT_LBL(1), leftSize: QUANT_LBL_SIZE, cmpScale: PARTS_BOARD_SCALE});
panel.appendChild(board); panel.appendChild(board);
let bbRaw = mkBBSvg(); let bbRaw = mkBBSvg();
@ -447,18 +460,13 @@ namespace pxsim.instructions {
if (c.visual === "buttonpair") { if (c.visual === "buttonpair") {
quant = 2; quant = 2;
} }
if (typeof c.visual === "string") { let cmp = mkCmpDiv(c.visual, {
let builtinVisual = <string>c.visual;
let cmp = mkCmpDiv(builtinVisual, {
left: QUANT_LBL(quant), left: QUANT_LBL(quant),
leftSize: QUANT_LBL_SIZE, leftSize: QUANT_LBL_SIZE,
cmpScale: PARTS_CMP_SCALE, cmpScale: PARTS_CMP_SCALE,
}); });
addClass(cmp, "partslist-cmp"); addClass(cmp, "partslist-cmp");
panel.appendChild(cmp); panel.appendChild(cmp);
} else {
//TODO: handle generic components
}
}); });
// wires // wires
@ -529,9 +537,7 @@ namespace pxsim.instructions {
} }
locs.forEach((l, i) => { locs.forEach((l, i) => {
let [row, col] = l; let [row, col] = l;
if (typeof c.visual === "string") { let cmp = mkCmpDiv(c.visual, {
let builtinVisual = <string>c.visual;
let cmp = mkCmpDiv(builtinVisual, {
top: `(${row},${col})`, top: `(${row},${col})`,
topSize: LOC_LBL_SIZE, topSize: LOC_LBL_SIZE,
cmpHeight: REQ_CMP_HEIGHT, cmpHeight: REQ_CMP_HEIGHT,
@ -539,9 +545,6 @@ namespace pxsim.instructions {
}) })
addClass(cmp, "cmp-div"); addClass(cmp, "cmp-div");
reqsDiv.appendChild(cmp); reqsDiv.appendChild(cmp);
} else {
//TODO: generic component
}
}); });
}); });
@ -633,7 +636,7 @@ ${tsPackage}
style.textContent += STYLE; style.textContent += STYLE;
const boardDef = MICROBIT_DEF; const boardDef = CURRENT_BOARD;
const cmpDefs = PART_DEFINITIONS; const cmpDefs = PART_DEFINITIONS;
//props //props

View File

@ -142,6 +142,28 @@ namespace pxsim.visuals {
}; };
} }
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};
}
export type Coord = [number, number]; export type Coord = [number, number];
export function findDistSqrd(a: Coord, b: Coord): number { export function findDistSqrd(a: Coord, b: Coord): number {
let x = a[0] - b[0]; let x = a[0] - b[0];

View File

@ -21,32 +21,43 @@ namespace pxsim.visuals {
private style: SVGStyleElement; private style: SVGStyleElement;
private defs: SVGDefsElement; private defs: SVGDefsElement;
private state: DalBoard; private state: DalBoard;
private useCrocClips: boolean;
constructor(opts: BoardHostOpts) { constructor(opts: BoardHostOpts) {
this.state = opts.state; this.state = opts.state;
let onboardCmps = opts.boardDef.onboardComponents || []; let onboardCmps = opts.boardDef.onboardComponents || [];
let activeComponents = (opts.cmpsList || []).filter(c => onboardCmps.indexOf(c) < 0); let activeComponents = (opts.cmpsList || []).filter(c => onboardCmps.indexOf(c) < 0);
activeComponents.sort(); activeComponents.sort();
this.useCrocClips = opts.boardDef.useCrocClips;
if (opts.boardDef.visual === "microbit") {
this.boardView = new visuals.MicrobitBoardSvg({ this.boardView = new visuals.MicrobitBoardSvg({
runtime: runtime, runtime: runtime,
theme: visuals.randomTheme(), theme: visuals.randomTheme(),
disableTilt: false, disableTilt: false,
wireframe: opts.wireframe, wireframe: opts.wireframe,
}); });
} else {
let boardVis = opts.boardDef.visual as BoardImageDefinition;
this.boardView = new visuals.GenericBoardSvg({
visualDef: boardVis,
wireframe: opts.wireframe,
});
}
let useBreadboard = 0 < activeComponents.length || opts.forceBreadboard; let useBreadboard = 0 < activeComponents.length || opts.forceBreadboard;
if (useBreadboard) { if (useBreadboard) {
this.breadboard = new Breadboard({ this.breadboard = new Breadboard({
wireframe: opts.wireframe, wireframe: opts.wireframe,
}); });
let bMarg = opts.boardDef.marginWhenBreadboarding || [0, 0, 40, 0];
let composition = composeSVG({ let composition = composeSVG({
el1: this.boardView.getView(), el1: this.boardView.getView(),
scaleUnit1: this.boardView.getPinDist(), scaleUnit1: this.boardView.getPinDist(),
el2: this.breadboard.getSVGAndSize(), el2: this.breadboard.getSVGAndSize(),
scaleUnit2: this.breadboard.getPinDist(), scaleUnit2: this.breadboard.getPinDist(),
margin: [0, 0, 20, 0], margin: [bMarg[0], bMarg[1], 20, bMarg[3]],
middleMargin: 80, middleMargin: bMarg[2],
maxWidth: opts.maxWidth, maxWidth: opts.maxWidth,
maxHeight: opts.maxHeight, maxHeight: opts.maxHeight,
}); });
@ -138,18 +149,24 @@ namespace pxsim.visuals {
public addComponent(cmpDesc: CmpInst): IBoardComponent<any> { public addComponent(cmpDesc: CmpInst): IBoardComponent<any> {
let cmp: IBoardComponent<any> = null; let cmp: IBoardComponent<any> = null;
let colOffset = 0;
if (typeof cmpDesc.visual === "string") { if (typeof cmpDesc.visual === "string") {
let builtinVisual = cmpDesc.visual as string; let builtinVisual = cmpDesc.visual as string;
let cnstr = builtinComponentSimVisual[builtinVisual]; let cnstr = builtinComponentSimVisual[builtinVisual];
let stateFn = builtinComponentSimState[builtinVisual]; let stateFn = builtinComponentSimState[builtinVisual];
cmp = cnstr(); cmp = cnstr();
cmp.init(this.state.bus, stateFn(this.state), this.view, cmpDesc.microbitPins, cmpDesc.otherArgs); cmp.init(this.state.bus, stateFn(this.state), this.view, cmpDesc.microbitPins, cmpDesc.otherArgs);
} else {
let vis = cmpDesc.visual as PartVisualDefinition;
cmp = new GenericPart(vis);
colOffset = vis.extraColumnOffset || 0;
}
this.components.push(cmp); this.components.push(cmp);
this.view.appendChild(cmp.element); this.view.appendChild(cmp.element);
if (cmp.defs) if (cmp.defs)
cmp.defs.forEach(d => this.defs.appendChild(d)); cmp.defs.forEach(d => this.defs.appendChild(d));
this.style.textContent += cmp.style || ""; this.style.textContent += cmp.style || "";
let rowCol = <BBRowCol>[`${cmpDesc.breadboardStartRow}`, `${cmpDesc.breadboardStartColumn}`]; let rowCol = <BBRowCol>[`${cmpDesc.breadboardStartRow}`, `${colOffset + cmpDesc.breadboardStartColumn}`];
let coord = this.getBBCoord(rowCol); let coord = this.getBBCoord(rowCol);
cmp.moveToCoord(coord); cmp.moveToCoord(coord);
let getCmpClass = (type: string) => `sim-${type}-cmp`; let getCmpClass = (type: string) => `sim-${type}-cmp`;
@ -158,15 +175,10 @@ namespace pxsim.visuals {
svg.addClass(cmp.element, "sim-cmp"); svg.addClass(cmp.element, "sim-cmp");
cmp.updateTheme(); cmp.updateTheme();
cmp.updateState(); cmp.updateState();
} else {
let vis = cmpDesc.visual as PartVisualDefinition;
console.log("TODO PART: " + vis.image);
//TODO: support generic parts
}
return cmp; return cmp;
} }
public addWire(inst: WireInst): Wire { public addWire(inst: WireInst): Wire {
return this.wireFactory.addWire(inst.start, inst.end, inst.color, true); return this.wireFactory.addWire(inst.start, inst.end, inst.color, this.useCrocClips);
} }
public addAll(basicWiresAndCmpsAndWires: AllocatorResult) { public addAll(basicWiresAndCmpsAndWires: AllocatorResult) {
let {powerWires, components} = basicWiresAndCmpsAndWires; let {powerWires, components} = basicWiresAndCmpsAndWires;

View File

@ -3,46 +3,6 @@
/// <reference path="../../libs/microbit/dal.d.ts"/> /// <reference path="../../libs/microbit/dal.d.ts"/>
namespace pxsim.visuals { namespace pxsim.visuals {
const svg = pxsim.svg;
export interface IBoardSvgProps {
runtime: pxsim.Runtime;
boardDef: BoardDefinition;
disableTilt?: boolean;
activeComponents: string[];
fnArgs?: any;
componentDefinitions: Map<PartDefinition>;
}
export const VIEW_WIDTH = 498;
export const VIEW_HEIGHT = 725;
const TOP_MARGIN = 20;
const MID_MARGIN = 40;
const BOT_MARGIN = 20;
const PIN_LBL_SIZE = PIN_DIST * 0.7;
const PIN_LBL_HOVER_SIZE = PIN_LBL_SIZE * 1.5;
const SQUARE_PIN_WIDTH = PIN_DIST * 0.66666;
const SQUARE_PIN_HOVER_WIDTH = PIN_DIST * 0.66666 + PIN_DIST / 3.0;
export type ComputedBoardDimensions = {
scaleFn: (n: number) => number,
height: number,
width: number,
xOff: number,
yOff: number
};
export function getBoardDimensions(vis: BoardImageDefinition): ComputedBoardDimensions {
let scaleFn = (n: number) => n * (PIN_DIST / vis.pinDist);
let width = scaleFn(vis.width);
return {
scaleFn: scaleFn,
height: scaleFn(vis.height),
width: width,
xOff: (VIEW_WIDTH - width) / 2.0,
yOff: TOP_MARGIN
}
}
export const BOARD_SYTLE = ` export const BOARD_SYTLE = `
.noselect { .noselect {
-webkit-touch-callout: none; /* iOS Safari */ -webkit-touch-callout: none; /* iOS Safari */
@ -53,52 +13,6 @@ namespace pxsim.visuals {
user-select: none; /* Non-prefixed version, currently user-select: none; /* Non-prefixed version, currently
not supported by any browser */ not supported by any browser */
} }
svg.sim.grayscale {
-moz-filter: grayscale(1);
-webkit-filter: grayscale(1);
filter: grayscale(1);
}
.sim-text {
font-family:"Lucida Console", Monaco, monospace;
font-size:25px;
fill:#fff;
pointer-events: none;
}
/* animations */
.sim-theme-glow {
animation-name: sim-theme-glow-animation;
animation-timing-function: ease-in-out;
animation-direction: alternate;
animation-iteration-count: infinite;
animation-duration: 1.25s;
}
@keyframes sim-theme-glow-animation {
from { opacity: 1; }
to { opacity: 0.75; }
}
.sim-flash {
animation-name: sim-flash-animation;
animation-duration: 0.1s;
}
@keyframes sim-flash-animation {
from { fill: yellow; }
to { fill: default; }
}
.sim-flash-stroke {
animation-name: sim-flash-stroke-animation;
animation-duration: 0.4s;
animation-timing-function: ease-in;
}
@keyframes sim-flash-stroke-animation {
from { stroke: yellow; }
to { stroke: default; }
}
.sim-board-pin { .sim-board-pin {
fill:#999; fill:#999;
@ -178,202 +92,72 @@ namespace pxsim.visuals {
stroke:#000; stroke:#000;
} }
`; `;
const PIN_LBL_SIZE = PIN_DIST * 0.7;
const PIN_LBL_HOVER_SIZE = PIN_LBL_SIZE * 1.5;
const SQUARE_PIN_WIDTH = PIN_DIST * 0.66666;
const SQUARE_PIN_HOVER_WIDTH = PIN_DIST * 0.66666 + PIN_DIST / 3.0;
export interface GenericBoardProps {
visualDef: BoardImageDefinition;
wireframe?: boolean;
}
let nextBoardId = 0; let nextBoardId = 0;
export class GenericBoardSvg /*TODO: implements BoardView*/ { export class GenericBoardSvg implements BoardView {
public hostElement: SVGSVGElement; private element: SVGSVGElement;
private style: SVGStyleElement; private style: SVGStyleElement;
private defs: SVGDefsElement; private defs: SVGDefsElement;
private g: SVGGElement; private g: SVGGElement;
public board: pxsim.DalBoard; private background: SVGElement;
public background: SVGElement; private width: number;
private components: IBoardComponent<any>[]; private height: number;
public breadboard: Breadboard;
private underboard: SVGGElement;
public boardDef: BoardDefinition;
private boardDim: ComputedBoardDimensions;
public componentDefs: Map<PartDefinition>;
private boardEdges: number[];
private id: number; private id: number;
public bbX: number;
public bbY: number; // pins & labels
private boardTopEdge: number; //(truth)
private boardBotEdge: number;
private wireFactory: WireFactory;
//truth
private allPins: GridPin[] = []; private allPins: GridPin[] = [];
private allLabels: GridLabel[] = []; private allLabels: GridLabel[] = [];
//cache //(cache)
private pinNmToLbl: Map<GridLabel> = {}; private pinNmToLbl: Map<GridLabel> = {};
private pinNmToPin: Map<GridPin> = {}; private pinNmToPin: Map<GridPin> = {};
constructor(public props: IBoardSvgProps) { constructor(public props: GenericBoardProps) {
//TODO: handle wireframe mode
this.id = nextBoardId++; this.id = nextBoardId++;
this.boardDef = props.boardDef; let visDef = props.visualDef;
this.boardDim = getBoardDimensions(<BoardImageDefinition>this.boardDef.visual); let imgHref = props.wireframe ? visDef.outlineImage : visDef.image;
this.board = this.props.runtime.board as pxsim.DalBoard; let boardImgAndSize = mkImageSVG({
this.board.updateView = () => this.updateState(); image: imgHref,
this.hostElement = <SVGSVGElement>svg.elt("svg") width: visDef.width,
svg.hydrate(this.hostElement, { height: visDef.height,
imageUnitDist: visDef.pinDist,
targetUnitDist: PIN_DIST
});
let scaleFn = mkScaleFn(visDef.pinDist, PIN_DIST);
this.width = boardImgAndSize.w;
this.height = boardImgAndSize.h;
let img = boardImgAndSize.el;
this.element = <SVGSVGElement>svg.elt("svg");
svg.hydrate(this.element, {
"version": "1.0", "version": "1.0",
"viewBox": `0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`, "viewBox": `0 0 ${this.width} ${this.height}`,
"enable-background": `new 0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`,
"class": `sim sim-board-id-${this.id}`, "class": `sim sim-board-id-${this.id}`,
"x": "0px", "x": "0px",
"y": "0px" "y": "0px"
}); });
this.style = <SVGStyleElement>svg.child(this.hostElement, "style", {}); if (props.wireframe)
svg.addClass(this.element, "sim-board-outline")
this.style = <SVGStyleElement>svg.child(this.element, "style", {});
this.style.textContent += BOARD_SYTLE; this.style.textContent += BOARD_SYTLE;
this.defs = <SVGDefsElement>svg.child(this.hostElement, "defs", {}); this.defs = <SVGDefsElement>svg.child(this.element, "defs", {});
this.g = <SVGGElement>svg.elt("g"); this.g = <SVGGElement>svg.elt("g");
this.hostElement.appendChild(this.g); this.element.appendChild(this.g);
this.underboard = <SVGGElement>svg.child(this.g, "g", {class: "sim-underboard"});
this.components = [];
this.componentDefs = props.componentDefinitions;
// breadboard
this.breadboard = new Breadboard({})
this.g.appendChild(this.breadboard.bb);
let bbSize = this.breadboard.getSVGAndSize();
let [bbWidth, bbHeight] = [bbSize.w, bbSize.h];
const bbX = (VIEW_WIDTH - bbWidth) / 2;
this.bbX = bbX;
const bbY = TOP_MARGIN + this.boardDim.height + MID_MARGIN;
this.bbY = bbY;
this.breadboard.updateLocation(bbX, bbY);
// edges
this.boardTopEdge = TOP_MARGIN;
this.boardBotEdge = TOP_MARGIN + this.boardDim.height;
this.boardEdges = [this.boardTopEdge, this.boardBotEdge, bbY, bbY + bbHeight]
this.wireFactory = new WireFactory(this.underboard, this.g, this.boardEdges, this.style, this.getLocCoord.bind(this));
this.buildDom();
this.updateTheme();
this.updateState();
let cmps = props.activeComponents;
if (cmps.length) {
let allocRes = allocateDefinitions({
boardDef: this.boardDef,
cmpDefs: this.componentDefs,
fnArgs: this.props.fnArgs,
getBBCoord: this.getBBCoord.bind(this),
cmpList: props.activeComponents,
});
this.addAll(allocRes);
}
}
private getBoardPinCoord(pinNm: string): Coord {
let pin = this.pinNmToPin[pinNm];
if (!pin)
return null;
return [pin.cx, pin.cy];
}
private getBBCoord(rowCol: BBRowCol): Coord {
let bbCoord = this.breadboard.getCoord(rowCol);
if (!bbCoord)
return null;
let [x, y] = bbCoord;
return [x + this.bbX, y + this.bbY];
}
public getLocCoord(loc: Loc): Coord {
let coord: Coord;
if (loc.type === "breadboard") {
let rowCol = (<BBLoc>loc).rowCol;
coord = this.getBBCoord(rowCol);
} else {
let pinNm = (<BoardLoc>loc).pin;
coord = this.getBoardPinCoord(pinNm);
}
if (!coord) {
console.error("Unknown location: " + name)
return [0, 0];
}
return coord;
}
private mkGrayCover(x: number, y: number, w: number, h: number) {
let rect = <SVGRectElement>svg.elt("rect");
svg.hydrate(rect, {x: x, y: y, width: w, height: h, class: "gray-cover"});
return rect;
}
private getCmpClass = (type: string) => `sim-${type}-cmp`;
public addWire(inst: WireInst): Wire {
return this.wireFactory.addWire(inst.start, inst.end, inst.color);
}
public addAll(basicWiresAndCmpsAndWires: AllocatorResult) {
let {powerWires, components} = basicWiresAndCmpsAndWires;
powerWires.forEach(w => this.addWire(w));
components.forEach((cAndWs, idx) => {
let {component, wires} = cAndWs;
wires.forEach(w => this.addWire(w));
this.addComponent(component);
});
}
public addComponent(cmpDesc: CmpInst): IBoardComponent<any> {
let cmp: IBoardComponent<any> = null;
if (typeof cmpDesc.visual === "string") {
let builtinVisual = cmpDesc.visual as string;
let cnstr = builtinComponentSimVisual[builtinVisual];
let stateFn = builtinComponentSimState[builtinVisual];
let state = stateFn(this.board);
cmp = cnstr();
cmp.init(this.board.bus, state, this.hostElement, cmpDesc.microbitPins, cmpDesc.otherArgs);
this.components.push(cmp);
this.g.appendChild(cmp.element);
if (cmp.defs)
cmp.defs.forEach(d => this.defs.appendChild(d));
this.style.textContent += cmp.style || "";
let rowCol = <BBRowCol>[`${cmpDesc.breadboardStartRow}`, `${cmpDesc.breadboardStartColumn}`];
let coord = this.getBBCoord(rowCol);
cmp.moveToCoord(coord);
let cls = this.getCmpClass(name);
svg.addClass(cmp.element, cls);
svg.addClass(cmp.element, "sim-cmp");
cmp.updateTheme();
cmp.updateState();
} else {
//TODO: adding generic components
}
return cmp;
}
private updateTheme() {
this.components.forEach(c => c.updateTheme());
}
public updateState() {
let state = this.board;
if (!state) return;
this.components.forEach(c => c.updateState());
if (!runtime || runtime.dead) svg.addClass(this.hostElement, "grayscale");
else svg.removeClass(this.hostElement, "grayscale");
}
private buildDom() {
// filters
let glow = svg.child(this.defs, "filter", { id: "filterglow", x: "-5%", y: "-5%", width: "120%", height: "120%" });
svg.child(glow, "feGaussianBlur", { stdDeviation: "5", result: "glow" });
let merge = svg.child(glow, "feMerge", {});
for (let i = 0; i < 3; ++i)
svg.child(merge, "feMergeNode", { in: "glow" })
// main board // main board
this.background = svg.child(this.g, "image", this.g.appendChild(img);
{ class: "sim-board", x: this.boardDim.xOff, y: this.boardDim.yOff, width: this.boardDim.width, height: this.boardDim.height, this.background = img;
"href": `${(<BoardImageDefinition>this.boardDef.visual).image}`}); svg.hydrate(img, { class: "sim-board" });
let backgroundCover = this.mkGrayCover(this.boardDim.xOff, this.boardDim.yOff, this.boardDim.width, this.boardDim.height); let backgroundCover = this.mkGrayCover(0, 0, this.width, this.height);
this.g.appendChild(backgroundCover); this.g.appendChild(backgroundCover);
// ----- pins // ----- pins
@ -398,8 +182,8 @@ namespace pxsim.visuals {
return {el: el, w: width, h: width, x: 0, y: 0}; return {el: el, w: width, h: width, x: 0, y: 0};
} }
const mkPinBlockGrid = (pinBlock: PinBlockDefinition, blockIdx: number) => { const mkPinBlockGrid = (pinBlock: PinBlockDefinition, blockIdx: number) => {
let xOffset = this.boardDim.xOff + this.boardDim.scaleFn(pinBlock.x) + PIN_DIST / 2.0; let xOffset = scaleFn(pinBlock.x) + PIN_DIST / 2.0;
let yOffset = this.boardDim.yOff + this.boardDim.scaleFn(pinBlock.y) + PIN_DIST / 2.0; let yOffset = scaleFn(pinBlock.y) + PIN_DIST / 2.0;
let rowCount = 1; let rowCount = 1;
let colCount = pinBlock.labels.length; let colCount = pinBlock.labels.length;
let getColName = (colIdx: number) => pinBlock.labels[colIdx]; let getColName = (colIdx: number) => pinBlock.labels[colIdx];
@ -422,9 +206,11 @@ namespace pxsim.visuals {
svg.addClass(gridRes.g, "sim-board-pin-group"); svg.addClass(gridRes.g, "sim-board-pin-group");
return gridRes; return gridRes;
}; };
let pinBlocks = (<BoardImageDefinition>this.boardDef.visual).pinBlocks.map(mkPinBlockGrid); let pinBlocks = visDef.pinBlocks.map(mkPinBlockGrid);
pinBlocks.forEach(blk => blk.allPins.forEach(p => { let pinToBlockDef: PinBlockDefinition[] = [];
pinBlocks.forEach((blk, blkIdx) => blk.allPins.forEach((p, pIdx) => {
this.allPins.push(p); this.allPins.push(p);
pinToBlockDef.push(visDef.pinBlocks[blkIdx]);
})); }));
//tooltip //tooltip
this.allPins.forEach(p => { this.allPins.forEach(p => {
@ -443,15 +229,12 @@ namespace pxsim.visuals {
}); });
// ----- labels // ----- labels
const mkLabelTxtEl = (pinX: number, pinY: number, size: number, txt: string): SVGTextElement => { const mkLabelTxtEl = (pinX: number, pinY: number, size: number, txt: string, pos: "above" | "below"): SVGTextElement => {
//TODO: extract constants //TODO: extract constants
let lblY: number; let lblY: number;
let lblX: number; let lblX: number;
let edges = [this.boardTopEdge, this.boardBotEdge];
let distFromTopBot = edges.map(e => Math.abs(e - pinY)); if (pos === "below") {
let closestEdgeIdx = distFromTopBot.reduce((pi, n, ni) => n < distFromTopBot[pi] ? ni : pi, 0);
let topEdge = closestEdgeIdx == 0;
if (topEdge) {
let lblLen = size * 0.25 * txt.length; let lblLen = size * 0.25 * txt.length;
lblX = pinX; lblX = pinX;
lblY = pinY + 12 + lblLen; lblY = pinY + 12 + lblLen;
@ -463,16 +246,17 @@ namespace pxsim.visuals {
let el = mkTxt(lblX, lblY, size, -90, txt); let el = mkTxt(lblX, lblY, size, -90, txt);
return el; return el;
}; };
const mkLabel = (pinX: number, pinY: number, txt: string): GridLabel => { const mkLabel = (pinX: number, pinY: number, txt: string, pos: "above" | "below"): GridLabel => {
let el = mkLabelTxtEl(pinX, pinY, PIN_LBL_SIZE, txt); let el = mkLabelTxtEl(pinX, pinY, PIN_LBL_SIZE, txt, pos);
svg.addClass(el, "sim-board-pin-lbl"); svg.addClass(el, "sim-board-pin-lbl");
let hoverEl = mkLabelTxtEl(pinX, pinY, PIN_LBL_HOVER_SIZE, txt); let hoverEl = mkLabelTxtEl(pinX, pinY, PIN_LBL_HOVER_SIZE, txt, pos);
svg.addClass(hoverEl, "sim-board-pin-lbl-hover"); svg.addClass(hoverEl, "sim-board-pin-lbl-hover");
let label: GridLabel = {el: el, hoverEl: hoverEl, txt: txt}; let label: GridLabel = {el: el, hoverEl: hoverEl, txt: txt};
return label; return label;
} }
this.allLabels = this.allPins.map(p => { this.allLabels = this.allPins.map((p, pIdx) => {
return mkLabel(p.cx, p.cy, p.col); let blk = pinToBlockDef[pIdx];
return mkLabel(p.cx, p.cy, p.col, blk.labelPosition);
}); });
//attach labels //attach labels
this.allLabels.forEach(l => { this.allLabels.forEach(l => {
@ -486,7 +270,29 @@ namespace pxsim.visuals {
}); });
} }
public highlightLoc(pinNm: string) { public getCoord(pinNm: string): Coord {
let pin = this.pinNmToPin[pinNm];
if (!pin)
return null;
return [pin.cx, pin.cy];
}
private mkGrayCover(x: number, y: number, w: number, h: number) {
let rect = <SVGRectElement>svg.elt("rect");
svg.hydrate(rect, {x: x, y: y, width: w, height: h, class: "gray-cover"});
return rect;
}
public getView(): SVGAndSize<SVGSVGElement> {
return {el: this.element, w: this.width, h: this.height, x: 0, y: 0};
}
public getPinDist() {
return PIN_DIST;
}
public highlightPin(pinNm: string) {
let lbl = this.pinNmToLbl[pinNm]; let lbl = this.pinNmToLbl[pinNm];
let pin = this.pinNmToPin[pinNm]; let pin = this.pinNmToPin[pinNm];
if (lbl && pin) { if (lbl && pin) {
@ -496,20 +302,5 @@ namespace pxsim.visuals {
svg.addClass(pin.hoverEl, "highlight"); svg.addClass(pin.hoverEl, "highlight");
} }
} }
public highlightWire(wire: Wire) {
//underboard wires
wire.wires.forEach(e => {
(<any>e).style["visibility"] = "visible";
});
//un greyed out
[wire.end1, wire.end2].forEach(e => {
svg.addClass(e, "highlight");
});
wire.wires.forEach(e => {
svg.addClass(e, "highlight");
});
}
} }
} }

View File

@ -1,16 +1,40 @@
namespace pxsim.visuals { namespace pxsim.visuals {
export class GenericComponentView implements IBoardComponent<any> { export function mkGenericPartSVG(partVisual: PartVisualDefinition): SVGAndSize<SVGImageElement> {
public style: string; let imgAndSize = mkImageSVG({
image: partVisual.image,
width: partVisual.width,
height: partVisual.height,
imageUnitDist: partVisual.pinDist,
targetUnitDist: PIN_DIST
});
return imgAndSize;
}
export class GenericPart implements IBoardComponent<any> {
public style: string = "";
public element: SVGElement; public element: SVGElement;
defs: SVGElement[]; defs: SVGElement[] = [];
init(bus: EventBus, state: any, svgEl: SVGSVGElement, gpioPins: string[], otherArgs: string[]): void {
constructor(partVisual: PartVisualDefinition) {
let imgAndSize = mkGenericPartSVG(partVisual);
let img = imgAndSize.el;
let scaleFn = mkScaleFn(partVisual.pinDist, PIN_DIST);
let [pinX, pinY] = partVisual.firstPin;
let left = -scaleFn(pinX);
let top = -scaleFn(pinY);
translateEl(img, [left, top]); // So that 0,0 is on the first pin
this.element = svg.elt("g");
this.element.appendChild(img);
} }
moveToCoord(xy: Coord): void { moveToCoord(xy: Coord): void {
translateEl(this.element, xy);
} }
updateState(): void {
} //unused
updateTheme(): void { init(bus: EventBus, state: any, svgEl: SVGSVGElement, gpioPins: string[], otherArgs: string[]): void { }
} updateState(): void { }
updateTheme(): void { }
} }
} }

View File

@ -92,8 +92,7 @@ namespace pxsim.visuals {
} }
public moveToCoord(xy: Coord) { public moveToCoord(xy: Coord) {
let [x, y] = xy; translateEl(this.element, xy);
translateEl(this.element, [x, y]);
} }
public updateTheme() { public updateTheme() {