pcb-scene3d-viewer 1.1.50 → 1.2.1

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.
Files changed (61) hide show
  1. package/NOTICE.md +8 -5
  2. package/README.md +54 -21
  3. package/docs/api.md +117 -8
  4. package/docs/circuitjson.md +143 -29
  5. package/docs/model-format.md +50 -7
  6. package/docs/release-notes-v1.2.0.md +122 -0
  7. package/docs/release-notes-v1.2.1.md +37 -0
  8. package/docs/testing.md +31 -1
  9. package/package.json +8 -4
  10. package/spec/library-scope.md +10 -2
  11. package/src/CircuitJsonCadModelAssetResolver.mjs +697 -83
  12. package/src/PcbAssemblyBoardSubstrateBuilder.mjs +43 -0
  13. package/src/PcbAssemblyGeometryBuilder.mjs +40 -18
  14. package/src/PcbAssemblyModelMeshLoader.mjs +101 -150
  15. package/src/PcbAssemblyPadMeshBuilder.mjs +22 -0
  16. package/src/PcbModelArchiveExporter.mjs +28 -112
  17. package/src/PcbModelArchiveSourceBundle.mjs +326 -0
  18. package/src/PcbScene3dAabbIndex.mjs +464 -0
  19. package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +9 -5
  20. package/src/PcbScene3dBoardShapeFactory.mjs +11 -118
  21. package/src/PcbScene3dBoardSolderMaskFactory.mjs +58 -35
  22. package/src/PcbScene3dCircuitJsonAdapter.mjs +151 -48
  23. package/src/PcbScene3dCircuitJsonDrillDetail.mjs +31 -0
  24. package/src/PcbScene3dCircuitJsonGeometry.mjs +184 -33
  25. package/src/PcbScene3dCircuitJsonInput.mjs +132 -0
  26. package/src/PcbScene3dCircuitJsonModelAsset.mjs +40 -0
  27. package/src/PcbScene3dController.mjs +40 -34
  28. package/src/PcbScene3dCopperFactory.mjs +36 -22
  29. package/src/PcbScene3dCopperFillAreaClipper.mjs +133 -235
  30. package/src/PcbScene3dCopperFillCoverageContext.mjs +204 -0
  31. package/src/PcbScene3dCopperFillLoopSetResolver.mjs +192 -0
  32. package/src/PcbScene3dCopperFillMeshBuilder.mjs +77 -295
  33. package/src/PcbScene3dCopperTextFactory.mjs +12 -4
  34. package/src/PcbScene3dCutoutCircleDetector.mjs +34 -17
  35. package/src/PcbScene3dCutoutGeometryFilter.mjs +104 -269
  36. package/src/PcbScene3dCutoutGridIndex.mjs +184 -0
  37. package/src/PcbScene3dDeferredModelFinalizer.mjs +52 -0
  38. package/src/PcbScene3dDescriptorSafeRecord.mjs +38 -0
  39. package/src/PcbScene3dDrillCutoutFilter.mjs +149 -143
  40. package/src/PcbScene3dDrillPathFactory.mjs +86 -16
  41. package/src/PcbScene3dDrillVoidFactory.mjs +6 -5
  42. package/src/PcbScene3dExternalModelGroupLoader.mjs +472 -31
  43. package/src/PcbScene3dExternalModels.mjs +23 -24
  44. package/src/PcbScene3dFacetedModelGroupBuilder.mjs +217 -0
  45. package/src/PcbScene3dModelContent.mjs +236 -0
  46. package/src/PcbScene3dModelFetchPolicy.mjs +304 -0
  47. package/src/PcbScene3dModelIdentity.mjs +106 -0
  48. package/src/PcbScene3dOcctImporterLoader.mjs +79 -0
  49. package/src/PcbScene3dPlatedDrillSpecResolver.mjs +141 -0
  50. package/src/PcbScene3dPreparedPolygon.mjs +709 -0
  51. package/src/PcbScene3dPreparedPolygonSet.mjs +70 -0
  52. package/src/PcbScene3dRuntime.mjs +40 -40
  53. package/src/PcbScene3dRuntimeBoardMeshes.mjs +84 -0
  54. package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +4 -2
  55. package/src/PcbScene3dShellRenderer.mjs +75 -6
  56. package/src/PcbScene3dSilkscreenCutoutContext.mjs +255 -0
  57. package/src/PcbScene3dSilkscreenFactory.mjs +87 -127
  58. package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +11 -5
  59. package/src/PcbScene3dStepLoader.mjs +29 -118
  60. package/src/PcbScene3dText.mjs +1 -1
  61. package/src/PcbScene3dTriangleVertexQueryBounds.mjs +302 -0
