pcb-scene3d-viewer 1.1.31 → 1.1.44

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 (36) hide show
  1. package/AGENTS.md +4 -3
  2. package/docs/api.md +35 -3
  3. package/docs/circuitjson.md +164 -25
  4. package/docs/model-format.md +54 -3
  5. package/package.json +2 -2
  6. package/src/CircuitJsonCadModelAssetResolver.mjs +261 -0
  7. package/src/PcbAssemblyBoardSubstrateBuilder.mjs +30 -9
  8. package/src/PcbAssemblyComponentMeshBuilder.mjs +110 -30
  9. package/src/PcbAssemblyGeometryBuildProgress.mjs +3 -0
  10. package/src/PcbAssemblyGeometryBuilder.mjs +54 -1
  11. package/src/PcbAssemblyGltfModelMeshParser.mjs +58 -1
  12. package/src/PcbAssemblyGltfWriter.mjs +184 -7
  13. package/src/PcbAssemblyModelMeshLoader.mjs +312 -2
  14. package/src/PcbAssemblyPadMeshBuilder.mjs +9 -7
  15. package/src/PcbAssemblyTextModelMeshParser.mjs +142 -19
  16. package/src/PcbScene3dCircuitJsonAdapter.mjs +316 -379
  17. package/src/PcbScene3dCircuitJsonCopperPourBuilder.mjs +298 -0
  18. package/src/PcbScene3dCircuitJsonDocumentationArtworkBuilder.mjs +606 -0
  19. package/src/PcbScene3dCircuitJsonGeometry.mjs +542 -0
  20. package/src/PcbScene3dCircuitJsonLayer.mjs +110 -0
  21. package/src/PcbScene3dCircuitJsonModelUrlResolver.mjs +142 -0
  22. package/src/PcbScene3dCircuitJsonSilkscreenBuilder.mjs +729 -0
  23. package/src/PcbScene3dCircuitJsonSolderPasteBuilder.mjs +359 -0
  24. package/src/PcbScene3dCircuitJsonThermalSpokeBuilder.mjs +167 -0
  25. package/src/PcbScene3dCircuitJsonTraceRouteBuilder.mjs +335 -0
  26. package/src/PcbScene3dComponentVisibility.mjs +5 -1
  27. package/src/PcbScene3dFallbackBodyFactory.mjs +45 -4
  28. package/src/PcbScene3dFallbackVisibility.mjs +17 -32
  29. package/src/PcbScene3dFootprintBodyBuilder.mjs +894 -0
  30. package/src/PcbScene3dModelSeatingPolicy.mjs +58 -0
  31. package/src/PcbScene3dRenderGroupVisibility.mjs +10 -13
  32. package/src/PcbScene3dRuntime.mjs +58 -53
  33. package/src/PcbScene3dRuntimeSurfaceArtworkLoader.mjs +91 -0
  34. package/src/PcbScene3dSelectionMarkerFactory.mjs +39 -4
  35. package/src/PcbScene3dStepLoader.mjs +42 -6
  36. package/src/scene3d.mjs +1 -0
package/AGENTS.md CHANGED
@@ -35,9 +35,10 @@
35
35
  ## Library Scope
36
36
 
37
37
  - Include browser-side Three.js scene runtime, geometry factories, STEP/WRL
38
- model loading, component picking, view presets, archive export, the optional
39
- DOM controller, and the optional 3D shell renderer/styles.
40
- - Do not include ECAD parser logic or format-specific scene-description
38
+ model loading, component picking, view presets, archive export, CircuitJSON
39
+ scene adapters and CAD model asset resolution, the optional DOM controller,
40
+ and the optional 3D shell renderer/styles.
41
+ - Do not include ECAD parser logic or source-format-specific scene-description
41
42
  builders. Altium and KiCad scene descriptions are produced by their own
42
43
  toolkits and passed into this package.
43
44
  - Keep renderer fixes universal. Never special-case a specific file name,
package/docs/api.md CHANGED
@@ -65,7 +65,13 @@ Methods:
65
65
  host `buildScene` callbacks.
66
66
  - `build(circuitJson, options?)`: returns a runtime-ready scene description.
67
67
  `options.modelUrlResolver` can attach caller-owned URL resolution metadata to
