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,3 +1,13 @@
|
|
|
1
|
+
import { CircuitJsonDocumentContext } from 'circuitjson-toolkit'
|
|
2
|
+
import { ToolkitAsset } from 'circuitjson-toolkit/parser'
|
|
3
|
+
import { PcbScene3dCircuitJsonInput } from './PcbScene3dCircuitJsonInput.mjs'
|
|
4
|
+
import { PcbScene3dDescriptorSafeRecord } from './PcbScene3dDescriptorSafeRecord.mjs'
|
|
5
|
+
import { PcbScene3dCircuitJsonModelAsset } from './PcbScene3dCircuitJsonModelAsset.mjs'
|
|
6
|
+
import { PcbScene3dModelContent } from './PcbScene3dModelContent.mjs'
|
|
7
|
+
import { PcbScene3dModelIdentity } from './PcbScene3dModelIdentity.mjs'
|
|
8
|
+
|
|
9
|
+
const AMBIGUOUS_ASSET = Symbol('ambiguous-model-asset')
|
|
10
|
+
|
|
1
11
|
/**
|
|
2
12
|
* Bridges CAD component model asset metadata to scene model resolution.
|
|
3
13
|
*/
|
|
@@ -10,6 +20,9 @@ export class CircuitJsonCadModelAssetResolver {
|
|
|
10
20
|
static withModelAssetUrls(documentModel) {
|
|
11
21
|
const elements =
|
|
12
22
|
CircuitJsonCadModelAssetResolver.#elements(documentModel)
|
|
23
|
+
if (!PcbScene3dCircuitJsonInput.isModel(elements)) {
|
|
24
|
+
return documentModel
|
|
25
|
+
}
|
|
13
26
|
if (
|
|
14
27
|
!elements.some((element) =>
|
|
15
28
|
CircuitJsonCadModelAssetResolver.#modelAssetUrlField(element)
|
|
@@ -24,44 +37,208 @@ export class CircuitJsonCadModelAssetResolver {
|
|
|
24
37
|
|
|
25
38
|
if (Array.isArray(documentModel)) {
|
|
26
39
|
Object.assign(nextElements, {
|
|
27
|
-
sourceFormat:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
sourceFormat: CircuitJsonCadModelAssetResolver.#ownData(
|
|
41
|
+
documentModel,
|
|
42
|
+
'sourceFormat'
|
|
43
|
+
),
|
|
44
|
+
kind: CircuitJsonCadModelAssetResolver.#ownData(
|
|
45
|
+
documentModel,
|
|
46
|
+
'kind'
|
|
47
|
+
),
|
|
48
|
+
fileName: CircuitJsonCadModelAssetResolver.#ownData(
|
|
49
|
+
documentModel,
|
|
50
|
+
'fileName'
|
|
51
|
+
),
|
|
52
|
+
bom: CircuitJsonCadModelAssetResolver.#ownData(
|
|
53
|
+
documentModel,
|
|
54
|
+
'bom'
|
|
55
|
+
)
|
|
31
56
|
})
|
|
32
57
|
return nextElements
|
|
33
58
|
}
|
|
34
59
|
|
|
35
|
-
if (
|
|
36
|
-
|
|
60
|
+
if (
|
|
61
|
+
CircuitJsonCadModelAssetResolver.#isContext(documentModel) &&
|
|
62
|
+
Array.isArray(documentModel.document?.model)
|
|
63
|
+
) {
|
|
64
|
+
const document = documentModel.document
|
|
65
|
+
return CircuitJsonDocumentContext.prepare(
|
|
66
|
+
{
|
|
67
|
+
...PcbScene3dDescriptorSafeRecord.copy(document),
|
|
68
|
+
model: nextElements
|
|
69
|
+
},
|
|
70
|
+
{ indexes: ['elements'] }
|
|
71
|
+
)
|
|
37
72
|
}
|
|
38
|
-
|
|
39
|
-
|
|
73
|
+
const wrappedDocument = CircuitJsonCadModelAssetResolver.#ownData(
|
|
74
|
+
documentModel,
|
|
75
|
+
'document'
|
|
76
|
+
)
|
|
77
|
+
if (
|
|
78
|
+
Array.isArray(
|
|
79
|
+
CircuitJsonCadModelAssetResolver.#ownData(
|
|
80
|
+
wrappedDocument,
|
|
81
|
+
'model'
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
) {
|
|
85
|
+
return {
|
|
86
|
+
...PcbScene3dDescriptorSafeRecord.copy(wrappedDocument),
|
|
87
|
+
model: nextElements
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const model = CircuitJsonCadModelAssetResolver.#ownData(
|
|
91
|
+
documentModel,
|
|
92
|
+
'model'
|
|
93
|
+
)
|
|
94
|
+
if (Array.isArray(model)) {
|
|
95
|
+
return {
|
|
96
|
+
...PcbScene3dDescriptorSafeRecord.copy(documentModel),
|
|
97
|
+
model: nextElements
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const wrappedElements = CircuitJsonCadModelAssetResolver.#ownData(
|
|
101
|
+
documentModel,
|
|
102
|
+
'elements'
|
|
103
|
+
)
|
|
104
|
+
if (Array.isArray(wrappedElements)) {
|
|
105
|
+
return {
|
|
106
|
+
...PcbScene3dDescriptorSafeRecord.copy(documentModel),
|
|
107
|
+
elements: nextElements
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const circuitJson = CircuitJsonCadModelAssetResolver.#ownData(
|
|
111
|
+
documentModel,
|
|
112
|
+
'circuitJson'
|
|
113
|
+
)
|
|
114
|
+
if (Array.isArray(circuitJson)) {
|
|
115
|
+
return {
|
|
116
|
+
...PcbScene3dDescriptorSafeRecord.copy(documentModel),
|
|
117
|
+
circuitJson: nextElements
|
|
118
|
+
}
|
|
40
119
|
}
|
|
41
120
|
return documentModel
|
|
42
121
|
}
|
|
43
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Returns whether an input is a prepared CircuitJSON context.
|
|
125
|
+
* @param {unknown} value Candidate input.
|
|
126
|
+
* @returns {boolean}
|
|
127
|
+
*/
|
|
128
|
+
static #isContext(value) {
|
|
129
|
+
try {
|
|
130
|
+
return value instanceof CircuitJsonDocumentContext
|
|
131
|
+
} catch {
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
44
136
|
/**
|
|
45
137
|
* Adds a session-asset-aware model URL resolver.
|
|
46
138
|
* @param {object} options Scene preparation options.
|
|
139
|
+
* @param {object[]} [documentAssets] Canonical document assets.
|
|
47
140
|
* @returns {object}
|
|
48
141
|
*/
|
|
49
|
-
static withSessionAssetResolver(options = {}) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
142
|
+
static withSessionAssetResolver(options = {}, documentAssets = []) {
|
|
143
|
+
const safeOptions = PcbScene3dDescriptorSafeRecord.copy(options)
|
|
144
|
+
const sessionAssets = CircuitJsonCadModelAssetResolver.#denseArray(
|
|
145
|
+
CircuitJsonCadModelAssetResolver.#ownData(
|
|
146
|
+
safeOptions,
|
|
147
|
+
'sessionAssets'
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
const canonicalAssets =
|
|
151
|
+
CircuitJsonCadModelAssetResolver.#denseArray(documentAssets)
|
|
152
|
+
const sessionAssetIndex =
|
|
153
|
+
CircuitJsonCadModelAssetResolver.#assetIndex(sessionAssets)
|
|
154
|
+
const documentAssetIndex = CircuitJsonCadModelAssetResolver.#assetIndex(
|
|
155
|
+
canonicalAssets,
|
|
156
|
+
true
|
|
157
|
+
)
|
|
158
|
+
return CircuitJsonCadModelAssetResolver.#withAssetIndexes(
|
|
159
|
+
safeOptions,
|
|
160
|
+
sessionAssets,
|
|
161
|
+
sessionAssetIndex,
|
|
162
|
+
documentAssetIndex
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Adds a model URL resolver backed by a context-cached document asset index.
|
|
168
|
+
* @param {object} options Scene preparation options.
|
|
169
|
+
* @param {CircuitJsonDocumentContext} context Prepared document context.
|
|
170
|
+
* @returns {object}
|
|
171
|
+
*/
|
|
172
|
+
static withContextAssetResolver(options = {}, context) {
|
|
173
|
+
if (!CircuitJsonCadModelAssetResolver.#isContext(context)) {
|
|
174
|
+
return CircuitJsonCadModelAssetResolver.withSessionAssetResolver(
|
|
175
|
+
options
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const safeOptions = PcbScene3dDescriptorSafeRecord.copy(options)
|
|
180
|
+
const sessionAssets = CircuitJsonCadModelAssetResolver.#denseArray(
|
|
181
|
+
CircuitJsonCadModelAssetResolver.#ownData(
|
|
182
|
+
safeOptions,
|
|
183
|
+
'sessionAssets'
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
const canonicalAssets = CircuitJsonCadModelAssetResolver.#denseArray(
|
|
187
|
+
context.assets
|
|
188
|
+
)
|
|
189
|
+
const sessionAssetIndex =
|
|
190
|
+
CircuitJsonCadModelAssetResolver.#assetIndex(sessionAssets)
|
|
191
|
+
const documentAssetIndex = canonicalAssets.length
|
|
192
|
+
? context.getOrCreateDerived(
|
|
193
|
+
'pcb-scene3d-viewer',
|
|
194
|
+
'model-assets-v1',
|
|
195
|
+
() =>
|
|
196
|
+
CircuitJsonCadModelAssetResolver.#assetIndex(
|
|
197
|
+
canonicalAssets,
|
|
198
|
+
true
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
: CircuitJsonCadModelAssetResolver.#assetIndex([])
|
|
202
|
+
|
|
203
|
+
return CircuitJsonCadModelAssetResolver.#withAssetIndexes(
|
|
204
|
+
safeOptions,
|
|
205
|
+
sessionAssets,
|
|
206
|
+
sessionAssetIndex,
|
|
207
|
+
documentAssetIndex
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Creates safe resolver options from prepared asset indexes.
|
|
213
|
+
* @param {Record<string, unknown>} safeOptions Descriptor-safe options.
|
|
214
|
+
* @param {unknown[]} sessionAssets Captured session asset rows.
|
|
215
|
+
* @param {Map<string, object>} sessionAssetIndex Session asset index.
|
|
216
|
+
* @param {Map<string, object>} documentAssetIndex Document asset index.
|
|
217
|
+
* @returns {object}
|
|
218
|
+
*/
|
|
219
|
+
static #withAssetIndexes(
|
|
220
|
+
safeOptions,
|
|
221
|
+
sessionAssets,
|
|
222
|
+
sessionAssetIndex,
|
|
223
|
+
documentAssetIndex
|
|
224
|
+
) {
|
|
225
|
+
const resolver = CircuitJsonCadModelAssetResolver.#ownData(
|
|
226
|
+
safeOptions,
|
|
227
|
+
'modelUrlResolver'
|
|
228
|
+
)
|
|
229
|
+
const callerResolver = typeof resolver === 'function' ? resolver : null
|
|
57
230
|
|
|
58
231
|
return {
|
|
59
|
-
...
|
|
232
|
+
...safeOptions,
|
|
233
|
+
...(Object.hasOwn(safeOptions, 'sessionAssets')
|
|
234
|
+
? { sessionAssets }
|
|
235
|
+
: {}),
|
|
60
236
|
modelUrlResolver: (url, context) =>
|
|
61
237
|
CircuitJsonCadModelAssetResolver.#resolveModelUrl(
|
|
62
238
|
url,
|
|
63
239
|
context,
|
|
64
|
-
|
|
240
|
+
sessionAssetIndex,
|
|
241
|
+
documentAssetIndex,
|
|
65
242
|
callerResolver
|
|
66
243
|
)
|
|
67
244
|
}
|
|
@@ -76,12 +253,22 @@ export class CircuitJsonCadModelAssetResolver {
|
|
|
76
253
|
const field =
|
|
77
254
|
CircuitJsonCadModelAssetResolver.#modelAssetUrlField(element)
|
|
78
255
|
if (!field) return element
|
|
79
|
-
const asset =
|
|
256
|
+
const asset =
|
|
257
|
+
CircuitJsonCadModelAssetResolver.#ownData(element, 'model_asset') ||
|
|
258
|
+
{}
|
|
80
259
|
const sourceUrl = String(
|
|
81
|
-
|
|
260
|
+
CircuitJsonCadModelAssetResolver.#ownData(
|
|
261
|
+
asset,
|
|
262
|
+
'project_relative_path'
|
|
263
|
+
) ||
|
|
264
|
+
CircuitJsonCadModelAssetResolver.#ownData(asset, 'url') ||
|
|
265
|
+
''
|
|
82
266
|
).trim()
|
|
83
267
|
if (!sourceUrl) return element
|
|
84
|
-
return {
|
|
268
|
+
return {
|
|
269
|
+
...PcbScene3dDescriptorSafeRecord.copy(element),
|
|
270
|
+
[field]: sourceUrl
|
|
271
|
+
}
|
|
85
272
|
}
|
|
86
273
|
|
|
87
274
|
/**
|
|
@@ -90,11 +277,19 @@ export class CircuitJsonCadModelAssetResolver {
|
|
|
90
277
|
* @returns {string}
|
|
91
278
|
*/
|
|
92
279
|
static #modelAssetUrlField(element) {
|
|
93
|
-
if (
|
|
94
|
-
|
|
280
|
+
if (
|
|
281
|
+
CircuitJsonCadModelAssetResolver.#ownData(element, 'type') !==
|
|
282
|
+
'cad_component'
|
|
283
|
+
) {
|
|
284
|
+
return ''
|
|
285
|
+
}
|
|
286
|
+
const asset = CircuitJsonCadModelAssetResolver.#ownData(
|
|
287
|
+
element,
|
|
288
|
+
'model_asset'
|
|
289
|
+
)
|
|
95
290
|
if (!asset || typeof asset !== 'object') return ''
|
|
96
291
|
if (CircuitJsonCadModelAssetResolver.#hasModelUrl(element)) return ''
|
|
97
|
-
const format =
|
|
292
|
+
const format = PcbScene3dCircuitJsonModelAsset.reference(asset)?.format
|
|
98
293
|
return (
|
|
99
294
|
{
|
|
100
295
|
'3mf': 'model_3mf_url',
|
|
@@ -112,42 +307,445 @@ export class CircuitJsonCadModelAssetResolver {
|
|
|
112
307
|
* Resolves a URL to a session asset or caller-provided value.
|
|
113
308
|
* @param {string} url Model URL.
|
|
114
309
|
* @param {object} context Resolver context.
|
|
115
|
-
* @param {object
|
|
310
|
+
* @param {Map<string, object>} sessionAssetIndex Session asset index.
|
|
311
|
+
* @param {Map<string, object>} documentAssetIndex Canonical document asset index.
|
|
116
312
|
* @param {((url: string, context: object) => any) | null} callerResolver Caller resolver.
|
|
117
313
|
* @returns {object | string | null}
|
|
118
314
|
*/
|
|
119
|
-
static #resolveModelUrl(
|
|
120
|
-
|
|
121
|
-
|
|
315
|
+
static #resolveModelUrl(
|
|
316
|
+
url,
|
|
317
|
+
context,
|
|
318
|
+
sessionAssetIndex,
|
|
319
|
+
documentAssetIndex,
|
|
320
|
+
callerResolver
|
|
321
|
+
) {
|
|
322
|
+
const matchedEntry =
|
|
323
|
+
CircuitJsonCadModelAssetResolver.#matchingIndexedEntry(
|
|
324
|
+
url,
|
|
325
|
+
context,
|
|
326
|
+
sessionAssetIndex
|
|
327
|
+
) ||
|
|
328
|
+
CircuitJsonCadModelAssetResolver.#matchingIndexedEntry(
|
|
122
329
|
url,
|
|
123
330
|
context,
|
|
124
|
-
|
|
331
|
+
documentAssetIndex
|
|
125
332
|
)
|
|
333
|
+
const matchedAsset = matchedEntry
|
|
334
|
+
? CircuitJsonCadModelAssetResolver.#resolvedIndexedAsset(
|
|
335
|
+
matchedEntry
|
|
336
|
+
)
|
|
337
|
+
: null
|
|
126
338
|
if (matchedAsset) {
|
|
127
|
-
return
|
|
339
|
+
return CircuitJsonCadModelAssetResolver.#withCompanionResources(
|
|
340
|
+
matchedAsset,
|
|
341
|
+
matchedEntry,
|
|
342
|
+
url,
|
|
343
|
+
context,
|
|
344
|
+
sessionAssetIndex,
|
|
345
|
+
documentAssetIndex
|
|
346
|
+
)
|
|
128
347
|
}
|
|
129
348
|
return callerResolver ? callerResolver(url, context) : null
|
|
130
349
|
}
|
|
131
350
|
|
|
132
351
|
/**
|
|
133
|
-
* Finds
|
|
352
|
+
* Finds the earliest indexed asset matching one CAD model URL.
|
|
134
353
|
* @param {string} url Model URL.
|
|
135
354
|
* @param {object} context Resolver context.
|
|
136
|
-
* @param {object
|
|
355
|
+
* @param {Map<string, object>} assetIndex Asset alias index.
|
|
137
356
|
* @returns {object | null}
|
|
138
357
|
*/
|
|
139
|
-
static #
|
|
358
|
+
static #matchingIndexedEntry(url, context, assetIndex) {
|
|
140
359
|
const candidates = CircuitJsonCadModelAssetResolver.#urlCandidates(
|
|
141
360
|
url,
|
|
142
361
|
context
|
|
143
362
|
)
|
|
363
|
+
let match = null
|
|
364
|
+
for (const candidate of candidates) {
|
|
365
|
+
const indexed = assetIndex.exact.get(candidate)
|
|
366
|
+
if (indexed && (!match || indexed.index < match.index)) {
|
|
367
|
+
match = indexed
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (match) return match
|
|
371
|
+
|
|
372
|
+
for (const candidate of candidates) {
|
|
373
|
+
const indexed = assetIndex.folded.get(candidate.toLowerCase())
|
|
374
|
+
if (
|
|
375
|
+
indexed &&
|
|
376
|
+
indexed !== AMBIGUOUS_ASSET &&
|
|
377
|
+
(!match || indexed.index < match.index)
|
|
378
|
+
) {
|
|
379
|
+
match = indexed
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return match
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Attaches referenced local sidecars without enabling implicit networking.
|
|
387
|
+
* @param {object} matchedAsset Resolved main model asset.
|
|
388
|
+
* @param {object} matchedEntry Indexed main model entry.
|
|
389
|
+
* @param {string} url Requested main model path.
|
|
390
|
+
* @param {object} context Model resolver context.
|
|
391
|
+
* @param {Map<string, object>} sessionAssetIndex Session asset index.
|
|
392
|
+
* @param {Map<string, object>} documentAssetIndex Document asset index.
|
|
393
|
+
* @returns {object}
|
|
394
|
+
*/
|
|
395
|
+
static #withCompanionResources(
|
|
396
|
+
matchedAsset,
|
|
397
|
+
matchedEntry,
|
|
398
|
+
url,
|
|
399
|
+
context,
|
|
400
|
+
sessionAssetIndex,
|
|
401
|
+
documentAssetIndex
|
|
402
|
+
) {
|
|
403
|
+
const format = String(
|
|
404
|
+
CircuitJsonCadModelAssetResolver.#ownData(context, 'format') || ''
|
|
405
|
+
).toLowerCase()
|
|
406
|
+
const references =
|
|
407
|
+
CircuitJsonCadModelAssetResolver.#companionReferences(
|
|
408
|
+
matchedAsset,
|
|
409
|
+
format
|
|
410
|
+
)
|
|
411
|
+
if (!references.length) return matchedAsset
|
|
412
|
+
|
|
413
|
+
const mainPath =
|
|
414
|
+
PcbScene3dModelIdentity.projectPath(matchedAsset) || String(url)
|
|
415
|
+
const baseDirectory =
|
|
416
|
+
mainPath.includes('/') && !/^[a-z][a-z\d+.-]*:/iu.test(mainPath)
|
|
417
|
+
? mainPath.slice(0, mainPath.lastIndexOf('/') + 1)
|
|
418
|
+
: ''
|
|
419
|
+
const companions = []
|
|
420
|
+
const seen = new Set()
|
|
421
|
+
for (const reference of references) {
|
|
422
|
+
const path = CircuitJsonCadModelAssetResolver.#safeCompanionPath(
|
|
423
|
+
reference,
|
|
424
|
+
baseDirectory
|
|
425
|
+
)
|
|
426
|
+
if (!path || seen.has(path)) continue
|
|
427
|
+
const entry = CircuitJsonCadModelAssetResolver.#indexedCompanion(
|
|
428
|
+
path,
|
|
429
|
+
sessionAssetIndex,
|
|
430
|
+
documentAssetIndex
|
|
431
|
+
)
|
|
432
|
+
if (!entry || entry === matchedEntry) continue
|
|
433
|
+
const asset =
|
|
434
|
+
CircuitJsonCadModelAssetResolver.#resolvedIndexedAsset(entry)
|
|
435
|
+
if (!asset) continue
|
|
436
|
+
companions.push({
|
|
437
|
+
...PcbScene3dDescriptorSafeRecord.copy(asset),
|
|
438
|
+
uri: reference
|
|
439
|
+
})
|
|
440
|
+
seen.add(path)
|
|
441
|
+
}
|
|
442
|
+
if (!companions.length) return matchedAsset
|
|
443
|
+
|
|
444
|
+
const field = format === 'gltf' ? 'externalBuffers' : 'resources'
|
|
445
|
+
const existing = CircuitJsonCadModelAssetResolver.#denseArray(
|
|
446
|
+
CircuitJsonCadModelAssetResolver.#ownData(matchedAsset, field)
|
|
447
|
+
)
|
|
448
|
+
return {
|
|
449
|
+
...PcbScene3dDescriptorSafeRecord.copy(matchedAsset),
|
|
450
|
+
[field]: [...existing, ...companions]
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Extracts referenced local companions from text-capable model formats.
|
|
456
|
+
* @param {object} asset Main model asset.
|
|
457
|
+
* @param {string} format Model format.
|
|
458
|
+
* @returns {string[]}
|
|
459
|
+
*/
|
|
460
|
+
static #companionReferences(asset, format) {
|
|
461
|
+
const text = CircuitJsonCadModelAssetResolver.#assetText(asset)
|
|
462
|
+
if (!text) return []
|
|
463
|
+
if (format === 'gltf') {
|
|
464
|
+
try {
|
|
465
|
+
const document = JSON.parse(text)
|
|
466
|
+
return CircuitJsonCadModelAssetResolver.#denseArray(
|
|
467
|
+
CircuitJsonCadModelAssetResolver.#ownData(
|
|
468
|
+
document,
|
|
469
|
+
'buffers'
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
.map((buffer) =>
|
|
473
|
+
String(
|
|
474
|
+
CircuitJsonCadModelAssetResolver.#ownData(
|
|
475
|
+
buffer,
|
|
476
|
+
'uri'
|
|
477
|
+
) || ''
|
|
478
|
+
).trim()
|
|
479
|
+
)
|
|
480
|
+
.filter(
|
|
481
|
+
(reference) =>
|
|
482
|
+
reference && !reference.startsWith('data:')
|
|
483
|
+
)
|
|
484
|
+
} catch {
|
|
485
|
+
return []
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (format === 'obj') {
|
|
489
|
+
return [...text.matchAll(/^\s*mtllib\s+(.+?)\s*$/gimu)]
|
|
490
|
+
.map((match) =>
|
|
491
|
+
String(match[1] || '')
|
|
492
|
+
.trim()
|
|
493
|
+
.replace(/^(['"])(.*)\1$/u, '$2')
|
|
494
|
+
)
|
|
495
|
+
.filter(Boolean)
|
|
496
|
+
}
|
|
497
|
+
if (format === 'wrl' || format === 'vrml') {
|
|
498
|
+
const references = []
|
|
499
|
+
for (const block of text.matchAll(
|
|
500
|
+
/ImageTexture\s*\{[\s\S]*?\}/giu
|
|
501
|
+
)) {
|
|
502
|
+
for (const match of String(block[0] || '').matchAll(
|
|
503
|
+
/(['"])([^'"]+)\1/gu
|
|
504
|
+
)) {
|
|
505
|
+
const reference = String(match[2] || '').trim()
|
|
506
|
+
if (reference) references.push(reference)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return references
|
|
510
|
+
}
|
|
511
|
+
return []
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Reads resident asset content as UTF-8 text without invoking accessors.
|
|
516
|
+
* @param {object} asset Asset candidate.
|
|
517
|
+
* @returns {string}
|
|
518
|
+
*/
|
|
519
|
+
static #assetText(asset) {
|
|
520
|
+
for (const key of ['payloadText', 'data']) {
|
|
521
|
+
const value = CircuitJsonCadModelAssetResolver.#ownData(asset, key)
|
|
522
|
+
if (typeof value === 'string') return value
|
|
523
|
+
}
|
|
524
|
+
for (const key of ['payloadBytes', 'bytes', 'data']) {
|
|
525
|
+
const bytes = CircuitJsonCadModelAssetResolver.#bytes(
|
|
526
|
+
CircuitJsonCadModelAssetResolver.#ownData(asset, key)
|
|
527
|
+
)
|
|
528
|
+
if (bytes) return new TextDecoder().decode(bytes)
|
|
529
|
+
}
|
|
530
|
+
return ''
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Resolves one path-exact companion with session priority.
|
|
535
|
+
* @param {string} path Safe project-relative companion path.
|
|
536
|
+
* @param {Map<string, object>} sessionAssetIndex Session asset index.
|
|
537
|
+
* @param {Map<string, object>} documentAssetIndex Document asset index.
|
|
538
|
+
* @returns {object | null}
|
|
539
|
+
*/
|
|
540
|
+
static #indexedCompanion(path, sessionAssetIndex, documentAssetIndex) {
|
|
541
|
+
const key = CircuitJsonCadModelAssetResolver.#key(path)
|
|
144
542
|
return (
|
|
145
|
-
|
|
146
|
-
|
|
543
|
+
CircuitJsonCadModelAssetResolver.#indexedAsset(
|
|
544
|
+
sessionAssetIndex,
|
|
545
|
+
key
|
|
546
|
+
) ||
|
|
547
|
+
CircuitJsonCadModelAssetResolver.#indexedAsset(
|
|
548
|
+
documentAssetIndex,
|
|
549
|
+
key
|
|
550
|
+
) ||
|
|
551
|
+
null
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Resolves an exact alias or one unambiguous case-insensitive fallback.
|
|
557
|
+
* @param {{ exact: Map<string, object>, folded: Map<string, object | symbol> }} index Asset index.
|
|
558
|
+
* @param {string} key Exact normalized key.
|
|
559
|
+
* @returns {object | null} Indexed asset entry.
|
|
560
|
+
*/
|
|
561
|
+
static #indexedAsset(index, key) {
|
|
562
|
+
const exact = index.exact.get(key)
|
|
563
|
+
if (exact) return exact
|
|
564
|
+
const folded = index.folded.get(key.toLowerCase())
|
|
565
|
+
return folded && folded !== AMBIGUOUS_ASSET ? folded : null
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Resolves a safe project-relative sidecar path without parent traversal.
|
|
570
|
+
* @param {string} reference Referenced resource path.
|
|
571
|
+
* @param {string} baseDirectory Main model directory.
|
|
572
|
+
* @returns {string}
|
|
573
|
+
*/
|
|
574
|
+
static #safeCompanionPath(reference, baseDirectory) {
|
|
575
|
+
const normalized = String(reference || '')
|
|
576
|
+
.trim()
|
|
577
|
+
.replaceAll('\\', '/')
|
|
578
|
+
let decoded = normalized
|
|
579
|
+
try {
|
|
580
|
+
decoded = decodeURIComponent(normalized)
|
|
581
|
+
} catch {
|
|
582
|
+
return ''
|
|
583
|
+
}
|
|
584
|
+
if (decoded.split('/').some((segment) => segment === '..')) {
|
|
585
|
+
return ''
|
|
586
|
+
}
|
|
587
|
+
return PcbScene3dModelContent.safeProjectPath(normalized, baseDirectory)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Returns a byte view for one resident binary payload.
|
|
592
|
+
* @param {unknown} value Binary payload candidate.
|
|
593
|
+
* @returns {Uint8Array | null}
|
|
594
|
+
*/
|
|
595
|
+
static #bytes(value) {
|
|
596
|
+
if (value instanceof Uint8Array) return value
|
|
597
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value)
|
|
598
|
+
if (ArrayBuffer.isView(value)) {
|
|
599
|
+
return new Uint8Array(
|
|
600
|
+
value.buffer,
|
|
601
|
+
value.byteOffset,
|
|
602
|
+
value.byteLength
|
|
603
|
+
)
|
|
604
|
+
}
|
|
605
|
+
return null
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Builds one descriptor-safe, first-match-preserving asset alias index.
|
|
610
|
+
* @param {object[]} assets Asset rows.
|
|
611
|
+
* @param {boolean} [canonical] Whether rows use the canonical toolkit asset contract.
|
|
612
|
+
* @returns {{ exact: Map<string, object>, folded: Map<string, object | symbol> }}
|
|
613
|
+
*/
|
|
614
|
+
static #assetIndex(assets, canonical = false) {
|
|
615
|
+
const exact = new Map()
|
|
616
|
+
const folded = new Map()
|
|
617
|
+
assets.forEach((asset, assetIndex) => {
|
|
618
|
+
const entry = {
|
|
619
|
+
asset,
|
|
620
|
+
canonical,
|
|
621
|
+
index: assetIndex,
|
|
622
|
+
materialized: false,
|
|
623
|
+
resolvedAsset: null
|
|
624
|
+
}
|
|
625
|
+
for (const alias of CircuitJsonCadModelAssetResolver.#assetAliases(
|
|
626
|
+
asset
|
|
627
|
+
)) {
|
|
628
|
+
if (!exact.has(alias)) exact.set(alias, entry)
|
|
629
|
+
const foldedAlias = alias.toLowerCase()
|
|
630
|
+
const previous = folded.get(foldedAlias)
|
|
631
|
+
if (!previous) {
|
|
632
|
+
folded.set(foldedAlias, entry)
|
|
633
|
+
} else if (previous !== entry) {
|
|
634
|
+
folded.set(foldedAlias, AMBIGUOUS_ASSET)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
})
|
|
638
|
+
return { exact, folded }
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Copies a dense ordinary array without invoking element accessors.
|
|
643
|
+
* @param {unknown} value Array candidate.
|
|
644
|
+
* @returns {unknown[]}
|
|
645
|
+
*/
|
|
646
|
+
static #denseArray(value) {
|
|
647
|
+
let descriptors
|
|
648
|
+
let prototype
|
|
649
|
+
try {
|
|
650
|
+
if (!Array.isArray(value)) return []
|
|
651
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
652
|
+
prototype = Object.getPrototypeOf(value)
|
|
653
|
+
} catch {
|
|
654
|
+
return []
|
|
655
|
+
}
|
|
656
|
+
const length = descriptors.length?.value
|
|
657
|
+
if (
|
|
658
|
+
prototype !== Array.prototype ||
|
|
659
|
+
!Number.isSafeInteger(length) ||
|
|
660
|
+
length < 0
|
|
661
|
+
) {
|
|
662
|
+
return []
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const result = new Array(length)
|
|
666
|
+
for (let index = 0; index < length; index += 1) {
|
|
667
|
+
const descriptor = descriptors[String(index)]
|
|
668
|
+
if (
|
|
669
|
+
!descriptor ||
|
|
670
|
+
descriptor.enumerable !== true ||
|
|
671
|
+
!Object.hasOwn(descriptor, 'value')
|
|
672
|
+
) {
|
|
673
|
+
return []
|
|
674
|
+
}
|
|
675
|
+
result[index] = descriptor.value
|
|
676
|
+
}
|
|
677
|
+
return result
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Materializes a canonical asset only when its first reference is used.
|
|
682
|
+
* @param {{ asset: object, canonical: boolean, materialized: boolean, resolvedAsset: object | null }} entry Indexed asset entry.
|
|
683
|
+
* @returns {object | null}
|
|
684
|
+
*/
|
|
685
|
+
static #resolvedIndexedAsset(entry) {
|
|
686
|
+
if (!entry.materialized) {
|
|
687
|
+
entry.resolvedAsset =
|
|
688
|
+
CircuitJsonCadModelAssetResolver.#canonicalAsset(entry.asset) ||
|
|
689
|
+
entry.asset
|
|
690
|
+
entry.materialized = true
|
|
691
|
+
}
|
|
692
|
+
return entry.resolvedAsset
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Materializes one validated canonical asset into safe resolver metadata.
|
|
697
|
+
* @param {unknown} asset Canonical asset candidate.
|
|
698
|
+
* @returns {Record<string, unknown> | null}
|
|
699
|
+
*/
|
|
700
|
+
static #canonicalAsset(asset) {
|
|
701
|
+
try {
|
|
702
|
+
const prepared = ToolkitAsset.create(asset)
|
|
703
|
+
return {
|
|
704
|
+
...PcbScene3dDescriptorSafeRecord.copy(prepared),
|
|
705
|
+
data: prepared.data
|
|
706
|
+
}
|
|
707
|
+
} catch {
|
|
708
|
+
return null
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Reads exact legacy and canonical path aliases without invoking accessors.
|
|
714
|
+
* @param {object} asset Asset row.
|
|
715
|
+
* @returns {Set<string>}
|
|
716
|
+
*/
|
|
717
|
+
static #assetAliases(asset) {
|
|
718
|
+
const source = CircuitJsonCadModelAssetResolver.#ownData(
|
|
719
|
+
asset,
|
|
720
|
+
'source'
|
|
721
|
+
)
|
|
722
|
+
return new Set(
|
|
723
|
+
[
|
|
724
|
+
CircuitJsonCadModelAssetResolver.#ownData(
|
|
147
725
|
asset,
|
|
148
|
-
|
|
149
|
-
)
|
|
150
|
-
|
|
726
|
+
'relativePath'
|
|
727
|
+
),
|
|
728
|
+
CircuitJsonCadModelAssetResolver.#ownData(asset, 'sourceUrl'),
|
|
729
|
+
CircuitJsonCadModelAssetResolver.#ownData(asset, 'url'),
|
|
730
|
+
CircuitJsonCadModelAssetResolver.#ownData(asset, 'name'),
|
|
731
|
+
CircuitJsonCadModelAssetResolver.#ownData(source, 'entryName'),
|
|
732
|
+
CircuitJsonCadModelAssetResolver.#ownData(
|
|
733
|
+
source,
|
|
734
|
+
'projectRelativePath'
|
|
735
|
+
),
|
|
736
|
+
CircuitJsonCadModelAssetResolver.#ownData(
|
|
737
|
+
source,
|
|
738
|
+
'project_relative_path'
|
|
739
|
+
),
|
|
740
|
+
CircuitJsonCadModelAssetResolver.#ownData(
|
|
741
|
+
source,
|
|
742
|
+
'relativePath'
|
|
743
|
+
),
|
|
744
|
+
CircuitJsonCadModelAssetResolver.#ownData(source, 'url'),
|
|
745
|
+
CircuitJsonCadModelAssetResolver.#ownData(source, 'uri')
|
|
746
|
+
]
|
|
747
|
+
.map((value) => CircuitJsonCadModelAssetResolver.#key(value))
|
|
748
|
+
.filter(Boolean)
|
|
151
749
|
)
|
|
152
750
|
}
|
|
153
751
|
|
|
@@ -158,11 +756,22 @@ export class CircuitJsonCadModelAssetResolver {
|
|
|
158
756
|
* @returns {Set<string>}
|
|
159
757
|
*/
|
|
160
758
|
static #urlCandidates(url, context) {
|
|
759
|
+
const cadComponent = CircuitJsonCadModelAssetResolver.#ownData(
|
|
760
|
+
context,
|
|
761
|
+
'cadComponent'
|
|
762
|
+
)
|
|
763
|
+
const modelAsset = CircuitJsonCadModelAssetResolver.#ownData(
|
|
764
|
+
cadComponent,
|
|
765
|
+
'model_asset'
|
|
766
|
+
)
|
|
161
767
|
return new Set(
|
|
162
768
|
[
|
|
163
769
|
url,
|
|
164
|
-
|
|
165
|
-
|
|
770
|
+
CircuitJsonCadModelAssetResolver.#ownData(
|
|
771
|
+
modelAsset,
|
|
772
|
+
'project_relative_path'
|
|
773
|
+
),
|
|
774
|
+
CircuitJsonCadModelAssetResolver.#ownData(modelAsset, 'url')
|
|
166
775
|
]
|
|
167
776
|
.map((value) => CircuitJsonCadModelAssetResolver.#key(value))
|
|
168
777
|
.filter(Boolean)
|
|
@@ -170,45 +779,21 @@ export class CircuitJsonCadModelAssetResolver {
|
|
|
170
779
|
}
|
|
171
780
|
|
|
172
781
|
/**
|
|
173
|
-
*
|
|
174
|
-
* @param {
|
|
175
|
-
* @param {
|
|
176
|
-
* @returns {
|
|
177
|
-
*/
|
|
178
|
-
static #assetMatches(asset, candidates) {
|
|
179
|
-
return [
|
|
180
|
-
asset?.relativePath,
|
|
181
|
-
asset?.sourceUrl,
|
|
182
|
-
asset?.url,
|
|
183
|
-
asset?.name
|
|
184
|
-
].some((value) =>
|
|
185
|
-
candidates.has(CircuitJsonCadModelAssetResolver.#key(value))
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Resolves a supported model format from asset metadata.
|
|
191
|
-
* @param {object} asset Model asset metadata.
|
|
192
|
-
* @returns {string}
|
|
782
|
+
* Reads one own data property without invoking accessors.
|
|
783
|
+
* @param {unknown} value Record candidate.
|
|
784
|
+
* @param {PropertyKey} key Property key.
|
|
785
|
+
* @returns {unknown}
|
|
193
786
|
*/
|
|
194
|
-
static #
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (/\bgltf\b|\.gltf(?:[?#]|$)/u.test(text)) return 'gltf'
|
|
204
|
-
if (/\b3mf\b|\.3mf(?:[?#]|$)/u.test(text)) return '3mf'
|
|
205
|
-
if (/\bobj\b|\.obj(?:[?#]|$)/u.test(text)) return 'obj'
|
|
206
|
-
if (/\bstl\b|\.stl(?:[?#]|$)/u.test(text)) return 'stl'
|
|
207
|
-
if (/\bwrl\b|vrml|\.wrl(?:[?#]|$)/u.test(text)) return 'wrl'
|
|
208
|
-
if (/step|stp|\.step(?:[?#]|$)|\.stp(?:[?#]|$)/u.test(text)) {
|
|
209
|
-
return 'step'
|
|
787
|
+
static #ownData(value, key) {
|
|
788
|
+
if (!value || typeof value !== 'object') return undefined
|
|
789
|
+
try {
|
|
790
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key)
|
|
791
|
+
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
792
|
+
? descriptor.value
|
|
793
|
+
: undefined
|
|
794
|
+
} catch {
|
|
795
|
+
return undefined
|
|
210
796
|
}
|
|
211
|
-
return ''
|
|
212
797
|
}
|
|
213
798
|
|
|
214
799
|
/**
|
|
@@ -227,7 +812,11 @@ export class CircuitJsonCadModelAssetResolver {
|
|
|
227
812
|
'model_gltf_url',
|
|
228
813
|
'model_stl_url',
|
|
229
814
|
'model_obj_url'
|
|
230
|
-
].some((field) =>
|
|
815
|
+
].some((field) =>
|
|
816
|
+
String(
|
|
817
|
+
CircuitJsonCadModelAssetResolver.#ownData(element, field) || ''
|
|
818
|
+
).trim()
|
|
819
|
+
)
|
|
231
820
|
}
|
|
232
821
|
|
|
233
822
|
/**
|
|
@@ -237,11 +826,36 @@ export class CircuitJsonCadModelAssetResolver {
|
|
|
237
826
|
*/
|
|
238
827
|
static #elements(documentModel) {
|
|
239
828
|
if (Array.isArray(documentModel)) return documentModel
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
829
|
+
if (
|
|
830
|
+
CircuitJsonCadModelAssetResolver.#isContext(documentModel) &&
|
|
831
|
+
Array.isArray(documentModel.document?.model)
|
|
832
|
+
) {
|
|
833
|
+
return documentModel.document.model
|
|
244
834
|
}
|
|
835
|
+
const wrappedDocument = CircuitJsonCadModelAssetResolver.#ownData(
|
|
836
|
+
documentModel,
|
|
837
|
+
'document'
|
|
838
|
+
)
|
|
839
|
+
const wrappedModel = CircuitJsonCadModelAssetResolver.#ownData(
|
|
840
|
+
wrappedDocument,
|
|
841
|
+
'model'
|
|
842
|
+
)
|
|
843
|
+
if (Array.isArray(wrappedModel)) return wrappedModel
|
|
844
|
+
const model = CircuitJsonCadModelAssetResolver.#ownData(
|
|
845
|
+
documentModel,
|
|
846
|
+
'model'
|
|
847
|
+
)
|
|
848
|
+
if (Array.isArray(model)) return model
|
|
849
|
+
const elements = CircuitJsonCadModelAssetResolver.#ownData(
|
|
850
|
+
documentModel,
|
|
851
|
+
'elements'
|
|
852
|
+
)
|
|
853
|
+
if (Array.isArray(elements)) return elements
|
|
854
|
+
const circuitJson = CircuitJsonCadModelAssetResolver.#ownData(
|
|
855
|
+
documentModel,
|
|
856
|
+
'circuitJson'
|
|
857
|
+
)
|
|
858
|
+
if (Array.isArray(circuitJson)) return circuitJson
|
|
245
859
|
return []
|
|
246
860
|
}
|
|
247
861
|
|
|
@@ -251,11 +865,11 @@ export class CircuitJsonCadModelAssetResolver {
|
|
|
251
865
|
* @returns {string}
|
|
252
866
|
*/
|
|
253
867
|
static #key(value) {
|
|
868
|
+
if (typeof value !== 'string' && typeof value !== 'number') return ''
|
|
254
869
|
return String(value || '')
|
|
255
870
|
.trim()
|
|
256
871
|
.split(/[?#]/u)[0]
|
|
257
872
|
.replace(/^https?:\/\/[^/]+\//iu, '')
|
|
258
873
|
.replace(/^\/+/u, '')
|
|
259
|
-
.toLowerCase()
|
|
260
874
|
}
|
|
261
875
|
}
|