This commit is contained in:
Nicolas Predella 2026-08-26 12:39:49 +03:00
parent 77323d29db
commit c77412bb50
5 changed files with 216 additions and 9 deletions

View file

@ -835,3 +835,125 @@ class SLICENODE_AssignPropertyToObject(SliceNode):
props.append(("", str(item))) props.append(("", str(item)))
return _assign_properties_to_item(geom, props) return _assign_properties_to_item(geom, props)
OBJECT_TYPE_ITEMS: list[tuple[str, str, str]] = [
("NO_CHANGE", "Keep Original", "Do not modify the object/part type"),
("ModelPart", "Model Part", "Standard model part"),
("ParameterModifier", "Modifier", "Parameter modifier volume"),
("NegativeVolume", "Negative Volume", "Negative volume for subtractions"),
("SupportBlocker", "Support Blocker", "Support blocker volume"),
("SupportEnforcer", "Support Enforcer", "Support enforcer volume"),
("WipeTower", "Wipe Tower", "Wipe tower marker"),
]
EXTRUDER_ITEMS: list[tuple[str, str, str]] = [
("NO_CHANGE", "Keep Original", "Do not modify extruder assignment"),
("0", "Default Extruder", "Default Extruder"),
("1", "Extruder 1", "Extruder 1"),
("2", "Extruder 2", "Extruder 2"),
("3", "Extruder 3", "Extruder 3"),
("4", "Extruder 4", "Extruder 4"),
("5", "Extruder 5", "Extruder 5"),
]
def _apply_modifiers_to_so(
so: SlicingObject,
part_type: str,
extruder: str,
modifiers_dict: dict[str, str] | None
) -> None:
if part_type != "NO_CHANGE":
so.object_type = part_type
if extruder != "NO_CHANGE":
so.extruder = extruder
if modifiers_dict:
existing = {m['param_id']: str(m['param_value']) for m in getattr(so, 'modifiers', []) if m and 'param_id' in m}
for k, v in modifiers_dict.items():
existing[str(k)] = str(v)
so.modifiers = [{'param_id': k, 'param_value': v} for k, v in existing.items()]
def _assign_modifiers_to_item(
geom: Any,
part_type: str = "NO_CHANGE",
extruder: str = "NO_CHANGE",
modifiers_dict: dict[str, str] | None = None
) -> Any:
if geom is None:
return None
elif isinstance(geom, (list, tuple)):
return [_assign_modifiers_to_item(item, part_type, extruder, modifiers_dict) for item in geom]
elif isinstance(geom, SlicingGroup):
copied = _copy_slicing_group(geom)
for coll in copied.collections.values():
for obj in coll.objects:
_apply_modifiers_to_so(obj, part_type, extruder, modifiers_dict)
return copied
elif isinstance(geom, SlicingCollection):
copied = _copy_slicing_collection(geom)
for obj in copied.objects:
_apply_modifiers_to_so(obj, part_type, extruder, modifiers_dict)
return copied
elif isinstance(geom, SlicingObject):
copied = _copy_slicing_object(geom)
_apply_modifiers_to_so(copied, part_type, extruder, modifiers_dict)
return copied
return geom
@register_class
class SLICENODE_AssignModifiers(SliceNode):
bl_label = "Assign Modifiers"
part_type: str = NfEnumProperty(
name="Part Type",
description="PrusaSlicer part/volume type",
items=OBJECT_TYPE_ITEMS,
default="NO_CHANGE"
)
extruder: str = NfEnumProperty(
name="Extruder",
description="Extruder assignment",
items=EXTRUDER_ITEMS,
default="NO_CHANGE"
)
def init(self, context):
self.add_input("SLICESOCK_Mesh", "Geometry")
self.add_input("SLICESOCK_Settings", "Modifiers")
self.add_output("SLICESOCK_Mesh", "Geometry")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "part_type", text="Type")
layout.prop(self, "extruder", text="Extruder")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Geometry":
return None
geom: Any = self.resolve("Geometry")
if not geom:
return None
raw_modifiers = self.resolve("Modifiers")
if raw_modifiers is None:
raw_modifiers = self.resolve("Settings")
mod_dict: dict[str, str] = {}
if raw_modifiers:
if isinstance(raw_modifiers, dict):
for k, v in raw_modifiers.items():
mod_dict[str(k)] = str(v)
elif isinstance(raw_modifiers, (list, tuple)):
for item in raw_modifiers:
if isinstance(item, tuple) and len(item) == 2:
mod_dict[str(item[0])] = str(item[1])
elif isinstance(item, dict):
for k, v in item.items():
mod_dict[str(k)] = str(v)
return _assign_modifiers_to_item(geom, self.part_type, self.extruder, mod_dict if mod_dict else None)

