Solvespace

Форк
0
/
solvespace.cpp 
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

10
SolveSpaceUI SolveSpace::SS = {};
11
Sketch SolveSpace::SK = {};
12

13
void SolveSpaceUI::Init() {
14
#if !defined(HEADLESS)
15
    // Check that the resource system works.
16
    dbp("%s", LoadString("banner.txt").data());
17
#endif
18

19
    Platform::SettingsRef settings = Platform::GetSettings();
20

21
    SS.tangentArcRadius = 10.0;
22
    SS.explodeDistance = 1.0;
23

24
    // Then, load the registry settings.
25
    // Default list of colors for the model material
26
    modelColor[0] = settings->ThawColor("ModelColor_0", RGBi(150, 150, 150));
27
    modelColor[1] = settings->ThawColor("ModelColor_1", RGBi(100, 100, 100));
28
    modelColor[2] = settings->ThawColor("ModelColor_2", RGBi( 30,  30,  30));
29
    modelColor[3] = settings->ThawColor("ModelColor_3", RGBi(150,   0,   0));
30
    modelColor[4] = settings->ThawColor("ModelColor_4", RGBi(  0, 100,   0));
31
    modelColor[5] = settings->ThawColor("ModelColor_5", RGBi(  0,  80,  80));
32
    modelColor[6] = settings->ThawColor("ModelColor_6", RGBi(  0,   0, 130));
33
    modelColor[7] = settings->ThawColor("ModelColor_7", RGBi( 80,   0,  80));
34
    // Light intensities
35
    lightIntensity[0] = settings->ThawFloat("LightIntensity_0", 1.0);
36
    lightIntensity[1] = settings->ThawFloat("LightIntensity_1", 0.5);
37
    ambientIntensity = settings->ThawFloat("Light_Ambient", 0.3);
38
    // Light positions
39
    lightDir[0].x = settings->ThawFloat("LightDir_0_Right",   -1.0);
40
    lightDir[0].y = settings->ThawFloat("LightDir_0_Up",       1.0);
41
    lightDir[0].z = settings->ThawFloat("LightDir_0_Forward",  0.0);
42
    lightDir[1].x = settings->ThawFloat("LightDir_1_Right",    1.0);
43
    lightDir[1].y = settings->ThawFloat("LightDir_1_Up",       0.0);
44
    lightDir[1].z = settings->ThawFloat("LightDir_1_Forward",  0.0);
45

46
    exportMode = false;
47
    // Chord tolerance
48
    chordTol = settings->ThawFloat("ChordTolerancePct", 0.1);
49
    // Max pwl segments to generate
50
    maxSegments = settings->ThawInt("MaxSegments", 20);
51
    // Chord tolerance
52
    exportChordTol = settings->ThawFloat("ExportChordTolerance", 0.1);
53
    // Max pwl segments to generate
54
    exportMaxSegments = settings->ThawInt("ExportMaxSegments", 64);
55
    // Timeout value for finding redundant constrains (ms)
56
    timeoutRedundantConstr = settings->ThawInt("TimeoutRedundantConstraints", 1000);
57
    // View units
58
    viewUnits = (Unit)settings->ThawInt("ViewUnits", (uint32_t)Unit::MM);
59
    // Number of digits after the decimal point
60
    afterDecimalMm = settings->ThawInt("AfterDecimalMm", 2);
61
    afterDecimalInch = settings->ThawInt("AfterDecimalInch", 3);
62
    afterDecimalDegree = settings->ThawInt("AfterDecimalDegree", 2);
63
    useSIPrefixes = settings->ThawBool("UseSIPrefixes", false);
64
    // Camera tangent (determines perspective)
65
    cameraTangent = settings->ThawFloat("CameraTangent", 0.3f/1e3);
66
    // Grid spacing
67
    gridSpacing = settings->ThawFloat("GridSpacing", 5.0);
68
    // Export scale factor
69
    exportScale = settings->ThawFloat("ExportScale", 1.0);
70
    // Export offset (cutter radius comp)
71
    exportOffset = settings->ThawFloat("ExportOffset", 0.0);
72
    // Dimensions on arcs default to diameter vs radius
73
    arcDimDefaultDiameter = settings->ThawBool("ArcDimDefaultDiameter", false);
74
    // Show full file path in the menu bar
75
    showFullFilePath = settings->ThawBool("ShowFullFilePath", true);
76
    // Rewrite exported colors close to white into black (assuming white bg)
77
    fixExportColors = settings->ThawBool("FixExportColors", true);
78
    // Export background color
79
    exportBackgroundColor = settings->ThawBool("ExportBackgroundColor", false);
80
    // Draw back faces of triangles (when mesh is leaky/self-intersecting)
81
    drawBackFaces = settings->ThawBool("DrawBackFaces", true);
82
    // Use camera mouse navigation
83
    cameraNav = settings->ThawBool("CameraNav", false);
84
    // Use turntable mouse navigation
85
    turntableNav = settings->ThawBool("TurntableNav", false);
86
    // Immediately edit dimension
87
    immediatelyEditDimension = settings->ThawBool("ImmediatelyEditDimension", true);
88
    // Check that contours are closed and not self-intersecting
89
    checkClosedContour = settings->ThawBool("CheckClosedContour", true);
90
    // Enable automatic constrains for lines
91
    automaticLineConstraints = settings->ThawBool("AutomaticLineConstraints", true);
92
    // Draw closed polygons areas
93
    showContourAreas = settings->ThawBool("ShowContourAreas", false);
94
    // Export shaded triangles in a 2d view
95
    exportShadedTriangles = settings->ThawBool("ExportShadedTriangles", true);
96
    // Export pwl curves (instead of exact) always
97
    exportPwlCurves = settings->ThawBool("ExportPwlCurves", false);
98
    // Background color on-screen
99
    backgroundColor = settings->ThawColor("BackgroundColor", RGBi(0, 0, 0));
100
    // Whether export canvas size is fixed or derived from bbox
101
    exportCanvasSizeAuto = settings->ThawBool("ExportCanvasSizeAuto", true);
102
    // Margins for automatic canvas size
103
    exportMargin.left   = settings->ThawFloat("ExportMargin_Left",   5.0);
104
    exportMargin.right  = settings->ThawFloat("ExportMargin_Right",  5.0);
105
    exportMargin.bottom = settings->ThawFloat("ExportMargin_Bottom", 5.0);
106
    exportMargin.top    = settings->ThawFloat("ExportMargin_Top",    5.0);
107
    // Dimensions for fixed canvas size
108
    exportCanvas.width  = settings->ThawFloat("ExportCanvas_Width",  100.0);
109
    exportCanvas.height = settings->ThawFloat("ExportCanvas_Height", 100.0);
110
    exportCanvas.dx     = settings->ThawFloat("ExportCanvas_Dx",     5.0);
111
    exportCanvas.dy     = settings->ThawFloat("ExportCanvas_Dy",     5.0);
112
    // Extra parameters when exporting G code
113
    gCode.depth         = settings->ThawFloat("GCode_Depth", 10.0);
114
    gCode.safeHeight    = settings->ThawFloat("GCode_SafeHeight", 5.0);
115
    gCode.passes        = settings->ThawInt("GCode_Passes", 1);
116
    gCode.feed          = settings->ThawFloat("GCode_Feed", 10.0);
117
    gCode.plungeFeed    = settings->ThawFloat("GCode_PlungeFeed", 10.0);
118
    // Show toolbar in the graphics window
119
    showToolbar = settings->ThawBool("ShowToolbar", true);
120
    // Recent files menus
121
    for(size_t i = 0; i < MAX_RECENT; i++) {
122
        std::string rawPath = settings->ThawString("RecentFile_" + std::to_string(i), "");
123
        if(rawPath.empty()) continue;
124
        recentFiles.push_back(Platform::Path::From(rawPath));
125
    }
126
    // Autosave timer
127
    autosaveInterval = settings->ThawInt("AutosaveInterval", 5);
128
    // Locale
129
    std::string locale = settings->ThawString("Locale", "");
130
    if(!locale.empty()) {
131
        SetLocale(locale);
132
    }
133

134
    refreshTimer = Platform::CreateTimer();
135
    refreshTimer->onTimeout = std::bind(&SolveSpaceUI::Refresh, &SS);
136

137
    autosaveTimer = Platform::CreateTimer();
138
    autosaveTimer->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

144
    ScheduleAutosave();
145

146
    NewFile();
147
    AfterNewFile();
148

149
    if(TW.window && GW.window) {
150
        TW.window->ThawPosition(settings, "TextWindow");
151
        GW.window->ThawPosition(settings, "GraphicsWindow");
152
        TW.window->SetVisible(true);
153
        GW.window->SetVisible(true);
154
        GW.window->Focus();
155

156
        // Do this once the window is created.
157
        Request3DConnexionEventsForWindow(GW.window);
158
    }
159
}
160

