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 { let g = 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 = 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 { //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 = svg.mkPath("sim-bb-wire", `M${coordStr(p1)} C${coordStr(c1)} ${coordStr(c2)} ${coordStr(p2)}`); (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 = 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}`); (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"}); (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"}); (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"}); (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 = 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 = 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; } } }