Solvespace

Форк
0
/
mouse.cpp 
1574 строки · 59.5 Кб
1
//-----------------------------------------------------------------------------
2
// Anything relating to mouse, keyboard, or 6-DOF mouse input.
3
//
4
// Copyright 2008-2013 Jonathan Westhues.
5
//-----------------------------------------------------------------------------
6
#include "solvespace.h"
7

8
void GraphicsWindow::UpdateDraggedPoint(hEntity hp, double mx, double my) {
9
    Entity *p = SK.GetEntity(hp);
10
    Vector pos = p->PointGetNum();
11
    UpdateDraggedNum(&pos, mx, my);
12
    p->PointForceTo(pos);
13

14
    SS.ScheduleShowTW();
15
}
16

17
void GraphicsWindow::UpdateDraggedNum(Vector *pos, double mx, double my) {
18
    *pos = pos->Plus(projRight.ScaledBy((mx - orig.mouse.x)/scale));
19
    *pos = pos->Plus(projUp.ScaledBy((my - orig.mouse.y)/scale));
20
}
21

22
void GraphicsWindow::AddPointToDraggedList(hEntity hp) {
23
    Entity *p = SK.GetEntity(hp);
24
    // If an entity and its points are both selected, then its points could
25
    // end up in the list twice. This would be bad, because it would move
26
    // twice as far as the mouse pointer...
27
    List<hEntity> *lhe = &(pending.points);
28
    for(hEntity *hee = lhe->First(); hee; hee = lhe->NextAfter(hee)) {
29
        if(*hee == hp) {
30
            // Exact same point.
31
            return;
32
        }
33
        Entity *pe = SK.GetEntity(*hee);
34
        if(pe->type == p->type &&
35
           pe->type != Entity::Type::POINT_IN_2D &&
36
           pe->type != Entity::Type::POINT_IN_3D &&
37
           pe->group == p->group)
38
        {
39
            // Transform-type point, from the same group. So it handles the
40
            // same unknowns.
41
            return;
42
        }
43
    }
44
    pending.points.Add(&hp);
45
}
46

47
void GraphicsWindow::StartDraggingByEntity(hEntity he) {
48
    Entity *e = SK.GetEntity(he);
49
    if(e->IsPoint()) {
50
        AddPointToDraggedList(e->h);
51
    } else if(e->type == Entity::Type::LINE_SEGMENT ||
52
              e->type == Entity::Type::ARC_OF_CIRCLE ||
53
              e->type == Entity::Type::CUBIC ||
54
              e->type == Entity::Type::CUBIC_PERIODIC ||
55
              e->type == Entity::Type::CIRCLE ||
56
              e->type == Entity::Type::TTF_TEXT ||
57
              e->type == Entity::Type::IMAGE)
58
    {
59
        int pts;
60
        EntReqTable::GetEntityInfo(e->type, e->extraPoints,
61
            NULL, &pts, NULL, NULL);
62
        for(int i = 0; i < pts; i++) {
63
            AddPointToDraggedList(e->point[i]);
64
        }
65
    }
66
}
67

68
void GraphicsWindow::StartDraggingBySelection() {
69
    List<Selection> *ls = &(selection);
70
    for(Selection *s = ls->First(); s; s = ls->NextAfter(s)) {
71
        if(!s->entity.v) continue;
72

73
        StartDraggingByEntity(s->entity);
74
    }
75
    // The user might select a point, and then click it again to start
76
    // dragging; but the point just got unselected by that click. So drag
77
    // the hovered item too, and they'll always have it.
78
    if(hover.entity.v) {
79
        hEntity dragEntity = ChooseFromHoverToDrag().entity;
80
        if(dragEntity != Entity::NO_ENTITY) {
81
            StartDraggingByEntity(dragEntity);
82
        }
83
    }
84
}
85

86
void GraphicsWindow::MouseMoved(double x, double y, bool leftDown,
87
            bool middleDown, bool rightDown, bool shiftDown, bool ctrlDown)
