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 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
@@ -0,0 +1,4 @@
1
+ export { CircuitJsonDocument } from './core/CircuitJsonDocument.mjs'
2
+ export { CircuitJsonIndexer } from './core/CircuitJsonIndexer.mjs'
3
+ export { CircuitJsonParser } from './core/CircuitJsonParser.mjs'
4
+ export { CircuitJsonUnits } from './core/CircuitJsonUnits.mjs'