View file

@ -517,6 +517,7 @@ class SLICENODE_MT_Geometry(Menu):
layout.operator("node.add_node", text="Rename Mesh").type = "SLICENODE_RenameMesh" layout.operator("node.add_node", text="Rename Mesh").type = "SLICENODE_RenameMesh"
layout.operator("node.add_node", text="Assign PDF Properties").type = "SLICENODE_AssignPdfProperties" layout.operator("node.add_node", text="Assign PDF Properties").type = "SLICENODE_AssignPdfProperties"
layout.operator("node.add_node", text="Assign Property to Object").type = "SLICENODE_AssignPropertyToObject" layout.operator("node.add_node", text="Assign Property to Object").type = "SLICENODE_AssignPropertyToObject"
layout.operator("node.add_node", text="Assign Modifiers").type = "SLICENODE_AssignModifiers"
layout.operator("node.add_node", text="Transform Geometry").type = "SLICENODE_TransformGeometry" layout.operator("node.add_node", text="Transform Geometry").type = "SLICENODE_TransformGeometry"
layout.operator("node.add_node", text="Drop Geometry").type = "SLICENODE_DropGeometry" layout.operator("node.add_node", text="Drop Geometry").type = "SLICENODE_DropGeometry"
layout.operator("node.add_node", text="Separate Mesh").type = "SLICENODE_SeparateMesh" layout.operator("node.add_node", text="Separate Mesh").type = "SLICENODE_SeparateMesh"

View file

@ -73,6 +73,11 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
- **Assign Property to Object**: Assigns custom settings/properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) for rendering in PDF properties tables. - **Assign Property to Object**: Assigns custom settings/properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) for rendering in PDF properties tables.
- **Geometry**: Generic geometry input socket (accepts `SLICESOCK_Mesh`, `SLICESOCK_Paths`, or `SLICESOCK_Geometry`). - **Geometry**: Generic geometry input socket (accepts `SLICESOCK_Mesh`, `SLICESOCK_Paths`, or `SLICESOCK_Geometry`).
- **Settings**: Input socket accepting configuration settings or property items. - **Settings**: Input socket accepting configuration settings or property items.
- **Assign Modifiers**: Assigns PrusaSlicer part/volume types (`ModelPart`, `ParameterModifier`, `NegativeVolume`, `SupportBlocker`, `SupportEnforcer`, `WipeTower`), extruder assignments (`0` to `5`), and config parameter modifiers directly to geometry (`SlicingObject`, `SlicingCollection`, or `SlicingGroup`) for 3MF export.
- **Geometry**: Generic geometry input socket (`SLICESOCK_Mesh`).
- **Modifiers**: Optional input socket accepting configuration settings dictionary (`SLICESOCK_Settings`).
- **Part Type**: Dropdown property specifying target part/volume type (`Keep Original`, `Model Part`, `Modifier`, `Negative Volume`, `Support Blocker`, `Support Enforcer`, `Wipe Tower`).
- **Extruder**: Dropdown property specifying target extruder assignment (`Keep Original`, `Default Extruder`, `Extruder 1-5`).
- **Transform Geometry**: Moves, rotates, and scales geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) relative to the origin. - **Transform Geometry**: Moves, rotates, and scales geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) relative to the origin.
- **Geometry**: Input geometry data. - **Geometry**: Input geometry data.
- **Translation**: 3D vector specifying the translation offset. - **Translation**: 3D vector specifying the translation offset.

View file

@ -7,14 +7,7 @@ import xml.etree.ElementTree as ET
from datetime import date from datetime import date
from typing import Any from typing import Any
<<<<<<< HEAD
from ..blender.blender_mesh_capture import SlicingGroup, SlicingCollection from ..blender.blender_mesh_capture import SlicingGroup, SlicingCollection
=======
try:
from ..blender.blender_mesh_capture import SlicingGroup, SlicingCollection
except ImportError:
from ..infra.blender_mesh_capture import SlicingGroup, SlicingCollection
>>>>>>> 9d282dee016bdbc10e4529120ee4e3faedbe6685
script_dir = os.path.dirname(os.path.abspath(__file__)) script_dir = os.path.dirname(os.path.abspath(__file__))

View file

