/* PCB rendering code */ var emptyContext2d = document.createElement("canvas").getContext("2d"); function deg2rad(deg) { return deg * Math.PI / 180; } function calcFontPoint(linepoint, text, offsetx, offsety, tilt) { var point = [ linepoint[0] * text.width + offsetx, linepoint[1] * text.height + offsety ]; // This approximates pcbnew behavior with how text tilts depending on horizontal justification point[0] -= (linepoint[1] + 0.5 * (1 + text.justify[0])) * text.height * tilt; return point; } function drawText(ctx, text, color) { if ("ref" in text && !settings.renderReferences) return; if ("val" in text && !settings.renderValues) return; ctx.save(); ctx.fillStyle = color; ctx.strokeStyle = color; ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.lineWidth = text.thickness; if ("svgpath" in text) { ctx.stroke(new Path2D(text.svgpath)); ctx.restore(); return; } if ("polygons" in text) { ctx.fill(getPolygonsPath(text)); ctx.restore(); return; } ctx.translate(...text.pos); ctx.translate(text.thickness * 0.5, 0); var angle = -text.angle; if (text.attr.includes("mirrored")) { ctx.scale(-1, 1); angle = -angle; } var tilt = 0; if (text.attr.includes("italic")) { tilt = 0.125; } var interline = text.height * 1.5 + text.thickness; var txt = text.text.split("\n"); // KiCad ignores last empty line. if (txt[txt.length - 1] == '') txt.pop(); ctx.rotate(deg2rad(angle)); var offsety = (1 - text.justify[1]) / 2 * text.height; // One line offset offsety -= (txt.length - 1) * (text.justify[1] + 1) / 2 * interline; // Multiline offset for (var i in txt) { var lineWidth = text.thickness + interline / 2 * tilt; for (var j = 0; j < txt[i].length; j++) { if (txt[i][j] == '\t') { var fourSpaces = 4 * pcbdata.font_data[' '].w * text.width; lineWidth += fourSpaces - lineWidth % fourSpaces; } else { if (txt[i][j] == '~') { j++; if (j == txt[i].length) break; } lineWidth += pcbdata.font_data[txt[i][j]].w * text.width; } } var offsetx = -lineWidth * (text.justify[0] + 1) / 2; var inOverbar = false; for (var j = 0; j < txt[i].length; j++) { if (config.kicad_text_formatting) { if (txt[i][j] == '\t') { var fourSpaces = 4 * pcbdata.font_data[' '].w * text.width; offsetx += fourSpaces - offsetx % fourSpaces; continue; } else if (txt[i][j] == '~') { j++; if (j == txt[i].length) break; if (txt[i][j] != '~') { inOverbar = !inOverbar; } } } var glyph = pcbdata.font_data[txt[i][j]]; if (inOverbar) { var overbarStart = [offsetx, -text.height * 1.4 + offsety]; var overbarEnd = [offsetx + text.width * glyph.w, overbarStart[1]]; if (!lastHadOverbar) { overbarStart[0] += text.height * 1.4 * tilt; lastHadOverbar = true; } ctx.beginPath(); ctx.moveTo(...overbarStart); ctx.lineTo(...overbarEnd); ctx.stroke(); } else { lastHadOverbar = false; } for (var line of glyph.l) { ctx.beginPath(); ctx.moveTo(...calcFontPoint(line[0], text, offsetx, offsety, tilt)); for (var k = 1; k < line.length; k++) { ctx.lineTo(...calcFontPoint(line[k], text, offsetx, offsety, tilt)); } ctx.stroke(); } offsetx += glyph.w * text.width; } offsety += interline; } ctx.restore(); } function drawedge(ctx, scalefactor, edge, color) { ctx.strokeStyle = color; ctx.fillStyle = color; ctx.lineWidth = Math.max(1 / scalefactor, edge.width); ctx.lineCap = "round"; ctx.lineJoin = "round"; if ("svgpath" in edge) { ctx.stroke(new Path2D(edge.svgpath)); } else { ctx.beginPath(); if (edge.type == "segment") { ctx.moveTo(...edge.start); ctx.lineTo(...edge.end); } if (edge.type == "rect") { ctx.moveTo(...edge.start); ctx.lineTo(edge.start[0], edge.end[1]); ctx.lineTo(...edge.end); ctx.lineTo(edge.end[0], edge.start[1]); ctx.lineTo(...edge.start); } if (edge.type == "arc") { ctx.arc( ...edge.start, edge.radius, deg2rad(edge.startangle), deg2rad(edge.endangle)); } if (edge.type == "circle") { ctx.arc( ...edge.start, edge.radius, 0, 2 * Math.PI); ctx.closePath(); } if (edge.type == "curve") { ctx.moveTo(...edge.start); ctx.bezierCurveTo(...edge.cpa, ...edge.cpb, ...edge.end); } if("filled" in edge && edge.filled) ctx.fill(); else ctx.stroke(); } } function getChamferedRectPath(size, radius, chamfpos, chamfratio) { // chamfpos is a bitmask, left = 1, right = 2, bottom left = 4, bottom right = 8 var path = new Path2D(); var width = size[0]; var height = size[1]; var x = width * -0.5; var y = height * -0.5; var chamfOffset = Math.min(width, height) * chamfratio; path.moveTo(x, 0); if (chamfpos & 4) { path.lineTo(x, y + height - chamfOffset); path.lineTo(x + chamfOffset, y + height); path.lineTo(0, y + height); } else { path.arcTo(x, y + height, x + width, y + height, radius); } if (chamfpos & 8) { path.lineTo(x + width - chamfOffset, y + height); path.lineTo(x + width, y + height - chamfOffset); path.lineTo(x + width, 0); } else { path.arcTo(x + width, y + height, x + width, y, radius); } if (chamfpos & 2) { path.lineTo(x + width, y + chamfOffset); path.lineTo(x + width - chamfOffset, y); path.lineTo(0, y); } else { path.arcTo(x + width, y, x, y, radius); } if (chamfpos & 1) { path.lineTo(x + chamfOffset, y); path.lineTo(x, y + chamfOffset); path.lineTo(x, 0); } else { path.arcTo(x, y, x, y + height, radius); } path.closePath(); return path; } function getOblongPath(size) { return getChamferedRectPath(size, Math.min(size[0], size[1]) / 2, 0, 0); } function getPolygonsPath(shape) { if (shape.path2d) { return shape.path2d; } if ("svgpath" in shape) { shape.path2d = new Path2D(shape.svgpath); } else { var path = new Path2D(); for (var polygon of shape.polygons) { path.moveTo(...polygon[0]); for (var i = 1; i < polygon.length; i++) { path.lineTo(...polygon[i]); } path.closePath(); } shape.path2d = path; } return shape.path2d; } function drawPolygonShape(ctx, scalefactor, shape, color) { ctx.save(); if (!("svgpath" in shape)) { ctx.translate(...shape.pos); ctx.rotate(deg2rad(-shape.angle)); } if("filled" in shape && !shape.filled) { ctx.strokeStyle = color; ctx.lineWidth = Math.max(1 / scalefactor, shape.width); ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.stroke(getPolygonsPath(shape)); } else { ctx.fillStyle = color; ctx.fill(getPolygonsPath(shape)); } ctx.restore(); } function drawDrawing(ctx, scalefactor, drawing, color) { if (["segment", "arc", "circle", "curve", "rect"].includes(drawing.type)) { drawedge(ctx, scalefactor, drawing, color); } else if (drawing.type == "polygon") { drawPolygonShape(ctx, scalefactor, drawing, color); } else { drawText(ctx, drawing, color); } } function getCirclePath(radius) { var path = new Path2D(); path.arc(0, 0, radius, 0, 2 * Math.PI); path.closePath(); return path; } function getCachedPadPath(pad) { if (!pad.path2d) { // if path2d is not set, build one and cache it on pad object if (pad.shape == "rect") { pad.path2d = new Path2D(); pad.path2d.rect(...pad.size.map(c => -c * 0.5), ...pad.size); } else if (pad.shape == "oval") { pad.path2d = getOblongPath(pad.size); } else if (pad.shape == "circle") { pad.path2d = getCirclePath(pad.size[0] / 2); } else if (pad.shape == "roundrect") { pad.path2d = getChamferedRectPath(pad.size, pad.radius, 0, 0); } else if (pad.shape == "chamfrect") { pad.path2d = getChamferedRectPath(pad.size, pad.radius, pad.chamfpos, pad.chamfratio) } else if (pad.shape == "custom") { pad.path2d = getPolygonsPath(pad); } } return pad.path2d; } function drawPad(ctx, pad, color, outline) { ctx.save(); ctx.translate(...pad.pos); ctx.rotate(-deg2rad(pad.angle)); if (pad.offset) { ctx.translate(...pad.offset); } ctx.fillStyle = color; ctx.strokeStyle = color; var path = getCachedPadPath(pad); if (outline) { ctx.stroke(path); } else { ctx.fill(path); } ctx.restore(); } function drawPadHole(ctx, pad, padHoleColor) { if (pad.type != "th") return; ctx.save(); ctx.translate(...pad.pos); ctx.rotate(-deg2rad(pad.angle)); ctx.fillStyle = padHoleColor; if (pad.drillshape == "oblong") { ctx.fill(getOblongPath(pad.drillsize)); } else { ctx.fill(getCirclePath(pad.drillsize[0] / 2)); } ctx.restore(); } function drawFootprint(ctx, layer, scalefactor, footprint, colors, highlight, outline) { if (highlight) { // draw bounding box if (footprint.layer == layer) { ctx.save(); ctx.globalAlpha = 0.2; ctx.translate(...footprint.bbox.pos); ctx.rotate(deg2rad(-footprint.bbox.angle)); ctx.translate(...footprint.bbox.relpos); ctx.fillStyle = colors.pad; ctx.fillRect(0, 0, ...footprint.bbox.size); ctx.globalAlpha = 1; ctx.strokeStyle = colors.pad; ctx.lineWidth = 3 / scalefactor; ctx.strokeRect(0, 0, ...footprint.bbox.size); ctx.restore(); } } // draw drawings for (var drawing of footprint.drawings) { if (drawing.layer == layer) { drawDrawing(ctx, scalefactor, drawing.drawing, colors.pad); } } ctx.lineWidth = 3 / scalefactor; // draw pads if (settings.renderPads) { for (var pad of footprint.pads) { if (pad.layers.includes(layer)) { drawPad(ctx, pad, colors.pad, outline); if (pad.pin1 && (settings.highlightpin1 == "all" || settings.highlightpin1 == "selected" && highlight)) { drawPad(ctx, pad, colors.outline, true); } } } for (var pad of footprint.pads) { drawPadHole(ctx, pad, colors.padHole); } } } function drawEdgeCuts(canvas, scalefactor) { var ctx = canvas.getContext("2d"); var edgecolor = getComputedStyle(topmostdiv).getPropertyValue('--pcb-edge-color'); for (var edge of pcbdata.edges) { drawDrawing(ctx, scalefactor, edge, edgecolor); } } function drawFootprints(canvas, layer, scalefactor, highlight) { var ctx = canvas.getContext("2d"); ctx.lineWidth = 3 / scalefactor; var style = getComputedStyle(topmostdiv); var colors = { pad: style.getPropertyValue('--pad-color'), padHole: style.getPropertyValue('--pad-hole-color'), outline: style.getPropertyValue('--pin1-outline-color'), } for (var i = 0; i < pcbdata.footprints.length; i++) { var mod = pcbdata.footprints[i]; var outline = settings.renderDnpOutline && pcbdata.bom.skipped.includes(i); var h = highlightedFootprints.includes(i); var d = markedFootprints.has(i); if (highlight) { if(h && d) { colors.pad = style.getPropertyValue('--pad-color-highlight-both'); colors.outline = style.getPropertyValue('--pin1-outline-color-highlight-both'); } else if (h) { colors.pad = style.getPropertyValue('--pad-color-highlight'); colors.outline = style.getPropertyValue('--pin1-outline-color-highlight'); } else if (d) { colors.pad = style.getPropertyValue('--pad-color-highlight-marked'); colors.outline = style.getPropertyValue('--pin1-outline-color-highlight-marked'); } } if( h || d || !highlight) { drawFootprint(ctx, layer, scalefactor, mod, colors, highlight, outline); } } } function drawBgLayer(layername, canvas, layer, scalefactor, edgeColor, polygonColor, textColor) { var ctx = canvas.getContext("2d"); for (var d of pcbdata.drawings[layername][layer]) { if (["segment", "arc", "circle", "curve", "rect"].includes(d.type)) { drawedge(ctx, scalefactor, d, edgeColor); } else if (d.type == "polygon") { drawPolygonShape(ctx, scalefactor, d, polygonColor); } else { drawText(ctx, d, textColor); } } } function drawTracks(canvas, layer, defaultColor, highlight) { ctx = canvas.getContext("2d"); ctx.lineCap = "round"; var hasHole = (track) => ( 'drillsize' in track && track.start[0] == track.end[0] && track.start[1] == track.end[1]); // First draw tracks and tented vias for (var track of pcbdata.tracks[layer]) { if (highlight && highlightedNet != track.net) continue; if (!hasHole(track)) { ctx.strokeStyle = highlight ? defaultColor : settings.netColors[track.net] || defaultColor; ctx.lineWidth = track.width; ctx.beginPath(); if ('radius' in track) { ctx.arc( ...track.center, track.radius, deg2rad(track.startangle), deg2rad(track.endangle)); } else { ctx.moveTo(...track.start); ctx.lineTo(...track.end); } ctx.stroke(); } } // Second pass to draw untented vias var style = getComputedStyle(topmostdiv); var holeColor = style.getPropertyValue('--pad-hole-color') for (var track of pcbdata.tracks[layer]) { if (highlight && highlightedNet != track.net) continue; if (hasHole(track)) { ctx.strokeStyle = highlight ? defaultColor : settings.netColors[track.net] || defaultColor; ctx.lineWidth = track.width; ctx.beginPath(); ctx.moveTo(...track.start); ctx.lineTo(...track.end); ctx.stroke(); ctx.strokeStyle = holeColor; ctx.lineWidth = track.drillsize; ctx.lineTo(...track.end); ctx.stroke(); } } } function drawZones(canvas, layer, defaultColor, highlight) { ctx = canvas.getContext("2d"); ctx.lineJoin = "round"; for (var zone of pcbdata.zones[layer]) { if (highlight && highlightedNet != zone.net) continue; ctx.strokeStyle = highlight ? defaultColor : settings.netColors[zone.net] || defaultColor; ctx.fillStyle = highlight ? defaultColor : settings.netColors[zone.net] || defaultColor; if (!zone.path2d) { zone.path2d = getPolygonsPath(zone); } ctx.fill(zone.path2d, zone.fillrule || "nonzero"); if (zone.width > 0) { ctx.lineWidth = zone.width; ctx.stroke(zone.path2d); } } } function clearCanvas(canvas, color = null) { var ctx = canvas.getContext("2d"); ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); if (color) { ctx.fillStyle = color; ctx.fillRect(0, 0, canvas.width, canvas.height); } else { if (!window.matchMedia("print").matches) ctx.clearRect(0, 0, canvas.width, canvas.height); } ctx.restore(); } function drawNets(canvas, layer, highlight) { var style = getComputedStyle(topmostdiv); if (settings.renderZones) { var zoneColor = style.getPropertyValue(highlight ? '--zone-color-highlight' : '--zone-color'); drawZones(canvas, layer, zoneColor, highlight); } if (settings.renderTracks) { var trackColor = style.getPropertyValue(highlight ? '--track-color-highlight' : '--track-color'); drawTracks(canvas, layer, trackColor, highlight); } if (highlight && settings.renderPads) { var padColor = style.getPropertyValue('--pad-color-highlight'); var padHoleColor = style.getPropertyValue('--pad-hole-color'); var ctx = canvas.getContext("2d"); for (var footprint of pcbdata.footprints) { // draw pads var padDrawn = false; for (var pad of footprint.pads) { if (highlightedNet != pad.net) continue; if (pad.layers.includes(layer)) { drawPad(ctx, pad, padColor, false); padDrawn = true; } } if (padDrawn) { // redraw all pad holes because some pads may overlap for (var pad of footprint.pads) { drawPadHole(ctx, pad, padHoleColor); } } } } } function drawHighlightsOnLayer(canvasdict, clear = true) { if (clear) { clearCanvas(canvasdict.highlight); } if (markedFootprints.size > 0 || highlightedFootprints.length > 0) { drawFootprints(canvasdict.highlight, canvasdict.layer, canvasdict.transform.s * canvasdict.transform.zoom, true); } if (highlightedNet !== null) { drawNets(canvasdict.highlight, canvasdict.layer, true); } } function drawHighlights() { drawHighlightsOnLayer(allcanvas.front); drawHighlightsOnLayer(allcanvas.back); } function drawBackground(canvasdict, clear = true) { if (clear) { clearCanvas(canvasdict.bg); clearCanvas(canvasdict.fab); clearCanvas(canvasdict.silk); } drawNets(canvasdict.bg, canvasdict.layer, false); drawFootprints(canvasdict.bg, canvasdict.layer, canvasdict.transform.s * canvasdict.transform.zoom, false); drawEdgeCuts(canvasdict.bg, canvasdict.transform.s * canvasdict.transform.zoom); var style = getComputedStyle(topmostdiv); var edgeColor = style.getPropertyValue('--silkscreen-edge-color'); var polygonColor = style.getPropertyValue('--silkscreen-polygon-color'); var textColor = style.getPropertyValue('--silkscreen-text-color'); if (settings.renderSilkscreen) { drawBgLayer( "silkscreen", canvasdict.silk, canvasdict.layer, canvasdict.transform.s * canvasdict.transform.zoom, edgeColor, polygonColor, textColor); } edgeColor = style.getPropertyValue('--fabrication-edge-color'); polygonColor = style.getPropertyValue('--fabrication-polygon-color'); textColor = style.getPropertyValue('--fabrication-text-color'); if (settings.renderFabrication) { drawBgLayer( "fabrication", canvasdict.fab, canvasdict.layer, canvasdict.transform.s * canvasdict.transform.zoom, edgeColor, polygonColor, textColor); } } function prepareCanvas(canvas, flip, transform) { var ctx = canvas.getContext("2d"); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.scale(transform.zoom, transform.zoom); ctx.translate(transform.panx, transform.pany); if (flip) { ctx.scale(-1, 1); } ctx.translate(transform.x, transform.y); ctx.rotate(deg2rad(settings.boardRotation + (flip && settings.offsetBackRotation ? - 180 : 0))); ctx.scale(transform.s, transform.s); } function prepareLayer(canvasdict) { var flip = (canvasdict.layer === "B"); for (var c of ["bg", "fab", "silk", "highlight"]) { prepareCanvas(canvasdict[c], flip, canvasdict.transform); } } function rotateVector(v, angle) { angle = deg2rad(angle); return [ v[0] * Math.cos(angle) - v[1] * Math.sin(angle), v[0] * Math.sin(angle) + v[1] * Math.cos(angle) ]; } function applyRotation(bbox, flip) { var corners = [ [bbox.minx, bbox.miny], [bbox.minx, bbox.maxy], [bbox.maxx, bbox.miny], [bbox.maxx, bbox.maxy], ]; corners = corners.map((v) => rotateVector(v, settings.boardRotation + (flip && settings.offsetBackRotation ? - 180 : 0))); return { minx: corners.reduce((a, v) => Math.min(a, v[0]), Infinity), miny: corners.reduce((a, v) => Math.min(a, v[1]), Infinity), maxx: corners.reduce((a, v) => Math.max(a, v[0]), -Infinity), maxy: corners.reduce((a, v) => Math.max(a, v[1]), -Infinity), } } function recalcLayerScale(layerdict, width, height) { var flip = (layerdict.layer === "B"); var bbox = applyRotation(pcbdata.edges_bbox, flip); var scalefactor = 0.98 * Math.min( width / (bbox.maxx - bbox.minx), height / (bbox.maxy - bbox.miny) ); if (scalefactor < 0.1) { scalefactor = 1; } layerdict.transform.s = scalefactor; if (flip) { layerdict.transform.x = -((bbox.maxx + bbox.minx) * scalefactor + width) * 0.5; } else { layerdict.transform.x = -((bbox.maxx + bbox.minx) * scalefactor - width) * 0.5; } layerdict.transform.y = -((bbox.maxy + bbox.miny) * scalefactor - height) * 0.5; for (var c of ["bg", "fab", "silk", "highlight"]) { canvas = layerdict[c]; canvas.width = width; canvas.height = height; canvas.style.width = (width / devicePixelRatio) + "px"; canvas.style.height = (height / devicePixelRatio) + "px"; } } function redrawCanvas(layerdict) { prepareLayer(layerdict); drawBackground(layerdict); drawHighlightsOnLayer(layerdict); } function resizeCanvas(layerdict) { var canvasdivid = { "F": "frontcanvas", "B": "backcanvas" } [layerdict.layer]; var width = document.getElementById(canvasdivid).clientWidth * devicePixelRatio; var height = document.getElementById(canvasdivid).clientHeight * devicePixelRatio; recalcLayerScale(layerdict, width, height); redrawCanvas(layerdict); } function resizeAll() { resizeCanvas(allcanvas.front); resizeCanvas(allcanvas.back); } function pointWithinDistanceToSegment(x, y, x1, y1, x2, y2, d) { var A = x - x1; var B = y - y1; var C = x2 - x1; var D = y2 - y1; var dot = A * C + B * D; var len_sq = C * C + D * D; var dx, dy; if (len_sq == 0) { // start and end of the segment coincide dx = x - x1; dy = y - y1; } else { var param = dot / len_sq; var xx, yy; if (param < 0) { xx = x1; yy = y1; } else if (param > 1) { xx = x2; yy = y2; } else { xx = x1 + param * C; yy = y1 + param * D; } dx = x - xx; dy = y - yy; } return dx * dx + dy * dy <= d * d; } function modulo(n, mod) { return ((n % mod) + mod) % mod; } function pointWithinDistanceToArc(x, y, xc, yc, radius, startangle, endangle, d) { var dx = x - xc; var dy = y - yc; var r_sq = dx * dx + dy * dy; var rmin = Math.max(0, radius - d); var rmax = radius + d; if (r_sq < rmin * rmin || r_sq > rmax * rmax) return false; var angle1 = modulo(deg2rad(startangle), 2 * Math.PI); var dx1 = xc + radius * Math.cos(angle1) - x; var dy1 = yc + radius * Math.sin(angle1) - y; if (dx1 * dx1 + dy1 * dy1 <= d * d) return true; var angle2 = modulo(deg2rad(endangle), 2 * Math.PI); var dx2 = xc + radius * Math.cos(angle2) - x; var dy2 = yc + radius * Math.sin(angle2) - y; if (dx2 * dx2 + dy2 * dy2 <= d * d) return true; var angle = modulo(Math.atan2(dy, dx), 2 * Math.PI); if (angle1 > angle2) return (angle >= angle2 || angle <= angle1); else return (angle >= angle1 && angle <= angle2); } function pointWithinPad(x, y, pad) { var v = [x - pad.pos[0], y - pad.pos[1]]; v = rotateVector(v, pad.angle); if (pad.offset) { v[0] -= pad.offset[0]; v[1] -= pad.offset[1]; } return emptyContext2d.isPointInPath(getCachedPadPath(pad), ...v); } function netHitScan(layer, x, y) { // Check track segments if (settings.renderTracks && pcbdata.tracks) { for (var track of pcbdata.tracks[layer]) { if ('radius' in track) { if (pointWithinDistanceToArc(x, y, ...track.center, track.radius, track.startangle, track.endangle, track.width / 2)) { return track.net; } } else { if (pointWithinDistanceToSegment(x, y, ...track.start, ...track.end, track.width / 2)) { return track.net; } } } } // Check pads if (settings.renderPads) { for (var footprint of pcbdata.footprints) { for (var pad of footprint.pads) { if (pad.layers.includes(layer) && pointWithinPad(x, y, pad)) { return pad.net; } } } } return null; } function pointWithinFootprintBbox(x, y, bbox) { var v = [x - bbox.pos[0], y - bbox.pos[1]]; v = rotateVector(v, bbox.angle); return bbox.relpos[0] <= v[0] && v[0] <= bbox.relpos[0] + bbox.size[0] && bbox.relpos[1] <= v[1] && v[1] <= bbox.relpos[1] + bbox.size[1]; } function bboxHitScan(layer, x, y) { var result = []; for (var i = 0; i < pcbdata.footprints.length; i++) { var footprint = pcbdata.footprints[i]; if (footprint.layer == layer) { if (pointWithinFootprintBbox(x, y, footprint.bbox)) { result.push(i); } } } return result; } function handlePointerDown(e, layerdict) { if (e.button != 0 && e.button != 1) { return; } e.preventDefault(); e.stopPropagation(); if (!e.hasOwnProperty("offsetX")) { // The polyfill doesn't set this properly e.offsetX = e.pageX - e.currentTarget.offsetLeft; e.offsetY = e.pageY - e.currentTarget.offsetTop; } layerdict.pointerStates[e.pointerId] = { distanceTravelled: 0, lastX: e.offsetX, lastY: e.offsetY, downTime: Date.now(), }; } function handleMouseClick(e, layerdict) { if (!e.hasOwnProperty("offsetX")) { // The polyfill doesn't set this properly e.offsetX = e.pageX - e.currentTarget.offsetLeft; e.offsetY = e.pageY - e.currentTarget.offsetTop; } var x = e.offsetX; var y = e.offsetY; var t = layerdict.transform; var flip = layerdict.layer === "B"; if (flip) { x = (devicePixelRatio * x / t.zoom - t.panx + t.x) / -t.s; } else { x = (devicePixelRatio * x / t.zoom - t.panx - t.x) / t.s; } y = (devicePixelRatio * y / t.zoom - t.y - t.pany) / t.s; var v = rotateVector([x, y], -settings.boardRotation + (flip && settings.offsetBackRotation ? - 180 : 0)); if ("nets" in pcbdata) { var net = netHitScan(layerdict.layer, ...v); if (net !== highlightedNet) { netClicked(net); } } if (highlightedNet === null) { var footprints = bboxHitScan(layerdict.layer, ...v); if (footprints.length > 0) { footprintsClicked(footprints); } } } function handlePointerLeave(e, layerdict) { e.preventDefault(); e.stopPropagation(); if (!settings.redrawOnDrag) { redrawCanvas(layerdict); } delete layerdict.pointerStates[e.pointerId]; } function resetTransform(layerdict) { layerdict.transform.panx = 0; layerdict.transform.pany = 0; layerdict.transform.zoom = 1; redrawCanvas(layerdict); } function handlePointerUp(e, layerdict) { if (!e.hasOwnProperty("offsetX")) { // The polyfill doesn't set this properly e.offsetX = e.pageX - e.currentTarget.offsetLeft; e.offsetY = e.pageY - e.currentTarget.offsetTop; } e.preventDefault(); e.stopPropagation(); if (e.button == 2) { // Reset pan and zoom on right click. resetTransform(layerdict); layerdict.anotherPointerTapped = false; return; } // We haven't necessarily had a pointermove event since the interaction started, so make sure we update this now var ptr = layerdict.pointerStates[e.pointerId]; ptr.distanceTravelled += Math.abs(e.offsetX - ptr.lastX) + Math.abs(e.offsetY - ptr.lastY); if (e.button == 0 && ptr.distanceTravelled < 10 && Date.now() - ptr.downTime <= 500) { if (Object.keys(layerdict.pointerStates).length == 1) { if (layerdict.anotherPointerTapped) { // This is the second pointer coming off of a two-finger tap resetTransform(layerdict); } else { // This is just a regular tap handleMouseClick(e, layerdict); } layerdict.anotherPointerTapped = false; } else { // This is the first finger coming off of what could become a two-finger tap layerdict.anotherPointerTapped = true; } } else { if (!settings.redrawOnDrag) { redrawCanvas(layerdict); } layerdict.anotherPointerTapped = false; } delete layerdict.pointerStates[e.pointerId]; } function handlePointerMove(e, layerdict) { if (!layerdict.pointerStates.hasOwnProperty(e.pointerId)) { return; } e.preventDefault(); e.stopPropagation(); if (!e.hasOwnProperty("offsetX")) { // The polyfill doesn't set this properly e.offsetX = e.pageX - e.currentTarget.offsetLeft; e.offsetY = e.pageY - e.currentTarget.offsetTop; } var thisPtr = layerdict.pointerStates[e.pointerId]; var dx = e.offsetX - thisPtr.lastX; var dy = e.offsetY - thisPtr.lastY; // If this number is low on pointer up, we count the action as a click thisPtr.distanceTravelled += Math.abs(dx) + Math.abs(dy); if (Object.keys(layerdict.pointerStates).length == 1) { // This is a simple drag layerdict.transform.panx += devicePixelRatio * dx / layerdict.transform.zoom; layerdict.transform.pany += devicePixelRatio * dy / layerdict.transform.zoom; } else if (Object.keys(layerdict.pointerStates).length == 2) { var otherPtr = Object.values(layerdict.pointerStates).filter((ptr) => ptr != thisPtr)[0]; var oldDist = Math.sqrt(Math.pow(thisPtr.lastX - otherPtr.lastX, 2) + Math.pow(thisPtr.lastY - otherPtr.lastY, 2)); var newDist = Math.sqrt(Math.pow(e.offsetX - otherPtr.lastX, 2) + Math.pow(e.offsetY - otherPtr.lastY, 2)); var scaleFactor = newDist / oldDist; if (scaleFactor != NaN) { layerdict.transform.zoom *= scaleFactor; var zoomd = (1 - scaleFactor) / layerdict.transform.zoom; layerdict.transform.panx += devicePixelRatio * otherPtr.lastX * zoomd; layerdict.transform.pany += devicePixelRatio * otherPtr.lastY * zoomd; } } thisPtr.lastX = e.offsetX; thisPtr.lastY = e.offsetY; if (settings.redrawOnDrag) { redrawCanvas(layerdict); } } function handleMouseWheel(e, layerdict) { e.preventDefault(); e.stopPropagation(); var t = layerdict.transform; var wheeldelta = e.deltaY; if (e.deltaMode == 1) { // FF only, scroll by lines wheeldelta *= 30; } else if (e.deltaMode == 2) { wheeldelta *= 300; } var m = Math.pow(1.1, -wheeldelta / 40); // Limit amount of zoom per tick. if (m > 2) { m = 2; } else if (m < 0.5) { m = 0.5; } t.zoom *= m; var zoomd = (1 - m) / t.zoom; t.panx += devicePixelRatio * e.offsetX * zoomd; t.pany += devicePixelRatio * e.offsetY * zoomd; redrawCanvas(layerdict); } function addMouseHandlers(div, layerdict) { div.addEventListener("pointerdown", function(e) { handlePointerDown(e, layerdict); }); div.addEventListener("pointermove", function(e) { handlePointerMove(e, layerdict); }); div.addEventListener("pointerup", function(e) { handlePointerUp(e, layerdict); }); var pointerleave = function(e) { handlePointerLeave(e, layerdict); } div.addEventListener("pointercancel", pointerleave); div.addEventListener("pointerleave", pointerleave); div.addEventListener("pointerout", pointerleave); div.onwheel = function(e) { handleMouseWheel(e, layerdict); } for (var element of [div, layerdict.bg, layerdict.fab, layerdict.silk, layerdict.highlight]) { element.addEventListener("contextmenu", function(e) { e.preventDefault(); }, false); } } function setRedrawOnDrag(value) { settings.redrawOnDrag = value; writeStorage("redrawOnDrag", value); } function setBoardRotation(value) { settings.boardRotation = value * 5; writeStorage("boardRotation", settings.boardRotation); document.getElementById("rotationDegree").textContent = settings.boardRotation; resizeAll(); } function setOffsetBackRotation(value) { settings.offsetBackRotation = value; writeStorage("offsetBackRotation", value); resizeAll(); } function initRender() { allcanvas = { front: { transform: { x: 0, y: 0, s: 1, panx: 0, pany: 0, zoom: 1, }, pointerStates: {}, anotherPointerTapped: false, bg: document.getElementById("F_bg"), fab: document.getElementById("F_fab"), silk: document.getElementById("F_slk"), highlight: document.getElementById("F_hl"), layer: "F", }, back: { transform: { x: 0, y: 0, s: 1, panx: 0, pany: 0, zoom: 1, }, pointerStates: {}, anotherPointerTapped: false, bg: document.getElementById("B_bg"), fab: document.getElementById("B_fab"), silk: document.getElementById("B_slk"), highlight: document.getElementById("B_hl"), layer: "B", } }; addMouseHandlers(document.getElementById("frontcanvas"), allcanvas.front); addMouseHandlers(document.getElementById("backcanvas"), allcanvas.back); }