pcb-scene3d-viewer 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -24,7 +24,7 @@ browser-based ECAD tools.
24
24
 
25
25
  ## CircuitJSON 1.1 convergence
26
26
 
27
- Version 1.2.0 accepts the common document and prepared-context shapes returned
27
+ Version 1.2.2 accepts the common document and prepared-context shapes returned
28
28
  by CircuitJSON, Gerber, Altium, and KiCad Toolkit 1.1-compatible APIs. The
29
29
  adapter requests the shared `elements` index once and reuses it across repeated
30
30
  scene builds. `PcbScene3dCircuitJsonAdapter.prepare()` exposes that proof-aware
@@ -47,6 +47,13 @@ Legal rectangular and square CircuitJSON drill apertures retain their exact
47
47
  width, height, and board-space rotation through substrate, pad, and assembly
48
48
  export meshes.
49
49
 
50
+ Canonical documents retain their exact `source.format` as the scene
51
+ `sourceFormat`; raw element arrays continue to use `circuitjson`. Routed traces
52
+ and copper pours with no authored solder-mask coverage value remain covered,
53
+ while `covered_with_solder_mask: false` keeps an explicit opening exposed.
54
+ Standard vias likewise default to tented and honor `is_tented: false` as an
55
+ explicit opening.
56
+
50
57
  The live runtime loads STEP/STP, WRL/VRML, STL, OBJ, GLTF/GLB, and 3MF from
51
58
  canonical text/bytes or browser files. Referenced GLTF buffers, OBJ material
52
59
  libraries, and WRL textures are attached from matching document/session assets
@@ -57,6 +64,13 @@ stay on the main model origin; `authHeadersForUrl` is the explicit per-URL
57
64
  override. The model ZIP exporter uses the same policy and writes each raw model
58
65
  under its original source basename with safe GLTF, OBJ, and WRL companions.
59
66
 
67
+ STEP loading uses the installed `@sunbox/occt-import-js` package directly. Its
68
+ package-owned worker is reused for browser imports, while runtimes without Web
69
+ Workers dynamically import the same ESM factory. Hosts only need to serve the
70
+ package `dist/` directory at
71
+ `/node_modules/@sunbox/occt-import-js/dist/`; no copied runtime, global script,
72
+ or host-owned worker is required.
73
+
60
74
  ## Install
61
75
 
