src/primitives.cc

Wed, 08 Jan 2014 13:57:10 +0200

author
Santeri Piippo <crimsondusk64@gmail.com>
date
Wed, 08 Jan 2014 13:57:10 +0200
changeset 608
487db37f0bb3
parent 606
3dd6f343ec06
child 609
a8dc74a809c6
permissions
-rw-r--r--

- if loading another file to replace an explicitly loaded file, this file won't get closed automatically and thus needs to be manually closed. We also need to check that it's safe to close before doing this. Also fixed a rather argh problem with ::save not using the proper path...

/*
 *  LDForge: LDraw parts authoring CAD
 *  Copyright (C) 2013, 2014 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 <http://www.gnu.org/licenses/>.
 */

#include <QDir>
#include <QRegExp>
#include <QFileDialog>
#include "document.h"
#include "gui.h"
#include "primitives.h"
#include "ui_makeprim.h"
#include "misc.h"
#include "colors.h"
#include "moc_primitives.cpp"

QList<PrimitiveCategory*> g_PrimitiveCategories;
QList<Primitive> g_primitives;
static PrimitiveLister* g_activePrimLister = null;
PrimitiveCategory* g_unmatched = null;

extern_cfg (String, ld_defaultname);
extern_cfg (String, ld_defaultuser);
extern_cfg (Int, ld_defaultlicense);

static const QStringList g_radialNameRoots =
{
	"edge",
	"cyli",
	"disc",
	"ndis",
	"ring",
	"con"
};

PrimitiveLister* getPrimitiveLister()
{
	return g_activePrimLister;
}

// =============================================================================
// -----------------------------------------------------------------------------
void loadPrimitives()
{
	log ("Loading primitives...\n");
	PrimitiveCategory::loadCategories();

	// Try to load prims.cfg
	File conf (Config::filepath ("prims.cfg"), File::Read);

	if (!conf)
	{
		// No prims.cfg, build it
		PrimitiveLister::start();
	}
	else
	{
		// Read primitives from prims.cfg
		for (QString line : conf)
		{
			int space = line.indexOf (" ");

			if (space == -1)
				continue;

			Primitive info;
			info.name = line.left (space);
			info.title = line.mid (space + 1);
			g_primitives << info;
		}

		PrimitiveCategory::populateCategories();
	}
}

// =============================================================================
// -----------------------------------------------------------------------------
static void recursiveGetFilenames (QDir dir, QList<QString>& fnames)
{
	QFileInfoList flist = dir.entryInfoList();

	for (const QFileInfo & info : flist)
	{
		if (info.fileName() == "." || info.fileName() == "..")
			continue; // skip . and ..

		if (info.isDir())
			recursiveGetFilenames (QDir (info.absoluteFilePath()), fnames);
		else
			fnames << info.absoluteFilePath();
	}
}

// =============================================================================
// -----------------------------------------------------------------------------
PrimitiveLister::PrimitiveLister (QObject* parent) :
	QObject (parent),
	m_i (0)
{
	g_activePrimLister = this;
	QDir dir (LDPaths::prims());
	assert (dir.exists());
	m_baselen = dir.absolutePath().length();
	recursiveGetFilenames (dir, m_files);
	emit starting (m_files.size());
}

// =============================================================================
// -----------------------------------------------------------------------------
PrimitiveLister::~PrimitiveLister()
{
	g_activePrimLister = null;
}

