src/extPrograms.cpp

changeset 931
85080f7a1c20
parent 929
66e8453e1768
child 941
f895379d7fab
equal deleted inserted replaced
929:66e8453e1768 931:85080f7a1c20
1 /*
2 * LDForge: LDraw parts authoring CAD
3 * Copyright (C) 2013 - 2015 Teemu Piippo
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include <QProcess>
20 #include <QTemporaryFile>
21 #include <QDialog>
22 #include <QDialogButtonBox>
23 #include <QSpinBox>
24 #include <QCheckBox>
25 #include <QComboBox>
26 #include <QGridLayout>
27 #include <QFileInfo>
28 #include "main.h"
29 #include "configuration.h"
30 #include "miscallenous.h"
31 #include "mainWindow.h"
32 #include "ldDocument.h"
33 #include "radioGroup.h"
34 #include "editHistory.h"
35 #include "ui_ytruder.h"
36 #include "ui_intersector.h"
37 #include "ui_rectifier.h"
38 #include "ui_coverer.h"
39 #include "ui_isecalc.h"
40 #include "ui_edger2.h"
41 #include "dialogs.h"
42
43 enum extprog
44 {
45 Isecalc,
46 Intersector,
47 Coverer,
48 Ytruder,
49 Rectifier,
50 Edger2,
51 };
52
53 // =============================================================================
54 //
55 CFGENTRY (String, IsecalcPath, "")
56 CFGENTRY (String, IntersectorPath, "")
57 CFGENTRY (String, CovererPath, "")
58 CFGENTRY (String, YtruderPath, "")
59 CFGENTRY (String, RectifierPath, "")
60 CFGENTRY (String, Edger2Path, "")
61
62 QString* const g_extProgPaths[] =
63 {
64 &cfg::IsecalcPath,
65 &cfg::IntersectorPath,
66 &cfg::CovererPath,
67 &cfg::YtruderPath,
68 &cfg::RectifierPath,
69 &cfg::Edger2Path,
70 };
71
72 CFGENTRY (Bool, IsecalcUsesWine, false)
73 CFGENTRY (Bool, IntersectorUsesWine, false)
74 CFGENTRY (Bool, CovererUsesWine, false)
75 CFGENTRY (Bool, YtruderUsesWine, false)
76 CFGENTRY (Bool, RectifierUsesWine, false)
77 CFGENTRY (Bool, Edger2UsesWine, false)
78
79 bool* const g_extProgWine[] =
80 {
81 &cfg::IsecalcUsesWine,
82 &cfg::IntersectorUsesWine,
83 &cfg::CovererUsesWine,
84 &cfg::YtruderUsesWine,
85 &cfg::RectifierUsesWine,
86 &cfg::Edger2UsesWine,
87 };
88
89 const char* g_extProgNames[] =
90 {
91 "Isecalc",
92 "Intersector",
93 "Coverer",
94 "Ytruder",
95 "Rectifier",
96 "Edger2"
97 };
98
99 // =============================================================================
100 //
101 static bool MakeTempFile (QTemporaryFile& tmp, QString& fname)
102 {
103 if (not tmp.open())
104 return false;
105
106 fname = tmp.fileName();
107 tmp.close();
108 return true;
109 }
110
111 // =============================================================================
112 //
113 static bool CheckExtProgramPath (const extprog prog)
114 {
115 QString& path = *g_extProgPaths[prog];
116
117 if (not path.isEmpty())
118 return true;
119
120 ExtProgPathPrompt* dlg = new ExtProgPathPrompt (g_extProgNames[prog]);
121
122 if (dlg->exec() and not dlg->getPath().isEmpty())
123 {
124 path = dlg->getPath();
125 return true;
126 }
127
128 return false;
129 }
130
131 // =============================================================================
132 //
133 static QString ProcessExtProgError (extprog prog, QProcess& proc)
134 {
135 switch (proc.error())
136 {
137 case QProcess::FailedToStart:
138 {
139 QString wineblurb;
140
141 #ifndef _WIN32
142 if (*g_extProgWine[prog])
143 wineblurb = "make sure Wine is installed and ";
144 #else
145 (void) prog;
146 #endif
147
148 return format ("Program failed to start, %1check your permissions", wineblurb);
149 } break;
150
151 case QProcess::Crashed:
152 return "Crashed.";
153
154 case QProcess::WriteError:
155 case QProcess::ReadError:
156 return "I/O error.";
157
158 case QProcess::UnknownError:
159 return "Unknown error";
160
161 case QProcess::Timedout:
162 return format ("Timed out (30 seconds)");
163 }
164
165 return "";
166 }
167
168 // =============================================================================
169 //
170 static void WriteObjects (const LDObjectList& objects, QFile& f)
171 {
172 for (LDObjectPtr obj : objects)
173 {
174 if (obj->type() == OBJ_Subfile)
175 {
176 LDSubfilePtr ref = obj.staticCast<LDSubfile>();
177 LDObjectList objs = ref->inlineContents (true, false);
178
179 WriteObjects (objs, f);
180
181 for (LDObjectPtr obj : objs)
182 obj->destroy();
183 }
184 else
185 f.write ((obj->asText() + "\r\n").toUtf8());
186 }
187 }
188
189 // =============================================================================
190 //
191 static void WriteObjects (const LDObjectList& objects, QString fname)
192 {
193 // Write the input file
194 QFile f (fname);
195
196 if (not f.open (QIODevice::WriteOnly | QIODevice::Text))
197 {
198 Critical (format ("Couldn't open temporary file %1 for writing: %2\n", fname, f.errorString()));
199 return;
200 }
201
202 WriteObjects (objects, f);
203 f.close();
204
205 #ifdef DEBUG
206 QFile::copy (fname, "debug_lastInput");
207 #endif
208 }
209
210 // =============================================================================
211 //
212 void WriteSelection (QString fname)
213 {
214 WriteObjects (Selection(), fname);
215 }
216
217 // =============================================================================
218 //
219 void WriteColorGroup (LDColor color, QString fname)
220 {
221 LDObjectList objects;
222
223 for (LDObjectPtr obj : CurrentDocument()->objects())
224 {
225 if (not obj->isColored() or obj->color() != color)
226 continue;
227
228 objects << obj;
229 }
230
231 WriteObjects (objects, fname);
232 }
233
234 // =============================================================================
235 //
236 bool RunExtProgram (extprog prog, QString path, QString argvstr)
237 {
238 QTemporaryFile input;
239 QStringList argv = argvstr.split (" ", QString::SkipEmptyParts);
240
241 #ifndef _WIN32
242 if (*g_extProgWine[prog])
243 {
244 argv.insert (0, path);
245 path = "wine";
246 }
247 #endif // _WIN32
248
249 print ("Running command: %1 %2\n", path, argv.join (" "));
250
251 if (not input.open())
252 return false;
253
254 QProcess proc;
255
256 // Begin!
257 proc.setStandardInputFile (input.fileName());
258 proc.start (path, argv);
259
260 if (not proc.waitForStarted())
261 {
262 Critical (format ("Couldn't start %1: %2\n", g_extProgNames[prog], ProcessExtProgError (prog, proc)));
263 return false;
264 }
265
266 // Write an enter, the utility tools all expect one
267 input.write ("\n");
268
269 // Wait while it runs
270 proc.waitForFinished();
271
272 QString err = "";
273
274 if (proc.exitStatus() != QProcess::NormalExit)
275 err = ProcessExtProgError (prog, proc);
276
277 // Check the return code
278 if (proc.exitCode() != 0)
279 err = format ("Program exited abnormally (return code %1).", proc.exitCode());
280
281 if (not err.isEmpty())
282 {
283 Critical (format ("%1 failed: %2\n", g_extProgNames[prog], err));
284 QString filename ("externalProgramOutput.txt");
285 QFile file (filename);
286
287 if (file.open (QIODevice::WriteOnly | QIODevice::Text))
288 {
289 file.write (proc.readAllStandardOutput());
290 file.write (proc.readAllStandardError());
291 print ("Wrote output and error logs to %1", QFileInfo (file).absoluteFilePath());
292 }
293 else
294 {
295 print ("Couldn't open %1 for writing: %2",
296 QFileInfo (filename).absoluteFilePath(), file.errorString());
297 }
298
299 return false;
300 }
301
302 return true;
303 }
304
305 // =============================================================================
306 //
307 static void InsertOutput (QString fname, bool replace, QList<LDColor> colorsToReplace)
308 {
309 #ifdef DEBUG
310 QFile::copy (fname, "./debug_lastOutput");
311 #endif // RELEASE
312
313 // Read the output file
314 QFile f (fname);
315
316 if (not f.open (QIODevice::ReadOnly))
317 {
318 Critical (format ("Couldn't open temporary file %1 for reading.\n", fname));
319 return;
320 }
321
322 LDObjectList objs = LoadFileContents (&f, null);
323
324 // If we replace the objects, delete the selection now.
325 if (replace)
326 g_win->deleteSelection();
327
328 for (LDColor color : colorsToReplace)
329 g_win->deleteByColor (color);
330
331 // Insert the new objects
332 CurrentDocument()->clearSelection();
333
334 for (LDObjectPtr obj : objs)
335 {
336 if (not obj->isScemantic())
337 {
338 obj->destroy();
339 continue;
340 }
341
342 CurrentDocument()->addObject (obj);
343 obj->select();
344 }
345
346 g_win->doFullRefresh();
347 }
348
349 // =============================================================================
350 // Interface for Ytruder
351 // =============================================================================
352 void MainWindow::actionYtruder()
353 {
354 setlocale (LC_ALL, "C");
355
356 if (not CheckExtProgramPath (Ytruder))
357 return;
358
359 QDialog* dlg = new QDialog;
360 Ui::YtruderUI ui;
361 ui.setupUi (dlg);
362
363 if (not dlg->exec())
364 return;
365
366 // Read the user's choices
367 const enum { Distance, Symmetry, Projection, Radial } mode =
368 ui.mode_distance->isChecked() ? Distance :
369 ui.mode_symmetry->isChecked() ? Symmetry :
370 ui.mode_projection->isChecked() ? Projection : Radial;
371
372 const Axis axis =
373 ui.axis_x->isChecked() ? X :
374 ui.axis_y->isChecked() ? Y : Z;
375
376 const double depth = ui.planeDepth->value(),
377 condAngle = ui.condAngle->value();
378
379 QTemporaryFile indat, outdat;
380 QString inDATName, outDATName;
381
382 // Make temp files for the input and output files
383 if (not MakeTempFile (indat, inDATName) or not MakeTempFile (outdat, outDATName))
384 return;
385
386 // Compose the command-line arguments
387 QString argv = Join (
388 {
389 (axis == X) ? "-x" : (axis == Y) ? "-y" : "-z",
390 (mode == Distance) ? "-d" : (mode == Symmetry) ? "-s" : (mode == Projection) ? "-p" : "-r",
391 depth,
392 "-a",
393 condAngle,
394 inDATName,
395 outDATName
396 });
397
398 WriteSelection (inDATName);
399
400 if (not RunExtProgram (Ytruder, cfg::YtruderPath, argv))
401 return;
402
403 InsertOutput (outDATName, false, {});
404 }
405
406 // =============================================================================
407 // Rectifier interface
408 // =============================================================================
409 void MainWindow::actionRectifier()
410 {
411 setlocale (LC_ALL, "C");
412
413 if (not CheckExtProgramPath (Rectifier))
414 return;
415
416 QDialog* dlg = new QDialog;
417 Ui::RectifierUI ui;
418 ui.setupUi (dlg);
419
420 if (not dlg->exec())
421 return;
422
423 QTemporaryFile indat, outdat;
424 QString inDATName, outDATName;
425
426 // Make temp files for the input and output files
427 if (not MakeTempFile (indat, inDATName) or not MakeTempFile (outdat, outDATName))
428 return;
429
430 // Compose arguments
431 QString argv = Join (
432 {
433 (not ui.cb_condense->isChecked()) ? "-q" : "",
434 (not ui.cb_subst->isChecked()) ? "-r" : "",
435 (ui.cb_condlineCheck->isChecked()) ? "-a" : "",
436 (ui.cb_colorize->isChecked()) ? "-c" : "",
437 "-t",
438 ui.dsb_coplthres->value(),
439 inDATName,
440 outDATName
441 });
442
443 WriteSelection (inDATName);
444
445 if (not RunExtProgram (Rectifier, cfg::RectifierPath, argv))
446 return;
447
448 InsertOutput (outDATName, true, {});
449 }
450
451 // =============================================================================
452 // Intersector interface
453 // =============================================================================
454 void MainWindow::actionIntersector()
455 {
456 setlocale (LC_ALL, "C");
457
458 if (not CheckExtProgramPath (Intersector))
459 return;
460
461 QDialog* dlg = new QDialog;
462 Ui::IntersectorUI ui;
463 ui.setupUi (dlg);
464
465 MakeColorComboBox (ui.cmb_incol);
466 MakeColorComboBox (ui.cmb_cutcol);
467 ui.cb_repeat->setWhatsThis ("If this is set, " APPNAME " runs Intersector a second time with inverse files to cut the "
468 " cutter group with the input group. Both groups are cut by the intersection.");
469 ui.cb_edges->setWhatsThis ("Makes " APPNAME " try run Isecalc to create edgelines for the intersection.");
470
471 LDColor inCol, cutCol;
472 const bool repeatInverse = ui.cb_repeat->isChecked();
473
474 forever
475 {
476 if (not dlg->exec())
477 return;
478
479 inCol = LDColor::fromIndex (ui.cmb_incol->itemData (ui.cmb_incol->currentIndex()).toInt());
480 cutCol = LDColor::fromIndex (ui.cmb_cutcol->itemData (ui.cmb_cutcol->currentIndex()).toInt());
481
482 if (inCol == cutCol)
483 {
484 Critical ("Cannot use the same color group for both input and cutter!");
485 continue;
486 }
487
488 break;
489 }
490
491 // Five temporary files!
492 // indat = input group file
493 // cutdat = cutter group file
494 // outdat = primary output
495 // outdat2 = inverse output
496 // edgesdat = edges output (isecalc)
497 QTemporaryFile indat, cutdat, outdat, outdat2, edgesdat;
498 QString inDATName, cutDATName, outDATName, outDAT2Name, edgesDATName;
499
500 if (not MakeTempFile (indat, inDATName) or
501 not MakeTempFile (cutdat, cutDATName) or
502 not MakeTempFile (outdat, outDATName) or
503 not MakeTempFile (outdat2, outDAT2Name) or
504 not MakeTempFile (edgesdat, edgesDATName))
505 {
506 return;
507 }
508
509 QString parms = Join (
510 {
511 (ui.cb_colorize->isChecked()) ? "-c" : "",
512 (ui.cb_nocondense->isChecked()) ? "-t" : "",
513 "-s",
514 ui.dsb_prescale->value()
515 });
516
517 QString argv_normal = Join (
518 {
519 parms,
520 inDATName,
521 cutDATName,
522 outDATName
523 });
524
525 QString argv_inverse = Join (
526 {
527 parms,
528 cutDATName,
529 inDATName,
530 outDAT2Name
531 });
532
533 WriteColorGroup (inCol, inDATName);
534 WriteColorGroup (cutCol, cutDATName);
535
536 if (not RunExtProgram (Intersector, cfg::IntersectorPath, argv_normal))
537 return;
538
539 InsertOutput (outDATName, false, {inCol});
540
541 if (repeatInverse and RunExtProgram (Intersector, cfg::IntersectorPath, argv_inverse))
542 InsertOutput (outDAT2Name, false, {cutCol});
543
544 if (ui.cb_edges->isChecked() and CheckExtProgramPath (Isecalc) and
545 RunExtProgram (Isecalc, cfg::IsecalcPath, Join ({inDATName, cutDATName, edgesDATName})))
546 {
547 InsertOutput (edgesDATName, false, {});
548 }
549 }
550
551 // =============================================================================
552 //
553 void MainWindow::actionCoverer()
554 {
555 setlocale (LC_ALL, "C");
556
557 if (not CheckExtProgramPath (Coverer))
558 return;
559
560 QDialog* dlg = new QDialog;
561 Ui::CovererUI ui;
562 ui.setupUi (dlg);
563 MakeColorComboBox (ui.cmb_col1);
564 MakeColorComboBox (ui.cmb_col2);
565
566 LDColor in1Col, in2Col;
567
568 forever
569 {
570 if (not dlg->exec())
571 return;
572
573 in1Col = LDColor::fromIndex (ui.cmb_col1->itemData (ui.cmb_col1->currentIndex()).toInt());
574 in2Col = LDColor::fromIndex (ui.cmb_col2->itemData (ui.cmb_col2->currentIndex()).toInt());
575
576 if (in1Col == in2Col)
577 {
578 Critical ("Cannot use the same color group for both inputs!");
579 continue;
580 }
581
582 break;
583 }
584
585 QTemporaryFile in1dat, in2dat, outdat;
586 QString in1DATName, in2DATName, outDATName;
587
588 if (not MakeTempFile (in1dat, in1DATName) or
589 not MakeTempFile (in2dat, in2DATName) or
590 not MakeTempFile (outdat, outDATName))
591 {
592 return;
593 }
594
595 QString argv = Join (
596 {
597 (ui.cb_oldsweep->isChecked() ? "-s" : ""),
598 (ui.cb_reverse->isChecked() ? "-r" : ""),
599 (ui.dsb_segsplit->value() != 0 ? format ("-l %1", ui.dsb_segsplit->value()) : ""),
600 (ui.sb_bias->value() != 0 ? format ("-s %1", ui.sb_bias->value()) : ""),
601 in1DATName,
602 in2DATName,
603 outDATName
604 });
605
606 WriteColorGroup (in1Col, in1DATName);
607 WriteColorGroup (in2Col, in2DATName);
608
609 if (not RunExtProgram (Coverer, cfg::CovererPath, argv))
610 return;
611
612 InsertOutput (outDATName, false, {});
613 }
614
615 // =============================================================================
616 //
617 void MainWindow::actionIsecalc()
618 {
619 setlocale (LC_ALL, "C");
620
621 if (not CheckExtProgramPath (Isecalc))
622 return;
623
624 Ui::IsecalcUI ui;
625 QDialog* dlg = new QDialog;
626 ui.setupUi (dlg);
627
628 MakeColorComboBox (ui.cmb_col1);
629 MakeColorComboBox (ui.cmb_col2);
630
631 LDColor in1Col, in2Col;
632
633 // Run the dialog and validate input
634 forever
635 {
636 if (not dlg->exec())
637 return;
638
639 in1Col = LDColor::fromIndex (ui.cmb_col1->itemData (ui.cmb_col1->currentIndex()).toInt());
640 in2Col = LDColor::fromIndex (ui.cmb_col2->itemData (ui.cmb_col2->currentIndex()).toInt());
641
642 if (in1Col == in2Col)
643 {
644 Critical ("Cannot use the same color group for both input and cutter!");
645 continue;
646 }
647
648 break;
649 }
650
651 QTemporaryFile in1dat, in2dat, outdat;
652 QString in1DATName, in2DATName, outDATName;
653
654 if (not MakeTempFile (in1dat, in1DATName) or
655 not MakeTempFile (in2dat, in2DATName) or
656 not MakeTempFile (outdat, outDATName))
657 {
658 return;
659 }
660
661 QString argv = Join (
662 {
663 in1DATName,
664 in2DATName,
665 outDATName
666 });
667
668 WriteColorGroup (in1Col, in1DATName);
669 WriteColorGroup (in2Col, in2DATName);
670 RunExtProgram (Isecalc, cfg::IsecalcPath, argv);
671 InsertOutput (outDATName, false, {});
672 }
673
674 // =============================================================================
675 //
676 void MainWindow::actionEdger2()
677 {
678 setlocale (LC_ALL, "C");
679
680 if (not CheckExtProgramPath (Edger2))
681 return;
682
683 QDialog* dlg = new QDialog;
684 Ui::Edger2Dialog ui;
685 ui.setupUi (dlg);
686
687 if (not dlg->exec())
688 return;
689
690 QTemporaryFile in, out;
691 QString inName, outName;
692
693 if (not MakeTempFile (in, inName) or not MakeTempFile (out, outName))
694 return;
695
696 int unmatched = ui.unmatched->currentIndex();
697
698 QString argv = Join (
699 {
700 format ("-p %1", ui.precision->value()),
701 format ("-af %1", ui.flatAngle->value()),
702 format ("-ac %1", ui.condAngle->value()),
703 format ("-ae %1", ui.edgeAngle->value()),
704 ui.delLines->isChecked() ? "-de" : "",
705 ui.delCondLines->isChecked() ? "-dc" : "",
706 ui.colored->isChecked() ? "-c" : "",
707 ui.bfc->isChecked() ? "-b" : "",
708 ui.convex->isChecked() ? "-cx" : "",
709 ui.concave->isChecked() ? "-cv" : "",
710 unmatched == 0 ? "-u+" : (unmatched == 2 ? "-u-" : ""),
711 inName,
712 outName,
713 });
714
715 WriteSelection (inName);
716
717 if (not RunExtProgram (Edger2, cfg::Edger2Path, argv))
718 return;
719
720 InsertOutput (outName, true, {});
721 }

mercurial