504 lines
19 KiB
C++
504 lines
19 KiB
C++
// 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 "./csg_tree.h"
|
|
#include "./impl.h"
|
|
#include "./parallel.h"
|
|
#include "manifold/polygon.h"
|
|
|
|
namespace manifold {
|
|
/**
|
|
* Constructs a smooth version of the input mesh by creating tangents; this
|
|
* method will throw if you have supplied tangents with your mesh already. The
|
|
* actual triangle resolution is unchanged; use the Refine() method to
|
|
* interpolate to a higher-resolution curve.
|
|
*
|
|
* By default, every edge is calculated for maximum smoothness (very much
|
|
* approximately), attempting to minimize the maximum mean Curvature magnitude.
|
|
* No higher-order derivatives are considered, as the interpolation is
|
|
* independent per triangle, only sharing constraints on their boundaries.
|
|
*
|
|
* @param meshGL input MeshGL.
|
|
* @param sharpenedEdges If desired, you can supply a vector of sharpened
|
|
* halfedges, which should in general be a small subset of all halfedges. Order
|
|
* of entries doesn't matter, as each one specifies the desired smoothness
|
|
* (between zero and one, with one the default for all unspecified halfedges)
|
|
* and the halfedge index (3 * triangle index + [0,1,2] where 0 is the edge
|
|
* between triVert 0 and 1, etc).
|
|
*
|
|
* At a smoothness value of zero, a sharp crease is made. The smoothness is
|
|
* interpolated along each edge, so the specified value should be thought of as
|
|
* an average. Where exactly two sharpened edges meet at a vertex, their
|
|
* tangents are rotated to be colinear so that the sharpened edge can be
|
|
* continuous. Vertices with only one sharpened edge are completely smooth,
|
|
* allowing sharpened edges to smoothly vanish at termination. A single vertex
|
|
* can be sharpened by sharping all edges that are incident on it, allowing
|
|
* cones to be formed.
|
|
*/
|
|
Manifold Manifold::Smooth(const MeshGL& meshGL,
|
|
const std::vector<Smoothness>& sharpenedEdges) {
|
|
DEBUG_ASSERT(meshGL.halfedgeTangent.empty(), std::runtime_error,
|
|
"when supplying tangents, the normal constructor should be used "
|
|
"rather than Smooth().");
|
|
|
|
std::shared_ptr<Impl> impl = std::make_shared<Impl>(meshGL);
|
|
impl->CreateTangents(impl->UpdateSharpenedEdges(sharpenedEdges));
|
|
return Manifold(impl);
|
|
}
|
|
|
|
/**
|
|
* Constructs a smooth version of the input mesh by creating tangents; this
|
|
* method will throw if you have supplied tangents with your mesh already. The
|
|
* actual triangle resolution is unchanged; use the Refine() method to
|
|
* interpolate to a higher-resolution curve.
|
|
*
|
|
* By default, every edge is calculated for maximum smoothness (very much
|
|
* approximately), attempting to minimize the maximum mean Curvature magnitude.
|
|
* No higher-order derivatives are considered, as the interpolation is
|
|
* independent per triangle, only sharing constraints on their boundaries.
|
|
*
|
|
* @param meshGL64 input MeshGL64.
|
|
* @param sharpenedEdges If desired, you can supply a vector of sharpened
|
|
* halfedges, which should in general be a small subset of all halfedges. Order
|
|
* of entries doesn't matter, as each one specifies the desired smoothness
|
|
* (between zero and one, with one the default for all unspecified halfedges)
|
|
* and the halfedge index (3 * triangle index + [0,1,2] where 0 is the edge
|
|
* between triVert 0 and 1, etc).
|
|
*
|
|
* At a smoothness value of zero, a sharp crease is made. The smoothness is
|
|
* interpolated along each edge, so the specified value should be thought of as
|
|
* an average. Where exactly two sharpened edges meet at a vertex, their
|
|
* tangents are rotated to be colinear so that the sharpened edge can be
|
|
* continuous. Vertices with only one sharpened edge are completely smooth,
|
|
* allowing sharpened edges to smoothly vanish at termination. A single vertex
|
|
* can be sharpened by sharping all edges that are incident on it, allowing
|
|
* cones to be formed.
|
|
*/
|
|
Manifold Manifold::Smooth(const MeshGL64& meshGL64,
|
|
const std::vector<Smoothness>& sharpenedEdges) {
|
|
DEBUG_ASSERT(meshGL64.halfedgeTangent.empty(), std::runtime_error,
|
|
"when supplying tangents, the normal constructor should be used "
|
|
"rather than Smooth().");
|
|
|
|
std::shared_ptr<Impl> impl = std::make_shared<Impl>(meshGL64);
|
|
impl->CreateTangents(impl->UpdateSharpenedEdges(sharpenedEdges));
|
|
return Manifold(impl);
|
|
}
|
|
|
|
/**
|
|
* Constructs a tetrahedron centered at the origin with one vertex at (1,1,1)
|
|
* and the rest at similarly symmetric points.
|
|
*/
|
|
Manifold Manifold::Tetrahedron() {
|
|
return Manifold(std::make_shared<Impl>(Impl::Shape::Tetrahedron));
|
|
}
|
|
|
|
/**
|
|
* Constructs a unit cube (edge lengths all one), by default in the first
|
|
* octant, touching the origin. If any dimensions in size are negative, or if
|
|
* all are zero, an empty Manifold will be returned.
|
|
*
|
|
* @param size The X, Y, and Z dimensions of the box.
|
|
* @param center Set to true to shift the center to the origin.
|
|
*/
|
|
Manifold Manifold::Cube(vec3 size, bool center) {
|
|
if (size.x < 0.0 || size.y < 0.0 || size.z < 0.0 || la::length(size) == 0.) {
|
|
return Invalid();
|
|
}
|
|
mat3x4 m({{size.x, 0.0, 0.0}, {0.0, size.y, 0.0}, {0.0, 0.0, size.z}},
|
|
center ? (-size / 2.0) : vec3(0.0));
|
|
return Manifold(std::make_shared<Impl>(Manifold::Impl::Shape::Cube, m));
|
|
}
|
|
|
|
/**
|
|
* A convenience constructor for the common case of extruding a circle. Can also
|
|
* form cones if both radii are specified.
|
|
*
|
|
* @param height Z-extent
|
|
* @param radiusLow Radius of bottom circle. Must be positive.
|
|
* @param radiusHigh Radius of top circle. Can equal zero. Default is equal to
|
|
* radiusLow.
|
|
* @param circularSegments How many line segments to use around the circle.
|
|
* Default is calculated by the static Defaults.
|
|
* @param center Set to true to shift the center to the origin. Default is
|
|
* origin at the bottom.
|
|
*/
|
|
Manifold Manifold::Cylinder(double height, double radiusLow, double radiusHigh,
|
|
int circularSegments, bool center) {
|
|
if (height <= 0.0 || radiusLow <= 0.0) {
|
|
return Invalid();
|
|
}
|
|
const double scale = radiusHigh >= 0.0 ? radiusHigh / radiusLow : 1.0;
|
|
const double radius = fmax(radiusLow, radiusHigh);
|
|
const int n = circularSegments > 2 ? circularSegments
|
|
: Quality::GetCircularSegments(radius);
|
|
|
|
SimplePolygon circle(n);
|
|
const double dPhi = 360.0 / n;
|
|
for (int i = 0; i < n; ++i) {
|
|
circle[i] = {radiusLow * cosd(dPhi * i), radiusLow * sind(dPhi * i)};
|
|
}
|
|
|
|
Manifold cylinder = Manifold::Extrude({circle}, height, 0, 0.0, vec2(scale));
|
|
if (center)
|
|
cylinder = cylinder.Translate(vec3(0.0, 0.0, -height / 2.0)).AsOriginal();
|
|
return cylinder;
|
|
}
|
|
|
|
/**
|
|
* Constructs a geodesic sphere of a given radius.
|
|
*
|
|
* @param radius Radius of the sphere. Must be positive.
|
|
* @param circularSegments Number of segments along its
|
|
* diameter. This number will always be rounded up to the nearest factor of
|
|
* four, as this sphere is constructed by refining an octahedron. This means
|
|
* there are a circle of vertices on all three of the axis planes. Default is
|
|
* calculated by the static Defaults.
|
|
*/
|
|
Manifold Manifold::Sphere(double radius, int circularSegments) {
|
|
if (radius <= 0.0) {
|
|
return Invalid();
|
|
}
|
|
int n = circularSegments > 0 ? (circularSegments + 3) / 4
|
|
: Quality::GetCircularSegments(radius) / 4;
|
|
auto pImpl_ = std::make_shared<Impl>(Impl::Shape::Octahedron);
|
|
pImpl_->Subdivide(
|
|
[n](vec3 edge, vec4 tangentStart, vec4 tangentEnd) { return n - 1; });
|
|
for_each_n(autoPolicy(pImpl_->NumVert(), 1e5), pImpl_->vertPos_.begin(),
|
|
pImpl_->NumVert(), [radius](vec3& v) {
|
|
v = la::cos(kHalfPi * (1.0 - v));
|
|
v = radius * la::normalize(v);
|
|
if (std::isnan(v.x)) v = vec3(0.0);
|
|
});
|
|
pImpl_->Finish();
|
|
// Ignore preceding octahedron.
|
|
pImpl_->InitializeOriginal();
|
|
return Manifold(pImpl_);
|
|
}
|
|
|
|
/**
|
|
* Constructs a manifold from a set of polygons by extruding them along the
|
|
* Z-axis.
|
|
* Note that high twistDegrees with small nDivisions may cause
|
|
* self-intersection. This is not checked here and it is up to the user to
|
|
* choose the correct parameters.
|
|
*
|
|
* @param crossSection A set of non-overlapping polygons to extrude.
|
|
* @param height Z-extent of extrusion.
|
|
* @param nDivisions Number of extra copies of the crossSection to insert into
|
|
* the shape vertically; especially useful in combination with twistDegrees to
|
|
* avoid interpolation artifacts. Default is none.
|
|
* @param twistDegrees Amount to twist the top crossSection relative to the
|
|
* bottom, interpolated linearly for the divisions in between.
|
|
* @param scaleTop Amount to scale the top (independently in X and Y). If the
|
|
* scale is {0, 0}, a pure cone is formed with only a single vertex at the top.
|
|
* Note that scale is applied after twist.
|
|
* Default {1, 1}.
|
|
*/
|
|
Manifold Manifold::Extrude(const Polygons& crossSection, double height,
|
|
int nDivisions, double twistDegrees, vec2 scaleTop) {
|
|
ZoneScoped;
|
|
if (crossSection.size() == 0 || height <= 0.0) {
|
|
return Invalid();
|
|
}
|
|
|
|
scaleTop.x = std::max(scaleTop.x, 0.0);
|
|
scaleTop.y = std::max(scaleTop.y, 0.0);
|
|
|
|
auto pImpl_ = std::make_shared<Impl>();
|
|
++nDivisions;
|
|
auto& vertPos = pImpl_->vertPos_;
|
|
Vec<ivec3> triVertsDH;
|
|
auto& triVerts = triVertsDH;
|
|
int nCrossSection = 0;
|
|
bool isCone = scaleTop.x == 0.0 && scaleTop.y == 0.0;
|
|
size_t idx = 0;
|
|
PolygonsIdx polygonsIndexed;
|
|
for (auto& poly : crossSection) {
|
|
nCrossSection += poly.size();
|
|
SimplePolygonIdx simpleIndexed;
|
|
for (const vec2& polyVert : poly) {
|
|
vertPos.push_back({polyVert.x, polyVert.y, 0.0});
|
|
simpleIndexed.push_back({polyVert, static_cast<int>(idx++)});
|
|
}
|
|
polygonsIndexed.push_back(simpleIndexed);
|
|
}
|
|
for (int i = 1; i < nDivisions + 1; ++i) {
|
|
double alpha = i / double(nDivisions);
|
|
double phi = alpha * twistDegrees;
|
|
vec2 scale = la::lerp(vec2(1.0), scaleTop, alpha);
|
|
mat2 rotation({cosd(phi), sind(phi)}, {-sind(phi), cosd(phi)});
|
|
mat2 transform = mat2({scale.x, 0.0}, {0.0, scale.y}) * rotation;
|
|
size_t j = 0;
|
|
size_t idx = 0;
|
|
for (const auto& poly : crossSection) {
|
|
for (size_t vert = 0; vert < poly.size(); ++vert) {
|
|
size_t offset = idx + nCrossSection * i;
|
|
size_t thisVert = vert + offset;
|
|
size_t lastVert = (vert == 0 ? poly.size() : vert) - 1 + offset;
|
|
if (i == nDivisions && isCone) {
|
|
triVerts.push_back(ivec3(nCrossSection * i + j,
|
|
lastVert - nCrossSection,
|
|
thisVert - nCrossSection));
|
|
} else {
|
|
vec2 pos = transform * poly[vert];
|
|
vertPos.push_back({pos.x, pos.y, height * alpha});
|
|
triVerts.push_back(
|
|
ivec3(thisVert, lastVert, thisVert - nCrossSection));
|
|
triVerts.push_back(ivec3(lastVert, lastVert - nCrossSection,
|
|
thisVert - nCrossSection));
|
|
}
|
|
}
|
|
++j;
|
|
idx += poly.size();
|
|
}
|
|
}
|
|
if (isCone)
|
|
for (size_t j = 0; j < crossSection.size();
|
|
++j) // Duplicate vertex for Genus
|
|
vertPos.push_back({0.0, 0.0, height});
|
|
std::vector<ivec3> top = TriangulateIdx(polygonsIndexed);
|
|
for (const ivec3& tri : top) {
|
|
triVerts.push_back({tri[0], tri[2], tri[1]});
|
|
if (!isCone) triVerts.push_back(tri + nCrossSection * nDivisions);
|
|
}
|
|
|
|
pImpl_->CreateHalfedges(triVertsDH);
|
|
pImpl_->Finish();
|
|
pImpl_->InitializeOriginal();
|
|
pImpl_->CreateFaces();
|
|
return Manifold(pImpl_);
|
|
}
|
|
|
|
/**
|
|
* Constructs a manifold from a set of polygons by revolving this cross-section
|
|
* around its Y-axis and then setting this as the Z-axis of the resulting
|
|
* manifold. If the polygons cross the Y-axis, only the part on the positive X
|
|
* side is used. Geometrically valid input will result in geometrically valid
|
|
* output.
|
|
*
|
|
* @param crossSection A set of non-overlapping polygons to revolve.
|
|
* @param circularSegments Number of segments along its diameter. Default is
|
|
* calculated by the static Defaults.
|
|
* @param revolveDegrees Number of degrees to revolve. Default is 360 degrees.
|
|
*/
|
|
Manifold Manifold::Revolve(const Polygons& crossSection, int circularSegments,
|
|
double revolveDegrees) {
|
|
ZoneScoped;
|
|
|
|
Polygons polygons;
|
|
double radius = 0;
|
|
for (const SimplePolygon& poly : crossSection) {
|
|
size_t i = 0;
|
|
while (i < poly.size() && poly[i].x < 0) {
|
|
++i;
|
|
}
|
|
if (i == poly.size()) {
|
|
continue;
|
|
}
|
|
polygons.push_back({});
|
|
const size_t start = i;
|
|
do {
|
|
if (poly[i].x >= 0) {
|
|
polygons.back().push_back(poly[i]);
|
|
radius = std::max(radius, poly[i].x);
|
|
}
|
|
const size_t next = i + 1 == poly.size() ? 0 : i + 1;
|
|
if ((poly[next].x < 0) != (poly[i].x < 0)) {
|
|
const double y = poly[next].y + poly[next].x *
|
|
(poly[i].y - poly[next].y) /
|
|
(poly[i].x - poly[next].x);
|
|
polygons.back().push_back({0, y});
|
|
}
|
|
i = next;
|
|
} while (i != start);
|
|
}
|
|
|
|
if (polygons.empty()) {
|
|
return Invalid();
|
|
}
|
|
|
|
if (revolveDegrees > 360.0) {
|
|
revolveDegrees = 360.0;
|
|
}
|
|
const bool isFullRevolution = revolveDegrees == 360.0;
|
|
|
|
const int nDivisions =
|
|
circularSegments > 2
|
|
? circularSegments
|
|
: Quality::GetCircularSegments(radius) * revolveDegrees / 360;
|
|
|
|
auto pImpl_ = std::make_shared<Impl>();
|
|
auto& vertPos = pImpl_->vertPos_;
|
|
Vec<ivec3> triVertsDH;
|
|
auto& triVerts = triVertsDH;
|
|
|
|
std::vector<int> startPoses;
|
|
std::vector<int> endPoses;
|
|
|
|
const double dPhi = revolveDegrees / nDivisions;
|
|
// first and last slice are distinguished if not a full revolution.
|
|
const int nSlices = isFullRevolution ? nDivisions : nDivisions + 1;
|
|
|
|
for (const auto& poly : polygons) {
|
|
std::size_t nPosVerts = 0;
|
|
std::size_t nRevolveAxisVerts = 0;
|
|
for (auto& pt : poly) {
|
|
if (pt.x > 0) {
|
|
nPosVerts++;
|
|
} else {
|
|
nRevolveAxisVerts++;
|
|
}
|
|
}
|
|
|
|
for (size_t polyVert = 0; polyVert < poly.size(); ++polyVert) {
|
|
const size_t startPosIndex = vertPos.size();
|
|
|
|
if (!isFullRevolution) startPoses.push_back(startPosIndex);
|
|
|
|
const vec2 currPolyVertex = poly[polyVert];
|
|
const vec2 prevPolyVertex =
|
|
poly[polyVert == 0 ? poly.size() - 1 : polyVert - 1];
|
|
|
|
const int prevStartPosIndex =
|
|
startPosIndex +
|
|
(polyVert == 0 ? nRevolveAxisVerts + (nSlices * nPosVerts) : 0) +
|
|
(prevPolyVertex.x == 0.0 ? -1 : -nSlices);
|
|
|
|
for (int slice = 0; slice < nSlices; ++slice) {
|
|
const double phi = slice * dPhi;
|
|
if (slice == 0 || currPolyVertex.x > 0) {
|
|
vertPos.push_back({currPolyVertex.x * cosd(phi),
|
|
currPolyVertex.x * sind(phi), currPolyVertex.y});
|
|
}
|
|
|
|
if (isFullRevolution || slice > 0) {
|
|
const int lastSlice = (slice == 0 ? nDivisions : slice) - 1;
|
|
if (currPolyVertex.x > 0.0) {
|
|
triVerts.push_back(ivec3(
|
|
startPosIndex + slice, startPosIndex + lastSlice,
|
|
// "Reuse" vertex of first slice if it lies on the revolve axis
|
|
(prevPolyVertex.x == 0.0 ? prevStartPosIndex
|
|
: prevStartPosIndex + lastSlice)));
|
|
}
|
|
|
|
if (prevPolyVertex.x > 0.0) {
|
|
triVerts.push_back(
|
|
ivec3(prevStartPosIndex + lastSlice, prevStartPosIndex + slice,
|
|
(currPolyVertex.x == 0.0 ? startPosIndex
|
|
: startPosIndex + slice)));
|
|
}
|
|
}
|
|
}
|
|
if (!isFullRevolution) endPoses.push_back(vertPos.size() - 1);
|
|
}
|
|
}
|
|
|
|
// Add front and back triangles if not a full revolution.
|
|
if (!isFullRevolution) {
|
|
std::vector<ivec3> frontTriangles = Triangulate(polygons, pImpl_->epsilon_);
|
|
for (auto& t : frontTriangles) {
|
|
triVerts.push_back({startPoses[t.x], startPoses[t.y], startPoses[t.z]});
|
|
}
|
|
|
|
for (auto& t : frontTriangles) {
|
|
triVerts.push_back({endPoses[t.z], endPoses[t.y], endPoses[t.x]});
|
|
}
|
|
}
|
|
|
|
pImpl_->CreateHalfedges(triVertsDH);
|
|
pImpl_->Finish();
|
|
pImpl_->InitializeOriginal();
|
|
pImpl_->CreateFaces();
|
|
return Manifold(pImpl_);
|
|
}
|
|
|
|
/**
|
|
* Constructs a new manifold from a vector of other manifolds. This is a purely
|
|
* topological operation, so care should be taken to avoid creating
|
|
* overlapping results. It is the inverse operation of Decompose().
|
|
*
|
|
* @param manifolds A vector of Manifolds to lazy-union together.
|
|
*/
|
|
Manifold Manifold::Compose(const std::vector<Manifold>& manifolds) {
|
|
std::vector<std::shared_ptr<CsgLeafNode>> children;
|
|
for (const auto& manifold : manifolds) {
|
|
children.push_back(manifold.pNode_->ToLeafNode());
|
|
}
|
|
return Manifold(CsgLeafNode::Compose(children));
|
|
}
|
|
|
|
/**
|
|
* This operation returns a vector of Manifolds that are topologically
|
|
* disconnected. If everything is connected, the vector is length one,
|
|
* containing a copy of the original. It is the inverse operation of Compose().
|
|
*/
|
|
std::vector<Manifold> Manifold::Decompose() const {
|
|
ZoneScoped;
|
|
UnionFind<> uf(NumVert());
|
|
// Graph graph;
|
|
auto pImpl_ = GetCsgLeafNode().GetImpl();
|
|
for (const Halfedge& halfedge : pImpl_->halfedge_) {
|
|
if (halfedge.IsForward()) uf.unionXY(halfedge.startVert, halfedge.endVert);
|
|
}
|
|
std::vector<int> componentIndices;
|
|
const int numComponents = uf.connectedComponents(componentIndices);
|
|
|
|
if (numComponents == 1) {
|
|
std::vector<Manifold> meshes(1);
|
|
meshes[0] = *this;
|
|
return meshes;
|
|
}
|
|
Vec<int> vertLabel(componentIndices);
|
|
|
|
const int numVert = NumVert();
|
|
std::vector<Manifold> meshes;
|
|
for (int i = 0; i < numComponents; ++i) {
|
|
auto impl = std::make_shared<Impl>();
|
|
// inherit original object's precision
|
|
impl->epsilon_ = pImpl_->epsilon_;
|
|
impl->tolerance_ = pImpl_->tolerance_;
|
|
|
|
Vec<int> vertNew2Old(numVert);
|
|
const int nVert =
|
|
copy_if(countAt(0), countAt(numVert), vertNew2Old.begin(),
|
|
[i, &vertLabel](int v) { return vertLabel[v] == i; }) -
|
|
vertNew2Old.begin();
|
|
impl->vertPos_.resize(nVert);
|
|
vertNew2Old.resize(nVert);
|
|
gather(vertNew2Old.begin(), vertNew2Old.end(), pImpl_->vertPos_.begin(),
|
|
impl->vertPos_.begin());
|
|
|
|
Vec<int> faceNew2Old(NumTri());
|
|
const auto& halfedge = pImpl_->halfedge_;
|
|
const int nFace =
|
|
copy_if(countAt(0_uz), countAt(NumTri()), faceNew2Old.begin(),
|
|
[i, &vertLabel, &halfedge](int face) {
|
|
return vertLabel[halfedge[3 * face].startVert] == i;
|
|
}) -
|
|
faceNew2Old.begin();
|
|
|
|
if (nFace == 0) continue;
|
|
faceNew2Old.resize(nFace);
|
|
|
|
impl->GatherFaces(*pImpl_, faceNew2Old);
|
|
impl->ReindexVerts(vertNew2Old, pImpl_->NumVert());
|
|
impl->Finish();
|
|
|
|
meshes.push_back(Manifold(impl));
|
|
}
|
|
return meshes;
|
|
}
|
|
} // namespace manifold
|