Solvespace

Форк
0
/
graphicswin.cpp 
1480 строк · 60.6 Кб
1
//-----------------------------------------------------------------------------
2
// Top-level implementation of the program's main window, in which a graphical
3
// representation of the model is drawn and edited by the user.
4
//
5
// Copyright 2008-2013 Jonathan Westhues.
6
//-----------------------------------------------------------------------------
7
#include "solvespace.h"
8

9
typedef void MenuHandler(Command id);
10
using MenuKind = Platform::MenuItem::Indicator;
11
struct MenuEntry {
12
    int          level;          // 0 == on menu bar, 1 == one level down
13
    const char  *label;          // or NULL for a separator
14
    Command      cmd;            // command ID
15
    int          accel;          // keyboard accelerator
16
    MenuKind     kind;
17
    MenuHandler *fn;
18
};
19

20
#define mView (&GraphicsWindow::MenuView)
21
#define mEdit (&GraphicsWindow::MenuEdit)
22
#define mClip (&GraphicsWindow::MenuClipboard)
23
#define mReq  (&GraphicsWindow::MenuRequest)
24
#define mCon  (&Constraint::MenuConstrain)
25
#define mFile (&SolveSpaceUI::MenuFile)
26
#define mGrp  (&Group::MenuGroup)
27
#define mAna  (&SolveSpaceUI::MenuAnalyze)
28
#define mHelp (&SolveSpaceUI::MenuHelp)
29
#define SHIFT_MASK 0x100
30
#define CTRL_MASK  0x200
31
#define FN_MASK    0x400
32

