moves all of pxt-arduino breadboarding here...
... see pxt-arduino history starting here: acd49bb795
This commit is contained in:
640
sim/visuals/breadboard.ts
Normal file
640
sim/visuals/breadboard.ts
Normal file
@ -0,0 +1,640 @@
|
||||
namespace pxsim.visuals {
|
||||
// The distance between the center of two pins. This is the constant on which everything else is based.
|
||||
const PIN_DIST = 15;
|
||||
// CSS styling for the breadboard
|
||||
const BLUE = "#1AA5D7";
|
||||
const RED = "#DD4BA0";
|
||||
const BREADBOARD_CSS = `
|
||||
/* bread board */
|
||||
.sim-bb-background {
|
||||
fill:#E0E0E0;
|
||||
}
|
||||
.sim-bb-pin {
|
||||
fill:#999;
|
||||
}
|
||||
.sim-bb-pin-hover {
|
||||
visibility: hidden;
|
||||
pointer-events: all;
|
||||
stroke-width: ${PIN_DIST / 2}px;
|
||||
stroke: transparent;
|
||||
fill: #777;
|
||||
}
|
||||
.sim-bb-pin-hover:hover {
|
||||
visibility: visible;
|
||||
fill:#444;
|
||||
}
|
||||
.sim-bb-group-wire {
|
||||
stroke: #999;
|
||||
stroke-width: ${PIN_DIST / 4}px;
|
||||
visibility: hidden;
|
||||
}
|
||||
.sim-bb-pin-group {
|
||||
pointer-events: all;
|
||||
}
|
||||
.sim-bb-label,
|
||||
.sim-bb-label-hover {
|
||||
font-family:"Lucida Console", Monaco, monospace;
|
||||
fill:#555;
|
||||
pointer-events: all;
|
||||
stroke-width: 0;
|
||||
}
|
||||
.sim-bb-label-hover {
|
||||
visibility: hidden;
|
||||
fill:#000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.sim-bb-bar {
|
||||
stroke-width: 0;
|
||||
}
|
||||
.sim-bb-blue {
|
||||
fill:${BLUE};
|
||||
stroke:${BLUE}
|
||||
}
|
||||
.sim-bb-red {
|
||||
fill:${RED};
|
||||
stroke:${RED};
|
||||
}
|
||||
.sim-bb-pin-group:hover .sim-bb-pin-hover,
|
||||
.sim-bb-pin-group:hover .sim-bb-group-wire,
|
||||
.sim-bb-pin-group:hover .sim-bb-label-hover {
|
||||
visibility: visible;
|
||||
}
|
||||
.sim-bb-pin-group:hover .sim-bb-label {
|
||||
visibility: hidden;
|
||||
}
|
||||
/* outline mode */
|
||||
.sim-bb-outline .sim-bb-background {
|
||||
stroke-width: ${PIN_DIST / 7}px;
|
||||
fill: #FFF;
|
||||
stroke: #000;
|
||||
}
|
||||
.sim-bb-outline .sim-bb-mid-channel {
|
||||
fill: #FFF;
|
||||
stroke: #888;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
/* grayed out */
|
||||
.grayed .sim-bb-red,
|
||||
.grayed .sim-bb-blue {
|
||||
fill: #BBB;
|
||||
}
|
||||
.grayed .sim-bb-pin {
|
||||
fill: #BBB;
|
||||
}
|
||||
.grayed .sim-bb-label {
|
||||
fill: #BBB;
|
||||
}
|
||||
.grayed .sim-bb-background {
|
||||
stroke: #BBB;
|
||||
}
|
||||
.grayed .sim-bb-group-wire {
|
||||
stroke: #DDD;
|
||||
}
|
||||
/* highlighted */
|
||||
.sim-bb-label.highlight {
|
||||
visibility: hidden;
|
||||
}
|
||||
.sim-bb-label-hover.highlight {
|
||||
visibility: visible;
|
||||
}
|
||||
.sim-bb-blue.highlight {
|
||||
fill:${BLUE};
|
||||
}
|
||||
.sim-bb-red.highlight {
|
||||
fill:${RED};
|
||||
}
|
||||
`
|
||||
// Pin rows and coluns
|
||||
const MID_ROWS = 10;
|
||||
const MID_ROW_GAPS = [4, 4];
|
||||
const MID_ROW_AND_GAPS = MID_ROWS + MID_ROW_GAPS.length;
|
||||
const MID_COLS = 30;
|
||||
const BAR_ROWS = 2;
|
||||
const BAR_COLS = 25;
|
||||
const POWER_ROWS = BAR_ROWS * 2;
|
||||
const POWER_COLS = BAR_COLS * 2;
|
||||
const BAR_COL_GAPS = [4, 9, 14, 19];
|
||||
const BAR_COL_AND_GAPS = BAR_COLS + BAR_COL_GAPS.length;
|
||||
// Essential dimensions
|
||||
const WIDTH = PIN_DIST * (MID_COLS + 3);
|
||||
const HEIGHT = PIN_DIST * (MID_ROW_AND_GAPS + POWER_ROWS + 5.5);
|
||||
const MID_RATIO = 2.0 / 3.0;
|
||||
const BAR_RATIO = (1.0 - MID_RATIO) * 0.5;
|
||||
const MID_HEIGHT = HEIGHT * MID_RATIO;
|
||||
const BAR_HEIGHT = HEIGHT * BAR_RATIO;
|
||||
// Pin grids
|
||||
const MID_GRID_WIDTH = (MID_COLS - 1) * PIN_DIST;
|
||||
const MID_GRID_HEIGHT = (MID_ROW_AND_GAPS - 1) * PIN_DIST;
|
||||
const MID_GRID_X = (WIDTH - MID_GRID_WIDTH) / 2.0;
|
||||
const MID_GRID_Y = BAR_HEIGHT + (MID_HEIGHT - MID_GRID_HEIGHT) / 2.0;
|
||||
const BAR_GRID_HEIGHT = (BAR_ROWS - 1) * PIN_DIST;
|
||||
const BAR_GRID_WIDTH = (BAR_COL_AND_GAPS - 1) * PIN_DIST;
|
||||
const BAR_TOP_GRID_X = (WIDTH - BAR_GRID_WIDTH) / 2.0;
|
||||
const BAR_TOP_GRID_Y = (BAR_HEIGHT - BAR_GRID_HEIGHT) / 2.0;
|
||||
const BAR_BOT_GRID_X = BAR_TOP_GRID_X;
|
||||
const BAR_BOT_GRID_Y = BAR_TOP_GRID_Y + BAR_HEIGHT + MID_HEIGHT;
|
||||
// Individual pins
|
||||
const PIN_HOVER_SCALAR = 1.3;
|
||||
const PIN_WIDTH = PIN_DIST / 2.5;
|
||||
const PIN_ROUNDING = PIN_DIST / 7.5;
|
||||
// Labels
|
||||
const PIN_LBL_SIZE = PIN_DIST * 0.7;
|
||||
const PIN_LBL_HOVER_SCALAR = 1.3;
|
||||
const PLUS_LBL_SIZE = PIN_DIST * 1.7;
|
||||
const MINUS_LBL_SIZE = PIN_DIST * 2;
|
||||
const POWER_LBL_OFFSET = PIN_DIST * 0.8;
|
||||
const MINUS_LBL_EXTRA_OFFSET = PIN_DIST * 0.07;
|
||||
const LBL_ROTATION = -90;
|
||||
// Channels
|
||||
const CHANNEL_HEIGHT = PIN_DIST * 1.0;
|
||||
const SMALL_CHANNEL_HEIGHT = PIN_DIST * 0.05;
|
||||
// Background
|
||||
const BACKGROUND_ROUNDING = PIN_DIST * 0.3;
|
||||
|
||||
export interface GridPin {
|
||||
el: SVGElement,
|
||||
hoverEl: SVGElement,
|
||||
cx: number,
|
||||
cy: number,
|
||||
row: string,
|
||||
col: string,
|
||||
group?: string
|
||||
};
|
||||
export interface GridOptions {
|
||||
xOffset?: number,
|
||||
yOffset?: number,
|
||||
rowCount: number,
|
||||
colCount: number,
|
||||
rowStartIdx?: number,
|
||||
colStartIdx?: number,
|
||||
pinDist: number,
|
||||
mkPin: () => SVGElAndSize,
|
||||
mkHoverPin: () => SVGElAndSize,
|
||||
getRowName: (rowIdx: number) => string,
|
||||
getColName: (colIdx: number) => string,
|
||||
getGroupName?: (rowIdx: number, colIdx: number) => string,
|
||||
rowIdxsWithGap?: number[],
|
||||
colIdxsWithGap?: number[],
|
||||
};
|
||||
export interface GridResult {
|
||||
g: SVGGElement,
|
||||
allPins: GridPin[],
|
||||
}
|
||||
export function mkGrid(opts: GridOptions): GridResult {
|
||||
let xOff = opts.xOffset || 0;
|
||||
let yOff = opts.yOffset || 0;
|
||||
let allPins: GridPin[] = [];
|
||||
let grid = <SVGGElement>svg.elt("g");
|
||||
let colIdxOffset = opts.colStartIdx || 0;
|
||||
let rowIdxOffset = opts.rowStartIdx || 0;
|
||||
let copyArr = <T>(arr: T[]): T[] => arr ? arr.slice(0, arr.length) : [];
|
||||
let removeAll = <T>(arr: T[], e: T): number => {
|
||||
let res = 0;
|
||||
let idx: number;
|
||||
while (0 <= (idx = arr.indexOf(e))) {
|
||||
arr.splice(idx, 1);
|
||||
res += 1;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
let rowGaps = 0;
|
||||
let rowIdxsWithGap = copyArr(opts.rowIdxsWithGap)
|
||||
for (let i = 0; i < opts.rowCount; i++) {
|
||||
let colGaps = 0;
|
||||
let colIdxsWithGap = copyArr(opts.colIdxsWithGap)
|
||||
let cy = yOff + i * opts.pinDist + rowGaps * opts.pinDist;
|
||||
let rowIdx = i + rowIdxOffset;
|
||||
for (let j = 0; j < opts.colCount; j++) {
|
||||
let cx = xOff + j * opts.pinDist + colGaps * opts.pinDist;
|
||||
let colIdx = j + colIdxOffset;
|
||||
const addEl = (pin: SVGElAndSize) => {
|
||||
let pinX = cx - pin.w * 0.5;
|
||||
let pinY = cy - pin.h * 0.5;
|
||||
svg.hydrate(pin.el, {x: pinX, y: pinY});
|
||||
grid.appendChild(pin.el);
|
||||
return pin.el;
|
||||
}
|
||||
let el = addEl(opts.mkPin());
|
||||
let hoverEl = addEl(opts.mkHoverPin());
|
||||
let row = opts.getRowName(rowIdx);
|
||||
let col = opts.getColName(colIdx);
|
||||
let group = opts.getGroupName ? opts.getGroupName(rowIdx, colIdx) : null;
|
||||
let gridPin: GridPin = {el: el, hoverEl: hoverEl, cx: cx, cy: cy, row: row, col: col, group: group};
|
||||
allPins.push(gridPin);
|
||||
//column gaps
|
||||
colGaps += removeAll(colIdxsWithGap, colIdx);
|
||||
}
|
||||
//row gaps
|
||||
rowGaps += removeAll(rowIdxsWithGap, rowIdx);
|
||||
}
|
||||
return {g: grid, allPins: allPins};
|
||||
}
|
||||
function mkBBPin(): SVGElAndSize {
|
||||
let el = svg.elt("rect");
|
||||
let width = PIN_WIDTH;
|
||||
svg.hydrate(el, {
|
||||
class: "sim-bb-pin",
|
||||
rx: PIN_ROUNDING,
|
||||
ry: PIN_ROUNDING,
|
||||
width: width,
|
||||
height: width
|
||||
});
|
||||
return {el: el, w: width, h: width, x: 0, y: 0};
|
||||
}
|
||||
function mkBBHoverPin(): SVGElAndSize {
|
||||
let el = svg.elt("rect");
|
||||
let width = PIN_WIDTH * PIN_HOVER_SCALAR;
|
||||
svg.hydrate(el, {
|
||||
class: "sim-bb-pin-hover",
|
||||
rx: PIN_ROUNDING,
|
||||
ry: PIN_ROUNDING,
|
||||
width: width,
|
||||
height: width,
|
||||
});
|
||||
return {el: el, w: width, h: width, x: 0, y: 0};
|
||||
}
|
||||
export interface GridLabel {
|
||||
el: SVGTextElement,
|
||||
hoverEl: SVGTextElement,
|
||||
txt: string,
|
||||
group?: string,
|
||||
};
|
||||
function mkBBLabel(cx: number, cy: number, size: number, rotation: number, txt: string, group: string, extraClasses?: string[]): GridLabel {
|
||||
//lbl
|
||||
let el = mkTxt(cx, cy, size, rotation, txt);
|
||||
svg.addClass(el, "sim-bb-label");
|
||||
if (extraClasses)
|
||||
extraClasses.forEach(c => svg.addClass(el, c));
|
||||
|
||||
//hover lbl
|
||||
let hoverEl = mkTxt(cx, cy, size * PIN_LBL_HOVER_SCALAR, rotation, txt);
|
||||
svg.addClass(hoverEl, "sim-bb-label-hover");
|
||||
if (extraClasses)
|
||||
extraClasses.forEach(c => svg.addClass(hoverEl, c));
|
||||
|
||||
let lbl = {el: el, hoverEl: hoverEl, txt: txt, group: group};
|
||||
return lbl;
|
||||
}
|
||||
interface BBBar {
|
||||
el: SVGRectElement,
|
||||
group?: string
|
||||
};
|
||||
export class Breadboard {
|
||||
public bb: SVGSVGElement;
|
||||
private styleEl: SVGStyleElement;
|
||||
private defs: SVGDefsElement;
|
||||
|
||||
//truth
|
||||
private allPins: GridPin[] = [];
|
||||
private allLabels: GridLabel[] = [];
|
||||
private allPowerBars: BBBar[] = [];
|
||||
//quick lookup caches
|
||||
private rowColToPin: Map<Map<GridPin>> = {};
|
||||
private rowColToLbls: Map<Map<GridLabel[]>> = {};
|
||||
|
||||
constructor() {
|
||||
this.buildDom();
|
||||
}
|
||||
|
||||
public updateLocation(x: number, y: number) {
|
||||
svg.hydrate(this.bb, {
|
||||
x: `${x}px`,
|
||||
y: `${y}px`,
|
||||
});
|
||||
}
|
||||
|
||||
public getPin(row: string, col: string): GridPin {
|
||||
let colToPin = this.rowColToPin[row];
|
||||
if (!colToPin)
|
||||
return null;
|
||||
let pin = colToPin[col];
|
||||
if (!pin)
|
||||
return null;
|
||||
return pin;
|
||||
}
|
||||
public getCoord(rowCol: BBRowCol): Coord {
|
||||
let [row, col] = rowCol;
|
||||
let pin = this.getPin(row, col);
|
||||
if (!pin)
|
||||
return null;
|
||||
return [pin.cx, pin.cy];
|
||||
}
|
||||
|
||||
public getPinDist() {
|
||||
return PIN_DIST;
|
||||
}
|
||||
|
||||
private buildDom() {
|
||||
this.bb = <SVGSVGElement>svg.elt("svg", {
|
||||
"version": "1.0",
|
||||
"viewBox": `0 0 ${WIDTH} ${HEIGHT}`,
|
||||
"class": `sim-bb`,
|
||||
"width": WIDTH + "px",
|
||||
"height": HEIGHT + "px",
|
||||
});
|
||||
this.styleEl = <SVGStyleElement>svg.child(this.bb, "style", {});
|
||||
this.styleEl.textContent += BREADBOARD_CSS;
|
||||
this.defs = <SVGDefsElement>svg.child(this.bb, "defs", {});
|
||||
|
||||
//background
|
||||
svg.child(this.bb, "rect", { class: "sim-bb-background", width: WIDTH, height: HEIGHT, rx: BACKGROUND_ROUNDING, ry: BACKGROUND_ROUNDING});
|
||||
|
||||
//mid channel
|
||||
let channelGid = "sim-bb-channel-grad";
|
||||
let channelGrad = <SVGLinearGradientElement>svg.elt("linearGradient")
|
||||
svg.hydrate(channelGrad, { id: channelGid, x1: "0%", y1: "0%", x2: "0%", y2: "100%" });
|
||||
this.defs.appendChild(channelGrad);
|
||||
let channelDark = "#AAA";
|
||||
let channelLight = "#CCC";
|
||||
let stop1 = svg.child(channelGrad, "stop", { offset: "0%", style: `stop-color: ${channelDark};` })
|
||||
let stop2 = svg.child(channelGrad, "stop", { offset: "20%", style: `stop-color: ${channelLight};` })
|
||||
let stop3 = svg.child(channelGrad, "stop", { offset: "80%", style: `stop-color: ${channelLight};` })
|
||||
let stop4 = svg.child(channelGrad, "stop", { offset: "100%", style: `stop-color: ${channelDark};` })
|
||||
|
||||
const mkChannel = (cy: number, h: number, cls?: string) => {
|
||||
let channel = svg.child(this.bb, "rect", { class: `sim-bb-channel ${cls || ""}`, y: cy - h / 2, width: WIDTH, height: h});
|
||||
channel.setAttribute("fill", `url(#${channelGid})`);
|
||||
return channel;
|
||||
}
|
||||
|
||||
mkChannel(BAR_HEIGHT + MID_HEIGHT / 2, CHANNEL_HEIGHT, "sim-bb-mid-channel");
|
||||
mkChannel(BAR_HEIGHT, SMALL_CHANNEL_HEIGHT);
|
||||
mkChannel(BAR_HEIGHT + MID_HEIGHT, SMALL_CHANNEL_HEIGHT);
|
||||
|
||||
//-----pins
|
||||
const getMidTopOrBot = (rowIdx: number) => rowIdx < MID_ROWS / 2.0 ? "b" : "t";
|
||||
const getBarTopOrBot = (colIdx: number) => colIdx < POWER_COLS / 2.0 ? "b" : "t";
|
||||
const alphabet = "abcdefghij".split("").reverse();
|
||||
const getColName = (colIdx: number) => `${colIdx + 1}`;
|
||||
const getMidRowName = (rowIdx: number) => alphabet[rowIdx];
|
||||
const getMidGroupName = (rowIdx: number, colIdx: number) => {
|
||||
let botOrTop = getMidTopOrBot(rowIdx);
|
||||
let colNm = getColName(colIdx);
|
||||
return `${botOrTop}${colNm}`;
|
||||
};
|
||||
const getBarRowName = (rowIdx: number) => rowIdx === 0 ? "-" : "+";
|
||||
const getBarGroupName = (rowIdx: number, colIdx: number) => {
|
||||
let botOrTop = getBarTopOrBot(colIdx);
|
||||
let rowName = getBarRowName(rowIdx);
|
||||
return `${rowName}${botOrTop}`;
|
||||
};
|
||||
|
||||
//mid grid
|
||||
let midGridRes = mkGrid({
|
||||
xOffset: MID_GRID_X,
|
||||
yOffset: MID_GRID_Y,
|
||||
rowCount: MID_ROWS,
|
||||
colCount: MID_COLS,
|
||||
pinDist: PIN_DIST,
|
||||
mkPin: mkBBPin,
|
||||
mkHoverPin: mkBBHoverPin,
|
||||
getRowName: getMidRowName,
|
||||
getColName: getColName,
|
||||
getGroupName: getMidGroupName,
|
||||
rowIdxsWithGap: MID_ROW_GAPS,
|
||||
});
|
||||
let midGridG = midGridRes.g;
|
||||
this.allPins = this.allPins.concat(midGridRes.allPins);
|
||||
|
||||
//bot bar
|
||||
let botBarGridRes = mkGrid({
|
||||
xOffset: BAR_BOT_GRID_X,
|
||||
yOffset: BAR_BOT_GRID_Y,
|
||||
rowCount: BAR_ROWS,
|
||||
colCount: BAR_COLS,
|
||||
pinDist: PIN_DIST,
|
||||
mkPin: mkBBPin,
|
||||
mkHoverPin: mkBBHoverPin,
|
||||
getRowName: getBarRowName,
|
||||
getColName: getColName,
|
||||
getGroupName: getBarGroupName,
|
||||
colIdxsWithGap: BAR_COL_GAPS,
|
||||
});
|
||||
let botBarGridG = botBarGridRes.g;
|
||||
this.allPins = this.allPins.concat(botBarGridRes.allPins);
|
||||
|
||||
//top bar
|
||||
let topBarGridRes = mkGrid({
|
||||
xOffset: BAR_TOP_GRID_X,
|
||||
yOffset: BAR_TOP_GRID_Y,
|
||||
rowCount: BAR_ROWS,
|
||||
colCount: BAR_COLS,
|
||||
colStartIdx: BAR_COLS,
|
||||
pinDist: PIN_DIST,
|
||||
mkPin: mkBBPin,
|
||||
mkHoverPin: mkBBHoverPin,
|
||||
getRowName: getBarRowName,
|
||||
getColName: getColName,
|
||||
getGroupName: getBarGroupName,
|
||||
colIdxsWithGap: BAR_COL_GAPS.map(g => g + BAR_COLS),
|
||||
});
|
||||
let topBarGridG = topBarGridRes.g;
|
||||
this.allPins = this.allPins.concat(topBarGridRes.allPins);
|
||||
|
||||
//tooltip
|
||||
this.allPins.forEach(pin => {
|
||||
let {el, row, col, hoverEl} = pin
|
||||
let title = `(${row},${col})`;
|
||||
svg.hydrate(el, {title: title});
|
||||
svg.hydrate(hoverEl, {title: title});
|
||||
})
|
||||
|
||||
//catalog pins
|
||||
this.allPins.forEach(pin => {
|
||||
let colToPin = this.rowColToPin[pin.row];
|
||||
if (!colToPin)
|
||||
colToPin = this.rowColToPin[pin.row] = {};
|
||||
colToPin[pin.col] = pin;
|
||||
})
|
||||
|
||||
//-----labels
|
||||
const mkBBLabelAtPin = (row: string, col: string, xOffset: number, yOffset: number, txt: string, group?: string): GridLabel => {
|
||||
let size = PIN_LBL_SIZE;
|
||||
let rotation = LBL_ROTATION;
|
||||
let loc = this.getCoord([row, col]);
|
||||
let [cx, cy] = loc;
|
||||
let t = mkBBLabel(cx + xOffset, cy + yOffset, size, rotation, txt, group);
|
||||
return t;
|
||||
}
|
||||
|
||||
//columns
|
||||
for (let colIdx = 0; colIdx < MID_COLS; colIdx++) {
|
||||
let colNm = getColName(colIdx);
|
||||
//top
|
||||
let rowTIdx = 0;
|
||||
let rowTNm = getMidRowName(rowTIdx);
|
||||
let groupT = getMidGroupName(rowTIdx, colIdx);
|
||||
let lblT = mkBBLabelAtPin(rowTNm, colNm, 0, -PIN_DIST, colNm, groupT);
|
||||
this.allLabels.push(lblT);
|
||||
//bottom
|
||||
let rowBIdx = MID_ROWS - 1;
|
||||
let rowBNm = getMidRowName(rowBIdx);
|
||||
let groupB = getMidGroupName(rowBIdx, colIdx);
|
||||
let lblB = mkBBLabelAtPin(rowBNm, colNm, 0, +PIN_DIST, colNm, groupB);
|
||||
this.allLabels.push(lblB);
|
||||
}
|
||||
//rows
|
||||
for (let rowIdx = 0; rowIdx < MID_ROWS; rowIdx++) {
|
||||
let rowNm = getMidRowName(rowIdx);
|
||||
//top
|
||||
let colTIdx = 0;
|
||||
let colTNm = getColName(colTIdx);
|
||||
let lblT = mkBBLabelAtPin(rowNm, colTNm, -PIN_DIST, 0, rowNm);
|
||||
this.allLabels.push(lblT);
|
||||
//top
|
||||
let colBIdx = MID_COLS - 1;
|
||||
let colBNm = getColName(colBIdx);
|
||||
let lblB = mkBBLabelAtPin(rowNm, colBNm, +PIN_DIST, 0, rowNm);
|
||||
this.allLabels.push(lblB);
|
||||
}
|
||||
|
||||
//+- labels
|
||||
let botPowerLabels = [
|
||||
//BL
|
||||
mkBBLabel(0 + POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, BAR_HEIGHT + MID_HEIGHT + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, 0), [`sim-bb-blue`]),
|
||||
mkBBLabel(0 + POWER_LBL_OFFSET, BAR_HEIGHT + MID_HEIGHT + BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, 0), [`sim-bb-red`]),
|
||||
//BR
|
||||
mkBBLabel(WIDTH - POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, BAR_HEIGHT + MID_HEIGHT + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, BAR_COLS - 1), [`sim-bb-blue`]),
|
||||
mkBBLabel(WIDTH - POWER_LBL_OFFSET, BAR_HEIGHT + MID_HEIGHT + BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, BAR_COLS - 1), [`sim-bb-red`]),
|
||||
];
|
||||
this.allLabels = this.allLabels.concat(botPowerLabels);
|
||||
let topPowerLabels = [
|
||||
//TL
|
||||
mkBBLabel(0 + POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, 0 + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, BAR_COLS), [`sim-bb-blue`]),
|
||||
mkBBLabel(0 + POWER_LBL_OFFSET, BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, BAR_COLS), [`sim-bb-red`]),
|
||||
//TR
|
||||
mkBBLabel(WIDTH - POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, 0 + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, POWER_COLS - 1), [`sim-bb-blue`]),
|
||||
mkBBLabel(WIDTH - POWER_LBL_OFFSET, BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, POWER_COLS - 1), [`sim-bb-red`]),
|
||||
];
|
||||
this.allLabels = this.allLabels.concat(topPowerLabels);
|
||||
|
||||
//catalog lbls
|
||||
let lblNmToLbls: Map<GridLabel[]> = {};
|
||||
this.allLabels.forEach(lbl => {
|
||||
let {el, txt} = lbl;
|
||||
let lbls = lblNmToLbls[txt] = lblNmToLbls[txt] || []
|
||||
lbls.push(lbl);
|
||||
});
|
||||
const isPowerPin = (pin: GridPin) => pin.row === "-" || pin.row === "+";
|
||||
this.allPins.forEach(pin => {
|
||||
let {row, col, group} = pin;
|
||||
let colToLbls = this.rowColToLbls[row] || (this.rowColToLbls[row] = {});
|
||||
let lbls = colToLbls[col] || (colToLbls[col] = []);
|
||||
if (isPowerPin(pin)) {
|
||||
//power pins
|
||||
let isBot = Number(col) <= BAR_COLS;
|
||||
if (isBot)
|
||||
botPowerLabels.filter(l => l.group == pin.group).forEach(l => lbls.push(l));
|
||||
else
|
||||
topPowerLabels.filter(l => l.group == pin.group).forEach(l => lbls.push(l));
|
||||
} else {
|
||||
//mid pins
|
||||
let rowLbls = lblNmToLbls[row];
|
||||
rowLbls.forEach(l => lbls.push(l));
|
||||
let colLbls = lblNmToLbls[col];
|
||||
colLbls.forEach(l => lbls.push(l));
|
||||
}
|
||||
})
|
||||
|
||||
//-----blue & red lines
|
||||
const lnLen = BAR_GRID_WIDTH + PIN_DIST * 1.5;
|
||||
const lnThickness = PIN_DIST / 5.0;
|
||||
const lnYOff = PIN_DIST * 0.6;
|
||||
const lnXOff = (lnLen - BAR_GRID_WIDTH) / 2.0;
|
||||
const mkPowerLine = (x: number, y: number, group: string, cls: string): BBBar => {
|
||||
let ln = <SVGRectElement>svg.elt("rect");
|
||||
svg.hydrate(ln, {
|
||||
class: `sim-bb-bar ${cls}`,
|
||||
x: x,
|
||||
y: y - lnThickness / 2.0,
|
||||
width: lnLen,
|
||||
height: lnThickness});
|
||||
let bar: BBBar = {el: ln, group: group};
|
||||
return bar;
|
||||
}
|
||||
let barLines = [
|
||||
//top
|
||||
mkPowerLine(BAR_BOT_GRID_X - lnXOff, BAR_BOT_GRID_Y - lnYOff, getBarGroupName(0, POWER_COLS - 1), "sim-bb-blue"),
|
||||
mkPowerLine(BAR_BOT_GRID_X - lnXOff, BAR_BOT_GRID_Y + PIN_DIST + lnYOff, getBarGroupName(1, POWER_COLS - 1), "sim-bb-red"),
|
||||
//bot
|
||||
mkPowerLine(BAR_TOP_GRID_X - lnXOff, BAR_TOP_GRID_Y - lnYOff, getBarGroupName(0, 0), "sim-bb-blue"),
|
||||
mkPowerLine(BAR_TOP_GRID_X - lnXOff, BAR_TOP_GRID_Y + PIN_DIST + lnYOff, getBarGroupName(1, 0), "sim-bb-red"),
|
||||
];
|
||||
this.allPowerBars = this.allPowerBars.concat(barLines);
|
||||
//attach power bars
|
||||
this.allPowerBars.forEach(b => this.bb.appendChild(b.el));
|
||||
|
||||
//-----electrically connected groups
|
||||
//make groups
|
||||
let allGrpNms = this.allPins.map(p => p.group).filter((g, i, a) => a.indexOf(g) == i);
|
||||
let groups: SVGGElement[] = allGrpNms.map(grpNm => {
|
||||
let g = <SVGGElement>svg.elt("g");
|
||||
return g;
|
||||
});
|
||||
groups.forEach(g => svg.addClass(g, "sim-bb-pin-group"));
|
||||
groups.forEach((g, i) => svg.addClass(g, `group-${allGrpNms[i]}`));
|
||||
let grpNmToGroup: Map<SVGGElement> = {};
|
||||
allGrpNms.forEach((g, i) => grpNmToGroup[g] = groups[i]);
|
||||
//group pins and add connecting wire
|
||||
let grpNmToPins: Map<GridPin[]> = {};
|
||||
this.allPins.forEach((p, i) => {
|
||||
let g = p.group;
|
||||
let pins = grpNmToPins[g] || (grpNmToPins[g] = []);
|
||||
pins.push(p);
|
||||
});
|
||||
//connecting wire
|
||||
allGrpNms.forEach(grpNm => {
|
||||
let pins = grpNmToPins[grpNm];
|
||||
let [xs, ys] = [pins.map(p => p.cx), pins.map(p => p.cy)];
|
||||
let minFn = (arr: number[]) => arr.reduce((a, b) => a < b ? a : b);
|
||||
let maxFn = (arr: number[]) => arr.reduce((a, b) => a > b ? a : b);
|
||||
let [minX, maxX, minY, maxY] = [minFn(xs), maxFn(xs), minFn(ys), maxFn(ys)];
|
||||
let wire = svg.elt("rect");
|
||||
let width = Math.max(maxX - minX, 0.0001/*rects with no width aren't displayed*/);
|
||||
let height = Math.max(maxY - minY, 0.0001);
|
||||
svg.hydrate(wire, {x: minX, y: minY, width: width, height: height});
|
||||
svg.addClass(wire, "sim-bb-group-wire")
|
||||
let g = grpNmToGroup[grpNm];
|
||||
g.appendChild(wire);
|
||||
});
|
||||
//group pins
|
||||
this.allPins.forEach(p => {
|
||||
let g = grpNmToGroup[p.group];
|
||||
g.appendChild(p.el);
|
||||
g.appendChild(p.hoverEl);
|
||||
})
|
||||
//group lbls
|
||||
let miscLblGroup = <SVGGElement>svg.elt("g");
|
||||
svg.hydrate(miscLblGroup, {class: "sim-bb-group-misc"});
|
||||
groups.push(miscLblGroup);
|
||||
this.allLabels.forEach(l => {
|
||||
if (l.group) {
|
||||
let g = grpNmToGroup[l.group];
|
||||
g.appendChild(l.el);
|
||||
g.appendChild(l.hoverEl);
|
||||
} else {
|
||||
miscLblGroup.appendChild(l.el);
|
||||
miscLblGroup.appendChild(l.hoverEl);
|
||||
}
|
||||
})
|
||||
|
||||
//attach to bb
|
||||
groups.forEach(g => this.bb.appendChild(g)); //attach to breadboard
|
||||
}
|
||||
|
||||
public getSVGAndSize(): SVGAndSize<SVGSVGElement> {
|
||||
return {el: this.bb, y: 0, x: 0, w: WIDTH, h: HEIGHT};
|
||||
}
|
||||
|
||||
public highlightLoc(row: string, col: string) {
|
||||
let pin = this.rowColToPin[row][col];
|
||||
let {cx, cy} = pin;
|
||||
let lbls = this.rowColToLbls[row][col];
|
||||
const highlightLbl = (lbl: GridLabel) => {
|
||||
svg.addClass(lbl.el, "highlight");
|
||||
svg.addClass(lbl.hoverEl, "highlight");
|
||||
};
|
||||
lbls.forEach(highlightLbl);
|
||||
}
|
||||
}
|
||||
}
|
204
sim/visuals/buttonpair.ts
Normal file
204
sim/visuals/buttonpair.ts
Normal file
@ -0,0 +1,204 @@
|
||||
/// <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 function mkBtnSvg(xy: Coord): SVGAndSize<SVGGElement> {
|
||||
let [innerCls, outerCls] = ["sim-button", "sim-button-outer"];
|
||||
const tabSize = PIN_DIST / 2.5;
|
||||
const pegR = PIN_DIST / 5;
|
||||
const btnR = PIN_DIST * .8;
|
||||
const pegMargin = PIN_DIST / 8;
|
||||
const plateR = PIN_DIST / 12;
|
||||
|
||||
const pegOffset = pegMargin + pegR;
|
||||
let [x, y] = xy;
|
||||
const left = x - tabSize / 2;
|
||||
const top = y - tabSize / 2;
|
||||
const plateH = 3 * PIN_DIST - tabSize;
|
||||
const plateW = 2 * PIN_DIST + tabSize;
|
||||
const plateL = left;
|
||||
const plateT = top + tabSize;
|
||||
const btnCX = plateL + plateW / 2;
|
||||
const btnCY = plateT + plateH / 2;
|
||||
|
||||
let btng = <SVGGElement>svg.elt("g");
|
||||
//tabs
|
||||
const mkTab = (x: number, y: number) => {
|
||||
svg.child(btng, "rect", { class: "sim-button-tab", x: x, y: y, width: tabSize, height: tabSize})
|
||||
}
|
||||
mkTab(left, top);
|
||||
mkTab(left + 2 * PIN_DIST, top);
|
||||
mkTab(left, top + 3 * PIN_DIST);
|
||||
mkTab(left + 2 * PIN_DIST, top + 3 * PIN_DIST);
|
||||
|
||||
//plate
|
||||
svg.child(btng, "rect", { class: outerCls, x: plateL, y: plateT, rx: plateR, ry: plateR, width: plateW, height: plateH });
|
||||
|
||||
//pegs
|
||||
const mkPeg = (x: number, y: number) => {
|
||||
svg.child(btng, "circle", { class: "sim-button-nut", cx: x, cy: y, r: pegR });
|
||||
}
|
||||
mkPeg(plateL + pegOffset, plateT + pegOffset)
|
||||
mkPeg(plateL + plateW - pegOffset, plateT + pegOffset)
|
||||
mkPeg(plateL + pegOffset, plateT + plateH - pegOffset)
|
||||
mkPeg(plateL + plateW - pegOffset, plateT + plateH - pegOffset)
|
||||
|
||||
//inner btn
|
||||
let innerBtn = svg.child(btng, "circle", { class: innerCls, cx: btnCX, cy: btnCY, r: btnR });
|
||||
|
||||
//return
|
||||
return { el: btng, y: top, x: left, w: plateW, h: plateH + 2 * tabSize };
|
||||
}
|
||||
export const BUTTON_PAIR_STYLE = `
|
||||
.sim-button {
|
||||
pointer-events: none;
|
||||
fill: #000;
|
||||
}
|
||||
.sim-button-outer:active ~ .sim-button,
|
||||
.sim-button-virtual:active {
|
||||
fill: #FFA500;
|
||||
}
|
||||
.sim-button-outer {
|
||||
cursor: pointer;
|
||||
fill: #979797;
|
||||
}
|
||||
.sim-button-outer:hover {
|
||||
stroke:gray;
|
||||
stroke-width: ${PIN_DIST / 5}px;
|
||||
}
|
||||
.sim-button-nut {
|
||||
fill:#000;
|
||||
pointer-events:none;
|
||||
}
|
||||
.sim-button-nut:hover {
|
||||
stroke:${PIN_DIST / 15}px solid #704A4A;
|
||||
}
|
||||
.sim-button-tab {
|
||||
fill:#FFF;
|
||||
pointer-events:none;
|
||||
}
|
||||
.sim-button-virtual {
|
||||
cursor: pointer;
|
||||
fill: rgba(255, 255, 255, 0.6);
|
||||
stroke: rgba(255, 255, 255, 1);
|
||||
stroke-width: ${PIN_DIST / 5}px;
|
||||
}
|
||||
.sim-button-virtual:hover {
|
||||
stroke: rgba(128, 128, 128, 1);
|
||||
}
|
||||
.sim-text-virtual {
|
||||
fill: #000;
|
||||
pointer-events:none;
|
||||
}
|
||||
`;
|
||||
export class ButtonPairView implements IBoardComponent<ButtonPairState> {
|
||||
public element: SVGElement;
|
||||
public defs: SVGElement[];
|
||||
public style = BUTTON_PAIR_STYLE;
|
||||
private state: ButtonPairState;
|
||||
private bus: EventBus;
|
||||
private aBtn: SVGGElement;
|
||||
private bBtn: SVGGElement;
|
||||
private abBtn: SVGGElement;
|
||||
|
||||
public init(bus: EventBus, state: ButtonPairState) {
|
||||
this.state = state;
|
||||
this.bus = bus;
|
||||
this.defs = [];
|
||||
this.element = this.mkBtns();
|
||||
this.updateState();
|
||||
this.attachEvents();
|
||||
}
|
||||
|
||||
public moveToCoord(xy: Coord) {
|
||||
let btnWidth = PIN_DIST * 3;
|
||||
let [x, y] = xy;
|
||||
translateEl(this.aBtn, [x, y])
|
||||
translateEl(this.bBtn, [x + btnWidth, y])
|
||||
translateEl(this.abBtn, [x + PIN_DIST * 1.5, y + PIN_DIST * 4])
|
||||
}
|
||||
|
||||
public updateState() {
|
||||
let stateBtns = [this.state.aBtn, this.state.bBtn, this.state.abBtn];
|
||||
let svgBtns = [this.aBtn, this.bBtn, this.abBtn];
|
||||
|
||||
if (this.state.usesButtonAB && this.abBtn.style.visibility != "visible") {
|
||||
this.abBtn.style.visibility = "visible";
|
||||
}
|
||||
}
|
||||
|
||||
public updateTheme() {}
|
||||
|
||||
private mkBtns() {
|
||||
this.aBtn = mkBtnSvg([0, 0]).el;
|
||||
this.bBtn = mkBtnSvg([0, 0]).el;
|
||||
|
||||
const mkVirtualBtn = () => {
|
||||
const numPins = 2;
|
||||
const w = PIN_DIST * 2.8;
|
||||
const offset = (w - (numPins * PIN_DIST)) / 2;
|
||||
const corner = PIN_DIST / 2;
|
||||
const cx = 0 - offset + w / 2;
|
||||
const cy = cx;
|
||||
const txtSize = PIN_DIST * 1.3;
|
||||
const x = -offset;
|
||||
const y = -offset;
|
||||
const txtXOff = PIN_DIST / 7;
|
||||
const txtYOff = PIN_DIST / 10;
|
||||
|
||||
let btng = <SVGGElement>svg.elt("g");
|
||||
let btn = svg.child(btng, "rect", { class: "sim-button-virtual", x: x, y: y, rx: corner, ry: corner, width: w, height: w});
|
||||
let btnTxt = mkTxt(cx + txtXOff, cy + txtYOff, txtSize, 0, "A+B");
|
||||
svg.addClass(btnTxt, "sim-text")
|
||||
svg.addClass(btnTxt, "sim-text-virtual");
|
||||
btng.appendChild(btnTxt);
|
||||
|
||||
return btng;
|
||||
}
|
||||
|
||||
this.abBtn = mkVirtualBtn();
|
||||
this.abBtn.style.visibility = "hidden";
|
||||
|
||||
let el = svg.elt("g");
|
||||
svg.addClass(el, "sim-buttonpair")
|
||||
el.appendChild(this.aBtn);
|
||||
el.appendChild(this.bBtn);
|
||||
el.appendChild(this.abBtn);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
private attachEvents() {
|
||||
let btnStates = [this.state.aBtn, this.state.bBtn];
|
||||
let btnSvgs = [this.aBtn, this.bBtn];
|
||||
btnSvgs.forEach((btn, index) => {
|
||||
btn.addEventListener(pointerEvents.down, ev => {
|
||||
btnStates[index].pressed = true;
|
||||
})
|
||||
btn.addEventListener(pointerEvents.leave, ev => {
|
||||
btnStates[index].pressed = false;
|
||||
})
|
||||
btn.addEventListener(pointerEvents.up, ev => {
|
||||
btnStates[index].pressed = false;
|
||||
this.bus.queue(btnStates[index].id, DAL.MICROBIT_BUTTON_EVT_UP);
|
||||
this.bus.queue(btnStates[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK);
|
||||
})
|
||||
})
|
||||
let updateBtns = (s: boolean) => {
|
||||
btnStates.forEach(b => b.pressed = s)
|
||||
};
|
||||
this.abBtn.addEventListener(pointerEvents.down, ev => {
|
||||
updateBtns(true);
|
||||
})
|
||||
this.abBtn.addEventListener(pointerEvents.leave, ev => {
|
||||
updateBtns(false);
|
||||
})
|
||||
this.abBtn.addEventListener(pointerEvents.up, ev => {
|
||||
updateBtns(false);
|
||||
this.bus.queue(this.state.abBtn.id, DAL.MICROBIT_BUTTON_EVT_UP);
|
||||
this.bus.queue(this.state.abBtn.id, DAL.MICROBIT_BUTTON_EVT_CLICK);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
515
sim/visuals/genericboard.ts
Normal file
515
sim/visuals/genericboard.ts
Normal file
@ -0,0 +1,515 @@
|
||||
/// <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 {
|
||||
const svg = pxsim.svg;
|
||||
|
||||
export interface IBoardSvgProps {
|
||||
runtime: pxsim.Runtime;
|
||||
boardDef: BoardDefinition;
|
||||
disableTilt?: boolean;
|
||||
activeComponents: string[];
|
||||
fnArgs?: any;
|
||||
componentDefinitions: Map<ComponentDefinition>;
|
||||
}
|
||||
|
||||
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 = `
|
||||
.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 */
|
||||
}
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
let nextBoardId = 0;
|
||||
export class GenericBoardSvg {
|
||||
public hostElement: SVGSVGElement;
|
||||
private style: SVGStyleElement;
|
||||
private defs: SVGDefsElement;
|
||||
private g: SVGGElement;
|
||||
public board: pxsim.DalBoard;
|
||||
public background: SVGElement;
|
||||
private components: IBoardComponent<any>[];
|
||||
public breadboard: Breadboard;
|
||||
private underboard: SVGGElement;
|
||||
public boardDef: BoardDefinition;
|
||||
private boardDim: ComputedBoardDimensions;
|
||||
public componentDefs: Map<ComponentDefinition>;
|
||||
private boardEdges: number[];
|
||||
private id: number;
|
||||
public bbX: number;
|
||||
public bbY: number;
|
||||
private boardTopEdge: number;
|
||||
private boardBotEdge: number;
|
||||
private wireFactory: WireFactory;
|
||||
//truth
|
||||
private allPins: GridPin[] = [];
|
||||
private allLabels: GridLabel[] = [];
|
||||
//cache
|
||||
private pinNmToLbl: Map<GridLabel> = {};
|
||||
private pinNmToPin: Map<GridPin> = {};
|
||||
|
||||
constructor(public props: IBoardSvgProps) {
|
||||
this.id = nextBoardId++;
|
||||
this.boardDef = props.boardDef;
|
||||
this.boardDim = getBoardDimensions(<BoardImageDefinition>this.boardDef.visual);
|
||||
this.board = this.props.runtime.board as pxsim.DalBoard;
|
||||
this.board.updateView = () => this.updateState();
|
||||
this.hostElement = <SVGSVGElement>svg.elt("svg")
|
||||
svg.hydrate(this.hostElement, {
|
||||
"version": "1.0",
|
||||
"viewBox": `0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`,
|
||||
"enable-background": `new 0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`,
|
||||
"class": `sim sim-board-id-${this.id}`,
|
||||
"x": "0px",
|
||||
"y": "0px"
|
||||
});
|
||||
this.style = <SVGStyleElement>svg.child(this.hostElement, "style", {});
|
||||
this.style.textContent += BOARD_SYTLE;
|
||||
this.defs = <SVGDefsElement>svg.child(this.hostElement, "defs", {});
|
||||
this.g = <SVGGElement>svg.elt("g");
|
||||
this.hostElement.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
|
||||
this.background = svg.child(this.g, "image",
|
||||
{ class: "sim-board", x: this.boardDim.xOff, y: this.boardDim.yOff, width: this.boardDim.width, height: this.boardDim.height,
|
||||
"href": `${(<BoardImageDefinition>this.boardDef.visual).image}`});
|
||||
let backgroundCover = this.mkGrayCover(this.boardDim.xOff, this.boardDim.yOff, this.boardDim.width, this.boardDim.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 = this.boardDim.xOff + this.boardDim.scaleFn(pinBlock.x) + PIN_DIST / 2.0;
|
||||
let yOffset = this.boardDim.yOff + this.boardDim.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 = (<BoardImageDefinition>this.boardDef.visual).pinBlocks.map(mkPinBlockGrid);
|
||||
pinBlocks.forEach(blk => blk.allPins.forEach(p => {
|
||||
this.allPins.push(p);
|
||||
}));
|
||||
//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): SVGTextElement => {
|
||||
//TODO: extract constants
|
||||
let lblY: number;
|
||||
let lblX: number;
|
||||
let edges = [this.boardTopEdge, this.boardBotEdge];
|
||||
let distFromTopBot = edges.map(e => Math.abs(e - pinY));
|
||||
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;
|
||||
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): GridLabel => {
|
||||
let el = mkLabelTxtEl(pinX, pinY, PIN_LBL_SIZE, txt);
|
||||
svg.addClass(el, "sim-board-pin-lbl");
|
||||
let hoverEl = mkLabelTxtEl(pinX, pinY, PIN_LBL_HOVER_SIZE, txt);
|
||||
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 => {
|
||||
return mkLabel(p.cx, p.cy, p.col);
|
||||
});
|
||||
//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 highlightLoc(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");
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
16
sim/visuals/genericpart.ts
Normal file
16
sim/visuals/genericpart.ts
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
namespace pxsim.visuals {
|
||||
export class GenericComponentView implements IBoardComponent<any> {
|
||||
public style: string;
|
||||
public element: SVGElement;
|
||||
defs: SVGElement[];
|
||||
init(bus: EventBus, state: any, svgEl: SVGSVGElement, gpioPins: string[], otherArgs: string[]): void {
|
||||
}
|
||||
moveToCoord(xy: Coord): void {
|
||||
}
|
||||
updateState(): void {
|
||||
}
|
||||
updateTheme(): void {
|
||||
}
|
||||
}
|
||||
}
|
130
sim/visuals/ledmatrix.ts
Normal file
130
sim/visuals/ledmatrix.ts
Normal file
@ -0,0 +1,130 @@
|
||||
/// <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 function mkLedMatrixSvg(xy: Coord, rows: number, cols: number):
|
||||
{el: SVGGElement, y: number, x: number, w: number, h: number, leds: SVGElement[], ledsOuter: SVGElement[], background: SVGElement} {
|
||||
let result: {el: SVGGElement, y: number, x: number, w: number, h: number, leds: SVGElement[], ledsOuter: SVGElement[], background: SVGElement}
|
||||
= {el: null, y: 0, x: 0, w: 0, h: 0, leds: [], ledsOuter: [], background: null};
|
||||
result.el = <SVGGElement>svg.elt("g");
|
||||
let width = cols * PIN_DIST;
|
||||
let height = rows * PIN_DIST;
|
||||
let ledRad = Math.round(PIN_DIST * .35);
|
||||
let spacing = PIN_DIST;
|
||||
let padding = (spacing - 2 * ledRad) / 2.0;
|
||||
let [x, y] = xy;
|
||||
let left = x - (ledRad + padding);
|
||||
let top = y - (ledRad + padding);
|
||||
result.x = left;
|
||||
result.y = top;
|
||||
result.w = width;
|
||||
result.h = height;
|
||||
result.background = svg.child(result.el, "rect", {class: "sim-display", x: left, y: top, width: width, height: height})
|
||||
|
||||
// ledsOuter
|
||||
result.leds = [];
|
||||
result.ledsOuter = [];
|
||||
let hoverRad = ledRad * 1.2;
|
||||
for (let i = 0; i < rows; ++i) {
|
||||
let y = top + ledRad + i * spacing + padding;
|
||||
for (let j = 0; j < cols; ++j) {
|
||||
let x = left + ledRad + j * spacing + padding;
|
||||
result.ledsOuter.push(svg.child(result.el, "circle", { class: "sim-led-back", cx: x, cy: y, r: ledRad }));
|
||||
result.leds.push(svg.child(result.el, "circle", { class: "sim-led", cx: x, cy: y, r: hoverRad, title: `(${j},${i})` }));
|
||||
}
|
||||
}
|
||||
|
||||
//default theme
|
||||
svg.fill(result.background, defaultLedMatrixTheme.background);
|
||||
svg.fills(result.leds, defaultLedMatrixTheme.ledOn);
|
||||
svg.fills(result.ledsOuter, defaultLedMatrixTheme.ledOff);
|
||||
|
||||
//turn off LEDs
|
||||
result.leds.forEach(l => (<SVGStylable><any>l).style.opacity = 0 + "");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface ILedMatrixTheme {
|
||||
background?: string;
|
||||
ledOn?: string;
|
||||
ledOff?: string;
|
||||
}
|
||||
export var defaultLedMatrixTheme: ILedMatrixTheme = {
|
||||
background: "#000",
|
||||
ledOn: "#ff5f5f",
|
||||
ledOff: "#DDD",
|
||||
};
|
||||
|
||||
export const LED_MATRIX_STYLE = `
|
||||
.sim-led-back:hover {
|
||||
stroke:#a0a0a0;
|
||||
stroke-width:3px;
|
||||
}
|
||||
.sim-led:hover {
|
||||
stroke:#ff7f7f;
|
||||
stroke-width:3px;
|
||||
}
|
||||
`
|
||||
|
||||
export class LedMatrixView implements IBoardComponent<LedMatrixState> {
|
||||
private background: SVGElement;
|
||||
private ledsOuter: SVGElement[];
|
||||
private leds: SVGElement[];
|
||||
private state: LedMatrixState;
|
||||
private bus: EventBus;
|
||||
public element: SVGElement;
|
||||
public defs: SVGElement[];
|
||||
private theme: ILedMatrixTheme;
|
||||
|
||||
private DRAW_SIZE = 8;
|
||||
private ACTIVE_SIZE = 5;
|
||||
|
||||
public style = LED_MATRIX_STYLE;
|
||||
|
||||
public init(bus: EventBus, state: LedMatrixState) {
|
||||
this.bus = bus;
|
||||
this.state = state;
|
||||
this.theme = defaultLedMatrixTheme;
|
||||
this.defs = [];
|
||||
this.element = this.buildDom();
|
||||
}
|
||||
|
||||
public moveToCoord(xy: Coord) {
|
||||
let [x, y] = xy;
|
||||
translateEl(this.element, [x, y]);
|
||||
}
|
||||
|
||||
public updateTheme() {
|
||||
svg.fill(this.background, this.theme.background);
|
||||
svg.fills(this.leds, this.theme.ledOn);
|
||||
svg.fills(this.ledsOuter, this.theme.ledOff);
|
||||
}
|
||||
|
||||
public updateState() {
|
||||
let bw = this.state.displayMode == pxsim.DisplayMode.bw
|
||||
let img = this.state.image;
|
||||
this.leds.forEach((led, i) => {
|
||||
let sel = (<SVGStylable><any>led)
|
||||
let dx = i % this.DRAW_SIZE;
|
||||
let dy = (i - dx) / this.DRAW_SIZE;
|
||||
if (dx < this.ACTIVE_SIZE && dy < this.ACTIVE_SIZE) {
|
||||
let j = dx + dy * this.ACTIVE_SIZE;
|
||||
sel.style.opacity = ((bw ? img.data[j] > 0 ? 255 : 0 : img.data[j]) / 255.0) + "";
|
||||
} else {
|
||||
sel.style.opacity = 0 + "";
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
let res = mkLedMatrixSvg([0, 0], this.DRAW_SIZE, this.DRAW_SIZE);
|
||||
let display = res.el;
|
||||
this.background = res.background;
|
||||
this.leds = res.leds;
|
||||
this.ledsOuter = res.ledsOuter;
|
||||
return display;
|
||||
}
|
||||
}
|
||||
}
|
256
sim/visuals/neopixel.ts
Normal file
256
sim/visuals/neopixel.ts
Normal file
@ -0,0 +1,256 @@
|
||||
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
||||
/// <reference path="../../libs/microbit/dal.d.ts"/>
|
||||
/// <reference path="../../libs/microbit/shims.d.ts"/>
|
||||
/// <reference path="../../libs/microbit/enums.d.ts"/>
|
||||
/// <reference path="../state/neopixel.ts"/>
|
||||
/// <reference path="../simlib.ts"/>
|
||||
|
||||
//TODO move to utils
|
||||
namespace pxsim.visuals {
|
||||
//expects rgb from 0,255, gives h in [0,360], s in [0, 100], l in [0, 100]
|
||||
export function rgbToHsl(rgb: [number, number, number]): [number, number, number] {
|
||||
let [r, g, b] = rgb;
|
||||
let [r$, g$, b$] = [r / 255, g / 255, b / 255];
|
||||
let cMin = Math.min(r$, g$, b$);
|
||||
let cMax = Math.max(r$, g$, b$);
|
||||
let cDelta = cMax - cMin;
|
||||
let h: number, s: number, l: number;
|
||||
let maxAndMin = cMax + cMin;
|
||||
|
||||
//lum
|
||||
l = (maxAndMin / 2) * 100
|
||||
|
||||
if (cDelta === 0)
|
||||
s = h = 0;
|
||||
else {
|
||||
//hue
|
||||
if (cMax === r$)
|
||||
h = 60 * (((g$ - b$) / cDelta) % 6);
|
||||
else if (cMax === g$)
|
||||
h = 60 * (((b$ - r$) / cDelta) + 2);
|
||||
else if (cMax === b$)
|
||||
h = 60 * (((r$ - g$) / cDelta) + 4);
|
||||
|
||||
//sat
|
||||
if (l > 50)
|
||||
s = 100 * (cDelta / (2 - maxAndMin));
|
||||
else
|
||||
s = 100 * (cDelta / maxAndMin);
|
||||
}
|
||||
|
||||
return [Math.floor(h), Math.floor(s), Math.floor(l)];
|
||||
}
|
||||
}
|
||||
|
||||
namespace pxsim.visuals {
|
||||
const PIXEL_SPACING = PIN_DIST * 3;
|
||||
const PIXEL_RADIUS = PIN_DIST;
|
||||
const CANVAS_WIDTH = 1.2 * PIN_DIST;
|
||||
const CANVAS_HEIGHT = 12 * PIN_DIST;
|
||||
const CANVAS_VIEW_WIDTH = CANVAS_WIDTH;
|
||||
const CANVAS_VIEW_HEIGHT = CANVAS_HEIGHT;
|
||||
const CANVAS_VIEW_PADDING = PIN_DIST * 4;
|
||||
const CANVAS_LEFT = 1.4 * PIN_DIST;
|
||||
const CANVAS_TOP = PIN_DIST;
|
||||
|
||||
// For the instructions parts list
|
||||
export function mkNeoPixelPart(xy: Coord = [0, 0]): SVGElAndSize {
|
||||
const NP_PART_XOFF = -13.5;
|
||||
const NP_PART_YOFF = -11;
|
||||
const NP_PART_WIDTH = 87.5;
|
||||
const NP_PART_HEIGHT = 190;
|
||||
const NEOPIXEL_PART_IMG = "neopixel-black-60-vert.svg";
|
||||
let [x, y] = xy;
|
||||
let l = x + NP_PART_XOFF;
|
||||
let t = y + NP_PART_YOFF;
|
||||
let w = NP_PART_WIDTH;
|
||||
let h = NP_PART_HEIGHT;
|
||||
let img = <SVGImageElement>svg.elt("image");
|
||||
svg.hydrate(img, {class: "sim-neopixel-strip", x: l, y: t, width: w, height: h,
|
||||
href: `/static/hardware/${NEOPIXEL_PART_IMG}`});
|
||||
return {el: img, x: l, y: t, w: w, h: h};
|
||||
}
|
||||
export class NeoPixel implements SVGAndSize<SVGCircleElement> {
|
||||
public el: SVGCircleElement;
|
||||
public w: number;
|
||||
public h: number;
|
||||
public x: number;
|
||||
public y: number;
|
||||
public cx: number;
|
||||
public cy: number;
|
||||
|
||||
constructor(xy: Coord = [0, 0]) {
|
||||
let circle = <SVGCircleElement>svg.elt("circle");
|
||||
let r = PIXEL_RADIUS;
|
||||
let [cx, cy] = xy;
|
||||
svg.hydrate(circle, {cx: cx, cy: cy, r: r, class: "sim-neopixel"});
|
||||
this.el = circle;
|
||||
this.w = r * 2;
|
||||
this.h = r * 2;
|
||||
this.x = cx - r;
|
||||
this.y = cy - r;
|
||||
this.cx = cx;
|
||||
this.cy = cy;
|
||||
}
|
||||
|
||||
public setRgb(rgb: [number, number, number]) {
|
||||
let hsl = rgbToHsl(rgb);
|
||||
let [h, s, l] = hsl;
|
||||
//We ignore luminosity since it doesn't map well to real-life brightness
|
||||
let fill = `hsl(${h}, ${s}%, 70%)`;
|
||||
this.el.setAttribute("fill", fill);
|
||||
}
|
||||
}
|
||||
|
||||
export class NeoPixelCanvas {
|
||||
public canvas: SVGSVGElement;
|
||||
public pin: number;
|
||||
public pixels: NeoPixel[];
|
||||
private viewBox: [number, number, number, number];
|
||||
private background: SVGRectElement;
|
||||
|
||||
constructor(pin: number) {
|
||||
this.pixels = [];
|
||||
this.pin = pin;
|
||||
let el = <SVGSVGElement>svg.elt("svg");
|
||||
svg.hydrate(el, {
|
||||
"class": `sim-neopixel-canvas`,
|
||||
"x": "0px",
|
||||
"y": "0px",
|
||||
"width": `${CANVAS_WIDTH}px`,
|
||||
"height": `${CANVAS_HEIGHT}px`,
|
||||
});
|
||||
this.canvas = el;
|
||||
this.background = <SVGRectElement>svg.child(el, "rect", { class: "sim-neopixel-background hidden"});
|
||||
this.updateViewBox(-CANVAS_VIEW_WIDTH / 2, 0, CANVAS_VIEW_WIDTH, CANVAS_VIEW_HEIGHT);
|
||||
}
|
||||
|
||||
private updateViewBox(x: number, y: number, w: number, h: number) {
|
||||
this.viewBox = [x, y, w, h];
|
||||
svg.hydrate(this.canvas, {"viewBox": `${x} ${y} ${w} ${h}`});
|
||||
svg.hydrate(this.background, {"x": x, "y": y, "width": w, "height": h});
|
||||
}
|
||||
|
||||
public update(colors: RGBW[]) {
|
||||
if (!colors || colors.length <= 0)
|
||||
return;
|
||||
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
let pixel = this.pixels[i];
|
||||
if (!pixel) {
|
||||
let cxy: Coord = [0, CANVAS_VIEW_PADDING + i * PIXEL_SPACING];
|
||||
pixel = this.pixels[i] = new NeoPixel(cxy);
|
||||
this.canvas.appendChild(pixel.el);
|
||||
}
|
||||
let color = colors[i];
|
||||
pixel.setRgb(color);
|
||||
svg.hydrate(pixel.el, {title: `offset: ${i}`});
|
||||
}
|
||||
|
||||
//show the canvas if it's hidden
|
||||
svg.removeClass(this.background, "hidden");
|
||||
|
||||
//resize if necessary
|
||||
let [first, last] = [this.pixels[0], this.pixels[this.pixels.length - 1]]
|
||||
let yDiff = last.cy - first.cy;
|
||||
let newH = yDiff + CANVAS_VIEW_PADDING * 2;
|
||||
let [oldX, oldY, oldW, oldH] = this.viewBox;
|
||||
if (oldH < newH) {
|
||||
let scalar = newH / oldH;
|
||||
let newW = oldW * scalar;
|
||||
this.updateViewBox(-newW / 2, oldY, newW, newH);
|
||||
}
|
||||
}
|
||||
|
||||
public setLoc(xy: Coord) {
|
||||
let [x, y] = xy;
|
||||
svg.hydrate(this.canvas, {x: x, y: y});
|
||||
}
|
||||
};
|
||||
|
||||
function gpioPinToPinNumber(gpioPin: string): number {
|
||||
let pinNumStr = gpioPin.split("P")[1];
|
||||
let pinNum = Number(pinNumStr) + 7 /*MICROBIT_ID_IO_P0; TODO: don't hardcode this, import enums.d.ts*/;
|
||||
return pinNum
|
||||
}
|
||||
function parseNeoPixelMode(modeStr: string): NeoPixelMode {
|
||||
const modeMap: Map<NeoPixelMode> = {
|
||||
"NeoPixelMode.RGB": NeoPixelMode.RGB,
|
||||
"NeoPixelMode.RGBW": NeoPixelMode.RGBW,
|
||||
"*": NeoPixelMode.RGB,
|
||||
};
|
||||
let mode: NeoPixelMode = null;
|
||||
for (let key in modeMap) {
|
||||
if (key == modeStr) {
|
||||
mode = modeMap[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
U.assert(mode != null, "Unknown NeoPixelMode: " + modeStr);
|
||||
return mode;
|
||||
}
|
||||
|
||||
export class NeoPixelView implements IBoardComponent<NeoPixelState> {
|
||||
public style: string = `
|
||||
.sim-neopixel-canvas {
|
||||
}
|
||||
.sim-neopixel-canvas-parent:hover {
|
||||
transform-origin: center;
|
||||
transform: scale(4) translateY(-60px);
|
||||
}
|
||||
.sim-neopixel-canvas .hidden {
|
||||
visibility:hidden;
|
||||
}
|
||||
.sim-neopixel-background {
|
||||
fill: rgba(255,255,255,0.9);
|
||||
}
|
||||
.sim-neopixel-strip {
|
||||
}
|
||||
`;
|
||||
public element: SVGElement;
|
||||
public defs: SVGElement[];
|
||||
private state: NeoPixelState;
|
||||
private canvas: NeoPixelCanvas;
|
||||
private part: SVGElAndSize;
|
||||
private stripGroup: SVGGElement;
|
||||
private lastLocation: Coord;
|
||||
private pin: number;
|
||||
private mode: NeoPixelMode;
|
||||
|
||||
public init(bus: EventBus, state: NeoPixelState, svgEl: SVGSVGElement, gpioPins: string[], otherArgs: string[]): void {
|
||||
U.assert(otherArgs.length === 1, "NeoPixels assumes a RGB vs RGBW mode is passed to it");
|
||||
let modeStr = otherArgs[0];
|
||||
this.mode = parseNeoPixelMode(modeStr);
|
||||
this.state = state;
|
||||
this.stripGroup = <SVGGElement>svg.elt("g");
|
||||
this.element = this.stripGroup;
|
||||
let pinStr = gpioPins[0];
|
||||
this.pin = gpioPinToPinNumber(pinStr);
|
||||
this.lastLocation = [0, 0];
|
||||
let part = mkNeoPixelPart();
|
||||
this.part = part;
|
||||
this.stripGroup.appendChild(part.el);
|
||||
let canvas = new NeoPixelCanvas(this.pin);
|
||||
this.canvas = canvas;
|
||||
let canvasG = svg.child(this.stripGroup, "g", {class: "sim-neopixel-canvas-parent"});
|
||||
canvasG.appendChild(canvas.canvas);
|
||||
this.updateStripLoc();
|
||||
}
|
||||
public moveToCoord(xy: Coord): void {
|
||||
let [x, y] = xy;
|
||||
let loc: Coord = [x, y];
|
||||
this.lastLocation = loc;
|
||||
this.updateStripLoc();
|
||||
}
|
||||
private updateStripLoc() {
|
||||
let [x, y] = this.lastLocation;
|
||||
this.canvas.setLoc([x + CANVAS_LEFT, y + CANVAS_TOP]);
|
||||
svg.hydrate(this.part.el, {transform: `translate(${x} ${y})`}); //TODO: update part's l,h, etc.
|
||||
}
|
||||
public updateState(): void {
|
||||
let colors = this.state.getColors(this.pin, this.mode);
|
||||
this.canvas.update(colors);
|
||||
}
|
||||
public updateTheme (): void { }
|
||||
}
|
||||
}
|
420
sim/visuals/wiring.ts
Normal file
420
sim/visuals/wiring.ts
Normal file
@ -0,0 +1,420 @@
|
||||
namespace pxsim.visuals {
|
||||
const WIRE_WIDTH = PIN_DIST / 2.5;
|
||||
const BB_WIRE_SMOOTH = 0.7;
|
||||
const INSTR_WIRE_SMOOTH = 0.8;
|
||||
const WIRE_PART_CURVE_OFF = 15;
|
||||
const WIRE_PART_LENGTH = 100;
|
||||
export const WIRES_CSS = `
|
||||
.sim-bb-wire {
|
||||
fill:none;
|
||||
stroke-linecap: round;
|
||||
stroke-width:${WIRE_WIDTH}px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.sim-bb-wire-end {
|
||||
stroke:#333;
|
||||
fill:#333;
|
||||
}
|
||||
.sim-bb-wire-bare-end {
|
||||
fill: #ccc;
|
||||
}
|
||||
.sim-bb-wire-hover {
|
||||
stroke-width: ${WIRE_WIDTH}px;
|
||||
visibility: hidden;
|
||||
stroke-dasharray: ${PIN_DIST / 10.0},${PIN_DIST / 1.5};
|
||||
/*stroke-opacity: 0.4;*/
|
||||
}
|
||||
.grayed .sim-bb-wire-end:not(.highlight) {
|
||||
stroke: #777;
|
||||
}
|
||||
.grayed .sim-bb-wire:not(.highlight) {
|
||||
stroke: #CCC;
|
||||
}
|
||||
.sim-bb-wire-ends-g:hover .sim-bb-wire-end {
|
||||
stroke: red;
|
||||
fill: red;
|
||||
}
|
||||
.sim-bb-wire-ends-g:hover .sim-bb-wire-bare-end {
|
||||
stroke: #FFF;
|
||||
fill: #FFF;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface Wire {
|
||||
endG: SVGGElement;
|
||||
end1: SVGElement;
|
||||
end2: SVGElement;
|
||||
wires: SVGElement[];
|
||||
}
|
||||
|
||||
function cssEncodeColor(color: string): string {
|
||||
//HACK/TODO: do real CSS encoding.
|
||||
return color
|
||||
.replace(/\#/g, "-")
|
||||
.replace(/\(/g, "-")
|
||||
.replace(/\)/g, "-")
|
||||
.replace(/\,/g, "-")
|
||||
.replace(/\./g, "-")
|
||||
.replace(/\s/g, "");
|
||||
}
|
||||
export enum WireEndStyle {
|
||||
BBJumper,
|
||||
OpenJumper,
|
||||
Croc,
|
||||
}
|
||||
export interface WireOpts { //TODO: use throughout
|
||||
color?: string,
|
||||
colorClass?: string,
|
||||
bendFactor?: number,
|
||||
}
|
||||
export function mkWirePart(cp: [number, number], clr: string): visuals.SVGAndSize<SVGGElement> {
|
||||
let g = <SVGGElement>svg.elt("g");
|
||||
let [cx, cy] = cp;
|
||||
let offset = WIRE_PART_CURVE_OFF;
|
||||
let p1: visuals.Coord = [cx - offset, cy - WIRE_PART_LENGTH / 2];
|
||||
let p2: visuals.Coord = [cx + offset, cy + WIRE_PART_LENGTH / 2];
|
||||
clr = visuals.mapWireColor(clr);
|
||||
let e1 = mkOpenJumperEnd(p1, true, clr);
|
||||
let s = mkWirePartSeg(p1, p2, clr);
|
||||
let e2 = mkOpenJumperEnd(p2, false, clr);
|
||||
g.appendChild(s.el);
|
||||
g.appendChild(e1.el);
|
||||
g.appendChild(e2.el);
|
||||
let l = Math.min(e1.x, e2.x);
|
||||
let r = Math.max(e1.x + e1.w, e2.x + e2.w);
|
||||
let t = Math.min(e1.y, e2.y);
|
||||
let b = Math.max(e1.y + e1.h, e2.y + e2.h);
|
||||
return {el: g, x: l, y: t, w: r - l, h: b - t};
|
||||
}
|
||||
function mkCurvedWireSeg(p1: [number, number], p2: [number, number], smooth: number, clrClass: string): SVGPathElement {
|
||||
const coordStr = (xy: [number, number]): string => {return `${xy[0]}, ${xy[1]}`};
|
||||
let [x1, y1] = p1;
|
||||
let [x2, y2] = p2
|
||||
let yLen = (y2 - y1);
|
||||
let c1: [number, number] = [x1, y1 + yLen * smooth];
|
||||
let c2: [number, number] = [x2, y2 - yLen * smooth];
|
||||
let w = <SVGPathElement>svg.mkPath("sim-bb-wire", `M${coordStr(p1)} C${coordStr(c1)} ${coordStr(c2)} ${coordStr(p2)}`);
|
||||
svg.addClass(w, `wire-stroke-${clrClass}`);
|
||||
return w;
|
||||
}
|
||||
function mkWirePartSeg(p1: [number, number], p2: [number, number], clr: string): visuals.SVGAndSize<SVGPathElement> {
|
||||
//TODO: merge with mkCurvedWireSeg
|
||||
const coordStr = (xy: [number, number]): string => {return `${xy[0]}, ${xy[1]}`};
|
||||
let [x1, y1] = p1;
|
||||
let [x2, y2] = p2
|
||||
let yLen = (y2 - y1);
|
||||
let c1: [number, number] = [x1, y1 + yLen * .8];
|
||||
let c2: [number, number] = [x2, y2 - yLen * .8];
|
||||
let e = <SVGPathElement>svg.mkPath("sim-bb-wire", `M${coordStr(p1)} C${coordStr(c1)} ${coordStr(c2)} ${coordStr(p2)}`);
|
||||
(<any>e).style["stroke"] = clr;
|
||||
return {el: e, x: Math.min(x1, x2), y: Math.min(y1, y2), w: Math.abs(x1 - x2), h: Math.abs(y1 - y2)};
|
||||
}
|
||||
function mkWireSeg(p1: [number, number], p2: [number, number], clrClass: string): SVGPathElement {
|
||||
const coordStr = (xy: [number, number]): string => {return `${xy[0]}, ${xy[1]}`};
|
||||
let w = <SVGPathElement>svg.mkPath("sim-bb-wire", `M${coordStr(p1)} L${coordStr(p2)}`);
|
||||
svg.addClass(w, `wire-stroke-${clrClass}`);
|
||||
return w;
|
||||
}
|
||||
function mkBBJumperEnd(p: [number, number], clrClass: string): SVGElement {
|
||||
const endW = PIN_DIST / 4;
|
||||
let w = svg.elt("circle");
|
||||
let x = p[0];
|
||||
let y = p[1];
|
||||
let r = WIRE_WIDTH / 2 + endW / 2;
|
||||
svg.hydrate(w, {cx: x, cy: y, r: r, class: "sim-bb-wire-end"});
|
||||
svg.addClass(w, `wire-fill-${clrClass}`);
|
||||
(<any>w).style["stroke-width"] = `${endW}px`;
|
||||
return w;
|
||||
}
|
||||
function mkOpenJumperEnd(p: [number, number], top: boolean, clr: string): visuals.SVGElAndSize {
|
||||
let k = visuals.PIN_DIST * 0.24;
|
||||
let plasticLength = k * 10;
|
||||
let plasticWidth = k * 2;
|
||||
let metalLength = k * 6;
|
||||
let metalWidth = k;
|
||||
const strokeWidth = visuals.PIN_DIST / 4.0;
|
||||
let [cx, cy] = p;
|
||||
let o = top ? -1 : 1;
|
||||
let g = svg.elt("g")
|
||||
|
||||
let el = svg.elt("rect");
|
||||
let h1 = plasticLength;
|
||||
let w1 = plasticWidth;
|
||||
let x1 = cx - w1 / 2;
|
||||
let y1 = cy - (h1 / 2);
|
||||
svg.hydrate(el, {x: x1, y: y1, width: w1, height: h1, rx: 0.5, ry: 0.5, class: "sim-bb-wire-end"});
|
||||
(<any>el).style["stroke-width"] = `${strokeWidth}px`;
|
||||
|
||||
let el2 = svg.elt("rect");
|
||||
let h2 = metalLength;
|
||||
let w2 = metalWidth;
|
||||
let cy2 = cy + o * (h1 / 2 + h2 / 2);
|
||||
let x2 = cx - w2 / 2;
|
||||
let y2 = cy2 - (h2 / 2);
|
||||
svg.hydrate(el2, {x: x2, y: y2, width: w2, height: h2, class: "sim-bb-wire-bare-end"});
|
||||
(<any>el2).style["fill"] = `#bbb`;
|
||||
|
||||
g.appendChild(el2);
|
||||
g.appendChild(el);
|
||||
return {el: g, x: x1 - strokeWidth, y: Math.min(y1, y2), w: w1 + strokeWidth * 2, h: h1 + h2};
|
||||
}
|
||||
function mkCrocEnd(p: [number, number], top: boolean, clr: string): SVGElAndSize {
|
||||
//TODO: merge with mkOpenJumperEnd()
|
||||
let k = visuals.PIN_DIST * 0.24;
|
||||
const plasticWidth = k * 4;
|
||||
const plasticLength = k * 10.0;
|
||||
const metalWidth = k * 3.5;
|
||||
const metalHeight = k * 3.5;
|
||||
const pointScalar = .15;
|
||||
const baseScalar = .3;
|
||||
const taperScalar = .7;
|
||||
const strokeWidth = visuals.PIN_DIST / 4.0;
|
||||
let [cx, cy] = p;
|
||||
let o = top ? -1 : 1;
|
||||
let g = svg.elt("g")
|
||||
|
||||
let el = svg.elt("polygon");
|
||||
let h1 = plasticLength;
|
||||
let w1 = plasticWidth;
|
||||
let x1 = cx - w1 / 2;
|
||||
let y1 = cy - (h1 / 2);
|
||||
let mkPnt = (xy: Coord) => `${xy[0]},${xy[1]}`;
|
||||
let mkPnts = (...xys: Coord[]) => xys.map(xy => mkPnt(xy)).join(" ");
|
||||
const topScalar = top ? pointScalar : baseScalar;
|
||||
const midScalar = top ? taperScalar : (1 - taperScalar);
|
||||
const botScalar = top ? baseScalar : pointScalar;
|
||||
svg.hydrate(el, {
|
||||
points: mkPnts(
|
||||
[x1 + w1 * topScalar, y1], //TL
|
||||
[x1 + w1 * (1 - topScalar), y1], //TR
|
||||
[x1 + w1, y1 + h1 * midScalar], //MR
|
||||
[x1 + w1 * (1 - botScalar), y1 + h1], //BR
|
||||
[x1 + w1 * botScalar, y1 + h1], //BL
|
||||
[x1, y1 + h1 * midScalar]) //ML
|
||||
});
|
||||
svg.hydrate(el, {rx: 0.5, ry: 0.5, class: "sim-bb-wire-end"});
|
||||
(<any>el).style["stroke-width"] = `${strokeWidth}px`;
|
||||
|
||||
let el2 = svg.elt("rect");
|
||||
let h2 = metalWidth;
|
||||
let w2 = metalHeight;
|
||||
let cy2 = cy + o * (h1 / 2 + h2 / 2);
|
||||
let x2 = cx - w2 / 2;
|
||||
let y2 = cy2 - (h2 / 2);
|
||||
svg.hydrate(el2, {x: x2, y: y2, width: w2, height: h2, class: "sim-bb-wire-bare-end"});
|
||||
|
||||
g.appendChild(el2);
|
||||
g.appendChild(el);
|
||||
return {el: g, x: x1 - strokeWidth, y: Math.min(y1, y2), w: w1 + strokeWidth * 2, h: h1 + h2};
|
||||
}
|
||||
|
||||
//TODO: make this stupid class obsolete
|
||||
export class WireFactory {
|
||||
private underboard: SVGGElement;
|
||||
private overboard: SVGGElement;
|
||||
private boardEdges: number[];
|
||||
private getLocCoord: (loc: Loc) => Coord;
|
||||
public styleEl: SVGStyleElement;
|
||||
|
||||
constructor(underboard: SVGGElement, overboard: SVGGElement, boardEdges: number[], styleEl: SVGStyleElement, getLocCoord: (loc: Loc) => Coord) {
|
||||
this.styleEl = styleEl;
|
||||
this.styleEl.textContent += WIRES_CSS;
|
||||
this.underboard = underboard;
|
||||
this.overboard = overboard;
|
||||
this.boardEdges = boardEdges;
|
||||
this.getLocCoord = getLocCoord;
|
||||
}
|
||||
|
||||
private indexOfMin(vs: number[]): number {
|
||||
let minIdx = 0;
|
||||
let min = vs[0];
|
||||
for (let i = 1; i < vs.length; i++) {
|
||||
if (vs[i] < min) {
|
||||
min = vs[i];
|
||||
minIdx = i;
|
||||
}
|
||||
}
|
||||
return minIdx;
|
||||
}
|
||||
private closestEdgeIdx(p: [number, number]): number {
|
||||
let dists = this.boardEdges.map(e => Math.abs(p[1] - e));
|
||||
let edgeIdx = this.indexOfMin(dists);
|
||||
return edgeIdx;
|
||||
}
|
||||
private closestEdge(p: [number, number]): number {
|
||||
return this.boardEdges[this.closestEdgeIdx(p)];
|
||||
}
|
||||
|
||||
private nextWireId = 0;
|
||||
private drawWire(pin1: Coord, pin2: Coord, color: string): Wire {
|
||||
let wires: SVGElement[] = [];
|
||||
let g = svg.child(this.overboard, "g", {class: "sim-bb-wire-group"});
|
||||
const closestPointOffBoard = (p: [number, number]): [number, number] => {
|
||||
const offset = PIN_DIST / 2;
|
||||
let e = this.closestEdge(p);
|
||||
let y: number;
|
||||
if (e - p[1] < 0)
|
||||
y = e - offset;
|
||||
else
|
||||
y = e + offset;
|
||||
return [p[0], y];
|
||||
}
|
||||
let wireId = this.nextWireId++;
|
||||
let clrClass = cssEncodeColor(color);
|
||||
let end1 = mkBBJumperEnd(pin1, clrClass);
|
||||
let end2 = mkBBJumperEnd(pin2, clrClass);
|
||||
let endG = <SVGGElement>svg.child(g, "g", {class: "sim-bb-wire-ends-g"});
|
||||
endG.appendChild(end1);
|
||||
endG.appendChild(end2);
|
||||
let edgeIdx1 = this.closestEdgeIdx(pin1);
|
||||
let edgeIdx2 = this.closestEdgeIdx(pin2);
|
||||
if (edgeIdx1 == edgeIdx2) {
|
||||
let seg = mkWireSeg(pin1, pin2, clrClass);
|
||||
g.appendChild(seg);
|
||||
wires.push(seg);
|
||||
} else {
|
||||
let offP1 = closestPointOffBoard(pin1);
|
||||
let offP2 = closestPointOffBoard(pin2);
|
||||
let offSeg1 = mkWireSeg(pin1, offP1, clrClass);
|
||||
let offSeg2 = mkWireSeg(pin2, offP2, clrClass);
|
||||
let midSeg: SVGElement;
|
||||
let midSegHover: SVGElement;
|
||||
let isBetweenMiddleTwoEdges = (edgeIdx1 == 1 || edgeIdx1 == 2) && (edgeIdx2 == 1 || edgeIdx2 == 2);
|
||||
if (isBetweenMiddleTwoEdges) {
|
||||
midSeg = mkCurvedWireSeg(offP1, offP2, BB_WIRE_SMOOTH, clrClass);
|
||||
midSegHover = mkCurvedWireSeg(offP1, offP2, BB_WIRE_SMOOTH, clrClass);
|
||||
} else {
|
||||
midSeg = mkWireSeg(offP1, offP2, clrClass);
|
||||
midSegHover = mkWireSeg(offP1, offP2, clrClass);
|
||||
}
|
||||
svg.addClass(midSegHover, "sim-bb-wire-hover");
|
||||
g.appendChild(offSeg1);
|
||||
wires.push(offSeg1);
|
||||
g.appendChild(offSeg2);
|
||||
wires.push(offSeg2);
|
||||
this.underboard.appendChild(midSeg);
|
||||
wires.push(midSeg);
|
||||
g.appendChild(midSegHover);
|
||||
wires.push(midSegHover);
|
||||
//set hover mechanism
|
||||
let wireIdClass = `sim-bb-wire-id-${wireId}`;
|
||||
const setId = (e: SVGElement) => svg.addClass(e, wireIdClass);
|
||||
setId(endG);
|
||||
setId(midSegHover);
|
||||
this.styleEl.textContent += `
|
||||
.${wireIdClass}:hover ~ .${wireIdClass}.sim-bb-wire-hover {
|
||||
visibility: visible;
|
||||
}`
|
||||
}
|
||||
|
||||
// wire colors
|
||||
let colorCSS = `
|
||||
.wire-stroke-${clrClass} {
|
||||
stroke: ${mapWireColor(color)};
|
||||
}
|
||||
.wire-fill-${clrClass} {
|
||||
fill: ${mapWireColor(color)};
|
||||
}
|
||||
`
|
||||
this.styleEl.textContent += colorCSS;
|
||||
|
||||
return {endG: endG, end1: end1, end2: end2, wires: wires};
|
||||
}
|
||||
private drawWireWithCrocs(pin1: Coord, pin2: Coord, color: string): Wire {
|
||||
//TODO: merge with drawWire()
|
||||
const PIN_Y_OFF = 40;
|
||||
const CROC_Y_OFF = -17;
|
||||
let wires: SVGElement[] = [];
|
||||
let g = svg.child(this.overboard, "g", {class: "sim-bb-wire-group"});
|
||||
const closestPointOffBoard = (p: [number, number]): [number, number] => {
|
||||
const offset = PIN_DIST / 2;
|
||||
let e = this.closestEdge(p);
|
||||
let y: number;
|
||||
if (e - p[1] < 0)
|
||||
y = e - offset;
|
||||
else
|
||||
y = e + offset;
|
||||
return [p[0], y];
|
||||
}
|
||||
let wireId = this.nextWireId++;
|
||||
let clrClass = cssEncodeColor(color);
|
||||
let end1 = mkBBJumperEnd(pin1, clrClass);
|
||||
let pin2orig = pin2;
|
||||
let [x2, y2] = pin2;
|
||||
pin2 = [x2, y2 + PIN_Y_OFF];//HACK
|
||||
[x2, y2] = pin2;
|
||||
let endCoord2: Coord = [x2, y2 + CROC_Y_OFF]
|
||||
let end2AndSize = mkCrocEnd(endCoord2, true, color);
|
||||
let end2 = end2AndSize.el;
|
||||
let endG = <SVGGElement>svg.child(g, "g", {class: "sim-bb-wire-ends-g"});
|
||||
endG.appendChild(end1);
|
||||
//endG.appendChild(end2);
|
||||
let edgeIdx1 = this.closestEdgeIdx(pin1);
|
||||
let edgeIdx2 = this.closestEdgeIdx(pin2orig);
|
||||
if (edgeIdx1 == edgeIdx2) {
|
||||
let seg = mkWireSeg(pin1, pin2, clrClass);
|
||||
g.appendChild(seg);
|
||||
wires.push(seg);
|
||||
} else {
|
||||
let offP1 = closestPointOffBoard(pin1);
|
||||
//let offP2 = closestPointOffBoard(pin2orig);
|
||||
let offSeg1 = mkWireSeg(pin1, offP1, clrClass);
|
||||
//let offSeg2 = mkWireSeg(pin2, offP2, clrClass);
|
||||
let midSeg: SVGElement;
|
||||
let midSegHover: SVGElement;
|
||||
let isBetweenMiddleTwoEdges = (edgeIdx1 == 1 || edgeIdx1 == 2) && (edgeIdx2 == 1 || edgeIdx2 == 2);
|
||||
if (isBetweenMiddleTwoEdges) {
|
||||
midSeg = mkCurvedWireSeg(offP1, pin2, BB_WIRE_SMOOTH, clrClass);
|
||||
midSegHover = mkCurvedWireSeg(offP1, pin2, BB_WIRE_SMOOTH, clrClass);
|
||||
} else {
|
||||
midSeg = mkWireSeg(offP1, pin2, clrClass);
|
||||
midSegHover = mkWireSeg(offP1, pin2, clrClass);
|
||||
}
|
||||
svg.addClass(midSegHover, "sim-bb-wire-hover");
|
||||
g.appendChild(offSeg1);
|
||||
wires.push(offSeg1);
|
||||
// g.appendChild(offSeg2);
|
||||
// wires.push(offSeg2);
|
||||
this.underboard.appendChild(midSeg);
|
||||
wires.push(midSeg);
|
||||
//g.appendChild(midSegHover);
|
||||
//wires.push(midSegHover);
|
||||
//set hover mechanism
|
||||
let wireIdClass = `sim-bb-wire-id-${wireId}`;
|
||||
const setId = (e: SVGElement) => svg.addClass(e, wireIdClass);
|
||||
setId(endG);
|
||||
setId(midSegHover);
|
||||
this.styleEl.textContent += `
|
||||
.${wireIdClass}:hover ~ .${wireIdClass}.sim-bb-wire-hover {
|
||||
visibility: visible;
|
||||
}`
|
||||
}
|
||||
endG.appendChild(end2);//HACK
|
||||
|
||||
// wire colors
|
||||
let colorCSS = `
|
||||
.wire-stroke-${clrClass} {
|
||||
stroke: ${mapWireColor(color)};
|
||||
}
|
||||
.wire-fill-${clrClass} {
|
||||
fill: ${mapWireColor(color)};
|
||||
}
|
||||
`
|
||||
this.styleEl.textContent += colorCSS;
|
||||
|
||||
return {endG: endG, end1: end1, end2: end2, wires: wires};
|
||||
}
|
||||
|
||||
public addWire(start: Loc, end: Loc, color: string, withCrocs: boolean = false): Wire {
|
||||
let startLoc = this.getLocCoord(start);
|
||||
let endLoc = this.getLocCoord(end);
|
||||
let wireEls: Wire;
|
||||
if (withCrocs && end.type == "dalboard") {
|
||||
wireEls = this.drawWireWithCrocs(startLoc, endLoc, color);
|
||||
} else {
|
||||
wireEls = this.drawWire(startLoc, endLoc, color);
|
||||
}
|
||||
return wireEls;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user