62
76
  ```bash
@@ -116,6 +130,8 @@ const controller = new PcbScene3dController(viewportNode, document)
116
130
 
117
131
  - [API](docs/api.md)
118
132
  - [CircuitJSON usage](docs/circuitjson.md)
133
+ - [1.2.2 release notes](docs/release-notes-v1.2.2.md)
134
+ - [1.2.1 release notes](docs/release-notes-v1.2.1.md)
119
135
  - [1.2.0 release notes](docs/release-notes-v1.2.0.md)
120
136
  - [Model format](docs/model-format.md)
121
137
  - [Testing](docs/testing.md)
package/docs/api.md CHANGED
@@ -108,6 +108,14 @@ Methods:
108
108
  and courtyard artwork as silkscreen detail. `showPcbPaste: true` renders
109
109
  direct solder-paste artwork as a separate top/bottom overlay.
110
110
 
111
+ For canonical document and prepared-context inputs, the returned scene's
112
+ `sourceFormat` is the exact canonical `source.format`. Dense element arrays,
113
+ which do not carry source metadata, retain the `circuitjson` fallback. Routed
114
+ traces and copper pours default to covered when their coverage property is
115
+ omitted; an explicit `covered_with_solder_mask: false` remains an exposed
116
+ opening. Standard vias default to tented and honor `is_tented: false` as an
117
+ explicit opening.
118
+
111
119
  `PcbScene3dController` and `PcbScene3dRuntime` call this adapter automatically
112
120
  when they receive direct CircuitJSON input. See
113
121
  [CircuitJSON usage](circuitjson.md) for supported elements, units, and examples.
@@ -163,6 +171,14 @@ the model, preventing implicit texture networking. STL, OBJ, GLTF, and GLB use
163
171
  the shared faceted mesh pipeline so runtime and assembly export preserve the
164
172
  same units, material color, opacity, and vertex-color behavior.
165
173
 
174
+ STEP imports resolve `occt-import-js.js`, `occt-import-js.wasm`, and
175
+ `occt-import-js-worker.js` from the installed `@sunbox/occt-import-js` package.
176
+ The package worker is persistent and serialized per loader. When Web Workers
177
+ are unavailable, the viewer dynamically imports the ESM factory directly; it
178
+ does not inject a classic script or depend on a global factory. Worker transfer
179
+ uses a loader-owned byte snapshot, and rejected ESM initialization is evicted
180
+ so callers retain their input and can retry transient failures.
181
+
166
182
  Static `authHeaders` are sent only to the main model origin. A host that
167
183
  intentionally authorizes another origin can return headers from
168
184
  `authHeadersForUrl(url, { mainUrl, sameOrigin, label })`. Each fetch scope
@@ -14,6 +14,10 @@ KiCad, Gerber, or other format-specific scene builder.
14
14
  Prepared contexts are the fastest repeated-render path because validation and
15
15
  the adapter's `elements` index are built at most once.
16
16
 
17
+ Canonical document envelopes and prepared contexts retain `source.format` as
18
+ the normalized scene's `sourceFormat`. Dense element arrays have no canonical
19
+ source metadata and therefore use `circuitjson`.
20
+
17
21
  ## Direct Controller Input
18
22
 
19
23
  Pass any accepted CircuitJSON shape as the `documentModel`. The controller
@@ -403,13 +407,15 @@ expected capsule outline:
403
407
 
404
408
  SMT pads, plated holes, and vias honor `is_covered_with_solder_mask` and
405
409
  `covered_with_solder_mask` when present. `true` keeps the copper under solder
406
- mask, while `false` exposes the copper on the applicable board side. For vias,
407
- covered values map to tenting metadata on both sides.
410
+ mask, while `false` exposes the copper on the applicable board side. Standard
411
+ vias also honor `is_tented`; omitted via tenting defaults to covered on both
412
+ sides, while `is_tented: false` retains an explicit opening.
408
413
 
409
414
  Copper pours can use rectangular, polygon, or B-Rep geometry. Rectangular pours
410
415
  support `rotation` or `ccw_rotation`; polygon and B-Rep point coordinates are
411
- converted from millimeters to mils. `covered_with_solder_mask: true` renders the
412
- zone under solder mask, while false or omitted values expose the copper:
416
+ converted from millimeters to mils. Omitted or true
417
+ `covered_with_solder_mask` renders the zone under solder mask, while an
418
+ explicit false value exposes the copper:
413
419
 
414
420
  ```js
415
421
  {
@@ -450,6 +456,11 @@ one track segment:
450
456
  }