33
#define S     SHIFT_MASK
34
#define C     CTRL_MASK
35
#define F     FN_MASK
36
#define KN    MenuKind::NONE
37
#define KC    MenuKind::CHECK_MARK
38
#define KR    MenuKind::RADIO_MARK
39
const MenuEntry Menu[] = {
40
//lv label                              cmd                        accel    kind
41
{ 0, N_("&File"),                       Command::NONE,             0,       KN, NULL   },
42
{ 1, N_("&New"),                        Command::NEW,              C|'n',   KN, mFile  },
43
{ 1, N_("&Open..."),                    Command::OPEN,             C|'o',   KN, mFile  },
44
{ 1, N_("Open &Recent"),                Command::OPEN_RECENT,      0,       KN, mFile  },
45
{ 1, N_("&Save"),                       Command::SAVE,             C|'s',   KN, mFile  },
46
{ 1, N_("Save &As..."),                 Command::SAVE_AS,          C|S|'s', KN, mFile  },
47
{ 1,  NULL,                             Command::NONE,             0,       KN, NULL   },
48
{ 1, N_("Export &Image..."),            Command::EXPORT_IMAGE,     0,       KN, mFile  },
49
{ 1, N_("Export 2d &View..."),          Command::EXPORT_VIEW,      0,       KN, mFile  },
50
{ 1, N_("Export 2d &Section..."),       Command::EXPORT_SECTION,   0,       KN, mFile  },
51
{ 1, N_("Export 3d &Wireframe..."),     Command::EXPORT_WIREFRAME, 0,       KN, mFile  },
52
{ 1, N_("Export Triangle &Mesh..."),    Command::EXPORT_MESH,      0,       KN, mFile  },
53
{ 1, N_("Export &Surfaces..."),         Command::EXPORT_SURFACES,  0,       KN, mFile  },
54
{ 1, N_("Im&port..."),                  Command::IMPORT,           0,       KN, mFile  },
55
#ifndef __APPLE__
56
{ 1,  NULL,                             Command::NONE,             0,       KN, NULL   },
57
{ 1, N_("E&xit"),                       Command::EXIT,             C|'q',   KN, mFile  },
58
#endif
59

60
{ 0, N_("&Edit"),                       Command::NONE,             0,       KN, NULL   },
61
{ 1, N_("&Undo"),                       Command::UNDO,             C|'z',   KN, mEdit  },
62
{ 1, N_("&Redo"),                       Command::REDO,             C|'y',   KN, mEdit  },
63
{ 1, N_("Re&generate All"),             Command::REGEN_ALL,        ' ',     KN, mEdit  },
64
{ 1,  NULL,                             Command::NONE,             0,       KN, NULL   },
65
{ 1, N_("Snap Selection to &Grid"),     Command::SNAP_TO_GRID,     '.',     KN, mEdit  },
66
{ 1, N_("Rotate Imported &90°"),        Command::ROTATE_90,        '9',     KN, mEdit  },
67
{ 1,  NULL,                             Command::NONE,             0,       KN, NULL   },
68
{ 1, N_("Cu&t"),                        Command::CUT,              C|'x',   KN, mClip  },
69
{ 1, N_("&Copy"),                       Command::COPY,             C|'c',   KN, mClip  },
70
{ 1, N_("&Paste"),                      Command::PASTE,            C|'v',   KN, mClip  },
71
{ 1, N_("Paste &Transformed..."),       Command::PASTE_TRANSFORM,  C|'t',   KN, mClip  },
72
{ 1, N_("&Delete"),                     Command::DELETE,           '\x7f',  KN, mClip  },
73
{ 1,  NULL,                             Command::NONE,             0,       KN, NULL   },
74
{ 1, N_("Select &Edge Chain"),          Command::SELECT_CHAIN,     C|'e',   KN, mEdit  },
75
{ 1, N_("Select &All"),                 Command::SELECT_ALL,       C|'a',   KN, mEdit  },
76
{ 1, N_("&Unselect All"),               Command::UNSELECT_ALL,     '\x1b',  KN, mEdit  },
77
{ 1,  NULL,                             Command::NONE,             0,       KN, NULL   },
78
{ 1, N_("&Line Styles..."),             Command::EDIT_LINE_STYLES, 0,       KN, mEdit  },
79
{ 1, N_("&View Projection..."),         Command::VIEW_PROJECTION,  0,       KN, mEdit  },
80
#ifndef __APPLE__
81
{ 1, N_("Con&figuration..."),           Command::CONFIGURATION,    0,       KN, mEdit  },
82
#endif
83

84
{ 0, N_("&View"),                       Command::NONE,             0,       KN, mView  },
85
{ 1, N_("Zoom &In"),                    Command::ZOOM_IN,          '+',     KN, mView  },
86
{ 1, N_("Zoom &Out"),                   Command::ZOOM_OUT,         '-',     KN, mView  },
87
{ 1, N_("Zoom To &Fit"),                Command::ZOOM_TO_FIT,      'f',     KN, mView  },
88
{ 1,  NULL,                             Command::NONE,             0,       KN, NULL   },
89
{ 1, N_("Align View to &Workplane"),    Command::ONTO_WORKPLANE,   'w',     KN, mView  },
90
{ 1, N_("Nearest &Ortho View"),         Command::NEAREST_ORTHO,    F|2,     KN, mView  },
91
{ 1, N_("Nearest &Isometric View"),     Command::NEAREST_ISO,      F|3,     KN, mView  },
92
{ 1, N_("&Center View At Point"),       Command::CENTER_VIEW,      F|4,     KN, mView  },
93
{ 1,  NULL,                             Command::NONE,             0,       KN, NULL   },
94
{ 1, N_("Show Snap &Grid"),             Command::SHOW_GRID,        '>',     KC, mView  },
95
{ 1, N_("Darken Inactive Solids"),      Command::DIM_SOLID_MODEL,  0,       KC, mView  },
96
{ 1, N_("Use &Perspective Projection"), Command::PERSPECTIVE_PROJ, '`',     KC, mView  },
97
{ 1, N_("Show E&xploded View"),         Command::EXPLODE_SKETCH,   '\\',    KC, mView  },
98
{ 1, N_("Dimension &Units"),            Command::NONE,             0,       KN, NULL  },
99
{ 2, N_("Dimensions in &Millimeters"),  Command::UNITS_MM,         0,       KR, mView },
100
{ 2, N_("Dimensions in M&eters"),       Command::UNITS_METERS,     0,       KR, mView },
101
{ 2, N_("Dimensions in &Inches"),       Command::UNITS_INCHES,     0,       KR, mView },
102
{ 2, N_("Dimensions in &Feet and Inches"), Command::UNITS_FEET_INCHES, 0,   KR, mView },
103
{ 1,  NULL,                             Command::NONE,             0,       KN, NULL   },
104
{ 1, N_("Show &Toolbar"),               Command::SHOW_TOOLBAR,     C|'\t',  KC, mView  },
105
{ 1, N_("Show Property Bro&wser"),      Command::SHOW_TEXT_WND,    '\t',    KC, mView  },
106
{ 1,  NULL,                             Command::NONE,             0,       KN, NULL   },
107
{ 1, N_("&Full Screen"),                Command::FULL_SCREEN,      C|F|11,  KC, mView  },
108

109
{ 0, N_("&New Group"),                  Command::NONE,             0,       KN, mGrp   },
110
{ 1, N_("Sketch In &3d"),               Command::GROUP_3D,         S|'3',   KN, mGrp   },
111
{ 1, N_("Sketch In New &Workplane"),    Command::GROUP_WRKPL,      S|'w',   KN, mGrp   },
112
{ 1, NULL,                              Command::NONE,             0,       KN, NULL   },
113
{ 1, N_("Step &Translating"),           Command::GROUP_TRANS,      S|'t',   KN, mGrp   },
114
{ 1, N_("Step &Rotating"),              Command::GROUP_ROT,        S|'r',   KN, mGrp   },
115
{ 1, NULL,                              Command::NONE,             0,       KN, NULL   },
116
{ 1, N_("E&xtrude"),                    Command::GROUP_EXTRUDE,    S|'x',   KN, mGrp   },
117
{ 1, N_("&Helix"),                      Command::GROUP_HELIX,      S|'h',   KN, mGrp   },
118
{ 1, N_("&Lathe"),                      Command::GROUP_LATHE,      S|'l',   KN, mGrp   },
119
{ 1, N_("Re&volve"),                    Command::GROUP_REVOLVE,    S|'v',   KN, mGrp   },
120
{ 1, NULL,                              Command::NONE,             0,       KN, NULL   },
121
{ 1, N_("Link / Assemble..."),          Command::GROUP_LINK,       S|'i',   KN, mGrp   },
122
{ 1, N_("Link Recent"),                 Command::GROUP_RECENT,     0,       KN, mGrp   },
123

124
{ 0, N_("&Sketch"),                     Command::NONE,             0,       KN, mReq   },
125
{ 1, N_("In &Workplane"),               Command::SEL_WORKPLANE,    '2',     KR, mReq   },
126
{ 1, N_("Anywhere In &3d"),             Command::FREE_IN_3D,       '3',     KR, mReq   },
127
{ 1, NULL,                              Command::NONE,             0,       KN, NULL   },
128
{ 1, N_("Datum &Point"),                Command::DATUM_POINT,      'p',     KN, mReq   },
129
{ 1, N_("Wor&kplane"),                  Command::WORKPLANE,        0,       KN, mReq   },
130
{ 1, NULL,                              Command::NONE,             0,       KN, NULL   },
131
{ 1, N_("Line &Segment"),               Command::LINE_SEGMENT,     's',     KN, mReq   },
132
{ 1, N_("C&onstruction Line Segment"),  Command::CONSTR_SEGMENT,   S|'s',   KN, mReq   },
133
{ 1, N_("&Rectangle"),                  Command::RECTANGLE,        'r',     KN, mReq   },
134
{ 1, N_("&Circle"),                     Command::CIRCLE,           'c',     KN, mReq   },
135
{ 1, N_("&Arc of a Circle"),            Command::ARC,              'a',     KN, mReq   },
136
{ 1, N_("&Bezier Cubic Spline"),        Command::CUBIC,            'b',     KN, mReq   },
137
{ 1, NULL,                              Command::NONE,             0,       KN, NULL   },
138
{ 1, N_("&Text in TrueType Font"),      Command::TTF_TEXT,         't',     KN, mReq   },
139
{ 1, N_("I&mage"),                      Command::IMAGE,            0,       KN, mReq   },
140
{ 1, NULL,                              Command::NONE,             0,       KN, NULL   },
141
{ 1, N_("To&ggle Construction"),        Command::CONSTRUCTION,     'g',     KN, mReq   },
142
{ 1, N_("Ta&ngent Arc at Point"),       Command::TANGENT_ARC,      S|'a',   KN, mReq   },
143
{ 1, N_("Split Curves at &Intersection"), Command::SPLIT_CURVES,   'i',     KN, mReq   },
144

145
{ 0, N_("&Constrain"),                  Command::NONE,             0,       KN, mCon   },
146
{ 1, N_("&Distance / Diameter"),        Command::DISTANCE_DIA,     'd',     KN, mCon   },
147
{ 1, N_("Re&ference Dimension"),        Command::REF_DISTANCE,     S|'d',   KN, mCon   },
148
{ 1, N_("A&ngle / Equal Angle"),        Command::ANGLE,            'n',     KN, mCon   },
149
{ 1, N_("Reference An&gle"),            Command::REF_ANGLE,        S|'n',   KN, mCon   },
150
{ 1, N_("Other S&upplementary Angle"),  Command::OTHER_ANGLE,      'u',     KN, mCon   },
151
{ 1, N_("Toggle R&eference Dim"),       Command::REFERENCE,        'e',     KN, mCon   },
152
{ 1, NULL,                              Command::NONE,             0,       KN, NULL   },
153
{ 1, N_("&Horizontal"),                 Command::HORIZONTAL,       'h',     KN, mCon   },
154
{ 1, N_("&Vertical"),                   Command::VERTICAL,         'v',     KN, mCon   },
155
{ 1, NULL,                              Command::NONE,             0,       KN, NULL   },
156
{ 1, N_("&On Point / Curve / Plane"),   Command::ON_ENTITY,        'o',     KN, mCon   },
157
{ 1, N_("E&qual Length / Radius"), Command::EQUAL,         'q',     KN, mCon   },
158
{ 1, N_("Length / Arc Ra&tio"),         Command::RATIO,            'z',     KN, mCon   },
159
{ 1, N_("Length / Arc Diff&erence"),    Command::DIFFERENCE,       'j',     KN, mCon   },
160
{ 1, N_("At &Midpoint"),                Command::AT_MIDPOINT,      'm',     KN, mCon   },
161
{ 1, N_("S&ymmetric"),                  Command::SYMMETRIC,        'y',     KN, mCon   },
162
{ 1, N_("Para&llel / Tangent"),         Command::PARALLEL,         'l',     KN, mCon   },
163
{ 1, N_("&Perpendicular"),              Command::PERPENDICULAR,    '[',     KN, mCon   },
164
{ 1, N_("Same Orient&ation"),           Command::ORIENTED_SAME,    'x',     KN, mCon   },
165
{ 1, N_("Lock Point Where &Dragged"),   Command::WHERE_DRAGGED,    ']',     KN, mCon   },
166
{ 1, NULL,                              Command::NONE,             0,       KN, NULL   },
167
{ 1, N_("Comment"),                     Command::COMMENT,          ';',     KN, mCon   },
168

169
{ 0, N_("&Analyze"),                    Command::NONE,             0,       KN, mAna   },
170
{ 1, N_("Measure &Volume"),             Command::VOLUME,           C|S|'v', KN, mAna   },
171
{ 1, N_("Measure A&rea"),               Command::AREA,             C|S|'a', KN, mAna   },
172
{ 1, N_("Measure &Perimeter"),          Command::PERIMETER,        C|S|'p', KN, mAna   },
173
{ 1, N_("Show &Interfering Parts"),     Command::INTERFERENCE,     C|S|'i', KN, mAna   },
174
{ 1, N_("Show &Naked Edges"),           Command::NAKED_EDGES,      C|S|'n', KN, mAna   },
175
{ 1, N_("Show &Center of Mass"),        Command::CENTER_OF_MASS,   C|S|'c', KN, mAna   },
176
{ 1, NULL,                              Command::NONE,             0,       KN, NULL   },
177
{ 1, N_("Show &Underconstrained Points"), Command::SHOW_DOF,       C|S|'f', KN, mAna   },
178
{ 1, NULL,                              Command::NONE,             0,       KN, NULL   },
179
{ 1, N_("&Trace Point"),                Command::TRACE_PT,         C|S|'t', KN, mAna   },
180
{ 1, N_("&Stop Tracing..."),            Command::STOP_TRACING,     C|S|'s', KN, mAna   },
181
{ 1, N_("Step &Dimension..."),          Command::STEP_DIM,         C|S|'d', KN, mAna   },
182

183
{ 0, N_("&Help"),                       Command::NONE,             0,       KN, mHelp  },
184
{ 1, N_("&Language"),                   Command::LOCALE,           0,       KN, mHelp  },
185
{ 1, N_("&Website / Manual"),           Command::WEBSITE,          0,       KN, mHelp  },
186
{ 1, N_("&Go to GitHub commit"),        Command::GITHUB,            0,       KN, mHelp  },
187
#ifndef __APPLE__
188
{ 1, N_("&About"),                      Command::ABOUT,            0,       KN, mHelp  },
189
#endif
190
{ -1, 0,                                Command::NONE,             0,       KN, NULL   }
191
};
192
#undef S
193
#undef C
194
#undef F
195
#undef KN
196
#undef KC
197
#undef KR
198

199
void GraphicsWindow::ActivateCommand(Command cmd) {
200
    for(int i = 0; Menu[i].level >= 0; i++) {
201
        if(cmd == Menu[i].cmd) {
202
            (Menu[i].fn)((Command)Menu[i].cmd);
203
            break;
204
        }
205
    }
206
}
207

208
Platform::KeyboardEvent GraphicsWindow::AcceleratorForCommand(Command cmd) {
209
    int rawAccel = 0;
210
    for(int i = 0; Menu[i].level >= 0; i++) {
211
        if(cmd == Menu[i].cmd) {
212
            rawAccel = Menu[i].accel;
213
            break;
214
        }
215
    }
216

217
    Platform::KeyboardEvent accel = {};
218
    accel.type = Platform::KeyboardEvent::Type::PRESS;
219
    if(rawAccel & SHIFT_MASK) {
220
        accel.shiftDown = true;
221
    }
222
    if(rawAccel & CTRL_MASK) {
223
        accel.controlDown = true;
224
    }
225
    if(rawAccel & FN_MASK) {
226
        accel.key = Platform::KeyboardEvent::Key::FUNCTION;
227
        accel.num = rawAccel & 0xff;
228
    } else {
229
        accel.key = Platform::KeyboardEvent::Key::CHARACTER;
230
        accel.chr = (char)(rawAccel & 0xff);
231
    }
232

233
    return accel;
234
}
235