// =============================================================================
// -----------------------------------------------------------------------------
void PrimitiveLister::work()
{
	int j = min (m_i + 300, m_files.size());
	log ("PrimitiveLister::work: %1 -> %2\n", m_i, j);

	for (; m_i < j; ++m_i)
	{
		QString fname = m_files[m_i];
		File f (fname, File::Read);
		Primitive info;
		info.name = fname.mid (m_baselen + 1);  // make full path relative
		info.name.replace ('/', '\\');  // use DOS backslashes, they're expected
		info.cat = null;

		if (!f.readLine (info.title))
			info.title = "";

		info.title = info.title.simplified();

		if (info.title[0] == '0')
		{
			info.title.remove (0, 1);  // remove 0
			info.title = info.title.simplified();
		}

		m_prims << info;
	}

	if (m_i == m_files.size())
	{
		// Done with primitives, now save to a config file
		File conf (Config::filepath ("prims.cfg"), File::Write);

		for (Primitive& info : m_prims)
			fprint (conf, "%1 %2\n", info.name, info.title);

		conf.close();

		g_primitives = m_prims;
		PrimitiveCategory::populateCategories();
		log ("%1 primitives listed", g_primitives.size());
		g_activePrimLister = null;
		emit workDone();
		deleteLater();
	}
	else
	{
		// Defer to event loop, pick up the work later
		emit update (m_i);
		QMetaObject::invokeMethod (this, "work", Qt::QueuedConnection);
	}
}

// =============================================================================
// -----------------------------------------------------------------------------
void PrimitiveLister::start()
{
	if (g_activePrimLister)
		return;

	PrimitiveLister* lister = new PrimitiveLister;
	/*
	connect (lister, SIGNAL (starting (int)), g_win, SLOT (primitiveLoaderStart (int)));
	connect (lister, SIGNAL (update (int)), g_win, SLOT (primitiveLoaderUpdate (int)));
	connect (lister, SIGNAL (workDone()), g_win, SLOT (primitiveLoaderEnd()));
	*/
	lister->work();
}

// =============================================================================
// -----------------------------------------------------------------------------
PrimitiveCategory::PrimitiveCategory (QString name, QObject* parent) :
	QObject (parent),
	m_Name (name) {}

// =============================================================================
// -----------------------------------------------------------------------------
void PrimitiveCategory::populateCategories()
{
	for (PrimitiveCategory* cat : g_PrimitiveCategories)
		cat->prims.clear();


	for (Primitive& prim : g_primitives)
	{
		bool matched = false;
		prim.cat = null;

		// Go over the categories and their regexes, if and when there's a match,
		// the primitive's category is set to the category the regex beloings to.
		for (PrimitiveCategory* cat : g_PrimitiveCategories)
		{
			for (RegexEntry& entry : cat->regexes)
			{
				switch (entry.type)
				{
					case EFilenameRegex:
					{
						// f-regex, check against filename
						matched = entry.regex.exactMatch (prim.name);
					} break;

					case ETitleRegex:
					{
						// t-regex, check against title
						matched = entry.regex.exactMatch (prim.title);
					} break;
				}

				if (matched)
				{
					prim.cat = cat;
					break;
				}
			}

			// Drop out if a category was decided on.
			if (prim.cat != null)
				break;
		}

		// If there was a match, add the primitive to the category.
		// Otherwise, add it to the list of unmatched primitives.
		if (prim.cat != null)
			prim.cat->prims << prim;
		else
			g_unmatched->prims << prim;
	}
}

// =============================================================================
// -----------------------------------------------------------------------------
void PrimitiveCategory::loadCategories()
{
	for (PrimitiveCategory* cat : g_PrimitiveCategories)
		delete cat;

	g_PrimitiveCategories.clear();
	File f (Config::dirpath() + "primregexps.cfg", File::Read);

	if (!f)
		f.open (":/data/primitive-categories.cfg", File::Read);

	if (!f)
	{
		critical (QObject::tr ("Failed to open primitive categories!"));
		return;
	}

	if (f)
	{
		PrimitiveCategory* cat = null;

		for (QString line : f)
		{
			int colon;

			if (line.length() == 0 || line[0] == '#')
				continue;

			if ((colon = line.indexOf (":")) == -1)
			{
				if (cat && cat->isValidToInclude())
					g_PrimitiveCategories << cat;

				cat = new PrimitiveCategory (line);
			}
			elif (cat != null)
			{
				QString cmd = line.left (colon);
				ERegexType type = EFilenameRegex;

				if (cmd == "f")
					type = EFilenameRegex;
				elif (cmd == "t")
					type = ETitleRegex;
				else
				{
					log (tr ("Warning: unknown command \"%1\" on line \"%2\""), cmd, line);
					continue;
				}

				QRegExp regex (line.mid (colon + 1));
				RegexEntry entry = { regex, type };
				cat->regexes << entry;
			}
			else
				log ("Warning: Rules given before the first category name");
		}

		if (cat->isValidToInclude())
			g_PrimitiveCategories << cat;
	}

	// Add a category for unmatched primitives.
	// Note: if this function is called the second time, g_unmatched has been
	// deleted at the beginning of the function and is dangling at this point.
	g_unmatched = new PrimitiveCategory (tr ("Other"));
	g_PrimitiveCategories << g_unmatched;
}

