diff -r 3d8ab0f89102 -r 450827da2376 src/Primitives.cc
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Primitives.cc Tue Jan 21 02:09:14 2014 +0200
@@ -0,0 +1,704 @@
+/*
+ * 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 .
+ */
+
+#include
+#include
+#include
+#include "Document.h"
+#include "MainWindow.h"
+#include "Primitives.h"
+#include "ui_makeprim.h"
+#include "Misc.h"
+#include "Colors.h"
+#include "moc_Primitives.cpp"
+
+QList g_PrimitiveCategories;
+QList g_primitives;
+static PrimitiveScanner* g_activeScanner = 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"
+};
+
+PrimitiveScanner* getActivePrimitiveScanner()
+{
+ return g_activeScanner;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void loadPrimitives()
+{
+ PrimitiveCategory::loadCategories();
+
+ // Try to load prims.cfg
+ QFile conf (Config::filepath ("prims.cfg"));
+
+ if (!conf.open (QIODevice::ReadOnly))
+ {
+ // No prims.cfg, build it
+ PrimitiveScanner::start();
+ }
+ else
+ {
+ while (conf.atEnd() == false)
+ {
+ QString line = conf.readLine();
+
+ if (line.endsWith ("\n"))
+ line.chop (1);
+
+ 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();
+ log ("%1 primitives loaded.\n", g_primitives.size());
+ }
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+static void recursiveGetFilenames (QDir dir, QList& fnames)
+{
+ QFileInfoList flist = dir.entryInfoList (QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
+
+ for (const QFileInfo& info : flist)
+ {
+ if (info.isDir())
+ recursiveGetFilenames (QDir (info.absoluteFilePath()), fnames);
+ else
+ fnames << info.absoluteFilePath();
+ }
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+PrimitiveScanner::PrimitiveScanner (QObject* parent) :
+ QObject (parent),
+ m_i (0)
+{
+ g_activeScanner = this;
+ QDir dir (LDPaths::prims());
+ assert (dir.exists());
+ m_baselen = dir.absolutePath().length();
+ recursiveGetFilenames (dir, m_files);
+ emit starting (m_files.size());
+ log ("Scanning primitives...");
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+PrimitiveScanner::~PrimitiveScanner()
+{
+ g_activeScanner = null;
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+void PrimitiveScanner::work()
+{
+ int j = min (m_i + 100, m_files.size());
+
+ for (; m_i < j; ++m_i)
+ {
+ QString fname = m_files[m_i];
+ QFile f (fname);
+
+ if (!f.open (QIODevice::ReadOnly))
+ continue;
+
+ 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;
+ QByteArray titledata = f.readLine();
+
+ if (titledata != QByteArray())
+ info.title = QString::fromUtf8 (titledata);
+
+ info.title = info.title.simplified();
+
+ if (Q_LIKELY (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
+ QString path = Config::filepath ("prims.cfg");
+ QFile conf (path);
+
+ if (!conf.open (QIODevice::WriteOnly | QIODevice::Text))
+ critical (fmt ("Couldn't write primitive list %1: %2",
+ path, conf.errorString()));
+ else
+ {
+ for (Primitive& info : m_prims)
+ fprint (conf, "%1 %2\r\n", info.name, info.title);
+
+ conf.close();
+ }
+
+ g_primitives = m_prims;
+ PrimitiveCategory::populateCategories();
+ log ("%1 primitives scanned", g_primitives.size());
+ g_activeScanner = 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 PrimitiveScanner::start()
+{
+ if (g_activeScanner)
+ return;
+
+ PrimitiveScanner* scanner = new PrimitiveScanner;
+ scanner->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();
+ QString path = Config::dirpath() + "primregexps.cfg";
+
+ if (!QFile::exists (path))
+ path = ":/data/primitive-categories.cfg";
+
+ QFile f (path);
+
+ if (!f.open (QIODevice::ReadOnly))
+ {
+ critical (fmt (QObject::tr ("Failed to open primitive categories: %1"), f.errorString()));
+ return;
+ }
+
+ PrimitiveCategory* cat = null;
+
+ while (f.atEnd() == false)
+ {
+ QString line = f.readLine();
+ int colon;
+
+ if (line.endsWith ("\n"))
+ line.chop (1);
+
+ 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;
+ f.close();
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+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_activeScanner != 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& 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));
+ }
+}
+
+// =============================================================================
+// -----------------------------------------------------------------------------
+LDObjectList makePrimitive (PrimitiveType type, int segs, int divs, int num)
+{
+ LDObjectList objs;
+ QList condLineSegs;
+ QList 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;
+}