From 5ed9836f2185cfe7b877d0f4a928c6a06e6aec28 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Sun, 27 Sep 2026 13:52:50 +0300 Subject: [PATCH] rm cudaslicer --- build.sh | 3 +- core/cudaslicer_loader.py | 21 -- core/nodeslicer/nodes/path.py | 238 ----------------------- doc/MODULES.md | 3 - doc/NODES.md | 12 -- tests/profile_node_eval.py | 197 ------------------- tests/test_cudaslicer.py | 352 ---------------------------------- 7 files changed, 2 insertions(+), 824 deletions(-) delete mode 100644 core/cudaslicer_loader.py delete mode 100644 tests/profile_node_eval.py delete mode 100644 tests/test_cudaslicer.py diff --git a/build.sh b/build.sh index 9761f68..c110376 100755 --- a/build.sh +++ b/build.sh @@ -9,7 +9,8 @@ cleanup() { rm -rf "$TMPDIR"; } trap cleanup EXIT mapfile -t FILES < <(git ls-files -co --exclude-standard \ - | grep -v -xE '(\.gitignore|build\.sh)') + | grep -v -xE '(\.gitignore|build\.sh)' \ + | while read -r f; do [ -e "$f" ] && echo "$f"; done) tar -cf - "${FILES[@]}" | tar -xf - -C "$TMPDIR" if [ -d wheels ]; then diff --git a/core/cudaslicer_loader.py b/core/cudaslicer_loader.py deleted file mode 100644 index 0ec0311..0000000 --- a/core/cudaslicer_loader.py +++ /dev/null @@ -1,21 +0,0 @@ -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 - diff --git a/core/nodeslicer/nodes/path.py b/core/nodeslicer/nodes/path.py index b24c5b8..8233f7c 100644 --- a/core/nodeslicer/nodes/path.py +++ b/core/nodeslicer/nodes/path.py @@ -176,244 +176,6 @@ 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 - ) - - execution_time_ms: float = NfFloatProperty( - name="Execution Time (ms)", - description="Last execution time in milliseconds", - default=0.0, - update=lambda s, c: None - ) - - def draw_label(self) -> str: - base = getattr(self, "label", None) or self.bl_label - time_ms = getattr(self, "execution_time_ms", 0.0) - if time_ms and time_ms > 0.0: - if time_ms >= 10.0: - return f"{base} ({int(round(time_ms))}ms)" - elif time_ms >= 1.0: - return f"{base} ({time_ms:.1f}ms)" - else: - return f"{base} ({time_ms:.2f}ms)" - return base - - 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: - t_start = time.perf_counter() - coll: SlicingCollection | SlicingGroup | None = self.resolve("Mesh") - if not coll: - self.execution_time_ms = 0.0 - 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: - self.execution_time_ms = 0.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 is_closed_path(cnt_3d): - 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 is_closed_path(cnt_3d): - 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 - first_z_um = int(round(first_layer_z * 1000)) - for i, p in enumerate(perim_sp): - if len(p) > 0 and int(round(p[0, 2] * 1000)) == first_z_um: - 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 - first_z_um = int(round(first_layer_z * 1000)) - for i, p in enumerate(infill_sp): - if len(p) > 0 and int(round(p[0, 2] * 1000)) == first_z_um: - 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) - - t_end = time.perf_counter() - self.execution_time_ms = (t_end - t_start) * 1000.0 - - 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" diff --git a/doc/MODULES.md b/doc/MODULES.md index b818b52..08acf13 100644 --- a/doc/MODULES.md +++ b/doc/MODULES.md @@ -40,9 +40,6 @@ 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`: diff --git a/doc/NODES.md b/doc/NODES.md index 6427bfd..a7068a7 100644 --- a/doc/NODES.md +++ b/doc/NODES.md @@ -82,18 +82,6 @@ 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. diff --git a/tests/profile_node_eval.py b/tests/profile_node_eval.py deleted file mode 100644 index 7fb83e0..0000000 --- a/tests/profile_node_eval.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env python3 -import time -from pathlib import Path -import numpy as np - -import sys -repo_root = Path(__file__).resolve().parent.parent -if str(repo_root.parent) not in sys.path: - sys.path.insert(0, str(repo_root.parent)) -if str(repo_root) not in sys.path: - sys.path.insert(0, str(repo_root)) - -from unittest.mock import MagicMock -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() - -from NodeSlicer.core.cudaslicer_loader import import_cudaslicer -from NodeSlicer.core.slice import align_path_seam_sharpest, SlicePaths -from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject - -def load_stl(file_path): - with open(file_path, "rb") as f: - f.seek(80) - num_tris = int(np.frombuffer(f.read(4), dtype=np.uint32)[0]) - record_dtype = np.dtype([ - ("normal", np.float32, 3), - ("v0", np.float32, 3), - ("v1", np.float32, 3), - ("v2", np.float32, 3), - ("attr", np.uint16), - ]) - data = np.fromfile(f, dtype=record_dtype, count=num_tris) - tris = np.stack([data["v0"], data["v1"], data["v2"]], axis=1) # (N, 3, 3) - normals = np.zeros((num_tris, 1, 3), dtype=np.float32) - mesh = np.concatenate([tris, normals], axis=1) # (N, 4, 3) - return mesh - -def profile_node_pipeline(): - stl_path = "/home/nicolas/Workspace/Development/cudaslicer/benchy.stl" - mesh_data = load_stl(stl_path) - so = SlicingObject() - so.mesh = mesh_data - objects = [so] - - cudaslicer = import_cudaslicer() - - layer_height = 0.20 - fl_height = 0.20 - perimeters_count = 2 - layer_width = 0.45 - pitch_mm = 0.10 - pad_mm = 1.5 - backend_choice = "AUTO" - - # Warmup - _ = cudaslicer.slice_mesh_to_sdf(mesh_data[:100, :3, :], layer_height=1.0, pitch_mm=0.10) - - print("==========================================================================") - print(f" PROFILING SLICENODE_CudaSliceMesh.evaluate_output (Benchy: {len(mesh_data):,} tris)") - print("==========================================================================") - - t_total_0 = time.perf_counter() - - # Step 1: Object merge - t0 = time.perf_counter() - merged_object = SlicingObject.merge(objects) - t_merge = (time.perf_counter() - t0) * 1000.0 - - # Step 2: Mesh array copy & float32 cast - t0 = time.perf_counter() - mesh_data_copy = merged_object.mesh.copy() - tris_3d = mesh_data_copy[:, :3, :].astype(np.float32) - t_copy = (time.perf_counter() - t0) * 1000.0 - - # Step 3: cudaslicer.slice_mesh_and_offset - t0 = time.perf_counter() - layers = cudaslicer.slice_mesh_and_offset( - mesh_or_triangles=tris_3d, - layer_height=float(layer_height), - first_layer_height=float(fl_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(), - ) - t_cudaslicer = (time.perf_counter() - t0) * 1000.0 - - # Step 4: Python conversion loop (unpacking contours, column_stack, vstack, align_path_seam_sharpest) - perim_paths = [] - perim_groups = [] - infill_paths = [] - infill_groups = [] - - t0 = time.perf_counter() - t_seam_align = 0.0 - t_stack = 0.0 - - 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 - t_st0 = time.perf_counter() - 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]]) - t_stack += (time.perf_counter() - t_st0) * 1000.0 - - t_s0 = time.perf_counter() - cnt_3d = align_path_seam_sharpest(cnt_3d) - t_seam_align += (time.perf_counter() - t_s0) * 1000.0 - - perim_paths.append(cnt_3d) - perim_groups.append(l_idx) - - for cnt in getattr(layer, "infill_contours", []): - if len(cnt) < 2: - continue - t_st0 = time.perf_counter() - 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]]) - t_stack += (time.perf_counter() - t_st0) * 1000.0 - - t_s0 = time.perf_counter() - cnt_3d = align_path_seam_sharpest(cnt_3d) - t_seam_align += (time.perf_counter() - t_s0) * 1000.0 - - infill_paths.append(cnt_3d) - infill_groups.append(l_idx) - - t_loop_total = (time.perf_counter() - t0) * 1000.0 - - # Step 5: SlicePaths object instantiation - t0 = time.perf_counter() - perim_sp = SlicePaths(perim_paths, groups=perim_groups) - infill_sp = SlicePaths(infill_paths, groups=infill_groups) - t_slicepaths = (time.perf_counter() - t0) * 1000.0 - - t_total = (time.perf_counter() - t_total_0) * 1000.0 - - print(f" 1. SlicingObject.merge: {t_merge:7.2f} ms") - print(f" 2. Mesh copy & astype(float32): {t_copy:7.2f} ms") - print(f" 3. cudaslicer.slice_mesh_and_offset: {t_cudaslicer:7.2f} ms") - print(f" 4. Python Loop Total: {t_loop_total:7.2f} ms") - print(f" • np.column_stack & np.vstack: {t_stack:7.2f} ms") - print(f" • align_path_seam_sharpest (Python): {t_seam_align:7.2f} ms") - print(f" 5. SlicePaths instantiation: {t_slicepaths:7.2f} ms") - print(f"--------------------------------------------------------------------------") - print(f" TOTAL evaluate_output Wall Time: {t_total:7.2f} ms") - print("==========================================================================") - -if __name__ == "__main__": - profile_node_pipeline() diff --git a/tests/test_cudaslicer.py b/tests/test_cudaslicer.py deleted file mode 100644 index 6963b4c..0000000 --- a/tests/test_cudaslicer.py +++ /dev/null @@ -1,352 +0,0 @@ -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) - - def test_cuda_slice_mesh_timing_in_label(self): - node = SLICENODE_CudaSliceMesh() - node.name = "CUDA_Slice_Mesh" - - # Initially no execution time recorded -> default label - self.assertEqual(node.draw_label(), "CUDA Slice Mesh") - - # Test simulated timing values and label formatting - node.execution_time_ms = 10.2 - self.assertEqual(node.draw_label(), "CUDA Slice Mesh (10ms)") - - node.execution_time_ms = 35.8 - self.assertEqual(node.draw_label(), "CUDA Slice Mesh (36ms)") - - node.execution_time_ms = 4.2 - self.assertEqual(node.draw_label(), "CUDA Slice Mesh (4.2ms)") - - node.execution_time_ms = 0.35 - self.assertEqual(node.draw_label(), "CUDA Slice Mesh (0.35ms)") - - # Custom user label support - node.label = "My Benchy Slicer" - node.execution_time_ms = 12.0 - self.assertEqual(node.draw_label(), "My Benchy Slicer (12ms)") - - # After real evaluation, timing should be automatically measured and formatted - node.label = "" - 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,10,0], [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) - - 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" - - node.evaluate_output("Perimeters") - self.assertGreater(node.execution_time_ms, 0.0) - label = node.draw_label() - self.assertTrue(label.startswith("CUDA Slice Mesh (") and label.endswith("ms)"), f"Label was: {label}") - - -if __name__ == "__main__": - unittest.main() -