161
bool SolveSpaceUI::LoadAutosaveFor(const Platform::Path &filename) {
162
    Platform::Path autosaveFile = filename.WithExtension(BACKUP_EXT);
163

164
    FILE *f = OpenFile(autosaveFile, "rb");
165
    if(!f)
166
        return false;
167
    fclose(f);
168

169
    Platform::MessageDialogRef dialog = CreateMessageDialog(GW.window);
170

171
    using Platform::MessageDialog;
172
    dialog->SetType(MessageDialog::Type::QUESTION);
173
    dialog->SetTitle(C_("title", "Autosave Available"));
174
    dialog->SetMessage(C_("dialog", "An autosave file is available for this sketch."));
175
    dialog->SetDescription(C_("dialog", "Do you want to load the autosave file instead?"));
176
    dialog->AddButton(C_("button", "&Load autosave"), MessageDialog::Response::YES,
177
                      /*isDefault=*/true);
178
    dialog->AddButton(C_("button", "Do&n't Load"), MessageDialog::Response::NO);
179

180
    // FIXME(async): asyncify this call
181
    if(dialog->RunModal() == MessageDialog::Response::YES) {
182
        unsaved = true;
183
        return LoadFromFile(autosaveFile, /*canCancel=*/true);
184
    }
185

186
    return false;
187
}
188

189
bool SolveSpaceUI::Load(const Platform::Path &filename) {
190
    bool autosaveLoaded = LoadAutosaveFor(filename);
191
    bool fileLoaded = autosaveLoaded || LoadFromFile(filename, /*canCancel=*/true);
192
    if(fileLoaded) {
193
        saveFile = filename;
194
        AddToRecentList(filename);
195
    } else {
196
        saveFile.Clear();
197
        NewFile();
198
    }
199
    AfterNewFile();
200
    unsaved = autosaveLoaded;
201
    return fileLoaded;
202
}
203

