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.
@@ -0,0 +1,333 @@
1
+ import type { CircuitJson } from "circuit-json"
2
+ import type { Triangle as GLTFTriangle } from "circuit-json-to-gltf"
3
+ import { convertCircuitJsonTo3D } from "circuit-json-to-gltf"
4
+ import type { Ref } from "stepts"
5
+ import type { Repository } from "stepts"
6
+ import {
7
+ AdvancedFace,
8
+ Axis2Placement3D,
9
+ CartesianPoint,
10
+ ClosedShell,
11
+ Direction,
12
+ EdgeCurve,
13
+ EdgeLoop,
14
+ FaceOuterBound,
15
+ Line,
16
+ ManifoldSolidBrep,
17
+ OrientedEdge,
18
+ Plane,
19
+ Vector,
20
+ VertexPoint,
21
+ } from "stepts"
22
+
23
+ export interface MeshGenerationOptions {
24
+ /** Repository to add STEP entities to */
25
+ repo: Repository
26
+ /** Circuit JSON elements to convert */
27
+ circuitJson: CircuitJson
28
+ /** Board thickness in mm */
29
+ boardThickness: number
30
+ /** Include external model meshes from model_*_url fields (default: false) */
31
+ includeExternalMeshes?: boolean
32
+ }
33
+
34
+ /**
35
+ * Generates triangles for a box mesh
36
+ */
37
+ function createBoxTriangles(box: {
38
+ center: { x: number; y: number; z: number }
39
+ size: { x: number; y: number; z: number }
40
+ rotation?: { x: number; y: number; z: number }
41
+ }): GLTFTriangle[] {
42
+ const { center, size } = box
43
+ const halfX = size.x / 2
44
+ const halfY = size.y / 2
45
+ const halfZ = size.z / 2
46
+
47
+ // Define 8 corners of the box
48
+ const corners = [
49
+ { x: -halfX, y: -halfY, z: -halfZ },
50
+ { x: halfX, y: -halfY, z: -halfZ },
51
+ { x: halfX, y: halfY, z: -halfZ },
52
+ { x: -halfX, y: halfY, z: -halfZ },
53
+ { x: -halfX, y: -halfY, z: halfZ },
54
+ { x: halfX, y: -halfY, z: halfZ },
55
+ { x: halfX, y: halfY, z: halfZ },
56
+ { x: -halfX, y: halfY, z: halfZ },
57
+ ].map((p) => ({ x: p.x + center.x, y: p.y + center.y, z: p.z + center.z }))
58
+
59
+ // Define triangles for each face (2 triangles per face)
60
+ const triangles: GLTFTriangle[] = [
61
+ // Bottom face (z = -halfZ)
62
+ {
63
+ vertices: [corners[0]!, corners[1]!, corners[2]!],
64
+ normal: { x: 0, y: 0, z: -1 },
65
+ },
66
+ {
67
+ vertices: [corners[0]!, corners[2]!, corners[3]!],
68
+ normal: { x: 0, y: 0, z: -1 },
69
+ },
70
+ // Top face (z = halfZ)
71
+ {
72
+ vertices: [corners[4]!, corners[6]!, corners[5]!],
73
+ normal: { x: 0, y: 0, z: 1 },
74
+ },
75
+ {
76
+ vertices: [corners[4]!, corners[7]!, corners[6]!],
77
+ normal: { x: 0, y: 0, z: 1 },
78
+ },
79
+ // Front face (y = -halfY)
80
+ {
81
+ vertices: [corners[0]!, corners[5]!, corners[1]!],
82
+ normal: { x: 0, y: -1, z: 0 },
83
+ },
84
+ {
85
+ vertices: [corners[0]!, corners[4]!, corners[5]!],
86
+ normal: { x: 0, y: -1, z: 0 },
87
+ },
88
+ // Back face (y = halfY)
89
+ {
90
+ vertices: [corners[2]!, corners[6]!, corners[7]!],
91
+ normal: { x: 0, y: 1, z: 0 },
92
+ },
93
+ {
94
+ vertices: [corners[2]!, corners[7]!, corners[3]!],
95
+ normal: { x: 0, y: 1, z: 0 },
96
+ },
97
+ // Left face (x = -halfX)
98
+ {
99
+ vertices: [corners[0]!, corners[3]!, corners[7]!],
100
+ normal: { x: -1, y: 0, z: 0 },
101
+ },
102
+ {
103
+ vertices: [corners[0]!, corners[7]!, corners[4]!],
104
+ normal: { x: -1, y: 0, z: 0 },
105
+ },
106
+ // Right face (x = halfX)
107
+ {
108
+ vertices: [corners[1]!, corners[6]!, corners[2]!],
109
+ normal: { x: 1, y: 0, z: 0 },
110
+ },
111
+ {
112
+ vertices: [corners[1]!, corners[5]!, corners[6]!],
113
+ normal: { x: 1, y: 0, z: 0 },
114
+ },
115
+ ]
116
+
117
+ return triangles
118
+ }
119
+
120
+ /**
121
+ * Creates STEP faces from GLTF triangles
122
+ */
123
+ function createStepFacesFromTriangles(
124
+ repo: Repository,
125
+ triangles: GLTFTriangle[],
126
+ ): Ref<AdvancedFace>[] {
127
+ const faces: Ref<AdvancedFace>[] = []
128
+
129
+ for (const triangle of triangles) {
130
+ // Create vertices for triangle
131
+ const v1 = repo.add(
132
+ new VertexPoint(
133
+ "",
134
+ repo.add(
135
+ new CartesianPoint(
136
+ "",
137
+ triangle.vertices[0]!.x,
138
+ triangle.vertices[0]!.y,
139
+ triangle.vertices[0]!.z,
140
+ ),
141
+ ),
142
+ ),
143
+ )
144
+ const v2 = repo.add(
145
+ new VertexPoint(
146
+ "",
147
+ repo.add(
148
+ new CartesianPoint(
149
+ "",
150
+ triangle.vertices[1]!.x,
151
+ triangle.vertices[1]!.y,
152
+ triangle.vertices[1]!.z,
153
+ ),
154
+ ),
155
+ ),
156
+ )
157
+ const v3 = repo.add(
158
+ new VertexPoint(
159
+ "",
160
+ repo.add(
161
+ new CartesianPoint(
162
+ "",
163
+ triangle.vertices[2]!.x,
164
+ triangle.vertices[2]!.y,
165
+ triangle.vertices[2]!.z,
166
+ ),
167
+ ),
168
+ ),
169
+ )
170
+
171
+ // Create edges between vertices
172
+ const p1 = v1.resolve(repo).pnt.resolve(repo)
173
+ const p2 = v2.resolve(repo).pnt.resolve(repo)
174
+
175
+ const createEdge = (
176
+ vStart: Ref<VertexPoint>,
177
+ vEnd: Ref<VertexPoint>,
178
+ ): Ref<EdgeCurve> => {
179
+ const pStart = vStart.resolve(repo).pnt.resolve(repo)
180
+ const pEnd = vEnd.resolve(repo).pnt.resolve(repo)
181
+ const dir = repo.add(
182
+ new Direction(
183
+ "",
184
+ pEnd.x - pStart.x,
185
+ pEnd.y - pStart.y,
186
+ pEnd.z - pStart.z,
187
+ ),
188
+ )
189
+ const vec = repo.add(new Vector("", dir, 1))
190
+ const line = repo.add(new Line("", vStart.resolve(repo).pnt, vec))
191
+ return repo.add(new EdgeCurve("", vStart, vEnd, line, true))
192
+ }
193
+
194
+ const edge1 = createEdge(v1, v2)
195
+ const edge2 = createEdge(v2, v3)
196
+ const edge3 = createEdge(v3, v1)
197
+
198
+ // Create edge loop for triangle
199
+ const edgeLoop = repo.add(
200
+ new EdgeLoop("", [
201
+ repo.add(new OrientedEdge("", edge1, true)),
202
+ repo.add(new OrientedEdge("", edge2, true)),
203
+ repo.add(new OrientedEdge("", edge3, true)),
204
+ ]),
205
+ )
206
+
207
+ // Create planar surface using triangle normal
208
+ const normalDir = repo.add(
209
+ new Direction(
210
+ "",
211
+ triangle.normal.x,
212
+ triangle.normal.y,
213
+ triangle.normal.z,
214
+ ),
215
+ )
216
+
217
+ // Use first vertex as origin, calculate reference direction from first edge
218
+ const refX = p2.x - p1.x
219
+ const refY = p2.y - p1.y
220
+ const refZ = p2.z - p1.z
221
+ const refDir = repo.add(new Direction("", refX, refY, refZ))
222
+
223
+ const placement = repo.add(
224
+ new Axis2Placement3D("", v1.resolve(repo).pnt, normalDir, refDir),
225
+ )
226
+ const plane = repo.add(new Plane("", placement))
227
+
228
+ // Create face
229
+ const face = repo.add(
230
+ new AdvancedFace(
231
+ "",
232
+ [repo.add(new FaceOuterBound("", edgeLoop, true))],
233
+ plane,
234
+ true,
235
+ ),
236
+ )
237
+ faces.push(face)
238
+ }
239
+
240
+ return faces
241
+ }
242
+
243
+ /**
244
+ * Generates component meshes from circuit JSON and converts them to STEP solids
245
+ *
246
+ * By default, model_*_url fields are filtered out to prevent hanging on external
247
+ * model fetches during conversion. Set includeExternalMeshes to true to allow
248
+ * external model fetching.
249
+ */
250
+ export async function generateComponentMeshes(
251
+ options: MeshGenerationOptions,
252
+ ): Promise<Ref<ManifoldSolidBrep>[]> {
253
+ const {
254
+ repo,
255
+ circuitJson,
256
+ boardThickness,
257
+ includeExternalMeshes = false,
258
+ } = options
259
+ const solids: Ref<ManifoldSolidBrep>[] = []
260
+
261
+ try {
262
+ // Filter circuit JSON and optionally remove model URLs
263
+ const filteredCircuitJson = circuitJson
264
+ .filter((e) => e.type !== "pcb_board")
265
+ .map((e) => {
266
+ if (!includeExternalMeshes && e.type === "cad_component") {
267
+ // Remove model_*_url fields to avoid hanging on external model fetches
268
+ return {
269
+ ...e,
270
+ model_3mf_url: undefined,
271
+ model_obj_url: undefined,
272
+ model_stl_url: undefined,
273
+ model_glb_url: undefined,
274
+ model_gltf_url: undefined,
275
+ }
276
+ }
277
+ return e
278
+ })
279
+
280
+ // Convert circuit JSON to 3D scene
281
+ const scene3d = await convertCircuitJsonTo3D(filteredCircuitJson, {
282
+ boardThickness,
283
+ renderBoardTextures: false,
284
+ })
285
+
286
+ // Extract or generate triangles from component boxes
287
+ const allTriangles: GLTFTriangle[] = []
288
+ for (const box of scene3d.boxes) {
289
+ if (box.mesh && "triangles" in box.mesh) {
290
+ allTriangles.push(...box.mesh.triangles)
291
+ } else {
292
+ // Generate simple box mesh for this component
293
+ const boxTriangles = createBoxTriangles(box)
294
+ allTriangles.push(...boxTriangles)
295
+ }
296
+ }
297
+
298
+ // Create STEP faces from triangles if we have any
299
+ if (allTriangles.length > 0) {
300
+ // Transform triangles from GLTF XZ plane (Y=up) to STEP XY plane (Z=up)
301
+ const transformedTriangles = allTriangles.map((tri) => ({
302
+ vertices: tri.vertices.map((v) => ({
303
+ x: v.x,
304
+ y: v.z, // GLTF Z becomes STEP Y
305
+ z: v.y, // GLTF Y becomes STEP Z
306
+ })),
307
+ normal: {
308
+ x: tri.normal.x,
309
+ y: tri.normal.z, // GLTF Z becomes STEP Y
310
+ z: tri.normal.y, // GLTF Y becomes STEP Z
311
+ },
312
+ }))
313
+ const componentFaces = createStepFacesFromTriangles(
314
+ repo,
315
+ transformedTriangles as any,
316
+ )
317
+
318
+ // Create closed shell and solid for components
319
+ const componentShell = repo.add(
320
+ new ClosedShell("", componentFaces as any),
321
+ )
322
+ const componentSolid = repo.add(
323
+ new ManifoldSolidBrep("Components", componentShell),
324
+ )
325
+ solids.push(componentSolid)
326
+ }
327
+ } catch (error) {
328
+ console.warn("Failed to generate component mesh:", error)
329
+ // Continue without components if generation fails
330
+ }
331
+
332
+ return solids
333
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "circuit-json-to-step",
3
+ "main": "dist/index.js",
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "pull-reference": "git clone https://github.com/tscircuit/circuit-json.git && find circuit-json/tests -name '*.test.ts' -exec bash -c 'mv \"$0\" \"${0%.test.ts}.ts\"' {} \\; && git clone https://github.com/tscircuit/stepts.git && find stepts/tests -name '*.test.ts' -exec bash -c 'mv \"$0\" \"${0%.test.ts}.ts\"' {} \\;",
8
+ "build": "tsup-node ./lib/index.ts --format esm --dts",
9
+ "format": "biome format --write .",
10
+ "format:check": "biome format ."
11
+ },
12
+ "devDependencies": {
13
+ "@types/bun": "latest",
14
+ "circuit-json": "^0.0.272",
15
+ "occt-import-js": "^0.0.23",
16
+ "stepts": "^0.0.1",
17
+ "tsup": "^8.5.0"
18
+ },
19
+ "peerDependencies": {
20
+ "typescript": "^5"
21
+ },
22
+ "dependencies": {
23
+ "@tscircuit/circuit-json-util": "^0.0.72",
24
+ "circuit-json-to-connectivity-map": "^0.0.22",
25
+ "circuit-json-to-gltf": "^0.0.19",
26
+ "circuit-to-svg": "^0.0.226",
27
+ "schematic-symbols": "^0.0.202"
28
+ }
29
+ }
@@ -0,0 +1,40 @@
1
+ [
2
+ {
3
+ "type": "pcb_board",
4
+ "pcb_board_id": "pcb_board_1",
5
+ "width": 20,
6
+ "height": 15,
7
+ "thickness": 1.6,
8
+ "center": { "x": 10, "y": 7.5 },
9
+ "outline": [
10
+ { "x": 0, "y": 0 },
11
+ { "x": 20, "y": 0 },
12
+ { "x": 20, "y": 15 },
13
+ { "x": 0, "y": 15 }
14
+ ]
15
+ },
16
+ {
17
+ "type": "pcb_hole",
18
+ "pcb_hole_id": "pcb_hole_1",
19
+ "hole_shape": "circle",
20
+ "hole_diameter": 1.0,
21
+ "x": 5,
22
+ "y": 5
23
+ },
24
+ {
25
+ "type": "pcb_hole",
26
+ "pcb_hole_id": "pcb_hole_2",
27
+ "hole_shape": "circle",
28
+ "hole_diameter": 1.5,
29
+ "x": 15,
30
+ "y": 5
31
+ },
32
+ {
33
+ "type": "pcb_hole",
34
+ "pcb_hole_id": "pcb_hole_3",
35
+ "hole_shape": "circle",
36
+ "hole_diameter": 2.0,
37
+ "x": 10,
38
+ "y": 10
39
+ }
40
+ ]
@@ -0,0 +1,54 @@
1
+ import { writeFileSync } from "node:fs"
2
+ import { test, expect } from "bun:test"
3
+ import { circuitJsonToStep } from "../../../lib/index"
4
+ import { importStepWithOcct } from "../../utils/occt/importer"
5
+ import circuitJson from "./basics01.json"
6
+
7
+ test("basics01: convert circuit json with circular holes to STEP", async () => {
8
+ const stepText = await circuitJsonToStep(circuitJson as any, {
9
+ boardWidth: 20,
10
+ boardHeight: 15,
11
+ boardThickness: 1.6,
12
+ productName: "TestPCB",
13
+ })
14
+
15
+ // Verify STEP format
16
+ expect(stepText).toContain("ISO-10303-21")
17
+ expect(stepText).toContain("END-ISO-10303-21")
18
+
19
+ // Verify product structure
20
+ expect(stepText).toContain("TestPCB")
21
+ expect(stepText).toContain("MANIFOLD_SOLID_BREP")
22
+
23
+ // Verify holes are created (should have CIRCLE and CYLINDRICAL_SURFACE entities)
24
+ expect(stepText).toContain("CIRCLE")
25
+ expect(stepText).toContain("CYLINDRICAL_SURFACE")
26
+
27
+ // Count CIRCLE occurrences - should have 12 (3 holes × 4 circles each: 2 for top/bottom faces, 2 for cylindrical surface)
28
+ const circleCount = (stepText.match(/CIRCLE/g) || []).length
29
+ expect(circleCount).toBe(12)
30
+
31
+ // Count CYLINDRICAL_SURFACE occurrences - should have 3 (one per hole)
32
+ const cylinderCount = (stepText.match(/CYLINDRICAL_SURFACE/g) || []).length
33
+ expect(cylinderCount).toBe(3)
34
+
35
+ // Write STEP file to debug-output
36
+ const outputPath = "debug-output/basics01.step"
37
+ writeFileSync(outputPath, stepText)
38
+
39
+ console.log("✓ STEP file generated successfully")
40
+ console.log(` - Circles created: ${circleCount}`)
41
+ console.log(` - STEP text length: ${stepText.length} bytes`)
42
+ console.log(` - Output: ${outputPath}`)
43
+
44
+ // Validate STEP file can be imported with occt-import-js
45
+ const occtResult = await importStepWithOcct(stepText)
46
+ expect(occtResult.success).toBe(true)
47
+ expect(occtResult.meshes.length).toBeGreaterThan(0)
48
+
49
+ const [firstMesh] = occtResult.meshes
50
+ expect(firstMesh.attributes.position.array.length).toBeGreaterThan(0)
51
+ expect(firstMesh.index.array.length).toBeGreaterThan(0)
52
+
53
+ console.log("✓ STEP file successfully validated with occt-import-js")
54
+ }, 20000)
@@ -0,0 +1,19 @@
1
+ [
2
+ {
3
+ "type": "pcb_board",
4
+ "pcb_board_id": "pcb_board_1",
5
+ "width": 20,
6
+ "height": 15,
7
+ "thickness": 1.6,
8
+ "center": { "x": 10, "y": 7.5 },
9
+ "outline": [
10
+ { "x": 2, "y": 3 },
11
+ { "x": 18, "y": 3 },
12
+ { "x": 18, "y": 7 },
13
+ { "x": 20, "y": 7 },
14
+ { "x": 10, "y": 15 },
15
+ { "x": 0, "y": 7 },
16
+ { "x": 2, "y": 7 }
17
+ ]
18
+ }
19
+ ]
@@ -0,0 +1,38 @@
1
+ import { writeFileSync } from "node:fs"
2
+ import { test, expect } from "bun:test"
3
+ import { circuitJsonToStep } from "../../../lib/index"
4
+ import { importStepWithOcct } from "../../utils/occt/importer"
5
+ import circuitJson from "./basics02.json"
6
+
7
+ test("basics02: convert pcb_board with outline only to STEP", async () => {
8
+ const stepText = await circuitJsonToStep(circuitJson as any, {
9
+ productName: "TestPCB_Outline",
10
+ })
11
+
12
+ // Verify STEP format
13
+ expect(stepText).toContain("ISO-10303-21")
14
+ expect(stepText).toContain("END-ISO-10303-21")
15
+
16
+ // Verify product structure
17
+ expect(stepText).toContain("TestPCB_Outline")
18
+ expect(stepText).toContain("MANIFOLD_SOLID_BREP")
19
+
20
+ // Write STEP file to debug-output
21
+ const outputPath = "debug-output/basics02.step"
22
+ writeFileSync(outputPath, stepText)
23
+
24
+ console.log("✓ STEP file generated successfully")
25
+ console.log(` - STEP text length: ${stepText.length} bytes`)
26
+ console.log(` - Output: ${outputPath}`)
27
+
28
+ // Validate STEP file can be imported with occt-import-js
29
+ const occtResult = await importStepWithOcct(stepText)
30
+ expect(occtResult.success).toBe(true)
31
+ expect(occtResult.meshes.length).toBeGreaterThan(0)
32
+
33
+ const [firstMesh] = occtResult.meshes
34
+ expect(firstMesh.attributes.position.array.length).toBeGreaterThan(0)
35
+ expect(firstMesh.index.array.length).toBeGreaterThan(0)
36
+
37
+ console.log("✓ STEP file successfully validated with occt-import-js")
38
+ }, 20000)
@@ -0,0 +1,49 @@
1
+ [
2
+ {
3
+ "type": "pcb_board",
4
+ "pcb_board_id": "pcb_board_1",
5
+ "width": 20,
6
+ "height": 15,
7
+ "thickness": 1.6,
8
+ "center": { "x": 10, "y": 7.5 },
9
+ "outline": [
10
+ { "x": 2, "y": 3 },
11
+ { "x": 18, "y": 3 },
12
+ { "x": 18, "y": 7 },
13
+ { "x": 20, "y": 7 },
14
+ { "x": 10, "y": 15 },
15
+ { "x": 0, "y": 7 },
16
+ { "x": 2, "y": 7 }
17
+ ]
18
+ },
19
+ {
20
+ "type": "pcb_plated_hole",
21
+ "pcb_plated_hole_id": "pcb_plated_hole_1",
22
+ "shape": "circle",
23
+ "outer_diameter": 2.0,
24
+ "hole_diameter": 1.0,
25
+ "x": 5,
26
+ "y": 5,
27
+ "layers": ["top", "bottom"]
28
+ },
29
+ {
30
+ "type": "pcb_plated_hole",
31
+ "pcb_plated_hole_id": "pcb_plated_hole_2",
32
+ "shape": "circle",
33
+ "outer_diameter": 2.5,
34
+ "hole_diameter": 1.5,
35
+ "x": 15,
36
+ "y": 5,
37
+ "layers": ["top", "bottom"]
38
+ },
39
+ {
40
+ "type": "pcb_plated_hole",
41
+ "pcb_plated_hole_id": "pcb_plated_hole_3",
42
+ "shape": "circle",
43
+ "outer_diameter": 3.0,
44
+ "hole_diameter": 2.0,
45
+ "x": 10,
46
+ "y": 10,
47
+ "layers": ["top", "bottom"]
48
+ }
49
+ ]
@@ -0,0 +1,38 @@
1
+ import { expect, test } from "bun:test"
2
+ import { writeFileSync } from "node:fs"
3
+ import { circuitJsonToStep } from "../../../lib/index"
4
+ import { importStepWithOcct } from "../../utils/occt/importer"
5
+ import circuitJson from "./basics03.json"
6
+
7
+ test("basics03: convert pcb_board with arrow outline and pcb_plated_holes to STEP", async () => {
8
+ const stepText = await circuitJsonToStep(circuitJson as any, {
9
+ productName: "TestPCB_ArrowWithPlatedHoles",
10
+ })
11
+
12
+ // Verify STEP format
13
+ expect(stepText).toContain("ISO-10303-21")
14
+ expect(stepText).toContain("END-ISO-10303-21")
15
+
16
+ // Verify product structure
17
+ expect(stepText).toContain("TestPCB_ArrowWithPlatedHoles")
18
+ expect(stepText).toContain("MANIFOLD_SOLID_BREP")
19
+
20
+ // Write STEP file to debug-output
21
+ const outputPath = "debug-output/basics03.step"
22
+ writeFileSync(outputPath, stepText)
23
+
24
+ console.log("✓ STEP file generated successfully")
25
+ console.log(` - STEP text length: ${stepText.length} bytes`)
26
+ console.log(` - Output: ${outputPath}`)
27
+
28
+ // Validate STEP file can be imported with occt-import-js
29
+ const occtResult = await importStepWithOcct(stepText)
30
+ expect(occtResult.success).toBe(true)
31
+ expect(occtResult.meshes.length).toBeGreaterThan(0)
32
+
33
+ const [firstMesh] = occtResult.meshes
34
+ expect(firstMesh.attributes.position.array.length).toBeGreaterThan(0)
35
+ expect(firstMesh.index.array.length).toBeGreaterThan(0)
36
+
37
+ console.log("✓ STEP file successfully validated with occt-import-js")
38
+ }, 20000)
@@ -0,0 +1,52 @@
1
+ [
2
+ {
3
+ "type": "pcb_board",
4
+ "pcb_board_id": "pcb_board_1",
5
+ "width": 30,
6
+ "height": 20,
7
+ "thickness": 1.6,
8
+ "center": { "x": 15, "y": 10 }
9
+ },
10
+ {
11
+ "type": "pcb_hole",
12
+ "pcb_hole_id": "pcb_hole_1",
13
+ "hole_shape": "circle",
14
+ "hole_diameter": 1.0,
15
+ "x": 5,
16
+ "y": 5
17
+ },
18
+ {
19
+ "type": "source_component",
20
+ "source_component_id": "source_component_1",
21
+ "name": "R1",
22
+ "supplier_part_numbers": {},
23
+ "ftype": "simple_resistor"
24
+ },
25
+ {
26
+ "type": "pcb_component",
27
+ "pcb_component_id": "pcb_component_1",
28
+ "source_component_id": "source_component_1",
29
+ "center": { "x": 15, "y": 10 },
30
+ "width": 3,
31
+ "height": 1.5,
32
+ "layer": "top",
33
+ "rotation": 0
34
+ },
35
+ {
36
+ "type": "source_component",
37
+ "source_component_id": "source_component_2",
38
+ "name": "C1",
39
+ "supplier_part_numbers": {},
40
+ "ftype": "simple_capacitor"
41
+ },
42
+ {
43
+ "type": "pcb_component",
44
+ "pcb_component_id": "pcb_component_2",
45
+ "source_component_id": "source_component_2",
46
+ "center": { "x": 20, "y": 10 },
47
+ "width": 2,
48
+ "height": 1.25,
49
+ "layer": "top",
50
+ "rotation": 90
51
+ }
52
+ ]