88
{
89
    if(window->IsEditorVisible()) return;
90
    if(context.active) return;
91

92
    SS.extraLine.draw = false;
93

94
    if(!orig.mouseDown) {
95
        // If someone drags the mouse into our window with the left button
96
        // already depressed, then we don't have our starting point; so
97
        // don't try.
98
        leftDown = false;
99
    }
100

101
    if(rightDown) {
102
        middleDown = true;
103
        shiftDown = !shiftDown;
104
    }
105

106
    // Not passing right-button and middle-button drags to the toolbar avoids
107
    // some cosmetic issues with trackpad pans/rotates implemented with
108
    // simulated right-button drag events causing spurious hover events.
109
    if(SS.showToolbar && !middleDown) {
110
        if(ToolbarMouseMoved((int)x, (int)y)) {
111
            hover.Clear();
112
            return;
113
        }
114
    }
115

116
    if(!leftDown && (pending.operation == Pending::DRAGGING_POINTS ||
117
                     pending.operation == Pending::DRAGGING_MARQUEE))
118
    {
119
        ClearPending();
120
        Invalidate();
121
    }
122

123
    Point2d mp = Point2d::From(x, y);
124
    currentMousePosition = mp;
125

126
    if(rightDown && orig.mouse.DistanceTo(mp) < 5 && !orig.startedMoving) {
127
        // Avoid accidentally panning (or rotating if shift is down) if the
128
        // user wants a context menu.
129
        return;
130
    }
131
    orig.startedMoving = true;
132

133
    // If the middle button is down, then mouse movement is used to pan and
134
    // rotate our view. This wins over everything else.
135
    if(middleDown) {
136
        hover.Clear();
137

138
        double dx = (x - orig.mouse.x) / scale;
139
        double dy = (y - orig.mouse.y) / scale;
140

141
        if(!(shiftDown || ctrlDown)) {
142
            double sign = SS.cameraNav ? -1.0 : 1.0;
143
            double s = 0.3*(PI/180)*scale*sign; // degrees per pixel
144
            if(SS.turntableNav) {               // lock the Z to vertical
145
                projRight = orig.projRight.RotatedAbout(Vector::From(0, 0, 1), -s * dx);
146
                projUp    = orig.projUp.RotatedAbout(
147
                    Vector::From(orig.projRight.x, orig.projRight.y, orig.projRight.y), s * dy);
148
            } else {
149
                projRight = orig.projRight.RotatedAbout(orig.projUp, -s * dx);
150
                projUp    = orig.projUp.RotatedAbout(orig.projRight, s * dy);
151
            }
152

153
            NormalizeProjectionVectors();
154
        } else if(ctrlDown) {
155
            double theta = atan2(orig.mouse.y, orig.mouse.x);
156
            theta -= atan2(y, x);
157
            SS.extraLine.draw = true;
158
            SS.extraLine.ptA = UnProjectPoint(Point2d::From(0, 0));
159
            SS.extraLine.ptB = UnProjectPoint(mp);
160

161
            Vector normal = orig.projRight.Cross(orig.projUp);
162
            projRight = orig.projRight.RotatedAbout(normal, theta);
163
            projUp = orig.projUp.RotatedAbout(normal, theta);
164

165
            NormalizeProjectionVectors();
166
        } else {
167
            offset.x = orig.offset.x + dx*projRight.x + dy*projUp.x;
168
            offset.y = orig.offset.y + dx*projRight.y + dy*projUp.y;
169
            offset.z = orig.offset.z + dx*projRight.z + dy*projUp.z;
170
        }
171

172
        orig.projRight = projRight;
173
        orig.projUp = projUp;
174
        orig.offset = offset;
175
        orig.mouse.x = x;
176
        orig.mouse.y = y;
177

178
        if(SS.TW.shown.screen == TextWindow::Screen::EDIT_VIEW) {
179
            if(havePainted) {
180
                SS.ScheduleShowTW();
181
            }
182
        }
183
        Invalidate();
184
        havePainted = false;
185
        return;
186
    }
187

188
    if(pending.operation == Pending::NONE) {
189
        double dm = orig.mouse.DistanceTo(mp);
190
        // If we're currently not doing anything, then see if we should
191
        // start dragging something.
192
        if(leftDown && dm > 3) {
193
            Entity *e = NULL;
194
            hEntity dragEntity = ChooseFromHoverToDrag().entity;
195
            if(dragEntity.v) e = SK.GetEntity(dragEntity);
196
            if(e && e->type != Entity::Type::WORKPLANE) {
197
                if(!hoverWasSelectedOnMousedown) {
198
                    // The user clicked an unselected entity, which
199
                    // means they're dragging just the hovered thing,
200
                    // not the full selection. So clear all the selection
201
                    // except that entity.
202
                    ClearSelection();
203
                    MakeSelected(dragEntity);
204
                }
205
                if(e->type == Entity::Type::CIRCLE && selection.n <= 1) {
206
                    // Drag the radius.
207
                    pending.circle = dragEntity;
208
                    pending.operation = Pending::DRAGGING_RADIUS;
209
                } else if(e->IsNormal()) {
210
                    pending.normal = dragEntity;
211
                    pending.operation = Pending::DRAGGING_NORMAL;
212
                } else {
213
                    StartDraggingBySelection();
214
                    hover.Clear();
215
                    pending.operation = Pending::DRAGGING_POINTS;
216
                }
217
            } else if(hover.constraint.v &&
218
                            SK.GetConstraint(hover.constraint)->HasLabel())
219
            {
220
                ClearSelection();
221
                pending.constraint = hover.constraint;
222
                pending.operation = Pending::DRAGGING_CONSTRAINT;
223
            }
224
            if(pending.operation != Pending::NONE) {
225
                // We just started a drag, so remember for the undo before
226
                // the drag changes anything.
227
                SS.UndoRemember();
228
            } else {
229
                if(!hover.constraint.v) {
230
                    // That's just marquee selection, which should not cause
231
                    // an undo remember.
232
                    if(dm > 10) {
233
                        if(hover.entity.v) {
234
                            // Avoid accidentally selecting workplanes when
235
                            // starting drags.
236
                            MakeUnselected(hover.entity, /*coincidentPointTrick=*/false);
237
                            hover.Clear();
238
                        }
239
                        pending.operation = Pending::DRAGGING_MARQUEE;
240
                        orig.marqueePoint =
241
                            UnProjectPoint(orig.mouseOnButtonDown);
242
                    }
243
                }
244
            }
245
        } else {
246
            // Otherwise, just hit test and give up; but don't hit test
247
            // if the mouse is down, because then the user could hover
248
            // a point, mouse down (thus selecting it), and drag, in an
249
            // effort to drag the point, but instead hover a different
250
            // entity before we move far enough to start the drag.
251
            if(!leftDown) {
252
                // Hit testing can potentially take a lot of time.
253
                // If we haven't painted since last time we highlighted
254
                // something, don't hit test again, since this just causes
255
                // a lag.
256
                if(!havePainted) return;
257
                HitTestMakeSelection(mp);
258
            }
259
        }
260
        return;
261
    }
262

263
    // If the user has started an operation from the menu, but not
264
    // completed it, then just do the selection.
265
    if(pending.operation == Pending::COMMAND) {
266
        HitTestMakeSelection(mp);
267
        return;
268
    }
269

270
    if(pending.operation == Pending::DRAGGING_POINTS && ctrlDown) {
271
        SS.extraLine.ptA = UnProjectPoint(orig.mouseOnButtonDown);
272
        SS.extraLine.ptB = UnProjectPoint(mp);
273
        SS.extraLine.draw = true;
274
    }
275

276
    // We're currently dragging something; so do that. But if we haven't
277
    // painted since the last time we solved, do nothing, because there's
278
    // no sense solving a frame and not displaying it.
279
    if(!havePainted) {
280
        return;
281
    }
282

283
    havePainted = false;
284
    switch(pending.operation) {
285
        case Pending::DRAGGING_CONSTRAINT: {
286
            Constraint *c = SK.constraint.FindById(pending.constraint);
287
            UpdateDraggedNum(&(c->disp.offset), x, y);
288
            orig.mouse = mp;
289
            Invalidate();
290
            return;
291
        }
292

293
        case Pending::DRAGGING_NEW_LINE_POINT:
294
            if(!ctrlDown) {
295
                SS.GW.pending.hasSuggestion =
296
                    SS.GW.SuggestLineConstraint(SS.GW.pending.request, &SS.GW.pending.suggestion);
297
            } else {
298
                SS.GW.pending.hasSuggestion = false;
299
            }
300
            // fallthrough
301
        case Pending::DRAGGING_NEW_POINT:
302
            UpdateDraggedPoint(pending.point, x, y);
303
            HitTestMakeSelection(mp);
304
            SS.MarkGroupDirtyByEntity(pending.point);
305
            orig.mouse = mp;
306
            break;
307

308
        case Pending::DRAGGING_POINTS:
309
            if(shiftDown || ctrlDown) {
310
                // Edit the rotation associated with a POINT_N_ROT_TRANS,
311
                // either within (ctrlDown) or out of (shiftDown) the plane
312
                // of the screen. So first get the rotation to apply, in qt.
313
                Quaternion qt;
314
                if(ctrlDown) {
315
                    double d = mp.DistanceTo(orig.mouseOnButtonDown);
316
                    if(d < 25) {
317
                        // Don't start dragging the position about the normal
318
                        // until we're a little ways out, to get a reasonable
319
                        // reference pos
320
                        qt = Quaternion::IDENTITY;
321
                    } else {
322
                        double theta = atan2(orig.mouse.y-orig.mouseOnButtonDown.y,
323
                                             orig.mouse.x-orig.mouseOnButtonDown.x);
324
                        theta -= atan2(y-orig.mouseOnButtonDown.y,
325
                                       x-orig.mouseOnButtonDown.x);
326

327
                        Vector gn = projRight.Cross(projUp);
328
                        qt = Quaternion::From(gn, -theta);
329
                    }
330
                } else {
331
                    double dx = -(x - orig.mouse.x);
332
                    double dy = -(y - orig.mouse.y);
333
                    double s = 0.3*(PI/180); // degrees per pixel
334
                    qt = Quaternion::From(projUp,   -s*dx).Times(
335
                         Quaternion::From(projRight, s*dy));
336
                }
337

338
                // Now apply this rotation to the points being dragged.
339
                List<hEntity> *lhe = &(pending.points);
340
                for(hEntity *he = lhe->First(); he; he = lhe->NextAfter(he)) {
341
                    Entity *e = SK.GetEntity(*he);
342
                    if(e->type != Entity::Type::POINT_N_ROT_TRANS) {
343
                        if(ctrlDown) {
344
                            Vector p = e->PointGetNum();
345
                            p = p.Minus(SS.extraLine.ptA);
346
                            p = qt.Rotate(p);
347
                            p = p.Plus(SS.extraLine.ptA);
348
                            e->PointForceTo(p);
349
                        } else {
350
                            UpdateDraggedPoint(*he, x, y);
351
                        }
352
                    } else {
353
                        Quaternion q = e->PointGetQuaternion();
354
                        Vector     p = e->PointGetNum();
355
                        q = qt.Times(q);
356
                        e->PointForceQuaternionTo(q);
357
                        // Let's rotate about the selected point; so fix up the
358
                        // translation so that that point didn't move.
359
                        e->PointForceTo(p);
360
                    }
361
                    SS.MarkGroupDirtyByEntity(e->h);
362
                }
363
            } else {
364
                List<hEntity> *lhe = &(pending.points);
365
                for(hEntity *he = lhe->First(); he; he = lhe->NextAfter(he)) {
366
                    UpdateDraggedPoint(*he, x, y);
367
                    SS.MarkGroupDirtyByEntity(*he);
368
                }
369
            }
370
            orig.mouse = mp;
371
            break;
372

373
        case Pending::DRAGGING_NEW_CUBIC_POINT: {
374
            UpdateDraggedPoint(pending.point, x, y);
375
            HitTestMakeSelection(mp);
376

377
            hRequest hr = pending.point.request();
378
            if(pending.point == hr.entity(4)) {
379
                // The very first segment; dragging final point drags both
380
                // tangent points.
381
                Vector p0 = SK.GetEntity(hr.entity(1))->PointGetNum(),
382
                       p3 = SK.GetEntity(hr.entity(4))->PointGetNum(),
383
                       p1 = p0.ScaledBy(2.0/3).Plus(p3.ScaledBy(1.0/3)),
384
                       p2 = p0.ScaledBy(1.0/3).Plus(p3.ScaledBy(2.0/3));
385
                SK.GetEntity(hr.entity(1+1))->PointForceTo(p1);
386
                SK.GetEntity(hr.entity(1+2))->PointForceTo(p2);
387
            } else {
388
                // A subsequent segment; dragging point drags only final
389
                // tangent point.
390
                int i = SK.GetEntity(hr.entity(0))->extraPoints;
391
                Vector pn   = SK.GetEntity(hr.entity(4+i))->PointGetNum(),
392
                       pnm2 = SK.GetEntity(hr.entity(2+i))->PointGetNum(),
393
                       pnm1 = (pn.Plus(pnm2)).ScaledBy(0.5);
394
                SK.GetEntity(hr.entity(3+i))->PointForceTo(pnm1);
395
            }
396

397
            orig.mouse = mp;
398
            SS.MarkGroupDirtyByEntity(pending.point);
399
            break;
400
        }
401
        case Pending::DRAGGING_NEW_ARC_POINT: {
402
            UpdateDraggedPoint(pending.point, x, y);
403
            HitTestMakeSelection(mp);
404

405
            hRequest hr = pending.point.request();
406
            Vector ona = SK.GetEntity(hr.entity(2))->PointGetNum();
407
            Vector onb = SK.GetEntity(hr.entity(3))->PointGetNum();
408
            Vector center = (ona.Plus(onb)).ScaledBy(0.5);
409

410
            SK.GetEntity(hr.entity(1))->PointForceTo(center);
411

412
            orig.mouse = mp;
413
            SS.MarkGroupDirtyByEntity(pending.point);
414
            break;
415
        }
416
        case Pending::DRAGGING_NEW_RADIUS:
417
        case Pending::DRAGGING_RADIUS: {
418
            Entity *circle = SK.GetEntity(pending.circle);
419
            Vector center = SK.GetEntity(circle->point[0])->PointGetNum();
420
            Point2d c2 = ProjectPoint(center);
421
            double r = c2.DistanceTo(mp)/scale;
422
            SK.GetEntity(circle->distance)->DistanceForceTo(r);
423

424
            SS.MarkGroupDirtyByEntity(pending.circle);
425
            SS.ScheduleShowTW();
426
            break;
427
        }
428

429
        case Pending::DRAGGING_NORMAL: {
430
            Entity *normal = SK.GetEntity(pending.normal);
431
            Vector p = SK.GetEntity(normal->point[0])->PointGetNum();
432
            Point2d p2 = ProjectPoint(p);
433

434
            Quaternion q = normal->NormalGetNum();
435
            Vector u = q.RotationU(), v = q.RotationV();
436

437
            if(ctrlDown) {
438
                double theta = atan2(orig.mouse.y-p2.y, orig.mouse.x-p2.x);
439
                theta -= atan2(y-p2.y, x-p2.x);
440

441
                Vector normal = projRight.Cross(projUp);
442
                u = u.RotatedAbout(normal, -theta);
443
                v = v.RotatedAbout(normal, -theta);
444
            } else {
445
                double dx = -(x - orig.mouse.x);
446
                double dy = -(y - orig.mouse.y);
447
                double s = 0.3*(PI/180); // degrees per pixel
448
                u = u.RotatedAbout(projUp, -s*dx);
449
                u = u.RotatedAbout(projRight, s*dy);
450
                v = v.RotatedAbout(projUp, -s*dx);
451
                v = v.RotatedAbout(projRight, s*dy);
452
            }
453
            orig.mouse = mp;
454
            normal->NormalForceTo(Quaternion::From(u, v));
455

456
            SS.MarkGroupDirtyByEntity(pending.normal);
457
            break;
458
        }
459

460
        case Pending::DRAGGING_MARQUEE:
461
            orig.mouse = mp;
462
            Invalidate();
463
            return;
464

465
        case Pending::NONE:
466
        case Pending::COMMAND:
467
            ssassert(false, "Unexpected pending operation");
468
    }
469
}
470

