circuit-json-to-tscircuit 0.0.94 → 0.0.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-WQVP4ME2.js → chunk-ULPDKPX5.js} +6 -42
- package/dist/cli/main.js +4 -4
- package/dist/lib/index.js +1 -1
- package/lib/board-converter/get-pcb-component-render-placement.ts +5 -80
- package/lib/board-converter/get-pin-labels.ts +6 -1
- package/lib/board-converter/localize-pcb-component-elements.ts +1 -0
- package/lib/board-converter/stages/convert-pcb-components.ts +0 -1
- package/package.json +3 -3
- package/tests/__snapshots__/beagleplay-usb-cad-origin-generated-3d.snap.png +0 -0
- package/tests/__snapshots__/beagleplay-usb-cad-origin-source-3d.snap.png +0 -0
- package/tests/__snapshots__/board-imported-bga-pin-labels-orangecrab-bga-pin-labels.snap.svg +1 -0
- package/tests/__snapshots__/dp83825-usb-cad-origin-generated-bottom-3d.snap.png +0 -0
- package/tests/__snapshots__/dp83825-usb-cad-origin-generated-top-3d.snap.png +0 -0
- package/tests/__snapshots__/dp83825-usb-cad-origin-source-bottom-3d.snap.png +0 -0
- package/tests/__snapshots__/dp83825-usb-cad-origin-source-top-3d.snap.png +0 -0
- package/tests/beagleplay-usb-cad-origin.test.ts +57 -0
- package/tests/board-imported-bga-pin-labels.test.ts +145 -0
- package/tests/dp83825-usb-cad-origin.test.ts +82 -0
- package/tests/fixtures/beagleplay-j13-origin.circuit.json +173 -0
- package/tests/fixtures/expect-jscad-cad-model-3d-snapshots.ts +66 -0
- package/tests/fixtures/expect-png-snapshot.ts +59 -0
- package/tests/fixtures/expect-ti-evm-3d-snapshot.ts +2 -57
- package/tests/fixtures/ti-evms/README.md +8 -0
- package/tests/fixtures/ti-evms/dp83825-usb-cad-origin.circuit.json.gz +0 -0
|
@@ -4019,30 +4019,8 @@ var generateFootprintTsx = (circuitJson, context) => {
|
|
|
4019
4019
|
};
|
|
4020
4020
|
|
|
4021
4021
|
// lib/board-converter/get-pcb-component-render-placement.ts
|
|
4022
|
-
import { getBoundsOfPcbElements } from "@tscircuit/circuit-json-util";
|
|
4023
4022
|
import { applyToPoint as applyToPoint6, inverse, translate as translate5 } from "transformation-matrix";
|
|
4024
|
-
var physicalPcbElementTypes = /* @__PURE__ */ new Set([
|
|
4025
|
-
"pcb_copper_text",
|
|
4026
|
-
"pcb_cutout",
|
|
4027
|
-
"pcb_hole",
|
|
4028
|
-
"pcb_keepout",
|
|
4029
|
-
"pcb_plated_hole",
|
|
4030
|
-
"pcb_smtpad",
|
|
4031
|
-
"pcb_trace",
|
|
4032
|
-
"pcb_via"
|
|
4033
|
-
]);
|
|
4034
|
-
var isPhysicalPcbElement = (element) => physicalPcbElementTypes.has(element.type);
|
|
4035
|
-
var getElementsAsRenderedByGeneratedTsx = (elements) => elements.map((element) => {
|
|
4036
|
-
if (element.type !== "pcb_plated_hole" || element.shape !== "rotated_pill_hole_with_rect_pad") {
|
|
4037
|
-
return element;
|
|
4038
|
-
}
|
|
4039
|
-
return {
|
|
4040
|
-
...element,
|
|
4041
|
-
rect_ccw_rotation: element.hole_ccw_rotation
|
|
4042
|
-
};
|
|
4043
|
-
});
|
|
4044
4023
|
var getPcbComponentRenderPlacement = ({
|
|
4045
|
-
circuitJson,
|
|
4046
4024
|
pcbBoard,
|
|
4047
4025
|
pcbComponent
|
|
4048
4026
|
}) => {
|
|
@@ -4050,32 +4028,18 @@ var getPcbComponentRenderPlacement = ({
|
|
|
4050
4028
|
inverse(translate5(pcbBoard.center.x, pcbBoard.center.y)),
|
|
4051
4029
|
pcbComponent.center
|
|
4052
4030
|
);
|
|
4053
|
-
const physicalElements = circuitJson.filter(
|
|
4054
|
-
(element) => isPhysicalPcbElement(element) && "pcb_component_id" in element && element.pcb_component_id === pcbComponent.pcb_component_id
|
|
4055
|
-
);
|
|
4056
|
-
const physicalBounds = getBoundsOfPcbElements(
|
|
4057
|
-
getElementsAsRenderedByGeneratedTsx(physicalElements)
|
|
4058
|
-
);
|
|
4059
|
-
const physicalWidth = physicalBounds.maxX - physicalBounds.minX;
|
|
4060
|
-
const physicalHeight = physicalBounds.maxY - physicalBounds.minY;
|
|
4061
|
-
if (Number.isFinite(physicalWidth) && Number.isFinite(physicalHeight) && physicalWidth > 0 && physicalHeight > 0) {
|
|
4062
|
-
return {
|
|
4063
|
-
cadModelParentCenter: {
|
|
4064
|
-
x: (physicalBounds.minX + physicalBounds.maxX) / 2,
|
|
4065
|
-
y: (physicalBounds.minY + physicalBounds.maxY) / 2
|
|
4066
|
-
},
|
|
4067
|
-
componentPosition
|
|
4068
|
-
};
|
|
4069
|
-
}
|
|
4070
4031
|
return {
|
|
4071
|
-
cadModelParentCenter:
|
|
4032
|
+
cadModelParentCenter: pcbComponent.center,
|
|
4072
4033
|
componentPosition
|
|
4073
4034
|
};
|
|
4074
4035
|
};
|
|
4075
4036
|
|
|
4076
4037
|
// lib/board-converter/get-pin-labels.ts
|
|
4077
4038
|
var getCompoundPinNumber = (sourcePort) => {
|
|
4078
|
-
if (sourcePort.pin_number !== void 0)
|
|
4039
|
+
if (sourcePort.pin_number !== void 0) {
|
|
4040
|
+
const pinNumber = Number(sourcePort.pin_number);
|
|
4041
|
+
return Number.isSafeInteger(pinNumber) && pinNumber >= 0 ? pinNumber : void 0;
|
|
4042
|
+
}
|
|
4079
4043
|
const firstPinNumber = /^\d+/u.exec(sourcePort.name)?.[0];
|
|
4080
4044
|
return firstPinNumber ? Number.parseInt(firstPinNumber, 10) : void 0;
|
|
4081
4045
|
};
|
|
@@ -4262,6 +4226,7 @@ var getOppositeOuterLayer = (layer) => {
|
|
|
4262
4226
|
};
|
|
4263
4227
|
var localizeBottomComponentLayers = (localComponentElements) => {
|
|
4264
4228
|
for (const localElement of localComponentElements) {
|
|
4229
|
+
if (localElement.type === "pcb_stackup") continue;
|
|
4265
4230
|
if ("layer" in localElement && (localElement.layer === "top" || localElement.layer === "bottom")) {
|
|
4266
4231
|
localElement.layer = getOppositeOuterLayer(localElement.layer);
|
|
4267
4232
|
}
|
|
@@ -4403,7 +4368,6 @@ var convertPcbComponents = ({
|
|
|
4403
4368
|
(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))
|
|
4404
4369
|
);
|
|
4405
4370
|
const { cadModelParentCenter, componentPosition } = getPcbComponentRenderPlacement({
|
|
4406
|
-
circuitJson,
|
|
4407
4371
|
pcbBoard,
|
|
4408
4372
|
pcbComponent
|
|
4409
4373
|
});
|
package/dist/cli/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
convertCircuitJsonToTscircuit
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-ULPDKPX5.js";
|
|
5
5
|
|
|
6
6
|
// cli/main.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -16,7 +16,7 @@ var package_default = {
|
|
|
16
16
|
"./cli": "./dist/cli/main.js"
|
|
17
17
|
},
|
|
18
18
|
type: "module",
|
|
19
|
-
version: "0.0.
|
|
19
|
+
version: "0.0.95",
|
|
20
20
|
repository: {
|
|
21
21
|
type: "git",
|
|
22
22
|
url: "https://github.com/tscircuit/circuit-json-to-tscircuit.git"
|
|
@@ -38,7 +38,7 @@ var package_default = {
|
|
|
38
38
|
"@types/react": "18",
|
|
39
39
|
"@types/react-dom": "18",
|
|
40
40
|
"bun-match-svg": "^0.0.15",
|
|
41
|
-
"circuit-json": "^0.0.
|
|
41
|
+
"circuit-json": "^0.0.539",
|
|
42
42
|
"circuit-json-to-gltf": "0.0.151",
|
|
43
43
|
"circuit-to-svg": "https://pkg.pr.new/tscircuit/circuit-to-svg@738ef1d",
|
|
44
44
|
color: "^5.0.3",
|
|
@@ -52,7 +52,7 @@ var package_default = {
|
|
|
52
52
|
shiki: "^4.3.1",
|
|
53
53
|
"stack-svgs": "^0.0.1",
|
|
54
54
|
"transformation-matrix": "3.1.0",
|
|
55
|
-
tscircuit: "0.0.
|
|
55
|
+
tscircuit: "0.0.3043-libonly",
|
|
56
56
|
tsup: "^8.3.5",
|
|
57
57
|
zod: "^3.25.76"
|
|
58
58
|
},
|
package/dist/lib/index.js
CHANGED
|
@@ -1,64 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { AnyCircuitElement, PcbBoard, PcbComponent } from "circuit-json"
|
|
1
|
+
import type { PcbBoard, PcbComponent } from "circuit-json"
|
|
3
2
|
import { applyToPoint, inverse, translate } from "transformation-matrix"
|
|
4
3
|
|
|
5
|
-
type PhysicalPcbElementType =
|
|
6
|
-
| "pcb_copper_text"
|
|
7
|
-
| "pcb_cutout"
|
|
8
|
-
| "pcb_hole"
|
|
9
|
-
| "pcb_keepout"
|
|
10
|
-
| "pcb_plated_hole"
|
|
11
|
-
| "pcb_smtpad"
|
|
12
|
-
| "pcb_trace"
|
|
13
|
-
| "pcb_via"
|
|
14
|
-
|
|
15
|
-
const physicalPcbElementTypes = new Set<PhysicalPcbElementType>([
|
|
16
|
-
"pcb_copper_text",
|
|
17
|
-
"pcb_cutout",
|
|
18
|
-
"pcb_hole",
|
|
19
|
-
"pcb_keepout",
|
|
20
|
-
"pcb_plated_hole",
|
|
21
|
-
"pcb_smtpad",
|
|
22
|
-
"pcb_trace",
|
|
23
|
-
"pcb_via",
|
|
24
|
-
])
|
|
25
|
-
|
|
26
|
-
const isPhysicalPcbElement = (
|
|
27
|
-
element: AnyCircuitElement,
|
|
28
|
-
): element is AnyCircuitElement & { type: PhysicalPcbElementType } =>
|
|
29
|
-
physicalPcbElementTypes.has(element.type as PhysicalPcbElementType)
|
|
30
|
-
|
|
31
|
-
const getElementsAsRenderedByGeneratedTsx = (
|
|
32
|
-
elements: AnyCircuitElement[],
|
|
33
|
-
): AnyCircuitElement[] =>
|
|
34
|
-
elements.map((element) => {
|
|
35
|
-
if (
|
|
36
|
-
element.type !== "pcb_plated_hole" ||
|
|
37
|
-
element.shape !== "rotated_pill_hole_with_rect_pad"
|
|
38
|
-
) {
|
|
39
|
-
return element
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// The plated-hole component has one rotation for both the hole and its
|
|
43
|
-
// rectangular pad. The TSX converter uses the imported hole rotation, so
|
|
44
|
-
// calculate the parent bounds from that same rendered geometry.
|
|
45
|
-
return {
|
|
46
|
-
...element,
|
|
47
|
-
rect_ccw_rotation: element.hole_ccw_rotation,
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
|
|
51
4
|
export interface PcbComponentRenderPlacement {
|
|
52
5
|
cadModelParentCenter: { x: number; y: number }
|
|
53
6
|
componentPosition: { x: number; y: number }
|
|
54
7
|
}
|
|
55
8
|
|
|
56
9
|
export const getPcbComponentRenderPlacement = ({
|
|
57
|
-
circuitJson,
|
|
58
10
|
pcbBoard,
|
|
59
11
|
pcbComponent,
|
|
60
12
|
}: {
|
|
61
|
-
circuitJson: AnyCircuitElement[]
|
|
62
13
|
pcbBoard: PcbBoard
|
|
63
14
|
pcbComponent: PcbComponent
|
|
64
15
|
}): PcbComponentRenderPlacement => {
|
|
@@ -66,37 +17,11 @@ export const getPcbComponentRenderPlacement = ({
|
|
|
66
17
|
inverse(translate(pcbBoard.center.x, pcbBoard.center.y)),
|
|
67
18
|
pcbComponent.center,
|
|
68
19
|
)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"pcb_component_id" in element &&
|
|
73
|
-
element.pcb_component_id === pcbComponent.pcb_component_id,
|
|
74
|
-
)
|
|
75
|
-
const physicalBounds = getBoundsOfPcbElements(
|
|
76
|
-
getElementsAsRenderedByGeneratedTsx(physicalElements),
|
|
77
|
-
)
|
|
78
|
-
const physicalWidth = physicalBounds.maxX - physicalBounds.minX
|
|
79
|
-
const physicalHeight = physicalBounds.maxY - physicalBounds.minY
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
Number.isFinite(physicalWidth) &&
|
|
83
|
-
Number.isFinite(physicalHeight) &&
|
|
84
|
-
physicalWidth > 0 &&
|
|
85
|
-
physicalHeight > 0
|
|
86
|
-
) {
|
|
87
|
-
return {
|
|
88
|
-
cadModelParentCenter: {
|
|
89
|
-
x: (physicalBounds.minX + physicalBounds.maxX) / 2,
|
|
90
|
-
y: (physicalBounds.minY + physicalBounds.maxY) / 2,
|
|
91
|
-
},
|
|
92
|
-
componentPosition,
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Core retains the board-local chip position when there are no physical
|
|
97
|
-
// footprint primitives, so use that position as the CAD model's parent.
|
|
20
|
+
// Core anchors cadModel.positionOffset at the chip's pre-layout footprint
|
|
21
|
+
// origin, not at its pad bounds. CAD offsets use absolute board XY axes;
|
|
22
|
+
// the chip's board-local position becomes pcbComponent.center after rendering.
|
|
98
23
|
return {
|
|
99
|
-
cadModelParentCenter:
|
|
24
|
+
cadModelParentCenter: pcbComponent.center,
|
|
100
25
|
componentPosition,
|
|
101
26
|
}
|
|
102
27
|
}
|
|
@@ -8,7 +8,12 @@ type SourceComponentId = NonNullable<SourceComponent["source_component_id"]>
|
|
|
8
8
|
type SourcePort = Extract<AnyCircuitElement, { type: "source_port" }>
|
|
9
9
|
|
|
10
10
|
const getCompoundPinNumber = (sourcePort: SourcePort): number | undefined => {
|
|
11
|
-
if (sourcePort.pin_number !== undefined)
|
|
11
|
+
if (sourcePort.pin_number !== undefined) {
|
|
12
|
+
const pinNumber = Number(sourcePort.pin_number)
|
|
13
|
+
return Number.isSafeInteger(pinNumber) && pinNumber >= 0
|
|
14
|
+
? pinNumber
|
|
15
|
+
: undefined
|
|
16
|
+
}
|
|
12
17
|
|
|
13
18
|
const firstPinNumber = /^\d+/u.exec(sourcePort.name)?.[0]
|
|
14
19
|
return firstPinNumber ? Number.parseInt(firstPinNumber, 10) : undefined
|
|
@@ -155,6 +155,7 @@ const localizeBottomComponentLayers = (
|
|
|
155
155
|
localComponentElements: AnyCircuitElement[],
|
|
156
156
|
): void => {
|
|
157
157
|
for (const localElement of localComponentElements) {
|
|
158
|
+
if (localElement.type === "pcb_stackup") continue
|
|
158
159
|
if (
|
|
159
160
|
"layer" in localElement &&
|
|
160
161
|
(localElement.layer === "top" || localElement.layer === "bottom")
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"./cli": "./dist/cli/main.js"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.96",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
12
|
"url": "https://github.com/tscircuit/circuit-json-to-tscircuit.git"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@types/react": "18",
|
|
29
29
|
"@types/react-dom": "18",
|
|
30
30
|
"bun-match-svg": "^0.0.15",
|
|
31
|
-
"circuit-json": "^0.0.
|
|
31
|
+
"circuit-json": "^0.0.539",
|
|
32
32
|
"circuit-json-to-gltf": "0.0.151",
|
|
33
33
|
"circuit-to-svg": "https://pkg.pr.new/tscircuit/circuit-to-svg@738ef1d",
|
|
34
34
|
"color": "^5.0.3",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"shiki": "^4.3.1",
|
|
43
43
|
"stack-svgs": "^0.0.1",
|
|
44
44
|
"transformation-matrix": "3.1.0",
|
|
45
|
-
"tscircuit": "0.0.
|
|
45
|
+
"tscircuit": "0.0.3043-libonly",
|
|
46
46
|
"tsup": "^8.3.5",
|
|
47
47
|
"zod": "^3.25.76"
|
|
48
48
|
},
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="260" data-software-used-string="@tscircuit/core@0.0.2113"><style></style><rect class="boundary" x="0" y="0" fill="#000" width="600" height="260" data-type="pcb_background" data-pcb-layer="global"/><rect class="pcb-boundary" fill="none" stroke="#fff" stroke-width="0.3" x="-300" y="-470" width="1200" height="1200" data-type="pcb_boundary" data-pcb-layer="global"/><path class="pcb-board" d="M -300 730 L 900 730 L 900 -470 L -300 -470 Z" fill="none" stroke="rgba(255, 255, 255, 0.5)" stroke-width="12" data-type="pcb_board" data-pcb-layer="board"/><circle class="pcb-pad" fill="rgb(200, 52, 52)" cx="180" cy="130" r="18" data-type="pcb_smtpad" data-pcb-layer="top" data-pad-name="U1.A1"/><circle class="pcb-pad" fill="rgb(200, 52, 52)" cx="300" cy="130" r="18" data-type="pcb_smtpad" data-pcb-layer="top" data-pad-name="U1.B2"/><circle class="pcb-pad" fill="rgb(200, 52, 52)" cx="420" cy="130" r="18" data-type="pcb_smtpad" data-pcb-layer="top" data-pad-name="U1.MP"/><style data-tscircuit-alphabet-font="" type="text/css">@font-face { font-family: 'TscircuitAlphabet'; src: url('data:font/ttf;base64,T1RUTwAJAIAAAwAQQ0ZGIFhQSpcAAAcwAABJ3U9TLzJqmmVhAAABAAAAAGBjbWFwFN0WswAABEwAAALEaGVhZC1P1HwAAACcAAAANmhoZWEDrgI1AAAA1AAAACRobXR44xAAAAAAURAAAAFQbWF4cABUUAAAAAD4AAAABm5hbWUZQ94BAAABYAAAAutwb3N0AAMAAAAABxAAAAAgAAEAAAABAADb+Z65Xw889QADA+gAAAAA5e9IsgAAAADl70iy/9P/PwKHAvkAAAADAAIAAAAAAAAAAQAAA6z/LAAAArQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAFQAAFAAAFQAAAADArQB9AAFAAACigK7AAAAjAKKArsAAAHfADEBAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAABYWFhYAEAAIQB6A6z/LAAAAvkAwQAAAAEAAAAAAiwC1wAAAAAAAAAAACIBngABAAAAAAAAAAEAWQABAAAAAAABABEAAAABAAAAAAACAAcAMwABAAAAAAADABsA/AABAAAAAAAEABkASAABAAAAAAAFAAsA2wABAAAAAAAGABgAkwABAAAAAAAHAAEAWQABAAAAAAAIAAEAWQABAAAAAAAJAAEAWQABAAAAAAAKAAEAWQABAAAAAAALAAEAWQABAAAAAAAMAAEAWQABAAAAAAANAAEAWQABAAAAAAAOAAEAWQABAAAAAAAQABEAAAABAAAAAAARAAcAMwADAAEECQAAAAIAgwADAAEECQABACIAEQADAAEECQACAA4AOgADAAEECQADADYBFwADAAEECQAEADIAYQADAAEECQAFABYA5gADAAEECQAGADAAqwADAAEECQAHAAIAgwADAAEECQAIAAIAgwADAAEECQAJAAIAgwADAAEECQAKAAIAgwADAAEECQALAAIAgwADAAEECQAMAAIAgwADAAEECQANAAIAgwADAAEECQAOAAIAgwADAAEECQAQACIAEQADAAEECQARAA4AOlRzY2lyY3VpdEFscGhhYmV0AFQAcwBjAGkAcgBjAHUAaQB0AEEAbABwAGgAYQBiAGUAdFJlZ3VsYXIAUgBlAGcAdQBsAGEAclRzY2lyY3VpdEFscGhhYmV0IFJlZ3VsYXIAVABzAGMAaQByAGMAdQBpAHQAQQBsAHAAaABhAGIAZQB0ACAA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BY6cBbz3BwWUmQWalQWcjgWciAWaggWVfQWPewU/+y8V+w5aBXqHBXmOBX2UBYGZBYebBY6bBZWZBZmUBfcOvQWcjgWciAWaggWVfgWPewWIewWBfQX7M00V+zObBXuQBX2WBYOZBYqcBZCaBZaYBZuTBZyMBfczewWchgWYgAWTfAWNewWFewWAfwV7gwX7VqAVNb0Ff5YFg5oFi5sFkZsFl5cFm5IFnIsFnIYF4FkFmIAFknwFjHoFhXwFf38Fe4QFeYoF+wzZFWbuBYmbBY+bBZaYBZqUBZyNBZyHBZmBBZR9BbAnBY17BYd7BYB+BXyDBXmIBXqQBX2VBQ75SPfu+UEVi/uBBYh7BYF9BXyCBXqIBXqOBXyUBYGZBYibBYv3gQWOmwWVmQWalAWcjwWchwWaggWVfQUO+Uj4Ko0VsmUFlX0FjnsFh3sFgX0FfYIFeYgFeo8FfZQFYrMFio0FiYwFY7sFiowFiowFYsMFiowFiowFY8oFio4FiY0FbcoFio4FiY4Fd8oFi44Fio0FgcMFi5IFi5IFlcMFjI0Fi44Fn8oFjY4FjI4FqcoFjY0FjI4Fs8oFjIwFjIwFtMMFjIwFjIwFs7sFjYwFjI0FtLMFmZQFnI8FnYgFmYIFlX0Fj3sFiHsFgX0FZGUFZV4FZFUFZE8Fb1AFeFEFg10Fk10FnlEFp1AFsk8FslUFDvlI95tWFYqJBWJjBX2CBXqHBXmOBX2UBYGZBYebBY6bBZWZBbKxBbG4BbLBBbLHBafGBZ7FBZO5BYO5BXjFBW/GBWTHBWTBBWW4BWSxBYGZBYibBY+bBZWZBZqUBZyOBZyHBZmCBbRjBYyJBY2KBbRbBYuKBY2KBbNTBYyKBYyKBbRMBYuIBY2JBapMBYuIBY2IBZ9MBYuIBYyJBZVTBYuEBYuEBYFTBYqJBYuIBXdMBYmIBYuIBWxMBYmJBYuIBWJMBYqKBYqKBWNTBYmKBYuKBWJbBQ75SPeU+FUV+zRWBXqIBXqPBX2VBYKZBYicBY+bBZaYBZqTBfc5wgUh9yQFhJoFi5sFkZsFmJYFm5IFnIsFm4UFmIAF8fsfBfH3HwWYlgWbkQWciwWchAWXgAWRewWLewWEfAUh+yQF9zlUBZqDBZZ+BY97BYh6BYJ9BX2BBXqHBXqOBfs0wAWL+yoFiHsFgX0FfIIFeogFeo4FfJQFgZkFiJsFDvlI95T39hWL90sFjpwFlZgFmpUFnI4FnIgFmoEFlX4FjnoFi/tLBfdpiwWciAWaggWVfQWOewWIegWBfgV8ggV6hwX7aYsFi/tLBYh7BYF9BXyCBXqIBXqOBXyUBYGZBYibBYv3SwX7aYsFeo8FfJQFgZgFiJwFjpsFlZkFmpQFnI4FDvlI96puFcJVBZV9BY56BYd7BYF+BXyCBXqIBXqOBX2VBTXfBYeRBYaRBYuMBYqMBYqSBYmSBYyMBYqMBY2SBYyTBYyLBYuMBZCRBY+RBfc/9zwFmpUFnI4FnIgFmoMFlX0Fj3sFiHsFgX0FDvlI90L35RX3kosFnIgFmoIFlX0FjnsFiHsFgX0FfIIFeogF+5KLBXqOBXyUBYGZBYibBY6bBZWZBZqUBQ75SPf/7RX7D4sFeY4FfZQFgZkFiJsFjpsFlZkFmZQFnY4F9w+LBZyIBZmCBZV9BY97BYd7BYF9BX2CBQ75SKOAFfhx+WMFl5cFm5EFnYwFm4UFmIAFknwFjHoFhXwF/HH9YwV/fwV7hQV5igV7kQV+lgWEmgWKnAUO+Uj4CfkPFUOeBUN4BUZQBWMvBYH7JQWj+wwFxi0F2F0FyYsF2LkFxukFo/cMBYH3JQVj5wVq9x4VjYoFkYgFkYgFjIoFjYoF4EEFi4sFjIoFj4UFkIUFi4oFjIoFuSEFjIQFjYUFlvsyBYqGBYuGBXH7GQWIhQWJhAVH+wIFiYoFiokFhocFh4YFiYoFiooFKVEFh4kFiIkFhooFh4oFh4sFh4oFM4sFh4wFh4sFh4wFhowFiI0Fh40FKcUFiowFiYwFh5AFho8Fio0FiowFRvcCBYmSBYiRBXH3GQWLkAWKkAWW9zIFjZEFjJIFufUFjIwFi4wFkJEFj5EFjIwFjIsF39UFjYwFjIwFkY4FkY4FjYwFjYsF6aMFjowFjowFkYoFkYwFjooFjooF6XMFDvlI9/v5ZRWQjgWNjAWNjAWSiwWSjQWMigWNiwWSigWSiQWMigWNiwWRhwWQhwWMigWNigWOhQWPhQWMigWLiQWMhQWNhAWL/REFh3sFgX0FfYIFeogFeY4FfZQFgZkFiJsFi/i2BTRFBXuDBXqJBXqPBX2WBYOZBYmbBY+bBZaYBfc19xUFDvlI90rmFffoiwWdiAWZggWVfQWOewWIewWBfQV9ggV5iAX8TosFg4wFg4wFiowFiosFhY8FhI8Fi4sFiowFhpEFh5EFi4wFiowFipIFiZIFi4wFi4wFjJIFjJIFjIwFi4wFj5EFj5EF9zn3PgX3L/dKBcTlBZezBYueBYOqBXeiBV6dBUaQBWaGBVBzBWttBX9mBYN9BX6ABXqGBXmNBXyTBYCYBYaaBYycBZu6BYuMBYyNBY+QBY6RBYyMBYyNBbi2BYuLBYuLBZOQBZKQBYuLBYuLBdWoBZCNBZGMBbqSBZCKBZCMBd2FBZKJBZKKBclxBYyKBY2LBZCHBZGHBYyKBY2JBaxlBYyKBYuKBY+FBY6EBYuKBYyKBZdeBYuFBYyGBYptBYqGBYuGBX1ZBYiFBYmGBUwpBYmJBYqJBfsy+04FiooFiooFDvlI9wqUFYeMBVObBXySBX+YBYabBY2bBZOaBZmVBZuQBZ2KBcB8BdeABeiJBb2WBdGxBbW2BZy5BYOvBWWzBUStBTSiBT2UBYiMBYiLBYWOBYaMBYmNBYiNBYePBYeOBYmOBYmNBYmQBYmQBYuOBYqOBYuQBYuRBYyNBYuOBY2QBY2QBY2NBY2OBY+PBY+PBY6MBY2NBZCNBZGNBY6LBY6MBduVBeKdBdGpBauoBZGqBXWtBV2uBUagBTyPBUp2BVZxBWhxBXyEBXmKBXuQBX2WBYOZBYqcBZCaBZeYBbCnBY+MBY6OBceoBY6MBY6NBdikBZOMBZSMBemGBZCKBZGKBd1yBZKIBZOHBcVeBZCGBZCGBa9UBY2EBY6FBYuKBYyKBYqEBYuDBYBQBYmHBYqHBYmIBYmHBYiIBYiHBVxfBYWGBYSHBUduBdFqBZGGBZKHBcBUBYyIBY6JBY2GBY6HBYuIBY2HBZZQBYuJBYyIBYqFBYuGBYqIBYuJBXNKBYuLBYuLBYeEBYaEBYuLBYuLBVZUBYWHBYaHBTlfBYWJBYWIBU19BYWLBYWKBSeNBYiMBYiLBTmWBQ75SPgF98IVi+oFj5sFlJgFmpUFnI4FnIgFmoEFlX4FjnsFiywF6YsFnIcFmoIFlX0FjnsFiHsFgX0FfIIFeogFLYsFi/s9BYh7BYF9BXyCBXqIBXqOBXyUBYKZBYebBYv3PQX71IsFhIwFhYwFiYwFiYsFhY8FhY0Fio0FiYwFh5AFh5AFi40FiY0FipEFiZEFi40Fi40FjJEFjJEFjI0Fi40Fj5EFjpAF+Ab4PgWYlgWcjwWcigWbgwWWfgWQfAWKegWCfQX7yfv5BQ75SKn5QRWOmwWVmQWalAWcjwX4WIsFnIcFmoIFlX0FjnsFiHsFgX0FfIIFeogF/CuLBYv7dwX3HakFk4sFk4wF9x57BZOIBZOJBcJuBZGGBZGHBbFiBYyJBYyKBY6FBY+FBYuKBYyJBaj7BAWLigWLigWLhAWMhAWKigWLiQV9UQWLiwWLigWHhAWIhAWLiwWKigVnXgWFhQWFhgVFYQWFiQWFiAU5dQWGigWGigU+hwWFjAWGiwVKlwWGjQWGjAVWogWGjgWFjwVUugWAmAWHmwWOmwWUmQWZlQWcjwWciQWaggW9YQW0eQXCgQXNjgXSngXFrgWlqwWUswVz5wVxpwVjoAX7DZkF+01iBYaLBYaKBYeMBYiKBYaNBYaMBYiNBYeMBYePBYeNBYmOBYiNBYmQBYiPBYuOBYmPBYuPBYqQBQ75SPch96QVi4sFi4sFbW4FioMFnlkFm24FtWEFtHIFzoEF2o8FxaMFsqoFnrMFjaoFf7MFeKwFcqQFZ5sFUJcFU44FU4MFU28F+yxcFYqPBYyRBYy5BYuLBYuLBY7cBY2RBYyRBafaBYyNBYuNBbTfBZCSBZCRBcC7BY6NBY2NBdC4BZGOBZCOBdykBYyLBYuLBeKlBZ2NBZuGBZmBBZN8BY17BYZ7BYB+BXyDBTRyBUFzBU9kBV5jBWdABXJEBYp7BZGOBZCPBdGtBZKNBZKOBc6UBZCLBY+LBcuJBY+KBY+LBc58BZCKBZCJBbt2BZKGBZKHBa5oBY6GBY+GBaNhBYyHBY2HBZpZBYyEBYyFBYlbBYuLBYuLBYmDBYmDBYuLBYuLBW9UBYuKBYqKBYaGBYeFBYqKBYqLBVhiBYaIBYaIBUNtBYOJBYOJBS+GBYeMBYaLBTmXBYqLBYqLBYSOBYOOBYuLBYqLBVasBYiPBYaNBVq8BYeQBYeQBXaxBYqOBYqOBXXHBYqSBYmTBQ75SPfP90gVg/saBYd7BYF+BXyCBXmJBXqPBX2VBYKZBYmbBZL3HAWMjQWLjQWc9wcFi4wFjI0Fms0FjI0Fi4wFnsYFjIwFi40FoL4FjIwFi40ForcFjIwFi40Fo7EFjIwFjI0FoqoFjI0FjIwFoqYFjYwF+96LBXmOBX2UBYGZBYibBY6bBZWZBZmUBZ2PBfhfiwWQigWQigWOigWPigWPiAWQiQWOiQWNiQWOhwWPhwWMiAWNiAWMhgWNhgWLiAWMiAWJhgWLhgWKiAWKiAWIhwWJhgWIiQWJiAWHiAWHiAVqeQV9gQVkaAV3dAV2bgV1aAV2YgV4WwV5UwV8TAUO+Uj3pvf2FUR0BUpaBXBZBYleBaZdBcVnBdlzBciJBeCmBb6wBaLEBYLEBWK1BTi2BVKfBdH3sxU4kQVLgAVXcwVrXQWVaQW4ZQW/bAXDeAWwlgXetQWdpgWMwwV1rwVr9wEVi4sFk4gFk4kFi4sFjIsF0GgFjokFj4kFjocFj4gFjYgFjYgFrVIFjIkFjYgFjIYFjYYFiogFjIgFiTwFi4gFi4kFiYUFioYFiYkFiogFbVwFiYkFiYgFh4gFh4cFiIoFiYkFUm4F0mYFkIcFkYcFw1IFjYcFj4gFjIcFjYgFjIYFjIcFmDoFi4kFi4kFioUFi4UFiokFiokFbDwFiYcFiogFh4cFiYgFiIgFiIkFR1sFhYgFhYcFJ2sFg4oFg4oFP40FhY0FhIsFMqcFho4FhY0FQ7gFio0FiYwFho8Fh5AFio0FiYwFZc0Fio4Fio4FipAFiZAFjI4Fio4Fjs0Fi4wFi4sFjZMFjZIFi4wFi4sFr80Fi4wFjIwFkJAFkJEFi4wFjIsF2sYFkY4Fko8FmI8FaZ8FiI4Fh40FUrwFiY0FiI4FiY8FiI8Fio4FiY4FeMsFi40Fi4wFipIFi5IFi4wFi4wFjpIFjZIFjIsFjI0Fu9IFjo4FjY4Fj44Fj44Fjo0Fjo0F0KsFkYwFkY0F15gFkIsFkYwF7YQFDvlI+Fr4mBWVnQV8xwVwqwVfoAVIkwVHgwVTdAVnagV+XwWYYAWvaQXDdAXMhAXMmgW8qgVf/BoVi4oFZ0IFgX4FfIIFeYgFeo8FfZUFgpkFiJsFj5sFr9QFtuoFwPcXBYuLBYuMBZKcBT55BYOKBYKLBTuUBYWNBYWMBUWoBYSPBYSQBVm6BYqNBYmMBYiQBYeQBYuOBYqNBXfNBYqOBYqOBYyQBYqQBYyOBYyPBZ/NBYyNBYuNBY+QBY6QBY2NBYyNBb26BZKPBZKQBdGnBZGMBZGNBduVBZGKBZGMBduBBZKJBZKJBcdvBYyKBYyLBZGGBZGHBYuKBYyKBbNcBYyKBYyKBY6FBY6F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iAWZggWVfQWPewWHewWBfQV9ggV6iAX8FYsFi/t4BffQiwWdiAWZggWVfQWOewWIewWBfQV9ggV5hwUO+Uj4fve0Ffs1iwV6jwV9lAWBmQWImwWOmwWVmQWZlAWdjgX3bIsFj4oFkIsFj4kFj4oFj4kFj4kFjogFj4kFjYgFjocFjYgFjYcFjIcFjYcFi4cFjIcFiocFi4cFXvtaBYqJBYuIBYiGBYiGBYmJBYqJBUtGBYmKBYqJBYWIBYaHBYmKBYmKBTpwBYqLBYuKBYOKBYOKBYqLBYqLBTWSBYSNBYSNBTmuBYWOBYaOBUfCBYaQBYaQBV/QBYuMBYqMBYmRBYiSBYuMBYuMBXf3UgWLkQWLkgWx90IFjIwFi40FjpEFjpEFjIwFjI0FzdsFjIwFi4wFkZAFkY8FjIwFjIsF37QFi4sFi4sFk40Fk44Fi4sFi4sF9xeVBZKKBZGLBfcQcQWbhQWXfwWRfAWLegWEfAV+gAV7hQV5iwX7CKMF+weCBUhrBVZJBWj7MwWe+0IFrlUFxFwF0W4Fz4UFx6AFvL8FDvlI+H74OhWL95sFj5sFlJkFmpQFnI8FnIcFmoIFlX0FjnsFi/0RBYh7BYF9BXyCBXqIBXqOBXyUBYKZBYebBYv3tgX8DosFi/u2BYd7BYJ9BXyCBXqIBXqOBXyUBYGZBYibBYv34AWL98UFjpsFlZkFmpQFnI8FnIcFmoIFlH0Fj3sFi/ubBQ75SPfu+UEVi/0RBYh7BYF9BXyCBXqIBXqOBXyUBYGZBYibBYv5EQWOmwWVmQWalAWcjwWchwWaggWVfQUO+Ui/9z4Vio4Fia8FjpsFlJkFmZUFnY4FnIkFmoIFlX4Fj3sFjGsFkXIFlXYFmXkFnX0FunoFzoQFu5EFwp8FoJkFnJ0Fl6EFk70FivhxBY6bBZWZBZqUBZyPBZyHBZqCBZR+BY96BYz8dAWKiAWLiAWCTgWJhQWJhAV5awWHhgWIhwVycQWHiAWIiAVtdwWGiQWGiAVKcwWGigWFigVRhAWGiwWGiwU8kwWFjAWFjAVRoQWFjwWFjQVvoQWHjwWHjgV2pQWJkAWIjwV+qAWKkAWJjwWEqwUO+Uj3Uff3FfsBOQWL+3UFiHsFgX0FfYIFeYgFeo4FfZQFgZkFh5sFi/kRBY+bBZWZBZmUBZyPBZ2HBZmCBZV9BY57BYv7xAX4Hve7BZuSBZyNBZyGBZiABZR8BYx7BYZ7BX9/Bfui+14F9+b70AWVfQWPewWHewWCfQV8ggV6iAV6jgV8lAUO+Uj3DOUV+CuLBZyIBZmCBZV9BY97BYd7BYF9BX2CBXqIBfxYiwV6jgV9lAWBmQWHmwWL+REFj5sFlZkFmZQFnI8FnYcFmYIFlX0FjnsFDvlI+KH5WxWOjwWNjQWNjQWRjQWPjgWOjAWOjAWRjAWRjAWOigWOiwWQiQWRigWNigWOigWQhwWPiAWNiQWNiQWOhgWOhgWMiQWMiAWLhgWNhQWL/REFh3sFgX0FfYIFeogFeY4FfZQFgZkFiJsFi/h1BftD+6wFiYkFiokFhocFh4cFiYkFiYoFhYkFhYgFiYsFiIoFhYwFhYoFiIwFiYsFhY4FhY0FiYwFiY0Fh48Fho8Fio0FiY0F+0P3rAWL/HUFiHsFgX0FfYIFeYgFeo4FfZQFgZkFh5sFi/kRBY2RBYuQBYyOBYyNBY6QBY6QBY2NBY2NBY+OBZCPBY6MBY2MBZGMBZCNBY6LBY6MBZGKBZGKBY6KBY6KBY+IBZGJBY2JBY2JBY6HBY+HBfdw+/UF93D39QUO+Uj4e/dPFYT4hgWOmwWVmQWZlAWcjwWdiAWZggWVfQWPewWU/REFioYFi4cFiocFiocFiYgFiYcFiIgFiIcFiIkFiIgFh4kFh4kFh4oFh4oFhosFh4oFhosFh4sFh40Fh4wFh40Fh4wFiI4Fh44FiI4FiI4F/BT4oQWL/IkFh3sFgn0FfIIFeogFeo4FfJQFgZkFiJsFi/kRBYyPBYuPBYyPBYyPBY6PBYyOBY6OBY6PBY6NBY6OBZCNBY+NBY+MBY6NBZCLBZCMBY+KBY+LBY+JBZCKBY+JBY6KBY+IBY+IBY2IBY6IBQ75SPgblBWFiAWJiwWJiwUzgAWKiwWKigWEjAWDjAWKiwWKiwUypQWJjAWIjAWGjgWGjgWJjQWJjAU62gWJjgWIjgWJjwWIjwWLjwWJjgVP96AFi40Fi4wFi5IFi5IFjIwFi40FxPddBY2PBYyPBY6OBY2PBY6NBY6PBdHEBYyMBYyMBZKOBZGOBY2LBYyMBfcOqQWPiwWPjAWQigWPjAWPiQWPiwX3B2cFjIoFjYsFkYcFkYgFjIoFjYkFzkoFjYgFjYkFjocFjYYFjIgFjIgFxfuJBYuDBYuDBW/7LgWIhAWJhQVYMgWKiQWLigWGhgWGhgWKigWKigUwTgWJigWKigUg0BXOkwXUvAW11gWl9x8FVvd1BVm8BTCoBShzBVdgBVj7RgXD+4sFy00FDvlI93j3xxWJigX7AooFi/uVBYd7BYF9BX2CBXqIBXmOBX2UBYGZBYibBYv5BQWMkQWMkgWMjAWLjQWPkQWOkAWNjAWMjQWQjwWRjwWNiwWMjAWSjQWRjAX3HZcFjosFj4sF9w6EBY+KBZCLBdp2BY+KBY+KBa96BZCIBZCIBcRbBYuKBYuLBZCFBZCEBYuLBYyLBaRXBY2EBY2EBZA/BYuJBYuJBYqFBYqFBYqJBYuJBXRcBYqJBYqJBYeHBYeFBYmKBYqKBWhxBYWIBYWIBVN2BYeKBYeJBTp8BYiLBYmLBfsCggWF4BX1kwXWmQW5nAWgmwWZpwWGxAV6sAVesQVxlwVEnQX7BpIFL4MFi/uJBQ75SPiTtRXQOQWTfAWNewWFewWAfwV7gwV6igV6kAV+lQVRzwVUYAWIigWJiQWGiQWGiAWHiwWIigU1fgWJjAWJigWEjAWFiwWJjAWIiwU0qQWJjAWIjAWGjgWGjgWJjgWJjAU75AWKjgWJjQWJkAWIkAWLjgWKjQVQ98MFi4sFi4sFi5MFi5MFi4sFi4wFw/d2BY2OBYyPBY2PBY6PBY2OBY2OBdDLBY2MBYyNBZGOBZGOBY2MBY2MBfcLrAWRjAWRjAWNigWOjAWRiQWRigX3BGMFjYoFjooFj4cFkYgFjIkFjYoFzUEFjIkFjYkFjYYFjoYFjIgFjIkFw/uoBYuEBYuEBW/7QgWJhQWJhQX7K0AVMfUFg5oFipsFkJsFlpcFm5MFnIwFnIYFmIEF0zcFmqkFpfc1BVb3lgVZxAU0qgUucQVXWgVY+2IFwvuvBctDBcx2BcmUBQ75SPfz+EoV3JcFrpgFoJsFlqIFjLMFfbkFgZYFd5gFcZUFQZsF+xSQBfsofgWL+10F90g8FftIhQWL+7cFh3oFgn4FfIEFeogFeo4FfJUFgZgFiJwFi/j8BYyRBYyRBYyNBYuNBY+QBY6RBY2MBYyNBZGOBZCPBY2MBY2MBZGMBZKNBfdUmwWOiwWOjAX3G4UFj4oFj4sF3HoFj4kFj4sFrX0Fj4kFj4kFp3kFj4cFkIgFoHQFi4oFjIsFjoQFj4UFi4oFjIoFnU0FjIUFjIUFiVQFi4sFiYMFiYMFi4sFeWQFiooFi4oFhoUFhoUFiosFi4oFaXAFhIgFhYcFXXoFhooFhokFNX8F94H7rQWTfQWNegWGewV/fwV8gwV5igV6kAV+lgX7nPfLBYOaBQ75SOXBFYaOBYqNBYiNBYmQBYeQBVr3AQWHnAWOmwWVmQWZlAWcjgWdiAWZggWVfgW3KgXNaQX3IIcF7bgFq/AFdsUFS68F+4ezBYiNBYiLBYaNBYaNBYmOBYiMBUu+BYiPBYiOBYmOBYiOBYqQBYmOBXrHBYuPBYqOBYuQBYuPBYyPBYyPBbT3DAWOkAWNkAWNjQWMjQWQjwWPjwXvzAWRjgWQjgWOiwWNjAWRiwWRjAX3R4IFjYoFjIwFkogFkooFjIoFjYoFkIcFkYcFjIoFjIoF1zAFknwFjXoFhXwFgH4Fe4QFeYoFe5AFfpYFTNYF+yOTBUBaBWssBZVoBbZoBfeDYwWTiQWTiAXfWgWQhwWQhwWNiQWMigWOhQWOhgWrNgWLhgWNhwWLhwWLiAWKhgWKhgVi+xgFi4oFiooFh4UFiIQFiosFiooFhYYFhYcFiosFiooF+xJRBYmLBYqKBYSKBYSJBYmLBYmLBfs2jwWJjAWJiwWFjQWEjAWJjAWJjAUzuQUO+Uj3yflsFfeiiwWchwWaggWVfQWOewWIewWBfQV8ggV6iAX7dYsFi/znBYh7BYF9BX2CBXmIBXqOBXyUBYKZBYebBYv45wX7hYsFeo4FfJQFgZkFiJsFjpsFlZkFmpQFnI8FDvlI+HT3hxWQ+E4Fj5sFlZkFmZQFnY4FnIcFmYIFlX0FjnsFhvxQBYqIBYyJBYFHBYqHBYqGBXdYBYmIBYmHBX53BYiHBYiIBW1sBYWHBYaHBVhxBYSJBYSJBU2BBYWLBYSLBTOUBYaNBYWMBUWlBYWPBYWOBVyvBYeQBYWQBWjCBYiRBYiRBXnSBYqPBYqOBYTsBYuNBYuMBYr39wWOmwWVmQWalAWcjwWchwWaggWVfgWOegWM+/UFki8Fm00Fp2EFr28FxXYF14IFu5MFsJ4FoqIFk5cFnLYFDvlI99H3NRX3a/ivBZSZBZqVBZyPBZyIBZqCBZV+BY97BYh6BfuS/REFi4sFi4oFhoUFh4QFi4sFiooFhIcFhYYFiosFi4sFg4kFg4oFiosFi4sFg4wFgowFi4wFiosFhI8FhI8Fi4sFi4wFhpEFhpEFi4wFi4sF+7j5EgWHmwWOmwWVmQWZlAWcjwWciAWaggWVfQUO+Uj4dvdYFdf4gwWQmwWXlwWbkgWcjAWchgWYgAWTfAWMegUq/REFiYcFioYFiYgFiocFiIgFiIcFiIkFiIgFh4kFh4kFh4oFh4kFh4sFhooFh4sFh4sFho0Fh4sFh40Fh40FiI4Fh40FiI4FiI4FiY8FiI4F+0b3/wX7Ofv2BYiHBYiGBYmJBYmJBYaIBYeHBYiKBYiKBYaKBYWJBYiLBYiLBYWMBYaLBYiNBYiLBYaOBYaOBYmNBYiNBYiPBYiPBYqOBYmOBYqQBYmQBTv5CQWNnAWTmQWYlgWckAWcigWbhAWWfgWRewXH/G8F9x/3vgWPkAWPkQWMjAWMjAWRjwWRjwWNiwWMjAWSjAWSjQWNiwWMiwWSiQWTigWMiwWMigWRhwWSiAWMigWMigWPhQWPhQUO+Uj3/Pf/FfeZ+7QFlH0FjXsFhnsFgH4FfIMFeYkFeo8FfpYF+5D3qQX7kPupBX6ABXqHBXmNBXyTBYCYBYabBY2bBZSZBfeZ97QF+5/3uwWCmQWJnAWQmwWWlwWalAWdjAWchwWYgQX3lvuxBfeW97EFmZUFm48FnYoFmoIFln8FkHsFiXoFgn0FDvlI9+j4EhWV++EFiHsFgn0FfYEFeogFeY0FfJQFgZkFh5sFgffjBfud96cFgpkFiJsFj5sFlpkFmpMFnI4FnIcFmYEF94n7kQX3k/eQBZqVBZyPBZyIBZqCBZV+BY96BYh7BYJ9BQ75SPci5RX4KosFnYgFmYIFlX0FjnsFiHsFgX0FfYIFeYgF/IKLBYWMBYaLBYiMBYeMBYeOBYaNBYmNBYiNBYiPBYePBYqOBYmOBYqQBYmPBYuPBYuOBYyQBYuQBYyOBYyOBY6PBY2QBfhP+M4F/CqLBXmOBX2UBYGZBYibBY6bBZWZBZmUBZ2PBfiCiwWRigWQigWOigWPigWPiQWQiAWNiQWOiQWOhwWPhwWMiAWNiAWMhwWNhgWLiAWLhwWKhgWLhgWKiAWKiAWIhwWJhgUO+Uj3h3QV9zaLBZyIBZmCBZV9BY97BYd7BYF9BX2CBXqHBftjiwV5jwV9lAWBmQWImwWL+Z8FjpsFlZkFmZQFnY4F92OLBZyIBZmCBZV9BY97BYd7BYF9BX2CBXqIBfs2iwUO+Ujw+VgV+HH9YwWRfAWKegWEfAV+gAV7hQV5jAV7kQV/lwX8cfljBYWaBYycBZKaBZiWBZuRBZ2KBZuFBQ75SPhWShWHewWBfQV9ggV6hwX7Y4sFeY8FfZQFgZkFiJsFjpsFlZkFmZQFnY4F9zaLBYv5SwX7NosFeY4FfZQFgZkFiJsFjpsFlZkFmZQFnY4F92OLBZyIBZmCBZV9BY97BQ75SPeo+WQVko8FjIsFjIwFkowFk40FjIoFjIwFk4kFkooFjIoFjIsFkocFkYcF9577ggWVfgWPewWIegWBfQV9ggV6hwV6jgV8lAX7f/dmBft/+2YFfIIFeogFeo8FfZQFgZkFiJwFj5sFlZgF9573ggUO+UiL+wEV+O6LBZyIBZqCBZV9BY57BYh7BYF9BXyCBXqIBfzuiwV6jgV8lAWBmQWImwWOmwWVmQWalAUO+Uj4POoVxcIFi/eQBVLBBVCnBS+LBVBvBUxQBWsvBYtQBaswBcpPBcdvBeaLBfcIZhWHiAWFhwVAZwWKigWJigWEigWFiQWJjAWJigX7BYsFiYwFiYoFhI0FhYwFiYwFiYwFQK8FhY8FhY8FP9MFiowFiowFiJEFh5EFiowFiowFZvYFipIFipIFi9IFjJIFjJEFsPcABYyNBYyMBY+RBY6RBYyMBYyMBdfTBZGPBZGPBdeuBYyMBY2MBZKMBZKNBYyKBY2MBfcFiwWMigWNjAWSiQWSigWMigWNigXWaAWQhwWQiAWLjwWPnAWUmAWalQWcjgWciAWagQWVfgWOegWL/IoFiHsFgX4FfIEFeogFeo4FfJUFgpgFh5sFDvlI9xOvFYaOBYuKBYd6BYF+BX2CBXqHBXmPBX2UBYGYBYicBYv5OAWOmwWVmQWZlAWdjgWciAWZggWVfQWPewWL+3YFkI4FkY8F1asFjIsFjIsFko0Fk40FjIoFjIwF9wSLBYyLBYuLBZOJBZOJBYyLBYuLBddrBZCHBZKIBdZKBYyKBY2KBY6FBY+GBYyJBYyKBbApBYyEBYyEBYtLBYqEBYqDBWYrBYqKBYqJBYeGBYiFBYmKBYqJBUBLBYWIBYWHBUBqBYqLBYqLBYSJBYOJBYqLBYqLBfsEiwWKjAWKigWDjQWEjQWKiwWKiwVBrAWA9w0VwlwFxnEF54sFx6UFycAFqdoFi70FbdsFTcAFT6QFL4sFUHIFVFwFDvlI+DLqFczNBZqVBZyPBZyIBZqDBZV9BY97BYl7BYF9BUVDBYWHBYWHBUZmBYiLBYmJBYWKBYWJBYiMBYmKBSGLBYmMBYiKBYWNBYWMBYmNBYmLBUSwBYWPBYWPBUXTBYqMBYqMBYeRBYeRBYuMBYqMBWn2BYqRBYqSBYvSBYyRBYySBa33AAWMjAWLjAWPkQWPkQWMjAWMjAXR0wWRjwWRjwXSrwWNiwWNjQWRjAWSjQWNigWNjAX1iwWNigWNjAWSiQWRigWNiQWNiwXRZwWRhwWRhwXRQwWVfQWNewWHewWBfQV8gwV6iAV6jwV8lQVKzQVWpgU3iwVVcAVRUAVtLgWLUAWoLwXGUAXCbwXdiwUO+Uj4PfUVwroFi/dxBVS6BVCkBS+LBU9yBU1WBW07BYtZBak8BclWBcdxBeeLBfcGYhWGiAWFhwVBagWKiwWKiwWEiQWDiQWKjAWKigX7BIsFiosFiosFhI0Fg40FiosFiosFQKwFhY8Fho4FP8sFio0FiYwFiJEFh5AFio0FiowFZusFipMFipIFi8sFjJIFjJIFsO0FjIwFjI0Fj5AFjpEFjYwFjIwF18wFkY4FkI8F16sFi4sFjIsFk40Fk40Fi4sFjIsF9wSLBYyKBYyMBZOJBZKJBYyLBYyLBdVrBZGHBZCIBYv3dgWPmwWVmQWZlAWcjgWdiAWZggWVfQWOewWL/TgFiHoFgX4FfYIFeYcFeo8FfZQFgZgFh5wFDvlI9x74IBV8YQX4GIsFg5QFZs0FbaYFS6cFJosFSm8F93r7/BXXzgWZlAWdjgWchwWZggWVfQWNegWIewWAfgU6QwWGiAWFhwU8ZgWJiwWKigWEigWEiQWJiwWKiwX7DYsFiosFiosFg40FhIwFiowFiosFOrAFhY8FhY4FO9MFiYwFiowFh5EFh5AFi40Fio0FY/YFipIFipIFi9IFjJIFjJIFs/cABYyNBYuNBY+QBY+QBYyNBY2MBdvTBZKOBZCPBdyvBYyLBYyMBZOMBZKNBYyLBYyLBfcNiwWMiwWMiwWTiQWSigWMigWMiwXbZwWQhwWRiAW1ZwWPhQWQhQWyRAWQewWJegWDfQV/ggWUhQWVfgWOegWIewWBfQV9ggV5iAX8SYsFi3UFrDAFz08FzG4F8IsFDvlI97n4VhWL/CQFiHsFgX0FfIIFeogFeo4FfJQFgZkFiJsFi/gkBfsGiwV6jgV8lQWCmAWHmwWPnAWUmAWalQWcjgX3BosFi8AFi40Fi40FjZEFjJEFjY0Fi4wFwOoFkZEFkJEFi4sFi4wFk44Fko8F9asFkowFkowF9YsFnIgFmoIFlX0FjnsFiHsFgX0FfIIFeogFKIsFOXIFZEUFi2EF9zuLBZyIBZqBBZV+BY56BYh7BYF+BXyBBXqIBQ75SPg99yMVwroFi/dxBVS7BVCkBS+LBU9yBU1WBW06BYtaBak7BclWBcdxBeeLBfcGYhWGiAWFhwVBagWKiwWKigWEigWDiQWKiwWKiwX7BIsFiosFiosFg40FhIwFiowFiosFQKwFhY8FhY4FQMwFiowFiYwFiJEFh5AFio0FiowFZuwFipIFipIFi8wFjJIFjJIFsOwFjI0FjI0Fj5AFjpAFjY0FjIwF1swFko4FkI8F16sFi4sFjIwFk4wFk40Fi4sFjIsF9wSLBYyLBYyLBZOJBZKKBYyKBYyLBdVrBZGHBZCIBYuLBY+cBZWYBZmVBZyOBZ2IBZmBBZV+BY56BYv8mAWKhAWKhAVmKgWKigWKiQWHhgWIhQWJigWKiQVlawWFhwWFhwVBbAWKigWKiwWEiQWDigWKiwWKiwX7BIsFiosFi4sFg4wFg40FiosFi4sFP6sFho8FhI8FQMsFio0FiYwFiJAFh5AFio0Fio0FZuwFiZwFj5sFlZgFm5QFnI0FnIcFmYEFlH0FrDQFyVYFx3EF54sFxqUFpKAFqdoFDvlI+L34CRWNhAWL+9AFiHsFgX0FfIIFeogFeo4FfJQFgZkFiJsFi/fIBW7TBVqfBSyLBU1yBS49BYv7vQWIewWBfQV8ggV6iAV6jgV8lAWBmQWImwWL+SwFjpsFlZkFmpQFnI4FnIgFmoIFlX0FjnsFi/uSBbKsBZGOBZGPBdeqBYyLBYuMBZOMBZONBYyLBYuLBfcGiwWMiwWLiwWTiQWTigWMigWLiwXXbAWLiwWLiwWShgWShwWLigWMiwWPhAWQhQWLigWLiwWxLAUO+Uj3mfmIFduLBZyIBZqCBZV9BY57BYh7BYF9BXyCBXqIBTuLBXqOBXyUBYGZBYibBY6bBZWZBZqUBfH7mRWL/E4FiHsFgX0FfIIFeogFeo4FfJQFgZkFiJsFi/hOBY6cBZWYBZqVBZyOBZyIBZqBBZV+BQ75SPgV+YgVyYsFnIgFmoIFlX0FjnsFiHsFgX0FfIIFeogFTYsFeo4FfJQFgpkFh5sFj5sFlJkFmpQF+wn+MhWGigWIjQWHiwUpqQV8kwWAmAWGmwWNmwWUmQWZlgWbjwWdiQXhcQXpoQWhyQWL+I4Fj5sFlJkFmpQFnI8FnIcFmoIFlX0FjnsFi/yVBYqEBYqFBWswBYiGBYmHBYmIBYmIBYeIBYeIBYiJBYiJBYaKBYaJBfsXbQWIiwWHigUO+Uj3f/eMFfsRLwWLIQWIewWBfQV8ggV6iAV6jgV8lAWCmQWHmwWL+SwFj5sFlJkFmpQFnI4FnIgFmoIFlX0FjnsFi/xXBff495sFm5IFnIwFnIYFmIEFk3wFjHoFhnwFgH4F+2v7MgX3mPtwBZV+BZB7BYh7BYJ9BX2BBXqHBXqNBXyTBQ75SPfu+WMVi/0xBYh7BYF9BXyCBXqIBXqOBXyUBYGZBYibBYv5MQWOmwWVmQWalAWcjwWchwWaggWVfQUO+Uj43/iYFZCGBYuKBYyJBYyFBY2EBYuKBYyJBYv8WwWHewWBfQV9ggV6iAV5jgV9lAWBmQWImwWL+EgFfJYFXIsFdHgFVDEFi/vmBYh7BYF9BX2CBXmIBXqOBX2UBYGZBYebBYv36gVo4wV1nAVdiwVzeAVVMQWL++YFh3sFgX0FfYIFeogFeY4FfZQFgZkFiJsFi/h9BY6cBZWYBZmVBZ2OBZyIBZmBBZV+BY2ABa2mBZGOBZGPBY2LBYyMBZKLBZKNBduLBZKJBZKLBYyKBY2LBZGHBZGIBbZoBYyJBY2KBY+GBY+GBYuJBY2JBZN1BZynBZCQBZCRBbWtBZGPBZGPBY2LBYyMBZKLBZKNBdyLBZKJBZKLBYyKBY2LBZGHBZGIBbZoBYyKBYyKBQ75SPi++B0VjIUFi/vyBYh7BYF9BXyCBXqIBXqOBXyUBYGZBYibBYv36wVt3QVaoQUtiwVPcAUsMwWL++AFiHsFgX0FfIIFeogFeo4FfJQFgZkFiJsFi/h9BY6cBZWYBZqVBZyOBZyIBZqBBZV+BY56BYtlBbCtBZKPBZCPBdiuBYyLBYyMBZKMBZONBYyLBYyLBfcGiwWMiwWNiwWSiQWSigWMigWNiwXWaAWMiwWLiwWShgWRhgWMiwWLiwWPhAWPhAWLiwWLiwWyIQUO+Uj37Ph4FTWLBVRwBU9PBWwvBYtQBaowBcdPBcNvBeCLBcKnBcfHBarmBYvGBWznBU/HBWH3AxWNjAWRiQWSigWNiQWNiwXTaAWRhgWRhwXTQwWLigWMiwWPhAWPhQWLigWMigWv+wAFjIUFjIQFi0QFioQFioUFZyAFiooFi4oFh4QFh4UFiosFi4oFQ0MFhYcFhYcFQ2YFiYsFiYkFhYoFhIkFiYwFiYoF+wCLBYmMBYmKBYSNBYWMBYmNBYmLBUOwBYWPBYWPBUPTBYuMBYqLBYeRBYeSBYuMBYqMBWf2BYqRBYqSBYvSBYySBYyRBa/3AAWMjAWLjAWPkQWPkgWMiwWLjAXT0wWSjwWQkAXTrgWNiwWNjQWSjAWRjQWNigWNjAX3AIsFDvlI9xTXFYWPBYv7dQWHewWCfQV8ggV6iAV6jgV8lAWBmQWImwWL+TMFjpwFlZgFmpUFnI4FnIgFmoEFlH4Fj3oFi4sFkY8FkI4F1asFjIsFjIwFk4wFko0FjIsFjIsF9wSLBYyLBYuLBZOJBZOKBYyKBYuLBdZsBZGHBZGHBdZLBY2KBYyJBY+GBY+GBYuJBY2JBa8rBY2EBYyDBYtLBYqEBYmEBWYsBYqJBYuJBYeGBYeFBYqKBYmKBUBLBYWHBYaIBUBqBYqLBYqKBYOKBYSJBYqLBYqLBfsEiwWKiwWKiwWDjQWEjAWKjAWKiwVBrAWA9w0VwlwFxnEF54sFx6UFyb8FqdoFi7wFbdsFTb8FT6QFL4sFUHIFVFwFDvlI+D33JRXCugWL924FVLoFUKQFL4sFT3IFTVcFbTsFi1oFqTwFyVcFx3EF54sF9wZiFYaIBYWHBUFqBYqLBYqLBYSJBYOJBYqMBYqKBfsEiwWKjAWKigWEjQWDjQWKiwWKiwVArAWGjgWFjwVAywWJjAWKjQWHkAWHkAWLjQWKjQVm6gWJkgWKkwWLywWMkgWNkgWw6wWMjQWLjQWPkAWPkQWMjAWNjAXWywWRjwWRjwXWqgWLiwWMjAWTjAWTjQWLiwWMiwX3BIsFjIsFjIsFkokFk4oFjIoFjIsF1WwFkIcFkYgFi4sFj5sFlJkFmpQFnI4FnIgFmoIFlX0FjnsFi/00BYh7BYF9BXyCBXqIBXqOBXyUBYKZBYebBQ75SPfp+MsVk40F9yaLBZyIBZmBBZV+BY96BYd7BYF+BX2BBXqIBfsdiwU4bgU5TwViNAWL+6MFiHsFgX0FfYIFeYgFeo4FfZQFgZkFh5sFi/h9BY+cBZWYBZmVBZyOBZ2IBZmBBZV+BY56BYtWBdTABZGNBZGPBeutBQ75SO6nFYSQBYuLBYuMBYeSBYeSBXLSBYmcBY+bBZaYBZuTBZyNBZyGBZmBBZN9BZ9TBbt0BfWLBeSsBbuhBYuoBVHCBfsBygX7BIsFiYsFiosFhI0FhIwFiowFiYwFQK8FhY8Fho8FiowFiYwFiJEFh5EFi4wFio0FipEFipIFi9IFjJMFjZMFi4sFi4wFkJEFj5IF1tMFjIsFjIwFkY8Fko8FjIsFi4wF764Fk4wFk40F9xGLBYyKBY2MBZKJBZKKBYyKBY2KBdZoBZmBBZN8BY17BYd7BYB+BXyDBXmJBXqPBUqqBSGLBTluBVRWBYtuBbN4BfcGiwWOigWPiwWQiQWQigWOigWPiQX3EEQFj4cFkIgF1kQFj4QFkIQFi4sFi4sFjYMFjIMFi0MFioUFioQFiooFi4kFh4UFiIYFiYoFiokFhYgFhoYFQGgFiYsFiYkFJ2cFg4oFg4kF+xGLBYmMBYmKBYWNBYSMBYmMBYqMBUCvBQ75SPdd+DEV+wyLBXqOBXyUBYGZBYibBY6bBZWZBZqUBZyOBfcMiwWL9zcFjpsFlZkFmpQFnI4FnIgFmoIFlX0FjnsFi/s3BfdDiwWciAWaggWVfQWOewWIewWBfQV8ggV6iAX7Q4sFi/uCBbNMBeF0BfOLBZyHBZqCBZV9BY57BYh7BYF9BXyCBXqIBfsCiwWFjAWFjAX7AqgFg48FhI4Fi4sFiowFhpEFhZAFVOIFio4FiY4FipAFiZAFjI4Fio4FDvlI+GX3SRWL9+AFjpsFlZkFmpQFnI4FnIgFmoIFlX0FjnsFi/x9BYh6BYF+BXyBBXqIBXqOBXyVBYGYBYicBYuyBWZpBYWHBYWHBT9oBYmKBYqKBYSKBYSJBYmMBYqKBfsGiwWJjAWKigWEjQWEjAWJjAWKjAU/rgWEkAWEkAWLiwWLiwWHkgWHkgVk9AWKkgWKkQWL9/IFjpsFlZkFmpQFnI4FnIgFmoIFlX0FjnsFi/vrBak6Bb10BeeLBcinBQ75SPfB9xcV92j4JgWWmAWakwWcjQWchwWZgQWUfQWNegWHewX7kfxyBYiHBYiGBYiJBYqJBYaIBYaIBYiKBYiKBYaKBYWKBYiLBYiLBYWMBYaMBYiMBYiMBYaOBYaOBYqNBYiNBYiQBYiPBfuQ+HIFhpsFjZwFlJkFmZUFnI8FnIkFmoMFln4FDvlI+Fj3TRX29+kFk5oFmZUFm5AFnYoFmoMFl34FkHsFiXsF+yr8cgWIhwWKhwWIiAWKhwWHiQWIhwWHiQWIiQWGiQWHiQWHiwWHigWHiwWGiwWHjAWHiwWHjQWGjQWIjQWHjQWIjwWIjQWJjwWIjgWKjwWIjwUg9+kFIPvpBYiHBYqHBYiIBYmHBYiJBYiHBYeJBYiJBYaJBYeJBYeLBYeKBYeLBYaLBYeMBYeLBYeNBYaNBYiNBYeNBYiPBYiNBYmPBYiOBYqPBYiPBfsq+HIFiZsFkJsFl5gFmpMFnYwFm4YFmYEFk3wF9wD76QX29+kFjY8FjI8Fjo4FjY8Fj44Fjo4Fjo0Fj40Fj40Fj40Fj4sFj4wFkIsFkIsFj4oFj4sFj4kFkIkFjokFjokFjogFj4gFjYcFjogFjIcFjYcFDvlI+AL3pxX3evtlBZV+BY97BYh7BYF9BX2BBXqIBXmOBX2UBft992YF+337ZgV9ggV5iAV6jgV9lQWBmQWImwWPmwWVmAX3evdlBft692UFgZgFh5sFjpwFlZgFmZUFnI4FnYgFmYMF9337ZwX3ffdnBZmTBZ2OBZyIBZmBBZV+BY56BYd7BYF+BQ75SOj7TxWDigWLiwWLiwVjiwV6jgV8lAWBmQWImwWOmwWVmQWalAWcjgWqiwXLpQXOwQXJ7gX7dfg4BYebBY2bBZSaBZiVBZyPBZ2JBZqDBZZ+BfdX/AEF91n4AQWWmAWakwWdjQWbhwWZgQWUfAWNewWGewX7gfxMBYuKBYqJBT37EQWGhQWGhgU9TAWEiAWGhwU8bAWLiwWLiwUO+Uj3UtkV98+LBZ2IBZmCBZV9BY57BYh7BYF9BX2CBXmIBfwsiwWEjAWFiwWJjQWIiwWGjgWGjgWJjQWJjAWHkAWHkAWKjQWKjQWKkQWJkAWLjgWLjQWMkQWLkQWNjQWLjQWPkAWOkQX38vguBfvPiwV5jgV9lAWBmQWImwWOmwWVmQWZlAWdjgX4LIsFkooFkYsFjYoFjooFkIgFkYgFjIkFjYoFj4YFj4YFjIkFjIkFjIUFjYYFi4gFi4kFioUFi4UFiYkFi4kFh4YFiIYFDgAAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAAK0AAACtAAAArQAAA==') format('truetype'); font-weight: normal; font-style: normal; }</style></svg>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import type { CadComponent, CircuitJson } from "circuit-json"
|
|
3
|
+
import { convertCircuitJsonToTscircuit } from "lib"
|
|
4
|
+
import { runTscircuitCode } from "tscircuit"
|
|
5
|
+
import { expectJscadCadModel3dSnapshots } from "./fixtures/expect-jscad-cad-model-3d-snapshots"
|
|
6
|
+
|
|
7
|
+
test("preserves BeaglePlay J13 CAD placement when pad bounds differ from its footprint origin", async () => {
|
|
8
|
+
// J13, its native pads and its ComponentBody come from BEAGLEPLAYV020_220915.
|
|
9
|
+
const sourceCircuitJson: CircuitJson = await Bun.file(
|
|
10
|
+
new URL("./fixtures/beagleplay-j13-origin.circuit.json", import.meta.url),
|
|
11
|
+
).json()
|
|
12
|
+
const sourceCadComponent = sourceCircuitJson.find(
|
|
13
|
+
(element): element is CadComponent => element.type === "cad_component",
|
|
14
|
+
)!
|
|
15
|
+
const generatedTsx = convertCircuitJsonToTscircuit(sourceCircuitJson, {
|
|
16
|
+
componentName: "BeaglePlayUsb",
|
|
17
|
+
})
|
|
18
|
+
expect(generatedTsx).toMatchInlineSnapshot(`
|
|
19
|
+
"export const BeaglePlayUsb = () => (
|
|
20
|
+
<board pcbX={-308.7624} pcbY={238.2139} width="24mm" height="20mm" thickness="1.6mm" layers={8} material="fr4">
|
|
21
|
+
<chip name="J13" pcbX={0} pcbY={0} pcbRotation="90deg" layer="top" noSchematicRepresentation cadModel={{ jscad: {"type":"colorize","color":[0.5019607843137255,0.5019607843137255,0.5019607843137255],"shape":{"type":"extrudeLinear","options":{"height":3.81},"shape":{"type":"fromPointsGeom2","points":[[-2.7939999999999827,-6.921500000000009],[2.7940000000000396,-6.921500000000009],[2.7940000000000396,6.921500000000009],[-2.7939999999999827,6.921500000000009]]}}}, positionOffset: { x: 0, y: 0, z: 0 }, rotationOffset: { x: 0, y: 0, z: -90 }, modelOriginPosition: { x: 0, y: 0, z: 0 }, modelUnitToMmScale: 1, modelBoardNormalDirection: "z+" }} obstructsWithinBounds={false} footprint={<footprint>
|
|
22
|
+
<platedhole portHints={["6"]} pcbX="4.032504mm" pcbY="3.080004mm" outerDiameter="2.199894mm" holeDiameter="1.400048mm" shape="circle" />
|
|
23
|
+
<platedhole portHints={["4"]} pcbX="6.0325mm" pcbY="2.100072mm" outerDiameter="1.199896mm" holeDiameter="0.649986mm" shape="circle" />
|
|
24
|
+
<platedhole portHints={["3"]} pcbX="6.0325mm" pcbY="0.700024mm" outerDiameter="1.199896mm" holeDiameter="0.649986mm" shape="circle" />
|
|
25
|
+
<platedhole portHints={["7"]} pcbX="-1.567434mm" pcbY="0mm" outerHeight="1.500124mm" outerWidth="2.9718mm" holeHeight="1.500124mm" holeWidth="2.9718mm" height="1.500124mm" shape="pill" pcbRotation="270deg" />
|
|
26
|
+
<platedhole portHints={["2"]} pcbX="6.0325mm" pcbY="-0.700024mm" outerDiameter="1.199896mm" holeDiameter="0.649986mm" shape="circle" />
|
|
27
|
+
<platedhole portHints={["1"]} pcbX="6.0325mm" pcbY="-2.100072mm" outerDiameter="1.199896mm" holeDiameter="0.649986mm" shape="circle" />
|
|
28
|
+
<platedhole portHints={["5"]} pcbX="4.032504mm" pcbY="-3.080004mm" outerDiameter="2.199894mm" holeDiameter="1.400048mm" shape="circle" />
|
|
29
|
+
</footprint>} />
|
|
30
|
+
</board>
|
|
31
|
+
)
|
|
32
|
+
export default BeaglePlayUsb"
|
|
33
|
+
`)
|
|
34
|
+
const renderedCircuitJson = (await runTscircuitCode(
|
|
35
|
+
generatedTsx,
|
|
36
|
+
)) as CircuitJson
|
|
37
|
+
const renderedCadComponent = renderedCircuitJson.find(
|
|
38
|
+
(element): element is CadComponent => element.type === "cad_component",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
await expectJscadCadModel3dSnapshots({
|
|
42
|
+
sourceCircuitJson,
|
|
43
|
+
generatedCircuitJson: renderedCircuitJson,
|
|
44
|
+
testPath: import.meta.path,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
for (const axis of ["x", "y", "z"] as const) {
|
|
48
|
+
expect(renderedCadComponent?.position[axis]).toBeCloseTo(
|
|
49
|
+
sourceCadComponent.position[axis],
|
|
50
|
+
8,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
expect(renderedCadComponent?.rotation).toEqual(sourceCadComponent.rotation)
|
|
54
|
+
expect(renderedCadComponent?.model_jscad).toEqual(
|
|
55
|
+
sourceCadComponent.model_jscad,
|
|
56
|
+
)
|
|
57
|
+
})
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { convertCircuitJsonToPcbSvg } from "circuit-to-svg"
|
|
3
|
+
import type { CircuitJson, PcbPort, SourcePort } from "circuit-json"
|
|
4
|
+
import { convertCircuitJsonToTscircuit } from "lib"
|
|
5
|
+
import { runTscircuitCode } from "tscircuit"
|
|
6
|
+
|
|
7
|
+
// Reduced from the OrangeCrab r0.2.1 import: KiCad physical pin names arrive
|
|
8
|
+
// as strings in pin_number, even though Circuit JSON's TS type is numeric.
|
|
9
|
+
const makeBoard = (physicalPins: Array<number | string>): CircuitJson =>
|
|
10
|
+
[
|
|
11
|
+
{
|
|
12
|
+
type: "pcb_board",
|
|
13
|
+
pcb_board_id: "board",
|
|
14
|
+
center: { x: 0, y: 0 },
|
|
15
|
+
width: 10,
|
|
16
|
+
height: 10,
|
|
17
|
+
num_layers: 2,
|
|
18
|
+
thickness: 1.6,
|
|
19
|
+
material: "fr4",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: "source_component",
|
|
23
|
+
source_component_id: "sc",
|
|
24
|
+
name: "U1",
|
|
25
|
+
ftype: "simple_chip",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: "pcb_component",
|
|
29
|
+
pcb_component_id: "pc",
|
|
30
|
+
source_component_id: "sc",
|
|
31
|
+
center: { x: 0, y: 0 },
|
|
32
|
+
width: 4,
|
|
33
|
+
height: 1,
|
|
34
|
+
layer: "top",
|
|
35
|
+
rotation: 0,
|
|
36
|
+
},
|
|
37
|
+
...physicalPins.flatMap((pin, i) => [
|
|
38
|
+
{
|
|
39
|
+
type: "source_port",
|
|
40
|
+
source_port_id: `sp${i}`,
|
|
41
|
+
source_component_id: "sc",
|
|
42
|
+
name: String(pin),
|
|
43
|
+
pin_number: pin,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: "pcb_port",
|
|
47
|
+
pcb_port_id: `pp${i}`,
|
|
48
|
+
pcb_component_id: "pc",
|
|
49
|
+
source_port_id: `sp${i}`,
|
|
50
|
+
x: i - 1,
|
|
51
|
+
y: 0,
|
|
52
|
+
layers: ["top"],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: "pcb_smtpad",
|
|
56
|
+
pcb_smtpad_id: `pad${i}`,
|
|
57
|
+
pcb_component_id: "pc",
|
|
58
|
+
pcb_port_id: `pp${i}`,
|
|
59
|
+
port_hints: [String(pin)],
|
|
60
|
+
shape: "circle",
|
|
61
|
+
x: i - 1,
|
|
62
|
+
y: 0,
|
|
63
|
+
radius: 0.15,
|
|
64
|
+
layer: "top",
|
|
65
|
+
},
|
|
66
|
+
]),
|
|
67
|
+
] as CircuitJson
|
|
68
|
+
|
|
69
|
+
for (const physicalPin of ["A1", "H3", "MP"]) {
|
|
70
|
+
test(`imports physical pin ${physicalPin} using valid pin label keys`, () => {
|
|
71
|
+
const generated = convertCircuitJsonToTscircuit(
|
|
72
|
+
makeBoard([1, physicalPin, 3]),
|
|
73
|
+
{ componentName: "BgaBoard" },
|
|
74
|
+
)
|
|
75
|
+
expect(generated).not.toContain(`"pin${physicalPin}"`)
|
|
76
|
+
expect(generated).toContain(
|
|
77
|
+
`"pin4":["${physicalPin}","imported_pcb_port_2"]`,
|
|
78
|
+
)
|
|
79
|
+
expect(generated).toContain('"pin1":["1","imported_pcb_port_1"]')
|
|
80
|
+
expect(generated).toContain('"pin3":["3","imported_pcb_port_3"]')
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
test("renders imported BGA balls without dropping the component", async () => {
|
|
85
|
+
const generated = convertCircuitJsonToTscircuit(
|
|
86
|
+
makeBoard(["A1", "B2", "MP"]),
|
|
87
|
+
{ componentName: "BgaBoard" },
|
|
88
|
+
)
|
|
89
|
+
const rendered = (await runTscircuitCode(generated)) as CircuitJson
|
|
90
|
+
expect(
|
|
91
|
+
rendered.filter(
|
|
92
|
+
(element) => element.type === "source_failed_to_create_component_error",
|
|
93
|
+
),
|
|
94
|
+
).toHaveLength(0)
|
|
95
|
+
expect(
|
|
96
|
+
rendered.filter((element) => element.type === "pcb_component"),
|
|
97
|
+
).toHaveLength(1)
|
|
98
|
+
const pads = rendered.filter((element) => element.type === "pcb_smtpad")
|
|
99
|
+
expect(pads).toHaveLength(3)
|
|
100
|
+
for (const [i, pin] of ["A1", "B2", "MP"].entries()) {
|
|
101
|
+
const port = rendered.find(
|
|
102
|
+
(element): element is SourcePort =>
|
|
103
|
+
element.type === "source_port" &&
|
|
104
|
+
element.port_hints?.includes(pin) === true,
|
|
105
|
+
)
|
|
106
|
+
expect(port?.type).toBe("source_port")
|
|
107
|
+
const pcbPort = rendered.find(
|
|
108
|
+
(element): element is PcbPort =>
|
|
109
|
+
element.type === "pcb_port" &&
|
|
110
|
+
element.source_port_id === port?.source_port_id,
|
|
111
|
+
)
|
|
112
|
+
expect(pcbPort).toMatchObject({ x: i - 1, y: 0 })
|
|
113
|
+
expect(pads[i]?.pcb_port_id).toBe(pcbPort?.pcb_port_id)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test("keeps numeric string pins compatible", async () => {
|
|
118
|
+
const generated = convertCircuitJsonToTscircuit(makeBoard(["1", "3"]), {
|
|
119
|
+
componentName: "NumericBoard",
|
|
120
|
+
})
|
|
121
|
+
expect(generated).toContain('"pin1":["1","imported_pcb_port_1"]')
|
|
122
|
+
expect(generated).toContain('"pin3":["3","imported_pcb_port_2"]')
|
|
123
|
+
const rendered = (await runTscircuitCode(generated)) as CircuitJson
|
|
124
|
+
expect(
|
|
125
|
+
rendered.filter((element) => element.type === "pcb_component"),
|
|
126
|
+
).toHaveLength(1)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test("renders an SVG of imported BGA component creation", async () => {
|
|
130
|
+
const generated = convertCircuitJsonToTscircuit(
|
|
131
|
+
makeBoard(["A1", "B2", "MP"]),
|
|
132
|
+
{ componentName: "BgaSvgBoard" },
|
|
133
|
+
)
|
|
134
|
+
const rendered = (await runTscircuitCode(generated)) as CircuitJson
|
|
135
|
+
const pcb = convertCircuitJsonToPcbSvg(rendered, {
|
|
136
|
+
width: 600,
|
|
137
|
+
height: 260,
|
|
138
|
+
includeVersion: false,
|
|
139
|
+
viewport: { minX: -2.5, maxX: 2.5, minY: -1, maxY: 1 },
|
|
140
|
+
})
|
|
141
|
+
await expect(pcb).toMatchSvgSnapshot(
|
|
142
|
+
import.meta.path,
|
|
143
|
+
"orangecrab-bga-pin-labels",
|
|
144
|
+
)
|
|
145
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
|
+
import { gunzipSync } from "node:zlib"
|
|
3
|
+
import type { CadComponent, CircuitJson } from "circuit-json"
|
|
4
|
+
import { convertCircuitJsonToTscircuit } from "lib"
|
|
5
|
+
import { runTscircuitCode } from "tscircuit"
|
|
6
|
+
import { expectTiEvm3dSnapshot } from "./fixtures/expect-ti-evm-3d-snapshot"
|
|
7
|
+
|
|
8
|
+
test("preserves the DP83825EVM Molex USB connector on its mounting pads", async () => {
|
|
9
|
+
const sourceCircuitJson: CircuitJson = JSON.parse(
|
|
10
|
+
gunzipSync(
|
|
11
|
+
await Bun.file(
|
|
12
|
+
new URL(
|
|
13
|
+
"./fixtures/ti-evms/dp83825-usb-cad-origin.circuit.json.gz",
|
|
14
|
+
import.meta.url,
|
|
15
|
+
),
|
|
16
|
+
).arrayBuffer(),
|
|
17
|
+
).toString("utf8"),
|
|
18
|
+
)
|
|
19
|
+
const sourceCadComponent = sourceCircuitJson.find(
|
|
20
|
+
(element): element is CadComponent => element.type === "cad_component",
|
|
21
|
+
)!
|
|
22
|
+
const generatedTsx = convertCircuitJsonToTscircuit(sourceCircuitJson, {
|
|
23
|
+
componentName: "Dp83825Usb",
|
|
24
|
+
})
|
|
25
|
+
expect(generatedTsx).toMatchInlineSnapshot(`
|
|
26
|
+
"export const Dp83825Usb = () => (
|
|
27
|
+
<board pcbX={20.605798895} pcbY={73.2536} width="18mm" height="16mm" thickness="1.6mm" layers={4} material="fr4">
|
|
28
|
+
<chip name="J1" pcbX={-2.7749988949999995} pcbY={-1.4210854715202004e-14} pcbRotation="270deg" layer="top" noSchematicRepresentation cadModel={<cadmodel modelUrl={"/cad-models/dp83825evm/8.step"} positionOffset={{ x: 12.800022659999996, y: -0.00008128000000340307, z: 2.25000058 }} rotationOffset={{ x: 270, y: 90, z: -270 }} modelUnitToMmScale={1} modelBoardNormalDirection={"z+"} modelOriginPosition={{ x: 0, y: 0, z: 0 }} showAsTranslucentModel={false} />} obstructsWithinBounds={false} footprint={<footprint>
|
|
29
|
+
<platedhole portHints={["7"]} pcbX="4.17500054mm" pcbY="1.44999964mm" holeShape="pill" padShape="rect" holeWidth="1.29999994mm" holeHeight="0.5999987999999999mm" rectPadWidth="0.8000009399999999mm" rectPadHeight="2.00000108mm" rectBorderRadius="0.40000046999999994mm" holeOffsetX="0mm" holeOffsetY="0mm" pcbRotation="270deg" shape="pill_hole_with_rect_pad" />
|
|
30
|
+
<platedhole portHints={["10"]} pcbX="-4.17500054mm" pcbY="1.44999964mm" holeShape="pill" padShape="rect" holeWidth="1.29999994mm" holeHeight="0.5999987999999999mm" rectPadWidth="0.8000009399999999mm" rectPadHeight="2.00000108mm" rectBorderRadius="0.40000046999999994mm" holeOffsetX="0mm" holeOffsetY="0mm" pcbRotation="270deg" shape="pill_hole_with_rect_pad" />
|
|
31
|
+
<platedhole portHints={["6"]} pcbX="2.22499936mm" pcbY="4.44999872mm" holeShape="pill" padShape="rect" holeWidth="0.8500008399999999mm" holeHeight="0.70000114mm" rectPadWidth="1.05000044mm" rectPadHeight="1.29999994mm" rectBorderRadius="0.52500022mm" holeOffsetX="0mm" holeOffsetY="0mm" pcbRotation="270deg" shape="pill_hole_with_rect_pad" />
|
|
32
|
+
<platedhole portHints={["11"]} pcbX="-2.22499936mm" pcbY="4.44999872mm" holeShape="pill" padShape="rect" holeWidth="0.8500008399999999mm" holeHeight="0.70000114mm" rectPadWidth="1.05000044mm" rectPadHeight="1.29999994mm" rectBorderRadius="0.52500022mm" holeOffsetX="0mm" holeOffsetY="0mm" pcbRotation="270deg" shape="pill_hole_with_rect_pad" />
|
|
33
|
+
<smtpad portHints={["3"]} pcbX="0mm" pcbY="4.12500064mm" layer="top" solderMaskMargin="0.04999989999999999mm" cornerRadius="0.04999989999999999mm" width="0.39999919999999994mm" height="1.34999984mm" ccwRotation={0} shape="rotated_rect" />
|
|
34
|
+
<smtpad portHints={["2"]} pcbX="-0.65000124mm" pcbY="4.12500064mm" layer="top" solderMaskMargin="0.04999989999999999mm" cornerRadius="0.04999989999999999mm" width="0.39999919999999994mm" height="1.34999984mm" ccwRotation={0} shape="rotated_rect" />
|
|
35
|
+
<smtpad portHints={["8"]} pcbX="1.15000024mm" pcbY="1.44999964mm" layer="top" solderMaskMargin="0.04999989999999999mm" cornerRadius="0.05399996819999999mm" width="1.79999894mm" height="2.00000108mm" ccwRotation={180} shape="rotated_rect" />
|
|
36
|
+
<smtpad portHints={["9"]} pcbX="-1.15000024mm" pcbY="1.44999964mm" layer="top" solderMaskMargin="0.04999989999999999mm" cornerRadius="0.05399996819999999mm" width="1.79999894mm" height="2.00000108mm" ccwRotation={180} shape="rotated_rect" />
|
|
37
|
+
<smtpad portHints={["5"]} pcbX="1.29999994mm" pcbY="4.12500064mm" layer="top" solderMaskMargin="0.04999989999999999mm" cornerRadius="0.04999989999999999mm" width="0.39999919999999994mm" height="1.34999984mm" ccwRotation={0} shape="rotated_rect" />
|
|
38
|
+
<smtpad portHints={["4"]} pcbX="0.65000124mm" pcbY="4.12500064mm" layer="top" solderMaskMargin="0.04999989999999999mm" cornerRadius="0.04999989999999999mm" width="0.39999919999999994mm" height="1.34999984mm" ccwRotation={0} shape="rotated_rect" />
|
|
39
|
+
<smtpad portHints={["1"]} pcbX="-1.29999994mm" pcbY="4.12500064mm" layer="top" solderMaskMargin="0.04999989999999999mm" cornerRadius="0.04999989999999999mm" width="0.39999919999999994mm" height="1.34999984mm" ccwRotation={0} shape="rotated_rect" />
|
|
40
|
+
<silkscreenline x1={3.750015359999992} y1={2.84699964} x2={3.750015359999992} y2={4.29999394} strokeWidth={0.127} />
|
|
41
|
+
<silkscreenline x1={3.750015359999992} y1={4.29999394} x2={3.200003759999987} y2={4.29999394} strokeWidth={0.127} />
|
|
42
|
+
<silkscreenline x1={-3.7499950400000017} y1={2.84699964} x2={-3.7499950400000017} y2={4.29999394} strokeWidth={0.127} />
|
|
43
|
+
<silkscreenline x1={-3.199983440000011} y1={4.29999394} x2={-3.7499950400000017} y2={4.29999394} strokeWidth={0.127} />
|
|
44
|
+
<fabricationnotetext pcbX={-2.2644176200000032} pcbY={1.4476831600000004} anchorAlignment="center" text="J1" font="tscircuit2024" fontSize={1.016} color="#ec4899" pcbRotation="0deg" />
|
|
45
|
+
<silkscreentext pcbX={-5.334000000000003} pcbY={1.0159999999999982} anchorAlignment="bottom_left" fontSize={0.762} font="tscircuit2024" pcbRotation="360deg" mirrored={false} layer="top" text="J1" />
|
|
46
|
+
</footprint>} />
|
|
47
|
+
</board>
|
|
48
|
+
)
|
|
49
|
+
export default Dp83825Usb"
|
|
50
|
+
`)
|
|
51
|
+
const generatedCircuitJson = (await runTscircuitCode(
|
|
52
|
+
generatedTsx,
|
|
53
|
+
)) as CircuitJson
|
|
54
|
+
|
|
55
|
+
for (const [snapshotName, circuitJson] of [
|
|
56
|
+
["source", sourceCircuitJson],
|
|
57
|
+
["generated", generatedCircuitJson],
|
|
58
|
+
] as const) {
|
|
59
|
+
await expectTiEvm3dSnapshot({
|
|
60
|
+
cameraReferenceCircuitJson: sourceCircuitJson,
|
|
61
|
+
circuitJson,
|
|
62
|
+
fixtureName: "dp83825evm",
|
|
63
|
+
snapshotName,
|
|
64
|
+
testPath: import.meta.path,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const generatedCadComponent = generatedCircuitJson.find(
|
|
69
|
+
(element): element is CadComponent => element.type === "cad_component",
|
|
70
|
+
)
|
|
71
|
+
for (const axis of ["x", "y", "z"] as const) {
|
|
72
|
+
expect(generatedCadComponent?.position[axis]).toBeCloseTo(
|
|
73
|
+
sourceCadComponent.position[axis],
|
|
74
|
+
8,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
expect(generatedCadComponent).toMatchObject({
|
|
78
|
+
rotation: sourceCadComponent.rotation,
|
|
79
|
+
model_step_url: sourceCadComponent.model_step_url,
|
|
80
|
+
model_origin_position: sourceCadComponent.model_origin_position,
|
|
81
|
+
})
|
|
82
|
+
}, 60_000)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"type": "pcb_board",
|
|
4
|
+
"pcb_board_id": "pcb_pcb_board_altium",
|
|
5
|
+
"center": {
|
|
6
|
+
"x": -308.7624,
|
|
7
|
+
"y": 238.2139
|
|
8
|
+
},
|
|
9
|
+
"width": 24,
|
|
10
|
+
"height": 20,
|
|
11
|
+
"thickness": 1.6,
|
|
12
|
+
"num_layers": 8,
|
|
13
|
+
"material": "fr4",
|
|
14
|
+
"is_via_in_pad_allowed": true,
|
|
15
|
+
"min_trace_width": 0.254,
|
|
16
|
+
"min_via_hole_diameter": 0.7111999999999999,
|
|
17
|
+
"min_via_pad_diameter": 1.27
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "source_component",
|
|
21
|
+
"ftype": "simple_chip",
|
|
22
|
+
"source_component_id": "pcb_source_component_altium_633",
|
|
23
|
+
"name": "J13",
|
|
24
|
+
"display_name": "J13"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "pcb_component",
|
|
28
|
+
"pcb_component_id": "pcb_pcb_component_altium_633",
|
|
29
|
+
"source_component_id": "pcb_source_component_altium_633",
|
|
30
|
+
"center": {
|
|
31
|
+
"x": -308.7624,
|
|
32
|
+
"y": 238.2139
|
|
33
|
+
},
|
|
34
|
+
"width": 8.359902000000025,
|
|
35
|
+
"height": 14.126972000000007,
|
|
36
|
+
"layer": "top",
|
|
37
|
+
"rotation": 90,
|
|
38
|
+
"position_mode": "none",
|
|
39
|
+
"obstructs_within_bounds": false
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "cad_component",
|
|
43
|
+
"cad_component_id": "pcb_cad_component_altium_primitive_227",
|
|
44
|
+
"pcb_component_id": "pcb_pcb_component_altium_633",
|
|
45
|
+
"source_component_id": "pcb_source_component_altium_633",
|
|
46
|
+
"position": {
|
|
47
|
+
"x": -308.7624,
|
|
48
|
+
"y": 238.2139,
|
|
49
|
+
"z": 0.8
|
|
50
|
+
},
|
|
51
|
+
"rotation": {
|
|
52
|
+
"x": 0,
|
|
53
|
+
"y": 0,
|
|
54
|
+
"z": 0
|
|
55
|
+
},
|
|
56
|
+
"layer": "top",
|
|
57
|
+
"model_jscad": {
|
|
58
|
+
"type": "colorize",
|
|
59
|
+
"color": [0.5019607843137255, 0.5019607843137255, 0.5019607843137255],
|
|
60
|
+
"shape": {
|
|
61
|
+
"type": "extrudeLinear",
|
|
62
|
+
"options": {
|
|
63
|
+
"height": 3.81
|
|
64
|
+
},
|
|
65
|
+
"shape": {
|
|
66
|
+
"type": "fromPointsGeom2",
|
|
67
|
+
"points": [
|
|
68
|
+
[-2.7939999999999827, -6.921500000000009],
|
|
69
|
+
[2.7940000000000396, -6.921500000000009],
|
|
70
|
+
[2.7940000000000396, 6.921500000000009],
|
|
71
|
+
[-2.7939999999999827, 6.921500000000009]
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"model_unit_to_mm_scale_factor": 1,
|
|
77
|
+
"model_board_normal_direction": "z+",
|
|
78
|
+
"model_origin_position": {
|
|
79
|
+
"x": 0,
|
|
80
|
+
"y": 0,
|
|
81
|
+
"z": 0
|
|
82
|
+
},
|
|
83
|
+
"model_object_fit": "contain_within_bounds",
|
|
84
|
+
"anchor_alignment": "center"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"type": "pcb_plated_hole",
|
|
88
|
+
"pcb_plated_hole_id": "pcb_pcb_plated_hole_altium_7085",
|
|
89
|
+
"shape": "circle",
|
|
90
|
+
"outer_diameter": 2.199894,
|
|
91
|
+
"hole_diameter": 1.400048,
|
|
92
|
+
"x": -311.842404,
|
|
93
|
+
"y": 242.24640399999998,
|
|
94
|
+
"layers": ["top", "bottom"],
|
|
95
|
+
"port_hints": ["6"],
|
|
96
|
+
"pcb_component_id": "pcb_pcb_component_altium_633"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"type": "pcb_plated_hole",
|
|
100
|
+
"pcb_plated_hole_id": "pcb_pcb_plated_hole_altium_7086",
|
|
101
|
+
"shape": "circle",
|
|
102
|
+
"outer_diameter": 1.199896,
|
|
103
|
+
"hole_diameter": 0.649986,
|
|
104
|
+
"x": -310.86247199999997,
|
|
105
|
+
"y": 244.2464,
|
|
106
|
+
"layers": ["top", "bottom"],
|
|
107
|
+
"port_hints": ["4"],
|
|
108
|
+
"pcb_component_id": "pcb_pcb_component_altium_633"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"type": "pcb_plated_hole",
|
|
112
|
+
"pcb_plated_hole_id": "pcb_pcb_plated_hole_altium_7087",
|
|
113
|
+
"shape": "circle",
|
|
114
|
+
"outer_diameter": 1.199896,
|
|
115
|
+
"hole_diameter": 0.649986,
|
|
116
|
+
"x": -309.462424,
|
|
117
|
+
"y": 244.2464,
|
|
118
|
+
"layers": ["top", "bottom"],
|
|
119
|
+
"port_hints": ["3"],
|
|
120
|
+
"pcb_component_id": "pcb_pcb_component_altium_633"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"type": "pcb_plated_hole",
|
|
124
|
+
"pcb_plated_hole_id": "pcb_pcb_plated_hole_altium_7088",
|
|
125
|
+
"shape": "pill",
|
|
126
|
+
"outer_width": 2.9718,
|
|
127
|
+
"outer_height": 1.500124,
|
|
128
|
+
"hole_width": 2.9718,
|
|
129
|
+
"hole_height": 1.500124,
|
|
130
|
+
"ccw_rotation": 0,
|
|
131
|
+
"x": -308.7624,
|
|
132
|
+
"y": 236.646466,
|
|
133
|
+
"layers": ["top", "bottom"],
|
|
134
|
+
"port_hints": ["7"],
|
|
135
|
+
"pcb_component_id": "pcb_pcb_component_altium_633"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"type": "pcb_plated_hole",
|
|
139
|
+
"pcb_plated_hole_id": "pcb_pcb_plated_hole_altium_7089",
|
|
140
|
+
"shape": "circle",
|
|
141
|
+
"outer_diameter": 1.199896,
|
|
142
|
+
"hole_diameter": 0.649986,
|
|
143
|
+
"x": -308.062376,
|
|
144
|
+
"y": 244.2464,
|
|
145
|
+
"layers": ["top", "bottom"],
|
|
146
|
+
"port_hints": ["2"],
|
|
147
|
+
"pcb_component_id": "pcb_pcb_component_altium_633"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"type": "pcb_plated_hole",
|
|
151
|
+
"pcb_plated_hole_id": "pcb_pcb_plated_hole_altium_7090",
|
|
152
|
+
"shape": "circle",
|
|
153
|
+
"outer_diameter": 1.199896,
|
|
154
|
+
"hole_diameter": 0.649986,
|
|
155
|
+
"x": -306.662328,
|
|
156
|
+
"y": 244.2464,
|
|
157
|
+
"layers": ["top", "bottom"],
|
|
158
|
+
"port_hints": ["1"],
|
|
159
|
+
"pcb_component_id": "pcb_pcb_component_altium_633"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"type": "pcb_plated_hole",
|
|
163
|
+
"pcb_plated_hole_id": "pcb_pcb_plated_hole_altium_7091",
|
|
164
|
+
"shape": "circle",
|
|
165
|
+
"outer_diameter": 2.199894,
|
|
166
|
+
"hole_diameter": 1.400048,
|
|
167
|
+
"x": -305.682396,
|
|
168
|
+
"y": 242.24640399999998,
|
|
169
|
+
"layers": ["top", "bottom"],
|
|
170
|
+
"port_hints": ["5"],
|
|
171
|
+
"pcb_component_id": "pcb_pcb_component_altium_633"
|
|
172
|
+
}
|
|
173
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { expect } from "bun:test"
|
|
2
|
+
import { basename, dirname, join } from "node:path"
|
|
3
|
+
import type { CircuitJson, PcbBoard } from "circuit-json"
|
|
4
|
+
import {
|
|
5
|
+
convertCircuitJsonToGltf,
|
|
6
|
+
getBestCameraPosition,
|
|
7
|
+
} from "circuit-json-to-gltf"
|
|
8
|
+
import { renderGLTFToPNGFromGLB } from "poppygl"
|
|
9
|
+
import { expectPngSnapshot } from "./expect-png-snapshot"
|
|
10
|
+
|
|
11
|
+
export async function expectJscadCadModel3dSnapshots({
|
|
12
|
+
sourceCircuitJson,
|
|
13
|
+
generatedCircuitJson,
|
|
14
|
+
testPath,
|
|
15
|
+
}: {
|
|
16
|
+
sourceCircuitJson: CircuitJson
|
|
17
|
+
generatedCircuitJson: CircuitJson
|
|
18
|
+
testPath: string
|
|
19
|
+
}): Promise<void> {
|
|
20
|
+
const cameraReferenceBoard = sourceCircuitJson.find(
|
|
21
|
+
(element): element is PcbBoard => element.type === "pcb_board",
|
|
22
|
+
)
|
|
23
|
+
if (!cameraReferenceBoard) throw new Error("Missing camera reference board")
|
|
24
|
+
const testName = basename(testPath).replace(/\.test\.tsx?$/u, "")
|
|
25
|
+
const cameraPosition = getBestCameraPosition([cameraReferenceBoard], {
|
|
26
|
+
aspectRatio: 4 / 3,
|
|
27
|
+
direction: [-0.7, 1.2, -0.8],
|
|
28
|
+
})
|
|
29
|
+
let sourcePng: Uint8Array | undefined
|
|
30
|
+
|
|
31
|
+
for (const [snapshotName, circuitJson] of [
|
|
32
|
+
["source", sourceCircuitJson],
|
|
33
|
+
["generated", generatedCircuitJson],
|
|
34
|
+
] as const) {
|
|
35
|
+
const glb = await convertCircuitJsonToGltf(circuitJson, {
|
|
36
|
+
boardTextureResolution: 1024,
|
|
37
|
+
format: "glb",
|
|
38
|
+
})
|
|
39
|
+
if (!(glb instanceof ArrayBuffer)) {
|
|
40
|
+
throw new Error("3D conversion did not return a GLB")
|
|
41
|
+
}
|
|
42
|
+
const renderedPng = await renderGLTFToPNGFromGLB(glb, {
|
|
43
|
+
...cameraPosition,
|
|
44
|
+
backgroundColor: "#07100c",
|
|
45
|
+
height: 750,
|
|
46
|
+
width: 1000,
|
|
47
|
+
})
|
|
48
|
+
await expectPngSnapshot({
|
|
49
|
+
renderedPng,
|
|
50
|
+
snapshotPath: join(
|
|
51
|
+
dirname(testPath),
|
|
52
|
+
"__snapshots__",
|
|
53
|
+
`${testName}-${snapshotName}-3d.snap.png`,
|
|
54
|
+
),
|
|
55
|
+
})
|
|
56
|
+
if (snapshotName === "source") {
|
|
57
|
+
sourcePng = renderedPng
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
if (!sourcePng) throw new Error("Missing source 3D render")
|
|
61
|
+
expect(
|
|
62
|
+
Buffer.from(renderedPng).equals(sourcePng),
|
|
63
|
+
"Converted 3D render must match the native source",
|
|
64
|
+
).toBe(true)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { expect } from "bun:test"
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
3
|
+
import { dirname } from "node:path"
|
|
4
|
+
import { decode } from "fast-png"
|
|
5
|
+
|
|
6
|
+
const MAX_SNAPSHOT_DIFFERENT_PIXEL_FRACTION = 0.06
|
|
7
|
+
const MAX_CHANNEL_DIFFERENCE = 7
|
|
8
|
+
|
|
9
|
+
export async function expectPngSnapshot({
|
|
10
|
+
renderedPng,
|
|
11
|
+
snapshotPath,
|
|
12
|
+
}: {
|
|
13
|
+
renderedPng: Uint8Array
|
|
14
|
+
snapshotPath: string
|
|
15
|
+
}): Promise<void> {
|
|
16
|
+
const shouldUpdate =
|
|
17
|
+
process.env.BUN_UPDATE_SNAPSHOTS === "1" ||
|
|
18
|
+
process.env.BUN_FORCE_UPDATE_SNAPSHOTS === "1"
|
|
19
|
+
if (shouldUpdate) {
|
|
20
|
+
await mkdir(dirname(snapshotPath), { recursive: true })
|
|
21
|
+
await writeFile(snapshotPath, renderedPng)
|
|
22
|
+
}
|
|
23
|
+
expect(await Bun.file(snapshotPath).exists()).toBe(true)
|
|
24
|
+
|
|
25
|
+
const renderedImage = decode(renderedPng)
|
|
26
|
+
const snapshotImage = decode(await readFile(snapshotPath))
|
|
27
|
+
expect(renderedImage.width).toBe(snapshotImage.width)
|
|
28
|
+
expect(renderedImage.height).toBe(snapshotImage.height)
|
|
29
|
+
expect(renderedImage.channels).toBe(snapshotImage.channels)
|
|
30
|
+
|
|
31
|
+
let differentPixelCount = 0
|
|
32
|
+
const pixelCount = renderedImage.width * renderedImage.height
|
|
33
|
+
for (let pixelIndex = 0; pixelIndex < pixelCount; pixelIndex += 1) {
|
|
34
|
+
const firstChannelIndex = pixelIndex * renderedImage.channels
|
|
35
|
+
let pixelIsDifferent = false
|
|
36
|
+
for (
|
|
37
|
+
let channelIndex = 0;
|
|
38
|
+
channelIndex < renderedImage.channels;
|
|
39
|
+
channelIndex += 1
|
|
40
|
+
) {
|
|
41
|
+
const sampleIndex = firstChannelIndex + channelIndex
|
|
42
|
+
if (
|
|
43
|
+
Math.abs(
|
|
44
|
+
(renderedImage.data[sampleIndex] ?? 0) -
|
|
45
|
+
(snapshotImage.data[sampleIndex] ?? 0),
|
|
46
|
+
) > MAX_CHANNEL_DIFFERENCE
|
|
47
|
+
) {
|
|
48
|
+
pixelIsDifferent = true
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (pixelIsDifferent) differentPixelCount += 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const differentPixelFraction = differentPixelCount / pixelCount
|
|
56
|
+
expect(differentPixelFraction).toBeLessThanOrEqual(
|
|
57
|
+
MAX_SNAPSHOT_DIFFERENT_PIXEL_FRACTION,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
1
|
+
import { readFile } from "node:fs/promises"
|
|
3
2
|
import { basename, dirname, join } from "node:path"
|
|
4
3
|
import { gunzipSync } from "node:zlib"
|
|
5
4
|
import type { CadComponent, CircuitJson, PcbBoard } from "circuit-json"
|
|
@@ -7,16 +6,14 @@ import {
|
|
|
7
6
|
convertCircuitJsonToGltf,
|
|
8
7
|
getBestCameraPosition,
|
|
9
8
|
} from "circuit-json-to-gltf"
|
|
10
|
-
import { decode } from "fast-png"
|
|
11
9
|
import { renderGLTFToPNGFromGLB } from "poppygl"
|
|
10
|
+
import { expectPngSnapshot } from "./expect-png-snapshot"
|
|
12
11
|
|
|
13
12
|
type CadModelFileName = string
|
|
14
13
|
type EmbeddedStepModelUrl = string
|
|
15
14
|
type BoardViewName = "bottom" | "top"
|
|
16
15
|
type SnapshotName = "generated" | "source"
|
|
17
16
|
|
|
18
|
-
const MAX_SNAPSHOT_DIFFERENT_PIXEL_FRACTION = 0.06
|
|
19
|
-
const MAX_CHANNEL_DIFFERENCE = 7
|
|
20
17
|
const TOP_BOARD_CAMERA_DIRECTION = [-0.7, 1.2, -0.8] as const
|
|
21
18
|
const BOTTOM_BOARD_CAMERA_DIRECTION = [-0.7, -1.2, -0.8] as const
|
|
22
19
|
|
|
@@ -118,58 +115,6 @@ async function renderBoardView({
|
|
|
118
115
|
})
|
|
119
116
|
}
|
|
120
117
|
|
|
121
|
-
async function expectPngSnapshot({
|
|
122
|
-
renderedPng,
|
|
123
|
-
snapshotPath,
|
|
124
|
-
}: {
|
|
125
|
-
renderedPng: Uint8Array
|
|
126
|
-
snapshotPath: string
|
|
127
|
-
}): Promise<void> {
|
|
128
|
-
const shouldUpdate =
|
|
129
|
-
process.env.BUN_UPDATE_SNAPSHOTS === "1" ||
|
|
130
|
-
process.env.BUN_FORCE_UPDATE_SNAPSHOTS === "1"
|
|
131
|
-
if (shouldUpdate) {
|
|
132
|
-
await mkdir(dirname(snapshotPath), { recursive: true })
|
|
133
|
-
await writeFile(snapshotPath, renderedPng)
|
|
134
|
-
}
|
|
135
|
-
expect(await Bun.file(snapshotPath).exists()).toBe(true)
|
|
136
|
-
|
|
137
|
-
const renderedImage = decode(renderedPng)
|
|
138
|
-
const snapshotImage = decode(await readFile(snapshotPath))
|
|
139
|
-
expect(renderedImage.width).toBe(snapshotImage.width)
|
|
140
|
-
expect(renderedImage.height).toBe(snapshotImage.height)
|
|
141
|
-
expect(renderedImage.channels).toBe(snapshotImage.channels)
|
|
142
|
-
|
|
143
|
-
let differentPixelCount = 0
|
|
144
|
-
const pixelCount = renderedImage.width * renderedImage.height
|
|
145
|
-
for (let pixelIndex = 0; pixelIndex < pixelCount; pixelIndex += 1) {
|
|
146
|
-
const firstChannelIndex = pixelIndex * renderedImage.channels
|
|
147
|
-
let pixelIsDifferent = false
|
|
148
|
-
for (
|
|
149
|
-
let channelIndex = 0;
|
|
150
|
-
channelIndex < renderedImage.channels;
|
|
151
|
-
channelIndex += 1
|
|
152
|
-
) {
|
|
153
|
-
const sampleIndex = firstChannelIndex + channelIndex
|
|
154
|
-
if (
|
|
155
|
-
Math.abs(
|
|
156
|
-
(renderedImage.data[sampleIndex] ?? 0) -
|
|
157
|
-
(snapshotImage.data[sampleIndex] ?? 0),
|
|
158
|
-
) > MAX_CHANNEL_DIFFERENCE
|
|
159
|
-
) {
|
|
160
|
-
pixelIsDifferent = true
|
|
161
|
-
break
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (pixelIsDifferent) differentPixelCount += 1
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const differentPixelFraction = differentPixelCount / pixelCount
|
|
168
|
-
expect(differentPixelFraction).toBeLessThanOrEqual(
|
|
169
|
-
MAX_SNAPSHOT_DIFFERENT_PIXEL_FRACTION,
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
118
|
export async function expectTiEvm3dSnapshot({
|
|
174
119
|
cameraReferenceCircuitJson,
|
|
175
120
|
circuitJson,
|
|
@@ -24,3 +24,11 @@ the same five TI EVMs. Their `cad-models` directories contain the referenced
|
|
|
24
24
|
STEP files in gzip form. The tests embed those models before rendering top and
|
|
25
25
|
bottom 3D snapshots, so the complete Circuit JSON to TSX round trip is visible
|
|
26
26
|
without network access.
|
|
27
|
+
|
|
28
|
+
`dp83825-usb-cad-origin.circuit.json.gz` isolates J1 (Molex 475890001), its
|
|
29
|
+
native pads and silkscreen, and its embedded STEP placement from the same
|
|
30
|
+
DP83825EVM PcbDoc, converted with `altium-to-circuit-json@0.0.114`. Only the
|
|
31
|
+
surrounding board is cropped, and references to omitted electrical ports are
|
|
32
|
+
removed. The model reuses `cad-models/dp83825evm/8.step.gz`, verified against
|
|
33
|
+
the PcbDoc's embedded model bytes (SHA-256
|
|
34
|
+
`5c5fe4d712828519a11d3421da933f5b610d55c80e6c82a976539dff61385191`).
|
|
Binary file
|