640 lines
25 KiB
TypeScript
640 lines
25 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
} |