--- a/src/primitives.cc Mon Jan 20 23:44:22 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,704 +0,0 @@ -/* - * 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 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<QString>& 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<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)); - } -} - -// ============================================================================= -// ----------------------------------------------------------------------------- -LDObjectList makePrimitive (PrimitiveType type, int segs, int divs, int num) -{ - LDObjectList 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; -}