pcb-scene3d-viewer 1.1.49 → 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.
Files changed (63) hide show
  1. package/README.md +46 -21
  2. package/docs/api.md +109 -8
  3. package/docs/circuitjson.md +143 -29
  4. package/docs/model-format.md +50 -7
  5. package/docs/release-notes-v1.2.0.md +122 -0
  6. package/docs/testing.md +30 -1
  7. package/package.json +6 -3
  8. package/spec/library-scope.md +10 -2
  9. package/src/CircuitJsonCadModelAssetResolver.mjs +697 -83
  10. package/src/PcbAssemblyBoardSubstrateBuilder.mjs +43 -0
  11. package/src/PcbAssemblyGeometryBuilder.mjs +40 -18
  12. package/src/PcbAssemblyModelMeshLoader.mjs +101 -150
  13. package/src/PcbAssemblyPadMeshBuilder.mjs +22 -0
  14. package/src/PcbModelArchiveExporter.mjs +28 -112
  15. package/src/PcbModelArchiveSourceBundle.mjs +326 -0
  16. package/src/PcbScene3dAabbIndex.mjs +464 -0
  17. package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -1
  18. package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +9 -5
  19. package/src/PcbScene3dBoardMaterialPalette.mjs +29 -0
  20. package/src/PcbScene3dBoardShapeFactory.mjs +11 -118
  21. package/src/PcbScene3dBoardSolderMaskFactory.mjs +65 -39
  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 +35 -10
  42. package/src/PcbScene3dExternalModelGroupLoader.mjs +472 -31
  43. package/src/PcbScene3dExternalModels.mjs +23 -24
  44. package/src/PcbScene3dFacetedModelGroupBuilder.mjs +217 -0
  45. package/src/PcbScene3dGeometryZCompressor.mjs +4 -2
  46. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +18 -4
  47. package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +75 -10
  48. package/src/PcbScene3dModelContent.mjs +236 -0
  49. package/src/PcbScene3dModelFetchPolicy.mjs +304 -0
  50. package/src/PcbScene3dModelIdentity.mjs +106 -0
  51. package/src/PcbScene3dPlatedDrillSpecResolver.mjs +141 -0
  52. package/src/PcbScene3dPreparedPolygon.mjs +709 -0
  53. package/src/PcbScene3dPreparedPolygonSet.mjs +70 -0
  54. package/src/PcbScene3dRuntime.mjs +45 -45
  55. package/src/PcbScene3dRuntimeBoardMeshes.mjs +85 -1
  56. package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +4 -2
  57. package/src/PcbScene3dShellRenderer.mjs +75 -6
  58. package/src/PcbScene3dSilkscreenCutoutContext.mjs +255 -0
  59. package/src/PcbScene3dSilkscreenFactory.mjs +87 -127
  60. package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +11 -5
  61. package/src/PcbScene3dStepLoader.mjs +11 -10
  62. package/src/PcbScene3dText.mjs +1 -1
  63. package/src/PcbScene3dTriangleVertexQueryBounds.mjs +302 -0
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,41 @@ 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.0 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
+
24
60
  ## Install
25
61
 
26
62
  ```bash
@@ -61,37 +97,26 @@ const controller = new PcbScene3dController(
61
97
  controller.setSelectedComponent('U1')
62
98
  ```
63
99
 
64
- Direct CircuitJSON input does not require a format-specific `buildScene`
100
+ Canonical document input does not require a format-specific `buildScene`
65
101
  callback:
66
102
 
67
103
  ```js
68
104
  import { PcbScene3dController } from 'pcb-scene3d-viewer'
105
+ import { Parser } from 'gerber-toolkit'
69
106
 
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
- ]
107
+ const document = await Parser.parseAsync({
108
+ fileName: file.name,
109
+ data: await file.arrayBuffer()
110
+ })
87
111
 
88
- const controller = new PcbScene3dController(viewportNode, circuitJson)
112
+ const controller = new PcbScene3dController(viewportNode, document)
89
113
  ```
90
114
 
91
115
  ## Documentation
92
116
 
93
117
  - [API](docs/api.md)
94
118
  - [CircuitJSON usage](docs/circuitjson.md)
119
+ - [1.2.0 release notes](docs/release-notes-v1.2.0.md)
95
120
  - [Model format](docs/model-format.md)
