pcb-scene3d-viewer 1.1.21 → 1.1.31
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 +3 -2
- package/docs/api.md +27 -1
- package/docs/circuitjson.md +63 -18
- package/docs/model-format.md +13 -2
- package/package.json +1 -1
- package/spec/library-scope.md +2 -2
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +25 -5
- package/src/PcbAssemblyComponentMeshBuilder.mjs +762 -0
- package/src/PcbAssemblyFillGeometryResolver.mjs +579 -0
- package/src/PcbAssemblyFillRingNormalizer.mjs +209 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +41 -301
- package/src/PcbAssemblyGltfModelMeshParser.mjs +912 -0
- package/src/PcbAssemblyGltfValidator.mjs +460 -0
- package/src/PcbAssemblyGltfWriter.mjs +801 -0
- package/src/PcbAssemblyMeshUtils.mjs +117 -0
- package/src/PcbAssemblyModelMeshLoader.mjs +18 -0
- package/src/PcbAssemblyPadMeshBuilder.mjs +394 -0
- package/src/PcbAssemblyStepWriter.mjs +24 -2
- package/src/PcbAssemblyTextModelMeshParser.mjs +602 -0
- package/src/PcbModelArchiveExporter.mjs +521 -7
- package/src/PcbScene3dCircuitJsonAdapter.mjs +409 -7
- package/src/PcbScene3dCircuitJsonModelTransform.mjs +232 -0
- package/src/PcbScene3dCompanionBasePlacementAdjuster.mjs +242 -0
- package/src/PcbScene3dComponentVisibility.mjs +100 -5
- package/src/PcbScene3dController.mjs +25 -55
- package/src/PcbScene3dCopperDetailFilter.mjs +86 -9
- package/src/PcbScene3dCopperDetailGroupBuilder.mjs +186 -15
- package/src/PcbScene3dCopperDrillCutoutBuilder.mjs +258 -0
- package/src/PcbScene3dCopperFactory.mjs +99 -85
- package/src/PcbScene3dCopperFillMeshBuilder.mjs +393 -0
- package/src/PcbScene3dCopperLayerFilter.mjs +32 -3
- package/src/PcbScene3dCutoutGeometryFilter.mjs +17 -14
- package/src/PcbScene3dExternalCompanionFallback.mjs +202 -0
- package/src/PcbScene3dExternalModelCenteringPolicy.mjs +21 -0
- package/src/PcbScene3dExternalModelOpacity.mjs +51 -0
- package/src/PcbScene3dExternalModelPlacementRepair.mjs +5 -2
- package/src/PcbScene3dExternalModelSourceOriginPolicy.mjs +41 -0
- package/src/PcbScene3dExternalModels.mjs +16 -7
- package/src/PcbScene3dFallbackBodyFactory.mjs +105 -0
- package/src/PcbScene3dFallbackVisibility.mjs +58 -1
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +68 -0
- package/src/PcbScene3dPadFactory.mjs +35 -2
- package/src/PcbScene3dRenderGroupVisibility.mjs +8 -1
- package/src/PcbScene3dRuntime.mjs +94 -100
- package/src/PcbScene3dRuntimeHelpers.mjs +39 -0
- package/src/PcbScene3dSelectionIndexBuilder.mjs +87 -0
- package/src/PcbScene3dSelectionInspectorRenderer.mjs +50 -11
- package/src/PcbScene3dSelectionMarkerFactory.mjs +333 -0
- package/src/PcbScene3dSelectionMarkerOverlay.mjs +70 -0
- package/src/PcbScene3dSelectionStyler.mjs +52 -2
- package/src/PcbScene3dStaticBodyFactory.mjs +263 -0
- package/src/PcbScene3dText.mjs +1 -0
- package/src/PcbScene3dTransparentMeshSplitter.mjs +623 -0
- package/src/PcbScene3dViaFactory.mjs +27 -7
- package/src/scene3d.mjs +3 -0
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
const MM_TO_MIL = 1000 / 25.4
|
|
2
|
+
const GLB_MAGIC = 0x46546c67
|
|
3
|
+
const GLB_VERSION = 2
|
|
4
|
+
const JSON_CHUNK_TYPE = 0x4e4f534a
|
|
5
|
+
const BIN_CHUNK_TYPE = 0x004e4942
|
|
6
|
+
const COMPONENT_BYTE_LENGTHS = new Map([
|
|
7
|
+
[5120, 1],
|
|
8
|
+
[5121, 1],
|
|
9
|
+
[5122, 2],
|
|
10
|
+
[5123, 2],
|
|
11
|
+
[5125, 4],
|
|
12
|
+
[5126, 4]
|
|
13
|
+
])
|
|
14
|
+
const ACCESSOR_WIDTHS = new Map([
|
|
15
|
+
['SCALAR', 1],
|
|
16
|
+
['VEC2', 2],
|
|
17
|
+
['VEC3', 3],
|
|
18
|
+
['VEC4', 4],
|
|
19
|
+
['MAT4', 16]
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parses GLTF 2.0 JSON and binary GLB component models into assembly meshes.
|
|
24
|
+
*/
|
|
25
|
+
export class PcbAssemblyGltfModelMeshParser {
|
|
26
|
+
/**
|
|
27
|
+
* Parses one GLTF JSON model.
|
|
28
|
+
* @param {{ name?: string, payloadText?: string, file?: any }} model Model metadata.
|
|
29
|
+
* @returns {Promise<object[]>}
|
|
30
|
+
*/
|
|
31
|
+
static async parseGltfModel(model) {
|
|
32
|
+
const text = await PcbAssemblyGltfModelMeshParser.#readModelText(
|
|
33
|
+
model,
|
|
34
|
+
'GLTF'
|
|
35
|
+
)
|
|
36
|
+
const gltf = JSON.parse(text)
|
|
37
|
+
return await PcbAssemblyGltfModelMeshParser.#parseDocument(
|
|
38
|
+
gltf,
|
|
39
|
+
model,
|
|
40
|
+
null
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parses one binary GLB model.
|
|
46
|
+
* @param {{ name?: string, payloadBytes?: any, bytes?: any, file?: any }} model Model metadata.
|
|
47
|
+
* @returns {Promise<object[]>}
|
|
48
|
+
*/
|
|
49
|
+
static async parseGlbModel(model) {
|
|
50
|
+
const bytes = await PcbAssemblyGltfModelMeshParser.#readModelBytes(
|
|
51
|
+
model,
|
|
52
|
+
'GLB'
|
|
53
|
+
)
|
|
54
|
+
const parsed = PcbAssemblyGltfModelMeshParser.parseGlb(bytes)
|
|
55
|
+
return await PcbAssemblyGltfModelMeshParser.#parseDocument(
|
|
56
|
+
parsed.gltf,
|
|
57
|
+
model,
|
|
58
|
+
parsed.binaryBuffer
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extracts a JSON document and optional binary chunk from GLB bytes.
|
|
64
|
+
* @param {Uint8Array} bytes GLB bytes.
|
|
65
|
+
* @returns {{ gltf: object, binaryBuffer: Uint8Array | null }}
|
|
66
|
+
*/
|
|
67
|
+
static parseGlb(bytes) {
|
|
68
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength < 20) {
|
|
69
|
+
throw new Error('GLB content is not available.')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const view = new DataView(
|
|
73
|
+
bytes.buffer,
|
|
74
|
+
bytes.byteOffset,
|
|
75
|
+
bytes.byteLength
|
|
76
|
+
)
|
|
77
|
+
if (
|
|
78
|
+
view.getUint32(0, true) !== GLB_MAGIC ||
|
|
79
|
+
view.getUint32(4, true) !== GLB_VERSION
|
|
80
|
+
) {
|
|
81
|
+
throw new Error('GLB header is not valid.')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const totalLength = view.getUint32(8, true)
|
|
85
|
+
if (totalLength > bytes.byteLength) {
|
|
86
|
+
throw new Error('GLB length exceeds available bytes.')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let offset = 12
|
|
90
|
+
let json = null
|
|
91
|
+
let binaryBuffer = null
|
|
92
|
+
while (offset + 8 <= totalLength) {
|
|
93
|
+
const chunkLength = view.getUint32(offset, true)
|
|
94
|
+
const chunkType = view.getUint32(offset + 4, true)
|
|
95
|
+
const chunkOffset = offset + 8
|
|
96
|
+
const chunkEnd = chunkOffset + chunkLength
|
|
97
|
+
if (chunkEnd > totalLength) {
|
|
98
|
+
throw new Error('GLB chunk exceeds container length.')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const chunk = bytes.slice(chunkOffset, chunkEnd)
|
|
102
|
+
if (chunkType === JSON_CHUNK_TYPE) {
|
|
103
|
+
json = JSON.parse(new TextDecoder().decode(chunk).trim())
|
|
104
|
+
} else if (chunkType === BIN_CHUNK_TYPE) {
|
|
105
|
+
binaryBuffer = chunk
|
|
106
|
+
}
|
|
107
|
+
offset = chunkEnd
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!json) {
|
|
111
|
+
throw new Error('GLB JSON chunk is missing.')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { gltf: json, binaryBuffer }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parses a loaded GLTF document.
|
|
119
|
+
* @param {object} gltf GLTF JSON.
|
|
120
|
+
* @param {object} model Model metadata.
|
|
121
|
+
* @param {Uint8Array | null} binaryBuffer Optional GLB binary chunk.
|
|
122
|
+
* @returns {Promise<object[]>}
|
|
123
|
+
*/
|
|
124
|
+
static async #parseDocument(gltf, model, binaryBuffer) {
|
|
125
|
+
const buffers = await PcbAssemblyGltfModelMeshParser.#resolveBuffers(
|
|
126
|
+
gltf,
|
|
127
|
+
model,
|
|
128
|
+
binaryBuffer
|
|
129
|
+
)
|
|
130
|
+
const meshes = []
|
|
131
|
+
const sceneNodes =
|
|
132
|
+
PcbAssemblyGltfModelMeshParser.#sceneNodeIndexes(gltf)
|
|
133
|
+
|
|
134
|
+
if (sceneNodes.length) {
|
|
135
|
+
sceneNodes.forEach((nodeIndex) => {
|
|
136
|
+
PcbAssemblyGltfModelMeshParser.#appendNodeMeshes(
|
|
137
|
+
gltf,
|
|
138
|
+
buffers,
|
|
139
|
+
nodeIndex,
|
|
140
|
+
PcbAssemblyGltfModelMeshParser.#identityMatrix(),
|
|
141
|
+
meshes
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
} else {
|
|
145
|
+
PcbAssemblyGltfModelMeshParser.#array(gltf?.meshes).forEach(
|
|
146
|
+
(mesh, index) => {
|
|
147
|
+
PcbAssemblyGltfModelMeshParser.#appendMeshPrimitives(
|
|
148
|
+
gltf,
|
|
149
|
+
buffers,
|
|
150
|
+
mesh,
|
|
151
|
+
PcbAssemblyGltfModelMeshParser.#identityMatrix(),
|
|
152
|
+
meshes,
|
|
153
|
+
index
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!meshes.length) {
|
|
160
|
+
throw new Error('No mesh geometry was found in GLTF.')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return meshes.map((mesh, index) => ({
|
|
164
|
+
...mesh,
|
|
165
|
+
name:
|
|
166
|
+
mesh.name || String(model?.name || 'gltf-model-' + (index + 1))
|
|
167
|
+
}))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Appends meshes from one node and its children.
|
|
172
|
+
* @param {object} gltf GLTF JSON.
|
|
173
|
+
* @param {Uint8Array[]} buffers Loaded buffers.
|
|
174
|
+
* @param {number} nodeIndex Node index.
|
|
175
|
+
* @param {number[]} parentMatrix Parent transform matrix.
|
|
176
|
+
* @param {object[]} meshes Mutable mesh output.
|
|
177
|
+
* @returns {void}
|
|
178
|
+
*/
|
|
179
|
+
static #appendNodeMeshes(gltf, buffers, nodeIndex, parentMatrix, meshes) {
|
|
180
|
+
const node = PcbAssemblyGltfModelMeshParser.#array(gltf?.nodes)[
|
|
181
|
+
nodeIndex
|
|
182
|
+
]
|
|
183
|
+
if (!node) {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const matrix = PcbAssemblyGltfModelMeshParser.#multiplyMatrices(
|
|
188
|
+
parentMatrix,
|
|
189
|
+
PcbAssemblyGltfModelMeshParser.#nodeMatrix(node)
|
|
190
|
+
)
|
|
191
|
+
const meshIndex = Number(node.mesh)
|
|
192
|
+
if (Number.isInteger(meshIndex)) {
|
|
193
|
+
PcbAssemblyGltfModelMeshParser.#appendMeshPrimitives(
|
|
194
|
+
gltf,
|
|
195
|
+
buffers,
|
|
196
|
+
PcbAssemblyGltfModelMeshParser.#array(gltf?.meshes)[meshIndex],
|
|
197
|
+
matrix,
|
|
198
|
+
meshes,
|
|
199
|
+
meshIndex,
|
|
200
|
+
node
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
PcbAssemblyGltfModelMeshParser.#array(node.children).forEach(
|
|
205
|
+
(childIndex) => {
|
|
206
|
+
PcbAssemblyGltfModelMeshParser.#appendNodeMeshes(
|
|
207
|
+
gltf,
|
|
208
|
+
buffers,
|
|
209
|
+
Number(childIndex),
|
|
210
|
+
matrix,
|
|
211
|
+
meshes
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Appends primitives from one GLTF mesh.
|
|
219
|
+
* @param {object} gltf GLTF JSON.
|
|
220
|
+
* @param {Uint8Array[]} buffers Loaded buffers.
|
|
221
|
+
* @param {object | undefined} mesh GLTF mesh.
|
|
222
|
+
* @param {number[]} matrix Node transform matrix.
|
|
223
|
+
* @param {object[]} meshes Mutable mesh output.
|
|
224
|
+
* @param {number} meshIndex Mesh index.
|
|
225
|
+
* @param {object | null} [node] Node metadata.
|
|
226
|
+
* @returns {void}
|
|
227
|
+
*/
|
|
228
|
+
static #appendMeshPrimitives(
|
|
229
|
+
gltf,
|
|
230
|
+
buffers,
|
|
231
|
+
mesh,
|
|
232
|
+
matrix,
|
|
233
|
+
meshes,
|
|
234
|
+
meshIndex,
|
|
235
|
+
node = null
|
|
236
|
+
) {
|
|
237
|
+
PcbAssemblyGltfModelMeshParser.#array(mesh?.primitives).forEach(
|
|
238
|
+
(primitive, primitiveIndex) => {
|
|
239
|
+
const parsed = PcbAssemblyGltfModelMeshParser.#primitiveMesh(
|
|
240
|
+
gltf,
|
|
241
|
+
buffers,
|
|
242
|
+
mesh,
|
|
243
|
+
primitive,
|
|
244
|
+
matrix,
|
|
245
|
+
meshIndex,
|
|
246
|
+
primitiveIndex,
|
|
247
|
+
node
|
|
248
|
+
)
|
|
249
|
+
if (parsed) {
|
|
250
|
+
meshes.push(parsed)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Converts one GLTF primitive into an assembly mesh.
|
|
258
|
+
* @param {object} gltf GLTF JSON.
|
|
259
|
+
* @param {Uint8Array[]} buffers Loaded buffers.
|
|
260
|
+
* @param {object | undefined} mesh GLTF mesh.
|
|
261
|
+
* @param {object} primitive GLTF primitive.
|
|
262
|
+
* @param {number[]} matrix Node transform matrix.
|
|
263
|
+
* @param {number} meshIndex Mesh index.
|
|
264
|
+
* @param {number} primitiveIndex Primitive index.
|
|
265
|
+
* @param {object | null} node Node metadata.
|
|
266
|
+
* @returns {object | null}
|
|
267
|
+
*/
|
|
268
|
+
static #primitiveMesh(
|
|
269
|
+
gltf,
|
|
270
|
+
buffers,
|
|
271
|
+
mesh,
|
|
272
|
+
primitive,
|
|
273
|
+
matrix,
|
|
274
|
+
meshIndex,
|
|
275
|
+
primitiveIndex,
|
|
276
|
+
node
|
|
277
|
+
) {
|
|
278
|
+
const positionAccessor = primitive?.attributes?.POSITION
|
|
279
|
+
if (!Number.isInteger(positionAccessor)) {
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const positions = PcbAssemblyGltfModelMeshParser.#readAccessor(
|
|
284
|
+
gltf,
|
|
285
|
+
buffers,
|
|
286
|
+
positionAccessor
|
|
287
|
+
)
|
|
288
|
+
const vertices = positions.map((point) =>
|
|
289
|
+
PcbAssemblyGltfModelMeshParser.#pointMmToMil(
|
|
290
|
+
PcbAssemblyGltfModelMeshParser.#transformPoint(matrix, point)
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
const indexes = Number.isInteger(primitive?.indices)
|
|
294
|
+
? PcbAssemblyGltfModelMeshParser.#readAccessor(
|
|
295
|
+
gltf,
|
|
296
|
+
buffers,
|
|
297
|
+
primitive.indices
|
|
298
|
+
).flat()
|
|
299
|
+
: vertices.map((_vertex, index) => index)
|
|
300
|
+
const faces =
|
|
301
|
+
PcbAssemblyGltfModelMeshParser.#triangleFacesFromIndexes(indexes)
|
|
302
|
+
|
|
303
|
+
if (!vertices.length || !faces.length) {
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
name: PcbAssemblyGltfModelMeshParser.#primitiveName(
|
|
309
|
+
mesh,
|
|
310
|
+
node,
|
|
311
|
+
meshIndex,
|
|
312
|
+
primitiveIndex
|
|
313
|
+
),
|
|
314
|
+
vertices,
|
|
315
|
+
faces,
|
|
316
|
+
...PcbAssemblyGltfModelMeshParser.#primitiveMaterial(
|
|
317
|
+
gltf,
|
|
318
|
+
primitive
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Reads an accessor into numeric tuples.
|
|
325
|
+
* @param {object} gltf GLTF JSON.
|
|
326
|
+
* @param {Uint8Array[]} buffers Loaded buffers.
|
|
327
|
+
* @param {number} accessorIndex Accessor index.
|
|
328
|
+
* @returns {number[][]}
|
|
329
|
+
*/
|
|
330
|
+
static #readAccessor(gltf, buffers, accessorIndex) {
|
|
331
|
+
const accessor = PcbAssemblyGltfModelMeshParser.#array(gltf?.accessors)[
|
|
332
|
+
accessorIndex
|
|
333
|
+
]
|
|
334
|
+
const bufferView = PcbAssemblyGltfModelMeshParser.#array(
|
|
335
|
+
gltf?.bufferViews
|
|
336
|
+
)[Number(accessor?.bufferView)]
|
|
337
|
+
if (!accessor || !bufferView) {
|
|
338
|
+
return []
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const componentLength =
|
|
342
|
+
COMPONENT_BYTE_LENGTHS.get(Number(accessor.componentType)) || 0
|
|
343
|
+
const width =
|
|
344
|
+
ACCESSOR_WIDTHS.get(String(accessor.type || 'SCALAR')) || 1
|
|
345
|
+
const buffer =
|
|
346
|
+
buffers[Number(bufferView.buffer || 0)] || new Uint8Array()
|
|
347
|
+
const view = new DataView(
|
|
348
|
+
buffer.buffer,
|
|
349
|
+
buffer.byteOffset,
|
|
350
|
+
buffer.byteLength
|
|
351
|
+
)
|
|
352
|
+
const stride =
|
|
353
|
+
Number(bufferView.byteStride || 0) || componentLength * width
|
|
354
|
+
const baseOffset =
|
|
355
|
+
Number(bufferView.byteOffset || 0) +
|
|
356
|
+
Number(accessor.byteOffset || 0)
|
|
357
|
+
const values = []
|
|
358
|
+
|
|
359
|
+
for (let index = 0; index < Number(accessor.count || 0); index += 1) {
|
|
360
|
+
const tuple = []
|
|
361
|
+
const tupleOffset = baseOffset + index * stride
|
|
362
|
+
for (let entry = 0; entry < width; entry += 1) {
|
|
363
|
+
tuple.push(
|
|
364
|
+
PcbAssemblyGltfModelMeshParser.#readComponent(
|
|
365
|
+
view,
|
|
366
|
+
tupleOffset + entry * componentLength,
|
|
367
|
+
Number(accessor.componentType),
|
|
368
|
+
accessor.normalized === true
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
values.push(tuple)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return values
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Reads one accessor component.
|
|
380
|
+
* @param {DataView} view Binary view.
|
|
381
|
+
* @param {number} offset Byte offset.
|
|
382
|
+
* @param {number} componentType GLTF component type.
|
|
383
|
+
* @param {boolean} normalized Whether integer data is normalized.
|
|
384
|
+
* @returns {number}
|
|
385
|
+
*/
|
|
386
|
+
static #readComponent(view, offset, componentType, normalized) {
|
|
387
|
+
if (offset < 0 || offset >= view.byteLength) {
|
|
388
|
+
return 0
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (componentType === 5120) {
|
|
392
|
+
const value = view.getInt8(offset)
|
|
393
|
+
return normalized ? Math.max(value / 127, -1) : value
|
|
394
|
+
}
|
|
395
|
+
if (componentType === 5121) {
|
|
396
|
+
const value = view.getUint8(offset)
|
|
397
|
+
return normalized ? value / 255 : value
|
|
398
|
+
}
|
|
399
|
+
if (componentType === 5122) {
|
|
400
|
+
const value = view.getInt16(offset, true)
|
|
401
|
+
return normalized ? Math.max(value / 32767, -1) : value
|
|
402
|
+
}
|
|
403
|
+
if (componentType === 5123) {
|
|
404
|
+
const value = view.getUint16(offset, true)
|
|
405
|
+
return normalized ? value / 65535 : value
|
|
406
|
+
}
|
|
407
|
+
if (componentType === 5125) {
|
|
408
|
+
return view.getUint32(offset, true)
|
|
409
|
+
}
|
|
410
|
+
if (componentType === 5126) {
|
|
411
|
+
return view.getFloat32(offset, true)
|
|
412
|
+
}
|
|
413
|
+
return 0
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Builds triangle faces from flat indexes.
|
|
418
|
+
* @param {number[]} indexes Flat indexes.
|
|
419
|
+
* @returns {number[][]}
|
|
420
|
+
*/
|
|
421
|
+
static #triangleFacesFromIndexes(indexes) {
|
|
422
|
+
const faces = []
|
|
423
|
+
for (let index = 0; index + 2 < indexes.length; index += 3) {
|
|
424
|
+
faces.push([
|
|
425
|
+
Number(indexes[index] || 0),
|
|
426
|
+
Number(indexes[index + 1] || 0),
|
|
427
|
+
Number(indexes[index + 2] || 0)
|
|
428
|
+
])
|
|
429
|
+
}
|
|
430
|
+
return faces
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Resolves material properties for one primitive.
|
|
435
|
+
* @param {object} gltf GLTF JSON.
|
|
436
|
+
* @param {object} primitive GLTF primitive.
|
|
437
|
+
* @returns {{ color?: number[] }}
|
|
438
|
+
*/
|
|
439
|
+
static #primitiveMaterial(gltf, primitive) {
|
|
440
|
+
const materialIndex = Number(primitive?.material)
|
|
441
|
+
const material = Number.isInteger(materialIndex)
|
|
442
|
+
? PcbAssemblyGltfModelMeshParser.#array(gltf?.materials)[
|
|
443
|
+
materialIndex
|
|
444
|
+
]
|
|
445
|
+
: null
|
|
446
|
+
const color =
|
|
447
|
+
material?.pbrMetallicRoughness?.baseColorFactor ||
|
|
448
|
+
material?.baseColorFactor
|
|
449
|
+
|
|
450
|
+
if (!Array.isArray(color) || color.length < 3) {
|
|
451
|
+
return {}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const rgb = [0, 1, 2].map((index) =>
|
|
455
|
+
PcbAssemblyGltfModelMeshParser.#clampUnit(color[index])
|
|
456
|
+
)
|
|
457
|
+
const alpha = PcbAssemblyGltfModelMeshParser.#clampUnit(color[3], 1)
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
color: alpha < 1 ? [...rgb, alpha] : rgb
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Clamps a numeric channel into the unit interval.
|
|
466
|
+
* @param {unknown} value Candidate number.
|
|
467
|
+
* @param {number} [fallback] Fallback value.
|
|
468
|
+
* @returns {number}
|
|
469
|
+
*/
|
|
470
|
+
static #clampUnit(value, fallback = 0) {
|
|
471
|
+
const number = Number(value)
|
|
472
|
+
return Number.isFinite(number)
|
|
473
|
+
? Math.min(Math.max(number, 0), 1)
|
|
474
|
+
: fallback
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Resolves a stable primitive mesh name.
|
|
479
|
+
* @param {object | undefined} mesh GLTF mesh.
|
|
480
|
+
* @param {object | null} node GLTF node.
|
|
481
|
+
* @param {number} meshIndex Mesh index.
|
|
482
|
+
* @param {number} primitiveIndex Primitive index.
|
|
483
|
+
* @returns {string}
|
|
484
|
+
*/
|
|
485
|
+
static #primitiveName(mesh, node, meshIndex, primitiveIndex) {
|
|
486
|
+
return String(
|
|
487
|
+
node?.name ||
|
|
488
|
+
mesh?.name ||
|
|
489
|
+
'gltf-mesh-' + (meshIndex + 1) + '-' + (primitiveIndex + 1)
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Resolves all declared buffers.
|
|
495
|
+
* @param {object} gltf GLTF JSON.
|
|
496
|
+
* @param {object} model Model metadata.
|
|
497
|
+
* @param {Uint8Array | null} binaryBuffer GLB binary chunk.
|
|
498
|
+
* @returns {Promise<Uint8Array[]>}
|
|
499
|
+
*/
|
|
500
|
+
static async #resolveBuffers(gltf, model, binaryBuffer) {
|
|
501
|
+
const buffers = []
|
|
502
|
+
for (const buffer of PcbAssemblyGltfModelMeshParser.#array(
|
|
503
|
+
gltf?.buffers
|
|
504
|
+
)) {
|
|
505
|
+
buffers.push(
|
|
506
|
+
await PcbAssemblyGltfModelMeshParser.#resolveBuffer(
|
|
507
|
+
buffer,
|
|
508
|
+
model,
|
|
509
|
+
binaryBuffer
|
|
510
|
+
)
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
return buffers
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Resolves one GLTF buffer.
|
|
518
|
+
* @param {object} buffer GLTF buffer.
|
|
519
|
+
* @param {object} model Model metadata.
|
|
520
|
+
* @param {Uint8Array | null} binaryBuffer GLB binary chunk.
|
|
521
|
+
* @returns {Promise<Uint8Array>}
|
|
522
|
+
*/
|
|
523
|
+
static async #resolveBuffer(buffer, model, binaryBuffer) {
|
|
524
|
+
const uri = String(buffer?.uri || '')
|
|
525
|
+
if (!uri && binaryBuffer) {
|
|
526
|
+
return binaryBuffer.slice(0, Number(buffer?.byteLength || 0))
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (uri.startsWith('data:')) {
|
|
530
|
+
return PcbAssemblyGltfModelMeshParser.#dataUriBytes(uri)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (uri) {
|
|
534
|
+
const external = await PcbAssemblyGltfModelMeshParser.#readResource(
|
|
535
|
+
uri,
|
|
536
|
+
model
|
|
537
|
+
)
|
|
538
|
+
if (external) {
|
|
539
|
+
return external
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
throw new Error('GLTF buffer content is not available.')
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Reads a resource that matches an external buffer URI.
|
|
548
|
+
* @param {string} uri Buffer URI.
|
|
549
|
+
* @param {object} model Model metadata.
|
|
550
|
+
* @returns {Promise<Uint8Array | null>}
|
|
551
|
+
*/
|
|
552
|
+
static async #readResource(uri, model) {
|
|
553
|
+
const normalizedUri = PcbAssemblyGltfModelMeshParser.#normalizePath(uri)
|
|
554
|
+
const uriName = normalizedUri.split('/').pop()
|
|
555
|
+
|
|
556
|
+
for (const resource of PcbAssemblyGltfModelMeshParser.#resources(
|
|
557
|
+
model
|
|
558
|
+
)) {
|
|
559
|
+
const names =
|
|
560
|
+
PcbAssemblyGltfModelMeshParser.#resourceNames(resource)
|
|
561
|
+
if (
|
|
562
|
+
names.some(
|
|
563
|
+
(name) =>
|
|
564
|
+
name === normalizedUri ||
|
|
565
|
+
name.endsWith('/' + normalizedUri) ||
|
|
566
|
+
name.split('/').pop() === uriName
|
|
567
|
+
)
|
|
568
|
+
) {
|
|
569
|
+
const bytes =
|
|
570
|
+
await PcbAssemblyGltfModelMeshParser.#readAnyBytes(resource)
|
|
571
|
+
if (bytes) {
|
|
572
|
+
return bytes
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return null
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Collects sidecar resource candidates from model metadata.
|
|
582
|
+
* @param {object} model Model metadata.
|
|
583
|
+
* @returns {object[]}
|
|
584
|
+
*/
|
|
585
|
+
static #resources(model) {
|
|
586
|
+
const resources = []
|
|
587
|
+
;[
|
|
588
|
+
model?.resources,
|
|
589
|
+
model?.relatedFiles,
|
|
590
|
+
model?.assets,
|
|
591
|
+
model?.files,
|
|
592
|
+
model?.externalBuffers,
|
|
593
|
+
model?.bufferFiles
|
|
594
|
+
].forEach((value) => {
|
|
595
|
+
if (Array.isArray(value)) {
|
|
596
|
+
resources.push(...value)
|
|
597
|
+
} else if (value && typeof value === 'object') {
|
|
598
|
+
Object.entries(value).forEach(([name, entry]) => {
|
|
599
|
+
resources.push({ name, ...(entry || {}) })
|
|
600
|
+
})
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
return resources
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Returns normalized resource names used for URI matching.
|
|
608
|
+
* @param {object} resource Resource metadata.
|
|
609
|
+
* @returns {string[]}
|
|
610
|
+
*/
|
|
611
|
+
static #resourceNames(resource) {
|
|
612
|
+
return [
|
|
613
|
+
resource?.uri,
|
|
614
|
+
resource?.relativePath,
|
|
615
|
+
resource?.name,
|
|
616
|
+
resource?.file?.name,
|
|
617
|
+
resource?.sourceUrl
|
|
618
|
+
]
|
|
619
|
+
.map((value) =>
|
|
620
|
+
PcbAssemblyGltfModelMeshParser.#normalizePath(value)
|
|
621
|
+
)
|
|
622
|
+
.filter(Boolean)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Builds node indexes from the active scene.
|
|
627
|
+
* @param {object} gltf GLTF JSON.
|
|
628
|
+
* @returns {number[]}
|
|
629
|
+
*/
|
|
630
|
+
static #sceneNodeIndexes(gltf) {
|
|
631
|
+
const scenes = PcbAssemblyGltfModelMeshParser.#array(gltf?.scenes)
|
|
632
|
+
const sceneIndex = Number.isInteger(gltf?.scene) ? gltf.scene : 0
|
|
633
|
+
const scene = scenes[sceneIndex] || scenes[0]
|
|
634
|
+
if (Array.isArray(scene?.nodes)) {
|
|
635
|
+
return scene.nodes
|
|
636
|
+
.map((index) => Number(index))
|
|
637
|
+
.filter(Number.isInteger)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return PcbAssemblyGltfModelMeshParser.#array(gltf?.nodes)
|
|
641
|
+
.map((node, index) => (Number.isInteger(node?.mesh) ? index : -1))
|
|
642
|
+
.filter((index) => index >= 0)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Builds a node transform matrix.
|
|
647
|
+
* @param {object} node GLTF node.
|
|
648
|
+
* @returns {number[]}
|
|
649
|
+
*/
|
|
650
|
+
static #nodeMatrix(node) {
|
|
651
|
+
if (Array.isArray(node?.matrix) && node.matrix.length >= 16) {
|
|
652
|
+
return node.matrix.slice(0, 16).map((value) => Number(value || 0))
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return PcbAssemblyGltfModelMeshParser.#trsMatrix(
|
|
656
|
+
node?.translation,
|
|
657
|
+
node?.rotation,
|
|
658
|
+
node?.scale
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Builds a transform matrix from translation, rotation, and scale.
|
|
664
|
+
* @param {number[] | undefined} translation Node translation.
|
|
665
|
+
* @param {number[] | undefined} rotation Node quaternion.
|
|
666
|
+
* @param {number[] | undefined} scale Node scale.
|
|
667
|
+
* @returns {number[]}
|
|
668
|
+
*/
|
|
669
|
+
static #trsMatrix(translation, rotation, scale) {
|
|
670
|
+
const t = [0, 1, 2].map((index) => Number(translation?.[index] || 0))
|
|
671
|
+
const s = [0, 1, 2].map((index) => Number(scale?.[index] ?? 1) || 1)
|
|
672
|
+
const q = [
|
|
673
|
+
Number(rotation?.[0] || 0),
|
|
674
|
+
Number(rotation?.[1] || 0),
|
|
675
|
+
Number(rotation?.[2] || 0),
|
|
676
|
+
Number(rotation?.[3] ?? 1) || 1
|
|
677
|
+
]
|
|
678
|
+
const x2 = q[0] + q[0]
|
|
679
|
+
const y2 = q[1] + q[1]
|
|
680
|
+
const z2 = q[2] + q[2]
|
|
681
|
+
const xx = q[0] * x2
|
|
682
|
+
const xy = q[0] * y2
|
|
683
|
+
const xz = q[0] * z2
|
|
684
|
+
const yy = q[1] * y2
|
|
685
|
+
const yz = q[1] * z2
|
|
686
|
+
const zz = q[2] * z2
|
|
687
|
+
const wx = q[3] * x2
|
|
688
|
+
const wy = q[3] * y2
|
|
689
|
+
const wz = q[3] * z2
|
|
690
|
+
|
|
691
|
+
return [
|
|
692
|
+
(1 - (yy + zz)) * s[0],
|
|
693
|
+
(xy + wz) * s[0],
|
|
694
|
+
(xz - wy) * s[0],
|
|
695
|
+
0,
|
|
696
|
+
(xy - wz) * s[1],
|
|
697
|
+
(1 - (xx + zz)) * s[1],
|
|
698
|
+
(yz + wx) * s[1],
|
|
699
|
+
0,
|
|
700
|
+
(xz + wy) * s[2],
|
|
701
|
+
(yz - wx) * s[2],
|
|
702
|
+
(1 - (xx + yy)) * s[2],
|
|
703
|
+
0,
|
|
704
|
+
t[0],
|
|
705
|
+
t[1],
|
|
706
|
+
t[2],
|
|
707
|
+
1
|
|
708
|
+
]
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Returns an identity matrix.
|
|
713
|
+
* @returns {number[]}
|
|
714
|
+
*/
|
|
715
|
+
static #identityMatrix() {
|
|
716
|
+
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Multiplies two column-major matrices.
|
|
721
|
+
* @param {number[]} left Left matrix.
|
|
722
|
+
* @param {number[]} right Right matrix.
|
|
723
|
+
* @returns {number[]}
|
|
724
|
+
*/
|
|
725
|
+
static #multiplyMatrices(left, right) {
|
|
726
|
+
const result = new Array(16).fill(0)
|
|
727
|
+
for (let column = 0; column < 4; column += 1) {
|
|
728
|
+
for (let row = 0; row < 4; row += 1) {
|
|
729
|
+
for (let index = 0; index < 4; index += 1) {
|
|
730
|
+
result[column * 4 + row] +=
|
|
731
|
+
left[index * 4 + row] * right[column * 4 + index]
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return result
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Transforms one point by a column-major matrix.
|
|
740
|
+
* @param {number[]} matrix Transform matrix.
|
|
741
|
+
* @param {number[]} point Source point.
|
|
742
|
+
* @returns {number[]}
|
|
743
|
+
*/
|
|
744
|
+
static #transformPoint(matrix, point) {
|
|
745
|
+
const x = Number(point?.[0] || 0)
|
|
746
|
+
const y = Number(point?.[1] || 0)
|
|
747
|
+
const z = Number(point?.[2] || 0)
|
|
748
|
+
return [
|
|
749
|
+
matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
|
|
750
|
+
matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
|
|
751
|
+
matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]
|
|
752
|
+
]
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Converts one point from millimeters into internal mils.
|
|
757
|
+
* @param {number[]} point Point in millimeters.
|
|
758
|
+
* @returns {number[]}
|
|
759
|
+
*/
|
|
760
|
+
static #pointMmToMil(point) {
|
|
761
|
+
return [0, 1, 2].map((index) => Number(point?.[index] || 0) * MM_TO_MIL)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Decodes a data URI into bytes.
|
|
766
|
+
* @param {string} uri Data URI.
|
|
767
|
+
* @returns {Uint8Array}
|
|
768
|
+
*/
|
|
769
|
+
static #dataUriBytes(uri) {
|
|
770
|
+
const commaIndex = uri.indexOf(',')
|
|
771
|
+
if (commaIndex < 0) {
|
|
772
|
+
return new Uint8Array()
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const metadata = uri.slice(0, commaIndex)
|
|
776
|
+
const payload = uri.slice(commaIndex + 1)
|
|
777
|
+
if (metadata.includes(';base64')) {
|
|
778
|
+
return PcbAssemblyGltfModelMeshParser.#base64Bytes(payload)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return new TextEncoder().encode(decodeURIComponent(payload))
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Decodes base64 in Node and browser runtimes.
|
|
786
|
+
* @param {string} value Base64 value.
|
|
787
|
+
* @returns {Uint8Array}
|
|
788
|
+
*/
|
|
789
|
+
static #base64Bytes(value) {
|
|
790
|
+
if (typeof Buffer !== 'undefined') {
|
|
791
|
+
return new Uint8Array(Buffer.from(value, 'base64'))
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const binary = atob(value)
|
|
795
|
+
const bytes = new Uint8Array(binary.length)
|
|
796
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
797
|
+
bytes[index] = binary.charCodeAt(index)
|
|
798
|
+
}
|
|
799
|
+
return bytes
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Reads model metadata as UTF-8 text.
|
|
804
|
+
* @param {{ payloadText?: string, file?: any }} model Model metadata.
|
|
805
|
+
* @param {string} label Format label.
|
|
806
|
+
* @returns {Promise<string>}
|
|
807
|
+
*/
|
|
808
|
+
static async #readModelText(model, label) {
|
|
809
|
+
if (typeof model?.payloadText === 'string') {
|
|
810
|
+
return model.payloadText
|
|
811
|
+
}
|
|
812
|
+
if (typeof model?.file?.text === 'function') {
|
|
813
|
+
return await model.file.text()
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return new TextDecoder().decode(
|
|
817
|
+
await PcbAssemblyGltfModelMeshParser.#readModelBytes(model, label)
|
|
818
|
+
)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Reads model metadata as bytes.
|
|
823
|
+
* @param {{ payloadText?: string, payloadBytes?: any, bytes?: any, file?: any }} model Model metadata.
|
|
824
|
+
* @param {string} label Format label.
|
|
825
|
+
* @returns {Promise<Uint8Array>}
|
|
826
|
+
*/
|
|
827
|
+
static async #readModelBytes(model, label) {
|
|
828
|
+
const bytes = await PcbAssemblyGltfModelMeshParser.#readAnyBytes(model)
|
|
829
|
+
if (bytes) {
|
|
830
|
+
return bytes
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
throw new Error(label + ' model content is not available.')
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Reads byte-like values from common metadata shapes.
|
|
838
|
+
* @param {{ payloadText?: string, payloadBytes?: any, bytes?: any, data?: any, file?: any }} source Source metadata.
|
|
839
|
+
* @returns {Promise<Uint8Array | null>}
|
|
840
|
+
*/
|
|
841
|
+
static async #readAnyBytes(source) {
|
|
842
|
+
if (typeof source?.payloadText === 'string') {
|
|
843
|
+
return new TextEncoder().encode(source.payloadText)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
for (const value of [
|
|
847
|
+
source?.payloadBytes,
|
|
848
|
+
source?.bytes,
|
|
849
|
+
source?.data,
|
|
850
|
+
source?.file,
|
|
851
|
+
source
|
|
852
|
+
]) {
|
|
853
|
+
const bytes =
|
|
854
|
+
await PcbAssemblyGltfModelMeshParser.#bytesFromValue(value)
|
|
855
|
+
if (bytes) {
|
|
856
|
+
return bytes
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return null
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Converts one byte-like value to a Uint8Array.
|
|
865
|
+
* @param {any} value Candidate value.
|
|
866
|
+
* @returns {Promise<Uint8Array | null>}
|
|
867
|
+
*/
|
|
868
|
+
static async #bytesFromValue(value) {
|
|
869
|
+
if (!value || typeof value === 'string') {
|
|
870
|
+
return null
|
|
871
|
+
}
|
|
872
|
+
if (value instanceof Uint8Array) {
|
|
873
|
+
return value
|
|
874
|
+
}
|
|
875
|
+
if (value instanceof ArrayBuffer) {
|
|
876
|
+
return new Uint8Array(value)
|
|
877
|
+
}
|
|
878
|
+
if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
|
|
879
|
+
return new Uint8Array(
|
|
880
|
+
value.buffer,
|
|
881
|
+
value.byteOffset,
|
|
882
|
+
value.byteLength
|
|
883
|
+
)
|
|
884
|
+
}
|
|
885
|
+
if (typeof value.arrayBuffer === 'function') {
|
|
886
|
+
return new Uint8Array(await value.arrayBuffer())
|
|
887
|
+
}
|
|
888
|
+
return null
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Normalizes a path-like value for matching.
|
|
893
|
+
* @param {unknown} value Path candidate.
|
|
894
|
+
* @returns {string}
|
|
895
|
+
*/
|
|
896
|
+
static #normalizePath(value) {
|
|
897
|
+
return String(value || '')
|
|
898
|
+
.split(/[?#]/u)[0]
|
|
899
|
+
.replace(/\\/gu, '/')
|
|
900
|
+
.replace(/^\/+/u, '')
|
|
901
|
+
.toLowerCase()
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Normalizes a value to an array.
|
|
906
|
+
* @param {unknown} value Candidate value.
|
|
907
|
+
* @returns {any[]}
|
|
908
|
+
*/
|
|
909
|
+
static #array(value) {
|
|
910
|
+
return Array.isArray(value) ? value : []
|
|
911
|
+
}
|
|
912
|
+
}
|