236
bool GraphicsWindow::KeyboardEvent(Platform::KeyboardEvent event) {
237
    using Platform::KeyboardEvent;
238

239
    if(event.type == KeyboardEvent::Type::RELEASE)
240
        return true;
241

242
    if(event.key == KeyboardEvent::Key::CHARACTER) {
243
        if(event.chr == '\b') {
244
            // Treat backspace identically to escape.
245
            MenuEdit(Command::UNSELECT_ALL);
246
            return true;
247
        } else if(event.chr == '=') {
248
            // Treat = as +. This is specific to US (and US-compatible) keyboard layouts,
249
            // but makes zooming from keyboard much more usable on these.
250
            // Ideally we'd have a platform-independent way of binding to a particular
251
            // physical key regardless of shift status...
252
            MenuView(Command::ZOOM_IN);
253
            return true;
254
        }
255
    }
256

257
    // On some platforms, the OS does not handle some or all keyboard accelerators,
258
    // so handle them here.
259
    for(int i = 0; Menu[i].level >= 0; i++) {
260
        if(AcceleratorForCommand(Menu[i].cmd).Equals(event)) {
261
            ActivateCommand(Menu[i].cmd);
262
            return true;
263
        }
264
    }
265

266
    return false;
267
}
268

269
void GraphicsWindow::PopulateMainMenu() {
270
    bool unique = false;
271
    Platform::MenuBarRef mainMenu = Platform::GetOrCreateMainMenu(&unique);
272
    if(unique) mainMenu->Clear();
273

274
    Platform::MenuRef currentSubMenu;
275
    std::vector<Platform::MenuRef> subMenuStack;
276
    for(int i = 0; Menu[i].level >= 0; i++) {
277
        while(Menu[i].level > 0 && Menu[i].level <= (int)subMenuStack.size()) {
278
            currentSubMenu = subMenuStack.back();
279
            subMenuStack.pop_back();
280
        }
281

282
        if(Menu[i].label == NULL) {
283
            currentSubMenu->AddSeparator();
284
            continue;
285
        }
286

287
        std::string label = Translate(Menu[i].label);
288
        if(Menu[i].level == 0) {
289
            currentSubMenu = mainMenu->AddSubMenu(label);
290
        } else if(Menu[i].cmd == Command::OPEN_RECENT) {
291
            openRecentMenu = currentSubMenu->AddSubMenu(label);
292
        } else if(Menu[i].cmd == Command::GROUP_RECENT) {
293
            linkRecentMenu = currentSubMenu->AddSubMenu(label);
294
        } else if(Menu[i].cmd == Command::LOCALE) {
295
            Platform::MenuRef localeMenu = currentSubMenu->AddSubMenu(label);
296
            for(const Locale &locale : Locales()) {
297
                localeMenu->AddItem(locale.displayName, [&]() {
298
                    SetLocale(locale.Culture());
299
                    Platform::GetSettings()->FreezeString("Locale", locale.Culture());
300

301
                    SS.UpdateWindowTitles();
302
                    PopulateMainMenu();
303
                    SS.GW.EnsureValidActives();
304
                });
305
            }
306
        } else if(Menu[i].fn == NULL) {
307
            subMenuStack.push_back(currentSubMenu);
308
            currentSubMenu = currentSubMenu->AddSubMenu(label);
309
        } else {
310
            Platform::MenuItemRef menuItem = currentSubMenu->AddItem(label);
311
            menuItem->SetIndicator(Menu[i].kind);
312
            if(Menu[i].accel != 0) {
313
                menuItem->SetAccelerator(AcceleratorForCommand(Menu[i].cmd));
314
            }
315
            menuItem->onTrigger = std::bind(Menu[i].fn, Menu[i].cmd);
316

317
            if(Menu[i].cmd == Command::SHOW_GRID) {
318
                showGridMenuItem = menuItem;
319
            } else if(Menu[i].cmd == Command::DIM_SOLID_MODEL) {
320
                dimSolidModelMenuItem = menuItem;
321
            } else if(Menu[i].cmd == Command::PERSPECTIVE_PROJ) {
322
                perspectiveProjMenuItem = menuItem;
323
            } else if(Menu[i].cmd == Command::EXPLODE_SKETCH) {
324
                explodeMenuItem = menuItem;
325
            } else if(Menu[i].cmd == Command::SHOW_TOOLBAR) {
326
                showToolbarMenuItem = menuItem;
327
            } else if(Menu[i].cmd == Command::SHOW_TEXT_WND) {
328
                showTextWndMenuItem = menuItem;
329
            } else if(Menu[i].cmd == Command::FULL_SCREEN) {
330
                fullScreenMenuItem = menuItem;
331
            } else if(Menu[i].cmd == Command::UNITS_MM) {
332
                unitsMmMenuItem = menuItem;
333
            } else if(Menu[i].cmd == Command::UNITS_METERS) {
334
                unitsMetersMenuItem = menuItem;
335
            } else if(Menu[i].cmd == Command::UNITS_INCHES) {
336
                unitsInchesMenuItem = menuItem;
337
            } else if(Menu[i].cmd == Command::UNITS_FEET_INCHES) {
338
                unitsFeetInchesMenuItem = menuItem;
339
            } else if(Menu[i].cmd == Command::SEL_WORKPLANE) {
340
                inWorkplaneMenuItem = menuItem;
341
            } else if(Menu[i].cmd == Command::FREE_IN_3D) {
342
                in3dMenuItem = menuItem;
343
            } else if(Menu[i].cmd == Command::UNDO) {
344
                undoMenuItem = menuItem;
345
            } else if(Menu[i].cmd == Command::REDO) {
346
                redoMenuItem = menuItem;
347
            }
348
        }
349
    }
350

351
    PopulateRecentFiles();
352
    SS.UndoEnableMenus();
353

354
    window->SetMenuBar(mainMenu);
355
}
356

357
static void PopulateMenuWithPathnames(Platform::MenuRef menu,
358
                                      std::vector<Platform::Path> pathnames,
359
                                      std::function<void(const Platform::Path &)> onTrigger) {
360
    menu->Clear();
361
    if(pathnames.empty()) {
362
        Platform::MenuItemRef menuItem = menu->AddItem(_("(no recent files)"));
363
        menuItem->SetEnabled(false);
364
    } else {
365
        for(Platform::Path pathname : pathnames) {
366
            Platform::MenuItemRef menuItem = menu->AddItem(pathname.raw, [=]() {
367
                if(FileExists(pathname)) {
368
                    onTrigger(pathname);
369
                } else {
370
                    Error(_("File '%s' does not exist."), pathname.raw.c_str());
371
                }
372
            }, /*mnemonics=*/false);
373
        }
374
    }
375
}
376

377
void GraphicsWindow::PopulateRecentFiles() {
378
    PopulateMenuWithPathnames(openRecentMenu, SS.recentFiles, [](const Platform::Path &path) {
379
        // OkayToStartNewFile could mutate recentFiles, which will invalidate path (which is a
380
        // reference into the recentFiles vector), so take a copy of it here.
381
        Platform::Path pathCopy(path);
382
        if(!SS.OkayToStartNewFile()) return;
383
        SS.Load(pathCopy);
384
    });
385

386
    PopulateMenuWithPathnames(linkRecentMenu, SS.recentFiles, [](const Platform::Path &path) {
387
        Group::MenuGroup(Command::GROUP_LINK, path);
388
    });
389
}
390

391
void GraphicsWindow::Init() {
392
    scale     = 5;
393
    offset    = Vector::From(0, 0, 0);
394
    projRight = Vector::From(1, 0, 0);
395
    projUp    = Vector::From(0, 1, 0);
396

397
    // Make sure those are valid; could get a mouse move without a mouse
398
    // down if someone depresses the button, then drags into our window.
399
    orig.projRight = projRight;
400
    orig.projUp = projUp;
401

402
    // And with the last group active
403
    ssassert(!SK.groupOrder.IsEmpty(),
404
             "Group order can't be empty since we will activate the last group.");
405
    activeGroup = *SK.groupOrder.Last();
406
    SK.GetGroup(activeGroup)->Activate();
407

408
    showWorkplanes = false;
409
    showNormals = true;
410
    showPoints = true;
411
    showConstruction = true;
412
    showConstraints = ShowConstraintMode::SCM_SHOW_ALL;
413
    showShaded = true;
414
    showEdges = true;
415
    showMesh = false;
416
    showOutlines = false;
417
    showFacesDrawing = false;
418
    showFacesNonDrawing = true;
419
    drawOccludedAs = DrawOccludedAs::INVISIBLE;
420

421
    showTextWindow = true;
422

423
    showSnapGrid = false;
424
    dimSolidModel = true;
425
    context.active = false;
426
    toolbarHovered = Command::NONE;
427

428
    if(!window) {
429
        window = Platform::CreateWindow();
430
        if(window) {
431
            using namespace std::placeholders;
432
            // Do this first, so that if it causes an onRender event we don't try to paint without
433
            // a canvas.
434
            window->SetMinContentSize(720, /*ToolbarDrawOrHitTest 636*/ 32 * 18 + 3 * 16 + 8 + 4);
435
            window->onClose = std::bind(&SolveSpaceUI::MenuFile, Command::EXIT);
436
            window->onContextLost = [&] {
437
                canvas = NULL;
438
                persistentCanvas = NULL;
439
                persistentDirty = true;
440
            };
441
            window->onRender = std::bind(&GraphicsWindow::Paint, this);
442
            window->onKeyboardEvent = std::bind(&GraphicsWindow::KeyboardEvent, this, _1);
443
            window->onMouseEvent = std::bind(&GraphicsWindow::MouseEvent, this, _1);
444
            window->onSixDofEvent = std::bind(&GraphicsWindow::SixDofEvent, this, _1);
445
            window->onEditingDone = std::bind(&GraphicsWindow::EditControlDone, this, _1);
446
            PopulateMainMenu();
447
        }
448
    }
449

450
    if(window) {
451
        canvas = CreateRenderer();
452
        if(canvas) {
453
            persistentCanvas = canvas->CreateBatch();
454
            persistentDirty = true;
455
        }
456
    }
457

458
    // Do this last, so that all the menus get updated correctly.
459
    ClearSuper();
460
}
461

