Skip to content

Commit

Permalink
feat(core): add tracespace core package
Browse files Browse the repository at this point in the history
  • Loading branch information
mcous committed Dec 27, 2022
1 parent 24d6d01 commit 20c6448
Show file tree
Hide file tree
Showing 25 changed files with 1,456 additions and 3 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"packageManager": "[email protected]",
"dependencies": {
"@tracespace/cli": "workspace:*",
"@tracespace/core": "workspace:*",
"@tracespace/fixtures": "workspace:*",
"@tracespace/identify-layers": "workspace:*",
"@tracespace/parser": "workspace:*",
Expand Down Expand Up @@ -137,6 +138,7 @@
"@typescript-eslint/consistent-type-assertions": "off",
"@typescript-eslint/consistent-type-imports": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"import/no-extraneous-dependencies": "off",
"max-nested-callbacks": "off",
"unicorn/no-array-for-each": "off",
Expand Down
42 changes: 42 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# @tracespace/core

The core tracespace read / parse / plot / render pipeline, built up of the following libraries:

- [@tracespace/identify-layers][]
- [@tracespace/parser][]
- [@tracespace/plotter][]
- [@tracespace/renderer][]

Part of the [tracespace][] collection of PCB visualization tools.

[tracespace]: https://github.com/tracespace/tracespace
[@tracespace/identify-layers]: ../identify-layers
[@tracespace/parser]: ../parser
[@tracespace/plotter]: ../plotter
[@tracespace/renderer]: ../renderer

## usage

```js
import fs from 'node:fs/promises
import {read, plot, render} from '@tracespace/parser'
const files = [
'top-copper.gbr',
'top-solder-mask.gbr',
'top-silk-screen.gbr',
'bottom-copper.gbr',
'bottom-solder-mask.gbr',
'outline.gbr',
'drill.xnc',
]
const readResult = await read(files)
const plotResult = plot(readResult)
const renderResult = plot(plotResult)
await Promise.all([
fs.writeFile('top.svg', renderResult.top.svg)
fs.writeFile('bottom.svg', renderResult.bottom.svg)
])
```
59 changes: 59 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"name": "@tracespace/core",
"publishConfig": {
"access": "public"
},
"version": "5.0.0-next.0",
"description": "Core tracespace render pipeline",
"types": "./lib/index.d.ts",
"exports": {
"types": "./lib/index.d.ts",
"source": "./src/index.ts",
"import": "./dist/tracespace-core.es.js",
"require": "./dist/tracespace-core.umd.cjs"
},
"files": [
"dist",
"lib",
"src",
"!**/__tests__/**"
],
"type": "module",
"sideEffects": false,
"repository": {
"type": "git",
"url": "git+https://github.com/tracespace/tracespace.git",
"directory": "packages/core"
},
"scripts": {
"build": "vite build"
},
"keywords": [
"gerber",
"excellon",
"pcb",
"circuit",
"hardware",
"electronics"
],
"contributors": [
"Mike Cousins <[email protected]> (https://mike.cousins.io)"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/tracespace/tracespace/issues"
},
"homepage": "https://github.com/tracespace/tracespace#readme",
"dependencies": {
"@tracespace/identify-layers": "workspace:*",
"@tracespace/parser": "workspace:*",
"@tracespace/plotter": "workspace:*",
"@tracespace/renderer": "workspace:*",
"@tracespace/xml-id": "workspace:*"
},
"devDependencies": {
"@types/lodash-es": "^4.17.6",
"hast-util-to-html": "^8.0.3",
"lodash-es": "^4.17.21"
}
}
55 changes: 55 additions & 0 deletions packages/core/src/__tests__/calculate-size.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {describe, beforeEach, afterEach, it, expect} from 'vitest'
import {replaceEsm, reset} from 'testdouble-vitest'
import * as td from 'testdouble'

import {MM} from '@tracespace/parser'

import type {ImageTree} from '@tracespace/plotter'

