@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,1682 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EasyEDA Symbol to KiCad Symbol Converter
|
|
3
|
+
* Converts EasyEDA symbol format to KiCad .kicad_sym format (KiCad 9 compatible)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
EasyEDAComponentData,
|
|
8
|
+
EasyEDAPin,
|
|
9
|
+
EasyEDASymbolData,
|
|
10
|
+
EasyEDASymbolRect,
|
|
11
|
+
EasyEDASymbolCircle,
|
|
12
|
+
EasyEDASymbolEllipse,
|
|
13
|
+
EasyEDASymbolArc,
|
|
14
|
+
EasyEDASymbolPolyline,
|
|
15
|
+
EasyEDASymbolPolygon,
|
|
16
|
+
EasyEDASymbolPath,
|
|
17
|
+
} from '../types/index.js';
|
|
18
|
+
import { KICAD_SYMBOL_VERSION, KICAD_DEFAULTS } from '../constants/index.js';
|
|
19
|
+
import { roundTo } from '../utils/index.js';
|
|
20
|
+
import { extractDisplayValue } from './value-normalizer.js';
|
|
21
|
+
import { getSymbolTemplate, type SymbolTemplate } from './symbol-templates.js';
|
|
22
|
+
import { getLibraryCategory, type LibraryCategory } from './category-router.js';
|
|
23
|
+
import { parseSvgArcPath, svgArcToCenter, radToDeg, normalizeAngle } from './svg-arc.js';
|
|
24
|
+
|
|
25
|
+
// Map library categories to template prefixes for passive components
|
|
26
|
+
const CATEGORY_TO_PREFIX: Partial<Record<LibraryCategory, string>> = {
|
|
27
|
+
Resistors: 'R',
|
|
28
|
+
Capacitors: 'C',
|
|
29
|
+
Inductors: 'L',
|
|
30
|
+
Diodes: 'D',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// EasyEDA uses 10mil units (0.254mm per unit)
|
|
34
|
+
const EE_TO_MM = 0.254;
|
|
35
|
+
|
|
36
|
+
// IC symbol layout constants (matching CDFER/KiCad conventions)
|
|
37
|
+
const IC_PIN_LENGTH = 2.54; // mm (100 mil) - standard pin length
|
|
38
|
+
const IC_PIN_SPACING = 2.54; // mm (100 mil) - vertical spacing between pins
|
|
39
|
+
const IC_BODY_HALF_WIDTH = 12.7; // mm (500 mil) - half body width (full = 25.4mm)
|
|
40
|
+
const IC_PIN_FONT_SIZE = 1.0; // mm - smaller font for pin names/numbers
|
|
41
|
+
const IC_MIN_BODY_SIZE = 5.08; // mm (200 mil) - minimum body dimension
|
|
42
|
+
const IC_BODY_PADDING = 2.54; // mm (100 mil) - padding from pins to body edge
|
|
43
|
+
|
|
44
|
+
export interface SymbolConversionOptions {
|
|
45
|
+
libraryName?: string;
|
|
46
|
+
symbolName?: string;
|
|
47
|
+
includeDatasheet?: boolean;
|
|
48
|
+
includeManufacturer?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface BoundingBox {
|
|
52
|
+
minX: number;
|
|
53
|
+
maxX: number;
|
|
54
|
+
minY: number;
|
|
55
|
+
maxY: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* DIP-style IC layout with calculated pin positions
|
|
60
|
+
*/
|
|
61
|
+
interface ICLayout {
|
|
62
|
+
bodyWidth: number;
|
|
63
|
+
bodyHeight: number;
|
|
64
|
+
leftPins: Array<{ pin: EasyEDAPin; x: number; y: number }>;
|
|
65
|
+
rightPins: Array<{ pin: EasyEDAPin; x: number; y: number }>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class SymbolConverter {
|
|
69
|
+
/**
|
|
70
|
+
* Convert EasyEDA component data to KiCad symbol format string (standalone library)
|
|
71
|
+
*/
|
|
72
|
+
convert(
|
|
73
|
+
component: EasyEDAComponentData,
|
|
74
|
+
options: SymbolConversionOptions = {}
|
|
75
|
+
): string {
|
|
76
|
+
const symbolEntry = this.convertToSymbolEntry(component, options);
|
|
77
|
+
return this.generateHeader() + symbolEntry + ')\n';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Convert component to a symbol entry (without library wrapper)
|
|
82
|
+
* Used for appending to existing libraries
|
|
83
|
+
*/
|
|
84
|
+
convertToSymbolEntry(
|
|
85
|
+
component: EasyEDAComponentData,
|
|
86
|
+
options: SymbolConversionOptions = {}
|
|
87
|
+
): string {
|
|
88
|
+
const { info, symbol } = component;
|
|
89
|
+
|
|
90
|
+
// Try prefix-based template first
|
|
91
|
+
let template = getSymbolTemplate(info.prefix);
|
|
92
|
+
|
|
93
|
+
// If no template from prefix, check if category maps to a passive template
|
|
94
|
+
if (!template) {
|
|
95
|
+
const category = getLibraryCategory(info.prefix, info.category, info.description);
|
|
96
|
+
const categoryPrefix = CATEGORY_TO_PREFIX[category];
|
|
97
|
+
if (categoryPrefix) {
|
|
98
|
+
template = getSymbolTemplate(categoryPrefix);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (template && symbol.pins.length === 2) {
|
|
103
|
+
// Use fixed layout for 2-pin passives only (R, C, L, D with 2 pins)
|
|
104
|
+
// Multi-pin components (bridge rectifiers, multi-terminal LEDs) use EasyEDA layout
|
|
105
|
+
return this.generateFromTemplate(component, template, options);
|
|
106
|
+
} else {
|
|
107
|
+
// Fall back to improved EasyEDA-derived layout for ICs
|
|
108
|
+
return this.generateFromEasyEDA(component, options);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Generate symbol from fixed template (for passives)
|
|
114
|
+
*/
|
|
115
|
+
private generateFromTemplate(
|
|
116
|
+
component: EasyEDAComponentData,
|
|
117
|
+
template: SymbolTemplate,
|
|
118
|
+
options: SymbolConversionOptions = {}
|
|
119
|
+
): string {
|
|
120
|
+
const { info, symbol } = component;
|
|
121
|
+
const name = options.symbolName ? this.sanitizeName(options.symbolName) : this.sanitizeName(info.name);
|
|
122
|
+
const pins = symbol.pins;
|
|
123
|
+
|
|
124
|
+
let output = this.generateSymbolStart(name);
|
|
125
|
+
output += this.generateTemplateProperties(info, name, template);
|
|
126
|
+
output += this.generateTemplateGraphics(name, template);
|
|
127
|
+
output += this.generateTemplatePins(name, pins, template);
|
|
128
|
+
output += this.generateSymbolEnd();
|
|
129
|
+
|
|
130
|
+
return output;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Generate symbol from EasyEDA data (for ICs and complex components)
|
|
135
|
+
* Uses parsed shapes if available, otherwise falls back to DIP-style layout
|
|
136
|
+
*/
|
|
137
|
+
private generateFromEasyEDA(
|
|
138
|
+
component: EasyEDAComponentData,
|
|
139
|
+
options: SymbolConversionOptions = {}
|
|
140
|
+
): string {
|
|
141
|
+
const { info, symbol } = component;
|
|
142
|
+
const name = options.symbolName ? this.sanitizeName(options.symbolName) : this.sanitizeName(info.name);
|
|
143
|
+
|
|
144
|
+
// Check if we have non-trivial shapes to render
|
|
145
|
+
if (this.hasRenderableShapes(symbol)) {
|
|
146
|
+
return this.generateFromShapes(component, name);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Fall back to DIP-style layout
|
|
150
|
+
const layout = this.calculateICLayout(symbol.pins);
|
|
151
|
+
|
|
152
|
+
let output = this.generateSymbolStart(name, false); // Show pin numbers for ICs
|
|
153
|
+
output += this.generateProperties(info, name);
|
|
154
|
+
output += this.generateICGraphics(name, layout);
|
|
155
|
+
output += this.generateICPins(name, layout);
|
|
156
|
+
output += this.generateSymbolEnd();
|
|
157
|
+
|
|
158
|
+
return output;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if symbol has shapes worth rendering (not just pins)
|
|
163
|
+
*/
|
|
164
|
+
private hasRenderableShapes(symbol: EasyEDASymbolData): boolean {
|
|
165
|
+
return (
|
|
166
|
+
symbol.rectangles.length > 0 ||
|
|
167
|
+
symbol.circles.length > 0 ||
|
|
168
|
+
symbol.ellipses.length > 0 ||
|
|
169
|
+
symbol.polylines.length > 0 ||
|
|
170
|
+
symbol.polygons.length > 0 ||
|
|
171
|
+
symbol.arcs.length > 0 ||
|
|
172
|
+
symbol.paths.length > 0
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Generate symbol using parsed EasyEDA shapes
|
|
178
|
+
*/
|
|
179
|
+
private generateFromShapes(
|
|
180
|
+
component: EasyEDAComponentData,
|
|
181
|
+
name: string
|
|
182
|
+
): string {
|
|
183
|
+
const { info, symbol } = component;
|
|
184
|
+
const origin = symbol.origin;
|
|
185
|
+
|
|
186
|
+
let output = this.generateSymbolStart(name, false);
|
|
187
|
+
output += this.generateProperties(info, name);
|
|
188
|
+
|
|
189
|
+
// Generate graphics unit with shapes
|
|
190
|
+
output += `\t\t(symbol "${name}_0_1"\n`;
|
|
191
|
+
|
|
192
|
+
// Convert each shape type
|
|
193
|
+
for (const rect of symbol.rectangles) {
|
|
194
|
+
output += this.convertRectangle(rect, origin);
|
|
195
|
+
}
|
|
196
|
+
for (const circle of symbol.circles) {
|
|
197
|
+
output += this.convertCircle(circle, origin);
|
|
198
|
+
}
|
|
199
|
+
for (const ellipse of symbol.ellipses) {
|
|
200
|
+
output += this.convertEllipse(ellipse, origin);
|
|
201
|
+
}
|
|
202
|
+
for (const polyline of symbol.polylines) {
|
|
203
|
+
output += this.convertPolyline(polyline, origin);
|
|
204
|
+
}
|
|
205
|
+
for (const polygon of symbol.polygons) {
|
|
206
|
+
output += this.convertPolygon(polygon, origin);
|
|
207
|
+
}
|
|
208
|
+
for (const arc of symbol.arcs) {
|
|
209
|
+
const arcOutput = this.convertArc(arc, origin);
|
|
210
|
+
if (arcOutput) output += arcOutput;
|
|
211
|
+
}
|
|
212
|
+
for (const path of symbol.paths) {
|
|
213
|
+
const pathOutput = this.convertPath(path, origin);
|
|
214
|
+
if (pathOutput) output += pathOutput;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
output += `\t\t)\n`;
|
|
218
|
+
|
|
219
|
+
// Generate pins unit
|
|
220
|
+
output += `\t\t(symbol "${name}_1_1"\n`;
|
|
221
|
+
for (const pin of symbol.pins) {
|
|
222
|
+
output += this.generateShapePin(pin, origin);
|
|
223
|
+
}
|
|
224
|
+
output += `\t\t)\n`;
|
|
225
|
+
|
|
226
|
+
output += this.generateSymbolEnd();
|
|
227
|
+
|
|
228
|
+
return output;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Convert EasyEDA X coordinate to KiCad (with origin offset)
|
|
233
|
+
*/
|
|
234
|
+
private convertX(x: number, originX: number): number {
|
|
235
|
+
return roundTo((x - originX) * EE_TO_MM, 3);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Convert EasyEDA Y coordinate to KiCad (Y-flipped with origin offset)
|
|
240
|
+
*/
|
|
241
|
+
private convertY(y: number, originY: number): number {
|
|
242
|
+
return roundTo(-(y - originY) * EE_TO_MM, 3);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Convert stroke width from EasyEDA to KiCad
|
|
247
|
+
*/
|
|
248
|
+
private convertStrokeWidth(width: number): number {
|
|
249
|
+
return roundTo(Math.max(width * EE_TO_MM, 0.1), 3);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Convert EasyEDA rectangle to KiCad format
|
|
254
|
+
*/
|
|
255
|
+
private convertRectangle(rect: EasyEDASymbolRect, origin: { x: number; y: number }): string {
|
|
256
|
+
const x1 = this.convertX(rect.x, origin.x);
|
|
257
|
+
const y1 = this.convertY(rect.y, origin.y);
|
|
258
|
+
const x2 = this.convertX(rect.x + rect.width, origin.x);
|
|
259
|
+
const y2 = this.convertY(rect.y + rect.height, origin.y);
|
|
260
|
+
const strokeWidth = this.convertStrokeWidth(rect.strokeWidth);
|
|
261
|
+
|
|
262
|
+
return `\t\t\t(rectangle
|
|
263
|
+
\t\t\t\t(start ${x1} ${y1})
|
|
264
|
+
\t\t\t\t(end ${x2} ${y2})
|
|
265
|
+
\t\t\t\t(stroke
|
|
266
|
+
\t\t\t\t\t(width ${strokeWidth})
|
|
267
|
+
\t\t\t\t\t(type default)
|
|
268
|
+
\t\t\t\t)
|
|
269
|
+
\t\t\t\t(fill
|
|
270
|
+
\t\t\t\t\t(type background)
|
|
271
|
+
\t\t\t\t)
|
|
272
|
+
\t\t\t)
|
|
273
|
+
`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Convert EasyEDA circle to KiCad format
|
|
278
|
+
*/
|
|
279
|
+
private convertCircle(circle: EasyEDASymbolCircle, origin: { x: number; y: number }): string {
|
|
280
|
+
const cx = this.convertX(circle.cx, origin.x);
|
|
281
|
+
const cy = this.convertY(circle.cy, origin.y);
|
|
282
|
+
const radius = roundTo(circle.radius * EE_TO_MM, 3);
|
|
283
|
+
const strokeWidth = this.convertStrokeWidth(circle.strokeWidth);
|
|
284
|
+
|
|
285
|
+
return `\t\t\t(circle
|
|
286
|
+
\t\t\t\t(center ${cx} ${cy})
|
|
287
|
+
\t\t\t\t(radius ${radius})
|
|
288
|
+
\t\t\t\t(stroke
|
|
289
|
+
\t\t\t\t\t(width ${strokeWidth})
|
|
290
|
+
\t\t\t\t\t(type default)
|
|
291
|
+
\t\t\t\t)
|
|
292
|
+
\t\t\t\t(fill
|
|
293
|
+
\t\t\t\t\t(type none)
|
|
294
|
+
\t\t\t\t)
|
|
295
|
+
\t\t\t)
|
|
296
|
+
`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Convert EasyEDA ellipse to KiCad format (approximated as circle if rx≈ry)
|
|
301
|
+
* KiCad symbols don't support true ellipses, so we use a circle with average radius
|
|
302
|
+
*/
|
|
303
|
+
private convertEllipse(ellipse: EasyEDASymbolEllipse, origin: { x: number; y: number }): string {
|
|
304
|
+
const cx = this.convertX(ellipse.cx, origin.x);
|
|
305
|
+
const cy = this.convertY(ellipse.cy, origin.y);
|
|
306
|
+
// Use average radius for approximation
|
|
307
|
+
const radius = roundTo(((ellipse.radiusX + ellipse.radiusY) / 2) * EE_TO_MM, 3);
|
|
308
|
+
const strokeWidth = this.convertStrokeWidth(ellipse.strokeWidth);
|
|
309
|
+
|
|
310
|
+
return `\t\t\t(circle
|
|
311
|
+
\t\t\t\t(center ${cx} ${cy})
|
|
312
|
+
\t\t\t\t(radius ${radius})
|
|
313
|
+
\t\t\t\t(stroke
|
|
314
|
+
\t\t\t\t\t(width ${strokeWidth})
|
|
315
|
+
\t\t\t\t\t(type default)
|
|
316
|
+
\t\t\t\t)
|
|
317
|
+
\t\t\t\t(fill
|
|
318
|
+
\t\t\t\t\t(type none)
|
|
319
|
+
\t\t\t\t)
|
|
320
|
+
\t\t\t)
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Convert EasyEDA polyline to KiCad format
|
|
326
|
+
*/
|
|
327
|
+
private convertPolyline(polyline: EasyEDASymbolPolyline, origin: { x: number; y: number }): string {
|
|
328
|
+
const points = this.parsePoints(polyline.points, origin);
|
|
329
|
+
if (points.length < 2) return '';
|
|
330
|
+
|
|
331
|
+
const strokeWidth = this.convertStrokeWidth(polyline.strokeWidth);
|
|
332
|
+
const pointsStr = points.map(p => `(xy ${p.x} ${p.y})`).join('\n\t\t\t\t\t');
|
|
333
|
+
|
|
334
|
+
return `\t\t\t(polyline
|
|
335
|
+
\t\t\t\t(pts
|
|
336
|
+
\t\t\t\t\t${pointsStr}
|
|
337
|
+
\t\t\t\t)
|
|
338
|
+
\t\t\t\t(stroke
|
|
339
|
+
\t\t\t\t\t(width ${strokeWidth})
|
|
340
|
+
\t\t\t\t\t(type default)
|
|
341
|
+
\t\t\t\t)
|
|
342
|
+
\t\t\t\t(fill
|
|
343
|
+
\t\t\t\t\t(type none)
|
|
344
|
+
\t\t\t\t)
|
|
345
|
+
\t\t\t)
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Convert EasyEDA polygon to KiCad format (closed, filled polyline)
|
|
351
|
+
*/
|
|
352
|
+
private convertPolygon(polygon: EasyEDASymbolPolygon, origin: { x: number; y: number }): string {
|
|
353
|
+
const points = this.parsePoints(polygon.points, origin);
|
|
354
|
+
if (points.length < 3) return '';
|
|
355
|
+
|
|
356
|
+
const strokeWidth = this.convertStrokeWidth(polygon.strokeWidth);
|
|
357
|
+
const pointsStr = points.map(p => `(xy ${p.x} ${p.y})`).join('\n\t\t\t\t\t');
|
|
358
|
+
|
|
359
|
+
return `\t\t\t(polyline
|
|
360
|
+
\t\t\t\t(pts
|
|
361
|
+
\t\t\t\t\t${pointsStr}
|
|
362
|
+
\t\t\t\t)
|
|
363
|
+
\t\t\t\t(stroke
|
|
364
|
+
\t\t\t\t\t(width ${strokeWidth})
|
|
365
|
+
\t\t\t\t\t(type default)
|
|
366
|
+
\t\t\t\t)
|
|
367
|
+
\t\t\t\t(fill
|
|
368
|
+
\t\t\t\t\t(type background)
|
|
369
|
+
\t\t\t\t)
|
|
370
|
+
\t\t\t)
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Convert EasyEDA arc to KiCad format
|
|
376
|
+
* Uses SVG arc to center parameterization conversion
|
|
377
|
+
*/
|
|
378
|
+
private convertArc(arc: EasyEDASymbolArc, origin: { x: number; y: number }): string | null {
|
|
379
|
+
// Parse SVG arc path
|
|
380
|
+
const params = parseSvgArcPath(arc.path);
|
|
381
|
+
if (!params) return null;
|
|
382
|
+
|
|
383
|
+
// Convert to center parameterization
|
|
384
|
+
const center = svgArcToCenter(params);
|
|
385
|
+
if (!center) return null;
|
|
386
|
+
|
|
387
|
+
// Convert coordinates from EasyEDA to KiCad
|
|
388
|
+
const cx = this.convertX(center.cx, origin.x);
|
|
389
|
+
const cy = this.convertY(center.cy, origin.y);
|
|
390
|
+
const radius = roundTo(center.rx * EE_TO_MM, 3); // Use rx (arcs are typically circular)
|
|
391
|
+
|
|
392
|
+
// Convert start and end points
|
|
393
|
+
const startX = this.convertX(params.x1, origin.x);
|
|
394
|
+
const startY = this.convertY(params.y1, origin.y);
|
|
395
|
+
const endX = this.convertX(params.x2, origin.x);
|
|
396
|
+
const endY = this.convertY(params.y2, origin.y);
|
|
397
|
+
|
|
398
|
+
// Calculate mid-point on arc for KiCad
|
|
399
|
+
const midAngle = center.startAngle + center.deltaAngle / 2;
|
|
400
|
+
const midX = roundTo(cx + radius * Math.cos(midAngle), 3);
|
|
401
|
+
const midY = roundTo(cy - radius * Math.sin(midAngle), 3); // Y inverted
|
|
402
|
+
|
|
403
|
+
const strokeWidth = this.convertStrokeWidth(arc.strokeWidth);
|
|
404
|
+
|
|
405
|
+
return `\t\t\t(arc
|
|
406
|
+
\t\t\t\t(start ${startX} ${startY})
|
|
407
|
+
\t\t\t\t(mid ${midX} ${midY})
|
|
408
|
+
\t\t\t\t(end ${endX} ${endY})
|
|
409
|
+
\t\t\t\t(stroke
|
|
410
|
+
\t\t\t\t\t(width ${strokeWidth})
|
|
411
|
+
\t\t\t\t\t(type default)
|
|
412
|
+
\t\t\t\t)
|
|
413
|
+
\t\t\t\t(fill
|
|
414
|
+
\t\t\t\t\t(type none)
|
|
415
|
+
\t\t\t\t)
|
|
416
|
+
\t\t\t)
|
|
417
|
+
`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Convert EasyEDA SVG path to KiCad polyline
|
|
422
|
+
* Supports M (move), L (line), Z (close) commands
|
|
423
|
+
*/
|
|
424
|
+
private convertPath(path: EasyEDASymbolPath, origin: { x: number; y: number }): string | null {
|
|
425
|
+
const points = this.parseSvgPath(path.path, origin);
|
|
426
|
+
if (points.length < 2) return null;
|
|
427
|
+
|
|
428
|
+
const strokeWidth = this.convertStrokeWidth(path.strokeWidth);
|
|
429
|
+
const hasFill = path.fillColor && path.fillColor !== 'none' && path.fillColor !== '';
|
|
430
|
+
|
|
431
|
+
let output = `\t\t\t(polyline\n`;
|
|
432
|
+
output += `\t\t\t\t(pts\n`;
|
|
433
|
+
|
|
434
|
+
for (const pt of points) {
|
|
435
|
+
output += `\t\t\t\t\t(xy ${pt.x} ${pt.y})\n`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
output += `\t\t\t\t)\n`;
|
|
439
|
+
output += `\t\t\t\t(stroke\n`;
|
|
440
|
+
output += `\t\t\t\t\t(width ${strokeWidth})\n`;
|
|
441
|
+
output += `\t\t\t\t\t(type default)\n`;
|
|
442
|
+
output += `\t\t\t\t)\n`;
|
|
443
|
+
output += `\t\t\t\t(fill\n`;
|
|
444
|
+
output += `\t\t\t\t\t(type ${hasFill ? 'outline' : 'none'})\n`;
|
|
445
|
+
output += `\t\t\t\t)\n`;
|
|
446
|
+
output += `\t\t\t)\n`;
|
|
447
|
+
|
|
448
|
+
return output;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Parse SVG path string (M/L/Z commands) to array of points
|
|
453
|
+
* Format: "M x1,y1 L x2,y2 L x3,y3 Z" or "M x1 y1 L x2 y2 ..."
|
|
454
|
+
*/
|
|
455
|
+
private parseSvgPath(pathStr: string, origin: { x: number; y: number }): Array<{ x: number; y: number }> {
|
|
456
|
+
const points: Array<{ x: number; y: number }> = [];
|
|
457
|
+
let firstPoint: { x: number; y: number } | null = null;
|
|
458
|
+
|
|
459
|
+
// Match M and L commands with coordinates (supports both comma and space separators)
|
|
460
|
+
const commandRegex = /([MLZ])\s*([\d.-]+)?[,\s]*([\d.-]+)?/gi;
|
|
461
|
+
let match;
|
|
462
|
+
|
|
463
|
+
while ((match = commandRegex.exec(pathStr)) !== null) {
|
|
464
|
+
const cmd = match[1].toUpperCase();
|
|
465
|
+
|
|
466
|
+
if (cmd === 'M' || cmd === 'L') {
|
|
467
|
+
const x = parseFloat(match[2]);
|
|
468
|
+
const y = parseFloat(match[3]);
|
|
469
|
+
|
|
470
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
471
|
+
const point = {
|
|
472
|
+
x: this.convertX(x, origin.x),
|
|
473
|
+
y: this.convertY(y, origin.y),
|
|
474
|
+
};
|
|
475
|
+
points.push(point);
|
|
476
|
+
|
|
477
|
+
if (cmd === 'M' && !firstPoint) {
|
|
478
|
+
firstPoint = point;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
} else if (cmd === 'Z' && firstPoint) {
|
|
482
|
+
// Close path - add first point if not already there
|
|
483
|
+
const lastPoint = points[points.length - 1];
|
|
484
|
+
if (lastPoint && (lastPoint.x !== firstPoint.x || lastPoint.y !== firstPoint.y)) {
|
|
485
|
+
points.push({ ...firstPoint });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return points;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Parse space-separated point string to array of {x, y}
|
|
495
|
+
*/
|
|
496
|
+
private parsePoints(pointsStr: string, origin: { x: number; y: number }): Array<{ x: number; y: number }> {
|
|
497
|
+
const values = pointsStr.trim().split(/\s+/).map(parseFloat);
|
|
498
|
+
const points: Array<{ x: number; y: number }> = [];
|
|
499
|
+
|
|
500
|
+
for (let i = 0; i < values.length - 1; i += 2) {
|
|
501
|
+
points.push({
|
|
502
|
+
x: this.convertX(values[i], origin.x),
|
|
503
|
+
y: this.convertY(values[i + 1], origin.y),
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return points;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Generate a pin using EasyEDA coordinates and rotation
|
|
512
|
+
*
|
|
513
|
+
* EasyEDA: pin.x/y is the WIRE connection point (junction)
|
|
514
|
+
* rotation points FROM wire TO body
|
|
515
|
+
* KiCad: pin position is the WIRE connection point
|
|
516
|
+
* rotation points FROM wire TO body
|
|
517
|
+
*
|
|
518
|
+
* So we just need coordinate conversion with Y-flip!
|
|
519
|
+
*/
|
|
520
|
+
private generateShapePin(pin: EasyEDAPin, origin: { x: number; y: number }): string {
|
|
521
|
+
const pinType = this.mapPinType(pin.electricalType);
|
|
522
|
+
|
|
523
|
+
// Convert wire/junction position to KiCad coordinates (with Y-flip)
|
|
524
|
+
const x = this.convertX(pin.x, origin.x);
|
|
525
|
+
const y = this.convertY(pin.y, origin.y);
|
|
526
|
+
|
|
527
|
+
// Map EasyEDA rotation to KiCad rotation
|
|
528
|
+
// Both point FROM wire TO body, but Y-axis is flipped
|
|
529
|
+
const rotation = this.mapPinRotation(pin.rotation);
|
|
530
|
+
|
|
531
|
+
// Convert pin length
|
|
532
|
+
const pinLength = roundTo(pin.pinLength * EE_TO_MM, 2);
|
|
533
|
+
|
|
534
|
+
// Determine pin style based on indicators
|
|
535
|
+
const pinStyle = this.getPinStyle(pin);
|
|
536
|
+
|
|
537
|
+
const ts = IC_PIN_FONT_SIZE;
|
|
538
|
+
|
|
539
|
+
return `\t\t\t(pin ${pinType} ${pinStyle}
|
|
540
|
+
\t\t\t\t(at ${x} ${y} ${rotation})
|
|
541
|
+
\t\t\t\t(length ${pinLength})
|
|
542
|
+
\t\t\t\t(name "${this.sanitizePinName(pin.name)}"
|
|
543
|
+
\t\t\t\t\t(effects
|
|
544
|
+
\t\t\t\t\t\t(font
|
|
545
|
+
\t\t\t\t\t\t\t(size ${ts} ${ts})
|
|
546
|
+
\t\t\t\t\t\t)
|
|
547
|
+
\t\t\t\t\t)
|
|
548
|
+
\t\t\t\t)
|
|
549
|
+
\t\t\t\t(number "${pin.number}"
|
|
550
|
+
\t\t\t\t\t(effects
|
|
551
|
+
\t\t\t\t\t\t(font
|
|
552
|
+
\t\t\t\t\t\t\t(size ${ts} ${ts})
|
|
553
|
+
\t\t\t\t\t\t)
|
|
554
|
+
\t\t\t\t\t)
|
|
555
|
+
\t\t\t\t)
|
|
556
|
+
\t\t\t)
|
|
557
|
+
`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Map EasyEDA pin rotation to KiCad rotation
|
|
562
|
+
*
|
|
563
|
+
* EasyEDA: rotation indicates direction pin EXTENDS from wire (away from body)
|
|
564
|
+
* KiCad: rotation indicates direction pin POINTS (from wire toward body)
|
|
565
|
+
*
|
|
566
|
+
* So we need to flip by 180°, then account for Y-axis flip:
|
|
567
|
+
* - EasyEDA 0 (extends right, body on left) -> KiCad 180 (points left toward body)
|
|
568
|
+
* - EasyEDA 180 (extends left, body on right) -> KiCad 0 (points right toward body)
|
|
569
|
+
* - EasyEDA 90 (extends down, body on top) -> KiCad 90 (points up toward body, Y-flipped)
|
|
570
|
+
* - EasyEDA 270 (extends up, body on bottom) -> KiCad 270 (points down toward body, Y-flipped)
|
|
571
|
+
*/
|
|
572
|
+
private mapPinRotation(eeRotation: number): number {
|
|
573
|
+
const normalized = ((eeRotation % 360) + 360) % 360;
|
|
574
|
+
// Flip 180° to point toward body instead of away
|
|
575
|
+
// Then swap vertical due to Y-flip (but 180° flip already handles this naturally)
|
|
576
|
+
return (normalized + 180) % 360;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Get KiCad pin style from EasyEDA pin indicators
|
|
581
|
+
*/
|
|
582
|
+
private getPinStyle(pin: EasyEDAPin): string {
|
|
583
|
+
if (pin.hasDot && pin.hasClock) return 'inverted_clock';
|
|
584
|
+
if (pin.hasDot) return 'inverted';
|
|
585
|
+
if (pin.hasClock) return 'clock';
|
|
586
|
+
return 'line';
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Calculate DIP-style IC layout from EasyEDA pins
|
|
591
|
+
* Matches CDFER layout: wide body, pins extending from edges
|
|
592
|
+
*/
|
|
593
|
+
private calculateICLayout(pins: EasyEDAPin[]): ICLayout {
|
|
594
|
+
const pinCount = pins.length;
|
|
595
|
+
const halfPins = Math.ceil(pinCount / 2);
|
|
596
|
+
|
|
597
|
+
// Sort pins by pin number for proper DIP arrangement
|
|
598
|
+
const sortedPins = [...pins].sort((a, b) => {
|
|
599
|
+
const numA = parseInt(a.number) || 0;
|
|
600
|
+
const numB = parseInt(b.number) || 0;
|
|
601
|
+
return numA - numB;
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Calculate body dimensions (matching CDFER style)
|
|
605
|
+
// Height based on pin count: top pin at (halfPins-1)*spacing, bottom at 0
|
|
606
|
+
// Add padding above top pin and below bottom pin
|
|
607
|
+
const topY = (halfPins - 1) * IC_PIN_SPACING;
|
|
608
|
+
const bodyHeight = topY + IC_PIN_SPACING * 2; // Add padding top and bottom
|
|
609
|
+
const bodyWidth = IC_BODY_HALF_WIDTH * 2;
|
|
610
|
+
const halfHeight = bodyHeight / 2;
|
|
611
|
+
|
|
612
|
+
// Pin X positions (body edge + pin length)
|
|
613
|
+
const leftX = roundTo(-(IC_BODY_HALF_WIDTH + IC_PIN_LENGTH), 2);
|
|
614
|
+
const rightX = roundTo(IC_BODY_HALF_WIDTH + IC_PIN_LENGTH, 2);
|
|
615
|
+
|
|
616
|
+
// Split pins: 1 to n/2 on left (top to bottom), rest on right (bottom to top)
|
|
617
|
+
const leftPins: ICLayout['leftPins'] = [];
|
|
618
|
+
const rightPins: ICLayout['rightPins'] = [];
|
|
619
|
+
|
|
620
|
+
for (let i = 0; i < sortedPins.length; i++) {
|
|
621
|
+
const pin = sortedPins[i];
|
|
622
|
+
if (i < halfPins) {
|
|
623
|
+
// Left side pins (top to bottom): pin 1 at top
|
|
624
|
+
const y = topY - (i * IC_PIN_SPACING);
|
|
625
|
+
leftPins.push({ pin, x: leftX, y: roundTo(y, 3) });
|
|
626
|
+
} else {
|
|
627
|
+
// Right side pins (bottom to top): continues from bottom
|
|
628
|
+
const rightIndex = i - halfPins;
|
|
629
|
+
const y = rightIndex * IC_PIN_SPACING;
|
|
630
|
+
rightPins.push({ pin, x: rightX, y: roundTo(y, 3) });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return { bodyWidth, bodyHeight, leftPins, rightPins };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Generate IC body rectangle and pins in single _0_1 unit (CDFER style)
|
|
639
|
+
*/
|
|
640
|
+
private generateICGraphics(name: string, layout: ICLayout): string {
|
|
641
|
+
// Body: extends from bottom pin - padding to top pin + padding
|
|
642
|
+
const topPinY = layout.leftPins[0]?.y ?? 0;
|
|
643
|
+
const bottomPinY = layout.leftPins[layout.leftPins.length - 1]?.y ?? 0;
|
|
644
|
+
const bodyTop = topPinY + IC_PIN_SPACING;
|
|
645
|
+
const bodyBottom = bottomPinY - IC_PIN_SPACING;
|
|
646
|
+
|
|
647
|
+
let output = `\t\t(symbol "${name}_0_1"
|
|
648
|
+
\t\t\t(rectangle
|
|
649
|
+
\t\t\t\t(start ${-IC_BODY_HALF_WIDTH} ${roundTo(bodyTop, 2)})
|
|
650
|
+
\t\t\t\t(end ${IC_BODY_HALF_WIDTH} ${roundTo(bodyBottom, 2)})
|
|
651
|
+
\t\t\t\t(stroke
|
|
652
|
+
\t\t\t\t\t(width 0)
|
|
653
|
+
\t\t\t\t\t(type default)
|
|
654
|
+
\t\t\t\t)
|
|
655
|
+
\t\t\t\t(fill
|
|
656
|
+
\t\t\t\t\t(type background)
|
|
657
|
+
\t\t\t\t)
|
|
658
|
+
\t\t\t)
|
|
659
|
+
`;
|
|
660
|
+
|
|
661
|
+
// Add all pins to _0_1 unit (CDFER style - no separate _1_1 unit)
|
|
662
|
+
// Left side pins (pointing right, rotation 0)
|
|
663
|
+
for (const { pin, x, y } of layout.leftPins) {
|
|
664
|
+
const pinType = this.mapPinType(pin.electricalType);
|
|
665
|
+
output += `\t\t\t(pin ${pinType} line
|
|
666
|
+
\t\t\t\t(at ${x} ${y} 0)
|
|
667
|
+
\t\t\t\t(length ${IC_PIN_LENGTH})
|
|
668
|
+
\t\t\t\t(name "${this.sanitizePinName(pin.name)}"
|
|
669
|
+
\t\t\t\t\t(effects
|
|
670
|
+
\t\t\t\t\t\t(font
|
|
671
|
+
\t\t\t\t\t\t\t(size ${IC_PIN_FONT_SIZE} ${IC_PIN_FONT_SIZE})
|
|
672
|
+
\t\t\t\t\t\t)
|
|
673
|
+
\t\t\t\t\t)
|
|
674
|
+
\t\t\t\t)
|
|
675
|
+
\t\t\t\t(number "${pin.number}"
|
|
676
|
+
\t\t\t\t\t(effects
|
|
677
|
+
\t\t\t\t\t\t(font
|
|
678
|
+
\t\t\t\t\t\t\t(size ${IC_PIN_FONT_SIZE} ${IC_PIN_FONT_SIZE})
|
|
679
|
+
\t\t\t\t\t\t)
|
|
680
|
+
\t\t\t\t\t)
|
|
681
|
+
\t\t\t\t)
|
|
682
|
+
\t\t\t)
|
|
683
|
+
`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Right side pins (pointing left, rotation 180)
|
|
687
|
+
for (const { pin, x, y } of layout.rightPins) {
|
|
688
|
+
const pinType = this.mapPinType(pin.electricalType);
|
|
689
|
+
output += `\t\t\t(pin ${pinType} line
|
|
690
|
+
\t\t\t\t(at ${x} ${y} 180)
|
|
691
|
+
\t\t\t\t(length ${IC_PIN_LENGTH})
|
|
692
|
+
\t\t\t\t(name "${this.sanitizePinName(pin.name)}"
|
|
693
|
+
\t\t\t\t\t(effects
|
|
694
|
+
\t\t\t\t\t\t(font
|
|
695
|
+
\t\t\t\t\t\t\t(size ${IC_PIN_FONT_SIZE} ${IC_PIN_FONT_SIZE})
|
|
696
|
+
\t\t\t\t\t\t)
|
|
697
|
+
\t\t\t\t\t)
|
|
698
|
+
\t\t\t\t)
|
|
699
|
+
\t\t\t\t(number "${pin.number}"
|
|
700
|
+
\t\t\t\t\t(effects
|
|
701
|
+
\t\t\t\t\t\t(font
|
|
702
|
+
\t\t\t\t\t\t\t(size ${IC_PIN_FONT_SIZE} ${IC_PIN_FONT_SIZE})
|
|
703
|
+
\t\t\t\t\t\t)
|
|
704
|
+
\t\t\t\t\t)
|
|
705
|
+
\t\t\t\t)
|
|
706
|
+
\t\t\t)
|
|
707
|
+
`;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
output += `\t\t)
|
|
711
|
+
`;
|
|
712
|
+
return output;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Generate empty IC pins unit (pins are in _0_1 for CDFER compatibility)
|
|
717
|
+
*/
|
|
718
|
+
private generateICPins(name: string, layout: ICLayout): string {
|
|
719
|
+
// No _1_1 unit needed - all pins in _0_1
|
|
720
|
+
return '';
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Create a new library file with multiple symbols
|
|
725
|
+
*/
|
|
726
|
+
createLibrary(components: EasyEDAComponentData[]): string {
|
|
727
|
+
let output = this.generateHeader();
|
|
728
|
+
for (const component of components) {
|
|
729
|
+
output += this.convertToSymbolEntry(component);
|
|
730
|
+
}
|
|
731
|
+
output += ')\n';
|
|
732
|
+
return output;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Append a symbol to an existing library file content
|
|
737
|
+
* Returns the updated library content
|
|
738
|
+
*/
|
|
739
|
+
appendToLibrary(
|
|
740
|
+
existingLibraryContent: string,
|
|
741
|
+
component: EasyEDAComponentData,
|
|
742
|
+
options: SymbolConversionOptions = {}
|
|
743
|
+
): string {
|
|
744
|
+
// Remove the closing parenthesis and any trailing whitespace
|
|
745
|
+
const trimmed = existingLibraryContent.trimEnd();
|
|
746
|
+
if (!trimmed.endsWith(')')) {
|
|
747
|
+
throw new Error('Invalid library file format: missing closing parenthesis');
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Remove the last closing paren
|
|
751
|
+
const withoutClose = trimmed.slice(0, -1);
|
|
752
|
+
|
|
753
|
+
// Add the new symbol entry and close the library
|
|
754
|
+
const newSymbol = this.convertToSymbolEntry(component, options);
|
|
755
|
+
return withoutClose + newSymbol + ')\n';
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Check if a symbol already exists in a library
|
|
760
|
+
*/
|
|
761
|
+
symbolExistsInLibrary(libraryContent: string, componentName: string): boolean {
|
|
762
|
+
const sanitizedName = this.sanitizeName(componentName);
|
|
763
|
+
// Look for (symbol "NAME" pattern
|
|
764
|
+
const pattern = new RegExp(`\\(symbol\\s+"${sanitizedName}"`, 'm');
|
|
765
|
+
return pattern.test(libraryContent);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Replace an existing symbol in a library with a new version
|
|
770
|
+
* If the symbol doesn't exist, it will be appended
|
|
771
|
+
*/
|
|
772
|
+
replaceInLibrary(
|
|
773
|
+
existingLibraryContent: string,
|
|
774
|
+
component: EasyEDAComponentData,
|
|
775
|
+
options: SymbolConversionOptions = {}
|
|
776
|
+
): string {
|
|
777
|
+
const sanitizedName = this.sanitizeName(component.info.name);
|
|
778
|
+
|
|
779
|
+
// Check if symbol exists
|
|
780
|
+
if (!this.symbolExistsInLibrary(existingLibraryContent, component.info.name)) {
|
|
781
|
+
// Symbol doesn't exist, just append it
|
|
782
|
+
return this.appendToLibrary(existingLibraryContent, component, options);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Remove the existing symbol
|
|
786
|
+
// Symbol format: (symbol "NAME" ... ) with nested parens
|
|
787
|
+
// We need to find the start and match balanced parens to find the end
|
|
788
|
+
|
|
789
|
+
const symbolStart = existingLibraryContent.indexOf(`(symbol "${sanitizedName}"`);
|
|
790
|
+
if (symbolStart === -1) {
|
|
791
|
+
// Shouldn't happen since we checked above, but fallback to append
|
|
792
|
+
return this.appendToLibrary(existingLibraryContent, component, options);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Find the matching closing paren by counting balance
|
|
796
|
+
let depth = 0;
|
|
797
|
+
let symbolEnd = symbolStart;
|
|
798
|
+
let inString = false;
|
|
799
|
+
let prevChar = '';
|
|
800
|
+
|
|
801
|
+
for (let i = symbolStart; i < existingLibraryContent.length; i++) {
|
|
802
|
+
const char = existingLibraryContent[i];
|
|
803
|
+
|
|
804
|
+
// Handle string escaping
|
|
805
|
+
if (char === '"' && prevChar !== '\\') {
|
|
806
|
+
inString = !inString;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (!inString) {
|
|
810
|
+
if (char === '(') depth++;
|
|
811
|
+
if (char === ')') {
|
|
812
|
+
depth--;
|
|
813
|
+
if (depth === 0) {
|
|
814
|
+
symbolEnd = i + 1;
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
prevChar = char;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Remove the old symbol (including any trailing newline)
|
|
824
|
+
let contentWithoutOldSymbol = existingLibraryContent.slice(0, symbolStart);
|
|
825
|
+
let afterSymbol = existingLibraryContent.slice(symbolEnd);
|
|
826
|
+
|
|
827
|
+
// Trim leading newlines from afterSymbol to avoid double spacing
|
|
828
|
+
while (afterSymbol.startsWith('\n')) {
|
|
829
|
+
afterSymbol = afterSymbol.slice(1);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
contentWithoutOldSymbol = contentWithoutOldSymbol + afterSymbol;
|
|
833
|
+
|
|
834
|
+
// Now append the new version
|
|
835
|
+
return this.appendToLibrary(contentWithoutOldSymbol, component, options);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Get the sanitized symbol name for a component
|
|
840
|
+
*/
|
|
841
|
+
getSymbolName(component: EasyEDAComponentData): string {
|
|
842
|
+
return this.sanitizeName(component.info.name);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Generate file header
|
|
847
|
+
*/
|
|
848
|
+
private generateHeader(): string {
|
|
849
|
+
return `(kicad_symbol_lib
|
|
850
|
+
\t(version ${KICAD_SYMBOL_VERSION})
|
|
851
|
+
\t(generator "ai-eda-jlc-mcp")
|
|
852
|
+
\t(generator_version "9.0")
|
|
853
|
+
`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Generate symbol start - NO library prefix in symbol name
|
|
858
|
+
*/
|
|
859
|
+
private generateSymbolStart(name: string, hideNumbers = true): string {
|
|
860
|
+
// For passives, hide pin numbers; for ICs, show them (CDFER style)
|
|
861
|
+
if (hideNumbers) {
|
|
862
|
+
return `\t(symbol "${name}"
|
|
863
|
+
\t\t(pin_numbers
|
|
864
|
+
\t\t\t(hide yes)
|
|
865
|
+
\t\t)
|
|
866
|
+
\t\t(pin_names
|
|
867
|
+
\t\t\t(offset 0)
|
|
868
|
+
\t\t)
|
|
869
|
+
\t\t(exclude_from_sim no)
|
|
870
|
+
\t\t(in_bom yes)
|
|
871
|
+
\t\t(on_board yes)
|
|
872
|
+
`;
|
|
873
|
+
}
|
|
874
|
+
// IC style - no pin_numbers/pin_names sections (show by default)
|
|
875
|
+
return `\t(symbol "${name}"
|
|
876
|
+
\t\t(exclude_from_sim no)
|
|
877
|
+
\t\t(in_bom yes)
|
|
878
|
+
\t\t(on_board yes)
|
|
879
|
+
`;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Generate KiCad property entries in KiCad 9 format
|
|
884
|
+
*/
|
|
885
|
+
private generateProperties(info: EasyEDAComponentData['info'], name: string): string {
|
|
886
|
+
const ts = KICAD_DEFAULTS.TEXT_SIZE;
|
|
887
|
+
let props = '';
|
|
888
|
+
|
|
889
|
+
// Extract normalized display value for passives
|
|
890
|
+
const displayValue = extractDisplayValue(
|
|
891
|
+
info.name,
|
|
892
|
+
info.description,
|
|
893
|
+
info.prefix,
|
|
894
|
+
info.category
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
// Reference property - centered above body (CDFER style, no "?" suffix)
|
|
898
|
+
const refDesignator = (info.prefix || 'U').replace(/\?$/, '');
|
|
899
|
+
props += `\t\t(property "Reference" "${refDesignator}"
|
|
900
|
+
\t\t\t(at 0 10.16 0)
|
|
901
|
+
\t\t\t(effects
|
|
902
|
+
\t\t\t\t(font
|
|
903
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
904
|
+
\t\t\t\t)
|
|
905
|
+
\t\t\t)
|
|
906
|
+
\t\t)
|
|
907
|
+
`;
|
|
908
|
+
|
|
909
|
+
// Value property - centered below reference (CDFER style)
|
|
910
|
+
props += `\t\t(property "Value" "${this.sanitizeText(displayValue)}"
|
|
911
|
+
\t\t\t(at 0 7.62 0)
|
|
912
|
+
\t\t\t(effects
|
|
913
|
+
\t\t\t\t(font
|
|
914
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
915
|
+
\t\t\t\t)
|
|
916
|
+
\t\t\t)
|
|
917
|
+
\t\t)
|
|
918
|
+
`;
|
|
919
|
+
|
|
920
|
+
// Footprint property
|
|
921
|
+
props += `\t\t(property "Footprint" "${info.package || ''}"
|
|
922
|
+
\t\t\t(at -1.778 0 90)
|
|
923
|
+
\t\t\t(effects
|
|
924
|
+
\t\t\t\t(font
|
|
925
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
926
|
+
\t\t\t\t)
|
|
927
|
+
\t\t\t\t(hide yes)
|
|
928
|
+
\t\t\t)
|
|
929
|
+
\t\t)
|
|
930
|
+
`;
|
|
931
|
+
|
|
932
|
+
// Datasheet property - prefer JLC API PDF URL, fallback to constructed URL
|
|
933
|
+
const datasheetUrl = info.datasheetPdf || (info.lcscId ? `https://www.lcsc.com/datasheet/${info.lcscId}.pdf` : '~');
|
|
934
|
+
props += `\t\t(property "Datasheet" "${datasheetUrl}"
|
|
935
|
+
\t\t\t(at 0 0 0)
|
|
936
|
+
\t\t\t(effects
|
|
937
|
+
\t\t\t\t(font
|
|
938
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
939
|
+
\t\t\t\t)
|
|
940
|
+
\t\t\t\t(hide yes)
|
|
941
|
+
\t\t\t)
|
|
942
|
+
\t\t)
|
|
943
|
+
`;
|
|
944
|
+
|
|
945
|
+
// Product Page property - link to LCSC product page
|
|
946
|
+
if (info.datasheet) {
|
|
947
|
+
props += `\t\t(property "Product Page" "${info.datasheet}"
|
|
948
|
+
\t\t\t(at 0 0 0)
|
|
949
|
+
\t\t\t(effects
|
|
950
|
+
\t\t\t\t(font
|
|
951
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
952
|
+
\t\t\t\t)
|
|
953
|
+
\t\t\t\t(hide yes)
|
|
954
|
+
\t\t\t)
|
|
955
|
+
\t\t)
|
|
956
|
+
`;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Description property - use actual description if available
|
|
960
|
+
const description = info.description || info.name;
|
|
961
|
+
props += `\t\t(property "Description" "${this.sanitizeText(description)}"
|
|
962
|
+
\t\t\t(at 0 0 0)
|
|
963
|
+
\t\t\t(effects
|
|
964
|
+
\t\t\t\t(font
|
|
965
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
966
|
+
\t\t\t\t)
|
|
967
|
+
\t\t\t\t(hide yes)
|
|
968
|
+
\t\t\t)
|
|
969
|
+
\t\t)
|
|
970
|
+
`;
|
|
971
|
+
|
|
972
|
+
// LCSC property
|
|
973
|
+
if (info.lcscId) {
|
|
974
|
+
props += `\t\t(property "LCSC" "${info.lcscId}"
|
|
975
|
+
\t\t\t(at 0 0 0)
|
|
976
|
+
\t\t\t(effects
|
|
977
|
+
\t\t\t\t(font
|
|
978
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
979
|
+
\t\t\t\t)
|
|
980
|
+
\t\t\t\t(hide yes)
|
|
981
|
+
\t\t\t)
|
|
982
|
+
\t\t)
|
|
983
|
+
`;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Manufacturer property
|
|
987
|
+
if (info.manufacturer) {
|
|
988
|
+
props += `\t\t(property "Manufacturer" "${this.sanitizeText(info.manufacturer)}"
|
|
989
|
+
\t\t\t(at 0 0 0)
|
|
990
|
+
\t\t\t(effects
|
|
991
|
+
\t\t\t\t(font
|
|
992
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
993
|
+
\t\t\t\t)
|
|
994
|
+
\t\t\t\t(hide yes)
|
|
995
|
+
\t\t\t)
|
|
996
|
+
\t\t)
|
|
997
|
+
`;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Category property
|
|
1001
|
+
if (info.category) {
|
|
1002
|
+
props += `\t\t(property "Category" "${this.sanitizeText(info.category)}"
|
|
1003
|
+
\t\t\t(at 0 0 0)
|
|
1004
|
+
\t\t\t(effects
|
|
1005
|
+
\t\t\t\t(font
|
|
1006
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1007
|
+
\t\t\t\t)
|
|
1008
|
+
\t\t\t\t(hide yes)
|
|
1009
|
+
\t\t\t)
|
|
1010
|
+
\t\t)
|
|
1011
|
+
`;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// ki_keywords property - LCSC ID for searchability (CDFER style)
|
|
1015
|
+
if (info.lcscId) {
|
|
1016
|
+
props += `\t\t(property "ki_keywords" "${info.lcscId}"
|
|
1017
|
+
\t\t\t(at 0 0 0)
|
|
1018
|
+
\t\t\t(effects
|
|
1019
|
+
\t\t\t\t(font
|
|
1020
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1021
|
+
\t\t\t\t)
|
|
1022
|
+
\t\t\t\t(hide yes)
|
|
1023
|
+
\t\t\t)
|
|
1024
|
+
\t\t)
|
|
1025
|
+
`;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// CDFER parity properties
|
|
1029
|
+
if (info.stock !== undefined) {
|
|
1030
|
+
props += `\t\t(property "Stock" "${info.stock}"
|
|
1031
|
+
\t\t\t(at 0 0 0)
|
|
1032
|
+
\t\t\t(effects
|
|
1033
|
+
\t\t\t\t(font
|
|
1034
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1035
|
+
\t\t\t\t)
|
|
1036
|
+
\t\t\t\t(hide yes)
|
|
1037
|
+
\t\t\t)
|
|
1038
|
+
\t\t)
|
|
1039
|
+
`;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (info.price !== undefined) {
|
|
1043
|
+
props += `\t\t(property "Price" "${info.price}USD"
|
|
1044
|
+
\t\t\t(at 0 0 0)
|
|
1045
|
+
\t\t\t(effects
|
|
1046
|
+
\t\t\t\t(font
|
|
1047
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1048
|
+
\t\t\t\t)
|
|
1049
|
+
\t\t\t\t(hide yes)
|
|
1050
|
+
\t\t\t)
|
|
1051
|
+
\t\t)
|
|
1052
|
+
`;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (info.process) {
|
|
1056
|
+
props += `\t\t(property "Process" "${info.process}"
|
|
1057
|
+
\t\t\t(at 0 0 0)
|
|
1058
|
+
\t\t\t(effects
|
|
1059
|
+
\t\t\t\t(font
|
|
1060
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1061
|
+
\t\t\t\t)
|
|
1062
|
+
\t\t\t\t(hide yes)
|
|
1063
|
+
\t\t\t)
|
|
1064
|
+
\t\t)
|
|
1065
|
+
`;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (info.minOrderQty !== undefined) {
|
|
1069
|
+
props += `\t\t(property "Minimum Qty" "${info.minOrderQty}"
|
|
1070
|
+
\t\t\t(at 0 0 0)
|
|
1071
|
+
\t\t\t(effects
|
|
1072
|
+
\t\t\t\t(font
|
|
1073
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1074
|
+
\t\t\t\t)
|
|
1075
|
+
\t\t\t\t(hide yes)
|
|
1076
|
+
\t\t\t)
|
|
1077
|
+
\t\t)
|
|
1078
|
+
`;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Attrition Qty - always add with default 0
|
|
1082
|
+
props += `\t\t(property "Attrition Qty" "0"
|
|
1083
|
+
\t\t\t(at 0 0 0)
|
|
1084
|
+
\t\t\t(effects
|
|
1085
|
+
\t\t\t\t(font
|
|
1086
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1087
|
+
\t\t\t\t)
|
|
1088
|
+
\t\t\t\t(hide yes)
|
|
1089
|
+
\t\t\t)
|
|
1090
|
+
\t\t)
|
|
1091
|
+
`;
|
|
1092
|
+
|
|
1093
|
+
if (info.partClass) {
|
|
1094
|
+
props += `\t\t(property "Class" "${this.sanitizeText(info.partClass)}"
|
|
1095
|
+
\t\t\t(at 0 0 0)
|
|
1096
|
+
\t\t\t(effects
|
|
1097
|
+
\t\t\t\t(font
|
|
1098
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1099
|
+
\t\t\t\t)
|
|
1100
|
+
\t\t\t\t(hide yes)
|
|
1101
|
+
\t\t\t)
|
|
1102
|
+
\t\t)
|
|
1103
|
+
`;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (info.partNumber) {
|
|
1107
|
+
props += `\t\t(property "Part" "${this.sanitizeText(info.partNumber)}"
|
|
1108
|
+
\t\t\t(at 0 0 0)
|
|
1109
|
+
\t\t\t(effects
|
|
1110
|
+
\t\t\t\t(font
|
|
1111
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1112
|
+
\t\t\t\t)
|
|
1113
|
+
\t\t\t\t(hide yes)
|
|
1114
|
+
\t\t\t)
|
|
1115
|
+
\t\t)
|
|
1116
|
+
`;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Component attributes as custom properties
|
|
1120
|
+
if (info.attributes) {
|
|
1121
|
+
for (const [key, value] of Object.entries(info.attributes)) {
|
|
1122
|
+
props += `\t\t(property "${this.sanitizeText(key)}" "${this.sanitizeText(String(value))}"
|
|
1123
|
+
\t\t\t(at 0 0 0)
|
|
1124
|
+
\t\t\t(effects
|
|
1125
|
+
\t\t\t\t(font
|
|
1126
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1127
|
+
\t\t\t\t)
|
|
1128
|
+
\t\t\t\t(hide yes)
|
|
1129
|
+
\t\t\t)
|
|
1130
|
+
\t\t)
|
|
1131
|
+
`;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return props;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Calculate bounding box from pins (for IC/complex components)
|
|
1140
|
+
* Uses improved padding for better visual appearance
|
|
1141
|
+
*/
|
|
1142
|
+
private calculateBoundingBox(pins: EasyEDAPin[], origin: { x: number; y: number }): BoundingBox {
|
|
1143
|
+
let minX = Infinity, maxX = -Infinity;
|
|
1144
|
+
let minY = Infinity, maxY = -Infinity;
|
|
1145
|
+
|
|
1146
|
+
for (const pin of pins) {
|
|
1147
|
+
const x = (pin.x - origin.x) * EE_TO_MM;
|
|
1148
|
+
const y = -(pin.y - origin.y) * EE_TO_MM;
|
|
1149
|
+
minX = Math.min(minX, x);
|
|
1150
|
+
maxX = Math.max(maxX, x);
|
|
1151
|
+
minY = Math.min(minY, y);
|
|
1152
|
+
maxY = Math.max(maxY, y);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// If no pins, use minimum IC body size
|
|
1156
|
+
if (!isFinite(minX)) {
|
|
1157
|
+
const half = IC_MIN_BODY_SIZE / 2;
|
|
1158
|
+
return { minX: -half, maxX: half, minY: -half, maxY: half };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Add padding and shrink to create body rectangle inside pin positions
|
|
1162
|
+
// Uses IC_BODY_PADDING for adequate spacing between pins and body edge
|
|
1163
|
+
return {
|
|
1164
|
+
minX: roundTo(minX + IC_BODY_PADDING, 3),
|
|
1165
|
+
maxX: roundTo(maxX - IC_BODY_PADDING, 3),
|
|
1166
|
+
minY: roundTo(minY + IC_BODY_PADDING, 3),
|
|
1167
|
+
maxY: roundTo(maxY - IC_BODY_PADDING, 3),
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Generate _0_1 unit with graphical elements (rectangle body)
|
|
1173
|
+
* Uses IC_MIN_BODY_SIZE for minimum dimensions
|
|
1174
|
+
*/
|
|
1175
|
+
private generateGraphicsUnit(name: string, bbox: BoundingBox): string {
|
|
1176
|
+
// Ensure minimum size for ICs
|
|
1177
|
+
const width = Math.max(bbox.maxX - bbox.minX, IC_MIN_BODY_SIZE);
|
|
1178
|
+
const height = Math.max(bbox.maxY - bbox.minY, IC_MIN_BODY_SIZE);
|
|
1179
|
+
|
|
1180
|
+
// Center the rectangle if too small
|
|
1181
|
+
let x1 = bbox.minX;
|
|
1182
|
+
let x2 = bbox.maxX;
|
|
1183
|
+
let y1 = bbox.minY;
|
|
1184
|
+
let y2 = bbox.maxY;
|
|
1185
|
+
|
|
1186
|
+
const halfMin = IC_MIN_BODY_SIZE / 2;
|
|
1187
|
+
if (width < IC_MIN_BODY_SIZE) {
|
|
1188
|
+
x1 = -halfMin;
|
|
1189
|
+
x2 = halfMin;
|
|
1190
|
+
}
|
|
1191
|
+
if (height < IC_MIN_BODY_SIZE) {
|
|
1192
|
+
y1 = -halfMin;
|
|
1193
|
+
y2 = halfMin;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
return `\t\t(symbol "${name}_0_1"
|
|
1197
|
+
\t\t\t(rectangle
|
|
1198
|
+
\t\t\t\t(start ${roundTo(x1, 3)} ${roundTo(y1, 3)})
|
|
1199
|
+
\t\t\t\t(end ${roundTo(x2, 3)} ${roundTo(y2, 3)})
|
|
1200
|
+
\t\t\t\t(stroke
|
|
1201
|
+
\t\t\t\t\t(width 0.254)
|
|
1202
|
+
\t\t\t\t\t(type default)
|
|
1203
|
+
\t\t\t\t)
|
|
1204
|
+
\t\t\t\t(fill
|
|
1205
|
+
\t\t\t\t\t(type background)
|
|
1206
|
+
\t\t\t\t)
|
|
1207
|
+
\t\t\t)
|
|
1208
|
+
\t\t)
|
|
1209
|
+
`;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Generate _1_1 unit with pins
|
|
1214
|
+
*/
|
|
1215
|
+
private generatePinsUnit(name: string, pins: EasyEDAPin[], origin: { x: number; y: number }): string {
|
|
1216
|
+
let output = `\t\t(symbol "${name}_1_1"
|
|
1217
|
+
`;
|
|
1218
|
+
|
|
1219
|
+
for (const pin of pins) {
|
|
1220
|
+
output += this.generatePin(pin, origin);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
output += `\t\t)
|
|
1224
|
+
`;
|
|
1225
|
+
return output;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Generate a single pin entry in KiCad 9 format
|
|
1230
|
+
* Uses IC_PIN_LENGTH for better readability with complex components
|
|
1231
|
+
*/
|
|
1232
|
+
private generatePin(pin: EasyEDAPin, origin: { x: number; y: number }): string {
|
|
1233
|
+
const pinType = this.mapPinType(pin.electricalType);
|
|
1234
|
+
const x = roundTo((pin.x - origin.x) * EE_TO_MM, 3);
|
|
1235
|
+
const y = roundTo(-(pin.y - origin.y) * EE_TO_MM, 3);
|
|
1236
|
+
const rotation = this.calculatePinRotation(x, y);
|
|
1237
|
+
const ts = KICAD_DEFAULTS.TEXT_SIZE;
|
|
1238
|
+
|
|
1239
|
+
return `\t\t\t(pin ${pinType} line
|
|
1240
|
+
\t\t\t\t(at ${x} ${y} ${rotation})
|
|
1241
|
+
\t\t\t\t(length ${IC_PIN_LENGTH})
|
|
1242
|
+
\t\t\t\t(name "${this.sanitizePinName(pin.name)}"
|
|
1243
|
+
\t\t\t\t\t(effects
|
|
1244
|
+
\t\t\t\t\t\t(font
|
|
1245
|
+
\t\t\t\t\t\t\t(size ${ts} ${ts})
|
|
1246
|
+
\t\t\t\t\t\t)
|
|
1247
|
+
\t\t\t\t\t)
|
|
1248
|
+
\t\t\t\t)
|
|
1249
|
+
\t\t\t\t(number "${pin.number}"
|
|
1250
|
+
\t\t\t\t\t(effects
|
|
1251
|
+
\t\t\t\t\t\t(font
|
|
1252
|
+
\t\t\t\t\t\t\t(size ${ts} ${ts})
|
|
1253
|
+
\t\t\t\t\t\t)
|
|
1254
|
+
\t\t\t\t\t)
|
|
1255
|
+
\t\t\t\t)
|
|
1256
|
+
\t\t\t)
|
|
1257
|
+
`;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Generate symbol end (closes the symbol, NOT the library)
|
|
1262
|
+
*/
|
|
1263
|
+
private generateSymbolEnd(): string {
|
|
1264
|
+
return `\t\t(embedded_fonts no)
|
|
1265
|
+
\t)
|
|
1266
|
+
`;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Generate properties for template-based symbols
|
|
1271
|
+
*/
|
|
1272
|
+
private generateTemplateProperties(
|
|
1273
|
+
info: EasyEDAComponentData['info'],
|
|
1274
|
+
name: string,
|
|
1275
|
+
template: SymbolTemplate
|
|
1276
|
+
): string {
|
|
1277
|
+
const ts = KICAD_DEFAULTS.TEXT_SIZE;
|
|
1278
|
+
let props = '';
|
|
1279
|
+
|
|
1280
|
+
// Extract normalized display value for passives
|
|
1281
|
+
const displayValue = extractDisplayValue(
|
|
1282
|
+
info.name,
|
|
1283
|
+
info.description,
|
|
1284
|
+
info.prefix,
|
|
1285
|
+
info.category
|
|
1286
|
+
);
|
|
1287
|
+
|
|
1288
|
+
// Reference property - positioned from template (no "?" suffix)
|
|
1289
|
+
const refDesignator = (info.prefix || 'U').replace(/\?$/, '');
|
|
1290
|
+
props += `\t\t(property "Reference" "${refDesignator}"
|
|
1291
|
+
\t\t\t(at ${template.refPosition.x} ${template.refPosition.y} ${template.refPosition.angle})
|
|
1292
|
+
\t\t\t(effects
|
|
1293
|
+
\t\t\t\t(font
|
|
1294
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1295
|
+
\t\t\t\t)
|
|
1296
|
+
\t\t\t)
|
|
1297
|
+
\t\t)
|
|
1298
|
+
`;
|
|
1299
|
+
|
|
1300
|
+
// Value property - positioned from template
|
|
1301
|
+
props += `\t\t(property "Value" "${this.sanitizeText(displayValue)}"
|
|
1302
|
+
\t\t\t(at ${template.valuePosition.x} ${template.valuePosition.y} ${template.valuePosition.angle})
|
|
1303
|
+
\t\t\t(effects
|
|
1304
|
+
\t\t\t\t(font
|
|
1305
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1306
|
+
\t\t\t\t)
|
|
1307
|
+
\t\t\t)
|
|
1308
|
+
\t\t)
|
|
1309
|
+
`;
|
|
1310
|
+
|
|
1311
|
+
// Footprint property
|
|
1312
|
+
props += `\t\t(property "Footprint" "${info.package || ''}"
|
|
1313
|
+
\t\t\t(at 0 0 0)
|
|
1314
|
+
\t\t\t(effects
|
|
1315
|
+
\t\t\t\t(font
|
|
1316
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1317
|
+
\t\t\t\t)
|
|
1318
|
+
\t\t\t\t(hide yes)
|
|
1319
|
+
\t\t\t)
|
|
1320
|
+
\t\t)
|
|
1321
|
+
`;
|
|
1322
|
+
|
|
1323
|
+
// Datasheet property - prefer JLC API PDF URL, fallback to constructed URL
|
|
1324
|
+
const datasheetUrl = info.datasheetPdf || (info.lcscId ? `https://www.lcsc.com/datasheet/${info.lcscId}.pdf` : '~');
|
|
1325
|
+
props += `\t\t(property "Datasheet" "${datasheetUrl}"
|
|
1326
|
+
\t\t\t(at 0 0 0)
|
|
1327
|
+
\t\t\t(effects
|
|
1328
|
+
\t\t\t\t(font
|
|
1329
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1330
|
+
\t\t\t\t)
|
|
1331
|
+
\t\t\t\t(hide yes)
|
|
1332
|
+
\t\t\t)
|
|
1333
|
+
\t\t)
|
|
1334
|
+
`;
|
|
1335
|
+
|
|
1336
|
+
// Product Page property - link to LCSC product page
|
|
1337
|
+
if (info.datasheet) {
|
|
1338
|
+
props += `\t\t(property "Product Page" "${info.datasheet}"
|
|
1339
|
+
\t\t\t(at 0 0 0)
|
|
1340
|
+
\t\t\t(effects
|
|
1341
|
+
\t\t\t\t(font
|
|
1342
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1343
|
+
\t\t\t\t)
|
|
1344
|
+
\t\t\t\t(hide yes)
|
|
1345
|
+
\t\t\t)
|
|
1346
|
+
\t\t)
|
|
1347
|
+
`;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// Description property
|
|
1351
|
+
const description = info.description || info.name;
|
|
1352
|
+
props += `\t\t(property "Description" "${this.sanitizeText(description)}"
|
|
1353
|
+
\t\t\t(at 0 0 0)
|
|
1354
|
+
\t\t\t(effects
|
|
1355
|
+
\t\t\t\t(font
|
|
1356
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1357
|
+
\t\t\t\t)
|
|
1358
|
+
\t\t\t\t(hide yes)
|
|
1359
|
+
\t\t\t)
|
|
1360
|
+
\t\t)
|
|
1361
|
+
`;
|
|
1362
|
+
|
|
1363
|
+
// LCSC property
|
|
1364
|
+
if (info.lcscId) {
|
|
1365
|
+
props += `\t\t(property "LCSC" "${info.lcscId}"
|
|
1366
|
+
\t\t\t(at 0 0 0)
|
|
1367
|
+
\t\t\t(effects
|
|
1368
|
+
\t\t\t\t(font
|
|
1369
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1370
|
+
\t\t\t\t)
|
|
1371
|
+
\t\t\t\t(hide yes)
|
|
1372
|
+
\t\t\t)
|
|
1373
|
+
\t\t)
|
|
1374
|
+
`;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Manufacturer property
|
|
1378
|
+
if (info.manufacturer) {
|
|
1379
|
+
props += `\t\t(property "Manufacturer" "${this.sanitizeText(info.manufacturer)}"
|
|
1380
|
+
\t\t\t(at 0 0 0)
|
|
1381
|
+
\t\t\t(effects
|
|
1382
|
+
\t\t\t\t(font
|
|
1383
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1384
|
+
\t\t\t\t)
|
|
1385
|
+
\t\t\t\t(hide yes)
|
|
1386
|
+
\t\t\t)
|
|
1387
|
+
\t\t)
|
|
1388
|
+
`;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Category property
|
|
1392
|
+
if (info.category) {
|
|
1393
|
+
props += `\t\t(property "Category" "${this.sanitizeText(info.category)}"
|
|
1394
|
+
\t\t\t(at 0 0 0)
|
|
1395
|
+
\t\t\t(effects
|
|
1396
|
+
\t\t\t\t(font
|
|
1397
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1398
|
+
\t\t\t\t)
|
|
1399
|
+
\t\t\t\t(hide yes)
|
|
1400
|
+
\t\t\t)
|
|
1401
|
+
\t\t)
|
|
1402
|
+
`;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// ki_keywords property - LCSC ID for searchability (CDFER style)
|
|
1406
|
+
if (info.lcscId) {
|
|
1407
|
+
props += `\t\t(property "ki_keywords" "${info.lcscId}"
|
|
1408
|
+
\t\t\t(at 0 0 0)
|
|
1409
|
+
\t\t\t(effects
|
|
1410
|
+
\t\t\t\t(font
|
|
1411
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1412
|
+
\t\t\t\t)
|
|
1413
|
+
\t\t\t\t(hide yes)
|
|
1414
|
+
\t\t\t)
|
|
1415
|
+
\t\t)
|
|
1416
|
+
`;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// CDFER parity properties
|
|
1420
|
+
if (info.stock !== undefined) {
|
|
1421
|
+
props += `\t\t(property "Stock" "${info.stock}"
|
|
1422
|
+
\t\t\t(at 0 0 0)
|
|
1423
|
+
\t\t\t(effects
|
|
1424
|
+
\t\t\t\t(font
|
|
1425
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1426
|
+
\t\t\t\t)
|
|
1427
|
+
\t\t\t\t(hide yes)
|
|
1428
|
+
\t\t\t)
|
|
1429
|
+
\t\t)
|
|
1430
|
+
`;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
if (info.price !== undefined) {
|
|
1434
|
+
props += `\t\t(property "Price" "${info.price}USD"
|
|
1435
|
+
\t\t\t(at 0 0 0)
|
|
1436
|
+
\t\t\t(effects
|
|
1437
|
+
\t\t\t\t(font
|
|
1438
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1439
|
+
\t\t\t\t)
|
|
1440
|
+
\t\t\t\t(hide yes)
|
|
1441
|
+
\t\t\t)
|
|
1442
|
+
\t\t)
|
|
1443
|
+
`;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (info.process) {
|
|
1447
|
+
props += `\t\t(property "Process" "${info.process}"
|
|
1448
|
+
\t\t\t(at 0 0 0)
|
|
1449
|
+
\t\t\t(effects
|
|
1450
|
+
\t\t\t\t(font
|
|
1451
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1452
|
+
\t\t\t\t)
|
|
1453
|
+
\t\t\t\t(hide yes)
|
|
1454
|
+
\t\t\t)
|
|
1455
|
+
\t\t)
|
|
1456
|
+
`;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (info.minOrderQty !== undefined) {
|
|
1460
|
+
props += `\t\t(property "Minimum Qty" "${info.minOrderQty}"
|
|
1461
|
+
\t\t\t(at 0 0 0)
|
|
1462
|
+
\t\t\t(effects
|
|
1463
|
+
\t\t\t\t(font
|
|
1464
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1465
|
+
\t\t\t\t)
|
|
1466
|
+
\t\t\t\t(hide yes)
|
|
1467
|
+
\t\t\t)
|
|
1468
|
+
\t\t)
|
|
1469
|
+
`;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// Attrition Qty - always add with default 0
|
|
1473
|
+
props += `\t\t(property "Attrition Qty" "0"
|
|
1474
|
+
\t\t\t(at 0 0 0)
|
|
1475
|
+
\t\t\t(effects
|
|
1476
|
+
\t\t\t\t(font
|
|
1477
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1478
|
+
\t\t\t\t)
|
|
1479
|
+
\t\t\t\t(hide yes)
|
|
1480
|
+
\t\t\t)
|
|
1481
|
+
\t\t)
|
|
1482
|
+
`;
|
|
1483
|
+
|
|
1484
|
+
if (info.partClass) {
|
|
1485
|
+
props += `\t\t(property "Class" "${this.sanitizeText(info.partClass)}"
|
|
1486
|
+
\t\t\t(at 0 0 0)
|
|
1487
|
+
\t\t\t(effects
|
|
1488
|
+
\t\t\t\t(font
|
|
1489
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1490
|
+
\t\t\t\t)
|
|
1491
|
+
\t\t\t\t(hide yes)
|
|
1492
|
+
\t\t\t)
|
|
1493
|
+
\t\t)
|
|
1494
|
+
`;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
if (info.partNumber) {
|
|
1498
|
+
props += `\t\t(property "Part" "${this.sanitizeText(info.partNumber)}"
|
|
1499
|
+
\t\t\t(at 0 0 0)
|
|
1500
|
+
\t\t\t(effects
|
|
1501
|
+
\t\t\t\t(font
|
|
1502
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1503
|
+
\t\t\t\t)
|
|
1504
|
+
\t\t\t\t(hide yes)
|
|
1505
|
+
\t\t\t)
|
|
1506
|
+
\t\t)
|
|
1507
|
+
`;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Component attributes as custom properties
|
|
1511
|
+
if (info.attributes) {
|
|
1512
|
+
for (const [key, value] of Object.entries(info.attributes)) {
|
|
1513
|
+
props += `\t\t(property "${this.sanitizeText(key)}" "${this.sanitizeText(String(value))}"
|
|
1514
|
+
\t\t\t(at 0 0 0)
|
|
1515
|
+
\t\t\t(effects
|
|
1516
|
+
\t\t\t\t(font
|
|
1517
|
+
\t\t\t\t\t(size ${ts} ${ts})
|
|
1518
|
+
\t\t\t\t)
|
|
1519
|
+
\t\t\t\t(hide yes)
|
|
1520
|
+
\t\t\t)
|
|
1521
|
+
\t\t)
|
|
1522
|
+
`;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
return props;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Generate graphics unit for template-based symbols
|
|
1531
|
+
*/
|
|
1532
|
+
private generateTemplateGraphics(name: string, template: SymbolTemplate): string {
|
|
1533
|
+
return `\t\t(symbol "${name}_0_1"
|
|
1534
|
+
${template.bodyGraphics}
|
|
1535
|
+
\t\t)
|
|
1536
|
+
`;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* Generate pins for template-based symbols (2-pin vertical layout)
|
|
1541
|
+
*/
|
|
1542
|
+
private generateTemplatePins(
|
|
1543
|
+
name: string,
|
|
1544
|
+
pins: EasyEDAPin[],
|
|
1545
|
+
template: SymbolTemplate
|
|
1546
|
+
): string {
|
|
1547
|
+
const ts = KICAD_DEFAULTS.TEXT_SIZE;
|
|
1548
|
+
const halfSpacing = template.pinSpacing / 2;
|
|
1549
|
+
|
|
1550
|
+
let output = `\t\t(symbol "${name}_1_1"
|
|
1551
|
+
`;
|
|
1552
|
+
|
|
1553
|
+
// For 2-pin components, place at fixed positions
|
|
1554
|
+
if (pins.length === 2) {
|
|
1555
|
+
// Pin 1 at top (270° points down toward body)
|
|
1556
|
+
const pin1 = pins[0];
|
|
1557
|
+
output += `\t\t\t(pin passive line
|
|
1558
|
+
\t\t\t\t(at 0 ${halfSpacing} 270)
|
|
1559
|
+
\t\t\t\t(length ${template.pinLength})
|
|
1560
|
+
\t\t\t\t(name "${this.sanitizePinName(pin1.name)}"
|
|
1561
|
+
\t\t\t\t\t(effects
|
|
1562
|
+
\t\t\t\t\t\t(font
|
|
1563
|
+
\t\t\t\t\t\t\t(size ${ts} ${ts})
|
|
1564
|
+
\t\t\t\t\t\t)
|
|
1565
|
+
\t\t\t\t\t)
|
|
1566
|
+
\t\t\t\t)
|
|
1567
|
+
\t\t\t\t(number "${pin1.number}"
|
|
1568
|
+
\t\t\t\t\t(effects
|
|
1569
|
+
\t\t\t\t\t\t(font
|
|
1570
|
+
\t\t\t\t\t\t\t(size ${ts} ${ts})
|
|
1571
|
+
\t\t\t\t\t\t)
|
|
1572
|
+
\t\t\t\t\t)
|
|
1573
|
+
\t\t\t\t)
|
|
1574
|
+
\t\t\t)
|
|
1575
|
+
`;
|
|
1576
|
+
|
|
1577
|
+
// Pin 2 at bottom (90° points up toward body)
|
|
1578
|
+
const pin2 = pins[1];
|
|
1579
|
+
output += `\t\t\t(pin passive line
|
|
1580
|
+
\t\t\t\t(at 0 ${-halfSpacing} 90)
|
|
1581
|
+
\t\t\t\t(length ${template.pinLength})
|
|
1582
|
+
\t\t\t\t(name "${this.sanitizePinName(pin2.name)}"
|
|
1583
|
+
\t\t\t\t\t(effects
|
|
1584
|
+
\t\t\t\t\t\t(font
|
|
1585
|
+
\t\t\t\t\t\t\t(size ${ts} ${ts})
|
|
1586
|
+
\t\t\t\t\t\t)
|
|
1587
|
+
\t\t\t\t\t)
|
|
1588
|
+
\t\t\t\t)
|
|
1589
|
+
\t\t\t\t(number "${pin2.number}"
|
|
1590
|
+
\t\t\t\t\t(effects
|
|
1591
|
+
\t\t\t\t\t\t(font
|
|
1592
|
+
\t\t\t\t\t\t\t(size ${ts} ${ts})
|
|
1593
|
+
\t\t\t\t\t\t)
|
|
1594
|
+
\t\t\t\t\t)
|
|
1595
|
+
\t\t\t\t)
|
|
1596
|
+
\t\t\t)
|
|
1597
|
+
`;
|
|
1598
|
+
} else {
|
|
1599
|
+
// For multi-pin components (e.g., LED with 3 pins), fall back to EasyEDA positions
|
|
1600
|
+
for (const pin of pins) {
|
|
1601
|
+
const pinType = this.mapPinType(pin.electricalType);
|
|
1602
|
+
output += `\t\t\t(pin ${pinType} line
|
|
1603
|
+
\t\t\t\t(at 0 0 0)
|
|
1604
|
+
\t\t\t\t(length ${template.pinLength})
|
|
1605
|
+
\t\t\t\t(name "${this.sanitizePinName(pin.name)}"
|
|
1606
|
+
\t\t\t\t\t(effects
|
|
1607
|
+
\t\t\t\t\t\t(font
|
|
1608
|
+
\t\t\t\t\t\t\t(size ${ts} ${ts})
|
|
1609
|
+
\t\t\t\t\t\t)
|
|
1610
|
+
\t\t\t\t\t)
|
|
1611
|
+
\t\t\t\t)
|
|
1612
|
+
\t\t\t\t(number "${pin.number}"
|
|
1613
|
+
\t\t\t\t\t(effects
|
|
1614
|
+
\t\t\t\t\t\t(font
|
|
1615
|
+
\t\t\t\t\t\t\t(size ${ts} ${ts})
|
|
1616
|
+
\t\t\t\t\t\t)
|
|
1617
|
+
\t\t\t\t\t)
|
|
1618
|
+
\t\t\t\t)
|
|
1619
|
+
\t\t\t)
|
|
1620
|
+
`;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
output += `\t\t)
|
|
1625
|
+
`;
|
|
1626
|
+
return output;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* Map EasyEDA pin type to KiCad pin type
|
|
1631
|
+
*/
|
|
1632
|
+
private mapPinType(eeType: string): string {
|
|
1633
|
+
const mapping: Record<string, string> = {
|
|
1634
|
+
'0': 'unspecified',
|
|
1635
|
+
'1': 'input',
|
|
1636
|
+
'2': 'output',
|
|
1637
|
+
'3': 'bidirectional',
|
|
1638
|
+
'4': 'power_in',
|
|
1639
|
+
'5': 'power_out',
|
|
1640
|
+
'6': 'open_collector',
|
|
1641
|
+
'7': 'open_emitter',
|
|
1642
|
+
'8': 'passive',
|
|
1643
|
+
'9': 'no_connect',
|
|
1644
|
+
};
|
|
1645
|
+
return mapping[eeType] || 'passive';
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* Calculate pin rotation based on position (points inward toward center)
|
|
1650
|
+
*/
|
|
1651
|
+
private calculatePinRotation(x: number, y: number): number {
|
|
1652
|
+
if (Math.abs(x) > Math.abs(y)) {
|
|
1653
|
+
return x > 0 ? 180 : 0;
|
|
1654
|
+
} else {
|
|
1655
|
+
return y > 0 ? 270 : 90;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/**
|
|
1660
|
+
* Sanitize component name for KiCad (no special chars except underscore/hyphen)
|
|
1661
|
+
*/
|
|
1662
|
+
private sanitizeName(name: string): string {
|
|
1663
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/**
|
|
1667
|
+
* Sanitize pin name (escape quotes)
|
|
1668
|
+
*/
|
|
1669
|
+
private sanitizePinName(name: string): string {
|
|
1670
|
+
if (!name || name.trim() === '') return '~';
|
|
1671
|
+
return name.replace(/"/g, "'").replace(/\\/g, '');
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
/**
|
|
1675
|
+
* Sanitize general text for properties
|
|
1676
|
+
*/
|
|
1677
|
+
private sanitizeText(text: string): string {
|
|
1678
|
+
return text.replace(/"/g, "'").replace(/\\/g, '').replace(/\n/g, ' ');
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
export const symbolConverter = new SymbolConverter();
|