471
void GraphicsWindow::ClearPending(bool scheduleShowTW) {
472
    pending.points.Clear();
473
    pending.requests.Clear();
474
    pending = {};
475
    if(scheduleShowTW) {
476
        SS.ScheduleShowTW();
477
    }
478
}
479

480
bool GraphicsWindow::IsFromPending(hRequest r) {
481
    for(auto &req : pending.requests) {
482
        if(req == r) return true;
483
    }
484
    return false;
485
}
486

487
void GraphicsWindow::AddToPending(hRequest r) {
488
    pending.requests.Add(&r);
489
}
490

491
void GraphicsWindow::ReplacePending(hRequest before, hRequest after) {
492
    for(auto &req : pending.requests) {
493
        if(req == before) {
494
            req = after;
495
        }
496
    }
497
}
498

499
void GraphicsWindow::MouseMiddleOrRightDown(double x, double y) {
500
    if(window->IsEditorVisible()) return;
501

502
    orig.offset = offset;
503
    orig.projUp = projUp;
504
    orig.projRight = projRight;
505
    orig.mouse.x = x;
506
    orig.mouse.y = y;
507
    orig.startedMoving = false;
508
}
509

510
void GraphicsWindow::MouseRightUp(double x, double y) {
511
    SS.extraLine.draw = false;
512
    Invalidate();
513

514
    // Don't show a context menu if the user is right-clicking the toolbar,
515
    // or if they are finishing a pan.
516
    if(ToolbarMouseMoved((int)x, (int)y)) return;
517
    if(orig.startedMoving) return;
518

519
    if(context.active) return;
520

521
    if(pending.operation == Pending::DRAGGING_NEW_LINE_POINT && pending.hasSuggestion) {
522
        Constraint::TryConstrain(SS.GW.pending.suggestion,
523
            Entity::NO_ENTITY, Entity::NO_ENTITY, pending.request.entity(0));
524
    }
525

526
    if(pending.operation == Pending::DRAGGING_NEW_LINE_POINT ||
527
       pending.operation == Pending::DRAGGING_NEW_CUBIC_POINT ||
528
       pending.operation == Pending::DRAGGING_NEW_ARC_POINT ||
529
       pending.operation == Pending::DRAGGING_NEW_RADIUS ||
530
       pending.operation == Pending::DRAGGING_NEW_POINT
531
       )
532
    {
533
        // Special case; use a right click to stop drawing lines, since
534
        // a left click would draw another one. This is quicker and more
535
        // intuitive than hitting escape. Likewise for other entities
536
        // for consistency.
537
        ClearPending();
538
        return;
539
    }
540

541
    // The current mouse location
542
    Vector v = offset.ScaledBy(-1);
543
    v = v.Plus(projRight.ScaledBy(x/scale));
544
    v = v.Plus(projUp.ScaledBy(y/scale));
545

546
    Platform::MenuRef menu = Platform::CreateMenu();
547
    context.active = true;
548

549
    if(!hover.IsEmpty()) {
550
        MakeSelected(&hover);
551
        SS.ScheduleShowTW();
552
    }
553
    GroupSelection();
554

555
    bool itemsSelected = (gs.n > 0 || gs.constraints > 0);
556
    if(itemsSelected) {
557
        if(gs.stylables > 0) {
558
            Platform::MenuRef styleMenu = menu->AddSubMenu(_("Assign to Style"));
559

560
            bool empty = true;
561
            for(const Style &s : SK.style) {
562
                if(s.h.v < Style::FIRST_CUSTOM) continue;
563

564
                uint32_t v = s.h.v;
565

566
                styleMenu->AddItem(s.DescriptionString(), [v]() {
567
                    Style::AssignSelectionToStyle(v);
568
                });
569
                empty = false;
570
            }
571

572
            if(!empty) styleMenu->AddSeparator();
573

574
            styleMenu->AddItem(_("No Style"), []() {
575
                Style::AssignSelectionToStyle(0);
576
            });
577
            styleMenu->AddItem(_("Newly Created Custom Style..."), [this]() {
578
                uint32_t vs = Style::CreateCustomStyle();
579
                Style::AssignSelectionToStyle(vs);
580
                ForceTextWindowShown();
581
            });
582
        }
583
        if(gs.n + gs.constraints == 1) {
584
            menu->AddItem(_("Group Info"), [this]() {
585
                hGroup hg;
586
                if(gs.entities == 1) {
587
                    hg = SK.GetEntity(gs.entity[0])->group;
588
                } else if(gs.points == 1) {
589
                    hg = SK.GetEntity(gs.point[0])->group;
590
                } else if(gs.constraints == 1) {
591
                    hg = SK.GetConstraint(gs.constraint[0])->group;
592
                } else {
593
                    return;
594
                }
595
                ClearSelection();
596

597
                SS.TW.GoToScreen(TextWindow::Screen::GROUP_INFO);
598
                SS.TW.shown.group = hg;
599
                SS.ScheduleShowTW();
600
                ForceTextWindowShown();
601
            });
602
        }
603
        if(gs.n + gs.constraints == 1 && gs.stylables == 1) {
604
            menu->AddItem(_("Style Info"), [this]() {
605
                hStyle hs;
606
                if(gs.entities == 1) {
607
                    hs = Style::ForEntity(gs.entity[0]);
608
                } else if(gs.points == 1) {
609
                    hs = Style::ForEntity(gs.point[0]);
610
                } else if(gs.constraints == 1) {
611
                    hs = SK.GetConstraint(gs.constraint[0])->GetStyle();
612
                } else {
613
                    return;
614
                }
615
                ClearSelection();
616

617
                SS.TW.GoToScreen(TextWindow::Screen::STYLE_INFO);
618
                SS.TW.shown.style = hs;
619
                SS.ScheduleShowTW();
620
                ForceTextWindowShown();
621
            });
622
        }
623
        if(gs.withEndpoints > 0) {
624
            menu->AddItem(_("Select Edge Chain"),
625
                []() { MenuEdit(Command::SELECT_CHAIN); });
626
        }
627
        if(gs.constraints == 1 && gs.n == 0) {
628
            Constraint *c = SK.GetConstraint(gs.constraint[0]);
629
            if(c->HasLabel() && c->type != Constraint::Type::COMMENT) {
630
                menu->AddItem(_("Toggle Reference Dimension"),
631
                    []() { Constraint::MenuConstrain(Command::REFERENCE); });
632
            }
633
            if(c->type == Constraint::Type::ANGLE ||
634
                c->type == Constraint::Type::EQUAL_ANGLE)
635
            {
636
                menu->AddItem(_("Other Supplementary Angle"),
637
                    []() { Constraint::MenuConstrain(Command::OTHER_ANGLE); });
638
            }
639
        }
640
        if(gs.constraintLabels > 0 || gs.points > 0) {
641
            menu->AddItem(_("Snap to Grid"),
642
                []() { MenuEdit(Command::SNAP_TO_GRID); });
643
        }
644

645
        if(gs.points == 1 && gs.point[0].isFromRequest()) {
646
            Request *r = SK.GetRequest(gs.point[0].request());
647
            int index = r->IndexOfPoint(gs.point[0]);
648
            if((r->type == Request::Type::CUBIC && (index > 1 && index < r->extraPoints + 2)) ||
649
                    r->type == Request::Type::CUBIC_PERIODIC) {
650
                menu->AddItem(_("Remove Spline Point"), [this, r]() {
651
                    int index = r->IndexOfPoint(gs.point[0]);
652
                    ssassert(r->extraPoints != 0,
653
                             "Expected a bezier with interior control points");
654

655
                    SS.UndoRemember();
656
                    Entity *e = SK.GetEntity(r->h.entity(0));
657

658
                    // First, fix point-coincident constraints involving this point.
659
                    // Then, remove all other constraints, since they would otherwise
660
                    // jump to an adjacent one and mess up the bezier after generation.
661
                    FixConstraintsForPointBeingDeleted(e->point[index]);
662
                    RemoveConstraintsForPointBeingDeleted(e->point[index]);
663

664
                    for(int i = index; i < MAX_POINTS_IN_ENTITY - 1; i++) {
665
                        if(e->point[i + 1].v == 0) break;
666
                        Entity *p0 = SK.GetEntity(e->point[i]);
667
                        Entity *p1 = SK.GetEntity(e->point[i + 1]);
668
                        ReplacePointInConstraints(p1->h, p0->h);
669
                        p0->PointForceTo(p1->PointGetNum());
670
                    }
671
                    r->extraPoints--;
672
                    SS.MarkGroupDirtyByEntity(gs.point[0]);
673
                    ClearSelection();
674
                });
675
            }
676
        }
677
        if(gs.entities == 1 && gs.entity[0].isFromRequest()) {
678
            Request *r = SK.GetRequest(gs.entity[0].request());
679
            if(r->type == Request::Type::CUBIC || r->type == Request::Type::CUBIC_PERIODIC) {
680
                Entity *e = SK.GetEntity(gs.entity[0]);
681
                int addAfterPoint = e->GetPositionOfPoint(GetCamera(), Point2d::From(x, y));
682
                ssassert(addAfterPoint != -1, "Expected a nearest bezier point to be located");
683
                // Skip derivative point.
684
                if(r->type == Request::Type::CUBIC) addAfterPoint++;
685
                menu->AddItem(_("Add Spline Point"), [this, r, addAfterPoint, v]() {
686
                    int pointCount = r->extraPoints +
687
                                     ((r->type == Request::Type::CUBIC_PERIODIC) ? 3 : 4);
688
                    if(pointCount >= MAX_POINTS_IN_ENTITY) {
689
                        Error(_("Cannot add spline point: maximum number of points reached."));
690
                        return;
691
                    }
692

693
                    SS.UndoRemember();
694
                    r->extraPoints++;
695
                    SS.MarkGroupDirtyByEntity(gs.entity[0]);
696
                    SS.GenerateAll(SolveSpaceUI::Generate::REGEN);
697

698
                    Entity *e = SK.GetEntity(r->h.entity(0));
699
                    for(int i = MAX_POINTS_IN_ENTITY; i > addAfterPoint + 1; i--) {
700
                        Entity *p0 = SK.entity.FindByIdNoOops(e->point[i]);
701
                        if(p0 == NULL) continue;
702
                        Entity *p1 = SK.GetEntity(e->point[i - 1]);
703
                        ReplacePointInConstraints(p1->h, p0->h);
704
                        p0->PointForceTo(p1->PointGetNum());
705
                    }
706
                    Entity *p = SK.GetEntity(e->point[addAfterPoint + 1]);
707
                    p->PointForceTo(v);
708
                    SS.MarkGroupDirtyByEntity(gs.entity[0]);
709
                    ClearSelection();
710
                });
711
            }
712
        }
713
        if(gs.entities == gs.n) {
714
            menu->AddItem(_("Toggle Construction"),
715
                []() { MenuRequest(Command::CONSTRUCTION); });
716
        }
717

718
        if(gs.points == 1) {
719
            Entity *p = SK.GetEntity(gs.point[0]);
720
            Constraint *c = nullptr;
721
            IdList<Constraint,hConstraint> *lc = &(SK.constraint);
722
            for(Constraint &ci : *lc) {
723
                if(ci.type != Constraint::Type::POINTS_COINCIDENT) continue;
724
                if(ci.ptA == p->h || ci.ptB == p->h) {
725
                    c = &ci;
726
                    break;
727
                }
728
            }
729
            if(c) {
730
                menu->AddItem(_("Delete Point-Coincident Constraint"), [this, p]() {
731
                    if(!p->IsPoint()) return;
732

733
                    SS.UndoRemember();
734
                    SK.constraint.ClearTags();
735
                    for(Constraint &c : SK.constraint) {
736
                        if(c.type != Constraint::Type::POINTS_COINCIDENT) continue;
737
                        if(c.ptA == p->h || c.ptB == p->h) {
738
                            c.tag = 1;
739
                        }
740
                    }
741
                    SK.constraint.RemoveTagged();
742
                    ClearSelection();
743
                });
744
            }
745
        }
746
        menu->AddSeparator();
747
        if(LockedInWorkplane()) {
748
            menu->AddItem(_("Cut"),
749
                []() { MenuClipboard(Command::CUT); });
750
            menu->AddItem(_("Copy"),
751
                []() { MenuClipboard(Command::COPY); });
752
        }
753
    } else {
754
        menu->AddItem(_("Select All"),
755
            []() { MenuEdit(Command::SELECT_ALL); });
756
    }
757

758
    if((!SS.clipboard.r.IsEmpty() || !SS.clipboard.c.IsEmpty()) && LockedInWorkplane()) {
759
        menu->AddItem(_("Paste"),
760
            []() { MenuClipboard(Command::PASTE); });
761
        menu->AddItem(_("Paste Transformed..."),
762
            []() { MenuClipboard(Command::PASTE_TRANSFORM); });
763
    }
764

765
    if(itemsSelected) {
766
        menu->AddItem(_("Delete"),
767
            []() { MenuClipboard(Command::DELETE); });
768
        menu->AddSeparator();
769
        menu->AddItem(_("Unselect All"),
770
            []() { MenuEdit(Command::UNSELECT_ALL); });
771
    }
772
    // If only one item is selected, then it must be the one that we just
773
    // selected from the hovered item; in which case unselect all and hovered
774
    // are equivalent.
775
    if(!hover.IsEmpty() && selection.n > 1) {
776
        menu->AddItem(_("Unselect Hovered"), [this] {
777
            if(!hover.IsEmpty()) {
778
                MakeUnselected(&hover, /*coincidentPointTrick=*/true);
779
            }
780
        });
781
    }
782

783
    if(itemsSelected) {
784
        menu->AddSeparator();
785
        menu->AddItem(_("Zoom to Fit"),
786
            []() { MenuView(Command::ZOOM_TO_FIT); });
787
    }
788

789
    menu->PopUp();
790

791
    context.active = false;
792
    SS.ScheduleShowTW();
793
}
794