462
void GraphicsWindow::AnimateOntoWorkplane() {
463
    if(!LockedInWorkplane()) return;
464

465
    Entity *w = SK.GetEntity(ActiveWorkplane());
466
    Quaternion quatf = w->Normal()->NormalGetNum();
467

468
    // Get Z pointing vertical, if we're on turntable nav mode:
469
    if(SS.turntableNav) {
470
        Vector normalRight = quatf.RotationU();
471
        Vector normalUp    = quatf.RotationV();
472
        Vector normal      = normalRight.Cross(normalUp);
473
        if(normalRight.z != 0) {
474
            double theta = atan2(normalUp.z, normalRight.z);
475
            theta -= atan2(1, 0);
476
            normalRight = normalRight.RotatedAbout(normal, theta);
477
            normalUp    = normalUp.RotatedAbout(normal, theta);
478
            quatf       = Quaternion::From(normalRight, normalUp);
479
        }
480
    }
481

482
    Vector offsetf = (SK.GetEntity(w->point[0])->PointGetNum()).ScaledBy(-1);
483

484
    // If the view screen is open, then we need to refresh it.
485
    SS.ScheduleShowTW();
486

487
    AnimateOnto(quatf, offsetf);
488
}
489

490
void GraphicsWindow::AnimateOnto(Quaternion quatf, Vector offsetf) {
491
    // Get our initial orientation and translation.
492
    Quaternion quat0 = Quaternion::From(projRight, projUp);
493
    Vector offset0 = offset;
494

495
    // Make sure we take the shorter of the two possible paths.
496
    double mp = (quatf.Minus(quat0)).Magnitude();
497
    double mm = (quatf.Plus(quat0)).Magnitude();
498
    if(mp > mm) {
499
        quatf = quatf.ScaledBy(-1);
500
        mp = mm;
501
    }
502
    double mo = (offset0.Minus(offsetf)).Magnitude()*scale;
503

504
    // Animate transition, unless it's a tiny move.
505
    int64_t t0 = GetMilliseconds();
506
    int32_t dt = (mp < 0.01 && mo < 10) ? (-20) :
507
                     (int32_t)(100 + 600*mp + 0.4*mo);
508
    // Don't ever animate for longer than 800 ms; we can get absurdly
509
    // long translations (as measured in pixels) if the user zooms out, moves,
510
    // and then zooms in again.
511
    if(dt > 800) dt = 800;
512
    Quaternion dq = quatf.Times(quat0.Inverse());
513

514
    if(!animateTimer) {
515
        animateTimer = Platform::CreateTimer();
516
    }
517
    animateTimer->onTimeout = [=] {
518
        int64_t tn = GetMilliseconds();
519
        if((tn - t0) < dt) {
520
            animateTimer->RunAfterNextFrame();
521

522
            double s = (tn - t0)/((double)dt);
523
            offset = (offset0.ScaledBy(1 - s)).Plus(offsetf.ScaledBy(s));
524
            Quaternion quat = (dq.ToThe(s)).Times(quat0).WithMagnitude(1);
525

526
            projRight = quat.RotationU();
527
            projUp    = quat.RotationV();
528
        } else {
529
            projRight = quatf.RotationU();
530
            projUp    = quatf.RotationV();
531
            offset    = offsetf;
532
        }
533
        window->Invalidate();
534
    };
535
    animateTimer->RunAfterNextFrame();
536
}
537

538
void GraphicsWindow::HandlePointForZoomToFit(Vector p, Point2d *pmax, Point2d *pmin,
539
                                             double *wmin, bool usePerspective,
540
                                             const Camera &camera)
541
{
542
    double w;
543
    Vector pp = camera.ProjectPoint4(p, &w);
544
    // If usePerspective is true, then we calculate a perspective projection of the point.
545
    // If not, then we do a parallel projection regardless of the current
546
    // scale factor.
547
    if(usePerspective) {
548
        pp = pp.ScaledBy(1.0/w);
549
    }
550

551
    pmax->x = max(pmax->x, pp.x);
552
    pmax->y = max(pmax->y, pp.y);
553
    pmin->x = min(pmin->x, pp.x);
554
    pmin->y = min(pmin->y, pp.y);
555
    *wmin = min(*wmin, w);
556
}
557
void GraphicsWindow::LoopOverPoints(const std::vector<Entity *> &entities,
558
                                    const std::vector<Constraint *> &constraints,
559
                                    const std::vector<hEntity> &faces,
560
                                    Point2d *pmax, Point2d *pmin, double *wmin,
561
                                    bool usePerspective, bool includeMesh,
562
                                    const Camera &camera) {
563

564
    for(Entity *e : entities) {
565
        if(e->IsPoint()) {
566
            HandlePointForZoomToFit(e->PointGetNum(), pmax, pmin, wmin, usePerspective, camera);
567
        } else if(e->type == Entity::Type::CIRCLE) {
568
            // Lots of entities can extend outside the bbox of their points,
569
            // but circles are particularly bad. We want to get things halfway
570
            // reasonable without the mesh, because a zoom to fit is used to
571
            // set the zoom level to set the chord tol.
572
            double r = e->CircleGetRadiusNum();
573
            Vector c = SK.GetEntity(e->point[0])->PointGetNum();
574
            Quaternion q = SK.GetEntity(e->normal)->NormalGetNum();
575
            for(int j = 0; j < 4; j++) {
576
                Vector p = (j == 0) ? (c.Plus(q.RotationU().ScaledBy( r))) :
577
                           (j == 1) ? (c.Plus(q.RotationU().ScaledBy(-r))) :
578
                           (j == 2) ? (c.Plus(q.RotationV().ScaledBy( r))) :
579
                                      (c.Plus(q.RotationV().ScaledBy(-r)));
580
                HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera);
581
            }
582
        } else {
583
            // We have to iterate children points, because we can select entities without points
584
            for(int i = 0; i < MAX_POINTS_IN_ENTITY; i++) {
585
                if(e->point[i].v == 0) break;
586
                Vector p = SK.GetEntity(e->point[i])->PointGetNum();
587
                HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera);
588
            }
589
        }
590
    }
591

592
    for(Constraint *c : constraints) {
593
        std::vector<Vector> refs;
594
        c->GetReferencePoints(camera, &refs);
595
        for(Vector p : refs) {
596
            HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera);
597
        }
598
    }
599

600
    if(!includeMesh && faces.empty()) return;
601

602
    Group *g = SK.GetGroup(activeGroup);
603
    g->GenerateDisplayItems();
604
    for(int i = 0; i < g->displayMesh.l.n; i++) {
605
        STriangle *tr = &(g->displayMesh.l[i]);
606
        if(!includeMesh) {
607
            bool found = false;
608
            for(const hEntity &face : faces) {
609
                if(face.v != tr->meta.face) continue;
610
                found = true;
611
                break;
612
            }
613
            if(!found) continue;
614
        }
615
        HandlePointForZoomToFit(tr->a, pmax, pmin, wmin, usePerspective, camera);
616
        HandlePointForZoomToFit(tr->b, pmax, pmin, wmin, usePerspective, camera);
617
        HandlePointForZoomToFit(tr->c, pmax, pmin, wmin, usePerspective, camera);
618
    }
619
    if(!includeMesh) return;
620
    for(int i = 0; i < g->polyLoops.l.n; i++) {
621
        SContour *sc = &(g->polyLoops.l[i]);
622
        for(int j = 0; j < sc->l.n; j++) {
623
            HandlePointForZoomToFit(sc->l[j].p, pmax, pmin, wmin, usePerspective, camera);
624
        }
625
    }