204
void SolveSpaceUI::Exit() {
205
    Platform::SettingsRef settings = Platform::GetSettings();
206

207
    GW.window->FreezePosition(settings, "GraphicsWindow");
208
    TW.window->FreezePosition(settings, "TextWindow");
209

210
    // Recent files
211
    for(size_t i = 0; i < MAX_RECENT; i++) {
212
        std::string rawPath;
213
        if(recentFiles.size() > i) {
214
            rawPath = recentFiles[i].raw;
215
        }
216
        settings->FreezeString("RecentFile_" + std::to_string(i), rawPath);
217
    }
218
    // Model colors
219
    for(size_t i = 0; i < MODEL_COLORS; i++)
220
        settings->FreezeColor("ModelColor_" + std::to_string(i), modelColor[i]);
221
    // Light intensities
222
    settings->FreezeFloat("LightIntensity_0", (float)lightIntensity[0]);
223
    settings->FreezeFloat("LightIntensity_1", (float)lightIntensity[1]);
224
    settings->FreezeFloat("Light_Ambient", (float)ambientIntensity);
225
    // Light directions
226
    settings->FreezeFloat("LightDir_0_Right",   (float)lightDir[0].x);
227
    settings->FreezeFloat("LightDir_0_Up",      (float)lightDir[0].y);
228
    settings->FreezeFloat("LightDir_0_Forward", (float)lightDir[0].z);
229
    settings->FreezeFloat("LightDir_1_Right",   (float)lightDir[1].x);
230
    settings->FreezeFloat("LightDir_1_Up",      (float)lightDir[1].y);
231
    settings->FreezeFloat("LightDir_1_Forward", (float)lightDir[1].z);
232
    // Chord tolerance
233
    settings->FreezeFloat("ChordTolerancePct", (float)chordTol);
234
    // Max pwl segments to generate
235
    settings->FreezeInt("MaxSegments", (uint32_t)maxSegments);
236
    // Export Chord tolerance
237
    settings->FreezeFloat("ExportChordTolerance", (float)exportChordTol);
238
    // Export Max pwl segments to generate
239
    settings->FreezeInt("ExportMaxSegments", (uint32_t)exportMaxSegments);
240
    // Timeout for finding which constraints to fix Jacobian
241
    settings->FreezeInt("TimeoutRedundantConstraints", (uint32_t)timeoutRedundantConstr);
242
    // View units
243
    settings->FreezeInt("ViewUnits", (uint32_t)viewUnits);
244
    // Number of digits after the decimal point
245
    settings->FreezeInt("AfterDecimalMm",   (uint32_t)afterDecimalMm);
246
    settings->FreezeInt("AfterDecimalInch", (uint32_t)afterDecimalInch);
247
    settings->FreezeInt("AfterDecimalDegree", (uint32_t)afterDecimalDegree);
248
    settings->FreezeBool("UseSIPrefixes", useSIPrefixes);
249
    // Camera tangent (determines perspective)
250
    settings->FreezeFloat("CameraTangent", (float)cameraTangent);
251
    // Grid spacing
252
    settings->FreezeFloat("GridSpacing", gridSpacing);
253
    // Export scale
254
    settings->FreezeFloat("ExportScale", exportScale);
255
    // Export offset (cutter radius comp)
256
    settings->FreezeFloat("ExportOffset", exportOffset);
257
    // Rewrite the default arc dimension setting
258
    settings->FreezeBool("ArcDimDefaultDiameter", arcDimDefaultDiameter);
259
    // Show full file path in the menu bar
260
    settings->FreezeBool("ShowFullFilePath", showFullFilePath);
261
    // Rewrite exported colors close to white into black (assuming white bg)
262
    settings->FreezeBool("FixExportColors", fixExportColors);
263
    // Export background color
264
    settings->FreezeBool("ExportBackgroundColor", exportBackgroundColor);
265
    // Draw back faces of triangles (when mesh is leaky/self-intersecting)
266
    settings->FreezeBool("DrawBackFaces", drawBackFaces);
267
    // Draw closed polygons areas
268
    settings->FreezeBool("ShowContourAreas", showContourAreas);
269
    // Check that contours are closed and not self-intersecting
270
    settings->FreezeBool("CheckClosedContour", checkClosedContour);
271
    // Use camera mouse navigation
272
    settings->FreezeBool("CameraNav", cameraNav);
273
    // Use turntable mouse navigation
274
    settings->FreezeBool("TurntableNav", turntableNav);
275
    // Immediately edit dimensions
276
    settings->FreezeBool("ImmediatelyEditDimension", immediatelyEditDimension);
277
    // Enable automatic constrains for lines
278
    settings->FreezeBool("AutomaticLineConstraints", automaticLineConstraints);
279
    // Export shaded triangles in a 2d view
280
    settings->FreezeBool("ExportShadedTriangles", exportShadedTriangles);
281
    // Export pwl curves (instead of exact) always
282
    settings->FreezeBool("ExportPwlCurves", exportPwlCurves);
283
    // Background color on-screen
284
    settings->FreezeColor("BackgroundColor", backgroundColor);
285
    // Whether export canvas size is fixed or derived from bbox
286
    settings->FreezeBool("ExportCanvasSizeAuto", exportCanvasSizeAuto);
287
    // Margins for automatic canvas size
288
    settings->FreezeFloat("ExportMargin_Left",   exportMargin.left);
289
    settings->FreezeFloat("ExportMargin_Right",  exportMargin.right);
290
    settings->FreezeFloat("ExportMargin_Bottom", exportMargin.bottom);
291
    settings->FreezeFloat("ExportMargin_Top",    exportMargin.top);
292
    // Dimensions for fixed canvas size
293
    settings->FreezeFloat("ExportCanvas_Width",  exportCanvas.width);
294
    settings->FreezeFloat("ExportCanvas_Height", exportCanvas.height);
295
    settings->FreezeFloat("ExportCanvas_Dx",     exportCanvas.dx);
296
    settings->FreezeFloat("ExportCanvas_Dy",     exportCanvas.dy);
297
     // Extra parameters when exporting G code
298
    settings->FreezeFloat("GCode_Depth", gCode.depth);
299
    settings->FreezeInt("GCode_Passes", gCode.passes);
300
    settings->FreezeFloat("GCode_Feed", gCode.feed);
301
    settings->FreezeFloat("GCode_PlungeFeed", gCode.plungeFeed);
302
    // Show toolbar in the graphics window
303
    settings->FreezeBool("ShowToolbar", showToolbar);
304
    // Autosave timer
305
    settings->FreezeInt("AutosaveInterval", autosaveInterval);
306

307
    // And the default styles, colors and line widths and such.
308
    Style::FreezeDefaultStyles(settings);
309

310
    Platform::ExitGui();
311
}
312

313
void SolveSpaceUI::Refresh() {
314
    // generateAll must happen bfore updating displays
315
    if(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.
318
        scheduledGenerateAll = false;
319
        GenerateAll(Generate::DIRTY, /*andFindFree=*/false, /*genForBBox=*/false);   
320
    }
321
    if(scheduledShowTW) {
322
        scheduledShowTW = false;
323
        TW.Show();
324
    }
325
}
326

327
void SolveSpaceUI::ScheduleGenerateAll() {
328
    scheduledGenerateAll = true;
329
    refreshTimer->RunAfterProcessingEvents();
330
}
331

332
void SolveSpaceUI::ScheduleShowTW() {
333
    scheduledShowTW = true;
334
    refreshTimer->RunAfterProcessingEvents();
335
}
336

337
void SolveSpaceUI::ScheduleAutosave() {
338
    autosaveTimer->RunAfter(autosaveInterval * 60 * 1000);
339
}
340

341
double SolveSpaceUI::MmPerUnit() {
342
    switch(viewUnits) {
343
        case Unit::INCHES: return 25.4;
344
        case Unit::FEET_INCHES: return 25.4; // The 'unit' is still inches
345
        case Unit::METERS: return 1000.0;
346
        case Unit::MM: return 1.0;
347
    }
348
    return 1.0;
349
}
350
const char *SolveSpaceUI::UnitName() {
351
    switch(viewUnits) {
352
        case Unit::INCHES: return "in";
353
        case Unit::FEET_INCHES: return "in";
354
        case Unit::METERS: return "m";
355
        case Unit::MM: return "mm";
356
    }
357
    return "";
358
}
359

