2d-operations via Clipper2 (#341)

* WIP Clipper2 wrapper for 2d subsystem

* Working example in clippoly_test

* Add clean_ property to clippoly

* Square center=false by default

* Square defaults to center=false

* Shorten union example

* + simplification and minkowski functions

* + Circle and note on Minkowski

* + Offset (InflatePaths)

* Clippoly -> CrossSection renaming

* Switch from NonZero to Positive fill rule

* -(minkowski, simplifs, clean_) + pre-alloc vecs

Remove minkowski, simplification algorithms other than simplify,
clean (union) arbitrary contours on construction, and pre-allocate
vectors when length is known.

* - index from polygon conversion + missed vec prealloc

* Use Polygon shorthand over std::vector

* Update copyright date

* Add Rotate

* Update copyrights to 2023

* Extrude and Revolve accept CrossSection only

* Fix incorrect use of vector constructor

* Fix remaining incorrect vector construction

* Revert "Update copyrights to 2023"

This reverts commit af8f6c8c95135eb3376e1f150fe720fddea16f33.

* Re-update copyright notice in cross_section files

* Add missing precision_ argument to cleaning Unions

* Lift circular resolution globals into Quality

* Replace uses of Manifold globals with Quality

* + Ellipse, Mirror, Area

* Include mirror in Union example and test area

* + Rewind (reverse winding of all contours)

* + fillrule/IsEmpty - Ellipse/Rewind

* Move Quality into public.h

* Remove bad definition

* Replace manual circles with CrossSection::Circle

* Lists of empty contours are empty

* + Rect class, RectClip, + const params/methods

* Add Transform to CrossSection and Rect
This commit is contained in:
Geoff deRosenroll
2023-03-03 17:11:15 -08:00
committed by GitHub
parent 7c8ce3c935
commit a97c666358
23 changed files with 872 additions and 128 deletions
+3
View File
@@ -11,3 +11,6 @@
path = bindings/python/third_party/pybind11
url = https://github.com/pybind/pybind11
branch = stable
[submodule "src/third_party/clipper2"]
path = src/third_party/clipper2
url = https://github.com/AngusJohnson/clipper2
+1 -1
View File
@@ -10,7 +10,7 @@ endif()
target_link_libraries(
manifoldc
PRIVATE manifold sdf meshIO graphlite
PRIVATE manifold sdf meshIO graphlite cross_section
)
target_include_directories(manifoldc PUBLIC ${PROJECT_SOURCE_DIR}/include)
+5 -4
View File
@@ -1,4 +1,5 @@
#include <conv.h>
#include <cross_section.h>
#include <manifold.h>
#include <manifoldc.h>
#include <meshIO.h>
@@ -406,19 +407,19 @@ float *manifold_curvature_vert_gaussian(void *mem, ManifoldCurvature *curv) {
// Static Quality Globals
void manifold_set_min_circular_angle(float degrees) {
Manifold::SetMinCircularAngle(degrees);
Quality::SetMinCircularAngle(degrees);
}
void manifold_set_min_circular_edge_length(float length) {
Manifold::SetMinCircularEdgeLength(length);
Quality::SetMinCircularEdgeLength(length);
}
void manifold_set_circular_segments(int number) {
Manifold::SetCircularSegments(number);
Quality::SetCircularSegments(number);
}
int manifold_get_circular_segments(float radius) {
return Manifold::GetCircularSegments(radius);
return Quality::GetCircularSegments(radius);
}
// memory size
+1 -1
View File
@@ -19,7 +19,7 @@ add_executable(manifoldjs bindings.cpp)
# make sure that we recompile the wasm when bindings.js is being modified
set_source_files_properties(bindings.cpp OBJECT_DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/bindings.js)
target_link_libraries(manifoldjs manifold sdf)
target_link_libraries(manifoldjs manifold sdf cross_section)
target_compile_options(manifoldjs PRIVATE ${MANIFOLD_FLAGS} -fexceptions)
target_link_options(manifoldjs PUBLIC --pre-js ${CMAKE_CURRENT_SOURCE_DIR}/bindings.js --bind -sALLOW_TABLE_GROWTH=1
-sEXPORTED_RUNTIME_METHODS=addFunction,removeFunction -sMODULARIZE=1 -sEXPORT_ES6=1)
+4 -4
View File
@@ -271,9 +271,9 @@ EMSCRIPTEN_BINDINGS(whatever) {
function("_intersectionN", &IntersectionN);
function("_Compose", &Manifold::Compose);
function("setMinCircularAngle", &Manifold::SetMinCircularAngle);
function("setMinCircularEdgeLength", &Manifold::SetMinCircularEdgeLength);
function("setCircularSegments", &Manifold::SetCircularSegments);
function("getCircularSegments", &Manifold::GetCircularSegments);
function("setMinCircularAngle", &Quality::SetMinCircularAngle);
function("setMinCircularEdgeLength", &Quality::SetMinCircularEdgeLength);
function("setCircularSegments", &Quality::SetCircularSegments);
function("getCircularSegments", &Quality::GetCircularSegments);
function("reserveIDs", &Manifold::ReserveIDs);
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2023 The Manifold Authors.
// Copyright 2022 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
+1 -1
View File
@@ -52,4 +52,4 @@ Manifold Scallop();
Manifold GyroidModule(float size = 20, int n = 20);
/** @} */ // end of Samples
} // namespace manifold
} // namespace manifold
+3 -6
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cross_section.h"
#include "samples.h"
namespace {
@@ -23,12 +24,8 @@ Manifold Base(float width, float radius, float decorRadius, float twistRadius,
int nCut, int nDivision) {
Manifold base = Manifold::Cylinder(width, radius + twistRadius / 2);
Polygons circle(1);
float dPhiDeg = 180.0f / nDivision;
for (int i = 0; i < 2 * nDivision; ++i) {
circle[0].push_back({decorRadius * cosd(dPhiDeg * i) + twistRadius,
decorRadius * sind(dPhiDeg * i)});
}
CrossSection circle =
CrossSection::Circle(decorRadius, nDivision).Translate({twistRadius, 0});
Manifold decor = Manifold::Extrude(circle, width, nDivision, 180)
.Scale({1.0f, 0.5f, 1.0f})
.Translate({0.0f, radius, 0.0f});
+4 -8
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cross_section.h"
#include "samples.h"
namespace {
@@ -33,7 +34,7 @@ namespace manifold {
* @param minorRadius Radius of the small cross-section of the imaginary donut.
* @param threadRadius Radius of the small cross-section of the actual object.
* @param circularSegments Number of linear segments making up the threadRadius
* circle. Default is Manifold.GetCircularSegments().
* circle. Default is Quality.GetCircularSegments().
* @param linearSegments Number of segments along the length of the knot.
* Default makes roughly square facets.
*/
@@ -44,16 +45,11 @@ Manifold TorusKnot(int p, int q, float majorRadius, float minorRadius,
p /= kLoops;
q /= kLoops;
int n = circularSegments > 2 ? circularSegments
: Manifold::GetCircularSegments(threadRadius);
: Quality::GetCircularSegments(threadRadius);
int m =
linearSegments > 2 ? linearSegments : n * q * majorRadius / threadRadius;
Polygons circle(1);
float dPhi = 360.0f / n;
for (int i = 0; i < n; ++i) {
circle[0].push_back({cosd(dPhi * i) + 2.0f, sind(dPhi * i)});
}
CrossSection circle = CrossSection::Circle(1., n).Translate({2, 0});
Manifold knot = Manifold::Revolve(circle, m);
knot =
+1 -1
View File
@@ -24,7 +24,7 @@ namespace manifold {
* @param edgeLength Distance between the corners.
* @param radius Radius of the frame members.
* @param circularSegments Number of segments in the cylinders and spheres.
* Defaults to Manifold.GetCircularSegments().
* Defaults to Quality.GetCircularSegments().
*/
Manifold RoundedFrame(float edgeLength, float radius, int circularSegments) {
Manifold edge = Manifold::Cylinder(edgeLength, radius, -1, circularSegments);
+1
View File
@@ -15,6 +15,7 @@
add_subdirectory(third_party)
add_subdirectory(utilities)
add_subdirectory(collider)
add_subdirectory(cross_section)
add_subdirectory(polygon)
add_subdirectory(manifold)
add_subdirectory(sdf)
+28
View File
@@ -0,0 +1,28 @@
# Copyright 2023 The Manifold Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
project (cross_section)
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
add_library(${PROJECT_NAME} OBJECT ${SOURCE_FILES})
target_include_directories(${PROJECT_NAME}
PUBLIC ${PROJECT_SOURCE_DIR}/include
)
target_link_libraries( ${PROJECT_NAME}
PUBLIC utilities Clipper2
)
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
+167
View File
@@ -0,0 +1,167 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <clipper2/clipper.h>
#include "clipper2/clipper.core.h"
#include "clipper2/clipper.offset.h"
#include "glm/ext/vector_float2.hpp"
#include "public.h"
namespace C2 = Clipper2Lib;
namespace manifold {
class Rect;
class CrossSection {
public:
CrossSection();
~CrossSection();
CrossSection(const CrossSection& other);
CrossSection& operator=(const CrossSection& other);
CrossSection(CrossSection&&) noexcept;
CrossSection& operator=(CrossSection&&) noexcept;
// Construction from arbitrary contours
enum class FillRule { EvenOdd, NonZero, Positive, Negative };
CrossSection(const SimplePolygon& contour,
FillRule fillrule = FillRule::Positive);
CrossSection(const Polygons& contours,
FillRule fillrule = FillRule::Positive);
// Shapes
static CrossSection Square(const glm::vec2 dims, bool center = false);
static CrossSection Circle(float radius, int circularSegments = 0);
// Booleans
enum class OpType { Add, Subtract, Intersect, Xor };
CrossSection Boolean(const CrossSection& second, OpType op) const;
static CrossSection BatchBoolean(
const std::vector<CrossSection>& crossSections, OpType op);
CrossSection operator+(const CrossSection&) const; // Add (Union)
CrossSection& operator+=(const CrossSection&);
CrossSection operator-(const CrossSection&) const; // Subtract (Difference)
CrossSection& operator-=(const CrossSection&);
CrossSection operator^(const CrossSection&) const; // Intersect
CrossSection& operator^=(const CrossSection&);
CrossSection RectClip(const Rect& rect) const;
// Transformations
CrossSection Translate(const glm::vec2 v) const;
CrossSection Rotate(float degrees) const;
CrossSection Scale(const glm::vec2 s) const;
CrossSection Mirror(const glm::vec2 ax) const;
CrossSection Transform(const glm::mat3x2& m) const;
// Path Simplification
CrossSection Simplify(double epsilon = 1e-6) const;
// Offsetting
enum class JoinType { Square, Round, Miter };
CrossSection Offset(double delta, JoinType jt, double miter_limit = 2.0,
double arc_tolerance = 0.0) const;
// Geometry
double Area() const;
bool IsEmpty() const;
Rect Bounds() const;
// Output
Polygons ToPolygons() const;
private:
C2::PathsD paths_;
CrossSection(C2::PathsD paths);
};
class Rect {
public:
glm::vec2 min = glm::vec2(0);
glm::vec2 max = glm::vec2(0);
/**
* Default constructor is an empty rectangle..
*/
Rect();
~Rect();
Rect(const Rect& other);
Rect& operator=(const Rect& other);
Rect(Rect&&) noexcept;
Rect& operator=(Rect&&) noexcept;
/**
* Creates a rectangle that contains the two given points.
*/
Rect(const glm::vec2 a, const glm::vec2 b);
/**
* Returns the dimensions of the Rect.
*/
glm::vec2 Size() const;
/**
* Returns the absolute-largest coordinate value of any contained
* point.
*/
float Scale() const;
/**
* Returns the center point of the Box.
*/
glm::vec2 Center() const;
bool Contains(const glm::vec2& pt) const;
bool Contains(const Rect& other) const;
bool DoesOverlap(const Rect& other) const;
bool IsEmpty() const;
bool IsFinite() const;
/**
* Expand this rectangle (in place) to include the given point.
*/
void Union(const glm::vec2 p);
/**
* Expand this rectangle to include the given Rect.
*/
Rect Union(const Rect& other) const;
/**
* Shift this rectangle by the given vector.
*/
Rect operator+(const glm::vec2 shift) const;
/**
* Shift this rectangle in-place by the given vector.
*/
Rect& operator+=(const glm::vec2 shift);
/**
* Scale this rectangle by the given vector.
*/
Rect operator*(const glm::vec2 scale) const;
/**
* Scale this rectangle in-place by the given vector.
*/
Rect& operator*=(const glm::vec2 scale);
Rect Transform(const glm::mat3x2& m) const;
CrossSection AsCrossSection() const;
};
} // namespace manifold
+439
View File
@@ -0,0 +1,439 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cross_section.h"
#include <clipper2/clipper.h>
#include <vector>
#include "clipper2/clipper.core.h"
#include "clipper2/clipper.engine.h"
#include "clipper2/clipper.offset.h"
#include "glm/ext/vector_float2.hpp"
#include "glm/geometric.hpp"
#include "glm/glm.hpp"
#include "public.h"
using namespace manifold;
namespace {
const int precision_ = 8;
C2::ClipType cliptype_of_op(CrossSection::OpType op) {
C2::ClipType ct = C2::ClipType::Union;
switch (op) {
case CrossSection::OpType::Add:
break;
case CrossSection::OpType::Subtract:
ct = C2::ClipType::Difference;
break;
case CrossSection::OpType::Intersect:
ct = C2::ClipType::Intersection;
break;
case CrossSection::OpType::Xor:
ct = C2::ClipType::Xor;
break;
};
return ct;
}
C2::FillRule fr(CrossSection::FillRule fillrule) {
C2::FillRule fr = C2::FillRule::EvenOdd;
switch (fillrule) {
case CrossSection::FillRule::EvenOdd:
break;
case CrossSection::FillRule::NonZero:
fr = C2::FillRule::NonZero;
break;
case CrossSection::FillRule::Positive:
fr = C2::FillRule::Positive;
break;
case CrossSection::FillRule::Negative:
fr = C2::FillRule::Negative;
break;
};
return fr;
}
C2::JoinType jt(CrossSection::JoinType jointype) {
C2::JoinType jt = C2::JoinType::Square;
switch (jointype) {
case CrossSection::JoinType::Square:
break;
case CrossSection::JoinType::Round:
jt = C2::JoinType::Round;
break;
case CrossSection::JoinType::Miter:
jt = C2::JoinType::Miter;
break;
};
return jt;
}
glm::vec2 v2_of_pd(const C2::PointD p) { return {p.x, p.y}; }
C2::PointD v2_to_pd(const glm::vec2 v) { return C2::PointD(v.x, v.y); }
C2::PathD pathd_of_contour(const SimplePolygon& ctr) {
auto p = C2::PathD();
p.reserve(ctr.size());
for (auto v : ctr) {
p.push_back(v2_to_pd(v));
}
return p;
}
} // namespace
namespace manifold {
CrossSection::CrossSection() { 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_);
}
CrossSection::CrossSection(C2::PathsD ps) { paths_ = ps; }
CrossSection::CrossSection(const SimplePolygon& contour, FillRule fillrule) {
auto ps = C2::PathsD{(pathd_of_contour(contour))};
paths_ = C2::Union(ps, fr(fillrule), precision_);
}
CrossSection::CrossSection(const Polygons& contours, FillRule fillrule) {
auto ps = C2::PathsD();
ps.reserve(contours.size());
for (auto ctr : contours) {
ps.push_back(pathd_of_contour(ctr));
}
paths_ = C2::Union(ps, fr(fillrule), precision_);
}
CrossSection CrossSection::Square(const glm::vec2 dims, bool center) {
auto p = C2::PathD(4);
if (center) {
auto w = dims.x / 2;
auto h = dims.y / 2;
p[0] = C2::PointD(w, h);
p[1] = C2::PointD(-w, h);
p[2] = C2::PointD(-w, -h);
p[3] = C2::PointD(w, -h);
} else {
double x = dims.x;
double y = dims.y;
p[0] = C2::PointD(0.0, 0.0);
p[1] = C2::PointD(x, 0.0);
p[2] = C2::PointD(x, y);
p[3] = C2::PointD(0.0, y);
}
return CrossSection(C2::PathsD{p});
}
CrossSection CrossSection::Circle(float radius, int circularSegments) {
int n = circularSegments > 2 ? circularSegments
: Quality::GetCircularSegments(radius);
float dPhi = 360.0f / n;
auto circle = C2::PathD(n);
for (int i = 0; i < n; ++i) {
circle[i] = C2::PointD(radius * cosd(dPhi * i), radius * sind(dPhi * i));
}
return CrossSection(C2::PathsD{circle});
}
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_);
return CrossSection(res);
}
CrossSection CrossSection::BatchBoolean(
const std::vector<CrossSection>& crossSections, OpType op) {
if (crossSections.size() == 0)
return CrossSection();
else if (crossSections.size() == 1)
return crossSections[0];
auto subjs = crossSections[0].paths_;
int n_clips = 0;
for (int i = 1; i < crossSections.size(); ++i) {
n_clips += crossSections[i].paths_.size();
}
auto clips = C2::PathsD();
clips.reserve(n_clips);
for (int i = 1; i < crossSections.size(); ++i) {
auto ps = crossSections[i].paths_;
clips.insert(clips.end(), ps.begin(), ps.end());
}
auto ct = cliptype_of_op(op);
auto res =
C2::BooleanOp(ct, C2::FillRule::Positive, subjs, clips, precision_);
return CrossSection(res);
}
/**
* Shorthand for Boolean Union.
*/
CrossSection CrossSection::operator+(const CrossSection& Q) const {
return Boolean(Q, OpType::Add);
}
/**
* Shorthand for Boolean Union assignment.
*/
CrossSection& CrossSection::operator+=(const CrossSection& Q) {
*this = *this + Q;
return *this;
}
/**
* Shorthand for Boolean Difference.
*/
CrossSection CrossSection::operator-(const CrossSection& Q) const {
return Boolean(Q, OpType::Subtract);
}
/**
* Shorthand for Boolean Difference assignment.
*/
CrossSection& CrossSection::operator-=(const CrossSection& Q) {
*this = *this - Q;
return *this;
}
/**
* Shorthand for Boolean Intersection.
*/
CrossSection CrossSection::operator^(const CrossSection& Q) const {
return Boolean(Q, OpType::Intersect);
}
/**
* Shorthand for Boolean Intersection assignment.
*/
CrossSection& CrossSection::operator^=(const CrossSection& Q) {
*this = *this ^ Q;
return *this;
}
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_);
return CrossSection(ps);
}
CrossSection CrossSection::Translate(const glm::vec2 v) const {
auto ps = C2::TranslatePaths(paths_, v.x, v.y);
return CrossSection(ps);
}
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);
}
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);
}
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);
}
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);
}
CrossSection CrossSection::Simplify(double epsilon) const {
auto ps = SimplifyPaths(paths_, 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);
return CrossSection(ps);
}
double CrossSection::Area() const { return C2::Area(paths_); }
Rect CrossSection::Bounds() const {
auto r = C2::GetBounds(paths_);
return Rect({r.left, r.bottom}, {r.right, r.top});
}
bool CrossSection::IsEmpty() const { return paths_.empty(); }
Polygons CrossSection::ToPolygons() const {
auto polys = Polygons();
polys.reserve(paths_.size());
for (auto p : paths_) {
auto sp = SimplePolygon();
sp.reserve(paths_.size());
for (auto v : p) {
sp.push_back({v.x, v.y});
}
polys.push_back(sp);
}
return polys;
}
// Rect
Rect::Rect() {}
Rect::~Rect() = default;
Rect::Rect(Rect&&) noexcept = default;
Rect& Rect::operator=(Rect&&) noexcept = default;
Rect::Rect(const Rect& other) {
min = glm::vec2(other.min);
max = glm::vec2(other.max);
}
Rect::Rect(const glm::vec2 a, const glm::vec2 b) {
min = glm::min(a, b);
max = glm::max(a, b);
}
glm::vec2 Rect::Size() const { return max - min; }
float Rect::Scale() const {
glm::vec2 absMax = glm::max(glm::abs(min), glm::abs(max));
return glm::max(absMax.x, absMax.y);
}
glm::vec2 Rect::Center() const { return 0.5f * (max + min); }
bool Rect::Contains(const glm::vec2& p) const {
return glm::all(glm::greaterThanEqual(p, min)) &&
glm::all(glm::greaterThanEqual(max, p));
}
bool Rect::Contains(const Rect& rect) const {
return glm::all(glm::greaterThanEqual(rect.min, min)) &&
glm::all(glm::greaterThanEqual(max, rect.max));
}
void Rect::Union(const glm::vec2 p) {
min = glm::min(min, p);
max = glm::max(max, p);
}
Rect Rect::Union(const Rect& rect) const {
Rect out;
out.min = glm::min(min, rect.min);
out.max = glm::max(max, rect.max);
return out;
}
Rect Rect::operator+(const glm::vec2 shift) const {
Rect out;
out.min = min + shift;
out.max = max + shift;
return out;
}
Rect& Rect::operator+=(const glm::vec2 shift) {
min += shift;
max += shift;
return *this;
}
Rect Rect::operator*(const glm::vec2 scale) const {
Rect out;
out.min = min * scale;
out.max = max * scale;
return out;
}
Rect& Rect::operator*=(const glm::vec2 scale) {
min *= scale;
max *= scale;
return *this;
}
Rect Rect::Transform(const glm::mat3x2& m) const {
Rect rect;
rect.min = m * glm::vec3(min, 1);
rect.max = m * glm::vec3(max, 1);
return rect;
}
bool Rect::DoesOverlap(const Rect& rect) const {
return min.x <= rect.max.x && min.y <= rect.max.y && max.x >= rect.min.x &&
max.y >= rect.min.y;
}
bool Rect::IsEmpty() const { return max.y <= min.y || max.x <= min.x; };
bool Rect::IsFinite() const {
return glm::all(glm::isfinite(min)) && glm::all(glm::isfinite(max));
}
CrossSection Rect::AsCrossSection() const {
SimplePolygon p(4);
p[0] = glm::vec2(min.x, max.y);
p[1] = glm::vec2(max.x, max.y);
p[2] = glm::vec2(max.x, min.y);
p[3] = glm::vec2(min.x, min.y);
return CrossSection(p);
}
} // namespace manifold
+2 -2
View File
@@ -24,8 +24,8 @@ endif()
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
target_link_libraries(${PROJECT_NAME}
PUBLIC utilities
PRIVATE collider polygon ${MANIFOLD_INCLUDE} graphlite
PUBLIC utilities cross_section
PRIVATE collider polygon ${MANIFOLD_INCLUDE} graphlite Clipper2
)
target_compile_features(${PROJECT_NAME}
+3 -17
View File
@@ -16,6 +16,7 @@
#include <functional>
#include <memory>
#include "cross_section.h"
#include "public.h"
namespace manifold {
@@ -62,10 +63,10 @@ class Manifold {
float radiusHigh = -1.0f, int circularSegments = 0,
bool center = false);
static Manifold Sphere(float radius, int circularSegments = 0);
static Manifold Extrude(Polygons crossSection, float height,
static Manifold Extrude(const CrossSection& crossSection, float height,
int nDivisions = 0, float twistDegrees = 0.0f,
glm::vec2 scaleTop = glm::vec2(1.0f));
static Manifold Revolve(const Polygons& crossSection,
static Manifold Revolve(const CrossSection& crossSection,
int circularSegments = 0);
///@}
@@ -80,21 +81,6 @@ class Manifold {
std::vector<Manifold> Decompose() const;
///@}
/** @name Defaults
* These static properties control how circular shapes are quantized by
* default on construction. If circularSegments is specified, it takes
* precedence. If it is zero, then instead the minimum is used of the segments
* calculated based on edge length and angle, rounded up to the nearest
* multiple of four. To get numbers not divisible by four, circularSegments
* must be specified.
*/
///@{
static void SetMinCircularAngle(float degrees);
static void SetMinCircularEdgeLength(float length);
static void SetCircularSegments(int number);
static int GetCircularSegments(float radius);
///@}
/** @name Information
* Details of the manifold
*/
+25 -20
View File
@@ -14,6 +14,7 @@
#include <thrust/sequence.h>
#include "cross_section.h"
#include "csg_tree.h"
#include "graph.h"
#include "impl.h"
@@ -171,12 +172,10 @@ Manifold Manifold::Cylinder(float height, float radiusLow, float radiusHigh,
int circularSegments, bool center) {
float scale = radiusHigh >= 0.0f ? radiusHigh / radiusLow : 1.0f;
float radius = fmax(radiusLow, radiusHigh);
int n = circularSegments > 2 ? circularSegments : GetCircularSegments(radius);
Polygons circle(1);
float dPhi = 360.0f / n;
for (int i = 0; i < n; ++i) {
circle[0].push_back(radiusLow * glm::vec2(cosd(dPhi * i), sind(dPhi * i)));
}
int n = circularSegments > 2 ? circularSegments
: Quality::GetCircularSegments(radius);
CrossSection circle = CrossSection::Circle(radiusLow, n);
Manifold cylinder =
Manifold::Extrude(circle, height, 0, 0.0f, glm::vec2(scale));
if (center)
@@ -197,7 +196,7 @@ Manifold Manifold::Cylinder(float height, float radiusLow, float radiusHigh,
*/
Manifold Manifold::Sphere(float radius, int circularSegments) {
int n = circularSegments > 0 ? (circularSegments + 3) / 4
: GetCircularSegments(radius) / 4;
: Quality::GetCircularSegments(radius) / 4;
auto pImpl_ = std::make_shared<Impl>(Impl::Shape::Octahedron);
pImpl_->Subdivide(n);
for_each_n(autoPolicy(pImpl_->NumVert()), pImpl_->vertPos_.begin(),
@@ -223,8 +222,11 @@ Manifold Manifold::Sphere(float radius, int circularSegments) {
* scale is {0, 0}, a pure cone is formed with only a single vertex at the top.
* Default {1, 1}.
*/
Manifold Manifold::Extrude(Polygons crossSection, float height, int nDivisions,
float twistDegrees, glm::vec2 scaleTop) {
Manifold Manifold::Extrude(const CrossSection& crossSection, float height,
int nDivisions, float twistDegrees,
glm::vec2 scaleTop) {
auto polygons = crossSection.ToPolygons();
scaleTop.x = glm::max(scaleTop.x, 0.0f);
scaleTop.y = glm::max(scaleTop.y, 0.0f);
@@ -236,15 +238,15 @@ Manifold Manifold::Extrude(Polygons crossSection, float height, int nDivisions,
int nCrossSection = 0;
bool isCone = scaleTop.x == 0.0 && scaleTop.y == 0.0;
int idx = 0;
PolygonsIdx crossSectionIndexed;
for (auto& poly : crossSection) {
PolygonsIdx polygonsIndexed;
for (auto& poly : polygons) {
nCrossSection += poly.size();
SimplePolygonIdx simpleIndexed;
for (const glm::vec2& polyVert : poly) {
vertPos.push_back({polyVert.x, polyVert.y, 0.0f});
simpleIndexed.push_back({polyVert, idx++});
}
crossSectionIndexed.push_back(simpleIndexed);
polygonsIndexed.push_back(simpleIndexed);
}
for (int i = 1; i < nDivisions + 1; ++i) {
float alpha = i / float(nDivisions);
@@ -254,7 +256,7 @@ Manifold Manifold::Extrude(Polygons crossSection, float height, int nDivisions,
transform = transform * glm::mat2(scale.x, 0.0f, 0.0f, scale.y);
int j = 0;
int idx = 0;
for (const auto& poly : crossSection) {
for (const auto& poly : polygons) {
for (int vert = 0; vert < poly.size(); ++vert) {
int offset = idx + nCrossSection * i;
int thisVert = vert + offset;
@@ -275,9 +277,9 @@ Manifold Manifold::Extrude(Polygons crossSection, float height, int nDivisions,
}
}
if (isCone)
for (int j = 0; j < crossSection.size(); ++j) // Duplicate vertex for Genus
for (int j = 0; j < polygons.size(); ++j) // Duplicate vertex for Genus
vertPos.push_back({0.0f, 0.0f, height});
std::vector<glm::ivec3> top = Triangulate(crossSectionIndexed);
std::vector<glm::ivec3> top = Triangulate(polygonsIndexed);
for (const glm::ivec3& tri : top) {
triVerts.push_back({tri[0], tri[2], tri[1]});
if (!isCone) triVerts.push_back(tri + nCrossSection * nDivisions);
@@ -302,21 +304,24 @@ Manifold Manifold::Extrude(Polygons crossSection, float height, int nDivisions,
* @param circularSegments Number of segments along its diameter. Default is
* calculated by the static Defaults.
*/
Manifold Manifold::Revolve(const Polygons& crossSection, int circularSegments) {
Manifold Manifold::Revolve(const CrossSection& crossSection,
int circularSegments) {
auto polygons = crossSection.ToPolygons();
float radius = 0.0f;
for (const auto& poly : crossSection) {
for (const auto& poly : polygons) {
for (const auto& vert : poly) {
radius = fmax(radius, vert.x);
}
}
int nDivisions =
circularSegments > 2 ? circularSegments : GetCircularSegments(radius);
int nDivisions = circularSegments > 2 ? circularSegments
: Quality::GetCircularSegments(radius);
auto pImpl_ = std::make_shared<Impl>();
auto& vertPos = pImpl_->vertPos_;
VecDH<glm::ivec3> triVertsDH;
auto& triVerts = triVertsDH;
float dPhi = 360.0f / nDivisions;
for (const auto& poly : crossSection) {
for (const auto& poly : polygons) {
int start = -1;
for (int polyVert = 0; polyVert < poly.size(); ++polyVert) {
if (poly[polyVert].x <= 0) {
-60
View File
@@ -306,66 +306,6 @@ MeshGL Manifold::GetMeshGL(glm::ivec3 normalIdx) const {
return out;
}
int Manifold::circularSegments_ = 0;
float Manifold::circularAngle_ = 10.0f;
float Manifold::circularEdgeLength_ = 1.0f;
/**
* Sets an angle constraint the default number of circular segments for the
* Cylinder(), Sphere(), and Revolve() constructors. The number of segments will
* be rounded up to the nearest factor of four.
*
* @param angle The minimum angle in degrees between consecutive segments. The
* angle will increase if the the segments hit the minimum edge length. Default
* is 10 degrees.
*/
void Manifold::SetMinCircularAngle(float angle) {
if (angle <= 0) return;
Manifold::circularAngle_ = angle;
}
/**
* Sets a length constraint the default number of circular segments for the
* Cylinder(), Sphere(), and Revolve() constructors. The number of segments will
* be rounded up to the nearest factor of four.
*
* @param length The minimum length of segments. The length will
* increase if the the segments hit the minimum angle. Default is 1.0.
*/
void Manifold::SetMinCircularEdgeLength(float length) {
if (length <= 0) return;
Manifold::circularEdgeLength_ = length;
}
/**
* Sets the default number of circular segments for the
* Cylinder(), Sphere(), and Revolve() constructors. Overrides the edge length
* and angle constraints and sets the number of segments to exactly this value.
*
* @param number Number of circular segments. Default is 0, meaning no
* constraint is applied.
*/
void Manifold::SetCircularSegments(int number) {
if (number < 3 && number != 0) return;
Manifold::circularSegments_ = number;
}
/**
* Determine the result of the SetMinCircularAngle(),
* SetMinCircularEdgeLength(), and SetCircularSegments() defaults.
*
* @param radius For a given radius of circle, determine how many default
* segments there will be.
*/
int Manifold::GetCircularSegments(float radius) {
if (Manifold::circularSegments_ > 0) return Manifold::circularSegments_;
int nSegA = 360.0f / Manifold::circularAngle_;
int nSegL = 2.0f * radius * glm::pi<float>() / Manifold::circularEdgeLength_;
int nSeg = fmin(nSegA, nSegL) + 3;
nSeg -= nSeg % 4;
return nSeg;
}
/**
* Does the Manifold have any triangles?
*/
+7
View File
@@ -1 +1,8 @@
add_subdirectory(graphlite)
set(CLIPPER2_UTILS OFF)
set(CLIPPER2_EXAMPLES OFF)
set(CLIPPER2_TESTS OFF)
add_definitions(-D_HAS_EXCEPTIONS=0) # disable exceptions for STL
add_subdirectory(clipper2/CPP)
Vendored Submodule
+1
+78
View File
@@ -131,6 +131,7 @@ using SimplePolygon = std::vector<glm::vec2>;
* [&epsilon;-valid](https://github.com/elalish/manifold/wiki/Manifold-Library#definition-of-%CE%B5-valid).
*/
using Polygons = std::vector<SimplePolygon>;
// };
/** @} */
/**
@@ -422,6 +423,83 @@ struct Box {
}
};
class Quality {
private:
inline static int circularSegments_ = 0;
inline static float circularAngle_ = 10.0f;
inline static float circularEdgeLength_ = 1.0f;
public:
/** @name Defaults
* These static properties control how circular shapes are quantized by
* default on construction. If circularSegments is specified, it takes
* precedence. If it is zero, then instead the minimum is used of the segments
* calculated based on edge length and angle, rounded up to the nearest
* multiple of four. To get numbers not divisible by four, circularSegments
* must be specified.
*/
///@{
/**
* Sets an angle constraint the default number of circular segments for the
* CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), and
* Manifold::Revolve() constructors. The number of segments will be rounded up
* to the nearest factor of four.
*
* @param angle The minimum angle in degrees between consecutive segments. The
* angle will increase if the the segments hit the minimum edge length.
* Default is 10 degrees.
*/
static void SetMinCircularAngle(float angle) {
if (angle <= 0) return;
circularAngle_ = angle;
}
/**
* Sets a length constraint the default number of circular segments for the
* CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), and
* Manifold::Revolve() constructors. The number of segments will be rounded up
* to the nearest factor of four.
*
* @param length The minimum length of segments. The length will
* increase if the the segments hit the minimum angle. Default is 1.0.
*/
static void SetMinCircularEdgeLength(float length) {
if (length <= 0) return;
circularEdgeLength_ = length;
}
/**
* Sets the default number of circular segments for the
* CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), and
* Manifold::Revolve() constructors. Overrides the edge length and angle
* constraints and sets the number of segments to exactly this value.
*
* @param number Number of circular segments. Default is 0, meaning no
* constraint is applied.
*/
static void SetCircularSegments(int number) {
if (number < 3 && number != 0) return;
circularSegments_ = number;
}
/**
* Determine the result of the SetMinCircularAngle(),
* SetMinCircularEdgeLength(), and SetCircularSegments() defaults.
*
* @param radius For a given radius of circle, determine how many default
* segments there will be.
*/
static int GetCircularSegments(float radius) {
if (circularSegments_ > 0) return circularSegments_;
int nSegA = 360.0f / circularAngle_;
int nSegL = 2.0f * radius * glm::pi<float>() / circularEdgeLength_;
int nSeg = fmin(nSegA, nSegL) + 3;
nSeg -= nSeg % 4;
return nSeg;
}
///@}
};
/** @defgroup Debug
* @brief Debugging features
*
+2 -2
View File
@@ -18,14 +18,14 @@ add_subdirectory(third_party)
enable_testing()
set(SOURCE_FILES polygon_test.cpp manifold_test.cpp boolean_test.cpp sdf_test.cpp samples_test.cpp test_main.cpp)
set(SOURCE_FILES polygon_test.cpp manifold_test.cpp boolean_test.cpp sdf_test.cpp samples_test.cpp test_main.cpp cross_section_test.cpp)
if(MANIFOLD_CBIND)
list(APPEND SOURCE_FILES manifoldc_test.cpp)
endif()
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
target_link_libraries(${PROJECT_NAME} polygon GTest::GTest manifold sdf samples samplesGPU)
target_link_libraries(${PROJECT_NAME} polygon GTest::GTest manifold sdf samples samplesGPU cross_section)
if(MANIFOLD_CBIND)
target_link_libraries(${PROJECT_NAME} manifoldc)
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2021 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cross_section.h"
#include <gtest/gtest.h>
#include "manifold.h"
#include "polygon.h"
#include "public.h"
#include "test.h"
#ifdef MANIFOLD_EXPORT
#include "meshIO.h"
#endif
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 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_TRUE(a.Mirror(glm::vec2(0)).IsEmpty());
}
TEST(CrossSection, RoundOffset) {
auto a = CrossSection::Square({20., 20.}, true);
auto rounded = a.Offset(5., CrossSection::JoinType::Round);
auto result = Manifold::Extrude(rounded, 5.);
#ifdef MANIFOLD_EXPORT
if (options.exportModels)
ExportMesh("cross_section_round_offset.glb", result.GetMesh(), {});
#endif
EXPECT_TRUE(result.IsManifold());
}
TEST(CrossSection, Empty) {
Polygons polys(2);
auto e = CrossSection(polys);
EXPECT_TRUE(e.IsEmpty());
}
TEST(CrossSection, RectClip) {
auto sq = CrossSection::Square({10, 10});
auto rect = Rect({0, 0}, {10, 5});
auto clipped = sq.RectClip(rect);
EXPECT_EQ(sq.Area() / 2, clipped.Area());
EXPECT_TRUE(rect.Contains({5, 5}));
EXPECT_TRUE(rect.Contains(Rect()));
EXPECT_TRUE(rect.DoesOverlap(Rect({5, 5}, {15, 15})));
EXPECT_TRUE(Rect().IsEmpty());
}
TEST(CrossSection, Transform) {
auto sq = CrossSection::Square({10., 10.});
auto a = sq.Rotate(45).Scale({2, 3}).Translate({4, 5});
glm::mat3x3 trans(1.0f, 0.0f, 0.0f, //
0.0f, 1.0f, 0.0f, //
4.0f, 5.0f, 1.0f);
glm::mat3x3 rot(cosd(45), sind(45), 0.0f, //
-sind(45), cosd(45), 0.0f, //
0.0f, 0.0f, 1.0f);
glm::mat3x3 scale(2.0f, 0.0f, 0.0f, //
0.0f, 3.0f, 0.0f, //
0.0f, 0.0f, 1.0f);
auto b = sq.Transform(trans * scale * rot);
Identical(Manifold::Extrude(a, 1.).GetMesh(),
Manifold::Extrude(b, 1.).GetMesh());
}