795
hRequest GraphicsWindow::AddRequest(Request::Type type) {
796
    return AddRequest(type, /*rememberForUndo=*/true);
797
}
798
hRequest GraphicsWindow::AddRequest(Request::Type type, bool rememberForUndo) {
799
    if(rememberForUndo) SS.UndoRemember();
800

801
    Request r = {};
802
    r.group = activeGroup;
803
    Group *g = SK.GetGroup(activeGroup);
804
    if(g->type == Group::Type::DRAWING_3D || g->type == Group::Type::DRAWING_WORKPLANE) {
805
        r.construction = false;
806
    } else {
807
        r.construction = true;
808
    }
809
    r.workplane = ActiveWorkplane();
810
    r.type = type;
811
    SK.request.AddAndAssignId(&r);
812

813
    // We must regenerate the parameters, so that the code that tries to
814
    // place this request's entities where the mouse is can do so. But
815
    // we mustn't try to solve until reasonable values have been supplied
816
    // for these new parameters, or else we'll get a numerical blowup.
817
    r.Generate(&SK.entity, &SK.param);
818
    SS.MarkGroupDirty(r.group);
819
    return r.h;
820
}
821

822
Vector GraphicsWindow::SnapToEntityByScreenPoint(Point2d pp, hEntity he) {
823
    Entity *e = SK.GetEntity(he);
824
    if(e->IsPoint()) return e->PointGetNum();
825
    SEdgeList *edges = e->GetOrGenerateEdges();
826

827
    double minD = -1.0f;
828
    double k = 0.0;
829
    const SEdge *edge = NULL;
830
    for(const auto &e : edges->l) {
831
        Point2d p0 = ProjectPoint(e.a);
832
        Point2d p1 = ProjectPoint(e.b);
833
        Point2d dir = p1.Minus(p0);
834
        double d = pp.DistanceToLine(p0, dir, /*asSegment=*/true);
835
        if(minD > 0.0 && d > minD) continue;
836
        minD = d;
837
        k = pp.Minus(p0).Dot(dir) / dir.Dot(dir);
838
        edge = &e;
839
    }
840
    if(edge == NULL) return UnProjectPoint(pp);
841
    return edge->a.Plus(edge->b.Minus(edge->a).ScaledBy(k));
842
}
843

