circuit-json-to-step 0.0.1

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/lib/index.ts ADDED
@@ -0,0 +1,545 @@
1
+ import type { CircuitJson } from "circuit-json"
2
+ import {
3
+ Repository,
4
+ ApplicationContext,
5
+ ApplicationProtocolDefinition,
6
+ ProductContext,
7
+ Product,
8
+ ProductDefinitionContext,
9
+ ProductDefinitionFormation,
10
+ ProductDefinition,
11
+ ProductDefinitionShape,
12
+ Unknown,
13
+ CartesianPoint,
14
+ Direction,
15
+ Axis2Placement3D,
16
+ Plane,
17
+ CylindricalSurface,
18
+ VertexPoint,
19
+ EdgeCurve,
20
+ Line,
21
+ Vector,
22
+ EdgeLoop,
23
+ OrientedEdge,
24
+ FaceOuterBound,
25
+ FaceBound,
26
+ AdvancedFace,
27
+ Circle,
28
+ ClosedShell,
29
+ ManifoldSolidBrep,
30
+ ColourRgb,
31
+ FillAreaStyleColour,
32
+ FillAreaStyle,
33
+ SurfaceStyleFillArea,
34
+ SurfaceSideStyle,
35
+ SurfaceStyleUsage,
36
+ PresentationStyleAssignment,
37
+ StyledItem,
38
+ MechanicalDesignGeometricPresentationRepresentation,
39
+ AdvancedBrepShapeRepresentation,
40
+ ShapeDefinitionRepresentation,
41
+ type Ref,
42
+ } from "stepts"
43
+ import { generateComponentMeshes } from "./mesh-generation"
44
+
45
+ export interface CircuitJsonToStepOptions {
46
+ /** Board width in mm (optional if pcb_board is present) */
47
+ boardWidth?: number
48
+ /** Board height in mm (optional if pcb_board is present) */
49
+ boardHeight?: number
50
+ /** Board thickness in mm (default: 1.6mm or from pcb_board) */
51
+ boardThickness?: number
52
+ /** Product name (default: "PCB") */
53
+ productName?: string
54
+ /** Include component meshes (default: false) */
55
+ includeComponents?: boolean
56
+ /** Include external model meshes from model_*_url fields (default: false). Only applicable when includeComponents is true. */
57
+ includeExternalMeshes?: boolean
58
+ }
59
+
60
+ /**
61
+ * Converts circuit JSON to STEP format, creating holes in a PCB board
62
+ */
63
+ export async function circuitJsonToStep(
64
+ circuitJson: CircuitJson,
65
+ options: CircuitJsonToStepOptions = {},
66
+ ): Promise<string> {
67
+ const repo = new Repository()
68
+
69
+ // Extract pcb_board and holes from circuit JSON
70
+ const pcbBoard = circuitJson.find((item) => item.type === "pcb_board")
71
+ const holes: any[] = circuitJson.filter(
72
+ (item) => item.type === "pcb_hole" || item.type === "pcb_plated_hole",
73
+ )
74
+
75
+ // Get dimensions from pcb_board or options
76
+ const boardWidth = options.boardWidth ?? pcbBoard?.width
77
+ const boardHeight = options.boardHeight ?? pcbBoard?.height
78
+ const boardThickness = options.boardThickness ?? pcbBoard?.thickness ?? 1.6
79
+ const productName = options.productName ?? "PCB"
80
+
81
+ // Get board center position (defaults to 0, 0 if not specified)
82
+ const boardCenterX = pcbBoard?.center?.x ?? 0
83
+ const boardCenterY = pcbBoard?.center?.y ?? 0
84
+
85
+ if (!boardWidth || !boardHeight) {
86
+ throw new Error(
87
+ "Board dimensions not found. Either provide boardWidth and boardHeight in options, or include a pcb_board in the circuit JSON with width and height properties.",
88
+ )
89
+ }
90
+
91
+ // Product structure (required for STEP validation)
92
+ const appContext = repo.add(
93
+ new ApplicationContext(
94
+ "core data for automotive mechanical design processes",
95
+ ),
96
+ )
97
+ repo.add(
98
+ new ApplicationProtocolDefinition(
99
+ "international standard",
100
+ "automotive_design",
101
+ 2010,
102
+ appContext,
103
+ ),
104
+ )
105
+ const productContext = repo.add(
106
+ new ProductContext("", appContext, "mechanical"),
107
+ )
108
+ const product = repo.add(
109
+ new Product(productName, productName, "", [productContext]),
110
+ )
111
+ const productDefContext = repo.add(
112
+ new ProductDefinitionContext("part definition", appContext, "design"),
113
+ )
114
+ const productDefFormation = repo.add(
115
+ new ProductDefinitionFormation("", "", product),
116
+ )
117
+ const productDef = repo.add(
118
+ new ProductDefinition("", "", productDefFormation, productDefContext),
119
+ )
120
+ const productDefShape = repo.add(
121
+ new ProductDefinitionShape("", "", productDef),
122
+ )
123
+
124
+ // Representation context
125
+ const lengthUnit = repo.add(
126
+ new Unknown("", [
127
+ "( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) )",
128
+ ]),
129
+ )
130
+ const angleUnit = repo.add(
131
+ new Unknown("", [
132
+ "( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) )",
133
+ ]),
134
+ )
135
+ const solidAngleUnit = repo.add(
136
+ new Unknown("", [
137
+ "( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() )",
138
+ ]),
139
+ )
140
+ const uncertainty = repo.add(
141
+ new Unknown("UNCERTAINTY_MEASURE_WITH_UNIT", [
142
+ `LENGTH_MEASURE(1.E-07)`,
143
+ `${lengthUnit}`,
144
+ `'distance_accuracy_value'`,
145
+ `'Maximum Tolerance'`,
146
+ ]),
147
+ )
148
+ const geomContext = repo.add(
149
+ new Unknown("", [
150
+ `( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((${uncertainty})) GLOBAL_UNIT_ASSIGNED_CONTEXT((${lengthUnit},${angleUnit},${solidAngleUnit})) REPRESENTATION_CONTEXT('${productName}','3D') )`,
151
+ ]),
152
+ )
153
+
154
+ // Create board vertices based on outline or rectangular shape
155
+ const outline = pcbBoard?.outline
156
+ let bottomVertices: Ref<VertexPoint>[]
157
+ let topVertices: Ref<VertexPoint>[]
158
+
159
+ if (outline && Array.isArray(outline) && outline.length >= 3) {
160
+ // Use custom outline (points are already relative to board center)
161
+ bottomVertices = outline.map((point) =>
162
+ repo.add(
163
+ new VertexPoint(
164
+ "",
165
+ repo.add(new CartesianPoint("", point.x, point.y, 0)),
166
+ ),
167
+ ),
168
+ )
169
+ topVertices = outline.map((point) =>
170
+ repo.add(
171
+ new VertexPoint(
172
+ "",
173
+ repo.add(new CartesianPoint("", point.x, point.y, boardThickness)),
174
+ ),
175
+ ),
176
+ )
177
+ } else {
178
+ // Fall back to rectangular shape centered at (boardCenterX, boardCenterY)
179
+ const halfWidth = boardWidth / 2
180
+ const halfHeight = boardHeight / 2
181
+ const corners = [
182
+ [boardCenterX - halfWidth, boardCenterY - halfHeight, 0],
183
+ [boardCenterX + halfWidth, boardCenterY - halfHeight, 0],
184
+ [boardCenterX + halfWidth, boardCenterY + halfHeight, 0],
185
+ [boardCenterX - halfWidth, boardCenterY + halfHeight, 0],
186
+ [boardCenterX - halfWidth, boardCenterY - halfHeight, boardThickness],
187
+ [boardCenterX + halfWidth, boardCenterY - halfHeight, boardThickness],
188
+ [boardCenterX + halfWidth, boardCenterY + halfHeight, boardThickness],
189
+ [boardCenterX - halfWidth, boardCenterY + halfHeight, boardThickness],
190
+ ]
191
+ const vertices = corners.map(([x, y, z]) =>
192
+ repo.add(
193
+ new VertexPoint("", repo.add(new CartesianPoint("", x!, y!, z!))),
194
+ ),
195
+ )
196
+ bottomVertices = [vertices[0]!, vertices[1]!, vertices[2]!, vertices[3]!]
197
+ topVertices = [vertices[4]!, vertices[5]!, vertices[6]!, vertices[7]!]
198
+ }
199
+
200
+ // Helper to create edge between vertices
201
+ function createEdge(
202
+ v1: Ref<VertexPoint>,
203
+ v2: Ref<VertexPoint>,
204
+ ): Ref<EdgeCurve> {
205
+ const p1 = v1.resolve(repo).pnt.resolve(repo)
206
+ const p2 = v2.resolve(repo).pnt.resolve(repo)
207
+ const dir = repo.add(
208
+ new Direction("", p2.x - p1.x, p2.y - p1.y, p2.z - p1.z),
209
+ )
210
+ const vec = repo.add(new Vector("", dir, 1))
211
+ const line = repo.add(new Line("", v1.resolve(repo).pnt, vec))
212
+ return repo.add(new EdgeCurve("", v1, v2, line, true))
213
+ }
214
+
215
+ // Create board edges
216
+ const bottomEdges: Ref<EdgeCurve>[] = []
217
+ const topEdges: Ref<EdgeCurve>[] = []
218
+ const verticalEdges: Ref<EdgeCurve>[] = []
219
+
220
+ // Bottom edges (connect vertices in a loop)
221
+ for (let i = 0; i < bottomVertices.length; i++) {
222
+ const v1 = bottomVertices[i]!
223
+ const v2 = bottomVertices[(i + 1) % bottomVertices.length]!
224
+ bottomEdges.push(createEdge(v1, v2))
225
+ }
226
+
227
+ // Top edges (connect vertices in a loop)
228
+ for (let i = 0; i < topVertices.length; i++) {
229
+ const v1 = topVertices[i]!
230
+ const v2 = topVertices[(i + 1) % topVertices.length]!
231
+ topEdges.push(createEdge(v1, v2))
232
+ }
233
+
234
+ // Vertical edges (connect bottom to top)
235
+ for (let i = 0; i < bottomVertices.length; i++) {
236
+ verticalEdges.push(createEdge(bottomVertices[i]!, topVertices[i]!))
237
+ }
238
+
239
+ const origin = repo.add(new CartesianPoint("", 0, 0, 0))
240
+ const xDir = repo.add(new Direction("", 1, 0, 0))
241
+ const zDir = repo.add(new Direction("", 0, 0, 1))
242
+
243
+ // Bottom face (z=0, normal pointing down)
244
+ const bottomFrame = repo.add(
245
+ new Axis2Placement3D(
246
+ "",
247
+ origin,
248
+ repo.add(new Direction("", 0, 0, -1)),
249
+ xDir,
250
+ ),
251
+ )
252
+ const bottomPlane = repo.add(new Plane("", bottomFrame))
253
+ const bottomLoop = repo.add(
254
+ new EdgeLoop(
255
+ "",
256
+ bottomEdges.map((edge) => repo.add(new OrientedEdge("", edge, true))),
257
+ ),
258
+ )
259
+
260
+ // Create holes in bottom face
261
+ const bottomHoleLoops: Ref<FaceBound>[] = []
262
+ for (const hole of holes) {
263
+ // Check shape (pcb_hole uses hole_shape, pcb_plated_hole uses shape)
264
+ const holeShape = hole.hole_shape || hole.shape
265
+ if (holeShape === "circle") {
266
+ const holeX = typeof hole.x === "number" ? hole.x : (hole.x as any).value
267
+ const holeY = typeof hole.y === "number" ? hole.y : (hole.y as any).value
268
+ const radius = hole.hole_diameter / 2
269
+
270
+ const holeCenter = repo.add(new CartesianPoint("", holeX, holeY, 0))
271
+ const holeVertex = repo.add(
272
+ new VertexPoint(
273
+ "",
274
+ repo.add(new CartesianPoint("", holeX + radius, holeY, 0)),
275
+ ),
276
+ )
277
+ const holePlacement = repo.add(
278
+ new Axis2Placement3D(
279
+ "",
280
+ holeCenter,
281
+ repo.add(new Direction("", 0, 0, -1)),
282
+ xDir,
283
+ ),
284
+ )
285
+ const holeCircle = repo.add(new Circle("", holePlacement, radius))
286
+ const holeEdge = repo.add(
287
+ new EdgeCurve("", holeVertex, holeVertex, holeCircle, true),
288
+ )
289
+ const holeLoop = repo.add(
290
+ new EdgeLoop("", [repo.add(new OrientedEdge("", holeEdge, false))]),
291
+ )
292
+ bottomHoleLoops.push(repo.add(new FaceBound("", holeLoop, true)))
293
+ }
294
+ }
295
+
296
+ const bottomFace = repo.add(
297
+ new AdvancedFace(
298
+ "",
299
+ [
300
+ repo.add(new FaceOuterBound("", bottomLoop, true)),
301
+ ...bottomHoleLoops,
302
+ ] as any,
303
+ bottomPlane,
304
+ true,
305
+ ),
306
+ )
307
+
308
+ // Top face (z=boardThickness, normal pointing up)
309
+ const topOrigin = repo.add(new CartesianPoint("", 0, 0, boardThickness))
310
+ const topFrame = repo.add(new Axis2Placement3D("", topOrigin, zDir, xDir))
311
+ const topPlane = repo.add(new Plane("", topFrame))
312
+ const topLoop = repo.add(
313
+ new EdgeLoop(
314
+ "",
315
+ topEdges.map((edge) => repo.add(new OrientedEdge("", edge, false))),
316
+ ),
317
+ )
318
+
319
+ // Create holes in top face
320
+ const topHoleLoops: Ref<FaceBound>[] = []
321
+ for (const hole of holes) {
322
+ // Check shape (pcb_hole uses hole_shape, pcb_plated_hole uses shape)
323
+ const holeShape = hole.hole_shape || hole.shape
324
+ if (holeShape === "circle") {
325
+ const holeX = typeof hole.x === "number" ? hole.x : (hole.x as any).value
326
+ const holeY = typeof hole.y === "number" ? hole.y : (hole.y as any).value
327
+ const radius = hole.hole_diameter / 2
328
+
329
+ const holeCenter = repo.add(
330
+ new CartesianPoint("", holeX, holeY, boardThickness),
331
+ )
332
+ const holeVertex = repo.add(
333
+ new VertexPoint(
334
+ "",
335
+ repo.add(
336
+ new CartesianPoint("", holeX + radius, holeY, boardThickness),
337
+ ),
338
+ ),
339
+ )
340
+ const holePlacement = repo.add(
341
+ new Axis2Placement3D("", holeCenter, zDir, xDir),
342
+ )
343
+ const holeCircle = repo.add(new Circle("", holePlacement, radius))
344
+ const holeEdge = repo.add(
345
+ new EdgeCurve("", holeVertex, holeVertex, holeCircle, true),
346
+ )
347
+ const holeLoop = repo.add(
348
+ new EdgeLoop("", [repo.add(new OrientedEdge("", holeEdge, true))]),
349
+ )
350
+ topHoleLoops.push(repo.add(new FaceBound("", holeLoop, true)))
351
+ }
352
+ }
353
+
354
+ const topFace = repo.add(
355
+ new AdvancedFace(
356
+ "",
357
+ [repo.add(new FaceOuterBound("", topLoop, true)), ...topHoleLoops] as any,
358
+ topPlane,
359
+ true,
360
+ ),
361
+ )
362
+
363
+ // Create side faces (one for each edge of the outline)
364
+ const sideFaces: Ref<AdvancedFace>[] = []
365
+ for (let i = 0; i < bottomEdges.length; i++) {
366
+ const nextI = (i + 1) % bottomEdges.length
367
+
368
+ // Get points for this side face
369
+ const bottomV1Pnt = bottomVertices[i]!.resolve(repo).pnt
370
+ const bottomV2Pnt = bottomVertices[nextI]!.resolve(repo).pnt
371
+ const bottomV1 = bottomV1Pnt.resolve(repo)
372
+ const bottomV2 = bottomV2Pnt.resolve(repo)
373
+
374
+ // Calculate edge direction and outward normal
375
+ const edgeDir = {
376
+ x: bottomV2.x - bottomV1.x,
377
+ y: bottomV2.y - bottomV1.y,
378
+ z: 0,
379
+ }
380
+ // Normal is perpendicular (rotate 90 degrees clockwise in XY plane for outward facing)
381
+ const normalDir = repo.add(new Direction("", edgeDir.y, -edgeDir.x, 0))
382
+
383
+ // Reference direction along the edge
384
+ const refDir = repo.add(new Direction("", edgeDir.x, edgeDir.y, 0))
385
+
386
+ const sideFrame = repo.add(
387
+ new Axis2Placement3D("", bottomV1Pnt, normalDir, refDir),
388
+ )
389
+ const sidePlane = repo.add(new Plane("", sideFrame))
390
+ const sideLoop = repo.add(
391
+ new EdgeLoop("", [
392
+ repo.add(new OrientedEdge("", bottomEdges[i]!, true)),
393
+ repo.add(new OrientedEdge("", verticalEdges[nextI]!, true)),
394
+ repo.add(new OrientedEdge("", topEdges[i]!, false)),
395
+ repo.add(new OrientedEdge("", verticalEdges[i]!, false)),
396
+ ]),
397
+ )
398
+ const sideFace = repo.add(
399
+ new AdvancedFace(
400
+ "",
401
+ [repo.add(new FaceOuterBound("", sideLoop, true))],
402
+ sidePlane,
403
+ true,
404
+ ),
405
+ )
406
+ sideFaces.push(sideFace)
407
+ }
408
+
409
+ // Create cylindrical faces for holes
410
+ const holeCylindricalFaces: Ref<AdvancedFace>[] = []
411
+ for (const hole of holes) {
412
+ const holeShape = hole.hole_shape || hole.shape
413
+ if (holeShape === "circle") {
414
+ const holeX = typeof hole.x === "number" ? hole.x : (hole.x as any).value
415
+ const holeY = typeof hole.y === "number" ? hole.y : (hole.y as any).value
416
+ const radius = hole.hole_diameter / 2
417
+
418
+ // Create circular edges at bottom and top
419
+ const bottomHoleCenter = repo.add(new CartesianPoint("", holeX, holeY, 0))
420
+ const bottomHoleVertex = repo.add(
421
+ new VertexPoint(
422
+ "",
423
+ repo.add(new CartesianPoint("", holeX + radius, holeY, 0)),
424
+ ),
425
+ )
426
+ const bottomHolePlacement = repo.add(
427
+ new Axis2Placement3D(
428
+ "",
429
+ bottomHoleCenter,
430
+ repo.add(new Direction("", 0, 0, -1)),
431
+ xDir,
432
+ ),
433
+ )
434
+ const bottomHoleCircle = repo.add(
435
+ new Circle("", bottomHolePlacement, radius),
436
+ )
437
+ const bottomHoleEdge = repo.add(
438
+ new EdgeCurve(
439
+ "",
440
+ bottomHoleVertex,
441
+ bottomHoleVertex,
442
+ bottomHoleCircle,
443
+ true,
444
+ ),
445
+ )
446
+
447
+ const topHoleCenter = repo.add(
448
+ new CartesianPoint("", holeX, holeY, boardThickness),
449
+ )
450
+ const topHoleVertex = repo.add(
451
+ new VertexPoint(
452
+ "",
453
+ repo.add(
454
+ new CartesianPoint("", holeX + radius, holeY, boardThickness),
455
+ ),
456
+ ),
457
+ )
458
+ const topHolePlacement = repo.add(
459
+ new Axis2Placement3D("", topHoleCenter, zDir, xDir),
460
+ )
461
+ const topHoleCircle = repo.add(new Circle("", topHolePlacement, radius))
462
+ const topHoleEdge = repo.add(
463
+ new EdgeCurve("", topHoleVertex, topHoleVertex, topHoleCircle, true),
464
+ )
465
+
466
+ // Create edge loop for cylindrical surface
467
+ const holeCylinderLoop = repo.add(
468
+ new EdgeLoop("", [
469
+ repo.add(new OrientedEdge("", bottomHoleEdge, true)),
470
+ repo.add(new OrientedEdge("", topHoleEdge, false)),
471
+ ]),
472
+ )
473
+
474
+ // Create cylindrical surface for the hole (axis along Z)
475
+ const holeCylinderPlacement = repo.add(
476
+ new Axis2Placement3D("", bottomHoleCenter, zDir, xDir),
477
+ )
478
+ const holeCylinderSurface = repo.add(
479
+ new CylindricalSurface("", holeCylinderPlacement, radius),
480
+ )
481
+ const holeCylinderFace = repo.add(
482
+ new AdvancedFace(
483
+ "",
484
+ [repo.add(new FaceOuterBound("", holeCylinderLoop, true))],
485
+ holeCylinderSurface,
486
+ false,
487
+ ),
488
+ )
489
+ holeCylindricalFaces.push(holeCylinderFace)
490
+ }
491
+ }
492
+
493
+ // Collect all faces
494
+ const allFaces = [bottomFace, topFace, ...sideFaces, ...holeCylindricalFaces]
495
+
496
+ // Create closed shell and solid
497
+ const shell = repo.add(new ClosedShell("", allFaces))
498
+ const solid = repo.add(new ManifoldSolidBrep(productName, shell))
499
+
500
+ // Array to hold all solids (board + optional components)
501
+ const allSolids: Ref<ManifoldSolidBrep>[] = [solid]
502
+
503
+ // Generate component mesh if requested
504
+ if (options.includeComponents) {
505
+ const componentSolids = await generateComponentMeshes({
506
+ repo,
507
+ circuitJson,
508
+ boardThickness,
509
+ includeExternalMeshes: options.includeExternalMeshes,
510
+ })
511
+ allSolids.push(...componentSolids)
512
+ }
513
+
514
+ // Add presentation/styling for all solids
515
+ const styledItems: Ref<StyledItem>[] = []
516
+
517
+ for (const solidRef of allSolids) {
518
+ const color = repo.add(new ColourRgb("", 0.2, 0.6, 0.2))
519
+ const fillColor = repo.add(new FillAreaStyleColour("", color))
520
+ const fillStyle = repo.add(new FillAreaStyle("", [fillColor]))
521
+ const surfaceFill = repo.add(new SurfaceStyleFillArea(fillStyle))
522
+ const surfaceSide = repo.add(new SurfaceSideStyle("", [surfaceFill]))
523
+ const surfaceUsage = repo.add(new SurfaceStyleUsage(".BOTH.", surfaceSide))
524
+ const presStyle = repo.add(new PresentationStyleAssignment([surfaceUsage]))
525
+ const styledItem = repo.add(new StyledItem("", [presStyle], solidRef))
526
+ styledItems.push(styledItem)
527
+ }
528
+
529
+ repo.add(
530
+ new MechanicalDesignGeometricPresentationRepresentation(
531
+ "",
532
+ styledItems,
533
+ geomContext,
534
+ ),
535
+ )
536
+
537
+ // Shape representation with all solids
538
+ const shapeRep = repo.add(
539
+ new AdvancedBrepShapeRepresentation(productName, allSolids, geomContext),
540
+ )
541
+ repo.add(new ShapeDefinitionRepresentation(productDefShape, shapeRep))
542
+
543
+ // Generate and return STEP file text
544
+ return repo.toPartFile({ name: productName })
545
+ }