diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 905624fb..5f3ccad9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v3 - uses: pnpm/action-setup@35ab4267a1a21c8e8cb1c087cf1642e891ff57bd with: - version: '7.0.0' + version: '7.5.0' - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@v3 - uses: pnpm/action-setup@35ab4267a1a21c8e8cb1c087cf1642e891ff57bd with: - version: '7.0.0' + version: '7.5.0' - uses: actions/setup-node@v3 with: node-version: '16' diff --git a/.gitignore b/.gitignore index b8b8fa66..01562d30 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ coverage *.tgz .vscode .DS_Store +packages/cli/bin/*.d.ts diff --git a/.prettierignore b/.prettierignore index 61e458f5..49929e67 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,3 +6,4 @@ CHANGELOG.md dist lib coverage +packages/cli/bin/*.d.ts diff --git a/config/tsconfig.base.json b/config/tsconfig.base.json index 0e703d21..d9afe2af 100644 --- a/config/tsconfig.base.json +++ b/config/tsconfig.base.json @@ -8,6 +8,7 @@ "strict": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "isolatedModules": true } } diff --git a/config/vite.config.base.ts b/config/vite.config.base.ts index 69262f1a..0ecb0349 100644 --- a/config/vite.config.base.ts +++ b/config/vite.config.base.ts @@ -1,10 +1,10 @@ -import {defineConfig} from 'vite' +import {UserConfig, defineConfig} from 'vite' -export const baseConfig = defineConfig({ +export const baseConfig: UserConfig = defineConfig({ resolve: { conditions: ['source'], }, -}) +}) as UserConfig export const getDefineConstants = (pkg: { name: string diff --git a/config/vitest.setup.ts b/config/vitest.setup.ts new file mode 100644 index 00000000..be34a7df --- /dev/null +++ b/config/vitest.setup.ts @@ -0,0 +1,16 @@ +import {expect} from 'vitest' + +expect.extend({ + approx(received: unknown, expected: number) { + const difference = + typeof received === 'number' + ? Math.abs(received - expected) + : Number.POSITIVE_INFINITY + + return { + message: () => + `expected ${String(received)} to be approximately ${expected}`, + pass: difference < 1e-12, + } + }, +}) diff --git a/package.json b/package.json index 827b6300..5398c09e 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,7 @@ "build": "concurrently -g pnpm:build:*", "build:packages": "pnpm -r build", "build:types": "tsc --build", - "clean": "concurrently -g pnpm:clean:*", - "clean:packages": "pnpm -r clean", - "clean:types": "tsc --build --clean", + "clean": "tsc --build --clean", "dev": "pnpm -C www dev" }, "engines": { @@ -31,16 +29,15 @@ "whats-that-gerber": "workspace:*" }, "devDependencies": { - "@types/node": "^17.0.30", - "c8": "^7.11.2", - "concurrently": "^7.1.0", - "prettier": "^2.6.2", - "rimraf": "^3.0.2", - "testdouble": "^3.16.5", - "typescript": "^4.6.4", - "vite": "^2.9.6", - "vitest": "^0.10.0", - "xo": "^0.48.0" + "@types/node": "^18.0.1", + "c8": "^7.11.3", + "concurrently": "^7.2.2", + "prettier": "^2.7.1", + "testdouble": "^3.16.6", + "typescript": "^4.7.4", + "vite": "^2.9.13", + "vitest": "^0.17.0", + "xo": "^0.50.0" }, "pnpm": { "peerDependencyRules": { @@ -74,6 +71,8 @@ "@typescript-eslint/naming-convention": "off", "import/extensions": "off", "unicorn/no-array-callback-reference": "off", + "unicorn/no-array-reduce": "off", + "n/file-extension-in-import": "off", "default-case": "off" }, "overrides": [ @@ -97,6 +96,8 @@ ], "rules": { "@typescript-eslint/triple-slash-reference": "off", + "@typescript-eslint/no-unnecessary-type-arguments": "off", + "@typescript-eslint/prefer-function-type": "off", "import/no-unassigned-import": "off" } }, @@ -107,8 +108,11 @@ ], "rules": { "@typescript-eslint/consistent-type-assertions": "off", + "@typescript-eslint/no-namespace": "off", "import/no-extraneous-dependencies": "off", - "unicorn/no-array-for-each": "off" + "max-nested-callbacks": "off", + "unicorn/no-array-for-each": "off", + "unicorn/prefer-module": "off" } } ] diff --git a/packages/cli/package.json b/packages/cli/package.json index 700b9fc4..ce91d0c4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -14,8 +14,7 @@ "types": "lib/index.d.ts", "sideEffects": false, "scripts": { - "build": "vite build", - "clean": "rimraf dist" + "build": "vite build" }, "repository": { "type": "git", diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 907c5211..69a8ffc9 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -80,7 +80,7 @@ export async function cli( const typesByName = Wtg.identifyLayers(filenames) const layers = filenames .map(filename => makeLayerFromFilename(filename, typesByName)) - .filter((ly): ly is InputLayer => Boolean(ly)) + .filter((ly): ly is InputLayer => ly !== null) if (layers.length === 0) { throw new Error('No valid Gerber or drill files found') diff --git a/packages/fixtures/gerbers/pads/polygon.gbr b/packages/fixtures/gerbers/pads/polygon.gbr index 616b6d04..b2bed65d 100644 --- a/packages/fixtures/gerbers/pads/polygon.gbr +++ b/packages/fixtures/gerbers/pads/polygon.gbr @@ -13,21 +13,21 @@ D10* X0Y0D03* D11* -X6000Y0D03* +X0Y6000D03* D12* -X12000Y0D03* +X0Y12000D03* D13* -X18000Y0D03* +X0Y18000D03* D14* -X24000Y0D03* +X0Y24000D03* D15* -X30000Y0D03* +X0Y30000D03* D16* -X36000Y0D03* +X0Y36000D03* D17* -X42000Y0D03* +X0Y42000D03* D18* -X48000Y0D03* +X0Y48000D03* D19* -X54000Y0D03* +X0Y54000D03* M02* diff --git a/packages/parser/package.json b/packages/parser/package.json index cf5d3d02..04b0b5c7 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -24,8 +24,7 @@ "directory": "packages/parser" }, "scripts": { - "build": "vite build", - "clean": "rimraf dist" + "build": "vite build" }, "keywords": [ "gerber", diff --git a/packages/parser/src/__tests__/helpers.ts b/packages/parser/src/__tests__/helpers.ts index 47fb6a05..5fed6b2f 100644 --- a/packages/parser/src/__tests__/helpers.ts +++ b/packages/parser/src/__tests__/helpers.ts @@ -21,8 +21,8 @@ export function simplifyTokens(tokens: Token[]): Array<{ } export function position( - start: [number, number, number], - end: [number, number, number] + start: [line: number, column: number, offset: number], + end: [line: number, column: number, offset: number] ): Position { return { start: {line: start[0], column: start[1], offset: start[2]}, diff --git a/packages/parser/src/constants.ts b/packages/parser/src/constants.ts index 615e8c98..ec1e1728 100644 --- a/packages/parser/src/constants.ts +++ b/packages/parser/src/constants.ts @@ -23,11 +23,13 @@ export const MACRO_SHAPE = 'macroShape' // Macro primitive codes export const MACRO_CIRCLE = '1' +export const MACRO_VECTOR_LINE_DEPRECATED = '2' export const MACRO_VECTOR_LINE = '20' export const MACRO_CENTER_LINE = '21' +export const MACRO_LOWER_LEFT_LINE_DEPRECATED = '22' export const MACRO_OUTLINE = '4' export const MACRO_POLYGON = '5' -export const MACRO_MOIRE = '6' +export const MACRO_MOIRE_DEPRECATED = '6' export const MACRO_THERMAL = '7' // Drawing constants diff --git a/packages/parser/src/syntax/__tests__/drill.test.ts b/packages/parser/src/syntax/__tests__/drill.test.ts index c035f90d..de1e9a42 100644 --- a/packages/parser/src/syntax/__tests__/drill.test.ts +++ b/packages/parser/src/syntax/__tests__/drill.test.ts @@ -564,7 +564,7 @@ const SPECS: Array<{ type: Tree.GRAPHIC, position: pos([1, 1, 0], [1, 16, 15]), graphic: SLOT, - coordinates: {x1: '07', y1: '08', x2: '09', y2: '10'}, + coordinates: {x0: '07', y0: '08', x: '09', y: '10'}, }, ], }, @@ -584,7 +584,7 @@ const SPECS: Array<{ type: Tree.GRAPHIC, position: pos([1, 1, 0], [1, 10, 9]), graphic: SLOT, - coordinates: {x1: '07', y2: '10'}, + coordinates: {x0: '07', y: '10'}, }, ], }, diff --git a/packages/parser/src/syntax/__tests__/gerber-macros.test.ts b/packages/parser/src/syntax/__tests__/gerber-macros.test.ts index 9df24454..2731474a 100644 --- a/packages/parser/src/syntax/__tests__/gerber-macros.test.ts +++ b/packages/parser/src/syntax/__tests__/gerber-macros.test.ts @@ -17,7 +17,7 @@ import { MACRO_CENTER_LINE, MACRO_OUTLINE, MACRO_POLYGON, - MACRO_MOIRE, + MACRO_MOIRE_DEPRECATED, MACRO_THERMAL, } from '../../constants' @@ -85,7 +85,7 @@ const SPECS: Array<{ type: Tree.MACRO_PRIMITIVE, position: pos([1, 11, 10], [1, 22, 21]), code: MACRO_CIRCLE, - modifiers: [1, 0.5, 0, 0], + parameters: [1, 0.5, 0, 0], }, ], }, @@ -126,7 +126,7 @@ const SPECS: Array<{ type: Tree.MACRO_PRIMITIVE, position: pos([1, 11, 10], [1, 32, 31]), code: MACRO_VECTOR_LINE, - modifiers: [1, 0.25, 0, 0, 0.5, 0.5, 0], + parameters: [1, 0.25, 0, 0, 0.5, 0.5, 0], }, ], }, @@ -165,7 +165,7 @@ const SPECS: Array<{ type: Tree.MACRO_PRIMITIVE, position: pos([1, 15, 14], [1, 34, 33]), code: MACRO_CENTER_LINE, - modifiers: [1, 0.5, 0.25, 0, 0, 0], + parameters: [1, 0.5, 0.25, 0, 0, 0], }, ], }, @@ -214,7 +214,7 @@ const SPECS: Array<{ type: Tree.MACRO_PRIMITIVE, position: pos([1, 12, 11], [1, 41, 40]), code: MACRO_OUTLINE, - modifiers: [1, 3, 0, 0, 0, 0.5, 0.5, 0.5, 0, 0, 0], + parameters: [1, 3, 0, 0, 0, 0.5, 0.5, 0.5, 0, 0, 0], }, ], }, @@ -253,7 +253,7 @@ const SPECS: Array<{ type: Tree.MACRO_PRIMITIVE, position: pos([1, 12, 11], [1, 27, 26]), code: MACRO_POLYGON, - modifiers: [1, 5, 0, 0, 0.5, 0], + parameters: [1, 5, 0, 0, 0.5, 0], }, ], }, @@ -297,8 +297,8 @@ const SPECS: Array<{ { type: Tree.MACRO_PRIMITIVE, position: pos([1, 10, 9], [1, 43, 42]), - code: MACRO_MOIRE, - modifiers: [0, 0, 0.5, 0.04, 0.03, 2, 0.01, 0.55, 0], + code: MACRO_MOIRE_DEPRECATED, + parameters: [0, 0, 0.5, 0.04, 0.03, 2, 0.01, 0.55, 0], }, ], }, @@ -337,7 +337,7 @@ const SPECS: Array<{ type: Tree.MACRO_PRIMITIVE, position: pos([1, 12, 11], [1, 31, 30]), code: MACRO_THERMAL, - modifiers: [0, 0, 0.5, 0.4, 0.1, 0], + parameters: [0, 0, 0.5, 0.4, 0.1, 0], }, ], }, @@ -448,7 +448,42 @@ $5=(1+(2-$4))x4* type: Tree.MACRO_PRIMITIVE, position: pos([6, 1, 59], [6, 14, 72]), code: MACRO_CIRCLE, - modifiers: [1, {left: '$5', right: 1, operator: '+'}, '$2', 0], + parameters: [1, {left: '$5', right: 1, operator: '+'}, '$2', 0], + }, + ], + }, + ], + }, + { + // Macro using `X` instead of `x` for multiplication + source: `%AMBAD_MULTIPLY* +$1=1X2* +%`, + expectedTokens: [ + t(Lexer.PERCENT, '%'), + t(Lexer.GERBER_TOOL_MACRO, 'BAD_MULTIPLY'), + t(Lexer.ASTERISK, '*'), + t(Lexer.NEWLINE, '\n'), + t(Lexer.GERBER_MACRO_VARIABLE, '$1'), + t(Lexer.EQUALS, '='), + t(Lexer.NUMBER, '1'), + t(Lexer.COORD_CHAR, 'X'), + t(Lexer.NUMBER, '2'), + t(Lexer.ASTERISK, '*'), + t(Lexer.NEWLINE, '\n'), + t(Lexer.PERCENT, '%'), + ], + expectedNodes: [ + { + type: Tree.TOOL_MACRO, + position: pos([1, 2, 1], [2, 8, 24]), + name: 'BAD_MULTIPLY', + children: [ + { + type: Tree.MACRO_VARIABLE, + position: pos([2, 1, 17], [2, 7, 23]), + name: '$1', + value: {left: 1, right: 2, operator: 'x'}, }, ], }, diff --git a/packages/parser/src/syntax/__tests__/gerber-tools.test.ts b/packages/parser/src/syntax/__tests__/gerber-tools.test.ts index a586f3a9..16e162ea 100644 --- a/packages/parser/src/syntax/__tests__/gerber-tools.test.ts +++ b/packages/parser/src/syntax/__tests__/gerber-tools.test.ts @@ -361,7 +361,7 @@ const SPECS: Array<{ type: Tree.TOOL_DEFINITION, position: pos([1, 2, 1], [1, 14, 13]), code: '23', - shape: {type: MACRO_SHAPE, name: 'MyMacro', params: []}, + shape: {type: MACRO_SHAPE, name: 'MyMacro', variableValues: []}, hole: null, }, ], @@ -386,7 +386,11 @@ const SPECS: Array<{ type: Tree.TOOL_DEFINITION, position: pos([1, 2, 1], [1, 26, 25]), code: '24', - shape: {type: MACRO_SHAPE, name: 'MyMacro', params: [0.1, 0.2, 0.3]}, + shape: { + type: MACRO_SHAPE, + name: 'MyMacro', + variableValues: [0.1, 0.2, 0.3], + }, hole: null, }, ], diff --git a/packages/parser/src/syntax/drill.ts b/packages/parser/src/syntax/drill.ts index c48211bd..a3646bbc 100644 --- a/packages/parser/src/syntax/drill.ts +++ b/packages/parser/src/syntax/drill.ts @@ -103,7 +103,7 @@ const tool: SyntaxRule = { } const mode: SyntaxRule = { - name: 'mode', + name: 'operationMode', rules: [ one([ token(Lexer.G_CODE, '0'), @@ -187,22 +187,19 @@ const slot: SyntaxRule = { createNodes(tokens) { const gCode = tokens.find(t => t.type === Lexer.G_CODE) const splitIdx = gCode ? tokens.indexOf(gCode) : -1 - const start = tokensToCoordinates(tokens.slice(0, splitIdx)) - const end = tokensToCoordinates(tokens.slice(splitIdx)) - - const startCoordinates = Object.fromEntries( - Object.entries(start).map(([axis, value]) => [`${axis}1`, value]) - ) - const endCoordinates = Object.fromEntries( - Object.entries(end).map(([axis, value]) => [`${axis}2`, value]) + const start = Object.fromEntries( + Object.entries(tokensToCoordinates(tokens.slice(0, splitIdx))).map( + ([axis, value]) => [`${axis}0`, value] + ) ) + const end = tokensToCoordinates(tokens.slice(splitIdx)) return [ { type: Tree.GRAPHIC, position: tokensToPosition(tokens), graphic: Constants.SLOT, - coordinates: {...startCoordinates, ...endCoordinates}, + coordinates: {...start, ...end}, }, ] }, diff --git a/packages/parser/src/syntax/gerber.ts b/packages/parser/src/syntax/gerber.ts index 0a1e7682..4df6dceb 100644 --- a/packages/parser/src/syntax/gerber.ts +++ b/packages/parser/src/syntax/gerber.ts @@ -215,7 +215,7 @@ const toolDefinition: SyntaxRule = { } default: { - shape = {type: Constants.MACRO_SHAPE, name, params: parameters} + shape = {type: Constants.MACRO_SHAPE, name, variableValues: parameters} } } diff --git a/packages/parser/src/syntax/macro.ts b/packages/parser/src/syntax/macro.ts index 5f83d306..07de5db4 100644 --- a/packages/parser/src/syntax/macro.ts +++ b/packages/parser/src/syntax/macro.ts @@ -1,7 +1,7 @@ // Gerber aperture macro syntax import * as Lexer from '../lexer' import * as Tree from '../tree' -import {MacroValue} from '../types' +import {MacroValue, MacroPrimitiveCode} from '../types' import {tokensToPosition} from './map-tokens' import { @@ -69,7 +69,7 @@ function createMacroComment(tokens: Lexer.Token[]): Tree.MacroComment[] { } function createMacroPrimitive(tokens: Lexer.Token[]): Tree.MacroPrimitive[] { - const code = tokens[0].value + const code = tokens[0].value as MacroPrimitiveCode const commaDelimitedTokens: Lexer.Token[][] = [[]] let currentGroup = commaDelimitedTokens[0] @@ -82,7 +82,7 @@ function createMacroPrimitive(tokens: Lexer.Token[]): Tree.MacroPrimitive[] { } } - const modifiers = commaDelimitedTokens.map(tokens => + const parameters = commaDelimitedTokens.map(tokens => parseMacroExpression(tokens) ) @@ -91,7 +91,7 @@ function createMacroPrimitive(tokens: Lexer.Token[]): Tree.MacroPrimitive[] { type: Tree.MACRO_PRIMITIVE, position: tokensToPosition(tokens), code, - modifiers, + parameters, }, ] } diff --git a/packages/parser/src/tree.ts b/packages/parser/src/tree.ts index b42daaeb..2905da8e 100644 --- a/packages/parser/src/tree.ts +++ b/packages/parser/src/tree.ts @@ -292,16 +292,16 @@ export interface CoordinateFormat extends BaseNode { * * A tool shape may be one of: * - * - {@linkcode Circle} - A circle defined by a diameter - * - {@linkcode Rectangle} - A rectangle defined by sizes in the x and y axis - * - {@linkcode Obround} - A "pill" rectangle, with a border-radius equal to half of its shorter side - * - {@linkcode Polygon} - A regular polygon defined by its diameter, number of vertices, and rotation - * - {@linkcode MacroShape} - A shape defined by a previous {@linkcode ToolMacro} + * - {@linkcode Types.Circle} - A circle defined by a diameter + * - {@linkcode Types.Rectangle} - A rectangle defined by sizes in the x and y axis + * - {@linkcode Types.Obround} - A "pill" rectangle, with a border-radius equal to half of its shorter side + * - {@linkcode Types.Polygon} - A regular polygon defined by its diameter, number of vertices, and rotation + * - {@linkcode Types.MacroShape} - A shape defined by a previous {@linkcode ToolMacro} * * A tool may have a hole in its center; the `hole`, if not `null`, may be a: * - * - {@linkcode Circle} - * - {@linkcode Rectangle} (deprecated by the Gerber specification) + * - {@linkcode Types.Circle} + * - {@linkcode Types.Rectangle} (deprecated by the Gerber specification) * * Only `Circle` or `Rectangle` tools without a `hole` may create strokes. * `MacroShape` tools may not have a `hole` defined. @@ -374,9 +374,9 @@ export interface MacroPrimitive extends BaseNode { /** Node type */ type: typeof MACRO_PRIMITIVE /** Primitive shape type */ - code: Types.MacroPrimitiveCode | string - /** Shape modifier values or expressions */ - modifiers: Types.MacroValue[] + code: Types.MacroPrimitiveCode + /** Shape parameter values or expressions */ + parameters: Types.MacroValue[] } /** diff --git a/packages/parser/src/types.ts b/packages/parser/src/types.ts index ee0725cf..99e94468 100644 --- a/packages/parser/src/types.ts +++ b/packages/parser/src/types.ts @@ -38,6 +38,13 @@ export type Mode = typeof Constants.ABSOLUTE | typeof Constants.INCREMENTAL */ export type ToolShape = Circle | Rectangle | Obround | Polygon | MacroShape +/** + * Union type of non-macro tool shapes + * + * @category Shape + */ +export type SimpleShape = Circle | Rectangle | Obround | Polygon + /** * Union type of valid tool hole shapes * @@ -100,7 +107,7 @@ export interface Polygon { export interface MacroShape { type: typeof Constants.MACRO_SHAPE name: string - params: number[] + variableValues: number[] } /** @@ -110,11 +117,13 @@ export interface MacroShape { */ export type MacroPrimitiveCode = | typeof Constants.MACRO_CIRCLE + | typeof Constants.MACRO_VECTOR_LINE_DEPRECATED | typeof Constants.MACRO_VECTOR_LINE | typeof Constants.MACRO_CENTER_LINE + | typeof Constants.MACRO_LOWER_LEFT_LINE_DEPRECATED | typeof Constants.MACRO_OUTLINE | typeof Constants.MACRO_POLYGON - | typeof Constants.MACRO_MOIRE + | typeof Constants.MACRO_MOIRE_DEPRECATED | typeof Constants.MACRO_THERMAL /** diff --git a/packages/plotter/package.json b/packages/plotter/package.json index 781ff291..6d8054f1 100644 --- a/packages/plotter/package.json +++ b/packages/plotter/package.json @@ -24,8 +24,7 @@ "directory": "packages/plotter" }, "scripts": { - "build": "vite build", - "clean": "rimraf dist" + "build": "vite build" }, "keywords": [ "pcb", diff --git a/packages/plotter/src/__tests__/bounding-box.test.ts b/packages/plotter/src/__tests__/bounding-box.test.ts new file mode 100644 index 00000000..b60d692e --- /dev/null +++ b/packages/plotter/src/__tests__/bounding-box.test.ts @@ -0,0 +1,346 @@ +// Tests for the bounding box module +import {describe, it, expect} from 'vitest' + +import * as Tree from '../tree' +import * as subject from '../bounding-box' +import {PI} from '../coordinate-math' + +import type {Box} from '../bounding-box' + +describe('bounding box calculations', () => { + it('should return an empty bounding box', () => { + const result = subject.empty() + expect(result).to.eql([]) + }) + + it('should add boxes', () => { + const box1: Box = [1, 2, 3, 4] + const box2: Box = [5, 6, 7, 8] + + expect(subject.add(box1, box2)).to.eql([1, 2, 7, 8]) + expect(subject.add(box2, box1)).to.eql([1, 2, 7, 8]) + }) + + it('should add empty boxes', () => { + const empty = subject.empty() + const notEmpty: Box = [1, 2, 3, 4] + + expect(subject.add(empty, notEmpty)).to.eql(notEmpty) + expect(subject.add(notEmpty, empty)).to.eql(notEmpty) + }) + + it('should identify empty boxes', () => { + const empty = subject.empty() + const notEmpty: Box = [1, 2, 3, 4] + + expect(subject.isEmpty(empty)).to.equal(true) + expect(subject.isEmpty(notEmpty)).to.equal(false) + }) + + it('should convert into a view box - [xMin, yMin, xSize, ySize]', () => { + expect(subject.toViewBox(subject.empty())).to.eql([0, 0, 0, 0]) + expect(subject.toViewBox([1, 2, 10, 20])).to.eql([1, 2, 9, 18]) + }) + + it('should create from a circle graphic', () => { + const result = subject.fromGraphic({ + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.CIRCLE, cx: 1, cy: 2, r: 3}, + }) + + expect(result).to.eql([-2, -1, 4, 5]) + }) + + it('should create from a rectangle graphic', () => { + const result = subject.fromGraphic({ + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.RECTANGLE, x: 1, y: 2, xSize: 3, ySize: 4}, + }) + + expect(result).to.eql([1, 2, 4, 6]) + }) + + it('should create from a polygon graphic', () => { + const result = subject.fromGraphic({ + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.POLYGON, + points: [ + [0, 0], + [0, 0.5], + [0.5, 0.5], + [0, 0], + ], + }, + }) + + expect(result).to.eql([0, 0, 0.5, 0.5]) + }) + + it('should create an empty box for an empty polygon', () => { + const result = subject.fromGraphic({ + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.POLYGON, + points: [], + }, + }) + + expect(result).to.eql([]) + }) + + it('should create from an outline graphic', () => { + const result = subject.fromGraphic({ + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.OUTLINE, + segments: [ + {type: Tree.LINE, start: [9, 8], end: [7, 6]}, + {type: Tree.LINE, start: [1, 2], end: [3, 4]}, + ], + }, + }) + + expect(result).to.eql([1, 2, 9, 8]) + }) + + it('should create an empty box for an empty outline', () => { + const result = subject.fromGraphic({ + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.OUTLINE, + segments: [], + }, + }) + + expect(result).to.eql([]) + }) + + it('should create from a layered shape graphic', () => { + const result = subject.fromGraphic({ + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.LAYERED_SHAPE, + shapes: [ + {type: Tree.RECTANGLE, x: 1, y: 2, xSize: 3, ySize: 4}, + {type: Tree.CIRCLE, cx: 1, cy: 2, r: 3}, + ], + }, + }) + + expect(result).to.eql([-2, -1, 4, 6]) + }) + + it('should ignore erase items in a layered shape graphic', () => { + const result = subject.fromGraphic({ + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.LAYERED_SHAPE, + shapes: [ + {type: Tree.RECTANGLE, x: 1, y: 2, xSize: 3, ySize: 4, erase: false}, + {type: Tree.CIRCLE, cx: 1, cy: 2, r: 3, erase: true}, + ], + }, + }) + + expect(result).to.eql([1, 2, 4, 6]) + }) + + it('should return an empty box for empty layered shape', () => { + const result = subject.fromGraphic({ + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.LAYERED_SHAPE, + shapes: [], + }, + }) + + expect(result).to.eql([]) + }) + + it('should return an empty box for empty path shape', () => { + const result = subject.fromGraphic({ + type: Tree.IMAGE_PATH, + width: 1, + segments: [], + }) + + expect(result).to.eql([]) + }) + + describe('paths and outlines', () => { + const halfSqrtTwo = 2 ** 0.5 / 2 + + it('should handle an arc through the positive Y-axis', () => { + const start: Tree.ArcPosition = [halfSqrtTwo, halfSqrtTwo, PI / 4] + const end: Tree.ArcPosition = [-halfSqrtTwo, halfSqrtTwo, (3 * PI) / 4] + const center: Tree.Position = [0, 0] + const radius = 1 + const expected = [ + -halfSqrtTwo - 0.5, + halfSqrtTwo - 0.5, + halfSqrtTwo + 0.5, + 1.5, + ] + + const ccwResult = subject.fromGraphic({ + type: Tree.IMAGE_PATH, + width: 1, + segments: [{type: Tree.ARC, start, end, center, radius}], + }) + + const cwResult = subject.fromGraphic({ + type: Tree.IMAGE_PATH, + width: 1, + segments: [{type: Tree.ARC, start: end, end: start, center, radius}], + }) + + expect(ccwResult).to.eql(expected) + expect(cwResult).to.eql(expected) + }) + + it('should handle a CCW arc through the negative X-axis', () => { + const start: Tree.ArcPosition = [-halfSqrtTwo, halfSqrtTwo, (3 * PI) / 4] + const end: Tree.ArcPosition = [-halfSqrtTwo, -halfSqrtTwo, (5 * PI) / 4] + const center: Tree.Position = [0, 0] + const radius = 1 + const expected = [ + -1.5, + -halfSqrtTwo - 0.5, + -halfSqrtTwo + 0.5, + halfSqrtTwo + 0.5, + ] + + const ccwResult = subject.fromGraphic({ + type: Tree.IMAGE_PATH, + width: 1, + segments: [{type: Tree.ARC, start, end, center, radius}], + }) + + const cwResult = subject.fromGraphic({ + type: Tree.IMAGE_PATH, + width: 1, + segments: [{type: Tree.ARC, start: end, end: start, center, radius}], + }) + + expect(ccwResult).to.eql(expected) + expect(cwResult).to.eql(expected) + }) + + it('should handle an arc through the negative Y-axis', () => { + const start: Tree.ArcPosition = [-halfSqrtTwo, -halfSqrtTwo, (5 * PI) / 4] + const end: Tree.ArcPosition = [halfSqrtTwo, -halfSqrtTwo, (7 * PI) / 4] + const center: Tree.Position = [0, 0] + const radius = 1 + const expected = [ + -halfSqrtTwo - 0.5, + -1.5, + halfSqrtTwo + 0.5, + -halfSqrtTwo + 0.5, + ] + + const ccwResult = subject.fromGraphic({ + type: Tree.IMAGE_PATH, + width: 1, + segments: [{type: Tree.ARC, start, end, center, radius}], + }) + + const cwResult = subject.fromGraphic({ + type: Tree.IMAGE_PATH, + width: 1, + segments: [{type: Tree.ARC, start: end, end: start, center, radius}], + }) + + expect(ccwResult).to.eql(expected) + expect(cwResult).to.eql(expected) + }) + + it('should handle an arc through the positive X-axis', () => { + const start: Tree.ArcPosition = [halfSqrtTwo, -halfSqrtTwo, -PI / 4] + const end: Tree.ArcPosition = [halfSqrtTwo, halfSqrtTwo, PI / 4] + const center: Tree.Position = [0, 0] + const radius = 1 + const expected = [ + halfSqrtTwo - 0.5, + -halfSqrtTwo - 0.5, + 1.5, + halfSqrtTwo + 0.5, + ] + + const ccwResult = subject.fromGraphic({ + type: Tree.IMAGE_PATH, + width: 1, + segments: [{type: Tree.ARC, start, end, center, radius}], + }) + + const cwResult = subject.fromGraphic({ + type: Tree.IMAGE_PATH, + width: 1, + segments: [{type: Tree.ARC, start: end, end: start, center, radius}], + }) + + expect(ccwResult).to.eql(expected) + expect(cwResult).to.eql(expected) + }) + + it('should create from an outline graphic with arcs', () => { + const result = subject.fromGraphic({ + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.OUTLINE, + segments: [ + { + type: Tree.ARC, + start: [1 + halfSqrtTwo, 2 + halfSqrtTwo, PI / 4], + end: [1 - halfSqrtTwo, 2 + halfSqrtTwo, (3 * PI) / 4], + center: [1, 2], + radius: 1, + }, + { + type: Tree.ARC, + start: [1 - halfSqrtTwo, 2 + halfSqrtTwo, (3 * PI) / 4], + end: [1 - halfSqrtTwo, 2 - halfSqrtTwo, (5 * PI) / 4], + center: [1, 2], + radius: 1, + }, + { + type: Tree.ARC, + start: [1 - halfSqrtTwo, 2 - halfSqrtTwo, (5 * PI) / 4], + end: [1 + halfSqrtTwo, 2 - halfSqrtTwo, (7 * PI) / 4], + center: [1, 2], + radius: 1, + }, + { + type: Tree.ARC, + start: [1 + halfSqrtTwo, 2 + halfSqrtTwo, (7 * PI) / 4], + end: [1 - halfSqrtTwo, 2 + halfSqrtTwo, (9 * PI) / 4], + center: [1, 2], + radius: 1, + }, + ], + }, + }) + + expect(result).to.eql([0, 1, 2, 3]) + }) + + it('should get the size of a region', () => { + const result = subject.fromGraphic({ + type: Tree.IMAGE_REGION, + segments: [{type: Tree.LINE, start: [3, 2], end: [1, 0]}], + }) + + expect(result).to.eql([1, 0, 3, 2]) + }) + + it('should get the size of paths with stroke width', () => { + const result = subject.fromGraphic({ + type: Tree.IMAGE_PATH, + width: 1, + segments: [{type: Tree.LINE, start: [3, 2], end: [1, 0]}], + }) + + expect(result).to.eql([0.5, -0.5, 3.5, 2.5]) + }) + }) +}) diff --git a/packages/plotter/src/__tests__/location-store.test.ts b/packages/plotter/src/__tests__/location-store.test.ts new file mode 100644 index 00000000..0908c193 --- /dev/null +++ b/packages/plotter/src/__tests__/location-store.test.ts @@ -0,0 +1,217 @@ +// Tests for the LocationStore +import {describe, it, beforeEach, expect} from 'vitest' + +import * as Parser from '@tracespace/parser' + +import {PlotOptions} from '../options' +import {LocationStore, createLocationStore} from '../location-store' + +describe('location store', () => { + let subject: LocationStore + let options: PlotOptions + + beforeEach(() => { + subject = createLocationStore() + options = {} as PlotOptions + }) + + it('should return origin by default', () => { + const result = subject.use( + {type: Parser.COMMENT, comment: 'hello'}, + options + ) + + expect(result).to.eql({ + startPoint: {x: 0, y: 0}, + endPoint: {x: 0, y: 0}, + arcOffsets: {i: 0, j: 0, a: 0}, + }) + }) + + it('should parse graphic nodes with decimal coordinates', () => { + const node: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.MOVE, + coordinates: {x: '1.234', y: '5.678'}, + } + const result = subject.use(node, options) + + expect(result).to.eql({ + startPoint: {x: 0, y: 0}, + endPoint: {x: 1.234, y: 5.678}, + arcOffsets: {i: 0, j: 0, a: 0}, + }) + }) + + it('should maintain coordinate state', () => { + const node1: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.MOVE, + coordinates: {x: '1.234', y: '5.678'}, + } + const node2: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.MOVE, + coordinates: {x: '0'}, + } + const node3: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.MOVE, + coordinates: {y: '0'}, + } + const noopNode: Parser.Comment = {type: Parser.COMMENT, comment: 'hello'} + + expect(subject.use(node1, options)).to.eql({ + startPoint: {x: 0, y: 0}, + endPoint: {x: 1.234, y: 5.678}, + arcOffsets: {i: 0, j: 0, a: 0}, + }) + expect(subject.use(noopNode, options)).to.eql({ + startPoint: {x: 1.234, y: 5.678}, + endPoint: {x: 1.234, y: 5.678}, + arcOffsets: {i: 0, j: 0, a: 0}, + }) + expect(subject.use(node2, options)).to.eql({ + startPoint: {x: 1.234, y: 5.678}, + endPoint: {x: 0, y: 5.678}, + arcOffsets: {i: 0, j: 0, a: 0}, + }) + expect(subject.use(noopNode, options)).to.eql({ + startPoint: {x: 0, y: 5.678}, + endPoint: {x: 0, y: 5.678}, + arcOffsets: {i: 0, j: 0, a: 0}, + }) + expect(subject.use(node3, options)).to.eql({ + startPoint: {x: 0, y: 5.678}, + endPoint: {x: 0, y: 0}, + arcOffsets: {i: 0, j: 0, a: 0}, + }) + }) + + it('should parse a coordinate string according to the format', () => { + options = {coordinateFormat: [3, 5]} as PlotOptions + const node: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.MOVE, + coordinates: {x: '12345678'}, + } + + const result = subject.use(node, options) + expect(result.endPoint.x).to.equal(123.456_78) + }) + + it('should parse a coordinate string with leading zero suppression', () => { + options = { + coordinateFormat: [3, 5], + zeroSuppression: Parser.LEADING, + } as PlotOptions + + const node: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.MOVE, + coordinates: {x: '123456'}, + } + + const result = subject.use(node, options) + expect(result.endPoint.x).to.equal(1.234_56) + }) + + it('should parse a coordinate string with trailing zero suppression', () => { + options = { + coordinateFormat: [3, 5], + zeroSuppression: Parser.TRAILING, + } as PlotOptions + + const node: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.MOVE, + coordinates: {x: '123456'}, + } + + const result = subject.use(node, options) + expect(result.endPoint.x).to.equal(123.456) + }) + + it('should parse a coordinate string with an explicit sign', () => { + options = { + coordinateFormat: [3, 3], + zeroSuppression: Parser.LEADING, + } as PlotOptions + + const node: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.MOVE, + coordinates: {x: '+1234', y: '-5678'}, + } + + const result = subject.use(node, options) + expect(result.endPoint.x).to.equal(1.234) + expect(result.endPoint.y).to.equal(-5.678) + }) + + it('should parse `i`, `j` arc coordinates', () => { + options = {coordinateFormat: [1, 2]} as PlotOptions + + const node: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.SEGMENT, + coordinates: {x: '123', y: '456', i: '789', j: '987'}, + } + + const result = subject.use(node, options) + expect(result.endPoint).to.eql({x: 1.23, y: 4.56}) + expect(result.arcOffsets).to.eql({i: 7.89, j: 9.87, a: 0}) + }) + + it('should parse `a` arc coordinates', () => { + options = {coordinateFormat: [1, 2]} as PlotOptions + + const node: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.SEGMENT, + coordinates: {x: '123', y: '456', a: '789'}, + } + + const result = subject.use(node, options) + expect(result.endPoint).to.eql({x: 1.23, y: 4.56}) + expect(result.arcOffsets).to.eql({i: 0, j: 0, a: 7.89}) + }) + + it('should parse `x0`, `y0` slot coordinates', () => { + options = {coordinateFormat: [1, 2]} as PlotOptions + + const node: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.SLOT, + coordinates: {x0: '123', y0: '456', x: '789', y: '987'}, + } + + const result = subject.use(node, options) + expect(result.startPoint).to.eql({x: 1.23, y: 4.56}) + expect(result.endPoint).to.eql({x: 7.89, y: 9.87}) + }) + + it('should feed `x`, `y` with `x0`, `y0` coordinates', () => { + options = {coordinateFormat: [1, 2]} as PlotOptions + + const node1: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.SLOT, + coordinates: {x0: '123', y: '987'}, + } + + const node2: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.SLOT, + coordinates: {y0: '456', x: '789'}, + } + + let result = subject.use(node1, options) + expect(result.startPoint).to.eql({x: 1.23, y: 0}) + expect(result.endPoint).to.eql({x: 1.23, y: 9.87}) + + result = subject.use(node2, options) + expect(result.startPoint).to.eql({x: 1.23, y: 4.56}) + expect(result.endPoint).to.eql({x: 7.89, y: 4.56}) + }) +}) diff --git a/packages/plotter/src/__tests__/main-layer.test.ts b/packages/plotter/src/__tests__/main-layer.test.ts new file mode 100644 index 00000000..dce6e7c4 --- /dev/null +++ b/packages/plotter/src/__tests__/main-layer.test.ts @@ -0,0 +1,79 @@ +// Tests for the MainLayer interface +import {vi, describe, beforeEach, afterEach, it, expect} from 'vitest' +import * as td from 'testdouble' + +import * as Parser from '@tracespace/parser' + +import * as Tree from '../tree' +import * as BoundingBox from '../bounding-box' +import {MainLayer, createMainLayer} from '../main-layer' + +vi.mock('../bounding-box', () => td.object()) + +describe('adding to the main image layer', () => { + let subject: MainLayer + + beforeEach(() => { + td.when(BoundingBox.empty()).thenReturn([0, 0, 0, 0]) + + subject = createMainLayer() + }) + + afterEach(() => { + td.reset() + }) + + it('should create an empty image by default', () => { + const result = subject.get() + + expect(result).to.eql({ + type: Tree.IMAGE_LAYER, + size: [0, 0, 0, 0], + children: [], + }) + }) + + it('should emit the same image if given no graphic', () => { + const result = subject.add({type: Parser.DONE}, []) + + expect(result).to.eql({ + type: Tree.IMAGE_LAYER, + size: [0, 0, 0, 0], + children: [], + }) + }) + + it('should add graphics to the image', () => { + const node1 = {type: Parser.GRAPHIC} as Parser.Graphic + const node2 = {type: Parser.GRAPHIC} as Parser.Graphic + + const graphic1: Tree.ImageGraphic = { + type: Tree.IMAGE_PATH, + width: 1, + segments: [{type: Tree.LINE, start: [2, 3], end: [4, 5]}], + } + const graphic2: Tree.ImageGraphic = { + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.CIRCLE, cx: 6, cy: 7, r: 8}, + } + + const size1: Tree.SizeEnvelope = [9, 10, 11, 12] + const size2: Tree.SizeEnvelope = [13, 14, 15, 16] + const expectedSize: Tree.SizeEnvelope = [17, 18, 19, 20] + + td.when(BoundingBox.fromGraphic(graphic1)).thenReturn(size1) + td.when(BoundingBox.fromGraphic(graphic2)).thenReturn(size2) + + td.when(BoundingBox.add([0, 0, 0, 0], size1)).thenReturn(size1) + td.when(BoundingBox.add(size1, size2)).thenReturn(expectedSize) + + subject.add(node1, [graphic1]) + const result = subject.add(node2, [graphic2]) + + expect(result).to.eql({ + type: Tree.IMAGE_LAYER, + size: expectedSize, + children: [graphic1, graphic2], + }) + }) +}) diff --git a/packages/plotter/src/__tests__/plot.test.ts b/packages/plotter/src/__tests__/plot.test.ts index b94ba329..25d664ff 100644 --- a/packages/plotter/src/__tests__/plot.test.ts +++ b/packages/plotter/src/__tests__/plot.test.ts @@ -1,29 +1,113 @@ -import {vi, describe, it, afterEach, expect} from 'vitest' +import {vi, describe, beforeEach, afterEach, it, expect} from 'vitest' import * as td from 'testdouble' import * as Parser from '@tracespace/parser' import * as Tree from '../tree' import {PlotOptions, getPlotOptions} from '../options' -import {createPlot} from '../plot-tree' -import {plot} from '..' +import {SIMPLE_TOOL, ToolStore, Tool, createToolStore} from '../tool-store' +import {LocationStore, Location, createLocationStore} from '../location-store' +import {MainLayer, createMainLayer} from '../main-layer' +import {GraphicPlotter, createGraphicPlotter} from '../graphic-plotter' -vi.mock('../options', () => td.object()) -vi.mock('../plot-tree', () => td.object()) +import {plot as subject} from '..' + +vi.mock('../options', async () => td.object()) +vi.mock('../tool-store', () => td.object()) +vi.mock('../location-store', () => td.object()) +vi.mock('../main-layer', () => td.object()) +vi.mock('../graphic-plotter', () => td.object()) +vi.mock('../bounding-box', () => td.object()) + +describe('creating a plot tree', () => { + let toolStore: td.TestDouble + let locationStore: td.TestDouble + let mainLayer: td.TestDouble + let graphicPlotter: td.TestDouble + + beforeEach(() => { + toolStore = td.object() + locationStore = td.object() + mainLayer = td.object() + graphicPlotter = td.object() + + td.when(createToolStore(), {times: 1}).thenReturn(toolStore) + td.when(createLocationStore(), {times: 1}).thenReturn(locationStore) + td.when(createMainLayer(), {times: 1}).thenReturn(mainLayer) + td.when(createGraphicPlotter(Parser.GERBER), {times: 1}).thenReturn( + graphicPlotter + ) + }) -describe('plot', () => { afterEach(() => { td.reset() }) it('should get plot options and plot', () => { - const tree = {type: Parser.ROOT} as Parser.GerberTree - const plotOptions = {units: Parser.MM} as PlotOptions - const imageTree = {type: Tree.IMAGE} as Tree.ImageTree + const tree: Parser.Root = { + type: Parser.ROOT, + filetype: Parser.GERBER, + children: [ + {type: Parser.GRAPHIC, graphic: Parser.SHAPE} as Parser.Graphic, + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT} as Parser.Graphic, + ], + } + const [node1, node2] = tree.children + const plotOptions = {units: Parser.MM} as PlotOptions td.when(getPlotOptions(tree)).thenReturn(plotOptions) - td.when(createPlot(tree, plotOptions)).thenReturn(imageTree) - const result = plot(tree) - expect(result).to.eql(imageTree) + const tool1: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 1}, + } + const tool2: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + } + td.when(toolStore.use(node1)).thenReturn(tool1) + td.when(toolStore.use(node2)).thenReturn(tool2) + + const location1: Location = { + startPoint: {x: 1, y: 2}, + endPoint: {x: 3, y: 4}, + arcOffsets: {i: 5, j: 6, a: 7}, + } + const location2: Location = { + startPoint: {x: 5, y: 6}, + endPoint: {x: 7, y: 8}, + arcOffsets: {i: 9, j: 0, a: 1}, + } + td.when(locationStore.use(node1, plotOptions)).thenReturn(location1) + td.when(locationStore.use(node2, plotOptions)).thenReturn(location2) + + const shape1: Tree.ImageShape = { + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.CIRCLE, cx: 1, cy: 2, r: 3}, + } + const shape2: Tree.ImageShape = { + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.CIRCLE, cx: 4, cy: 5, r: 6}, + } + td.when(graphicPlotter.plot(node1, tool1, location1)).thenReturn([shape1]) + td.when(graphicPlotter.plot(node2, tool2, location2)).thenReturn([shape2]) + + const layer1 = { + type: Tree.IMAGE_LAYER, + size: [1, 2, 3, 4], + } as Tree.ImageLayer + const layer2 = { + type: Tree.IMAGE_LAYER, + size: [5, 6, 7, 8], + } as Tree.ImageLayer + td.when(mainLayer.add(node1, [shape1])).thenReturn(layer1) + td.when(mainLayer.add(node2, [shape2])).thenReturn(layer2) + + const result = subject(tree) + + expect(result).to.eql({ + type: Tree.IMAGE, + units: Parser.MM, + children: [layer2], + }) }) }) diff --git a/packages/plotter/src/__tests__/tool-store.test.ts b/packages/plotter/src/__tests__/tool-store.test.ts new file mode 100644 index 00000000..42362dcb --- /dev/null +++ b/packages/plotter/src/__tests__/tool-store.test.ts @@ -0,0 +1,111 @@ +// Tests for the ToolStore interface +import {describe, it, expect} from 'vitest' + +import * as Parser from '@tracespace/parser' + +import {SIMPLE_TOOL, MACRO_TOOL, createToolStore} from '../tool-store' + +describe('tool state store', () => { + it('should handle tool definitions', () => { + const toolDefinition: Parser.ToolDefinition = { + type: Parser.TOOL_DEFINITION, + code: '42', + shape: {type: 'circle', diameter: 42}, + hole: null, + } + + const subject = createToolStore() + const result = subject.use(toolDefinition) + + expect(result).to.eql({ + type: SIMPLE_TOOL, + shape: {type: 'circle', diameter: 42}, + }) + }) + + it('should handle a tool change after a tool definition', () => { + const tool1: Parser.ToolDefinition = { + type: Parser.TOOL_DEFINITION, + code: '1', + shape: {type: 'circle', diameter: 1}, + hole: null, + } + const tool2: Parser.ToolDefinition = { + type: Parser.TOOL_DEFINITION, + code: '2', + shape: {type: 'circle', diameter: 2}, + hole: null, + } + const toolChange: Parser.ToolChange = { + type: Parser.TOOL_CHANGE, + code: '1', + } + + const subject = createToolStore() + + expect(subject.use(tool1)).to.eql({ + type: SIMPLE_TOOL, + shape: {type: 'circle', diameter: 1}, + }) + expect(subject.use(tool2)).to.eql({ + type: SIMPLE_TOOL, + shape: {type: 'circle', diameter: 2}, + }) + expect(subject.use(toolChange)).to.eql({ + type: SIMPLE_TOOL, + shape: {type: 'circle', diameter: 1}, + }) + }) + + it('should keep track of the current tool', () => { + const comment: Parser.Comment = { + type: Parser.COMMENT, + comment: 'hello world', + } + const tool: Parser.ToolDefinition = { + type: Parser.TOOL_DEFINITION, + code: '42', + shape: {type: 'circle', diameter: 42}, + hole: null, + } + + const subject = createToolStore() + + expect(subject.use(comment)).to.equal(undefined) + expect(subject.use(tool)).to.eql({ + type: SIMPLE_TOOL, + shape: {type: 'circle', diameter: 42}, + }) + expect(subject.use(comment)).to.eql({ + type: SIMPLE_TOOL, + shape: {type: 'circle', diameter: 42}, + }) + }) + + it('should track tool macros', () => { + const macro: Parser.ToolMacro = { + type: Parser.TOOL_MACRO, + name: 'cool-macro', + children: [{type: Parser.MACRO_COMMENT, comment: 'hello world'}], + } + const tool: Parser.ToolDefinition = { + type: Parser.TOOL_DEFINITION, + code: '42', + shape: { + type: Parser.MACRO_SHAPE, + name: 'cool-macro', + variableValues: [1, 2, 3], + }, + hole: null, + } + + const subject = createToolStore() + + expect(subject.use(macro)).to.equal(undefined) + expect(subject.use(tool)).to.eql({ + type: MACRO_TOOL, + macro: [{type: Parser.MACRO_COMMENT, comment: 'hello world'}], + variableValues: [1, 2, 3], + }) + }) +}) diff --git a/packages/plotter/src/bounding-box.ts b/packages/plotter/src/bounding-box.ts new file mode 100644 index 00000000..657fd9a7 --- /dev/null +++ b/packages/plotter/src/bounding-box.ts @@ -0,0 +1,115 @@ +import * as Tree from './tree' +import {TWO_PI, limitAngle, rotateQuadrant} from './coordinate-math' +import type {SizeEnvelope as Box} from './tree' + +export type {SizeEnvelope as Box} from './tree' + +export type ViewBox = [xMin: number, yMin: number, xSize: number, ySize: number] + +export function isEmpty(box: Box): box is [] { + return box.length === 0 +} + +export function empty(): Box { + return [] +} + +export function add(a: Box, b: Box): Box { + if (isEmpty(a)) return b + if (isEmpty(b)) return a + + return [ + Math.min(a[0], b[0]), + Math.min(a[1], b[1]), + Math.max(a[2], b[2]), + Math.max(a[3], b[3]), + ] +} + +export function toViewBox(box: Box): ViewBox { + return isEmpty(box) + ? [0, 0, 0, 0] + : [box[0], box[1], box[2] - box[0], box[3] - box[1]] +} + +export function fromGraphic(graphic: Tree.ImageGraphic): Box { + return graphic.type === Tree.IMAGE_SHAPE + ? fromShape(graphic.shape) + : fromPath( + graphic.segments, + graphic.type === Tree.IMAGE_PATH ? graphic.width : undefined + ) +} + +export function fromShape(shape: Tree.Shape): Box { + switch (shape.type) { + case Tree.CIRCLE: { + const {cx, cy, r} = shape + return [cx - r, cy - r, cx + r, cy + r] + } + + case Tree.RECTANGLE: { + const {x, y, xSize, ySize} = shape + return [x, y, x + xSize, y + ySize] + } + + case Tree.POLYGON: { + return shape.points + .map(([x, y]) => [x, y, x, y]) + .reduce(add, empty()) + } + + case Tree.OUTLINE: { + return fromPath(shape.segments) + } + + case Tree.LAYERED_SHAPE: { + return shape.shapes + .filter(({erase}) => !erase) + .map(fromShape) + .reduce(add, empty()) + } + } +} + +function fromPath(segments: Tree.PathSegment[], width = 0): Box { + const rTool = width / 2 + const keyPoints: Array = [] + + for (const segment of segments) { + keyPoints.push(segment.start, segment.end) + + if (segment.type === Tree.ARC) { + const {start, end, center, radius} = segment + const sweep = Math.abs(end[2] - start[2]) + + // Normalize direction to counter-clockwise + let [thetaStart, thetaEnd] = + end[2] > start[2] ? [start[2], end[2]] : [end[2], start[2]] + + thetaStart = limitAngle(thetaStart) + thetaEnd = limitAngle(thetaEnd) + + const axisPoints: Tree.Position[] = [ + [center[0] + radius, center[1]], + [center[0], center[1] + radius], + [center[0] - radius, center[1]], + [center[0], center[1] - radius], + ] + + for (const p of axisPoints) { + if (thetaStart > thetaEnd || sweep === TWO_PI) { + keyPoints.push(p) + } + + // Rotate to check for next axis key point + thetaStart = rotateQuadrant(thetaStart) + thetaEnd = rotateQuadrant(thetaEnd) + } + } + } + + return keyPoints + .map(([x, y]) => [x - rTool, y - rTool, x + rTool, y + rTool]) + .reduce(add, empty()) +} diff --git a/packages/plotter/src/coordinate-math.ts b/packages/plotter/src/coordinate-math.ts new file mode 100644 index 00000000..8db54530 --- /dev/null +++ b/packages/plotter/src/coordinate-math.ts @@ -0,0 +1,40 @@ +// Mathematical procedures +import {Position} from './tree' + +export const {PI} = Math +export const HALF_PI = PI / 2 +export const THREE_HALF_PI = 3 * HALF_PI +export const TWO_PI = 2 * PI + +export function limitAngle(theta: number): number { + if (theta >= 0 && theta <= TWO_PI) return theta + if (theta < 0) return theta + TWO_PI + if (theta > TWO_PI) return theta - TWO_PI + return limitAngle(theta) +} + +export function rotateQuadrant(theta: number): number { + return theta >= HALF_PI ? theta - HALF_PI : theta + THREE_HALF_PI +} + +export function degreesToRadians(degrees: number): number { + return (degrees * Math.PI) / 180 +} + +export function rotateAndShift( + point: Position, + shift: Position, + degrees = 0 +): Position { + const rotation = degreesToRadians(degrees) + const [sin, cos] = [Math.sin(rotation), Math.cos(rotation)] + const [x, y] = point + const nextX = x * cos - y * sin + shift[0] + const nextY = x * sin + y * cos + shift[1] + + return [nextX, nextY] +} + +export function positionsEqual(a: number[], b: number[]): boolean { + return a[0] === b[0] && a[1] === b[1] +} diff --git a/packages/plotter/src/graphic-plotter/__tests__/plot-drill.test.ts b/packages/plotter/src/graphic-plotter/__tests__/plot-drill.test.ts new file mode 100644 index 00000000..7a7e2315 --- /dev/null +++ b/packages/plotter/src/graphic-plotter/__tests__/plot-drill.test.ts @@ -0,0 +1,207 @@ +// Tests for plotting drill file objects using the GraphicPlotter interface +import {describe, it, beforeEach, expect} from 'vitest' + +import * as Parser from '@tracespace/parser' + +import * as Tree from '../../tree' +import {SIMPLE_TOOL, Tool} from '../../tool-store' +import {Location} from '../../location-store' +import {PI} from '../../coordinate-math' +import {GraphicPlotter, createGraphicPlotter} from '..' + +type SubjectCall = Parameters +type SubjectReturn = ReturnType + +const subject = (...calls: Array>): SubjectReturn => { + const plotter = createGraphicPlotter(Parser.DRILL) + return calls.flatMap(call => plotter.plot(...(call as SubjectCall))) +} + +describe('plot drill file graphics', () => { + let node: Parser.ChildNode + + beforeEach(() => { + node = {type: Parser.GRAPHIC, graphic: null, coordinates: {}} + }) + + it('should plot a drill hit', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + } + const location = {endPoint: {x: 3, y: 4}} as Location + + const results = subject([node, tool, location]) + + expect(results).to.eql([ + { + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.CIRCLE, cx: 3, cy: 4, r: 1}, + }, + ]) + }) + + it('should plot a drill slot', () => { + node = {type: Parser.GRAPHIC, graphic: Parser.SLOT, coordinates: {}} + + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + } + const location = { + startPoint: {x: 1, y: 2}, + endPoint: {x: 3, y: 4}, + } as Location + + const results = subject([node, tool, location]) + + expect(results).to.eql([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [{type: Tree.LINE, start: [1, 2], end: [3, 4]}], + }, + ]) + }) + + it('should plot linear drill routes', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + } + const location1 = { + startPoint: {x: 1, y: 2}, + endPoint: {x: 3, y: 4}, + } as Location + const location2 = { + startPoint: {x: 5, y: 6}, + endPoint: {x: 7, y: 8}, + } as Location + + const results = subject( + [{type: Parser.INTERPOLATE_MODE, mode: Parser.LINE}], + [node, tool, location1], + [{type: Parser.INTERPOLATE_MODE, mode: Parser.MOVE}], + [{type: Parser.INTERPOLATE_MODE, mode: Parser.LINE}], + [node, tool, location2], + [{type: Parser.DONE}] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [{type: Tree.LINE, start: [1, 2], end: [3, 4]}], + }, + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [{type: Tree.LINE, start: [5, 6], end: [7, 8]}], + }, + ]) + }) + + it('should plot a CCW arc drill route', () => { + const halfSqrtTwo = 2 ** 0.5 / 2 + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + } + const location: Location = { + startPoint: {x: 1, y: 0}, + endPoint: {x: halfSqrtTwo, y: halfSqrtTwo}, + arcOffsets: {i: 0, j: 0, a: 1}, + } + + const results = subject( + [{type: Parser.INTERPOLATE_MODE, mode: Parser.CCW_ARC}], + [node, tool, location], + [{type: Parser.DONE}] + ) + + expect(results).toEqual([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [ + { + type: Tree.ARC, + start: [1, 0, expect.approx(0)], + end: [halfSqrtTwo, halfSqrtTwo, PI / 4], + center: [expect.approx(0), expect.approx(0)], + radius: 1, + }, + ], + }, + ]) + }) + + it('should plot a CW arc drill route', () => { + const halfSqrtTwo = 2 ** 0.5 / 2 + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + } + const location: Location = { + startPoint: {x: halfSqrtTwo, y: halfSqrtTwo}, + endPoint: {x: 1, y: 0}, + arcOffsets: {i: 0, j: 0, a: 1}, + } + + const results = subject( + [{type: Parser.INTERPOLATE_MODE, mode: Parser.CW_ARC}], + [node, tool, location], + [{type: Parser.DONE}] + ) + + expect(results).toEqual([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [ + { + type: Tree.ARC, + start: [halfSqrtTwo, halfSqrtTwo, PI / 4], + end: [1, 0, expect.approx(0)], + center: [expect.approx(0), expect.approx(0)], + radius: 1, + }, + ], + }, + ]) + }) + + it('should transition from route mode back to drill mode', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + } + const location1 = { + startPoint: {x: 1, y: 2}, + endPoint: {x: 3, y: 4}, + } as Location + const location2 = { + startPoint: {x: 5, y: 6}, + endPoint: {x: 7, y: 8}, + } as Location + + const results = subject( + [{type: Parser.INTERPOLATE_MODE, mode: Parser.LINE}], + [node, tool, location1], + [{type: Parser.INTERPOLATE_MODE, mode: Parser.DRILL}], + [node, tool, location2] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [{type: Tree.LINE, start: [1, 2], end: [3, 4]}], + }, + { + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.CIRCLE, cx: 7, cy: 8, r: 1}, + }, + ]) + }) +}) diff --git a/packages/plotter/src/graphic-plotter/__tests__/plot-path-arc.test.ts b/packages/plotter/src/graphic-plotter/__tests__/plot-path-arc.test.ts new file mode 100644 index 00000000..b52851dc --- /dev/null +++ b/packages/plotter/src/graphic-plotter/__tests__/plot-path-arc.test.ts @@ -0,0 +1,228 @@ +import {describe, it, expect} from 'vitest' + +import * as Parser from '@tracespace/parser' + +import * as Tree from '../../tree' +import {SIMPLE_TOOL, Tool} from '../../tool-store' +import {Location} from '../../location-store' +import {HALF_PI, PI, TWO_PI} from '../../coordinate-math' + +import {GraphicPlotter, createGraphicPlotter} from '..' + +type SubjectCall = Parameters +type SubjectReturn = ReturnType + +const subject = (...calls: Array>): SubjectReturn => { + const plotter = createGraphicPlotter(Parser.GERBER) + return calls.flatMap(call => plotter.plot(...(call as SubjectCall))) +} + +describe('plot stroke arc paths', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + } + + it('should plot a zero-length CCW arc as a full circle', () => { + const location: Location = { + startPoint: {x: 1, y: 0}, + endPoint: {x: 1, y: 0}, + arcOffsets: {i: -1, j: 0, a: 0}, + } + + const results = subject( + [{type: Parser.INTERPOLATE_MODE, mode: Parser.CCW_ARC}], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location, + ], + [{type: Parser.DONE}] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [ + { + type: Tree.ARC, + start: [1, 0, 0], + end: [1, 0, TWO_PI], + center: [0, 0], + radius: 1, + }, + ], + }, + ]) + }) + + it('should plot a zero-length CW arc as a full circle', () => { + const location: Location = { + startPoint: {x: 1, y: 0}, + endPoint: {x: 1, y: 0}, + arcOffsets: {i: -1, j: 0, a: 0}, + } + + const results = subject( + [{type: Parser.INTERPOLATE_MODE, mode: Parser.CW_ARC}], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location, + ], + [{type: Parser.DONE}] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [ + { + type: Tree.ARC, + start: [1, 0, TWO_PI], + end: [1, 0, 0], + center: [0, 0], + radius: 1, + }, + ], + }, + ]) + }) + + it('should plot a CCW arc with length', () => { + const location: Location = { + startPoint: {x: 0, y: 1}, + endPoint: {x: -1, y: 0}, + arcOffsets: {i: 0, j: -1, a: 0}, + } + + const results = subject( + [{type: Parser.INTERPOLATE_MODE, mode: Parser.CCW_ARC}], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location, + ], + [{type: Parser.DONE}] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [ + { + type: Tree.ARC, + start: [0, 1, HALF_PI], + end: [-1, 0, PI], + center: [0, 0], + radius: 1, + }, + ], + }, + ]) + }) + + describe('single quadrant mode', () => { + const halfSqrtTwo = 2 ** 0.5 / 2 + + it('should plot a zero-length arc as a zero-length line', () => { + const location: Location = { + startPoint: {x: 1, y: 0}, + endPoint: {x: 1, y: 0}, + arcOffsets: {i: -1, j: 0, a: 0}, + } + + const results = subject( + [{type: Parser.QUADRANT_MODE, quadrant: Parser.SINGLE}], + [{type: Parser.INTERPOLATE_MODE, mode: Parser.CCW_ARC}], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location, + ], + [{type: Parser.DONE}] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [{type: Tree.LINE, start: [1, 0], end: [1, 0]}], + }, + ]) + }) + + it('should select the proper center for an offset in `i`', () => { + const location: Location = { + startPoint: {x: 1, y: 0}, + endPoint: {x: halfSqrtTwo, y: halfSqrtTwo}, + arcOffsets: {i: 1, j: 0, a: 0}, + } + + const results = subject( + [{type: Parser.QUADRANT_MODE, quadrant: Parser.SINGLE}], + [{type: Parser.INTERPOLATE_MODE, mode: Parser.CCW_ARC}], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location, + ], + [{type: Parser.DONE}] + ) + + expect(results).toEqual([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [ + { + type: Tree.ARC, + start: [1, 0, expect.approx(0)], + end: [halfSqrtTwo, halfSqrtTwo, PI / 4], + center: [expect.approx(0), expect.approx(0)], + radius: 1, + }, + ], + }, + ]) + }) + + it('should select the proper center for an offset in `j`', () => { + const location: Location = { + startPoint: {x: 0, y: 1}, + endPoint: {x: halfSqrtTwo, y: halfSqrtTwo}, + arcOffsets: {i: 0, j: 1, a: 0}, + } + + const results = subject( + [{type: Parser.QUADRANT_MODE, quadrant: Parser.SINGLE}], + [{type: Parser.INTERPOLATE_MODE, mode: Parser.CW_ARC}], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location, + ], + [{type: Parser.DONE}] + ) + + expect(results).toEqual([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [ + { + type: Tree.ARC, + start: [0, 1, expect.approx(HALF_PI)], + end: [halfSqrtTwo, halfSqrtTwo, PI / 4], + center: [expect.approx(0), expect.approx(0)], + radius: 1, + }, + ], + }, + ]) + }) + }) +}) diff --git a/packages/plotter/src/graphic-plotter/__tests__/plot-path-region.test.ts b/packages/plotter/src/graphic-plotter/__tests__/plot-path-region.test.ts new file mode 100644 index 00000000..a8a26c5d --- /dev/null +++ b/packages/plotter/src/graphic-plotter/__tests__/plot-path-region.test.ts @@ -0,0 +1,192 @@ +import {describe, it, expect} from 'vitest' + +import * as Parser from '@tracespace/parser' + +import * as Tree from '../../tree' +import {SIMPLE_TOOL, Tool} from '../../tool-store' +import {Location} from '../../location-store' +import {GraphicPlotter, createGraphicPlotter} from '..' + +type SubjectCall = Parameters +type SubjectReturn = ReturnType + +const subject = (...calls: Array>): SubjectReturn => { + const plotter = createGraphicPlotter(Parser.GERBER) + return calls.flatMap(call => plotter.plot(...(call as SubjectCall))) +} + +describe('plot stroke paths', () => { + const location1 = { + startPoint: {x: 0, y: 0}, + endPoint: {x: 1, y: 0}, + } as Location + + const location2 = { + startPoint: {x: 1, y: 0}, + endPoint: {x: 1, y: 1}, + } as Location + + const location3 = { + startPoint: {x: 1, y: 1}, + endPoint: {x: 0, y: 0}, + } as Location + + it('should plot a region', () => { + const results = subject( + [{type: Parser.REGION_MODE, region: true}], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + undefined, + location1, + ], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + undefined, + location2, + ], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + undefined, + location3, + ], + [{type: Parser.DONE}] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_REGION, + segments: [ + {type: Tree.LINE, start: [0, 0], end: [1, 0]}, + {type: Tree.LINE, start: [1, 0], end: [1, 1]}, + {type: Tree.LINE, start: [1, 1], end: [0, 0]}, + ], + }, + ]) + }) + + it('should plot a region in between plotting paths', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + } + + const location0 = { + startPoint: {x: -10, y: -10}, + endPoint: {x: -5, y: -5}, + } as Location + + const location4 = { + startPoint: {x: 5, y: 5}, + endPoint: {x: 10, y: 10}, + } as Location + + const results = subject( + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location0, + ], + [{type: Parser.REGION_MODE, region: true}], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location1, + ], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location2, + ], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location3, + ], + [{type: Parser.REGION_MODE, region: false}], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location4, + ], + [{type: Parser.DONE}] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [{type: Tree.LINE, start: [-10, -10], end: [-5, -5]}], + }, + { + type: Tree.IMAGE_REGION, + segments: [ + {type: Tree.LINE, start: [0, 0], end: [1, 0]}, + {type: Tree.LINE, start: [1, 0], end: [1, 1]}, + {type: Tree.LINE, start: [1, 1], end: [0, 0]}, + ], + }, + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [{type: Tree.LINE, start: [5, 5], end: [10, 10]}], + }, + ]) + }) + + it('should plot multiple regions upon move node', () => { + const results = subject( + [{type: Parser.REGION_MODE, region: true}], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + undefined, + location1, + ], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + undefined, + location2, + ], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + undefined, + location3, + ], + [{type: Parser.GRAPHIC, graphic: Parser.MOVE, coordinates: {}}], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + undefined, + location1, + ], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + undefined, + location2, + ], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + undefined, + location3, + ], + [{type: Parser.DONE}] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_REGION, + segments: [ + {type: Tree.LINE, start: [0, 0], end: [1, 0]}, + {type: Tree.LINE, start: [1, 0], end: [1, 1]}, + {type: Tree.LINE, start: [1, 1], end: [0, 0]}, + ], + }, + { + type: Tree.IMAGE_REGION, + segments: [ + {type: Tree.LINE, start: [0, 0], end: [1, 0]}, + {type: Tree.LINE, start: [1, 0], end: [1, 1]}, + {type: Tree.LINE, start: [1, 1], end: [0, 0]}, + ], + }, + ]) + }) +}) diff --git a/packages/plotter/src/graphic-plotter/__tests__/plot-path.test.ts b/packages/plotter/src/graphic-plotter/__tests__/plot-path.test.ts new file mode 100644 index 00000000..8d5afd3f --- /dev/null +++ b/packages/plotter/src/graphic-plotter/__tests__/plot-path.test.ts @@ -0,0 +1,223 @@ +import {describe, it, expect} from 'vitest' + +import * as Parser from '@tracespace/parser' + +import * as Tree from '../../tree' +import {SIMPLE_TOOL, Tool} from '../../tool-store' +import {Location} from '../../location-store' +import {GraphicPlotter, createGraphicPlotter} from '..' + +type SubjectArgs = Parameters +type SubjectReturn = ReturnType + +const subject = (...calls: Array>): SubjectReturn => { + const plotter = createGraphicPlotter(Parser.GERBER) + return calls.flatMap(call => plotter.plot(...(call as SubjectArgs))) +} + +describe('plot stroke paths', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + } + + it('should not plot anything if no path', () => { + const location = { + startPoint: {x: 3, y: 4}, + endPoint: {x: 5, y: 6}, + } as Location + + const results = subject([{type: Parser.DONE}, tool, location]) + expect(results).to.eql([]) + }) + + it('should plot a line segment', () => { + const location = { + startPoint: {x: 3, y: 4}, + endPoint: {x: 5, y: 6}, + } as Location + + const results = subject( + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location, + ], + [{type: Parser.DONE}] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [{type: Tree.LINE, start: [3, 4], end: [5, 6]}], + }, + ]) + }) + + it('should plot several line segments', () => { + const location1 = { + startPoint: {x: 3, y: 4}, + endPoint: {x: 5, y: 6}, + } as Location + const location2 = { + startPoint: {x: 7, y: 8}, + endPoint: {x: 9, y: 10}, + } as Location + + const results = subject( + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location1, + ], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location2, + ], + [{type: Parser.DONE}, tool, location2] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [ + {type: Tree.LINE, start: [3, 4], end: [5, 6]}, + {type: Tree.LINE, start: [7, 8], end: [9, 10]}, + ], + }, + ]) + }) + + it('should plot several line segments with several tools', () => { + const tool1: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 1}, + } + const location1 = { + startPoint: {x: 3, y: 4}, + endPoint: {x: 5, y: 6}, + } as Location + + const tool2: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + } + const location2 = { + startPoint: {x: 7, y: 8}, + endPoint: {x: 9, y: 10}, + } as Location + + const results = subject( + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool1, + location1, + ], + [{type: Parser.TOOL_CHANGE, code: '123'}, tool2, location1], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool2, + location2, + ], + [{type: Parser.TOOL_CHANGE, code: '456'}, tool1, location2] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_PATH, + width: 1, + segments: [{type: Tree.LINE, start: [3, 4], end: [5, 6]}], + }, + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [{type: Tree.LINE, start: [7, 8], end: [9, 10]}], + }, + ]) + }) + + it('should plot line segments and shapes', () => { + const location1 = {endPoint: {x: 3, y: 4}} as Location + const location2 = { + startPoint: {x: 5, y: 6}, + endPoint: {x: 7, y: 8}, + } as Location + const location3 = {endPoint: {x: 9, y: 10}} as Location + + const results = subject( + [ + {type: Parser.GRAPHIC, graphic: Parser.SHAPE, coordinates: {}}, + tool, + location1, + ], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location2, + ], + [ + {type: Parser.GRAPHIC, graphic: Parser.SHAPE, coordinates: {}}, + tool, + location3, + ] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.CIRCLE, cx: 3, cy: 4, r: 1}, + }, + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [{type: Tree.LINE, start: [5, 6], end: [7, 8]}], + }, + { + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.CIRCLE, cx: 9, cy: 10, r: 1}, + }, + ]) + }) + + it('should continue to plot paths without explicit graphics mode set', () => { + const location1 = {endPoint: {x: 3, y: 4}} as Location + const location2 = { + startPoint: {x: 5, y: 6}, + endPoint: {x: 7, y: 8}, + } as Location + + const results = subject( + [ + {type: Parser.GRAPHIC, graphic: Parser.SHAPE, coordinates: {}}, + tool, + location1, + ], + [{type: Parser.GRAPHIC, graphic: null, coordinates: {}}, tool, location1], + [ + {type: Parser.GRAPHIC, graphic: Parser.SEGMENT, coordinates: {}}, + tool, + location2, + ], + [{type: Parser.GRAPHIC, graphic: null, coordinates: {}}, tool, location2], + [{type: Parser.DONE}] + ) + + expect(results).to.eql([ + { + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.CIRCLE, cx: 3, cy: 4, r: 1}, + }, + { + type: Tree.IMAGE_PATH, + width: 2, + segments: [ + {type: Tree.LINE, start: [5, 6], end: [7, 8]}, + {type: Tree.LINE, start: [5, 6], end: [7, 8]}, + ], + }, + ]) + }) +}) diff --git a/packages/plotter/src/graphic-plotter/__tests__/plot-shape-macro.test.ts b/packages/plotter/src/graphic-plotter/__tests__/plot-shape-macro.test.ts new file mode 100644 index 00000000..47911c4d --- /dev/null +++ b/packages/plotter/src/graphic-plotter/__tests__/plot-shape-macro.test.ts @@ -0,0 +1,49 @@ +import {describe, it, beforeEach, expect} from 'vitest' + +import * as Parser from '@tracespace/parser' + +import * as Tree from '../../tree' +import {MACRO_TOOL, Tool} from '../../tool-store' +import {Location} from '../../location-store' + +import {GraphicPlotter, createGraphicPlotter} from '..' + +describe('plot shape macros', () => { + const node: Parser.Graphic = { + type: Parser.GRAPHIC, + graphic: Parser.SHAPE, + coordinates: {}, + } + let subject: GraphicPlotter + + beforeEach(() => { + subject = createGraphicPlotter(Parser.GERBER) + }) + + it('should plot a circle primitive', () => { + const location = {endPoint: {x: 1, y: 2}} as Location + const tool: Tool = { + type: MACRO_TOOL, + variableValues: [], + macro: [ + { + type: Parser.MACRO_PRIMITIVE, + code: Parser.MACRO_CIRCLE, + parameters: [1, 4, 3, 4, 0], + }, + ], + } + + const results = subject.plot(node, tool, location) + + expect(results).to.eql([ + { + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.LAYERED_SHAPE, + shapes: [{type: Tree.CIRCLE, cx: 4, cy: 6, r: 2, erase: false}], + }, + }, + ]) + }) +}) diff --git a/packages/plotter/src/graphic-plotter/__tests__/plot-shape.test.ts b/packages/plotter/src/graphic-plotter/__tests__/plot-shape.test.ts new file mode 100644 index 00000000..e97d9810 --- /dev/null +++ b/packages/plotter/src/graphic-plotter/__tests__/plot-shape.test.ts @@ -0,0 +1,430 @@ +// Tests for plotting simple shapes using the GraphicPlotter interface +import {describe, it, beforeEach, expect} from 'vitest' + +import * as Parser from '@tracespace/parser' + +import * as Tree from '../../tree' +import {SIMPLE_TOOL, Tool} from '../../tool-store' +import {Location} from '../../location-store' +import {HALF_PI, PI, THREE_HALF_PI, TWO_PI} from '../../coordinate-math' +import {GraphicPlotter, createGraphicPlotter} from '..' + +describe('plot shape graphics', () => { + let subject: GraphicPlotter + let node: Parser.Graphic + + beforeEach(() => { + subject = createGraphicPlotter(Parser.GERBER) + node = {type: Parser.GRAPHIC, graphic: Parser.SHAPE, coordinates: {}} + }) + + it('should plot a circle', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + } + const location = {endPoint: {x: 3, y: 4}} as Location + + const results = subject.plot(node, tool, location) + + expect(results).to.eql([ + { + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.CIRCLE, cx: 3, cy: 4, r: 1}, + }, + ]) + }) + + it('should plot a rectangle', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.RECTANGLE, xSize: 6, ySize: 7}, + } + const location = {endPoint: {x: 2, y: -1}} as Location + + const results = subject.plot(node, tool, location) + + expect(results).to.eql([ + { + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.RECTANGLE, x: -1, y: -4.5, xSize: 6, ySize: 7}, + }, + ]) + }) + + it('should plot an obround tool in portrait', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.OBROUND, xSize: 6, ySize: 8}, + } + const location = {endPoint: {x: 1, y: 2}} as Location + + const results = subject.plot(node, tool, location) + + expect(results).to.eql([ + { + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.RECTANGLE, x: -2, y: -2, xSize: 6, ySize: 8, r: 3}, + }, + ]) + }) + + it('should plot an obround tool in landscape', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.OBROUND, xSize: 8, ySize: 6}, + } + const location = {endPoint: {x: 1, y: 2}} as Location + + const results = subject.plot(node, tool, location) + + expect(results).to.eql([ + { + type: Tree.IMAGE_SHAPE, + shape: {type: Tree.RECTANGLE, x: -3, y: -1, xSize: 8, ySize: 6, r: 3}, + }, + ]) + }) + + it('should plot a polygon', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.POLYGON, diameter: 16, vertices: 4, rotation: null}, + } + const location = {endPoint: {x: 2, y: 2}} as Location + + const results = subject.plot(node, tool, location) + + expect(results).toEqual([ + { + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.POLYGON, + points: [ + [expect.approx(10), expect.approx(2)], + [expect.approx(2), expect.approx(10)], + [expect.approx(-6), expect.approx(2)], + [expect.approx(2), expect.approx(-6)], + ], + }, + }, + ]) + }) + + describe('with circle holes', () => { + it('should plot a circle', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + hole: {type: Parser.CIRCLE, diameter: 1}, + } + const location = {endPoint: {x: 3, y: 4}} as Location + + const results = subject.plot(node, tool, location) + + expect(results).to.eql([ + { + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.OUTLINE, + segments: [ + { + type: Tree.ARC, + center: [3, 4], + start: [4, 4, 0], + end: [4, 4, TWO_PI], + radius: 1, + }, + { + type: Tree.ARC, + center: [3, 4], + start: [3.5, 4, 0], + end: [3.5, 4, TWO_PI], + radius: 0.5, + }, + ], + }, + }, + ]) + }) + + it('should plot a rectangle', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.RECTANGLE, xSize: 6, ySize: 7}, + hole: {type: Parser.CIRCLE, diameter: 1}, + } + const location = {endPoint: {x: 2, y: -1}} as Location + + const results = subject.plot(node, tool, location) + + expect(results).to.eql([ + { + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.OUTLINE, + segments: [ + {type: Tree.LINE, start: [-1, -4.5], end: [5, -4.5]}, + {type: Tree.LINE, start: [5, -4.5], end: [5, 2.5]}, + {type: Tree.LINE, start: [5, 2.5], end: [-1, 2.5]}, + {type: Tree.LINE, start: [-1, 2.5], end: [-1, -4.5]}, + { + type: Tree.ARC, + center: [2, -1], + start: [2.5, -1, 0], + end: [2.5, -1, TWO_PI], + radius: 0.5, + }, + ], + }, + }, + ]) + }) + + it('should plot an obround tool in portrait', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.OBROUND, xSize: 6, ySize: 8}, + hole: {type: Parser.CIRCLE, diameter: 1}, + } + const location = {endPoint: {x: 1, y: 2}} as Location + + const results = subject.plot(node, tool, location) + + expect(results).to.eql([ + { + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.OUTLINE, + segments: [ + {type: Tree.LINE, start: [4, 1], end: [4, 3]}, + { + type: Tree.ARC, + center: [1, 3], + start: [4, 3, 0], + end: [-2, 3, PI], + radius: 3, + }, + {type: Tree.LINE, start: [-2, 3], end: [-2, 1]}, + { + type: Tree.ARC, + center: [1, 1], + start: [-2, 1, PI], + end: [4, 1, TWO_PI], + radius: 3, + }, + { + type: Tree.ARC, + start: [1.5, 2, 0], + end: [1.5, 2, TWO_PI], + center: [1, 2], + radius: 0.5, + }, + ], + }, + }, + ]) + }) + + it('should plot an obround tool in landscape', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.OBROUND, xSize: 8, ySize: 6}, + hole: {type: Parser.CIRCLE, diameter: 1}, + } + const location = {endPoint: {x: 1, y: 2}} as Location + + const results = subject.plot(node, tool, location) + + expect(results).to.eql([ + { + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.OUTLINE, + segments: [ + {type: Tree.LINE, start: [0, -1], end: [2, -1]}, + { + type: Tree.ARC, + center: [2, 2], + start: [2, -1, -HALF_PI], + end: [2, 5, HALF_PI], + radius: 3, + }, + {type: Tree.LINE, start: [2, 5], end: [0, 5]}, + { + type: Tree.ARC, + center: [0, 2], + start: [0, 5, HALF_PI], + end: [0, -1, THREE_HALF_PI], + radius: 3, + }, + { + type: Tree.ARC, + start: [1.5, 2, 0], + end: [1.5, 2, TWO_PI], + center: [1, 2], + radius: 0.5, + }, + ], + }, + }, + ]) + }) + + it('should plot a polygon', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: { + type: Parser.POLYGON, + diameter: 16, + vertices: 4, + rotation: null, + }, + hole: {type: Parser.CIRCLE, diameter: 1}, + } + const location = {endPoint: {x: 2, y: 2}} as Location + + const results = subject.plot(node, tool, location) + + expect(results).toEqual([ + { + type: Tree.IMAGE_SHAPE, + shape: { + type: Tree.OUTLINE, + segments: [ + { + type: Tree.LINE, + start: [expect.approx(10), expect.approx(2)], + end: [expect.approx(2), expect.approx(10)], + }, + { + type: Tree.LINE, + start: [expect.approx(2), expect.approx(10)], + end: [expect.approx(-6), expect.approx(2)], + }, + { + type: Tree.LINE, + start: [expect.approx(-6), expect.approx(2)], + end: [expect.approx(2), expect.approx(-6)], + }, + { + type: Tree.LINE, + start: [expect.approx(2), expect.approx(-6)], + end: [expect.approx(10), expect.approx(2)], + }, + { + type: Tree.ARC, + start: [2.5, 2, 0], + end: [2.5, 2, TWO_PI], + center: [2, 2], + radius: 0.5, + }, + ], + }, + }, + ]) + }) + }) + + describe('with rectangle holes', () => { + it('should plot a circle', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.CIRCLE, diameter: 2}, + hole: {type: Parser.RECTANGLE, xSize: 1, ySize: 1}, + } + const location = {endPoint: {x: 3, y: 4}} as Location + + const results = subject.plot(node, tool, location) as Tree.ImageShape[] + const resultShape = results[0].shape as Tree.OutlineShape + + expect(resultShape.segments.slice(-4)).to.eql([ + {type: Tree.LINE, start: [2.5, 3.5], end: [3.5, 3.5]}, + {type: Tree.LINE, start: [3.5, 3.5], end: [3.5, 4.5]}, + {type: Tree.LINE, start: [3.5, 4.5], end: [2.5, 4.5]}, + {type: Tree.LINE, start: [2.5, 4.5], end: [2.5, 3.5]}, + ]) + }) + + it('should plot a rectangle', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.RECTANGLE, xSize: 6, ySize: 7}, + hole: {type: Parser.RECTANGLE, xSize: 1, ySize: 1}, + } + const location = {endPoint: {x: 2, y: -1}} as Location + + const results = subject.plot(node, tool, location) as Tree.ImageShape[] + const resultShape = results[0].shape as Tree.OutlineShape + + expect(resultShape.segments.slice(-4)).to.eql([ + {type: Tree.LINE, start: [1.5, -1.5], end: [2.5, -1.5]}, + {type: Tree.LINE, start: [2.5, -1.5], end: [2.5, -0.5]}, + {type: Tree.LINE, start: [2.5, -0.5], end: [1.5, -0.5]}, + {type: Tree.LINE, start: [1.5, -0.5], end: [1.5, -1.5]}, + ]) + }) + + it('should plot an obround tool in portrait', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.OBROUND, xSize: 6, ySize: 8}, + hole: {type: Parser.RECTANGLE, xSize: 1, ySize: 1}, + } + const location = {endPoint: {x: 1, y: 2}} as Location + + const results = subject.plot(node, tool, location) as Tree.ImageShape[] + const resultShape = results[0].shape as Tree.OutlineShape + + expect(resultShape.segments.slice(-4)).to.eql([ + {type: Tree.LINE, start: [0.5, 1.5], end: [1.5, 1.5]}, + {type: Tree.LINE, start: [1.5, 1.5], end: [1.5, 2.5]}, + {type: Tree.LINE, start: [1.5, 2.5], end: [0.5, 2.5]}, + {type: Tree.LINE, start: [0.5, 2.5], end: [0.5, 1.5]}, + ]) + }) + + it('should plot an obround tool in landscape', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: {type: Parser.OBROUND, xSize: 8, ySize: 6}, + hole: {type: Parser.RECTANGLE, xSize: 1, ySize: 1}, + } + const location = {endPoint: {x: 1, y: 2}} as Location + + const results = subject.plot(node, tool, location) as Tree.ImageShape[] + const resultShape = results[0].shape as Tree.OutlineShape + + expect(resultShape.segments.slice(-4)).to.eql([ + {type: Tree.LINE, start: [0.5, 1.5], end: [1.5, 1.5]}, + {type: Tree.LINE, start: [1.5, 1.5], end: [1.5, 2.5]}, + {type: Tree.LINE, start: [1.5, 2.5], end: [0.5, 2.5]}, + {type: Tree.LINE, start: [0.5, 2.5], end: [0.5, 1.5]}, + ]) + }) + + it('should plot a polygon', () => { + const tool: Tool = { + type: SIMPLE_TOOL, + shape: { + type: Parser.POLYGON, + diameter: 16, + vertices: 4, + rotation: null, + }, + hole: {type: Parser.RECTANGLE, xSize: 1, ySize: 1}, + } + const location = {endPoint: {x: 2, y: 2}} as Location + + const results = subject.plot(node, tool, location) as Tree.ImageShape[] + const resultShape = results[0].shape as Tree.OutlineShape + + expect(resultShape.segments.slice(-4)).to.eql([ + {type: Tree.LINE, start: [1.5, 1.5], end: [2.5, 1.5]}, + {type: Tree.LINE, start: [2.5, 1.5], end: [2.5, 2.5]}, + {type: Tree.LINE, start: [2.5, 2.5], end: [1.5, 2.5]}, + {type: Tree.LINE, start: [1.5, 2.5], end: [1.5, 1.5]}, + ]) + }) + }) +}) diff --git a/packages/plotter/src/graphic-plotter/__tests__/typings.d.ts b/packages/plotter/src/graphic-plotter/__tests__/typings.d.ts new file mode 100644 index 00000000..3937e88a --- /dev/null +++ b/packages/plotter/src/graphic-plotter/__tests__/typings.d.ts @@ -0,0 +1,9 @@ +export {} + +declare global { + namespace Vi { + interface ExpectStatic { + approx(expected: number): any + } + } +} diff --git a/packages/plotter/src/graphic-plotter/index.ts b/packages/plotter/src/graphic-plotter/index.ts new file mode 100644 index 00000000..9d245f15 --- /dev/null +++ b/packages/plotter/src/graphic-plotter/index.ts @@ -0,0 +1,207 @@ +// Graphic plotter +// Takes nodes and turns them into graphics to be added to the image +import { + GRAPHIC, + SHAPE, + SEGMENT, + MOVE, + SLOT, + DONE, + LINE, + CCW_ARC, + CW_ARC, + DRILL, + SINGLE, + INTERPOLATE_MODE, + QUADRANT_MODE, + REGION_MODE, + GerberNode, + GraphicType, + Filetype, + InterpolateModeType, +} from '@tracespace/parser' + +import * as Tree from '../tree' +import {SIMPLE_TOOL, MACRO_TOOL, Tool} from '../tool-store' +import {Location} from '../location-store' + +import {plotShape} from './plot-shape' +import {plotMacro} from './plot-macro' +import {CCW, CW, ArcDirection, plotSegment, plotPath} from './plot-path' + +export interface GraphicPlotter { + plot( + node: GerberNode, + tool: Tool | undefined, + location: Location + ): Tree.ImageGraphic[] +} + +export function createGraphicPlotter(filetype: Filetype): GraphicPlotter { + const plotter = Object.create(GraphicPlotterPrototype) + + return filetype === DRILL + ? Object.assign(plotter, DrillGraphicPlotterTrait) + : plotter +} + +interface GraphicPlotterImpl extends GraphicPlotter { + _currentPath: CurrentPath | undefined + _arcDirection: ArcDirection | undefined + _ambiguousArcCenter: boolean + _regionMode: boolean + _defaultGraphic: NonNullable | undefined + + _setGraphicState(node: GerberNode): NonNullable | undefined + + _plotCurrentPath( + node: GerberNode, + nextTool: Tool | undefined, + nextGraphicType: NonNullable | undefined + ): Tree.ImageGraphic | undefined +} + +interface CurrentPath { + segments: Tree.PathSegment[] + tool: Tool | undefined + region: boolean +} + +const GraphicPlotterPrototype: GraphicPlotterImpl = { + _currentPath: undefined, + _arcDirection: undefined, + _ambiguousArcCenter: false, + _regionMode: false, + _defaultGraphic: undefined, + + plot( + node: GerberNode, + tool: Tool | undefined, + location: Location + ): Tree.ImageGraphic[] { + const graphics: Tree.ImageGraphic[] = [] + const nextGraphicType = this._setGraphicState(node) + const pathGraphic = this._plotCurrentPath(node, tool, nextGraphicType) + + if (pathGraphic) { + graphics.push(pathGraphic) + } + + if (nextGraphicType === SHAPE && tool?.type === SIMPLE_TOOL) { + graphics.push({type: Tree.IMAGE_SHAPE, shape: plotShape(tool, location)}) + } + + if (nextGraphicType === SHAPE && tool?.type === MACRO_TOOL) { + graphics.push({type: Tree.IMAGE_SHAPE, shape: plotMacro(tool, location)}) + } + + if (nextGraphicType === SEGMENT) { + this._currentPath = this._currentPath ?? { + segments: [], + region: this._regionMode, + tool, + } + + this._currentPath.segments.push( + plotSegment(location, this._arcDirection, this._ambiguousArcCenter) + ) + } + + if (nextGraphicType === SLOT) { + const pathGraphic = plotPath([plotSegment(location)], tool) + + if (pathGraphic) { + graphics.push(pathGraphic) + } + } + + return graphics + }, + + _setGraphicState(node: GerberNode): NonNullable | undefined { + if (node.type === INTERPOLATE_MODE) { + this._arcDirection = arcDirectionFromMode(node.mode) + } + + if (node.type === QUADRANT_MODE) { + this._ambiguousArcCenter = node.quadrant === SINGLE + } + + if (node.type === REGION_MODE) { + this._regionMode = node.region + } + + if (node.type !== GRAPHIC) { + return undefined + } + + if (node.graphic === SEGMENT) { + this._defaultGraphic = SEGMENT + } else if (node.graphic !== null) { + this._defaultGraphic = undefined + } + + return node.graphic ?? this._defaultGraphic + }, + + _plotCurrentPath( + node: GerberNode, + nextTool: Tool | undefined, + nextGraphicType: NonNullable | undefined + ): Tree.ImageGraphic | undefined { + if (this._currentPath === undefined) { + return undefined + } + + if ( + nextTool !== this._currentPath.tool || + node.type === REGION_MODE || + node.type === DONE || + (nextGraphicType === MOVE && this._currentPath.region) || + (nextGraphicType === SHAPE && this._currentPath !== undefined) + ) { + const pathGraphic = plotPath( + this._currentPath.segments, + this._currentPath.tool, + this._currentPath.region + ) + + this._currentPath = undefined + return pathGraphic + } + }, +} + +const DrillGraphicPlotterTrait: Partial = { + _defaultGraphic: SHAPE, + _ambiguousArcCenter: true, + + _setGraphicState(node: GerberNode): NonNullable | undefined { + if (node.type === INTERPOLATE_MODE) { + const {mode} = node + this._arcDirection = arcDirectionFromMode(mode) + + if (mode === CW_ARC || mode === CCW_ARC || mode === LINE) { + this._defaultGraphic = SEGMENT + } else if (mode === MOVE) { + this._defaultGraphic = MOVE + } else { + this._defaultGraphic = SHAPE + } + } + + if (node.type !== GRAPHIC) { + return undefined + } + + return node.graphic ?? this._defaultGraphic + }, +} + +function arcDirectionFromMode( + mode: InterpolateModeType +): ArcDirection | undefined { + if (mode === CCW_ARC) return CCW + if (mode === CW_ARC) return CW + return undefined +} diff --git a/packages/plotter/src/graphic-plotter/plot-macro.ts b/packages/plotter/src/graphic-plotter/plot-macro.ts new file mode 100644 index 00000000..4ec93b48 --- /dev/null +++ b/packages/plotter/src/graphic-plotter/plot-macro.ts @@ -0,0 +1,353 @@ +// Plot a tool macro as shapes +import { + MACRO_VARIABLE, + MACRO_PRIMITIVE, + MACRO_CIRCLE, + MACRO_VECTOR_LINE_DEPRECATED, + MACRO_VECTOR_LINE, + MACRO_CENTER_LINE, + MACRO_LOWER_LEFT_LINE_DEPRECATED, + MACRO_OUTLINE, + MACRO_POLYGON, + MACRO_MOIRE_DEPRECATED, + MACRO_THERMAL, + MacroPrimitiveCode, + MacroValue, +} from '@tracespace/parser' + +import {PI, rotateAndShift, positionsEqual} from '../coordinate-math' + +import * as Tree from '../tree' +import {MacroTool} from '../tool-store' +import {Location} from '../location-store' + +import {shapeToSegments} from './shapes' +import {CW, CCW, getArcPositions} from './plot-path' + +type VariableValues = Record + +export function plotMacro( + tool: MacroTool, + location: Location +): Tree.LayeredShape { + const shapes: Tree.ErasableShape[] = [] + const variableValues: VariableValues = Object.fromEntries( + tool.variableValues.map((value, i) => [`$${i + 1}`, value]) + ) + + for (const block of tool.macro) { + if (block.type === MACRO_VARIABLE) { + variableValues[block.name] = solveExpression(block.value, variableValues) + } + + if (block.type === MACRO_PRIMITIVE) { + const origin: Tree.Position = [location.endPoint.x, location.endPoint.y] + const parameters = block.parameters.map(p => { + return solveExpression(p, variableValues) + }) + + shapes.push(...plotPrimitive(block.code, origin, parameters)) + } + } + + return {type: Tree.LAYERED_SHAPE, shapes} +} + +function solveExpression( + expression: MacroValue, + variables: VariableValues +): number { + if (typeof expression === 'number') return expression + if (typeof expression === 'string') return variables[expression] + + const left = solveExpression(expression.left, variables) + const right = solveExpression(expression.right, variables) + + switch (expression.operator) { + case '+': + return left + right + case '-': + return left - right + case 'x': + return left * right + case '/': + return left / right + } +} + +function plotPrimitive( + code: MacroPrimitiveCode, + origin: Tree.Position, + parameters: number[] +): Tree.ErasableShape[] { + switch (code) { + case MACRO_CIRCLE: { + return [plotCircle(origin, parameters)] + } + + case MACRO_VECTOR_LINE: + case MACRO_VECTOR_LINE_DEPRECATED: { + return [plotVectorLine(origin, parameters)] + } + + case MACRO_CENTER_LINE: { + return [plotCenterLine(origin, parameters)] + } + + case MACRO_LOWER_LEFT_LINE_DEPRECATED: { + return [plotLowerLeftLine(origin, parameters)] + } + + case MACRO_OUTLINE: { + return [plotOutline(origin, parameters)] + } + + case MACRO_POLYGON: { + return [plotPolygon(origin, parameters)] + } + + case MACRO_MOIRE_DEPRECATED: { + return plotMoire(origin, parameters) + } + + case MACRO_THERMAL: { + return [plotThermal(origin, parameters)] + } + } + + return [] +} + +function plotCircle( + origin: Tree.Position, + parameters: number[] +): Tree.ErasableShape { + const [exposure, diameter, cx0, cy0, degrees] = parameters + const r = diameter / 2 + const [cx, cy] = rotateAndShift([cx0, cy0], origin, degrees) + + return {type: Tree.CIRCLE, erase: exposure === 0, cx, cy, r} +} + +function plotVectorLine( + origin: Tree.Position, + parameters: number[] +): Tree.ErasableShape { + const [exposure, width, sx, sy, ex, ey, degrees] = parameters + const [dy, dx] = [ey - sy, ex - sx] + const halfWid = width / 2 + const dist = Math.sqrt(dy ** 2 + dx ** 2) + const [xOff, yOff] = [(halfWid * dx) / dist, (halfWid * dy) / dist] + + return { + type: Tree.POLYGON, + erase: exposure === 0, + points: ( + [ + [sx + xOff, sy - yOff], + [ex + xOff, ey - yOff], + [ex - xOff, ey + yOff], + [sx - xOff, sy + yOff], + ] as Tree.Position[] + ).map(p => rotateAndShift(p, origin, degrees)), + } +} + +function plotCenterLine( + origin: Tree.Position, + parameters: number[] +): Tree.ErasableShape { + const [exposure, width, height, cx, cy, degrees] = parameters + const [halfWidth, halfHeight] = [width / 2, height / 2] + + return { + type: Tree.POLYGON, + erase: exposure === 0, + points: ( + [ + [cx - halfWidth, cy - halfHeight], + [cx + halfWidth, cy - halfHeight], + [cx + halfWidth, cy + halfHeight], + [cx - halfWidth, cy + halfHeight], + ] as Tree.Position[] + ).map(p => rotateAndShift(p, origin, degrees)), + } +} + +function plotLowerLeftLine( + origin: Tree.Position, + parameters: number[] +): Tree.ErasableShape { + const [exposure, width, height, x, y, degrees] = parameters + + return { + type: Tree.POLYGON, + erase: exposure === 0, + points: ( + [ + [x, y], + [x + width, y], + [x + width, y + height], + [x, y + height], + ] as Tree.Position[] + ).map(p => rotateAndShift(p, origin, degrees)), + } +} + +function plotOutline( + origin: Tree.Position, + parameters: number[] +): Tree.ErasableShape { + const [exposure, , ...coords] = parameters.slice(0, -1) + const degrees = parameters[parameters.length - 1] + + return { + type: Tree.POLYGON, + erase: exposure === 0, + points: coords + .flatMap<[number, number]>((coordinate, i) => + i % 2 === 1 ? [[coords[i - 1], coordinate]] : [] + ) + .map(p => rotateAndShift(p, origin, degrees)), + } +} + +function plotPolygon( + origin: Tree.Position, + parameters: number[] +): Tree.ErasableShape { + const [exposure, vertices, cx, cy, diameter, degrees] = parameters + const r = diameter / 2 + const step = (2 * PI) / vertices + const points: Tree.Position[] = [] + let i + + for (i = 0; i < vertices; i++) { + const theta = step * i + const pointX = cx + r * Math.cos(theta) + const pointY = cy + r * Math.sin(theta) + points.push(rotateAndShift([pointX, pointY], origin, degrees)) + } + + return {type: Tree.POLYGON, erase: exposure === 0, points} +} + +function plotMoire( + origin: Tree.Position, + parameters: number[] +): Tree.ErasableShape[] { + const rotate = (p: Tree.Position): Tree.Position => + rotateAndShift(p, origin, parameters[8]) + + const [cx0, cy0, d, ringThx, ringGap, ringN, lineThx, lineLength] = parameters + const [cx, cy] = rotate([cx0, cy0]) + const halfLineThx = lineThx / 2 + const halfLineLength = lineLength / 2 + + const radii = [] + let count = 0 + let dRemain = d + + while (dRemain >= 0 && count < ringN) { + const r = dRemain / 2 + const rHole = r - ringThx + + radii.push(r) + if (rHole > 0) radii.push(rHole) + count += 1 + dRemain = 2 * (rHole - ringGap) + } + + return [ + { + type: Tree.OUTLINE, + segments: radii.flatMap(r => { + return shapeToSegments({type: Tree.CIRCLE, cx, cy, r}) + }), + }, + // Vertical stroke + { + type: Tree.POLYGON, + points: ( + [ + [cx0 - halfLineThx, cy0 - halfLineLength], + [cx0 + halfLineThx, cy0 - halfLineLength], + [cx0 + halfLineThx, cy0 + halfLineLength], + [cx0 - halfLineThx, cy0 + halfLineLength], + ] as Tree.Position[] + ).map(rotate), + }, + // Horizontal stroke + { + type: Tree.POLYGON, + points: ( + [ + [cx0 - halfLineLength, cy0 - halfLineThx], + [cx0 + halfLineLength, cy0 - halfLineThx], + [cx0 + halfLineLength, cy0 + halfLineThx], + [cx0 - halfLineLength, cy0 + halfLineThx], + ] as Tree.Position[] + ).map(rotate), + }, + ] +} + +function plotThermal( + origin: Tree.Position, + parameters: number[] +): Tree.ErasableShape { + const [cx0, cy0, od, id, gap, degrees] = parameters + const center = rotateAndShift([cx0, cy0], origin, degrees) + const [or, ir] = [od / 2, id / 2] + const halfGap = gap / 2 + const oIntSquare = or ** 2 - halfGap ** 2 + const iIntSquare = ir ** 2 - halfGap ** 2 + const oInt = Math.sqrt(oIntSquare) + const iInt = iIntSquare >= 0 ? Math.sqrt(iIntSquare) : halfGap + const positions = [0, 90, 180, 270] + const segments: Tree.PathSegment[] = [] + + for (const rot of positions) { + const points = ( + [ + [iInt, halfGap], + [oInt, halfGap], + [halfGap, oInt], + [halfGap, iInt], + ] as Tree.Position[] + ) + .map(p => rotateAndShift(p, [cx0, cy0], rot)) + .map(p => rotateAndShift(p, origin, degrees)) + + const [os, oe, oc] = getArcPositions( + {x: points[1][0], y: points[1][1]}, + {x: points[2][0], y: points[2][1]}, + {x: center[0], y: center[1]}, + CCW + ) + + segments.push( + {type: Tree.LINE, start: points[0], end: points[1]}, + {type: Tree.ARC, start: os, end: oe, center: oc, radius: or}, + {type: Tree.LINE, start: points[2], end: points[3]} + ) + + if (!positionsEqual(points[0], points[3])) { + const [is, ie, ic] = getArcPositions( + {x: points[3][0], y: points[3][1]}, + {x: points[0][0], y: points[0][1]}, + {x: center[0], y: center[1]}, + CW + ) + segments.push({ + type: Tree.ARC, + start: is, + end: ie, + center: ic, + radius: ir, + }) + } + } + + return {type: Tree.OUTLINE, segments} +} diff --git a/packages/plotter/src/graphic-plotter/plot-path.ts b/packages/plotter/src/graphic-plotter/plot-path.ts new file mode 100644 index 00000000..1a51c337 --- /dev/null +++ b/packages/plotter/src/graphic-plotter/plot-path.ts @@ -0,0 +1,155 @@ +import * as Tree from '../tree' +import {SIMPLE_TOOL, Tool} from '../tool-store' +import {Location, Point} from '../location-store' +import {TWO_PI} from '../coordinate-math' + +import {plotRectPath} from './plot-rect-path' + +export const CW = 'cw' +export const CCW = 'ccw' + +export type ArcDirection = typeof CW | typeof CCW + +export function plotSegment( + location: Location, + arcDirection?: ArcDirection, + ambiguousArcCenter?: boolean +): Tree.PathSegment { + return arcDirection === undefined + ? createLineSegment(location) + : createArcSegment(location, arcDirection, ambiguousArcCenter) +} + +export function plotPath( + segments: Tree.PathSegment[], + tool: Tool | undefined, + region = false +): Tree.ImageGraphic | undefined { + if (segments.length > 0) { + if (region) { + return {type: Tree.IMAGE_REGION, segments} + } + + if (tool?.type === SIMPLE_TOOL && tool.shape.type === Tree.CIRCLE) { + return {type: Tree.IMAGE_PATH, width: tool.shape.diameter, segments} + } + + if (tool?.type === SIMPLE_TOOL && tool.shape.type === Tree.RECTANGLE) { + return plotRectPath(segments, tool.shape) + } + } +} + +function createLineSegment(location: Location): Tree.PathLineSegment { + return { + type: Tree.LINE, + start: [location.startPoint.x, location.startPoint.y], + end: [location.endPoint.x, location.endPoint.y], + } +} + +function createArcSegment( + location: Location, + arcDirection: ArcDirection, + ambiguousArcCenter = false +): Tree.PathSegment { + const {startPoint, endPoint, arcOffsets} = location + const radius = + arcOffsets.a > 0 + ? arcOffsets.a + : (arcOffsets.i ** 2 + arcOffsets.j ** 2) ** 0.5 + + if (ambiguousArcCenter || arcOffsets.a > 0) { + if (startPoint.x === endPoint.x && startPoint.y === endPoint.y) { + return createLineSegment(location) + } + + // Get the center candidates and select the candidate with the smallest arc + const [start, end, center] = findCenterCandidates(location, radius) + .map(centerPoint => { + return getArcPositions(startPoint, endPoint, centerPoint, arcDirection) + }) + .sort(([startA, endA], [startB, endB]) => { + const absSweepA = Math.abs(endA[2] - startA[2]) + const absSweepB = Math.abs(endB[2] - startB[2]) + return absSweepA - absSweepB + })[0] + + return {type: Tree.ARC, start, end, center, radius} + } + + const centerPoint = { + x: startPoint.x + arcOffsets.i, + y: startPoint.y + arcOffsets.j, + } + + const [start, end, center] = getArcPositions( + startPoint, + endPoint, + centerPoint, + arcDirection + ) + + return {type: Tree.ARC, start, end, center, radius} +} + +export function getArcPositions( + startPoint: Point, + endPoint: Point, + centerPoint: Point, + arcDirection: ArcDirection +): [start: Tree.ArcPosition, end: Tree.ArcPosition, center: Tree.Position] { + let startAngle = Math.atan2( + startPoint.y - centerPoint.y, + startPoint.x - centerPoint.x + ) + let endAngle = Math.atan2( + endPoint.y - centerPoint.y, + endPoint.x - centerPoint.x + ) + + // If counter-clockwise, end angle should be greater than start angle + if (arcDirection === CCW) { + endAngle = endAngle > startAngle ? endAngle : endAngle + TWO_PI + } else { + startAngle = startAngle > endAngle ? startAngle : startAngle + TWO_PI + } + + return [ + [startPoint.x, startPoint.y, startAngle], + [endPoint.x, endPoint.y, endAngle], + [centerPoint.x, centerPoint.y], + ] +} + +// Find arc center candidates by finding the intersection points +// of two circles with `radius` centered on the start and end points +// https://math.stackexchange.com/a/1367732 +function findCenterCandidates(location: Location, radius: number): Point[] { + // This function assumes that start and end are different points + const {x: x1, y: y1} = location.startPoint + const {x: x2, y: y2} = location.endPoint + + // Distance between the start and end points + const [dx, dy] = [x2 - x1, y2 - y1] + const [sx, sy] = [x2 + x1, y2 + y1] + const distance = Math.sqrt(dx ** 2 + dy ** 2) + + // If the distance to the midpoint equals the arc radius, then there is + // exactly one intersection at the midpoint; if the distance to the midpoint + // is greater than the radius, assume we've got a rounding error and just use + // the midpoint + if (radius <= distance / 2) { + return [{x: x1 + dx / 2, y: y1 + dy / 2}] + } + + // No good name for these variables, but it's how the math works out + const factor = Math.sqrt((4 * radius ** 2) / distance ** 2 - 1) + const [xBase, yBase] = [sx / 2, sy / 2] + const [xAddend, yAddend] = [(dy * factor) / 2, (dx * factor) / 2] + + return [ + {x: xBase + xAddend, y: yBase - yAddend}, + {x: xBase - xAddend, y: yBase + yAddend}, + ] +} diff --git a/packages/plotter/src/plot-tree/plot-rect-path.ts b/packages/plotter/src/graphic-plotter/plot-rect-path.ts similarity index 72% rename from packages/plotter/src/plot-tree/plot-rect-path.ts rename to packages/plotter/src/graphic-plotter/plot-rect-path.ts index e9fe9bb3..8ee74504 100644 --- a/packages/plotter/src/plot-tree/plot-rect-path.ts +++ b/packages/plotter/src/graphic-plotter/plot-rect-path.ts @@ -1,23 +1,35 @@ // Functions for stroking rectangular tools +// Stroking rectangular tools is deprecated by the Gerber spec +// This functionality may be dropped and replaced with a warning import {Rectangle} from '@tracespace/parser' -import {Position} from '../types' -import {ImageRegion, IMAGE_REGION} from '../tree' -import {line} from './geometry' -import {positionsEqual, HALF_PI, PI} from './math' +import * as Tree from '../tree' +import {positionsEqual, HALF_PI, PI} from '../coordinate-math' // Rectangular tools make interesting stroke geometry; see the Gerber spec // for graphics and examples export function plotRectPath( - start: Position, - end: Position, + segments: Tree.PathSegment[], shape: Rectangle -): ImageRegion { +): Tree.ImageShape { + const shapes = segments + .filter((s): s is Tree.PathLineSegment => s.type === Tree.LINE) + .map(segment => plotRectPathSegment(segment, shape)) + + return {type: Tree.IMAGE_SHAPE, shape: {type: Tree.LAYERED_SHAPE, shapes}} +} + +function plotRectPathSegment( + segment: Tree.PathLineSegment, + shape: Rectangle +): Tree.PolygonShape { // Since a rectangular stroke like this is so unique to Gerber, it's easier // for downstream graphics generators if we calculate the boundaries of the // correct shape and emit a region rather than a path with a width (which is // what we do for circle tools) - const [[sx, sy], [ex, ey]] = [start, end] + const {start, end} = segment + const [sx, sy] = start + const [ex, ey] = end const [xOffset, yOffset] = [shape.xSize / 2, shape.ySize / 2] const theta = Math.atan2(ey - sy, ex - ey) @@ -28,7 +40,7 @@ export function plotRectPath( // Go through the quadrants of the XY plane centered about start to decide // which segments define the boundaries of the stroke shape - let points: Position[] = [] + let points: Tree.Position[] = [] if (positionsEqual(start, end)) { points = [ [sxMin, syMin], @@ -80,10 +92,5 @@ export function plotRectPath( ] } - const segments = points.map((start, i) => { - const end = points[i < points.length - 1 ? i + 1 : 0] - return line({start, end}) - }) - - return {type: IMAGE_REGION, meta: {regionMode: false}, segments} + return {type: Tree.POLYGON, points} } diff --git a/packages/plotter/src/graphic-plotter/plot-shape.ts b/packages/plotter/src/graphic-plotter/plot-shape.ts new file mode 100644 index 00000000..e88633fd --- /dev/null +++ b/packages/plotter/src/graphic-plotter/plot-shape.ts @@ -0,0 +1,22 @@ +import {MACRO_SHAPE} from '@tracespace/parser' + +import * as Tree from '../tree' +import {Location} from '../location-store' +import {SimpleTool} from '../tool-store' + +import {createShape, shapeToSegments} from './shapes' + +export function plotShape(tool: SimpleTool, location: Location): Tree.Shape { + const {shape: toolShape, hole: toolHole} = tool + const shape = createShape(toolShape, location.endPoint) + const holeShape = toolHole + ? createShape(toolHole, location.endPoint) + : undefined + + return holeShape === undefined + ? shape + : { + type: Tree.OUTLINE, + segments: [...shapeToSegments(shape), ...shapeToSegments(holeShape)], + } +} diff --git a/packages/plotter/src/graphic-plotter/shapes.ts b/packages/plotter/src/graphic-plotter/shapes.ts new file mode 100644 index 00000000..f3185ae7 --- /dev/null +++ b/packages/plotter/src/graphic-plotter/shapes.ts @@ -0,0 +1,154 @@ +import { + CIRCLE, + RECTANGLE, + OBROUND, + POLYGON, + SimpleShape, +} from '@tracespace/parser' + +import { + HALF_PI, + PI, + THREE_HALF_PI, + TWO_PI, + degreesToRadians, +} from '../coordinate-math' + +import * as Tree from '../tree' +import {Point} from '../location-store' + +export function createShape( + shape: SimpleShape, + point: Point +): Tree.SimpleShape { + const {x, y} = point + + switch (shape.type) { + case CIRCLE: { + const {diameter} = shape + return {type: Tree.CIRCLE, cx: x, cy: y, r: diameter / 2} + } + + case RECTANGLE: + case OBROUND: { + const {xSize, ySize} = shape + const xHalf = xSize / 2 + const yHalf = ySize / 2 + const rectangle: Tree.RectangleShape = { + type: Tree.RECTANGLE, + x: x - xHalf, + y: y - yHalf, + xSize, + ySize, + } + + if (shape.type === OBROUND) { + rectangle.r = Math.min(xHalf, yHalf) + } + + return rectangle + } + + case POLYGON: { + const {diameter, rotation, vertices} = shape + const r = diameter / 2 + const offset = degreesToRadians(rotation ?? 0) + const step = TWO_PI / vertices + const points = Array.from({length: vertices}).map( + (_, i) => { + const theta = step * i + offset + const pointX = x + r * Math.cos(theta) + const pointY = y + r * Math.sin(theta) + return [pointX, pointY] + } + ) + + return {type: Tree.POLYGON, points} + } + } +} + +export function shapeToSegments(shape: Tree.SimpleShape): Tree.PathSegment[] { + if (shape.type === Tree.CIRCLE) { + const {cx, cy, r} = shape + return [ + { + type: Tree.ARC, + start: [cx + r, cy, 0], + end: [cx + r, cy, TWO_PI], + center: [cx, cy], + radius: r, + }, + ] + } + + if (shape.type === Tree.RECTANGLE) { + const {x, y, xSize, ySize, r} = shape + + if (r === xSize / 2) { + return [ + { + type: Tree.LINE, + start: [x + xSize, y + r], + end: [x + xSize, y + ySize - r], + }, + { + type: Tree.ARC, + start: [x + xSize, y + ySize - r, 0], + end: [x, y + ySize - r, PI], + center: [x + r, y + ySize - r], + radius: r, + }, + {type: Tree.LINE, start: [x, y + ySize - r], end: [x, y + r]}, + { + type: Tree.ARC, + start: [x, y + r, PI], + end: [x + xSize, y + r, TWO_PI], + center: [x + r, y + r], + radius: r, + }, + ] + } + + if (r === ySize / 2) { + return [ + {type: Tree.LINE, start: [x + r, y], end: [x + xSize - r, y]}, + { + type: Tree.ARC, + start: [x + xSize - r, y, -HALF_PI], + end: [x + xSize - r, y + ySize, HALF_PI], + center: [x + xSize - r, y + r], + radius: r, + }, + { + type: Tree.LINE, + start: [x + xSize - r, y + ySize], + end: [x + r, y + ySize], + }, + { + type: Tree.ARC, + start: [x + r, y + ySize, HALF_PI], + end: [x + r, y, THREE_HALF_PI], + center: [x + r, y + r], + radius: r, + }, + ] + } + + return [ + {type: Tree.LINE, start: [x, y], end: [x + xSize, y]}, + {type: Tree.LINE, start: [x + xSize, y], end: [x + xSize, y + ySize]}, + {type: Tree.LINE, start: [x + xSize, y + ySize], end: [x, y + ySize]}, + {type: Tree.LINE, start: [x, y + ySize], end: [x, y]}, + ] + } + + if (shape.type === Tree.POLYGON) { + return shape.points.map((start, i) => { + const endIndex = i < shape.points.length - 1 ? i + 1 : 0 + return {type: Tree.LINE, start, end: shape.points[endIndex]} + }) + } + + return shape.segments +} diff --git a/packages/plotter/src/index.ts b/packages/plotter/src/index.ts index e412bbc6..0c4e9de3 100644 --- a/packages/plotter/src/index.ts +++ b/packages/plotter/src/index.ts @@ -3,15 +3,30 @@ import {GerberTree} from '@tracespace/parser' import {getPlotOptions} from './options' -import {createPlot} from './plot-tree' -import {ImageTree} from './tree' +import {createToolStore} from './tool-store' +import {createLocationStore} from './location-store' +import {createMainLayer} from './main-layer' +import {createGraphicPlotter} from './graphic-plotter' +import {IMAGE, ImageTree} from './tree' export * from './tree' -export * from './types' +export * as BoundingBox from './bounding-box' +export {positionsEqual} from './coordinate-math' export function plot(tree: GerberTree): ImageTree { const plotOptions = getPlotOptions(tree) - const imageTree = createPlot(tree, plotOptions) + const toolStore = createToolStore() + const locationStore = createLocationStore() + const mainLayer = createMainLayer() + const graphicPlotter = createGraphicPlotter(tree.filetype) + let result = mainLayer.get() - return imageTree + for (const node of tree.children) { + const tool = toolStore.use(node) + const location = locationStore.use(node, plotOptions) + const graphics = graphicPlotter.plot(node, tool, location) + result = mainLayer.add(node, graphics) + } + + return {type: IMAGE, units: plotOptions.units, children: [result]} } diff --git a/packages/plotter/src/location-store.ts b/packages/plotter/src/location-store.ts new file mode 100644 index 00000000..3d7eeb6c --- /dev/null +++ b/packages/plotter/src/location-store.ts @@ -0,0 +1,104 @@ +// Track the location of the plotter and parse coordinate strings +import {GRAPHIC, TRAILING, GerberNode} from '@tracespace/parser' + +import {PlotOptions} from './options' + +export interface Point { + x: number + y: number +} + +export interface ArcOffsets { + i: number + j: number + a: number +} + +export interface Location { + startPoint: Point + endPoint: Point + arcOffsets: ArcOffsets +} + +export interface LocationStore { + use(node: GerberNode, options: PlotOptions): Location +} + +export function createLocationStore(): LocationStore { + return Object.create(LocationStorePrototype) +} + +interface LocationStoreState { + _DEFAULT_ARC_OFFSETS: ArcOffsets + _previousPoint: Point +} + +const LocationStorePrototype: LocationStore & LocationStoreState = { + _DEFAULT_ARC_OFFSETS: {i: 0, j: 0, a: 0}, + _previousPoint: {x: 0, y: 0}, + + use(node: GerberNode, options: PlotOptions): Location { + let arcOffsets = this._DEFAULT_ARC_OFFSETS + let startPoint = this._previousPoint + let endPoint = startPoint + + if (node.type === GRAPHIC) { + const {coordinates} = node + const x0 = parseCoordinate(coordinates.x0, startPoint.x, options) + const y0 = parseCoordinate(coordinates.y0, startPoint.y, options) + const x = parseCoordinate(coordinates.x, x0, options) + const y = parseCoordinate(coordinates.y, y0, options) + const i = parseCoordinate(coordinates.i, 0, options) + const j = parseCoordinate(coordinates.j, 0, options) + const a = parseCoordinate(coordinates.a, 0, options) + + if (startPoint.x !== x0 || startPoint.y !== y0) { + startPoint = {x: x0, y: y0} + } + + if (endPoint.x !== x || endPoint.y !== y) { + endPoint = {x, y} + } + + if (i !== 0 || j !== 0 || a !== 0) { + arcOffsets = {i, j, a} + } + } + + this._previousPoint = endPoint + return {startPoint, endPoint, arcOffsets} + }, +} + +function parseCoordinate( + coordinate: string | undefined, + defaultValue: number, + options: PlotOptions +): number { + if (typeof coordinate !== 'string') { + return defaultValue + } + + if (coordinate.includes('.') || coordinate === '0') { + return Number(coordinate) + } + + const {coordinateFormat, zeroSuppression} = options + const [integerPlaces, decimalPlaces] = coordinateFormat + + const [sign, signlessCoordinate] = + coordinate.startsWith('+') || coordinate.startsWith('-') + ? [coordinate[0], coordinate.slice(1)] + : ['+', coordinate] + + const digits = integerPlaces + decimalPlaces + const paddedCoordinate = + zeroSuppression === TRAILING + ? signlessCoordinate.padEnd(digits, '0') + : signlessCoordinate.padStart(digits, '0') + + const leading = paddedCoordinate.slice(0, integerPlaces) + const trailing = paddedCoordinate.slice(integerPlaces) + + return Number(`${sign}${leading}.${trailing}`) +} diff --git a/packages/plotter/src/main-layer.ts b/packages/plotter/src/main-layer.ts new file mode 100644 index 00000000..96d88f7d --- /dev/null +++ b/packages/plotter/src/main-layer.ts @@ -0,0 +1,45 @@ +// Main layer +// Collects graphic objects onto the main layer, transforming as necessary +import {GerberNode} from '@tracespace/parser' + +import * as BoundingBox from './bounding-box' +import {IMAGE_LAYER, ImageLayer, ImageGraphic} from './tree' + +export interface MainLayer { + get(): ImageLayer + add(node: GerberNode, graphic: ImageGraphic[]): ImageLayer +} + +export function createMainLayer(): MainLayer { + return Object.create(MainLayerPrototype) +} + +interface MainLayerState { + _layer: ImageLayer | undefined +} + +const MainLayerPrototype: MainLayer & MainLayerState = { + _layer: undefined, + + get(): ImageLayer { + this._layer = this._layer ?? { + type: IMAGE_LAYER, + size: BoundingBox.empty(), + children: [], + } + + return this._layer + }, + + add(node: GerberNode, graphics: ImageGraphic[]): ImageLayer { + const layer = this.get() + + for (const graphic of graphics) { + const graphicSize = BoundingBox.fromGraphic(graphic) + layer.size = BoundingBox.add(layer.size, graphicSize) + layer.children.push(graphic) + } + + return layer + }, +} diff --git a/packages/plotter/src/options.ts b/packages/plotter/src/options.ts index d148a1ec..45e10d5d 100644 --- a/packages/plotter/src/options.ts +++ b/packages/plotter/src/options.ts @@ -1,18 +1,30 @@ -import * as Parser from '@tracespace/parser' +import { + UNITS, + COORDINATE_FORMAT, + GRAPHIC, + COMMENT, + LEADING, + TRAILING, + IN, + GerberTree, + UnitsType, + Format, + ZeroSuppression, +} from '@tracespace/parser' export interface PlotOptions { - units: Parser.UnitsType | null - coordinateFormat: Parser.Format | null - zeroSuppression: Parser.ZeroSuppression | null + units: UnitsType + coordinateFormat: Format + zeroSuppression: ZeroSuppression } const FORMAT_COMMENT_RE = /FORMAT={?(\d):(\d)/ -export function getPlotOptions(tree: Parser.GerberTree): PlotOptions { +export function getPlotOptions(tree: GerberTree): PlotOptions { const {children: treeNodes} = tree - let units: Parser.UnitsType | null = null - let coordinateFormat: Parser.Format | null = null - let zeroSuppression: Parser.ZeroSuppression | null = null + let units: UnitsType | null = null + let coordinateFormat: Format | null = null + let zeroSuppression: ZeroSuppression | null = null let index = 0 while ( @@ -22,41 +34,41 @@ export function getPlotOptions(tree: Parser.GerberTree): PlotOptions { const node = treeNodes[index] switch (node.type) { - case Parser.UNITS: { + case UNITS: { units = node.units break } - case Parser.COORDINATE_FORMAT: { + case COORDINATE_FORMAT: { coordinateFormat = node.format zeroSuppression = node.zeroSuppression break } - case Parser.GRAPHIC: { + case GRAPHIC: { const {coordinates} = node for (const coordinate of Object.values(coordinates)) { if (zeroSuppression !== null) break if (coordinate!.endsWith('0') || coordinate!.includes('.')) { - zeroSuppression = Parser.LEADING + zeroSuppression = LEADING } else if (coordinate!.startsWith('0')) { - zeroSuppression = Parser.TRAILING + zeroSuppression = TRAILING } } break } - case Parser.COMMENT: { + case COMMENT: { const {comment} = node const formatMatch = FORMAT_COMMENT_RE.exec(comment) if (/suppress trailing/i.test(comment)) { - zeroSuppression = Parser.TRAILING + zeroSuppression = TRAILING } else if (/(suppress leading|keep zeros)/i.test(comment)) { - zeroSuppression = Parser.LEADING + zeroSuppression = LEADING } if (formatMatch) { @@ -72,5 +84,9 @@ export function getPlotOptions(tree: Parser.GerberTree): PlotOptions { index += 1 } - return {units, coordinateFormat, zeroSuppression} + return { + units: units ?? IN, + coordinateFormat: coordinateFormat ?? [2, 4], + zeroSuppression: zeroSuppression ?? LEADING, + } } diff --git a/packages/plotter/src/plot-tree/__tests__/plot-path.test.ts b/packages/plotter/src/plot-tree/__tests__/plot-path.test.ts deleted file mode 100644 index 3121a580..00000000 --- a/packages/plotter/src/plot-tree/__tests__/plot-path.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {describe, it, expect} from 'vitest' -import * as Parser from '@tracespace/parser' - -import {Position} from '../../types' -import * as Tree from '../../tree' -import {addSegmentToPath} from '../plot-path' - -interface PlotSegmentSpec { - tool: Parser.ToolDefinition - path: Tree.ImagePath | null - interpolateMode: Parser.InterpolateModeType - regionMode: false - start: Position - end: Position - expectedPath: Tree.ImagePath | Tree.ImageRegion -} - -const t = ( - shape: Parser.ToolShape, - hole: Parser.HoleShape | null = null -): Parser.ToolDefinition => ({ - type: Parser.TOOL_DEFINITION, - code: '10', - shape, - hole, -}) - -describe('shape plotting', () => { - const SPECS: Record = { - 'circle tool line segment': { - tool: t({type: Parser.CIRCLE, diameter: 2}), - path: null, - interpolateMode: Tree.LINE, - regionMode: false, - start: [3, 4], - end: [5, 6], - expectedPath: { - type: Tree.IMAGE_PATH, - width: 2, - segments: [{type: Tree.LINE, start: [3, 4], end: [5, 6]}], - }, - }, - } - - for (const [name, spec] of Object.entries(SPECS)) { - const {tool, path, interpolateMode, regionMode, start, end, expectedPath} = - spec - - it(name, () => { - const nextPath = addSegmentToPath({ - path, - start, - end, - offsets: {i: null, j: null, a: null}, - tool, - interpolateMode, - regionMode, - quadrantMode: null, - }) - - expect(nextPath).to.eql(expectedPath) - }) - } -}) diff --git a/packages/plotter/src/plot-tree/__tests__/plot-shape.test.ts b/packages/plotter/src/plot-tree/__tests__/plot-shape.test.ts deleted file mode 100644 index 6ba84c59..00000000 --- a/packages/plotter/src/plot-tree/__tests__/plot-shape.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import {describe, it, expect} from 'vitest' -import * as Parser from '@tracespace/parser' - -import {Position, Box} from '../../types' -import * as Tree from '../../tree' -import {plotShape, getShapeBox} from '../plot-shape' - -interface PlotShapeSpec { - tool: Parser.ToolDefinition - position: Position - expectedShape: Tree.Shape - expectedBox: Box -} - -const t = ( - shape: Parser.Circle | Parser.Rectangle | Parser.Obround | Parser.Polygon, - hole: Parser.HoleShape | null = null -): Parser.ToolDefinition => ({ - type: Parser.TOOL_DEFINITION, - code: '10', - shape, - hole, -}) - -describe('shape plotting', () => { - const SPECS: Record = { - 'circle tool': { - tool: t({type: Parser.CIRCLE, diameter: 2}), - position: [3, 4], - expectedShape: {type: Tree.CIRCLE, cx: 3, cy: 4, r: 1}, - expectedBox: [2, 3, 4, 5], - }, - 'rectangle tool': { - tool: t({type: Parser.RECTANGLE, xSize: 6, ySize: 7}), - position: [2, -1], - expectedShape: { - type: Tree.RECTANGLE, - x: -1, - y: -4.5, - xSize: 6, - ySize: 7, - r: null, - }, - expectedBox: [-1, -4.5, 5, 2.5], - }, - 'obround tool (portrait)': { - tool: t({type: Parser.OBROUND, xSize: 6, ySize: 8}), - position: [1, 2], - expectedShape: { - type: Tree.RECTANGLE, - x: -2, - y: -2, - xSize: 6, - ySize: 8, - r: 3, - }, - expectedBox: [-2, -2, 4, 6], - }, - 'obround tool (landscape)': { - tool: t({type: Parser.OBROUND, xSize: 8, ySize: 6}), - position: [1, 2], - expectedShape: { - type: Tree.RECTANGLE, - x: -3, - y: -1, - xSize: 8, - ySize: 6, - r: 3, - }, - expectedBox: [-3, -1, 5, 5], - }, - polygon: { - tool: t({ - type: Parser.POLYGON, - diameter: 16, - vertices: 4, - rotation: null, - }), - position: [2, 2], - expectedShape: { - type: Tree.POLYGON, - points: [ - [10, 2], - [2, 10], - [-6, 2], - [2, -6], - ], - }, - expectedBox: [-6, -6, 10, 10], - }, - } - - for (const [name, spec] of Object.entries(SPECS)) { - const {tool, position, expectedShape, expectedBox} = spec - - it(name, () => { - const shape = plotShape(tool.shape as any, tool.hole, position) - const box = getShapeBox(shape) - - expect(shape).to.eql(expectedShape) - expect(box).to.eql(expectedBox) - }) - } -}) diff --git a/packages/plotter/src/plot-tree/arc-segment.ts b/packages/plotter/src/plot-tree/arc-segment.ts deleted file mode 100644 index 877e4f58..00000000 --- a/packages/plotter/src/plot-tree/arc-segment.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { - InterpolateModeType, - QuadrantModeType, - CW_ARC, - SINGLE, - MULTI, -} from '@tracespace/parser' - -import {Position, ArcPosition, Offsets} from '../types' -import {Direction, PathSegment, CW, CCW} from '../tree' -import {line, arc} from './geometry' -import {roundToPrecision} from './math' - -const TWO_PI = Math.PI * 2 - -export interface ArcSegmentOptions { - start: Position - end: Position - offsets: Offsets - interpolateMode: InterpolateModeType - quadrantMode: QuadrantModeType -} - -export function makeArcSegment({ - start, - end, - offsets, - interpolateMode, - quadrantMode, -}: ArcSegmentOptions): PathSegment { - // Arc radius (distance from start and end points to candidates) - const [sx, sy] = start - const direction = interpolateMode === CW_ARC ? CW : CCW - const {i, j, a} = {i: offsets.i ?? 0, j: offsets.j ?? 0, a: offsets.a} - const radius = a === null ? Math.sqrt(i ** 2 + j ** 2) : a - - // Short circuit if start and end are the same point - // multi-quadrant: full circle - // else: dot (same as zero-length line segment) - if (start[0] === end[0] && start[1] === end[1]) { - if (quadrantMode === MULTI) { - const [arcStart, arcEnd, center] = getCenterAngles( - start, - end, - [sx + i, sy + j], - direction - ) - - return arc({ - start: arcStart, - end: arcEnd, - center, - radius, - direction, - sweep: TWO_PI, - }) - } - - return line({start, end}) - } - - // Get candidates for arc center based on arc radius - let candidates = findCenterCandidates(start, end, radius) - - // If we have more than one candidate and offsets, sort the candidates by how - // closely they compare to locations specified by the offsets - if (candidates.length === 2 && a === null) { - let offsetLocations: Position[] = [[sx + i, sy + j]] - - const getMinOffsetDistance = (center: Position): number => { - let result = Number.POSITIVE_INFINITY - - for (const location of offsetLocations) { - const [x1, y1] = location - const [x2, y2] = center - result = Math.min(result, (x1 - x2) ** 2 + (y1 - y2) ** 2) - } - - return result - } - - // If we're in multi-quadrant mode, i and j will be signed, but in single - // quadrant mode we got multiple sign combinations to check - if (quadrantMode === SINGLE) { - offsetLocations = [ - ...offsetLocations, - [sx + i, sy - j], - [sx - i, sy + j], - [sx - i, sy - j], - ] - } - - // Sort candidates by proximity to a valid center according to offsets - candidates = - getMinOffsetDistance(candidates[0]) <= getMinOffsetDistance(candidates[1]) - ? candidates - : [candidates[1], candidates[0]] - } - - // In multi-quadrant, rely on the accuracy of i and j - // otherwise, pick the center with the smallest sweep to satisfy the single or - // double quadrant case - let selectedArc - - if (candidates.length === 1 || quadrantMode === MULTI) { - selectedArc = getCenterAngles(start, end, candidates[0], direction) - } else { - const arcs = candidates.map(c => getCenterAngles(start, end, c, direction)) - selectedArc = arcs[0] - - for (const arc of arcs.slice(1)) { - if (arc[3] < selectedArc[3]) { - selectedArc = arc - } - } - } - - const [arcStart, arcEnd, center, sweep] = selectedArc - return arc({start: arcStart, end: arcEnd, center, radius, sweep, direction}) -} - -// Find arc center candidates by calculating the arc radius and finding -// intersection points between the circles with that radius centered at the -// start and end points of the arc -// https://math.stackexchange.com/a/1367732 -export function findCenterCandidates( - start: Position, - end: Position, - radius: number -): [Position] | [Position, Position] { - // This function assumes that start and end are different points - const [x1, y1] = start - const [x2, y2] = end - - // Distance between the start and end points - const [dx, dy] = [x2 - x1, y2 - y1] - const [sx, sy] = [x2 + x1, y2 + y1] - const distance = Math.sqrt(dx ** 2 + dy ** 2) - - // If the distance to the midpoint equals the arc radius, then there is - // exactly one intersection at the midpoint; if the distance to the midpoint - // is greater than the radius, assume we've got a rounding error and just use - // the midpoint - if (radius <= distance / 2) { - return [[roundToPrecision(x1 + dx / 2), roundToPrecision(y1 + dy / 2)]] - } - - // No good name for these variables, but it's how the math works out - const factor = Math.sqrt((4 * radius ** 2) / distance ** 2 - 1) - const [xBase, yBase] = [sx / 2, sy / 2] - const [xAddend, yAddend] = [(dy * factor) / 2, (dx * factor) / 2] - - return [ - [roundToPrecision(xBase + xAddend), roundToPrecision(yBase - yAddend)], - [roundToPrecision(xBase - xAddend), roundToPrecision(yBase + yAddend)], - ] -} - -export function getCenterAngles( - start: Position, - end: Position, - center: Position, - direction: Direction -): [start: ArcPosition, end: ArcPosition, center: Position, sweep: number] { - let thetaStart = Math.atan2(start[1] - center[1], start[0] - center[0]) - let thetaEnd = Math.atan2(end[1] - center[1], end[0] - center[0]) - - // If cw, ensure the start angle is greater than the end angle - // if cww, start should be less than end - if (direction === CW) { - thetaStart = thetaStart >= thetaEnd ? thetaStart : thetaStart + TWO_PI - } else { - thetaEnd = thetaEnd >= thetaStart ? thetaEnd : thetaEnd + TWO_PI - } - - const sweep = Math.abs(thetaStart - thetaEnd) - - return [ - [start[0], start[1], roundToPrecision(thetaStart)], - [end[0], end[1], roundToPrecision(thetaEnd)], - center, - roundToPrecision(sweep), - ] -} diff --git a/packages/plotter/src/plot-tree/bounding-box.ts b/packages/plotter/src/plot-tree/bounding-box.ts deleted file mode 100644 index 4acc2ff6..00000000 --- a/packages/plotter/src/plot-tree/bounding-box.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {Box, Position} from '../types' -import {roundToPrecision} from './math' - -export function empty(): Box { - return [ - Number.POSITIVE_INFINITY, - Number.POSITIVE_INFINITY, - Number.NEGATIVE_INFINITY, - Number.NEGATIVE_INFINITY, - ] -} - -export function add(a: Box, b: Box): Box { - return [ - Math.min(a[0], b[0]), - Math.min(a[1], b[1]), - Math.max(a[2], b[2]), - Math.max(a[3], b[3]), - ] -} - -export function fromRectangle( - x: number, - y: number, - xSize: number, - ySize: number -): Box { - return [x, y, x + xSize, y + ySize] -} - -export function fromCircle(cx: number, cy: number, r: number): Box { - return [cx - r, cy - r, cx + r, cy + r] -} - -export function addPosition(box: Box, position: Position): Box { - const [x, y] = position - - return [ - Math.min(box[0], x), - Math.min(box[1], y), - Math.max(box[2], x), - Math.max(box[3], y), - ] -} - -export function toViewBox(box: Box): Box { - if ( - box.some( - v => v === Number.POSITIVE_INFINITY || v === Number.NEGATIVE_INFINITY - ) - ) - return [0, 0, 0, 0] - - return [box[0], box[1], box[2] - box[0], box[3] - box[1]].map( - roundToPrecision - ) as Box -} diff --git a/packages/plotter/src/plot-tree/coordinates.ts b/packages/plotter/src/plot-tree/coordinates.ts deleted file mode 100644 index 34042742..00000000 --- a/packages/plotter/src/plot-tree/coordinates.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Coordinate string utilities -import {TRAILING} from '@tracespace/parser' -import {PlotOptions} from '../options' - -export function parseCoordinate( - coord: string | null, - options: PlotOptions -): number { - if (coord === null) return Number.NaN - - // Short-circuit if coordinate has a decimal point - if (coord.includes('.')) return Number(coord) - - const {coordinateFormat, zeroSuppression} = options - const [integerPlaces, decimalPlaces] = coordinateFormat ?? [2, 4] - const numberDigits = integerPlaces + decimalPlaces - let sign = '+' - - // Handle optional sign - if (coord.startsWith('-') || coord.startsWith('+')) { - sign = coord[0] - coord = coord.slice(1) - } - - coord = - zeroSuppression === TRAILING - ? coord.padEnd(numberDigits, '0') - : coord.padStart(numberDigits, '0') - - const leading = coord.slice(0, integerPlaces) - const trailing = coord.slice(integerPlaces) - - return Number(`${sign}${leading}.${trailing}`) -} diff --git a/packages/plotter/src/plot-tree/create-plot.ts b/packages/plotter/src/plot-tree/create-plot.ts deleted file mode 100644 index 9eb030b3..00000000 --- a/packages/plotter/src/plot-tree/create-plot.ts +++ /dev/null @@ -1,217 +0,0 @@ -import * as Parser from '@tracespace/parser' - -import {PlotOptions} from '../options' -import {Position} from '../types' - -import { - ImageTree, - ImageLayer, - ImagePath, - ImageRegion, - IMAGE, - IMAGE_LAYER, - IMAGE_SHAPE, -} from '../tree' - -import * as BBox from './bounding-box' -import {parseCoordinate} from './coordinates' -import {pathFinished, addSegmentToPath, getPathBox} from './plot-path' -import {plotShape, getShapeBox} from './plot-shape' -import {plotMacro} from './plot-macro' - -const last = (coll: E[]): E | null => coll[coll.length - 1] || null - -export function createPlot( - tree: Parser.GerberTree, - options: PlotOptions -): ImageTree { - const tools = tree.children.filter( - (n): n is Parser.ToolDefinition => n.type === Parser.TOOL_DEFINITION - ) - const macros = tree.children.filter( - (n): n is Parser.ToolMacro => n.type === Parser.TOOL_MACRO - ) - const toolMap = Object.fromEntries(tools.map(node => [node.code, node])) - const macroMap = Object.fromEntries(macros.map(node => [node.name, node])) - const getTool = (code: string): Parser.ToolDefinition | null => { - return toolMap[code] || null - } - - let position: Position = [0, 0] - let tool: Parser.ToolDefinition | null = last(tools) ?? null - let lastGraphicSet: Parser.GraphicType = null - - let regionMode = false - let interpolateMode: Parser.InterpolateModeType = - tree.filetype === Parser.GERBER ? Parser.LINE : null - - // Arcs in drill files are always 180 degrees max - let quadrantMode: Parser.QuadrantModeType = null - - const currentLayer: ImageLayer = { - type: IMAGE_LAYER, - size: BBox.empty(), - children: [], - } - - let currentPath: ImagePath | ImageRegion | null = null - - for (const node of tree.children) { - visitNode(node) - } - - addCurrentPathToLayer() - - return { - type: IMAGE, - children: [currentLayer], - } - - function addCurrentPathToLayer(): void { - if (currentPath) { - const pathSize = getPathBox(currentPath) - currentLayer.size = BBox.add(currentLayer.size, pathSize) - currentLayer.children.push(currentPath) - currentPath = null - } - } - - function visitNode(node: Parser.ChildNode): void { - switch (node.type) { - case Parser.TOOL_CHANGE: { - tool = getTool(node.code) - break - } - - case Parser.INTERPOLATE_MODE: { - interpolateMode = node.mode - break - } - - case Parser.QUADRANT_MODE: { - quadrantMode = node.quadrant - break - } - - case Parser.REGION_MODE: { - regionMode = node.region - break - } - - case Parser.GRAPHIC: { - visitGraphicNode(node) - break - } - - default: - } - } - - function visitGraphicNode(node: Parser.Graphic): void { - const {graphic, coordinates} = node - const x = parseCoordinate(coordinates.x ?? null, options) - const y = parseCoordinate(coordinates.y ?? null, options) - let nextPosition: Position = [ - Number.isFinite(x) ? x : position[0], - Number.isFinite(y) ? y : position[1], - ] - let nextGraphic = graphic ?? lastGraphicSet - - // All drill files will have graphic set to null; check interpolate (route) - // mode for routing or drilling - if (graphic === null && tree.filetype === Parser.DRILL) { - if (interpolateMode === null) { - nextGraphic = Parser.SHAPE - } else if (interpolateMode === Parser.MOVE) { - nextGraphic = Parser.MOVE - } else { - nextGraphic = Parser.SEGMENT - } - } - - if (tool && nextGraphic === Parser.SHAPE) { - addShapeGraphic(tool, nextPosition) - } else if (nextGraphic === Parser.SEGMENT) { - addSegmentGraphic(coordinates, nextPosition) - } else if (nextGraphic === Parser.SLOT) { - nextPosition = addSlotGraphic(coordinates, nextPosition) - } - - lastGraphicSet = nextGraphic - position = nextPosition - } - - function addShapeGraphic( - tool: Parser.ToolDefinition, - nextPosition: Position - ): void { - const shape = - tool.shape.type === Parser.MACRO_SHAPE - ? plotMacro(macroMap[tool.shape.name], tool.shape.params, nextPosition) - : plotShape(tool.shape, tool.hole, nextPosition) - - const shapeSize = getShapeBox(shape) - - currentLayer.children.push({type: IMAGE_SHAPE, shape}) - currentLayer.size = BBox.add(currentLayer.size, shapeSize) - } - - function addSegmentGraphic( - coordinates: Parser.Coordinates, - nextPosition: Position - ): void { - if (currentPath !== null && pathFinished(currentPath, tool, regionMode)) { - addCurrentPathToLayer() - } - - const offsets = { - i: parseCoordinate(coordinates.i ?? null, options) || null, - j: parseCoordinate(coordinates.j ?? null, options) || null, - a: parseCoordinate(coordinates.a ?? null, options) || null, - } - - currentPath = addSegmentToPath({ - path: currentPath, - start: position, - end: nextPosition, - offsets, - tool, - interpolateMode, - regionMode, - quadrantMode, - }) - } - - function addSlotGraphic( - coordinates: Parser.Coordinates, - nextPosition: Position - ): Position { - const x1 = parseCoordinate(coordinates.x1 ?? null, options) - const y1 = parseCoordinate(coordinates.y1 ?? null, options) - const x2 = parseCoordinate(coordinates.x2 ?? null, options) - const y2 = parseCoordinate(coordinates.y2 ?? null, options) - const startPosition: Position = [ - Number.isFinite(x1) ? x1 : position[0], - Number.isFinite(y1) ? y1 : position[1], - ] - nextPosition = [ - Number.isFinite(x2) ? x2 : startPosition[0], - Number.isFinite(y2) ? y2 : startPosition[1], - ] - - addCurrentPathToLayer() - currentPath = addSegmentToPath({ - path: null, - start: startPosition, - end: nextPosition, - offsets: {i: null, j: null, a: null}, - tool, - interpolateMode: Parser.LINE, - regionMode: false, - quadrantMode: null, - }) - addCurrentPathToLayer() - - return nextPosition - } -} diff --git a/packages/plotter/src/plot-tree/geometry.ts b/packages/plotter/src/plot-tree/geometry.ts deleted file mode 100644 index 93755cf0..00000000 --- a/packages/plotter/src/plot-tree/geometry.ts +++ /dev/null @@ -1,112 +0,0 @@ -// Geometry object creators -import * as Tree from '../tree' -import {HALF_PI, PI, THREE_HALF_PI, TWO_PI, roundToPrecision} from './math' - -export const circle = ( - payload: Omit -): Tree.CircleShape => ({type: Tree.CIRCLE, ...payload}) - -export const rectangle = ( - payload: Omit -): Tree.RectangleShape => ({type: Tree.RECTANGLE, ...payload}) - -export const polygon = ( - payload: Omit -): Tree.PolygonShape => ({type: Tree.POLYGON, ...payload}) - -export const outline = ( - payload: Omit -): Tree.OutlineShape => ({type: Tree.OUTLINE, ...payload}) - -export const clearOutline = ( - payload: Omit -): Tree.ClearOutlineShape => ({type: Tree.CLEAR_OUTLINE, ...payload}) - -export const layeredShape = ( - payload: Omit -): Tree.LayeredShape => ({type: Tree.LAYERED_SHAPE, ...payload}) - -export const line = ( - payload: Omit -): Tree.PathLineSegment => ({type: Tree.LINE, ...payload}) - -export const arc = ( - payload: Omit & - Partial> -): Tree.PathArcSegment => ({ - ...payload, - type: Tree.ARC, - direction: payload.direction ?? Tree.CCW, -}) - -export const shapeToSegments = ( - shape: Tree.SimpleShape -): Tree.PathSegment[] => { - if (shape.type === Tree.CIRCLE) { - const {cx, cy, r} = shape - return [ - arc({ - start: [roundToPrecision(cx + r), cy, 0], - end: [roundToPrecision(cx + r), cy, 0], - center: [cx, cy], - radius: r, - sweep: TWO_PI, - }), - ] - } - - if (shape.type === Tree.RECTANGLE) { - const {x, y, xSize, ySize, r} = shape - - return r === null - ? [ - line({start: [x, y], end: [x + xSize, y]}), - line({start: [x + xSize, y], end: [x + xSize, y + ySize]}), - line({start: [x + xSize, y + ySize], end: [x, y + ySize]}), - line({start: [x, y + ySize], end: [x, y]}), - ] - : [ - line({start: [x + r, y], end: [x + xSize - r, y]}), - arc({ - start: [x + xSize - r, y, THREE_HALF_PI], - end: [x + xSize, y + r, 0], - center: [x + xSize - r, y + r], - radius: r, - sweep: HALF_PI, - }), - line({start: [x + xSize, y + r], end: [x + xSize, y + ySize - r]}), - arc({ - start: [x + xSize, y + ySize - r, 0], - end: [x + xSize - r, y + ySize, HALF_PI], - center: [x + xSize - r, y + ySize - r], - radius: r, - sweep: HALF_PI, - }), - line({start: [x + xSize - r, y + ySize], end: [x + r, y + ySize]}), - arc({ - start: [x + r, y + ySize, HALF_PI], - end: [x, y + ySize - r, PI], - center: [x + r, y + ySize - r], - radius: r, - sweep: HALF_PI, - }), - line({start: [x, y + ySize - r], end: [x, y + r]}), - arc({ - start: [x, y + r, PI], - end: [x + r, y, THREE_HALF_PI], - center: [x + r, y + r], - radius: r, - sweep: HALF_PI, - }), - ] - } - - if (shape.type === Tree.POLYGON) { - return shape.points.flatMap((end, i) => { - const start = shape.points[i - 1] - return start ? [line({start, end})] : [] - }) - } - - return shape.segments -} diff --git a/packages/plotter/src/plot-tree/index.ts b/packages/plotter/src/plot-tree/index.ts deleted file mode 100644 index 693702a6..00000000 --- a/packages/plotter/src/plot-tree/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './create-plot' -export {getShapeBox} from './plot-shape' - -export * as BoundingBox from './bounding-box' diff --git a/packages/plotter/src/plot-tree/math.ts b/packages/plotter/src/plot-tree/math.ts deleted file mode 100644 index f21eeabb..00000000 --- a/packages/plotter/src/plot-tree/math.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Mathematical procedures -import {Position} from '../types' - -const PRECISION = 10_000_000_000 - -export const {PI} = Math -export const HALF_PI = PI / 2 -export const THREE_HALF_PI = 3 * HALF_PI -export const TWO_PI = 2 * PI - -export const limitAngle = (theta: number): number => { - if (theta >= 0 && theta <= TWO_PI) return theta - if (theta < 0) return theta + TWO_PI - if (theta > TWO_PI) return theta - TWO_PI - return limitAngle(theta) -} - -export const rotateQuadrant = (theta: number): number => - theta >= HALF_PI ? theta - HALF_PI : theta + THREE_HALF_PI - -export const degreesToRadians = (degrees: number): number => - (degrees * Math.PI) / 180 - -export function rotateAndShift( - point: Position, - shift: Position, - degrees = 0 -): Position { - const rotation = degreesToRadians(degrees) - const [sin, cos] = [Math.sin(rotation), Math.cos(rotation)] - const [x, y] = point - - return [ - roundToPrecision(x * cos - y * sin + shift[0]), - roundToPrecision(x * sin + y * cos + shift[1]), - ] -} - -// Avoid weird floating point rounding stuff -export function roundToPrecision(n: number): number { - const rounded = Math.round(n * PRECISION) / PRECISION - - // Remove -0 for ease - return rounded === 0 ? 0 : rounded -} - -export const positionsEqual = (a: number[], b: number[]): boolean => { - const [ax, ay, bx, by] = [a[0], a[1], b[0], b[1]].map(roundToPrecision) - return ax === bx && ay === by -} diff --git a/packages/plotter/src/plot-tree/plot-macro.ts b/packages/plotter/src/plot-tree/plot-macro.ts deleted file mode 100644 index 41708c79..00000000 --- a/packages/plotter/src/plot-tree/plot-macro.ts +++ /dev/null @@ -1,306 +0,0 @@ -// Plot a tool macro as shapes -import * as Parser from '@tracespace/parser' -import * as Tree from '../tree' -import {Position} from '../types' -import * as Geo from './geometry' -import {rotateAndShift, roundToPrecision, positionsEqual, PI} from './math' -import {getCenterAngles} from './arc-segment' - -type ParametersMap = Record - -export function plotMacro( - macro: Parser.ToolMacro, - parameters: number[], - origin: Position -): Tree.LayeredShape { - const shapes: Tree.Shape[] = [] - const parameterMap = Object.fromEntries( - parameters.map((value, i) => [`$${i + 1}`, value]) - ) - - for (const block of macro.children) { - if (block.type === Parser.MACRO_VARIABLE) { - parameterMap[block.name] = solveExpression(block.value, parameterMap) - } else if (block.type === Parser.MACRO_PRIMITIVE) { - const mods = block.modifiers.map(m => solveExpression(m, parameterMap)) - const shape = plotPrimitive(block.code, origin, mods) - - if (shape !== null) shapes.push(shape) - } - } - - return Geo.layeredShape({shapes}) -} - -function solveExpression( - expression: Parser.MacroValue, - parameters: ParametersMap -): number { - if (typeof expression === 'number') return expression - if (typeof expression === 'string') return parameters[expression] - - const left = solveExpression(expression.left, parameters) - const right = solveExpression(expression.right, parameters) - - switch (expression.operator) { - case '+': - return left + right - case '-': - return left - right - case 'x': - return left * right - case '/': - return left / right - } -} - -function plotPrimitive( - code: Parser.MacroPrimitiveCode | string, - origin: Position, - modifiers: number[] -): Tree.Shape | null { - switch (code) { - case Parser.MACRO_CIRCLE: { - return plotCircle(origin, modifiers) - } - - case Parser.MACRO_VECTOR_LINE: { - return plotVectorLine(origin, modifiers) - } - - case Parser.MACRO_CENTER_LINE: { - return plotCenterLine(origin, modifiers) - } - - case Parser.MACRO_OUTLINE: { - return plotOutline(origin, modifiers) - } - - case Parser.MACRO_POLYGON: { - return plotPolygon(origin, modifiers) - } - - case Parser.MACRO_MOIRE: { - return plotMoire(origin, modifiers) - } - - case Parser.MACRO_THERMAL: { - return plotThermal(origin, modifiers) - } - } - - return null -} - -function plotCircle(origin: Position, modifiers: number[]): Tree.Shape { - const [exposure, diameter, cx0, cy0, degrees] = modifiers - const r = diameter / 2 - const [cx, cy] = rotateAndShift([cx0, cy0], origin, degrees) - const circle = Geo.circle({cx, cy, r}) - - return exposure === 1 - ? circle - : Geo.clearOutline({segments: Geo.shapeToSegments(circle)}) -} - -function plotVectorLine(origin: Position, modifiers: number[]): Tree.Shape { - const [exposure, width, sx, sy, ex, ey, degrees] = modifiers - const [dy, dx] = [ey - sy, ex - sx] - const halfWid = width / 2 - const dist = Math.sqrt(dy ** 2 + dx ** 2) - const [xOff, yOff] = [(halfWid * dx) / dist, (halfWid * dy) / dist] - const polygon = Geo.polygon({ - points: ( - [ - [sx + xOff, sy - yOff], - [ex + xOff, ey - yOff], - [ex - xOff, ey + yOff], - [sx - xOff, sy + yOff], - ] as Position[] - ).map(p => rotateAndShift(p, origin, degrees)), - }) - - return exposure === 1 - ? polygon - : Geo.clearOutline({segments: Geo.shapeToSegments(polygon)}) -} - -function plotCenterLine(origin: Position, modifiers: number[]): Tree.Shape { - const [exposure, width, height, cx, cy, degrees] = modifiers - const [halfWidth, halfHeight] = [width / 2, height / 2] - const polygon = Geo.polygon({ - points: ( - [ - [cx - halfWidth, cy - halfHeight], - [cx + halfWidth, cy - halfHeight], - [cx + halfWidth, cy + halfHeight], - [cx - halfWidth, cy + halfHeight], - ] as Position[] - ).map(p => rotateAndShift(p, origin, degrees)), - }) - - return exposure === 1 - ? polygon - : Geo.clearOutline({segments: Geo.shapeToSegments(polygon)}) -} - -function plotOutline(origin: Position, modifiers: number[]): Tree.Shape { - const [exposure, , ...coords] = modifiers.slice(0, -1) - const degrees = modifiers[modifiers.length - 1] - const polygon = Geo.polygon({ - points: coords - .flatMap<[number, number]>((coordinate, i) => - i % 2 === 1 ? [[coords[i - 1], coordinate]] : [] - ) - .map(p => rotateAndShift(p, origin, degrees)), - }) - - return exposure === 1 - ? polygon - : Geo.clearOutline({segments: Geo.shapeToSegments(polygon)}) -} - -function plotPolygon(origin: Position, modifiers: number[]): Tree.Shape { - const [exposure, vertices, cx, cy, diameter, degrees] = modifiers - const r = diameter / 2 - const step = (2 * PI) / vertices - const points: Position[] = [] - let i - - for (i = 0; i < vertices; i++) { - const theta = step * i - const pointX = cx + r * Math.cos(theta) - const pointY = cy + r * Math.sin(theta) - points.push(rotateAndShift([pointX, pointY], origin, degrees)) - } - - const polygon = Geo.polygon({points}) - - return exposure === 1 - ? polygon - : Geo.clearOutline({segments: Geo.shapeToSegments(polygon)}) -} - -function plotMoire(origin: Position, modifiers: number[]): Tree.Shape { - const rotate = (p: Position): Position => - rotateAndShift(p, origin, modifiers[8]) - - const [cx0, cy0, d, ringThx, ringGap, ringN, lineThx, lineLength] = modifiers - const [cx, cy] = rotate([cx0, cy0]) - const halfLineThx = lineThx / 2 - const halfLineLength = lineLength / 2 - - const radii = [] - let count = 0 - let dRemain = d - - while (dRemain >= 0 && count < ringN) { - const r = roundToPrecision(dRemain / 2) - const rHole = roundToPrecision(r - ringThx) - - radii.push(r) - if (rHole > 0) radii.push(rHole) - count += 1 - dRemain = 2 * (rHole - ringGap) - } - - const moireShapes: Tree.SimpleShape[] = [ - Geo.outline({ - segments: radii.flatMap(r => - Geo.shapeToSegments(Geo.circle({cx, cy, r})) - ), - }), - // Vertical stroke - Geo.polygon({ - points: ( - [ - [cx0 - halfLineThx, cy0 - halfLineLength], - [cx0 + halfLineThx, cy0 - halfLineLength], - [cx0 + halfLineThx, cy0 + halfLineLength], - [cx0 - halfLineThx, cy0 + halfLineLength], - ] as Position[] - ).map(rotate), - }), - // Horizontal stroke - Geo.polygon({ - points: ( - [ - [cx0 - halfLineLength, cy0 - halfLineThx], - [cx0 + halfLineLength, cy0 - halfLineThx], - [cx0 + halfLineLength, cy0 + halfLineThx], - [cx0 - halfLineLength, cy0 + halfLineThx], - ] as Position[] - ).map(rotate), - }), - ] - - return Geo.layeredShape({shapes: moireShapes}) -} - -function plotThermal(origin: Position, modifiers: number[]): Tree.Shape { - const [cx0, cy0, od, id, gap, degrees] = modifiers - const center = rotateAndShift([cx0, cy0], origin, degrees) - const [or, ir] = [od / 2, id / 2] - const halfGap = gap / 2 - const oIntSquare = or ** 2 - halfGap ** 2 - const iIntSquare = ir ** 2 - halfGap ** 2 - const oInt = Math.sqrt(oIntSquare) - const iInt = iIntSquare >= 0 ? Math.sqrt(iIntSquare) : halfGap - const positions = [0, 90, 180, 270] - const segments: Tree.PathSegment[] = [] - - for (const rot of positions) { - const points = ( - [ - [iInt, halfGap], - [oInt, halfGap], - [halfGap, oInt], - [halfGap, iInt], - ] as Position[] - ) - .map(p => rotateAndShift(p, [cx0, cy0], rot)) - .map(p => rotateAndShift(p, origin, degrees)) - - const [os, oe, , oSweep] = getCenterAngles( - points[1], - points[2], - center, - Tree.CCW - ) - - segments.push( - Geo.line({start: points[0], end: points[1]}), - Geo.arc({ - start: os, - end: oe, - center, - radius: or, - sweep: oSweep, - direction: Tree.CCW, - }), - Geo.line({start: points[2], end: points[3]}) - ) - - if (!positionsEqual(points[0], points[3])) { - const [is, ie, , iSweep] = getCenterAngles( - points[3], - points[0], - center, - Tree.CW - ) - - segments.push( - Geo.arc({ - start: is, - end: ie, - center, - radius: ir, - sweep: iSweep, - direction: Tree.CW, - }) - ) - } - } - - return Geo.outline({segments}) -} diff --git a/packages/plotter/src/plot-tree/plot-path.ts b/packages/plotter/src/plot-tree/plot-path.ts deleted file mode 100644 index 1cf3ddbd..00000000 --- a/packages/plotter/src/plot-tree/plot-path.ts +++ /dev/null @@ -1,152 +0,0 @@ -// Adds a path segment to the current ImagePath or returns a new ImagePath -// if the tool has changed -import { - ToolDefinition, - InterpolateModeType, - QuadrantModeType, - CIRCLE as PARSER_CIRCLE, - RECTANGLE as PARSER_RECTANGLE, - LINE as PARSER_LINE, - CW_ARC as PARSER_CW_ARC, - CCW_ARC as PARSER_CCW_ARC, -} from '@tracespace/parser' - -import { - ImagePath, - ImageRegion, - OutlineShape, - ClearOutlineShape, - PathSegment, - IMAGE_PATH, - IMAGE_REGION, - ARC, - CCW, -} from '../tree' - -import {Box, Position, Offsets} from '../types' -import {rotateQuadrant, limitAngle, TWO_PI} from './math' -import * as BBox from './bounding-box' -import {line} from './geometry' -import {makeArcSegment} from './arc-segment' -import {plotRectPath} from './plot-rect-path' - -export function pathFinished( - path: ImagePath | ImageRegion, - tool: ToolDefinition | null, - regionMode: boolean -): boolean { - return ( - // Path is done if region mode has switched - (path.type === IMAGE_REGION && !regionMode) || - (path.type === IMAGE_PATH && regionMode) || - // If we drew a convenience region (e.g rectangular stroke) but now we're - // doing a region for real, we need to end the path - (path.type === IMAGE_REGION && regionMode && !path.meta.regionMode) || - // Path is done if the tool is a circle and the diameter has changed - (path.type === IMAGE_PATH && - tool !== null && - tool.shape.type === PARSER_CIRCLE && - tool.shape.diameter !== path.width) || - // Only use one path per non-region rectangular tool stroke - (!regionMode && tool !== null && tool.shape.type === PARSER_RECTANGLE) - ) -} - -export interface AddSegmentToPathOptions { - path: ImagePath | ImageRegion | null - start: Position - end: Position - offsets: Offsets - tool: ToolDefinition | null - interpolateMode: InterpolateModeType - regionMode: boolean - quadrantMode: QuadrantModeType -} - -export function addSegmentToPath({ - path, - start, - end, - offsets, - tool, - interpolateMode, - regionMode, - quadrantMode, -}: AddSegmentToPathOptions): ImagePath | ImageRegion { - if (!regionMode && tool && tool.shape.type === PARSER_RECTANGLE) { - return plotRectPath(start, end, tool.shape) - } - - const diameter = - tool && tool.shape.type === PARSER_CIRCLE ? tool.shape.diameter : 0 - - let segment: PathSegment | null = null - - if (interpolateMode === PARSER_LINE) { - segment = line({start, end}) - } else if ( - interpolateMode === PARSER_CW_ARC || - interpolateMode === PARSER_CCW_ARC - ) { - segment = makeArcSegment({ - start, - end, - offsets, - interpolateMode, - quadrantMode, - }) - } - - let nextPath: ImagePath | ImageRegion | null = path - - if (!nextPath) { - nextPath = regionMode - ? {type: IMAGE_REGION, segments: [], meta: {regionMode: true}} - : {type: IMAGE_PATH, width: diameter, segments: []} - } - - if (segment) nextPath.segments.push(segment) - - return nextPath -} - -export function getPathBox( - path: ImagePath | ImageRegion | OutlineShape | ClearOutlineShape -): Box { - let box = BBox.empty() - - for (const segment of path.segments) { - const rTool = path.type === IMAGE_PATH ? path.width / 2 : 0 - const keyPoints = [segment.start, segment.end] - - if (segment.type === ARC) { - const {start, end, center, sweep, radius} = segment - // Normalize direction to counter-clockwise - let [thetaStart, thetaEnd] = - segment.direction === CCW ? [start[2], end[2]] : [end[2], start[2]] - - thetaStart = limitAngle(thetaStart) - thetaEnd = limitAngle(thetaEnd) - - const axisPoints: Position[] = [ - [center[0] + radius, center[1]], - [center[0], center[1] + radius], - [center[0] - radius, center[1]], - [center[0], center[1] - radius], - ] - - for (const p of axisPoints) { - if (thetaStart > thetaEnd || sweep === TWO_PI) keyPoints.push(p) - // Rotate to check for next axis key point - thetaStart = rotateQuadrant(thetaStart) - thetaEnd = rotateQuadrant(thetaEnd) - } - } - - for (const b of keyPoints.map(p => BBox.fromCircle(p[0], p[1], rTool))) { - box = BBox.add(box, b) - } - } - - return box -} diff --git a/packages/plotter/src/plot-tree/plot-shape.ts b/packages/plotter/src/plot-tree/plot-shape.ts deleted file mode 100644 index f9c4a410..00000000 --- a/packages/plotter/src/plot-tree/plot-shape.ts +++ /dev/null @@ -1,141 +0,0 @@ -import * as Parser from '@tracespace/parser' - -import { - CIRCLE, - RECTANGLE, - POLYGON, - OUTLINE, - CLEAR_OUTLINE, - LAYERED_SHAPE, - Shape, - SimpleShape, - HoleShape, -} from '../tree' - -import {Position, Box} from '../types' - -import {roundToPrecision, degreesToRadians, PI} from './math' -import {getPathBox} from './plot-path' -import * as BBox from './bounding-box' -import * as Geo from './geometry' - -type SimpleParserShape = - | Parser.Circle - | Parser.Rectangle - | Parser.Obround - | Parser.Polygon - -export function plotShape( - toolShape: SimpleParserShape, - holeShape: Parser.HoleShape | null, - position: Position -): SimpleShape { - const [x, y] = position - const hole = plotHole(holeShape, position) - const holeSegments = hole === null ? null : Geo.shapeToSegments(hole) - - switch (toolShape.type) { - case Parser.CIRCLE: { - const circle = Geo.circle({cx: x, cy: y, r: toolShape.diameter / 2}) - - return holeSegments === null - ? circle - : Geo.outline({ - segments: [...Geo.shapeToSegments(circle), ...holeSegments], - }) - } - - case Parser.RECTANGLE: - case Parser.OBROUND: { - const {xSize, ySize} = toolShape - const xHalf = xSize / 2 - const yHalf = ySize / 2 - const r = - toolShape.type === Parser.OBROUND ? Math.min(xHalf, yHalf) : null - const rectangle = Geo.rectangle({ - x: x - xHalf, - y: y - yHalf, - xSize, - ySize, - r, - }) - - return holeSegments === null - ? rectangle - : Geo.outline({ - segments: [...Geo.shapeToSegments(rectangle), ...holeSegments], - }) - } - - case Parser.POLYGON: { - const r = toolShape.diameter / 2 - const offset = degreesToRadians(toolShape.rotation ?? 0) - const step = (2 * PI) / toolShape.vertices - const points = [] - let i - - for (i = 0; i < toolShape.vertices; i++) { - const theta = step * i + offset - const pointX = roundToPrecision(x + r * Math.cos(theta)) - const pointY = roundToPrecision(y + r * Math.sin(theta)) - points.push([pointX, pointY] as Position) - } - - const polygon = Geo.polygon({points}) - - return holeSegments === null - ? polygon - : Geo.outline({ - segments: [...Geo.shapeToSegments(polygon), ...holeSegments], - }) - } - } -} - -function plotHole( - holeShape: Parser.HoleShape | null, - position: Position -): HoleShape { - if (holeShape === null) return null - const hole = plotShape(holeShape, null, position) - return hole.type === CIRCLE || hole.type === RECTANGLE ? hole : null -} - -export function getShapeBox(shape: Shape): Box { - switch (shape.type) { - case CIRCLE: { - return BBox.fromCircle(shape.cx, shape.cy, shape.r) - } - - case RECTANGLE: { - return BBox.fromRectangle(shape.x, shape.y, shape.xSize, shape.ySize) - } - - case POLYGON: { - let box = BBox.empty() - for (const point of shape.points) { - box = BBox.addPosition(box, point) - } - - return box - } - - case OUTLINE: - case CLEAR_OUTLINE: { - return getPathBox(shape) - } - - case LAYERED_SHAPE: { - let box = BBox.empty() - for (const s of shape.shapes) { - box = BBox.add(box, getShapeBox(s)) - } - - return box - } - - default: - } - - return BBox.empty() -} diff --git a/packages/plotter/src/tool-store.ts b/packages/plotter/src/tool-store.ts new file mode 100644 index 00000000..dcd8bff4 --- /dev/null +++ b/packages/plotter/src/tool-store.ts @@ -0,0 +1,78 @@ +// Tool store +// Keeps track of the defined tools, defined macros, and the current tool +import { + MACRO_SHAPE, + TOOL_CHANGE, + TOOL_DEFINITION, + TOOL_MACRO, + GerberNode, + SimpleShape, + HoleShape, + MacroBlock, +} from '@tracespace/parser' + +export const SIMPLE_TOOL = 'simpleTool' + +export const MACRO_TOOL = 'macroTool' + +export interface SimpleTool { + type: typeof SIMPLE_TOOL + shape: SimpleShape + hole?: HoleShape +} + +export interface MacroTool { + type: typeof MACRO_TOOL + macro: MacroBlock[] + variableValues: number[] +} + +export type Tool = SimpleTool | MacroTool + +export interface ToolStore { + use(node: GerberNode): Tool | undefined +} + +export function createToolStore(): ToolStore { + return Object.create(ToolStorePrototype) +} + +interface ToolStoreState { + _currentToolCode: string | undefined + _toolsByCode: Partial> + _macrosByName: Partial> +} + +const ToolStorePrototype: ToolStore & ToolStoreState = { + _currentToolCode: undefined, + _toolsByCode: {}, + _macrosByName: {}, + + use(node: GerberNode): Tool | undefined { + if (node.type === TOOL_MACRO) { + this._macrosByName[node.name] = node.children + } + + if (node.type === TOOL_DEFINITION) { + const {shape, hole} = node + const tool: Tool = + shape.type === MACRO_SHAPE + ? { + type: MACRO_TOOL, + macro: this._macrosByName[shape.name] ?? [], + variableValues: shape.variableValues, + } + : {type: SIMPLE_TOOL, shape, ...(hole && {hole})} + + this._toolsByCode[node.code] = tool + } + + if (node.type === TOOL_DEFINITION || node.type === TOOL_CHANGE) { + this._currentToolCode = node.code + } + + return typeof this._currentToolCode === 'string' + ? this._toolsByCode[this._currentToolCode] + : undefined + }, +} diff --git a/packages/plotter/src/tree.ts b/packages/plotter/src/tree.ts index bd9fc7de..0a5314bd 100644 --- a/packages/plotter/src/tree.ts +++ b/packages/plotter/src/tree.ts @@ -1,5 +1,6 @@ import {Node, Parent} from 'unist' -import {Position, ArcPosition, Box} from './types' + +import {UnitsType} from '@tracespace/parser' export const IMAGE = 'image' export const IMAGE_LAYER = 'imageLayer' @@ -7,21 +8,20 @@ export const IMAGE_SHAPE = 'imageShape' export const IMAGE_PATH = 'imagePath' export const IMAGE_REGION = 'imageRegion' -export const DRAW = 'draw' -export const CLEAR = 'clear' export const LINE = 'line' export const ARC = 'arc' -export const CW = 'cw' -export const CCW = 'ccw' export const CIRCLE = 'circle' export const RECTANGLE = 'rectangle' export const POLYGON = 'polygon' export const OUTLINE = 'outline' -export const CLEAR_OUTLINE = 'clearOutline' export const LAYERED_SHAPE = 'layeredShape' -export type Direction = typeof CW | typeof CCW +export type Position = [x: number, y: number] + +export type ArcPosition = [x: number, y: number, theta: number] + +export type SizeEnvelope = [x1: number, y1: number, x2: number, y2: number] | [] export type ImageNode = | ImageTree @@ -43,7 +43,7 @@ export interface RectangleShape { y: number xSize: number ySize: number - r: number | null + r?: number } export interface PolygonShape { @@ -56,38 +56,35 @@ export interface OutlineShape { segments: PathSegment[] } -export interface ClearOutlineShape { - type: typeof CLEAR_OUTLINE - segments: PathSegment[] -} - export interface LayeredShape { type: typeof LAYERED_SHAPE - shapes: Array + shapes: ErasableShape[] } -export type HoleShape = CircleShape | RectangleShape | null +export type HoleShape = CircleShape | RectangleShape export type SimpleShape = | CircleShape | RectangleShape | PolygonShape | OutlineShape - | ClearOutlineShape export type Shape = SimpleShape | LayeredShape +export type ErasableShape = SimpleShape & {erase?: boolean} + +export type ImageGraphic = ImageShape | ImagePath | ImageRegion + export interface ImageTree extends Parent { type: typeof IMAGE + units: UnitsType children: [ImageLayer] } export interface ImageLayer extends Parent { type: typeof IMAGE_LAYER - size: Box - polarity?: Polarity - repeat?: [number, number, number, number] - children: Array + size: SizeEnvelope + children: ImageGraphic[] } export interface ImageShape extends Node { @@ -104,15 +101,10 @@ export interface ImagePath extends Node { export interface ImageRegion extends Node { type: typeof IMAGE_REGION segments: PathSegment[] - meta: {regionMode: boolean} } -export type Polarity = typeof DRAW | typeof CLEAR - export type PathSegment = PathLineSegment | PathArcSegment -export type ArcDirection = typeof CW | typeof CCW - export interface PathLineSegment { type: typeof LINE start: Position @@ -125,6 +117,4 @@ export interface PathArcSegment { end: ArcPosition center: Position radius: number - sweep: number - direction: ArcDirection } diff --git a/packages/plotter/src/types.ts b/packages/plotter/src/types.ts deleted file mode 100644 index 8c6ea3fd..00000000 --- a/packages/plotter/src/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type Position = [x: number, y: number] - -export type ArcPosition = [x: number, y: number, theta: number] - -export type Box = [x1: number, y1: number, x2: number, y2: number] - -export type Offsets = {i: number | null; j: number | null; a: number | null} diff --git a/packages/plotter/src/typings/object.d.ts b/packages/plotter/src/typings/object.d.ts new file mode 100644 index 00000000..60320f32 --- /dev/null +++ b/packages/plotter/src/typings/object.d.ts @@ -0,0 +1,7 @@ +export {} + +declare global { + interface ObjectConstructor { + create(o: T): T + } +} diff --git a/packages/plotter/vite.config.ts b/packages/plotter/vite.config.ts index 4018c411..461b9926 100644 --- a/packages/plotter/vite.config.ts +++ b/packages/plotter/vite.config.ts @@ -1,6 +1,7 @@ import {defineConfig} from 'vite' import {baseConfig, libraryFilename} from '../../config/vite.config.base' +import pkg from './package.json' export default defineConfig({ ...baseConfig, @@ -11,7 +12,7 @@ export default defineConfig({ fileName: libraryFilename('tracespace-plotter'), }, rollupOptions: { - external: ['@tracespace/parser'], + external: Object.keys(pkg.dependencies), output: { globals: { '@tracespace/parser': 'TracespaceParser', diff --git a/packages/renderer/.npmignore b/packages/renderer/.npmignore new file mode 100644 index 00000000..ebe5ad3a --- /dev/null +++ b/packages/renderer/.npmignore @@ -0,0 +1,2 @@ +*.tsbuildinfo +__tests__ diff --git a/packages/renderer/README.md b/packages/renderer/README.md new file mode 100644 index 00000000..0978b204 --- /dev/null +++ b/packages/renderer/README.md @@ -0,0 +1,42 @@ +# @tracespace/renderer + +An SVG renderer for [@tracespace/plotter][] images. + +Part of the [tracespace][] collection of PCB visualization tools. + +**This package is still in development and is not yet published.** + +```shell +npm install @tracespace/renderer@next +``` + +[@tracespace/plotter]: https://www.npmjs.com/package/@tracespace/plotter +[tracespace]: https://github.com/tracespace/tracespace + +## usage + +```js +import {createParser} from '@tracespace/parser' +import {plot} from '@tracespace/plotter' +import {render} from '@tracespace/renderer' + +const syntaxTree = createParser().feed(/* ...some gerber string... */).results() +const imageTree = plot(syntaxTree) +const image = render(imageTree) +``` + +### script tag + +If you're not using a bundler and you want to try out the parser in a browser, you can use a `script` tag: + +```html + + + + +``` diff --git a/packages/renderer/package.json b/packages/renderer/package.json new file mode 100644 index 00000000..5a19eb9b --- /dev/null +++ b/packages/renderer/package.json @@ -0,0 +1,57 @@ +{ + "name": "@tracespace/renderer", + "publishConfig": { + "access": "public" + }, + "version": "0.0.0-unreleased", + "description": "Render @tracespace/plotter image trees as SVGs", + "main": "./dist/tracespace-renderer.umd.cjs", + "module": "./dist/tracespace-renderer.es.js", + "types": "./lib/index.d.ts", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "source": "./src/index.ts", + "import": "./dist/tracespace-renderer.es.js", + "require": "./dist/tracespace-renderer.umd.cjs" + } + }, + "type": "module", + "sideEffects": false, + "repository": { + "type": "git", + "url": "git+https://github.com/tracespace/tracespace.git", + "directory": "packages/renderer" + }, + "scripts": { + "build": "vite build" + }, + "keywords": [ + "pcb", + "circuit", + "board", + "hardware", + "electronics" + ], + "contributors": [ + "Mike Cousins (https://mike.cousins.io)" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/tracespace/tracespace/issues" + }, + "homepage": "https://github.com/tracespace/tracespace#readme", + "dependencies": { + "@tracespace/plotter": "workspace:*", + "@tracespace/parser": "workspace:*", + "@tracespace/xml-id": "workspace:*", + "@types/hast": "^2.3.4", + "whats-that-gerber": "workspace:*" + }, + "devDependencies": { + "@types/unist": "^2.0.6", + "hastscript": "^7.0.2", + "unist-util-map": "^3.1.1", + "unist-util-visit-parents": "^5.1.0" + } +} diff --git a/packages/renderer/src/__tests__/shape-to-element.test.ts b/packages/renderer/src/__tests__/shape-to-element.test.ts new file mode 100644 index 00000000..526d6a26 --- /dev/null +++ b/packages/renderer/src/__tests__/shape-to-element.test.ts @@ -0,0 +1,159 @@ +import {describe, it, expect} from 'vitest' + +import * as Plotter from '@tracespace/plotter' +import {shapeToElement as mapShapeToElement} from '../render' + +describe('mapping a shape to an element', () => { + it('should map a circle', () => { + const shape: Plotter.Shape = {type: Plotter.CIRCLE, cx: 1, cy: 2, r: 3} + const result = mapShapeToElement(shape) + + expect(result).to.deep.include({ + tagName: 'circle', + properties: {cx: 1, cy: 2, r: 3}, + children: [], + }) + }) + + it('should map a rectangle', () => { + const shape: Plotter.Shape = { + type: Plotter.RECTANGLE, + x: 1, + y: 2, + xSize: 3, + ySize: 4, + } + const result = mapShapeToElement(shape) + + expect(result).to.deep.include({ + tagName: 'rect', + properties: {x: 1, y: 2, width: 3, height: 4}, + children: [], + }) + }) + + it('should map a rectangle with radius', () => { + const shape: Plotter.Shape = { + type: Plotter.RECTANGLE, + x: 1, + y: 2, + xSize: 3, + ySize: 4, + r: 0.25, + } + const result = mapShapeToElement(shape) + + expect(result).to.deep.include({ + tagName: 'rect', + properties: {x: 1, y: 2, width: 3, height: 4, rx: 0.25, ry: 0.25}, + children: [], + }) + }) + + it('should map a polygon', () => { + const shape: Plotter.Shape = { + type: Plotter.POLYGON, + points: [ + [1, 1], + [2, 1], + [2, 2], + [1, 2], + ], + } + const result = mapShapeToElement(shape) + + expect(result).to.deep.include({ + tagName: 'polygon', + properties: {points: '1,1 2,1 2,2 1,2'}, + children: [], + }) + }) + + describe('outline shapes', () => { + it('should map contiguous line segments', () => { + const shape: Plotter.Shape = { + type: Plotter.OUTLINE, + segments: [ + {type: Plotter.LINE, start: [0, 0], end: [1, 1]}, + {type: Plotter.LINE, start: [1, 1], end: [2, 2]}, + ], + } + const result = mapShapeToElement(shape) + + expect(result).to.deep.include({ + tagName: 'path', + properties: {d: 'M0 0L1 1L2 2'}, + children: [], + }) + }) + + it('should map non-contiguous line segments', () => { + const shape: Plotter.Shape = { + type: Plotter.OUTLINE, + segments: [ + {type: Plotter.LINE, start: [1, 2], end: [3, 4]}, + {type: Plotter.LINE, start: [5, 6], end: [7, 8]}, + ], + } + const result = mapShapeToElement(shape) + + expect(result).to.deep.include({ + tagName: 'path', + properties: {d: 'M1 2L3 4M5 6L7 8'}, + children: [], + }) + }) + + it('should map contiguous arc segments', () => { + const shape: Plotter.Shape = { + type: Plotter.OUTLINE, + segments: [ + { + type: Plotter.ARC, + start: [0, 0, 3.141_592_653_6], + end: [0.25, 0.25, 1.570_796_326_8], + center: [0.25, 0], + radius: 0.25, + }, + { + type: Plotter.ARC, + start: [0.25, 0.25, -1.570_796_326_8], + end: [0.5, 0.5, 0], + center: [0.25, 0.5], + radius: 0.25, + }, + { + type: Plotter.ARC, + start: [0.5, 0.5, 3.141_592_653_6], + end: [0.75, 0.25, -1.570_796_326_8], + center: [0.75, 0.5], + radius: 0.25, + }, + { + type: Plotter.ARC, + start: [0.75, 0.25, 1.570_796_326_8], + end: [1, 0, 6.283_185_307_2], + center: [0.75, 0], + radius: 0.25, + }, + ], + } + const result = mapShapeToElement(shape) + + // Circular arc: `A ${r} ${r} 0 ${large_arc} ${sweep} ${x} ${y}` + expect(result).to.deep.include({ + tagName: 'path', + properties: { + d: [ + 'M0 0', + 'A0.25 0.25 0 0 0 0.25 0.25', + 'A0.25 0.25 0 0 1 0.5 0.5', + 'A0.25 0.25 0 1 0 0.75 0.25', + 'A0.25 0.25 0 1 1 1 0', + ].join(''), + }, + children: [], + }) + }) + }) +}) diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts new file mode 100644 index 00000000..7be54d28 --- /dev/null +++ b/packages/renderer/src/index.ts @@ -0,0 +1,87 @@ +import {s} from 'hastscript' +import {map} from 'unist-util-map' +import {visitParents} from 'unist-util-visit-parents' + +import { + ImageTree, + ImageNode, + ImageLayer, + IMAGE, + IMAGE_LAYER, + IMAGE_SHAPE, + IMAGE_PATH, + IMAGE_REGION, + BoundingBox as BBox, +} from '@tracespace/plotter' + +import {renderShape, renderPath} from './render' +import type {SvgElement} from './types' + +export type {SvgElement} from './types' + +export const BASE_SVG_PROPS = { + version: '1.1', + xmlns: 'http://www.w3.org/2000/svg', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + 'stroke-width': '0', + 'fill-rule': 'evenodd', + fill: 'currentColor', + stroke: 'currentColor', +} + +export function render(image: ImageTree): SvgElement { + const svgTree = map(image, mapImageTreeToSvg) + + if (svgTree.properties) { + const {width, height} = svgTree.properties + + if (typeof width === 'number') { + svgTree.properties.width = `${width}${image.units}` + } + + if (typeof height === 'number') { + svgTree.properties.height = `${height}${image.units}` + } + } + + return svgTree +} + +function mapImageTreeToSvg(node: ImageNode): SvgElement { + switch (node.type) { + case IMAGE: { + let box = BBox.empty() + visitParents(node, IMAGE_LAYER, (layer: ImageLayer) => { + box = BBox.add(box, layer.size) + }) + const [xMin, yMin, width, height] = BBox.toViewBox(box) + const props = { + ...BASE_SVG_PROPS, + width, + height, + viewBox: `${xMin} ${yMin} ${width} ${height}`, + } + return s('svg', props) + } + + case IMAGE_LAYER: { + const vbox = BBox.toViewBox(node.size) + return s('g', { + transform: `translate(0, ${vbox[3] + 2 * vbox[1]}) scale(1,-1)`, + }) + } + + case IMAGE_SHAPE: { + return renderShape(node) + } + + case IMAGE_PATH: + case IMAGE_REGION: { + return renderPath(node) + } + } + + return s('metadata', [JSON.stringify(node)]) +} diff --git a/packages/renderer/src/render.ts b/packages/renderer/src/render.ts new file mode 100644 index 00000000..c70c3d05 --- /dev/null +++ b/packages/renderer/src/render.ts @@ -0,0 +1,126 @@ +import {s} from 'hastscript' + +import {random as createId} from '@tracespace/xml-id' +import { + BoundingBox, + positionsEqual, + ImageShape, + ImagePath, + ImageRegion, + PathSegment, + Shape, + OutlineShape, + IMAGE_PATH, + CIRCLE, + RECTANGLE, + POLYGON, + OUTLINE, + LAYERED_SHAPE, + LINE, + ARC, +} from '@tracespace/plotter' + +import type {SvgElement} from './types' + +export function renderShape(node: ImageShape): SvgElement { + const {shape} = node + + return shapeToElement(shape) +} + +export function shapeToElement(shape: Shape): SvgElement { + switch (shape.type) { + case CIRCLE: { + const {cx, cy, r} = shape + return s('circle', {cx, cy, r}) + } + + case RECTANGLE: { + const {x, y, xSize: width, ySize: height, r} = shape + return s('rect', {x, y, width, height, rx: r, ry: r}) + } + + case POLYGON: { + const points = shape.points.map(p => p.join(',')).join(' ') + return s('polygon', {points}) + } + + case OUTLINE: { + return s('path', {d: segmentsToPathData(shape.segments)}) + } + + case LAYERED_SHAPE: { + const boundingBox = BoundingBox.fromShape(shape) + const clipIdBase = createId() + const defs: SvgElement[] = [] + let children: SvgElement[] = [] + + for (const [i, layerShape] of shape.shapes.entries()) { + if (layerShape.erase && !BoundingBox.isEmpty(boundingBox)) { + const [bx1, by1, bx2, by2] = boundingBox + const clipId = `${clipIdBase}__${i}` + const boundingPath = `M${bx1} ${by1} H${bx2} V${by2} H${bx1} V${by1}` + + defs.push(s('clipPath', {id: clipId}, [shapeToElement(layerShape)])) + children = [s('g', {clipPath: `url(#${clipId})`}, children)] + } else { + children.push(shapeToElement(layerShape)) + } + } + + if (defs.length > 0) children.unshift(s('defs', defs)) + if (children.length === 1) return children[0] + return s('g', children) + } + + default: { + return s('g') + } + } +} + +export function renderPath(node: ImagePath | ImageRegion): SvgElement { + const pathData = segmentsToPathData(node.segments) + const props = + node.type === IMAGE_PATH ? {strokeWidth: node.width, fill: 'none'} : {} + + return s('path', {...props, d: pathData}) +} + +function segmentsToPathData(segments: PathSegment[]): string { + const pathCommands: string[] = [] + + for (const [i, next] of segments.entries()) { + const previous = segments[i - 1] + const {start, end} = next + + if (!previous || !positionsEqual(previous.end, start)) { + pathCommands.push(`M${start[0]} ${start[1]}`) + } + + if (next.type === LINE) { + pathCommands.push(`L${end[0]} ${end[1]}`) + } else if (next.type === ARC) { + const sweep = next.end[2] - next.start[2] + const absSweep = Math.abs(sweep) + const {center, radius} = next + + // Sweep flag flipped from SVG value because Y-axis is positive-down + const sweepFlag = sweep < 0 ? '0' : '1' + let largeFlag = absSweep <= Math.PI ? '0' : '1' + + // A full circle needs two SVG arcs to draw + if (absSweep === 2 * Math.PI) { + const [mx, my] = [2 * center[0] - end[0], 2 * center[1] - end[1]] + largeFlag = '0' + pathCommands.push(`A${radius} ${radius} 0 0 ${sweepFlag} ${mx} ${my}`) + } + + pathCommands.push( + `A${radius} ${radius} 0 ${largeFlag} ${sweepFlag} ${end[0]} ${end[1]}` + ) + } + } + + return pathCommands.join('') +} diff --git a/packages/renderer/src/types.ts b/packages/renderer/src/types.ts new file mode 100644 index 00000000..a7687804 --- /dev/null +++ b/packages/renderer/src/types.ts @@ -0,0 +1,13 @@ +export type {Element as SvgElement} from 'hast' + +export interface SvgOptions { + colors: Colors +} + +export interface Colors { + copper: string + mask: string + silkscreen: string + paste: string + substrate: string +} diff --git a/packages/renderer/tsconfig.json b/packages/renderer/tsconfig.json new file mode 100644 index 00000000..3eda5c23 --- /dev/null +++ b/packages/renderer/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../config/tsconfig.base.json", + "references": [ + {"path": "../parser"}, + {"path": "../plotter"}, + {"path": "../whats-that-gerber"}, + {"path": "../xml-id"} + ], + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": ["src", "typings"] +} diff --git a/packages/renderer/typings/unist-util-map.d.ts b/packages/renderer/typings/unist-util-map.d.ts new file mode 100644 index 00000000..fe72da09 --- /dev/null +++ b/packages/renderer/typings/unist-util-map.d.ts @@ -0,0 +1,12 @@ +declare module 'unist-util-map' { + import {Node, Data} from 'unist' + + interface MapFunction, OutTree extends Node> { + (node: Tree, index: number | null, parent: Tree | null): OutTree + } + + export function map< + Tree extends Node, + OutTree extends Node = Tree + >(tree: Tree, mapFunction: MapFunction): OutTree +} diff --git a/packages/renderer/vite.config.ts b/packages/renderer/vite.config.ts new file mode 100644 index 00000000..d2e643ec --- /dev/null +++ b/packages/renderer/vite.config.ts @@ -0,0 +1,26 @@ +import {defineConfig} from 'vite' + +import {baseConfig, libraryFilename} from '../../config/vite.config.base' +import pkg from './package.json' + +export default defineConfig({ + ...baseConfig, + build: { + lib: { + entry: 'src/index.ts', + name: 'TracespaceRenderer', + fileName: libraryFilename('tracespace-renderer'), + }, + rollupOptions: { + external: Object.keys(pkg.dependencies), + output: { + globals: { + '@tracespace/parser': 'TracespaceParser', + '@tracespace/plotter': 'TracespacePlotter', + '@tracespace/xml-id': 'TracespaceXmlId', + 'whats-that-gerber': 'WhatsThatGerber', + }, + }, + }, + }, +}) diff --git a/packages/whats-that-gerber/package.json b/packages/whats-that-gerber/package.json index 4a05ad99..fa558326 100644 --- a/packages/whats-that-gerber/package.json +++ b/packages/whats-that-gerber/package.json @@ -16,8 +16,7 @@ "type": "module", "sideEffects": false, "scripts": { - "build": "vite build", - "clean": "rimraf dist" + "build": "vite build" }, "repository": { "type": "git", diff --git a/packages/whats-that-gerber/src/get-matches.ts b/packages/whats-that-gerber/src/get-matches.ts index e21f0cf9..d15e6e96 100644 --- a/packages/whats-that-gerber/src/get-matches.ts +++ b/packages/whats-that-gerber/src/get-matches.ts @@ -4,5 +4,5 @@ import {LayerTestMatch} from './types' export function getMatches(filename: string): LayerTestMatch[] { return matchers .map(m => (m.match.test(filename) ? {...m, filename} : null)) - .filter((m: LayerTestMatch | null): m is LayerTestMatch => Boolean(m)) + .filter((m: LayerTestMatch | null): m is LayerTestMatch => m !== null) } diff --git a/packages/xml-id/package.json b/packages/xml-id/package.json index daaa473d..da1c84c4 100644 --- a/packages/xml-id/package.json +++ b/packages/xml-id/package.json @@ -19,8 +19,7 @@ "type": "module", "sideEffects": false, "scripts": { - "build": "vite build", - "clean": "rimraf dist" + "build": "vite build" }, "repository": { "type": "git", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5536af05..0de4b165 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,17 +10,16 @@ importers: '@tracespace/plotter': workspace:* '@tracespace/www': workspace:* '@tracespace/xml-id': workspace:* - '@types/node': ^17.0.30 - c8: ^7.11.2 - concurrently: ^7.1.0 - prettier: ^2.6.2 - rimraf: ^3.0.2 - testdouble: ^3.16.5 - typescript: ^4.6.4 - vite: ^2.9.6 - vitest: ^0.10.0 + '@types/node': ^18.0.1 + c8: ^7.11.3 + concurrently: ^7.2.2 + prettier: ^2.7.1 + testdouble: ^3.16.6 + typescript: ^4.7.4 + vite: ^2.9.13 + vitest: ^0.17.0 whats-that-gerber: workspace:* - xo: ^0.48.0 + xo: ^0.50.0 dependencies: '@tracespace/cli': link:packages/cli '@tracespace/fixtures': link:packages/fixtures @@ -30,16 +29,15 @@ importers: '@tracespace/xml-id': link:packages/xml-id whats-that-gerber: link:packages/whats-that-gerber devDependencies: - '@types/node': 17.0.31 - c8: 7.11.2 - concurrently: 7.1.0 - prettier: 2.6.2 - rimraf: 3.0.2 - testdouble: 3.16.5 - typescript: 4.6.4 - vite: 2.9.8 - vitest: 0.10.4_c8@7.11.2 - xo: 0.48.0 + '@types/node': 18.0.1 + c8: 7.11.3 + concurrently: 7.2.2 + prettier: 2.7.1 + testdouble: 3.16.6 + typescript: 4.7.4 + vite: 2.9.13 + vitest: 0.17.0_c8@7.11.3 + xo: 0.50.0 packages/cli: specifiers: @@ -96,6 +94,29 @@ importers: '@tracespace/parser': link:../parser '@types/unist': 2.0.6 + packages/renderer: + specifiers: + '@tracespace/parser': workspace:* + '@tracespace/plotter': workspace:* + '@tracespace/xml-id': workspace:* + '@types/hast': ^2.3.4 + '@types/unist': ^2.0.6 + hastscript: ^7.0.2 + unist-util-map: ^3.1.1 + unist-util-visit-parents: ^5.1.0 + whats-that-gerber: workspace:* + dependencies: + '@tracespace/parser': link:../parser + '@tracespace/plotter': link:../plotter + '@tracespace/xml-id': link:../xml-id + '@types/hast': 2.3.4 + whats-that-gerber: link:../whats-that-gerber + devDependencies: + '@types/unist': 2.0.6 + hastscript: 7.0.2 + unist-util-map: 3.1.1 + unist-util-visit-parents: 5.1.0 + packages/whats-that-gerber: specifiers: {} @@ -112,12 +133,18 @@ importers: '@preact/preset-vite': ^2.2.0 '@tracespace/parser': workspace:* '@tracespace/plotter': workspace:* + '@tracespace/renderer': workspace:* '@types/stringify-object': 3.3.0 babel-plugin-transform-hook-names: ^1.0.2 express: ^4.18.0 + hast-util-to-html: ^8.0.3 + hastscript: ^7.0.2 + moo: ^0.5.1 preact: ^10.7.1 preact-render-to-string: ^5.1.21 stringify-object: 3.3.0 + unist-util-map: ^3.1.1 + unist-util-visit-parents: ^5.1.0 vite-plugin-ssr: ^0.3.64 vite-plugin-windicss: ^1.8.4 windicss: ^3.5.1 @@ -126,8 +153,14 @@ importers: '@fontsource/open-sans': 4.5.8 '@tracespace/parser': link:../packages/parser '@tracespace/plotter': link:../packages/plotter + '@tracespace/renderer': link:../packages/renderer + hast-util-to-html: 8.0.3 + hastscript: 7.0.2 + moo: 0.5.1 preact: 10.7.1 stringify-object: 3.3.0 + unist-util-map: 3.1.1 + unist-util-visit-parents: 5.1.0 windicss: 3.5.1 devDependencies: '@babel/core': 7.17.9 @@ -163,6 +196,13 @@ packages: '@babel/highlight': 7.17.9 dev: true + /@babel/code-frame/7.18.6: + resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.18.6 + dev: true + /@babel/compat-data/7.17.7: resolution: {integrity: sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==} engines: {node: '>=6.9.0'} @@ -289,6 +329,11 @@ packages: engines: {node: '>=6.9.0'} dev: true + /@babel/helper-validator-identifier/7.18.6: + resolution: {integrity: sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-validator-option/7.16.7: resolution: {integrity: sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==} engines: {node: '>=6.9.0'} @@ -314,10 +359,21 @@ packages: js-tokens: 4.0.0 dev: true + /@babel/highlight/7.18.6: + resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.18.6 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + /@babel/parser/7.17.9: resolution: {integrity: sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg==} engines: {node: '>=6.0.0'} hasBin: true + dependencies: + '@babel/types': 7.17.0 dev: true /@babel/plugin-syntax-jsx/7.16.7_@babel+core@7.17.9: @@ -401,14 +457,14 @@ packages: resolution: {integrity: sha512-ayIUIOFM98wqgMnCquDlzENRBLmJ82hsKnosaaG2U4+ijUVwi1E2LzD0TwhAC8G7vBqp0BbT172mfTXBVMIw5A==} dev: true - /@eslint/eslintrc/1.2.2: - resolution: {integrity: sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==} + /@eslint/eslintrc/1.3.0: + resolution: {integrity: sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 debug: 4.3.4 - espree: 9.3.1 - globals: 13.13.0 + espree: 9.3.2 + globals: 13.15.0 ignore: 5.2.0 import-fresh: 3.3.0 js-yaml: 4.1.0 @@ -447,10 +503,26 @@ packages: engines: {node: '>=6.0.0'} dev: true + /@jridgewell/resolve-uri/3.0.8: + resolution: {integrity: sha512-YK5G9LaddzGbcucK4c8h5tWFmMPBvRZ/uyWmN1/SbBdIvqGUdWGkJ5BAaccgs6XbzVLsqbPJrBSFwKv3kT9i7w==} + engines: {node: '>=6.0.0'} + dev: true + /@jridgewell/sourcemap-codec/1.4.11: resolution: {integrity: sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==} dev: true + /@jridgewell/sourcemap-codec/1.4.14: + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + dev: true + + /@jridgewell/trace-mapping/0.3.14: + resolution: {integrity: sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==} + dependencies: + '@jridgewell/resolve-uri': 3.0.8 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + /@jridgewell/trace-mapping/0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} dependencies: @@ -614,7 +686,7 @@ packages: /@types/eslint/7.29.0: resolution: {integrity: sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==} dependencies: - '@types/estree': 0.0.51 + '@types/estree': 0.0.52 '@types/json-schema': 7.0.11 dev: true @@ -632,11 +704,14 @@ packages: resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} dev: true + /@types/estree/0.0.52: + resolution: {integrity: sha512-BZWrtCU0bMVAIliIV+HJO1f1PR41M7NKjfxrFJwwhKI1KwhwOxYw1SXg9ao+CIMt774nFuGiG6eU+udtbEI9oQ==} + dev: true + /@types/hast/2.3.4: resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==} dependencies: '@types/unist': 2.0.6 - dev: true /@types/istanbul-lib-coverage/2.0.4: resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} @@ -647,7 +722,7 @@ packages: dev: true /@types/json5/0.0.29: - resolution: {integrity: sha1-7ihweulOEdK4J7y+UnC86n8+ce4=} + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true /@types/mdast/3.0.10: @@ -679,8 +754,8 @@ packages: resolution: {integrity: sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==} dev: false - /@types/node/17.0.31: - resolution: {integrity: sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==} + /@types/node/18.0.1: + resolution: {integrity: sha512-CmR8+Tsy95hhwtZBKJBs0/FFq4XX7sDZHlGGf+0q+BRZfMbOTkzkj0AFAuTyXbObDIoanaBBW0+KEW+m3N16Wg==} dev: true /@types/normalize-package-data/2.4.1: @@ -708,8 +783,8 @@ packages: '@types/yargs-parser': 21.0.0 dev: false - /@typescript-eslint/eslint-plugin/5.22.0_lnjlwhtxjffjmj5o7dnwvwyqxq: - resolution: {integrity: sha512-YCiy5PUzpAeOPGQ7VSGDEY2NeYUV1B0swde2e0HzokRsHBYjSdF6DZ51OuRZxVPHx0032lXGLvOMls91D8FXlg==} + /@typescript-eslint/eslint-plugin/5.30.5_6zdoc3rn4mpiddqwhppni2mnnm: + resolution: {integrity: sha512-lftkqRoBvc28VFXEoRgyZuztyVUQ04JvUnATSPtIRFAccbXTWL6DEtXGYMcbg998kXw1NLUJm7rTQ9eUt+q6Ig==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: '@typescript-eslint/parser': ^5.0.0 @@ -719,24 +794,24 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.22.0_t725usgvqspm5woeqpaxbfp2qu - '@typescript-eslint/scope-manager': 5.22.0 - '@typescript-eslint/type-utils': 5.22.0_t725usgvqspm5woeqpaxbfp2qu - '@typescript-eslint/utils': 5.22.0_t725usgvqspm5woeqpaxbfp2qu + '@typescript-eslint/parser': 5.30.5_4x5o4skxv6sl53vpwefgt23khm + '@typescript-eslint/scope-manager': 5.30.5 + '@typescript-eslint/type-utils': 5.30.5_4x5o4skxv6sl53vpwefgt23khm + '@typescript-eslint/utils': 5.30.5_4x5o4skxv6sl53vpwefgt23khm debug: 4.3.4 - eslint: 8.14.0 + eslint: 8.19.0 functional-red-black-tree: 1.0.1 ignore: 5.2.0 regexpp: 3.2.0 semver: 7.3.7 - tsutils: 3.21.0_typescript@4.6.4 - typescript: 4.6.4 + tsutils: 3.21.0_typescript@4.7.4 + typescript: 4.7.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser/5.22.0_t725usgvqspm5woeqpaxbfp2qu: - resolution: {integrity: sha512-piwC4krUpRDqPaPbFaycN70KCP87+PC5WZmrWs+DlVOxxmF+zI6b6hETv7Quy4s9wbkV16ikMeZgXsvzwI3icQ==} + /@typescript-eslint/parser/5.30.5_4x5o4skxv6sl53vpwefgt23khm: + resolution: {integrity: sha512-zj251pcPXI8GO9NDKWWmygP6+UjwWmrdf9qMW/L/uQJBM/0XbU2inxe5io/234y/RCvwpKEYjZ6c1YrXERkK4Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -745,26 +820,26 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 5.22.0 - '@typescript-eslint/types': 5.22.0 - '@typescript-eslint/typescript-estree': 5.22.0_typescript@4.6.4 + '@typescript-eslint/scope-manager': 5.30.5 + '@typescript-eslint/types': 5.30.5 + '@typescript-eslint/typescript-estree': 5.30.5_typescript@4.7.4 debug: 4.3.4 - eslint: 8.14.0 - typescript: 4.6.4 + eslint: 8.19.0 + typescript: 4.7.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/scope-manager/5.22.0: - resolution: {integrity: sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==} + /@typescript-eslint/scope-manager/5.30.5: + resolution: {integrity: sha512-NJ6F+YHHFT/30isRe2UTmIGGAiXKckCyMnIV58cE3JkHmaD6e5zyEYm5hBDv0Wbin+IC0T1FWJpD3YqHUG/Ydg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': 5.22.0 - '@typescript-eslint/visitor-keys': 5.22.0 + '@typescript-eslint/types': 5.30.5 + '@typescript-eslint/visitor-keys': 5.30.5 dev: true - /@typescript-eslint/type-utils/5.22.0_t725usgvqspm5woeqpaxbfp2qu: - resolution: {integrity: sha512-iqfLZIsZhK2OEJ4cQ01xOq3NaCuG5FQRKyHicA3xhZxMgaxQazLUHbH/B2k9y5i7l3+o+B5ND9Mf1AWETeMISA==} + /@typescript-eslint/type-utils/5.30.5_4x5o4skxv6sl53vpwefgt23khm: + resolution: {integrity: sha512-k9+ejlv1GgwN1nN7XjVtyCgE0BTzhzT1YsQF0rv4Vfj2U9xnslBgMYYvcEYAFVdvhuEscELJsB7lDkN7WusErw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '*' @@ -773,22 +848,22 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/utils': 5.22.0_t725usgvqspm5woeqpaxbfp2qu + '@typescript-eslint/utils': 5.30.5_4x5o4skxv6sl53vpwefgt23khm debug: 4.3.4 - eslint: 8.14.0 - tsutils: 3.21.0_typescript@4.6.4 - typescript: 4.6.4 + eslint: 8.19.0 + tsutils: 3.21.0_typescript@4.7.4 + typescript: 4.7.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/types/5.22.0: - resolution: {integrity: sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==} + /@typescript-eslint/types/5.30.5: + resolution: {integrity: sha512-kZ80w/M2AvsbRvOr3PjaNh6qEW1LFqs2pLdo2s5R38B2HYXG8Z0PP48/4+j1QHJFL3ssHIbJ4odPRS8PlHrFfw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/typescript-estree/5.22.0_typescript@4.6.4: - resolution: {integrity: sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==} + /@typescript-eslint/typescript-estree/5.30.5_typescript@4.7.4: + resolution: {integrity: sha512-qGTc7QZC801kbYjAr4AgdOfnokpwStqyhSbiQvqGBLixniAKyH+ib2qXIVo4P9NgGzwyfD9I0nlJN7D91E1VpQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: typescript: '*' @@ -796,41 +871,41 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 5.22.0 - '@typescript-eslint/visitor-keys': 5.22.0 + '@typescript-eslint/types': 5.30.5 + '@typescript-eslint/visitor-keys': 5.30.5 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.7 - tsutils: 3.21.0_typescript@4.6.4 - typescript: 4.6.4 + tsutils: 3.21.0_typescript@4.7.4 + typescript: 4.7.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils/5.22.0_t725usgvqspm5woeqpaxbfp2qu: - resolution: {integrity: sha512-HodsGb037iobrWSUMS7QH6Hl1kppikjA1ELiJlNSTYf/UdMEwzgj0WIp+lBNb6WZ3zTwb0tEz51j0Wee3iJ3wQ==} + /@typescript-eslint/utils/5.30.5_4x5o4skxv6sl53vpwefgt23khm: + resolution: {integrity: sha512-o4SSUH9IkuA7AYIfAvatldovurqTAHrfzPApOZvdUq01hHojZojCFXx06D/aFpKCgWbMPRdJBWAC3sWp3itwTA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: '@types/json-schema': 7.0.11 - '@typescript-eslint/scope-manager': 5.22.0 - '@typescript-eslint/types': 5.22.0 - '@typescript-eslint/typescript-estree': 5.22.0_typescript@4.6.4 - eslint: 8.14.0 + '@typescript-eslint/scope-manager': 5.30.5 + '@typescript-eslint/types': 5.30.5 + '@typescript-eslint/typescript-estree': 5.30.5_typescript@4.7.4 + eslint: 8.19.0 eslint-scope: 5.1.1 - eslint-utils: 3.0.0_eslint@8.14.0 + eslint-utils: 3.0.0_eslint@8.19.0 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/visitor-keys/5.22.0: - resolution: {integrity: sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==} + /@typescript-eslint/visitor-keys/5.30.5: + resolution: {integrity: sha512-D+xtGo9HUMELzWIUqcQc0p2PO4NyvTrgIOK/VnSH083+8sq0tiLozNRKuLarwHYGRuA6TVBQSuuLwJUDWd3aaA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': 5.22.0 + '@typescript-eslint/types': 5.30.5 eslint-visitor-keys: 3.3.0 dev: true @@ -874,12 +949,26 @@ packages: acorn: 8.7.0 dev: true + /acorn-jsx/5.3.2_acorn@8.7.1: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.7.1 + dev: true + /acorn/8.7.0: resolution: {integrity: sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==} engines: {node: '>=0.4.0'} hasBin: true dev: true + /acorn/8.7.1: + resolution: {integrity: sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /ajv/6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -924,7 +1013,7 @@ packages: dev: true /array-find/1.0.0: - resolution: {integrity: sha1-bI4obRHtdoMn+OYuzuhzU8o+eLg=} + resolution: {integrity: sha512-kO/vVCacW9mnpn3WPWbTVlEnOabK2L7LWi2HViURtCM46y1zb6I8UMjx4LgbiqadTgHnLInUronwn3ampNTJtQ==} dev: true /array-flatten/1.1.1: @@ -937,8 +1026,8 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.0 - get-intrinsic: 1.1.1 + es-abstract: 1.20.1 + get-intrinsic: 1.1.2 is-string: 1.0.7 dev: true @@ -952,12 +1041,12 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.0 + es-abstract: 1.20.1 es-shim-unscopables: 1.0.0 dev: true /arrify/1.0.1: - resolution: {integrity: sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=} + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} dev: true @@ -1007,6 +1096,8 @@ packages: raw-body: 2.5.1 type-is: 1.6.18 unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color dev: true /brace-expansion/1.1.11: @@ -1034,18 +1125,30 @@ packages: picocolors: 1.0.0 dev: true - /builtin-modules/3.2.0: - resolution: {integrity: sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==} + /builtin-modules/3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} dev: true + /builtins/4.1.0: + resolution: {integrity: sha512-1bPRZQtmKaO6h7qV1YHXNtr6nCK28k0Zo95KM4dXfILcZZwoHJBN1m3lfLv9LPkcOZlrSr+J1bzMaZFO98Yq0w==} + dependencies: + semver: 7.3.7 + dev: true + + /builtins/5.0.1: + resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} + dependencies: + semver: 7.3.7 + dev: true + /bytes/3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} dev: true - /c8/7.11.2: - resolution: {integrity: sha512-6ahJSrhS6TqSghHm+HnWt/8Y2+z0hM/FQyB1ybKhAR30+NYL9CTQ1uwHxuWw6U7BHlHv6wvhgOrH81I+lfCkxg==} + /c8/7.11.3: + resolution: {integrity: sha512-6YBmsaNmqRm9OS3ZbIiL2EZgi1+Xc4O24jL3vMYGE6idixYuGdy76rIfIdltSKDj9DpLNrcXSonUTR1miBD0wA==} engines: {node: '>=10.12.0'} hasBin: true dependencies: @@ -1058,7 +1161,7 @@ packages: istanbul-reports: 3.1.4 rimraf: 3.0.2 test-exclude: 6.0.0 - v8-to-istanbul: 9.0.0 + v8-to-istanbul: 9.0.1 yargs: 16.2.0 yargs-parser: 20.2.9 dev: true @@ -1072,25 +1175,25 @@ packages: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: function-bind: 1.1.1 - get-intrinsic: 1.1.1 + get-intrinsic: 1.1.2 dev: true /caller-callsite/2.0.0: - resolution: {integrity: sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=} + resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} engines: {node: '>=4'} dependencies: callsites: 2.0.0 dev: false /caller-path/2.0.0: - resolution: {integrity: sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=} + resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==} engines: {node: '>=4'} dependencies: caller-callsite: 2.0.0 dev: false /callsites/2.0.0: - resolution: {integrity: sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=} + resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==} engines: {node: '>=4'} dev: false @@ -1125,7 +1228,6 @@ packages: /ccount/2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - dev: true /chai/4.3.6: resolution: {integrity: sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==} @@ -1159,11 +1261,9 @@ packages: /character-entities-html4/2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - dev: true /character-entities-legacy/3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - dev: true /character-entities/2.0.1: resolution: {integrity: sha512-OzmutCf2Kmc+6DrFrrPS8/tDh2+DpnrfzdICHWhcVC9eOd0N1PXmQEE1a8iM4IziIAG+8tmTq3K+oo0ubH6RRQ==} @@ -1174,15 +1274,15 @@ packages: dev: true /check-error/1.0.2: - resolution: {integrity: sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=} + resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} dev: true - /ci-info/3.3.0: - resolution: {integrity: sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==} + /ci-info/3.3.2: + resolution: {integrity: sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==} dev: true /clean-regexp/1.0.0: - resolution: {integrity: sha1-jffHquUf02h06PjQW5GAvBGj/tc=} + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} dependencies: escape-string-regexp: 1.0.5 @@ -1217,7 +1317,8 @@ packages: color-name: 1.1.4 /color-name/1.1.3: - resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=} + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true /color-name/1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -1225,39 +1326,39 @@ packages: /color-string/1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} dependencies: - color-name: 1.1.3 + color-name: 1.1.4 simple-swizzle: 0.2.2 dev: false /comma-separated-tokens/2.0.2: resolution: {integrity: sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==} - dev: true /common-prefix/1.1.0: - resolution: {integrity: sha1-46Xqf6+u/H64TnYFI+GvuYX5DwA=} + resolution: {integrity: sha512-9HAWgTP6U7K4G94u3J/oETIczTkzqZGn/cVFHDKVh/iFtZPBxuJ/qT+8Q5Np1XSXMZsRgnrpes7SrWOTJxFtaw==} dev: false /commondir/1.0.1: - resolution: {integrity: sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=} + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} dev: true /concat-map/0.0.1: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} dev: true - /concurrently/7.1.0: - resolution: {integrity: sha512-Bz0tMlYKZRUDqJlNiF/OImojMB9ruKUz6GCfmhFnSapXgPe+3xzY4byqoKG9tUZ7L2PGEUjfLPOLfIX3labnmw==} + /concurrently/7.2.2: + resolution: {integrity: sha512-DcQkI0ruil5BA/g7Xy3EWySGrFJovF5RYAYxwGvv9Jf9q9B1v3jPFP2tl6axExNf1qgF30kjoNYrangZ0ey4Aw==} engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0} hasBin: true dependencies: chalk: 4.1.2 date-fns: 2.28.0 lodash: 4.17.21 - rxjs: 6.6.7 + rxjs: 7.5.5 + shell-quote: 1.7.3 spawn-command: 0.0.2-1 supports-color: 8.1.1 tree-kill: 1.2.2 - yargs: 16.2.0 + yargs: 17.5.1 dev: true /confusing-browser-globals/1.0.11: @@ -1328,12 +1429,22 @@ packages: /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true dependencies: ms: 2.0.0 dev: true /debug/3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true dependencies: ms: 2.1.3 dev: true @@ -1350,7 +1461,7 @@ packages: ms: 2.1.2 /decamelize-keys/1.1.0: - resolution: {integrity: sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=} + resolution: {integrity: sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==} engines: {node: '>=0.10.0'} dependencies: decamelize: 1.2.0 @@ -1358,7 +1469,7 @@ packages: dev: true /decamelize/1.2.0: - resolution: {integrity: sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=} + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} /decamelize/5.0.1: @@ -1465,14 +1576,14 @@ packages: dev: true /enhance-visitors/1.0.0: - resolution: {integrity: sha1-qpRdBdpGVnKh69OP7i7T2oUY6Vo=} + resolution: {integrity: sha512-+29eJLiUixTEDRaZ35Vu8jP3gPLNcQQkQkOQjLp2X+6cZGGPDD/uasbFzvLsJKnGZnvmyZ0srxudwOtskHeIDA==} engines: {node: '>=4.0.0'} dependencies: lodash: 4.17.21 dev: true /enhanced-resolve/0.9.1: - resolution: {integrity: sha1-TW5omzcl+GCQknzMhs2fFjW4ni4=} + resolution: {integrity: sha512-kxpoMgrdtkXZ5h0SeraBS1iRntpTpQ3R8ussdb38+UAFnMGX5DDyJXePm+OCHOcoXvHDw7mc2erbJBpDnl7TPw==} engines: {node: '>=0.6'} dependencies: graceful-fs: 4.2.10 @@ -1490,15 +1601,15 @@ packages: dependencies: is-arrayish: 0.2.1 - /es-abstract/1.20.0: - resolution: {integrity: sha512-URbD8tgRthKD3YcC39vbvSDrX23upXnPcnGAjQfgxXF5ID75YcENawc9ZX/9iTP9ptUyfCLIxTTuMYoRfiOVKA==} + /es-abstract/1.20.1: + resolution: {integrity: sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 es-to-primitive: 1.2.1 function-bind: 1.1.1 function.prototype.name: 1.1.5 - get-intrinsic: 1.1.1 + get-intrinsic: 1.1.2 get-symbol-description: 1.0.0 has: 1.0.3 has-property-descriptors: 1.0.0 @@ -1510,7 +1621,7 @@ packages: is-shared-array-buffer: 1.0.2 is-string: 1.0.7 is-weakref: 1.0.2 - object-inspect: 1.12.0 + object-inspect: 1.12.2 object-keys: 1.1.1 object.assign: 4.1.2 regexp.prototype.flags: 1.4.3 @@ -1538,8 +1649,8 @@ packages: is-symbol: 1.0.4 dev: true - /esbuild-android-64/0.14.38: - resolution: {integrity: sha512-aRFxR3scRKkbmNuGAK+Gee3+yFxkTJO/cx83Dkyzo4CnQl/2zVSurtG6+G86EQIZ+w+VYngVyK7P3HyTBKu3nw==} + /esbuild-android-64/0.14.47: + resolution: {integrity: sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g==} engines: {node: '>=12'} cpu: [x64] os: [android] @@ -1547,8 +1658,8 @@ packages: dev: true optional: true - /esbuild-android-arm64/0.14.38: - resolution: {integrity: sha512-L2NgQRWuHFI89IIZIlpAcINy9FvBk6xFVZ7xGdOwIm8VyhX1vNCEqUJO3DPSSy945Gzdg98cxtNt8Grv1CsyhA==} + /esbuild-android-arm64/0.14.47: + resolution: {integrity: sha512-OkwOjj7ts4lBp/TL6hdd8HftIzOy/pdtbrNA4+0oVWgGG64HrdVzAF5gxtJufAPOsEjkyh1oIYvKAUinKKQRSQ==} engines: {node: '>=12'} cpu: [arm64] os: [android] @@ -1556,8 +1667,8 @@ packages: dev: true optional: true - /esbuild-darwin-64/0.14.38: - resolution: {integrity: sha512-5JJvgXkX87Pd1Og0u/NJuO7TSqAikAcQQ74gyJ87bqWRVeouky84ICoV4sN6VV53aTW+NE87qLdGY4QA2S7KNA==} + /esbuild-darwin-64/0.14.47: + resolution: {integrity: sha512-R6oaW0y5/u6Eccti/TS6c/2c1xYTb1izwK3gajJwi4vIfNs1s8B1dQzI1UiC9T61YovOQVuePDcfqHLT3mUZJA==} engines: {node: '>=12'} cpu: [x64] os: [darwin] @@ -1565,8 +1676,8 @@ packages: dev: true optional: true - /esbuild-darwin-arm64/0.14.38: - resolution: {integrity: sha512-eqF+OejMI3mC5Dlo9Kdq/Ilbki9sQBw3QlHW3wjLmsLh+quNfHmGMp3Ly1eWm981iGBMdbtSS9+LRvR2T8B3eQ==} + /esbuild-darwin-arm64/0.14.47: + resolution: {integrity: sha512-seCmearlQyvdvM/noz1L9+qblC5vcBrhUaOoLEDDoLInF/VQ9IkobGiLlyTPYP5dW1YD4LXhtBgOyevoIHGGnw==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] @@ -1574,8 +1685,8 @@ packages: dev: true optional: true - /esbuild-freebsd-64/0.14.38: - resolution: {integrity: sha512-epnPbhZUt93xV5cgeY36ZxPXDsQeO55DppzsIgWM8vgiG/Rz+qYDLmh5ts3e+Ln1wA9dQ+nZmVHw+RjaW3I5Ig==} + /esbuild-freebsd-64/0.14.47: + resolution: {integrity: sha512-ZH8K2Q8/Ux5kXXvQMDsJcxvkIwut69KVrYQhza/ptkW50DC089bCVrJZZ3sKzIoOx+YPTrmsZvqeZERjyYrlvQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] @@ -1583,8 +1694,8 @@ packages: dev: true optional: true - /esbuild-freebsd-arm64/0.14.38: - resolution: {integrity: sha512-/9icXUYJWherhk+y5fjPI5yNUdFPtXHQlwP7/K/zg8t8lQdHVj20SqU9/udQmeUo5pDFHMYzcEFfJqgOVeKNNQ==} + /esbuild-freebsd-arm64/0.14.47: + resolution: {integrity: sha512-ZJMQAJQsIOhn3XTm7MPQfCzEu5b9STNC+s90zMWe2afy9EwnHV7Ov7ohEMv2lyWlc2pjqLW8QJnz2r0KZmeAEQ==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] @@ -1592,8 +1703,8 @@ packages: dev: true optional: true - /esbuild-linux-32/0.14.38: - resolution: {integrity: sha512-QfgfeNHRFvr2XeHFzP8kOZVnal3QvST3A0cgq32ZrHjSMFTdgXhMhmWdKzRXP/PKcfv3e2OW9tT9PpcjNvaq6g==} + /esbuild-linux-32/0.14.47: + resolution: {integrity: sha512-FxZOCKoEDPRYvq300lsWCTv1kcHgiiZfNrPtEhFAiqD7QZaXrad8LxyJ8fXGcWzIFzRiYZVtB3ttvITBvAFhKw==} engines: {node: '>=12'} cpu: [ia32] os: [linux] @@ -1601,8 +1712,8 @@ packages: dev: true optional: true - /esbuild-linux-64/0.14.38: - resolution: {integrity: sha512-uuZHNmqcs+Bj1qiW9k/HZU3FtIHmYiuxZ/6Aa+/KHb/pFKr7R3aVqvxlAudYI9Fw3St0VCPfv7QBpUITSmBR1Q==} + /esbuild-linux-64/0.14.47: + resolution: {integrity: sha512-nFNOk9vWVfvWYF9YNYksZptgQAdstnDCMtR6m42l5Wfugbzu11VpMCY9XrD4yFxvPo9zmzcoUL/88y0lfJZJJw==} engines: {node: '>=12'} cpu: [x64] os: [linux] @@ -1610,8 +1721,8 @@ packages: dev: true optional: true - /esbuild-linux-arm/0.14.38: - resolution: {integrity: sha512-FiFvQe8J3VKTDXG01JbvoVRXQ0x6UZwyrU4IaLBZeq39Bsbatd94Fuc3F1RGqPF5RbIWW7RvkVQjn79ejzysnA==} + /esbuild-linux-arm/0.14.47: + resolution: {integrity: sha512-ZGE1Bqg/gPRXrBpgpvH81tQHpiaGxa8c9Rx/XOylkIl2ypLuOcawXEAo8ls+5DFCcRGt/o3sV+PzpAFZobOsmA==} engines: {node: '>=12'} cpu: [arm] os: [linux] @@ -1619,8 +1730,8 @@ packages: dev: true optional: true - /esbuild-linux-arm64/0.14.38: - resolution: {integrity: sha512-HlMGZTEsBrXrivr64eZ/EO0NQM8H8DuSENRok9d+Jtvq8hOLzrxfsAT9U94K3KOGk2XgCmkaI2KD8hX7F97lvA==} + /esbuild-linux-arm64/0.14.47: + resolution: {integrity: sha512-ywfme6HVrhWcevzmsufjd4iT3PxTfCX9HOdxA7Hd+/ZM23Y9nXeb+vG6AyA6jgq/JovkcqRHcL9XwRNpWG6XRw==} engines: {node: '>=12'} cpu: [arm64] os: [linux] @@ -1628,8 +1739,8 @@ packages: dev: true optional: true - /esbuild-linux-mips64le/0.14.38: - resolution: {integrity: sha512-qd1dLf2v7QBiI5wwfil9j0HG/5YMFBAmMVmdeokbNAMbcg49p25t6IlJFXAeLzogv1AvgaXRXvgFNhScYEUXGQ==} + /esbuild-linux-mips64le/0.14.47: + resolution: {integrity: sha512-mg3D8YndZ1LvUiEdDYR3OsmeyAew4MA/dvaEJxvyygahWmpv1SlEEnhEZlhPokjsUMfRagzsEF/d/2XF+kTQGg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] @@ -1637,8 +1748,8 @@ packages: dev: true optional: true - /esbuild-linux-ppc64le/0.14.38: - resolution: {integrity: sha512-mnbEm7o69gTl60jSuK+nn+pRsRHGtDPfzhrqEUXyCl7CTOCLtWN2bhK8bgsdp6J/2NyS/wHBjs1x8aBWwP2X9Q==} + /esbuild-linux-ppc64le/0.14.47: + resolution: {integrity: sha512-WER+f3+szmnZiWoK6AsrTKGoJoErG2LlauSmk73LEZFQ/iWC+KhhDsOkn1xBUpzXWsxN9THmQFltLoaFEH8F8w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] @@ -1646,8 +1757,8 @@ packages: dev: true optional: true - /esbuild-linux-riscv64/0.14.38: - resolution: {integrity: sha512-+p6YKYbuV72uikChRk14FSyNJZ4WfYkffj6Af0/Tw63/6TJX6TnIKE+6D3xtEc7DeDth1fjUOEqm+ApKFXbbVQ==} + /esbuild-linux-riscv64/0.14.47: + resolution: {integrity: sha512-1fI6bP3A3rvI9BsaaXbMoaOjLE3lVkJtLxsgLHqlBhLlBVY7UqffWBvkrX/9zfPhhVMd9ZRFiaqXnB1T7BsL2g==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] @@ -1655,8 +1766,8 @@ packages: dev: true optional: true - /esbuild-linux-s390x/0.14.38: - resolution: {integrity: sha512-0zUsiDkGJiMHxBQ7JDU8jbaanUY975CdOW1YDrurjrM0vWHfjv9tLQsW9GSyEb/heSK1L5gaweRjzfUVBFoybQ==} + /esbuild-linux-s390x/0.14.47: + resolution: {integrity: sha512-eZrWzy0xFAhki1CWRGnhsHVz7IlSKX6yT2tj2Eg8lhAwlRE5E96Hsb0M1mPSE1dHGpt1QVwwVivXIAacF/G6mw==} engines: {node: '>=12'} cpu: [s390x] os: [linux] @@ -1664,8 +1775,8 @@ packages: dev: true optional: true - /esbuild-netbsd-64/0.14.38: - resolution: {integrity: sha512-cljBAApVwkpnJZfnRVThpRBGzCi+a+V9Ofb1fVkKhtrPLDYlHLrSYGtmnoTVWDQdU516qYI8+wOgcGZ4XIZh0Q==} + /esbuild-netbsd-64/0.14.47: + resolution: {integrity: sha512-Qjdjr+KQQVH5Q2Q1r6HBYswFTToPpss3gqCiSw2Fpq/ua8+eXSQyAMG+UvULPqXceOwpnPo4smyZyHdlkcPppQ==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] @@ -1673,8 +1784,8 @@ packages: dev: true optional: true - /esbuild-openbsd-64/0.14.38: - resolution: {integrity: sha512-CDswYr2PWPGEPpLDUO50mL3WO/07EMjnZDNKpmaxUPsrW+kVM3LoAqr/CE8UbzugpEiflYqJsGPLirThRB18IQ==} + /esbuild-openbsd-64/0.14.47: + resolution: {integrity: sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] @@ -1682,8 +1793,8 @@ packages: dev: true optional: true - /esbuild-sunos-64/0.14.38: - resolution: {integrity: sha512-2mfIoYW58gKcC3bck0j7lD3RZkqYA7MmujFYmSn9l6TiIcAMpuEvqksO+ntBgbLep/eyjpgdplF7b+4T9VJGOA==} + /esbuild-sunos-64/0.14.47: + resolution: {integrity: sha512-uOeSgLUwukLioAJOiGYm3kNl+1wJjgJA8R671GYgcPgCx7QR73zfvYqXFFcIO93/nBdIbt5hd8RItqbbf3HtAQ==} engines: {node: '>=12'} cpu: [x64] os: [sunos] @@ -1691,8 +1802,8 @@ packages: dev: true optional: true - /esbuild-windows-32/0.14.38: - resolution: {integrity: sha512-L2BmEeFZATAvU+FJzJiRLFUP+d9RHN+QXpgaOrs2klshoAm1AE6Us4X6fS9k33Uy5SzScn2TpcgecbqJza1Hjw==} + /esbuild-windows-32/0.14.47: + resolution: {integrity: sha512-H0fWsLTp2WBfKLBgwYT4OTfFly4Im/8B5f3ojDv1Kx//kiubVY0IQunP2Koc/fr/0wI7hj3IiBDbSrmKlrNgLQ==} engines: {node: '>=12'} cpu: [ia32] os: [win32] @@ -1700,8 +1811,8 @@ packages: dev: true optional: true - /esbuild-windows-64/0.14.38: - resolution: {integrity: sha512-Khy4wVmebnzue8aeSXLC+6clo/hRYeNIm0DyikoEqX+3w3rcvrhzpoix0S+MF9vzh6JFskkIGD7Zx47ODJNyCw==} + /esbuild-windows-64/0.14.47: + resolution: {integrity: sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ==} engines: {node: '>=12'} cpu: [x64] os: [win32] @@ -1709,8 +1820,8 @@ packages: dev: true optional: true - /esbuild-windows-arm64/0.14.38: - resolution: {integrity: sha512-k3FGCNmHBkqdJXuJszdWciAH77PukEyDsdIryEHn9cKLQFxzhT39dSumeTuggaQcXY57UlmLGIkklWZo2qzHpw==} + /esbuild-windows-arm64/0.14.47: + resolution: {integrity: sha512-HFSW2lnp62fl86/qPQlqw6asIwCnEsEoNIL1h2uVMgakddf+vUuMcCbtUY1i8sst7KkgHrVKCJQB33YhhOweCQ==} engines: {node: '>=12'} cpu: [arm64] os: [win32] @@ -1718,32 +1829,32 @@ packages: dev: true optional: true - /esbuild/0.14.38: - resolution: {integrity: sha512-12fzJ0fsm7gVZX1YQ1InkOE5f9Tl7cgf6JPYXRJtPIoE0zkWAbHdPHVPPaLi9tYAcEBqheGzqLn/3RdTOyBfcA==} + /esbuild/0.14.47: + resolution: {integrity: sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA==} engines: {node: '>=12'} hasBin: true requiresBuild: true optionalDependencies: - esbuild-android-64: 0.14.38 - esbuild-android-arm64: 0.14.38 - esbuild-darwin-64: 0.14.38 - esbuild-darwin-arm64: 0.14.38 - esbuild-freebsd-64: 0.14.38 - esbuild-freebsd-arm64: 0.14.38 - esbuild-linux-32: 0.14.38 - esbuild-linux-64: 0.14.38 - esbuild-linux-arm: 0.14.38 - esbuild-linux-arm64: 0.14.38 - esbuild-linux-mips64le: 0.14.38 - esbuild-linux-ppc64le: 0.14.38 - esbuild-linux-riscv64: 0.14.38 - esbuild-linux-s390x: 0.14.38 - esbuild-netbsd-64: 0.14.38 - esbuild-openbsd-64: 0.14.38 - esbuild-sunos-64: 0.14.38 - esbuild-windows-32: 0.14.38 - esbuild-windows-64: 0.14.38 - esbuild-windows-arm64: 0.14.38 + esbuild-android-64: 0.14.47 + esbuild-android-arm64: 0.14.47 + esbuild-darwin-64: 0.14.47 + esbuild-darwin-arm64: 0.14.47 + esbuild-freebsd-64: 0.14.47 + esbuild-freebsd-arm64: 0.14.47 + esbuild-linux-32: 0.14.47 + esbuild-linux-64: 0.14.47 + esbuild-linux-arm: 0.14.47 + esbuild-linux-arm64: 0.14.47 + esbuild-linux-mips64le: 0.14.47 + esbuild-linux-ppc64le: 0.14.47 + esbuild-linux-riscv64: 0.14.47 + esbuild-linux-s390x: 0.14.47 + esbuild-netbsd-64: 0.14.47 + esbuild-openbsd-64: 0.14.47 + esbuild-sunos-64: 0.14.47 + esbuild-windows-32: 0.14.47 + esbuild-windows-64: 0.14.47 + esbuild-windows-arm64: 0.14.47 dev: true /escalade/3.1.1: @@ -1755,7 +1866,7 @@ packages: resolution: {integrity: sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=} /escape-string-regexp/1.0.5: - resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=} + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} dev: true @@ -1764,36 +1875,36 @@ packages: engines: {node: '>=10'} dev: true - /eslint-config-prettier/8.5.0_eslint@8.14.0: + /eslint-config-prettier/8.5.0_eslint@8.19.0: resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 8.14.0 + eslint: 8.19.0 dev: true - /eslint-config-xo-typescript/0.50.0_rlpazuwa65tlndzjygmyw3fv34: - resolution: {integrity: sha512-Ru2tXB8y2w9fFHLm4v2AVfY6P81UbfEuDZuxEpeXlfV65Ezlk0xO4nBaT899ojIFkWfr60rP9Ye4CdVUUT1UYg==} + /eslint-config-xo-typescript/0.51.1_mrmiyy5gtk737x73xppquzywim: + resolution: {integrity: sha512-bqUYpPLylgOtuueawFJnLFX/t2W6shKYR+IwbwBZhw9ivr2sLd+8I2vLfKDDzxMrHzij8bkdVoRoDLRvugQoXg==} engines: {node: '>=12'} peerDependencies: - '@typescript-eslint/eslint-plugin': '>=5.8.0' + '@typescript-eslint/eslint-plugin': '>=5.22.0' eslint: '>=8.0.0' typescript: '>=4.4' dependencies: - '@typescript-eslint/eslint-plugin': 5.22.0_lnjlwhtxjffjmj5o7dnwvwyqxq - eslint: 8.14.0 - typescript: 4.6.4 + '@typescript-eslint/eslint-plugin': 5.30.5_6zdoc3rn4mpiddqwhppni2mnnm + eslint: 8.19.0 + typescript: 4.7.4 dev: true - /eslint-config-xo/0.40.0_eslint@8.14.0: - resolution: {integrity: sha512-msI1O0JGxeK2bbExg3U6EGaWKcjhOFzEjwzObywG/DC5GSNZTOyJT+b2l9MZGBeZsVdxfIGwdXTNeWXl8cN9iw==} + /eslint-config-xo/0.41.0_eslint@8.19.0: + resolution: {integrity: sha512-cyTc182COQVdalOi5105h0Cw/Qb52IRGyIZLmUICIauANm9Upmv81UEsuFkdKnvwr4NtU95qjdk3g4/kNspA6g==} engines: {node: '>=12'} peerDependencies: - eslint: '>=8.6.0' + eslint: '>=8.14.0' dependencies: confusing-browser-globals: 1.0.11 - eslint: 8.14.0 + eslint: 8.19.0 dev: true /eslint-formatter-pretty/4.1.0: @@ -1803,7 +1914,7 @@ packages: '@types/eslint': 7.29.0 ansi-escapes: 4.3.2 chalk: 4.1.2 - eslint-rule-docs: 1.1.231 + eslint-rule-docs: 1.1.235 log-symbols: 4.1.0 plur: 4.0.0 string-width: 4.2.3 @@ -1814,7 +1925,9 @@ packages: resolution: {integrity: sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==} dependencies: debug: 3.2.7 - resolve: 1.22.0 + resolve: 1.22.1 + transitivePeerDependencies: + - supports-color dev: true /eslint-import-resolver-webpack/0.13.2_fkfqfehjtk7sk2efaqbgxsuasa: @@ -1830,35 +1943,56 @@ packages: array-find: 1.0.0 debug: 3.2.7 enhanced-resolve: 0.9.1 - eslint-plugin-import: 2.26.0_eslint@8.14.0 + eslint-plugin-import: 2.26.0_umvwevlnf7obdkl4ojuppx7fhy find-root: 1.1.0 has: 1.0.3 interpret: 1.4.0 is-core-module: 2.9.0 is-regex: 1.1.4 lodash: 4.17.21 - resolve: 1.22.0 + resolve: 1.22.1 semver: 5.7.1 + transitivePeerDependencies: + - supports-color dev: true - /eslint-module-utils/2.7.3: + /eslint-module-utils/2.7.3_nnoslj5sfirk4b2da3uhvivypi: resolution: {integrity: sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==} engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true dependencies: + '@typescript-eslint/parser': 5.30.5_4x5o4skxv6sl53vpwefgt23khm debug: 3.2.7 + eslint-import-resolver-node: 0.3.6 + eslint-import-resolver-webpack: 0.13.2_fkfqfehjtk7sk2efaqbgxsuasa find-up: 2.1.0 + transitivePeerDependencies: + - supports-color dev: true - /eslint-plugin-ava/13.2.0_eslint@8.14.0: + /eslint-plugin-ava/13.2.0_eslint@8.19.0: resolution: {integrity: sha512-i5B5izsEdERKQLruk1nIWzTTE7C26/ju8qQf7JeyRv32XT2lRMW0zMFZNhIrEf5/5VvpSz2rqrV7UcjClGbKsw==} engines: {node: '>=12.22 <13 || >=14.17 <15 || >=16.4'} peerDependencies: eslint: '>=7.22.0' dependencies: enhance-visitors: 1.0.0 - eslint: 8.14.0 - eslint-utils: 3.0.0_eslint@8.14.0 - espree: 9.3.1 + eslint: 8.19.0 + eslint-utils: 3.0.0_eslint@8.19.0 + espree: 9.3.2 espurify: 2.1.1 import-modules: 2.1.0 micro-spelling-correcter: 1.1.1 @@ -1866,48 +2000,74 @@ packages: resolve-from: 5.0.0 dev: true - /eslint-plugin-es/3.0.1_eslint@8.14.0: - resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} + /eslint-plugin-es/4.1.0_eslint@8.19.0: + resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} engines: {node: '>=8.10.0'} peerDependencies: eslint: '>=4.19.1' dependencies: - eslint: 8.14.0 + eslint: 8.19.0 eslint-utils: 2.1.0 regexpp: 3.2.0 dev: true - /eslint-plugin-eslint-comments/3.2.0_eslint@8.14.0: + /eslint-plugin-eslint-comments/3.2.0_eslint@8.19.0: resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==} engines: {node: '>=6.5.0'} peerDependencies: eslint: '>=4.19.1' dependencies: escape-string-regexp: 1.0.5 - eslint: 8.14.0 + eslint: 8.19.0 ignore: 5.2.0 dev: true - /eslint-plugin-import/2.26.0_eslint@8.14.0: + /eslint-plugin-import/2.26.0_umvwevlnf7obdkl4ojuppx7fhy: resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==} engines: {node: '>=4'} peerDependencies: + '@typescript-eslint/parser': '*' eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true dependencies: + '@typescript-eslint/parser': 5.30.5_4x5o4skxv6sl53vpwefgt23khm array-includes: 3.1.5 array.prototype.flat: 1.3.0 debug: 2.6.9 doctrine: 2.1.0 - eslint: 8.14.0 + eslint: 8.19.0 eslint-import-resolver-node: 0.3.6 - eslint-module-utils: 2.7.3 + eslint-module-utils: 2.7.3_nnoslj5sfirk4b2da3uhvivypi has: 1.0.3 is-core-module: 2.9.0 is-glob: 4.0.3 minimatch: 3.1.2 object.values: 1.1.5 - resolve: 1.22.0 + resolve: 1.22.1 tsconfig-paths: 3.14.1 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-n/15.2.4_eslint@8.19.0: + resolution: {integrity: sha512-tjnVMv2fiXYMnuiIFI8QMtyUFI42SckEEWvi8h68SWGWshfqO6SSCASy24dGMGAiy7NUk6DZt90DM0iNUsmQ5w==} + engines: {node: '>=12.22.0'} + peerDependencies: + eslint: '>=7.0.0' + dependencies: + builtins: 5.0.1 + eslint: 8.19.0 + eslint-plugin-es: 4.1.0_eslint@8.19.0 + eslint-utils: 3.0.0_eslint@8.19.0 + ignore: 5.2.0 + is-core-module: 2.9.0 + minimatch: 3.1.2 + resolve: 1.22.1 + semver: 7.3.7 dev: true /eslint-plugin-no-use-extend-native/0.5.0: @@ -1920,24 +2080,9 @@ packages: is-proto-prop: 2.0.0 dev: true - /eslint-plugin-node/11.1.0_eslint@8.14.0: - resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} - engines: {node: '>=8.10.0'} - peerDependencies: - eslint: '>=5.16.0' - dependencies: - eslint: 8.14.0 - eslint-plugin-es: 3.0.1_eslint@8.14.0 - eslint-utils: 2.1.0 - ignore: 5.2.0 - minimatch: 3.1.2 - resolve: 1.22.0 - semver: 6.3.0 - dev: true - - /eslint-plugin-prettier/4.0.0_mzpligoj26dazigcet37nxg2zy: - resolution: {integrity: sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==} - engines: {node: '>=6.0.0'} + /eslint-plugin-prettier/4.2.1_7uxdfn2xinezdgvmbammh6ev5i: + resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} + engines: {node: '>=12.0.0'} peerDependencies: eslint: '>=7.28.0' eslint-config-prettier: '*' @@ -1946,23 +2091,23 @@ packages: eslint-config-prettier: optional: true dependencies: - eslint: 8.14.0 - eslint-config-prettier: 8.5.0_eslint@8.14.0 - prettier: 2.6.2 + eslint: 8.19.0 + eslint-config-prettier: 8.5.0_eslint@8.19.0 + prettier: 2.7.1 prettier-linter-helpers: 1.0.0 dev: true - /eslint-plugin-unicorn/40.1.0_eslint@8.14.0: - resolution: {integrity: sha512-y5doK2DF9Sr5AqKEHbHxjFllJ167nKDRU01HDcWyv4Tnmaoe9iNxMrBnaybZvWZUaE3OC5Unu0lNIevYamloig==} + /eslint-plugin-unicorn/42.0.0_eslint@8.19.0: + resolution: {integrity: sha512-ixBsbhgWuxVaNlPTT8AyfJMlhyC5flCJFjyK3oKE8TRrwBnaHvUbuIkCM1lqg8ryYrFStL/T557zfKzX4GKSlg==} engines: {node: '>=12'} peerDependencies: - eslint: '>=7.32.0' + eslint: '>=8.8.0' dependencies: - '@babel/helper-validator-identifier': 7.16.7 - ci-info: 3.3.0 + '@babel/helper-validator-identifier': 7.18.6 + ci-info: 3.3.2 clean-regexp: 1.0.0 - eslint: 8.14.0 - eslint-utils: 3.0.0_eslint@8.14.0 + eslint: 8.19.0 + eslint-utils: 3.0.0_eslint@8.19.0 esquery: 1.4.0 indent-string: 4.0.0 is-builtin-module: 3.1.0 @@ -1975,8 +2120,8 @@ packages: strip-indent: 3.0.0 dev: true - /eslint-rule-docs/1.1.231: - resolution: {integrity: sha512-egHz9A1WG7b8CS0x1P6P/Rj5FqZOjray/VjpJa14tMZalfRKvpE2ONJ3plCM7+PcinmU4tcmbPLv0VtwzSdLVA==} + /eslint-rule-docs/1.1.235: + resolution: {integrity: sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==} dev: true /eslint-scope/5.1.1: @@ -2002,13 +2147,13 @@ packages: eslint-visitor-keys: 1.3.0 dev: true - /eslint-utils/3.0.0_eslint@8.14.0: + /eslint-utils/3.0.0_eslint@8.19.0: resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} peerDependencies: eslint: '>=5' dependencies: - eslint: 8.14.0 + eslint: 8.19.0 eslint-visitor-keys: 2.1.0 dev: true @@ -2027,12 +2172,12 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint/8.14.0: - resolution: {integrity: sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==} + /eslint/8.19.0: + resolution: {integrity: sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint/eslintrc': 1.2.2 + '@eslint/eslintrc': 1.3.0 '@humanwhocodes/config-array': 0.9.5 ajv: 6.12.6 chalk: 4.1.2 @@ -2041,16 +2186,16 @@ packages: doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.1.1 - eslint-utils: 3.0.0_eslint@8.14.0 + eslint-utils: 3.0.0_eslint@8.19.0 eslint-visitor-keys: 3.3.0 - espree: 9.3.1 + espree: 9.3.2 esquery: 1.4.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 functional-red-black-tree: 1.0.1 glob-parent: 6.0.2 - globals: 13.13.0 + globals: 13.15.0 ignore: 5.2.0 import-fresh: 3.3.0 imurmurhash: 0.1.4 @@ -2071,16 +2216,19 @@ packages: - supports-color dev: true - /esm-utils/2.2.0: - resolution: {integrity: sha512-kYj4yNRo4W3by0f1mj4AfRh1nsRTTpQG921Ik3AfyUq6upGlkI1fnMLypHn6XtFzZPdCYH1k9mtQA5MyZF9m+w==} + /esm-utils/4.0.0: + resolution: {integrity: sha512-1x5H25/8BQWV94T8+KRb1gcSdVQ3g+8P0NikggAujVaurUa0cOoR+UO8ie3y29iQO70HjNA93c9ie+qqI/8zzw==} + dependencies: + import-meta-resolve: 1.1.1 + url-or-path: 2.1.0 dev: true - /espree/9.3.1: - resolution: {integrity: sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==} + /espree/9.3.2: + resolution: {integrity: sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.7.0 - acorn-jsx: 5.3.2_acorn@8.7.0 + acorn: 8.7.1 + acorn-jsx: 5.3.2_acorn@8.7.1 eslint-visitor-keys: 3.3.0 dev: true @@ -2211,6 +2359,8 @@ packages: type-is: 1.6.18 utils-merge: 1.0.1 vary: 1.1.2 + transitivePeerDependencies: + - supports-color dev: true /extend/3.0.2: @@ -2240,7 +2390,7 @@ packages: dev: true /fast-levenshtein/2.0.6: - resolution: {integrity: sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=} + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true /fastq/1.13.0: @@ -2272,6 +2422,8 @@ packages: parseurl: 1.3.3 statuses: 2.0.1 unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color dev: true /find-cache-dir/3.3.2: @@ -2288,7 +2440,7 @@ packages: dev: true /find-up/2.1.0: - resolution: {integrity: sha1-RdG35QbHF93UgndaK3eSCjwMV6c=} + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} engines: {node: '>=4'} dependencies: locate-path: 2.0.0 @@ -2313,7 +2465,7 @@ packages: resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: - locate-path: 7.1.0 + locate-path: 7.1.1 path-exists: 5.0.0 dev: true @@ -2321,12 +2473,12 @@ packages: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} engines: {node: ^10.12.0 || >=12.0.0} dependencies: - flatted: 3.2.5 + flatted: 3.2.6 rimraf: 3.0.2 dev: true - /flatted/3.2.5: - resolution: {integrity: sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==} + /flatted/3.2.6: + resolution: {integrity: sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==} dev: true /foreground-child/2.0.0: @@ -2369,12 +2521,12 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.0 + es-abstract: 1.20.1 functions-have-names: 1.2.3 dev: true /functional-red-black-tree/1.0.1: - resolution: {integrity: sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=} + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} dev: true /functions-have-names/1.2.3: @@ -2430,11 +2582,11 @@ packages: engines: {node: 6.* || 8.* || >= 10.*} /get-func-name/2.0.0: - resolution: {integrity: sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=} + resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} dev: true - /get-intrinsic/1.1.1: - resolution: {integrity: sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==} + /get-intrinsic/1.1.2: + resolution: {integrity: sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==} dependencies: function-bind: 1.1.1 has: 1.0.3 @@ -2446,7 +2598,7 @@ packages: dev: false /get-set-props/0.1.0: - resolution: {integrity: sha1-mYR1wXhEVobQsyJG2l3428++jqM=} + resolution: {integrity: sha512-7oKuKzAGKj0ag+eWZwcGw2fjiZ78tXnXQoBgY0aU7ZOxTu4bB7hSuQSDgtKy978EDH062P5FmD2EWiDpQS9K9Q==} engines: {node: '>=0.10.0'} dev: true @@ -2465,7 +2617,7 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 - get-intrinsic: 1.1.1 + get-intrinsic: 1.1.2 dev: true /glob-parent/5.1.2: @@ -2481,8 +2633,19 @@ packages: is-glob: 4.0.3 dev: true - /glob/7.2.0: - resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + /glob/7.2.2: + resolution: {integrity: sha512-NzDgHDiJwKYByLrL5lONmQFpK/2G78SMMfo+E9CuGlX4IkvfKDsiQSNPwAYxEy+e6p7ZQ3uslSLlwlJcqezBmQ==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob/7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -2497,8 +2660,8 @@ packages: engines: {node: '>=4'} dev: true - /globals/13.13.0: - resolution: {integrity: sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==} + /globals/13.15.0: + resolution: {integrity: sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==} engines: {node: '>=8'} dependencies: type-fest: 0.20.2 @@ -2515,8 +2678,8 @@ packages: merge2: 1.4.1 slash: 3.0.0 - /globby/13.1.1: - resolution: {integrity: sha512-XMzoDZbGZ37tufiv7g0N4F/zp3zkwdFtVbV3EHsVl1KQr4RPLfNoT068/97RPshz2J5xYNEjLKKBKaGHifBd3Q==} + /globby/13.1.2: + resolution: {integrity: sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: dir-glob: 3.0.1 @@ -2540,7 +2703,7 @@ packages: dev: true /has-flag/3.0.0: - resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=} + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} dev: true @@ -2552,7 +2715,7 @@ packages: /has-property-descriptors/1.0.0: resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} dependencies: - get-intrinsic: 1.1.1 + get-intrinsic: 1.1.2 dev: true /has-symbols/1.0.3: @@ -2574,6 +2737,18 @@ packages: function-bind: 1.1.1 dev: true + /hast-util-is-element/2.1.2: + resolution: {integrity: sha512-thjnlGAnwP8ef/GSO1Q8BfVk2gundnc2peGQqEg2kUt/IqesiGg/5mSwN2fE7nLzy61pg88NG6xV+UrGOrx9EA==} + dependencies: + '@types/hast': 2.3.4 + '@types/unist': 2.0.6 + dev: false + + /hast-util-parse-selector/3.1.0: + resolution: {integrity: sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==} + dependencies: + '@types/hast': 2.3.4 + /hast-util-to-estree/2.0.2: resolution: {integrity: sha512-UQrZVeBj6A9od0lpFvqHKNSH9zvDrNoyWKbveu1a2oSCXEDUI+3bnd6BoiQLPnLrcXXn/jzJ6y9hmJTTlvf8lQ==} dependencies: @@ -2595,9 +2770,32 @@ packages: - supports-color dev: true + /hast-util-to-html/8.0.3: + resolution: {integrity: sha512-/D/E5ymdPYhHpPkuTHOUkSatxr4w1ZKrZsG0Zv/3C2SRVT0JFJG53VS45AMrBtYk0wp5A7ksEhiC8QaOZM95+A==} + dependencies: + '@types/hast': 2.3.4 + ccount: 2.0.1 + comma-separated-tokens: 2.0.2 + hast-util-is-element: 2.1.2 + hast-util-whitespace: 2.0.0 + html-void-elements: 2.0.1 + property-information: 6.1.1 + space-separated-tokens: 2.0.1 + stringify-entities: 4.0.2 + unist-util-is: 5.1.1 + dev: false + /hast-util-whitespace/2.0.0: resolution: {integrity: sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==} - dev: true + + /hastscript/7.0.2: + resolution: {integrity: sha512-uA8ooUY4ipaBvKcMuPehTAB/YfFLSSzCwFSwT6ltJbocFUKH/GDHLN+tflq7lSRf9H86uOuxOFkh1KgIy3Gg2g==} + dependencies: + '@types/hast': 2.3.4 + comma-separated-tokens: 2.0.2 + hast-util-parse-selector: 3.1.0 + property-information: 6.1.1 + space-separated-tokens: 2.0.1 /hosted-git-info/2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -2614,6 +2812,10 @@ packages: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true + /html-void-elements/2.0.1: + resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} + dev: false + /http-errors/2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -2642,7 +2844,7 @@ packages: engines: {node: '>= 4'} /import-fresh/2.0.0: - resolution: {integrity: sha1-2BNVwVYS04bGH53dOSLUMEgipUY=} + resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} engines: {node: '>=4'} dependencies: caller-path: 2.0.0 @@ -2657,13 +2859,19 @@ packages: resolve-from: 4.0.0 dev: true + /import-meta-resolve/1.1.1: + resolution: {integrity: sha512-JiTuIvVyPaUg11eTrNDx5bgQ/yMKMZffc7YSjvQeSMXy58DO2SQ8BtAf3xteZvmzvjYh14wnqNjL8XVeDy2o9A==} + dependencies: + builtins: 4.1.0 + dev: true + /import-modules/2.1.0: resolution: {integrity: sha512-8HEWcnkbGpovH9yInoisxaSoIg9Brbul+Ju3Kqe2UsYDUBJD/iQjSgEj0zPcTDPKfPp2fs5xlv1i+JSye/m1/A==} engines: {node: '>=8'} dev: true /imurmurhash/0.1.4: - resolution: {integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o=} + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} dev: true @@ -2695,7 +2903,7 @@ packages: resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.1.1 + get-intrinsic: 1.1.2 has: 1.0.3 side-channel: 1.0.4 dev: true @@ -2735,7 +2943,7 @@ packages: dev: true /is-arrayish/0.2.1: - resolution: {integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=} + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} /is-arrayish/0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} @@ -2764,7 +2972,7 @@ packages: resolution: {integrity: sha512-OV7JjAgOTfAFJmHZLvpSTb4qi0nIILDV1gWPYDnDJUTNFM5aGlRAhk4QcT8i7TuAleeEV5Fdkqn3t4mS+Q11fg==} engines: {node: '>=6'} dependencies: - builtin-modules: 3.2.0 + builtin-modules: 3.3.0 dev: true /is-callable/1.2.4: @@ -2790,7 +2998,7 @@ packages: dev: true /is-directory/0.3.1: - resolution: {integrity: sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=} + resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==} engines: {node: '>=0.10.0'} dev: false @@ -2801,7 +3009,7 @@ packages: dev: true /is-extglob/2.1.1: - resolution: {integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=} + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} /is-fullwidth-code-point/3.0.0: @@ -2809,7 +3017,7 @@ packages: engines: {node: '>=8'} /is-get-set-prop/1.0.0: - resolution: {integrity: sha1-JzGHfk14pqae3M5rudaLB3nnYxI=} + resolution: {integrity: sha512-DvAYZ1ZgGUz4lzxKMPYlt08qAUqyG9ckSg2pIjfvcQ7+pkVNUHk8yVLXOnCLe5WKXhLop8oorWFBJHpwWQpszQ==} dependencies: get-set-props: 0.1.0 lowercase-keys: 1.0.1 @@ -2826,13 +3034,13 @@ packages: dev: true /is-js-type/2.0.0: - resolution: {integrity: sha1-c2FwBtZZtOtHKbunR9KHgt8PfiI=} + resolution: {integrity: sha512-Aj13l47+uyTjlQNHtXBV8Cji3jb037vxwMWCgopRR8h6xocgBGW3qG8qGlIOEmbXQtkKShKuBM9e8AA1OeQ+xw==} dependencies: js-types: 1.0.0 dev: true /is-negated-glob/1.0.0: - resolution: {integrity: sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=} + resolution: {integrity: sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==} engines: {node: '>=0.10.0'} dev: true @@ -2853,7 +3061,7 @@ packages: engines: {node: '>=0.12.0'} /is-obj-prop/1.0.0: - resolution: {integrity: sha1-s03nnEULjXxzqyzfZ9yHWtuF+A4=} + resolution: {integrity: sha512-5Idb61slRlJlsAzi0Wsfwbp+zZY+9LXKUAZpvT/1ySw+NxKLRWfa0Bzj+wXI3fX5O9hiddm5c3DAaRSNP/yl2w==} dependencies: lowercase-keys: 1.0.1 obj-props: 1.4.0 @@ -2870,7 +3078,7 @@ packages: dev: false /is-plain-obj/1.1.0: - resolution: {integrity: sha1-caUMhCnfync8kqOQpKA7OfzVHT4=} + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} dev: true @@ -2901,7 +3109,7 @@ packages: dev: true /is-regexp/1.0.0: - resolution: {integrity: sha1-/S2INUXEa6xaYz57mgnof6LLUGk=} + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} engines: {node: '>=0.10.0'} /is-relative/1.0.0: @@ -2967,7 +3175,7 @@ packages: dev: true /isexe/2.0.0: - resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true /istanbul-lib-coverage/3.2.0: @@ -3002,7 +3210,7 @@ packages: dev: true /js-types/1.0.0: - resolution: {integrity: sha1-0kLmSU7Vcq08koCfyL7X92h8vwM=} + resolution: {integrity: sha512-bfwqBW9cC/Lp7xcRpug7YrXm0IVw+T9e3g4mCYnv0Pjr3zIzU9PCQElYU9oSGAWzXlbdl9X5SAMPejO9sxkeUw==} engines: {node: '>=0.10.0'} dev: true @@ -3040,7 +3248,7 @@ packages: dev: true /json-stable-stringify-without-jsonify/1.0.1: - resolution: {integrity: sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=} + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true /json5/1.0.1: @@ -3082,7 +3290,7 @@ packages: resolution: {integrity: sha512-Atocnm7Wr9nuvAn97yEPQa3pcQI5eLQGBz+m6iTb+CVw+IOzYB9MrYK7jI7BfC9ISnT4Fu0eiwhAScV//rp4Hw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: - type-fest: 2.12.2 + type-fest: 2.16.0 dev: true /lines-and-columns/1.2.4: @@ -3095,7 +3303,7 @@ packages: dev: true /locate-path/2.0.0: - resolution: {integrity: sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=} + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} engines: {node: '>=4'} dependencies: p-locate: 2.0.0 @@ -3115,8 +3323,8 @@ packages: p-locate: 5.0.0 dev: true - /locate-path/7.1.0: - resolution: {integrity: sha512-HNx5uOnYeK4SxEoid5qnhRfprlJeGMzFRKPLCf/15N3/B4AiofNwC/yq7VBKdVk9dx7m+PiYCJOGg55JYTAqoQ==} + /locate-path/7.1.1: + resolution: {integrity: sha512-vJXaRMJgRVD3+cUZs3Mncj2mxpt5mP0EmNOsxRSZRMlbqjvxzDEOIUWXGmavo0ZC9+tNZCBLQ66reA11nbpHZg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: p-locate: 6.0.0 @@ -3127,11 +3335,11 @@ packages: dev: true /lodash.fill/3.4.0: - resolution: {integrity: sha1-o8dK5kDQU63w3CB5+HIHiOi/74U=} + resolution: {integrity: sha512-YgunwHKIxPWOe3VnM65J3oi6oShakIxdLMeIZ9xxcsMxc8X/FQC2VlA4eJzMv+7GlC5gebQLn+U+qcNoG18iLA==} dev: false /lodash.isfinite/3.3.2: - resolution: {integrity: sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=} + resolution: {integrity: sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==} dev: false /lodash.isfunction/3.0.9: @@ -3143,11 +3351,11 @@ packages: dev: true /lodash.padend/4.6.1: - resolution: {integrity: sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=} + resolution: {integrity: sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw==} dev: false /lodash.padstart/4.6.1: - resolution: {integrity: sha1-0uPuv/DZ05rVD1y9G1KnvOa7YRs=} + resolution: {integrity: sha512-sW73O6S8+Tg66eY56DBk85aQzzUJDtpoXFBgELMd5P/SotAguo+1kYO6RuYgXxA4HJH3LFTFPASX6ET6bjfriw==} dev: false /lodash/4.17.21: @@ -3198,7 +3406,7 @@ packages: semver: 6.3.0 /map-obj/1.0.1: - resolution: {integrity: sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=} + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} dev: true @@ -3329,11 +3537,11 @@ packages: dev: true /memory-fs/0.2.0: - resolution: {integrity: sha1-8rslNovBIeORwlIN6Slpyu4KApA=} + resolution: {integrity: sha512-+y4mDxU4rvXXu5UDSGCGNiesFmwCHuefGMoPCO1WYucNYj7DsLqrFaa2fXVI0H+NNiPTwwzKwspn9yTZqUGqng==} dev: true - /meow/10.1.2: - resolution: {integrity: sha512-zbuAlN+V/sXlbGchNS9WTWjUzeamwMt/BApKCJi7B0QyZstZaMx0n4Unll/fg0njGtMdC9UP5SAscvOCLYdM+Q==} + /meow/10.1.3: + resolution: {integrity: sha512-0WL7RMCPPdUTE00+GxJjL4d5Dm6eUbmAzxlzywJWiRUKCW093owmZ7/q74tH9VI91vxw9KJJNxAcvdpxb2G4iA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: '@types/minimist': 1.2.2 @@ -3686,7 +3894,6 @@ packages: /moo/0.5.1: resolution: {integrity: sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==} - dev: true /mri/1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} @@ -3694,7 +3901,7 @@ packages: dev: true /ms/2.0.0: - resolution: {integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=} + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: true /ms/2.1.2: @@ -3711,7 +3918,7 @@ packages: dev: true /natural-compare/1.4.0: - resolution: {integrity: sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=} + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true /negotiator/0.6.3: @@ -3727,7 +3934,7 @@ packages: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.0 + resolve: 1.22.1 semver: 5.7.1 validate-npm-package-license: 3.0.4 dev: true @@ -3754,8 +3961,8 @@ packages: engines: {node: '>=0.10.0'} dev: true - /object-inspect/1.12.0: - resolution: {integrity: sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==} + /object-inspect/1.12.2: + resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} dev: true /object-keys/1.1.1: @@ -3779,7 +3986,7 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.0 + es-abstract: 1.20.1 dev: true /on-finished/2.4.1: @@ -3861,7 +4068,7 @@ packages: dev: true /p-locate/2.0.0: - resolution: {integrity: sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=} + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} engines: {node: '>=4'} dependencies: p-limit: 1.3.0 @@ -3888,7 +4095,7 @@ packages: dev: true /p-try/1.0.0: - resolution: {integrity: sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=} + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} engines: {node: '>=4'} dev: true @@ -3917,7 +4124,7 @@ packages: dev: true /parse-json/4.0.0: - resolution: {integrity: sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=} + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} engines: {node: '>=4'} dependencies: error-ex: 1.3.2 @@ -3928,7 +4135,7 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.16.7 + '@babel/code-frame': 7.18.6 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -3940,7 +4147,7 @@ packages: dev: true /path-exists/3.0.0: - resolution: {integrity: sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=} + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} dev: true @@ -4046,8 +4253,8 @@ packages: engines: {node: '>=4'} dev: true - /postcss/8.4.13: - resolution: {integrity: sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==} + /postcss/8.4.14: + resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.4 @@ -4066,7 +4273,6 @@ packages: /preact/10.7.1: resolution: {integrity: sha512-MufnRFz39aIhs9AMFisonjzTud1PK1bY+jcJLo6m2T9Uh8AqjD77w11eAAawmjUogoGOnipECq7e/1RClIKsxg==} - dev: false /prelude-ls/1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -4080,8 +4286,8 @@ packages: fast-diff: 1.2.0 dev: true - /prettier/2.6.2: - resolution: {integrity: sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==} + /prettier/2.7.1: + resolution: {integrity: sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==} engines: {node: '>=10.13.0'} hasBin: true dev: true @@ -4092,7 +4298,6 @@ packages: /property-information/6.1.1: resolution: {integrity: sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==} - dev: true /proto-props/2.0.0: resolution: {integrity: sha512-2yma2tog9VaRZY2mn3Wq51uiSW4NcPYT1cQdBagwyrznrilKSZwIZ0UG3ZPL/mx+axEns0hE35T5ufOYZXEnBQ==} @@ -4127,7 +4332,7 @@ packages: engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} dependencies: lodash: 4.17.21 - resolve: 1.22.0 + resolve: 1.22.1 dev: true /quick-lru/5.1.1: @@ -4253,7 +4458,7 @@ packages: dev: true /require-directory/2.1.1: - resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=} + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} /require-main-filename/2.0.0: @@ -4261,7 +4466,7 @@ packages: dev: false /resolve-from/3.0.0: - resolution: {integrity: sha1-six699nWiBvItuZTM17rywoYh0g=} + resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} engines: {node: '>=4'} dev: false @@ -4284,6 +4489,15 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /resolve/1.22.1: + resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + hasBin: true + dependencies: + is-core-module: 2.9.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + /reusify/1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -4292,11 +4506,11 @@ packages: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true dependencies: - glob: 7.2.0 + glob: 7.2.2 dev: true - /rollup/2.72.0: - resolution: {integrity: sha512-KqtR2YcO35/KKijg4nx4STO3569aqCUeGRkKWnJ6r+AvBBrVY9L4pmf4NHVrQr4mTOq6msbohflxr2kpihhaOA==} + /rollup/2.75.7: + resolution: {integrity: sha512-VSE1iy0eaAYNCxEXaleThdFXqZJ42qDBatAwrfnPlENEZ8erQ+0LYX4JXOLPceWfZpV1VtZwZ3dFCuOZiSyFtQ==} engines: {node: '>=10.0.0'} hasBin: true optionalDependencies: @@ -4312,11 +4526,10 @@ packages: resolution: {integrity: sha512-iFPgh7SatHXOG1ClcpdwHI63geV3Hc/iL6crGSyBlH2PY7Rm/za+zoKz6FfY/Qlw5K7JwSol8pseO8fN6CMhhQ==} dev: false - /rxjs/6.6.7: - resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} - engines: {npm: '>=2.0.0'} + /rxjs/7.5.5: + resolution: {integrity: sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==} dependencies: - tslib: 1.14.1 + tslib: 2.4.0 dev: true /sade/1.8.1: @@ -4377,6 +4590,8 @@ packages: on-finished: 2.4.1 range-parser: 1.2.1 statuses: 2.0.1 + transitivePeerDependencies: + - supports-color dev: true /serve-static/1.15.0: @@ -4387,10 +4602,12 @@ packages: escape-html: 1.0.3 parseurl: 1.3.3 send: 0.18.0 + transitivePeerDependencies: + - supports-color dev: true /set-blocking/2.0.0: - resolution: {integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc=} + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: false /setprototypeof/1.2.0: @@ -4409,12 +4626,16 @@ packages: engines: {node: '>=8'} dev: true + /shell-quote/1.7.3: + resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==} + dev: true + /side-channel/1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: call-bind: 1.0.2 - get-intrinsic: 1.1.1 - object-inspect: 1.12.0 + get-intrinsic: 1.1.2 + object-inspect: 1.12.2 dev: true /signal-exit/3.0.7: @@ -4422,7 +4643,7 @@ packages: dev: true /simple-swizzle/0.2.2: - resolution: {integrity: sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=} + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} dependencies: is-arrayish: 0.3.2 dev: false @@ -4457,10 +4678,9 @@ packages: /space-separated-tokens/2.0.1: resolution: {integrity: sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==} - dev: true /spawn-command/0.0.2-1: - resolution: {integrity: sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=} + resolution: {integrity: sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==} dev: true /spdx-correct/3.1.1: @@ -4486,7 +4706,7 @@ packages: dev: true /sprintf-js/1.0.3: - resolution: {integrity: sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=} + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: false /statuses/2.0.1: @@ -4507,7 +4727,7 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.0 + es-abstract: 1.20.1 dev: true /string.prototype.trimstart/1.0.5: @@ -4515,7 +4735,7 @@ packages: dependencies: call-bind: 1.0.2 define-properties: 1.1.4 - es-abstract: 1.20.0 + es-abstract: 1.20.1 dev: true /string_decoder/1.3.0: @@ -4529,10 +4749,9 @@ packages: dependencies: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 - dev: true /stringify-object-es5/2.5.0: - resolution: {integrity: sha1-BXw8mpChJzObudFwSikLt70KHsU=} + resolution: {integrity: sha512-vE7Xdx9ylG4JI16zy7/ObKUB+MtxuMcWlj/WHHr3+yAlQoN6sst2stU9E+2Qs3OrlJw/Pf3loWxL1GauEHf6MA==} engines: {node: '>=0.10.0'} dependencies: is-plain-obj: 1.1.0 @@ -4555,7 +4774,7 @@ packages: ansi-regex: 5.0.1 /strip-bom/3.0.0: - resolution: {integrity: sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=} + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} dev: true @@ -4624,7 +4843,7 @@ packages: dev: true /tapable/0.1.10: - resolution: {integrity: sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=} + resolution: {integrity: sha512-jX8Et4hHg57mug1/079yitEKWGB3LCwoxByLsNim89LABq8NqgiX+6iYVOsq0vX8uJHkU+DZ5fnq95f800bEsQ==} engines: {node: '>=0.6'} dev: true @@ -4633,12 +4852,12 @@ packages: engines: {node: '>=8'} dependencies: '@istanbuljs/schema': 0.1.3 - glob: 7.2.0 + glob: 7.2.3 minimatch: 3.1.2 dev: true - /testdouble/3.16.5: - resolution: {integrity: sha512-2/0vR503/X/6LuZzQObpcBtu+KycyEnw7AdX/NPboR7sam1NTuYc128UMW8n7tBDpOPGAKqCUtKVBzvgV6C3bA==} + /testdouble/3.16.6: + resolution: {integrity: sha512-mijMgc9y7buK9IG9zSVhzlXsFMqWbLQHRei4SLX7F7K4Qtrcnglg6lIMTCmNs6RwDUyLGWtpIe+TzkugYHB+qA==} engines: {node: '>= 4.0.0'} dependencies: lodash: 4.17.21 @@ -4648,25 +4867,25 @@ packages: dev: true /text-table/0.2.0: - resolution: {integrity: sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=} + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true /theredoc/1.0.0: resolution: {integrity: sha512-KU3SA3TjRRM932jpNfD3u4Ec3bSvedyo5ITPI7zgWYnKep7BwQQaxlhI9qbO+lKJoRnoAbEVfMcAHRuKVYikDA==} dev: true - /tinypool/0.1.3: - resolution: {integrity: sha512-2IfcQh7CP46XGWGGbdyO4pjcKqsmVqFAPcXfPxcPXmOWt9cYkTP9HcDmGgsfijYoAEc4z9qcpM/BaBz46Y9/CQ==} + /tinypool/0.2.1: + resolution: {integrity: sha512-HFU5ZYVq3wBfhSaf8qdqGsneaqXm0FgJQpoUlJbVdHpRLzm77IneKAD3RjzJWZvIv0YpPB9S7LUW53f6BE6ZSg==} engines: {node: '>=14.0.0'} dev: true - /tinyspy/0.3.2: - resolution: {integrity: sha512-2+40EP4D3sFYy42UkgkFFB+kiX2Tg3URG/lVvAZFfLxgGpnWl5qQJuBw1gaLttq8UOS+2p3C0WrhJnQigLTT2Q==} + /tinyspy/0.3.3: + resolution: {integrity: sha512-gRiUR8fuhUf0W9lzojPf1N1euJYA30ISebSfgca8z76FOvXtVXqd5ojEIaKLWbDQhAaC3ibxZIjqbyi4ybjcTw==} engines: {node: '>=14.0.0'} dev: true /to-absolute-glob/2.0.2: - resolution: {integrity: sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=} + resolution: {integrity: sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==} engines: {node: '>=0.10.0'} dependencies: is-absolute: 1.0.0 @@ -4716,14 +4935,18 @@ packages: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true - /tsutils/3.21.0_typescript@4.6.4: + /tslib/2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + dev: true + + /tsutils/3.21.0_typescript@4.7.4: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 4.6.4 + typescript: 4.7.4 dev: true /type-check/0.4.0: @@ -4763,8 +4986,8 @@ packages: engines: {node: '>=10'} dev: true - /type-fest/2.12.2: - resolution: {integrity: sha512-qt6ylCGpLjZ7AaODxbpyBZSs9fCI9SkL3Z9q2oxMBQhs/uyY+VD8jHA8ULCGmWQJlBgqvO3EJeAngOHD8zQCrQ==} + /type-fest/2.16.0: + resolution: {integrity: sha512-qpaThT2HQkFb83gMOrdKVsfCN7LKxP26Yq+smPzY1FqoHRjqmjqHXA7n5Gkxi8efirtbeEUxzfEdePthQWCuHw==} engines: {node: '>=12.20'} dev: true @@ -4776,8 +4999,8 @@ packages: mime-types: 2.1.35 dev: true - /typescript/4.6.4: - resolution: {integrity: sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==} + /typescript/4.7.4: + resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} engines: {node: '>=4.2.0'} hasBin: true dev: true @@ -4792,7 +5015,7 @@ packages: dev: true /unc-path-regex/0.1.2: - resolution: {integrity: sha1-5z3T17DXxe2G+6xrCufYxqadUPo=} + resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} engines: {node: '>=0.10.0'} dev: true @@ -4820,7 +5043,11 @@ packages: /unist-util-is/5.1.1: resolution: {integrity: sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==} - dev: true + + /unist-util-map/3.1.1: + resolution: {integrity: sha512-n36sjBn4ibPtAzrFweyT4FOcCI/UdzboaEcsZvwoAyD/gVw5B3OLlMBySePMO6r+uzjxQEyRll2akfVaT4SHhw==} + dependencies: + '@types/unist': 2.0.6 /unist-util-position-from-estree/1.1.1: resolution: {integrity: sha512-xtoY50b5+7IH8tFbkw64gisG9tMSpxDjhX9TmaJJae/XuxQ9R/Kc8Nv1eOsf43Gt4KV/LkriMy9mptDr7XLcaw==} @@ -4859,7 +5086,6 @@ packages: dependencies: '@types/unist': 2.0.6 unist-util-is: 5.1.1 - dev: true /unist-util-visit/3.1.0: resolution: {integrity: sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==} @@ -4893,8 +5119,12 @@ packages: punycode: 2.1.1 dev: true + /url-or-path/2.1.0: + resolution: {integrity: sha512-dsBD6GbytSMj9YDb3jVzSRENwFh50oUORnWBeSHfo0Lnwv2KMm/J4npyGy1P9rivUPsUGLjTA53XqAFqpe0nww==} + dev: true + /util-deprecate/1.0.2: - resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false /utils-merge/1.0.1: @@ -4917,11 +5147,11 @@ packages: resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} dev: true - /v8-to-istanbul/9.0.0: - resolution: {integrity: sha512-HcvgY/xaRm7isYmyx+lFKA4uQmfUbN0J4M0nNItvzTvH/iQ9kW5j/t4YSR+Ge323/lrgDAWJoF46tzGQHwBHFw==} + /v8-to-istanbul/9.0.1: + resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==} engines: {node: '>=10.12.0'} dependencies: - '@jridgewell/trace-mapping': 0.3.9 + '@jridgewell/trace-mapping': 0.3.14 '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.8.0 dev: true @@ -4962,7 +5192,7 @@ packages: dev: true /viewbox/1.0.0: - resolution: {integrity: sha1-eWDGEGvx1+yiG4lLagZlvjaNgso=} + resolution: {integrity: sha512-bDN0374iFVhSqze54pPxGkXsx1oHod2iv03C+X7UzSSuTV3SeVDXZwLR1ApCe9u+c63liV6kum0Srm5gEr3yoQ==} dev: false /vite-plugin-import-build/0.1.3: @@ -5005,8 +5235,8 @@ packages: - supports-color dev: true - /vite/2.9.8: - resolution: {integrity: sha512-zsBGwn5UT3YS0NLSJ7hnR54+vUKfgzMUh/Z9CxF1YKEBVIe213+63jrFLmZphgGI5zXwQCSmqIdbPuE8NJywPw==} + /vite/2.9.13: + resolution: {integrity: sha512-AsOBAaT0AD7Mhe8DuK+/kE4aWYFMx/i0ZNi98hJclxb4e0OhQcZYUrvLjIaQ8e59Ui7txcvKMiJC1yftqpQoDw==} engines: {node: '>=12.2.0'} hasBin: true peerDependencies: @@ -5021,24 +5251,27 @@ packages: stylus: optional: true dependencies: - esbuild: 0.14.38 - postcss: 8.4.13 - resolve: 1.22.0 - rollup: 2.72.0 + esbuild: 0.14.47 + postcss: 8.4.14 + resolve: 1.22.1 + rollup: 2.75.7 optionalDependencies: fsevents: 2.3.2 dev: true - /vitest/0.10.4_c8@7.11.2: - resolution: {integrity: sha512-FJ2av2PVozmyz9nqHRoC3H8j2z0OQXj8P8jS5oyMY9mfPWB06GS5k/1Ot++TkVBLQRHZCcVzjbK4BO7zqAJZGQ==} + /vitest/0.17.0_c8@7.11.3: + resolution: {integrity: sha512-5YO9ubHo0Zg35mea3+zZAr4sCku32C3usvIH5COeJB48TZV/R0J9aGNtGOOqEWZYfOKP0pGZUvTokne3x/QEFg==} engines: {node: '>=v14.16.0'} hasBin: true peerDependencies: + '@edge-runtime/vm': '*' '@vitest/ui': '*' c8: '*' happy-dom: '*' jsdom: '*' peerDependenciesMeta: + '@edge-runtime/vm': + optional: true '@vitest/ui': optional: true c8: @@ -5050,16 +5283,19 @@ packages: dependencies: '@types/chai': 4.3.1 '@types/chai-subset': 1.3.3 - c8: 7.11.2 + '@types/node': 18.0.1 + c8: 7.11.3 chai: 4.3.6 + debug: 4.3.4 local-pkg: 0.4.1 - tinypool: 0.1.3 - tinyspy: 0.3.2 - vite: 2.9.8 + tinypool: 0.2.1 + tinyspy: 0.3.3 + vite: 2.9.13 transitivePeerDependencies: - less - sass - stylus + - supports-color dev: true /whats-that-gerber/4.2.7: @@ -5079,7 +5315,7 @@ packages: dev: true /which-module/2.0.0: - resolution: {integrity: sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=} + resolution: {integrity: sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==} dev: false /which/2.0.2: @@ -5123,53 +5359,54 @@ packages: dev: true /xml-element-string/1.0.0: - resolution: {integrity: sha1-g8XWM1Jl8nCZJEwzrgyT1Ke6DIw=} + resolution: {integrity: sha512-JhO/ZCCwce8c9rLVXEA/KAy3Kg5340Ey4QBglQ+9ScQrQdroothAXblTRtyGkJZRRqQkkJSMZx8Kd7DGPMBsAw==} dependencies: escape-html: 1.0.3 dev: false - /xo/0.48.0: - resolution: {integrity: sha512-f0sbQGJoML3nwOLG7EIAJroBypmLokoGJqTPN+bI/oogKLMciqWBEiFh9Vpxnfwxafq1AkHoWrQZQWSflDCG1w==} + /xo/0.50.0: + resolution: {integrity: sha512-yIz7mdIbUlxBYLnV3OqMTdrE+OFr0CPINkU9rxY3ZHNAIZrVckmONLujU6LkdNrEWerQTx8zzwnVrUjmj6vVCg==} engines: {node: '>=12.20'} hasBin: true dependencies: - '@eslint/eslintrc': 1.2.2 - '@typescript-eslint/eslint-plugin': 5.22.0_lnjlwhtxjffjmj5o7dnwvwyqxq - '@typescript-eslint/parser': 5.22.0_t725usgvqspm5woeqpaxbfp2qu + '@eslint/eslintrc': 1.3.0 + '@typescript-eslint/eslint-plugin': 5.30.5_6zdoc3rn4mpiddqwhppni2mnnm + '@typescript-eslint/parser': 5.30.5_4x5o4skxv6sl53vpwefgt23khm arrify: 3.0.0 cosmiconfig: 7.0.1 define-lazy-prop: 3.0.0 - eslint: 8.14.0 - eslint-config-prettier: 8.5.0_eslint@8.14.0 - eslint-config-xo: 0.40.0_eslint@8.14.0 - eslint-config-xo-typescript: 0.50.0_rlpazuwa65tlndzjygmyw3fv34 + eslint: 8.19.0 + eslint-config-prettier: 8.5.0_eslint@8.19.0 + eslint-config-xo: 0.41.0_eslint@8.19.0 + eslint-config-xo-typescript: 0.51.1_mrmiyy5gtk737x73xppquzywim eslint-formatter-pretty: 4.1.0 eslint-import-resolver-webpack: 0.13.2_fkfqfehjtk7sk2efaqbgxsuasa - eslint-plugin-ava: 13.2.0_eslint@8.14.0 - eslint-plugin-eslint-comments: 3.2.0_eslint@8.14.0 - eslint-plugin-import: 2.26.0_eslint@8.14.0 + eslint-plugin-ava: 13.2.0_eslint@8.19.0 + eslint-plugin-eslint-comments: 3.2.0_eslint@8.19.0 + eslint-plugin-import: 2.26.0_umvwevlnf7obdkl4ojuppx7fhy + eslint-plugin-n: 15.2.4_eslint@8.19.0 eslint-plugin-no-use-extend-native: 0.5.0 - eslint-plugin-node: 11.1.0_eslint@8.14.0 - eslint-plugin-prettier: 4.0.0_mzpligoj26dazigcet37nxg2zy - eslint-plugin-unicorn: 40.1.0_eslint@8.14.0 - esm-utils: 2.2.0 + eslint-plugin-prettier: 4.2.1_7uxdfn2xinezdgvmbammh6ev5i + eslint-plugin-unicorn: 42.0.0_eslint@8.19.0 + esm-utils: 4.0.0 find-cache-dir: 3.3.2 find-up: 6.3.0 get-stdin: 9.0.0 - globby: 13.1.1 + globby: 13.1.2 imurmurhash: 0.1.4 json-stable-stringify-without-jsonify: 1.0.1 json5: 2.2.1 lodash-es: 4.17.21 - meow: 10.1.2 + meow: 10.1.3 micromatch: 4.0.5 open-editor: 4.0.0 - prettier: 2.6.2 + prettier: 2.7.1 semver: 7.3.7 slash: 4.0.0 to-absolute-glob: 2.0.2 - typescript: 4.6.4 + typescript: 4.7.4 transitivePeerDependencies: + - eslint-import-resolver-typescript - supports-color - webpack dev: true @@ -5249,6 +5486,19 @@ packages: yargs-parser: 20.2.9 dev: true + /yargs/17.5.1: + resolution: {integrity: sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==} + engines: {node: '>=12'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.0.1 + dev: true + /yocto-queue/0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/tsconfig.json b/tsconfig.json index 3fce6f6e..556efe41 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ {"path": "./packages/cli"}, {"path": "./packages/parser"}, {"path": "./packages/plotter"}, + {"path": "./packages/renderer"}, {"path": "./packages/xml-id"}, {"path": "./packages/whats-that-gerber"}, {"path": "./www"} diff --git a/vite.config.ts b/vite.config.ts index 4aac65bc..74aa6728 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,7 +3,15 @@ import {baseConfig} from './config/vite.config.base' export default defineConfig({ ...baseConfig, + define: { + __PKG_NAME__: JSON.stringify('@tracespace/test'), + __PKG_VERSION__: JSON.stringify('0.0.0-test'), + __PKG_DESCRIPTION__: JSON.stringify('Test description'), + }, + assetsInclude: ['**/*.gbr', '**/*.drl'], test: { + setupFiles: './config/vitest.setup.ts', + outputDiffLines: 100, coverage: { all: true, extension: ['ts', 'tsx'], diff --git a/www/package.json b/www/package.json index a2b0e139..ff4d5334 100644 --- a/www/package.json +++ b/www/package.json @@ -12,8 +12,7 @@ "scripts": { "dev": "node server.js", "build": "vite build && vite build --ssr && vite-plugin-ssr prerender", - "preview": "node server.js preview", - "clean": "rimraf dist" + "preview": "node server.js preview" }, "contributors": [ "Mike Cousins (https://mike.cousins.io)" @@ -27,8 +26,14 @@ "@fontsource/open-sans": "^4.5.8", "@tracespace/parser": "workspace:*", "@tracespace/plotter": "workspace:*", + "@tracespace/renderer": "workspace:*", + "hast-util-to-html": "^8.0.3", + "hastscript": "^7.0.2", + "moo": "^0.5.1", "preact": "^10.7.1", "stringify-object": "3.3.0", + "unist-util-map": "^3.1.1", + "unist-util-visit-parents": "^5.1.0", "windicss": "^3.5.1" }, "devDependencies": { diff --git a/www/src/base/_default.page.client.tsx b/www/src/base/_default.page.client.tsx index a485bd08..ce37591b 100644 --- a/www/src/base/_default.page.client.tsx +++ b/www/src/base/_default.page.client.tsx @@ -1,5 +1,6 @@ import '@fontsource/open-sans/variable.css' import 'virtual:windi.css' +import 'virtual:windi-devtools' // eslint-disable-line import/no-unassigned-import import {hydrate, render} from 'preact' import {useClientRouter} from 'vite-plugin-ssr/client/router' diff --git a/www/src/components/gerber-render.tsx b/www/src/components/gerber-render.tsx index f5a42a0e..1ccfc8ec 100644 --- a/www/src/components/gerber-render.tsx +++ b/www/src/components/gerber-render.tsx @@ -1,8 +1,13 @@ -import {useMemo} from 'preact/hooks' +import {Ref} from 'preact' +import {useMemo, useRef, useEffect} from 'preact/hooks' +import {toHtml} from 'hast-util-to-html' import stringifyObject from 'stringify-object' import {GerberTree, GerberNode, createParser} from '@tracespace/parser' import {ImageTree, ImageNode, plot} from '@tracespace/plotter' +import {render} from '@tracespace/renderer' + +import type {SvgElement} from '@tracespace/renderer' export interface GerberFixture { contents: string @@ -17,6 +22,26 @@ const useImageTree = (gerberTree: GerberTree): ImageTree => { return useMemo(() => plot(gerberTree), [gerberTree]) } +const useRenderTree = (imageTree: ImageTree): SvgElement => { + return useMemo(() => render(imageTree), [imageTree]) +} + +const useRenderHtml = (renderTree: SvgElement): string => { + return useMemo(() => toHtml(renderTree), [renderTree]) +} + +const useHighlight = (highlight?: boolean): Ref => { + const element = useRef(null) + + useEffect(() => { + if (highlight) { + element.current?.scrollIntoView({block: 'nearest', behavior: 'smooth'}) + } + }, [highlight]) + + return element +} + export interface GerberRenderProps { contents: string highlightedLines: number[] @@ -89,6 +114,23 @@ export function GerberPlot( ) } +export function GerberSvg( + props: Pick +): JSX.Element { + const {contents, class: className} = props + const gerberTree = useGerberTree(contents) + const imageTree = useImageTree(gerberTree) + const renderTree = useRenderTree(imageTree) + const renderHtml = useRenderHtml(renderTree) + + return ( + + ) +} + interface GerberLineProps { text: string @@ -105,11 +147,14 @@ function GerberLine(props: GerberLineProps): JSX.Element { line >= Math.min(...highlightedLines) && line <= Math.max(...highlightedLines) + const element = useHighlight(highlight) + return (

{line} {text} @@ -127,7 +172,7 @@ function GerberNodeItem(props: GerberNodeProps): JSX.Element { const {node, highlightedLines, setHighlightedLines} = props const startLine = node.position?.start.line ?? null const endLine = node.position?.end.line ?? null - const lines = [startLine!, endLine!].filter(_ => _) + const lines = [startLine!, endLine!].filter(Boolean) const onMouseEnter = () => setHighlightedLines(lines) const onMouseLeave = () => setHighlightedLines([]) const highlight = highlightedLines.some( @@ -185,11 +230,14 @@ function TreeNode(props: TreeNodeProps): JSX.Element { return 0 }) + const element = useHighlight(highlight) + return (

type: {type}

    diff --git a/www/src/pages/index.page.tsx b/www/src/pages/index.page.tsx index 52139bf8..85548761 100644 --- a/www/src/pages/index.page.tsx +++ b/www/src/pages/index.page.tsx @@ -4,6 +4,7 @@ import { GerberContents, GerberParse, GerberPlot, + GerberSvg, } from '../components/gerber-render' const gerberFixtures = import.meta.glob( @@ -90,25 +91,27 @@ function Fixture(props: FixtureProps): JSX.Element { {sectionName}/{name}
    -

    Source

    -

    Parse

    -

    Plot

    +

    Source

    +

    Parse

    +

    Plot

    +

    Render

-
+
- + +
) diff --git a/www/tsconfig.json b/www/tsconfig.json index 8dd3d6af..d5c2e02b 100644 --- a/www/tsconfig.json +++ b/www/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../config/tsconfig.base.json", "references": [ {"path": "../packages/parser"}, - {"path": "../packages/plotter"} + {"path": "../packages/plotter"}, + {"path": "../packages/renderer"} ], "compilerOptions": { "composite": true, diff --git a/www/vite.config.ts b/www/vite.config.ts index 3c8834ae..49560131 100644 --- a/www/vite.config.ts +++ b/www/vite.config.ts @@ -11,4 +11,13 @@ export default defineConfig({ ...baseConfig, define: getDefineConstants(packageMeta), plugins: [preact(), mdx(), ssr(), windiCSS()], + // TODO(mc, 2022-05-28): remove this override when vite outputs ESM for SSR + // https://github.com/vitejs/vite/issues/8150 + build: { + rollupOptions: { + output: { + format: 'es', + }, + }, + }, })