1 /*******************************************************************
2
3 Part of the Fritzing project - http://fritzing.org
4 Copyright (c) 2007-2015 Fachhochschule Potsdam - http://fh-potsdam.de
5
6 Fritzing is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Fritzing is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Fritzing. If not, see <http://www.gnu.org/licenses/>.
18
19 ********************************************************************
20
21 $Revision: 6976 $:
22 $Author: irascibl@gmail.com $:
23 $Date: 2013-04-21 09:50:09 +0200 (So, 21. Apr 2013) $
24
25 ********************************************************************/
26
27 #include <QMessageBox>
28 #include <QFileDialog>
29 #include <QSvgRenderer>
30 #include <qmath.h>
31
32 #include "gerbergenerator.h"
33 #include "../debugdialog.h"
34 #include "../fsvgrenderer.h"
35 #include "../sketch/pcbsketchwidget.h"
36 #include "../connectors/connectoritem.h"
37 #include "../connectors/svgidlayer.h"
38 #include "svgfilesplitter.h"
39 #include "groundplanegenerator.h"
40 #include "../utils/graphicsutils.h"
41 #include "../utils/textutils.h"
42 #include "../utils/folderutils.h"
43 #include "../version/version.h"
44
45 static const QRegExp AaCc("[aAcCqQtTsS]");
46 static const QRegExp MFinder("([mM])\\s*([0-9.]*)[\\s,]*([0-9.]*)");
47 const QRegExp GerberGenerator::MultipleZs("z\\s*[^\\s]");
48
49 const QString GerberGenerator::SilkTopSuffix = "_silkTop.gto";
50 const QString GerberGenerator::SilkBottomSuffix = "_silkBottom.gbo";
51 const QString GerberGenerator::CopperTopSuffix = "_copperTop.gtl";
52 const QString GerberGenerator::CopperBottomSuffix = "_copperBottom.gbl";
53 const QString GerberGenerator::MaskTopSuffix = "_maskTop.gts";
54 const QString GerberGenerator::MaskBottomSuffix = "_maskBottom.gbs";
55 const QString GerberGenerator::PasteMaskTopSuffix = "_pasteMaskTop.gtp";
56 const QString GerberGenerator::PasteMaskBottomSuffix = "_pasteMaskBottom.gbp";
57 const QString GerberGenerator::DrillSuffix = "_drill.txt";
58 const QString GerberGenerator::OutlineSuffix = "_contour.gm1";
59 const QString GerberGenerator::MagicBoardOutlineID = "boardoutline";
60
61 const double GerberGenerator::MaskClearanceMils = 5;
62
63 ////////////////////////////////////////////
64
pixelsCollide(QImage * image1,QImage * image2,int x1,int y1,int x2,int y2)65 bool pixelsCollide(QImage * image1, QImage * image2, int x1, int y1, int x2, int y2) {
66 for (int y = y1; y < y2; y++) {
67 for (int x = x1; x < x2; x++) {
68 QRgb p1 = image1->pixel(x, y);
69 if (p1 == 0xffffffff) continue;
70
71 QRgb p2 = image2->pixel(x, y);
72 if (p2 == 0xffffffff) continue;
73
74 //DebugDialog::debug(QString("p1:%1 p2:%2").arg(p1, 0, 16).arg(p2, 0, 16));
75
76 return true;
77 }
78 }
79
80 return false;
81 }
82
83 ////////////////////////////////////////////
84
exportToGerber(const QString & prefix,const QString & exportDir,ItemBase * board,PCBSketchWidget * sketchWidget,bool displayMessageBoxes)85 void GerberGenerator::exportToGerber(const QString & prefix, const QString & exportDir, ItemBase * board, PCBSketchWidget * sketchWidget, bool displayMessageBoxes)
86 {
87 if (board == NULL) {
88 int boardCount;
89 board = sketchWidget->findSelectedBoard(boardCount);
90 if (boardCount == 0) {
91 DebugDialog::debug("board not found");
92 return;
93 }
94 if (board == NULL) {
95 DebugDialog::debug("multiple boards found");
96 return;
97 }
98 }
99
100 exportPickAndPlace(prefix, exportDir, board, sketchWidget, displayMessageBoxes);
101
102 LayerList viewLayerIDs = ViewLayer::copperLayers(ViewLayer::NewBottom);
103 int copperInvalidCount = doCopper(board, sketchWidget, viewLayerIDs, "Copper0", CopperBottomSuffix, prefix, exportDir, displayMessageBoxes);
104
105 if (sketchWidget->boardLayers() == 2) {
106 viewLayerIDs = ViewLayer::copperLayers(ViewLayer::NewTop);
107 copperInvalidCount += doCopper(board, sketchWidget, viewLayerIDs, "Copper1", CopperTopSuffix, prefix, exportDir, displayMessageBoxes);
108 }
109
110 LayerList maskLayerIDs = ViewLayer::maskLayers(ViewLayer::NewBottom);
111 QString maskBottom, maskTop;
112 int maskInvalidCount = doMask(maskLayerIDs, "Mask0", MaskBottomSuffix, board, sketchWidget, prefix, exportDir, displayMessageBoxes, maskBottom);
113
114 if (sketchWidget->boardLayers() == 2) {
115 maskLayerIDs = ViewLayer::maskLayers(ViewLayer::NewTop);
116 maskInvalidCount += doMask(maskLayerIDs, "Mask1", MaskTopSuffix, board, sketchWidget, prefix, exportDir, displayMessageBoxes, maskTop);
117 }
118
119 maskLayerIDs = ViewLayer::maskLayers(ViewLayer::NewBottom);
120 int pasteMaskInvalidCount = doPasteMask(maskLayerIDs, "PasteMask0", PasteMaskBottomSuffix, board, sketchWidget, prefix, exportDir, displayMessageBoxes);
121
122 if (sketchWidget->boardLayers() == 2) {
123 maskLayerIDs = ViewLayer::maskLayers(ViewLayer::NewTop);
124 pasteMaskInvalidCount += doPasteMask(maskLayerIDs, "PasteMask1", PasteMaskTopSuffix, board, sketchWidget, prefix, exportDir, displayMessageBoxes);
125 }
126
127 LayerList silkLayerIDs = ViewLayer::silkLayers(ViewLayer::NewTop);
128 int silkInvalidCount = doSilk(silkLayerIDs, "Silk1", SilkTopSuffix, board, sketchWidget, prefix, exportDir, displayMessageBoxes, maskTop);
129 silkLayerIDs = ViewLayer::silkLayers(ViewLayer::NewBottom);
130 silkInvalidCount += doSilk(silkLayerIDs, "Silk0", SilkBottomSuffix, board, sketchWidget, prefix, exportDir, displayMessageBoxes, maskBottom);
131
132 // now do it for the outline/contour
133 LayerList outlineLayerIDs = ViewLayer::outlineLayers();
134 bool empty;
135 QString svgOutline = renderTo(outlineLayerIDs, board, sketchWidget, empty);
136 if (empty || svgOutline.isEmpty()) {
137 displayMessage(QObject::tr("outline is empty"), displayMessageBoxes);
138 return;
139 }
140
141 svgOutline = cleanOutline(svgOutline);
142 // at this point svgOutline must be a single element; a path element may contain cutouts
143 QMultiHash<long, ConnectorItem *> treatAsCircle;
144 svgOutline = clipToBoard(svgOutline, board, "board", SVG2gerber::ForOutline, "", displayMessageBoxes, treatAsCircle);
145 QSizeF svgSize = TextUtils::parseForWidthAndHeight(svgOutline);
146
147 // create outline gerber from svg
148 SVG2gerber outlineGerber;
149 int outlineInvalidCount = outlineGerber.convert(svgOutline, sketchWidget->boardLayers() == 2, "contour", SVG2gerber::ForOutline, svgSize * GraphicsUtils::StandardFritzingDPI);
150
151 //DebugDialog::debug(QString("outline output: %1").arg(outlineGerber.getGerber()));
152 saveEnd("contour", exportDir, prefix, OutlineSuffix, displayMessageBoxes, outlineGerber);
153
154 doDrill(board, sketchWidget, prefix, exportDir, displayMessageBoxes);
155
156 if (outlineInvalidCount > 0 || silkInvalidCount > 0 || copperInvalidCount > 0 || maskInvalidCount || pasteMaskInvalidCount) {
157 QString s;
158 if (outlineInvalidCount > 0) s += QObject::tr("the board outline layer, ");
159 if (silkInvalidCount > 0) s += QObject::tr("silkscreen layer(s), ");
160 if (copperInvalidCount > 0) s += QObject::tr("copper layer(s), ");
161 if (maskInvalidCount > 0) s += QObject::tr("mask layer(s), ");
162 if (pasteMaskInvalidCount > 0) s += QObject::tr("paste mask layer(s), ");
163 s.chop(2);
164 displayMessage(QObject::tr("Unable to translate svg curves in %1").arg(s), displayMessageBoxes);
165 }
166
167 }
168
doCopper(ItemBase * board,PCBSketchWidget * sketchWidget,LayerList & viewLayerIDs,const QString & copperName,const QString & copperSuffix,const QString & filename,const QString & exportDir,bool displayMessageBoxes)169 int GerberGenerator::doCopper(ItemBase * board, PCBSketchWidget * sketchWidget, LayerList & viewLayerIDs, const QString & copperName, const QString & copperSuffix, const QString & filename, const QString & exportDir, bool displayMessageBoxes)
170 {
171 bool empty;
172 QString svg = renderTo(viewLayerIDs, board, sketchWidget, empty);
173 if (empty || svg.isEmpty()) {
174 displayMessage(QObject::tr("%1 layer export is empty.").arg(copperName), displayMessageBoxes);
175 return 0;
176 }
177
178 QMultiHash<long, ConnectorItem *> treatAsCircle;
179 foreach (QGraphicsItem * item, sketchWidget->scene()->collidingItems(board)) {
180 ConnectorItem * connectorItem = dynamic_cast<ConnectorItem *>(item);
181 if (connectorItem == NULL) continue;
182 if (!connectorItem->isPath()) continue;
183 if (connectorItem->radius() == 0) continue;
184
185 treatAsCircle.insert(connectorItem->attachedToID(), connectorItem);
186 }
187
188 QSizeF svgSize = TextUtils::parseForWidthAndHeight(svg);
189
190 svg = clipToBoard(svg, board, copperName, SVG2gerber::ForCopper, "", displayMessageBoxes, treatAsCircle);
191 if (svg.isEmpty()) {
192 displayMessage(QObject::tr("%1 layer export is empty (case 2).").arg(copperName), displayMessageBoxes);
193 return 0;
194 }
195
196 return doEnd(svg, sketchWidget->boardLayers(), copperName, SVG2gerber::ForCopper, svgSize * GraphicsUtils::StandardFritzingDPI, exportDir, filename, copperSuffix, displayMessageBoxes);
197 }
198
199
doSilk(LayerList silkLayerIDs,const QString & silkName,const QString & gerberSuffix,ItemBase * board,PCBSketchWidget * sketchWidget,const QString & filename,const QString & exportDir,bool displayMessageBoxes,const QString & clipString)200 int GerberGenerator::doSilk(LayerList silkLayerIDs, const QString & silkName, const QString & gerberSuffix, ItemBase * board, PCBSketchWidget * sketchWidget, const QString & filename, const QString & exportDir, bool displayMessageBoxes, const QString & clipString)
201 {
202
203 bool empty;
204 QString svgSilk = renderTo(silkLayerIDs, board, sketchWidget, empty);
205 if (empty || svgSilk.isEmpty()) {
206 if (silkLayerIDs.contains(ViewLayer::Silkscreen1)) {
207 displayMessage(QObject::tr("silk layer %1 export is empty").arg(silkName), displayMessageBoxes);
208 }
209 return 0;
210 }
211
212 //QFile f(silkName + "original.svg");
213 //f.open(QFile::WriteOnly);
214 //QTextStream fs(&f);
215 //fs << svgSilk;
216 //f.close();
217
218 QSizeF svgSize = TextUtils::parseForWidthAndHeight(svgSilk);
219
220 QMultiHash<long, ConnectorItem *> treatAsCircle;
221 svgSilk = clipToBoard(svgSilk, board, silkName, SVG2gerber::ForSilk, clipString, displayMessageBoxes, treatAsCircle);
222 if (svgSilk.isEmpty()) {
223 displayMessage(QObject::tr("silk export failure"), displayMessageBoxes);
224 return 0;
225 }
226
227 //QFile f2(silkName + "clipped.svg");
228 //f2.open(QFile::WriteOnly);
229 //QTextStream fs2(&f2);
230 //fs2 << svgSilk;
231 //f2.close();
232
233 return doEnd(svgSilk, sketchWidget->boardLayers(), silkName, SVG2gerber::ForSilk, svgSize * GraphicsUtils::StandardFritzingDPI, exportDir, filename, gerberSuffix, displayMessageBoxes);
234 }
235
236
doDrill(ItemBase * board,PCBSketchWidget * sketchWidget,const QString & filename,const QString & exportDir,bool displayMessageBoxes)237 int GerberGenerator::doDrill(ItemBase * board, PCBSketchWidget * sketchWidget, const QString & filename, const QString & exportDir, bool displayMessageBoxes)
238 {
239 LayerList drillLayerIDs;
240 drillLayerIDs << ViewLayer::drillLayers();
241
242 bool empty;
243 QString svgDrill = renderTo(drillLayerIDs, board, sketchWidget, empty);
244 if (empty || svgDrill.isEmpty()) {
245 displayMessage(QObject::tr("exported drill file is empty"), displayMessageBoxes);
246 return 0;
247 }
248
249 QSizeF svgSize = TextUtils::parseForWidthAndHeight(svgDrill);
250 QMultiHash<long, ConnectorItem *> treatAsCircle;
251 foreach (QGraphicsItem * item, sketchWidget->scene()->collidingItems(board)) {
252 ConnectorItem * connectorItem = dynamic_cast<ConnectorItem *>(item);
253 if (connectorItem == NULL) continue;
254 if (!connectorItem->isPath()) continue;
255 if (connectorItem->radius() == 0) continue;
256
257 treatAsCircle.insert(connectorItem->attachedToID(), connectorItem);
258 }
259
260 svgDrill = clipToBoard(svgDrill, board, "Copper0", SVG2gerber::ForDrill, "", displayMessageBoxes, treatAsCircle);
261 if (svgDrill.isEmpty()) {
262 displayMessage(QObject::tr("drill export failure"), displayMessageBoxes);
263 return 0;
264 }
265
266 return doEnd(svgDrill, sketchWidget->boardLayers(), "drill", SVG2gerber::ForDrill, svgSize * GraphicsUtils::StandardFritzingDPI, exportDir, filename, DrillSuffix, displayMessageBoxes);
267 }
268
doMask(LayerList maskLayerIDs,const QString & maskName,const QString & gerberSuffix,ItemBase * board,PCBSketchWidget * sketchWidget,const QString & filename,const QString & exportDir,bool displayMessageBoxes,QString & clipString)269 int GerberGenerator::doMask(LayerList maskLayerIDs, const QString &maskName, const QString & gerberSuffix, ItemBase * board, PCBSketchWidget * sketchWidget, const QString & filename, const QString & exportDir, bool displayMessageBoxes, QString & clipString)
270 {
271 // don't want these in the mask laqyer
272 QList<ItemBase *> copperLogoItems;
273 sketchWidget->hideCopperLogoItems(copperLogoItems);
274
275 bool empty;
276 QString svgMask = renderTo(maskLayerIDs, board, sketchWidget, empty);
277 sketchWidget->restoreCopperLogoItems(copperLogoItems);
278
279 if (empty || svgMask.isEmpty()) {
280 displayMessage(QObject::tr("exported mask layer %1 is empty").arg(maskName), displayMessageBoxes);
281 return 0;
282 }
283
284 svgMask = TextUtils::expandAndFill(svgMask, "black", MaskClearanceMils * 2);
285 if (svgMask.isEmpty()) {
286 displayMessage(QObject::tr("%1 mask export failure (2)").arg(maskName), displayMessageBoxes);
287 return 0;
288 }
289
290 QSizeF svgSize = TextUtils::parseForWidthAndHeight(svgMask);
291 QMultiHash<long, ConnectorItem *> treatAsCircle;
292 svgMask = clipToBoard(svgMask, board, maskName, SVG2gerber::ForCopper, "", displayMessageBoxes, treatAsCircle);
293 if (svgMask.isEmpty()) {
294 displayMessage(QObject::tr("mask export failure"), displayMessageBoxes);
295 return 0;
296 }
297
298 clipString = svgMask;
299
300 return doEnd(svgMask, sketchWidget->boardLayers(), maskName, SVG2gerber::ForCopper, svgSize * GraphicsUtils::StandardFritzingDPI, exportDir, filename, gerberSuffix, displayMessageBoxes);
301 }
302
doPasteMask(LayerList maskLayerIDs,const QString & maskName,const QString & gerberSuffix,ItemBase * board,PCBSketchWidget * sketchWidget,const QString & filename,const QString & exportDir,bool displayMessageBoxes)303 int GerberGenerator::doPasteMask(LayerList maskLayerIDs, const QString &maskName, const QString & gerberSuffix, ItemBase * board, PCBSketchWidget * sketchWidget, const QString & filename, const QString & exportDir, bool displayMessageBoxes)
304 {
305 // don't want these in the mask laqyer
306 QList<ItemBase *> copperLogoItems;
307 sketchWidget->hideCopperLogoItems(copperLogoItems);
308 QList<ItemBase *> holes;
309 sketchWidget->hideHoles(holes);
310
311 bool empty;
312 QString svgMask = renderTo(maskLayerIDs, board, sketchWidget, empty);
313 sketchWidget->restoreCopperLogoItems(copperLogoItems);
314 sketchWidget->restoreCopperLogoItems(holes);
315
316 if (empty || svgMask.isEmpty()) {
317 displayMessage(QObject::tr("exported paste mask layer is empty"), displayMessageBoxes);
318 return 0;
319 }
320
321 svgMask = sketchWidget->makePasteMask(svgMask, board, GraphicsUtils::StandardFritzingDPI, maskLayerIDs);
322 if (svgMask.isEmpty()) return 0;
323
324 QSizeF svgSize = TextUtils::parseForWidthAndHeight(svgMask);
325 QMultiHash<long, ConnectorItem *> treatAsCircle;
326 svgMask = clipToBoard(svgMask, board, maskName, SVG2gerber::ForCopper, "", displayMessageBoxes, treatAsCircle);
327 if (svgMask.isEmpty()) {
328 displayMessage(QObject::tr("mask export failure"), displayMessageBoxes);
329 return 0;
330 }
331
332 return doEnd(svgMask, sketchWidget->boardLayers(), maskName, SVG2gerber::ForCopper, svgSize * GraphicsUtils::StandardFritzingDPI, exportDir, filename, gerberSuffix, displayMessageBoxes);
333 }
334
doEnd(const QString & svg,int boardLayers,const QString & layerName,SVG2gerber::ForWhy forWhy,QSizeF svgSize,const QString & exportDir,const QString & prefix,const QString & suffix,bool displayMessageBoxes)335 int GerberGenerator::doEnd(const QString & svg, int boardLayers, const QString & layerName, SVG2gerber::ForWhy forWhy, QSizeF svgSize,
336 const QString & exportDir, const QString & prefix, const QString & suffix, bool displayMessageBoxes)
337 {
338 // create mask gerber from svg
339 SVG2gerber gerber;
340 int invalidCount = gerber.convert(svg, boardLayers == 2, layerName, forWhy, svgSize);
341
342 saveEnd(layerName, exportDir, prefix, suffix, displayMessageBoxes, gerber);
343
344 return invalidCount;
345 }
346
saveEnd(const QString & layerName,const QString & exportDir,const QString & prefix,const QString & suffix,bool displayMessageBoxes,SVG2gerber & gerber)347 bool GerberGenerator::saveEnd(const QString & layerName, const QString & exportDir, const QString & prefix, const QString & suffix, bool displayMessageBoxes, SVG2gerber & gerber)
348 {
349
350 QString outname = exportDir + "/" + prefix + suffix;
351 QFile out(outname);
352 if (!out.open(QIODevice::WriteOnly | QIODevice::Text)) {
353 displayMessage(QObject::tr("%1 layer: unable to save to '%2'").arg(layerName).arg(outname), displayMessageBoxes);
354 return false;
355 }
356
357 QTextStream stream(&out);
358 stream << gerber.getGerber();
359 stream.flush();
360 out.close();
361 return true;
362
363 }
364
displayMessage(const QString & message,bool displayMessageBoxes)365 void GerberGenerator::displayMessage(const QString & message, bool displayMessageBoxes) {
366 // don't use QMessageBox if running conversion as a service
367 if (displayMessageBoxes) {
368 QMessageBox::warning(NULL, QObject::tr("Fritzing"), message);
369 return;
370 }
371
372 DebugDialog::debug(message);
373 }
374
clipToBoard(QString svgString,ItemBase * board,const QString & layerName,SVG2gerber::ForWhy forWhy,const QString & clipString,bool displayMessageBoxes,QMultiHash<long,ConnectorItem * > & treatAsCircle)375 QString GerberGenerator::clipToBoard(QString svgString, ItemBase * board, const QString & layerName, SVG2gerber::ForWhy forWhy, const QString & clipString, bool displayMessageBoxes, QMultiHash<long, ConnectorItem *> & treatAsCircle) {
376 QRectF source = board->sceneBoundingRect();
377 source.moveTo(0, 0);
378 return clipToBoard(svgString, source, layerName, forWhy, clipString, displayMessageBoxes, treatAsCircle);
379 }
380
clipToBoard(QString svgString,QRectF & boardRect,const QString & layerName,SVG2gerber::ForWhy forWhy,const QString & clipString,bool displayMessageBoxes,QMultiHash<long,ConnectorItem * > & treatAsCircle)381 QString GerberGenerator::clipToBoard(QString svgString, QRectF & boardRect, const QString & layerName, SVG2gerber::ForWhy forWhy, const QString & clipString, bool displayMessageBoxes, QMultiHash<long, ConnectorItem *> & treatAsCircle) {
382 // document 1 will contain svg that is easy to convert to gerber
383 QDomDocument domDocument1;
384 QString errorStr;
385 int errorLine;
386 int errorColumn;
387 bool result = domDocument1.setContent(svgString, &errorStr, &errorLine, &errorColumn);
388 if (!result) {
389 return "";
390 }
391
392 QDomElement root1 = domDocument1.documentElement();
393 if (root1.firstChildElement().isNull()) {
394 return "";
395 }
396
397 if (forWhy != SVG2gerber::ForDrill) {
398 QDomNodeList nodeList = root1.elementsByTagName("circle");
399 QList<QDomElement> justHoles;
400 for (int i = 0; i < nodeList.count(); i++) {
401 QDomElement circle = nodeList.at(i).toElement();
402 if (circle.attribute("id").contains(FSvgRenderer::NonConnectorName)) {
403 double sw = circle.attribute("stroke-width").toDouble();
404 if (sw == 0) {
405 justHoles << circle;
406 }
407 }
408 }
409 foreach (QDomElement circle, justHoles) {
410 circle.setTagName("g");
411 }
412 }
413
414 handleDonuts(root1, treatAsCircle);
415
416 bool multipleContours = false;
417 if (forWhy == SVG2gerber::ForOutline) {
418 multipleContours = dealWithMultipleContours(root1, displayMessageBoxes);
419 }
420
421 // document 2 will contain svg that must be rasterized for gerber conversion
422 QDomDocument domDocument2 = domDocument1.cloneNode(true).toDocument();
423
424 bool anyConverted = false;
425 if (TextUtils::squashElement(domDocument1, "text", "", QRegExp())) {
426 anyConverted = true;
427 }
428
429 // gerber can't handle ellipses that are rotated, so cull them all
430 if (TextUtils::squashElement(domDocument1, "ellipse", "", QRegExp())) {
431 anyConverted = true;
432 }
433
434 if (TextUtils::squashElement(domDocument1, "rect", "rx", QRegExp())) {
435 anyConverted = true;
436 }
437
438 if (TextUtils::squashElement(domDocument1, "rect", "ry", QRegExp())) {
439 anyConverted = true;
440 }
441
442 // gerber can't handle paths with curves
443 if (TextUtils::squashElement(domDocument1, "path", "d", AaCc)) {
444 anyConverted = true;
445 }
446
447 // gerber can't handle multiple subpaths if there are intersections
448 if (TextUtils::squashElement(domDocument1, "path", "d", MultipleZs)) {
449 anyConverted = true;
450 }
451
452 if (TextUtils::squashElement(domDocument1, "image", "", QRegExp())) {
453 anyConverted = true;
454 }
455
456 // can't handle scaled paths very well. There is probably a deeper bug that needs to be chased down.
457 // is this only necessary for contour view?
458 QDomNodeList nodeList = root1.elementsByTagName("path");
459 for (int i = 0; i < nodeList.count(); i++) {
460 QDomNode parent = nodeList.at(i);
461 while (!parent.isNull()) {
462 QString transformString = parent.toElement().attribute("transform");
463 if (!transformString.isNull()) {
464 QMatrix matrix = TextUtils::transformStringToMatrix(transformString);
465 QTransform transform(matrix);
466 if (transform.isScaling()) {
467 nodeList.at(i).toElement().setTagName("g");
468 anyConverted = true;
469 break;
470 }
471
472 }
473
474 parent = parent.parentNode();
475 }
476 }
477
478 QVector <QDomElement> leaves1;
479 int transformCount1 = 0;
480 QDomElement e1 = domDocument1.documentElement();
481 TextUtils::collectLeaves(e1, transformCount1, leaves1);
482
483 QVector <QDomElement> leaves2;
484 int transformCount2 = 0;
485 QDomElement e2 = domDocument2.documentElement();
486 TextUtils::collectLeaves(e2, transformCount2, leaves2);
487
488 double res = GraphicsUtils::StandardFritzingDPI;
489 // convert from pixel dpi to StandardFritzingDPI
490 QRectF sourceRes(boardRect.left() * res / GraphicsUtils::SVGDPI, boardRect.top() * res / GraphicsUtils::SVGDPI,
491 boardRect.width() * res / GraphicsUtils::SVGDPI, boardRect.height() * res / GraphicsUtils::SVGDPI);
492 int twidth = sourceRes.width();
493 int theight = sourceRes.height();
494 QSize imgSize(twidth + 2, theight + 2);
495 QRectF target(0, 0, twidth, theight);
496
497 QImage * clipImage = NULL;
498 if (!clipString.isEmpty()) {
499 clipImage = new QImage(imgSize, QImage::Format_Mono);
500 clipImage->fill(0xffffffff);
501 clipImage->setDotsPerMeterX(res * GraphicsUtils::InchesPerMeter);
502 clipImage->setDotsPerMeterY(res * GraphicsUtils::InchesPerMeter);
503
504 QXmlStreamReader reader(clipString);
505 QSvgRenderer renderer(&reader);
506 QPainter painter;
507 painter.begin(clipImage);
508 renderer.render(&painter, target);
509 painter.end();
510
511 #ifndef QT_NO_DEBUG
512 clipImage->save(FolderUtils::getUserDataStorePath("") + "/clip.png");
513 #endif
514
515 }
516
517 svgString = TextUtils::removeXMLEntities(domDocument1.toString());
518
519 QList<QDomElement> possibleHoles;
520 QXmlStreamReader reader(svgString);
521 QSvgRenderer renderer(&reader);
522 bool anyClipped = false;
523 if (forWhy != SVG2gerber::ForOutline) {
524 for (int i = 0; i < transformCount1; i++) {
525 QString n = QString::number(i);
526 QRectF bounds = renderer.boundsOnElement(n);
527 QMatrix m = renderer.matrixForElement(n);
528 QDomElement element = leaves1.at(i);
529 QRectF mBounds = m.mapRect(bounds);
530 if (mBounds.left() < sourceRes.left() - 0.1|| mBounds.top() < sourceRes.top() - 0.1 || mBounds.right() > sourceRes.right() + 0.1 || mBounds.bottom() > sourceRes.bottom() + 0.1) {
531 if (element.tagName() == "circle") {
532 possibleHoles.append(element);
533 }
534 // element is outside of bounds--squash it so it will be clipped
535 // we don't care if the board shape is irregular
536 // since anything printed between the shape and the bounding rectangle
537 // will be physically clipped when the board is cut out
538 element.setTagName("g");
539 anyClipped = anyConverted = true;
540 }
541 }
542 }
543
544
545 if (possibleHoles.count() > 0) {
546 QList<QDomElement> newHoles;
547 int ix = 0;
548 foreach (QDomElement element, possibleHoles) {
549 QDomElement newElement = element.cloneNode(false).toElement();
550 double radius = element.attribute("r").toDouble();
551 double sw = element.attribute("stroke-width").toDouble();
552 element.parentNode().insertAfter(newElement, element);
553 newElement.setAttribute("id", QString("__%1__").arg(ix++));
554 newElement.setAttribute("stroke-width", 0);
555 newElement.setAttribute("r", QString::number(radius - (sw / 2)));
556 newElement.setTagName("circle");
557 newHoles.append(newElement);
558 }
559
560 QSvgRenderer renderer(domDocument1.toByteArray());
561 for (int i = newHoles.count() - 1; i >= 0; i--) {
562 QString id = QString("__%1__").arg(i);
563 QRectF bounds = renderer.boundsOnElement(id);
564 QMatrix m = renderer.matrixForElement(id);
565 QDomElement newElement = newHoles.at(i);
566 QRectF mBounds = m.mapRect(bounds);
567 if (mBounds.left() < sourceRes.left() - 0.1 || mBounds.top() < sourceRes.top() - 0.1 || mBounds.right() > sourceRes.right() + 0.1 || mBounds.bottom() > sourceRes.bottom() + 0.1) {
568 // hole is still clipped
569 newHoles.removeAt(i);
570 newElement.parentNode().removeChild(newElement);
571 }
572 else {
573 // enlarge it a little due to aliasing when the clipped portion is converted to raster and back
574 double radius = newElement.attribute("r").toDouble();
575 radius += 4;
576 newElement.setAttribute("r", QString::number(radius));
577 newElement.setAttribute("stroke-width", 2);
578 }
579 }
580 }
581
582 if (clipImage) {
583 QImage another(imgSize, QImage::Format_Mono);
584 another.fill(0xffffffff);
585 another.setDotsPerMeterX(res * GraphicsUtils::InchesPerMeter);
586 another.setDotsPerMeterY(res * GraphicsUtils::InchesPerMeter);
587
588 svgString = TextUtils::removeXMLEntities(domDocument1.toString());
589 QXmlStreamReader reader(svgString);
590 QSvgRenderer renderer(&reader);
591 QPainter painter;
592 painter.begin(&another);
593 renderer.render(&painter, target);
594 painter.end();
595
596 for (int i = 0; i < transformCount1; i++) {
597 QDomElement element = leaves1.at(i);
598 if (element.tagName().compare("g") == 0) {
599 // element is already converted to raster space, we'll clip it later
600 continue;
601 }
602
603 QString n = QString::number(i);
604 QRectF bounds = renderer.boundsOnElement(n);
605 QMatrix m = renderer.matrixForElement(n);
606 QRectF mBounds = m.mapRect(bounds);
607
608 int x1 = qFloor(qMax(0.0, mBounds.left() - sourceRes.left())); // atmel compiler fails without cast
609 int x2 = qCeil(qMin(sourceRes.width(), mBounds.right() - sourceRes.left()));
610 int y1 = qFloor(qMax(0.0, mBounds.top() - sourceRes.top())); // atmel compiler fails without cast
611 int y2 = qCeil(qMin(sourceRes.height(), mBounds.bottom() - sourceRes.top()));
612
613 if (pixelsCollide(&another, clipImage, x1, y1, x2, y2)) {
614 element.setTagName("g");
615 anyClipped = anyConverted = true;
616 }
617 }
618 }
619
620 if (anyClipped) {
621 // svg has been changed by clipping process so get the string again
622 svgString = TextUtils::removeXMLEntities(domDocument1.toString());
623 }
624
625 if (anyConverted) {
626 for (int i = 0; i < transformCount1; i++) {
627 QDomElement element1 = leaves1.at(i);
628 if (element1.tagName().compare("g") != 0) {
629 // document 1 element svg can be directly converted to gerber
630 // so remove it from document 2
631 QDomElement element2 = leaves2.at(i);
632 element2.setTagName("g");
633 }
634 }
635
636
637 // expand the svg to fill the space of the image
638 QDomElement root2 = domDocument2.documentElement();
639 root2.setAttribute("width", QString("%1px").arg(twidth));
640 root2.setAttribute("height", QString("%1px").arg(theight));
641 if (boardRect.x() != 0 || boardRect.y() != 0) {
642 QString viewBox = root2.attribute("viewBox");
643 QStringList coords = viewBox.split(" ", QString::SkipEmptyParts);
644 coords[0] = QString::number(sourceRes.left());
645 coords[1] = QString::number(sourceRes.top());
646 root2.setAttribute("viewBox", coords.join(" "));
647 }
648
649 QStringList exceptions;
650 exceptions << "none" << "";
651 QString toColor("#000000");
652 SvgFileSplitter::changeColors(root2, toColor, exceptions);
653
654 QImage image(imgSize, QImage::Format_Mono);
655 image.setDotsPerMeterX(res * GraphicsUtils::InchesPerMeter);
656 image.setDotsPerMeterY(res * GraphicsUtils::InchesPerMeter);
657
658 if (forWhy == SVG2gerber::ForOutline) {
659 QDomNodeList paths = root2.elementsByTagName("path");
660 if (paths.count() == 0) {
661 // some non-path element makes up the outline
662 mergeOutlineElement(image, target, res, domDocument2, svgString, 0, layerName);
663 }
664 else {
665 for (int p = 0; p < paths.count(); p++) {
666 QDomElement path = paths.at(p).toElement();
667 path.setTagName("g");
668 }
669 for (int p = 0; p < paths.count(); p++) {
670 QDomElement path = paths.at(p).toElement();
671 path.setTagName("path");
672 if (p > 0) {
673 paths.at(p - 1).toElement().setTagName("g");
674 }
675 mergeOutlineElement(image, target, res, domDocument2, svgString, p, layerName);
676 }
677 }
678 }
679 else {
680 image.fill(0xffffffff);
681 QByteArray svg = TextUtils::removeXMLEntities(domDocument2.toString()).toUtf8();
682 QSvgRenderer renderer(svg);
683 QPainter painter;
684 painter.begin(&image);
685 renderer.render(&painter, target);
686 painter.end();
687 image.invertPixels(); // need white pixels on a black background for GroundPlaneGenerator
688
689 #ifndef QT_NO_DEBUG
690 image.save(FolderUtils::getUserDataStorePath("") + "/preclip_output.png");
691 #endif
692
693 if (clipImage != NULL) {
694 // can this be done with a single blt using composition mode
695 // if not, grab a scanline instead of testing every pixel
696 for (int y = 0; y < theight; y++) {
697 for (int x = 0; x < twidth; x++) {
698 if (clipImage->pixel(x, y) != 0xffffffff) {
699 image.setPixel(x, y, 0);
700 }
701 }
702 }
703 }
704
705 #ifndef QT_NO_DEBUG
706 image.save(FolderUtils::getUserDataStorePath("") + "/output.png");
707 #endif
708
709 QString path = makePath(image, res / GraphicsUtils::StandardFritzingDPI, "#000000");
710 svgString.replace("</svg>", path + "</svg>");
711
712 /*
713
714 GroundPlaneGenerator gpg;
715 gpg.setLayerName(layerName);
716 gpg.setMinRunSize(1, 1);
717 gpg.scanImage(image, image.width(), image.height(), GraphicsUtils::StandardFritzingDPI / res, GraphicsUtils::StandardFritzingDPI, "#000000", false, false, QSizeF(0, 0), 0, sourceRes.topLeft());
718 if (gpg.newSVGs().count() > 0) {
719 svgString = gpg.mergeSVGs(svgString, "");
720 }
721
722 */
723 }
724 }
725
726 if (clipImage) delete clipImage;
727
728 return QString(svgString);
729 }
730
cleanOutline(const QString & outlineSvg)731 QString GerberGenerator::cleanOutline(const QString & outlineSvg)
732 {
733 QDomDocument doc;
734 doc.setContent(outlineSvg);
735 QList<QDomElement> leaves;
736 QDomElement root = doc.documentElement();
737 TextUtils::collectLeaves(root, leaves);
738 QDomNodeList textNodes = root.elementsByTagName("text");
739 for (int t = 0; t < textNodes.count(); t++) {
740 leaves << textNodes.at(t).toElement();
741 }
742
743 if (leaves.count() == 0) return "";
744 if (leaves.count() == 1) return outlineSvg;
745
746 if (leaves.count() > 1) {
747 for (int i = 0; i < leaves.count(); i++) {
748 QDomElement leaf = leaves.at(i);
749 if (leaf.attribute("id", "").compare(MagicBoardOutlineID) == 0) {
750 for (int j = 0; j < leaves.count(); j++) {
751 if (i != j) {
752 QDomElement jleaf = leaves.at(j);
753 jleaf.parentNode().removeChild(jleaf);
754 }
755 }
756
757 return doc.toString();
758 }
759 }
760 }
761
762 if (leaves.count() == 0) return "";
763
764 return outlineSvg;
765 }
766
mergeOutlineElement(QImage & image,QRectF & target,double res,QDomDocument & document,QString & svgString,int ix,const QString & layerName)767 void GerberGenerator::mergeOutlineElement(QImage & image, QRectF & target, double res, QDomDocument & document, QString & svgString, int ix, const QString & layerName) {
768
769 image.fill(0xffffffff);
770 QByteArray svg = TextUtils::removeXMLEntities(document.toString()).toUtf8();
771
772 QSvgRenderer renderer(svg);
773 QPainter painter;
774 painter.begin(&image);
775 renderer.render(&painter, target);
776 painter.end();
777 image.invertPixels(); // need white pixels on a black background for GroundPlaneGenerator
778
779 #ifndef QT_NO_DEBUG
780 image.save(QString("%2/output%1.png").arg(ix).arg(FolderUtils::getUserDataStorePath("")));
781 #else
782 Q_UNUSED(ix);
783 #endif
784
785 GroundPlaneGenerator gpg;
786 gpg.setLayerName(layerName);
787 gpg.setMinRunSize(1, 1);
788 gpg.scanOutline(image, image.width(), image.height(), GraphicsUtils::StandardFritzingDPI / res, GraphicsUtils::StandardFritzingDPI, "#000000", false, false, QSizeF(0, 0), 0);
789 if (gpg.newSVGs().count() > 0) {
790 svgString = gpg.mergeSVGs(svgString, "");
791 }
792 }
793
makePath(QImage & image,double unit,const QString & colorString)794 QString GerberGenerator::makePath(QImage & image, double unit, const QString & colorString)
795 {
796 double halfUnit = unit / 2;
797 QString paths;
798 int lineCount = 0;
799 for (int y = 0; y < image.height(); y++) {
800 bool inWhite = false;
801 int whiteStart = 0;
802 for (int x = 0; x < image.width(); x++) {
803 QRgb current = image.pixel(x, y);
804 if (inWhite) {
805 if (current == 0xffffffff) {
806 // another white pixel, keep moving
807 continue;
808 }
809
810 // got black: close up this segment;
811 inWhite = false;
812 paths += QString("M%1,%2L%3,%2 ").arg(whiteStart + halfUnit).arg(y + halfUnit).arg(x - 1 + halfUnit);
813 if (++lineCount == 10) {
814 lineCount = 0;
815 paths += "\n";
816 }
817 }
818 else {
819 if (current != 0xffffffff) {
820 // another black pixel, keep moving
821 continue;
822 }
823
824 inWhite = true;
825 whiteStart = x;
826 }
827 }
828 }
829
830 QString path = QString("<path fill='none' stroke='%1' stroke-width='%2' stroke-linecap='square' d='").arg(colorString).arg(unit);
831 return path + paths + "' />\n";
832 }
833
dealWithMultipleContours(QDomElement & root,bool displayMessageBoxes)834 bool GerberGenerator::dealWithMultipleContours(QDomElement & root, bool displayMessageBoxes) {
835 bool multipleContours = false;
836 bool contoursOK = true;
837
838 // split path into multiple contours
839 QDomNodeList paths = root.elementsByTagName("path");
840 // should only be one
841 for (int p = 0; p < paths.count() && contoursOK; p++) {
842 QDomElement path = paths.at(p).toElement();
843 QString originalPath = path.attribute("d", "").trimmed();
844 if (MultipleZs.indexIn(originalPath) < 0) continue;
845
846 multipleContours = true;
847 QStringList subpaths = path.attribute("d").split("z", QString::SkipEmptyParts);
848 foreach (QString subpath, subpaths) {
849 if (!subpath.trimmed().startsWith("m", Qt::CaseInsensitive)) {
850 contoursOK = false;
851 break;
852 }
853 }
854 }
855
856 if (!multipleContours) return false;
857
858 if (!contoursOK) {
859 QString msg =
860 QObject::tr("Fritzing is unable to process the cutouts in this custom PCB shape. ") +
861 QObject::tr("You may need to reload the shape SVG. ") +
862 QObject::tr("Fritzing requires that you make cutouts using a shape 'subtraction' or 'difference' operation in your vector graphics editor.");
863 displayMessage(msg, displayMessageBoxes);
864 return false;
865 }
866
867 for (int p = 0; p < paths.count(); p++) {
868 QDomElement path = paths.at(p).toElement();
869 QString originalPath = path.attribute("d", "").trimmed();
870 if (MultipleZs.indexIn(originalPath) >= 0) {
871 QStringList subpaths = path.attribute("d").split("z", QString::SkipEmptyParts);
872 QString priorM;
873 MFinder.indexIn(subpaths.at(0).trimmed());
874 priorM += MFinder.cap(1) + MFinder.cap(2) + "," + MFinder.cap(3) + " ";
875 for (int i = 1; i < subpaths.count(); i++) {
876 QDomElement newPath = path.cloneNode(true).toElement();
877 QString z = ((i < subpaths.count() - 1) || originalPath.endsWith("z", Qt::CaseInsensitive)) ? "z" : "";
878 QString d = subpaths.at(i).trimmed() + z;
879 MFinder.indexIn(d);
880 if (d.startsWith("m", Qt::CaseSensitive)) {
881 d = priorM + d;
882 }
883 priorM += MFinder.cap(1) + MFinder.cap(2) + "," + MFinder.cap(3) + " ";
884 newPath.setAttribute("d", d);
885 path.parentNode().appendChild(newPath);
886 }
887 path.setAttribute("d", subpaths.at(0) + "z");
888 }
889 }
890
891 return true;
892 }
893
exportPickAndPlace(const QString & prefix,const QString & exportDir,ItemBase * board,PCBSketchWidget * sketchWidget,bool displayMessageBoxes)894 void GerberGenerator::exportPickAndPlace(const QString & prefix, const QString & exportDir, ItemBase * board, PCBSketchWidget * sketchWidget, bool displayMessageBoxes)
895 {
896 QPointF bottomLeft = board->sceneBoundingRect().bottomLeft();
897 QSet<ItemBase *> itemBases;
898 foreach (QGraphicsItem * item, sketchWidget->scene()->collidingItems(board)) {
899 ItemBase * itemBase = dynamic_cast<ItemBase *>(item);
900 if (itemBase == NULL) continue;
901 if (itemBase == board) continue;
902 if (itemBase->itemType() == ModelPart::Wire) continue;
903
904 itemBase = itemBase->layerKinChief();
905 if (!itemBase->isEverVisible()) continue;
906 if (itemBase == board) continue;
907
908 itemBases.insert(itemBase->layerKinChief());
909 }
910
911 QString outname = exportDir + "/" + prefix + "_pnp.txt";
912 QFile out(outname);
913 if (!out.open(QIODevice::WriteOnly | QIODevice::Text)) {
914 displayMessage(QObject::tr("Unable to save pick and place file: %2").arg(outname), displayMessageBoxes);
915 return;
916 }
917
918 QTextStream stream(&out);
919 stream << "*Pick And Place List\n"
920 << "*Company=\n"
921 << "*Author=\n"
922 //*Tel=
923 //*Fax=
924 << "*eMail=\n"
925 << "*\n"
926 << QString("*Project=%1\n").arg(prefix)
927 // *Variant=<alle Bauteile>
928 << QString("*Date=%1\n").arg(QTime::currentTime().toString())
929 << QString("*CreatedBy=Fritzing %1\n").arg(Version::versionString())
930 << "*\n"
931 << "*\n*Coordinates in mm, always center of component\n"
932 << "*Origin 0/0=Lower left corner of PCB\n"
933 << "*Rotation in degree (0-360, math. pos.)\n"
934 << "*\n"
935 << "*No;Value;Package;X;Y;Rotation;Side;Name\n"
936 ;
937
938 QStringList valueKeys;
939 valueKeys << "resistance" << "capacitance" << "inductance" << "voltage" << "current" << "power";
940
941 int ix = 1;
942 foreach (ItemBase * itemBase, itemBases) {
943 QString value;
944 foreach (QString valueKey, valueKeys) {
945 value = itemBase->modelPart()->localProp(valueKey).toString();
946 if (!value.isEmpty()) break;
947
948 value = itemBase->modelPart()->properties().value(valueKey);
949 if (!value.isEmpty()) break;
950 }
951
952 QPointF loc = itemBase->sceneBoundingRect().center();
953 QTransform transform = itemBase->transform();
954 // doesn't account for scaling
955 double angle = atan2(transform.m12(), transform.m11()) * 180 / M_PI;
956 // No;Value;Package;X;Y;Rotation;Side;Name
957 QString string = QString("%1;%2;%3;%4;%5;%6;%7;%8\n")
958 .arg(ix++)
959 .arg(value)
960 .arg(itemBase->modelPart()->properties().value("package"))
961 .arg(GraphicsUtils::pixels2mm(loc.x() - bottomLeft.x(), GraphicsUtils::SVGDPI))
962 .arg(GraphicsUtils::pixels2mm(loc.y() - bottomLeft.y(), GraphicsUtils::SVGDPI))
963 .arg(angle)
964 .arg(itemBase->viewLayerID() == ViewLayer::Copper1 ? "Top" : "Bottom")
965 .arg(itemBase->instanceTitle())
966 ;
967 stream << string;
968 stream.flush();
969 }
970
971 out.close();
972 }
973
handleDonuts(QDomElement & root1,QMultiHash<long,ConnectorItem * > & treatAsCircle)974 void GerberGenerator::handleDonuts(QDomElement & root1, QMultiHash<long, ConnectorItem *> & treatAsCircle) {
975 // most of this would not be necessary if we cached cleaned SVGs
976
977 static const QString unique("%%%%%%%%%%%%%%%%%%%%%%%%_________________________________%%%%%%%%%%%%%%%%%%%%%%%%%%%%%");
978
979 QDomNodeList nodeList = root1.elementsByTagName("path");
980 if (treatAsCircle.count() > 0) {
981 QStringList ids;
982 foreach (ConnectorItem * connectorItem, treatAsCircle.values()) {
983 ItemBase * itemBase = connectorItem->attachedTo();
984 SvgIdLayer * svgIdLayer = connectorItem->connector()->fullPinInfo(itemBase->viewID(), itemBase->viewLayerID());
985 DebugDialog::debug(QString("treat as circle %1").arg(svgIdLayer->m_svgId));
986 ids << svgIdLayer->m_svgId;
987 }
988
989 for (int n = 0; n < nodeList.count(); n++) {
990 QDomElement path = nodeList.at(n).toElement();
991 QString id = path.attribute("id");
992 if (id.isEmpty()) continue;
993
994 DebugDialog::debug(QString("checking for %1").arg(id));
995 if (!ids.contains(id)) continue;
996
997 QString pid;
998 ConnectorItem * connectorItem = NULL;
999 for (QDomElement parent = path.parentNode().toElement(); !parent.isNull(); parent = parent.parentNode().toElement()) {
1000 pid = parent.attribute("partID");
1001 if (pid.isEmpty()) continue;
1002
1003 QList<ConnectorItem *> connectorItems = treatAsCircle.values(pid.toLong());
1004 if (connectorItems.count() == 0) break;
1005
1006 foreach (ConnectorItem * candidate, connectorItems) {
1007 ItemBase * itemBase = candidate->attachedTo();
1008 SvgIdLayer * svgIdLayer = candidate->connector()->fullPinInfo(itemBase->viewID(), itemBase->viewLayerID());
1009 if (svgIdLayer->m_svgId == id) {
1010 connectorItem = candidate;
1011 break;
1012 }
1013 }
1014
1015 if (connectorItem) break;
1016 }
1017 if (connectorItem == NULL) continue;
1018
1019 //QString string;
1020 //QTextStream stream(&string);
1021 //path.save(stream, 0);
1022 //DebugDialog::debug("path " + string);
1023
1024 connectorItem->debugInfo("make path");
1025 path.setAttribute("id", unique);
1026 QSvgRenderer renderer;
1027 renderer.load(root1.ownerDocument().toByteArray());
1028 QRectF bounds = renderer.boundsOnElement(unique);
1029 path.removeAttribute("id");
1030
1031 QDomElement circle = root1.ownerDocument().createElement("circle");
1032 path.parentNode().insertBefore(circle, path);
1033 circle.setAttribute("id", id);
1034 QPointF p = bounds.center();
1035 circle.setAttribute("cx", QString::number(p.x()));
1036 circle.setAttribute("cy", QString::number(p.y()));
1037 circle.setAttribute("r", QString::number(connectorItem->radius() * GraphicsUtils::StandardFritzingDPI / GraphicsUtils::SVGDPI));
1038 circle.setAttribute("stroke-width", QString::number(connectorItem->strokeWidth() * GraphicsUtils::StandardFritzingDPI / GraphicsUtils::SVGDPI));
1039
1040 }
1041 }
1042 }
1043
renderTo(const LayerList & layers,ItemBase * board,PCBSketchWidget * sketchWidget,bool & empty)1044 QString GerberGenerator::renderTo(const LayerList & layers, ItemBase * board, PCBSketchWidget * sketchWidget, bool & empty) {
1045 RenderThing renderThing;
1046 renderThing.printerScale = GraphicsUtils::SVGDPI;
1047 renderThing.blackOnly = true;
1048 renderThing.dpi = GraphicsUtils::StandardFritzingDPI;
1049 renderThing.hideTerminalPoints = true;
1050 renderThing.selectedItems = renderThing.renderBlocker = false;
1051 QString svg = sketchWidget->renderToSVG(renderThing, board, layers);
1052 empty = renderThing.empty;
1053 return svg;
1054 }
1055