circuit-json-to-tscircuit 0.0.91 → 0.0.93

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.
Files changed (72) hide show
  1. package/dist/{chunk-TYZGD7TY.js → chunk-B7377FUZ.js} +1482 -199
  2. package/dist/cli/main.js +5 -2
  3. package/dist/lib/index.js +1 -1
  4. package/lib/board-conversion-context.ts +21 -6
  5. package/lib/board-conversion-types.ts +2 -0
  6. package/lib/board-converter/BoardConverterContext.ts +18 -0
  7. package/lib/board-converter/convert-board-circuit-json.ts +80 -0
  8. package/lib/board-converter/create-component-preserving-board-conversion-context.ts +98 -0
  9. package/lib/board-converter/get-board-child-runtime-names.ts +129 -0
  10. package/lib/board-converter/get-pcb-component-render-placement.ts +102 -0
  11. package/lib/board-converter/get-pin-labels.ts +99 -0
  12. package/lib/board-converter/get-schematic-components-for-pcb-component.ts +50 -0
  13. package/lib/board-converter/localize-pcb-component-elements.ts +270 -0
  14. package/lib/board-converter/stages/convert-board-cad-models.ts +36 -0
  15. package/lib/board-converter/stages/convert-board-nets.ts +19 -0
  16. package/lib/board-converter/stages/convert-board-properties.ts +49 -0
  17. package/lib/board-converter/stages/convert-board-schematic.ts +16 -0
  18. package/lib/board-converter/stages/convert-board-traces.ts +12 -0
  19. package/lib/board-converter/stages/convert-copper-pours.ts +12 -0
  20. package/lib/board-converter/stages/convert-pcb-components.ts +190 -0
  21. package/lib/board-converter/stages/convert-standalone-pcb-primitives.ts +46 -0
  22. package/lib/format-jsx-string-attribute.ts +9 -0
  23. package/lib/generate-board-cad-models-tsx/converter-types.ts +6 -0
  24. package/lib/generate-board-cad-models-tsx/execute-jscad-operation.ts +7 -0
  25. package/lib/generate-board-cad-models-tsx/generate-cad-model-group-model-tsx.ts +68 -0
  26. package/lib/generate-board-cad-models-tsx/generate-cad-model-group-tsx.ts +24 -19
  27. package/lib/generate-board-cad-models-tsx/generate-cad-model-tsx.ts +6 -33
  28. package/lib/generate-board-cad-models-tsx/generate-jscad-cad-model-prop-tsx.ts +113 -0
  29. package/lib/generate-board-cad-models-tsx/get-cad-model-groups.ts +3 -2
  30. package/lib/generate-board-cad-models-tsx/get-cad-model-placement-attributes.ts +50 -0
  31. package/lib/generate-board-cad-models-tsx/get-jscad-operation-fit-scale-factors.ts +66 -0
  32. package/lib/generate-board-cad-models-tsx/get-jscad-operation-in-base-model-frame.ts +179 -0
  33. package/lib/generate-board-cad-models-tsx/has-cad-model-source.ts +9 -0
  34. package/lib/generate-board-cad-models-tsx/is-jscad-cad-model-group-translucent.ts +15 -0
  35. package/lib/generate-board-cad-models-tsx/pack-jscad-operations.ts +69 -0
  36. package/lib/generate-board-cad-models-tsx.ts +13 -2
  37. package/lib/generate-board-nets-tsx.ts +32 -2
  38. package/lib/generate-board-traces-tsx.ts +28 -0
  39. package/lib/generate-footprint-tsx/get-imported-port-hints.ts +10 -1
  40. package/lib/get-board-using-template.ts +3 -95
  41. package/lib/get-runtime-net-names.ts +93 -5
  42. package/package.json +4 -1
  43. package/tests/board-autorouted-pad-connectivity.test.tsx +153 -0
  44. package/tests/board-bottom-cad-model-rotation.test.ts +5 -5
  45. package/tests/board-bottom-jscad-cad-model.test.ts +67 -0
  46. package/tests/board-component-cad-models-on-multiple-layers.test.ts +81 -0
  47. package/tests/board-connected-net-aliases.test.ts +56 -0
  48. package/tests/board-duplicate-component-names.test.tsx +60 -0
  49. package/tests/board-grouped-jscad-cad-model-fit.test.ts +111 -0
  50. package/tests/board-grouped-jscad-cad-model.test.ts +125 -0
  51. package/tests/board-imported-board-name-collision.test.ts +74 -0
  52. package/tests/board-imported-cad-model-name-collision.test.ts +86 -0
  53. package/tests/board-jscad-cad-model.test.ts +93 -0
  54. package/tests/board-mixed-url-and-jscad-cad-models.test.ts +139 -0
  55. package/tests/board-near-zero-local-coordinates.test.ts +65 -0
  56. package/tests/board-net-endpoint-order.test.ts +135 -0
  57. package/tests/board-net-name-collision.test.ts +1 -1
  58. package/tests/board-net-routing-order.test.ts +50 -0
  59. package/tests/board-nonnumeric-port-placement.test.ts +130 -0
  60. package/tests/board-off-center-footprint-cad-model.test.ts +82 -0
  61. package/tests/board-rotated-pad-orientation.test.ts +120 -0
  62. package/tests/board-rotated-pill-cad-model-placement.test.ts +93 -0
  63. package/tests/board-standalone-primitives-do-not-obstruct.test.ts +34 -0
  64. package/tests/test20-board-conversion-drops-children.test.tsx +31 -30
  65. package/tests/ti-evms/__snapshots__/ti-drv8307evm-roundtrip-pcb-comparison.snap.svg +1 -1
  66. package/tests/ti-evms/__snapshots__/ti-drv8307evm-roundtrip.test.tsx.snap +4361 -4109
  67. package/tests/ti-evms/__snapshots__/ti-lm251772evm-pd-roundtrip-pcb-comparison.snap.svg +1 -1
  68. package/tests/ti-evms/__snapshots__/ti-lm251772evm-pd-roundtrip.test.tsx.snap +5557 -5171
  69. package/tests/ti-evms/__snapshots__/ti-lm5155evm-fly-roundtrip-pcb-comparison.snap.svg +1 -1
  70. package/tests/ti-evms/__snapshots__/ti-lmg342x-bb-evm-roundtrip-pcb-comparison.snap.svg +1 -1
  71. package/tests/ti-evms/__snapshots__/ti-lmg342x-bb-evm-roundtrip.test.tsx.snap +4592 -4226
  72. package/tests/ti-evms/ti-lm5155evm-fly-roundtrip.test.tsx +2823 -2608
@@ -1,19 +1,26 @@
1
- // lib/get-board-using-template.ts
2
- import { mmStr as mmStr9 } from "@tscircuit/mm";
3
- import { su as su21 } from "@tscircuit/soup-util";
4
-
5
1
  // lib/get-runtime-net-names.ts
