pcb-scene3d-viewer 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.1 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
@@ -57,6 +57,13 @@ stay on the main model origin; `authHeadersForUrl` is the explicit per-URL
57
57
  override. The model ZIP exporter uses the same policy and writes each raw model
58
58
  under its original source basename with safe GLTF, OBJ, and WRL companions.
59
59
 
60
+ STEP loading uses the installed `@sunbox/occt-import-js` package directly. Its
61
+ package-owned worker is reused for browser imports, while runtimes without Web
62
+ Workers dynamically import the same ESM factory. Hosts only need to serve the
63
+ package `dist/` directory at
64
+ `/node_modules/@sunbox/occt-import-js/dist/`; no copied runtime, global script,
65
+ or host-owned worker is required.
66
+
60
67
  ## Install
61
68
 
62
69
  ```bash
@@ -116,6 +123,7 @@ const controller = new PcbScene3dController(viewportNode, document)
116
123
 
117
124
  - [API](docs/api.md)
118
125
  - [CircuitJSON usage](docs/circuitjson.md)
126
+ - [1.2.1 release notes](docs/release-notes-v1.2.1.md)
119
127
  - [1.2.0 release notes](docs/release-notes-v1.2.0.md)
120
128
  - [Model format](docs/model-format.md)
121
129
  - [Testing](docs/testing.md)
package/docs/api.md CHANGED
@@ -163,6 +163,14 @@ the model, preventing implicit texture networking. STL, OBJ, GLTF, and GLB use
163
163
  the shared faceted mesh pipeline so runtime and assembly export preserve the
164
164
  same units, material color, opacity, and vertex-color behavior.
165
165
 
166
+ STEP imports resolve `occt-import-js.js`, `occt-import-js.wasm`, and
167
+ `occt-import-js-worker.js` from the installed `@sunbox/occt-import-js` package.
168
+ The package worker is persistent and serialized per loader. When Web Workers
169
+ are unavailable, the viewer dynamically imports the ESM factory directly; it
170
+ does not inject a classic script or depend on a global factory. Worker transfer
171
+ uses a loader-owned byte snapshot, and rejected ESM initialization is evicted
172
+ so callers retain their input and can retry transient failures.
173
+
166
174
  Static `authHeaders` are sent only to the main model origin. A host that
167
175
  intentionally authorizes another origin can return headers from
168
176
  `authHeadersForUrl(url, { mainUrl, sameOrigin, label })`. Each fetch scope
@@ -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.
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.1",
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,7 @@
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",
37
38
  "docs/model-format.md",
38
39
  "docs/testing.md",
39
40
  "spec",
@@ -54,7 +55,7 @@
54
55
  "check:format": "prettier --check ."
55
56
  },
56
57
  "dependencies": {
57
- "@sunbox/occt-import-js": "^0.0.25",
58
+ "@sunbox/occt-import-js": "^0.0.28",
58
59
  "circuitjson-toolkit": "^1.1.0",
59
60
  "earcut": "3.0.2",
60
61
  "fflate": "^0.8.2",
@@ -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
  }