68
- `cad_component` external models without fetching them.
68
+ `cad_component` external models without fetching them. `projectBaseUrl`
69
+ resolves relative model URLs and package-style `node_modules/...` model paths,
70
+ `drawFauxBoard: true` generates a board around component bounds when no board
71
+ or panel exists, `boardDrillQuality` controls generated circular
72
+ drill/cutout sampling, and `showPcbNotes: true` renders note, fabrication,
73
+ and courtyard artwork as silkscreen detail. `showPcbPaste: true` renders
74
+ direct solder-paste artwork as a separate top/bottom overlay.
69
75
 
70
76
  `PcbScene3dController` and `PcbScene3dRuntime` call this adapter automatically
71
77
  when they receive direct CircuitJSON input. See
@@ -130,7 +136,10 @@ Workers respond with `scene3d:success` and `sceneDescription`, or
130
136
  Builds export meshes from a prepared scene description. `options.modelMeshLoader`
131
137
  can provide external model meshes, `options.includeModels: false` skips external
132
138
  model loading, and `options.renderFallbackBodies: false` disables procedural
133
- component bodies for unresolved models.
139
+ component bodies for unresolved models. Placements with `renderAsBoundingBox:
140
+ true` are exported as procedural component bodies instead of loading the
141
+ referenced model. Optional solder-paste detail exports as distinct paste meshes
142
+ when present in `sceneDescription.detail.paste`.
134
143
 
135
144
  ### `PcbAssemblyGltfWriter.write(options?)`
136
145
 
@@ -143,9 +152,32 @@ Options:
143
152
  `color`, optional `opacity`, and optional board `texture` data URIs.
144
153
  - `format`: `gltf` or `glb`.
145
154
  - `binary`: optional boolean equivalent to `format: 'glb'`.
155
+ - `includeSceneMetadata`: when `true`, emits a default camera and punctual light
156
+ for third-party GLTF viewers.
146
157
 
147
158
  Returns a GLTF JSON object for `gltf` and a `Uint8Array` for `glb`. RGBA colors
148
- or opacity values below `1` are emitted as blended GLTF materials.
159
+ or opacity values below `1` are emitted as blended GLTF materials. Mesh
160
+ `vertexColors` are exported as `COLOR_0`, and mesh `material` metadata is
161
+ preserved in material extras.
162
+
163
+ ### `new PcbAssemblyModelMeshLoader(options?)`
164
+
165
+ Loads STEP, WRL, STL, OBJ, GLTF, and GLB external models into assembly meshes.
166
+ By default the loader only reads embedded payloads, session files, and provided
167
+ byte buffers. Network loading is opt-in through either an injected `fetch`
168
+ function or `allowNetworkModelFetch: true`. Remote `.gltf` buffer sidecars are
169
+ resolved relative to the `.gltf` URL and use the same fetch, auth-header,
170
+ timeout, and cache policy as the top-level model.
171
+
172
+ Options:
173
+
174
+ - `stepLoader`: custom STEP loader.
175
+ - `fetch`: host-provided fetch function for resolved model URLs.
176
+ - `allowNetworkModelFetch`: use `globalThis.fetch` for `resolvedUrl` or
177
+ `sourceUrl` models when no local payload is present.
178
+ - `authHeaders`: headers forwarded to network model requests.
179
+ - `fetchTimeoutMs`: abort timeout for network model requests.
180
+ - `modelCache`: optional cache map keyed by resolved model URL.
149
181
 
150
182
  ## Model Archive Export
151
183
 
@@ -124,6 +124,24 @@ const sceneDescription = PcbScene3dCircuitJsonAdapter.build(circuitJson, {
124
124
  })
