diff -r 5df69eb50182 -r f116b63c4844 src/ldDocument.cc --- a/src/ldDocument.cc Sat Aug 29 16:29:11 2015 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1545 +0,0 @@ -/* - * LDForge: LDraw parts authoring CAD - * Copyright (C) 2013, 2014 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 . - */ - -#include -#include -#include -#include -#include - -#include "main.h" -#include "configuration.h" -#include "ldDocument.h" -#include "miscallenous.h" -#include "mainWindow.h" -#include "editHistory.h" -#include "dialogs.h" -#include "glRenderer.h" -#include "glCompiler.h" -#include "partDownloader.h" - -CFGENTRY (String, LDrawPath, "") -CFGENTRY (List, RecentFiles, {}) -CFGENTRY (Bool, TryDownloadMissingFiles, false) -EXTERN_CFGENTRY (String, DownloadFilePath) -EXTERN_CFGENTRY (Bool, UseLogoStuds) - -static bool g_loadingMainFile = false; -static const int g_maxRecentFiles = 10; -static bool g_aborted = false; -static LDDocument* g_logoedStud; -static LDDocument* g_logoedStud2; -static QList g_allDocuments; -static QList g_explicitDocuments; -static LDDocument* g_currentDocument; -static bool g_loadingLogoedStuds = false; - -const QStringList g_specialSubdirectories ({ "s", "48", "8" }); - -// ============================================================================= -// -namespace LDPaths -{ - static QString pathError; - - struct - { - QString LDConfigPath; - QString partsPath, primsPath; - } pathInfo; - - void initPaths() - { - if (not tryConfigure (cfg::LDrawPath)) - { - LDrawPathDialog dlg (false); - - if (not dlg.exec()) - Exit(); - - cfg::LDrawPath = dlg.filename(); - } - } - - bool tryConfigure (QString path) - { - QDir dir; - - if (not dir.cd (path)) - { - pathError = "Directory does not exist."; - return false; - } - - QStringList mustHave = { "LDConfig.ldr", "parts", "p" }; - QStringList contents = dir.entryList (mustHave); - - if (contents.size() != mustHave.size()) - { - pathError = "Not an LDraw directory! Must
have LDConfig.ldr, parts/ and p/."; - return false; - } - - pathInfo.partsPath = format ("%1" DIRSLASH "parts", path); - pathInfo.LDConfigPath = format ("%1" DIRSLASH "LDConfig.ldr", path); - pathInfo.primsPath = format ("%1" DIRSLASH "p", path); - - return true; - } - - // Accessors - QString getError() - { - return pathError; - } - - QString ldconfig() - { - return pathInfo.LDConfigPath; - } - - QString prims() - { - return pathInfo.primsPath; - } - - QString parts() - { - return pathInfo.partsPath; - } -} - -// ============================================================================= -// -LDDocument::LDDocument() : - m_isImplicit (true), - m_flags (0), - m_verticesOutdated (true), - m_needVertexMerge (true), - m_gldata (new LDGLData) -{ - setSavePosition (-1); - setTabIndex (-1); - m_history = new History; - m_history->setDocument (this); - m_needsReCache = true; - g_allDocuments << this; -} - -// ============================================================================= -// -LDDocument* LDDocument::createNew() -{ - return new LDDocument(); -} - -// ============================================================================= -// -LDDocument::~LDDocument() -{ - g_allDocuments.removeOne (this); - m_flags |= DOCF_IsBeingDestroyed; - delete m_history; - delete m_gldata; -} - -// ============================================================================= -// -void LDDocument::setImplicit (bool const& a) -{ - if (m_isImplicit != a) - { - m_isImplicit = a; - - if (a == false) - { - g_explicitDocuments << this; - print ("Opened %1", name()); - - // Implicit files are not compiled by the GL renderer. Now that this - // part is no longer implicit, it needs to be compiled. - if (g_win != null) - g_win->R()->compiler()->compileDocument (this); - } - else - { - g_explicitDocuments.removeOne (this); - print ("Closed %1", name()); - } - - if (g_win != null) - g_win->updateDocumentList(); - - // If the current document just became implicit (e.g. it was 'closed'), - // we need to get a new current document. - if (current() == this and isImplicit()) - { - if (explicitDocuments().isEmpty()) - newFile(); - else - setCurrent (explicitDocuments().first()); - } - } -} - -// ============================================================================= -// -QList const& LDDocument::explicitDocuments() -{ - return g_explicitDocuments; -} - -// ============================================================================= -// -LDDocument* FindDocument (QString name) -{ - for (LDDocument* file : g_allDocuments) - { - if (file == null) - continue; - - if (Eq (name, file->name(), file->defaultName())) - return file; - } - - return nullptr; -} - -// ============================================================================= -// -QString Dirname (QString path) -{ - long lastpos = path.lastIndexOf (DIRSLASH); - - if (lastpos > 0) - return path.left (lastpos); - -#ifndef _WIN32 - if (path[0] == DIRSLASH_CHAR) - return DIRSLASH; -#endif // _WIN32 - - return ""; -} - -// ============================================================================= -// -QString Basename (QString path) -{ - long lastpos = path.lastIndexOf (DIRSLASH); - - if (lastpos != -1) - return path.mid (lastpos + 1); - - return path; -} - -// ============================================================================= -// -static QString FindDocumentPath (QString relpath, bool subdirs) -{ - QString fullPath; - - // LDraw models use Windows-style path separators. If we're not on Windows, - // replace the path separator now before opening any files. Qt expects - // forward-slashes as directory separators. -#ifndef WIN32 - relpath.replace ("\\", "/"); -#endif // WIN32 - - // Try find it relative to other currently open documents. We want a file - // in the immediate vicinity of a current model to override stock LDraw stuff. - QString reltop = Basename (Dirname (relpath)); - - for (LDDocument* doc : g_allDocuments) - { - if (doc == null) - continue; - - QString partpath = format ("%1/%2", Dirname (doc->fullPath()), relpath); - QFile f (partpath); - - if (f.exists()) - { - // ensure we don't mix subfiles and 48-primitives with non-subfiles and non-48 - QString proptop = Basename (Dirname (partpath)); - - bool bogus = false; - - for (QString s : g_specialSubdirectories) - { - if ((proptop == s and reltop != s) or (reltop == s and proptop != s)) - { - bogus = true; - break; - } - } - - if (not bogus) - return partpath; - } - } - - if (QFile::exists (relpath)) - return relpath; - - // Try with just the LDraw path first - fullPath = format ("%1" DIRSLASH "%2", cfg::LDrawPath, relpath); - - if (QFile::exists (fullPath)) - return fullPath; - - if (subdirs) - { - // Look in sub-directories: parts and p. Also look in net_downloadpath, since that's - // where we download parts from the PT to. - for (const QString& topdir : QList ({ cfg::LDrawPath, cfg::DownloadFilePath })) - { - for (const QString& subdir : QList ({ "parts", "p" })) - { - fullPath = format ("%1" DIRSLASH "%2" DIRSLASH "%3", topdir, subdir, relpath); - - if (QFile::exists (fullPath)) - return fullPath; - } - } - } - - // Did not find the file. - return ""; -} - -// ============================================================================= -// -QFile* OpenLDrawFile (QString relpath, bool subdirs, QString* pathpointer) -{ - print ("Opening %1...\n", relpath); - QString path = FindDocumentPath (relpath, subdirs); - - if (pathpointer != null) - *pathpointer = path; - - if (path.isEmpty()) - return null; - - QFile* fp = new QFile (path); - - if (fp->open (QIODevice::ReadOnly)) - return fp; - - fp->deleteLater(); - return null; -} - -// ============================================================================= -// -void LDFileLoader::start() -{ - setDone (false); - setProgress (0); - setAborted (false); - - if (isOnForeground()) - { - g_aborted = false; - - // Show a progress dialog if we're loading the main ldDocument.here so we can - // show progress updates and keep the WM posted that we're still here. - // Of course we cannot exec() the dialog because then the dialog would - // block. - dlg = new OpenProgressDialog (g_win); - dlg->setNumLines (lines().size()); - dlg->setModal (true); - dlg->show(); - - // Connect the loader in so we can show updates - connect (this, SIGNAL (workDone()), dlg, SLOT (accept())); - connect (dlg, SIGNAL (rejected()), this, SLOT (abort())); - } - else - dlg = null; - - // Begin working - work (0); -} - -// ============================================================================= -// -void LDFileLoader::work (int i) -{ - // User wishes to abort, so stop here now. - if (isAborted()) - { - for (LDObject* obj : m_objects) - obj->destroy(); - - m_objects.clear(); - setDone (true); - return; - } - - // Parse up to 300 lines per iteration - int max = i + 300; - - for (; i < max and i < (int) lines().size(); ++i) - { - QString line = lines()[i]; - - // Trim the trailing newline - QChar c; - - while (line.endsWith ("\n") or line.endsWith ("\r")) - line.chop (1); - - LDObject* obj = ParseLine (line); - - // Check for parse errors and warn about tthem - if (obj->type() == OBJ_Error) - { - print ("Couldn't parse line #%1: %2", - progress() + 1, static_cast (obj)->reason()); - - if (warnings() != null) - (*warnings())++; - } - - m_objects << obj; - setProgress (i); - - // If we have a dialog pointer, update the progress now - if (isOnForeground()) - dlg->updateProgress (i); - } - - // If we're done now, tell the environment we're done and stop. - if (i >= ((int) lines().size()) - 1) - { - emit workDone(); - setDone (true); - return; - } - - // Otherwise, continue, by recursing back. - if (not isDone()) - { - // If we have a dialog to show progress output to, we cannot just call - // work() again immediately as the dialog needs some processor cycles as - // well. Thus, take a detour through the event loop by using the - // meta-object system. - // - // This terminates the loop here and control goes back to the function - // which called the file loader. It will keep processing the event loop - // until we're ready (see loadFileContents), thus the event loop will - // eventually catch the invokation we throw here and send us back. Though - // it's not technically recursion anymore, more like a for loop. :P - if (isOnForeground()) - QMetaObject::invokeMethod (this, "work", Qt::QueuedConnection, Q_ARG (int, i)); - else - work (i); - } -} - -// ============================================================================= -// -void LDFileLoader::abort() -{ - setAborted (true); - - if (isOnForeground()) - g_aborted = true; -} - -// ============================================================================= -// -LDObjectList LoadFileContents (QFile* fp, int* numWarnings, bool* ok) -{ - QStringList lines; - LDObjectList objs; - - if (numWarnings) - *numWarnings = 0; - - // Read in the lines - while (not fp->atEnd()) - lines << QString::fromUtf8 (fp->readLine()); - - LDFileLoader* loader = new LDFileLoader; - loader->setWarnings (numWarnings); - loader->setLines (lines); - loader->setOnForeground (g_loadingMainFile); - loader->start(); - - // After start() returns, if the loader isn't done yet, it's delaying - // its next iteration through the event loop. We need to catch this here - // by telling the event loop to tick, which will tick the file loader again. - // We keep doing this until the file loader is ready. - while (not loader->isDone()) - qApp->processEvents(); - - // If we wanted the success value, supply that now - if (ok) - *ok = not loader->isAborted(); - - objs = loader->objects(); - delete loader; - return objs; -} - -// ============================================================================= -// -LDDocument* OpenDocument (QString path, bool search, bool implicit, LDDocument* fileToOverride) -{ - // Convert the file name to lowercase when searching because some parts contain subfile - // subfile references with uppercase file names. I'll assume here that the library will always - // use lowercase file names for the part files. - QFile* fp; - QString fullpath; - - if (search) - { - fp = OpenLDrawFile (path.toLower(), true, &fullpath); - } - else - { - fp = new QFile (path); - fullpath = path; - - if (not fp->open (QIODevice::ReadOnly)) - { - delete fp; - return nullptr; - } - } - - if (not fp) - return nullptr; - - LDDocument* load = (fileToOverride != null ? fileToOverride : LDDocument::createNew()); - load->setImplicit (implicit); - load->setFullPath (fullpath); - load->setName (LDDocument::shortenName (load->fullPath())); - - // Loading the file shouldn't count as actual edits to the document. - load->history()->setIgnoring (true); - - int numWarnings; - bool ok; - LDObjectList objs = LoadFileContents (fp, &numWarnings, &ok); - fp->close(); - fp->deleteLater(); - - if (not ok) - { - load->dismiss(); - return nullptr; - } - - load->addObjects (objs); - - if (g_loadingMainFile) - { - LDDocument::setCurrent (load); - g_win->R()->setDocument (load); - print (QObject::tr ("File %1 parsed successfully (%2 errors)."), path, numWarnings); - } - - load->history()->setIgnoring (false); - return load; -} - -// ============================================================================= -// -bool LDDocument::isSafeToClose() -{ - using msgbox = QMessageBox; - setlocale (LC_ALL, "C"); - - // If we have unsaved changes, warn and give the option of saving. - if (hasUnsavedChanges()) - { - QString message = format (QObject::tr ("There are unsaved changes to %1. Should it be saved?"), -getDisplayName()); - - int button = msgbox::question (g_win, QObject::tr ("Unsaved Changes"), message, - (msgbox::Yes | msgbox::No | msgbox::Cancel), msgbox::Cancel); - - switch (button) - { - case msgbox::Yes: - { - // If we don't have a file path yet, we have to ask the user for one. - if (name().length() == 0) - { - QString newpath = QFileDialog::getSaveFileName (g_win, QObject::tr ("Save As"), - CurrentDocument()->name(), QObject::tr ("LDraw files (*.dat *.ldr)")); - - if (newpath.length() == 0) - return false; - - setName (newpath); - } - - if (not save()) - { - message = format (QObject::tr ("Failed to save %1 (%2)\nDo you still want to close?"), - name(), strerror (errno)); - - if (msgbox::critical (g_win, QObject::tr ("Save Failure"), message, - (msgbox::Yes | msgbox::No), msgbox::No) == msgbox::No) - { - return false; - } - } - break; - } - - case msgbox::Cancel: - return false; - - default: - break; - } - } - - return true; -} - -// ============================================================================= -// -void CloseAllDocuments() -{ - for (LDDocument* file : g_explicitDocuments) - file->dismiss(); -} - -// ============================================================================= -// -void newFile() -{ - // Create a new anonymous file and set it to our current - LDDocument* f = LDDocument::createNew(); - f->setName (""); - f->setImplicit (false); - LDDocument::setCurrent (f); - LDDocument::closeInitialFile(); - g_win->R()->setDocument (f); - g_win->doFullRefresh(); - g_win->updateTitle(); - g_win->updateActions(); -} - -// ============================================================================= -// -void AddRecentFile (QString path) -{ - int idx = cfg::RecentFiles.indexOf (path); - - // If this file already is in the list, pop it out. - if (idx != -1) - { - if (idx == cfg::RecentFiles.size() - 1) - return; // first recent file - abort and do nothing - - cfg::RecentFiles.removeAt (idx); - } - - // If there's too many recent files, drop one out. - while (cfg::RecentFiles.size() > (g_maxRecentFiles - 1)) - cfg::RecentFiles.removeAt (0); - - // Add the file - cfg::RecentFiles << path; - - Config::Save(); - g_win->updateRecentFilesMenu(); -} - -// ============================================================================= -// Open an LDraw file and set it as the main model -// ============================================================================= -void OpenMainModel (QString path) -{ - // If there's already a file with the same name, this file must replace it. - LDDocument* documentToReplace = nullptr; - LDDocument* file = nullptr; - QString shortName = LDDocument::shortenName (path); - - for (LDDocument* doc : g_allDocuments) - { - if (doc != null and doc->name() == shortName) - { - documentToReplace = doc; - break; - } - } - - // We cannot open this file if the document this would replace is not - // safe to close. - if (documentToReplace != null and not documentToReplace->isSafeToClose()) - return; - - g_loadingMainFile = true; - - // If we're replacing an existing document, clear the document and - // make it ready for being loaded to. - if (documentToReplace != null) - { - file = documentToReplace; - file->clear(); - } - - file = OpenDocument (path, false, false, file); - - if (file == null) - { - if (not g_aborted) - { - // Tell the user loading failed. - setlocale (LC_ALL, "C"); - Critical (format (QObject::tr ("Failed to open %1: %2"), path, strerror (errno))); - } - - g_loadingMainFile = false; - return; - } - - file->setImplicit (false); - - // If we have an anonymous, unchanged file open as the only open file - // (aside of the one we just opened), close it now. - LDDocument::closeInitialFile(); - - // Rebuild the object tree view now. - LDDocument::setCurrent (file); - g_win->doFullRefresh(); - - // Add it to the recent files list. - AddRecentFile (path); - g_loadingMainFile = false; - - // If there were problems loading subfile references, try see if we can find these - // files on the parts tracker. - QStringList unknowns; - - for (LDObject* obj : file->objects()) - { - if (obj->type() != OBJ_Error or static_cast (obj)->fileReferenced().isEmpty()) - continue; - - unknowns << static_cast (obj)->fileReferenced(); - } - - if (cfg::TryDownloadMissingFiles and not unknowns.isEmpty()) - { - PartDownloader dl; - - if (dl.checkValidPath()) - { - dl.setSource (PartDownloader::PartsTracker); - dl.setPrimaryFile (file); - - for (QString const& unknown : unknowns) - dl.downloadFromPartsTracker (unknown); - - dl.exec(); - dl.checkIfFinished(); - file->reloadAllSubfiles(); - } - } -} - -// ============================================================================= -// -bool LDDocument::save (QString path, int64* sizeptr) -{ - if (isImplicit()) - return false; - - if (not path.length()) - path = fullPath(); - - // If the second object in the list holds the file name, update that now. - LDObject* nameObject = getObject (1); - - if (nameObject != null and nameObject->type() == OBJ_Comment) - { - LDComment* nameComment = static_cast (nameObject); - - if (nameComment->text().left (6) == "Name: ") - { - QString newname = shortenName (path); - nameComment->setText (format ("Name: %1", newname)); - g_win->buildObjList(); - } - } - - QByteArray data; - - if (sizeptr != null) - *sizeptr = 0; - - // File is open, now save the model to it. Note that LDraw requires files to have DOS line endings. - for (LDObject* obj : objects()) - { - QByteArray subdata ((obj->asText() + "\r\n").toUtf8()); - data.append (subdata); - - if (sizeptr != null) - *sizeptr += subdata.size(); - } - - QFile f (path); - - if (not f.open (QIODevice::WriteOnly)) - return false; - - f.write (data); - f.close(); - - // We have successfully saved, update the save position now. - setSavePosition (history()->position()); - setFullPath (path); - setName (shortenName (path)); - - g_win->updateDocumentListItem (this); - g_win->updateTitle(); - return true; -} - -// ============================================================================= -// -void LDDocument::clear() -{ - for (LDObject* obj : objects()) - forgetObject (obj); -} - -// ============================================================================= -// -static void CheckTokenCount (const QStringList& tokens, int num) -{ - if (tokens.size() != num) - throw QString (format ("Bad amount of tokens, expected %1, got %2", num, tokens.size())); -} - -// ============================================================================= -// -static void CheckTokenNumbers (const QStringList& tokens, int min, int max) -{ - bool ok; - - QRegExp scient ("\\-?[0-9]+\\.[0-9]+e\\-[0-9]+"); - - for (int i = min; i <= max; ++i) - { - // Check for floating point - tokens[i].toDouble (&ok); - if (ok) - return; - - // Check hex - if (tokens[i].startsWith ("0x")) - { - tokens[i].mid (2).toInt (&ok, 16); - - if (ok) - return; - } - - // Check scientific notation, e.g. 7.99361e-15 - if (scient.exactMatch (tokens[i])) - return; - - throw QString (format ("Token #%1 was `%2`, expected a number (matched length: %3)", - (i + 1), tokens[i], scient.matchedLength())); - } -} - -// ============================================================================= -// -static Vertex ParseVertex (QStringList& s, const int n) -{ - Vertex v; - v.apply ([&] (Axis ax, double& a) { a = s[n + ax].toDouble(); }); - return v; -} - -static int32 StringToNumber (QString a, bool* ok = null) -{ - int base = 10; - - if (a.startsWith ("0x")) - { - a.remove (0, 2); - base = 16; - } - - return a.toLong (ok, base); -} - -// ============================================================================= -// This is the LDraw code parser function. It takes in a string containing LDraw -// code and returns the object parsed from it. parseLine never returns null, -// the object will be LDError if it could not be parsed properly. -// ============================================================================= -LDObject* ParseLine (QString line) -{ - try - { - QStringList tokens = line.split (" ", QString::SkipEmptyParts); - - if (tokens.size() <= 0) - { - // Line was empty, or only consisted of whitespace - return LDSpawn(); - } - - if (tokens[0].length() != 1 or not tokens[0][0].isDigit()) - throw QString ("Illogical line code"); - - int num = tokens[0][0].digitValue(); - - switch (num) - { - case 0: - { - // Comment - QString commentText (line.mid (line.indexOf ("0") + 2)); - QString commentTextSimplified (commentText.simplified()); - - // Handle BFC statements - if (tokens.size() > 2 and tokens[1] == "BFC") - { - for_enum (BFCStatement, i) - { - if (commentTextSimplified == format ("BFC %1", - LDBFC::StatementStrings[int (i)])) - { - return LDSpawn (i); - } - } - - // MLCAD is notorious for stuffing these statements in parts it - // creates. The above block only handles valid statements, so we - // need to handle MLCAD-style invertnext, clip and noclip separately. - if (commentTextSimplified == "BFC CERTIFY INVERTNEXT") - return LDSpawn (BFCStatement::InvertNext); - elif (commentTextSimplified == "BFC CERTIFY CLIP") - return LDSpawn (BFCStatement::Clip); - elif (commentTextSimplified == "BFC CERTIFY NOCLIP") - return LDSpawn (BFCStatement::NoClip); - } - - if (tokens.size() > 2 and tokens[1] == "!LDFORGE") - { - // Handle LDForge-specific types, they're embedded into comments too - if (tokens[2] == "VERTEX") - { - // Vertex (0 !LDFORGE VERTEX) - CheckTokenCount (tokens, 7); - CheckTokenNumbers (tokens, 3, 6); - - LDVertex* obj = LDSpawn(); - obj->setColor (StringToNumber (tokens[3])); - obj->pos.apply ([&](Axis ax, double& value) - { value = tokens[4 + ax].toDouble(); }); - return obj; - } - elif (tokens[2] == "OVERLAY") - { - CheckTokenCount (tokens, 9); - CheckTokenNumbers (tokens, 5, 8); - - LDOverlay* obj = LDSpawn(); - obj->setFileName (tokens[3]); - obj->setCamera (tokens[4].toLong()); - obj->setX (tokens[5].toLong()); - obj->setY (tokens[6].toLong()); - obj->setWidth (tokens[7].toLong()); - obj->setHeight (tokens[8].toLong()); - return obj; - } - } - - // Just a regular comment: - LDComment* obj = LDSpawn(); - obj->setText (commentText); - return obj; - } - - case 1: - { - // Subfile - CheckTokenCount (tokens, 15); - CheckTokenNumbers (tokens, 1, 13); - - // Try open the file. Disable g_loadingMainFile temporarily since we're - // not loading the main file now, but the subfile in question. - bool tmp = g_loadingMainFile; - g_loadingMainFile = false; - LDDocument* load = GetDocument (tokens[14]); - g_loadingMainFile = tmp; - - // If we cannot open the file, mark it an error. Note we cannot use LDParseError - // here because the error object needs the document reference. - if (not load) - { - LDError* obj = LDSpawn (line, format ("Could not open %1", tokens[14])); - obj->setFileReferenced (tokens[14]); - return obj; - } - - LDSubfile* obj = LDSpawn(); - obj->setColor (StringToNumber (tokens[1])); - obj->setPosition (ParseVertex (tokens, 2)); // 2 - 4 - - Matrix transform; - - for (int i = 0; i < 9; ++i) - transform[i] = tokens[i + 5].toDouble(); // 5 - 13 - - obj->setTransform (transform); - obj->setFileInfo (load); - return obj; - } - - case 2: - { - CheckTokenCount (tokens, 8); - CheckTokenNumbers (tokens, 1, 7); - - // Line - LDLine* obj (LDSpawn()); - obj->setColor (StringToNumber (tokens[1])); - - for (int i = 0; i < 2; ++i) - obj->setVertex (i, ParseVertex (tokens, 2 + (i * 3))); // 2 - 7 - - return obj; - } - - case 3: - { - CheckTokenCount (tokens, 11); - CheckTokenNumbers (tokens, 1, 10); - - // Triangle - LDTriangle* obj (LDSpawn()); - obj->setColor (StringToNumber (tokens[1])); - - for (int i = 0; i < 3; ++i) - obj->setVertex (i, ParseVertex (tokens, 2 + (i * 3))); // 2 - 10 - - return obj; - } - - case 4: - case 5: - { - CheckTokenCount (tokens, 14); - CheckTokenNumbers (tokens, 1, 13); - - // Quadrilateral / Conditional line - LDObject* obj; - - if (num == 4) - obj = LDSpawn(); - else - obj = LDSpawn(); - - obj->setColor (StringToNumber (tokens[1])); - - for (int i = 0; i < 4; ++i) - obj->setVertex (i, ParseVertex (tokens, 2 + (i * 3))); // 2 - 13 - - return obj; - } - - default: - throw QString ("Unknown line code number"); - } - } - catch (QString& e) - { - // Strange line we couldn't parse - return LDSpawn (line, e); - } -} - -// ============================================================================= -// -LDDocument* GetDocument (QString filename) -{ - // Try find the file in the list of loaded files - LDDocument* doc = FindDocument (filename); - - // If it's not loaded, try open it - if (not doc) - doc = OpenDocument (filename, true, true); - - return doc; -} - -// ============================================================================= -// -void LDDocument::reloadAllSubfiles() -{ - print ("Reloading subfiles of %1", getDisplayName()); - - // Go through all objects in the current file and reload the subfiles - for (LDObject* obj : objects()) - { - if (obj->type() == OBJ_Subfile) - { - LDSubfile* ref = static_cast (obj); - LDDocument* fileInfo = GetDocument (ref->fileInfo()->name()); - - if (fileInfo != null) - { - ref->setFileInfo (fileInfo); - } - else - { - ref->replace (LDSpawn (ref->asText(), - format ("Could not open %1", ref->fileInfo()->name()))); - } - } - - // Reparse gibberish files. It could be that they are invalid because - // of loading errors. Circumstances may be different now. - if (obj->type() == OBJ_Error) - obj->replace (ParseLine (static_cast (obj)->contents())); - } - - m_needsReCache = true; - - if (this == CurrentDocument()) - g_win->buildObjList(); -} - -// ============================================================================= -// -int LDDocument::addObject (LDObject* obj) -{ - history()->add (new AddHistory (objects().size(), obj)); - m_objects << obj; - addKnownVertices (obj); - obj->setDocument (this); - g_win->R()->compileObject (obj); - return getObjectCount() - 1; -} - -// ============================================================================= -// -void LDDocument::addObjects (const LDObjectList& objs) -{ - for (LDObject* obj : objs) - { - if (obj != null) - addObject (obj); - } -} - -// ============================================================================= -// -void LDDocument::insertObj (int pos, LDObject* obj) -{ - history()->add (new AddHistory (pos, obj)); - m_objects.insert (pos, obj); - obj->setDocument (this); - g_win->R()->compileObject (obj); - - -#ifdef DEBUG - if (not isImplicit()) - dprint ("Inserted object #%1 (%2) at %3\n", obj->id(), obj->typeName(), pos); -#endif -} - -// ============================================================================= -// -void LDDocument::addKnownVertices (LDObject* obj) -{ - auto it = m_objectVertices.find (obj); - - if (it == m_objectVertices.end()) - it = m_objectVertices.insert (obj, QVector()); - else - it->clear(); - - obj->getVertices (*it); - needVertexMerge(); -} - -// ============================================================================= -// -void LDDocument::forgetObject (LDObject* obj) -{ - int idx = obj->lineNumber(); - - if (m_objects[idx] == obj) - { - obj->deselect(); - - if (not isImplicit() and not (flags() & DOCF_IsBeingDestroyed)) - { - history()->add (new DelHistory (idx, obj)); - m_objectVertices.remove (obj); - } - - m_objects.removeAt (idx); - obj->setDocument (nullptr); - } -} - -// ============================================================================= -// -bool IsSafeToCloseAll() -{ - for (LDDocument* f : LDDocument::explicitDocuments()) - { - if (not f->isSafeToClose()) - return false; - } - - return true; -} - -// ============================================================================= -// -void LDDocument::setObject (int idx, LDObject* obj) -{ - assert (idx >= 0 and idx < m_objects.size()); - - // Mark this change to history - if (not m_history->isIgnoring()) - { - QString oldcode = getObject (idx)->asText(); - QString newcode = obj->asText(); - *m_history << new EditHistory (idx, oldcode, newcode); - } - - m_objectVertices.remove (m_objects[idx]); - m_objects[idx]->deselect(); - m_objects[idx]->setDocument (nullptr); - obj->setDocument (this); - addKnownVertices (obj); - g_win->R()->compileObject (obj); - m_objects[idx] = obj; - needVertexMerge(); -} - -// ============================================================================= -// -LDObject* LDDocument::getObject (int pos) const -{ - if (pos < m_objects.size()) - return m_objects[pos]; - else - return nullptr; -} - -// ============================================================================= -// -int LDDocument::getObjectCount() const -{ - return objects().size(); -} - -// ============================================================================= -// -bool LDDocument::hasUnsavedChanges() const -{ - return not isImplicit() and history()->position() != savePosition(); -} - -// ============================================================================= -// -QString LDDocument::getDisplayName() -{ - if (not name().isEmpty()) - return name(); - - if (not defaultName().isEmpty()) - return "[" + defaultName() + "]"; - - return QObject::tr ("untitled"); -} - -// ============================================================================= -// -void LDDocument::initializeCachedData() -{ - if (m_needsReCache) - { - m_vertices.clear(); - - for (LDObject* obj : inlineContents (true, true)) - { - if (obj->type() == OBJ_Subfile) - { - print ("Warning: unable to inline %1 into %2", - static_cast (obj)->fileInfo()->getDisplayName(), - getDisplayName()); - continue; - } - - LDPolygon* data = obj->getPolygon(); - - if (data != null) - { - m_polygonData << *data; - delete data; - } - } - - m_needsReCache = false; - } - - if (m_verticesOutdated) - { - m_objectVertices.clear(); - - for (LDObject* obj : inlineContents (true, false)) - addKnownVertices (obj); - - mergeVertices(); - m_verticesOutdated = false; - } - - if (m_needVertexMerge) - mergeVertices(); -} - -// ============================================================================= -// -void LDDocument::mergeVertices() -{ - m_vertices.clear(); - - for (QVector const& verts : m_objectVertices) - m_vertices << verts; - - RemoveDuplicates (m_vertices); - m_needVertexMerge = false; -} - -// ============================================================================= -// -QList LDDocument::inlinePolygons() -{ - initializeCachedData(); - return polygonData(); -} - -// ============================================================================= -// ----------------------------------------------------------------------------- -LDObjectList LDDocument::inlineContents (bool deep, bool renderinline) -{ - // Possibly substitute with logoed studs: - // stud.dat -> stud-logo.dat - // stud2.dat -> stud-logo2.dat - if (cfg::UseLogoStuds and renderinline) - { - // Ensure logoed studs are loaded first - LoadLogoStuds(); - - if (name() == "stud.dat" and g_logoedStud != null) - return g_logoedStud->inlineContents (deep, renderinline); - elif (name() == "stud2.dat" and g_logoedStud2 != null) - return g_logoedStud2->inlineContents (deep, renderinline); - } - - LDObjectList objs, objcache; - - for (LDObject* obj : objects()) - { - // Skip those without scemantic meaning - if (not obj->isScemantic()) - continue; - - // Got another sub-file reference, inline it if we're deep-inlining. If not, - // just add it into the objects normally. Yay, recursion! - if (deep == true and obj->type() == OBJ_Subfile) - { - for (LDObject* otherobj : static_cast (obj)->inlineContents (deep, renderinline)) - objs << otherobj; - } - else - objs << obj->createCopy(); - } - - return objs; -} - -// ============================================================================= -// -LDDocument* LDDocument::current() -{ - return g_currentDocument; -} - -// ============================================================================= -// Sets the given file as the current one on display. At some point in time this -// was an operation completely unheard of. ;) -// -// TODO: f can be temporarily null. This probably should not be the case. -// ============================================================================= -void LDDocument::setCurrent (LDDocument* f) -{ - // Implicit files were loaded for caching purposes and must never be set - // current. - if (f != null and f->isImplicit()) - return; - - g_currentDocument = f; - - if (g_win and f) - { - // A ton of stuff needs to be updated - g_win->updateDocumentListItem (f); - g_win->buildObjList(); - g_win->updateTitle(); - g_win->R()->setDocument (f); - g_win->R()->compiler()->needMerge(); - print ("Changed file to %1", f->getDisplayName()); - } -} - -// ============================================================================= -// -int LDDocument::countExplicitFiles() -{ - return g_explicitDocuments.size(); -} - -// ============================================================================= -// This little beauty closes the initial file that was open at first when opening -// a new file over it. -// ============================================================================= -void LDDocument::closeInitialFile() -{ - if (g_explicitDocuments.size() == 2 and - g_explicitDocuments[0]->name().isEmpty() and - not g_explicitDocuments[1]->name().isEmpty() and - not g_explicitDocuments[0]->hasUnsavedChanges()) - { - LDDocument* filetoclose = g_explicitDocuments.first(); - filetoclose->dismiss(); - } -} - -// ============================================================================= -// -void LoadLogoStuds() -{ - if (g_loadingLogoedStuds or (g_logoedStud and g_logoedStud2)) - return; - - g_loadingLogoedStuds = true; - g_logoedStud = OpenDocument ("stud-logo.dat", true, true); - g_logoedStud2 = OpenDocument ("stud2-logo.dat", true, true); - print (QObject::tr ("Logoed studs loaded.\n")); - g_loadingLogoedStuds = false; -} - -// ============================================================================= -// -void LDDocument::addToSelection (LDObject* obj) // [protected] -{ - if (obj->isSelected()) - return; - - assert (obj->document() == this); - m_sel << obj; - g_win->R()->compileObject (obj); - obj->setSelected (true); -} - -// ============================================================================= -// -void LDDocument::removeFromSelection (LDObject* obj) // [protected] -{ - if (not obj->isSelected()) - return; - - assert (obj->document() == this); - m_sel.removeOne (obj); - g_win->R()->compileObject (obj); - obj->setSelected (false); -} - -// ============================================================================= -// -void LDDocument::clearSelection() -{ - for (LDObject* obj : m_sel) - removeFromSelection (obj); - - assert (m_sel.isEmpty()); -} - -// ============================================================================= -// -const LDObjectList& LDDocument::getSelection() const -{ - return m_sel; -} - -// ============================================================================= -// -void LDDocument::swapObjects (LDObject* one, LDObject* other) -{ - int a = m_objects.indexOf (one); - int b = m_objects.indexOf (other); - assert (a != b and a != -1 and b != -1); - m_objects[b] = one; - m_objects[a] = other; - addToHistory (new SwapHistory (one->id(), other->id())); -} - -// ============================================================================= -// -QString LDDocument::shortenName (QString a) // [static] -{ - QString shortname = Basename (a); - QString topdirname = Basename (Dirname (a)); - - if (g_specialSubdirectories.contains (topdirname)) - shortname.prepend (topdirname + "\\"); - - return shortname; -} - -// ============================================================================= -// -QVector const& LDDocument::inlineVertices() -{ - initializeCachedData(); - return m_vertices; -} - -void LDDocument::redoVertices() -{ - m_verticesOutdated = true; -} - -void LDDocument::needVertexMerge() -{ - m_needVertexMerge = true; -}