|
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 } |