125
125
  ```
126
126
 
127
+ Hosts can also pass `projectBaseUrl` to resolve relative model URLs without a
128
+ custom resolver. Package-style `node_modules/...` paths resolve to the
129
+ project-origin `/package_files/download` endpoint with package and file-path
130
+ 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.
133
+ `boardDrillQuality` accepts `low`, `medium`, or `high` and controls generated
134
+ circle sampling for direct CircuitJSON drill/cutout geometry. When a
135
+ CircuitJSON file has components but no board or panel, `drawFauxBoard: true`
136
+ generates a board from component bounds with a 2 mm margin per side.
137
+ Set `showPcbNotes: true` to render `pcb_note_text`,
138
+ `pcb_fabrication_note_text`, note/fabrication path artwork, and courtyard
139
+ artwork as silkscreen detail. Notes are hidden by default so manufacturing
140
+ annotations do not unexpectedly appear in board previews.
141
+ Set `showPcbPaste: true` to render `pcb_solder_paste` as a grey top/bottom
142
+ surface overlay. Paste is hidden by default so stencil-only manufacturing data
143
+ does not clutter normal board previews.
144
+
127
145
  `isDirectCircuitJsonModel(value)` returns `false` for compatibility arrays that
128
146
  also carry legacy parser fields such as `pcb`, `schematic`, or `bom`. Those
129
147
  arrays continue through the host-provided `buildScene` callback so existing
@@ -138,12 +156,27 @@ the Three.js runtime.
138
156
  The board center defaults to `{ x: 0, y: 0 }` when omitted. If no `pcb_panel`
139
157
  or `pcb_board` element is present, the adapter creates a 25.4 mm by 25.4 mm
140
158
  board with a 1.6 mm thickness so incomplete test or preview models still
141
- render.
159
+ render. With `drawFauxBoard: true`, that fallback board is instead sized around
160
+ the PCB component bounds with a minimum 10 mm by 10 mm footprint.
161
+
162
+ `cad_component` records with `show_as_bounding_box: true` are exported as
163
+ procedural component bodies. If `size` or `model_size` is present, those
164
+ dimensions drive the generated body; otherwise the paired `pcb_component`
165
+ footprint dimensions are used.
166
+
167
+ When a `cad_component` has no explicit model URL, package footprint metadata
168
+ from `footprinter_string`, `footprint_string`, `footprint`, or `package` can
169
+ generate a richer fallback body. Common dual-row, quad-row, SOT-style,
170
+ through-hole pin-header, passive-chip, pushbutton, testpoint, and TO-style
171
+ package strings add simple lead, pin, or terminal geometry while keeping the
172
+ plain box fallback for unrecognized package text.
142
173
 
143
174
  Layer values resolve as follows:
144
175
 
145
- - `1`, `top`, `front`, and `f.cu` map to the top side.
146
- - `32`, `bottom`, `back`, and `b.cu` map to the bottom side.
176
+ - `1`, `top`, `front`, `f.cu`, and top-side artwork layer names map to the
177
+ top side.
178
+ - `32`, `bottom`, `back`, `b.cu`, and bottom-side artwork layer names map to
179
+ the bottom side.
147
180
  - Unknown layer values default to the top side.
148
181
 
149
182
  ## Supported Elements
@@ -151,21 +184,37 @@ Layer values resolve as follows:
151
184
  The adapter focuses on renderer-ready PCB geometry and ignores unsupported
152
185
  CircuitJSON elements instead of failing the whole scene.
153
186
 
154
- | Element type | Rendered as |
155
- | --------------------- | ------------------------------------------------------------------- |
156
- | `pcb_panel` | Preferred board/panel size, thickness, center, and optional outline |
157
- | `pcb_board` | Board size, thickness, center, and optional outline |
158
- | `pcb_cutout` | Through-board cutout loop |
159
- | `source_component` | Component designator and package metadata |
160
- | `pcb_component` | Fallback component body and selection target |
161
- | `cad_component` | External model URL metadata and placement data |
162
- | `pcb_smtpad` | Top or bottom SMT pad copper |
163
- | `pcb_plated_hole` | Through-hole pad copper and drill |
164
- | `pcb_hole` | Non-plated drill opening |
165
- | `pcb_via` | Via copper and drill |
166
- | `pcb_trace` | Routed copper track segments |
167
- | `pcb_silkscreen_line` | Top or bottom silkscreen stroke |
168
- | `pcb_silkscreen_text` | Top or bottom silkscreen text placeholder |
187
+ | Element type | Rendered as |
188
+ | --------------------------- | ------------------------------------------------------------------- |
189
+ | `pcb_panel` | Preferred board/panel size, thickness, center, and optional outline |
190
+ | `pcb_board` | Board size, thickness, center, and optional outline |
191
+ | `pcb_cutout` | Through-board cutout loop |
192
+ | `source_component` | Component designator and package metadata |
193
+ | `pcb_component` | Fallback component body and selection target |
194
+ | `cad_component` | External model URL metadata and placement data |
195
+ | `pcb_smtpad` | Top or bottom SMT pad copper, including pill pads |
196
+ | `pcb_plated_hole` | Through-hole pad copper and drill |
197
+ | `pcb_hole` | Non-plated drill opening |
198
+ | `pcb_via` | Via copper and drill |
199
+ | `pcb_trace` | Routed copper track segments and route-derived surface vias |
200
+ | `pcb_copper_pour` | Top or bottom copper zone polygon |
201
+ | `pcb_solder_paste` | Optional top or bottom paste fill when `showPcbPaste` is enabled |
202
+ | `pcb_silkscreen_line` | Top or bottom silkscreen stroke |
203
+ | `pcb_silkscreen_path` | Top or bottom routed silkscreen strokes |
204
+ | `pcb_silkscreen_circle` | Top or bottom full-circle silkscreen stroke |
205
+ | `pcb_silkscreen_rect` | Top or bottom rectangular silkscreen outline |
206
+ | `pcb_silkscreen_oval` | Top or bottom oval silkscreen outline |
207
+ | `pcb_silkscreen_pill` | Top or bottom pill silkscreen outline |
208
+ | `pcb_silkscreen_text` | Top or bottom silkscreen text placeholder |
209
+ | `pcb_note_text` | Optional top or bottom note text when `showPcbNotes` is enabled |
210
+ | `pcb_note_line` | Optional top or bottom note stroke when `showPcbNotes` is enabled |
211
+ | `pcb_note_path` | Optional top or bottom note path when `showPcbNotes` is enabled |
212
+ | `pcb_note_rect` | Optional top or bottom note outline when `showPcbNotes` is enabled |
213
+ | `pcb_fabrication_note_text` | Optional fabrication text when `showPcbNotes` is enabled |
214
+ | `pcb_fabrication_note_path` | Optional fabrication path when `showPcbNotes` is enabled |
215
+ | `pcb_courtyard_rect` | Optional courtyard rectangle when `showPcbNotes` is enabled |
216
+ | `pcb_courtyard_circle` | Optional courtyard circle when `showPcbNotes` is enabled |
217
+ | `pcb_courtyard_outline` | Optional courtyard outline when `showPcbNotes` is enabled |
169
218
 
170
219
  `cad_component` entries can provide external model URLs and model-local
171
220
  placement hints. The adapter maps `model_unit_to_mm_scale_factor`,
@@ -189,7 +238,9 @@ Board outlines can be supplied as `pcb_board.outline`, using an array of points:
189
238
  ```