844
bool GraphicsWindow::ConstrainPointByHovered(hEntity pt, const Point2d *projected) {
845
    if(!hover.entity.v) return false;
846

847
    Entity *point = SK.GetEntity(pt);
848
    Entity *e = SK.GetEntity(hover.entity);
849
    if(e->IsPoint()) {
850
        point->PointForceTo(e->PointGetNum());
851
        Constraint::ConstrainCoincident(e->h, pt);
852
        return true;
853
    }
854
    if(e->IsCircle()) {
855
        if(projected != NULL) {
856
            Vector snapPos = SnapToEntityByScreenPoint(*projected, e->h);
857
            point->PointForceTo(snapPos);
858
        }
859
        Constraint::Constrain(Constraint::Type::PT_ON_CIRCLE,
860
            pt, Entity::NO_ENTITY, e->h);
861
        return true;
862
    }
863
    if(e->type == Entity::Type::LINE_SEGMENT) {
864
        if(projected != NULL) {
865
            Vector snapPos = SnapToEntityByScreenPoint(*projected, e->h);
866
            point->PointForceTo(snapPos);
867
        }
868
        Constraint::Constrain(Constraint::Type::PT_ON_LINE,
869
            pt, Entity::NO_ENTITY, e->h);
870
        return true;
871
    }
872

873
    return false;
874
}
875

876
bool GraphicsWindow::MouseEvent(Platform::MouseEvent event) {
877
    using Platform::MouseEvent;
878
    double width, height;
879
    window->GetContentSize(&width, &height);
880

881
    event.x = event.x - width / 2;
882
    event.y = height / 2 - event.y;
883

884
    switch(event.type) {
885
        case MouseEvent::Type::MOTION:
886
            this->MouseMoved(event.x, event.y,
887
                             event.button == MouseEvent::Button::LEFT,
888
                             event.button == MouseEvent::Button::MIDDLE,
889
                             event.button == MouseEvent::Button::RIGHT,
890
                             event.shiftDown,
891
                             event.controlDown);
892
            break;
893

894
        case MouseEvent::Type::PRESS:
895
            if(event.button == MouseEvent::Button::LEFT) {
896
                this->MouseLeftDown(event.x, event.y, event.shiftDown, event.controlDown);
897
            } else if(event.button == MouseEvent::Button::MIDDLE ||
898
                      event.button == MouseEvent::Button::RIGHT) {
899
                this->MouseMiddleOrRightDown(event.x, event.y);
900
            }
901
            break;
902

903
        case MouseEvent::Type::DBL_PRESS:
904
            if(event.button == MouseEvent::Button::LEFT) {
905
                this->MouseLeftDoubleClick(event.x, event.y);
906
            }
907
            break;
908

909
        case MouseEvent::Type::RELEASE:
910
            if(event.button == MouseEvent::Button::LEFT) {
911
                this->MouseLeftUp(event.x, event.y, event.shiftDown, event.controlDown);
912
            } else if(event.button == MouseEvent::Button::RIGHT) {
913
                this->MouseRightUp(event.x, event.y);
914
            }
915
            break;
916

917
        case MouseEvent::Type::SCROLL_VERT:
918
            this->MouseScroll(event.shiftDown ? event.scrollDelta / 10 : event.scrollDelta);
919
            break;
920

921
        case MouseEvent::Type::LEAVE:
922
            this->MouseLeave();
923
            break;
924
    }
925

926
    return true;
927
}
928

