circuitjson-toolkit 1.1.2 → 1.2.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/README.md CHANGED
@@ -49,6 +49,12 @@ theme fallback, and generic component and symbol bodies explicitly use the
49
49
  ECAD Forge schematic palette. See the
50
50
  [1.1.2 release notes](docs/release-notes-v1.1.2.md).
51
51
 
52
+ Version 1.2.0 extends the canonical PCB contract with validated source text
53
+ fidelity, rotated drilled-pad bounds, and rounded-pad corner radii. Worker and
54
+ metadata ownership traversals now support deeply nested, valid ECAD documents
55
+ up to 256 levels. See the
56
+ [1.2.0 release notes](docs/release-notes-v1.2.0.md).
57
+
52
58
  Before 1.1.0:
53
59
 
54
60
  ```js
@@ -82,6 +88,8 @@ const model = document.model
82
88
  defaults
83
89
  - Asset-backed `schematic_image` rows and hierarchical
84
90
  `schematic_sheet_symbol` rows with shared bounds and SVG behavior
91
+ - Validated PCB text source-fidelity fields for independent font dimensions,
92
+ exact nine-position anchors, visibility, layer provenance, and native type
85
93
  - Reusable exact PCB interaction and spatial indexes
86
94
  - Query, manufacturing, and injected simulation services
87
95
  - Data-only, millimeter-based, right-handed Z-up PCB 3D scenes
@@ -310,6 +318,7 @@ copy while keeping sync, direct async, and worker results mutation-isolated.
310
318
  - [1.1.0 release notes](docs/release-notes-v1.1.0.md)
311
319
  - [1.1.1 release notes](docs/release-notes-v1.1.1.md)
312
320
  - [1.1.2 release notes](docs/release-notes-v1.1.2.md)
321
+ - [1.2.0 release notes](docs/release-notes-v1.2.0.md)
313
322
  - [Library scope](spec/library-scope.md)
314
323
 
315
324
  ## Package scope
package/docs/api.md CHANGED
@@ -144,6 +144,15 @@ The canonical union is the pinned upstream schema plus the source-neutral
144
144
  snapshot, `canonicalElementTypes()` for all accepted types, and
145
145
  `extensionElementTypes()` for the toolkit-owned additions.
146
146
 
147
+ Pinned PCB text rows may also retain the validated source-fidelity fields
148
+ `font_width`, `font_height`, `stroke_width`, `source_anchor_alignment`,
149
+ `is_hidden`, `source_layer`, `source_type`, and `source_text_kind`. Board note
150
+ rotation uses `ccw_rotation`; fabrication note mirroring uses `is_mirrored`.
151
+ Canonical upstream properties remain authoritative when they exist. In
152
+ particular, `pcb_note_text.anchor_alignment` keeps the narrower upstream enum,
153
+ while `source_anchor_alignment` can preserve the exact nine-position source
154
+ anchor for lossless rendering.
155
+
147
156
  The projection covers legacy schematic table row/column/span geometry, PCB
148
157
  artwork `points`/`width` paths, pad-clearance diagnostic relations, courtyard
149
158
  line/path/polygon forms, outer-layer aliases, and retained stroke dash fields.
@@ -157,7 +166,12 @@ rendering and interaction consumers. Polygon `pad_outline` points determine
157
166
  rotation-local outer width and height; pill drill width, height, diameter, and
158
167
  board-space rotation remain distinct from outer-pad rotation. Legal
159
168
  `outer_width`, `outer_height`, `rect_ccw_rotation`, and `hole_ccw_rotation`
160
- variants are preserved. Import this retained source-neutral helper from
169
+ variants are preserved. The returned geometry includes the parsed rectangular
170
+ `cornerRadius`. Rotated rectangular bounds use the exact visible
171
+ rounded-rectangle support dimensions, with the effective radius clamped from
172
+ zero through half the smaller outer dimension. A zero radius therefore retains
173
+ sharp-rectangle bounds, while a half-minor-dimension radius has the same support
174
+ extent as a pill. Import this retained source-neutral helper from
161
175
  `circuitjson-toolkit/extensions`.
162
176
 
163
177
  Packed release checks reject any missing or additional root export.
@@ -119,6 +119,40 @@ validation/proof boundary. Table cell geometry, PCB artwork routes,
119
119
  pad-clearance relations, courtyards, layer aliases, and stroke dashes are
120
120
  derived structurally and never from a source filename or fixture identity.
121
121
 
122
+ ### PCB text source fidelity
123
+
124
+ `pcb_note_text`, `pcb_fabrication_note_text`, and `pcb_silkscreen_text` retain
125
+ canonical upstream fields first. Source formats with independent width and
126
+ height, edge-center anchors, native layer names, or hidden-text state may add:
127
+
128
+ ```js
129
+ {
130
+ type: 'pcb_note_text',
131
+ pcb_note_text_id: 'board_text_1',
132
+ text: 'BOARD MARK',
133
+ anchor_position: { x: 12.5, y: 4.25 },
134
+ layer: 'bottom',
135
+ ccw_rotation: 28,
136
+ font_size: 1.2,
137
+ font_width: 0.8,
138
+ font_height: 1.2,
139
+ stroke_width: 0.12,
140
+ anchor_alignment: 'center',
141
+ source_anchor_alignment: 'center_left',
142
+ is_mirrored_from_top_view: true,
143
+ is_hidden: false,
144
+ source_layer: 'B.SilkS',
145
+ source_type: 'gr_text'
146
+ }
147
+ ```
148
+
149
+ The extension fields are validated and preserved through immutable document
150
+ preparation. Lengths accept the same numeric millimeter or unit-suffixed style
151
+ as canonical dimensions. `source_anchor_alignment` accepts all nine standard
152
+ anchor positions. The narrower upstream anchor on board and fabrication notes
153
+ must remain `center` or a corner; renderers use the source anchor when they
154
+ need the exact edge-center placement.
155
+
122
156
  ## Assets and diagnostics
123
157
 
124
158
  Assets have exact `id`, `kind`, `name`, `mediaType`, `byteLength`, `data`, and
@@ -0,0 +1,33 @@
1
+ # circuitjson-toolkit 1.2.0
2
+
3
+ ## Canonical PCB fidelity
4
+
5
+ This minor release expands the shared CircuitJSON contract used by the Gerber,
6
+ Altium, KiCad, and 3D viewer packages. Existing canonical fields remain
7
+ authoritative, while source formats can retain exact rendering information in
8
+ validated, source-neutral extension fields.
9
+
10
+ ### API additions
11
+
12
+ - `pcb_note_text`, `pcb_fabrication_note_text`, and `pcb_silkscreen_text` may
13
+ retain independent `font_width` and `font_height`, `stroke_width`, the exact
14
+ nine-position `source_anchor_alignment`, `is_hidden`, `source_layer`,
15
+ `source_type`, and `source_text_kind`.
16
+ - Board-note `ccw_rotation` and fabrication-note `is_mirrored` are validated
17
+ when present, so downstream renderers can consume the original orientation
18
+ without source-format adapters.
19
+ - `CircuitJsonPcbHolePrimitiveModel.build()` now returns `cornerRadius` and
20
+ computes board-space bounds for rotated rectangles and pills instead of
21
+ treating their local width and height as axis-aligned.
22
+
23
+ ### Behavior and performance
24
+
25
+ - Structured metadata snapshots and worker request graphs accept valid nesting
26
+ up to 256 levels, matching the deeper native data graphs emitted by ECAD
27
+ parsers while retaining the existing item and byte limits.
28
+ - The new fields pass through the existing immutable document ownership and
29
+ validation boundaries; no viewer or host-app workaround is required.
30
+
31
+ No existing public class, method, package subpath, parameter, or document
32
+ envelope is removed in this release. Consumers that exhaustively validate PCB
33
+ text or primitive return objects should accept the additive fields above.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circuitjson-toolkit",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Canonical CircuitJSON parsing, project, rendering, query, manufacturing, simulation, and scene contracts",
5
5
  "keywords": [
6
6
  "circuitjson",
@@ -53,6 +53,7 @@
53
53
  "docs/release-notes-v1.1.0.md",
54
54
  "docs/release-notes-v1.1.1.md",
55
55
  "docs/release-notes-v1.1.2.md",
56
+ "docs/release-notes-v1.2.0.md",
56
57
  "docs/testing.md",
57
58
  "spec",
58
59
  "LICENSE",
@@ -29,19 +29,25 @@ export class CircuitJsonPcbHolePrimitiveModel {
29
29
  shape,
30
30
  size
31
31
  )
32
- const bounds = points.length
33
- ? CircuitJsonPcbPrimitiveGeometry.pointsBounds(points)
34
- : CircuitJsonPcbPrimitiveGeometry.centerBounds(
35
- center,
36
- size.width,
37
- size.height
38
- )
32
+ const cornerRadius = CircuitJsonUnits.length(
33
+ element.rect_border_radius ?? element.corner_radius,
34
+ 0
35
+ )
36
+ const bounds = CircuitJsonPcbHolePrimitiveModel.#outerBounds(
37
+ center,
38
+ size,
39
+ shape,
40
+ rotation,
41
+ cornerRadius,
42
+ points
43
+ )
39
44
 
40
45
  return {
41
46
  shape,
42
47
  width: size.width,
43
48
  height: size.height,
44
49
  diameter: Math.max(size.width, size.height),
50
+ cornerRadius,
45
51
  holeShape: hole.shape,
46
52
  holeDiameter: hole.diameter,
47
53
  holeWidth: hole.width,
@@ -123,6 +129,64 @@ export class CircuitJsonPcbHolePrimitiveModel {
123
129
  }
124
130
  }
125
131
 
132
+ /**
133
+ * Resolves the board-space axis-aligned bounds of the visible outer shape.
134
+ * @param {{ x: number, y: number }} center Shape center.
135
+ * @param {{ width: number, height: number }} size Local shape size.
136
+ * @param {'circle' | 'pill' | 'polygon' | 'rect'} shape Shape kind.
137
+ * @param {number} rotation Counter-clockwise rotation in degrees.
138
+ * @param {number} cornerRadius Parsed rectangular corner radius.
139
+ * @param {{ x: number, y: number }[]} points Polygon points.
140
+ * @returns {object} Board-space axis-aligned bounds.
141
+ */
142
+ static #outerBounds(center, size, shape, rotation, cornerRadius, points) {
143
+ if (points.length) {
144
+ return CircuitJsonPcbPrimitiveGeometry.pointsBounds(points)
145
+ }
146
+ if (shape === 'circle' || rotation === 0) {
147
+ return CircuitJsonPcbPrimitiveGeometry.centerBounds(
148
+ center,
149
+ size.width,
150
+ size.height
151
+ )
152
+ }
153
+
154
+ const radians = (rotation * Math.PI) / 180
155
+ if (shape === 'pill') {
156
+ const minor = Math.min(size.width, size.height)
157
+ const lineLength = Math.max(size.width, size.height) - minor
158
+ const axisRadians =
159
+ radians + (size.height > size.width ? Math.PI / 2 : 0)
160
+ const width = Math.abs(Math.cos(axisRadians)) * lineLength + minor
161
+ const height = Math.abs(Math.sin(axisRadians)) * lineLength + minor
162
+ return CircuitJsonPcbPrimitiveGeometry.centerBounds(
163
+ center,
164
+ width,
165
+ height
166
+ )
167
+ }
168
+
169
+ const radius = Math.max(
170
+ 0,
171
+ Math.min(cornerRadius, Math.min(size.width, size.height) / 2)
172
+ )
173
+ const innerWidth = size.width - radius * 2
174
+ const innerHeight = size.height - radius * 2
175
+ const width =
176
+ Math.abs(Math.cos(radians)) * innerWidth +
177
+ Math.abs(Math.sin(radians)) * innerHeight +
178
+ radius * 2
179
+ const height =
180
+ Math.abs(Math.sin(radians)) * innerWidth +
181
+ Math.abs(Math.cos(radians)) * innerHeight +
182
+ radius * 2
183
+ return CircuitJsonPcbPrimitiveGeometry.centerBounds(
184
+ center,
185
+ width,
186
+ height
187
+ )
188
+ }
189
+
126
190
  /**
127
191
  * Resolves polygonal outer pad points.
128
192
  * @param {object} element Drilled PCB element.
@@ -1,7 +1,25 @@
1
+ import { optionalAngle, optionalLength } from './CircuitJsonUnitParsers.mjs'
2
+
1
3
  const TOOLKIT_ELEMENT_TYPES = new Set([
2
4
  'schematic_image',
3
5
  'schematic_sheet_symbol'
4
6
  ])
7
+ const PCB_TEXT_EXTENSION_TYPES = new Set([
8
+ 'pcb_note_text',
9
+ 'pcb_fabrication_note_text',
10
+ 'pcb_silkscreen_text'
11
+ ])
12
+ const PCB_TEXT_ANCHOR_ALIGNMENTS = new Set([
13
+ 'top_left',
14
+ 'top_center',
15
+ 'top_right',
16
+ 'center_left',
17
+ 'center',
18
+ 'center_right',
19
+ 'bottom_left',
20
+ 'bottom_center',
21
+ 'bottom_right'
22
+ ])
5
23
 
6
24
  /**
7
25
  * Validates canonical toolkit element types that are newer than the pinned
@@ -45,6 +63,22 @@ export class CircuitJsonToolkitElementSchema {
45
63
  ]
46
64
  }
47
65
 
66
+ /**
67
+ * Validates source-fidelity fields retained on pinned upstream elements.
68
+ * @param {Record<string, any>} value Element value.
69
+ * @param {string} type Element type.
70
+ * @param {string} [location] Human-readable location suffix.
71
+ * @returns {string[]} Empty on success or one validation error.
72
+ */
73
+ static validateExtensions(value, type, location = '') {
74
+ if (!PCB_TEXT_EXTENSION_TYPES.has(type)) return []
75
+ return CircuitJsonToolkitElementSchema.#pcbTextExtensions(value, type)
76
+ ? []
77
+ : [
78
+ `CircuitJSON element ${type}${location} does not match the canonical toolkit extension schema.`
79
+ ]
80
+ }
81
+
48
82
  /**
49
83
  * Validates an asset-backed schematic image.
50
84
  * @param {Record<string, any>} value Image element.
@@ -156,6 +190,68 @@ export class CircuitJsonToolkitElementSchema {
156
190
  )
157
191
  }
158
192
 
193
+ /**
194
+ * Validates exact PCB text dimensions, alignment, visibility, and source provenance.
195
+ * @param {Record<string, any>} value Text element.
196
+ * @param {string} type Text element type.
197
+ * @returns {boolean} Whether the extension fields match their contract.
198
+ */
199
+ static #pcbTextExtensions(value, type) {
200
+ for (const field of ['font_width', 'font_height']) {
201
+ if (
202
+ value[field] !== undefined &&
203
+ !CircuitJsonToolkitElementSchema.#positiveLength(value[field])
204
+ ) {
205
+ return false
206
+ }
207
+ }
208
+ if (
209
+ value.stroke_width !== undefined &&
210
+ !CircuitJsonToolkitElementSchema.#nonNegativeLength(
211
+ value.stroke_width
212
+ )
213
+ ) {
214
+ return false
215
+ }
216
+ for (const field of [
217
+ 'source_layer',
218
+ 'source_type',
219
+ 'source_text_kind'
220
+ ]) {
221
+ if (
222
+ value[field] !== undefined &&
223
+ !CircuitJsonToolkitElementSchema.#requiredString(value[field])
224
+ ) {
225
+ return false
226
+ }
227
+ }
228
+ if (
229
+ value.source_anchor_alignment !== undefined &&
230
+ !PCB_TEXT_ANCHOR_ALIGNMENTS.has(value.source_anchor_alignment)
231
+ ) {
232
+ return false
233
+ }
234
+ if (
235
+ value.is_hidden !== undefined &&
236
+ typeof value.is_hidden !== 'boolean'
237
+ ) {
238
+ return false
239
+ }
240
+ if (
241
+ type === 'pcb_note_text' &&
242
+ value.ccw_rotation !== undefined &&
243
+ (!CircuitJsonToolkitElementSchema.#unitScalar(value.ccw_rotation) ||
244
+ optionalAngle(value.ccw_rotation) === null)
245
+ ) {
246
+ return false
247
+ }
248
+ return !(
249
+ type === 'pcb_fabrication_note_text' &&
250
+ value.is_mirrored !== undefined &&
251
+ typeof value.is_mirrored !== 'boolean'
252
+ )
253
+ }
254
+
159
255
  /**
160
256
  * Validates a required non-empty string.
161
257
  * @param {unknown} value Candidate.
@@ -183,6 +279,37 @@ export class CircuitJsonToolkitElementSchema {
183
279
  return CircuitJsonToolkitElementSchema.#number(value) && value > 0
184
280
  }
185
281
 
282
+ /**
283
+ * Validates a positive CircuitJSON length with optional unit suffix.
284
+ * @param {unknown} value Candidate.
285
+ * @returns {boolean} Whether the length is positive.
286
+ */
287
+ static #positiveLength(value) {
288
+ if (!CircuitJsonToolkitElementSchema.#unitScalar(value)) return false
289
+ const length = optionalLength(value)
290
+ return length !== null && length > 0
291
+ }
292
+
293
+ /**
294
+ * Validates a non-negative CircuitJSON length with optional unit suffix.
295
+ * @param {unknown} value Candidate.
296
+ * @returns {boolean} Whether the length is non-negative.
297
+ */
298
+ static #nonNegativeLength(value) {
299
+ if (!CircuitJsonToolkitElementSchema.#unitScalar(value)) return false
300
+ const length = optionalLength(value)
301
+ return length !== null && length >= 0
302
+ }
303
+
304
+ /**
305
+ * Returns whether a unit field is a primitive parser input.
306
+ * @param {unknown} value Candidate.
307
+ * @returns {boolean} Whether the value can be parsed without coercion.
308
+ */
309
+ static #unitScalar(value) {
310
+ return typeof value === 'number' || typeof value === 'string'
311
+ }
312
+
186
313
  /**
187
314
  * Validates one finite point.
188
315
  * @param {unknown} value Candidate.
@@ -31,11 +31,16 @@ export class CircuitJsonUpstreamValidator {
31
31
  type === 'source_net'
32
32
  ? CircuitJsonUpstreamValidator.#sourceNet(value)
33
33
  : CircuitJsonUpstreamValidator.#matches(schema, value)
34
- return matches
35
- ? []
36
- : [
37
- `CircuitJSON element ${type}${location} does not match the pinned upstream schema.`
38
- ]
34
+ if (!matches) {
35
+ return [
36
+ `CircuitJSON element ${type}${location} does not match the pinned upstream schema.`
37
+ ]
38
+ }
39
+ return CircuitJsonToolkitElementSchema.validateExtensions(
40
+ value,
41
+ type,
42
+ location
43
+ )
39
44
  }
40
45
 
41
46
  /**
@@ -1,6 +1,6 @@
1
1
  import { BinaryDataSnapshot } from './BinaryDataSnapshot.mjs'
2
2
 
3
- const METADATA_MAX_DEPTH = 64
3
+ const METADATA_MAX_DEPTH = 256
4
4
  const METADATA_MAX_ITEMS = 100_000
5
5
  const UNBOUNDED_METADATA_BYTES = Number.MAX_SAFE_INTEGER
6
6
  const DATE_GET_TIME = Date.prototype.getTime
@@ -52,7 +52,7 @@ const UINT8_ARRAY_CONSTRUCTOR = Uint8Array
52
52
  const UINT8_ARRAY_SET = Uint8Array.prototype.set
53
53
  const STRUCTURED_CLONE = globalThis.structuredClone
54
54
  const MAX_REQUEST_BYTES = 100_000_000
55
- const MAX_REQUEST_DEPTH = 64
55
+ const MAX_REQUEST_DEPTH = 256
56
56
  const MAX_REQUEST_VALUES = 100_000
57
57
  const MAX_RESULT_BYTES = 250_000_000
58
58
  const MAX_RESULT_VALUES = 2_000_000