190
239
 
191
240
  Through-board cutouts can be supplied as polygon, rectangle, or circle
192
- `pcb_cutout` elements:
241
+ `pcb_cutout` elements. A `pcb_cutout` with `pcb_board_id` only applies when that
242
+ board is selected, while cutouts without a board target are global. Rectangular
243
+ cutouts support `ccw_rotation`:
193
244
 
194
245
  ```js
195
246
  {
@@ -197,17 +248,80 @@ Through-board cutouts can be supplied as polygon, rectangle, or circle
197
248
  shape: 'rect',
198
249
  center: { x: 25, y: 15 },
199
250
  width: 4,
200
- height: 2
251
+ height: 2,
252
+ ccw_rotation: 45
253
+ }
254
+ ```
255
+
256
+ Drills can use circular, pill, or rotated pill geometry. `hole_offset_x` and
257
+ `hole_offset_y` move the drill center independently of the pad/copper center:
258
+
259
+ ```js
260
+ {
261
+ type: 'pcb_plated_hole',
262
+ x: 10,
263
+ y: 8,
264
+ hole_shape: 'rotated_pill',
265
+ hole_width: 0.5,
266
+ hole_height: 1.5,
267
+ hole_offset_x: 0.1,
268
+ hole_offset_y: -0.1,
269
+ outer_diameter: 2
270
+ }
271
+ ```
272
+
273
+ SMT pads can use circular, rectangular, rotated rectangular, or pill geometry.
274
+ Pill pads are normalized as rounded rectangles so their copper keeps the
275
+ expected capsule outline:
276
+
277
+ ```js
278
+ {
279
+ type: 'pcb_smtpad',
280
+ layer: 'top',
281
+ shape: 'rotated_pill',
282
+ x: 12,
283
+ y: 8,
284
+ width: 2,
285
+ height: 1,
286
+ ccw_rotation: 30
287
+ }
288
+ ```
289
+
290
+ SMT pads, plated holes, and vias honor `is_covered_with_solder_mask` and
291
+ `covered_with_solder_mask` when present. `true` keeps the copper under solder
292
+ mask, while `false` exposes the copper on the applicable board side. For vias,
293
+ covered values map to tenting metadata on both sides.
294
+
295
+ Copper pours can use rectangular, polygon, or B-Rep geometry. Rectangular pours
296
+ support `rotation` or `ccw_rotation`; polygon and B-Rep point coordinates are
297
+ converted from millimeters to mils. `covered_with_solder_mask: true` renders the
298
+ zone under solder mask, while false or omitted values expose the copper:
299
+
300
+ ```js
301
+ {
302
+ type: 'pcb_copper_pour',
303
+ layer: 'top',
304
+ shape: 'rect',
305
+ center: { x: 25, y: 15 },
306
+ width: 8,
307
+ height: 5,
308
+ rotation: 45,
309
+ covered_with_solder_mask: false
201
310
  }
202
311
  ```