// =============================================================================
// -----------------------------------------------------------------------------
bool PrimitiveCategory::isValidToInclude()
{
	if (regexes.size() == 0)
	{
		log (tr ("Warning: category \"%1\" left without patterns"), getName());
		deleteLater();
		return false;
	}

	return true;
}

// =============================================================================
// -----------------------------------------------------------------------------
bool isPrimitiveLoaderBusy()
{
	return g_activePrimLister != null;
}

// =============================================================================
// -----------------------------------------------------------------------------
static double radialPoint (int i, int divs, double (*func) (double))
{
	return (*func) ((i * 2 * pi) / divs);
}

// =============================================================================
// -----------------------------------------------------------------------------
void makeCircle (int segs, int divs, double radius, QList<QLineF>& lines)
{
	for (int i = 0; i < segs; ++i)
	{
		double x0 = radius * radialPoint (i, divs, cos),
			x1 = radius * radialPoint (i + 1, divs, cos),
			z0 = radius * radialPoint (i, divs, sin),
			z1 = radius * radialPoint (i + 1, divs, sin);

		lines << QLineF (QPointF (x0, z0), QPointF (x1, z1));
	}
}

// =============================================================================
// -----------------------------------------------------------------------------
QList<LDObject*> makePrimitive (PrimitiveType type, int segs, int divs, int num)
{
	QList<LDObject*> objs;
	QList<int> condLineSegs;
	QList<QLineF> circle;

	makeCircle (segs, divs, 1, circle);

	for (int i = 0; i < segs; ++i)
	{
		double x0 = circle[i].x1(),
				   x1 = circle[i].x2(),
				   z0 = circle[i].y1(),
				   z1 = circle[i].y2();

		switch (type)
		{
			case Circle:
			{
				Vertex v0 (x0, 0.0f, z0),
				  v1 (x1, 0.0f, z1);

				LDLine* line = new LDLine;
				line->setVertex (0, v0);
				line->setVertex (1, v1);
				line->setColor (edgecolor);
				objs << line;
			} break;

			case Cylinder:
			case Ring:
			case Cone:
			{
				double x2, x3, z2, z3;
				double y0, y1, y2, y3;

				if (type == Cylinder)
				{
					x2 = x1;
					x3 = x0;
					z2 = z1;
					z3 = z0;

					y0 = y1 = 0.0f;
					y2 = y3 = 1.0f;
				}
				else
				{
					x2 = x1 * (num + 1);
					x3 = x0 * (num + 1);
					z2 = z1 * (num + 1);
					z3 = z0 * (num + 1);

					x0 *= num;
					x1 *= num;
					z0 *= num;
					z1 *= num;

					if (type == Ring)
						y0 = y1 = y2 = y3 = 0.0f;
					else
					{
						y0 = y1 = 1.0f;
						y2 = y3 = 0.0f;
					}
				}

				Vertex v0 (x0, y0, z0),
					   v1 (x1, y1, z1),
					   v2 (x2, y2, z2),
					   v3 (x3, y3, z3);

				LDQuad* quad = new LDQuad;
				quad->setColor (maincolor);
				quad->setVertex (0, v0);
				quad->setVertex (1, v1);
				quad->setVertex (2, v2);
				quad->setVertex (3, v3);

				if (type == Cylinder)
					quad->invert();

				objs << quad;

				if (type == Cylinder || type == Cone)
					condLineSegs << i;
			} break;

			case Disc:
			case DiscNeg:
			{
				double x2, z2;

				if (type == Disc)
					x2 = z2 = 0.0f;
				else
				{
					x2 = (x0 >= 0.0f) ? 1.0f : -1.0f;
					z2 = (z0 >= 0.0f) ? 1.0f : -1.0f;
				}

				Vertex v0 (x0, 0.0f, z0),
					   v1 (x1, 0.0f, z1),
					   v2 (x2, 0.0f, z2);

				// Disc negatives need to go the other way around, otherwise
				// they'll end up upside-down.
				LDTriangle* seg = new LDTriangle;
				seg->setColor (maincolor);
				seg->setVertex (type == Disc ? 0 : 2, v0);
				seg->setVertex (1, v1);
				seg->setVertex (type == Disc ? 2 : 0, v2);
				objs << seg;
			} break;
		}
	}

	// If this is not a full circle, we need a conditional line at the other
	// end, too.
	if (segs < divs && condLineSegs.size() != 0)
		condLineSegs << segs;

	for (int i : condLineSegs)
	{
		Vertex v0 (radialPoint (i, divs, cos), 0.0f, radialPoint (i, divs, sin)),
		  v1,
		  v2 (radialPoint (i + 1, divs, cos), 0.0f, radialPoint (i + 1, divs, sin)),
		  v3 (radialPoint (i - 1, divs, cos), 0.0f, radialPoint (i - 1, divs, sin));

		if (type == Cylinder)
			v1 = Vertex (v0[X], 1.0f, v0[Z]);
		elif (type == Cone)
		{
			v1 = Vertex (v0[X] * (num + 1), 0.0f, v0[Z] * (num + 1));
			v0[X] *= num;
			v0[Y] = 1.0f;
			v0[Z] *= num;
		}

		LDCondLine* line = new LDCondLine;
		line->setColor (edgecolor);
		line->setVertex (0, v0);
		line->setVertex (1, v1);
		line->setVertex (2, v2);
		line->setVertex (3, v3);
		objs << line;
	}

	return objs;
}