929
void GraphicsWindow::MouseLeftDown(double mx, double my, bool shiftDown, bool ctrlDown) {
930
    orig.mouseDown = true;
931

932
    if(window->IsEditorVisible()) {
933
        orig.mouse = Point2d::From(mx, my);
934
        orig.mouseOnButtonDown = orig.mouse;
935
        window->HideEditor();
936
        return;
937
    }
938
    SS.TW.HideEditControl();
939

940
    if(SS.showToolbar) {
941
        if(ToolbarMouseDown((int)mx, (int)my)) return;
942
    }
943

944
    // This will be clobbered by MouseMoved below.
945
    bool hasConstraintSuggestion = pending.hasSuggestion;
946
    Constraint::Type constraintSuggestion = pending.suggestion;
947

948
    // Make sure the hover is up to date.
949
    MouseMoved(mx, my, /*leftDown=*/false, /*middleDown=*/false, /*rightDown=*/false,
950
        /*shiftDown=*/false, /*ctrlDown=*/false);
951
    orig.mouse.x = mx;
952
    orig.mouse.y = my;
953
    orig.mouseOnButtonDown = orig.mouse;
954
    Point2d mouse = Point2d::From(mx, my);
955

956
    // The current mouse location
957
    Vector v = offset.ScaledBy(-1);
958
    v = v.Plus(projRight.ScaledBy(mx/scale));
959
    v = v.Plus(projUp.ScaledBy(my/scale));
960

961
    hRequest hr = {};
962
    hConstraint hc = {};
963
    switch(pending.operation) {
964
        case Pending::COMMAND:
965
            switch(pending.command) {
966
                case Command::DATUM_POINT:
967
                    hr = AddRequest(Request::Type::DATUM_POINT);
968
                    SK.GetEntity(hr.entity(0))->PointForceTo(v);
969
                    ConstrainPointByHovered(hr.entity(0), &mouse);
970

971
                    ClearSuper();
972
                    break;
973

974
                case Command::LINE_SEGMENT:
975
                case Command::CONSTR_SEGMENT:
976
                    hr = AddRequest(Request::Type::LINE_SEGMENT);
977
                    SK.GetRequest(hr)->construction = (pending.command == Command::CONSTR_SEGMENT);
978
                    SK.GetEntity(hr.entity(1))->PointForceTo(v);
979
                    ConstrainPointByHovered(hr.entity(1), &mouse);
980

981
                    ClearSuper();
982
                    AddToPending(hr);
983

984
                    pending.operation = Pending::DRAGGING_NEW_LINE_POINT;
985
                    pending.request = hr;
986
                    pending.point = hr.entity(2);
987
                    pending.description = _("click next point of line, or press Esc");
988
                    SK.GetEntity(pending.point)->PointForceTo(v);
989
                    break;
990

991
                case Command::RECTANGLE: {
992
                    if(!SS.GW.LockedInWorkplane()) {
993
                        Error(_("Can't draw rectangle in 3d; first, activate a workplane "
994
                                "with Sketch -> In Workplane."));
995
                        ClearSuper();
996
                        break;
997
                    }
998
                    hRequest lns[4];
999
                    int i;
1000
                    SS.UndoRemember();
1001
                    for(i = 0; i < 4; i++) {
1002
                        lns[i] = AddRequest(Request::Type::LINE_SEGMENT, /*rememberForUndo=*/false);
1003
                        AddToPending(lns[i]);
1004
                    }
1005
                    for(i = 0; i < 4; i++) {
1006
                        Constraint::ConstrainCoincident(
1007
                            lns[i].entity(1), lns[(i+1)%4].entity(2));
1008
                        SK.GetEntity(lns[i].entity(1))->PointForceTo(v);
1009
                        SK.GetEntity(lns[i].entity(2))->PointForceTo(v);
1010
                    }
1011
                    for(i = 0; i < 4; i++) {
1012
                        Constraint::Constrain(
1013
                            (i % 2) ? Constraint::Type::HORIZONTAL : Constraint::Type::VERTICAL,
1014
                            Entity::NO_ENTITY, Entity::NO_ENTITY,
1015
                            lns[i].entity(0));
1016
                    }
1017
                    if(ConstrainPointByHovered(lns[2].entity(1), &mouse)) {
1018
                        Vector pos = SK.GetEntity(lns[2].entity(1))->PointGetNum();
1019
                        for(i = 0; i < 4; i++) {
1020
                            SK.GetEntity(lns[i].entity(1))->PointForceTo(pos);
1021
                            SK.GetEntity(lns[i].entity(2))->PointForceTo(pos);
1022
                        }
1023
                    }
1024

1025
                    pending.operation = Pending::DRAGGING_NEW_POINT;
1026
                    pending.point = lns[1].entity(2);
1027
                    pending.description = _("click to place other corner of rectangle");
1028
                    hr = lns[0];
1029
                    break;
1030
                }
1031
                case Command::CIRCLE:
1032
                    hr = AddRequest(Request::Type::CIRCLE);
1033
                    // Centered where we clicked
1034
                    SK.GetEntity(hr.entity(1))->PointForceTo(v);
1035
                    // Normal to the screen
1036
                    SK.GetEntity(hr.entity(32))->NormalForceTo(
1037
                        Quaternion::From(SS.GW.projRight, SS.GW.projUp));
1038
                    // Initial radius zero
1039
                    SK.GetEntity(hr.entity(64))->DistanceForceTo(0);
1040

1041
                    ConstrainPointByHovered(hr.entity(1), &mouse);
1042

1043
                    ClearSuper();
1044
                    AddToPending(hr);
1045

1046
                    pending.operation = Pending::DRAGGING_NEW_RADIUS;
1047
                    pending.circle = hr.entity(0);
1048
                    pending.description = _("click to set radius");
1049
                    break;
1050

1051
                case Command::ARC: {
1052
                    if(!SS.GW.LockedInWorkplane()) {
1053
                        Error(_("Can't draw arc in 3d; first, activate a workplane "
1054
                                "with Sketch -> In Workplane."));
1055
                        ClearPending();
1056
                        break;
1057
                    }
1058
                    hr = AddRequest(Request::Type::ARC_OF_CIRCLE);
1059
                    // This fudge factor stops us from immediately failing to solve
1060
                    // because of the arc's implicit (equal radius) tangent.
1061
                    Vector adj = SS.GW.projRight.WithMagnitude(2/SS.GW.scale);
1062
                    SK.GetEntity(hr.entity(1))->PointForceTo(v.Minus(adj));
1063
                    SK.GetEntity(hr.entity(2))->PointForceTo(v);
1064
                    SK.GetEntity(hr.entity(3))->PointForceTo(v);
1065
                    ConstrainPointByHovered(hr.entity(2), &mouse);
1066

1067
                    ClearSuper();
1068
                    AddToPending(hr);
1069

1070
                    pending.operation = Pending::DRAGGING_NEW_ARC_POINT;
1071
                    pending.point = hr.entity(3);
1072
                    pending.description = _("click to place point");
1073
                    break;
1074
                }
1075
                case Command::CUBIC:
1076
                    hr = AddRequest(Request::Type::CUBIC);
1077
                    SK.GetEntity(hr.entity(1))->PointForceTo(v);
1078
                    SK.GetEntity(hr.entity(2))->PointForceTo(v);
1079
                    SK.GetEntity(hr.entity(3))->PointForceTo(v);
1080
                    SK.GetEntity(hr.entity(4))->PointForceTo(v);
1081
                    ConstrainPointByHovered(hr.entity(1), &mouse);
1082

1083
                    ClearSuper();
1084
                    AddToPending(hr);
1085

1086
                    pending.operation = Pending::DRAGGING_NEW_CUBIC_POINT;
1087
                    pending.point = hr.entity(4);
1088
                    pending.description = _("click next point of cubic, or press Esc");
1089
                    break;
1090

1091
                case Command::WORKPLANE:
1092
                    if(LockedInWorkplane()) {
1093
                        Error(_("Sketching in a workplane already; sketch in 3d before "
1094
                                "creating new workplane."));
1095
                        ClearSuper();
1096
                        break;
1097
                    }
1098
                    hr = AddRequest(Request::Type::WORKPLANE);
1099
                    SK.GetEntity(hr.entity(1))->PointForceTo(v);
1100
                    SK.GetEntity(hr.entity(32))->NormalForceTo(
1101
                        Quaternion::From(SS.GW.projRight, SS.GW.projUp));
1102
                    ConstrainPointByHovered(hr.entity(1), &mouse);
1103

1104
                    ClearSuper();
1105
                    break;
1106

1107
                case Command::TTF_TEXT: {
1108
                    if(!SS.GW.LockedInWorkplane()) {
1109
                        Error(_("Can't draw text in 3d; first, activate a workplane "
1110
                                "with Sketch -> In Workplane."));
1111
                        ClearSuper();
1112
                        break;
1113
                    }
1114
                    hr = AddRequest(Request::Type::TTF_TEXT);
1115
                    AddToPending(hr);
1116
                    Request *r = SK.GetRequest(hr);
1117
                    r->str = "Abc";
1118
                    r->font = Platform::embeddedFont;
1119

1120
                    for(int i = 1; i <= 4; i++) {
1121
                        SK.GetEntity(hr.entity(i))->PointForceTo(v);
1122
                    }
1123

1124
                    pending.operation = Pending::DRAGGING_NEW_POINT;
1125
                    pending.point = hr.entity(3);
1126
                    pending.description = _("click to place bottom right of text");
1127
                    break;
1128
                }
1129

1130
                case Command::IMAGE: {
1131
                    if(!SS.GW.LockedInWorkplane()) {
1132
                        Error(_("Can't draw image in 3d; first, activate a workplane "
1133
                                "with Sketch -> In Workplane."));
1134
                        ClearSuper();
1135
                        break;
1136
                    }
1137
                    hr = AddRequest(Request::Type::IMAGE);
1138
                    AddToPending(hr);
1139
                    Request *r = SK.GetRequest(hr);
1140
                    r->file = pending.filename;
1141
                    r->construction = true;
1142

1143
                    for(int i = 1; i <= 4; i++) {
1144
                        SK.GetEntity(hr.entity(i))->PointForceTo(v);
1145
                    }
1146

1147
                    pending.operation = Pending::DRAGGING_NEW_POINT;
1148
                    pending.point = hr.entity(3);
1149
                    pending.description = "click to place bottom right of image";
1150
                    break;
1151
                }
1152

1153
                case Command::COMMENT: {
1154
                    ClearSuper();
1155
                    Constraint c = {};
1156
                    c.group       = SS.GW.activeGroup;
1157
                    c.workplane   = SS.GW.ActiveWorkplane();
1158
                    c.type        = Constraint::Type::COMMENT;
1159
                    c.disp.offset = v;
1160
                    c.comment     = _("NEW COMMENT -- DOUBLE-CLICK TO EDIT");
1161
                    hc = Constraint::AddConstraint(&c);
1162
                    break;
1163
                }
1164
                default: ssassert(false, "Unexpected pending menu id");
1165
            }
1166
            break;
1167

1168
        case Pending::DRAGGING_RADIUS:
1169
            ClearPending();
1170
            break;
1171

1172
        case Pending::DRAGGING_NEW_POINT:
1173
        case Pending::DRAGGING_NEW_ARC_POINT:
1174
            ConstrainPointByHovered(pending.point, &mouse);
1175
            ClearPending();
1176
            break;
1177

1178
        case Pending::DRAGGING_NEW_CUBIC_POINT: {
1179
            hRequest hr = pending.point.request();
1180
            Request *r = SK.GetRequest(hr);
1181

1182
            if(hover.entity == hr.entity(1) && r->extraPoints >= 2) {
1183
                // They want the endpoints coincident, which means a periodic
1184
                // spline instead.
1185
                r->type = Request::Type::CUBIC_PERIODIC;
1186
                // Remove the off-curve control points, which are no longer
1187
                // needed here; so move [2,ep+1] down, skipping first pt.
1188
                int i;
1189
                for(i = 2; i <= r->extraPoints+1; i++) {
1190
                    SK.GetEntity(hr.entity((i-1)+1))->PointForceTo(
1191
                        SK.GetEntity(hr.entity(i+1))->PointGetNum());
1192
                }
1193
                // and move ep+3 down by two, skipping both
1194
                SK.GetEntity(hr.entity((r->extraPoints+1)+1))->PointForceTo(
1195
                  SK.GetEntity(hr.entity((r->extraPoints+3)+1))->PointGetNum());
1196
                r->extraPoints -= 2;
1197
                // And we're done.
1198
                SS.MarkGroupDirty(r->group);
1199
                ClearPending();
1200
                break;
1201
            }
1202

1203
            if(ConstrainPointByHovered(pending.point, &mouse)) {
1204
                ClearPending();
1205
                break;
1206
            }
1207

1208
            Entity e;
1209
            if(r->extraPoints >= (int)arraylen(e.point) - 4) {
1210
                ClearPending();
1211
                break;
1212
            }
1213

1214
            (SK.GetRequest(hr)->extraPoints)++;
1215
            SS.GenerateAll(SolveSpaceUI::Generate::REGEN);
1216

1217
            int ep = r->extraPoints;
1218
            Vector last = SK.GetEntity(hr.entity(3+ep))->PointGetNum();
1219

1220
            SK.GetEntity(hr.entity(2+ep))->PointForceTo(last);
1221
            SK.GetEntity(hr.entity(3+ep))->PointForceTo(v);
1222
            SK.GetEntity(hr.entity(4+ep))->PointForceTo(v);
1223
            pending.point = hr.entity(4+ep);
1224
            break;
1225
        }
1226

1227
        case Pending::DRAGGING_NEW_LINE_POINT: {
1228
            if(hover.entity.v) {
1229
                Entity *e = SK.GetEntity(hover.entity);
1230
                if(e->IsPoint()) {
1231
                    hRequest hrl = pending.point.request();
1232
                    Entity *sp = SK.GetEntity(hrl.entity(1));
1233
                    if(( e->PointGetNum()).Equals(
1234
                       (sp->PointGetNum())))
1235
                    {
1236
                        // If we constrained by the hovered point, then we
1237
                        // would create a zero-length line segment. That's
1238
                        // not good, so just stop drawing.
1239
                        ClearPending();
1240
                        break;
1241
                    }
1242
                }
1243
            }
1244

1245
            bool doneDragging = ConstrainPointByHovered(pending.point, &mouse);
1246

1247
            // Constrain the line segment horizontal or vertical if close enough
1248
            if(hasConstraintSuggestion) {
1249
                Constraint::TryConstrain(constraintSuggestion,
1250
                    Entity::NO_ENTITY, Entity::NO_ENTITY, pending.request.entity(0));
1251
            }
1252

1253
            if(doneDragging) {
1254
                ClearPending();
1255
                break;
1256
            }
1257

1258
            // Create a new line segment, so that we continue drawing.
1259
            hRequest hr = AddRequest(Request::Type::LINE_SEGMENT);
1260
            ReplacePending(pending.request, hr);
1261
            SK.GetRequest(hr)->construction = SK.GetRequest(pending.request)->construction;
1262
            // Displace the second point of the new line segment slightly,
1263
            // to avoid creating zero-length edge warnings.
1264
            SK.GetEntity(hr.entity(2))->PointForceTo(
1265
                v.Plus(projRight.ScaledBy(0.5/scale)));
1266

1267
            // Constrain the line segments to share an endpoint
1268
            Constraint::ConstrainCoincident(pending.point, hr.entity(1));
1269
            Vector pendingPos = SK.GetEntity(pending.point)->PointGetNum();
1270
            SK.GetEntity(hr.entity(1))->PointForceTo(pendingPos);
1271

1272
            // And drag an endpoint of the new line segment
1273
            pending.operation = Pending::DRAGGING_NEW_LINE_POINT;
1274
            pending.request = hr;
1275
            pending.point = hr.entity(2);
1276
            pending.description = _("click next point of line, or press Esc");
1277

1278
            break;
1279
        }
1280

1281
        case Pending::NONE:
1282
        default:
1283
            ClearPending();
1284
            if(!hover.IsEmpty()) {
1285
                if(!ctrlDown) {
1286
                    hoverWasSelectedOnMousedown = IsSelected(&hover);
1287
                    MakeSelected(&hover);
1288
                } else {
1289
                    MakeUnselected(&hover, /*coincidentPointTrick=*/true);
1290
                }
1291
            }
1292
            break;
1293
    }
1294

1295
    // Activate group with newly created request/constraint
1296
    Group *g = NULL;
1297
    if(hr.v != 0) {
1298
        g = SK.GetGroup(SK.GetRequest(hr)->group);
1299
    }
1300
    if(hc.v != 0) {
1301
        g = SK.GetGroup(SK.GetConstraint(hc)->group);
1302
    }
1303
    if(g != NULL) {
1304
        g->visible = true;
1305
    }
1306

1307
    SS.ScheduleShowTW();
1308
    Invalidate();
1309
}
1310

