src/toolsets/extprogramtoolset.cpp

Thu, 11 Jan 2018 15:09:44 +0200

author
Santeri Piippo
date
Thu, 11 Jan 2018 15:09:44 +0200
changeset 1231
ce0c9f2e6b9c
parent 1222
34def2630300
permissions
-rw-r--r--

begin rendering rework

/*
 *  LDForge: LDraw parts authoring CAD
 *  Copyright (C) 2013 - 2018 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 <QSettings>
#include <QFileInfo>
#include "../guiutilities.h"
#include "../main.h"
#include "../miscallenous.h"
#include "../mainwindow.h"
#include "../ldDocument.h"
#include "../radioGroup.h"
#include "../editHistory.h"
#include "../dialogs.h"
#include "../documentmanager.h"
#include "extprogramtoolset.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"

// =============================================================================
//
ConfigOption(QString IsecalcPath)
ConfigOption(QString IntersectorPath)
ConfigOption(QString CovererPath)
ConfigOption(QString RectifierPath)
ConfigOption(QString YtruderPath)
ConfigOption(QString Edger2Path)
ConfigOption(bool IsecalcUsesWine = false)
ConfigOption(bool IntersectorUsesWine = false)
ConfigOption(bool CovererUsesWine = false)
ConfigOption(bool YtruderUsesWine = false)
ConfigOption(bool RectifierUsesWine = false)
ConfigOption(bool Edger2UsesWine = false)

ExtProgramToolset::ExtProgramToolset(MainWindow* parent) :
	Toolset(parent)
{
	extProgramInfo[Isecalc].name = "Isecalc";
	extProgramInfo[Intersector].name = "Intersector";
	extProgramInfo[Coverer].name = "Coverer";
	extProgramInfo[Ytruder].name = "Ytruder";
	extProgramInfo[Rectifier].name = "Rectifier";
	extProgramInfo[Edger2].name = "Edger2";
}

bool ExtProgramToolset::makeTempFile(QTemporaryFile& tmp, QString& fname)
{
	if (not tmp.open())
		return false;

	fname = tmp.fileName();
	tmp.close();
	return true;
}

bool ExtProgramToolset::programUsesWine(ExtProgramType program)
{
#ifndef Q_OS_WIN32
	return getWineSetting(program);
#else
	return false;
#endif
}

bool ExtProgramToolset::getWineSetting(ExtProgramType program)
{
	return m_window->getConfigValue(externalProgramName(program) + "UsesWine").toBool();
}

QString ExtProgramToolset::getPathSetting(ExtProgramType program)
{
	return m_window->getConfigValue(externalProgramName(program) + "Path").toString();
}

void ExtProgramToolset::setPathSetting(ExtProgramType program, QString value)
{
	m_window->getSettings()->setValue(externalProgramName(program) + "Path", QVariant::fromValue(value));
}

void ExtProgramToolset::setWineSetting(ExtProgramType program, bool value)
{
	m_window->getSettings()->setValue(externalProgramName(program) + "UsesWine", QVariant::fromValue(value));
}

QString ExtProgramToolset::externalProgramName(ExtProgramType program)
{
	return extProgramInfo[program].name;
}

bool ExtProgramToolset::checkExtProgramPath(ExtProgramType program)
{
	QString path = getPathSetting(program);

	if (not path.isEmpty())
		return true;

	ExtProgPathPrompt* dialog = new ExtProgPathPrompt(externalProgramName(program));

	if (dialog->exec() and not dialog->getPath().isEmpty())
	{
		setPathSetting(program, dialog->getPath());
		return true;
	}

	return false;
}

// =============================================================================
//
QString ExtProgramToolset::errorCodeString(ExtProgramType program, QProcess& process)
{
	switch(process.error())
	{
	case QProcess::FailedToStart:
		if (programUsesWine(program))
			return tr("Program failed to start, make sure that Wine is installed and check your permissions.");

		return tr("Program failed to start, %1check your permissions");

	case QProcess::Crashed:
		return tr("Crashed.");

	case QProcess::WriteError:
	case QProcess::ReadError:
		return tr("I/O error.");

	case QProcess::UnknownError:
		return tr("Unknown error");

	case QProcess::Timedout:
		return tr("Timed out(30 seconds)");
	}

	return "";
}

// =============================================================================
//
void ExtProgramToolset::writeObjects(const LDObjectList& objects, QFile& f)
{
	for (LDObject* obj : objects)
	{
		if (obj->type() == OBJ_SubfileReference)
		{
			LDSubfileReference* ref = static_cast<LDSubfileReference*>(obj);
			LDObjectList objs = ref->inlineContents(true, false);
			writeObjects(objs, f);

			for (LDObject* obj : objs)
				obj->destroy();
		}
		else if (obj->type() == OBJ_BezierCurve)
		{
			LDBezierCurve* curve = static_cast<LDBezierCurve*>(obj);
			LDObjectList objs = curve->rasterize();
			writeObjects(objs, f);

			for (LDObject* obj : objs)
				obj->destroy();
		}
		else
			f.write((obj->asText() + "\r\n").toUtf8());
	}
}

// =============================================================================
//
void ExtProgramToolset::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 ExtProgramToolset::writeSelection(QString fname)
{
	writeObjects(selectedObjects(), fname);
}

// =============================================================================
//
void ExtProgramToolset::writeColorGroup(LDColor color, QString fname)
{
	LDObjectList objects;

	for (LDObject* obj : currentDocument()->objects())
	{
		if (not obj->isColored() or obj->color() != color)
			continue;

		objects << obj;
	}

	writeObjects(objects, fname);
}

// =============================================================================
//
bool ExtProgramToolset::runExtProgram(ExtProgramType program, QStringList argv)
{
	QString path = getPathSetting(program);
	QTemporaryFile input;

#ifndef Q_OS_WIN32
	if (programUsesWine(program))
	{
		argv.insert(0, path);
		path = "wine";
	}
#endif // Q_OS_WIN32

	print("Running command: %1 %2\n", path, argv.join(" "));

	if (not input.open())
		return false;

	QProcess process;

	// Begin!
	process.setStandardInputFile(input.fileName());
	process.start(path, argv);

	if (not process.waitForStarted())
	{
		Critical(format("Couldn't start %1: %2\n", externalProgramName(program),
			errorCodeString(program, process)));
		return false;
	}

	// Write an enter, the utility tools all expect one
	input.write("\n");

	// Wait while it runs
	process.waitForFinished();

	QString err = "";

	if (process.exitStatus() != QProcess::NormalExit)
		err = errorCodeString(program, process);

	// Check the return code
	if (process.exitCode() != 0)
		err = format("Program exited abnormally(return code %1).",  process.exitCode());

	if (not err.isEmpty())
	{
		Critical(format("%1 failed: %2\n", externalProgramName(program), err));
		QString filename("externalProgramOutput.txt");
		QFile file(filename);

		if (file.open(QIODevice::WriteOnly | QIODevice::Text))
		{
			file.write(process.readAllStandardOutput());
			file.write(process.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;
}

// =============================================================================
//
void ExtProgramToolset::insertOutput(QString fname, bool replace, QList<LDColor> colorsToReplace)
{
#ifdef DEBUG
	QFile::copy(fname, "./debug_lastOutput");
#endif

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

	// TODO: I don't like how I need to go to the document manager to load objects from a file...
	// We're not loading this as a document so it shouldn't be necessary.
	LDObjectList objs = m_documents->loadFileContents(&f, nullptr, nullptr);

	// If we replace the objects, delete the selection now.
	if (replace)
		m_window->deleteSelection();

	for (LDColor color : colorsToReplace)
		m_window->deleteByColor(color);

	// Insert the new objects
	currentDocument()->clearSelection();

	for (LDObject* obj : objs)
	{
		if (not obj->isScemantic())
		{
			obj->destroy();
			continue;
		}

		currentDocument()->addObject(obj);
		obj->select();
	}

	m_window->doFullRefresh();
}

// =============================================================================
// Interface for Ytruder
// =============================================================================
void ExtProgramToolset::ytruder()
{
	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
	QStringList argv {
		(axis == X) ? "-x" :
			(axis == Y) ? "-y" :
			"-z",
		(mode == Distance) ? "-d" :
			(mode == Symmetry) ? "-s" :
			(mode == Projection) ? "-p" :
			"-r",
		toString(depth),
		"-a",
		toString(condAngle),
		inDATName,
		outDATName,
	};

	writeSelection(inDATName);

	if (not runExtProgram(Ytruder, argv))
		return;

	insertOutput(outDATName, false, {});
}

// =============================================================================
// Rectifier interface
// =============================================================================
void ExtProgramToolset::rectifier()
{
	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
	QStringList argv = {
		(not ui.cb_condense->isChecked()) ? "-q" : "",
		(not ui.cb_subst->isChecked()) ? "-r" : "",
		(ui.cb_condlineCheck->isChecked()) ? "-a" : "",
		(ui.cb_colorize->isChecked()) ? "-c" : "",
		"-t",
		toString(ui.dsb_coplthres->value()),
		inDATName,
		outDATName
	};

	writeSelection(inDATName);

	if (not runExtProgram(Rectifier, argv))
		return;

	insertOutput(outDATName, true, {});
}

// =============================================================================
// Intersector interface
// =============================================================================
void ExtProgramToolset::intersector()
{
	setlocale(LC_ALL, "C");

	if (not checkExtProgramPath(Intersector))
		return;

	QDialog* dlg = new QDialog;
	Ui::IntersectorUI ui;
	ui.setupUi(dlg);
	guiUtilities()->fillUsedColorsToComboBox(ui.cmb_incol);
	guiUtilities()->fillUsedColorsToComboBox(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 = ui.cmb_incol->itemData(ui.cmb_incol->currentIndex()).toInt();
		cutCol = 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;
	}

	QStringList parms {
		(ui.cb_colorize->isChecked()) ? "-c" : "",
		(ui.cb_nocondense->isChecked()) ? "-t" : "",
		"-s",
		toString(ui.dsb_prescale->value()),
	};

	QStringList argv_normal = parms + QStringList {
		inDATName,
		cutDATName,
		outDATName,
	};

	QStringList argv_inverse = parms + QStringList {
		cutDATName,
		inDATName,
		outDAT2Name,
	};

	writeColorGroup(inCol, inDATName);
	writeColorGroup(cutCol, cutDATName);

	if (not runExtProgram(Intersector, argv_normal))
		return;

	insertOutput(outDATName, false, {inCol});

	if (repeatInverse and runExtProgram(Intersector, argv_inverse))
		insertOutput(outDAT2Name, false, {cutCol});

	if (ui.cb_edges->isChecked()
		and checkExtProgramPath(Isecalc)
		and runExtProgram(Isecalc, {inDATName, cutDATName, edgesDATName}))
	{
		insertOutput(edgesDATName, false, {});
	}
}

// =============================================================================
//
void ExtProgramToolset::coverer()
{
	setlocale(LC_ALL, "C");

	if (not checkExtProgramPath(Coverer))
		return;

	QDialog* dlg = new QDialog;
	Ui::CovererUI ui;
	ui.setupUi(dlg);
	guiUtilities()->fillUsedColorsToComboBox(ui.cmb_col1);
	guiUtilities()->fillUsedColorsToComboBox(ui.cmb_col2);

	LDColor in1Col, in2Col;

	forever
	{
		if (not dlg->exec())
			return;

		in1Col = ui.cmb_col1->itemData(ui.cmb_col1->currentIndex()).toInt();
		in2Col = 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;
	}

	QStringList argv = {
		(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, argv))
		return;

	insertOutput(outDATName, false, {});
}

// =============================================================================
//
void ExtProgramToolset::isecalc()
{
	setlocale(LC_ALL, "C");

	if (not checkExtProgramPath(Isecalc))
		return;

	Ui::IsecalcUI ui;
	QDialog* dlg = new QDialog;
	ui.setupUi(dlg);

	guiUtilities()->fillUsedColorsToComboBox(ui.cmb_col1);
	guiUtilities()->fillUsedColorsToComboBox(ui.cmb_col2);

	LDColor in1Col, in2Col;

	// Run the dialog and validate input
	forever
	{
		if (not dlg->exec())
			return;

		in1Col = ui.cmb_col1->itemData(ui.cmb_col1->currentIndex()).toInt();
		in2Col = 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;
	}

	writeColorGroup(in1Col, in1DATName);
	writeColorGroup(in2Col, in2DATName);
	runExtProgram(Isecalc, {in1DATName, in2DATName, outDATName});
	insertOutput(outDATName, false, {});
}

// =============================================================================
//
void ExtProgramToolset::edger2()
{
	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();

	QStringList argv = {
		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, argv))
		return;

	insertOutput(outName, true, {});
}

mercurial