360
std::string SolveSpaceUI::MmToString(double v, bool editable) {
361
    v /= 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.
365
    if(viewUnits == Unit::FEET_INCHES && !editable) {
366
        // Now convert v from inches to 64'ths of an inch, to make rounding easier.
367
        v = floor((v + (1.0 / 128.0)) * 64.0);
368
        int feet = (int)(v / (12.0 * 64.0));
369
        v = v - (feet * 12.0 * 64.0);
370
        // v is now the feet-less remainder in 1/64 inches
371
        int inches = (int)(v / 64.0);
372
        int numerator = (int)(v - ((double)inches * 64.0));
373
        int denominator = 64;
374
        // Divide down to smallest denominator where the numerator is still a whole number
375
        while ((numerator != 0) && ((numerator & 1) == 0)) {
376
            numerator /= 2;
377
            denominator /= 2;
378
        }
379
        std::ostringstream str;
380
        if(feet != 0) {
381
            str << feet << "'-";
382
        }
383
        // For something like 0.5, show 1/2" rather than 0 1/2"
384
        if(!(feet == 0 && inches == 0 && numerator != 0)) {
385
            str << inches;
386
        }
387
        if(numerator != 0) {
388
            str << " " << numerator << "/" << denominator;
389
        }
390
        str << "\"";
391
        return str.str();
392
    }
393

394
    int digits = UnitDigitsAfterDecimal();
395
    double minimum = 0.5 * pow(10,-digits);
396
    while ((v < minimum) && (v > LENGTH_EPS)) {
397
        digits++;
398
        minimum *= 0.1;
399
    }
400
    return ssprintf("%.*f", digits, v);
401
}
402
static const char *DimToString(int dim) {
403
    switch(dim) {
404
        case 3: return "³";
405
        case 2: return "²";
406
        case 1: return "";
407
        default: ssassert(false, "Unexpected dimension");
408
    }
409
}
410
static 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)
413
    switch(dim) {
414
        case 0:
415
        case 1:
416
                 if(ord >=  3) return {  3, "km" };
417
            else if(ord >=  0) return {  0, "m"  };
418
            else if(ord >= -2) return { -2, "cm" };
419
            else if(ord >= -3) return { -3, "mm" };
420
            else if(ord >= -6) return { -6, "µm" };
421
            else               return { -9, "nm" };
422
            break;
423
        case 2:
424
                 if(ord >=  5) return {  3, "km" };
425
            else if(ord >=  0) return {  0, "m"  };
426
            else if(ord >= -2) return { -2, "cm" };
427
            else if(ord >= -6) return { -3, "mm" };
428
            else if(ord >= -13) return { -6, "µm" };
429
            else               return { -9, "nm" };
430
            break;
431
        case 3:
432
                 if(ord >=  7) return {  3, "km" };
433
            else if(ord >=  0) return {  0, "m"  };
434
            else if(ord >= -5) return { -2, "cm" };
435
            else if(ord >= -11) return { -3, "mm" };
436
            else                return { -6, "µm" };
437
            break;
438
        default:
439
            dbp ("dimensions over 3 not supported");
440
            break;
441
    }
442
    return {0, "m"};
443
}
444
static std::pair<int, std::string> SelectSIPrefixInch(int deg) {
445
         if(deg >=  0) return {  0, "in"  };
446
    else if(deg >= -3) return { -3, "mil" };
447
    else               return { -6, "µin" };
448
}
449
std::string SolveSpaceUI::MmToStringSI(double v, int dim) {
450
    bool compact = false;
451
    if(dim == 0) {
452
        if(!useSIPrefixes) return MmToString(v);
453
        compact = true;
454
        dim = 1;
455
    }
456

457
    bool inches = (viewUnits == Unit::INCHES) || (viewUnits == Unit::FEET_INCHES);
458
    v /= pow(inches ? 25.4 : 1000, dim);
459
    int vdeg = (int)(log10(fabs(v)));
460
    std::string unit;
461
    if(fabs(v) > 0.0) {
462
        int sdeg = 0;
463
        std::tie(sdeg, unit) =
464
            inches
465
            ? SelectSIPrefixInch(vdeg/dim)
466
            : SelectSIPrefixMm(vdeg, dim);
467
        v /= pow(10.0, sdeg * dim);
468
    }
469
    if(viewUnits == Unit::FEET_INCHES && fabs(v) > pow(12.0, dim)) {
470
        unit = "ft";
471
        v /= pow(12.0, dim);
472
    }
473
    int pdeg = (int)ceil(log10(fabs(v) + 1e-10));
474
    return ssprintf("%.*g%s%s%s", pdeg + UnitDigitsAfterDecimal(), v,
475
                    compact ? "" : " ", unit.c_str(), DimToString(dim));
476
}
477
std::string SolveSpaceUI::DegreeToString(double v) {
478
    if(fabs(v - floor(v)) > 1e-10) {
479
        return ssprintf("%.*f", afterDecimalDegree, v);
480
    } else {
481
        return ssprintf("%.0f", v);
482
    }
483
}
484
double SolveSpaceUI::ExprToMm(Expr *e) {
485
    return (e->Eval()) * MmPerUnit();
486
}
487
double SolveSpaceUI::StringToMm(const std::string &str) {
488
    return std::stod(str) * MmPerUnit();
489
}
490
double SolveSpaceUI::ChordTolMm() {
491
    if(exportMode) return ExportChordTolMm();
492
    return chordTolCalculated;
493
}
494
double SolveSpaceUI::ExportChordTolMm() {
495
    return exportChordTol / exportScale;
496
}
497
int SolveSpaceUI::GetMaxSegments() {
498
    if(exportMode) return exportMaxSegments;
499
    return maxSegments;
500
}
501
int SolveSpaceUI::UnitDigitsAfterDecimal() {
502
    return (viewUnits == Unit::INCHES || viewUnits == Unit::FEET_INCHES) ?
503
           afterDecimalInch : afterDecimalMm;
504
}
505
void SolveSpaceUI::SetUnitDigitsAfterDecimal(int v) {
506
    if(viewUnits == Unit::INCHES || viewUnits == Unit::FEET_INCHES) {
507
        afterDecimalInch = v;
508
    } else {
509
        afterDecimalMm = v;
510
    }
511
}
512

513
double SolveSpaceUI::CameraTangent() {
514
    if(!usePerspectiveProj) {
515
        return 0;
516
    } else {
517
        return cameraTangent;
518
    }
519
}
520