1311
void GraphicsWindow::MouseLeftUp(double mx, double my, bool shiftDown, bool ctrlDown) {
1312
    orig.mouseDown = false;
1313

1314
    switch(pending.operation) {
1315
        case Pending::DRAGGING_POINTS:
1316
        case Pending::DRAGGING_CONSTRAINT:
1317
        case Pending::DRAGGING_NORMAL:
1318
        case Pending::DRAGGING_RADIUS:
1319
            if(!hoverWasSelectedOnMousedown) {
1320
                // And then clear the selection again, since they
1321
                // probably didn't want that selected if they just
1322
                // were dragging it.
1323
                ClearSelection();
1324
            }
1325
            hoverWasSelectedOnMousedown = false;
1326
            SS.extraLine.draw = false;
1327
            ClearPending();
1328
            Invalidate();
1329
            break;
1330

1331
        case Pending::DRAGGING_MARQUEE:
1332
            SelectByMarquee();
1333
            ClearPending();
1334
            Invalidate();
1335
            break;
1336

1337
        case Pending::NONE:
1338
            if(hover.IsEmpty() && !ctrlDown) {
1339
                ClearSelection();
1340
            }
1341
            break;
1342

1343
        default:
1344
            break;  // do nothing
1345
    }
1346
}
1347

