1 //-----------------------------------------------------------------------------
2 // Entry point in to the program, our registry-stored settings and top-level
3 // housekeeping when we open, save, and create new files.
4 //
5 // Copyright 2008-2013 Jonathan Westhues.
6 //-----------------------------------------------------------------------------
7 #include "solvespace.h"
8 #include "config.h"
9 
10 SolveSpaceUI SolveSpace::SS = {};
11 Sketch SolveSpace::SK = {};
12 
13 std::string SolveSpace::RecentFile[MAX_RECENT] = {};
14 
Init()15 void SolveSpaceUI::Init() {
16     SS.tangentArcRadius = 10.0;
17 
18     // Then, load the registry settings.
19     int i;
20     // Default list of colors for the model material
21     modelColor[0] = CnfThawColor(RGBi(150, 150, 150), "ModelColor_0");
22     modelColor[1] = CnfThawColor(RGBi(100, 100, 100), "ModelColor_1");
23     modelColor[2] = CnfThawColor(RGBi( 30,  30,  30), "ModelColor_2");
24     modelColor[3] = CnfThawColor(RGBi(150,   0,   0), "ModelColor_3");
25     modelColor[4] = CnfThawColor(RGBi(  0, 100,   0), "ModelColor_4");
26     modelColor[5] = CnfThawColor(RGBi(  0,  80,  80), "ModelColor_5");
27     modelColor[6] = CnfThawColor(RGBi(  0,   0, 130), "ModelColor_6");
28     modelColor[7] = CnfThawColor(RGBi( 80,   0,  80), "ModelColor_7");
29     // Light intensities
30     lightIntensity[0] = CnfThawFloat(1.0f, "LightIntensity_0");
31     lightIntensity[1] = CnfThawFloat(0.5f, "LightIntensity_1");
32     ambientIntensity = 0.3; // no setting for that yet
33     // Light positions
34     lightDir[0].x = CnfThawFloat(-1.0f, "LightDir_0_Right"     );
35     lightDir[0].y = CnfThawFloat( 1.0f, "LightDir_0_Up"        );
36     lightDir[0].z = CnfThawFloat( 0.0f, "LightDir_0_Forward"   );
37     lightDir[1].x = CnfThawFloat( 1.0f, "LightDir_1_Right"     );
38     lightDir[1].y = CnfThawFloat( 0.0f, "LightDir_1_Up"        );
39     lightDir[1].z = CnfThawFloat( 0.0f, "LightDir_1_Forward"   );
40 
41     exportMode = false;
42     // Chord tolerance
43     chordTol = CnfThawFloat(0.5f, "ChordTolerancePct");
44     // Max pwl segments to generate
45     maxSegments = CnfThawInt(10, "MaxSegments");
46     // Chord tolerance
47     exportChordTol = CnfThawFloat(0.1f, "ExportChordTolerance");
48     // Max pwl segments to generate
49     exportMaxSegments = CnfThawInt(64, "ExportMaxSegments");
50     // View units
51     viewUnits = (Unit)CnfThawInt((uint32_t)UNIT_MM, "ViewUnits");
52     // Number of digits after the decimal point
53     afterDecimalMm = CnfThawInt(2, "AfterDecimalMm");
54     afterDecimalInch = CnfThawInt(3, "AfterDecimalInch");
55     // Camera tangent (determines perspective)
56     cameraTangent = CnfThawFloat(0.3f/1e3f, "CameraTangent");
57     // Grid spacing
58     gridSpacing = CnfThawFloat(5.0f, "GridSpacing");
59     // Export scale factor
60     exportScale = CnfThawFloat(1.0f, "ExportScale");
61     // Export offset (cutter radius comp)
62     exportOffset = CnfThawFloat(0.0f, "ExportOffset");
63     // Rewrite exported colors close to white into black (assuming white bg)
64     fixExportColors = CnfThawBool(true, "FixExportColors");
65     // Draw back faces of triangles (when mesh is leaky/self-intersecting)
66     drawBackFaces = CnfThawBool(true, "DrawBackFaces");
67     // Check that contours are closed and not self-intersecting
68     checkClosedContour = CnfThawBool(true, "CheckClosedContour");
69     // Export shaded triangles in a 2d view
70     exportShadedTriangles = CnfThawBool(true, "ExportShadedTriangles");
71     // Export pwl curves (instead of exact) always
72     exportPwlCurves = CnfThawBool(false, "ExportPwlCurves");
73     // Background color on-screen
74     backgroundColor = CnfThawColor(RGBi(0, 0, 0), "BackgroundColor");
75     // Whether export canvas size is fixed or derived from bbox
76     exportCanvasSizeAuto = CnfThawBool(true, "ExportCanvasSizeAuto");
77     // Margins for automatic canvas size
78     exportMargin.left   = CnfThawFloat(5.0f, "ExportMargin_Left");
79     exportMargin.right  = CnfThawFloat(5.0f, "ExportMargin_Right");
80     exportMargin.bottom = CnfThawFloat(5.0f, "ExportMargin_Bottom");
81     exportMargin.top    = CnfThawFloat(5.0f, "ExportMargin_Top");
82     // Dimensions for fixed canvas size
83     exportCanvas.width  = CnfThawFloat(100.0f, "ExportCanvas_Width");
84     exportCanvas.height = CnfThawFloat(100.0f, "ExportCanvas_Height");
85     exportCanvas.dx     = CnfThawFloat(  5.0f, "ExportCanvas_Dx");
86     exportCanvas.dy     = CnfThawFloat(  5.0f, "ExportCanvas_Dy");
87     // Extra parameters when exporting G code
88     gCode.depth         = CnfThawFloat(10.0f, "GCode_Depth");
89     gCode.passes        = CnfThawInt(1, "GCode_Passes");
90     gCode.feed          = CnfThawFloat(10.0f, "GCode_Feed");
91     gCode.plungeFeed    = CnfThawFloat(10.0f, "GCode_PlungeFeed");
92     // Show toolbar in the graphics window
93     showToolbar = CnfThawBool(true, "ShowToolbar");
94     // Recent files menus
95     for(i = 0; i < MAX_RECENT; i++) {
96         RecentFile[i] = CnfThawString("", "RecentFile_" + std::to_string(i));
97     }
98     RefreshRecentMenus();
99     // Autosave timer
100     autosaveInterval = CnfThawInt(5, "AutosaveInterval");
101 
102     // The default styles (colors, line widths, etc.) are also stored in the
103     // configuration file, but we will automatically load those as we need
104     // them.
105 
106     SetAutosaveTimerFor(autosaveInterval);
107 
108     NewFile();
109     AfterNewFile();
110 }
111 
LoadAutosaveFor(const std::string & filename)112 bool SolveSpaceUI::LoadAutosaveFor(const std::string &filename) {
113     std::string autosaveFile = filename + AUTOSAVE_SUFFIX;
114 
115     FILE *f = ssfopen(autosaveFile, "rb");
116     if(!f)
117         return false;
118     fclose(f);
119 
120     if(LoadAutosaveYesNo() == DIALOG_YES) {
121         unsaved = true;
122         return LoadFromFile(autosaveFile);
123     }
124 
125     return false;
126 }
127 
OpenFile(const std::string & filename)128 bool SolveSpaceUI::OpenFile(const std::string &filename) {
129     bool autosaveLoaded = LoadAutosaveFor(filename);
130     bool fileLoaded = autosaveLoaded || LoadFromFile(filename);
131     if(fileLoaded)
132         saveFile = filename;
133     bool success = fileLoaded && ReloadAllImported(/*canCancel=*/true);
134     if(success) {
135         AddToRecentList(filename);
136     } else {
137         saveFile = "";
138         NewFile();
139     }
140     AfterNewFile();
141     unsaved = autosaveLoaded;
142     return success;
143 }
144 
Exit(void)145 void SolveSpaceUI::Exit(void) {
146     // Recent files
147     for(int i = 0; i < MAX_RECENT; i++)
148         CnfFreezeString(RecentFile[i], "RecentFile_" + std::to_string(i));
149     // Model colors
150     for(int i = 0; i < MODEL_COLORS; i++)
151         CnfFreezeColor(modelColor[i], "ModelColor_" + std::to_string(i));
152     // Light intensities
153     CnfFreezeFloat((float)lightIntensity[0], "LightIntensity_0");
154     CnfFreezeFloat((float)lightIntensity[1], "LightIntensity_1");
155     // Light directions
156     CnfFreezeFloat((float)lightDir[0].x, "LightDir_0_Right");
157     CnfFreezeFloat((float)lightDir[0].y, "LightDir_0_Up");
158     CnfFreezeFloat((float)lightDir[0].z, "LightDir_0_Forward");
159     CnfFreezeFloat((float)lightDir[1].x, "LightDir_1_Right");
160     CnfFreezeFloat((float)lightDir[1].y, "LightDir_1_Up");
161     CnfFreezeFloat((float)lightDir[1].z, "LightDir_1_Forward");
162     // Chord tolerance
163     CnfFreezeFloat((float)chordTol, "ChordTolerancePct");
164     // Max pwl segments to generate
165     CnfFreezeInt((uint32_t)maxSegments, "MaxSegments");
166     // Export Chord tolerance
167     CnfFreezeFloat((float)exportChordTol, "ExportChordTolerance");
168     // Export Max pwl segments to generate
169     CnfFreezeInt((uint32_t)exportMaxSegments, "ExportMaxSegments");
170     // View units
171     CnfFreezeInt((uint32_t)viewUnits, "ViewUnits");
172     // Number of digits after the decimal point
173     CnfFreezeInt((uint32_t)afterDecimalMm, "AfterDecimalMm");
174     CnfFreezeInt((uint32_t)afterDecimalInch, "AfterDecimalInch");
175     // Camera tangent (determines perspective)
176     CnfFreezeFloat((float)cameraTangent, "CameraTangent");
177     // Grid spacing
178     CnfFreezeFloat(gridSpacing, "GridSpacing");
179     // Export scale
180     CnfFreezeFloat(exportScale, "ExportScale");
181     // Export offset (cutter radius comp)
182     CnfFreezeFloat(exportOffset, "ExportOffset");
183     // Rewrite exported colors close to white into black (assuming white bg)
184     CnfFreezeBool(fixExportColors, "FixExportColors");
185     // Draw back faces of triangles (when mesh is leaky/self-intersecting)
186     CnfFreezeBool(drawBackFaces, "DrawBackFaces");
187     // Check that contours are closed and not self-intersecting
188     CnfFreezeBool(checkClosedContour, "CheckClosedContour");
189     // Export shaded triangles in a 2d view
190     CnfFreezeBool(exportShadedTriangles, "ExportShadedTriangles");
191     // Export pwl curves (instead of exact) always
192     CnfFreezeBool(exportPwlCurves, "ExportPwlCurves");
193     // Background color on-screen
194     CnfFreezeColor(backgroundColor, "BackgroundColor");
195     // Whether export canvas size is fixed or derived from bbox
196     CnfFreezeBool(exportCanvasSizeAuto, "ExportCanvasSizeAuto");
197     // Margins for automatic canvas size
198     CnfFreezeFloat(exportMargin.left,   "ExportMargin_Left");
199     CnfFreezeFloat(exportMargin.right,  "ExportMargin_Right");
200     CnfFreezeFloat(exportMargin.bottom, "ExportMargin_Bottom");
201     CnfFreezeFloat(exportMargin.top,    "ExportMargin_Top");
202     // Dimensions for fixed canvas size
203     CnfFreezeFloat(exportCanvas.width,  "ExportCanvas_Width");
204     CnfFreezeFloat(exportCanvas.height, "ExportCanvas_Height");
205     CnfFreezeFloat(exportCanvas.dx,     "ExportCanvas_Dx");
206     CnfFreezeFloat(exportCanvas.dy,     "ExportCanvas_Dy");
207      // Extra parameters when exporting G code
208     CnfFreezeFloat(gCode.depth,         "GCode_Depth");
209     CnfFreezeInt(gCode.passes,          "GCode_Passes");
210     CnfFreezeFloat(gCode.feed,          "GCode_Feed");
211     CnfFreezeFloat(gCode.plungeFeed,    "GCode_PlungeFeed");
212     // Show toolbar in the graphics window
213     CnfFreezeBool(showToolbar, "ShowToolbar");
214     // Autosave timer
215     CnfFreezeInt(autosaveInterval, "AutosaveInterval");
216 
217     // And the default styles, colors and line widths and such.
218     Style::FreezeDefaultStyles();
219 
220     ExitNow();
221 }
222 
ScheduleGenerateAll()223 void SolveSpaceUI::ScheduleGenerateAll() {
224     if(!later.scheduled) ScheduleLater();
225     later.scheduled = true;
226     later.generateAll = true;
227 }
228 
ScheduleShowTW()229 void SolveSpaceUI::ScheduleShowTW() {
230     if(!later.scheduled) ScheduleLater();
231     later.scheduled = true;
232     later.showTW = true;
233 }
234 
DoLater(void)235 void SolveSpaceUI::DoLater(void) {
236     if(later.generateAll) GenerateAll();
237     if(later.showTW) TW.Show();
238     later = {};
239 }
240 
MmPerUnit(void)241 double SolveSpaceUI::MmPerUnit(void) {
242     if(viewUnits == UNIT_INCHES) {
243         return 25.4;
244     } else {
245         return 1.0;
246     }
247 }
UnitName(void)248 const char *SolveSpaceUI::UnitName(void) {
249     if(viewUnits == UNIT_INCHES) {
250         return "inch";
251     } else {
252         return "mm";
253     }
254 }
MmToString(double v)255 std::string SolveSpaceUI::MmToString(double v) {
256     if(viewUnits == UNIT_INCHES) {
257         return ssprintf("%.*f", afterDecimalInch, v/25.4);
258     } else {
259         return ssprintf("%.*f", afterDecimalMm, v);
260     }
261 }
ExprToMm(Expr * e)262 double SolveSpaceUI::ExprToMm(Expr *e) {
263     return (e->Eval()) * MmPerUnit();
264 }
StringToMm(const std::string & str)265 double SolveSpaceUI::StringToMm(const std::string &str) {
266     return std::stod(str) * MmPerUnit();
267 }
ChordTolMm(void)268 double SolveSpaceUI::ChordTolMm(void) {
269     if(exportMode) return ExportChordTolMm();
270     return chordTolCalculated;
271 }
ExportChordTolMm(void)272 double SolveSpaceUI::ExportChordTolMm(void) {
273     return exportChordTol / exportScale;
274 }
GetMaxSegments(void)275 int SolveSpaceUI::GetMaxSegments(void) {
276     if(exportMode) return exportMaxSegments;
277     return maxSegments;
278 }
UnitDigitsAfterDecimal(void)279 int SolveSpaceUI::UnitDigitsAfterDecimal(void) {
280     return (viewUnits == UNIT_INCHES) ? afterDecimalInch : afterDecimalMm;
281 }
SetUnitDigitsAfterDecimal(int v)282 void SolveSpaceUI::SetUnitDigitsAfterDecimal(int v) {
283     if(viewUnits == UNIT_INCHES) {
284         afterDecimalInch = v;
285     } else {
286         afterDecimalMm = v;
287     }
288 }
289 
CameraTangent(void)290 double SolveSpaceUI::CameraTangent(void) {
291     if(!usePerspectiveProj) {
292         return 0;
293     } else {
294         return cameraTangent;
295     }
296 }
297 
AfterNewFile(void)298 void SolveSpaceUI::AfterNewFile(void) {
299     // Clear out the traced point, which is no longer valid
300     traced.point = Entity::NO_ENTITY;
301     traced.path.l.Clear();
302     // and the naked edges
303     nakedEdges.Clear();
304 
305     // Quit export mode
306     justExportedInfo.draw = false;
307     exportMode = false;
308 
309     // GenerateAll() expects the view to be valid, because it uses that to
310     // fill in default values for extrusion depths etc. (which won't matter
311     // here, but just don't let it work on garbage)
312     SS.GW.offset    = Vector::From(0, 0, 0);
313     SS.GW.projRight = Vector::From(1, 0, 0);
314     SS.GW.projUp    = Vector::From(0, 1, 0);
315 
316     GenerateAll(GENERATE_REGEN);
317 
318     TW.Init();
319     GW.Init();
320 
321     unsaved = false;
322 
323     int w, h;
324     GetGraphicsWindowSize(&w, &h);
325     GW.width = w;
326     GW.height = h;
327 
328     // The triangles haven't been generated yet, but zoom to fit the entities
329     // roughly in the window, since that sets the mesh tolerance. Consider
330     // invisible entities, so we still get something reasonable if the only
331     // thing visible is the not-yet-generated surfaces.
332     GW.ZoomToFit(true);
333 
334     GenerateAll(GENERATE_ALL);
335     SS.ScheduleShowTW();
336     // Then zoom to fit again, to fit the triangles
337     GW.ZoomToFit(false);
338 
339     // Create all the default styles; they'll get created on the fly anyways,
340     // but can't hurt to do it now.
341     Style::CreateAllDefaultStyles();
342 
343     UpdateWindowTitle();
344 }
345 
RemoveFromRecentList(const std::string & filename)346 void SolveSpaceUI::RemoveFromRecentList(const std::string &filename) {
347     int src, dest;
348     dest = 0;
349     for(src = 0; src < MAX_RECENT; src++) {
350         if(filename != RecentFile[src]) {
351             if(src != dest) RecentFile[dest] = RecentFile[src];
352             dest++;
353         }
354     }
355     while(dest < MAX_RECENT) RecentFile[dest++].clear();
356     RefreshRecentMenus();
357 }
AddToRecentList(const std::string & filename)358 void SolveSpaceUI::AddToRecentList(const std::string &filename) {
359     RemoveFromRecentList(filename);
360 
361     int src;
362     for(src = MAX_RECENT - 2; src >= 0; src--) {
363         RecentFile[src+1] = RecentFile[src];
364     }
365     RecentFile[0] = filename;
366     RefreshRecentMenus();
367 }
368 
GetFilenameAndSave(bool saveAs)369 bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
370     std::string prevSaveFile = saveFile;
371 
372     if(saveAs || saveFile.empty()) {
373         if(!GetSaveFile(&saveFile, "", SlvsFileFilter)) return false;
374         // need to get new filename directly into saveFile, since that
375         // determines linkFileRel path
376     }
377 
378     if(SaveToFile(saveFile)) {
379         AddToRecentList(saveFile);
380         RemoveAutosave();
381         unsaved = false;
382         return true;
383     } else {
384         // don't store an invalid save filename
385         saveFile = prevSaveFile;
386         return false;
387     }
388 }
389 
Autosave()390 bool SolveSpaceUI::Autosave()
391 {
392     SetAutosaveTimerFor(autosaveInterval);
393 
394     if(!saveFile.empty() && unsaved)
395         return SaveToFile(saveFile + AUTOSAVE_SUFFIX);
396 
397     return false;
398 }
399 
RemoveAutosave()400 void SolveSpaceUI::RemoveAutosave()
401 {
402     std::string autosaveFile = saveFile + AUTOSAVE_SUFFIX;
403     ssremove(autosaveFile);
404 }
405 
OkayToStartNewFile(void)406 bool SolveSpaceUI::OkayToStartNewFile(void) {
407     if(!unsaved) return true;
408 
409     switch(SaveFileYesNoCancel()) {
410         case DIALOG_YES:
411             return GetFilenameAndSave(false);
412 
413         case DIALOG_NO:
414             RemoveAutosave();
415             return true;
416 
417         case DIALOG_CANCEL:
418             return false;
419 
420         default: oops(); break;
421     }
422 }
423 
UpdateWindowTitle(void)424 void SolveSpaceUI::UpdateWindowTitle(void) {
425     SetCurrentFilename(saveFile);
426 }
427 
Extension(const std::string & filename)428 static std::string Extension(const std::string &filename) {
429     int dot = filename.rfind('.');
430     if(dot >= 0) {
431         std::string ext = filename.substr(dot + 1, filename.length());
432         std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
433         return ext;
434     }
435     return "";
436 }
437 
MenuFile(int id)438 void SolveSpaceUI::MenuFile(int id) {
439     if(id >= RECENT_OPEN && id < (RECENT_OPEN+MAX_RECENT)) {
440         if(!SS.OkayToStartNewFile()) return;
441 
442         std::string newFile = RecentFile[id - RECENT_OPEN];
443         SS.OpenFile(newFile);
444         return;
445     }
446 
447     switch(id) {
448         case GraphicsWindow::MNU_NEW:
449             if(!SS.OkayToStartNewFile()) break;
450 
451             SS.saveFile = "";
452             SS.NewFile();
453             SS.AfterNewFile();
454             break;
455 
456         case GraphicsWindow::MNU_OPEN: {
457             if(!SS.OkayToStartNewFile()) break;
458 
459             std::string newFile;
460             if(GetOpenFile(&newFile, "", SlvsFileFilter)) {
461                 SS.OpenFile(newFile);
462             }
463             break;
464         }
465 
466         case GraphicsWindow::MNU_SAVE:
467             SS.GetFilenameAndSave(false);
468             break;
469 
470         case GraphicsWindow::MNU_SAVE_AS:
471             SS.GetFilenameAndSave(true);
472             break;
473 
474         case GraphicsWindow::MNU_EXPORT_PNG: {
475             std::string exportFile;
476             if(!GetSaveFile(&exportFile, "", PngFileFilter)) break;
477             SS.ExportAsPngTo(exportFile);
478             break;
479         }
480 
481         case GraphicsWindow::MNU_EXPORT_VIEW: {
482             std::string exportFile;
483             if(!GetSaveFile(&exportFile, CnfThawString("", "ViewExportFormat"),
484                             VectorFileFilter)) break;
485             CnfFreezeString(Extension(exportFile), "ViewExportFormat");
486 
487             // If the user is exporting something where it would be
488             // inappropriate to include the constraints, then warn.
489             if(SS.GW.showConstraints &&
490                 (FilenameHasExtension(exportFile, ".txt") ||
491                  fabs(SS.exportOffset) > LENGTH_EPS))
492             {
493                 Message("Constraints are currently shown, and will be exported "
494                         "in the toolpath. This is probably not what you want; "
495                         "hide them by clicking the link at the top of the "
496                         "text window.");
497             }
498 
499             SS.ExportViewOrWireframeTo(exportFile, false);
500             break;
501         }
502 
503         case GraphicsWindow::MNU_EXPORT_WIREFRAME: {
504             std::string exportFile;
505             if(!GetSaveFile(&exportFile, CnfThawString("", "WireframeExportFormat"),
506                             Vector3dFileFilter)) break;
507             CnfFreezeString(Extension(exportFile), "WireframeExportFormat");
508 
509             SS.ExportViewOrWireframeTo(exportFile, true);
510             break;
511         }
512 
513         case GraphicsWindow::MNU_EXPORT_SECTION: {
514             std::string exportFile;
515             if(!GetSaveFile(&exportFile, CnfThawString("", "SectionExportFormat"),
516                             VectorFileFilter)) break;
517             CnfFreezeString(Extension(exportFile), "SectionExportFormat");
518 
519             SS.ExportSectionTo(exportFile);
520             break;
521         }
522 
523         case GraphicsWindow::MNU_EXPORT_MESH: {
524             std::string exportFile;
525             if(!GetSaveFile(&exportFile, CnfThawString("", "MeshExportFormat"),
526                             MeshFileFilter)) break;
527             CnfFreezeString(Extension(exportFile), "MeshExportFormat");
528 
529             SS.ExportMeshTo(exportFile);
530             break;
531         }
532 
533         case GraphicsWindow::MNU_EXPORT_SURFACES: {
534             std::string exportFile;
535             if(!GetSaveFile(&exportFile, CnfThawString("", "SurfacesExportFormat"),
536                             SurfaceFileFilter)) break;
537             CnfFreezeString(Extension(exportFile), "SurfacesExportFormat");
538 
539             StepFileWriter sfw = {};
540             sfw.ExportSurfacesTo(exportFile);
541             break;
542         }
543 
544         case GraphicsWindow::MNU_IMPORT: {
545             std::string importFile;
546             if(!GetOpenFile(&importFile, CnfThawString("", "ImportFormat"),
547                             ImportableFileFilter)) break;
548             CnfFreezeString(Extension(importFile), "ImportFormat");
549 
550             if(Extension(importFile) == "dxf") {
551                 ImportDxf(importFile);
552             } else if(Extension(importFile) == "dwg") {
553                 ImportDwg(importFile);
554             } else {
555                 Error("Can't identify file type from file extension of "
556                       "filename '%s'; try .dxf or .dwg.", importFile.c_str());
557             }
558 
559             SS.GenerateAll(SolveSpaceUI::GENERATE_UNTIL_ACTIVE);
560             SS.ScheduleShowTW();
561             break;
562         }
563 
564         case GraphicsWindow::MNU_EXIT:
565             if(!SS.OkayToStartNewFile()) break;
566             SS.Exit();
567             break;
568 
569         default: oops();
570     }
571 
572     SS.UpdateWindowTitle();
573 }
574 
MenuAnalyze(int id)575 void SolveSpaceUI::MenuAnalyze(int id) {
576     SS.GW.GroupSelection();
577 #define gs (SS.GW.gs)
578 
579     switch(id) {
580         case GraphicsWindow::MNU_STEP_DIM:
581             if(gs.constraints == 1 && gs.n == 0) {
582                 Constraint *c = SK.GetConstraint(gs.constraint[0]);
583                 if(c->HasLabel() && !c->reference) {
584                     SS.TW.shown.dimFinish = c->valA;
585                     SS.TW.shown.dimSteps = 10;
586                     SS.TW.shown.dimIsDistance =
587                         (c->type != Constraint::ANGLE) &&
588                         (c->type != Constraint::LENGTH_RATIO) &&
589                         (c->type != Constraint::LENGTH_DIFFERENCE);
590                     SS.TW.shown.constraint = c->h;
591                     SS.TW.shown.screen = TextWindow::SCREEN_STEP_DIMENSION;
592 
593                     // The step params are specified in the text window,
594                     // so force that to be shown.
595                     SS.GW.ForceTextWindowShown();
596 
597                     SS.ScheduleShowTW();
598                     SS.GW.ClearSelection();
599                 } else {
600                     Error("Constraint must have a label, and must not be "
601                           "a reference dimension.");
602                 }
603             } else {
604                 Error("Bad selection for step dimension; select a constraint.");
605             }
606             break;
607 
608         case GraphicsWindow::MNU_NAKED_EDGES: {
609             SS.nakedEdges.Clear();
610 
611             Group *g = SK.GetGroup(SS.GW.activeGroup);
612             SMesh *m = &(g->displayMesh);
613             SKdNode *root = SKdNode::From(m);
614             bool inters, leaks;
615             root->MakeCertainEdgesInto(&(SS.nakedEdges),
616                 SKdNode::NAKED_OR_SELF_INTER_EDGES, true, &inters, &leaks);
617 
618             InvalidateGraphics();
619 
620             const char *intersMsg = inters ?
621                 "The mesh is self-intersecting (NOT okay, invalid)." :
622                 "The mesh is not self-intersecting (okay, valid).";
623             const char *leaksMsg = leaks ?
624                 "The mesh has naked edges (NOT okay, invalid)." :
625                 "The mesh is watertight (okay, valid).";
626 
627             std::string cntMsg = ssprintf("\n\nThe model contains %d triangles, from "
628                             "%d surfaces.", g->displayMesh.l.n, g->runningShell.surface.n);
629 
630             if(SS.nakedEdges.l.n == 0) {
631                 Message("%s\n\n%s\n\nZero problematic edges, good.%s",
632                     intersMsg, leaksMsg, cntMsg.c_str());
633             } else {
634                 Error("%s\n\n%s\n\n%d problematic edges, bad.%s",
635                     intersMsg, leaksMsg, SS.nakedEdges.l.n, cntMsg.c_str());
636             }
637             break;
638         }
639 
640         case GraphicsWindow::MNU_INTERFERENCE: {
641             SS.nakedEdges.Clear();
642 
643             SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh);
644             SKdNode *root = SKdNode::From(m);
645             bool inters, leaks;
646             root->MakeCertainEdgesInto(&(SS.nakedEdges),
647                 SKdNode::SELF_INTER_EDGES, false, &inters, &leaks);
648 
649             InvalidateGraphics();
650 
651             if(inters) {
652                 Error("%d edges interfere with other triangles, bad.",
653                     SS.nakedEdges.l.n);
654             } else {
655                 Message("The assembly does not interfere, good.");
656             }
657             break;
658         }
659 
660         case GraphicsWindow::MNU_VOLUME: {
661             SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh);
662 
663             double vol = 0;
664             int i;
665             for(i = 0; i < m->l.n; i++) {
666                 STriangle tr = m->l.elem[i];
667 
668                 // Translate to place vertex A at (x, y, 0)
669                 Vector trans = Vector::From(tr.a.x, tr.a.y, 0);
670                 tr.a = (tr.a).Minus(trans);
671                 tr.b = (tr.b).Minus(trans);
672                 tr.c = (tr.c).Minus(trans);
673 
674                 // Rotate to place vertex B on the y-axis. Depending on
675                 // whether the triangle is CW or CCW, C is either to the
676                 // right or to the left of the y-axis. This handles the
677                 // sign of our normal.
678                 Vector u = Vector::From(-tr.b.y, tr.b.x, 0);
679                 u = u.WithMagnitude(1);
680                 Vector v = Vector::From(tr.b.x, tr.b.y, 0);
681                 v = v.WithMagnitude(1);
682                 Vector n = Vector::From(0, 0, 1);
683 
684                 tr.a = (tr.a).DotInToCsys(u, v, n);
685                 tr.b = (tr.b).DotInToCsys(u, v, n);
686                 tr.c = (tr.c).DotInToCsys(u, v, n);
687 
688                 n = tr.Normal().WithMagnitude(1);
689 
690                 // Triangles on edge don't contribute
691                 if(fabs(n.z) < LENGTH_EPS) continue;
692 
693                 // The plane has equation p dot n = a dot n
694                 double d = (tr.a).Dot(n);
695                 // nx*x + ny*y + nz*z = d
696                 // nz*z = d - nx*x - ny*y
697                 double A = -n.x/n.z, B = -n.y/n.z, C = d/n.z;
698 
699                 double mac = tr.c.y/tr.c.x, mbc = (tr.c.y - tr.b.y)/tr.c.x;
700                 double xc = tr.c.x, yb = tr.b.y;
701 
702                 // I asked Maple for
703                 //    int(int(A*x + B*y +C, y=mac*x..(mbc*x + yb)), x=0..xc);
704                 double integral =
705                     (1.0/3)*(
706                         A*(mbc-mac)+
707                         (1.0/2)*B*(mbc*mbc-mac*mac)
708                     )*(xc*xc*xc)+
709                     (1.0/2)*(A*yb+B*yb*mbc+C*(mbc-mac))*xc*xc+
710                     C*yb*xc+
711                     (1.0/2)*B*yb*yb*xc;
712 
713                 vol += integral;
714             }
715 
716             std::string msg = ssprintf("The volume of the solid model is:\n\n""    %.3f %s^3",
717                 vol / pow(SS.MmPerUnit(), 3),
718                 SS.UnitName());
719 
720             if(SS.viewUnits == SolveSpaceUI::UNIT_MM) {
721                 msg += ssprintf("\n    %.2f mL", vol/(10*10*10));
722             }
723             msg += "\n\nCurved surfaces have been approximated as triangles.\n"
724                    "This introduces error, typically of around 1%.";
725             Message("%s", msg.c_str());
726             break;
727         }
728 
729         case GraphicsWindow::MNU_AREA: {
730             Group *g = SK.GetGroup(SS.GW.activeGroup);
731             if(g->polyError.how != Group::POLY_GOOD) {
732                 Error("This group does not contain a correctly-formed "
733                       "2d closed area. It is open, not coplanar, or self-"
734                       "intersecting.");
735                 break;
736             }
737             SEdgeList sel = {};
738             g->polyLoops.MakeEdgesInto(&sel);
739             SPolygon sp = {};
740             sel.AssemblePolygon(&sp, NULL, true);
741             sp.normal = sp.ComputeNormal();
742             sp.FixContourDirections();
743             double area = sp.SignedArea();
744             double scale = SS.MmPerUnit();
745             Message("The area of the region sketched in this group is:\n\n"
746                     "    %.3f %s^2\n\n"
747                     "Curves have been approximated as piecewise linear.\n"
748                     "This introduces error, typically of around 1%%.",
749                 area / (scale*scale),
750                 SS.UnitName());
751             sel.Clear();
752             sp.Clear();
753             break;
754         }
755 
756         case GraphicsWindow::MNU_SHOW_DOF:
757             // This works like a normal solve, except that it calculates
758             // which variables are free/bound at the same time.
759             SS.GenerateAll(SolveSpaceUI::GENERATE_ALL, true);
760             break;
761 
762         case GraphicsWindow::MNU_TRACE_PT:
763             if(gs.points == 1 && gs.n == 1) {
764                 SS.traced.point = gs.point[0];
765                 SS.GW.ClearSelection();
766             } else {
767                 Error("Bad selection for trace; select a single point.");
768             }
769             break;
770 
771         case GraphicsWindow::MNU_STOP_TRACING: {
772             std::string exportFile;
773             if(GetSaveFile(&exportFile, "", CsvFileFilter)) {
774                 FILE *f = ssfopen(exportFile, "wb");
775                 if(f) {
776                     int i;
777                     SContour *sc = &(SS.traced.path);
778                     for(i = 0; i < sc->l.n; i++) {
779                         Vector p = sc->l.elem[i].p;
780                         double s = SS.exportScale;
781                         fprintf(f, "%.10f, %.10f, %.10f\r\n",
782                             p.x/s, p.y/s, p.z/s);
783                     }
784                     fclose(f);
785                 } else {
786                     Error("Couldn't write to '%s'", exportFile.c_str());
787                 }
788             }
789             // Clear the trace, and stop tracing
790             SS.traced.point = Entity::NO_ENTITY;
791             SS.traced.path.l.Clear();
792             InvalidateGraphics();
793             break;
794         }
795 
796         default: oops();
797     }
798 }
799 
MenuHelp(int id)800 void SolveSpaceUI::MenuHelp(int id) {
801     switch(id) {
802         case GraphicsWindow::MNU_WEBSITE:
803             OpenWebsite("http://solvespace.com/helpmenu");
804             break;
805 
806         case GraphicsWindow::MNU_ABOUT:
807             Message(
808 "This is SolveSpace version " PACKAGE_VERSION ".\n"
809 "\n"
810 "For more information, see http://solvespace.com/\n"
811 "\n"
812 "SolveSpace is free software: you are free to modify\n"
813 "and/or redistribute it under the terms of the GNU\n"
814 "General Public License (GPL) version 3 or later.\n"
815 "\n"
816 "There is NO WARRANTY, to the extent permitted by\n"
817 "law. For details, visit http://gnu.org/licenses/\n"
818 "\n"
819 "© 2008-2016 Jonathan Westhues and other authors.\n"
820 );
821             break;
822 
823         default: oops();
824     }
825 }
826 
Clear(void)827 void SolveSpaceUI::Clear(void) {
828     sys.Clear();
829     for(int i = 0; i < MAX_UNDO; i++) {
830         if(i < undo.cnt) undo.d[i].Clear();
831         if(i < redo.cnt) redo.d[i].Clear();
832     }
833 }
834 
Clear(void)835 void Sketch::Clear(void) {
836     group.Clear();
837     groupOrder.Clear();
838     constraint.Clear();
839     request.Clear();
840     style.Clear();
841     entity.Clear();
842     param.Clear();
843 }
844 
CalculateEntityBBox(bool includingInvisible)845 BBox Sketch::CalculateEntityBBox(bool includingInvisible) {
846     BBox box = {};
847     bool first = true;
848     for(int i = 0; i < entity.n; i++) {
849         Entity *e = (Entity *)&entity.elem[i];
850         if(!(e->IsVisible() || includingInvisible)) continue;
851 
852         Vector point;
853         double r = 0.0;
854         if(e->IsPoint()) {
855             point = e->PointGetNum();
856         } else {
857             switch(e->type) {
858                 case Entity::ARC_OF_CIRCLE:
859                 case Entity::CIRCLE:
860                     r = e->CircleGetRadiusNum();
861                     point = GetEntity(e->point[0])->PointGetNum();
862                     break;
863                 default: continue;
864             }
865         }
866 
867         if(first) {
868             box.minp = point;
869             box.maxp = point;
870             box.Include(point, r);
871             first = false;
872         } else {
873             box.Include(point, r);
874         }
875     }
876     return box;
877 }
878 
GetRunningMeshGroupFor(hGroup h)879 Group *Sketch::GetRunningMeshGroupFor(hGroup h) {
880     Group *g = GetGroup(h);
881     while(g != NULL) {
882         if(g->IsMeshGroup()) {
883             return g;
884         }
885         g = g->PreviousGroup();
886     }
887     return NULL;
888 }
889