diff -r 9374fea8f77f -r f1b8cb53d2a2 src/extprogs.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/extprogs.cpp Wed May 08 15:19:06 2013 +0300 @@ -0,0 +1,461 @@ +/* + * LDForge: LDraw parts authoring CAD + * Copyright (C) 2013 Santeri Piippo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "common.h" +#include "config.h" +#include "misc.h" +#include "extprogs.h" +#include "gui.h" +#include "file.h" +#include "radiobox.h" +#include "history.h" + +// ============================================================================= +cfg (str, prog_isecalc, ""); +cfg (str, prog_intersector, ""); +cfg (str, prog_coverer, ""); +cfg (str, prog_ytruder, ""); +cfg (str, prog_datheader, ""); +cfg (str, prog_rectifier, ""); + +const char* g_extProgNames[] = { + "Isecalc", + "Intersector", + "Coverer", + "Ytruder", + "Rectifier", + "DATHeader", +}; + +// ============================================================================= +static bool checkProgPath (str path, const extprog prog) { + if (~path) + return true; + + const char* name = g_extProgNames[prog]; + + critical (fmt ("Couldn't run %s as no path has " + "been defined for it. Use the configuration dialog's External Programs " + "tab to define a path for %s.", name, name)); + return false; +} + +// ============================================================================= +static void processError (const extprog prog, QProcess& proc) { + const char* name = g_extProgNames[prog]; + str errmsg; + + switch (proc.error ()) { + case QProcess::FailedToStart: + errmsg = fmt ("Failed to launch %s. Check that you have set the proper path " + "to %s and that you have the proper permissions to launch it.", name, name); + break; + + case QProcess::Crashed: + errmsg = fmt ("%s crashed.", name); + break; + + case QProcess::WriteError: + case QProcess::ReadError: + errmsg = fmt ("I/O error while interacting with %s.", name); + break; + + case QProcess::UnknownError: + errmsg = fmt ("Unknown error occurred while executing %s.", name); + break; + + case QProcess::Timedout: + errmsg = fmt ("%s timed out.", name); + break; + } + + critical (errmsg); +} + +// ============================================================================= +static bool mkTempFile (QTemporaryFile& tmp, str& fname) { + if (!tmp.open ()) + return false; + + fname = tmp.fileName (); + tmp.close (); + return true; +} + +// ============================================================================= +void writeObjects (std::vector& objects, str fname) { + // Write the input file + FILE* fp = fopen (fname, "w"); + if (!fp) { + critical (fmt ("Couldn't open temporary file %s for writing.\n", fname.chars ())); + return; + } + + for (LDObject* obj : objects) { + str line = fmt ("%s\r\n", obj->getContents ().chars ()); + fwrite (line.chars(), 1, ~line, fp); + } + +#ifndef RELEASE + ushort idx = rand (); + printf ("%s -> debug_%u\n", fname.chars (), idx); + QFile::copy (fname.chars (), fmt ("debug_%u", idx)); +#endif // RELEASE + + fclose (fp); +} + +// ============================================================================= +void writeSelection (str fname) { + writeObjects (g_win->sel (), fname); +} + +// ============================================================================= +void writeColorGroup (const short colnum, str fname) { + std::vector objects; + for (LDObject*& obj : g_curfile->m_objs) { + if (obj->isColored () == false || obj->dColor != colnum) + continue; + + objects.push_back (obj); + } + + writeObjects (objects, fname); +} + +// ============================================================================= +void runUtilityProcess (extprog prog, str path, QString argvstr) { + QTemporaryFile input, output; + str inputname, outputname; + QStringList argv = argvstr.split (" ", QString::SkipEmptyParts); + + printf ("cmdline: %s %s\n", path.chars (), qchars (argvstr)); + + if (!mkTempFile (input, inputname) || !mkTempFile (output, outputname)) + return; + + QProcess proc; + + // Init stdin + FILE* stdinfp = fopen (inputname, "w"); + + // Begin! + proc.setStandardInputFile (inputname); + proc.start (path, argv); + + // Write an enter - one is expected + char enter[2] = "\n"; + enter[1] = '\0'; + fwrite (enter, 1, sizeof enter, stdinfp); + fflush (stdinfp); + + // Wait while it runs + proc.waitForFinished (); + +#ifndef RELASE + printf ("%s", qchars (QString (proc.readAllStandardOutput ()))); +#endif // RELEASE + + if (proc.exitStatus () == QProcess::CrashExit) { + processError (prog, proc); + return; + } +} + +// ======================================================================================================================================== +static void insertOutput (str fname, bool replace, vector colorsToReplace) { +#ifndef RELEASE + QFile::copy (fname, "./debug_lastOutput"); +#endif // RELEASE + + // Read the output file + FILE* fp = fopen (fname, "r"); + if (!fp) { + critical (fmt ("Couldn't open temporary file %s for reading.\n", fname.chars ())); + return; + } + + ComboHistory* cmb = new ComboHistory ({}); + std::vector objs = loadFileContents (fp, null), + copies; + std::vector indices; + + // If we replace the objects, delete the selection now. + if (replace) + *cmb << g_win->deleteSelection (); + + for (const short colnum : colorsToReplace) + *cmb << g_win->deleteByColor (colnum); + + // Insert the new objects + g_win->sel ().clear (); + for (LDObject* obj : objs) { + if (!obj->isSchemantic ()) { + delete obj; + continue; + } + + ulong idx = g_curfile->addObject (obj); + indices.push_back (idx); + copies.push_back (obj->clone ()); + g_win->sel ().push_back (obj); + } + + if (indices.size() > 0) + *cmb << new AddHistory ({indices, copies}); + + if (cmb->paEntries.size () > 0) + History::addEntry (cmb); + else + delete cmb; + + fclose (fp); + g_win->refresh (); +} + +QDialogButtonBox* makeButtonBox (QDialog& dlg) { + QDialogButtonBox* bbx_buttons = new QDialogButtonBox (QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + QWidget::connect (bbx_buttons, SIGNAL (accepted ()), &dlg, SLOT (accept ())); + QWidget::connect (bbx_buttons, SIGNAL (rejected ()), &dlg, SLOT (reject ())); + return bbx_buttons; +} + +// ============================================================================= +// Interface for Ytruder +MAKE_ACTION (ytruder, "Ytruder", "ytruder", "Extrude selected lines to a given plane", KEY (F4)) { + setlocale (LC_ALL, "C"); + + if (!checkProgPath (prog_ytruder, Ytruder)) + return; + + QDialog dlg; + + RadioBox* rb_mode = new RadioBox ("Extrusion mode", {"Distance", "Symmetry", "Projection", "Radial"}, 0, Qt::Horizontal); + RadioBox* rb_axis = new RadioBox ("Axis", {"X", "Y", "Z"}, 0, Qt::Horizontal); + LabeledWidget* dsb_depth = new LabeledWidget ("Plane depth"), + *dsb_condAngle = new LabeledWidget ("Conditional line threshold"); + + rb_axis->setValue (Y); + dsb_depth->w ()->setMinimum (-10000.0); + dsb_depth->w ()->setMaximum (10000.0); + dsb_depth->w ()->setDecimals (3); + dsb_condAngle->w ()->setValue (30.0f); + + QVBoxLayout* layout = new QVBoxLayout (&dlg); + layout->addWidget (rb_mode); + layout->addWidget (rb_axis); + layout->addWidget (dsb_depth); + layout->addWidget (dsb_condAngle); + layout->addWidget (makeButtonBox (dlg)); + + dlg.setWindowIcon (getIcon ("extrude")); + + if (!dlg.exec ()) + return; + + // Read the user's choices + const enum modetype { Distance, Symmetry, Projection, Radial } mode = (modetype) rb_mode->value (); + const Axis axis = (Axis) rb_axis->value (); + const double depth = dsb_depth->w ()->value (), + condAngle = dsb_condAngle->w ()->value (); + + QTemporaryFile indat, outdat; + str inDATName, outDATName; + + // Make temp files for the input and output files + if (!mkTempFile (indat, inDATName) || !mkTempFile (outdat, outDATName)) + return; + + // Compose the command-line arguments + str argv = fmt ("%s %s %f -a %f %s %s", + (axis == X) ? "-x" : (axis == Y) ? "-y" : "-z", + (mode == Distance) ? "-d" : (mode == Symmetry) ? "-s" : (mode == Projection) ? "-p" : "-r", + depth, condAngle, inDATName.chars (), outDATName.chars ()); + + writeSelection (inDATName); + runUtilityProcess (Ytruder, prog_ytruder, argv); + insertOutput (outDATName, false, {}); +} + +// ======================================================================================================================================== +// Rectifier interface +MAKE_ACTION (rectifier, "Rectifier", "rectifier", "Optimizes quads into rect primitives.", KEY (F8)) { + setlocale (LC_ALL, "C"); + + if (!checkProgPath (prog_rectifier, Rectifier)) + return; + + QDialog dlg; + QCheckBox* cb_condense = new QCheckBox ("Condense triangles to quads"), + *cb_subst = new QCheckBox ("Substitute rect primitives"), + *cb_condlineCheck = new QCheckBox ("Don't replace quads with adj. condlines"), + *cb_colorize = new QCheckBox ("Colorize resulting objects"); + LabeledWidget* dsb_coplthres = new LabeledWidget ("Coplanarity threshold"); + + dsb_coplthres->w ()->setMinimum (0.0f); + dsb_coplthres->w ()->setMaximum (360.0f); + dsb_coplthres->w ()->setDecimals (3); + dsb_coplthres->w ()->setValue (0.95f); + cb_condense->setChecked (true); + cb_subst->setChecked (true); + + QVBoxLayout* layout = new QVBoxLayout (&dlg); + layout->addWidget (cb_condense); + layout->addWidget (cb_subst); + layout->addWidget (cb_condlineCheck); + layout->addWidget (cb_colorize); + layout->addWidget (dsb_coplthres); + layout->addWidget (makeButtonBox (dlg)); + + if (!dlg.exec ()) + return; + + const bool condense = cb_condense->isChecked (), + subst = cb_subst->isChecked (), + condlineCheck = cb_condlineCheck->isChecked (), + colorize = cb_colorize->isChecked (); + const double coplthres = dsb_coplthres->w ()->value (); + + QTemporaryFile indat, outdat; + str inDATName, outDATName; + + // Make temp files for the input and output files + if (!mkTempFile (indat, inDATName) || !mkTempFile (outdat, outDATName)) + return; + + // Compose arguments + str argv = fmt ("%s %s %s %s -t %f %s %s", + (condense == false) ? "-q" : "", + (subst == false) ? "-r" : "", + (condlineCheck) ? "-a" : "", + (colorize) ? "-c" : "", + coplthres, inDATName.chars (), outDATName.chars ()); + + writeSelection (inDATName); + runUtilityProcess (Rectifier, prog_rectifier, argv); + insertOutput (outDATName, true, {}); +} + +// ======================================================================================================================================= +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +// ======================================================================================================================================= +// Intersector interface +MAKE_ACTION (intersector, "Intersector", "intersector", "Perform clipping between two input groups.", KEY (F5)) { + setlocale (LC_ALL, "C"); + + if (!checkProgPath (prog_intersector, Intersector)) + return; + + QDialog dlg; + + LabeledWidget* cmb_incol = new LabeledWidget ("Input", new QComboBox), + *cmb_cutcol = new LabeledWidget ("Cutter", new QComboBox); + QCheckBox* cb_colorize = new QCheckBox ("Colorize output"), + *cb_nocondense = new QCheckBox ("No condensing"), + *cb_repeatInverse = new QCheckBox ("Repeat inverse"), + *cb_edges = new QCheckBox ("Add edges"); + LabeledWidget* dsb_prescale = new LabeledWidget ("Prescaling factor"); + + cb_repeatInverse->setWhatsThis ("If this is set, " APPNAME " runs Intersector a second time with inverse files to cut the " + " cutter group with the input group. Both groups are cut by the intersection."); + cb_edges->setWhatsThis ("Makes " APPNAME " try run Isecalc to create edgelines for the intersection."); + + makeColorSelector (cmb_incol->w ()); + makeColorSelector (cmb_cutcol->w ()); + dsb_prescale->w ()->setMinimum (0.0f); + dsb_prescale->w ()->setMaximum (10000.0f); + dsb_prescale->w ()->setSingleStep (0.01f); + dsb_prescale->w ()->setValue (1.0f); + + QVBoxLayout* layout = new QVBoxLayout (&dlg); + layout->addWidget (cmb_incol); + layout->addWidget (cmb_cutcol); + + QHBoxLayout* cblayout = new QHBoxLayout; + cblayout->addWidget (cb_colorize); + cblayout->addWidget (cb_nocondense); + + QHBoxLayout* cb2layout = new QHBoxLayout; + cb2layout->addWidget (cb_repeatInverse); + cb2layout->addWidget (cb_edges); + + layout->addLayout (cblayout); + layout->addLayout (cb2layout); + layout->addWidget (dsb_prescale); + layout->addWidget (makeButtonBox (dlg)); + +exec: + if (!dlg.exec ()) + return; + + const short inCol = cmb_incol->w ()->itemData (cmb_incol->w ()->currentIndex ()).toInt (), + cutCol = cmb_cutcol->w ()->itemData (cmb_cutcol->w ()->currentIndex ()).toInt (); + const bool repeatInverse = cb_repeatInverse->isChecked (); + + if (inCol == cutCol) { + critical ("Cannot use the same color group for both input and cutter!"); + goto exec; + } + + // Five temporary files! + // indat = input group file + // cutdat = cutter group file + // outdat = primary output + // outdat2 = inverse output + // edgesdat = edges output (isecalc) + QTemporaryFile indat, cutdat, outdat, outdat2, edgesdat; + str inDATName, cutDATName, outDATName, outDAT2Name, edgesDATName; + + if (!mkTempFile (indat, inDATName) || !mkTempFile (cutdat, cutDATName) || + !mkTempFile (outdat, outDATName) || !mkTempFile (outdat2, outDAT2Name) || + !mkTempFile (edgesdat, edgesDATName)) + { + return; + } + + str parms = fmt ("%s %s -s %f", + (cb_colorize->isChecked ()) ? "-c" : "", + (cb_nocondense->isChecked ()) ? "-t" : "", + dsb_prescale->w ()->value ()); + + str argv_normal = fmt ("%s %s %s %s", parms.chars (), inDATName.chars (), cutDATName.chars (), outDATName.chars ()); + str argv_inverse = fmt ("%s %s %s %s", parms.chars (), cutDATName.chars (), inDATName.chars (), outDAT2Name.chars ()); + + writeColorGroup (inCol, inDATName); + writeColorGroup (cutCol, cutDATName); + runUtilityProcess (Intersector, prog_intersector, argv_normal); + insertOutput (outDATName, false, {inCol}); + + if (repeatInverse) { + runUtilityProcess (Intersector, prog_intersector, argv_inverse); + insertOutput (outDAT2Name, false, {cutCol}); + } + + if (cb_edges->isChecked ()) { + runUtilityProcess (Isecalc, prog_isecalc, fmt ("%s %s %s", inDATName.chars (), cutDATName.chars (), edgesDATName.chars ())); + insertOutput (edgesDATName, false, {}); + } +} \ No newline at end of file