Mon, 15 Apr 2013 18:07:29 +0300
Added select by type. Selection is now preserved over operations.
/* * LDForge: LDraw parts authoring CAD * Copyright (C) 2013 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 <vector> #include <stdio.h> #include "common.h" #include "config.h" #include "file.h" #include "misc.h" #include "bbox.h" #include "gui.h" cfg (str, io_ldpath, ""); cfg (str, io_recentfiles, ""); // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= OpenFile* findLoadedFile (str zName) { for (OpenFile* file : g_LoadedFiles) if (file->zFileName == zName) return file; return null; } // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= FILE* openLDrawFile (str path, bool bSubDirectories) { str zTruePath = path; #ifndef WIN32 zTruePath.replace ("\\", "/"); #endif // WIN32 FILE* fp = fopen (path.chars (), "r"); str zFilePath; if (fp != null) return fp; if (~io_ldpath.value) { // Try with just the LDraw path first zFilePath = str::mkfmt ("%s" DIRSLASH "%s", io_ldpath.value.chars(), zTruePath.chars()); printf ("try %s\n", zFilePath.chars()); fp = fopen (zFilePath, "r"); if (fp != null) return fp; if (bSubDirectories) { char const* saSubdirectories[] = { "parts", "p", }; for (char const* sSubdir : saSubdirectories) { zFilePath = str::mkfmt ("%s" DIRSLASH "%s" DIRSLASH "%s", io_ldpath.value.chars(), sSubdir, zTruePath.chars()); printf ("try %s\n", zFilePath.chars()); fp = fopen (zFilePath.chars (), "r"); if (fp) return fp; } } } return null; } // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= OpenFile* openDATFile (str path) { logf ("Opening %s...\n", path.chars()); // Convert the file name to lowercase since some parts contain uppercase // file names. I'll assume here that the library will always use lowercase // file names for the actual parts.. FILE* fp = openLDrawFile (-path, true); if (!fp) { logf (LOG_Error, "Couldn't open %s: %s\n", path.chars (), strerror (errno)); return null; } OpenFile* load = new OpenFile; ulong numWarnings = 0; load->zFileName = path; vector<str> lines; { char line[1024]; while (fgets (line, sizeof line, fp)) { // Trim the trailing newline str zLine = line; while (zLine[~zLine - 1] == '\n' || zLine[~zLine - 1] == '\r') zLine -= 1; lines.push_back (zLine); } } fclose (fp); for (str line : lines) { LDObject* obj = parseLine (line); load->objects.push_back (obj); // Check for parse errors and warn about tthem if (obj->getType() == OBJ_Gibberish) { logf (LOG_Warning, "Couldn't parse line #%lu: %s\n", (&line - &(lines[0])), static_cast<LDGibberish*> (obj)->zReason.chars()); logf (LOG_Warning, "- Line was: %s\n", line.chars()); numWarnings++; } } g_LoadedFiles.push_back (load); logf (LOG_Success, "File %s parsed successfully (%lu warning%s).\n", path.chars(), numWarnings, PLURAL (numWarnings)); return load; } // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= // Clear everything from the model void OpenFile::close () { for (LDObject* obj : objects) delete obj; // Clear the cache as well for (LDObject* obj : objCache) delete obj; delete this; } // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= void closeAll () { if (!g_LoadedFiles.size()) return; // Remove all loaded files and the objects they contain for (OpenFile* file : g_LoadedFiles) file->close (); // Clear the array g_LoadedFiles.clear(); g_CurrentFile = NULL; g_ForgeWindow->refresh (); } // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= void newFile () { // Create a new anonymous file and set it to our current closeAll (); OpenFile* f = new OpenFile; f->zFileName = ""; g_LoadedFiles.push_back (f); g_CurrentFile = f; g_BBox.calculate(); g_ForgeWindow->refresh (); } // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= void addRecentFile (str zPath) { long lPos = io_recentfiles.value.first (zPath); // If this file already is in the list, pop it out. if (lPos != -1) { if (~io_recentfiles.value == ~zPath) return; // only recent file - do nothing // Pop it out. str zFront = io_recentfiles.value.substr (0, lPos); str zBack = io_recentfiles.value.substr (lPos + ~zPath + 1, -1); io_recentfiles.value = zFront + zBack; } // If there's too many recent files, drop one out. while (io_recentfiles.value.count ('@') > 3) io_recentfiles.value = io_recentfiles.value.substr (io_recentfiles.value.first ("@") + 1, -1); // Add the file if (~io_recentfiles.value > 0) io_recentfiles.value += "@"; io_recentfiles += zPath; config::save (); g_ForgeWindow->updateRecentFilesMenu (); } // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= void openMainFile (str zPath) { closeAll (); OpenFile* pFile = openDATFile (zPath); if (!pFile) return; g_CurrentFile = pFile; // Recalculate the bounding box g_BBox.calculate(); // Rebuild the object tree view now. g_ForgeWindow->refresh (); g_ForgeWindow->setTitle (); // Add it to the recent files list. addRecentFile (zPath); } // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= bool OpenFile::save (str zPath) { if (!~zPath) zPath = zFileName; FILE* fp = fopen (zPath, "w"); if (!fp) return false; // Write all entries now for (LDObject* obj : objects) { // LDraw requires lines to have DOS line endings str zLine = str::mkfmt ("%s\r\n", obj->getContents ().chars ()); fwrite (zLine.chars(), 1, ~zLine, fp); } fclose (fp); return true; } #define CHECK_TOKEN_COUNT(N) \ if (tokens.size() != N) \ return new LDGibberish (zLine, "Bad amount of tokens"); #define CHECK_TOKEN_NUMBERS(MIN,MAX) \ for (ushort i = MIN; i <= MAX; ++i) \ if (!isNumber (tokens[i])) \ return new LDGibberish (zLine, str::mkfmt ("Token #%u was `%s`, expected a number", \ (i + 1), tokens[i].chars())); // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= static vertex parseVertex (vector<str>& s, const ushort n) { // Disable the locale while parsing the line or atof's behavior changes // between locales (i.e. fails to read decimals properly). That is // quite undesired... setlocale (LC_NUMERIC, "C"); vertex v; v.x = atof (s[n]); v.y = atof (s[n + 1]); v.z = atof (s[n + 2]); return v; } // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= LDObject* parseLine (str zLine) { vector<str> tokens = zLine.split (" ", true); if (!tokens.size ()) { // Line was empty, or only consisted of whitespace return new LDEmpty; } if (~tokens[0] != 1) return new LDGibberish (zLine, "Illogical line code"); const char c = tokens[0][0]; switch (c - '0') { case 0: { // Comment str zComment; for (uint i = 1; i < tokens.size(); ++i) { zComment += tokens[i]; if (i != tokens.size() - 1) zComment += ' '; } // Handle BFC statements if (tokens.size() > 2 && tokens[1] == "BFC") { for (short i = 0; i < NUM_BFCStatements; ++i) if (zComment == str::mkfmt ("BFC %s", LDBFC::saStatements [i])) return new LDBFC (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 separately. if (zComment == "BFC CERTIFY INVERTNEXT") return new LDBFC (BFC_InvertNext); } if (tokens.size() > 2 && tokens[1] == "!LDFORGE") { // Handle LDForge-specific types, they're embedded into comments if (tokens[2] == "VERTEX") { // Vertex (0 !LDFORGE VERTEX) CHECK_TOKEN_COUNT (7) CHECK_TOKEN_NUMBERS (3, 6) LDVertex* obj = new LDVertex; obj->dColor = atol (tokens[3]); obj->vPosition.x = atof (tokens[4]); obj->vPosition.y = atof (tokens[5]); obj->vPosition.z = atof (tokens[6]); return obj; } } LDComment* obj = new LDComment; obj->zText = zComment; return obj; } case 1: { // Subfile CHECK_TOKEN_COUNT (15) CHECK_TOKEN_NUMBERS (1, 13) // Try open the file OpenFile* pFile = loadSubfile (tokens[14]); // If we cannot open the file, mark it an error if (!pFile) return new LDGibberish (zLine, "Could not open referred file"); LDSubfile* obj = new LDSubfile; obj->dColor = atol (tokens[1]); obj->vPosition = parseVertex (tokens, 2); // 2 - 4 for (short i = 0; i < 9; ++i) obj->mMatrix[i] = atof (tokens[i + 5]); // 5 - 13 obj->zFileName = tokens[14]; obj->pFile = pFile; return obj; } case 2: { CHECK_TOKEN_COUNT (8) CHECK_TOKEN_NUMBERS (1, 7) // Line LDLine* obj = new LDLine; obj->dColor = atol (tokens[1]); for (short i = 0; i < 2; ++i) obj->vaCoords[i] = parseVertex (tokens, 2 + (i * 3)); // 2 - 7 return obj; } case 3: { CHECK_TOKEN_COUNT (11) CHECK_TOKEN_NUMBERS (1, 10) // Triangle LDTriangle* obj = new LDTriangle; obj->dColor = atol (tokens[1]); for (short i = 0; i < 3; ++i) obj->vaCoords[i] = parseVertex (tokens, 2 + (i * 3)); // 2 - 10 return obj; } case 4: { CHECK_TOKEN_COUNT (14) CHECK_TOKEN_NUMBERS (1, 13) // Quadrilateral LDQuad* obj = new LDQuad; obj->dColor = atol (tokens[1]); for (short i = 0; i < 4; ++i) obj->vaCoords[i] = parseVertex (tokens, 2 + (i * 3)); // 2 - 13 return obj; } case 5: { CHECK_TOKEN_COUNT (14) CHECK_TOKEN_NUMBERS (1, 13) // Conditional line LDCondLine* obj = new LDCondLine; obj->dColor = atol (tokens[1]); for (short i = 0; i < 4; ++i) obj->vaCoords[i] = parseVertex (tokens, 2 + (i * 3)); // 2 - 13 return obj; } default: // Strange line we couldn't parse return new LDGibberish (zLine, "Unknown line code number"); } } // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= OpenFile* loadSubfile (str zFile) { // Try open the file OpenFile* pFile = findLoadedFile (zFile); if (!pFile) pFile = openDATFile (zFile); return pFile; } // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= void reloadAllSubfiles () { if (!g_CurrentFile) return; // First, close all but the current open file. for (OpenFile* file : g_LoadedFiles) if (file != g_CurrentFile) file->close (); g_LoadedFiles.clear (); g_LoadedFiles.push_back (g_CurrentFile); // Go through all objects in the current file and reload the subfiles for (LDObject* obj : g_CurrentFile->objects) { if (obj->getType() == OBJ_Subfile) { // Note: ref->pFile is invalid right now since all subfiles were closed. LDSubfile* ref = static_cast<LDSubfile*> (obj); OpenFile* pFile = loadSubfile (ref->zFileName); if (pFile) ref->pFile = pFile; else { // Couldn't load the file, mark it an error ref->replace (new LDGibberish (ref->getContents (), "Could not open referred file")); } } // Reparse gibberish files. It could be that they are invalid because // the file could not be opened. Circumstances may be different now. if (obj->getType() == OBJ_Gibberish) obj->replace (parseLine (static_cast<LDGibberish*> (obj)->zContents)); } } // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= ulong OpenFile::addObject (LDObject* obj) { if (this != g_CurrentFile) { objects.insert (objects.end (), obj); return objects.size() - 1; } const ulong ulSpot = g_ForgeWindow->getInsertionPoint (); objects.insert (objects.begin() + ulSpot, obj); return ulSpot; } // ============================================================================= // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // ============================================================================= void OpenFile::forgetObject (LDObject* obj) { // Find the index for the given object ulong ulIndex; for (ulIndex = 0; ulIndex < (ulong)objects.size(); ++ulIndex) if (objects[ulIndex] == obj) break; // found it if (ulIndex >= objects.size ()) return; // was not found // Erase it from memory objects.erase (objects.begin() + ulIndex); }