908 lines
37 KiB
C++
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.");
|
|
}
|
|
|