521
void SolveSpaceUI::AfterNewFile() {
522
    // Clear out the traced point, which is no longer valid
523
    traced.point = Entity::NO_ENTITY;
524
    traced.path.l.Clear();
525
    // and the naked edges
526
    nakedEdges.Clear();
527

528
    // Quit export mode
529
    justExportedInfo.draw = false;
530
    centerOfMass.draw = false;
531
    exportMode = 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)
536
    SS.GW.offset    = Vector::From(0, 0, 0);
537
    SS.GW.projRight = Vector::From(1, 0, 0);
538
    SS.GW.projUp    = Vector::From(0, 1, 0);
539

540
    GenerateAll(Generate::ALL);
541

542
    GW.Init();
543
    TW.Init();
544

545
    unsaved = false;
546

547
    GW.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.
551
    Style::CreateAllDefaultStyles();
552

553
    UpdateWindowTitles();
554
}
555

556
void SolveSpaceUI::AddToRecentList(const Platform::Path &filename) {
557
    auto it = std::find_if(recentFiles.begin(), recentFiles.end(),
558
                           [&](const Platform::Path &p) { return p.Equals(filename); });
559
    if(it != recentFiles.end()) {
560
        recentFiles.erase(it);
561
    }
562

563
    if(recentFiles.size() > MAX_RECENT) {
564
        recentFiles.erase(recentFiles.begin() + MAX_RECENT);
565
    }
566

567
    recentFiles.insert(recentFiles.begin(), filename);
568
    GW.PopulateRecentFiles();
569
}
570

571
bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
572
    Platform::SettingsRef settings = Platform::GetSettings();
573
    Platform::Path newSaveFile = saveFile;
574

575
    if(saveAs || saveFile.IsEmpty()) {
576
        Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(GW.window);
577
        // FIXME(emscripten):
578
        dbp("Calling AddFilter()...");
579
        dialog->AddFilter(C_("file-type", "SolveSpace models"), { SKETCH_EXT });
580
        dbp("Calling ThawChoices()...");
581
        dialog->ThawChoices(settings, "Sketch");
582
        if(!newSaveFile.IsEmpty()) {
583
            dbp("Calling SetFilename()...");
584
            dialog->SetFilename(newSaveFile);
585
        }
586
        dbp("Calling RunModal()...");
587
        if(dialog->RunModal()) {
588
            dbp("Calling FreezeChoices()...");
589
            dialog->FreezeChoices(settings, "Sketch");
590
            newSaveFile = dialog->GetFilename();
591
        } else {
592
            return false;
593
        }
594
    }
595

596
    if(SaveToFile(newSaveFile)) {
597
        AddToRecentList(newSaveFile);
598
        RemoveAutosave();
599
        saveFile = newSaveFile;
600
        unsaved = false;
601
        if (this->OnSaveFinished) {
602
            this->OnSaveFinished(newSaveFile, saveAs, false);
603
        }
604
        return true;
605
    } else {
606
        return false;
607
    }
608
}
609

610
void SolveSpaceUI::Autosave()
611
{
612
    ScheduleAutosave();
613

614
    if(!saveFile.IsEmpty() && unsaved) {
615
        Platform::Path saveFileName = saveFile.WithExtension(BACKUP_EXT);
616
        SaveToFile(saveFileName);
617
        if (this->OnSaveFinished) {
618
            this->OnSaveFinished(saveFileName, false, true);
619
        }
620
    }
621
}
622

623
void SolveSpaceUI::RemoveAutosave()
624
{
625
    Platform::Path autosaveFile = saveFile.WithExtension(BACKUP_EXT);
626
    RemoveFile(autosaveFile);
627
}
628

629
bool SolveSpaceUI::OkayToStartNewFile() {
630
    if(!unsaved) return true;
631

632
    Platform::MessageDialogRef dialog = CreateMessageDialog(GW.window);
633

634
    using Platform::MessageDialog;
635
    dialog->SetType(MessageDialog::Type::QUESTION);
636
    dialog->SetTitle(C_("title", "Modified File"));
637
    if(!SolveSpace::SS.saveFile.IsEmpty()) {
638
        dialog->SetMessage(ssprintf(C_("dialog", "Do you want to save the changes you made to "
639
                                                 "the sketch “%s”?"), saveFile.raw.c_str()));
640
    } else {
641
        dialog->SetMessage(C_("dialog", "Do you want to save the changes you made to "
642
                                        "the new sketch?"));
643
    }
644
    dialog->SetDescription(C_("dialog", "Your changes will be lost if you don't save them."));
645
    dialog->AddButton(C_("button", "&Save"), MessageDialog::Response::YES,
646
                      /*isDefault=*/true);
647
    dialog->AddButton(C_("button", "Do&n't Save"), MessageDialog::Response::NO);
648
    dialog->AddButton(C_("button", "&Cancel"), MessageDialog::Response::CANCEL);
649

650
    // FIXME(async): asyncify this call
651
    switch(dialog->RunModal()) {
652
        case MessageDialog::Response::YES:
653
            return GetFilenameAndSave(/*saveAs=*/false);
654

655
        case MessageDialog::Response::NO:
656
            RemoveAutosave();
657
            return true;
658

659
        default:
660
            return false;
661
    }
662
}
663

664
void SolveSpaceUI::UpdateWindowTitles() {
665
    if(!GW.window || !TW.window) return;
666

667
    if(saveFile.IsEmpty()) {
668
        GW.window->SetTitle(C_("title", "(new sketch)"));
669
    } else {
670
        if(!GW.window->SetTitleForFilename(saveFile)) {
671
            if(SS.showFullFilePath) {
672
                GW.window->SetTitle(saveFile.raw);
673
            } else {
674
                GW.window->SetTitle(saveFile.raw.substr(saveFile.raw.find_last_of("/\\") + 1));
675
            }
676
        }
677
    }
678

679
    TW.window->SetTitle(C_("title", "Property Browser"));
680
}
681

