arranging
This commit is contained in:
parent
96919bfe70
commit
918df11861
9 changed files with 382 additions and 1 deletions
|
|
@ -48,6 +48,9 @@ wheels = [
|
||||||
"./wheels/pybgcode_jneilliii-0.2.1-cp313-cp313-macosx_11_0_arm64.whl",
|
"./wheels/pybgcode_jneilliii-0.2.1-cp313-cp313-macosx_11_0_arm64.whl",
|
||||||
"./wheels/pybgcode_jneilliii-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
|
"./wheels/pybgcode_jneilliii-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
|
||||||
"./wheels/pybgcode_jneilliii-0.2.1-cp313-cp313-win_amd64.whl",
|
"./wheels/pybgcode_jneilliii-0.2.1-cp313-cp313-win_amd64.whl",
|
||||||
|
|
||||||
|
"./wheels/python_libnest2d-0.1.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
|
||||||
|
"./wheels/python_libnest2d-0.1.3-cp313-cp313-win_amd64.whl",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Optional: add-ons can list which resources they will require:
|
# Optional: add-ons can list which resources they will require:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from ..bpy_ns_types import NfBoolProperty, NfEnumProperty, NfIntProperty, notify_tree_changed
|
from ..bpy_ns_types import NfBoolProperty, NfEnumProperty, NfIntProperty, NfFloatProperty, notify_tree_changed
|
||||||
from ....registry import register_class
|
from ....registry import register_class
|
||||||
from .base import SliceNode, SlicingCollection, get_all_children, SliceNodeJoiner, SlicingGroup, SlicingObject
|
from .base import SliceNode, SlicingCollection, get_all_children, SliceNodeJoiner, SlicingGroup, SlicingObject
|
||||||
|
|
||||||
|
|
@ -250,3 +250,182 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
|
||||||
res.objects.append(so_new)
|
res.objects.append(so_new)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_ArrangeMesh(SliceNode):
|
||||||
|
bl_label = "Arrange Slicing Group"
|
||||||
|
|
||||||
|
method: NfEnumProperty(
|
||||||
|
name="Method",
|
||||||
|
items=[
|
||||||
|
('NFP', "NFP (Default)", "Nest using No-Fit Polygon placer + First-Fit selection"),
|
||||||
|
('BL', "Bottom-Left", "Nest using Bottom-Left placer + First-Fit selection"),
|
||||||
|
('DJD', "DJD Heuristic", "Nest using No-Fit Polygon placer + DJD Heuristic selection"),
|
||||||
|
],
|
||||||
|
default='NFP',
|
||||||
|
update=notify_tree_changed
|
||||||
|
) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
|
allow_rotations: NfBoolProperty(
|
||||||
|
name="Allow Rotations",
|
||||||
|
default=True,
|
||||||
|
update=notify_tree_changed
|
||||||
|
) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Mesh", "Slicing Group", display_shape='LIST')
|
||||||
|
self.add_input("SLICESOCK_Float", "Bed X").value = 250.0
|
||||||
|
self.add_input("SLICESOCK_Float", "Bed Y").value = 250.0
|
||||||
|
self.add_input("SLICESOCK_Float", "Spacing").value = 2.0
|
||||||
|
self.add_output("SLICESOCK_Mesh", "Slicing Group", display_shape='LIST')
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "method", text="")
|
||||||
|
layout.prop(self, "allow_rotations")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup | None:
|
||||||
|
if socket_name != "Slicing Group":
|
||||||
|
return None
|
||||||
|
|
||||||
|
mesh_data: SlicingCollection | SlicingGroup | None = self.resolve("Slicing Group")
|
||||||
|
if not mesh_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
bed_x: float = self.resolve("Bed X") or 250.0
|
||||||
|
bed_y: float = self.resolve("Bed Y") or 250.0
|
||||||
|
spacing: float = self.resolve("Spacing") or 2.0
|
||||||
|
|
||||||
|
# Convert input to SlicingGroup if it's a SlicingCollection
|
||||||
|
if isinstance(mesh_data, SlicingGroup):
|
||||||
|
slicing_group = mesh_data
|
||||||
|
else:
|
||||||
|
slicing_group = SlicingGroup([])
|
||||||
|
slicing_group.collections = {mesh_data.name: mesh_data}
|
||||||
|
|
||||||
|
# Create a deep copy of the group to avoid mutating cached inputs
|
||||||
|
copied_group = _copy_slicing_group(slicing_group)
|
||||||
|
|
||||||
|
# Prepare the list of items to pack
|
||||||
|
import pynest2d
|
||||||
|
|
||||||
|
items_list = []
|
||||||
|
collection_map = {}
|
||||||
|
for name, sc in copied_group.collections.items():
|
||||||
|
if sc.min_x is None or sc.max_x is None or sc.min_y is None or sc.max_y is None:
|
||||||
|
continue
|
||||||
|
w_mm = sc.max_x - sc.min_x
|
||||||
|
h_mm = sc.max_y - sc.min_y
|
||||||
|
if w_mm <= 0 or h_mm <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
w = pynest2d.mm(w_mm)
|
||||||
|
h = pynest2d.mm(h_mm)
|
||||||
|
item = pynest2d.Rectangle(w, h)
|
||||||
|
items_list.append(item)
|
||||||
|
collection_map[id(item)] = (name, sc)
|
||||||
|
|
||||||
|
if not items_list:
|
||||||
|
return copied_group
|
||||||
|
|
||||||
|
# Define the bin
|
||||||
|
bin_w = pynest2d.mm(bed_x)
|
||||||
|
bin_h = pynest2d.mm(bed_y)
|
||||||
|
bin_center = pynest2d.Point(pynest2d.mm(bed_x / 2.0), pynest2d.mm(bed_y / 2.0))
|
||||||
|
box = pynest2d.Box(bin_w, bin_h, bin_center)
|
||||||
|
|
||||||
|
# Spacing in internal units
|
||||||
|
spacing_units = pynest2d.mm(spacing)
|
||||||
|
|
||||||
|
# Configure and run the selected placer
|
||||||
|
placer = getattr(self, "method", "NFP")
|
||||||
|
allow_rot = getattr(self, "allow_rotations", True)
|
||||||
|
|
||||||
|
if placer == 'BL':
|
||||||
|
config = pynest2d.BLConfig()
|
||||||
|
config.allow_rotations = allow_rot
|
||||||
|
pynest2d.nest_blp(items_list, box, spacing_units, config)
|
||||||
|
elif placer == 'DJD':
|
||||||
|
placer_config = pynest2d.NfpConfig()
|
||||||
|
if not allow_rot:
|
||||||
|
placer_config.rotations = [0.0]
|
||||||
|
pynest2d.nest_djd(items_list, box, spacing_units, placer_config)
|
||||||
|
else: # 'NFP'
|
||||||
|
config = pynest2d.NfpConfig()
|
||||||
|
if not allow_rot:
|
||||||
|
config.rotations = [0.0]
|
||||||
|
pynest2d.nest(items_list, box, spacing_units, config)
|
||||||
|
|
||||||
|
# Apply the resulting translations and rotations to the copied collections
|
||||||
|
for item in items_list:
|
||||||
|
bin_id = item.binId()
|
||||||
|
if bin_id == 0:
|
||||||
|
name, sc = collection_map[id(item)]
|
||||||
|
trans = item.translation()
|
||||||
|
rot = item.rotation()
|
||||||
|
|
||||||
|
tx = trans.x() / pynest2d.MM_IN_COORDS
|
||||||
|
ty = trans.y() / pynest2d.MM_IN_COORDS
|
||||||
|
|
||||||
|
min_x = sc.min_x
|
||||||
|
min_y = sc.min_y
|
||||||
|
|
||||||
|
cos_val = np.cos(rot)
|
||||||
|
sin_val = np.sin(rot)
|
||||||
|
|
||||||
|
for so in sc.objects:
|
||||||
|
if so.mesh is None or so.mesh.size == 0:
|
||||||
|
continue
|
||||||
|
vx = so.mesh[:, :3, 0]
|
||||||
|
vy = so.mesh[:, :3, 1]
|
||||||
|
|
||||||
|
vx_new = (vx - min_x) * cos_val - (vy - min_y) * sin_val + tx
|
||||||
|
vy_new = (vx - min_x) * sin_val + (vy - min_y) * cos_val + ty
|
||||||
|
|
||||||
|
so.mesh[:, :3, 0] = vx_new
|
||||||
|
so.mesh[:, :3, 1] = vy_new
|
||||||
|
|
||||||
|
nx = so.mesh[:, 3, 0]
|
||||||
|
ny = so.mesh[:, 3, 1]
|
||||||
|
|
||||||
|
nx_new = nx * cos_val - ny * sin_val
|
||||||
|
ny_new = nx * sin_val + ny * cos_val
|
||||||
|
|
||||||
|
so.mesh[:, 3, 0] = nx_new
|
||||||
|
so.mesh[:, 3, 1] = ny_new
|
||||||
|
else:
|
||||||
|
name, sc = collection_map[id(item)]
|
||||||
|
print(f"Warning: Collection '{name}' did not fit on the build plate (binId: {bin_id})")
|
||||||
|
|
||||||
|
return copied_group
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_slicing_object(so: SlicingObject) -> SlicingObject:
|
||||||
|
new_so = SlicingObject()
|
||||||
|
new_so.name = so.name
|
||||||
|
new_so.parent = so.parent
|
||||||
|
new_so.mesh = so.mesh.copy() if so.mesh is not None else np.empty((0, 4, 3))
|
||||||
|
new_so.attributes = {}
|
||||||
|
for k, v in so.attributes.items():
|
||||||
|
new_so.attributes[k] = {
|
||||||
|
"domain": v["domain"],
|
||||||
|
"type": v["type"],
|
||||||
|
"values": v["values"].copy() if isinstance(v["values"], np.ndarray) else v["values"]
|
||||||
|
}
|
||||||
|
new_so.object_type = so.object_type
|
||||||
|
new_so.modifiers = so.modifiers.copy() if so.modifiers else []
|
||||||
|
new_so.extruder = so.extruder
|
||||||
|
return new_so
|
||||||
|
|
||||||
|
def _copy_slicing_collection(sc: SlicingCollection) -> SlicingCollection:
|
||||||
|
new_sc = SlicingCollection([], sc.name)
|
||||||
|
new_sc.objects = [_copy_slicing_object(so) for so in sc.objects]
|
||||||
|
return new_sc
|
||||||
|
|
||||||
|
def _copy_slicing_group(sg: SlicingGroup) -> SlicingGroup:
|
||||||
|
new_sg = SlicingGroup([])
|
||||||
|
new_sg.collections = {k: _copy_slicing_collection(sc) for k, sc in sg.collections.items()}
|
||||||
|
new_sg.wipe_tower_xy = sg.wipe_tower_xy.copy()
|
||||||
|
new_sg.wipe_tower_rotation_deg = sg.wipe_tower_rotation_deg
|
||||||
|
return new_sg
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -498,6 +498,7 @@ class SLICENODE_MT_Geometry(Menu):
|
||||||
layout.operator("node.add_node", text="Get Mesh").type = "SLICENODE_GetMesh"
|
layout.operator("node.add_node", text="Get Mesh").type = "SLICENODE_GetMesh"
|
||||||
layout.operator("node.add_node", text="Separate Mesh").type = "SLICENODE_SeparateMesh"
|
layout.operator("node.add_node", text="Separate Mesh").type = "SLICENODE_SeparateMesh"
|
||||||
layout.operator("node.add_node", text="Separate Mesh by Range").type = "SLICENODE_SeparateMeshByRange"
|
layout.operator("node.add_node", text="Separate Mesh by Range").type = "SLICENODE_SeparateMeshByRange"
|
||||||
|
layout.operator("node.add_node", text="Arrange Slicing Group").type = "SLICENODE_ArrangeMesh"
|
||||||
layout.operator("node.add_node", text="Get Paths").type = "SLICENODE_GetPaths"
|
layout.operator("node.add_node", text="Get Paths").type = "SLICENODE_GetPaths"
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator("node.add_node", text="Slice Shells").type = "SLICENODE_SliceShells"
|
layout.operator("node.add_node", text="Slice Shells").type = "SLICENODE_SliceShells"
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,11 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
||||||
- **Infill Rotation**: Base rotation of the infill pattern.
|
- **Infill Rotation**: Base rotation of the infill pattern.
|
||||||
- **Layer Rotation**: Rotation of the entire slice to align infill with specific planes.
|
- **Layer Rotation**: Rotation of the entire slice to align infill with specific planes.
|
||||||
- **Boolean Paths**: Performs 2D boolean operations (`UNION`, `INTERSECTION`, `DIFFERENCE`, `XOR`) on multiple slice sets.
|
- **Boolean Paths**: Performs 2D boolean operations (`UNION`, `INTERSECTION`, `DIFFERENCE`, `XOR`) on multiple slice sets.
|
||||||
|
- **Arrange Slicing Group**: Arranges the slicing collections in a slicing group on the build plate using the `libnest2d` bin-packing library. It nests their 2D bounding boxes to prevent collisions.
|
||||||
|
- **Method**: Switch between **NFP** (No-Fit Polygon placer + First-Fit selection), **Bottom-Left** (Bottom-Left placer + First-Fit selection), and **DJD Heuristic** (No-Fit Polygon placer + Dyckhoff's heuristic).
|
||||||
|
- **Allow Rotations**: Toggle whether to allow rotating objects (by 90-degree steps) during nesting.
|
||||||
|
- **Bed X / Y**: The build plate dimensions in mm.
|
||||||
|
- **Spacing**: The minimum distance between arranged objects in mm.
|
||||||
|
|
||||||
### Utility & Joiners
|
### Utility & Joiners
|
||||||
- **Join Mesh**: Merges multiple `SlicingObject` instances into one.
|
- **Join Mesh**: Merges multiple `SlicingObject` instances into one.
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,5 @@ dependencies = [
|
||||||
"numpy>=2.2.6",
|
"numpy>=2.2.6",
|
||||||
"pyclipper>=1.4.0",
|
"pyclipper>=1.4.0",
|
||||||
"shapely>=2.1.2",
|
"shapely>=2.1.2",
|
||||||
|
"python-libnest2d>=0.1.3",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
175
tests/test_arrange.py
Normal file
175
tests/test_arrange.py
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
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 and its subpackages before importing core modules
|
||||||
|
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 classes to test
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_ArrangeMesh
|
||||||
|
from NodeSlicer.infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject
|
||||||
|
|
||||||
|
class TestArrangeMesh(unittest.TestCase):
|
||||||
|
def test_arrange_mesh_simple(self):
|
||||||
|
# Create a SlicingObject with a single triangle
|
||||||
|
so = SlicingObject()
|
||||||
|
so.name = "part1"
|
||||||
|
so.mesh = np.array([[[10.0, 10.0, 0.0],
|
||||||
|
[20.0, 10.0, 0.0],
|
||||||
|
[15.0, 20.0, 0.0],
|
||||||
|
[0.0, 0.0, 1.0]]], dtype=np.float64)
|
||||||
|
|
||||||
|
# Create SlicingCollection
|
||||||
|
sc = SlicingCollection([], "coll1")
|
||||||
|
sc.objects = [so]
|
||||||
|
|
||||||
|
# Create SlicingGroup
|
||||||
|
sg = SlicingGroup([])
|
||||||
|
sg.collections = {"coll1": sc}
|
||||||
|
|
||||||
|
# Mock node inputs resolution
|
||||||
|
node = SLICENODE_ArrangeMesh()
|
||||||
|
node.method = 'NFP'
|
||||||
|
|
||||||
|
# We override resolve to simulate inputs
|
||||||
|
def mock_resolve(fieldname):
|
||||||
|
if fieldname == "Slicing Group":
|
||||||
|
return sg
|
||||||
|
elif fieldname == "Bed X":
|
||||||
|
return 250.0
|
||||||
|
elif fieldname == "Bed Y":
|
||||||
|
return 250.0
|
||||||
|
elif fieldname == "Spacing":
|
||||||
|
return 2.0
|
||||||
|
return None
|
||||||
|
|
||||||
|
node.resolve = mock_resolve
|
||||||
|
|
||||||
|
# Evaluate output
|
||||||
|
result_group = node.evaluate_output("Slicing Group")
|
||||||
|
self.assertIsNotNone(result_group)
|
||||||
|
self.assertIn("coll1", result_group.collections)
|
||||||
|
|
||||||
|
arranged_sc = result_group.collections["coll1"]
|
||||||
|
# The bounding box should now be centered on the 250x250 bed
|
||||||
|
self.assertEqual(arranged_sc.min_x, 120.0)
|
||||||
|
self.assertEqual(arranged_sc.min_y, 120.0)
|
||||||
|
self.assertEqual(arranged_sc.max_x, 130.0)
|
||||||
|
self.assertEqual(arranged_sc.max_y, 130.0)
|
||||||
|
|
||||||
|
# Check normal remains unchanged
|
||||||
|
self.assertTrue(np.allclose(arranged_sc.objects[0].mesh[0, 3, :], [0.0, 0.0, 1.0]))
|
||||||
|
|
||||||
|
def test_arrange_mesh_bl(self):
|
||||||
|
so = SlicingObject()
|
||||||
|
so.name = "part1"
|
||||||
|
so.mesh = np.array([[[10.0, 10.0, 0.0],
|
||||||
|
[20.0, 10.0, 0.0],
|
||||||
|
[15.0, 20.0, 0.0],
|
||||||
|
[0.0, 0.0, 1.0]]], dtype=np.float64)
|
||||||
|
|
||||||
|
sc = SlicingCollection([], "coll1")
|
||||||
|
sc.objects = [so]
|
||||||
|
sg = SlicingGroup([])
|
||||||
|
sg.collections = {"coll1": sc}
|
||||||
|
|
||||||
|
node = SLICENODE_ArrangeMesh()
|
||||||
|
node.method = 'BL'
|
||||||
|
|
||||||
|
def mock_resolve(fieldname):
|
||||||
|
if fieldname == "Slicing Group":
|
||||||
|
return sg
|
||||||
|
elif fieldname == "Bed X":
|
||||||
|
return 250.0
|
||||||
|
elif fieldname == "Bed Y":
|
||||||
|
return 250.0
|
||||||
|
elif fieldname == "Spacing":
|
||||||
|
return 2.0
|
||||||
|
return None
|
||||||
|
node.resolve = mock_resolve
|
||||||
|
|
||||||
|
result_group = node.evaluate_output("Slicing Group")
|
||||||
|
self.assertIsNotNone(result_group)
|
||||||
|
arranged_sc = result_group.collections["coll1"]
|
||||||
|
self.assertGreaterEqual(arranged_sc.min_x, 0.0)
|
||||||
|
self.assertGreaterEqual(arranged_sc.min_y, 0.0)
|
||||||
|
|
||||||
|
def test_arrange_mesh_djd(self):
|
||||||
|
so = SlicingObject()
|
||||||
|
so.name = "part1"
|
||||||
|
so.mesh = np.array([[[10.0, 10.0, 0.0],
|
||||||
|
[20.0, 10.0, 0.0],
|
||||||
|
[15.0, 20.0, 0.0],
|
||||||
|
[0.0, 0.0, 1.0]]], dtype=np.float64)
|
||||||
|
|
||||||
|
sc = SlicingCollection([], "coll1")
|
||||||
|
sc.objects = [so]
|
||||||
|
sg = SlicingGroup([])
|
||||||
|
sg.collections = {"coll1": sc}
|
||||||
|
|
||||||
|
node = SLICENODE_ArrangeMesh()
|
||||||
|
node.method = 'DJD'
|
||||||
|
|
||||||
|
def mock_resolve(fieldname):
|
||||||
|
if fieldname == "Slicing Group":
|
||||||
|
return sg
|
||||||
|
elif fieldname == "Bed X":
|
||||||
|
return 250.0
|
||||||
|
elif fieldname == "Bed Y":
|
||||||
|
return 250.0
|
||||||
|
elif fieldname == "Spacing":
|
||||||
|
return 2.0
|
||||||
|
return None
|
||||||
|
node.resolve = mock_resolve
|
||||||
|
|
||||||
|
result_group = node.evaluate_output("Slicing Group")
|
||||||
|
self.assertIsNotNone(result_group)
|
||||||
|
arranged_sc = result_group.collections["coll1"]
|
||||||
|
self.assertEqual(arranged_sc.min_x, 120.0)
|
||||||
|
self.assertEqual(arranged_sc.min_y, 120.0)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
17
uv.lock
generated
17
uv.lock
generated
|
|
@ -24,6 +24,7 @@ dependencies = [
|
||||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||||
{ name = "pyclipper" },
|
{ name = "pyclipper" },
|
||||||
|
{ name = "python-libnest2d" },
|
||||||
{ name = "shapely" },
|
{ name = "shapely" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -32,6 +33,7 @@ requires-dist = [
|
||||||
{ name = "fake-bpy-module", specifier = ">=20260317" },
|
{ name = "fake-bpy-module", specifier = ">=20260317" },
|
||||||
{ name = "numpy", specifier = ">=2.2.6" },
|
{ name = "numpy", specifier = ">=2.2.6" },
|
||||||
{ name = "pyclipper", specifier = ">=1.4.0" },
|
{ name = "pyclipper", specifier = ">=1.4.0" },
|
||||||
|
{ name = "python-libnest2d", specifier = ">=0.1.3" },
|
||||||
{ name = "shapely", specifier = ">=2.1.2" },
|
{ name = "shapely", specifier = ">=2.1.2" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -225,6 +227,21 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/59/81050abdc9e5b90ffc2c765738c5e40e9abd8e44864aaa737b600f16c562/pyclipper-1.4.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98b2a40f98e1fc1b29e8a6094072e7e0c7dfe901e573bf6cfc6eb7ce84a7ae87", size = 126495, upload-time = "2025-12-01T13:15:33.743Z" },
|
{ url = "https://files.pythonhosted.org/packages/18/59/81050abdc9e5b90ffc2c765738c5e40e9abd8e44864aaa737b600f16c562/pyclipper-1.4.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98b2a40f98e1fc1b29e8a6094072e7e0c7dfe901e573bf6cfc6eb7ce84a7ae87", size = 126495, upload-time = "2025-12-01T13:15:33.743Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-libnest2d"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/6b/616287af0afc6074c6ed4d40d71d6cac44c1e9ea697360cc72db9622adc2/python_libnest2d-0.1.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e25d406302d0080e93dcda6f5937a714944bed2062f2f63b7d1ba029f0da621", size = 1120509, upload-time = "2026-03-15T23:01:13.728Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/74/4aa63d421e6a745fe4a051e175f7477398b5ed3c9fa82818a1fe9381a63a/python_libnest2d-0.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:84ad58d2bde15d7ba4241897e8430b69880f222bfc3b1e34313591d124c921f8", size = 1237883, upload-time = "2026-03-15T23:01:15.547Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/05/f2bdcf8ee4bd07622db0c4d495a8669db5b67ea0b48f779964a9109ad845/python_libnest2d-0.1.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d452565ca44101e452529a35ff6ca58ac3015d3244c017eeb833f20a30d66e8c", size = 1121974, upload-time = "2026-03-15T23:01:17.324Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/7f/b4089649e0dead34ce4473758ad922b7889d85482b1467fe057e4a5181cb/python_libnest2d-0.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:8e6c802e757cc75a4134d8160f5c2afff2f22192f7337ca90831f825d006f906", size = 1238832, upload-time = "2026-03-15T23:01:19.12Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/70/ff657e7be57f000262e1002498e3cc4571c73d8c0f1dc30f26593500c27a/python_libnest2d-0.1.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7de467b5afd5451a861747b56e2e0ec4e0f2e07f453fc1802f003e0947b0e30", size = 1121510, upload-time = "2026-03-15T23:01:21.122Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/9d/4d128b6428d1cd2ef6f925270a74a669f831462d6b6ad5e8ac81a00fd384/python_libnest2d-0.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0084e53383c215a5566703b953c4638aeade5ecb3250fd3ab44dab533b25f348", size = 1239001, upload-time = "2026-03-15T23:01:22.867Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/24/33ec7aa4b2ce557c71e41a66464eed31fcfaaea000e02dc3eee3a8b2a919/python_libnest2d-0.1.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ae839990bdd31f9b60c6e00f5d06eef055d03d3977cfe095cd68b1674a99531e", size = 1121496, upload-time = "2026-03-15T23:01:24.845Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/62/c42b3d5fd8cf5174144788cd5fd5a17f0cc5f2cf60d7abb79ed659ee4708/python_libnest2d-0.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:b8e864651033a7497f466c56353e6d5c6e71f3b44220cfd7598cc38b535c7319", size = 1239026, upload-time = "2026-03-15T23:01:26.461Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shapely"
|
name = "shapely"
|
||||||
version = "2.1.2"
|
version = "2.1.2"
|
||||||
|
|
|
||||||
Binary file not shown.
BIN
wheels/python_libnest2d-0.1.3-cp313-cp313-win_amd64.whl
Normal file
BIN
wheels/python_libnest2d-0.1.3-cp313-cp313-win_amd64.whl
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue