import numpy as np import unittest from unittest.mock import MagicMock import sys from pathlib import Path # Add workspace directory to sys.path to resolve root package name (NodeSlicer) PROJECT_ROOT = Path(__file__).resolve().parent.parent sys.path.append(str(PROJECT_ROOT.parent)) # Define dummy classes to avoid metaclass conflicts class DummyNodeSocket: pass class DummyNode: pass class DummyObject: pass 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 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'] = MagicMock() sys.modules['bpy.app'] = MagicMock() sys.modules['bpy.app.handlers'] = MagicMock() # Import the actual functions to test from NodeSlicer.core.slice import segments_to_slice, link_segments class TestSlice(unittest.TestCase): def test_link_segments_closed(self): # Closed triangle: A(0,0,10) -> B(10,0,10) -> C(0,10,10) -> A(0,0,10) segments = np.array([ [[0.0, 0.0, 10.0], [10.0, 0.0, 10.0]], [[10.0, 0.0, 10.0], [0.0, 10.0, 10.0]], [[0.0, 10.0, 10.0], [0.0, 0.0, 10.0]] ]) linked = link_segments(segments) self.assertEqual(len(linked), 1) path = linked[0] # Should start and end at the same point (A) self.assertTrue(np.allclose(path[0], path[-1])) # Verify segments_to_slice closes it sliced = segments_to_slice(segments) self.assertEqual(len(sliced), 1) sliced_path = sliced[0] self.assertTrue(np.allclose(sliced_path[0], sliced_path[-1])) def test_link_segments_open(self): # Open path: A(0,0,10) -> B(10,0,10) -> C(0,10,10) (no C->A segment) segments = np.array([ [[0.0, 0.0, 10.0], [10.0, 0.0, 10.0]], [[10.0, 0.0, 10.0], [0.0, 10.0, 10.0]] ]) linked = link_segments(segments) self.assertEqual(len(linked), 1) path = linked[0] # Should NOT start and end at the same point self.assertFalse(np.allclose(path[0], path[-1])) # Verify segments_to_slice does NOT close it sliced = segments_to_slice(segments) self.assertEqual(len(sliced), 1) sliced_path = sliced[0] self.assertFalse(np.allclose(sliced_path[0], sliced_path[-1]), f"Path was incorrectly closed: {sliced_path}") def test_buffer_curve_basic(self): # Open path (line segment from (0,0,10) to (10,0,10)) path = np.array([[0.0, 0.0, 10.0], [10.0, 0.0, 10.0]]) from NodeSlicer.core.slice import SlicePaths slice_paths = SlicePaths([path]) # Buffer curve with distance 1.0 buffered = slice_paths.buffer_curve(1.0, cap_style='flat') # Output should be a SlicePaths instance self.assertIsInstance(buffered, SlicePaths) self.assertEqual(len(buffered), 1) # Check that the path is now closed (buffered area boundary) buffered_path = buffered[0] self.assertTrue(np.allclose(buffered_path[0], buffered_path[-1])) # Flat cap with distance 1.0 should result in a rectangle: # width = 10 (from 0 to 10), height = 2 (from -1 to 1) -> area = 20 # Let's compute 2D polygon area of the resulting path xs = buffered_path[:, 0] ys = buffered_path[:, 1] # Shoelace formula for area area = 0.5 * np.abs(np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1))) self.assertAlmostEqual(area, 20.0, places=3) # Check Z coordinate is preserved self.assertTrue(np.allclose(buffered_path[:, 2], 10.0)) def test_buffer_curve_cap_styles(self): path = np.array([[0.0, 0.0, 5.0], [10.0, 0.0, 5.0]]) from NodeSlicer.core.slice import SlicePaths slice_paths = SlicePaths([path]) # Round cap buffered_round = slice_paths.buffer_curve(1.0, cap_style='round') # Flat cap buffered_flat = slice_paths.buffer_curve(1.0, cap_style='flat') # Square cap buffered_square = slice_paths.buffer_curve(1.0, cap_style='square') # Area comparison: flat (20.0) < round (20.0 + pi) < square (24.0) def get_area(p): xs, ys = p[:, 0], p[:, 1] return 0.5 * np.abs(np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1))) area_flat = get_area(buffered_flat[0]) area_round = get_area(buffered_round[0]) area_square = get_area(buffered_square[0]) self.assertAlmostEqual(area_flat, 20.0, places=3) self.assertAlmostEqual(area_square, 24.0, places=3) self.assertGreater(area_round, area_flat) self.assertLess(area_round, area_square) def test_buffer_curve_single_sided(self): path = np.array([[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]]) from NodeSlicer.core.slice import SlicePaths slice_paths = SlicePaths([path]) buffered = slice_paths.buffer_curve(1.0, cap_style='flat', single_sided=True) # For a single sided flat cap buffer, the rectangle area is distance * length = 1.0 * 10.0 = 10.0 xs, ys = buffered[0][:, 0], buffered[0][:, 1] area = 0.5 * np.abs(np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1))) self.assertAlmostEqual(area, 10.0, places=3) def test_offset_curve_basic(self): # Open path (line segment from (0,0,10) to (10,0,10)) path = np.array([[0.0, 0.0, 10.0], [10.0, 0.0, 10.0]]) from NodeSlicer.core.slice import SlicePaths slice_paths = SlicePaths([path]) # Offset curve by 1.0 (should shift it to (0, 1, 10) -> (10, 1, 10)) offset = slice_paths.offset_curve(1.0) self.assertIsInstance(offset, SlicePaths) self.assertEqual(len(offset), 1) offset_path = offset[0] # Should remain an open path (first and last coordinate not equal) self.assertFalse(np.allclose(offset_path[0], offset_path[-1])) # Verify the shifted coordinates self.assertTrue(np.allclose(offset_path[:, 1], 1.0)) self.assertTrue(np.allclose(offset_path[:, 2], 10.0)) self.assertTrue(np.allclose(offset_path[0, 0], 0.0)) self.assertTrue(np.allclose(offset_path[1, 0], 10.0)) def test_offset_curve_closed_loop(self): # Closed square path: (0,0) -> (0,10) -> (10,10) -> (10,0) -> (0,0) at Z=5.0 path = np.array([ [0.0, 0.0, 5.0], [0.0, 10.0, 5.0], [10.0, 10.0, 5.0], [10.0, 0.0, 5.0], [0.0, 0.0, 5.0] ]) from NodeSlicer.core.slice import SlicePaths slice_paths = SlicePaths([path]) # Offset curve by +1.0 # Positive distance in a clockwise loop should shift it inward (to a square of size 8.0) offset_in = slice_paths.offset_curve(1.0, join_style='mitre') self.assertIsInstance(offset_in, SlicePaths) self.assertEqual(len(offset_in), 1) offset_path = offset_in[0] # Should remain perfectly closed (first and last coordinate equal) self.assertTrue(np.allclose(offset_path[0], offset_path[-1]), f"Offset path was not closed: {offset_path}") # Calculate the area of the offset path xs, ys = offset_path[:, 0], offset_path[:, 1] area = 0.5 * np.abs(np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1))) self.assertAlmostEqual(area, 64.0, places=3) # Check Z coordinate is preserved self.assertTrue(np.allclose(offset_path[:, 2], 5.0)) def test_get_infill_mask(self): # Two overlapping closed squares on the same layer Z=3.0 # Square 1: (0,0) to (10,10) sq1 = np.array([ [0.0, 0.0, 3.0], [0.0, 10.0, 3.0], [10.0, 10.0, 3.0], [10.0, 0.0, 3.0], [0.0, 0.0, 3.0] ]) # Square 2: (5,5) to (15,15) - overlaps from (5,5) to (10,10) sq2 = np.array([ [5.0, 5.0, 3.0], [5.0, 15.0, 3.0], [15.0, 15.0, 3.0], [15.0, 5.0, 3.0], [5.0, 5.0, 3.0] ]) from NodeSlicer.core.slice import SlicePaths # Both in group 0 slice_paths = SlicePaths([sq1, sq2], groups=[0, 0]) mask = slice_paths.get_infill_mask() self.assertIsInstance(mask, SlicePaths) self.assertEqual(len(mask), 1) # Intersection of two squares should be one square (5,5) to (10,10) mask_path = mask[0] self.assertTrue(np.allclose(mask_path[0], mask_path[-1])) # Area of intersection should be 5 * 5 = 25 xs, ys = mask_path[:, 0], mask_path[:, 1] area = 0.5 * np.abs(np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1))) self.assertAlmostEqual(area, 25.0, places=3) self.assertTrue(np.allclose(mask_path[:, 2], 3.0)) def test_get_overhang_mask(self): # Layer 2 (Z=2.0): square of size 4 (from -2 to 2) sq2 = np.array([ [-2.0, -2.0, 2.0], [-2.0, 2.0, 2.0], [ 2.0, 2.0, 2.0], [ 2.0, -2.0, 2.0], [-2.0, -2.0, 2.0] ]) # Layer 1 (Z=1.0): square of size 2 (from -1 to 1) sq1 = np.array([ [-1.0, -1.0, 1.0], [-1.0, 1.0, 1.0], [ 1.0, 1.0, 1.0], [ 1.0, -1.0, 1.0], [-1.0, -1.0, 1.0] ]) # Layer 0 (Z=0.0): square of size 4 (from -2 to 2) sq0 = np.array([ [-2.0, -2.0, 0.0], [-2.0, 2.0, 0.0], [ 2.0, 2.0, 0.0], [ 2.0, -2.0, 0.0], [-2.0, -2.0, 0.0] ]) from NodeSlicer.core.slice import SlicePaths # Groups sorted by Z height internally: group 0 (Z=0.0), group 1 (Z=1.0), group 2 (Z=2.0) slice_paths = SlicePaths([sq0, sq1, sq2], groups=[0, 1, 2]) overhang = slice_paths.get_overhang_mask(tolerance=0.0) self.assertIsInstance(overhang, SlicePaths) # Top layer Z=2.0 (group 2) and bottom layer Z=0.0 (group 0) should have no overhang paths. # Only layer Z=1.0 (group 1) should have overhang paths. # The paths on group 1 represent the ring: outer boundary (4x4) and inner boundary (2x2). self.assertEqual(len(overhang), 2) self.assertTrue(all(g == 1 for g in overhang.groups)) # Total area of the two paths on layer 1 should be: outer square (16) - inner square (4) = 12.0 from shapely.geometry import Polygon poly_out = Polygon(overhang[0][:, :2]) poly_in = Polygon(overhang[1][:, :2]) area1 = poly_out.area area2 = poly_in.area self.assertEqual(sorted([area1, area2]), [4.0, 16.0]) def test_get_overhang_mask_vertical_walls(self): # Two identical squares on adjacent layers Z=0.0 and Z=1.0, representing perfectly vertical walls # To simulate slicing noise/discrepancies, we shift one by a tiny amount (0.001) sq0 = np.array([ [0.0, 0.0, 0.0], [0.0, 10.0, 0.0], [10.0, 10.0, 0.0], [10.0, 0.0, 0.0], [0.0, 0.0, 0.0] ]) sq1 = np.array([ [0.001, 0.001, 1.0], [0.001, 10.001, 1.0], [10.001, 10.001, 1.0], [10.001, 0.001, 1.0], [0.001, 0.001, 1.0] ]) from NodeSlicer.core.slice import SlicePaths slice_paths = SlicePaths([sq0, sq1], groups=[0, 1]) # With default tolerance=0.05, it should completely filter out the vertical wall noise, yielding 0 overhang paths. overhang_with_tol = slice_paths.get_overhang_mask(tolerance=0.05) self.assertEqual(len(overhang_with_tol), 0) # With tolerance=0.0, it should preserve the tiny difference sliver, yielding non-empty paths. overhang_no_tol = slice_paths.get_overhang_mask(tolerance=0.0) self.assertGreater(len(overhang_no_tol), 0) def test_get_overhang_mask_propagation_no_gaps(self): # 3-layer tapering model (cone-like structure pointing down) # Layer 2 (Z=2.0): 4x4 square (from -2 to 2) sq2 = np.array([ [-2.0, -2.0, 2.0], [-2.0, 2.0, 2.0], [ 2.0, 2.0, 2.0], [ 2.0, -2.0, 2.0], [-2.0, -2.0, 2.0] ]) # Layer 1 (Z=1.0): 3x3 square (from -1.5 to 1.5) sq1 = np.array([ [-1.5, -1.5, 1.0], [-1.5, 1.5, 1.0], [ 1.5, 1.5, 1.0], [ 1.5, -1.5, 1.0], [-1.5, -1.5, 1.0] ]) # Layer 0 (Z=0.0): 2x2 square (from -1 to 1) sq0 = np.array([ [-1.0, -1.0, 0.0], [-1.0, 1.0, 0.0], [ 1.0, 1.0, 0.0], [ 1.0, -1.0, 0.0], [-1.0, -1.0, 0.0] ]) from NodeSlicer.core.slice import SlicePaths slice_paths = SlicePaths([sq0, sq1, sq2], groups=[0, 1, 2]) # tolerance=0.5: # At layer 0 (Z=0.0), support should cover: # (geom_above (3x3) union S_1 (4x4 \ 3x3)) \ geom_0 = 4x4 \ 2x2 # Buffered geom_0_sub is 2.5x2.5 (radius 0.5 outward from 2x2). # So output_geom = 4x4 \ 2.5x2.5. # This is a single ring with area: 16 - 6.25 = 9.75. # It must NOT contain gaps or be fragmented into multiple nested rings. overhang = slice_paths.get_overhang_mask(tolerance=0.5) # Support should be generated on layer 1 and layer 0. # Let's group outputs by Z. layer_paths = {} for path in overhang: z = path[0, 2] layer_paths.setdefault(z, []).append(path) self.assertIn(0.0, layer_paths) self.assertIn(1.0, layer_paths) # At Z=0.0 (layer 0): # We expect exactly 2 paths (exterior of 4x4 and interior hole of 2.5x2.5). # If there is fragmentation, there will be more paths. paths_z0 = layer_paths[0.0] self.assertEqual(len(paths_z0), 2) from shapely.geometry import Polygon # The exterior polygon and the interior hole p0 = Polygon(paths_z0[0][:, :2]) p1 = Polygon(paths_z0[1][:, :2]) areas = sorted([p0.area, p1.area]) # Outer area is 16, inner hole area is approx 8.784 due to rounded corners of the buffer self.assertAlmostEqual(areas[0], 8.784, places=2) self.assertAlmostEqual(areas[1], 16.0, places=2) def test_pather_retraction_lift(self): from NodeSlicer.core.slice import Pather path1 = np.array([ [10.0, 10.0, 0.2], [20.0, 10.0, 0.2] ]) path2 = np.array([ [30.0, 30.0, 0.2], [40.0, 30.0, 0.2] ]) # Test default (no retraction or lift) pather = Pather([path1, path2]) pather.retraction = 0.0 pather.lift = 0.0 gcode = pather.to_gcode(include_header=False) self.assertNotIn("E-", gcode) self.assertNotIn("Z0.700", gcode) # no lift # Test with retraction and lift (default retraction feedrate uses default feedrate 1200) pather = Pather([path1, path2]) pather.retraction = 1.2 pather.lift = 0.5 gcode = pather.to_gcode(include_header=False) # Retraction should be present before travel using default feedrate 1200.0 self.assertIn("G1 E-1.20000 F1200.000", gcode) # Lift should be present: 0.2 (last_nozzle_z) + 0.5 (lift) = 0.7 self.assertIn("G0 Z0.700 F720", gcode) # Un-lift back to 0.2 should be present self.assertIn("G0 Z0.200 F720", gcode) # Un-retraction should be present self.assertIn("G1 E1.20000 F1200.000", gcode) # Test with explicit retraction_feedrate pather = Pather([path1, path2]) pather.retraction = 1.2 pather.retraction_feedrate = 1800.0 gcode_rf = pather.to_gcode(include_header=False) self.assertIn("G1 E-1.20000 F1800.000", gcode_rf) self.assertIn("G1 E1.20000 F1800.000", gcode_rf) # Test with lift only (no retraction) pather = Pather([path1, path2]) pather.retraction = 0.0 pather.lift = 0.5 gcode = pather.to_gcode(include_header=False) self.assertNotIn("E-", gcode) # Lift should be present: 0.2 (last_nozzle_z) + 0.5 (lift) = 0.7 self.assertIn("G0 Z0.700 F720", gcode) # Un-lift back to 0.2 should be present self.assertIn("G0 Z0.200 F720", gcode) # Test with unparsable retraction and lift fields pather = Pather([path1, path2]) pather.retraction = "invalid" pather.lift = None gcode = pather.to_gcode(include_header=False) self.assertNotIn("E-", gcode) self.assertNotIn("Z0.700", gcode) def test_pather_travel_feedrate(self): from NodeSlicer.core.slice import Pather path1 = np.array([ [10.0, 10.0, 0.2], [20.0, 10.0, 0.2] ]) path2 = np.array([ [30.0, 30.0, 0.2], [40.0, 30.0, 0.2] ]) pather = Pather([path1, path2]) pather.feedrate = 1200.0 pather.travel_feedrate = 3000.0 pather.bed_center_x = 25.0 pather.bed_center_y = 20.0 # Verify travel feedrate is in the header comments gcode_with_header = pather.to_gcode(include_header=True) self.assertIn(";DEFAULT_TRAVEL_FEEDRATE=3000.0", gcode_with_header) self.assertIn(";DEFAULT_FEEDRATE=1200.0", gcode_with_header) # Verify G0 travel moves use the travel_feedrate gcode = pather.to_gcode(include_header=False) # The first move should travel to the start of path1 at travel_feedrate self.assertIn("G0 X10.000 Y10.000 Z0.200 F3000.0", gcode) # The transition travel move to the start of path2 should also use travel_feedrate self.assertIn("G0 X30.000 Y30.000 F3000.0", gcode) def test_pather_layer_metadata(self): import math from NodeSlicer.core.slice import Pather, SlicePaths path1 = np.array([ [10.0, 10.0, 0.2], [20.0, 10.0, 0.2] ]) slices = SlicePaths([path1]) slices.layer_height = 0.25 pather = Pather(slices) pather.layer_height = getattr(slices, "layer_height") pather.extrusion_rate = 0.035 expected_width = (0.035 * math.pi * (1.75 / 2.0) ** 2) / 0.25 gcode = pather.to_gcode(include_header=True) self.assertIn(";DEFAULT_LAYER_HEIGHT=0.250000", gcode) self.assertIn(f";DEFAULT_LAYER_WIDTH={expected_width:.6f}", gcode) def test_first_layer_height(self): from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_SliceShells from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject, SlicingCollection # Create a simple box mesh [8, 4, 3] from Z=0 to Z=1 vertices = np.array([ [0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 10.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 1.0], [10.0, 0.0, 1.0], [10.0, 10.0, 1.0], [0.0, 10.0, 1.0], ]) faces = [ [0, 1, 2], [0, 2, 3], [4, 6, 5], [4, 7, 6], [0, 4, 5], [0, 5, 1], [1, 5, 6], [1, 6, 2], [2, 6, 7], [2, 7, 3], [3, 7, 4], [3, 4, 0] ] triangles = np.zeros((len(faces), 4, 3)) for i, face in enumerate(faces): triangles[i, 0] = vertices[face[0]] triangles[i, 1] = vertices[face[1]] triangles[i, 2] = vertices[face[2]] sobj = SlicingObject() sobj.mesh = triangles scoll = SlicingCollection([], "test") scoll.objects = [sobj] node = SLICENODE_SliceShells() # Test without inputs (simulating missing socket resolution) node.resolve = lambda name: scoll if name == "Mesh" else (0.2 if name == "Layer Height" else (0.3 if name == "First Layer Height" else None)) slices = node.evaluate_output("Paths") self.assertTrue(len(slices) > 0) # First slice Z should be min_z + fl_height = 0.0 + 0.3 = 0.3 self.assertAlmostEqual(slices[0][0, 2], 0.3) self.assertEqual(getattr(slices, "first_layer_height", None), 0.3) # First layer extrusion rates should be multiplied by 0.3 / 0.2 = 1.5 np.testing.assert_allclose(slices.extrusion_rate[0], 1.5) def test_slicepaths_attribute_conservation(self): from NodeSlicer.core.slice import SlicePaths path1 = np.array([ [0.0, 0.0, 0.2], [10.0, 0.0, 0.2], [10.0, 10.0, 0.2], [0.0, 10.0, 0.2], [0.0, 0.0, 0.2] ]) sp = SlicePaths([path1], groups=[0], extrusion_rate=[np.array([1.5, 1.5, 1.5, 1.5, 1.5])]) sp.layer_height = 0.2 sp.first_layer_height = 0.3 sp.custom_meta = "test_value" # Test offset off = sp.offset(np.array([1.0, 1.0, 0.0])) self.assertEqual(getattr(off, "layer_height", None), 0.2) self.assertEqual(getattr(off, "first_layer_height", None), 0.3) self.assertEqual(getattr(off, "custom_meta", None), "test_value") np.testing.assert_allclose(off.extrusion_rate[0], 1.5) # Test simplify simp = sp.simplify(0.05) self.assertEqual(getattr(simp, "layer_height", None), 0.2) self.assertEqual(getattr(simp, "first_layer_height", None), 0.3) self.assertEqual(getattr(simp, "custom_meta", None), "test_value") self.assertEqual(len(simp.extrusion_rate[0]), len(simp[0])) np.testing.assert_allclose(simp.extrusion_rate[0], 1.5) # Test buffer buf = sp.buffer(0.5) self.assertEqual(getattr(buf, "layer_height", None), 0.2) self.assertEqual(getattr(buf, "first_layer_height", None), 0.3) self.assertEqual(getattr(buf, "custom_meta", None), "test_value") self.assertTrue(len(buf) > 0) np.testing.assert_allclose(buf.extrusion_rate[0], 1.5) # Test buffer_multiple buf_mult = sp.buffer_multiple(0.2, count=2, width_step=0.2) self.assertEqual(getattr(buf_mult, "layer_height", None), 0.2) self.assertEqual(getattr(buf_mult, "first_layer_height", None), 0.3) self.assertEqual(getattr(buf_mult, "custom_meta", None), "test_value") self.assertTrue(len(buf_mult) >= 2) for ext in buf_mult.extrusion_rate: np.testing.assert_allclose(ext, 1.5) # Test zip_paths & extend_paths sp2 = SlicePaths([path1], groups=[1], extrusion_rate=[np.array([1.0, 1.0, 1.0, 1.0, 1.0])]) zipped = SlicePaths.zip_paths([sp, sp2]) self.assertEqual(getattr(zipped, "layer_height", None), 0.2) self.assertEqual(getattr(zipped, "first_layer_height", None), 0.3) self.assertEqual(getattr(zipped, "custom_meta", None), "test_value") extended = SlicePaths.extend_paths([sp, sp2]) self.assertEqual(getattr(extended, "layer_height", None), 0.2) self.assertEqual(getattr(extended, "first_layer_height", None), 0.3) self.assertEqual(getattr(extended, "custom_meta", None), "test_value") def test_align_seam_sharpest_rectangle(self): from NodeSlicer.core.slice import align_path_seam_sharpest # Rectangle where index 0 is a midpoint on a flat edge (5, 0, 0) path = np.array([ [5.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 10.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 0.0], [5.0, 0.0, 0.0] ]) aligned = align_path_seam_sharpest(path) self.assertEqual(len(aligned), len(path)) self.assertTrue(np.allclose(aligned[0], aligned[-1])) # Start point should be moved from flat edge (5,0,0) to one of the 90-degree corners self.assertFalse(np.allclose(aligned[0, :2], [5.0, 0.0])) corners = np.array([[10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]]) is_corner = any(np.allclose(aligned[0, :2], c) for c in corners) self.assertTrue(is_corner) def test_align_seam_sharpest_triangle(self): from NodeSlicer.core.slice import align_path_seam_sharpest # Right triangle with acute 30-degree vertex at (10, 0) # Vertices: 90 deg at (0,0), 60 deg at (0, 5.7735), 30 deg at (10,0) # Start at 90 deg vertex (0,0) path = np.array([ [0.0, 0.0, 0.0], [0.0, 5.7735, 0.0], [10.0, 0.0, 0.0], [0.0, 0.0, 0.0] ]) aligned = align_path_seam_sharpest(path) self.assertTrue(np.allclose(aligned[0], aligned[-1])) # Sharpest angle is 30 degrees at (10.0, 0.0) np.testing.assert_allclose(aligned[0, :2], [10.0, 0.0], atol=1e-3) def test_slicepaths_align_seams(self): from NodeSlicer.core.slice import SlicePaths path = np.array([ [5.0, 0.0, 0.2], [10.0, 0.0, 0.2], [10.0, 10.0, 0.2], [0.0, 10.0, 0.2], [0.0, 0.0, 0.2], [5.0, 0.0, 0.2] ]) ext_rates = np.array([1.0, 1.1, 1.2, 1.3, 1.4, 1.0]) sp = SlicePaths([path], groups=[0], extrusion_rate=[ext_rates]) sp.layer_height = 0.2 aligned_sp = sp.align_seams(mode="sharpest") self.assertEqual(getattr(aligned_sp, "layer_height", None), 0.2) self.assertEqual(len(aligned_sp[0]), len(path)) self.assertTrue(np.allclose(aligned_sp[0][0], aligned_sp[0][-1])) self.assertFalse(np.allclose(aligned_sp[0][0, :2], [5.0, 0.0])) self.assertEqual(len(aligned_sp.extrusion_rate[0]), len(aligned_sp[0])) def test_align_seam_sharpest_concave_l_shape(self): from NodeSlicer.core.slice import align_path_seam_sharpest # L-shaped polygon in CCW order: # Outer convex corners: (0,0), (10,0), (10,5), (5,10), (0,10) # Inner concave corner: (5,5) path = np.array([ [0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 5.0, 0.0], [5.0, 5.0, 0.0], [5.0, 10.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 0.0] ]) aligned = align_path_seam_sharpest(path) self.assertEqual(len(aligned), len(path)) self.assertTrue(np.allclose(aligned[0], aligned[-1])) # Seam should be placed at the concave inner corner (5.0, 5.0) np.testing.assert_allclose(aligned[0, :2], [5.0, 5.0]) def test_intrinsics_retraction_feedrate(self): from NodeSlicer.core.nodeslicer.nodes.base import SliceIntrinsics from NodeSlicer.core.nodeslicer.nodes.inputs import SLICENODE_InputIntrinsics # Test SliceIntrinsics default and custom retraction feedrate intr = SliceIntrinsics(retraction_feedrate=1800.0) self.assertEqual(intr.retraction_feedrate, 1800.0) intr_default = SliceIntrinsics() self.assertEqual(intr_default.retraction_feedrate, 0.0) # Test Node evaluation node = SLICENODE_InputIntrinsics() values = { "Bed X": 200.0, "Bed Y": 200.0, "Feedrate": 1500.0, "Travel Feedrate": 3000.0, "Extrusion Rate": 0.02, "Retraction": 1.0, "Retraction Feedrate": 1800.0, "Lift": 0.2 } node.resolve = lambda name: values.get(name, 0.0) res_intr = node.evaluate_output("SLICESOCK_Intrinsics") self.assertEqual(res_intr.retraction_feedrate, 1800.0) def test_gcode_name_from_metadata_paths_to_gcode(self): from NodeSlicer.core.slice import SlicePaths, Pather from NodeSlicer.infra.networking.gcode_utils import get_gcode_name_from_metadata # 1. Test SlicePaths with explicit name p1 = np.array([[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]]) sp = SlicePaths([p1], name="GearPart") pather = Pather(sp) gcode = pather.to_gcode(include_header=True) self.assertIn("; objects_info =", gcode) name = get_gcode_name_from_metadata(gcode) self.assertEqual(name, "GearPart") # 2. Test SlicePaths with objects_info (multiple objects) sp_multi = SlicePaths( [p1], objects_info={"objects": [{"name": "PartA", "polygon": []}, {"name": "PartB", "polygon": []}]} ) pather_multi = Pather(sp_multi) gcode_multi = pather_multi.to_gcode(include_header=True) self.assertIn("; objects_info =", gcode_multi) name_multi = get_gcode_name_from_metadata(gcode_multi) self.assertEqual(name_multi, "PartA-PartB") # 3. Test get_gcode_name_from_metadata fallback object comments gcode_fallback = "; Generated Gcode\n; OBJECT_NAME = CustomBracket\nG1 X10 Y10\n" self.assertEqual(get_gcode_name_from_metadata(gcode_fallback), "CustomBracket") def test_separate_mesh_by_range_padding(self): from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_SeparateMeshByRange from NodeSlicer.infra.blender.blender_mesh_capture import SlicingCollection, SlicingObject node = SLICENODE_SeparateMeshByRange() node.end_val = 6 so = SlicingObject() so.name = "CA" so.mesh = np.zeros((4, 4, 3)) so.attributes = { "part_id": { "domain": "FACE", "type": "INT", "values": np.array([1, 1, 2, 2]) } } coll = SlicingCollection.__new__(SlicingCollection) coll.objects = [so] coll.name = "TestColl" coll.transforms = [] node.resolve = lambda name: coll if name == "Mesh" else ("part_id" if name == "Attribute" else None) res_1 = node.evaluate_output("1") self.assertIsNotNone(res_1) self.assertEqual(res_1.objects[0].name, "CA01") if __name__ == '__main__': unittest.main()