626
}
627
void GraphicsWindow::ZoomToFit(bool includingInvisibles, bool useSelection) {
628
    if(!window) return;
629

630
    scale = ZoomToFit(GetCamera(), includingInvisibles, useSelection);
631
}
632
double GraphicsWindow::ZoomToFit(const Camera &camera,
633
                                 bool includingInvisibles, bool useSelection) {
634
    std::vector<Entity *> entities;
635
    std::vector<Constraint *> constraints;
636
    std::vector<hEntity> faces;
637

638
    if(useSelection) {
639
        for(int i = 0; i < selection.n; i++) {
640
            Selection *s = &selection[i];
641
            if(s->entity.v != 0) {
642
                Entity *e = SK.entity.FindById(s->entity);
643
                if(e->IsFace()) {
644
                    faces.push_back(e->h);
645
                    continue;
646
                }
647
                entities.push_back(e);
648
            }
649
            if(s->constraint.v != 0) {
650
                Constraint *c = SK.constraint.FindById(s->constraint);
651
                constraints.push_back(c);
652
            }
653
        }
654
    }
655

656
    bool selectionUsed = !entities.empty() || !constraints.empty() || !faces.empty();
657

658
    if(!selectionUsed) {
659
        for(Entity &e : SK.entity) {
660
            // we don't want to handle separate points, because we will iterate them inside entities.
661
            if(e.IsPoint()) continue;
662
            if(!includingInvisibles && !e.IsVisible()) continue;
663
            entities.push_back(&e);
664
        }
665

666
        for(Constraint &c : SK.constraint) {
667
            if(!c.IsVisible()) continue;
668
            constraints.push_back(&c);
669
        }
670
    }
671

672
    // On the first run, ignore perspective.
673
    Point2d pmax = { -1e12, -1e12 }, pmin = { 1e12, 1e12 };
674
    double wmin = 1;
675
    LoopOverPoints(entities, constraints, faces, &pmax, &pmin, &wmin,
676
                   /*usePerspective=*/false, /*includeMesh=*/!selectionUsed,
677
                   camera);
678

679
    double xm = (pmax.x + pmin.x)/2, ym = (pmax.y + pmin.y)/2;
680
    double dx = pmax.x - pmin.x, dy = pmax.y - pmin.y;
681

682
    offset = offset.Plus(projRight.ScaledBy(-xm)).Plus(
683
                         projUp.   ScaledBy(-ym));
684

685
    // And based on this, we calculate the scale and offset
686
    double scale;
687
    if(EXACT(dx == 0 && dy == 0)) {
688
        scale = 5;
689
    } else {
690
        double scalex = 1e12, scaley = 1e12;
691
        if(EXACT(dx != 0)) scalex = 0.9*camera.width /dx;
692
        if(EXACT(dy != 0)) scaley = 0.9*camera.height/dy;
693
        scale = min(scalex, scaley);
694

695
        scale = min(300.0, scale);
696
        scale = max(0.003, scale);
697
    }
698

699
    // Then do another run, considering the perspective.
700
    pmax.x = -1e12; pmax.y = -1e12;
701
    pmin.x =  1e12; pmin.y =  1e12;
702
    wmin = 1;
703
    LoopOverPoints(entities, constraints, faces, &pmax, &pmin, &wmin,
704
                   /*usePerspective=*/true, /*includeMesh=*/!selectionUsed,
705
                   camera);
706

707
    // Adjust the scale so that no points are behind the camera
708
    if(wmin < 0.1) {
709
        double k = camera.tangent;
710
        // w = 1+k*scale*z
711
        double zmin = (wmin - 1)/(k*scale);
712
        // 0.1 = 1 + k*scale*zmin
713
        // (0.1 - 1)/(k*zmin) = scale
714
        scale = min(scale, (0.1 - 1)/(k*zmin));
715
    }
716

717
    return scale;
718
}
719

720

721
void GraphicsWindow::ZoomToMouse(double zoomMultiplyer) {
722
    double offsetRight = offset.Dot(projRight);
723
    double offsetUp    = offset.Dot(projUp);
724

725
    double width, height;
726
    window->GetContentSize(&width, &height);
727

728
    double righti = currentMousePosition.x / scale - offsetRight;
729
    double upi    = currentMousePosition.y / scale - offsetUp;
730

731
    // zoomMultiplyer of 1 gives a default zoom factor of 1.2x: zoomMultiplyer * 1.2
732
    // zoom = adjusted zoom negative zoomMultiplyer will zoom out, positive will zoom in
733
    //
734

735
    scale *= exp(0.1823216 * zoomMultiplyer); // ln(1.2) = 0.1823216
736

737
    double rightf = currentMousePosition.x / scale - offsetRight;
738
    double upf    = currentMousePosition.y / scale - offsetUp;
739

740
    offset = offset.Plus(projRight.ScaledBy(rightf - righti));
741
    offset = offset.Plus(projUp.ScaledBy(upf - upi));
742

743
    if(SS.TW.shown.screen == TextWindow::Screen::EDIT_VIEW) {
744
        if(havePainted) {
745
            SS.ScheduleShowTW();
746
        }
747
    }
748
    havePainted = false;
749
    Invalidate();
750
}
751

752

753
void GraphicsWindow::MenuView(Command id) {
754
    switch(id) {
755
        case Command::ZOOM_IN:
756
            SS.GW.ZoomToMouse(1);
757
            break;
758

759
        case Command::ZOOM_OUT:
760
            SS.GW.ZoomToMouse(-1);
761
            break;
762

763
        case Command::ZOOM_TO_FIT:
764
            SS.GW.ZoomToFit(/*includingInvisibles=*/false, /*useSelection=*/true);
765
            SS.ScheduleShowTW();
766
            break;
767

768
        case Command::SHOW_GRID:
769
            SS.GW.showSnapGrid = !SS.GW.showSnapGrid;
770
            SS.GW.EnsureValidActives();
771
            SS.GW.Invalidate();
772
            if(SS.GW.showSnapGrid && !SS.GW.LockedInWorkplane()) {
773
                Message(_("No workplane is active, so the grid will not appear."));
774
            }
775
            break;
776

777
        case Command::DIM_SOLID_MODEL:
778
            SS.GW.dimSolidModel = !SS.GW.dimSolidModel;
779
            SS.GW.EnsureValidActives();
780
            SS.GW.Invalidate(/*clearPersistent=*/true);
781
            break;
782

783
        case Command::PERSPECTIVE_PROJ:
784
            SS.usePerspectiveProj = !SS.usePerspectiveProj;
785
            SS.GW.EnsureValidActives();
786
            SS.GW.Invalidate();
787
            if(SS.cameraTangent < 1e-6) {
788
                Error(_("The perspective factor is set to zero, so the view will "
789
                        "always be a parallel projection.\n\n"
790
                        "For a perspective projection, modify the perspective "
791
                        "factor in the configuration screen. A value around 0.3 "
792
                        "is typical."));
793
            }
794
            break;
795

796
        case Command::EXPLODE_SKETCH:
797
            SS.explode = !SS.explode;
798
            SS.GW.EnsureValidActives();
799
            SS.MarkGroupDirty(SS.GW.activeGroup, true);
800
            break;
801

802
        case Command::ONTO_WORKPLANE:
803
            if(SS.GW.LockedInWorkplane()) {
804
                SS.GW.AnimateOntoWorkplane();
805
                break;
806
            }  // if not in 2d mode use ORTHO logic
807
            // fallthrough
808
        case Command::NEAREST_ORTHO:
809
        case Command::NEAREST_ISO: {
810
            static const Vector ortho[3] = {
811
                Vector::From(1, 0, 0),
812
                Vector::From(0, 1, 0),
813
                Vector::From(0, 0, 1)
814
            };
815
            double sqrt2 = sqrt(2.0), sqrt6 = sqrt(6.0);
816
            Quaternion quat0 = Quaternion::From(SS.GW.projRight, SS.GW.projUp);
817
            Quaternion quatf = quat0;
818
            double dmin = 1e10;
819

820
            // There are 24 possible views (3*2*2*2), if all are
821
            // allowed.  If the user is in turn-table mode, the
822
            // isometric view must have the z-axis facing up, leaving
823
            // 8 possible views (2*1*2*2).
824

825
            bool require_turntable = (id==Command::NEAREST_ISO && SS.turntableNav);
826
            for(int i = 0; i < 3; i++) {
827
                for(int j = 0; j < 3; j++) {
828
                    if(i == j) continue;
829
                    if(require_turntable && (j!=2)) continue;
830
                    for(int negi = 0; negi < 2; negi++) {
831
                        for(int negj = 0; negj < 2; negj++) {
832
                            Vector ou = ortho[i], ov = ortho[j];
833
                            if(negi) ou = ou.ScaledBy(-1);
834
                            if(negj) ov = ov.ScaledBy(-1);
835
                            Vector on = ou.Cross(ov);
836

837
                            Vector u, v;
838
                            if(id == Command::NEAREST_ORTHO || id == Command::ONTO_WORKPLANE) {
839
                                u = ou;
840
                                v = ov;
841
                            } else {
842
                                u =
843
                                    ou.ScaledBy(1/sqrt2).Plus(
844
                                    on.ScaledBy(-1/sqrt2));
845
                                v =
846
                                    ou.ScaledBy(-1/sqrt6).Plus(
847
                                    ov.ScaledBy(2/sqrt6).Plus(
848
                                    on.ScaledBy(-1/sqrt6)));
849
                            }
850

851
                            Quaternion quatt = Quaternion::From(u, v);
852
                            double d = min(
853
                                (quatt.Minus(quat0)).Magnitude(),
854
                                (quatt.Plus(quat0)).Magnitude());
855
                            if(d < dmin) {
856
                                dmin = d;
857
                                quatf = quatt;
858
                            }
859
                        }
860
                    }
861
                }
862
            }
863

864
            SS.GW.AnimateOnto(quatf, SS.GW.offset);
865
            break;
866
        }
867

868
        case Command::CENTER_VIEW:
869
            SS.GW.GroupSelection();
870
            if(SS.GW.gs.n == 1 && SS.GW.gs.points == 1) {
871
                Quaternion quat0;
872
                // Offset is the selected point, quaternion is same as before
873
                Vector pt = SK.GetEntity(SS.GW.gs.point[0])->PointGetNum();
874
                quat0 = Quaternion::From(SS.GW.projRight, SS.GW.projUp);
875
                SS.GW.ClearSelection();
876
                SS.GW.AnimateOnto(quat0, pt.ScaledBy(-1));
877
            } else {
878
                Error(_("Select a point; this point will become the center "
879
                        "of the view on screen."));
880
            }
881
            break;
882

883
        case Command::SHOW_TOOLBAR:
884
            SS.showToolbar = !SS.showToolbar;
885
            SS.GW.EnsureValidActives();
886
            SS.GW.Invalidate();
887
            break;
888

889
        case Command::SHOW_TEXT_WND:
890
            SS.GW.showTextWindow = !SS.GW.showTextWindow;
891
            SS.GW.EnsureValidActives();
892
            break;
893

894
        case Command::UNITS_INCHES:
895
            SS.viewUnits = Unit::INCHES;
896
            SS.ScheduleShowTW();
897
            SS.GW.EnsureValidActives();
898
            break;
899

900
        case Command::UNITS_FEET_INCHES:
901
            SS.viewUnits = Unit::FEET_INCHES;
902
            SS.ScheduleShowTW();
903
            SS.GW.EnsureValidActives();
904
            break;
905

906
        case Command::UNITS_MM:
907
            SS.viewUnits = Unit::MM;
908
            SS.ScheduleShowTW();
909
            SS.GW.EnsureValidActives();
910
            break;
911

912
        case Command::UNITS_METERS:
913
            SS.viewUnits = Unit::METERS;
914
            SS.ScheduleShowTW();
915
            SS.GW.EnsureValidActives();
916
            break;
917

918
        case Command::FULL_SCREEN:
919
            SS.GW.window->SetFullScreen(!SS.GW.window->IsFullScreen());
920
            SS.GW.EnsureValidActives();
921
            break;
922

923
        default: ssassert(false, "Unexpected menu ID");
924
    }
925
    SS.GW.Invalidate();
926
}
927

