From 3b1cf729beb16028422622aac6ba6a872946ad61 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 07:43:02 -0500 Subject: Add script to render gerbv test files --- examples/gerbv_test_files/include-file-1.gbx | 7 +++ .../gerbv_test_files/test-aperture-circle-1.gbx | 38 ++++++++++++ .../test-aperture-circle-flash-1.gbx | 27 +++++++++ .../gerbv_test_files/test-aperture-obround-1.gbx | 38 ++++++++++++ .../test-aperture-obround-flash-1.gbx | 27 +++++++++ .../gerbv_test_files/test-aperture-polygon-1.gbx | 38 ++++++++++++ .../test-aperture-polygon-flash-1.gbx | 27 +++++++++ .../gerbv_test_files/test-aperture-rectangle-1.gbx | 38 ++++++++++++ .../test-aperture-rectangle-flash-1.gbx | 27 +++++++++ .../test-circular-interpolation-1.gbx | 35 +++++++++++ .../gerbv_test_files/test-drill-leading-zero-1.exc | 8 +++ examples/gerbv_test_files/test-drill-repeat-1.exc | 19 ++++++ .../test-drill-trailing-zero-1.exc | 8 +++ examples/gerbv_test_files/test-image-justify-1.gbx | 19 ++++++ examples/gerbv_test_files/test-image-justify-2.gbx | 19 ++++++ examples/gerbv_test_files/test-image-offset-1.gbx | 18 ++++++ examples/gerbv_test_files/test-image-offset-2.gbx | 19 ++++++ .../gerbv_test_files/test-image-polarity-1.gbx | 17 ++++++ .../gerbv_test_files/test-image-rotation-1.gbx | 21 +++++++ examples/gerbv_test_files/test-include-file-1.gbx | 11 ++++ .../gerbv_test_files/test-layer-axis-select-1.gbx | 15 +++++ .../gerbv_test_files/test-layer-knockout-1.gbx | 28 +++++++++ .../gerbv_test_files/test-layer-knockout-2.gbx | 28 +++++++++ .../gerbv_test_files/test-layer-mirror-image-1.gbx | 23 ++++++++ examples/gerbv_test_files/test-layer-mode-1.gbx | 68 ++++++++++++++++++++++ examples/gerbv_test_files/test-layer-offset-1.gbx | 18 ++++++ .../gerbv_test_files/test-layer-rotation-1.gbx | 23 ++++++++ .../gerbv_test_files/test-layer-scale-factor-1.gbx | 17 ++++++ .../test-layer-step-and_repeat-1.gbx | 17 ++++++ .../test-layer-step-and_repeat-2.gbx | 18 ++++++ examples/gerbv_test_files/test-polygon-fill-1.gbx | 45 ++++++++++++++ examples/render_gerbv_tests.py | 59 +++++++++++++++++++ 32 files changed, 820 insertions(+) create mode 100755 examples/gerbv_test_files/include-file-1.gbx create mode 100755 examples/gerbv_test_files/test-aperture-circle-1.gbx create mode 100755 examples/gerbv_test_files/test-aperture-circle-flash-1.gbx create mode 100755 examples/gerbv_test_files/test-aperture-obround-1.gbx create mode 100755 examples/gerbv_test_files/test-aperture-obround-flash-1.gbx create mode 100755 examples/gerbv_test_files/test-aperture-polygon-1.gbx create mode 100755 examples/gerbv_test_files/test-aperture-polygon-flash-1.gbx create mode 100755 examples/gerbv_test_files/test-aperture-rectangle-1.gbx create mode 100755 examples/gerbv_test_files/test-aperture-rectangle-flash-1.gbx create mode 100755 examples/gerbv_test_files/test-circular-interpolation-1.gbx create mode 100755 examples/gerbv_test_files/test-drill-leading-zero-1.exc create mode 100755 examples/gerbv_test_files/test-drill-repeat-1.exc create mode 100755 examples/gerbv_test_files/test-drill-trailing-zero-1.exc create mode 100755 examples/gerbv_test_files/test-image-justify-1.gbx create mode 100755 examples/gerbv_test_files/test-image-justify-2.gbx create mode 100755 examples/gerbv_test_files/test-image-offset-1.gbx create mode 100755 examples/gerbv_test_files/test-image-offset-2.gbx create mode 100755 examples/gerbv_test_files/test-image-polarity-1.gbx create mode 100755 examples/gerbv_test_files/test-image-rotation-1.gbx create mode 100755 examples/gerbv_test_files/test-include-file-1.gbx create mode 100755 examples/gerbv_test_files/test-layer-axis-select-1.gbx create mode 100755 examples/gerbv_test_files/test-layer-knockout-1.gbx create mode 100755 examples/gerbv_test_files/test-layer-knockout-2.gbx create mode 100755 examples/gerbv_test_files/test-layer-mirror-image-1.gbx create mode 100755 examples/gerbv_test_files/test-layer-mode-1.gbx create mode 100755 examples/gerbv_test_files/test-layer-offset-1.gbx create mode 100755 examples/gerbv_test_files/test-layer-rotation-1.gbx create mode 100755 examples/gerbv_test_files/test-layer-scale-factor-1.gbx create mode 100755 examples/gerbv_test_files/test-layer-step-and_repeat-1.gbx create mode 100755 examples/gerbv_test_files/test-layer-step-and_repeat-2.gbx create mode 100755 examples/gerbv_test_files/test-polygon-fill-1.gbx create mode 100755 examples/render_gerbv_tests.py diff --git a/examples/gerbv_test_files/include-file-1.gbx b/examples/gerbv_test_files/include-file-1.gbx new file mode 100755 index 0000000..75b5325 --- /dev/null +++ b/examples/gerbv_test_files/include-file-1.gbx @@ -0,0 +1,7 @@ +X-1000Y0D02* +G54D10* +X1000Y0D01* +X0Y-1000D02* +G54D10* +X0Y1000D01* + diff --git a/examples/gerbv_test_files/test-aperture-circle-1.gbx b/examples/gerbv_test_files/test-aperture-circle-1.gbx new file mode 100755 index 0000000..ab56aae --- /dev/null +++ b/examples/gerbv_test_files/test-aperture-circle-1.gbx @@ -0,0 +1,38 @@ +G04 Test drawing with circular apertures* +G04 Hand coded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%ADD10C,0.050*% +G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when* +G04 drawing with it (only should show in flashes)* +%ADD11C,0.075X0.050*% + +G04 No hole, centered at 0,0 * +G54D10* +G04 Recenter to 0,0 +G01X0Y0D02* +G04 Draw a line segment* +X00100Y0D01* +G04 Turn off for a segment* +X00200Y0D02* +G04 Draw another line at angle* +G54D11* +X00300Y00100D01* +G04 Turn off for a segment* +X0Y00100D02* +G54D10* +G04 Turn on circular interpolation* +G75* +G03X0Y00300I0J00100D01* + +G04 Turn off for a segment* +X00500Y00D02* +G04 Draw a larger radius arc* +G03X00350Y00150I-00250J-00050D01* + +G04 Turn off for a segment* +X00250Y00200D02* +G04 Draw a larger clockwise radius arc* +G02X00350Y00350I00250J-00050D01* + +M02* diff --git a/examples/gerbv_test_files/test-aperture-circle-flash-1.gbx b/examples/gerbv_test_files/test-aperture-circle-flash-1.gbx new file mode 100755 index 0000000..91c4bfc --- /dev/null +++ b/examples/gerbv_test_files/test-aperture-circle-flash-1.gbx @@ -0,0 +1,27 @@ +G04 Test flashing of circular apertures* +G04 Four groups of circular apertures are arranged in a square* +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%ADD10C,0.050*% +%ADD11C,0.050X0.025*% +%ADD12C,0.050X0.025X0.030*% + +G04 No hole, centered at 0,0 * +G54D10* +X0Y0D03* + +G04 Round hole, centered at 0.1,0 * +G54D11* +X00100Y0D03* + +G04 Square hole, centered at 0,0.1 * +G54D12* +X0Y00100D03* + +G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 * +G54D11* +X00100Y00090D03* +X00100Y00110D03* + +M02* diff --git a/examples/gerbv_test_files/test-aperture-obround-1.gbx b/examples/gerbv_test_files/test-aperture-obround-1.gbx new file mode 100755 index 0000000..763232b --- /dev/null +++ b/examples/gerbv_test_files/test-aperture-obround-1.gbx @@ -0,0 +1,38 @@ +G04 Test drawing with rectangular apertures* +G04 Hand coded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%ADD10O,0.050X0.025*% +G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when* +G04 drawing with it (only should show in flashes)* +%ADD11O,0.075X0.050X0.025*% + +G04 No hole, centered at 0,0 * +G54D10* +G04 Recenter to 0,0 +G01X0Y0D02* +G04 Draw a line segment* +X00100Y0D01* +G04 Turn off for a segment* +X00200Y0D02* +G04 Draw another line at angle* +G54D11* +X00300Y00100D01* +G04 Turn off for a segment* +X0Y00100D02* +G54D10* +G04 Turn on circular interpolation* +G75* +G03X0Y00300I0J00100D01* + +G04 Turn off for a segment* +X00500Y00D02* +G04 Draw a larger radius arc* +G03X00350Y00150I-00250J-00050D01* + +G04 Turn off for a segment* +X00250Y00200D02* +G04 Draw a larger clockwise radius arc* +G02X00350Y00350I00250J-00050D01* + +M02* diff --git a/examples/gerbv_test_files/test-aperture-obround-flash-1.gbx b/examples/gerbv_test_files/test-aperture-obround-flash-1.gbx new file mode 100755 index 0000000..b09bf43 --- /dev/null +++ b/examples/gerbv_test_files/test-aperture-obround-flash-1.gbx @@ -0,0 +1,27 @@ +G04 Test flashing of obround apertures* +G04 Four groups of obround apertures are arranged in a square* +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%ADD10O,0.050X0.080*% +%ADD11O,0.080X0.050X0.025*% +%ADD12O,0.050X0.025X0.025X0.0150*% + +G04 No hole, centered at 0,0 * +G54D10* +X0Y0D03* + +G04 Round hole, centered at 0.1,0 * +G54D11* +X00100Y0D03* + +G04 Square hole, centered at 0,0.1 * +G54D12* +X0Y00100D03* + +G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 * +G54D11* +X00100Y00090D03* +X00100Y00110D03* + +M02* diff --git a/examples/gerbv_test_files/test-aperture-polygon-1.gbx b/examples/gerbv_test_files/test-aperture-polygon-1.gbx new file mode 100755 index 0000000..27f7e8e --- /dev/null +++ b/examples/gerbv_test_files/test-aperture-polygon-1.gbx @@ -0,0 +1,38 @@ +G04 Test drawing with polygon apertures* +G04 Hand coded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%ADD10O,0.050X0.025*% +G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when* +G04 drawing with it (only should show in flashes)* +%ADD11O,0.075X0.050X0.025*% + +G04 No hole, centered at 0,0 * +G54D10* +G04 Recenter to 0,0 +G01X0Y0D02* +G04 Draw a line segment* +X00100Y0D01* +G04 Turn off for a segment* +X00200Y0D02* +G04 Draw another line at angle* +G54D11* +X00300Y00100D01* +G04 Turn off for a segment* +X0Y00100D02* +G54D10* +G04 Turn on circular interpolation* +G75* +G03X0Y00300I0J00100D01* + +G04 Turn off for a segment* +X00500Y00D02* +G04 Draw a larger radius arc* +G03X00350Y00150I-00250J-00050D01* + +G04 Turn off for a segment* +X00250Y00200D02* +G04 Draw a larger clockwise radius arc* +G02X00350Y00350I00250J-00050D01* + +M02* diff --git a/examples/gerbv_test_files/test-aperture-polygon-flash-1.gbx b/examples/gerbv_test_files/test-aperture-polygon-flash-1.gbx new file mode 100755 index 0000000..788a755 --- /dev/null +++ b/examples/gerbv_test_files/test-aperture-polygon-flash-1.gbx @@ -0,0 +1,27 @@ +G04 Test flashing of polygon apertures* +G04 Four groups of polygon apertures are arranged in a square* +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%ADD10P,0.050X3*% +%ADD11P,0.050X6X-45X0.035*% +%ADD12P,0.040X10X25X0.025X0.025X0.0150*% + +G04 Triangle, centered at 0,0 * +G54D10* +X0Y0D03* + +G04 Hexagon with round hole rotate 45 degreed ccwise, centered at 0.1,0 * +G54D11* +X00100Y0D03* + +G04 10-sided with square hole rotated 25 degrees, centered at 0,0.1 * +G54D12* +X0Y00100D03* + +G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 * +G54D11* +X00100Y00090D03* +X00100Y00110D03* + +M02* diff --git a/examples/gerbv_test_files/test-aperture-rectangle-1.gbx b/examples/gerbv_test_files/test-aperture-rectangle-1.gbx new file mode 100755 index 0000000..77a8c2e --- /dev/null +++ b/examples/gerbv_test_files/test-aperture-rectangle-1.gbx @@ -0,0 +1,38 @@ +G04 Test drawing with rectangular apertures* +G04 Hand coded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%ADD10R,0.050X0.025*% +G04 Note: aperture 11 has a round hole in it, but this shouldn't ever show when* +G04 drawing with it (only should show in flashes)* +%ADD11R,0.075X0.050X0.025*% + +G04 No hole, centered at 0,0 * +G54D10* +G04 Recenter to 0,0 +G01X0Y0D02* +G04 Draw a line segment* +X00100Y0D01* +G04 Turn off for a segment* +X00200Y0D02* +G04 Draw another line at angle* +G54D11* +X00300Y00100D01* +G04 Turn off for a segment* +X0Y00100D02* +G54D10* +G04 Turn on circular interpolation* +G75* +G03X0Y00300I0J00100D01* + +G04 Turn off for a segment* +X00500Y00D02* +G04 Draw a larger radius arc* +G03X00350Y00150I-00250J-00050D01* + +G04 Turn off for a segment* +X00250Y00200D02* +G04 Draw a larger clockwise radius arc* +G02X00350Y00350I00250J-00050D01* + +M02* diff --git a/examples/gerbv_test_files/test-aperture-rectangle-flash-1.gbx b/examples/gerbv_test_files/test-aperture-rectangle-flash-1.gbx new file mode 100755 index 0000000..8aeb79c --- /dev/null +++ b/examples/gerbv_test_files/test-aperture-rectangle-flash-1.gbx @@ -0,0 +1,27 @@ +G04 Test flashing of rectangular apertures* +G04 Four groups of rectangular apertures are arranged in a square* +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%ADD10R,0.050X0.080*% +%ADD11R,0.080X0.050X0.025*% +%ADD12R,0.050X0.025X0.025X0.0150*% + +G04 No hole, centered at 0,0 * +G54D10* +X0Y0D03* + +G04 Round hole, centered at 0.1,0 * +G54D11* +X00100Y0D03* + +G04 Square hole, centered at 0,0.1 * +G54D12* +X0Y00100D03* + +G04 Two, with round holes, slightly overlapping, centered at 0.1,0.1 * +G54D11* +X00100Y00090D03* +X00100Y00110D03* + +M02* diff --git a/examples/gerbv_test_files/test-circular-interpolation-1.gbx b/examples/gerbv_test_files/test-circular-interpolation-1.gbx new file mode 100755 index 0000000..75cffc9 --- /dev/null +++ b/examples/gerbv_test_files/test-circular-interpolation-1.gbx @@ -0,0 +1,35 @@ +G04 Test circular interpolation* +G04 Hand coded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%ADD10C,0.050*% +G54D10* + +G04 Recenter to 0,0* +G01X0Y0D02* + +G04 Turn on multi-quadrant mode* +G75* +G03X0Y00200I0J00100D01* + +G04 Switch to quadrant mode, draw ccwise* +G74* +G01X00400Y0D02* +G03X00470Y00080I0J00100D01* + +G04 Draw things clockwise on the top two objects* + +G04 Turn on multi-quadrant mode* +G75* +G01X00100Y00300D02* +G02X00100Y00500I0J00100D01* + +G04 Switch to quadrant mode, draw clockwise* +G04 Note: since this is single quadrant mode, I and J must be* +G04 positive, and the parser should automatically negate the J value* +G04 to make the curve travel in the clockwise direction* +G74* +G01X00400Y00300D02* +G02X00500Y00300I00150J00300D01* + +M02* diff --git a/examples/gerbv_test_files/test-drill-leading-zero-1.exc b/examples/gerbv_test_files/test-drill-leading-zero-1.exc new file mode 100755 index 0000000..7e53954 --- /dev/null +++ b/examples/gerbv_test_files/test-drill-leading-zero-1.exc @@ -0,0 +1,8 @@ +M48 +INCH,LZ +T13C0.05 +% +T13 +X-001000Y030000 +X00000Y03000 +X001Y03 M30 diff --git a/examples/gerbv_test_files/test-drill-repeat-1.exc b/examples/gerbv_test_files/test-drill-repeat-1.exc new file mode 100755 index 0000000..8a87d5a --- /dev/null +++ b/examples/gerbv_test_files/test-drill-repeat-1.exc @@ -0,0 +1,19 @@ +M48 +INCH,TZ +T01C0.050 +% +T01 +X0000Y0000 +X10000Y10000 +R5X1000 +X20000Y10000 +R5Y1000 +X30000Y10000 +R5X1000Y1500 +X10000Y00000 +R5X-1000 +X20000Y00000 +R5Y-1000 +X30000Y00000 +R5X-1000Y-1500 +M30 diff --git a/examples/gerbv_test_files/test-drill-trailing-zero-1.exc b/examples/gerbv_test_files/test-drill-trailing-zero-1.exc new file mode 100755 index 0000000..5218b81 --- /dev/null +++ b/examples/gerbv_test_files/test-drill-trailing-zero-1.exc @@ -0,0 +1,8 @@ +M48 +INCH,TZ +T13C0.05 +% +T13 +X-001000Y030000 +X0Y030000 +X01000Y30000 M30 diff --git a/examples/gerbv_test_files/test-image-justify-1.gbx b/examples/gerbv_test_files/test-image-justify-1.gbx new file mode 100755 index 0000000..6e71d62 --- /dev/null +++ b/examples/gerbv_test_files/test-image-justify-1.gbx @@ -0,0 +1,19 @@ +G04 Test image justify 1* +G04 Crosshairs should be justified to the X axis * +G04 and 0.5 inches offset from Y axis * +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%IJB.5*% +%ADD10C,0.050*% + +G04 Crosshairs * +X-1000Y0D02* +G54D10* +X1000Y0D01* + +X0Y-1000D02* +G54D10* +X0Y1000D01* + +M02* diff --git a/examples/gerbv_test_files/test-image-justify-2.gbx b/examples/gerbv_test_files/test-image-justify-2.gbx new file mode 100755 index 0000000..12fa617 --- /dev/null +++ b/examples/gerbv_test_files/test-image-justify-2.gbx @@ -0,0 +1,19 @@ +G04 Test image justify 2* +G04 Crosshairs should be centered in X and Y (platen size * +G04 is assumed to be 2x the overall size of the image) * +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%IJACBC*% +%ADD10C,0.050*% + +G04 Crosshairs * +X-1000Y0D02* +G54D10* +X1000Y0D01* + +X0Y-1000D02* +G54D10* +X0Y1000D01* + +M02* diff --git a/examples/gerbv_test_files/test-image-offset-1.gbx b/examples/gerbv_test_files/test-image-offset-1.gbx new file mode 100755 index 0000000..6f79713 --- /dev/null +++ b/examples/gerbv_test_files/test-image-offset-1.gbx @@ -0,0 +1,18 @@ +G04 Test image polarity * +G04 Crosshairs should be centered on 0,0 in final rendering* +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%IOA-2.0B-1.0*% +%ADD10C,0.050*% + +G04 Crosshairs to be on 0,0 * +X1000Y1000D02* +G54D10* +X3000Y1000D01* + +X2000Y0D02* +G54D10* +X2000Y2000D01* + +M02* diff --git a/examples/gerbv_test_files/test-image-offset-2.gbx b/examples/gerbv_test_files/test-image-offset-2.gbx new file mode 100755 index 0000000..34e56e1 --- /dev/null +++ b/examples/gerbv_test_files/test-image-offset-2.gbx @@ -0,0 +1,19 @@ +G04 Test image offset uses current units * +G04 Crosshairs should be centered on 0,0 in final rendering* +G04 Handcoded by Julian Lamb * +%MOMM*% +%FSLAX23Y23*% +%IOB-25.4*% +%MOIN*% +%ADD10C,0.050*% + +G04 Crosshairs to be on 0,0 * +X-1000Y1000D02* +G54D10* +X1000Y1000D01* + +X0Y0D02* +G54D10* +X0Y2000D01* + +M02* diff --git a/examples/gerbv_test_files/test-image-polarity-1.gbx b/examples/gerbv_test_files/test-image-polarity-1.gbx new file mode 100755 index 0000000..e012966 --- /dev/null +++ b/examples/gerbv_test_files/test-image-polarity-1.gbx @@ -0,0 +1,17 @@ +G04 Test image polarity * +G04 Crosshairs should be cut out of a positive background* +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%IPNEG*% +%ADD10C,0.050*% + +G04 Draw crosshairs * +X-1000Y0D02* +G54D10* +X1000Y0D01* +X0Y-1000D02* +G54D10* +X0Y1000D01* + +M02* diff --git a/examples/gerbv_test_files/test-image-rotation-1.gbx b/examples/gerbv_test_files/test-image-rotation-1.gbx new file mode 100755 index 0000000..c136b5b --- /dev/null +++ b/examples/gerbv_test_files/test-image-rotation-1.gbx @@ -0,0 +1,21 @@ +G04 Test image rotation * +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%IR270*% +%ADD10C,0.050*% + +G04 Quarter star * +X1000Y0D02* +G54D10* +X2000Y0D01* + +X1000Y0D02* +G54D10* +X2000Y1000D01* + +X1000Y0D02* +G54D10* +X1000Y1000D01* + +M02* diff --git a/examples/gerbv_test_files/test-include-file-1.gbx b/examples/gerbv_test_files/test-include-file-1.gbx new file mode 100755 index 0000000..9576c79 --- /dev/null +++ b/examples/gerbv_test_files/test-include-file-1.gbx @@ -0,0 +1,11 @@ +G04 Test include file 1 * +G04 Crosshairs should be drawn at 0,0 in final rendering* +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%IOA-2.0B-1.0*% +%ADD10C,0.050*% + +G04 Crosshairs to be on 0,0 * +%IFinclude-file-1.gbx*% +M02* diff --git a/examples/gerbv_test_files/test-layer-axis-select-1.gbx b/examples/gerbv_test_files/test-layer-axis-select-1.gbx new file mode 100755 index 0000000..d5f003e --- /dev/null +++ b/examples/gerbv_test_files/test-layer-axis-select-1.gbx @@ -0,0 +1,15 @@ +G04 Test layer axis select * +G04 Line is drawn along A axis, then axis select switches it and renders * +G04 line along y axis * +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%ASAYBX*% +%ADD10C,0.050*% + +G04 Draw line * +X-1000Y0D02* +G54D10* +X1000Y0D01* + +M02* diff --git a/examples/gerbv_test_files/test-layer-knockout-1.gbx b/examples/gerbv_test_files/test-layer-knockout-1.gbx new file mode 100755 index 0000000..1d400e5 --- /dev/null +++ b/examples/gerbv_test_files/test-layer-knockout-1.gbx @@ -0,0 +1,28 @@ +G04 Test layer knockout 1* +G04 A cleared 3x3 square should surround the crosshairs * +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%ADD10C,0.050*% + +G04 Create a large dark area * +G36* +X-2000Y-2000D02* +X2000Y-2000D01* +X2000Y2000D01* +X-2000Y2000D01* +X-2000Y-2000D01* +G37* + +G04 Create the knockout region * +%KOCX-1.5Y-1.5I3J3*% + +G04 Draw crosshairs * +X-1000Y0D02* +G54D10* +X1000Y0D01* +X0Y-1000D02* +G54D10* +X0Y1000D01* + +M02* diff --git a/examples/gerbv_test_files/test-layer-knockout-2.gbx b/examples/gerbv_test_files/test-layer-knockout-2.gbx new file mode 100755 index 0000000..2b331c3 --- /dev/null +++ b/examples/gerbv_test_files/test-layer-knockout-2.gbx @@ -0,0 +1,28 @@ +G04 Test layer knockout 2* +G04 A cleared 0.5 inch border should surround the crosshairs * +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%ADD10C,0.050*% + +G04 Create a large dark area * +G36* +X-2000Y-2000D02* +X2000Y-2000D01* +X2000Y2000D01* +X-2000Y2000D01* +X-2000Y-2000D01* +G37* + +G04 Create the knockout region * +%KOCK0.5*% + +G04 Draw crosshairs * +X-1000Y0D02* +G54D10* +X1000Y0D01* +X0Y-1000D02* +G54D10* +X0Y1000D01* + +M02* diff --git a/examples/gerbv_test_files/test-layer-mirror-image-1.gbx b/examples/gerbv_test_files/test-layer-mirror-image-1.gbx new file mode 100755 index 0000000..6c76132 --- /dev/null +++ b/examples/gerbv_test_files/test-layer-mirror-image-1.gbx @@ -0,0 +1,23 @@ +G04 Test layer mirror image 1 * +G04 Quarter star is drawn pointing towards +X, +Y. Mirror +G04 flips around the Y axis and the star should point towards -X, -Y * +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%MIA1B1*% +%ADD10C,0.050*% + +G04 Draw quarter star * +X0Y0D02* +G54D10* +X1000Y0D01* + +X0Y0D02* +G54D10* +X1000Y1000D01* + +X0Y0D02* +G54D10* +X0Y1000D01* + +M02* diff --git a/examples/gerbv_test_files/test-layer-mode-1.gbx b/examples/gerbv_test_files/test-layer-mode-1.gbx new file mode 100755 index 0000000..95b8555 --- /dev/null +++ b/examples/gerbv_test_files/test-layer-mode-1.gbx @@ -0,0 +1,68 @@ +G04 Test handling of unit changes within a RS274X file * +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +G04 Aperture 10 should be in Inches * +%ADD10C,0.050*% +%MOMM*% +G04 Aperture 11 should be in MMs * +%ADD11C,1.250*% +G04 Aperture 12 should be in MMs * +%AMTHERMAL* +7,0,0,25.4,12.7,2.54,0*% +%MOIN*% +G04 Aperture 13 is in inches * +%AMTHERMALTWO* +7,0,0,1,0.5,0.1,0*% +%MOMM*% +%ADD12THERMAL*% +%MOIN*% +%ADD13THERMALTWO*% + +%MOIN*% +G04 Box 1, using aperture 10* +X0Y0D02* +G54D10* +X0Y0D01* +X1000D01* +Y1000D01* +X0D01* +Y0D01* + +G04 Box 2, using aperture 11* +X2000Y0D02* +G54D11* +X2000Y0D01* +X3000D01* +Y1000D01* +X2000D01* +Y0D01* + +%MOMM*% +G04 Box 3, using aperture 10* +X100000Y0D02* +G54D10* +X100000Y0D01* +X125000D01* +Y25000D01* +X100000D01* +Y0D01* + +G04 Draw Thermal in box 1* +G54D12* +Y12000X12700D03* + +G04 Draw Thermal in box 2* +G04 ..switch to inches for coordinates* +G70* +Y500X2500D02* +G54D12* +Y500X2500D03* + +G04 ..switch to mms for coordinates* +G71* +G04 Draw Thermal in box 3* +G54D13* +Y12000X112000D03* + +M02* diff --git a/examples/gerbv_test_files/test-layer-offset-1.gbx b/examples/gerbv_test_files/test-layer-offset-1.gbx new file mode 100755 index 0000000..df26e4d --- /dev/null +++ b/examples/gerbv_test_files/test-layer-offset-1.gbx @@ -0,0 +1,18 @@ +G04 Test layer offset 1 * +G04 Crosshairs should be centered on 0,0* +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%OFA-2.0B-1.0*% +%ADD10C,0.050*% + +G04 Crosshairs to be on 0,0 * +X1000Y1000D02* +G54D10* +X3000Y1000D01* + +X2000Y0D02* +G54D10* +X2000Y2000D01* + +M02* diff --git a/examples/gerbv_test_files/test-layer-rotation-1.gbx b/examples/gerbv_test_files/test-layer-rotation-1.gbx new file mode 100755 index 0000000..d49e39c --- /dev/null +++ b/examples/gerbv_test_files/test-layer-rotation-1.gbx @@ -0,0 +1,23 @@ +G04 Test layer rotation 1 * +G04 Quarter star should be rotated 45 degrees counterclockwise, pointing* +G04 the center line straight up * +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%RO45*% +%ADD10C,0.025*% + +G04 Quarter star * +X1000Y0D02* +G54D10* +X2000Y0D01* + +X1000Y0D02* +G54D10* +X2000Y1000D01* + +X1000Y0D02* +G54D10* +X1000Y1000D01* + +M02* diff --git a/examples/gerbv_test_files/test-layer-scale-factor-1.gbx b/examples/gerbv_test_files/test-layer-scale-factor-1.gbx new file mode 100755 index 0000000..02a7b7d --- /dev/null +++ b/examples/gerbv_test_files/test-layer-scale-factor-1.gbx @@ -0,0 +1,17 @@ +G04 Test layer scale factor 1 * +G04 Crosshairs should be centered on 0,0 and 2 inches wide and 1 inch tall* +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%SFA2B1*% +%ADD10C,0.025*% + +G04 Crosshairs to be on 0,0 * +X-500Y0D02* +G54D10* +X500Y0D01* +X0Y-500D02* +G54D10* +X0Y500D01* + +M02* diff --git a/examples/gerbv_test_files/test-layer-step-and_repeat-1.gbx b/examples/gerbv_test_files/test-layer-step-and_repeat-1.gbx new file mode 100755 index 0000000..4f69391 --- /dev/null +++ b/examples/gerbv_test_files/test-layer-step-and_repeat-1.gbx @@ -0,0 +1,17 @@ +G04 Test step and repeat 1* +G04 Repeat a crosshair 3 times in the x direction and 2 times in the Y * +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%SRX3Y2I5.0J2*% +%ADD10C,0.050*% + +G04 Draw crosshairs * +X-1000Y0D02* +G54D10* +X1000Y0D01* +X0Y-1000D02* +G54D10* +X0Y1000D01* + +M02* diff --git a/examples/gerbv_test_files/test-layer-step-and_repeat-2.gbx b/examples/gerbv_test_files/test-layer-step-and_repeat-2.gbx new file mode 100755 index 0000000..392036f --- /dev/null +++ b/examples/gerbv_test_files/test-layer-step-and_repeat-2.gbx @@ -0,0 +1,18 @@ +G04 Test step and repeat 1* +G04 Repeat a crosshair 3 times in the x direction and 2 times in the Y * +G04 Handcoded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%SRX3Y2I1J1*% +%ADD10C,0.050*% + +G04 Draw a simple square* +G36* +G01X00400Y0D02* +X00600Y0D01* +X00600Y00200D01* +X00400Y00200D01* +X00400Y0D01* +G37* + +M02* diff --git a/examples/gerbv_test_files/test-polygon-fill-1.gbx b/examples/gerbv_test_files/test-polygon-fill-1.gbx new file mode 100755 index 0000000..ec25644 --- /dev/null +++ b/examples/gerbv_test_files/test-polygon-fill-1.gbx @@ -0,0 +1,45 @@ +G04 Test drawing with polygon apertures* +G04 Four small polygon fills aranged in a square +G04 Hand coded by Julian Lamb * +%MOIN*% +%FSLAX23Y23*% +%ADD10C,0.050*% + +G04 Draw a rectangle with a rounded right side* +G36* +G01X0Y0D02* +X00200Y0D01* +G75* +G03X00200Y00200I0J00100D01* +X0Y00200D01* +G04 Do not close with a final line, so let gerbv automatically close* +G37* + +G04 Draw a simple square* +G36* +G01X00400Y0D02* +X00600Y0D01* +X00600Y00200D01* +X00400Y00200D01* +X00400Y0D01* +G37* + +G04 Draw a small diamond* +G36* +G01X00100Y00300D02* +X00200Y00400D01* +X00100Y00500D01* +X0Y00400D01* +X00100Y00300D01* +G37* + +G04 Draw a very-narrow slit* +G36* +G01X00500Y00300D02* +X00510Y00300D01* +X00510Y00500D01* +X00500Y00500D01* +X00500Y00300D01* +G37* + +M02* diff --git a/examples/render_gerbv_tests.py b/examples/render_gerbv_tests.py new file mode 100755 index 0000000..d29ec11 --- /dev/null +++ b/examples/render_gerbv_tests.py @@ -0,0 +1,59 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright 2015 Hamilton Kibbe + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +""" +This example renders the gerber files from the gerbv test suite +""" + +import os +from gerber.rs274x import read as gerber_read +from gerber.excellon import read as excellon_read +from gerber.render import GerberCairoContext +from gerber.utils import listdir + +GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbv_test_files')) + +if not os.path.isdir(os.path.join(os.path.dirname(__file__), 'outputs')): + os.mkdir(os.path.join(os.path.dirname(__file__), 'outputs')) + +for infile in listdir(GERBER_FOLDER): + if infile.startswith('test'): + try: + outfile = os.path.splitext(infile)[0] + '.png' + if infile.endswith('gbx'): + layer = gerber_read(os.path.join(GERBER_FOLDER, infile)) + print("Loaded Gerber file: {}".format(infile)) + elif infile.endswith('exc'): + layer = excellon_read(os.path.join(GERBER_FOLDER, infile)) + print("Loaded Excellon file: {}".format(infile)) + else: + continue + + # Create a new drawing context + ctx = GerberCairoContext(1200) + ctx.color = (80./255, 80/255., 154/255.) + ctx.drill_color = ctx.color + + # Draw the layer, and specify the rendering settings to use + layer.render(ctx) + + # Write output to png file + print("Writing output to: {}".format(outfile)) + ctx.dump(os.path.join(os.path.dirname(__file__), 'outputs', outfile)) + except Exception as exc: + import traceback + traceback.print_exc() -- cgit From ca2143380fccf20e4815656e34677a67d85cea84 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 07:45:07 -0500 Subject: Add python3.5 to CI test versions --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 2deada8..b8cf3d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ python: - "2.7" - "3.3" - "3.4" + - "3.5" # command to install dependencies install: -- cgit From 41a7b90dff19b69ef03fed4104ecfdcbfcb21641 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 07:55:43 -0500 Subject: Excellon update --- gerber/excellon.py | 45 ++++++------ gerber/excellon_statements.py | 66 +++++++++--------- gerber/excellon_tool.py | 70 ++++++++++--------- gerber/tests/test_excellon.py | 154 ++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 242 insertions(+), 93 deletions(-) diff --git a/gerber/excellon.py b/gerber/excellon.py index 9825c5a..c3de948 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -100,12 +100,12 @@ class DrillHit(object): self.position = position def to_inch(self): - if self.tool.units == 'metric': + if self.tool.settings.units == 'metric': self.tool.to_inch() self.position = tuple(map(inch, self.position)) def to_metric(self): - if self.tool.units == 'inch': + if self.tool.settings.units == 'inch': self.tool.to_metric() self.position = tuple(map(metric, self.position)) @@ -120,7 +120,7 @@ class DrillHit(object): max_y = position[1] + radius return ((min_x, max_x), (min_y, max_y)) - def offset(self, x_offset, y_offset): + def offset(self, x_offset=0, y_offset=0): self.position = tuple(map(operator.add, self.position, (x_offset, y_offset))) def __str__(self): @@ -141,13 +141,13 @@ class DrillSlot(object): self.slot_type = slot_type def to_inch(self): - if self.tool.units == 'metric': + if self.tool.settings.units == 'metric': self.tool.to_inch() self.start = tuple(map(inch, self.start)) self.end = tuple(map(inch, self.end)) def to_metric(self): - if self.tool.units == 'inch': + if self.tool.settings.units == 'inch': self.tool.to_metric() self.start = tuple(map(metric, self.start)) self.end = tuple(map(metric, self.end)) @@ -163,7 +163,7 @@ class DrillSlot(object): max_y = max(start[1], end[1]) + radius return ((min_x, max_x), (min_y, max_y)) - def offset(self, x_offset, y_offset): + def offset(self, x_offset=0, y_offset=0): self.start = tuple(map(operator.add, self.start, (x_offset, y_offset))) self.end = tuple(map(operator.add, self.end, (x_offset, y_offset))) @@ -183,6 +183,7 @@ class ExcellonFile(CamFile): hits : list of tuples list of drill hits as (, (x, y)) + settings : dict Dictionary of gerber file settings @@ -211,16 +212,17 @@ class ExcellonFile(CamFile): primitives = [] for hit in self.hits: if isinstance(hit, DrillHit): - primitives.append(Drill(hit.position, hit.tool.diameter, hit, units=self.settings.units)) + primitives.append(Drill(hit.position, hit.tool.diameter, + units=self.settings.units)) elif isinstance(hit, DrillSlot): - primitives.append(Slot(hit.start, hit.end, hit.tool.diameter, hit, units=self.settings.units)) + primitives.append(Slot(hit.start, hit.end, hit.tool.diameter, + units=self.settings.units)) else: raise ValueError('Unknown hit type') - return primitives @property - def bounds(self): + def bounding_box(self): xmin = ymin = 100000000000 xmax = ymax = -100000000000 for hit in self.hits: @@ -282,29 +284,31 @@ class ExcellonFile(CamFile): Convert units to inches """ if self.units != 'inch': - self.units = 'inch' for statement in self.statements: statement.to_inch() for tool in iter(self.tools.values()): tool.to_inch() - for primitive in self.primitives: - primitive.to_inch() - for hit in self.hits: - hit.to_inch() + #for primitive in self.primitives: + # primitive.to_inch() + #for hit in self.hits: + # hit.to_inch() + self.units = 'inch' def to_metric(self): """ Convert units to metric """ if self.units != 'metric': - self.units = 'metric' for statement in self.statements: statement.to_metric() for tool in iter(self.tools.values()): tool.to_metric() - for primitive in self.primitives: - primitive.to_metric() + #for primitive in self.primitives: + # print("Converting to metric: {}".format(primitive)) + # primitive.to_metric() + # print(primitive) for hit in self.hits: hit.to_metric() + self.units = 'metric' def offset(self, x_offset=0, y_offset=0): for statement in self.statements: @@ -663,7 +667,8 @@ class ExcellonParser(object): if 'G85' in line: stmt = SlotStmt.from_excellon(line, self._settings()) - # I don't know if this is actually correct, but it makes sense that this is where the tool would end + # I don't know if this is actually correct, but it makes sense + # that this is where the tool would end x = stmt.x_end y = stmt.y_end @@ -835,7 +840,7 @@ def detect_excellon_format(data=None, filename=None): try: p = ExcellonParser(settings) ef = p.parse_raw(data) - size = tuple([t[0] - t[1] for t in ef.bounds]) + size = tuple([t[0] - t[1] for t in ef.bounding_box]) hole_area = 0.0 for hit in p.hits: tool = hit.tool diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index ac9c528..bcf35e4 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -113,16 +113,16 @@ class ExcellonTool(ExcellonStatement): hit_count : integer Number of tool hits in excellon file. """ - + PLATED_UNKNOWN = None PLATED_YES = 'plated' PLATED_NO = 'nonplated' PLATED_OPTIONAL = 'optional' - + @classmethod def from_tool(cls, tool): args = {} - + args['depth_offset'] = tool.depth_offset args['diameter'] = tool.diameter args['feed_rate'] = tool.feed_rate @@ -131,7 +131,7 @@ class ExcellonTool(ExcellonStatement): args['plated'] = tool.plated args['retract_rate'] = tool.retract_rate args['rpm'] = tool.rpm - + return cls(None, **args) @classmethod @@ -172,9 +172,9 @@ class ExcellonTool(ExcellonStatement): args['number'] = int(val) elif cmd == 'Z': args['depth_offset'] = parse_gerber_value(val, nformat, zero_suppression) - + if plated != ExcellonTool.PLATED_UNKNOWN: - # Sometimees we can can parse the + # Sometimees we can can parse the plating status args['plated'] = plated return cls(settings, **args) @@ -209,7 +209,7 @@ class ExcellonTool(ExcellonStatement): self.max_hit_count = kwargs.get('max_hit_count') self.depth_offset = kwargs.get('depth_offset') self.plated = kwargs.get('plated') - + self.hit_count = 0 def to_excellon(self, settings=None): @@ -249,15 +249,15 @@ class ExcellonTool(ExcellonStatement): def _hit(self): self.hit_count += 1 - + def equivalent(self, other): """ Is the other tool equal to this, ignoring the tool number, and other file specified properties """ - + if type(self) != type(other): return False - + return (self.diameter == other.diameter and self.feed_rate == other.feed_rate and self.retract_rate == other.retract_rate @@ -314,12 +314,12 @@ class ToolSelectionStmt(ExcellonStatement): if self.compensation_index is not None: stmt += '%02d' % self.compensation_index return stmt - + class NextToolSelectionStmt(ExcellonStatement): - + # TODO the statement exists outside of the context of the file, # so it is imposible to know that it is really the next tool - + def __init__(self, cur_tool, next_tool, **kwargs): """ Select the next tool in the wheel. @@ -329,10 +329,10 @@ class NextToolSelectionStmt(ExcellonStatement): next_tool : the that that is now selected """ super(NextToolSelectionStmt, self).__init__(**kwargs) - + self.cur_tool = cur_tool self.next_tool = next_tool - + def to_excellon(self, settings=None): stmt = 'M00' return stmt @@ -651,11 +651,11 @@ class EndOfProgramStmt(ExcellonStatement): class UnitStmt(ExcellonStatement): - + @classmethod def from_settings(cls, settings): """Create the unit statement from the FileSettings""" - + return cls(settings.units, settings.zeros) @classmethod @@ -742,7 +742,7 @@ class FormatStmt(ExcellonStatement): def to_excellon(self, settings=None): return 'FMAT,%d' % self.format - + @property def format_tuple(self): return (self.format, 6 - self.format) @@ -844,38 +844,38 @@ class UnknownStmt(ExcellonStatement): class SlotStmt(ExcellonStatement): """ G85 statement. Defines a slot created by multiple drills between two specified points. - + Format is two coordinates, split by G85in the middle, for example, XnY0nG85XnYn """ - + @classmethod def from_points(cls, start, end): - + return cls(start[0], start[1], end[0], end[1]) - + @classmethod def from_excellon(cls, line, settings, **kwargs): # Split the line based on the G85 separator sub_coords = line.split('G85') (x_start_coord, y_start_coord) = SlotStmt.parse_sub_coords(sub_coords[0], settings) (x_end_coord, y_end_coord) = SlotStmt.parse_sub_coords(sub_coords[1], settings) - + # Some files seem to specify only one of the coordinates if x_end_coord == None: x_end_coord = x_start_coord if y_end_coord == None: y_end_coord = y_start_coord - + c = cls(x_start_coord, y_start_coord, x_end_coord, y_end_coord, **kwargs) c.units = settings.units - return c - + return c + @staticmethod def parse_sub_coords(line, settings): - + x_coord = None y_coord = None - + if line[0] == 'X': splitline = line.strip('X').split('Y') x_coord = parse_gerber_value(splitline[0], settings.format, @@ -886,7 +886,7 @@ class SlotStmt(ExcellonStatement): else: y_coord = parse_gerber_value(line.strip(' Y'), settings.format, settings.zero_suppression) - + return (x_coord, y_coord) @@ -907,16 +907,16 @@ class SlotStmt(ExcellonStatement): if self.y_start is not None: stmt += 'Y%s' % write_gerber_value(self.y_start, settings.format, settings.zero_suppression) - + stmt += 'G85' - + if self.x_end is not None: stmt += 'X%s' % write_gerber_value(self.x_end, settings.format, settings.zero_suppression) if self.y_end is not None: stmt += 'Y%s' % write_gerber_value(self.y_end, settings.format, settings.zero_suppression) - + return stmt def to_inch(self): @@ -959,7 +959,7 @@ class SlotStmt(ExcellonStatement): start_str += 'X: %g ' % self.x_start if self.y_start is not None: start_str += 'Y: %g ' % self.y_start - + end_str = '' if self.x_end is not None: end_str += 'X: %g ' % self.x_end diff --git a/gerber/excellon_tool.py b/gerber/excellon_tool.py index bd76e54..a9ac450 100644 --- a/gerber/excellon_tool.py +++ b/gerber/excellon_tool.py @@ -28,9 +28,9 @@ try: from cStringIO import StringIO except(ImportError): from io import StringIO - + from .excellon_statements import ExcellonTool - + def loads(data, settings=None): """ Read tool file information and return a map of tools Parameters @@ -52,13 +52,13 @@ class ExcellonToolDefinitionParser(object): ---------- None """ - + allegro_tool = re.compile(r'(?P[0-9/.]+)\s+(?PP|N)\s+T(?P[0-9]{2})\s+(?P[0-9/.]+)\s+(?P[0-9/.]+)') allegro_comment_mils = re.compile('Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+') allegro2_comment_mils = re.compile('T(?P[0-9]{1,2}) Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+') allegro_comment_mm = re.compile('Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+') allegro2_comment_mm = re.compile('T(?P[0-9]{1,2}) Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+') - + matchers = [ (allegro_tool, 'mils'), (allegro_comment_mils, 'mils'), @@ -66,34 +66,34 @@ class ExcellonToolDefinitionParser(object): (allegro_comment_mm, 'mm'), (allegro2_comment_mm, 'mm'), ] - + def __init__(self, settings=None): self.tools = {} self.settings = settings - + def parse_raw(self, data): for line in StringIO(data): self._parse(line.strip()) - + return self.tools - + def _parse(self, line): - + for matcher in ExcellonToolDefinitionParser.matchers: m = matcher[0].match(line) if m: unit = matcher[1] - + size = float(m.group('size')) platedstr = m.group('plated') toolid = int(m.group('toolid')) xtol = float(m.group('xtol')) ytol = float(m.group('ytol')) - + size = self._convert_length(size, unit) xtol = self._convert_length(xtol, unit) ytol = self._convert_length(ytol, unit) - + if platedstr == 'PLATED': plated = ExcellonTool.PLATED_YES elif platedstr == 'NON_PLATED': @@ -102,19 +102,20 @@ class ExcellonToolDefinitionParser(object): plated = ExcellonTool.PLATED_OPTIONAL else: plated = ExcellonTool.PLATED_UNKNOWN - - tool = ExcellonTool(None, number=toolid, diameter=size, plated=plated) - + + tool = ExcellonTool(None, number=toolid, diameter=size, + plated=plated) + self.tools[tool.number] = tool - + break - + def _convert_length(self, value, unit): - + # Convert the value to mm if unit == 'mils': value /= 39.3700787402 - + # Now convert to the settings unit if self.settings.units == 'inch': return value / 25.4 @@ -137,34 +138,35 @@ def loads_rep(data, settings=None): return ExcellonReportParser(settings).parse_raw(data) class ExcellonReportParser(object): - + # We sometimes get files with different encoding, so we can't actually # match the text - the best we can do it detect the table header header = re.compile(r'====\s+====\s+====\s+====\s+=====\s+===') - + def __init__(self, settings=None): self.tools = {} self.settings = settings - + self.found_header = False - + def parse_raw(self, data): for line in StringIO(data): self._parse(line.strip()) - + return self.tools - + def _parse(self, line): - + # skip empty lines and "comments" if not line.strip(): return - + if not self.found_header: - # Try to find the heaader, since we need that to be sure we understand the contents correctly. + # Try to find the heaader, since we need that to be sure we + # understand the contents correctly. if ExcellonReportParser.header.match(line): self.found_header = True - + elif line[0] != '=': # Already found the header, so we know to to map the contents parts = line.split() @@ -180,7 +182,9 @@ class ExcellonReportParser(object): feedrate = int(parts[3]) speed = int(parts[4]) qty = int(parts[5]) - - tool = ExcellonTool(None, number=toolid, diameter=size, plated=plated, feed_rate=feedrate, rpm=speed) - - self.tools[tool.number] = tool \ No newline at end of file + + tool = ExcellonTool(None, number=toolid, diameter=size, + plated=plated, feed_rate=feedrate, + rpm=speed) + + self.tools[tool.number] = tool diff --git a/gerber/tests/test_excellon.py b/gerber/tests/test_excellon.py index 1402938..6cddb60 100644 --- a/gerber/tests/test_excellon.py +++ b/gerber/tests/test_excellon.py @@ -6,6 +6,7 @@ import os from ..cam import FileSettings from ..excellon import read, detect_excellon_format, ExcellonFile, ExcellonParser +from ..excellon import DrillHit, DrillSlot from ..excellon_statements import ExcellonTool from .tests import * @@ -50,29 +51,28 @@ def test_read_settings(): assert_equal(ncdrill.settings['zeros'], 'trailing') -def test_bounds(): +def test_bounding_box(): ncdrill = read(NCDRILL_FILE) - xbound, ybound = ncdrill.bounds + xbound, ybound = ncdrill.bounding_box assert_array_almost_equal(xbound, (0.1300, 2.1430)) assert_array_almost_equal(ybound, (0.3946, 1.7164)) def test_report(): ncdrill = read(NCDRILL_FILE) - + rprt = ncdrill.report() def test_conversion(): import copy ncdrill = read(NCDRILL_FILE) assert_equal(ncdrill.settings.units, 'inch') ncdrill_inch = copy.deepcopy(ncdrill) + ncdrill.to_metric() assert_equal(ncdrill.settings.units, 'metric') - inch_primitives = ncdrill_inch.primitives for tool in iter(ncdrill_inch.tools.values()): tool.to_metric() - for primitive in inch_primitives: - primitive.to_metric() + for statement in ncdrill_inch.statements: statement.to_metric() @@ -80,7 +80,8 @@ def test_conversion(): iter(ncdrill_inch.tools.values())): assert_equal(i_tool, m_tool) - for m, i in zip(ncdrill.primitives, inch_primitives): + for m, i in zip(ncdrill.primitives, ncdrill_inch.primitives): + assert_equal(m.position, i.position, '%s not equal to %s' % (m, i)) assert_equal(m.diameter, i.diameter, '%s not equal to %s' % (m, i)) @@ -197,3 +198,142 @@ def test_parse_unknown(): p = ExcellonParser(FileSettings()) p._parse_line('Not A Valid Statement') assert_equal(p.statements[0].stmt, 'Not A Valid Statement') + +def test_drill_hit_units_conversion(): + """ Test unit conversion for drill hits + """ + # Inch hit + settings = FileSettings(units='inch') + tool = ExcellonTool(settings, diameter=1.0) + hit = DrillHit(tool, (1.0, 1.0)) + + assert_equal(hit.tool.settings.units, 'inch') + assert_equal(hit.tool.diameter, 1.0) + assert_equal(hit.position, (1.0, 1.0)) + + # No Effect + hit.to_inch() + + assert_equal(hit.tool.settings.units, 'inch') + assert_equal(hit.tool.diameter, 1.0) + assert_equal(hit.position, (1.0, 1.0)) + + # Should convert + hit.to_metric() + + assert_equal(hit.tool.settings.units, 'metric') + assert_equal(hit.tool.diameter, 25.4) + assert_equal(hit.position, (25.4, 25.4)) + + # No Effect + hit.to_metric() + + assert_equal(hit.tool.settings.units, 'metric') + assert_equal(hit.tool.diameter, 25.4) + assert_equal(hit.position, (25.4, 25.4)) + + # Convert back to inch + hit.to_inch() + + assert_equal(hit.tool.settings.units, 'inch') + assert_equal(hit.tool.diameter, 1.0) + assert_equal(hit.position, (1.0, 1.0)) + +def test_drill_hit_offset(): + TEST_VECTORS = [ + ((0.0 ,0.0), (0.0, 1.0), (0.0, 1.0)), + ((0.0, 0.0), (1.0, 1.0), (1.0, 1.0)), + ((1.0, 1.0), (0.0, -1.0), (1.0, 0.0)), + ((1.0, 1.0), (-1.0, -1.0), (0.0, 0.0)), + + ] + for position, offset, expected in TEST_VECTORS: + settings = FileSettings(units='inch') + tool = ExcellonTool(settings, diameter=1.0) + hit = DrillHit(tool, position) + + assert_equal(hit.position, position) + + hit.offset(offset[0], offset[1]) + + assert_equal(hit.position, expected) + + +def test_drill_slot_units_conversion(): + """ Test unit conversion for drill hits + """ + # Inch hit + settings = FileSettings(units='inch') + tool = ExcellonTool(settings, diameter=1.0) + hit = DrillSlot(tool, (1.0, 1.0), (10.0, 10.0), DrillSlot.TYPE_ROUT) + + assert_equal(hit.tool.settings.units, 'inch') + assert_equal(hit.tool.diameter, 1.0) + assert_equal(hit.start, (1.0, 1.0)) + assert_equal(hit.end, (10.0, 10.0)) + + # No Effect + hit.to_inch() + + assert_equal(hit.tool.settings.units, 'inch') + assert_equal(hit.tool.diameter, 1.0) + assert_equal(hit.start, (1.0, 1.0)) + assert_equal(hit.end, (10.0, 10.0)) + + # Should convert + hit.to_metric() + + assert_equal(hit.tool.settings.units, 'metric') + assert_equal(hit.tool.diameter, 25.4) + assert_equal(hit.start, (25.4, 25.4)) + assert_equal(hit.end, (254.0, 254.0)) + + # No Effect + hit.to_metric() + + assert_equal(hit.tool.settings.units, 'metric') + assert_equal(hit.tool.diameter, 25.4) + assert_equal(hit.start, (25.4, 25.4)) + assert_equal(hit.end, (254.0, 254.0)) + + # Convert back to inch + hit.to_inch() + + assert_equal(hit.tool.settings.units, 'inch') + assert_equal(hit.tool.diameter, 1.0) + assert_equal(hit.start, (1.0, 1.0)) + assert_equal(hit.end, (10.0, 10.0)) + +def test_drill_slot_offset(): + TEST_VECTORS = [ + ((0.0 ,0.0), (1.0, 1.0), (0.0, 0.0), (0.0, 0.0), (1.0, 1.0)), + ((0.0, 0.0), (1.0, 1.0), (1.0, 0.0), (1.0, 0.0), (2.0, 1.0)), + ((0.0, 0.0), (1.0, 1.0), (1.0, 1.0), (1.0, 1.0), (2.0, 2.0)), + ((0.0, 0.0), (1.0, 1.0), (-1.0, 1.0), (-1.0, 1.0), (0.0, 2.0)), + ] + for start, end, offset, expected_start, expected_end in TEST_VECTORS: + settings = FileSettings(units='inch') + tool = ExcellonTool(settings, diameter=1.0) + slot = DrillSlot(tool, start, end, DrillSlot.TYPE_ROUT) + + assert_equal(slot.start, start) + assert_equal(slot.end, end) + + slot.offset(offset[0], offset[1]) + + assert_equal(slot.start, expected_start) + assert_equal(slot.end, expected_end) + +def test_drill_slot_bounds(): + TEST_VECTORS = [ + ((0.0, 0.0), (1.0, 1.0), 1.0, ((-0.5, 1.5), (-0.5, 1.5))), + ((0.0, 0.0), (1.0, 1.0), 0.5, ((-0.25, 1.25), (-0.25, 1.25))), + ] + for start, end, diameter, expected, in TEST_VECTORS: + settings = FileSettings(units='inch') + tool = ExcellonTool(settings, diameter=diameter) + slot = DrillSlot(tool, start, end, DrillSlot.TYPE_ROUT) + + assert_equal(slot.bounding_box, expected) + +#def test_exce -- cgit From c70ece73eaef13b755ce117f7b580ecd2d45e604 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 07:56:51 -0500 Subject: Add support for square holes in basic primitives --- gerber/gerber_statements.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 7322b3c..43596be 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -279,22 +279,36 @@ class ADParamStmt(ParamStmt): return cls('AD', dcode, 'R', ([width, height],)) @classmethod - def circle(cls, dcode, diameter, hole_diameter): + def circle(cls, dcode, diameter, hole_diameter=None, hole_width=None, hole_height=None): '''Create a circular aperture definition statement''' - if hole_diameter != None: + if hole_diameter is not None and hole_diameter > 0: return cls('AD', dcode, 'C', ([diameter, hole_diameter],)) + elif (hole_width is not None and hole_width > 0 + and hole_height is not None and hole_height > 0): + return cls('AD', dcode, 'C', ([diameter, hole_width, hole_height],)) return cls('AD', dcode, 'C', ([diameter],)) @classmethod - def obround(cls, dcode, width, height): + def obround(cls, dcode, width, height, hole_diameter=None, hole_width=None, hole_height=None): '''Create an obround aperture definition statement''' + if hole_diameter is not None and hole_diameter > 0: + return cls('AD', dcode, 'O', ([width, height, hole_diameter],)) + elif (hole_width is not None and hole_width > 0 + and hole_height is not None and hole_height > 0): + return cls('AD', dcode, 'O', ([width, height, hole_width, hole_height],)) return cls('AD', dcode, 'O', ([width, height],)) @classmethod - def polygon(cls, dcode, diameter, num_vertices, rotation, hole_diameter): + def polygon(cls, dcode, diameter, num_vertices, rotation, hole_diameter=None, hole_width=None, hole_height=None): '''Create a polygon aperture definition statement''' - return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation, hole_diameter],)) + if hole_diameter is not None and hole_diameter > 0: + return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation, hole_diameter],)) + elif (hole_width is not None and hole_width > 0 + and hole_height is not None and hole_height > 0): + return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation, hole_width, hole_height],)) + return cls('AD', dcode, 'P', ([diameter, num_vertices, rotation],)) + @classmethod def macro(cls, dcode, name): -- cgit From 6b672e98ff36b25e289c0d9e2ccc28337baa3c27 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 08:02:22 -0500 Subject: Add support for IF (Include File) rs274x command --- gerber/rs274x.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 5d64597..ff8addd 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -20,6 +20,7 @@ import copy import json +import os import re import sys @@ -146,7 +147,7 @@ class GerberFile(CamFile): return ((min_x, max_x), (min_y, max_y)) def write(self, filename, settings=None): - """ Write data out to a gerber file + """ Write data out to a gerber file. """ with open(filename, 'w') as f: for statement in self.statements: @@ -193,6 +194,9 @@ class GerberParser(object): AD_POLY = r"(?PAD)D(?P\d+)(?PP)[,](?P[^,%]*)" AD_MACRO = r"(?PAD)D(?P\d+)(?P{name})[,]?(?P[^,%]*)".format(name=NAME) AM = r"(?PAM)(?P{name})\*(?P[^%]*)".format(name=NAME) + # Include File + IF = r"(?PIF)(?P.*)" + # begin deprecated AS = r"(?PAS)(?P(AXBY)|(AYBX))" @@ -208,7 +212,7 @@ class GerberParser(object): # end deprecated PARAMS = (FS, MO, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_POLY, - AD_MACRO, AM, AS, IN, IP, IR, MI, OF, SF, LN) + AD_MACRO, AM, AS, IF, IN, IP, IR, MI, OF, SF, LN) PARAM_STMT = [re.compile(r"%?{0}\*%?".format(p)) for p in PARAMS] @@ -230,7 +234,11 @@ class GerberParser(object): REGION_MODE_STMT = re.compile(r'(?PG3[67])\*') QUAD_MODE_STMT = re.compile(r'(?PG7[45])\*') + # Keep include loop from crashing us + INCLUDE_FILE_RECURSION_LIMIT = 10 + def __init__(self): + self.filename = None self.settings = FileSettings() self.statements = [] self.primitives = [] @@ -248,13 +256,16 @@ class GerberParser(object): self.region_mode = 'off' self.quadrant_mode = 'multi-quadrant' self.step_and_repeat = (1, 1, 0, 0) + self._recursion_depth = 0 def parse(self, filename): + self.filename = filename with open(filename, "rU") as fp: data = fp.read() return self.parse_raw(data, filename) def parse_raw(self, data, filename=None): + self.filename = filename for stmt in self._parse(self._split_commands(data)): self.evaluate(stmt) self.statements.append(stmt) @@ -371,6 +382,17 @@ class GerberParser(object): yield stmt elif param["param"] == "OF": yield OFParamStmt.from_dict(param) + elif param["param"] == "IF": + # Don't crash on include loop + if self._recursion_depth < self.INCLUDE_FILE_RECURSION_LIMIT: + self._recursion_depth += 1 + with open(os.path.join(os.path.dirname(self.filename), param["filename"]), 'r') as f: + inc_data = f.read() + for stmt in self._parse(self._split_commands(inc_data)): + yield stmt + self._recursion_depth -= 1 + else: + raise IOError("Include file nesting depth limit exceeded.") elif param["param"] == "IN": yield INParamStmt.from_dict(param) elif param["param"] == "LN": -- cgit From a7f1f6ef0fdd9c792b3234931754dac5d81b15e5 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 08:05:57 -0500 Subject: Finish adding square hole support, fix some primitive calculations, etc. --- gerber/primitives.py | 232 ++++++++++++++++++++++++++++++--------------------- gerber/rs274x.py | 115 ++++++++++++++++++------- gerber/utils.py | 7 +- requirements.txt | 2 +- 4 files changed, 223 insertions(+), 133 deletions(-) diff --git a/gerber/primitives.py b/gerber/primitives.py index bd93e04..f583ca9 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -64,7 +64,6 @@ class Primitive(object): @property def flashed(self): '''Is this a flashed primitive''' - raise NotImplementedError('Is flashed must be ' 'implemented in subclass') @@ -271,9 +270,9 @@ class Line(Primitive): @property def vertices(self): if self._vertices is None: + start = self.start + end = self.end if isinstance(self.aperture, Rectangle): - start = self.start - end = self.end width = self.aperture.width height = self.aperture.height @@ -289,6 +288,11 @@ class Line(Primitive): # The line is defined by the convex hull of the points self._vertices = convex_hull((start_ll, start_lr, start_ul, start_ur, end_ll, end_lr, end_ul, end_ur)) + elif isinstance(self.aperture, Polygon): + points = [map(add, point, vertex) + for vertex in self.aperture.vertices + for point in (start, end)] + self._vertices = convex_hull(points) return self._vertices def offset(self, x_offset=0, y_offset=0): @@ -309,11 +313,18 @@ class Line(Primitive): return nearly_equal(self.start, equiv_start) and nearly_equal(self.end, equiv_end) + def __str__(self): + return "".format(self.start, self.end) + + def __repr__(self): + return str(self) + class Arc(Primitive): """ """ - def __init__(self, start, end, center, direction, aperture, quadrant_mode, **kwargs): + def __init__(self, start, end, center, direction, aperture, quadrant_mode, + **kwargs): super(Arc, self).__init__(**kwargs) self._start = start self._end = end @@ -371,15 +382,15 @@ class Arc(Primitive): @property def start_angle(self): - dy, dx = tuple([start - center for start, center + dx, dy = tuple([start - center for start, center in zip(self.start, self.center)]) - return math.atan2(dx, dy) + return math.atan2(dy, dx) @property def end_angle(self): - dy, dx = tuple([end - center for end, center + dx, dy = tuple([end - center for end, center in zip(self.end, self.center)]) - return math.atan2(dx, dy) + return math.atan2(dy, dx) @property def sweep_angle(self): @@ -399,77 +410,98 @@ class Arc(Primitive): theta0 = (self.start_angle + two_pi) % two_pi theta1 = (self.end_angle + two_pi) % two_pi points = [self.start, self.end] + if self.quadrant_mode == 'multi-quadrant': + if self.direction == 'counterclockwise': + # Passes through 0 degrees + if theta0 >= theta1: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if (((theta0 <= math.pi / 2.) and ((theta1 >= math.pi / 2.) or (theta1 <= theta0))) + or ((theta1 > math.pi / 2.) and (theta1 <= theta0))): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if ((theta0 <= math.pi and (theta1 >= math.pi or theta1 <= theta0)) + or ((theta1 > math.pi) and (theta1 <= theta0))): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + if (theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 <= theta0) + or ((theta1 > math.pi * 1.5) and (theta1 <= theta0))): + points.append((self.center[0], self.center[1] - self.radius)) + else: + # Passes through 0 degrees + if theta1 >= theta0: + points.append((self.center[0] + self.radius, self.center[1])) + # Passes through 90 degrees + if (((theta1 <= math.pi / 2.) and (theta0 >= math.pi / 2. or theta0 <= theta1)) + or ((theta0 > math.pi / 2.) and (theta0 <= theta1))): + points.append((self.center[0], self.center[1] + self.radius)) + # Passes through 180 degrees + if (((theta1 <= math.pi) and (theta0 >= math.pi or theta0 <= theta1)) + or ((theta0 > math.pi) and (theta0 <= theta1))): + points.append((self.center[0] - self.radius, self.center[1])) + # Passes through 270 degrees + if (((theta1 <= math.pi * 1.5) and (theta0 >= math.pi * 1.5 or theta0 <= theta1)) + or ((theta0 > math.pi * 1.5) and (theta0 <= theta1))): + points.append((self.center[0], self.center[1] - self.radius)) + x, y = zip(*points) + if hasattr(self.aperture, 'radius'): + min_x = min(x) - self.aperture.radius + max_x = max(x) + self.aperture.radius + min_y = min(y) - self.aperture.radius + max_y = max(y) + self.aperture.radius + else: + min_x = min(x) - self.aperture.width + max_x = max(x) + self.aperture.width + min_y = min(y) - self.aperture.height + max_y = max(y) + self.aperture.height + + self._bounding_box = ((min_x, max_x), (min_y, max_y)) + return self._bounding_box + + @property + def bounding_box_no_aperture(self): + '''Gets the bounding box without considering the aperture''' + two_pi = 2 * math.pi + theta0 = (self.start_angle + two_pi) % two_pi + theta1 = (self.end_angle + two_pi) % two_pi + points = [self.start, self.end] + if self.quadrant_mode == 'multi-quadrant': if self.direction == 'counterclockwise': # Passes through 0 degrees - if theta0 > theta1: + if theta0 >= theta1: points.append((self.center[0] + self.radius, self.center[1])) # Passes through 90 degrees - if theta0 <= math.pi / \ - 2. and (theta1 >= math.pi / 2. or theta1 < theta0): + if (((theta0 <= math.pi / 2.) and ( + (theta1 >= math.pi / 2.) or (theta1 <= theta0))) + or ((theta1 > math.pi / 2.) and (theta1 <= theta0))): points.append((self.center[0], self.center[1] + self.radius)) # Passes through 180 degrees - if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0): + if ((theta0 <= math.pi and (theta1 >= math.pi or theta1 <= theta0)) + or ((theta1 > math.pi) and (theta1 <= theta0))): points.append((self.center[0] - self.radius, self.center[1])) # Passes through 270 degrees - if theta0 <= math.pi * \ - 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0): + if (theta0 <= math.pi * 1.5 and ( + theta1 >= math.pi * 1.5 or theta1 <= theta0) + or ((theta1 > math.pi * 1.5) and (theta1 <= theta0))): points.append((self.center[0], self.center[1] - self.radius)) else: # Passes through 0 degrees - if theta1 > theta0: + if theta1 >= theta0: points.append((self.center[0] + self.radius, self.center[1])) # Passes through 90 degrees - if theta1 <= math.pi / \ - 2. and (theta0 >= math.pi / 2. or theta0 < theta1): + if (((theta1 <= math.pi / 2.) and ( + theta0 >= math.pi / 2. or theta0 <= theta1)) + or ((theta0 > math.pi / 2.) and (theta0 <= theta1))): points.append((self.center[0], self.center[1] + self.radius)) # Passes through 180 degrees - if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1): + if (((theta1 <= math.pi) and (theta0 >= math.pi or theta0 <= theta1)) + or ((theta0 > math.pi) and (theta0 <= theta1))): points.append((self.center[0] - self.radius, self.center[1])) # Passes through 270 degrees - if theta1 <= math.pi * \ - 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1): + if (((theta1 <= math.pi * 1.5) and ( + theta0 >= math.pi * 1.5 or theta0 <= theta1)) + or ((theta0 > math.pi * 1.5) and (theta0 <= theta1))): points.append((self.center[0], self.center[1] - self.radius)) - x, y = zip(*points) - min_x = min(x) - self.aperture.radius - max_x = max(x) + self.aperture.radius - min_y = min(y) - self.aperture.radius - max_y = max(y) + self.aperture.radius - self._bounding_box = ((min_x, max_x), (min_y, max_y)) - return self._bounding_box - - @property - def bounding_box_no_aperture(self): - '''Gets the bounding box without considering the aperture''' - two_pi = 2 * math.pi - theta0 = (self.start_angle + two_pi) % two_pi - theta1 = (self.end_angle + two_pi) % two_pi - points = [self.start, self.end] - if self.direction == 'counterclockwise': - # Passes through 0 degrees - if theta0 > theta1: - points.append((self.center[0] + self.radius, self.center[1])) - # Passes through 90 degrees - if theta0 <= math.pi / 2. and (theta1 >= math.pi / 2. or theta1 < theta0): - points.append((self.center[0], self.center[1] + self.radius)) - # Passes through 180 degrees - if theta0 <= math.pi and (theta1 >= math.pi or theta1 < theta0): - points.append((self.center[0] - self.radius, self.center[1])) - # Passes through 270 degrees - if theta0 <= math.pi * 1.5 and (theta1 >= math.pi * 1.5 or theta1 < theta0): - points.append((self.center[0], self.center[1] - self.radius )) - else: - # Passes through 0 degrees - if theta1 > theta0: - points.append((self.center[0] + self.radius, self.center[1])) - # Passes through 90 degrees - if theta1 <= math.pi / 2. and (theta0 >= math.pi / 2. or theta0 < theta1): - points.append((self.center[0], self.center[1] + self.radius)) - # Passes through 180 degrees - if theta1 <= math.pi and (theta0 >= math.pi or theta0 < theta1): - points.append((self.center[0] - self.radius, self.center[1])) - # Passes through 270 degrees - if theta1 <= math.pi * 1.5 and (theta0 >= math.pi * 1.5 or theta0 < theta1): - points.append((self.center[0], self.center[1] - self.radius )) x, y = zip(*points) min_x = min(x) @@ -489,13 +521,16 @@ class Circle(Primitive): """ """ - def __init__(self, position, diameter, hole_diameter = None, **kwargs): + def __init__(self, position, diameter, hole_diameter=None, + hole_width=0, hole_height=0, **kwargs): super(Circle, self).__init__(**kwargs) validate_coordinates(position) self._position = position self._diameter = diameter self.hole_diameter = hole_diameter - self._to_convert = ['position', 'diameter', 'hole_diameter'] + self.hole_width = hole_width + self.hole_height = hole_height + self._to_convert = ['position', 'diameter', 'hole_diameter', 'hole_width', 'hole_height'] @property def flashed(self): @@ -631,14 +666,18 @@ class Rectangle(Primitive): then you don't need to worry about rotation """ - def __init__(self, position, width, height, hole_diameter=0, **kwargs): + def __init__(self, position, width, height, hole_diameter=0, + hole_width=0, hole_height=0, **kwargs): super(Rectangle, self).__init__(**kwargs) validate_coordinates(position) self._position = position self._width = width self._height = height self.hole_diameter = hole_diameter - self._to_convert = ['position', 'width', 'height', 'hole_diameter'] + self.hole_width = hole_width + self.hole_height = hole_height + self._to_convert = ['position', 'width', 'height', 'hole_diameter', + 'hole_width', 'hole_height'] # TODO These are probably wrong when rotated self._lower_left = None self._upper_right = None @@ -736,6 +775,12 @@ class Rectangle(Primitive): return nearly_equal(self.position, equiv_position) + def __str__(self): + return "".format(self.width, self.height, self.rotation * 180/math.pi) + + def __repr__(self): + return self.__str__() + class Diamond(Primitive): """ @@ -898,7 +943,8 @@ class ChamferRectangle(Primitive): ((self.position[0] - delta_w), (self.position[1] - delta_h)), ((self.position[0] + delta_w), (self.position[1] - delta_h)) ] - for idx, corner, chamfered in enumerate((rect_corners, self.corners)): + for idx, params in enumerate(zip(rect_corners, self.corners)): + corner, chamfered = params x, y = corner if chamfered: if idx == 0: @@ -1019,14 +1065,18 @@ class Obround(Primitive): """ """ - def __init__(self, position, width, height, hole_diameter=0, **kwargs): + def __init__(self, position, width, height, hole_diameter=0, + hole_width=0,hole_height=0, **kwargs): super(Obround, self).__init__(**kwargs) validate_coordinates(position) self._position = position self._width = width self._height = height self.hole_diameter = hole_diameter - self._to_convert = ['position', 'width', 'height', 'hole_diameter'] + self.hole_width = hole_width + self.hole_height = hole_height + self._to_convert = ['position', 'width', 'height', 'hole_diameter', + 'hole_width', 'hole_height' ] @property def flashed(self): @@ -1116,14 +1166,18 @@ class Polygon(Primitive): """ Polygon flash defined by a set number of sides. """ - def __init__(self, position, sides, radius, hole_diameter, **kwargs): + def __init__(self, position, sides, radius, hole_diameter=0, + hole_width=0, hole_height=0, **kwargs): super(Polygon, self).__init__(**kwargs) validate_coordinates(position) self._position = position self.sides = sides self._radius = radius self.hole_diameter = hole_diameter - self._to_convert = ['position', 'radius', 'hole_diameter'] + self.hole_width = hole_width + self.hole_height = hole_height + self._to_convert = ['position', 'radius', 'hole_diameter', + 'hole_width', 'hole_height'] @property def flashed(self): @@ -1174,25 +1228,14 @@ class Polygon(Primitive): def vertices(self): offset = self.rotation - da = 360.0 / self.sides + delta_angle = 360.0 / self.sides points = [] - for i in xrange(self.sides): - points.append(rotate_point((self.position[0] + self.radius, self.position[1]), offset + da * i, self.position)) - + for i in range(self.sides): + points.append( + rotate_point((self.position[0] + self.radius, self.position[1]), offset + delta_angle * i, self.position)) return points - @property - def vertices(self): - if self._vertices is None: - theta = math.radians(360/self.sides) - vertices = [(self.position[0] + (math.cos(theta * side) * self.radius), - self.position[1] + (math.sin(theta * side) * self.radius)) - for side in range(self.sides)] - self._vertices = [(((x * self._cos_theta) - (y * self._sin_theta)), - ((x * self._sin_theta) + (y * self._cos_theta))) - for x, y in vertices] - return self._vertices def equivalent(self, other, offset): """ @@ -1555,15 +1598,12 @@ class SquareRoundDonut(Primitive): class Drill(Primitive): """ A drill hole """ - def __init__(self, position, diameter, hit, **kwargs): + def __init__(self, position, diameter, **kwargs): super(Drill, self).__init__('dark', **kwargs) validate_coordinates(position) self._position = position self._diameter = diameter - self.hit = hit - self._to_convert = ['position', 'diameter', 'hit'] - - # TODO Ths won't handle the hit updates correctly + self._to_convert = ['position', 'diameter'] @property def flashed(self): @@ -1606,23 +1646,21 @@ class Drill(Primitive): self.position = tuple(map(add, self.position, (x_offset, y_offset))) def __str__(self): - return '' % (self.diameter, self.position[0], self.position[1], self.hit) + return '' % (self.diameter, self.units, self.position[0], self.position[1]) class Slot(Primitive): """ A drilled slot """ - def __init__(self, start, end, diameter, hit, **kwargs): + def __init__(self, start, end, diameter, **kwargs): super(Slot, self).__init__('dark', **kwargs) validate_coordinates(start) validate_coordinates(end) self.start = start self.end = end self.diameter = diameter - self.hit = hit - self._to_convert = ['start', 'end', 'diameter', 'hit'] + self._to_convert = ['start', 'end', 'diameter'] - # TODO this needs to use cached bounding box @property def flashed(self): @@ -1630,8 +1668,8 @@ class Slot(Primitive): def bounding_box(self): if self._bounding_box is None: - ll = tuple([c - self.outer_diameter / 2. for c in self.position]) - ur = tuple([c + self.outer_diameter / 2. for c in self.position]) + ll = tuple([c - self.diameter / 2. for c in self.position]) + ur = tuple([c + self.diameter / 2. for c in self.position]) self._bounding_box = ((ll[0], ur[0]), (ll[1], ur[1])) return self._bounding_box diff --git a/gerber/rs274x.py b/gerber/rs274x.py index ff8addd..5191fb7 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -514,32 +514,51 @@ class GerberParser(object): if shape == 'C': diameter = modifiers[0][0] - if len(modifiers[0]) >= 2: + hole_diameter = 0 + rectangular_hole = (0, 0) + if len(modifiers[0]) == 2: hole_diameter = modifiers[0][1] - else: - hole_diameter = None + elif len(modifiers[0]) == 3: + rectangular_hole = modifiers[0][1:3] + + aperture = Circle(position=None, diameter=diameter, + hole_diameter=hole_diameter, + hole_width=rectangular_hole[0], + hole_height=rectangular_hole[1], + units=self.settings.units) - aperture = Circle(position=None, diameter=diameter, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'R': width = modifiers[0][0] height = modifiers[0][1] - if len(modifiers[0]) >= 3: + hole_diameter = 0 + rectangular_hole = (0, 0) + if len(modifiers[0]) == 3: hole_diameter = modifiers[0][2] - else: - hole_diameter = None - - aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) + elif len(modifiers[0]) == 4: + rectangular_hole = modifiers[0][2:4] + + aperture = Rectangle(position=None, width=width, height=height, + hole_diameter=hole_diameter, + hole_width=rectangular_hole[0], + hole_height=rectangular_hole[1], + units=self.settings.units) elif shape == 'O': width = modifiers[0][0] height = modifiers[0][1] - if len(modifiers[0]) >= 3: + hole_diameter = 0 + rectangular_hole = (0, 0) + if len(modifiers[0]) == 3: hole_diameter = modifiers[0][2] - else: - hole_diameter = None - - aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) + elif len(modifiers[0]) == 4: + rectangular_hole = modifiers[0][2:4] + + aperture = Obround(position=None, width=width, height=height, + hole_diameter=hole_diameter, + hole_width=rectangular_hole[0], + hole_height=rectangular_hole[1], + units=self.settings.units) elif shape == 'P': outer_diameter = modifiers[0][0] number_vertices = int(modifiers[0][1]) @@ -548,11 +567,19 @@ class GerberParser(object): else: rotation = 0 - if len(modifiers[0]) > 3: + hole_diameter = 0 + rectangular_hole = (0, 0) + if len(modifiers[0]) == 4: hole_diameter = modifiers[0][3] - else: - hole_diameter = None - aperture = Polygon(position=None, sides=number_vertices, radius=outer_diameter/2.0, hole_diameter=hole_diameter, rotation=rotation) + elif len(modifiers[0]) >= 5: + rectangular_hole = modifiers[0][3:5] + + aperture = Polygon(position=None, sides=number_vertices, + radius=outer_diameter/2.0, + hole_diameter=hole_diameter, + hole_width=rectangular_hole[0], + hole_height=rectangular_hole[1], + rotation=rotation) else: aperture = self.macros[shape].build(modifiers) @@ -663,13 +690,18 @@ class GerberParser(object): quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units)) + # Gerbv seems to reset interpolation mode in regions.. + # TODO: Make sure this is right. + self.interpolation = 'linear' elif self.op == "D02" or self.op == "D2": if self.region_mode == "on": # D02 in the middle of a region finishes that region and starts a new one if self.current_region and len(self.current_region) > 1: - self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity, units=self.settings.units)) + self.primitives.append(Region(self.current_region, + level_polarity=self.level_polarity, + units=self.settings.units)) self.current_region = None elif self.op == "D03" or self.op == "D3": @@ -694,29 +726,53 @@ class GerberParser(object): def _find_center(self, start, end, offsets): """ - In single quadrant mode, the offsets are always positive, which means there are 4 possible centers. - The correct center is the only one that results in an arc with sweep angle of less than or equal to 90 degrees + In single quadrant mode, the offsets are always positive, which means + there are 4 possible centers. The correct center is the only one that + results in an arc with sweep angle of less than or equal to 90 degrees + in the specified direction """ - + two_pi = 2 * math.pi if self.quadrant_mode == 'single-quadrant': + # The Gerber spec says single quadrant only has one possible center, + # and you can detect it based on the angle. But for real files, this + # seems to work better - there is usually only one option that makes + # sense for the center (since the distance should be the same + # from start and end). We select the center with the least error in + # radius from all the options with a valid sweep angle. - # The Gerber spec says single quadrant only has one possible center, and you can detect - # based on the angle. But for real files, this seems to work better - there is usually - # only one option that makes sense for the center (since the distance should be the same - # from start and end). Find the center that makes the most sense sqdist_diff_min = sys.maxint center = None for factors in [(1, 1), (1, -1), (-1, 1), (-1, -1)]: - test_center = (start[0] + offsets[0] * factors[0], start[1] + offsets[1] * factors[1]) + test_center = (start[0] + offsets[0] * factors[0], + start[1] + offsets[1] * factors[1]) + + # Find angle from center to start and end points + start_angle = math.atan2(*reversed([_start - _center for _start, _center in zip(start, test_center)])) + end_angle = math.atan2(*reversed([_end - _center for _end, _center in zip(end, test_center)])) + # Clamp angles to 0, 2pi + theta0 = (start_angle + two_pi) % two_pi + theta1 = (end_angle + two_pi) % two_pi + + # Determine sweep angle in the current arc direction + if self.direction == 'counterclockwise': + sweep_angle = abs(theta1 - theta0) + else: + theta0 += two_pi + sweep_angle = abs(theta0 - theta1) % two_pi + + # Calculate the radius error sqdist_start = sq_distance(start, test_center) sqdist_end = sq_distance(end, test_center) - if abs(sqdist_start - sqdist_end) < sqdist_diff_min: + # Take the option with the lowest radius error from the set of + # options with a valid sweep angle + if ((abs(sqdist_start - sqdist_end) < sqdist_diff_min) + and (sweep_angle >= 0) + and (sweep_angle <= math.pi / 2.0)): center = test_center sqdist_diff_min = abs(sqdist_start - sqdist_end) - return center else: return (start[0] + offsets[0], start[1] + offsets[1]) @@ -724,7 +780,6 @@ class GerberParser(object): def _evaluate_aperture(self, stmt): self.aperture = stmt.d - def _match_one(expr, data): match = expr.match(data) if match is None: diff --git a/gerber/utils.py b/gerber/utils.py index c62ad2a..06adfd7 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -25,9 +25,7 @@ files. import os from math import radians, sin, cos -from operator import sub -from copy import deepcopy -from pyhull.convex_hull import ConvexHull +from scipy.spatial import ConvexHull MILLIMETERS_PER_INCH = 25.4 @@ -344,5 +342,4 @@ def listdir(directory, ignore_hidden=True, ignore_os=True): def convex_hull(points): vertices = ConvexHull(points).vertices - return [points[idx] for idx in - set([point for pair in vertices for point in pair])] + return [points[idx] for idx in vertices] diff --git a/requirements.txt b/requirements.txt index 014e92b..e049232 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ ## The following requirements were added by pip --freeze: cairocffi==0.6 -pyhull==1.5.6 +scipy -- cgit From 5696fc7064af674d02cf84cf7934c1ac7446259e Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 08:09:03 -0500 Subject: Fix a bunch of bugs in rendering that showed up when rendering the gerbv test suite --- gerber/render/cairo_backend.py | 488 +++++++++++++++++++++++++---------------- 1 file changed, 305 insertions(+), 183 deletions(-) diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index 31a1e77..a2baa47 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright 2014 Hamilton Kibbe @@ -29,6 +29,7 @@ import os from .render import GerberContext, RenderSettings from .theme import THEMES from ..primitives import * +from ..utils import rotate_point from io import BytesIO @@ -67,16 +68,13 @@ class GerberCairoContext(GerberContext): size_in_pixels = self.scale_point(size_in_inch) self.origin_in_inch = origin_in_inch if self.origin_in_inch is None else self.origin_in_inch self.size_in_inch = size_in_inch if self.size_in_inch is None else self.size_in_inch + self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0, + x0=-self.origin_in_pixels[0], + y0=self.size_in_pixels[1]) if (self.surface is None) or new_surface: self.surface_buffer = tempfile.NamedTemporaryFile() self.surface = cairo.SVGSurface(self.surface_buffer, size_in_pixels[0], size_in_pixels[1]) self.output_ctx = cairo.Context(self.surface) - self.output_ctx.scale(1, -1) - self.output_ctx.translate(-(origin_in_inch[0] * self.scale[0]), - (-origin_in_inch[1] * self.scale[0]) - size_in_pixels[1]) - self._xform_matrix = cairo.Matrix(xx=1.0, yy=-1.0, - x0=-self.origin_in_pixels[0], - y0=self.size_in_pixels[1] + self.origin_in_pixels[1]) def render_layer(self, layer, filename=None, settings=None, bgsettings=None, verbose=False): @@ -155,6 +153,23 @@ class GerberCairoContext(GerberContext): self.surface_buffer.close() self.surface_buffer = None + def _new_mask(self): + class Mask: + def __enter__(msk): + size_in_pixels = self.size_in_pixels + msk.surface = cairo.SVGSurface(None, size_in_pixels[0], + size_in_pixels[1]) + msk.ctx = cairo.Context(msk.surface) + msk.ctx.translate(-self.origin_in_pixels[0], -self.origin_in_pixels[1]) + return msk + + + def __exit__(msk, exc_type, exc_val, traceback): + if hasattr(msk.surface, 'finish'): + msk.surface.finish() + + return Mask() + def _render_layer(self, layer, settings): self.invert = settings.invert # Get a new clean layer to render on @@ -167,31 +182,36 @@ class GerberCairoContext(GerberContext): def _render_line(self, line, color): start = [pos * scale for pos, scale in zip(line.start, self.scale)] end = [pos * scale for pos, scale in zip(line.end, self.scale)] - self.ctx.set_operator(cairo.OPERATOR_SOURCE - if line.level_polarity == 'dark' and - (not self.invert) else cairo.OPERATOR_CLEAR) - if isinstance(line.aperture, Circle): - width = line.aperture.diameter - self.ctx.set_line_width(width * self.scale[0]) - self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - self.ctx.move_to(*start) - self.ctx.line_to(*end) - self.ctx.stroke() - elif isinstance(line.aperture, Rectangle): - points = [self.scale_point(x) for x in line.vertices] - self.ctx.set_line_width(0) - self.ctx.move_to(*points[0]) - for point in points[1:]: - self.ctx.line_to(*point) - self.ctx.fill() + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and line.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._new_mask() as mask: + if isinstance(line.aperture, Circle): + width = line.aperture.diameter + mask.ctx.set_line_width(width * self.scale[0]) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + mask.ctx.move_to(*start) + mask.ctx.line_to(*end) + mask.ctx.stroke() + + elif hasattr(line, 'vertices') and line.vertices is not None: + points = [self.scale_point(x) for x in line.vertices] + mask.ctx.set_line_width(0) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_arc(self, arc, color): center = self.scale_point(arc.center) start = self.scale_point(arc.start) end = self.scale_point(arc.end) radius = self.scale[0] * arc.radius - angle1 = arc.start_angle - angle2 = arc.end_angle + two_pi = 2 * math.pi + angle1 = (arc.start_angle + two_pi) % two_pi + angle2 = (arc.end_angle + two_pi) % two_pi if angle1 == angle2 and arc.quadrant_mode != 'single-quadrant': # Make the angles slightly different otherwise Cario will draw nothing angle2 -= 0.000000001 @@ -200,61 +220,111 @@ class GerberCairoContext(GerberContext): else: width = max(arc.aperture.width, arc.aperture.height, 0.001) - self.ctx.set_operator(cairo.OPERATOR_SOURCE - if arc.level_polarity == 'dark' and - (not self.invert) else cairo.OPERATOR_CLEAR) - self.ctx.set_line_width(width * self.scale[0]) - self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - self.ctx.move_to(*start) # You actually have to do this... - if arc.direction == 'counterclockwise': - self.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) - else: - self.ctx.arc_negative(*center, radius=radius, - angle1=angle1, angle2=angle2) - self.ctx.move_to(*end) # ...lame + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and arc.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + + with self._new_mask() as mask: + mask.ctx.set_line_width(width * self.scale[0]) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND if isinstance(arc.aperture, Circle) else cairo.LINE_CAP_SQUARE) + mask.ctx.move_to(*start) # You actually have to do this... + if arc.direction == 'counterclockwise': + mask.ctx.arc(*center, radius=radius, angle1=angle1, angle2=angle2) + else: + mask.ctx.arc_negative(*center, radius=radius, + angle1=angle1, angle2=angle2) + mask.ctx.move_to(*end) # ...lame + mask.ctx.stroke() + + #if isinstance(arc.aperture, Rectangle): + # print("Flash Rectangle Ends") + # print(arc.aperture.rotation * 180/math.pi) + # rect = arc.aperture + # width = self.scale[0] * rect.width + # height = self.scale[1] * rect.height + # for point, angle in zip((start, end), (angle1, angle2)): + # print("{} w {} h{}".format(point, rect.width, rect.height)) + # mask.ctx.rectangle(point[0] - width/2.0, + # point[1] - height/2.0, width, height) + # mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) + def _render_region(self, region, color): - self.ctx.set_operator(cairo.OPERATOR_SOURCE - if region.level_polarity == 'dark' and - (not self.invert) else cairo.OPERATOR_CLEAR) - self.ctx.set_line_width(0) - self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - self.ctx.move_to(*self.scale_point(region.primitives[0].start)) - for prim in region.primitives: - if isinstance(prim, Line): - self.ctx.line_to(*self.scale_point(prim.end)) - else: - center = self.scale_point(prim.center) - radius = self.scale[0] * prim.radius - angle1 = prim.start_angle - angle2 = prim.end_angle - if prim.direction == 'counterclockwise': - self.ctx.arc(*center, radius=radius, - angle1=angle1, angle2=angle2) + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) and region.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._new_mask() as mask: + mask.ctx.set_line_width(0) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + mask.ctx.move_to(*self.scale_point(region.primitives[0].start)) + for prim in region.primitives: + if isinstance(prim, Line): + mask.ctx.line_to(*self.scale_point(prim.end)) else: - self.ctx.arc_negative(*center, radius=radius, - angle1=angle1, angle2=angle2) - self.ctx.fill() + center = self.scale_point(prim.center) + radius = self.scale[0] * prim.radius + angle1 = prim.start_angle + angle2 = prim.end_angle + if prim.direction == 'counterclockwise': + mask.ctx.arc(*center, radius=radius, + angle1=angle1, angle2=angle2) + else: + mask.ctx.arc_negative(*center, radius=radius, + angle1=angle1, angle2=angle2) + mask.ctx.fill() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_circle(self, circle, color): center = self.scale_point(circle.position) - self.ctx.set_operator(cairo.OPERATOR_SOURCE - if circle.level_polarity == 'dark' and - (not self.invert) else cairo.OPERATOR_CLEAR) - self.ctx.set_line_width(0) - self.ctx.arc(*center, radius=(circle.radius * self.scale[0]), angle1=0, - angle2=(2 * math.pi)) - self.ctx.fill() - - if circle.hole_diameter > 0: - # Render the center clear - - self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) - self.ctx.arc(center[0], center[1], - radius=circle.hole_radius * self.scale[0], angle1=0, - angle2=2 * math.pi) - self.ctx.fill() + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and circle.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + + with self._new_mask() as mask: + mask.ctx.set_line_width(0) + mask.ctx.arc(center[0], + center[1], + radius=(circle.radius * self.scale[0]), + angle1=0, + angle2=(2 * math.pi)) + mask.ctx.fill() + + if hasattr(circle, 'hole_diameter') and circle.hole_diameter > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR) + mask.ctx.arc(center[0], + center[1], + radius=circle.hole_radius * self.scale[0], + angle1=0, + angle2=2 * math.pi) + mask.ctx.fill() + + if (hasattr(circle, 'hole_width') and hasattr(circle, 'hole_height') + and circle.hole_width > 0 and circle.hole_height > 0): + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if circle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height = self.scale_point((circle.hole_width, circle.hole_height)) + lower_left = rotate_point( + (center[0] - width / 2.0, center[1] - height / 2.0), + circle.rotation, center) + lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), + circle.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), + circle.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), + circle.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_rectangle(self, rectangle, color): @@ -262,101 +332,156 @@ class GerberCairoContext(GerberContext): width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))]) - self.ctx.set_operator(cairo.OPERATOR_SOURCE - if rectangle.level_polarity == 'dark' and - (not self.invert) else cairo.OPERATOR_CLEAR) - - if rectangle.rotation != 0: - self.ctx.save() - - center = map(mul, rectangle.position, self.scale) - matrix = cairo.Matrix() - matrix.translate(center[0], center[1]) - # For drawing, we already handles the translation - lower_left[0] = lower_left[0] - center[0] - lower_left[1] = lower_left[1] - center[1] - matrix.rotate(rectangle.rotation) - self.ctx.transform(matrix) - - if rectangle.hole_diameter > 0: - self.ctx.push_group() - - self.ctx.set_line_width(0) - self.ctx.rectangle(*lower_left, width=width, height=height) - self.ctx.fill() - - if rectangle.hole_diameter > 0: - # Render the center clear - self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - self.ctx.set_operator(cairo.OPERATOR_CLEAR - if rectangle.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_SOURCE) - center = map(mul, rectangle.position, self.scale) - self.ctx.arc(center[0], center[1], - radius=rectangle.hole_radius * self.scale[0], angle1=0, - angle2=2 * math.pi) - self.ctx.fill() - - if rectangle.rotation != 0: - self.ctx.restore() + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and rectangle.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._new_mask() as mask: + + mask.ctx.set_line_width(0) + mask.ctx.rectangle(*lower_left, width=width, height=height) + mask.ctx.fill() + + center = self.scale_point(rectangle.position) + if rectangle.hole_diameter > 0: + # Render the center clear + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if rectangle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + + mask.ctx.arc(center[0], center[1], + radius=rectangle.hole_radius * self.scale[0], angle1=0, + angle2=2 * math.pi) + mask.ctx.fill() + + if rectangle.hole_width > 0 and rectangle.hole_height > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if rectangle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height = self.scale_point((rectangle.hole_width, rectangle.hole_height)) + lower_left = rotate_point((center[0] - width/2.0, center[1] - height/2.0), rectangle.rotation, center) + lower_right = rotate_point((center[0] + width/2.0, center[1] - height/2.0), rectangle.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), rectangle.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), rectangle.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_obround(self, obround, color): - if obround.hole_diameter > 0: - self.ctx.push_group() - - self._render_circle(obround.subshapes['circle1'], color) - self._render_circle(obround.subshapes['circle2'], color) - self._render_rectangle(obround.subshapes['rectangle'], color) - - if obround.hole_diameter > 0: - # Render the center clear - self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - self.ctx.set_operator(cairo.OPERATOR_CLEAR) - center = map(mul, obround.position, self.scale) - self.ctx.arc(center[0], center[1], - radius=obround.hole_radius * self.scale[0], angle1=0, - angle2=2 * math.pi) - self.ctx.fill() - - self.ctx.pop_group_to_source() - self.ctx.paint_with_alpha(1) - + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and obround.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._new_mask() as mask: + mask.ctx.set_line_width(0) + + # Render circles + for circle in (obround.subshapes['circle1'], obround.subshapes['circle2']): + center = self.scale_point(circle.position) + mask.ctx.arc(center[0], + center[1], + radius=(circle.radius * self.scale[0]), + angle1=0, + angle2=(2 * math.pi)) + mask.ctx.fill() + + # Render Rectangle + rectangle = obround.subshapes['rectangle'] + lower_left = self.scale_point(rectangle.lower_left) + width, height = tuple([abs(coord) for coord in + self.scale_point((rectangle.width, + rectangle.height))]) + mask.ctx.rectangle(*lower_left, width=width, height=height) + mask.ctx.fill() + + center = self.scale_point(obround.position) + if obround.hole_diameter > 0: + # Render the center clear + mask.ctx.set_operator(cairo.OPERATOR_CLEAR) + mask.ctx.arc(center[0], center[1], + radius=obround.hole_radius * self.scale[0], angle1=0, + angle2=2 * math.pi) + mask.ctx.fill() + + if obround.hole_width > 0 and obround.hole_height > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if rectangle.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height =self.scale_point((obround.hole_width, obround.hole_height)) + lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0), + obround.rotation, center) + lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), + obround.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), + obround.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), + obround.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_polygon(self, polygon, color): - - # TODO Ths does not handle rotation of a polygon - self.ctx.set_operator(cairo.OPERATOR_SOURCE - if polygon.level_polarity == 'dark' and - (not self.invert) else cairo.OPERATOR_CLEAR) - if polygon.hole_radius > 0: - self.ctx.push_group() - - vertices = polygon.vertices - - self.ctx.set_line_width(0) - self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - - # Start from before the end so it is easy to iterate and make sure it is closed - self.ctx.move_to(*map(mul, vertices[-1], self.scale)) - for v in vertices: - self.ctx.line_to(*map(mul, v, self.scale)) - - self.ctx.fill() - - if polygon.hole_radius > 0: - # Render the center clear - center = tuple(map(mul, polygon.position, self.scale)) - self.ctx.set_source_rgba(color[0], color[1], color[2], self.alpha) - self.ctx.set_operator(cairo.OPERATOR_CLEAR - if polygon.level_polarity == 'dark' - and (not self.invert) - else cairo.OPERATOR_SOURCE) - self.ctx.set_line_width(0) - self.ctx.arc(center[0], - center[1], - polygon.hole_radius * self.scale[0], 0, 2 * math.pi) - self.ctx.fill() + self.ctx.set_operator(cairo.OPERATOR_OVER + if (not self.invert) + and polygon.level_polarity == 'dark' + else cairo.OPERATOR_CLEAR) + with self._new_mask() as mask: + + vertices = polygon.vertices + mask.ctx.set_line_width(0) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + # Start from before the end so it is easy to iterate and make sure + # it is closed + mask.ctx.move_to(*self.scale_point(vertices[-1])) + for v in vertices: + mask.ctx.line_to(*self.scale_point(v)) + mask.ctx.fill() + + center = self.scale_point(polygon.position) + if polygon.hole_radius > 0: + # Render the center clear + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if polygon.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + mask.ctx.set_line_width(0) + mask.ctx.arc(center[0], + center[1], + polygon.hole_radius * self.scale[0], 0, 2 * math.pi) + mask.ctx.fill() + + if polygon.hole_width > 0 and polygon.hole_height > 0: + mask.ctx.set_operator(cairo.OPERATOR_CLEAR + if polygon.level_polarity == 'dark' + and (not self.invert) + else cairo.OPERATOR_OVER) + width, height = self.scale_point((polygon.hole_width, polygon.hole_height)) + lower_left = rotate_point((center[0] - width / 2.0, center[1] - height / 2.0), + polygon.rotation, center) + lower_right = rotate_point((center[0] + width / 2.0, center[1] - height / 2.0), + polygon.rotation, center) + upper_left = rotate_point((center[0] - width / 2.0, center[1] + height / 2.0), + polygon.rotation, center) + upper_right = rotate_point((center[0] + width / 2.0, center[1] + height / 2.0), + polygon.rotation, center) + points = (lower_left, lower_right, upper_right, upper_left) + mask.ctx.move_to(*points[-1]) + for point in points: + mask.ctx.line_to(*point) + mask.ctx.fill() + + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_drill(self, circle, color=None): color = color if color is not None else self.drill_color @@ -368,22 +493,20 @@ class GerberCairoContext(GerberContext): width = slot.diameter - self.ctx.set_operator(cairo.OPERATOR_SOURCE + self.ctx.set_operator(cairo.OPERATOR_OVER if slot.level_polarity == 'dark' and (not self.invert) else cairo.OPERATOR_CLEAR) - - self.ctx.set_line_width(width * self.scale[0]) - self.ctx.set_line_cap(cairo.LINE_CAP_ROUND) - self.ctx.move_to(*start) - self.ctx.line_to(*end) - self.ctx.stroke() + with self._new_mask() as mask: + mask.ctx.set_line_width(width * self.scale[0]) + mask.ctx.set_line_cap(cairo.LINE_CAP_ROUND) + mask.ctx.move_to(*start) + mask.ctx.line_to(*end) + mask.ctx.stroke() + self.ctx.mask_surface(mask.surface, self.origin_in_pixels[0]) def _render_amgroup(self, amgroup, color): - self.ctx.push_group() for primitive in amgroup.primitives: self.render(primitive) - self.ctx.pop_group_to_source() - self.ctx.paint_with_alpha(1) def _render_test_record(self, primitive, color): position = [pos + origin for pos, origin in @@ -392,7 +515,7 @@ class GerberCairoContext(GerberContext): 'monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) self.ctx.set_font_size(13) self._render_circle(Circle(position, 0.015), color) - self.ctx.set_operator(cairo.OPERATOR_SOURCE + self.ctx.set_operator(cairo.OPERATOR_OVER if primitive.level_polarity == 'dark' and (not self.invert) else cairo.OPERATOR_CLEAR) self.ctx.move_to(*[self.scale[0] * (coord + 0.015) for coord in position]) @@ -405,26 +528,25 @@ class GerberCairoContext(GerberContext): matrix = copy.copy(self._xform_matrix) layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1]) ctx = cairo.Context(layer) - ctx.scale(1, -1) - ctx.translate(-(self.origin_in_inch[0] * self.scale[0]), - (-self.origin_in_inch[1] * self.scale[0]) - size_in_pixels[1]) + if self.invert: + ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0) ctx.set_operator(cairo.OPERATOR_OVER) ctx.paint() if mirror: matrix.xx = -1.0 matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0] self.ctx = ctx + self.ctx.set_matrix(matrix) self.active_layer = layer self.active_matrix = matrix + def _flatten(self, color=None, alpha=None): color = color if color is not None else self.color alpha = alpha if alpha is not None else self.alpha - ptn = cairo.SurfacePattern(self.active_layer) - ptn.set_matrix(self.active_matrix) self.output_ctx.set_source_rgba(*color, alpha=alpha) - self.output_ctx.mask(ptn) + self.output_ctx.mask_surface(self.active_layer) self.ctx = None self.active_layer = None self.active_matrix = None -- cgit From 0ae5c48a65d59df8624a17c2b5a6aabff4c05e25 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 08:10:32 -0500 Subject: Fix rs274x output bugs --- gerber/render/rs274x_backend.py | 51 ++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/gerber/render/rs274x_backend.py b/gerber/render/rs274x_backend.py index 13e871c..d32602a 100644 --- a/gerber/render/rs274x_backend.py +++ b/gerber/render/rs274x_backend.py @@ -159,7 +159,7 @@ class Rs274xContext(GerberContext): # Select the right aperture if not already selected if aperture: if isinstance(aperture, Circle): - aper = self._get_circle(aperture.diameter, aperture.hole_diameter) + aper = self._get_circle(aperture.diameter, aperture.hole_diameter, aperture.hole_width, aperture.hole_height) elif isinstance(aperture, Rectangle): aper = self._get_rectangle(aperture.width, aperture.height) elif isinstance(aperture, Obround): @@ -283,10 +283,12 @@ class Rs274xContext(GerberContext): self._pos = primitive.position - def _get_circle(self, diameter, hole_diameter, dcode = None): + def _get_circle(self, diameter, hole_diameter=None, hole_width=None, + hole_height=None, dcode = None): '''Define a circlar aperture''' - aper = self._circles.get((diameter, hole_diameter), None) + key = (diameter, hole_diameter, hole_width, hole_height) + aper = self._circles.get(key, None) if not aper: if not dcode: @@ -295,21 +297,22 @@ class Rs274xContext(GerberContext): else: self._next_dcode = max(dcode + 1, self._next_dcode) - aper = ADParamStmt.circle(dcode, diameter, hole_diameter) - self._circles[(diameter, hole_diameter)] = aper + aper = ADParamStmt.circle(dcode, diameter, hole_diameter, hole_width, hole_height) + self._circles[(diameter, hole_diameter, hole_width, hole_height)] = aper self.header.append(aper) return aper def _render_circle(self, circle, color): - aper = self._get_circle(circle.diameter, circle.hole_diameter) + aper = self._get_circle(circle.diameter, circle.hole_diameter, circle.hole_width, circle.hole_height) self._render_flash(circle, aper) - def _get_rectangle(self, width, height, dcode = None): + def _get_rectangle(self, width, height, hole_diameter=None, hole_width=None, + hole_height=None, dcode = None): '''Get a rectanglar aperture. If it isn't defined, create it''' - key = (width, height) + key = (width, height, hole_diameter, hole_width, hole_height) aper = self._rects.get(key, None) if not aper: @@ -319,20 +322,23 @@ class Rs274xContext(GerberContext): else: self._next_dcode = max(dcode + 1, self._next_dcode) - aper = ADParamStmt.rect(dcode, width, height) - self._rects[(width, height)] = aper + aper = ADParamStmt.rect(dcode, width, height, hole_diameter, hole_width, hole_height) + self._rects[(width, height, hole_diameter, hole_width, hole_height)] = aper self.header.append(aper) return aper def _render_rectangle(self, rectangle, color): - aper = self._get_rectangle(rectangle.width, rectangle.height) + aper = self._get_rectangle(rectangle.width, rectangle.height, + rectangle.hole_diameter, + rectangle.hole_width, rectangle.hole_height) self._render_flash(rectangle, aper) - def _get_obround(self, width, height, dcode = None): + def _get_obround(self, width, height, hole_diameter=None, hole_width=None, + hole_height=None, dcode = None): - key = (width, height) + key = (width, height, hole_diameter, hole_width, hole_height) aper = self._obrounds.get(key, None) if not aper: @@ -342,7 +348,7 @@ class Rs274xContext(GerberContext): else: self._next_dcode = max(dcode + 1, self._next_dcode) - aper = ADParamStmt.obround(dcode, width, height) + aper = ADParamStmt.obround(dcode, width, height, hole_diameter, hole_width, hole_height) self._obrounds[key] = aper self.header.append(aper) @@ -350,17 +356,22 @@ class Rs274xContext(GerberContext): def _render_obround(self, obround, color): - aper = self._get_obround(obround.width, obround.height) + aper = self._get_obround(obround.width, obround.height, + obround.hole_diameter, obround.hole_width, + obround.hole_height) self._render_flash(obround, aper) def _render_polygon(self, polygon, color): - aper = self._get_polygon(polygon.radius, polygon.sides, polygon.rotation, polygon.hole_radius) + aper = self._get_polygon(polygon.radius, polygon.sides, + polygon.rotation, polygon.hole_diameter, + polygon.hole_width, polygon.hole_height) self._render_flash(polygon, aper) - def _get_polygon(self, radius, num_vertices, rotation, hole_radius, dcode = None): + def _get_polygon(self, radius, num_vertices, rotation, hole_diameter=None, + hole_width=None, hole_height=None, dcode = None): - key = (radius, num_vertices, rotation, hole_radius) + key = (radius, num_vertices, rotation, hole_diameter, hole_width, hole_height) aper = self._polygons.get(key, None) if not aper: @@ -370,7 +381,9 @@ class Rs274xContext(GerberContext): else: self._next_dcode = max(dcode + 1, self._next_dcode) - aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices, rotation, hole_radius * 2) + aper = ADParamStmt.polygon(dcode, radius * 2, num_vertices, + rotation, hole_diameter, hole_width, + hole_height) self._polygons[key] = aper self.header.append(aper) -- cgit From 33e84943184d6643e92298825bd61441ee033a4f Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 08:11:56 -0500 Subject: Add more tests for primitives --- gerber/tests/test_primitives.py | 99 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py index 52d774c..97b335b 100644 --- a/gerber/tests/test_primitives.py +++ b/gerber/tests/test_primitives.py @@ -192,16 +192,53 @@ def test_arc_sweep_angle(): def test_arc_bounds(): """ Test Arc primitive bounding box calculation """ - cases = [((1, 0), (0, 1), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))), - ((1, 0), (0, 1), (0, 0), 'counterclockwise', - ((-0.5, 1.5), (-0.5, 1.5))), - # TODO: ADD MORE TEST CASES HERE - ] + cases = [ + ((1, 0), (0, 1), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))), + ((1, 0), (0, 1), (0, 0), 'counterclockwise',((-0.5, 1.5), (-0.5, 1.5))), + + ((0, 1), (-1, 0), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))), + ((0, 1), (-1, 0), (0, 0), 'counterclockwise', ((-1.5, 0.5), (-0.5, 1.5))), + + ((-1, 0), (0, -1), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))), + ((-1, 0), (0, -1), (0, 0), 'counterclockwise', ((-1.5, 0.5), (-1.5, 0.5))), + + ((0, -1), (1, 0), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))), + ((0, -1), (1, 0), (0, 0), 'counterclockwise',((-0.5, 1.5), (-1.5, 0.5))), + + # Arcs with the same start and end point render a full circle + ((1, 0), (1, 0), (0, 0), 'clockwise', ((-1.5, 1.5), (-1.5, 1.5))), + ((1, 0), (1, 0), (0, 0), 'counterclockwise', ((-1.5, 1.5), (-1.5, 1.5))), + ] for start, end, center, direction, bounds in cases: c = Circle((0,0), 1) - a = Arc(start, end, center, direction, c, 'single-quadrant') + a = Arc(start, end, center, direction, c, 'multi-quadrant') assert_equal(a.bounding_box, bounds) +def test_arc_bounds_no_aperture(): + """ Test Arc primitive bounding box calculation ignoring aperture + """ + cases = [ + ((1, 0), (0, 1), (0, 0), 'clockwise', ((-1.0, 1.0), (-1.0, 1.0))), + ((1, 0), (0, 1), (0, 0), 'counterclockwise',((0.0, 1.0), (0.0, 1.0))), + + ((0, 1), (-1, 0), (0, 0), 'clockwise', ((-1.0, 1.0), (-1.0, 1.0))), + ((0, 1), (-1, 0), (0, 0), 'counterclockwise', ((-1.0, 0.0), (0.0, 1.0))), + + ((-1, 0), (0, -1), (0, 0), 'clockwise', ((-1.0, 1.0), (-1.0, 1.0))), + ((-1, 0), (0, -1), (0, 0), 'counterclockwise', ((-1.0, 0.0), (-1.0, 0.0))), + + ((0, -1), (1, 0), (0, 0), 'clockwise', ((-1.0, 1.0), (-1.0, 1.0))), + ((0, -1), (1, 0), (0, 0), 'counterclockwise',((-0.0, 1.0), (-1.0, 0.0))), + + # Arcs with the same start and end point render a full circle + ((1, 0), (1, 0), (0, 0), 'clockwise', ((-1.0, 1.0), (-1.0, 1.0))), + ((1, 0), (1, 0), (0, 0), 'counterclockwise', ((-1.0, 1.0), (-1.0, 1.0))), + ] + for start, end, center, direction, bounds in cases: + c = Circle((0,0), 1) + a = Arc(start, end, center, direction, c, 'multi-quadrant') + assert_equal(a.bounding_box_no_aperture, bounds) + def test_arc_conversion(): c = Circle((0, 0), 25.4, units='metric') @@ -438,6 +475,7 @@ def test_rectangle_ctor(): assert_equal(r.width, width) assert_equal(r.height, height) + def test_rectangle_hole_radius(): """ Test rectangle hole diameter calculation """ @@ -448,7 +486,6 @@ def test_rectangle_hole_radius(): assert_equal(0.5, r.hole_radius) - def test_rectangle_bounds(): """ Test rectangle bounding box calculation """ @@ -461,6 +498,32 @@ def test_rectangle_bounds(): assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2))) assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2))) +def test_rectangle_vertices(): + sqrt2 = math.sqrt(2.0) + TEST_VECTORS = [ + ((0, 0), 2.0, 2.0, 0.0, ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0))), + ((0, 0), 2.0, 3.0, 0.0, ((-1.0, -1.5), (-1.0, 1.5), (1.0, 1.5), (1.0, -1.5))), + ((0, 0), 2.0, 2.0, 90.0,((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0))), + ((0, 0), 3.0, 2.0, 90.0,((-1.0, -1.5), (-1.0, 1.5), (1.0, 1.5), (1.0, -1.5))), + ((0, 0), 2.0, 2.0, 45.0,((-sqrt2, 0.0), (0.0, sqrt2), (sqrt2, 0), (0, -sqrt2))), + ] + for pos, width, height, rotation, expected in TEST_VECTORS: + r = Rectangle(pos, width, height, rotation=rotation) + for test, expect in zip(sorted(r.vertices), sorted(expected)): + assert_array_almost_equal(test, expect) + + r = Rectangle((0, 0), 2.0, 2.0, rotation=0.0) + r.rotation = 45.0 + for test, expect in zip(sorted(r.vertices), sorted(((-sqrt2, 0.0), (0.0, sqrt2), (sqrt2, 0), (0, -sqrt2)))): + assert_array_almost_equal(test, expect) + +def test_rectangle_segments(): + + r = Rectangle((0, 0), 2.0, 2.0) + expected = [vtx for segment in r.segments for vtx in segment] + for vertex in r.vertices: + assert_in(vertex, expected) + def test_rectangle_conversion(): """Test converting rectangles between units""" @@ -697,6 +760,18 @@ def test_chamfer_rectangle_offset(): r.offset(0, 1) assert_equal(r.position, (1., 1.)) +def test_chamfer_rectangle_vertices(): + TEST_VECTORS = [ + (1.0, (True, True, True, True), ((-2.5, -1.5), (-2.5, 1.5), (-1.5, 2.5), (1.5, 2.5), (2.5, 1.5), (2.5, -1.5), (1.5, -2.5), (-1.5, -2.5))), + (1.0, (True, False, False, False), ((-2.5, -2.5), (-2.5, 2.5), (1.5, 2.5), (2.5, 1.5), (2.5, -2.5))), + (1.0, (False, True, False, False), ((-2.5, -2.5), (-2.5, 1.5), (-1.5, 2.5), (2.5, 2.5), (2.5, -2.5))), + (1.0, (False, False, True, False), ((-2.5, -1.5), (-2.5, 2.5), (2.5, 2.5), (2.5, -2.5), (-1.5, -2.5))), + (1.0, (False, False, False, True), ((-2.5, -2.5), (-2.5, 2.5), (2.5, 2.5), (2.5, -1.5), (1.5, -2.5))), + ] + for chamfer, corners, expected in TEST_VECTORS: + r = ChamferRectangle((0, 0), 5, 5, chamfer, corners) + assert_equal(set(r.vertices), set(expected)) + def test_round_rectangle_ctor(): """ Test round rectangle creation @@ -1237,7 +1312,7 @@ def test_drill_conversion(): assert_equal(d.position, (0.1, 1.0)) assert_equal(d.diameter, 10.0) - d = Drill((0.1, 1.0), 10., None, units='inch') + d = Drill((0.1, 1.0), 10., units='inch') # No effect d.to_inch() @@ -1255,7 +1330,7 @@ def test_drill_conversion(): def test_drill_offset(): - d = Drill((0, 0), 1., None) + d = Drill((0, 0), 1.) d.offset(1, 0) assert_equal(d.position, (1., 0.)) d.offset(0, 1) @@ -1263,8 +1338,8 @@ def test_drill_offset(): def test_drill_equality(): - d = Drill((2.54, 25.4), 254., None) - d1 = Drill((2.54, 25.4), 254., None) + d = Drill((2.54, 25.4), 254.) + d1 = Drill((2.54, 25.4), 254.) assert_equal(d, d1) - d1 = Drill((2.54, 25.4), 254.2, None) + d1 = Drill((2.54, 25.4), 254.2) assert_not_equal(d, d1) -- cgit From 389c273a8787a20f3e6ea5fdb951f62d7d5d4999 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 08:12:55 -0500 Subject: Clean up rs274x output tests --- gerber/tests/test_rs274x_backend.py | 38 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/gerber/tests/test_rs274x_backend.py b/gerber/tests/test_rs274x_backend.py index 89512f0..e128841 100644 --- a/gerber/tests/test_rs274x_backend.py +++ b/gerber/tests/test_rs274x_backend.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Author: Garret Fick -import io + import os from ..render.rs274x_backend import Rs274xContext @@ -16,7 +16,7 @@ def test_render_two_boxes(): def _test_render_single_quadrant(): """Umaco exapmle of a single quadrant arc""" - + # TODO there is probably a bug here _test_render('resources/example_single_quadrant.gbr', 'golden/example_single_quadrant.gbr') @@ -25,17 +25,17 @@ def _test_render_simple_contour(): """Umaco exapmle of a simple arrow-shaped contour""" _test_render('resources/example_simple_contour.gbr', 'golden/example_simple_contour.gbr') - + def _test_render_single_contour_1(): """Umaco example of a single contour - + The resulting image for this test is used by other tests because they must generate the same output.""" _test_render('resources/example_single_contour_1.gbr', 'golden/example_single_contour.gbr') def _test_render_single_contour_2(): """Umaco exapmle of a single contour, alternate contour end order - + The resulting image for this test is used by other tests because they must generate the same output.""" _test_render('resources/example_single_contour_2.gbr', 'golden/example_single_contour.gbr') @@ -43,12 +43,12 @@ def _test_render_single_contour_2(): def _test_render_single_contour_3(): """Umaco exapmle of a single contour with extra line""" _test_render('resources/example_single_contour_3.gbr', 'golden/example_single_contour_3.gbr') - - + + def _test_render_not_overlapping_contour(): """Umaco example of D02 staring a second contour""" _test_render('resources/example_not_overlapping_contour.gbr', 'golden/example_not_overlapping_contour.gbr') - + def _test_render_not_overlapping_touching(): """Umaco example of D02 staring a second contour""" @@ -67,7 +67,7 @@ def _test_render_overlapping_contour(): def _DISABLED_test_render_level_holes(): """Umaco example of using multiple levels to create multiple holes""" - + # TODO This is clearly rendering wrong. I'm temporarily checking this in because there are more # rendering fixes in the related repository that may resolve these. _test_render('resources/example_level_holes.gbr', 'golden/example_overlapping_contour.gbr') @@ -96,7 +96,7 @@ def _test_render_cutin_multiple(): """Umaco example of a region with multiple cutins""" _test_render('resources/example_cutin_multiple.gbr', 'golden/example_cutin_multiple.gbr') - + def _test_flash_circle(): """Umaco example a simple circular flash with and without a hole""" @@ -141,7 +141,7 @@ def _resolve_path(path): def _test_render(gerber_path, png_expected_path, create_output_path = None): """Render the gerber file and compare to the expected PNG output. - + Parameters ---------- gerber_path : string @@ -150,14 +150,14 @@ def _test_render(gerber_path, png_expected_path, create_output_path = None): Path to the PNG file to compare to create_output : string|None If not None, write the generated PNG to the specified path. - This is primarily to help with + This is primarily to help with """ - + gerber_path = _resolve_path(gerber_path) png_expected_path = _resolve_path(png_expected_path) if create_output_path: create_output_path = _resolve_path(create_output_path) - + gerber = read(gerber_path) # Create GBR output from the input file @@ -165,7 +165,7 @@ def _test_render(gerber_path, png_expected_path, create_output_path = None): gerber.render(ctx) actual_contents = ctx.dump() - + # If we want to write the file bytes, do it now. This happens if create_output_path: with open(create_output_path, 'wb') as out_file: @@ -174,12 +174,12 @@ def _test_render(gerber_path, png_expected_path, create_output_path = None): # So if we are creating the output, we make the test fail on purpose so you # won't forget to disable this assert_false(True, 'Test created the output %s. This needs to be disabled to make sure the test behaves correctly' % (create_output_path,)) - + # Read the expected PNG file - + with open(png_expected_path, 'r') as expected_file: expected_contents = expected_file.read() - + assert_equal(expected_contents, actual_contents.getvalue()) - + return gerber -- cgit From e07ccc805fbaf05cff35e423d1559279bb2bc15e Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 08:14:26 -0500 Subject: Fix drill tests --- gerber/tests/test_primitives.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py index 97b335b..2fe5a4b 100644 --- a/gerber/tests/test_primitives.py +++ b/gerber/tests/test_primitives.py @@ -1270,7 +1270,7 @@ def test_drill_ctor(): """ test_cases = (((0, 0), 2), ((1, 1), 3), ((2, 2), 5)) for position, diameter in test_cases: - d = Drill(position, diameter, None) + d = Drill(position, diameter) assert_equal(d.position, position) assert_equal(d.diameter, diameter) assert_equal(d.radius, diameter / 2.) @@ -1279,24 +1279,24 @@ def test_drill_ctor(): def test_drill_ctor_validation(): """ Test drill argument validation """ - assert_raises(TypeError, Drill, 3, 5, None) - assert_raises(TypeError, Drill, (3,4,5), 5, None) + assert_raises(TypeError, Drill, 3, 5) + assert_raises(TypeError, Drill, (3,4,5), 5) def test_drill_bounds(): - d = Drill((0, 0), 2, None) + d = Drill((0, 0), 2) xbounds, ybounds = d.bounding_box assert_array_almost_equal(xbounds, (-1, 1)) assert_array_almost_equal(ybounds, (-1, 1)) - d = Drill((1, 2), 2, None) + d = Drill((1, 2), 2) xbounds, ybounds = d.bounding_box assert_array_almost_equal(xbounds, (0, 2)) assert_array_almost_equal(ybounds, (1, 3)) def test_drill_conversion(): - d = Drill((2.54, 25.4), 254., None, units='metric') + d = Drill((2.54, 25.4), 254., units='metric') #No effect d.to_metric() -- cgit From 845737ebd85b8907d21a33b2874e83f7bfd7dadd Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 08:23:02 -0500 Subject: Temporarily disable python3.3 until i can sort out the issue with py3.3 and scipy on travis CI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b8cf3d1..0b3bd80 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: python python: - "2.7" - - "3.3" +# - "3.3" - "3.4" - "3.5" -- cgit From c1b29035218467b496fffed76ea85390461150c7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 18 Nov 2016 08:26:06 -0500 Subject: Try using wheel for scipy --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0b3bd80..a419b28 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,13 @@ language: python python: - "2.7" -# - "3.3" + - "3.3" - "3.4" - "3.5" # command to install dependencies install: - - "pip install -r requirements.txt" + - "pip install --only-binary=scipy -r requirements.txt" - "pip install -r test-requirements.txt" - "pip install coveralls" -- cgit