@jlcpcb/cli 0.2.0 → 0.3.0

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/index.js CHANGED
@@ -34073,22 +34073,25 @@ var {
34073
34073
 
34074
34074
  // ../core/dist/index.js
34075
34075
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
34076
- import { existsSync } from "fs";
34077
- import { join as join2 } from "path";
34078
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
34079
34076
  import { existsSync as existsSync2 } from "fs";
34080
34077
  import { join as join3 } from "path";
34081
- import { homedir, platform } from "os";
34078
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
34079
+ import { existsSync as existsSync3 } from "fs";
34080
+ import { join as join4 } from "path";
34081
+ import { homedir as homedir2, platform as platform2 } from "os";
34082
34082
  import { readdir, readFile, writeFile, mkdir, access, stat } from "fs/promises";
34083
34083
  import { join, dirname, extname, basename } from "path";
34084
+ import { existsSync } from "fs";
34085
+ import { homedir, platform } from "os";
34086
+ import { join as join2 } from "path";
34084
34087
  import { execSync } from "child_process";
34085
- import { existsSync as existsSync3 } from "fs";
34086
- import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
34087
- import { homedir as homedir2, platform as platform2 } from "os";
34088
- import { join as join4 } from "path";
34088
+ import { existsSync as existsSync4 } from "fs";
34089
+ import { readFile as readFile4, readdir as readdir2, unlink } from "fs/promises";
34090
+ import { homedir as homedir3, platform as platform3 } from "os";
34091
+ import { join as join5 } from "path";
34089
34092
  import { createServer } from "http";
34090
34093
  import { readFileSync } from "fs";
34091
- import { join as join5, dirname as dirname3 } from "path";
34094
+ import { join as join6, dirname as dirname3 } from "path";
34092
34095
  import { fileURLToPath } from "url";
34093
34096
  var __defProp2 = Object.defineProperty;
34094
34097
  var __export2 = (target, all) => {
@@ -34117,9 +34120,16 @@ function containsKeyword(text, keyword) {
34117
34120
  return false;
34118
34121
  }
34119
34122
  function getLibraryCategory(prefix, category, description) {
34120
- const normalizedPrefix = prefix.toUpperCase();
34121
- if (PREFIX_CATEGORY_MAP[normalizedPrefix]) {
34122
- return PREFIX_CATEGORY_MAP[normalizedPrefix];
34123
+ const cleanedPrefix = prefix.replace(/[^a-zA-Z0-9]/g, "").toUpperCase();
34124
+ if (cleanedPrefix.length >= 2) {
34125
+ const twoCharPrefix = cleanedPrefix.slice(0, 2);
34126
+ if (PREFIX_CATEGORY_MAP[twoCharPrefix]) {
34127
+ return PREFIX_CATEGORY_MAP[twoCharPrefix];
34128
+ }
34129
+ }
34130
+ const singleCharPrefix = cleanedPrefix.slice(0, 1);
34131
+ if (PREFIX_CATEGORY_MAP[singleCharPrefix]) {
34132
+ return PREFIX_CATEGORY_MAP[singleCharPrefix];
34123
34133
  }
34124
34134
  const searchText = [category || "", description || ""].join(" ");
34125
34135
  if (searchText.trim()) {
@@ -34183,6 +34193,7 @@ var init_category_router = __esm2(() => {
34183
34193
  X: "Crystals",
34184
34194
  J: "Connectors",
34185
34195
  P: "Connectors",
34196
+ RJ: "Connectors",
34186
34197
  K: "Misc",
34187
34198
  F: "Misc"
34188
34199
  };
@@ -34428,6 +34439,48 @@ var init_category_router = __esm2(() => {
34428
34439
  "ceramic resonator"
34429
34440
  ]
34430
34441
  },
34442
+ {
34443
+ category: "Connectors",
34444
+ keywords: [
34445
+ "connector",
34446
+ "header",
34447
+ "socket",
34448
+ "terminal",
34449
+ "terminal block",
34450
+ "jack",
34451
+ "plug",
34452
+ "receptacle",
34453
+ "usb-c",
34454
+ "usb type-c",
34455
+ "micro usb",
34456
+ "mini usb",
34457
+ "usb-a",
34458
+ "usb-b",
34459
+ "hdmi connector",
34460
+ "rj45",
34461
+ "rj11",
34462
+ "barrel jack",
34463
+ "dc jack",
34464
+ "audio jack",
34465
+ "jst",
34466
+ "jst-xh",
34467
+ "jst-ph",
34468
+ "molex",
34469
+ "dupont",
34470
+ "pin header",
34471
+ "female header",
34472
+ "fpc",
34473
+ "ffc",
34474
+ "sim card",
34475
+ "sd card",
34476
+ "microsd",
34477
+ "pogo pin",
34478
+ "spring contact",
34479
+ "test point",
34480
+ "ethernet connector",
34481
+ "modular connector"
34482
+ ]
34483
+ },
34431
34484
  {
34432
34485
  category: "Transistors",
34433
34486
  keywords: [
@@ -34528,46 +34581,6 @@ var init_category_router = __esm2(() => {
34528
34581
  "chip resistor"
34529
34582
  ]
34530
34583
  },
34531
- {
34532
- category: "Connectors",
34533
- keywords: [
34534
- "connector",
34535
- "header",
34536
- "socket",
34537
- "terminal",
34538
- "terminal block",
34539
- "jack",
34540
- "plug",
34541
- "receptacle",
34542
- "usb-c",
34543
- "usb type-c",
34544
- "micro usb",
34545
- "mini usb",
34546
- "usb-a",
34547
- "usb-b",
34548
- "hdmi connector",
34549
- "rj45",
34550
- "rj11",
34551
- "barrel jack",
34552
- "dc jack",
34553
- "audio jack",
34554
- "jst",
34555
- "jst-xh",
34556
- "jst-ph",
34557
- "molex",
34558
- "dupont",
34559
- "pin header",
34560
- "female header",
34561
- "fpc",
34562
- "ffc",
34563
- "sim card",
34564
- "sd card",
34565
- "microsd",
34566
- "pogo pin",
34567
- "spring contact",
34568
- "test point"
34569
- ]
34570
- },
34571
34584
  {
34572
34585
  category: "ICs",
34573
34586
  keywords: [
@@ -34661,9 +34674,9 @@ function addLibraryToTable(tableContent, libraryName, libraryPath, type, descrip
34661
34674
  `;
34662
34675
  }
34663
34676
  async function ensureSymLibTable(projectDir, symbolLibPath, libraryName = DEFAULT_LIBRARY_NAME, description = DEFAULT_LIBRARY_DESCRIPTION) {
34664
- const tablePath = join2(projectDir, "sym-lib-table");
34677
+ const tablePath = join3(projectDir, "sym-lib-table");
34665
34678
  const relativePath = symbolLibPath.startsWith(projectDir) ? "${KIPRJMOD}" + symbolLibPath.slice(projectDir.length) : symbolLibPath;
34666
- if (!existsSync(tablePath)) {
34679
+ if (!existsSync2(tablePath)) {
34667
34680
  const content = generateSymLibTable(relativePath, libraryName, description);
34668
34681
  await writeFile2(tablePath, content, "utf-8");
34669
34682
  return { created: true, modified: false, path: tablePath };
@@ -34677,9 +34690,9 @@ async function ensureSymLibTable(projectDir, symbolLibPath, libraryName = DEFAUL
34677
34690
  return { created: false, modified: true, path: tablePath };
34678
34691
  }
34679
34692
  async function ensureFpLibTable(projectDir, footprintLibPath, libraryName = DEFAULT_LIBRARY_NAME, description = DEFAULT_LIBRARY_DESCRIPTION) {
34680
- const tablePath = join2(projectDir, "fp-lib-table");
34693
+ const tablePath = join3(projectDir, "fp-lib-table");
34681
34694
  const relativePath = footprintLibPath.startsWith(projectDir) ? "${KIPRJMOD}" + footprintLibPath.slice(projectDir.length) : footprintLibPath;
34682
- if (!existsSync(tablePath)) {
34695
+ if (!existsSync2(tablePath)) {
34683
34696
  const content = generateFpLibTable(relativePath, libraryName, description);
34684
34697
  await writeFile2(tablePath, content, "utf-8");
34685
34698
  return { created: true, modified: false, path: tablePath };
@@ -34700,37 +34713,37 @@ __export2(exports_global_lib_table, {
34700
34713
  ensureGlobalLibraryTables: () => ensureGlobalLibraryTables,
34701
34714
  ensureGlobalEasyEDALibrary: () => ensureGlobalEasyEDALibrary
34702
34715
  });
34703
- function detectKicadVersion() {
34704
- const home = homedir();
34705
- const baseDir = join3(home, "Documents", "KiCad");
34716
+ function detectKicadVersion2() {
34717
+ const home = homedir2();
34718
+ const baseDir = join4(home, "Documents", "KiCad");
34706
34719
  for (const version of KICAD_VERSIONS2) {
34707
- if (existsSync2(join3(baseDir, version))) {
34720
+ if (existsSync3(join4(baseDir, version))) {
34708
34721
  return version;
34709
34722
  }
34710
34723
  }
34711
34724
  return "9.0";
34712
34725
  }
34713
- function getKicadConfigDir(version) {
34714
- const home = homedir();
34715
- const plat = platform();
34726
+ function getKicadConfigDir2(version) {
34727
+ const home = homedir2();
34728
+ const plat = platform2();
34716
34729
  if (plat === "darwin") {
34717
- return join3(home, "Library", "Preferences", "kicad", version);
34730
+ return join4(home, "Library", "Preferences", "kicad", version);
34718
34731
  } else if (plat === "win32") {
34719
- return join3(process.env.APPDATA || join3(home, "AppData", "Roaming"), "kicad", version);
34732
+ return join4(process.env.APPDATA || join4(home, "AppData", "Roaming"), "kicad", version);
34720
34733
  } else {
34721
- return join3(home, ".config", "kicad", version);
34734
+ return join4(home, ".config", "kicad", version);
34722
34735
  }
34723
34736
  }
34724
34737
  function get3rdPartyBaseDir(version) {
34725
- const home = homedir();
34726
- const plat = platform();
34738
+ const home = homedir2();
34739
+ const plat = platform2();
34727
34740
  if (plat === "linux") {
34728
- return join3(home, ".local", "share", "kicad", version, "3rdparty", LIBRARY_NAMESPACE);
34741
+ return join4(home, ".local", "share", "kicad", version, "3rdparty", LIBRARY_NAMESPACE);
34729
34742
  }
34730
- return join3(home, "Documents", "KiCad", version, "3rdparty", LIBRARY_NAMESPACE);
34743
+ return join4(home, "Documents", "KiCad", version, "3rdparty", LIBRARY_NAMESPACE);
34731
34744
  }
34732
34745
  function getSymbolLibPath(category, version) {
34733
- return join3(get3rdPartyBaseDir(version), "symbols", getLibraryFilename(category));
34746
+ return join4(get3rdPartyBaseDir(version), "symbols", getLibraryFilename(category));
34734
34747
  }
34735
34748
  function getSymbolLibUri(category) {
34736
34749
  return `${KICAD_3RD_PARTY_VAR}/${LIBRARY_NAMESPACE}/symbols/${getLibraryFilename(category)}`;
@@ -34771,11 +34784,11 @@ function generateFpLibTable2() {
34771
34784
  `;
34772
34785
  }
34773
34786
  async function ensureGlobalSymLibTable(version) {
34774
- const configDir = getKicadConfigDir(version);
34775
- const tablePath = join3(configDir, "sym-lib-table");
34787
+ const configDir = getKicadConfigDir2(version);
34788
+ const tablePath = join4(configDir, "sym-lib-table");
34776
34789
  const categories = getAllCategories();
34777
34790
  await mkdir2(configDir, { recursive: true });
34778
- if (!existsSync2(tablePath)) {
34791
+ if (!existsSync3(tablePath)) {
34779
34792
  const content2 = generateSymLibTable2();
34780
34793
  await writeFile3(tablePath, content2, "utf-8");
34781
34794
  return {
@@ -34815,11 +34828,11 @@ async function ensureGlobalSymLibTable(version) {
34815
34828
  };
34816
34829
  }
34817
34830
  async function ensureGlobalFpLibTable(version) {
34818
- const configDir = getKicadConfigDir(version);
34819
- const tablePath = join3(configDir, "fp-lib-table");
34831
+ const configDir = getKicadConfigDir2(version);
34832
+ const tablePath = join4(configDir, "fp-lib-table");
34820
34833
  const name = LIBRARY_PREFIX2;
34821
34834
  await mkdir2(configDir, { recursive: true });
34822
- if (!existsSync2(tablePath)) {
34835
+ if (!existsSync3(tablePath)) {
34823
34836
  const content2 = generateFpLibTable2();
34824
34837
  await writeFile3(tablePath, content2, "utf-8");
34825
34838
  return {
@@ -34853,13 +34866,13 @@ async function ensureGlobalFpLibTable(version) {
34853
34866
  }
34854
34867
  async function ensureLibraryStubs(version) {
34855
34868
  const baseDir = get3rdPartyBaseDir(version);
34856
- const symbolsDir = join3(baseDir, "symbols");
34857
- const footprintsDir = join3(baseDir, "footprints", getFootprintDirName());
34858
- const models3dDir = join3(baseDir, "3dmodels", get3DModelsDirName());
34869
+ const symbolsDir = join4(baseDir, "symbols");
34870
+ const footprintsDir = join4(baseDir, "footprints", getFootprintDirName());
34871
+ const models3dDir = join4(baseDir, "3dmodels", get3DModelsDirName());
34859
34872
  const directoriesCreated = [];
34860
34873
  const symbolsCreated = [];
34861
34874
  for (const dir of [symbolsDir, footprintsDir, models3dDir]) {
34862
- if (!existsSync2(dir)) {
34875
+ if (!existsSync3(dir)) {
34863
34876
  await mkdir2(dir, { recursive: true });
34864
34877
  directoriesCreated.push(dir);
34865
34878
  }
@@ -34868,7 +34881,7 @@ async function ensureLibraryStubs(version) {
34868
34881
  const emptyContent = generateEmptySymbolLibrary();
34869
34882
  for (const category of categories) {
34870
34883
  const filePath = getSymbolLibPath(category, version);
34871
- if (!existsSync2(filePath)) {
34884
+ if (!existsSync3(filePath)) {
34872
34885
  await writeFile3(filePath, emptyContent, "utf-8");
34873
34886
  symbolsCreated.push(filePath);
34874
34887
  }
@@ -34882,11 +34895,11 @@ function getEasyEDAFootprintLibUri() {
34882
34895
  return `${KICAD_3RD_PARTY_VAR}/${LIBRARY_NAMESPACE}/footprints/${EASYEDA_FOOTPRINT_LIBRARY_NAME}`;
34883
34896
  }
34884
34897
  async function ensureGlobalEasyEDALibrary() {
34885
- const version = detectKicadVersion();
34886
- const configDir = getKicadConfigDir(version);
34898
+ const version = detectKicadVersion2();
34899
+ const configDir = getKicadConfigDir2(version);
34887
34900
  await mkdir2(configDir, { recursive: true });
34888
- const symTablePath = join3(configDir, "sym-lib-table");
34889
- if (existsSync2(symTablePath)) {
34901
+ const symTablePath = join4(configDir, "sym-lib-table");
34902
+ if (existsSync3(symTablePath)) {
34890
34903
  let content = await readFile3(symTablePath, "utf-8");
34891
34904
  if (!libraryExistsInTable(content, EASYEDA_LIBRARY_NAME)) {
34892
34905
  const uri = getEasyEDASymbolLibUri();
@@ -34902,8 +34915,8 @@ async function ensureGlobalEasyEDALibrary() {
34902
34915
  `;
34903
34916
  await writeFile3(symTablePath, content, "utf-8");
34904
34917
  }
34905
- const fpTablePath = join3(configDir, "fp-lib-table");
34906
- if (existsSync2(fpTablePath)) {
34918
+ const fpTablePath = join4(configDir, "fp-lib-table");
34919
+ if (existsSync3(fpTablePath)) {
34907
34920
  let content = await readFile3(fpTablePath, "utf-8");
34908
34921
  if (!libraryExistsInTable(content, EASYEDA_LIBRARY_NAME)) {
34909
34922
  const uri = getEasyEDAFootprintLibUri();
@@ -34922,7 +34935,7 @@ async function ensureGlobalEasyEDALibrary() {
34922
34935
  }
34923
34936
  async function ensureGlobalLibraryTables() {
34924
34937
  const errors2 = [];
34925
- const version = detectKicadVersion();
34938
+ const version = detectKicadVersion2();
34926
34939
  let symLibTable = {
34927
34940
  path: "",
34928
34941
  created: false,
@@ -38950,6 +38963,9 @@ var coerce = {
38950
38963
  };
38951
38964
  var NEVER = INVALID;
38952
38965
  var LCSCPartNumberSchema = exports_external.string().regex(/^C\d+$/, "Invalid LCSC part number format (expected C followed by digits)");
38966
+ var EasyEDAUuidSchema = exports_external.string().regex(/^[a-f0-9]{32}$/i, "Invalid EasyEDA UUID (expected 32-character hex string)").transform((s) => s.toLowerCase());
38967
+ var ComponentIdSchema = exports_external.union([LCSCPartNumberSchema, EasyEDAUuidSchema]);
38968
+ var SafePathSchema = exports_external.string().min(1, "Path cannot be empty").refine((p) => !p.includes(".."), "Path traversal (..) not allowed").refine((p) => !p.includes("\x00"), "Null bytes not allowed in path");
38953
38969
  var PackageSchema = exports_external.string().min(1, "Package cannot be empty");
38954
38970
  var PriceTierSchema = exports_external.object({
38955
38971
  quantity: exports_external.number().positive(),
@@ -39106,7 +39122,58 @@ var logger = new Logger({ prefix: "ai-eda" });
39106
39122
  function createLogger(packageName) {
39107
39123
  return logger.child(packageName);
39108
39124
  }
39125
+ var KICAD_LAYERS = {
39126
+ F_CU: "F.Cu",
39127
+ B_CU: "B.Cu",
39128
+ IN1_CU: "In1.Cu",
39129
+ IN2_CU: "In2.Cu",
39130
+ IN3_CU: "In3.Cu",
39131
+ IN4_CU: "In4.Cu",
39132
+ F_ADHES: "F.Adhes",
39133
+ F_PASTE: "F.Paste",
39134
+ F_SILKS: "F.SilkS",
39135
+ F_MASK: "F.Mask",
39136
+ F_CRTYD: "F.CrtYd",
39137
+ F_FAB: "F.Fab",
39138
+ B_ADHES: "B.Adhes",
39139
+ B_PASTE: "B.Paste",
39140
+ B_SILKS: "B.SilkS",
39141
+ B_MASK: "B.Mask",
39142
+ B_CRTYD: "B.CrtYd",
39143
+ B_FAB: "B.Fab",
39144
+ EDGE_CUTS: "Edge.Cuts",
39145
+ MARGIN: "Margin",
39146
+ DWGS_USER: "Dwgs.User",
39147
+ CMTS_USER: "Cmts.User",
39148
+ ECO1_USER: "Eco1.User",
39149
+ ECO2_USER: "Eco2.User"
39150
+ };
39151
+ var KICAD_DEFAULTS = {
39152
+ TEXT_SIZE: 1.27,
39153
+ TEXT_THICKNESS: 0.15,
39154
+ WIRE_WIDTH: 0.25,
39155
+ GRID_SCHEMATIC: 2.54,
39156
+ GRID_PCB: 0.25,
39157
+ PIN_LENGTH: 1.27,
39158
+ PIN_NAME_OFFSET: 0
39159
+ };
39160
+ var KICAD_SYMBOL_VERSION = "20241209";
39161
+ var KICAD_FOOTPRINT_VERSION = "20241209";
39162
+ var KICAD_VERSIONS = ["9.0", "8.0"];
39163
+ function detectKicadVersion() {
39164
+ const home = homedir();
39165
+ const baseDir = join2(home, "Documents", "KiCad");
39166
+ for (const version of KICAD_VERSIONS) {
39167
+ if (existsSync(join2(baseDir, version))) {
39168
+ return version;
39169
+ }
39170
+ }
39171
+ return "9.0";
39172
+ }
39109
39173
  var logger3 = createLogger("jlc-api");
39174
+ function stripTemperaturePrefix(description) {
39175
+ return description.replace(/^-?\d+℃~[+-]?\d+℃\s*/, "");
39176
+ }
39110
39177
  var JLCPCB_SEARCH_API = "https://jlcpcb.com/api/overseas-pcb-order/v1/shoppingCart/smtGood/selectSmtComponentList/v2";
39111
39178
 
39112
39179
  class JLCClient {
@@ -39152,7 +39219,7 @@ class JLCClient {
39152
39219
  package: c.componentSpecificationEn || "",
39153
39220
  price: c.componentPrices?.[0]?.productPrice,
39154
39221
  stock: c.stockCount,
39155
- description: c.describe,
39222
+ description: stripTemperaturePrefix(c.describe),
39156
39223
  productUrl: c.lcscGoodsUrl,
39157
39224
  datasheetPdf: c.dataManualUrl,
39158
39225
  category: c.componentTypeEn,
@@ -39392,6 +39459,25 @@ function parseSymbolPath(data) {
39392
39459
  return null;
39393
39460
  }
39394
39461
  }
39462
+ function parseSymbolText(data) {
39463
+ try {
39464
+ const f = data.split("~");
39465
+ const isPinPart = f[f.length - 1] === "pinpart" || f.some((field) => field === "pinpart");
39466
+ return {
39467
+ x: safeParseFloat(f[2]),
39468
+ y: safeParseFloat(f[3]),
39469
+ rotation: safeParseFloat(f[4]),
39470
+ color: f[5] || "#000000",
39471
+ fontSize: safeParseFloat(f[7], 7),
39472
+ textType: f[11] || "",
39473
+ text: f[12] || "",
39474
+ id: f[15] || f[13] || "",
39475
+ isPinPart
39476
+ };
39477
+ } catch {
39478
+ return null;
39479
+ }
39480
+ }
39395
39481
  function parsePad(data) {
39396
39482
  try {
39397
39483
  const f = data.split("~");
@@ -39559,6 +39645,7 @@ function parseSymbolShapes(shapes) {
39559
39645
  const polylines = [];
39560
39646
  const polygons = [];
39561
39647
  const paths = [];
39648
+ const texts = [];
39562
39649
  for (const line of shapes) {
39563
39650
  if (typeof line !== "string")
39564
39651
  continue;
@@ -39612,10 +39699,15 @@ function parseSymbolShapes(shapes) {
39612
39699
  paths.push(path);
39613
39700
  break;
39614
39701
  }
39615
- case "T":
39702
+ case "T": {
39703
+ const text = parseSymbolText(line);
39704
+ if (text && text.text.trim())
39705
+ texts.push(text);
39616
39706
  break;
39707
+ }
39617
39708
  }
39618
39709
  }
39710
+ associatePinNamesFromTexts(pins, texts);
39619
39711
  return {
39620
39712
  pins,
39621
39713
  rectangles,
@@ -39624,9 +39716,30 @@ function parseSymbolShapes(shapes) {
39624
39716
  arcs,
39625
39717
  polylines,
39626
39718
  polygons,
39627
- paths
39719
+ paths,
39720
+ texts
39628
39721
  };
39629
39722
  }
39723
+ function associatePinNamesFromTexts(pins, texts) {
39724
+ const pinpartTexts = texts.filter((t) => t.isPinPart && t.text.trim());
39725
+ for (const text of pinpartTexts) {
39726
+ const textContent = text.text.trim();
39727
+ const jMatch = textContent.match(/^J(\d+)(?:\/(.+))?$/i);
39728
+ if (jMatch) {
39729
+ const pinNum = jMatch[1];
39730
+ const pinFunction = jMatch[2] || textContent;
39731
+ const pin = pins.find((p) => p.number === pinNum);
39732
+ if (pin && (!pin.name || pin.name === pinNum)) {
39733
+ pin.name = pinFunction || textContent;
39734
+ }
39735
+ continue;
39736
+ }
39737
+ const numMatch = textContent.match(/^(\d+)$/);
39738
+ if (numMatch) {
39739
+ continue;
39740
+ }
39741
+ }
39742
+ }
39630
39743
  function parseFootprintShapes(shapes) {
39631
39744
  const pads = [];
39632
39745
  const tracks = [];
@@ -39813,6 +39926,7 @@ class EasyEDAClient {
39813
39926
  polylines: symbolData.polylines,
39814
39927
  polygons: symbolData.polygons,
39815
39928
  paths: symbolData.paths,
39929
+ texts: symbolData.texts,
39816
39930
  origin: symbolOrigin
39817
39931
  },
39818
39932
  footprint: {
@@ -39986,6 +40100,7 @@ class EasyEDACommunityClient {
39986
40100
  polylines: symbolData.polylines,
39987
40101
  polygons: symbolData.polygons,
39988
40102
  paths: symbolData.paths,
40103
+ texts: symbolData.texts,
39989
40104
  origin: symbolOrigin,
39990
40105
  head: dataStr.head || {}
39991
40106
  },
@@ -40012,14 +40127,14 @@ class EasyEDACommunityClient {
40012
40127
  }
40013
40128
  }
40014
40129
  var easyedaCommunityClient = new EasyEDACommunityClient;
40015
- function isLcscId(id) {
40130
+ function isLcscId2(id) {
40016
40131
  return /^C\d+$/.test(id);
40017
40132
  }
40018
40133
  function createComponentService() {
40019
40134
  return {
40020
40135
  async search(query, options = {}) {
40021
40136
  const { source = "lcsc", limit = 20, inStock, basicOnly } = options;
40022
- if (source === "easyeda-community") {
40137
+ if (source === "community" || source === "easyeda-community") {
40023
40138
  const results = await easyedaCommunityClient.search({
40024
40139
  query,
40025
40140
  limit
@@ -40036,7 +40151,7 @@ function createComponentService() {
40036
40151
  return jlcClient.search(query, { limit, inStock, basicOnly });
40037
40152
  },
40038
40153
  async fetch(id) {
40039
- if (isLcscId(id)) {
40154
+ if (isLcscId2(id)) {
40040
40155
  return easyedaClient.getComponentData(id);
40041
40156
  }
40042
40157
  return null;
@@ -40064,43 +40179,6 @@ function createComponentService() {
40064
40179
  }
40065
40180
  };
40066
40181
  }
40067
- var KICAD_LAYERS = {
40068
- F_CU: "F.Cu",
40069
- B_CU: "B.Cu",
40070
- IN1_CU: "In1.Cu",
40071
- IN2_CU: "In2.Cu",
40072
- IN3_CU: "In3.Cu",
40073
- IN4_CU: "In4.Cu",
40074
- F_ADHES: "F.Adhes",
40075
- F_PASTE: "F.Paste",
40076
- F_SILKS: "F.SilkS",
40077
- F_MASK: "F.Mask",
40078
- F_CRTYD: "F.CrtYd",
40079
- F_FAB: "F.Fab",
40080
- B_ADHES: "B.Adhes",
40081
- B_PASTE: "B.Paste",
40082
- B_SILKS: "B.SilkS",
40083
- B_MASK: "B.Mask",
40084
- B_CRTYD: "B.CrtYd",
40085
- B_FAB: "B.Fab",
40086
- EDGE_CUTS: "Edge.Cuts",
40087
- MARGIN: "Margin",
40088
- DWGS_USER: "Dwgs.User",
40089
- CMTS_USER: "Cmts.User",
40090
- ECO1_USER: "Eco1.User",
40091
- ECO2_USER: "Eco2.User"
40092
- };
40093
- var KICAD_DEFAULTS = {
40094
- TEXT_SIZE: 1.27,
40095
- TEXT_THICKNESS: 0.15,
40096
- WIRE_WIDTH: 0.25,
40097
- GRID_SCHEMATIC: 2.54,
40098
- GRID_PCB: 0.25,
40099
- PIN_LENGTH: 1.27,
40100
- PIN_NAME_OFFSET: 0
40101
- };
40102
- var KICAD_SYMBOL_VERSION = "20241209";
40103
- var KICAD_FOOTPRINT_VERSION = "20241209";
40104
40182
  var PATTERNS = {
40105
40183
  resistor: /(\d+(?:\.\d+)?)\s*([kKmMgGrR]?)\s*(?:ohm|Ohm|OHM|Ω|R)?/i,
40106
40184
  capacitor: /(\d+(?:\.\d+)?)\s*([pnuμmM]?)[Ff]?\s*(?:[\/\s]*(\d+)\s*[Vv])?/i,
@@ -40510,6 +40588,29 @@ function parseSvgArcPath(path) {
40510
40588
  return null;
40511
40589
  }
40512
40590
  }
40591
+ function interpolateArc(params, segmentsPerQuarter = 4) {
40592
+ const center = svgArcToCenter(params);
40593
+ if (!center) {
40594
+ return [{ x: params.x2, y: params.y2 }];
40595
+ }
40596
+ const { cx, cy, rx, ry, startAngle, deltaAngle } = center;
40597
+ const phiRad = params.phi * Math.PI / 180;
40598
+ const cosPhi = Math.cos(phiRad);
40599
+ const sinPhi = Math.sin(phiRad);
40600
+ const arcDegrees = Math.abs(deltaAngle) * (180 / Math.PI);
40601
+ const numSegments = Math.max(2, Math.ceil(arcDegrees / 90 * segmentsPerQuarter));
40602
+ const points = [];
40603
+ for (let i = 1;i <= numSegments; i++) {
40604
+ const t = i / numSegments;
40605
+ const angle = startAngle + deltaAngle * t;
40606
+ const px = rx * Math.cos(angle);
40607
+ const py = ry * Math.sin(angle);
40608
+ const x = cosPhi * px - sinPhi * py + cx;
40609
+ const y = sinPhi * px + cosPhi * py + cy;
40610
+ points.push({ x, y });
40611
+ }
40612
+ return points;
40613
+ }
40513
40614
  var CATEGORY_TO_PREFIX = {
40514
40615
  Resistors: "R",
40515
40616
  Capacitors: "C",
@@ -40572,7 +40673,12 @@ class SymbolConverter {
40572
40673
  return output;
40573
40674
  }
40574
40675
  hasRenderableShapes(symbol) {
40575
- return symbol.rectangles.length > 0 || symbol.circles.length > 0 || symbol.ellipses.length > 0 || symbol.polylines.length > 0 || symbol.polygons.length > 0 || symbol.arcs.length > 0 || symbol.paths.length > 0;
40676
+ const pinNamePattern = /^J\d+(?:\/|$)/i;
40677
+ const hasDecorativeText = (symbol.texts || []).some((t) => {
40678
+ const text = t.text.trim();
40679
+ return text && !pinNamePattern.test(text);
40680
+ });
40681
+ return symbol.rectangles.length > 0 || symbol.circles.length > 0 || symbol.ellipses.length > 0 || symbol.polylines.length > 0 || symbol.polygons.length > 0 || symbol.arcs.length > 0 || symbol.paths.length > 0 || hasDecorativeText;
40576
40682
  }
40577
40683
  generateFromShapes(component, name) {
40578
40684
  const { info, symbol } = component;
@@ -40606,6 +40712,18 @@ class SymbolConverter {
40606
40712
  if (pathOutput)
40607
40713
  output += pathOutput;
40608
40714
  }
40715
+ const pinNamePattern = /^J\d+(?:\/|$)/i;
40716
+ const decorativeTexts = (symbol.texts || []).filter((t) => {
40717
+ const text = t.text.trim();
40718
+ if (!text)
40719
+ return false;
40720
+ return !pinNamePattern.test(text);
40721
+ });
40722
+ for (const text of decorativeTexts) {
40723
+ const textOutput = this.convertText(text, origin);
40724
+ if (textOutput)
40725
+ output += textOutput;
40726
+ }
40609
40727
  output += ` )
40610
40728
  `;
40611
40729
  output += ` (symbol "${name}_1_1"
@@ -40790,6 +40908,24 @@ class SymbolConverter {
40790
40908
  `;
40791
40909
  return output;
40792
40910
  }
40911
+ convertText(text, origin) {
40912
+ if (!text.text || text.text.trim() === "")
40913
+ return null;
40914
+ const x = this.convertX(text.x, origin.x);
40915
+ const y = this.convertY(text.y, origin.y);
40916
+ const fontSize = roundTo(Math.max(text.fontSize * EE_TO_MM * 0.8, 0.5), 3);
40917
+ const rotation = (360 - text.rotation) % 360;
40918
+ const content = text.text.replace(/"/g, "'").replace(/\\/g, "");
40919
+ return ` (text "${content}"
40920
+ (at ${x} ${y} ${rotation})
40921
+ (effects
40922
+ (font
40923
+ (size ${fontSize} ${fontSize})
40924
+ )
40925
+ )
40926
+ )
40927
+ `;
40928
+ }
40793
40929
  parseSvgPath(pathStr, origin) {
40794
40930
  const points = [];
40795
40931
  let firstPoint = null;
@@ -41995,7 +42131,7 @@ class FootprintConverter {
41995
42131
  return this.generatePad({ ...pad, shape: "RECT", points: "" }, origin);
41996
42132
  }
41997
42133
  let holeRadius = pad.holeRadius;
41998
- if (holeRadius === 0 && pad.isPlated && pad.shape === "POLYGON") {
42134
+ if (holeRadius === 0 && pad.layerId === 11 && pad.shape === "POLYGON") {
41999
42135
  holeRadius = this.calculateDrillRadiusFromPolygon(points, pad.centerX, pad.centerY);
42000
42136
  }
42001
42137
  const isSmd = holeRadius === 0;
@@ -42337,12 +42473,26 @@ ${justify ? ` (justify ${justify})
42337
42473
  break;
42338
42474
  case "A":
42339
42475
  if (args.length >= 7) {
42476
+ const arcParams = {
42477
+ x1: currentX,
42478
+ y1: currentY,
42479
+ rx: args[0],
42480
+ ry: args[1],
42481
+ phi: args[2],
42482
+ largeArc: args[3] === 1,
42483
+ sweep: args[4] === 1,
42484
+ x2: args[5],
42485
+ y2: args[6]
42486
+ };
42487
+ const arcPoints = interpolateArc(arcParams, 4);
42488
+ for (const pt of arcPoints) {
42489
+ points.push({
42490
+ x: convertX(pt.x, origin.x),
42491
+ y: convertY(pt.y, origin.y)
42492
+ });
42493
+ }
42340
42494
  currentX = args[5];
42341
42495
  currentY = args[6];
42342
- points.push({
42343
- x: convertX(currentX, origin.x),
42344
- y: convertY(currentY, origin.y)
42345
- });
42346
42496
  }
42347
42497
  break;
42348
42498
  case "Z":
@@ -42516,7 +42666,6 @@ init_category_router();
42516
42666
  var FOOTPRINT_LIBRARY_NAME = getFootprintDirName();
42517
42667
  var MODELS_3D_NAME = get3DModelsDirName();
42518
42668
  var LIBRARY_NAMESPACE2 = "jlc_mcp";
42519
- var KICAD_VERSIONS3 = ["9.0", "8.0"];
42520
42669
  var EASYEDA_LIBRARY_NAME2 = "EasyEDA";
42521
42670
  var EASYEDA_SYMBOL_LIBRARY_NAME2 = "EasyEDA.kicad_sym";
42522
42671
  var EASYEDA_FOOTPRINT_LIBRARY_NAME2 = "EasyEDA.pretty";
@@ -42525,58 +42674,48 @@ var EASYEDA_LOCAL_LIBRARY_NAME = "EasyEDA-local";
42525
42674
  var EASYEDA_LOCAL_SYMBOL_LIBRARY_NAME = "EasyEDA-local.kicad_sym";
42526
42675
  var EASYEDA_LOCAL_FOOTPRINT_LIBRARY_NAME = "EasyEDA-local.pretty";
42527
42676
  var EASYEDA_LOCAL_LIBRARY_DESCRIPTION = "EasyEDA Community Component Library (Project-local)";
42528
- function detectKicadVersion2() {
42529
- const home = homedir2();
42530
- const baseDir = join4(home, "Documents", "KiCad");
42531
- for (const version of KICAD_VERSIONS3) {
42532
- if (existsSync3(join4(baseDir, version))) {
42533
- return version;
42534
- }
42535
- }
42536
- return "9.0";
42537
- }
42538
42677
  function getGlobalLibraryPaths() {
42539
- const home = homedir2();
42540
- const version = detectKicadVersion2();
42541
- const plat = platform2();
42678
+ const home = homedir3();
42679
+ const version = detectKicadVersion();
42680
+ const plat = platform3();
42542
42681
  let base;
42543
42682
  if (plat === "linux") {
42544
- base = join4(home, ".local", "share", "kicad", version, "3rdparty", LIBRARY_NAMESPACE2);
42683
+ base = join5(home, ".local", "share", "kicad", version, "3rdparty", LIBRARY_NAMESPACE2);
42545
42684
  } else {
42546
- base = join4(home, "Documents", "KiCad", version, "3rdparty", LIBRARY_NAMESPACE2);
42685
+ base = join5(home, "Documents", "KiCad", version, "3rdparty", LIBRARY_NAMESPACE2);
42547
42686
  }
42548
42687
  return {
42549
42688
  base,
42550
- symbolsDir: join4(base, "symbols"),
42551
- footprintsDir: join4(base, "footprints"),
42552
- models3dDir: join4(base, "3dmodels"),
42553
- footprintDir: join4(base, "footprints", FOOTPRINT_LIBRARY_NAME),
42554
- models3dFullDir: join4(base, "3dmodels", MODELS_3D_NAME)
42689
+ symbolsDir: join5(base, "symbols"),
42690
+ footprintsDir: join5(base, "footprints"),
42691
+ models3dDir: join5(base, "3dmodels"),
42692
+ footprintDir: join5(base, "footprints", FOOTPRINT_LIBRARY_NAME),
42693
+ models3dFullDir: join5(base, "3dmodels", MODELS_3D_NAME)
42555
42694
  };
42556
42695
  }
42557
42696
  function getProjectLibraryPaths(projectPath) {
42558
- const librariesDir = join4(projectPath, "libraries");
42697
+ const librariesDir = join5(projectPath, "libraries");
42559
42698
  return {
42560
42699
  base: librariesDir,
42561
- symbolsDir: join4(librariesDir, "symbols"),
42562
- footprintsDir: join4(librariesDir, "footprints"),
42563
- models3dDir: join4(librariesDir, "3dmodels"),
42564
- footprintDir: join4(librariesDir, "footprints", FOOTPRINT_LIBRARY_NAME),
42565
- models3dFullDir: join4(librariesDir, "3dmodels", MODELS_3D_NAME)
42700
+ symbolsDir: join5(librariesDir, "symbols"),
42701
+ footprintsDir: join5(librariesDir, "footprints"),
42702
+ models3dDir: join5(librariesDir, "3dmodels"),
42703
+ footprintDir: join5(librariesDir, "footprints", FOOTPRINT_LIBRARY_NAME),
42704
+ models3dFullDir: join5(librariesDir, "3dmodels", MODELS_3D_NAME)
42566
42705
  };
42567
42706
  }
42568
- function isLcscId2(id) {
42707
+ function isLcscId3(id) {
42569
42708
  return /^C\d+$/.test(id);
42570
42709
  }
42571
- function getKicadConfigDir2(version) {
42572
- const home = homedir2();
42573
- const plat = platform2();
42710
+ function getKicadConfigDir3(version) {
42711
+ const home = homedir3();
42712
+ const plat = platform3();
42574
42713
  if (plat === "darwin") {
42575
- return join4(home, "Library", "Preferences", "kicad", version);
42714
+ return join5(home, "Library", "Preferences", "kicad", version);
42576
42715
  } else if (plat === "win32") {
42577
- return join4(process.env.APPDATA || join4(home, "AppData", "Roaming"), "kicad", version);
42716
+ return join5(process.env.APPDATA || join5(home, "AppData", "Roaming"), "kicad", version);
42578
42717
  } else {
42579
- return join4(home, ".config", "kicad", version);
42718
+ return join5(home, ".config", "kicad", version);
42580
42719
  }
42581
42720
  }
42582
42721
  async function parseSymbolLibrary(filePath, libraryName, category, models3dDir) {
@@ -42602,8 +42741,8 @@ async function parseSymbolLibrary(filePath, libraryName, category, models3dDir)
42602
42741
  const name = nameParts.length > 1 ? nameParts[1] : fullSymbolRef;
42603
42742
  const fpMatch = symbolBlock.match(/\(property\s+"Footprint"\s+"([^"]+)"/);
42604
42743
  const footprintRef = fpMatch ? fpMatch[1] : "";
42605
- const model3dPath = join4(models3dDir, `${name}.step`);
42606
- const has3dModel = existsSync3(model3dPath);
42744
+ const model3dPath = join5(models3dDir, `${name}.step`);
42745
+ const has3dModel = existsSync4(model3dPath);
42607
42746
  if (lcscId) {
42608
42747
  components.push({
42609
42748
  lcscId,
@@ -42619,6 +42758,42 @@ async function parseSymbolLibrary(filePath, libraryName, category, models3dDir)
42619
42758
  } catch {}
42620
42759
  return components;
42621
42760
  }
42761
+ function removeSymbolFromLibrary(content, symbolName) {
42762
+ const lines = content.split(`
42763
+ `);
42764
+ const result = [];
42765
+ let depth = 0;
42766
+ let inTargetSymbol = false;
42767
+ let symbolStartDepth = 0;
42768
+ for (const line of lines) {
42769
+ const openCount = (line.match(/\(/g) || []).length;
42770
+ const closeCount = (line.match(/\)/g) || []).length;
42771
+ const symbolMatch = line.match(/^\s*\(symbol\s+"([^"]+)"/);
42772
+ if (symbolMatch && !inTargetSymbol) {
42773
+ const fullRef = symbolMatch[1];
42774
+ const nameOnly = fullRef.includes(":") ? fullRef.split(":")[1] : fullRef;
42775
+ if (nameOnly === symbolName || nameOnly.startsWith(`${symbolName}_`)) {
42776
+ inTargetSymbol = true;
42777
+ symbolStartDepth = depth;
42778
+ depth += openCount - closeCount;
42779
+ continue;
42780
+ }
42781
+ }
42782
+ if (inTargetSymbol) {
42783
+ depth += openCount - closeCount;
42784
+ if (depth <= symbolStartDepth) {
42785
+ inTargetSymbol = false;
42786
+ }
42787
+ continue;
42788
+ }
42789
+ depth += openCount - closeCount;
42790
+ result.push(line);
42791
+ }
42792
+ return result.join(`
42793
+ `).replace(/\n{3,}/g, `
42794
+
42795
+ `);
42796
+ }
42622
42797
  function adaptCommunityComponent(component) {
42623
42798
  const symbolHead = component.symbol.head;
42624
42799
  const cPara = symbolHead?.c_para || {};
@@ -42641,6 +42816,7 @@ function adaptCommunityComponent(component) {
42641
42816
  polylines: component.symbol.polylines,
42642
42817
  polygons: component.symbol.polygons,
42643
42818
  paths: component.symbol.paths,
42819
+ texts: component.symbol.texts || [],
42644
42820
  origin: component.symbol.origin
42645
42821
  },
42646
42822
  footprint: {
@@ -42664,11 +42840,11 @@ function adaptCommunityComponent(component) {
42664
42840
  function createLibraryService() {
42665
42841
  return {
42666
42842
  async install(id, options = {}) {
42667
- const isCommunityComponent = !isLcscId2(id);
42843
+ const isCommunityComponent = !isLcscId3(id);
42668
42844
  const isGlobal = !options.projectPath;
42669
42845
  const paths = isGlobal ? getGlobalLibraryPaths() : getProjectLibraryPaths(options.projectPath);
42670
42846
  let component = null;
42671
- if (isLcscId2(id)) {
42847
+ if (isLcscId3(id)) {
42672
42848
  component = await easyedaClient.getComponentData(id);
42673
42849
  } else {
42674
42850
  const communityComponent = await easyedaCommunityClient.getComponent(id);
@@ -42717,18 +42893,18 @@ function createLibraryService() {
42717
42893
  let easyedaModelsDir;
42718
42894
  if (isGlobal) {
42719
42895
  symbolsDir = paths.symbolsDir;
42720
- footprintDir = join4(paths.footprintsDir, fpLibDir);
42721
- easyedaModelsDir = join4(paths.models3dDir, models3dDirName);
42896
+ footprintDir = join5(paths.footprintsDir, fpLibDir);
42897
+ easyedaModelsDir = join5(paths.models3dDir, models3dDirName);
42722
42898
  } else {
42723
- const librariesDir = join4(options.projectPath, "libraries");
42724
- symbolsDir = join4(librariesDir, "symbols");
42725
- footprintDir = join4(librariesDir, "footprints", fpLibDir);
42726
- easyedaModelsDir = join4(librariesDir, "3dmodels", models3dDirName);
42899
+ const librariesDir = join5(options.projectPath, "libraries");
42900
+ symbolsDir = join5(librariesDir, "symbols");
42901
+ footprintDir = join5(librariesDir, "footprints", fpLibDir);
42902
+ easyedaModelsDir = join5(librariesDir, "3dmodels", models3dDirName);
42727
42903
  }
42728
42904
  models3dDir = easyedaModelsDir;
42729
42905
  await ensureDir(symbolsDir);
42730
42906
  await ensureDir(footprintDir);
42731
- symbolFile = join4(symbolsDir, symLibFile);
42907
+ symbolFile = join5(symbolsDir, symLibFile);
42732
42908
  symbolName = component.info.name.replace(/[^a-zA-Z0-9_-]/g, "_");
42733
42909
  const include3d = options.include3d ?? true;
42734
42910
  let modelRelativePath;
@@ -42737,7 +42913,7 @@ function createLibraryService() {
42737
42913
  const model = await easyedaCommunityClient.get3DModel(component.model3d.uuid, "step");
42738
42914
  if (model) {
42739
42915
  const modelFilename = `${symbolName}.step`;
42740
- modelPath = join4(models3dDir, modelFilename);
42916
+ modelPath = join5(models3dDir, modelFilename);
42741
42917
  await writeBinary(modelPath, model);
42742
42918
  if (isGlobal) {
42743
42919
  modelRelativePath = `\${KICAD9_3RD_PARTY}/${LIBRARY_NAMESPACE2}/3dmodels/${models3dDirName}/${modelFilename}`;
@@ -42751,7 +42927,7 @@ function createLibraryService() {
42751
42927
  include3DModel: !!modelRelativePath,
42752
42928
  modelPath: modelRelativePath
42753
42929
  });
42754
- footprintPath = join4(footprintDir, `${symbolName}.kicad_mod`);
42930
+ footprintPath = join5(footprintDir, `${symbolName}.kicad_mod`);
42755
42931
  footprintRef = `${libName}:${symbolName}`;
42756
42932
  await writeText(footprintPath, footprint);
42757
42933
  component.info.package = footprintRef;
@@ -42766,20 +42942,20 @@ function createLibraryService() {
42766
42942
  } else {
42767
42943
  category = getLibraryCategory(component.info.prefix, component.info.category, component.info.description);
42768
42944
  const symbolLibraryFilename = getLibraryFilename(category);
42769
- symbolFile = join4(paths.symbolsDir, symbolLibraryFilename);
42945
+ symbolFile = join5(paths.symbolsDir, symbolLibraryFilename);
42770
42946
  footprintDir = paths.footprintDir;
42771
42947
  models3dDir = paths.models3dFullDir;
42772
42948
  await ensureDir(paths.symbolsDir);
42773
42949
  await ensureDir(paths.footprintDir);
42774
42950
  symbolName = symbolConverter.getSymbolName(component);
42775
- const include3d = options.include3d ?? false;
42951
+ const include3d = options.include3d ?? true;
42776
42952
  let modelRelativePath;
42777
42953
  if (include3d && component.model3d) {
42778
42954
  await ensureDir(models3dDir);
42779
42955
  const model = await easyedaClient.get3DModel(component.model3d.uuid, "step");
42780
42956
  if (model) {
42781
42957
  const modelFilename = `${symbolName}.step`;
42782
- modelPath = join4(models3dDir, modelFilename);
42958
+ modelPath = join5(models3dDir, modelFilename);
42783
42959
  await writeBinary(modelPath, model);
42784
42960
  modelRelativePath = `\${KICAD9_3RD_PARTY}/${LIBRARY_NAMESPACE2}/3dmodels/${MODELS_3D_NAME}/${modelFilename}`;
42785
42961
  }
@@ -42791,7 +42967,7 @@ function createLibraryService() {
42791
42967
  if (footprintResult.type === "reference") {
42792
42968
  footprintRef = footprintResult.reference;
42793
42969
  } else {
42794
- footprintPath = join4(footprintDir, `${footprintResult.name}.kicad_mod`);
42970
+ footprintPath = join5(footprintDir, `${footprintResult.name}.kicad_mod`);
42795
42971
  footprintRef = getFootprintReference(footprintResult.name);
42796
42972
  await writeText(footprintPath, footprintResult.content);
42797
42973
  }
@@ -42804,7 +42980,7 @@ function createLibraryService() {
42804
42980
  }
42805
42981
  let symbolContent;
42806
42982
  let symbolAction;
42807
- if (existsSync3(symbolFile)) {
42983
+ if (existsSync4(symbolFile)) {
42808
42984
  const existingContent = await readFile4(symbolFile, "utf-8");
42809
42985
  if (symbolConverter.symbolExistsInLibrary(existingContent, component.info.name)) {
42810
42986
  if (options.force) {
@@ -42896,9 +43072,9 @@ function createLibraryService() {
42896
43072
  if (options.category && options.category !== category)
42897
43073
  continue;
42898
43074
  const libraryFilename = getLibraryFilename(category);
42899
- const libraryPath = join4(paths.symbolsDir, libraryFilename);
43075
+ const libraryPath = join5(paths.symbolsDir, libraryFilename);
42900
43076
  const libraryName = `JLC-MCP-${category}`;
42901
- if (existsSync3(libraryPath)) {
43077
+ if (existsSync4(libraryPath)) {
42902
43078
  const components = await parseSymbolLibrary(libraryPath, libraryName, category, paths.models3dFullDir);
42903
43079
  allComponents.push(...components);
42904
43080
  }
@@ -42913,13 +43089,13 @@ function createLibraryService() {
42913
43089
  await ensureGlobalLibraryTables2();
42914
43090
  },
42915
43091
  async getStatus() {
42916
- const version = detectKicadVersion2();
43092
+ const version = detectKicadVersion();
42917
43093
  const paths = getGlobalLibraryPaths();
42918
- const configDir = getKicadConfigDir2(version);
42919
- const symLibTablePath = join4(configDir, "sym-lib-table");
42920
- const fpLibTablePath = join4(configDir, "fp-lib-table");
43094
+ const configDir = getKicadConfigDir3(version);
43095
+ const symLibTablePath = join5(configDir, "sym-lib-table");
43096
+ const fpLibTablePath = join5(configDir, "fp-lib-table");
42921
43097
  let installed = false;
42922
- if (existsSync3(paths.symbolsDir)) {
43098
+ if (existsSync4(paths.symbolsDir)) {
42923
43099
  try {
42924
43100
  const files = await readdir2(paths.symbolsDir);
42925
43101
  installed = files.some((f) => f.endsWith(".kicad_sym"));
@@ -42928,7 +43104,7 @@ function createLibraryService() {
42928
43104
  }
42929
43105
  }
42930
43106
  let linked = false;
42931
- if (existsSync3(symLibTablePath)) {
43107
+ if (existsSync4(symLibTablePath)) {
42932
43108
  try {
42933
43109
  const content = await readFile4(symLibTablePath, "utf-8");
42934
43110
  linked = content.includes("JLC-MCP");
@@ -42954,8 +43130,8 @@ function createLibraryService() {
42954
43130
  },
42955
43131
  async isEasyEDAInstalled(componentName) {
42956
43132
  const paths = getGlobalLibraryPaths();
42957
- const easyedaLibPath = join4(paths.symbolsDir, EASYEDA_SYMBOL_LIBRARY_NAME2);
42958
- if (!existsSync3(easyedaLibPath)) {
43133
+ const easyedaLibPath = join5(paths.symbolsDir, EASYEDA_SYMBOL_LIBRARY_NAME2);
43134
+ if (!existsSync4(easyedaLibPath)) {
42959
43135
  return false;
42960
43136
  }
42961
43137
  try {
@@ -42966,6 +43142,85 @@ function createLibraryService() {
42966
43142
  } catch {
42967
43143
  return false;
42968
43144
  }
43145
+ },
43146
+ async regenerate(options = {}) {
43147
+ const include3d = options.include3d ?? true;
43148
+ const installed = await this.listInstalled({ projectPath: options.projectPath });
43149
+ const results = {
43150
+ total: installed.length,
43151
+ success: 0,
43152
+ failed: 0,
43153
+ components: []
43154
+ };
43155
+ for (let i = 0;i < installed.length; i++) {
43156
+ const component = installed[i];
43157
+ options.onProgress?.(i + 1, installed.length, component, "start");
43158
+ try {
43159
+ await this.install(component.lcscId, {
43160
+ projectPath: options.projectPath,
43161
+ include3d,
43162
+ force: true
43163
+ });
43164
+ results.success++;
43165
+ results.components.push({
43166
+ id: component.lcscId,
43167
+ name: component.name,
43168
+ status: "success"
43169
+ });
43170
+ options.onProgress?.(i + 1, installed.length, component, "success");
43171
+ } catch (error) {
43172
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
43173
+ results.failed++;
43174
+ results.components.push({
43175
+ id: component.lcscId,
43176
+ name: component.name,
43177
+ status: "failed",
43178
+ error: errorMessage
43179
+ });
43180
+ options.onProgress?.(i + 1, installed.length, component, "error", errorMessage);
43181
+ }
43182
+ }
43183
+ return results;
43184
+ },
43185
+ async remove(lcscId, options = {}) {
43186
+ const paths = options.projectPath ? getProjectLibraryPaths(options.projectPath) : getGlobalLibraryPaths();
43187
+ const installed = await this.listInstalled({ projectPath: options.projectPath });
43188
+ const component = installed.find((c) => c.lcscId === lcscId);
43189
+ if (!component) {
43190
+ throw new Error(`Component ${lcscId} not found in library`);
43191
+ }
43192
+ const result = {
43193
+ success: false,
43194
+ lcscId,
43195
+ symbolRemoved: false,
43196
+ footprintRemoved: false,
43197
+ model3dRemoved: false
43198
+ };
43199
+ const libraryFilename = getLibraryFilename(component.category);
43200
+ const symbolLibPath = join5(paths.symbolsDir, libraryFilename);
43201
+ if (existsSync4(symbolLibPath)) {
43202
+ const content = await readFile4(symbolLibPath, "utf-8");
43203
+ const newContent = removeSymbolFromLibrary(content, component.name);
43204
+ await writeText(symbolLibPath, newContent);
43205
+ result.symbolRemoved = true;
43206
+ }
43207
+ if (component.footprintRef?.startsWith("JLC-MCP:")) {
43208
+ const fpName = component.footprintRef.split(":")[1];
43209
+ const footprintPath = join5(paths.footprintDir, `${fpName}.kicad_mod`);
43210
+ if (existsSync4(footprintPath)) {
43211
+ await unlink(footprintPath);
43212
+ result.footprintRemoved = true;
43213
+ }
43214
+ }
43215
+ if (component.has3dModel) {
43216
+ const modelPath = join5(paths.models3dFullDir, `${component.name}.step`);
43217
+ if (existsSync4(modelPath)) {
43218
+ await unlink(modelPath);
43219
+ result.model3dRemoved = true;
43220
+ }
43221
+ }
43222
+ result.success = result.symbolRemoved;
43223
+ return result;
42969
43224
  }
42970
43225
  };
42971
43226
  }
@@ -42981,16 +43236,16 @@ function getHtmlPage() {
42981
43236
  return htmlCache;
42982
43237
  try {
42983
43238
  const possiblePaths = [
42984
- join5(__dirname2, "assets/search.html"),
42985
- join5(__dirname2, "../dist/assets/search.html"),
42986
- join5(__dirname2, "../assets/search.html"),
42987
- join5(__dirname2, "../assets/search-built.html"),
42988
- join5(process.cwd(), "dist/assets/search.html"),
42989
- join5(process.cwd(), "packages/core/dist/assets/search.html"),
42990
- join5(__dirname2, "../../dist/assets/search.html"),
42991
- join5(__dirname2, "../../../core/dist/assets/search.html"),
42992
- join5(__dirname2, "../../../../packages/core/dist/assets/search.html"),
42993
- join5(__dirname2, "../../node_modules/@jlcpcb/core/dist/assets/search.html")
43239
+ join6(__dirname2, "assets/search.html"),
43240
+ join6(__dirname2, "../dist/assets/search.html"),
43241
+ join6(__dirname2, "../assets/search.html"),
43242
+ join6(__dirname2, "../assets/search-built.html"),
43243
+ join6(process.cwd(), "dist/assets/search.html"),
43244
+ join6(process.cwd(), "packages/core/dist/assets/search.html"),
43245
+ join6(__dirname2, "../../dist/assets/search.html"),
43246
+ join6(__dirname2, "../../../core/dist/assets/search.html"),
43247
+ join6(__dirname2, "../../../../packages/core/dist/assets/search.html"),
43248
+ join6(__dirname2, "../../node_modules/@jlcpcb/core/dist/assets/search.html")
42994
43249
  ];
42995
43250
  for (const path of possiblePaths) {
42996
43251
  try {
@@ -43072,121 +43327,2628 @@ async function handleSearch(url, res) {
43072
43327
  hasNext: page < totalPages,
43073
43328
  hasPrev: page > 1
43074
43329
  }
43075
- }));
43076
- } catch (error) {
43077
- logger7.error("Search error:", error);
43078
- res.writeHead(500, { "Content-Type": "application/json" });
43079
- res.end(JSON.stringify({ error: "Search failed" }));
43330
+ }));
43331
+ } catch (error) {
43332
+ logger7.error("Search error:", error);
43333
+ res.writeHead(500, { "Content-Type": "application/json" });
43334
+ res.end(JSON.stringify({ error: "Search failed" }));
43335
+ }
43336
+ }
43337
+ function communityToComponentData(community) {
43338
+ const cPara = community.symbol.head?.c_para ?? {};
43339
+ const fpCPara = community.footprint.head?.c_para ?? {};
43340
+ return {
43341
+ info: {
43342
+ name: community.title || cPara.name || "Unknown",
43343
+ prefix: cPara.pre || cPara.Prefix || "U",
43344
+ package: fpCPara.package || community.footprint.name,
43345
+ manufacturer: cPara.Manufacturer || cPara.BOM_Manufacturer,
43346
+ description: community.description || cPara.BOM_Manufacturer_Part,
43347
+ category: cPara.package
43348
+ },
43349
+ symbol: community.symbol,
43350
+ footprint: community.footprint,
43351
+ model3d: community.model3d,
43352
+ rawData: community.rawData
43353
+ };
43354
+ }
43355
+ async function handleComponent(uuid, res) {
43356
+ if (!uuid) {
43357
+ res.writeHead(400, { "Content-Type": "application/json" });
43358
+ res.end(JSON.stringify({ error: "Missing UUID" }));
43359
+ return;
43360
+ }
43361
+ try {
43362
+ const component2 = await easyedaCommunityClient.getComponent(uuid);
43363
+ if (!component2) {
43364
+ res.writeHead(404, { "Content-Type": "application/json" });
43365
+ res.end(JSON.stringify({ error: "Component not found" }));
43366
+ return;
43367
+ }
43368
+ const componentData = communityToComponentData(component2);
43369
+ let symbolSexpr = "";
43370
+ let footprintSexpr = "";
43371
+ try {
43372
+ symbolSexpr = symbolConverter.convertToSymbolEntry(componentData);
43373
+ } catch (e) {
43374
+ logger7.warn("Symbol conversion failed:", e);
43375
+ }
43376
+ try {
43377
+ footprintSexpr = footprintConverter.convert(componentData);
43378
+ } catch (e) {
43379
+ logger7.warn("Footprint conversion failed:", e);
43380
+ }
43381
+ res.writeHead(200, { "Content-Type": "application/json" });
43382
+ res.end(JSON.stringify({
43383
+ uuid: component2.uuid,
43384
+ title: component2.title,
43385
+ description: component2.description,
43386
+ owner: component2.owner,
43387
+ symbolSexpr,
43388
+ footprintSexpr,
43389
+ model3d: component2.model3d
43390
+ }));
43391
+ } catch (error) {
43392
+ logger7.error("Component fetch error:", error);
43393
+ res.writeHead(500, { "Content-Type": "application/json" });
43394
+ res.end(JSON.stringify({ error: "Failed to fetch component" }));
43395
+ }
43396
+ }
43397
+ var logger8 = createLogger("http-server");
43398
+ var DEFAULT_PORT = 3847;
43399
+ var serverInstance = null;
43400
+ function startHttpServer(options = {}) {
43401
+ if (serverInstance) {
43402
+ logger8.debug("HTTP server already running");
43403
+ const port2 = options.port ?? parseInt(process.env.JLC_MCP_HTTP_PORT || String(DEFAULT_PORT), 10);
43404
+ options.onReady?.(`http://localhost:${port2}`);
43405
+ return port2;
43406
+ }
43407
+ const port = options.port ?? parseInt(process.env.JLC_MCP_HTTP_PORT || String(DEFAULT_PORT), 10);
43408
+ serverInstance = createServer(async (req, res) => {
43409
+ res.setHeader("Access-Control-Allow-Origin", "*");
43410
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
43411
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
43412
+ if (req.method === "OPTIONS") {
43413
+ res.writeHead(204);
43414
+ res.end();
43415
+ return;
43416
+ }
43417
+ try {
43418
+ await handleRequest(req, res);
43419
+ } catch (error) {
43420
+ logger8.error("Request error:", error);
43421
+ res.writeHead(500, { "Content-Type": "application/json" });
43422
+ res.end(JSON.stringify({ error: "Internal server error" }));
43423
+ }
43424
+ });
43425
+ serverInstance.listen(port, () => {
43426
+ const url = `http://localhost:${port}`;
43427
+ logger8.info(`HTTP server listening on ${url}`);
43428
+ options.onReady?.(url);
43429
+ });
43430
+ serverInstance.on("error", (error) => {
43431
+ if (error.code === "EADDRINUSE") {
43432
+ logger8.warn(`Port ${port} already in use, HTTP server not started`);
43433
+ } else {
43434
+ logger8.error("HTTP server error:", error);
43435
+ }
43436
+ });
43437
+ return port;
43438
+ }
43439
+ function stopHttpServer() {
43440
+ if (serverInstance) {
43441
+ serverInstance.close();
43442
+ serverInstance = null;
43443
+ logger8.info("HTTP server stopped");
43444
+ }
43445
+ }
43446
+ var DEFAULT_COMPARISON_OPTIONS = {
43447
+ positionTolerance: 0.05,
43448
+ sizeTolerance: 0.02,
43449
+ holeTolerance: 0.02,
43450
+ sizeWarningsOnly: false,
43451
+ ignoreExtraPads: false,
43452
+ ignorePinNames: false
43453
+ };
43454
+ var EE_TO_MM3 = 10 * 0.0254;
43455
+ async function fetchReferenceSVG(lcscCode) {
43456
+ const normalizedCode = lcscCode.replace(/^C/i, "");
43457
+ const fullCode = `C${normalizedCode}`;
43458
+ const url = `https://easyeda.com/api/products/${fullCode}/svgs`;
43459
+ const response = await fetch(url, {
43460
+ headers: {
43461
+ Accept: "application/json",
43462
+ "User-Agent": "jlc-cli/1.0"
43463
+ }
43464
+ });
43465
+ if (!response.ok) {
43466
+ throw new Error(`Failed to fetch SVG for ${fullCode}: ${response.status} ${response.statusText}`);
43467
+ }
43468
+ const data = await response.json();
43469
+ if (!data.success || !data.result) {
43470
+ throw new Error(`API error for ${fullCode}: ${JSON.stringify(data)}`);
43471
+ }
43472
+ const symbol = data.result.find((r) => r.docType === 2);
43473
+ const footprint = data.result.find((r) => r.docType === 4);
43474
+ return {
43475
+ lcscCode: fullCode,
43476
+ symbolSvg: symbol?.svg || null,
43477
+ footprintSvg: footprint?.svg || null,
43478
+ componentUuid: footprint?.component_uuid || symbol?.component_uuid || null,
43479
+ bbox: footprint?.bbox || symbol?.bbox
43480
+ };
43481
+ }
43482
+ var PASSIVES = [
43483
+ {
43484
+ lcsc: "C25076",
43485
+ name: "100nF 0402 Capacitor",
43486
+ features: ["smd"],
43487
+ expectedPadCount: 2,
43488
+ notes: "Standard 0402 - should use KiCad built-in"
43489
+ },
43490
+ {
43491
+ lcsc: "C25111",
43492
+ name: "10K 0603 Resistor",
43493
+ features: ["smd"],
43494
+ expectedPadCount: 2,
43495
+ notes: "Standard 0603 - should use KiCad built-in"
43496
+ },
43497
+ {
43498
+ lcsc: "C1525",
43499
+ name: "10uF 0805 Capacitor",
43500
+ features: ["smd"],
43501
+ expectedPadCount: 2,
43502
+ notes: "Standard 0805 - should use KiCad built-in"
43503
+ }
43504
+ ];
43505
+ var THERMAL_PADS = [
43506
+ {
43507
+ lcsc: "C22467599",
43508
+ name: "MicroSD Socket A-MicroTF-1.85A",
43509
+ features: ["smd", "shield"],
43510
+ notes: "SD card socket with shield tabs"
43511
+ },
43512
+ {
43513
+ lcsc: "C94919",
43514
+ name: "ESP32-WROOM-32E",
43515
+ features: ["thermal", "vias", "castellated", "antenna"],
43516
+ notes: "WiFi module with large ground pad and vias"
43517
+ },
43518
+ {
43519
+ lcsc: "C2040",
43520
+ name: "ATmega328P-MU QFN-32",
43521
+ features: ["qfn", "thermal"],
43522
+ notes: "QFN with exposed thermal pad"
43523
+ }
43524
+ ];
43525
+ var CASTELLATED = [
43526
+ {
43527
+ lcsc: "C3029878",
43528
+ name: "ESP32-C3-MINI-1",
43529
+ features: ["castellated", "antenna"],
43530
+ skip: true,
43531
+ notes: "Compact WiFi/BLE module with castellated edges"
43532
+ },
43533
+ {
43534
+ lcsc: "C82891",
43535
+ name: "ESP-12F",
43536
+ features: ["castellated", "antenna"],
43537
+ notes: "Classic ESP8266 module"
43538
+ },
43539
+ {
43540
+ lcsc: "C2838283",
43541
+ name: "RP2040",
43542
+ features: ["qfn", "thermal"],
43543
+ skip: true,
43544
+ notes: "Raspberry Pi microcontroller - QFN-56"
43545
+ }
43546
+ ];
43547
+ var CONNECTORS = [
43548
+ {
43549
+ lcsc: "C165948",
43550
+ name: "USB-C Receptacle TYPE-C-31-M-12",
43551
+ features: ["smd", "shield", "complex_pads"],
43552
+ notes: "USB Type-C with shield tabs and SMD pads"
43553
+ },
43554
+ {
43555
+ lcsc: "C46398",
43556
+ name: "Micro USB B TYPE-C-31-M-17",
43557
+ features: ["smd", "shield"],
43558
+ notes: "Micro USB with SMD mounting"
43559
+ },
43560
+ {
43561
+ lcsc: "C2939982",
43562
+ name: "RJ45 with Magnetics HR911105A",
43563
+ features: ["tht", "leds", "shield"],
43564
+ skip: true,
43565
+ notes: "Ethernet jack with integrated magnetics"
43566
+ },
43567
+ {
43568
+ lcsc: "C91754",
43569
+ name: "RJ45 Socket",
43570
+ features: ["tht", "shield"],
43571
+ notes: "Standard RJ45 ethernet jack"
43572
+ }
43573
+ ];
43574
+ var THROUGH_HOLE = [
43575
+ {
43576
+ lcsc: "C2062",
43577
+ name: "LM7805 TO-220",
43578
+ features: ["tht", "tab"],
43579
+ skip: true,
43580
+ notes: "Classic TO-220 voltage regulator"
43581
+ },
43582
+ {
43583
+ lcsc: "C88305",
43584
+ name: "Pin Header 2.54mm 1x10",
43585
+ features: ["tht", "high_pin_count"],
43586
+ skip: true,
43587
+ notes: 'Standard 0.1" pin header'
43588
+ },
43589
+ {
43590
+ lcsc: "C2337",
43591
+ name: "Electrolytic Cap 100uF 16V",
43592
+ features: ["tht"],
43593
+ notes: "Radial electrolytic capacitor"
43594
+ }
43595
+ ];
43596
+ var QFN_QFP = [
43597
+ {
43598
+ lcsc: "C14902",
43599
+ name: "STM32F103C8T6 LQFP-48",
43600
+ features: ["qfp", "fine_pitch", "high_pin_count"],
43601
+ skip: true,
43602
+ notes: "Popular STM32 - 0.5mm pitch LQFP"
43603
+ },
43604
+ {
43605
+ lcsc: "C8734",
43606
+ name: "CH340G SOP-16",
43607
+ features: ["smd"],
43608
+ notes: "USB-UART bridge IC"
43609
+ },
43610
+ {
43611
+ lcsc: "C2843335",
43612
+ name: "STM32G071GBU6 UFQFPN-28",
43613
+ features: ["qfn", "thermal", "vias"],
43614
+ notes: "STM32G0 with thermal vias - already tested"
43615
+ }
43616
+ ];
43617
+ var POWER = [
43618
+ {
43619
+ lcsc: "C78244",
43620
+ name: "MP1584 Buck Converter",
43621
+ features: ["smd", "thermal"],
43622
+ skip: true,
43623
+ notes: "DC-DC converter with thermal pad"
43624
+ },
43625
+ {
43626
+ lcsc: "C32767",
43627
+ name: "AMS1117-3.3 SOT-223",
43628
+ features: ["smd", "tab"],
43629
+ skip: true,
43630
+ notes: "LDO with tab pad"
43631
+ },
43632
+ {
43633
+ lcsc: "C155580",
43634
+ name: "IRF540N TO-220",
43635
+ features: ["tht", "tab"],
43636
+ notes: "Power MOSFET - TO-220"
43637
+ }
43638
+ ];
43639
+ var SPECIALTY = [
43640
+ {
43641
+ lcsc: "C88093",
43642
+ name: "WS2812B-V5 LED",
43643
+ features: ["smd"],
43644
+ skip: true,
43645
+ notes: "RGB LED with integrated driver"
43646
+ },
43647
+ {
43648
+ lcsc: "C82942",
43649
+ name: "BME280 Sensor",
43650
+ features: ["smd", "fine_pitch"],
43651
+ notes: "Environmental sensor - tiny LGA"
43652
+ },
43653
+ {
43654
+ lcsc: "C14879",
43655
+ name: "Crystal 8MHz 3215",
43656
+ features: ["smd"],
43657
+ notes: "SMD crystal oscillator"
43658
+ }
43659
+ ];
43660
+ var TEST_CATEGORIES = [
43661
+ {
43662
+ name: "passives",
43663
+ description: "Standard passive components (R, C, L)",
43664
+ components: PASSIVES
43665
+ },
43666
+ {
43667
+ name: "thermal",
43668
+ description: "Components with thermal pads and/or vias",
43669
+ components: THERMAL_PADS
43670
+ },
43671
+ {
43672
+ name: "castellated",
43673
+ description: "Modules with castellated edge pads",
43674
+ components: CASTELLATED
43675
+ },
43676
+ {
43677
+ name: "connectors",
43678
+ description: "USB, SD card, and other connectors",
43679
+ components: CONNECTORS
43680
+ },
43681
+ {
43682
+ name: "throughHole",
43683
+ description: "Through-hole components",
43684
+ components: THROUGH_HOLE
43685
+ },
43686
+ {
43687
+ name: "qfn",
43688
+ description: "QFN, QFP, and fine-pitch ICs",
43689
+ components: QFN_QFP
43690
+ },
43691
+ {
43692
+ name: "power",
43693
+ description: "Power components with thermal features",
43694
+ components: POWER
43695
+ },
43696
+ {
43697
+ name: "specialty",
43698
+ description: "LEDs, sensors, and other specialty parts",
43699
+ components: SPECIALTY
43700
+ }
43701
+ ];
43702
+ function getAllTestComponents() {
43703
+ return TEST_CATEGORIES.flatMap((cat) => cat.components);
43704
+ }
43705
+ function getTestComponentsByCategory(categoryName) {
43706
+ const category = TEST_CATEGORIES.find((cat) => cat.name === categoryName);
43707
+ return category?.components || [];
43708
+ }
43709
+ function getCategoryNames() {
43710
+ return TEST_CATEGORIES.map((cat) => cat.name);
43711
+ }
43712
+ var EE_TO_MM4 = 10 * 0.0254;
43713
+ function extractFromReferenceSVG(svg) {
43714
+ const rawPads = [];
43715
+ const rawVias = [];
43716
+ const rawHoles = [];
43717
+ const padRegex = /<g[^>]*c_partid="part_pad"[^>]*>/g;
43718
+ let match;
43719
+ while ((match = padRegex.exec(svg)) !== null) {
43720
+ const groupTag = match[0];
43721
+ const endIndex = findGroupEnd(svg, match.index);
43722
+ const groupContent = svg.slice(match.index, endIndex);
43723
+ const pad = parsePadGroup(groupTag, groupContent);
43724
+ if (pad) {
43725
+ rawPads.push(pad);
43726
+ }
43727
+ }
43728
+ const viaRegex = /<g[^>]*c_partid="part_via"[^>]*>/g;
43729
+ while ((match = viaRegex.exec(svg)) !== null) {
43730
+ const groupTag = match[0];
43731
+ const endIndex = findGroupEnd(svg, match.index);
43732
+ const groupContent = svg.slice(match.index, endIndex);
43733
+ const via = parseViaGroup(groupTag, groupContent);
43734
+ if (via) {
43735
+ rawVias.push(via);
43736
+ }
43737
+ }
43738
+ const holeRegex = /<g[^>]*c_partid="part_hole"[^>]*>/g;
43739
+ while ((match = holeRegex.exec(svg)) !== null) {
43740
+ const groupTag = match[0];
43741
+ const endIndex = findGroupEnd(svg, match.index);
43742
+ const groupContent = svg.slice(match.index, endIndex);
43743
+ const hole = parseHoleGroup(groupTag, groupContent);
43744
+ if (hole) {
43745
+ rawHoles.push(hole);
43746
+ }
43747
+ }
43748
+ const centroid = calculateBoundingBoxCenter(rawPads);
43749
+ const pads = rawPads.map((p) => ({
43750
+ ...p,
43751
+ x: roundTo2(p.x - centroid.x, 4),
43752
+ y: roundTo2(p.y - centroid.y, 4)
43753
+ }));
43754
+ const vias = rawVias.map((v) => ({
43755
+ ...v,
43756
+ x: roundTo2(v.x - centroid.x, 4),
43757
+ y: roundTo2(v.y - centroid.y, 4)
43758
+ }));
43759
+ const holes = rawHoles.map((h) => ({
43760
+ ...h,
43761
+ x: roundTo2(h.x - centroid.x, 4),
43762
+ y: roundTo2(h.y - centroid.y, 4)
43763
+ }));
43764
+ const allPoints = [
43765
+ ...pads.map((p) => ({ x: p.x, y: p.y })),
43766
+ ...vias.map((v) => ({ x: v.x, y: v.y })),
43767
+ ...holes.map((h) => ({ x: h.x, y: h.y }))
43768
+ ];
43769
+ const bounds = calculateBounds(allPoints);
43770
+ return { pads, vias, holes, bounds };
43771
+ }
43772
+ function calculateBoundingBoxCenter(pads) {
43773
+ if (pads.length === 0) {
43774
+ return { x: 0, y: 0 };
43775
+ }
43776
+ let minX = Infinity, maxX = -Infinity;
43777
+ let minY = Infinity, maxY = -Infinity;
43778
+ for (const pad of pads) {
43779
+ const hw = pad.width / 2;
43780
+ const hh = pad.height / 2;
43781
+ minX = Math.min(minX, pad.x - hw);
43782
+ maxX = Math.max(maxX, pad.x + hw);
43783
+ minY = Math.min(minY, pad.y - hh);
43784
+ maxY = Math.max(maxY, pad.y + hh);
43785
+ }
43786
+ return {
43787
+ x: (minX + maxX) / 2,
43788
+ y: (minY + maxY) / 2
43789
+ };
43790
+ }
43791
+ function roundTo2(value, decimals) {
43792
+ const factor = Math.pow(10, decimals);
43793
+ return Math.round(value * factor) / factor;
43794
+ }
43795
+ function findGroupEnd(svg, startIndex) {
43796
+ let depth = 0;
43797
+ let i = startIndex;
43798
+ while (i < svg.length) {
43799
+ if (svg.slice(i, i + 2) === "<g") {
43800
+ depth++;
43801
+ i += 2;
43802
+ } else if (svg.slice(i, i + 4) === "</g>") {
43803
+ depth--;
43804
+ if (depth === 0) {
43805
+ return i + 4;
43806
+ }
43807
+ i += 4;
43808
+ } else {
43809
+ i++;
43810
+ }
43811
+ }
43812
+ return svg.length;
43813
+ }
43814
+ function parsePadGroup(groupTag, groupContent) {
43815
+ const numberMatch = groupTag.match(/number="([^"]+)"/);
43816
+ const number = numberMatch?.[1] || "";
43817
+ const originMatch = groupTag.match(/c_origin="([^"]+)"/);
43818
+ if (!originMatch)
43819
+ return null;
43820
+ const [xStr, yStr] = originMatch[1].split(",");
43821
+ const x = parseFloat(xStr) * EE_TO_MM4;
43822
+ const y = parseFloat(yStr) * EE_TO_MM4;
43823
+ const layerMatch = groupTag.match(/layerid="(\d+)"/);
43824
+ const layerId = layerMatch ? parseInt(layerMatch[1], 10) : 1;
43825
+ const { shape, width, height, hasHole, holeRadius, rotation } = extractPadShape(groupContent);
43826
+ return {
43827
+ number,
43828
+ x,
43829
+ y,
43830
+ width,
43831
+ height,
43832
+ shape,
43833
+ layerId,
43834
+ hasHole,
43835
+ holeRadius,
43836
+ rotation
43837
+ };
43838
+ }
43839
+ function extractPadShape(content) {
43840
+ let shape = "rect";
43841
+ let width = 0;
43842
+ let height = 0;
43843
+ let hasHole = false;
43844
+ let holeRadius;
43845
+ let rotation;
43846
+ const groupWidthMatch = content.match(/c_width="([^"]+)"/);
43847
+ const groupHeightMatch = content.match(/c_height="([^"]+)"/);
43848
+ if (groupWidthMatch && groupHeightMatch) {
43849
+ width = parseFloat(groupWidthMatch[1]) * EE_TO_MM4;
43850
+ height = parseFloat(groupHeightMatch[1]) * EE_TO_MM4;
43851
+ }
43852
+ const shapeAttrMatch = content.match(/c_shape="([^"]+)"/);
43853
+ if (shapeAttrMatch) {
43854
+ const shapeAttr = shapeAttrMatch[1].toUpperCase();
43855
+ if (shapeAttr === "RECT")
43856
+ shape = "rect";
43857
+ else if (shapeAttr === "OVAL" || shapeAttr === "ELLIPSE")
43858
+ shape = "oval";
43859
+ else if (shapeAttr === "POLYGON")
43860
+ shape = "polygon";
43861
+ else if (shapeAttr === "CIRCLE")
43862
+ shape = "circle";
43863
+ }
43864
+ const rectMatch = content.match(/<rect[^>]*width="([^"]+)"[^>]*height="([^"]+)"[^>]*>/);
43865
+ if (rectMatch) {
43866
+ width = parseFloat(rectMatch[1]) * EE_TO_MM4;
43867
+ height = parseFloat(rectMatch[2]) * EE_TO_MM4;
43868
+ shape = "rect";
43869
+ if (content.match(/<rect[^>]*rx="[^"]+"/)) {
43870
+ shape = "roundrect";
43871
+ }
43872
+ }
43873
+ const circleMatch = content.match(/<circle[^>]*r="([^"]+)"[^>]*>/);
43874
+ if (circleMatch) {
43875
+ const r = parseFloat(circleMatch[1]) * EE_TO_MM4;
43876
+ width = r * 2;
43877
+ height = r * 2;
43878
+ shape = "circle";
43879
+ }
43880
+ const ellipseMatch = content.match(/<ellipse[^>]*rx="([^"]+)"[^>]*ry="([^"]+)"[^>]*>/);
43881
+ if (ellipseMatch) {
43882
+ width = parseFloat(ellipseMatch[1]) * 2 * EE_TO_MM4;
43883
+ height = parseFloat(ellipseMatch[2]) * 2 * EE_TO_MM4;
43884
+ shape = "oval";
43885
+ }
43886
+ if (content.match(/<polygon[^>]*>/) || content.match(/<path[^>]*d="[^"]*[Cc][^"]*"/)) {
43887
+ shape = "polygon";
43888
+ const polyMatch = content.match(/<polygon[^>]*points="([^"]+)"/);
43889
+ if (polyMatch) {
43890
+ const points = polyMatch[1].split(/[\s,]+/).map(parseFloat);
43891
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
43892
+ for (let i = 0;i < points.length; i += 2) {
43893
+ minX = Math.min(minX, points[i]);
43894
+ maxX = Math.max(maxX, points[i]);
43895
+ minY = Math.min(minY, points[i + 1]);
43896
+ maxY = Math.max(maxY, points[i + 1]);
43897
+ }
43898
+ width = (maxX - minX) * EE_TO_MM4;
43899
+ height = (maxY - minY) * EE_TO_MM4;
43900
+ }
43901
+ }
43902
+ const circleHoleMatch = content.match(/<circle[^>]*c_padhole="1"[^>]*r="([^"]+)"[^>]*>/i);
43903
+ if (circleHoleMatch) {
43904
+ const radius = parseFloat(circleHoleMatch[1]);
43905
+ if (radius > 0) {
43906
+ hasHole = true;
43907
+ holeRadius = radius * EE_TO_MM4;
43908
+ }
43909
+ }
43910
+ const polylineHoleMatch = content.match(/<polyline[^>]*c_padhole="1"[^>]*stroke-width="([^"]+)"[^>]*>/i);
43911
+ if (polylineHoleMatch) {
43912
+ const strokeWidth = parseFloat(polylineHoleMatch[1]);
43913
+ if (strokeWidth > 0) {
43914
+ hasHole = true;
43915
+ holeRadius = strokeWidth / 2 * EE_TO_MM4;
43916
+ }
43917
+ }
43918
+ if (!hasHole && content.includes('c_etype="pinhole"')) {
43919
+ hasHole = true;
43920
+ const innerCircle = content.match(/<circle[^>]*fill="[^"]*(?:white|#fff)[^"]*"[^>]*r="([^"]+)"/i);
43921
+ if (innerCircle) {
43922
+ holeRadius = parseFloat(innerCircle[1]) * EE_TO_MM4;
43923
+ }
43924
+ }
43925
+ const transformMatch = content.match(/transform="[^"]*rotate\(([^)]+)\)"/);
43926
+ if (transformMatch) {
43927
+ rotation = parseFloat(transformMatch[1].split(",")[0]);
43928
+ }
43929
+ return { shape, width, height, hasHole, holeRadius, rotation };
43930
+ }
43931
+ function parseViaGroup(groupTag, groupContent) {
43932
+ const originMatch = groupTag.match(/c_origin="([^"]+)"/);
43933
+ if (!originMatch)
43934
+ return null;
43935
+ const [xStr, yStr] = originMatch[1].split(",");
43936
+ const x = parseFloat(xStr) * EE_TO_MM4;
43937
+ const y = parseFloat(yStr) * EE_TO_MM4;
43938
+ const circles = [...groupContent.matchAll(/<circle[^>]*r="([^"]+)"[^>]*>/g)];
43939
+ if (circles.length === 0)
43940
+ return null;
43941
+ const radii = circles.map((m) => parseFloat(m[1]) * EE_TO_MM4);
43942
+ const outerDiameter = Math.max(...radii) * 2;
43943
+ const holeDiameter = Math.min(...radii) * 2;
43944
+ return { x, y, outerDiameter, holeDiameter };
43945
+ }
43946
+ function parseHoleGroup(groupTag, groupContent) {
43947
+ const originMatch = groupTag.match(/c_origin="([^"]+)"/);
43948
+ if (!originMatch)
43949
+ return null;
43950
+ const [xStr, yStr] = originMatch[1].split(",");
43951
+ const x = parseFloat(xStr) * EE_TO_MM4;
43952
+ const y = parseFloat(yStr) * EE_TO_MM4;
43953
+ const circleMatch = groupContent.match(/<circle[^>]*r="([^"]+)"[^>]*>/);
43954
+ if (!circleMatch)
43955
+ return null;
43956
+ const diameter = parseFloat(circleMatch[1]) * 2 * EE_TO_MM4;
43957
+ const plated = groupTag.includes('layerid="11"') || groupTag.includes('c_etype="plated"');
43958
+ return { x, y, diameter, plated };
43959
+ }
43960
+ function extractFromKiCadFootprint(sexp) {
43961
+ const pads = [];
43962
+ const vias = [];
43963
+ const holes = [];
43964
+ const padRegex = /\(pad\s+"([^"]*)"\s+(\w+)\s+(\w+)/g;
43965
+ let match;
43966
+ while ((match = padRegex.exec(sexp)) !== null) {
43967
+ const padStart = match.index;
43968
+ const padEnd = findSexpEnd(sexp, padStart);
43969
+ const padSexp = sexp.slice(padStart, padEnd);
43970
+ const pad = parseKiCadPad(match[1], match[2], match[3], padSexp);
43971
+ if (pad) {
43972
+ if (pad.number === "" && pad.hasHole) {
43973
+ vias.push({
43974
+ x: pad.x,
43975
+ y: pad.y,
43976
+ outerDiameter: pad.width,
43977
+ holeDiameter: pad.holeRadius ? pad.holeRadius * 2 : 0
43978
+ });
43979
+ } else {
43980
+ pads.push(pad);
43981
+ }
43982
+ }
43983
+ }
43984
+ const holeRegex = /\(pad\s+""\s+np_thru_hole/g;
43985
+ while ((match = holeRegex.exec(sexp)) !== null) {
43986
+ const holeStart = match.index;
43987
+ const holeEnd = findSexpEnd(sexp, holeStart);
43988
+ const holeSexp = sexp.slice(holeStart, holeEnd);
43989
+ const hole = parseKiCadHole(holeSexp);
43990
+ if (hole) {
43991
+ holes.push(hole);
43992
+ }
43993
+ }
43994
+ const allPoints = [
43995
+ ...pads.map((p) => ({ x: p.x, y: p.y })),
43996
+ ...vias.map((v) => ({ x: v.x, y: v.y })),
43997
+ ...holes.map((h) => ({ x: h.x, y: h.y }))
43998
+ ];
43999
+ const bounds = calculateBounds(allPoints);
44000
+ return { pads, vias, holes, bounds };
44001
+ }
44002
+ function findSexpEnd(sexp, startIndex) {
44003
+ let depth = 0;
44004
+ let i = startIndex;
44005
+ while (i < sexp.length) {
44006
+ if (sexp[i] === "(") {
44007
+ depth++;
44008
+ } else if (sexp[i] === ")") {
44009
+ depth--;
44010
+ if (depth === 0) {
44011
+ return i + 1;
44012
+ }
44013
+ }
44014
+ i++;
44015
+ }
44016
+ return sexp.length;
44017
+ }
44018
+ function parseKiCadPad(number, padType, shape, sexp) {
44019
+ const atMatch = sexp.match(/\(at\s+([\d.-]+)\s+([\d.-]+)(?:\s+([\d.-]+))?\)/);
44020
+ if (!atMatch)
44021
+ return null;
44022
+ const x = parseFloat(atMatch[1]);
44023
+ const y = parseFloat(atMatch[2]);
44024
+ const rotation = atMatch[3] ? parseFloat(atMatch[3]) : 0;
44025
+ const sizeMatch = sexp.match(/\(size\s+([\d.-]+)\s+([\d.-]+)\)/);
44026
+ if (!sizeMatch)
44027
+ return null;
44028
+ const width = parseFloat(sizeMatch[1]);
44029
+ const height = parseFloat(sizeMatch[2]);
44030
+ let layerId = 1;
44031
+ if (sexp.includes("B.Cu")) {
44032
+ layerId = 2;
44033
+ }
44034
+ if (sexp.includes("*.Cu")) {
44035
+ layerId = 11;
44036
+ }
44037
+ const drillMatch = sexp.match(/\(drill\s+([\d.-]+)/);
44038
+ const hasHole = padType === "thru_hole" || padType === "np_thru_hole" || drillMatch !== null;
44039
+ const holeRadius = drillMatch ? parseFloat(drillMatch[1]) / 2 : undefined;
44040
+ const shapeMap = {
44041
+ rect: "rect",
44042
+ circle: "circle",
44043
+ oval: "oval",
44044
+ roundrect: "roundrect",
44045
+ custom: "custom",
44046
+ polygon: "polygon"
44047
+ };
44048
+ return {
44049
+ number,
44050
+ x,
44051
+ y,
44052
+ width,
44053
+ height,
44054
+ shape: shapeMap[shape] || "rect",
44055
+ layerId,
44056
+ hasHole,
44057
+ holeRadius,
44058
+ rotation: rotation !== 0 ? rotation : undefined
44059
+ };
44060
+ }
44061
+ function parseKiCadHole(sexp) {
44062
+ const atMatch = sexp.match(/\(at\s+([\d.-]+)\s+([\d.-]+)/);
44063
+ const drillMatch = sexp.match(/\(drill\s+([\d.-]+)/);
44064
+ if (!atMatch || !drillMatch)
44065
+ return null;
44066
+ return {
44067
+ x: parseFloat(atMatch[1]),
44068
+ y: parseFloat(atMatch[2]),
44069
+ diameter: parseFloat(drillMatch[1]),
44070
+ plated: !sexp.includes("np_thru_hole")
44071
+ };
44072
+ }
44073
+ function areShapesEquivalent(shape1, shape2) {
44074
+ if (shape1 === shape2)
44075
+ return true;
44076
+ const rectangularShapes = ["polygon", "rect", "roundrect"];
44077
+ const roundShapes = ["circle", "oval"];
44078
+ if (rectangularShapes.includes(shape1) && rectangularShapes.includes(shape2)) {
44079
+ return true;
44080
+ }
44081
+ if (roundShapes.includes(shape1) && roundShapes.includes(shape2)) {
44082
+ return true;
44083
+ }
44084
+ return false;
44085
+ }
44086
+ function compareFootprints(reference, generated, options = {}) {
44087
+ const opts = { ...DEFAULT_COMPARISON_OPTIONS, ...options };
44088
+ const diffs = [];
44089
+ const padCountMatch = reference.pads.length === generated.pads.length;
44090
+ const viaCountMatch = reference.vias.length === generated.vias.length;
44091
+ const holeCountMatch = reference.holes.length === generated.holes.length;
44092
+ const usedGenIndices = new Set;
44093
+ for (const refPad of reference.pads) {
44094
+ const match = findMatchingPad(refPad, generated.pads, usedGenIndices, opts.positionTolerance);
44095
+ if (!match) {
44096
+ diffs.push({
44097
+ padNumber: refPad.number,
44098
+ field: "missing",
44099
+ severity: "error",
44100
+ message: `Pad ${refPad.number} missing in generated output`
44101
+ });
44102
+ continue;
44103
+ }
44104
+ const genPad = match.pad;
44105
+ usedGenIndices.add(match.index);
44106
+ const dx = Math.abs(refPad.x - genPad.x);
44107
+ const dy = Math.abs(refPad.y - genPad.y);
44108
+ if (dx > opts.positionTolerance || dy > opts.positionTolerance) {
44109
+ diffs.push({
44110
+ padNumber: refPad.number,
44111
+ field: "position",
44112
+ severity: "error",
44113
+ message: `Position differs: expected (${refPad.x.toFixed(3)}, ${refPad.y.toFixed(3)}), got (${genPad.x.toFixed(3)}, ${genPad.y.toFixed(3)})`,
44114
+ expected: `(${refPad.x.toFixed(3)}, ${refPad.y.toFixed(3)})`,
44115
+ actual: `(${genPad.x.toFixed(3)}, ${genPad.y.toFixed(3)})`
44116
+ });
44117
+ }
44118
+ const dw = Math.abs(refPad.width - genPad.width);
44119
+ const dh = Math.abs(refPad.height - genPad.height);
44120
+ const dwSwapped = Math.abs(refPad.width - genPad.height);
44121
+ const dhSwapped = Math.abs(refPad.height - genPad.width);
44122
+ const sizeMatches = dw <= opts.sizeTolerance && dh <= opts.sizeTolerance || dwSwapped <= opts.sizeTolerance && dhSwapped <= opts.sizeTolerance;
44123
+ if (!sizeMatches) {
44124
+ const posInfo = ` at (${refPad.x.toFixed(2)}, ${refPad.y.toFixed(2)})`;
44125
+ diffs.push({
44126
+ padNumber: refPad.number,
44127
+ field: "size",
44128
+ severity: opts.sizeWarningsOnly ? "warning" : "error",
44129
+ message: `Size differs${posInfo}: expected ${refPad.width.toFixed(3)}x${refPad.height.toFixed(3)}mm, got ${genPad.width.toFixed(3)}x${genPad.height.toFixed(3)}mm`,
44130
+ expected: `${refPad.width.toFixed(3)}x${refPad.height.toFixed(3)}`,
44131
+ actual: `${genPad.width.toFixed(3)}x${genPad.height.toFixed(3)}`
44132
+ });
44133
+ }
44134
+ if (!areShapesEquivalent(refPad.shape, genPad.shape)) {
44135
+ diffs.push({
44136
+ padNumber: refPad.number,
44137
+ field: "shape",
44138
+ severity: "warning",
44139
+ message: `Shape differs: expected ${refPad.shape}, got ${genPad.shape}`,
44140
+ expected: refPad.shape,
44141
+ actual: genPad.shape
44142
+ });
44143
+ }
44144
+ if (refPad.hasHole !== genPad.hasHole) {
44145
+ diffs.push({
44146
+ padNumber: refPad.number,
44147
+ field: "hole",
44148
+ severity: "error",
44149
+ message: `Hole mismatch: expected ${refPad.hasHole ? "THT" : "SMD"}, got ${genPad.hasHole ? "THT" : "SMD"}`
44150
+ });
44151
+ } else if (refPad.hasHole && refPad.holeRadius && genPad.holeRadius) {
44152
+ const drillDiff = Math.abs(refPad.holeRadius - genPad.holeRadius);
44153
+ if (drillDiff > opts.holeTolerance) {
44154
+ diffs.push({
44155
+ padNumber: refPad.number,
44156
+ field: "hole",
44157
+ severity: "warning",
44158
+ message: `Hole size differs: expected ø${(refPad.holeRadius * 2).toFixed(3)}mm, got ø${(genPad.holeRadius * 2).toFixed(3)}mm`,
44159
+ expected: (refPad.holeRadius * 2).toFixed(3),
44160
+ actual: (genPad.holeRadius * 2).toFixed(3)
44161
+ });
44162
+ }
44163
+ }
44164
+ }
44165
+ if (!opts.ignoreExtraPads) {
44166
+ for (let i = 0;i < generated.pads.length; i++) {
44167
+ if (usedGenIndices.has(i))
44168
+ continue;
44169
+ const genPad = generated.pads[i];
44170
+ const matchByPos = reference.pads.find((p) => Math.abs(p.x - genPad.x) < opts.positionTolerance && Math.abs(p.y - genPad.y) < opts.positionTolerance);
44171
+ if (!matchByPos) {
44172
+ diffs.push({
44173
+ padNumber: genPad.number,
44174
+ field: "extra",
44175
+ severity: "warning",
44176
+ message: `Extra pad ${genPad.number} in generated output at (${genPad.x.toFixed(3)}, ${genPad.y.toFixed(3)})`
44177
+ });
44178
+ }
44179
+ }
44180
+ }
44181
+ const errors2 = diffs.filter((d) => d.severity === "error");
44182
+ const warnings = diffs.filter((d) => d.severity === "warning");
44183
+ const passed = errors2.length === 0 && padCountMatch;
44184
+ return {
44185
+ passed,
44186
+ padCountMatch,
44187
+ viaCountMatch,
44188
+ holeCountMatch,
44189
+ referencePadCount: reference.pads.length,
44190
+ generatedPadCount: generated.pads.length,
44191
+ referenceViaCount: reference.vias.length,
44192
+ generatedViaCount: generated.vias.length,
44193
+ diffs,
44194
+ errors: errors2,
44195
+ warnings
44196
+ };
44197
+ }
44198
+ function findMatchingPad(refPad, generatedPads, usedIndices, tolerance) {
44199
+ const candidates = [];
44200
+ for (let i = 0;i < generatedPads.length; i++) {
44201
+ if (usedIndices.has(i))
44202
+ continue;
44203
+ const p = generatedPads[i];
44204
+ if (p.number === refPad.number) {
44205
+ const distance = Math.sqrt((p.x - refPad.x) ** 2 + (p.y - refPad.y) ** 2);
44206
+ candidates.push({ pad: p, index: i, distance });
44207
+ }
44208
+ }
44209
+ if (candidates.length > 0) {
44210
+ candidates.sort((a, b) => a.distance - b.distance);
44211
+ return { pad: candidates[0].pad, index: candidates[0].index };
44212
+ }
44213
+ for (let i = 0;i < generatedPads.length; i++) {
44214
+ if (usedIndices.has(i))
44215
+ continue;
44216
+ const p = generatedPads[i];
44217
+ if (Math.abs(p.x - refPad.x) < tolerance && Math.abs(p.y - refPad.y) < tolerance) {
44218
+ return { pad: p, index: i };
44219
+ }
44220
+ }
44221
+ return null;
44222
+ }
44223
+ function calculateBounds(points) {
44224
+ if (points.length === 0) {
44225
+ return { minX: 0, maxX: 0, minY: 0, maxY: 0 };
44226
+ }
44227
+ return {
44228
+ minX: Math.min(...points.map((p) => p.x)),
44229
+ maxX: Math.max(...points.map((p) => p.x)),
44230
+ minY: Math.min(...points.map((p) => p.y)),
44231
+ maxY: Math.max(...points.map((p) => p.y))
44232
+ };
44233
+ }
44234
+ function formatComparisonResult(result) {
44235
+ const lines = [];
44236
+ const status = result.passed ? "✓ PASS" : "✗ FAIL";
44237
+ lines.push(`Footprint Comparison: ${status}`);
44238
+ lines.push("");
44239
+ lines.push(` Pad count: ${result.generatedPadCount}/${result.referencePadCount} ${result.padCountMatch ? "✓" : "✗"}`);
44240
+ lines.push(` Via count: ${result.generatedViaCount}/${result.referenceViaCount} ${result.viaCountMatch ? "✓" : "✗"}`);
44241
+ lines.push("");
44242
+ if (result.errors.length > 0) {
44243
+ lines.push(` Errors (${result.errors.length}):`);
44244
+ for (const err of result.errors) {
44245
+ lines.push(` ✗ Pad ${err.padNumber}: ${err.message}`);
44246
+ }
44247
+ lines.push("");
44248
+ }
44249
+ if (result.warnings.length > 0) {
44250
+ lines.push(` Warnings (${result.warnings.length}):`);
44251
+ for (const warn of result.warnings) {
44252
+ lines.push(` ⚠ Pad ${warn.padNumber}: ${warn.message}`);
44253
+ }
44254
+ }
44255
+ return lines.join(`
44256
+ `);
44257
+ }
44258
+ var EE_TO_MM5 = 10 * 0.0254;
44259
+ var MIL_TO_MM2 = 0.0254;
44260
+ function extractFromReferenceSVG2(svg) {
44261
+ const rawPins = [];
44262
+ const pinRegex = /<g[^>]*c_partid="part_pin"[^>]*>/g;
44263
+ let match;
44264
+ while ((match = pinRegex.exec(svg)) !== null) {
44265
+ const groupTag = match[0];
44266
+ const endIndex = findGroupEnd2(svg, match.index);
44267
+ const groupContent = svg.slice(match.index, endIndex);
44268
+ const pin = parsePinGroup(groupTag, groupContent);
44269
+ if (pin) {
44270
+ rawPins.push(pin);
44271
+ }
44272
+ }
44273
+ const centroid = calculateCentroid(rawPins.map((p) => ({ x: p.x, y: p.y })));
44274
+ const pins = rawPins.map((p) => ({
44275
+ ...p,
44276
+ x: roundTo3(p.x - centroid.x, 4),
44277
+ y: roundTo3(p.y - centroid.y, 4)
44278
+ }));
44279
+ const bounds = calculateBounds2(pins.map((p) => ({ x: p.x, y: p.y })));
44280
+ return { pins, bounds };
44281
+ }
44282
+ function findGroupEnd2(svg, startIndex) {
44283
+ let depth = 0;
44284
+ let i = startIndex;
44285
+ while (i < svg.length) {
44286
+ if (svg.slice(i, i + 2) === "<g") {
44287
+ depth++;
44288
+ i += 2;
44289
+ } else if (svg.slice(i, i + 4) === "</g>") {
44290
+ depth--;
44291
+ if (depth === 0) {
44292
+ return i + 4;
44293
+ }
44294
+ i += 4;
44295
+ } else {
44296
+ i++;
44297
+ }
44298
+ }
44299
+ return svg.length;
44300
+ }
44301
+ function parsePinGroup(groupTag, groupContent) {
44302
+ const spicepinMatch = groupTag.match(/c_spicepin="([^"]+)"/);
44303
+ const numberMatch = groupTag.match(/number="([^"]+)"/);
44304
+ const number = spicepinMatch?.[1] || numberMatch?.[1] || "";
44305
+ const textMatch = groupContent.match(/<text[^>]*>([^<]*)<\/text>/);
44306
+ const name = textMatch?.[1]?.trim() || number;
44307
+ const originMatch = groupTag.match(/c_origin="([^"]+)"/);
44308
+ if (!originMatch)
44309
+ return null;
44310
+ const [xStr, yStr] = originMatch[1].split(",");
44311
+ const x = parseFloat(xStr) * EE_TO_MM5;
44312
+ const y = -parseFloat(yStr) * EE_TO_MM5;
44313
+ const rotMatch = groupTag.match(/c_rotation="([^"]+)"/);
44314
+ const rotation = rotMatch ? parseFloat(rotMatch[1]) : 0;
44315
+ const etypeMatch = groupTag.match(/c_etype="([^"]+)"/);
44316
+ const electrical = mapElectricalType(etypeMatch?.[1] || "");
44317
+ return {
44318
+ number,
44319
+ name,
44320
+ x,
44321
+ y,
44322
+ rotation,
44323
+ electrical
44324
+ };
44325
+ }
44326
+ function mapElectricalType(eeType) {
44327
+ const typeMap = {
44328
+ input: "input",
44329
+ output: "output",
44330
+ bi: "bidirectional",
44331
+ tristate: "tri_state",
44332
+ passive: "passive",
44333
+ power: "power_in",
44334
+ power_in: "power_in",
44335
+ power_out: "power_out",
44336
+ open_collector: "open_collector",
44337
+ open_emitter: "open_emitter",
44338
+ unconnected: "no_connect",
44339
+ nc: "no_connect"
44340
+ };
44341
+ return typeMap[eeType.toLowerCase()] || "unspecified";
44342
+ }
44343
+ function extractFromKiCadSymbol(sexp) {
44344
+ const pins = [];
44345
+ const pinRegex = /\(pin\s+(\w+)\s+(\w+)/g;
44346
+ let match;
44347
+ while ((match = pinRegex.exec(sexp)) !== null) {
44348
+ const pinStart = match.index;
44349
+ const pinEnd = findSexpEnd2(sexp, pinStart);
44350
+ const pinSexp = sexp.slice(pinStart, pinEnd);
44351
+ const pin = parseKiCadPin(match[1], match[2], pinSexp);
44352
+ if (pin) {
44353
+ pins.push(pin);
44354
+ }
44355
+ }
44356
+ const bounds = calculateBounds2(pins.map((p) => ({ x: p.x, y: p.y })));
44357
+ return { pins, bounds };
44358
+ }
44359
+ function findSexpEnd2(sexp, startIndex) {
44360
+ let depth = 0;
44361
+ let i = startIndex;
44362
+ while (i < sexp.length) {
44363
+ if (sexp[i] === "(") {
44364
+ depth++;
44365
+ } else if (sexp[i] === ")") {
44366
+ depth--;
44367
+ if (depth === 0) {
44368
+ return i + 1;
44369
+ }
44370
+ }
44371
+ i++;
44372
+ }
44373
+ return sexp.length;
44374
+ }
44375
+ function parseKiCadPin(electricalType, shape, sexp) {
44376
+ const atMatch = sexp.match(/\(at\s+([\d.-]+)\s+([\d.-]+)(?:\s+([\d.-]+))?\)/);
44377
+ if (!atMatch)
44378
+ return null;
44379
+ const x = parseFloat(atMatch[1]) * MIL_TO_MM2;
44380
+ const y = parseFloat(atMatch[2]) * MIL_TO_MM2;
44381
+ const rotation = atMatch[3] ? parseFloat(atMatch[3]) : 0;
44382
+ const nameMatch = sexp.match(/\(name\s+"([^"]*)"/);
44383
+ const name = nameMatch?.[1] || "";
44384
+ const numberMatch = sexp.match(/\(number\s+"([^"]*)"/);
44385
+ const number = numberMatch?.[1] || "";
44386
+ const electrical = mapKiCadElectricalType(electricalType);
44387
+ return {
44388
+ number,
44389
+ name,
44390
+ x,
44391
+ y,
44392
+ rotation,
44393
+ electrical
44394
+ };
44395
+ }
44396
+ function mapKiCadElectricalType(type) {
44397
+ const typeMap = {
44398
+ input: "input",
44399
+ output: "output",
44400
+ bidirectional: "bidirectional",
44401
+ tri_state: "tri_state",
44402
+ passive: "passive",
44403
+ free: "free",
44404
+ unspecified: "unspecified",
44405
+ power_in: "power_in",
44406
+ power_out: "power_out",
44407
+ open_collector: "open_collector",
44408
+ open_emitter: "open_emitter",
44409
+ no_connect: "no_connect"
44410
+ };
44411
+ return typeMap[type.toLowerCase()] || "unspecified";
44412
+ }
44413
+ function compareSymbols(reference, generated, options = {}) {
44414
+ const opts = { ...DEFAULT_COMPARISON_OPTIONS, ...options };
44415
+ const diffs = [];
44416
+ const pinCountMatch = reference.pins.length === generated.pins.length;
44417
+ const matchedPins = new Set;
44418
+ for (const refPin of reference.pins) {
44419
+ const genPin = findMatchingPin(refPin, generated.pins, opts.positionTolerance);
44420
+ if (!genPin) {
44421
+ diffs.push({
44422
+ pinNumber: refPin.number,
44423
+ field: "missing",
44424
+ severity: "error",
44425
+ message: `Pin ${refPin.number} (${refPin.name}) missing in generated output`
44426
+ });
44427
+ continue;
44428
+ }
44429
+ matchedPins.add(genPin.number);
44430
+ const dx = Math.abs(refPin.x - genPin.x);
44431
+ const dy = Math.abs(refPin.y - genPin.y);
44432
+ const posTolerance = opts.positionTolerance * 10;
44433
+ if (dx > posTolerance || dy > posTolerance) {
44434
+ diffs.push({
44435
+ pinNumber: refPin.number,
44436
+ field: "position",
44437
+ severity: "warning",
44438
+ message: `Position differs: expected (${refPin.x.toFixed(2)}, ${refPin.y.toFixed(2)}), got (${genPin.x.toFixed(2)}, ${genPin.y.toFixed(2)})`,
44439
+ expected: `(${refPin.x.toFixed(2)}, ${refPin.y.toFixed(2)})`,
44440
+ actual: `(${genPin.x.toFixed(2)}, ${genPin.y.toFixed(2)})`
44441
+ });
44442
+ }
44443
+ if (!opts.ignorePinNames && refPin.name !== genPin.name) {
44444
+ if (refPin.name.toLowerCase() !== genPin.name.toLowerCase()) {
44445
+ diffs.push({
44446
+ pinNumber: refPin.number,
44447
+ field: "name",
44448
+ severity: "warning",
44449
+ message: `Name differs: expected "${refPin.name}", got "${genPin.name}"`,
44450
+ expected: refPin.name,
44451
+ actual: genPin.name
44452
+ });
44453
+ }
44454
+ }
44455
+ const rotDiff = Math.abs(refPin.rotation - genPin.rotation) % 360;
44456
+ if (rotDiff > 1 && rotDiff < 359) {
44457
+ diffs.push({
44458
+ pinNumber: refPin.number,
44459
+ field: "rotation",
44460
+ severity: "info",
44461
+ message: `Rotation differs: expected ${refPin.rotation}°, got ${genPin.rotation}°`,
44462
+ expected: refPin.rotation,
44463
+ actual: genPin.rotation
44464
+ });
44465
+ }
44466
+ if (refPin.electrical && genPin.electrical && refPin.electrical !== genPin.electrical) {
44467
+ diffs.push({
44468
+ pinNumber: refPin.number,
44469
+ field: "electrical",
44470
+ severity: "warning",
44471
+ message: `Electrical type differs: expected ${refPin.electrical}, got ${genPin.electrical}`,
44472
+ expected: refPin.electrical,
44473
+ actual: genPin.electrical
44474
+ });
44475
+ }
44476
+ }
44477
+ for (const genPin of generated.pins) {
44478
+ if (!matchedPins.has(genPin.number)) {
44479
+ const matchByPos = reference.pins.find((p) => Math.abs(p.x - genPin.x) < opts.positionTolerance * 10 && Math.abs(p.y - genPin.y) < opts.positionTolerance * 10);
44480
+ if (!matchByPos) {
44481
+ diffs.push({
44482
+ pinNumber: genPin.number,
44483
+ field: "extra",
44484
+ severity: "warning",
44485
+ message: `Extra pin ${genPin.number} (${genPin.name}) in generated output`
44486
+ });
44487
+ }
44488
+ }
44489
+ }
44490
+ const errors2 = diffs.filter((d) => d.severity === "error");
44491
+ const warnings = diffs.filter((d) => d.severity === "warning");
44492
+ const passed = errors2.length === 0 && pinCountMatch;
44493
+ return {
44494
+ passed,
44495
+ pinCountMatch,
44496
+ referencePinCount: reference.pins.length,
44497
+ generatedPinCount: generated.pins.length,
44498
+ diffs,
44499
+ errors: errors2,
44500
+ warnings
44501
+ };
44502
+ }
44503
+ function findMatchingPin(refPin, generatedPins, tolerance) {
44504
+ const byNumber = generatedPins.find((p) => p.number === refPin.number);
44505
+ if (byNumber)
44506
+ return byNumber;
44507
+ return generatedPins.find((p) => Math.abs(p.x - refPin.x) < tolerance * 10 && Math.abs(p.y - refPin.y) < tolerance * 10) || null;
44508
+ }
44509
+ function calculateCentroid(points) {
44510
+ if (points.length === 0) {
44511
+ return { x: 0, y: 0 };
44512
+ }
44513
+ const sum = points.reduce((acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }), { x: 0, y: 0 });
44514
+ return {
44515
+ x: sum.x / points.length,
44516
+ y: sum.y / points.length
44517
+ };
44518
+ }
44519
+ function roundTo3(value, decimals) {
44520
+ const factor = Math.pow(10, decimals);
44521
+ return Math.round(value * factor) / factor;
44522
+ }
44523
+ function calculateBounds2(points) {
44524
+ if (points.length === 0) {
44525
+ return { minX: 0, maxX: 0, minY: 0, maxY: 0 };
44526
+ }
44527
+ return {
44528
+ minX: Math.min(...points.map((p) => p.x)),
44529
+ maxX: Math.max(...points.map((p) => p.x)),
44530
+ minY: Math.min(...points.map((p) => p.y)),
44531
+ maxY: Math.max(...points.map((p) => p.y))
44532
+ };
44533
+ }
44534
+ function formatSymbolComparisonResult(result) {
44535
+ const lines = [];
44536
+ const status = result.passed ? "✓ PASS" : "✗ FAIL";
44537
+ lines.push(`Symbol Comparison: ${status}`);
44538
+ lines.push("");
44539
+ lines.push(` Pin count: ${result.generatedPinCount}/${result.referencePinCount} ${result.pinCountMatch ? "✓" : "✗"}`);
44540
+ lines.push("");
44541
+ if (result.errors.length > 0) {
44542
+ lines.push(` Errors (${result.errors.length}):`);
44543
+ for (const err of result.errors) {
44544
+ lines.push(` ✗ Pin ${err.pinNumber}: ${err.message}`);
44545
+ }
44546
+ lines.push("");
44547
+ }
44548
+ if (result.warnings.length > 0) {
44549
+ lines.push(` Warnings (${result.warnings.length}):`);
44550
+ for (const warn of result.warnings) {
44551
+ lines.push(` ⚠ Pin ${warn.pinNumber}: ${warn.message}`);
44552
+ }
44553
+ }
44554
+ return lines.join(`
44555
+ `);
44556
+ }
44557
+ function generateValidationReport(result, svgs) {
44558
+ const statusClass = result.passed ? "pass" : "fail";
44559
+ const statusText = result.passed ? "PASS" : "FAIL";
44560
+ return `<!DOCTYPE html>
44561
+ <html lang="en">
44562
+ <head>
44563
+ <meta charset="UTF-8">
44564
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
44565
+ <title>Validation: ${result.lcscCode} - ${result.componentName}</title>
44566
+ <style>${getStyles()}</style>
44567
+ </head>
44568
+ <body>
44569
+ <div class="container">
44570
+ <header>
44571
+ <h1>${result.componentName}</h1>
44572
+ <div class="meta">
44573
+ <span class="lcsc">LCSC: ${result.lcscCode}</span>
44574
+ <span class="status ${statusClass}">${statusText}</span>
44575
+ <span class="duration">${result.durationMs}ms</span>
44576
+ </div>
44577
+ </header>
44578
+
44579
+ ${result.error ? `<div class="error-banner">${escapeHtml(result.error)}</div>` : ""}
44580
+
44581
+ <div class="main-columns">
44582
+ ${svgs?.footprintRef || svgs?.footprintGen || result.footprint ? `
44583
+ <div class="main-column">
44584
+ <h2 class="column-header">Footprint</h2>
44585
+ ${svgs?.footprintRef || svgs?.footprintGen ? `
44586
+ <div class="svg-comparison">
44587
+ <div class="svg-panel">
44588
+ <h3>Reference (EasyEDA/JLCPCB)</h3>
44589
+ <div class="svg-container">${svgs?.footprintRef || '<div class="no-preview">Not available</div>'}</div>
44590
+ </div>
44591
+ <div class="svg-panel">
44592
+ <h3>Generated (JLC-CLI)</h3>
44593
+ <div class="svg-container">${svgs?.footprintGen || '<div class="no-preview">Not available</div>'}</div>
44594
+ </div>
44595
+ </div>
44596
+ ` : ""}
44597
+ ${result.footprint ? generateFootprintSection(result.footprint) : ""}
44598
+ </div>
44599
+ ` : ""}
44600
+
44601
+ ${svgs?.symbolRef || svgs?.symbolGen || result.symbol ? `
44602
+ <div class="main-column">
44603
+ <h2 class="column-header">Symbol</h2>
44604
+ ${svgs?.symbolRef || svgs?.symbolGen ? `
44605
+ <div class="svg-comparison">
44606
+ <div class="svg-panel">
44607
+ <h3>Reference (EasyEDA/JLCPCB)</h3>
44608
+ <div class="svg-container">${svgs?.symbolRef || '<div class="no-preview">Not available</div>'}</div>
44609
+ </div>
44610
+ <div class="svg-panel">
44611
+ <h3>Generated (JLC-CLI)</h3>
44612
+ <div class="svg-container">${svgs?.symbolGen || '<div class="no-preview">Not available</div>'}</div>
44613
+ </div>
44614
+ </div>
44615
+ ` : ""}
44616
+ ${result.symbol ? generateSymbolSection(result.symbol) : ""}
44617
+ </div>
44618
+ ` : ""}
44619
+ </div>
44620
+
44621
+ <footer>
44622
+ <p>Generated: ${result.timestamp.toISOString()}</p>
44623
+ <p>JLC-CLI Validation Report</p>
44624
+ </footer>
44625
+ </div>
44626
+ </body>
44627
+ </html>`;
44628
+ }
44629
+ function generateBatchReport(results, title = "Validation Report") {
44630
+ const passed = results.filter((r) => r.passed).length;
44631
+ const failed = results.filter((r) => !r.passed).length;
44632
+ const errorCount = results.filter((r) => r.error).length;
44633
+ return `<!DOCTYPE html>
44634
+ <html lang="en">
44635
+ <head>
44636
+ <meta charset="UTF-8">
44637
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
44638
+ <title>${title}</title>
44639
+ <style>${getStyles()}</style>
44640
+ </head>
44641
+ <body>
44642
+ <div class="container">
44643
+ <header>
44644
+ <h1>${title}</h1>
44645
+ <div class="summary">
44646
+ <span class="stat pass">${passed} passed</span>
44647
+ <span class="stat fail">${failed} failed</span>
44648
+ <span class="stat error">${errorCount} errors</span>
44649
+ <span class="stat total">${results.length} total</span>
44650
+ </div>
44651
+ </header>
44652
+
44653
+ <section class="results-table">
44654
+ <table>
44655
+ <thead>
44656
+ <tr>
44657
+ <th>LCSC</th>
44658
+ <th>Component</th>
44659
+ <th>Footprint</th>
44660
+ <th>Symbol</th>
44661
+ <th>Status</th>
44662
+ <th>Duration</th>
44663
+ </tr>
44664
+ </thead>
44665
+ <tbody>
44666
+ ${results.map((r) => generateResultRow(r)).join(`
44667
+ `)}
44668
+ </tbody>
44669
+ </table>
44670
+ </section>
44671
+
44672
+ ${generateDetailedResults(results)}
44673
+
44674
+ <footer>
44675
+ <p>Generated: ${new Date().toISOString()}</p>
44676
+ <p>JLC-CLI Validation Report</p>
44677
+ </footer>
44678
+ </div>
44679
+ </body>
44680
+ </html>`;
44681
+ }
44682
+ function generateFootprintSection(result) {
44683
+ const statusClass = result.passed ? "pass" : "fail";
44684
+ return `
44685
+ <section class="validation-section footprint">
44686
+ <h2>Footprint Validation <span class="${statusClass}">${result.passed ? "✓" : "✗"}</span></h2>
44687
+
44688
+ <div class="stats-grid">
44689
+ <div class="stat-item">
44690
+ <span class="label">Pad Count</span>
44691
+ <span class="value ${result.padCountMatch ? "pass" : "fail"}">
44692
+ ${result.generatedPadCount}/${result.referencePadCount}
44693
+ </span>
44694
+ </div>
44695
+ <div class="stat-item">
44696
+ <span class="label">Via Count</span>
44697
+ <span class="value ${result.viaCountMatch ? "pass" : "fail"}">
44698
+ ${result.generatedViaCount}/${result.referenceViaCount}
44699
+ </span>
44700
+ </div>
44701
+ <div class="stat-item">
44702
+ <span class="label">Errors</span>
44703
+ <span class="value ${result.errors.length > 0 ? "fail" : "pass"}">
44704
+ ${result.errors.length}
44705
+ </span>
44706
+ </div>
44707
+ <div class="stat-item">
44708
+ <span class="label">Warnings</span>
44709
+ <span class="value warn">${result.warnings.length}</span>
44710
+ </div>
44711
+ </div>
44712
+
44713
+ ${result.errors.length > 0 ? `
44714
+ <div class="diff-list errors">
44715
+ <h3>Errors</h3>
44716
+ ${result.errors.map((d) => formatPadDiff(d)).join(`
44717
+ `)}
44718
+ </div>
44719
+ ` : ""}
44720
+
44721
+ ${result.warnings.length > 0 ? `
44722
+ <div class="diff-list warnings">
44723
+ <h3>Warnings</h3>
44724
+ ${result.warnings.map((d) => formatPadDiff(d)).join(`
44725
+ `)}
44726
+ </div>
44727
+ ` : ""}
44728
+ </section>
44729
+ `;
44730
+ }
44731
+ function generateSymbolSection(result) {
44732
+ const statusClass = result.passed ? "pass" : "fail";
44733
+ return `
44734
+ <section class="validation-section symbol">
44735
+ <h2>Symbol Validation <span class="${statusClass}">${result.passed ? "✓" : "✗"}</span></h2>
44736
+
44737
+ <div class="stats-grid">
44738
+ <div class="stat-item">
44739
+ <span class="label">Pin Count</span>
44740
+ <span class="value ${result.pinCountMatch ? "pass" : "fail"}">
44741
+ ${result.generatedPinCount}/${result.referencePinCount}
44742
+ </span>
44743
+ </div>
44744
+ <div class="stat-item">
44745
+ <span class="label">Errors</span>
44746
+ <span class="value ${result.errors.length > 0 ? "fail" : "pass"}">
44747
+ ${result.errors.length}
44748
+ </span>
44749
+ </div>
44750
+ <div class="stat-item">
44751
+ <span class="label">Warnings</span>
44752
+ <span class="value warn">${result.warnings.length}</span>
44753
+ </div>
44754
+ </div>
44755
+
44756
+ ${result.errors.length > 0 ? `
44757
+ <div class="diff-list errors">
44758
+ <h3>Errors</h3>
44759
+ ${result.errors.map((d) => formatPinDiff(d)).join(`
44760
+ `)}
44761
+ </div>
44762
+ ` : ""}
44763
+
44764
+ ${result.warnings.length > 0 ? `
44765
+ <div class="diff-list warnings">
44766
+ <h3>Warnings</h3>
44767
+ ${result.warnings.map((d) => formatPinDiff(d)).join(`
44768
+ `)}
44769
+ </div>
44770
+ ` : ""}
44771
+ </section>
44772
+ `;
44773
+ }
44774
+ function generateResultRow(result) {
44775
+ const status = result.passed ? "pass" : result.error ? "error" : "fail";
44776
+ const fpStatus = result.footprint ? result.footprint.passed ? "✓" : "✗" : result.error ? "⚠" : "-";
44777
+ const symStatus = result.symbol ? result.symbol.passed ? "✓" : "✗" : result.error ? "⚠" : "-";
44778
+ return `
44779
+ <tr class="${status}">
44780
+ <td><a href="#${result.lcscCode}">${result.lcscCode}</a></td>
44781
+ <td>${escapeHtml(result.componentName)}</td>
44782
+ <td class="center">${fpStatus}</td>
44783
+ <td class="center">${symStatus}</td>
44784
+ <td class="status">${result.passed ? "PASS" : result.error ? "ERROR" : "FAIL"}</td>
44785
+ <td class="right">${result.durationMs}ms</td>
44786
+ </tr>
44787
+ `;
44788
+ }
44789
+ function generateDetailedResults(results) {
44790
+ const failedResults = results.filter((r) => !r.passed);
44791
+ if (failedResults.length === 0)
44792
+ return "";
44793
+ return `
44794
+ <section class="detailed-results">
44795
+ <h2>Detailed Results</h2>
44796
+ ${failedResults.map((r) => `
44797
+ <div class="result-detail" id="${r.lcscCode}">
44798
+ <h3>${r.lcscCode} - ${escapeHtml(r.componentName)}</h3>
44799
+ ${r.error ? `<div class="error-message">${escapeHtml(r.error)}</div>` : ""}
44800
+ ${r.footprint ? generateFootprintSection(r.footprint) : ""}
44801
+ ${r.symbol ? generateSymbolSection(r.symbol) : ""}
44802
+ </div>
44803
+ `).join(`
44804
+ `)}
44805
+ </section>
44806
+ `;
44807
+ }
44808
+ function formatPadDiff(diff) {
44809
+ const icon = diff.severity === "error" ? "✗" : diff.severity === "warning" ? "⚠" : "ℹ";
44810
+ return `
44811
+ <div class="diff-item ${diff.severity}">
44812
+ <span class="icon">${icon}</span>
44813
+ <span class="pad">Pad ${diff.padNumber}</span>
44814
+ <span class="field">${diff.field}</span>
44815
+ <span class="message">${escapeHtml(diff.message)}</span>
44816
+ </div>
44817
+ `;
44818
+ }
44819
+ function formatPinDiff(diff) {
44820
+ const icon = diff.severity === "error" ? "✗" : diff.severity === "warning" ? "⚠" : "ℹ";
44821
+ return `
44822
+ <div class="diff-item ${diff.severity}">
44823
+ <span class="icon">${icon}</span>
44824
+ <span class="pin">Pin ${diff.pinNumber}</span>
44825
+ <span class="field">${diff.field}</span>
44826
+ <span class="message">${escapeHtml(diff.message)}</span>
44827
+ </div>
44828
+ `;
44829
+ }
44830
+ function escapeHtml(text) {
44831
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
44832
+ }
44833
+ function getStyles() {
44834
+ return `
44835
+ * {
44836
+ box-sizing: border-box;
44837
+ margin: 0;
44838
+ padding: 0;
44839
+ }
44840
+
44841
+ body {
44842
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
44843
+ background: #f5f5f5;
44844
+ color: #333;
44845
+ line-height: 1.6;
44846
+ }
44847
+
44848
+ .container {
44849
+ max-width: 1400px;
44850
+ margin: 0 auto;
44851
+ padding: 2rem;
44852
+ }
44853
+
44854
+ header {
44855
+ background: #fff;
44856
+ padding: 1.5rem;
44857
+ border-radius: 8px;
44858
+ margin-bottom: 2rem;
44859
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
44860
+ }
44861
+
44862
+ h1 {
44863
+ font-size: 1.5rem;
44864
+ margin-bottom: 0.5rem;
44865
+ }
44866
+
44867
+ .meta {
44868
+ display: flex;
44869
+ gap: 1rem;
44870
+ align-items: center;
44871
+ }
44872
+
44873
+ .lcsc {
44874
+ font-family: monospace;
44875
+ background: #e8e8e8;
44876
+ padding: 0.25rem 0.5rem;
44877
+ border-radius: 4px;
44878
+ }
44879
+
44880
+ .status {
44881
+ font-weight: bold;
44882
+ padding: 0.25rem 0.75rem;
44883
+ border-radius: 4px;
44884
+ }
44885
+
44886
+ .status.pass, .pass { color: #22c55e; }
44887
+ .status.fail, .fail { color: #ef4444; }
44888
+ .status.error { color: #f59e0b; }
44889
+ .warn { color: #f59e0b; }
44890
+
44891
+ .summary {
44892
+ display: flex;
44893
+ gap: 1rem;
44894
+ margin-top: 1rem;
44895
+ }
44896
+
44897
+ .stat {
44898
+ padding: 0.5rem 1rem;
44899
+ border-radius: 4px;
44900
+ background: #f0f0f0;
44901
+ }
44902
+
44903
+ .stat.pass { background: #dcfce7; }
44904
+ .stat.fail { background: #fee2e2; }
44905
+ .stat.error { background: #fef3c7; }
44906
+
44907
+ .error-banner {
44908
+ background: #fee2e2;
44909
+ color: #b91c1c;
44910
+ padding: 1rem;
44911
+ border-radius: 8px;
44912
+ margin-bottom: 2rem;
44913
+ }
44914
+
44915
+ .comparison {
44916
+ display: grid;
44917
+ grid-template-columns: 1fr 1fr;
44918
+ gap: 2rem;
44919
+ margin-bottom: 2rem;
44920
+ }
44921
+
44922
+ .svg-panel {
44923
+ background: #fff;
44924
+ padding: 1rem;
44925
+ border-radius: 8px;
44926
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
44927
+ }
44928
+
44929
+ .svg-panel h2 {
44930
+ font-size: 1rem;
44931
+ margin-bottom: 1rem;
44932
+ color: #666;
44933
+ }
44934
+
44935
+ .main-columns {
44936
+ display: grid;
44937
+ grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
44938
+ gap: 2rem;
44939
+ margin-bottom: 2rem;
44940
+ }
44941
+
44942
+ .main-column {
44943
+ background: #fff;
44944
+ border-radius: 8px;
44945
+ padding: 1.5rem;
44946
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
44947
+ }
44948
+
44949
+ .column-header {
44950
+ font-size: 1.25rem;
44951
+ margin-bottom: 1.5rem;
44952
+ color: #333;
44953
+ text-align: center;
44954
+ border-bottom: 2px solid #e0e0e0;
44955
+ padding-bottom: 0.75rem;
44956
+ }
44957
+
44958
+ .svg-comparison {
44959
+ display: grid;
44960
+ grid-template-columns: 1fr 1fr;
44961
+ gap: 1rem;
44962
+ margin-bottom: 1.5rem;
44963
+ }
44964
+
44965
+ .svg-panel {
44966
+ background: #f8f8f8;
44967
+ border-radius: 8px;
44968
+ padding: 0.75rem;
44969
+ }
44970
+
44971
+ .svg-panel h3 {
44972
+ font-size: 0.75rem;
44973
+ color: #666;
44974
+ margin-bottom: 0.5rem;
44975
+ text-align: center;
44976
+ }
44977
+
44978
+ .svg-container {
44979
+ background: #1a1a1a;
44980
+ padding: 0.75rem;
44981
+ border-radius: 4px;
44982
+ overflow: hidden;
44983
+ display: flex;
44984
+ justify-content: center;
44985
+ align-items: center;
44986
+ height: 220px;
44987
+ }
44988
+
44989
+ .svg-container svg {
44990
+ width: 100%;
44991
+ height: 100%;
44992
+ object-fit: contain;
44993
+ }
44994
+
44995
+ .svg-container .no-preview {
44996
+ color: #888;
44997
+ font-style: italic;
44998
+ font-size: 0.875rem;
44999
+ }
45000
+
45001
+ .validation-section {
45002
+ background: #f8f8f8;
45003
+ padding: 1rem;
45004
+ border-radius: 8px;
45005
+ }
45006
+
45007
+ .validation-section h2 {
45008
+ display: flex;
45009
+ align-items: center;
45010
+ gap: 0.5rem;
45011
+ margin-bottom: 1rem;
45012
+ font-size: 1rem;
45013
+ }
45014
+
45015
+ .stats-grid {
45016
+ display: grid;
45017
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
45018
+ gap: 1rem;
45019
+ margin-bottom: 1.5rem;
45020
+ }
45021
+
45022
+ .stat-item {
45023
+ background: #f8f8f8;
45024
+ padding: 0.75rem;
45025
+ border-radius: 4px;
45026
+ text-align: center;
45027
+ }
45028
+
45029
+ .stat-item .label {
45030
+ display: block;
45031
+ font-size: 0.75rem;
45032
+ color: #666;
45033
+ margin-bottom: 0.25rem;
45034
+ }
45035
+
45036
+ .stat-item .value {
45037
+ font-size: 1.25rem;
45038
+ font-weight: bold;
45039
+ }
45040
+
45041
+ .diff-list {
45042
+ margin-top: 1rem;
45043
+ }
45044
+
45045
+ .diff-list h3 {
45046
+ font-size: 0.875rem;
45047
+ color: #666;
45048
+ margin-bottom: 0.5rem;
45049
+ }
45050
+
45051
+ .diff-item {
45052
+ display: flex;
45053
+ align-items: center;
45054
+ gap: 0.5rem;
45055
+ padding: 0.5rem;
45056
+ background: #f8f8f8;
45057
+ border-radius: 4px;
45058
+ margin-bottom: 0.25rem;
45059
+ font-size: 0.875rem;
45060
+ }
45061
+
45062
+ .diff-item.error { background: #fee2e2; }
45063
+ .diff-item.warning { background: #fef3c7; }
45064
+ .diff-item.info { background: #e0f2fe; }
45065
+
45066
+ .diff-item .icon { width: 1rem; }
45067
+ .diff-item .pad, .diff-item .pin {
45068
+ font-weight: bold;
45069
+ min-width: 4rem;
45070
+ }
45071
+ .diff-item .field {
45072
+ color: #666;
45073
+ min-width: 5rem;
45074
+ }
45075
+ .diff-item .message {
45076
+ flex: 1;
45077
+ }
45078
+
45079
+ .results-table {
45080
+ background: #fff;
45081
+ border-radius: 8px;
45082
+ overflow: hidden;
45083
+ margin-bottom: 2rem;
45084
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
45085
+ }
45086
+
45087
+ table {
45088
+ width: 100%;
45089
+ border-collapse: collapse;
45090
+ }
45091
+
45092
+ th, td {
45093
+ padding: 0.75rem 1rem;
45094
+ text-align: left;
45095
+ border-bottom: 1px solid #eee;
45096
+ }
45097
+
45098
+ th {
45099
+ background: #f8f8f8;
45100
+ font-weight: 600;
45101
+ }
45102
+
45103
+ tr.pass { background: #f0fdf4; }
45104
+ tr.fail { background: #fef2f2; }
45105
+ tr.error { background: #fffbeb; }
45106
+
45107
+ .center { text-align: center; }
45108
+ .right { text-align: right; }
45109
+
45110
+ .detailed-results {
45111
+ margin-top: 2rem;
45112
+ }
45113
+
45114
+ .result-detail {
45115
+ background: #fff;
45116
+ padding: 1.5rem;
45117
+ border-radius: 8px;
45118
+ margin-bottom: 1.5rem;
45119
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
45120
+ }
45121
+
45122
+ .result-detail h3 {
45123
+ margin-bottom: 1rem;
45124
+ }
45125
+
45126
+ .error-message {
45127
+ background: #fee2e2;
45128
+ color: #b91c1c;
45129
+ padding: 0.75rem;
45130
+ border-radius: 4px;
45131
+ margin-bottom: 1rem;
45132
+ }
45133
+
45134
+ footer {
45135
+ text-align: center;
45136
+ color: #666;
45137
+ font-size: 0.875rem;
45138
+ margin-top: 2rem;
45139
+ padding-top: 2rem;
45140
+ border-top: 1px solid #ddd;
45141
+ }
45142
+
45143
+ @media (max-width: 768px) {
45144
+ .comparison {
45145
+ grid-template-columns: 1fr;
45146
+ }
45147
+ }
45148
+ `;
45149
+ }
45150
+ function tokenize(input) {
45151
+ const tokens = [];
45152
+ let i = 0;
45153
+ while (i < input.length) {
45154
+ const char = input[i];
45155
+ if (/\s/.test(char)) {
45156
+ i++;
45157
+ continue;
45158
+ }
45159
+ if (char === "(" || char === ")") {
45160
+ tokens.push(char);
45161
+ i++;
45162
+ continue;
45163
+ }
45164
+ if (char === '"') {
45165
+ let str = "";
45166
+ i++;
45167
+ while (i < input.length) {
45168
+ if (input[i] === "\\" && i + 1 < input.length) {
45169
+ str += input[i + 1];
45170
+ i += 2;
45171
+ } else if (input[i] === '"') {
45172
+ i++;
45173
+ break;
45174
+ } else {
45175
+ str += input[i];
45176
+ i++;
45177
+ }
45178
+ }
45179
+ tokens.push(`"${str}"`);
45180
+ continue;
45181
+ }
45182
+ let atom = "";
45183
+ while (i < input.length && !/[\s()]/.test(input[i])) {
45184
+ atom += input[i];
45185
+ i++;
45186
+ }
45187
+ if (atom) {
45188
+ tokens.push(atom);
45189
+ }
45190
+ }
45191
+ return tokens;
45192
+ }
45193
+ function parseTokens(tokens) {
45194
+ if (tokens.length === 0) {
45195
+ return { expr: [], remaining: [] };
45196
+ }
45197
+ const token = tokens[0];
45198
+ if (token === "(") {
45199
+ const list = [];
45200
+ let rest = tokens.slice(1);
45201
+ while (rest.length > 0 && rest[0] !== ")") {
45202
+ const { expr, remaining } = parseTokens(rest);
45203
+ list.push(expr);
45204
+ rest = remaining;
45205
+ }
45206
+ if (rest[0] === ")") {
45207
+ rest = rest.slice(1);
45208
+ }
45209
+ return { expr: list, remaining: rest };
45210
+ }
45211
+ if (token === ")") {
45212
+ return { expr: [], remaining: tokens.slice(1) };
45213
+ }
45214
+ let value = token;
45215
+ if (value.startsWith('"') && value.endsWith('"')) {
45216
+ value = value.slice(1, -1);
45217
+ }
45218
+ return { expr: value, remaining: tokens.slice(1) };
45219
+ }
45220
+ function parseSExpr(input) {
45221
+ const tokens = tokenize(input);
45222
+ const { expr } = parseTokens(tokens);
45223
+ return expr;
45224
+ }
45225
+ function isList(expr) {
45226
+ return Array.isArray(expr);
45227
+ }
45228
+ function isAtom(expr) {
45229
+ return typeof expr === "string";
45230
+ }
45231
+ function getTag(expr) {
45232
+ if (isList(expr) && expr.length > 0 && isAtom(expr[0])) {
45233
+ return expr[0];
45234
+ }
45235
+ return;
45236
+ }
45237
+ function findChild(expr, tag) {
45238
+ if (!isList(expr))
45239
+ return;
45240
+ for (const child of expr) {
45241
+ if (isList(child) && getTag(child) === tag) {
45242
+ return child;
45243
+ }
45244
+ }
45245
+ return;
45246
+ }
45247
+ function findChildren(expr, tag) {
45248
+ if (!isList(expr))
45249
+ return [];
45250
+ const results = [];
45251
+ for (const child of expr) {
45252
+ if (isList(child) && getTag(child) === tag) {
45253
+ results.push(child);
45254
+ }
45255
+ }
45256
+ return results;
45257
+ }
45258
+ function getAttr(expr, tag) {
45259
+ const child = findChild(expr, tag);
45260
+ if (child && child.length >= 2 && isAtom(child[1])) {
45261
+ return child[1];
45262
+ }
45263
+ return;
45264
+ }
45265
+ function getNumericAttr(expr, tag) {
45266
+ const value = getAttr(expr, tag);
45267
+ if (value !== undefined) {
45268
+ const num = parseFloat(value);
45269
+ if (!isNaN(num))
45270
+ return num;
45271
+ }
45272
+ return;
45273
+ }
45274
+ function getPoint(expr, tag) {
45275
+ const child = findChild(expr, tag);
45276
+ if (child && child.length >= 3) {
45277
+ const x = parseFloat(isAtom(child[1]) ? child[1] : "");
45278
+ const y = parseFloat(isAtom(child[2]) ? child[2] : "");
45279
+ if (!isNaN(x) && !isNaN(y)) {
45280
+ return { x, y };
45281
+ }
45282
+ }
45283
+ return;
45284
+ }
45285
+ function getPointWithRotation(expr, tag) {
45286
+ const child = findChild(expr, tag);
45287
+ if (child && child.length >= 3) {
45288
+ const x = parseFloat(isAtom(child[1]) ? child[1] : "");
45289
+ const y = parseFloat(isAtom(child[2]) ? child[2] : "");
45290
+ if (!isNaN(x) && !isNaN(y)) {
45291
+ const result = { x, y };
45292
+ if (child.length >= 4 && isAtom(child[3])) {
45293
+ const rotation = parseFloat(child[3]);
45294
+ if (!isNaN(rotation)) {
45295
+ result.rotation = rotation;
45296
+ }
45297
+ }
45298
+ return result;
45299
+ }
43080
45300
  }
45301
+ return;
43081
45302
  }
43082
- function communityToComponentData(community) {
43083
- const cPara = community.symbol.head?.c_para ?? {};
43084
- const fpCPara = community.footprint.head?.c_para ?? {};
43085
- return {
43086
- info: {
43087
- name: community.title || cPara.name || "Unknown",
43088
- prefix: cPara.pre || cPara.Prefix || "U",
43089
- package: fpCPara.package || community.footprint.name,
43090
- manufacturer: cPara.Manufacturer || cPara.BOM_Manufacturer,
43091
- description: community.description || cPara.BOM_Manufacturer_Part,
43092
- category: cPara.package
43093
- },
43094
- symbol: community.symbol,
43095
- footprint: community.footprint,
43096
- model3d: community.model3d,
43097
- rawData: community.rawData
43098
- };
45303
+ function getSize(expr) {
45304
+ const child = findChild(expr, "size");
45305
+ if (child && child.length >= 3) {
45306
+ const width = parseFloat(isAtom(child[1]) ? child[1] : "");
45307
+ const height = parseFloat(isAtom(child[2]) ? child[2] : "");
45308
+ if (!isNaN(width) && !isNaN(height)) {
45309
+ return { width, height };
45310
+ }
45311
+ }
45312
+ return;
43099
45313
  }
43100
- async function handleComponent(uuid, res) {
43101
- if (!uuid) {
43102
- res.writeHead(400, { "Content-Type": "application/json" });
43103
- res.end(JSON.stringify({ error: "Missing UUID" }));
45314
+ function getStroke(expr) {
45315
+ const stroke = findChild(expr, "stroke");
45316
+ if (!stroke)
45317
+ return;
45318
+ const width = getNumericAttr(stroke, "width") ?? 0.254;
45319
+ const type = getAttr(stroke, "type") ?? "default";
45320
+ return { width, type };
45321
+ }
45322
+ function getFillType(expr) {
45323
+ const fill = findChild(expr, "fill");
45324
+ if (!fill)
43104
45325
  return;
45326
+ const typeAttr = getAttr(fill, "type");
45327
+ if (typeAttr)
45328
+ return typeAttr;
45329
+ if (fill.length >= 2 && isAtom(fill[1])) {
45330
+ return fill[1];
43105
45331
  }
43106
- try {
43107
- const component2 = await easyedaCommunityClient.getComponent(uuid);
43108
- if (!component2) {
43109
- res.writeHead(404, { "Content-Type": "application/json" });
43110
- res.end(JSON.stringify({ error: "Component not found" }));
43111
- return;
43112
- }
43113
- const componentData = communityToComponentData(component2);
43114
- let symbolSexpr = "";
43115
- let footprintSexpr = "";
43116
- try {
43117
- symbolSexpr = symbolConverter.convertToSymbolEntry(componentData);
43118
- } catch (e) {
43119
- logger7.warn("Symbol conversion failed:", e);
43120
- }
43121
- try {
43122
- footprintSexpr = footprintConverter.convert(componentData);
43123
- } catch (e) {
43124
- logger7.warn("Footprint conversion failed:", e);
45332
+ return;
45333
+ }
45334
+ function getPoints(expr) {
45335
+ const pts = findChild(expr, "pts");
45336
+ if (!pts)
45337
+ return [];
45338
+ const points = [];
45339
+ for (const child of pts) {
45340
+ if (isList(child) && getTag(child) === "xy" && child.length >= 3) {
45341
+ const x = parseFloat(isAtom(child[1]) ? child[1] : "");
45342
+ const y = parseFloat(isAtom(child[2]) ? child[2] : "");
45343
+ if (!isNaN(x) && !isNaN(y)) {
45344
+ points.push({ x, y });
45345
+ }
43125
45346
  }
43126
- res.writeHead(200, { "Content-Type": "application/json" });
43127
- res.end(JSON.stringify({
43128
- uuid: component2.uuid,
43129
- title: component2.title,
43130
- description: component2.description,
43131
- owner: component2.owner,
43132
- symbolSexpr,
43133
- footprintSexpr,
43134
- model3d: component2.model3d
43135
- }));
45347
+ }
45348
+ return points;
45349
+ }
45350
+ function getLayers(expr) {
45351
+ const layer = getAttr(expr, "layer");
45352
+ if (layer)
45353
+ return [layer];
45354
+ const layers = findChild(expr, "layers");
45355
+ if (!layers)
45356
+ return [];
45357
+ const result = [];
45358
+ for (let i = 1;i < layers.length; i++) {
45359
+ const item = layers[i];
45360
+ if (isAtom(item)) {
45361
+ result.push(item);
45362
+ }
45363
+ }
45364
+ return result;
45365
+ }
45366
+ var SYMBOL_COLORS = {
45367
+ background: "#FFFFF8",
45368
+ body: "#840000",
45369
+ bodyFill: "#FFFFC4",
45370
+ pin: "#840000",
45371
+ pinText: "#008484",
45372
+ text: "#000000"
45373
+ };
45374
+ var FOOTPRINT_COLORS = {
45375
+ background: "#000000",
45376
+ fCu: "#840000",
45377
+ bCu: "#008400",
45378
+ in1Cu: "#C2C200",
45379
+ in2Cu: "#C200C2",
45380
+ fSilkS: "#008484",
45381
+ bSilkS: "#840084",
45382
+ fMask: "#840084",
45383
+ bMask: "#848400",
45384
+ fPaste: "#840000",
45385
+ bPaste: "#00C2C2",
45386
+ fFab: "#848484",
45387
+ bFab: "#000084",
45388
+ fCrtYd: "#C2C2C2",
45389
+ bCrtYd: "#848484",
45390
+ edgeCuts: "#C2C200",
45391
+ dwgsUser: "#C2C2C2",
45392
+ cmtsUser: "#848484",
45393
+ margin: "#C200C2",
45394
+ padFront: "#840000",
45395
+ padBack: "#008400",
45396
+ padThruHole: "#C2C200",
45397
+ drill: "#848484"
45398
+ };
45399
+ var SYMBOL_SCALE = 10;
45400
+ function escapeXml(text) {
45401
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
45402
+ }
45403
+ function renderSymbolSvg(sexpr) {
45404
+ if (!sexpr || sexpr.trim() === "") {
45405
+ return createErrorSvg("No symbol data");
45406
+ }
45407
+ try {
45408
+ let parsed = parseSExpr(sexpr);
45409
+ if (isList(parsed) && getTag(parsed) === "kicad_symbol_lib") {
45410
+ const symbols = findChildren(parsed, "symbol");
45411
+ if (symbols.length === 0) {
45412
+ return createErrorSvg("No symbol found in library");
45413
+ }
45414
+ parsed = symbols[0];
45415
+ }
45416
+ if (!isList(parsed) || getTag(parsed) !== "symbol") {
45417
+ return createErrorSvg("Invalid symbol format");
45418
+ }
45419
+ const elements = [];
45420
+ const bounds = { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity };
45421
+ const units = findChildren(parsed, "symbol");
45422
+ for (const unit of units) {
45423
+ for (const rect of findChildren(unit, "rectangle")) {
45424
+ const svg = renderSymbolRectangle(rect, bounds);
45425
+ if (svg)
45426
+ elements.push(svg);
45427
+ }
45428
+ for (const polyline of findChildren(unit, "polyline")) {
45429
+ const svg = renderSymbolPolyline(polyline, bounds);
45430
+ if (svg)
45431
+ elements.push(svg);
45432
+ }
45433
+ for (const circle of findChildren(unit, "circle")) {
45434
+ const svg = renderSymbolCircle(circle, bounds);
45435
+ if (svg)
45436
+ elements.push(svg);
45437
+ }
45438
+ for (const arc of findChildren(unit, "arc")) {
45439
+ const svg = renderSymbolArc(arc, bounds);
45440
+ if (svg)
45441
+ elements.push(svg);
45442
+ }
45443
+ for (const pin of findChildren(unit, "pin")) {
45444
+ const svg = renderSymbolPin(pin, bounds);
45445
+ if (svg)
45446
+ elements.push(svg);
45447
+ }
45448
+ }
45449
+ const padding = 5;
45450
+ if (!isFinite(bounds.minX)) {
45451
+ bounds.minX = -20;
45452
+ bounds.maxX = 20;
45453
+ bounds.minY = -20;
45454
+ bounds.maxY = 20;
45455
+ }
45456
+ const width = (bounds.maxX - bounds.minX + padding * 2) * SYMBOL_SCALE;
45457
+ const height = (bounds.maxY - bounds.minY + padding * 2) * SYMBOL_SCALE;
45458
+ const viewBox = `${(bounds.minX - padding) * SYMBOL_SCALE} ${(-bounds.maxY - padding) * SYMBOL_SCALE} ${width} ${height}`;
45459
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" width="100%" height="100%" style="background-color: ${SYMBOL_COLORS.background}">
45460
+ <g transform="scale(${SYMBOL_SCALE}, ${-SYMBOL_SCALE})">
45461
+ ${elements.join(`
45462
+ `)}
45463
+ </g>
45464
+ </svg>`;
43136
45465
  } catch (error) {
43137
- logger7.error("Component fetch error:", error);
43138
- res.writeHead(500, { "Content-Type": "application/json" });
43139
- res.end(JSON.stringify({ error: "Failed to fetch component" }));
45466
+ return createErrorSvg(`Parse error: ${error}`);
43140
45467
  }
43141
45468
  }
43142
- var logger8 = createLogger("http-server");
43143
- var DEFAULT_PORT = 3847;
43144
- var serverInstance = null;
43145
- function startHttpServer(options = {}) {
43146
- if (serverInstance) {
43147
- logger8.debug("HTTP server already running");
43148
- const port2 = options.port ?? parseInt(process.env.JLC_MCP_HTTP_PORT || String(DEFAULT_PORT), 10);
43149
- options.onReady?.(`http://localhost:${port2}`);
43150
- return port2;
45469
+ function renderSymbolRectangle(rect, bounds) {
45470
+ const start = getPoint(rect, "start");
45471
+ const end = getPoint(rect, "end");
45472
+ if (!start || !end)
45473
+ return "";
45474
+ const stroke = getStroke(rect);
45475
+ const fillType = getFillType(rect);
45476
+ const fill = fillType === "background" ? SYMBOL_COLORS.bodyFill : "none";
45477
+ updateBounds(bounds, start.x, start.y);
45478
+ updateBounds(bounds, end.x, end.y);
45479
+ const x = Math.min(start.x, end.x);
45480
+ const y = Math.min(start.y, end.y);
45481
+ const w = Math.abs(end.x - start.x);
45482
+ const h = Math.abs(end.y - start.y);
45483
+ return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${fill}" stroke="${SYMBOL_COLORS.body}" stroke-width="${stroke?.width ?? 0.254}"/>`;
45484
+ }
45485
+ function renderSymbolPolyline(polyline, bounds) {
45486
+ const points = getPoints(polyline);
45487
+ if (points.length < 2)
45488
+ return "";
45489
+ const stroke = getStroke(polyline);
45490
+ const fillType = getFillType(polyline);
45491
+ const fill = fillType === "background" ? SYMBOL_COLORS.bodyFill : "none";
45492
+ const pointsStr = points.map((p) => {
45493
+ updateBounds(bounds, p.x, p.y);
45494
+ return `${p.x},${p.y}`;
45495
+ }).join(" ");
45496
+ return `<polyline points="${pointsStr}" fill="${fill}" stroke="${SYMBOL_COLORS.body}" stroke-width="${stroke?.width ?? 0.254}" stroke-linecap="round" stroke-linejoin="round"/>`;
45497
+ }
45498
+ function renderSymbolCircle(circle, bounds) {
45499
+ const center = getPoint(circle, "center");
45500
+ const radius = getNumericAttr(circle, "radius");
45501
+ if (!center || radius === undefined)
45502
+ return "";
45503
+ const stroke = getStroke(circle);
45504
+ const fillType = getFillType(circle);
45505
+ const fill = fillType === "background" ? SYMBOL_COLORS.bodyFill : "none";
45506
+ updateBounds(bounds, center.x - radius, center.y - radius);
45507
+ updateBounds(bounds, center.x + radius, center.y + radius);
45508
+ return `<circle cx="${center.x}" cy="${center.y}" r="${radius}" fill="${fill}" stroke="${SYMBOL_COLORS.body}" stroke-width="${stroke?.width ?? 0.254}"/>`;
45509
+ }
45510
+ function renderSymbolArc(arc, bounds) {
45511
+ const start = getPoint(arc, "start");
45512
+ const mid = getPoint(arc, "mid");
45513
+ const end = getPoint(arc, "end");
45514
+ if (!start || !mid || !end)
45515
+ return "";
45516
+ const stroke = getStroke(arc);
45517
+ updateBounds(bounds, start.x, start.y);
45518
+ updateBounds(bounds, mid.x, mid.y);
45519
+ updateBounds(bounds, end.x, end.y);
45520
+ const arcPath = calculateArcPath(start, mid, end);
45521
+ return `<path d="${arcPath}" fill="none" stroke="${SYMBOL_COLORS.body}" stroke-width="${stroke?.width ?? 0.254}" stroke-linecap="round"/>`;
45522
+ }
45523
+ function calculateArcPath(start, mid, end) {
45524
+ const { x: ax, y: ay } = start;
45525
+ const { x: bx, y: by } = mid;
45526
+ const { x: cx, y: cy } = end;
45527
+ const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
45528
+ if (Math.abs(d) < 0.0001) {
45529
+ return `M ${start.x} ${start.y} L ${end.x} ${end.y}`;
45530
+ }
45531
+ const ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;
45532
+ const uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;
45533
+ const radius = Math.sqrt((ax - ux) ** 2 + (ay - uy) ** 2);
45534
+ const cross = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
45535
+ const sweepFlag = cross > 0 ? 0 : 1;
45536
+ const largeArcFlag = 0;
45537
+ return `M ${start.x} ${start.y} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${end.x} ${end.y}`;
45538
+ }
45539
+ function renderSymbolPin(pin, bounds) {
45540
+ const at = getPointWithRotation(pin, "at");
45541
+ const length = getNumericAttr(pin, "length") ?? 2.54;
45542
+ if (!at)
45543
+ return "";
45544
+ const nameChild = findChild(pin, "name");
45545
+ const numberChild = findChild(pin, "number");
45546
+ const pinName = nameChild && nameChild.length >= 2 && isAtom(nameChild[1]) ? nameChild[1] : "";
45547
+ const pinNumber = numberChild && numberChild.length >= 2 && isAtom(numberChild[1]) ? numberChild[1] : "";
45548
+ const rotation = at.rotation ?? 0;
45549
+ const radians = rotation * Math.PI / 180;
45550
+ const endX = at.x + length * Math.cos(radians);
45551
+ const endY = at.y + length * Math.sin(radians);
45552
+ updateBounds(bounds, at.x, at.y);
45553
+ updateBounds(bounds, endX, endY);
45554
+ const elements = [];
45555
+ elements.push(`<line x1="${at.x}" y1="${at.y}" x2="${endX}" y2="${endY}" stroke="${SYMBOL_COLORS.pin}" stroke-width="0.254"/>`);
45556
+ elements.push(`<circle cx="${at.x}" cy="${at.y}" r="0.3" fill="${SYMBOL_COLORS.pin}"/>`);
45557
+ if (pinName && pinName !== "~") {
45558
+ const textOffset = 0.5;
45559
+ let textX = endX;
45560
+ let textY = endY;
45561
+ let anchor = "start";
45562
+ let baseline = "middle";
45563
+ if (rotation === 0) {
45564
+ textX = endX + textOffset;
45565
+ anchor = "start";
45566
+ } else if (rotation === 180) {
45567
+ textX = endX - textOffset;
45568
+ anchor = "end";
45569
+ } else if (rotation === 90) {
45570
+ textY = endY + textOffset;
45571
+ baseline = "hanging";
45572
+ anchor = "middle";
45573
+ } else if (rotation === 270) {
45574
+ textY = endY - textOffset;
45575
+ baseline = "alphabetic";
45576
+ anchor = "middle";
45577
+ }
45578
+ elements.push(`<text x="${textX}" y="${textY}" fill="${SYMBOL_COLORS.pinText}" font-size="1" font-family="sans-serif" text-anchor="${anchor}" dominant-baseline="${baseline}" transform="scale(1,-1) translate(0,${-2 * textY})">${escapeXml(pinName)}</text>`);
45579
+ }
45580
+ if (pinNumber) {
45581
+ const midX = (at.x + endX) / 2;
45582
+ const midY = (at.y + endY) / 2;
45583
+ const offsetY = rotation === 0 || rotation === 180 ? 0.8 : 0;
45584
+ elements.push(`<text x="${midX}" y="${midY + offsetY}" fill="${SYMBOL_COLORS.pinText}" font-size="0.8" font-family="sans-serif" text-anchor="middle" dominant-baseline="middle" transform="scale(1,-1) translate(0,${-2 * (midY + offsetY)})">${escapeXml(pinNumber)}</text>`);
45585
+ }
45586
+ return elements.join(`
45587
+ `);
45588
+ }
45589
+ function renderFootprintSvg(sexpr) {
45590
+ if (!sexpr || sexpr.trim() === "") {
45591
+ return createErrorSvg("No footprint data");
43151
45592
  }
43152
- const port = options.port ?? parseInt(process.env.JLC_MCP_HTTP_PORT || String(DEFAULT_PORT), 10);
43153
- serverInstance = createServer(async (req, res) => {
43154
- res.setHeader("Access-Control-Allow-Origin", "*");
43155
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
43156
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
43157
- if (req.method === "OPTIONS") {
43158
- res.writeHead(204);
43159
- res.end();
43160
- return;
43161
- }
43162
- try {
43163
- await handleRequest(req, res);
43164
- } catch (error) {
43165
- logger8.error("Request error:", error);
43166
- res.writeHead(500, { "Content-Type": "application/json" });
43167
- res.end(JSON.stringify({ error: "Internal server error" }));
45593
+ try {
45594
+ const parsed = parseSExpr(sexpr);
45595
+ if (!isList(parsed) || getTag(parsed) !== "footprint") {
45596
+ return createErrorSvg("Invalid footprint format");
45597
+ }
45598
+ const elements = [];
45599
+ const bounds = { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity };
45600
+ for (const pad of findChildren(parsed, "pad")) {
45601
+ const svg = renderFootprintPad(pad, bounds);
45602
+ if (svg)
45603
+ elements.push(svg);
45604
+ }
45605
+ for (const line of findChildren(parsed, "fp_line")) {
45606
+ const svg = renderFootprintLine(line, bounds);
45607
+ if (svg)
45608
+ elements.push(svg);
45609
+ }
45610
+ for (const circle of findChildren(parsed, "fp_circle")) {
45611
+ const svg = renderFootprintCircle(circle, bounds);
45612
+ if (svg)
45613
+ elements.push(svg);
45614
+ }
45615
+ for (const arc of findChildren(parsed, "fp_arc")) {
45616
+ const svg = renderFootprintArc(arc, bounds);
45617
+ if (svg)
45618
+ elements.push(svg);
45619
+ }
45620
+ for (const rect of findChildren(parsed, "fp_rect")) {
45621
+ const svg = renderFootprintRect(rect, bounds);
45622
+ if (svg)
45623
+ elements.push(svg);
45624
+ }
45625
+ for (const poly of findChildren(parsed, "fp_poly")) {
45626
+ const svg = renderFootprintPoly(poly, bounds);
45627
+ if (svg)
45628
+ elements.push(svg);
45629
+ }
45630
+ for (const line of findChildren(parsed, "gr_line")) {
45631
+ const svg = renderFootprintLine(line, bounds);
45632
+ if (svg)
45633
+ elements.push(svg);
45634
+ }
45635
+ for (const circle of findChildren(parsed, "gr_circle")) {
45636
+ const svg = renderFootprintCircle(circle, bounds);
45637
+ if (svg)
45638
+ elements.push(svg);
45639
+ }
45640
+ for (const arc of findChildren(parsed, "gr_arc")) {
45641
+ const svg = renderFootprintArc(arc, bounds);
45642
+ if (svg)
45643
+ elements.push(svg);
45644
+ }
45645
+ for (const rect of findChildren(parsed, "gr_rect")) {
45646
+ const svg = renderFootprintRect(rect, bounds);
45647
+ if (svg)
45648
+ elements.push(svg);
45649
+ }
45650
+ for (const poly of findChildren(parsed, "gr_poly")) {
45651
+ const svg = renderFootprintPoly(poly, bounds);
45652
+ if (svg)
45653
+ elements.push(svg);
45654
+ }
45655
+ for (const text of findChildren(parsed, "fp_text")) {
45656
+ const svg = renderFootprintText(text, bounds);
45657
+ if (svg)
45658
+ elements.push(svg);
45659
+ }
45660
+ const padding = 1;
45661
+ if (!isFinite(bounds.minX)) {
45662
+ bounds.minX = -5;
45663
+ bounds.maxX = 5;
45664
+ bounds.minY = -5;
45665
+ bounds.maxY = 5;
45666
+ }
45667
+ const width = bounds.maxX - bounds.minX + padding * 2;
45668
+ const height = bounds.maxY - bounds.minY + padding * 2;
45669
+ const viewBox = `${bounds.minX - padding} ${bounds.minY - padding} ${width} ${height}`;
45670
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" width="100%" height="100%" style="background-color: ${FOOTPRINT_COLORS.background}">
45671
+ <g>
45672
+ ${elements.join(`
45673
+ `)}
45674
+ </g>
45675
+ </svg>`;
45676
+ } catch (error) {
45677
+ return createErrorSvg(`Parse error: ${error}`);
45678
+ }
45679
+ }
45680
+ function getLayerColor(layer) {
45681
+ if (layer.includes("F.Cu"))
45682
+ return FOOTPRINT_COLORS.fCu;
45683
+ if (layer.includes("B.Cu"))
45684
+ return FOOTPRINT_COLORS.bCu;
45685
+ if (/In\d+\.Cu/.test(layer))
45686
+ return FOOTPRINT_COLORS.in1Cu;
45687
+ if (layer.includes("F.SilkS"))
45688
+ return FOOTPRINT_COLORS.fSilkS;
45689
+ if (layer.includes("B.SilkS"))
45690
+ return FOOTPRINT_COLORS.bSilkS;
45691
+ if (layer.includes("F.Mask"))
45692
+ return FOOTPRINT_COLORS.fMask;
45693
+ if (layer.includes("B.Mask"))
45694
+ return FOOTPRINT_COLORS.bMask;
45695
+ if (layer.includes("F.Paste"))
45696
+ return FOOTPRINT_COLORS.fPaste;
45697
+ if (layer.includes("B.Paste"))
45698
+ return FOOTPRINT_COLORS.bPaste;
45699
+ if (layer.includes("F.Fab"))
45700
+ return FOOTPRINT_COLORS.fFab;
45701
+ if (layer.includes("B.Fab"))
45702
+ return FOOTPRINT_COLORS.bFab;
45703
+ if (layer.includes("F.CrtYd"))
45704
+ return FOOTPRINT_COLORS.fCrtYd;
45705
+ if (layer.includes("B.CrtYd"))
45706
+ return FOOTPRINT_COLORS.bCrtYd;
45707
+ if (layer.includes("Edge.Cuts"))
45708
+ return FOOTPRINT_COLORS.edgeCuts;
45709
+ if (layer.includes("Dwgs.User"))
45710
+ return FOOTPRINT_COLORS.dwgsUser;
45711
+ if (layer.includes("Cmts.User"))
45712
+ return FOOTPRINT_COLORS.cmtsUser;
45713
+ if (layer.includes("Margin"))
45714
+ return FOOTPRINT_COLORS.margin;
45715
+ return FOOTPRINT_COLORS.fSilkS;
45716
+ }
45717
+ function renderFootprintPad(pad, bounds) {
45718
+ if (!isList(pad) || pad.length < 4)
45719
+ return "";
45720
+ const padNumber = isAtom(pad[1]) ? pad[1] : "";
45721
+ const padType = isAtom(pad[2]) ? pad[2] : "";
45722
+ const padShape = isAtom(pad[3]) ? pad[3] : "";
45723
+ const at = getPointWithRotation(pad, "at");
45724
+ const size = getSize(pad);
45725
+ if (!at || !size)
45726
+ return "";
45727
+ updateBounds(bounds, at.x - size.width / 2, at.y - size.height / 2);
45728
+ updateBounds(bounds, at.x + size.width / 2, at.y + size.height / 2);
45729
+ const elements = [];
45730
+ const layers = getLayers(pad);
45731
+ let color;
45732
+ if (padType === "thru_hole" || padType === "np_thru_hole") {
45733
+ color = FOOTPRINT_COLORS.padThruHole;
45734
+ } else if (layers.some((l) => l.includes("B."))) {
45735
+ color = FOOTPRINT_COLORS.padBack;
45736
+ } else {
45737
+ color = FOOTPRINT_COLORS.padFront;
45738
+ }
45739
+ const rotation = at.rotation ?? 0;
45740
+ const transform = rotation !== 0 ? ` transform="rotate(${rotation}, ${at.x}, ${at.y})"` : "";
45741
+ if (padShape === "circle") {
45742
+ const r = Math.min(size.width, size.height) / 2;
45743
+ elements.push(`<circle cx="${at.x}" cy="${at.y}" r="${r}" fill="${color}"${transform}/>`);
45744
+ } else if (padShape === "oval") {
45745
+ const rx = size.width / 2;
45746
+ const ry = size.height / 2;
45747
+ elements.push(`<ellipse cx="${at.x}" cy="${at.y}" rx="${rx}" ry="${ry}" fill="${color}"${transform}/>`);
45748
+ } else if (padShape === "roundrect") {
45749
+ const rratio = getNumericAttr(pad, "roundrect_rratio") ?? 0.25;
45750
+ const rx = Math.min(size.width, size.height) * rratio / 2;
45751
+ const x = at.x - size.width / 2;
45752
+ const y = at.y - size.height / 2;
45753
+ elements.push(`<rect x="${x}" y="${y}" width="${size.width}" height="${size.height}" rx="${rx}" fill="${color}"${transform}/>`);
45754
+ } else if (padShape === "custom") {
45755
+ const primitives = findChild(pad, "primitives");
45756
+ if (primitives) {
45757
+ for (const prim of findChildren(primitives, "gr_poly")) {
45758
+ const points = getPoints(prim);
45759
+ if (points.length >= 3) {
45760
+ const polyStr = points.map((p) => {
45761
+ const absX = at.x + p.x;
45762
+ const absY = at.y + p.y;
45763
+ updateBounds(bounds, absX, absY);
45764
+ return `${absX},${absY}`;
45765
+ }).join(" ");
45766
+ elements.push(`<polygon points="${polyStr}" fill="${color}"${transform}/>`);
45767
+ }
45768
+ }
45769
+ }
45770
+ if (elements.length === 0) {
45771
+ const x = at.x - size.width / 2;
45772
+ const y = at.y - size.height / 2;
45773
+ elements.push(`<rect x="${x}" y="${y}" width="${size.width}" height="${size.height}" fill="${color}"${transform}/>`);
43168
45774
  }
43169
- });
43170
- serverInstance.listen(port, () => {
43171
- const url = `http://localhost:${port}`;
43172
- logger8.info(`HTTP server listening on ${url}`);
43173
- options.onReady?.(url);
43174
- });
43175
- serverInstance.on("error", (error) => {
43176
- if (error.code === "EADDRINUSE") {
43177
- logger8.warn(`Port ${port} already in use, HTTP server not started`);
43178
- } else {
43179
- logger8.error("HTTP server error:", error);
45775
+ } else {
45776
+ const x = at.x - size.width / 2;
45777
+ const y = at.y - size.height / 2;
45778
+ elements.push(`<rect x="${x}" y="${y}" width="${size.width}" height="${size.height}" fill="${color}"${transform}/>`);
45779
+ }
45780
+ if (padType === "thru_hole" || padType === "np_thru_hole") {
45781
+ const drill = findChild(pad, "drill");
45782
+ if (drill && drill.length >= 2) {
45783
+ const isOval = isAtom(drill[1]) && drill[1] === "oval";
45784
+ if (isOval && drill.length >= 4) {
45785
+ const dw = parseFloat(isAtom(drill[2]) ? drill[2] : "0");
45786
+ const dh = parseFloat(isAtom(drill[3]) ? drill[3] : "0");
45787
+ elements.push(`<ellipse cx="${at.x}" cy="${at.y}" rx="${dw / 2}" ry="${dh / 2}" fill="${FOOTPRINT_COLORS.drill}"${transform}/>`);
45788
+ } else {
45789
+ const drillSize = parseFloat(isAtom(drill[1]) ? drill[1] : "0");
45790
+ elements.push(`<circle cx="${at.x}" cy="${at.y}" r="${drillSize / 2}" fill="${FOOTPRINT_COLORS.drill}"/>`);
45791
+ }
43180
45792
  }
43181
- });
43182
- return port;
45793
+ }
45794
+ if (padNumber) {
45795
+ const fontSize = Math.min(size.width, size.height) * 0.5;
45796
+ elements.push(`<text x="${at.x}" y="${at.y}" fill="#FFFFFF" font-size="${fontSize}" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">${escapeXml(padNumber)}</text>`);
45797
+ }
45798
+ return elements.join(`
45799
+ `);
43183
45800
  }
43184
- function stopHttpServer() {
43185
- if (serverInstance) {
43186
- serverInstance.close();
43187
- serverInstance = null;
43188
- logger8.info("HTTP server stopped");
45801
+ function renderFootprintLine(line, bounds) {
45802
+ const start = getPoint(line, "start");
45803
+ const end = getPoint(line, "end");
45804
+ if (!start || !end)
45805
+ return "";
45806
+ const stroke = getStroke(line);
45807
+ const layers = getLayers(line);
45808
+ const layer = layers[0] ?? "F.SilkS";
45809
+ if (layer.includes("CrtYd"))
45810
+ return "";
45811
+ const color = getLayerColor(layer);
45812
+ updateBounds(bounds, start.x, start.y);
45813
+ updateBounds(bounds, end.x, end.y);
45814
+ return `<line x1="${start.x}" y1="${start.y}" x2="${end.x}" y2="${end.y}" stroke="${color}" stroke-width="${stroke?.width ?? 0.15}" stroke-linecap="round"/>`;
45815
+ }
45816
+ function renderFootprintCircle(circle, bounds) {
45817
+ const center = getPoint(circle, "center");
45818
+ const end = getPoint(circle, "end");
45819
+ if (!center || !end)
45820
+ return "";
45821
+ const radius = Math.sqrt((end.x - center.x) ** 2 + (end.y - center.y) ** 2);
45822
+ const stroke = getStroke(circle);
45823
+ const layers = getLayers(circle);
45824
+ const layer = layers[0] ?? "F.SilkS";
45825
+ if (layer.includes("CrtYd"))
45826
+ return "";
45827
+ const color = getLayerColor(layer);
45828
+ updateBounds(bounds, center.x - radius, center.y - radius);
45829
+ updateBounds(bounds, center.x + radius, center.y + radius);
45830
+ return `<circle cx="${center.x}" cy="${center.y}" r="${radius}" fill="none" stroke="${color}" stroke-width="${stroke?.width ?? 0.15}"/>`;
45831
+ }
45832
+ function renderFootprintArc(arc, bounds) {
45833
+ const start = getPoint(arc, "start");
45834
+ const mid = getPoint(arc, "mid");
45835
+ const end = getPoint(arc, "end");
45836
+ if (!start || !mid || !end)
45837
+ return "";
45838
+ const stroke = getStroke(arc);
45839
+ const layers = getLayers(arc);
45840
+ const layer = layers[0] ?? "F.SilkS";
45841
+ if (layer.includes("CrtYd"))
45842
+ return "";
45843
+ const color = getLayerColor(layer);
45844
+ updateBounds(bounds, start.x, start.y);
45845
+ updateBounds(bounds, mid.x, mid.y);
45846
+ updateBounds(bounds, end.x, end.y);
45847
+ const arcPath = calculateArcPath(start, mid, end);
45848
+ return `<path d="${arcPath}" fill="none" stroke="${color}" stroke-width="${stroke?.width ?? 0.15}" stroke-linecap="round"/>`;
45849
+ }
45850
+ function renderFootprintPoly(poly, bounds) {
45851
+ const points = getPoints(poly);
45852
+ if (points.length < 3)
45853
+ return "";
45854
+ const layers = getLayers(poly);
45855
+ const layer = layers[0] ?? "F.Cu";
45856
+ if (layer.includes("CrtYd"))
45857
+ return "";
45858
+ const color = getLayerColor(layer);
45859
+ const stroke = getStroke(poly);
45860
+ const fillType = getFillType(poly);
45861
+ const fill = fillType === "solid" || fillType === "yes" ? color : "none";
45862
+ const pointsStr = points.map((p) => {
45863
+ updateBounds(bounds, p.x, p.y);
45864
+ return `${p.x},${p.y}`;
45865
+ }).join(" ");
45866
+ const strokeWidth = stroke?.width ?? 0;
45867
+ const strokeAttr = strokeWidth > 0 ? ` stroke="${color}" stroke-width="${strokeWidth}"` : "";
45868
+ return `<polygon points="${pointsStr}" fill="${fill}"${strokeAttr}/>`;
45869
+ }
45870
+ function renderFootprintRect(rect, bounds) {
45871
+ const start = getPoint(rect, "start");
45872
+ const end = getPoint(rect, "end");
45873
+ if (!start || !end)
45874
+ return "";
45875
+ const layers = getLayers(rect);
45876
+ const layer = layers[0] ?? "F.SilkS";
45877
+ if (layer.includes("CrtYd"))
45878
+ return "";
45879
+ const color = getLayerColor(layer);
45880
+ const stroke = getStroke(rect);
45881
+ const fillType = getFillType(rect);
45882
+ const fill = fillType === "solid" ? color : "none";
45883
+ updateBounds(bounds, start.x, start.y);
45884
+ updateBounds(bounds, end.x, end.y);
45885
+ const x = Math.min(start.x, end.x);
45886
+ const y = Math.min(start.y, end.y);
45887
+ const w = Math.abs(end.x - start.x);
45888
+ const h = Math.abs(end.y - start.y);
45889
+ return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${fill}" stroke="${color}" stroke-width="${stroke?.width ?? 0.15}"/>`;
45890
+ }
45891
+ function renderFootprintText(text, bounds) {
45892
+ if (!isList(text) || text.length < 3)
45893
+ return "";
45894
+ const textType = isAtom(text[1]) ? text[1] : "";
45895
+ const textContent = isAtom(text[2]) ? text[2] : "";
45896
+ if (textType === "reference" || textType === "value")
45897
+ return "";
45898
+ const at = getPointWithRotation(text, "at");
45899
+ if (!at)
45900
+ return "";
45901
+ const layers = getLayers(text);
45902
+ const layer = layers[0] ?? "F.SilkS";
45903
+ const color = getLayerColor(layer);
45904
+ const effects = findChild(text, "effects");
45905
+ let fontSize = 1;
45906
+ let textAnchor = "middle";
45907
+ if (effects) {
45908
+ const font = findChild(effects, "font");
45909
+ if (font) {
45910
+ const size = getSize(font);
45911
+ if (size)
45912
+ fontSize = size.height;
45913
+ }
45914
+ const justify = findChild(effects, "justify");
45915
+ if (justify) {
45916
+ for (let i = 1;i < justify.length; i++) {
45917
+ const val = isAtom(justify[i]) ? justify[i] : "";
45918
+ if (val === "left")
45919
+ textAnchor = "start";
45920
+ else if (val === "right")
45921
+ textAnchor = "end";
45922
+ }
45923
+ }
45924
+ }
45925
+ const textWidth = textContent.length * fontSize * 0.6;
45926
+ const textHeight = fontSize;
45927
+ if (textAnchor === "start") {
45928
+ updateBounds(bounds, at.x, at.y - textHeight / 2);
45929
+ updateBounds(bounds, at.x + textWidth, at.y + textHeight / 2);
45930
+ } else if (textAnchor === "end") {
45931
+ updateBounds(bounds, at.x - textWidth, at.y - textHeight / 2);
45932
+ updateBounds(bounds, at.x, at.y + textHeight / 2);
45933
+ } else {
45934
+ updateBounds(bounds, at.x - textWidth / 2, at.y - textHeight / 2);
45935
+ updateBounds(bounds, at.x + textWidth / 2, at.y + textHeight / 2);
43189
45936
  }
45937
+ const rotation = at.rotation ?? 0;
45938
+ const transform = rotation !== 0 ? ` transform="rotate(${rotation}, ${at.x}, ${at.y})"` : "";
45939
+ return `<text x="${at.x}" y="${at.y}" dy="0.35em" fill="${color}" font-size="${fontSize}" font-family="sans-serif" text-anchor="${textAnchor}"${transform}>${escapeXml(textContent)}</text>`;
45940
+ }
45941
+ function updateBounds(bounds, x, y) {
45942
+ bounds.minX = Math.min(bounds.minX, x);
45943
+ bounds.maxX = Math.max(bounds.maxX, x);
45944
+ bounds.minY = Math.min(bounds.minY, y);
45945
+ bounds.maxY = Math.max(bounds.maxY, y);
45946
+ }
45947
+ function createErrorSvg(message) {
45948
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100" width="100%" height="100%">
45949
+ <rect width="200" height="100" fill="#FFF0F0"/>
45950
+ <text x="100" y="50" fill="#CC0000" font-size="12" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">${escapeXml(message)}</text>
45951
+ </svg>`;
43190
45952
  }
43191
45953
 
43192
45954
  // ../../node_modules/ink/build/render.js
@@ -43364,12 +46126,12 @@ var isWebWorker = typeof WorkerGlobalScope !== "undefined" && globalThis instanc
43364
46126
  var isDedicatedWorker = typeof DedicatedWorkerGlobalScope !== "undefined" && globalThis instanceof DedicatedWorkerGlobalScope;
43365
46127
  var isSharedWorker = typeof SharedWorkerGlobalScope !== "undefined" && globalThis instanceof SharedWorkerGlobalScope;
43366
46128
  var isServiceWorker = typeof ServiceWorkerGlobalScope !== "undefined" && globalThis instanceof ServiceWorkerGlobalScope;
43367
- var platform3 = globalThis.navigator?.userAgentData?.platform;
43368
- var isMacOs = platform3 === "macOS" || globalThis.navigator?.platform === "MacIntel" || globalThis.navigator?.userAgent?.includes(" Mac ") === true || globalThis.process?.platform === "darwin";
43369
- var isWindows = platform3 === "Windows" || globalThis.navigator?.platform === "Win32" || globalThis.process?.platform === "win32";
43370
- var isLinux = platform3 === "Linux" || globalThis.navigator?.platform?.startsWith("Linux") === true || globalThis.navigator?.userAgent?.includes(" Linux ") === true || globalThis.process?.platform === "linux";
43371
- var isIos = platform3 === "iOS" || globalThis.navigator?.platform === "MacIntel" && globalThis.navigator?.maxTouchPoints > 1 || /iPad|iPhone|iPod/.test(globalThis.navigator?.platform);
43372
- var isAndroid = platform3 === "Android" || globalThis.navigator?.platform === "Android" || globalThis.navigator?.userAgent?.includes(" Android ") === true || globalThis.process?.platform === "android";
46129
+ var platform4 = globalThis.navigator?.userAgentData?.platform;
46130
+ var isMacOs = platform4 === "macOS" || globalThis.navigator?.platform === "MacIntel" || globalThis.navigator?.userAgent?.includes(" Mac ") === true || globalThis.process?.platform === "darwin";
46131
+ var isWindows = platform4 === "Windows" || globalThis.navigator?.platform === "Win32" || globalThis.process?.platform === "win32";
46132
+ var isLinux = platform4 === "Linux" || globalThis.navigator?.platform?.startsWith("Linux") === true || globalThis.navigator?.userAgent?.includes(" Linux ") === true || globalThis.process?.platform === "linux";
46133
+ var isIos = platform4 === "iOS" || globalThis.navigator?.platform === "MacIntel" && globalThis.navigator?.maxTouchPoints > 1 || /iPad|iPhone|iPod/.test(globalThis.navigator?.platform);
46134
+ var isAndroid = platform4 === "Android" || globalThis.navigator?.platform === "Android" || globalThis.navigator?.userAgent?.includes(" Android ") === true || globalThis.process?.platform === "android";
43373
46135
 
43374
46136
  // ../../node_modules/ansi-escapes/base.js
43375
46137
  var ESC = "\x1B[";
@@ -45804,7 +48566,7 @@ function parseAnsiCode(string, offset) {
45804
48566
  return string.slice(0, endIndex + 1);
45805
48567
  }
45806
48568
  }
45807
- function tokenize(string, endCharacter = Number.POSITIVE_INFINITY) {
48569
+ function tokenize2(string, endCharacter = Number.POSITIVE_INFINITY) {
45808
48570
  const returnValue = [];
45809
48571
  let index = 0;
45810
48572
  let visibleCount = 0;
@@ -45857,7 +48619,7 @@ function undoAnsiCodes(codes) {
45857
48619
  return endCodes.reverse().join("");
45858
48620
  }
45859
48621
  function sliceAnsi(string, start, end) {
45860
- const tokens = tokenize(string, end);
48622
+ const tokens = tokenize2(string, end);
45861
48623
  let activeCodes = [];
45862
48624
  let position = 0;
45863
48625
  let returnValue = "";
@@ -47644,7 +50406,7 @@ function splitCompoundSGRSequences(code) {
47644
50406
  }
47645
50407
  return ret.map((part) => `\x1B[${part}m`);
47646
50408
  }
47647
- function tokenize2(str, endChar = Number.POSITIVE_INFINITY) {
50409
+ function tokenize3(str, endChar = Number.POSITIVE_INFINITY) {
47648
50410
  const ret = [];
47649
50411
  let index = 0;
47650
50412
  let visible = 0;
@@ -47802,7 +50564,7 @@ class Output {
47802
50564
  for (const transformer of transformers) {
47803
50565
  line = transformer(line, index);
47804
50566
  }
47805
- const characters = styledCharsFromTokens(tokenize2(line));
50567
+ const characters = styledCharsFromTokens(tokenize3(line));
47806
50568
  let offsetX = x;
47807
50569
  for (const character of characters) {
47808
50570
  currentLine[offsetX] = character;
@@ -49145,7 +51907,11 @@ function useNavigation() {
49145
51907
  }
49146
51908
  function useCurrentScreen() {
49147
51909
  const { history, currentIndex } = useNavigation();
49148
- return history[currentIndex];
51910
+ const entry = history[currentIndex];
51911
+ if (!entry) {
51912
+ throw new Error(`Invalid navigation state: currentIndex=${currentIndex}, history.length=${history.length}`);
51913
+ }
51914
+ return entry;
49149
51915
  }
49150
51916
 
49151
51917
  // src/app/state/AppStateContext.tsx
@@ -49740,7 +52506,7 @@ var is_in_ssh_default = isInSsh;
49740
52506
  var fallbackAttemptSymbol = Symbol("fallbackAttempt");
49741
52507
  var __dirname3 = import.meta.url ? path.dirname(fileURLToPath2(import.meta.url)) : "";
49742
52508
  var localXdgOpenPath = path.join(__dirname3, "xdg-open");
49743
- var { platform: platform5, arch } = process19;
52509
+ var { platform: platform6, arch } = process19;
49744
52510
  var tryEachApp = async (apps, opener) => {
49745
52511
  if (apps.length === 0) {
49746
52512
  return;
@@ -49836,7 +52602,7 @@ var baseOpen = async (options) => {
49836
52602
  if (is_wsl_default && !isInsideContainer() && !is_in_ssh_default && !app) {
49837
52603
  shouldUseWindowsInWsl = await canAccessPowerShell();
49838
52604
  }
49839
- if (platform5 === "darwin") {
52605
+ if (platform6 === "darwin") {
49840
52606
  command = "open";
49841
52607
  if (options.wait) {
49842
52608
  cliArguments.push("--wait-apps");
@@ -49850,7 +52616,7 @@ var baseOpen = async (options) => {
49850
52616
  if (app) {
49851
52617
  cliArguments.push("-a", app);
49852
52618
  }
49853
- } else if (platform5 === "win32" || shouldUseWindowsInWsl) {
52619
+ } else if (platform6 === "win32" || shouldUseWindowsInWsl) {
49854
52620
  command = await powerShellPath2();
49855
52621
  cliArguments.push(...executePowerShell.argumentsPrefix);
49856
52622
  if (!is_wsl_default) {
@@ -49889,7 +52655,7 @@ var baseOpen = async (options) => {
49889
52655
  await fs6.access(localXdgOpenPath, fsConstants2.X_OK);
49890
52656
  exeLocalXdgOpen = true;
49891
52657
  } catch {}
49892
- const useSystemXdgOpen = process19.versions.electron ?? (platform5 === "android" || isBundled || !exeLocalXdgOpen);
52658
+ const useSystemXdgOpen = process19.versions.electron ?? (platform6 === "android" || isBundled || !exeLocalXdgOpen);
49893
52659
  command = useSystemXdgOpen ? "xdg-open" : localXdgOpenPath;
49894
52660
  }
49895
52661
  if (appArguments.length > 0) {
@@ -49900,7 +52666,7 @@ var baseOpen = async (options) => {
49900
52666
  childProcessOptions.detached = true;
49901
52667
  }
49902
52668
  }
49903
- if (platform5 === "darwin" && appArguments.length > 0) {
52669
+ if (platform6 === "darwin" && appArguments.length > 0) {
49904
52670
  cliArguments.push("--args", ...appArguments);
49905
52671
  }
49906
52672
  if (options.target) {
@@ -49963,12 +52729,12 @@ function detectArchBinary(binary) {
49963
52729
  }
49964
52730
  return archBinary;
49965
52731
  }
49966
- function detectPlatformBinary({ [platform5]: platformBinary }, { wsl } = {}) {
52732
+ function detectPlatformBinary({ [platform6]: platformBinary }, { wsl } = {}) {
49967
52733
  if (wsl && is_wsl_default) {
49968
52734
  return detectArchBinary(wsl);
49969
52735
  }
49970
52736
  if (!platformBinary) {
49971
- throw new Error(`${platform5} is not supported`);
52737
+ throw new Error(`${platform6} is not supported`);
49972
52738
  }
49973
52739
  return detectArchBinary(platformBinary);
49974
52740
  }
@@ -50255,7 +53021,7 @@ function DetailView({ component, terminalWidth, isInstalled, installedInfo, stat
50255
53021
  }, undefined, true, undefined, this)
50256
53022
  ]
50257
53023
  }, undefined, true, undefined, this) : null;
50258
- const footerText = isInstalled ? "S SymbolF FootprintM 3D Model • R Regenerate • D Datasheet • Esc Back" : "Enter Install • D Datasheet • Esc Back";
53024
+ const footerText = isInstalled ? "R RegenerateD DeleteO Datasheet • Esc Back" : "Enter Install • O Datasheet • Esc Back";
50259
53025
  return /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Box_default, {
50260
53026
  flexDirection: "column",
50261
53027
  width: "100%",
@@ -50354,29 +53120,12 @@ function InfoScreen() {
50354
53120
  }, [params.componentId, params.component]);
50355
53121
  const datasheetUrl = component && ("datasheetPdf" in component ? component.datasheetPdf : ("datasheet" in component) ? component.datasheet : undefined);
50356
53122
  const [isRegenerating, setIsRegenerating] = import_react30.useState(false);
53123
+ const [isDeleting, setIsDeleting] = import_react30.useState(false);
50357
53124
  const [regenerateMessage, setRegenerateMessage] = import_react30.useState(null);
50358
53125
  use_input_default((input, key) => {
50359
- if (isLoading || !component || isCheckingLibrary || isRegenerating)
53126
+ if (isLoading || !component || isCheckingLibrary || isRegenerating || isDeleting)
50360
53127
  return;
50361
53128
  const lowerInput = input.toLowerCase();
50362
- if (lowerInput === "s" && installedInfo && libraryStatus) {
50363
- const symbolPath = `${libraryStatus.paths.symbolsDir}/JLC-MCP-${installedInfo.category}.kicad_sym`;
50364
- open_default(symbolPath);
50365
- return;
50366
- }
50367
- if (lowerInput === "f" && installedInfo && libraryStatus) {
50368
- if (installedInfo.footprintRef?.startsWith("JLC-MCP:")) {
50369
- const fpName = installedInfo.footprintRef.split(":")[1];
50370
- const footprintPath = `${libraryStatus.paths.footprintsDir}/JLC-MCP.pretty/${fpName}.kicad_mod`;
50371
- open_default(footprintPath);
50372
- }
50373
- return;
50374
- }
50375
- if (lowerInput === "m" && installedInfo && libraryStatus && installedInfo.has3dModel) {
50376
- const modelPath = `${libraryStatus.paths.models3dDir}/${installedInfo.name}.step`;
50377
- open_default(modelPath);
50378
- return;
50379
- }
50380
53129
  if (lowerInput === "r" && installedInfo) {
50381
53130
  setIsRegenerating(true);
50382
53131
  setRegenerateMessage("Regenerating symbol and footprint...");
@@ -50389,7 +53138,20 @@ function InfoScreen() {
50389
53138
  }).finally(() => setIsRegenerating(false));
50390
53139
  return;
50391
53140
  }
50392
- if (lowerInput === "d" && datasheetUrl) {
53141
+ if (lowerInput === "d" && installedInfo) {
53142
+ setIsDeleting(true);
53143
+ setRegenerateMessage("Deleting component...");
53144
+ libraryService.remove(installedInfo.lcscId).then(() => {
53145
+ setRegenerateMessage("✓ Component deleted");
53146
+ setInstalledInfo(null);
53147
+ setTimeout(() => setRegenerateMessage(null), 2000);
53148
+ }).catch((err) => {
53149
+ setRegenerateMessage(`✗ Delete failed: ${err instanceof Error ? err.message : "Unknown error"}`);
53150
+ setTimeout(() => setRegenerateMessage(null), 3000);
53151
+ }).finally(() => setIsDeleting(false));
53152
+ return;
53153
+ }
53154
+ if (lowerInput === "o" && datasheetUrl) {
50393
53155
  open_default(datasheetUrl);
50394
53156
  return;
50395
53157
  }
@@ -51721,11 +54483,12 @@ function App2({ initialScreen, initialParams }) {
51721
54483
  }, undefined, false, undefined, this)
51722
54484
  }, undefined, false, undefined, this);
51723
54485
  }
51724
- function renderApp(initialScreen, initialParams) {
51725
- render_default(/* @__PURE__ */ jsx_dev_runtime15.jsxDEV(App2, {
54486
+ async function renderApp(initialScreen, initialParams) {
54487
+ const instance = render_default(/* @__PURE__ */ jsx_dev_runtime15.jsxDEV(App2, {
51726
54488
  initialScreen,
51727
54489
  initialParams
51728
54490
  }, undefined, false, undefined, this));
54491
+ await instance.waitUntilExit();
51729
54492
  }
51730
54493
 
51731
54494
  // src/commands/search.ts
@@ -51748,7 +54511,7 @@ async function searchCommand(query, options) {
51748
54511
  return;
51749
54512
  }
51750
54513
  process.stdout.write("\x1B[1A\x1B[2K");
51751
- renderApp("search", { query, results });
54514
+ await renderApp("search", { query, results });
51752
54515
  } catch (error) {
51753
54516
  console.error(`Search failed: ${error instanceof Error ? error.message : "Unknown error"}`);
51754
54517
  process.exit(1);
@@ -52112,6 +54875,33 @@ class x {
52112
54875
  }
52113
54876
  }
52114
54877
  }
54878
+ var SD = Object.defineProperty;
54879
+ var $D = (t, u, F) => (u in t) ? SD(t, u, { enumerable: true, configurable: true, writable: true, value: F }) : t[u] = F;
54880
+ var q = (t, u, F) => ($D(t, typeof u != "symbol" ? u + "" : u, F), F);
54881
+
54882
+ class jD extends x {
54883
+ constructor(u) {
54884
+ super(u, false), q(this, "options"), q(this, "cursor", 0), this.options = u.options, this.cursor = this.options.findIndex(({ value: F }) => F === u.initialValue), this.cursor === -1 && (this.cursor = 0), this.changeValue(), this.on("cursor", (F) => {
54885
+ switch (F) {
54886
+ case "left":
54887
+ case "up":
54888
+ this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
54889
+ break;
54890
+ case "down":
54891
+ case "right":
54892
+ this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
54893
+ break;
54894
+ }
54895
+ this.changeValue();
54896
+ });
54897
+ }
54898
+ get _value() {
54899
+ return this.options[this.cursor];
54900
+ }
54901
+ changeValue() {
54902
+ this.value = this._value.value;
54903
+ }
54904
+ }
52115
54905
  class PD extends x {
52116
54906
  get valueWithCursor() {
52117
54907
  if (this.state === "submit")
@@ -52174,6 +54964,16 @@ var y2 = (s) => {
52174
54964
  return import_picocolors2.default.green(S2);
52175
54965
  }
52176
54966
  };
54967
+ var k2 = (s) => {
54968
+ const { cursor: n, options: t, style: i } = s, r2 = s.maxItems ?? Number.POSITIVE_INFINITY, c2 = Math.max(process.stdout.rows - 4, 0), o = Math.min(c2, Math.max(r2, 5));
54969
+ let l2 = 0;
54970
+ n >= l2 + o - 3 ? l2 = Math.max(Math.min(n - o + 3, t.length - o), 0) : n < l2 + 2 && (l2 = Math.max(n - 2, 0));
54971
+ const $2 = o < t.length && l2 > 0, d2 = o < t.length && l2 + o < t.length;
54972
+ return t.slice(l2, l2 + o).map((w2, b2, C) => {
54973
+ const I2 = b2 === 0 && $2, x2 = b2 === C.length - 1 && d2;
54974
+ return I2 || x2 ? import_picocolors2.default.dim("...") : i(w2, b2 + l2 === n);
54975
+ });
54976
+ };
52177
54977
  var ue = (s) => new PD({ validate: s.validate, placeholder: s.placeholder, defaultValue: s.defaultValue, initialValue: s.initialValue, render() {
52178
54978
  const n = `${import_picocolors2.default.gray(a)}
52179
54979
  ${y2(this.state)} ${s.message}
@@ -52195,6 +54995,38 @@ ${import_picocolors2.default.cyan(m2)}
52195
54995
  `;
52196
54996
  }
52197
54997
  } }).prompt();
54998
+ var de = (s) => {
54999
+ const n = (t, i) => {
55000
+ const r2 = t.label ?? String(t.value);
55001
+ switch (i) {
55002
+ case "selected":
55003
+ return `${import_picocolors2.default.dim(r2)}`;
55004
+ case "active":
55005
+ return `${import_picocolors2.default.green(j2)} ${r2} ${t.hint ? import_picocolors2.default.dim(`(${t.hint})`) : ""}`;
55006
+ case "cancelled":
55007
+ return `${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(r2))}`;
55008
+ default:
55009
+ return `${import_picocolors2.default.dim(R2)} ${import_picocolors2.default.dim(r2)}`;
55010
+ }
55011
+ };
55012
+ return new jD({ options: s.options, initialValue: s.initialValue, render() {
55013
+ const t = `${import_picocolors2.default.gray(a)}
55014
+ ${y2(this.state)} ${s.message}
55015
+ `;
55016
+ switch (this.state) {
55017
+ case "submit":
55018
+ return `${t}${import_picocolors2.default.gray(a)} ${n(this.options[this.cursor], "selected")}`;
55019
+ case "cancel":
55020
+ return `${t}${import_picocolors2.default.gray(a)} ${n(this.options[this.cursor], "cancelled")}
55021
+ ${import_picocolors2.default.gray(a)}`;
55022
+ default:
55023
+ return `${t}${import_picocolors2.default.cyan(a)} ${k2({ cursor: this.cursor, options: this.options, maxItems: s.maxItems, style: (i, r2) => n(i, r2 ? "active" : "inactive") }).join(`
55024
+ ${import_picocolors2.default.cyan(a)} `)}
55025
+ ${import_picocolors2.default.cyan(m2)}
55026
+ `;
55027
+ }
55028
+ } }).prompt();
55029
+ };
52198
55030
  var ve = (s = "") => {
52199
55031
  process.stdout.write(`${import_picocolors2.default.gray(m2)} ${import_picocolors2.default.red(s)}
52200
55032
 
@@ -52280,24 +55112,24 @@ async function infoCommand(id, options) {
52280
55112
  }
52281
55113
  return;
52282
55114
  }
52283
- renderApp("info", { componentId: id });
55115
+ await renderApp("info", { componentId: id });
52284
55116
  }
52285
55117
 
52286
55118
  // src/commands/install.ts
52287
55119
  var componentService7 = createComponentService();
52288
55120
  var libraryService6 = createLibraryService();
52289
- function isLcscId3(id) {
55121
+ function isLcscId(id) {
52290
55122
  return /^C\d+$/i.test(id);
52291
55123
  }
52292
55124
  async function installCommand(id, options) {
52293
- if (id && !isLcscId3(id)) {
55125
+ if (id && !isLcscId(id)) {
52294
55126
  v2.error(`"${id}" is not an LCSC part number (e.g., C2040).`);
52295
55127
  v2.info(`For EasyEDA community components, use: ${source_default.cyan(`jlc easyeda install ${id}`)}`);
52296
55128
  process.exit(1);
52297
55129
  }
52298
55130
  if (id && options.force) {
52299
- const spinner2 = L2();
52300
- spinner2.start(`Installing component ${id}...`);
55131
+ const spinner = L2();
55132
+ spinner.start(`Installing component ${id}...`);
52301
55133
  try {
52302
55134
  await libraryService6.ensureGlobalTables();
52303
55135
  const result = await libraryService6.install(id, {
@@ -52305,7 +55137,7 @@ async function installCommand(id, options) {
52305
55137
  include3d: options.include3d,
52306
55138
  force: true
52307
55139
  });
52308
- spinner2.stop(source_default.green("✓ Component installed"));
55140
+ spinner.stop(source_default.green("✓ Component installed"));
52309
55141
  console.log();
52310
55142
  console.log(source_default.cyan("Symbol: "), result.symbolRef);
52311
55143
  console.log(source_default.cyan("Footprint: "), result.footprintRef);
@@ -52316,22 +55148,20 @@ async function installCommand(id, options) {
52316
55148
  console.log();
52317
55149
  console.log(source_default.dim(`Library: ${result.files.symbolLibrary}`));
52318
55150
  } catch (error) {
52319
- spinner2.stop(source_default.red("✗ Installation failed"));
55151
+ spinner.stop(source_default.red("✗ Installation failed"));
52320
55152
  v2.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
52321
55153
  process.exit(1);
52322
55154
  }
52323
55155
  return;
52324
55156
  }
52325
55157
  if (id) {
52326
- const spinner2 = L2();
52327
- spinner2.start(`Fetching component ${id}...`);
55158
+ console.log(`Fetching component ${id}...`);
52328
55159
  try {
52329
55160
  const details = await componentService7.getDetails(id);
52330
- spinner2.stop("Component found");
52331
- renderApp("info", { componentId: id, component: details });
55161
+ process.stdout.write("\x1B[1A\x1B[2K");
55162
+ await renderApp("info", { componentId: id, component: details });
52332
55163
  } catch (error) {
52333
- spinner2.stop("Failed to fetch component");
52334
- v2.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
55164
+ console.error(`Failed to fetch component: ${error instanceof Error ? error.message : "Unknown error"}`);
52335
55165
  process.exit(1);
52336
55166
  }
52337
55167
  return;
@@ -52349,8 +55179,7 @@ async function installCommand(id, options) {
52349
55179
  ve("Installation cancelled");
52350
55180
  process.exit(0);
52351
55181
  }
52352
- const spinner = L2();
52353
- spinner.start(`Searching for "${query}"...`);
55182
+ console.log(`Searching for "${query}"...`);
52354
55183
  const searchOptions = { limit: 20 };
52355
55184
  let results = await componentService7.search(query, searchOptions);
52356
55185
  results = results.sort((a2, b2) => {
@@ -52360,12 +55189,12 @@ async function installCommand(id, options) {
52360
55189
  return 1;
52361
55190
  return 0;
52362
55191
  });
52363
- spinner.stop(`Found ${results.length} results`);
52364
55192
  if (results.length === 0) {
52365
- v2.warn("No components found. Try a different search term.");
55193
+ console.log("No components found. Try a different search term.");
52366
55194
  return;
52367
55195
  }
52368
- renderApp("search", { query, results });
55196
+ process.stdout.write("\x1B[1A\x1B[2K");
55197
+ await renderApp("search", { query, results });
52369
55198
  }
52370
55199
 
52371
55200
  // src/commands/library.ts
@@ -52397,7 +55226,41 @@ async function libraryCommand(options) {
52397
55226
  }
52398
55227
  return;
52399
55228
  }
52400
- renderApp("library", {});
55229
+ await renderApp("library", {});
55230
+ }
55231
+ async function regenerateCommand(options) {
55232
+ const include3d = options.include3d ?? true;
55233
+ const components = await libraryService7.listInstalled({ projectPath: options.projectPath });
55234
+ if (components.length === 0) {
55235
+ v2.warn("No installed components found to regenerate.");
55236
+ return;
55237
+ }
55238
+ v2.info(`Found ${source_default.cyan(components.length)} installed components to regenerate.`);
55239
+ console.log();
55240
+ let currentSpinner = null;
55241
+ const result = await libraryService7.regenerate({
55242
+ projectPath: options.projectPath,
55243
+ include3d,
55244
+ onProgress: (current, total, component, status, error) => {
55245
+ if (status === "start") {
55246
+ currentSpinner = L2();
55247
+ currentSpinner.start(`[${current}/${total}] Regenerating ${source_default.cyan(component.name)} (${component.lcscId})...`);
55248
+ } else if (status === "success") {
55249
+ currentSpinner?.stop(source_default.green(`✓ [${current}/${total}] ${component.name} (${component.lcscId})`));
55250
+ } else if (status === "error") {
55251
+ currentSpinner?.stop(source_default.red(`✗ [${current}/${total}] ${component.name} (${component.lcscId}): ${error}`));
55252
+ }
55253
+ }
55254
+ });
55255
+ console.log();
55256
+ v2.success(`Regeneration complete: ${source_default.green(result.success)} succeeded, ${source_default.red(result.failed)} failed`);
55257
+ if (result.failed > 0) {
55258
+ console.log();
55259
+ v2.warn("Failed components:");
55260
+ for (const comp of result.components.filter((c2) => c2.status === "failed")) {
55261
+ console.log(source_default.red(` • ${comp.name} (${comp.id}): ${comp.error}`));
55262
+ }
55263
+ }
52401
55264
  }
52402
55265
 
52403
55266
  // src/commands/easyeda.ts
@@ -52427,8 +55290,8 @@ Shutting down server...`);
52427
55290
  }
52428
55291
  async function easyedaInstallCommand(uuid, options) {
52429
55292
  if (uuid && options.force) {
52430
- const spinner2 = L2();
52431
- spinner2.start(`Installing EasyEDA component ${uuid}...`);
55293
+ const spinner = L2();
55294
+ spinner.start(`Installing EasyEDA component ${uuid}...`);
52432
55295
  try {
52433
55296
  await libraryService8.ensureGlobalTables();
52434
55297
  const result = await libraryService8.install(uuid, {
@@ -52436,7 +55299,7 @@ async function easyedaInstallCommand(uuid, options) {
52436
55299
  include3d: options.include3d,
52437
55300
  force: true
52438
55301
  });
52439
- spinner2.stop(source_default.green("✓ Component installed"));
55302
+ spinner.stop(source_default.green("✓ Component installed"));
52440
55303
  console.log();
52441
55304
  console.log(source_default.cyan("Symbol: "), result.symbolRef);
52442
55305
  console.log(source_default.cyan("Footprint: "), result.footprintRef);
@@ -52447,14 +55310,14 @@ async function easyedaInstallCommand(uuid, options) {
52447
55310
  console.log();
52448
55311
  console.log(source_default.dim(`Library: ${result.files.symbolLibrary}`));
52449
55312
  } catch (error) {
52450
- spinner2.stop(source_default.red("✗ Installation failed"));
55313
+ spinner.stop(source_default.red("✗ Installation failed"));
52451
55314
  v2.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
52452
55315
  process.exit(1);
52453
55316
  }
52454
55317
  return;
52455
55318
  }
52456
55319
  if (uuid) {
52457
- renderApp("easyeda-info", { uuid });
55320
+ await renderApp("easyeda-info", { uuid });
52458
55321
  return;
52459
55322
  }
52460
55323
  const query = await ue({
@@ -52470,19 +55333,197 @@ async function easyedaInstallCommand(uuid, options) {
52470
55333
  ve("Installation cancelled");
52471
55334
  process.exit(0);
52472
55335
  }
52473
- const spinner = L2();
52474
- spinner.start(`Searching EasyEDA community for "${query}"...`);
55336
+ console.log(`Searching EasyEDA community for "${query}"...`);
52475
55337
  const searchOptions = {
52476
55338
  limit: 20,
52477
55339
  source: "easyeda-community"
52478
55340
  };
52479
55341
  const results = await componentService8.search(query, searchOptions);
52480
- spinner.stop(`Found ${results.length} results`);
52481
55342
  if (results.length === 0) {
52482
- v2.warn("No components found. Try a different search term.");
55343
+ console.log("No components found. Try a different search term.");
52483
55344
  return;
52484
55345
  }
52485
- renderApp("search", { query, results });
55346
+ process.stdout.write("\x1B[1A\x1B[2K");
55347
+ await renderApp("search", { query, results });
55348
+ }
55349
+
55350
+ // src/commands/validate.ts
55351
+ import { writeFileSync } from "fs";
55352
+ async function validateComponent(lcscCode, options) {
55353
+ const startTime = Date.now();
55354
+ const normalizedCode = lcscCode.replace(/^C/i, "");
55355
+ const fullCode = `C${normalizedCode}`;
55356
+ try {
55357
+ const reference = await fetchReferenceSVG(fullCode);
55358
+ const componentData = await easyedaClient.getComponentData(fullCode);
55359
+ if (!componentData) {
55360
+ return {
55361
+ lcscCode: fullCode,
55362
+ componentName: fullCode,
55363
+ passed: false,
55364
+ footprint: null,
55365
+ symbol: null,
55366
+ timestamp: new Date,
55367
+ durationMs: Date.now() - startTime,
55368
+ error: "Failed to fetch component data from EasyEDA"
55369
+ };
55370
+ }
55371
+ let footprintResult = null;
55372
+ let symbolResult = null;
55373
+ if (options.footprint && reference.footprintSvg) {
55374
+ const kicadContent = footprintConverter.convert(componentData, {});
55375
+ const refData = extractFromReferenceSVG(reference.footprintSvg);
55376
+ const genData = extractFromKiCadFootprint(kicadContent);
55377
+ footprintResult = compareFootprints(refData, genData, {
55378
+ sizeWarningsOnly: true,
55379
+ positionTolerance: 50
55380
+ });
55381
+ }
55382
+ if (options.symbol && reference.symbolSvg) {
55383
+ const kicadContent = symbolConverter.convert(componentData, {
55384
+ symbolName: componentData.info.name
55385
+ });
55386
+ const refData = extractFromReferenceSVG2(reference.symbolSvg);
55387
+ const genData = extractFromKiCadSymbol(kicadContent);
55388
+ symbolResult = compareSymbols(refData, genData, {
55389
+ positionTolerance: 50
55390
+ });
55391
+ }
55392
+ const passed = (!footprintResult || footprintResult.passed) && (!symbolResult || symbolResult.passed);
55393
+ return {
55394
+ lcscCode: fullCode,
55395
+ componentName: componentData.info.name || fullCode,
55396
+ passed,
55397
+ footprint: footprintResult,
55398
+ symbol: symbolResult,
55399
+ timestamp: new Date,
55400
+ durationMs: Date.now() - startTime
55401
+ };
55402
+ } catch (error) {
55403
+ return {
55404
+ lcscCode: fullCode,
55405
+ componentName: fullCode,
55406
+ passed: false,
55407
+ footprint: null,
55408
+ symbol: null,
55409
+ timestamp: new Date,
55410
+ durationMs: Date.now() - startTime,
55411
+ error: error instanceof Error ? error.message : String(error)
55412
+ };
55413
+ }
55414
+ }
55415
+ async function validateCommand(id, options) {
55416
+ const validateFootprint = !options.symbolOnly;
55417
+ const validateSymbol = !options.footprintOnly;
55418
+ if (id && !options.category && !options.all) {
55419
+ const spinner = L2();
55420
+ spinner.start(`Validating ${id}...`);
55421
+ const result = await validateComponent(id, {
55422
+ footprint: validateFootprint,
55423
+ symbol: validateSymbol
55424
+ });
55425
+ spinner.stop(result.passed ? source_default.green("✓ Validation passed") : source_default.red("✗ Validation failed"));
55426
+ console.log();
55427
+ console.log(source_default.bold(`${result.componentName} (${result.lcscCode})`));
55428
+ console.log(source_default.dim(`Duration: ${result.durationMs}ms`));
55429
+ console.log();
55430
+ if (result.error) {
55431
+ console.log(source_default.red(`Error: ${result.error}`));
55432
+ process.exit(1);
55433
+ }
55434
+ if (result.footprint) {
55435
+ console.log(formatComparisonResult(result.footprint));
55436
+ }
55437
+ if (result.symbol) {
55438
+ console.log(formatSymbolComparisonResult(result.symbol));
55439
+ }
55440
+ if (options.json) {
55441
+ console.log(JSON.stringify(result, null, 2));
55442
+ }
55443
+ if (options.html) {
55444
+ const reference = await fetchReferenceSVG(id);
55445
+ const componentData = await easyedaClient.getComponentData(id);
55446
+ const svgs = {
55447
+ footprintRef: reference.footprintSvg || undefined,
55448
+ symbolRef: reference.symbolSvg || undefined
55449
+ };
55450
+ if (componentData) {
55451
+ if (validateFootprint) {
55452
+ const kicadFootprint = footprintConverter.convert(componentData, {});
55453
+ svgs.footprintGen = renderFootprintSvg(kicadFootprint);
55454
+ }
55455
+ if (validateSymbol) {
55456
+ const kicadSymbol = symbolConverter.convert(componentData, {
55457
+ symbolName: componentData.info.name
55458
+ });
55459
+ svgs.symbolGen = renderSymbolSvg(kicadSymbol);
55460
+ }
55461
+ }
55462
+ const html = generateValidationReport(result, svgs);
55463
+ writeFileSync(options.html, html);
55464
+ console.log(source_default.dim(`HTML report: ${options.html}`));
55465
+ }
55466
+ process.exit(result.passed ? 0 : 1);
55467
+ }
55468
+ let components = options.all ? getAllTestComponents() : options.category ? getTestComponentsByCategory(options.category) : [];
55469
+ if (components.length === 0 && !id) {
55470
+ const categories = getCategoryNames();
55471
+ const selected = await de({
55472
+ message: "Select a category to validate:",
55473
+ options: [
55474
+ { value: "all", label: "All categories" },
55475
+ ...categories.map((c2) => ({ value: c2, label: c2 }))
55476
+ ]
55477
+ });
55478
+ if (BD(selected)) {
55479
+ ve("Validation cancelled");
55480
+ process.exit(0);
55481
+ }
55482
+ components = selected === "all" ? getAllTestComponents() : getTestComponentsByCategory(selected);
55483
+ }
55484
+ components = components.filter((c2) => !c2.skip);
55485
+ if (components.length === 0) {
55486
+ v2.warn("No components to validate");
55487
+ process.exit(0);
55488
+ }
55489
+ console.log(source_default.bold(`
55490
+ Validating ${components.length} components...
55491
+ `));
55492
+ const results = [];
55493
+ let passed = 0;
55494
+ let failed = 0;
55495
+ let errors = 0;
55496
+ for (const component of components) {
55497
+ process.stdout.write(` ${component.lcsc.padEnd(12)} ${component.name.slice(0, 40).padEnd(42)} `);
55498
+ const result = await validateComponent(component.lcsc, {
55499
+ footprint: validateFootprint,
55500
+ symbol: validateSymbol
55501
+ });
55502
+ results.push(result);
55503
+ if (result.error) {
55504
+ errors++;
55505
+ console.log(source_default.yellow("⚠ ERROR"));
55506
+ } else if (result.passed) {
55507
+ passed++;
55508
+ console.log(source_default.green("✓ PASS"));
55509
+ } else {
55510
+ failed++;
55511
+ console.log(source_default.red("✗ FAIL"));
55512
+ }
55513
+ }
55514
+ console.log();
55515
+ console.log(source_default.bold("Summary:"));
55516
+ console.log(` ${source_default.green(`${passed} passed`)} ${source_default.red(`${failed} failed`)} ${source_default.yellow(`${errors} errors`)}`);
55517
+ console.log();
55518
+ if (options.json) {
55519
+ console.log(JSON.stringify(results, null, 2));
55520
+ }
55521
+ if (options.html) {
55522
+ const html = generateBatchReport(results, "JLC-CLI Validation Report");
55523
+ writeFileSync(options.html, html);
55524
+ console.log(source_default.dim(`HTML report: ${options.html}`));
55525
+ }
55526
+ process.exit(failed > 0 || errors > 0 ? 1 : 0);
52486
55527
  }
52487
55528
 
52488
55529
  // src/index.ts
@@ -52507,11 +55548,28 @@ program2.command("install [id]").description("Install component to KiCad librari
52507
55548
  force: options.force
52508
55549
  });
52509
55550
  });
52510
- program2.command("library").description("View JLC-MCP library status and installed components").option("--json", "Output as JSON").action(async (options) => {
55551
+ var library = program2.command("library").description("JLC-MCP library management");
55552
+ library.command("status", { isDefault: true }).description("View library status and installed components").option("--json", "Output as JSON").action(async (options) => {
52511
55553
  await libraryCommand({
52512
55554
  json: options.json
52513
55555
  });
52514
55556
  });
55557
+ library.command("regenerate").description("Regenerate all installed components (refetch and reconvert symbols, footprints, 3D models)").option("-p, --project <path>", "Regenerate project-local library").option("--no-3d", "Skip 3D model download").action(async (options) => {
55558
+ await regenerateCommand({
55559
+ projectPath: options.project,
55560
+ include3d: options["3d"]
55561
+ });
55562
+ });
55563
+ program2.command("validate [id]").description("Validate footprint/symbol generation against JLCPCB reference").option("--footprint-only", "Only validate footprint").option("--symbol-only", "Only validate symbol").option("-c, --category <name>", "Validate all components in category").option("-a, --all", "Validate all test fixtures").option("--html <path>", "Output HTML report to file").option("--json", "Output as JSON").action(async (id, options) => {
55564
+ await validateCommand(id, {
55565
+ footprintOnly: options.footprintOnly,
55566
+ symbolOnly: options.symbolOnly,
55567
+ category: options.category,
55568
+ all: options.all,
55569
+ html: options.html,
55570
+ json: options.json
55571
+ });
55572
+ });
52515
55573
  var easyeda = program2.command("easyeda").description("EasyEDA community component browser");
52516
55574
  easyeda.command("search <query...>").description("Open browser-based component search").option("-p, --port <number>", "HTTP server port", "3847").action(async (queryParts, options) => {
52517
55575
  const query = queryParts.join(" ");