-
-
Notifications
You must be signed in to change notification settings - Fork 103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(stats): collect drill stats #373
Open
KN4CK3R
wants to merge
20
commits into
tracespace:v5
Choose a base branch
from
KN4CK3R:stats
base: v5
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
64c57e7
feat(stats): collect drill stats
KN4CK3R aea388d
Fixed type errors.
KN4CK3R 650bfe7
Do not use instanceof.
KN4CK3R 480babd
lint
KN4CK3R 9f1bf35
Merge branch 'v5' of https://github.com/tracespace/tracespace into v5…
KN4CK3R 14a6cd4
Convert to simple method.
KN4CK3R 366871f
Added suggestions.
KN4CK3R 8d2b7ab
Merge branch 'v5' of https://github.com/tracespace/tracespace into v5…
KN4CK3R d7dc863
fixup: add @tracespace/stats to build system
mcous eaba8ab
Use Record instead of Map.
KN4CK3R bfe02ec
Moved code into single file.
KN4CK3R 9b73853
Refactored and added tests.
KN4CK3R 378d2ec
fixup: undo crlf change to lockfile
mcous fba5062
fixup: undo space to tab change in package.json
mcous 2bad149
Added suggestions.
KN4CK3R 6244690
Added tests.
KN4CK3R 53cf603
lint
KN4CK3R f5fb450
Merge branch 'v5' of https://github.com/tracespace/tracespace into v5…
KN4CK3R 1a20637
Added suggestions.
KN4CK3R a72a604
lint
KN4CK3R File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
*.tsbuildinfo | ||
__tests__ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# @tracespace/stats | ||
|
||
A drill stats collector for [@tracespace/parser][] ASTs. | ||
|
||
Part of the [tracespace][] collection of PCB visualization tools. | ||
|
||
**This package is still in development and is not yet published.** | ||
|
||
## usage | ||
|
||
```js | ||
import {createParser} from '@tracespace/parser' | ||
import {collectDrillStats} from '@tracespace/stats' | ||
|
||
const syntaxTree = createParser().feed(/* ...some gerber string... */).result() | ||
|
||
const stats = collectDrillStats([syntaxTree]) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
{ | ||
"name": "@tracespace/stats", | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"version": "0.0.0-unreleased", | ||
"description": "Collect drill statistics for PCB fabrication files.", | ||
"main": "./dist/tracespace-plotter.umd.cjs", | ||
"module": "./dist/tracespace-plotter.es.js", | ||
"types": "./lib/index.d.ts", | ||
"exports": { | ||
".": { | ||
"types": "./lib/index.d.ts", | ||
"source": "./src/index.ts", | ||
"import": "./dist/tracespace-stats.es.js", | ||
"require": "./dist/tracespace-stats.umd.cjs" | ||
} | ||
}, | ||
"type": "module", | ||
"sideEffects": false, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/tracespace/tracespace.git", | ||
"directory": "packages/stats" | ||
}, | ||
"scripts": { | ||
"build": "vite build", | ||
"clean": "rimraf dist" | ||
}, | ||
"keywords": [ | ||
"gerber", | ||
"excellon", | ||
"pcb", | ||
"circuit", | ||
"hardware", | ||
"electronics" | ||
], | ||
"contributors": [ | ||
"KN4CK3R <[email protected]> (https://www.oldschoolhack.me)", | ||
"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/parser": "workspace:*" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import {describe, it, expect} from 'vitest' | ||
import * as Parser from '@tracespace/parser' | ||
import {collectDrillStats, DrillStats} from '..' | ||
|
||
describe('@tracespace/stats', () => { | ||
const emptyStats: DrillStats = { | ||
drillHits: [], | ||
drillRoutes: [], | ||
totalDrills: 0, | ||
totalRoutes: 0, | ||
minDrillSize: 0, | ||
maxDrillSize: 0, | ||
} | ||
|
||
const tree1: Parser.GerberTree = { | ||
type: Parser.ROOT, | ||
filetype: Parser.DRILL, | ||
children: [ | ||
{ | ||
type: Parser.TOOL_DEFINITION, | ||
code: '1', | ||
shape: { | ||
type: Parser.CIRCLE, | ||
diameter: 1, | ||
}, | ||
}, | ||
{ | ||
type: Parser.GRAPHIC, | ||
graphic: null, | ||
}, | ||
{ | ||
type: Parser.INTERPOLATE_MODE, | ||
mode: Parser.DRILL, | ||
}, | ||
{ | ||
type: Parser.GRAPHIC, | ||
graphic: null, | ||
}, | ||
{ | ||
type: Parser.INTERPOLATE_MODE, | ||
mode: Parser.LINE, | ||
}, | ||
{ | ||
type: Parser.GRAPHIC, | ||
graphic: null, | ||
}, | ||
{ | ||
type: Parser.GRAPHIC, | ||
graphic: Parser.SLOT, | ||
}, | ||
] as Parser.ChildNode[], | ||
} | ||
|
||
it('should return an empty result', () => { | ||
expect(collectDrillStats([])).to.eql(emptyStats) | ||
}) | ||
|
||
it('should skip tree', () => { | ||
const tree: Parser.GerberTree = { | ||
type: Parser.ROOT, | ||
filetype: Parser.GERBER, | ||
children: [], | ||
} | ||
expect(collectDrillStats([tree])).to.eql(emptyStats) | ||
}) | ||
|
||
it('should ignore only tool definitions', () => { | ||
const tree: Parser.GerberTree = { | ||
type: Parser.ROOT, | ||
filetype: Parser.DRILL, | ||
children: [ | ||
{ | ||
type: Parser.TOOL_DEFINITION, | ||
code: '1', | ||
shape: { | ||
type: Parser.CIRCLE, | ||
diameter: 1, | ||
}, | ||
}, | ||
{ | ||
type: Parser.TOOL_DEFINITION, | ||
code: '2', | ||
shape: { | ||
type: Parser.RECTANGLE, | ||
xSize: 1, | ||
ySize: 1, | ||
}, | ||
}, | ||
] as Parser.ChildNode[], | ||
} | ||
|
||
expect(collectDrillStats([tree])).to.eql(emptyStats) | ||
}) | ||
|
||
it('should ignore unset tools', () => { | ||
const tree: Parser.GerberTree = { | ||
type: Parser.ROOT, | ||
filetype: Parser.DRILL, | ||
children: [ | ||
{ | ||
type: Parser.GRAPHIC, | ||
graphic: null, | ||
}, | ||
] as Parser.ChildNode[], | ||
} | ||
|
||
expect(collectDrillStats([tree])).to.eql(emptyStats) | ||
}) | ||
|
||
it('should collect drill stats', () => { | ||
const expected: DrillStats = { | ||
drillHits: [{count: 2, diameter: 1}], | ||
drillRoutes: [{count: 2, diameter: 1}], | ||
totalDrills: 2, | ||
totalRoutes: 2, | ||
minDrillSize: 1, | ||
maxDrillSize: 1, | ||
} | ||
|
||
expect(collectDrillStats([tree1])).to.eql(expected) | ||
}) | ||
|
||
it('should collect drill stats and combine them', () => { | ||
const tree2: Parser.GerberTree = { | ||
type: Parser.ROOT, | ||
filetype: Parser.DRILL, | ||
children: [ | ||
{ | ||
type: Parser.TOOL_DEFINITION, | ||
code: '2', | ||
shape: { | ||
type: Parser.CIRCLE, | ||
diameter: 2, | ||
}, | ||
}, | ||
{ | ||
type: Parser.GRAPHIC, | ||
graphic: null, | ||
}, | ||
] as Parser.ChildNode[], | ||
} | ||
|
||
const expected: DrillStats = { | ||
drillHits: [ | ||
{count: 2, diameter: 1}, | ||
{count: 1, diameter: 2}, | ||
], | ||
drillRoutes: [{count: 2, diameter: 1}], | ||
totalDrills: 3, | ||
totalRoutes: 2, | ||
minDrillSize: 1, | ||
maxDrillSize: 2, | ||
} | ||
|
||
expect(collectDrillStats([tree1, tree2])).to.eql(expected) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import * as Parser from '@tracespace/parser' | ||
|
||
export interface DrillUsage { | ||
diameter: number | ||
count: number | ||
} | ||
|
||
export interface DrillStats { | ||
drillHits: DrillUsage[] | ||
drillRoutes: DrillUsage[] | ||
totalDrills: number | ||
totalRoutes: number | ||
minDrillSize: number | ||
maxDrillSize: number | ||
} | ||
|
||
/** | ||
* Method to collect drill stats of parsed drill files. | ||
* | ||
* @example | ||
* ```ts | ||
* import {createParser} from '@tracespace/parser' | ||
* import {collectDrillStats} from '@tracespace/stats' | ||
* | ||
* const parser = createParser() | ||
* | ||
* parser.feed(...) | ||
* | ||
* const tree = parser.result() | ||
* const stats = collectDrillStats([tree]) | ||
* ``` | ||
* | ||
* @category Stats | ||
*/ | ||
export function collectDrillStats(trees: Parser.Root[]): DrillStats { | ||
const state: DrillStatsState = { | ||
usedTools: {}, | ||
drillsPerTool: {}, | ||
routesPerTool: {}, | ||
totalDrills: 0, | ||
totalRoutes: 0, | ||
minDrillSize: null, | ||
maxDrillSize: null, | ||
} | ||
|
||
for (const tree of trees) { | ||
if (tree.filetype !== Parser.DRILL) { | ||
continue | ||
} | ||
|
||
_updateDrillStats(state, tree) | ||
} | ||
|
||
const hits = Object.entries(state.drillsPerTool).map(([key, count]) => ({ | ||
diameter: state.usedTools[key], | ||
count, | ||
})) | ||
const routes = Object.entries(state.routesPerTool).map(([key, count]) => ({ | ||
diameter: state.usedTools[key], | ||
count, | ||
})) | ||
|
||
const stats: DrillStats = { | ||
drillHits: hits, | ||
drillRoutes: routes, | ||
totalDrills: state.totalDrills, | ||
totalRoutes: state.totalRoutes, | ||
minDrillSize: state.minDrillSize ?? 0, | ||
maxDrillSize: state.maxDrillSize ?? 0, | ||
} | ||
|
||
return stats | ||
} | ||
|
||
interface DrillStatsState { | ||
usedTools: Record<string, number> | ||
drillsPerTool: Record<string, number> | ||
routesPerTool: Record<string, number> | ||
totalDrills: number | ||
totalRoutes: number | ||
minDrillSize: number | null | ||
maxDrillSize: number | null | ||
} | ||
|
||
function _updateDrillStats(state: DrillStatsState, tree: Parser.Root) { | ||
let currentTool = null | ||
let currentMode: Parser.InterpolateModeType = Parser.DRILL | ||
for (const node of tree.children) { | ||
switch (node.type) { | ||
case Parser.TOOL_DEFINITION: | ||
currentTool = node.code | ||
|
||
if (node.shape.type === Parser.CIRCLE) { | ||
state.usedTools[currentTool] = node.shape.diameter | ||
} | ||
|
||
break | ||
case Parser.TOOL_CHANGE: | ||
currentTool = node.code | ||
break | ||
case Parser.INTERPOLATE_MODE: | ||
currentMode = node.mode | ||
break | ||
case Parser.GRAPHIC: { | ||
if (currentTool === null) { | ||
continue | ||
} | ||
|
||
if (state.usedTools[currentTool] !== undefined) { | ||
const diameter = state.usedTools[currentTool] | ||
state.minDrillSize = Math.min( | ||
diameter, | ||
state.minDrillSize ?? Number.POSITIVE_INFINITY | ||
) | ||
state.maxDrillSize = Math.max( | ||
diameter, | ||
state.maxDrillSize ?? Number.NEGATIVE_INFINITY | ||
) | ||
} | ||
|
||
if (node.graphic === null) { | ||
switch (currentMode) { | ||
case Parser.DRILL: { | ||
state.totalDrills++ | ||
const drillCount = state.drillsPerTool[currentTool] ?? 0 | ||
state.drillsPerTool[currentTool] = drillCount + 1 | ||
break | ||
} | ||
|
||
case Parser.LINE: | ||
case Parser.CW_ARC: | ||
case Parser.CCW_ARC: { | ||
state.totalRoutes++ | ||
const routeCount = state.routesPerTool[currentTool] ?? 0 | ||
state.routesPerTool[currentTool] = routeCount + 1 | ||
break | ||
} | ||
|
||
default: | ||
break | ||
} | ||
} else if (node.graphic === Parser.SLOT) { | ||
state.totalRoutes++ | ||
const routeCount = state.routesPerTool[currentTool] ?? 0 | ||
state.routesPerTool[currentTool] = routeCount + 1 | ||
} | ||
|
||
break | ||
} | ||
|
||
default: | ||
break | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Functions that mutate an input parameter to do their main work are usually a bad idea. I would solve this problem a different way, perhaps with a function that takes a single tree and returns the stats and another function that combines the stats
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about that too but the merging bloats the whole thing for not much value:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that
DrillStatsState
doesn't lend itself to merging, but it's also a completely internal interface. IfDrillStatsState
's shape is such that it leads to doing things the way_updateDrillStats
is currently written, I thinkDrillStatsState
should be changed