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.
- package/README.md +46 -21
- package/docs/api.md +109 -8
- package/docs/circuitjson.md +143 -29
- package/docs/model-format.md +50 -7
- package/docs/release-notes-v1.2.0.md +122 -0
- package/docs/testing.md +30 -1
- package/package.json +6 -3
- package/spec/library-scope.md +10 -2
- package/src/CircuitJsonCadModelAssetResolver.mjs +697 -83
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +43 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +40 -18
- package/src/PcbAssemblyModelMeshLoader.mjs +101 -150
- package/src/PcbAssemblyPadMeshBuilder.mjs +22 -0
- package/src/PcbModelArchiveExporter.mjs +28 -112
- package/src/PcbModelArchiveSourceBundle.mjs +326 -0
- package/src/PcbScene3dAabbIndex.mjs +464 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -1
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +9 -5
- package/src/PcbScene3dBoardMaterialPalette.mjs +29 -0
- package/src/PcbScene3dBoardShapeFactory.mjs +11 -118
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +65 -39
- package/src/PcbScene3dCircuitJsonAdapter.mjs +151 -48
- package/src/PcbScene3dCircuitJsonDrillDetail.mjs +31 -0
- package/src/PcbScene3dCircuitJsonGeometry.mjs +184 -33
- package/src/PcbScene3dCircuitJsonInput.mjs +132 -0
- package/src/PcbScene3dCircuitJsonModelAsset.mjs +40 -0
- package/src/PcbScene3dController.mjs +40 -34
- package/src/PcbScene3dCopperFactory.mjs +36 -22
- package/src/PcbScene3dCopperFillAreaClipper.mjs +133 -235
- package/src/PcbScene3dCopperFillCoverageContext.mjs +204 -0
- package/src/PcbScene3dCopperFillLoopSetResolver.mjs +192 -0
- package/src/PcbScene3dCopperFillMeshBuilder.mjs +77 -295
- package/src/PcbScene3dCopperTextFactory.mjs +12 -4
- package/src/PcbScene3dCutoutCircleDetector.mjs +34 -17
- package/src/PcbScene3dCutoutGeometryFilter.mjs +104 -269
- package/src/PcbScene3dCutoutGridIndex.mjs +184 -0
- package/src/PcbScene3dDeferredModelFinalizer.mjs +52 -0
- package/src/PcbScene3dDescriptorSafeRecord.mjs +38 -0
- package/src/PcbScene3dDrillCutoutFilter.mjs +149 -143
- package/src/PcbScene3dDrillPathFactory.mjs +86 -16
- package/src/PcbScene3dDrillVoidFactory.mjs +35 -10
- package/src/PcbScene3dExternalModelGroupLoader.mjs +472 -31
- package/src/PcbScene3dExternalModels.mjs +23 -24
- package/src/PcbScene3dFacetedModelGroupBuilder.mjs +217 -0
- package/src/PcbScene3dGeometryZCompressor.mjs +4 -2
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +18 -4
- package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +75 -10
- package/src/PcbScene3dModelContent.mjs +236 -0
- package/src/PcbScene3dModelFetchPolicy.mjs +304 -0
- package/src/PcbScene3dModelIdentity.mjs +106 -0
- package/src/PcbScene3dPlatedDrillSpecResolver.mjs +141 -0
- package/src/PcbScene3dPreparedPolygon.mjs +709 -0
- package/src/PcbScene3dPreparedPolygonSet.mjs +70 -0
- package/src/PcbScene3dRuntime.mjs +45 -45
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +85 -1
- package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +4 -2
- package/src/PcbScene3dShellRenderer.mjs +75 -6
- package/src/PcbScene3dSilkscreenCutoutContext.mjs +255 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +87 -127
- package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +11 -5
- package/src/PcbScene3dStepLoader.mjs +11 -10
- package/src/PcbScene3dText.mjs +1 -1
- package/src/PcbScene3dTriangleVertexQueryBounds.mjs +302 -0
|
@@ -2,6 +2,9 @@ import { zipSync } from 'fflate'
|
|
|
2
2
|
import { PcbAssemblyMeshUtils } from './PcbAssemblyMeshUtils.mjs'
|
|
3
3
|
import { PcbAssemblyModelMeshLoader } from './PcbAssemblyModelMeshLoader.mjs'
|
|
4
4
|
import { PcbAssemblyStepWriter } from './PcbAssemblyStepWriter.mjs'
|
|
5
|
+
import { PcbModelArchiveSourceBundle } from './PcbModelArchiveSourceBundle.mjs'
|
|
6
|
+
import { PcbScene3dModelContent } from './PcbScene3dModelContent.mjs'
|
|
7
|
+
import { PcbScene3dModelIdentity } from './PcbScene3dModelIdentity.mjs'
|
|
5
8
|
|
|
6
9
|
const STATIC_BODY_COLOR = [0.5, 0.5, 0.5]
|
|
7
10
|
|
|
@@ -12,10 +15,13 @@ const STATIC_BODY_COLOR = [0.5, 0.5, 0.5]
|
|
|
12
15
|
export class PcbModelArchiveExporter {
|
|
13
16
|
/**
|
|
14
17
|
* Builds one ZIP archive from the resolved scene description.
|
|
15
|
-
* @param {{ archiveBaseName?: string, sceneDescription?: { components?: any[], externalPlacements?: any[], staticBodyPlacements?: any[] }, modelMeshLoader?: ((placement: object) => Promise<object | object[]> | object | object[]) | PcbAssemblyModelMeshLoader, includeRawModels?: boolean, includeStitchedComponents?: boolean, stitchedDesignators?: string[] }} [options]
|
|
18
|
+
* @param {{ archiveBaseName?: string, sceneDescription?: { components?: any[], externalPlacements?: any[], staticBodyPlacements?: any[] }, modelMeshLoader?: ((placement: object) => Promise<object | object[]> | object | object[]) | PcbAssemblyModelMeshLoader, modelLoaderOptions?: object, includeRawModels?: boolean, includeStitchedComponents?: boolean, stitchedDesignators?: string[] }} [options]
|
|
16
19
|
* @returns {Promise<{ archiveName: string, archiveBytes: Uint8Array, exportedEntries: { archivePath: string, pattern: string, designator: string, format: string, modelName: string, kind?: string }[], skippedEntries: { designator: string, reason: string }[] }>}
|
|
17
20
|
*/
|
|
18
21
|
static async buildArchive(options = {}) {
|
|
22
|
+
const modelLoaderOptions = PcbScene3dModelContent.createFetchScope(
|
|
23
|
+
options.modelLoaderOptions
|
|
24
|
+
)
|
|
19
25
|
const archiveName = PcbModelArchiveExporter.#resolveArchiveName(
|
|
20
26
|
options.archiveBaseName
|
|
21
27
|
)
|
|
@@ -28,18 +34,20 @@ export class PcbModelArchiveExporter {
|
|
|
28
34
|
const exportedEntries = []
|
|
29
35
|
const skippedEntries = []
|
|
30
36
|
const archiveInput = {}
|
|
37
|
+
const rawBundleDirectories = new Set()
|
|
31
38
|
|
|
32
39
|
for (const entry of resolvedEntries) {
|
|
33
40
|
try {
|
|
34
|
-
const
|
|
41
|
+
const bundle = await PcbModelArchiveSourceBundle.build(
|
|
35
42
|
entry,
|
|
36
|
-
|
|
43
|
+
rawBundleDirectories,
|
|
44
|
+
modelLoaderOptions
|
|
37
45
|
)
|
|
38
|
-
|
|
39
|
-
await PcbModelArchiveExporter.#readModelBytes(entry.model)
|
|
40
|
-
archiveInput[archivePath] = modelBytes
|
|
46
|
+
Object.assign(archiveInput, bundle.files)
|
|
41
47
|
exportedEntries.push({
|
|
42
|
-
archivePath,
|
|
48
|
+
archivePath: bundle.archivePath,
|
|
49
|
+
bundleDirectory: bundle.bundleDirectory,
|
|
50
|
+
companionPaths: bundle.companionPaths,
|
|
43
51
|
pattern: entry.pattern,
|
|
44
52
|
designator: entry.designator,
|
|
45
53
|
format: entry.model.format,
|
|
@@ -61,7 +69,8 @@ export class PcbModelArchiveExporter {
|
|
|
61
69
|
: await PcbModelArchiveExporter.#buildStitchedEntries(
|
|
62
70
|
options.sceneDescription,
|
|
63
71
|
options.modelMeshLoader,
|
|
64
|
-
options.stitchedDesignators
|
|
72
|
+
options.stitchedDesignators,
|
|
73
|
+
modelLoaderOptions
|
|
65
74
|
)
|
|
66
75
|
skippedEntries.push(...stitchedEntries.skippedEntries)
|
|
67
76
|
for (const entry of stitchedEntries.entries) {
|
|
@@ -197,12 +206,14 @@ export class PcbModelArchiveExporter {
|
|
|
197
206
|
* @param {{ components?: object[], externalPlacements?: object[], staticBodyPlacements?: object[] } | undefined} sceneDescription Scene data.
|
|
198
207
|
* @param {((placement: object) => Promise<object | object[]> | object | object[]) | PcbAssemblyModelMeshLoader | undefined} modelMeshLoader Mesh loader override.
|
|
199
208
|
* @param {string[] | undefined} stitchedDesignators Optional designator filter.
|
|
209
|
+
* @param {object | undefined} modelLoaderOptions Shared model loading policy.
|
|
200
210
|
* @returns {Promise<{ entries: { pattern: string, designator: string, modelName: string, payloadText: string }[], skippedEntries: { designator: string, reason: string }[] }>}
|
|
201
211
|
*/
|
|
202
212
|
static async #buildStitchedEntries(
|
|
203
213
|
sceneDescription,
|
|
204
214
|
modelMeshLoader,
|
|
205
|
-
stitchedDesignators
|
|
215
|
+
stitchedDesignators,
|
|
216
|
+
modelLoaderOptions
|
|
206
217
|
) {
|
|
207
218
|
const candidates = PcbModelArchiveExporter.#collectStitchedCandidates(
|
|
208
219
|
sceneDescription,
|
|
@@ -212,8 +223,10 @@ export class PcbModelArchiveExporter {
|
|
|
212
223
|
return { entries: [], skippedEntries: [] }
|
|
213
224
|
}
|
|
214
225
|
|
|
215
|
-
const loader =
|
|
216
|
-
|
|
226
|
+
const loader = PcbModelArchiveExporter.#resolveModelMeshLoader(
|
|
227
|
+
modelMeshLoader,
|
|
228
|
+
modelLoaderOptions
|
|
229
|
+
)
|
|
217
230
|
const ownsLoader =
|
|
218
231
|
!(
|
|
219
232
|
typeof modelMeshLoader === 'function' ||
|
|
@@ -473,9 +486,10 @@ export class PcbModelArchiveExporter {
|
|
|
473
486
|
/**
|
|
474
487
|
* Resolves a mesh loader for generated stitched components.
|
|
475
488
|
* @param {((placement: object) => Promise<object | object[]> | object | object[]) | PcbAssemblyModelMeshLoader | undefined} modelMeshLoader Loader option.
|
|
489
|
+
* @param {object | undefined} modelLoaderOptions Shared model loading policy.
|
|
476
490
|
* @returns {((placement: object) => Promise<object | object[]> | object | object[]) | PcbAssemblyModelMeshLoader}
|
|
477
491
|
*/
|
|
478
|
-
static #resolveModelMeshLoader(modelMeshLoader) {
|
|
492
|
+
static #resolveModelMeshLoader(modelMeshLoader, modelLoaderOptions) {
|
|
479
493
|
if (
|
|
480
494
|
typeof modelMeshLoader === 'function' ||
|
|
481
495
|
modelMeshLoader instanceof PcbAssemblyModelMeshLoader
|
|
@@ -483,7 +497,7 @@ export class PcbModelArchiveExporter {
|
|
|
483
497
|
return modelMeshLoader
|
|
484
498
|
}
|
|
485
499
|
|
|
486
|
-
return new PcbAssemblyModelMeshLoader()
|
|
500
|
+
return new PcbAssemblyModelMeshLoader(modelLoaderOptions)
|
|
487
501
|
}
|
|
488
502
|
|
|
489
503
|
/**
|
|
@@ -573,36 +587,6 @@ export class PcbModelArchiveExporter {
|
|
|
573
587
|
return componentByDesignator
|
|
574
588
|
}
|
|
575
589
|
|
|
576
|
-
/**
|
|
577
|
-
* Resolves one archive file name for the exported model.
|
|
578
|
-
* @param {{ pattern: string, model: { format?: string } }} entry
|
|
579
|
-
* @param {{ archivePath: string }[]} exportedEntries
|
|
580
|
-
* @returns {string}
|
|
581
|
-
*/
|
|
582
|
-
static #resolveArchivePath(entry, exportedEntries) {
|
|
583
|
-
const sanitizedPattern = PcbModelArchiveExporter.#sanitizeFileToken(
|
|
584
|
-
entry.pattern
|
|
585
|
-
)
|
|
586
|
-
const extension = PcbModelArchiveExporter.#resolveExtension(
|
|
587
|
-
entry.model?.format
|
|
588
|
-
)
|
|
589
|
-
const basePath = sanitizedPattern + '.' + extension
|
|
590
|
-
let archivePath = basePath
|
|
591
|
-
let duplicateIndex = 2
|
|
592
|
-
|
|
593
|
-
while (
|
|
594
|
-
exportedEntries.some(
|
|
595
|
-
(exportedEntry) => exportedEntry.archivePath === archivePath
|
|
596
|
-
)
|
|
597
|
-
) {
|
|
598
|
-
archivePath =
|
|
599
|
-
sanitizedPattern + '--' + duplicateIndex + '.' + extension
|
|
600
|
-
duplicateIndex += 1
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
return archivePath
|
|
604
|
-
}
|
|
605
|
-
|
|
606
590
|
/**
|
|
607
591
|
* Resolves one archive path for a stitched component STEP.
|
|
608
592
|
* @param {{ designator: string }} entry Stitched entry.
|
|
@@ -634,28 +618,6 @@ export class PcbModelArchiveExporter {
|
|
|
634
618
|
return archivePath
|
|
635
619
|
}
|
|
636
620
|
|
|
637
|
-
/**
|
|
638
|
-
* Reads one resolved model into ZIP-ready bytes.
|
|
639
|
-
* @param {{ origin?: string, payloadText?: string, file?: Blob | File | null }} model
|
|
640
|
-
* @returns {Promise<Uint8Array>}
|
|
641
|
-
*/
|
|
642
|
-
static async #readModelBytes(model) {
|
|
643
|
-
if (model?.origin === 'embedded') {
|
|
644
|
-
const payloadText = String(model?.payloadText || '')
|
|
645
|
-
if (!payloadText) {
|
|
646
|
-
throw new Error('Embedded STEP payload is unavailable.')
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
return new TextEncoder().encode(payloadText)
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
if (typeof model?.file?.arrayBuffer === 'function') {
|
|
653
|
-
return new Uint8Array(await model.file.arrayBuffer())
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
throw new Error('Session model bytes are unavailable.')
|
|
657
|
-
}
|
|
658
|
-
|
|
659
621
|
/**
|
|
660
622
|
* Resolves one user-facing archive name from the provided base name.
|
|
661
623
|
* @param {string | undefined} archiveBaseName
|
|
@@ -704,53 +666,7 @@ export class PcbModelArchiveExporter {
|
|
|
704
666
|
* @returns {string}
|
|
705
667
|
*/
|
|
706
668
|
static #resolveModelIdentity(model) {
|
|
707
|
-
|
|
708
|
-
return [
|
|
709
|
-
'embedded',
|
|
710
|
-
String(model?.sourceStream || ''),
|
|
711
|
-
String(model?.name || ''),
|
|
712
|
-
String(model?.checksum || ''),
|
|
713
|
-
String(model?.format || '')
|
|
714
|
-
].join('::')
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
return [
|
|
718
|
-
'session',
|
|
719
|
-
String(model?.relativePath || ''),
|
|
720
|
-
String(model?.name || ''),
|
|
721
|
-
String(model?.format || '')
|
|
722
|
-
].join('::')
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/**
|
|
726
|
-
* Resolves a safe archive extension from the model format.
|
|
727
|
-
* @param {string | undefined} format
|
|
728
|
-
* @returns {string}
|
|
729
|
-
*/
|
|
730
|
-
static #resolveExtension(format) {
|
|
731
|
-
const normalizedFormat = String(format || '')
|
|
732
|
-
.trim()
|
|
733
|
-
.toLowerCase()
|
|
734
|
-
if (
|
|
735
|
-
[
|
|
736
|
-
'step',
|
|
737
|
-
'stp',
|
|
738
|
-
'wrl',
|
|
739
|
-
'vrml',
|
|
740
|
-
'glb',
|
|
741
|
-
'gltf',
|
|
742
|
-
'stl',
|
|
743
|
-
'obj'
|
|
744
|
-
].includes(normalizedFormat)
|
|
745
|
-
) {
|
|
746
|
-
return normalizedFormat === 'stp'
|
|
747
|
-
? 'step'
|
|
748
|
-
: normalizedFormat === 'vrml'
|
|
749
|
-
? 'wrl'
|
|
750
|
-
: normalizedFormat
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
return 'step'
|
|
669
|
+
return PcbScene3dModelIdentity.resolve(model)
|
|
754
670
|
}
|
|
755
671
|
|
|
756
672
|
/**
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { PcbScene3dDescriptorSafeRecord } from './PcbScene3dDescriptorSafeRecord.mjs'
|
|
2
|
+
import { PcbScene3dModelContent } from './PcbScene3dModelContent.mjs'
|
|
3
|
+
import { PcbScene3dModelIdentity } from './PcbScene3dModelIdentity.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds one self-contained raw model subtree for ZIP export.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbModelArchiveSourceBundle {
|
|
9
|
+
/**
|
|
10
|
+
* Builds one main source and its safe relative companion resources.
|
|
11
|
+
* @param {{ pattern: string, model: object }} entry Raw model entry.
|
|
12
|
+
* @param {Set<string>} usedDirectories Existing bundle directories.
|
|
13
|
+
* @param {object} modelLoaderOptions Scoped loading policy.
|
|
14
|
+
* @returns {Promise<{ archivePath: string, bundleDirectory: string, companionPaths: string[], files: Record<string, Uint8Array> }>}
|
|
15
|
+
*/
|
|
16
|
+
static async build(entry, usedDirectories, modelLoaderOptions) {
|
|
17
|
+
const bundleDirectory = PcbModelArchiveSourceBundle.#directory(
|
|
18
|
+
entry.pattern,
|
|
19
|
+
usedDirectories
|
|
20
|
+
)
|
|
21
|
+
const sourceName = PcbModelArchiveSourceBundle.#sourceName(entry.model)
|
|
22
|
+
const archivePath = bundleDirectory + '/' + sourceName
|
|
23
|
+
const mainBytes = await PcbScene3dModelContent.bytes(
|
|
24
|
+
entry.model,
|
|
25
|
+
modelLoaderOptions,
|
|
26
|
+
'Resolved'
|
|
27
|
+
)
|
|
28
|
+
const files = { [archivePath]: mainBytes }
|
|
29
|
+
const companionPaths = await PcbModelArchiveSourceBundle.#companions(
|
|
30
|
+
entry.model,
|
|
31
|
+
mainBytes,
|
|
32
|
+
bundleDirectory,
|
|
33
|
+
modelLoaderOptions,
|
|
34
|
+
files
|
|
35
|
+
)
|
|
36
|
+
usedDirectories.add(bundleDirectory)
|
|
37
|
+
return { archivePath, bundleDirectory, companionPaths, files }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Reads and writes safe relative companion resources.
|
|
42
|
+
* @param {object} model Main model metadata.
|
|
43
|
+
* @param {Uint8Array} mainBytes Main source bytes.
|
|
44
|
+
* @param {string} bundleDirectory Unique archive directory.
|
|
45
|
+
* @param {object} options Scoped loading policy.
|
|
46
|
+
* @param {Record<string, Uint8Array>} files Bundle output.
|
|
47
|
+
* @returns {Promise<string[]>} Companion archive paths.
|
|
48
|
+
*/
|
|
49
|
+
static async #companions(
|
|
50
|
+
model,
|
|
51
|
+
mainBytes,
|
|
52
|
+
bundleDirectory,
|
|
53
|
+
options,
|
|
54
|
+
files
|
|
55
|
+
) {
|
|
56
|
+
const format = String(
|
|
57
|
+
PcbModelArchiveSourceBundle.#ownData(model, 'format') || ''
|
|
58
|
+
).toLowerCase()
|
|
59
|
+
if (!['gltf', 'obj', 'wrl', 'vrml'].includes(format)) return []
|
|
60
|
+
|
|
61
|
+
const resources = PcbModelArchiveSourceBundle.#resources(model, format)
|
|
62
|
+
const byPath = new Map()
|
|
63
|
+
for (const resource of resources) {
|
|
64
|
+
const path = PcbModelArchiveSourceBundle.#resourcePath(resource)
|
|
65
|
+
if (path && !byPath.has(path)) byPath.set(path, resource)
|
|
66
|
+
}
|
|
67
|
+
const references = PcbModelArchiveSourceBundle.#references(
|
|
68
|
+
format,
|
|
69
|
+
new TextDecoder().decode(mainBytes)
|
|
70
|
+
)
|
|
71
|
+
const paths = new Set([...byPath.keys(), ...references])
|
|
72
|
+
const mainUrl = PcbModelArchiveSourceBundle.#mainUrl(model)
|
|
73
|
+
const companionPaths = []
|
|
74
|
+
for (const path of paths) {
|
|
75
|
+
const archivePath = bundleDirectory + '/' + path
|
|
76
|
+
if (Object.hasOwn(files, archivePath)) continue
|
|
77
|
+
const attached = byPath.get(path)
|
|
78
|
+
const resource = attached
|
|
79
|
+
? {
|
|
80
|
+
...PcbScene3dDescriptorSafeRecord.copy(attached),
|
|
81
|
+
mainModelUrl: mainUrl
|
|
82
|
+
}
|
|
83
|
+
: PcbModelArchiveSourceBundle.#remoteResource(path, mainUrl)
|
|
84
|
+
const bytes = await PcbScene3dModelContent.bytes(
|
|
85
|
+
resource,
|
|
86
|
+
options,
|
|
87
|
+
'Model companion'
|
|
88
|
+
)
|
|
89
|
+
files[archivePath] = bytes
|
|
90
|
+
companionPaths.push(archivePath)
|
|
91
|
+
}
|
|
92
|
+
return companionPaths
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns safe attached resource rows for one multi-file format.
|
|
97
|
+
* @param {object} model Model metadata.
|
|
98
|
+
* @param {string} format Model format.
|
|
99
|
+
* @returns {object[]} Resource rows.
|
|
100
|
+
*/
|
|
101
|
+
static #resources(model, format) {
|
|
102
|
+
const field = format === 'gltf' ? 'externalBuffers' : 'resources'
|
|
103
|
+
return PcbModelArchiveSourceBundle.#array(
|
|
104
|
+
PcbModelArchiveSourceBundle.#ownData(model, field)
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extracts safe relative resource references from a main source.
|
|
110
|
+
* @param {string} format Model format.
|
|
111
|
+
* @param {string} text Main source text.
|
|
112
|
+
* @returns {string[]} Safe unique paths.
|
|
113
|
+
*/
|
|
114
|
+
static #references(format, text) {
|
|
115
|
+
let references = []
|
|
116
|
+
if (format === 'gltf') {
|
|
117
|
+
try {
|
|
118
|
+
const document = JSON.parse(text)
|
|
119
|
+
references = [
|
|
120
|
+
...PcbModelArchiveSourceBundle.#array(document?.buffers),
|
|
121
|
+
...PcbModelArchiveSourceBundle.#array(document?.images)
|
|
122
|
+
].map((row) => row?.uri)
|
|
123
|
+
} catch {
|
|
124
|
+
references = []
|
|
125
|
+
}
|
|
126
|
+
} else if (format === 'obj') {
|
|
127
|
+
references = [...text.matchAll(/^\s*mtllib\s+(.+?)\s*$/gimu)].map(
|
|
128
|
+
(match) =>
|
|
129
|
+
String(match[1] || '')
|
|
130
|
+
.trim()
|
|
131
|
+
.replace(/^(['"])(.*)\1$/u, '$2')
|
|
132
|
+
)
|
|
133
|
+
} else {
|
|
134
|
+
for (const block of text.matchAll(
|
|
135
|
+
/ImageTexture\s*\{[\s\S]*?\}/giu
|
|
136
|
+
)) {
|
|
137
|
+
references.push(
|
|
138
|
+
...[...String(block[0]).matchAll(/(['"])([^'"]+)\1/gu)].map(
|
|
139
|
+
(match) => match[2]
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return [
|
|
145
|
+
...new Set(
|
|
146
|
+
references
|
|
147
|
+
.map((reference) =>
|
|
148
|
+
PcbModelArchiveSourceBundle.#safePath(reference)
|
|
149
|
+
)
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
)
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Creates one explicitly fetchable missing companion.
|
|
157
|
+
* @param {string} path Safe relative resource path.
|
|
158
|
+
* @param {string} mainUrl Main model URL or path.
|
|
159
|
+
* @returns {object} Resource metadata.
|
|
160
|
+
*/
|
|
161
|
+
static #remoteResource(path, mainUrl) {
|
|
162
|
+
return {
|
|
163
|
+
format: 'model-resource',
|
|
164
|
+
name: path.split('/').pop(),
|
|
165
|
+
resolvedUrl: PcbScene3dModelContent.resolveRelativeUrl(
|
|
166
|
+
path,
|
|
167
|
+
mainUrl
|
|
168
|
+
),
|
|
169
|
+
mainModelUrl: mainUrl
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolves a safe relative path from one attached resource.
|
|
175
|
+
* @param {object} resource Resource metadata.
|
|
176
|
+
* @returns {string}
|
|
177
|
+
*/
|
|
178
|
+
static #resourcePath(resource) {
|
|
179
|
+
return PcbModelArchiveSourceBundle.#safePath(
|
|
180
|
+
PcbModelArchiveSourceBundle.#ownData(resource, 'uri') ||
|
|
181
|
+
PcbModelArchiveSourceBundle.#ownData(resource, 'name')
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Normalizes a project-relative archive path without traversal or schemes.
|
|
187
|
+
* @param {unknown} value Path candidate.
|
|
188
|
+
* @returns {string}
|
|
189
|
+
*/
|
|
190
|
+
static #safePath(value) {
|
|
191
|
+
const raw = String(value || '')
|
|
192
|
+
.trim()
|
|
193
|
+
.split(/[?#]/u)[0]
|
|
194
|
+
return PcbScene3dModelContent.safeProjectPath(raw)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Selects one unique human-readable bundle directory.
|
|
199
|
+
* @param {string} pattern Component pattern.
|
|
200
|
+
* @param {Set<string>} used Existing directories.
|
|
201
|
+
* @returns {string}
|
|
202
|
+
*/
|
|
203
|
+
static #directory(pattern, used) {
|
|
204
|
+
const base = PcbModelArchiveSourceBundle.#token(pattern || 'model')
|
|
205
|
+
let candidate = base
|
|
206
|
+
let suffix = 2
|
|
207
|
+
while (used.has(candidate)) {
|
|
208
|
+
candidate = base + '--' + suffix
|
|
209
|
+
suffix += 1
|
|
210
|
+
}
|
|
211
|
+
return candidate
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Resolves the original source basename with a format fallback.
|
|
216
|
+
* @param {object} model Main model metadata.
|
|
217
|
+
* @returns {string}
|
|
218
|
+
*/
|
|
219
|
+
static #sourceName(model) {
|
|
220
|
+
const sourcePath = PcbScene3dModelIdentity.projectPath(model)
|
|
221
|
+
let pathname = sourcePath
|
|
222
|
+
try {
|
|
223
|
+
pathname = new URL(sourcePath).pathname
|
|
224
|
+
} catch {
|
|
225
|
+
pathname = sourcePath
|
|
226
|
+
}
|
|
227
|
+
const candidate =
|
|
228
|
+
String(pathname || '')
|
|
229
|
+
.split(/[?#]/u)[0]
|
|
230
|
+
.replaceAll('\\', '/')
|
|
231
|
+
.split('/')
|
|
232
|
+
.filter(Boolean)
|
|
233
|
+
.pop() ||
|
|
234
|
+
String(PcbModelArchiveSourceBundle.#ownData(model, 'name') || '')
|
|
235
|
+
.replaceAll('\\', '/')
|
|
236
|
+
.split('/')
|
|
237
|
+
.pop()
|
|
238
|
+
const extension = PcbModelArchiveSourceBundle.#extension(
|
|
239
|
+
PcbModelArchiveSourceBundle.#ownData(model, 'format')
|
|
240
|
+
)
|
|
241
|
+
const safe = PcbModelArchiveSourceBundle.#token(candidate || 'model')
|
|
242
|
+
return safe.includes('.') ? safe : safe + '.' + extension
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Resolves the main URL used for companion origin policy.
|
|
247
|
+
* @param {object} model Main model metadata.
|
|
248
|
+
* @returns {string}
|
|
249
|
+
*/
|
|
250
|
+
static #mainUrl(model) {
|
|
251
|
+
return String(
|
|
252
|
+
PcbModelArchiveSourceBundle.#ownData(model, 'resolvedUrl') ||
|
|
253
|
+
PcbModelArchiveSourceBundle.#ownData(model, 'sourceUrl') ||
|
|
254
|
+
PcbScene3dModelIdentity.projectPath(model) ||
|
|
255
|
+
''
|
|
256
|
+
).trim()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Resolves a normalized fallback extension.
|
|
261
|
+
* @param {unknown} format Format candidate.
|
|
262
|
+
* @returns {string}
|
|
263
|
+
*/
|
|
264
|
+
static #extension(format) {
|
|
265
|
+
const value = String(format || '').toLowerCase()
|
|
266
|
+
if (value === 'stp') return 'step'
|
|
267
|
+
if (value === 'vrml') return 'wrl'
|
|
268
|
+
return ['step', 'wrl', 'glb', 'gltf', 'stl', 'obj', '3mf'].includes(
|
|
269
|
+
value
|
|
270
|
+
)
|
|
271
|
+
? value
|
|
272
|
+
: 'step'
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Sanitizes one archive path segment.
|
|
277
|
+
* @param {unknown} value Segment candidate.
|
|
278
|
+
* @returns {string}
|
|
279
|
+
*/
|
|
280
|
+
static #token(value) {
|
|
281
|
+
return (
|
|
282
|
+
String(value || '')
|
|
283
|
+
.replace(/[<>:"/\\|?*\u0000-\u001f]+/gu, '-')
|
|
284
|
+
.replace(/\s+/gu, ' ')
|
|
285
|
+
.trim() || 'model'
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Converts descriptor-safe array or object resources to rows.
|
|
291
|
+
* @param {unknown} value Resource collection.
|
|
292
|
+
* @returns {object[]}
|
|
293
|
+
*/
|
|
294
|
+
static #array(value) {
|
|
295
|
+
if (Array.isArray(value)) return value
|
|
296
|
+
return value && typeof value === 'object'
|
|
297
|
+
? Object.entries(PcbScene3dDescriptorSafeRecord.copy(value)).map(
|
|
298
|
+
([name, row]) => ({
|
|
299
|
+
name,
|
|
300
|
+
...PcbScene3dDescriptorSafeRecord.copy(row)
|
|
301
|
+
})
|
|
302
|
+
)
|
|
303
|
+
: []
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Reads one own data property without invoking accessors.
|
|
308
|
+
* @param {unknown} value Record candidate.
|
|
309
|
+
* @param {PropertyKey} key Property name.
|
|
310
|
+
* @returns {unknown}
|
|
311
|
+
*/
|
|
312
|
+
static #ownData(value, key) {
|
|
313
|
+
if (!value || typeof value !== 'object') return undefined
|
|
314
|
+
try {
|
|
315
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key)
|
|
316
|
+
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
317
|
+
? descriptor.value
|
|
318
|
+
: undefined
|
|
319
|
+
} catch {
|
|
320
|
+
return undefined
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
Object.freeze(PcbModelArchiveSourceBundle.prototype)
|
|
326
|
+
Object.freeze(PcbModelArchiveSourceBundle)
|