src/primitives.cc

changeset 931
85080f7a1c20
parent 929
66e8453e1768
child 932
738673e8a6b4
equal deleted inserted replaced
929:66e8453e1768 931:85080f7a1c20
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 <QDir>
20 #include <QRegExp>
21 #include <QFileDialog>
22 #include "ldDocument.h"
23 #include "mainWindow.h"
24 #include "primitives.h"
25 #include "ui_makeprim.h"
26 #include "miscallenous.h"
27 #include "colors.h"
28
29 QList<PrimitiveCategory*> g_PrimitiveCategories;
30 QList<Primitive> g_primitives;
31 static PrimitiveScanner* g_activeScanner = null;
32 PrimitiveCategory* g_unmatched = null;
33
34 EXTERN_CFGENTRY (String, DefaultName)
35 EXTERN_CFGENTRY (String, DefaultUser)
36 EXTERN_CFGENTRY (Int, DefaultLicense)
37
38 static const QStringList g_radialNameRoots =
39 {
40 "edge",
41 "cyli",
42 "disc",
43 "ndis",
44 "ring",
45 "con"
46 };
47
48 PrimitiveScanner* ActivePrimitiveScanner()
49 {
50 return g_activeScanner;
51 }
52
53 // =============================================================================
54 //
55 void LoadPrimitives()
56 {
57 // Try to load prims.cfg
58 QFile conf (Config::FilePath ("prims.cfg"));
59
60 if (not conf.open (QIODevice::ReadOnly))
61 {
62 // No prims.cfg, build it
63 PrimitiveScanner::start();
64 }
65 else
66 {
67 while (not conf.atEnd())
68 {
69 QString line = conf.readLine();
70
71 if (line.endsWith ("\n"))
72 line.chop (1);
73
74 if (line.endsWith ("\r"))
75 line.chop (1);
76
77 int space = line.indexOf (" ");
78
79 if (space == -1)
80 continue;
81
82 Primitive info;
83 info.name = line.left (space);
84 info.title = line.mid (space + 1);
85 g_primitives << info;
86 }
87
88 PrimitiveCategory::populateCategories();
89 print ("%1 primitives loaded.\n", g_primitives.size());
90 }
91 }
92
93 // =============================================================================
94 //
95 static void GetRecursiveFilenames (QDir dir, QList<QString>& fnames)
96 {
97 QFileInfoList flist = dir.entryInfoList (QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
98
99 for (const QFileInfo& info : flist)
100 {
101 if (info.isDir())
102 GetRecursiveFilenames (QDir (info.absoluteFilePath()), fnames);
103 else
104 fnames << info.absoluteFilePath();
105 }
106 }
107
108 // =============================================================================
109 //
110 PrimitiveScanner::PrimitiveScanner (QObject* parent) :
111 QObject (parent),
112 m_i (0)
113 {
114 g_activeScanner = this;
115 QDir dir (LDPaths::prims());
116 assert (dir.exists());
117 m_baselen = dir.absolutePath().length();
118 GetRecursiveFilenames (dir, m_files);
119 emit starting (m_files.size());
120 print ("Scanning primitives...");
121 }
122
123 // =============================================================================
124 //
125 PrimitiveScanner::~PrimitiveScanner()
126 {
127 g_activeScanner = null;
128 }
129
130 // =============================================================================
131 //
132 void PrimitiveScanner::work()
133 {
134 int j = Min (m_i + 100, m_files.size());
135
136 for (; m_i < j; ++m_i)
137 {
138 QString fname = m_files[m_i];
139 QFile f (fname);
140
141 if (not f.open (QIODevice::ReadOnly))
142 continue;
143
144 Primitive info;
145 info.name = fname.mid (m_baselen + 1); // make full path relative
146 info.name.replace ('/', '\\'); // use DOS backslashes, they're expected
147 info.category = null;
148 QByteArray titledata = f.readLine();
149
150 if (titledata != QByteArray())
151 info.title = QString::fromUtf8 (titledata);
152
153 info.title = info.title.simplified();
154
155 if (Q_LIKELY (info.title[0] == '0'))
156 {
157 info.title.remove (0, 1); // remove 0
158 info.title = info.title.simplified();
159 }
160
161 m_prims << info;
162 }
163
164 if (m_i == m_files.size())
165 {
166 // Done with primitives, now save to a config file
167 QString path = Config::FilePath ("prims.cfg");
168 QFile conf (path);
169
170 if (not conf.open (QIODevice::WriteOnly | QIODevice::Text))
171 Critical (format ("Couldn't write primitive list %1: %2",
172 path, conf.errorString()));
173 else
174 {
175 for (Primitive& info : m_prims)
176 fprint (conf, "%1 %2\r\n", info.name, info.title);
177
178 conf.close();
179 }
180
181 g_primitives = m_prims;
182 PrimitiveCategory::populateCategories();
183 print ("%1 primitives scanned", g_primitives.size());
184 g_activeScanner = null;
185 emit workDone();
186 deleteLater();
187 }
188 else
189 {
190 // Defer to event loop, pick up the work later
191 emit update (m_i);
192 QMetaObject::invokeMethod (this, "work", Qt::QueuedConnection);
193 }
194 }
195
196 // =============================================================================
197 //
198 void PrimitiveScanner::start()
199 {
200 if (g_activeScanner)
201 return;
202
203 PrimitiveCategory::loadCategories();
204 PrimitiveScanner* scanner = new PrimitiveScanner;
205 scanner->work();
206 }
207
208 // =============================================================================
209 //
210 PrimitiveCategory::PrimitiveCategory (QString name, QObject* parent) :
211 QObject (parent),
212 m_name (name) {}
213
214 // =============================================================================
215 //
216 void PrimitiveCategory::populateCategories()
217 {
218 loadCategories();
219
220 for (PrimitiveCategory* cat : g_PrimitiveCategories)
221 cat->prims.clear();
222
223 for (Primitive& prim : g_primitives)
224 {
225 bool matched = false;
226 prim.category = null;
227
228 // Go over the categories and their regexes, if and when there's a match,
229 // the primitive's category is set to the category the regex beloings to.
230 for (PrimitiveCategory* cat : g_PrimitiveCategories)
231 {
232 for (RegexEntry& entry : cat->regexes)
233 {
234 switch (entry.type)
235 {
236 case EFilenameRegex:
237 {
238 // f-regex, check against filename
239 matched = entry.regex.exactMatch (prim.name);
240 } break;
241
242 case ETitleRegex:
243 {
244 // t-regex, check against title
245 matched = entry.regex.exactMatch (prim.title);
246 } break;
247 }
248
249 if (matched)
250 {
251 prim.category = cat;
252 break;
253 }
254 }
255
256 // Drop out if a category was decided on.
257 if (prim.category != null)
258 break;
259 }
260
261 // If there was a match, add the primitive to the category.
262 // Otherwise, add it to the list of unmatched primitives.
263 if (prim.category != null)
264 prim.category->prims << prim;
265 else
266 g_unmatched->prims << prim;
267 }
268
269 // Sort the categories. Note that we do this here because we need the existing
270 // order for regex matching.
271 qSort (g_PrimitiveCategories.begin(), g_PrimitiveCategories.end(),
272 [](PrimitiveCategory* const& a, PrimitiveCategory* const& b) -> bool
273 {
274 return a->name() < b->name();
275 });
276 }
277
278 // =============================================================================
279 //
280 void PrimitiveCategory::loadCategories()
281 {
282 for (PrimitiveCategory* cat : g_PrimitiveCategories)
283 delete cat;
284
285 g_PrimitiveCategories.clear();
286 QString path = Config::DirectoryPath() + "primregexps.cfg";
287
288 if (not QFile::exists (path))
289 path = ":/data/primitive-categories.cfg";
290
291 QFile f (path);
292
293 if (not f.open (QIODevice::ReadOnly))
294 {
295 Critical (format (QObject::tr ("Failed to open primitive categories: %1"), f.errorString()));
296 return;
297 }
298
299 PrimitiveCategory* cat = null;
300
301 while (not f.atEnd())
302 {
303 QString line = f.readLine();
304 int colon;
305
306 if (line.endsWith ("\n"))
307 line.chop (1);
308
309 if (line.length() == 0 or line[0] == '#')
310 continue;
311
312 if ((colon = line.indexOf (":")) == -1)
313 {
314 if (cat and cat->isValidToInclude())
315 g_PrimitiveCategories << cat;
316
317 cat = new PrimitiveCategory (line);
318 }
319 elif (cat != null)
320 {
321 QString cmd = line.left (colon);
322 RegexType type = EFilenameRegex;
323
324 if (cmd == "f")
325 type = EFilenameRegex;
326 elif (cmd == "t")
327 type = ETitleRegex;
328 else
329 {
330 print (tr ("Warning: unknown command \"%1\" on line \"%2\""), cmd, line);
331 continue;
332 }
333
334 QRegExp regex (line.mid (colon + 1));
335 RegexEntry entry = { regex, type };
336 cat->regexes << entry;
337 }
338 else
339 print ("Warning: Rules given before the first category name");
340 }
341
342 if (cat->isValidToInclude())
343 g_PrimitiveCategories << cat;
344
345 // Add a category for unmatched primitives.
346 // Note: if this function is called the second time, g_unmatched has been
347 // deleted at the beginning of the function and is dangling at this point.
348 g_unmatched = new PrimitiveCategory (tr ("Other"));
349 g_PrimitiveCategories << g_unmatched;
350 f.close();
351 }
352
353 // =============================================================================
354 //
355 bool PrimitiveCategory::isValidToInclude()
356 {
357 if (regexes.isEmpty())
358 {
359 print (tr ("Warning: category \"%1\" left without patterns"), name());
360 deleteLater();
361 return false;
362 }
363
364 return true;
365 }
366
367 // =============================================================================
368 //
369 bool IsPrimitiveLoaderBusy()
370 {
371 return g_activeScanner != null;
372 }
373
374 // =============================================================================
375 //
376 static double GetRadialPoint (int i, int divs, double (*func) (double))
377 {
378 return (*func) ((i * 2 * Pi) / divs);
379 }
380
381 // =============================================================================
382 //
383 void MakeCircle (int segs, int divs, double radius, QList<QLineF>& lines)
384 {
385 for (int i = 0; i < segs; ++i)
386 {
387 double x0 = radius * GetRadialPoint (i, divs, cos),
388 x1 = radius * GetRadialPoint (i + 1, divs, cos),
389 z0 = radius * GetRadialPoint (i, divs, sin),
390 z1 = radius * GetRadialPoint (i + 1, divs, sin);
391
392 lines << QLineF (QPointF (x0, z0), QPointF (x1, z1));
393 }
394 }
395
396 // =============================================================================
397 //
398 LDObjectList MakePrimitive (PrimitiveType type, int segs, int divs, int num)
399 {
400 LDObjectList objs;
401 QList<int> condLineSegs;
402 QList<QLineF> circle;
403
404 MakeCircle (segs, divs, 1, circle);
405
406 for (int i = 0; i < segs; ++i)
407 {
408 double x0 = circle[i].x1(),
409 x1 = circle[i].x2(),
410 z0 = circle[i].y1(),
411 z1 = circle[i].y2();
412
413 switch (type)
414 {
415 case Circle:
416 {
417 Vertex v0 (x0, 0.0f, z0),
418 v1 (x1, 0.0f, z1);
419
420 LDLinePtr line (LDSpawn<LDLine>());
421 line->setVertex (0, v0);
422 line->setVertex (1, v1);
423 line->setColor (EdgeColor());
424 objs << line;
425 } break;
426
427 case Cylinder:
428 case Ring:
429 case Cone:
430 {
431 double x2, x3, z2, z3;
432 double y0, y1, y2, y3;
433
434 if (type == Cylinder)
435 {
436 x2 = x1;
437 x3 = x0;
438 z2 = z1;
439 z3 = z0;
440
441 y0 = y1 = 0.0f;
442 y2 = y3 = 1.0f;
443 }
444 else
445 {
446 x2 = x1 * (num + 1);
447 x3 = x0 * (num + 1);
448 z2 = z1 * (num + 1);
449 z3 = z0 * (num + 1);
450
451 x0 *= num;
452 x1 *= num;
453 z0 *= num;
454 z1 *= num;
455
456 if (type == Ring)
457 y0 = y1 = y2 = y3 = 0.0f;
458 else
459 {
460 y0 = y1 = 1.0f;
461 y2 = y3 = 0.0f;
462 }
463 }
464
465 Vertex v0 (x0, y0, z0),
466 v1 (x1, y1, z1),
467 v2 (x2, y2, z2),
468 v3 (x3, y3, z3);
469
470 LDQuadPtr quad (LDSpawn<LDQuad> (v0, v1, v2, v3));
471 quad->setColor (MainColor());
472
473 if (type == Cylinder)
474 quad->invert();
475
476 objs << quad;
477
478 if (type == Cylinder or type == Cone)
479 condLineSegs << i;
480 } break;
481
482 case Disc:
483 case DiscNeg:
484 {
485 double x2, z2;
486
487 if (type == Disc)
488 x2 = z2 = 0.0f;
489 else
490 {
491 x2 = (x0 >= 0.0f) ? 1.0f : -1.0f;
492 z2 = (z0 >= 0.0f) ? 1.0f : -1.0f;
493 }
494
495 Vertex v0 (x0, 0.0f, z0),
496 v1 (x1, 0.0f, z1),
497 v2 (x2, 0.0f, z2);
498
499 // Disc negatives need to go the other way around, otherwise
500 // they'll end up upside-down.
501 LDTrianglePtr seg (LDSpawn<LDTriangle>());
502 seg->setColor (MainColor());
503 seg->setVertex (type == Disc ? 0 : 2, v0);
504 seg->setVertex (1, v1);
505 seg->setVertex (type == Disc ? 2 : 0, v2);
506 objs << seg;
507 } break;
508 }
509 }
510
511 // If this is not a full circle, we need a conditional line at the other
512 // end, too.
513 if (segs < divs and condLineSegs.size() != 0)
514 condLineSegs << segs;
515
516 for (int i : condLineSegs)
517 {
518 Vertex v0 (GetRadialPoint (i, divs, cos), 0.0f, GetRadialPoint (i, divs, sin)),
519 v1,
520 v2 (GetRadialPoint (i + 1, divs, cos), 0.0f, GetRadialPoint (i + 1, divs, sin)),
521 v3 (GetRadialPoint (i - 1, divs, cos), 0.0f, GetRadialPoint (i - 1, divs, sin));
522
523 if (type == Cylinder)
524 {
525 v1 = Vertex (v0[X], 1.0f, v0[Z]);
526 }
527 elif (type == Cone)
528 {
529 v1 = Vertex (v0[X] * (num + 1), 0.0f, v0[Z] * (num + 1));
530 v0.setX (v0.x() * num);
531 v0.setY (1.0);
532 v0.setZ (v0.z() * num);
533 }
534
535 LDCondLinePtr line = (LDSpawn<LDCondLine>());
536 line->setColor (EdgeColor());
537 line->setVertex (0, v0);
538 line->setVertex (1, v1);
539 line->setVertex (2, v2);
540 line->setVertex (3, v3);
541 objs << line;
542 }
543
544 return objs;
545 }
546
547 // =============================================================================
548 //
549 static QString PrimitiveTypeName (PrimitiveType type)
550 {
551 // Not translated as primitives are in English.
552 return type == Circle ? "Circle" :
553 type == Cylinder ? "Cylinder" :
554 type == Disc ? "Disc" :
555 type == DiscNeg ? "Disc Negative" :
556 type == Ring ? "Ring" : "Cone";
557 }
558
559 // =============================================================================
560 //
561 QString MakeRadialFileName (PrimitiveType type, int segs, int divs, int num)
562 {
563 int numer = segs,
564 denom = divs;
565
566 // Simplify the fractional part, but the denominator must be at least 4.
567 Simplify (numer, denom);
568
569 if (denom < 4)
570 {
571 const int factor = 4 / denom;
572 numer *= factor;
573 denom *= factor;
574 }
575
576 // Compose some general information: prefix, fraction, root, ring number
577 QString prefix = (divs == LowResolution) ? "" : format ("%1/", divs);
578 QString frac = format ("%1-%2", numer, denom);
579 QString root = g_radialNameRoots[type];
580 QString numstr = (type == Ring or type == Cone) ? format ("%1", num) : "";
581
582 // Truncate the root if necessary (7-16rin4.dat for instance).
583 // However, always keep the root at least 2 characters.
584 int extra = (frac.length() + numstr.length() + root.length()) - 8;
585 root.chop (Clamp (extra, 0, 2));
586
587 // Stick them all together and return the result.
588 return prefix + frac + root + numstr + ".dat";
589 }
590
591 // =============================================================================
592 //
593 LDDocumentPtr GeneratePrimitive (PrimitiveType type, int segs, int divs, int num)
594 {
595 // Make the description
596 QString frac = QString::number ((float) segs / divs);
597 QString name = MakeRadialFileName (type, segs, divs, num);
598 QString descr;
599
600 // Ensure that there's decimals, even if they're 0.
601 if (frac.indexOf (".") == -1)
602 frac += ".0";
603
604 if (type == Ring or type == Cone)
605 {
606 QString spacing =
607 (num < 10) ? " " :
608 (num < 100) ? " " : "";
609
610 descr = format ("%1 %2%3 x %4", PrimitiveTypeName (type), spacing, num, frac);
611 }
612 else
613 descr = format ("%1 %2", PrimitiveTypeName (type), frac);
614
615 // Prepend "Hi-Res" if 48/ primitive.
616 if (divs == HighResolution)
617 descr.insert (0, "Hi-Res ");
618
619 LDDocumentPtr f = LDDocument::createNew();
620 f->setDefaultName (name);
621
622 QString author = APPNAME;
623 QString license = "";
624
625 if (not cfg::DefaultName.isEmpty())
626 {
627 license = PreferredLicenseText();
628 author = format ("%1 [%2]", cfg::DefaultName, cfg::DefaultUser);
629 }
630
631 LDObjectList objs;
632
633 objs << LDSpawn<LDComment> (descr)
634 << LDSpawn<LDComment> (format ("Name: %1", name))
635 << LDSpawn<LDComment> (format ("Author: %1", author))
636 << LDSpawn<LDComment> (format ("!LDRAW_ORG Unofficial_%1Primitive",
637 divs == HighResolution ? "48_" : ""))
638 << LDSpawn<LDComment> (license)
639 << LDSpawn<LDEmpty>()
640 << LDSpawn<LDBFC> (BFCStatement::CertifyCCW)
641 << LDSpawn<LDEmpty>();
642
643 f->setImplicit (false);
644 f->history()->setIgnoring (false);
645 f->addObjects (objs);
646 f->addObjects (MakePrimitive (type, segs, divs, num));
647 f->addHistoryStep();
648 return f;
649 }
650
651 // =============================================================================
652 //
653 LDDocumentPtr GetPrimitive (PrimitiveType type, int segs, int divs, int num)
654 {
655 QString name = MakeRadialFileName (type, segs, divs, num);
656 LDDocumentPtr f = GetDocument (name);
657
658 if (f != null)
659 return f;
660
661 return GeneratePrimitive (type, segs, divs, num);
662 }
663
664 // =============================================================================
665 //
666 PrimitivePrompt::PrimitivePrompt (QWidget* parent, Qt::WindowFlags f) :
667 QDialog (parent, f)
668 {
669 ui = new Ui_MakePrimUI;
670 ui->setupUi (this);
671 connect (ui->cb_hires, SIGNAL (toggled (bool)), this, SLOT (hiResToggled (bool)));
672 }
673
674 // =============================================================================
675 //
676 PrimitivePrompt::~PrimitivePrompt()
677 {
678 delete ui;
679 }
680
681 // =============================================================================
682 //
683 void PrimitivePrompt::hiResToggled (bool on)
684 {
685 ui->sb_segs->setMaximum (on ? HighResolution : LowResolution);
686
687 // If the current value is 16 and we switch to hi-res, default the
688 // spinbox to 48.
689 if (on and ui->sb_segs->value() == LowResolution)
690 ui->sb_segs->setValue (HighResolution);
691 }
692
693 // =============================================================================
694 //
695 void MainWindow::actionMakePrimitive()
696 {
697 PrimitivePrompt* dlg = new PrimitivePrompt (g_win);
698
699 if (not dlg->exec())
700 return;
701
702 int segs = dlg->ui->sb_segs->value();
703 int divs = dlg->ui->cb_hires->isChecked() ? HighResolution : LowResolution;
704 int num = dlg->ui->sb_ringnum->value();
705 PrimitiveType type =
706 dlg->ui->rb_circle->isChecked() ? Circle :
707 dlg->ui->rb_cylinder->isChecked() ? Cylinder :
708 dlg->ui->rb_disc->isChecked() ? Disc :
709 dlg->ui->rb_ndisc->isChecked() ? DiscNeg :
710 dlg->ui->rb_ring->isChecked() ? Ring : Cone;
711
712 LDDocumentPtr f = GeneratePrimitive (type, segs, divs, num);
713 f->setImplicit (false);
714 g_win->save (f, false);
715 }

mercurial