306 lines
11 KiB
TypeScript
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");
|
|
}
|
|
}
|
|
}
|
|
} |