Solvespace
1228 строк · 45.6 Кб
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
10SolveSpaceUI SolveSpace::SS = {};
11Sketch SolveSpace::SK = {};
12
13void SolveSpaceUI::Init() {
14#if !defined(HEADLESS)
15// Check that the resource system works.
16dbp("%s", LoadString("banner.txt").data());
17#endif
18
19Platform::SettingsRef settings = Platform::GetSettings();
20
21SS.tangentArcRadius = 10.0;
22SS.explodeDistance = 1.0;
23
24// Then, load the registry settings.
25// Default list of colors for the model material
26modelColor[0] = settings->ThawColor("ModelColor_0", RGBi(150, 150, 150));
27modelColor[1] = settings->ThawColor("ModelColor_1", RGBi(100, 100, 100));
28modelColor[2] = settings->ThawColor("ModelColor_2", RGBi( 30, 30, 30));
29modelColor[3] = settings->ThawColor("ModelColor_3", RGBi(150, 0, 0));
30modelColor[4] = settings->ThawColor("ModelColor_4", RGBi( 0, 100, 0));
31modelColor[5] = settings->ThawColor("ModelColor_5", RGBi( 0, 80, 80));
32modelColor[6] = settings->ThawColor("ModelColor_6", RGBi( 0, 0, 130));
33modelColor[7] = settings->ThawColor("ModelColor_7", RGBi( 80, 0, 80));
34// Light intensities
35lightIntensity[0] = settings->ThawFloat("LightIntensity_0", 1.0);
36lightIntensity[1] = settings->ThawFloat("LightIntensity_1", 0.5);
37ambientIntensity = settings->ThawFloat("Light_Ambient", 0.3);
38// Light positions
39lightDir[0].x = settings->ThawFloat("LightDir_0_Right", -1.0);
40lightDir[0].y = settings->ThawFloat("LightDir_0_Up", 1.0);
41lightDir[0].z = settings->ThawFloat("LightDir_0_Forward", 0.0);
42lightDir[1].x = settings->ThawFloat("LightDir_1_Right", 1.0);
43lightDir[1].y = settings->ThawFloat("LightDir_1_Up", 0.0);
44lightDir[1].z = settings->ThawFloat("LightDir_1_Forward", 0.0);
45
46exportMode = false;
47// Chord tolerance
48chordTol = settings->ThawFloat("ChordTolerancePct", 0.1);
49// Max pwl segments to generate
50maxSegments = settings->ThawInt("MaxSegments", 20);
51// Chord tolerance
52exportChordTol = settings->ThawFloat("ExportChordTolerance", 0.1);
53// Max pwl segments to generate
54exportMaxSegments = settings->ThawInt("ExportMaxSegments", 64);
55// Timeout value for finding redundant constrains (ms)
56timeoutRedundantConstr = settings->ThawInt("TimeoutRedundantConstraints", 1000);
57// View units
58viewUnits = (Unit)settings->ThawInt("ViewUnits", (uint32_t)Unit::MM);
59// Number of digits after the decimal point
60afterDecimalMm = settings->ThawInt("AfterDecimalMm", 2);
61afterDecimalInch = settings->ThawInt("AfterDecimalInch", 3);
62afterDecimalDegree = settings->ThawInt("AfterDecimalDegree", 2);
63useSIPrefixes = settings->ThawBool("UseSIPrefixes", false);
64// Camera tangent (determines perspective)
65cameraTangent = settings->ThawFloat("CameraTangent", 0.3f/1e3);
66// Grid spacing
67gridSpacing = settings->ThawFloat("GridSpacing", 5.0);
68// Export scale factor
69exportScale = settings->ThawFloat("ExportScale", 1.0);
70// Export offset (cutter radius comp)
71exportOffset = settings->ThawFloat("ExportOffset", 0.0);
72// Dimensions on arcs default to diameter vs radius
73arcDimDefaultDiameter = settings->ThawBool("ArcDimDefaultDiameter", false);
74// Show full file path in the menu bar
75showFullFilePath = settings->ThawBool("ShowFullFilePath", true);
76// Rewrite exported colors close to white into black (assuming white bg)
77fixExportColors = settings->ThawBool("FixExportColors", true);
78// Export background color
79exportBackgroundColor = settings->ThawBool("ExportBackgroundColor", false);
80// Draw back faces of triangles (when mesh is leaky/self-intersecting)
81drawBackFaces = settings->ThawBool("DrawBackFaces", true);
82// Use camera mouse navigation
83cameraNav = settings->ThawBool("CameraNav", false);
84// Use turntable mouse navigation
85turntableNav = settings->ThawBool("TurntableNav", false);
86// Immediately edit dimension
87immediatelyEditDimension = settings->ThawBool("ImmediatelyEditDimension", true);
88// Check that contours are closed and not self-intersecting
89checkClosedContour = settings->ThawBool("CheckClosedContour", true);
90// Enable automatic constrains for lines
91automaticLineConstraints = settings->ThawBool("AutomaticLineConstraints", true);
92// Draw closed polygons areas
93showContourAreas = settings->ThawBool("ShowContourAreas", false);
94// Export shaded triangles in a 2d view
95exportShadedTriangles = settings->ThawBool("ExportShadedTriangles", true);
96// Export pwl curves (instead of exact) always
97exportPwlCurves = settings->ThawBool("ExportPwlCurves", false);
98// Background color on-screen
99backgroundColor = settings->ThawColor("BackgroundColor", RGBi(0, 0, 0));
100// Whether export canvas size is fixed or derived from bbox
101exportCanvasSizeAuto = settings->ThawBool("ExportCanvasSizeAuto", true);
102// Margins for automatic canvas size
103exportMargin.left = settings->ThawFloat("ExportMargin_Left", 5.0);
104exportMargin.right = settings->ThawFloat("ExportMargin_Right", 5.0);
105exportMargin.bottom = settings->ThawFloat("ExportMargin_Bottom", 5.0);
106exportMargin.top = settings->ThawFloat("ExportMargin_Top", 5.0);
107// Dimensions for fixed canvas size
108exportCanvas.width = settings->ThawFloat("ExportCanvas_Width", 100.0);
109exportCanvas.height = settings->ThawFloat("ExportCanvas_Height", 100.0);
110exportCanvas.dx = settings->ThawFloat("ExportCanvas_Dx", 5.0);
111exportCanvas.dy = settings->ThawFloat("ExportCanvas_Dy", 5.0);
112// Extra parameters when exporting G code
113gCode.depth = settings->ThawFloat("GCode_Depth", 10.0);
114gCode.safeHeight = settings->ThawFloat("GCode_SafeHeight", 5.0);
115gCode.passes = settings->ThawInt("GCode_Passes", 1);
116gCode.feed = settings->ThawFloat("GCode_Feed", 10.0);
117gCode.plungeFeed = settings->ThawFloat("GCode_PlungeFeed", 10.0);
118// Show toolbar in the graphics window
119showToolbar = settings->ThawBool("ShowToolbar", true);
120// Recent files menus
121for(size_t i = 0; i < MAX_RECENT; i++) {
122std::string rawPath = settings->ThawString("RecentFile_" + std::to_string(i), "");
123if(rawPath.empty()) continue;
124recentFiles.push_back(Platform::Path::From(rawPath));
125}
126// Autosave timer
127autosaveInterval = settings->ThawInt("AutosaveInterval", 5);
128// Locale
129std::string locale = settings->ThawString("Locale", "");
130if(!locale.empty()) {
131SetLocale(locale);
132}
133
134refreshTimer = Platform::CreateTimer();
135refreshTimer->onTimeout = std::bind(&SolveSpaceUI::Refresh, &SS);
136
137autosaveTimer = Platform::CreateTimer();
138autosaveTimer->onTimeout = std::bind(&SolveSpaceUI::Autosave, &SS);
139
140// The default styles (colors, line widths, etc.) are also stored in the
141// configuration file, but we will automatically load those as we need
142// them.
143
144ScheduleAutosave();
145
146NewFile();
147AfterNewFile();
148
149if(TW.window && GW.window) {
150TW.window->ThawPosition(settings, "TextWindow");
151GW.window->ThawPosition(settings, "GraphicsWindow");
152TW.window->SetVisible(true);
153GW.window->SetVisible(true);
154GW.window->Focus();
155
156// Do this once the window is created.
157Request3DConnexionEventsForWindow(GW.window);
158}
159}
160
161bool SolveSpaceUI::LoadAutosaveFor(const Platform::Path &filename) {
162Platform::Path autosaveFile = filename.WithExtension(BACKUP_EXT);
163
164FILE *f = OpenFile(autosaveFile, "rb");
165if(!f)
166return false;
167fclose(f);
168
169Platform::MessageDialogRef dialog = CreateMessageDialog(GW.window);
170
171using Platform::MessageDialog;
172dialog->SetType(MessageDialog::Type::QUESTION);
173dialog->SetTitle(C_("title", "Autosave Available"));
174dialog->SetMessage(C_("dialog", "An autosave file is available for this sketch."));
175dialog->SetDescription(C_("dialog", "Do you want to load the autosave file instead?"));
176dialog->AddButton(C_("button", "&Load autosave"), MessageDialog::Response::YES,
177/*isDefault=*/true);
178dialog->AddButton(C_("button", "Do&n't Load"), MessageDialog::Response::NO);
179
180// FIXME(async): asyncify this call
181if(dialog->RunModal() == MessageDialog::Response::YES) {
182unsaved = true;
183return LoadFromFile(autosaveFile, /*canCancel=*/true);
184}
185
186return false;
187}
188
189bool SolveSpaceUI::Load(const Platform::Path &filename) {
190bool autosaveLoaded = LoadAutosaveFor(filename);
191bool fileLoaded = autosaveLoaded || LoadFromFile(filename, /*canCancel=*/true);
192if(fileLoaded) {
193saveFile = filename;
194AddToRecentList(filename);
195} else {
196saveFile.Clear();
197NewFile();
198}
199AfterNewFile();
200unsaved = autosaveLoaded;
201return fileLoaded;
202}
203
204void SolveSpaceUI::Exit() {
205Platform::SettingsRef settings = Platform::GetSettings();
206
207GW.window->FreezePosition(settings, "GraphicsWindow");
208TW.window->FreezePosition(settings, "TextWindow");
209
210// Recent files
211for(size_t i = 0; i < MAX_RECENT; i++) {
212std::string rawPath;
213if(recentFiles.size() > i) {
214rawPath = recentFiles[i].raw;
215}
216settings->FreezeString("RecentFile_" + std::to_string(i), rawPath);
217}
218// Model colors
219for(size_t i = 0; i < MODEL_COLORS; i++)
220settings->FreezeColor("ModelColor_" + std::to_string(i), modelColor[i]);
221// Light intensities
222settings->FreezeFloat("LightIntensity_0", (float)lightIntensity[0]);
223settings->FreezeFloat("LightIntensity_1", (float)lightIntensity[1]);
224settings->FreezeFloat("Light_Ambient", (float)ambientIntensity);
225// Light directions
226settings->FreezeFloat("LightDir_0_Right", (float)lightDir[0].x);
227settings->FreezeFloat("LightDir_0_Up", (float)lightDir[0].y);
228settings->FreezeFloat("LightDir_0_Forward", (float)lightDir[0].z);
229settings->FreezeFloat("LightDir_1_Right", (float)lightDir[1].x);
230settings->FreezeFloat("LightDir_1_Up", (float)lightDir[1].y);
231settings->FreezeFloat("LightDir_1_Forward", (float)lightDir[1].z);
232// Chord tolerance
233settings->FreezeFloat("ChordTolerancePct", (float)chordTol);
234// Max pwl segments to generate
235settings->FreezeInt("MaxSegments", (uint32_t)maxSegments);
236// Export Chord tolerance
237settings->FreezeFloat("ExportChordTolerance", (float)exportChordTol);
238// Export Max pwl segments to generate
239settings->FreezeInt("ExportMaxSegments", (uint32_t)exportMaxSegments);
240// Timeout for finding which constraints to fix Jacobian
241settings->FreezeInt("TimeoutRedundantConstraints", (uint32_t)timeoutRedundantConstr);
242// View units
243settings->FreezeInt("ViewUnits", (uint32_t)viewUnits);
244// Number of digits after the decimal point
245settings->FreezeInt("AfterDecimalMm", (uint32_t)afterDecimalMm);
246settings->FreezeInt("AfterDecimalInch", (uint32_t)afterDecimalInch);
247settings->FreezeInt("AfterDecimalDegree", (uint32_t)afterDecimalDegree);
248settings->FreezeBool("UseSIPrefixes", useSIPrefixes);
249// Camera tangent (determines perspective)
250settings->FreezeFloat("CameraTangent", (float)cameraTangent);
251// Grid spacing
252settings->FreezeFloat("GridSpacing", gridSpacing);
253// Export scale
254settings->FreezeFloat("ExportScale", exportScale);
255// Export offset (cutter radius comp)
256settings->FreezeFloat("ExportOffset", exportOffset);
257// Rewrite the default arc dimension setting
258settings->FreezeBool("ArcDimDefaultDiameter", arcDimDefaultDiameter);
259// Show full file path in the menu bar
260settings->FreezeBool("ShowFullFilePath", showFullFilePath);
261// Rewrite exported colors close to white into black (assuming white bg)
262settings->FreezeBool("FixExportColors", fixExportColors);
263// Export background color
264settings->FreezeBool("ExportBackgroundColor", exportBackgroundColor);
265// Draw back faces of triangles (when mesh is leaky/self-intersecting)
266settings->FreezeBool("DrawBackFaces", drawBackFaces);
267// Draw closed polygons areas
268settings->FreezeBool("ShowContourAreas", showContourAreas);
269// Check that contours are closed and not self-intersecting
270settings->FreezeBool("CheckClosedContour", checkClosedContour);
271// Use camera mouse navigation
272settings->FreezeBool("CameraNav", cameraNav);
273// Use turntable mouse navigation
274settings->FreezeBool("TurntableNav", turntableNav);
275// Immediately edit dimensions
276settings->FreezeBool("ImmediatelyEditDimension", immediatelyEditDimension);
277// Enable automatic constrains for lines
278settings->FreezeBool("AutomaticLineConstraints", automaticLineConstraints);
279// Export shaded triangles in a 2d view
280settings->FreezeBool("ExportShadedTriangles", exportShadedTriangles);
281// Export pwl curves (instead of exact) always
282settings->FreezeBool("ExportPwlCurves", exportPwlCurves);
283// Background color on-screen
284settings->FreezeColor("BackgroundColor", backgroundColor);
285// Whether export canvas size is fixed or derived from bbox
286settings->FreezeBool("ExportCanvasSizeAuto", exportCanvasSizeAuto);
287// Margins for automatic canvas size
288settings->FreezeFloat("ExportMargin_Left", exportMargin.left);
289settings->FreezeFloat("ExportMargin_Right", exportMargin.right);
290settings->FreezeFloat("ExportMargin_Bottom", exportMargin.bottom);
291settings->FreezeFloat("ExportMargin_Top", exportMargin.top);
292// Dimensions for fixed canvas size
293settings->FreezeFloat("ExportCanvas_Width", exportCanvas.width);
294settings->FreezeFloat("ExportCanvas_Height", exportCanvas.height);
295settings->FreezeFloat("ExportCanvas_Dx", exportCanvas.dx);
296settings->FreezeFloat("ExportCanvas_Dy", exportCanvas.dy);
297// Extra parameters when exporting G code
298settings->FreezeFloat("GCode_Depth", gCode.depth);
299settings->FreezeInt("GCode_Passes", gCode.passes);
300settings->FreezeFloat("GCode_Feed", gCode.feed);
301settings->FreezeFloat("GCode_PlungeFeed", gCode.plungeFeed);
302// Show toolbar in the graphics window
303settings->FreezeBool("ShowToolbar", showToolbar);
304// Autosave timer
305settings->FreezeInt("AutosaveInterval", autosaveInterval);
306
307// And the default styles, colors and line widths and such.
308Style::FreezeDefaultStyles(settings);
309
310Platform::ExitGui();
311}
312
313void SolveSpaceUI::Refresh() {
314// generateAll must happen bfore updating displays
315if(scheduledGenerateAll) {
316// Clear the flag so that if the call to GenerateAll is blocked by a Message or Error,
317// subsequent refreshes do not try to Generate again.
318scheduledGenerateAll = false;
319GenerateAll(Generate::DIRTY, /*andFindFree=*/false, /*genForBBox=*/false);
320}
321if(scheduledShowTW) {
322scheduledShowTW = false;
323TW.Show();
324}
325}
326
327void SolveSpaceUI::ScheduleGenerateAll() {
328scheduledGenerateAll = true;
329refreshTimer->RunAfterProcessingEvents();
330}
331
332void SolveSpaceUI::ScheduleShowTW() {
333scheduledShowTW = true;
334refreshTimer->RunAfterProcessingEvents();
335}
336
337void SolveSpaceUI::ScheduleAutosave() {
338autosaveTimer->RunAfter(autosaveInterval * 60 * 1000);
339}
340
341double SolveSpaceUI::MmPerUnit() {
342switch(viewUnits) {
343case Unit::INCHES: return 25.4;
344case Unit::FEET_INCHES: return 25.4; // The 'unit' is still inches
345case Unit::METERS: return 1000.0;
346case Unit::MM: return 1.0;
347}
348return 1.0;
349}
350const char *SolveSpaceUI::UnitName() {
351switch(viewUnits) {
352case Unit::INCHES: return "in";
353case Unit::FEET_INCHES: return "in";
354case Unit::METERS: return "m";
355case Unit::MM: return "mm";
356}
357return "";
358}
359
360std::string SolveSpaceUI::MmToString(double v, bool editable) {
361v /= MmPerUnit();
362// The syntax 2' 6" for feet and inches is not something we can (currently)
363// parse back from a string so if editable is true, we treat FEET_INCHES the
364// same as INCHES and just return the unadorned decimal number of inches.
365if(viewUnits == Unit::FEET_INCHES && !editable) {
366// Now convert v from inches to 64'ths of an inch, to make rounding easier.
367v = floor((v + (1.0 / 128.0)) * 64.0);
368int feet = (int)(v / (12.0 * 64.0));
369v = v - (feet * 12.0 * 64.0);
370// v is now the feet-less remainder in 1/64 inches
371int inches = (int)(v / 64.0);
372int numerator = (int)(v - ((double)inches * 64.0));
373int denominator = 64;
374// Divide down to smallest denominator where the numerator is still a whole number
375while ((numerator != 0) && ((numerator & 1) == 0)) {
376numerator /= 2;
377denominator /= 2;
378}
379std::ostringstream str;
380if(feet != 0) {
381str << feet << "'-";
382}
383// For something like 0.5, show 1/2" rather than 0 1/2"
384if(!(feet == 0 && inches == 0 && numerator != 0)) {
385str << inches;
386}
387if(numerator != 0) {
388str << " " << numerator << "/" << denominator;
389}
390str << "\"";
391return str.str();
392}
393
394int digits = UnitDigitsAfterDecimal();
395double minimum = 0.5 * pow(10,-digits);
396while ((v < minimum) && (v > LENGTH_EPS)) {
397digits++;
398minimum *= 0.1;
399}
400return ssprintf("%.*f", digits, v);
401}
402static const char *DimToString(int dim) {
403switch(dim) {
404case 3: return "³";
405case 2: return "²";
406case 1: return "";
407default: ssassert(false, "Unexpected dimension");
408}
409}
410static std::pair<int, std::string> SelectSIPrefixMm(int ord, int dim) {
411// decide what units to use depending on the order of magnitude of the
412// measure in meters and the dimension (1,2,3 lenear, area, volume)
413switch(dim) {
414case 0:
415case 1:
416if(ord >= 3) return { 3, "km" };
417else if(ord >= 0) return { 0, "m" };
418else if(ord >= -2) return { -2, "cm" };
419else if(ord >= -3) return { -3, "mm" };
420else if(ord >= -6) return { -6, "µm" };
421else return { -9, "nm" };
422break;
423case 2:
424if(ord >= 5) return { 3, "km" };
425else if(ord >= 0) return { 0, "m" };
426else if(ord >= -2) return { -2, "cm" };
427else if(ord >= -6) return { -3, "mm" };
428else if(ord >= -13) return { -6, "µm" };
429else return { -9, "nm" };
430break;
431case 3:
432if(ord >= 7) return { 3, "km" };
433else if(ord >= 0) return { 0, "m" };
434else if(ord >= -5) return { -2, "cm" };
435else if(ord >= -11) return { -3, "mm" };
436else return { -6, "µm" };
437break;
438default:
439dbp ("dimensions over 3 not supported");
440break;
441}
442return {0, "m"};
443}
444static std::pair<int, std::string> SelectSIPrefixInch(int deg) {
445if(deg >= 0) return { 0, "in" };
446else if(deg >= -3) return { -3, "mil" };
447else return { -6, "µin" };
448}
449std::string SolveSpaceUI::MmToStringSI(double v, int dim) {
450bool compact = false;
451if(dim == 0) {
452if(!useSIPrefixes) return MmToString(v);
453compact = true;
454dim = 1;
455}
456
457bool inches = (viewUnits == Unit::INCHES) || (viewUnits == Unit::FEET_INCHES);
458v /= pow(inches ? 25.4 : 1000, dim);
459int vdeg = (int)(log10(fabs(v)));
460std::string unit;
461if(fabs(v) > 0.0) {
462int sdeg = 0;
463std::tie(sdeg, unit) =
464inches
465? SelectSIPrefixInch(vdeg/dim)
466: SelectSIPrefixMm(vdeg, dim);
467v /= pow(10.0, sdeg * dim);
468}
469if(viewUnits == Unit::FEET_INCHES && fabs(v) > pow(12.0, dim)) {
470unit = "ft";
471v /= pow(12.0, dim);
472}
473int pdeg = (int)ceil(log10(fabs(v) + 1e-10));
474return ssprintf("%.*g%s%s%s", pdeg + UnitDigitsAfterDecimal(), v,
475compact ? "" : " ", unit.c_str(), DimToString(dim));
476}
477std::string SolveSpaceUI::DegreeToString(double v) {
478if(fabs(v - floor(v)) > 1e-10) {
479return ssprintf("%.*f", afterDecimalDegree, v);
480} else {
481return ssprintf("%.0f", v);
482}
483}
484double SolveSpaceUI::ExprToMm(Expr *e) {
485return (e->Eval()) * MmPerUnit();
486}
487double SolveSpaceUI::StringToMm(const std::string &str) {
488return std::stod(str) * MmPerUnit();
489}
490double SolveSpaceUI::ChordTolMm() {
491if(exportMode) return ExportChordTolMm();
492return chordTolCalculated;
493}
494double SolveSpaceUI::ExportChordTolMm() {
495return exportChordTol / exportScale;
496}
497int SolveSpaceUI::GetMaxSegments() {
498if(exportMode) return exportMaxSegments;
499return maxSegments;
500}
501int SolveSpaceUI::UnitDigitsAfterDecimal() {
502return (viewUnits == Unit::INCHES || viewUnits == Unit::FEET_INCHES) ?
503afterDecimalInch : afterDecimalMm;
504}
505void SolveSpaceUI::SetUnitDigitsAfterDecimal(int v) {
506if(viewUnits == Unit::INCHES || viewUnits == Unit::FEET_INCHES) {
507afterDecimalInch = v;
508} else {
509afterDecimalMm = v;
510}
511}
512
513double SolveSpaceUI::CameraTangent() {
514if(!usePerspectiveProj) {
515return 0;
516} else {
517return cameraTangent;
518}
519}
520
521void SolveSpaceUI::AfterNewFile() {
522// Clear out the traced point, which is no longer valid
523traced.point = Entity::NO_ENTITY;
524traced.path.l.Clear();
525// and the naked edges
526nakedEdges.Clear();
527
528// Quit export mode
529justExportedInfo.draw = false;
530centerOfMass.draw = false;
531exportMode = false;
532
533// GenerateAll() expects the view to be valid, because it uses that to
534// fill in default values for extrusion depths etc. (which won't matter
535// here, but just don't let it work on garbage)
536SS.GW.offset = Vector::From(0, 0, 0);
537SS.GW.projRight = Vector::From(1, 0, 0);
538SS.GW.projUp = Vector::From(0, 1, 0);
539
540GenerateAll(Generate::ALL);
541
542GW.Init();
543TW.Init();
544
545unsaved = false;
546
547GW.ZoomToFit();
548
549// Create all the default styles; they'll get created on the fly anyways,
550// but can't hurt to do it now.
551Style::CreateAllDefaultStyles();
552
553UpdateWindowTitles();
554}
555
556void SolveSpaceUI::AddToRecentList(const Platform::Path &filename) {
557auto it = std::find_if(recentFiles.begin(), recentFiles.end(),
558[&](const Platform::Path &p) { return p.Equals(filename); });
559if(it != recentFiles.end()) {
560recentFiles.erase(it);
561}
562
563if(recentFiles.size() > MAX_RECENT) {
564recentFiles.erase(recentFiles.begin() + MAX_RECENT);
565}
566
567recentFiles.insert(recentFiles.begin(), filename);
568GW.PopulateRecentFiles();
569}
570
571bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
572Platform::SettingsRef settings = Platform::GetSettings();
573Platform::Path newSaveFile = saveFile;
574
575if(saveAs || saveFile.IsEmpty()) {
576Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(GW.window);
577// FIXME(emscripten):
578dbp("Calling AddFilter()...");
579dialog->AddFilter(C_("file-type", "SolveSpace models"), { SKETCH_EXT });
580dbp("Calling ThawChoices()...");
581dialog->ThawChoices(settings, "Sketch");
582if(!newSaveFile.IsEmpty()) {
583dbp("Calling SetFilename()...");
584dialog->SetFilename(newSaveFile);
585}
586dbp("Calling RunModal()...");
587if(dialog->RunModal()) {
588dbp("Calling FreezeChoices()...");
589dialog->FreezeChoices(settings, "Sketch");
590newSaveFile = dialog->GetFilename();
591} else {
592return false;
593}
594}
595
596if(SaveToFile(newSaveFile)) {
597AddToRecentList(newSaveFile);
598RemoveAutosave();
599saveFile = newSaveFile;
600unsaved = false;
601if (this->OnSaveFinished) {
602this->OnSaveFinished(newSaveFile, saveAs, false);
603}
604return true;
605} else {
606return false;
607}
608}
609
610void SolveSpaceUI::Autosave()
611{
612ScheduleAutosave();
613
614if(!saveFile.IsEmpty() && unsaved) {
615Platform::Path saveFileName = saveFile.WithExtension(BACKUP_EXT);
616SaveToFile(saveFileName);
617if (this->OnSaveFinished) {
618this->OnSaveFinished(saveFileName, false, true);
619}
620}
621}
622
623void SolveSpaceUI::RemoveAutosave()
624{
625Platform::Path autosaveFile = saveFile.WithExtension(BACKUP_EXT);
626RemoveFile(autosaveFile);
627}
628
629bool SolveSpaceUI::OkayToStartNewFile() {
630if(!unsaved) return true;
631
632Platform::MessageDialogRef dialog = CreateMessageDialog(GW.window);
633
634using Platform::MessageDialog;
635dialog->SetType(MessageDialog::Type::QUESTION);
636dialog->SetTitle(C_("title", "Modified File"));
637if(!SolveSpace::SS.saveFile.IsEmpty()) {
638dialog->SetMessage(ssprintf(C_("dialog", "Do you want to save the changes you made to "
639"the sketch “%s”?"), saveFile.raw.c_str()));
640} else {
641dialog->SetMessage(C_("dialog", "Do you want to save the changes you made to "
642"the new sketch?"));
643}
644dialog->SetDescription(C_("dialog", "Your changes will be lost if you don't save them."));
645dialog->AddButton(C_("button", "&Save"), MessageDialog::Response::YES,
646/*isDefault=*/true);
647dialog->AddButton(C_("button", "Do&n't Save"), MessageDialog::Response::NO);
648dialog->AddButton(C_("button", "&Cancel"), MessageDialog::Response::CANCEL);
649
650// FIXME(async): asyncify this call
651switch(dialog->RunModal()) {
652case MessageDialog::Response::YES:
653return GetFilenameAndSave(/*saveAs=*/false);
654
655case MessageDialog::Response::NO:
656RemoveAutosave();
657return true;
658
659default:
660return false;
661}
662}
663
664void SolveSpaceUI::UpdateWindowTitles() {
665if(!GW.window || !TW.window) return;
666
667if(saveFile.IsEmpty()) {
668GW.window->SetTitle(C_("title", "(new sketch)"));
669} else {
670if(!GW.window->SetTitleForFilename(saveFile)) {
671if(SS.showFullFilePath) {
672GW.window->SetTitle(saveFile.raw);
673} else {
674GW.window->SetTitle(saveFile.raw.substr(saveFile.raw.find_last_of("/\\") + 1));
675}
676}
677}
678
679TW.window->SetTitle(C_("title", "Property Browser"));
680}
681
682void SolveSpaceUI::MenuFile(Command id) {
683Platform::SettingsRef settings = Platform::GetSettings();
684
685switch(id) {
686case Command::NEW:
687if(!SS.OkayToStartNewFile()) break;
688
689SS.saveFile.Clear();
690SS.NewFile();
691SS.AfterNewFile();
692break;
693
694case Command::OPEN: {
695if(!SS.OkayToStartNewFile()) break;
696
697Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window);
698dialog->AddFilters(Platform::SolveSpaceModelFileFilters);
699dialog->ThawChoices(settings, "Sketch");
700if(dialog->RunModal()) {
701dialog->FreezeChoices(settings, "Sketch");
702SS.Load(dialog->GetFilename());
703}
704break;
705}
706
707case Command::SAVE:
708SS.GetFilenameAndSave(/*saveAs=*/false);
709break;
710
711case Command::SAVE_AS:
712SS.GetFilenameAndSave(/*saveAs=*/true);
713break;
714
715case Command::EXPORT_IMAGE: {
716Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
717dialog->AddFilters(Platform::RasterFileFilters);
718dialog->ThawChoices(settings, "ExportImage");
719dialog->SuggestFilename(SS.saveFile);
720if(dialog->RunModal()) {
721dialog->FreezeChoices(settings, "ExportImage");
722SS.ExportAsPngTo(dialog->GetFilename());
723if (SS.OnSaveFinished) {
724SS.OnSaveFinished(dialog->GetFilename(), false, false);
725}
726}
727break;
728}
729
730case Command::EXPORT_VIEW: {
731Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
732dialog->AddFilters(Platform::VectorFileFilters);
733dialog->ThawChoices(settings, "ExportView");
734dialog->SuggestFilename(SS.saveFile);
735if(!dialog->RunModal()) break;
736dialog->FreezeChoices(settings, "ExportView");
737
738// If the user is exporting something where it would be
739// inappropriate to include the constraints, then warn.
740if(SS.GW.showConstraints != GraphicsWindow::ShowConstraintMode::SCM_NOSHOW &&
741(dialog->GetFilename().HasExtension("txt") || fabs(SS.exportOffset) > LENGTH_EPS)) {
742Message(_("Constraints are currently shown, and will be exported "
743"in the toolpath. This is probably not what you want; "
744"hide them by clicking the link at the top of the "
745"text window."));
746}
747
748SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe=*/false);
749if (SS.OnSaveFinished) {
750SS.OnSaveFinished(dialog->GetFilename(), false, false);
751}
752break;
753}
754
755case Command::EXPORT_WIREFRAME: {
756Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
757dialog->AddFilters(Platform::Vector3dFileFilters);
758dialog->ThawChoices(settings, "ExportWireframe");
759dialog->SuggestFilename(SS.saveFile);
760if(!dialog->RunModal()) break;
761dialog->FreezeChoices(settings, "ExportWireframe");
762
763SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe*/true);
764if (SS.OnSaveFinished) {
765SS.OnSaveFinished(dialog->GetFilename(), false, false);
766}
767break;
768}
769
770case Command::EXPORT_SECTION: {
771Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
772dialog->AddFilters(Platform::VectorFileFilters);
773dialog->ThawChoices(settings, "ExportSection");
774dialog->SuggestFilename(SS.saveFile);
775if(!dialog->RunModal()) break;
776dialog->FreezeChoices(settings, "ExportSection");
777
778SS.ExportSectionTo(dialog->GetFilename());
779if (SS.OnSaveFinished) {
780SS.OnSaveFinished(dialog->GetFilename(), false, false);
781}
782break;
783}
784
785case Command::EXPORT_MESH: {
786Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
787dialog->AddFilters(Platform::MeshFileFilters);
788dialog->ThawChoices(settings, "ExportMesh");
789dialog->SuggestFilename(SS.saveFile);
790if(!dialog->RunModal()) break;
791dialog->FreezeChoices(settings, "ExportMesh");
792
793SS.ExportMeshTo(dialog->GetFilename());
794if (SS.OnSaveFinished) {
795SS.OnSaveFinished(dialog->GetFilename(), false, false);
796}
797
798break;
799}
800
801case Command::EXPORT_SURFACES: {
802Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
803dialog->AddFilters(Platform::SurfaceFileFilters);
804dialog->ThawChoices(settings, "ExportSurfaces");
805dialog->SuggestFilename(SS.saveFile);
806if(!dialog->RunModal()) break;
807dialog->FreezeChoices(settings, "ExportSurfaces");
808
809StepFileWriter sfw = {};
810sfw.ExportSurfacesTo(dialog->GetFilename());
811if (SS.OnSaveFinished) {
812SS.OnSaveFinished(dialog->GetFilename(), false, false);
813}
814break;
815}
816
817case Command::IMPORT: {
818Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window);
819dialog->AddFilters(Platform::ImportFileFilters);
820dialog->ThawChoices(settings, "Import");
821if(!dialog->RunModal()) break;
822dialog->FreezeChoices(settings, "Import");
823
824Platform::Path importFile = dialog->GetFilename();
825if(importFile.HasExtension("dxf")) {
826ImportDxf(importFile);
827} else if(importFile.HasExtension("dwg")) {
828ImportDwg(importFile);
829} else {
830Error(_("Can't identify file type from file extension of "
831"filename '%s'; try .dxf or .dwg."), importFile.raw.c_str());
832break;
833}
834
835SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE);
836SS.ScheduleShowTW();
837break;
838}
839
840case Command::EXIT:
841if(!SS.OkayToStartNewFile()) break;
842SS.Exit();
843break;
844
845default: ssassert(false, "Unexpected menu ID");
846}
847
848SS.UpdateWindowTitles();
849}
850
851void SolveSpaceUI::MenuAnalyze(Command id) {
852Platform::SettingsRef settings = Platform::GetSettings();
853
854SS.GW.GroupSelection();
855auto const &gs = SS.GW.gs;
856
857switch(id) {
858case Command::STEP_DIM:
859if(gs.constraints == 1 && gs.n == 0) {
860Constraint *c = SK.GetConstraint(gs.constraint[0]);
861if(c->HasLabel() && !c->reference) {
862SS.TW.stepDim.finish = c->valA;
863SS.TW.stepDim.steps = 10;
864SS.TW.stepDim.isDistance =
865(c->type != Constraint::Type::ANGLE) &&
866(c->type != Constraint::Type::LENGTH_RATIO) &&
867(c->type != Constraint::Type::ARC_ARC_LEN_RATIO) &&
868(c->type != Constraint::Type::ARC_LINE_LEN_RATIO) &&
869(c->type != Constraint::Type::LENGTH_DIFFERENCE) &&
870(c->type != Constraint::Type::ARC_ARC_DIFFERENCE) &&
871(c->type != Constraint::Type::ARC_LINE_DIFFERENCE) ;
872SS.TW.shown.constraint = c->h;
873SS.TW.shown.screen = TextWindow::Screen::STEP_DIMENSION;
874
875// The step params are specified in the text window,
876// so force that to be shown.
877SS.GW.ForceTextWindowShown();
878
879SS.ScheduleShowTW();
880SS.GW.ClearSelection();
881} else {
882Error(_("Constraint must have a label, and must not be "
883"a reference dimension."));
884}
885} else {
886Error(_("Bad selection for step dimension; select a constraint."));
887}
888break;
889
890case Command::NAKED_EDGES: {
891ShowNakedEdges(/*reportOnlyWhenNotOkay=*/false);
892break;
893}
894
895case Command::INTERFERENCE: {
896SS.nakedEdges.Clear();
897
898SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh);
899SKdNode *root = SKdNode::From(m);
900bool inters, leaks;
901root->MakeCertainEdgesInto(&(SS.nakedEdges),
902EdgeKind::SELF_INTER, /*coplanarIsInter=*/false, &inters, &leaks);
903
904SS.GW.Invalidate();
905
906if(inters) {
907Error("%d edges interfere with other triangles, bad.",
908SS.nakedEdges.l.n);
909} else {
910Message(_("The assembly does not interfere, good."));
911}
912break;
913}
914
915case Command::CENTER_OF_MASS: {
916SS.UpdateCenterOfMass();
917SS.centerOfMass.draw = true;
918SS.GW.Invalidate();
919break;
920}
921
922case Command::VOLUME: {
923Group *g = SK.GetGroup(SS.GW.activeGroup);
924double totalVol = g->displayMesh.CalculateVolume();
925std::string msg = ssprintf(
926_("The volume of the solid model is:\n\n"
927" %s"),
928SS.MmToStringSI(totalVol, /*dim=*/3).c_str());
929
930SMesh curMesh = {};
931g->thisShell.TriangulateInto(&curMesh);
932double curVol = curMesh.CalculateVolume();
933if(curVol > 0.0) {
934msg += ssprintf(
935_("\nThe volume of current group mesh is:\n\n"
936" %s"),
937SS.MmToStringSI(curVol, /*dim=*/3).c_str());
938}
939
940msg += _("\n\nCurved surfaces have been approximated as triangles.\n"
941"This introduces error, typically of around 1%.");
942Message("%s", msg.c_str());
943break;
944}
945
946case Command::AREA: {
947Group *g = SK.GetGroup(SS.GW.activeGroup);
948SS.GW.GroupSelection();
949
950if(gs.faces > 0) {
951std::vector<uint32_t> faces;
952faces.push_back(gs.face[0].v);
953if(gs.faces > 1) faces.push_back(gs.face[1].v);
954double area = g->displayMesh.CalculateSurfaceArea(faces);
955Message(_("The surface area of the selected faces is:\n\n"
956" %s\n\n"
957"Curves have been approximated as piecewise linear.\n"
958"This introduces error, typically of around 1%%."),
959SS.MmToStringSI(area, /*dim=*/2).c_str());
960break;
961}
962
963if(g->polyError.how != PolyError::GOOD) {
964Error(_("This group does not contain a correctly-formed "
965"2d closed area. It is open, not coplanar, or self-"
966"intersecting."));
967break;
968}
969SEdgeList sel = {};
970g->polyLoops.MakeEdgesInto(&sel);
971SPolygon sp = {};
972sel.AssemblePolygon(&sp, NULL, /*keepDir=*/true);
973sp.normal = sp.ComputeNormal();
974sp.FixContourDirections();
975double area = sp.SignedArea();
976Message(_("The area of the region sketched in this group is:\n\n"
977" %s\n\n"
978"Curves have been approximated as piecewise linear.\n"
979"This introduces error, typically of around 1%%."),
980SS.MmToStringSI(area, /*dim=*/2).c_str());
981sel.Clear();
982sp.Clear();
983break;
984}
985
986case Command::PERIMETER: {
987if(gs.n > 0 && gs.n == gs.entities) {
988double perimeter = 0.0;
989for(int i = 0; i < gs.entities; i++) {
990Entity *en = SK.entity.FindById(gs.entity[i]);
991SEdgeList *el = en->GetOrGenerateEdges();
992for(const SEdge &e : el->l) {
993perimeter += e.b.Minus(e.a).Magnitude();
994}
995}
996Message(_("The total length of the selected entities is:\n\n"
997" %s\n\n"
998"Curves have been approximated as piecewise linear.\n"
999"This introduces error, typically of around 1%%."),
1000SS.MmToStringSI(perimeter, /*dim=*/1).c_str());
1001} else {
1002Error(_("Bad selection for perimeter; select line segments, arcs, and curves."));
1003}
1004break;
1005}
1006
1007case Command::SHOW_DOF:
1008// This works like a normal solve, except that it calculates
1009// which variables are free/bound at the same time.
1010SS.GenerateAll(SolveSpaceUI::Generate::ALL, /*andFindFree=*/true);
1011break;
1012
1013case Command::TRACE_PT:
1014if(gs.points == 1 && gs.n == 1) {
1015SS.traced.point = gs.point[0];
1016SS.GW.ClearSelection();
1017} else {
1018Error(_("Bad selection for trace; select a single point."));
1019}
1020break;
1021
1022case Command::STOP_TRACING: {
1023if (SS.traced.point == Entity::NO_ENTITY) {
1024break;
1025}
1026Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
1027dialog->AddFilters(Platform::CsvFileFilters);
1028dialog->ThawChoices(settings, "Trace");
1029dialog->SetFilename(SS.saveFile);
1030if(dialog->RunModal()) {
1031dialog->FreezeChoices(settings, "Trace");
1032
1033FILE *f = OpenFile(dialog->GetFilename(), "wb");
1034if(f) {
1035int i;
1036SContour *sc = &(SS.traced.path);
1037for(i = 0; i < sc->l.n; i++) {
1038Vector p = sc->l[i].p;
1039double s = SS.exportScale;
1040fprintf(f, "%.10f, %.10f, %.10f\r\n",
1041p.x/s, p.y/s, p.z/s);
1042}
1043fclose(f);
1044} else {
1045Error(_("Couldn't write to '%s'"), dialog->GetFilename().raw.c_str());
1046}
1047}
1048// Clear the trace, and stop tracing
1049SS.traced.point = Entity::NO_ENTITY;
1050SS.traced.path.l.Clear();
1051SS.GW.Invalidate();
1052break;
1053}
1054
1055default: ssassert(false, "Unexpected menu ID");
1056}
1057}
1058
1059void SolveSpaceUI::ShowNakedEdges(bool reportOnlyWhenNotOkay) {
1060SS.nakedEdges.Clear();
1061
1062Group *g = SK.GetGroup(SS.GW.activeGroup);
1063SMesh *m = &(g->displayMesh);
1064SKdNode *root = SKdNode::From(m);
1065bool inters, leaks;
1066root->MakeCertainEdgesInto(&(SS.nakedEdges),
1067EdgeKind::NAKED_OR_SELF_INTER, /*coplanarIsInter=*/true, &inters, &leaks);
1068
1069if(reportOnlyWhenNotOkay && !inters && !leaks && SS.nakedEdges.l.IsEmpty()) {
1070return;
1071}
1072SS.GW.Invalidate();
1073
1074const char *intersMsg = inters ?
1075_("The mesh is self-intersecting (NOT okay, invalid).") :
1076_("The mesh is not self-intersecting (okay, valid).");
1077const char *leaksMsg = leaks ?
1078_("The mesh has naked edges (NOT okay, invalid).") :
1079_("The mesh is watertight (okay, valid).");
1080
1081std::string cntMsg = ssprintf(
1082_("\n\nThe model contains %d triangles, from %d surfaces."),
1083g->displayMesh.l.n, g->runningShell.surface.n);
1084
1085if(SS.nakedEdges.l.IsEmpty()) {
1086Message(_("%s\n\n%s\n\nZero problematic edges, good.%s"),
1087intersMsg, leaksMsg, cntMsg.c_str());
1088} else {
1089Error(_("%s\n\n%s\n\n%d problematic edges, bad.%s"),
1090intersMsg, leaksMsg, SS.nakedEdges.l.n, cntMsg.c_str());
1091}
1092}
1093
1094void SolveSpaceUI::MenuHelp(Command id) {
1095switch(id) {
1096case Command::WEBSITE:
1097Platform::OpenInBrowser("http://solvespace.com/helpmenu");
1098break;
1099
1100case Command::ABOUT:
1101Message(_(
1102"This is SolveSpace version %s.\n"
1103"\n"
1104"For more information, see http://solvespace.com/\n"
1105"\n"
1106"SolveSpace is free software: you are free to modify\n"
1107"and/or redistribute it under the terms of the GNU\n"
1108"General Public License (GPL) version 3 or later.\n"
1109"\n"
1110"There is NO WARRANTY, to the extent permitted by\n"
1111"law. For details, visit http://gnu.org/licenses/\n"
1112"\n"
1113"© 2008-%d Jonathan Westhues and other authors.\n"),
1114PACKAGE_VERSION, 2024);
1115break;
1116
1117case Command::GITHUB:
1118Platform::OpenInBrowser(GIT_HASH_URL);
1119break;
1120
1121default: ssassert(false, "Unexpected menu ID");
1122}
1123}
1124
1125void SolveSpaceUI::Clear() {
1126sys.Clear();
1127for(int i = 0; i < MAX_UNDO; i++) {
1128if(i < undo.cnt) undo.d[i].Clear();
1129if(i < redo.cnt) redo.d[i].Clear();
1130}
1131TW.window = NULL;
1132GW.openRecentMenu = NULL;
1133GW.linkRecentMenu = NULL;
1134GW.showGridMenuItem = NULL;
1135GW.dimSolidModelMenuItem = NULL;
1136GW.perspectiveProjMenuItem = NULL;
1137GW.explodeMenuItem = NULL;
1138GW.showToolbarMenuItem = NULL;
1139GW.showTextWndMenuItem = NULL;
1140GW.fullScreenMenuItem = NULL;
1141GW.unitsMmMenuItem = NULL;
1142GW.unitsMetersMenuItem = NULL;
1143GW.unitsInchesMenuItem = NULL;
1144GW.unitsFeetInchesMenuItem = NULL;
1145GW.inWorkplaneMenuItem = NULL;
1146GW.in3dMenuItem = NULL;
1147GW.undoMenuItem = NULL;
1148GW.redoMenuItem = NULL;
1149GW.window = NULL;
1150}
1151
1152void Sketch::Clear() {
1153group.Clear();
1154groupOrder.Clear();
1155constraint.Clear();
1156request.Clear();
1157style.Clear();
1158entity.Clear();
1159param.Clear();
1160}
1161
1162BBox Sketch::CalculateEntityBBox(bool includingInvisible) {
1163BBox box = {};
1164bool first = true;
1165
1166auto includePoint = [&](const Vector &point) {
1167if(first) {
1168box.minp = point;
1169box.maxp = point;
1170first = false;
1171} else {
1172box.Include(point);
1173}
1174};
1175
1176for(const Entity &e : entity) {
1177if(e.construction) continue;
1178if(!(includingInvisible || e.IsVisible())) continue;
1179
1180// arc center point shouldn't be included in bounding box calculation
1181if(e.IsPoint() && e.h.isFromRequest()) {
1182Request *r = SK.GetRequest(e.h.request());
1183if(r->type == Request::Type::ARC_OF_CIRCLE && e.h == r->h.entity(1)) {
1184continue;
1185}
1186}
1187
1188if(e.IsPoint()) {
1189includePoint(e.PointGetNum());
1190continue;
1191}
1192
1193switch(e.type) {
1194// Circles and arcs are special cases. We calculate their bounds
1195// based on Bezier curve bounds. This is not exact for arcs,
1196// but the implementation is rather simple.
1197case Entity::Type::CIRCLE:
1198case Entity::Type::ARC_OF_CIRCLE: {
1199SBezierList sbl = {};
1200e.GenerateBezierCurves(&sbl);
1201
1202for(const SBezier &sb : sbl.l) {
1203for(int j = 0; j <= sb.deg; j++) {
1204includePoint(sb.ctrl[j]);
1205}
1206}
1207sbl.Clear();
1208continue;
1209}
1210
1211default:
1212continue;
1213}
1214}
1215
1216return box;
1217}
1218
1219Group *Sketch::GetRunningMeshGroupFor(hGroup h) {
1220Group *g = GetGroup(h);
1221while(g != NULL) {
1222if(g->IsMeshGroup()) {
1223return g;
1224}
1225g = g->PreviousGroup();
1226}
1227return NULL;
1228}
1229