src/primitives.cc

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

mercurial