6
2
  var getRuntimeNetNames = (circuitJson, sourceNetIdsRequiringSelector) => {
7
3
  const sourceNets = circuitJson.filter(
8
4
  (element) => element.type === "source_net"
9
5
  );
6
+ const canonicalSourceNetIdBySourceNetId = getCanonicalSourceNetIdBySourceNetId(circuitJson);
7
+ const sourceNetById = new Map(
8
+ sourceNets.map((sourceNet) => [sourceNet.source_net_id, sourceNet])
9
+ );
10
+ const canonicalSourceNets = sourceNets.filter(
11
+ (sourceNet) => canonicalSourceNetIdBySourceNetId.get(sourceNet.source_net_id) === sourceNet.source_net_id
12
+ );
10
13
  const sourceNetNamesRequiringSelector = /* @__PURE__ */ new Set();
11
- for (const sourceNet of sourceNets) {
12
- if (sourceNetIdsRequiringSelector.has(sourceNet.source_net_id)) {
13
- sourceNetNamesRequiringSelector.add(sourceNet.name);
14
+ for (const sourceNetId of sourceNetIdsRequiringSelector) {
15
+ const canonicalSourceNetId = canonicalSourceNetIdBySourceNetId.get(sourceNetId) ?? sourceNetId;
16
+ const canonicalSourceNet = sourceNetById.get(canonicalSourceNetId);
17
+ if (canonicalSourceNet) {
18
+ sourceNetNamesRequiringSelector.add(canonicalSourceNet.name);
14
19
  }
15
20
  }
16
- const sourceNetNames = [...new Set(sourceNets.map(({ name }) => name))];
21
+ const sourceNetNames = [
22
+ ...new Set(canonicalSourceNets.map(({ name }) => name))
23
+ ];
17
24
  const runtimeNameByOriginalName = /* @__PURE__ */ new Map();
18
25
  const usedRuntimeNetNames = /* @__PURE__ */ new Set();
19
26
  const namesThatRemainUnchanged = sourceNetNames.filter(
@@ -36,13 +43,74 @@ var getRuntimeNetNames = (circuitJson, sourceNetIdsRequiringSelector) => {
36
43
  }
37
44
  const runtimeNetNameBySourceNetId = /* @__PURE__ */ new Map();
38
45
  for (const sourceNet of sourceNets) {
46
+ const canonicalSourceNetId = canonicalSourceNetIdBySourceNetId.get(sourceNet.source_net_id) ?? sourceNet.source_net_id;
47
+ const canonicalSourceNet = sourceNetById.get(canonicalSourceNetId);
48
+ if (!canonicalSourceNet) continue;
39
49
  runtimeNetNameBySourceNetId.set(
40
50
  sourceNet.source_net_id,
41
- runtimeNameByOriginalName.get(sourceNet.name)
51
+ runtimeNameByOriginalName.get(canonicalSourceNet.name)
42
52
  );
43
53
  }
44
54
  return runtimeNetNameBySourceNetId;
45
55
  };
56
+ var getCanonicalSourceNetIdBySourceNetId = (circuitJson) => {
57
+ const parentSourceNetIdBySourceNetId = /* @__PURE__ */ new Map();
58
+ for (const element of circuitJson) {
59
+ if (element.type !== "source_net") continue;
60
+ parentSourceNetIdBySourceNetId.set(
61
+ element.source_net_id,
62
+ element.source_net_id
63
+ );
64
+ }
65
+ for (const element of circuitJson) {
66
+ if (element.type !== "source_trace") continue;
67
+ const sourceNetIds = element.connected_source_net_ids.filter(
68
+ (sourceNetId) => parentSourceNetIdBySourceNetId.has(sourceNetId)
69
+ );
70
+ const firstSourceNetId = sourceNetIds[0];
71
+ if (!firstSourceNetId) continue;
72
+ const canonicalSourceNetId = findCanonicalSourceNetId({
73
+ parentSourceNetIdBySourceNetId,
74
+ sourceNetId: firstSourceNetId
75
+ });
76
+ for (const sourceNetId of sourceNetIds.slice(1)) {
77
+ const aliasCanonicalSourceNetId = findCanonicalSourceNetId({
78
+ parentSourceNetIdBySourceNetId,
79
+ sourceNetId
80
+ });
81
+ if (aliasCanonicalSourceNetId === canonicalSourceNetId) continue;
82
+ parentSourceNetIdBySourceNetId.set(
83
+ aliasCanonicalSourceNetId,
84
+ canonicalSourceNetId
85
+ );
86
+ }
87
+ }
88
+ return new Map(
89
+ [...parentSourceNetIdBySourceNetId.keys()].map((sourceNetId) => [
90
+ sourceNetId,
91
+ findCanonicalSourceNetId({
92
+ parentSourceNetIdBySourceNetId,
93
+ sourceNetId
94
+ })
95
+ ])
96
+ );
97
+ };
98
+ var findCanonicalSourceNetId = ({
99
+ parentSourceNetIdBySourceNetId,
100
+ sourceNetId
101
+ }) => {
102
+ let currentSourceNetId = sourceNetId;
103
+ const visitedSourceNetIds = /* @__PURE__ */ new Set();
104
+ while (!visitedSourceNetIds.has(currentSourceNetId)) {
105
+ visitedSourceNetIds.add(currentSourceNetId);
106
+ const parentSourceNetId = parentSourceNetIdBySourceNetId.get(currentSourceNetId);
107
+ if (!parentSourceNetId || parentSourceNetId === currentSourceNetId) {
108
+ return currentSourceNetId;
109
+ }
110
+ currentSourceNetId = parentSourceNetId;
111
+ }
112
+ return currentSourceNetId;
113
+ };
46
114
  var getUniqueRuntimeNetName = (sourceNetName, usedRuntimeNetNames) => {
47
115
  const baseName = getRuntimeNetNameBase(sourceNetName);
48
116
  let candidate = baseName;
@@ -59,8 +127,11 @@ var getRuntimeNetNameBase = (sourceNetName) => {
59
127
  };
60
128
 
61
129
  // lib/board-conversion-context.ts
62
- var PCB_CHIP_NAME = "ImportedBoard";
63
- var createBoardConversionContext = (circuitJson) => {
130
+ var DEFAULT_PCB_CHIP_NAME = "ImportedBoard";
131
+ var createBoardConversionContext = ({
132
+ circuitJson,
133
+ pcbChipName = DEFAULT_PCB_CHIP_NAME
134
+ }) => {
64
135
  const sourceNetIdsRequiringSelector = getSourceNetIdsRequiringSelector(circuitJson);
65
136
  const runtimeNetNameBySourceNetId = getRuntimeNetNames(
66
137
  circuitJson,
@@ -78,22 +149,29 @@ var createBoardConversionContext = (circuitJson) => {
78
149
  circuitJson,
79
150
  runtimeNetNameBySourceNetId
80
151
  });
152
+ const pcbPortSelectorsBySourcePortId = /* @__PURE__ */ new Map();
81
153
  const pcbPortSelectorsByRuntimeNetName = /* @__PURE__ */ new Map();
82
154
  for (const [pcbPortId, sourcePortId] of sourcePortIdByPcbPortId) {
83
155
  const portHint = portHintByPcbPortId.get(pcbPortId);
84
156
  if (!portHint) continue;
157
+ const selector = `.${pcbChipName} > .${portHint}`;
158
+ const sourcePortSelectors = pcbPortSelectorsBySourcePortId.get(sourcePortId) ?? [];
159
+ sourcePortSelectors.push(selector);
160
+ pcbPortSelectorsBySourcePortId.set(sourcePortId, sourcePortSelectors);
85
161
  for (const runtimeNetName of runtimeNetNamesBySourcePortId.get(
86
162
  sourcePortId
87
163
  ) ?? []) {
88
164
  const selectors = pcbPortSelectorsByRuntimeNetName.get(runtimeNetName) ?? [];
89
- selectors.push(`.${PCB_CHIP_NAME} > .${portHint}`);
165
+ selectors.push(selector);
90
166
  pcbPortSelectorsByRuntimeNetName.set(runtimeNetName, selectors);
91
167
  }
92
168
  }
93
169
  return {
94
- pcbChipName: PCB_CHIP_NAME,
170
+ pcbChipName,
171
+ pcbPortIdsWithUnambiguousComponentSelectors: /* @__PURE__ */ new Set(),
95
172
  portHintByPcbPortId,
96
173
  runtimeNetNameBySourceNetId,
174
+ pcbPortSelectorsBySourcePortId,
97
175
  pcbPortSelectorsByRuntimeNetName
98
176
  };
99
177
  };
@@ -140,6 +218,206 @@ var getRuntimeNetNamesBySourcePortId = ({
140
218
  return result;
141
219
  };
142
220
 
221
+ // lib/board-converter/create-component-preserving-board-conversion-context.ts
222
+ function createComponentPreservingBoardConversionContext({
223
+ circuitJson,
224
+ pcbChipName,
225
+ pcbComponentRuntimeNameByPcbComponentId
226
+ }) {
227
+ const context = createBoardConversionContext({ circuitJson, pcbChipName });
228
+ const pcbComponentsById = /* @__PURE__ */ new Map();
229
+ for (const element of circuitJson) {
230
+ if (element.type === "pcb_component") {
231
+ pcbComponentsById.set(element.pcb_component_id, element);
232
+ }
233
+ }
234
+ const pcbPortSelectorsBySourcePortId = /* @__PURE__ */ new Map();
235
+ const pcbPortIdsWithUnambiguousComponentSelectors = new Set(
236
+ circuitJson.flatMap((element) => {
237
+ if (element.type !== "pcb_port") return [];
238
+ const pcbComponent = element.pcb_component_id ? pcbComponentsById.get(element.pcb_component_id) : void 0;
239
+ return pcbComponent && pcbComponentRuntimeNameByPcbComponentId.has(
240
+ pcbComponent.pcb_component_id
241
+ ) ? [element.pcb_port_id] : [];
242
+ })
243
+ );
244
+ for (const element of circuitJson) {
245
+ if (element.type !== "pcb_port") continue;
246
+ const pcbComponent = element.pcb_component_id ? pcbComponentsById.get(element.pcb_component_id) : void 0;
247
+ const portHint = context.portHintByPcbPortId.get(element.pcb_port_id);
248
+ if (!portHint) continue;
249
+ const componentName = pcbComponent ? pcbComponentRuntimeNameByPcbComponentId.get(
250
+ pcbComponent.pcb_component_id
251
+ ) : void 0;
252
+ const selectors = pcbPortSelectorsBySourcePortId.get(element.source_port_id) ?? [];
253
+ selectors.push(`.${componentName ?? context.pcbChipName} > .${portHint}`);
254
+ pcbPortSelectorsBySourcePortId.set(element.source_port_id, selectors);
255
+ }
256
+ const pcbPortSelectorsByRuntimeNetName = /* @__PURE__ */ new Map();
257
+ for (const element of circuitJson) {
258
+ if (element.type !== "source_trace") continue;
259
+ for (const sourceNetId of element.connected_source_net_ids) {
260
+ const runtimeNetName = context.runtimeNetNameBySourceNetId.get(sourceNetId);
261
+ if (!runtimeNetName) continue;
262
+ const selectors = pcbPortSelectorsByRuntimeNetName.get(runtimeNetName) ?? [];
263
+ for (const sourcePortId of element.connected_source_port_ids) {
264
+ for (const selector of pcbPortSelectorsBySourcePortId.get(
265
+ sourcePortId
266
+ ) ?? []) {
267
+ if (!selectors.includes(selector)) selectors.push(selector);
268
+ }
269
+ }
270
+ pcbPortSelectorsByRuntimeNetName.set(runtimeNetName, selectors);
271
+ }
272
+ }
273
+ return {
274
+ ...context,
275
+ pcbPortIdsWithUnambiguousComponentSelectors,
276
+ pcbPortSelectorsBySourcePortId,
277
+ pcbPortSelectorsByRuntimeNetName
278
+ };
279
+ }
280
+
281
+ // lib/generate-board-cad-models-tsx/get-cad-model-url.ts
282
+ function getCadModelUrl(cadComponent) {
283
+ if (cadComponent.model_glb_url) {
284
+ return { format: "glb", url: cadComponent.model_glb_url };
285
+ }
286
+ if (cadComponent.model_gltf_url) {
287
+ return { format: "gltf", url: cadComponent.model_gltf_url };
288
+ }
289
+ if (cadComponent.model_obj_url) {
290
+ return { format: "obj", url: cadComponent.model_obj_url };
291
+ }
292
+ if (cadComponent.model_step_url) {
293
+ return { format: "step", url: cadComponent.model_step_url };
294
+ }
295
+ if (cadComponent.model_stl_url) {
296
+ return { format: "stl", url: cadComponent.model_stl_url };
297
+ }
298
+ if (cadComponent.model_wrl_url) {
299
+ return { format: "wrl", url: cadComponent.model_wrl_url };
300
+ }
301
+ return void 0;
302
+ }
303
+
304
+ // lib/generate-board-cad-models-tsx/has-cad-model-source.ts
305
+ function hasCadModelSource(cadComponent) {
306
+ return cadComponent.model_jscad !== void 0 || getCadModelUrl(cadComponent) !== void 0;
307
+ }
308
+
309
+ // lib/generate-board-cad-models-tsx/get-cad-model-groups.ts
310
+ function getCadModelGroups(circuitJson) {
311
+ const pcbComponentById = /* @__PURE__ */ new Map();
312
+ for (const element of circuitJson) {
313
+ if (element.type === "pcb_component") {
314
+ pcbComponentById.set(element.pcb_component_id, element);
315
+ }
316
+ }
317
+ const cadModelGroupByKey = /* @__PURE__ */ new Map();
318
+ for (const element of circuitJson) {
319
+ if (element.type !== "cad_component" || !hasCadModelSource(element))
320
+ continue;
321
+ const pcbComponent = element.pcb_component_id ? pcbComponentById.get(element.pcb_component_id) : void 0;
322
+ const layer = (element.layer ?? pcbComponent?.layer) === "bottom" ? "bottom" : "top";
323
+ let parentPlacement = {
324
+ center: element.position,
325
+ ccwRotationDegrees: 0,
326
+ layer
327
+ };
328
+ let cadModelGroupKey = element.cad_component_id;
329
+ if (pcbComponent !== void 0) {
330
+ parentPlacement = {
331
+ center: pcbComponent.center,
332
+ ccwRotationDegrees: pcbComponent.rotation,
333
+ layer
334
+ };
335
+ cadModelGroupKey = `${pcbComponent.pcb_component_id}:${layer}`;
336
+ }
337
+ const cadModelGroup = cadModelGroupByKey.get(cadModelGroupKey) ?? {
338
+ cadComponents: [],
339
+ parentPlacement
340
+ };
341
+ cadModelGroup.cadComponents.push(element);
342
+ cadModelGroupByKey.set(cadModelGroupKey, cadModelGroup);
343
+ }
344
+ return [...cadModelGroupByKey.values()];
345
+ }
346
+
347
+ // lib/board-converter/get-board-child-runtime-names.ts
348
+ var getUnusedRuntimeName = ({
349
+ preferredRuntimeName,
350
+ usedRuntimeNames
351
+ }) => {
352
+ if (!usedRuntimeNames.has(preferredRuntimeName)) return preferredRuntimeName;
353
+ let duplicateNumber = 2;
354
+ while (usedRuntimeNames.has(`${preferredRuntimeName}_${duplicateNumber}`)) {
355
+ duplicateNumber += 1;
356
+ }
357
+ return `${preferredRuntimeName}_${duplicateNumber}`;
358
+ };
359
+ var getBoardChildRuntimeNames = (circuitJson) => {
360
+ const sourceComponentBySourceComponentId = /* @__PURE__ */ new Map();
361
+ for (const element of circuitJson) {
362
+ if (element.type === "source_component" && !sourceComponentBySourceComponentId.has(element.source_component_id)) {
363
+ sourceComponentBySourceComponentId.set(
364
+ element.source_component_id,
365
+ element
366
+ );
367
+ }
368
+ }
369
+ const usedRuntimeNames = /* @__PURE__ */ new Set();
370
+ const pcbComponentRuntimeNameByPcbComponentId = /* @__PURE__ */ new Map();
371
+ const pcbComponents = circuitJson.filter(
372
+ (element) => element.type === "pcb_component"
373
+ );
374
+ for (const [pcbComponentIndex, pcbComponent] of pcbComponents.entries()) {
375
+ const sourceComponent = pcbComponent.source_component_id ? sourceComponentBySourceComponentId.get(pcbComponent.source_component_id) : void 0;
376
+ const preferredRuntimeName = sourceComponent?.name ?? `imported_component_${pcbComponentIndex + 1}`;
377
+ const runtimeName = getUnusedRuntimeName({
378
+ preferredRuntimeName,
379
+ usedRuntimeNames
380
+ });
381
+ usedRuntimeNames.add(runtimeName);
382
+ pcbComponentRuntimeNameByPcbComponentId.set(
383
+ pcbComponent.pcb_component_id,
384
+ runtimeName
385
+ );
386
+ }
387
+ const pcbChipName = getUnusedRuntimeName({
388
+ preferredRuntimeName: DEFAULT_PCB_CHIP_NAME,
389
+ usedRuntimeNames
390
+ });
391
+ usedRuntimeNames.add(pcbChipName);
392
+ const standaloneCadModelRuntimeNameByCadComponentId = /* @__PURE__ */ new Map();
393
+ const standaloneCadModelGroups = getCadModelGroups(circuitJson).filter(
394
+ (cadModelGroup) => cadModelGroup.cadComponents.every(
395
+ (cadComponent) => cadComponent.pcb_component_id === void 0
396
+ )
397
+ );
398
+ for (const [
399
+ groupIndex,
400
+ cadModelGroup
401
+ ] of standaloneCadModelGroups.entries()) {
402
+ const firstCadComponent = cadModelGroup.cadComponents[0];
403
+ if (!firstCadComponent) continue;
404
+ const runtimeName = getUnusedRuntimeName({
405
+ preferredRuntimeName: `ImportedCadModel${groupIndex + 1}`,
406
+ usedRuntimeNames
407
+ });
408
+ usedRuntimeNames.add(runtimeName);
409
+ standaloneCadModelRuntimeNameByCadComponentId.set(
410
+ firstCadComponent.cad_component_id,
411
+ runtimeName
412
+ );
413
+ }
414
+ return {
415
+ pcbChipName,
416
+ pcbComponentRuntimeNameByPcbComponentId,
417
+ standaloneCadModelRuntimeNameByCadComponentId
418
+ };
419
+ };
420
+
143
421
  // lib/generate-board-cad-models-tsx/format-point-prop.ts
144
422
  function formatPointProp(point) {
145
423
  return `{ x: ${point.x}, y: ${point.y}, z: ${point.z} }`;
@@ -192,27 +470,44 @@ function getCadModelPlacement({
192
470
  };
193
471
  }
194
472
 
195
- // lib/generate-board-cad-models-tsx/get-cad-model-url.ts
196
- function getCadModelUrl(cadComponent) {
197
- if (cadComponent.model_glb_url) {
198
- return { format: "glb", url: cadComponent.model_glb_url };
199
- }
200
- if (cadComponent.model_gltf_url) {
201
- return { format: "gltf", url: cadComponent.model_gltf_url };
202
- }
203
- if (cadComponent.model_obj_url) {
204
- return { format: "obj", url: cadComponent.model_obj_url };
473
+ // lib/generate-board-cad-models-tsx/get-cad-model-placement-attributes.ts
474
+ function getCadModelPlacementAttributes({
475
+ cadComponent,
476
+ parentPlacement,
477
+ pcbBoard
478
+ }) {
479
+ const placement = getCadModelPlacement({
480
+ cadComponent,
481
+ parentPlacement,
482
+ pcbBoard
483
+ });
484
+ const attributes = [
485
+ `positionOffset={${formatPointProp(placement.positionOffset)}}`,
486
+ `rotationOffset={${formatPointProp(placement.ccwRotationOffsetDegrees)}}`
487
+ ];
488
+ if (cadComponent.model_unit_to_mm_scale_factor !== void 0) {
489
+ attributes.push(
490
+ `modelUnitToMmScale={${cadComponent.model_unit_to_mm_scale_factor}}`
491
+ );
205
492
  }
206
- if (cadComponent.model_step_url) {
207
- return { format: "step", url: cadComponent.model_step_url };
493
+ if (cadComponent.model_board_normal_direction !== void 0) {
494
+ attributes.push(
495
+ `modelBoardNormalDirection={${JSON.stringify(cadComponent.model_board_normal_direction)}}`
496
+ );
208
497
  }
209
- if (cadComponent.model_stl_url) {
210
- return { format: "stl", url: cadComponent.model_stl_url };
498
+ const modelOriginPosition = getCadModelOriginPosition(cadComponent);
499
+ if (modelOriginPosition !== void 0) {
500
+ attributes.push(
501
+ `modelOriginPosition={${formatPointProp(modelOriginPosition)}}`
502
+ );
211
503
  }
212
- if (cadComponent.model_wrl_url) {
213
- return { format: "wrl", url: cadComponent.model_wrl_url };
504
+ if (cadComponent.size !== void 0) {
505
+ attributes.push(`size={${formatPointProp(cadComponent.size)}}`);
214
506
  }
215
- return void 0;
507
+ attributes.push(
508
+ `showAsTranslucentModel={${cadComponent.show_as_translucent_model === true}}`
509
+ );
510
+ return attributes;
216
511
  }
217
512
 
218
513
  // lib/generate-board-cad-models-tsx/get-model-url-with-format-hint.ts
@@ -233,141 +528,571 @@ function generateCadModelTsx({
233
528
  }) {
234
529
  const modelUrl = getCadModelUrl(cadComponent);
235
530
  if (!modelUrl) return void 0;
531
+ const attributes = [
532
+ `modelUrl={${JSON.stringify(getModelUrlWithFormatHint(modelUrl))}}`,
533
+ ...getCadModelPlacementAttributes({
534
+ cadComponent,
535
+ parentPlacement,
536
+ pcbBoard
537
+ })
538
+ ];
539
+ return `<cadmodel ${attributes.join(" ")} />`;
540
+ }
541
+
542
+ // lib/generate-board-cad-models-tsx/get-jscad-operation-fit-scale-factors.ts
543
+ import jscad2 from "@jscad/modeling";
544
+
545
+ // lib/generate-board-cad-models-tsx/execute-jscad-operation.ts
546
+ import jscad from "@jscad/modeling";
547
+ import { executeJscadOperations } from "jscad-planner";
548
+ function executeJscadOperation(operation) {
549
+ return executeJscadOperations(jscad, operation);
550
+ }
551
+
552
+ // lib/generate-board-cad-models-tsx/get-jscad-operation-fit-scale-factors.ts
553
+ var identityScaleFactors = { x: 1, y: 1, z: 1 };
554
+ function getSafeScaleFactor({
555
+ sourceLength,
556
+ targetLength
557
+ }) {
558
+ return sourceLength > 0 ? targetLength / sourceLength : 1;
559
+ }
560
+ function getJscadOperationFitScaleFactors({
561
+ cadComponent,
562
+ operation
563
+ }) {
564
+ if (cadComponent.size === void 0) return identityScaleFactors;
565
+ const geometry = executeJscadOperation(operation);
566
+ if (!jscad2.geometries.geom3.isA(geometry)) return identityScaleFactors;
567
+ const [minimumPoint, maximumPoint] = jscad2.measurements.measureBoundingBox(geometry);
568
+ const modelUnitToMmScaleFactor = cadComponent.model_unit_to_mm_scale_factor ?? 1;
569
+ const axisScaleFactors = {
570
+ x: getSafeScaleFactor({
571
+ sourceLength: maximumPoint[0] - minimumPoint[0],
572
+ targetLength: cadComponent.size.x * modelUnitToMmScaleFactor
573
+ }),
574
+ y: getSafeScaleFactor({
575
+ sourceLength: maximumPoint[1] - minimumPoint[1],
576
+ targetLength: cadComponent.size.y * modelUnitToMmScaleFactor
577
+ }),
578
+ z: getSafeScaleFactor({
579
+ sourceLength: maximumPoint[2] - minimumPoint[2],
580
+ targetLength: cadComponent.size.z * modelUnitToMmScaleFactor
581
+ })
582
+ };
583
+ if (cadComponent.model_object_fit === "fill_bounds") {
584
+ return axisScaleFactors;
585
+ }
586
+ const uniformScaleFactor = Math.min(
587
+ axisScaleFactors.x,
588
+ axisScaleFactors.y,
589
+ axisScaleFactors.z
590
+ );
591
+ return {
592
+ x: uniformScaleFactor,
593
+ y: uniformScaleFactor,
594
+ z: uniformScaleFactor
595
+ };
596
+ }
597
+
598
+ // lib/generate-board-cad-models-tsx/get-jscad-operation-in-base-model-frame.ts
599
+ var degreesToRadians = (angleDegrees) => angleDegrees * Math.PI / 180;
600
+ var getCadComponentRotation = (cadComponent) => cadComponent.rotation ?? { x: 0, y: 0, z: 0 };
601
+ var getModelOriginPosition = (cadComponent) => cadComponent.model_origin_position ?? { x: 0, y: 0, z: 0 };
602
+ var getBoardNormalRotation = (cadComponent) => {
603
+ switch (cadComponent.model_board_normal_direction ?? "z+") {
604
+ case "x+":
605
+ return { x: 0, y: -90, z: 0 };
606
+ case "x-":
607
+ return { x: 0, y: 90, z: 0 };
608
+ case "y+":
609
+ return { x: 90, y: 0, z: 0 };
610
+ case "y-":
611
+ return { x: -90, y: 0, z: 0 };
612
+ case "z-":
613
+ return { x: 180, y: 0, z: 0 };
614
+ case "z+":
615
+ return { x: 0, y: 0, z: 0 };
616
+ }
617
+ };
618
+ var rotateOperation = ({
619
+ operation,
620
+ rotationDegrees
621
+ }) => {
622
+ let rotatedOperation = operation;
623
+ for (const [axis, angleDegrees] of [
624
+ ["Z", rotationDegrees.z],
625
+ ["Y", rotationDegrees.y],
626
+ ["X", rotationDegrees.x]
627
+ ]) {
628
+ if (angleDegrees === 0) continue;
629
+ rotatedOperation = {
630
+ type: `rotate${axis}`,
631
+ angle: degreesToRadians(angleDegrees),
632
+ shape: rotatedOperation
633
+ };
634
+ }
635
+ return rotatedOperation;
636
+ };
637
+ var rotateOperationByInverse = ({
638
+ operation,
639
+ rotationDegrees
640
+ }) => {
641
+ let rotatedOperation = operation;
642
+ for (const [axis, angleDegrees] of [
643
+ ["X", rotationDegrees.x],
644
+ ["Y", rotationDegrees.y],
645
+ ["Z", rotationDegrees.z]
646
+ ]) {
647
+ if (angleDegrees === 0) continue;
648
+ rotatedOperation = {
649
+ type: `rotate${axis}`,
650
+ angle: degreesToRadians(-angleDegrees),
651
+ shape: rotatedOperation
652
+ };
653
+ }
654
+ return rotatedOperation;
655
+ };
656
+ var translateOperation = ({
657
+ operation,
658
+ translation
659
+ }) => {
660
+ if (translation.x === 0 && translation.y === 0 && translation.z === 0) {
661
+ return operation;
662
+ }
663
+ return {
664
+ type: "translate",
665
+ vector: [translation.x, translation.y, translation.z],
666
+ shape: operation
667
+ };
668
+ };
669
+ var scaleOperation = ({
670
+ operation,
671
+ scaleFactors
672
+ }) => {
673
+ if (scaleFactors.x === 1 && scaleFactors.y === 1 && scaleFactors.z === 1) {
674
+ return operation;
675
+ }
676
+ return {
677
+ type: "scale",
678
+ factors: [scaleFactors.x, scaleFactors.y, scaleFactors.z],
679
+ shape: operation
680
+ };
681
+ };
682
+ function getJscadOperationInBaseModelFrame({
683
+ baseCadComponent,
684
+ basePositionOffset,
685
+ cadComponent,
686
+ positionOffset
687
+ }) {
688
+ if (cadComponent.model_jscad === void 0) return void 0;
689
+ const modelOriginPosition = getModelOriginPosition(cadComponent);
690
+ const cadRotationDegrees = getCadComponentRotation(cadComponent);
691
+ const baseCadRotationDegrees = getCadComponentRotation(baseCadComponent);
692
+ const modelUnitToMmScaleFactor = cadComponent.model_unit_to_mm_scale_factor ?? 1;
693
+ let operation = cadComponent.model_jscad;
694
+ operation = scaleOperation({
695
+ operation,
696
+ scaleFactors: {
697
+ x: modelUnitToMmScaleFactor,
698
+ y: modelUnitToMmScaleFactor,
699
+ z: modelUnitToMmScaleFactor
700
+ }
701
+ });
702
+ operation = translateOperation({
703
+ operation,
704
+ translation: {
705
+ x: -modelOriginPosition.x,
706
+ y: -modelOriginPosition.y,
707
+ z: -modelOriginPosition.z
708
+ }
709
+ });
710
+ operation = rotateOperation({
711
+ operation,
712
+ rotationDegrees: getBoardNormalRotation(cadComponent)
713
+ });
714
+ operation = scaleOperation({
715
+ operation,
716
+ scaleFactors: getJscadOperationFitScaleFactors({
717
+ cadComponent,
718
+ operation
719
+ })
720
+ });
721
+ operation = rotateOperation({
722
+ operation,
723
+ rotationDegrees: cadRotationDegrees
724
+ });
725
+ operation = translateOperation({
726
+ operation,
727
+ translation: {
728
+ x: positionOffset.x - basePositionOffset.x,
729
+ y: positionOffset.y - basePositionOffset.y,
730
+ z: positionOffset.z - basePositionOffset.z
731
+ }
732
+ });
733
+ return rotateOperationByInverse({
734
+ operation,
735
+ rotationDegrees: baseCadRotationDegrees
736
+ });
737
+ }
738
+
739
+ // lib/generate-board-cad-models-tsx/pack-jscad-operations.ts
740
+ import jscad3 from "@jscad/modeling";
741
+ import "jscad-planner";
742
+ var copyPolygon = ({
743
+ polygon,
744
+ geometryColor
745
+ }) => ({
746
+ vertices: polygon.vertices.map(([x, y, z]) => [x, y, z]),
747
+ color: polygon.color ?? geometryColor
748
+ });
749
+ var getPackedJscadSize = (polygons) => {
750
+ const vertices = polygons.flatMap((polygon) => polygon.vertices);
751
+ if (vertices.length === 0) return void 0;
752
+ const [firstVertex, ...remainingVertices] = vertices;
753
+ let minX = firstVertex[0];
754
+ let minY = firstVertex[1];
755
+ let minZ = firstVertex[2];
756
+ let maxX = firstVertex[0];
757
+ let maxY = firstVertex[1];
758
+ let maxZ = firstVertex[2];
759
+ for (const [x, y, z] of remainingVertices) {
760
+ minX = Math.min(minX, x);
761
+ minY = Math.min(minY, y);
762
+ minZ = Math.min(minZ, z);
763
+ maxX = Math.max(maxX, x);
764
+ maxY = Math.max(maxY, y);
765
+ maxZ = Math.max(maxZ, z);
766
+ }
767
+ return { x: maxX - minX, y: maxY - minY, z: maxZ - minZ };
768
+ };
769
+ var packJscadOperations = (operations) => {
770
+ const polygons = [];
771
+ for (const operation of operations) {
772
+ const geometry = executeJscadOperation(operation);
773
+ if (!jscad3.geometries.geom3.isA(geometry)) return void 0;
774
+ polygons.push(
775
+ ...jscad3.geometries.geom3.toPolygons(geometry).map(
776
+ (polygon) => copyPolygon({
777
+ polygon,
778
+ geometryColor: geometry.color
779
+ })
780
+ )
781
+ );
782
+ }
783
+ return {
784
+ jscad: { type: "createGeom3", polygons },
785
+ size: getPackedJscadSize(polygons)
786
+ };
787
+ };
788
+
789
+ // lib/generate-board-cad-models-tsx/generate-jscad-cad-model-prop-tsx.ts
790
+ function generateSingleJscadCadModelPropTsx({
791
+ cadComponent,
792
+ parentPlacement,
793
+ pcbBoard
794
+ }) {
236
795
  const placement = getCadModelPlacement({
237
796
  cadComponent,
238
797
  parentPlacement,
239
798
  pcbBoard
240
799
  });
241
- const attributes = [
242
- `modelUrl={${JSON.stringify(getModelUrlWithFormatHint(modelUrl))}}`,
243
- `positionOffset={${formatPointProp(placement.positionOffset)}}`,
244
- `rotationOffset={${formatPointProp(placement.ccwRotationOffsetDegrees)}}`
800
+ const properties = [
801
+ `jscad: ${JSON.stringify(cadComponent.model_jscad)}`,
802
+ `positionOffset: ${formatPointProp(placement.positionOffset)}`,
803
+ `rotationOffset: ${formatPointProp(placement.ccwRotationOffsetDegrees)}`
245
804
  ];
246
- if (cadComponent.model_unit_to_mm_scale_factor !== void 0) {
247
- attributes.push(
248
- `modelUnitToMmScale={${cadComponent.model_unit_to_mm_scale_factor}}`
805
+ const modelOriginPosition = getCadModelOriginPosition(cadComponent);
806
+ if (modelOriginPosition !== void 0) {
807
+ properties.push(
808
+ `modelOriginPosition: ${formatPointProp(modelOriginPosition)}`
249
809
  );
250
810
  }
251
- if (cadComponent.model_board_normal_direction !== void 0) {
252
- attributes.push(
253
- `modelBoardNormalDirection={${JSON.stringify(cadComponent.model_board_normal_direction)}}`
811
+ if (cadComponent.model_unit_to_mm_scale_factor !== void 0) {
812
+ properties.push(
813
+ `modelUnitToMmScale: ${cadComponent.model_unit_to_mm_scale_factor}`
254
814
  );
255
815
  }
256
- const modelOriginPosition = getCadModelOriginPosition(cadComponent);
257
- if (modelOriginPosition !== void 0) {
258
- attributes.push(
259
- `modelOriginPosition={${formatPointProp(modelOriginPosition)}}`
816
+ if (cadComponent.model_board_normal_direction !== void 0) {
817
+ properties.push(
818
+ `modelBoardNormalDirection: ${JSON.stringify(cadComponent.model_board_normal_direction)}`
260
819
  );
261
820
  }
262
821
  if (cadComponent.size !== void 0) {
263
- attributes.push(`size={${formatPointProp(cadComponent.size)}}`);
264
- }
265
- if (cadComponent.show_as_translucent_model === true) {
266
- attributes.push("showAsTranslucentModel");
822
+ properties.push(`size: ${formatPointProp(cadComponent.size)}`);
267
823
  }
268
- return `<cadmodel ${attributes.join(" ")} />`;
824
+ return `{ ${properties.join(", ")} }`;
269
825
  }
270
-
271
- // lib/generate-board-cad-models-tsx/generate-cad-model-group-tsx.ts
272
- function generateCadModelGroupTsx({
273
- cadModelGroup,
274
- groupIndex,
826
+ function generateJscadCadModelPropTsx({
827
+ cadComponents,
828
+ parentPlacement,
275
829
  pcbBoard
276
830
  }) {
277
- const { cadComponents, parentPlacement } = cadModelGroup;
278
- const modelElements = cadComponents.flatMap((cadComponent) => {
279
- const modelElement = generateCadModelTsx({
831
+ const baseCadComponent = cadComponents[0];
832
+ if (!baseCadComponent || baseCadComponent.model_jscad === void 0) {
833
+ return void 0;
834
+ }
835
+ if (cadComponents.length === 1) {
836
+ return generateSingleJscadCadModelPropTsx({
837
+ cadComponent: baseCadComponent,
838
+ parentPlacement,
839
+ pcbBoard
840
+ });
841
+ }
842
+ const basePlacement = getCadModelPlacement({
843
+ cadComponent: baseCadComponent,
844
+ parentPlacement,
845
+ pcbBoard
846
+ });
847
+ const shapes = cadComponents.flatMap((cadComponent) => {
848
+ const placement = getCadModelPlacement({
280
849
  cadComponent,
281
850
  parentPlacement,
282
851
  pcbBoard
283
852
  });
284
- return modelElement ? [modelElement] : [];
853
+ const operation = getJscadOperationInBaseModelFrame({
854
+ baseCadComponent,
855
+ basePositionOffset: basePlacement.positionOffset,
856
+ cadComponent,
857
+ positionOffset: placement.positionOffset
858
+ });
859
+ return operation ? [operation] : [];
860
+ });
861
+ if (shapes.length === 0) return void 0;
862
+ const packedJscadOperations = packJscadOperations(shapes);
863
+ const jscad4 = packedJscadOperations?.jscad ?? {
864
+ type: "union",
865
+ shapes
866
+ };
867
+ const properties = [
868
+ `jscad: ${JSON.stringify(jscad4)}`,
869
+ `positionOffset: ${formatPointProp(basePlacement.positionOffset)}`,
870
+ `rotationOffset: ${formatPointProp(basePlacement.ccwRotationOffsetDegrees)}`,
871
+ `modelOriginPosition: ${formatPointProp({ x: 0, y: 0, z: 0 })}`,
872
+ "modelUnitToMmScale: 1",
873
+ 'modelBoardNormalDirection: "z+"'
874
+ ];
875
+ if (packedJscadOperations?.size !== void 0) {
876
+ properties.push(`size: ${formatPointProp(packedJscadOperations.size)}`);
877
+ }
878
+ return `{ ${properties.join(", ")} }`;
879
+ }
880
+
881
+ // lib/generate-board-cad-models-tsx/generate-cad-model-group-model-tsx.ts
882
+ var generateUrlCadModelElementsTsx = ({
883
+ cadModelGroup,
884
+ pcbBoard
885
+ }) => cadModelGroup.cadComponents.flatMap((cadComponent) => {
886
+ if (getCadModelUrl(cadComponent) === void 0) return [];
887
+ const modelElement = generateCadModelTsx({
888
+ cadComponent,
889
+ parentPlacement: cadModelGroup.parentPlacement,
890
+ pcbBoard
285
891
  });
286
- if (modelElements.length === 0) return void 0;
287
- let cadModelTsx = modelElements[0];
288
- if (modelElements.length > 1) {
289
- cadModelTsx = `<cadassembly>${modelElements.join("\n")}</cadassembly>`;
892
+ return modelElement ? [modelElement] : [];
893
+ });
894
+ var getJscadCadComponents = (cadModelGroup) => cadModelGroup.cadComponents.filter(
895
+ (cadComponent) => cadComponent.model_jscad !== void 0
896
+ );
897
+ function generateCadModelGroupModelTsx({
898
+ cadModelGroup,
899
+ pcbBoard
900
+ }) {
901
+ const jscadCadComponents = getJscadCadComponents(cadModelGroup);
902
+ if (jscadCadComponents.length > 0) {
903
+ return generateJscadCadModelPropTsx({
904
+ cadComponents: jscadCadComponents,
905
+ parentPlacement: cadModelGroup.parentPlacement,
906
+ pcbBoard
907
+ });
290
908
  }
909
+ const urlCadModelElementsTsx = generateUrlCadModelElementsTsx({
910
+ cadModelGroup,
911
+ pcbBoard
912
+ });
913
+ if (urlCadModelElementsTsx.length === 0) return void 0;
914
+ if (urlCadModelElementsTsx.length === 1) return urlCadModelElementsTsx[0];
915
+ return `<cadassembly>${urlCadModelElementsTsx.join("\n")}</cadassembly>`;
916
+ }
917
+ function generateCadModelGroupChildrenTsx({
918
+ cadModelGroup,
919
+ pcbBoard
920
+ }) {
921
+ if (getJscadCadComponents(cadModelGroup).length === 0) return [];
922
+ return generateUrlCadModelElementsTsx({ cadModelGroup, pcbBoard });
923
+ }
924
+
925
+ // lib/generate-board-cad-models-tsx/is-jscad-cad-model-group-translucent.ts
926
+ function isJscadCadModelGroupTranslucent(cadModelGroup) {
927
+ const jscadCadComponents = cadModelGroup.cadComponents.filter(
928
+ (cadComponent) => cadComponent.model_jscad !== void 0
929
+ );
930
+ return jscadCadComponents.length > 0 && jscadCadComponents.every(
931
+ (cadComponent) => cadComponent.show_as_translucent_model === true
932
+ );
933
+ }
934
+
935
+ // lib/generate-board-cad-models-tsx/generate-cad-model-group-tsx.ts
936
+ function generateCadModelGroupTsx({
937
+ cadModelGroup,
938
+ pcbBoard,
939
+ runtimeName
940
+ }) {
941
+ const { parentPlacement } = cadModelGroup;
942
+ const cadModelTsx = generateCadModelGroupModelTsx({
943
+ cadModelGroup,
944
+ pcbBoard
945
+ });
291
946
  if (!cadModelTsx) return void 0;
292
947
  const attributes = [
293
- `name={${JSON.stringify(`ImportedCadModel${groupIndex + 1}`)}}`,
948
+ `name={${JSON.stringify(runtimeName)}}`,
294
949
  `pcbX={${parentPlacement.center.x}}`,
295
950
  `pcbY={${parentPlacement.center.y}}`,
296
951
  `pcbRotation="${parentPlacement.ccwRotationDegrees}deg"`,
297
952
  `layer="${parentPlacement.layer}"`,
298
953
  "noSchematicRepresentation",
299
- "obstructsWithinBounds={false}",
300
- "footprint={<footprint />}",
301
- `cadModel={${cadModelTsx}}`
302
- ];
303
- return `<chip ${attributes.join(" ")} />`;
304
- }
305
-
306
- // lib/generate-board-cad-models-tsx/get-cad-model-groups.ts
307
- function getCadModelGroups(circuitJson) {
308
- const pcbComponentById = /* @__PURE__ */ new Map();
309
- for (const element of circuitJson) {
310
- if (element.type === "pcb_component") {
311
- pcbComponentById.set(element.pcb_component_id, element);
312
- }
313
- }
314
- const cadModelGroupByKey = /* @__PURE__ */ new Map();
315
- for (const element of circuitJson) {
316
- if (element.type !== "cad_component" || !getCadModelUrl(element)) continue;
317
- const pcbComponent = element.pcb_component_id ? pcbComponentById.get(element.pcb_component_id) : void 0;
318
- const layer = (element.layer ?? pcbComponent?.layer) === "bottom" ? "bottom" : "top";
319
- let parentPlacement = {
320
- center: element.position,
321
- ccwRotationDegrees: 0,
322
- layer
323
- };
324
- let cadModelGroupKey = element.cad_component_id;
325
- if (pcbComponent !== void 0) {
326
- parentPlacement = {
327
- center: pcbComponent.center,
328
- ccwRotationDegrees: pcbComponent.rotation,
329
- layer
330
- };
331
- cadModelGroupKey = `${pcbComponent.pcb_component_id}:${layer}`;
332
- }
333
- const cadModelGroup = cadModelGroupByKey.get(cadModelGroupKey) ?? {
334
- cadComponents: [],
335
- parentPlacement
336
- };
337
- cadModelGroup.cadComponents.push(element);
338
- cadModelGroupByKey.set(cadModelGroupKey, cadModelGroup);
954
+ "obstructsWithinBounds={false}",
955
+ "footprint={<footprint />}",
956
+ `cadModel={${cadModelTsx}}`
957
+ ];
958
+ if (isJscadCadModelGroupTranslucent(cadModelGroup)) {
959
+ attributes.push("showAsTranslucentModel");
339
960
  }
340
- return [...cadModelGroupByKey.values()];
961
+ const childModelElementsTsx = generateCadModelGroupChildrenTsx({
962
+ cadModelGroup,
963
+ pcbBoard
964
+ });
965
+ if (childModelElementsTsx.length === 0) {
966
+ return `<chip ${attributes.join(" ")} />`;
967
+ }
968
+ return `<chip ${attributes.join(" ")}>${childModelElementsTsx.join("\n")}</chip>`;
341
969
  }
342
970
 
343
- // lib/generate-board-cad-models-tsx.ts
344
- function generateBoardCadModelsTsx({
971
+ // lib/board-converter/stages/convert-board-cad-models.ts
972
+ var convertBoardCadModels = ({
345
973
  circuitJson,
346
- pcbBoard
347
- }) {
348
- if (!pcbBoard) return [];
349
- return getCadModelGroups(circuitJson).flatMap((cadModelGroup, groupIndex) => {
974
+ pcbBoard,
975
+ boardChildren,
976
+ standaloneCadModelRuntimeNameByCadComponentId
977
+ }) => {
978
+ const standaloneCadModelGroups = getCadModelGroups(circuitJson).filter(
979
+ (cadModelGroup) => cadModelGroup.cadComponents.every(
980
+ (cadComponent) => cadComponent.pcb_component_id === void 0
981
+ )
982
+ );
983
+ for (const cadModelGroup of standaloneCadModelGroups) {
984
+ const firstCadComponent = cadModelGroup.cadComponents[0];
985
+ if (!firstCadComponent) continue;
986
+ const runtimeName = standaloneCadModelRuntimeNameByCadComponentId.get(
987
+ firstCadComponent.cad_component_id
988
+ );
989
+ if (!runtimeName) {
990
+ throw new Error(
991
+ `Missing runtime name for standalone CAD model ${firstCadComponent.cad_component_id}`
992
+ );
993
+ }
350
994
  const cadModelGroupTsx = generateCadModelGroupTsx({
351
995
  cadModelGroup,
352
- groupIndex,
353
- pcbBoard
996
+ pcbBoard,
997
+ runtimeName
354
998
  });
355
- return cadModelGroupTsx ? [cadModelGroupTsx] : [];
356
- });
357
- }
999
+ if (cadModelGroupTsx) boardChildren.push(cadModelGroupTsx);
1000
+ }
1001
+ };
358
1002
 
359
1003
  // lib/generate-board-nets-tsx.ts
360
- var generateBoardNetsTsx = (context) => {
361
- const runtimeNetNames = new Set(context.runtimeNetNameBySourceNetId.values());
362
- return [...runtimeNetNames].map((runtimeNetName) => {
1004
+ var generateBoardNetsTsx = (circuitJson, context) => {
1005
+ const runtimeNetNames = getRuntimeNetNamesInTraceOrder(circuitJson, context);
1006
+ return runtimeNetNames.map((runtimeNetName) => {
363
1007
  const selectors = context.pcbPortSelectorsByRuntimeNetName.get(runtimeNetName) ?? [];
364
1008
  const connectsTo = selectors.length ? ` connectsTo={${JSON.stringify(selectors)}}` : "";
365
1009
  return `<net name={${JSON.stringify(runtimeNetName)}}${connectsTo} />`;
366
1010
  });
367
1011
  };
1012
+ var getRuntimeNetNamesInTraceOrder = (circuitJson, context) => {
1013
+ const orderedNames = [];
1014
+ const addedNames = /* @__PURE__ */ new Set();
1015
+ for (const element of circuitJson) {
1016
+ if (element.type !== "source_trace") continue;
1017
+ for (const sourceNetId of element.connected_source_net_ids) {
1018
+ const runtimeNetName = context.runtimeNetNameBySourceNetId.get(sourceNetId);
1019
+ if (!runtimeNetName || addedNames.has(runtimeNetName)) continue;
1020
+ orderedNames.push(runtimeNetName);
1021
+ addedNames.add(runtimeNetName);
1022
+ }
1023
+ }
1024
+ for (const runtimeNetName of context.runtimeNetNameBySourceNetId.values()) {
1025
+ if (addedNames.has(runtimeNetName)) continue;
1026
+ orderedNames.push(runtimeNetName);
1027
+ addedNames.add(runtimeNetName);
1028
+ }
1029
+ return orderedNames;
1030
+ };
368
1031
 
369
- // lib/generate-board-schematic-sheet-tsx.ts
1032
+ // lib/board-converter/stages/convert-board-nets.ts
1033
+ var convertBoardNets = ({
1034
+ boardChildren,
1035
+ boardProps,
1036
+ circuitJson,
1037
+ footprintConversionContext
1038
+ }) => {
1039
+ boardChildren.push(
1040
+ ...generateBoardNetsTsx(circuitJson, footprintConversionContext)
1041
+ );
1042
+ if (circuitJson.some((element) => element.type === "pcb_trace") && !boardProps.includes("routingDisabled")) {
1043
+ boardProps.push("routingDisabled");
1044
+ }
1045
+ };
1046
+
1047
+ // lib/board-converter/stages/convert-board-properties.ts
370
1048
  import { mmStr } from "@tscircuit/mm";
1049
+ var convertBoardProperties = ({
1050
+ pcbBoard,
1051
+ boardProps
1052
+ }) => {
1053
+ if (pcbBoard.center.x !== 0) {
1054
+ boardProps.push(`pcbX={${pcbBoard.center.x}}`);
1055
+ }
1056
+ if (pcbBoard.center.y !== 0) {
1057
+ boardProps.push(`pcbY={${pcbBoard.center.y}}`);
1058
+ }
1059
+ if (pcbBoard.width !== void 0) {
1060
+ boardProps.push(`width="${mmStr(pcbBoard.width)}"`);
1061
+ }
1062
+ if (pcbBoard.height !== void 0) {
1063
+ boardProps.push(`height="${mmStr(pcbBoard.height)}"`);
1064
+ }
1065
+ if (pcbBoard.outline && pcbBoard.outline.length > 0) {
1066
+ const points = pcbBoard.outline.map((point) => `{ x: ${point.x}, y: ${point.y} }`).join(", ");
1067
+ boardProps.push(`outline={[${points}]}`);
1068
+ }
1069
+ if (pcbBoard.thickness !== void 0) {
1070
+ boardProps.push(`thickness="${mmStr(pcbBoard.thickness)}"`);
1071
+ }
1072
+ if (pcbBoard.num_layers !== void 0) {
1073
+ boardProps.push(`layers={${pcbBoard.num_layers}}`);
1074
+ }
1075
+ if (pcbBoard.material !== void 0) {
1076
+ boardProps.push(`material="${pcbBoard.material}"`);
1077
+ }
1078
+ if (pcbBoard.solder_mask_color !== void 0) {
1079
+ boardProps.push(`solderMaskColor="${pcbBoard.solder_mask_color}"`);
1080
+ }
1081
+ if (pcbBoard.silkscreen_color !== void 0) {
1082
+ boardProps.push(`silkscreenColor="${pcbBoard.silkscreen_color}"`);
1083
+ }
1084
+ if (pcbBoard.anchor_position !== void 0) {
1085
+ boardProps.push(
1086
+ `boardAnchorPosition={{ x: ${pcbBoard.anchor_position.x}, y: ${pcbBoard.anchor_position.y} }}`
1087
+ );
1088
+ }
1089
+ if (pcbBoard.anchor_alignment !== void 0) {
1090
+ boardProps.push(`boardAnchorAlignment="${pcbBoard.anchor_alignment}"`);
1091
+ }
1092
+ };
1093
+
1094
+ // lib/generate-board-schematic-sheet-tsx.ts
1095
+ import { mmStr as mmStr2 } from "@tscircuit/mm";
371
1096
  var DEFAULT_SHEET_DIMENSIONS_MM = {
372
1097
  a4: { width: 297, height: 210 },
373
1098
  ansi_b: { width: 431.8, height: 279.4 }
@@ -391,8 +1116,8 @@ var generateBoardSchematicSheetTsx = ({
391
1116
  const defaultDimensions = DEFAULT_SHEET_DIMENSIONS_MM[sheet.sheet_size ?? "a4"];
392
1117
  const sheetWidth = sheet.sheet_width ?? defaultDimensions.width;
393
1118
  const sheetHeight = sheet.sheet_height ?? defaultDimensions.height;
394
- attrs.push(`sheetWidth="${mmStr(sheetWidth)}"`);
395
- attrs.push(`sheetHeight="${mmStr(sheetHeight)}"`);
1119
+ attrs.push(`sheetWidth="${mmStr2(sheetWidth)}"`);
1120
+ attrs.push(`sheetHeight="${mmStr2(sheetHeight)}"`);
396
1121
  return `<schematicsheet ${attrs.join(" ")}>
397
1122
  ${schematicElements.join("\n")}
398
1123
  </schematicsheet>`;
@@ -1589,6 +2314,51 @@ var generateBoardSchematicElements = (circuitJson) => {
1589
2314
  ];
1590
2315
  };
1591
2316
 
2317
+ // lib/board-converter/stages/convert-board-schematic.ts
2318
+ var convertBoardSchematic = ({
2319
+ boardChildren,
2320
+ circuitJson
2321
+ }) => {
2322
+ const schematicElements = generateBoardSchematicElements(circuitJson);
2323
+ boardChildren.push(
2324
+ generateBoardSchematicSheetTsx({
2325
+ circuitJson,
2326
+ schematicElements
2327
+ })
2328
+ );
2329
+ };
2330
+
2331
+ // lib/generate-board-traces-tsx.ts
2332
+ function generateBoardTracesTsx(circuitJson, context) {
2333
+ return circuitJson.flatMap((element) => {
2334
+ if (element.type !== "source_trace") return [];
2335
+ const portSelectors = element.connected_source_port_ids.flatMap(
2336
+ (sourcePortId) => context.pcbPortSelectorsBySourcePortId.get(sourcePortId) ?? []
2337
+ );
2338
+ const netSelectors = [
2339
+ ...new Set(
2340
+ element.connected_source_net_ids.flatMap((sourceNetId) => {
2341
+ const runtimeNetName = context.runtimeNetNameBySourceNetId.get(sourceNetId);
2342
+ return runtimeNetName ? [`net.${runtimeNetName}`] : [];
2343
+ })
2344
+ )
2345
+ ];
2346
+ const path = [...portSelectors, ...netSelectors];
2347
+ return path.length >= 2 ? [`<trace path={${JSON.stringify(path)}} />`] : [];
2348
+ });
2349
+ }
2350
+
2351
+ // lib/board-converter/stages/convert-board-traces.ts
2352
+ var convertBoardTraces = ({
2353
+ boardChildren,
2354
+ circuitJson,
2355
+ footprintConversionContext
2356
+ }) => {
2357
+ boardChildren.push(
2358
+ ...generateBoardTracesTsx(circuitJson, footprintConversionContext)
2359
+ );
2360
+ };
2361
+
1592
2362
  // lib/generate-copper-pours-tsx.ts
1593
2363
  import { su as su6 } from "@tscircuit/soup-util";
1594
2364
 
@@ -1798,6 +2568,26 @@ var getUnassignedCopperPourNetName = ({
1798
2568
  return name;
1799
2569
  };
1800
2570
 
2571
+ // lib/board-converter/stages/convert-copper-pours.ts
2572
+ var convertCopperPours = ({
2573
+ boardChildren,
2574
+ circuitJson,
2575
+ footprintConversionContext
2576
+ }) => {
2577
+ boardChildren.push(
2578
+ ...generateCopperPoursTsx(circuitJson, footprintConversionContext)
2579
+ );
2580
+ };
2581
+
2582
+ // lib/format-jsx-string-attribute.ts
2583
+ var formatJsxStringAttribute = (text) => {
2584
+ const stringText = String(text ?? "");
2585
+ if (!/["\\\r\n\t{}<&>]/.test(stringText)) {
2586
+ return `"${stringText}"`;
2587
+ }
2588
+ return `{${JSON.stringify(stringText)}}`;
2589
+ };
2590
+
1801
2591
  // lib/generate-footprint-tsx/convert-copper-text.ts
1802
2592
  import { su as su7 } from "@tscircuit/soup-util";
1803
2593
 
@@ -1897,8 +2687,8 @@ var convertCourtyard = (circuitJson) => {
1897
2687
  import { su as su9 } from "@tscircuit/soup-util";
1898
2688
 
1899
2689
  // lib/generate-footprint-tsx/footprint-tsx-attribute-formatters/format-mm.ts
1900
- import { mmStr as mmStr2 } from "@tscircuit/mm";
1901
- var formatMm = (value) => mmStr2(value ?? 0);
2690
+ import { mmStr as mmStr3 } from "@tscircuit/mm";
2691
+ var formatMm = (value) => mmStr3(value ?? 0);
1902
2692
 
1903
2693
  // lib/generate-footprint-tsx/convert-cutouts.ts
1904
2694
  var convertCutouts = (circuitJson) => {
@@ -2043,26 +2833,26 @@ var convertFabrication = (circuitJson) => {
2043
2833
 
2044
2834
  // lib/generate-footprint-tsx/convert-holes.ts
2045
2835
  import { su as su11 } from "@tscircuit/soup-util";
2046
- import { mmStr as mmStr3 } from "@tscircuit/mm";
2836
+ import { mmStr as mmStr4 } from "@tscircuit/mm";
2047
2837
  var convertHoles = (circuitJson) => {
2048
2838
  const holes = su11(circuitJson).pcb_hole.list();
2049
2839
  const elementStrings = [];
2050
2840
  for (const hole of holes) {
2051
2841
  if (hole.hole_shape === "circle") {
2052
2842
  elementStrings.push(
2053
- `<hole pcbX="${mmStr3(hole.x)}" pcbY="${mmStr3(hole.y)}" diameter="${mmStr3(hole.hole_diameter)}" />`
2843
+ `<hole pcbX="${mmStr4(hole.x)}" pcbY="${mmStr4(hole.y)}" diameter="${mmStr4(hole.hole_diameter)}" />`
2054
2844
  );
2055
2845
  } else if (hole.hole_shape === "rect") {
2056
2846
  elementStrings.push(
2057
- `<hole pcbX="${mmStr3(hole.x)}" pcbY="${mmStr3(hole.y)}" width="${mmStr3(hole.hole_width)}" height="${mmStr3(hole.hole_height)}" shape="rect" />`
2847
+ `<hole pcbX="${mmStr4(hole.x)}" pcbY="${mmStr4(hole.y)}" width="${mmStr4(hole.hole_width)}" height="${mmStr4(hole.hole_height)}" shape="rect" />`
2058
2848
  );
2059
2849
  } else if (hole.hole_shape === "oval") {
2060
2850
  elementStrings.push(
2061
- `<hole pcbX="${mmStr3(hole.x)}" pcbY="${mmStr3(hole.y)}" width="${mmStr3(hole.hole_width)}" height="${mmStr3(hole.hole_height)}" shape="oval" />`
2851
+ `<hole pcbX="${mmStr4(hole.x)}" pcbY="${mmStr4(hole.y)}" width="${mmStr4(hole.hole_width)}" height="${mmStr4(hole.hole_height)}" shape="oval" />`
2062
2852
  );
2063
2853
  } else if (hole.hole_shape === "pill" || hole.hole_shape === "rotated_pill") {
2064
2854
  elementStrings.push(
2065
- `<hole pcbX="${mmStr3(hole.x)}" pcbY="${mmStr3(hole.y)}" width="${mmStr3(hole.hole_width)}" height="${mmStr3(hole.hole_height)}" shape="pill"${formatPcbRotationAttr("ccw_rotation" in hole ? hole.ccw_rotation : void 0)} />`
2855
+ `<hole pcbX="${mmStr4(hole.x)}" pcbY="${mmStr4(hole.y)}" width="${mmStr4(hole.hole_width)}" height="${mmStr4(hole.hole_height)}" shape="pill"${formatPcbRotationAttr("ccw_rotation" in hole ? hole.ccw_rotation : void 0)} />`
2066
2856
  );
2067
2857
  }
2068
2858
  }
@@ -2276,13 +3066,13 @@ var convertPcbTraces = (circuitJson) => su14(circuitJson).pcb_trace.list().map((
2276
3066
 
2277
3067
  // lib/generate-footprint-tsx/convert-plated-holes.ts
2278
3068
  import { su as su15 } from "@tscircuit/soup-util";
2279
- import { mmStr as mmStr5 } from "@tscircuit/mm";
3069
+ import { mmStr as mmStr6 } from "@tscircuit/mm";
2280
3070
 
2281
3071
  // lib/generate-footprint-tsx/footprint-tsx-attribute-formatters/format-optional-mm-attr.ts
2282
- import { mmStr as mmStr4 } from "@tscircuit/mm";
3072
+ import { mmStr as mmStr5 } from "@tscircuit/mm";
2283
3073
  var formatOptionalMmAttr = (attrName, value) => {
2284
3074
  if (value === void 0) return "";
2285
- return ` ${attrName}="${mmStr4(value)}"`;
3075
+ return ` ${attrName}="${mmStr5(value)}"`;
2286
3076
  };
2287
3077
 
2288
3078
  // lib/generate-footprint-tsx/footprint-tsx-attribute-formatters/format-solder-mask-attrs.ts
@@ -2298,7 +3088,13 @@ var formatSolderMaskAttrs = (platedHole) => {
2298
3088
  var getImportedPortHints = (primitive, context) => {
2299
3089
  if (!primitive.pcb_port_id || !context) return primitive.port_hints;
2300
3090
  const importedHint = context.portHintByPcbPortId.get(primitive.pcb_port_id);
2301
- return importedHint ? [importedHint] : primitive.port_hints;
3091
+ if (!importedHint) return primitive.port_hints;
3092
+ if (!context.pcbPortIdsWithUnambiguousComponentSelectors.has(
3093
+ primitive.pcb_port_id
3094
+ )) {
3095
+ return [importedHint];
3096
+ }
3097
+ return [.../* @__PURE__ */ new Set([importedHint, ...primitive.port_hints ?? []])];
2302
3098
  };
2303
3099
 
2304
3100
  // lib/generate-footprint-tsx/convert-plated-holes.ts
@@ -2311,26 +3107,26 @@ var convertPlatedHoles = (circuitJson, context) => {
2311
3107
  const tagSpacing = context ? " " : " ";
2312
3108
  if (platedHole.shape === "oval" || platedHole.shape === "pill") {
2313
3109
  elementStrings.push(
2314
- `<platedhole${tagSpacing}${portHintsAttr} pcbX="${mmStr5(platedHole.x)}" pcbY="${mmStr5(platedHole.y)}"${formatSolderMaskAttrs(platedHole)} outerHeight="${mmStr5(platedHole.outer_height)}" outerWidth="${mmStr5(platedHole.outer_width)}" holeHeight="${mmStr5(platedHole.hole_height)}" holeWidth="${mmStr5(platedHole.hole_width)}" height="${mmStr5(platedHole.hole_height)}" shape="${platedHole.shape}"${formatPcbRotationAttr("ccw_rotation" in platedHole ? platedHole.ccw_rotation : void 0)} />`
3110
+ `<platedhole${tagSpacing}${portHintsAttr} pcbX="${mmStr6(platedHole.x)}" pcbY="${mmStr6(platedHole.y)}"${formatSolderMaskAttrs(platedHole)} outerHeight="${mmStr6(platedHole.outer_height)}" outerWidth="${mmStr6(platedHole.outer_width)}" holeHeight="${mmStr6(platedHole.hole_height)}" holeWidth="${mmStr6(platedHole.hole_width)}" height="${mmStr6(platedHole.hole_height)}" shape="${platedHole.shape}"${formatPcbRotationAttr("ccw_rotation" in platedHole ? platedHole.ccw_rotation : void 0)} />`
2315
3111
  );
2316
3112
  } else if (platedHole.shape === "circle") {
2317
3113
  elementStrings.push(
2318
- `<platedhole${tagSpacing}${portHintsAttr} pcbX="${mmStr5(platedHole.x)}" pcbY="${mmStr5(platedHole.y)}"${formatSolderMaskAttrs(platedHole)} outerDiameter="${mmStr5(platedHole.outer_diameter)}" holeDiameter="${mmStr5(platedHole.hole_diameter)}" shape="circle" />`
3114
+ `<platedhole${tagSpacing}${portHintsAttr} pcbX="${mmStr6(platedHole.x)}" pcbY="${mmStr6(platedHole.y)}"${formatSolderMaskAttrs(platedHole)} outerDiameter="${mmStr6(platedHole.outer_diameter)}" holeDiameter="${mmStr6(platedHole.hole_diameter)}" shape="circle" />`
2319
3115
  );
2320
3116
  } else if (platedHole.shape === "circular_hole_with_rect_pad") {
2321
3117
  elementStrings.push(
2322
- `<platedhole${tagSpacing}${portHintsAttr} pcbX="${mmStr5(platedHole.x)}" pcbY="${mmStr5(platedHole.y)}"${platedHole.hole_shape === void 0 ? "" : ` holeShape="${platedHole.hole_shape}"`}${platedHole.pad_shape === void 0 ? "" : ` padShape="${platedHole.pad_shape}"`}${formatSolderMaskAttrs(platedHole)} holeDiameter="${mmStr5(platedHole.hole_diameter)}" rectPadWidth="${mmStr5(platedHole.rect_pad_width)}" rectPadHeight="${mmStr5(platedHole.rect_pad_height)}"${formatOptionalMmAttr("rectBorderRadius", platedHole.rect_border_radius)}${formatOptionalMmAttr("holeOffsetX", platedHole.hole_offset_x)}${formatOptionalMmAttr("holeOffsetY", platedHole.hole_offset_y)}${formatPcbRotationAttr(platedHole.rect_ccw_rotation)} shape="circular_hole_with_rect_pad" />`
3118
+ `<platedhole${tagSpacing}${portHintsAttr} pcbX="${mmStr6(platedHole.x)}" pcbY="${mmStr6(platedHole.y)}"${platedHole.hole_shape === void 0 ? "" : ` holeShape="${platedHole.hole_shape}"`}${platedHole.pad_shape === void 0 ? "" : ` padShape="${platedHole.pad_shape}"`}${formatSolderMaskAttrs(platedHole)} holeDiameter="${mmStr6(platedHole.hole_diameter)}" rectPadWidth="${mmStr6(platedHole.rect_pad_width)}" rectPadHeight="${mmStr6(platedHole.rect_pad_height)}"${formatOptionalMmAttr("rectBorderRadius", platedHole.rect_border_radius)}${formatOptionalMmAttr("holeOffsetX", platedHole.hole_offset_x)}${formatOptionalMmAttr("holeOffsetY", platedHole.hole_offset_y)}${formatPcbRotationAttr(platedHole.rect_ccw_rotation)} shape="circular_hole_with_rect_pad" />`
2323
3119
  );
2324
3120
  } else if (platedHole.shape === "pill_hole_with_rect_pad" || platedHole.shape === "rotated_pill_hole_with_rect_pad") {
2325
3121
  const holeShape = platedHole.shape === "rotated_pill_hole_with_rect_pad" ? "pill" : platedHole.hole_shape;
2326
3122
  const rotation = platedHole.shape === "rotated_pill_hole_with_rect_pad" ? platedHole.hole_ccw_rotation : void 0;
2327
3123
  elementStrings.push(
2328
- `<platedhole${tagSpacing}${portHintsAttr} pcbX="${mmStr5(platedHole.x)}" pcbY="${mmStr5(platedHole.y)}"${holeShape === void 0 ? "" : ` holeShape="${holeShape}"`}${platedHole.pad_shape === void 0 ? "" : ` padShape="${platedHole.pad_shape}"`}${formatSolderMaskAttrs(platedHole)} holeWidth="${mmStr5(platedHole.hole_width)}" holeHeight="${mmStr5(platedHole.hole_height)}" rectPadWidth="${mmStr5(platedHole.rect_pad_width)}" rectPadHeight="${mmStr5(platedHole.rect_pad_height)}"${formatOptionalMmAttr("rectBorderRadius", platedHole.rect_border_radius)}${formatOptionalMmAttr("holeOffsetX", platedHole.hole_offset_x)}${formatOptionalMmAttr("holeOffsetY", platedHole.hole_offset_y)}${formatPcbRotationAttr(rotation)} shape="pill_hole_with_rect_pad" />`
3124
+ `<platedhole${tagSpacing}${portHintsAttr} pcbX="${mmStr6(platedHole.x)}" pcbY="${mmStr6(platedHole.y)}"${holeShape === void 0 ? "" : ` holeShape="${holeShape}"`}${platedHole.pad_shape === void 0 ? "" : ` padShape="${platedHole.pad_shape}"`}${formatSolderMaskAttrs(platedHole)} holeWidth="${mmStr6(platedHole.hole_width)}" holeHeight="${mmStr6(platedHole.hole_height)}" rectPadWidth="${mmStr6(platedHole.rect_pad_width)}" rectPadHeight="${mmStr6(platedHole.rect_pad_height)}"${formatOptionalMmAttr("rectBorderRadius", platedHole.rect_border_radius)}${formatOptionalMmAttr("holeOffsetX", platedHole.hole_offset_x)}${formatOptionalMmAttr("holeOffsetY", platedHole.hole_offset_y)}${formatPcbRotationAttr(rotation)} shape="pill_hole_with_rect_pad" />`
2329
3125
  );
2330
3126
  } else if (platedHole.shape === "hole_with_polygon_pad") {
2331
- const holeSizeAttrs = platedHole.hole_shape === "circle" ? ` holeDiameter="${mmStr5(platedHole.hole_diameter ?? 0)}"` : `${formatOptionalMmAttr("holeWidth", platedHole.hole_width)}${formatOptionalMmAttr("holeHeight", platedHole.hole_height)}`;
3127
+ const holeSizeAttrs = platedHole.hole_shape === "circle" ? ` holeDiameter="${mmStr6(platedHole.hole_diameter ?? 0)}"` : `${formatOptionalMmAttr("holeWidth", platedHole.hole_width)}${formatOptionalMmAttr("holeHeight", platedHole.hole_height)}`;
2332
3128
  elementStrings.push(
2333
- `<platedhole${tagSpacing}${portHintsAttr} pcbX="${mmStr5(platedHole.x)}" pcbY="${mmStr5(platedHole.y)}"${platedHole.hole_shape === void 0 ? "" : ` holeShape="${platedHole.hole_shape}"`}${formatSolderMaskAttrs(platedHole)}${holeSizeAttrs} padOutline={${JSON.stringify(platedHole.pad_outline)}} holeOffsetX="${mmStr5(platedHole.hole_offset_x ?? 0)}" holeOffsetY="${mmStr5(platedHole.hole_offset_y ?? 0)}" shape="hole_with_polygon_pad" />`
3129
+ `<platedhole${tagSpacing}${portHintsAttr} pcbX="${mmStr6(platedHole.x)}" pcbY="${mmStr6(platedHole.y)}"${platedHole.hole_shape === void 0 ? "" : ` holeShape="${platedHole.hole_shape}"`}${formatSolderMaskAttrs(platedHole)}${holeSizeAttrs} padOutline={${JSON.stringify(platedHole.pad_outline)}} holeOffsetX="${mmStr6(platedHole.hole_offset_x ?? 0)}" holeOffsetY="${mmStr6(platedHole.hole_offset_y ?? 0)}" shape="hole_with_polygon_pad" />`
2334
3130
  );
2335
3131
  }
2336
3132
  }
@@ -2535,7 +3331,7 @@ var convertSilkscreenGraphics = (circuitJson) => {
2535
3331
 
2536
3332
  // lib/generate-footprint-tsx/convert-silkscreen-text.ts
2537
3333
  import { su as su18 } from "@tscircuit/soup-util";
2538
- import { mmStr as mmStr6 } from "@tscircuit/mm";
3334
+ import { mmStr as mmStr7 } from "@tscircuit/mm";
2539
3335
  var convertSilkscreenText = (circuitJson) => {
2540
3336
  const silkscreenTexts = su18(circuitJson).pcb_silkscreen_text.list();
2541
3337
  const elementStrings = [];
@@ -2545,7 +3341,7 @@ var convertSilkscreenText = (circuitJson) => {
2545
3341
  const knockoutPadding = silkscreenText.knockout_padding;
2546
3342
  const hasUniformKnockoutPadding = knockoutPadding && knockoutPadding.left === knockoutPadding.top && knockoutPadding.top === knockoutPadding.right && knockoutPadding.right === knockoutPadding.bottom;
2547
3343
  elementStrings.push(
2548
- `<silkscreentext pcbX={${pcbX}} pcbY={${pcbY}} anchorAlignment="${silkscreenText.anchor_alignment}" fontSize={${silkscreenText.font_size}}${silkscreenText.font !== void 0 ? ` font="${silkscreenText.font}"` : ""}${silkscreenText.ccw_rotation !== void 0 ? ` pcbRotation="${typeof silkscreenText.ccw_rotation === "number" ? `${silkscreenText.ccw_rotation}deg` : silkscreenText.ccw_rotation}"` : ""}${silkscreenText.is_knockout !== void 0 ? ` isKnockout={${silkscreenText.is_knockout}}` : ""}${knockoutPadding ? hasUniformKnockoutPadding ? ` knockoutPadding="${mmStr6(knockoutPadding.left)}"` : ` knockoutPaddingLeft="${mmStr6(knockoutPadding.left)}" knockoutPaddingTop="${mmStr6(knockoutPadding.top)}" knockoutPaddingRight="${mmStr6(knockoutPadding.right)}" knockoutPaddingBottom="${mmStr6(knockoutPadding.bottom)}"` : ""}${silkscreenText.is_mirrored !== void 0 ? ` mirrored={${silkscreenText.is_mirrored}}` : ""}${silkscreenText.layer !== void 0 ? ` layer="${silkscreenText.layer}"` : ""} text="${escapeJsxText2(silkscreenText.text)}" />`
3344
+ `<silkscreentext pcbX={${pcbX}} pcbY={${pcbY}} anchorAlignment="${silkscreenText.anchor_alignment}" fontSize={${silkscreenText.font_size}}${silkscreenText.font !== void 0 ? ` font="${silkscreenText.font}"` : ""}${silkscreenText.ccw_rotation !== void 0 ? ` pcbRotation="${typeof silkscreenText.ccw_rotation === "number" ? `${silkscreenText.ccw_rotation}deg` : silkscreenText.ccw_rotation}"` : ""}${silkscreenText.is_knockout !== void 0 ? ` isKnockout={${silkscreenText.is_knockout}}` : ""}${knockoutPadding ? hasUniformKnockoutPadding ? ` knockoutPadding="${mmStr7(knockoutPadding.left)}"` : ` knockoutPaddingLeft="${mmStr7(knockoutPadding.left)}" knockoutPaddingTop="${mmStr7(knockoutPadding.top)}" knockoutPaddingRight="${mmStr7(knockoutPadding.right)}" knockoutPaddingBottom="${mmStr7(knockoutPadding.bottom)}"` : ""}${silkscreenText.is_mirrored !== void 0 ? ` mirrored={${silkscreenText.is_mirrored}}` : ""}${silkscreenText.layer !== void 0 ? ` layer="${silkscreenText.layer}"` : ""} text="${escapeJsxText2(silkscreenText.text)}" />`
2549
3345
  );
2550
3346
  }
2551
3347
  return elementStrings;
@@ -2553,7 +3349,7 @@ var convertSilkscreenText = (circuitJson) => {
2553
3349
 
2554
3350
  // lib/generate-footprint-tsx/convert-smt-pads.ts
2555
3351
  import { su as su19 } from "@tscircuit/soup-util";
2556
- import { mmStr as mmStr7 } from "@tscircuit/mm";
3352
+ import { mmStr as mmStr8 } from "@tscircuit/mm";
2557
3353
  var convertSmtPads = (circuitJson, context) => {
2558
3354
  const smtPads = su19(circuitJson).pcb_smtpad.list();
2559
3355
  const elementStrings = [];
@@ -2565,29 +3361,29 @@ var convertSmtPads = (circuitJson, context) => {
2565
3361
  commonAttrs.push(`portHints={${JSON.stringify(portHints)}}`);
2566
3362
  }
2567
3363
  if ("x" in smtPad && smtPad.x !== void 0) {
2568
- commonAttrs.push(`pcbX="${mmStr7(smtPad.x)}"`);
3364
+ commonAttrs.push(`pcbX="${mmStr8(smtPad.x)}"`);
2569
3365
  }
2570
3366
  if ("y" in smtPad && smtPad.y !== void 0) {
2571
- commonAttrs.push(`pcbY="${mmStr7(smtPad.y)}"`);
3367
+ commonAttrs.push(`pcbY="${mmStr8(smtPad.y)}"`);
2572
3368
  }
2573
3369
  if (smtPad.layer !== void 0) {
2574
3370
  commonAttrs.push(`layer="${smtPad.layer}"`);
2575
3371
  }
2576
3372
  if (smtPad.shape === "circle") {
2577
3373
  elementStrings.push(
2578
- `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} radius="${mmStr7(smtPad.radius ?? 0)}" shape="circle" />`
3374
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} radius="${mmStr8(smtPad.radius ?? 0)}" shape="circle" />`
2579
3375
  );
2580
3376
  } else if (smtPad.shape === "rect") {
2581
3377
  elementStrings.push(
2582
- `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs}${formatOptionalMmAttr("rectBorderRadius", smtPad.rect_border_radius)}${formatOptionalMmAttr("cornerRadius", smtPad.corner_radius)}${formatOptionalMmAttr("solderMaskMarginLeft", smtPad.soldermask_margin_left)}${formatOptionalMmAttr("solderMaskMarginTop", smtPad.soldermask_margin_top)}${formatOptionalMmAttr("solderMaskMarginRight", smtPad.soldermask_margin_right)}${formatOptionalMmAttr("solderMaskMarginBottom", smtPad.soldermask_margin_bottom)} width="${mmStr7(smtPad.width ?? 0)}" height="${mmStr7(smtPad.height ?? 0)}" shape="rect" />`
3378
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs}${formatOptionalMmAttr("rectBorderRadius", smtPad.rect_border_radius)}${formatOptionalMmAttr("cornerRadius", smtPad.corner_radius)}${formatOptionalMmAttr("solderMaskMarginLeft", smtPad.soldermask_margin_left)}${formatOptionalMmAttr("solderMaskMarginTop", smtPad.soldermask_margin_top)}${formatOptionalMmAttr("solderMaskMarginRight", smtPad.soldermask_margin_right)}${formatOptionalMmAttr("solderMaskMarginBottom", smtPad.soldermask_margin_bottom)} width="${mmStr8(smtPad.width ?? 0)}" height="${mmStr8(smtPad.height ?? 0)}" shape="rect" />`
2583
3379
  );
2584
3380
  } else if (smtPad.shape === "pill") {
2585
3381
  elementStrings.push(
2586
- `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} width="${mmStr7(smtPad.width ?? 0)}" height="${mmStr7(smtPad.height ?? 0)}" radius="${mmStr7(smtPad.radius ?? 0)}" shape="pill" />`
3382
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} width="${mmStr8(smtPad.width ?? 0)}" height="${mmStr8(smtPad.height ?? 0)}" radius="${mmStr8(smtPad.radius ?? 0)}" shape="pill" />`
2587
3383
  );
2588
3384
  } else if (smtPad.shape === "rotated_pill") {
2589
3385
  elementStrings.push(
2590
- `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} width="${mmStr7(smtPad.width ?? 0)}" height="${mmStr7(smtPad.height ?? 0)}" radius="${mmStr7(smtPad.radius ?? 0)}"${formatPcbRotationAttr(smtPad.ccw_rotation)} shape="pill" />`
3386
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs} width="${mmStr8(smtPad.width ?? 0)}" height="${mmStr8(smtPad.height ?? 0)}" radius="${mmStr8(smtPad.radius ?? 0)}"${formatPcbRotationAttr(smtPad.ccw_rotation)} shape="pill" />`
2591
3387
  );
2592
3388
  } else if (smtPad.shape === "polygon") {
2593
3389
  elementStrings.push(
@@ -2596,7 +3392,7 @@ var convertSmtPads = (circuitJson, context) => {
2596
3392
  } else if (smtPad.shape === "rotated_rect") {
2597
3393
  const cornerRadius = smtPad.corner_radius ?? smtPad.rect_border_radius ?? void 0;
2598
3394
  elementStrings.push(
2599
- `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs}${formatOptionalMmAttr("cornerRadius", cornerRadius)}${formatOptionalMmAttr("solderMaskMarginLeft", smtPad.soldermask_margin_left)}${formatOptionalMmAttr("solderMaskMarginTop", smtPad.soldermask_margin_top)}${formatOptionalMmAttr("solderMaskMarginRight", smtPad.soldermask_margin_right)}${formatOptionalMmAttr("solderMaskMarginBottom", smtPad.soldermask_margin_bottom)} width="${mmStr7(smtPad.width ?? 0)}" height="${mmStr7(smtPad.height ?? 0)}" ccwRotation={${smtPad.ccw_rotation ?? 0}} shape="rotated_rect" />`
3395
+ `<smtpad ${commonAttrs.join(" ")}${solderMaskAttrs}${formatOptionalMmAttr("cornerRadius", cornerRadius)}${formatOptionalMmAttr("solderMaskMarginLeft", smtPad.soldermask_margin_left)}${formatOptionalMmAttr("solderMaskMarginTop", smtPad.soldermask_margin_top)}${formatOptionalMmAttr("solderMaskMarginRight", smtPad.soldermask_margin_right)}${formatOptionalMmAttr("solderMaskMarginBottom", smtPad.soldermask_margin_bottom)} width="${mmStr8(smtPad.width ?? 0)}" height="${mmStr8(smtPad.height ?? 0)}" ccwRotation={${smtPad.ccw_rotation ?? 0}} shape="rotated_rect" />`
2600
3396
  );
2601
3397
  }
2602
3398
  }
@@ -2604,7 +3400,7 @@ var convertSmtPads = (circuitJson, context) => {
2604
3400
  };
2605
3401
 
2606
3402
  // lib/generate-footprint-tsx/convert-vias.ts
2607
- import { mmStr as mmStr8 } from "@tscircuit/mm";
3403
+ import { mmStr as mmStr9 } from "@tscircuit/mm";
2608
3404
  import { su as su20 } from "@tscircuit/soup-util";
2609
3405
  var convertVias = (circuitJson, context) => {
2610
3406
  return su20(circuitJson).pcb_via.list().map((via) => {
@@ -2620,7 +3416,7 @@ var convertVias = (circuitJson, context) => {
2620
3416
  const tentedAttr = tentedOnTop !== void 0 || tentedOnBottom !== void 0 ? ` tented="${tented}"` : "";
2621
3417
  const runtimeNetName = via.source_net_id ? context?.runtimeNetNameBySourceNetId.get(via.source_net_id) : void 0;
2622
3418
  const connectsToAttr = runtimeNetName ? ` connectsTo={${JSON.stringify(`net.${runtimeNetName}`)}}` : "";
2623
- return `<via pcbX="${mmStr8(via.x)}" pcbY="${mmStr8(via.y)}" holeDiameter="${mmStr8(via.hole_diameter)}" outerDiameter="${mmStr8(via.outer_diameter)}" fromLayer="${fromLayer}" toLayer="${toLayer}"${tentedAttr}${connectsToAttr} />`;
3419
+ return `<via pcbX="${mmStr9(via.x)}" pcbY="${mmStr9(via.y)}" holeDiameter="${mmStr9(via.hole_diameter)}" outerDiameter="${mmStr9(via.outer_diameter)}" fromLayer="${fromLayer}" toLayer="${toLayer}"${tentedAttr}${connectsToAttr} />`;
2624
3420
  });
2625
3421
  };
2626
3422
 
@@ -2655,80 +3451,567 @@ var generateFootprintTsx = (circuitJson, context) => {
2655
3451
  `.trim();
2656
3452
  };
2657
3453
 
2658
- // lib/get-board-using-template.ts
2659
- var getBoardUsingTemplate = ({
3454
+ // lib/board-converter/get-pcb-component-render-placement.ts
3455
+ import { getBoundsOfPcbElements } from "@tscircuit/circuit-json-util";
3456
+ import { applyToPoint as applyToPoint5, inverse, translate as translate5 } from "transformation-matrix";
3457
+ var physicalPcbElementTypes = /* @__PURE__ */ new Set([
3458
+ "pcb_copper_text",
3459
+ "pcb_cutout",
3460
+ "pcb_hole",
3461
+ "pcb_keepout",
3462
+ "pcb_plated_hole",
3463
+ "pcb_smtpad",
3464
+ "pcb_trace",
3465
+ "pcb_via"
3466
+ ]);
3467
+ var isPhysicalPcbElement = (element) => physicalPcbElementTypes.has(element.type);
3468
+ var getElementsAsRenderedByGeneratedTsx = (elements) => elements.map((element) => {
3469
+ if (element.type !== "pcb_plated_hole" || element.shape !== "rotated_pill_hole_with_rect_pad") {
3470
+ return element;
3471
+ }
3472
+ return {
3473
+ ...element,
3474
+ rect_ccw_rotation: element.hole_ccw_rotation
3475
+ };
3476
+ });
3477
+ var getPcbComponentRenderPlacement = ({
2660
3478
  circuitJson,
2661
- componentName
3479
+ pcbBoard,
3480
+ pcbComponent
3481
+ }) => {
3482
+ const componentPosition = applyToPoint5(
3483
+ inverse(translate5(pcbBoard.center.x, pcbBoard.center.y)),
3484
+ pcbComponent.center
3485
+ );
3486
+ const physicalElements = circuitJson.filter(
3487
+ (element) => isPhysicalPcbElement(element) && "pcb_component_id" in element && element.pcb_component_id === pcbComponent.pcb_component_id
3488
+ );
3489
+ const physicalBounds = getBoundsOfPcbElements(
3490
+ getElementsAsRenderedByGeneratedTsx(physicalElements)
3491
+ );
3492
+ const physicalWidth = physicalBounds.maxX - physicalBounds.minX;
3493
+ const physicalHeight = physicalBounds.maxY - physicalBounds.minY;
3494
+ if (Number.isFinite(physicalWidth) && Number.isFinite(physicalHeight) && physicalWidth > 0 && physicalHeight > 0) {
3495
+ return {
3496
+ cadModelParentCenter: {
3497
+ x: (physicalBounds.minX + physicalBounds.maxX) / 2,
3498
+ y: (physicalBounds.minY + physicalBounds.maxY) / 2
3499
+ },
3500
+ componentPosition
3501
+ };
3502
+ }
3503
+ return {
3504
+ cadModelParentCenter: componentPosition,
3505
+ componentPosition
3506
+ };
3507
+ };
3508
+
3509
+ // lib/board-converter/get-pin-labels.ts
3510
+ var getCompoundPinNumber = (sourcePort) => {
3511
+ if (sourcePort.pin_number !== void 0) return sourcePort.pin_number;
3512
+ const firstPinNumber = /^\d+/u.exec(sourcePort.name)?.[0];
3513
+ return firstPinNumber ? Number.parseInt(firstPinNumber, 10) : void 0;
3514
+ };
3515
+ var getPinLabels = ({
3516
+ circuitJson,
3517
+ footprintConversionContext,
3518
+ pcbComponent,
3519
+ sourceComponents
2662
3520
  }) => {
2663
- const pcbBoard = su21(circuitJson).pcb_board.list()[0];
2664
- const boardProps = [];
2665
- if (pcbBoard) {
2666
- if (pcbBoard.width !== void 0) {
2667
- boardProps.push(`width="${mmStr9(pcbBoard.width)}"`);
3521
+ if (sourceComponents.length === 0) return void 0;
3522
+ const pinLabels = {};
3523
+ const sourceComponentIds = new Set(
3524
+ sourceComponents.map((sourceComponent) => sourceComponent.source_component_id).filter(
3525
+ (sourceComponentId) => sourceComponentId !== void 0
3526
+ )
3527
+ );
3528
+ const sourcePorts = circuitJson.filter(
3529
+ (element) => element.type === "source_port" && element.source_component_id !== void 0 && sourceComponentIds.has(element.source_component_id) && (element.pin_number !== void 0 || element.source_component_id === pcbComponent.source_component_id)
3530
+ );
3531
+ const usedPinNumbers = new Set(
3532
+ sourcePorts.map(getCompoundPinNumber).filter((pinNumber) => pinNumber !== void 0)
3533
+ );
3534
+ let nextSyntheticPinNumber = Math.max(0, ...usedPinNumbers) + 1;
3535
+ for (const sourcePort of sourcePorts) {
3536
+ const importedPortHints = circuitJson.flatMap((element) => {
3537
+ if (sourcePort.source_component_id !== pcbComponent.source_component_id || element.type !== "pcb_port" || element.source_port_id !== sourcePort.source_port_id) {
3538
+ return [];
3539
+ }
3540
+ const importedPortHint = footprintConversionContext.portHintByPcbPortId.get(element.pcb_port_id);
3541
+ return importedPortHint ? [importedPortHint] : [];
3542
+ });
3543
+ const aliases = [
3544
+ sourcePort.name,
3545
+ ...sourcePort.port_hints ?? [],
3546
+ ...importedPortHints
3547
+ ].filter(
3548
+ (alias, aliasIndex, allAliases) => alias.length > 0 && allAliases.indexOf(alias) === aliasIndex
3549
+ );
3550
+ let pinNumber = getCompoundPinNumber(sourcePort);
3551
+ if (pinNumber === void 0) {
3552
+ while (usedPinNumbers.has(nextSyntheticPinNumber)) {
3553
+ nextSyntheticPinNumber += 1;
3554
+ }
3555
+ pinNumber = nextSyntheticPinNumber;
3556
+ usedPinNumbers.add(pinNumber);
3557
+ nextSyntheticPinNumber += 1;
2668
3558
  }
2669
- if (pcbBoard.height !== void 0) {
2670
- boardProps.push(`height="${mmStr9(pcbBoard.height)}"`);
3559
+ const pinLabelKey = `pin${pinNumber}`;
3560
+ pinLabels[pinLabelKey] = [
3561
+ ...pinLabels[pinLabelKey] ?? [],
3562
+ ...aliases
3563
+ ].filter(
3564
+ (alias, aliasIndex, allAliases) => allAliases.indexOf(alias) === aliasIndex
3565
+ );
3566
+ }
3567
+ return Object.keys(pinLabels).length > 0 ? pinLabels : void 0;
3568
+ };
3569
+
3570
+ // lib/board-converter/get-schematic-components-for-pcb-component.ts
3571
+ var getSchematicComponentsForPcbComponent = ({
3572
+ circuitJson,
3573
+ pcbComponent
3574
+ }) => {
3575
+ const sourceComponents = circuitJson.filter(
3576
+ (element) => element.type === "source_component"
3577
+ );
3578
+ const pcbSourceComponent = sourceComponents.find(
3579
+ (sourceComponent) => sourceComponent.source_component_id === pcbComponent.source_component_id
3580
+ );
3581
+ if (!pcbSourceComponent) return [];
3582
+ const matchingSourceComponentIds = new Set(
3583
+ sourceComponents.filter(
3584
+ (sourceComponent) => sourceComponent.source_component_id === pcbSourceComponent.source_component_id || pcbSourceComponent.name !== void 0 && sourceComponent.name === pcbSourceComponent.name
3585
+ ).map((sourceComponent) => sourceComponent.source_component_id).filter(
3586
+ (sourceComponentId) => sourceComponentId !== void 0
3587
+ )
3588
+ );
3589
+ return circuitJson.filter(
3590
+ (element) => element.type === "schematic_component" && element.source_component_id !== void 0 && matchingSourceComponentIds.has(element.source_component_id)
3591
+ );
3592
+ };
3593
+
3594
+ // lib/board-converter/localize-pcb-component-elements.ts
3595
+ import { transformPCBElements } from "@tscircuit/circuit-json-util";
3596
+ import {
3597
+ applyToPoint as applyToPoint6,
3598
+ compose as compose4,
3599
+ decomposeTSR,
3600
+ flipY,
3601
+ inverse as inverse2,
3602
+ rotateDEG as rotateDEG3,
3603
+ translate as translate6
3604
+ } from "transformation-matrix";
3605
+ var FLOATING_POINT_NOISE_THRESHOLD = 1e-12;
3606
+ var LOCAL_COORDINATE_DECIMAL_PLACES = 12;
3607
+ var normalizeFloatingPointNoise = (elementPropertyValue) => {
3608
+ if (Array.isArray(elementPropertyValue)) {
3609
+ for (const item of elementPropertyValue) normalizeFloatingPointNoise(item);
3610
+ return;
3611
+ }
3612
+ if (typeof elementPropertyValue !== "object" || elementPropertyValue === null)
3613
+ return;
3614
+ const record = elementPropertyValue;
3615
+ for (const [key, property] of Object.entries(record)) {
3616
+ if (typeof property === "number" && Math.abs(property) < FLOATING_POINT_NOISE_THRESHOLD) {
3617
+ record[key] = 0;
3618
+ } else {
3619
+ normalizeFloatingPointNoise(property);
2671
3620
  }
2672
- if (pcbBoard.outline && pcbBoard.outline.length > 0) {
2673
- const points = pcbBoard.outline.map((p) => `{ x: ${p.x}, y: ${p.y} }`).join(", ");
2674
- boardProps.push(`outline={[${points}]}`);
3621
+ }
3622
+ };
3623
+ var normalizeRoutableElementCoordinates = (localComponentElements) => {
3624
+ for (const element of localComponentElements) {
3625
+ if (element.type !== "pcb_smtpad" && element.type !== "pcb_plated_hole") {
3626
+ continue;
2675
3627
  }
2676
- if (pcbBoard.thickness !== void 0) {
2677
- boardProps.push(`thickness="${mmStr9(pcbBoard.thickness)}"`);
3628
+ if (!("x" in element) || !("y" in element)) continue;
3629
+ element.x = Number(element.x.toFixed(LOCAL_COORDINATE_DECIMAL_PLACES));
3630
+ element.y = Number(element.y.toFixed(LOCAL_COORDINATE_DECIMAL_PLACES));
3631
+ }
3632
+ };
3633
+ var transformSilkscreenGraphic = ({
3634
+ absoluteToComponentLocalTransform,
3635
+ graphic
3636
+ }) => {
3637
+ if (graphic.shape !== "brep") return;
3638
+ graphic.brep_shape.outer_ring.vertices = graphic.brep_shape.outer_ring.vertices.map(
3639
+ (vertex) => applyToPoint6(absoluteToComponentLocalTransform, vertex)
3640
+ );
3641
+ graphic.brep_shape.inner_rings = graphic.brep_shape.inner_rings.map(
3642
+ (ring) => ({
3643
+ ...ring,
3644
+ vertices: ring.vertices.map(
3645
+ (vertex) => applyToPoint6(absoluteToComponentLocalTransform, vertex)
3646
+ )
3647
+ })
3648
+ );
3649
+ };
3650
+ var normalizeDegrees = (degrees) => (degrees % 360 + 360) % 360;
3651
+ var getTransformedCcwRotationDegrees = ({
3652
+ absoluteCcwRotationDegrees,
3653
+ absoluteToComponentLocalRotationTransform
3654
+ }) => {
3655
+ const transformedRotation = decomposeTSR(
3656
+ compose4(
3657
+ absoluteToComponentLocalRotationTransform,
3658
+ rotateDEG3(absoluteCcwRotationDegrees)
3659
+ )
3660
+ ).rotation.angle;
3661
+ return normalizeDegrees(transformedRotation * 180 / Math.PI);
3662
+ };
3663
+ var localizePrimitiveRotations = ({
3664
+ absoluteComponentElements,
3665
+ absoluteToComponentLocalRotationTransform,
3666
+ localComponentElements
3667
+ }) => {
3668
+ const rotationPropertyNames = [
3669
+ "ccw_rotation",
3670
+ "hole_ccw_rotation",
3671
+ "rect_ccw_rotation"
3672
+ ];
3673
+ for (const [elementIndex, localElement] of localComponentElements.entries()) {
3674
+ if (localElement.type === "pcb_silkscreen_text" || localElement.type === "pcb_copper_text" || localElement.type === "pcb_fabrication_note_text") {
3675
+ continue;
2678
3676
  }
2679
- if (pcbBoard.num_layers !== void 0) {
2680
- boardProps.push(`layers={${pcbBoard.num_layers}}`);
3677
+ const absoluteElement = absoluteComponentElements[elementIndex];
3678
+ if (absoluteElement?.type !== localElement.type) continue;
3679
+ const absoluteRotations = absoluteElement;
3680
+ const localRotations = localElement;
3681
+ for (const rotationPropertyName of rotationPropertyNames) {
3682
+ const absoluteCcwRotationDegrees = absoluteRotations[rotationPropertyName];
3683
+ if (typeof absoluteCcwRotationDegrees !== "number") continue;
3684
+ localRotations[rotationPropertyName] = getTransformedCcwRotationDegrees({
3685
+ absoluteCcwRotationDegrees,
3686
+ absoluteToComponentLocalRotationTransform
3687
+ });
2681
3688
  }
2682
- if (pcbBoard.material !== void 0) {
2683
- boardProps.push(`material="${pcbBoard.material}"`);
3689
+ }
3690
+ };
3691
+ var getOppositeOuterLayer = (layer) => {
3692
+ if (layer === "top") return "bottom";
3693
+ if (layer === "bottom") return "top";
3694
+ return layer;
3695
+ };
3696
+ var localizeBottomComponentLayers = (localComponentElements) => {
3697
+ for (const localElement of localComponentElements) {
3698
+ if ("layer" in localElement && (localElement.layer === "top" || localElement.layer === "bottom")) {
3699
+ localElement.layer = getOppositeOuterLayer(localElement.layer);
2684
3700
  }
2685
- if (pcbBoard.solder_mask_color !== void 0) {
2686
- boardProps.push(`solderMaskColor="${pcbBoard.solder_mask_color}"`);
3701
+ if ("layers" in localElement && Array.isArray(localElement.layers)) {
3702
+ localElement.layers = localElement.layers.map(
3703
+ (layer) => layer === "top" || layer === "bottom" ? getOppositeOuterLayer(layer) : layer
3704
+ );
2687
3705
  }
2688
- if (pcbBoard.silkscreen_color !== void 0) {
2689
- boardProps.push(`silkscreenColor="${pcbBoard.silkscreen_color}"`);
3706
+ }
3707
+ };
3708
+ var localizeTextRotations = ({
3709
+ absoluteComponentElements,
3710
+ componentCcwRotationDegrees,
3711
+ localComponentElements
3712
+ }) => {
3713
+ for (const [elementIndex, localElement] of localComponentElements.entries()) {
3714
+ if (localElement.type !== "pcb_silkscreen_text" && localElement.type !== "pcb_copper_text" && localElement.type !== "pcb_fabrication_note_text") {
3715
+ continue;
2690
3716
  }
2691
- const resolvedAnchorPosition = pcbBoard.anchor_position ?? pcbBoard.center;
2692
- if (pcbBoard.anchor_position !== void 0 || resolvedAnchorPosition.x !== 0 || resolvedAnchorPosition.y !== 0) {
2693
- boardProps.push(
2694
- `boardAnchorPosition={{ x: ${resolvedAnchorPosition.x}, y: ${resolvedAnchorPosition.y} }}`
3717
+ const absoluteElement = absoluteComponentElements[elementIndex];
3718
+ if (absoluteElement?.type !== localElement.type) continue;
3719
+ const absoluteRotation = normalizeDegrees(absoluteElement.ccw_rotation ?? 0);
3720
+ if (localElement.type === "pcb_silkscreen_text") {
3721
+ localElement.ccw_rotation = absoluteRotation || 360;
3722
+ } else if (localElement.type === "pcb_copper_text") {
3723
+ localElement.ccw_rotation = absoluteRotation;
3724
+ } else {
3725
+ localElement.ccw_rotation = normalizeDegrees(
3726
+ absoluteRotation - componentCcwRotationDegrees
2695
3727
  );
2696
3728
  }
2697
- if (pcbBoard.anchor_alignment !== void 0) {
2698
- boardProps.push(`boardAnchorAlignment="${pcbBoard.anchor_alignment}"`);
3729
+ }
3730
+ };
3731
+ var localizePcbComponentElements = ({
3732
+ circuitJson,
3733
+ pcbComponent
3734
+ }) => {
3735
+ const absoluteComponentElements = circuitJson.filter(
3736
+ (element) => "pcb_component_id" in element && element.pcb_component_id === pcbComponent.pcb_component_id
3737
+ );
3738
+ const localComponentElements = structuredClone(absoluteComponentElements);
3739
+ const componentLocalToAbsoluteTransform = compose4(
3740
+ translate6(pcbComponent.center.x, pcbComponent.center.y),
3741
+ rotateDEG3(pcbComponent.rotation)
3742
+ );
3743
+ let absoluteToComponentLocalTransform = inverse2(
3744
+ componentLocalToAbsoluteTransform
3745
+ );
3746
+ if (pcbComponent.layer === "bottom") {
3747
+ absoluteToComponentLocalTransform = compose4(
3748
+ flipY(),
3749
+ absoluteToComponentLocalTransform
3750
+ );
3751
+ }
3752
+ transformPCBElements(
3753
+ localComponentElements,
3754
+ absoluteToComponentLocalTransform
3755
+ );
3756
+ for (const localElement of localComponentElements) {
3757
+ if (localElement.type === "pcb_silkscreen_graphic") {
3758
+ transformSilkscreenGraphic({
3759
+ absoluteToComponentLocalTransform,
3760
+ graphic: localElement
3761
+ });
2699
3762
  }
2700
3763
  }
2701
- const boardPropsStr = boardProps.join(" ");
2702
- const context = createBoardConversionContext(circuitJson);
2703
- const footprintTsx = generateFootprintTsx(circuitJson, context);
2704
- const copperPoursTsx = generateCopperPoursTsx(circuitJson, context);
2705
- const boardNetsTsx = generateBoardNetsTsx(context);
2706
- const boardCadModelsTsx = generateBoardCadModelsTsx({
2707
- circuitJson,
2708
- pcbBoard
3764
+ localizeTextRotations({
3765
+ absoluteComponentElements,
3766
+ componentCcwRotationDegrees: pcbComponent.rotation,
3767
+ localComponentElements
2709
3768
  });
2710
- const schematicElements = generateBoardSchematicElements(circuitJson);
2711
- const schematicSheetTsx = generateBoardSchematicSheetTsx({
3769
+ localizePrimitiveRotations({
3770
+ absoluteComponentElements,
3771
+ absoluteToComponentLocalRotationTransform: inverse2(
3772
+ componentLocalToAbsoluteTransform
3773
+ ),
3774
+ localComponentElements
3775
+ });
3776
+ if (pcbComponent.layer === "bottom") {
3777
+ localizeBottomComponentLayers(localComponentElements);
3778
+ }
3779
+ normalizeRoutableElementCoordinates(localComponentElements);
3780
+ normalizeFloatingPointNoise(localComponentElements);
3781
+ return localComponentElements;
3782
+ };
3783
+
3784
+ // lib/board-converter/stages/convert-pcb-components.ts
3785
+ var getComponentFootprintTsx = ({
3786
+ circuitJson,
3787
+ footprintConversionContext,
3788
+ pcbComponent
3789
+ }) => {
3790
+ const componentElements = localizePcbComponentElements({
2712
3791
  circuitJson,
2713
- schematicElements
3792
+ pcbComponent
2714
3793
  });
2715
- const children = [
2716
- ...boardNetsTsx,
2717
- footprintTsx ? `<chip name="${context.pcbChipName}" noSchematicRepresentation footprint={${footprintTsx}} />` : "",
2718
- ...copperPoursTsx,
2719
- ...boardCadModelsTsx,
2720
- schematicSheetTsx
2721
- ].filter(Boolean).join("\n");
3794
+ return generateFootprintTsx(componentElements, footprintConversionContext);
3795
+ };
3796
+ var convertPcbComponents = ({
3797
+ circuitJson,
3798
+ footprintConversionContext,
3799
+ pcbComponentRuntimeNameByPcbComponentId,
3800
+ pcbBoard,
3801
+ boardChildren
3802
+ }) => {
3803
+ const pcbComponents = circuitJson.filter(
3804
+ (element) => element.type === "pcb_component"
3805
+ );
3806
+ const cadModelGroupByPcbComponentId = /* @__PURE__ */ new Map();
3807
+ for (const cadModelGroup of getCadModelGroups(circuitJson)) {
3808
+ const pcbComponentId = cadModelGroup.cadComponents[0]?.pcb_component_id;
3809
+ if (pcbComponentId === void 0) continue;
3810
+ const existingCadModelGroup = cadModelGroupByPcbComponentId.get(pcbComponentId);
3811
+ cadModelGroupByPcbComponentId.set(
3812
+ pcbComponentId,
3813
+ existingCadModelGroup ? {
3814
+ ...existingCadModelGroup,
3815
+ cadComponents: [
3816
+ ...existingCadModelGroup.cadComponents,
3817
+ ...cadModelGroup.cadComponents
3818
+ ]
3819
+ } : cadModelGroup
3820
+ );
3821
+ }
3822
+ for (const pcbComponent of pcbComponents) {
3823
+ const sourceComponent = circuitJson.find(
3824
+ (element) => element.type === "source_component" && element.source_component_id === pcbComponent.source_component_id
3825
+ );
3826
+ const schematicComponents = getSchematicComponentsForPcbComponent({
3827
+ circuitJson,
3828
+ pcbComponent
3829
+ });
3830
+ const schematicSourceComponentIds = new Set(
3831
+ schematicComponents.map((schematicComponent) => schematicComponent.source_component_id).filter(
3832
+ (sourceComponentId) => sourceComponentId !== void 0
3833
+ )
3834
+ );
3835
+ const sourceComponents = circuitJson.filter(
3836
+ (element) => element.type === "source_component" && (element.source_component_id === pcbComponent.source_component_id || element.source_component_id !== void 0 && schematicSourceComponentIds.has(element.source_component_id))
3837
+ );
3838
+ const { cadModelParentCenter, componentPosition } = getPcbComponentRenderPlacement({
3839
+ circuitJson,
3840
+ pcbBoard,
3841
+ pcbComponent
3842
+ });
3843
+ const footprintTsx = getComponentFootprintTsx({
3844
+ circuitJson,
3845
+ footprintConversionContext,
3846
+ pcbComponent
3847
+ });
3848
+ const pinLabels = getPinLabels({
3849
+ circuitJson,
3850
+ footprintConversionContext,
3851
+ pcbComponent,
3852
+ sourceComponents
3853
+ });
3854
+ const componentName = pcbComponentRuntimeNameByPcbComponentId.get(
3855
+ pcbComponent.pcb_component_id
3856
+ );
3857
+ if (!componentName) {
3858
+ throw new Error("Expected every PCB component to have a runtime name");
3859
+ }
3860
+ const cadModelGroup = cadModelGroupByPcbComponentId.get(
3861
+ pcbComponent.pcb_component_id
3862
+ );
3863
+ const localizedCadModelGroup = cadModelGroup ? {
3864
+ ...cadModelGroup,
3865
+ parentPlacement: {
3866
+ center: cadModelParentCenter,
3867
+ ccwRotationDegrees: pcbComponent.rotation,
3868
+ layer: pcbComponent.layer === "bottom" ? "bottom" : "top"
3869
+ }
3870
+ } : void 0;
3871
+ const cadModelTsx = localizedCadModelGroup ? generateCadModelGroupModelTsx({
3872
+ cadModelGroup: localizedCadModelGroup,
3873
+ pcbBoard
3874
+ }) : void 0;
3875
+ const childModelElementsTsx = localizedCadModelGroup ? generateCadModelGroupChildrenTsx({
3876
+ cadModelGroup: localizedCadModelGroup,
3877
+ pcbBoard
3878
+ }) : [];
3879
+ const componentProps = [
3880
+ `name=${formatJsxStringAttribute(componentName)}`,
3881
+ `pcbX={${componentPosition.x}}`,
3882
+ `pcbY={${componentPosition.y}}`,
3883
+ `pcbRotation="${pcbComponent.rotation}deg"`,
3884
+ `layer="${pcbComponent.layer}"`,
3885
+ "noSchematicRepresentation"
3886
+ ];
3887
+ if (cadModelTsx) {
3888
+ componentProps.push(`cadModel={${cadModelTsx}}`);
3889
+ }
3890
+ if (localizedCadModelGroup && isJscadCadModelGroupTranslucent(localizedCadModelGroup)) {
3891
+ componentProps.push("showAsTranslucentModel");
3892
+ }
3893
+ if (pinLabels) {
3894
+ componentProps.push(`pinLabels={${JSON.stringify(pinLabels)}}`);
3895
+ }
3896
+ if (sourceComponent?.manufacturer_part_number) {
3897
+ componentProps.push(
3898
+ `manufacturerPartNumber=${formatJsxStringAttribute(sourceComponent.manufacturer_part_number)}`
3899
+ );
3900
+ }
3901
+ if (sourceComponent?.supplier_part_numbers && Object.keys(sourceComponent.supplier_part_numbers).length > 0) {
3902
+ componentProps.push(
3903
+ `supplierPartNumbers={${JSON.stringify(sourceComponent.supplier_part_numbers)}}`
3904
+ );
3905
+ }
3906
+ if (pcbComponent.do_not_place === true) {
3907
+ componentProps.push("doNotPlace");
3908
+ }
3909
+ if (pcbComponent.obstructs_within_bounds === false) {
3910
+ componentProps.push("obstructsWithinBounds={false}");
3911
+ }
3912
+ const chipOpeningTsx = `<chip ${componentProps.join(" ")} footprint={${footprintTsx ?? "<footprint />"}}`;
3913
+ boardChildren.push(
3914
+ childModelElementsTsx.length === 0 ? `${chipOpeningTsx} />` : `${chipOpeningTsx}>${childModelElementsTsx.join("\n")}</chip>`
3915
+ );
3916
+ }
3917
+ };
3918
+
3919
+ // lib/board-converter/stages/convert-standalone-pcb-primitives.ts
3920
+ var convertStandalonePcbPrimitives = ({
3921
+ circuitJson,
3922
+ footprintConversionContext,
3923
+ pcbBoard,
3924
+ boardChildren
3925
+ }) => {
3926
+ const existingPcbComponentIds = new Set(
3927
+ circuitJson.flatMap(
3928
+ (element) => element.type === "pcb_component" ? [element.pcb_component_id] : []
3929
+ )
3930
+ );
3931
+ const standaloneElements = circuitJson.filter(
3932
+ (element) => !("pcb_component_id" in element) || !element.pcb_component_id || !existingPcbComponentIds.has(element.pcb_component_id)
3933
+ );
3934
+ const footprintTsx = generateFootprintTsx(
3935
+ standaloneElements,
3936
+ footprintConversionContext
3937
+ );
3938
+ if (!footprintTsx) return;
3939
+ const footprintContainerProps = [
3940
+ `name="${footprintConversionContext.pcbChipName}"`,
3941
+ "noSchematicRepresentation",
3942
+ "obstructsWithinBounds={false}"
3943
+ ];
3944
+ if (pcbBoard.center.x !== 0) {
3945
+ footprintContainerProps.push(`pcbX={${-pcbBoard.center.x}}`);
3946
+ }
3947
+ if (pcbBoard.center.y !== 0) {
3948
+ footprintContainerProps.push(`pcbY={${-pcbBoard.center.y}}`);
3949
+ }
3950
+ boardChildren.push(
3951
+ `<chip${footprintContainerProps.length > 0 ? ` ${footprintContainerProps.join(" ")}` : ""} footprint={${footprintTsx}} />`
3952
+ );
3953
+ };
3954
+
3955
+ // lib/board-converter/convert-board-circuit-json.ts
3956
+ var conversionStages = [
3957
+ convertBoardProperties,
3958
+ convertPcbComponents,
3959
+ convertBoardNets,
3960
+ convertBoardTraces,
3961
+ convertCopperPours,
3962
+ convertBoardSchematic,
3963
+ convertStandalonePcbPrimitives,
3964
+ convertBoardCadModels
3965
+ ];
3966
+ var convertBoardCircuitJson = ({
3967
+ circuitJson,
3968
+ componentName
3969
+ }) => {
3970
+ const pcbBoard = circuitJson.find((element) => element.type === "pcb_board");
3971
+ if (!pcbBoard) {
3972
+ throw new Error("Board conversion requires a pcb_board element");
3973
+ }
3974
+ const {
3975
+ pcbChipName,
3976
+ pcbComponentRuntimeNameByPcbComponentId,
3977
+ standaloneCadModelRuntimeNameByCadComponentId
3978
+ } = getBoardChildRuntimeNames(circuitJson);
3979
+ const context = {
3980
+ circuitJson,
3981
+ footprintConversionContext: createComponentPreservingBoardConversionContext(
3982
+ {
3983
+ circuitJson,
3984
+ pcbChipName,
3985
+ pcbComponentRuntimeNameByPcbComponentId
3986
+ }
3987
+ ),
3988
+ pcbComponentRuntimeNameByPcbComponentId,
3989
+ standaloneCadModelRuntimeNameByCadComponentId,
3990
+ pcbBoard,
3991
+ boardProps: [],
3992
+ boardChildren: []
3993
+ };
3994
+ for (const convertStage of conversionStages) {
3995
+ convertStage(context);
3996
+ }
3997
+ const boardProps = context.boardProps.join(" ");
3998
+ const boardChildren = context.boardChildren.join("\n");
2722
3999
  return `
2723
4000
  ${componentName ? `export const ${componentName} =` : "export default"} () => (
2724
- <board${boardPropsStr ? ` ${boardPropsStr}` : ""}>
2725
- ${children}
4001
+ <board${boardProps ? ` ${boardProps}` : ""}>
4002
+ ${boardChildren.replace(/\n/g, "\n ")}
2726
4003
  </board>
2727
4004
  )
2728
4005
  ${componentName ? `export default ${componentName}` : ""}
2729
4006
  `.replace(/\n\s*\n/g, "\n").trim();
2730
4007
  };
2731
4008
 
4009
+ // lib/get-board-using-template.ts
4010
+ var getBoardUsingTemplate = ({
4011
+ circuitJson,
4012
+ componentName
4013
+ }) => convertBoardCircuitJson({ circuitJson, componentName });
4014
+
2732
4015
  // lib/get-pin-attributes.ts
2733
4016
  var attributeNames = {
2734
4017
  is_input: "isInput",