203
312
 
313
+ Silkscreen text uses `anchor_position` when available, falling back to `x` and
314
+ `y`. Common `anchor_alignment` values such as `center`, `bottom_left`, or
315
+ `top_right` are normalized into horizontal and vertical alignment metadata for
316
+ stroke text rendering.
317
+
204
318
  Component model metadata can be supplied on `cad_component` elements with
205
- `model_step_url`, `model_wrl_url`, `model_glb_url`, `model_gltf_url`,
206
- `model_stl_url`, or `model_obj_url`. The adapter emits external placement
207
- metadata for the host runtime or export pipeline to resolve.
319
+ `model_3mf_url`, `model_step_url`, `model_wrl_url`, `model_glb_url`,
320
+ `model_gltf_url`, `model_stl_url`, or `model_obj_url`. The adapter emits
321
+ external placement metadata for the host runtime or export pipeline to resolve.
208
322
 
209
- Traces can be supplied as a `route` array. Each adjacent point pair becomes one
210
- track segment:
323
+ Traces can be supplied as a `route` array. Each adjacent wire point pair becomes
324
+ one track segment:
211
325
 
212
326
  ```js
213
327
  {
@@ -222,6 +336,31 @@ track segment:
222
336
  }
223
337
  ```
224
338
 
339
+ Route entries with `route_type: 'via'` produce via primitives when their
340
+ `from_layer`, `to_layer`, or `layer` touches the top or bottom surface. Adjacent
341
+ surface wire segments remain visible through those vias. Inner-only vias and
342
+ inner-only wire segments are ignored by the surface-copper view. If a route via
343
+ omits `hole_diameter`, `via_hole_diameter`, or `drill_diameter`, the drill
344
+ diameter defaults to half of `via_diameter`.
345
+
346
+ ```js
347
+ {
348
+ type: 'pcb_trace',
349
+ route: [
350
+ { route_type: 'wire', x: 5, y: 5, width: 0.2, layer: 'top' },
351
+ {
352
+ route_type: 'via',
353
+ x: 8,
354
+ y: 5,
355
+ from_layer: 'top',
356
+ to_layer: 'bottom',
357
+ via_diameter: 0.4
358
+ },
359
+ { route_type: 'wire', x: 8, y: 10, width: 0.2, layer: '32' }
360
+ ]
361
+ }
362
+ ```
363
+
225
364
  ## Diagnostics
226
365
 
227
366
  Malformed CircuitJSON input is rejected by `circuitjson-toolkit` before render
@@ -35,6 +35,10 @@ toolkits are responsible for creating this data.
35
35
  silkscreen: {
36
36
  top: {},
37
37
  bottom: {}
38
+ },
39
+ paste: {
40
+ top: {},
41
+ bottom: {}
38
42
  }
39
43
  }
40
44
  }
@@ -92,14 +96,48 @@ External placements describe STEP, WRL, GLB, GLTF, STL, or OBJ model instances:
92
96
  name: 'soic-8.step',
93
97
  relativePath: 'packages/soic-8.step',
94
98
  format: 'step',
99
+ sourceUrl: '/models/soic-8.step',
100
+ resolvedUrl: 'https://assets.example.invalid/models/soic-8.step',
95
101
  file: File
96
102
  }
97
103
  }