451
457
  ```
452
458
 
459
+ Trace-level or route-entry `covered_with_solder_mask` values control the
460
+ rendered opening. Omitted values default to covered; an explicit false value
461
+ remains exposed. A route-entry value takes precedence over its trace-level
462
+ value.
463
+
453
464
  Route entries with `route_type: 'via'` produce via primitives when their
454
465
  `from_layer`, `to_layer`, or `layer` touches the top or bottom surface. Adjacent
455
466
  surface wire segments remain visible through those vias. Inner-only vias and
@@ -0,0 +1,37 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 André Fiedler
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # PCB Scene3D Viewer 1.2.1
7
+
8
+ Version 1.2.1 removes the host-specific OCCT integration layer and consumes the
9
+ installed importer package directly.
10
+
11
+ ## Compatibility changes
12
+
13
+ - Requires `@sunbox/occt-import-js ^0.0.28`.
14
+ - STEP workers now load from the scoped package path
15
+ `/node_modules/@sunbox/occt-import-js/dist/occt-import-js-worker.js`.
16
+ - The no-worker path dynamically imports the package ESM factory and resolves
17
+ its WASM through the same package directory.
18
+ - Classic script injection, global `occtimportjs` lookup, unscoped package
19
+ aliases, and app-vendored importer paths are no longer used.
20
+ - The package worker stays persistent across imports and failed model or worker
21
+ requests remain retryable through the existing cache/reset behavior.
22
+ - Byte-backed model inputs are copied into loader ownership before worker
23
+ transfer, so the caller's typed arrays and buffers are never detached.
24
+ - Rejected no-worker ESM initialization attempts are evicted, allowing a later
25
+ load to recover from transient module or WASM delivery failures.
26
+
27
+ Hosts serving browser dependencies must expose the installed package `dist/`
28
+ directory byte-for-byte at
29
+ `/node_modules/@sunbox/occt-import-js/dist/`. No copied JavaScript, WASM, or
30
+ custom worker is required.
31
+
32
+ ## Performance
33
+
34
+ The viewer consumes the optimized 0.0.28 importer build and continues to retain
35
+ typed mesh arrays, compact face-color runs, persistent worker reuse, and parsed
36
+ model caching. This avoids script duplication and keeps large STEP payloads off
37
+ the main browser thread when workers are available.
@@ -0,0 +1,33 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 André Fiedler
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # PCB Scene3D Viewer 1.2.2
7
+
8
+ Version 1.2.2 preserves canonical source identity and aligns solder-mask
9
+ coverage with the converged CircuitJSON contract.
10
+
11
+ ## API and behavior changes
12
+
13
+ - Canonical document and prepared-context inputs now retain their exact
14
+ `source.format` as the rendered scene's `sourceFormat`. Dense CircuitJSON
15
+ arrays continue to use the `circuitjson` fallback because they carry no
16
+ canonical source metadata.
17
+ - Routed traces and copper pours now default omitted
18
+ `covered_with_solder_mask` values to covered. Explicit false values remain
19
+ exposed through the solder mask.
20
+ - Standard vias now honor canonical `is_tented` metadata. Omitted values
21
+ default to tented, while `is_tented: false` remains exposed.
22
+ - Covered canonical copper follows the existing solder-mask material palette,
23
+ including tracks, pours, and via annuli.
24
+
25
+ Consumers that used the generic `circuitjson` marker for canonical documents
26
+ must now handle the retained source identity such as `gerber`, `altium`, or
27
+ `kicad`.
28
+
29
+ ## Dependencies and validation
30
+
31
+ - Requires `circuitjson-toolkit ^1.1.2` and Node.js 20 or newer.
32
+ - The full test suite and both owned performance benchmarks cover the canonical
33
+ source, coverage, material, exact-geometry, and prepared-context paths.
package/docs/testing.md CHANGED
@@ -12,6 +12,7 @@ The test suite covers:
12
12
  copper, and silkscreen;
13
13
  - runtime camera, preset, resizing, selection, and visibility behavior;
14
14
  - external STEP/STP, WRL/VRML, STL, OBJ, GLTF/GLB, and 3MF live loading;
15
+ - direct ESM and package-owned worker loading for the installed OCCT importer;
15
16
  - model ZIP archive export;
16
17
  - optional shell renderer and CSS contract;
17
18
  - worker-client request routing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pcb-scene3d-viewer",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Reusable Three.js PCB 3D scene viewer for normalized ECAD and CircuitJSON scene descriptions",
5
5
  "keywords": [
6
6
  "pcb",
@@ -34,6 +34,8 @@
34
34
  "docs/api.md",
35
35
  "docs/circuitjson.md",
36
36
  "docs/release-notes-v1.2.0.md",
37
+ "docs/release-notes-v1.2.1.md",
38
+ "docs/release-notes-v1.2.2.md",
37
39
  "docs/model-format.md",
38
40
  "docs/testing.md",
39
41
  "spec",
@@ -54,8 +56,8 @@
54
56
  "check:format": "prettier --check ."
55
57
  },
56
58
  "dependencies": {
57
- "@sunbox/occt-import-js": "^0.0.25",
58
- "circuitjson-toolkit": "^1.1.0",
59
+ "@sunbox/occt-import-js": "^0.0.28",
60
+ "circuitjson-toolkit": "^1.1.2",
59
61
  "earcut": "3.0.2",
60
62
  "fflate": "^0.8.2",
61
63
  "polygon-clipping": "^0.15.7",
@@ -131,7 +131,7 @@ export class PcbScene3dCircuitJsonAdapter {
131
131
  )
132
132
 
133
133
  return {
134
- sourceFormat: 'circuitjson',
134
+ sourceFormat: context.source?.format || 'circuitjson',
135
135
  coordinateSystem: 'circuitjson-mm',
136
136
  board,
137
137
  components,
@@ -257,7 +257,12 @@ export class PcbScene3dCircuitJsonCopperPourBuilder {
257
257
  */
258
258
  static #isCoveredWithMask(pour) {
259
259
  const value = pour?.covered_with_solder_mask
260
- return value === true || String(value).toLowerCase() === 'true'
260
+ if (typeof value === 'boolean') return value
261
+ if (value === undefined || value === null || value === '') return true
262
+
263
+ const text = String(value).trim().toLowerCase()
264
+ if (text === 'false') return false
265
+ return true
261
266
  }
262
267
 
263
268
  /**
@@ -85,7 +85,11 @@ export class PcbScene3dCircuitJsonTraceRouteBuilder {
85
85
  0.1524
86
86
  ),
87
87
  layerId: PcbScene3dCircuitJsonLayer.layerId(side),
88
- solderMaskOpening: true
88
+ solderMaskOpening:
89
+ PcbScene3dCircuitJsonTraceRouteBuilder.#solderMaskOpening(
90
+ entry,
91
+ trace
92
+ )
89
93
  }
90
94
  }
91
95
 
@@ -199,7 +203,12 @@ export class PcbScene3dCircuitJsonTraceRouteBuilder {
199
203
  0.1524
200
204
  ),
201
205
  layerId: PcbScene3dCircuitJsonLayer.layerId(side),
202
- solderMaskOpening: true
206
+ solderMaskOpening:
207
+ PcbScene3dCircuitJsonTraceRouteBuilder.#solderMaskOpening(
208
+ start,
209
+ end,
210
+ trace
211
+ )
203
212
  }
204
213
  }
205
214
 
@@ -303,19 +312,50 @@ export class PcbScene3dCircuitJsonTraceRouteBuilder {
303
312
  * @returns {boolean}
304
313
  */
305
314
  static #isSolderMaskCovered(via) {
306
- const value =
307
- via?.is_covered_with_solder_mask ?? via?.covered_with_solder_mask
308
- if (typeof value === 'boolean') {
309
- return value
315
+ if (typeof via?.is_tented === 'boolean') {
316
+ return via.is_tented
310
317
  }
311
-
312
318
  return (
313
- String(value || '')
314
- .trim()
315
- .toLowerCase() === 'true'
319
+ PcbScene3dCircuitJsonTraceRouteBuilder.#solderMaskCoveredValue(
320
+ via
321
+ ) ?? true
316
322
  )
317
323
  }
318
324
 
325
+ /**
326
+ * Resolves whether trace-like copper has an explicitly authored opening.
327
+ * @param {...object} elements Route entries followed by their trace.
328
+ * @returns {boolean}
329
+ */
330
+ static #solderMaskOpening(...elements) {
331
+ for (const element of elements) {
332
+ const covered =
333
+ PcbScene3dCircuitJsonTraceRouteBuilder.#solderMaskCoveredValue(
334
+ element
335
+ )
336
+ if (covered !== null) return !covered
337
+ }
338
+ return false
339
+ }
340
+
341
+ /**
342
+ * Reads one explicit solder-mask coverage value.
343
+ * @param {object} element CircuitJSON copper element.
344
+ * @returns {boolean | null}
345
+ */
346
+ static #solderMaskCoveredValue(element) {
347
+ const value =
348
+ element?.is_covered_with_solder_mask ??
349
+ element?.covered_with_solder_mask
350
+ if (typeof value === 'boolean') return value
351
+ if (value === undefined || value === null || value === '') return null
352
+
353
+ const text = String(value).trim().toLowerCase()
354
+ if (text === 'true') return true
355
+ if (text === 'false') return false
356
+ return null
357
+ }
358
+
319
359
  /**
320
360
  * Converts a route point from millimeters to mils.
321
361
  * @param {object | undefined} point Source point.
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Loads the ESM-shaped OCCT browser package without relying on global scripts.
3
+ */
4
+ export class PcbScene3dOcctImporterLoader {
5
+ /** @type {Map<string, Promise<Record<string, any>>>} */
6
+ static #cachedImports = new Map()
7
+
8
+ /**
9
+ * Imports and instantiates the OCCT module from its installed package.
10
+ * @param {{ resolveAssetUrl: (fileName: string) => string, loadModule?: (url: string) => Promise<Record<string, any>> }} options Loader options.
11
+ * @returns {Promise<Record<string, any>>} Initialized OCCT importer.
12
+ */
13
+ static async load(options) {
14
+ const resolveAssetUrl = options?.resolveAssetUrl
15
+ if (typeof resolveAssetUrl !== 'function') {
16
+ throw new TypeError('OCCT asset URL resolver is required.')
17
+ }
18
+
19
+ const loadModule =
20
+ options?.loadModule ||
21
+ ((url) => PcbScene3dOcctImporterLoader.#importModule(url))
22
+ const module = await loadModule(resolveAssetUrl('occt-import-js.js'))
23
+ const factory = module?.default || module?.occtimportjs
24
+ if (typeof factory !== 'function') {
25
+ throw new Error('occt-import-js did not export a factory.')
26
+ }
27
+
28
+ return await factory({
29
+ locateFile: (fileName) => resolveAssetUrl(fileName)
30
+ })
31
+ }
32
+
33
+ /**
34
+ * Loads one importer per resolved module URL and evicts rejected attempts.
35
+ * @param {{ resolveAssetUrl: (fileName: string) => string, loadModule?: (url: string) => Promise<Record<string, any>> }} options Loader options.
36
+ * @returns {Promise<Record<string, any>>} Cached initialized OCCT importer.
37
+ */
38
+ static async loadCached(options) {
39
+ const resolveAssetUrl = options?.resolveAssetUrl
40
+ if (typeof resolveAssetUrl !== 'function') {
41
+ throw new TypeError('OCCT asset URL resolver is required.')
42
+ }
43
+
44
+ const moduleUrl = resolveAssetUrl('occt-import-js.js')
45
+ let pendingImport =
46
+ PcbScene3dOcctImporterLoader.#cachedImports.get(moduleUrl)
47
+ if (!pendingImport) {
48
+ pendingImport = PcbScene3dOcctImporterLoader.load(options)
49
+ PcbScene3dOcctImporterLoader.#cachedImports.set(
50
+ moduleUrl,
51
+ pendingImport
52
+ )
53
+ }
54
+
55
+ try {
56
+ return await pendingImport
57
+ } catch (error) {
58
+ if (
59
+ PcbScene3dOcctImporterLoader.#cachedImports.get(moduleUrl) ===
60
+ pendingImport
61
+ ) {
62
+ PcbScene3dOcctImporterLoader.#cachedImports.delete(moduleUrl)
63
+ }
64
+ throw error
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Dynamically imports one browser module URL.
70
+ * @param {string} url Module URL.
71
+ * @returns {Promise<Record<string, any>>} Imported namespace.
72
+ */
73
+ static async #importModule(url) {
74
+ return await import(url)
75
+ }
76
+ }
77
+
78
+ Object.freeze(PcbScene3dOcctImporterLoader.prototype)
79
+ Object.freeze(PcbScene3dOcctImporterLoader)
@@ -1,18 +1,10 @@
1
1
  import { PcbScene3dModelIdentity } from './PcbScene3dModelIdentity.mjs'
2
+ import { PcbScene3dOcctImporterLoader } from './PcbScene3dOcctImporterLoader.mjs'
2
3
 
3
4
  /**
4
5
  * Browser-side STEP mesh loader backed by occt-import-js.
5
6
  */
6
7
  export class PcbScene3dStepLoader {
7
- /** @type {(() => Promise<{ ReadStepFile?: (content: Uint8Array, params: Record<string, any> | null) => any }>) | null} */
8
- static #browserImporterLoader
9
-
10
- /** @type {Promise<{ ReadStepFile?: (content: Uint8Array, params: Record<string, any> | null) => any }> | null} */
11
- static #browserImporterPromise
12
-
13
- /** @type {Promise<void> | null} */
14
- static #browserScriptPromise
15
-
16
8
  /** @type {() => Promise<{ ReadStepFile?: (content: Uint8Array, params: Record<string, any> | null) => any }>} */
17
9
  #importerLoader
18
10
 
@@ -182,7 +174,7 @@ export class PcbScene3dStepLoader {
182
174
 
183
175
  /**
184
176
  * Reads one importer result through the loader-owned persistent worker.
185
- * Requests are serialized because the vendored worker does not echo a
177
+ * Requests are serialized because the package worker does not echo a
186
178
  * request id back in its response payload.
187
179
  * @param {Uint8Array} content
188
180
  * @returns {Promise<any>}
@@ -347,20 +339,20 @@ export class PcbScene3dStepLoader {
347
339
  return null
348
340
  }
349
341
  if (value instanceof Uint8Array) {
350
- return value
342
+ return new Uint8Array(value)
351
343
  }
352
344
  if (value instanceof ArrayBuffer) {
353
- return new Uint8Array(value)
345
+ return new Uint8Array(value.slice(0))
354
346
  }
355
347
  if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
356
348
  return new Uint8Array(
357
349
  value.buffer,
358
350
  value.byteOffset,
359
351
  value.byteLength
360
- )
352
+ ).slice()
361
353
  }
362
354
  if (typeof value.arrayBuffer === 'function') {
363
- return new Uint8Array(await value.arrayBuffer())
355
+ return new Uint8Array(await value.arrayBuffer()).slice()
364
356
  }
365
357
  return null
366
358
  }
@@ -599,39 +591,10 @@ export class PcbScene3dStepLoader {
599
591
  * @returns {Promise<{ ReadStepFile?: (content: Uint8Array, params: Record<string, any> | null) => any }>}
600
592
  */
601
593
  static async #loadBrowserImporter() {
602
- if (!PcbScene3dStepLoader.#browserImporterLoader) {
603
- PcbScene3dStepLoader.#browserImporterLoader = async () => {
604
- if (
605
- typeof window === 'undefined' ||
606
- typeof document === 'undefined'
607
- ) {
608
- throw new Error(
609
- 'Browser STEP importer requires window and document.'
610
- )
611
- }
612
-
613
- await PcbScene3dStepLoader.#ensureBrowserScript()
614
-
615
- const factory = globalThis.occtimportjs
616
- if (typeof factory !== 'function') {
617
- throw new Error(
618
- 'occt-import-js did not register a browser factory.'
619
- )
620
- }
621
-
622
- return await factory({
623
- locateFile: (fileName) =>
624
- PcbScene3dStepLoader.#resolveVendorAssetUrl(fileName)
625
- })
626
- }
627
- }
628
-
629
- if (!PcbScene3dStepLoader.#browserImporterPromise) {
630
- PcbScene3dStepLoader.#browserImporterPromise =
631
- PcbScene3dStepLoader.#browserImporterLoader()
632
- }
633
-
634
- return await PcbScene3dStepLoader.#browserImporterPromise
594
+ return await PcbScene3dOcctImporterLoader.loadCached({
595
+ resolveAssetUrl: (fileName) =>
596
+ PcbScene3dStepLoader.#resolveImporterAssetUrl(fileName)
597
+ })
635
598
  }
636
599
 
637
600
  /**
@@ -645,77 +608,24 @@ export class PcbScene3dStepLoader {
645
608
 
646
609
  return () =>
647
610
  new globalThis.Worker(
648
- PcbScene3dStepLoader.#resolveVendorAssetUrl(
611
+ PcbScene3dStepLoader.#resolveImporterAssetUrl(
649
612
  'occt-import-js-worker.js'
650
613
  )
651
614
  )
652
615
  }
653
616
 
654
617
  /**
655
- * Ensures the browser importer script has been loaded once.
656
- * @returns {Promise<void>}
657
- */
658
- static async #ensureBrowserScript() {
659
- if (PcbScene3dStepLoader.#browserScriptPromise) {
660
- return await PcbScene3dStepLoader.#browserScriptPromise
661
- }
662
-
663
- PcbScene3dStepLoader.#browserScriptPromise = new Promise(
664
- (resolve, reject) => {
665
- const existingScript = document.querySelector(
666
- 'script[data-occt-import-js]'
667
- )
668
- if (existingScript) {
669
- existingScript.addEventListener('load', () => resolve(), {
670
- once: true
671
- })
672
- existingScript.addEventListener(
673
- 'error',
674
- () =>
675
- reject(
676
- new Error(
677
- 'STEP importer script failed to load.'
678
- )
679
- ),
680
- { once: true }
681
- )
682
- if (typeof globalThis.occtimportjs === 'function') {
683
- resolve()
684
- }
685
- return
686
- }
687
-
688
- const script = document.createElement('script')
689
- script.async = true
690
- script.dataset.occtImportJs = 'true'
691
- script.src =
692
- PcbScene3dStepLoader.#resolveVendorAssetUrl(
693
- 'occt-import-js.js'
694
- )
695
- script.addEventListener('load', () => resolve(), { once: true })
696
- script.addEventListener(
697
- 'error',
698
- () =>
699
- reject(
700
- new Error('STEP importer script failed to load.')
701
- ),
702
- { once: true }
703
- )
704
- document.head.append(script)
705
- }
706
- )
707
-
708
- return await PcbScene3dStepLoader.#browserScriptPromise
709
- }
710
-
711
- /**
712
- * Resolves one vendored importer asset URL with the current app version.
618
+ * Resolves one installed importer asset URL with the current app version.
713
619
  * @param {string} fileName
714
620
  * @returns {string}
715
621
  */
716
- static #resolveVendorAssetUrl(fileName) {
622
+ static #resolveImporterAssetUrl(fileName) {
717
623
  const versionKey = new URL(import.meta.url).searchParams.get('v') || ''
718
624
  const suffix = versionKey ? '?v=' + encodeURIComponent(versionKey) : ''
719
- return '/vendor/occt-import-js/dist/' + String(fileName || '') + suffix
625
+ return (
626
+ '/node_modules/@sunbox/occt-import-js/dist/' +
627
+ String(fileName || '') +
628
+ suffix
629
+ )
720
630
  }
721
631
  }