src/ldDocument.cc

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

mercurial