928
void GraphicsWindow::EnsureValidActives() {
929
    bool change = false;
930
    // The active group must exist, and not be the references.
931
    Group *g = SK.group.FindByIdNoOops(activeGroup);
932
    if((!g) || (g->h == Group::HGROUP_REFERENCES)) {
933
        // Not using range-for because this is used to find an index.
934
        int i;
935
        for(i = 0; i < SK.groupOrder.n; i++) {
936
            if(SK.groupOrder[i] != Group::HGROUP_REFERENCES) {
937
                break;
938
            }
939
        }
940
        if(i >= SK.groupOrder.n) {
941
            // This can happen if the user deletes all the groups in the
942
            // sketch. It's difficult to prevent that, because the last
943
            // group might have been deleted automatically, because it failed
944
            // a dependency. There needs to be something, so create a plane
945
            // drawing group and activate that. They should never be able
946
            // to delete the references, though.
947
            activeGroup = SS.CreateDefaultDrawingGroup();
948
            // We've created the default group, but not the workplane entity;
949
            // do it now so that drawing mode isn't switched to "Free in 3d".
950
            SS.GenerateAll(SolveSpaceUI::Generate::ALL);
951
        } else {
952
            activeGroup = SK.groupOrder[i];
953
        }
954
        SK.GetGroup(activeGroup)->Activate();
955
        change = true;
956
    }
957

958
    // The active coordinate system must also exist.
959
    if(LockedInWorkplane()) {
960
        Entity *e = SK.entity.FindByIdNoOops(ActiveWorkplane());
961
        if(e) {
962
            hGroup hgw = e->group;
963
            if(hgw != activeGroup && SS.GroupsInOrder(activeGroup, hgw)) {
964
                // The active workplane is in a group that comes after the
965
                // active group; so any request or constraint will fail.
966
                SetWorkplaneFreeIn3d();
967
                change = true;
968
            }
969
        } else {
970
            SetWorkplaneFreeIn3d();
971
            change = true;
972
        }
973
    }
974

975
    if(!window) return;
976

977
    // And update the checked state for various menus
978
    bool locked = LockedInWorkplane();
979
    in3dMenuItem->SetActive(!locked);
980
    inWorkplaneMenuItem->SetActive(locked);
981

982
    SS.UndoEnableMenus();
983

984
    switch(SS.viewUnits) {
985
        case Unit::MM:
986
        case Unit::METERS:
987
        case Unit::INCHES:
988
        case Unit::FEET_INCHES:
989
            break;
990
        default:
991
            SS.viewUnits = Unit::MM;
992
            break;
993
    }
994
    unitsMmMenuItem->SetActive(SS.viewUnits == Unit::MM);
995
    unitsMetersMenuItem->SetActive(SS.viewUnits == Unit::METERS);
996
    unitsInchesMenuItem->SetActive(SS.viewUnits == Unit::INCHES);
997
    unitsFeetInchesMenuItem->SetActive(SS.viewUnits == Unit::FEET_INCHES);
998

999
    if(SS.TW.window) SS.TW.window->SetVisible(SS.GW.showTextWindow);
1000
    showTextWndMenuItem->SetActive(SS.GW.showTextWindow);
1001

1002
    showGridMenuItem->SetActive(SS.GW.showSnapGrid);
1003
    dimSolidModelMenuItem->SetActive(SS.GW.dimSolidModel);
1004
    perspectiveProjMenuItem->SetActive(SS.usePerspectiveProj);
1005
    explodeMenuItem->SetActive(SS.explode);
1006
    showToolbarMenuItem->SetActive(SS.showToolbar);
1007
    fullScreenMenuItem->SetActive(SS.GW.window->IsFullScreen());
1008

1009
    if(change) SS.ScheduleShowTW();
1010
}
1011

1012
void GraphicsWindow::SetWorkplaneFreeIn3d() {
1013
    SK.GetGroup(activeGroup)->activeWorkplane = Entity::FREE_IN_3D;
1014
}
1015
hEntity GraphicsWindow::ActiveWorkplane() {
1016
    Group *g = SK.group.FindByIdNoOops(activeGroup);
1017
    if(g) {
1018
        return g->activeWorkplane;
1019
    } else {
1020
        return Entity::FREE_IN_3D;
1021
    }
1022
}
1023
bool GraphicsWindow::LockedInWorkplane() {
1024
    return (SS.GW.ActiveWorkplane() != Entity::FREE_IN_3D);
1025
}
1026

1027
void GraphicsWindow::ForceTextWindowShown() {
1028
    if(!showTextWindow) {
1029
        showTextWindow = true;
1030
        showTextWndMenuItem->SetActive(true);
1031
        SS.TW.window->SetVisible(true);
1032
    }
1033
}
1034

1035
void GraphicsWindow::DeleteTaggedRequests() {
1036
    // Delete any requests that were affected by this deletion.
1037
    for(Request &r : SK.request) {
1038
        if(r.workplane == Entity::FREE_IN_3D) continue;
1039
        if(!r.workplane.isFromRequest()) continue;
1040
        Request *wrkpl = SK.GetRequest(r.workplane.request());
1041
        if(wrkpl->tag)
1042
            r.tag = 1;
1043
    }
1044
    // Rewrite any point-coincident constraints that were affected by this
1045
    // deletion.
1046
    for(Request &r : SK.request) {
1047
        if(!r.tag) continue;
1048
        FixConstraintsForRequestBeingDeleted(r.h);
1049
    }
1050
    // and then delete the tagged requests.
1051
    SK.request.RemoveTagged();
1052

1053
    // An edit might be in progress for the just-deleted item. So
1054
    // now it's not.
1055
    window->HideEditor();
1056
    SS.TW.HideEditControl();
1057
    // And clear out the selection, which could contain that item.
1058
    ClearSuper();
1059
    // And regenerate to get rid of what it generates, plus anything
1060
    // that references it (since the regen code checks for that).
1061
    SS.GenerateAll(SolveSpaceUI::Generate::ALL);
1062
    EnsureValidActives();
1063
    SS.ScheduleShowTW();
1064
}
1065

1066
Vector GraphicsWindow::SnapToGrid(Vector p) {
1067
    if(!LockedInWorkplane()) return p;
1068

1069
    EntityBase *wrkpl = SK.GetEntity(ActiveWorkplane()),
1070
               *norm  = wrkpl->Normal();
1071
    Vector wo = SK.GetEntity(wrkpl->point[0])->PointGetNum(),
1072
           wu = norm->NormalU(),
1073
           wv = norm->NormalV(),
1074
           wn = norm->NormalN();
1075

1076
    Vector pp = (p.Minus(wo)).DotInToCsys(wu, wv, wn);
1077
    pp.x = floor((pp.x / SS.gridSpacing) + 0.5)*SS.gridSpacing;
1078
    pp.y = floor((pp.y / SS.gridSpacing) + 0.5)*SS.gridSpacing;
1079
    pp.z = 0;
1080

1081
    return pp.ScaleOutOfCsys(wu, wv, wn).Plus(wo);
1082
}
1083

1084
void GraphicsWindow::MenuEdit(Command id) {
1085
    switch(id) {
1086
        case Command::UNSELECT_ALL:
1087
            SS.GW.GroupSelection();
1088
            // If there's nothing selected to de-select, and no operation
1089
            // to cancel, then perhaps they want to return to the home
1090
            // screen in the text window.
1091
            if(SS.GW.gs.n               == 0 &&
1092
               SS.GW.gs.constraints     == 0 &&
1093
               SS.GW.pending.operation  == Pending::NONE)
1094
            {
1095
                if(!(SS.TW.window->IsEditorVisible() ||
1096
                     SS.GW.window->IsEditorVisible()))
1097
                {
1098
                    if(SS.TW.shown.screen == TextWindow::Screen::STYLE_INFO) {
1099
                        SS.TW.GoToScreen(TextWindow::Screen::LIST_OF_STYLES);
1100
                    } else {
1101
                        SS.TW.ClearSuper();
1102
                    }
1103
                }
1104
            }
1105
            // some pending operations need an Undo to properly clean up on ESC
1106
            if ( (SS.GW.pending.operation == Pending::DRAGGING_NEW_POINT)
1107
              || (SS.GW.pending.operation == Pending::DRAGGING_NEW_LINE_POINT)
1108
              || (SS.GW.pending.operation == Pending::DRAGGING_NEW_ARC_POINT)
1109
              || (SS.GW.pending.operation == Pending::DRAGGING_NEW_CUBIC_POINT)
1110
              || (SS.GW.pending.operation == Pending::DRAGGING_NEW_RADIUS) )
1111
            {
1112
              SS.GW.ClearSuper();
1113
              SS.UndoUndo();
1114
            }
1115
            SS.GW.ClearSuper();
1116
            SS.TW.HideEditControl();
1117
            SS.nakedEdges.Clear();
1118
            SS.justExportedInfo.draw = false;
1119
            SS.centerOfMass.draw = false;
1120
            // This clears the marks drawn to indicate which points are
1121
            // still free to drag.
1122
            for(Param &p : SK.param) {
1123
                p.free = false;
1124
            }
1125
            if(SS.exportMode) {
1126
                SS.exportMode = false;
1127
                SS.GenerateAll(SolveSpaceUI::Generate::ALL);
1128
            }
1129
            SS.GW.persistentDirty = true;
1130
            break;
1131

1132
        case Command::SELECT_ALL: {
1133
            for(Entity &e : SK.entity) {
1134
                if(e.group != SS.GW.activeGroup) continue;
1135
                if(e.IsFace() || e.IsDistance()) continue;
1136
                if(!e.IsVisible()) continue;
1137

1138
                SS.GW.MakeSelected(e.h);
1139
            }
1140
            SS.GW.Invalidate();
1141
            SS.ScheduleShowTW();
1142
            break;
1143
        }
1144

1145
        case Command::SELECT_CHAIN: {
1146
            int newlySelected = 0;
1147
            bool didSomething;
1148
            do {
1149
                didSomething = false;
1150
                for(Entity &e : SK.entity) {
1151
                    if(e.group != SS.GW.activeGroup) continue;
1152
                    if(!e.HasEndpoints()) continue;
1153
                    if(!e.IsVisible()) continue;
1154

1155
                    Vector st = e.EndpointStart(),
1156
                           fi = e.EndpointFinish();
1157

1158
                    bool onChain = false, alreadySelected = false;
1159
                    List<Selection> *ls = &(SS.GW.selection);
1160
                    for(Selection *s = ls->First(); s; s = ls->NextAfter(s)) {
1161
                        if(!s->entity.v) continue;
1162
                        if(s->entity == e.h) {
1163
                            alreadySelected = true;
1164
                            continue;
1165
                        }
1166
                        Entity *se = SK.GetEntity(s->entity);
1167
                        if(!se->HasEndpoints()) continue;
1168

1169
                        Vector sst = se->EndpointStart(),
1170
                               sfi = se->EndpointFinish();
1171

1172
                        if(sst.Equals(st) || sst.Equals(fi) ||
1173
                           sfi.Equals(st) || sfi.Equals(fi))
1174
                        {
1175
                            onChain = true;
1176
                        }
1177
                    }
1178
                    if(onChain && !alreadySelected) {
1179
                        SS.GW.MakeSelected(e.h);
1180
                        newlySelected++;
1181
                        didSomething = true;
1182
                    }
1183
                }
1184
            } while(didSomething);
1185
            SS.GW.Invalidate();
1186
            SS.ScheduleShowTW();
1187
            if(newlySelected == 0) {
1188
                Error(_("No additional entities share endpoints with the selected entities."));
1189
            }
1190
            break;
1191
        }
1192

1193
        case Command::ROTATE_90: {
1194
            SS.GW.GroupSelection();
1195
            Entity *e = NULL;
1196
            if(SS.GW.gs.n == 1 && SS.GW.gs.points == 1) {
1197
                e = SK.GetEntity(SS.GW.gs.point[0]);
1198
            } else if(SS.GW.gs.n == 1 && SS.GW.gs.entities == 1) {
1199
                e = SK.GetEntity(SS.GW.gs.entity[0]);
1200
            }
1201
            SS.GW.ClearSelection();
1202

1203
            hGroup hg = e ? e->group : SS.GW.activeGroup;
1204
            Group *g = SK.GetGroup(hg);
1205
            if(g->type != Group::Type::LINKED) {
1206
                Error(_("To use this command, select a point or other "
1207
                        "entity from an linked part, or make a link "
1208
                        "group the active group."));
1209
                break;
1210
            }
1211

1212
            SS.UndoRemember();
1213
            // Rotate by ninety degrees about the coordinate axis closest
1214
            // to the screen normal.
1215
            Vector norm = SS.GW.projRight.Cross(SS.GW.projUp);
1216
            norm = norm.ClosestOrtho();
1217
            norm = norm.WithMagnitude(1);
1218
            Quaternion qaa = Quaternion::From(norm, PI/2);
1219

1220
            g->TransformImportedBy(Vector::From(0, 0, 0), qaa);
1221

1222
            // and regenerate as necessary.
1223
            SS.MarkGroupDirty(hg);
1224
            break;
1225
        }
1226

1227
        case Command::SNAP_TO_GRID: {
1228
            if(!SS.GW.LockedInWorkplane()) {
1229
                Error(_("No workplane is active. Activate a workplane "
1230
                        "(with Sketch -> In Workplane) to define the plane "
1231
                        "for the snap grid."));
1232
                break;
1233
            }
1234
            SS.GW.GroupSelection();
1235
            if(SS.GW.gs.points == 0 && SS.GW.gs.constraintLabels == 0) {
1236
                Error(_("Can't snap these items to grid; select points, "
1237
                        "text comments, or constraints with a label. "
1238
                        "To snap a line, select its endpoints."));
1239
                break;
1240
            }
1241
            SS.UndoRemember();
1242

1243
            List<Selection> *ls = &(SS.GW.selection);
1244
            for(Selection *s = ls->First(); s; s = ls->NextAfter(s)) {
1245
                if(s->entity.v) {
1246
                    hEntity hp = s->entity;
1247
                    Entity *ep = SK.GetEntity(hp);
1248
                    if(!ep->IsPoint()) continue;
1249

1250
                    Vector p = ep->PointGetNum();
1251
                    ep->PointForceTo(SS.GW.SnapToGrid(p));
1252
                    SS.GW.pending.points.Add(&hp);
1253
                    SS.MarkGroupDirty(ep->group);
1254
                } else if(s->constraint.v) {
1255
                    Constraint *c = SK.GetConstraint(s->constraint);
1256
                    std::vector<Vector> refs;
1257
                    c->GetReferencePoints(SS.GW.GetCamera(), &refs);
1258
                    c->disp.offset = c->disp.offset.Plus(SS.GW.SnapToGrid(refs[0]).Minus(refs[0]));
1259
                }
1260
            }
1261
            // Regenerate, with these points marked as dragged so that they
1262
            // get placed as close as possible to our snap grid.
1263
            SS.GW.ClearSelection();
1264
            break;
1265
        }
1266

1267
        case Command::UNDO:
1268
            SS.UndoUndo();
1269
            break;
1270

1271
        case Command::REDO:
1272
            SS.UndoRedo();
1273
            break;
1274

1275
        case Command::REGEN_ALL:
1276
            SS.images.clear();
1277
            SS.ReloadAllLinked(SS.saveFile);
1278
            SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE);
1279
            SS.ScheduleShowTW();
1280
            break;
1281

1282
        case Command::EDIT_LINE_STYLES:
1283
            SS.TW.GoToScreen(TextWindow::Screen::LIST_OF_STYLES);
1284
            SS.GW.ForceTextWindowShown();
1285
            SS.ScheduleShowTW();
1286
            break;
1287
        case Command::VIEW_PROJECTION:
1288
            SS.TW.GoToScreen(TextWindow::Screen::EDIT_VIEW);
1289
            SS.GW.ForceTextWindowShown();
1290
            SS.ScheduleShowTW();
1291
            break;
1292
        case Command::CONFIGURATION:
1293
            SS.TW.GoToScreen(TextWindow::Screen::CONFIGURATION);
1294
            SS.GW.ForceTextWindowShown();
1295
            SS.ScheduleShowTW();
1296
            break;
1297

1298
        default: ssassert(false, "Unexpected menu ID");
1299
    }
