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.
Files changed (24) hide show
  1. package/dist/{chunk-WQVP4ME2.js → chunk-ULPDKPX5.js} +6 -42
  2. package/dist/cli/main.js +4 -4
  3. package/dist/lib/index.js +1 -1
  4. package/lib/board-converter/get-pcb-component-render-placement.ts +5 -80
  5. package/lib/board-converter/get-pin-labels.ts +6 -1
  6. package/lib/board-converter/localize-pcb-component-elements.ts +1 -0
  7. package/lib/board-converter/stages/convert-pcb-components.ts +0 -1
  8. package/package.json +3 -3
  9. package/tests/__snapshots__/beagleplay-usb-cad-origin-generated-3d.snap.png +0 -0
  10. package/tests/__snapshots__/beagleplay-usb-cad-origin-source-3d.snap.png +0 -0
  11. package/tests/__snapshots__/board-imported-bga-pin-labels-orangecrab-bga-pin-labels.snap.svg +1 -0
  12. package/tests/__snapshots__/dp83825-usb-cad-origin-generated-bottom-3d.snap.png +0 -0
  13. package/tests/__snapshots__/dp83825-usb-cad-origin-generated-top-3d.snap.png +0 -0
  14. package/tests/__snapshots__/dp83825-usb-cad-origin-source-bottom-3d.snap.png +0 -0
  15. package/tests/__snapshots__/dp83825-usb-cad-origin-source-top-3d.snap.png +0 -0
  16. package/tests/beagleplay-usb-cad-origin.test.ts +57 -0
  17. package/tests/board-imported-bga-pin-labels.test.ts +145 -0
  18. package/tests/dp83825-usb-cad-origin.test.ts +82 -0
  19. package/tests/fixtures/beagleplay-j13-origin.circuit.json +173 -0
  20. package/tests/fixtures/expect-jscad-cad-model-3d-snapshots.ts +66 -0
  21. package/tests/fixtures/expect-png-snapshot.ts +59 -0
  22. package/tests/fixtures/expect-ti-evm-3d-snapshot.ts +2 -57
  23. package/tests/fixtures/ti-evms/README.md +8 -0
  24. 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: componentPosition,
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) return sourcePort.pin_number;
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-WQVP4ME2.js";
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.93",
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.521",
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.2775-libonly",
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,6 +1,6 @@
1
1
  import {
2
2
  convertCircuitJsonToTscircuit
3
- } from "../chunk-WQVP4ME2.js";
3
+ } from "../chunk-ULPDKPX5.js";
4
4
  export {
5
5
  convertCircuitJsonToTscircuit
6
6
  };
@@ -1,64 +1,15 @@
1
- import { getBoundsOfPcbElements } from "@tscircuit/circuit-json-util"
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
- const physicalElements = circuitJson.filter(
70
- (element) =>
71
- isPhysicalPcbElement(element) &&
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: componentPosition,
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) return sourcePort.pin_number
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")
@@ -94,7 +94,6 @@ export const convertPcbComponents: BoardConverterStage = ({
94
94
  )
95
95
  const { cadModelParentCenter, componentPosition } =
96
96
  getPcbComponentRenderPlacement({
97
- circuitJson,
98
97
  pcbBoard,
99
98
  pcbComponent,
100
99
  })
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.94",
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.521",
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.2775-libonly",
45
+ "tscircuit": "0.0.3043-libonly",
46
46
  "tsup": "^8.3.5",
47
47
  "zod": "^3.25.76"
48
48
  },
