diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c9cd204..72c5f62 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -158,6 +158,7 @@ set(solvespace_SOURCES view.cpp render/render.cpp render/rendergl1.cpp + render/render2d.cpp srf/boolean.cpp srf/curve.cpp srf/merge.cpp diff --git a/src/bsp.cpp b/src/bsp.cpp index 06bca1b..b3c6ccb 100644 --- a/src/bsp.cpp +++ b/src/bsp.cpp @@ -11,7 +11,7 @@ SBsp2 *SBsp2::Alloc() { return (SBsp2 *)AllocTemporary(sizeof(SBsp2)); } SBsp3 *SBsp3::Alloc() { return (SBsp3 *)AllocTemporary(sizeof(SBsp3)); } -SBsp3 *SBsp3::FromMesh(SMesh *m) { +SBsp3 *SBsp3::FromMesh(const SMesh *m) { SBsp3 *bsp3 = NULL; int i; diff --git a/src/export.cpp b/src/export.cpp index d435a8f..c9e7585 100644 --- a/src/export.cpp +++ b/src/export.cpp @@ -407,7 +407,17 @@ void SolveSpaceUI::ExportLinesAndMesh(SEdgeList *sel, SBezierList *sbl, SMesh *s SEdgeList edges = {}; // Split the original edge against the mesh edges.AddEdge(se->a, se->b, se->auxA); - root->OcclusionTestLine(*se, &edges, cnt, /*removeHidden=*/!SS.GW.showHdnLines); + root->OcclusionTestLine(*se, &edges, cnt); + if(SS.GW.showHdnLines) { + for(SEdge &se : edges.l) { + if(se.tag == 1) { + se.auxA = Style::HIDDEN_EDGE; + } + } + } else { + edges.l.RemoveTagged(); + } + // the occlusion test splits unnecessarily; so fix those edges.MergeCollinearSegments(se->a, se->b); cnt++; diff --git a/src/mesh.cpp b/src/mesh.cpp index 39c4aaa..fe84248 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -648,10 +648,10 @@ void SKdNode::SnapToMesh(SMesh *m) { //----------------------------------------------------------------------------- // For all the edges in sel, split them against the given triangle, and test -// them for occlusion. Keep only the visible segments. sel is both our input -// and our output. +// them for occlusion. sel is both our input and our output. tag indicates +// whether an edge is occluded. //----------------------------------------------------------------------------- -void SKdNode::SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr, bool removeHidden) const { +void SKdNode::SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr) const { SEdgeList seln = {}; Vector tn = tr->Normal().WithMagnitude(1); @@ -661,7 +661,8 @@ void SKdNode::SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr, bool remo if(tn.z > LENGTH_EPS) { // If the edge crosses our triangle's plane, then split into above // and below parts. Note that we must preserve auxA, which contains - // the style associated with this line. + // the style associated with this line, as well as the tag, which + // contains the occlusion status. SEdge *se; for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) { double da = (se->a).Dot(tn) - td, @@ -672,12 +673,12 @@ void SKdNode::SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr, bool remo Vector m = Vector::AtIntersectionOfPlaneAndLine( tn, td, se->a, se->b, NULL); - seln.AddEdge(m, se->b, se->auxA); + seln.AddEdge(m, se->b, se->auxA, 0, se->tag); se->b = m; } } for(se = seln.l.First(); se; se = seln.l.NextAfter(se)) { - sel->AddEdge(se->a, se->b, se->auxA); + sel->AddEdge(se->a, se->b, se->auxA, 0, se->tag); } seln.Clear(); @@ -723,48 +724,49 @@ void SKdNode::SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr, bool remo double dab = (db - da); Vector spl = ((se->a).ScaledBy( db/dab)).Plus( (se->b).ScaledBy(-da/dab)); - seln.AddEdge(spl, se->b, se->auxA); + seln.AddEdge(spl, se->b, se->auxA, 0, se->tag); se->b = spl; } } for(se = seln.l.First(); se; se = seln.l.NextAfter(se)) { // The split pieces are all behind the triangle, since only // edges behind the triangle got split. So their auxB is 0. - sel->AddEdge(se->a, se->b, se->auxA, 0); + sel->AddEdge(se->a, se->b, se->auxA, 0, se->tag); } seln.Clear(); } for(se = sel->l.First(); se; se = sel->l.NextAfter(se)) { + bool occluded; if(se->auxB) { // Lies above or on the triangle plane, so triangle doesn't // occlude it. - se->tag = 0; + occluded = false; } else { // Test the segment to see if it lies outside the triangle // (i.e., outside wrt at least one edge), and keep it only // then. Point2d pt = ((se->a).Plus(se->b).ScaledBy(0.5)).ProjectXy(); - se->tag = 1; + occluded = true; for(i = 0; i < 3; i++) { // If the test point lies on the boundary of our triangle, // then we still discard the edge. - if(n[i].Dot(pt) - d[i] > LENGTH_EPS) se->tag = 0; + if(n[i].Dot(pt) - d[i] > LENGTH_EPS) occluded = false; } } - if(!removeHidden && se->tag == 1) - se->auxA = Style::HIDDEN_EDGE; + + if(occluded) { + se->tag = 1; + } } - if(removeHidden) - sel->l.RemoveTagged(); } } //----------------------------------------------------------------------------- // Given an edge orig, occlusion test it against our mesh. We output an edge -// list in sel, containing the visible portions of that edge. +// list in sel, where only invisible portions of the edge are tagged. //----------------------------------------------------------------------------- -void SKdNode::OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt, bool removeHidden) const { +void SKdNode::OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt) const { if(gt && lt) { double ac = (orig.a).Element(which), bc = (orig.b).Element(which); @@ -774,13 +776,13 @@ void SKdNode::OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt, bool remove bc < c + KDTREE_EPS || which == 2) { - lt->OcclusionTestLine(orig, sel, cnt, removeHidden); + lt->OcclusionTestLine(orig, sel, cnt); } if(ac > c - KDTREE_EPS || bc > c - KDTREE_EPS || which == 2) { - gt->OcclusionTestLine(orig, sel, cnt, removeHidden); + gt->OcclusionTestLine(orig, sel, cnt); } } else { STriangleLl *ll; @@ -789,7 +791,7 @@ void SKdNode::OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt, bool remove if(tr->tag == cnt) continue; - SplitLinesAgainstTriangle(sel, tr, removeHidden); + SplitLinesAgainstTriangle(sel, tr); tr->tag = cnt; } } diff --git a/src/polygon.cpp b/src/polygon.cpp index 3712627..ca7afc8 100644 --- a/src/polygon.cpp +++ b/src/polygon.cpp @@ -156,8 +156,9 @@ void SEdgeList::Clear() { l.Clear(); } -void SEdgeList::AddEdge(Vector a, Vector b, int auxA, int auxB) { +void SEdgeList::AddEdge(Vector a, Vector b, int auxA, int auxB, int tag) { SEdge e = {}; + e.tag = tag; e.a = a; e.b = b; e.auxA = auxA; diff --git a/src/polygon.h b/src/polygon.h index c02d643..bc3e085 100644 --- a/src/polygon.h +++ b/src/polygon.h @@ -50,7 +50,7 @@ public: List l; void Clear(); - void AddEdge(Vector a, Vector b, int auxA=0, int auxB=0); + void AddEdge(Vector a, Vector b, int auxA=0, int auxB=0, int tag=0); bool AssemblePolygon(SPolygon *dest, SEdge *errorAt, bool keepDir=false) const; bool AssembleContour(Vector first, Vector last, SContour *dest, SEdge *errorAt, bool keepDir) const; @@ -226,7 +226,7 @@ public: SBsp2 *edges; static SBsp3 *Alloc(); - static SBsp3 *FromMesh(SMesh *m); + static SBsp3 *FromMesh(const SMesh *m); Vector IntersectionWith(Vector a, Vector b) const; @@ -342,8 +342,8 @@ public: bool *inter, bool *leaky, int auxA = 0) const; void MakeOutlinesInto(SOutlineList *sel, EdgeKind tagKind) const; - void OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt, bool removeHidden) const; - void SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr, bool removeHidden) const; + void OcclusionTestLine(SEdge orig, SEdgeList *sel, int cnt) const; + void SplitLinesAgainstTriangle(SEdgeList *sel, STriangle *tr) const; void SnapToMesh(SMesh *m); void SnapToVertex(Vector v, SMesh *extras); diff --git a/src/render/render.cpp b/src/render/render.cpp index e19baef..62f01b3 100644 --- a/src/render/render.cpp +++ b/src/render/render.cpp @@ -78,6 +78,27 @@ Vector Camera::AlignToPixelGrid(Vector v) const { return UnProjectPoint3(v); } +SBezier Camera::ProjectBezier(SBezier b) const { + Quaternion q = Quaternion::From(projRight, projUp); + q = q.Inverse(); + // we want Q*(p - o) = Q*p - Q*o + b = b.TransformedBy(q.Rotate(offset).ScaledBy(scale), q, scale); + for(int i = 0; i <= b.deg; i++) { + Vector4 ct = Vector4::From(b.weight[i], b.ctrl[i]); + // so the desired curve, before perspective, is + // (x/w, y/w, z/w) + // and after perspective is + // ((x/w)/(1 - (z/w)*tangent, ... + // = (x/(w - z*tangent), ... + // so we want to let w' = w - z*tangent + ct.w = ct.w - ct.z*tangent; + + b.ctrl[i] = ct.PerspectiveProject(); + b.weight[i] = ct.w; + } + return b; +} + void Camera::LoadIdentity() { offset = { 0.0, 0.0, 0.0 }; projRight = { 1.0, 0.0, 0.0 }; diff --git a/src/render/render.h b/src/render/render.h index b0b4b24..621951d 100644 --- a/src/render/render.h +++ b/src/render/render.h @@ -7,6 +7,10 @@ #ifndef SOLVESPACE_RENDER_H #define SOLVESPACE_RENDER_H +//----------------------------------------------------------------------------- +// Interfaces and utilities common for all renderers. +//----------------------------------------------------------------------------- + enum class StipplePattern : uint32_t; // A mapping from 3d sketch coordinates to 2d screen coordinates, using @@ -31,6 +35,8 @@ public: Vector VectorFromProjs(Vector rightUpForward) const; Vector AlignToPixelGrid(Vector v) const; + SBezier ProjectBezier(SBezier b) const; + void LoadIdentity(); void NormalizeProjectionVectors(); }; @@ -204,6 +210,70 @@ public: bool Pick(std::function drawFn); }; +// A canvas that renders onto a 2d surface, performing z-index sorting, occlusion testing, etc, +// on the CPU. +class SurfaceRenderer : public Canvas { +public: + Camera camera; + Lighting lighting; + // Chord tolerance, for converting beziers to pwl. + double chordTolerance; + // Render lists. + handle_map edges; + handle_map beziers; + SMesh mesh; + // State. + BBox bbox; + + SurfaceRenderer() : camera(), lighting(), chordTolerance(), mesh(), bbox() {} + virtual void Clear(); + + // Canvas interface. + const Camera &GetCamera() const override { return camera; } + + void DrawLine(const Vector &a, const Vector &b, hStroke hcs) override; + void DrawEdges(const SEdgeList &el, hStroke hcs) override; + bool DrawBeziers(const SBezierList &bl, hStroke hcs) override; + void DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) override; + void DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) override; + + void DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) override; + void DrawPoint(const Vector &o, double s, hFill hcf) override; + void DrawPolygon(const SPolygon &p, hFill hcf) override; + void DrawMesh(const SMesh &m, hFill hcfFront, hFill hcfBack, hStroke hcsTriangles) override; + void DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) override; + + void DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) override; + void InvalidatePixmap(std::shared_ptr pm) override; + + // Geometry manipulation. + void CalculateBBox(); + void ConvertBeziersToEdges(); + void CullOccludedStrokes(); + + // Renderer operations. + void OutputInPaintOrder(); + + virtual bool CanOutputCurves() const = 0; + virtual bool CanOutputTriangles() const = 0; + + virtual void OutputStart() = 0; + virtual void OutputBezier(const SBezier &b, hStroke hcs) = 0; + virtual void OutputTriangle(const STriangle &tr) = 0; + virtual void OutputEnd() = 0; + + void OutputBezierAsNonrationalCubic(const SBezier &b, hStroke hcs); +}; + +//----------------------------------------------------------------------------- +// 3d renderers. +//----------------------------------------------------------------------------- + // An offscreen renderer based on OpenGL framebuffers. class GlOffscreen { public: diff --git a/src/render/render2d.cpp b/src/render/render2d.cpp new file mode 100644 index 0000000..53c2952 --- /dev/null +++ b/src/render/render2d.cpp @@ -0,0 +1,411 @@ +//----------------------------------------------------------------------------- +// Rendering projections to 2d surfaces: z-sorting, occlusion testing, etc. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include "solvespace.h" + +namespace SolveSpace { + +// FIXME: The export coordinate system has a different handedness than display +// coordinate system; lighting and occlusion calculations are right-handed. +static Vector ProjectPoint3RH(const Camera &camera, Vector p) { + p = p.Plus(camera.offset); + + Vector r; + r.x = p.Dot(camera.projRight); + r.y = p.Dot(camera.projUp); + r.z = p.Dot(camera.projRight.Cross(camera.projUp)); + + double w = 1 + r.z*camera.tangent*camera.scale; + return r.ScaledBy(camera.scale/w); +} + +//----------------------------------------------------------------------------- +// Accumulation of geometry +//----------------------------------------------------------------------------- + +void SurfaceRenderer::DrawLine(const Vector &a, const Vector &b, hStroke hcs) { + edges[hcs].AddEdge(ProjectPoint3RH(camera, a), + ProjectPoint3RH(camera, b)); +} + +void SurfaceRenderer::DrawEdges(const SEdgeList &el, hStroke hcs) { + for(const SEdge &e : el.l) { + edges[hcs].AddEdge(ProjectPoint3RH(camera, e.a), + ProjectPoint3RH(camera, e.b)); + } +} + +bool SurfaceRenderer::DrawBeziers(const SBezierList &bl, hStroke hcs) { + if(!CanOutputCurves()) + return false; + + for(const SBezier &b : bl.l) { + SBezier pb = camera.ProjectBezier(b); + beziers[hcs].l.Add(&pb); + } + return true; +} + +void SurfaceRenderer::DrawOutlines(const SOutlineList &ol, hStroke hcs, DrawOutlinesAs drawAs) { + Vector projDir = camera.projRight.Cross(camera.projUp); + for(const SOutline &o : ol.l) { + if(drawAs == DrawOutlinesAs::EMPHASIZED_AND_CONTOUR && + !(o.IsVisible(projDir) || o.tag != 0)) + continue; + if(drawAs == DrawOutlinesAs::EMPHASIZED_WITHOUT_CONTOUR && + !(!o.IsVisible(projDir) && o.tag != 0)) + continue; + if(drawAs == DrawOutlinesAs::CONTOUR_ONLY && + !(o.IsVisible(projDir))) + continue; + + edges[hcs].AddEdge(ProjectPoint3RH(camera, o.a), + ProjectPoint3RH(camera, o.b)); + } +} + +void SurfaceRenderer::DrawVectorText(const std::string &text, double height, + const Vector &o, const Vector &u, const Vector &v, + hStroke hcs) { + auto traceEdge = [&](Vector a, Vector b) { + edges[hcs].AddEdge(ProjectPoint3RH(camera, a), + ProjectPoint3RH(camera, b)); + }; + VectorFont::Builtin()->Trace(height, o, u, v, text, traceEdge, camera); +} + +void SurfaceRenderer::DrawQuad(const Vector &a, const Vector &b, const Vector &c, const Vector &d, + hFill hcf) { + Fill *fill = fills.FindById(hcf); + ssassert(fill->layer == Layer::NORMAL || + fill->layer == Layer::DEPTH_ONLY || + fill->layer == Layer::FRONT || + fill->layer == Layer::BACK, "Unexpected mesh layer"); + + Vector zOffset = {}; + if(fill->layer == Layer::BACK) { + zOffset.z -= 1e6; + } else if(fill->layer == Layer::FRONT) { + zOffset.z += 1e6; + } + zOffset.z += camera.scale * fill->zIndex; + + STriMeta meta = {}; + if(fill->layer != Layer::DEPTH_ONLY) { + meta.color = fill->color; + } + Vector ta = ProjectPoint3RH(camera, a).Plus(zOffset), + tb = ProjectPoint3RH(camera, b).Plus(zOffset), + tc = ProjectPoint3RH(camera, c).Plus(zOffset), + td = ProjectPoint3RH(camera, d).Plus(zOffset); + mesh.AddTriangle(meta, tc, tb, ta); + mesh.AddTriangle(meta, ta, td, tc); +} + +void SurfaceRenderer::DrawPoint(const Vector &o, double s, hFill hcf) { + Vector u = camera.projRight.ScaledBy(1 / camera.scale * s), + v = camera.projUp.ScaledBy(1 / camera.scale * s); + DrawQuad(o.Minus(u).Minus(v), o.Minus(u).Plus(v), + o.Plus(u).Plus(v), o.Plus(u).Minus(v), hcf); +} + +void SurfaceRenderer::DrawPolygon(const SPolygon &p, hFill hcf) { + SMesh m = {}; + p.TriangulateInto(&m); + DrawMesh(m, hcf, {}, {}); + m.Clear(); +} + +void SurfaceRenderer::DrawMesh(const SMesh &m, + hFill hcfFront, hFill hcfBack, hStroke hcsTriangles) { + Fill *fill = fills.FindById(hcfFront); + ssassert(fill->layer == Layer::NORMAL || + fill->layer == Layer::DEPTH_ONLY, "Unexpected mesh layer"); + + Vector l0 = (lighting.lightDirection[0]).WithMagnitude(1), + l1 = (lighting.lightDirection[1]).WithMagnitude(1); + for(STriangle tr : m.l) { + tr.a = ProjectPoint3RH(camera, tr.a); + tr.b = ProjectPoint3RH(camera, tr.b); + tr.c = ProjectPoint3RH(camera, tr.c); + + if(CanOutputTriangles() && fill->layer == Layer::NORMAL) { + if(fill->color.IsEmpty()) { + // Compute lighting, since we're going to draw the shaded triangles. + Vector n = tr.Normal().WithMagnitude(1); + double intensity = lighting.ambientIntensity + + max(0.0, (lighting.lightIntensity[0])*(n.Dot(l0))) + + max(0.0, (lighting.lightIntensity[1])*(n.Dot(l1))); + double r = min(1.0, tr.meta.color.redF() * intensity), + g = min(1.0, tr.meta.color.greenF() * intensity), + b = min(1.0, tr.meta.color.blueF() * intensity); + tr.meta.color = RGBf(r, g, b); + } else { + // We're going to draw this triangle, but it's not shaded. + tr.meta.color = fill->color; + } + } else { + // This triangle is just for occlusion testing. + tr.meta.color = {}; + } + mesh.AddTriangle(&tr); + } + + if(hcsTriangles.v != 0) { + for(const STriangle &tr : m.l) { + edges[hcsTriangles].AddEdge(ProjectPoint3RH(camera, tr.a), + ProjectPoint3RH(camera, tr.b)); + edges[hcsTriangles].AddEdge(ProjectPoint3RH(camera, tr.b), + ProjectPoint3RH(camera, tr.c)); + edges[hcsTriangles].AddEdge(ProjectPoint3RH(camera, tr.c), + ProjectPoint3RH(camera, tr.a)); + } + } +} + +void SurfaceRenderer::DrawFaces(const SMesh &m, const std::vector &faces, hFill hcf) { + Fill *fill = fills.FindById(hcf); + ssassert(fill->layer == Layer::NORMAL || + fill->layer == Layer::DEPTH_ONLY, "Unexpected mesh layer"); + + Vector zOffset = {}; + zOffset.z += camera.scale * fill->zIndex; + + size_t facesSize = faces.size(); + for(STriangle tr : m.l) { + uint32_t face = tr.meta.face; + for(size_t j = 0; j < facesSize; j++) { + if(faces[j] != face) continue; + if(!fill->color.IsEmpty()) { + tr.meta.color = fill->color; + } + mesh.AddTriangle(tr.meta, + ProjectPoint3RH(camera, tr.a).Plus(zOffset), + ProjectPoint3RH(camera, tr.b).Plus(zOffset), + ProjectPoint3RH(camera, tr.c).Plus(zOffset)); + break; + } + } +} + +void SurfaceRenderer::DrawPixmap(std::shared_ptr pm, + const Vector &o, const Vector &u, const Vector &v, + const Point2d &ta, const Point2d &tb, hFill hcf) { + ssassert(false, "Not implemented"); +} + +void SurfaceRenderer::InvalidatePixmap(std::shared_ptr pm) { + ssassert(false, "Not implemented"); +} + +//----------------------------------------------------------------------------- +// Processing of geometry +//----------------------------------------------------------------------------- + +void SurfaceRenderer::CalculateBBox() { + bbox.minp = Vector::From(VERY_POSITIVE, VERY_POSITIVE, VERY_POSITIVE); + bbox.maxp = Vector::From(VERY_NEGATIVE, VERY_NEGATIVE, VERY_NEGATIVE); + + for(auto &it : edges) { + SEdgeList &el = it.second; + for(SEdge &e : el.l) { + bbox.Include(e.a); + bbox.Include(e.b); + } + } + + for(auto &it : beziers) { + SBezierList &bl = it.second; + for(SBezier &b : bl.l) { + for(int i = 0; i <= b.deg; i++) { + bbox.Include(b.ctrl[i]); + } + } + } + + for(STriangle &tr : mesh.l) { + for(int i = 0; i < 3; i++) { + bbox.Include(tr.vertices[i]); + } + } +} + + +void SurfaceRenderer::ConvertBeziersToEdges() { + for(auto &it : beziers) { + hStroke hcs = it.first; + SBezierList &bl = it.second; + + SEdgeList &el = edges[hcs]; + for(const SBezier &b : bl.l) { + if(b.deg == 1) { + el.AddEdge(b.ctrl[0], b.ctrl[1]); + } else { + List lv = {}; + b.MakePwlInto(&lv, chordTolerance); + for(int i = 1; i < lv.n; i++) { + el.AddEdge(lv.elem[i-1], lv.elem[i]); + } + lv.Clear(); + } + } + bl.l.Clear(); + } + beziers.clear(); +} + +void SurfaceRenderer::CullOccludedStrokes() { + // Perform occlusion testing, if necessary. + if(mesh.l.n == 0) return; + + // We can't perform hidden line removal on exact curves. + ConvertBeziersToEdges(); + + // Remove hidden lines (on NORMAL layers), or remove visible lines (on OCCLUDED layers). + SKdNode *root = SKdNode::From(&mesh); + root->ClearTags(); + + int cnt = 1234; + for(auto &eit : edges) { + hStroke hcs = eit.first; + SEdgeList &el = eit.second; + + Stroke *stroke = strokes.FindById(hcs); + if(stroke->layer != Layer::NORMAL && + stroke->layer != Layer::OCCLUDED) continue; + + SEdgeList nel = {}; + for(const SEdge &e : el.l) { + SEdgeList oel = {}; + oel.AddEdge(e.a, e.b); + root->OcclusionTestLine(e, &oel, cnt); + + if(stroke->layer == Layer::OCCLUDED) { + for(SEdge &oe : oel.l) { + oe.tag = !oe.tag; + } + } + oel.l.RemoveTagged(); + + oel.MergeCollinearSegments(e.a, e.b); + for(const SEdge &oe : oel.l) { + nel.AddEdge(oe.a, oe.b); + } + + oel.Clear(); + cnt++; + } + + el.l.Clear(); + el.l = nel.l; + } +} + +void SurfaceRenderer::OutputInPaintOrder() { + // Sort our strokes in paint order. + std::vector> paintOrder; + paintOrder.emplace_back(Layer::NORMAL, 0); // mesh + for(const Stroke &cs : strokes) { + paintOrder.emplace_back(cs.layer, cs.zIndex); + } + + const Layer stackup[] = { + Layer::BACK, Layer::NORMAL, Layer::DEPTH_ONLY, Layer::OCCLUDED, Layer::FRONT + }; + std::sort(paintOrder.begin(), paintOrder.end(), + [&](std::pair a, std::pair b) { + Layer aLayer = a.first, + bLayer = b.first; + int aZIndex = a.second, + bZIndex = b.second; + + int aLayerIndex = + std::find(std::begin(stackup), std::end(stackup), aLayer) - std::begin(stackup); + int bLayerIndex = + std::find(std::begin(stackup), std::end(stackup), bLayer) - std::begin(stackup); + if(aLayerIndex == bLayerIndex) { + return aZIndex < bZIndex; + } else { + return aLayerIndex < bLayerIndex; + } + }); + + auto last = std::unique(paintOrder.begin(), paintOrder.end()); + paintOrder.erase(last, paintOrder.end()); + + // Output geometry in paint order. + OutputStart(); + for(auto &it : paintOrder) { + Layer layer = it.first; + int zIndex = it.second; + + if(layer == Layer::NORMAL && zIndex == 0) { + SMesh mp = {}; + SBsp3 *bsp = SBsp3::FromMesh(&mesh); + if(bsp) bsp->GenerateInPaintOrder(&mp); + + for(const STriangle &tr : mp.l) { + // Cull back-facing and invisible triangles. + if(tr.Normal().z < 0) continue; + if(tr.meta.color.IsEmpty()) continue; + OutputTriangle(tr); + } + } + + for(auto eit : edges) { + hStroke hcs = eit.first; + const SEdgeList &el = eit.second; + + Stroke *stroke = strokes.FindById(hcs); + if(stroke->layer != layer || stroke->zIndex != zIndex) continue; + + for(const SEdge &e : el.l) { + OutputBezier(SBezier::From(e.a, e.b), hcs); + } + } + + for(auto &bit : beziers) { + hStroke hcs = bit.first; + const SBezierList &bl = bit.second; + + Stroke *stroke = strokes.FindById(hcs); + if(stroke->layer != layer || stroke->zIndex != zIndex) continue; + + for(const SBezier &b : bl.l) { + OutputBezier(b, hcs); + } + } + } + OutputEnd(); +} + +void SurfaceRenderer::Clear() { + for(auto &eit : edges) { + SEdgeList &el = eit.second; + el.l.Clear(); + } + edges.clear(); + + for(auto &bit : beziers) { + SBezierList &bl = bit.second; + bl.l.Clear(); + } + beziers.clear(); + + mesh.Clear(); +} + +void SurfaceRenderer::OutputBezierAsNonrationalCubic(const SBezier &b, hStroke hcs) { + // Arbitrary choice of tolerance; make it a little finer than pwl tolerance since + // it should be easier to achieve that with the smooth curves. + SBezierList bl; + b.MakeNonrationalCubicInto(&bl, chordTolerance / 2); + for(const SBezier &cb : bl.l) { + OutputBezier(cb, hcs); + } + bl.Clear(); +} + +} diff --git a/src/sketch.h b/src/sketch.h index 7423771..3097d3a 100644 --- a/src/sketch.h +++ b/src/sketch.h @@ -43,6 +43,8 @@ enum class StipplePattern : uint32_t { LAST = ZIGZAG }; +std::vector StipplePatternDashes(StipplePattern pattern, double scale); + enum class Command : uint32_t; // All of the hWhatever handles are a 32-bit ID, that is used to represent diff --git a/src/solvespace.h b/src/solvespace.h index 59d6078..ebdba26 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -293,6 +293,14 @@ void vl(); // debug function to validate heaps // End of platform-specific functions //================ +template +struct CompareHandle { + bool operator()(T lhs, T rhs) const { return lhs.v < rhs.v; } +}; + +template +using handle_map = std::map>; + class Group; class SSurface; #include "dsc.h" diff --git a/src/srf/ratpoly.cpp b/src/srf/ratpoly.cpp index 304e124..d9d721b 100644 --- a/src/srf/ratpoly.cpp +++ b/src/srf/ratpoly.cpp @@ -322,6 +322,38 @@ void SBezier::MakePwlInitialWorker(List *l, double ta, double tb, double } } +void SBezier::MakeNonrationalCubicInto(SBezierList *bl, double tolerance, int depth) const { + Vector t0 = TangentAt(0), t1 = TangentAt(1); + // The curve is correct, and the first derivatives are correct, at the + // endpoints. + SBezier bnr = SBezier::From( + Start(), + Start().Plus(t0.ScaledBy(1.0/3)), + Finish().Minus(t1.ScaledBy(1.0/3)), + Finish()); + + bool closeEnough = true; + int i; + for(i = 1; i <= 3; i++) { + double t = i/4.0; + Vector p0 = PointAt(t), + pn = bnr.PointAt(t); + double d = (p0.Minus(pn)).Magnitude(); + if(d > tolerance) { + closeEnough = false; + } + } + + if(closeEnough || depth > 3) { + bl->l.Add(this); + } else { + SBezier bef, aft; + SplitAt(0.5, &bef, &aft); + bef.MakeNonrationalCubicInto(bl, tolerance, depth+1); + aft.MakeNonrationalCubicInto(bl, tolerance, depth+1); + } +} + Vector SSurface::PointAt(Point2d puv) const { return PointAt(puv.x, puv.y); } diff --git a/src/srf/surface.h b/src/srf/surface.h index 5d5f1ae..2eae2a3 100644 --- a/src/srf/surface.h +++ b/src/srf/surface.h @@ -14,6 +14,7 @@ double Bernstein(int k, int deg, double t); double BernsteinDerivative(int k, int deg, double t); +class SBezierList; class SSurface; class SCurvePt; @@ -95,6 +96,7 @@ public: void MakePwlInto(List *l, double chordTol=0) const; void MakePwlWorker(List *l, double ta, double tb, double chordTol) const; void MakePwlInitialWorker(List *l, double ta, double tb, double chordTol) const; + void MakeNonrationalCubicInto(SBezierList *bl, double tolerance, int depth = 0) const; void AllIntersectionsWith(const SBezier *sbb, SPointList *spl) const; void GetBoundingProjd(Vector u, Vector orig, double *umin, double *umax) const; diff --git a/src/util.cpp b/src/util.cpp index 0b35830..34c14ba 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -1085,3 +1085,39 @@ bool BBox::Contains(const Point2d &p, double r) const { p.x <= (maxp.x + r) && p.y <= (maxp.y + r); } + +std::vector SolveSpace::StipplePatternDashes(StipplePattern pattern, double scale) { + // Inkscape ignores all elements that are exactly zero instead of drawing + // them as dots. + double zero = 1e-6; + + std::vector result; + switch(pattern) { + case StipplePattern::CONTINUOUS: + break; + case StipplePattern::SHORT_DASH: + result = { scale, scale * 2.0 }; + break; + case StipplePattern::DASH: + result = { scale, scale }; + break; + case StipplePattern::DASH_DOT: + result = { scale, scale * 0.5, zero, scale * 0.5 }; + break; + case StipplePattern::DASH_DOT_DOT: + result = { scale, scale * 0.5, zero, scale * 0.5, scale * 0.5, zero }; + break; + case StipplePattern::DOT: + result = { zero, scale * 0.5 }; + break; + case StipplePattern::LONG_DASH: + result = { scale * 2.0, scale * 0.5 }; + break; + + case StipplePattern::FREEHAND: + case StipplePattern::ZIGZAG: + ssassert(false, "Freehand and zigzag export not implemented"); + } + + return result; +}