1300
}
1301

1302
void GraphicsWindow::MenuRequest(Command id) {
1303
    const char *s;
1304
    switch(id) {
1305
        case Command::SEL_WORKPLANE: {
1306
            SS.GW.GroupSelection();
1307
            Group *g = SK.GetGroup(SS.GW.activeGroup);
1308

1309
            if(SS.GW.gs.n == 1 && SS.GW.gs.workplanes == 1) {
1310
                // A user-selected workplane
1311
                g->activeWorkplane = SS.GW.gs.entity[0];
1312
                SS.GW.EnsureValidActives();
1313
                SS.ScheduleShowTW();
1314
            } else if(g->type == Group::Type::DRAWING_WORKPLANE) {
1315
                // The group's default workplane
1316
                g->activeWorkplane = g->h.entity(0);
1317
                MessageAndRun([] {
1318
                    // Align the view with the selected workplane
1319
                    SS.GW.ClearSuper();
1320
                    SS.GW.AnimateOntoWorkplane();
1321
                }, _("No workplane selected. Activating default workplane "
1322
                     "for this group."));
1323
            } else {
1324
                Error(_("No workplane is selected, and the active group does "
1325
                        "not have a default workplane. Try selecting a "
1326
                        "workplane, or activating a sketch-in-new-workplane "
1327
                        "group."));
1328
                //update checkboxes in the menus
1329
                SS.GW.EnsureValidActives();
1330
            }
1331
            break;
1332
        }
1333
        case Command::FREE_IN_3D:
1334
            SS.GW.SetWorkplaneFreeIn3d();
1335
            SS.GW.EnsureValidActives();
1336
            SS.ScheduleShowTW();
1337
            SS.GW.Invalidate();
1338
            break;
1339

1340
        case Command::TANGENT_ARC:
1341
            SS.GW.GroupSelection();
1342
            if(SS.GW.gs.n == 1 && SS.GW.gs.points == 1) {
1343
                SS.GW.MakeTangentArc();
1344
            } else if(SS.GW.gs.n != 0) {
1345
                Error(_("Bad selection for tangent arc at point. Select a "
1346
                        "single point, or select nothing to set up arc "
1347
                        "parameters."));
1348
            } else {
1349
                SS.TW.GoToScreen(TextWindow::Screen::TANGENT_ARC);
1350
                SS.GW.ForceTextWindowShown();
1351
                SS.ScheduleShowTW();
1352
                SS.GW.Invalidate(); // repaint toolbar
1353
            }
1354
            break;
1355

1356
        case Command::ARC: s = _("click point on arc (draws anti-clockwise)"); goto c;
1357
        case Command::DATUM_POINT: s = _("click to place datum point"); goto c;
1358
        case Command::LINE_SEGMENT: s = _("click first point of line segment"); goto c;
1359
        case Command::CONSTR_SEGMENT:
1360
            s = _("click first point of construction line segment"); goto c;
1361
        case Command::CUBIC: s = _("click first point of cubic segment"); goto c;
1362
        case Command::CIRCLE: s = _("click center of circle"); goto c;
1363
        case Command::WORKPLANE: s = _("click origin of workplane"); goto c;
1364
        case Command::RECTANGLE: s = _("click one corner of rectangle"); goto c;
1365
        case Command::TTF_TEXT: s = _("click top left of text"); goto c;
1366
        case Command::IMAGE:
1367
            if(!SS.ReloadLinkedImage(SS.saveFile, &SS.GW.pending.filename,
1368
                                     /*canCancel=*/true)) {
1369
                return;
1370
            }
1371
            s = _("click top left of image"); goto c;
1372
c:
1373
            SS.GW.pending.operation = GraphicsWindow::Pending::COMMAND;
1374
            SS.GW.pending.command = id;
1375
            SS.GW.pending.description = s;
1376
            SS.ScheduleShowTW();
1377
            SS.GW.Invalidate(); // repaint toolbar
1378
            break;
1379

1380
        case Command::CONSTRUCTION: {
1381
            // if we are drawing
1382
            if(SS.GW.pending.operation == Pending::DRAGGING_NEW_POINT ||
1383
               SS.GW.pending.operation == Pending::DRAGGING_NEW_LINE_POINT ||
1384
               SS.GW.pending.operation == Pending::DRAGGING_NEW_ARC_POINT ||
1385
               SS.GW.pending.operation == Pending::DRAGGING_NEW_CUBIC_POINT ||
1386
               SS.GW.pending.operation == Pending::DRAGGING_NEW_RADIUS) {
1387
                for(auto &hr : SS.GW.pending.requests) {
1388
                    Request* r = SK.GetRequest(hr);
1389
                    r->construction = !(r->construction);
1390
                    SS.MarkGroupDirty(r->group);
1391
                }
1392
                SS.GW.Invalidate();
1393
                break;
1394
            }
1395
            SS.GW.GroupSelection();
1396
            if(SS.GW.gs.entities == 0) {
1397
                Error(_("No entities are selected. Select entities before "
1398
                        "trying to toggle their construction state."));
1399
                break;
1400
            }
1401
            SS.UndoRemember();
1402
            int i;
1403
            for(i = 0; i < SS.GW.gs.entities; i++) {
1404
                hEntity he = SS.GW.gs.entity[i];
1405
                if(!he.isFromRequest()) continue;
1406
                Request *r = SK.GetRequest(he.request());
1407
                r->construction = !(r->construction);
1408
                SS.MarkGroupDirty(r->group);
1409
            }
1410
            SS.GW.ClearSelection();
1411
            break;
1412
        }
1413

1414
        case Command::SPLIT_CURVES:
1415
            SS.GW.SplitLinesOrCurves();
1416
            break;
1417

1418
        default: ssassert(false, "Unexpected menu ID");
1419
    }
1420
}
1421

1422
void GraphicsWindow::ClearSuper() {
1423
    if(window) window->HideEditor();
1424
    ClearPending();
1425
    ClearSelection();
1426
    hover.Clear();
1427
    EnsureValidActives();
1428
}
1429

1430
void GraphicsWindow::ToggleBool(bool *v) {
1431
    *v = !*v;
1432

1433
    // The faces are shown as special stippling on the shaded triangle mesh,
1434
    // so not meaningful to show them and hide the shaded.
1435
    if(!showShaded) showFaces = false;
1436

1437
    // If the mesh or edges were previously hidden, they haven't been generated,
1438
    // and if we are going to show them, we need to generate them first.
1439
    Group *g = SK.GetGroup(SS.GW.activeGroup);
1440
    if(*v && (g->displayOutlines.l.IsEmpty() && (v == &showEdges || v == &showOutlines))) {
1441
        SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE);
1442
    }
1443

1444
    if(v == &showFaces) {
1445
        if(g->type == Group::Type::DRAWING_WORKPLANE || g->type == Group::Type::DRAWING_3D) {
1446
            showFacesDrawing = showFaces;
1447
        } else {
1448
            showFacesNonDrawing = showFaces;
1449
        }
1450
    }
1451

1452
    Invalidate(/*clearPersistent=*/true);
1453
    SS.ScheduleShowTW();
1454
}
1455

1456
bool GraphicsWindow::SuggestLineConstraint(hRequest request, Constraint::Type *type) {
1457
    if(!(LockedInWorkplane() && SS.automaticLineConstraints))
1458
        return false;
1459

1460
    Entity *ptA = SK.GetEntity(request.entity(1)),
1461
           *ptB = SK.GetEntity(request.entity(2));
1462

1463
    Expr *au, *av, *bu, *bv;
1464

1465
    ptA->PointGetExprsInWorkplane(ActiveWorkplane(), &au, &av);
1466
    ptB->PointGetExprsInWorkplane(ActiveWorkplane(), &bu, &bv);
1467

1468
    double du = au->Minus(bu)->Eval();
1469
    double dv = av->Minus(bv)->Eval();
1470

1471
    const double TOLERANCE_RATIO = 0.02;
1472
    if(fabs(dv) > LENGTH_EPS && fabs(du / dv) < TOLERANCE_RATIO) {
1473
        *type = Constraint::Type::VERTICAL;
1474
        return true;
1475
    } else if(fabs(du) > LENGTH_EPS && fabs(dv / du) < TOLERANCE_RATIO) {
1476
        *type = Constraint::Type::HORIZONTAL;
1477
        return true;
1478
    }
1479
    return false;
1480
}
1481

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

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

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

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