//----------------------------------------------------------------------------- // 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 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 MAX_SELECTABLE_FACES faces may be selected at a time. unsigned 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++; // See also GraphicsWindow::GroupSelection "if(e->IsFace())" // and Group::DrawMesh "case DrawMeshAs::SELECTED:" if(c >= MAX_SELECTABLE_FACES) 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), ¶llel); 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(); } }