up
This commit is contained in:
parent
767fd08060
commit
935191c147
9 changed files with 554 additions and 2 deletions
|
|
@ -59,6 +59,7 @@ wheels = [
|
|||
|
||||
"./wheels/python_libnest2d-0.1.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
|
||||
"./wheels/python_libnest2d-0.1.3-cp313-cp313-win_amd64.whl",
|
||||
"./wheels/cudaslicer-1.0.0-cp313-cp313-linux_x86_64.whl",
|
||||
"./wheels/shapely_polyskel-0.1.2-py3-none-any.whl",
|
||||
"./wheels/euclid3-0.1-py3-none-any.whl",
|
||||
"./wheels/pyransac3d-0.6.0-py3-none-any.whl",
|
||||
|
|
|
|||
4
build.sh
4
build.sh
|
|
@ -12,6 +12,10 @@ mapfile -t FILES < <(git ls-files -co --exclude-standard \
|
|||
| grep -v -xE '(\.gitignore|build\.sh)')
|
||||
|
||||
tar -cf - "${FILES[@]}" | tar -xf - -C "$TMPDIR"
|
||||
if [ -d wheels ]; then
|
||||
mkdir -p "$TMPDIR/wheels"
|
||||
cp -r wheels/* "$TMPDIR/wheels/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
cd "$TMPDIR"
|
||||
/home/nicolas/Applications/blender-5.1.0-linux-x64/blender \
|
||||
|
|
|
|||
21
core/cudaslicer_loader.py
Normal file
21
core/cudaslicer_loader.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
def import_cudaslicer() -> Any | None:
|
||||
# Check local development path first to prioritize live compiled binaries during development
|
||||
dev_paths = [
|
||||
Path("/home/nicolas/Workspace/Development/cudaslicer/python"),
|
||||
Path(__file__).resolve().parent.parent / "deps" / "cudaslicer" / "python",
|
||||
]
|
||||
for p in dev_paths:
|
||||
if p.exists() and str(p) not in sys.path:
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
try:
|
||||
import cudaslicer
|
||||
return cudaslicer
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
|
@ -4,6 +4,7 @@ import numpy as np
|
|||
from ..bpy_ns_types import NfEnumProperty, NfBoolProperty, notify_tree_changed
|
||||
from ....registry import register_class
|
||||
from .base import SliceNode, SlicePaths, SlicingObject, SlicingCollection, slice_at_range, SliceNodeJoiner, SlicingGroup
|
||||
from ...slice import align_path_seam_sharpest
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GetPaths(SliceNode):
|
||||
|
|
@ -168,6 +169,217 @@ class SLICENODE_SliceShells(SliceNode):
|
|||
|
||||
return slices
|
||||
|
||||
@register_class
|
||||
class SLICENODE_CudaSliceMesh(SliceNode):
|
||||
bl_label = "CUDA Slice Mesh"
|
||||
|
||||
backend: str = NfEnumProperty(
|
||||
name="Backend",
|
||||
description="Execution backend: AUTO uses CUDA GPU if available, CPU uses multi-threaded CPU fallback",
|
||||
items=[
|
||||
('AUTO', "Auto", "Automatically use CUDA GPU if available, fallback to multi-threaded CPU"),
|
||||
('CUDA', "CUDA", "Force CUDA GPU execution (requires NVIDIA GPU)"),
|
||||
('CPU', "CPU", "Multi-threaded CPU execution"),
|
||||
],
|
||||
default='AUTO',
|
||||
update=notify_tree_changed
|
||||
)
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Mesh", "Mesh", description="3D mesh geometry to slice")
|
||||
self.add_input("NodeSocketFloat", "Layer Height", description="Layer slicing height in millimeters (Z distance between consecutive layers)").value = 0.2
|
||||
self.add_input("NodeSocketFloat", "First Layer Height", description="Initial first layer height in millimeters (defaults to Layer Height if 0 or unlinked)").value = 0.2
|
||||
self.add_input("NodeSocketInt", "Perimeters Count", description="Number of concentric outer perimeter wall passes to generate").value = 2
|
||||
self.add_input("NodeSocketFloat", "Layer Width", description="Extrusion line width in millimeters").value = 0.45
|
||||
self.add_input("NodeSocketFloat", "Pitch", description="GPU distance transform grid pixel resolution in millimeters (lower = higher accuracy)").value = 0.10
|
||||
self.add_input("NodeSocketFloat", "Pad", description="Bounding box margin padding in millimeters around the sliced model").value = 1.5
|
||||
self.add_input("NodeSocketRotation", "Rotation", description="Optional 3D Euler rotation angles to orient the mesh before slicing")
|
||||
self.add_output("SLICESOCK_Paths", "Perimeters", description="Generated perimeter wall toolpaths for each layer")
|
||||
self.add_output("SLICESOCK_Paths", "Infill Contours", description="Boundary contour loops defining the infill region")
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "backend", text="")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **kwargs) -> SlicePaths:
|
||||
from ...cudaslicer_loader import import_cudaslicer
|
||||
cudaslicer = import_cudaslicer()
|
||||
if not cudaslicer or not cudaslicer.is_available():
|
||||
print("Warning: cudaslicer backend is not available.")
|
||||
return SlicePaths()
|
||||
|
||||
cache = kwargs.get("cache")
|
||||
node_ptr = self.as_pointer() if hasattr(self, "as_pointer") else id(self)
|
||||
cache_key = (node_ptr, "cudaslicer_layers_eval")
|
||||
|
||||
if cache is not None and cache_key in cache:
|
||||
perim_sp, infill_sp = cache[cache_key]
|
||||
else:
|
||||
coll: SlicingCollection | SlicingGroup | None = self.resolve("Mesh")
|
||||
if not coll:
|
||||
return SlicePaths()
|
||||
|
||||
layer_height: float = float(self.resolve("Layer Height") or 0.2)
|
||||
fl_height: float | None = None
|
||||
if hasattr(self, "inputs") and "First Layer Height" in self.inputs:
|
||||
try:
|
||||
fl_height = self.resolve("First Layer Height")
|
||||
except Exception:
|
||||
fl_height = None
|
||||
else:
|
||||
try:
|
||||
fl_height = self.resolve("First Layer Height")
|
||||
except Exception:
|
||||
fl_height = None
|
||||
if fl_height is not None and fl_height <= 0:
|
||||
fl_height = None
|
||||
|
||||
perimeters_count: int = int(self.resolve("Perimeters Count") or 2)
|
||||
layer_width: float = float(self.resolve("Layer Width") or 0.45)
|
||||
pitch_mm: float = float(self.resolve("Pitch") or 0.10)
|
||||
pad_mm: float = float(self.resolve("Pad") or 1.5)
|
||||
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
|
||||
|
||||
backend_choice = getattr(self, "backend", "AUTO").upper()
|
||||
|
||||
if isinstance(coll, SlicingGroup):
|
||||
objects = []
|
||||
for c in coll.collections.values():
|
||||
objects.extend(c.objects)
|
||||
elif isinstance(coll, SlicingCollection):
|
||||
objects = coll.objects
|
||||
elif isinstance(coll, SlicingObject):
|
||||
objects = [coll]
|
||||
elif isinstance(coll, (list, tuple)):
|
||||
objects = list(coll)
|
||||
else:
|
||||
objects = []
|
||||
|
||||
merged_object = SlicingObject.merge(objects)
|
||||
if merged_object.mesh is None or merged_object.mesh.size == 0:
|
||||
return SlicePaths()
|
||||
|
||||
# Copy mesh for rotation
|
||||
mesh_data = merged_object.mesh.copy()
|
||||
mat_inv = None
|
||||
if rotation and any(rotation):
|
||||
try:
|
||||
from mathutils import Euler
|
||||
mat = np.array(Euler(rotation).to_matrix().inverted())
|
||||
mat_inv = np.array(Euler(rotation).to_matrix())
|
||||
except ImportError:
|
||||
from ...geometry import euler_rotation_matrix
|
||||
mat_inv = euler_rotation_matrix(float(rotation[0]), float(rotation[1]), float(rotation[2]))
|
||||
mat = mat_inv.T
|
||||
mesh_data[:, :3, :] = mesh_data[:, :3, :] @ mat.T
|
||||
mesh_data[:, 3:4, :] = mesh_data[:, 3:4, :] @ mat.T
|
||||
|
||||
tris_3d = mesh_data[:, :3, :].astype(np.float32)
|
||||
|
||||
perim_paths: list[np.ndarray] = []
|
||||
perim_groups: list[int] = []
|
||||
infill_paths: list[np.ndarray] = []
|
||||
infill_groups: list[int] = []
|
||||
first_layer_z = None
|
||||
|
||||
layers = cudaslicer.slice_mesh_and_offset(
|
||||
mesh_or_triangles=tris_3d,
|
||||
layer_height=float(layer_height),
|
||||
first_layer_height=float(fl_height) if fl_height is not None else float(layer_height),
|
||||
perimeters_count=int(perimeters_count),
|
||||
layer_width=float(layer_width),
|
||||
pitch_mm=float(pitch_mm),
|
||||
pad_mm=float(pad_mm),
|
||||
return_infill_mask=True,
|
||||
backend="cpu" if backend_choice == "CPU" else backend_choice.lower(),
|
||||
)
|
||||
if len(layers) > 0:
|
||||
first_layer_z = float(layers[0].z)
|
||||
|
||||
for l_idx, layer in enumerate(layers):
|
||||
z = float(layer.z)
|
||||
for perim in getattr(layer, "perimeters", []):
|
||||
for cnt in perim.get("contours", []):
|
||||
if len(cnt) < 2:
|
||||
continue
|
||||
cnt_3d = np.column_stack([cnt, np.full(len(cnt), z)])
|
||||
if not np.allclose(cnt_3d[0], cnt_3d[-1]):
|
||||
cnt_3d = np.vstack([cnt_3d, cnt_3d[0]])
|
||||
cnt_3d = align_path_seam_sharpest(cnt_3d)
|
||||
perim_paths.append(cnt_3d)
|
||||
perim_groups.append(l_idx)
|
||||
|
||||
for cnt in getattr(layer, "infill_contours", []):
|
||||
if len(cnt) < 2:
|
||||
continue
|
||||
cnt_3d = np.column_stack([cnt, np.full(len(cnt), z)])
|
||||
if not np.allclose(cnt_3d[0], cnt_3d[-1]):
|
||||
cnt_3d = np.vstack([cnt_3d, cnt_3d[0]])
|
||||
cnt_3d = align_path_seam_sharpest(cnt_3d)
|
||||
infill_paths.append(cnt_3d)
|
||||
infill_groups.append(l_idx)
|
||||
|
||||
names = []
|
||||
if objects:
|
||||
for o in objects:
|
||||
n = getattr(o, "name", "")
|
||||
if n:
|
||||
if n.endswith("_merged"):
|
||||
n = n[:-7]
|
||||
if n and n not in names:
|
||||
names.append(n)
|
||||
if not names and hasattr(coll, "name") and getattr(coll, "name"):
|
||||
names.append(str(getattr(coll, "name")))
|
||||
|
||||
# Build Perimeters SlicePaths
|
||||
perim_sp = SlicePaths(perim_paths, groups=perim_groups)
|
||||
perim_sp.layer_height = layer_height
|
||||
if fl_height is not None:
|
||||
perim_sp.first_layer_height = fl_height
|
||||
perim_sp.transforms = list(getattr(coll, "transforms", [])) or list(getattr(merged_object, "transforms", []))
|
||||
|
||||
if names:
|
||||
perim_sp.name = "-".join(names)
|
||||
perim_sp.objects_info = {"objects": [{"name": n, "polygon": []} for n in names]}
|
||||
|
||||
if first_layer_z is not None and fl_height is not None and layer_height > 0:
|
||||
mult = fl_height / layer_height
|
||||
for i, p in enumerate(perim_sp):
|
||||
if len(p) > 0 and abs(p[0, 2] - first_layer_z) < 1e-4:
|
||||
perim_sp.extrusion_rate[i] = perim_sp.extrusion_rate[i] * mult
|
||||
|
||||
if rotation and any(rotation) and mat_inv is not None:
|
||||
perim_sp = perim_sp.transform(mat_inv)
|
||||
|
||||
# Build Infill Contours SlicePaths
|
||||
infill_sp = SlicePaths(infill_paths, groups=infill_groups)
|
||||
infill_sp.layer_height = layer_height
|
||||
if fl_height is not None:
|
||||
infill_sp.first_layer_height = fl_height
|
||||
infill_sp.transforms = list(getattr(coll, "transforms", [])) or list(getattr(merged_object, "transforms", []))
|
||||
|
||||
if names:
|
||||
infill_sp.name = "-".join(names) + "_infill"
|
||||
infill_sp.objects_info = {"objects": [{"name": n + "_infill", "polygon": []} for n in names]}
|
||||
|
||||
if first_layer_z is not None and fl_height is not None and layer_height > 0:
|
||||
mult = fl_height / layer_height
|
||||
for i, p in enumerate(infill_sp):
|
||||
if len(p) > 0 and abs(p[0, 2] - first_layer_z) < 1e-4:
|
||||
infill_sp.extrusion_rate[i] = infill_sp.extrusion_rate[i] * mult
|
||||
|
||||
if rotation and any(rotation) and mat_inv is not None:
|
||||
infill_sp = infill_sp.transform(mat_inv)
|
||||
|
||||
if cache is not None:
|
||||
cache[cache_key] = (perim_sp, infill_sp)
|
||||
|
||||
if socket_name in ("Perimeters", "Paths"):
|
||||
return perim_sp
|
||||
elif socket_name in ("Infill Contours", "Infill"):
|
||||
return infill_sp
|
||||
return perim_sp
|
||||
|
||||
@register_class
|
||||
class SLICENODE_BufferShells(SliceNode):
|
||||
bl_label = "Buffer Shells"
|
||||
|
|
|
|||
|
|
@ -505,6 +505,7 @@ class SLICENODE_MT_Geometry(Menu):
|
|||
layout.operator("node.add_node", text="Get Paths").type = "SLICENODE_GetPaths"
|
||||
layout.separator()
|
||||
layout.operator("node.add_node", text="Slice Shells").type = "SLICENODE_SliceShells"
|
||||
layout.operator("node.add_node", text="CUDA Slice Mesh").type = "SLICENODE_CudaSliceMesh"
|
||||
layout.operator("node.add_node", text="Buffer Shells").type = "SLICENODE_BufferShells"
|
||||
layout.operator("node.add_node", text="Buffer Shells Multiple").type = "SLICENODE_BufferShellsMultiple"
|
||||
layout.operator("node.add_node", text="Buffer Curve").type = "SLICENODE_BufferCurve"
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ This file provides a mapping of modules to their primary responsibilities and ke
|
|||
- `geometry.py`:
|
||||
- Geometry utility functions.
|
||||
|
||||
- `cudaslicer_loader.py`:
|
||||
- `import_cudaslicer()`: Helper function to safely load the `cudaslicer` library.
|
||||
|
||||
## `/core/pdf_exporter` (PDF Export & Technical Marking Engine)
|
||||
|
||||
- `sa2_measures.py`:
|
||||
|
|
|
|||
12
doc/NODES.md
12
doc/NODES.md
|
|
@ -82,6 +82,18 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
|||
- **Extra Attributes**: Dictionary mapping mesh attribute names to custom G-code prefixes (e.g., `opening_size` -> `S`).
|
||||
- **Slice Shells**: Slices a `SlicingObject` into 2D `SlicePaths` at a given layer height. Automatically aligns path seams to the sharpest concave angle (inner corner) of each layer. Supports rotation for slicing at an angle.
|
||||
- **Ignore Holes**: Boolean option to omit interior hole perimeters and generate only the outer shell boundaries.
|
||||
- **CUDA Slice Mesh**: High-performance GPU-accelerated 3D mesh slicing and multi-pass perimeter offsetting via `cudaslicer`. Automatically aligns path seams to sharpest concave angles.
|
||||
- **Mesh**: Input mesh geometry (`SlicingGroup`, `SlicingCollection`, or `SlicingObject`).
|
||||
- **Layer Height**: Z layer slice height in mm (default: `0.2`).
|
||||
- **First Layer Height**: First layer slice height in mm (default: `0.2`). Adjusts first layer extrusion rate accordingly.
|
||||
- **Perimeters Count**: Number of concentric outer perimeter passes (default: `2`).
|
||||
- **Layer Width**: Extrusion line width in mm (default: `0.45`).
|
||||
- **Pitch**: Raster resolution pitch in mm (default: `0.10`).
|
||||
- **Pad**: Bounding box margin padding in mm (default: `1.5`).
|
||||
- **Rotation**: Optional Euler rotation for slicing at arbitrary angles.
|
||||
- **Backend**: Execution backend switch: **Auto** (CUDA GPU with multi-threaded CPU fallback), **CUDA** (force CUDA GPU), or **CPU** (multi-threaded CPU execution).
|
||||
- **Perimeters**: Output `SlicePaths` for perimeter wall toolpaths.
|
||||
- **Infill Contours**: Output `SlicePaths` for infill boundary loops.
|
||||
- **Buffer Shells**: Offsets/buffers 2D `SlicePaths`. Supports rotated offsetting and multiple miter types.
|
||||
- **Buffer Shells Multiple**: Offsets 2D `SlicePaths` with multiple buffer passes.
|
||||
- **Buffer Curve**: Buffers open/closed curves using Shapely, allowing open path offsetting.
|
||||
|
|
|
|||
|
|
@ -128,22 +128,32 @@ class SliceNode(Node, AutoBlIdName):
|
|||
return None
|
||||
return resolve_input(inputs.get(fieldname), cache=cache, visited=visited)
|
||||
|
||||
def add_output(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = ""):
|
||||
def add_output(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = "", description: str = ""):
|
||||
bl_idname = NATIVE_SOCKET_MAP.get(bl_idname, bl_idname)
|
||||
eval_name = name or NATIVE_SOCKET_LABELS.get(bl_idname) or get_label_by_name(bl_idname) or bl_idname
|
||||
sock = self.outputs.new(bl_idname, eval_name, identifier=identifier or eval_name)
|
||||
sock.hide = hide
|
||||
if display_shape:
|
||||
sock.display_shape = display_shape
|
||||
if description and hasattr(sock, "description"):
|
||||
try:
|
||||
sock.description = description
|
||||
except Exception:
|
||||
pass
|
||||
return sock
|
||||
|
||||
def add_input(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = ""):
|
||||
def add_input(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = "", description: str = ""):
|
||||
bl_idname = NATIVE_SOCKET_MAP.get(bl_idname, bl_idname)
|
||||
eval_name = name or NATIVE_SOCKET_LABELS.get(bl_idname) or get_label_by_name(bl_idname) or bl_idname
|
||||
sock = self.inputs.new(bl_idname, eval_name, identifier=identifier or eval_name)
|
||||
sock.hide = hide
|
||||
if display_shape:
|
||||
sock.display_shape = display_shape
|
||||
if description and hasattr(sock, "description"):
|
||||
try:
|
||||
sock.description = description
|
||||
except Exception:
|
||||
pass
|
||||
return sock
|
||||
|
||||
def add_multi_socket_group(self, in_type: str, out_type: str = "", name: str = "", in_name: str = "", out_name: str = "", in_display_shape: str = "", out_display_shape: str = ""):
|
||||
|
|
|
|||
288
tests/test_cudaslicer.py
Normal file
288
tests/test_cudaslicer.py
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
import sys
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
# Add workspace directory to sys.path to resolve root package name (NodeSlicer)
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(PROJECT_ROOT.parent) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT.parent))
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
# Define dummy classes to avoid metaclass conflicts
|
||||
class DummyNodeSocket: pass
|
||||
class DummyNode: pass
|
||||
class DummyObject:
|
||||
def __init__(self, name="TestObj"):
|
||||
self.name = name
|
||||
class DummyMesh: pass
|
||||
class DummyNodeSocketShader: pass
|
||||
class DummyNodeCustomGroup: pass
|
||||
class DummyNodeTree: pass
|
||||
class DummyOperator: pass
|
||||
class DummyMenu: pass
|
||||
class DummyPanel: pass
|
||||
class DummyPropertyGroup: pass
|
||||
|
||||
mock_types = MagicMock()
|
||||
mock_types.NodeSocket = DummyNodeSocket
|
||||
mock_types.Node = DummyNode
|
||||
mock_types.Object = DummyObject
|
||||
mock_types.Mesh = DummyMesh
|
||||
mock_types.NodeSocketShader = DummyNodeSocketShader
|
||||
mock_types.NodeCustomGroup = DummyNodeCustomGroup
|
||||
mock_types.NodeTree = DummyNodeTree
|
||||
mock_types.Operator = DummyOperator
|
||||
mock_types.Menu = DummyMenu
|
||||
mock_types.Panel = DummyPanel
|
||||
mock_types.PropertyGroup = DummyPropertyGroup
|
||||
|
||||
mock_bpy = MagicMock()
|
||||
mock_bpy.context.scene = MagicMock()
|
||||
mock_bpy.context.scene.unit_settings.scale_length = 0.001
|
||||
mock_bpy.types = mock_types
|
||||
|
||||
mock_previews = MagicMock()
|
||||
mock_previews.ImagePreviewCollection = MagicMock
|
||||
|
||||
sys.modules['bpy'] = mock_bpy
|
||||
sys.modules['bpy.types'] = mock_types
|
||||
sys.modules['bpy.props'] = MagicMock()
|
||||
sys.modules['bpy.utils'] = MagicMock()
|
||||
sys.modules['bpy.utils.previews'] = mock_previews
|
||||
sys.modules['bpy.app'] = MagicMock()
|
||||
sys.modules['bpy.app.handlers'] = MagicMock()
|
||||
|
||||
# Import cudaslicer loader and NodeSlicer modules
|
||||
from NodeSlicer.core.cudaslicer_loader import import_cudaslicer
|
||||
from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject
|
||||
from NodeSlicer.core.slice import SlicePaths
|
||||
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_CudaSliceMesh
|
||||
|
||||
class TestCudaSlicer(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.cudaslicer = import_cudaslicer()
|
||||
|
||||
def test_cudaslicer_loader(self):
|
||||
self.assertIsNotNone(self.cudaslicer, "cudaslicer loader should successfully import cudaslicer")
|
||||
self.assertTrue(self.cudaslicer.is_available(), "cudaslicer backend should be available")
|
||||
|
||||
def test_cudaslicer_mesh_slicing(self):
|
||||
# Create a simple 10x10x10 cube
|
||||
tris = np.array([
|
||||
[[0,0,0], [10,0,0], [10,10,0]],
|
||||
[[0,0,0], [10,10,0], [0,10,0]],
|
||||
[[0,0,10], [10,10,10], [10,0,10]],
|
||||
[[0,0,10], [0,10,10], [10,10,10]],
|
||||
[[0,0,0], [0,10,0], [0,10,10]],
|
||||
[[0,0,0], [0,10,10], [0,0,10]],
|
||||
[[10,0,0], [10,10,10], [10,10,0]],
|
||||
[[10,0,0], [10,0,10], [10,10,10]],
|
||||
[[0,0,0], [10,0,10], [10,0,0]],
|
||||
[[0,0,0], [0,0,10], [10,0,10]],
|
||||
[[0,10,0], [10,10,0], [10,10,10]],
|
||||
[[0,10,0], [10,10,10], [0,10,10]],
|
||||
], dtype=np.float32)
|
||||
|
||||
layers = self.cudaslicer.slice_mesh_and_offset(
|
||||
mesh_or_triangles=tris,
|
||||
layer_height=1.0,
|
||||
first_layer_height=0.5,
|
||||
perimeters_count=2,
|
||||
layer_width=0.45,
|
||||
pitch_mm=0.1,
|
||||
pad_mm=1.5,
|
||||
backend="auto"
|
||||
)
|
||||
|
||||
self.assertGreater(len(layers), 0, "Should generate sliced layers")
|
||||
self.assertAlmostEqual(layers[0].z, 0.5, places=4)
|
||||
self.assertGreater(len(layers[0].perimeters), 0)
|
||||
|
||||
def test_cuda_slice_mesh_node_evaluation(self):
|
||||
# Create dummy SlicingObject
|
||||
so = SlicingObject()
|
||||
so.name = "TestCube"
|
||||
|
||||
# Build (12, 4, 3) mesh representation
|
||||
tris = np.array([
|
||||
[[0,0,0], [10,0,0], [10,10,0]],
|
||||
[[0,0,0], [10,10,0], [0,10,0]],
|
||||
[[0,0,10], [10,10,10], [10,0,10]],
|
||||
[[0,0,10], [0,10,10], [10,10,10]],
|
||||
[[0,0,0], [0,10,0], [0,10,10]],
|
||||
[[0,0,0], [0,10,10], [0,0,10]],
|
||||
[[10,0,0], [10,10,10], [10,10,0]],
|
||||
[[10,0,0], [10,0,10], [10,10,10]],
|
||||
[[0,0,0], [10,0,10], [10,0,0]],
|
||||
[[0,0,0], [0,0,10], [10,0,10]],
|
||||
[[0,10,0], [10,10,0], [10,10,10]],
|
||||
[[0,10,0], [10,10,10], [0,10,10]],
|
||||
], dtype=np.float64)
|
||||
|
||||
normals = np.zeros((12, 1, 3), dtype=np.float64)
|
||||
so.mesh = np.concatenate([tris, normals], axis=1)
|
||||
|
||||
node = SLICENODE_CudaSliceMesh()
|
||||
node.name = "CUDA_Slice_Mesh"
|
||||
|
||||
# Mock resolve method
|
||||
resolve_map = {
|
||||
"Mesh": so,
|
||||
"Layer Height": 1.0,
|
||||
"First Layer Height": 0.5,
|
||||
"Perimeters Count": 2,
|
||||
"Layer Width": 0.45,
|
||||
"Pitch": 0.10,
|
||||
"Pad": 1.5,
|
||||
"Rotation": (0.0, 0.0, 0.0),
|
||||
}
|
||||
node.resolve = lambda key: resolve_map.get(key)
|
||||
node.backend = "AUTO"
|
||||
|
||||
cache = {}
|
||||
perimeters_paths = node.evaluate_output("Perimeters", cache=cache)
|
||||
self.assertIsInstance(perimeters_paths, SlicePaths)
|
||||
self.assertGreater(len(perimeters_paths), 0)
|
||||
self.assertEqual(perimeters_paths.layer_height, 1.0)
|
||||
self.assertEqual(perimeters_paths.first_layer_height, 0.5)
|
||||
|
||||
# First layer paths should have adjusted extrusion rate
|
||||
z_first = 0.5
|
||||
for i, p in enumerate(perimeters_paths):
|
||||
if len(p) > 0 and abs(p[0, 2] - z_first) < 1e-4:
|
||||
self.assertTrue(np.allclose(perimeters_paths.extrusion_rate[i], 0.5))
|
||||
|
||||
# Test infill contours evaluation (should reuse cache)
|
||||
infill_paths = node.evaluate_output("Infill Contours", cache=cache)
|
||||
self.assertIsInstance(infill_paths, SlicePaths)
|
||||
self.assertGreater(len(infill_paths), 0)
|
||||
|
||||
def test_cuda_slice_mesh_with_rotation(self):
|
||||
so = SlicingObject()
|
||||
so.name = "RotatedCube"
|
||||
|
||||
tris = np.array([
|
||||
[[0,0,0], [10,0,0], [10,10,0]],
|
||||
[[0,0,0], [10,10,0], [0,10,0]],
|
||||
[[0,0,10], [10,10,10], [10,0,10]],
|
||||
[[0,0,10], [0,10,10], [10,10,10]],
|
||||
[[0,0,0], [0,10,0], [0,10,10]],
|
||||
[[0,0,0], [0,10,10], [0,0,10]],
|
||||
[[10,0,0], [10,10,10], [10,10,0]],
|
||||
[[10,0,0], [10,0,10], [10,10,10]],
|
||||
[[0,0,0], [10,0,10], [10,0,0]],
|
||||
[[0,0,0], [0,0,10], [10,0,10]],
|
||||
[[0,10,0], [10,10,0], [10,10,10]],
|
||||
[[0,10,0], [10,10,10], [0,10,10]],
|
||||
], dtype=np.float64)
|
||||
|
||||
normals = np.zeros((12, 1, 3), dtype=np.float64)
|
||||
so.mesh = np.concatenate([tris, normals], axis=1)
|
||||
|
||||
node = SLICENODE_CudaSliceMesh()
|
||||
resolve_map = {
|
||||
"Mesh": so,
|
||||
"Layer Height": 1.0,
|
||||
"First Layer Height": 1.0,
|
||||
"Perimeters Count": 1,
|
||||
"Layer Width": 0.45,
|
||||
"Pitch": 0.10,
|
||||
"Pad": 1.5,
|
||||
"Rotation": (0.0, 0.0, float(np.radians(45.0))),
|
||||
}
|
||||
node.resolve = lambda key: resolve_map.get(key)
|
||||
node.backend = "AUTO"
|
||||
|
||||
paths = node.evaluate_output("Perimeters")
|
||||
self.assertIsInstance(paths, SlicePaths)
|
||||
self.assertGreater(len(paths), 0)
|
||||
|
||||
def test_cuda_slice_mesh_backends(self):
|
||||
so = SlicingObject()
|
||||
tris = np.array([
|
||||
[[0,0,0], [10,0,0], [10,10,0]],
|
||||
[[0,0,0], [10,10,0], [0,10,0]],
|
||||
[[0,0,10], [10,10,10], [10,0,10]],
|
||||
[[0,0,10], [0,10,10], [10,10,10]],
|
||||
[[0,0,0], [0,10,0], [0,10,10]],
|
||||
[[0,0,0], [0,10,10], [0,0,10]],
|
||||
[[10,0,0], [10,10,10], [10,10,0]],
|
||||
[[10,0,0], [10,0,10], [10,10,10]],
|
||||
[[0,0,0], [10,0,10], [10,0,0]],
|
||||
[[0,0,0], [0,0,10], [10,0,10]],
|
||||
[[0,10,0], [10,10,0], [10,10,10]],
|
||||
[[0,10,0], [10,10,10], [0,10,10]],
|
||||
], dtype=np.float64)
|
||||
so.mesh = np.concatenate([tris, np.zeros((12, 1, 3))], axis=1)
|
||||
|
||||
node = SLICENODE_CudaSliceMesh()
|
||||
resolve_map = {
|
||||
"Mesh": so,
|
||||
"Layer Height": 1.0,
|
||||
"Perimeters Count": 1,
|
||||
"Layer Width": 0.45,
|
||||
"Pitch": 0.10,
|
||||
"Pad": 1.5,
|
||||
}
|
||||
node.resolve = lambda key: resolve_map.get(key)
|
||||
|
||||
for b in ["AUTO", "CPU"]:
|
||||
node.backend = b
|
||||
paths = node.evaluate_output("Perimeters")
|
||||
self.assertGreater(len(paths), 0, f"Backend {b} should produce paths")
|
||||
|
||||
def test_cuda_slice_mesh_empty(self):
|
||||
node = SLICENODE_CudaSliceMesh()
|
||||
node.resolve = lambda key: None
|
||||
paths = node.evaluate_output("Perimeters")
|
||||
self.assertIsInstance(paths, SlicePaths)
|
||||
self.assertEqual(len(paths), 0)
|
||||
|
||||
def test_cuda_slice_mesh_evaluation_cleaned(self):
|
||||
so = SlicingObject()
|
||||
so.name = "TestCube"
|
||||
tris = np.array([
|
||||
[[0,0,0], [10,0,0], [10,10,0]],
|
||||
[[0,0,0], [10,10,0], [0,10,0]],
|
||||
[[0,0,10], [10,10,10], [10,0,10]],
|
||||
[[0,0,10], [0,10,10], [10,10,10]],
|
||||
[[0,0,0], [0,10,0], [0,10,10]],
|
||||
[[0,0,0], [0,10,10], [0,0,10]],
|
||||
[[10,0,0], [10,10,10], [10,10,0]],
|
||||
[[10,0,0], [10,0,10], [10,10,10]],
|
||||
[[0,0,0], [10,0,10], [10,0,0]],
|
||||
[[0,0,0], [0,0,10], [10,0,10]],
|
||||
[[0,10,0], [10,10,0], [10,10,10]],
|
||||
[[0,10,0], [10,10,10], [0,10,10]],
|
||||
], dtype=np.float64)
|
||||
so.mesh = np.concatenate([tris, np.zeros((12, 1, 3))], axis=1)
|
||||
|
||||
node = SLICENODE_CudaSliceMesh()
|
||||
resolve_map = {
|
||||
"Mesh": so,
|
||||
"Layer Height": 1.0,
|
||||
"First Layer Height": 0.5,
|
||||
"Perimeters Count": 2,
|
||||
"Layer Width": 0.45,
|
||||
"Pitch": 0.10,
|
||||
"Pad": 1.5,
|
||||
"Rotation": (0.0, 0.0, 0.0),
|
||||
}
|
||||
node.resolve = lambda key: resolve_map.get(key)
|
||||
node.backend = "AUTO"
|
||||
|
||||
cache = {}
|
||||
perim_paths = node.evaluate_output("Perimeters", cache=cache)
|
||||
infill_contours = node.evaluate_output("Infill Contours", cache=cache)
|
||||
|
||||
self.assertIsInstance(perim_paths, SlicePaths)
|
||||
self.assertIsInstance(infill_contours, SlicePaths)
|
||||
self.assertGreater(len(perim_paths), 0)
|
||||
self.assertGreater(len(infill_contours), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Reference in a new issue