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") self.add_output("SLICESOCK_Geometry", "Geometry")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name not in ("Geometry"): if socket_name not in ("Geometry",):
return None return None
geom_data: Any = self.resolve("Geometry") geom_data: Any = self.resolve("Geometry")
@ -835,6 +835,3 @@ 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)

View file

@ -1,18 +1,18 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
from numpy.typing import NDArray
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
import os, shutil, tempfile import os, shutil, tempfile
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from datetime import date 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__)) 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 # Custom sorting order for object types
object_type_order = { object_type_order = {
'ModelPart': 0, 'ModelPart': 0,
@ -28,23 +28,31 @@ def write_metadata_xml(group: SlicingGroup, filepath):
for j, (k, collection) in enumerate(valid_collections.items()): 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") 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) ET.SubElement(object_elem, "metadata", type="object", key="name", value=collection.name)
for i, (start, end, metadata) in enumerate(sorted_data): 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)) 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) ET.SubElement(volume_elem, "metadata", type="volume", key="name", value=metadata.name)
if metadata.object_type == "ParameterModifier": if metadata.object_type == "ParameterModifier":
ET.SubElement(volume_elem, "metadata", type="volume", key="modifier", value="1") ET.SubElement(volume_elem, "metadata", type="volume", key="modifier", value="1")
if metadata.object_type in ["ModelPart", "ParameterModifier"]:
for mod in metadata.modifiers: 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=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="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="extruder", value=str(metadata.extruder))
ET.SubElement(volume_elem, "metadata", type="volume", key="source_object_id", value="0") 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 = ET.ElementTree(xml_content)
xml_tree.write(filepath, encoding="UTF-8", xml_declaration=True) 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: 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') 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() now = date.today().isoformat()
# Open file for writing # Open file for writing
with open(filename, 'w', encoding="UTF-8") as file: with open(filename, 'w', encoding="UTF-8") as file:
# Write the XML declaration and opening model tag # Write the XML declaration and opening model tag
file.write(f'<?xml version="1.0" encoding="UTF-8"?>\n') file.write('<?xml version="1.0" encoding="UTF-8"?>\n')
file.write(f'<model xmlns="" unit="millimeter" xml:lang="en-US" xmlns:slic3rpe="">\n') file.write('<model xmlns="" unit="millimeter" xml:lang="en-US" xmlns:slic3rpe="">\n')
# Write metadata entries using list comprehension # Write metadata entries using list comprehension
metadata_entries: list[tuple[str, str]] = [ 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]) file.writelines([f' <metadata name="{name}">{value}</metadata>\n' for name, value in metadata_entries])
# Write resources element and object using list comprehension # 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)) 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)) 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: if uv.size and t_idx.size:
file.write(f' <object id="{str(i+1)}" type="model">\n') 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.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.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(' </mesh>\n')
file.write(f' </object>\n') file.write(' </object>\n')
file.write(f' </resources>\n') file.write(' </resources>\n')
# Write build element # Write build element
file.write(f' <build>\n') file.write(' <build>\n')
ox, oy, oz = group.object_offset[0], group.object_offset[1], group.object_offset[2] 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}" transform_str = f"1 0 0 0 1 0 0 0 1 {ox} {oy} {oz}"
for i, k in enumerate(valid_collections): for i, k in enumerate(valid_collections):
if not valid_collections[k].unique_verts[0].size: continue 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.writelines([f' <item objectid="{str(i+1)}" transform="{transform_str}" printable="1" />\n'])
file.write(f' </build>\n') file.write(' </build>\n')
# Close the model tag # 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) 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' new_file_path = os.path.splitext(zip_file_path)[0] + '.3mf'
os.replace(zip_file_path, new_file_path) 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") root = ET.Element("custom_gcodes_per_print_z", bed_idx="0")
for c in z_gcodes: for c in z_gcodes:
@ -151,10 +162,10 @@ def write_z_gcodes(z_gcodes, filename: str):
ET.ElementTree(root).write(filename, encoding="utf-8", xml_declaration=True) 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') source_folder = os.path.join(script_dir, 'prusaslicer_3mf')
temp_dir = tempfile.mkdtemp() with tempfile.TemporaryDirectory() as temp_dir:
shutil.copytree(source_folder, temp_dir, dirs_exist_ok=True) 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, '3D'), exist_ok=True)
@ -168,5 +179,3 @@ def prepare_3mf(filepath: Path, geoms: SlicingGroup, conf, z_gcodes) -> None:
conf.write_ini_3mf(os.path.join(temp_dir, 'Metadata', 'Slic3r_PE.config')) conf.write_ini_3mf(os.path.join(temp_dir, 'Metadata', 'Slic3r_PE.config'))
to_3mf(temp_dir, filepath) to_3mf(temp_dir, filepath)
return None

View file

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

View file

@ -6,7 +6,33 @@ import os
import sys import sys
PROJECT_ROOT = Path(__file__).resolve().parent.parent 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.core.nodeslicer.nodes.inputs import SLICENODE_WipeTower, SLICENODE_OffsetObject
from NodeSlicer.infra.prusaslicer_profiles.models import ConfigWriter from NodeSlicer.infra.prusaslicer_profiles.models import ConfigWriter