pxt-calliope/sim/visuals/genericboard.ts
2016-08-31 18:03:34 -07:00

306 lines
11 KiB
TypeScript

/// <reference path="../../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
/// <reference path="../../libs/microbit/dal.d.ts"/>
namespace pxsim.visuals {
export const BOARD_SYTLE = `
.noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Chrome/Safari/Opera */
-khtml-user-select: none; /* Konqueror */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
not supported by any browser */
}
.sim-board-pin {
fill:#999;
stroke:#000;
stroke-width:${PIN_DIST / 3.0}px;
}
.sim-board-pin-lbl {
fill: #333;
}
.gray-cover {
fill:#FFF;
opacity: 0.7;
stroke-width:0;
visibility: hidden;
}
.sim-board-pin-hover {
visibility: hidden;
pointer-events: all;
stroke-width:${PIN_DIST / 6.0}px;
}
.sim-board-pin-hover:hover {
visibility: visible;
}
.sim-board-pin-lbl {
visibility: hidden;
}
.sim-board-outline .sim-board-pin-lbl {
visibility: visible;
}
.sim-board-pin-lbl {
fill: #555;
}
.sim-board-pin-lbl-hover {
fill: red;
}
.sim-board-outline .sim-board-pin-lbl-hover {
fill: black;
}
.sim-board-pin-lbl,
.sim-board-pin-lbl-hover {
font-family:"Lucida Console", Monaco, monospace;
pointer-events: all;
stroke-width: 0;
}
.sim-board-pin-lbl-hover {
visibility: hidden;
}
.sim-board-outline .sim-board-pin-hover:hover + .sim-board-pin-lbl,
.sim-board-pin-lbl.highlight {
visibility: hidden;
}
.sim-board-outline .sim-board-pin-hover:hover + * + .sim-board-pin-lbl-hover,
.sim-board-pin-lbl-hover.highlight {
visibility: visible;
}
/* Graying out */
.grayed .sim-board-pin-lbl:not(.highlight) {
fill: #AAA;
}
.grayed .sim-board-pin:not(.highlight) {
fill:#BBB;
stroke:#777;
}
.grayed .gray-cover {
visibility: inherit;
}
.grayed .sim-cmp:not(.notgrayed) {
opacity: 0.3;
}
/* Highlighting */
.sim-board-pin-lbl.highlight {
fill: #000;
font-weight: bold;
}
.sim-board-pin.highlight {
fill:#999;
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;
export class GenericBoardSvg implements BoardView {
private element: SVGSVGElement;
private style: SVGStyleElement;
private defs: SVGDefsElement;
private g: SVGGElement;
private background: SVGElement;
private width: number;
private height: number;
private id: number;
// pins & labels
//(truth)
private allPins: GridPin[] = [];
private allLabels: GridLabel[] = [];
//(cache)
private pinNmToLbl: Map<GridLabel> = {};
private pinNmToPin: Map<GridPin> = {};
constructor(public props: GenericBoardProps) {
//TODO: handle wireframe mode
this.id = nextBoardId++;
let visDef = props.visualDef;
let imgHref = props.wireframe ? visDef.outlineImage : visDef.image;
let boardImgAndSize = mkImageSVG({
image: imgHref,
width: visDef.width,
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",
"viewBox": `0 0 ${this.width} ${this.height}`,
"class": `sim sim-board-id-${this.id}`,
"x": "0px",
"y": "0px"
});
if (props.wireframe)
svg.addClass(this.element, "sim-board-outline")
this.style = <SVGStyleElement>svg.child(this.element, "style", {});
this.style.textContent += BOARD_SYTLE;
this.defs = <SVGDefsElement>svg.child(this.element, "defs", {});
this.g = <SVGGElement>svg.elt("g");
this.element.appendChild(this.g);
// main board
this.g.appendChild(img);
this.background = img;
svg.hydrate(img, { class: "sim-board" });
let backgroundCover = this.mkGrayCover(0, 0, this.width, this.height);
this.g.appendChild(backgroundCover);
// ----- pins
const mkSquarePin = (): SVGElAndSize => {
let el = svg.elt("rect");
let width = SQUARE_PIN_WIDTH;
svg.hydrate(el, {
class: "sim-board-pin",
width: width,
height: width,
});
return {el: el, w: width, h: width, x: 0, y: 0};
}
const mkSquareHoverPin = (): SVGElAndSize => {
let el = svg.elt("rect");
let width = SQUARE_PIN_HOVER_WIDTH;
svg.hydrate(el, {
class: "sim-board-pin-hover",
width: width,
height: width
});
return {el: el, w: width, h: width, x: 0, y: 0};
}
const mkPinBlockGrid = (pinBlock: PinBlockDefinition, blockIdx: number) => {
let xOffset = scaleFn(pinBlock.x) + PIN_DIST / 2.0;
let yOffset = scaleFn(pinBlock.y) + PIN_DIST / 2.0;
let rowCount = 1;
let colCount = pinBlock.labels.length;
let getColName = (colIdx: number) => pinBlock.labels[colIdx];
let getRowName = () => `${blockIdx + 1}`
let getGroupName = () => pinBlock.labels.join(" ");
let gridRes = mkGrid({
xOffset: xOffset,
yOffset: yOffset,
rowCount: rowCount,
colCount: colCount,
pinDist: PIN_DIST,
mkPin: mkSquarePin,
mkHoverPin: mkSquareHoverPin,
getRowName: getRowName,
getColName: getColName,
getGroupName: getGroupName,
});
let pins = gridRes.allPins;
let pinsG = gridRes.g;
svg.addClass(gridRes.g, "sim-board-pin-group");
return gridRes;
};
let pinBlocks = visDef.pinBlocks.map(mkPinBlockGrid);
let pinToBlockDef: PinBlockDefinition[] = [];
pinBlocks.forEach((blk, blkIdx) => blk.allPins.forEach((p, pIdx) => {
this.allPins.push(p);
pinToBlockDef.push(visDef.pinBlocks[blkIdx]);
}));
//tooltip
this.allPins.forEach(p => {
let tooltip = p.col;
svg.hydrate(p.el, {title: tooltip});
svg.hydrate(p.hoverEl, {title: tooltip});
});
//attach pins
this.allPins.forEach(p => {
this.g.appendChild(p.el);
this.g.appendChild(p.hoverEl);
});
//catalog pins
this.allPins.forEach(p => {
this.pinNmToPin[p.col] = p;
});
// ----- labels
const mkLabelTxtEl = (pinX: number, pinY: number, size: number, txt: string, pos: "above" | "below"): SVGTextElement => {
//TODO: extract constants
let lblY: number;
let lblX: number;
if (pos === "below") {
let lblLen = size * 0.25 * txt.length;
lblX = pinX;
lblY = pinY + 12 + lblLen;
} else {
let lblLen = size * 0.32 * txt.length;
lblX = pinX;
lblY = pinY - 11 - lblLen;
}
let el = mkTxt(lblX, lblY, size, -90, txt);
return el;
};
const mkLabel = (pinX: number, pinY: number, txt: string, pos: "above" | "below"): GridLabel => {
let el = mkLabelTxtEl(pinX, pinY, PIN_LBL_SIZE, txt, pos);
svg.addClass(el, "sim-board-pin-lbl");
let hoverEl = mkLabelTxtEl(pinX, pinY, PIN_LBL_HOVER_SIZE, txt, pos);
svg.addClass(hoverEl, "sim-board-pin-lbl-hover");
let label: GridLabel = {el: el, hoverEl: hoverEl, txt: txt};
return label;
}
this.allLabels = this.allPins.map((p, pIdx) => {
let blk = pinToBlockDef[pIdx];
return mkLabel(p.cx, p.cy, p.col, blk.labelPosition);
});
//attach labels
this.allLabels.forEach(l => {
this.g.appendChild(l.el);
this.g.appendChild(l.hoverEl);
});
//catalog labels
this.allPins.forEach((pin, pinIdx) => {
let lbl = this.allLabels[pinIdx];
this.pinNmToLbl[pin.col] = lbl;
});
}
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 pin = this.pinNmToPin[pinNm];
if (lbl && pin) {
svg.addClass(lbl.el, "highlight");
svg.addClass(lbl.hoverEl, "highlight");
svg.addClass(pin.el, "highlight");
svg.addClass(pin.hoverEl, "highlight");
}
}
}
}