682
void SolveSpaceUI::MenuFile(Command id) {
683
    Platform::SettingsRef settings = Platform::GetSettings();
684

685
    switch(id) {
686
        case Command::NEW:
687
            if(!SS.OkayToStartNewFile()) break;
688

689
            SS.saveFile.Clear();
690
            SS.NewFile();
691
            SS.AfterNewFile();
692
            break;
693

694
        case Command::OPEN: {
695
            if(!SS.OkayToStartNewFile()) break;
696

697
            Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window);
698
            dialog->AddFilters(Platform::SolveSpaceModelFileFilters);
699
            dialog->ThawChoices(settings, "Sketch");
700
            if(dialog->RunModal()) {
701
                dialog->FreezeChoices(settings, "Sketch");
702
                SS.Load(dialog->GetFilename());
703
            }
704
            break;
705
        }
706

707
        case Command::SAVE:
708
            SS.GetFilenameAndSave(/*saveAs=*/false);
709
            break;
710

711
        case Command::SAVE_AS:
712
            SS.GetFilenameAndSave(/*saveAs=*/true);
713
            break;
714

715
        case Command::EXPORT_IMAGE: {
716
            Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
717
            dialog->AddFilters(Platform::RasterFileFilters);
718
            dialog->ThawChoices(settings, "ExportImage");
719
            dialog->SuggestFilename(SS.saveFile);
720
            if(dialog->RunModal()) {
721
                dialog->FreezeChoices(settings, "ExportImage");
722
                SS.ExportAsPngTo(dialog->GetFilename());
723
                if (SS.OnSaveFinished) {
724
                    SS.OnSaveFinished(dialog->GetFilename(), false, false);
725
                }
726
            }
727
            break;
728
        }
729

730
        case Command::EXPORT_VIEW: {
731
            Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
732
            dialog->AddFilters(Platform::VectorFileFilters);
733
            dialog->ThawChoices(settings, "ExportView");
734
            dialog->SuggestFilename(SS.saveFile);
735
            if(!dialog->RunModal()) break;
736
            dialog->FreezeChoices(settings, "ExportView");
737

738
            // If the user is exporting something where it would be
739
            // inappropriate to include the constraints, then warn.
740
            if(SS.GW.showConstraints != GraphicsWindow::ShowConstraintMode::SCM_NOSHOW &&
741
               (dialog->GetFilename().HasExtension("txt") || fabs(SS.exportOffset) > LENGTH_EPS)) {
742
                Message(_("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

748
            SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe=*/false);
749
            if (SS.OnSaveFinished) {
750
                SS.OnSaveFinished(dialog->GetFilename(), false, false);
751
            }
752
            break;
753
        }
754

755
        case Command::EXPORT_WIREFRAME: {
756
            Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
757
            dialog->AddFilters(Platform::Vector3dFileFilters);
758
            dialog->ThawChoices(settings, "ExportWireframe");
759
            dialog->SuggestFilename(SS.saveFile);
760
            if(!dialog->RunModal()) break;
761
            dialog->FreezeChoices(settings, "ExportWireframe");
762

763
            SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe*/true);
764
            if (SS.OnSaveFinished) {
765
                SS.OnSaveFinished(dialog->GetFilename(), false, false);
766
            }
767
            break;
768
        }
769

770
        case Command::EXPORT_SECTION: {
771
            Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
772
            dialog->AddFilters(Platform::VectorFileFilters);
773
            dialog->ThawChoices(settings, "ExportSection");
774
            dialog->SuggestFilename(SS.saveFile);
775
            if(!dialog->RunModal()) break;
776
            dialog->FreezeChoices(settings, "ExportSection");
777

778
            SS.ExportSectionTo(dialog->GetFilename());
779
            if (SS.OnSaveFinished) {
780
                SS.OnSaveFinished(dialog->GetFilename(), false, false);
781
            }
782
            break;
783
        }
784

785
        case Command::EXPORT_MESH: {
786
            Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
787
            dialog->AddFilters(Platform::MeshFileFilters);
788
            dialog->ThawChoices(settings, "ExportMesh");
789
            dialog->SuggestFilename(SS.saveFile);
790
            if(!dialog->RunModal()) break;
791
            dialog->FreezeChoices(settings, "ExportMesh");
792

793
            SS.ExportMeshTo(dialog->GetFilename());
794
            if (SS.OnSaveFinished) {
795
                SS.OnSaveFinished(dialog->GetFilename(), false, false);
796
            }
797

798
            break;
799
        }
800

801
        case Command::EXPORT_SURFACES: {
802
            Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
803
            dialog->AddFilters(Platform::SurfaceFileFilters);
804
            dialog->ThawChoices(settings, "ExportSurfaces");
805
            dialog->SuggestFilename(SS.saveFile);
806
            if(!dialog->RunModal()) break;
807
            dialog->FreezeChoices(settings, "ExportSurfaces");
808

809
            StepFileWriter sfw = {};
810
            sfw.ExportSurfacesTo(dialog->GetFilename());
811
            if (SS.OnSaveFinished) {
812
                SS.OnSaveFinished(dialog->GetFilename(), false, false);
813
            }
814
            break;
815
        }
816

817
        case Command::IMPORT: {
818
            Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window);
819
            dialog->AddFilters(Platform::ImportFileFilters);
820
            dialog->ThawChoices(settings, "Import");
821
            if(!dialog->RunModal()) break;
822
            dialog->FreezeChoices(settings, "Import");
823

824
            Platform::Path importFile = dialog->GetFilename();
825
            if(importFile.HasExtension("dxf")) {
826
                ImportDxf(importFile);
827
            } else if(importFile.HasExtension("dwg")) {
828
                ImportDwg(importFile);
829
            } else {
830
                Error(_("Can't identify file type from file extension of "
831
                        "filename '%s'; try .dxf or .dwg."), importFile.raw.c_str());
832
                break;
833
            }
834

835
            SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE);
836
            SS.ScheduleShowTW();
837
            break;
838
        }
839

840
        case Command::EXIT:
841
            if(!SS.OkayToStartNewFile()) break;
842
            SS.Exit();
843
            break;
844

845
        default: ssassert(false, "Unexpected menu ID");
846
    }
847

848
    SS.UpdateWindowTitles();
849
}
850