@ -35,9 +35,11 @@ sys.modules['bpy.app'] = MagicMock()
sys.modules['bpy.app.handlers'] = MagicMock() sys.modules['bpy.app.handlers'] = MagicMock()
from NodeSlicer.core.nodeslicer.nodes.inputs import SLICENODE_WipeTower, SLICENODE_OffsetObject from NodeSlicer.core.nodeslicer.nodes.inputs import SLICENODE_WipeTower, SLICENODE_OffsetObject
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_AssignModifiers
from NodeSlicer.infra.prusaslicer_profiles.models import ConfigWriter from NodeSlicer.infra.prusaslicer_profiles.models import ConfigWriter
from NodeSlicer.infra._3mf_writer._3mf import write_model_xml from NodeSlicer.infra._3mf_writer._3mf import write_model_xml, write_metadata_xml
from NodeSlicer.infra.blender.blender_mesh_capture import SlicingGroup from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject, SlicingCollection, SlicingGroup
import xml.etree.ElementTree as ET
class TestWipeTowerNode(unittest.TestCase): class TestWipeTowerNode(unittest.TestCase):
def test_wipe_tower_node_output(self): def test_wipe_tower_node_output(self):
@ -77,5 +79,89 @@ class TestWipeTowerNode(unittest.TestCase):
self.assertNotIn("wipe_tower_position", content) self.assertNotIn("wipe_tower_position", content)
self.assertNotIn("offset_object", content) self.assertNotIn("offset_object", content)
class TestAssignModifiersNode(unittest.TestCase):
def setUp(self):
self.so = SlicingObject()
self.so.name = "TestPart"
self.so.object_type = "ModelPart"
self.so.extruder = "1"
self.so.modifiers = [{"param_id": "initial_param", "param_value": "123"}]
self.so.mesh = np.array([[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]], dtype=np.float64)
self.sc = SlicingCollection([], "TestColl")
self.sc.objects = [self.so]
self.sc.mesh_start_ids = [0]
self.sc.mesh_end_ids = [1]
self.sg = SlicingGroup([])
self.sg.collections = {"TestColl": self.sc}
def test_assign_modifiers_to_slicing_object(self):
node = SLICENODE_AssignModifiers()
node.part_type = "ParameterModifier"
node.extruder = "3"
node.resolve = lambda f: {
"Geometry": self.so,
"Modifiers": {"infill_density": "20%", "layer_height": "0.15"}
}.get(f)
result_so = node.evaluate_output("Geometry")
self.assertIsNotNone(result_so)
self.assertEqual(result_so.object_type, "ParameterModifier")
self.assertEqual(result_so.extruder, "3")
mod_dict = {m['param_id']: m['param_value'] for m in result_so.modifiers}
self.assertEqual(mod_dict["initial_param"], "123")
self.assertEqual(mod_dict["infill_density"], "20%")
self.assertEqual(mod_dict["layer_height"], "0.15")
def test_assign_modifiers_to_slicing_group(self):
node = SLICENODE_AssignModifiers()
node.part_type = "NegativeVolume"
node.extruder = "2"
node.resolve = lambda f: {
"Geometry": self.sg,
"Modifiers": {"perimeters": "4"}
}.get(f)
result_sg = node.evaluate_output("Geometry")
self.assertIsNotNone(result_sg)
res_so = result_sg.collections["TestColl"].objects[0]
self.assertEqual(res_so.object_type, "NegativeVolume")
self.assertEqual(res_so.extruder, "2")
mod_dict = {m['param_id']: m['param_value'] for m in res_so.modifiers}
self.assertEqual(mod_dict["perimeters"], "4")
def test_3mf_metadata_xml_output(self):
node = SLICENODE_AssignModifiers()
node.part_type = "ParameterModifier"
node.extruder = "2"
node.resolve = lambda f: {
"Geometry": self.sg,
"Modifiers": {"fill_density": "30%"}
}.get(f)
modified_sg = node.evaluate_output("Geometry")
with tempfile.TemporaryDirectory() as temp_dir:
xml_path = Path(temp_dir) / "Slic3r_PE_model.config"
write_metadata_xml(modified_sg, xml_path)
tree = ET.parse(xml_path)
root = tree.getroot()
volumes = root.findall(".//volume")
self.assertEqual(len(volumes), 1)
vol = volumes[0]
meta_entries = {m.get("key"): m.get("value") for m in vol.findall("metadata")}
self.assertEqual(meta_entries.get("volume_type"), "ParameterModifier")
self.assertEqual(meta_entries.get("modifier"), "1")
self.assertEqual(meta_entries.get("extruder"), "2")
self.assertEqual(meta_entries.get("fill_density"), "30%")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()