//----------------------------------------------------------------------------- // Backend-agnostic rendering interface, and various backends we use. // // Copyright 2016 whitequark //----------------------------------------------------------------------------- #include "solvespace.h" namespace SolveSpace { //----------------------------------------------------------------------------- // Camera transformations. //----------------------------------------------------------------------------- Point2d Camera::ProjectPoint(Vector p) const { Vector p3 = ProjectPoint3(p); Point2d p2 = { p3.x, p3.y }; return p2; } Vector Camera::ProjectPoint3(Vector p) const { double w; Vector r = ProjectPoint4(p, &w); return r.ScaledBy(scale/w); } Vector Camera::ProjectPoint4(Vector p, double *w) const { 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*tangent*scale; return r; } Vector Camera::UnProjectPoint(Point2d p) const { Vector orig = offset.ScaledBy(-1); // Note that we're 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 Camera::UnProjectPoint3(Vector p) const { p.z = p.z / (scale - p.z * tangent * scale); double w = 1 + p.z * tangent * 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; } Vector Camera::VectorFromProjs(Vector rightUpForward) const { Vector n = projRight.Cross(projUp); Vector r = (projRight.ScaledBy(rightUpForward.x)); r = r.Plus(projUp.ScaledBy(rightUpForward.y)); r = r.Plus(n.ScaledBy(rightUpForward.z)); return r; } Vector Camera::AlignToPixelGrid(Vector v) const { if(!hasPixels) return v; v = ProjectPoint3(v); v.x = floor(v.x) + 0.5; v.y = floor(v.y) + 0.5; return UnProjectPoint3(v); } void Camera::LoadIdentity() { offset = { 0.0, 0.0, 0.0 }; projRight = { 1.0, 0.0, 0.0 }; projUp = { 0.0, 1.0, 0.0 }; scale = 1.0; tangent = 0.0; } void Camera::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); } //----------------------------------------------------------------------------- // Stroke and fill caching. //----------------------------------------------------------------------------- bool Canvas::Stroke::Equals(const Stroke &other) const { return (layer == other.layer && zIndex == other.zIndex && color.Equals(other.color) && width == other.width && stipplePattern == other.stipplePattern && stippleScale == other.stippleScale); } bool Canvas::Fill::Equals(const Fill &other) const { return (layer == other.layer && zIndex == other.zIndex && color.Equals(other.color) && pattern == other.pattern); } Canvas::hStroke Canvas::GetStroke(const Stroke &stroke) { for(const Stroke &s : strokes) { if(s.Equals(stroke)) return s.h; } Stroke strokeCopy = stroke; return strokes.AddAndAssignId(&strokeCopy); } Canvas::hFill Canvas::GetFill(const Fill &fill) { for(const Fill &f : fills) { if(f.Equals(fill)) return f.h; } Fill fillCopy = fill; return fills.AddAndAssignId(&fillCopy); } //----------------------------------------------------------------------------- // A wrapper around Canvas that simplifies drawing UI in screen coordinates //----------------------------------------------------------------------------- void UiCanvas::DrawLine(int x1, int y1, int x2, int y2, RgbaColor color, int width) { Vector va = { (double)x1 + 0.5, (double)Flip(y1) + 0.5, 0.0 }, vb = { (double)x2 + 0.5, (double)Flip(y2) + 0.5, 0.0 }; Canvas::Stroke stroke = {}; stroke.layer = Canvas::Layer::FRONT; stroke.width = (double)width; stroke.color = color; Canvas::hStroke hcs = canvas->GetStroke(stroke); canvas->DrawLine(va, vb, hcs); } void UiCanvas::DrawRect(int l, int r, int t, int b, RgbaColor fillColor, RgbaColor outlineColor) { Vector va = { (double)l + 0.5, (double)Flip(b) + 0.5, 0.0 }, vb = { (double)l + 0.5, (double)Flip(t) + 0.5, 0.0 }, vc = { (double)r + 0.5, (double)Flip(t) + 0.5, 0.0 }, vd = { (double)r + 0.5, (double)Flip(b) + 0.5, 0.0 }; if(!fillColor.IsEmpty()) { Canvas::Fill fill = {}; fill.layer = Canvas::Layer::FRONT; fill.color = fillColor; Canvas::hFill hcf = canvas->GetFill(fill); canvas->DrawQuad(va, vb, vc, vd, hcf); } if(!outlineColor.IsEmpty()) { Canvas::Stroke stroke = {}; stroke.layer = Canvas::Layer::FRONT; stroke.width = 1.0; stroke.color = outlineColor; Canvas::hStroke hcs = canvas->GetStroke(stroke); canvas->DrawLine(va, vb, hcs); canvas->DrawLine(vb, vc, hcs); canvas->DrawLine(vc, vd, hcs); canvas->DrawLine(vd, va, hcs); } } void UiCanvas::DrawPixmap(std::shared_ptr pm, int x, int y) { Canvas::Fill fill = {}; fill.layer = Canvas::Layer::FRONT; fill.color = { 0, 0, 0, 255 }; Canvas::hFill hcf = canvas->GetFill(fill); canvas->DrawPixmap(pm, { (double)x, (double)(flip ? Flip(y) - pm->height : y), 0.0 }, { (double)pm->width, 0.0, 0.0 }, { 0.0, (double)pm->height, 0.0 }, { 0.0, 1.0 }, { 1.0, 0.0 }, hcf); } void UiCanvas::DrawBitmapChar(char32_t codepoint, int x, int y, RgbaColor color) { BitmapFont *font = BitmapFont::Builtin(); Canvas::Fill fill = {}; fill.layer = Canvas::Layer::FRONT; fill.color = color; Canvas::hFill hcf = canvas->GetFill(fill); if(codepoint >= 0xe000 && codepoint <= 0xefff) { // Special character, like a checkbox or a radio button x -= 3; } double s0, t0, s1, t1; size_t w, h; if(font->LocateGlyph(codepoint, &s0, &t0, &s1, &t1, &w, &h)) { // LocateGlyph modified the texture, reload it. canvas->InvalidatePixmap(font->texture); } canvas->DrawPixmap(font->texture, { (double)x, (double)Flip(y), 0.0 }, { (double)w, 0.0, 0.0 }, { 0.0, (double) h, 0.0 }, { s0, t1 }, { s1, t0 }, hcf); } void UiCanvas::DrawBitmapText(const std::string &str, int x, int y, RgbaColor color) { BitmapFont *font = BitmapFont::Builtin(); for(char32_t codepoint : ReadUTF8(str)) { DrawBitmapChar(codepoint, x, y, color); x += font->GetWidth(codepoint) * 8; } } //----------------------------------------------------------------------------- // A canvas that performs picking against drawn geometry. //----------------------------------------------------------------------------- void ObjectPicker::DoCompare(double distance, int zIndex, int comparePosition) { if(distance > selRadius) return; if((zIndex == maxZIndex && distance < minDistance) || (zIndex > maxZIndex)) { minDistance = distance; maxZIndex = zIndex; position = comparePosition; } } void ObjectPicker::DoQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, int zIndex, int comparePosition) { Point2d corners[4] = { camera.ProjectPoint(a), camera.ProjectPoint(b), camera.ProjectPoint(c), camera.ProjectPoint(d) }; double minNegative = VERY_NEGATIVE, maxPositive = VERY_POSITIVE; for(int i = 0; i < 4; i++) { Point2d ap = corners[i], bp = corners[(i + 1) % 4]; double distance = point.DistanceToLineSigned(ap, bp.Minus(ap), /*asSegment=*/true); if(distance < 0) minNegative = std::max(minNegative, distance); if(distance > 0) maxPositive = std::min(maxPositive, distance); } bool insideQuad = (minNegative == VERY_NEGATIVE || maxPositive == VERY_POSITIVE); if(insideQuad) { DoCompare(0.0, zIndex, comparePosition); } else { double distance = std::min(fabs(minNegative), fabs(maxPositive)); DoCompare(distance, zIndex, comparePosition); } } void ObjectPicker::DrawLine(const Vector &a, const Vector &b, hStroke hcs) { Stroke *stroke = strokes.FindById(hcs); Point2d ap = camera.ProjectPoint(a); Point2d bp = camera.ProjectPoint(b); double distance = point.DistanceToLine(ap, bp.Minus(ap), /*asSegment=*/true); DoCompare(distance - stroke->width / 2.0, stroke->zIndex); } void ObjectPicker::DrawEdges(const SEdgeList &el, hStroke hcs) { Stroke *stroke = strokes.FindById(hcs); for(const SEdge &e : el.l) { Point2d ap = camera.ProjectPoint(e.a); Point2d bp = camera.ProjectPoint(e.b); double distance = point.DistanceToLine(ap, bp.Minus(ap), /*asSegment=*/true); DoCompare(distance - stroke->width / 2.0, stroke->zIndex, e.auxB); } } void ObjectPicker::DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) { ssassert(false, "Not implemented"); } void ObjectPicker::DrawVectorText(const std::string &text, double height, const Vector &o, const Vector &u, const Vector &v, hStroke hcs) { Stroke *stroke = strokes.FindById(hcs); double w = VectorFont::Builtin()-> GetWidth(height, text), h = VectorFont::Builtin()->GetHeight(height); DoQuad(o, o.Plus(v.ScaledBy(h)), o.Plus(u.ScaledBy(w)).Plus(v.ScaledBy(h)), o.Plus(u.ScaledBy(w)), stroke->zIndex); } void ObjectPicker::DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, hFill hcf) { Fill *fill = fills.FindById(hcf); DoQuad(a, b, c, d, fill->zIndex); } void ObjectPicker::DrawPoint(const Vector &o, double s, hFill hcf) { Fill *fill = fills.FindById(hcf); double distance = point.DistanceTo(camera.ProjectPoint(o)) - s / 2; DoCompare(distance, fill->zIndex); } void ObjectPicker::DrawPolygon(const SPolygon &p, hFill hcf) { ssassert(false, "Not implemented"); } void ObjectPicker::DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack, hStroke hcsTriangles) { ssassert(false, "Not implemented"); } void ObjectPicker::DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) { ssassert(false, "Not implemented"); } void ObjectPicker::DrawPixmap(std::shared_ptr pm, const Vector &o, const Vector &u, const Vector &v, const Point2d &ta, const Point2d &tb, Canvas::hFill hcf) { ssassert(false, "Not implemented"); } bool ObjectPicker::Pick(std::function drawFn) { minDistance = VERY_POSITIVE; drawFn(); return minDistance < selRadius; } }