@tracespace/plotter
Version:
Plot @tracespace/parser ASTs into image trees.
1 lines • 68.7 kB
Source Map (JSON)
{"version":3,"file":"tracespace-plotter.umd.cjs","sources":["../src/tree.ts","../src/coordinate-math.ts","../src/bounding-box.ts","../src/options.ts","../src/tool-store.ts","../src/location-store.ts","../src/graphic-plotter/shapes.ts","../src/graphic-plotter/plot-shape.ts","../src/graphic-plotter/plot-rect-path.ts","../src/graphic-plotter/plot-path.ts","../src/graphic-plotter/plot-macro.ts","../src/graphic-plotter/index.ts","../src/index.ts"],"sourcesContent":["import type {Node, Parent} from 'unist'\n\nimport type {UnitsType} from '@tracespace/parser'\n\nexport const IMAGE = 'image'\nexport const IMAGE_SHAPE = 'imageShape'\nexport const IMAGE_PATH = 'imagePath'\nexport const IMAGE_REGION = 'imageRegion'\n\nexport const LINE = 'line'\nexport const ARC = 'arc'\n\nexport const CIRCLE = 'circle'\nexport const RECTANGLE = 'rectangle'\nexport const POLYGON = 'polygon'\nexport const OUTLINE = 'outline'\nexport const LAYERED_SHAPE = 'layeredShape'\n\nexport type Position = [x: number, y: number]\n\nexport type ArcPosition = [x: number, y: number, theta: number]\n\nexport type SizeEnvelope = [x1: number, y1: number, x2: number, y2: number] | []\n\nexport type ImageNode = ImageTree | ImageShape | ImagePath | ImageRegion\n\nexport interface CircleShape {\n type: typeof CIRCLE\n cx: number\n cy: number\n r: number\n}\n\nexport interface RectangleShape {\n type: typeof RECTANGLE\n x: number\n y: number\n xSize: number\n ySize: number\n r?: number\n}\n\nexport interface PolygonShape {\n type: typeof POLYGON\n points: Position[]\n}\n\nexport interface OutlineShape {\n type: typeof OUTLINE\n segments: PathSegment[]\n}\n\nexport interface LayeredShape {\n type: typeof LAYERED_SHAPE\n shapes: ErasableShape[]\n}\n\nexport type HoleShape = CircleShape | RectangleShape\n\nexport type SimpleShape =\n | CircleShape\n | RectangleShape\n | PolygonShape\n | OutlineShape\n\nexport type Shape = SimpleShape | LayeredShape\n\nexport type ErasableShape = SimpleShape & {erase?: boolean}\n\nexport type ImageGraphic = ImageShape | ImagePath | ImageRegion\n\nexport interface ImageTree extends Parent {\n type: typeof IMAGE\n units: UnitsType\n size: SizeEnvelope\n children: ImageGraphic[]\n}\n\nexport interface ImageShape extends Node {\n type: typeof IMAGE_SHAPE\n shape: Shape\n}\n\nexport interface ImagePath extends Node {\n type: typeof IMAGE_PATH\n width: number\n segments: PathSegment[]\n}\n\nexport interface ImageRegion extends Node {\n type: typeof IMAGE_REGION\n segments: PathSegment[]\n}\n\nexport type PathSegment = PathLineSegment | PathArcSegment\n\nexport interface PathLineSegment {\n type: typeof LINE\n start: Position\n end: Position\n}\n\nexport interface PathArcSegment {\n type: typeof ARC\n start: ArcPosition\n end: ArcPosition\n center: Position\n radius: number\n}\n","// Mathematical procedures\nimport type {Position} from './tree'\n\nexport const {PI} = Math\nexport const HALF_PI = PI / 2\nexport const THREE_HALF_PI = 3 * HALF_PI\nexport const TWO_PI = 2 * PI\n\nexport function limitAngle(theta: number): number {\n if (theta >= 0 && theta <= TWO_PI) return theta\n if (theta < 0) return theta + TWO_PI\n if (theta > TWO_PI) return theta - TWO_PI\n return limitAngle(theta)\n}\n\nexport function rotateQuadrant(theta: number): number {\n return theta >= HALF_PI ? theta - HALF_PI : theta + THREE_HALF_PI\n}\n\nexport function degreesToRadians(degrees: number): number {\n return (degrees * Math.PI) / 180\n}\n\nexport function rotateAndShift(\n point: Position,\n shift: Position,\n degrees = 0\n): Position {\n const rotation = degreesToRadians(degrees)\n const [sin, cos] = [Math.sin(rotation), Math.cos(rotation)]\n const [x, y] = point\n const nextX = x * cos - y * sin + shift[0]\n const nextY = x * sin + y * cos + shift[1]\n\n return [nextX, nextY]\n}\n\nexport function positionsEqual(a: number[], b: number[]): boolean {\n return a[0] === b[0] && a[1] === b[1]\n}\n","import * as Tree from './tree'\nimport {TWO_PI, limitAngle, rotateQuadrant} from './coordinate-math'\nimport type {SizeEnvelope as Box, Position, ArcPosition} from './tree'\n\nexport type {SizeEnvelope as Box} from './tree'\n\nexport function isEmpty(box: Box): box is [] {\n return box.length === 0\n}\n\nexport function empty(): Box {\n return []\n}\n\nexport function add(a: Box, b: Box): Box {\n if (isEmpty(a)) return b\n if (isEmpty(b)) return a\n\n return [\n Math.min(a[0], b[0]),\n Math.min(a[1], b[1]),\n Math.max(a[2], b[2]),\n Math.max(a[3], b[3]),\n ]\n}\n\nexport function sum(boxes: Box[]): Box {\n return boxes.reduce(add, empty())\n}\n\nexport function fromGraphics(graphics: Tree.ImageGraphic[]): Box {\n return sum(graphics.map(fromGraphic))\n}\n\nexport function fromGraphic(graphic: Tree.ImageGraphic): Box {\n return graphic.type === Tree.IMAGE_SHAPE\n ? fromShape(graphic.shape)\n : fromPath(\n graphic.segments,\n graphic.type === Tree.IMAGE_PATH ? graphic.width : undefined\n )\n}\n\nexport function fromShape(shape: Tree.Shape): Box {\n switch (shape.type) {\n case Tree.CIRCLE: {\n const {cx, cy, r} = shape\n return fromPosition([cx, cy], r)\n }\n\n case Tree.RECTANGLE: {\n const {x, y, xSize, ySize} = shape\n return [x, y, x + xSize, y + ySize]\n }\n\n case Tree.POLYGON: {\n return sum(shape.points.map(p => fromPosition(p)))\n }\n\n case Tree.OUTLINE: {\n return fromPath(shape.segments)\n }\n\n case Tree.LAYERED_SHAPE: {\n return sum(shape.shapes.filter(({erase}) => !erase).map(fromShape))\n }\n }\n}\n\nexport function fromPath(segments: Tree.PathSegment[], width = 0): Box {\n const rTool = width / 2\n const keyPoints: Array<Tree.Position | Tree.ArcPosition> = []\n\n for (const segment of segments) {\n keyPoints.push(segment.start, segment.end)\n\n if (segment.type === Tree.ARC) {\n const {start, end, center, radius} = segment\n const sweep = Math.abs(end[2] - start[2])\n\n // Normalize direction to counter-clockwise\n let [thetaStart, thetaEnd] =\n end[2] > start[2] ? [start[2], end[2]] : [end[2], start[2]]\n\n thetaStart = limitAngle(thetaStart)\n thetaEnd = limitAngle(thetaEnd)\n\n const axisPoints: Tree.Position[] = [\n [center[0] + radius, center[1]],\n [center[0], center[1] + radius],\n [center[0] - radius, center[1]],\n [center[0], center[1] - radius],\n ]\n\n for (const p of axisPoints) {\n if (thetaStart > thetaEnd || sweep === TWO_PI) {\n keyPoints.push(p)\n }\n\n // Rotate to check for next axis key point\n thetaStart = rotateQuadrant(thetaStart)\n thetaEnd = rotateQuadrant(thetaEnd)\n }\n }\n }\n\n return sum(keyPoints.map(p => fromPosition(p, rTool)))\n}\n\nfunction fromPosition(position: Position | ArcPosition, radius = 0): Box {\n return [\n position[0] - radius,\n position[1] - radius,\n position[0] + radius,\n position[1] + radius,\n ]\n}\n","import type {\n GerberTree,\n UnitsType,\n Format,\n ZeroSuppression,\n} from '@tracespace/parser'\nimport {\n UNITS,\n COORDINATE_FORMAT,\n GRAPHIC,\n COMMENT,\n LEADING,\n TRAILING,\n IN,\n} from '@tracespace/parser'\n\nexport interface PlotOptions {\n units: UnitsType\n coordinateFormat: Format\n zeroSuppression: ZeroSuppression\n}\n\nconst FORMAT_COMMENT_RE = /FORMAT={?(\\d):(\\d)/\n\nexport function getPlotOptions(tree: GerberTree): PlotOptions {\n const {children: treeNodes} = tree\n let units: UnitsType | null = null\n let coordinateFormat: Format | null = null\n let zeroSuppression: ZeroSuppression | null = null\n let index = 0\n\n while (\n index < treeNodes.length &&\n (units === null || coordinateFormat === null || zeroSuppression === null)\n ) {\n const node = treeNodes[index]\n\n switch (node.type) {\n case UNITS: {\n units = node.units\n break\n }\n\n case COORDINATE_FORMAT: {\n coordinateFormat = node.format\n zeroSuppression = node.zeroSuppression\n break\n }\n\n case GRAPHIC: {\n const {coordinates} = node\n\n for (const coordinate of Object.values(coordinates)) {\n if (zeroSuppression !== null) break\n\n if (coordinate!.endsWith('0') || coordinate!.includes('.')) {\n zeroSuppression = LEADING\n } else if (coordinate!.startsWith('0')) {\n zeroSuppression = TRAILING\n }\n }\n\n break\n }\n\n case COMMENT: {\n const {comment} = node\n const formatMatch = FORMAT_COMMENT_RE.exec(comment)\n\n if (/suppress trailing/i.test(comment)) {\n zeroSuppression = TRAILING\n } else if (/(suppress leading|keep zeros)/i.test(comment)) {\n zeroSuppression = LEADING\n }\n\n if (formatMatch) {\n coordinateFormat = [Number(formatMatch[1]), Number(formatMatch[2])]\n }\n\n break\n }\n\n default:\n }\n\n index += 1\n }\n\n return {\n units: units ?? IN,\n coordinateFormat: coordinateFormat ?? [2, 4],\n zeroSuppression: zeroSuppression ?? LEADING,\n }\n}\n","// Tool store\n// Keeps track of the defined tools, defined macros, and the current tool\nimport type {\n GerberNode,\n SimpleShape,\n HoleShape,\n MacroBlock,\n} from '@tracespace/parser'\nimport {\n MACRO_SHAPE,\n TOOL_CHANGE,\n TOOL_DEFINITION,\n TOOL_MACRO,\n} from '@tracespace/parser'\n\nexport const SIMPLE_TOOL = 'simpleTool'\n\nexport const MACRO_TOOL = 'macroTool'\n\nexport interface SimpleTool {\n type: typeof SIMPLE_TOOL\n shape: SimpleShape\n hole?: HoleShape\n}\n\nexport interface MacroTool {\n type: typeof MACRO_TOOL\n macro: MacroBlock[]\n variableValues: number[]\n}\n\nexport type Tool = SimpleTool | MacroTool\n\nexport interface ToolStore {\n use(node: GerberNode): Tool | undefined\n}\n\nexport function createToolStore(): ToolStore {\n return Object.create(ToolStorePrototype)\n}\n\ninterface ToolStoreState {\n _currentToolCode: string | undefined\n _toolsByCode: Partial<Record<string, Tool>>\n _macrosByName: Partial<Record<string, MacroBlock[]>>\n}\n\nconst ToolStorePrototype: ToolStore & ToolStoreState = {\n _currentToolCode: undefined,\n _toolsByCode: {},\n _macrosByName: {},\n\n use(node: GerberNode): Tool | undefined {\n if (node.type === TOOL_MACRO) {\n this._macrosByName[node.name] = node.children\n }\n\n if (node.type === TOOL_DEFINITION) {\n const {shape, hole} = node\n const tool: Tool =\n shape.type === MACRO_SHAPE\n ? {\n type: MACRO_TOOL,\n macro: this._macrosByName[shape.name] ?? [],\n variableValues: shape.variableValues,\n }\n : {type: SIMPLE_TOOL, shape, ...(hole && {hole})}\n\n this._toolsByCode[node.code] = tool\n }\n\n if (node.type === TOOL_DEFINITION || node.type === TOOL_CHANGE) {\n this._currentToolCode = node.code\n }\n\n return typeof this._currentToolCode === 'string'\n ? this._toolsByCode[this._currentToolCode]\n : undefined\n },\n}\n","// Track the location of the plotter and parse coordinate strings\nimport type {GerberNode} from '@tracespace/parser'\nimport {GRAPHIC, TRAILING} from '@tracespace/parser'\n\nimport type {PlotOptions} from './options'\n\nexport interface Point {\n x: number\n y: number\n}\n\nexport interface ArcOffsets {\n i: number\n j: number\n a: number\n}\n\nexport interface Location {\n startPoint: Point\n endPoint: Point\n arcOffsets: ArcOffsets\n}\n\nexport interface LocationStore {\n use(node: GerberNode, options: PlotOptions): Location\n}\n\nexport function createLocationStore(): LocationStore {\n return Object.create(LocationStorePrototype)\n}\n\ninterface LocationStoreState {\n _DEFAULT_ARC_OFFSETS: ArcOffsets\n _previousPoint: Point\n}\n\nconst LocationStorePrototype: LocationStore & LocationStoreState = {\n _DEFAULT_ARC_OFFSETS: {i: 0, j: 0, a: 0},\n _previousPoint: {x: 0, y: 0},\n\n use(node: GerberNode, options: PlotOptions): Location {\n let arcOffsets = this._DEFAULT_ARC_OFFSETS\n let startPoint = this._previousPoint\n let endPoint = startPoint\n\n if (node.type === GRAPHIC) {\n const {coordinates} = node\n const x0 = parseCoordinate(coordinates.x0, startPoint.x, options)\n const y0 = parseCoordinate(coordinates.y0, startPoint.y, options)\n const x = parseCoordinate(coordinates.x, x0, options)\n const y = parseCoordinate(coordinates.y, y0, options)\n const i = parseCoordinate(coordinates.i, 0, options)\n const j = parseCoordinate(coordinates.j, 0, options)\n const a = parseCoordinate(coordinates.a, 0, options)\n\n if (startPoint.x !== x0 || startPoint.y !== y0) {\n startPoint = {x: x0, y: y0}\n }\n\n if (endPoint.x !== x || endPoint.y !== y) {\n endPoint = {x, y}\n }\n\n if (i !== 0 || j !== 0 || a !== 0) {\n arcOffsets = {i, j, a}\n }\n }\n\n this._previousPoint = endPoint\n return {startPoint, endPoint, arcOffsets}\n },\n}\n\nfunction parseCoordinate(\n coordinate: string | undefined,\n defaultValue: number,\n options: PlotOptions\n): number {\n if (typeof coordinate !== 'string') {\n return defaultValue\n }\n\n if (coordinate.includes('.') || coordinate === '0') {\n return Number(coordinate)\n }\n\n const {coordinateFormat, zeroSuppression} = options\n const [integerPlaces, decimalPlaces] = coordinateFormat\n\n const [sign, signlessCoordinate] =\n coordinate.startsWith('+') || coordinate.startsWith('-')\n ? [coordinate[0], coordinate.slice(1)]\n : ['+', coordinate]\n\n const digits = integerPlaces + decimalPlaces\n const paddedCoordinate =\n zeroSuppression === TRAILING\n ? signlessCoordinate.padEnd(digits, '0')\n : signlessCoordinate.padStart(digits, '0')\n\n const leading = paddedCoordinate.slice(0, integerPlaces)\n const trailing = paddedCoordinate.slice(integerPlaces)\n\n return Number(`${sign}${leading}.${trailing}`)\n}\n","import type {SimpleShape} from '@tracespace/parser'\nimport {CIRCLE, RECTANGLE, OBROUND, POLYGON} from '@tracespace/parser'\n\nimport {\n HALF_PI,\n PI,\n THREE_HALF_PI,\n TWO_PI,\n degreesToRadians,\n} from '../coordinate-math'\n\nimport * as Tree from '../tree'\nimport type {Point} from '../location-store'\n\nexport function createShape(\n shape: SimpleShape,\n point: Point\n): Tree.SimpleShape {\n const {x, y} = point\n\n switch (shape.type) {\n case CIRCLE: {\n const {diameter} = shape\n return {type: Tree.CIRCLE, cx: x, cy: y, r: diameter / 2}\n }\n\n case RECTANGLE:\n case OBROUND: {\n const {xSize, ySize} = shape\n const xHalf = xSize / 2\n const yHalf = ySize / 2\n const rectangle: Tree.RectangleShape = {\n type: Tree.RECTANGLE,\n x: x - xHalf,\n y: y - yHalf,\n xSize,\n ySize,\n }\n\n if (shape.type === OBROUND) {\n rectangle.r = Math.min(xHalf, yHalf)\n }\n\n return rectangle\n }\n\n case POLYGON: {\n const {diameter, rotation, vertices} = shape\n const r = diameter / 2\n const offset = degreesToRadians(rotation ?? 0)\n const step = TWO_PI / vertices\n const points = Array.from({length: vertices}).map<Tree.Position>(\n (_, i) => {\n const theta = step * i + offset\n const pointX = x + r * Math.cos(theta)\n const pointY = y + r * Math.sin(theta)\n return [pointX, pointY]\n }\n )\n\n return {type: Tree.POLYGON, points}\n }\n }\n}\n\nexport function shapeToSegments(shape: Tree.SimpleShape): Tree.PathSegment[] {\n if (shape.type === Tree.CIRCLE) {\n const {cx, cy, r} = shape\n return [\n {\n type: Tree.ARC,\n start: [cx + r, cy, 0],\n end: [cx + r, cy, TWO_PI],\n center: [cx, cy],\n radius: r,\n },\n ]\n }\n\n if (shape.type === Tree.RECTANGLE) {\n const {x, y, xSize, ySize, r} = shape\n\n if (r === xSize / 2) {\n return [\n {\n type: Tree.LINE,\n start: [x + xSize, y + r],\n end: [x + xSize, y + ySize - r],\n },\n {\n type: Tree.ARC,\n start: [x + xSize, y + ySize - r, 0],\n end: [x, y + ySize - r, PI],\n center: [x + r, y + ySize - r],\n radius: r,\n },\n {type: Tree.LINE, start: [x, y + ySize - r], end: [x, y + r]},\n {\n type: Tree.ARC,\n start: [x, y + r, PI],\n end: [x + xSize, y + r, TWO_PI],\n center: [x + r, y + r],\n radius: r,\n },\n ]\n }\n\n if (r === ySize / 2) {\n return [\n {type: Tree.LINE, start: [x + r, y], end: [x + xSize - r, y]},\n {\n type: Tree.ARC,\n start: [x + xSize - r, y, -HALF_PI],\n end: [x + xSize - r, y + ySize, HALF_PI],\n center: [x + xSize - r, y + r],\n radius: r,\n },\n {\n type: Tree.LINE,\n start: [x + xSize - r, y + ySize],\n end: [x + r, y + ySize],\n },\n {\n type: Tree.ARC,\n start: [x + r, y + ySize, HALF_PI],\n end: [x + r, y, THREE_HALF_PI],\n center: [x + r, y + r],\n radius: r,\n },\n ]\n }\n\n return [\n {type: Tree.LINE, start: [x, y], end: [x + xSize, y]},\n {type: Tree.LINE, start: [x + xSize, y], end: [x + xSize, y + ySize]},\n {type: Tree.LINE, start: [x + xSize, y + ySize], end: [x, y + ySize]},\n {type: Tree.LINE, start: [x, y + ySize], end: [x, y]},\n ]\n }\n\n if (shape.type === Tree.POLYGON) {\n return shape.points.map((start, i) => {\n const endIndex = i < shape.points.length - 1 ? i + 1 : 0\n return {type: Tree.LINE, start, end: shape.points[endIndex]}\n })\n }\n\n return shape.segments\n}\n","import {MACRO_SHAPE} from '@tracespace/parser'\n\nimport * as Tree from '../tree'\nimport type {Location} from '../location-store'\nimport type {SimpleTool} from '../tool-store'\n\nimport {createShape, shapeToSegments} from './shapes'\n\nexport function plotShape(tool: SimpleTool, location: Location): Tree.Shape {\n const {shape: toolShape, hole: toolHole} = tool\n const shape = createShape(toolShape, location.endPoint)\n const holeShape = toolHole\n ? createShape(toolHole, location.endPoint)\n : undefined\n\n return holeShape === undefined\n ? shape\n : {\n type: Tree.OUTLINE,\n segments: [...shapeToSegments(shape), ...shapeToSegments(holeShape)],\n }\n}\n","// Functions for stroking rectangular tools\n// Stroking rectangular tools is deprecated by the Gerber spec\n// This functionality may be dropped and replaced with a warning\nimport type {Rectangle} from '@tracespace/parser'\n\nimport * as Tree from '../tree'\nimport {positionsEqual, HALF_PI, PI} from '../coordinate-math'\n\n// Rectangular tools make interesting stroke geometry; see the Gerber spec\n// for graphics and examples\nexport function plotRectPath(\n segments: Tree.PathSegment[],\n shape: Rectangle\n): Tree.ImageShape {\n const shapes = segments\n .filter((s): s is Tree.PathLineSegment => s.type === Tree.LINE)\n .map(segment => plotRectPathSegment(segment, shape))\n\n return {type: Tree.IMAGE_SHAPE, shape: {type: Tree.LAYERED_SHAPE, shapes}}\n}\n\nfunction plotRectPathSegment(\n segment: Tree.PathLineSegment,\n shape: Rectangle\n): Tree.PolygonShape {\n // Since a rectangular stroke like this is so unique to Gerber, it's easier\n // for downstream graphics generators if we calculate the boundaries of the\n // correct shape and emit a region rather than a path with a width (which is\n // what we do for circle tools)\n const {start, end} = segment\n const [sx, sy] = start\n const [ex, ey] = end\n const [xOffset, yOffset] = [shape.xSize / 2, shape.ySize / 2]\n const theta = Math.atan2(ey - sy, ex - ey)\n\n const [sxMin, sxMax] = [sx - xOffset, sx + xOffset]\n const [syMin, syMax] = [sy - yOffset, sy + yOffset]\n const [exMin, exMax] = [ex - xOffset, ex + xOffset]\n const [eyMin, eyMax] = [ey - yOffset, ey + yOffset]\n\n // Go through the quadrants of the XY plane centered about start to decide\n // which segments define the boundaries of the stroke shape\n let points: Tree.Position[] = []\n if (positionsEqual(start, end)) {\n points = [\n [sxMin, syMin],\n [sxMax, syMin],\n [exMax, eyMin],\n [exMax, eyMax],\n [exMin, eyMax],\n [sxMin, syMax],\n ]\n } else if (theta >= 0 && theta < HALF_PI) {\n // First quadrant move\n points = [\n [sxMin, syMin],\n [sxMax, syMin],\n [exMax, eyMin],\n [exMax, eyMax],\n [exMin, eyMax],\n [sxMin, syMax],\n ]\n } else if (theta >= HALF_PI && theta <= PI) {\n // Second quadrant move\n points = [\n [sxMax, syMin],\n [sxMax, syMax],\n [exMax, eyMax],\n [exMin, eyMax],\n [exMin, eyMin],\n [sxMin, syMin],\n ]\n } else if (theta >= -PI && theta < -HALF_PI) {\n // Third quadrant move\n points = [\n [sxMax, syMax],\n [sxMin, syMax],\n [exMin, eyMax],\n [exMin, eyMin],\n [exMax, eyMin],\n [sxMax, syMin],\n ]\n } else {\n // Fourth quadrant move\n points = [\n [sxMin, syMax],\n [sxMin, syMin],\n [exMin, eyMin],\n [exMax, eyMin],\n [exMax, eyMax],\n [sxMax, syMax],\n ]\n }\n\n return {type: Tree.POLYGON, points}\n}\n","import * as Tree from '../tree'\nimport type {Tool} from '../tool-store'\nimport {SIMPLE_TOOL} from '../tool-store'\nimport type {Location, Point} from '../location-store'\nimport {TWO_PI} from '../coordinate-math'\n\nimport {plotRectPath} from './plot-rect-path'\n\nexport const CW = 'cw'\nexport const CCW = 'ccw'\n\nexport type ArcDirection = typeof CW | typeof CCW\n\nexport function plotSegment(\n location: Location,\n arcDirection?: ArcDirection,\n ambiguousArcCenter?: boolean\n): Tree.PathSegment {\n return arcDirection === undefined\n ? createLineSegment(location)\n : createArcSegment(location, arcDirection, ambiguousArcCenter)\n}\n\nexport function plotPath(\n segments: Tree.PathSegment[],\n tool: Tool | undefined,\n region = false\n): Tree.ImageGraphic | undefined {\n if (segments.length > 0) {\n if (region) {\n return {type: Tree.IMAGE_REGION, segments}\n }\n\n if (tool?.type === SIMPLE_TOOL && tool.shape.type === Tree.CIRCLE) {\n return {type: Tree.IMAGE_PATH, width: tool.shape.diameter, segments}\n }\n\n if (tool?.type === SIMPLE_TOOL && tool.shape.type === Tree.RECTANGLE) {\n return plotRectPath(segments, tool.shape)\n }\n }\n}\n\nfunction createLineSegment(location: Location): Tree.PathLineSegment {\n return {\n type: Tree.LINE,\n start: [location.startPoint.x, location.startPoint.y],\n end: [location.endPoint.x, location.endPoint.y],\n }\n}\n\nfunction createArcSegment(\n location: Location,\n arcDirection: ArcDirection,\n ambiguousArcCenter = false\n): Tree.PathSegment {\n const {startPoint, endPoint, arcOffsets} = location\n const radius =\n arcOffsets.a > 0\n ? arcOffsets.a\n : (arcOffsets.i ** 2 + arcOffsets.j ** 2) ** 0.5\n\n if (ambiguousArcCenter || arcOffsets.a > 0) {\n if (startPoint.x === endPoint.x && startPoint.y === endPoint.y) {\n return createLineSegment(location)\n }\n\n // Get the center candidates and select the candidate with the smallest arc\n const [start, end, center] = findCenterCandidates(location, radius)\n .map(centerPoint => {\n return getArcPositions(startPoint, endPoint, centerPoint, arcDirection)\n })\n .sort(([startA, endA], [startB, endB]) => {\n const absSweepA = Math.abs(endA[2] - startA[2])\n const absSweepB = Math.abs(endB[2] - startB[2])\n return absSweepA - absSweepB\n })[0]\n\n return {type: Tree.ARC, start, end, center, radius}\n }\n\n const centerPoint = {\n x: startPoint.x + arcOffsets.i,\n y: startPoint.y + arcOffsets.j,\n }\n\n const [start, end, center] = getArcPositions(\n startPoint,\n endPoint,\n centerPoint,\n arcDirection\n )\n\n return {type: Tree.ARC, start, end, center, radius}\n}\n\nexport function getArcPositions(\n startPoint: Point,\n endPoint: Point,\n centerPoint: Point,\n arcDirection: ArcDirection\n): [start: Tree.ArcPosition, end: Tree.ArcPosition, center: Tree.Position] {\n let startAngle = Math.atan2(\n startPoint.y - centerPoint.y,\n startPoint.x - centerPoint.x\n )\n let endAngle = Math.atan2(\n endPoint.y - centerPoint.y,\n endPoint.x - centerPoint.x\n )\n\n // If counter-clockwise, end angle should be greater than start angle\n if (arcDirection === CCW) {\n endAngle = endAngle > startAngle ? endAngle : endAngle + TWO_PI\n } else {\n startAngle = startAngle > endAngle ? startAngle : startAngle + TWO_PI\n }\n\n return [\n [startPoint.x, startPoint.y, startAngle],\n [endPoint.x, endPoint.y, endAngle],\n [centerPoint.x, centerPoint.y],\n ]\n}\n\n// Find arc center candidates by finding the intersection points\n// of two circles with `radius` centered on the start and end points\n// https://math.stackexchange.com/a/1367732\nfunction findCenterCandidates(location: Location, radius: number): Point[] {\n // This function assumes that start and end are different points\n const {x: x1, y: y1} = location.startPoint\n const {x: x2, y: y2} = location.endPoint\n\n // Distance between the start and end points\n const [dx, dy] = [x2 - x1, y2 - y1]\n const [sx, sy] = [x2 + x1, y2 + y1]\n const distance = Math.sqrt(dx ** 2 + dy ** 2)\n\n // If the distance to the midpoint equals the arc radius, then there is\n // exactly one intersection at the midpoint; if the distance to the midpoint\n // is greater than the radius, assume we've got a rounding error and just use\n // the midpoint\n if (radius <= distance / 2) {\n return [{x: x1 + dx / 2, y: y1 + dy / 2}]\n }\n\n // No good name for these variables, but it's how the math works out\n const factor = Math.sqrt((4 * radius ** 2) / distance ** 2 - 1)\n const [xBase, yBase] = [sx / 2, sy / 2]\n const [xAddend, yAddend] = [(dy * factor) / 2, (dx * factor) / 2]\n\n return [\n {x: xBase + xAddend, y: yBase - yAddend},\n {x: xBase - xAddend, y: yBase + yAddend},\n ]\n}\n","// Plot a tool macro as shapes\nimport type {MacroPrimitiveCode, MacroValue} from '@tracespace/parser'\nimport {\n MACRO_VARIABLE,\n MACRO_PRIMITIVE,\n MACRO_CIRCLE,\n MACRO_VECTOR_LINE_DEPRECATED,\n MACRO_VECTOR_LINE,\n MACRO_CENTER_LINE,\n MACRO_LOWER_LEFT_LINE_DEPRECATED,\n MACRO_OUTLINE,\n MACRO_POLYGON,\n MACRO_MOIRE_DEPRECATED,\n MACRO_THERMAL,\n} from '@tracespace/parser'\n\nimport {PI, rotateAndShift, positionsEqual} from '../coordinate-math'\n\nimport * as Tree from '../tree'\nimport type {MacroTool} from '../tool-store'\nimport type {Location} from '../location-store'\n\nimport {shapeToSegments} from './shapes'\nimport {CW, CCW, getArcPositions} from './plot-path'\n\ntype VariableValues = Record<string, number>\n\nexport function plotMacro(\n tool: MacroTool,\n location: Location\n): Tree.LayeredShape {\n const shapes: Tree.ErasableShape[] = []\n const variableValues: VariableValues = Object.fromEntries(\n tool.variableValues.map((value, i) => [`$${i + 1}`, value])\n )\n\n for (const block of tool.macro) {\n if (block.type === MACRO_VARIABLE) {\n variableValues[block.name] = solveExpression(block.value, variableValues)\n }\n\n if (block.type === MACRO_PRIMITIVE) {\n const origin: Tree.Position = [location.endPoint.x, location.endPoint.y]\n const parameters = block.parameters.map(p => {\n return solveExpression(p, variableValues)\n })\n\n shapes.push(...plotPrimitive(block.code, origin, parameters))\n }\n }\n\n return {type: Tree.LAYERED_SHAPE, shapes}\n}\n\nfunction solveExpression(\n expression: MacroValue,\n variables: VariableValues\n): number {\n if (typeof expression === 'number') return expression\n if (typeof expression === 'string') return variables[expression]\n\n const left = solveExpression(expression.left, variables)\n const right = solveExpression(expression.right, variables)\n\n switch (expression.operator) {\n case '+': {\n return left + right\n }\n\n case '-': {\n return left - right\n }\n\n case 'x': {\n return left * right\n }\n\n case '/': {\n return left / right\n }\n }\n}\n\nfunction plotPrimitive(\n code: MacroPrimitiveCode,\n origin: Tree.Position,\n parameters: number[]\n): Tree.ErasableShape[] {\n switch (code) {\n case MACRO_CIRCLE: {\n return [plotCircle(origin, parameters)]\n }\n\n case MACRO_VECTOR_LINE:\n case MACRO_VECTOR_LINE_DEPRECATED: {\n return [plotVectorLine(origin, parameters)]\n }\n\n case MACRO_CENTER_LINE: {\n return [plotCenterLine(origin, parameters)]\n }\n\n case MACRO_LOWER_LEFT_LINE_DEPRECATED: {\n return [plotLowerLeftLine(origin, parameters)]\n }\n\n case MACRO_OUTLINE: {\n return [plotOutline(origin, parameters)]\n }\n\n case MACRO_POLYGON: {\n return [plotPolygon(origin, parameters)]\n }\n\n case MACRO_MOIRE_DEPRECATED: {\n return plotMoire(origin, parameters)\n }\n\n case MACRO_THERMAL: {\n return [plotThermal(origin, parameters)]\n }\n }\n\n return []\n}\n\nfunction plotCircle(\n origin: Tree.Position,\n parameters: number[]\n): Tree.ErasableShape {\n const [exposure, diameter, cx0, cy0, degrees] = parameters\n const r = diameter / 2\n const [cx, cy] = rotateAndShift([cx0, cy0], origin, degrees)\n\n return {type: Tree.CIRCLE, erase: exposure === 0, cx, cy, r}\n}\n\nfunction plotVectorLine(\n origin: Tree.Position,\n parameters: number[]\n): Tree.ErasableShape {\n const [exposure, width, sx, sy, ex, ey, degrees] = parameters\n const [dy, dx] = [ey - sy, ex - sx]\n const halfWid = width / 2\n const dist = Math.sqrt(dy ** 2 + dx ** 2)\n const [xOff, yOff] = [(halfWid * dx) / dist, (halfWid * dy) / dist]\n\n return {\n type: Tree.POLYGON,\n erase: exposure === 0,\n points: (\n [\n [sx + xOff, sy - yOff],\n [ex + xOff, ey - yOff],\n [ex - xOff, ey + yOff],\n [sx - xOff, sy + yOff],\n ] as Tree.Position[]\n ).map(p => rotateAndShift(p, origin, degrees)),\n }\n}\n\nfunction plotCenterLine(\n origin: Tree.Position,\n parameters: number[]\n): Tree.ErasableShape {\n const [exposure, width, height, cx, cy, degrees] = parameters\n const [halfWidth, halfHeight] = [width / 2, height / 2]\n\n return {\n type: Tree.POLYGON,\n erase: exposure === 0,\n points: (\n [\n [cx - halfWidth, cy - halfHeight],\n [cx + halfWidth, cy - halfHeight],\n [cx + halfWidth, cy + halfHeight],\n [cx - halfWidth, cy + halfHeight],\n ] as Tree.Position[]\n ).map(p => rotateAndShift(p, origin, degrees)),\n }\n}\n\nfunction plotLowerLeftLine(\n origin: Tree.Position,\n parameters: number[]\n): Tree.ErasableShape {\n const [exposure, width, height, x, y, degrees] = parameters\n\n return {\n type: Tree.POLYGON,\n erase: exposure === 0,\n points: (\n [\n [x, y],\n [x + width, y],\n [x + width, y + height],\n [x, y + height],\n ] as Tree.Position[]\n ).map(p => rotateAndShift(p, origin, degrees)),\n }\n}\n\nfunction plotOutline(\n origin: Tree.Position,\n parameters: number[]\n): Tree.ErasableShape {\n const [exposure, , ...coords] = parameters.slice(0, -1)\n const degrees = parameters[parameters.length - 1]\n\n return {\n type: Tree.POLYGON,\n erase: exposure === 0,\n points: coords\n .flatMap<[number, number]>((coordinate, i) =>\n i % 2 === 1 ? [[coords[i - 1], coordinate]] : []\n )\n .map(p => rotateAndShift(p, origin, degrees)),\n }\n}\n\nfunction plotPolygon(\n origin: Tree.Position,\n parameters: number[]\n): Tree.ErasableShape {\n const [exposure, vertices, cx, cy, diameter, degrees] = parameters\n const r = diameter / 2\n const step = (2 * PI) / vertices\n const points: Tree.Position[] = []\n let i\n\n for (i = 0; i < vertices; i++) {\n const theta = step * i\n const pointX = cx + r * Math.cos(theta)\n const pointY = cy + r * Math.sin(theta)\n points.push(rotateAndShift([pointX, pointY], origin, degrees))\n }\n\n return {type: Tree.POLYGON, erase: exposure === 0, points}\n}\n\nfunction plotMoire(\n origin: Tree.Position,\n parameters: number[]\n): Tree.ErasableShape[] {\n const rotate = (p: Tree.Position): Tree.Position =>\n rotateAndShift(p, origin, parameters[8])\n\n const [cx0, cy0, d, ringThx, ringGap, ringN, lineThx, lineLength] = parameters\n const [cx, cy] = rotate([cx0, cy0])\n const halfLineThx = lineThx / 2\n const halfLineLength = lineLength / 2\n\n const radii = []\n let count = 0\n let dRemain = d\n\n while (dRemain >= 0 && count < ringN) {\n const r = dRemain / 2\n const rHole = r - ringThx\n\n radii.push(r)\n if (rHole > 0) radii.push(rHole)\n count += 1\n dRemain = 2 * (rHole - ringGap)\n }\n\n return [\n {\n type: Tree.OUTLINE,\n segments: radii.flatMap(r => {\n return shapeToSegments({type: Tree.CIRCLE, cx, cy, r})\n }),\n },\n // Vertical stroke\n {\n type: Tree.POLYGON,\n points: (\n [\n [cx0 - halfLineThx, cy0 - halfLineLength],\n [cx0 + halfLineThx, cy0 - halfLineLength],\n [cx0 + halfLineThx, cy0 + halfLineLength],\n [cx0 - halfLineThx, cy0 + halfLineLength],\n ] as Tree.Position[]\n ).map(rotate),\n },\n // Horizontal stroke\n {\n type: Tree.POLYGON,\n points: (\n [\n [cx0 - halfLineLength, cy0 - halfLineThx],\n [cx0 + halfLineLength, cy0 - halfLineThx],\n [cx0 + halfLineLength, cy0 + halfLineThx],\n [cx0 - halfLineLength, cy0 + halfLineThx],\n ] as Tree.Position[]\n ).map(rotate),\n },\n ]\n}\n\nfunction plotThermal(\n origin: Tree.Position,\n parameters: number[]\n): Tree.ErasableShape {\n const [cx0, cy0, od, id, gap, degrees] = parameters\n const center = rotateAndShift([cx0, cy0], origin, degrees)\n const [or, ir] = [od / 2, id / 2]\n const halfGap = gap / 2\n const oIntSquare = or ** 2 - halfGap ** 2\n const iIntSquare = ir ** 2 - halfGap ** 2\n const oInt = Math.sqrt(oIntSquare)\n const iInt = iIntSquare >= 0 ? Math.sqrt(iIntSquare) : halfGap\n const positions = [0, 90, 180, 270]\n const segments: Tree.PathSegment[] = []\n\n for (const rot of positions) {\n const points = (\n [\n [iInt, halfGap],\n [oInt, halfGap],\n [halfGap, oInt],\n [halfGap, iInt],\n ] as Tree.Position[]\n )\n .map(p => rotateAndShift(p, [cx0, cy0], rot))\n .map(p => rotateAndShift(p, origin, degrees))\n\n const [os, oe, oc] = getArcPositions(\n {x: points[1][0], y: points[1][1]},\n {x: points[2][0], y: points[2][1]},\n {x: center[0], y: center[1]},\n CCW\n )\n\n segments.push(\n {type: Tree.LINE, start: points[0], end: points[1]},\n {type: Tree.ARC, start: os, end: oe, center: oc, radius: or},\n {type: Tree.LINE, start: points[2], end: points[3]}\n )\n\n if (!positionsEqual(points[0], points[3])) {\n const [is, ie, ic] = getArcPositions(\n {x: points[3][0], y: points[3][1]},\n {x: points[0][0], y: points[0][1]},\n {x: center[0], y: center[1]},\n CW\n )\n segments.push({\n type: Tree.ARC,\n start: is,\n end: ie,\n center: ic,\n radius: ir,\n })\n }\n }\n\n return {type: Tree.OUTLINE, segments}\n}\n","// Graphic plotter\n// Takes nodes and turns them into graphics to be added to the image\nimport type {\n GerberNode,\n GraphicType,\n Filetype,\n InterpolateModeType,\n} from '@tracespace/parser'\nimport {\n GRAPHIC,\n SHAPE,\n SEGMENT,\n MOVE,\n SLOT,\n DONE,\n LINE,\n CCW_ARC,\n CW_ARC,\n DRILL,\n SINGLE,\n INTERPOLATE_MODE,\n QUADRANT_MODE,\n REGION_MODE,\n} from '@tracespace/parser'\n\nimport * as Tree from '../tree'\nimport type {Tool} from '../tool-store'\nimport {SIMPLE_TOOL, MACRO_TOOL} from '../tool-store'\nimport type {Location} from '../location-store'\n\nimport {plotShape} from './plot-shape'\nimport {plotMacro} from './plot-macro'\nimport type {ArcDirection} from './plot-path'\nimport {CCW, CW, plotSegment, plotPath} from './plot-path'\n\nexport interface GraphicPlotter {\n plot(\n node: GerberNode,\n tool: Tool | undefined,\n location: Location\n ): Tree.ImageGraphic[]\n}\n\nexport function createGraphicPlotter(filetype: Filetype): GraphicPlotter {\n const plotter = Object.create(GraphicPlotterPrototype)\n\n return filetype === DRILL\n ? Object.assign(plotter, DrillGraphicPlotterTrait)\n : plotter\n}\n\ninterface GraphicPlotterImpl extends GraphicPlotter {\n _currentPath: CurrentPath | undefined\n _arcDirection: ArcDirection | undefined\n _ambiguousArcCenter: boolean\n _regionMode: boolean\n _defaultGraphic: NonNullable<GraphicType> | undefined\n\n _setGraphicState(node: GerberNode): NonNullable<GraphicType> | undefined\n\n _plotCurrentPath(\n node: GerberNode,\n nextTool: Tool | undefined,\n nextGraphicType: NonNullable<GraphicType> | undefined\n ): Tree.ImageGraphic | undefined\n}\n\ninterface CurrentPath {\n segments: Tree.PathSegment[]\n tool: Tool | undefined\n region: boolean\n}\n\nconst GraphicPlotterPrototype: GraphicPlotterImpl = {\n _currentPath: undefined,\n _arcDirection: undefined,\n _ambiguousArcCenter: false,\n _regionMode: false,\n _defaultGraphic: undefined,\n\n plot(\n node: GerberNode,\n tool: Tool | undefined,\n location: Location\n ): Tree.ImageGraphic[] {\n const graphics: Tree.ImageGraphic[] = []\n const nextGraphicType = this._setGraphicState(node)\n const pathGraphic = this._plotCurrentPath(node, tool, nextGraphicType)\n\n if (pathGraphic) {\n graphics.push(pathGraphic)\n }\n\n if (nextGraphicType === SHAPE && tool?.type === SIMPLE_TOOL) {\n graphics.push({type: Tree.IMAGE_SHAPE, shape: plotShape(tool, location)})\n }\n\n if (nextGraphicType === SHAPE && tool?.type === MACRO_TOOL) {\n graphics.push({type: Tree.IMAGE_SHAPE, shape: plotMacro(tool, location)})\n }\n\n if (nextGraphicType === SEGMENT) {\n this._currentPath = this._currentPath ?? {\n segments: [],\n region: this._regionMode,\n tool,\n }\n\n this._currentPath.segments.push(\n plotSegment(location, this._arcDirection, this._ambiguousArcCenter)\n )\n }\n\n if (nextGraphicType === SLOT) {\n const pathGraphic = plotPath([plotSegment(location)], tool)\n\n if (pathGraphic) {\n graphics.push(pathGraphic)\n }\n }\n\n return graphics\n },\n\n _setGraphicState(node: GerberNode): NonNullable<GraphicType> | undefined {\n if (node.type === INTERPOLATE_MODE) {\n this._arcDirection = arcDirectionFromMode(node.mode)\n }\n\n if (node.type === QUADRANT_MODE) {\n this._ambiguousArcCenter = node.quadrant === SINGLE\n }\n\n if (node.type === REGION_MODE) {\n this._regionMode = node.region\n }\n\n if (node.type !== GRAPHIC) {\n return undefined\n }\n\n if (node.graphic === SEGMENT) {\n this._defaultGraphic = SEGMENT\n } else if (node.graphic !== null) {\n this._defaultGraphic = undefined\n }\n\n return node.graphic ?? this._defaultGraphic\n },\n\n _plotCurrentPath(\n node: GerberNode,\n nextTool: Tool | undefined,\n nextGraphicType: NonNullable<GraphicType> | undefined\n ): Tree.ImageGraphic | undefined {\n if (this._currentPath === undefined) {\n return undefined\n }\n\n if (\n nextTool !== this._currentPath.tool ||\n node.type === REGION_MODE ||\n node.type === DONE ||\n (nextGraphicType === MOVE && this._currentPath.region) ||\n (nextGraphicType === SHAPE && this._currentPath !== undefined)\n ) {\n const pathGraphic = plotPath(\n this._currentPath.segments,\n this._currentPath.tool,\n this._currentPath.region\n )\n\n this._currentPath = undefined\n return pathGraphic\n }\n },\n}\n\nconst DrillGraphicPlotterTrait: Partial<GraphicPlotterImpl> = {\n _defaultGraphic: SHAPE,\n _ambiguousArcCenter: true,\n\n _setGraphicState(node: GerberNode): NonNullable<GraphicType> | undefined {\n if (node.type === INTERPOLATE_MODE) {\n const {mode} = node\n this._arcDirection = arcDirectionFromMode(mode)\n\n if (mode === CW_ARC || mode === CCW_ARC || mode === LINE) {\n this._defaultGraphic = SEGMENT\n } else if (mode === MOVE) {\n this._defaultGraphic = MOVE\n } else {\n this._defaultGraphic = SHAPE\n }\n }\n\n if (node.type !== GRAPHIC) {\n return undefined\n }\n\n return node.graphic ?? this._defaultGraphic\n },\n}\n\nfunction arcDirectionFromMode(\n mode: InterpolateModeType\n): ArcDirection | undefined {\n if (mode === CCW_ARC) return CCW\n if (mode === CW_ARC) return CW\n return undefined\n}\n","// @tracespace/plotter\n// build abstract board images from @tracespace/parser ASTs\nimport type {GerberTree} from '@tracespace/parser'\n\nimport {fromGraphics as sizeFromGraphics} from './bounding-box'\nimport {getPlotOptions} from './options'\nimport {createToolStore} from './tool-store'\nimport {createLocationStore} from './location-store'\nimport {createGraphicPlotter} from './graphic-plotter'\nimport {IMAGE} from './tree'\nimport type {ImageTree} from './tree'\n\nexport * from './tree'\nexport * as BoundingBox from './bounding-box'\nexport {TWO_PI, positionsEqual} from './coordinate-math'\n\nexport function plot(tree: GerberTree): ImageTree {\n const plotOptions = getPlotOptions(tree)\n const toolStore = createToolStore()\n const locationStore = createLocationStore()\n const graphicPlotter = createGraphicPlotter(tree.filetype)\n const children = []\n\n for (const node of tree.children) {\n const tool = toolStore.use(node)\n const location = locationStore.use(node, plotOptions)\n const graphics = graphicPlotter.plot(node, tool, location)\n\n children.push(...graphics)\n }\n\n return {\n type: IMAGE,\n units: plotOptions.units,\n size: sizeFromGraphics(children),\n children,\n }\n}\n"],"names":["IMAGE","IMAGE_SHAPE","IMAGE_PATH","IMAGE_REGION","LINE","ARC","CIRCLE","RECTANGLE","POLYGON","OUTLINE","LAYERED_SHAPE","PI","HALF_PI","THREE_HALF_PI","TWO_PI","limitAngle","theta","rotateQuadrant","degreesToRadians","degrees","rotateAndShift","point","shift","rotation","sin","cos","x","y","nextX","nextY","positionsEqual","a","b","isEmpty","box","empty","add","sum","boxes","fromGraphics","graphics","fromGraphic","graphic","Tree.IMAGE_SHAPE","fromShape","fromPath","Tree.IMAGE_PATH","shape","Tree.CIRCLE","cx","cy","r","fromPosition","Tree.RECTANGLE","xSize","ySize","Tree.POLYGON","p","Tree.OUTLINE","Tree.LAYERED_SHAPE","erase","segments","width","rTool","keyPoints","segment","Tree.ARC","start","end","center","radius","sweep","thetaStart","thetaEnd","axisPoints","position","FORMAT_COMMENT_RE","getPlotOptions","tree","treeNodes","units","coordinateFormat","zeroSuppression","index","node","UNITS","COORDINATE_FORMAT","GRAPHIC","coordinates","coordinate","LEADING","TRAILING","COMMENT","comment","formatMatch","IN","SIMPLE_TOOL","MACRO_TOOL","createToolStore","ToolStorePrototype","TOOL_MACRO","TOOL_DEFINITION","hole","tool","MACRO_SHAPE","TOOL_CHANGE","createLocationStore","LocationStorePrototype","options","arcOffsets","startPoint","endPoint","x0","parseCoordinate","y0","i","j","defaultValue","integerPlaces","decimalPlaces","sign","signlessCoordinate","digits","paddedCoordinate","leading","trailing","createShape","diameter","OBROUND","xHalf","yHalf","rectangle","vertices","offset","step","points","_","pointX","pointY","shapeToSegments","Tree.LINE","endIndex","plotShape","location","toolShape","toolHole","holeShape","plotRectPath","shapes","plotRectPathSegment","sx","sy","ex","ey","xOffset","yOffset","sxMin","sxMax","syMin","syMax","exMin","exMax","eyMin","eyMax","CW","CCW","plotSegment","arcDirection","ambiguousArcCenter","createLineSegment","createArcSegment","plotPath","region","Tree.IMAGE_REGION","findCenterCandidates","centerPoint","getArcPositions","startA","endA","startB","endB","absSweepA","absSweepB","startAngle","endAngle","x1","y1","x2","y2","dx","dy","distance","factor","xBase","yBase","xAddend","yAddend","plotMacro","variableValues","value","block","MACRO_VARIABLE","solveExpression","MACRO_PRIMITIVE","origin","parameters","plotPrimitive","expression","variables","left","right","code","MACRO_CIRCLE","plotCircle","MACRO_VECTOR_LINE","MACRO_VECTOR_LINE_DEPRECATED","plotVectorLine","MACRO_CENTER_LINE","plotCenterLine","MACRO_LOWER_LEFT_LINE_DEPRECATED","plotLowerLeftLine","MACRO_OUTLINE","plotOutline","MACRO_POLYGON","plotPolygon","MACRO_MOIRE_DEPRECATED","plotMoire","MACRO_THERMAL","plotThermal","exposure","cx0","cy0","halfWid","dist","xOff","yOff","height","halfWidth","halfHeight","coords","rotate","d","ringThx","ringGap","ringN","lineThx","lineLength","halfLineThx","halfLineLength","radii","count","dRemain","rHole","od","id","gap","or","ir","halfGap","oIntSquare","iIntSquare","oInt","iInt","positions","rot","os","oe","oc","is","ie","ic","createGraphicPlotter","filetype","plotter","GraphicPlotterPrototype","DRILL","DrillGraphicPlotterTrait","nextGraphicType","pathGraphic","SHAPE","SEGMENT","SLOT","INTERPOLATE_MODE","arcDirectionFromMode","QUADRANT_MODE","SINGLE","REGION_MODE","nextTool","DONE","MOVE","mode","CW_ARC","CCW_ARC","plot","plotOptions","toolStore","locationStore","graphicPlotter","children","sizeFromGraphics"],"mappings":"iTAIa,MAAAA,EAAQ,QACRC,EAAc,aACdC,EAAa,YACbC,EAAe,cAEfC,EAAO,OACPC,EAAM,MAENC,EAAS,SACTC,EAAY,YACZC,EAAU,UACVC,EAAU,UACVC,EAAgB,eCbhB,CAAC,GAAAC,CAAM,EAAA,KACPC,EAAUD,EAAK,EACfE,EAAgB,EAAID,EACpBE,EAAS,EAAIH,EAEnB,SAASI,EAAWC,EAAuB,CAC5C,OAAAA,GAAS,GAAKA,GAASF,EAAeE,EACtCA,EAAQ,EAAUA,EAAQF,EAC1BE,EAAQF,EAAeE,EAAQF,EAC5BC,EAAWC,CAAK,CACzB,CAEO,SAASC,EAAeD,EAAuB,CACpD,OAAOA,GAASJ,EAAUI,EAAQJ,EAAUI,EAAQH,CACtD,CAEO,SAASK,EAAiBC,EAAyB,CAChD,OAAAA,EAAU,KAAK,GAAM,GAC/B,CAEO,SAASC,EACdC,EACAC,EACAH,EAAU,EACA,CACJ,MAAAI,EAAWL,EAAiBC,CAAO,EACnC,CAACK,EAAKC,CAAG,EAAI,CAAC,KAAK,IAAIF,CAAQ,EAAG,KAAK,IAAIA,CAAQ,CAAC,EACpD,CAACG,EAAGC,CAAC,EAAIN,EACTO,EAAQF,EAAID,EAAME,EAAIH,EAAMF,EAAM,CAAC,EACnCO,EAAQH,EAAIF,EAAMG,EAAIF,EAAMH,EAAM,CAAC,EAElC,MAAA,CAACM,EAAOC,CAAK,CACtB,CAEgB,SAAAC,EAAeC,EAAaC,EAAsB,CACzD,OAAAD,EAAE,CAAC,IAAMC,EAAE,CAAC,GAAKD,EAAE,CAAC,IAAMC,EAAE,CAAC,CACtC,CCjCO,SAASC,EAAQC,EAAqB,CAC3C,OAAOA,EAAI,SAAW,CACxB,CAEO,SAASC,IAAa,CAC3B,MAAO,EACT,CAEgB,SAAAC,GAAIL,EAAQC,EAAa,CACvC,OAAIC,EAAQF,CAAC,EAAUC,EACnBC,EAAQD,CAAC,EAAUD,EAEhB,CACL,KAAK,IAAIA,EAAE,CAAC,EAAGC,EAAE,CAAC,CAAC,EACnB,KAAK,IAAID,EAAE,CAAC,EAAGC,EAAE,CAAC,CAAC,EACnB,KAAK,IAAID,EAAE,CAAC,EAAGC,EAAE,CAAC,CAAC,EACnB,KAAK,IAAID,EAAE,CAAC,EAAGC,EAAE,CAAC,CAAC,CAAA,CAEvB,CAEO,SAASK,EAAIC,EAAmB,CACrC,OAAOA,EAAM,OAAOF,GAAKD,GAAO,CAAA,CAClC,CAEO,SAASI,GAAaC,EAAoC,CAC/D,OAAOH,EAAIG,EAAS,IAAIC,EAAW,CAAC,CACtC,CAEO,SAASA,GAAYC,EAAiC,CAC3D,OAAOA,EAAQ,OAASC,EACpBC,EAAUF,EAAQ,KAAK,EACvBG,EACEH,EAAQ,SACRA,EAAQ,OAASI,EAAkBJ,EAAQ,MAAQ,MAAA,CAE3D,CAEO,SAASE,EAAUG,EAAwB,CAChD,OAAQA,EAAM,KAAM,CAClB,KAAKC,EAAa,CAChB,KAAM,CAAC,GAAAC,EAAI,GAAAC,EAAI,EAAAC,CAAA,EAAKJ,EACpB,OAAOK,EAAa,CAACH,EAAIC,CAAE,EAAGC,CAAC,CACjC,CAEA,KAAKE,EAAgB,CACnB,KAAM,CAAC,EAAA3B,EAAG,EAAAC,EAAG,MAAA2B,EAAO,MAAAC,GAASR,EAC7B,MAAO,CAACrB,EAAGC,EAAGD,EAAI4B,EAAO3B,EAAI4B,CAAK,CACpC,CAEA,KAAKC,EACI,OAAAnB,EAAIU,EAAM,OAAO,OAASK,EAAaK,CAAC,CAAC,CAAC,EAGnD,KAAKC,EACI,OAAAb,EAASE,EAAM,QAAQ,EAGhC,KAAKY,EACH,OAAOtB,EAAIU,EAAM,OAAO,OAAO,CAAC,CAAC,MAAAa,CAAK,IAAM,CAACA,CAAK,EAAE,IAAIhB,CAAS,CAAC,CAEtE,CACF,CAEgB,SAAAC,EAASgB,EAA8BC,EAAQ,EAAQ,CACrE,MAAMC,EAAQD,EAAQ,EAChBE,EAAqD,CAAA,EAE3D,UAAWC,KAAWJ,EAGhB,GAFJG,EAAU,KAAKC,EAAQ,MAAOA,EAAQ,GAAG,EAErCA,EAAQ,OAASC,EAAU,CAC7B,KAAM,CAAC,MAAAC,EAAO,IAAAC,EAAK,OAAAC,EAAQ,OAAAC,GAAUL,EAC/BM,EAAQ,KAAK,IAAIH,EAAI,CAAC,EAAID,EAAM,CAAC,CAAC,EAGpC,GAAA,CAACK,EAAYC,CAAQ,EACvBL,EAAI,CAAC,EAAID,EAAM,CAAC,EAAI,CAACA,EAAM,CAAC,EAAGC,EAAI,CAAC,CAAC,EAAI,CAACA,EAAI,CAAC,EAAGD,EAAM,CAAC,CAAC,EAE5DK,EAAazD,EAAWyD,CAAU,EAClCC,EAAW1D,EAAW0D,CAAQ,EAE9B,MAAMC,EAA8B,CAClC,CAACL,EAAO,CAAC,EAAIC,EAAQD,EAAO,CAAC,CAAC,EAC9B,CAACA,EAAO,CAAC,EAAGA,EAAO,CAAC,EAAIC,CAAM,EAC9B,CAACD,EAAO,CAAC,EAAIC,EAAQD,EAAO,CAAC,CAAC,EAC9B,CAACA,EAAO,CAAC,EAAGA,EAAO,CAAC,EAAIC,CAAM,CAAA,EAGhC,UAAW,KAAKI,GACVF,EAAaC,GAAYF,IAAUzD,IACrCkD,EAAU,KAAK,CAAC,EAIlBQ,EAAavD,EAAeuD,CAAU,EACtCC,EAAWxD,EAAewD,CAAQ,CAEtC,CAGK,OAAApC,EAAI2B,EAAU,IAAIP,GAAKL,EAAaK,EAAGM,CAAK,CAAC,CAAC,CACvD,CAEA,SAASX,EAAauB,EAAkCL,EAAS,EAAQ,CAChE,MAAA,CACLK,EAAS,CAAC,EAAIL,EACdK,EAAS,CAAC,EAAIL,EACdK,EAAS,CAAC,EAAIL,EACdK,EAAS,CAAC,EAAIL,CAAA,CAElB,2LC9FMM,GAAoB,qBAEnB,SAASC,GAAeC,EAA+B,CACtD,KAAA,CAAC,SAAUC,CAAa,EAAAD,EAC9B,IAAIE,EAA0B,KAC1BC,EAAkC,KAClCC,EAA0C,KAC1CC,EAAQ,EAGV,KAAAA,EAAQJ,EAAU,SACjBC,IAAU,MAAQC,IAAqB,MAAQC,IAAoB,OACpE,CACM,MAAAE,EAAOL,EAAUI,CAAK,EAE5B,OAAQC,EAAK,KAAM,CACjB,KAAKC,QAAO,CACVL,EAAQI,EAAK,MACb,KACF,CAEA,KAAKE,oBAAmB,CACtBL,EAAmBG,EAAK,OACxBF,EAAkBE,EAAK,gBACvB,KACF,CAEA,KAAKG,UAAS,CACN,KAAA,CAAC,YAAAC,CAAe,EAAAJ,EAEtB,UAAWK,KAAc,OAAO,OAAOD,CAAW,EAAG,CACnD,GAAIN,IAAoB,KAAM,MAE1BO,EAAY,SAAS,GAAG,GAAKA,EAAY,SAAS,GAAG,EACrCP,EAAAQ,EAAAA,QACTD,EAAY,WAAW,GAAG,IACjBP,EAAAS,EAAAA,SAEtB,CAEA,KACF,CAEA,KAAKC,UAAS,CACN,KAAA,CAAC,QAAAC,CAAW,EAAAT,EACZU,EAAclB,GAAkB,KAAKiB,CAAO,EAE9C,qBAAqB,KAAKA,CAAO,EACjBX,EAAAS,EAAAA,SACT,iCAAiC,KAAKE,CAAO,IA