package/NOTICE.md CHANGED
@@ -27,10 +27,13 @@ required by the applicable license:
27
27
  For applications with an "About", "Licenses", or "Legal Notices" screen,
28
28
  include a reasonable reference to this project and its original author there.
29
29
 
30
- Vendored and third-party components retain their own notices and license terms.
31
- In particular, preserve the notices in:
32
-
33
- - `src/vendor/occt-import-js/dist/license.occt-import-js.txt`;
34
- - `src/vendor/occt-import-js/dist/license.occt.txt`.
30
+ Third-party package dependencies retain their own notices and license terms.
31
+ In particular, distributions that include `@sunbox/occt-import-js` and Open
32
+ CASCADE Technology must preserve the notices shipped by that installed package
33
+ in:
34
+
35
+ - `node_modules/@sunbox/occt-import-js/dist/license.occt-import-js.txt`;
36
+ - `node_modules/@sunbox/occt-import-js/dist/license.occt.txt`;
37
+ - `node_modules/@sunbox/occt-import-js/dist/OCCT_LGPL_EXCEPTION.txt`.
35
38
 
36
39
  Package-manager dependencies retain their own licenses.
package/README.md CHANGED
@@ -7,12 +7,13 @@ SPDX-License-Identifier: CC-BY-SA-4.0
7
7
  # PCB Scene3D Viewer
8
8
 
9
9
  Reusable browser-side 3D PCB viewer utilities for normalized ECAD scene
10
- descriptions and direct CircuitJSON element arrays.
10
+ descriptions and canonical CircuitJSON documents.
11
11
 
12
12
  This package renders scene descriptions produced by packages such as
13
13
  `altium-toolkit/scene3d` and `kicad-toolkit/scene3d`. It does not parse ECAD
14
14
  source files and does not build format-specific scene data. Hosts that already
15
- have CircuitJSON can pass the element array directly to the controller or
15
+ have CircuitJSON can pass a common `DocumentResult`, a prepared
16
+ `CircuitJsonDocumentContext`, or an element array directly to the controller or
16
17
  runtime.
17
18
 
