file.cpp

changeset 183
f1b8cb53d2a2
parent 182
9374fea8f77f
child 184
fae3bc9ce319
equal deleted inserted replaced
182:9374fea8f77f 183:f1b8cb53d2a2
1 /*
2 * LDForge: LDraw parts authoring CAD
3 * Copyright (C) 2013 Santeri 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 <vector>
20 #include <stdio.h>
21 #include <qmessagebox.h>
22 #include <QDir>
23 #include "common.h"
24 #include "config.h"
25 #include "file.h"
26 #include "misc.h"
27 #include "bbox.h"
28 #include "gui.h"
29 #include "history.h"
30 #include "zz_ldrawPathDialog.h"
31
32 cfg (str, io_ldpath, "");
33 cfg (str, io_recentfiles, "");
34
35 // =============================================================================
36 namespace LDPaths {
37 static str pathError;
38
39 struct {
40 str LDConfigPath;
41 str partsPath, primsPath;
42 } pathInfo;
43
44 void initPaths () {
45 if (!tryConfigure (io_ldpath)) {
46 LDrawPathDialog dlg (false);
47
48 if (!dlg.exec ())
49 exit (0);
50
51 io_ldpath = dlg.path ();
52 }
53 }
54
55 bool tryConfigure (str path) {
56 QDir dir;
57
58 if (!dir.cd (path)) {
59 pathError = "Directory does not exist.";
60 return false;
61 }
62
63 QStringList mustHave = {"LDConfig.ldr", "parts", "p"};
64 QStringList contents = dir.entryList (mustHave);
65
66 if (contents.size () != mustHave.size ()) {
67 pathError = "Not an LDraw directory! Must<br />have LDConfig.ldr, parts/ and p/.";
68 return false;
69 }
70
71 pathInfo.partsPath = fmt ("%s" DIRSLASH "parts", path.chars ());
72 pathInfo.LDConfigPath = fmt ("%s" DIRSLASH "LDConfig.ldr", path.chars ());
73 pathInfo.primsPath = fmt ("%s" DIRSLASH "p", path.chars ());
74
75 return true;
76 }
77
78 // Accessors
79 str getError () { return pathError; }
80 str ldconfig () { return pathInfo.LDConfigPath; }
81 str prims () { return pathInfo.primsPath; }
82 str parts () { return pathInfo.partsPath; }
83 }
84
85 // =============================================================================
86 OpenFile::OpenFile () {
87 m_implicit = true;
88 savePos = -1;
89 }
90
91 // =============================================================================
92 OpenFile::~OpenFile () {
93 // Clear everything from the model
94 for (LDObject* obj : m_objs)
95 delete obj;
96
97 // Clear the cache as well
98 for (LDObject* obj : m_objCache)
99 delete obj;
100 }
101
102 // =============================================================================
103 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
104 // =============================================================================
105 OpenFile* findLoadedFile (str zName) {
106 for (OpenFile* file : g_loadedFiles)
107 if (file->m_filename == zName)
108 return file;
109
110 return null;
111 }
112
113 // =============================================================================
114 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
115 // =============================================================================
116 FILE* openLDrawFile (str path, bool bSubDirectories) {
117 str zTruePath = path;
118
119 #ifndef WIN32
120 zTruePath.replace ("\\", "/");
121 #endif // WIN32
122
123 FILE* fp = fopen (path.chars (), "r");
124 str zFilePath;
125
126 if (fp != null)
127 return fp;
128
129 if (~io_ldpath.value) {
130 // Try with just the LDraw path first
131 zFilePath = fmt ("%s" DIRSLASH "%s",
132 io_ldpath.value.chars(), zTruePath.chars());
133 logf ("Trying %s\n", zFilePath.chars());
134
135 fp = fopen (zFilePath, "r");
136 if (fp != null)
137 return fp;
138
139 if (bSubDirectories) {
140 char const* saSubdirectories[] = {
141 "parts",
142 "p",
143 };
144
145 for (char const* sSubdir : saSubdirectories) {
146 zFilePath = fmt ("%s" DIRSLASH "%s" DIRSLASH "%s",
147 io_ldpath.value.chars(), sSubdir, zTruePath.chars());
148 printf ("try %s\n", zFilePath.chars());
149
150 fp = fopen (zFilePath.chars (), "r");
151
152 if (fp)
153 return fp;
154 }
155 }
156 }
157
158 return null;
159 }
160
161 // =============================================================================
162 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
163 // =============================================================================
164 std::vector<LDObject*> loadFileContents (FILE* fp, ulong* numWarnings) {
165 char line[1024];
166 vector<str> lines;
167 vector<LDObject*> objs;
168 ulong lnum = 0;
169
170 if (numWarnings)
171 *numWarnings = 0;
172
173 while (fgets (line, sizeof line, fp)) {
174 // Trim the trailing newline
175 str data = line;
176 while (data[~data - 1] == '\n' || data[~data - 1] == '\r')
177 data -= 1;
178
179 LDObject* obj = parseLine (data);
180 assert (obj != null);
181
182 // Check for parse errors and warn about tthem
183 if (obj->getType() == LDObject::Gibberish) {
184 logf (LOG_Warning, "Couldn't parse line #%lu: %s\n",
185 lnum, static_cast<LDGibberish*> (obj)->zReason.chars());
186
187 logf (LOG_Warning, "- Line was: %s\n", data.chars());
188
189 if (numWarnings)
190 (*numWarnings)++;
191 }
192
193 objs.push_back (obj);
194 lnum++;
195 }
196
197 return objs;
198 }
199
200 // =============================================================================
201 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
202 // =============================================================================
203 OpenFile* openDATFile (str path, bool search) {
204 logf ("Opening %s...\n", path.chars());
205
206 // Convert the file name to lowercase since some parts contain uppercase
207 // file names. I'll assume here that the library will always use lowercase
208 // file names for the actual parts..
209 FILE* fp;
210 if (search)
211 fp = openLDrawFile (-path, true);
212 else
213 fp = fopen (path, "r");
214
215 if (!fp) {
216 logf (LOG_Error, "Couldn't open %s: %s\n", path.chars (), strerror (errno));
217 return null;
218 }
219
220 OpenFile* load = new OpenFile;
221 load->m_filename = path;
222 ulong numWarnings;
223 std::vector<LDObject*> objs = loadFileContents (fp, &numWarnings);
224
225 for (LDObject* obj : objs)
226 load->m_objs.push_back (obj);
227
228 fclose (fp);
229 g_loadedFiles.push_back (load);
230
231 logf ("File %s parsed successfully (%lu warning%s).\n",
232 path.chars(), numWarnings, PLURAL (numWarnings));
233
234 return load;
235 }
236
237 // =============================================================================
238 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
239 // =============================================================================
240 bool OpenFile::safeToClose () {
241 setlocale (LC_ALL, "C");
242
243 // If we have unsaved changes, warn and give the option of saving.
244 if (!m_implicit && History::pos () != savePos) {
245 switch (QMessageBox::question (g_win, "Unsaved Changes",
246 fmt ("There are unsaved changes to %s. Should it be saved?", m_filename.chars ()),
247 (QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel), QMessageBox::Cancel))
248 {
249 case QMessageBox::Yes:
250 if (!save ()) {
251 str errormsg = fmt ("Failed to save %s: %s\nDo you still want to close?",
252 m_filename.chars (), strerror (lastError));
253
254 if (QMessageBox::critical (g_win, "Save Failure", errormsg,
255 (QMessageBox::Yes | QMessageBox::No), QMessageBox::No) == QMessageBox::No)
256 {
257 return false;
258 }
259 }
260
261 break;
262
263 case QMessageBox::Cancel:
264 return false;
265
266 default:
267 break;
268 }
269 }
270
271 return true;
272 }
273
274 // =============================================================================
275 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
276 // =============================================================================
277 void closeAll () {
278 if (!g_loadedFiles.size())
279 return;
280
281 // Remove all loaded files and the objects they contain
282 for (OpenFile* file : g_loadedFiles)
283 delete file;
284
285 // Clear the array
286 g_loadedFiles.clear();
287 g_curfile = NULL;
288
289 g_win->refresh ();
290 }
291
292 // =============================================================================
293 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
294 // =============================================================================
295 void newFile () {
296 // Create a new anonymous file and set it to our current
297 closeAll ();
298
299 OpenFile* f = new OpenFile;
300 f->m_filename = "";
301 g_loadedFiles.push_back (f);
302 g_curfile = f;
303
304 g_BBox.calculate();
305 g_win->refresh ();
306 }
307
308 // =============================================================================
309 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
310 // =============================================================================
311 void addRecentFile (str zPath) {
312 long lPos = io_recentfiles.value.first (zPath);
313
314 // If this file already is in the list, pop it out.
315 if (lPos != -1) {
316 if (~io_recentfiles.value == ~zPath)
317 return; // only recent file - do nothing
318
319 // Pop it out.
320 str zFront = io_recentfiles.value.substr (0, lPos);
321 str zBack = io_recentfiles.value.substr (lPos + ~zPath + 1, -1);
322 io_recentfiles.value = zFront + zBack;
323 }
324
325 // If there's too many recent files, drop one out.
326 while (io_recentfiles.value.count ('@') > 3)
327 io_recentfiles.value = io_recentfiles.value.substr (io_recentfiles.value.first ("@") + 1, -1);
328
329 // Add the file
330 if (~io_recentfiles.value > 0)
331 io_recentfiles.value += "@";
332
333 io_recentfiles += zPath;
334
335 config::save ();
336 g_win->updateRecentFilesMenu ();
337 }
338
339 // =============================================================================
340 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
341 // =============================================================================
342 void openMainFile (str path) {
343 closeAll ();
344
345 OpenFile* file = openDATFile (path, false);
346
347 if (!file) {
348 // Tell the user loading failed.
349 setlocale (LC_ALL, "C");
350 critical (fmt ("Failed to open %s: %s", path.chars(), strerror (errno)));
351 return;
352 }
353
354 file->m_implicit = false;
355 g_curfile = file;
356
357 // Recalculate the bounding box
358 g_BBox.calculate();
359
360 // Rebuild the object tree view now.
361 g_win->refresh ();
362 g_win->setTitle ();
363 g_win->R ()->resetAngles ();
364
365 History::clear ();
366
367 // Add it to the recent files list.
368 addRecentFile (path);
369 }
370
371 // =============================================================================
372 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
373 // =============================================================================
374 bool OpenFile::save (str path) {
375 if (!~path)
376 path = m_filename;
377
378 FILE* fp = fopen (path, "w");
379
380 if (!fp) {
381 lastError = errno;
382 return false;
383 }
384
385 // Write all entries now
386 for (LDObject* obj : m_objs) {
387 // LDraw requires lines to have DOS line endings
388 str zLine = fmt ("%s\r\n", obj->getContents ().chars ());
389
390 fwrite (zLine.chars(), 1, ~zLine, fp);
391 }
392
393 fclose (fp);
394
395 // We have successfully saved, update the save position now.
396 savePos = History::pos ();
397
398 g_win->setTitle ();
399
400 return true;
401 }
402
403 #define CHECK_TOKEN_COUNT(N) \
404 if (tokens.size() != N) \
405 return new LDGibberish (zLine, "Bad amount of tokens");
406
407 #define CHECK_TOKEN_NUMBERS(MIN,MAX) \
408 for (ushort i = MIN; i <= MAX; ++i) \
409 if (!isNumber (tokens[i])) \
410 return new LDGibberish (zLine, fmt ("Token #%u was `%s`, expected a number", \
411 (i + 1), tokens[i].chars()));
412
413 // =============================================================================
414 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
415 // =============================================================================
416 static vertex parseVertex (vector<str>& s, const ushort n) {
417 // Disable the locale while parsing the line or atof's behavior changes
418 // between locales (i.e. fails to read decimals properly). That is
419 // quite undesired...
420 setlocale (LC_NUMERIC, "C");
421
422 vertex v;
423 for (const Axis ax : g_Axes)
424 v[ax] = atof (s[n + ax]);
425
426 return v;
427 }
428
429 // =============================================================================
430 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
431 // =============================================================================
432 LDObject* parseLine (str zLine) {
433 vector<str> tokens = zLine.split (" ", true);
434
435 if (!tokens.size ()) {
436 // Line was empty, or only consisted of whitespace
437 return new LDEmpty;
438 }
439
440 if (~tokens[0] != 1)
441 return new LDGibberish (zLine, "Illogical line code");
442
443 const char c = tokens[0][0];
444 switch (c - '0') {
445 case 0:
446 {
447 // Comment
448 str comm;
449 for (uint i = 1; i < tokens.size(); ++i) {
450 comm += tokens[i];
451
452 if (i != tokens.size() - 1)
453 comm += ' ';
454 }
455
456 // Handle BFC statements
457 if (tokens.size() > 2 && tokens[1] == "BFC") {
458 for (short i = 0; i < LDBFC::NumStatements; ++i)
459 if (comm == fmt ("BFC %s", LDBFC::statements [i]))
460 return new LDBFC ((LDBFC::Type) i);
461
462 // MLCAD is notorious for stuffing these statements in parts it
463 // creates. The above block only handles valid statements, so we
464 // need to handle MLCAD-style invertnext separately.
465 if (comm == "BFC CERTIFY INVERTNEXT")
466 return new LDBFC (LDBFC::InvertNext);
467 }
468
469 if (tokens.size() > 2 && tokens[1] == "!LDFORGE") {
470 // Handle LDForge-specific types, they're embedded into comments
471
472 if (tokens[2] == "VERTEX") {
473 // Vertex (0 !LDFORGE VERTEX)
474 CHECK_TOKEN_COUNT (7)
475 CHECK_TOKEN_NUMBERS (3, 6)
476
477 LDVertex* obj = new LDVertex;
478 obj->dColor = atol (tokens[3]);
479
480 for (const Axis ax : g_Axes)
481 obj->vPosition[ax] = atof (tokens[4 + ax]); // 4 - 6
482
483 return obj;
484 }
485
486 if (tokens[2] == "RADIAL") {
487 CHECK_TOKEN_COUNT (20)
488 CHECK_TOKEN_NUMBERS (4, 19)
489
490 LDRadial::Type eType = LDRadial::NumTypes;
491
492 for (int i = 0; i < LDRadial::NumTypes; ++i) {
493 if (str (LDRadial::radialTypeName ((LDRadial::Type) i)).toupper ().strip (' ') == tokens[3]) {
494 eType = (LDRadial::Type) i;
495 break;
496 }
497 }
498
499 if (eType == LDRadial::NumTypes)
500 return new LDGibberish (zLine, fmt ("Unknown radial type %s", tokens[3].chars ()));
501
502 LDRadial* obj = new LDRadial;
503
504 obj->eRadialType = eType; // 3
505 obj->dColor = atol (tokens[4]); // 4
506 obj->dSegments = atol (tokens[5]); // 5
507 obj->dDivisions = atol (tokens[6]); // 6
508 obj->dRingNum = atol (tokens[7]); // 7
509
510 obj->vPosition = parseVertex (tokens, 8); // 8 - 10
511
512 for (short i = 0; i < 9; ++i)
513 obj->mMatrix[i] = atof (tokens[i + 11]); // 11 - 19
514
515 return obj;
516 }
517 }
518
519 LDComment* obj = new LDComment;
520 obj->text = comm;
521 return obj;
522 }
523
524 case 1:
525 {
526 // Subfile
527 CHECK_TOKEN_COUNT (15)
528 CHECK_TOKEN_NUMBERS (1, 13)
529
530 // Try open the file
531 OpenFile* pFile = loadSubfile (tokens[14]);
532
533 // If we cannot open the file, mark it an error
534 if (!pFile)
535 return new LDGibberish (zLine, "Could not open referred file");
536
537 LDSubfile* obj = new LDSubfile;
538 obj->dColor = atol (tokens[1]);
539 obj->vPosition = parseVertex (tokens, 2); // 2 - 4
540
541 for (short i = 0; i < 9; ++i)
542 obj->mMatrix[i] = atof (tokens[i + 5]); // 5 - 13
543
544 obj->zFileName = tokens[14];
545 obj->pFile = pFile;
546 return obj;
547 }
548
549 case 2:
550 {
551 CHECK_TOKEN_COUNT (8)
552 CHECK_TOKEN_NUMBERS (1, 7)
553
554 // Line
555 LDLine* obj = new LDLine;
556 obj->dColor = atol (tokens[1]);
557 for (short i = 0; i < 2; ++i)
558 obj->vaCoords[i] = parseVertex (tokens, 2 + (i * 3)); // 2 - 7
559 return obj;
560 }
561
562 case 3:
563 {
564 CHECK_TOKEN_COUNT (11)
565 CHECK_TOKEN_NUMBERS (1, 10)
566
567 // Triangle
568 LDTriangle* obj = new LDTriangle;
569 obj->dColor = atol (tokens[1]);
570
571 for (short i = 0; i < 3; ++i)
572 obj->vaCoords[i] = parseVertex (tokens, 2 + (i * 3)); // 2 - 10
573
574 return obj;
575 }
576
577 case 4:
578 {
579 CHECK_TOKEN_COUNT (14)
580 CHECK_TOKEN_NUMBERS (1, 13)
581
582 // Quadrilateral
583 LDQuad* obj = new LDQuad;
584 obj->dColor = atol (tokens[1]);
585
586 for (short i = 0; i < 4; ++i)
587 obj->vaCoords[i] = parseVertex (tokens, 2 + (i * 3)); // 2 - 13
588
589 return obj;
590 }
591
592 case 5:
593 {
594 CHECK_TOKEN_COUNT (14)
595 CHECK_TOKEN_NUMBERS (1, 13)
596
597 // Conditional line
598 LDCondLine* obj = new LDCondLine;
599 obj->dColor = atol (tokens[1]);
600
601 for (short i = 0; i < 4; ++i)
602 obj->vaCoords[i] = parseVertex (tokens, 2 + (i * 3)); // 2 - 13
603
604 return obj;
605 }
606
607 default: // Strange line we couldn't parse
608 return new LDGibberish (zLine, "Unknown line code number");
609 }
610 }
611
612 // =============================================================================
613 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
614 // =============================================================================
615 OpenFile* loadSubfile (str zFile) {
616 // Try open the file
617 OpenFile* pFile = findLoadedFile (zFile);
618 if (!pFile)
619 pFile = openDATFile (zFile, true);
620
621 return pFile;
622 }
623
624 // =============================================================================
625 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
626 // =============================================================================
627 void reloadAllSubfiles () {
628 if (!g_curfile)
629 return;
630
631 // First, close all but the current open file.
632 for (OpenFile* file : g_loadedFiles)
633 if (file != g_curfile)
634 delete file;
635
636 g_loadedFiles.clear ();
637 g_loadedFiles.push_back (g_curfile);
638
639 // Go through all objects in the current file and reload the subfiles
640 for (LDObject* obj : g_curfile->m_objs) {
641 if (obj->getType() == LDObject::Subfile) {
642 // Note: ref->pFile is invalid right now since all subfiles were closed.
643 LDSubfile* ref = static_cast<LDSubfile*> (obj);
644 OpenFile* pFile = loadSubfile (ref->zFileName);
645
646 if (pFile)
647 ref->pFile = pFile;
648 else {
649 // Couldn't load the file, mark it an error
650 ref->replace (new LDGibberish (ref->getContents (), "Could not open referred file"));
651 }
652 }
653
654 // Reparse gibberish files. It could be that they are invalid because
655 // the file could not be opened. Circumstances may be different now.
656 if (obj->getType() == LDObject::Gibberish)
657 obj->replace (parseLine (static_cast<LDGibberish*> (obj)->zContents));
658 }
659 }
660
661 // =============================================================================
662 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
663 // =============================================================================
664 ulong OpenFile::addObject (LDObject* obj) {
665 m_objs.push_back (obj);
666
667 if (this == g_curfile)
668 g_BBox.calcObject (obj);
669
670 return m_objs.size() - 1;
671 }
672
673 // =============================================================================
674 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
675 // =============================================================================
676 void OpenFile::insertObj (const ulong pos, LDObject* obj) {
677 m_objs.insert (m_objs.begin () + pos, obj);
678
679 if (this == g_curfile)
680 g_BBox.calcObject (obj);
681 }
682
683 // =============================================================================
684 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
685 // =============================================================================
686 void OpenFile::forgetObject (LDObject* obj) {
687 // Find the index for the given object
688 ulong ulIndex;
689 for (ulIndex = 0; ulIndex < (ulong)m_objs.size(); ++ulIndex)
690 if (m_objs[ulIndex] == obj)
691 break; // found it
692
693 if (ulIndex >= m_objs.size ())
694 return; // was not found
695
696 // Erase it from memory
697 m_objs.erase (m_objs.begin() + ulIndex);
698
699 // Update the bounding box
700 if (this == g_curfile)
701 g_BBox.calculate ();
702 }
703
704 // =============================================================================
705 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
706 // =============================================================================
707 std::vector<partListEntry> g_PartList;
708
709 void initPartList () {
710 logf ("%s: initializing parts.lst\n", __func__);
711
712 FILE* fp = openLDrawFile ("parts.lst", false);
713
714 if (!fp)
715 return;
716
717 char sLine[1024];
718 while (fgets (sLine, sizeof sLine, fp)) {
719 // Locate the first whitespace
720 char* cpWhite = strstr (sLine, " ");
721
722 char sName[65];
723 size_t uLength = (cpWhite - sLine);
724
725 if (uLength >= 64)
726 continue; // too long
727
728 strncpy (sName, sLine, uLength);
729 sName[uLength] = '\0';
730
731 // Surf through the whitespace sea!
732 while (*cpWhite == ' ')
733 cpWhite++;
734
735 // Get the end point
736 char* cpEnd = strstr (sLine, "\r");
737
738 if (cpEnd == null) {
739 // must not be DOS-formatted
740 cpEnd = strstr (sLine, "\n");
741 }
742
743 assert (cpEnd != null);
744
745 // Make the file title now
746 char sTitle[81];
747 uLength = (cpEnd - cpWhite);
748 strncpy (sTitle, cpWhite, uLength);
749 sTitle[uLength] = '\0';
750
751 // Add it to the array.
752 partListEntry entry;
753 strcpy (entry.sName, sName);
754 strcpy (entry.sTitle, sTitle);
755 g_PartList.push_back (entry);
756 }
757 }

mercurial