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
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { PcbAssemblyModelMeshLoader } from './PcbAssemblyModelMeshLoader.mjs'
|
|
1
2
|
import { PcbScene3dBufferAttributeFactory } from './PcbScene3dBufferAttributeFactory.mjs'
|
|
3
|
+
import { PcbScene3dDescriptorSafeRecord } from './PcbScene3dDescriptorSafeRecord.mjs'
|
|
4
|
+
import { PcbScene3dFacetedModelGroupBuilder } from './PcbScene3dFacetedModelGroupBuilder.mjs'
|
|
2
5
|
import { PcbScene3dModelBounds } from './PcbScene3dModelBounds.mjs'
|
|
6
|
+
import { PcbScene3dModelContent } from './PcbScene3dModelContent.mjs'
|
|
7
|
+
import { PcbScene3dModelIdentity } from './PcbScene3dModelIdentity.mjs'
|
|
8
|
+
|
|
9
|
+
const FACETED_FORMATS = new Set(['glb', 'gltf', 'obj', 'stl'])
|
|
10
|
+
const MM_TO_MIL = 1000 / 25.4
|
|
3
11
|
|
|
4
12
|
/**
|
|
5
13
|
* Loads raw external model geometry before placement-specific wrapping.
|
|
@@ -11,25 +19,43 @@ export class PcbScene3dExternalModelGroupLoader {
|
|
|
11
19
|
* @param {any} model Resolved external model metadata.
|
|
12
20
|
* @param {import('./PcbScene3dStepLoader.mjs').PcbScene3dStepLoader} stepLoader STEP model loader.
|
|
13
21
|
* @param {string} versionKey Cache-busting version key for dynamic loader imports.
|
|
22
|
+
* @param {{ createVrmlLoader?: () => any | Promise<any>, createModelLoader?: (format: string) => any | Promise<any>, fetch?: (url: string, options: object) => Promise<any>, allowNetworkModelFetch?: boolean, authHeaders?: Record<string, string>, fetchTimeoutMs?: number, modelCache?: Map<string, Promise<Uint8Array>> }} [runtime] Optional loader runtime.
|
|
14
23
|
* @returns {Promise<any>}
|
|
15
24
|
*/
|
|
16
|
-
static async load(THREE, model, stepLoader, versionKey = '') {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
25
|
+
static async load(THREE, model, stepLoader, versionKey = '', runtime = {}) {
|
|
26
|
+
const scopedRuntime = PcbScene3dModelContent.createFetchScope(runtime)
|
|
27
|
+
const format = String(model?.format || '').toLowerCase()
|
|
28
|
+
if (format === 'wrl' || format === 'vrml') {
|
|
22
29
|
return PcbScene3dExternalModelGroupLoader.#loadVrmlModel(
|
|
23
|
-
model
|
|
24
|
-
versionKey
|
|
30
|
+
model,
|
|
31
|
+
versionKey,
|
|
32
|
+
scopedRuntime
|
|
25
33
|
)
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
if (
|
|
36
|
+
if (format === 'step' || format === 'stp') {
|
|
29
37
|
return PcbScene3dExternalModelGroupLoader.#loadStepModel(
|
|
30
38
|
THREE,
|
|
31
39
|
model,
|
|
32
|
-
stepLoader
|
|
40
|
+
stepLoader,
|
|
41
|
+
scopedRuntime
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (FACETED_FORMATS.has(format)) {
|
|
46
|
+
return PcbScene3dExternalModelGroupLoader.#loadFacetedModel(
|
|
47
|
+
THREE,
|
|
48
|
+
model,
|
|
49
|
+
stepLoader,
|
|
50
|
+
scopedRuntime
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (format === '3mf') {
|
|
55
|
+
return PcbScene3dExternalModelGroupLoader.#loadThreeMfModel(
|
|
56
|
+
model,
|
|
57
|
+
versionKey,
|
|
58
|
+
scopedRuntime
|
|
33
59
|
)
|
|
34
60
|
}
|
|
35
61
|
|
|
@@ -37,33 +63,411 @@ export class PcbScene3dExternalModelGroupLoader {
|
|
|
37
63
|
}
|
|
38
64
|
|
|
39
65
|
/**
|
|
40
|
-
* Loads one VRML model from a browser file.
|
|
41
|
-
* @param {
|
|
66
|
+
* Loads one VRML model from canonical bytes, text, or a browser file.
|
|
67
|
+
* @param {any} model Resolved WRL model metadata.
|
|
42
68
|
* @param {string} versionKey Cache-busting version key for the loader import.
|
|
69
|
+
* @param {{ createVrmlLoader?: () => any | Promise<any>, createModelLoader?: (format: string) => any | Promise<any>, fetch?: (url: string, options: object) => Promise<any>, allowNetworkModelFetch?: boolean }} runtime Loader runtime.
|
|
43
70
|
* @returns {Promise<any>}
|
|
44
71
|
*/
|
|
45
|
-
static async #loadVrmlModel(
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
static async #loadVrmlModel(model, versionKey, runtime) {
|
|
73
|
+
const sourceText = await PcbScene3dExternalModelGroupLoader.#vrmlText(
|
|
74
|
+
model,
|
|
75
|
+
runtime
|
|
76
|
+
)
|
|
77
|
+
if (!sourceText) {
|
|
78
|
+
throw new Error('Resolved WRL model content is unavailable.')
|
|
79
|
+
}
|
|
80
|
+
const loader = await PcbScene3dExternalModelGroupLoader.#vrmlLoader(
|
|
81
|
+
versionKey,
|
|
82
|
+
runtime
|
|
83
|
+
)
|
|
84
|
+
const safeText =
|
|
85
|
+
await PcbScene3dExternalModelGroupLoader.#vrmlWithSafeTextures(
|
|
86
|
+
sourceText,
|
|
87
|
+
model,
|
|
88
|
+
runtime
|
|
50
89
|
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const objectUrl = URL.createObjectURL(file)
|
|
90
|
+
return loader.parse(safeText, '')
|
|
91
|
+
}
|
|
54
92
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Replaces VRML texture references with explicitly loaded data URIs.
|
|
95
|
+
* @param {string} text VRML source text.
|
|
96
|
+
* @param {object} model Resolved WRL model metadata.
|
|
97
|
+
* @param {object} runtime Loader runtime.
|
|
98
|
+
* @returns {Promise<string>}
|
|
99
|
+
*/
|
|
100
|
+
static async #vrmlWithSafeTextures(text, model, runtime) {
|
|
101
|
+
const references =
|
|
102
|
+
PcbScene3dExternalModelGroupLoader.#vrmlTextureReferences(text)
|
|
103
|
+
if (!references.length) return text
|
|
104
|
+
|
|
105
|
+
const resources =
|
|
106
|
+
PcbScene3dExternalModelGroupLoader.#vrmlResourceIndex(model)
|
|
107
|
+
const resolved = new Map()
|
|
108
|
+
for (const reference of references) {
|
|
109
|
+
resolved.set(
|
|
110
|
+
reference,
|
|
111
|
+
await PcbScene3dExternalModelGroupLoader.#vrmlTextureDataUri(
|
|
112
|
+
reference,
|
|
113
|
+
model,
|
|
114
|
+
resources,
|
|
115
|
+
runtime
|
|
62
116
|
)
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return String(text).replace(
|
|
121
|
+
/(['"])([^'"]+)\1/gu,
|
|
122
|
+
(match, quote, reference) =>
|
|
123
|
+
resolved.has(reference)
|
|
124
|
+
? quote + resolved.get(reference) + quote
|
|
125
|
+
: match
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Lists unique URLs declared by VRML ImageTexture nodes.
|
|
131
|
+
* @param {string} text VRML source text.
|
|
132
|
+
* @returns {string[]}
|
|
133
|
+
*/
|
|
134
|
+
static #vrmlTextureReferences(text) {
|
|
135
|
+
const references = new Set()
|
|
136
|
+
for (const block of String(text || '').matchAll(
|
|
137
|
+
/ImageTexture\s*\{[\s\S]*?\}/giu
|
|
138
|
+
)) {
|
|
139
|
+
for (const quoted of String(block[0] || '').matchAll(
|
|
140
|
+
/(['"])([^'"]+)\1/gu
|
|
141
|
+
)) {
|
|
142
|
+
const reference = String(quoted[2] || '').trim()
|
|
143
|
+
if (reference) references.add(reference)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return [...references]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolves one VRML texture through local resources or explicit fetch policy.
|
|
151
|
+
* @param {string} reference Texture URL from the VRML source.
|
|
152
|
+
* @param {object} model Resolved WRL model metadata.
|
|
153
|
+
* @param {Map<string, object>} resources Local resource index.
|
|
154
|
+
* @param {object} runtime Loader runtime.
|
|
155
|
+
* @returns {Promise<string>}
|
|
156
|
+
*/
|
|
157
|
+
static async #vrmlTextureDataUri(reference, model, resources, runtime) {
|
|
158
|
+
if (reference.startsWith('data:')) return reference
|
|
159
|
+
const modelPath = PcbScene3dModelIdentity.projectPath(model)
|
|
160
|
+
const baseDirectory = modelPath.includes('/')
|
|
161
|
+
? modelPath.slice(0, modelPath.lastIndexOf('/') + 1)
|
|
162
|
+
: ''
|
|
163
|
+
const projectPath = PcbScene3dModelContent.safeProjectPath(
|
|
164
|
+
reference,
|
|
165
|
+
baseDirectory
|
|
166
|
+
)
|
|
167
|
+
const resource =
|
|
168
|
+
resources.get(
|
|
169
|
+
PcbScene3dExternalModelGroupLoader.#vrmlResourceKey(projectPath)
|
|
170
|
+
) ||
|
|
171
|
+
resources.get(
|
|
172
|
+
PcbScene3dExternalModelGroupLoader.#vrmlResourceKey(reference)
|
|
173
|
+
)
|
|
174
|
+
if (resource) {
|
|
175
|
+
const bytes = await PcbScene3dModelContent.bytes(
|
|
176
|
+
resource,
|
|
177
|
+
runtime,
|
|
178
|
+
'VRML texture'
|
|
179
|
+
)
|
|
180
|
+
return PcbScene3dExternalModelGroupLoader.#dataUri(bytes, reference)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (
|
|
184
|
+
typeof runtime?.fetch !== 'function' &&
|
|
185
|
+
runtime?.allowNetworkModelFetch !== true
|
|
186
|
+
) {
|
|
187
|
+
return 'data:,'
|
|
188
|
+
}
|
|
189
|
+
const base = String(
|
|
190
|
+
model?.resolvedUrl || model?.sourceUrl || modelPath || ''
|
|
191
|
+
).trim()
|
|
192
|
+
const resolvedUrl = PcbScene3dModelContent.resolveRelativeUrl(
|
|
193
|
+
reference,
|
|
194
|
+
base
|
|
195
|
+
)
|
|
196
|
+
if (!resolvedUrl) return 'data:,'
|
|
197
|
+
const bytes = await PcbScene3dModelContent.bytes(
|
|
198
|
+
{
|
|
199
|
+
format: 'texture',
|
|
200
|
+
name: reference,
|
|
201
|
+
resolvedUrl,
|
|
202
|
+
mainModelUrl: base,
|
|
203
|
+
...(projectPath
|
|
204
|
+
? { source: { projectRelativePath: projectPath } }
|
|
205
|
+
: {})
|
|
206
|
+
},
|
|
207
|
+
runtime,
|
|
208
|
+
'VRML texture'
|
|
209
|
+
)
|
|
210
|
+
return PcbScene3dExternalModelGroupLoader.#dataUri(bytes, reference)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Builds an exact alias index for local WRL companion resources.
|
|
215
|
+
* @param {object} model Resolved WRL model metadata.
|
|
216
|
+
* @returns {Map<string, object>}
|
|
217
|
+
*/
|
|
218
|
+
static #vrmlResourceIndex(model) {
|
|
219
|
+
const index = new Map()
|
|
220
|
+
const resources = PcbScene3dExternalModelGroupLoader.#resourceArray(
|
|
221
|
+
PcbScene3dExternalModelGroupLoader.#ownData(model, 'resources')
|
|
222
|
+
)
|
|
223
|
+
resources.forEach((resource) => {
|
|
224
|
+
const source = PcbScene3dExternalModelGroupLoader.#ownData(
|
|
225
|
+
resource,
|
|
226
|
+
'source'
|
|
227
|
+
)
|
|
228
|
+
const aliases = [
|
|
229
|
+
PcbScene3dExternalModelGroupLoader.#ownData(resource, 'uri'),
|
|
230
|
+
PcbScene3dExternalModelGroupLoader.#ownData(resource, 'name'),
|
|
231
|
+
PcbScene3dExternalModelGroupLoader.#ownData(
|
|
232
|
+
resource,
|
|
233
|
+
'relativePath'
|
|
234
|
+
),
|
|
235
|
+
PcbScene3dExternalModelGroupLoader.#ownData(
|
|
236
|
+
source,
|
|
237
|
+
'projectRelativePath'
|
|
238
|
+
),
|
|
239
|
+
PcbScene3dExternalModelGroupLoader.#ownData(
|
|
240
|
+
source,
|
|
241
|
+
'project_relative_path'
|
|
242
|
+
)
|
|
243
|
+
]
|
|
244
|
+
aliases.forEach((alias) => {
|
|
245
|
+
const key =
|
|
246
|
+
PcbScene3dExternalModelGroupLoader.#vrmlResourceKey(alias)
|
|
247
|
+
if (key && !index.has(key)) index.set(key, resource)
|
|
63
248
|
})
|
|
64
|
-
}
|
|
65
|
-
|
|
249
|
+
})
|
|
250
|
+
return index
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Converts local resources to a descriptor-safe array.
|
|
255
|
+
* @param {unknown} value Resource collection.
|
|
256
|
+
* @returns {object[]}
|
|
257
|
+
*/
|
|
258
|
+
static #resourceArray(value) {
|
|
259
|
+
try {
|
|
260
|
+
if (Array.isArray(value)) {
|
|
261
|
+
if (Object.getPrototypeOf(value) !== Array.prototype) return []
|
|
262
|
+
const descriptors = Object.getOwnPropertyDescriptors(value)
|
|
263
|
+
const length = descriptors.length?.value
|
|
264
|
+
if (!Number.isSafeInteger(length) || length < 0) return []
|
|
265
|
+
const resources = []
|
|
266
|
+
for (let index = 0; index < length; index += 1) {
|
|
267
|
+
const descriptor = descriptors[String(index)]
|
|
268
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
269
|
+
return []
|
|
270
|
+
}
|
|
271
|
+
resources.push(descriptor.value)
|
|
272
|
+
}
|
|
273
|
+
return resources
|
|
274
|
+
}
|
|
275
|
+
if (!value || typeof value !== 'object') return []
|
|
276
|
+
return Object.entries(Object.getOwnPropertyDescriptors(value))
|
|
277
|
+
.filter(([, descriptor]) => Object.hasOwn(descriptor, 'value'))
|
|
278
|
+
.map(([name, descriptor]) => ({
|
|
279
|
+
name,
|
|
280
|
+
...PcbScene3dDescriptorSafeRecord.copy(descriptor.value)
|
|
281
|
+
}))
|
|
282
|
+
} catch {
|
|
283
|
+
return []
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Encodes texture bytes as a browser-safe data URI.
|
|
289
|
+
* @param {Uint8Array} bytes Texture bytes.
|
|
290
|
+
* @param {string} reference Texture file reference.
|
|
291
|
+
* @returns {string}
|
|
292
|
+
*/
|
|
293
|
+
static #dataUri(bytes, reference) {
|
|
294
|
+
const alphabet =
|
|
295
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
296
|
+
let base64 = ''
|
|
297
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
298
|
+
const first = bytes[index]
|
|
299
|
+
const second = bytes[index + 1]
|
|
300
|
+
const third = bytes[index + 2]
|
|
301
|
+
const packed =
|
|
302
|
+
(first << 16) |
|
|
303
|
+
((second === undefined ? 0 : second) << 8) |
|
|
304
|
+
(third === undefined ? 0 : third)
|
|
305
|
+
base64 += alphabet[(packed >>> 18) & 63]
|
|
306
|
+
base64 += alphabet[(packed >>> 12) & 63]
|
|
307
|
+
base64 += second === undefined ? '=' : alphabet[(packed >>> 6) & 63]
|
|
308
|
+
base64 += third === undefined ? '=' : alphabet[packed & 63]
|
|
309
|
+
}
|
|
310
|
+
return (
|
|
311
|
+
'data:' +
|
|
312
|
+
PcbScene3dExternalModelGroupLoader.#textureMediaType(reference) +
|
|
313
|
+
';base64,' +
|
|
314
|
+
base64
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Resolves a common texture media type from its filename.
|
|
320
|
+
* @param {string} reference Texture file reference.
|
|
321
|
+
* @returns {string}
|
|
322
|
+
*/
|
|
323
|
+
static #textureMediaType(reference) {
|
|
324
|
+
const extension = String(reference || '')
|
|
325
|
+
.split(/[?#]/u)[0]
|
|
326
|
+
.split('.')
|
|
327
|
+
.pop()
|
|
328
|
+
.toLowerCase()
|
|
329
|
+
return (
|
|
330
|
+
{
|
|
331
|
+
bmp: 'image/bmp',
|
|
332
|
+
gif: 'image/gif',
|
|
333
|
+
jpeg: 'image/jpeg',
|
|
334
|
+
jpg: 'image/jpeg',
|
|
335
|
+
png: 'image/png',
|
|
336
|
+
webp: 'image/webp'
|
|
337
|
+
}[extension] || 'application/octet-stream'
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Normalizes a local resource alias.
|
|
343
|
+
* @param {unknown} value Resource path candidate.
|
|
344
|
+
* @returns {string}
|
|
345
|
+
*/
|
|
346
|
+
static #vrmlResourceKey(value) {
|
|
347
|
+
return String(value || '')
|
|
348
|
+
.trim()
|
|
349
|
+
.split(/[?#]/u)[0]
|
|
350
|
+
.replaceAll('\\', '/')
|
|
351
|
+
.replace(/^\.\//u, '')
|
|
352
|
+
.toLowerCase()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Reads one own data property without invoking accessors.
|
|
357
|
+
* @param {unknown} value Record candidate.
|
|
358
|
+
* @param {PropertyKey} key Property key.
|
|
359
|
+
* @returns {unknown}
|
|
360
|
+
*/
|
|
361
|
+
static #ownData(value, key) {
|
|
362
|
+
if (!value || typeof value !== 'object') return undefined
|
|
363
|
+
try {
|
|
364
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key)
|
|
365
|
+
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
366
|
+
? descriptor.value
|
|
367
|
+
: undefined
|
|
368
|
+
} catch {
|
|
369
|
+
return undefined
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Creates the production or injected Three.js VRML loader.
|
|
375
|
+
* @param {string} versionKey Cache-busting version key.
|
|
376
|
+
* @param {{ createVrmlLoader?: () => any | Promise<any>, createModelLoader?: (format: string) => any | Promise<any> }} runtime Loader runtime.
|
|
377
|
+
* @returns {Promise<any>}
|
|
378
|
+
*/
|
|
379
|
+
static async #vrmlLoader(versionKey, runtime) {
|
|
380
|
+
if (typeof runtime?.createModelLoader === 'function') {
|
|
381
|
+
return runtime.createModelLoader('wrl')
|
|
382
|
+
}
|
|
383
|
+
if (typeof runtime?.createVrmlLoader === 'function') {
|
|
384
|
+
return runtime.createVrmlLoader()
|
|
66
385
|
}
|
|
386
|
+
const { VRMLLoader } = await import(
|
|
387
|
+
'/node_modules/three/examples/jsm/loaders/VRMLLoader.js' +
|
|
388
|
+
(versionKey ? '?v=' + encodeURIComponent(versionKey) : '')
|
|
389
|
+
)
|
|
390
|
+
return new VRMLLoader()
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Reads UTF-8 VRML text from every supported external-model source shape.
|
|
395
|
+
* @param {any} model Resolved WRL model metadata.
|
|
396
|
+
* @param {object} runtime Loader runtime.
|
|
397
|
+
* @returns {Promise<string>}
|
|
398
|
+
*/
|
|
399
|
+
static async #vrmlText(model, runtime) {
|
|
400
|
+
return PcbScene3dModelContent.text(model, runtime, 'WRL')
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Loads STL, OBJ, GLTF, or GLB through the shared faceted model pipeline.
|
|
405
|
+
* @param {any} THREE Three.js namespace.
|
|
406
|
+
* @param {any} model Resolved external model metadata.
|
|
407
|
+
* @param {import('./PcbScene3dStepLoader.mjs').PcbScene3dStepLoader} stepLoader STEP model loader.
|
|
408
|
+
* @param {object} runtime Loader runtime.
|
|
409
|
+
* @returns {Promise<any>}
|
|
410
|
+
*/
|
|
411
|
+
static async #loadFacetedModel(THREE, model, stepLoader, runtime) {
|
|
412
|
+
const loader = new PcbAssemblyModelMeshLoader({
|
|
413
|
+
stepLoader,
|
|
414
|
+
fetch: runtime?.fetch,
|
|
415
|
+
allowNetworkModelFetch: runtime?.allowNetworkModelFetch,
|
|
416
|
+
authHeaders: runtime?.authHeaders,
|
|
417
|
+
fetchTimeoutMs: runtime?.fetchTimeoutMs,
|
|
418
|
+
modelCache: runtime?.modelCache
|
|
419
|
+
})
|
|
420
|
+
const meshes = await loader.loadPlacement({ externalModel: model })
|
|
421
|
+
return PcbScene3dFacetedModelGroupBuilder.build(THREE, meshes)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Loads a 3MF model with the Three.js loader and converts millimeters to mils.
|
|
426
|
+
* @param {any} model Resolved 3MF model metadata.
|
|
427
|
+
* @param {string} versionKey Cache-busting loader version.
|
|
428
|
+
* @param {object} runtime Loader runtime.
|
|
429
|
+
* @returns {Promise<any>}
|
|
430
|
+
*/
|
|
431
|
+
static async #loadThreeMfModel(model, versionKey, runtime) {
|
|
432
|
+
const bytes = await PcbScene3dModelContent.bytes(model, runtime, '3MF')
|
|
433
|
+
const loader = await PcbScene3dExternalModelGroupLoader.#threeMfLoader(
|
|
434
|
+
versionKey,
|
|
435
|
+
runtime
|
|
436
|
+
)
|
|
437
|
+
const group = loader.parse(
|
|
438
|
+
PcbScene3dExternalModelGroupLoader.#arrayBuffer(bytes)
|
|
439
|
+
)
|
|
440
|
+
group?.scale?.setScalar?.(MM_TO_MIL)
|
|
441
|
+
return group
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Creates the production or injected Three.js 3MF loader.
|
|
446
|
+
* @param {string} versionKey Cache-busting version key.
|
|
447
|
+
* @param {object} runtime Loader runtime.
|
|
448
|
+
* @returns {Promise<any>}
|
|
449
|
+
*/
|
|
450
|
+
static async #threeMfLoader(versionKey, runtime) {
|
|
451
|
+
if (typeof runtime?.createModelLoader === 'function') {
|
|
452
|
+
return runtime.createModelLoader('3mf')
|
|
453
|
+
}
|
|
454
|
+
const { ThreeMFLoader } = await import(
|
|
455
|
+
'/node_modules/three/examples/jsm/loaders/3MFLoader.js' +
|
|
456
|
+
(versionKey ? '?v=' + encodeURIComponent(versionKey) : '')
|
|
457
|
+
)
|
|
458
|
+
return new ThreeMFLoader()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Returns an exact ArrayBuffer view for a byte slice.
|
|
463
|
+
* @param {Uint8Array} bytes Source bytes.
|
|
464
|
+
* @returns {ArrayBuffer}
|
|
465
|
+
*/
|
|
466
|
+
static #arrayBuffer(bytes) {
|
|
467
|
+
return bytes.buffer.slice(
|
|
468
|
+
bytes.byteOffset,
|
|
469
|
+
bytes.byteOffset + bytes.byteLength
|
|
470
|
+
)
|
|
67
471
|
}
|
|
68
472
|
|
|
69
473
|
/**
|
|
@@ -71,12 +475,19 @@ export class PcbScene3dExternalModelGroupLoader {
|
|
|
71
475
|
* @param {any} THREE Three.js namespace.
|
|
72
476
|
* @param {any} model Resolved STEP model metadata.
|
|
73
477
|
* @param {import('./PcbScene3dStepLoader.mjs').PcbScene3dStepLoader} stepLoader STEP model loader.
|
|
478
|
+
* @param {object} runtime Loader runtime.
|
|
74
479
|
* @returns {Promise<any>}
|
|
75
480
|
*/
|
|
76
|
-
static async #loadStepModel(THREE, model, stepLoader) {
|
|
481
|
+
static async #loadStepModel(THREE, model, stepLoader, runtime) {
|
|
482
|
+
const resolvedModel =
|
|
483
|
+
await PcbScene3dExternalModelGroupLoader.#modelWithUrlBytes(
|
|
484
|
+
model,
|
|
485
|
+
runtime,
|
|
486
|
+
'STEP'
|
|
487
|
+
)
|
|
77
488
|
const loadedModel = Array.isArray(model?.preparedMeshPayloads)
|
|
78
489
|
? { meshPayloads: model.preparedMeshPayloads }
|
|
79
|
-
: await stepLoader.loadModel(
|
|
490
|
+
: await stepLoader.loadModel(resolvedModel)
|
|
80
491
|
const group = new THREE.Group()
|
|
81
492
|
const sourceBounds = PcbScene3dModelBounds.measureSourceBoundsMil(
|
|
82
493
|
loadedModel.meshPayloads
|
|
@@ -134,6 +545,36 @@ export class PcbScene3dExternalModelGroupLoader {
|
|
|
134
545
|
return group
|
|
135
546
|
}
|
|
136
547
|
|
|
548
|
+
/**
|
|
549
|
+
* Attaches fetched URL bytes only when a model has no local content.
|
|
550
|
+
* @param {any} model External model metadata.
|
|
551
|
+
* @param {object} runtime Loader runtime.
|
|
552
|
+
* @param {string} format Human-readable model format.
|
|
553
|
+
* @returns {Promise<any>}
|
|
554
|
+
*/
|
|
555
|
+
static async #modelWithUrlBytes(model, runtime, format) {
|
|
556
|
+
if (
|
|
557
|
+
Array.isArray(model?.preparedMeshPayloads) ||
|
|
558
|
+
PcbScene3dModelContent.hasLocal(model)
|
|
559
|
+
) {
|
|
560
|
+
return model
|
|
561
|
+
}
|
|
562
|
+
const url = String(model?.resolvedUrl || model?.sourceUrl || '').trim()
|
|
563
|
+
const canFetch =
|
|
564
|
+
typeof runtime?.fetch === 'function' ||
|
|
565
|
+
(runtime?.allowNetworkModelFetch === true &&
|
|
566
|
+
typeof globalThis.fetch === 'function')
|
|
567
|
+
if (!url || !canFetch) return model
|
|
568
|
+
return {
|
|
569
|
+
...model,
|
|
570
|
+
payloadBytes: await PcbScene3dModelContent.bytes(
|
|
571
|
+
model,
|
|
572
|
+
runtime,
|
|
573
|
+
format
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
137
578
|
/**
|
|
138
579
|
* Builds the material set for one STEP mesh and assigns face-color groups
|
|
139
580
|
* when the importer exposes them.
|
|
@@ -8,6 +8,8 @@ import { PcbScene3dExternalModelLoadOrder } from './PcbScene3dExternalModelLoadO
|
|
|
8
8
|
import { PcbScene3dExternalModelSourceOriginPolicy } from './PcbScene3dExternalModelSourceOriginPolicy.mjs'
|
|
9
9
|
import { PcbScene3dExternalPlacementDefaults } from './PcbScene3dExternalPlacementDefaults.mjs'
|
|
10
10
|
import { PcbScene3dModelBounds } from './PcbScene3dModelBounds.mjs'
|
|
11
|
+
import { PcbScene3dModelIdentity } from './PcbScene3dModelIdentity.mjs'
|
|
12
|
+
import { PcbScene3dModelContent } from './PcbScene3dModelContent.mjs'
|
|
11
13
|
import { PcbScene3dMountRig } from './PcbScene3dMountRig.mjs'
|
|
12
14
|
import { PcbScene3dStepLoader } from './PcbScene3dStepLoader.mjs'
|
|
13
15
|
import { PcbScene3dViewCompensation } from './PcbScene3dViewCompensation.mjs'
|
|
@@ -18,10 +20,13 @@ import { PcbScene3dViewCompensation } from './PcbScene3dViewCompensation.mjs'
|
|
|
18
20
|
export class PcbScene3dExternalModels {
|
|
19
21
|
/**
|
|
20
22
|
* Loads all available external models into the supplied group.
|
|
21
|
-
* @param {{ three: any, sceneDescription: any, externalModelsGroup: any, modelViewScale?: { x?: number, y?: number, z?: number }, isDisposed?: () => boolean, onPlacementGroup?: (placement: any, placementGroup: any) => void, stepLoader?: PcbScene3dStepLoader }} options
|
|
23
|
+
* @param {{ three: any, sceneDescription: any, externalModelsGroup: any, modelViewScale?: { x?: number, y?: number, z?: number }, isDisposed?: () => boolean, onPlacementGroup?: (placement: any, placementGroup: any) => void, stepLoader?: PcbScene3dStepLoader, modelLoaderOptions?: object }} options
|
|
22
24
|
* @returns {Promise<string[]>}
|
|
23
25
|
*/
|
|
24
26
|
static async loadIntoScene(options) {
|
|
27
|
+
const modelLoaderOptions = PcbScene3dModelContent.createFetchScope(
|
|
28
|
+
options?.modelLoaderOptions
|
|
29
|
+
)
|
|
25
30
|
const placements = PcbScene3dExternalModelLoadOrder.sort(
|
|
26
31
|
PcbScene3dExternalModels.#resolvePlacements(
|
|
27
32
|
options?.sceneDescription
|
|
@@ -52,7 +57,8 @@ export class PcbScene3dExternalModels {
|
|
|
52
57
|
stepLoader,
|
|
53
58
|
cachedModelGroups,
|
|
54
59
|
options?.modelViewScale,
|
|
55
|
-
options?.sceneDescription
|
|
60
|
+
options?.sceneDescription,
|
|
61
|
+
modelLoaderOptions
|
|
56
62
|
)
|
|
57
63
|
if (!loadedGroup || options?.isDisposed?.()) {
|
|
58
64
|
continue
|
|
@@ -183,6 +189,7 @@ export class PcbScene3dExternalModels {
|
|
|
183
189
|
* @param {Map<string, any>} cachedModelGroups
|
|
184
190
|
* @param {{ x?: number, y?: number, z?: number } | null | undefined} modelViewScale Active scene view scale.
|
|
185
191
|
* @param {object | null | undefined} sceneDescription Scene description.
|
|
192
|
+
* @param {object | null | undefined} modelLoaderOptions Model loader options.
|
|
186
193
|
* @returns {Promise<any>}
|
|
187
194
|
*/
|
|
188
195
|
static async #loadPlacementGroup(
|
|
@@ -191,7 +198,8 @@ export class PcbScene3dExternalModels {
|
|
|
191
198
|
stepLoader,
|
|
192
199
|
cachedModelGroups,
|
|
193
200
|
modelViewScale,
|
|
194
|
-
sceneDescription
|
|
201
|
+
sceneDescription,
|
|
202
|
+
modelLoaderOptions
|
|
195
203
|
) {
|
|
196
204
|
const model = placement?.externalModel
|
|
197
205
|
if (!model) {
|
|
@@ -203,7 +211,8 @@ export class PcbScene3dExternalModels {
|
|
|
203
211
|
THREE,
|
|
204
212
|
model,
|
|
205
213
|
stepLoader,
|
|
206
|
-
cachedModelGroups
|
|
214
|
+
cachedModelGroups,
|
|
215
|
+
modelLoaderOptions
|
|
207
216
|
)
|
|
208
217
|
|
|
209
218
|
return PcbScene3dExternalModels.#buildPlacementWrapper(
|
|
@@ -222,13 +231,15 @@ export class PcbScene3dExternalModels {
|
|
|
222
231
|
* @param {any} model
|
|
223
232
|
* @param {PcbScene3dStepLoader} stepLoader
|
|
224
233
|
* @param {Map<string, any>} cachedModelGroups
|
|
234
|
+
* @param {object | null | undefined} modelLoaderOptions Model loader options.
|
|
225
235
|
* @returns {Promise<any>}
|
|
226
236
|
*/
|
|
227
237
|
static async #loadCachedModelGroup(
|
|
228
238
|
THREE,
|
|
229
239
|
model,
|
|
230
240
|
stepLoader,
|
|
231
|
-
cachedModelGroups
|
|
241
|
+
cachedModelGroups,
|
|
242
|
+
modelLoaderOptions
|
|
232
243
|
) {
|
|
233
244
|
const identity = PcbScene3dExternalModels.#resolveModelIdentity(model)
|
|
234
245
|
if (cachedModelGroups.has(identity)) {
|
|
@@ -238,7 +249,8 @@ export class PcbScene3dExternalModels {
|
|
|
238
249
|
const modelGroup = await PcbScene3dExternalModels.#loadModelGroup(
|
|
239
250
|
THREE,
|
|
240
251
|
model,
|
|
241
|
-
stepLoader
|
|
252
|
+
stepLoader,
|
|
253
|
+
modelLoaderOptions
|
|
242
254
|
)
|
|
243
255
|
cachedModelGroups.set(identity, modelGroup)
|
|
244
256
|
|
|
@@ -250,14 +262,16 @@ export class PcbScene3dExternalModels {
|
|
|
250
262
|
* @param {any} THREE
|
|
251
263
|
* @param {any} model
|
|
252
264
|
* @param {PcbScene3dStepLoader} stepLoader
|
|
265
|
+
* @param {object | null | undefined} modelLoaderOptions Model loader options.
|
|
253
266
|
* @returns {Promise<any>}
|
|
254
267
|
*/
|
|
255
|
-
static async #loadModelGroup(THREE, model, stepLoader) {
|
|
268
|
+
static async #loadModelGroup(THREE, model, stepLoader, modelLoaderOptions) {
|
|
256
269
|
return PcbScene3dExternalModelGroupLoader.load(
|
|
257
270
|
THREE,
|
|
258
271
|
model,
|
|
259
272
|
stepLoader,
|
|
260
|
-
new URL(import.meta.url).searchParams.get('v') || ''
|
|
273
|
+
new URL(import.meta.url).searchParams.get('v') || '',
|
|
274
|
+
modelLoaderOptions || {}
|
|
261
275
|
)
|
|
262
276
|
}
|
|
263
277
|
|
|
@@ -719,22 +733,7 @@ export class PcbScene3dExternalModels {
|
|
|
719
733
|
* @returns {string}
|
|
720
734
|
*/
|
|
721
735
|
static #resolveModelIdentity(model) {
|
|
722
|
-
|
|
723
|
-
return [
|
|
724
|
-
'embedded',
|
|
725
|
-
String(model?.sourceStream || ''),
|
|
726
|
-
String(model?.name || ''),
|
|
727
|
-
String(model?.checksum || ''),
|
|
728
|
-
String(model?.format || '')
|
|
729
|
-
].join('::')
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
return [
|
|
733
|
-
'session',
|
|
734
|
-
String(model?.relativePath || ''),
|
|
735
|
-
String(model?.name || ''),
|
|
736
|
-
String(model?.format || '')
|
|
737
|
-
].join('::')
|
|
736
|
+
return PcbScene3dModelIdentity.resolve(model)
|
|
738
737
|
}
|
|
739
738
|
|
|
740
739
|
/**
|