96
121
  - [Testing](docs/testing.md)
97
122
  - [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,38 @@ 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
+ Static `authHeaders` are sent only to the main model origin. A host that
167
+ intentionally authorizes another origin can return headers from
168
+ `authHeadersForUrl(url, { mainUrl, sameOrigin, label })`. Each fetch scope
169
+ defaults to 128 MiB per resource, 256 resources, and 512 MiB aggregate; override
170
+ these with `maxModelBytes`, `maxModelResources`, and `maxModelTotalBytes`.
171
+
172
+ ### `PcbModelArchiveExporter.buildArchive(options?)`
173
+
174
+ Exports resolved STEP/STP, WRL/VRML, 3MF, GLB/GLTF, STL, and OBJ sources. Raw
175
+ models accept the same canonical text, byte, file, and explicitly enabled URL
176
+ sources as the runtime. `modelLoaderOptions` is shared with stitched-component
177
+ mesh loading and raw URL export. Exact canonical project paths, source streams,
178
+ or IDs define deduplication; same-basename models in different paths remain
179
+ distinct. A controller derives the archive base name from `summary.title`,
180
+ canonical `source.fileName`, or the legacy `fileName`, in that order.
181
+ Each raw source is written to a unique pattern directory under its original
182
+ source basename. Safe relative GLTF buffers/images, OBJ resources, and WRL
183
+ textures are written beside it so internal references remain valid. Every raw
184
+ `exportedEntries` row includes `bundleDirectory` and `companionPaths`.
93
185
 
94
186
  Methods:
95
187
 
@@ -175,9 +267,15 @@ Options:
175
267
  - `fetch`: host-provided fetch function for resolved model URLs.
176
268
  - `allowNetworkModelFetch`: use `globalThis.fetch` for `resolvedUrl` or
177
269
  `sourceUrl` models when no local payload is present.
178
- - `authHeaders`: headers forwarded to network model requests.
270
+ - `authHeaders`: headers forwarded only to same-origin model requests.
271
+ - `authHeadersForUrl(url, context)`: explicit per-URL headers. Static
272
+ `authHeaders` are withheld when `context.sameOrigin` is false.
179
273
  - `fetchTimeoutMs`: abort timeout for network model requests.
180
274
  - `modelCache`: optional cache map keyed by resolved model URL.
275
+ - `maxModelBytes`: maximum bytes per fetched resource; defaults to 134217728.
276
+ - `maxModelResources`: maximum fetched main/sidecar resources per scope;
277
+ defaults to 256.
278
+ - `maxModelTotalBytes`: maximum aggregate fetched bytes per scope; defaults to 536870912.
181
279
 
182
280
  ## Model Archive Export
183
281
 
@@ -189,12 +287,15 @@ Options:
189
287
 
190
288
  - `archiveBaseName`: base name for the downloaded archive.
191
289
  - `sceneDescription`: scene description containing resolved external models.
290
+ - `modelLoaderOptions`: the same bounded explicit fetch policy used by runtime
291
+ model loading.
192
292
 
193
293
  Returns:
194
294
 
195
295
  - `archiveName`;
196
296
  - `archiveBytes`;
197
- - `exportedEntries`;
297
+ - `exportedEntries`, including `archivePath`, `bundleDirectory`, and
298
+ `companionPaths` for each raw source;
198
299
  - `skippedEntries`.
199
300
 
200
301
  ## 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.
@@ -70,7 +70,8 @@ Components describe fallback package bodies and selection metadata:
70
70
 
71
71
  ## External Placements
72
72
 
73
- External placements describe STEP, WRL, GLB, GLTF, STL, or OBJ model instances:
73
+ External placements describe STEP, WRL, 3MF, GLB, GLTF, STL, or OBJ model
74
+ instances:
74
75
 
75
76
  ```js
76
77
  {
@@ -112,12 +113,47 @@ GLTF/GLB import consumes triangle primitives and ignores unsupported primitive
112
113
  modes such as points or lines. Remote `.gltf` files can reference external
113
114
  `buffers[].uri` sidecars; when URL-backed model loading is enabled, the loader
114
115
  resolves those sidecars relative to the `.gltf` URL and fetches them with the
115
- same cache, timeout, and auth-header policy.
116
+ same cache, timeout, and auth-header policy. This also works when the main model
117
+ path is project-relative. Resident GLTF JSON uses already attached buffers and
118
+ fetches only missing sidecars when an explicit policy is configured.
119
+
120
+ Canonical document/session asset resolution attaches safe project-relative
121
+ GLTF BIN, OBJ MTL, and WRL texture companions directly. WRL textures are encoded
122
+ as data URIs before parsing. Unmatched texture URLs become inert unless the host
123
+ supplies an explicit fetch policy, so Three.js cannot perform an implicit
124
+ network request.
116
125
 
117
126
  Resolved model URLs are not fetched by default. Hosts that want URL-backed model
118
- loading must provide an explicit fetch policy through `PcbAssemblyModelMeshLoader`
119
- or a higher-level export service. Use `authHeaders`, `fetchTimeoutMs`, and a
120
- caller-owned `modelCache` when remote model requests are enabled.
127
+ loading must provide an explicit fetch policy through
128
+ `PcbScene3dController`/`PcbScene3dRuntime` `modelLoaderOptions`,
129
+ `PcbScene3dExternalModels.loadIntoScene({ modelLoaderOptions })`, or
130
+ `PcbAssemblyModelMeshLoader`. Use `authHeaders`, `fetchTimeoutMs`, and a
131
+ caller-owned `modelCache` when remote model requests are enabled. Successful
132
+ STEP/STP, WRL/VRML, and 3MF requests share that cache; rejected requests are
133
+ evicted so callers can retry. Embedded `payloadText`, `text`, `payloadBytes`,
134
+ `bytes`, canonical string/binary `data`, and browser `File`/`Blob` values are
135
+ loaded without enabling network access.
136
+
137
+ Static `authHeaders` are origin-bound to the main model URL and are not copied
138
+ to cross-origin GLTF buffers or WRL textures. Use
139
+ `authHeadersForUrl(url, { mainUrl, sameOrigin, label })` for an intentional
140
+ per-resource exception. Network scopes default to 128 MiB per resource, 256
141
+ resources, and 512 MiB aggregate across main models and sidecars. Configure
142
+ `maxModelBytes`, `maxModelResources`, and `maxModelTotalBytes` to adjust those
143
+ bounds.
144
+
145
+ Model caches and ZIP deduplication use exact canonical project paths, retained
146
+ source streams, or asset IDs before falling back to checksums and names. Two
147
+ different directories may therefore contain the same basename without a cache
148
+ collision, while repeated placements of one exact source reuse parsed geometry.
149
+ Canonical/session asset aliases resolve exact case-sensitive paths first. A
150
+ case-insensitive fallback is accepted only when unique, preventing collisions
151
+ between files that differ only by case.
152
+
153
+ Raw model archives use one unique pattern directory per source. The main file
154
+ keeps its original source basename, and safe relative GLTF buffers/images, OBJ
155
+ resources, and WRL textures are emitted beneath the same directory so their
156
+ references remain usable after extraction.
121
157
 
122
158
  When `projectBaseUrl` is provided to the CircuitJSON adapter, package-style
123
159
  model paths such as `node_modules/package-name/path/to/model.step` are resolved
@@ -167,9 +203,16 @@ model matching, the source toolkit should encode those decisions in the scene
167
203
  description.
168
204
 
169
205
  Through-hole pads can describe offset or slotted drills with `holeOffsetX`,
170
- `holeOffsetY`, `holeSlotLength`, and `holeRotation`. Board substrate export uses
206
+ `holeOffsetY`, `holeSlotLength`, and board-space `holeRotation`; pad `rotation`
207
+ is not added to the drill angle. Board substrate export uses
171
208
  `detail.drillQuality` values of `low`, `medium`, or `high` to choose circular
172
- drill sampling density.
209
+ drill sampling density. Direct CircuitJSON polygon-plated holes are normalized
210
+ through the shared hole primitive model: polygon extents become the pad's local
211
+ copper size and pill width/height become `holeDiameter` and `holeSlotLength`.
212
+
213
+ `board.contours` contains every independently extruded board or panel outline.
214
+ The top-level board bounds center all detail, while runtime bodies, outlines,
215
+ solder-mask faces, and exported substrate meshes iterate the contour list.
173
216
 
174
217
  Rounded SMT pads set `hasRoundedRect`, the side-specific
175
218
  `roundedRectShapeTop`/`roundedRectShapeBottom`, and