--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/extPrograms.cpp Tue Mar 03 22:29:27 2015 +0200 @@ -0,0 +1,721 @@ +/* + * LDForge: LDraw parts authoring CAD + * Copyright (C) 2013 - 2015 Teemu 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 <http://www.gnu.org/licenses/>. + */ + +#include <QProcess> +#include <QTemporaryFile> +#include <QDialog> +#include <QDialogButtonBox> +#include <QSpinBox> +#include <QCheckBox> +#include <QComboBox> +#include <QGridLayout> +#include <QFileInfo> +#include "main.h" +#include "configuration.h" +#include "miscallenous.h" +#include "mainWindow.h" +#include "ldDocument.h" +#include "radioGroup.h" +#include "editHistory.h" +#include "ui_ytruder.h" +#include "ui_intersector.h" +#include "ui_rectifier.h" +#include "ui_coverer.h" +#include "ui_isecalc.h" +#include "ui_edger2.h" +#include "dialogs.h" + +enum extprog +{ + Isecalc, + Intersector, + Coverer, + Ytruder, + Rectifier, + Edger2, +}; + +// ============================================================================= +// +CFGENTRY (String, IsecalcPath, "") +CFGENTRY (String, IntersectorPath, "") +CFGENTRY (String, CovererPath, "") +CFGENTRY (String, YtruderPath, "") +CFGENTRY (String, RectifierPath, "") +CFGENTRY (String, Edger2Path, "") + +QString* const g_extProgPaths[] = +{ + &cfg::IsecalcPath, + &cfg::IntersectorPath, + &cfg::CovererPath, + &cfg::YtruderPath, + &cfg::RectifierPath, + &cfg::Edger2Path, +}; + +CFGENTRY (Bool, IsecalcUsesWine, false) +CFGENTRY (Bool, IntersectorUsesWine, false) +CFGENTRY (Bool, CovererUsesWine, false) +CFGENTRY (Bool, YtruderUsesWine, false) +CFGENTRY (Bool, RectifierUsesWine, false) +CFGENTRY (Bool, Edger2UsesWine, false) + +bool* const g_extProgWine[] = +{ + &cfg::IsecalcUsesWine, + &cfg::IntersectorUsesWine, + &cfg::CovererUsesWine, + &cfg::YtruderUsesWine, + &cfg::RectifierUsesWine, + &cfg::Edger2UsesWine, +}; + +const char* g_extProgNames[] = +{ + "Isecalc", + "Intersector", + "Coverer", + "Ytruder", + "Rectifier", + "Edger2" +}; + +// ============================================================================= +// +static bool MakeTempFile (QTemporaryFile& tmp, QString& fname) +{ + if (not tmp.open()) + return false; + + fname = tmp.fileName(); + tmp.close(); + return true; +} + +// ============================================================================= +// +static bool CheckExtProgramPath (const extprog prog) +{ + QString& path = *g_extProgPaths[prog]; + + if (not path.isEmpty()) + return true; + + ExtProgPathPrompt* dlg = new ExtProgPathPrompt (g_extProgNames[prog]); + + if (dlg->exec() and not dlg->getPath().isEmpty()) + { + path = dlg->getPath(); + return true; + } + + return false; +} + +// ============================================================================= +// +static QString ProcessExtProgError (extprog prog, QProcess& proc) +{ + switch (proc.error()) + { + case QProcess::FailedToStart: + { + QString wineblurb; + +#ifndef _WIN32 + if (*g_extProgWine[prog]) + wineblurb = "make sure Wine is installed and "; +#else + (void) prog; +#endif + + return format ("Program failed to start, %1check your permissions", wineblurb); + } break; + + case QProcess::Crashed: + return "Crashed."; + + case QProcess::WriteError: + case QProcess::ReadError: + return "I/O error."; + + case QProcess::UnknownError: + return "Unknown error"; + + case QProcess::Timedout: + return format ("Timed out (30 seconds)"); + } + + return ""; +} + +// ============================================================================= +// +static void WriteObjects (const LDObjectList& objects, QFile& f) +{ + for (LDObjectPtr obj : objects) + { + if (obj->type() == OBJ_Subfile) + { + LDSubfilePtr ref = obj.staticCast<LDSubfile>(); + LDObjectList objs = ref->inlineContents (true, false); + + WriteObjects (objs, f); + + for (LDObjectPtr obj : objs) + obj->destroy(); + } + else + f.write ((obj->asText() + "\r\n").toUtf8()); + } +} + +// ============================================================================= +// +static void WriteObjects (const LDObjectList& objects, QString fname) +{ + // Write the input file + QFile f (fname); + + if (not f.open (QIODevice::WriteOnly | QIODevice::Text)) + { + Critical (format ("Couldn't open temporary file %1 for writing: %2\n", fname, f.errorString())); + return; + } + + WriteObjects (objects, f); + f.close(); + +#ifdef DEBUG + QFile::copy (fname, "debug_lastInput"); +#endif +} + +// ============================================================================= +// +void WriteSelection (QString fname) +{ + WriteObjects (Selection(), fname); +} + +// ============================================================================= +// +void WriteColorGroup (LDColor color, QString fname) +{ + LDObjectList objects; + + for (LDObjectPtr obj : CurrentDocument()->objects()) + { + if (not obj->isColored() or obj->color() != color) + continue; + + objects << obj; + } + + WriteObjects (objects, fname); +} + +// ============================================================================= +// +bool RunExtProgram (extprog prog, QString path, QString argvstr) +{ + QTemporaryFile input; + QStringList argv = argvstr.split (" ", QString::SkipEmptyParts); + +#ifndef _WIN32 + if (*g_extProgWine[prog]) + { + argv.insert (0, path); + path = "wine"; + } +#endif // _WIN32 + + print ("Running command: %1 %2\n", path, argv.join (" ")); + + if (not input.open()) + return false; + + QProcess proc; + + // Begin! + proc.setStandardInputFile (input.fileName()); + proc.start (path, argv); + + if (not proc.waitForStarted()) + { + Critical (format ("Couldn't start %1: %2\n", g_extProgNames[prog], ProcessExtProgError (prog, proc))); + return false; + } + + // Write an enter, the utility tools all expect one + input.write ("\n"); + + // Wait while it runs + proc.waitForFinished(); + + QString err = ""; + + if (proc.exitStatus() != QProcess::NormalExit) + err = ProcessExtProgError (prog, proc); + + // Check the return code + if (proc.exitCode() != 0) + err = format ("Program exited abnormally (return code %1).", proc.exitCode()); + + if (not err.isEmpty()) + { + Critical (format ("%1 failed: %2\n", g_extProgNames[prog], err)); + QString filename ("externalProgramOutput.txt"); + QFile file (filename); + + if (file.open (QIODevice::WriteOnly | QIODevice::Text)) + { + file.write (proc.readAllStandardOutput()); + file.write (proc.readAllStandardError()); + print ("Wrote output and error logs to %1", QFileInfo (file).absoluteFilePath()); + } + else + { + print ("Couldn't open %1 for writing: %2", + QFileInfo (filename).absoluteFilePath(), file.errorString()); + } + + return false; + } + + return true; +} + +// ============================================================================= +// +static void InsertOutput (QString fname, bool replace, QList<LDColor> colorsToReplace) +{ +#ifdef DEBUG + QFile::copy (fname, "./debug_lastOutput"); +#endif // RELEASE + + // Read the output file + QFile f (fname); + + if (not f.open (QIODevice::ReadOnly)) + { + Critical (format ("Couldn't open temporary file %1 for reading.\n", fname)); + return; + } + + LDObjectList objs = LoadFileContents (&f, null); + + // If we replace the objects, delete the selection now. + if (replace) + g_win->deleteSelection(); + + for (LDColor color : colorsToReplace) + g_win->deleteByColor (color); + + // Insert the new objects + CurrentDocument()->clearSelection(); + + for (LDObjectPtr obj : objs) + { + if (not obj->isScemantic()) + { + obj->destroy(); + continue; + } + + CurrentDocument()->addObject (obj); + obj->select(); + } + + g_win->doFullRefresh(); +} + +// ============================================================================= +// Interface for Ytruder +// ============================================================================= +void MainWindow::actionYtruder() +{ + setlocale (LC_ALL, "C"); + + if (not CheckExtProgramPath (Ytruder)) + return; + + QDialog* dlg = new QDialog; + Ui::YtruderUI ui; + ui.setupUi (dlg); + + if (not dlg->exec()) + return; + + // Read the user's choices + const enum { Distance, Symmetry, Projection, Radial } mode = + ui.mode_distance->isChecked() ? Distance : + ui.mode_symmetry->isChecked() ? Symmetry : + ui.mode_projection->isChecked() ? Projection : Radial; + + const Axis axis = + ui.axis_x->isChecked() ? X : + ui.axis_y->isChecked() ? Y : Z; + + const double depth = ui.planeDepth->value(), + condAngle = ui.condAngle->value(); + + QTemporaryFile indat, outdat; + QString inDATName, outDATName; + + // Make temp files for the input and output files + if (not MakeTempFile (indat, inDATName) or not MakeTempFile (outdat, outDATName)) + return; + + // Compose the command-line arguments + QString argv = Join ( + { + (axis == X) ? "-x" : (axis == Y) ? "-y" : "-z", + (mode == Distance) ? "-d" : (mode == Symmetry) ? "-s" : (mode == Projection) ? "-p" : "-r", + depth, + "-a", + condAngle, + inDATName, + outDATName + }); + + WriteSelection (inDATName); + + if (not RunExtProgram (Ytruder, cfg::YtruderPath, argv)) + return; + + InsertOutput (outDATName, false, {}); +} + +// ============================================================================= +// Rectifier interface +// ============================================================================= +void MainWindow::actionRectifier() +{ + setlocale (LC_ALL, "C"); + + if (not CheckExtProgramPath (Rectifier)) + return; + + QDialog* dlg = new QDialog; + Ui::RectifierUI ui; + ui.setupUi (dlg); + + if (not dlg->exec()) + return; + + QTemporaryFile indat, outdat; + QString inDATName, outDATName; + + // Make temp files for the input and output files + if (not MakeTempFile (indat, inDATName) or not MakeTempFile (outdat, outDATName)) + return; + + // Compose arguments + QString argv = Join ( + { + (not ui.cb_condense->isChecked()) ? "-q" : "", + (not ui.cb_subst->isChecked()) ? "-r" : "", + (ui.cb_condlineCheck->isChecked()) ? "-a" : "", + (ui.cb_colorize->isChecked()) ? "-c" : "", + "-t", + ui.dsb_coplthres->value(), + inDATName, + outDATName + }); + + WriteSelection (inDATName); + + if (not RunExtProgram (Rectifier, cfg::RectifierPath, argv)) + return; + + InsertOutput (outDATName, true, {}); +} + +// ============================================================================= +// Intersector interface +// ============================================================================= +void MainWindow::actionIntersector() +{ + setlocale (LC_ALL, "C"); + + if (not CheckExtProgramPath (Intersector)) + return; + + QDialog* dlg = new QDialog; + Ui::IntersectorUI ui; + ui.setupUi (dlg); + + MakeColorComboBox (ui.cmb_incol); + MakeColorComboBox (ui.cmb_cutcol); + ui.cb_repeat->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."); + ui.cb_edges->setWhatsThis ("Makes " APPNAME " try run Isecalc to create edgelines for the intersection."); + + LDColor inCol, cutCol; + const bool repeatInverse = ui.cb_repeat->isChecked(); + + forever + { + if (not dlg->exec()) + return; + + inCol = LDColor::fromIndex (ui.cmb_incol->itemData (ui.cmb_incol->currentIndex()).toInt()); + cutCol = LDColor::fromIndex (ui.cmb_cutcol->itemData (ui.cmb_cutcol->currentIndex()).toInt()); + + if (inCol == cutCol) + { + Critical ("Cannot use the same color group for both input and cutter!"); + continue; + } + + break; + } + + // 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; + QString inDATName, cutDATName, outDATName, outDAT2Name, edgesDATName; + + if (not MakeTempFile (indat, inDATName) or + not MakeTempFile (cutdat, cutDATName) or + not MakeTempFile (outdat, outDATName) or + not MakeTempFile (outdat2, outDAT2Name) or + not MakeTempFile (edgesdat, edgesDATName)) + { + return; + } + + QString parms = Join ( + { + (ui.cb_colorize->isChecked()) ? "-c" : "", + (ui.cb_nocondense->isChecked()) ? "-t" : "", + "-s", + ui.dsb_prescale->value() + }); + + QString argv_normal = Join ( + { + parms, + inDATName, + cutDATName, + outDATName + }); + + QString argv_inverse = Join ( + { + parms, + cutDATName, + inDATName, + outDAT2Name + }); + + WriteColorGroup (inCol, inDATName); + WriteColorGroup (cutCol, cutDATName); + + if (not RunExtProgram (Intersector, cfg::IntersectorPath, argv_normal)) + return; + + InsertOutput (outDATName, false, {inCol}); + + if (repeatInverse and RunExtProgram (Intersector, cfg::IntersectorPath, argv_inverse)) + InsertOutput (outDAT2Name, false, {cutCol}); + + if (ui.cb_edges->isChecked() and CheckExtProgramPath (Isecalc) and + RunExtProgram (Isecalc, cfg::IsecalcPath, Join ({inDATName, cutDATName, edgesDATName}))) + { + InsertOutput (edgesDATName, false, {}); + } +} + +// ============================================================================= +// +void MainWindow::actionCoverer() +{ + setlocale (LC_ALL, "C"); + + if (not CheckExtProgramPath (Coverer)) + return; + + QDialog* dlg = new QDialog; + Ui::CovererUI ui; + ui.setupUi (dlg); + MakeColorComboBox (ui.cmb_col1); + MakeColorComboBox (ui.cmb_col2); + + LDColor in1Col, in2Col; + + forever + { + if (not dlg->exec()) + return; + + in1Col = LDColor::fromIndex (ui.cmb_col1->itemData (ui.cmb_col1->currentIndex()).toInt()); + in2Col = LDColor::fromIndex (ui.cmb_col2->itemData (ui.cmb_col2->currentIndex()).toInt()); + + if (in1Col == in2Col) + { + Critical ("Cannot use the same color group for both inputs!"); + continue; + } + + break; + } + + QTemporaryFile in1dat, in2dat, outdat; + QString in1DATName, in2DATName, outDATName; + + if (not MakeTempFile (in1dat, in1DATName) or + not MakeTempFile (in2dat, in2DATName) or + not MakeTempFile (outdat, outDATName)) + { + return; + } + + QString argv = Join ( + { + (ui.cb_oldsweep->isChecked() ? "-s" : ""), + (ui.cb_reverse->isChecked() ? "-r" : ""), + (ui.dsb_segsplit->value() != 0 ? format ("-l %1", ui.dsb_segsplit->value()) : ""), + (ui.sb_bias->value() != 0 ? format ("-s %1", ui.sb_bias->value()) : ""), + in1DATName, + in2DATName, + outDATName + }); + + WriteColorGroup (in1Col, in1DATName); + WriteColorGroup (in2Col, in2DATName); + + if (not RunExtProgram (Coverer, cfg::CovererPath, argv)) + return; + + InsertOutput (outDATName, false, {}); +} + +// ============================================================================= +// +void MainWindow::actionIsecalc() +{ + setlocale (LC_ALL, "C"); + + if (not CheckExtProgramPath (Isecalc)) + return; + + Ui::IsecalcUI ui; + QDialog* dlg = new QDialog; + ui.setupUi (dlg); + + MakeColorComboBox (ui.cmb_col1); + MakeColorComboBox (ui.cmb_col2); + + LDColor in1Col, in2Col; + + // Run the dialog and validate input + forever + { + if (not dlg->exec()) + return; + + in1Col = LDColor::fromIndex (ui.cmb_col1->itemData (ui.cmb_col1->currentIndex()).toInt()); + in2Col = LDColor::fromIndex (ui.cmb_col2->itemData (ui.cmb_col2->currentIndex()).toInt()); + + if (in1Col == in2Col) + { + Critical ("Cannot use the same color group for both input and cutter!"); + continue; + } + + break; + } + + QTemporaryFile in1dat, in2dat, outdat; + QString in1DATName, in2DATName, outDATName; + + if (not MakeTempFile (in1dat, in1DATName) or + not MakeTempFile (in2dat, in2DATName) or + not MakeTempFile (outdat, outDATName)) + { + return; + } + + QString argv = Join ( + { + in1DATName, + in2DATName, + outDATName + }); + + WriteColorGroup (in1Col, in1DATName); + WriteColorGroup (in2Col, in2DATName); + RunExtProgram (Isecalc, cfg::IsecalcPath, argv); + InsertOutput (outDATName, false, {}); +} + +// ============================================================================= +// +void MainWindow::actionEdger2() +{ + setlocale (LC_ALL, "C"); + + if (not CheckExtProgramPath (Edger2)) + return; + + QDialog* dlg = new QDialog; + Ui::Edger2Dialog ui; + ui.setupUi (dlg); + + if (not dlg->exec()) + return; + + QTemporaryFile in, out; + QString inName, outName; + + if (not MakeTempFile (in, inName) or not MakeTempFile (out, outName)) + return; + + int unmatched = ui.unmatched->currentIndex(); + + QString argv = Join ( + { + format ("-p %1", ui.precision->value()), + format ("-af %1", ui.flatAngle->value()), + format ("-ac %1", ui.condAngle->value()), + format ("-ae %1", ui.edgeAngle->value()), + ui.delLines->isChecked() ? "-de" : "", + ui.delCondLines->isChecked() ? "-dc" : "", + ui.colored->isChecked() ? "-c" : "", + ui.bfc->isChecked() ? "-b" : "", + ui.convex->isChecked() ? "-cx" : "", + ui.concave->isChecked() ? "-cv" : "", + unmatched == 0 ? "-u+" : (unmatched == 2 ? "-u-" : ""), + inName, + outName, + }); + + WriteSelection (inName); + + if (not RunExtProgram (Edger2, cfg::Edger2Path, argv)) + return; + + InsertOutput (outName, true, {}); +}