solvespace/src/draw.cpp
Přemysl Eric Janouch 9dd67c7ba0 Use Range-based for Loops Instead of NextAfter for all IdList Objects
Based on commit '3b395bb5a7' by pjanx

Resolves a performance regression of iteration being O(n * log n)
rather than O(n).
2021-05-12 19:50:23 -04:00

938 lines
33 KiB
C++

//-----------------------------------------------------------------------------
// The root function to paint our graphics window, after setting up all the
// views and such appropriately. Also contains all the stuff to manage the
// selection.
//
// Copyright 2008-2013 Jonathan Westhues.
//-----------------------------------------------------------------------------
#include "solvespace.h"
bool GraphicsWindow::Selection::Equals(Selection *b) {
if(entity != b->entity) return false;
if(constraint != b->constraint) return false;
return true;
}
bool GraphicsWindow::Selection::IsEmpty() {
if(entity.v) return false;
if(constraint.v) return false;
return true;
}
bool GraphicsWindow::Selection::HasEndpoints() {
if(!entity.v) return false;
Entity *e = SK.GetEntity(entity);
return e->HasEndpoints();
}
void GraphicsWindow::Selection::Clear() {
entity.v = constraint.v = 0;
emphasized = false;
}
void GraphicsWindow::Selection::Draw(bool isHovered, Canvas *canvas) {
const Camera &camera = canvas->GetCamera();
std::vector<Vector> refs;
if(entity.v) {
Entity *e = SK.GetEntity(entity);
e->Draw(isHovered ? Entity::DrawAs::HOVERED :
Entity::DrawAs::SELECTED,
canvas);
if(emphasized) {
e->GetReferencePoints(&refs);
}
}
if(constraint.v) {
Constraint *c = SK.GetConstraint(constraint);
c->Draw(isHovered ? Constraint::DrawAs::HOVERED :
Constraint::DrawAs::SELECTED,
canvas);
if(emphasized) {
c->GetReferencePoints(camera, &refs);
}
}
if(emphasized && (constraint.v || entity.v)) {
// We want to emphasize this constraint or entity, by drawing a thick
// line from the top left corner of the screen to the reference point(s)
// of that entity or constraint.
Canvas::Stroke strokeEmphasis = {};
strokeEmphasis.layer = Canvas::Layer::FRONT;
strokeEmphasis.color = Style::Color(Style::HOVERED).WithAlpha(50);
strokeEmphasis.width = 40;
strokeEmphasis.unit = Canvas::Unit::PX;
Canvas::hStroke hcsEmphasis = canvas->GetStroke(strokeEmphasis);
Point2d topLeftScreen;
topLeftScreen.x = -(double)camera.width / 2;
topLeftScreen.y = (double)camera.height / 2;
Vector topLeft = camera.UnProjectPoint(topLeftScreen);
auto it = std::unique(refs.begin(), refs.end(),
[](Vector a, Vector b) { return a.Equals(b); });
refs.erase(it, refs.end());
for(Vector p : refs) {
canvas->DrawLine(topLeft, p, hcsEmphasis);
}
}
}
void GraphicsWindow::ClearSelection() {
selection.Clear();
SS.ScheduleShowTW();
Invalidate();
}
void GraphicsWindow::ClearNonexistentSelectionItems() {
bool change = false;
Selection *s;
selection.ClearTags();
for(s = selection.First(); s; s = selection.NextAfter(s)) {
if(s->constraint.v && !(SK.constraint.FindByIdNoOops(s->constraint))) {
s->tag = 1;
change = true;
}
if(s->entity.v && !(SK.entity.FindByIdNoOops(s->entity))) {
s->tag = 1;
change = true;
}
}
selection.RemoveTagged();
if(change) Invalidate();
}
//-----------------------------------------------------------------------------
// Is this entity/constraint selected?
//-----------------------------------------------------------------------------
bool GraphicsWindow::IsSelected(hEntity he) {
Selection s = {};
s.entity = he;
return IsSelected(&s);
}
bool GraphicsWindow::IsSelected(Selection *st) {
Selection *s;
for(s = selection.First(); s; s = selection.NextAfter(s)) {
if(s->Equals(st)) {
return true;
}
}
return false;
}
//-----------------------------------------------------------------------------
// Unselect an item, if it is selected. We can either unselect just that item,
// or also unselect any coincident points. The latter is useful if the user
// somehow selects two coincident points (like with select all), because it
// would otherwise be impossible to de-select the lower of the two.
//-----------------------------------------------------------------------------
void GraphicsWindow::MakeUnselected(hEntity he, bool coincidentPointTrick) {
Selection stog = {};
stog.entity = he;
MakeUnselected(&stog, coincidentPointTrick);
}
void GraphicsWindow::MakeUnselected(Selection *stog, bool coincidentPointTrick){
if(stog->IsEmpty()) return;
Selection *s;
// If an item was selected, then we just un-select it.
selection.ClearTags();
for(s = selection.First(); s; s = selection.NextAfter(s)) {
if(s->Equals(stog)) {
s->tag = 1;
}
}
// If two points are coincident, then it's impossible to hover one of
// them. But make sure to deselect both, to avoid mysterious seeming
// inability to deselect if the bottom one did somehow get selected.
if(stog->entity.v && coincidentPointTrick) {
Entity *e = SK.GetEntity(stog->entity);
if(e->IsPoint()) {
Vector ep = e->PointGetNum();
for(s = selection.First(); s; s = selection.NextAfter(s)) {
if(!s->entity.v) continue;
if(s->entity == stog->entity)
continue;
Entity *se = SK.GetEntity(s->entity);
if(!se->IsPoint()) continue;
if(ep.Equals(se->PointGetNum())) {
s->tag = 1;
}
}
}
}
selection.RemoveTagged();
}
//-----------------------------------------------------------------------------
// Select an item, if it isn't selected already.
//-----------------------------------------------------------------------------
void GraphicsWindow::MakeSelected(hEntity he) {
Selection stog = {};
stog.entity = he;
MakeSelected(&stog);
}
void GraphicsWindow::MakeSelected(hConstraint hc) {
Selection stog = {};
stog.constraint = hc;
MakeSelected(&stog);
}
void GraphicsWindow::MakeSelected(Selection *stog) {
if(stog->IsEmpty()) return;
if(IsSelected(stog)) return;
if(stog->entity.v != 0 && SK.GetEntity(stog->entity)->IsFace()) {
// In the interest of speed for the triangle drawing code,
// only two faces may be selected at a time.
int c = 0;
Selection *s;
selection.ClearTags();
for(s = selection.First(); s; s = selection.NextAfter(s)) {
hEntity he = s->entity;
if(he.v != 0 && SK.GetEntity(he)->IsFace()) {
c++;
if(c >= 2) s->tag = 1;
}
}
selection.RemoveTagged();
}
selection.Add(stog);
}
//-----------------------------------------------------------------------------
// Select everything that lies within the marquee view-aligned rectangle.
//-----------------------------------------------------------------------------
void GraphicsWindow::SelectByMarquee() {
Point2d marqueePoint = ProjectPoint(orig.marqueePoint);
BBox marqueeBBox = BBox::From(Vector::From(marqueePoint.x, marqueePoint.y, VERY_NEGATIVE),
Vector::From(orig.mouse.x, orig.mouse.y, VERY_POSITIVE));
for(Entity &e : SK.entity) {
if(e.group != SS.GW.activeGroup) continue;
if(e.IsFace() || e.IsDistance()) continue;
if(!e.IsVisible()) continue;
bool entityHasBBox;
BBox entityBBox = e.GetOrGenerateScreenBBox(&entityHasBBox);
if(entityHasBBox && entityBBox.Overlaps(marqueeBBox)) {
MakeSelected(e.h);
}
}
}
//-----------------------------------------------------------------------------
// Sort the selection according to various critieria: the entities and
// constraints separately, counts of certain types of entities (circles,
// lines, etc.), and so on.
//-----------------------------------------------------------------------------
void GraphicsWindow::GroupSelection() {
gs = {};
int i;
for(i = 0; i < selection.n; i++) {
Selection *s = &(selection[i]);
if(s->entity.v) {
(gs.n)++;
Entity *e = SK.entity.FindById(s->entity);
if(e->IsStylable()) gs.stylables++;
// A list of points, and a list of all entities that aren't points.
if(e->IsPoint()) {
gs.points++;
gs.point.push_back(s->entity);
} else {
gs.entities++;
gs.entity.push_back(s->entity);
}
// And an auxiliary list of normals, including normals from
// workplanes.
if(e->IsNormal()) {
gs.anyNormals++;
gs.anyNormal.push_back(s->entity);
} else if(e->IsWorkplane()) {
gs.anyNormals++;
gs.anyNormal.push_back(e->Normal()->h);
}
// And of vectors (i.e., stuff with a direction to constrain)
if(e->HasVector()) {
gs.vectors++;
gs.vector.push_back(s->entity);
}
// Faces (which are special, associated/drawn with triangles)
if(e->IsFace()) {
gs.faces++;
gs.face.push_back(s->entity);
}
if(e->HasEndpoints()) {
(gs.withEndpoints)++;
}
// And some aux counts too
switch(e->type) {
case Entity::Type::WORKPLANE: (gs.workplanes)++; break;
case Entity::Type::LINE_SEGMENT: (gs.lineSegments)++; break;
case Entity::Type::CUBIC: (gs.cubics)++; break;
case Entity::Type::CUBIC_PERIODIC: (gs.periodicCubics)++; break;
case Entity::Type::ARC_OF_CIRCLE:
(gs.circlesOrArcs)++;
(gs.arcs)++;
break;
case Entity::Type::CIRCLE: (gs.circlesOrArcs)++; break;
default: break;
}
}
if(s->constraint.v) {
gs.constraints++;
gs.constraint.push_back(s->constraint);
Constraint *c = SK.GetConstraint(s->constraint);
if(c->IsStylable()) gs.stylables++;
if(c->HasLabel()) gs.constraintLabels++;
}
}
}
Camera GraphicsWindow::GetCamera() const {
Camera camera = {};
if(window) {
window->GetContentSize(&camera.width, &camera.height);
camera.pixelRatio = window->GetDevicePixelRatio();
camera.gridFit = (window->GetDevicePixelRatio() == 1);
} else { // solvespace-cli
camera.width = 297.0; // A4? Whatever...
camera.height = 210.0;
camera.pixelRatio = 1.0;
camera.gridFit = camera.pixelRatio == 1.0;
}
camera.offset = offset;
camera.projUp = projUp;
camera.projRight = projRight;
camera.scale = scale;
camera.tangent = SS.CameraTangent();
return camera;
}
Lighting GraphicsWindow::GetLighting() const {
Lighting lighting = {};
lighting.backgroundColor = SS.backgroundColor;
lighting.ambientIntensity = SS.ambientIntensity;
lighting.lightIntensity[0] = SS.lightIntensity[0];
lighting.lightIntensity[1] = SS.lightIntensity[1];
lighting.lightDirection[0] = SS.lightDir[0];
lighting.lightDirection[1] = SS.lightDir[1];
return lighting;
}
GraphicsWindow::Selection GraphicsWindow::ChooseFromHoverToSelect() {
Selection sel = {};
if(hoverList.IsEmpty())
return sel;
Group *activeGroup = SK.GetGroup(SS.GW.activeGroup);
int bestOrder = -1;
int bestZIndex = 0;
double bestDepth = VERY_POSITIVE;
for(const Hover &hov : hoverList) {
hGroup hg = {};
if(hov.selection.entity.v != 0) {
hg = SK.GetEntity(hov.selection.entity)->group;
} else if(hov.selection.constraint.v != 0) {
hg = SK.GetConstraint(hov.selection.constraint)->group;
}
Group *g = SK.GetGroup(hg);
if(g->order > activeGroup->order) continue;
if(bestOrder != -1 && (bestOrder > g->order || bestZIndex > hov.zIndex)) continue;
// we have hov.zIndex is >= best and hov.group is >= best (but not > active group)
if(hov.depth > bestDepth && bestOrder == g->order && bestZIndex == hov.zIndex) continue;
bestOrder = g->order;
bestZIndex = hov.zIndex;
bestDepth = hov.depth;
sel = hov.selection;
}
return sel;
}
// This uses the same logic as hovering and static entity selection
// but ignores points known not to be draggable
GraphicsWindow::Selection GraphicsWindow::ChooseFromHoverToDrag() {
Selection sel = {};
if(hoverList.IsEmpty())
return sel;
Group *activeGroup = SK.GetGroup(SS.GW.activeGroup);
int bestOrder = -1;
int bestZIndex = 0;
double bestDepth = VERY_POSITIVE;
for(const Hover &hov : hoverList) {
hGroup hg = {};
if(hov.selection.entity.v != 0) {
Entity *e = SK.GetEntity(hov.selection.entity);
if (!e->CanBeDragged()) continue;
hg = e->group;
} else if(hov.selection.constraint.v != 0) {
hg = SK.GetConstraint(hov.selection.constraint)->group;
}
Group *g = SK.GetGroup(hg);
if(g->order > activeGroup->order) continue;
if(bestOrder != -1 && (bestOrder > g->order || bestZIndex > hov.zIndex)) continue;
// we have hov.zIndex is >= best and hov.group is >= best (but not > active group)
if(hov.depth > bestDepth && bestOrder == g->order && bestZIndex == hov.zIndex) continue;
bestOrder = g->order;
bestZIndex = hov.zIndex;
sel = hov.selection;
}
return sel;
}
void GraphicsWindow::HitTestMakeSelection(Point2d mp) {
hoverList = {};
Selection sel = {};
// Did the view projection change? If so, invalidate bounding boxes.
if(!offset.EqualsExactly(cached.offset) ||
!projRight.EqualsExactly(cached.projRight) ||
!projUp.EqualsExactly(cached.projUp) ||
EXACT(scale != cached.scale)) {
cached.offset = offset;
cached.projRight = projRight;
cached.projUp = projUp;
cached.scale = scale;
for(Entity &e : SK.entity) {
e.screenBBoxValid = false;
}
}
ObjectPicker canvas = {};
canvas.camera = GetCamera();
canvas.selRadius = 10.0;
canvas.point = mp;
canvas.maxZIndex = -1;
// Always do the entities; we might be dragging something that should
// be auto-constrained, and we need the hover for that.
for(Entity &e : SK.entity) {
if(!e.IsVisible()) continue;
// If faces aren't selectable, image entities aren't either.
if(e.type == Entity::Type::IMAGE && !showFaces) continue;
// Don't hover whatever's being dragged.
if(IsFromPending(e.h.request())) {
// The one exception is when we're creating a new cubic; we
// want to be able to hover the first point, because that's
// how we turn it into a periodic spline.
if(!e.IsPoint()) continue;
if(!e.h.isFromRequest()) continue;
Request *r = SK.GetRequest(e.h.request());
if(r->type != Request::Type::CUBIC) continue;
if(r->extraPoints < 2) continue;
if(e.h.v != r->h.entity(1).v) continue;
}
if(canvas.Pick([&]{ e.Draw(Entity::DrawAs::DEFAULT, &canvas); })) {
Hover hov = {};
hov.distance = canvas.minDistance;
hov.zIndex = canvas.maxZIndex;
hov.depth = canvas.minDepth;
hov.selection.entity = e.h;
hoverList.Add(&hov);
}
}
// The constraints and faces happen only when nothing's in progress.
if(pending.operation == Pending::NONE) {
// Constraints
for(Constraint &c : SK.constraint) {
if(canvas.Pick([&]{ c.Draw(Constraint::DrawAs::DEFAULT, &canvas); })) {
Hover hov = {};
hov.distance = canvas.minDistance;
hov.zIndex = canvas.maxZIndex;
hov.selection.constraint = c.h;
hoverList.Add(&hov);
}
}
}
std::sort(hoverList.begin(), hoverList.end(),
[](const Hover &a, const Hover &b) {
if(a.zIndex == b.zIndex) return a.distance < b.distance;
return a.zIndex > b.zIndex;
});
sel = ChooseFromHoverToSelect();
if(pending.operation == Pending::NONE) {
// Faces, from the triangle mesh; these are lowest priority
if(sel.constraint.v == 0 && sel.entity.v == 0 && showShaded && showFaces) {
Group *g = SK.GetGroup(activeGroup);
SMesh *m = &(g->displayMesh);
uint32_t v = m->FirstIntersectionWith(mp);
if(v) {
sel.entity.v = v;
}
}
}
canvas.Clear();
if(!sel.Equals(&hover)) {
hover = sel;
Invalidate();
}
}
//-----------------------------------------------------------------------------
// Project a point in model space to screen space, exactly as gl would; return
// units are pixels.
//-----------------------------------------------------------------------------
Point2d GraphicsWindow::ProjectPoint(Vector p) {
Vector p3 = ProjectPoint3(p);
Point2d p2 = { p3.x, p3.y };
return p2;
}
//-----------------------------------------------------------------------------
// Project a point in model space to screen space, exactly as gl would; return
// units are pixels. The z coordinate is also returned, also in pixels.
//-----------------------------------------------------------------------------
Vector GraphicsWindow::ProjectPoint3(Vector p) {
double w;
Vector r = ProjectPoint4(p, &w);
return r.ScaledBy(scale/w);
}
//-----------------------------------------------------------------------------
// Project a point in model space halfway into screen space. The scale is
// not applied, and the perspective divide isn't applied; instead the w
// coordinate is returned separately.
//-----------------------------------------------------------------------------
Vector GraphicsWindow::ProjectPoint4(Vector p, double *w) {
p = p.Plus(offset);
Vector r;
r.x = p.Dot(projRight);
r.y = p.Dot(projUp);
r.z = p.Dot(projUp.Cross(projRight));
*w = 1 + r.z*SS.CameraTangent()*scale;
return r;
}
//-----------------------------------------------------------------------------
// Return a point in the plane parallel to the screen and through the offset,
// that projects onto the specified (x, y) coordinates.
//-----------------------------------------------------------------------------
Vector GraphicsWindow::UnProjectPoint(Point2d p) {
Vector orig = offset.ScaledBy(-1);
// Note that we ignoring the effects of perspective. Since our returned
// point has the same component normal to the screen as the offset, it
// will have z = 0 after the rotation is applied, thus w = 1. So this is
// correct.
orig = orig.Plus(projRight.ScaledBy(p.x / scale)).Plus(
projUp. ScaledBy(p.y / scale));
return orig;
}
Vector GraphicsWindow::UnProjectPoint3(Vector p) {
p.z = p.z / (scale - p.z * SS.CameraTangent() * scale);
double w = 1 + p.z * SS.CameraTangent() * scale;
p.x *= w / scale;
p.y *= w / scale;
Vector orig = offset.ScaledBy(-1);
orig = orig.Plus(projRight.ScaledBy(p.x)).Plus(
projUp. ScaledBy(p.y).Plus(
projRight.Cross(projUp). ScaledBy(p.z)));
return orig;
}
void GraphicsWindow::NormalizeProjectionVectors() {
if(projRight.Magnitude() < LENGTH_EPS) {
projRight = Vector::From(1, 0, 0);
}
Vector norm = projRight.Cross(projUp);
// If projRight and projUp somehow ended up parallel, then pick an
// arbitrary projUp normal to projRight.
if(norm.Magnitude() < LENGTH_EPS) {
norm = projRight.Normal(0);
}
projUp = norm.Cross(projRight);
projUp = projUp.WithMagnitude(1);
projRight = projRight.WithMagnitude(1);
}
void GraphicsWindow::DrawSnapGrid(Canvas *canvas) {
if(!LockedInWorkplane()) return;
const Camera &camera = canvas->GetCamera();
double width = camera.width,
height = camera.height;
hEntity he = ActiveWorkplane();
EntityBase *wrkpl = SK.GetEntity(he),
*norm = wrkpl->Normal();
Vector n = projUp.Cross(projRight);
Vector wu, wv, wn, wp;
wp = SK.GetEntity(wrkpl->point[0])->PointGetNum();
wu = norm->NormalU();
wv = norm->NormalV();
wn = norm->NormalN();
double g = SS.gridSpacing;
double umin = VERY_POSITIVE, umax = VERY_NEGATIVE,
vmin = VERY_POSITIVE, vmax = VERY_NEGATIVE;
int a;
for(a = 0; a < 4; a++) {
// Ideally, we would just do +/- half the width and height; but
// allow some extra slop for rounding.
Vector horiz = projRight.ScaledBy((0.6*width)/scale + 2*g),
vert = projUp. ScaledBy((0.6*height)/scale + 2*g);
if(a == 2 || a == 3) horiz = horiz.ScaledBy(-1);
if(a == 1 || a == 3) vert = vert. ScaledBy(-1);
Vector tp = horiz.Plus(vert).Minus(offset);
// Project the point into our grid plane, normal to the screen
// (not to the grid plane). If the plane is on edge then this is
// impossible so don't try to draw the grid.
bool parallel;
Vector tpp = Vector::AtIntersectionOfPlaneAndLine(
wn, wn.Dot(wp),
tp, tp.Plus(n),
&parallel);
if(parallel) return;
tpp = tpp.Minus(wp);
double uu = tpp.Dot(wu),
vv = tpp.Dot(wv);
umin = min(uu, umin);
umax = max(uu, umax);
vmin = min(vv, vmin);
vmax = max(vv, vmax);
}
int i, j, i0, i1, j0, j1;
i0 = (int)(umin / g);
i1 = (int)(umax / g);
j0 = (int)(vmin / g);
j1 = (int)(vmax / g);
if(i0 > i1 || i1 - i0 > 400) return;
if(j0 > j1 || j1 - j0 > 400) return;
Canvas::Stroke stroke = {};
stroke.layer = Canvas::Layer::BACK;
stroke.color = Style::Color(Style::DATUM).WithAlpha(75);
stroke.unit = Canvas::Unit::PX;
stroke.width = 1.0f;
Canvas::hStroke hcs = canvas->GetStroke(stroke);
for(i = i0 + 1; i < i1; i++) {
canvas->DrawLine(wp.Plus(wu.ScaledBy(i*g)).Plus(wv.ScaledBy(j0*g)),
wp.Plus(wu.ScaledBy(i*g)).Plus(wv.ScaledBy(j1*g)),
hcs);
}
for(j = j0 + 1; j < j1; j++) {
canvas->DrawLine(wp.Plus(wu.ScaledBy(i0*g)).Plus(wv.ScaledBy(j*g)),
wp.Plus(wu.ScaledBy(i1*g)).Plus(wv.ScaledBy(j*g)),
hcs);
}
}
void GraphicsWindow::DrawEntities(Canvas *canvas, bool persistent) {
for(Entity &e : SK.entity) {
if(persistent == (e.IsNormal() || e.IsWorkplane())) continue;
switch(SS.GW.drawOccludedAs) {
case DrawOccludedAs::VISIBLE:
e.Draw(Entity::DrawAs::OVERLAY, canvas);
break;
case DrawOccludedAs::STIPPLED:
e.Draw(Entity::DrawAs::HIDDEN, canvas);
/* fallthrough */
case DrawOccludedAs::INVISIBLE:
e.Draw(Entity::DrawAs::DEFAULT, canvas);
break;
}
}
}
void GraphicsWindow::DrawPersistent(Canvas *canvas) {
// Draw the active group; this does stuff like the mesh and edges.
SK.GetGroup(activeGroup)->Draw(canvas);
// Now draw the entities that don't change with viewport.
DrawEntities(canvas, /*persistent=*/true);
// Draw filled paths in all groups, when those filled paths were requested
// specially by assigning a style with a fill color, or when the filled
// paths are just being filled by default. This should go last, to make
// the transparency work.
for(hGroup hg : SK.groupOrder) {
Group *g = SK.GetGroup(hg);
if(!(g->IsVisible())) continue;
g->DrawFilledPaths(canvas);
}
}
void GraphicsWindow::Draw(Canvas *canvas) {
const Camera &camera = canvas->GetCamera();
// Nasty case when we're reloading the linked files; could be that
// we get an error, so a dialog pops up, and a message loop starts, and
// we have to get called to paint ourselves. If the sketch is screwed
// up, then we could trigger an oops trying to draw.
if(!SS.allConsistent) return;
if(showSnapGrid) DrawSnapGrid(canvas);
// Draw all the things that don't change when we rotate.
if(persistentCanvas != NULL) {
if(persistentDirty) {
persistentDirty = false;
persistentCanvas->Clear();
DrawPersistent(&*persistentCanvas);
persistentCanvas->Finalize();
}
persistentCanvas->Draw();
} else {
DrawPersistent(canvas);
}
// Draw the entities that do change with viewport.
DrawEntities(canvas, /*persistent=*/false);
// Draw the polygon errors.
if(SS.checkClosedContour) {
SK.GetGroup(activeGroup)->DrawPolyError(canvas);
}
// Draw the constraints
for(Constraint &c : SK.constraint) {
c.Draw(Constraint::DrawAs::DEFAULT, canvas);
}
// Draw areas
if(SS.showContourAreas) {
for(hGroup hg : SK.groupOrder) {
Group *g = SK.GetGroup(hg);
if(g->h != activeGroup) continue;
if(!(g->IsVisible())) continue;
g->DrawContourAreaLabels(canvas);
}
}
// Draw the "pending" constraint, i.e. a constraint that would be
// placed on a line that is almost horizontal or vertical.
if(SS.GW.pending.operation == Pending::DRAGGING_NEW_LINE_POINT &&
SS.GW.pending.hasSuggestion) {
Constraint c = {};
c.group = SS.GW.activeGroup;
c.workplane = SS.GW.ActiveWorkplane();
c.type = SS.GW.pending.suggestion;
c.entityA = SS.GW.pending.request.entity(0);
c.Draw(Constraint::DrawAs::DEFAULT, canvas);
}
Canvas::Stroke strokeAnalyze = Style::Stroke(Style::ANALYZE);
strokeAnalyze.layer = Canvas::Layer::FRONT;
Canvas::hStroke hcsAnalyze = canvas->GetStroke(strokeAnalyze);
// Draw the traced path, if one exists
SEdgeList tracedEdges = {};
SS.traced.path.MakeEdgesInto(&tracedEdges);
canvas->DrawEdges(tracedEdges, hcsAnalyze);
tracedEdges.Clear();
Canvas::Stroke strokeError = Style::Stroke(Style::DRAW_ERROR);
strokeError.layer = Canvas::Layer::FRONT;
strokeError.width = 12;
Canvas::hStroke hcsError = canvas->GetStroke(strokeError);
// And the naked edges, if the user did Analyze -> Show Naked Edges.
canvas->DrawEdges(SS.nakedEdges, hcsError);
// Then redraw whatever the mouse is hovering over, highlighted.
hover.Draw(/*isHovered=*/true, canvas);
SK.GetGroup(activeGroup)->DrawMesh(Group::DrawMeshAs::HOVERED, canvas);
// And finally draw the selection, same mechanism.
for(Selection *s = selection.First(); s; s = selection.NextAfter(s)) {
s->Draw(/*isHovered=*/false, canvas);
}
SK.GetGroup(activeGroup)->DrawMesh(Group::DrawMeshAs::SELECTED, canvas);
Canvas::Stroke strokeDatum = Style::Stroke(Style::DATUM);
strokeDatum.unit = Canvas::Unit::PX;
strokeDatum.layer = Canvas::Layer::FRONT;
strokeDatum.width = 1;
Canvas::hStroke hcsDatum = canvas->GetStroke(strokeDatum);
// An extra line, used to indicate the origin when rotating within the
// plane of the monitor.
if(SS.extraLine.draw) {
canvas->DrawLine(SS.extraLine.ptA, SS.extraLine.ptB, hcsDatum);
}
if(SS.centerOfMass.draw && !SS.centerOfMass.dirty) {
Vector p = SS.centerOfMass.position;
Vector u = camera.projRight;
Vector v = camera.projUp;
const double size = 10.0;
const int subdiv = 16;
double h = Style::DefaultTextHeight() / camera.scale;
std::string s =
SS.MmToStringSI(p.x) + ", " +
SS.MmToStringSI(p.y) + ", " +
SS.MmToStringSI(p.z);
canvas->DrawVectorText(s.c_str(), h,
p.Plus(u.ScaledBy((size + 5.0)/scale)).Minus(v.ScaledBy(h / 2.0)),
u, v, hcsDatum);
u = u.WithMagnitude(size / scale);
v = v.WithMagnitude(size / scale);
canvas->DrawLine(p.Minus(u), p.Plus(u), hcsDatum);
canvas->DrawLine(p.Minus(v), p.Plus(v), hcsDatum);
Vector prev;
for(int i = 0; i <= subdiv; i++) {
double a = (double)i / subdiv * 2.0 * PI;
Vector point = p.Plus(u.ScaledBy(cos(a))).Plus(v.ScaledBy(sin(a)));
if(i > 0) {
canvas->DrawLine(point, prev, hcsDatum);
}
prev = point;
}
}
// A note to indicate the origin in the just-exported file.
if(SS.justExportedInfo.draw) {
Vector p, u, v;
if(SS.justExportedInfo.showOrigin) {
p = SS.justExportedInfo.pt,
u = SS.justExportedInfo.u,
v = SS.justExportedInfo.v;
} else {
p = camera.offset.ScaledBy(-1);
u = camera.projRight;
v = camera.projUp;
}
canvas->DrawVectorText("previewing exported geometry; press Esc to return",
Style::DefaultTextHeight() / camera.scale,
p.Plus(u.ScaledBy(10/scale)).Plus(v.ScaledBy(10/scale)), u, v,
hcsDatum);
if(SS.justExportedInfo.showOrigin) {
Vector um = p.Plus(u.WithMagnitude(-15/scale)),
up = p.Plus(u.WithMagnitude(30/scale)),
vm = p.Plus(v.WithMagnitude(-15/scale)),
vp = p.Plus(v.WithMagnitude(30/scale));
canvas->DrawLine(um, up, hcsDatum);
canvas->DrawLine(vm, vp, hcsDatum);
canvas->DrawVectorText("(x, y) = (0, 0) for file just exported",
Style::DefaultTextHeight() / camera.scale,
p.Plus(u.ScaledBy(40/scale)).Plus(
v.ScaledBy(-(Style::DefaultTextHeight())/scale)), u, v,
hcsDatum);
}
}
}
void GraphicsWindow::Paint() {
ssassert(window != NULL && canvas != NULL,
"Cannot paint without window and canvas");
havePainted = true;
Camera camera = GetCamera();
Lighting lighting = GetLighting();
if(!SS.ActiveGroupsOkay()) {
// Draw a different background whenever we're having solve problems.
RgbaColor bgColor = Style::Color(Style::DRAW_ERROR);
bgColor = RgbaColor::FromFloat(0.4f*bgColor.redF(),
0.4f*bgColor.greenF(),
0.4f*bgColor.blueF());
lighting.backgroundColor = bgColor;
// And show the text window, which has info to debug it
ForceTextWindowShown();
}
canvas->SetLighting(lighting);
canvas->SetCamera(camera);
canvas->StartFrame();
// Draw the 3d objects.
Draw(canvas.get());
canvas->FlushFrame();
// Draw the 2d UI overlay.
camera.LoadIdentity();
camera.offset.x = -(double)camera.width / 2.0;
camera.offset.y = -(double)camera.height / 2.0;
canvas->SetCamera(camera);
UiCanvas uiCanvas = {};
uiCanvas.canvas = canvas;
// If a marquee selection is in progress, then draw the selection
// rectangle, as an outline and a transparent fill.
if(pending.operation == Pending::DRAGGING_MARQUEE) {
Point2d begin = ProjectPoint(orig.marqueePoint);
uiCanvas.DrawRect((int)orig.mouse.x + (int)camera.width / 2,
(int)begin.x + (int)camera.width / 2,
(int)orig.mouse.y + (int)camera.height / 2,
(int)begin.y + (int)camera.height / 2,
/*fillColor=*/Style::Color(Style::HOVERED).WithAlpha(25),
/*outlineColor=*/Style::Color(Style::HOVERED));
}
// If we've had a screenshot requested, take it now, before the UI is overlaid.
if(!SS.screenshotFile.IsEmpty()) {
FILE *f = OpenFile(SS.screenshotFile, "wb");
if(!f || !canvas->ReadFrame()->WritePng(f, /*flip=*/true)) {
Error("Couldn't write to '%s'", SS.screenshotFile.raw.c_str());
}
if(f) fclose(f);
SS.screenshotFile.Clear();
}
// And finally the toolbar.
if(SS.showToolbar) {
canvas->SetCamera(camera);
ToolbarDraw(&uiCanvas);
}
canvas->FlushFrame();
canvas->FinishFrame();
canvas->Clear();
}
void GraphicsWindow::Invalidate(bool clearPersistent) {
if(window) {
if(clearPersistent) {
persistentDirty = true;
}
window->Invalidate();
}
}