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 +8 -5
- package/README.md +9 -1
- package/docs/api.md +8 -0
- package/docs/release-notes-v1.2.1.md +37 -0
- package/docs/testing.md +1 -0
- package/package.json +3 -2
- package/src/PcbScene3dOcctImporterLoader.mjs +79 -0
- package/src/PcbScene3dStepLoader.mjs +18 -108
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
|
-
|
|
31
|
-
In particular,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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.#
|
|
611
|
+
PcbScene3dStepLoader.#resolveImporterAssetUrl(
|
|
649
612
|
'occt-import-js-worker.js'
|
|
650
613
|
)
|
|
651
614
|
)
|
|
652
615
|
}
|
|
653
616
|
|
|
654
617
|
/**
|
|
655
|
-
*
|
|
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 #
|
|
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
|
|
625
|
+
return (
|
|
626
|
+
'/node_modules/@sunbox/occt-import-js/dist/' +
|
|
627
|
+
String(fileName || '') +
|
|
628
|
+
suffix
|
|
629
|
+
)
|
|
720
630
|
}
|
|
721
631
|
}
|