cudaslicer/python/cudaslicer/bindings.cpp
2026-09-26 21:22:01 +03:00

908 lines
37 KiB
C++

#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include <pybind11/stl.h>
#include "gpu_offset.h"
#include <vector>
#include <iostream>
#include <string>
namespace py = pybind11;
static int parse_backend_str(const std::string& backend) {
if (backend.empty() || backend == "auto" || backend == "AUTO") return GPU_OFFSET_BACKEND_AUTO;
if (backend == "cuda" || backend == "gpu" || backend == "CUDA" || backend == "GPU") return GPU_OFFSET_BACKEND_CUDA;
if (backend == "cpu" || backend == "compatibility" || backend == "CPU") return GPU_OFFSET_BACKEND_CPU;
throw std::invalid_argument("Invalid backend '" + backend + "'. Allowed options: 'auto', 'cuda', 'cpu'.");
}
// 1. Direct NumPy Mask Offsetting
py::array_t<uint8_t> py_offset_masks(
py::array_t<uint8_t, py::array::c_style | py::array::forcecast> in_masks,
int perimeters_count,
float layer_width,
float pitch_mm,
const std::vector<float>& offset_distances,
const std::string& backend
) {
py::buffer_info buf = in_masks.request();
int num_layers = 1;
int height = 0;
int width = 0;
if (buf.ndim == 2) {
num_layers = 1;
height = (int)buf.shape[0];
width = (int)buf.shape[1];
} else if (buf.ndim == 3) {
num_layers = (int)buf.shape[0];
height = (int)buf.shape[1];
width = (int)buf.shape[2];
} else {
throw std::runtime_error("Input mask must be a 2D (H, W) or 3D (N, H, W) numpy array.");
}
std::vector<float> resolved_offsets;
if (!offset_distances.empty()) {
resolved_offsets = offset_distances;
} else {
int P = (perimeters_count >= 0) ? perimeters_count : 2;
float W = (layer_width > 0.0f) ? layer_width : 0.45f;
resolved_offsets.resize(P + 1);
for (int i = 0; i < P; ++i) {
resolved_offsets[i] = (i + 0.5f) * W;
}
resolved_offsets[P] = (float)P * W;
}
int num_offsets = (int)resolved_offsets.size();
if (num_offsets == 0) {
throw std::runtime_error("offset distances cannot be empty.");
}
std::vector<ssize_t> out_shape;
if (buf.ndim == 2) {
out_shape = { (ssize_t)num_offsets, (ssize_t)height, (ssize_t)width };
} else {
out_shape = { (ssize_t)num_offsets, (ssize_t)num_layers, (ssize_t)height, (ssize_t)width };
}
py::array_t<uint8_t> out_masks(out_shape);
py::buffer_info out_buf = out_masks.request();
const uint8_t* ptr_in = static_cast<const uint8_t*>(buf.ptr);
uint8_t* ptr_out = static_cast<uint8_t*>(out_buf.ptr);
int req_backend = parse_backend_str(backend);
int prev_backend = gpu_offset_get_backend();
if (!backend.empty()) {
gpu_offset_set_backend(req_backend);
}
GpuOffsetStats stats = {0};
int ret;
{
py::gil_scoped_release release;
ret = gpu_offset_compute_masks(
ptr_in, width, height, num_layers, pitch_mm,
resolved_offsets.data(), num_offsets, ptr_out, &stats
);
}
if (!backend.empty()) {
gpu_offset_set_backend(prev_backend);
}
if (ret == GPU_OFFSET_ERR_NO_CUDA) {
throw std::runtime_error("CUDA backend was requested, but no functional CUDA-capable device was found.");
} else if (ret != GPU_OFFSET_SUCCESS) {
throw std::runtime_error("Offset computation failed in compute backend.");
}
return out_masks;
}
// 2. Direct Python Polygon Offsetting
py::list py_offset_polygon_layers(
py::list py_layers,
float min_x, float min_y, float max_x, float max_y,
int perimeters_count,
float layer_width,
float pitch_mm,
float pad_mm,
bool export_infill_mask,
const std::vector<float>& offset_distances,
const std::string& backend,
bool output_3d = false
) {
size_t num_layers = py_layers.size();
if (num_layers == 0) return py::list();
std::vector<GpuSliceLayer> c_layers(num_layers);
std::vector<std::vector<GpuContour>> c_contours_pool(num_layers);
std::vector<std::vector<std::vector<GpuPoint2D>>> c_points_pool(num_layers);
for (size_t l = 0; l < num_layers; ++l) {
py::dict layer_dict = py_layers[l].cast<py::dict>();
float z_h = layer_dict.contains("z") ? layer_dict["z"].cast<float>() : 0.0f;
py::list cnt_list = layer_dict["contours"].cast<py::list>();
size_t num_cnts = cnt_list.size();
c_layers[l].z_height = z_h;
c_layers[l].num_contours = num_cnts;
c_contours_pool[l].resize(num_cnts);
c_points_pool[l].resize(num_cnts);
for (size_t c = 0; c < num_cnts; ++c) {
py::array_t<float, py::array::c_style | py::array::forcecast> arr = cnt_list[c].cast<py::array_t<float>>();
py::buffer_info buf = arr.request();
size_t n_pts = buf.shape[0];
auto& pts = c_points_pool[l][c];
pts.resize(n_pts);
const float* raw_data = static_cast<const float*>(buf.ptr);
for (size_t p = 0; p < n_pts; ++p) {
pts[p].x = raw_data[p * 2 + 0];
pts[p].y = raw_data[p * 2 + 1];
}
c_contours_pool[l][c].points = pts.data();
c_contours_pool[l][c].num_points = n_pts;
c_contours_pool[l][c].is_hole = 0;
}
c_layers[l].contours = c_contours_pool[l].data();
}
GpuOffsetConfig config = {0};
config.pitch_mm = pitch_mm;
config.pad_mm = pad_mm;
config.perimeters_count = perimeters_count;
config.layer_width = layer_width;
config.export_infill_mask = export_infill_mask ? 1 : 0;
config.use_gpu_rasterizer = 1;
config.backend = parse_backend_str(backend);
if (!offset_distances.empty()) {
config.offset_distances_mm = offset_distances.data();
config.num_offsets = offset_distances.size();
} else {
config.offset_distances_mm = nullptr;
config.num_offsets = 0;
}
GpuLayerResult* results = nullptr;
GpuOffsetStats stats = {0};
int ret;
{
py::gil_scoped_release release;
ret = gpu_offset_polygon_layers(
c_layers.data(), num_layers,
min_x, min_y, max_x, max_y,
&config, &results, &stats
);
}
if (ret == GPU_OFFSET_ERR_NO_CUDA) {
throw std::runtime_error("CUDA backend was requested, but no functional CUDA-capable device was found.");
} else if (ret != GPU_OFFSET_SUCCESS || !results) {
throw std::runtime_error("Polygon offsetting failed in compute backend.");
}
py::list py_layer_results;
for (size_t l = 0; l < num_layers; ++l) {
py::dict py_layer_dict;
py_layer_dict["z"] = results[l].z_height;
// Perimeters
py::list py_perimeters;
for (size_t o = 0; o < results[l].num_perimeters; ++o) {
py::dict py_perim_dict;
py_perim_dict["offset_mm"] = results[l].perimeters[o].offset_distance_mm;
py::list py_cnt_list;
for (size_t c = 0; c < results[l].perimeters[o].num_contours; ++c) {
const auto& cnt = results[l].perimeters[o].contours[c];
if (output_3d && cnt.num_points > 0) {
py::array_t<float> pts_arr({ (ssize_t)(cnt.num_points + 1), (ssize_t)3 });
auto r = pts_arr.mutable_unchecked<2>();
float z = results[l].z_height;
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
r(p, 2) = z;
}
r(cnt.num_points, 0) = cnt.points[0].x;
r(cnt.num_points, 1) = cnt.points[0].y;
r(cnt.num_points, 2) = z;
py_cnt_list.append(pts_arr);
} else {
py::array_t<float> pts_arr({ (ssize_t)cnt.num_points, (ssize_t)2 });
auto r = pts_arr.mutable_unchecked<2>();
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
}
py_cnt_list.append(pts_arr);
}
}
py_perim_dict["contours"] = py_cnt_list;
py_perimeters.append(py_perim_dict);
}
py_layer_dict["perimeters"] = py_perimeters;
// Infill Contours
py::list py_infill_cnts;
for (size_t c = 0; c < results[l].infill.num_contours; ++c) {
const auto& cnt = results[l].infill.contours[c];
if (output_3d && cnt.num_points > 0) {
py::array_t<float> pts_arr({ (ssize_t)(cnt.num_points + 1), (ssize_t)3 });
auto r = pts_arr.mutable_unchecked<2>();
float z = results[l].z_height;
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
r(p, 2) = z;
}
r(cnt.num_points, 0) = cnt.points[0].x;
r(cnt.num_points, 1) = cnt.points[0].y;
r(cnt.num_points, 2) = z;
py_infill_cnts.append(pts_arr);
} else {
py::array_t<float> pts_arr({ (ssize_t)cnt.num_points, (ssize_t)2 });
auto r = pts_arr.mutable_unchecked<2>();
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
}
py_infill_cnts.append(pts_arr);
}
}
py_layer_dict["infill_contours"] = py_infill_cnts;
py_layer_dict["infill_offset_mm"] = results[l].infill.offset_distance_mm;
// Infill 2D Mask
if (results[l].infill.mask != nullptr) {
int H = results[l].infill.height;
int W = results[l].infill.width;
py::array_t<uint8_t> mask_arr({ (ssize_t)H, (ssize_t)W });
py::buffer_info mb = mask_arr.request();
memcpy(mb.ptr, results[l].infill.mask, (size_t)H * W * sizeof(uint8_t));
py_layer_dict["infill_mask"] = mask_arr;
}
py_layer_results.append(py_layer_dict);
}
gpu_offset_free_results(results, num_layers);
return py_layer_results;
}
// 3. Direct 3D Mesh Slicing & Offsetting
py::list py_slice_mesh_and_offset(
py::array_t<float, py::array::c_style | py::array::forcecast> triangles_arr,
float min_x, float min_y, float min_z,
float max_x, float max_y, float max_z,
float layer_height,
float first_layer_height,
int perimeters_count,
float layer_width,
float pitch_mm,
float pad_mm,
bool export_infill_mask,
const std::vector<float>& offset_distances,
const std::string& backend,
float simplify_tolerance_mm,
bool subpixel_lerp,
bool output_3d = false
) {
py::buffer_info buf = triangles_arr.request();
size_t num_tris = 0;
if (buf.ndim == 3 && buf.shape[1] == 3 && buf.shape[2] == 3) {
num_tris = buf.shape[0];
} else if (buf.ndim == 2 && buf.shape[1] == 9) {
num_tris = buf.shape[0];
} else {
throw std::runtime_error("Triangles array must be of shape (N, 3, 3) or (N, 9) float32.");
}
if (num_tris == 0) return py::list();
const GpuTriangle3D* tris_ptr = static_cast<const GpuTriangle3D*>(buf.ptr);
GpuOffsetConfig config = {0};
config.pitch_mm = pitch_mm;
config.pad_mm = pad_mm;
config.perimeters_count = perimeters_count;
config.layer_width = layer_width;
config.first_layer_height = first_layer_height;
config.export_infill_mask = export_infill_mask ? 1 : 0;
config.simplify_tolerance_mm = simplify_tolerance_mm;
config.subpixel_lerp = subpixel_lerp ? 1 : 0;
config.use_gpu_rasterizer = 1;
config.backend = parse_backend_str(backend);
if (!offset_distances.empty()) {
config.offset_distances_mm = offset_distances.data();
config.num_offsets = offset_distances.size();
} else {
config.offset_distances_mm = nullptr;
config.num_offsets = 0;
}
GpuLayerResult* results = nullptr;
size_t out_num_layers = 0;
GpuOffsetStats stats = {0};
int ret;
{
py::gil_scoped_release release;
ret = gpu_offset_slice_mesh_and_offset(
tris_ptr, num_tris,
min_x, min_y, min_z, max_x, max_y, max_z,
layer_height, first_layer_height, &config,
&results, &out_num_layers, &stats
);
}
if (ret == GPU_OFFSET_ERR_NO_CUDA) {
throw std::runtime_error("CUDA backend was requested, but no functional CUDA-capable device was found.");
} else if (ret != GPU_OFFSET_SUCCESS || !results) {
throw std::runtime_error("Slice mesh and offset failed in compute backend.");
}
py::list py_layer_results;
for (size_t l = 0; l < out_num_layers; ++l) {
py::dict py_layer_dict;
py_layer_dict["z"] = results[l].z_height;
// Perimeters
py::list py_perimeters;
for (size_t o = 0; o < results[l].num_perimeters; ++o) {
py::dict py_perim_dict;
py_perim_dict["offset_mm"] = results[l].perimeters[o].offset_distance_mm;
py::list py_cnt_list;
for (size_t c = 0; c < results[l].perimeters[o].num_contours; ++c) {
const auto& cnt = results[l].perimeters[o].contours[c];
if (output_3d && cnt.num_points > 0) {
py::array_t<float> pts_arr({ (ssize_t)(cnt.num_points + 1), (ssize_t)3 });
auto r = pts_arr.mutable_unchecked<2>();
float z = results[l].z_height;
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
r(p, 2) = z;
}
r(cnt.num_points, 0) = cnt.points[0].x;
r(cnt.num_points, 1) = cnt.points[0].y;
r(cnt.num_points, 2) = z;
py_cnt_list.append(pts_arr);
} else {
py::array_t<float> pts_arr({ (ssize_t)cnt.num_points, (ssize_t)2 });
auto r = pts_arr.mutable_unchecked<2>();
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
}
py_cnt_list.append(pts_arr);
}
}
py_perim_dict["contours"] = py_cnt_list;
py_perimeters.append(py_perim_dict);
}
py_layer_dict["perimeters"] = py_perimeters;
// Infill Contours
py::list py_infill_cnts;
for (size_t c = 0; c < results[l].infill.num_contours; ++c) {
const auto& cnt = results[l].infill.contours[c];
if (output_3d && cnt.num_points > 0) {
py::array_t<float> pts_arr({ (ssize_t)(cnt.num_points + 1), (ssize_t)3 });
auto r = pts_arr.mutable_unchecked<2>();
float z = results[l].z_height;
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
r(p, 2) = z;
}
r(cnt.num_points, 0) = cnt.points[0].x;
r(cnt.num_points, 1) = cnt.points[0].y;
r(cnt.num_points, 2) = z;
py_infill_cnts.append(pts_arr);
} else {
py::array_t<float> pts_arr({ (ssize_t)cnt.num_points, (ssize_t)2 });
auto r = pts_arr.mutable_unchecked<2>();
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
}
py_infill_cnts.append(pts_arr);
}
}
py_layer_dict["infill_contours"] = py_infill_cnts;
py_layer_dict["infill_offset_mm"] = results[l].infill.offset_distance_mm;
// Infill 2D Mask
if (results[l].infill.mask != nullptr) {
int H = results[l].infill.height;
int W = results[l].infill.width;
py::array_t<uint8_t> mask_arr({ (ssize_t)H, (ssize_t)W });
py::buffer_info mb = mask_arr.request();
memcpy(mb.ptr, results[l].infill.mask, (size_t)H * W * sizeof(uint8_t));
py_layer_dict["infill_mask"] = mask_arr;
}
py_layer_results.append(py_layer_dict);
}
gpu_offset_free_results(results, out_num_layers);
return py_layer_results;
}
#include "gpu_sdf.h"
#include <memory>
class PyGpuSdfStack {
private:
GpuSdfStackHandle handle_;
public:
PyGpuSdfStack(GpuSdfStackHandle handle) : handle_(handle) {
if (!handle_) throw std::runtime_error("Failed to allocate GPU SDF Stack.");
}
~PyGpuSdfStack() {
if (handle_) {
gpu_sdf_destroy(handle_);
handle_ = nullptr;
}
}
PyGpuSdfStack(const PyGpuSdfStack&) = delete;
PyGpuSdfStack& operator=(const PyGpuSdfStack&) = delete;
PyGpuSdfStack(PyGpuSdfStack&& other) noexcept : handle_(other.handle_) {
other.handle_ = nullptr;
}
PyGpuSdfStack& operator=(PyGpuSdfStack&& other) noexcept {
if (this != &other) {
if (handle_) gpu_sdf_destroy(handle_);
handle_ = other.handle_;
other.handle_ = nullptr;
}
return *this;
}
GpuSdfStackHandle get_handle() const { return handle_; }
std::shared_ptr<PyGpuSdfStack> clone() const {
return std::make_shared<PyGpuSdfStack>(gpu_sdf_clone(handle_));
}
py::dict get_metadata() const {
GpuSdfMetadata meta;
if (gpu_sdf_get_metadata(handle_, &meta) != GPU_OFFSET_SUCCESS) {
throw std::runtime_error("Failed to get SDF metadata.");
}
py::dict d;
d["width"] = meta.width;
d["height"] = meta.height;
d["num_layers"] = meta.num_layers;
d["min_x_um"] = meta.min_x_um;
d["min_y_um"] = meta.min_y_um;
d["pitch_um"] = meta.pitch_um;
d["min_x_mm"] = (float)meta.min_x_um * 0.001f;
d["min_y_mm"] = (float)meta.min_y_um * 0.001f;
d["pitch_mm"] = (float)meta.pitch_um * 0.001f;
return d;
}
py::array_t<int32_t> get_layer_z_um() const {
GpuSdfMetadata meta;
gpu_sdf_get_metadata(handle_, &meta);
py::array_t<int32_t> arr((ssize_t)meta.num_layers);
auto buf = arr.request();
gpu_sdf_get_layer_z_um(handle_, static_cast<int32_t*>(buf.ptr));
return arr;
}
py::array_t<float> get_layer_z_mm() const {
GpuSdfMetadata meta;
gpu_sdf_get_metadata(handle_, &meta);
std::vector<int32_t> z_um(meta.num_layers);
gpu_sdf_get_layer_z_um(handle_, z_um.data());
py::array_t<float> arr((ssize_t)meta.num_layers);
auto r = arr.mutable_unchecked<1>();
for (size_t i = 0; i < meta.num_layers; ++i) {
r(i) = (float)z_um[i] * 0.001f;
}
return arr;
}
py::array_t<float> to_numpy() const {
GpuSdfMetadata meta;
gpu_sdf_get_metadata(handle_, &meta);
std::vector<ssize_t> shape = { (ssize_t)meta.num_layers, (ssize_t)meta.height, (ssize_t)meta.width };
py::array_t<float> out_arr(shape);
auto buf = out_arr.request();
if (gpu_sdf_to_host(handle_, static_cast<float*>(buf.ptr)) != GPU_OFFSET_SUCCESS) {
throw std::runtime_error("Failed to copy SDF to NumPy host buffer.");
}
return out_arr;
}
std::shared_ptr<PyGpuSdfStack> csg_op(
const PyGpuSdfStack& other,
const std::string& op_name,
const std::string& match_mode_name = "dominant",
float blend_radius_mm = 0.0f
) const {
GpuSdfOp op = GPU_SDF_OP_UNION;
if (op_name == "union" || op_name == "or" || op_name == "+") op = GPU_SDF_OP_UNION;
else if (op_name == "intersection" || op_name == "and" || op_name == "&") op = GPU_SDF_OP_INTERSECTION;
else if (op_name == "difference" || op_name == "subtract" || op_name == "-") op = GPU_SDF_OP_DIFFERENCE;
else if (op_name == "smooth_union") op = GPU_SDF_OP_SMOOTH_UNION;
else if (op_name == "smooth_intersection" || op_name == "smooth_intersect") op = GPU_SDF_OP_SMOOTH_INTERSECT;
else if (op_name == "smooth_difference" || op_name == "smooth_diff") op = GPU_SDF_OP_SMOOTH_DIFF;
else throw std::invalid_argument("Unknown CSG op: " + op_name);
GpuSdfLayerMatchMode match = GPU_SDF_LAYER_MATCH_A_DOMINANT;
if (match_mode_name == "dominant" || match_mode_name == "a") match = GPU_SDF_LAYER_MATCH_A_DOMINANT;
else if (match_mode_name == "union") match = GPU_SDF_LAYER_MATCH_UNION;
else if (match_mode_name == "intersection" || match_mode_name == "intersect") match = GPU_SDF_LAYER_MATCH_INTERSECTION;
else throw std::invalid_argument("Unknown layer match mode: " + match_mode_name);
GpuSdfStackHandle res = gpu_sdf_csg_op(handle_, other.handle_, op, match, blend_radius_mm);
if (!res) throw std::runtime_error("CSG operation failed.");
return std::make_shared<PyGpuSdfStack>(res);
}
std::shared_ptr<PyGpuSdfStack> op_union(const PyGpuSdfStack& other, float blend_k = 0.0f) const {
return csg_op(other, blend_k > 0.0f ? "smooth_union" : "union", "dominant", blend_k);
}
std::shared_ptr<PyGpuSdfStack> op_intersection(const PyGpuSdfStack& other, float blend_k = 0.0f) const {
return csg_op(other, blend_k > 0.0f ? "smooth_intersection" : "intersection", "dominant", blend_k);
}
std::shared_ptr<PyGpuSdfStack> op_difference(const PyGpuSdfStack& other, float blend_k = 0.0f) const {
return csg_op(other, blend_k > 0.0f ? "smooth_difference" : "difference", "dominant", blend_k);
}
std::shared_ptr<PyGpuSdfStack> offset(float delta_mm) const {
GpuSdfStackHandle res = gpu_sdf_offset(handle_, delta_mm);
if (!res) throw std::runtime_error("SDF offset failed.");
return std::make_shared<PyGpuSdfStack>(res);
}
std::shared_ptr<PyGpuSdfStack> shell(float thickness_mm) const {
GpuSdfStackHandle res = gpu_sdf_shell(handle_, thickness_mm);
if (!res) throw std::runtime_error("SDF shell failed.");
return std::make_shared<PyGpuSdfStack>(res);
}
py::list vectorize(
int perimeters_count = 2,
float layer_width = 0.45f,
bool export_infill_mask = true,
float simplify_tolerance_mm = 0.02f,
bool subpixel_lerp = true,
const std::vector<float>& offset_distances = std::vector<float>(),
bool output_3d = false
) const {
GpuOffsetConfig config = {0};
config.perimeters_count = perimeters_count;
config.layer_width = layer_width;
config.export_infill_mask = export_infill_mask ? 1 : 0;
config.simplify_tolerance_mm = simplify_tolerance_mm;
config.subpixel_lerp = subpixel_lerp ? 1 : 0;
config.use_gpu_rasterizer = 1;
config.backend = GPU_OFFSET_BACKEND_CUDA;
if (!offset_distances.empty()) {
config.offset_distances_mm = offset_distances.data();
config.num_offsets = offset_distances.size();
}
GpuLayerResult* results = nullptr;
size_t out_num_layers = 0;
GpuOffsetStats stats = {0};
int ret = gpu_sdf_vectorize(handle_, &config, &results, &out_num_layers, &stats);
if (ret != GPU_OFFSET_SUCCESS || !results) {
throw std::runtime_error("SDF vectorization failed.");
}
py::list py_layer_results;
for (size_t l = 0; l < out_num_layers; ++l) {
py::dict py_layer_dict;
py_layer_dict["z"] = results[l].z_height;
py_layer_dict["z_um"] = (int32_t)std::round(results[l].z_height * 1000.0f);
// Perimeters
py::list py_perimeters;
for (size_t o = 0; o < results[l].num_perimeters; ++o) {
py::dict py_perim_dict;
py_perim_dict["offset_mm"] = results[l].perimeters[o].offset_distance_mm;
py::list py_cnt_list;
for (size_t c = 0; c < results[l].perimeters[o].num_contours; ++c) {
const auto& cnt = results[l].perimeters[o].contours[c];
if (output_3d && cnt.num_points > 0) {
py::array_t<float> pts_arr({ (ssize_t)(cnt.num_points + 1), (ssize_t)3 });
auto r = pts_arr.mutable_unchecked<2>();
float z = results[l].z_height;
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
r(p, 2) = z;
}
r(cnt.num_points, 0) = cnt.points[0].x;
r(cnt.num_points, 1) = cnt.points[0].y;
r(cnt.num_points, 2) = z;
py_cnt_list.append(pts_arr);
} else {
py::array_t<float> pts_arr({ (ssize_t)cnt.num_points, (ssize_t)2 });
auto r = pts_arr.mutable_unchecked<2>();
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
}
py_cnt_list.append(pts_arr);
}
}
py_perim_dict["contours"] = py_cnt_list;
py_perimeters.append(py_perim_dict);
}
py_layer_dict["perimeters"] = py_perimeters;
// Infill Contours
py::list py_infill_cnts;
for (size_t c = 0; c < results[l].infill.num_contours; ++c) {
const auto& cnt = results[l].infill.contours[c];
if (output_3d && cnt.num_points > 0) {
py::array_t<float> pts_arr({ (ssize_t)(cnt.num_points + 1), (ssize_t)3 });
auto r = pts_arr.mutable_unchecked<2>();
float z = results[l].z_height;
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
r(p, 2) = z;
}
r(cnt.num_points, 0) = cnt.points[0].x;
r(cnt.num_points, 1) = cnt.points[0].y;
r(cnt.num_points, 2) = z;
py_infill_cnts.append(pts_arr);
} else {
py::array_t<float> pts_arr({ (ssize_t)cnt.num_points, (ssize_t)2 });
auto r = pts_arr.mutable_unchecked<2>();
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
}
py_infill_cnts.append(pts_arr);
}
}
py_layer_dict["infill_contours"] = py_infill_cnts;
py_layer_dict["infill_offset_mm"] = results[l].infill.offset_distance_mm;
// Infill 2D Mask
if (results[l].infill.mask != nullptr) {
int H = results[l].infill.height;
int W = results[l].infill.width;
py::array_t<uint8_t> mask_arr({ (ssize_t)H, (ssize_t)W });
py::buffer_info mb = mask_arr.request();
memcpy(mb.ptr, results[l].infill.mask, (size_t)H * W * sizeof(uint8_t));
py_layer_dict["infill_mask"] = mask_arr;
}
py_layer_results.append(py_layer_dict);
}
gpu_offset_free_results(results, out_num_layers);
return py_layer_results;
}
py::dict cuda_array_interface() const {
GpuSdfMetadata meta;
gpu_sdf_get_metadata(handle_, &meta);
float* d_ptr = gpu_sdf_get_device_ptr(handle_);
py::dict iface;
iface["shape"] = py::make_tuple(meta.num_layers, meta.height, meta.width);
iface["typestr"] = "<f4";
iface["data"] = py::make_tuple((uintptr_t)d_ptr, false);
iface["version"] = 3;
return iface;
}
};
// Python Factory Helpers
std::shared_ptr<PyGpuSdfStack> py_slice_mesh_to_sdf(
py::array_t<float, py::array::c_style | py::array::forcecast> triangles_arr,
float min_x, float min_y, float min_z,
float max_x, float max_y, float max_z,
float layer_height_mm = 0.20f,
float first_layer_height_mm = 0.0f,
float pitch_mm = 0.10f,
float pad_mm = 1.5f
) {
py::buffer_info buf = triangles_arr.request();
size_t num_tris = 0;
if (buf.ndim == 3 && buf.shape[1] == 3 && buf.shape[2] == 3) {
num_tris = buf.shape[0];
} else if (buf.ndim == 2 && buf.shape[1] == 9) {
num_tris = buf.shape[0];
} else {
throw std::runtime_error("Triangles array must be of shape (N, 3, 3) or (N, 9) float32.");
}
if (num_tris == 0) throw std::runtime_error("Triangle array is empty.");
const GpuTriangle3D* tris_ptr = static_cast<const GpuTriangle3D*>(buf.ptr);
GpuSdfStackHandle handle = gpu_sdf_from_mesh(
tris_ptr, num_tris,
min_x, min_y, min_z, max_x, max_y, max_z,
layer_height_mm, first_layer_height_mm, pitch_mm, pad_mm
);
if (!handle) throw std::runtime_error("Failed to slice mesh to GPU SDF stack.");
return std::make_shared<PyGpuSdfStack>(handle);
}
std::shared_ptr<PyGpuSdfStack> py_masks_to_sdf(
py::array_t<uint8_t, py::array::c_style | py::array::forcecast> in_masks,
py::array_t<int32_t, py::array::c_style | py::array::forcecast> layer_z_um_arr,
float min_x_mm = 0.0f,
float min_y_mm = 0.0f,
float pitch_mm = 0.10f
) {
py::buffer_info buf = in_masks.request();
size_t num_layers = 1;
int height = 0;
int width = 0;
if (buf.ndim == 2) {
num_layers = 1;
height = (int)buf.shape[0];
width = (int)buf.shape[1];
} else if (buf.ndim == 3) {
num_layers = (size_t)buf.shape[0];
height = (int)buf.shape[1];
width = (int)buf.shape[2];
} else {
throw std::runtime_error("Input mask must be 2D (H, W) or 3D (N, H, W).");
}
py::buffer_info z_buf = layer_z_um_arr.request();
const int32_t* z_ptr = nullptr;
if (z_buf.size == (ssize_t)num_layers) {
z_ptr = static_cast<const int32_t*>(z_buf.ptr);
}
int32_t min_x_um = (int32_t)std::round(min_x_mm * 1000.0f);
int32_t min_y_um = (int32_t)std::round(min_y_mm * 1000.0f);
int32_t pitch_um = (int32_t)std::round(pitch_mm * 1000.0f);
GpuSdfStackHandle handle = gpu_sdf_from_masks(
static_cast<const uint8_t*>(buf.ptr),
width, height, num_layers,
z_ptr, min_x_um, min_y_um, pitch_um
);
if (!handle) throw std::runtime_error("Failed to create GPU SDF stack from masks.");
return std::make_shared<PyGpuSdfStack>(handle);
}
PYBIND11_MODULE(_cudaslicer_backend, m) {
m.doc() = "GPU/CPU Slicing, Signed Distance Fields (SDF), CSG Booleans & Vectorizer Extension";
m.def("has_cuda", &gpu_offset_has_cuda, "Returns 1 if a functional CUDA GPU is present, 0 otherwise.");
m.def("is_cuda_available", &gpu_offset_has_cuda, "Returns 1 if a functional CUDA GPU is present, 0 otherwise.");
m.def("set_backend", [](const std::string& b) {
int code = gpu_offset_set_backend(parse_backend_str(b));
if (code != GPU_OFFSET_SUCCESS) throw std::invalid_argument("Failed to set backend.");
}, py::arg("backend") = "auto", "Set global compute backend ('auto', 'cuda', 'cpu').");
m.def("get_backend", []() -> std::string {
int b = gpu_offset_get_backend();
if (b == GPU_OFFSET_BACKEND_CUDA) return "cuda";
if (b == GPU_OFFSET_BACKEND_CPU) return "cpu";
return "auto";
}, "Get global compute backend ('auto', 'cuda', 'cpu').");
m.def("init", &gpu_offset_init, py::arg("device_id") = 0, "Initialize CUDA runtime and allocate stream.");
m.def("cleanup", &gpu_offset_cleanup, "Release backend resources.");
m.def("offset_masks", &py_offset_masks,
py::arg("in_masks"),
py::arg("perimeters_count") = 2,
py::arg("layer_width") = 0.45f,
py::arg("pitch_mm") = 0.10f,
py::arg("offset_distances") = std::vector<float>(),
py::arg("backend") = "auto",
"Offset 2D (H,W) or 3D (N,H,W) binary occupancy numpy masks.");
m.def("offset_polygon_layers", &py_offset_polygon_layers,
py::arg("py_layers"), py::arg("min_x"), py::arg("min_y"),
py::arg("max_x"), py::arg("max_y"),
py::arg("perimeters_count") = 2,
py::arg("layer_width") = 0.45f,
py::arg("pitch_mm") = 0.10f,
py::arg("pad_mm") = 1.5f,
py::arg("export_infill_mask") = true,
py::arg("offset_distances") = std::vector<float>(),
py::arg("backend") = "auto",
py::arg("output_3d") = false,
"Compute polygon perimeter offsets and infill mask.");
m.def("slice_mesh_and_offset", &py_slice_mesh_and_offset,
py::arg("triangles_arr"), py::arg("min_x"), py::arg("min_y"), py::arg("min_z"),
py::arg("max_x"), py::arg("max_y"), py::arg("max_z"),
py::arg("layer_height") = 0.20f,
py::arg("first_layer_height") = 0.0f,
py::arg("perimeters_count") = 2,
py::arg("layer_width") = 0.45f,
py::arg("pitch_mm") = 0.10f,
py::arg("pad_mm") = 1.5f,
py::arg("export_infill_mask") = true,
py::arg("offset_distances") = std::vector<float>(),
py::arg("backend") = "auto",
py::arg("simplify_tolerance_mm") = 0.0f,
py::arg("subpixel_lerp") = true,
py::arg("output_3d") = false,
"Mesh slicing, perimeter offsetting, and infill mask generation.");
// GPU SDF Stack Factory Functions
m.def("slice_mesh_to_sdf", &py_slice_mesh_to_sdf,
py::arg("triangles_arr"), py::arg("min_x"), py::arg("min_y"), py::arg("min_z"),
py::arg("max_x"), py::arg("max_y"), py::arg("max_z"),
py::arg("layer_height_mm") = 0.20f,
py::arg("first_layer_height_mm") = 0.0f,
py::arg("pitch_mm") = 0.10f,
py::arg("pad_mm") = 1.5f,
"Slice 3D mesh directly to a GPU-resident Signed Distance Field stack.");
m.def("masks_to_sdf", &py_masks_to_sdf,
py::arg("in_masks"),
py::arg("layer_z_um_arr") = py::array_t<int32_t>(),
py::arg("min_x_mm") = 0.0f,
py::arg("min_y_mm") = 0.0f,
py::arg("pitch_mm") = 0.10f,
"Compute GPU Signed Distance Field stack from 2D/3D binary raster masks.");
// GPU SDF Stack Class Binding
py::class_<PyGpuSdfStack, std::shared_ptr<PyGpuSdfStack>>(m, "GpuSdfStack")
.def("get_metadata", &PyGpuSdfStack::get_metadata, "Get dictionary of spatial metadata.")
.def("get_layer_z_um", &PyGpuSdfStack::get_layer_z_um, "Get 1D array of layer heights in micrometers.")
.def("get_layer_z_mm", &PyGpuSdfStack::get_layer_z_mm, "Get 1D array of layer heights in millimeters.")
.def("to_numpy", &PyGpuSdfStack::to_numpy, "Download SDF float array (N, H, W) to CPU NumPy buffer.")
.def("clone", &PyGpuSdfStack::clone, "Clone this SDF stack on GPU.")
.def("csg_op", &PyGpuSdfStack::csg_op,
py::arg("other"),
py::arg("op_name") = "union",
py::arg("match_mode_name") = "dominant",
py::arg("blend_radius_mm") = 0.0f,
"Execute a GPU CSG operation with another SDF stack.")
.def("op_union", &PyGpuSdfStack::op_union, py::arg("other"), py::arg("blend_radius_mm") = 0.0f, "Compute A U B on GPU.")
.def("op_intersection", &PyGpuSdfStack::op_intersection, py::arg("other"), py::arg("blend_radius_mm") = 0.0f, "Compute A ^ B on GPU.")
.def("op_difference", &PyGpuSdfStack::op_difference, py::arg("other"), py::arg("blend_radius_mm") = 0.0f, "Compute A \\ B on GPU.")
.def("offset", &PyGpuSdfStack::offset, py::arg("delta_mm"), "Offset/dilate (delta > 0) or inset/erode (delta < 0) the SDF.")
.def("shell", &PyGpuSdfStack::shell, py::arg("thickness_mm"), "Hollow/shell the SDF with specified wall thickness.")
.def("vectorize", &PyGpuSdfStack::vectorize,
py::arg("perimeters_count") = 2,
py::arg("layer_width") = 0.45f,
py::arg("export_infill_mask") = true,
py::arg("simplify_tolerance_mm") = 0.02f,
py::arg("subpixel_lerp") = true,
py::arg("offset_distances") = std::vector<float>(),
py::arg("output_3d") = false,
"Vectorize SDF stack directly with sub-pixel LERP and RDP polygon simplification.")
.def_property_readonly("__cuda_array_interface__", &PyGpuSdfStack::cuda_array_interface,
"Zero-copy CUDA array interface for PyTorch / CuPy integration.");
}