Skip to content
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ set(UVULA_SRC
src/xatlas.cpp
src/unwrap.cpp
src/project.cpp
src/connect_faces.cpp
src/needs_supports.cpp
src/Vector3F.cpp
src/Vector2F.cpp
src/Matrix33F.cpp
Expand Down
24 changes: 18 additions & 6 deletions UvulaJS/UvulaJS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -249,12 +249,24 @@ EMSCRIPTEN_BINDINGS(uvula)
.function("z", &Vector3F::z);

value_object<Face>("Face")
.field("i1", &Face::i1)
.field("i2", &Face::i2)
.field("i3", &Face::i3);
.field("i1",
emscripten::optional_override([](const Face& self) -> uint32_t { return std::get<0>(self);} ),
emscripten::optional_override([](Face& self, const uint32_t value) -> void {std::get<0>(self) = value;} ))
.field("i2",
emscripten::optional_override([](const Face& self) -> uint32_t { return std::get<1>(self);} ),
emscripten::optional_override([](Face& self, const uint32_t value) -> void {std::get<1>(self) = value;} ))
.field("i3",
emscripten::optional_override([](const Face& self) -> uint32_t { return std::get<2>(self);} ),
emscripten::optional_override([](Face& self, const uint32_t value)-> void {std::get<2>(self) = value;} ));

value_object<FaceSigned>("FaceSigned")
.field("i1", &FaceSigned::i1)
.field("i2", &FaceSigned::i2)
.field("i3", &FaceSigned::i3);
.field("i1",
emscripten::optional_override([](const FaceSigned& self) -> int32_t { return std::get<0>(self);} ),
emscripten::optional_override([](FaceSigned& self, const int32_t value) -> void {std::get<0>(self) = value;} ))
.field("i2",
emscripten::optional_override([](const FaceSigned& self) -> int32_t { return std::get<1>(self);} ),
emscripten::optional_override([](FaceSigned& self, const int32_t value) -> void {std::get<1>(self) = value;} ))
.field("i3",
emscripten::optional_override([](const FaceSigned& self) -> int32_t { return std::get<2>(self);} ),
emscripten::optional_override([](FaceSigned& self, const int32_t value)-> void {std::get<2>(self) = value;} ));
}
10 changes: 3 additions & 7 deletions include/Face.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@

#pragma once

#include <array>
#include <cstdint>

template<typename IndexType>
struct FaceIndex
{
IndexType i1{ 0 };
IndexType i2{ 0 };
IndexType i3{ 0 };
};
using FaceIndex = std::array<IndexType, 3>;

using Face = FaceIndex<uint32_t>;

using FaceSigned = FaceIndex<int32_t>;
using FaceSigned = FaceIndex<int32_t>;
23 changes: 22 additions & 1 deletion include/Point3F.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

#pragma once

class Matrix44F;
#include <bit>
#include <memory>

class Point3F
{
Expand Down Expand Up @@ -41,8 +42,28 @@ class Point3F
return lhs.z_ < rhs.z_;
}

inline bool operator==(const Point3F& other) const = default;

// NOTE: _Exact_ hash, don't use when comparison requires near-equal.
inline size_t hash_() const noexcept
{
return std::hash<float>{}(x_) ^ (std::hash<float>{}(y_) << 1) ^ (std::hash<float>{}(z_) << 2);
}

private:
float x_{ 0.0 };
float y_{ 0.0 };
float z_{ 0.0 };
};

namespace std
{
template<>
struct hash<Point3F>
{
inline size_t operator()(const Point3F& pt) const noexcept
{
return pt.hash_();
}
};
} // namespace std
7 changes: 1 addition & 6 deletions include/Triangle2F.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,4 @@

#include "Point2F.h"

struct Triangle2F
{
Point2F p1;
Point2F p2;
Point2F p3;
};
using Triangle2F = std::array<Point2F, 3>;
26 changes: 3 additions & 23 deletions include/Triangle3F.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,14 @@

#pragma once

#include <array>

#include "Point3F.h"
#include "Vector3F.h"

