holes
This commit is contained in:
parent
bcf9334d78
commit
8d5fb68aaa
9 changed files with 329 additions and 10 deletions
|
|
@ -60,7 +60,7 @@ _MATH_OPS: dict[str, Any] = {
|
|||
'ADD': lambda a, b, t: a + b,
|
||||
'SUBTRACT': lambda a, b, t: a - b,
|
||||
'MULTIPLY': lambda a, b, t: a * b,
|
||||
'DIVIDE': lambda a, b, t: a // b if b != 0 else 0,
|
||||
'DIVIDE': lambda a, b, t: a / b if b != 0 else 0,
|
||||
'ABS': lambda a, b, t: abs(a),
|
||||
'NEGATE': lambda a, b, t: -a,
|
||||
'POWER': lambda a, b, t: t(a ** b) if not (a == 0 and b < 0) else 0,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class SLICENODE_CollectionObjects(SliceNode):
|
|||
return
|
||||
|
||||
objs = [obj for obj in col.objects if getattr(obj, "type", "") == 'MESH']
|
||||
expected_names = [obj.name for obj in objs]
|
||||
expected_names = sorted([obj.name for obj in objs])
|
||||
|
||||
# Remove outputs that are no longer in the collection
|
||||
to_remove = [o for o in self.outputs if o.name not in expected_names]
|
||||
|
|
@ -79,6 +79,13 @@ class SLICENODE_CollectionObjects(SliceNode):
|
|||
if name not in self.outputs:
|
||||
self.add_output("NodeSocketObject", name, identifier=name)
|
||||
|
||||
# Re-order to match sorted alphabetical order
|
||||
if hasattr(self.outputs, "find") and hasattr(self.outputs, "move"):
|
||||
for i, name in enumerate(expected_names):
|
||||
current_index = self.outputs.find(name)
|
||||
if current_index != -1 and current_index != i:
|
||||
self.outputs.move(current_index, i)
|
||||
|
||||
def evaluate_output(self, socket_name: str, **kwargs) -> Any:
|
||||
if self.collection and socket_name in bpy.data.objects:
|
||||
return bpy.data.objects[socket_name]
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ class SLICENODE_GetPaths(SliceNode):
|
|||
class SLICENODE_SliceShells(SliceNode):
|
||||
bl_label = "Slice Shells"
|
||||
|
||||
ignore_holes: bool = NfBoolProperty(
|
||||
name="Ignore Holes",
|
||||
description="Ignore interior hole boundaries and generate only outer shells",
|
||||
default=False
|
||||
)
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Mesh")
|
||||
self.add_input("NodeSocketFloat", "Layer Height").value = 0.2
|
||||
|
|
@ -48,6 +54,11 @@ class SLICENODE_SliceShells(SliceNode):
|
|||
self.add_input("NodeSocketRotation", "Rotation")
|
||||
self.add_output("SLICESOCK_Paths")
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
if hasattr(self, "ignore_holes"):
|
||||
layout.prop(self, "ignore_holes")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||
if socket_name != "Paths": return SlicePaths()
|
||||
|
||||
|
|
@ -69,6 +80,15 @@ class SLICENODE_SliceShells(SliceNode):
|
|||
|
||||
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
|
||||
|
||||
ignore_holes: bool = bool(getattr(self, "ignore_holes", False))
|
||||
if hasattr(self, "inputs") and "Ignore Holes" in self.inputs:
|
||||
try:
|
||||
val = self.resolve("Ignore Holes")
|
||||
if val is not None:
|
||||
ignore_holes = bool(val)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not coll: return SlicePaths()
|
||||
|
||||
# Handle SlicingGroup flattening
|
||||
|
|
@ -104,7 +124,11 @@ class SLICENODE_SliceShells(SliceNode):
|
|||
else:
|
||||
z_values = np.array([])
|
||||
|
||||
slices_group: list[list[np.ndarray]] | None = slice_at_range([merged_object.mesh], z_values)
|
||||
slices_group: list[list[np.ndarray]] | None = slice_at_range(
|
||||
[merged_object.mesh],
|
||||
z_values,
|
||||
ignore_holes=ignore_holes
|
||||
)
|
||||
assert slices_group
|
||||
slices = SlicePaths(slices_group[0])
|
||||
slices.layer_height = layer_height
|
||||
|
|
|
|||
|
|
@ -610,7 +610,7 @@ class SlicePaths(list[np.ndarray]):
|
|||
|
||||
return self.copy_with(new_paths, extrusion_rate=new_ext, feedrate=new_feed, extra_attributes=new_extra)
|
||||
|
||||
def slice_at_range(models, z_values) -> list[list[np.ndarray]]:
|
||||
def slice_at_range(models, z_values, ignore_holes: bool = False) -> list[list[np.ndarray]]:
|
||||
if not models:
|
||||
return []
|
||||
|
||||
|
|
@ -634,7 +634,7 @@ def slice_at_range(models, z_values) -> list[list[np.ndarray]]:
|
|||
seg3d[..., 2] = zf
|
||||
|
||||
# Build geometry for this slice
|
||||
slice_paths = segments_to_slice(seg3d)
|
||||
slice_paths = segments_to_slice(seg3d, ignore_holes=ignore_holes)
|
||||
all_model_paths.extend(slice_paths)
|
||||
|
||||
mmps.append(all_model_paths)
|
||||
|
|
@ -746,7 +746,7 @@ def link_segments(segments: np.ndarray, tolerance=1e-6) -> list[np.ndarray]:
|
|||
return paths
|
||||
|
||||
@ndarray_lru_cache(maxsize=128)
|
||||
def segments_to_slice(segments: np.ndarray) -> list[np.ndarray]:
|
||||
def segments_to_slice(segments: np.ndarray, ignore_holes: bool = False) -> list[np.ndarray]:
|
||||
|
||||
linked = link_segments(segments)
|
||||
if not linked: return []
|
||||
|
|
@ -771,8 +771,21 @@ def segments_to_slice(segments: np.ndarray) -> list[np.ndarray]:
|
|||
scaled = (path_pts * SCALE).astype(np.int64).tolist()
|
||||
pc.AddPath(scaled, pyclipper.PT_SUBJECT, closed = True) # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
# Use EVENODD for robust hole detection.
|
||||
solution = pc.Execute(pyclipper.CT_UNION, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD) # pyright: ignore[reportAttributeAccessIssue]
|
||||
if ignore_holes:
|
||||
tree = pc.Execute2(pyclipper.CT_UNION, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD) # pyright: ignore[reportAttributeAccessIssue]
|
||||
def collect_outer_contours(node) -> list[list[int]]:
|
||||
contours = []
|
||||
if hasattr(node, 'Contour') and node.Contour and not getattr(node, 'IsHole', False):
|
||||
contours.append(node.Contour)
|
||||
if hasattr(node, 'Childs') and node.Childs:
|
||||
for child in node.Childs:
|
||||
contours.extend(collect_outer_contours(child))
|
||||
return contours
|
||||
|
||||
solution = collect_outer_contours(tree)
|
||||
else:
|
||||
# Use EVENODD for robust hole detection.
|
||||
solution = pc.Execute(pyclipper.CT_UNION, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD) # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
for path in solution:
|
||||
if not path: continue
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ This file provides a mapping of modules to their primary responsibilities and ke
|
|||
## `/core` (Geometry & Slicing)
|
||||
|
||||
- `slice.py`:
|
||||
- `slice_at_range()`: Top-level slicing function that iterates through Z-heights.
|
||||
- `slice_at_range()`: Top-level slicing function that iterates through Z-heights (supports `ignore_holes` option).
|
||||
- `slice_at_z()`: Vectorized plane-mesh intersection using NumPy.
|
||||
- `align_path_seam_sharpest()`: High-performance vectorized heuristic that positions the start vertex (seam) of closed polygon loops at the sharpest concave angle (inner corner), with fallback to sharpest angle overall.
|
||||
- `SlicePaths.align_seams()`: Method on `SlicePaths` to align path seams to sharpest concave angles while preserving per-vertex attributes and metadata.
|
||||
- `Pather`: Class that converts toolpaths into G-code strings. Supports per-segment extrusion rates, feedrates, and custom extra attributes.
|
||||
- `segments_to_slice()`: Utility to convert raw intersection segments into continuous paths (closed polygons are processed via Pyclipper, open paths remain open).
|
||||
- `segments_to_slice()`: Utility to convert raw intersection segments into continuous paths (closed polygons are processed via Pyclipper, supports `ignore_holes`, open paths remain open).
|
||||
- `link_segments()`: Logic for joining loose segments into continuous paths.
|
||||
- `buffer_slice()`: Offset utility using PyclipperOffset. Supports rotated offsetting.
|
||||
- `infill_slice()`: Generates rotated grid or rectilinear infill patterns, ensuring layer-to-layer consistency.
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
|||
- **Float**: Single float input.
|
||||
- **Integer**: Single integer input.
|
||||
- **String**: Single string/path input.
|
||||
- **Collection Objects**: Selects a Blender collection and dynamically exposes its mesh objects as output sockets, sorted alphabetically.
|
||||
- **Settings**: Read-only/write configuration settings dictionary.
|
||||
- **Printer**: Printer name picker.
|
||||
- **Printer List**: List of configured printers.
|
||||
|
|
@ -80,6 +81,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
|||
- **Feedrate Attribute**: Mesh attribute to use for per-segment feedrates.
|
||||
- **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.
|
||||
- **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.
|
||||
|
|
|
|||
178
tests/test_collection_objects.py
Normal file
178
tests/test_collection_objects.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.append(str(PROJECT_ROOT.parent))
|
||||
|
||||
class DummyBase:
|
||||
def _get_data_dict(self):
|
||||
if not hasattr(self, "_data") or self._data is None:
|
||||
self._data = {}
|
||||
return self._data
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._get_data_dict()[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._get_data_dict()[key] = value
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._get_data_dict()
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self._get_data_dict().get(key, default)
|
||||
|
||||
class DummyObject:
|
||||
def __init__(self, name="TestObj", obj_type='MESH'):
|
||||
self.name = name
|
||||
self.type = obj_type
|
||||
|
||||
mock_types = MagicMock()
|
||||
mock_types.NodeSocket = type("NodeSocket", (DummyBase,), {})
|
||||
mock_types.Node = DummyBase
|
||||
mock_types.NodeCustomGroup = type("NodeCustomGroup", (DummyBase,), {})
|
||||
mock_types.NodeTree = type("NodeTree", (DummyBase,), {})
|
||||
mock_types.PropertyGroup = type("PropertyGroup", (DummyBase,), {})
|
||||
mock_types.Operator = type("Operator", (DummyBase,), {})
|
||||
mock_types.Panel = type("Panel", (DummyBase,), {})
|
||||
mock_types.Menu = type("Menu", (DummyBase,), {})
|
||||
mock_types.Object = DummyObject
|
||||
mock_types.Collection = type("Collection", (DummyBase,), {})
|
||||
|
||||
if 'bpy' not in sys.modules:
|
||||
mock_bpy = MagicMock()
|
||||
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 bpy
|
||||
|
||||
class TestNodeSocket:
|
||||
def __init__(self, name="", identifier="", node=None):
|
||||
self.name = name
|
||||
self.identifier = identifier or name
|
||||
self.is_linked = False
|
||||
self.links = []
|
||||
self.node = node
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TestNodeSocket name={self.name!r} identifier={self.identifier!r}>"
|
||||
|
||||
class TestSocketCollection(list):
|
||||
def __init__(self, node=None):
|
||||
super().__init__()
|
||||
self.node = node
|
||||
|
||||
def new(self, type_name, name, identifier=""):
|
||||
sock = TestNodeSocket(name, identifier or name, node=self.node)
|
||||
self.append(sock)
|
||||
return sock
|
||||
|
||||
def remove(self, sock):
|
||||
if sock in self:
|
||||
super().remove(sock)
|
||||
|
||||
def get(self, name, default=None):
|
||||
for item in self:
|
||||
if item.name == name or item.identifier == name:
|
||||
return item
|
||||
return default
|
||||
|
||||
def find(self, name):
|
||||
for i, item in enumerate(self):
|
||||
if item.name == name or item.identifier == name:
|
||||
return i
|
||||
return -1
|
||||
|
||||
def __contains__(self, item):
|
||||
if isinstance(item, str):
|
||||
return any(sock.name == item or sock.identifier == item for sock in self)
|
||||
return super().__contains__(item)
|
||||
|
||||
def move(self, from_idx, to_idx):
|
||||
item = self.pop(from_idx)
|
||||
self.insert(to_idx, item)
|
||||
|
||||
class DummyCollection:
|
||||
def __init__(self, name="TestCollection", objects=None):
|
||||
self.name = name
|
||||
self.objects = objects or []
|
||||
|
||||
from NodeSlicer.core.nodeslicer.nodes.inputs import SLICENODE_CollectionObjects
|
||||
|
||||
class TestCollectionObjectsSorting(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.node = SLICENODE_CollectionObjects()
|
||||
self.node.inputs = TestSocketCollection(self.node)
|
||||
self.node.outputs = TestSocketCollection(self.node)
|
||||
|
||||
def test_outputs_sorted_alphabetically(self):
|
||||
# Create collection with objects in non-alphabetical order
|
||||
obj_z = DummyObject("Zebra", 'MESH')
|
||||
obj_a = DummyObject("Apple", 'MESH')
|
||||
obj_m = DummyObject("Mango", 'MESH')
|
||||
obj_camera = DummyObject("Camera", 'CAMERA') # Should be ignored
|
||||
|
||||
collection = DummyCollection("Fruits", [obj_z, obj_a, obj_camera, obj_m])
|
||||
self.node.collection = collection
|
||||
|
||||
self.node._update_outputs()
|
||||
|
||||
output_names = [o.name for o in self.node.outputs]
|
||||
self.assertEqual(output_names, ["Apple", "Mango", "Zebra"])
|
||||
|
||||
def test_dynamic_add_and_reorder(self):
|
||||
# Initial collection with B and D
|
||||
obj_b = DummyObject("Banana", 'MESH')
|
||||
obj_d = DummyObject("Date", 'MESH')
|
||||
collection = DummyCollection("Fruits", [obj_b, obj_d])
|
||||
self.node.collection = collection
|
||||
|
||||
self.node._update_outputs()
|
||||
self.assertEqual([o.name for o in self.node.outputs], ["Banana", "Date"])
|
||||
|
||||
# Add Apple (which should be placed at the beginning) and Cherry (between Banana and Date)
|
||||
obj_a = DummyObject("Apple", 'MESH')
|
||||
obj_c = DummyObject("Cherry", 'MESH')
|
||||
collection.objects.extend([obj_a, obj_c])
|
||||
|
||||
self.node._update_outputs()
|
||||
self.assertEqual([o.name for o in self.node.outputs], ["Apple", "Banana", "Cherry", "Date"])
|
||||
|
||||
def test_remove_object(self):
|
||||
obj_a = DummyObject("Apple", 'MESH')
|
||||
obj_b = DummyObject("Banana", 'MESH')
|
||||
obj_c = DummyObject("Cherry", 'MESH')
|
||||
collection = DummyCollection("Fruits", [obj_a, obj_b, obj_c])
|
||||
self.node.collection = collection
|
||||
|
||||
self.node._update_outputs()
|
||||
self.assertEqual([o.name for o in self.node.outputs], ["Apple", "Banana", "Cherry"])
|
||||
|
||||
# Remove Banana
|
||||
collection.objects = [obj_a, obj_c]
|
||||
self.node._update_outputs()
|
||||
self.assertEqual([o.name for o in self.node.outputs], ["Apple", "Cherry"])
|
||||
|
||||
def test_evaluate_output(self):
|
||||
obj_a = DummyObject("Apple", 'MESH')
|
||||
bpy.data.objects = {"Apple": obj_a}
|
||||
collection = DummyCollection("Fruits", [obj_a])
|
||||
self.node.collection = collection
|
||||
self.node._update_outputs()
|
||||
|
||||
result = self.node.evaluate_output("Apple")
|
||||
self.assertEqual(result, obj_a)
|
||||
|
||||
result_nonexistent = self.node.evaluate_output("NonExistent")
|
||||
self.assertIsNone(result_nonexistent)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -813,6 +813,101 @@ class TestSlice(unittest.TestCase):
|
|||
self.assertIsNotNone(res_full_2)
|
||||
self.assertEqual(res_full_2.objects[0].name, "FullMesh00")
|
||||
|
||||
def test_segments_to_slice_ignore_holes(self):
|
||||
# Outer square: [0,0] to [20,20] at z=10
|
||||
outer_segments = [
|
||||
[[0.0, 0.0, 10.0], [20.0, 0.0, 10.0]],
|
||||
[[20.0, 0.0, 10.0], [20.0, 20.0, 10.0]],
|
||||
[[20.0, 20.0, 10.0], [0.0, 20.0, 10.0]],
|
||||
[[0.0, 20.0, 10.0], [0.0, 0.0, 10.0]],
|
||||
]
|
||||
# Inner hole square: [5,5] to [15,15] at z=10
|
||||
hole_segments = [
|
||||
[[5.0, 5.0, 10.0], [15.0, 5.0, 10.0]],
|
||||
[[15.0, 5.0, 10.0], [15.0, 15.0, 10.0]],
|
||||
[[15.0, 15.0, 10.0], [5.0, 15.0, 10.0]],
|
||||
[[5.0, 15.0, 10.0], [5.0, 5.0, 10.0]],
|
||||
]
|
||||
all_segs = np.array(outer_segments + hole_segments, dtype=np.float64)
|
||||
|
||||
# Default (ignore_holes=False) -> produces 2 paths (outer + hole)
|
||||
res_with_holes = segments_to_slice(all_segs, ignore_holes=False)
|
||||
self.assertEqual(len(res_with_holes), 2)
|
||||
|
||||
# ignore_holes=True -> produces 1 path (outer shell only)
|
||||
res_no_holes = segments_to_slice(all_segs, ignore_holes=True)
|
||||
self.assertEqual(len(res_no_holes), 1)
|
||||
# Verify outer shell bounds [0, 20]
|
||||
self.assertAlmostEqual(res_no_holes[0][:, 0].min(), 0.0)
|
||||
self.assertAlmostEqual(res_no_holes[0][:, 0].max(), 20.0)
|
||||
|
||||
def test_slice_shells_node_ignore_holes(self):
|
||||
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_SliceShells
|
||||
from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject, SlicingCollection
|
||||
|
||||
# Construct a hollow box mesh
|
||||
# Outer box: [0, 20] x [0, 20] x [0, 1]
|
||||
# Inner hole box: [5, 15] x [5, 15] x [0, 1]
|
||||
def make_quad(v0, v1, v2, v3):
|
||||
return [
|
||||
[v0, v1, v2],
|
||||
[v0, v2, v3]
|
||||
]
|
||||
|
||||
# 4 outer vertical walls + 4 inner vertical walls
|
||||
z0, z1 = 0.0, 1.0
|
||||
faces = []
|
||||
# Outer walls
|
||||
faces += make_quad([0, 0, z0], [20, 0, z0], [20, 0, z1], [0, 0, z1])
|
||||
faces += make_quad([20, 0, z0], [20, 20, z0], [20, 20, z1], [20, 0, z1])
|
||||
faces += make_quad([20, 20, z0], [0, 20, z0], [0, 20, z1], [20, 20, z1])
|
||||
faces += make_quad([0, 20, z0], [0, 0, z0], [0, 0, z1], [0, 20, z1])
|
||||
# Inner hole walls
|
||||
faces += make_quad([5, 5, z0], [15, 5, z0], [15, 5, z1], [5, 5, z1])
|
||||
faces += make_quad([15, 5, z0], [15, 15, z0], [15, 15, z1], [15, 5, z1])
|
||||
faces += make_quad([15, 15, z0], [5, 15, z0], [5, 15, z1], [15, 15, z1])
|
||||
faces += make_quad([5, 15, z0], [5, 5, z0], [5, 5, z1], [5, 15, z1])
|
||||
|
||||
triangles = np.zeros((len(faces), 4, 3))
|
||||
for i, tri in enumerate(faces):
|
||||
triangles[i, 0] = tri[0]
|
||||
triangles[i, 1] = tri[1]
|
||||
triangles[i, 2] = tri[2]
|
||||
|
||||
sobj = SlicingObject()
|
||||
sobj.mesh = triangles
|
||||
scoll = SlicingCollection([], "test_hollow")
|
||||
scoll.objects = [sobj]
|
||||
|
||||
# 1. Normal evaluation (ignore_holes=False)
|
||||
node = SLICENODE_SliceShells()
|
||||
node.ignore_holes = False
|
||||
node.resolve = lambda name: scoll if name == "Mesh" else (0.5 if name == "Layer Height" else None)
|
||||
slices_normal = node.evaluate_output("Paths")
|
||||
self.assertEqual(len(slices_normal), 2) # Outer + hole
|
||||
|
||||
# 2. Evaluation with ignore_holes=True
|
||||
node.ignore_holes = True
|
||||
slices_ignored = node.evaluate_output("Paths")
|
||||
self.assertEqual(len(slices_ignored), 1) # Only outer
|
||||
|
||||
# 3. Evaluation when node lacks ignore_holes property (simulating missing property / legacy tree)
|
||||
node_missing = SLICENODE_SliceShells.__new__(SLICENODE_SliceShells)
|
||||
# Only set resolve and standard dictionary without ignore_holes
|
||||
node_missing.resolve = lambda name: scoll if name == "Mesh" else (0.5 if name == "Layer Height" else None)
|
||||
slices_missing = node_missing.evaluate_output("Paths")
|
||||
self.assertIsNotNone(slices_missing)
|
||||
self.assertEqual(len(slices_missing), 2)
|
||||
|
||||
# 4. draw_buttons with and without ignore_holes does not fail
|
||||
mock_layout = MagicMock()
|
||||
node.draw_buttons(None, mock_layout)
|
||||
mock_layout.prop.assert_called_with(node, "ignore_holes")
|
||||
|
||||
mock_layout_missing = MagicMock()
|
||||
node_missing.draw_buttons(None, mock_layout_missing)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue