pcb-scene3d-viewer 1.1.19 → 1.1.21
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/package.json +3 -2
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +467 -0
- package/src/PcbAssemblyExportCoordinateFrame.mjs +19 -0
- package/src/PcbAssemblyGeometryBuildProgress.mjs +201 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +994 -0
- package/src/PcbAssemblyMeshUtils.mjs +541 -0
- package/src/PcbAssemblyModelMeshLoader.mjs +447 -0
- package/src/PcbAssemblyPolygonTriangulator.mjs +301 -0
- package/src/PcbAssemblyStepWriter.mjs +952 -0
- package/src/PcbAssemblyWrlWriter.mjs +144 -0
- package/src/PcbScene3dCameraRig.mjs +11 -5
- package/src/PcbScene3dComponentVisibility.mjs +161 -0
- package/src/PcbScene3dController.mjs +29 -4
- package/src/PcbScene3dExternalModelPlacementRepair.mjs +1 -1
- package/src/PcbScene3dInteractionHints.mjs +14 -6
- package/src/PcbScene3dRenderScheduler.mjs +55 -0
- package/src/PcbScene3dRuntime.mjs +56 -4
- package/src/PcbScene3dSelectionInspectorRenderer.mjs +64 -3
- package/src/PcbScene3dSelectionVisibilityBinder.mjs +119 -0
- package/src/PcbScene3dText.mjs +4 -2
- package/src/scene3d.mjs +12 -0
- package/src/styles/scene3d.css +48 -0
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
import { PcbAssemblyExportCoordinateFrame } from './PcbAssemblyExportCoordinateFrame.mjs'
|
|
2
|
+
import { PcbAssemblyMeshUtils } from './PcbAssemblyMeshUtils.mjs'
|
|
3
|
+
import { PcbAssemblyPolygonTriangulator } from './PcbAssemblyPolygonTriangulator.mjs'
|
|
4
|
+
|
|
5
|
+
const TESSELLATED_MESH_THRESHOLD = 512
|
|
6
|
+
const TESSELLATED_FACE_THRESHOLD = 5000
|
|
7
|
+
const TESSELLATED_VERTEX_THRESHOLD = 20000
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Writes PCB assembly meshes as an ISO 10303 STEP file.
|
|
11
|
+
*/
|
|
12
|
+
export class PcbAssemblyStepWriter {
|
|
13
|
+
/**
|
|
14
|
+
* Writes meshes to STEP text.
|
|
15
|
+
* @param {{ name?: string, meshes?: object[] }} assembly Assembly data.
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
static write(assembly = {}) {
|
|
19
|
+
const writer = new PcbAssemblyStepWriter(assembly?.name || 'assembly')
|
|
20
|
+
return writer.#write(PcbAssemblyStepWriter.#array(assembly.meshes))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @type {string} */
|
|
24
|
+
#name
|
|
25
|
+
|
|
26
|
+
/** @type {string[]} */
|
|
27
|
+
#entities
|
|
28
|
+
|
|
29
|
+
/** @type {number} */
|
|
30
|
+
#nextId
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} name Assembly name.
|
|
34
|
+
*/
|
|
35
|
+
constructor(name) {
|
|
36
|
+
this.#name = String(name || 'assembly')
|
|
37
|
+
this.#entities = []
|
|
38
|
+
this.#nextId = 1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Writes the full STEP file.
|
|
43
|
+
* @param {object[]} meshes Assembly meshes.
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
#write(meshes) {
|
|
47
|
+
const tessellated = PcbAssemblyStepWriter.#shouldUseTessellated(meshes)
|
|
48
|
+
const contextIds = this.#writeContext(tessellated)
|
|
49
|
+
const representationId = tessellated
|
|
50
|
+
? this.#writeTessellatedRepresentation(meshes, contextIds)
|
|
51
|
+
: this.#writeBrepRepresentation(meshes, contextIds)
|
|
52
|
+
const shapeId = this.#add(
|
|
53
|
+
"PRODUCT_DEFINITION_SHAPE('','',#" +
|
|
54
|
+
contextIds.productDefinitionId +
|
|
55
|
+
')'
|
|
56
|
+
)
|
|
57
|
+
this.#add(
|
|
58
|
+
'SHAPE_DEFINITION_REPRESENTATION(#' +
|
|
59
|
+
shapeId +
|
|
60
|
+
',#' +
|
|
61
|
+
representationId +
|
|
62
|
+
')'
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
PcbAssemblyStepWriter.#header(this.#name, tessellated) +
|
|
67
|
+
this.#entities.map((entity) => entity + ';').join('\n') +
|
|
68
|
+
'\nENDSEC;\nEND-ISO-10303-21;\n'
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Writes an advanced B-rep shape representation.
|
|
74
|
+
* @param {object[]} meshes Assembly meshes.
|
|
75
|
+
* @param {{ axisId: number, representationContextId: number }} contextIds STEP context ids.
|
|
76
|
+
* @returns {number}
|
|
77
|
+
*/
|
|
78
|
+
#writeBrepRepresentation(meshes, contextIds) {
|
|
79
|
+
const solidIds = meshes
|
|
80
|
+
.map((mesh) => this.#writeMesh(mesh))
|
|
81
|
+
.filter((id) => id > 0)
|
|
82
|
+
const representationType = solidIds.length
|
|
83
|
+
? 'ADVANCED_BREP_SHAPE_REPRESENTATION'
|
|
84
|
+
: 'SHAPE_REPRESENTATION'
|
|
85
|
+
const representationId = this.#add(
|
|
86
|
+
representationType +
|
|
87
|
+
"('" +
|
|
88
|
+
PcbAssemblyStepWriter.#escape(this.#name) +
|
|
89
|
+
"',(" +
|
|
90
|
+
PcbAssemblyStepWriter.#referenceList([
|
|
91
|
+
contextIds.axisId,
|
|
92
|
+
...solidIds
|
|
93
|
+
]) +
|
|
94
|
+
'),#' +
|
|
95
|
+
contextIds.representationContextId +
|
|
96
|
+
')'
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return representationId
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Writes an AP242 tessellated shape representation for dense meshes.
|
|
104
|
+
* @param {object[]} meshes Assembly meshes.
|
|
105
|
+
* @param {{ axisId: number, representationContextId: number }} contextIds STEP context ids.
|
|
106
|
+
* @returns {number}
|
|
107
|
+
*/
|
|
108
|
+
#writeTessellatedRepresentation(meshes, contextIds) {
|
|
109
|
+
const surfaceIds = meshes
|
|
110
|
+
.map((mesh) => this.#writeTessellatedSurfaceSet(mesh))
|
|
111
|
+
.filter((id) => id > 0)
|
|
112
|
+
const representationType = surfaceIds.length
|
|
113
|
+
? 'TESSELLATED_SHAPE_REPRESENTATION'
|
|
114
|
+
: 'SHAPE_REPRESENTATION'
|
|
115
|
+
|
|
116
|
+
return this.#add(
|
|
117
|
+
representationType +
|
|
118
|
+
"('" +
|
|
119
|
+
PcbAssemblyStepWriter.#escape(this.#name) +
|
|
120
|
+
"',(" +
|
|
121
|
+
PcbAssemblyStepWriter.#referenceList([
|
|
122
|
+
contextIds.axisId,
|
|
123
|
+
...surfaceIds
|
|
124
|
+
]) +
|
|
125
|
+
'),#' +
|
|
126
|
+
contextIds.representationContextId +
|
|
127
|
+
')'
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Writes product and geometric context entities.
|
|
133
|
+
* @param {boolean} tessellated Whether AP242 tessellated context should be emitted.
|
|
134
|
+
* @returns {{ axisId: number, representationContextId: number, productDefinitionId: number }}
|
|
135
|
+
*/
|
|
136
|
+
#writeContext(tessellated) {
|
|
137
|
+
const appContext = tessellated
|
|
138
|
+
? 'managed model based 3d engineering'
|
|
139
|
+
: 'automotive_design'
|
|
140
|
+
const protocol = tessellated
|
|
141
|
+
? 'ap242_managed_model_based_3d_engineering'
|
|
142
|
+
: 'automotive_design'
|
|
143
|
+
const protocolYear = tessellated ? '2014' : '2000'
|
|
144
|
+
const appContextId = this.#add(
|
|
145
|
+
"APPLICATION_CONTEXT('" + appContext + "')"
|
|
146
|
+
)
|
|
147
|
+
this.#add(
|
|
148
|
+
"APPLICATION_PROTOCOL_DEFINITION('international standard','" +
|
|
149
|
+
protocol +
|
|
150
|
+
"'," +
|
|
151
|
+
protocolYear +
|
|
152
|
+
',#' +
|
|
153
|
+
appContextId +
|
|
154
|
+
')'
|
|
155
|
+
)
|
|
156
|
+
const productContextId = this.#add(
|
|
157
|
+
"PRODUCT_CONTEXT('',#" + appContextId + ",'mechanical')"
|
|
158
|
+
)
|
|
159
|
+
const productId = this.#add(
|
|
160
|
+
"PRODUCT('" +
|
|
161
|
+
PcbAssemblyStepWriter.#escape(this.#name) +
|
|
162
|
+
"','" +
|
|
163
|
+
PcbAssemblyStepWriter.#escape(this.#name) +
|
|
164
|
+
"','',(#" +
|
|
165
|
+
productContextId +
|
|
166
|
+
'))'
|
|
167
|
+
)
|
|
168
|
+
const formationId = this.#add(
|
|
169
|
+
"PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE('','',#" +
|
|
170
|
+
productId +
|
|
171
|
+
',.NOT_KNOWN.)'
|
|
172
|
+
)
|
|
173
|
+
const definitionContextId = this.#add(
|
|
174
|
+
"PRODUCT_DEFINITION_CONTEXT('part definition',#" +
|
|
175
|
+
appContextId +
|
|
176
|
+
",'design')"
|
|
177
|
+
)
|
|
178
|
+
const productDefinitionId = this.#add(
|
|
179
|
+
"PRODUCT_DEFINITION('design','',#" +
|
|
180
|
+
formationId +
|
|
181
|
+
',#' +
|
|
182
|
+
definitionContextId +
|
|
183
|
+
')'
|
|
184
|
+
)
|
|
185
|
+
const originId = this.#point([0, 0, 0])
|
|
186
|
+
const zDirectionId = this.#direction([0, 0, 1])
|
|
187
|
+
const xDirectionId = this.#direction([1, 0, 0])
|
|
188
|
+
const axisId = this.#add(
|
|
189
|
+
"AXIS2_PLACEMENT_3D('',#" +
|
|
190
|
+
originId +
|
|
191
|
+
',#' +
|
|
192
|
+
zDirectionId +
|
|
193
|
+
',#' +
|
|
194
|
+
xDirectionId +
|
|
195
|
+
')'
|
|
196
|
+
)
|
|
197
|
+
const lengthUnitId = this.#add(
|
|
198
|
+
'(LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.))'
|
|
199
|
+
)
|
|
200
|
+
const angleUnitId = this.#add(
|
|
201
|
+
'(NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.))'
|
|
202
|
+
)
|
|
203
|
+
const solidAngleUnitId = this.#add(
|
|
204
|
+
'(NAMED_UNIT(*) SOLID_ANGLE_UNIT() SI_UNIT($,.STERADIAN.))'
|
|
205
|
+
)
|
|
206
|
+
const uncertaintyId = this.#add(
|
|
207
|
+
'UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-06),#' +
|
|
208
|
+
lengthUnitId +
|
|
209
|
+
",'distance_accuracy_value','')"
|
|
210
|
+
)
|
|
211
|
+
const representationContextId = this.#add(
|
|
212
|
+
'GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#' +
|
|
213
|
+
uncertaintyId +
|
|
214
|
+
')) GLOBAL_UNIT_ASSIGNED_CONTEXT((#' +
|
|
215
|
+
lengthUnitId +
|
|
216
|
+
',#' +
|
|
217
|
+
angleUnitId +
|
|
218
|
+
',#' +
|
|
219
|
+
solidAngleUnitId +
|
|
220
|
+
")) REPRESENTATION_CONTEXT('','')"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return { axisId, representationContextId, productDefinitionId }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Writes one mesh as a surface-backed manifold solid B-rep.
|
|
228
|
+
* @param {{ name?: string, vertices?: number[][], faces?: number[][] }} mesh Mesh data.
|
|
229
|
+
* @returns {number}
|
|
230
|
+
*/
|
|
231
|
+
#writeMesh(mesh) {
|
|
232
|
+
const vertices = PcbAssemblyStepWriter.#array(mesh?.vertices)
|
|
233
|
+
const faces = PcbAssemblyStepWriter.#array(mesh?.faces)
|
|
234
|
+
const context = this.#createMeshContext(vertices)
|
|
235
|
+
const faceIds = faces
|
|
236
|
+
.map((face) => this.#writeAdvancedFace(face, context))
|
|
237
|
+
.filter((id) => id > 0)
|
|
238
|
+
|
|
239
|
+
if (!faceIds.length) {
|
|
240
|
+
return 0
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const shellId = this.#add(
|
|
244
|
+
"CLOSED_SHELL('',(" +
|
|
245
|
+
PcbAssemblyStepWriter.#referenceList(faceIds) +
|
|
246
|
+
'))'
|
|
247
|
+
)
|
|
248
|
+
const solidId = this.#add(
|
|
249
|
+
"MANIFOLD_SOLID_BREP('" +
|
|
250
|
+
PcbAssemblyStepWriter.#escape(
|
|
251
|
+
PcbAssemblyMeshUtils.safeName(mesh?.name || 'mesh')
|
|
252
|
+
) +
|
|
253
|
+
"',#" +
|
|
254
|
+
shellId +
|
|
255
|
+
')'
|
|
256
|
+
)
|
|
257
|
+
this.#writePresentationStyle(solidId, mesh?.color)
|
|
258
|
+
return solidId
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Writes one mesh as an AP242 triangulated surface set.
|
|
263
|
+
* @param {{ name?: string, vertices?: number[][], faces?: number[][] }} mesh Mesh data.
|
|
264
|
+
* @returns {number}
|
|
265
|
+
*/
|
|
266
|
+
#writeTessellatedSurfaceSet(mesh) {
|
|
267
|
+
const tessellation = PcbAssemblyStepWriter.#tessellateMesh(mesh)
|
|
268
|
+
|
|
269
|
+
if (!tessellation.triangles.length) {
|
|
270
|
+
return 0
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const coordinatesId = this.#add(
|
|
274
|
+
"COORDINATES_LIST(''," +
|
|
275
|
+
tessellation.vertices.length +
|
|
276
|
+
',(' +
|
|
277
|
+
PcbAssemblyStepWriter.#coordinateList(tessellation.vertices) +
|
|
278
|
+
'))'
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
const surfaceSetId = this.#add(
|
|
282
|
+
"TRIANGULATED_SURFACE_SET('" +
|
|
283
|
+
PcbAssemblyStepWriter.#escape(
|
|
284
|
+
PcbAssemblyMeshUtils.safeName(mesh?.name || 'mesh')
|
|
285
|
+
) +
|
|
286
|
+
"',#" +
|
|
287
|
+
coordinatesId +
|
|
288
|
+
',' +
|
|
289
|
+
tessellation.vertices.length +
|
|
290
|
+
',(),(),(' +
|
|
291
|
+
PcbAssemblyStepWriter.#triangleList(tessellation.triangles) +
|
|
292
|
+
'))'
|
|
293
|
+
)
|
|
294
|
+
this.#writePresentationStyle(surfaceSetId, mesh?.color)
|
|
295
|
+
return surfaceSetId
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Writes a STEP presentation style for one shape item.
|
|
300
|
+
* @param {number} itemId Styled representation item id.
|
|
301
|
+
* @param {number[] | undefined} color RGB color.
|
|
302
|
+
* @returns {void}
|
|
303
|
+
*/
|
|
304
|
+
#writePresentationStyle(itemId, color) {
|
|
305
|
+
const rgb = PcbAssemblyStepWriter.#colorComponents(color)
|
|
306
|
+
const colorId = this.#add(
|
|
307
|
+
"COLOUR_RGB(''," +
|
|
308
|
+
rgb.map((value) => PcbAssemblyStepWriter.#formatNumber(value)) +
|
|
309
|
+
')'
|
|
310
|
+
)
|
|
311
|
+
const fillColorId = this.#add(
|
|
312
|
+
"FILL_AREA_STYLE_COLOUR('',#" + colorId + ')'
|
|
313
|
+
)
|
|
314
|
+
const fillStyleId = this.#add(
|
|
315
|
+
"FILL_AREA_STYLE('',(#" + fillColorId + '))'
|
|
316
|
+
)
|
|
317
|
+
const surfaceFillId = this.#add(
|
|
318
|
+
'SURFACE_STYLE_FILL_AREA(#' + fillStyleId + ')'
|
|
319
|
+
)
|
|
320
|
+
const sideStyleId = this.#add(
|
|
321
|
+
"SURFACE_SIDE_STYLE('',(#" + surfaceFillId + '))'
|
|
322
|
+
)
|
|
323
|
+
const surfaceStyleId = this.#add(
|
|
324
|
+
'SURFACE_STYLE_USAGE(.BOTH.,#' + sideStyleId + ')'
|
|
325
|
+
)
|
|
326
|
+
const assignmentId = this.#add(
|
|
327
|
+
'PRESENTATION_STYLE_ASSIGNMENT((#' + surfaceStyleId + '))'
|
|
328
|
+
)
|
|
329
|
+
this.#add("STYLED_ITEM('',(#" + assignmentId + '),#' + itemId + ')')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Creates per-mesh STEP topology caches.
|
|
334
|
+
* @param {number[][]} vertices Mesh vertices in mils.
|
|
335
|
+
* @returns {{ vertices: number[][], vertexRefs: Map<string, { key: string, pointId: number, vertexId: number, scaled: number[] }>, edges: Map<string, { id: number, startKey: string, endKey: string }> }}
|
|
336
|
+
*/
|
|
337
|
+
#createMeshContext(vertices) {
|
|
338
|
+
return {
|
|
339
|
+
vertices,
|
|
340
|
+
vertexRefs: new Map(),
|
|
341
|
+
edges: new Map()
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Writes one polygon face as an advanced planar face.
|
|
347
|
+
* @param {number[]} face Face vertex indices.
|
|
348
|
+
* @param {{ vertices: number[][], vertexRefs: Map<string, { key: string, pointId: number, vertexId: number, scaled: number[] }>, edges: Map<string, { id: number, startKey: string, endKey: string }> }} context Mesh write context.
|
|
349
|
+
* @returns {number}
|
|
350
|
+
*/
|
|
351
|
+
#writeAdvancedFace(face, context) {
|
|
352
|
+
const indexes = PcbAssemblyStepWriter.#normalizeFaceIndexes(
|
|
353
|
+
face,
|
|
354
|
+
context.vertices
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if (indexes.length < 3) {
|
|
358
|
+
return 0
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const frame = PcbAssemblyStepWriter.#faceFrame(
|
|
362
|
+
indexes,
|
|
363
|
+
context.vertices
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
if (!frame) {
|
|
367
|
+
return 0
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const orientedEdgeIds = []
|
|
371
|
+
for (let index = 0; index < indexes.length; index += 1) {
|
|
372
|
+
const startIndex = indexes[index]
|
|
373
|
+
const endIndex = indexes[(index + 1) % indexes.length]
|
|
374
|
+
const edge = this.#edge(context, startIndex, endIndex)
|
|
375
|
+
|
|
376
|
+
if (!edge) {
|
|
377
|
+
return 0
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
orientedEdgeIds.push(
|
|
381
|
+
this.#add(
|
|
382
|
+
"ORIENTED_EDGE('',*,*,#" +
|
|
383
|
+
edge.id +
|
|
384
|
+
',' +
|
|
385
|
+
PcbAssemblyStepWriter.#stepBoolean(edge.forward) +
|
|
386
|
+
')'
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const loopId = this.#add(
|
|
392
|
+
"EDGE_LOOP('',(" +
|
|
393
|
+
PcbAssemblyStepWriter.#referenceList(orientedEdgeIds) +
|
|
394
|
+
'))'
|
|
395
|
+
)
|
|
396
|
+
const boundId = this.#add("FACE_OUTER_BOUND('',#" + loopId + ',.T.)')
|
|
397
|
+
const normalDirectionId = this.#direction(frame.normal)
|
|
398
|
+
const xDirectionId = this.#direction(frame.xDirection)
|
|
399
|
+
const axisId = this.#add(
|
|
400
|
+
"AXIS2_PLACEMENT_3D('',#" +
|
|
401
|
+
this.#vertexRef(context, indexes[0]).pointId +
|
|
402
|
+
',#' +
|
|
403
|
+
normalDirectionId +
|
|
404
|
+
',#' +
|
|
405
|
+
xDirectionId +
|
|
406
|
+
')'
|
|
407
|
+
)
|
|
408
|
+
const planeId = this.#add("PLANE('',#" + axisId + ')')
|
|
409
|
+
return this.#add(
|
|
410
|
+
"ADVANCED_FACE('',(#" + boundId + '),#' + planeId + ',.T.)'
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Writes or reuses one vertex reference.
|
|
416
|
+
* @param {{ vertices: number[][], vertexRefs: Map<string, { key: string, pointId: number, vertexId: number, scaled: number[] }> }} context Mesh write context.
|
|
417
|
+
* @param {number} index Mesh vertex index.
|
|
418
|
+
* @returns {{ key: string, pointId: number, vertexId: number, scaled: number[] }}
|
|
419
|
+
*/
|
|
420
|
+
#vertexRef(context, index) {
|
|
421
|
+
const vertex = context.vertices[index]
|
|
422
|
+
const key = PcbAssemblyStepWriter.#vertexKey(vertex)
|
|
423
|
+
const existing = context.vertexRefs.get(key)
|
|
424
|
+
|
|
425
|
+
if (existing) {
|
|
426
|
+
return existing
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const scaled = PcbAssemblyStepWriter.#scaledVertex(vertex)
|
|
430
|
+
const pointId = this.#point(vertex)
|
|
431
|
+
const vertexId = this.#add("VERTEX_POINT('',#" + pointId + ')')
|
|
432
|
+
const ref = { key, pointId, vertexId, scaled }
|
|
433
|
+
context.vertexRefs.set(key, ref)
|
|
434
|
+
return ref
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Writes or reuses one straight edge curve.
|
|
439
|
+
* @param {{ vertices: number[][], vertexRefs: Map<string, { key: string, pointId: number, vertexId: number, scaled: number[] }>, edges: Map<string, { id: number, startKey: string, endKey: string }> }} context Mesh write context.
|
|
440
|
+
* @param {number} startIndex Start vertex index.
|
|
441
|
+
* @param {number} endIndex End vertex index.
|
|
442
|
+
* @returns {{ id: number, forward: boolean } | null}
|
|
443
|
+
*/
|
|
444
|
+
#edge(context, startIndex, endIndex) {
|
|
445
|
+
const start = this.#vertexRef(context, startIndex)
|
|
446
|
+
const end = this.#vertexRef(context, endIndex)
|
|
447
|
+
|
|
448
|
+
if (start.key === end.key) {
|
|
449
|
+
return null
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const key =
|
|
453
|
+
start.key < end.key
|
|
454
|
+
? start.key + '|' + end.key
|
|
455
|
+
: end.key + '|' + start.key
|
|
456
|
+
const existing = context.edges.get(key)
|
|
457
|
+
|
|
458
|
+
if (existing) {
|
|
459
|
+
return {
|
|
460
|
+
id: existing.id,
|
|
461
|
+
forward:
|
|
462
|
+
existing.startKey === start.key &&
|
|
463
|
+
existing.endKey === end.key
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const lineId = this.#line(start.pointId, start.scaled, end.scaled)
|
|
468
|
+
const edgeId = this.#add(
|
|
469
|
+
"EDGE_CURVE('',#" +
|
|
470
|
+
start.vertexId +
|
|
471
|
+
',#' +
|
|
472
|
+
end.vertexId +
|
|
473
|
+
',#' +
|
|
474
|
+
lineId +
|
|
475
|
+
',.T.)'
|
|
476
|
+
)
|
|
477
|
+
context.edges.set(key, {
|
|
478
|
+
id: edgeId,
|
|
479
|
+
startKey: start.key,
|
|
480
|
+
endKey: end.key
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
return { id: edgeId, forward: true }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Writes one unbounded line curve for an edge.
|
|
488
|
+
* @param {number} startPointId STEP point id at the start of the line.
|
|
489
|
+
* @param {number[]} start Start point in millimetres.
|
|
490
|
+
* @param {number[]} end End point in millimetres.
|
|
491
|
+
* @returns {number}
|
|
492
|
+
*/
|
|
493
|
+
#line(startPointId, start, end) {
|
|
494
|
+
const delta = PcbAssemblyStepWriter.#subtract(end, start)
|
|
495
|
+
const length = PcbAssemblyStepWriter.#distance(delta)
|
|
496
|
+
const direction = PcbAssemblyStepWriter.#normalize(delta)
|
|
497
|
+
const directionId = this.#direction(direction)
|
|
498
|
+
const vectorId = this.#add(
|
|
499
|
+
"VECTOR('',#" +
|
|
500
|
+
directionId +
|
|
501
|
+
',' +
|
|
502
|
+
PcbAssemblyStepWriter.#formatNumber(length) +
|
|
503
|
+
')'
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
return this.#add("LINE('',#" + startPointId + ',#' + vectorId + ')')
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Writes one Cartesian point.
|
|
511
|
+
* @param {number[]} vertex Coordinates in mils.
|
|
512
|
+
* @returns {number}
|
|
513
|
+
*/
|
|
514
|
+
#point(vertex) {
|
|
515
|
+
const scaled = PcbAssemblyStepWriter.#scaledVertex(vertex)
|
|
516
|
+
|
|
517
|
+
return this.#add(
|
|
518
|
+
"CARTESIAN_POINT('',(" +
|
|
519
|
+
scaled
|
|
520
|
+
.map((value) => PcbAssemblyStepWriter.#formatNumber(value))
|
|
521
|
+
.join(',') +
|
|
522
|
+
'))'
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Writes one direction.
|
|
528
|
+
* @param {number[]} direction Direction components.
|
|
529
|
+
* @returns {number}
|
|
530
|
+
*/
|
|
531
|
+
#direction(direction) {
|
|
532
|
+
return this.#add(
|
|
533
|
+
"DIRECTION('',(" +
|
|
534
|
+
[
|
|
535
|
+
PcbAssemblyStepWriter.#formatNumber(direction[0] || 0),
|
|
536
|
+
PcbAssemblyStepWriter.#formatNumber(direction[1] || 0),
|
|
537
|
+
PcbAssemblyStepWriter.#formatNumber(direction[2] || 0)
|
|
538
|
+
].join(',') +
|
|
539
|
+
'))'
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Removes invalid, duplicate, and closing indices from one face.
|
|
545
|
+
* @param {unknown} face Face index list.
|
|
546
|
+
* @param {number[][]} vertices Mesh vertices.
|
|
547
|
+
* @returns {number[]}
|
|
548
|
+
*/
|
|
549
|
+
static #normalizeFaceIndexes(face, vertices) {
|
|
550
|
+
const indexes = []
|
|
551
|
+
|
|
552
|
+
for (const candidate of PcbAssemblyStepWriter.#array(face)) {
|
|
553
|
+
const index = Number(candidate)
|
|
554
|
+
const lastIndex = indexes[indexes.length - 1]
|
|
555
|
+
|
|
556
|
+
if (
|
|
557
|
+
Number.isInteger(index) &&
|
|
558
|
+
index >= 0 &&
|
|
559
|
+
index < vertices.length &&
|
|
560
|
+
index !== lastIndex &&
|
|
561
|
+
PcbAssemblyStepWriter.#isFiniteVertex(vertices[index])
|
|
562
|
+
) {
|
|
563
|
+
indexes.push(index)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (indexes.length > 2 && indexes[0] === indexes[indexes.length - 1]) {
|
|
568
|
+
indexes.pop()
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return indexes.length >= 3 ? indexes : []
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Converts mesh polygon faces into compact one-based triangle indexes.
|
|
576
|
+
* @param {{ vertices?: number[][], faces?: number[][] }} mesh Mesh data.
|
|
577
|
+
* @returns {{ vertices: number[][], triangles: number[][] }}
|
|
578
|
+
*/
|
|
579
|
+
static #tessellateMesh(mesh) {
|
|
580
|
+
const sourceVertices = PcbAssemblyStepWriter.#array(mesh?.vertices)
|
|
581
|
+
const coordinateIndexes = new Map()
|
|
582
|
+
const vertices = []
|
|
583
|
+
const triangles = []
|
|
584
|
+
const resolveIndex = (sourceIndex) => {
|
|
585
|
+
const existing = coordinateIndexes.get(sourceIndex)
|
|
586
|
+
|
|
587
|
+
if (existing) {
|
|
588
|
+
return existing
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const coordinateIndex = vertices.length + 1
|
|
592
|
+
coordinateIndexes.set(sourceIndex, coordinateIndex)
|
|
593
|
+
vertices.push(sourceVertices[sourceIndex])
|
|
594
|
+
return coordinateIndex
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
for (const face of PcbAssemblyStepWriter.#array(mesh?.faces)) {
|
|
598
|
+
const indexes = PcbAssemblyStepWriter.#normalizeFaceIndexes(
|
|
599
|
+
face,
|
|
600
|
+
sourceVertices
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
for (const triangleIndexes of PcbAssemblyPolygonTriangulator.triangulateFace(
|
|
604
|
+
indexes,
|
|
605
|
+
sourceVertices
|
|
606
|
+
)) {
|
|
607
|
+
const triangle = triangleIndexes.map(
|
|
608
|
+
(index) => sourceVertices[index]
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
if (!PcbAssemblyStepWriter.#triangleHasArea(triangle)) {
|
|
612
|
+
continue
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
triangles.push(triangleIndexes.map(resolveIndex))
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return { vertices, triangles }
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Returns true when one triangle has non-zero area.
|
|
624
|
+
* @param {number[][]} triangle Triangle vertices in mils.
|
|
625
|
+
* @returns {boolean}
|
|
626
|
+
*/
|
|
627
|
+
static #triangleHasArea(triangle) {
|
|
628
|
+
const points = triangle.map((vertex) =>
|
|
629
|
+
PcbAssemblyStepWriter.#scaledVertex(vertex)
|
|
630
|
+
)
|
|
631
|
+
const firstEdge = PcbAssemblyStepWriter.#subtract(points[1], points[0])
|
|
632
|
+
const secondEdge = PcbAssemblyStepWriter.#subtract(points[2], points[0])
|
|
633
|
+
return (
|
|
634
|
+
PcbAssemblyStepWriter.#distance(
|
|
635
|
+
PcbAssemblyStepWriter.#cross(firstEdge, secondEdge)
|
|
636
|
+
) > 0.0000001
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Resolves the planar frame for one face.
|
|
642
|
+
* @param {number[]} indexes Face vertex indices.
|
|
643
|
+
* @param {number[][]} vertices Mesh vertices in mils.
|
|
644
|
+
* @returns {{ normal: number[], xDirection: number[] } | null}
|
|
645
|
+
*/
|
|
646
|
+
static #faceFrame(indexes, vertices) {
|
|
647
|
+
const points = indexes.map((index) =>
|
|
648
|
+
PcbAssemblyStepWriter.#scaledVertex(vertices[index])
|
|
649
|
+
)
|
|
650
|
+
const anchor = points[0]
|
|
651
|
+
|
|
652
|
+
for (let first = 1; first < points.length - 1; first += 1) {
|
|
653
|
+
const firstEdge = PcbAssemblyStepWriter.#subtract(
|
|
654
|
+
points[first],
|
|
655
|
+
anchor
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
if (PcbAssemblyStepWriter.#distance(firstEdge) <= 0.0000001) {
|
|
659
|
+
continue
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
for (let second = first + 1; second < points.length; second += 1) {
|
|
663
|
+
const secondEdge = PcbAssemblyStepWriter.#subtract(
|
|
664
|
+
points[second],
|
|
665
|
+
anchor
|
|
666
|
+
)
|
|
667
|
+
const normal = PcbAssemblyStepWriter.#normalize(
|
|
668
|
+
PcbAssemblyStepWriter.#cross(firstEdge, secondEdge)
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
if (PcbAssemblyStepWriter.#distance(normal) > 0) {
|
|
672
|
+
return {
|
|
673
|
+
normal,
|
|
674
|
+
xDirection: PcbAssemblyStepWriter.#normalize(firstEdge)
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return null
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Scales one mesh vertex from mils to millimetres.
|
|
685
|
+
* @param {number[]} vertex Vertex coordinates in mils.
|
|
686
|
+
* @returns {number[]}
|
|
687
|
+
*/
|
|
688
|
+
static #scaledVertex(vertex) {
|
|
689
|
+
return PcbAssemblyExportCoordinateFrame.vertexMilToMm(vertex)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Builds a stable coordinate key for one vertex.
|
|
694
|
+
* @param {number[]} vertex Vertex coordinates in mils.
|
|
695
|
+
* @returns {string}
|
|
696
|
+
*/
|
|
697
|
+
static #vertexKey(vertex) {
|
|
698
|
+
return PcbAssemblyStepWriter.#scaledVertex(vertex)
|
|
699
|
+
.map((value) => PcbAssemblyStepWriter.#formatNumber(value))
|
|
700
|
+
.join(',')
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Formats AP242 coordinates for a tessellated surface set.
|
|
705
|
+
* @param {number[][]} vertices Mesh vertices in mils.
|
|
706
|
+
* @returns {string}
|
|
707
|
+
*/
|
|
708
|
+
static #coordinateList(vertices) {
|
|
709
|
+
return vertices
|
|
710
|
+
.map(
|
|
711
|
+
(vertex) =>
|
|
712
|
+
'(' +
|
|
713
|
+
PcbAssemblyStepWriter.#scaledVertex(vertex)
|
|
714
|
+
.map((value) =>
|
|
715
|
+
PcbAssemblyStepWriter.#formatNumber(value)
|
|
716
|
+
)
|
|
717
|
+
.join(',') +
|
|
718
|
+
')'
|
|
719
|
+
)
|
|
720
|
+
.join(',')
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Formats AP242 triangle coordinate indexes.
|
|
725
|
+
* @param {number[][]} triangles One-based triangle coordinate indexes.
|
|
726
|
+
* @returns {string}
|
|
727
|
+
*/
|
|
728
|
+
static #triangleList(triangles) {
|
|
729
|
+
return triangles
|
|
730
|
+
.map(
|
|
731
|
+
(triangle) =>
|
|
732
|
+
'(' +
|
|
733
|
+
triangle
|
|
734
|
+
.map((index) => String(Number(index || 0)))
|
|
735
|
+
.join(',') +
|
|
736
|
+
')'
|
|
737
|
+
)
|
|
738
|
+
.join(',')
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Returns clamped RGB components for one mesh color.
|
|
743
|
+
* @param {number[] | undefined} color Mesh RGB color.
|
|
744
|
+
* @returns {number[]}
|
|
745
|
+
*/
|
|
746
|
+
static #colorComponents(color) {
|
|
747
|
+
const normalized = Array.isArray(color) ? color : [0.55, 0.56, 0.58]
|
|
748
|
+
return [0, 1, 2].map((index) =>
|
|
749
|
+
Math.max(Math.min(Number(normalized[index] || 0), 1), 0)
|
|
750
|
+
)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Tests whether a vertex has finite coordinates.
|
|
755
|
+
* @param {unknown} vertex Candidate vertex.
|
|
756
|
+
* @returns {boolean}
|
|
757
|
+
*/
|
|
758
|
+
static #isFiniteVertex(vertex) {
|
|
759
|
+
return (
|
|
760
|
+
Array.isArray(vertex) &&
|
|
761
|
+
vertex.length >= 3 &&
|
|
762
|
+
Number.isFinite(Number(vertex[0])) &&
|
|
763
|
+
Number.isFinite(Number(vertex[1])) &&
|
|
764
|
+
Number.isFinite(Number(vertex[2]))
|
|
765
|
+
)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Subtracts two 3D vectors.
|
|
770
|
+
* @param {number[]} left Left vector.
|
|
771
|
+
* @param {number[]} right Right vector.
|
|
772
|
+
* @returns {number[]}
|
|
773
|
+
*/
|
|
774
|
+
static #subtract(left, right) {
|
|
775
|
+
return [
|
|
776
|
+
Number(left?.[0] || 0) - Number(right?.[0] || 0),
|
|
777
|
+
Number(left?.[1] || 0) - Number(right?.[1] || 0),
|
|
778
|
+
Number(left?.[2] || 0) - Number(right?.[2] || 0)
|
|
779
|
+
]
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Computes one 3D cross product.
|
|
784
|
+
* @param {number[]} left Left vector.
|
|
785
|
+
* @param {number[]} right Right vector.
|
|
786
|
+
* @returns {number[]}
|
|
787
|
+
*/
|
|
788
|
+
static #cross(left, right) {
|
|
789
|
+
return [
|
|
790
|
+
left[1] * right[2] - left[2] * right[1],
|
|
791
|
+
left[2] * right[0] - left[0] * right[2],
|
|
792
|
+
left[0] * right[1] - left[1] * right[0]
|
|
793
|
+
]
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Computes the length of one 3D vector.
|
|
798
|
+
* @param {number[]} vector Vector components.
|
|
799
|
+
* @returns {number}
|
|
800
|
+
*/
|
|
801
|
+
static #distance(vector) {
|
|
802
|
+
return Math.sqrt(
|
|
803
|
+
Number(vector?.[0] || 0) ** 2 +
|
|
804
|
+
Number(vector?.[1] || 0) ** 2 +
|
|
805
|
+
Number(vector?.[2] || 0) ** 2
|
|
806
|
+
)
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Normalizes one 3D vector.
|
|
811
|
+
* @param {number[]} vector Vector components.
|
|
812
|
+
* @returns {number[]}
|
|
813
|
+
*/
|
|
814
|
+
static #normalize(vector) {
|
|
815
|
+
const length = PcbAssemblyStepWriter.#distance(vector)
|
|
816
|
+
|
|
817
|
+
if (length <= 0.0000001) {
|
|
818
|
+
return [0, 0, 0]
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return [
|
|
822
|
+
Number(vector[0] || 0) / length,
|
|
823
|
+
Number(vector[1] || 0) / length,
|
|
824
|
+
Number(vector[2] || 0) / length
|
|
825
|
+
]
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Formats a STEP boolean literal.
|
|
830
|
+
* @param {boolean} value Boolean value.
|
|
831
|
+
* @returns {string}
|
|
832
|
+
*/
|
|
833
|
+
static #stepBoolean(value) {
|
|
834
|
+
return value ? '.T.' : '.F.'
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Returns true when dense geometry should use compact AP242 tessellation.
|
|
839
|
+
* @param {object[]} meshes Assembly meshes.
|
|
840
|
+
* @returns {boolean}
|
|
841
|
+
*/
|
|
842
|
+
static #shouldUseTessellated(meshes) {
|
|
843
|
+
let faceCount = 0
|
|
844
|
+
let vertexCount = 0
|
|
845
|
+
|
|
846
|
+
for (const mesh of PcbAssemblyStepWriter.#array(meshes)) {
|
|
847
|
+
faceCount += PcbAssemblyStepWriter.#array(mesh?.faces).length
|
|
848
|
+
vertexCount += PcbAssemblyStepWriter.#array(mesh?.vertices).length
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return (
|
|
852
|
+
meshes.length > TESSELLATED_MESH_THRESHOLD ||
|
|
853
|
+
faceCount > TESSELLATED_FACE_THRESHOLD ||
|
|
854
|
+
vertexCount > TESSELLATED_VERTEX_THRESHOLD
|
|
855
|
+
)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Adds one STEP entity and returns its numeric id.
|
|
860
|
+
* @param {string} body Entity body.
|
|
861
|
+
* @returns {number}
|
|
862
|
+
*/
|
|
863
|
+
#add(body) {
|
|
864
|
+
const id = this.#nextId
|
|
865
|
+
this.#nextId += 1
|
|
866
|
+
this.#entities.push('#' + id + '=' + body)
|
|
867
|
+
return id
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Builds the STEP header.
|
|
872
|
+
* @param {string} name Assembly name.
|
|
873
|
+
* @param {boolean} tessellated Whether AP242 tessellated schema should be used.
|
|
874
|
+
* @returns {string}
|
|
875
|
+
*/
|
|
876
|
+
static #header(name, tessellated) {
|
|
877
|
+
const schema = tessellated
|
|
878
|
+
? 'AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF'
|
|
879
|
+
: 'AUTOMOTIVE_DESIGN_CC2'
|
|
880
|
+
|
|
881
|
+
return (
|
|
882
|
+
'ISO-10303-21;\nHEADER;\n' +
|
|
883
|
+
"FILE_DESCRIPTION(('ECAD Forge PCB assembly export'),'2;1');\n" +
|
|
884
|
+
"FILE_NAME('" +
|
|
885
|
+
PcbAssemblyStepWriter.#escape(name) +
|
|
886
|
+
".step','',('ECAD Forge'),('ECAD Forge'),'ECAD Forge','ECAD Forge','');\n" +
|
|
887
|
+
"FILE_SCHEMA(('" +
|
|
888
|
+
schema +
|
|
889
|
+
"'));\n" +
|
|
890
|
+
'ENDSEC;\nDATA;\n'
|
|
891
|
+
)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Formats a STEP numeric literal.
|
|
896
|
+
* @param {number} value Numeric value.
|
|
897
|
+
* @returns {string}
|
|
898
|
+
*/
|
|
899
|
+
static #formatNumber(value) {
|
|
900
|
+
const number = Number(value || 0)
|
|
901
|
+
return Math.abs(number) < 0.0000001
|
|
902
|
+
? '0.'
|
|
903
|
+
: Number(number.toFixed(6)).toString()
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Escapes STEP string content.
|
|
908
|
+
* @param {string} value Source value.
|
|
909
|
+
* @returns {string}
|
|
910
|
+
*/
|
|
911
|
+
static #escape(value) {
|
|
912
|
+
return String(value || '').replaceAll("'", "''")
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Formats STEP entity ids as a reference list.
|
|
917
|
+
* @param {unknown} value Candidate id list.
|
|
918
|
+
* @returns {string}
|
|
919
|
+
*/
|
|
920
|
+
static #referenceList(value) {
|
|
921
|
+
const references = PcbAssemblyStepWriter.#array(value)
|
|
922
|
+
.map((id) => Number(id || 0))
|
|
923
|
+
.filter((id) => Number.isFinite(id) && id > 0)
|
|
924
|
+
.map((id) => '#' + id)
|
|
925
|
+
|
|
926
|
+
if (references.length <= 12) {
|
|
927
|
+
return references.join(',')
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return (
|
|
931
|
+
'\n' +
|
|
932
|
+
references
|
|
933
|
+
.map(
|
|
934
|
+
(reference, index) =>
|
|
935
|
+
' ' +
|
|
936
|
+
reference +
|
|
937
|
+
(index === references.length - 1 ? '' : ',')
|
|
938
|
+
)
|
|
939
|
+
.join('\n') +
|
|
940
|
+
'\n'
|
|
941
|
+
)
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Normalizes a value to an array.
|
|
946
|
+
* @param {unknown} value Candidate value.
|
|
947
|
+
* @returns {any[]}
|
|
948
|
+
*/
|
|
949
|
+
static #array(value) {
|
|
950
|
+
return Array.isArray(value) ? value : []
|
|
951
|
+
}
|
|
952
|
+
}
|