@jlcpcb/core 0.1.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 +10 -0
- package/README.md +474 -0
- package/package.json +48 -0
- package/src/api/easyeda-community.ts +259 -0
- package/src/api/easyeda.ts +153 -0
- package/src/api/index.ts +7 -0
- package/src/api/jlc.ts +185 -0
- package/src/constants/design-rules.ts +119 -0
- package/src/constants/footprints.ts +68 -0
- package/src/constants/index.ts +7 -0
- package/src/constants/kicad.ts +147 -0
- package/src/converter/category-router.ts +638 -0
- package/src/converter/footprint-mapper.ts +236 -0
- package/src/converter/footprint.ts +949 -0
- package/src/converter/global-lib-table.ts +394 -0
- package/src/converter/index.ts +46 -0
- package/src/converter/lib-table.ts +181 -0
- package/src/converter/svg-arc.ts +179 -0
- package/src/converter/symbol-templates.ts +214 -0
- package/src/converter/symbol.ts +1682 -0
- package/src/converter/value-normalizer.ts +262 -0
- package/src/index.ts +25 -0
- package/src/parsers/easyeda-shapes.ts +628 -0
- package/src/parsers/http-client.ts +96 -0
- package/src/parsers/index.ts +38 -0
- package/src/parsers/utils.ts +29 -0
- package/src/services/component-service.ts +100 -0
- package/src/services/fix-service.ts +50 -0
- package/src/services/index.ts +9 -0
- package/src/services/library-service.ts +696 -0
- package/src/types/component.ts +61 -0
- package/src/types/easyeda-community.ts +78 -0
- package/src/types/easyeda.ts +356 -0
- package/src/types/index.ts +12 -0
- package/src/types/jlc.ts +84 -0
- package/src/types/kicad.ts +136 -0
- package/src/types/mcp.ts +77 -0
- package/src/types/project.ts +60 -0
- package/src/utils/conversion.ts +104 -0
- package/src/utils/file-system.ts +143 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +96 -0
- package/src/utils/validation.ts +110 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library Service
|
|
3
|
+
* High-level API for installing components to KiCad libraries
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
import { readFile, readdir } from 'fs/promises';
|
|
8
|
+
import { homedir, platform } from 'os';
|
|
9
|
+
import { join, basename } from 'path';
|
|
10
|
+
|
|
11
|
+
import type { EasyEDAComponentData, EasyEDACommunityComponent } from '../types/index.js';
|
|
12
|
+
import type { LibraryCategory } from '../converter/category-router.js';
|
|
13
|
+
import { ensureDir, writeText, writeBinary } from '../utils/index.js';
|
|
14
|
+
import { easyedaClient } from '../api/easyeda.js';
|
|
15
|
+
import { easyedaCommunityClient } from '../api/easyeda-community.js';
|
|
16
|
+
import { jlcClient } from '../api/jlc.js';
|
|
17
|
+
import { symbolConverter } from '../converter/symbol.js';
|
|
18
|
+
import { footprintConverter } from '../converter/footprint.js';
|
|
19
|
+
import {
|
|
20
|
+
ensureSymLibTable,
|
|
21
|
+
ensureFpLibTable,
|
|
22
|
+
} from '../converter/lib-table.js';
|
|
23
|
+
import {
|
|
24
|
+
getLibraryCategory,
|
|
25
|
+
getLibraryFilename,
|
|
26
|
+
getFootprintDirName,
|
|
27
|
+
get3DModelsDirName,
|
|
28
|
+
getSymbolReference,
|
|
29
|
+
getFootprintReference,
|
|
30
|
+
getAllCategories,
|
|
31
|
+
} from '../converter/category-router.js';
|
|
32
|
+
|
|
33
|
+
// Library naming
|
|
34
|
+
const FOOTPRINT_LIBRARY_NAME = getFootprintDirName();
|
|
35
|
+
const MODELS_3D_NAME = get3DModelsDirName();
|
|
36
|
+
const LIBRARY_NAMESPACE = 'jlc_mcp';
|
|
37
|
+
|
|
38
|
+
// KiCad versions to check
|
|
39
|
+
const KICAD_VERSIONS = ['9.0', '8.0'];
|
|
40
|
+
|
|
41
|
+
// EasyEDA community library naming
|
|
42
|
+
const EASYEDA_LIBRARY_NAME = 'EasyEDA';
|
|
43
|
+
const EASYEDA_SYMBOL_LIBRARY_NAME = 'EasyEDA.kicad_sym';
|
|
44
|
+
const EASYEDA_FOOTPRINT_LIBRARY_NAME = 'EasyEDA.pretty';
|
|
45
|
+
const EASYEDA_LIBRARY_DESCRIPTION = 'EasyEDA Community Component Library';
|
|
46
|
+
|
|
47
|
+
export interface InstallOptions {
|
|
48
|
+
projectPath?: string;
|
|
49
|
+
include3d?: boolean;
|
|
50
|
+
force?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface InstallResult {
|
|
54
|
+
success: boolean;
|
|
55
|
+
id: string;
|
|
56
|
+
source: 'lcsc' | 'easyeda_community';
|
|
57
|
+
storageMode: 'global' | 'project-local';
|
|
58
|
+
category?: string;
|
|
59
|
+
symbolName: string;
|
|
60
|
+
symbolRef: string;
|
|
61
|
+
footprintRef: string;
|
|
62
|
+
footprintType: 'reference' | 'generated';
|
|
63
|
+
datasheet?: string;
|
|
64
|
+
files: {
|
|
65
|
+
symbolLibrary: string;
|
|
66
|
+
footprint?: string;
|
|
67
|
+
model3d?: string;
|
|
68
|
+
};
|
|
69
|
+
symbolAction: 'created' | 'appended' | 'exists' | 'replaced';
|
|
70
|
+
validationData: ValidationData;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ValidationData {
|
|
74
|
+
component: {
|
|
75
|
+
name: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
package?: string;
|
|
78
|
+
manufacturer?: string;
|
|
79
|
+
datasheet_url?: string;
|
|
80
|
+
};
|
|
81
|
+
symbol: {
|
|
82
|
+
pin_count: number;
|
|
83
|
+
pins: Array<{ number: string; name: string; electrical_type?: string }>;
|
|
84
|
+
};
|
|
85
|
+
footprint: {
|
|
86
|
+
type: string;
|
|
87
|
+
pad_count: number;
|
|
88
|
+
pads?: Array<{ number: string; shape: string }> | null;
|
|
89
|
+
is_kicad_standard: boolean;
|
|
90
|
+
kicad_ref: string;
|
|
91
|
+
};
|
|
92
|
+
checks: {
|
|
93
|
+
pin_pad_count_match: boolean;
|
|
94
|
+
has_power_pins: boolean;
|
|
95
|
+
has_ground_pins: boolean;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface InstalledComponent {
|
|
100
|
+
lcscId: string;
|
|
101
|
+
name: string;
|
|
102
|
+
category: LibraryCategory;
|
|
103
|
+
symbolRef: string;
|
|
104
|
+
footprintRef: string;
|
|
105
|
+
library: string;
|
|
106
|
+
has3dModel: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface LibraryStatus {
|
|
110
|
+
installed: boolean;
|
|
111
|
+
linked: boolean;
|
|
112
|
+
version: string;
|
|
113
|
+
componentCount: number;
|
|
114
|
+
paths: {
|
|
115
|
+
symbolsDir: string;
|
|
116
|
+
footprintsDir: string;
|
|
117
|
+
models3dDir: string;
|
|
118
|
+
symLibTable: string;
|
|
119
|
+
fpLibTable: string;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ListOptions {
|
|
124
|
+
category?: LibraryCategory;
|
|
125
|
+
projectPath?: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface UpdateOptions {
|
|
129
|
+
category?: LibraryCategory;
|
|
130
|
+
projectPath?: string;
|
|
131
|
+
dryRun?: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface UpdateResult {
|
|
135
|
+
updated: number;
|
|
136
|
+
failed: number;
|
|
137
|
+
skipped: number;
|
|
138
|
+
components: Array<{ id: string; status: 'updated' | 'failed' | 'skipped'; error?: string }>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface LibraryService {
|
|
142
|
+
install(id: string, options?: InstallOptions): Promise<InstallResult>;
|
|
143
|
+
listInstalled(options?: ListOptions): Promise<InstalledComponent[]>;
|
|
144
|
+
update(options?: UpdateOptions): Promise<UpdateResult>;
|
|
145
|
+
ensureGlobalTables(): Promise<void>;
|
|
146
|
+
getStatus(): Promise<LibraryStatus>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface LibraryPaths {
|
|
150
|
+
base: string;
|
|
151
|
+
symbolsDir: string;
|
|
152
|
+
footprintsDir: string;
|
|
153
|
+
models3dDir: string;
|
|
154
|
+
footprintDir: string;
|
|
155
|
+
models3dFullDir: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function detectKicadVersion(): string {
|
|
159
|
+
const home = homedir();
|
|
160
|
+
const baseDir = join(home, 'Documents', 'KiCad');
|
|
161
|
+
|
|
162
|
+
for (const version of KICAD_VERSIONS) {
|
|
163
|
+
if (existsSync(join(baseDir, version))) {
|
|
164
|
+
return version;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return '9.0';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getGlobalLibraryPaths(): LibraryPaths {
|
|
171
|
+
const home = homedir();
|
|
172
|
+
const version = detectKicadVersion();
|
|
173
|
+
const plat = platform();
|
|
174
|
+
|
|
175
|
+
let base: string;
|
|
176
|
+
if (plat === 'linux') {
|
|
177
|
+
base = join(home, '.local', 'share', 'kicad', version, '3rdparty', LIBRARY_NAMESPACE);
|
|
178
|
+
} else {
|
|
179
|
+
base = join(home, 'Documents', 'KiCad', version, '3rdparty', LIBRARY_NAMESPACE);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
base,
|
|
184
|
+
symbolsDir: join(base, 'symbols'),
|
|
185
|
+
footprintsDir: join(base, 'footprints'),
|
|
186
|
+
models3dDir: join(base, '3dmodels'),
|
|
187
|
+
footprintDir: join(base, 'footprints', FOOTPRINT_LIBRARY_NAME),
|
|
188
|
+
models3dFullDir: join(base, '3dmodels', MODELS_3D_NAME),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getProjectLibraryPaths(projectPath: string): LibraryPaths {
|
|
193
|
+
const librariesDir = join(projectPath, 'libraries');
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
base: librariesDir,
|
|
197
|
+
symbolsDir: join(librariesDir, 'symbols'),
|
|
198
|
+
footprintsDir: join(librariesDir, 'footprints'),
|
|
199
|
+
models3dDir: join(librariesDir, '3dmodels'),
|
|
200
|
+
footprintDir: join(librariesDir, 'footprints', FOOTPRINT_LIBRARY_NAME),
|
|
201
|
+
models3dFullDir: join(librariesDir, '3dmodels', MODELS_3D_NAME),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isLcscId(id: string): boolean {
|
|
206
|
+
return /^C\d+$/.test(id);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getKicadConfigDir(version: string): string {
|
|
210
|
+
const home = homedir();
|
|
211
|
+
const plat = platform();
|
|
212
|
+
|
|
213
|
+
if (plat === 'darwin') {
|
|
214
|
+
return join(home, 'Library', 'Preferences', 'kicad', version);
|
|
215
|
+
} else if (plat === 'win32') {
|
|
216
|
+
return join(process.env.APPDATA || join(home, 'AppData', 'Roaming'), 'kicad', version);
|
|
217
|
+
} else {
|
|
218
|
+
return join(home, '.config', 'kicad', version);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Parse .kicad_sym file to extract installed components
|
|
223
|
+
async function parseSymbolLibrary(
|
|
224
|
+
filePath: string,
|
|
225
|
+
libraryName: string,
|
|
226
|
+
category: LibraryCategory,
|
|
227
|
+
models3dDir: string
|
|
228
|
+
): Promise<InstalledComponent[]> {
|
|
229
|
+
const components: InstalledComponent[] = [];
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const content = await readFile(filePath, 'utf-8');
|
|
233
|
+
|
|
234
|
+
// Match symbol definitions: (symbol "JLC-MCP-Resistors:R_100k" ...)
|
|
235
|
+
const symbolPattern = /\(symbol\s+"([^"]+)"\s+/g;
|
|
236
|
+
const lcscPattern = /\(property\s+"LCSC"\s+"(C\d+)"/g;
|
|
237
|
+
const footprintPattern = /\(property\s+"Footprint"\s+"([^"]+)"/g;
|
|
238
|
+
|
|
239
|
+
// Split by symbol definitions to process each
|
|
240
|
+
const symbols = content.split(/(?=\(symbol\s+"[^"]+"\s+\()/);
|
|
241
|
+
|
|
242
|
+
for (const symbolBlock of symbols) {
|
|
243
|
+
// Skip header block
|
|
244
|
+
if (!symbolBlock.includes('(symbol "')) continue;
|
|
245
|
+
|
|
246
|
+
// Extract symbol name
|
|
247
|
+
const symbolMatch = symbolBlock.match(/\(symbol\s+"([^"]+)"/);
|
|
248
|
+
if (!symbolMatch) continue;
|
|
249
|
+
|
|
250
|
+
const fullSymbolRef = symbolMatch[1];
|
|
251
|
+
// Skip if this is a subsymbol (contains _1_1, _0_1, etc.)
|
|
252
|
+
if (fullSymbolRef.includes('_') && /_(0|1)_(0|1)$/.test(fullSymbolRef)) continue;
|
|
253
|
+
|
|
254
|
+
// Extract LCSC ID
|
|
255
|
+
const lcscMatch = symbolBlock.match(/\(property\s+"LCSC"\s+"(C\d+)"/);
|
|
256
|
+
const lcscId = lcscMatch ? lcscMatch[1] : '';
|
|
257
|
+
|
|
258
|
+
// Extract name from symbol ref (after the colon)
|
|
259
|
+
const nameParts = fullSymbolRef.split(':');
|
|
260
|
+
const name = nameParts.length > 1 ? nameParts[1] : fullSymbolRef;
|
|
261
|
+
|
|
262
|
+
// Extract footprint reference
|
|
263
|
+
const fpMatch = symbolBlock.match(/\(property\s+"Footprint"\s+"([^"]+)"/);
|
|
264
|
+
const footprintRef = fpMatch ? fpMatch[1] : '';
|
|
265
|
+
|
|
266
|
+
// Check for 3D model
|
|
267
|
+
const model3dPath = join(models3dDir, `${name}.step`);
|
|
268
|
+
const has3dModel = existsSync(model3dPath);
|
|
269
|
+
|
|
270
|
+
if (lcscId) {
|
|
271
|
+
components.push({
|
|
272
|
+
lcscId,
|
|
273
|
+
name,
|
|
274
|
+
category,
|
|
275
|
+
symbolRef: fullSymbolRef,
|
|
276
|
+
footprintRef,
|
|
277
|
+
library: libraryName,
|
|
278
|
+
has3dModel,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
// File read error - return empty
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return components;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function adaptCommunityComponent(component: EasyEDACommunityComponent): EasyEDAComponentData {
|
|
290
|
+
const symbolHead = component.symbol.head as Record<string, unknown> | undefined;
|
|
291
|
+
const cPara = (symbolHead?.c_para as Record<string, string>) || {};
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
info: {
|
|
295
|
+
name: component.title,
|
|
296
|
+
prefix: cPara.pre || 'U',
|
|
297
|
+
package: component.footprint.name,
|
|
298
|
+
manufacturer: cPara.Manufacturer || cPara.BOM_Manufacturer,
|
|
299
|
+
datasheet: cPara.link,
|
|
300
|
+
lcscId: undefined,
|
|
301
|
+
jlcId: undefined,
|
|
302
|
+
},
|
|
303
|
+
symbol: {
|
|
304
|
+
pins: component.symbol.pins,
|
|
305
|
+
rectangles: component.symbol.rectangles,
|
|
306
|
+
circles: component.symbol.circles,
|
|
307
|
+
ellipses: component.symbol.ellipses,
|
|
308
|
+
arcs: component.symbol.arcs,
|
|
309
|
+
polylines: component.symbol.polylines,
|
|
310
|
+
polygons: component.symbol.polygons,
|
|
311
|
+
paths: component.symbol.paths,
|
|
312
|
+
origin: component.symbol.origin,
|
|
313
|
+
},
|
|
314
|
+
footprint: {
|
|
315
|
+
name: component.footprint.name,
|
|
316
|
+
type: component.footprint.type,
|
|
317
|
+
pads: component.footprint.pads,
|
|
318
|
+
tracks: component.footprint.tracks,
|
|
319
|
+
holes: component.footprint.holes,
|
|
320
|
+
circles: component.footprint.circles,
|
|
321
|
+
arcs: component.footprint.arcs,
|
|
322
|
+
rects: component.footprint.rects,
|
|
323
|
+
texts: component.footprint.texts,
|
|
324
|
+
vias: component.footprint.vias,
|
|
325
|
+
solidRegions: component.footprint.solidRegions,
|
|
326
|
+
origin: component.footprint.origin,
|
|
327
|
+
},
|
|
328
|
+
model3d: component.model3d,
|
|
329
|
+
rawData: component.rawData,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function createLibraryService(): LibraryService {
|
|
334
|
+
return {
|
|
335
|
+
async install(id: string, options: InstallOptions = {}): Promise<InstallResult> {
|
|
336
|
+
const isCommunityComponent = !isLcscId(id);
|
|
337
|
+
|
|
338
|
+
// Community components require projectPath
|
|
339
|
+
if (isCommunityComponent && !options.projectPath) {
|
|
340
|
+
throw new Error('EasyEDA community components require projectPath for local storage');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Determine storage location
|
|
344
|
+
const isGlobal = !isCommunityComponent && !options.projectPath;
|
|
345
|
+
const paths = isGlobal
|
|
346
|
+
? getGlobalLibraryPaths()
|
|
347
|
+
: getProjectLibraryPaths(options.projectPath!);
|
|
348
|
+
|
|
349
|
+
// Fetch component data
|
|
350
|
+
let component: EasyEDAComponentData | null = null;
|
|
351
|
+
|
|
352
|
+
if (isLcscId(id)) {
|
|
353
|
+
component = await easyedaClient.getComponentData(id);
|
|
354
|
+
} else {
|
|
355
|
+
const communityComponent = await easyedaCommunityClient.getComponent(id);
|
|
356
|
+
if (communityComponent) {
|
|
357
|
+
component = adaptCommunityComponent(communityComponent);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!component) {
|
|
362
|
+
throw new Error(`Component ${id} not found`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Enrich with JLC API data for LCSC components
|
|
366
|
+
if (!isCommunityComponent) {
|
|
367
|
+
try {
|
|
368
|
+
const jlcDetails = await jlcClient.getComponentDetails(id);
|
|
369
|
+
if (jlcDetails) {
|
|
370
|
+
if (jlcDetails.datasheetPdf) {
|
|
371
|
+
component.info.datasheetPdf = jlcDetails.datasheetPdf;
|
|
372
|
+
}
|
|
373
|
+
if (jlcDetails.description && jlcDetails.description !== jlcDetails.name) {
|
|
374
|
+
component.info.description = jlcDetails.description;
|
|
375
|
+
}
|
|
376
|
+
if (jlcDetails.attributes) {
|
|
377
|
+
component.info.attributes = {
|
|
378
|
+
...component.info.attributes,
|
|
379
|
+
...jlcDetails.attributes,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch {
|
|
384
|
+
// JLC enrichment is optional
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Variables for library paths and refs
|
|
389
|
+
let symbolFile: string;
|
|
390
|
+
let symbolName: string;
|
|
391
|
+
let symbolRef: string;
|
|
392
|
+
let footprintPath: string | undefined;
|
|
393
|
+
let footprintRef: string;
|
|
394
|
+
let footprintDir: string;
|
|
395
|
+
let models3dDir: string;
|
|
396
|
+
let category: string | undefined;
|
|
397
|
+
let modelPath: string | undefined;
|
|
398
|
+
|
|
399
|
+
if (isCommunityComponent) {
|
|
400
|
+
// EasyEDA community component → project-local EasyEDA library
|
|
401
|
+
const librariesDir = join(options.projectPath!, 'libraries');
|
|
402
|
+
const symbolsDir = join(librariesDir, 'symbols');
|
|
403
|
+
footprintDir = join(librariesDir, 'footprints', EASYEDA_FOOTPRINT_LIBRARY_NAME);
|
|
404
|
+
models3dDir = join(librariesDir, '3dmodels', 'EasyEDA.3dshapes');
|
|
405
|
+
|
|
406
|
+
await ensureDir(symbolsDir);
|
|
407
|
+
await ensureDir(footprintDir);
|
|
408
|
+
|
|
409
|
+
symbolFile = join(symbolsDir, EASYEDA_SYMBOL_LIBRARY_NAME);
|
|
410
|
+
symbolName = component.info.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
411
|
+
|
|
412
|
+
// Download 3D model first if available (needed for footprint generation)
|
|
413
|
+
// Community components include 3D by default
|
|
414
|
+
const include3d = options.include3d ?? true;
|
|
415
|
+
let modelRelativePath: string | undefined;
|
|
416
|
+
|
|
417
|
+
if (include3d && component.model3d) {
|
|
418
|
+
await ensureDir(models3dDir);
|
|
419
|
+
const model = await easyedaCommunityClient.get3DModel(component.model3d.uuid, 'step');
|
|
420
|
+
if (model) {
|
|
421
|
+
const modelFilename = `${symbolName}.step`;
|
|
422
|
+
modelPath = join(models3dDir, modelFilename);
|
|
423
|
+
await writeBinary(modelPath, model);
|
|
424
|
+
// Use relative path for project-local libraries
|
|
425
|
+
modelRelativePath = `\${KIPRJMOD}/libraries/3dmodels/EasyEDA.3dshapes/${modelFilename}`;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Generate custom footprint with 3D model
|
|
430
|
+
const footprint = footprintConverter.convert(component, {
|
|
431
|
+
libraryName: EASYEDA_LIBRARY_NAME,
|
|
432
|
+
include3DModel: !!modelRelativePath,
|
|
433
|
+
modelPath: modelRelativePath,
|
|
434
|
+
});
|
|
435
|
+
footprintPath = join(footprintDir, `${symbolName}.kicad_mod`);
|
|
436
|
+
footprintRef = `${EASYEDA_LIBRARY_NAME}:${symbolName}`;
|
|
437
|
+
await writeText(footprintPath, footprint);
|
|
438
|
+
|
|
439
|
+
component.info.package = footprintRef;
|
|
440
|
+
|
|
441
|
+
// Update lib tables
|
|
442
|
+
await ensureSymLibTable(options.projectPath!, symbolFile, EASYEDA_LIBRARY_NAME, EASYEDA_LIBRARY_DESCRIPTION);
|
|
443
|
+
await ensureFpLibTable(options.projectPath!, footprintDir, EASYEDA_LIBRARY_NAME, EASYEDA_LIBRARY_DESCRIPTION);
|
|
444
|
+
|
|
445
|
+
symbolRef = `${EASYEDA_LIBRARY_NAME}:${symbolName}`;
|
|
446
|
+
} else {
|
|
447
|
+
// LCSC component → JLC-MCP category-based library
|
|
448
|
+
category = getLibraryCategory(
|
|
449
|
+
component.info.prefix,
|
|
450
|
+
component.info.category,
|
|
451
|
+
component.info.description
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const symbolLibraryFilename = getLibraryFilename(category as LibraryCategory);
|
|
455
|
+
symbolFile = join(paths.symbolsDir, symbolLibraryFilename);
|
|
456
|
+
footprintDir = paths.footprintDir;
|
|
457
|
+
models3dDir = paths.models3dFullDir;
|
|
458
|
+
|
|
459
|
+
await ensureDir(paths.symbolsDir);
|
|
460
|
+
await ensureDir(paths.footprintDir);
|
|
461
|
+
|
|
462
|
+
// Pre-compute symbol name for 3D model path
|
|
463
|
+
symbolName = symbolConverter.getSymbolName(component);
|
|
464
|
+
|
|
465
|
+
// Download 3D model first if requested (needed for footprint generation)
|
|
466
|
+
const include3d = options.include3d ?? false;
|
|
467
|
+
let modelRelativePath: string | undefined;
|
|
468
|
+
|
|
469
|
+
if (include3d && component.model3d) {
|
|
470
|
+
await ensureDir(models3dDir);
|
|
471
|
+
const model = await easyedaClient.get3DModel(component.model3d.uuid, 'step');
|
|
472
|
+
if (model) {
|
|
473
|
+
const modelFilename = `${symbolName}.step`;
|
|
474
|
+
modelPath = join(models3dDir, modelFilename);
|
|
475
|
+
await writeBinary(modelPath, model);
|
|
476
|
+
// Use KiCad variable for portable path
|
|
477
|
+
modelRelativePath = `\${KICAD9_3RD_PARTY}/${LIBRARY_NAMESPACE}/3dmodels/${MODELS_3D_NAME}/${modelFilename}`;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Determine footprint (may use KiCad standard)
|
|
482
|
+
const footprintResult = footprintConverter.getFootprint(component, {
|
|
483
|
+
include3DModel: !!modelRelativePath,
|
|
484
|
+
modelPath: modelRelativePath,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
if (footprintResult.type === 'reference') {
|
|
488
|
+
footprintRef = footprintResult.reference!;
|
|
489
|
+
} else {
|
|
490
|
+
footprintPath = join(footprintDir, `${footprintResult.name}.kicad_mod`);
|
|
491
|
+
footprintRef = getFootprintReference(footprintResult.name);
|
|
492
|
+
await writeText(footprintPath, footprintResult.content!);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
component.info.package = footprintRef;
|
|
496
|
+
symbolRef = getSymbolReference(category as LibraryCategory, symbolName);
|
|
497
|
+
|
|
498
|
+
// Update lib tables (project-local only)
|
|
499
|
+
if (!isGlobal && options.projectPath) {
|
|
500
|
+
await ensureSymLibTable(options.projectPath, symbolFile);
|
|
501
|
+
await ensureFpLibTable(options.projectPath, footprintDir);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Handle symbol library (append, replace, or create)
|
|
506
|
+
let symbolContent: string;
|
|
507
|
+
let symbolAction: 'created' | 'appended' | 'exists' | 'replaced';
|
|
508
|
+
|
|
509
|
+
if (existsSync(symbolFile)) {
|
|
510
|
+
const existingContent = await readFile(symbolFile, 'utf-8');
|
|
511
|
+
|
|
512
|
+
if (symbolConverter.symbolExistsInLibrary(existingContent, component.info.name)) {
|
|
513
|
+
if (options.force) {
|
|
514
|
+
// Force reinstall - replace existing symbol
|
|
515
|
+
symbolContent = symbolConverter.replaceInLibrary(existingContent, component, {
|
|
516
|
+
libraryName: isCommunityComponent ? EASYEDA_LIBRARY_NAME : undefined,
|
|
517
|
+
symbolName: isCommunityComponent ? symbolName : undefined,
|
|
518
|
+
});
|
|
519
|
+
symbolAction = 'replaced';
|
|
520
|
+
} else {
|
|
521
|
+
symbolAction = 'exists';
|
|
522
|
+
symbolContent = existingContent;
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
symbolContent = symbolConverter.appendToLibrary(existingContent, component, {
|
|
526
|
+
libraryName: isCommunityComponent ? EASYEDA_LIBRARY_NAME : undefined,
|
|
527
|
+
symbolName: isCommunityComponent ? symbolName : undefined,
|
|
528
|
+
});
|
|
529
|
+
symbolAction = 'appended';
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
symbolContent = symbolConverter.convert(component, {
|
|
533
|
+
libraryName: isCommunityComponent ? EASYEDA_LIBRARY_NAME : undefined,
|
|
534
|
+
symbolName: isCommunityComponent ? symbolName : undefined,
|
|
535
|
+
});
|
|
536
|
+
symbolAction = 'created';
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (symbolAction !== 'exists') {
|
|
540
|
+
await writeText(symbolFile, symbolContent);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Build validation data
|
|
544
|
+
const isKicadStandardFootprint = !isCommunityComponent && !footprintPath;
|
|
545
|
+
const footprintType = isKicadStandardFootprint ? 'reference' : 'generated';
|
|
546
|
+
|
|
547
|
+
const validationData: ValidationData = {
|
|
548
|
+
component: {
|
|
549
|
+
name: component.info.name,
|
|
550
|
+
description: component.info.description,
|
|
551
|
+
package: component.info.package,
|
|
552
|
+
manufacturer: component.info.manufacturer,
|
|
553
|
+
datasheet_url: component.info.datasheetPdf || component.info.datasheet,
|
|
554
|
+
},
|
|
555
|
+
symbol: {
|
|
556
|
+
pin_count: component.symbol.pins.length,
|
|
557
|
+
pins: component.symbol.pins.map(p => ({
|
|
558
|
+
number: p.number,
|
|
559
|
+
name: p.name,
|
|
560
|
+
electrical_type: p.electricalType,
|
|
561
|
+
})),
|
|
562
|
+
},
|
|
563
|
+
footprint: {
|
|
564
|
+
type: component.footprint.type,
|
|
565
|
+
pad_count: component.footprint.pads.length,
|
|
566
|
+
pads: footprintType === 'generated'
|
|
567
|
+
? component.footprint.pads.map(p => ({
|
|
568
|
+
number: p.number,
|
|
569
|
+
shape: p.shape,
|
|
570
|
+
}))
|
|
571
|
+
: null,
|
|
572
|
+
is_kicad_standard: isKicadStandardFootprint,
|
|
573
|
+
kicad_ref: footprintRef,
|
|
574
|
+
},
|
|
575
|
+
checks: {
|
|
576
|
+
pin_pad_count_match: component.symbol.pins.length === component.footprint.pads.length,
|
|
577
|
+
has_power_pins: component.symbol.pins.some(p =>
|
|
578
|
+
p.electricalType === 'power_in' || p.electricalType === 'power_out'
|
|
579
|
+
),
|
|
580
|
+
has_ground_pins: component.symbol.pins.some(p =>
|
|
581
|
+
p.name.toLowerCase().includes('gnd') || p.name.toLowerCase().includes('vss')
|
|
582
|
+
),
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
success: true,
|
|
588
|
+
id,
|
|
589
|
+
source: isCommunityComponent ? 'easyeda_community' : 'lcsc',
|
|
590
|
+
storageMode: isGlobal ? 'global' : 'project-local',
|
|
591
|
+
category,
|
|
592
|
+
symbolName,
|
|
593
|
+
symbolRef,
|
|
594
|
+
footprintRef,
|
|
595
|
+
footprintType,
|
|
596
|
+
datasheet: component.info.datasheet || (!isCommunityComponent ? `https://www.lcsc.com/datasheet/lcsc_datasheet_${id}.pdf` : undefined),
|
|
597
|
+
files: {
|
|
598
|
+
symbolLibrary: symbolFile,
|
|
599
|
+
footprint: footprintPath,
|
|
600
|
+
model3d: modelPath,
|
|
601
|
+
},
|
|
602
|
+
symbolAction,
|
|
603
|
+
validationData,
|
|
604
|
+
};
|
|
605
|
+
},
|
|
606
|
+
|
|
607
|
+
async listInstalled(options: ListOptions = {}): Promise<InstalledComponent[]> {
|
|
608
|
+
const paths = options.projectPath
|
|
609
|
+
? getProjectLibraryPaths(options.projectPath)
|
|
610
|
+
: getGlobalLibraryPaths();
|
|
611
|
+
|
|
612
|
+
const allComponents: InstalledComponent[] = [];
|
|
613
|
+
const categories = getAllCategories();
|
|
614
|
+
|
|
615
|
+
for (const category of categories) {
|
|
616
|
+
// Filter by category if specified
|
|
617
|
+
if (options.category && options.category !== category) continue;
|
|
618
|
+
|
|
619
|
+
const libraryFilename = getLibraryFilename(category);
|
|
620
|
+
const libraryPath = join(paths.symbolsDir, libraryFilename);
|
|
621
|
+
const libraryName = `JLC-MCP-${category}`;
|
|
622
|
+
|
|
623
|
+
if (existsSync(libraryPath)) {
|
|
624
|
+
const components = await parseSymbolLibrary(
|
|
625
|
+
libraryPath,
|
|
626
|
+
libraryName,
|
|
627
|
+
category,
|
|
628
|
+
paths.models3dFullDir
|
|
629
|
+
);
|
|
630
|
+
allComponents.push(...components);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return allComponents;
|
|
635
|
+
},
|
|
636
|
+
|
|
637
|
+
async update(_options: UpdateOptions = {}): Promise<UpdateResult> {
|
|
638
|
+
// TODO: Implement by re-fetching all components in a library
|
|
639
|
+
return { updated: 0, failed: 0, skipped: 0, components: [] };
|
|
640
|
+
},
|
|
641
|
+
|
|
642
|
+
async ensureGlobalTables(): Promise<void> {
|
|
643
|
+
const { ensureGlobalLibraryTables } = await import('../converter/global-lib-table.js');
|
|
644
|
+
await ensureGlobalLibraryTables();
|
|
645
|
+
},
|
|
646
|
+
|
|
647
|
+
async getStatus(): Promise<LibraryStatus> {
|
|
648
|
+
const version = detectKicadVersion();
|
|
649
|
+
const paths = getGlobalLibraryPaths();
|
|
650
|
+
const configDir = getKicadConfigDir(version);
|
|
651
|
+
|
|
652
|
+
const symLibTablePath = join(configDir, 'sym-lib-table');
|
|
653
|
+
const fpLibTablePath = join(configDir, 'fp-lib-table');
|
|
654
|
+
|
|
655
|
+
// Check if library directories exist with actual content
|
|
656
|
+
let installed = false;
|
|
657
|
+
if (existsSync(paths.symbolsDir)) {
|
|
658
|
+
try {
|
|
659
|
+
const files = await readdir(paths.symbolsDir);
|
|
660
|
+
installed = files.some(f => f.endsWith('.kicad_sym'));
|
|
661
|
+
} catch {
|
|
662
|
+
installed = false;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Check if libraries are linked in KiCad tables
|
|
667
|
+
let linked = false;
|
|
668
|
+
if (existsSync(symLibTablePath)) {
|
|
669
|
+
try {
|
|
670
|
+
const content = await readFile(symLibTablePath, 'utf-8');
|
|
671
|
+
linked = content.includes('JLC-MCP');
|
|
672
|
+
} catch {
|
|
673
|
+
linked = false;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Count installed components
|
|
678
|
+
const components = await this.listInstalled({});
|
|
679
|
+
const componentCount = components.length;
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
installed,
|
|
683
|
+
linked,
|
|
684
|
+
version,
|
|
685
|
+
componentCount,
|
|
686
|
+
paths: {
|
|
687
|
+
symbolsDir: paths.symbolsDir,
|
|
688
|
+
footprintsDir: paths.footprintsDir,
|
|
689
|
+
models3dDir: paths.models3dFullDir,
|
|
690
|
+
symLibTable: symLibTablePath,
|
|
691
|
+
fpLibTable: fpLibTablePath,
|
|
692
|
+
},
|
|
693
|
+
};
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
}
|