1348
void GraphicsWindow::EditConstraint(hConstraint constraint) {
1349
    constraintBeingEdited = constraint;
1350
    ClearSuper();
1351

1352
    Constraint *c = SK.GetConstraint(constraintBeingEdited);
1353
    if(!c->HasLabel()) {
1354
        // Not meaningful to edit a constraint without a dimension
1355
        return;
1356
    }
1357
    if(c->reference) {
1358
        // Not meaningful to edit a reference dimension
1359
        return;
1360
    }
1361

1362
    Vector p3 = c->GetLabelPos(GetCamera());
1363
    Point2d p2 = ProjectPoint(p3);
1364

1365
    std::string editValue;
1366
    std::string editPlaceholder;
1367
    switch(c->type) {
1368
        case Constraint::Type::COMMENT:
1369
            editValue = c->comment;
1370
            editPlaceholder = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
1371
            break;
1372

1373
        default: {
1374
            double value = fabs(c->valA);
1375

1376
            // If displayed as radius, also edit as radius.
1377
            if(c->type == Constraint::Type::DIAMETER && c->other)
1378
                value /= 2;
1379

1380
            // Try showing value with default number of digits after decimal first.
1381
            if(c->type == Constraint::Type::LENGTH_RATIO || c->type == Constraint::Type::ARC_ARC_LEN_RATIO || c->type == Constraint::Type::ARC_LINE_LEN_RATIO) {
1382
                editValue = ssprintf("%.3f", value);
1383
            } else if(c->type == Constraint::Type::ANGLE) {
1384
                editValue = SS.DegreeToString(value);
1385
            } else {
1386
                editValue = SS.MmToString(value, true);
1387
                value /= SS.MmPerUnit();
1388
            }
1389
            // If that's not enough to represent it exactly, show the value with as many
1390
            // digits after decimal as required, up to 10.
1391
            int digits = 0;
1392
            while(fabs(std::stod(editValue) - value) > 1e-10) {
1393
                editValue = ssprintf("%.*f", digits, value);
1394
                digits++;
1395
            }
1396
            editPlaceholder = "10.000000";
1397
            break;
1398
        }
1399
    }
1400

1401
    double width, height;
1402
    window->GetContentSize(&width, &height);
1403
    hStyle hs = c->disp.style;
1404
    if(hs.v == 0) hs.v = Style::CONSTRAINT;
1405
    double capHeight = Style::TextHeight(hs);
1406
    double fontHeight = VectorFont::Builtin()->GetHeight(capHeight);
1407
    double editMinWidth = VectorFont::Builtin()->GetWidth(capHeight, editPlaceholder);
1408
    window->ShowEditor(p2.x + width / 2, height / 2 - p2.y,
1409
                        fontHeight, editMinWidth,
1410
                        /*isMonospace=*/false, editValue);
1411
}
1412

1413
void GraphicsWindow::MouseLeftDoubleClick(double mx, double my) {
1414
    if(window->IsEditorVisible()) return;
1415
    SS.TW.HideEditControl();
1416

1417
    if(hover.constraint.v) {
1418
        EditConstraint(hover.constraint);
1419
    }
1420
}
1421

1422
void GraphicsWindow::EditControlDone(const std::string &s) {
1423
    window->HideEditor();
1424
    window->Invalidate();
1425

1426
    Constraint *c = SK.GetConstraint(constraintBeingEdited);
1427

1428
    if(c->type == Constraint::Type::COMMENT) {
1429
        SS.UndoRemember();
1430
        c->comment = s;
1431
        return;
1432
    }
1433

1434
    if(Expr *e = Expr::From(s, true)) {
1435
        SS.UndoRemember();
1436

1437
        switch(c->type) {
1438
            case Constraint::Type::PROJ_PT_DISTANCE:
1439
            case Constraint::Type::PT_LINE_DISTANCE:
1440
            case Constraint::Type::PT_FACE_DISTANCE:
1441
            case Constraint::Type::PT_PLANE_DISTANCE:
1442
            case Constraint::Type::LENGTH_DIFFERENCE:
1443
            case Constraint::Type::ARC_ARC_DIFFERENCE:
1444
            case Constraint::Type::ARC_LINE_DIFFERENCE: {
1445
                // The sign is not displayed to the user, but this is a signed
1446
                // distance internally. To flip the sign, the user enters a
1447
                // negative distance.
1448
                bool wasNeg = (c->valA < 0);
1449
                if(wasNeg) {
1450
                    c->valA = -SS.ExprToMm(e);
1451
                } else {
1452
                    c->valA = SS.ExprToMm(e);
1453
                }
1454
                break;
1455
            }
1456
            case Constraint::Type::ANGLE:
1457
            case Constraint::Type::LENGTH_RATIO:
1458
            case Constraint::Type::ARC_ARC_LEN_RATIO:
1459
            case Constraint::Type::ARC_LINE_LEN_RATIO:
1460
                // These don't get the units conversion for distance, and
1461
                // they're always positive
1462
                c->valA = fabs(e->Eval());
1463
                break;
1464

1465
            case Constraint::Type::DIAMETER:
1466
                c->valA = fabs(SS.ExprToMm(e));
1467

1468
                // If displayed and edited as radius, convert back
1469
                // to diameter
1470
                if(c->other)
1471
                    c->valA *= 2;
1472
                break;
1473

1474
            default:
1475
                // These are always positive, and they get the units conversion.
1476
                c->valA = fabs(SS.ExprToMm(e));
1477
                break;
1478
        }
1479
        SS.MarkGroupDirty(c->group);
1480
    }
1481
}
1482

1483
void GraphicsWindow::MouseScroll(double zoomMultiplyer) {
1484
    // To support smooth scrolling where scroll wheel events come in increments
1485
    // smaller (or larger) than 1 we do:
1486
    //     scale *= exp(ln(1.2) * zoomMultiplyer);
1487
    // to ensure that the same total scroll delta always results in the same
1488
    // total zoom irrespective of in how many increments the zoom was applied.
1489
    // For example if we scroll a total delta of a+b in two events vs. one then
1490
    //     scale * e^a * e^b == scale * e^(a+b)
1491
    // while
1492
    //     scale * a * b != scale * (a+b)
1493
    // So this constant is ln(1.2) = 0.1823216 to make the default zoom 1.2x
1494
    ZoomToMouse(zoomMultiplyer);
1495
}
1496

1497
void GraphicsWindow::MouseLeave() {
1498
    // Un-hover everything when the mouse leaves our window, unless there's
1499
    // currently a context menu shown.
1500
    if(!context.active) {
1501
        hover.Clear();
1502
        toolbarHovered = Command::NONE;
1503
        Invalidate();
1504
    }
1505
    SS.extraLine.draw = false;
1506
}
1507

1508
void GraphicsWindow::SixDofEvent(Platform::SixDofEvent event) {
1509
    if(event.type == Platform::SixDofEvent::Type::RELEASE) {
1510
        ZoomToFit(/*includingInvisibles=*/false, /*useSelection=*/true);
1511
        Invalidate();
1512
        return;
1513
    }
1514

1515
    if(!havePainted) return;
1516
    Vector out = projRight.Cross(projUp);
1517

1518
    // rotation vector is axis of rotation, and its magnitude is angle
1519
    Vector aa = Vector::From(event.rotationX, event.rotationY, event.rotationZ);
1520
    // but it's given with respect to screen projection frame
1521
    aa = aa.ScaleOutOfCsys(projRight, projUp, out);
1522
    double aam = aa.Magnitude();
1523
    if(aam > 0.0) aa = aa.WithMagnitude(1);
1524

1525
    // This can either transform our view, or transform a linked part.
1526
    GroupSelection();
1527
    Entity *e = NULL;
1528
    Group *g = NULL;
1529
    if(gs.points == 1   && gs.n == 1) e = SK.GetEntity(gs.point [0]);
1530
    if(gs.entities == 1 && gs.n == 1) e = SK.GetEntity(gs.entity[0]);
1531
    if(e) g = SK.GetGroup(e->group);
1532
    if(g && g->type == Group::Type::LINKED && !event.shiftDown) {
1533
        // Apply the transformation to a linked part. Gain down the Z
1534
        // axis, since it's hard to see what you're doing on that one since
1535
        // it's normal to the screen.
1536
        Vector t = projRight.ScaledBy(event.translationX/scale).Plus(
1537
                   projUp   .ScaledBy(event.translationY/scale).Plus(
1538
                   out      .ScaledBy(0.1*event.translationZ/scale)));
1539
        Quaternion q = Quaternion::From(aa, aam);
1540

1541
        // If we go five seconds without SpaceNavigator input, or if we've
1542
        // switched groups, then consider that a new action and save an undo
1543
        // point.
1544
        int64_t now = GetMilliseconds();
1545
        if(now - last6DofTime > 5000 ||
1546
           last6DofGroup != g->h)
1547
        {
1548
            SS.UndoRemember();
1549
        }
1550

1551
        g->TransformImportedBy(t, q);
1552

1553
        last6DofTime = now;
1554
        last6DofGroup = g->h;
1555
        SS.MarkGroupDirty(g->h);
1556
    } else {
1557
        // Apply the transformation to the view of the everything. The
1558
        // x and y components are translation; but z component is scale,
1559
        // not translation, or else it would do nothing in a parallel
1560
        // projection
1561
        offset = offset.Plus(projRight.ScaledBy(event.translationX/scale));
1562
        offset = offset.Plus(projUp.ScaledBy(event.translationY/scale));
1563
        scale *= exp(0.001*event.translationZ);
1564

1565
        if(aam > 0.0) {
1566
            projRight = projRight.RotatedAbout(aa, -aam);
1567
            projUp    = projUp.   RotatedAbout(aa, -aam);
1568
            NormalizeProjectionVectors();
1569
        }
1570
    }
1571

1572
    havePainted = false;
1573
    Invalidate();
1574
}
1575

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

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

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

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