describe('calculate board size', () => {
let plotter: typeof import('@tracespace/plotter')
let subject: typeof import('../calculate-size')

beforeEach(async () => {
plotter = await replaceEsm('@tracespace/plotter')
subject = await import('../calculate-size')
})

afterEach(() => {
reset()
})

it('should return an empty size if given no input', () => {
td.when(plotter.BoundingBox.sum()).thenReturn([])

const result = subject.calculateSize([])

expect(result).to.eql([])
})

it('should sum up all layers if no outline', () => {
const plotTree1: ImageTree = {
type: plotter.IMAGE,
units: MM,
children: [
{type: plotter.IMAGE_LAYER, size: [0.1, 0.2, 0.3, 0.4], children: []},
],
}

const plotTree2: ImageTree = {
type: plotter.IMAGE,
units: MM,
children: [
{type: plotter.IMAGE_LAYER, size: [0.4, 0.3, 0.2, 0.1], children: []},
],
}

td.when(
plotter.BoundingBox.sum([0.1, 0.2, 0.3, 0.4], [0.4, 0.3, 0.2, 0.1])
).thenReturn([1, 2, 3, 4])

const result = subject.calculateSize([plotTree1, plotTree2])

expect(result).to.eql([1, 2, 3, 4])
})
})
209 changes: 209 additions & 0 deletions packages/core/src/__tests__/core.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// @vitest-environment jsdom
import {describe, beforeEach, afterEach, it, expect} from 'vitest'
import {replaceEsm, reset} from 'testdouble-vitest'
import * as td from 'testdouble'

import {
TYPE_COPPER,
TYPE_OUTLINE,
SIDE_ALL,
SIDE_TOP,
} from '@tracespace/identify-layers'

import type {GerberTree} from '@tracespace/parser'
import type {ImageTree} from '@tracespace/plotter'
import type {SvgElement} from '@tracespace/renderer'
import type {LayerIdentity} from '@tracespace/identify-layers'

import type {ReadResult, PlotResult} from '..'
import type {OutlineRender} from '../render-outline'

