This commit is contained in:
Nicolas Predella 2026-08-26 12:34:26 +03:00
parent bedc33c7d3
commit ec539fa9fe
4 changed files with 85 additions and 53 deletions

View file

@ -534,7 +534,7 @@ class SLICENODE_TransformGeometry(SliceNode):
self.add_output("SLICESOCK_Geometry", "Geometry")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name not in ("Geometry"):
if socket_name not in ("Geometry",):
return None
geom_data: Any = self.resolve("Geometry")
@ -835,6 +835,3 @@ class SLICENODE_AssignPropertyToObject(SliceNode):
props.append(("", str(item)))
return _assign_properties_to_item(geom, props)

View file

@ -1,18 +1,18 @@
from __future__ import annotations
from typing import Any
from numpy.typing import NDArray
from pathlib import Path
import numpy as np
import os, shutil, tempfile
import xml.etree.ElementTree as ET
from datetime import date
from typing import Any
from ..blender.blender_mesh_capture import SlicingObject, SlicingGroup, SlicingCollection
from ..blender.blender_mesh_capture import SlicingGroup, SlicingCollection
script_dir = os.path.dirname(os.path.abspath(__file__))
def write_metadata_xml(group: SlicingGroup, filepath):
def write_metadata_xml(group: SlicingGroup, filepath: str | Path) -> None:
# Custom sorting order for object types
object_type_order = {
'ModelPart': 0,
@ -28,23 +28,31 @@ def write_metadata_xml(group: SlicingGroup, filepath):
for j, (k, collection) in enumerate(valid_collections.items()):
sorted_data: list[tuple[NDArray[Any], NDArray[Any], SlicingObject]] = sorted(zip(collection.mesh_start_ids, collection.mesh_end_ids, collection.objects), key=lambda x: object_type_order.get(x[2].object_type, 5))
sorted_data = sorted(
zip(collection.mesh_start_ids, collection.mesh_end_ids, collection.objects),
key=lambda x: (
0 if x[2].name == collection.name else 1,
object_type_order.get(x[2].object_type, 5),
x[2].name,
),
)
object_elem = ET.SubElement(xml_content, "object", id=str(j+1), instances_count="1")
ET.SubElement(object_elem, "metadata", type="object", key="name", value=collection.name)
for i, (start, end, metadata) in enumerate(sorted_data):
if metadata.object_type == "ModelPart":
for mod in metadata.modifiers:
ET.SubElement(object_elem, "metadata", type="object", key=mod['param_id'], value=mod['param_value'])
volume_elem = ET.SubElement(object_elem, "volume", firstid=str(start), lastid=str(end))
ET.SubElement(volume_elem, "metadata", type="volume", key="name", value=metadata.name)
if metadata.object_type == "ParameterModifier":
ET.SubElement(volume_elem, "metadata", type="volume", key="modifier", value="1")
if metadata.object_type in ["ModelPart", "ParameterModifier"]:
for mod in metadata.modifiers:
if not mod: continue
ET.SubElement(volume_elem, "metadata", type="volume", key=mod['param_id'], value=mod['param_value'])
ET.SubElement(volume_elem, "metadata", type="volume", key="volume_type", value=metadata.object_type)
ET.SubElement(volume_elem, "metadata", type="volume", key="extruder", value=str(metadata.extruder))
ET.SubElement(volume_elem, "metadata", type="volume", key="source_object_id", value="0")
@ -56,19 +64,21 @@ def write_metadata_xml(group: SlicingGroup, filepath):
xml_tree = ET.ElementTree(xml_content)
xml_tree.write(filepath, encoding="UTF-8", xml_declaration=True)
def write_wipe_tower_xml(group: SlicingGroup, filename):
def write_wipe_tower_xml(group: SlicingGroup, filename: str | Path) -> None:
with open(filename, 'w', encoding="UTF-8") as file:
file.write(f'<?xml version="1.0" encoding="utf-8"?>\n')
file.write('<?xml version="1.0" encoding="utf-8"?>\n')
file.write(f'<wipe_tower_information bed_idx="0" position_x="{group.wipe_tower_xy[0]}" position_y="{group.wipe_tower_xy[1]}" rotation_deg="{group.wipe_tower_rotation_deg}"/>\n')
def write_model_xml(group: SlicingGroup, filename: str):
def write_model_xml(group: SlicingGroup, filename: str | Path) -> None:
now = date.today().isoformat()
# Open file for writing
with open(filename, 'w', encoding="UTF-8") as file:
# Write the XML declaration and opening model tag
file.write(f'<?xml version="1.0" encoding="UTF-8"?>\n')
file.write(f'<model xmlns="" unit="millimeter" xml:lang="en-US" xmlns:slic3rpe="">\n')
file.write('<?xml version="1.0" encoding="UTF-8"?>\n')
file.write('<model xmlns="" unit="millimeter" xml:lang="en-US" xmlns:slic3rpe="">\n')
# Write metadata entries using list comprehension
metadata_entries: list[tuple[str, str]] = [
@ -86,7 +96,7 @@ def write_model_xml(group: SlicingGroup, filename: str):
file.writelines([f' <metadata name="{name}">{value}</metadata>\n' for name, value in metadata_entries])
# Write resources element and object using list comprehension
file.write(f' <resources>\n')
file.write(' <resources>\n')
verts_template = np.vectorize(lambda x, y, z: '<vertex x="%.6f" y="%.6f" z="%.6f" />\n' % (x, y, z))
idx_template = np.vectorize(lambda a, b, c: '<triangle v1="%d" v2="%d" v3="%d" />\n' % (a, b, c))
@ -101,40 +111,41 @@ def write_model_xml(group: SlicingGroup, filename: str):
if uv.size and t_idx.size:
file.write(f' <object id="{str(i+1)}" type="model">\n')
file.write(f' <mesh>\n')
file.write(' <mesh>\n')
file.write(f' <vertices>\n')
file.write(' <vertices>\n')
file.writelines(verts_template(uv[:,0], uv[:,1], uv[:,2]))
file.write(f' </vertices>\n')
file.write(' </vertices>\n')
file.write(f' <triangles>\n')
file.write(' <triangles>\n')
file.writelines(idx_template(t_idx[:,0], t_idx[:,1], t_idx[:,2]))
file.write(f' </triangles>\n')
file.write(' </triangles>\n')
file.write(f' </mesh>\n')
file.write(f' </object>\n')
file.write(' </mesh>\n')
file.write(' </object>\n')
file.write(f' </resources>\n')
file.write(' </resources>\n')
# Write build element
file.write(f' <build>\n')
ox, oy, oz = group.object_offset[0], group.object_offset[1], group.object_offset[2]
file.write(' <build>\n')
ox, oy, oz = getattr(group, "object_offset", (0.0, 0.0, 0.0))
transform_str = f"1 0 0 0 1 0 0 0 1 {ox} {oy} {oz}"
for i, k in enumerate(valid_collections):
if not valid_collections[k].unique_verts[0].size: continue
file.writelines([f' <item objectid="{str(i+1)}" transform="{transform_str}" printable="1" />\n' ])
file.write(f' </build>\n')
file.writelines([f' <item objectid="{str(i+1)}" transform="{transform_str}" printable="1" />\n'])
file.write(' </build>\n')
# Close the model tag
file.write(f'</model>\n')
file.write('</model>\n')
def to_3mf(folder_path, output_base_path):
def to_3mf(folder_path: str | Path, output_base_path: str | Path) -> None:
zip_file_path = shutil.make_archive(os.path.splitext(output_base_path)[0], 'zip', folder_path)
new_file_path = os.path.splitext(zip_file_path)[0] + '.3mf'
os.replace(zip_file_path, new_file_path)
def write_z_gcodes(z_gcodes, filename: str):
def write_z_gcodes(z_gcodes, filename: str | Path) -> None:
root = ET.Element("custom_gcodes_per_print_z", bed_idx="0")
for c in z_gcodes:
@ -151,22 +162,20 @@ def write_z_gcodes(z_gcodes, filename: str):
ET.ElementTree(root).write(filename, encoding="utf-8", xml_declaration=True)
def prepare_3mf(filepath: Path, geoms: SlicingGroup, conf, z_gcodes) -> None:
def prepare_3mf(filepath: Path, geoms: SlicingGroup, conf: Any, z_gcodes: Any, pg: Any = None, cx: Any = None) -> None:
source_folder = os.path.join(script_dir, 'prusaslicer_3mf')
temp_dir = tempfile.mkdtemp()
shutil.copytree(source_folder, temp_dir, dirs_exist_ok=True)
os.makedirs(os.path.join(temp_dir, '3D'), exist_ok=True)
os.makedirs(os.path.join(temp_dir, 'Metadata'), exist_ok=True)
with tempfile.TemporaryDirectory() as temp_dir:
shutil.copytree(source_folder, temp_dir, dirs_exist_ok=True)
os.makedirs(os.path.join(temp_dir, '3D'), exist_ok=True)
os.makedirs(os.path.join(temp_dir, 'Metadata'), exist_ok=True)
write_model_xml(geoms, os.path.join(temp_dir, '3D', '3dmodel.model'))
write_model_xml(geoms, os.path.join(temp_dir, '3D', '3dmodel.model'))
write_metadata_xml(geoms, os.path.join(temp_dir, 'Metadata', 'Slic3r_PE_model.config'))
write_wipe_tower_xml(geoms, os.path.join(temp_dir, 'Metadata', 'Prusa_Slicer_wipe_tower_information.xml'))
write_z_gcodes(z_gcodes, os.path.join(temp_dir, 'Metadata', 'Prusa_Slicer_custom_gcode_per_print_z.xml'))
conf.write_ini_3mf(os.path.join(temp_dir, 'Metadata', 'Slic3r_PE.config'))
write_metadata_xml(geoms, os.path.join(temp_dir, 'Metadata', 'Slic3r_PE_model.config'))
write_wipe_tower_xml(geoms, os.path.join(temp_dir, 'Metadata', 'Prusa_Slicer_wipe_tower_information.xml'))
write_z_gcodes(z_gcodes, os.path.join(temp_dir, 'Metadata', 'Prusa_Slicer_custom_gcode_per_print_z.xml'))
conf.write_ini_3mf(os.path.join(temp_dir, 'Metadata', 'Slic3r_PE.config'))
to_3mf(temp_dir, filepath)
return None
to_3mf(temp_dir, filepath)

View file

@ -170,7 +170,7 @@ class TestTransform(unittest.TestCase):
# Define mock resolver to return our inputs
def mock_resolve(fieldname):
if fieldname == "Mesh":
if fieldname == "Geometry":
return sc
elif fieldname == "Translation":
return (10.0, 20.0, 30.0)
@ -184,7 +184,7 @@ class TestTransform(unittest.TestCase):
node.resolve = mock_resolve
# Evaluate output
result_sc = node.evaluate_output("Mesh")
result_sc = node.evaluate_output("Geometry")
self.assertIsNotNone(result_sc)
# Ensure it's not the same object (deep copied)
@ -338,13 +338,13 @@ class TestTransform(unittest.TestCase):
# 1. Transform Geometry
t_node = SLICENODE_TransformGeometry()
t_node.resolve = lambda f: {
"Mesh": sc,
"Geometry": sc,
"Translation": (10.0, 20.0, 0.0),
"Rotation": (0.0, 0.0, 0.0),
"Scale": (1.0, 1.0, 1.0)
}.get(f)
sc_transformed = t_node.evaluate_output("Mesh")
sc_transformed = t_node.evaluate_output("Geometry")
self.assertIsNotNone(sc_transformed)
self.assertEqual(len(sc_transformed.transforms), 1)
self.assertEqual(sc_transformed.transforms[0]["type"], "transform_geometry")

View file

@ -6,7 +6,33 @@ import os
import sys
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.append(str(PROJECT_ROOT))
sys.path.append(str(PROJECT_ROOT.parent))
from unittest.mock import MagicMock
class DummyBase: pass
mock_types = MagicMock()
mock_types.NodeSocket = type("NodeSocket", (DummyBase,), {})
mock_types.Node = type("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.Object = MagicMock
mock_types.Mesh = MagicMock
mock_bpy = MagicMock()
mock_bpy.types = mock_types
mock_bpy.path.abspath = lambda path: path.replace("//", "/abs/")
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.nodeslicer.nodes.inputs import SLICENODE_WipeTower, SLICENODE_OffsetObject
from NodeSlicer.infra.prusaslicer_profiles.models import ConfigWriter