diff --git a/src/board.spec.ts b/src/board.spec.ts index 8da9044..1e29060 100644 --- a/src/board.spec.ts +++ b/src/board.spec.ts @@ -1,4 +1,4 @@ -import { convertShape } from './board'; +import { convertBoardToArray, convertShape } from './board'; import { encodeObject, ISpectraList } from './spectra'; function removeNulls(a: ISpectraList): ISpectraList { @@ -25,10 +25,14 @@ function normalize(obj: ISpectraList) { return round(removeNulls(obj)); } +function conversionState(nets: string[] = []) { + return { nets, innerLayers: 0 }; +} + describe('convertTrack', () => { it('should convert copper tracks into segments', () => { const input = 'TRACK~0.63~1~GND~4000 3000 4000 3030~gge606~0'; - expect(normalize(convertShape(input, ['', 'GND']))).toEqual([ + expect(normalize(convertShape(input, conversionState(['', 'GND'])))).toEqual([ [ 'segment', ['start', 0, 0], @@ -42,21 +46,21 @@ describe('convertTrack', () => { it(`should throw an error if the given layer number doesn't exist`, () => { const input = 'TRACK~0.63~999~GND~4000 3000 4000 3030~gge606~0'; - const fn = () => convertShape(input, ['', 'GND']); + const fn = () => convertShape(input, conversionState(['', 'GND'])); expect(fn).toThrow('Missing layer id: 999'); }); it('should convert non-copper layer tracks into gr_lines', () => { const input = 'TRACK~0.63~10~GND~4000 3000 4000 3030~gge606~0'; - expect(normalize(convertShape(input, ['']))).toEqual([ + expect(normalize(convertShape(input, conversionState()))).toEqual([ ['gr_line', ['start', 0, 0], ['end', 0, 7.62], ['width', 0.16], ['layer', 'Edge.Cuts']], ]); }); it('should add missing nets into the netlist (issue #29)', () => { const input = 'TRACK~0.63~1~5V~4000 3000 4000 3030~gge606~0'; - const netList = ['', 'GND']; - expect(normalize(convertShape(input, netList))).toEqual([ + const nets = ['', 'GND']; + expect(normalize(convertShape(input, conversionState(nets)))).toEqual([ [ 'segment', ['start', 0, 0], @@ -66,14 +70,28 @@ describe('convertTrack', () => { ['net', 2], ], ]); - expect(netList).toEqual(['', 'GND', '5V']); + expect(nets).toEqual(['', 'GND', '5V']); + }); + + it('should support inner layers (issue #33)', () => { + const input = 'TRACK~0.63~21~GND~4000 3000 4000 3030~gge606~0'; + expect(normalize(convertShape(input, conversionState(['', 'GND'])))).toEqual([ + [ + 'segment', + ['start', 0, 0], + ['end', 0, 7.62], + ['width', 0.16], + ['layer', 'In1.Cu'], + ['net', 1], + ], + ]); }); }); describe('convertPadToVia', () => { it('should correctly parse a PAD and convert it to a Via', () => { const input = 'PAD~ELLIPSE~4150~3071.5~6~6~11~GND~1~1.8~~0~gge196~0~~Y~0~~~4150,3071.5'; - expect(normalize(convertShape(input, [])[0])).toEqual([ + expect(normalize(convertShape(input, conversionState())[0])).toEqual([ 'via', ['at', 38.1, 18.161], ['size', 1.524], @@ -85,7 +103,7 @@ describe('convertPadToVia', () => { it('should wrap non ellipse pads in KiCad Footprint', () => { const input = 'PAD~RECT~4150~3071.5~6~6~11~VCC~1~1.8~~0~gge196~0~~Y~0~~~4150,3071.5'; - expect(normalize(convertShape(input, ['', 'VCC'])[0])).toEqual([ + expect(normalize(convertShape(input, conversionState(['', 'VCC']))[0])).toEqual([ 'module', 'AutoGenerated:Pad_1.52mm', ['layer', 'F.Cu'], @@ -111,14 +129,14 @@ describe('convertPadToVia', () => { describe('convertArc', () => { it('should convert arcs', () => { const input = 'ARC~1~10~~M4050,3060 A10,10 0 0 1 4060,3050~~gge276~0'; - expect(encodeObject(convertShape(input, [])[0])).toEqual( + expect(encodeObject(convertShape(input, conversionState())[0])).toEqual( '(gr_arc (start 15.24 15.24) (end 12.7 15.24) (angle 90) (width 0.254) (layer "Edge.Cuts"))' ); }); it('should parse different path formats', () => { const input = 'ARC~1~10~~M4000 3000A10 10 0 0 1 4050 3050~~gge170~0'; - expect(encodeObject(convertShape(input, [])[0])).toEqual( + expect(encodeObject(convertShape(input, conversionState())[0])).toEqual( '(gr_arc (start 6.35 6.35) (end 0 0) (angle 180) (width 0.254) (layer "Edge.Cuts"))' ); }); @@ -126,14 +144,14 @@ describe('convertArc', () => { it('should support negative numbers in arc path', () => { const input = 'ARC~0.6~4~~M 3977.3789 3026.2151 A 28.4253 28.4253 -150 1 1 3977.6376 3026.643~~gge66~0'; - expect(encodeObject(convertShape(input, [])[0])).toEqual( + expect(encodeObject(convertShape(input, conversionState())[0])).toEqual( '(gr_arc (start 0.465 2.978) (end -5.746 6.659) (angle 358.992) (width 0.152) (layer "B.SilkS"))' ); }); it('should correctly determine the arc start and end point (issue #16)', () => { const input = 'ARC~1~1~S$9~M4262.5,3279.5 A33.5596,33.5596 0 0 0 4245.5921,3315.5816~~gge8~0'; - expect(encodeObject(convertShape(input, [])[0])).toEqual( + expect(encodeObject(convertShape(input, conversionState())[0])).toEqual( '(gr_arc (start 70.739 78.486) (end 62.38 80.158) (angle 72.836) (width 0.254) (layer "F.Cu"))' ); }); @@ -143,7 +161,7 @@ describe('convertCopperArea', () => { it('should correctly parse the given SVG path', () => { const input = 'COPPERAREA~1~2~GND~M 4050 3050 L 4164 3050 L 4160 3120 L4050,3100 Z~1~solid~gge221~spoke~none~~0~~2~1~1~0~yes'; - expect(normalize(convertShape(input, ['', 'GND'])[0])).toEqual([ + expect(normalize(convertShape(input, conversionState(['', 'GND']))[0])).toEqual([ 'zone', ['net', 1], ['net_name', 'GND'], @@ -162,7 +180,7 @@ describe('convertSolidRegion', () => { it('should correctly parse the given SVG path', () => { const input = 'SOLIDREGION~2~L3_2~M 4280 3173 L 4280 3127.5 L 4358.5 3128 L 4358.5 3163.625 L 4371.5 3163.625 L 4374.5 3168.625 L 4374.5 3173.125 L 4369 3173.125 L 4358.5 3173.125 L 4358.5 3179.625 L 4406.5 3179.625 L 4459 3179.5 L 4459 3252.5 L 4280.5 3253 L 4280 3173 Z~cutout~gge40~0'; - expect(normalize(convertShape(input, ['L3_2'])[0])).toEqual([ + expect(normalize(convertShape(input, conversionState(['L3_2']))[0])).toEqual([ 'zone', ['net', 0], ['net_name', ''], @@ -196,14 +214,14 @@ describe('convertSolidRegion', () => { it('should ignore solid regions with circles (issue #12)', () => { const input = 'SOLIDREGION~1~~M 4367 3248 A 33.8 33.8 0 1 0 4366.99 3248 Z ~cutout~gge1953~~~~0'; - expect(normalize(convertShape(input, []))).toEqual([]); + expect(normalize(convertShape(input, conversionState()))).toEqual([]); }); }); describe('convertHole()', () => { it('should convert HOLE into KiCad footprint', () => { const input = 'HOLE~4475.5~3170.5~2.9528~gge1205~1'; - expect(normalize(convertShape(input, [])[0])).toEqual([ + expect(normalize(convertShape(input, conversionState())[0])).toEqual([ 'module', 'AutoGenerated:MountingHole_1.50mm', 'locked', @@ -229,7 +247,7 @@ describe('convertHole()', () => { describe('convert circle', () => { it('should correctly determine the end point according to radius', () => { const input = 'CIRCLE~4000~3000~12.4~1~3~gge635~0~'; - expect(normalize(convertShape(input, [])[0])).toEqual([ + expect(normalize(convertShape(input, conversionState())[0])).toEqual([ 'gr_circle', ['center', 0, 0], ['end', 3.15, 0], @@ -243,7 +261,7 @@ describe('convertLib()', () => { it('should include the footprint name in the exported module', () => { const input = 'LIB~4228~3187.5~package`1206`~270~~gge12~2~a8f323e85d754372811837f27f204a01~1564555550~0'; - expect(normalize(convertShape(input, [])[0])).toEqual([ + expect(normalize(convertShape(input, conversionState())[0])).toEqual([ 'module', 'easyeda:1206', ['layer', 'F.Cu'], @@ -265,7 +283,7 @@ describe('convertLib()', () => { const pad = '#@$PAD~ELLIPSE~4010~3029~4~4~11~SEG1C~4~1.5~~270~gge181~0~~Y~0~0~0.4~4010.05,3029.95'; const input = lib + pad; - expect(normalize(convertShape(input, [])[0])).toEqual([ + expect(normalize(convertShape(input, conversionState())[0])).toEqual([ 'module', 'easyeda:1206', ['layer', 'F.Cu'], @@ -297,7 +315,7 @@ describe('convertLib()', () => { const text = '#@$TEXT~N~4363~3153~0.6~90~~3~~4.5~0.5pF~M 4359.51 3158.63 L 4359.71 3159.25~none~gge188~~0~'; const input = lib + text; - expect(normalize(convertShape(input, [])[0])).toEqual([ + expect(normalize(convertShape(input, conversionState())[0])).toEqual([ 'module', 'easyeda:1206', ['layer', 'F.Cu'], @@ -326,7 +344,7 @@ describe('convertLib()', () => { const input = 'LIB~4177~3107~package`0402`3DModel`R_0603_1608Metric`~90~~gge464~1~405bb71866794ab59459d3b2854a4d33~1541687137~0~#@$TEXT~P~4173.31~3108.77~0.5~90~0~3~~2.4~R2~M 4172.0001 3108.77 L 4174.2901 3108.77 M 4172.0001 3108.77 L 4172.0001 3107.79 L 4172.1101 3107.46 L 4172.2201 3107.35 L 4172.4401 3107.24 L 4172.6601 3107.24 L 4172.8701 3107.35 L 4172.9801 3107.46 L 4173.0901 3107.79 L 4173.0901 3108.77 M 4173.0901 3108.01 L 4174.2901 3107.24 M 4172.5501 3106.41 L 4172.4401 3106.41 L 4172.2201 3106.3 L 4172.1101 3106.2 L 4172.0001 3105.98 L 4172.0001 3105.54 L 4172.1101 3105.32 L 4172.2201 3105.21 L 4172.4401 3105.1 L 4172.6601 3105.1 L 4172.8701 3105.21 L 4173.2001 3105.43 L 4174.2901 3106.52 L 4174.2901 3105~~gge467~~0~#@$TEXT~N~4160~3102.72~0.5~90~0~3~~4.5~2K2~M 4158.57 3102.52 L 4158.36 3102.52 L 4157.95 3102.31 L 4157.75 3102.11 L 4157.55 3101.7 L 4157.55 3100.88 L 4157.75 3100.47 L 4157.95 3100.27 L 4158.36 3100.06 L 4158.77 3100.06 L 4159.18 3100.27 L 4159.8 3100.67 L 4161.84 3102.72 L 4161.84 3099.86 M 4157.55 3098.51 L 4161.84 3098.51 M 4157.55 3095.64 L 4160.41 3098.51 M 4159.39 3097.48 L 4161.84 3095.64 M 4158.57 3094.09 L 4158.36 3094.09 L 4157.95 3093.88 L 4157.75 3093.68 L 4157.55 3093.27 L 4157.55 3092.45 L 4157.75 3092.04 L 4157.95 3091.84 L 4158.36 3091.63 L 4158.77 3091.63 L 4159.18 3091.84 L 4159.8 3092.25 L 4161.84 3094.29 L 4161.84 3091.43~none~gge468~~0~#@$PAD~RECT~4177~3108.67~2.362~2.559~1~SWCLK~1~0~4175.72 3109.85 4175.72 3107.49 4178.28 3107.49 4178.28 3109.85~90~gge466~0~~Y~0~0~0.4~4177,3108.67#@$SVGNODE~{"gId":"gge464_outline","nodeName":"g","nodeType":1,"layerid":"19","attrs":{"c_width":"6.4","c_height":"3.1898","c_rotation":"0,0,90","z":"0","c_origin":"4177,3107.03","uuid":"14d29194d76d4abda3f419dd15e5ae1e","c_etype":"outline3D","id":"gge464_outline","title":"R_0603_1608Metric","layerid":"19","transform":"scale(10.1587) translate(-3765.8265, -2801.1817)","style":""},"childNodes":[{"gId":"gge464_outline_line0","nodeName":"polyline","nodeType":1,"attrs":{"fill":"none","id":"gge464_outline_line0","c_shapetype":"line","points":"4176.843 3107.345 4177.157 3107.345 4177.157 3107.343 4177.157 3107.341 4177.157 3107.338 4177.157 3107.335 4177.157 3107.331 4177.157 3107.327 4177.157 3107.245 4177.157 3107.241 4177.157 3107.237 4177.157 3107.234 4177.157 3107.231 4177.157 3107.229 4177.157 3107.227 4177.157 3106.833 4177.157 3106.831 4177.157 3106.829 4177.157 3106.826 4177.157 3106.823 4177.157 3106.819 4177.157 3106.815 4177.157 3106.733 4177.157 3106.729 4177.157 3106.725 4177.157 3106.722 4177.157 3106.719 4177.157 3106.717 4177.157 3106.715 4176.843 3106.715 4176.843 3106.717 4176.843 3106.719 4176.843 3106.722 4176.843 3106.725 4176.843 3106.729 4176.843 3106.733 4176.843 3106.815 4176.843 3106.819 4176.843 3106.823 4176.843 3106.826 4176.843 3106.829 4176.843 3106.831 4176.843 3106.833 4176.843 3107.227 4176.843 3107.229 4176.843 3107.231 4176.843 3107.234 4176.843 3107.237 4176.843 3107.241 4176.843 3107.245 4176.843 3107.327 4176.843 3107.331 4176.843 3107.335 4176.843 3107.338 4176.843 3107.341 4176.843 3107.343 4176.843 3107.345 4176.843 3107.345'; - expect(normalize(convertShape(input, ['', '+3V3', 'SWCLK'])[0])).toEqual([ + expect(normalize(convertShape(input, conversionState(['', '+3V3', 'SWCLK']))[0])).toEqual([ 'module', 'easyeda:0402', ['layer', 'F.Cu'], @@ -374,7 +392,7 @@ describe('convertLib()', () => { const input = 'LIB~4401~3164~package`IEC_HIGHVOLTAGE_SMALL`~~~gge846~1~~~0~#@$SOLIDREGION~3~~M 4400.3 3160.5 L 4401.8 3160.5 L 4399.1 3165.8 L 4402.9 3164.7 L 4400.9 3169.3 L 4401.7 3169.1 L 4400.1 3170.9 L 4399.8 3168.8 L 4400.3 3169.2 L 4401.3 3165.9 L 4397.6 3167.1 Z ~solid~gge849~~~~0'; - expect(normalize(convertShape(input, [''])[0])).toEqual([ + expect(normalize(convertShape(input, conversionState())[0])).toEqual([ 'module', 'easyeda:IEC_HIGHVOLTAGE_SMALL', ['layer', 'F.Cu'], @@ -413,7 +431,7 @@ describe('convertLib()', () => { const input = 'LIB~4401~3164~package`IEC_HIGHVOLTAGE_SMALL`~~~gge846~1~~~0~#@$#@$SOLIDREGION~3~~M 4513.5 3294 A 12.125 12.125 0 0 1 4495.5 3294 Z ~solid~gge636~~~~0'; - expect(normalize(convertShape(input, [''])[0])).toEqual([ + expect(normalize(convertShape(input, conversionState())[0])).toEqual([ 'module', 'easyeda:IEC_HIGHVOLTAGE_SMALL', ['layer', 'F.Cu'], @@ -432,7 +450,7 @@ describe('convertLib()', () => { it('should not respected the locked attribute (issue #23)', () => { const input = 'LIB~4050~3050~package`Test`~~~gge123~1~~~1~'; - expect(normalize(convertShape(input, [''])[0])).toEqual([ + expect(normalize(convertShape(input, conversionState())[0])).toEqual([ 'module', 'easyeda:Test', 'locked', @@ -453,7 +471,7 @@ describe('convertLib()', () => { const input = 'LIB~612.25~388.7~package`0603`value`1.00k~~~rep30~1~c25f29e5d54148509f1fe8ecc29bd248~1549637911~0~#@$PAD~POLYGON~613.999~396.939~3.9399~3.14~1~GND~1~0~612.03 398.51 612.03 395.37 615.97 398.51~90~rep28~0~~Y~0~0~0.4~613.999,396.939'; const nets = ['', 'GND', 'B-IN']; - expect(normalize(convertShape(input, nets)[0])).toEqual([ + expect(normalize(convertShape(input, conversionState(nets))[0])).toEqual([ 'module', 'easyeda:0603', ['layer', 'F.Cu'], @@ -492,7 +510,7 @@ describe('convertLib()', () => { const input = 'LIB~585.7~338.9~package`0603`value`1.00k~90~~gge35720~1~c25f29e5d54148509f1fe8ecc29bd248~1549637911~0~#@$PAD~POLYGON~593.939~338.901~0~0~1~SYNC-OUT~1~0~595.51 340.87 592.37 340.87 595.51 336.93~180~gge35721~0~~Y~0~0~0.4~593.939,338.901'; const nets = ['', 'GND', 'B-IN']; - expect(normalize(convertShape(input, nets)[0])).toEqual([ + expect(normalize(convertShape(input, conversionState(nets))[0])).toEqual([ 'module', 'easyeda:0603', ['layer', 'F.Cu'], @@ -531,7 +549,7 @@ describe('convertLib()', () => { const input = 'LIB~585.7~338.9~package`0603`value`1.00k~90~~gge35720~1~c25f29e5d54148509f1fe8ecc29bd248~1549637911~0~#@$PAD~POLYGON~593.939~338.901~0~0~1~SYNC-OUT~1~0~595.51 340.87 592.37 340.87 592.37 336.93 595.51 336.93~180~gge35721~0~~Y~0~0~0.4~593.939,338.901#@$PAD~POLYGON~586.459~338.901~3.15~3.94~1~SYNC-OUT~2~0~588.03 340.87 584.88 340.87 584.88 336.93 588.03 336.93~180~gge35727~0~~Y~0~0~0.4~586.459,338.901'; const nets = ['', 'GND', 'B-IN']; - expect(normalize(convertShape(input, nets)[0])).toEqual([ + expect(normalize(convertShape(input, conversionState(nets))[0])).toEqual([ 'module', 'easyeda:0603', ['layer', 'F.Cu'], @@ -568,3 +586,82 @@ describe('convertLib()', () => { ]); }); }); + +describe('integration', () => { + it('should successfully convert a simple 4-layer board (issue #33)', () => { + const input = { + head: { + docType: '3', + editorVersion: '6.4.3', + newgId: true, + c_para: {}, + hasIdFlag: true, + x: '4020', + y: '3438.5', + importFlag: 0, + transformList: '', + }, + canvas: '', + shape: ['TRACK~0.6~22~S$3354~4344.6 3172.8 4344.5 3225~gge2291~0'], + layers: [] as string[], + objects: [] as string[], + BBox: { x: 4246, y: 3014, width: 227.5, height: 251 }, + preference: { hideFootprints: '', hideNets: '' }, + DRCRULE: { + Default: { + trackWidth: 1, + clearance: 0.6, + viaHoleDiameter: 2.4, + viaHoleD: 1.2, + }, + isRealtime: true, + isDrcOnRoutingOrPlaceVia: false, + checkObjectToCopperarea: true, + showDRCRangeLine: true, + }, + netColors: {}, + }; + expect(convertBoardToArray(input)).toEqual([ + 'kicad_pcb', + ['version', 20171130], + ['host', 'pcbnew', '(5.1.5)-3'], + ['page', 'A4'], + [ + 'layers', + [0, 'F.Cu', 'signal'], + [1, 'In1.Cu', 'signal'], + [2, 'In2.Cu', 'signal'], + [31, 'B.Cu', 'signal'], + [32, 'B.Adhes', 'user'], + [33, 'F.Adhes', 'user'], + [34, 'B.Paste', 'user'], + [35, 'F.Paste', 'user'], + [36, 'B.SilkS', 'user'], + [37, 'F.SilkS', 'user'], + [38, 'B.Mask', 'user'], + [39, 'F.Mask', 'user'], + [40, 'Dwgs.User', 'user'], + [41, 'Cmts.User', 'user'], + [42, 'Eco1.User', 'user'], + [43, 'Eco2.User', 'user'], + [44, 'Edge.Cuts', 'user'], + [45, 'Margin', 'user'], + [46, 'B.CrtYd', 'user'], + [47, 'F.CrtYd', 'user'], + [48, 'B.Fab', 'user', 'hide'], + [49, 'F.Fab', 'user', 'hide'], + ], + ['net', 0, ''], + ['net', 1, 'S$3354'], + [ + 'segment', + ['start', 87.52840000000009, 43.89120000000005], + ['end', 87.503, 57.15], + ['width', 0.15239999999999998], + ['layer', 'In2.Cu'], + ['net', 1], + null, + ], + ]); + }); +}); diff --git a/src/board.ts b/src/board.ts index cd61327..deb6959 100644 --- a/src/board.ts +++ b/src/board.ts @@ -1,10 +1,15 @@ import { IEasyEDABoard } from './easyeda-types'; -import { encodeObject } from './spectra'; +import { encodeObject, ISpectraList } from './spectra'; import { computeArc } from './svg-arc'; // doc: https://docs.easyeda.com/en/DocumentFormat/3-EasyEDA-PCB-File-Format/index.html#shapes -function getLayerName(id: string) { +interface IConversionState { + nets: string[]; + innerLayers: number; +} + +function getLayerName(id: string, conversionState: IConversionState) { const layers: { [key: string]: string } = { 1: 'F.Cu', 2: 'B.Cu', @@ -23,6 +28,14 @@ function getLayerName(id: string) { if (id in layers) { return layers[id]; } + + // Inner layers: 21 -> In1.Cu + const intId = parseInt(id, 10); + if (intId >= 21 && intId <= 50) { + const innerLayerId = intId - 20; + conversionState.innerLayers = Math.max(conversionState.innerLayers, innerLayerId); + return `In${innerLayerId}.Cu`; + } throw new Error(`Missing layer id: ${id}`); } @@ -96,7 +109,7 @@ function isCopper(layerName: string) { return layerName.endsWith('.Cu'); } -function getNetId(nets: string[], netName: string) { +function getNetId({ nets }: IConversionState, netName: string) { if (!netName) { return -1; } @@ -108,7 +121,11 @@ function getNetId(nets: string[], netName: string) { return nets.length - 1; } -function convertVia(args: string[], nets: string[], parentCoords?: IParentTransform) { +function convertVia( + args: string[], + conversionState: IConversionState, + parentCoords?: IParentTransform +) { const [x, y, diameter, net, drill, id, locked] = args; return [ 'via', @@ -116,11 +133,15 @@ function convertVia(args: string[], nets: string[], parentCoords?: IParentTransf ['size', kiUnits(diameter)], ['drill', kiUnits(drill) * 2], ['layers', 'F.Cu', 'B.Cu'], - ['net', getNetId(nets, net)], + ['net', getNetId(conversionState, net)], ]; } -function convertPadToVia(args: string[], nets: string[], parentCoords?: IParentTransform) { +function convertPadToVia( + args: string[], + conversionState: IConversionState, + parentCoords?: IParentTransform +) { const [ shape, x, @@ -154,7 +175,7 @@ function convertPadToVia(args: string[], nets: string[], parentCoords?: IParentT ['attr', 'virtual'], ['fp_text', 'reference', '', ['at', 0, 0], ['layer', 'F.SilkS']], ['fp_text', 'value', '', ['at', 0, 0], ['layer', 'F.SilkS']], - convertPad(args, nets, { ...kiCoords(x, y), angle: 0 }), + convertPad(args, conversionState, { ...kiCoords(x, y), angle: 0 }), ]; } @@ -164,21 +185,21 @@ function convertPadToVia(args: string[], nets: string[], parentCoords?: IParentT ['size', kiUnits(holeRadius)], ['drill', kiUnits(drill) * 2], ['layers', 'F.Cu', 'B.Cu'], - ['net', getNetId(nets, net)], + ['net', getNetId(conversionState, net)], ]; } function convertTrack( args: string[], - nets: string[], + conversionState: IConversionState, objName = 'segment', parentCoords?: IParentTransform ) { const [width, layer, net, coords, id, locked] = args; - const netId = getNetId(nets, net); + const netId = getNetId(conversionState, net); const coordList = coords.split(' '); const result = []; - const layerName = getLayerName(layer); + const layerName = getLayerName(layer, conversionState); const lineType = objName === 'segment' && !isCopper(layerName) ? 'gr_line' : objName; for (let i = 0; i < coordList.length - 2; i += 2) { result.push([ @@ -199,8 +220,13 @@ function convertTrack( return result; } -function textLayer(layer: string, footprint: boolean, isName: boolean) { - const layerName = getLayerName(layer); +function textLayer( + layer: string, + conversionState: IConversionState, + footprint: boolean, + isName: boolean +) { + const layerName = getLayerName(layer, conversionState); if (footprint && isName) { return layerName.replace('.SilkS', '.Fab'); } else { @@ -208,7 +234,12 @@ function textLayer(layer: string, footprint: boolean, isName: boolean) { } } -function convertText(args: string[], objName = 'gr_text', parentCoords?: IParentTransform) { +function convertText( + args: string[], + conversionState: IConversionState, + objName = 'gr_text', + parentCoords?: IParentTransform +) { const [ type, // N/P/L (Name/Prefix/Label) x, @@ -226,7 +257,7 @@ function convertText(args: string[], objName = 'gr_text', parentCoords?: IParent font, locked, ] = args; - const layerName = textLayer(layer, objName === 'fp_text', type === 'N'); + const layerName = textLayer(layer, conversionState, objName === 'fp_text', type === 'N'); const fontTable: { [key: string]: { width: number; thickness: number } } = { 'NotoSerifCJKsc-Medium': { width: 0.8, thickness: 0.3 }, 'NotoSansCJKjp-DemiLight': { width: 0.6, thickness: 0.5 }, @@ -252,7 +283,12 @@ function convertText(args: string[], objName = 'gr_text', parentCoords?: IParent ]; } -function convertArc(args: string[], objName = 'gr_arc', transform?: IParentTransform) { +function convertArc( + args: string[], + conversionState: IConversionState, + objName = 'gr_arc', + transform?: IParentTransform +) { const [width, layer, net, path, _, id, locked] = args; const [match, startPoint, arcParams] = /^M\s*([-\d.\s]+)A\s*([-\d.\s]+)$/.exec( path.replace(/[,\s]+/g, ' ') @@ -280,7 +316,7 @@ function convertArc(args: string[], objName = 'gr_arc', transform?: IParentTrans ['end', endPoint.x, endPoint.y], ['angle', Math.abs(extent)], ['width', kiUnits(width)], - ['layer', getLayerName(layer)], + ['layer', getLayerName(layer, conversionState)], ]; } @@ -315,7 +351,11 @@ function rectangleSize(points: number[], rotation: number) { return Math.round(Math.abs(rotation)) % 180 === 90 ? [height, width] : [width, height]; } -function convertPad(args: string[], nets: string[], transform: IParentTransform) { +function convertPad( + args: string[], + conversionState: IConversionState, + transform: IParentTransform +) { const [ shape, x, @@ -356,7 +396,7 @@ function convertPad(args: string[], nets: string[], transform: IParentTransform) return null; } - const netId = getNetId(nets, net); + const netId = getNetId(conversionState, net); const layers: { [key: string]: string[] } = { 1: ['F.Cu', 'F.Paste', 'F.Mask'], 2: ['B.Cu', 'B.Paste', 'B.Mask'], @@ -404,14 +444,19 @@ function convertLibHole(args: string[], transform: IParentTransform) { ]; } -function convertCircle(args: string[], type = 'gr_circle', parentCoords?: IParentTransform) { +function convertCircle( + args: string[], + conversionState: IConversionState, + type = 'gr_circle', + parentCoords?: IParentTransform +) { const [x, y, radius, strokeWidth, layer, id, locked] = args; const center = kiCoords(x, y, parentCoords); return [ type, ['center', center.x, center.y], ['end', center.x + kiUnits(radius), center.y], - ['layer', getLayerName(layer)], + ['layer', getLayerName(layer, conversionState)], ['width', kiUnits(strokeWidth)], ]; } @@ -434,7 +479,11 @@ function pathToPolygon(path: string, parentCoords?: IParentTransform) { return pointListToPolygon(points, parentCoords); } -function convertPolygon(args: string[], parentCoords?: IParentTransform) { +function convertPolygon( + args: string[], + conversionState: IConversionState, + parentCoords?: IParentTransform +) { const [layerId, net, path, type, id, , , locked] = args; if (type !== 'solid') { console.warn(`Warning: unsupported SOLIDREGION type in footprint: ${type}`); @@ -444,10 +493,15 @@ function convertPolygon(args: string[], parentCoords?: IParentTransform) { if (!polygonPoints) { return null; } - return ['fp_poly', ['pts', ...polygonPoints], ['layer', getLayerName(layerId)], ['width', 0]]; + return [ + 'fp_poly', + ['pts', ...polygonPoints], + ['layer', getLayerName(layerId, conversionState)], + ['width', 0], + ]; } -function convertLib(args: string[], nets: string[]) { +function convertLib(args: string[], conversionState: IConversionState) { const [x, y, attributes, rotation, importFlag, id, , , , locked] = args; const shapeList = args.join('~').split('#@$').slice(1); const attrList = attributes.split('`'); @@ -460,19 +514,19 @@ function convertLib(args: string[], nets: string[]) { for (const shape of shapeList) { const [type, ...shapeArgs] = shape.split('~'); if (type === 'TRACK') { - shapes.push(...convertTrack(shapeArgs, nets, 'fp_line', transform)); + shapes.push(...convertTrack(shapeArgs, conversionState, 'fp_line', transform)); } else if (type === 'TEXT') { - shapes.push(convertText(shapeArgs, 'fp_text', transform)); + shapes.push(convertText(shapeArgs, conversionState, 'fp_text', transform)); } else if (type === 'ARC') { - shapes.push(convertArc(shapeArgs, 'fp_arc', transform)); + shapes.push(convertArc(shapeArgs, conversionState, 'fp_arc', transform)); } else if (type === 'HOLE') { shapes.push(convertLibHole(shapeArgs, transform)); } else if (type === 'PAD') { - shapes.push(convertPad(shapeArgs, nets, transform)); + shapes.push(convertPad(shapeArgs, conversionState, transform)); } else if (type === 'CIRCLE') { - shapes.push(convertCircle(shapeArgs, 'fp_circle', transform)); + shapes.push(convertCircle(shapeArgs, conversionState, 'fp_circle', transform)); } else if (type === 'SOLIDREGION') { - shapes.push(convertPolygon(shapeArgs, transform)); + shapes.push(convertPolygon(shapeArgs, conversionState, transform)); } else { console.warn(`Warning: unsupported shape ${type} in footprint ${id}`); } @@ -504,7 +558,7 @@ function convertLib(args: string[], nets: string[]) { ]; } -function convertCopperArea(args: string[], nets: string[]) { +function convertCopperArea(args: string[], conversionState: IConversionState) { const [ strokeWidth, layerId, @@ -518,7 +572,7 @@ function convertCopperArea(args: string[], nets: string[]) { copperZone, locked, ] = args; - const netId = getNetId(nets, net); + const netId = getNetId(conversionState, net); // fill style: solid/none // id: gge27 // thermal: spoke/direct @@ -532,7 +586,7 @@ function convertCopperArea(args: string[], nets: string[]) { 'zone', ['net', netId], ['net_name', net], - ['layer', getLayerName(layerId)], + ['layer', getLayerName(layerId, conversionState)], ['hatch', 'edge', 0.508], ['connect_pads', ['clearance', kiUnits(clearanceWidth)]], // TODO (min_thickness 0.254) @@ -541,10 +595,10 @@ function convertCopperArea(args: string[], nets: string[]) { ]; } -function convertSolidRegion(args: string[], nets: string[]) { +function convertSolidRegion(args: string[], conversionState: IConversionState) { const [layerId, net, path, type, id, locked] = args; const polygonPoints = pathToPolygon(path); - const netId = getNetId(nets, net); + const netId = getNetId(conversionState, net); if (!polygonPoints) { return null; } @@ -555,7 +609,7 @@ function convertSolidRegion(args: string[], nets: string[]) { ['net', netId], ['net_name', ''], ['hatch', 'edge', 0.508], - ['layer', getLayerName(layerId)], + ['layer', getLayerName(layerId, conversionState)], ['keepout', ['tracks', 'allowed'], ['vias', 'allowed'], ['copperpour', 'not_allowed']], ['polygon', ['pts', ...polygonPoints]], ]; @@ -566,7 +620,7 @@ function convertSolidRegion(args: string[], nets: string[]) { // Unfortunately, KiCad does not support net for gr_poly // ['net', netId], ['pts', ...polygonPoints], - ['layer', getLayerName(layerId)], + ['layer', getLayerName(layerId, conversionState)], ['width', 0], ]; @@ -601,29 +655,29 @@ function convertHole(args: string[]) { ]; } -export function convertShape(shape: string, nets: string[]) { +export function convertShape(shape: string, conversionState: IConversionState) { const [type, ...args] = shape.split('~'); switch (type) { case 'VIA': - return [convertVia(args, nets)]; + return [convertVia(args, conversionState)]; case 'TRACK': - return convertTrack(args, nets); + return convertTrack(args, conversionState); case 'TEXT': - return [convertText(args)]; + return [convertText(args, conversionState)]; case 'ARC': - return [convertArc(args)]; + return [convertArc(args, conversionState)]; case 'COPPERAREA': - return [convertCopperArea(args, nets)]; + return [convertCopperArea(args, conversionState)]; case 'SOLIDREGION': - return [convertSolidRegion(args, nets)]; + return [convertSolidRegion(args, conversionState)]; case 'CIRCLE': - return [convertCircle(args)]; + return [convertCircle(args, conversionState)]; case 'HOLE': return [convertHole(args)]; case 'LIB': - return [convertLib(args, nets)]; + return [convertLib(args, conversionState)]; case 'PAD': - return [convertPadToVia(args, nets)]; + return [convertPadToVia(args, conversionState)]; default: console.warn(`Warning: unsupported shape ${type}`); return null; @@ -634,43 +688,54 @@ function flatten(arr: T[]) { return [].concat(...arr); } -export function convertBoard(input: IEasyEDABoard) { +export function convertBoardToArray(input: IEasyEDABoard): ISpectraList { const { nets } = input.routerRule || { nets: [] as string[] }; + const conversionState = { nets, innerLayers: 0 }; nets.unshift(''); // Kicad expects net 0 to be empty - const shapes = flatten(input.shape.map((shape) => convertShape(shape, nets))); + const shapes = flatten(input.shape.map((shape) => convertShape(shape, conversionState))); const outputObjs = [...nets.map((net, idx) => ['net', idx, net]), ...shapes].filter( (obj) => obj != null ); - const output = ` -(kicad_pcb (version 20171130) (host pcbnew "(5.0.2)-1") - -(page A4) -(layers - (0 F.Cu signal) - (31 B.Cu signal) - (32 B.Adhes user) - (33 F.Adhes user) - (34 B.Paste user) - (35 F.Paste user) - (36 B.SilkS user) - (37 F.SilkS user) - (38 B.Mask user) - (39 F.Mask user) - (40 Dwgs.User user) - (41 Cmts.User user) - (42 Eco1.User user) - (43 Eco2.User user) - (44 Edge.Cuts user) - (45 Margin user) - (46 B.CrtYd user) - (47 F.CrtYd user) - (48 B.Fab user hide) - (49 F.Fab user hide) -) - -${outputObjs.map(encodeObject).join('\n')} -) -`; - return output; + const innerLayers = []; + for (let i = 1; i <= conversionState.innerLayers; i++) { + innerLayers.push([i, `In${i}.Cu`, 'signal']); + } + + const layers = [ + [0, 'F.Cu', 'signal'], + ...innerLayers, + [31, 'B.Cu', 'signal'], + [32, 'B.Adhes', 'user'], + [33, 'F.Adhes', 'user'], + [34, 'B.Paste', 'user'], + [35, 'F.Paste', 'user'], + [36, 'B.SilkS', 'user'], + [37, 'F.SilkS', 'user'], + [38, 'B.Mask', 'user'], + [39, 'F.Mask', 'user'], + [40, 'Dwgs.User', 'user'], + [41, 'Cmts.User', 'user'], + [42, 'Eco1.User', 'user'], + [43, 'Eco2.User', 'user'], + [44, 'Edge.Cuts', 'user'], + [45, 'Margin', 'user'], + [46, 'B.CrtYd', 'user'], + [47, 'F.CrtYd', 'user'], + [48, 'B.Fab', 'user', 'hide'], + [49, 'F.Fab', 'user', 'hide'], + ]; + + return [ + 'kicad_pcb', + ['version', 20171130], + ['host', 'pcbnew', '(5.1.5)-3'], + ['page', 'A4'], + ['layers', ...layers], + ...outputObjs, + ]; +} + +export function convertBoard(board: IEasyEDABoard) { + return encodeObject(convertBoardToArray(board)); } diff --git a/src/easyeda-types.ts b/src/easyeda-types.ts index 71e916d..6393997 100644 --- a/src/easyeda-types.ts +++ b/src/easyeda-types.ts @@ -16,13 +16,13 @@ export interface IEasyEDABoard { head: Head; canvas: string; shape: string[]; - systemColor: string; + systemColor?: string; layers: string[]; objects: string[]; BBox: BBox; preference: Preference; DRCRULE: Drcrule; - routerRule: RouterRule; + routerRule?: RouterRule; netColors: {}; } diff --git a/src/index.ts b/src/index.ts index 52ad640..99ae602 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ -export { convertBoard } from './board'; +export { convertSchematic } from './schematic'; +export { convertBoard, convertBoardToArray } from './board'; diff --git a/src/main.ts b/src/main.ts index ca6f654..f894bae 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import { convertBoard } from './board'; import { convertSchematic } from './schematic'; +import { encodeObject } from './spectra'; if (process.argv.length < 3) { console.error(`Usage: ${process.argv[1]} [output.kicad_pcb]`);