@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,949 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EasyEDA Footprint to KiCad Footprint Converter
|
|
3
|
+
* Complete rewrite to handle all EasyEDA shape types
|
|
4
|
+
*
|
|
5
|
+
* Supported shapes: PAD, TRACK, HOLE, CIRCLE, ARC, RECT, VIA, TEXT, SOLIDREGION
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
EasyEDAComponentData,
|
|
10
|
+
EasyEDAPad,
|
|
11
|
+
EasyEDATrack,
|
|
12
|
+
EasyEDAHole,
|
|
13
|
+
EasyEDACircle,
|
|
14
|
+
EasyEDAArc,
|
|
15
|
+
EasyEDARect,
|
|
16
|
+
EasyEDAVia,
|
|
17
|
+
EasyEDAText,
|
|
18
|
+
EasyEDASolidRegion,
|
|
19
|
+
} from '../types/index.js';
|
|
20
|
+
import { KICAD_FOOTPRINT_VERSION, KICAD_LAYERS } from '../constants/index.js';
|
|
21
|
+
import { roundTo } from '../utils/index.js';
|
|
22
|
+
import { mapToKicadFootprint, getKicadFootprintRef } from './footprint-mapper.js';
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Constants - EasyEDA to KiCad mappings from easyeda2kicad.py
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
// EasyEDA uses 10mil units (0.254mm per unit)
|
|
29
|
+
const EE_TO_MM = 10 * 0.0254; // = 0.254
|
|
30
|
+
|
|
31
|
+
// General layer mapping for graphics (TRACK, CIRCLE, ARC, RECT, TEXT)
|
|
32
|
+
const KI_LAYERS: Record<number, string> = {
|
|
33
|
+
1: 'F.Cu',
|
|
34
|
+
2: 'B.Cu',
|
|
35
|
+
3: 'F.SilkS',
|
|
36
|
+
4: 'B.SilkS',
|
|
37
|
+
5: 'F.Paste',
|
|
38
|
+
6: 'B.Paste',
|
|
39
|
+
7: 'F.Mask',
|
|
40
|
+
8: 'B.Mask',
|
|
41
|
+
10: 'Edge.Cuts',
|
|
42
|
+
11: 'Edge.Cuts',
|
|
43
|
+
12: 'Cmts.User',
|
|
44
|
+
13: 'F.Fab',
|
|
45
|
+
14: 'B.Fab',
|
|
46
|
+
15: 'Dwgs.User',
|
|
47
|
+
101: 'F.Fab',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Layer mapping for SMD pads (includes paste layer)
|
|
51
|
+
const KI_PAD_LAYER_SMD: Record<number, string> = {
|
|
52
|
+
1: '"F.Cu" "F.Paste" "F.Mask"',
|
|
53
|
+
2: '"B.Cu" "B.Paste" "B.Mask"',
|
|
54
|
+
11: '"*.Cu" "*.Paste" "*.Mask"',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Layer mapping for THT pads (no paste layer)
|
|
58
|
+
const KI_PAD_LAYER_THT: Record<number, string> = {
|
|
59
|
+
1: '"F.Cu" "F.Mask"',
|
|
60
|
+
2: '"B.Cu" "B.Mask"',
|
|
61
|
+
11: '"*.Cu" "*.Mask"',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Pad shape mapping
|
|
65
|
+
const KI_PAD_SHAPE: Record<string, string> = {
|
|
66
|
+
ELLIPSE: 'circle',
|
|
67
|
+
RECT: 'rect',
|
|
68
|
+
OVAL: 'oval',
|
|
69
|
+
POLYGON: 'custom',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// Types
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
export interface FootprintConversionOptions {
|
|
77
|
+
libraryName?: string;
|
|
78
|
+
include3DModel?: boolean;
|
|
79
|
+
modelPath?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface FootprintResult {
|
|
83
|
+
type: 'reference' | 'generated';
|
|
84
|
+
reference?: string;
|
|
85
|
+
content?: string;
|
|
86
|
+
name: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface Point {
|
|
90
|
+
x: number;
|
|
91
|
+
y: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface BoundingBox {
|
|
95
|
+
minX: number;
|
|
96
|
+
maxX: number;
|
|
97
|
+
minY: number;
|
|
98
|
+
maxY: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Calculate the geometric center of pads (in EasyEDA units, before conversion)
|
|
103
|
+
* Used to center the footprint at (0,0) in KiCad
|
|
104
|
+
*/
|
|
105
|
+
function calculatePadCenter(pads: EasyEDAPad[]): Point {
|
|
106
|
+
if (pads.length === 0) {
|
|
107
|
+
return { x: 0, y: 0 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let minX = Infinity,
|
|
111
|
+
maxX = -Infinity;
|
|
112
|
+
let minY = Infinity,
|
|
113
|
+
maxY = -Infinity;
|
|
114
|
+
|
|
115
|
+
for (const pad of pads) {
|
|
116
|
+
// Include pad size in bounds calculation
|
|
117
|
+
const hw = pad.width / 2;
|
|
118
|
+
const hh = pad.height / 2;
|
|
119
|
+
minX = Math.min(minX, pad.centerX - hw);
|
|
120
|
+
maxX = Math.max(maxX, pad.centerX + hw);
|
|
121
|
+
minY = Math.min(minY, pad.centerY - hh);
|
|
122
|
+
maxY = Math.max(maxY, pad.centerY + hh);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
x: (minX + maxX) / 2,
|
|
127
|
+
y: (minY + maxY) / 2,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// Helper functions
|
|
133
|
+
// =============================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Convert EasyEDA coordinate to mm (relative to origin)
|
|
137
|
+
*/
|
|
138
|
+
function toMM(value: number): number {
|
|
139
|
+
return value * EE_TO_MM;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Convert EasyEDA X coordinate (origin-relative, Y-flipped for KiCad)
|
|
144
|
+
*/
|
|
145
|
+
function convertX(x: number, originX: number): number {
|
|
146
|
+
return roundTo((x - originX) * EE_TO_MM, 4);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Convert EasyEDA Y coordinate (origin-relative)
|
|
151
|
+
* Note: KiCad footprints use same Y convention as EasyEDA (Y positive going down)
|
|
152
|
+
*/
|
|
153
|
+
function convertY(y: number, originY: number): number {
|
|
154
|
+
return roundTo((y - originY) * EE_TO_MM, 4);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse space-separated point string "x1 y1 x2 y2 ..." into Point array
|
|
159
|
+
*/
|
|
160
|
+
function parsePoints(pointsStr: string): Point[] {
|
|
161
|
+
const values = pointsStr.trim().split(/\s+/).map(Number);
|
|
162
|
+
const points: Point[] = [];
|
|
163
|
+
for (let i = 0; i < values.length - 1; i += 2) {
|
|
164
|
+
points.push({ x: values[i], y: values[i + 1] });
|
|
165
|
+
}
|
|
166
|
+
return points;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get KiCad layer name from EasyEDA layer ID
|
|
171
|
+
*/
|
|
172
|
+
function getLayer(layerId: number): string {
|
|
173
|
+
return KI_LAYERS[layerId] || 'F.SilkS';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get KiCad pad layers based on pad type and EasyEDA layer
|
|
178
|
+
*/
|
|
179
|
+
function getPadLayers(layerId: number, isSmd: boolean): string {
|
|
180
|
+
if (isSmd) {
|
|
181
|
+
return KI_PAD_LAYER_SMD[layerId] || '"F.Cu" "F.Paste" "F.Mask"';
|
|
182
|
+
}
|
|
183
|
+
return KI_PAD_LAYER_THT[layerId] || '"*.Cu" "*.Mask"';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Parse SVG arc path and extract arc parameters
|
|
188
|
+
* Format: "M x1 y1 A rx ry rotation large_arc sweep x2 y2"
|
|
189
|
+
*/
|
|
190
|
+
function parseSvgArcPath(
|
|
191
|
+
path: string,
|
|
192
|
+
originX: number,
|
|
193
|
+
originY: number
|
|
194
|
+
): { start: Point; end: Point; mid: Point } | null {
|
|
195
|
+
try {
|
|
196
|
+
// Match the SVG arc command pattern
|
|
197
|
+
const pathMatch = path.match(
|
|
198
|
+
/M\s*([\d.-]+)\s*([\d.-]+)\s*A\s*([\d.-]+)\s*([\d.-]+)\s*([\d.-]+)\s*(\d)\s*(\d)\s*([\d.-]+)\s*([\d.-]+)/i
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (!pathMatch) return null;
|
|
202
|
+
|
|
203
|
+
const [, x1, y1, rx, ry, rotation, largeArc, sweep, x2, y2] = pathMatch.map(Number);
|
|
204
|
+
|
|
205
|
+
// Convert to KiCad coordinates
|
|
206
|
+
const start: Point = {
|
|
207
|
+
x: convertX(x1, originX),
|
|
208
|
+
y: convertY(y1, originY),
|
|
209
|
+
};
|
|
210
|
+
const end: Point = {
|
|
211
|
+
x: convertX(x2, originX),
|
|
212
|
+
y: convertY(y2, originY),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Calculate midpoint on arc (simplified - uses center approximation)
|
|
216
|
+
const centerX = (x1 + x2) / 2;
|
|
217
|
+
const centerY = (y1 + y2) / 2;
|
|
218
|
+
|
|
219
|
+
// Offset midpoint perpendicular to chord based on arc direction
|
|
220
|
+
const chordLen = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
|
221
|
+
const sagitta = Math.min(rx, ry) * 0.5; // Approximation
|
|
222
|
+
|
|
223
|
+
// Normal vector to chord
|
|
224
|
+
const nx = -(y2 - y1) / chordLen;
|
|
225
|
+
const ny = (x2 - x1) / chordLen;
|
|
226
|
+
|
|
227
|
+
// Adjust direction based on sweep flag (0 = counter-clockwise, 1 = clockwise)
|
|
228
|
+
const direction = sweep === 1 ? 1 : -1;
|
|
229
|
+
|
|
230
|
+
const midX = centerX + nx * sagitta * direction;
|
|
231
|
+
const midY = centerY + ny * sagitta * direction;
|
|
232
|
+
|
|
233
|
+
const mid: Point = {
|
|
234
|
+
x: convertX(midX, originX),
|
|
235
|
+
y: convertY(midY, originY),
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
return { start, end, mid };
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// =============================================================================
|
|
245
|
+
// Footprint Converter Class
|
|
246
|
+
// =============================================================================
|
|
247
|
+
|
|
248
|
+
export class FootprintConverter {
|
|
249
|
+
/**
|
|
250
|
+
* Convert EasyEDA component data to KiCad footprint format string
|
|
251
|
+
*/
|
|
252
|
+
convert(component: EasyEDAComponentData, options: FootprintConversionOptions = {}): string {
|
|
253
|
+
const { info, footprint, model3d } = component;
|
|
254
|
+
const name = this.sanitizeName(footprint.name);
|
|
255
|
+
// Calculate geometric center from pads to center footprint at (0,0)
|
|
256
|
+
const origin = calculatePadCenter(footprint.pads);
|
|
257
|
+
const { include3DModel = false } = options;
|
|
258
|
+
|
|
259
|
+
// Calculate bounds first (needed for text positioning)
|
|
260
|
+
const bounds = this.calculateBounds(footprint, origin);
|
|
261
|
+
|
|
262
|
+
// Map footprint type for attr token
|
|
263
|
+
const attrType = footprint.type === 'tht' ? 'through_hole' : 'smd';
|
|
264
|
+
|
|
265
|
+
let output = `(footprint "${name}"
|
|
266
|
+
\t(version ${KICAD_FOOTPRINT_VERSION})
|
|
267
|
+
\t(generator "ai-eda-jlc-mcp")
|
|
268
|
+
\t(layer "${KICAD_LAYERS.F_CU}")
|
|
269
|
+
\t(descr "${this.escapeString(info.description || name)}")
|
|
270
|
+
\t(tags "${this.escapeString(info.category || 'component')}")
|
|
271
|
+
`;
|
|
272
|
+
|
|
273
|
+
// Add properties (with bounds for text positioning)
|
|
274
|
+
output += this.generateProperties(info, name, bounds);
|
|
275
|
+
|
|
276
|
+
// Add attributes
|
|
277
|
+
output += `\t(attr ${attrType})\n`;
|
|
278
|
+
|
|
279
|
+
// Generate all pads
|
|
280
|
+
for (const pad of footprint.pads) {
|
|
281
|
+
output += this.generatePad(pad, origin);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Generate HOLEs as NPTH pads
|
|
285
|
+
for (const hole of footprint.holes) {
|
|
286
|
+
output += this.generateHole(hole, origin);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Generate VIAs as through-hole pads (rare in footprints but possible)
|
|
290
|
+
for (const via of footprint.vias) {
|
|
291
|
+
output += this.generateVia(via, origin);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Generate TRACKs as fp_line (silkscreen, fab, etc.)
|
|
295
|
+
for (const track of footprint.tracks) {
|
|
296
|
+
output += this.generateTrack(track, origin);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Generate CIRCLEs as fp_circle
|
|
300
|
+
for (const circle of footprint.circles) {
|
|
301
|
+
output += this.generateCircle(circle, origin);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Generate ARCs as fp_arc
|
|
305
|
+
for (const arc of footprint.arcs) {
|
|
306
|
+
output += this.generateArc(arc, origin);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Generate RECTs as 4 fp_line elements
|
|
310
|
+
for (const rect of footprint.rects) {
|
|
311
|
+
output += this.generateRect(rect, origin);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Generate TEXT elements (not REF/VAL - those are in properties)
|
|
315
|
+
for (const text of footprint.texts) {
|
|
316
|
+
output += this.generateText(text, origin);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Generate SOLIDREGION elements as fp_poly
|
|
320
|
+
for (const solidRegion of footprint.solidRegions) {
|
|
321
|
+
output += this.generateSolidRegion(solidRegion, origin);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Add fab reference text
|
|
325
|
+
output += this.generateFabReference();
|
|
326
|
+
|
|
327
|
+
// Add courtyard
|
|
328
|
+
output += this.generateCourtyard(bounds);
|
|
329
|
+
|
|
330
|
+
// Add embedded_fonts declaration
|
|
331
|
+
output += `\t(embedded_fonts no)\n`;
|
|
332
|
+
|
|
333
|
+
// Add 3D model reference if available
|
|
334
|
+
if (include3DModel && model3d && options.modelPath) {
|
|
335
|
+
output += this.generate3DModel(options.modelPath);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
output += `)`;
|
|
339
|
+
|
|
340
|
+
return output;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get footprint using hybrid approach:
|
|
345
|
+
* - Use KiCad standard footprint if available (for common packages)
|
|
346
|
+
* - Generate custom footprint if no standard mapping exists
|
|
347
|
+
*/
|
|
348
|
+
getFootprint(
|
|
349
|
+
component: EasyEDAComponentData,
|
|
350
|
+
options: FootprintConversionOptions = {}
|
|
351
|
+
): FootprintResult {
|
|
352
|
+
const { info, footprint } = component;
|
|
353
|
+
const packageName = footprint.name;
|
|
354
|
+
const prefix = info.prefix;
|
|
355
|
+
|
|
356
|
+
// Try to map to KiCad standard footprint
|
|
357
|
+
const mapping = mapToKicadFootprint(packageName, prefix, info.category, info.description);
|
|
358
|
+
|
|
359
|
+
if (mapping) {
|
|
360
|
+
return {
|
|
361
|
+
type: 'reference',
|
|
362
|
+
reference: getKicadFootprintRef(mapping),
|
|
363
|
+
name: mapping.footprint,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Generate custom footprint from EasyEDA data
|
|
368
|
+
const content = this.convert(component, options);
|
|
369
|
+
const name = this.sanitizeName(footprint.name);
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
type: 'generated',
|
|
373
|
+
content,
|
|
374
|
+
name,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ===========================================================================
|
|
379
|
+
// Element generators
|
|
380
|
+
// ===========================================================================
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Generate PAD element
|
|
384
|
+
* Handles all shapes: RECT, ELLIPSE, OVAL, POLYGON
|
|
385
|
+
*/
|
|
386
|
+
private generatePad(pad: EasyEDAPad, origin: Point): string {
|
|
387
|
+
const x = convertX(pad.centerX, origin.x);
|
|
388
|
+
const y = convertY(pad.centerY, origin.y);
|
|
389
|
+
const w = roundTo(toMM(pad.width), 4);
|
|
390
|
+
const h = roundTo(toMM(pad.height), 4);
|
|
391
|
+
const rotation = pad.rotation || 0;
|
|
392
|
+
|
|
393
|
+
// Determine if SMD or THT based on hole radius
|
|
394
|
+
const isSmd = pad.holeRadius === 0;
|
|
395
|
+
const padType = isSmd ? 'smd' : 'thru_hole';
|
|
396
|
+
const layers = getPadLayers(pad.layerId, isSmd);
|
|
397
|
+
|
|
398
|
+
// Handle POLYGON (custom) pads
|
|
399
|
+
if (pad.shape === 'POLYGON' && pad.points) {
|
|
400
|
+
return this.generatePolygonPad(pad, origin, layers);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Map shape
|
|
404
|
+
const shape = KI_PAD_SHAPE[pad.shape] || 'rect';
|
|
405
|
+
const kicadShape = isSmd && shape === 'rect' ? 'roundrect' : shape;
|
|
406
|
+
|
|
407
|
+
let output = `\t(pad "${pad.number}" ${padType} ${kicadShape}\n`;
|
|
408
|
+
output += `\t\t(at ${x} ${y}${rotation !== 0 ? ` ${rotation}` : ''})\n`;
|
|
409
|
+
output += `\t\t(size ${w} ${h})\n`;
|
|
410
|
+
output += `\t\t(layers ${layers})\n`;
|
|
411
|
+
|
|
412
|
+
// Add roundrect ratio for SMD rect pads
|
|
413
|
+
if (kicadShape === 'roundrect') {
|
|
414
|
+
output += `\t\t(roundrect_rratio 0.25)\n`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Add drill for THT pads
|
|
418
|
+
if (!isSmd) {
|
|
419
|
+
const drillDiameter = roundTo(toMM(pad.holeRadius * 2), 4);
|
|
420
|
+
|
|
421
|
+
// Check for slot (oval hole)
|
|
422
|
+
if (pad.holeLength && pad.holeLength > 0) {
|
|
423
|
+
const holeW = drillDiameter;
|
|
424
|
+
const holeH = roundTo(toMM(pad.holeLength), 4);
|
|
425
|
+
output += `\t\t(drill oval ${holeW} ${holeH})\n`;
|
|
426
|
+
} else {
|
|
427
|
+
output += `\t\t(drill ${drillDiameter})\n`;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
output += `\t)\n`;
|
|
432
|
+
return output;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Generate custom POLYGON pad using gr_poly primitive
|
|
437
|
+
* Supports both SMD and through-hole polygon pads
|
|
438
|
+
*/
|
|
439
|
+
private generatePolygonPad(pad: EasyEDAPad, origin: Point, layers: string): string {
|
|
440
|
+
const x = convertX(pad.centerX, origin.x);
|
|
441
|
+
const y = convertY(pad.centerY, origin.y);
|
|
442
|
+
|
|
443
|
+
// Parse polygon points
|
|
444
|
+
const points = parsePoints(pad.points);
|
|
445
|
+
if (points.length < 3) {
|
|
446
|
+
// Fallback to rect if not enough points
|
|
447
|
+
return this.generatePad({ ...pad, shape: 'RECT', points: '' }, origin);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Determine if SMD or THT based on hole radius (same logic as generatePad)
|
|
451
|
+
const isSmd = pad.holeRadius === 0;
|
|
452
|
+
const padType = isSmd ? 'smd' : 'thru_hole';
|
|
453
|
+
|
|
454
|
+
// Convert points relative to pad center (no Y-flip - KiCad footprints use same Y convention)
|
|
455
|
+
const polyPoints = points.map((p) => ({
|
|
456
|
+
x: roundTo(toMM(p.x - pad.centerX), 2),
|
|
457
|
+
y: roundTo(toMM(p.y - pad.centerY), 2),
|
|
458
|
+
}));
|
|
459
|
+
|
|
460
|
+
// Custom/polygon pads - rotation handled by polygon points
|
|
461
|
+
let output = `\t(pad "${pad.number}" ${padType} custom\n`;
|
|
462
|
+
output += `\t\t(at ${x} ${y})\n`;
|
|
463
|
+
output += `\t\t(size 0.01 0.01)\n`;
|
|
464
|
+
|
|
465
|
+
// Add drill for through-hole pads
|
|
466
|
+
if (!isSmd) {
|
|
467
|
+
const drillDiameter = roundTo(toMM(pad.holeRadius * 2), 4);
|
|
468
|
+
if (pad.holeLength && pad.holeLength > 0) {
|
|
469
|
+
// Slot/oval hole
|
|
470
|
+
const holeH = roundTo(toMM(pad.holeLength), 4);
|
|
471
|
+
output += `\t\t(drill oval ${drillDiameter} ${holeH})\n`;
|
|
472
|
+
} else {
|
|
473
|
+
output += `\t\t(drill ${drillDiameter})\n`;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
output += `\t\t(layers ${layers})\n`;
|
|
478
|
+
output += `\t\t(primitives\n`;
|
|
479
|
+
output += `\t\t\t(gr_poly\n`;
|
|
480
|
+
output += `\t\t\t\t(pts\n`;
|
|
481
|
+
|
|
482
|
+
for (const pt of polyPoints) {
|
|
483
|
+
output += `\t\t\t\t\t(xy ${pt.x} ${pt.y})\n`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
output += `\t\t\t\t)\n`;
|
|
487
|
+
output += `\t\t\t\t(width 0.1)\n`;
|
|
488
|
+
output += `\t\t\t)\n`;
|
|
489
|
+
output += `\t\t)\n`;
|
|
490
|
+
output += `\t)\n`;
|
|
491
|
+
|
|
492
|
+
return output;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Generate HOLE as NPTH pad
|
|
497
|
+
*/
|
|
498
|
+
private generateHole(hole: EasyEDAHole, origin: Point): string {
|
|
499
|
+
const x = convertX(hole.centerX, origin.x);
|
|
500
|
+
const y = convertY(hole.centerY, origin.y);
|
|
501
|
+
const diameter = roundTo(toMM(hole.radius * 2), 4);
|
|
502
|
+
|
|
503
|
+
return `\t(pad "" np_thru_hole circle
|
|
504
|
+
\t\t(at ${x} ${y})
|
|
505
|
+
\t\t(size ${diameter} ${diameter})
|
|
506
|
+
\t\t(drill ${diameter})
|
|
507
|
+
\t\t(layers "*.Cu" "*.Mask")
|
|
508
|
+
\t)\n`;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Generate VIA as through-hole pad (no number)
|
|
513
|
+
*/
|
|
514
|
+
private generateVia(via: EasyEDAVia, origin: Point): string {
|
|
515
|
+
const x = convertX(via.centerX, origin.x);
|
|
516
|
+
const y = convertY(via.centerY, origin.y);
|
|
517
|
+
const outerDiameter = roundTo(toMM(via.diameter), 4);
|
|
518
|
+
const drillDiameter = roundTo(toMM(via.radius * 2), 4);
|
|
519
|
+
|
|
520
|
+
return `\t(pad "" thru_hole circle
|
|
521
|
+
\t\t(at ${x} ${y})
|
|
522
|
+
\t\t(size ${outerDiameter} ${outerDiameter})
|
|
523
|
+
\t\t(drill ${drillDiameter})
|
|
524
|
+
\t\t(layers "*.Cu" "*.Mask")
|
|
525
|
+
\t)\n`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Generate TRACK as fp_line segments
|
|
530
|
+
*/
|
|
531
|
+
private generateTrack(track: EasyEDATrack, origin: Point): string {
|
|
532
|
+
const layer = getLayer(track.layerId);
|
|
533
|
+
const strokeWidth = roundTo(toMM(track.strokeWidth), 2);
|
|
534
|
+
const points = parsePoints(track.points);
|
|
535
|
+
|
|
536
|
+
if (points.length < 2) return '';
|
|
537
|
+
|
|
538
|
+
let output = '';
|
|
539
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
540
|
+
const x1 = convertX(points[i].x, origin.x);
|
|
541
|
+
const y1 = convertY(points[i].y, origin.y);
|
|
542
|
+
const x2 = convertX(points[i + 1].x, origin.x);
|
|
543
|
+
const y2 = convertY(points[i + 1].y, origin.y);
|
|
544
|
+
|
|
545
|
+
output += `\t(fp_line
|
|
546
|
+
\t\t(start ${x1} ${y1})
|
|
547
|
+
\t\t(end ${x2} ${y2})
|
|
548
|
+
\t\t(stroke
|
|
549
|
+
\t\t\t(width ${strokeWidth})
|
|
550
|
+
\t\t\t(type solid)
|
|
551
|
+
\t\t)
|
|
552
|
+
\t\t(layer "${layer}")
|
|
553
|
+
\t)\n`;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return output;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Generate CIRCLE as fp_circle
|
|
561
|
+
*/
|
|
562
|
+
private generateCircle(circle: EasyEDACircle, origin: Point): string {
|
|
563
|
+
const cx = convertX(circle.cx, origin.x);
|
|
564
|
+
const cy = convertY(circle.cy, origin.y);
|
|
565
|
+
const r = roundTo(toMM(circle.radius), 4);
|
|
566
|
+
const strokeWidth = roundTo(toMM(circle.strokeWidth), 2);
|
|
567
|
+
const layer = getLayer(circle.layerId);
|
|
568
|
+
|
|
569
|
+
// KiCad fp_circle uses center and end point (point on circumference)
|
|
570
|
+
const endX = roundTo(cx + r, 4);
|
|
571
|
+
|
|
572
|
+
return `\t(fp_circle
|
|
573
|
+
\t\t(center ${cx} ${cy})
|
|
574
|
+
\t\t(end ${endX} ${cy})
|
|
575
|
+
\t\t(stroke
|
|
576
|
+
\t\t\t(width ${strokeWidth})
|
|
577
|
+
\t\t\t(type solid)
|
|
578
|
+
\t\t)
|
|
579
|
+
\t\t(layer "${layer}")
|
|
580
|
+
\t)\n`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Generate ARC as fp_arc (from SVG path)
|
|
585
|
+
*/
|
|
586
|
+
private generateArc(arc: EasyEDAArc, origin: Point): string {
|
|
587
|
+
const layer = getLayer(arc.layerId);
|
|
588
|
+
const strokeWidth = roundTo(toMM(arc.strokeWidth), 2);
|
|
589
|
+
|
|
590
|
+
const arcData = parseSvgArcPath(arc.path, origin.x, origin.y);
|
|
591
|
+
if (!arcData) return '';
|
|
592
|
+
|
|
593
|
+
const { start, end, mid } = arcData;
|
|
594
|
+
|
|
595
|
+
return `\t(fp_arc
|
|
596
|
+
\t\t(start ${start.x} ${start.y})
|
|
597
|
+
\t\t(mid ${mid.x} ${mid.y})
|
|
598
|
+
\t\t(end ${end.x} ${end.y})
|
|
599
|
+
\t\t(stroke
|
|
600
|
+
\t\t\t(width ${strokeWidth})
|
|
601
|
+
\t\t\t(type solid)
|
|
602
|
+
\t\t)
|
|
603
|
+
\t\t(layer "${layer}")
|
|
604
|
+
\t)\n`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Generate RECT as 4 fp_line elements
|
|
609
|
+
*/
|
|
610
|
+
private generateRect(rect: EasyEDARect, origin: Point): string {
|
|
611
|
+
const layer = getLayer(rect.layerId);
|
|
612
|
+
const strokeWidth = roundTo(toMM(rect.strokeWidth), 2);
|
|
613
|
+
|
|
614
|
+
const x1 = convertX(rect.x, origin.x);
|
|
615
|
+
const y1 = convertY(rect.y, origin.y);
|
|
616
|
+
const x2 = convertX(rect.x + rect.width, origin.x);
|
|
617
|
+
const y2 = convertY(rect.y + rect.height, origin.y);
|
|
618
|
+
|
|
619
|
+
// Generate 4 lines for rectangle
|
|
620
|
+
const lines = [
|
|
621
|
+
{ start: [x1, y1], end: [x2, y1] }, // top
|
|
622
|
+
{ start: [x2, y1], end: [x2, y2] }, // right
|
|
623
|
+
{ start: [x2, y2], end: [x1, y2] }, // bottom
|
|
624
|
+
{ start: [x1, y2], end: [x1, y1] }, // left
|
|
625
|
+
];
|
|
626
|
+
|
|
627
|
+
let output = '';
|
|
628
|
+
for (const line of lines) {
|
|
629
|
+
output += `\t(fp_line
|
|
630
|
+
\t\t(start ${line.start[0]} ${line.start[1]})
|
|
631
|
+
\t\t(end ${line.end[0]} ${line.end[1]})
|
|
632
|
+
\t\t(stroke
|
|
633
|
+
\t\t\t(width ${strokeWidth})
|
|
634
|
+
\t\t\t(type solid)
|
|
635
|
+
\t\t)
|
|
636
|
+
\t\t(layer "${layer}")
|
|
637
|
+
\t)\n`;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return output;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Generate TEXT as fp_text (user text, not REF/VAL)
|
|
645
|
+
*/
|
|
646
|
+
private generateText(text: EasyEDAText, origin: Point): string {
|
|
647
|
+
// Skip if not displayed or is reference/value placeholder
|
|
648
|
+
if (!text.isDisplayed) return '';
|
|
649
|
+
if (text.type === 'N' || text.type === 'P') return ''; // Netname or prefix
|
|
650
|
+
|
|
651
|
+
const x = convertX(text.centerX, origin.x);
|
|
652
|
+
const y = convertY(text.centerY, origin.y);
|
|
653
|
+
const layer = getLayer(text.layerId);
|
|
654
|
+
const fontSize = roundTo(toMM(text.fontSize), 2);
|
|
655
|
+
const rotation = text.rotation || 0;
|
|
656
|
+
|
|
657
|
+
// Determine text justification based on position relative to origin
|
|
658
|
+
// EasyEDA text coordinates are anchor points, not center points
|
|
659
|
+
// Left side text should extend right (justify left)
|
|
660
|
+
// Right side text should extend left (justify right)
|
|
661
|
+
let justify = '';
|
|
662
|
+
if (x < -0.5) {
|
|
663
|
+
justify = 'left'; // Text on left side, anchor at left edge
|
|
664
|
+
} else if (x > 0.5) {
|
|
665
|
+
justify = 'right'; // Text on right side, anchor at right edge
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return `\t(fp_text user "${this.escapeString(text.text)}"
|
|
669
|
+
\t\t(at ${x} ${y}${rotation !== 0 ? ` ${rotation}` : ''})
|
|
670
|
+
\t\t(layer "${layer}")
|
|
671
|
+
\t\t(effects
|
|
672
|
+
\t\t\t(font
|
|
673
|
+
\t\t\t\t(size ${fontSize} ${fontSize})
|
|
674
|
+
\t\t\t\t(thickness ${roundTo(fontSize * 0.15, 2)})
|
|
675
|
+
\t\t\t)
|
|
676
|
+
${justify ? `\t\t\t(justify ${justify})\n` : ''}\t\t)
|
|
677
|
+
\t)\n`;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Generate SOLIDREGION as fp_poly (filled polygon)
|
|
682
|
+
* Parses SVG path with M/L/Z commands and converts to KiCad polygon
|
|
683
|
+
*/
|
|
684
|
+
private generateSolidRegion(region: EasyEDASolidRegion, origin: Point): string {
|
|
685
|
+
const layer = getLayer(region.layerId);
|
|
686
|
+
const points = this.parseSvgPathToPoints(region.path, origin);
|
|
687
|
+
|
|
688
|
+
if (points.length < 3) return '';
|
|
689
|
+
|
|
690
|
+
let output = `\t(fp_poly\n`;
|
|
691
|
+
output += `\t\t(pts\n`;
|
|
692
|
+
|
|
693
|
+
for (const pt of points) {
|
|
694
|
+
output += `\t\t\t(xy ${pt.x} ${pt.y})\n`;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
output += `\t\t)\n`;
|
|
698
|
+
output += `\t\t(stroke\n`;
|
|
699
|
+
output += `\t\t\t(width 0)\n`;
|
|
700
|
+
output += `\t\t\t(type solid)\n`;
|
|
701
|
+
output += `\t\t)\n`;
|
|
702
|
+
output += `\t\t(fill solid)\n`;
|
|
703
|
+
output += `\t\t(layer "${layer}")\n`;
|
|
704
|
+
output += `\t)\n`;
|
|
705
|
+
|
|
706
|
+
return output;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Parse SVG path string (M/L/Z commands) to array of points
|
|
711
|
+
* Format: "M x1,y1 L x2,y2 L x3,y3 Z" or "M x1,y1 L x2,y2 ..."
|
|
712
|
+
*/
|
|
713
|
+
private parseSvgPathToPoints(path: string, origin: Point): Point[] {
|
|
714
|
+
const points: Point[] = [];
|
|
715
|
+
|
|
716
|
+
// Match M and L commands with coordinates
|
|
717
|
+
// Supports both "M425,230" and "M 425 230" formats
|
|
718
|
+
const commandRegex = /([ML])\s*([\d.-]+)[,\s]+([\d.-]+)/gi;
|
|
719
|
+
let match;
|
|
720
|
+
|
|
721
|
+
while ((match = commandRegex.exec(path)) !== null) {
|
|
722
|
+
const x = parseFloat(match[2]);
|
|
723
|
+
const y = parseFloat(match[3]);
|
|
724
|
+
|
|
725
|
+
points.push({
|
|
726
|
+
x: convertX(x, origin.x),
|
|
727
|
+
y: convertY(y, origin.y),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return points;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ===========================================================================
|
|
735
|
+
// Property and outline generation
|
|
736
|
+
// ===========================================================================
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Calculate bounding box from all footprint elements
|
|
740
|
+
*/
|
|
741
|
+
private calculateBounds(
|
|
742
|
+
footprint: EasyEDAComponentData['footprint'],
|
|
743
|
+
origin: Point
|
|
744
|
+
): BoundingBox {
|
|
745
|
+
let minX = Infinity,
|
|
746
|
+
maxX = -Infinity;
|
|
747
|
+
let minY = Infinity,
|
|
748
|
+
maxY = -Infinity;
|
|
749
|
+
|
|
750
|
+
const updateBounds = (x: number, y: number, margin = 0) => {
|
|
751
|
+
minX = Math.min(minX, x - margin);
|
|
752
|
+
maxX = Math.max(maxX, x + margin);
|
|
753
|
+
minY = Math.min(minY, y - margin);
|
|
754
|
+
maxY = Math.max(maxY, y + margin);
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// Include pads
|
|
758
|
+
for (const pad of footprint.pads) {
|
|
759
|
+
const x = convertX(pad.centerX, origin.x);
|
|
760
|
+
const y = convertY(pad.centerY, origin.y);
|
|
761
|
+
const hw = toMM(pad.width) / 2;
|
|
762
|
+
const hh = toMM(pad.height) / 2;
|
|
763
|
+
updateBounds(x, y, Math.max(hw, hh));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Include holes
|
|
767
|
+
for (const hole of footprint.holes) {
|
|
768
|
+
const x = convertX(hole.centerX, origin.x);
|
|
769
|
+
const y = convertY(hole.centerY, origin.y);
|
|
770
|
+
const r = toMM(hole.radius);
|
|
771
|
+
updateBounds(x, y, r);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Include tracks
|
|
775
|
+
for (const track of footprint.tracks) {
|
|
776
|
+
const points = parsePoints(track.points);
|
|
777
|
+
for (const pt of points) {
|
|
778
|
+
const x = convertX(pt.x, origin.x);
|
|
779
|
+
const y = convertY(pt.y, origin.y);
|
|
780
|
+
updateBounds(x, y);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Include circles
|
|
785
|
+
for (const circle of footprint.circles) {
|
|
786
|
+
const x = convertX(circle.cx, origin.x);
|
|
787
|
+
const y = convertY(circle.cy, origin.y);
|
|
788
|
+
const r = toMM(circle.radius);
|
|
789
|
+
updateBounds(x, y, r);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Handle empty case
|
|
793
|
+
if (!isFinite(minX)) {
|
|
794
|
+
return { minX: -1, maxX: 1, minY: -1, maxY: 1 };
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return { minX, maxX, minY, maxY };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Generate footprint properties
|
|
802
|
+
* Positions Reference above courtyard and Value below courtyard
|
|
803
|
+
*/
|
|
804
|
+
private generateProperties(info: EasyEDAComponentData['info'], name: string, bounds: BoundingBox): string {
|
|
805
|
+
let props = '';
|
|
806
|
+
|
|
807
|
+
// Calculate text positions based on courtyard bounds
|
|
808
|
+
// Add margin for courtyard (0.25) + text offset (1.5)
|
|
809
|
+
const textOffset = 1.75;
|
|
810
|
+
const refY = roundTo(bounds.minY - textOffset, 2);
|
|
811
|
+
const valY = roundTo(bounds.maxY + textOffset, 2);
|
|
812
|
+
|
|
813
|
+
// Reference (required, visible on silkscreen) - above courtyard
|
|
814
|
+
props += `\t(property "Reference" "REF**"
|
|
815
|
+
\t\t(at 0 ${refY} 0)
|
|
816
|
+
\t\t(layer "${KICAD_LAYERS.F_SILKS}")
|
|
817
|
+
\t\t(effects
|
|
818
|
+
\t\t\t(font
|
|
819
|
+
\t\t\t\t(size 1 1)
|
|
820
|
+
\t\t\t\t(thickness 0.15)
|
|
821
|
+
\t\t\t)
|
|
822
|
+
\t\t)
|
|
823
|
+
\t)\n`;
|
|
824
|
+
|
|
825
|
+
// Value (required, visible on fab layer) - below courtyard
|
|
826
|
+
props += `\t(property "Value" "${this.escapeString(this.sanitizeName(info.name))}"
|
|
827
|
+
\t\t(at 0 ${valY} 0)
|
|
828
|
+
\t\t(layer "${KICAD_LAYERS.F_FAB}")
|
|
829
|
+
\t\t(effects
|
|
830
|
+
\t\t\t(font
|
|
831
|
+
\t\t\t\t(size 1 1)
|
|
832
|
+
\t\t\t\t(thickness 0.15)
|
|
833
|
+
\t\t\t)
|
|
834
|
+
\t\t)
|
|
835
|
+
\t)\n`;
|
|
836
|
+
|
|
837
|
+
// Hidden properties
|
|
838
|
+
const hiddenProps: Array<{ key: string; value: string | undefined }> = [
|
|
839
|
+
{ key: 'Description', value: info.description },
|
|
840
|
+
{ key: 'LCSC', value: info.lcscId },
|
|
841
|
+
{ key: 'Manufacturer', value: info.manufacturer },
|
|
842
|
+
];
|
|
843
|
+
|
|
844
|
+
// Add component attributes
|
|
845
|
+
if (info.attributes) {
|
|
846
|
+
for (const [key, value] of Object.entries(info.attributes)) {
|
|
847
|
+
hiddenProps.push({ key, value: String(value) });
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
for (const { key, value } of hiddenProps) {
|
|
852
|
+
if (value) {
|
|
853
|
+
props += `\t(property "${this.escapeString(key)}" "${this.escapeString(value)}"
|
|
854
|
+
\t\t(at 0 0 0)
|
|
855
|
+
\t\t(layer "${KICAD_LAYERS.F_FAB}")
|
|
856
|
+
\t\thide
|
|
857
|
+
\t\t(effects
|
|
858
|
+
\t\t\t(font
|
|
859
|
+
\t\t\t\t(size 1.27 1.27)
|
|
860
|
+
\t\t\t\t(thickness 0.15)
|
|
861
|
+
\t\t\t)
|
|
862
|
+
\t\t)
|
|
863
|
+
\t)\n`;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return props;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Generate fab reference text
|
|
872
|
+
*/
|
|
873
|
+
private generateFabReference(): string {
|
|
874
|
+
return `\t(fp_text user "\${REFERENCE}"
|
|
875
|
+
\t\t(at 0 0 0)
|
|
876
|
+
\t\t(layer "${KICAD_LAYERS.F_FAB}")
|
|
877
|
+
\t\t(effects
|
|
878
|
+
\t\t\t(font
|
|
879
|
+
\t\t\t\t(size 0.5 0.5)
|
|
880
|
+
\t\t\t\t(thickness 0.08)
|
|
881
|
+
\t\t\t)
|
|
882
|
+
\t\t)
|
|
883
|
+
\t)\n`;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Generate courtyard outline
|
|
888
|
+
*/
|
|
889
|
+
private generateCourtyard(bounds: BoundingBox): string {
|
|
890
|
+
const margin = 0.25;
|
|
891
|
+
const minX = roundTo(bounds.minX - margin, 2);
|
|
892
|
+
const maxX = roundTo(bounds.maxX + margin, 2);
|
|
893
|
+
const minY = roundTo(bounds.minY - margin, 2);
|
|
894
|
+
const maxY = roundTo(bounds.maxY + margin, 2);
|
|
895
|
+
|
|
896
|
+
const lines = [
|
|
897
|
+
{ start: [minX, minY], end: [maxX, minY] },
|
|
898
|
+
{ start: [maxX, minY], end: [maxX, maxY] },
|
|
899
|
+
{ start: [maxX, maxY], end: [minX, maxY] },
|
|
900
|
+
{ start: [minX, maxY], end: [minX, minY] },
|
|
901
|
+
];
|
|
902
|
+
|
|
903
|
+
let output = '';
|
|
904
|
+
for (const line of lines) {
|
|
905
|
+
output += `\t(fp_line
|
|
906
|
+
\t\t(start ${line.start[0]} ${line.start[1]})
|
|
907
|
+
\t\t(end ${line.end[0]} ${line.end[1]})
|
|
908
|
+
\t\t(stroke
|
|
909
|
+
\t\t\t(width 0.05)
|
|
910
|
+
\t\t\t(type solid)
|
|
911
|
+
\t\t)
|
|
912
|
+
\t\t(layer "${KICAD_LAYERS.F_CRTYD}")
|
|
913
|
+
\t)\n`;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return output;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Generate 3D model reference
|
|
921
|
+
*/
|
|
922
|
+
private generate3DModel(modelPath: string): string {
|
|
923
|
+
return `\t(model "${modelPath}"
|
|
924
|
+
\t\t(offset
|
|
925
|
+
\t\t\t(xyz 0 0 0)
|
|
926
|
+
\t\t)
|
|
927
|
+
\t\t(scale
|
|
928
|
+
\t\t\t(xyz 1 1 1)
|
|
929
|
+
\t\t)
|
|
930
|
+
\t\t(rotate
|
|
931
|
+
\t\t\t(xyz 0 0 0)
|
|
932
|
+
\t\t)
|
|
933
|
+
\t)\n`;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// ===========================================================================
|
|
937
|
+
// Utility methods
|
|
938
|
+
// ===========================================================================
|
|
939
|
+
|
|
940
|
+
private sanitizeName(name: string): string {
|
|
941
|
+
return name.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
private escapeString(str: string): string {
|
|
945
|
+
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export const footprintConverter = new FootprintConverter();
|