solvespace/src/mouse.cpp
dustinhartlyn fc16cdb370
Fix for zoom in/out error (#1221)
* minor fix open/save dialogue on windows 

On windows 10 the open/save dialogue box has an minor error, and I believe I fixed it. 

When "Open" is selected from the menu, the title of the dialogue box says "SolveSpace - Save File" and the entered file name is "united". My fix correctly titles the dialoged box, and leaves the address bar blank when a file is being opened because "united" is only needed as a default name when a file being saved. 

I found that class FileDialogImplWin32 from guiwin.cpp contains two if statements for "isSaveDialog". This is redundant. I removed the first where the title was originally set, but not working. I then set the title in the second if statement and moved the 'if isEmpty'' to this section.

* Update guiwin.cpp

replaced tabs with spaces

* Created ZoomToMouse function in graphicswin.cpp which referances the mouse position directly. Simplified MouseScroll in mouse.cpp to point to this function instead of altering zoom directly. Also pointed zoom commpand from keyboard and menu to ZoomToMouse so that it works avoids different behavior.

* clean up some comments
2022-02-28 14:22:35 -05:00

1573 lines
59 KiB
C++

//-----------------------------------------------------------------------------
// Anything relating to mouse, keyboard, or 6-DOF mouse input.
//
// Copyright 2008-2013 Jonathan Westhues.
//-----------------------------------------------------------------------------
#include "solvespace.h"
void GraphicsWindow::UpdateDraggedPoint(hEntity hp, double mx, double my) {
Entity *p = SK.GetEntity(hp);
Vector pos = p->PointGetNum();
UpdateDraggedNum(&pos, mx, my);
p->PointForceTo(pos);
SS.ScheduleShowTW();
}
void GraphicsWindow::UpdateDraggedNum(Vector *pos, double mx, double my) {
*pos = pos->Plus(projRight.ScaledBy((mx - orig.mouse.x)/scale));
*pos = pos->Plus(projUp.ScaledBy((my - orig.mouse.y)/scale));
}
void GraphicsWindow::AddPointToDraggedList(hEntity hp) {
Entity *p = SK.GetEntity(hp);
// If an entity and its points are both selected, then its points could
// end up in the list twice. This would be bad, because it would move
// twice as far as the mouse pointer...
List<hEntity> *lhe = &(pending.points);
for(hEntity *hee = lhe->First(); hee; hee = lhe->NextAfter(hee)) {
if(*hee == hp) {
// Exact same point.
return;
}
Entity *pe = SK.GetEntity(*hee);
if(pe->type == p->type &&
pe->type != Entity::Type::POINT_IN_2D &&
pe->type != Entity::Type::POINT_IN_3D &&
pe->group == p->group)
{
// Transform-type point, from the same group. So it handles the
// same unknowns.
return;
}
}
pending.points.Add(&hp);
}
void GraphicsWindow::StartDraggingByEntity(hEntity he) {
Entity *e = SK.GetEntity(he);
if(e->IsPoint()) {
AddPointToDraggedList(e->h);
} else if(e->type == Entity::Type::LINE_SEGMENT ||
e->type == Entity::Type::ARC_OF_CIRCLE ||
e->type == Entity::Type::CUBIC ||
e->type == Entity::Type::CUBIC_PERIODIC ||
e->type == Entity::Type::CIRCLE ||
e->type == Entity::Type::TTF_TEXT ||
e->type == Entity::Type::IMAGE)
{
int pts;
EntReqTable::GetEntityInfo(e->type, e->extraPoints,
NULL, &pts, NULL, NULL);
for(int i = 0; i < pts; i++) {
AddPointToDraggedList(e->point[i]);
}
}
}
void GraphicsWindow::StartDraggingBySelection() {
List<Selection> *ls = &(selection);
for(Selection *s = ls->First(); s; s = ls->NextAfter(s)) {
if(!s->entity.v) continue;
StartDraggingByEntity(s->entity);
}
// The user might select a point, and then click it again to start
// dragging; but the point just got unselected by that click. So drag
// the hovered item too, and they'll always have it.
if(hover.entity.v) {
hEntity dragEntity = ChooseFromHoverToDrag().entity;
if(dragEntity != Entity::NO_ENTITY) {
StartDraggingByEntity(dragEntity);
}
}
}
void GraphicsWindow::MouseMoved(double x, double y, bool leftDown,
bool middleDown, bool rightDown, bool shiftDown, bool ctrlDown)
{
if(window->IsEditorVisible()) return;
if(context.active) return;
SS.extraLine.draw = false;
if(!orig.mouseDown) {
// If someone drags the mouse into our window with the left button
// already depressed, then we don't have our starting point; so
// don't try.
leftDown = false;
}
if(rightDown) {
middleDown = true;
shiftDown = !shiftDown;
}
// Not passing right-button and middle-button drags to the toolbar avoids
// some cosmetic issues with trackpad pans/rotates implemented with
// simulated right-button drag events causing spurious hover events.
if(SS.showToolbar && !middleDown) {
if(ToolbarMouseMoved((int)x, (int)y)) {
hover.Clear();
return;
}
}
if(!leftDown && (pending.operation == Pending::DRAGGING_POINTS ||
pending.operation == Pending::DRAGGING_MARQUEE))
{
ClearPending();
Invalidate();
}
Point2d mp = Point2d::From(x, y);
currentMousePosition = mp;
if(rightDown && orig.mouse.DistanceTo(mp) < 5 && !orig.startedMoving) {
// Avoid accidentally panning (or rotating if shift is down) if the
// user wants a context menu.
return;
}
orig.startedMoving = true;
// If the middle button is down, then mouse movement is used to pan and
// rotate our view. This wins over everything else.
if(middleDown) {
hover.Clear();
double dx = (x - orig.mouse.x) / scale;
double dy = (y - orig.mouse.y) / scale;
if(!(shiftDown || ctrlDown)) {
double s = 0.3*(PI/180)*scale; // degrees per pixel
if(SS.turntableNav) { // lock the Z to vertical
projRight = orig.projRight.RotatedAbout(Vector::From(0, 0, 1), -s * dx);
projUp = orig.projUp.RotatedAbout(
Vector::From(orig.projRight.x, orig.projRight.y, orig.projRight.y), s * dy);
} else {
projRight = orig.projRight.RotatedAbout(orig.projUp, -s * dx);
projUp = orig.projUp.RotatedAbout(orig.projRight, s * dy);
}
NormalizeProjectionVectors();
} else if(ctrlDown) {
double theta = atan2(orig.mouse.y, orig.mouse.x);
theta -= atan2(y, x);
SS.extraLine.draw = true;
SS.extraLine.ptA = UnProjectPoint(Point2d::From(0, 0));
SS.extraLine.ptB = UnProjectPoint(mp);
Vector normal = orig.projRight.Cross(orig.projUp);
projRight = orig.projRight.RotatedAbout(normal, theta);
projUp = orig.projUp.RotatedAbout(normal, theta);
NormalizeProjectionVectors();
} else {
offset.x = orig.offset.x + dx*projRight.x + dy*projUp.x;
offset.y = orig.offset.y + dx*projRight.y + dy*projUp.y;
offset.z = orig.offset.z + dx*projRight.z + dy*projUp.z;
}
orig.projRight = projRight;
orig.projUp = projUp;
orig.offset = offset;
orig.mouse.x = x;
orig.mouse.y = y;
if(SS.TW.shown.screen == TextWindow::Screen::EDIT_VIEW) {
if(havePainted) {
SS.ScheduleShowTW();
}
}
Invalidate();
havePainted = false;
return;
}
if(pending.operation == Pending::NONE) {
double dm = orig.mouse.DistanceTo(mp);
// If we're currently not doing anything, then see if we should
// start dragging something.
if(leftDown && dm > 3) {
Entity *e = NULL;
hEntity dragEntity = ChooseFromHoverToDrag().entity;
if(dragEntity.v) e = SK.GetEntity(dragEntity);
if(e && e->type != Entity::Type::WORKPLANE) {
if(!hoverWasSelectedOnMousedown) {
// The user clicked an unselected entity, which
// means they're dragging just the hovered thing,
// not the full selection. So clear all the selection
// except that entity.
ClearSelection();
MakeSelected(dragEntity);
}
if(e->type == Entity::Type::CIRCLE && selection.n <= 1) {
// Drag the radius.
pending.circle = dragEntity;
pending.operation = Pending::DRAGGING_RADIUS;
} else if(e->IsNormal()) {
pending.normal = dragEntity;
pending.operation = Pending::DRAGGING_NORMAL;
} else {
StartDraggingBySelection();
hover.Clear();
pending.operation = Pending::DRAGGING_POINTS;
}
} else if(hover.constraint.v &&
SK.GetConstraint(hover.constraint)->HasLabel())
{
ClearSelection();
pending.constraint = hover.constraint;
pending.operation = Pending::DRAGGING_CONSTRAINT;
}
if(pending.operation != Pending::NONE) {
// We just started a drag, so remember for the undo before
// the drag changes anything.
SS.UndoRemember();
} else {
if(!hover.constraint.v) {
// That's just marquee selection, which should not cause
// an undo remember.
if(dm > 10) {
if(hover.entity.v) {
// Avoid accidentally selecting workplanes when
// starting drags.
MakeUnselected(hover.entity, /*coincidentPointTrick=*/false);
hover.Clear();
}
pending.operation = Pending::DRAGGING_MARQUEE;
orig.marqueePoint =
UnProjectPoint(orig.mouseOnButtonDown);
}
}
}
} else {
// Otherwise, just hit test and give up; but don't hit test
// if the mouse is down, because then the user could hover
// a point, mouse down (thus selecting it), and drag, in an
// effort to drag the point, but instead hover a different
// entity before we move far enough to start the drag.
if(!leftDown) {
// Hit testing can potentially take a lot of time.
// If we haven't painted since last time we highlighted
// something, don't hit test again, since this just causes
// a lag.
if(!havePainted) return;
HitTestMakeSelection(mp);
}
}
return;
}
// If the user has started an operation from the menu, but not
// completed it, then just do the selection.
if(pending.operation == Pending::COMMAND) {
HitTestMakeSelection(mp);
return;
}
if(pending.operation == Pending::DRAGGING_POINTS && ctrlDown) {
SS.extraLine.ptA = UnProjectPoint(orig.mouseOnButtonDown);
SS.extraLine.ptB = UnProjectPoint(mp);
SS.extraLine.draw = true;
}
// We're currently dragging something; so do that. But if we haven't
// painted since the last time we solved, do nothing, because there's
// no sense solving a frame and not displaying it.
if(!havePainted) {
return;
}
havePainted = false;
switch(pending.operation) {
case Pending::DRAGGING_CONSTRAINT: {
Constraint *c = SK.constraint.FindById(pending.constraint);
UpdateDraggedNum(&(c->disp.offset), x, y);
orig.mouse = mp;
Invalidate();
return;
}
case Pending::DRAGGING_NEW_LINE_POINT:
if(!ctrlDown) {
SS.GW.pending.hasSuggestion =
SS.GW.SuggestLineConstraint(SS.GW.pending.request, &SS.GW.pending.suggestion);
} else {
SS.GW.pending.hasSuggestion = false;
}
// fallthrough
case Pending::DRAGGING_NEW_POINT:
UpdateDraggedPoint(pending.point, x, y);
HitTestMakeSelection(mp);
SS.MarkGroupDirtyByEntity(pending.point);
orig.mouse = mp;
break;
case Pending::DRAGGING_POINTS:
if(shiftDown || ctrlDown) {
// Edit the rotation associated with a POINT_N_ROT_TRANS,
// either within (ctrlDown) or out of (shiftDown) the plane
// of the screen. So first get the rotation to apply, in qt.
Quaternion qt;
if(ctrlDown) {
double d = mp.DistanceTo(orig.mouseOnButtonDown);
if(d < 25) {
// Don't start dragging the position about the normal
// until we're a little ways out, to get a reasonable
// reference pos
qt = Quaternion::IDENTITY;
} else {
double theta = atan2(orig.mouse.y-orig.mouseOnButtonDown.y,
orig.mouse.x-orig.mouseOnButtonDown.x);
theta -= atan2(y-orig.mouseOnButtonDown.y,
x-orig.mouseOnButtonDown.x);
Vector gn = projRight.Cross(projUp);
qt = Quaternion::From(gn, -theta);
}
} else {
double dx = -(x - orig.mouse.x);
double dy = -(y - orig.mouse.y);
double s = 0.3*(PI/180); // degrees per pixel
qt = Quaternion::From(projUp, -s*dx).Times(
Quaternion::From(projRight, s*dy));
}
// Now apply this rotation to the points being dragged.
List<hEntity> *lhe = &(pending.points);
for(hEntity *he = lhe->First(); he; he = lhe->NextAfter(he)) {
Entity *e = SK.GetEntity(*he);
if(e->type != Entity::Type::POINT_N_ROT_TRANS) {
if(ctrlDown) {
Vector p = e->PointGetNum();
p = p.Minus(SS.extraLine.ptA);
p = qt.Rotate(p);
p = p.Plus(SS.extraLine.ptA);
e->PointForceTo(p);
} else {
UpdateDraggedPoint(*he, x, y);
}
} else {
Quaternion q = e->PointGetQuaternion();
Vector p = e->PointGetNum();
q = qt.Times(q);
e->PointForceQuaternionTo(q);
// Let's rotate about the selected point; so fix up the
// translation so that that point didn't move.
e->PointForceTo(p);
}
SS.MarkGroupDirtyByEntity(e->h);
}
} else {
List<hEntity> *lhe = &(pending.points);
for(hEntity *he = lhe->First(); he; he = lhe->NextAfter(he)) {
UpdateDraggedPoint(*he, x, y);
SS.MarkGroupDirtyByEntity(*he);
}
}
orig.mouse = mp;
break;
case Pending::DRAGGING_NEW_CUBIC_POINT: {
UpdateDraggedPoint(pending.point, x, y);
HitTestMakeSelection(mp);
hRequest hr = pending.point.request();
if(pending.point == hr.entity(4)) {
// The very first segment; dragging final point drags both
// tangent points.
Vector p0 = SK.GetEntity(hr.entity(1))->PointGetNum(),
p3 = SK.GetEntity(hr.entity(4))->PointGetNum(),
p1 = p0.ScaledBy(2.0/3).Plus(p3.ScaledBy(1.0/3)),
p2 = p0.ScaledBy(1.0/3).Plus(p3.ScaledBy(2.0/3));
SK.GetEntity(hr.entity(1+1))->PointForceTo(p1);
SK.GetEntity(hr.entity(1+2))->PointForceTo(p2);
} else {
// A subsequent segment; dragging point drags only final
// tangent point.
int i = SK.GetEntity(hr.entity(0))->extraPoints;
Vector pn = SK.GetEntity(hr.entity(4+i))->PointGetNum(),
pnm2 = SK.GetEntity(hr.entity(2+i))->PointGetNum(),
pnm1 = (pn.Plus(pnm2)).ScaledBy(0.5);
SK.GetEntity(hr.entity(3+i))->PointForceTo(pnm1);
}
orig.mouse = mp;
SS.MarkGroupDirtyByEntity(pending.point);
break;
}
case Pending::DRAGGING_NEW_ARC_POINT: {
UpdateDraggedPoint(pending.point, x, y);
HitTestMakeSelection(mp);
hRequest hr = pending.point.request();
Vector ona = SK.GetEntity(hr.entity(2))->PointGetNum();
Vector onb = SK.GetEntity(hr.entity(3))->PointGetNum();
Vector center = (ona.Plus(onb)).ScaledBy(0.5);
SK.GetEntity(hr.entity(1))->PointForceTo(center);
orig.mouse = mp;
SS.MarkGroupDirtyByEntity(pending.point);
break;
}
case Pending::DRAGGING_NEW_RADIUS:
case Pending::DRAGGING_RADIUS: {
Entity *circle = SK.GetEntity(pending.circle);
Vector center = SK.GetEntity(circle->point[0])->PointGetNum();
Point2d c2 = ProjectPoint(center);
double r = c2.DistanceTo(mp)/scale;
SK.GetEntity(circle->distance)->DistanceForceTo(r);
SS.MarkGroupDirtyByEntity(pending.circle);
SS.ScheduleShowTW();
break;
}
case Pending::DRAGGING_NORMAL: {
Entity *normal = SK.GetEntity(pending.normal);
Vector p = SK.GetEntity(normal->point[0])->PointGetNum();
Point2d p2 = ProjectPoint(p);
Quaternion q = normal->NormalGetNum();
Vector u = q.RotationU(), v = q.RotationV();
if(ctrlDown) {
double theta = atan2(orig.mouse.y-p2.y, orig.mouse.x-p2.x);
theta -= atan2(y-p2.y, x-p2.x);
Vector normal = projRight.Cross(projUp);
u = u.RotatedAbout(normal, -theta);
v = v.RotatedAbout(normal, -theta);
} else {
double dx = -(x - orig.mouse.x);
double dy = -(y - orig.mouse.y);
double s = 0.3*(PI/180); // degrees per pixel
u = u.RotatedAbout(projUp, -s*dx);
u = u.RotatedAbout(projRight, s*dy);
v = v.RotatedAbout(projUp, -s*dx);
v = v.RotatedAbout(projRight, s*dy);
}
orig.mouse = mp;
normal->NormalForceTo(Quaternion::From(u, v));
SS.MarkGroupDirtyByEntity(pending.normal);
break;
}
case Pending::DRAGGING_MARQUEE:
orig.mouse = mp;
Invalidate();
return;
case Pending::NONE:
case Pending::COMMAND:
ssassert(false, "Unexpected pending operation");
}
}
void GraphicsWindow::ClearPending(bool scheduleShowTW) {
pending.points.Clear();
pending.requests.Clear();
pending = {};
if(scheduleShowTW) {
SS.ScheduleShowTW();
}
}
bool GraphicsWindow::IsFromPending(hRequest r) {
for(auto &req : pending.requests) {
if(req == r) return true;
}
return false;
}
void GraphicsWindow::AddToPending(hRequest r) {
pending.requests.Add(&r);
}
void GraphicsWindow::ReplacePending(hRequest before, hRequest after) {
for(auto &req : pending.requests) {
if(req == before) {
req = after;
}
}
}
void GraphicsWindow::MouseMiddleOrRightDown(double x, double y) {
if(window->IsEditorVisible()) return;
orig.offset = offset;
orig.projUp = projUp;
orig.projRight = projRight;
orig.mouse.x = x;
orig.mouse.y = y;
orig.startedMoving = false;
}
void GraphicsWindow::MouseRightUp(double x, double y) {
SS.extraLine.draw = false;
Invalidate();
// Don't show a context menu if the user is right-clicking the toolbar,
// or if they are finishing a pan.
if(ToolbarMouseMoved((int)x, (int)y)) return;
if(orig.startedMoving) return;
if(context.active) return;
if(pending.operation == Pending::DRAGGING_NEW_LINE_POINT && pending.hasSuggestion) {
Constraint::TryConstrain(SS.GW.pending.suggestion,
Entity::NO_ENTITY, Entity::NO_ENTITY, pending.request.entity(0));
}
if(pending.operation == Pending::DRAGGING_NEW_LINE_POINT ||
pending.operation == Pending::DRAGGING_NEW_CUBIC_POINT ||
pending.operation == Pending::DRAGGING_NEW_ARC_POINT ||
pending.operation == Pending::DRAGGING_NEW_RADIUS ||
pending.operation == Pending::DRAGGING_NEW_POINT
)
{
// Special case; use a right click to stop drawing lines, since
// a left click would draw another one. This is quicker and more
// intuitive than hitting escape. Likewise for other entities
// for consistency.
ClearPending();
return;
}
// The current mouse location
Vector v = offset.ScaledBy(-1);
v = v.Plus(projRight.ScaledBy(x/scale));
v = v.Plus(projUp.ScaledBy(y/scale));
Platform::MenuRef menu = Platform::CreateMenu();
context.active = true;
if(!hover.IsEmpty()) {
MakeSelected(&hover);
SS.ScheduleShowTW();
}
GroupSelection();
bool itemsSelected = (gs.n > 0 || gs.constraints > 0);
if(itemsSelected) {
if(gs.stylables > 0) {
Platform::MenuRef styleMenu = menu->AddSubMenu(_("Assign to Style"));
bool empty = true;
for(const Style &s : SK.style) {
if(s.h.v < Style::FIRST_CUSTOM) continue;
uint32_t v = s.h.v;
styleMenu->AddItem(s.DescriptionString(), [v]() {
Style::AssignSelectionToStyle(v);
});
empty = false;
}
if(!empty) styleMenu->AddSeparator();
styleMenu->AddItem(_("No Style"), []() {
Style::AssignSelectionToStyle(0);
});
styleMenu->AddItem(_("Newly Created Custom Style..."), [this]() {
uint32_t vs = Style::CreateCustomStyle();
Style::AssignSelectionToStyle(vs);
ForceTextWindowShown();
});
}
if(gs.n + gs.constraints == 1) {
menu->AddItem(_("Group Info"), [this]() {
hGroup hg;
if(gs.entities == 1) {
hg = SK.GetEntity(gs.entity[0])->group;
} else if(gs.points == 1) {
hg = SK.GetEntity(gs.point[0])->group;
} else if(gs.constraints == 1) {
hg = SK.GetConstraint(gs.constraint[0])->group;
} else {
return;
}
ClearSelection();
SS.TW.GoToScreen(TextWindow::Screen::GROUP_INFO);
SS.TW.shown.group = hg;
SS.ScheduleShowTW();
ForceTextWindowShown();
});
}
if(gs.n + gs.constraints == 1 && gs.stylables == 1) {
menu->AddItem(_("Style Info"), [this]() {
hStyle hs;
if(gs.entities == 1) {
hs = Style::ForEntity(gs.entity[0]);
} else if(gs.points == 1) {
hs = Style::ForEntity(gs.point[0]);
} else if(gs.constraints == 1) {
hs = SK.GetConstraint(gs.constraint[0])->GetStyle();
} else {
return;
}
ClearSelection();
SS.TW.GoToScreen(TextWindow::Screen::STYLE_INFO);
SS.TW.shown.style = hs;
SS.ScheduleShowTW();
ForceTextWindowShown();
});
}
if(gs.withEndpoints > 0) {
menu->AddItem(_("Select Edge Chain"),
[]() { MenuEdit(Command::SELECT_CHAIN); });
}
if(gs.constraints == 1 && gs.n == 0) {
Constraint *c = SK.GetConstraint(gs.constraint[0]);
if(c->HasLabel() && c->type != Constraint::Type::COMMENT) {
menu->AddItem(_("Toggle Reference Dimension"),
[]() { Constraint::MenuConstrain(Command::REFERENCE); });
}
if(c->type == Constraint::Type::ANGLE ||
c->type == Constraint::Type::EQUAL_ANGLE)
{
menu->AddItem(_("Other Supplementary Angle"),
[]() { Constraint::MenuConstrain(Command::OTHER_ANGLE); });
}
}
if(gs.constraintLabels > 0 || gs.points > 0) {
menu->AddItem(_("Snap to Grid"),
[]() { MenuEdit(Command::SNAP_TO_GRID); });
}
if(gs.points == 1 && gs.point[0].isFromRequest()) {
Request *r = SK.GetRequest(gs.point[0].request());
int index = r->IndexOfPoint(gs.point[0]);
if((r->type == Request::Type::CUBIC && (index > 1 && index < r->extraPoints + 2)) ||
r->type == Request::Type::CUBIC_PERIODIC) {
menu->AddItem(_("Remove Spline Point"), [this, r]() {
int index = r->IndexOfPoint(gs.point[0]);
ssassert(r->extraPoints != 0,
"Expected a bezier with interior control points");
SS.UndoRemember();
Entity *e = SK.GetEntity(r->h.entity(0));
// First, fix point-coincident constraints involving this point.
// Then, remove all other constraints, since they would otherwise
// jump to an adjacent one and mess up the bezier after generation.
FixConstraintsForPointBeingDeleted(e->point[index]);
RemoveConstraintsForPointBeingDeleted(e->point[index]);
for(int i = index; i < MAX_POINTS_IN_ENTITY - 1; i++) {
if(e->point[i + 1].v == 0) break;
Entity *p0 = SK.GetEntity(e->point[i]);
Entity *p1 = SK.GetEntity(e->point[i + 1]);
ReplacePointInConstraints(p1->h, p0->h);
p0->PointForceTo(p1->PointGetNum());
}
r->extraPoints--;
SS.MarkGroupDirtyByEntity(gs.point[0]);
ClearSelection();
});
}
}
if(gs.entities == 1 && gs.entity[0].isFromRequest()) {
Request *r = SK.GetRequest(gs.entity[0].request());
if(r->type == Request::Type::CUBIC || r->type == Request::Type::CUBIC_PERIODIC) {
Entity *e = SK.GetEntity(gs.entity[0]);
int addAfterPoint = e->GetPositionOfPoint(GetCamera(), Point2d::From(x, y));
ssassert(addAfterPoint != -1, "Expected a nearest bezier point to be located");
// Skip derivative point.
if(r->type == Request::Type::CUBIC) addAfterPoint++;
menu->AddItem(_("Add Spline Point"), [this, r, addAfterPoint, v]() {
int pointCount = r->extraPoints +
((r->type == Request::Type::CUBIC_PERIODIC) ? 3 : 4);
if(pointCount >= MAX_POINTS_IN_ENTITY) {
Error(_("Cannot add spline point: maximum number of points reached."));
return;
}
SS.UndoRemember();
r->extraPoints++;
SS.MarkGroupDirtyByEntity(gs.entity[0]);
SS.GenerateAll(SolveSpaceUI::Generate::REGEN);
Entity *e = SK.GetEntity(r->h.entity(0));
for(int i = MAX_POINTS_IN_ENTITY; i > addAfterPoint + 1; i--) {
Entity *p0 = SK.entity.FindByIdNoOops(e->point[i]);
if(p0 == NULL) continue;
Entity *p1 = SK.GetEntity(e->point[i - 1]);
ReplacePointInConstraints(p1->h, p0->h);
p0->PointForceTo(p1->PointGetNum());
}
Entity *p = SK.GetEntity(e->point[addAfterPoint + 1]);
p->PointForceTo(v);
SS.MarkGroupDirtyByEntity(gs.entity[0]);
ClearSelection();
});
}
}
if(gs.entities == gs.n) {
menu->AddItem(_("Toggle Construction"),
[]() { MenuRequest(Command::CONSTRUCTION); });
}
if(gs.points == 1) {
Entity *p = SK.GetEntity(gs.point[0]);
Constraint *c = nullptr;
IdList<Constraint,hConstraint> *lc = &(SK.constraint);
for(Constraint &ci : *lc) {
if(ci.type != Constraint::Type::POINTS_COINCIDENT) continue;
if(ci.ptA == p->h || ci.ptB == p->h) {
c = &ci;
break;
}
}
if(c) {
menu->AddItem(_("Delete Point-Coincident Constraint"), [this, p]() {
if(!p->IsPoint()) return;
SS.UndoRemember();
SK.constraint.ClearTags();
for(Constraint &c : SK.constraint) {
if(c.type != Constraint::Type::POINTS_COINCIDENT) continue;
if(c.ptA == p->h || c.ptB == p->h) {
c.tag = 1;
}
}
SK.constraint.RemoveTagged();
ClearSelection();
});
}
}
menu->AddSeparator();
if(LockedInWorkplane()) {
menu->AddItem(_("Cut"),
[]() { MenuClipboard(Command::CUT); });
menu->AddItem(_("Copy"),
[]() { MenuClipboard(Command::COPY); });
}
} else {
menu->AddItem(_("Select All"),
[]() { MenuEdit(Command::SELECT_ALL); });
}
if((!SS.clipboard.r.IsEmpty() || !SS.clipboard.c.IsEmpty()) && LockedInWorkplane()) {
menu->AddItem(_("Paste"),
[]() { MenuClipboard(Command::PASTE); });
menu->AddItem(_("Paste Transformed..."),
[]() { MenuClipboard(Command::PASTE_TRANSFORM); });
}
if(itemsSelected) {
menu->AddItem(_("Delete"),
[]() { MenuClipboard(Command::DELETE); });
menu->AddSeparator();
menu->AddItem(_("Unselect All"),
[]() { MenuEdit(Command::UNSELECT_ALL); });
}
// If only one item is selected, then it must be the one that we just
// selected from the hovered item; in which case unselect all and hovered
// are equivalent.
if(!hover.IsEmpty() && selection.n > 1) {
menu->AddItem(_("Unselect Hovered"), [this] {
if(!hover.IsEmpty()) {
MakeUnselected(&hover, /*coincidentPointTrick=*/true);
}
});
}
if(itemsSelected) {
menu->AddSeparator();
menu->AddItem(_("Zoom to Fit"),
[]() { MenuView(Command::ZOOM_TO_FIT); });
}
menu->PopUp();
context.active = false;
SS.ScheduleShowTW();
}
hRequest GraphicsWindow::AddRequest(Request::Type type) {
return AddRequest(type, /*rememberForUndo=*/true);
}
hRequest GraphicsWindow::AddRequest(Request::Type type, bool rememberForUndo) {
if(rememberForUndo) SS.UndoRemember();
Request r = {};
r.group = activeGroup;
Group *g = SK.GetGroup(activeGroup);
if(g->type == Group::Type::DRAWING_3D || g->type == Group::Type::DRAWING_WORKPLANE) {
r.construction = false;
} else {
r.construction = true;
}
r.workplane = ActiveWorkplane();
r.type = type;
SK.request.AddAndAssignId(&r);
// We must regenerate the parameters, so that the code that tries to
// place this request's entities where the mouse is can do so. But
// we mustn't try to solve until reasonable values have been supplied
// for these new parameters, or else we'll get a numerical blowup.
r.Generate(&SK.entity, &SK.param);
SS.MarkGroupDirty(r.group);
return r.h;
}
Vector GraphicsWindow::SnapToEntityByScreenPoint(Point2d pp, hEntity he) {
Entity *e = SK.GetEntity(he);
if(e->IsPoint()) return e->PointGetNum();
SEdgeList *edges = e->GetOrGenerateEdges();
double minD = -1.0f;
double k;
const SEdge *edge = NULL;
for(const auto &e : edges->l) {
Point2d p0 = ProjectPoint(e.a);
Point2d p1 = ProjectPoint(e.b);
Point2d dir = p1.Minus(p0);
double d = pp.DistanceToLine(p0, dir, /*asSegment=*/true);
if(minD > 0.0 && d > minD) continue;
minD = d;
k = pp.Minus(p0).Dot(dir) / dir.Dot(dir);
edge = &e;
}
if(edge == NULL) return UnProjectPoint(pp);
return edge->a.Plus(edge->b.Minus(edge->a).ScaledBy(k));
}
bool GraphicsWindow::ConstrainPointByHovered(hEntity pt, const Point2d *projected) {
if(!hover.entity.v) return false;
Entity *point = SK.GetEntity(pt);
Entity *e = SK.GetEntity(hover.entity);
if(e->IsPoint()) {
point->PointForceTo(e->PointGetNum());
Constraint::ConstrainCoincident(e->h, pt);
return true;
}
if(e->IsCircle()) {
if(projected != NULL) {
Vector snapPos = SnapToEntityByScreenPoint(*projected, e->h);
point->PointForceTo(snapPos);
}
Constraint::Constrain(Constraint::Type::PT_ON_CIRCLE,
pt, Entity::NO_ENTITY, e->h);
return true;
}
if(e->type == Entity::Type::LINE_SEGMENT) {
if(projected != NULL) {
Vector snapPos = SnapToEntityByScreenPoint(*projected, e->h);
point->PointForceTo(snapPos);
}
Constraint::Constrain(Constraint::Type::PT_ON_LINE,
pt, Entity::NO_ENTITY, e->h);
return true;
}
return false;
}
bool GraphicsWindow::MouseEvent(Platform::MouseEvent event) {
using Platform::MouseEvent;
double width, height;
window->GetContentSize(&width, &height);
event.x = event.x - width / 2;
event.y = height / 2 - event.y;
switch(event.type) {
case MouseEvent::Type::MOTION:
this->MouseMoved(event.x, event.y,
event.button == MouseEvent::Button::LEFT,
event.button == MouseEvent::Button::MIDDLE,
event.button == MouseEvent::Button::RIGHT,
event.shiftDown,
event.controlDown);
break;
case MouseEvent::Type::PRESS:
if(event.button == MouseEvent::Button::LEFT) {
this->MouseLeftDown(event.x, event.y, event.shiftDown, event.controlDown);
} else if(event.button == MouseEvent::Button::MIDDLE ||
event.button == MouseEvent::Button::RIGHT) {
this->MouseMiddleOrRightDown(event.x, event.y);
}
break;
case MouseEvent::Type::DBL_PRESS:
if(event.button == MouseEvent::Button::LEFT) {
this->MouseLeftDoubleClick(event.x, event.y);
}
break;
case MouseEvent::Type::RELEASE:
if(event.button == MouseEvent::Button::LEFT) {
this->MouseLeftUp(event.x, event.y, event.shiftDown, event.controlDown);
} else if(event.button == MouseEvent::Button::RIGHT) {
this->MouseRightUp(event.x, event.y);
}
break;
case MouseEvent::Type::SCROLL_VERT:
this->MouseScroll(event.shiftDown ? event.scrollDelta / 10 : event.scrollDelta);
break;
case MouseEvent::Type::LEAVE:
this->MouseLeave();
break;
}
return true;
}
void GraphicsWindow::MouseLeftDown(double mx, double my, bool shiftDown, bool ctrlDown) {
orig.mouseDown = true;
if(window->IsEditorVisible()) {
orig.mouse = Point2d::From(mx, my);
orig.mouseOnButtonDown = orig.mouse;
window->HideEditor();
return;
}
SS.TW.HideEditControl();
if(SS.showToolbar) {
if(ToolbarMouseDown((int)mx, (int)my)) return;
}
// This will be clobbered by MouseMoved below.
bool hasConstraintSuggestion = pending.hasSuggestion;
Constraint::Type constraintSuggestion = pending.suggestion;
// Make sure the hover is up to date.
MouseMoved(mx, my, /*leftDown=*/false, /*middleDown=*/false, /*rightDown=*/false,
/*shiftDown=*/false, /*ctrlDown=*/false);
orig.mouse.x = mx;
orig.mouse.y = my;
orig.mouseOnButtonDown = orig.mouse;
Point2d mouse = Point2d::From(mx, my);
// The current mouse location
Vector v = offset.ScaledBy(-1);
v = v.Plus(projRight.ScaledBy(mx/scale));
v = v.Plus(projUp.ScaledBy(my/scale));
hRequest hr = {};
hConstraint hc = {};
switch(pending.operation) {
case Pending::COMMAND:
switch(pending.command) {
case Command::DATUM_POINT:
hr = AddRequest(Request::Type::DATUM_POINT);
SK.GetEntity(hr.entity(0))->PointForceTo(v);
ConstrainPointByHovered(hr.entity(0), &mouse);
ClearSuper();
break;
case Command::LINE_SEGMENT:
case Command::CONSTR_SEGMENT:
hr = AddRequest(Request::Type::LINE_SEGMENT);
SK.GetRequest(hr)->construction = (pending.command == Command::CONSTR_SEGMENT);
SK.GetEntity(hr.entity(1))->PointForceTo(v);
ConstrainPointByHovered(hr.entity(1), &mouse);
ClearSuper();
AddToPending(hr);
pending.operation = Pending::DRAGGING_NEW_LINE_POINT;
pending.request = hr;
pending.point = hr.entity(2);
pending.description = _("click next point of line, or press Esc");
SK.GetEntity(pending.point)->PointForceTo(v);
break;
case Command::RECTANGLE: {
if(!SS.GW.LockedInWorkplane()) {
Error(_("Can't draw rectangle in 3d; first, activate a workplane "
"with Sketch -> In Workplane."));
ClearSuper();
break;
}
hRequest lns[4];
int i;
SS.UndoRemember();
for(i = 0; i < 4; i++) {
lns[i] = AddRequest(Request::Type::LINE_SEGMENT, /*rememberForUndo=*/false);
AddToPending(lns[i]);
}
for(i = 0; i < 4; i++) {
Constraint::ConstrainCoincident(
lns[i].entity(1), lns[(i+1)%4].entity(2));
SK.GetEntity(lns[i].entity(1))->PointForceTo(v);
SK.GetEntity(lns[i].entity(2))->PointForceTo(v);
}
for(i = 0; i < 4; i++) {
Constraint::Constrain(
(i % 2) ? Constraint::Type::HORIZONTAL : Constraint::Type::VERTICAL,
Entity::NO_ENTITY, Entity::NO_ENTITY,
lns[i].entity(0));
}
if(ConstrainPointByHovered(lns[2].entity(1), &mouse)) {
Vector pos = SK.GetEntity(lns[2].entity(1))->PointGetNum();
for(i = 0; i < 4; i++) {
SK.GetEntity(lns[i].entity(1))->PointForceTo(pos);
SK.GetEntity(lns[i].entity(2))->PointForceTo(pos);
}
}
pending.operation = Pending::DRAGGING_NEW_POINT;
pending.point = lns[1].entity(2);
pending.description = _("click to place other corner of rectangle");
hr = lns[0];
break;
}
case Command::CIRCLE:
hr = AddRequest(Request::Type::CIRCLE);
// Centered where we clicked
SK.GetEntity(hr.entity(1))->PointForceTo(v);
// Normal to the screen
SK.GetEntity(hr.entity(32))->NormalForceTo(
Quaternion::From(SS.GW.projRight, SS.GW.projUp));
// Initial radius zero
SK.GetEntity(hr.entity(64))->DistanceForceTo(0);
ConstrainPointByHovered(hr.entity(1), &mouse);
ClearSuper();
AddToPending(hr);
pending.operation = Pending::DRAGGING_NEW_RADIUS;
pending.circle = hr.entity(0);
pending.description = _("click to set radius");
break;
case Command::ARC: {
if(!SS.GW.LockedInWorkplane()) {
Error(_("Can't draw arc in 3d; first, activate a workplane "
"with Sketch -> In Workplane."));
ClearPending();
break;
}
hr = AddRequest(Request::Type::ARC_OF_CIRCLE);
// This fudge factor stops us from immediately failing to solve
// because of the arc's implicit (equal radius) tangent.
Vector adj = SS.GW.projRight.WithMagnitude(2/SS.GW.scale);
SK.GetEntity(hr.entity(1))->PointForceTo(v.Minus(adj));
SK.GetEntity(hr.entity(2))->PointForceTo(v);
SK.GetEntity(hr.entity(3))->PointForceTo(v);
ConstrainPointByHovered(hr.entity(2), &mouse);
ClearSuper();
AddToPending(hr);
pending.operation = Pending::DRAGGING_NEW_ARC_POINT;
pending.point = hr.entity(3);
pending.description = _("click to place point");
break;
}
case Command::CUBIC:
hr = AddRequest(Request::Type::CUBIC);
SK.GetEntity(hr.entity(1))->PointForceTo(v);
SK.GetEntity(hr.entity(2))->PointForceTo(v);
SK.GetEntity(hr.entity(3))->PointForceTo(v);
SK.GetEntity(hr.entity(4))->PointForceTo(v);
ConstrainPointByHovered(hr.entity(1), &mouse);
ClearSuper();
AddToPending(hr);
pending.operation = Pending::DRAGGING_NEW_CUBIC_POINT;
pending.point = hr.entity(4);
pending.description = _("click next point of cubic, or press Esc");
break;
case Command::WORKPLANE:
if(LockedInWorkplane()) {
Error(_("Sketching in a workplane already; sketch in 3d before "
"creating new workplane."));
ClearSuper();
break;
}
hr = AddRequest(Request::Type::WORKPLANE);
SK.GetEntity(hr.entity(1))->PointForceTo(v);
SK.GetEntity(hr.entity(32))->NormalForceTo(
Quaternion::From(SS.GW.projRight, SS.GW.projUp));
ConstrainPointByHovered(hr.entity(1), &mouse);
ClearSuper();
break;
case Command::TTF_TEXT: {
if(!SS.GW.LockedInWorkplane()) {
Error(_("Can't draw text in 3d; first, activate a workplane "
"with Sketch -> In Workplane."));
ClearSuper();
break;
}
hr = AddRequest(Request::Type::TTF_TEXT);
AddToPending(hr);
Request *r = SK.GetRequest(hr);
r->str = "Abc";
r->font = Platform::embeddedFont;
for(int i = 1; i <= 4; i++) {
SK.GetEntity(hr.entity(i))->PointForceTo(v);
}
pending.operation = Pending::DRAGGING_NEW_POINT;
pending.point = hr.entity(3);
pending.description = _("click to place bottom right of text");
break;
}
case Command::IMAGE: {
if(!SS.GW.LockedInWorkplane()) {
Error(_("Can't draw image in 3d; first, activate a workplane "
"with Sketch -> In Workplane."));
ClearSuper();
break;
}
hr = AddRequest(Request::Type::IMAGE);
AddToPending(hr);
Request *r = SK.GetRequest(hr);
r->file = pending.filename;
for(int i = 1; i <= 4; i++) {
SK.GetEntity(hr.entity(i))->PointForceTo(v);
}
pending.operation = Pending::DRAGGING_NEW_POINT;
pending.point = hr.entity(3);
pending.description = "click to place bottom right of image";
break;
}
case Command::COMMENT: {
ClearSuper();
Constraint c = {};
c.group = SS.GW.activeGroup;
c.workplane = SS.GW.ActiveWorkplane();
c.type = Constraint::Type::COMMENT;
c.disp.offset = v;
c.comment = _("NEW COMMENT -- DOUBLE-CLICK TO EDIT");
hc = Constraint::AddConstraint(&c);
break;
}
default: ssassert(false, "Unexpected pending menu id");
}
break;
case Pending::DRAGGING_RADIUS:
ClearPending();
break;
case Pending::DRAGGING_NEW_POINT:
case Pending::DRAGGING_NEW_ARC_POINT:
ConstrainPointByHovered(pending.point, &mouse);
ClearPending();
break;
case Pending::DRAGGING_NEW_CUBIC_POINT: {
hRequest hr = pending.point.request();
Request *r = SK.GetRequest(hr);
if(hover.entity == hr.entity(1) && r->extraPoints >= 2) {
// They want the endpoints coincident, which means a periodic
// spline instead.
r->type = Request::Type::CUBIC_PERIODIC;
// Remove the off-curve control points, which are no longer
// needed here; so move [2,ep+1] down, skipping first pt.
int i;
for(i = 2; i <= r->extraPoints+1; i++) {
SK.GetEntity(hr.entity((i-1)+1))->PointForceTo(
SK.GetEntity(hr.entity(i+1))->PointGetNum());
}
// and move ep+3 down by two, skipping both
SK.GetEntity(hr.entity((r->extraPoints+1)+1))->PointForceTo(
SK.GetEntity(hr.entity((r->extraPoints+3)+1))->PointGetNum());
r->extraPoints -= 2;
// And we're done.
SS.MarkGroupDirty(r->group);
ClearPending();
break;
}
if(ConstrainPointByHovered(pending.point, &mouse)) {
ClearPending();
break;
}
Entity e;
if(r->extraPoints >= (int)arraylen(e.point) - 4) {
ClearPending();
break;
}
(SK.GetRequest(hr)->extraPoints)++;
SS.GenerateAll(SolveSpaceUI::Generate::REGEN);
int ep = r->extraPoints;
Vector last = SK.GetEntity(hr.entity(3+ep))->PointGetNum();
SK.GetEntity(hr.entity(2+ep))->PointForceTo(last);
SK.GetEntity(hr.entity(3+ep))->PointForceTo(v);
SK.GetEntity(hr.entity(4+ep))->PointForceTo(v);
pending.point = hr.entity(4+ep);
break;
}
case Pending::DRAGGING_NEW_LINE_POINT: {
if(hover.entity.v) {
Entity *e = SK.GetEntity(hover.entity);
if(e->IsPoint()) {
hRequest hrl = pending.point.request();
Entity *sp = SK.GetEntity(hrl.entity(1));
if(( e->PointGetNum()).Equals(
(sp->PointGetNum())))
{
// If we constrained by the hovered point, then we
// would create a zero-length line segment. That's
// not good, so just stop drawing.
ClearPending();
break;
}
}
}
bool doneDragging = ConstrainPointByHovered(pending.point, &mouse);
// Constrain the line segment horizontal or vertical if close enough
if(hasConstraintSuggestion) {
Constraint::TryConstrain(constraintSuggestion,
Entity::NO_ENTITY, Entity::NO_ENTITY, pending.request.entity(0));
}
if(doneDragging) {
ClearPending();
break;
}
// Create a new line segment, so that we continue drawing.
hRequest hr = AddRequest(Request::Type::LINE_SEGMENT);
ReplacePending(pending.request, hr);
SK.GetRequest(hr)->construction = SK.GetRequest(pending.request)->construction;
// Displace the second point of the new line segment slightly,
// to avoid creating zero-length edge warnings.
SK.GetEntity(hr.entity(2))->PointForceTo(
v.Plus(projRight.ScaledBy(0.5/scale)));
// Constrain the line segments to share an endpoint
Constraint::ConstrainCoincident(pending.point, hr.entity(1));
Vector pendingPos = SK.GetEntity(pending.point)->PointGetNum();
SK.GetEntity(hr.entity(1))->PointForceTo(pendingPos);
// And drag an endpoint of the new line segment
pending.operation = Pending::DRAGGING_NEW_LINE_POINT;
pending.request = hr;
pending.point = hr.entity(2);
pending.description = _("click next point of line, or press Esc");
break;
}
case Pending::NONE:
default:
ClearPending();
if(!hover.IsEmpty()) {
if(!ctrlDown) {
hoverWasSelectedOnMousedown = IsSelected(&hover);
MakeSelected(&hover);
} else {
MakeUnselected(&hover, /*coincidentPointTrick=*/true);
}
}
break;
}
// Activate group with newly created request/constraint
Group *g = NULL;
if(hr.v != 0) {
g = SK.GetGroup(SK.GetRequest(hr)->group);
}
if(hc.v != 0) {
g = SK.GetGroup(SK.GetConstraint(hc)->group);
}
if(g != NULL) {
g->visible = true;
}
SS.ScheduleShowTW();
Invalidate();
}
void GraphicsWindow::MouseLeftUp(double mx, double my, bool shiftDown, bool ctrlDown) {
orig.mouseDown = false;
switch(pending.operation) {
case Pending::DRAGGING_POINTS:
case Pending::DRAGGING_CONSTRAINT:
case Pending::DRAGGING_NORMAL:
case Pending::DRAGGING_RADIUS:
if(!hoverWasSelectedOnMousedown) {
// And then clear the selection again, since they
// probably didn't want that selected if they just
// were dragging it.
ClearSelection();
}
hoverWasSelectedOnMousedown = false;
SS.extraLine.draw = false;
ClearPending();
Invalidate();
break;
case Pending::DRAGGING_MARQUEE:
SelectByMarquee();
ClearPending();
Invalidate();
break;
case Pending::NONE:
if(hover.IsEmpty() && !ctrlDown) {
ClearSelection();
}
break;
default:
break; // do nothing
}
}
void GraphicsWindow::EditConstraint(hConstraint constraint) {
constraintBeingEdited = constraint;
ClearSuper();
Constraint *c = SK.GetConstraint(constraintBeingEdited);
if(!c->HasLabel()) {
// Not meaningful to edit a constraint without a dimension
return;
}
if(c->reference) {
// Not meaningful to edit a reference dimension
return;
}
Vector p3 = c->GetLabelPos(GetCamera());
Point2d p2 = ProjectPoint(p3);
std::string editValue;
std::string editPlaceholder;
switch(c->type) {
case Constraint::Type::COMMENT:
editValue = c->comment;
editPlaceholder = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
break;
default: {
double value = fabs(c->valA);
// If displayed as radius, also edit as radius.
if(c->type == Constraint::Type::DIAMETER && c->other)
value /= 2;
// Try showing value with default number of digits after decimal first.
if(c->type == Constraint::Type::LENGTH_RATIO || c->type == Constraint::Type::ARC_ARC_LEN_RATIO || c->type == Constraint::Type::ARC_LINE_LEN_RATIO) {
editValue = ssprintf("%.3f", value);
} else if(c->type == Constraint::Type::ANGLE) {
editValue = SS.DegreeToString(value);
} else {
editValue = SS.MmToString(value, true);
value /= SS.MmPerUnit();
}
// If that's not enough to represent it exactly, show the value with as many
// digits after decimal as required, up to 10.
int digits = 0;
while(fabs(std::stod(editValue) - value) > 1e-10) {
editValue = ssprintf("%.*f", digits, value);
digits++;
}
editPlaceholder = "10.000000";
break;
}
}
double width, height;
window->GetContentSize(&width, &height);
hStyle hs = c->disp.style;
if(hs.v == 0) hs.v = Style::CONSTRAINT;
double capHeight = Style::TextHeight(hs);
double fontHeight = VectorFont::Builtin()->GetHeight(capHeight);
double editMinWidth = VectorFont::Builtin()->GetWidth(capHeight, editPlaceholder);
window->ShowEditor(p2.x + width / 2, height / 2 - p2.y,
fontHeight, editMinWidth,
/*isMonospace=*/false, editValue);
}
void GraphicsWindow::MouseLeftDoubleClick(double mx, double my) {
if(window->IsEditorVisible()) return;
SS.TW.HideEditControl();
if(hover.constraint.v) {
EditConstraint(hover.constraint);
}
}
void GraphicsWindow::EditControlDone(const std::string &s) {
window->HideEditor();
window->Invalidate();
Constraint *c = SK.GetConstraint(constraintBeingEdited);
if(c->type == Constraint::Type::COMMENT) {
SS.UndoRemember();
c->comment = s;
return;
}
if(Expr *e = Expr::From(s, true)) {
SS.UndoRemember();
switch(c->type) {
case Constraint::Type::PROJ_PT_DISTANCE:
case Constraint::Type::PT_LINE_DISTANCE:
case Constraint::Type::PT_FACE_DISTANCE:
case Constraint::Type::PT_PLANE_DISTANCE:
case Constraint::Type::LENGTH_DIFFERENCE:
case Constraint::Type::ARC_ARC_DIFFERENCE:
case Constraint::Type::ARC_LINE_DIFFERENCE: {
// The sign is not displayed to the user, but this is a signed
// distance internally. To flip the sign, the user enters a
// negative distance.
bool wasNeg = (c->valA < 0);
if(wasNeg) {
c->valA = -SS.ExprToMm(e);
} else {
c->valA = SS.ExprToMm(e);
}
break;
}
case Constraint::Type::ANGLE:
case Constraint::Type::LENGTH_RATIO:
case Constraint::Type::ARC_ARC_LEN_RATIO:
case Constraint::Type::ARC_LINE_LEN_RATIO:
// These don't get the units conversion for distance, and
// they're always positive
c->valA = fabs(e->Eval());
break;
case Constraint::Type::DIAMETER:
c->valA = fabs(SS.ExprToMm(e));
// If displayed and edited as radius, convert back
// to diameter
if(c->other)
c->valA *= 2;
break;
default:
// These are always positive, and they get the units conversion.
c->valA = fabs(SS.ExprToMm(e));
break;
}
SS.MarkGroupDirty(c->group);
}
}
void GraphicsWindow::MouseScroll(double zoomMultiplyer) {
// To support smooth scrolling where scroll wheel events come in increments
// smaller (or larger) than 1 we do:
// scale *= exp(ln(1.2) * zoomMultiplyer);
// to ensure that the same total scroll delta always results in the same
// total zoom irrespective of in how many increments the zoom was applied.
// For example if we scroll a total delta of a+b in two events vs. one then
// scale * e^a * e^b == scale * e^(a+b)
// while
// scale * a * b != scale * (a+b)
// So this constant is ln(1.2) = 0.1823216 to make the default zoom 1.2x
ZoomToMouse(zoomMultiplyer);
}
void GraphicsWindow::MouseLeave() {
// Un-hover everything when the mouse leaves our window, unless there's
// currently a context menu shown.
if(!context.active) {
hover.Clear();
toolbarHovered = Command::NONE;
Invalidate();
}
SS.extraLine.draw = false;
}
void GraphicsWindow::SixDofEvent(Platform::SixDofEvent event) {
if(event.type == Platform::SixDofEvent::Type::RELEASE) {
ZoomToFit(/*includingInvisibles=*/false, /*useSelection=*/true);
Invalidate();
return;
}
if(!havePainted) return;
Vector out = projRight.Cross(projUp);
// rotation vector is axis of rotation, and its magnitude is angle
Vector aa = Vector::From(event.rotationX, event.rotationY, event.rotationZ);
// but it's given with respect to screen projection frame
aa = aa.ScaleOutOfCsys(projRight, projUp, out);
double aam = aa.Magnitude();
if(aam > 0.0) aa = aa.WithMagnitude(1);
// This can either transform our view, or transform a linked part.
GroupSelection();
Entity *e = NULL;
Group *g = NULL;
if(gs.points == 1 && gs.n == 1) e = SK.GetEntity(gs.point [0]);
if(gs.entities == 1 && gs.n == 1) e = SK.GetEntity(gs.entity[0]);
if(e) g = SK.GetGroup(e->group);
if(g && g->type == Group::Type::LINKED && !event.shiftDown) {
// Apply the transformation to a linked part. Gain down the Z
// axis, since it's hard to see what you're doing on that one since
// it's normal to the screen.
Vector t = projRight.ScaledBy(event.translationX/scale).Plus(
projUp .ScaledBy(event.translationY/scale).Plus(
out .ScaledBy(0.1*event.translationZ/scale)));
Quaternion q = Quaternion::From(aa, aam);
// If we go five seconds without SpaceNavigator input, or if we've
// switched groups, then consider that a new action and save an undo
// point.
int64_t now = GetMilliseconds();
if(now - last6DofTime > 5000 ||
last6DofGroup != g->h)
{
SS.UndoRemember();
}
g->TransformImportedBy(t, q);
last6DofTime = now;
last6DofGroup = g->h;
SS.MarkGroupDirty(g->h);
} else {
// Apply the transformation to the view of the everything. The
// x and y components are translation; but z component is scale,
// not translation, or else it would do nothing in a parallel
// projection
offset = offset.Plus(projRight.ScaledBy(event.translationX/scale));
offset = offset.Plus(projUp.ScaledBy(event.translationY/scale));
scale *= exp(0.001*event.translationZ);
if(aam > 0.0) {
projRight = projRight.RotatedAbout(aa, -aam);
projUp = projUp. RotatedAbout(aa, -aam);
NormalizeProjectionVectors();
}
}
havePainted = false;
Invalidate();
}