class Triangle3F
class Triangle3F : public std::array<Point3F, 3>
{
public:
explicit Triangle3F(const Point3F& p1, const Point3F& p2, const Point3F& p3);

[[nodiscard]] const Point3F& p1() const
{
return p1_;
};

[[nodiscard]] const Point3F& p2() const
{
return p2_;
}

[[nodiscard]] const Point3F& p3() const
{
return p3_;
}

/*! @return The unitary normal of this triangle, or nullopt if it is degenerate */
[[nodiscard]] std::optional<Vector3F> normal() const;

private:
Point3F p1_;
Point3F p2_;
Point3F p3_;
};
2 changes: 2 additions & 0 deletions include/Vector3F.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class Vector3F

[[nodiscard]] Vector3F cross(const Vector3F& other) const;

[[nodiscard]] Vector3F abs() const;

Vector3F operator+(const Vector3F& other) const;

void operator+=(const Vector3F& other);
Expand Down
18 changes: 18 additions & 0 deletions include/connect_faces.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// (c) 2026, UltiMaker -- see LICENCE for details

#pragma once

#include "Face.h"

#include <span>
#include <vector>

class Point3F;

/**
* \brief Creates a face-connectivity data-structure.
* \param vertices The vertices of the mesh.
* \param indices The indices of the faces of the mesh, grouped by face.
* \param out_face_connects The face-connectivity is saved here; each face has 3 connections; the index '-1' is used when there is no neighbour.
*/
void connectFaces(const std::span<const Point3F>& vertices, const std::span<const Face>& indices, std::vector<FaceSigned>& out_face_connects);
40 changes: 40 additions & 0 deletions include/needs_supports.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// (c) 2026, UltiMaker -- see LICENCE for details

#pragma once

#include "Face.h"

#include <span>

class Point3F;

/**
* \brief Checks for downwards pointing (so, smaller in y-axis) vertices (unless 'close enough' to y=0) given a mesh.
* \param close_to_buildplate_dist Any vertex below this on the y-axis will be ignored.
* \param vertices The vertices of the mesh.
* \param indices The indices of the faces of the mesh, grouped by face.
* \return True if there are any such vertices, false otherwise.
*/
bool checkForDownVertices(
const float close_to_buildplate_dist,
const std::span<const Point3F>& vertices,
const std::span<const Face>& indices);

/**
* \brief Checks for neighbouring faces with a normal 'close enough' to pointing downwards (so, smaller in y-axis)
* that form at least one 'large enough' area (unless 'close enough' to y=0) given a mesh (+ connectivity).
* \param support_angle Determines when a face is 'downwards' (has a normal close enough to 'down') enough to count.
* \param close_to_buildplate_dist Any face below this on the y-axis will be ignored.
* \param min_support_area How large any grouped area of faces should be (measured by area) for it to be counted.
* \param vertices The vertices of the mesh.
* \param indices The indices of the faces of the mesh, grouped by face.
* \param mesh_connects The mesh-connectivity; per face, contains the indices of the neighbour-faces (-1 for none).
* \return True if there are any such vertices, false otherwise.
*/
bool checkForDownFaces(
const float support_angle,
const float close_to_buildplate_dist,
const float min_support_area,
const std::span<const Point3F>& vertices,
const std::span<const Face>& indices,
const std::span<const FaceSigned>& mesh_connects);
49 changes: 49 additions & 0 deletions pyUvula/pyUvula.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
#include <pybind11/pybind11.h>

#include "Face.h"
#include "connect_faces.h"
#include "Matrix44F.h"
#include "needs_supports.h"
#include "Point2F.h"
#include "Point3F.h"
#include "Vector3F.h"
Expand Down Expand Up @@ -149,12 +151,59 @@ py::list pyGetConnectedFaces(
return makePyPolygonsList(result);
}

