src/ldDocument.cc

changeset 952
f116b63c4844
parent 950
5df69eb50182
child 953
8349552ee5e9
equal deleted inserted replaced
950:5df69eb50182 952:f116b63c4844
1 /*
2 * LDForge: LDraw parts authoring CAD
3 * Copyright (C) 2013, 2014 Teemu Piippo
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include <QMessageBox>
20 #include <QFileDialog>
21 #include <QDir>
22 #include <QTime>
23 #include <QApplication>
24
25 #include "main.h"
26 #include "configuration.h"
27 #include "ldDocument.h"
28 #include "miscallenous.h"
29 #include "mainWindow.h"
30 #include "editHistory.h"
31 #include "dialogs.h"
32 #include "glRenderer.h"
33 #include "glCompiler.h"
34 #include "partDownloader.h"
35
36 CFGENTRY (String, LDrawPath, "")
37 CFGENTRY (List, RecentFiles, {})
38 CFGENTRY (Bool, TryDownloadMissingFiles, false)
39 EXTERN_CFGENTRY (String, DownloadFilePath)
40 EXTERN_CFGENTRY (Bool, UseLogoStuds)
41
42 static bool g_loadingMainFile = false;
43 static const int g_maxRecentFiles = 10;
44 static bool g_aborted = false;
45 static LDDocument* g_logoedStud;
46 static LDDocument* g_logoedStud2;
47 static QList<LDDocument*> g_allDocuments;
48 static QList<LDDocument*> g_explicitDocuments;
49 static LDDocument* g_currentDocument;
50 static bool g_loadingLogoedStuds = false;
51
52 const QStringList g_specialSubdirectories ({ "s", "48", "8" });
53
54 // =============================================================================
55 //
56 namespace LDPaths
57 {
58 static QString pathError;
59
60 struct
61 {
62 QString LDConfigPath;
63 QString partsPath, primsPath;
64 } pathInfo;
65
66 void initPaths()
67 {
68 if (not tryConfigure (cfg::LDrawPath))
69 {
70 LDrawPathDialog dlg (false);
71
72 if (not dlg.exec())
73 Exit();
74
75 cfg::LDrawPath = dlg.filename();
76 }
77 }
78
79 bool tryConfigure (QString path)
80 {
81 QDir dir;
82
83 if (not dir.cd (path))
84 {
85 pathError = "Directory does not exist.";
86 return false;
87 }
88
89 QStringList mustHave = { "LDConfig.ldr", "parts", "p" };
90 QStringList contents = dir.entryList (mustHave);
91
92 if (contents.size() != mustHave.size())
93 {
94 pathError = "Not an LDraw directory! Must<br />have LDConfig.ldr, parts/ and p/.";
95 return false;
96 }
97
98 pathInfo.partsPath = format ("%1" DIRSLASH "parts", path);
99 pathInfo.LDConfigPath = format ("%1" DIRSLASH "LDConfig.ldr", path);
100 pathInfo.primsPath = format ("%1" DIRSLASH "p", path);
101
102 return true;
103 }
104
105 // Accessors
106 QString getError()
107 {
108 return pathError;
109 }
110
111 QString ldconfig()
112 {
113 return pathInfo.LDConfigPath;
114 }
115
116 QString prims()
117 {
118 return pathInfo.primsPath;
119 }
120
121 QString parts()
122 {
123 return pathInfo.partsPath;
124 }
125 }
126
127 // =============================================================================
128 //
129 LDDocument::LDDocument() :
130 m_isImplicit (true),
131 m_flags (0),
132 m_verticesOutdated (true),
133 m_needVertexMerge (true),
134 m_gldata (new LDGLData)
135 {
136 setSavePosition (-1);
137 setTabIndex (-1);
138 m_history = new History;
139 m_history->setDocument (this);
140 m_needsReCache = true;
141 g_allDocuments << this;
142 }
143
144 // =============================================================================
145 //
146 LDDocument* LDDocument::createNew()
147 {
148 return new LDDocument();
149 }
150
151 // =============================================================================
152 //
153 LDDocument::~LDDocument()
154 {
155 g_allDocuments.removeOne (this);
156 m_flags |= DOCF_IsBeingDestroyed;
157 delete m_history;
158 delete m_gldata;
159 }
160
161 // =============================================================================
162 //
163 void LDDocument::setImplicit (bool const& a)
164 {
165 if (m_isImplicit != a)
166 {
167 m_isImplicit = a;
168
169 if (a == false)
170 {
171 g_explicitDocuments << this;
172 print ("Opened %1", name());
173
174 // Implicit files are not compiled by the GL renderer. Now that this
175 // part is no longer implicit, it needs to be compiled.
176 if (g_win != null)
177 g_win->R()->compiler()->compileDocument (this);
178 }
179 else
180 {
181 g_explicitDocuments.removeOne (this);
182 print ("Closed %1", name());
183 }
184
185 if (g_win != null)
186 g_win->updateDocumentList();
187
188 // If the current document just became implicit (e.g. it was 'closed'),
189 // we need to get a new current document.
190 if (current() == this and isImplicit())
191 {
192 if (explicitDocuments().isEmpty())
193 newFile();
194 else
195 setCurrent (explicitDocuments().first());
196 }
197 }
198 }
199
200 // =============================================================================
201 //
202 QList<LDDocument*> const& LDDocument::explicitDocuments()
203 {
204 return g_explicitDocuments;
205 }
206
207 // =============================================================================
208 //
209 LDDocument* FindDocument (QString name)
210 {
211 for (LDDocument* file : g_allDocuments)
212 {
213 if (file == null)
214 continue;
215
216 if (Eq (name, file->name(), file->defaultName()))
217 return file;
218 }
219
220 return nullptr;
221 }
222
223 // =============================================================================
224 //
225 QString Dirname (QString path)
226 {
227 long lastpos = path.lastIndexOf (DIRSLASH);
228
229 if (lastpos > 0)
230 return path.left (lastpos);
231
232 #ifndef _WIN32
233 if (path[0] == DIRSLASH_CHAR)
234 return DIRSLASH;
235 #endif // _WIN32
236
237 return "";
238 }
239
240 // =============================================================================
241 //
242 QString Basename (QString path)
243 {
244 long lastpos = path.lastIndexOf (DIRSLASH);
245
246 if (lastpos != -1)
247 return path.mid (lastpos + 1);
248
249 return path;
250 }
251
252 // =============================================================================
253 //
254 static QString FindDocumentPath (QString relpath, bool subdirs)
255 {
256 QString fullPath;
257
258 // LDraw models use Windows-style path separators. If we're not on Windows,
259 // replace the path separator now before opening any files. Qt expects
260 // forward-slashes as directory separators.
261 #ifndef WIN32
262 relpath.replace ("\\", "/");
263 #endif // WIN32
264
265 // Try find it relative to other currently open documents. We want a file
266 // in the immediate vicinity of a current model to override stock LDraw stuff.
267 QString reltop = Basename (Dirname (relpath));
268
269 for (LDDocument* doc : g_allDocuments)
270 {
271 if (doc == null)
272 continue;
273
274 QString partpath = format ("%1/%2", Dirname (doc->fullPath()), relpath);
275 QFile f (partpath);
276
277 if (f.exists())
278 {
279 // ensure we don't mix subfiles and 48-primitives with non-subfiles and non-48
280 QString proptop = Basename (Dirname (partpath));
281
282 bool bogus = false;
283
284 for (QString s : g_specialSubdirectories)
285 {
286 if ((proptop == s and reltop != s) or (reltop == s and proptop != s))
287 {
288 bogus = true;
289 break;
290 }
291 }
292
293 if (not bogus)
294 return partpath;
295 }
296 }
297
298 if (QFile::exists (relpath))
299 return relpath;
300
301 // Try with just the LDraw path first
302 fullPath = format ("%1" DIRSLASH "%2", cfg::LDrawPath, relpath);
303
304 if (QFile::exists (fullPath))
305 return fullPath;
306
307 if (subdirs)
308 {
309 // Look in sub-directories: parts and p. Also look in net_downloadpath, since that's
310 // where we download parts from the PT to.
311 for (const QString& topdir : QList<QString> ({ cfg::LDrawPath, cfg::DownloadFilePath }))
312 {
313 for (const QString& subdir : QList<QString> ({ "parts", "p" }))
314 {
315 fullPath = format ("%1" DIRSLASH "%2" DIRSLASH "%3", topdir, subdir, relpath);
316
317 if (QFile::exists (fullPath))
318 return fullPath;
319 }
320 }
321 }
322
323 // Did not find the file.
324 return "";
325 }
326
327 // =============================================================================
328 //
329 QFile* OpenLDrawFile (QString relpath, bool subdirs, QString* pathpointer)
330 {
331 print ("Opening %1...\n", relpath);
332 QString path = FindDocumentPath (relpath, subdirs);
333
334 if (pathpointer != null)
335 *pathpointer = path;
336
337 if (path.isEmpty())
338 return null;
339
340 QFile* fp = new QFile (path);
341
342 if (fp->open (QIODevice::ReadOnly))
343 return fp;
344
345 fp->deleteLater();
346 return null;
347 }
348
349 // =============================================================================
350 //
351 void LDFileLoader::start()
352 {
353 setDone (false);
354 setProgress (0);
355 setAborted (false);
356
357 if (isOnForeground())
358 {
359 g_aborted = false;
360
361 // Show a progress dialog if we're loading the main ldDocument.here so we can
362 // show progress updates and keep the WM posted that we're still here.
363 // Of course we cannot exec() the dialog because then the dialog would
364 // block.
365 dlg = new OpenProgressDialog (g_win);
366 dlg->setNumLines (lines().size());
367 dlg->setModal (true);
368 dlg->show();
369
370 // Connect the loader in so we can show updates
371 connect (this, SIGNAL (workDone()), dlg, SLOT (accept()));
372 connect (dlg, SIGNAL (rejected()), this, SLOT (abort()));
373 }
374 else
375 dlg = null;
376
377 // Begin working
378 work (0);
379 }
380
381 // =============================================================================
382 //
383 void LDFileLoader::work (int i)
384 {
385 // User wishes to abort, so stop here now.
386 if (isAborted())
387 {
388 for (LDObject* obj : m_objects)
389 obj->destroy();
390
391 m_objects.clear();
392 setDone (true);
393 return;
394 }
395
396 // Parse up to 300 lines per iteration
397 int max = i + 300;
398
399 for (; i < max and i < (int) lines().size(); ++i)
400 {
401 QString line = lines()[i];
402
403 // Trim the trailing newline
404 QChar c;
405
406 while (line.endsWith ("\n") or line.endsWith ("\r"))
407 line.chop (1);
408
409 LDObject* obj = ParseLine (line);
410
411 // Check for parse errors and warn about tthem
412 if (obj->type() == OBJ_Error)
413 {
414 print ("Couldn't parse line #%1: %2",
415 progress() + 1, static_cast<LDError*> (obj)->reason());
416
417 if (warnings() != null)
418 (*warnings())++;
419 }
420
421 m_objects << obj;
422 setProgress (i);
423
424 // If we have a dialog pointer, update the progress now
425 if (isOnForeground())
426 dlg->updateProgress (i);
427 }
428
429 // If we're done now, tell the environment we're done and stop.
430 if (i >= ((int) lines().size()) - 1)
431 {
432 emit workDone();
433 setDone (true);
434 return;
435 }
436
437 // Otherwise, continue, by recursing back.
438 if (not isDone())
439 {
440 // If we have a dialog to show progress output to, we cannot just call
441 // work() again immediately as the dialog needs some processor cycles as
442 // well. Thus, take a detour through the event loop by using the
443 // meta-object system.
444 //
445 // This terminates the loop here and control goes back to the function
446 // which called the file loader. It will keep processing the event loop
447 // until we're ready (see loadFileContents), thus the event loop will
448 // eventually catch the invokation we throw here and send us back. Though
449 // it's not technically recursion anymore, more like a for loop. :P
450 if (isOnForeground())
451 QMetaObject::invokeMethod (this, "work", Qt::QueuedConnection, Q_ARG (int, i));
452 else
453 work (i);
454 }
455 }
456
457 // =============================================================================
458 //
459 void LDFileLoader::abort()
460 {
461 setAborted (true);
462
463 if (isOnForeground())
464 g_aborted = true;
465 }
466
467 // =============================================================================
468 //
469 LDObjectList LoadFileContents (QFile* fp, int* numWarnings, bool* ok)
470 {
471 QStringList lines;
472 LDObjectList objs;
473
474 if (numWarnings)
475 *numWarnings = 0;
476
477 // Read in the lines
478 while (not fp->atEnd())
479 lines << QString::fromUtf8 (fp->readLine());
480
481 LDFileLoader* loader = new LDFileLoader;
482 loader->setWarnings (numWarnings);
483 loader->setLines (lines);
484 loader->setOnForeground (g_loadingMainFile);
485 loader->start();
486
487 // After start() returns, if the loader isn't done yet, it's delaying
488 // its next iteration through the event loop. We need to catch this here
489 // by telling the event loop to tick, which will tick the file loader again.
490 // We keep doing this until the file loader is ready.
491 while (not loader->isDone())
492 qApp->processEvents();
493
494 // If we wanted the success value, supply that now
495 if (ok)
496 *ok = not loader->isAborted();
497
498 objs = loader->objects();
499 delete loader;
500 return objs;
501 }
502
503 // =============================================================================
504 //
505 LDDocument* OpenDocument (QString path, bool search, bool implicit, LDDocument* fileToOverride)
506 {
507 // Convert the file name to lowercase when searching because some parts contain subfile
508 // subfile references with uppercase file names. I'll assume here that the library will always
509 // use lowercase file names for the part files.
510 QFile* fp;
511 QString fullpath;
512
513 if (search)
514 {
515 fp = OpenLDrawFile (path.toLower(), true, &fullpath);
516 }
517 else
518 {
519 fp = new QFile (path);
520 fullpath = path;
521
522 if (not fp->open (QIODevice::ReadOnly))
523 {
524 delete fp;
525 return nullptr;
526 }
527 }
528
529 if (not fp)
530 return nullptr;
531
532 LDDocument* load = (fileToOverride != null ? fileToOverride : LDDocument::createNew());
533 load->setImplicit (implicit);
534 load->setFullPath (fullpath);
535 load->setName (LDDocument::shortenName (load->fullPath()));
536
537 // Loading the file shouldn't count as actual edits to the document.
538 load->history()->setIgnoring (true);
539
540 int numWarnings;
541 bool ok;
542 LDObjectList objs = LoadFileContents (fp, &numWarnings, &ok);
543 fp->close();
544 fp->deleteLater();
545
546 if (not ok)
547 {
548 load->dismiss();
549 return nullptr;
550 }
551
552 load->addObjects (objs);
553
554 if (g_loadingMainFile)
555 {
556 LDDocument::setCurrent (load);
557 g_win->R()->setDocument (load);
558 print (QObject::tr ("File %1 parsed successfully (%2 errors)."), path, numWarnings);
559 }
560
561 load->history()->setIgnoring (false);
562 return load;
563 }
564
565 // =============================================================================
566 //
567 bool LDDocument::isSafeToClose()
568 {
569 using msgbox = QMessageBox;
570 setlocale (LC_ALL, "C");
571
572 // If we have unsaved changes, warn and give the option of saving.
573 if (hasUnsavedChanges())
574 {
575 QString message = format (QObject::tr ("There are unsaved changes to %1. Should it be saved?"),
576 getDisplayName());
577
578 int button = msgbox::question (g_win, QObject::tr ("Unsaved Changes"), message,
579 (msgbox::Yes | msgbox::No | msgbox::Cancel), msgbox::Cancel);
580
581 switch (button)
582 {
583 case msgbox::Yes:
584 {
585 // If we don't have a file path yet, we have to ask the user for one.
586 if (name().length() == 0)
587 {
588 QString newpath = QFileDialog::getSaveFileName (g_win, QObject::tr ("Save As"),
589 CurrentDocument()->name(), QObject::tr ("LDraw files (*.dat *.ldr)"));
590
591 if (newpath.length() == 0)
592 return false;
593
594 setName (newpath);
595 }
596
597 if (not save())
598 {
599 message = format (QObject::tr ("Failed to save %1 (%2)\nDo you still want to close?"),
600 name(), strerror (errno));
601
602 if (msgbox::critical (g_win, QObject::tr ("Save Failure"), message,
603 (msgbox::Yes | msgbox::No), msgbox::No) == msgbox::No)
604 {
605 return false;
606 }
607 }
608 break;
609 }
610
611 case msgbox::Cancel:
612 return false;
613
614 default:
615 break;
616 }
617 }
618
619 return true;
620 }
621
622 // =============================================================================
623 //
624 void CloseAllDocuments()
625 {
626 for (LDDocument* file : g_explicitDocuments)
627 file->dismiss();
628 }
629
630 // =============================================================================
631 //
632 void newFile()
633 {
634 // Create a new anonymous file and set it to our current
635 LDDocument* f = LDDocument::createNew();
636 f->setName ("");
637 f->setImplicit (false);
638 LDDocument::setCurrent (f);
639 LDDocument::closeInitialFile();
640 g_win->R()->setDocument (f);
641 g_win->doFullRefresh();
642 g_win->updateTitle();
643 g_win->updateActions();
644 }
645
646 // =============================================================================
647 //
648 void AddRecentFile (QString path)
649 {
650 int idx = cfg::RecentFiles.indexOf (path);
651
652 // If this file already is in the list, pop it out.
653 if (idx != -1)
654 {
655 if (idx == cfg::RecentFiles.size() - 1)
656 return; // first recent file - abort and do nothing
657
658 cfg::RecentFiles.removeAt (idx);
659 }
660
661 // If there's too many recent files, drop one out.
662 while (cfg::RecentFiles.size() > (g_maxRecentFiles - 1))
663 cfg::RecentFiles.removeAt (0);
664
665 // Add the file
666 cfg::RecentFiles << path;
667
668 Config::Save();
669 g_win->updateRecentFilesMenu();
670 }
671
672 // =============================================================================
673 // Open an LDraw file and set it as the main model
674 // =============================================================================
675 void OpenMainModel (QString path)
676 {
677 // If there's already a file with the same name, this file must replace it.
678 LDDocument* documentToReplace = nullptr;
679 LDDocument* file = nullptr;
680 QString shortName = LDDocument::shortenName (path);
681
682 for (LDDocument* doc : g_allDocuments)
683 {
684 if (doc != null and doc->name() == shortName)
685 {
686 documentToReplace = doc;
687 break;
688 }
689 }
690
691 // We cannot open this file if the document this would replace is not
692 // safe to close.
693 if (documentToReplace != null and not documentToReplace->isSafeToClose())
694 return;
695
696 g_loadingMainFile = true;
697
698 // If we're replacing an existing document, clear the document and
699 // make it ready for being loaded to.
700 if (documentToReplace != null)
701 {
702 file = documentToReplace;
703 file->clear();
704 }
705
706 file = OpenDocument (path, false, false, file);
707
708 if (file == null)
709 {
710 if (not g_aborted)
711 {
712 // Tell the user loading failed.
713 setlocale (LC_ALL, "C");
714 Critical (format (QObject::tr ("Failed to open %1: %2"), path, strerror (errno)));
715 }
716
717 g_loadingMainFile = false;
718 return;
719 }
720
721 file->setImplicit (false);
722
723 // If we have an anonymous, unchanged file open as the only open file
724 // (aside of the one we just opened), close it now.
725 LDDocument::closeInitialFile();
726
727 // Rebuild the object tree view now.
728 LDDocument::setCurrent (file);
729 g_win->doFullRefresh();
730
731 // Add it to the recent files list.
732 AddRecentFile (path);
733 g_loadingMainFile = false;
734
735 // If there were problems loading subfile references, try see if we can find these
736 // files on the parts tracker.
737 QStringList unknowns;
738
739 for (LDObject* obj : file->objects())
740 {
741 if (obj->type() != OBJ_Error or static_cast<LDError*> (obj)->fileReferenced().isEmpty())
742 continue;
743
744 unknowns << static_cast<LDError*> (obj)->fileReferenced();
745 }
746
747 if (cfg::TryDownloadMissingFiles and not unknowns.isEmpty())
748 {
749 PartDownloader dl;
750
751 if (dl.checkValidPath())
752 {
753 dl.setSource (PartDownloader::PartsTracker);
754 dl.setPrimaryFile (file);
755
756 for (QString const& unknown : unknowns)
757 dl.downloadFromPartsTracker (unknown);
758
759 dl.exec();
760 dl.checkIfFinished();
761 file->reloadAllSubfiles();
762 }
763 }
764 }
765
766 // =============================================================================
767 //
768 bool LDDocument::save (QString path, int64* sizeptr)
769 {
770 if (isImplicit())
771 return false;
772
773 if (not path.length())
774 path = fullPath();
775
776 // If the second object in the list holds the file name, update that now.
777 LDObject* nameObject = getObject (1);
778
779 if (nameObject != null and nameObject->type() == OBJ_Comment)
780 {
781 LDComment* nameComment = static_cast<LDComment*> (nameObject);
782
783 if (nameComment->text().left (6) == "Name: ")
784 {
785 QString newname = shortenName (path);
786 nameComment->setText (format ("Name: %1", newname));
787 g_win->buildObjList();
788 }
789 }
790
791 QByteArray data;
792
793 if (sizeptr != null)
794 *sizeptr = 0;
795
796 // File is open, now save the model to it. Note that LDraw requires files to have DOS line endings.
797 for (LDObject* obj : objects())
798 {
799 QByteArray subdata ((obj->asText() + "\r\n").toUtf8());
800 data.append (subdata);
801
802 if (sizeptr != null)
803 *sizeptr += subdata.size();
804 }
805
806 QFile f (path);
807
808 if (not f.open (QIODevice::WriteOnly))
809 return false;
810
811 f.write (data);
812 f.close();
813
814 // We have successfully saved, update the save position now.
815 setSavePosition (history()->position());
816 setFullPath (path);
817 setName (shortenName (path));
818
819 g_win->updateDocumentListItem (this);
820 g_win->updateTitle();
821 return true;
822 }
823
824 // =============================================================================
825 //
826 void LDDocument::clear()
827 {
828 for (LDObject* obj : objects())
829 forgetObject (obj);
830 }
831
832 // =============================================================================
833 //
834 static void CheckTokenCount (const QStringList& tokens, int num)
835 {
836 if (tokens.size() != num)
837 throw QString (format ("Bad amount of tokens, expected %1, got %2", num, tokens.size()));
838 }
839
840 // =============================================================================
841 //
842 static void CheckTokenNumbers (const QStringList& tokens, int min, int max)
843 {
844 bool ok;
845
846 QRegExp scient ("\\-?[0-9]+\\.[0-9]+e\\-[0-9]+");
847
848 for (int i = min; i <= max; ++i)
849 {
850 // Check for floating point
851 tokens[i].toDouble (&ok);
852 if (ok)
853 return;
854
855 // Check hex
856 if (tokens[i].startsWith ("0x"))
857 {
858 tokens[i].mid (2).toInt (&ok, 16);
859
860 if (ok)
861 return;
862 }
863
864 // Check scientific notation, e.g. 7.99361e-15
865 if (scient.exactMatch (tokens[i]))
866 return;
867
868 throw QString (format ("Token #%1 was `%2`, expected a number (matched length: %3)",
869 (i + 1), tokens[i], scient.matchedLength()));
870 }
871 }
872
873 // =============================================================================
874 //
875 static Vertex ParseVertex (QStringList& s, const int n)
876 {
877 Vertex v;
878 v.apply ([&] (Axis ax, double& a) { a = s[n + ax].toDouble(); });
879 return v;
880 }
881
882 static int32 StringToNumber (QString a, bool* ok = null)
883 {
884 int base = 10;
885
886 if (a.startsWith ("0x"))
887 {
888 a.remove (0, 2);
889 base = 16;
890 }
891
892 return a.toLong (ok, base);
893 }
894
895 // =============================================================================
896 // This is the LDraw code parser function. It takes in a string containing LDraw
897 // code and returns the object parsed from it. parseLine never returns null,
898 // the object will be LDError if it could not be parsed properly.
899 // =============================================================================
900 LDObject* ParseLine (QString line)
901 {
902 try
903 {
904 QStringList tokens = line.split (" ", QString::SkipEmptyParts);
905
906 if (tokens.size() <= 0)
907 {
908 // Line was empty, or only consisted of whitespace
909 return LDSpawn<LDEmpty>();
910 }
911
912 if (tokens[0].length() != 1 or not tokens[0][0].isDigit())
913 throw QString ("Illogical line code");
914
915 int num = tokens[0][0].digitValue();
916
917 switch (num)
918 {
919 case 0:
920 {
921 // Comment
922 QString commentText (line.mid (line.indexOf ("0") + 2));
923 QString commentTextSimplified (commentText.simplified());
924
925 // Handle BFC statements
926 if (tokens.size() > 2 and tokens[1] == "BFC")
927 {
928 for_enum (BFCStatement, i)
929 {
930 if (commentTextSimplified == format ("BFC %1",
931 LDBFC::StatementStrings[int (i)]))
932 {
933 return LDSpawn<LDBFC> (i);
934 }
935 }
936
937 // MLCAD is notorious for stuffing these statements in parts it
938 // creates. The above block only handles valid statements, so we
939 // need to handle MLCAD-style invertnext, clip and noclip separately.
940 if (commentTextSimplified == "BFC CERTIFY INVERTNEXT")
941 return LDSpawn<LDBFC> (BFCStatement::InvertNext);
942 elif (commentTextSimplified == "BFC CERTIFY CLIP")
943 return LDSpawn<LDBFC> (BFCStatement::Clip);
944 elif (commentTextSimplified == "BFC CERTIFY NOCLIP")
945 return LDSpawn<LDBFC> (BFCStatement::NoClip);
946 }
947
948 if (tokens.size() > 2 and tokens[1] == "!LDFORGE")
949 {
950 // Handle LDForge-specific types, they're embedded into comments too
951 if (tokens[2] == "VERTEX")
952 {
953 // Vertex (0 !LDFORGE VERTEX)
954 CheckTokenCount (tokens, 7);
955 CheckTokenNumbers (tokens, 3, 6);
956
957 LDVertex* obj = LDSpawn<LDVertex>();
958 obj->setColor (StringToNumber (tokens[3]));
959 obj->pos.apply ([&](Axis ax, double& value)
960 { value = tokens[4 + ax].toDouble(); });
961 return obj;
962 }
963 elif (tokens[2] == "OVERLAY")
964 {
965 CheckTokenCount (tokens, 9);
966 CheckTokenNumbers (tokens, 5, 8);
967
968 LDOverlay* obj = LDSpawn<LDOverlay>();
969 obj->setFileName (tokens[3]);
970 obj->setCamera (tokens[4].toLong());
971 obj->setX (tokens[5].toLong());
972 obj->setY (tokens[6].toLong());
973 obj->setWidth (tokens[7].toLong());
974 obj->setHeight (tokens[8].toLong());
975 return obj;
976 }
977 }
978
979 // Just a regular comment:
980 LDComment* obj = LDSpawn<LDComment>();
981 obj->setText (commentText);
982 return obj;
983 }
984
985 case 1:
986 {
987 // Subfile
988 CheckTokenCount (tokens, 15);
989 CheckTokenNumbers (tokens, 1, 13);
990
991 // Try open the file. Disable g_loadingMainFile temporarily since we're
992 // not loading the main file now, but the subfile in question.
993 bool tmp = g_loadingMainFile;
994 g_loadingMainFile = false;
995 LDDocument* load = GetDocument (tokens[14]);
996 g_loadingMainFile = tmp;
997
998 // If we cannot open the file, mark it an error. Note we cannot use LDParseError
999 // here because the error object needs the document reference.
1000 if (not load)
1001 {
1002 LDError* obj = LDSpawn<LDError> (line, format ("Could not open %1", tokens[14]));
1003 obj->setFileReferenced (tokens[14]);
1004 return obj;
1005 }
1006
1007 LDSubfile* obj = LDSpawn<LDSubfile>();
1008 obj->setColor (StringToNumber (tokens[1]));
1009 obj->setPosition (ParseVertex (tokens, 2)); // 2 - 4
1010
1011 Matrix transform;
1012
1013 for (int i = 0; i < 9; ++i)
1014 transform[i] = tokens[i + 5].toDouble(); // 5 - 13
1015
1016 obj->setTransform (transform);
1017 obj->setFileInfo (load);
1018 return obj;
1019 }
1020
1021 case 2:
1022 {
1023 CheckTokenCount (tokens, 8);
1024 CheckTokenNumbers (tokens, 1, 7);
1025
1026 // Line
1027 LDLine* obj (LDSpawn<LDLine>());
1028 obj->setColor (StringToNumber (tokens[1]));
1029
1030 for (int i = 0; i < 2; ++i)
1031 obj->setVertex (i, ParseVertex (tokens, 2 + (i * 3))); // 2 - 7
1032
1033 return obj;
1034 }
1035
1036 case 3:
1037 {
1038 CheckTokenCount (tokens, 11);
1039 CheckTokenNumbers (tokens, 1, 10);
1040
1041 // Triangle
1042 LDTriangle* obj (LDSpawn<LDTriangle>());
1043 obj->setColor (StringToNumber (tokens[1]));
1044
1045 for (int i = 0; i < 3; ++i)
1046 obj->setVertex (i, ParseVertex (tokens, 2 + (i * 3))); // 2 - 10
1047
1048 return obj;
1049 }
1050
1051 case 4:
1052 case 5:
1053 {
1054 CheckTokenCount (tokens, 14);
1055 CheckTokenNumbers (tokens, 1, 13);
1056
1057 // Quadrilateral / Conditional line
1058 LDObject* obj;
1059
1060 if (num == 4)
1061 obj = LDSpawn<LDQuad>();
1062 else
1063 obj = LDSpawn<LDCondLine>();
1064
1065 obj->setColor (StringToNumber (tokens[1]));
1066
1067 for (int i = 0; i < 4; ++i)
1068 obj->setVertex (i, ParseVertex (tokens, 2 + (i * 3))); // 2 - 13
1069
1070 return obj;
1071 }
1072
1073 default:
1074 throw QString ("Unknown line code number");
1075 }
1076 }
1077 catch (QString& e)
1078 {
1079 // Strange line we couldn't parse
1080 return LDSpawn<LDError> (line, e);
1081 }
1082 }
1083
1084 // =============================================================================
1085 //
1086 LDDocument* GetDocument (QString filename)
1087 {
1088 // Try find the file in the list of loaded files
1089 LDDocument* doc = FindDocument (filename);
1090
1091 // If it's not loaded, try open it
1092 if (not doc)
1093 doc = OpenDocument (filename, true, true);
1094
1095 return doc;
1096 }
1097
1098 // =============================================================================
1099 //
1100 void LDDocument::reloadAllSubfiles()
1101 {
1102 print ("Reloading subfiles of %1", getDisplayName());
1103
1104 // Go through all objects in the current file and reload the subfiles
1105 for (LDObject* obj : objects())
1106 {
1107 if (obj->type() == OBJ_Subfile)
1108 {
1109 LDSubfile* ref = static_cast<LDSubfile*> (obj);
1110 LDDocument* fileInfo = GetDocument (ref->fileInfo()->name());
1111
1112 if (fileInfo != null)
1113 {
1114 ref->setFileInfo (fileInfo);
1115 }
1116 else
1117 {
1118 ref->replace (LDSpawn<LDError> (ref->asText(),
1119 format ("Could not open %1", ref->fileInfo()->name())));
1120 }
1121 }
1122
1123 // Reparse gibberish files. It could be that they are invalid because
1124 // of loading errors. Circumstances may be different now.
1125 if (obj->type() == OBJ_Error)
1126 obj->replace (ParseLine (static_cast<LDError*> (obj)->contents()));
1127 }
1128
1129 m_needsReCache = true;
1130
1131 if (this == CurrentDocument())
1132 g_win->buildObjList();
1133 }
1134
1135 // =============================================================================
1136 //
1137 int LDDocument::addObject (LDObject* obj)
1138 {
1139 history()->add (new AddHistory (objects().size(), obj));
1140 m_objects << obj;
1141 addKnownVertices (obj);
1142 obj->setDocument (this);
1143 g_win->R()->compileObject (obj);
1144 return getObjectCount() - 1;
1145 }
1146
1147 // =============================================================================
1148 //
1149 void LDDocument::addObjects (const LDObjectList& objs)
1150 {
1151 for (LDObject* obj : objs)
1152 {
1153 if (obj != null)
1154 addObject (obj);
1155 }
1156 }
1157
1158 // =============================================================================
1159 //
1160 void LDDocument::insertObj (int pos, LDObject* obj)
1161 {
1162 history()->add (new AddHistory (pos, obj));
1163 m_objects.insert (pos, obj);
1164 obj->setDocument (this);
1165 g_win->R()->compileObject (obj);
1166
1167
1168 #ifdef DEBUG
1169 if (not isImplicit())
1170 dprint ("Inserted object #%1 (%2) at %3\n", obj->id(), obj->typeName(), pos);
1171 #endif
1172 }
1173
1174 // =============================================================================
1175 //
1176 void LDDocument::addKnownVertices (LDObject* obj)
1177 {
1178 auto it = m_objectVertices.find (obj);
1179
1180 if (it == m_objectVertices.end())
1181 it = m_objectVertices.insert (obj, QVector<Vertex>());
1182 else
1183 it->clear();
1184
1185 obj->getVertices (*it);
1186 needVertexMerge();
1187 }
1188
1189 // =============================================================================
1190 //
1191 void LDDocument::forgetObject (LDObject* obj)
1192 {
1193 int idx = obj->lineNumber();
1194
1195 if (m_objects[idx] == obj)
1196 {
1197 obj->deselect();
1198
1199 if (not isImplicit() and not (flags() & DOCF_IsBeingDestroyed))
1200 {
1201 history()->add (new DelHistory (idx, obj));
1202 m_objectVertices.remove (obj);
1203 }
1204
1205 m_objects.removeAt (idx);
1206 obj->setDocument (nullptr);
1207 }
1208 }
1209
1210 // =============================================================================
1211 //
1212 bool IsSafeToCloseAll()
1213 {
1214 for (LDDocument* f : LDDocument::explicitDocuments())
1215 {
1216 if (not f->isSafeToClose())
1217 return false;
1218 }
1219
1220 return true;
1221 }
1222
1223 // =============================================================================
1224 //
1225 void LDDocument::setObject (int idx, LDObject* obj)
1226 {
1227 assert (idx >= 0 and idx < m_objects.size());
1228
1229 // Mark this change to history
1230 if (not m_history->isIgnoring())
1231 {
1232 QString oldcode = getObject (idx)->asText();
1233 QString newcode = obj->asText();
1234 *m_history << new EditHistory (idx, oldcode, newcode);
1235 }
1236
1237 m_objectVertices.remove (m_objects[idx]);
1238 m_objects[idx]->deselect();
1239 m_objects[idx]->setDocument (nullptr);
1240 obj->setDocument (this);
1241 addKnownVertices (obj);
1242 g_win->R()->compileObject (obj);
1243 m_objects[idx] = obj;
1244 needVertexMerge();
1245 }
1246
1247 // =============================================================================
1248 //
1249 LDObject* LDDocument::getObject (int pos) const
1250 {
1251 if (pos < m_objects.size())
1252 return m_objects[pos];
1253 else
1254 return nullptr;
1255 }
1256
1257 // =============================================================================
1258 //
1259 int LDDocument::getObjectCount() const
1260 {
1261 return objects().size();
1262 }
1263
1264 // =============================================================================
1265 //
1266 bool LDDocument::hasUnsavedChanges() const
1267 {
1268 return not isImplicit() and history()->position() != savePosition();
1269 }
1270
1271 // =============================================================================
1272 //
1273 QString LDDocument::getDisplayName()
1274 {
1275 if (not name().isEmpty())
1276 return name();
1277
1278 if (not defaultName().isEmpty())
1279 return "[" + defaultName() + "]";
1280
1281 return QObject::tr ("untitled");
1282 }
1283
1284 // =============================================================================
1285 //
1286 void LDDocument::initializeCachedData()
1287 {
1288 if (m_needsReCache)
1289 {
1290 m_vertices.clear();
1291
1292 for (LDObject* obj : inlineContents (true, true))
1293 {
1294 if (obj->type() == OBJ_Subfile)
1295 {
1296 print ("Warning: unable to inline %1 into %2",
1297 static_cast<LDSubfile*> (obj)->fileInfo()->getDisplayName(),
1298 getDisplayName());
1299 continue;
1300 }
1301
1302 LDPolygon* data = obj->getPolygon();
1303
1304 if (data != null)
1305 {
1306 m_polygonData << *data;
1307 delete data;
1308 }
1309 }
1310
1311 m_needsReCache = false;
1312 }
1313
1314 if (m_verticesOutdated)
1315 {
1316 m_objectVertices.clear();
1317
1318 for (LDObject* obj : inlineContents (true, false))
1319 addKnownVertices (obj);
1320
1321 mergeVertices();
1322 m_verticesOutdated = false;
1323 }
1324
1325 if (m_needVertexMerge)
1326 mergeVertices();
1327 }
1328
1329 // =============================================================================
1330 //
1331 void LDDocument::mergeVertices()
1332 {
1333 m_vertices.clear();
1334
1335 for (QVector<Vertex> const& verts : m_objectVertices)
1336 m_vertices << verts;
1337
1338 RemoveDuplicates (m_vertices);
1339 m_needVertexMerge = false;
1340 }
1341
1342 // =============================================================================
1343 //
1344 QList<LDPolygon> LDDocument::inlinePolygons()
1345 {
1346 initializeCachedData();
1347 return polygonData();
1348 }
1349
1350 // =============================================================================
1351 // -----------------------------------------------------------------------------
1352 LDObjectList LDDocument::inlineContents (bool deep, bool renderinline)
1353 {
1354 // Possibly substitute with logoed studs:
1355 // stud.dat -> stud-logo.dat
1356 // stud2.dat -> stud-logo2.dat
1357 if (cfg::UseLogoStuds and renderinline)
1358 {
1359 // Ensure logoed studs are loaded first
1360 LoadLogoStuds();
1361
1362 if (name() == "stud.dat" and g_logoedStud != null)
1363 return g_logoedStud->inlineContents (deep, renderinline);
1364 elif (name() == "stud2.dat" and g_logoedStud2 != null)
1365 return g_logoedStud2->inlineContents (deep, renderinline);
1366 }
1367
1368 LDObjectList objs, objcache;
1369
1370 for (LDObject* obj : objects())
1371 {
1372 // Skip those without scemantic meaning
1373 if (not obj->isScemantic())
1374 continue;
1375
1376 // Got another sub-file reference, inline it if we're deep-inlining. If not,
1377 // just add it into the objects normally. Yay, recursion!
1378 if (deep == true and obj->type() == OBJ_Subfile)
1379 {
1380 for (LDObject* otherobj : static_cast<LDSubfile*> (obj)->inlineContents (deep, renderinline))
1381 objs << otherobj;
1382 }
1383 else
1384 objs << obj->createCopy();
1385 }
1386
1387 return objs;
1388 }
1389
1390 // =============================================================================
1391 //
1392 LDDocument* LDDocument::current()
1393 {
1394 return g_currentDocument;
1395 }
1396
1397 // =============================================================================
1398 // Sets the given file as the current one on display. At some point in time this
1399 // was an operation completely unheard of. ;)
1400 //
1401 // TODO: f can be temporarily null. This probably should not be the case.
1402 // =============================================================================
1403 void LDDocument::setCurrent (LDDocument* f)
1404 {
1405 // Implicit files were loaded for caching purposes and must never be set
1406 // current.
1407 if (f != null and f->isImplicit())
1408 return;
1409
1410 g_currentDocument = f;
1411
1412 if (g_win and f)
1413 {
1414 // A ton of stuff needs to be updated
1415 g_win->updateDocumentListItem (f);
1416 g_win->buildObjList();
1417 g_win->updateTitle();
1418 g_win->R()->setDocument (f);
1419 g_win->R()->compiler()->needMerge();
1420 print ("Changed file to %1", f->getDisplayName());
1421 }
1422 }
1423
1424 // =============================================================================
1425 //
1426 int LDDocument::countExplicitFiles()
1427 {
1428 return g_explicitDocuments.size();
1429 }
1430
1431 // =============================================================================
1432 // This little beauty closes the initial file that was open at first when opening
1433 // a new file over it.
1434 // =============================================================================
1435 void LDDocument::closeInitialFile()
1436 {
1437 if (g_explicitDocuments.size() == 2 and
1438 g_explicitDocuments[0]->name().isEmpty() and
1439 not g_explicitDocuments[1]->name().isEmpty() and
1440 not g_explicitDocuments[0]->hasUnsavedChanges())
1441 {
1442 LDDocument* filetoclose = g_explicitDocuments.first();
1443 filetoclose->dismiss();
1444 }
1445 }
1446
1447 // =============================================================================
1448 //
1449 void LoadLogoStuds()
1450 {
1451 if (g_loadingLogoedStuds or (g_logoedStud and g_logoedStud2))
1452 return;
1453
1454 g_loadingLogoedStuds = true;
1455 g_logoedStud = OpenDocument ("stud-logo.dat", true, true);
1456 g_logoedStud2 = OpenDocument ("stud2-logo.dat", true, true);
1457 print (QObject::tr ("Logoed studs loaded.\n"));
1458 g_loadingLogoedStuds = false;
1459 }
1460
1461 // =============================================================================
1462 //
1463 void LDDocument::addToSelection (LDObject* obj) // [protected]
1464 {
1465 if (obj->isSelected())
1466 return;
1467
1468 assert (obj->document() == this);
1469 m_sel << obj;
1470 g_win->R()->compileObject (obj);
1471 obj->setSelected (true);
1472 }
1473
1474 // =============================================================================
1475 //
1476 void LDDocument::removeFromSelection (LDObject* obj) // [protected]
1477 {
1478 if (not obj->isSelected())
1479 return;
1480
1481 assert (obj->document() == this);
1482 m_sel.removeOne (obj);
1483 g_win->R()->compileObject (obj);
1484 obj->setSelected (false);
1485 }
1486
1487 // =============================================================================
1488 //
1489 void LDDocument::clearSelection()
1490 {
1491 for (LDObject* obj : m_sel)
1492 removeFromSelection (obj);
1493
1494 assert (m_sel.isEmpty());
1495 }
1496
1497 // =============================================================================
1498 //
1499 const LDObjectList& LDDocument::getSelection() const
1500 {
1501 return m_sel;
1502 }
1503
1504 // =============================================================================
1505 //
1506 void LDDocument::swapObjects (LDObject* one, LDObject* other)
1507 {
1508 int a = m_objects.indexOf (one);
1509 int b = m_objects.indexOf (other);
1510 assert (a != b and a != -1 and b != -1);
1511 m_objects[b] = one;
1512 m_objects[a] = other;
1513 addToHistory (new SwapHistory (one->id(), other->id()));
1514 }
1515
1516 // =============================================================================
1517 //
1518 QString LDDocument::shortenName (QString a) // [static]
1519 {
1520 QString shortname = Basename (a);
1521 QString topdirname = Basename (Dirname (a));
1522
1523 if (g_specialSubdirectories.contains (topdirname))
1524 shortname.prepend (topdirname + "\\");
1525
1526 return shortname;
1527 }
1528
1529 // =============================================================================
1530 //
1531 QVector<Vertex> const& LDDocument::inlineVertices()
1532 {
1533 initializeCachedData();
1534 return m_vertices;
1535 }
1536
1537 void LDDocument::redoVertices()
1538 {
1539 m_verticesOutdated = true;
1540 }
1541
1542 void LDDocument::needVertexMerge()
1543 {
1544 m_needVertexMerge = true;
1545 }

mercurial