UNPKG

gerber-plotter

Version:

Streaming Gerber / NC drill layer image plotter

471 lines (408 loc) 11.9 kB
// operate the plotter 'use strict' var boundingBox = require('./_box') var HALF_PI = Math.PI / 2 var PI = Math.PI var TWO_PI = Math.PI * 2 var THREE_HALF_PI = (3 * Math.PI) / 2 // flash operation // returns a bounding box for the operation var flash = function(coord, tool, region, plotter) { // no flashing allowed in region mode if (region) { plotter._warn('flash in region ignored') return boundingBox.new() } // warn if tool was not defined if (!tool) { plotter._warn('flash with unknown tool ignored') return boundingBox.new() } // push the pad shape if needed if (!tool.flashed) { tool.flashed = true plotter.push({type: 'shape', tool: tool.code, shape: tool.pad}) } plotter.push({type: 'pad', tool: tool.code, x: coord[0], y: coord[1]}) return boundingBox.translate(tool.box, coord) } // given a start, end, direction, arc quadrant mode, and list of potential centers, find the // angles of the start and end points, the sweep angle, and the center var findCenterAndAngles = function(start, end, mode, arc, centers) { var thetaStart var thetaEnd var sweep var candidate var center while (center == null && centers.length > 0) { candidate = centers.pop() thetaStart = Math.atan2(start[1] - candidate[1], start[0] - candidate[0]) thetaEnd = Math.atan2(end[1] - candidate[1], end[0] - candidate[0]) // in clockwise mode, ensure the start is greater than the end and check the sweep // do the opposite for counter-clockwise if (mode === 'cw') { thetaStart = thetaStart >= thetaEnd ? thetaStart : thetaStart + TWO_PI } else { thetaEnd = thetaEnd >= thetaStart ? thetaEnd : thetaEnd + TWO_PI } sweep = Math.abs(thetaStart - thetaEnd) // in single quadrant mode, the center is only valid if the sweep is less than 90 degrees // in multiquandrant mode there's only one candidate; we're within spec to assume it's good if (arc === 's') { if (sweep <= HALF_PI) { center = candidate } } else { center = candidate } } if (center == null) { return undefined } // ensure the thetas are [0, TWO_PI) thetaStart = thetaStart >= 0 ? thetaStart : thetaStart + TWO_PI thetaStart = thetaStart < TWO_PI ? thetaStart : thetaStart - TWO_PI thetaEnd = thetaEnd >= 0 ? thetaEnd : thetaEnd + TWO_PI thetaEnd = thetaEnd < TWO_PI ? thetaEnd : thetaEnd - TWO_PI return { center: center, sweep: sweep, start: start.concat(thetaStart), end: end.concat(thetaEnd), } } var arcBox = function(cenAndAngles, r, region, tool, dir) { var startPoint = cenAndAngles.start var endPoint = cenAndAngles.end var center = cenAndAngles.center var sweep = cenAndAngles.sweep var start var end // normalize direction to counter-clockwise if (dir === 'cw') { start = endPoint[2] end = startPoint[2] } else { start = startPoint[2] end = endPoint[2] } // get bounding box definition points var points = [startPoint, endPoint] // check for sweep past 0 degrees if (start > end || sweep === TWO_PI) { points.push([center[0] + r, center[1]]) } // rotate to check for sweep past 90 degrees start = start >= HALF_PI ? start - HALF_PI : start + THREE_HALF_PI end = end >= HALF_PI ? end - HALF_PI : end + THREE_HALF_PI if (start > end || sweep === TWO_PI) { points.push([center[0], center[1] + r]) } // rotate again to check for sweep past 180 degrees start = start >= HALF_PI ? start - HALF_PI : start + THREE_HALF_PI end = end >= HALF_PI ? end - HALF_PI : end + THREE_HALF_PI if (start > end || sweep === TWO_PI) { points.push([center[0] - r, center[1]]) } // rotate again to check for sweep past 270 degrees start = start >= HALF_PI ? start - HALF_PI : start + THREE_HALF_PI end = end >= HALF_PI ? end - HALF_PI : end + THREE_HALF_PI if (start > end || sweep === TWO_PI) { points.push([center[0], center[1] - r]) } return points.reduce(function(result, m) { if (!region) { var mBox = boundingBox.translate(tool.box, m) return boundingBox.add(result, mBox) } return boundingBox.addPoint(result, m) }, boundingBox.new()) } var roundToZero = function(number, epsilon) { return number >= epsilon ? number : 0 } // find the center of an arc given its endpoints and its radius // assume the arc is <= 180 degress // thank you this guy: http://math.stackexchange.com/a/87912 var arcCenterFromRadius = function(start, end, mode, epsilon, radius) { var sign = mode === 'ccw' ? 1 : -1 var xAve = (start[0] + end[0]) / 2 var yAve = (start[1] + end[1]) / 2 var deltaX = end[0] - start[1] var deltaY = end[1] - start[1] var distance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)) var halfDistance = distance / 2 var squareDifference = Math.sqrt( Math.pow(radius, 2) - Math.pow(halfDistance, 2) ) var xOffset = (-sign * deltaY * squareDifference) / distance var yOffset = (sign * deltaX * squareDifference) / distance return [ [ roundToZero(xAve + xOffset, epsilon), roundToZero(yAve + yOffset, epsilon), ], ] } var drawArc = function( start, end, offset, tool, mode, arc, region, epsilon, pathGraph, plotter ) { // get the radius of the arc from the offsets var r = offset[2] || Math.sqrt(Math.pow(offset[0], 2) + Math.pow(offset[1], 2)) // potential candidates for the arc center // in single quadrant mode, all offset signs are implicit, so we need to check a few var candidates = [] var xCandidates = [] var yCandidates = [] if (offset[0] && arc === 's') { xCandidates.push(start[0] + offset[0], start[0] - offset[0]) } else { xCandidates.push(start[0] + offset[0]) } if (offset[1] && arc === 's') { yCandidates.push(start[1] + offset[1], start[1] - offset[1]) } else { yCandidates.push(start[1] + offset[1]) } for (var i = 0; i < xCandidates.length; i++) { for (var j = 0; j < yCandidates.length; j++) { candidates.push([xCandidates[i], yCandidates[j]]) } } // find valid centers by comparing the distance to start and end for equality with the radius var validCenters if (offset[2]) { arc = 'm' validCenters = arcCenterFromRadius(start, end, mode, epsilon, offset[2]) } else if (arc === 's') { validCenters = candidates.filter(function(c) { var startDist = Math.sqrt( Math.pow(c[0] - start[0], 2) + Math.pow(c[1] - start[1], 2) ) var endDist = Math.sqrt( Math.pow(c[0] - end[0], 2) + Math.pow(c[1] - end[1], 2) ) return ( Math.abs(startDist - r) <= epsilon && Math.abs(endDist - r) <= epsilon ) }) } else { validCenters = candidates } var cenAndAngles = findCenterAndAngles(start, end, mode, arc, validCenters) // edge case: matching start and end in multi quadrant mode is a full circle if (arc === 'm' && start[0] === end[0] && start[1] === end[1]) { cenAndAngles.sweep = TWO_PI } var box = boundingBox.new() if (cenAndAngles != null) { pathGraph.add({ type: 'arc', start: cenAndAngles.start, end: cenAndAngles.end, center: cenAndAngles.center, sweep: cenAndAngles.sweep, radius: r, dir: mode, }) box = arcBox(cenAndAngles, r, region, tool, mode) } else { plotter._warn('skipping impossible arc') } return box } var drawLine = function(start, end, tool, region, pathGraph) { pathGraph.add({type: 'line', start: start, end: end}) if (!region) { var startBox = boundingBox.translate(tool.box, start) var endBox = boundingBox.translate(tool.box, end) return boundingBox.add(startBox, endBox) } var box = boundingBox.new() box = boundingBox.addPoint(box, start) box = boundingBox.addPoint(box, end) return box } // interpolate a rectangle and emit the fill immdeiately var interpolateRect = function(start, end, tool, pathGraph, plotter) { var hWidth = tool.trace[0] / 2 var hHeight = tool.trace[1] / 2 var theta = Math.atan2(end[1] - start[1], end[0] - start[0]) var sXMin = start[0] - hWidth var sXMax = start[0] + hWidth var sYMin = start[1] - hHeight var sYMax = start[1] + hHeight var eXMin = end[0] - hWidth var eXMax = end[0] + hWidth var eYMin = end[1] - hHeight var eYMax = end[1] + hHeight var points = [] // no movement if (start[0] === end[0] && start[1] === end[1]) { points.push([sXMin, sYMin], [sXMax, sYMin], [sXMax, sYMax], [sXMin, sYMax]) } else if (theta >= 0 && theta < HALF_PI) { // first quadrant move points.push( [sXMin, sYMin], [sXMax, sYMin], [eXMax, eYMin], [eXMax, eYMax], [eXMin, eYMax], [sXMin, sYMax] ) } else if (theta >= HALF_PI && theta <= PI) { // second quadrant move points.push( [sXMax, sYMin], [sXMax, sYMax], [eXMax, eYMax], [eXMin, eYMax], [eXMin, eYMin], [sXMin, sYMin] ) } else if (theta >= -PI && theta < -HALF_PI) { // third quadrant move points.push( [sXMax, sYMax], [sXMin, sYMax], [eXMin, eYMax], [eXMin, eYMin], [eXMax, eYMin], [sXMax, sYMin] ) } else { // fourth quadrant move points.push( [sXMin, sYMax], [sXMin, sYMin], [eXMin, eYMin], [eXMax, eYMin], [eXMax, eYMax], [sXMax, sYMax] ) } points.forEach(function(p, i) { var j = i < points.length - 1 ? i + 1 : 0 pathGraph.add({type: 'line', start: p, end: points[j]}) }) plotter._finishPath() return boundingBox.add( boundingBox.translate(tool.box, start), boundingBox.translate(tool.box, end) ) } // interpolate operation // returns a bounding box for the operation var interpolate = function( start, end, offset, tool, mode, arc, region, epsilon, pathGraph, plotter ) { var strokableTool = region || (tool && tool.trace.length > 0) var arcableTool = region || (tool && tool.trace.length === 1) var toolCode = tool ? tool.code : '[NO TOOL SET]' if (!strokableTool) { plotter._warn( 'tool ' + toolCode + ' is not strokable; ignoring interpolate' ) return boundingBox.new() } if (mode === 'i') { // add a line to the path normally if region mode is on or the tool is a circle if (region || tool.trace.length === 1) { return drawLine(start, end, tool, region, pathGraph) } // else, the tool is a rectangle, which needs a special interpolation function return interpolateRect(start, end, tool, pathGraph, plotter) } // else, make sure we're allowed to be drawing an arc, then draw an arc if (!arcableTool) { plotter._warn( 'cannot draw arc with non-circular tool ' + toolCode + '; ignoring interpolate' ) return boundingBox.new() } return drawArc( start, end, offset, tool, mode, arc, region, epsilon, pathGraph, plotter ) } // takes the start point, the op type, the op coords, the tool, and the push function // returns the new plotter position var operate = function( type, coord, start, tool, mode, arc, region, pathGraph, epsilon, plotter ) { var end = [ coord.x != null ? coord.x : start[0], coord.y != null ? coord.y : start[1], ] var offset = [ coord.i != null ? coord.i : 0, coord.j != null ? coord.j : 0, coord.a, ] var box switch (type) { case 'flash': box = flash(end, tool, region, plotter) break case 'int': box = interpolate( start, end, offset, tool, mode, arc, region, epsilon, pathGraph, plotter ) break default: box = boundingBox.new() break } return { pos: end, box: box, } } module.exports = operate