538 lines
21 KiB
Python
538 lines
21 KiB
Python
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
|
|
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
|
|
self.assertIn("G1 E-1.20000 F1800", 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 F1800", gcode)
|
|
|
|
# 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)
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|