cudaslicer
This commit is contained in:
parent
1019a91e34
commit
9cc37e1128
9 changed files with 2431 additions and 1 deletions
|
|
@ -31,6 +31,7 @@ include_directories(
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
add_library(cudaslicer_offset SHARED
|
add_library(cudaslicer_offset SHARED
|
||||||
src/gpu_offset.cu
|
src/gpu_offset.cu
|
||||||
|
src/gpu_sdf.cu
|
||||||
src/cpu_offset.cpp
|
src/cpu_offset.cpp
|
||||||
)
|
)
|
||||||
set_target_properties(cudaslicer_offset PROPERTIES
|
set_target_properties(cudaslicer_offset PROPERTIES
|
||||||
|
|
@ -45,6 +46,7 @@ target_link_libraries(cudaslicer_offset PUBLIC cudart)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
add_library(cudaslicer_offset_static STATIC
|
add_library(cudaslicer_offset_static STATIC
|
||||||
src/gpu_offset.cu
|
src/gpu_offset.cu
|
||||||
|
src/gpu_sdf.cu
|
||||||
src/cpu_offset.cpp
|
src/cpu_offset.cpp
|
||||||
)
|
)
|
||||||
set_target_properties(cudaslicer_offset_static PROPERTIES
|
set_target_properties(cudaslicer_offset_static PROPERTIES
|
||||||
|
|
|
||||||
118
examples/gpu_sdf_csg_example.py
Normal file
118
examples/gpu_sdf_csg_example.py
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
GPU-Resident Signed Distance Field (SDF) CSG & Infill Example
|
||||||
|
=============================================================
|
||||||
|
Demonstrates chaining 3D mesh slicing, Boolean CSG operations (Union, Intersection,
|
||||||
|
Difference, Smooth Blend), constant-distance offsetting, volume shelling, procedural
|
||||||
|
3D gyroid infill generation, and contour vectorization entirely in GPU VRAM without
|
||||||
|
host-to-device memory roundtrips.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Ensure local build is in path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "python"))
|
||||||
|
import cudaslicer
|
||||||
|
import trimesh
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 70)
|
||||||
|
print(" CUDASlicer: GPU Signed Distance Field (SDF) & CSG Engine")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
if not cudaslicer.has_cuda():
|
||||||
|
print("[Error] CUDA-capable GPU is required for this demo.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 1. Load or generate 3D models
|
||||||
|
stl_path = os.path.join(os.path.dirname(__file__), "..", "benchy.stl")
|
||||||
|
if os.path.exists(stl_path):
|
||||||
|
print(f"\n[1] Loading main model: {stl_path}")
|
||||||
|
mesh_a = trimesh.load(stl_path)
|
||||||
|
else:
|
||||||
|
print("\n[1] Generating synthetic main cylinder...")
|
||||||
|
mesh_a = trimesh.creation.cylinder(radius=15.0, height=30.0)
|
||||||
|
|
||||||
|
# Generate a modifier box volume to subtract from the model
|
||||||
|
print("[1] Generating modifier cutting box...")
|
||||||
|
mesh_b = trimesh.creation.box(extents=(12.0, 60.0, 25.0))
|
||||||
|
mesh_b.apply_translation((0.0, 0.0, 15.0))
|
||||||
|
|
||||||
|
# 2. Direct GPU Slicing to SDF Stacks
|
||||||
|
layer_height_mm = 0.20
|
||||||
|
pitch_mm = 0.10
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
sdf_a = cudaslicer.slice_mesh_to_sdf(mesh_a, layer_height=layer_height_mm, pitch_mm=pitch_mm)
|
||||||
|
sdf_b = cudaslicer.slice_mesh_to_sdf(mesh_b, layer_height=layer_height_mm, pitch_mm=pitch_mm)
|
||||||
|
t_slice = (time.perf_counter() - t0) * 1000.0
|
||||||
|
|
||||||
|
print(f"\n[2] Direct GPU Slicing & Signed Distance Generation ({t_slice:.2f} ms):")
|
||||||
|
print(f" - Main Model Stack: {sdf_a}")
|
||||||
|
print(f" Layer Z range (µm): {sdf_a.layer_z_um[0]} µm to {sdf_a.layer_z_um[-1]} µm ({len(sdf_a.layer_z_um)} layers)")
|
||||||
|
print(f" - Modifier Box Stack: {sdf_b}")
|
||||||
|
print(f" Layer Z range (µm): {sdf_b.layer_z_um[0]} µm to {sdf_b.layer_z_um[-1]} µm ({len(sdf_b.layer_z_um)} layers)")
|
||||||
|
|
||||||
|
# 3. GPU-Resident Boolean Operations (Zero CPU transfers)
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
# Chained expression: (Model A \ (Modifier B + 0.5mm offset))
|
||||||
|
modifier_expanded = sdf_b.offset(delta_mm=0.5)
|
||||||
|
sdf_carved = sdf_a.difference(modifier_expanded)
|
||||||
|
t_csg = (time.perf_counter() - t0) * 1000.0
|
||||||
|
|
||||||
|
print(f"\n[3] Chained GPU CSG (Carve / Subtraction) ({t_csg:.2f} ms):")
|
||||||
|
print(f" - Result Stack: {sdf_carved}")
|
||||||
|
print(f" - Automatic spatial bounding grid expansion: {sdf_carved.shape}")
|
||||||
|
|
||||||
|
# 4. Procedural 3D Gyroid Infill Generation on GPU
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
sdf_gyroid = sdf_carved.generate_infill(
|
||||||
|
pattern="gyroid",
|
||||||
|
cell_size_mm=4.0,
|
||||||
|
density=0.25,
|
||||||
|
wall_offset_mm=1.20
|
||||||
|
)
|
||||||
|
t_infill = (time.perf_counter() - t0) * 1000.0
|
||||||
|
|
||||||
|
print(f"\n[4] GPU Procedural Gyroid Infill Generation ({t_infill:.2f} ms):")
|
||||||
|
print(f" - Infill Stack: {sdf_gyroid}")
|
||||||
|
|
||||||
|
# 5. Vectorize directly to polygon perimeters and infill contours
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
layer_results = sdf_gyroid.vectorize(
|
||||||
|
perimeters_count=2,
|
||||||
|
layer_width=0.45,
|
||||||
|
export_infill_mask=True
|
||||||
|
)
|
||||||
|
t_vec = (time.perf_counter() - t0) * 1000.0
|
||||||
|
|
||||||
|
total_contours = sum(
|
||||||
|
sum(len(p["contours"]) for p in l.perimeters) + len(l.infill_contours)
|
||||||
|
for l in layer_results
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\n[5] Vectorization to Slicer Contours ({t_vec:.2f} ms):")
|
||||||
|
print(f" - Total Layers Output: {len(layer_results)}")
|
||||||
|
print(f" - Total Vector Contours Extracted: {total_contours}")
|
||||||
|
|
||||||
|
# Sample mid layer
|
||||||
|
mid_idx = len(layer_results) // 2
|
||||||
|
sample_layer = layer_results[mid_idx]
|
||||||
|
print(f"\n[6] Sample Mid Layer (Layer {mid_idx}):")
|
||||||
|
print(f" - Z Height: {sample_layer.z:.3f} mm ({sample_layer.z_um} µm)")
|
||||||
|
print(f" - Perimeters: {len(sample_layer.perimeters)}")
|
||||||
|
for i, p in enumerate(sample_layer.perimeters):
|
||||||
|
print(f" * Perimeter {i+1} (offset {p['offset_mm']:.2f} mm): {len(p['contours'])} contour loop(s)")
|
||||||
|
print(f" - Infill Contours: {len(sample_layer.infill_contours)} contour loop(s)")
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(" Pipeline completed successfully with 100% GPU-resident computation!")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -105,6 +105,8 @@ typedef struct {
|
||||||
int use_gpu_rasterizer; // 1 = GPU-accelerated rasterization (when CUDA enabled), 0 = CPU multi-threaded
|
int use_gpu_rasterizer; // 1 = GPU-accelerated rasterization (when CUDA enabled), 0 = CPU multi-threaded
|
||||||
float first_layer_height; // First layer height in mm (default: 0.0f -> uses layer_height)
|
float first_layer_height; // First layer height in mm (default: 0.0f -> uses layer_height)
|
||||||
int export_infill_mask; // 1 = export binary 2D infill mask into GpuLayerResult (default: 1)
|
int export_infill_mask; // 1 = export binary 2D infill mask into GpuLayerResult (default: 1)
|
||||||
|
float simplify_tolerance_mm; // RDP simplification tolerance in mm (default: 0.02 mm, 0 to disable)
|
||||||
|
int subpixel_lerp; // 1 = Continuous SDF sub-pixel LERP vectorization (default: 1), 0 = discrete bitpack
|
||||||
int backend; // GpuOffsetBackend: 0 = AUTO, 1 = CUDA, 2 = CPU (default: 0)
|
int backend; // GpuOffsetBackend: 0 = AUTO, 1 = CUDA, 2 = CPU (default: 0)
|
||||||
} GpuOffsetConfig;
|
} GpuOffsetConfig;
|
||||||
|
|
||||||
|
|
|
||||||
209
include/gpu_sdf.h
Normal file
209
include/gpu_sdf.h
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
#ifndef GPU_SDF_H
|
||||||
|
#define GPU_SDF_H
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#ifdef GPU_OFFSET_EXPORTS
|
||||||
|
#define GPU_SDF_API __declspec(dllexport)
|
||||||
|
#else
|
||||||
|
#define GPU_SDF_API __declspec(dllimport)
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
#define GPU_SDF_API __attribute__((visibility("default")))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "gpu_offset.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief CSG Operation Types for GPU SDF combinations
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
GPU_SDF_OP_UNION = 0, // A U B: max(d_A, d_B)
|
||||||
|
GPU_SDF_OP_INTERSECTION = 1, // A ^ B: min(d_A, d_B)
|
||||||
|
GPU_SDF_OP_DIFFERENCE = 2, // A \ B: min(d_A, -d_B)
|
||||||
|
GPU_SDF_OP_SMOOTH_UNION = 3, // Smooth union with blend radius k
|
||||||
|
GPU_SDF_OP_SMOOTH_INTERSECT= 4, // Smooth intersection with blend radius k
|
||||||
|
GPU_SDF_OP_SMOOTH_DIFF = 5 // Smooth difference with blend radius k
|
||||||
|
} GpuSdfOp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Layer matching policy when operating on two stacks
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
GPU_SDF_LAYER_MATCH_A_DOMINANT = 0, // Keep layers matching stack A (resample B at A's Z)
|
||||||
|
GPU_SDF_LAYER_MATCH_UNION = 1, // Output union of all unique layer Z heights
|
||||||
|
GPU_SDF_LAYER_MATCH_INTERSECTION = 2 // Output only exact matching layer Z heights
|
||||||
|
} GpuSdfLayerMatchMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Procedural 3D Infill Pattern Types
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
GPU_SDF_INFILL_SOLID = 0,
|
||||||
|
GPU_SDF_INFILL_GRID = 1,
|
||||||
|
GPU_SDF_INFILL_GYROID = 2,
|
||||||
|
GPU_SDF_INFILL_SCHWARZ_P = 3,
|
||||||
|
GPU_SDF_INFILL_SCHWARZ_D = 4
|
||||||
|
} GpuSdfInfillPattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Spatial metadata for a 2D/3D GPU Signed Distance Field stack
|
||||||
|
* All spatial coordinates and pitches are stored in integer micrometers (µm)
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
int32_t min_x_um; // Minimum X bound in micrometers (1 µm = 0.001 mm)
|
||||||
|
int32_t min_y_um; // Minimum Y bound in micrometers
|
||||||
|
int32_t pitch_um; // Pixel raster pitch in micrometers (e.g. 100 µm = 0.1 mm)
|
||||||
|
int width; // Grid columns (X)
|
||||||
|
int height; // Grid rows (Y)
|
||||||
|
size_t num_layers; // Total number of layers (Z)
|
||||||
|
} GpuSdfMetadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Opaque handle representing a GPU-resident Signed Distance Field stack.
|
||||||
|
* Distance values are in millimeters (float):
|
||||||
|
* d > 0 : Inside the object (positive Euclidean distance to boundary)
|
||||||
|
* d < 0 : Outside the object (negative Euclidean distance to boundary)
|
||||||
|
* d = 0 : Exact object boundary
|
||||||
|
*/
|
||||||
|
typedef struct GpuSdfStack_T* GpuSdfStackHandle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Create an empty GPU SDF stack with given metadata and layer Z heights (in µm).
|
||||||
|
*/
|
||||||
|
GPU_SDF_API GpuSdfStackHandle gpu_sdf_create(
|
||||||
|
const GpuSdfMetadata* meta,
|
||||||
|
const int32_t* layer_z_heights_um
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Create a GPU SDF stack from 3D mesh triangles directly on GPU.
|
||||||
|
*/
|
||||||
|
GPU_SDF_API GpuSdfStackHandle gpu_sdf_from_mesh(
|
||||||
|
const GpuTriangle3D* triangles,
|
||||||
|
size_t num_triangles,
|
||||||
|
float min_x, float min_y, float min_z,
|
||||||
|
float max_x, float max_y, float max_z,
|
||||||
|
float layer_height_mm,
|
||||||
|
float first_layer_height_mm,
|
||||||
|
float pitch_mm,
|
||||||
|
float pad_mm
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Create a GPU SDF stack from raw host binary occupancy masks (0 = background, >0 = object).
|
||||||
|
*/
|
||||||
|
GPU_SDF_API GpuSdfStackHandle gpu_sdf_from_masks(
|
||||||
|
const uint8_t* h_masks,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
size_t num_layers,
|
||||||
|
const int32_t* layer_z_heights_um,
|
||||||
|
int32_t min_x_um,
|
||||||
|
int32_t min_y_um,
|
||||||
|
int32_t pitch_um
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Clone an existing GPU SDF stack (allocates new GPU memory).
|
||||||
|
*/
|
||||||
|
GPU_SDF_API GpuSdfStackHandle gpu_sdf_clone(GpuSdfStackHandle stack);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Free GPU memory allocated for a GPU SDF stack.
|
||||||
|
*/
|
||||||
|
GPU_SDF_API void gpu_sdf_destroy(GpuSdfStackHandle stack);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Query metadata of a GPU SDF stack.
|
||||||
|
*/
|
||||||
|
GPU_SDF_API int gpu_sdf_get_metadata(GpuSdfStackHandle stack, GpuSdfMetadata* out_meta);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Retrieve layer Z heights in micrometers (copies to host buffer of size num_layers).
|
||||||
|
*/
|
||||||
|
GPU_SDF_API int gpu_sdf_get_layer_z_um(GpuSdfStackHandle stack, int32_t* out_z_um);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Retrieve raw SDF distance data (float mm) copied from GPU to Host.
|
||||||
|
* Buffer size must be: width * height * num_layers * sizeof(float).
|
||||||
|
*/
|
||||||
|
GPU_SDF_API int gpu_sdf_to_host(GpuSdfStackHandle stack, float* h_out_data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get raw GPU device pointer for zero-copy interop (CuPy / PyTorch).
|
||||||
|
*/
|
||||||
|
GPU_SDF_API float* gpu_sdf_get_device_ptr(GpuSdfStackHandle stack);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get raw GPU device pointer for layer Z heights (int32_t* on device).
|
||||||
|
*/
|
||||||
|
GPU_SDF_API int32_t* gpu_sdf_get_device_z_ptr(GpuSdfStackHandle stack);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Execute a CSG operation between two GPU SDF stacks without host roundtrips.
|
||||||
|
* Handles spatial bounding box differences and layer Z matching automatically.
|
||||||
|
*
|
||||||
|
* @param a First operand stack (GPU)
|
||||||
|
* @param b Second operand stack (GPU)
|
||||||
|
* @param op CSG operation (UNION, INTERSECTION, DIFFERENCE, SMOOTH_...)
|
||||||
|
* @param match_mode Layer Z matching mode
|
||||||
|
* @param blend_radius_mm Blend radius parameter for smooth operations
|
||||||
|
* @return GpuSdfStackHandle Resulting stack on GPU (caller must free with gpu_sdf_destroy)
|
||||||
|
*/
|
||||||
|
GPU_SDF_API GpuSdfStackHandle gpu_sdf_csg_op(
|
||||||
|
GpuSdfStackHandle a,
|
||||||
|
GpuSdfStackHandle b,
|
||||||
|
GpuSdfOp op,
|
||||||
|
GpuSdfLayerMatchMode match_mode,
|
||||||
|
float blend_radius_mm
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Apply a constant distance offset (dilation/expansion if delta > 0, inset/erosion if delta < 0).
|
||||||
|
*/
|
||||||
|
GPU_SDF_API GpuSdfStackHandle gpu_sdf_offset(
|
||||||
|
GpuSdfStackHandle stack,
|
||||||
|
float delta_mm
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Apply hollowing / shelling with a given wall thickness.
|
||||||
|
*/
|
||||||
|
GPU_SDF_API GpuSdfStackHandle gpu_sdf_shell(
|
||||||
|
GpuSdfStackHandle stack,
|
||||||
|
float thickness_mm
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Generate a procedural 3D infill pattern intersected with the interior region.
|
||||||
|
*/
|
||||||
|
GPU_SDF_API GpuSdfStackHandle gpu_sdf_generate_infill(
|
||||||
|
GpuSdfStackHandle stack,
|
||||||
|
GpuSdfInfillPattern pattern,
|
||||||
|
float cell_size_mm,
|
||||||
|
float density_ratio,
|
||||||
|
float wall_offset_mm
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Vectorize GPU SDF stack directly to polygon perimeter contours and infill mask.
|
||||||
|
*/
|
||||||
|
GPU_SDF_API int gpu_sdf_vectorize(
|
||||||
|
GpuSdfStackHandle stack,
|
||||||
|
const GpuOffsetConfig* config,
|
||||||
|
GpuLayerResult** out_results,
|
||||||
|
size_t* out_num_layers,
|
||||||
|
GpuOffsetStats* stats
|
||||||
|
);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // GPU_SDF_H
|
||||||
|
|
@ -347,11 +347,225 @@ def slice_mesh_and_offset(
|
||||||
# Alias for convenience
|
# Alias for convenience
|
||||||
slice_mesh = slice_mesh_and_offset
|
slice_mesh = slice_mesh_and_offset
|
||||||
|
|
||||||
|
|
||||||
|
class SdfStack:
|
||||||
|
"""
|
||||||
|
GPU-resident Signed Distance Field (SDF) stack representing a 3D model or modifier volume.
|
||||||
|
Enables chained GPU-accelerated Boolean CSG operations (union, intersection, difference,
|
||||||
|
smooth blend, offset, shelling, procedural 3D infill) without host-to-device data transfers.
|
||||||
|
"""
|
||||||
|
def __init__(self, backend_stack: Any):
|
||||||
|
self._stack = backend_stack
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metadata(self) -> Dict[str, Any]:
|
||||||
|
"""Dictionary containing spatial bounds, grid dimensions, and layer count."""
|
||||||
|
return self._stack.get_metadata()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def layer_z_um(self) -> np.ndarray:
|
||||||
|
"""1D array of layer heights in micrometers (µm)."""
|
||||||
|
return self._stack.get_layer_z_um()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def layer_z_mm(self) -> np.ndarray:
|
||||||
|
"""1D array of layer heights in millimeters (mm)."""
|
||||||
|
return self._stack.get_layer_z_mm()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shape(self) -> Tuple[int, int, int]:
|
||||||
|
"""Grid shape (num_layers, height, width)."""
|
||||||
|
m = self.metadata
|
||||||
|
return (m["num_layers"], m["height"], m["width"])
|
||||||
|
|
||||||
|
def numpy(self) -> np.ndarray:
|
||||||
|
"""Download raw 3D distance field array (num_layers, H, W) to CPU NumPy float32."""
|
||||||
|
return self._stack.to_numpy()
|
||||||
|
|
||||||
|
def clone(self) -> "SdfStack":
|
||||||
|
"""Duplicate this SDF stack in GPU VRAM."""
|
||||||
|
return SdfStack(self._stack.clone())
|
||||||
|
|
||||||
|
def union(self, other: "SdfStack", blend_radius_mm: float = 0.0) -> "SdfStack":
|
||||||
|
"""Compute Boolean Union (A ∪ B) on GPU. Optional blend_radius_mm for smooth fillet union."""
|
||||||
|
if not isinstance(other, SdfStack):
|
||||||
|
raise TypeError(f"Operand must be an SdfStack, got {type(other)}")
|
||||||
|
return SdfStack(self._stack.op_union(other._stack, float(blend_radius_mm)))
|
||||||
|
|
||||||
|
def intersection(self, other: "SdfStack", blend_radius_mm: float = 0.0) -> "SdfStack":
|
||||||
|
"""Compute Boolean Intersection (A ∩ B) on GPU. Optional blend_radius_mm for smooth intersection."""
|
||||||
|
if not isinstance(other, SdfStack):
|
||||||
|
raise TypeError(f"Operand must be an SdfStack, got {type(other)}")
|
||||||
|
return SdfStack(self._stack.op_intersection(other._stack, float(blend_radius_mm)))
|
||||||
|
|
||||||
|
def difference(self, other: "SdfStack", blend_radius_mm: float = 0.0) -> "SdfStack":
|
||||||
|
"""Compute Boolean Difference / Subtraction (A \\ B) on GPU. Optional blend_radius_mm for smooth difference."""
|
||||||
|
if not isinstance(other, SdfStack):
|
||||||
|
raise TypeError(f"Operand must be an SdfStack, got {type(other)}")
|
||||||
|
return SdfStack(self._stack.op_difference(other._stack, float(blend_radius_mm)))
|
||||||
|
|
||||||
|
def offset(self, delta_mm: float) -> "SdfStack":
|
||||||
|
"""Constant-distance offset/dilation (delta > 0) or inset/erosion (delta < 0) on GPU."""
|
||||||
|
return SdfStack(self._stack.offset(float(delta_mm)))
|
||||||
|
|
||||||
|
def shell(self, thickness_mm: float) -> "SdfStack":
|
||||||
|
"""Hollow / shell the volume with specified wall thickness on GPU."""
|
||||||
|
return SdfStack(self._stack.shell(float(thickness_mm)))
|
||||||
|
|
||||||
|
def generate_infill(
|
||||||
|
self,
|
||||||
|
pattern: str = "gyroid",
|
||||||
|
cell_size_mm: float = 5.0,
|
||||||
|
density: float = 0.20,
|
||||||
|
wall_offset_mm: float = 1.0
|
||||||
|
) -> "SdfStack":
|
||||||
|
"""Generate continuous 3D procedural lattice (gyroid, schwarz_p, schwarz_d, grid) on GPU."""
|
||||||
|
return SdfStack(self._stack.generate_infill(str(pattern), float(cell_size_mm), float(density), float(wall_offset_mm)))
|
||||||
|
|
||||||
|
def vectorize(
|
||||||
|
self,
|
||||||
|
perimeters_count: int = 2,
|
||||||
|
layer_width: float = 0.45,
|
||||||
|
export_infill_mask: bool = True,
|
||||||
|
simplify_tolerance_mm: float = 0.02,
|
||||||
|
subpixel_lerp: bool = True,
|
||||||
|
offset_distances: Optional[Sequence[float]] = None
|
||||||
|
) -> List[LayerResult]:
|
||||||
|
"""
|
||||||
|
Vectorize GPU SDF stack directly to smooth polygon perimeter contours and infill masks.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
perimeters_count : int, default=2
|
||||||
|
Number of perimeter passes.
|
||||||
|
layer_width : float, default=0.45
|
||||||
|
Extrusion line width in mm.
|
||||||
|
export_infill_mask : bool, default=True
|
||||||
|
Whether to export binary 2D infill mask.
|
||||||
|
simplify_tolerance_mm : float, default=0.02
|
||||||
|
Ramer-Douglas-Peucker (RDP) polygon simplification tolerance in mm (eliminates micro-steps).
|
||||||
|
subpixel_lerp : bool, default=True
|
||||||
|
Continuous SDF linear interpolation for sub-pixel boundary placement at arbitrary angles.
|
||||||
|
offset_distances : sequence of float, optional
|
||||||
|
Custom offset distances.
|
||||||
|
"""
|
||||||
|
offsets_float = [float(d) for d in offset_distances] if offset_distances is not None else []
|
||||||
|
raw_res = self._stack.vectorize(
|
||||||
|
int(perimeters_count),
|
||||||
|
float(layer_width),
|
||||||
|
bool(export_infill_mask),
|
||||||
|
float(simplify_tolerance_mm),
|
||||||
|
bool(subpixel_lerp),
|
||||||
|
offsets_float
|
||||||
|
)
|
||||||
|
return [LayerResult(r) for r in raw_res]
|
||||||
|
|
||||||
|
# Operator Overloads for fluent Python syntax
|
||||||
|
def __or__(self, other: "SdfStack") -> "SdfStack":
|
||||||
|
return self.union(other)
|
||||||
|
|
||||||
|
def __and__(self, other: "SdfStack") -> "SdfStack":
|
||||||
|
return self.intersection(other)
|
||||||
|
|
||||||
|
def __sub__(self, other: "SdfStack") -> "SdfStack":
|
||||||
|
return self.difference(other)
|
||||||
|
|
||||||
|
def __add__(self, delta: float) -> "SdfStack":
|
||||||
|
if isinstance(delta, (int, float)):
|
||||||
|
return self.offset(float(delta))
|
||||||
|
raise TypeError(f"Unsupported operand type for +: 'SdfStack' and '{type(delta)}'")
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
m = self.metadata
|
||||||
|
return f"<SdfStack layers={m['num_layers']} shape=({m['height']}, {m['width']}) pitch={m['pitch_mm']:.2f}mm>"
|
||||||
|
|
||||||
|
|
||||||
|
def slice_mesh_to_sdf(
|
||||||
|
mesh_or_triangles: Any,
|
||||||
|
layer_height: float = 0.20,
|
||||||
|
first_layer_height: Optional[float] = None,
|
||||||
|
pitch_mm: float = 0.10,
|
||||||
|
pad_mm: float = 1.5
|
||||||
|
) -> SdfStack:
|
||||||
|
"""
|
||||||
|
Slice a 3D mesh directly to a GPU-resident Signed Distance Field (SDF) stack.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
mesh_or_triangles : trimesh.Trimesh OR np.ndarray
|
||||||
|
Mesh or triangle array of shape (N, 3, 3) or (N, 9).
|
||||||
|
layer_height : float, default=0.20
|
||||||
|
Z slicing layer height in millimeters.
|
||||||
|
first_layer_height : float, optional
|
||||||
|
First layer height in millimeters. Defaults to `layer_height` if None.
|
||||||
|
pitch_mm : float, default=0.10
|
||||||
|
Pixel raster pitch in millimeters.
|
||||||
|
pad_mm : float, default=1.5
|
||||||
|
Bounding box padding in millimeters.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
SdfStack
|
||||||
|
GPU-resident SDF stack object ready for CSG operations or vectorization.
|
||||||
|
"""
|
||||||
|
if not _HAS_BACKEND or _backend is None:
|
||||||
|
raise RuntimeError("CUDASlicer backend extension (_cudaslicer_backend) is not available.")
|
||||||
|
|
||||||
|
if hasattr(mesh_or_triangles, "triangles"):
|
||||||
|
tris = np.ascontiguousarray(mesh_or_triangles.triangles, dtype=np.float32)
|
||||||
|
bounds = mesh_or_triangles.bounds
|
||||||
|
min_x, min_y, min_z = float(bounds[0][0]), float(bounds[0][1]), float(bounds[0][2])
|
||||||
|
max_x, max_y, max_z = float(bounds[1][0]), float(bounds[1][1]), float(bounds[1][2])
|
||||||
|
else:
|
||||||
|
tris = np.ascontiguousarray(mesh_or_triangles, dtype=np.float32)
|
||||||
|
if tris.ndim == 2 and tris.shape[1] == 9:
|
||||||
|
tris_3d = tris.reshape(-1, 3, 3)
|
||||||
|
else:
|
||||||
|
tris_3d = tris
|
||||||
|
min_x = float(tris_3d[:, :, 0].min())
|
||||||
|
max_x = float(tris_3d[:, :, 0].max())
|
||||||
|
min_y = float(tris_3d[:, :, 1].min())
|
||||||
|
max_y = float(tris_3d[:, :, 1].max())
|
||||||
|
min_z = float(tris_3d[:, :, 2].min())
|
||||||
|
max_z = float(tris_3d[:, :, 2].max())
|
||||||
|
|
||||||
|
h_first = float(first_layer_height) if first_layer_height is not None and first_layer_height > 0 else float(layer_height)
|
||||||
|
|
||||||
|
backend_sdf = _backend.slice_mesh_to_sdf(
|
||||||
|
tris, min_x, min_y, min_z, max_x, max_y, max_z,
|
||||||
|
float(layer_height), float(h_first), float(pitch_mm), float(pad_mm)
|
||||||
|
)
|
||||||
|
return SdfStack(backend_sdf)
|
||||||
|
|
||||||
|
|
||||||
|
def masks_to_sdf(
|
||||||
|
masks: np.ndarray,
|
||||||
|
layer_z_um: Optional[Sequence[int]] = None,
|
||||||
|
min_x_mm: float = 0.0,
|
||||||
|
min_y_mm: float = 0.0,
|
||||||
|
pitch_mm: float = 0.10
|
||||||
|
) -> SdfStack:
|
||||||
|
"""
|
||||||
|
Compute a GPU-resident Signed Distance Field from 2D (H, W) or 3D (N, H, W) binary masks.
|
||||||
|
"""
|
||||||
|
if not _HAS_BACKEND or _backend is None:
|
||||||
|
raise RuntimeError("CUDASlicer backend extension (_cudaslicer_backend) is not available.")
|
||||||
|
|
||||||
|
masks_u8 = np.ascontiguousarray(masks, dtype=np.uint8)
|
||||||
|
z_arr = np.ascontiguousarray(layer_z_um, dtype=np.int32) if layer_z_um is not None else np.array([], dtype=np.int32)
|
||||||
|
|
||||||
|
backend_sdf = _backend.masks_to_sdf(masks_u8, z_arr, float(min_x_mm), float(min_y_mm), float(pitch_mm))
|
||||||
|
return SdfStack(backend_sdf)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"offset_masks",
|
"offset_masks",
|
||||||
"offset_polygons",
|
"offset_polygons",
|
||||||
"slice_mesh_and_offset",
|
"slice_mesh_and_offset",
|
||||||
"slice_mesh",
|
"slice_mesh",
|
||||||
|
"slice_mesh_to_sdf",
|
||||||
|
"masks_to_sdf",
|
||||||
|
"SdfStack",
|
||||||
"LayerResult",
|
"LayerResult",
|
||||||
"is_available",
|
"is_available",
|
||||||
"has_cuda",
|
"has_cuda",
|
||||||
|
|
@ -361,3 +575,4 @@ __all__ = [
|
||||||
"get_cuda_device_count",
|
"get_cuda_device_count",
|
||||||
"__version__",
|
"__version__",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -362,8 +362,352 @@ py::list py_slice_mesh_and_offset(
|
||||||
return py_layer_results;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<PyGpuSdfStack> generate_infill(
|
||||||
|
const std::string& pattern_str = "gyroid",
|
||||||
|
float cell_size_mm = 5.0f,
|
||||||
|
float density_ratio = 0.20f,
|
||||||
|
float wall_offset_mm = 1.0f
|
||||||
|
) const {
|
||||||
|
GpuSdfInfillPattern pat = GPU_SDF_INFILL_GYROID;
|
||||||
|
if (pattern_str == "gyroid") pat = GPU_SDF_INFILL_GYROID;
|
||||||
|
else if (pattern_str == "schwarz_p" || pattern_str == "schwarz-p") pat = GPU_SDF_INFILL_SCHWARZ_P;
|
||||||
|
else if (pattern_str == "schwarz_d" || pattern_str == "schwarz-d") pat = GPU_SDF_INFILL_SCHWARZ_D;
|
||||||
|
else if (pattern_str == "grid") pat = GPU_SDF_INFILL_GRID;
|
||||||
|
else throw std::invalid_argument("Unknown infill pattern: " + pattern_str);
|
||||||
|
|
||||||
|
GpuSdfStackHandle res = gpu_sdf_generate_infill(handle_, pat, cell_size_mm, density_ratio, wall_offset_mm);
|
||||||
|
if (!res) throw std::runtime_error("SDF infill generation 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>()
|
||||||
|
) 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];
|
||||||
|
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];
|
||||||
|
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) {
|
PYBIND11_MODULE(_cudaslicer_backend, m) {
|
||||||
m.doc() = "GPU/CPU Slicing & SDF/JFA Offsetting C++/CUDA Python Extension";
|
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("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("is_cuda_available", &gpu_offset_has_cuda, "Returns 1 if a functional CUDA GPU is present, 0 otherwise.");
|
||||||
|
|
@ -417,4 +761,58 @@ PYBIND11_MODULE(_cudaslicer_backend, m) {
|
||||||
py::arg("offset_distances") = std::vector<float>(),
|
py::arg("offset_distances") = std::vector<float>(),
|
||||||
py::arg("backend") = "auto",
|
py::arg("backend") = "auto",
|
||||||
"Mesh slicing, perimeter offsetting, and infill mask generation.");
|
"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("generate_infill", &PyGpuSdfStack::generate_infill,
|
||||||
|
py::arg("pattern_str") = "gyroid",
|
||||||
|
py::arg("cell_size_mm") = 5.0f,
|
||||||
|
py::arg("density_ratio") = 0.20f,
|
||||||
|
py::arg("wall_offset_mm") = 1.0f,
|
||||||
|
"Intersect interior volume with 3D procedural infill lattice directly on GPU.")
|
||||||
|
.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>(),
|
||||||
|
"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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,39 @@ __global__ void gpu_parity_scan_to_mask_kernel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function for direct mesh slicing to device mask
|
||||||
|
extern "C" int gpu_slice_mesh_to_device_mask(
|
||||||
|
const GpuTriangle3D* triangles,
|
||||||
|
size_t num_triangles,
|
||||||
|
float min_z, float h_first, float layer_height, int num_layers,
|
||||||
|
float bounds_min_x, float bounds_min_y, float pitch,
|
||||||
|
int cols, int rows,
|
||||||
|
uint8_t* d_masks,
|
||||||
|
cudaStream_t stream
|
||||||
|
) {
|
||||||
|
int row_stride_words = (cols + 31) / 32;
|
||||||
|
size_t total_words = (size_t)num_layers * rows * row_stride_words;
|
||||||
|
|
||||||
|
unsigned int* d_parity = nullptr;
|
||||||
|
CUDA_CHECK(cudaMalloc(&d_parity, total_words * sizeof(unsigned int)));
|
||||||
|
CUDA_CHECK(cudaMemsetAsync(d_parity, 0, total_words * sizeof(unsigned int), stream));
|
||||||
|
|
||||||
|
int block_sz = 256;
|
||||||
|
int grid_sz = (num_triangles + block_sz - 1) / block_sz;
|
||||||
|
gpu_mesh_slice_raster_kernel<<<grid_sz, block_sz, 0, stream>>>(
|
||||||
|
triangles, num_triangles, min_z, h_first, layer_height, num_layers,
|
||||||
|
bounds_min_x, bounds_min_y, pitch, cols, rows, row_stride_words, d_parity
|
||||||
|
);
|
||||||
|
|
||||||
|
dim3 scan_grid(rows, num_layers);
|
||||||
|
gpu_parity_scan_to_mask_kernel<<<scan_grid, 1, 0, stream>>>(
|
||||||
|
d_parity, d_masks, cols, rows, num_layers, row_stride_words
|
||||||
|
);
|
||||||
|
|
||||||
|
cudaFree(d_parity);
|
||||||
|
return GPU_OFFSET_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Public C API Implementation & Backend Management
|
// Public C API Implementation & Backend Management
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
1184
src/gpu_sdf.cu
Normal file
1184
src/gpu_sdf.cu
Normal file
File diff suppressed because it is too large
Load diff
269
src/vectorizer.h
269
src/vectorizer.h
|
|
@ -28,6 +28,275 @@ inline GpuPoint2D get_edge_point(int edge_id, int stride_w, float min_x, float m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sub-pixel linear interpolation on continuous distance field
|
||||||
|
inline GpuPoint2D get_sdf_edge_point(
|
||||||
|
int edge_id,
|
||||||
|
const float* sdf,
|
||||||
|
int width, int height,
|
||||||
|
float min_x, float min_y, float pitch_mm,
|
||||||
|
float iso_val
|
||||||
|
) {
|
||||||
|
int type = edge_id & 1;
|
||||||
|
int cell = edge_id >> 1;
|
||||||
|
int stride_w = width + 1;
|
||||||
|
int y = cell / stride_w;
|
||||||
|
int x = cell % stride_w;
|
||||||
|
|
||||||
|
if (type == 0) {
|
||||||
|
// Horizontal edge: from (x, y) to (x + 1, y)
|
||||||
|
float d0 = -1e5f;
|
||||||
|
float d1 = -1e5f;
|
||||||
|
if (y >= 0 && y < height) {
|
||||||
|
if (x >= 0 && x < width) d0 = sdf[y * width + x];
|
||||||
|
if (x + 1 >= 0 && x + 1 < width) d1 = sdf[y * width + (x + 1)];
|
||||||
|
}
|
||||||
|
float t = 0.5f;
|
||||||
|
float denom = d1 - d0;
|
||||||
|
if (std::abs(denom) > 1e-7f) {
|
||||||
|
t = (iso_val - d0) / denom;
|
||||||
|
if (t < 0.0f) t = 0.0f;
|
||||||
|
else if (t > 1.0f) t = 1.0f;
|
||||||
|
}
|
||||||
|
return GpuPoint2D{ min_x + ((float)x + t) * pitch_mm, min_y + (float)y * pitch_mm };
|
||||||
|
} else {
|
||||||
|
// Vertical edge: from (x, y) to (x, y + 1)
|
||||||
|
float d0 = -1e5f;
|
||||||
|
float d3 = -1e5f;
|
||||||
|
if (x >= 0 && x < width) {
|
||||||
|
if (y >= 0 && y < height) d0 = sdf[y * width + x];
|
||||||
|
if (y + 1 >= 0 && y + 1 < height) d3 = sdf[(y + 1) * width + x];
|
||||||
|
}
|
||||||
|
float t = 0.5f;
|
||||||
|
float denom = d3 - d0;
|
||||||
|
if (std::abs(denom) > 1e-7f) {
|
||||||
|
t = (iso_val - d0) / denom;
|
||||||
|
if (t < 0.0f) t = 0.0f;
|
||||||
|
else if (t > 1.0f) t = 1.0f;
|
||||||
|
}
|
||||||
|
return GpuPoint2D{ min_x + (float)x * pitch_mm, min_y + ((float)y + t) * pitch_mm };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ramer-Douglas-Peucker (RDP) Polyline Simplification
|
||||||
|
static inline void rdp_simplify_recursive(
|
||||||
|
const std::vector<GpuPoint2D>& pts,
|
||||||
|
size_t i, size_t j,
|
||||||
|
float eps_sq,
|
||||||
|
std::vector<bool>& keep
|
||||||
|
) {
|
||||||
|
if (j <= i + 1) return;
|
||||||
|
|
||||||
|
float p1x = pts[i].x, p1y = pts[i].y;
|
||||||
|
float p2x = pts[j].x, p2y = pts[j].y;
|
||||||
|
float dx = p2x - p1x;
|
||||||
|
float dy = p2y - p1y;
|
||||||
|
float len_sq = dx * dx + dy * dy;
|
||||||
|
|
||||||
|
float max_dist_sq = 0.0f;
|
||||||
|
size_t max_idx = i;
|
||||||
|
|
||||||
|
for (size_t k = i + 1; k < j; ++k) {
|
||||||
|
float px = pts[k].x, py = pts[k].y;
|
||||||
|
float dist_sq = 0.0f;
|
||||||
|
|
||||||
|
if (len_sq < 1e-10f) {
|
||||||
|
float dpx = px - p1x;
|
||||||
|
float dpy = py - p1y;
|
||||||
|
dist_sq = dpx * dpx + dpy * dpy;
|
||||||
|
} else {
|
||||||
|
float cross = dy * (px - p1x) - dx * (py - p1y);
|
||||||
|
dist_sq = (cross * cross) / len_sq;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dist_sq > max_dist_sq) {
|
||||||
|
max_dist_sq = dist_sq;
|
||||||
|
max_idx = k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max_dist_sq > eps_sq) {
|
||||||
|
keep[max_idx] = true;
|
||||||
|
rdp_simplify_recursive(pts, i, max_idx, eps_sq, keep);
|
||||||
|
rdp_simplify_recursive(pts, max_idx, j, eps_sq, keep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline std::vector<GpuPoint2D> simplify_contour_rdp(
|
||||||
|
const std::vector<GpuPoint2D>& loop_pts,
|
||||||
|
float tolerance_mm
|
||||||
|
) {
|
||||||
|
size_t n = loop_pts.size();
|
||||||
|
if (n < 4 || tolerance_mm <= 1e-6f) return loop_pts;
|
||||||
|
|
||||||
|
float eps_sq = tolerance_mm * tolerance_mm;
|
||||||
|
|
||||||
|
// For closed loop: find point furthest from loop_pts[0]
|
||||||
|
float max_d_sq = 0.0f;
|
||||||
|
size_t split_idx = n / 2;
|
||||||
|
for (size_t i = 1; i < n; ++i) {
|
||||||
|
float dx = loop_pts[i].x - loop_pts[0].x;
|
||||||
|
float dy = loop_pts[i].y - loop_pts[0].y;
|
||||||
|
float d_sq = dx * dx + dy * dy;
|
||||||
|
if (d_sq > max_d_sq) {
|
||||||
|
max_d_sq = d_sq;
|
||||||
|
split_idx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<bool> keep(n, false);
|
||||||
|
keep[0] = true;
|
||||||
|
keep[split_idx] = true;
|
||||||
|
|
||||||
|
rdp_simplify_recursive(loop_pts, 0, split_idx, eps_sq, keep);
|
||||||
|
rdp_simplify_recursive(loop_pts, split_idx, n - 1, eps_sq, keep);
|
||||||
|
|
||||||
|
std::vector<GpuPoint2D> simplified;
|
||||||
|
simplified.reserve(n);
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
if (keep[i]) simplified.push_back(loop_pts[i]);
|
||||||
|
}
|
||||||
|
return simplified;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void vectorize_sdf_layer(
|
||||||
|
const float* sdf,
|
||||||
|
int width, int height,
|
||||||
|
float min_x, float min_y, float pitch_mm,
|
||||||
|
float iso_val,
|
||||||
|
float simplify_tolerance_mm,
|
||||||
|
std::vector<int>& next_edge_buf,
|
||||||
|
std::vector<int>& active_edges_buf,
|
||||||
|
std::vector<uint8_t>& visited_buf,
|
||||||
|
std::vector<GpuContour>& out_contours
|
||||||
|
) {
|
||||||
|
int stride_w = width + 1;
|
||||||
|
active_edges_buf.clear();
|
||||||
|
|
||||||
|
auto add_directed_edge = [&](int e0, int e1) {
|
||||||
|
next_edge_buf[e0] = e1;
|
||||||
|
visited_buf[e0] = 0;
|
||||||
|
active_edges_buf.push_back(e0);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int y = 0; y < height - 1; ++y) {
|
||||||
|
const float* row0 = sdf + y * width;
|
||||||
|
const float* row1 = sdf + (y + 1) * width;
|
||||||
|
|
||||||
|
for (int x = 0; x < width - 1; ++x) {
|
||||||
|
float c0 = row0[x];
|
||||||
|
float c1 = row0[x + 1];
|
||||||
|
float c2 = row1[x + 1];
|
||||||
|
float c3 = row1[x];
|
||||||
|
|
||||||
|
int b0 = (c0 >= iso_val) ? 1 : 0;
|
||||||
|
int b1 = (c1 >= iso_val) ? 1 : 0;
|
||||||
|
int b2 = (c2 >= iso_val) ? 1 : 0;
|
||||||
|
int b3 = (c3 >= iso_val) ? 1 : 0;
|
||||||
|
|
||||||
|
int case_idx = b0 | (b1 << 1) | (b2 << 2) | (b3 << 3);
|
||||||
|
if (case_idx == 0 || case_idx == 15) continue;
|
||||||
|
|
||||||
|
int e_bottom = get_h_edge_id(x, y, stride_w);
|
||||||
|
int e_top = get_h_edge_id(x, y + 1, stride_w);
|
||||||
|
int e_left = get_v_edge_id(x, y, stride_w);
|
||||||
|
int e_right = get_v_edge_id(x + 1, y, stride_w);
|
||||||
|
|
||||||
|
switch (case_idx) {
|
||||||
|
case 1: add_directed_edge(e_left, e_bottom); break;
|
||||||
|
case 2: add_directed_edge(e_bottom, e_right); break;
|
||||||
|
case 3: add_directed_edge(e_left, e_right); break;
|
||||||
|
case 4: add_directed_edge(e_right, e_top); break;
|
||||||
|
case 5: {
|
||||||
|
float center = (c0 + c1 + c2 + c3) * 0.25f;
|
||||||
|
if (center >= iso_val) {
|
||||||
|
add_directed_edge(e_left, e_top);
|
||||||
|
add_directed_edge(e_bottom, e_right);
|
||||||
|
} else {
|
||||||
|
add_directed_edge(e_left, e_bottom);
|
||||||
|
add_directed_edge(e_right, e_top);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 6: add_directed_edge(e_bottom, e_top); break;
|
||||||
|
case 7: add_directed_edge(e_left, e_top); break;
|
||||||
|
case 8: add_directed_edge(e_top, e_left); break;
|
||||||
|
case 9: add_directed_edge(e_top, e_bottom); break;
|
||||||
|
case 10: {
|
||||||
|
float center = (c0 + c1 + c2 + c3) * 0.25f;
|
||||||
|
if (center >= iso_val) {
|
||||||
|
add_directed_edge(e_bottom, e_left);
|
||||||
|
add_directed_edge(e_top, e_right);
|
||||||
|
} else {
|
||||||
|
add_directed_edge(e_top, e_left);
|
||||||
|
add_directed_edge(e_bottom, e_right);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 11: add_directed_edge(e_top, e_right); break;
|
||||||
|
case 12: add_directed_edge(e_right, e_left); break;
|
||||||
|
case 13: add_directed_edge(e_right, e_bottom); break;
|
||||||
|
case 14: add_directed_edge(e_bottom, e_left); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active_edges_buf.empty()) return;
|
||||||
|
|
||||||
|
std::vector<GpuPoint2D> loop_pts;
|
||||||
|
loop_pts.reserve(1000);
|
||||||
|
|
||||||
|
for (int start_e : active_edges_buf) {
|
||||||
|
if (visited_buf[start_e]) continue;
|
||||||
|
|
||||||
|
loop_pts.clear();
|
||||||
|
int cur = start_e;
|
||||||
|
|
||||||
|
while (cur != -1 && !visited_buf[cur]) {
|
||||||
|
visited_buf[cur] = 1;
|
||||||
|
GpuPoint2D pt = get_sdf_edge_point(cur, sdf, width, height, min_x, min_y, pitch_mm, iso_val);
|
||||||
|
|
||||||
|
size_t n = loop_pts.size();
|
||||||
|
if (n >= 2) {
|
||||||
|
float dx1 = loop_pts[n-1].x - loop_pts[n-2].x;
|
||||||
|
float dy1 = loop_pts[n-1].y - loop_pts[n-2].y;
|
||||||
|
float dx2 = pt.x - loop_pts[n-1].x;
|
||||||
|
float dy2 = pt.y - loop_pts[n-1].y;
|
||||||
|
|
||||||
|
float cross = dx1 * dy2 - dy1 * dx2;
|
||||||
|
if (std::abs(cross) < 1e-7f) {
|
||||||
|
loop_pts[n-1] = pt; // simplify collinear vertex
|
||||||
|
} else {
|
||||||
|
loop_pts.push_back(pt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loop_pts.push_back(pt);
|
||||||
|
}
|
||||||
|
|
||||||
|
cur = next_edge_buf[cur];
|
||||||
|
if (cur == start_e) break;
|
||||||
|
if (loop_pts.size() > 50000) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loop_pts.size() >= 3) {
|
||||||
|
std::vector<GpuPoint2D> simplified = simplify_contour_rdp(loop_pts, simplify_tolerance_mm);
|
||||||
|
if (simplified.size() >= 3) {
|
||||||
|
GpuContour c;
|
||||||
|
c.num_points = simplified.size();
|
||||||
|
c.points = (GpuPoint2D*)malloc(c.num_points * sizeof(GpuPoint2D));
|
||||||
|
memcpy(c.points, simplified.data(), c.num_points * sizeof(GpuPoint2D));
|
||||||
|
c.is_hole = 0;
|
||||||
|
out_contours.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset thread-local edge table for zero re-allocation overhead
|
||||||
|
for (int e : active_edges_buf) {
|
||||||
|
next_edge_buf[e] = -1;
|
||||||
|
visited_buf[e] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static inline void vectorize_mask_layer(
|
static inline void vectorize_mask_layer(
|
||||||
const uint8_t* mask,
|
const uint8_t* mask,
|
||||||
int width, int height,
|
int width, int height,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue