Solvespace
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
9typedef void MenuHandler(Command id);
10using MenuKind = Platform::MenuItem::Indicator;
11struct MenuEntry {
12int level; // 0 == on menu bar, 1 == one level down
13const char *label; // or NULL for a separator
14Command cmd; // command ID
15int accel; // keyboard accelerator
16MenuKind kind;
17MenuHandler *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
39const 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
199void GraphicsWindow::ActivateCommand(Command cmd) {
200for(int i = 0; Menu[i].level >= 0; i++) {
201if(cmd == Menu[i].cmd) {
202(Menu[i].fn)((Command)Menu[i].cmd);
203break;
204}
205}
206}
207
208Platform::KeyboardEvent GraphicsWindow::AcceleratorForCommand(Command cmd) {
209int rawAccel = 0;
210for(int i = 0; Menu[i].level >= 0; i++) {
211if(cmd == Menu[i].cmd) {
212rawAccel = Menu[i].accel;
213break;
214}
215}
216
217Platform::KeyboardEvent accel = {};
218accel.type = Platform::KeyboardEvent::Type::PRESS;
219if(rawAccel & SHIFT_MASK) {
220accel.shiftDown = true;
221}
222if(rawAccel & CTRL_MASK) {
223accel.controlDown = true;
224}
225if(rawAccel & FN_MASK) {
226accel.key = Platform::KeyboardEvent::Key::FUNCTION;
227accel.num = rawAccel & 0xff;
228} else {
229accel.key = Platform::KeyboardEvent::Key::CHARACTER;
230accel.chr = (char)(rawAccel & 0xff);
231}
232
233return accel;
234}
235
236bool GraphicsWindow::KeyboardEvent(Platform::KeyboardEvent event) {
237using Platform::KeyboardEvent;
238
239if(event.type == KeyboardEvent::Type::RELEASE)
240return true;
241
242if(event.key == KeyboardEvent::Key::CHARACTER) {
243if(event.chr == '\b') {
244// Treat backspace identically to escape.
245MenuEdit(Command::UNSELECT_ALL);
246return 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...
252MenuView(Command::ZOOM_IN);
253return true;
254}
255}
256
257// On some platforms, the OS does not handle some or all keyboard accelerators,
258// so handle them here.
259for(int i = 0; Menu[i].level >= 0; i++) {
260if(AcceleratorForCommand(Menu[i].cmd).Equals(event)) {
261ActivateCommand(Menu[i].cmd);
262return true;
263}
264}
265
266return false;
267}
268
269void GraphicsWindow::PopulateMainMenu() {
270bool unique = false;
271Platform::MenuBarRef mainMenu = Platform::GetOrCreateMainMenu(&unique);
272if(unique) mainMenu->Clear();
273
274Platform::MenuRef currentSubMenu;
275std::vector<Platform::MenuRef> subMenuStack;
276for(int i = 0; Menu[i].level >= 0; i++) {
277while(Menu[i].level > 0 && Menu[i].level <= (int)subMenuStack.size()) {
278currentSubMenu = subMenuStack.back();
279subMenuStack.pop_back();
280}
281
282if(Menu[i].label == NULL) {
283currentSubMenu->AddSeparator();
284continue;
285}
286
287std::string label = Translate(Menu[i].label);
288if(Menu[i].level == 0) {
289currentSubMenu = mainMenu->AddSubMenu(label);
290} else if(Menu[i].cmd == Command::OPEN_RECENT) {
291openRecentMenu = currentSubMenu->AddSubMenu(label);
292} else if(Menu[i].cmd == Command::GROUP_RECENT) {
293linkRecentMenu = currentSubMenu->AddSubMenu(label);
294} else if(Menu[i].cmd == Command::LOCALE) {
295Platform::MenuRef localeMenu = currentSubMenu->AddSubMenu(label);
296for(const Locale &locale : Locales()) {
297localeMenu->AddItem(locale.displayName, [&]() {
298SetLocale(locale.Culture());
299Platform::GetSettings()->FreezeString("Locale", locale.Culture());
300
301SS.UpdateWindowTitles();
302PopulateMainMenu();
303SS.GW.EnsureValidActives();
304});
305}
306} else if(Menu[i].fn == NULL) {
307subMenuStack.push_back(currentSubMenu);
308currentSubMenu = currentSubMenu->AddSubMenu(label);
309} else {
310Platform::MenuItemRef menuItem = currentSubMenu->AddItem(label);
311menuItem->SetIndicator(Menu[i].kind);
312if(Menu[i].accel != 0) {
313menuItem->SetAccelerator(AcceleratorForCommand(Menu[i].cmd));
314}
315menuItem->onTrigger = std::bind(Menu[i].fn, Menu[i].cmd);
316
317if(Menu[i].cmd == Command::SHOW_GRID) {
318showGridMenuItem = menuItem;
319} else if(Menu[i].cmd == Command::DIM_SOLID_MODEL) {
320dimSolidModelMenuItem = menuItem;
321} else if(Menu[i].cmd == Command::PERSPECTIVE_PROJ) {
322perspectiveProjMenuItem = menuItem;
323} else if(Menu[i].cmd == Command::EXPLODE_SKETCH) {
324explodeMenuItem = menuItem;
325} else if(Menu[i].cmd == Command::SHOW_TOOLBAR) {
326showToolbarMenuItem = menuItem;
327} else if(Menu[i].cmd == Command::SHOW_TEXT_WND) {
328showTextWndMenuItem = menuItem;
329} else if(Menu[i].cmd == Command::FULL_SCREEN) {
330fullScreenMenuItem = menuItem;
331} else if(Menu[i].cmd == Command::UNITS_MM) {
332unitsMmMenuItem = menuItem;
333} else if(Menu[i].cmd == Command::UNITS_METERS) {
334unitsMetersMenuItem = menuItem;
335} else if(Menu[i].cmd == Command::UNITS_INCHES) {
336unitsInchesMenuItem = menuItem;
337} else if(Menu[i].cmd == Command::UNITS_FEET_INCHES) {
338unitsFeetInchesMenuItem = menuItem;
339} else if(Menu[i].cmd == Command::SEL_WORKPLANE) {
340inWorkplaneMenuItem = menuItem;
341} else if(Menu[i].cmd == Command::FREE_IN_3D) {
342in3dMenuItem = menuItem;
343} else if(Menu[i].cmd == Command::UNDO) {
344undoMenuItem = menuItem;
345} else if(Menu[i].cmd == Command::REDO) {
346redoMenuItem = menuItem;
347}
348}
349}
350
351PopulateRecentFiles();
352SS.UndoEnableMenus();
353
354window->SetMenuBar(mainMenu);
355}
356
357static void PopulateMenuWithPathnames(Platform::MenuRef menu,
358std::vector<Platform::Path> pathnames,
359std::function<void(const Platform::Path &)> onTrigger) {
360menu->Clear();
361if(pathnames.empty()) {
362Platform::MenuItemRef menuItem = menu->AddItem(_("(no recent files)"));
363menuItem->SetEnabled(false);
364} else {
365for(Platform::Path pathname : pathnames) {
366Platform::MenuItemRef menuItem = menu->AddItem(pathname.raw, [=]() {
367if(FileExists(pathname)) {
368onTrigger(pathname);
369} else {
370Error(_("File '%s' does not exist."), pathname.raw.c_str());
371}
372}, /*mnemonics=*/false);
373}
374}
375}
376
377void GraphicsWindow::PopulateRecentFiles() {
378PopulateMenuWithPathnames(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.
381Platform::Path pathCopy(path);
382if(!SS.OkayToStartNewFile()) return;
383SS.Load(pathCopy);
384});
385
386PopulateMenuWithPathnames(linkRecentMenu, SS.recentFiles, [](const Platform::Path &path) {
387Group::MenuGroup(Command::GROUP_LINK, path);
388});
389}
390
391void GraphicsWindow::Init() {
392scale = 5;
393offset = Vector::From(0, 0, 0);
394projRight = Vector::From(1, 0, 0);
395projUp = 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.
399orig.projRight = projRight;
400orig.projUp = projUp;
401
402// And with the last group active
403ssassert(!SK.groupOrder.IsEmpty(),
404"Group order can't be empty since we will activate the last group.");
405activeGroup = *SK.groupOrder.Last();
406SK.GetGroup(activeGroup)->Activate();
407
408showWorkplanes = false;
409showNormals = true;
410showPoints = true;
411showConstruction = true;
412showConstraints = ShowConstraintMode::SCM_SHOW_ALL;
413showShaded = true;
414showEdges = true;
415showMesh = false;
416showOutlines = false;
417showFacesDrawing = false;
418showFacesNonDrawing = true;
419drawOccludedAs = DrawOccludedAs::INVISIBLE;
420
421showTextWindow = true;
422
423showSnapGrid = false;
424dimSolidModel = true;
425context.active = false;
426toolbarHovered = Command::NONE;
427
428if(!window) {
429window = Platform::CreateWindow();
430if(window) {
431using 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.
434window->SetMinContentSize(720, /*ToolbarDrawOrHitTest 636*/ 32 * 18 + 3 * 16 + 8 + 4);
435window->onClose = std::bind(&SolveSpaceUI::MenuFile, Command::EXIT);
436window->onContextLost = [&] {
437canvas = NULL;
438persistentCanvas = NULL;
439persistentDirty = true;
440};
441window->onRender = std::bind(&GraphicsWindow::Paint, this);
442window->onKeyboardEvent = std::bind(&GraphicsWindow::KeyboardEvent, this, _1);
443window->onMouseEvent = std::bind(&GraphicsWindow::MouseEvent, this, _1);
444window->onSixDofEvent = std::bind(&GraphicsWindow::SixDofEvent, this, _1);
445window->onEditingDone = std::bind(&GraphicsWindow::EditControlDone, this, _1);
446PopulateMainMenu();
447}
448}
449
450if(window) {
451canvas = CreateRenderer();
452if(canvas) {
453persistentCanvas = canvas->CreateBatch();
454persistentDirty = true;
455}
456}
457
458// Do this last, so that all the menus get updated correctly.
459ClearSuper();
460}
461
462void GraphicsWindow::AnimateOntoWorkplane() {
463if(!LockedInWorkplane()) return;
464
465Entity *w = SK.GetEntity(ActiveWorkplane());
466Quaternion quatf = w->Normal()->NormalGetNum();
467
468// Get Z pointing vertical, if we're on turntable nav mode:
469if(SS.turntableNav) {
470Vector normalRight = quatf.RotationU();
471Vector normalUp = quatf.RotationV();
472Vector normal = normalRight.Cross(normalUp);
473if(normalRight.z != 0) {
474double theta = atan2(normalUp.z, normalRight.z);
475theta -= atan2(1, 0);
476normalRight = normalRight.RotatedAbout(normal, theta);
477normalUp = normalUp.RotatedAbout(normal, theta);
478quatf = Quaternion::From(normalRight, normalUp);
479}
480}
481
482Vector offsetf = (SK.GetEntity(w->point[0])->PointGetNum()).ScaledBy(-1);
483
484// If the view screen is open, then we need to refresh it.
485SS.ScheduleShowTW();
486
487AnimateOnto(quatf, offsetf);
488}
489
490void GraphicsWindow::AnimateOnto(Quaternion quatf, Vector offsetf) {
491// Get our initial orientation and translation.
492Quaternion quat0 = Quaternion::From(projRight, projUp);
493Vector offset0 = offset;
494
495// Make sure we take the shorter of the two possible paths.
496double mp = (quatf.Minus(quat0)).Magnitude();
497double mm = (quatf.Plus(quat0)).Magnitude();
498if(mp > mm) {
499quatf = quatf.ScaledBy(-1);
500mp = mm;
501}
502double mo = (offset0.Minus(offsetf)).Magnitude()*scale;
503
504// Animate transition, unless it's a tiny move.
505int64_t t0 = GetMilliseconds();
506int32_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.
511if(dt > 800) dt = 800;
512Quaternion dq = quatf.Times(quat0.Inverse());
513
514if(!animateTimer) {
515animateTimer = Platform::CreateTimer();
516}
517animateTimer->onTimeout = [=] {
518int64_t tn = GetMilliseconds();
519if((tn - t0) < dt) {
520animateTimer->RunAfterNextFrame();
521
522double s = (tn - t0)/((double)dt);
523offset = (offset0.ScaledBy(1 - s)).Plus(offsetf.ScaledBy(s));
524Quaternion quat = (dq.ToThe(s)).Times(quat0).WithMagnitude(1);
525
526projRight = quat.RotationU();
527projUp = quat.RotationV();
528} else {
529projRight = quatf.RotationU();
530projUp = quatf.RotationV();
531offset = offsetf;
532}
533window->Invalidate();
534};
535animateTimer->RunAfterNextFrame();
536}
537
538void GraphicsWindow::HandlePointForZoomToFit(Vector p, Point2d *pmax, Point2d *pmin,
539double *wmin, bool usePerspective,
540const Camera &camera)
541{
542double w;
543Vector 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.
547if(usePerspective) {
548pp = pp.ScaledBy(1.0/w);
549}
550
551pmax->x = max(pmax->x, pp.x);
552pmax->y = max(pmax->y, pp.y);
553pmin->x = min(pmin->x, pp.x);
554pmin->y = min(pmin->y, pp.y);
555*wmin = min(*wmin, w);
556}
557void GraphicsWindow::LoopOverPoints(const std::vector<Entity *> &entities,
558const std::vector<Constraint *> &constraints,
559const std::vector<hEntity> &faces,
560Point2d *pmax, Point2d *pmin, double *wmin,
561bool usePerspective, bool includeMesh,
562const Camera &camera) {
563
564for(Entity *e : entities) {
565if(e->IsPoint()) {
566HandlePointForZoomToFit(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.
572double r = e->CircleGetRadiusNum();
573Vector c = SK.GetEntity(e->point[0])->PointGetNum();
574Quaternion q = SK.GetEntity(e->normal)->NormalGetNum();
575for(int j = 0; j < 4; j++) {
576Vector 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)));
580HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera);
581}
582} else {
583// We have to iterate children points, because we can select entities without points
584for(int i = 0; i < MAX_POINTS_IN_ENTITY; i++) {
585if(e->point[i].v == 0) break;
586Vector p = SK.GetEntity(e->point[i])->PointGetNum();
587HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera);
588}
589}
590}
591
592for(Constraint *c : constraints) {
593std::vector<Vector> refs;
594c->GetReferencePoints(camera, &refs);
595for(Vector p : refs) {
596HandlePointForZoomToFit(p, pmax, pmin, wmin, usePerspective, camera);
597}
598}
599
600if(!includeMesh && faces.empty()) return;
601
602Group *g = SK.GetGroup(activeGroup);
603g->GenerateDisplayItems();
604for(int i = 0; i < g->displayMesh.l.n; i++) {
605STriangle *tr = &(g->displayMesh.l[i]);
606if(!includeMesh) {
607bool found = false;
608for(const hEntity &face : faces) {
609if(face.v != tr->meta.face) continue;
610found = true;
611break;
612}
613if(!found) continue;
614}
615HandlePointForZoomToFit(tr->a, pmax, pmin, wmin, usePerspective, camera);
616HandlePointForZoomToFit(tr->b, pmax, pmin, wmin, usePerspective, camera);
617HandlePointForZoomToFit(tr->c, pmax, pmin, wmin, usePerspective, camera);
618}
619if(!includeMesh) return;
620for(int i = 0; i < g->polyLoops.l.n; i++) {
621SContour *sc = &(g->polyLoops.l[i]);
622for(int j = 0; j < sc->l.n; j++) {
623HandlePointForZoomToFit(sc->l[j].p, pmax, pmin, wmin, usePerspective, camera);
624}
625}
626}
627void GraphicsWindow::ZoomToFit(bool includingInvisibles, bool useSelection) {
628if(!window) return;
629
630scale = ZoomToFit(GetCamera(), includingInvisibles, useSelection);
631}
632double GraphicsWindow::ZoomToFit(const Camera &camera,
633bool includingInvisibles, bool useSelection) {
634std::vector<Entity *> entities;
635std::vector<Constraint *> constraints;
636std::vector<hEntity> faces;
637
638if(useSelection) {
639for(int i = 0; i < selection.n; i++) {
640Selection *s = &selection[i];
641if(s->entity.v != 0) {
642Entity *e = SK.entity.FindById(s->entity);
643if(e->IsFace()) {
644faces.push_back(e->h);
645continue;
646}
647entities.push_back(e);
648}
649if(s->constraint.v != 0) {
650Constraint *c = SK.constraint.FindById(s->constraint);
651constraints.push_back(c);
652}
653}
654}
655
656bool selectionUsed = !entities.empty() || !constraints.empty() || !faces.empty();
657
658if(!selectionUsed) {
659for(Entity &e : SK.entity) {
660// we don't want to handle separate points, because we will iterate them inside entities.
661if(e.IsPoint()) continue;
662if(!includingInvisibles && !e.IsVisible()) continue;
663entities.push_back(&e);
664}
665
666for(Constraint &c : SK.constraint) {
667if(!c.IsVisible()) continue;
668constraints.push_back(&c);
669}
670}
671
672// On the first run, ignore perspective.
673Point2d pmax = { -1e12, -1e12 }, pmin = { 1e12, 1e12 };
674double wmin = 1;
675LoopOverPoints(entities, constraints, faces, &pmax, &pmin, &wmin,
676/*usePerspective=*/false, /*includeMesh=*/!selectionUsed,
677camera);
678
679double xm = (pmax.x + pmin.x)/2, ym = (pmax.y + pmin.y)/2;
680double dx = pmax.x - pmin.x, dy = pmax.y - pmin.y;
681
682offset = offset.Plus(projRight.ScaledBy(-xm)).Plus(
683projUp. ScaledBy(-ym));
684
685// And based on this, we calculate the scale and offset
686double scale;
687if(EXACT(dx == 0 && dy == 0)) {
688scale = 5;
689} else {
690double scalex = 1e12, scaley = 1e12;
691if(EXACT(dx != 0)) scalex = 0.9*camera.width /dx;
692if(EXACT(dy != 0)) scaley = 0.9*camera.height/dy;
693scale = min(scalex, scaley);
694
695scale = min(300.0, scale);
696scale = max(0.003, scale);
697}
698
699// Then do another run, considering the perspective.
700pmax.x = -1e12; pmax.y = -1e12;
701pmin.x = 1e12; pmin.y = 1e12;
702wmin = 1;
703LoopOverPoints(entities, constraints, faces, &pmax, &pmin, &wmin,
704/*usePerspective=*/true, /*includeMesh=*/!selectionUsed,
705camera);
706
707// Adjust the scale so that no points are behind the camera
708if(wmin < 0.1) {
709double k = camera.tangent;
710// w = 1+k*scale*z
711double zmin = (wmin - 1)/(k*scale);
712// 0.1 = 1 + k*scale*zmin
713// (0.1 - 1)/(k*zmin) = scale
714scale = min(scale, (0.1 - 1)/(k*zmin));
715}
716
717return scale;
718}
719
720
721void GraphicsWindow::ZoomToMouse(double zoomMultiplyer) {
722double offsetRight = offset.Dot(projRight);
723double offsetUp = offset.Dot(projUp);
724
725double width, height;
726window->GetContentSize(&width, &height);
727
728double righti = currentMousePosition.x / scale - offsetRight;
729double 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
735scale *= exp(0.1823216 * zoomMultiplyer); // ln(1.2) = 0.1823216
736
737double rightf = currentMousePosition.x / scale - offsetRight;
738double upf = currentMousePosition.y / scale - offsetUp;
739
740offset = offset.Plus(projRight.ScaledBy(rightf - righti));
741offset = offset.Plus(projUp.ScaledBy(upf - upi));
742
743if(SS.TW.shown.screen == TextWindow::Screen::EDIT_VIEW) {
744if(havePainted) {
745SS.ScheduleShowTW();
746}
747}
748havePainted = false;
749Invalidate();
750}
751
752
753void GraphicsWindow::MenuView(Command id) {
754switch(id) {
755case Command::ZOOM_IN:
756SS.GW.ZoomToMouse(1);
757break;
758
759case Command::ZOOM_OUT:
760SS.GW.ZoomToMouse(-1);
761break;
762
763case Command::ZOOM_TO_FIT:
764SS.GW.ZoomToFit(/*includingInvisibles=*/false, /*useSelection=*/true);
765SS.ScheduleShowTW();
766break;
767
768case Command::SHOW_GRID:
769SS.GW.showSnapGrid = !SS.GW.showSnapGrid;
770SS.GW.EnsureValidActives();
771SS.GW.Invalidate();
772if(SS.GW.showSnapGrid && !SS.GW.LockedInWorkplane()) {
773Message(_("No workplane is active, so the grid will not appear."));
774}
775break;
776
777case Command::DIM_SOLID_MODEL:
778SS.GW.dimSolidModel = !SS.GW.dimSolidModel;
779SS.GW.EnsureValidActives();
780SS.GW.Invalidate(/*clearPersistent=*/true);
781break;
782
783case Command::PERSPECTIVE_PROJ:
784SS.usePerspectiveProj = !SS.usePerspectiveProj;
785SS.GW.EnsureValidActives();
786SS.GW.Invalidate();
787if(SS.cameraTangent < 1e-6) {
788Error(_("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}
794break;
795
796case Command::EXPLODE_SKETCH:
797SS.explode = !SS.explode;
798SS.GW.EnsureValidActives();
799SS.MarkGroupDirty(SS.GW.activeGroup, true);
800break;
801
802case Command::ONTO_WORKPLANE:
803if(SS.GW.LockedInWorkplane()) {
804SS.GW.AnimateOntoWorkplane();
805break;
806} // if not in 2d mode use ORTHO logic
807// fallthrough
808case Command::NEAREST_ORTHO:
809case Command::NEAREST_ISO: {
810static const Vector ortho[3] = {
811Vector::From(1, 0, 0),
812Vector::From(0, 1, 0),
813Vector::From(0, 0, 1)
814};
815double sqrt2 = sqrt(2.0), sqrt6 = sqrt(6.0);
816Quaternion quat0 = Quaternion::From(SS.GW.projRight, SS.GW.projUp);
817Quaternion quatf = quat0;
818double 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
825bool require_turntable = (id==Command::NEAREST_ISO && SS.turntableNav);
826for(int i = 0; i < 3; i++) {
827for(int j = 0; j < 3; j++) {
828if(i == j) continue;
829if(require_turntable && (j!=2)) continue;
830for(int negi = 0; negi < 2; negi++) {
831for(int negj = 0; negj < 2; negj++) {
832Vector ou = ortho[i], ov = ortho[j];
833if(negi) ou = ou.ScaledBy(-1);
834if(negj) ov = ov.ScaledBy(-1);
835Vector on = ou.Cross(ov);
836
837Vector u, v;
838if(id == Command::NEAREST_ORTHO || id == Command::ONTO_WORKPLANE) {
839u = ou;
840v = ov;
841} else {
842u =
843ou.ScaledBy(1/sqrt2).Plus(
844on.ScaledBy(-1/sqrt2));
845v =
846ou.ScaledBy(-1/sqrt6).Plus(
847ov.ScaledBy(2/sqrt6).Plus(
848on.ScaledBy(-1/sqrt6)));
849}
850
851Quaternion quatt = Quaternion::From(u, v);
852double d = min(
853(quatt.Minus(quat0)).Magnitude(),
854(quatt.Plus(quat0)).Magnitude());
855if(d < dmin) {
856dmin = d;
857quatf = quatt;
858}
859}
860}
861}
862}
863
864SS.GW.AnimateOnto(quatf, SS.GW.offset);
865break;
866}
867
868case Command::CENTER_VIEW:
869SS.GW.GroupSelection();
870if(SS.GW.gs.n == 1 && SS.GW.gs.points == 1) {
871Quaternion quat0;
872// Offset is the selected point, quaternion is same as before
873Vector pt = SK.GetEntity(SS.GW.gs.point[0])->PointGetNum();
874quat0 = Quaternion::From(SS.GW.projRight, SS.GW.projUp);
875SS.GW.ClearSelection();
876SS.GW.AnimateOnto(quat0, pt.ScaledBy(-1));
877} else {
878Error(_("Select a point; this point will become the center "
879"of the view on screen."));
880}
881break;
882
883case Command::SHOW_TOOLBAR:
884SS.showToolbar = !SS.showToolbar;
885SS.GW.EnsureValidActives();
886SS.GW.Invalidate();
887break;
888
889case Command::SHOW_TEXT_WND:
890SS.GW.showTextWindow = !SS.GW.showTextWindow;
891SS.GW.EnsureValidActives();
892break;
893
894case Command::UNITS_INCHES:
895SS.viewUnits = Unit::INCHES;
896SS.ScheduleShowTW();
897SS.GW.EnsureValidActives();
898break;
899
900case Command::UNITS_FEET_INCHES:
901SS.viewUnits = Unit::FEET_INCHES;
902SS.ScheduleShowTW();
903SS.GW.EnsureValidActives();
904break;
905
906case Command::UNITS_MM:
907SS.viewUnits = Unit::MM;
908SS.ScheduleShowTW();
909SS.GW.EnsureValidActives();
910break;
911
912case Command::UNITS_METERS:
913SS.viewUnits = Unit::METERS;
914SS.ScheduleShowTW();
915SS.GW.EnsureValidActives();
916break;
917
918case Command::FULL_SCREEN:
919SS.GW.window->SetFullScreen(!SS.GW.window->IsFullScreen());
920SS.GW.EnsureValidActives();
921break;
922
923default: ssassert(false, "Unexpected menu ID");
924}
925SS.GW.Invalidate();
926}
927
928void GraphicsWindow::EnsureValidActives() {
929bool change = false;
930// The active group must exist, and not be the references.
931Group *g = SK.group.FindByIdNoOops(activeGroup);
932if((!g) || (g->h == Group::HGROUP_REFERENCES)) {
933// Not using range-for because this is used to find an index.
934int i;
935for(i = 0; i < SK.groupOrder.n; i++) {
936if(SK.groupOrder[i] != Group::HGROUP_REFERENCES) {
937break;
938}
939}
940if(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.
947activeGroup = 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".
950SS.GenerateAll(SolveSpaceUI::Generate::ALL);
951} else {
952activeGroup = SK.groupOrder[i];
953}
954SK.GetGroup(activeGroup)->Activate();
955change = true;
956}
957
958// The active coordinate system must also exist.
959if(LockedInWorkplane()) {
960Entity *e = SK.entity.FindByIdNoOops(ActiveWorkplane());
961if(e) {
962hGroup hgw = e->group;
963if(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.
966SetWorkplaneFreeIn3d();
967change = true;
968}
969} else {
970SetWorkplaneFreeIn3d();
971change = true;
972}
973}
974
975if(!window) return;
976
977// And update the checked state for various menus
978bool locked = LockedInWorkplane();
979in3dMenuItem->SetActive(!locked);
980inWorkplaneMenuItem->SetActive(locked);
981
982SS.UndoEnableMenus();
983
984switch(SS.viewUnits) {
985case Unit::MM:
986case Unit::METERS:
987case Unit::INCHES:
988case Unit::FEET_INCHES:
989break;
990default:
991SS.viewUnits = Unit::MM;
992break;
993}
994unitsMmMenuItem->SetActive(SS.viewUnits == Unit::MM);
995unitsMetersMenuItem->SetActive(SS.viewUnits == Unit::METERS);
996unitsInchesMenuItem->SetActive(SS.viewUnits == Unit::INCHES);
997unitsFeetInchesMenuItem->SetActive(SS.viewUnits == Unit::FEET_INCHES);
998
999if(SS.TW.window) SS.TW.window->SetVisible(SS.GW.showTextWindow);
1000showTextWndMenuItem->SetActive(SS.GW.showTextWindow);
1001
1002showGridMenuItem->SetActive(SS.GW.showSnapGrid);
1003dimSolidModelMenuItem->SetActive(SS.GW.dimSolidModel);
1004perspectiveProjMenuItem->SetActive(SS.usePerspectiveProj);
1005explodeMenuItem->SetActive(SS.explode);
1006showToolbarMenuItem->SetActive(SS.showToolbar);
1007fullScreenMenuItem->SetActive(SS.GW.window->IsFullScreen());
1008
1009if(change) SS.ScheduleShowTW();
1010}
1011
1012void GraphicsWindow::SetWorkplaneFreeIn3d() {
1013SK.GetGroup(activeGroup)->activeWorkplane = Entity::FREE_IN_3D;
1014}
1015hEntity GraphicsWindow::ActiveWorkplane() {
1016Group *g = SK.group.FindByIdNoOops(activeGroup);
1017if(g) {
1018return g->activeWorkplane;
1019} else {
1020return Entity::FREE_IN_3D;
1021}
1022}
1023bool GraphicsWindow::LockedInWorkplane() {
1024return (SS.GW.ActiveWorkplane() != Entity::FREE_IN_3D);
1025}
1026
1027void GraphicsWindow::ForceTextWindowShown() {
1028if(!showTextWindow) {
1029showTextWindow = true;
1030showTextWndMenuItem->SetActive(true);
1031SS.TW.window->SetVisible(true);
1032}
1033}
1034
1035void GraphicsWindow::DeleteTaggedRequests() {
1036// Delete any requests that were affected by this deletion.
1037for(Request &r : SK.request) {
1038if(r.workplane == Entity::FREE_IN_3D) continue;
1039if(!r.workplane.isFromRequest()) continue;
1040Request *wrkpl = SK.GetRequest(r.workplane.request());
1041if(wrkpl->tag)
1042r.tag = 1;
1043}
1044// Rewrite any point-coincident constraints that were affected by this
1045// deletion.
1046for(Request &r : SK.request) {
1047if(!r.tag) continue;
1048FixConstraintsForRequestBeingDeleted(r.h);
1049}
1050// and then delete the tagged requests.
1051SK.request.RemoveTagged();
1052
1053// An edit might be in progress for the just-deleted item. So
1054// now it's not.
1055window->HideEditor();
1056SS.TW.HideEditControl();
1057// And clear out the selection, which could contain that item.
1058ClearSuper();
1059// And regenerate to get rid of what it generates, plus anything
1060// that references it (since the regen code checks for that).
1061SS.GenerateAll(SolveSpaceUI::Generate::ALL);
1062EnsureValidActives();
1063SS.ScheduleShowTW();
1064}
1065
1066Vector GraphicsWindow::SnapToGrid(Vector p) {
1067if(!LockedInWorkplane()) return p;
1068
1069EntityBase *wrkpl = SK.GetEntity(ActiveWorkplane()),
1070*norm = wrkpl->Normal();
1071Vector wo = SK.GetEntity(wrkpl->point[0])->PointGetNum(),
1072wu = norm->NormalU(),
1073wv = norm->NormalV(),
1074wn = norm->NormalN();
1075
1076Vector pp = (p.Minus(wo)).DotInToCsys(wu, wv, wn);
1077pp.x = floor((pp.x / SS.gridSpacing) + 0.5)*SS.gridSpacing;
1078pp.y = floor((pp.y / SS.gridSpacing) + 0.5)*SS.gridSpacing;
1079pp.z = 0;
1080
1081return pp.ScaleOutOfCsys(wu, wv, wn).Plus(wo);
1082}
1083
1084void GraphicsWindow::MenuEdit(Command id) {
1085switch(id) {
1086case Command::UNSELECT_ALL:
1087SS.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.
1091if(SS.GW.gs.n == 0 &&
1092SS.GW.gs.constraints == 0 &&
1093SS.GW.pending.operation == Pending::NONE)
1094{
1095if(!(SS.TW.window->IsEditorVisible() ||
1096SS.GW.window->IsEditorVisible()))
1097{
1098if(SS.TW.shown.screen == TextWindow::Screen::STYLE_INFO) {
1099SS.TW.GoToScreen(TextWindow::Screen::LIST_OF_STYLES);
1100} else {
1101SS.TW.ClearSuper();
1102}
1103}
1104}
1105// some pending operations need an Undo to properly clean up on ESC
1106if ( (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{
1112SS.GW.ClearSuper();
1113SS.UndoUndo();
1114}
1115SS.GW.ClearSuper();
1116SS.TW.HideEditControl();
1117SS.nakedEdges.Clear();
1118SS.justExportedInfo.draw = false;
1119SS.centerOfMass.draw = false;
1120// This clears the marks drawn to indicate which points are
1121// still free to drag.
1122for(Param &p : SK.param) {
1123p.free = false;
1124}
1125if(SS.exportMode) {
1126SS.exportMode = false;
1127SS.GenerateAll(SolveSpaceUI::Generate::ALL);
1128}
1129SS.GW.persistentDirty = true;
1130break;
1131
1132case Command::SELECT_ALL: {
1133for(Entity &e : SK.entity) {
1134if(e.group != SS.GW.activeGroup) continue;
1135if(e.IsFace() || e.IsDistance()) continue;
1136if(!e.IsVisible()) continue;
1137
1138SS.GW.MakeSelected(e.h);
1139}
1140SS.GW.Invalidate();
1141SS.ScheduleShowTW();
1142break;
1143}
1144
1145case Command::SELECT_CHAIN: {
1146int newlySelected = 0;
1147bool didSomething;
1148do {
1149didSomething = false;
1150for(Entity &e : SK.entity) {
1151if(e.group != SS.GW.activeGroup) continue;
1152if(!e.HasEndpoints()) continue;
1153if(!e.IsVisible()) continue;
1154
1155Vector st = e.EndpointStart(),
1156fi = e.EndpointFinish();
1157
1158bool onChain = false, alreadySelected = false;
1159List<Selection> *ls = &(SS.GW.selection);
1160for(Selection *s = ls->First(); s; s = ls->NextAfter(s)) {
1161if(!s->entity.v) continue;
1162if(s->entity == e.h) {
1163alreadySelected = true;
1164continue;
1165}
1166Entity *se = SK.GetEntity(s->entity);
1167if(!se->HasEndpoints()) continue;
1168
1169Vector sst = se->EndpointStart(),
1170sfi = se->EndpointFinish();
1171
1172if(sst.Equals(st) || sst.Equals(fi) ||
1173sfi.Equals(st) || sfi.Equals(fi))
1174{
1175onChain = true;
1176}
1177}
1178if(onChain && !alreadySelected) {
1179SS.GW.MakeSelected(e.h);
1180newlySelected++;
1181didSomething = true;
1182}
1183}
1184} while(didSomething);
1185SS.GW.Invalidate();
1186SS.ScheduleShowTW();
1187if(newlySelected == 0) {
1188Error(_("No additional entities share endpoints with the selected entities."));
1189}
1190break;
1191}
1192
1193case Command::ROTATE_90: {
1194SS.GW.GroupSelection();
1195Entity *e = NULL;
1196if(SS.GW.gs.n == 1 && SS.GW.gs.points == 1) {
1197e = SK.GetEntity(SS.GW.gs.point[0]);
1198} else if(SS.GW.gs.n == 1 && SS.GW.gs.entities == 1) {
1199e = SK.GetEntity(SS.GW.gs.entity[0]);
1200}
1201SS.GW.ClearSelection();
1202
1203hGroup hg = e ? e->group : SS.GW.activeGroup;
1204Group *g = SK.GetGroup(hg);
1205if(g->type != Group::Type::LINKED) {
1206Error(_("To use this command, select a point or other "
1207"entity from an linked part, or make a link "
1208"group the active group."));
1209break;
1210}
1211
1212SS.UndoRemember();
1213// Rotate by ninety degrees about the coordinate axis closest
1214// to the screen normal.
1215Vector norm = SS.GW.projRight.Cross(SS.GW.projUp);
1216norm = norm.ClosestOrtho();
1217norm = norm.WithMagnitude(1);
1218Quaternion qaa = Quaternion::From(norm, PI/2);
1219
1220g->TransformImportedBy(Vector::From(0, 0, 0), qaa);
1221
1222// and regenerate as necessary.
1223SS.MarkGroupDirty(hg);
1224break;
1225}
1226
1227case Command::SNAP_TO_GRID: {
1228if(!SS.GW.LockedInWorkplane()) {
1229Error(_("No workplane is active. Activate a workplane "
1230"(with Sketch -> In Workplane) to define the plane "
1231"for the snap grid."));
1232break;
1233}
1234SS.GW.GroupSelection();
1235if(SS.GW.gs.points == 0 && SS.GW.gs.constraintLabels == 0) {
1236Error(_("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."));
1239break;
1240}
1241SS.UndoRemember();
1242
1243List<Selection> *ls = &(SS.GW.selection);
1244for(Selection *s = ls->First(); s; s = ls->NextAfter(s)) {
1245if(s->entity.v) {
1246hEntity hp = s->entity;
1247Entity *ep = SK.GetEntity(hp);
1248if(!ep->IsPoint()) continue;
1249
1250Vector p = ep->PointGetNum();
1251ep->PointForceTo(SS.GW.SnapToGrid(p));
1252SS.GW.pending.points.Add(&hp);
1253SS.MarkGroupDirty(ep->group);
1254} else if(s->constraint.v) {
1255Constraint *c = SK.GetConstraint(s->constraint);
1256std::vector<Vector> refs;
1257c->GetReferencePoints(SS.GW.GetCamera(), &refs);
1258c->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.
1263SS.GW.ClearSelection();
1264break;
1265}
1266
1267case Command::UNDO:
1268SS.UndoUndo();
1269break;
1270
1271case Command::REDO:
1272SS.UndoRedo();
1273break;
1274
1275case Command::REGEN_ALL:
1276SS.images.clear();
1277SS.ReloadAllLinked(SS.saveFile);
1278SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE);
1279SS.ScheduleShowTW();
1280break;
1281
1282case Command::EDIT_LINE_STYLES:
1283SS.TW.GoToScreen(TextWindow::Screen::LIST_OF_STYLES);
1284SS.GW.ForceTextWindowShown();
1285SS.ScheduleShowTW();
1286break;
1287case Command::VIEW_PROJECTION:
1288SS.TW.GoToScreen(TextWindow::Screen::EDIT_VIEW);
1289SS.GW.ForceTextWindowShown();
1290SS.ScheduleShowTW();
1291break;
1292case Command::CONFIGURATION:
1293SS.TW.GoToScreen(TextWindow::Screen::CONFIGURATION);
1294SS.GW.ForceTextWindowShown();
1295SS.ScheduleShowTW();
1296break;
1297
1298default: ssassert(false, "Unexpected menu ID");
1299}
1300}
1301
1302void GraphicsWindow::MenuRequest(Command id) {
1303const char *s;
1304switch(id) {
1305case Command::SEL_WORKPLANE: {
1306SS.GW.GroupSelection();
1307Group *g = SK.GetGroup(SS.GW.activeGroup);
1308
1309if(SS.GW.gs.n == 1 && SS.GW.gs.workplanes == 1) {
1310// A user-selected workplane
1311g->activeWorkplane = SS.GW.gs.entity[0];
1312SS.GW.EnsureValidActives();
1313SS.ScheduleShowTW();
1314} else if(g->type == Group::Type::DRAWING_WORKPLANE) {
1315// The group's default workplane
1316g->activeWorkplane = g->h.entity(0);
1317MessageAndRun([] {
1318// Align the view with the selected workplane
1319SS.GW.ClearSuper();
1320SS.GW.AnimateOntoWorkplane();
1321}, _("No workplane selected. Activating default workplane "
1322"for this group."));
1323} else {
1324Error(_("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
1329SS.GW.EnsureValidActives();
1330}
1331break;
1332}
1333case Command::FREE_IN_3D:
1334SS.GW.SetWorkplaneFreeIn3d();
1335SS.GW.EnsureValidActives();
1336SS.ScheduleShowTW();
1337SS.GW.Invalidate();
1338break;
1339
1340case Command::TANGENT_ARC:
1341SS.GW.GroupSelection();
1342if(SS.GW.gs.n == 1 && SS.GW.gs.points == 1) {
1343SS.GW.MakeTangentArc();
1344} else if(SS.GW.gs.n != 0) {
1345Error(_("Bad selection for tangent arc at point. Select a "
1346"single point, or select nothing to set up arc "
1347"parameters."));
1348} else {
1349SS.TW.GoToScreen(TextWindow::Screen::TANGENT_ARC);
1350SS.GW.ForceTextWindowShown();
1351SS.ScheduleShowTW();
1352SS.GW.Invalidate(); // repaint toolbar
1353}
1354break;
1355
1356case Command::ARC: s = _("click point on arc (draws anti-clockwise)"); goto c;
1357case Command::DATUM_POINT: s = _("click to place datum point"); goto c;
1358case Command::LINE_SEGMENT: s = _("click first point of line segment"); goto c;
1359case Command::CONSTR_SEGMENT:
1360s = _("click first point of construction line segment"); goto c;
1361case Command::CUBIC: s = _("click first point of cubic segment"); goto c;
1362case Command::CIRCLE: s = _("click center of circle"); goto c;
1363case Command::WORKPLANE: s = _("click origin of workplane"); goto c;
1364case Command::RECTANGLE: s = _("click one corner of rectangle"); goto c;
1365case Command::TTF_TEXT: s = _("click top left of text"); goto c;
1366case Command::IMAGE:
1367if(!SS.ReloadLinkedImage(SS.saveFile, &SS.GW.pending.filename,
1368/*canCancel=*/true)) {
1369return;
1370}
1371s = _("click top left of image"); goto c;
1372c:
1373SS.GW.pending.operation = GraphicsWindow::Pending::COMMAND;
1374SS.GW.pending.command = id;
1375SS.GW.pending.description = s;
1376SS.ScheduleShowTW();
1377SS.GW.Invalidate(); // repaint toolbar
1378break;
1379
1380case Command::CONSTRUCTION: {
1381// if we are drawing
1382if(SS.GW.pending.operation == Pending::DRAGGING_NEW_POINT ||
1383SS.GW.pending.operation == Pending::DRAGGING_NEW_LINE_POINT ||
1384SS.GW.pending.operation == Pending::DRAGGING_NEW_ARC_POINT ||
1385SS.GW.pending.operation == Pending::DRAGGING_NEW_CUBIC_POINT ||
1386SS.GW.pending.operation == Pending::DRAGGING_NEW_RADIUS) {
1387for(auto &hr : SS.GW.pending.requests) {
1388Request* r = SK.GetRequest(hr);
1389r->construction = !(r->construction);
1390SS.MarkGroupDirty(r->group);
1391}
1392SS.GW.Invalidate();
1393break;
1394}
1395SS.GW.GroupSelection();
1396if(SS.GW.gs.entities == 0) {
1397Error(_("No entities are selected. Select entities before "
1398"trying to toggle their construction state."));
1399break;
1400}
1401SS.UndoRemember();
1402int i;
1403for(i = 0; i < SS.GW.gs.entities; i++) {
1404hEntity he = SS.GW.gs.entity[i];
1405if(!he.isFromRequest()) continue;
1406Request *r = SK.GetRequest(he.request());
1407r->construction = !(r->construction);
1408SS.MarkGroupDirty(r->group);
1409}
1410SS.GW.ClearSelection();
1411break;
1412}
1413
1414case Command::SPLIT_CURVES:
1415SS.GW.SplitLinesOrCurves();
1416break;
1417
1418default: ssassert(false, "Unexpected menu ID");
1419}
1420}
1421
1422void GraphicsWindow::ClearSuper() {
1423if(window) window->HideEditor();
1424ClearPending();
1425ClearSelection();
1426hover.Clear();
1427EnsureValidActives();
1428}
1429
1430void 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.
1435if(!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.
1439Group *g = SK.GetGroup(SS.GW.activeGroup);
1440if(*v && (g->displayOutlines.l.IsEmpty() && (v == &showEdges || v == &showOutlines))) {
1441SS.GenerateAll(SolveSpaceUI::Generate::UNTIL_ACTIVE);
1442}
1443
1444if(v == &showFaces) {
1445if(g->type == Group::Type::DRAWING_WORKPLANE || g->type == Group::Type::DRAWING_3D) {
1446showFacesDrawing = showFaces;
1447} else {
1448showFacesNonDrawing = showFaces;
1449}
1450}
1451
1452Invalidate(/*clearPersistent=*/true);
1453SS.ScheduleShowTW();
1454}
1455
1456bool GraphicsWindow::SuggestLineConstraint(hRequest request, Constraint::Type *type) {
1457if(!(LockedInWorkplane() && SS.automaticLineConstraints))
1458return false;
1459
1460Entity *ptA = SK.GetEntity(request.entity(1)),
1461*ptB = SK.GetEntity(request.entity(2));
1462
1463Expr *au, *av, *bu, *bv;
1464
1465ptA->PointGetExprsInWorkplane(ActiveWorkplane(), &au, &av);
1466ptB->PointGetExprsInWorkplane(ActiveWorkplane(), &bu, &bv);
1467
1468double du = au->Minus(bu)->Eval();
1469double dv = av->Minus(bv)->Eval();
1470
1471const double TOLERANCE_RATIO = 0.02;
1472if(fabs(dv) > LENGTH_EPS && fabs(du / dv) < TOLERANCE_RATIO) {
1473*type = Constraint::Type::VERTICAL;
1474return true;
1475} else if(fabs(du) > LENGTH_EPS && fabs(dv / du) < TOLERANCE_RATIO) {
1476*type = Constraint::Type::HORIZONTAL;
1477return true;
1478}
1479return false;
1480}
1481