851
void SolveSpaceUI::MenuAnalyze(Command id) {
852
    Platform::SettingsRef settings = Platform::GetSettings();
853

854
    SS.GW.GroupSelection();
855
    auto const &gs = SS.GW.gs;
856

857
    switch(id) {
858
        case Command::STEP_DIM:
859
            if(gs.constraints == 1 && gs.n == 0) {
860
                Constraint *c = SK.GetConstraint(gs.constraint[0]);
861
                if(c->HasLabel() && !c->reference) {
862
                    SS.TW.stepDim.finish = c->valA;
863
                    SS.TW.stepDim.steps = 10;
864
                    SS.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) ;
872
                    SS.TW.shown.constraint = c->h;
873
                    SS.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.
877
                    SS.GW.ForceTextWindowShown();
878

879
                    SS.ScheduleShowTW();
880
                    SS.GW.ClearSelection();
881
                } else {
882
                    Error(_("Constraint must have a label, and must not be "
883
                            "a reference dimension."));
884
                }
885
            } else {
886
                Error(_("Bad selection for step dimension; select a constraint."));
887
            }
888
            break;
889

890
        case Command::NAKED_EDGES: {
891
            ShowNakedEdges(/*reportOnlyWhenNotOkay=*/false);
892
            break;
893
        }
894

895
        case Command::INTERFERENCE: {
896
            SS.nakedEdges.Clear();
897

898
            SMesh *m = &(SK.GetGroup(SS.GW.activeGroup)->displayMesh);
899
            SKdNode *root = SKdNode::From(m);
900
            bool inters, leaks;
901
            root->MakeCertainEdgesInto(&(SS.nakedEdges),
902
                EdgeKind::SELF_INTER, /*coplanarIsInter=*/false, &inters, &leaks);
903

904
            SS.GW.Invalidate();
905

906
            if(inters) {
907
                Error("%d edges interfere with other triangles, bad.",
908
                    SS.nakedEdges.l.n);
909
            } else {
910
                Message(_("The assembly does not interfere, good."));
911
            }
912
            break;
913
        }
914

915
        case Command::CENTER_OF_MASS: {
916
            SS.UpdateCenterOfMass();
917
            SS.centerOfMass.draw = true;
918
            SS.GW.Invalidate();
919
            break;
920
        }
921

922
        case Command::VOLUME: {
923
            Group *g = SK.GetGroup(SS.GW.activeGroup);
924
            double totalVol = g->displayMesh.CalculateVolume();
925
            std::string msg = ssprintf(
926
                _("The volume of the solid model is:\n\n"
927
                  "    %s"),
928
                SS.MmToStringSI(totalVol, /*dim=*/3).c_str());
929

930
            SMesh curMesh = {};
931
            g->thisShell.TriangulateInto(&curMesh);
932
            double curVol = curMesh.CalculateVolume();
933
            if(curVol > 0.0) {
934
                msg += ssprintf(
935
                    _("\nThe volume of current group mesh is:\n\n"
936
                      "    %s"),
937
                    SS.MmToStringSI(curVol, /*dim=*/3).c_str());
938
            }
939

940
            msg += _("\n\nCurved surfaces have been approximated as triangles.\n"
941
                     "This introduces error, typically of around 1%.");
942
            Message("%s", msg.c_str());
943
            break;
944
        }
945

946
        case Command::AREA: {
947
            Group *g = SK.GetGroup(SS.GW.activeGroup);
948
            SS.GW.GroupSelection();
949

950
            if(gs.faces > 0) {
951
                std::vector<uint32_t> faces;
952
                faces.push_back(gs.face[0].v);
953
                if(gs.faces > 1) faces.push_back(gs.face[1].v);
954
                double area = g->displayMesh.CalculateSurfaceArea(faces);
955
                Message(_("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%%."),
959
                    SS.MmToStringSI(area, /*dim=*/2).c_str());
960
                break;
961
            }
962

963
            if(g->polyError.how != PolyError::GOOD) {
964
                Error(_("This group does not contain a correctly-formed "
965
                        "2d closed area. It is open, not coplanar, or self-"
966
                        "intersecting."));
967
                break;
968
            }
969
            SEdgeList sel = {};
970
            g->polyLoops.MakeEdgesInto(&sel);
971
            SPolygon sp = {};
972
            sel.AssemblePolygon(&sp, NULL, /*keepDir=*/true);
973
            sp.normal = sp.ComputeNormal();
974
            sp.FixContourDirections();
975
            double area = sp.SignedArea();
976
            Message(_("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%%."),
980
                SS.MmToStringSI(area, /*dim=*/2).c_str());
981
            sel.Clear();
982
            sp.Clear();
983
            break;
984
        }
985

986
        case Command::PERIMETER: {
987
            if(gs.n > 0 && gs.n == gs.entities) {
988
                double perimeter = 0.0;
989
                for(int i = 0; i < gs.entities; i++) {
990
                    Entity *en = SK.entity.FindById(gs.entity[i]);
991
                    SEdgeList *el = en->GetOrGenerateEdges();
992
                    for(const SEdge &e : el->l) {
993
                        perimeter += e.b.Minus(e.a).Magnitude();
994
                    }
995
                }
996
                Message(_("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%%."),
1000
                    SS.MmToStringSI(perimeter, /*dim=*/1).c_str());
1001
            } else {
1002
                Error(_("Bad selection for perimeter; select line segments, arcs, and curves."));
1003
            }
1004
            break;
1005
        }
1006

1007
        case Command::SHOW_DOF:
1008
            // This works like a normal solve, except that it calculates
1009
            // which variables are free/bound at the same time.
1010
            SS.GenerateAll(SolveSpaceUI::Generate::ALL, /*andFindFree=*/true);
1011
            break;
1012

1013
        case Command::TRACE_PT:
1014
            if(gs.points == 1 && gs.n == 1) {
1015
                SS.traced.point = gs.point[0];
1016
                SS.GW.ClearSelection();
1017
            } else {
1018
                Error(_("Bad selection for trace; select a single point."));
1019
            }
1020
            break;
1021

1022
        case Command::STOP_TRACING: {
1023
            if (SS.traced.point == Entity::NO_ENTITY) {
1024
                break;
1025
            }
1026
            Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(SS.GW.window);
1027
            dialog->AddFilters(Platform::CsvFileFilters);
1028
            dialog->ThawChoices(settings, "Trace");
1029
            dialog->SetFilename(SS.saveFile);
1030
            if(dialog->RunModal()) {
1031
                dialog->FreezeChoices(settings, "Trace");
1032

1033
                FILE *f = OpenFile(dialog->GetFilename(), "wb");
1034
                if(f) {
1035
                    int i;
1036
                    SContour *sc = &(SS.traced.path);
1037
                    for(i = 0; i < sc->l.n; i++) {
1038
                        Vector p = sc->l[i].p;
1039
                        double s = SS.exportScale;
1040
                        fprintf(f, "%.10f, %.10f, %.10f\r\n",
1041
                            p.x/s, p.y/s, p.z/s);
1042
                    }
1043
                    fclose(f);
1044
                } else {
1045
                    Error(_("Couldn't write to '%s'"), dialog->GetFilename().raw.c_str());
1046
                }
1047
            }
1048
            // Clear the trace, and stop tracing
1049
            SS.traced.point = Entity::NO_ENTITY;
1050
            SS.traced.path.l.Clear();
1051
            SS.GW.Invalidate();
1052
            break;
1053
        }
1054

1055
        default: ssassert(false, "Unexpected menu ID");
1056
    }