describe('tracespace core', () => {
let parser: typeof import('@tracespace/parser')
let plotter: typeof import('@tracespace/plotter')
let renderer: typeof import('@tracespace/renderer')
let xmlId: typeof import('@tracespace/xml-id')
let fileReader: typeof import('../read-file')
let sizeCalculator: typeof import('../calculate-size')
let layerTypeDeterminer: typeof import('../determine-layer-types')
let outlineRenderer: typeof import('../render-outline')
let svgStringifier: typeof import('../stringify-svg')
let layerSorter: typeof import('../sort-layers')
let subject: typeof import('..')

beforeEach(async () => {
parser = await replaceEsm('@tracespace/parser')
plotter = await replaceEsm('@tracespace/plotter')
renderer = await replaceEsm('@tracespace/renderer')
xmlId = await replaceEsm('@tracespace/xml-id')
fileReader = await replaceEsm('../read-file')
sizeCalculator = await replaceEsm('../calculate-size')
layerTypeDeterminer = await replaceEsm('../determine-layer-types')
outlineRenderer = await replaceEsm('../render-outline')
svgStringifier = await replaceEsm('../stringify-svg')
layerSorter = await replaceEsm('../sort-layers')
subject = await import('..')
})

afterEach(() => {
reset()
})

it('should read a set of files', async () => {
const files = [new File(['foo'], 'foo.gbr'), new File(['bar'], 'bar.gbr')]

const parseTreeFoo = {
type: parser.ROOT,
filetype: parser.GERBER,
} as GerberTree

const parseTreeBar = {
type: parser.ROOT,
filetype: parser.DRILL,
} as GerberTree

const layerTypes: Record<string, LayerIdentity> = {
'id-foo': {type: TYPE_COPPER, side: SIDE_TOP},
'id-bar': {type: TYPE_OUTLINE, side: SIDE_ALL},
}

td.when(xmlId.random()).thenReturn('id-foo', 'id-bar', '')
td.when(fileReader.readFile(files[0])).thenResolve('hello from')
td.when(fileReader.readFile(files[1])).thenResolve('the other side')
td.when(parser.parse('hello from')).thenReturn(parseTreeFoo)
td.when(parser.parse('the other side')).thenReturn(parseTreeBar)
td.when(
layerTypeDeterminer.determineLayerTypes([
{id: 'id-foo', filename: 'foo.gbr', parseTree: parseTreeFoo},
{id: 'id-bar', filename: 'bar.gbr', parseTree: parseTreeBar},
])
).thenReturn(layerTypes)

const result = await subject.read(files)

expect(result).to.eql({
layers: [
{id: 'id-foo', filename: 'foo.gbr', type: TYPE_COPPER, side: SIDE_TOP},
{id: 'id-bar', filename: 'bar.gbr', type: TYPE_OUTLINE, side: SIDE_ALL},
],
parseTreesById: {'id-foo': parseTreeFoo, 'id-bar': parseTreeBar},
})
})

it('should plot a set of read and parsed layers', () => {
const parseTreeFoo = {
type: parser.ROOT,
filetype: parser.GERBER,
} as GerberTree

const parseTreeBar = {
type: parser.ROOT,
filetype: parser.DRILL,
} as GerberTree

const readResult: ReadResult = {
layers: [
{id: 'id-foo', filename: 'foo.gbr', type: TYPE_COPPER, side: SIDE_TOP},
{id: 'id-bar', filename: 'bar.gbr', type: TYPE_OUTLINE, side: SIDE_ALL},
],
parseTreesById: {'id-foo': parseTreeFoo, 'id-bar': parseTreeBar},
}

const plotTreeFoo = {type: plotter.IMAGE, units: parser.MM} as ImageTree
const plotTreeBar = {type: plotter.IMAGE, units: parser.IN} as ImageTree

td.when(plotter.plot(parseTreeFoo)).thenReturn(plotTreeFoo)
td.when(plotter.plot(parseTreeBar)).thenReturn(plotTreeBar)
td.when(
sizeCalculator.calculateSize([plotTreeFoo, plotTreeBar])
).thenReturn([1, 2, 3, 4])

const result = subject.plot(readResult)

expect(result).to.eql({
layers: [
{id: 'id-foo', filename: 'foo.gbr', type: TYPE_COPPER, side: SIDE_TOP},
{id: 'id-bar', filename: 'bar.gbr', type: TYPE_OUTLINE, side: SIDE_ALL},
],
size: [1, 2, 3, 4],
plotTreesById: {'id-foo': plotTreeFoo, 'id-bar': plotTreeBar},
})
})

it('should render a set of plotted layers', () => {
const plotTreeFoo = {type: plotter.IMAGE, units: parser.MM} as ImageTree
const plotTreeBar = {type: plotter.IMAGE, units: parser.IN} as ImageTree
const plotResult: PlotResult = {
layers: [
{id: 'id-foo', filename: 'foo.gbr', type: TYPE_COPPER, side: SIDE_TOP},
{id: 'id-bar', filename: 'bar.gbr', type: TYPE_OUTLINE, side: SIDE_ALL},
],
size: [1, 2, 3, 4],
plotTreesById: {'id-foo': plotTreeFoo, 'id-bar': plotTreeBar},
}

const svgTreeFoo: SvgElement = {
type: 'element',
tagName: 'foo',
children: [],
}
const svgTreeBar: SvgElement = {
type: 'element',
tagName: 'bar',
children: [],
}

const mechanicalLayers = {
drill: ['id-foo'],
outline: 'id-bar',
}
const topLayers = {
copper: ['id-foo'],
solderMask: ['id-foo'],
silkScreen: ['id-foo'],
solderPaste: ['id-foo'],
}
const bottomLayers = {
copper: ['id-bar'],
solderMask: ['id-bar'],
silkScreen: ['id-bar'],
solderPaste: ['id-bar'],
}

const outlineRender: OutlineRender = {
svgFragment: '<g id="outline"/>',
viewBox: [5, 6, 7, 8],
}

td.when(renderer.renderFragment(plotTreeFoo)).thenReturn(svgTreeFoo)
td.when(renderer.renderFragment(plotTreeBar)).thenReturn(svgTreeBar)
td.when(svgStringifier.stringifySvg(svgTreeFoo)).thenReturn('<g id="foo"/>')
td.when(svgStringifier.stringifySvg(svgTreeBar)).thenReturn('<g id="bar"/>')
td.when(layerSorter.getMechanicalLayers(plotResult.layers)).thenReturn(
mechanicalLayers
)
td.when(layerSorter.getSideLayers('top', plotResult.layers)).thenReturn(
topLayers
)
td.when(layerSorter.getSideLayers('bottom', plotResult.layers)).thenReturn(
bottomLayers
)
td.when(
outlineRenderer.renderOutline(plotTreeBar, [1, 2, 3, 4], 0.02)
).thenReturn(outlineRender)

const result = subject.renderFragments(plotResult)

expect(result).to.eql({
layers: plotResult.layers,
topLayers,
bottomLayers,
mechanicalLayers,
outlineRender,
svgFragmentsById: {
'id-foo': '<g id="foo"/>',
'id-bar': '<g id="bar"/>',
},
})
})
})

0 comments on commit 20c6448

Please sign in to comment.