@@ -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+QBY6QBY2NBYyNBb26BZKPBZKQBdGnBZGMBZGNBduVBZGKBZGMBduBBZKJBZKJBcdvBYyKBYyLBZGGBZGHBYuKBYyKBbNcBYyKBYyKBY6FBY6FBYuJBYyKBZ83BYuFBYyEBYqJBYuJBYmFBYmGBYqHBUH7SAVW+xgFi4oFiooFXysFDvlI9y/3yxX4OvtDBZqBBZR9BY57BYd7BYF9BXyCBXqJBXqOBfyY92oFhI8FhY8FiowFiowFh5EFh5EFi4wFiosFipMFiZIFi4wFi4wFjZIFjJIFjIwFi4wFj5EFj5EFjIwFjIsFkY8Fko8F+Jj3agWcjwWciAWaggWVfgWPewWIegWCfQV8ggUO+Ui2+GkV+JiLBZyIBZqCBZV9BY57BYh7BYF9BXyCBXqIBfyYiwV6jgV8lAWBmQWImwWOmwWVmQWalAWc+4gV+JiLBZyIBZqCBZV9BY57BYh7BYF9BXyCBXqIBfyYiwV6jgV8lAWBmQWImwWOmwWVmQWalAUO+Uj44/fpFYyKBY+FBY+FBYuKBYyKBYyEBY2EBYuKBYuKBYmEBYqDBYqLBYuKBYeFBYeFBYqKBYqKBYWHBYSHBfyY+2oFeogFeo0FfJQFgZkFh5sFjpsFlJkFmpUF+Dv3QwX8O/dDBXyUBYKZBYicBY+bBZWYBZqUBZyOBZyHBfiY+2oFkocFkYcFDvlI+Ev3kxWGigX7q4sFi4sFLPtyBYF9BX2CBXqIBXmOBX2UBYGYBYecBY6bBfek+REFkJIFkJIFi4sFi4sFko8FkpAFi4sFjIsFk40Fk40FjIsFi4sFk4kFlIoFi4sFi4sFkoYFkocFjIsFi4sFkIQFj4QF97r9EQWPewWIegWBfgV9gQV6iAV5jgV9lAWBmAX7IvfIFfsB94AFJvuABQ75SPe++GUV2bYFlMsFTL8F+weeBSmGBYv7VgX32vxEFYSJBYmLBYmLBfv7hwWKiwWLiwWDjAWCjQWLiwWLiwWEjwWEkAWKiwWLiwWGkgWHkQWLjAWKiwWKkwWJkgWLjAWLiwWL+QoFjJIFjZIFi4wFi4wFkJEFjpEFjIwFjIwFkY8FkY8FjYsFjIwFkowFk40F9yWTBZCKBZCLBfcZdQWOigWPigWQiQWQiQWNiQWOiQXlQQWNiAWPiQWNhwWOhwWMhwWNiAWLhgWMhwWLhwWLhwV9+wAFiokFi4gFiIYFiYYFiYkFiogFh4cFh4cFiIoFiYkFSGYF9wJmBY6JBY+KBY+IBY+JBY2IBY6IBdE3BY6GBY6FBYyJBYuKBYyEBYyEBYX7AAWLiQWLiQWJhQWJhQWKiQWKiQVsWwWIiQWKiAWHiAWHhwWIigWIiQU+ZAWJigWJigVu3BXEqAWeqAWP3QVYyQX7JLsF+1CWBYv7nAUO+Uj4ZPclFaDbBZOZBZiXBZuQBZ2KBZuEBZd/BZF7BYp7BXIwBYqIBYqHBYiHBYmHBYiIBYmIBUNLBYiJBYmJBYaJBYaIBYiLBYiJBSl1BYuLBYuLBYOLBYKLBYuLBYuLBfsEnAWJiwWKjAWEjQWEjgWKjAWKiwVRsQWFkQWFkQVU3QWLiwWLiwWIkwWIkgWLiwWLiwVm918Fi5MFi5MFuvdeBYyMBYuNBY+RBY2RBY2MBYyNBc7XBYyMBYuLBZGQBZKQBYuLBYuLBdGsBZKNBZKOBdqVBYyLBYyLBZOKBZKLBYyKBY2LBfVqBYyLBYyLBZKHBZGHBYyLBYyKBcNZBYyLBYuKBZCFBY+FBYyKBYuLBapEBY57BYh7BYF9BXyCBXqIBXqOBXyVBYGYBXDJBWKwBTSlBU6EBVVxBVRNBWD7TAWt+04FuUgFtG8F6H0F2JwFDvlI93uVFYaKBfs1iAWLiwWKiwWDjAWDjQWKiwWLiwWEjwWEjwWLjAWKiwWHkgWGkQWLjAWLiwWJkwWJkgWMjAWKiwWT+QQFjJAFi5EFjY0Fi48Fj48FjY8FjY4FjY0FkI4Fj44Fjo0FjowFkIwFkY0F7pgFj4sFjowF14kFkYoFkIsF9xBrBYuLBYuLBZOHBZKIBYyLBYuKBeZIBYyKBYuLBZCFBZGFBYuLBYuKBb8wBY2FBY6GBaAsBYuHBYyHBY1EBYqHBYuIBYJOBYmHBYqHBXhXBYeFBYmGBUw8BYWGBYWGBTdZBYWJBYWIBfsdaAV53hX3EasF0rUFwM4Fm7UFlMAFicsFeOAFXtoFPsQF+wGnBUiMBVGDBYX8tAUO+Uj3EeUV+CGLBZyIBZqCBZV9BY57BYh7BYF9BXyCBXqIBfxOiwV6jgV8lAWCmQWHmwWL99MFi/fSBY+bBZSZBZqUBZyPBfhOiwWchwWaggWVfQWOewWIewWBfQV8ggV6iAX8IYsFi/t+BfeciwWdiAWZggWVfQWOewWIegWBfgV9ggV5hwX7nIsFDvlI9xf32hWL+6oFiHsFgX0FfYIFeYgFeo4FfZQFgZkFh5sFi/kRBY+bBZWZBZmUBZyPBZ2HBYyKBfgviwWciAWZggWVfQWPewWHewWBfQV9ggV6iAX8FYsFi/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 { expect } from "bun:test"
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`).