// =============================================================================
// -----------------------------------------------------------------------------
static QString primitiveTypeName (PrimitiveType type)
{
	// Not translated as primitives are in English.
	return type == Circle   ? "Circle" :
		   type == Cylinder ? "Cylinder" :
		   type == Disc     ? "Disc" :
		   type == DiscNeg  ? "Disc Negative" :
		   type == Ring     ? "Ring" : "Cone";
}

// =============================================================================
// -----------------------------------------------------------------------------
QString radialFileName (PrimitiveType type, int segs, int divs, int num)
{
	int numer = segs,
			denom = divs;

	// Simplify the fractional part, but the denominator must be at least 4.
	simplify (numer, denom);

	if (denom < 4)
	{
		const int factor = 4 / denom;
		numer *= factor;
		denom *= factor;
	}

	// Compose some general information: prefix, fraction, root, ring number
	QString prefix = (divs == lores) ? "" : fmt ("%1/", divs);
	QString frac = fmt ("%1-%2", numer, denom);
	QString root = g_radialNameRoots[type];
	QString numstr = (type == Ring || type == Cone) ? fmt ("%1", num) : "";

	// Truncate the root if necessary (7-16rin4.dat for instance).
	// However, always keep the root at least 2 characters.
	int extra = (frac.length() + numstr.length() + root.length()) - 8;
	root.chop (clamp (extra, 0, 2));

	// Stick them all together and return the result.
	return prefix + frac + root + numstr + ".dat";
}

