619 lines
22 KiB
Python
619 lines
22 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
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.append(str(PROJECT_ROOT.parent))
|
|
|
|
# Define dummy classes to avoid metaclass conflicts with bpy
|
|
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()
|
|
|
|
from NodeSlicer.core.slice import (
|
|
SlicePaths,
|
|
is_closed_path,
|
|
find_path_mesh_intersection,
|
|
reposition_path_seam,
|
|
extract_mesh_triangles,
|
|
)
|
|
from NodeSlicer.infra.blender.blender_mesh_capture import (
|
|
SlicingObject,
|
|
SlicingCollection,
|
|
SlicingGroup,
|
|
)
|
|
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_SeamPlacement
|
|
|
|
|
|
def create_triangle_mesh(v0, v1, v2) -> np.ndarray:
|
|
"""Helper to create a single triangle mesh with shape (1, 4, 3)."""
|
|
e1 = np.array(v1) - np.array(v0)
|
|
e2 = np.array(v2) - np.array(v0)
|
|
n = np.cross(e1, e2)
|
|
norm = np.linalg.norm(n)
|
|
if norm > 0:
|
|
n = n / norm
|
|
tri = np.zeros((1, 4, 3), dtype=np.float64)
|
|
tri[0, 0] = v0
|
|
tri[0, 1] = v1
|
|
tri[0, 2] = v2
|
|
tri[0, 3] = n
|
|
return tri
|
|
|
|
|
|
def create_quad_mesh(p0, p1, p2, p3) -> np.ndarray:
|
|
"""Helper to create a quad mesh (2 triangles) with shape (2, 4, 3)."""
|
|
t1 = create_triangle_mesh(p0, p1, p2)
|
|
t2 = create_triangle_mesh(p0, p2, p3)
|
|
return np.concatenate([t1, t2], axis=0)
|
|
|
|
|
|
class TestSeamPlacement(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
# Base square path at Z=0: from (-10,-10) -> (10,-10) -> (10,10) -> (-10,10) -> (-10,-10)
|
|
self.square_path = np.array([
|
|
[-10.0, -10.0, 0.0],
|
|
[10.0, -10.0, 0.0],
|
|
[10.0, 10.0, 0.0],
|
|
[-10.0, 10.0, 0.0],
|
|
[-10.0, -10.0, 0.0]
|
|
], dtype=np.float64)
|
|
|
|
def test_single_intersection_edge_midpoint(self):
|
|
# Vertical cutting plane through top edge at X=0, Y=10
|
|
# Triangle cutting only the top edge: X in [-0.5, 0.5], Y in [9, 11], Z in [-5, 5]
|
|
cutting_plane = create_quad_mesh(
|
|
[0.0, 5.0, -5.0],
|
|
[0.0, 15.0, -5.0],
|
|
[0.0, 15.0, 5.0],
|
|
[0.0, 5.0, 5.0]
|
|
)
|
|
|
|
sp = SlicePaths([self.square_path])
|
|
res = sp.place_seams(cutting_plane)
|
|
|
|
self.assertEqual(len(res), 1)
|
|
mod_p = res[0]
|
|
|
|
# Should be a valid closed loop
|
|
self.assertTrue(is_closed_path(mod_p))
|
|
# Seam should now be at (0, 10, 0)
|
|
np.testing.assert_allclose(mod_p[0], [0.0, 10.0, 0.0], atol=1e-4)
|
|
np.testing.assert_allclose(mod_p[-1], [0.0, 10.0, 0.0], atol=1e-4)
|
|
|
|
# Total points should have increased by 1 (inserted vertex)
|
|
self.assertEqual(len(mod_p), 6)
|
|
|
|
def test_intersection_snap_to_existing_vertex(self):
|
|
# Cutting plane passing right through corner vertex (10, 10, 0)
|
|
cutting_plane = create_quad_mesh(
|
|
[9.0, 9.0, -5.0],
|
|
[11.0, 11.0, -5.0],
|
|
[11.0, 11.0, 5.0],
|
|
[9.0, 9.0, 5.0]
|
|
)
|
|
|
|
sp = SlicePaths([self.square_path])
|
|
res = sp.place_seams(cutting_plane)
|
|
|
|
self.assertEqual(len(res), 1)
|
|
mod_p = res[0]
|
|
|
|
self.assertTrue(is_closed_path(mod_p))
|
|
np.testing.assert_allclose(mod_p[0], [10.0, 10.0, 0.0], atol=1e-4)
|
|
np.testing.assert_allclose(mod_p[-1], [10.0, 10.0, 0.0], atol=1e-4)
|
|
# Should snap to existing vertex without increasing point count
|
|
self.assertEqual(len(mod_p), 5)
|
|
|
|
def test_no_intersection_preserves_path(self):
|
|
# Guide mesh far away at Z=100
|
|
mesh_far = create_quad_mesh(
|
|
[0.0, 5.0, 95.0],
|
|
[0.0, 15.0, 95.0],
|
|
[0.0, 15.0, 105.0],
|
|
[0.0, 5.0, 105.0]
|
|
)
|
|
|
|
sp = SlicePaths([self.square_path])
|
|
res = sp.place_seams(mesh_far)
|
|
|
|
self.assertEqual(len(res), 1)
|
|
np.testing.assert_allclose(res[0], self.square_path, atol=1e-7)
|
|
|
|
def test_open_path_untouched(self):
|
|
# Open path (3 vertices, not closed)
|
|
open_p = np.array([
|
|
[-10.0, -10.0, 0.0],
|
|
[0.0, 0.0, 0.0],
|
|
[10.0, 10.0, 0.0]
|
|
], dtype=np.float64)
|
|
|
|
cutting_plane = create_quad_mesh(
|
|
[0.0, -5.0, -5.0],
|
|
[0.0, 5.0, -5.0],
|
|
[0.0, 5.0, 5.0],
|
|
[0.0, -5.0, 5.0]
|
|
)
|
|
|
|
sp = SlicePaths([open_p])
|
|
res = sp.place_seams(cutting_plane)
|
|
|
|
self.assertEqual(len(res), 1)
|
|
np.testing.assert_allclose(res[0], open_p, atol=1e-7)
|
|
|
|
def test_holes_handling(self):
|
|
# Outer boundary square: 40x40 from (-20,-20) to (20,20)
|
|
outer_p = np.array([
|
|
[-20.0, -20.0, 0.0],
|
|
[20.0, -20.0, 0.0],
|
|
[20.0, 20.0, 0.0],
|
|
[-20.0, 20.0, 0.0],
|
|
[-20.0, -20.0, 0.0]
|
|
], dtype=np.float64)
|
|
|
|
# Hole 1 in top-left: square 6x6 from (-15, 5) to (-9, 11) (clockwise)
|
|
hole1_p = np.array([
|
|
[-15.0, 11.0, 0.0],
|
|
[-9.0, 11.0, 0.0],
|
|
[-9.0, 5.0, 0.0],
|
|
[-15.0, 5.0, 0.0],
|
|
[-15.0, 11.0, 0.0]
|
|
], dtype=np.float64)
|
|
|
|
# Hole 2 in bottom-right: square 6x6 from (9, -15) to (15, -9)
|
|
hole2_p = np.array([
|
|
[9.0, -9.0, 0.0],
|
|
[15.0, -9.0, 0.0],
|
|
[15.0, -15.0, 0.0],
|
|
[9.0, -15.0, 0.0],
|
|
[9.0, -9.0, 0.0]
|
|
], dtype=np.float64)
|
|
|
|
# Cutting plane cutting through outer top edge (at X=-12, Y=20) and Hole 1 (at X=-12, Y=11 and Y=5)
|
|
# Plane is at X = -12, spanning Y in [0, 25], Z in [-5, 5]
|
|
cutting_plane = create_quad_mesh(
|
|
[-12.0, 0.0, -5.0],
|
|
[-12.0, 25.0, -5.0],
|
|
[-12.0, 25.0, 5.0],
|
|
[-12.0, 0.0, 5.0]
|
|
)
|
|
|
|
sp = SlicePaths([outer_p, hole1_p, hole2_p])
|
|
res = sp.place_seams(cutting_plane)
|
|
|
|
self.assertEqual(len(res), 3)
|
|
|
|
# 1. Outer boundary seam should be moved to (-12, 20, 0)
|
|
np.testing.assert_allclose(res[0][0], [-12.0, 20.0, 0.0], atol=1e-4)
|
|
np.testing.assert_allclose(res[0][-1], [-12.0, 20.0, 0.0], atol=1e-4)
|
|
self.assertTrue(is_closed_path(res[0]))
|
|
|
|
# 2. Hole 1 seam should be moved to (-12, 11, 0) (or intersection on hole1)
|
|
self.assertTrue(is_closed_path(res[1]))
|
|
np.testing.assert_allclose(res[1][0, 0], -12.0, atol=1e-4)
|
|
np.testing.assert_allclose(res[1][-1, 0], -12.0, atol=1e-4)
|
|
|
|
# 3. Hole 2 does NOT intersect the plane at X=-12, so its seam stays unchanged!
|
|
np.testing.assert_allclose(res[2], hole2_p, atol=1e-7)
|
|
|
|
def test_mesh_input_types_agnostic(self):
|
|
# Test SlicingObject, SlicingCollection, SlicingGroup, and list
|
|
tri_data = create_quad_mesh(
|
|
[0.0, 5.0, -5.0],
|
|
[0.0, 15.0, -5.0],
|
|
[0.0, 15.0, 5.0],
|
|
[0.0, 5.0, 5.0]
|
|
)
|
|
|
|
so = SlicingObject()
|
|
so.mesh = tri_data
|
|
|
|
sc = SlicingCollection([], "test_coll")
|
|
sc.objects = [so]
|
|
|
|
sg = SlicingGroup([])
|
|
sg.collections["default"] = sc
|
|
|
|
sp = SlicePaths([self.square_path])
|
|
|
|
# 1. SlicingObject
|
|
res_so = sp.place_seams(so)
|
|
np.testing.assert_allclose(res_so[0][0], [0.0, 10.0, 0.0], atol=1e-4)
|
|
|
|
# 2. SlicingCollection
|
|
res_sc = sp.place_seams(sc)
|
|
np.testing.assert_allclose(res_sc[0][0], [0.0, 10.0, 0.0], atol=1e-4)
|
|
|
|
# 3. SlicingGroup
|
|
res_sg = sp.place_seams(sg)
|
|
np.testing.assert_allclose(res_sg[0][0], [0.0, 10.0, 0.0], atol=1e-4)
|
|
|
|
# 4. List of objects
|
|
res_list = sp.place_seams([so, sc])
|
|
np.testing.assert_allclose(res_list[0][0], [0.0, 10.0, 0.0], atol=1e-4)
|
|
|
|
# 5. None / Empty
|
|
res_none = sp.place_seams(None)
|
|
np.testing.assert_allclose(res_none[0], self.square_path, atol=1e-7)
|
|
|
|
def test_attribute_rotation_and_interpolation(self):
|
|
# Square path with non-uniform extrusion rate and feedrate
|
|
ext = np.array([1.0, 2.0, 3.0, 4.0, 1.0], dtype=np.float64)
|
|
feed = np.array([100.0, 200.0, 300.0, 400.0, 100.0], dtype=np.float64)
|
|
extra = {"temp": [np.array([210.0, 215.0, 220.0, 225.0, 210.0], dtype=np.float64)]}
|
|
|
|
sp = SlicePaths([self.square_path], extrusion_rate=[ext], feedrate=[feed], extra_attributes=extra)
|
|
|
|
# Plane cutting segment 2 (between (10, 10) and (-10, 10)) at midpoint X=0, Y=10 (t=0.5)
|
|
cutting_plane = create_quad_mesh(
|
|
[0.0, 5.0, -5.0],
|
|
[0.0, 15.0, -5.0],
|
|
[0.0, 15.0, 5.0],
|
|
[0.0, 5.0, 5.0]
|
|
)
|
|
|
|
res = sp.place_seams(cutting_plane)
|
|
self.assertEqual(len(res), 1)
|
|
|
|
# Interpolated value at t=0.5 on segment 2:
|
|
# segment 2 is between vertex 2 (ext=3.0) and vertex 3 (ext=4.0)
|
|
# Expected interpolated ext at new seam: 3.5
|
|
expected_seam_ext = 3.5
|
|
self.assertAlmostEqual(res.extrusion_rate[0][0], expected_seam_ext, places=3)
|
|
self.assertAlmostEqual(res.extrusion_rate[0][-1], expected_seam_ext, places=3)
|
|
|
|
# Feedrate at t=0.5: between 300.0 and 400.0 -> 350.0
|
|
self.assertAlmostEqual(res.feedrate[0][0], 350.0, places=3)
|
|
self.assertAlmostEqual(res.feedrate[0][-1], 350.0, places=3)
|
|
|
|
# Extra attribute at t=0.5: between 220.0 and 225.0 -> 222.5
|
|
self.assertAlmostEqual(res.extra_attributes["temp"][0][0], 222.5, places=3)
|
|
self.assertAlmostEqual(res.extra_attributes["temp"][0][-1], 222.5, places=3)
|
|
|
|
def test_seam_placement_modes(self):
|
|
# Plane that cuts square path at two places:
|
|
# 1. Top edge at (0, 10, 0)
|
|
# 2. Bottom edge at (0, -10, 0)
|
|
full_plane = create_quad_mesh(
|
|
[0.0, -15.0, -5.0],
|
|
[0.0, 15.0, -5.0],
|
|
[0.0, 15.0, 5.0],
|
|
[0.0, -15.0, 5.0]
|
|
)
|
|
|
|
# Original square starts at (-10, -10, 0) (closer to (0, -10, 0))
|
|
sp = SlicePaths([self.square_path])
|
|
|
|
# Mode: CLOSEST -> should pick (0, -10, 0) because original seam is at (-10, -10, 0)
|
|
res_closest = sp.place_seams(full_plane, mode="closest")
|
|
np.testing.assert_allclose(res_closest[0][0], [0.0, -10.0, 0.0], atol=1e-4)
|
|
|
|
# Mode: FIRST -> segment 0 goes from (-10, -10) to (10, -10), so first intersection is (0, -10, 0)
|
|
res_first = sp.place_seams(full_plane, mode="first")
|
|
np.testing.assert_allclose(res_first[0][0], [0.0, -10.0, 0.0], atol=1e-4)
|
|
|
|
def test_seam_placement_node_execution(self):
|
|
# Test SLICENODE_SeamPlacement evaluate_output method
|
|
node = SLICENODE_SeamPlacement()
|
|
node.mode = 'CLOSEST'
|
|
|
|
cutting_plane = create_quad_mesh(
|
|
[0.0, 5.0, -5.0],
|
|
[0.0, 15.0, -5.0],
|
|
[0.0, 15.0, 5.0],
|
|
[0.0, 5.0, 5.0]
|
|
)
|
|
|
|
sp = SlicePaths([self.square_path])
|
|
so = SlicingObject()
|
|
so.mesh = cutting_plane
|
|
|
|
# Mock resolve method
|
|
def mock_resolve(socket_name):
|
|
if socket_name == "Paths":
|
|
return sp
|
|
elif socket_name == "Mesh":
|
|
return so
|
|
return None
|
|
|
|
node.resolve = mock_resolve
|
|
out = node.evaluate_output("Paths")
|
|
|
|
self.assertIsInstance(out, SlicePaths)
|
|
self.assertEqual(len(out), 1)
|
|
np.testing.assert_allclose(out[0][0], [0.0, 10.0, 0.0], atol=1e-4)
|
|
|
|
def test_acquire_transforms(self):
|
|
# Shifted square path by +50 in X and +30 in Y
|
|
shifted_square = self.square_path + np.array([50.0, 30.0, 0.0])
|
|
transforms = [{
|
|
"type": "transform",
|
|
"translation": (50.0, 30.0, 0.0),
|
|
"rotation": (0.0, 0.0, 0.0),
|
|
"scale": (1.0, 1.0, 1.0)
|
|
}]
|
|
|
|
sp = SlicePaths([shifted_square], transforms=transforms)
|
|
|
|
# Cutting plane at original untransformed location at X=0, Y=10
|
|
cutting_plane = create_quad_mesh(
|
|
[0.0, 5.0, -5.0],
|
|
[0.0, 15.0, -5.0],
|
|
[0.0, 15.0, 5.0],
|
|
[0.0, 5.0, 5.0]
|
|
)
|
|
|
|
# 1. With acquire_transforms=True (default):
|
|
# The cutting plane should be transformed by +50 X and +30 Y, intersecting at (50, 40, 0)
|
|
res_acquired = sp.place_seams(cutting_plane, acquire_transforms=True)
|
|
self.assertEqual(len(res_acquired), 1)
|
|
np.testing.assert_allclose(res_acquired[0][0], [50.0, 40.0, 0.0], atol=1e-4)
|
|
np.testing.assert_allclose(res_acquired[0][-1], [50.0, 40.0, 0.0], atol=1e-4)
|
|
|
|
# 2. With acquire_transforms=False:
|
|
# The cutting plane remains at (0, 10, 0) and does not intersect the path at (50, 30, 0)
|
|
res_no_acquire = sp.place_seams(cutting_plane, acquire_transforms=False)
|
|
self.assertEqual(len(res_no_acquire), 1)
|
|
np.testing.assert_allclose(res_no_acquire[0], shifted_square, atol=1e-7)
|
|
|
|
def test_node_acquire_transforms_checkbox(self):
|
|
node = SLICENODE_SeamPlacement()
|
|
self.assertTrue(node.acquire_transforms)
|
|
|
|
shifted_square = self.square_path + np.array([50.0, 30.0, 0.0])
|
|
transforms = [{
|
|
"type": "transform",
|
|
"translation": (50.0, 30.0, 0.0),
|
|
"rotation": (0.0, 0.0, 0.0),
|
|
"scale": (1.0, 1.0, 1.0)
|
|
}]
|
|
sp = SlicePaths([shifted_square], transforms=transforms)
|
|
|
|
cutting_plane = create_quad_mesh(
|
|
[0.0, 5.0, -5.0],
|
|
[0.0, 15.0, -5.0],
|
|
[0.0, 15.0, 5.0],
|
|
[0.0, 5.0, 5.0]
|
|
)
|
|
so = SlicingObject()
|
|
so.mesh = cutting_plane
|
|
|
|
def mock_resolve(socket_name):
|
|
if socket_name == "Paths":
|
|
return sp
|
|
elif socket_name == "Mesh":
|
|
return so
|
|
return None
|
|
|
|
node.resolve = mock_resolve
|
|
|
|
# When checkbox is True
|
|
node.acquire_transforms = True
|
|
out_true = node.evaluate_output("Paths")
|
|
np.testing.assert_allclose(out_true[0][0], [50.0, 40.0, 0.0], atol=1e-4)
|
|
|
|
# When checkbox is False
|
|
node.acquire_transforms = False
|
|
out_false = node.evaluate_output("Paths")
|
|
np.testing.assert_allclose(out_false[0], shifted_square, atol=1e-7)
|
|
|
|
def test_large_dataset_performance(self):
|
|
# 500 layers of complex circular paths (100 vertices per circle)
|
|
num_layers = 500
|
|
angles = np.linspace(0, 2 * np.pi, 100)
|
|
circle_2d = np.stack([10.0 * np.cos(angles), 10.0 * np.sin(angles)], axis=-1)
|
|
|
|
paths = []
|
|
for i in range(num_layers):
|
|
z = i * 0.2
|
|
p = np.empty((len(circle_2d), 3), dtype=np.float64)
|
|
p[:, :2] = circle_2d
|
|
p[:, 2] = z
|
|
paths.append(p)
|
|
|
|
sp = SlicePaths(paths)
|
|
|
|
# Mesh with 1,000 triangles spanning Z in [0, 100]
|
|
# Cutting quad at X=0, Y in [5, 15], Z in [-5, 105]
|
|
cutting_plane = create_quad_mesh(
|
|
[0.0, 5.0, -5.0],
|
|
[0.0, 15.0, -5.0],
|
|
[0.0, 15.0, 105.0],
|
|
[0.0, 5.0, 105.0]
|
|
)
|
|
|
|
import time
|
|
start_time = time.perf_counter()
|
|
res = sp.place_seams(cutting_plane)
|
|
duration = time.perf_counter() - start_time
|
|
|
|
self.assertEqual(len(res), num_layers)
|
|
for i in range(num_layers):
|
|
self.assertTrue(is_closed_path(res[i]))
|
|
# Should have seam on positive Y axis at X=0
|
|
np.testing.assert_allclose(res[i][0, 0], 0.0, atol=1e-3)
|
|
self.assertGreater(res[i][0, 1], 9.0)
|
|
|
|
# 500 layers must complete in well under 1 second (typically < 50ms)
|
|
self.assertLess(duration, 1.0, f"Execution took too long: {duration:.3f}s")
|
|
|
|
def test_complex_high_poly_mesh_performance(self):
|
|
# 500 layers, each with an outer contour and an inner hole (1000 paths total)
|
|
num_layers = 500
|
|
angles = np.linspace(0, 2 * np.pi, 80)
|
|
outer_2d = np.stack([20.0 * np.cos(angles), 20.0 * np.sin(angles)], axis=-1)
|
|
inner_2d = np.stack([10.0 * np.cos(angles[::-1]), 10.0 * np.sin(angles[::-1])], axis=-1)
|
|
|
|
paths = []
|
|
for i in range(num_layers):
|
|
z = i * 0.2
|
|
po = np.empty((len(outer_2d), 3), dtype=np.float64)
|
|
po[:, :2] = outer_2d
|
|
po[:, 2] = z
|
|
paths.append(po)
|
|
|
|
pi = np.empty((len(inner_2d), 3), dtype=np.float64)
|
|
pi[:, :2] = inner_2d
|
|
pi[:, 2] = z
|
|
paths.append(pi)
|
|
|
|
sp = SlicePaths(paths)
|
|
|
|
# Generate a high-poly mesh of 25,000 quads (50,000 triangles)
|
|
# spanning Z in [-10, 110]
|
|
n_quads = 25000
|
|
z_grid = np.linspace(-10, 110, n_quads + 1)
|
|
tris = np.zeros((n_quads * 2, 4, 3), dtype=np.float64)
|
|
for q in range(n_quads):
|
|
z0 = z_grid[q]
|
|
z1 = z_grid[q + 1]
|
|
# Quad cutting at X=0, Y in [5, 25]
|
|
p0 = np.array([0.0, 5.0, z0])
|
|
p1 = np.array([0.0, 25.0, z0])
|
|
p2 = np.array([0.0, 25.0, z1])
|
|
p3 = np.array([0.0, 5.0, z1])
|
|
|
|
tris[q * 2, 0] = p0
|
|
tris[q * 2, 1] = p1
|
|
tris[q * 2, 2] = p2
|
|
|
|
tris[q * 2 + 1, 0] = p0
|
|
tris[q * 2 + 1, 1] = p2
|
|
tris[q * 2 + 1, 2] = p3
|
|
|
|
import time
|
|
start_time = time.perf_counter()
|
|
res = sp.place_seams(tris)
|
|
duration = time.perf_counter() - start_time
|
|
|
|
self.assertEqual(len(res), num_layers * 2)
|
|
# Even with 50,000 triangles and 1,000 paths across 500 layers,
|
|
# layer batching must finish in under 0.5s
|
|
self.assertLess(duration, 0.5, f"High-poly seam placement took {duration:.3f}s (must be < 0.5s)")
|
|
|
|
def test_acquire_transforms_with_rotate_path_center(self):
|
|
# Square centered at (100, 100): from (90, 90) to (110, 110)
|
|
center = np.array([100.0, 100.0, 0.0])
|
|
square_offset = np.array([
|
|
[90.0, 90.0, 0.0],
|
|
[110.0, 90.0, 0.0],
|
|
[110.0, 110.0, 0.0],
|
|
[90.0, 110.0, 0.0],
|
|
[90.0, 90.0, 0.0]
|
|
], dtype=np.float64)
|
|
|
|
sp = SlicePaths([square_offset])
|
|
|
|
# Cutting plane cutting top edge at (100, 110, 0)
|
|
cutting_plane = create_quad_mesh(
|
|
[100.0, 105.0, -5.0],
|
|
[100.0, 115.0, -5.0],
|
|
[100.0, 115.0, 5.0],
|
|
[100.0, 105.0, 5.0]
|
|
)
|
|
|
|
# Rotate path by 90 degrees around center (100, 100, 0)
|
|
# Top edge (100, 110) -> rotates by +90 deg (CCW) around (100, 100) to (90, 100)
|
|
rotated_sp = sp.rotate(90.0, center=center)
|
|
|
|
# When acquire_transforms=True, the cutting plane must rotate around the SAME center (100, 100)
|
|
# So the cutting plane at (100, 110) rotates to (90, 100) and cuts the rotated path at (90, 100, 0)
|
|
res = rotated_sp.place_seams(cutting_plane, acquire_transforms=True)
|
|
self.assertEqual(len(res), 1)
|
|
self.assertTrue(is_closed_path(res[0]))
|
|
np.testing.assert_allclose(res[0][0], [90.0, 100.0, 0.0], atol=1e-4)
|
|
np.testing.assert_allclose(res[0][-1], [90.0, 100.0, 0.0], atol=1e-4)
|
|
|
|
def test_acquire_transforms_when_mesh_already_transformed(self):
|
|
# Transformed path and transformed SlicingCollection
|
|
# Both already have transforms: translation +142.93 in Z, rotation -90 deg around Y
|
|
trans_record = [{
|
|
"type": "transform",
|
|
"translation": (0.0, 0.0, 142.93),
|
|
"rotation": (0.0, -np.pi / 2.0, 0.0),
|
|
"scale": (1.0, 1.0, 1.0)
|
|
}]
|
|
|
|
# Create original path and cutting plane
|
|
orig_p = np.array([
|
|
[-10.0, -10.0, 0.0],
|
|
[10.0, -10.0, 0.0],
|
|
[10.0, 10.0, 0.0],
|
|
[-10.0, 10.0, 0.0],
|
|
[-10.0, -10.0, 0.0]
|
|
], dtype=np.float64)
|
|
|
|
cutting_plane = create_quad_mesh(
|
|
[0.0, 5.0, -5.0],
|
|
[0.0, 15.0, -5.0],
|
|
[0.0, 15.0, 5.0],
|
|
[0.0, 5.0, 5.0]
|
|
)
|
|
|
|
from NodeSlicer.core.pdf_exporter.geometry import transform_slice_paths
|
|
from NodeSlicer.core.slice import apply_transforms_to_triangles
|
|
|
|
# Transform both the paths and the cutting mesh
|
|
sp = transform_slice_paths(SlicePaths([orig_p]), (0.0, 0.0, 142.93), (0.0, -np.pi / 2.0, 0.0), (1.0, 1.0, 1.0))
|
|
sp.transforms = [dict(t) for t in trans_record]
|
|
|
|
transformed_tris = apply_transforms_to_triangles(cutting_plane, trans_record)
|
|
so = SlicingObject()
|
|
so.mesh = transformed_tris
|
|
so.transforms = [dict(t) for t in trans_record]
|
|
|
|
# When acquire_transforms=True is passed with a mesh that is ALREADY transformed,
|
|
# it must NOT double-transform the mesh!
|
|
res = sp.place_seams(so, acquire_transforms=True)
|
|
self.assertEqual(len(res), 1)
|
|
self.assertTrue(is_closed_path(res[0]))
|
|
|
|
# The seam must be placed at the intersection (not left unchanged)
|
|
# Transformed seam should NOT equal the unmodified transformed start point
|
|
self.assertFalse(np.allclose(res[0][0], sp[0][0]))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|
|
|
|
|
|
|
|
|