Skip to content

Commit

Permalink
Lazy transformations for CrossSection (#355)
Browse files Browse the repository at this point in the history
Add transformation composition with lazy application when needed, similar to the arrangement used in the CSG tree backing Manifold.
  • Loading branch information
geoffder authored Mar 10, 2023
1 parent 754a481 commit dca9cce
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 75 deletions.
7 changes: 6 additions & 1 deletion src/cross_section/include/cross_section.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@

#include <clipper2/clipper.h>

#include <memory>

#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"

Expand Down Expand Up @@ -96,8 +99,10 @@ class CrossSection {
///@}

private:
C2::PathsD paths_;
mutable std::shared_ptr<const C2::PathsD> paths_;
mutable glm::mat3x2 transform_ = glm::mat3x2(1.0f);
CrossSection(C2::PathsD paths);
C2::PathsD GetPaths() const;
};
/** @} */

Expand Down
144 changes: 75 additions & 69 deletions src/cross_section/src/cross_section.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@

#include <clipper2/clipper.h>

#include <memory>
#include <vector>

#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"
Expand Down Expand Up @@ -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<const C2::PathsD> shared_paths(const C2::PathsD& ps) {
return std::make_shared<const C2::PathsD>(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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}

Expand All @@ -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());
}

Expand Down Expand Up @@ -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});
}
Expand Down
14 changes: 9 additions & 5 deletions test/cross_section_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include <gtest/gtest.h>

#include "glm/geometric.hpp"
#include "manifold.h"
#include "polygon.h"
#include "public.h"
Expand All @@ -30,16 +31,15 @@ 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
if (options.exportModels)
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());
}

Expand Down Expand Up @@ -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());
}

0 comments on commit dca9cce

Please sign in to comment.