18
19
  The package was extracted from [ECAD Forge](https://ecadforge.app/), where it
@@ -21,6 +22,48 @@ factories, model loading, component picking, view presets, archive export,
21
22
  GLTF/GLB assembly writing, and optional DOM shell can be reused by other
22
23
  browser-based ECAD tools.
23
24
 
25
+ ## CircuitJSON 1.1 convergence
26
+
27
+ Version 1.2.1 accepts the common document and prepared-context shapes returned
28
+ by CircuitJSON, Gerber, Altium, and KiCad Toolkit 1.1-compatible APIs. The
29
+ adapter requests the shared `elements` index once and reuses it across repeated
30
+ scene builds. `PcbScene3dCircuitJsonAdapter.prepare()` exposes that proof-aware
31
+ path to hosts. Existing bare arrays are normalized by the shared CircuitJSON
32
+ boundary, while legacy parser-compatible hybrid arrays retain their native
33
+ builder behavior. Canonical `model_asset` paths and matching document or session
34
+ assets are resolved directly by the adapter; hosts do not need an app-side
35
+ document transform or resolver wrapper. Document asset indexes are created only
36
+ for referenced models and cached through a prepared context. Exact
37
+ case-sensitive paths win; case-insensitive fallback is used only when unique.
38
+ Polygon-plated holes use the shared CircuitJSON hole primitive model, including
39
+ rotation-local `pad_outline` extents and pill-slot width, height, and rotation;
40
+ outer-pad and drill rotations remain independent and board-space drill angles
41
+ are applied exactly once. Gerber routed slots therefore retain horizontal,
42
+ diagonal, and vertical canonical geometry in the viewer. Multiple disjoint
43
+ `pcb_board` rows (or multiple `pcb_panel` rows) render as independent substrate,
44
+ outline, mask, and export contours instead of dropping every row after the
45
+ first.
46
+ Legal rectangular and square CircuitJSON drill apertures retain their exact
47
+ width, height, and board-space rotation through substrate, pad, and assembly
48
+ export meshes.
49
+
50
+ The live runtime loads STEP/STP, WRL/VRML, STL, OBJ, GLTF/GLB, and 3MF from
51
+ canonical text/bytes or browser files. Referenced GLTF buffers, OBJ material
52
+ libraries, and WRL textures are attached from matching document/session assets
53
+ using safe project-relative paths. URL loading is explicit through
54
+ `modelLoaderOptions.fetch` or `allowNetworkModelFetch: true`, with optional
55
+ headers, timeout, cache, and bounded-resource settings. Static `authHeaders`
56
+ stay on the main model origin; `authHeadersForUrl` is the explicit per-URL
57
+ override. The model ZIP exporter uses the same policy and writes each raw model
58
+ under its original source basename with safe GLTF, OBJ, and WRL companions.
59
+
60
+ STEP loading uses the installed `@sunbox/occt-import-js` package directly. Its
61
+ package-owned worker is reused for browser imports, while runtimes without Web
62
+ Workers dynamically import the same ESM factory. Hosts only need to serve the
63
+ package `dist/` directory at
64
+ `/node_modules/@sunbox/occt-import-js/dist/`; no copied runtime, global script,
65
+ or host-owned worker is required.
66
+
24
67
  ## Install
25
68
 
26
69
  ```bash
@@ -61,37 +104,27 @@ const controller = new PcbScene3dController(
61
104
  controller.setSelectedComponent('U1')
62
105
  ```
63
106
 
64
- Direct CircuitJSON input does not require a format-specific `buildScene`
107
+ Canonical document input does not require a format-specific `buildScene`
65
108
  callback:
66
109
 
67
110
  ```js
68
111
  import { PcbScene3dController } from 'pcb-scene3d-viewer'
112
+ import { Parser } from 'gerber-toolkit'
69
113
 
70
- const circuitJson = [
71
- {
72
- type: 'pcb_board',
73
- width: 50,
74
- height: 30,
75
- thickness: 1.6,
76
- center: { x: 25, y: 15 }
77
- },
78
- {
79
- type: 'pcb_component',
80
- source_component_id: 'source-r1',
81
- layer: 1,
82
- center: { x: 20, y: 15 },
83
- width: 3,
84
- height: 1.5
85
- }
86
- ]
114
+ const document = await Parser.parseAsync({
115
+ fileName: file.name,
116
+ data: await file.arrayBuffer()
117
+ })
87
118
 
88
- const controller = new PcbScene3dController(viewportNode, circuitJson)
119
+ const controller = new PcbScene3dController(viewportNode, document)
89
120
  ```
90
121
 
91
122
  ## Documentation
92
123
 
93
124
  - [API](docs/api.md)
94
125
  - [CircuitJSON usage](docs/circuitjson.md)
126
+ - [1.2.1 release notes](docs/release-notes-v1.2.1.md)
127
+ - [1.2.0 release notes](docs/release-notes-v1.2.0.md)
95
128
  - [Model format](docs/model-format.md)
96
129
  - [Testing](docs/testing.md)
97
130
  - [Library scope](spec/library-scope.md)
package/docs/api.md CHANGED
@@ -8,6 +8,10 @@ The package exports all public APIs from `pcb-scene3d-viewer` and
8
8
  ### `PcbScene3dShellRenderer.render(documentModel, translate?)`
9
9
 
10
10
  Returns HTML markup for the optional interactive 3D scene shell.
11
+ `documentModel` accepts a legacy scene/parser model, a dense CircuitJSON array,
12
+ an `ecad-toolkit.document.v1` result, or a prepared
13
+ `CircuitJsonDocumentContext`. CircuitJSON shapes use the shared context index to
14
+ derive board, component, and BOM summary counts.
11
15
 
12
16
  The markup includes:
13
17
 
@@ -36,10 +40,21 @@ Important options:
36
40
  - `createModelRegistry`: optional function passed into `buildScene`.
37
41
  - `createRuntime`: optional runtime factory for tests or custom hosts.
38
42
  - `sessionAssets`: companion model assets passed to scene preparation.
43
+ - `modelLoaderOptions`: optional runtime model-loading policy. It accepts the
44
+ same `fetch`, `allowNetworkModelFetch`, `authHeaders`, `authHeadersForUrl`,
45
+ `fetchTimeoutMs`, `modelCache`, `maxModelBytes`, `maxModelResources`, and
46
+ `maxModelTotalBytes` fields as `PcbAssemblyModelMeshLoader`. The policy is
47
+ also forwarded unchanged to model ZIP export.
39
48
  - `setLoadingVisible`: callback for shell loading state.
40
49
  - `onComponentSelectionChange`: callback for 3D picks.
41
50
  - `translate`: optional `(key) => string` translation function.
42
51
 
52
+ Preparation precedence is explicit: `sceneDescription`, then
53
+ `scenePrepClient`, then direct canonical CircuitJSON adaptation, and finally
54
+ the legacy `buildScene` callback. If asynchronous scene preparation fails, a
55
+ canonical document still falls back to direct adaptation with the supplied
56
+ adapter and asset options.
57
+
43
58
  Methods:
44
59
 
45
60
  - `getDocumentModel()`: returns the mounted document model.
@@ -55,15 +70,35 @@ controller only and are forwarded to the runtime through
55
70
 
56
71
  ### `PcbScene3dCircuitJsonAdapter`
57
72
 
58
- Converts serialized CircuitJSON element arrays into the normalized scene
59
- description consumed by the runtime.
73
+ Converts a common CircuitJSON `DocumentResult`, prepared
74
+ `CircuitJsonDocumentContext`, or serialized element array into the normalized
75
+ scene description consumed by the runtime.
60
76
 
61
77
  Methods:
62
78
 
63
- - `isCircuitJsonModel(value)`: returns true for serialized CircuitJSON arrays.
64
- - `isDirectCircuitJsonModel(value)`: returns true when the array should bypass
65
- host `buildScene` callbacks.
66
- - `build(circuitJson, options?)`: returns a runtime-ready scene description.
79
+ - `isCircuitJsonModel(value)`: returns true for accepted document, context, or
80
+ structurally valid array inputs without freezing or mutating an unprepared
81
+ caller value. Shared normalization and schema validation occur in `prepare`.
82
+ - `isDirectCircuitJsonModel(value)`: returns true when the canonical input
83
+ should bypass host `buildScene` callbacks.
84
+ - `prepare(circuitJson)`: returns a `CircuitJsonDocumentContext`, normalizes
85
+ supported legacy rows through the shared CircuitJSON boundary, reuses a
86
+ canonical document validation proof, and prepares the `elements` index once.
87
+ - `build(circuitJson, options?)`: returns a runtime-ready scene description and
88
+ reuses an existing context `elements` index when supplied. Canonical
89
+ `cad_component.model_asset` records are consumed directly and matched against
90
+ canonical document assets or `options.sessionAssets` through a prebuilt
91
+ descriptor-safe alias index. The index is not built for documents without
92
+ model references, is cached in a supplied context for repeated builds, and
93
+ materializes a canonical payload only when its first model reference is used.
94
+ Plated-hole copper and drill geometry comes from the shared
95
+ `CircuitJsonPcbHolePrimitiveModel`, so polygon pad outlines and pill slots use
96
+ the same canonical shape and dimensions as the producing toolkit. Drill
97
+ rotation is board-space and is never added to pad rotation a second time.
98
+ Rectangular and square apertures retain width, height, and rotation rather
99
+ than being approximated as circles or pill slots, including assembly export.
100
+ Every selected panel contour, or every board contour when no panel exists,
101
+ is retained in `board.contours` and rendered/exported independently.
67
102
  `options.modelUrlResolver` can attach caller-owned URL resolution metadata to
68
103
  `cad_component` external models without fetching them. `projectBaseUrl`
69
104
  resolves relative model URLs and package-style `node_modules/...` model paths,
@@ -77,6 +112,31 @@ Methods:
77
112
  when they receive direct CircuitJSON input. See
78
113
  [CircuitJSON usage](circuitjson.md) for supported elements, units, and examples.
79
114
 
115
+ ### `CircuitJsonCadModelAssetResolver`
116
+
117
+ `withModelAssetUrls(document)` accepts an element array, common
118
+ `DocumentResult`, or prepared context. It returns the same input shape when no
119
+ derived URL is needed; otherwise it returns a new array, canonical document
120
+ envelope, or prepared context with `model_asset` paths promoted to explicit
121
+ CircuitJSON model URL fields. Source metadata, extensions, assets, diagnostics,
122
+ and statistics are preserved. This utility remains available for consumers
123
+ that need explicit URL fields; the viewer adapter does not require it.
124
+
125
+ `withSessionAssetResolver(options, documentAssets?)` retains its public session
126
+ asset behavior and also accepts canonical document assets. Alias lookup is
127
+ indexed once and does not invoke asset accessors. Exact case-sensitive project
128
+ paths take precedence. Case-insensitive lookup is a compatibility fallback only
129
+ when one unique asset owns the folded path; ambiguous folded paths do not
130
+ resolve. Resolved metadata is copied through a descriptor-safe boundary.
131
+
132
+ `withContextAssetResolver(options, context)` uses the same behavior while
133
+ caching the canonical document-asset alias index in the supplied prepared
134
+ context. Session assets remain request-specific and take precedence over
135
+ document assets. Referenced GLTF `buffers[].uri`, OBJ `mtllib`, and WRL
136
+ `ImageTexture` companions are attached as `externalBuffers` or `resources`
137
+ when their safe project-relative path matches an indexed asset. Parent
138
+ traversal, absolute paths, and URL schemes are never attached implicitly.
139
+
80
140
  ## Runtime
81
141
 
82
142
  ### `new PcbScene3dRuntime(viewportNode, sceneDescription, hooks?)`
@@ -90,6 +150,46 @@ Hooks:
90
150
  - `loadRuntimeModules()`: optional async loader returning `{ THREE,
91
151
  OrbitControls }`.
92
152
  - `translate`: optional translation function for interaction hints.
153
+ - `modelLoaderOptions`: opt-in model URL fetch policy and cache settings.
154
+
155
+ The live runtime accepts STEP/STP, WRL/VRML, STL, OBJ, GLTF/GLB, and 3MF
156
+ placements. Text-capable formats accept `payloadText`, `text`, and string
157
+ `data`; every format accepts binary `data`, `bytes`, `payloadBytes`, or browser
158
+ `File`/`Blob` content. URL fetching remains opt-in through an injected `fetch`
159
+ function or `allowNetworkModelFetch: true`. Relative GLTF sidecars resolve
160
+ beside absolute or project-relative main model paths. WRL texture references
161
+ are replaced with local or explicitly fetched data URIs before Three.js parses
162
+ the model, preventing implicit texture networking. STL, OBJ, GLTF, and GLB use
163
+ the shared faceted mesh pipeline so runtime and assembly export preserve the
164
+ same units, material color, opacity, and vertex-color behavior.
165
+
166
+ STEP imports resolve `occt-import-js.js`, `occt-import-js.wasm`, and
167
+ `occt-import-js-worker.js` from the installed `@sunbox/occt-import-js` package.
168
+ The package worker is persistent and serialized per loader. When Web Workers
169
+ are unavailable, the viewer dynamically imports the ESM factory directly; it
170
+ does not inject a classic script or depend on a global factory. Worker transfer
171
+ uses a loader-owned byte snapshot, and rejected ESM initialization is evicted
172
+ so callers retain their input and can retry transient failures.
173
+
174
+ Static `authHeaders` are sent only to the main model origin. A host that
175
+ intentionally authorizes another origin can return headers from
176
+ `authHeadersForUrl(url, { mainUrl, sameOrigin, label })`. Each fetch scope
177
+ defaults to 128 MiB per resource, 256 resources, and 512 MiB aggregate; override
178
+ these with `maxModelBytes`, `maxModelResources`, and `maxModelTotalBytes`.
179
+
180
+ ### `PcbModelArchiveExporter.buildArchive(options?)`
181
+
182
+ Exports resolved STEP/STP, WRL/VRML, 3MF, GLB/GLTF, STL, and OBJ sources. Raw
183
+ models accept the same canonical text, byte, file, and explicitly enabled URL
184
+ sources as the runtime. `modelLoaderOptions` is shared with stitched-component
185
+ mesh loading and raw URL export. Exact canonical project paths, source streams,
186
+ or IDs define deduplication; same-basename models in different paths remain
187
+ distinct. A controller derives the archive base name from `summary.title`,
188
+ canonical `source.fileName`, or the legacy `fileName`, in that order.
189
+ Each raw source is written to a unique pattern directory under its original
190
+ source basename. Safe relative GLTF buffers/images, OBJ resources, and WRL
191
+ textures are written beside it so internal references remain valid. Every raw
192
+ `exportedEntries` row includes `bundleDirectory` and `companionPaths`.
93
193
 
94
194
  Methods:
95
195
 
@@ -175,9 +275,15 @@ Options:
175
275
  - `fetch`: host-provided fetch function for resolved model URLs.
176
276
  - `allowNetworkModelFetch`: use `globalThis.fetch` for `resolvedUrl` or
177
277
  `sourceUrl` models when no local payload is present.
178
- - `authHeaders`: headers forwarded to network model requests.
278
+ - `authHeaders`: headers forwarded only to same-origin model requests.
279
+ - `authHeadersForUrl(url, context)`: explicit per-URL headers. Static
280
+ `authHeaders` are withheld when `context.sameOrigin` is false.
179
281
  - `fetchTimeoutMs`: abort timeout for network model requests.
180
282
  - `modelCache`: optional cache map keyed by resolved model URL.
283
+ - `maxModelBytes`: maximum bytes per fetched resource; defaults to 134217728.
284
+ - `maxModelResources`: maximum fetched main/sidecar resources per scope;
285
+ defaults to 256.
286
+ - `maxModelTotalBytes`: maximum aggregate fetched bytes per scope; defaults to 536870912.
181
287
 
182
288
  ## Model Archive Export
183
289
 
@@ -189,12 +295,15 @@ Options:
189
295
 
190
296
  - `archiveBaseName`: base name for the downloaded archive.
191
297
  - `sceneDescription`: scene description containing resolved external models.
298
+ - `modelLoaderOptions`: the same bounded explicit fetch policy used by runtime
299
+ model loading.
192
300
 
193
301
  Returns:
194
302
 
195
303
  - `archiveName`;
196
304
  - `archiveBytes`;
197
- - `exportedEntries`;
305
+ - `exportedEntries`, including `archivePath`, `bundleDirectory`, and
306
+ `companionPaths` for each raw source;
198
307
  - `skippedEntries`.
199
308
 
200
309
  ## Geometry Factories
@@ -1,14 +1,24 @@
1
1
  # CircuitJSON Usage
2
2
 
3
- `pcb-scene3d-viewer` can render serialized CircuitJSON element arrays directly.
4
- Use this path when the host application already has CircuitJSON and does not
5
- need an Altium, KiCad, or other format-specific scene builder.
3
+ `pcb-scene3d-viewer` can render common CircuitJSON document envelopes, prepared
4
+ document contexts, and serialized element arrays directly. Use this path when
5
+ the host application already has CircuitJSON and does not need an Altium,
6
+ KiCad, Gerber, or other format-specific scene builder.
7
+
8
+ ## Accepted Input Shapes
9
+
10
+ - `ecad-toolkit.document.v1` results returned by converged toolkit parsers;
11
+ - `CircuitJsonDocumentContext` instances prepared by a host for reuse; or
12
+ - dense serialized CircuitJSON element arrays.
13
+
14
+ Prepared contexts are the fastest repeated-render path because validation and
15
+ the adapter's `elements` index are built at most once.
6
16
 
7
17
  ## Direct Controller Input
8
18
 
9
- Pass the CircuitJSON array as the `documentModel`. The controller detects direct
10
- CircuitJSON input, converts it to the internal render model, and mounts the
11
- runtime without a `buildScene` callback.
19
+ Pass any accepted CircuitJSON shape as the `documentModel`. The controller
20
+ detects direct CircuitJSON input, converts it to the internal render model, and
21
+ mounts the runtime without a `buildScene` callback.
12
22
 
13
23
  ```js
14
24
  import {
@@ -19,6 +29,7 @@ import {
19
29
  const circuitJson = [
20
30
  {
21
31
  type: 'pcb_board',
32
+ pcb_board_id: 'board-1',
22
33
  width: 50,
23
34
  height: 30,
24
35
  thickness: 1.6,
@@ -26,14 +37,16 @@ const circuitJson = [
26
37
  },
27
38
  {
28
39
  type: 'source_component',
29
- id: 'source-r1',
40
+ source_component_id: 'source-r1',
30
41
  name: 'R1',
31
- ftype: 'R_0603'
42
+ ftype: 'simple_resistor',
43
+ resistance: '10k'
32
44
  },
33
45
  {
34
46
  type: 'pcb_component',
47
+ pcb_component_id: 'pcb-r1',
35
48
  source_component_id: 'source-r1',
36
- layer: 1,
49
+ layer: 'top',
37
50
  center: { x: 20, y: 15 },
38
51
  rotation: 90,
39
52
  width: 1.6,
@@ -42,7 +55,10 @@ const circuitJson = [
42
55
  },
43
56
  {
44
57
  type: 'pcb_smtpad',
45
- layer: 1,
58
+ pcb_smtpad_id: 'pad-r1-1',
59
+ pcb_component_id: 'pcb-r1',
60
+ layer: 'top',
61
+ shape: 'rect',
46
62
  x: 19.2,
47
63
  y: 15,
48
64
  width: 0.7,
@@ -50,7 +66,10 @@ const circuitJson = [
50
66
  },
51
67
  {
52
68
  type: 'pcb_smtpad',
53
- layer: 1,
69
+ pcb_smtpad_id: 'pad-r1-2',
70
+ pcb_component_id: 'pcb-r1',
71
+ layer: 'top',
72
+ shape: 'rect',
54
73
  x: 20.8,
55
74
  y: 15,
56
75
  width: 0.7,
@@ -58,12 +77,23 @@ const circuitJson = [
58
77
  },
59
78
  {
60
79
  type: 'pcb_trace',
61
- layer: 1,
80
+ pcb_trace_id: 'trace-r1-1',
62
81
  route: [
63
- { x: 18, y: 15 },
64
- { x: 19.2, y: 15 }
65
- ],
66
- width: 0.25
82
+ {
83
+ route_type: 'wire',
84
+ x: 18,
85
+ y: 15,
86
+ width: 0.25,
87
+ layer: 'top'
88
+ },
89
+ {
90
+ route_type: 'wire',
91
+ x: 19.2,
92
+ y: 15,
93
+ width: 0.25,
94
+ layer: 'top'
95
+ }
96
+ ]
67
97
  }
68
98
  ]
69
99
 
@@ -75,14 +105,14 @@ const controller = new PcbScene3dController(
75
105
  )
76
106
  ```
77
107
 
78
- The shell renderer does not inspect the CircuitJSON data. It only renders the
79
- optional DOM controls. The controller performs the CircuitJSON detection and
108
+ The shell renderer uses the shared CircuitJSON context/index to derive its
109
+ board, component, and BOM summary. The controller performs the full render-model
80
110
  conversion.
81
111
 
82
112
  ## Direct Runtime Input
83
113
 
84
- For custom UI shells, pass the same CircuitJSON array directly to
85
- `PcbScene3dRuntime`.
114
+ For custom UI shells, pass the same document, context, or CircuitJSON array
115
+ directly to `PcbScene3dRuntime`.
86
116
 
87
117
  ```js
88
118
  import { PcbScene3dRuntime } from 'pcb-scene3d-viewer'
@@ -109,9 +139,49 @@ if (PcbScene3dCircuitJsonAdapter.isCircuitJsonModel(circuitJson)) {
109
139
  }
110
140
  ```
111
141
 
142
+ For repeated builds, prepare and retain the shared context:
143
+
144
+ ```js
145
+ import { CircuitJsonDocumentContext } from 'circuitjson-toolkit'
146
+ import { PcbScene3dCircuitJsonAdapter } from 'pcb-scene3d-viewer'
147
+
148
+ const context = CircuitJsonDocumentContext.prepare(document, {
149
+ indexes: ['elements']
150
+ })
151
+ const firstScene = PcbScene3dCircuitJsonAdapter.build(context)
152
+ const secondScene = PcbScene3dCircuitJsonAdapter.build(context)
153
+ ```
154
+
155
+ Both builds reuse the same validated model and `elements` index. If the model
156
+ contains CAD model references, its canonical document-asset alias index is also
157
+ built at most once in the same context. Documents without model references do
158
+ not pay that indexing cost.
159
+
160
+ The viewer exposes the same operation as
161
+ `PcbScene3dCircuitJsonAdapter.prepare(document)`. Controller routing uses this
162
+ proof-aware path before mounting, so a validated toolkit document is not
163
+ validated again. Bare arrays first pass a non-mutating structural predicate and
164
+ are then normalized and validated by the shared CircuitJSON context; supported
165
+ legacy rows are not rejected by an earlier strict viewer precheck.
166
+
167
+ Canonical CAD rows may retain a `model_asset` path instead of an explicit
168
+ `model_step_url`, `model_glb_url`, or equivalent field. The adapter consumes
169
+ that field directly and resolves matching assets from the canonical document
170
+ or `sessionAssets`; no document pre-transform or resolver wrapper is required.
171
+ Canonical `ToolkitAsset` payloads may expose their immutable `data` through the
172
+ shared accessor-backed contract. The resolver recognizes and lazily
173
+ materializes those trusted assets before its descriptor-safe viewer copy;
174
+ arbitrary accessor-backed session rows remain unread and cannot execute.
175
+ For GLTF, OBJ, and WRL main assets, safe project-relative BIN, MTL, and texture
176
+ references are attached automatically from the same indexed asset sets.
177
+ Session companions take precedence over document companions. Absolute URLs,
178
+ parent traversal, and accessor-backed entries are excluded without executing
179
+ caller accessors.
180
+ `CircuitJsonCadModelAssetResolver.withModelAssetUrls()` remains available when
181
+ a separate consumer specifically needs explicit URL fields.
182
+
112
183
  Hosts that need URL policy control can pass a synchronous `modelUrlResolver`.
113
- The adapter records the returned metadata on each external model but does not
114
- fetch the referenced file:
184
+ The adapter records the returned metadata on each external model:
115
185
 
116
186
  ```js
117
187
  const sceneDescription = PcbScene3dCircuitJsonAdapter.build(circuitJson, {
@@ -128,12 +198,20 @@ Hosts can also pass `projectBaseUrl` to resolve relative model URLs without a
128
198
  custom resolver. Package-style `node_modules/...` paths resolve to the
129
199
  project-origin `/package_files/download` endpoint with package and file-path
130
200
  query parameters. Package download file paths are normalized under `dist/` when
131
- the source path does not already include it. Model fetching remains a separate
132
- host/export decision.
201
+ the source path does not already include it. Model fetching remains opt-in.
202
+ Pass `modelLoaderOptions` to the controller or runtime hooks with an injected
203
+ `fetch`, or set `allowNetworkModelFetch: true` to use `globalThis.fetch`.
204
+ Canonical document bytes and session `File`/`Blob` assets need no network
205
+ option. Static `authHeaders` remain on the main model origin. Use
206
+ `authHeadersForUrl` for an intentional cross-origin header and
207
+ `maxModelBytes`/`maxModelResources`/`maxModelTotalBytes` to adjust the bounded
208
+ fetch defaults.
133
209
  `boardDrillQuality` accepts `low`, `medium`, or `high` and controls generated
134
210
  circle sampling for direct CircuitJSON drill/cutout geometry. When a
135
211
  CircuitJSON file has components but no board or panel, `drawFauxBoard: true`
136
212
  generates a board from component bounds with a 2 mm margin per side.
213
+ The optional shell honors the same flag, so component-only documents show the
214
+ same faux-board scene controls instead of an empty-state message.
137
215
  Set `showPcbNotes: true` to render `pcb_note_text`,
138
216
  `pcb_fabrication_note_text`, note/fabrication path artwork, and courtyard
139
217
  artwork as silkscreen detail. Notes are hidden by default so manufacturing
@@ -145,7 +223,9 @@ does not clutter normal board previews.
145
223
  `isDirectCircuitJsonModel(value)` returns `false` for compatibility arrays that
146
224
  also carry legacy parser fields such as `pcb`, `schematic`, or `bom`. Those
147
225
  arrays continue through the host-provided `buildScene` callback so existing
148
- parser integrations keep their source-specific conversion behavior.
226
+ parser integrations keep their source-specific conversion behavior. Canonical
227
+ document envelopes and prepared contexts always use the direct CircuitJSON path
228
+ regardless of their original source format.
149
229
 
150
230
  ## Units And Coordinates
151
231
 
@@ -159,6 +239,12 @@ board with a 1.6 mm thickness so incomplete test or preview models still
159
239
  render. With `drawFauxBoard: true`, that fallback board is instead sized around
160
240
  the PCB component bounds with a minimum 10 mm by 10 mm footprint.
161
241
 
242
+ When panels exist, every `pcb_panel` is an independent physical contour and
243
+ child `pcb_board` rows are not duplicated as substrate. Without panels, every
244
+ `pcb_board` is retained. `board.widthMil`, `heightMil`, and center describe the
245
+ aggregate bounds; `board.contours` carries each physical outline, thickness,
246
+ and targeted cutouts for runtime rendering and assembly export.
247
+
162
248
  `cad_component` records with `show_as_bounding_box: true` are exported as
163
249
  procedural component bodies. If `size` or `model_size` is present, those
164
250
  dimensions drive the generated body; otherwise the paired `pcb_component`
@@ -270,6 +356,34 @@ Drills can use circular, pill, or rotated pill geometry. `hole_offset_x` and
270
356
  }
271
357
  ```
272
358
 
359
+ Polygon-plated slots use the canonical `hole_with_polygon_pad` variant. The
360
+ shared CircuitJSON primitive model derives the copper dimensions in the pad's
361
+ rotation-local coordinate system and preserves the pill drill dimensions:
362
+
363
+ ```js
364
+ {
365
+ type: 'pcb_plated_hole',
366
+ shape: 'hole_with_polygon_pad',
367
+ hole_shape: 'pill',
368
+ x: 10,
369
+ y: 8,
370
+ hole_width: 2.6,
371
+ hole_height: 0.6,
372
+ pad_outline: [
373
+ { x: 8.7, y: 7.7 },
374
+ { x: 11.3, y: 7.7 },
375
+ { x: 11.3, y: 8.3 },
376
+ { x: 8.7, y: 8.3 }
377
+ ]
378
+ }
379
+ ```
380
+
381
+ This is the same canonical shape emitted for a plated Gerber routed slot; no
382
+ format-specific scene adapter is required. `holeRotation` is board-space, so a
383
+ 45-degree or 90-degree routed slot is applied once even when its outer pad uses
384
+ the same rotation. Rectangular outer pads may independently use
385
+ `rect_ccw_rotation`, while rotated pill drills use `hole_ccw_rotation`.
386
+
273
387
  SMT pads can use circular, rectangular, rotated rectangular, or pill geometry.
274
388
  Pill pads are normalized as rounded rectangles so their copper keeps the
275
389
  expected capsule outline:
@@ -368,7 +482,7 @@ model conversion. Use `PcbScene3dCircuitJsonAdapter.isCircuitJsonModel(value)`
368
482
  for a cheap guard when accepting untrusted JSON from users, and catch conversion
369
483
  errors around `build(value)` when you need to show a custom diagnostic.
370
484
 
371
- The viewer does not fetch external assets for CircuitJSON input by itself.
372
- Model URL matching, same-origin checks, proxying, and file loading remain the
373
- responsibility of source-specific toolkits or host applications that create or
374
- post-process normalized scene descriptions.
485
+ The viewer never fetches external assets unless the host explicitly enables a
486
+ model loader fetch policy. Model URL matching and same-origin/proxy decisions
487
+ remain host-owned; canonical document bytes and session files are consumed
488
+ directly without an app-side resolver wrapper.