1057
}
1058

1059
void SolveSpaceUI::ShowNakedEdges(bool reportOnlyWhenNotOkay) {
1060
    SS.nakedEdges.Clear();
1061

1062
    Group *g = SK.GetGroup(SS.GW.activeGroup);
1063
    SMesh *m = &(g->displayMesh);
1064
    SKdNode *root = SKdNode::From(m);
1065
    bool inters, leaks;
1066
    root->MakeCertainEdgesInto(&(SS.nakedEdges),
1067
        EdgeKind::NAKED_OR_SELF_INTER, /*coplanarIsInter=*/true, &inters, &leaks);
1068

1069
    if(reportOnlyWhenNotOkay && !inters && !leaks && SS.nakedEdges.l.IsEmpty()) {
1070
        return;
1071
    }
1072
    SS.GW.Invalidate();
1073

1074
    const char *intersMsg = inters ?
1075
        _("The mesh is self-intersecting (NOT okay, invalid).") :
1076
        _("The mesh is not self-intersecting (okay, valid).");
1077
    const char *leaksMsg = leaks ?
1078
        _("The mesh has naked edges (NOT okay, invalid).") :
1079
        _("The mesh is watertight (okay, valid).");
1080

1081
    std::string cntMsg = ssprintf(
1082
        _("\n\nThe model contains %d triangles, from %d surfaces."),
1083
        g->displayMesh.l.n, g->runningShell.surface.n);
1084

1085
    if(SS.nakedEdges.l.IsEmpty()) {
1086
        Message(_("%s\n\n%s\n\nZero problematic edges, good.%s"),
1087
            intersMsg, leaksMsg, cntMsg.c_str());
1088
    } else {
1089
        Error(_("%s\n\n%s\n\n%d problematic edges, bad.%s"),
1090
            intersMsg, leaksMsg, SS.nakedEdges.l.n, cntMsg.c_str());
1091
    }
1092
}
1093

1094
void SolveSpaceUI::MenuHelp(Command id) {
1095
    switch(id) {
1096
        case Command::WEBSITE:
1097
            Platform::OpenInBrowser("http://solvespace.com/helpmenu");
1098
            break;
1099

1100
        case Command::ABOUT:
1101
            Message(_(
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"),
1114
PACKAGE_VERSION, 2024);
1115
            break;
1116

1117
        case Command::GITHUB:
1118
            Platform::OpenInBrowser(GIT_HASH_URL);
1119
            break;
1120

1121
        default: ssassert(false, "Unexpected menu ID");
1122
    }
1123
}
1124

1125
void SolveSpaceUI::Clear() {
1126
    sys.Clear();
1127
    for(int i = 0; i < MAX_UNDO; i++) {
1128
        if(i < undo.cnt) undo.d[i].Clear();
1129
        if(i < redo.cnt) redo.d[i].Clear();
1130
    }
1131
    TW.window = NULL;
1132
    GW.openRecentMenu = NULL;
1133
    GW.linkRecentMenu = NULL;
1134
    GW.showGridMenuItem = NULL;
1135
    GW.dimSolidModelMenuItem = NULL;
1136
    GW.perspectiveProjMenuItem = NULL;
1137
    GW.explodeMenuItem = NULL;
1138
    GW.showToolbarMenuItem = NULL;
1139
    GW.showTextWndMenuItem = NULL;
1140
    GW.fullScreenMenuItem = NULL;
1141
    GW.unitsMmMenuItem = NULL;
1142
    GW.unitsMetersMenuItem = NULL;
1143
    GW.unitsInchesMenuItem = NULL;
1144
    GW.unitsFeetInchesMenuItem = NULL;
1145
    GW.inWorkplaneMenuItem = NULL;
1146
    GW.in3dMenuItem = NULL;
1147
    GW.undoMenuItem = NULL;
1148
    GW.redoMenuItem = NULL;
1149
    GW.window = NULL;
1150
}
1151

1152
void Sketch::Clear() {
1153
    group.Clear();
1154
    groupOrder.Clear();
1155
    constraint.Clear();
1156
    request.Clear();
1157
    style.Clear();
1158
    entity.Clear();
1159
    param.Clear();
1160
}
1161

1162
BBox Sketch::CalculateEntityBBox(bool includingInvisible) {
1163
    BBox box = {};
1164
    bool first = true;
1165

1166
    auto includePoint = [&](const Vector &point) {
1167
        if(first) {
1168
            box.minp = point;
1169
            box.maxp = point;
1170
            first = false;
1171
        } else {
1172
            box.Include(point);
1173
        }
1174
    };
1175

1176
    for(const Entity &e : entity) {
1177
        if(e.construction) continue;
1178
        if(!(includingInvisible || e.IsVisible())) continue;
1179

1180
        // arc center point shouldn't be included in bounding box calculation
1181
        if(e.IsPoint() && e.h.isFromRequest()) {
1182
            Request *r = SK.GetRequest(e.h.request());
1183
            if(r->type == Request::Type::ARC_OF_CIRCLE && e.h == r->h.entity(1)) {
1184
                continue;
1185
            }
1186
        }
1187

1188
        if(e.IsPoint()) {
1189
            includePoint(e.PointGetNum());
1190
            continue;
1191
        }
1192

1193
        switch(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.
1197
            case Entity::Type::CIRCLE:
1198
            case Entity::Type::ARC_OF_CIRCLE: {
1199
                SBezierList sbl = {};
1200
                e.GenerateBezierCurves(&sbl);
1201

1202
                for(const SBezier &sb : sbl.l) {
1203
                    for(int j = 0; j <= sb.deg; j++) {
1204
                        includePoint(sb.ctrl[j]);
1205
                    }
1206
                }
1207
                sbl.Clear();
1208
                continue;
1209
            }
1210

1211
            default:
1212
                continue;
1213
        }
1214
    }
1215

1216
    return box;
1217
}
1218

1219
Group *Sketch::GetRunningMeshGroupFor(hGroup h) {
1220
    Group *g = GetGroup(h);
1221
    while(g != NULL) {
1222
        if(g->IsMeshGroup()) {
1223
            return g;
1224
        }
1225
        g = g->PreviousGroup();
1226
    }
1227
    return NULL;
1228
}
1229

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.