// =============================================================================
// -----------------------------------------------------------------------------
LDDocument* generatePrimitive (PrimitiveType type, int segs, int divs, int num)
{
	// Make the description
	QString frac = QString::number ((float) segs / divs);
	QString name = radialFileName (type, segs, divs, num);
	QString descr;

	// Ensure that there's decimals, even if they're 0.
	if (frac.indexOf (".") == -1)
		frac += ".0";

	if (type == Ring || type == Cone)
	{
		QString spacing =
			(num < 10) ? "  " :
			(num < 100) ? " "  : "";

		descr = fmt ("%1 %2%3 x %4", primitiveTypeName (type), spacing, num, frac);
	}
	else
		descr = fmt ("%1 %2", primitiveTypeName (type), frac);

	// Prepend "Hi-Res" if 48/ primitive.
	if (divs == hires)
		descr.insert (0, "Hi-Res ");

	LDDocument* f = new LDDocument;
	f->setDefaultName (name);

	QString author = APPNAME;
	QString license = "";

	if (ld_defaultname.isEmpty() == false)
	{
		license = getLicenseText (ld_defaultlicense);
		author = fmt ("%1 [%2]", ld_defaultname, ld_defaultuser);
	}

	f->addObjects (
	{
		new LDComment (descr),
		new LDComment (fmt ("Name: %1", name)),
		new LDComment (fmt ("Author: %1", author)),
		new LDComment (fmt ("!LDRAW_ORG Unofficial_%1Primitive", divs == hires ? "48_" : "")),
		new LDComment (license),
		new LDEmpty,
		new LDBFC (LDBFC::CertifyCCW),
		new LDEmpty,
	});

	f->addObjects (makePrimitive (type, segs, divs, num));
	return f;
}

// =============================================================================
// -----------------------------------------------------------------------------
LDDocument* getPrimitive (PrimitiveType type, int segs, int divs, int num)
{
	QString name = radialFileName (type, segs, divs, num);
	LDDocument* f = getDocument (name);

	if (f != null)
		return f;

	return generatePrimitive (type, segs, divs, num);
}

// =============================================================================
// -----------------------------------------------------------------------------
PrimitivePrompt::PrimitivePrompt (QWidget* parent, Qt::WindowFlags f) :
	QDialog (parent, f)
{
	ui = new Ui_MakePrimUI;
	ui->setupUi (this);
	connect (ui->cb_hires, SIGNAL (toggled (bool)), this, SLOT (hiResToggled (bool)));
}

// =============================================================================
// -----------------------------------------------------------------------------
PrimitivePrompt::~PrimitivePrompt()
{
	delete ui;
}

// =============================================================================
// -----------------------------------------------------------------------------
void PrimitivePrompt::hiResToggled (bool on)
{
	ui->sb_segs->setMaximum (on ? hires : lores);

	// If the current value is 16 and we switch to hi-res, default the
	// spinbox to 48.
	if (on && ui->sb_segs->value() == lores)
		ui->sb_segs->setValue (hires);
}

// =============================================================================
// -----------------------------------------------------------------------------
DEFINE_ACTION (MakePrimitive, 0)
{
	PrimitivePrompt* dlg = new PrimitivePrompt (g_win);

	if (!dlg->exec())
		return;

	int segs = dlg->ui->sb_segs->value();
	int divs = dlg->ui->cb_hires->isChecked() ? hires : lores;
	int num = dlg->ui->sb_ringnum->value();
	PrimitiveType type =
		dlg->ui->rb_circle->isChecked()   ? Circle :
		dlg->ui->rb_cylinder->isChecked() ? Cylinder :
		dlg->ui->rb_disc->isChecked()     ? Disc :
		dlg->ui->rb_ndisc->isChecked()    ? DiscNeg :
		dlg->ui->rb_ring->isChecked()     ? Ring : Cone;

	LDDocument* f = generatePrimitive (type, segs, divs, num);

	g_win->save (f, false);
	delete f;
}

mercurial