circuitjson-toolkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +51 -0
- package/README.md +24 -0
- package/package.json +36 -0
- package/src/core/CircuitJsonDocument.mjs +80 -0
- package/src/core/CircuitJsonIndexer.mjs +62 -0
- package/src/core/CircuitJsonParser.mjs +55 -0
- package/src/core/CircuitJsonUnits.mjs +50 -0
- package/src/index.mjs +4 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# AGENTS
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
- Repository: `CircuitJSON Toolkit` JavaScript library.
|
|
6
|
+
- Source is in `src/`.
|
|
7
|
+
- Tests are in `tests/`.
|
|
8
|
+
- Specifications are in `spec/`.
|
|
9
|
+
- Documentation is in `docs/`.
|
|
10
|
+
- The package contains dependency-free CircuitJSON validation, indexing, and
|
|
11
|
+
utility helpers for browser and Node consumers.
|
|
12
|
+
|
|
13
|
+
## Build, Run, Test
|
|
14
|
+
|
|
15
|
+
- Install: `npm install`
|
|
16
|
+
- Test: `npm test`
|
|
17
|
+
- Format: `npm run format`
|
|
18
|
+
- Check formatting: `npm run check:format`
|
|
19
|
+
|
|
20
|
+
## Coding Style & Naming Conventions
|
|
21
|
+
|
|
22
|
+
- Prettier settings are in `.prettierrc.json`: 4-space indent, single quotes,
|
|
23
|
+
no semicolons, no trailing commas.
|
|
24
|
+
- Keep files under 1000 lines; split modules/classes when they grow.
|
|
25
|
+
- Add JSDoc for every function/method, including private helpers.
|
|
26
|
+
- Add inline comments only where non-obvious behavior needs context.
|
|
27
|
+
- Utility modules should use class-based organization with static methods when
|
|
28
|
+
appropriate.
|
|
29
|
+
- For single-class modules, name the `.mjs` file in CamelCase to match the
|
|
30
|
+
class name.
|
|
31
|
+
- For private internals, use ECMAScript private elements.
|
|
32
|
+
- Prefer `async/await` for naturally asynchronous operations.
|
|
33
|
+
|
|
34
|
+
## Library Scope
|
|
35
|
+
|
|
36
|
+
- Include CircuitJSON element-array validation, parsing, indexing, unit helpers,
|
|
37
|
+
and small summary helpers.
|
|
38
|
+
- Do not include renderer code, Three.js code, ECAD parser logic, UI wiring, or
|
|
39
|
+
source-format-specific compatibility adapters.
|
|
40
|
+
|
|
41
|
+
## Testing Guidelines
|
|
42
|
+
|
|
43
|
+
- Use repo scripts only: `npm test`.
|
|
44
|
+
- For every feature/fix/behavior change, add or update tests in `tests/`.
|
|
45
|
+
- Keep tests focused on observable CircuitJSON utility behavior.
|
|
46
|
+
- Tests must use small fake CircuitJSON samples only.
|
|
47
|
+
|
|
48
|
+
## Security & Configuration Tips
|
|
49
|
+
|
|
50
|
+
- Treat CircuitJSON files as untrusted input.
|
|
51
|
+
- Keep helpers local-first and dependency-free by default.
|
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# CircuitJSON Toolkit
|
|
2
|
+
|
|
3
|
+
Dependency-free utilities for working with serialized CircuitJSON element
|
|
4
|
+
arrays.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import {
|
|
10
|
+
CircuitJsonDocument,
|
|
11
|
+
CircuitJsonIndexer,
|
|
12
|
+
CircuitJsonParser
|
|
13
|
+
} from 'circuitjson-toolkit'
|
|
14
|
+
|
|
15
|
+
const circuitJson = CircuitJsonParser.parseText(fileText)
|
|
16
|
+
CircuitJsonDocument.assertModel(circuitJson)
|
|
17
|
+
|
|
18
|
+
const index = CircuitJsonIndexer.index(circuitJson)
|
|
19
|
+
console.log(index.elementsByType.get('pcb_board'))
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## License
|
|
23
|
+
|
|
24
|
+
AGPL-3.0-or-later.
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "circuitjson-toolkit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Dependency-free CircuitJSON validation and indexing utilities",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"circuitjson",
|
|
7
|
+
"circuit-json",
|
|
8
|
+
"pcb",
|
|
9
|
+
"eda",
|
|
10
|
+
"ecad"
|
|
11
|
+
],
|
|
12
|
+
"license": "AGPL-3.0-or-later",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./src/index.mjs",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"docs",
|
|
21
|
+
"spec",
|
|
22
|
+
"README.md",
|
|
23
|
+
"AGENTS.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "node --test",
|
|
27
|
+
"format": "prettier --write .",
|
|
28
|
+
"check:format": "prettier --check ."
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"prettier": "^3.4.2"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates serialized CircuitJSON element arrays.
|
|
3
|
+
*/
|
|
4
|
+
export class CircuitJsonDocument {
|
|
5
|
+
/**
|
|
6
|
+
* Returns true when a value is a CircuitJSON element.
|
|
7
|
+
* @param {unknown} value Candidate value.
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
static isElement(value) {
|
|
11
|
+
return (
|
|
12
|
+
Boolean(value) &&
|
|
13
|
+
typeof value === 'object' &&
|
|
14
|
+
typeof value.type === 'string' &&
|
|
15
|
+
value.type.trim().length > 0
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns true when a value is a CircuitJSON element array.
|
|
21
|
+
* @param {unknown} value Candidate model.
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
static isModel(value) {
|
|
25
|
+
return (
|
|
26
|
+
Array.isArray(value) &&
|
|
27
|
+
value.every((element) => CircuitJsonDocument.isElement(element))
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Throws when a value is not a CircuitJSON element array.
|
|
33
|
+
* @param {unknown} value Candidate model.
|
|
34
|
+
* @returns {void}
|
|
35
|
+
*/
|
|
36
|
+
static assertModel(value) {
|
|
37
|
+
if (!CircuitJsonDocument.isModel(value)) {
|
|
38
|
+
throw new TypeError('Expected a CircuitJSON element array.')
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Attaches non-serialized metadata to a CircuitJSON array.
|
|
44
|
+
* @template {object[]} T
|
|
45
|
+
* @param {T} circuitJson CircuitJSON model.
|
|
46
|
+
* @param {{ fileName?: string, fileType?: string, kind?: string }} [metadata]
|
|
47
|
+
* @returns {T}
|
|
48
|
+
*/
|
|
49
|
+
static attachMetadata(circuitJson, metadata = {}) {
|
|
50
|
+
CircuitJsonDocument.assertModel(circuitJson)
|
|
51
|
+
Object.defineProperties(circuitJson, {
|
|
52
|
+
fileName: {
|
|
53
|
+
configurable: true,
|
|
54
|
+
enumerable: true,
|
|
55
|
+
value: String(metadata.fileName || ''),
|
|
56
|
+
writable: true
|
|
57
|
+
},
|
|
58
|
+
fileType: {
|
|
59
|
+
configurable: true,
|
|
60
|
+
enumerable: true,
|
|
61
|
+
value: String(metadata.fileType || 'circuitjson'),
|
|
62
|
+
writable: true
|
|
63
|
+
},
|
|
64
|
+
kind: {
|
|
65
|
+
configurable: true,
|
|
66
|
+
enumerable: true,
|
|
67
|
+
value: String(metadata.kind || 'pcb'),
|
|
68
|
+
writable: true
|
|
69
|
+
},
|
|
70
|
+
sourceFormat: {
|
|
71
|
+
configurable: true,
|
|
72
|
+
enumerable: true,
|
|
73
|
+
value: 'circuitjson',
|
|
74
|
+
writable: true
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return circuitJson
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { CircuitJsonDocument } from './CircuitJsonDocument.mjs'
|
|
2
|
+
|
|
3
|
+
const ID_FIELDS_BY_TYPE = {
|
|
4
|
+
pcb_board: 'pcb_board_id',
|
|
5
|
+
pcb_component: 'pcb_component_id',
|
|
6
|
+
pcb_hole: 'pcb_hole_id',
|
|
7
|
+
pcb_plated_hole: 'pcb_plated_hole_id',
|
|
8
|
+
pcb_port: 'pcb_port_id',
|
|
9
|
+
pcb_smtpad: 'pcb_smtpad_id',
|
|
10
|
+
pcb_trace: 'pcb_trace_id',
|
|
11
|
+
pcb_via: 'pcb_via_id',
|
|
12
|
+
source_component: 'source_component_id',
|
|
13
|
+
source_net: 'source_net_id',
|
|
14
|
+
source_port: 'source_port_id',
|
|
15
|
+
source_trace: 'source_trace_id'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Builds lookup maps for CircuitJSON element arrays.
|
|
20
|
+
*/
|
|
21
|
+
export class CircuitJsonIndexer {
|
|
22
|
+
/**
|
|
23
|
+
* Indexes a CircuitJSON model.
|
|
24
|
+
* @param {object[]} circuitJson CircuitJSON model.
|
|
25
|
+
* @returns {{ elements: object[], elementsByType: Map<string, object[]>, elementsById: Map<string, object>, sourceComponentById: Map<string, object>, pcbComponentById: Map<string, object> }}
|
|
26
|
+
*/
|
|
27
|
+
static index(circuitJson) {
|
|
28
|
+
CircuitJsonDocument.assertModel(circuitJson)
|
|
29
|
+
const elementsByType = new Map()
|
|
30
|
+
const elementsById = new Map()
|
|
31
|
+
const sourceComponentById = new Map()
|
|
32
|
+
const pcbComponentById = new Map()
|
|
33
|
+
|
|
34
|
+
circuitJson.forEach((element) => {
|
|
35
|
+
const type = String(element?.type || '')
|
|
36
|
+
if (!elementsByType.has(type)) {
|
|
37
|
+
elementsByType.set(type, [])
|
|
38
|
+
}
|
|
39
|
+
elementsByType.get(type).push(element)
|
|
40
|
+
|
|
41
|
+
const idField = ID_FIELDS_BY_TYPE[type]
|
|
42
|
+
const id = idField ? String(element?.[idField] || '') : ''
|
|
43
|
+
if (id) {
|
|
44
|
+
elementsById.set(`${type}:${id}`, element)
|
|
45
|
+
}
|
|
46
|
+
if (type === 'source_component' && id) {
|
|
47
|
+
sourceComponentById.set(id, element)
|
|
48
|
+
}
|
|
49
|
+
if (type === 'pcb_component' && id) {
|
|
50
|
+
pcbComponentById.set(id, element)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
elements: circuitJson,
|
|
56
|
+
elementsByType,
|
|
57
|
+
elementsById,
|
|
58
|
+
sourceComponentById,
|
|
59
|
+
pcbComponentById
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CircuitJsonDocument } from './CircuitJsonDocument.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parses standalone CircuitJSON files.
|
|
5
|
+
*/
|
|
6
|
+
export class CircuitJsonParser {
|
|
7
|
+
/**
|
|
8
|
+
* Parses standalone CircuitJSON text.
|
|
9
|
+
* @param {string} text JSON text.
|
|
10
|
+
* @param {{ fileName?: string }} [options] Parse metadata.
|
|
11
|
+
* @returns {object[]}
|
|
12
|
+
*/
|
|
13
|
+
static parseText(text, options = {}) {
|
|
14
|
+
let parsed
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(String(text || ''))
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new SyntaxError(
|
|
19
|
+
'CircuitJSON file is not valid JSON: ' +
|
|
20
|
+
String(error?.message || error || 'Unknown error.')
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
CircuitJsonDocument.assertModel(parsed)
|
|
25
|
+
return CircuitJsonDocument.attachMetadata(parsed, {
|
|
26
|
+
fileName: options.fileName || '',
|
|
27
|
+
fileType: 'circuitjson',
|
|
28
|
+
kind: CircuitJsonParser.#resolveKind(parsed)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parses standalone CircuitJSON bytes.
|
|
34
|
+
* @param {ArrayBuffer | Uint8Array} bytes File bytes.
|
|
35
|
+
* @param {{ fileName?: string }} [options] Parse metadata.
|
|
36
|
+
* @returns {object[]}
|
|
37
|
+
*/
|
|
38
|
+
static parseBytes(bytes, options = {}) {
|
|
39
|
+
const view =
|
|
40
|
+
bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || [])
|
|
41
|
+
const text = new TextDecoder().decode(view)
|
|
42
|
+
return CircuitJsonParser.parseText(text, options)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolves a broad document kind from available elements.
|
|
47
|
+
* @param {object[]} model CircuitJSON model.
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
static #resolveKind(model) {
|
|
51
|
+
return model.some((element) => String(element?.type) === 'pcb_board')
|
|
52
|
+
? 'pcb'
|
|
53
|
+
: 'circuitjson'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const MILS_PER_MM = 39.37007874015748
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unit helpers for CircuitJSON's millimeter-based PCB dimensions.
|
|
5
|
+
*/
|
|
6
|
+
export class CircuitJsonUnits {
|
|
7
|
+
/**
|
|
8
|
+
* Converts millimeters to mils.
|
|
9
|
+
* @param {unknown} value Millimeter value.
|
|
10
|
+
* @param {number} [fallback] Fallback millimeter value.
|
|
11
|
+
* @returns {number}
|
|
12
|
+
*/
|
|
13
|
+
static mmToMil(value, fallback = 0) {
|
|
14
|
+
return CircuitJsonUnits.#round(
|
|
15
|
+
CircuitJsonUnits.#number(value, fallback) * MILS_PER_MM
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Converts a CircuitJSON point from millimeters to mils.
|
|
21
|
+
* @param {{ x?: unknown, y?: unknown } | null | undefined} point Point.
|
|
22
|
+
* @returns {{ x: number, y: number }}
|
|
23
|
+
*/
|
|
24
|
+
static pointMmToMil(point) {
|
|
25
|
+
return {
|
|
26
|
+
x: CircuitJsonUnits.mmToMil(point?.x, 0),
|
|
27
|
+
y: CircuitJsonUnits.mmToMil(point?.y, 0)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Converts a value to a finite number.
|
|
33
|
+
* @param {unknown} value Candidate number.
|
|
34
|
+
* @param {number} fallback Fallback number.
|
|
35
|
+
* @returns {number}
|
|
36
|
+
*/
|
|
37
|
+
static #number(value, fallback) {
|
|
38
|
+
const numeric = Number(value)
|
|
39
|
+
return Number.isFinite(numeric) ? numeric : fallback
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Rounds render-unit conversions to stable precision.
|
|
44
|
+
* @param {number} value Numeric value.
|
|
45
|
+
* @returns {number}
|
|
46
|
+
*/
|
|
47
|
+
static #round(value) {
|
|
48
|
+
return Math.round(value * 1_000_000) / 1_000_000
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.mjs
ADDED