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