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