diff --git a/src/cross_section/include/cross_section.h b/src/cross_section/include/cross_section.h index 0a62cc2bf..c38991ead 100644 --- a/src/cross_section/include/cross_section.h +++ b/src/cross_section/include/cross_section.h @@ -16,8 +16,11 @@ #include +#include + #include "clipper2/clipper.core.h" #include "clipper2/clipper.offset.h" +#include "glm/ext/matrix_float3x2.hpp" #include "glm/ext/vector_float2.hpp" #include "public.h" @@ -96,8 +99,10 @@ class CrossSection { ///@} private: - C2::PathsD paths_; + mutable std::shared_ptr paths_; + mutable glm::mat3x2 transform_ = glm::mat3x2(1.0f); CrossSection(C2::PathsD paths); + C2::PathsD GetPaths() const; }; /** @} */ diff --git a/src/cross_section/src/cross_section.cpp b/src/cross_section/src/cross_section.cpp index 5197a56fc..5cea54b39 100644 --- a/src/cross_section/src/cross_section.cpp +++ b/src/cross_section/src/cross_section.cpp @@ -16,11 +16,13 @@ #include +#include #include #include "clipper2/clipper.core.h" #include "clipper2/clipper.engine.h" #include "clipper2/clipper.offset.h" +#include "glm/ext/matrix_float3x2.hpp" #include "glm/ext/vector_float2.hpp" #include "glm/geometric.hpp" #include "glm/glm.hpp" @@ -91,21 +93,42 @@ C2::PathD pathd_of_contour(const SimplePolygon& ctr) { } return p; } + +C2::PathsD transform(const C2::PathsD ps, const glm::mat3x2 m) { + const bool invert = glm::determinant(glm::mat2(m)) < 0; + auto transformed = C2::PathsD(); + transformed.reserve(ps.size()); + for (auto path : ps) { + auto sz = path.size(); + auto s = C2::PathD(sz); + for (int i = 0; i < sz; ++i) { + auto idx = invert ? sz - 1 - i : i; + s[idx] = v2_to_pd(m * glm::vec3(path[i].x, path[i].y, 1)); + } + transformed.push_back(s); + } + return transformed; +} + +std::shared_ptr shared_paths(const C2::PathsD& ps) { + return std::make_shared(ps); +} } // namespace namespace manifold { -CrossSection::CrossSection() { paths_ = C2::PathsD(); } +CrossSection::CrossSection() { paths_ = shared_paths(C2::PathsD()); } CrossSection::~CrossSection() = default; CrossSection::CrossSection(CrossSection&&) noexcept = default; CrossSection& CrossSection::operator=(CrossSection&&) noexcept = default; CrossSection::CrossSection(const CrossSection& other) { - paths_ = C2::PathsD(other.paths_); + paths_ = other.paths_; + transform_ = other.transform_; } -CrossSection::CrossSection(C2::PathsD ps) { paths_ = ps; } +CrossSection::CrossSection(C2::PathsD ps) { paths_ = shared_paths(ps); } CrossSection::CrossSection(const SimplePolygon& contour, FillRule fillrule) { auto ps = C2::PathsD{(pathd_of_contour(contour))}; - paths_ = C2::Union(ps, fr(fillrule), precision_); + paths_ = shared_paths(C2::Union(ps, fr(fillrule), precision_)); } CrossSection::CrossSection(const Polygons& contours, FillRule fillrule) { @@ -114,7 +137,18 @@ CrossSection::CrossSection(const Polygons& contours, FillRule fillrule) { for (auto ctr : contours) { ps.push_back(pathd_of_contour(ctr)); } - paths_ = C2::Union(ps, fr(fillrule), precision_); + paths_ = shared_paths(C2::Union(ps, fr(fillrule), precision_)); +} + +// All access to paths_ should be done through the GetPaths() method, which +// applies the accumulated transform_ +C2::PathsD CrossSection::GetPaths() const { + if (transform_ == glm::mat3x2(1.0f)) { + return *paths_; + } + paths_ = shared_paths(transform(*paths_, transform_)); + transform_ = glm::mat3x2(1.0f); + return *paths_; } CrossSection CrossSection::Square(const glm::vec2 dims, bool center) { @@ -151,8 +185,8 @@ CrossSection CrossSection::Circle(float radius, int circularSegments) { CrossSection CrossSection::Boolean(const CrossSection& second, OpType op) const { auto ct = cliptype_of_op(op); - auto res = C2::BooleanOp(ct, C2::FillRule::Positive, paths_, second.paths_, - precision_); + auto res = C2::BooleanOp(ct, C2::FillRule::Positive, GetPaths(), + second.GetPaths(), precision_); return CrossSection(res); } @@ -163,15 +197,15 @@ CrossSection CrossSection::BatchBoolean( else if (crossSections.size() == 1) return crossSections[0]; - auto subjs = crossSections[0].paths_; + auto subjs = crossSections[0].GetPaths(); int n_clips = 0; for (int i = 1; i < crossSections.size(); ++i) { - n_clips += crossSections[i].paths_.size(); + n_clips += crossSections[i].GetPaths().size(); } auto clips = C2::PathsD(); clips.reserve(n_clips); for (int i = 1; i < crossSections.size(); ++i) { - auto ps = crossSections[i].paths_; + auto ps = crossSections[i].GetPaths(); clips.insert(clips.end(), ps.begin(), ps.end()); } @@ -228,105 +262,77 @@ CrossSection& CrossSection::operator^=(const CrossSection& Q) { CrossSection CrossSection::RectClip(const Rect& rect) const { auto r = C2::RectD(rect.min.x, rect.min.y, rect.max.x, rect.max.y); - auto ps = C2::RectClip(r, paths_, false, precision_); + auto ps = C2::RectClip(r, GetPaths(), false, precision_); return CrossSection(ps); } CrossSection CrossSection::Translate(const glm::vec2 v) const { - auto ps = C2::TranslatePaths(paths_, v.x, v.y); - return CrossSection(ps); + glm::mat3x2 m(1.0f, 0.0f, // + 0.0f, 1.0f, // + v.x, v.y); + return Transform(m); } CrossSection CrossSection::Rotate(float degrees) const { - auto rotated = C2::PathsD(); - rotated.reserve(paths_.size()); auto s = sind(degrees); auto c = cosd(degrees); - for (auto path : paths_) { - auto r = C2::PathD(); - r.reserve(path.size()); - for (auto p : path) { - auto rx = (p.x * c) - (p.y * s); - auto ry = (p.y * c) + (p.x * s); - r.push_back(C2::PointD(rx, ry)); - } - rotated.push_back(r); - } - return CrossSection(rotated); + glm::mat3x2 m(c, s, // + -s, c, // + 0.0f, 0.0f); + return Transform(m); } CrossSection CrossSection::Scale(const glm::vec2 scale) const { - auto scaled = C2::PathsD(); - scaled.reserve(paths_.size()); - for (auto path : paths_) { - auto s = C2::PathD(); - s.reserve(path.size()); - for (auto p : path) { - s.push_back(C2::PointD(p.x * scale.x, p.y * scale.y)); - } - scaled.push_back(s); - } - return CrossSection(scaled); + glm::mat3x2 m(scale.x, 0.0f, // + 0.0f, scale.y, // + 0.0f, 0.0f); + return Transform(m); } CrossSection CrossSection::Mirror(const glm::vec2 ax) const { if (glm::length(ax) == 0.) { return CrossSection(); } - auto mirrored = C2::PathsD(); - mirrored.reserve(paths_.size()); - for (auto path : paths_) { - auto sz = path.size(); - auto m = C2::PathD(sz); - for (int i = 0; i < sz; ++i) { - auto v = v2_of_pd(path[sz - 1 - i]); - m[i] = v2_to_pd(ax * (2 * glm::dot(v, ax) / glm::dot(ax, ax)) - v); - } - mirrored.push_back(m); - } - return CrossSection(mirrored); + auto n = glm::normalize(glm::abs(ax)); + auto m = glm::mat3x2(glm::mat2(1.0f) - 2.0f * glm::outerProduct(n, n)); + return Transform(m); } CrossSection CrossSection::Transform(const glm::mat3x2& m) const { - auto transformed = C2::PathsD(); - transformed.reserve(paths_.size()); - for (auto path : paths_) { - auto s = C2::PathD(); - s.reserve(path.size()); - for (auto p : path) { - s.push_back(v2_to_pd(m * glm::vec3(p.x, p.y, 1))); - } - transformed.push_back(s); - } - return CrossSection(transformed); + auto transformed = CrossSection(); + transformed.transform_ = m * glm::mat3(transform_); + transformed.paths_ = paths_; + return transformed; } CrossSection CrossSection::Simplify(double epsilon) const { - auto ps = SimplifyPaths(paths_, epsilon, false); + auto ps = SimplifyPaths(GetPaths(), epsilon, false); return CrossSection(ps); } CrossSection CrossSection::Offset(double delta, JoinType jointype, double miter_limit, double arc_tolerance) const { - auto ps = C2::InflatePaths(paths_, delta, jt(jointype), C2::EndType::Polygon, - miter_limit, precision_, arc_tolerance); + auto ps = + C2::InflatePaths(GetPaths(), delta, jt(jointype), C2::EndType::Polygon, + miter_limit, precision_, arc_tolerance); return CrossSection(ps); } -double CrossSection::Area() const { return C2::Area(paths_); } +double CrossSection::Area() const { return C2::Area(GetPaths()); } Rect CrossSection::Bounds() const { - auto r = C2::GetBounds(paths_); + auto r = C2::GetBounds(GetPaths()); return Rect({r.left, r.bottom}, {r.right, r.top}); } -bool CrossSection::IsEmpty() const { return paths_.empty(); } +bool CrossSection::IsEmpty() const { return GetPaths().empty(); } Polygons CrossSection::ToPolygons() const { auto polys = Polygons(); - polys.reserve(paths_.size()); - for (auto p : paths_) { + auto paths = GetPaths(); + polys.reserve(paths.size()); + for (auto p : paths) { auto sp = SimplePolygon(); - sp.reserve(paths_.size()); + sp.reserve(p.size()); for (auto v : p) { sp.push_back({v.x, v.y}); } diff --git a/test/cross_section_test.cpp b/test/cross_section_test.cpp index 22263ae6e..1cb90d073 100644 --- a/test/cross_section_test.cpp +++ b/test/cross_section_test.cpp @@ -16,6 +16,7 @@ #include +#include "glm/geometric.hpp" #include "manifold.h" #include "polygon.h" #include "public.h" @@ -30,7 +31,7 @@ using namespace manifold; TEST(CrossSection, MirrorUnion) { auto a = CrossSection::Square({5., 5.}, true); auto b = a.Translate({2.5, 2.5}); - auto cross = a + b + b.Mirror({-1, 1}); + auto cross = a + b + b.Mirror({1, 1}); auto result = Manifold::Extrude(cross, 5.); #ifdef MANIFOLD_EXPORT @@ -38,8 +39,7 @@ TEST(CrossSection, MirrorUnion) { ExportMesh("cross_section_mirror_union.glb", result.GetMesh(), {}); #endif - auto area_a = a.Area(); - EXPECT_EQ(area_a + 1.5 * area_a, cross.Area()); + EXPECT_FLOAT_EQ(2.5 * a.Area(), cross.Area()); EXPECT_TRUE(a.Mirror(glm::vec2(0)).IsEmpty()); } @@ -89,7 +89,11 @@ TEST(CrossSection, Transform) { 0.0f, 0.0f, 1.0f); auto b = sq.Transform(trans * scale * rot); + auto b_copy = CrossSection(b); - Identical(Manifold::Extrude(a, 1.).GetMesh(), - Manifold::Extrude(b, 1.).GetMesh()); + auto ex_b = Manifold::Extrude(b, 1.).GetMesh(); + Identical(Manifold::Extrude(a, 1.).GetMesh(), ex_b); + + // same transformations are applied in b_copy (giving same result) + Identical(ex_b, Manifold::Extrude(b_copy, 1.).GetMesh()); }