98
104
  ```
99
105
 
100
106
  OBJ models can carry sidecar material libraries in `resources`, `relatedFiles`,
101
- `assets`, or similar metadata collections. GLTF/GLB material alpha and mesh
102
- opacity are preserved in exported GLTF/GLB materials.
107
+ `assets`, or similar metadata collections. OBJ diffuse, ambient, specular,
108
+ shininess, alpha, and vertex colors are preserved in the parsed mesh metadata.
109
+ GLTF/GLB material alpha, mesh opacity, and `COLOR_0` vertex colors are
110
+ preserved in imported and exported GLTF/GLB materials and primitives.
111
+ GLTF/GLB import consumes triangle primitives and ignores unsupported primitive
112
+ modes such as points or lines. Remote `.gltf` files can reference external
113
+ `buffers[].uri` sidecars; when URL-backed model loading is enabled, the loader
114
+ resolves those sidecars relative to the `.gltf` URL and fetches them with the
115
+ same cache, timeout, and auth-header policy.
116
+
117
+ 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.
121
+
122
+ When `projectBaseUrl` is provided to the CircuitJSON adapter, package-style
123
+ model paths such as `node_modules/package-name/path/to/model.step` are resolved
124
+ to `/package_files/download` on the project origin with
125
+ `package_name_with_version` and `file_path` query parameters. Package download
126
+ file paths are normalized under `dist/` unless they already start there. This
127
+ only records a resolved URL; fetching still requires an explicit loader/export
128
+ fetch policy.
129
+
130
+ CAD placements can request procedural bounding-box output with
131
+ `show_as_bounding_box: true`. The adapter marks those placements with
132
+ `renderAsBoundingBox: true`, copies `size`/`model_size` into fallback body
133
+ dimensions when present, and the assembly geometry builder skips external model
134
+ loading for that placement.
135
+
136
+ When package footprint text is available, fallback bodies can derive simple
137
+ package-specific geometry for passive chips, dual-row and quad-row ICs,
138
+ SOT-style packages, pin headers, pushbuttons, testpoints, and TO-style
139
+ through-hole packages. Testpoints and radial capacitors use round fallback
140
+ bodies in both runtime rendering and assembly export.
103
141
 
104
142
  Embedded STEP models can use:
105
143
 
@@ -118,11 +156,24 @@ The runtime expects pre-normalized primitive lists for:
118
156
 
119
157
  - pads and vias with drill and copper dimensions;
120
158
  - tracks and arcs with layer metadata;
121
- - fills and polygons with point loops;
159
+ - fills and polygons with point loops or B-Rep ring metadata;
122
160
  - silkscreen tracks, arcs, fills, and texts;
161
+ - optional solder-paste fills in `detail.paste`;
123
162
  - copper text primitives.
124
163
 
125
164
  The viewer does not infer source-file semantics. When a scene needs
126
165
  format-specific layer mapping, solder-mask interpretation, text layout, or
127
166
  model matching, the source toolkit should encode those decisions in the scene
128
167
  description.
168
+
169
+ Through-hole pads can describe offset or slotted drills with `holeOffsetX`,
170
+ `holeOffsetY`, `holeSlotLength`, and `holeRotation`. Board substrate export uses
171
+ `detail.drillQuality` values of `low`, `medium`, or `high` to choose circular
172
+ drill sampling density.
173
+
174
+ Rounded SMT pads set `hasRoundedRect`, the side-specific
175
+ `roundedRectShapeTop`/`roundedRectShapeBottom`, and
176
+ `cornerRadiusTop`/`cornerRadiusBottom` as a percent of the shortest copper side.
177
+ Route-derived vias use the same `detail.vias` shape as standalone vias, and
178
+ surface route segments use `layerId: 1` for top copper or `layerId: 32` for
179
+ bottom copper.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pcb-scene3d-viewer",
3
- "version": "1.1.31",
3
+ "version": "1.1.44",
4
4
  "description": "Reusable Three.js PCB 3D scene viewer for normalized ECAD and CircuitJSON scene descriptions",
5
5
  "keywords": [
6
6
  "pcb",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@sunbox/occt-import-js": "^0.0.25",
55
- "circuitjson-toolkit": "^1.0.1",
55
+ "circuitjson-toolkit": "^1.0.10",
56
56
  "earcut": "^3.0.2",
57
57
  "fflate": "^0.8.2",
58
58
  "three": "^0.183.2"
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Bridges CAD component model asset metadata to scene model resolution.
3
+ */
4
+ export class CircuitJsonCadModelAssetResolver {
5
+ /**
6
+ * Adds URL fields derived from CAD `model_asset` records.
7
+ * @param {object | object[]} documentModel Document model.
8
+ * @returns {object | object[]}
9
+ */
10
+ static withModelAssetUrls(documentModel) {
11
+ const elements =
12
+ CircuitJsonCadModelAssetResolver.#elements(documentModel)
13
+ if (
14
+ !elements.some((element) =>
15
+ CircuitJsonCadModelAssetResolver.#modelAssetUrlField(element)
16
+ )
17
+ ) {
18
+ return documentModel
19
+ }
20
+
21
+ const nextElements = elements.map((element) =>
22
+ CircuitJsonCadModelAssetResolver.#withModelAssetUrl(element)
23
+ )
24
+
25
+ if (Array.isArray(documentModel)) {
26
+ Object.assign(nextElements, {
27
+ sourceFormat: documentModel.sourceFormat,
28
+ kind: documentModel.kind,
29
+ fileName: documentModel.fileName,
30
+ bom: documentModel.bom
31
+ })
32
+ return nextElements
33
+ }
34
+
35
+ if (Array.isArray(documentModel?.elements)) {
36
+ return { ...documentModel, elements: nextElements }
37
+ }
38
+ if (Array.isArray(documentModel?.circuitJson)) {
39
+ return { ...documentModel, circuitJson: nextElements }
40
+ }
41
+ return documentModel
42
+ }
43
+
44
+ /**
45
+ * Adds a session-asset-aware model URL resolver.
46
+ * @param {object} options Scene preparation options.
47
+ * @returns {object}
48
+ */
49
+ static withSessionAssetResolver(options = {}) {
50
+ const sessionAssets = Array.isArray(options.sessionAssets)
51
+ ? options.sessionAssets
52
+ : []
53
+ const callerResolver =
54
+ typeof options.modelUrlResolver === 'function'
55
+ ? options.modelUrlResolver
56
+ : null
57
+
58
+ return {
59
+ ...options,
60
+ modelUrlResolver: (url, context) =>
61
+ CircuitJsonCadModelAssetResolver.#resolveModelUrl(
62
+ url,
63
+ context,
64
+ sessionAssets,
65
+ callerResolver
66
+ )
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Adds a model URL field to one CAD component when possible.
72
+ * @param {object} element Element row.
73
+ * @returns {object}
74
+ */
75
+ static #withModelAssetUrl(element) {
76
+ const field =
77
+ CircuitJsonCadModelAssetResolver.#modelAssetUrlField(element)
78
+ if (!field) return element
79
+ const asset = element.model_asset || {}
80
+ const sourceUrl = String(
81
+ asset.project_relative_path || asset.url || ''
82
+ ).trim()
83
+ if (!sourceUrl) return element
84
+ return { ...element, [field]: sourceUrl }
85
+ }
86
+
87
+ /**
88
+ * Resolves a model URL field for one CAD component asset.
89
+ * @param {object} element Element row.
90
+ * @returns {string}
91
+ */
92
+ static #modelAssetUrlField(element) {
93
+ if (element?.type !== 'cad_component') return ''
94
+ const asset = element.model_asset
95
+ if (!asset || typeof asset !== 'object') return ''
96
+ if (CircuitJsonCadModelAssetResolver.#hasModelUrl(element)) return ''
97
+ const format = CircuitJsonCadModelAssetResolver.#format(asset)
98
+ return (
99
+ {
100
+ '3mf': 'model_3mf_url',
101
+ glb: 'model_glb_url',
102
+ gltf: 'model_gltf_url',
103
+ obj: 'model_obj_url',
104
+ step: 'model_step_url',
105
+ stl: 'model_stl_url',
106
+ wrl: 'model_wrl_url'
107
+ }[format] || ''
108
+ )
109
+ }
110
+
111
+ /**
112
+ * Resolves a URL to a session asset or caller-provided value.
113
+ * @param {string} url Model URL.
114
+ * @param {object} context Resolver context.
115
+ * @param {object[]} sessionAssets Session asset rows.
116
+ * @param {((url: string, context: object) => any) | null} callerResolver Caller resolver.
117
+ * @returns {object | string | null}
118
+ */
119
+ static #resolveModelUrl(url, context, sessionAssets, callerResolver) {
120
+ const matchedAsset =
121
+ CircuitJsonCadModelAssetResolver.#matchingSessionAsset(
122
+ url,
123
+ context,
124
+ sessionAssets
125
+ )
126
+ if (matchedAsset) {
127
+ return matchedAsset
128
+ }
129
+ return callerResolver ? callerResolver(url, context) : null
130
+ }
131
+
132
+ /**
133
+ * Finds a session asset matching one CAD model URL.
134
+ * @param {string} url Model URL.
135
+ * @param {object} context Resolver context.
136
+ * @param {object[]} sessionAssets Session assets.
137
+ * @returns {object | null}
138
+ */
139
+ static #matchingSessionAsset(url, context, sessionAssets) {
140
+ const candidates = CircuitJsonCadModelAssetResolver.#urlCandidates(
141
+ url,
142
+ context
143
+ )
144
+ return (
145
+ sessionAssets.find((asset) =>
146
+ CircuitJsonCadModelAssetResolver.#assetMatches(
147
+ asset,
148
+ candidates
149
+ )
150
+ ) || null
151
+ )
152
+ }
153
+
154
+ /**
155
+ * Builds normalized URL/path candidates for matching.
156
+ * @param {string} url Model URL.
157
+ * @param {object} context Resolver context.
158
+ * @returns {Set<string>}
159
+ */
160
+ static #urlCandidates(url, context) {
161
+ return new Set(
162
+ [
163
+ url,
164
+ context?.cadComponent?.model_asset?.project_relative_path,
165
+ context?.cadComponent?.model_asset?.url
166
+ ]
167
+ .map((value) => CircuitJsonCadModelAssetResolver.#key(value))
168
+ .filter(Boolean)
169
+ )
170
+ }
171
+
172
+ /**
173
+ * Returns true when a session asset matches a model URL.
174
+ * @param {object} asset Session asset.
175
+ * @param {Set<string>} candidates URL candidates.
176
+ * @returns {boolean}
177
+ */
178
+ static #assetMatches(asset, candidates) {
179
+ return [
180
+ asset?.relativePath,
181
+ asset?.sourceUrl,
182
+ asset?.url,
183
+ asset?.name
184
+ ].some((value) =>
185
+ candidates.has(CircuitJsonCadModelAssetResolver.#key(value))
186
+ )
187
+ }
188
+
189
+ /**
190
+ * Resolves a supported model format from asset metadata.
191
+ * @param {object} asset Model asset metadata.
192
+ * @returns {string}
193
+ */
194
+ static #format(asset) {
195
+ const text = String(
196
+ asset.format ||
197
+ asset.mimetype ||
198
+ asset.project_relative_path ||
199
+ asset.url ||
200
+ ''
201
+ ).toLowerCase()
202
+ if (/\bglb\b|\.glb(?:[?#]|$)/u.test(text)) return 'glb'
203
+ if (/\bgltf\b|\.gltf(?:[?#]|$)/u.test(text)) return 'gltf'
204
+ if (/\b3mf\b|\.3mf(?:[?#]|$)/u.test(text)) return '3mf'
205
+ if (/\bobj\b|\.obj(?:[?#]|$)/u.test(text)) return 'obj'
206
+ if (/\bstl\b|\.stl(?:[?#]|$)/u.test(text)) return 'stl'
207
+ if (/\bwrl\b|vrml|\.wrl(?:[?#]|$)/u.test(text)) return 'wrl'
208
+ if (/step|stp|\.step(?:[?#]|$)|\.stp(?:[?#]|$)/u.test(text)) {
209
+ return 'step'
210
+ }
211
+ return ''
212
+ }
213
+
214
+ /**
215
+ * Returns true when a CAD component already has a model URL.
216
+ * @param {object} element Element row.
217
+ * @returns {boolean}
218
+ */
219
+ static #hasModelUrl(element) {
220
+ return [
221
+ 'model_step_url',
222
+ 'model_stp_url',
223
+ 'model_3mf_url',
224
+ 'model_wrl_url',
225
+ 'model_vrml_url',
226
+ 'model_glb_url',
227
+ 'model_gltf_url',
228
+ 'model_stl_url',
229
+ 'model_obj_url'
230
+ ].some((field) => String(element?.[field] || '').trim())
231
+ }
232
+
233
+ /**
234
+ * Reads element rows from document wrappers.
235
+ * @param {object | object[]} documentModel Document model.
236
+ * @returns {object[]}
237
+ */
238
+ static #elements(documentModel) {
239
+ if (Array.isArray(documentModel)) return documentModel
240
+ if (Array.isArray(documentModel?.elements))
241
+ return documentModel.elements
242
+ if (Array.isArray(documentModel?.circuitJson)) {
243
+ return documentModel.circuitJson
244
+ }
245
+ return []
246
+ }
247
+
248
+ /**
249
+ * Normalizes a path or URL key.
250
+ * @param {unknown} value Raw path.
251
+ * @returns {string}
252
+ */
253
+ static #key(value) {
254
+ return String(value || '')
255
+ .trim()
256
+ .split(/[?#]/u)[0]
257
+ .replace(/^https?:\/\/[^/]+\//iu, '')
258
+ .replace(/^\/+/u, '')
259
+ .toLowerCase()
260
+ }
261
+ }