py::array pyConnectFaces(const py::array_t<float>& vertices_array, const py::array_t<uint32_t>& indices_array)
{
// Fetch/cast input.
pybind11::buffer_info mesh_vertices_buffer = vertices_array.request();
pybind11::buffer_info mesh_indices_buffer = indices_array.request();
const std::span<const Point3F> vertices = std::span(static_cast<const Point3F*>(mesh_vertices_buffer.ptr), mesh_vertices_buffer.shape[0]);
const std::span<const Face> indices = std::span(static_cast<const Face*>(mesh_indices_buffer.ptr), mesh_indices_buffer.shape[0]);

// Define output shape.
const std::vector<py::ssize_t> shape = { static_cast<py::ssize_t>(indices.size()), 3 };
const std::vector<py::ssize_t> strides = { static_cast<py::ssize_t>(sizeof(int32_t) * shape[1]), static_cast<py::ssize_t>(sizeof(int32_t)) };
std::vector<FaceSigned> res(shape[0], { -1, -1, -1 });

// Do the actual calculation.
{
py::gil_scoped_release release;
connectFaces(vertices, indices, res);
}
return py::array(py::buffer_info(res.data(), strides[1], py::format_descriptor<int32_t>::format(), shape.size(), shape, strides));
}

py::bool_ pyCheckDownwardsFeatures(
const float support_angle,
const float close_to_buildplate_dist,
const float min_support_area,
const py::array_t<float>& vertices_array,
const py::array_t<uint32_t>& indices_array,
const py::array_t<int32_t>& mesh_faces_connectivity_array)
{
pybind11::buffer_info mesh_vertices_buffer = vertices_array.request();
pybind11::buffer_info mesh_indices_buffer = indices_array.request();
pybind11::buffer_info mesh_faces_connectivity_buffer = mesh_faces_connectivity_array.request();
const std::span<const Point3F> vertices = std::span(static_cast<const Point3F*>(mesh_vertices_buffer.ptr), mesh_vertices_buffer.shape[0]);
const std::span<const Face> indices = std::span(static_cast<const Face*>(mesh_indices_buffer.ptr), mesh_indices_buffer.shape[0]);
const std::span<const FaceSigned> face_connects = std::span(static_cast<FaceSigned*>(mesh_faces_connectivity_buffer.ptr), mesh_faces_connectivity_buffer.shape[0]);

bool res = false;
{
py::gil_scoped_release release;
res = checkForDownVertices(close_to_buildplate_dist, vertices, indices)
|| checkForDownFaces(support_angle, close_to_buildplate_dist, min_support_area, vertices, indices, face_connects);
}
return res;
}

PYBIND11_MODULE(pyUvula, module)
{
module.doc() = "UV-unwrapping library (or bindings to library), segmentation uses a classic normal-based grouping and charts packing uses xatlas";
module.attr("__version__") = PYUVULA_VERSION;

module.def("unwrap", &pyUnwrap, "Given the vertices, indices of a mesh, unwrap UV for texture-coordinates.");
module.def("project", &pyProject, "Projects a stroke polygon into an object texture.");
module.def("connectFaces", &pyConnectFaces, "Make a face-connectivity data-structure from raw vertices + indices input.");
module.def("checkDownwardsFeatures", &pyCheckDownwardsFeatures, "Checks for parts of a mesh that would be unsupported, given no supports.");
module.def("getConnectedFaces", &pyGetConnectedFaces, "Gets the polygons of the faces connected to the given initial face.");
}
9 changes: 1 addition & 8 deletions src/Triangle3F.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,7 @@
#include "geometry_utils.h"


Triangle3F::Triangle3F(const Point3F& p1, const Point3F& p2, const Point3F& p3)
: p1_(p1)
, p2_(p2)
, p3_(p3)
{
}

std::optional<Vector3F> Triangle3F::normal() const
{
return geometry_utils::triangleNormal(p1_, p2_, p3_);
return geometry_utils::triangleNormal(std::get<0>(*this), std::get<1>(*this), std::get<2>(*this));
}
5 changes: 5 additions & 0 deletions src/Vector3F.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ Vector3F Vector3F::cross(const Vector3F& other) const
return Vector3F((y_ * other.z_) - (z_ * other.y_), (z_ * other.x_) - (x_ * other.z_), (x_ * other.y_) - (y_ * other.x_));
}

Vector3F Vector3F::abs() const
{
return Vector3F(std::abs(x_), std::abs(y_), std::abs(z_));
}

Vector3F Vector3F::operator+(const Vector3F& other) const
{
return Vector3F(x_ + other.x_, y_ + other.y_, z_ + other.z_);
Expand Down
Loading