@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/CHANGELOG.md +44 -0
- package/dist/assets/search.html +11 -11
- package/dist/index.js +3441 -383
- package/package.json +2 -1
- package/src/app/App.tsx +4 -3
- package/src/app/components/DetailView.tsx +2 -2
- package/src/app/navigation/NavigationContext.tsx +5 -1
- package/src/app/screens/InfoScreen.tsx +23 -28
- package/src/commands/easyeda.ts +6 -8
- package/src/commands/info.ts +1 -1
- package/src/commands/install.ts +10 -15
- package/src/commands/library.ts +51 -2
- package/src/commands/search.ts +1 -1
- package/src/commands/validate.ts +287 -0
- package/src/index.ts +41 -3
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 {
|
|
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
|
|
34086
|
-
import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
|
|
34087
|
-
import { homedir as
|
|
34088
|
-
import { join as
|
|
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
|
|
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
|
|
34121
|
-
if (
|
|
34122
|
-
|
|
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 =
|
|
34677
|
+
const tablePath = join3(projectDir, "sym-lib-table");
|
|
34665
34678
|
const relativePath = symbolLibPath.startsWith(projectDir) ? "${KIPRJMOD}" + symbolLibPath.slice(projectDir.length) : symbolLibPath;
|
|
34666
|
-
if (!
|
|
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 =
|
|
34693
|
+
const tablePath = join3(projectDir, "fp-lib-table");
|
|
34681
34694
|
const relativePath = footprintLibPath.startsWith(projectDir) ? "${KIPRJMOD}" + footprintLibPath.slice(projectDir.length) : footprintLibPath;
|
|
34682
|
-
if (!
|
|
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
|
|
34704
|
-
const home =
|
|
34705
|
-
const baseDir =
|
|
34716
|
+
function detectKicadVersion2() {
|
|
34717
|
+
const home = homedir2();
|
|
34718
|
+
const baseDir = join4(home, "Documents", "KiCad");
|
|
34706
34719
|
for (const version of KICAD_VERSIONS2) {
|
|
34707
|
-
if (
|
|
34720
|
+
if (existsSync3(join4(baseDir, version))) {
|
|
34708
34721
|
return version;
|
|
34709
34722
|
}
|
|
34710
34723
|
}
|
|
34711
34724
|
return "9.0";
|
|
34712
34725
|
}
|
|
34713
|
-
function
|
|
34714
|
-
const home =
|
|
34715
|
-
const plat =
|
|
34726
|
+
function getKicadConfigDir2(version) {
|
|
34727
|
+
const home = homedir2();
|
|
34728
|
+
const plat = platform2();
|
|
34716
34729
|
if (plat === "darwin") {
|
|
34717
|
-
return
|
|
34730
|
+
return join4(home, "Library", "Preferences", "kicad", version);
|
|
34718
34731
|
} else if (plat === "win32") {
|
|
34719
|
-
return
|
|
34732
|
+
return join4(process.env.APPDATA || join4(home, "AppData", "Roaming"), "kicad", version);
|
|
34720
34733
|
} else {
|
|
34721
|
-
return
|
|
34734
|
+
return join4(home, ".config", "kicad", version);
|
|
34722
34735
|
}
|
|
34723
34736
|
}
|
|
34724
34737
|
function get3rdPartyBaseDir(version) {
|
|
34725
|
-
const home =
|
|
34726
|
-
const plat =
|
|
34738
|
+
const home = homedir2();
|
|
34739
|
+
const plat = platform2();
|
|
34727
34740
|
if (plat === "linux") {
|
|
34728
|
-
return
|
|
34741
|
+
return join4(home, ".local", "share", "kicad", version, "3rdparty", LIBRARY_NAMESPACE);
|
|
34729
34742
|
}
|
|
34730
|
-
return
|
|
34743
|
+
return join4(home, "Documents", "KiCad", version, "3rdparty", LIBRARY_NAMESPACE);
|
|
34731
34744
|
}
|
|
34732
34745
|
function getSymbolLibPath(category, version) {
|
|
34733
|
-
return
|
|
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 =
|
|
34775
|
-
const tablePath =
|
|
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 (!
|
|
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 =
|
|
34819
|
-
const tablePath =
|
|
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 (!
|
|
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 =
|
|
34857
|
-
const footprintsDir =
|
|
34858
|
-
const models3dDir =
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
34886
|
-
const configDir =
|
|
34898
|
+
const version = detectKicadVersion2();
|
|
34899
|
+
const configDir = getKicadConfigDir2(version);
|
|
34887
34900
|
await mkdir2(configDir, { recursive: true });
|
|
34888
|
-
const symTablePath =
|
|
34889
|
-
if (
|
|
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 =
|
|
34906
|
-
if (
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
42540
|
-
const version =
|
|
42541
|
-
const plat =
|
|
42678
|
+
const home = homedir3();
|
|
42679
|
+
const version = detectKicadVersion();
|
|
42680
|
+
const plat = platform3();
|
|
42542
42681
|
let base;
|
|
42543
42682
|
if (plat === "linux") {
|
|
42544
|
-
base =
|
|
42683
|
+
base = join5(home, ".local", "share", "kicad", version, "3rdparty", LIBRARY_NAMESPACE2);
|
|
42545
42684
|
} else {
|
|
42546
|
-
base =
|
|
42685
|
+
base = join5(home, "Documents", "KiCad", version, "3rdparty", LIBRARY_NAMESPACE2);
|
|
42547
42686
|
}
|
|
42548
42687
|
return {
|
|
42549
42688
|
base,
|
|
42550
|
-
symbolsDir:
|
|
42551
|
-
footprintsDir:
|
|
42552
|
-
models3dDir:
|
|
42553
|
-
footprintDir:
|
|
42554
|
-
models3dFullDir:
|
|
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 =
|
|
42697
|
+
const librariesDir = join5(projectPath, "libraries");
|
|
42559
42698
|
return {
|
|
42560
42699
|
base: librariesDir,
|
|
42561
|
-
symbolsDir:
|
|
42562
|
-
footprintsDir:
|
|
42563
|
-
models3dDir:
|
|
42564
|
-
footprintDir:
|
|
42565
|
-
models3dFullDir:
|
|
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
|
|
42707
|
+
function isLcscId3(id) {
|
|
42569
42708
|
return /^C\d+$/.test(id);
|
|
42570
42709
|
}
|
|
42571
|
-
function
|
|
42572
|
-
const home =
|
|
42573
|
-
const plat =
|
|
42710
|
+
function getKicadConfigDir3(version) {
|
|
42711
|
+
const home = homedir3();
|
|
42712
|
+
const plat = platform3();
|
|
42574
42713
|
if (plat === "darwin") {
|
|
42575
|
-
return
|
|
42714
|
+
return join5(home, "Library", "Preferences", "kicad", version);
|
|
42576
42715
|
} else if (plat === "win32") {
|
|
42577
|
-
return
|
|
42716
|
+
return join5(process.env.APPDATA || join5(home, "AppData", "Roaming"), "kicad", version);
|
|
42578
42717
|
} else {
|
|
42579
|
-
return
|
|
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 =
|
|
42606
|
-
const has3dModel =
|
|
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 = !
|
|
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 (
|
|
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 =
|
|
42721
|
-
easyedaModelsDir =
|
|
42896
|
+
footprintDir = join5(paths.footprintsDir, fpLibDir);
|
|
42897
|
+
easyedaModelsDir = join5(paths.models3dDir, models3dDirName);
|
|
42722
42898
|
} else {
|
|
42723
|
-
const librariesDir =
|
|
42724
|
-
symbolsDir =
|
|
42725
|
-
footprintDir =
|
|
42726
|
-
easyedaModelsDir =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ??
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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 =
|
|
43075
|
+
const libraryPath = join5(paths.symbolsDir, libraryFilename);
|
|
42900
43076
|
const libraryName = `JLC-MCP-${category}`;
|
|
42901
|
-
if (
|
|
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 =
|
|
43092
|
+
const version = detectKicadVersion();
|
|
42917
43093
|
const paths = getGlobalLibraryPaths();
|
|
42918
|
-
const configDir =
|
|
42919
|
-
const symLibTablePath =
|
|
42920
|
-
const fpLibTablePath =
|
|
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 (
|
|
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 (
|
|
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 =
|
|
42958
|
-
if (!
|
|
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
|
-
|
|
42985
|
-
|
|
42986
|
-
|
|
42987
|
-
|
|
42988
|
-
|
|
42989
|
-
|
|
42990
|
-
|
|
42991
|
-
|
|
42992
|
-
|
|
42993
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
43083
|
-
const
|
|
43084
|
-
|
|
43085
|
-
|
|
43086
|
-
|
|
43087
|
-
|
|
43088
|
-
|
|
43089
|
-
|
|
43090
|
-
|
|
43091
|
-
|
|
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
|
-
|
|
43101
|
-
|
|
43102
|
-
|
|
43103
|
-
|
|
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
|
-
|
|
43107
|
-
|
|
43108
|
-
|
|
43109
|
-
|
|
43110
|
-
|
|
43111
|
-
|
|
43112
|
-
|
|
43113
|
-
|
|
43114
|
-
|
|
43115
|
-
|
|
43116
|
-
|
|
43117
|
-
|
|
43118
|
-
|
|
43119
|
-
|
|
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
|
-
|
|
43127
|
-
|
|
43128
|
-
|
|
43129
|
-
|
|
43130
|
-
|
|
43131
|
-
|
|
43132
|
-
|
|
43133
|
-
|
|
43134
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
-
|
|
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
|
-
|
|
43143
|
-
|
|
43144
|
-
|
|
43145
|
-
|
|
43146
|
-
|
|
43147
|
-
|
|
43148
|
-
|
|
43149
|
-
|
|
43150
|
-
|
|
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
|
-
|
|
43153
|
-
|
|
43154
|
-
|
|
43155
|
-
|
|
43156
|
-
|
|
43157
|
-
|
|
43158
|
-
|
|
43159
|
-
|
|
43160
|
-
|
|
43161
|
-
|
|
43162
|
-
|
|
43163
|
-
|
|
43164
|
-
|
|
43165
|
-
|
|
43166
|
-
|
|
43167
|
-
|
|
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
|
-
|
|
43171
|
-
const
|
|
43172
|
-
|
|
43173
|
-
|
|
43174
|
-
|
|
43175
|
-
|
|
43176
|
-
if (
|
|
43177
|
-
|
|
43178
|
-
|
|
43179
|
-
|
|
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
|
-
|
|
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
|
|
43185
|
-
|
|
43186
|
-
|
|
43187
|
-
|
|
43188
|
-
|
|
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
|
|
43368
|
-
var isMacOs =
|
|
43369
|
-
var isWindows =
|
|
43370
|
-
var isLinux =
|
|
43371
|
-
var isIos =
|
|
43372
|
-
var isAndroid =
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
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 (
|
|
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 ?? (
|
|
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 (
|
|
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({ [
|
|
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(`${
|
|
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 ? "
|
|
53024
|
+
const footerText = isInstalled ? "R Regenerate • D Delete • O 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" &&
|
|
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
|
|
55121
|
+
function isLcscId(id) {
|
|
52290
55122
|
return /^C\d+$/i.test(id);
|
|
52291
55123
|
}
|
|
52292
55124
|
async function installCommand(id, options) {
|
|
52293
|
-
if (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
|
|
52300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52327
|
-
spinner2.start(`Fetching component ${id}...`);
|
|
55158
|
+
console.log(`Fetching component ${id}...`);
|
|
52328
55159
|
try {
|
|
52329
55160
|
const details = await componentService7.getDetails(id);
|
|
52330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55193
|
+
console.log("No components found. Try a different search term.");
|
|
52366
55194
|
return;
|
|
52367
55195
|
}
|
|
52368
|
-
|
|
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
|
|
52431
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55343
|
+
console.log("No components found. Try a different search term.");
|
|
52483
55344
|
return;
|
|
52484
55345
|
}
|
|
52485
|
-
|
|
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("
|
|
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(" ");
|