@tscircuit/cli 0.1.1739 → 0.1.1740

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/cli/main.js CHANGED
@@ -39814,7 +39814,7 @@ var init_chunk_4ISXDEM5 = __esm(() => {
39814
39814
  var exports_dist_3YR26ALR = {};
39815
39815
  __export2(exports_dist_3YR26ALR, {
39816
39816
  isSimulationVoltageProbe: () => isSimulationVoltageProbe,
39817
- isSimulationTransientVoltageGraph: () => isSimulationTransientVoltageGraph2,
39817
+ isSimulationTransientVoltageGraph: () => isSimulationTransientVoltageGraph,
39818
39818
  isSimulationExperiment: () => isSimulationExperiment2,
39819
39819
  getSoftwareUsedString: () => getSoftwareUsedString,
39820
39820
  createSvgObjectsForSchComponentPortHovers: () => createSvgObjectsForSchComponentPortHovers,
@@ -49688,7 +49688,7 @@ function convertCircuitJsonToSchematicSvg3(circuitJson, options) {
49688
49688
  };
49689
49689
  return stringify(svgObject);
49690
49690
  }
49691
- function isSimulationTransientVoltageGraph2(value) {
49691
+ function isSimulationTransientVoltageGraph(value) {
49692
49692
  return value?.type === "simulation_transient_voltage_graph";
49693
49693
  }
49694
49694
  function isSimulationExperiment2(value) {
@@ -49707,7 +49707,7 @@ function convertCircuitJsonToSimulationGraphSvg2({
49707
49707
  }) {
49708
49708
  const selectedIds = simulation_transient_voltage_graph_ids ? new Set(simulation_transient_voltage_graph_ids) : null;
49709
49709
  const experiment = circuitJson.find((element) => isSimulationExperiment2(element) && element.simulation_experiment_id === simulation_experiment_id);
49710
- const graphs = circuitJson.filter((element) => isSimulationTransientVoltageGraph2(element) && element.simulation_experiment_id === simulation_experiment_id && (!selectedIds || selectedIds.has(element.simulation_transient_voltage_graph_id)));
49710
+ const graphs = circuitJson.filter((element) => isSimulationTransientVoltageGraph(element) && element.simulation_experiment_id === simulation_experiment_id && (!selectedIds || selectedIds.has(element.simulation_transient_voltage_graph_id)));
49711
49711
  if (graphs.length === 0) {
49712
49712
  throw new Error(`No simulation_transient_voltage_graph elements found for simulation_experiment_id "${simulation_experiment_id}"`);
49713
49713
  }
@@ -50249,7 +50249,7 @@ function convertCircuitJsonToSchematicSimulationSvg2({
50249
50249
  graphAboveSchematic = false,
50250
50250
  showErrorsInTextOverlay
50251
50251
  }) {
50252
- const schematicElements = circuitJson.filter((element) => !isSimulationExperiment2(element) && !isSimulationTransientVoltageGraph2(element));
50252
+ const schematicElements = circuitJson.filter((element) => !isSimulationExperiment2(element) && !isSimulationTransientVoltageGraph(element));
50253
50253
  const clampedRatio = clamp01(schematicHeightRatio);
50254
50254
  const rawSchematicHeight = Math.max(1, height * clampedRatio);
50255
50255
  const rawSimulationHeight = Math.max(1, height - rawSchematicHeight);
@@ -118506,7 +118506,7 @@ var import_perfect_cli = __toESM2(require_dist2(), 1);
118506
118506
  // lib/getVersion.ts
118507
118507
  import { createRequire as createRequire2 } from "node:module";
118508
118508
  // package.json
118509
- var version = "0.1.1738";
118509
+ var version = "0.1.1739";
118510
118510
  var package_default = {
118511
118511
  name: "@tscircuit/cli",
118512
118512
  version,
@@ -118529,7 +118529,7 @@ var package_default = {
118529
118529
  "@tscircuit/image-utils": "^0.0.8",
118530
118530
  "@tscircuit/krt-wasm": "^0.1.6",
118531
118531
  "@tscircuit/math-utils": "0.0.36",
118532
- "@tscircuit/props": "^0.0.580",
118532
+ "@tscircuit/props": "^0.0.592",
118533
118533
  "@tscircuit/runframe": "^0.0.2274",
118534
118534
  "@tscircuit/schematic-match-adapt": "^0.0.22",
118535
118535
  "@tscircuit/solver-utils": "0.0.16",
@@ -118543,21 +118543,21 @@ var package_default = {
118543
118543
  "@types/semver": "^7.5.8",
118544
118544
  "bun-match-svg": "^0.0.12",
118545
118545
  chokidar: "4.0.1",
118546
- "circuit-json": "^0.0.446",
118546
+ "circuit-json": "^0.0.455",
118547
118547
  "circuit-json-to-3d-png": "^0.0.6",
118548
118548
  "circuit-json-to-bom-csv": "^0.0.7",
118549
118549
  "circuit-json-to-connectivity-map": "^0.0.25",
118550
- "circuit-json-to-footprinter": "^0.0.1",
118550
+ "circuit-json-to-footprinter": "^0.0.15",
118551
118551
  "circuit-json-to-gerber": "^0.0.83",
118552
118552
  "circuit-json-to-kicad": "0.0.157",
118553
118553
  "circuit-json-to-pnp-csv": "^0.0.7",
118554
118554
  "circuit-json-to-readable-netlist": "^0.0.15",
118555
- "circuit-json-to-spice": "^0.0.40",
118555
+ "circuit-json-to-spice": "^0.0.45",
118556
118556
  "circuit-json-to-step": "^0.0.38",
118557
118557
  "circuit-json-to-tscircuit": "^0.0.42",
118558
118558
  "circuit-json-trace-length-analysis": "github:tscircuit/circuit-json-trace-length-analysis#2b44792a40df0ca83b6bfb6ac95ed5e35e7168b8",
118559
118559
  "circuit-to-canvas": "^0.0.111",
118560
- "circuit-to-svg": "^0.0.378",
118560
+ "circuit-to-svg": "^0.0.393",
118561
118561
  commander: "^14.0.0",
118562
118562
  conf: "^13.1.0",
118563
118563
  configstore: "^7.0.0",
@@ -118586,9 +118586,10 @@ var package_default = {
118586
118586
  prompts: "^2.4.2",
118587
118587
  redaxios: "^0.5.1",
118588
118588
  semver: "^7.6.3",
118589
+ spicets: "^0.0.4",
118589
118590
  stepts: "^0.0.3",
118590
118591
  tempy: "^3.1.0",
118591
- tscircuit: "0.0.2099-libonly",
118592
+ tscircuit: "0.0.2142-libonly",
118592
118593
  tsx: "^4.7.1",
118593
118594
  "typed-ky": "^0.0.4",
118594
118595
  zod: "^3.23.8"
@@ -133667,20 +133668,26 @@ async function loadLocalStepModelFsMap(circuitJson) {
133667
133668
  import {
133668
133669
  convertCircuitJsonToSchematicSimulationSvg,
133669
133670
  convertCircuitJsonToSimulationGraphSvg,
133670
- isSimulationExperiment,
133671
- isSimulationTransientVoltageGraph
133671
+ isSimulationExperiment
133672
133672
  } from "circuit-to-svg";
133673
- var isSimulationTransientCurrentGraph = (element) => element.type === "simulation_transient_current_graph";
133673
+ var SIMULATION_ANALYSIS_RESULT_TYPES = new Set([
133674
+ "simulation_transient_voltage_graph",
133675
+ "simulation_transient_current_graph",
133676
+ "simulation_dc_operating_point_voltage",
133677
+ "simulation_dc_operating_point_current",
133678
+ "simulation_dc_sweep_voltage_graph",
133679
+ "simulation_dc_sweep_current_graph",
133680
+ "simulation_ac_sweep_voltage_graph",
133681
+ "simulation_ac_sweep_current_graph"
133682
+ ]);
133683
+ var isSimulationAnalysisResult = (element) => SIMULATION_ANALYSIS_RESULT_TYPES.has(element.type);
133674
133684
  var getSimulationSvgInputs = (circuitJson, simulationExperimentId) => {
133675
- const simulationTransientCurrentGraphIds = circuitJson.filter((element) => isSimulationTransientCurrentGraph(element) && element.simulation_experiment_id === simulationExperimentId).map((element) => element.simulation_transient_current_graph_id);
133676
- const simulationTransientVoltageGraphIds = circuitJson.filter((element) => isSimulationTransientVoltageGraph(element) && element.simulation_experiment_id === simulationExperimentId).map((element) => element.simulation_transient_voltage_graph_id);
133677
- if (simulationTransientCurrentGraphIds.length === 0 && simulationTransientVoltageGraphIds.length === 0) {
133685
+ const hasAnalysisResult = circuitJson.some((element) => isSimulationAnalysisResult(element) && element.simulation_experiment_id === simulationExperimentId);
133686
+ if (!hasAnalysisResult) {
133678
133687
  return;
133679
133688
  }
133680
133689
  return {
133681
- simulation_experiment_id: simulationExperimentId,
133682
- simulation_transient_current_graph_ids: simulationTransientCurrentGraphIds,
133683
- simulation_transient_voltage_graph_ids: simulationTransientVoltageGraphIds
133690
+ simulation_experiment_id: simulationExperimentId
133684
133691
  };
133685
133692
  };
133686
133693
  var toFileNameSuffix = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -286063,140 +286070,69 @@ var convertKicadFootprintToTsx = async ({
286063
286070
  console.log(kleur_default.green(`Converted ${outputPath}`));
286064
286071
  };
286065
286072
 
286066
- // node_modules/circuit-json-to-footprinter/dist/index.js
286067
- import { fp as fp3 } from "@tscircuit/footprinter";
286068
- import { getFootprintNames as getFootprintNames2, getFootprintSizes } from "@tscircuit/footprinter";
286069
- var toNumber4 = (value, fallback = 0) => {
286070
- if (typeof value === "number" && Number.isFinite(value))
286071
- return value;
286072
- if (typeof value === "string") {
286073
- const parsed = Number.parseFloat(value);
286074
- if (Number.isFinite(parsed))
286075
- return parsed;
286073
+ // node_modules/circuit-json-to-footprinter/dist/chunk-HCIYAG7H.js
286074
+ import {
286075
+ getBoundsFromPoints as getBoundsFromPoints4
286076
+ } from "@tscircuit/math-utils";
286077
+ import { getBoundsCenter, isPointInsidePolygon as isPointInsidePolygon3 } from "@tscircuit/math-utils";
286078
+ var toRadians2 = (degrees) => degrees * Math.PI / 180;
286079
+ var rotatePoint4 = (x4, y4, radians) => ({
286080
+ x: x4 * Math.cos(radians) - y4 * Math.sin(radians),
286081
+ y: x4 * Math.sin(radians) + y4 * Math.cos(radians)
286082
+ });
286083
+ var getSizedBounds = (points) => {
286084
+ const bounds = getBoundsFromPoints4([...points]);
286085
+ if (!bounds) {
286086
+ throw new Error("Cannot calculate bounds for an empty point list");
286076
286087
  }
286077
- return fallback;
286078
- };
286079
- var normalizePortHint = (hint) => {
286080
- const trimmed = hint.trim();
286081
- const pinMatch = trimmed.match(/^pin(\d+)$/i);
286082
- if (pinMatch)
286083
- return `pin${pinMatch[1]}`;
286084
- const numericMatch = trimmed.match(/^(\d+)$/);
286085
- return numericMatch ? `pin${numericMatch[1]}` : trimmed;
286088
+ return {
286089
+ ...bounds,
286090
+ height: bounds.maxY - bounds.minY,
286091
+ width: bounds.maxX - bounds.minX
286092
+ };
286086
286093
  };
286087
- var normalizeShape = (shape, width, height) => {
286088
- const normalized = typeof shape === "string" ? shape.toLowerCase() : "rect";
286089
- if (normalized === "circle" || normalized === "ellipse")
286090
- return "circle";
286091
- if (normalized === "pill" || normalized === "oval")
286092
- return "pill";
286093
- if (Math.abs(width - height) < 0.00001 && normalized === "round") {
286094
- return "circle";
286094
+ var getPolygonArea = (points) => {
286095
+ let doubledArea = 0;
286096
+ for (let index = 0;index < points.length; index += 1) {
286097
+ const current2 = points[index];
286098
+ const next = points[(index + 1) % points.length];
286099
+ doubledArea += current2.x * next.y - next.x * current2.y;
286095
286100
  }
286096
- return "rect";
286097
- };
286098
- var getCornerRadius = (element, width, height) => {
286099
- const radius = toNumber4(element.corner_radius ?? element.cornerRadius ?? element.rect_border_radius);
286100
- return radius > 0 ? Math.min(radius, width / 2, height / 2) : undefined;
286101
+ return Math.abs(doubledArea) / 2;
286101
286102
  };
286102
- var circuitJsonToPreview = (circuitJson, options = {}) => {
286103
- const pads = circuitJson.flatMap((rawElement, index) => {
286104
- const element = rawElement;
286105
- if (element.type === "pcb_smtpad") {
286106
- const width = toNumber4(element.width);
286107
- const height = toNumber4(element.height);
286108
- return [
286109
- {
286110
- cornerRadius: getCornerRadius(element, width, height),
286111
- height,
286112
- id: String(element.pcb_smtpad_id ?? `pcb_smtpad_${index + 1}`),
286113
- kind: "smt",
286114
- layer: String(element.layer ?? "top"),
286115
- portHints: Array.isArray(element.port_hints) ? element.port_hints.map((hint) => normalizePortHint(String(hint))) : [],
286116
- rotation: toNumber4(element.rotation),
286117
- shape: normalizeShape(element.shape, width, height),
286118
- width,
286119
- x: toNumber4(element.x),
286120
- y: toNumber4(element.y)
286121
- }
286122
- ];
286123
- }
286124
- if (element.type === "pcb_plated_hole") {
286125
- const width = toNumber4(element.width ?? element.outer_diameter ?? element.outerDiameter, 0.6);
286126
- const height = toNumber4(element.height, width);
286127
- return [
286128
- {
286129
- cornerRadius: getCornerRadius(element, width, height),
286130
- height,
286131
- id: String(element.pcb_plated_hole_id ?? `pcb_plated_hole_${index + 1}`),
286132
- kind: "plated-hole",
286133
- layer: Array.isArray(element.layers) ? String(element.layers[0] ?? "top") : "top",
286134
- portHints: Array.isArray(element.port_hints) ? element.port_hints.map((hint) => normalizePortHint(String(hint))) : [],
286135
- rotation: toNumber4(element.rotation),
286136
- shape: normalizeShape(element.shape ?? "circle", width, height),
286137
- width,
286138
- x: toNumber4(element.x),
286139
- y: toNumber4(element.y)
286140
- }
286141
- ];
286142
- }
286143
- return [];
286144
- });
286145
- if (pads.length === 0) {
286146
- throw new Error("Circuit JSON must contain at least one PCB SMT pad or plated hole");
286103
+ var getPolygonWorldPoints = (shape) => {
286104
+ if (shape.shape !== "polygon" || !shape.points) {
286105
+ throw new Error("Polygon preview shapes require points");
286147
286106
  }
286148
- return {
286149
- pads,
286150
- sourceHints: options.sourceHints,
286151
- subtitle: options.subtitle ?? "Circuit JSON footprint",
286152
- title: options.title ?? "Circuit JSON"
286153
- };
286154
- };
286155
- var footprinterStringToPreview = (footprinterString) => {
286156
- const normalized = footprinterString.trim();
286157
- if (!normalized)
286158
- throw new Error("Footprinter string is required");
286159
- return circuitJsonToPreview(fp3.string(normalized).circuitJson(), {
286160
- subtitle: "Generated by @tscircuit/footprinter",
286161
- title: normalized
286107
+ const radians = toRadians2(shape.rotation);
286108
+ return shape.points.map((point9) => {
286109
+ const rotated = rotatePoint4(point9.x, point9.y, radians);
286110
+ return { x: rotated.x + shape.x, y: rotated.y + shape.y };
286162
286111
  });
286163
286112
  };
286164
- var DEFAULT_GRID_SIZE = 320;
286165
- var toRadians2 = (degrees) => degrees * Math.PI / 180;
286166
- var rotatePoint4 = (x4, y4, radians) => ({
286167
- x: x4 * Math.cos(radians) - y4 * Math.sin(radians),
286168
- y: x4 * Math.sin(radians) + y4 * Math.cos(radians)
286169
- });
286170
- var getPadBounds3 = (pad2) => {
286171
- const halfWidth = pad2.width / 2;
286172
- const halfHeight = pad2.height / 2;
286173
- const radians = toRadians2(pad2.rotation);
286113
+ var getShapeBounds = (shape) => {
286114
+ if (shape.shape === "polygon") {
286115
+ if (!shape.points || shape.points.length < 3) {
286116
+ throw new Error("Polygon preview shapes require at least three points");
286117
+ }
286118
+ return getSizedBounds(getPolygonWorldPoints(shape));
286119
+ }
286120
+ const halfWidth = shape.width / 2;
286121
+ const halfHeight = shape.height / 2;
286122
+ const radians = toRadians2(shape.rotation);
286174
286123
  const corners = [
286175
286124
  rotatePoint4(-halfWidth, -halfHeight, radians),
286176
286125
  rotatePoint4(halfWidth, -halfHeight, radians),
286177
286126
  rotatePoint4(halfWidth, halfHeight, radians),
286178
286127
  rotatePoint4(-halfWidth, halfHeight, radians)
286179
286128
  ].map((corner) => ({
286180
- x: corner.x + pad2.x,
286181
- y: corner.y + pad2.y
286129
+ x: corner.x + shape.x,
286130
+ y: corner.y + shape.y
286182
286131
  }));
286183
- const xs3 = corners.map((corner) => corner.x);
286184
- const ys3 = corners.map((corner) => corner.y);
286185
- const minX = Math.min(...xs3);
286186
- const maxX = Math.max(...xs3);
286187
- const minY = Math.min(...ys3);
286188
- const maxY = Math.max(...ys3);
286189
- return {
286190
- height: maxY - minY,
286191
- maxX,
286192
- maxY,
286193
- minX,
286194
- minY,
286195
- width: maxX - minX
286196
- };
286132
+ return getSizedBounds(corners);
286197
286133
  };
286198
- var getFootprintBounds = (pads) => {
286199
- if (!pads.length) {
286134
+ var getFootprintBounds = (shapes) => {
286135
+ if (shapes.length === 0) {
286200
286136
  return {
286201
286137
  height: 1,
286202
286138
  maxX: 0.5,
@@ -286206,20 +286142,13 @@ var getFootprintBounds = (pads) => {
286206
286142
  width: 1
286207
286143
  };
286208
286144
  }
286209
- const bounds = pads.map(getPadBounds3);
286210
- const minX = Math.min(...bounds.map((bound) => bound.minX));
286211
- const minY = Math.min(...bounds.map((bound) => bound.minY));
286212
- const maxX = Math.max(...bounds.map((bound) => bound.maxX));
286213
- const maxY = Math.max(...bounds.map((bound) => bound.maxY));
286214
- return {
286215
- height: maxY - minY,
286216
- maxX,
286217
- maxY,
286218
- minX,
286219
- minY,
286220
- width: maxX - minX
286221
- };
286145
+ const bounds = shapes.map(getShapeBounds);
286146
+ return getSizedBounds(bounds.flatMap((bound) => [
286147
+ { x: bound.minX, y: bound.minY },
286148
+ { x: bound.maxX, y: bound.maxY }
286149
+ ]));
286222
286150
  };
286151
+ var DEFAULT_GRID_SIZE = 320;
286223
286152
  var addPadding = (bounds) => {
286224
286153
  const padX = Math.max(bounds.width * 0.18, 0.65);
286225
286154
  const padY = Math.max(bounds.height * 0.18, 0.65);
@@ -286242,21 +286171,40 @@ var translateFootprint = (footprint, deltaX, deltaY) => ({
286242
286171
  });
286243
286172
  var centerFootprint = (footprint) => {
286244
286173
  const bounds = getFootprintBounds(footprint.pads);
286245
- const centerX = (bounds.minX + bounds.maxX) / 2;
286246
- const centerY = (bounds.minY + bounds.maxY) / 2;
286247
- return translateFootprint(footprint, -centerX, -centerY);
286248
- };
286249
- var pointInPad = (x4, y4, pad2) => {
286250
- const dx3 = x4 - pad2.x;
286251
- const dy3 = y4 - pad2.y;
286252
- const local = rotatePoint4(dx3, dy3, -toRadians2(pad2.rotation));
286253
- const halfWidth = pad2.width / 2;
286254
- const halfHeight = pad2.height / 2;
286255
- if (pad2.shape === "circle") {
286174
+ const center2 = getBoundsCenter(bounds);
286175
+ return translateFootprint(footprint, -center2.x, -center2.y);
286176
+ };
286177
+ var getHoleShapes = (pads) => pads.flatMap((pad2) => {
286178
+ if (!pad2.hole)
286179
+ return [];
286180
+ return [
286181
+ {
286182
+ height: pad2.hole.height,
286183
+ rotation: pad2.hole.rotation,
286184
+ shape: pad2.hole.shape,
286185
+ width: pad2.hole.width,
286186
+ x: pad2.x + pad2.hole.offsetX,
286187
+ y: pad2.y + pad2.hole.offsetY
286188
+ }
286189
+ ];
286190
+ });
286191
+ var pointInShape = (x4, y4, shape) => {
286192
+ const dx3 = x4 - shape.x;
286193
+ const dy3 = y4 - shape.y;
286194
+ const local = rotatePoint4(dx3, dy3, -toRadians2(shape.rotation));
286195
+ const halfWidth = shape.width / 2;
286196
+ const halfHeight = shape.height / 2;
286197
+ if (shape.shape === "polygon") {
286198
+ if (!shape.points || shape.points.length < 3) {
286199
+ throw new Error("Polygon preview shapes require at least three points");
286200
+ }
286201
+ return isPointInsidePolygon3(local, shape.points);
286202
+ }
286203
+ if (shape.shape === "circle") {
286256
286204
  return Math.hypot(local.x, local.y) <= Math.min(halfWidth, halfHeight);
286257
286205
  }
286258
- if (pad2.shape === "rect") {
286259
- const cornerRadius = Math.max(0, Math.min(pad2.cornerRadius ?? 0, halfWidth, halfHeight));
286206
+ if (shape.shape === "rect") {
286207
+ const cornerRadius = Math.max(0, Math.min(shape.cornerRadius ?? 0, halfWidth, halfHeight));
286260
286208
  if (cornerRadius === 0) {
286261
286209
  return Math.abs(local.x) <= halfWidth && Math.abs(local.y) <= halfHeight;
286262
286210
  }
@@ -286272,7 +286220,7 @@ var pointInPad = (x4, y4, pad2) => {
286272
286220
  const cornerDy = absY - innerHalfHeight;
286273
286221
  return cornerDx >= 0 && cornerDy >= 0 && cornerDx * cornerDx + cornerDy * cornerDy <= cornerRadius * cornerRadius;
286274
286222
  }
286275
- if (pad2.width >= pad2.height) {
286223
+ if (shape.width >= shape.height) {
286276
286224
  const capsuleLength2 = halfWidth - halfHeight;
286277
286225
  if (Math.abs(local.x) <= capsuleLength2 && Math.abs(local.y) <= halfHeight) {
286278
286226
  return true;
@@ -286299,12 +286247,18 @@ var mergeBounds3 = (left, right) => {
286299
286247
  width: maxX - minX
286300
286248
  };
286301
286249
  };
286302
- var summarizeCopperComparison = (left, right, gridSize = DEFAULT_GRID_SIZE) => {
286303
- const normalizedLeft = centerFootprint(left);
286304
- const normalizedRight = centerFootprint(right);
286305
- const bounds = addPadding(mergeBounds3(getFootprintBounds(normalizedLeft.pads), getFootprintBounds(normalizedRight.pads)));
286250
+ var getComparisonBounds = (left, right) => {
286251
+ if (!left.length)
286252
+ return addPadding(getFootprintBounds(right));
286253
+ if (!right.length)
286254
+ return addPadding(getFootprintBounds(left));
286255
+ return addPadding(mergeBounds3(getFootprintBounds(left), getFootprintBounds(right)));
286256
+ };
286257
+ var rasterizeShapes = (left, right, gridSize, includeOccupancy) => {
286258
+ const bounds = getComparisonBounds(left, right);
286306
286259
  const cellWidth = bounds.width / gridSize;
286307
286260
  const cellHeight = bounds.height / gridSize;
286261
+ const occupancy = includeOccupancy ? new Uint8Array(gridSize * gridSize) : undefined;
286308
286262
  let leftCount = 0;
286309
286263
  let rightCount = 0;
286310
286264
  let overlapCount = 0;
@@ -286312,24 +286266,230 @@ var summarizeCopperComparison = (left, right, gridSize = DEFAULT_GRID_SIZE) => {
286312
286266
  const sampleY = bounds.maxY - (row + 0.5) * cellHeight;
286313
286267
  for (let column = 0;column < gridSize; column += 1) {
286314
286268
  const sampleX = bounds.minX + (column + 0.5) * cellWidth;
286315
- const inLeft = normalizedLeft.pads.some((pad2) => pointInPad(sampleX, sampleY, pad2));
286316
- const inRight = normalizedRight.pads.some((pad2) => pointInPad(sampleX, sampleY, pad2));
286269
+ const inLeft = left.some((shape) => pointInShape(sampleX, sampleY, shape));
286270
+ const inRight = right.some((shape) => pointInShape(sampleX, sampleY, shape));
286317
286271
  if (inLeft)
286318
286272
  leftCount += 1;
286319
286273
  if (inRight)
286320
286274
  rightCount += 1;
286321
286275
  if (inLeft && inRight)
286322
286276
  overlapCount += 1;
286277
+ if (occupancy) {
286278
+ const index = row * gridSize + column;
286279
+ occupancy[index] = inLeft && inRight ? 3 : inLeft ? 1 : inRight ? 2 : 0;
286280
+ }
286323
286281
  }
286324
286282
  }
286325
286283
  const unionCount = leftCount + rightCount - overlapCount;
286326
- const cellArea = cellWidth * cellHeight;
286327
- const intersectionArea2 = overlapCount * cellArea;
286328
- const unionArea = unionCount * cellArea;
286329
286284
  return {
286330
- copperIntersectionOverUnion: unionArea === 0 ? 0 : intersectionArea2 / unionArea
286285
+ coverageLeft: leftCount === 0 ? 0 : overlapCount / leftCount,
286286
+ coverageRight: rightCount === 0 ? 0 : overlapCount / rightCount,
286287
+ iou: unionCount === 0 ? 0 : overlapCount / unionCount,
286288
+ leftOnlyRatio: unionCount === 0 ? 0 : Math.max(leftCount - overlapCount, 0) / unionCount,
286289
+ occupancy,
286290
+ rightOnlyRatio: unionCount === 0 ? 0 : Math.max(rightCount - overlapCount, 0) / unionCount
286291
+ };
286292
+ };
286293
+ var compareNormalizedFootprints = (left, right, gridSize, includeOccupancy) => {
286294
+ const normalizedLeft = centerFootprint(left);
286295
+ const normalizedRight = centerFootprint(right);
286296
+ const comparison = rasterizeShapes(normalizedLeft.pads, normalizedRight.pads, gridSize, includeOccupancy);
286297
+ return { comparison, normalizedLeft, normalizedRight };
286298
+ };
286299
+ var summarizeCopperComparison = (left, right, gridSize = DEFAULT_GRID_SIZE) => {
286300
+ const { comparison, normalizedLeft, normalizedRight } = compareNormalizedFootprints(left, right, gridSize, false);
286301
+ const leftHoles = getHoleShapes(normalizedLeft.pads);
286302
+ const rightHoles = getHoleShapes(normalizedRight.pads);
286303
+ const holeIntersectionOverUnion = leftHoles.length === 0 && rightHoles.length === 0 ? 1 : rasterizeShapes(leftHoles, rightHoles, gridSize, false).iou;
286304
+ return {
286305
+ copperIntersectionOverUnion: comparison.iou,
286306
+ holeIntersectionOverUnion
286307
+ };
286308
+ };
286309
+
286310
+ // node_modules/circuit-json-to-footprinter/dist/index.js
286311
+ import { fp as fp3 } from "@tscircuit/footprinter";
286312
+ import {
286313
+ getBoundsCenter as getBoundsCenter2,
286314
+ getBoundsFromPoints as getBoundsFromPoints5
286315
+ } from "@tscircuit/math-utils";
286316
+ import { getFootprintNames as getFootprintNames2, getFootprintSizes } from "@tscircuit/footprinter";
286317
+ var toNumber4 = (value, fallback = 0) => {
286318
+ if (typeof value === "number" && Number.isFinite(value))
286319
+ return value;
286320
+ if (typeof value === "string") {
286321
+ const parsed = Number.parseFloat(value);
286322
+ if (Number.isFinite(parsed))
286323
+ return parsed;
286324
+ }
286325
+ return fallback;
286326
+ };
286327
+ var normalizePortHint = (hint) => {
286328
+ const trimmed = hint.trim();
286329
+ const pinMatch = trimmed.match(/^pin(\d+)$/i);
286330
+ if (pinMatch)
286331
+ return `pin${pinMatch[1]}`;
286332
+ const numericMatch = trimmed.match(/^(\d+)$/);
286333
+ return numericMatch ? `pin${numericMatch[1]}` : trimmed;
286334
+ };
286335
+ var normalizeShape = (shape, width, height) => {
286336
+ const normalized = typeof shape === "string" ? shape.toLowerCase() : "rect";
286337
+ if (normalized === "circle" || normalized === "ellipse")
286338
+ return "circle";
286339
+ if (normalized === "pill" || normalized === "oval" || normalized === "rotated_pill") {
286340
+ return "pill";
286341
+ }
286342
+ if (Math.abs(width - height) < 0.00001 && normalized === "round") {
286343
+ return "circle";
286344
+ }
286345
+ return "rect";
286346
+ };
286347
+ var getCornerRadius = (element, width, height) => {
286348
+ const radius = toNumber4(element.corner_radius ?? element.cornerRadius ?? element.rect_border_radius);
286349
+ return radius > 0 ? Math.min(radius, width / 2, height / 2) : undefined;
286350
+ };
286351
+ var getPlatedHolePadGeometry = (element) => {
286352
+ const elementShape = String(element.shape ?? "circle").toLowerCase();
286353
+ const hasRectPad = element.pad_shape === "rect" || elementShape.includes("with_rect_pad");
286354
+ const outerDiameter = toNumber4(element.outer_diameter ?? element.outerDiameter, 0.6);
286355
+ const width = hasRectPad ? toNumber4(element.rect_pad_width ?? element.outer_width ?? element.width, outerDiameter) : toNumber4(element.outer_width ?? element.width, outerDiameter);
286356
+ const height = hasRectPad ? toNumber4(element.rect_pad_height ?? element.outer_height ?? element.height, width) : toNumber4(element.outer_height ?? element.height, width);
286357
+ const shape = hasRectPad ? normalizeShape(element.pad_shape ?? "rect", width, height) : normalizeShape(elementShape, width, height);
286358
+ const rotation11 = hasRectPad ? toNumber4(element.rect_ccw_rotation ?? element.ccw_rotation ?? element.rotation) : toNumber4(element.ccw_rotation ?? element.rotation);
286359
+ return { height, rotation: rotation11, shape, width };
286360
+ };
286361
+ var getPlatedHoleGeometry = (element) => {
286362
+ const elementShape = String(element.shape ?? "circle").toLowerCase();
286363
+ const rawHoleShape = String(element.hole_shape ?? (elementShape === "circle" || elementShape === "oval" || elementShape === "pill" ? elementShape : "circle")).toLowerCase();
286364
+ const holeDiameter = toNumber4(element.hole_diameter);
286365
+ const width = toNumber4(element.hole_width, holeDiameter);
286366
+ const height = toNumber4(element.hole_height, width);
286367
+ if (width <= 0 || height <= 0)
286368
+ return;
286369
+ return {
286370
+ height,
286371
+ offsetX: toNumber4(element.hole_offset_x),
286372
+ offsetY: toNumber4(element.hole_offset_y),
286373
+ rotation: toNumber4(element.hole_ccw_rotation ?? (elementShape === "oval" || elementShape === "pill" ? element.ccw_rotation : undefined) ?? element.rotation),
286374
+ shape: normalizeShape(rawHoleShape, width, height),
286375
+ width
286376
+ };
286377
+ };
286378
+ var parsePolygonPoints = (value, elementName) => {
286379
+ if (!Array.isArray(value) || value.length < 3) {
286380
+ throw new Error(`${elementName} must contain at least three points`);
286381
+ }
286382
+ const points = value.map((rawPoint) => {
286383
+ if (!rawPoint || typeof rawPoint !== "object") {
286384
+ throw new Error(`${elementName} points must contain finite x/y values`);
286385
+ }
286386
+ const point9 = rawPoint;
286387
+ const x4 = toNumber4(point9.x, Number.NaN);
286388
+ const y4 = toNumber4(point9.y, Number.NaN);
286389
+ if (!Number.isFinite(x4) || !Number.isFinite(y4)) {
286390
+ throw new Error(`${elementName} points must contain finite x/y values`);
286391
+ }
286392
+ return { x: x4, y: y4 };
286393
+ });
286394
+ if (getPolygonArea(points) <= 0.000000000001) {
286395
+ throw new Error(`${elementName} must enclose a non-zero area`);
286396
+ }
286397
+ return points;
286398
+ };
286399
+ var getPolygonSmtPadGeometry = (element) => {
286400
+ const absolutePoints = parsePolygonPoints(element.points, "Polygon PCB SMT pads");
286401
+ const bounds = getBoundsFromPoints5(absolutePoints);
286402
+ if (!bounds) {
286403
+ throw new Error("Polygon PCB SMT pads must contain at least three points");
286404
+ }
286405
+ const { x: x4, y: y4 } = getBoundsCenter2(bounds);
286406
+ return {
286407
+ height: bounds.maxY - bounds.minY,
286408
+ points: absolutePoints.map((point9) => ({
286409
+ x: point9.x - x4,
286410
+ y: point9.y - y4
286411
+ })),
286412
+ width: bounds.maxX - bounds.minX,
286413
+ x: x4,
286414
+ y: y4
286415
+ };
286416
+ };
286417
+ var getSmtPadMetadata = (element, index) => ({
286418
+ id: String(element.pcb_smtpad_id ?? `pcb_smtpad_${index + 1}`),
286419
+ kind: "smt",
286420
+ layer: String(element.layer ?? "top"),
286421
+ portHints: Array.isArray(element.port_hints) ? element.port_hints.map((hint) => normalizePortHint(String(hint))) : []
286422
+ });
286423
+ var parseSmtPad = (element, index) => {
286424
+ const metadata = getSmtPadMetadata(element, index);
286425
+ if (element.shape === "polygon") {
286426
+ return {
286427
+ ...metadata,
286428
+ ...getPolygonSmtPadGeometry(element),
286429
+ rotation: 0,
286430
+ shape: "polygon"
286431
+ };
286432
+ }
286433
+ const diameter = toNumber4(element.radius) * 2;
286434
+ const width = toNumber4(element.width, diameter);
286435
+ const height = toNumber4(element.height, width);
286436
+ return {
286437
+ ...metadata,
286438
+ cornerRadius: getCornerRadius(element, width, height),
286439
+ height,
286440
+ rotation: toNumber4(element.ccw_rotation ?? element.rotation),
286441
+ shape: normalizeShape(element.shape, width, height),
286442
+ width,
286443
+ x: toNumber4(element.x),
286444
+ y: toNumber4(element.y)
286445
+ };
286446
+ };
286447
+ var circuitJsonToPreview = (circuitJson, options = {}) => {
286448
+ const pads = circuitJson.flatMap((rawElement, index) => {
286449
+ const element = rawElement;
286450
+ if (element.type === "pcb_smtpad") {
286451
+ return [parseSmtPad(element, index)];
286452
+ }
286453
+ if (element.type === "pcb_plated_hole") {
286454
+ const { height, rotation: rotation11, shape, width } = getPlatedHolePadGeometry(element);
286455
+ return [
286456
+ {
286457
+ cornerRadius: getCornerRadius(element, width, height),
286458
+ height,
286459
+ hole: getPlatedHoleGeometry(element),
286460
+ id: String(element.pcb_plated_hole_id ?? `pcb_plated_hole_${index + 1}`),
286461
+ kind: "plated-hole",
286462
+ layer: Array.isArray(element.layers) ? String(element.layers[0] ?? "top") : "top",
286463
+ portHints: Array.isArray(element.port_hints) ? element.port_hints.map((hint) => normalizePortHint(String(hint))) : [],
286464
+ rotation: rotation11,
286465
+ shape,
286466
+ width,
286467
+ x: toNumber4(element.x),
286468
+ y: toNumber4(element.y)
286469
+ }
286470
+ ];
286471
+ }
286472
+ return [];
286473
+ });
286474
+ if (pads.length === 0) {
286475
+ throw new Error("Circuit JSON must contain at least one PCB SMT pad or plated hole");
286476
+ }
286477
+ return {
286478
+ pads,
286479
+ sourceHints: options.sourceHints,
286480
+ subtitle: options.subtitle ?? "Circuit JSON footprint",
286481
+ title: options.title ?? "Circuit JSON"
286331
286482
  };
286332
286483
  };
286484
+ var footprinterStringToPreview = (footprinterString) => {
286485
+ const normalized = footprinterString.trim();
286486
+ if (!normalized)
286487
+ throw new Error("Footprinter string is required");
286488
+ return circuitJsonToPreview(fp3.string(normalized).circuitJson(), {
286489
+ subtitle: "Generated by @tscircuit/footprinter",
286490
+ title: normalized
286491
+ });
286492
+ };
286333
286493
  var SEARCH_GRID_SIZE = 112;
286334
286494
  var MAX_OPTIMIZED_SEEDS = 10;
286335
286495
  var OPTIMIZATION_STEPS = 16;
@@ -286345,6 +286505,34 @@ var NUMERIC_PARAMETERS = [
286345
286505
  "od",
286346
286506
  "id"
286347
286507
  ];
286508
+ var FOOTPRINT_ROTATIONS = [0, 90, 180, 270];
286509
+ var PIN1_LOCATIONS = [
286510
+ ["leftside", "top"],
286511
+ ["leftside", "bottom"],
286512
+ ["rightside", "top"],
286513
+ ["rightside", "bottom"],
286514
+ ["topside", "left"],
286515
+ ["topside", "right"],
286516
+ ["bottomside", "left"],
286517
+ ["bottomside", "right"]
286518
+ ];
286519
+ var getOrientedHeuristics = (seed, analysis) => {
286520
+ const heuristics = seed.family === "lga" ? {
286521
+ ...analysis.heuristics,
286522
+ h: analysis.bounds.height,
286523
+ pl: analysis.lgaPadLength,
286524
+ pw: analysis.lgaPadWidth,
286525
+ w: analysis.bounds.width
286526
+ } : analysis.heuristics;
286527
+ if (seed.searchRotation !== 90 && seed.searchRotation !== 270) {
286528
+ return heuristics;
286529
+ }
286530
+ return {
286531
+ ...heuristics,
286532
+ h: heuristics.w,
286533
+ w: heuristics.h
286534
+ };
286535
+ };
286348
286536
  var clamp5 = (value, min, max2) => Math.min(Math.max(value, min), max2);
286349
286537
  var median = (values) => {
286350
286538
  if (!values.length)
@@ -286353,36 +286541,8 @@ var median = (values) => {
286353
286541
  const middle = Math.floor(sorted.length / 2);
286354
286542
  return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
286355
286543
  };
286356
- var getPadBounds22 = (pad2) => {
286357
- const radians = pad2.rotation * Math.PI / 180;
286358
- const cosine = Math.abs(Math.cos(radians));
286359
- const sine = Math.abs(Math.sin(radians));
286360
- const width = pad2.width * cosine + pad2.height * sine;
286361
- const height = pad2.width * sine + pad2.height * cosine;
286362
- return {
286363
- height,
286364
- maxX: pad2.x + width / 2,
286365
- maxY: pad2.y + height / 2,
286366
- minX: pad2.x - width / 2,
286367
- minY: pad2.y - height / 2,
286368
- width
286369
- };
286370
- };
286371
- var getBounds3 = (pads) => {
286372
- const padBounds = pads.map(getPadBounds22);
286373
- const minX = Math.min(...padBounds.map((bound) => bound.minX));
286374
- const minY = Math.min(...padBounds.map((bound) => bound.minY));
286375
- const maxX = Math.max(...padBounds.map((bound) => bound.maxX));
286376
- const maxY = Math.max(...padBounds.map((bound) => bound.maxY));
286377
- return {
286378
- height: maxY - minY,
286379
- maxX,
286380
- maxY,
286381
- minX,
286382
- minY,
286383
- width: maxX - minX
286384
- };
286385
- };
286544
+ var getPadBounds3 = (pad2) => getFootprintBounds([pad2]);
286545
+ var getBounds3 = (pads) => getFootprintBounds(pads);
286386
286546
  var clusterCoordinates = (values, tolerance) => {
286387
286547
  const sorted = [...values].sort((left, right) => left - right);
286388
286548
  const clusters = [];
@@ -286412,39 +286572,59 @@ var getPitchEstimate = (pads, tolerance) => {
286412
286572
  }
286413
286573
  if (!differences.length)
286414
286574
  return Math.max(tolerance * 2, 1);
286415
- return median(differences);
286575
+ const sortedDifferences = differences.toSorted((left, right) => left - right);
286576
+ return sortedDifferences[Math.floor((sortedDifferences.length - 1) * 0.25)];
286416
286577
  };
286417
286578
  var analyzeTarget = (target) => {
286418
286579
  const bounds = getBounds3(target.pads);
286419
- const padBounds = target.pads.map(getPadBounds22);
286580
+ const padBounds = target.pads.map(getPadBounds3);
286420
286581
  const medianPadWidth = median(padBounds.map((bound) => bound.width));
286421
286582
  const medianPadHeight = median(padBounds.map((bound) => bound.height));
286422
286583
  const tolerance = Math.max(Math.min(medianPadWidth, medianPadHeight) * 0.22, 0.015);
286423
- const xCoordinates = clusterCoordinates(target.pads.map((pad2) => pad2.x), tolerance);
286424
- const yCoordinates = clusterCoordinates(target.pads.map((pad2) => pad2.y), tolerance);
286425
286584
  const centerX = (bounds.minX + bounds.maxX) / 2;
286426
286585
  const centerY = (bounds.minY + bounds.maxY) / 2;
286427
- const edgeToleranceX = Math.max(medianPadWidth * 0.75, bounds.width * 0.08);
286428
- const edgeToleranceY = Math.max(medianPadHeight * 0.75, bounds.height * 0.08);
286429
- const sidePads = target.pads.filter((pad2) => Math.abs(pad2.x - (bounds.minX + medianPadWidth / 2)) <= edgeToleranceX || Math.abs(pad2.x - (bounds.maxX - medianPadWidth / 2)) <= edgeToleranceX || Math.abs(pad2.y - (bounds.minY + medianPadHeight / 2)) <= edgeToleranceY || Math.abs(pad2.y - (bounds.maxY - medianPadHeight / 2)) <= edgeToleranceY);
286430
286586
  const medianPadArea = median(padBounds.map((bound) => bound.width * bound.height));
286431
- const thermalPad = target.pads.map((pad2) => ({ bound: getPadBounds22(pad2), pad: pad2 })).filter(({ bound, pad: pad2 }) => Math.abs(pad2.x - centerX) <= medianPadWidth && Math.abs(pad2.y - centerY) <= medianPadHeight && bound.width * bound.height > medianPadArea * 2.5).sort((left, right) => right.bound.width * right.bound.height - left.bound.width * left.bound.height)[0]?.bound;
286432
- const gridOccupancy = target.pads.length / Math.max(xCoordinates.length * yCoordinates.length, 1);
286433
- const hasPadsOnFourSides = sidePads.filter((pad2) => Math.abs(pad2.x - centerX) > bounds.width * 0.3).length >= 4 && sidePads.filter((pad2) => Math.abs(pad2.y - centerY) > bounds.height * 0.3).length >= 4;
286587
+ const thermalPadEntry = target.pads.map((pad2) => ({ bound: getPadBounds3(pad2), pad: pad2 })).filter(({ bound, pad: pad2 }) => Math.abs(pad2.x - centerX) <= medianPadWidth && Math.abs(pad2.y - centerY) <= medianPadHeight && bound.width * bound.height > medianPadArea * 2.5).sort((left, right) => right.bound.width * right.bound.height - left.bound.width * left.bound.height)[0];
286588
+ const topologyPads = thermalPadEntry ? target.pads.filter((pad2) => pad2 !== thermalPadEntry.pad) : target.pads;
286589
+ const topologyBounds = getBounds3(topologyPads);
286590
+ const topologyCenterX = (topologyBounds.minX + topologyBounds.maxX) / 2;
286591
+ const topologyCenterY = (topologyBounds.minY + topologyBounds.maxY) / 2;
286592
+ const xCoordinates = clusterCoordinates(topologyPads.map((pad2) => pad2.x), tolerance);
286593
+ const yCoordinates = clusterCoordinates(topologyPads.map((pad2) => pad2.y), tolerance);
286594
+ const edgeToleranceX = Math.max(medianPadWidth * 0.75, topologyBounds.width * 0.08);
286595
+ const edgeToleranceY = Math.max(medianPadHeight * 0.75, topologyBounds.height * 0.08);
286596
+ const sidePads = topologyPads.filter((pad2) => Math.abs(pad2.x - (topologyBounds.minX + medianPadWidth / 2)) <= edgeToleranceX || Math.abs(pad2.x - (topologyBounds.maxX - medianPadWidth / 2)) <= edgeToleranceX || Math.abs(pad2.y - (topologyBounds.minY + medianPadHeight / 2)) <= edgeToleranceY || Math.abs(pad2.y - (topologyBounds.maxY - medianPadHeight / 2)) <= edgeToleranceY);
286597
+ const minPadCenterX = Math.min(...topologyPads.map((pad2) => pad2.x));
286598
+ const maxPadCenterX = Math.max(...topologyPads.map((pad2) => pad2.x));
286599
+ const minPadCenterY = Math.min(...topologyPads.map((pad2) => pad2.y));
286600
+ const maxPadCenterY = Math.max(...topologyPads.map((pad2) => pad2.y));
286601
+ const topologyPadEntries = topologyPads.map((pad2) => ({
286602
+ bounds: getPadBounds3(pad2),
286603
+ pad: pad2
286604
+ }));
286605
+ const leftRightEdgePads = topologyPadEntries.filter(({ pad: pad2 }) => Math.abs(pad2.x - minPadCenterX) <= tolerance || Math.abs(pad2.x - maxPadCenterX) <= tolerance);
286606
+ const topBottomEdgePads = topologyPadEntries.filter(({ pad: pad2 }) => Math.abs(pad2.y - minPadCenterY) <= tolerance || Math.abs(pad2.y - maxPadCenterY) <= tolerance);
286607
+ const leftSidePadCount = topologyPadEntries.filter(({ bounds: bound, pad: pad2 }) => Math.abs(pad2.x - minPadCenterX) <= edgeToleranceX && bound.width >= bound.height * 0.95).length;
286608
+ const rightSidePadCount = topologyPadEntries.filter(({ bounds: bound, pad: pad2 }) => Math.abs(pad2.x - maxPadCenterX) <= edgeToleranceX && bound.width >= bound.height * 0.95).length;
286609
+ const bottomSidePadCount = topologyPadEntries.filter(({ bounds: bound, pad: pad2 }) => Math.abs(pad2.y - minPadCenterY) <= edgeToleranceY && bound.height >= bound.width * 0.95).length;
286610
+ const topSidePadCount = topologyPadEntries.filter(({ bounds: bound, pad: pad2 }) => Math.abs(pad2.y - maxPadCenterY) <= edgeToleranceY && bound.height >= bound.width * 0.95).length;
286611
+ const gridOccupancy = topologyPads.length / Math.max(xCoordinates.length * yCoordinates.length, 1);
286612
+ const hasPadsOnFourSides = sidePads.filter((pad2) => Math.abs(pad2.x - topologyCenterX) > topologyBounds.width * 0.3).length >= 4 && sidePads.filter((pad2) => Math.abs(pad2.y - topologyCenterY) > topologyBounds.height * 0.3).length >= 4;
286434
286613
  let topology = "irregular";
286435
286614
  if (xCoordinates.length === 1 || yCoordinates.length === 1) {
286436
286615
  topology = "linear";
286437
286616
  } else if (xCoordinates.length <= 2 || yCoordinates.length <= 2) {
286438
286617
  topology = "two-sided";
286439
- } else if (target.pads.length >= 4 && xCoordinates.length >= 2 && yCoordinates.length >= 2 && gridOccupancy >= 0.68) {
286618
+ } else if (topologyPads.length >= 4 && xCoordinates.length >= 2 && yCoordinates.length >= 2 && gridOccupancy >= 0.68) {
286440
286619
  topology = "grid";
286441
- } else if (hasPadsOnFourSides && target.pads.length >= 8) {
286620
+ } else if (hasPadsOnFourSides && topologyPads.length >= 8) {
286442
286621
  topology = "four-sided";
286443
286622
  }
286444
286623
  const pitch = getPitchEstimate(target.pads, tolerance);
286445
286624
  const medianPadLongSide = median(padBounds.map((bound) => Math.max(bound.width, bound.height)));
286446
286625
  const medianPadShortSide = median(padBounds.map((bound) => Math.min(bound.width, bound.height)));
286447
286626
  const medianPadDiameter = median(padBounds.map((bound) => Math.sqrt(bound.width * bound.height)));
286627
+ const medianHoleDiameter = median(target.pads.flatMap((pad2) => pad2.hole ? [Math.sqrt(pad2.hole.width * pad2.hole.height)] : []));
286448
286628
  const platedHoleCount = target.pads.filter((pad2) => pad2.kind === "plated-hole").length;
286449
286629
  const insetQuadAdjustment = topology === "four-sided" ? 0.2 : 0;
286450
286630
  return {
@@ -286454,7 +286634,7 @@ var analyzeTarget = (target) => {
286454
286634
  heuristics: {
286455
286635
  ball: medianPadDiameter,
286456
286636
  h: bounds.height + insetQuadAdjustment,
286457
- id: medianPadDiameter * 0.6,
286637
+ id: medianHoleDiameter || medianPadDiameter * 0.6,
286458
286638
  od: medianPadDiameter,
286459
286639
  p: pitch,
286460
286640
  pad: medianPadDiameter,
@@ -286463,10 +286643,17 @@ var analyzeTarget = (target) => {
286463
286643
  pw: medianPadShortSide,
286464
286644
  w: bounds.width + insetQuadAdjustment
286465
286645
  },
286646
+ horizontalSidePadCount: Math.max(leftSidePadCount, rightSidePadCount),
286647
+ lgaPadLength: leftRightEdgePads.length ? median(leftRightEdgePads.map(({ bounds: bound }) => bound.width)) : median(topBottomEdgePads.map(({ bounds: bound }) => bound.height)),
286648
+ lgaPadWidth: leftRightEdgePads.length ? median(leftRightEdgePads.map(({ bounds: bound }) => bound.height)) : median(topBottomEdgePads.map(({ bounds: bound }) => bound.width)),
286466
286649
  perimeterPadCount: sidePads.length,
286467
286650
  platedHoleCount,
286468
- thermalPad: thermalPad ? { height: thermalPad.height, width: thermalPad.width } : undefined,
286469
- topology
286651
+ thermalPad: thermalPadEntry ? {
286652
+ height: thermalPadEntry.bound.height,
286653
+ width: thermalPadEntry.bound.width
286654
+ } : undefined,
286655
+ topology,
286656
+ verticalSidePadCount: Math.max(bottomSidePadCount, topSidePadCount)
286470
286657
  };
286471
286658
  };
286472
286659
  var normalizePads = (pads) => {
@@ -286479,8 +286666,28 @@ var normalizePads = (pads) => {
286479
286666
  y: pad2.y - centerY
286480
286667
  }));
286481
286668
  };
286669
+ var rotateFootprint = (footprint, rotation11) => {
286670
+ if (rotation11 === 0)
286671
+ return footprint;
286672
+ const radians = rotation11 * Math.PI / 180;
286673
+ return {
286674
+ ...footprint,
286675
+ pads: footprint.pads.map((pad2) => ({
286676
+ ...pad2,
286677
+ hole: pad2.hole ? {
286678
+ ...pad2.hole,
286679
+ offsetX: pad2.hole.offsetX * Math.cos(radians) - pad2.hole.offsetY * Math.sin(radians),
286680
+ offsetY: pad2.hole.offsetX * Math.sin(radians) + pad2.hole.offsetY * Math.cos(radians),
286681
+ rotation: (pad2.hole.rotation + rotation11) % 360
286682
+ } : undefined,
286683
+ rotation: (pad2.rotation + rotation11) % 360,
286684
+ x: pad2.x * Math.cos(radians) - pad2.y * Math.sin(radians),
286685
+ y: pad2.x * Math.sin(radians) + pad2.y * Math.cos(radians)
286686
+ }))
286687
+ };
286688
+ };
286482
286689
  var getOrientedPadSize = (pad2) => {
286483
- const bounds = getPadBounds22({ ...pad2, x: 0, y: 0 });
286690
+ const bounds = getPadBounds3({ ...pad2, x: 0, y: 0 });
286484
286691
  return { height: bounds.height, width: bounds.width };
286485
286692
  };
286486
286693
  var matchPadsByPosition = (left, right) => {
@@ -286521,6 +286728,22 @@ var getGeometryLoss = (candidate, target) => {
286521
286728
  loss += 4;
286522
286729
  if (candidatePad.shape !== targetPad.shape)
286523
286730
  loss += 0.08;
286731
+ if (Boolean(candidatePad.hole) !== Boolean(targetPad.hole)) {
286732
+ loss += 4;
286733
+ } else if (candidatePad.hole && targetPad.hole) {
286734
+ const holeWidthScale = Math.max(targetPad.hole.width, 0.05);
286735
+ const holeHeightScale = Math.max(targetPad.hole.height, 0.05);
286736
+ const holeWidthDifference = (candidatePad.hole.width - targetPad.hole.width) / holeWidthScale;
286737
+ const holeHeightDifference = (candidatePad.hole.height - targetPad.hole.height) / holeHeightScale;
286738
+ const holeOffsetXDifference = (candidatePad.hole.offsetX - targetPad.hole.offsetX) / positionScale;
286739
+ const holeOffsetYDifference = (candidatePad.hole.offsetY - targetPad.hole.offsetY) / positionScale;
286740
+ loss += holeWidthDifference * holeWidthDifference + holeHeightDifference * holeHeightDifference + holeOffsetXDifference * holeOffsetXDifference * 4 + holeOffsetYDifference * holeOffsetYDifference * 4;
286741
+ if (candidatePad.hole.shape !== targetPad.hole.shape)
286742
+ loss += 0.08;
286743
+ }
286744
+ if (targetPad.portHints.length > 0 && !candidatePad.portHints.some((hint) => targetPad.portHints.includes(hint))) {
286745
+ loss += 0.04;
286746
+ }
286524
286747
  }
286525
286748
  return loss / pairs4.length;
286526
286749
  };
@@ -286530,11 +286753,13 @@ var getDomainScore = (target, family) => {
286530
286753
  const aliases2 = {
286531
286754
  cap: ["capacitor", "cap"],
286532
286755
  dfn: ["dfn"],
286756
+ lga: ["lga"],
286533
286757
  qfn: ["qfn"],
286534
286758
  res: ["resistor", "res"],
286535
286759
  soic: ["soic", "so-"],
286536
286760
  ssop: ["ssop"],
286537
- tssop: ["tssop"]
286761
+ tssop: ["tssop"],
286762
+ usbcmidmount: ["usb-c", "usb c", "type-c", "type c", "usbc"]
286538
286763
  };
286539
286764
  const terms = aliases2[family] ?? [family];
286540
286765
  return terms.some((term) => description.includes(term)) ? 1 : 0;
@@ -286559,7 +286784,71 @@ var buildParameterizedString = (seed, parameters) => {
286559
286784
  }).join("_");
286560
286785
  return suffix ? `${seed}_${suffix}` : seed;
286561
286786
  };
286562
- var geometrySignature = (preview) => preview.pads.map((pad2) => [pad2.kind, pad2.shape, pad2.x, pad2.y, pad2.width, pad2.height, pad2.rotation].map(String).join(":")).join("|");
286787
+ var geometrySignature = (preview) => preview.pads.map((pad2) => {
286788
+ const holeSignature = pad2.hole ? [
286789
+ pad2.hole.shape,
286790
+ pad2.hole.offsetX,
286791
+ pad2.hole.offsetY,
286792
+ pad2.hole.width,
286793
+ pad2.hole.height,
286794
+ pad2.hole.rotation
286795
+ ].join(":") : "no-hole";
286796
+ const pointSignature = pad2.shape === "polygon" ? pad2.points?.map((point9) => `${point9.x}:${point9.y}`).join(",") : "no-points";
286797
+ return [
286798
+ pad2.kind,
286799
+ pad2.shape,
286800
+ pad2.x,
286801
+ pad2.y,
286802
+ pad2.width,
286803
+ pad2.height,
286804
+ pad2.rotation,
286805
+ pointSignature,
286806
+ holeSignature
286807
+ ].map(String).join(":");
286808
+ }).join("|");
286809
+ var padShapeSignature = (preview) => preview.pads.map((pad2) => `${pad2.kind}:${pad2.shape}`).toSorted().join("|");
286810
+ var areClose = (left, right) => Math.abs(left - right) <= 0.00001;
286811
+ var areSamePoint = (left, right) => areClose(left.x, right.x) && areClose(left.y, right.y);
286812
+ var haveSamePolygon = (left, right) => {
286813
+ if (left.shape !== "polygon" && right.shape !== "polygon")
286814
+ return true;
286815
+ if (left.shape !== "polygon" || right.shape !== "polygon")
286816
+ return false;
286817
+ const leftPoints = getPolygonWorldPoints(left);
286818
+ const rightPoints = getPolygonWorldPoints(right);
286819
+ if (leftPoints.length !== rightPoints.length)
286820
+ return false;
286821
+ const matchesFrom = (startIndex, direction2) => leftPoints.every((leftPoint, index) => {
286822
+ const rightIndex = (startIndex + direction2 * index + rightPoints.length) % rightPoints.length;
286823
+ return areSamePoint(leftPoint, rightPoints[rightIndex]);
286824
+ });
286825
+ return rightPoints.some((rightPoint, startIndex) => areSamePoint(leftPoints[0], rightPoint) && (matchesFrom(startIndex, 1) || matchesFrom(startIndex, -1)));
286826
+ };
286827
+ var haveSameOrientedPads = (left, right) => {
286828
+ if (left.pads.length !== right.pads.length)
286829
+ return false;
286830
+ return left.pads.every((leftPad, index) => {
286831
+ const rightPad = right.pads[index];
286832
+ if (!rightPad)
286833
+ return false;
286834
+ const leftSize = getOrientedPadSize(leftPad);
286835
+ const rightSize = getOrientedPadSize(rightPad);
286836
+ const holesMatch = !leftPad.hole && !rightPad.hole ? true : Boolean(leftPad.hole && rightPad.hole) && leftPad.hole?.shape === rightPad.hole?.shape && areClose(leftPad.hole?.offsetX ?? 0, rightPad.hole?.offsetX ?? 0) && areClose(leftPad.hole?.offsetY ?? 0, rightPad.hole?.offsetY ?? 0) && areClose(leftPad.hole?.width ?? 0, rightPad.hole?.width ?? 0) && areClose(leftPad.hole?.height ?? 0, rightPad.hole?.height ?? 0) && areClose(leftPad.hole?.rotation ?? 0, rightPad.hole?.rotation ?? 0);
286837
+ return leftPad.kind === rightPad.kind && leftPad.shape === rightPad.shape && leftPad.portHints.join("|") === rightPad.portHints.join("|") && areClose(leftPad.x, rightPad.x) && areClose(leftPad.y, rightPad.y) && areClose(leftSize.width, rightSize.width) && areClose(leftSize.height, rightSize.height) && haveSamePolygon(leftPad, rightPad) && holesMatch;
286838
+ });
286839
+ };
286840
+ var encodeOrientationInFootprinterString = (footprinterString, searchRotation, orientedPreview) => {
286841
+ if (searchRotation === 0)
286842
+ return footprinterString;
286843
+ for (const [side, alignment] of PIN1_LOCATIONS) {
286844
+ const orientedString = `${footprinterString}_pin1location(${side},${alignment})`;
286845
+ const preview = tryBuild(orientedString);
286846
+ if (preview && haveSameOrientedPads(preview, orientedPreview)) {
286847
+ return orientedString;
286848
+ }
286849
+ }
286850
+ return null;
286851
+ };
286563
286852
  var getPreferredFamilies = (analysis) => {
286564
286853
  if (analysis.platedHoleCount > analysis.perimeterPadCount / 2) {
286565
286854
  return /* @__PURE__ */ new Set([
@@ -286569,16 +286858,18 @@ var getPreferredFamilies = (analysis) => {
286569
286858
  "pinrow",
286570
286859
  "radial",
286571
286860
  "to220",
286572
- "to92"
286861
+ "to92",
286862
+ "usbcmidmount"
286573
286863
  ]);
286574
286864
  }
286575
286865
  if (analysis.topology === "grid")
286576
286866
  return /* @__PURE__ */ new Set(["bga"]);
286577
286867
  if (analysis.topology === "four-sided") {
286578
- return /* @__PURE__ */ new Set(["lqfp", "mlp", "qfn", "qfp", "quad", "tqfp"]);
286868
+ return /* @__PURE__ */ new Set(["lga", "lqfp", "mlp", "qfn", "qfp", "quad", "tqfp"]);
286579
286869
  }
286580
286870
  if (analysis.topology === "two-sided") {
286581
286871
  return /* @__PURE__ */ new Set([
286872
+ "lga",
286582
286873
  "soic",
286583
286874
  "tssop",
286584
286875
  "ssop",
@@ -286599,20 +286890,43 @@ var generateSeeds = (target, analysis) => {
286599
286890
  const seeds = /* @__PURE__ */ new Set;
286600
286891
  for (const family of getFootprintNames2()) {
286601
286892
  seeds.add(`${family}${padCount}`);
286602
- seeds.add(family);
286893
+ if (family !== "usbcmidmount")
286894
+ seeds.add(family);
286603
286895
  }
286604
286896
  if (analysis.thermalPad && analysis.perimeterPadCount > 0) {
286605
- const thermalPadDimensions = `${formatMillimeters(analysis.thermalPad.width)}x${formatMillimeters(analysis.thermalPad.height)}`;
286606
- for (const family of ["mlp", "qfn", "quad"]) {
286897
+ const thermalPadDimensionOptions = /* @__PURE__ */ new Set([
286898
+ `${formatMillimeters(analysis.thermalPad.width)}x${formatMillimeters(analysis.thermalPad.height)}`,
286899
+ `${formatMillimeters(analysis.thermalPad.height)}x${formatMillimeters(analysis.thermalPad.width)}`
286900
+ ]);
286901
+ const thermalPadFamilies = analysis.topology === "two-sided" ? ["dfn", "msop", "soic", "ssop", "tssop", "vssop"] : ["mlp", "qfn", "quad"];
286902
+ for (const family of thermalPadFamilies) {
286607
286903
  seeds.add(`${family}${analysis.perimeterPadCount}_thermalpad`);
286608
- seeds.add(`${family}${analysis.perimeterPadCount}_thermalpad${thermalPadDimensions}`);
286904
+ for (const thermalPadDimensions of thermalPadDimensionOptions) {
286905
+ seeds.add(`${family}${analysis.perimeterPadCount}_thermalpad${thermalPadDimensions}`);
286906
+ }
286609
286907
  }
286610
286908
  }
286611
286909
  if (analysis.topology === "grid") {
286612
286910
  seeds.add(`bga${padCount}_grid${analysis.gridColumns}x${analysis.gridRows}`);
286613
286911
  }
286912
+ if (analysis.topology === "four-sided" && 2 * (analysis.horizontalSidePadCount + analysis.verticalSidePadCount) === analysis.perimeterPadCount) {
286913
+ seeds.add(`lga${analysis.perimeterPadCount}_grid${analysis.horizontalSidePadCount}x${analysis.verticalSidePadCount}`);
286914
+ }
286915
+ const hasLgaHint = target.sourceHints?.some((hint) => hint.toLowerCase().includes("lga"));
286916
+ if (analysis.topology === "two-sided" && hasLgaHint) {
286917
+ const padsPerSide = analysis.perimeterPadCount / 2;
286918
+ if (Number.isInteger(padsPerSide)) {
286919
+ const grid = analysis.gridColumns <= 2 ? `${padsPerSide}x0` : `0x${padsPerSide}`;
286920
+ seeds.add(`lga${analysis.perimeterPadCount}_grid${grid}`);
286921
+ }
286922
+ }
286923
+ if (analysis.topology === "two-sided" && padCount % 2 === 1) {
286924
+ for (let missingPosition = 1;missingPosition <= padCount + 1; missingPosition += 1) {
286925
+ seeds.add(`dfn${padCount + 1}_missing(${missingPosition})`);
286926
+ }
286927
+ }
286614
286928
  if (padCount === 2 && analysis.platedHoleCount === 0) {
286615
- const padBounds = target.pads.map(getPadBounds22);
286929
+ const padBounds = target.pads.map(getPadBounds3);
286616
286930
  const passiveDimensions = `p${formatMillimeters(analysis.heuristics.p)}_pw${formatMillimeters(median(padBounds.map((bound) => bound.width)))}_ph${formatMillimeters(median(padBounds.map((bound) => bound.height)))}`;
286617
286931
  seeds.add(`res_${passiveDimensions}`);
286618
286932
  seeds.add(`cap_${passiveDimensions}`);
@@ -286622,18 +286936,49 @@ var generateSeeds = (target, analysis) => {
286622
286936
  seeds.add(`res${size.imperial}`);
286623
286937
  }
286624
286938
  }
286939
+ if (target.pads.some((pad2) => pad2.shape === "pill")) {
286940
+ for (const seed of [...seeds]) {
286941
+ const pillPadSeed = `${seed}_pillpads`;
286942
+ const preview = tryBuild(pillPadSeed);
286943
+ if (preview?.pads.length === padCount && preview.pads.some((pad2) => pad2.shape === "pill")) {
286944
+ seeds.add(pillPadSeed);
286945
+ }
286946
+ }
286947
+ }
286625
286948
  return [...seeds];
286626
286949
  };
286627
- var selectSeedsToOptimize = (candidates, analysis) => {
286950
+ var selectSeedsToOptimize = (candidates, analysis, target) => {
286628
286951
  const selected = /* @__PURE__ */ new Map;
286952
+ const targetPadShapeSignature = padShapeSignature(target);
286953
+ const selectedShapeFamilies = /* @__PURE__ */ new Set;
286954
+ if (analysis.topology === "four-sided" || analysis.topology === "two-sided") {
286955
+ const lgaGridCandidate = candidates.find((candidate) => candidate.family === "lga" && candidate.footprinterString.includes("_grid") && candidate.searchRotation === 0);
286956
+ if (lgaGridCandidate) {
286957
+ selected.set(`${lgaGridCandidate.footprinterString}:${lgaGridCandidate.searchRotation}`, lgaGridCandidate);
286958
+ }
286959
+ }
286960
+ if (target.pads.some((pad2) => pad2.shape === "pill")) {
286961
+ for (const candidate of candidates) {
286962
+ if (padShapeSignature(candidate.preview) !== targetPadShapeSignature) {
286963
+ continue;
286964
+ }
286965
+ if (selectedShapeFamilies.has(candidate.family))
286966
+ continue;
286967
+ selectedShapeFamilies.add(candidate.family);
286968
+ selected.set(`${candidate.footprinterString}:${candidate.searchRotation}`, candidate);
286969
+ if (selected.size >= 4)
286970
+ break;
286971
+ }
286972
+ }
286629
286973
  for (const candidate of candidates.slice(0, 4)) {
286630
- selected.set(candidate.footprinterString, candidate);
286974
+ selected.set(`${candidate.footprinterString}:${candidate.searchRotation}`, candidate);
286631
286975
  }
286632
286976
  const preferredFamilies = getPreferredFamilies(analysis);
286633
286977
  for (const family of preferredFamilies) {
286634
- const candidate = candidates.find((entry) => entry.family === family && entry.footprinterString === family) ?? candidates.find((entry) => entry.family === family);
286635
- if (candidate)
286636
- selected.set(candidate.footprinterString, candidate);
286978
+ const candidate = (analysis.thermalPad ? candidates.find((entry) => entry.family === family && entry.footprinterString.includes("_thermalpad")) : undefined) ?? (family === "dfn" ? candidates.find((entry) => entry.family === family && entry.footprinterString.includes("_missing(")) : undefined) ?? (family === "lga" ? candidates.find((entry) => entry.family === family && entry.footprinterString.includes("_grid")) : undefined) ?? candidates.find((entry) => entry.family === family && entry.footprinterString === family) ?? candidates.find((entry) => entry.family === family);
286979
+ if (candidate) {
286980
+ selected.set(`${candidate.footprinterString}:${candidate.searchRotation}`, candidate);
286981
+ }
286637
286982
  if (selected.size >= MAX_OPTIMIZED_SEEDS)
286638
286983
  break;
286639
286984
  }
@@ -286642,12 +286987,14 @@ var selectSeedsToOptimize = (candidates, analysis) => {
286642
286987
  var findActiveParameters = (seed, analysis) => {
286643
286988
  const active = [];
286644
286989
  const baseSignature = geometrySignature(seed.preview);
286990
+ const heuristics = getOrientedHeuristics(seed, analysis);
286645
286991
  for (const parameter of NUMERIC_PARAMETERS) {
286646
- const heuristic = Math.max(analysis.heuristics[parameter], 0.05);
286992
+ const heuristic = Math.max(heuristics[parameter], 0.05);
286647
286993
  const preview = tryBuild(buildParameterizedString(seed.footprinterString, {
286648
286994
  [parameter]: heuristic
286649
286995
  }));
286650
- if (preview && preview.pads.length === seed.preview.pads.length && geometrySignature(preview) !== baseSignature) {
286996
+ const orientedPreview = preview ? rotateFootprint(preview, seed.searchRotation) : null;
286997
+ if (orientedPreview && orientedPreview.pads.length === seed.preview.pads.length && geometrySignature(orientedPreview) !== baseSignature) {
286651
286998
  active.push(parameter);
286652
286999
  }
286653
287000
  }
@@ -286662,13 +287009,17 @@ var optimizeSeed = (seed, target, analysis) => {
286662
287009
  };
286663
287010
  }
286664
287011
  const values = {};
287012
+ const heuristics = getOrientedHeuristics(seed, analysis);
286665
287013
  for (const parameter of activeParameters) {
286666
- values[parameter] = Math.max(analysis.heuristics[parameter], 0.05);
287014
+ values[parameter] = Math.max(heuristics[parameter], 0.05);
286667
287015
  }
286668
287016
  const evaluate = (parameters) => {
286669
287017
  const footprinterString = buildParameterizedString(seed.footprinterString, parameters);
286670
- const preview = tryBuild(footprinterString);
286671
- if (!preview || preview.pads.length !== target.pads.length) {
287018
+ const unrotatedPreview = tryBuild(footprinterString);
287019
+ if (!unrotatedPreview)
287020
+ return null;
287021
+ const preview = rotateFootprint(unrotatedPreview, seed.searchRotation);
287022
+ if (preview.pads.length !== target.pads.length) {
286672
287023
  return null;
286673
287024
  }
286674
287025
  return {
@@ -286710,9 +287061,9 @@ var optimizeSeed = (seed, target, analysis) => {
286710
287061
  secondMoments[parameter] = secondMoment;
286711
287062
  const correctedFirstMoment = firstMoment / (1 - 0.9 ** iteration);
286712
287063
  const correctedSecondMoment = secondMoment / (1 - 0.999 ** iteration);
286713
- const stepScale = Math.max(analysis.heuristics[parameter] * 0.075, 0.025);
287064
+ const stepScale = Math.max(heuristics[parameter] * 0.075, 0.025);
286714
287065
  const currentValue = values[parameter] ?? 0.05;
286715
- const upperBound2 = Math.max(analysis.bounds.width, analysis.bounds.height, analysis.heuristics[parameter]) * 3;
287066
+ const upperBound2 = Math.max(analysis.bounds.width, analysis.bounds.height, heuristics[parameter]) * 3;
286716
287067
  values[parameter] = clamp5(currentValue - stepScale * correctedFirstMoment / (Math.sqrt(correctedSecondMoment) + 0.00000001), 0.025, Math.max(upperBound2, 0.1));
286717
287068
  }
286718
287069
  current2 = evaluate(values);
@@ -286734,17 +287085,20 @@ var optimizeSeed = (seed, target, analysis) => {
286734
287085
  const withoutParameter = { ...simplifiedParameters };
286735
287086
  delete withoutParameter[parameter];
286736
287087
  const simplifiedPreview2 = tryBuild(buildParameterizedString(seed.footprinterString, withoutParameter));
286737
- if (simplifiedPreview2 && geometrySignature(simplifiedPreview2) === bestSignature) {
287088
+ const orientedSimplifiedPreview = simplifiedPreview2 ? rotateFootprint(simplifiedPreview2, seed.searchRotation) : null;
287089
+ if (orientedSimplifiedPreview && geometrySignature(orientedSimplifiedPreview) === bestSignature) {
286738
287090
  delete simplifiedParameters[parameter];
286739
287091
  }
286740
287092
  }
286741
287093
  const simplifiedString = buildParameterizedString(seed.footprinterString, simplifiedParameters);
286742
- const simplifiedPreview = tryBuild(simplifiedString) ?? best.preview;
287094
+ const unrotatedSimplifiedPreview = tryBuild(simplifiedString);
287095
+ const simplifiedPreview = unrotatedSimplifiedPreview ? rotateFootprint(unrotatedSimplifiedPreview, seed.searchRotation) : best.preview;
286743
287096
  return {
286744
287097
  family: seed.family,
286745
287098
  footprinterString: simplifiedString,
286746
287099
  geometryScore: 1 / (1 + best.loss),
286747
287100
  optimizedParameters: simplifiedParameters,
287101
+ searchRotation: seed.searchRotation,
286748
287102
  preview: simplifiedPreview
286749
287103
  };
286750
287104
  };
@@ -286752,44 +287106,63 @@ var discoverFootprinterString = (target, maxCandidates = 5) => {
286752
287106
  const analysis = analyzeTarget(target);
286753
287107
  const rawSeeds = generateSeeds(target, analysis);
286754
287108
  const seedCandidates = rawSeeds.flatMap((footprinterString) => {
286755
- const preview = tryBuild(footprinterString);
286756
- if (!preview || preview.pads.length !== target.pads.length)
287109
+ const unrotatedPreview = tryBuild(footprinterString);
287110
+ if (!unrotatedPreview || unrotatedPreview.pads.length !== target.pads.length) {
286757
287111
  return [];
286758
- const platedHoleCount = preview.pads.filter((pad2) => pad2.kind === "plated-hole").length;
286759
- if (platedHoleCount !== analysis.platedHoleCount)
286760
- return [];
286761
- return [
286762
- {
286763
- family: getFamily(footprinterString),
286764
- footprinterString,
286765
- geometryScore: getGeometryScore(preview, target),
286766
- preview
286767
- }
286768
- ];
287112
+ }
287113
+ return FOOTPRINT_ROTATIONS.flatMap((searchRotation) => {
287114
+ const preview = rotateFootprint(unrotatedPreview, searchRotation);
287115
+ const platedHoleCount = preview.pads.filter((pad2) => pad2.kind === "plated-hole").length;
287116
+ if (platedHoleCount !== analysis.platedHoleCount)
287117
+ return [];
287118
+ return [
287119
+ {
287120
+ family: getFamily(footprinterString),
287121
+ footprinterString,
287122
+ geometryScore: getGeometryScore(preview, target),
287123
+ preview,
287124
+ searchRotation
287125
+ }
287126
+ ];
287127
+ });
286769
287128
  });
286770
- seedCandidates.sort((left, right) => right.geometryScore - left.geometryScore);
286771
- const selectedSeeds = selectSeedsToOptimize(seedCandidates, analysis);
287129
+ seedCandidates.sort((left, right) => right.geometryScore - left.geometryScore || left.searchRotation - right.searchRotation);
287130
+ const selectedSeeds = selectSeedsToOptimize(seedCandidates, analysis, target);
286772
287131
  const optimized = selectedSeeds.map((seed) => optimizeSeed(seed, target, analysis));
287132
+ const targetPadShapeSignature = padShapeSignature(target);
286773
287133
  const allCandidates = [...optimized, ...seedCandidates].map((candidate) => {
286774
- const copperIntersectionOverUnion = summarizeCopperComparison(candidate.preview, target, SEARCH_GRID_SIZE).copperIntersectionOverUnion;
287134
+ const { copperIntersectionOverUnion, holeIntersectionOverUnion } = summarizeCopperComparison(candidate.preview, target, SEARCH_GRID_SIZE);
286775
287135
  const domainScore = getDomainScore(target, candidate.family);
287136
+ const shapesMatch = padShapeSignature(candidate.preview) === targetPadShapeSignature;
286776
287137
  return {
286777
287138
  copperIntersectionOverUnion,
286778
287139
  domainScore,
286779
287140
  family: candidate.family,
286780
287141
  footprinterString: candidate.footprinterString,
286781
287142
  geometryScore: candidate.geometryScore,
287143
+ holeIntersectionOverUnion,
286782
287144
  optimizedParameters: "optimizedParameters" in candidate ? candidate.optimizedParameters : {},
286783
- rankingScore: copperIntersectionOverUnion + domainScore * 0.08
287145
+ preview: candidate.preview,
287146
+ rankingScore: copperIntersectionOverUnion + (holeIntersectionOverUnion - 1) * 0.12 + domainScore * (shapesMatch ? 0.08 : 0.01),
287147
+ searchRotation: candidate.searchRotation
286784
287148
  };
286785
- }).sort((left, right) => right.rankingScore - left.rankingScore || right.copperIntersectionOverUnion - left.copperIntersectionOverUnion || right.domainScore - left.domainScore || right.geometryScore - left.geometryScore || left.footprinterString.length - right.footprinterString.length);
287149
+ }).sort((left, right) => right.rankingScore - left.rankingScore || right.copperIntersectionOverUnion - left.copperIntersectionOverUnion || right.holeIntersectionOverUnion - left.holeIntersectionOverUnion || right.domainScore - left.domainScore || right.geometryScore - left.geometryScore || left.searchRotation - right.searchRotation || left.footprinterString.length - right.footprinterString.length);
286786
287150
  const uniqueCandidates = [];
286787
287151
  const seenStrings = /* @__PURE__ */ new Set;
286788
287152
  for (const candidate of allCandidates) {
286789
- if (seenStrings.has(candidate.footprinterString))
287153
+ const orientedString = encodeOrientationInFootprinterString(candidate.footprinterString, candidate.searchRotation, candidate.preview);
287154
+ if (!orientedString || seenStrings.has(orientedString))
286790
287155
  continue;
286791
- seenStrings.add(candidate.footprinterString);
286792
- uniqueCandidates.push(candidate);
287156
+ seenStrings.add(orientedString);
287157
+ const {
287158
+ preview: _preview,
287159
+ searchRotation: _searchRotation,
287160
+ ...publicData
287161
+ } = candidate;
287162
+ uniqueCandidates.push({
287163
+ ...publicData,
287164
+ footprinterString: orientedString
287165
+ });
286793
287166
  if (uniqueCandidates.length >= clamp5(maxCandidates, 1, 10))
286794
287167
  break;
286795
287168
  }
@@ -286810,6 +287183,9 @@ var circuitJsonToFootprinter = (circuitJson, options = {}) => {
286810
287183
  return discoverFootprinterString(target, options.maxCandidates);
286811
287184
  };
286812
287185
 
287186
+ // lib/shared/circuit-json-to-footprinter.ts
287187
+ var circuitJsonToFootprinter2 = circuitJsonToFootprinter;
287188
+
286813
287189
  // cli/convert/discover-footprinter-from-file.ts
286814
287190
  import fs51 from "node:fs/promises";
286815
287191
  import path56 from "node:path";
@@ -286853,7 +287229,7 @@ var discoverFootprinterFromFile = async ({
286853
287229
  }) => {
286854
287230
  const circuitJson = await loadCircuitJsonForFootprinter(inputPath);
286855
287231
  const inputText = await fs51.readFile(inputPath, "utf-8");
286856
- const result = circuitJsonToFootprinter(circuitJson, {
287232
+ const result = circuitJsonToFootprinter2(circuitJson, {
286857
287233
  sourceHints: [path56.basename(inputPath), inputText.slice(0, 20000)]
286858
287234
  });
286859
287235
  if (!result.best) {
@@ -291789,7 +292165,7 @@ import {
291789
292165
 
291790
292166
  // node_modules/dsn-converter/dist/index.js
291791
292167
  import { su as su14 } from "@tscircuit/soup-util";
291792
- import { getBoundsFromPoints as getBoundsFromPoints4 } from "@tscircuit/math-utils";
292168
+ import { getBoundsFromPoints as getBoundsFromPoints6 } from "@tscircuit/math-utils";
291793
292169
  import { applyToPoint as applyToPoint31, scale as scale8 } from "transformation-matrix";
291794
292170
  import { su as su24 } from "@tscircuit/soup-util";
291795
292171
  import { su as su34 } from "@tscircuit/soup-util";
@@ -292033,9 +292409,9 @@ function getPolygonCentroid2(points) {
292033
292409
  y: centroidY / areaFactor
292034
292410
  };
292035
292411
  }
292036
- function getPolygonSmtPadGeometry(pad2) {
292412
+ function getPolygonSmtPadGeometry2(pad2) {
292037
292413
  const points = getNormalizedPoints(pad2.points);
292038
- const bounds = getBoundsFromPoints4(points);
292414
+ const bounds = getBoundsFromPoints6(points);
292039
292415
  if (!bounds) {
292040
292416
  return {
292041
292417
  center: { x: 0, y: 0 },
@@ -292071,7 +292447,7 @@ function createAndAddPadstackFromPcbSmtPad({
292071
292447
  }) {
292072
292448
  const isCircle = pad2.shape === "circle";
292073
292449
  const isPolygon = pad2.shape === "polygon";
292074
- const polygonPadGeometry = isPolygon ? getPolygonSmtPadGeometry(pad2) : undefined;
292450
+ const polygonPadGeometry = isPolygon ? getPolygonSmtPadGeometry2(pad2) : undefined;
292075
292451
  let shape = "rect";
292076
292452
  let outerDiameter;
292077
292453
  let holeDiameter;
@@ -292128,7 +292504,7 @@ function createPinForImage({
292128
292504
  return;
292129
292505
  const isCircle = pad2.shape === "circle";
292130
292506
  const isPolygon = pad2.shape === "polygon";
292131
- const polygonPadGeometry = isPolygon ? getPolygonSmtPadGeometry(pad2) : undefined;
292507
+ const polygonPadGeometry = isPolygon ? getPolygonSmtPadGeometry2(pad2) : undefined;
292132
292508
  let shape = "rect";
292133
292509
  let outerDiameter;
292134
292510
  let holeDiameter;
@@ -304038,11 +304414,11 @@ function generateArcFromSweep(startX, startY, endX, endY, radius, largeArcFlag,
304038
304414
  return path71;
304039
304415
  }
304040
304416
  var mil10ToMm = (value) => value * 10 * 0.0254;
304041
- var getBoundsCenter = (bounds) => ({
304417
+ var getBoundsCenter3 = (bounds) => ({
304042
304418
  x: (bounds.minX + bounds.maxX) / 2,
304043
304419
  y: (bounds.minY + bounds.maxY) / 2
304044
304420
  });
304045
- var getBoundsFromPoints5 = (points) => {
304421
+ var getBoundsFromPoints7 = (points) => {
304046
304422
  if (points.length === 0) {
304047
304423
  return null;
304048
304424
  }
@@ -304108,8 +304484,8 @@ var getEasyEdaModelOriginOnBoardMm = ({
304108
304484
  }
304109
304485
  let modelOriginMm = null;
304110
304486
  if (points.length > 0) {
304111
- const bounds = getBoundsFromPoints5(points);
304112
- modelOriginMm = bounds ? getBoundsCenter(bounds) : null;
304487
+ const bounds = getBoundsFromPoints7(points);
304488
+ modelOriginMm = bounds ? getBoundsCenter3(bounds) : null;
304113
304489
  } else {
304114
304490
  const [originX, originY] = String(svgNode.svgData.attrs?.c_origin ?? "0,0").split(",").map((value) => Number(value.trim()));
304115
304491
  if (Number.isFinite(originX) && Number.isFinite(originY)) {
@@ -304142,7 +304518,7 @@ var getCadModelOffsetMmFromBounds = (easyEdaJson, bounds, {
304142
304518
  const rotationDeg = (rotationZRaw % 360 + 360) % 360;
304143
304519
  if (![0, 90, 180, 270].includes(rotationDeg))
304144
304520
  return null;
304145
- const modelCenter = getBoundsCenter(getXyBoundsFromModelBounds(bounds));
304521
+ const modelCenter = getBoundsCenter3(getXyBoundsFromModelBounds(bounds));
304146
304522
  const footprintCenter = easyEdaJson.packageDetail.dataStr.head;
304147
304523
  const footprintCenterMm = footprintBoundsCenterMm ?? {
304148
304524
  x: mil10ToMm(footprintCenter.x),
@@ -311791,11 +312167,11 @@ function generateArcFromSweep2(startX, startY, endX, endY, radius, largeArcFlag,
311791
312167
  return path71;
311792
312168
  }
311793
312169
  var mil10ToMm2 = (value) => value * 10 * 0.0254;
311794
- var getBoundsCenter2 = (bounds) => ({
312170
+ var getBoundsCenter4 = (bounds) => ({
311795
312171
  x: (bounds.minX + bounds.maxX) / 2,
311796
312172
  y: (bounds.minY + bounds.maxY) / 2
311797
312173
  });
311798
- var getBoundsFromPoints6 = (points) => {
312174
+ var getBoundsFromPoints8 = (points) => {
311799
312175
  if (points.length === 0) {
311800
312176
  return null;
311801
312177
  }
@@ -311861,8 +312237,8 @@ var getEasyEdaModelOriginOnBoardMm2 = ({
311861
312237
  }
311862
312238
  let modelOriginMm = null;
311863
312239
  if (points.length > 0) {
311864
- const bounds = getBoundsFromPoints6(points);
311865
- modelOriginMm = bounds ? getBoundsCenter2(bounds) : null;
312240
+ const bounds = getBoundsFromPoints8(points);
312241
+ modelOriginMm = bounds ? getBoundsCenter4(bounds) : null;
311866
312242
  } else {
311867
312243
  const [originX, originY] = String(svgNode.svgData.attrs?.c_origin ?? "0,0").split(",").map((value) => Number(value.trim()));
311868
312244
  if (Number.isFinite(originX) && Number.isFinite(originY)) {
@@ -311891,7 +312267,7 @@ var getCadModelOffsetMmFromBounds2 = (easyEdaJson, bounds, {
311891
312267
  const rotationDeg = (rotationZRaw % 360 + 360) % 360;
311892
312268
  if (![0, 90, 180, 270].includes(rotationDeg))
311893
312269
  return null;
311894
- const modelCenter = getBoundsCenter2(getXyBoundsFromModelBounds2(bounds));
312270
+ const modelCenter = getBoundsCenter4(getXyBoundsFromModelBounds2(bounds));
311895
312271
  const footprintCenter = easyEdaJson.packageDetail.dataStr.head;
311896
312272
  const footprintCenterMm = footprintBoundsCenterMm ?? {
311897
312273
  x: mil10ToMm2(footprintCenter.x),
@@ -313452,7 +313828,7 @@ var convertImportedFootprintToFootprinter = ({
313452
313828
  tsx
313453
313829
  }) => {
313454
313830
  try {
313455
- const discovery = circuitJsonToFootprinter(circuitJson, {
313831
+ const discovery = circuitJsonToFootprinter2(circuitJson, {
313456
313832
  maxCandidates: 5,
313457
313833
  sourceHints
313458
313834
  });