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; cursor: default; } .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 = svg.elt("g"); let colIdxOffset = opts.colStartIdx || 0; let rowIdxOffset = opts.rowStartIdx || 0; let copyArr = (arr: T[]): T[] => arr ? arr.slice(0, arr.length) : []; let removeAll = (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 interface BreadboardOpts { wireframe?: boolean, } 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> = {}; private rowColToLbls: Map> = {}; constructor(opts: BreadboardOpts) { this.buildDom(); if (opts.wireframe) svg.addClass(this.bb, "sim-bb-outline"); } 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 = svg.elt("svg", { "version": "1.0", "viewBox": `0 0 ${WIDTH} ${HEIGHT}`, "class": `sim-bb`, "width": WIDTH + "px", "height": HEIGHT + "px", }); this.styleEl = svg.child(this.bb, "style", {}); this.styleEl.textContent += BREADBOARD_CSS; this.defs = 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 = 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 = {}; 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 = 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 = 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 = {}; allGrpNms.forEach((g, i) => grpNmToGroup[g] = groups[i]); //group pins and add connecting wire let grpNmToPins: Map = {}; 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 = 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 { return {el: this.bb, y: 0, x: 0, w: WIDTH, h: HEIGHT}; } public highlightLoc(rowCol: BBRowCol) { let [row, col] = rowCol; 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); } } }