src/extprogs.cpp

changeset 183
f1b8cb53d2a2
child 184
fae3bc9ce319
equal deleted inserted replaced
182:9374fea8f77f 183:f1b8cb53d2a2
1 /*
2 * LDForge: LDraw parts authoring CAD
3 * Copyright (C) 2013 Santeri 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.h>
20 #include <qtemporaryfile.h>
21 #include <qeventloop.h>
22 #include <qdialog.h>
23 #include <qdialogbuttonbox.h>
24 #include <qspinbox.h>
25 #include <qcheckbox.h>
26 #include <qcombobox.h>
27 #include "common.h"
28 #include "config.h"
29 #include "misc.h"
30 #include "extprogs.h"
31 #include "gui.h"
32 #include "file.h"
33 #include "radiobox.h"
34 #include "history.h"
35
36 // =============================================================================
37 cfg (str, prog_isecalc, "");
38 cfg (str, prog_intersector, "");
39 cfg (str, prog_coverer, "");
40 cfg (str, prog_ytruder, "");
41 cfg (str, prog_datheader, "");
42 cfg (str, prog_rectifier, "");
43
44 const char* g_extProgNames[] = {
45 "Isecalc",
46 "Intersector",
47 "Coverer",
48 "Ytruder",
49 "Rectifier",
50 "DATHeader",
51 };
52
53 // =============================================================================
54 static bool checkProgPath (str path, const extprog prog) {
55 if (~path)
56 return true;
57
58 const char* name = g_extProgNames[prog];
59
60 critical (fmt ("Couldn't run %s as no path has "
61 "been defined for it. Use the configuration dialog's External Programs "
62 "tab to define a path for %s.", name, name));
63 return false;
64 }
65
66 // =============================================================================
67 static void processError (const extprog prog, QProcess& proc) {
68 const char* name = g_extProgNames[prog];
69 str errmsg;
70
71 switch (proc.error ()) {
72 case QProcess::FailedToStart:
73 errmsg = fmt ("Failed to launch %s. Check that you have set the proper path "
74 "to %s and that you have the proper permissions to launch it.", name, name);
75 break;
76
77 case QProcess::Crashed:
78 errmsg = fmt ("%s crashed.", name);
79 break;
80
81 case QProcess::WriteError:
82 case QProcess::ReadError:
83 errmsg = fmt ("I/O error while interacting with %s.", name);
84 break;
85
86 case QProcess::UnknownError:
87 errmsg = fmt ("Unknown error occurred while executing %s.", name);
88 break;
89
90 case QProcess::Timedout:
91 errmsg = fmt ("%s timed out.", name);
92 break;
93 }
94
95 critical (errmsg);
96 }
97
98 // =============================================================================
99 static bool mkTempFile (QTemporaryFile& tmp, str& fname) {
100 if (!tmp.open ())
101 return false;
102
103 fname = tmp.fileName ();
104 tmp.close ();
105 return true;
106 }
107
108 // =============================================================================
109 void writeObjects (std::vector<LDObject*>& objects, str fname) {
110 // Write the input file
111 FILE* fp = fopen (fname, "w");
112 if (!fp) {
113 critical (fmt ("Couldn't open temporary file %s for writing.\n", fname.chars ()));
114 return;
115 }
116
117 for (LDObject* obj : objects) {
118 str line = fmt ("%s\r\n", obj->getContents ().chars ());
119 fwrite (line.chars(), 1, ~line, fp);
120 }
121
122 #ifndef RELEASE
123 ushort idx = rand ();
124 printf ("%s -> debug_%u\n", fname.chars (), idx);
125 QFile::copy (fname.chars (), fmt ("debug_%u", idx));
126 #endif // RELEASE
127
128 fclose (fp);
129 }
130
131 // =============================================================================
132 void writeSelection (str fname) {
133 writeObjects (g_win->sel (), fname);
134 }
135
136 // =============================================================================
137 void writeColorGroup (const short colnum, str fname) {
138 std::vector<LDObject*> objects;
139 for (LDObject*& obj : g_curfile->m_objs) {
140 if (obj->isColored () == false || obj->dColor != colnum)
141 continue;
142
143 objects.push_back (obj);
144 }
145
146 writeObjects (objects, fname);
147 }
148
149 // =============================================================================
150 void runUtilityProcess (extprog prog, str path, QString argvstr) {
151 QTemporaryFile input, output;
152 str inputname, outputname;
153 QStringList argv = argvstr.split (" ", QString::SkipEmptyParts);
154
155 printf ("cmdline: %s %s\n", path.chars (), qchars (argvstr));
156
157 if (!mkTempFile (input, inputname) || !mkTempFile (output, outputname))
158 return;
159
160 QProcess proc;
161
162 // Init stdin
163 FILE* stdinfp = fopen (inputname, "w");
164
165 // Begin!
166 proc.setStandardInputFile (inputname);
167 proc.start (path, argv);
168
169 // Write an enter - one is expected
170 char enter[2] = "\n";
171 enter[1] = '\0';
172 fwrite (enter, 1, sizeof enter, stdinfp);
173 fflush (stdinfp);
174
175 // Wait while it runs
176 proc.waitForFinished ();
177
178 #ifndef RELASE
179 printf ("%s", qchars (QString (proc.readAllStandardOutput ())));
180 #endif // RELEASE
181
182 if (proc.exitStatus () == QProcess::CrashExit) {
183 processError (prog, proc);
184 return;
185 }
186 }
187
188 // ========================================================================================================================================
189 static void insertOutput (str fname, bool replace, vector<short> colorsToReplace) {
190 #ifndef RELEASE
191 QFile::copy (fname, "./debug_lastOutput");
192 #endif // RELEASE
193
194 // Read the output file
195 FILE* fp = fopen (fname, "r");
196 if (!fp) {
197 critical (fmt ("Couldn't open temporary file %s for reading.\n", fname.chars ()));
198 return;
199 }
200
201 ComboHistory* cmb = new ComboHistory ({});
202 std::vector<LDObject*> objs = loadFileContents (fp, null),
203 copies;
204 std::vector<ulong> indices;
205
206 // If we replace the objects, delete the selection now.
207 if (replace)
208 *cmb << g_win->deleteSelection ();
209
210 for (const short colnum : colorsToReplace)
211 *cmb << g_win->deleteByColor (colnum);
212
213 // Insert the new objects
214 g_win->sel ().clear ();
215 for (LDObject* obj : objs) {
216 if (!obj->isSchemantic ()) {
217 delete obj;
218 continue;
219 }
220
221 ulong idx = g_curfile->addObject (obj);
222 indices.push_back (idx);
223 copies.push_back (obj->clone ());
224 g_win->sel ().push_back (obj);
225 }
226
227 if (indices.size() > 0)
228 *cmb << new AddHistory ({indices, copies});
229
230 if (cmb->paEntries.size () > 0)
231 History::addEntry (cmb);
232 else
233 delete cmb;
234
235 fclose (fp);
236 g_win->refresh ();
237 }
238
239 QDialogButtonBox* makeButtonBox (QDialog& dlg) {
240 QDialogButtonBox* bbx_buttons = new QDialogButtonBox (QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
241 QWidget::connect (bbx_buttons, SIGNAL (accepted ()), &dlg, SLOT (accept ()));
242 QWidget::connect (bbx_buttons, SIGNAL (rejected ()), &dlg, SLOT (reject ()));
243 return bbx_buttons;
244 }
245
246 // =============================================================================
247 // Interface for Ytruder
248 MAKE_ACTION (ytruder, "Ytruder", "ytruder", "Extrude selected lines to a given plane", KEY (F4)) {
249 setlocale (LC_ALL, "C");
250
251 if (!checkProgPath (prog_ytruder, Ytruder))
252 return;
253
254 QDialog dlg;
255
256 RadioBox* rb_mode = new RadioBox ("Extrusion mode", {"Distance", "Symmetry", "Projection", "Radial"}, 0, Qt::Horizontal);
257 RadioBox* rb_axis = new RadioBox ("Axis", {"X", "Y", "Z"}, 0, Qt::Horizontal);
258 LabeledWidget<QDoubleSpinBox>* dsb_depth = new LabeledWidget<QDoubleSpinBox> ("Plane depth"),
259 *dsb_condAngle = new LabeledWidget<QDoubleSpinBox> ("Conditional line threshold");
260
261 rb_axis->setValue (Y);
262 dsb_depth->w ()->setMinimum (-10000.0);
263 dsb_depth->w ()->setMaximum (10000.0);
264 dsb_depth->w ()->setDecimals (3);
265 dsb_condAngle->w ()->setValue (30.0f);
266
267 QVBoxLayout* layout = new QVBoxLayout (&dlg);
268 layout->addWidget (rb_mode);
269 layout->addWidget (rb_axis);
270 layout->addWidget (dsb_depth);
271 layout->addWidget (dsb_condAngle);
272 layout->addWidget (makeButtonBox (dlg));
273
274 dlg.setWindowIcon (getIcon ("extrude"));
275
276 if (!dlg.exec ())
277 return;
278
279 // Read the user's choices
280 const enum modetype { Distance, Symmetry, Projection, Radial } mode = (modetype) rb_mode->value ();
281 const Axis axis = (Axis) rb_axis->value ();
282 const double depth = dsb_depth->w ()->value (),
283 condAngle = dsb_condAngle->w ()->value ();
284
285 QTemporaryFile indat, outdat;
286 str inDATName, outDATName;
287
288 // Make temp files for the input and output files
289 if (!mkTempFile (indat, inDATName) || !mkTempFile (outdat, outDATName))
290 return;
291
292 // Compose the command-line arguments
293 str argv = fmt ("%s %s %f -a %f %s %s",
294 (axis == X) ? "-x" : (axis == Y) ? "-y" : "-z",
295 (mode == Distance) ? "-d" : (mode == Symmetry) ? "-s" : (mode == Projection) ? "-p" : "-r",
296 depth, condAngle, inDATName.chars (), outDATName.chars ());
297
298 writeSelection (inDATName);
299 runUtilityProcess (Ytruder, prog_ytruder, argv);
300 insertOutput (outDATName, false, {});
301 }
302
303 // ========================================================================================================================================
304 // Rectifier interface
305 MAKE_ACTION (rectifier, "Rectifier", "rectifier", "Optimizes quads into rect primitives.", KEY (F8)) {
306 setlocale (LC_ALL, "C");
307
308 if (!checkProgPath (prog_rectifier, Rectifier))
309 return;
310
311 QDialog dlg;
312 QCheckBox* cb_condense = new QCheckBox ("Condense triangles to quads"),
313 *cb_subst = new QCheckBox ("Substitute rect primitives"),
314 *cb_condlineCheck = new QCheckBox ("Don't replace quads with adj. condlines"),
315 *cb_colorize = new QCheckBox ("Colorize resulting objects");
316 LabeledWidget<QDoubleSpinBox>* dsb_coplthres = new LabeledWidget<QDoubleSpinBox> ("Coplanarity threshold");
317
318 dsb_coplthres->w ()->setMinimum (0.0f);
319 dsb_coplthres->w ()->setMaximum (360.0f);
320 dsb_coplthres->w ()->setDecimals (3);
321 dsb_coplthres->w ()->setValue (0.95f);
322 cb_condense->setChecked (true);
323 cb_subst->setChecked (true);
324
325 QVBoxLayout* layout = new QVBoxLayout (&dlg);
326 layout->addWidget (cb_condense);
327 layout->addWidget (cb_subst);
328 layout->addWidget (cb_condlineCheck);
329 layout->addWidget (cb_colorize);
330 layout->addWidget (dsb_coplthres);
331 layout->addWidget (makeButtonBox (dlg));
332
333 if (!dlg.exec ())
334 return;
335
336 const bool condense = cb_condense->isChecked (),
337 subst = cb_subst->isChecked (),
338 condlineCheck = cb_condlineCheck->isChecked (),
339 colorize = cb_colorize->isChecked ();
340 const double coplthres = dsb_coplthres->w ()->value ();
341
342 QTemporaryFile indat, outdat;
343 str inDATName, outDATName;
344
345 // Make temp files for the input and output files
346 if (!mkTempFile (indat, inDATName) || !mkTempFile (outdat, outDATName))
347 return;
348
349 // Compose arguments
350 str argv = fmt ("%s %s %s %s -t %f %s %s",
351 (condense == false) ? "-q" : "",
352 (subst == false) ? "-r" : "",
353 (condlineCheck) ? "-a" : "",
354 (colorize) ? "-c" : "",
355 coplthres, inDATName.chars (), outDATName.chars ());
356
357 writeSelection (inDATName);
358 runUtilityProcess (Rectifier, prog_rectifier, argv);
359 insertOutput (outDATName, true, {});
360 }
361
362 // =======================================================================================================================================
363 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
364 // =======================================================================================================================================
365 // Intersector interface
366 MAKE_ACTION (intersector, "Intersector", "intersector", "Perform clipping between two input groups.", KEY (F5)) {
367 setlocale (LC_ALL, "C");
368
369 if (!checkProgPath (prog_intersector, Intersector))
370 return;
371
372 QDialog dlg;
373
374 LabeledWidget<QComboBox>* cmb_incol = new LabeledWidget<QComboBox> ("Input", new QComboBox),
375 *cmb_cutcol = new LabeledWidget<QComboBox> ("Cutter", new QComboBox);
376 QCheckBox* cb_colorize = new QCheckBox ("Colorize output"),
377 *cb_nocondense = new QCheckBox ("No condensing"),
378 *cb_repeatInverse = new QCheckBox ("Repeat inverse"),
379 *cb_edges = new QCheckBox ("Add edges");
380 LabeledWidget<QDoubleSpinBox>* dsb_prescale = new LabeledWidget<QDoubleSpinBox> ("Prescaling factor");
381
382 cb_repeatInverse->setWhatsThis ("If this is set, " APPNAME " runs Intersector a second time with inverse files to cut the "
383 " cutter group with the input group. Both groups are cut by the intersection.");
384 cb_edges->setWhatsThis ("Makes " APPNAME " try run Isecalc to create edgelines for the intersection.");
385
386 makeColorSelector (cmb_incol->w ());
387 makeColorSelector (cmb_cutcol->w ());
388 dsb_prescale->w ()->setMinimum (0.0f);
389 dsb_prescale->w ()->setMaximum (10000.0f);
390 dsb_prescale->w ()->setSingleStep (0.01f);
391 dsb_prescale->w ()->setValue (1.0f);
392
393 QVBoxLayout* layout = new QVBoxLayout (&dlg);
394 layout->addWidget (cmb_incol);
395 layout->addWidget (cmb_cutcol);
396
397 QHBoxLayout* cblayout = new QHBoxLayout;
398 cblayout->addWidget (cb_colorize);
399 cblayout->addWidget (cb_nocondense);
400
401 QHBoxLayout* cb2layout = new QHBoxLayout;
402 cb2layout->addWidget (cb_repeatInverse);
403 cb2layout->addWidget (cb_edges);
404
405 layout->addLayout (cblayout);
406 layout->addLayout (cb2layout);
407 layout->addWidget (dsb_prescale);
408 layout->addWidget (makeButtonBox (dlg));
409
410 exec:
411 if (!dlg.exec ())
412 return;
413
414 const short inCol = cmb_incol->w ()->itemData (cmb_incol->w ()->currentIndex ()).toInt (),
415 cutCol = cmb_cutcol->w ()->itemData (cmb_cutcol->w ()->currentIndex ()).toInt ();
416 const bool repeatInverse = cb_repeatInverse->isChecked ();
417
418 if (inCol == cutCol) {
419 critical ("Cannot use the same color group for both input and cutter!");
420 goto exec;
421 }
422
423 // Five temporary files!
424 // indat = input group file
425 // cutdat = cutter group file
426 // outdat = primary output
427 // outdat2 = inverse output
428 // edgesdat = edges output (isecalc)
429 QTemporaryFile indat, cutdat, outdat, outdat2, edgesdat;
430 str inDATName, cutDATName, outDATName, outDAT2Name, edgesDATName;
431
432 if (!mkTempFile (indat, inDATName) || !mkTempFile (cutdat, cutDATName) ||
433 !mkTempFile (outdat, outDATName) || !mkTempFile (outdat2, outDAT2Name) ||
434 !mkTempFile (edgesdat, edgesDATName))
435 {
436 return;
437 }
438
439 str parms = fmt ("%s %s -s %f",
440 (cb_colorize->isChecked ()) ? "-c" : "",
441 (cb_nocondense->isChecked ()) ? "-t" : "",
442 dsb_prescale->w ()->value ());
443
444 str argv_normal = fmt ("%s %s %s %s", parms.chars (), inDATName.chars (), cutDATName.chars (), outDATName.chars ());
445 str argv_inverse = fmt ("%s %s %s %s", parms.chars (), cutDATName.chars (), inDATName.chars (), outDAT2Name.chars ());
446
447 writeColorGroup (inCol, inDATName);
448 writeColorGroup (cutCol, cutDATName);
449 runUtilityProcess (Intersector, prog_intersector, argv_normal);
450 insertOutput (outDATName, false, {inCol});
451
452 if (repeatInverse) {
453 runUtilityProcess (Intersector, prog_intersector, argv_inverse);
454 insertOutput (outDAT2Name, false, {cutCol});
455 }
456
457 if (cb_edges->isChecked ()) {
458 runUtilityProcess (Isecalc, prog_isecalc, fmt ("%s %s %s", inDATName.chars (), cutDATName.chars (), edgesDATName.chars ()));
459 insertOutput (edgesDATName, false, {});
460 }
461 }

mercurial