This commit is contained in:
Nicolas Predella 2026-08-26 12:04:35 +03:00
parent bedc33c7d3
commit 9d282dee01

View file

@ -1,18 +1,21 @@
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 try:
from ..blender.blender_mesh_capture import SlicingGroup, SlicingCollection
except ImportError:
from ..infra.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 +31,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 +67,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 +99,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 +114,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 +165,99 @@ 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 rgb_to_html_hex(rgb: tuple[float, float, float]) -> str:
r: int = int(round(rgb[0] * 255))
g: int = int(round(rgb[1] * 255))
b: int = int(round(rgb[2] * 255))
return "#{:02X}{:02X}{:02X}".format(r, g, b)
def generate_full_spectrum_data(pg: Any, cx: Any = None) -> dict:
physical_colors = [
pg.filament_color,
pg.filament_2_color,
pg.filament_3_color,
pg.filament_4_color,
pg.filament_5_color,
]
physical_extruders = [
{
"color": rgb_to_html_hex(color),
"id": i + 1
}
for i, color in enumerate(physical_colors)
]
virtual_extruders = []
if cx is not None:
try:
from ..blender.blender_bridge import get_inherited_virtual_extruders
from ..blender.blender_mesh_capture import TYPES_NAME
except ImportError:
from ..infra.blender_bridge import get_inherited_virtual_extruders
from .. import TYPES_NAME
ve_list = get_inherited_virtual_extruders(cx, TYPES_NAME)
else:
ve_list = [
{'id': 6 + idx, 'ratios': list(item.ratios), 'inherited': False}
for idx, item in enumerate(getattr(pg, 'virtual_extruders', []))
]
for ve_item in ve_list:
idx_id = ve_item['id']
raw_ratios = list(ve_item['ratios'])
active = [(i + 1, r) for i, r in enumerate(raw_ratios) if r > 0.0]
if len(active) > 3:
active = sorted(active, key=lambda x: x[1], reverse=True)[:3]
active.sort(key=lambda x: x[0])
total_sum = sum(r for _, r in active)
if total_sum > 1.0:
normalized = [(ext, r / total_sum) for ext, r in active]
else:
normalized = active
components = [
{"extruder": ext, "ratio": round(r, 5)}
for ext, r in normalized
]
if total_sum > 0:
calc_norm = total_sum if total_sum > 1.0 else total_sum
r_mix = sum(r * physical_colors[ext - 1][0] for ext, r in normalized) / calc_norm
g_mix = sum(r * physical_colors[ext - 1][1] for ext, r in normalized) / calc_norm
b_mix = sum(r * physical_colors[ext - 1][2] for ext, r in normalized) / calc_norm
virt_color = rgb_to_html_hex((r_mix, g_mix, b_mix)).lower()
else:
virt_color = "#000000"
virtual_extruders.append({
"color": virt_color,
"components": components,
"id": idx_id,
"kind": "fullspectrum"
})
return {
"physical_extruders": physical_extruders,
"version": 1,
"virtual_extruders": virtual_extruders
}
def write_full_spectrum_json(pg: Any, filepath: str | Path, cx: Any = None) -> None:
import json
data = generate_full_spectrum_data(pg, cx=cx)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4)
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)
@ -167,6 +270,11 @@ def prepare_3mf(filepath: Path, geoms: SlicingGroup, conf, z_gcodes) -> None:
write_z_gcodes(z_gcodes, os.path.join(temp_dir, 'Metadata', 'Prusa_Slicer_custom_gcode_per_print_z.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')) conf.write_ini_3mf(os.path.join(temp_dir, 'Metadata', 'Slic3r_PE.config'))
to_3mf(temp_dir, filepath) if pg is not None:
full_spec = generate_full_spectrum_data(pg, cx=cx)
if len(full_spec["virtual_extruders"]) > 0:
import json
with open(os.path.join(temp_dir, 'Metadata', 'Prusa_Slicer_full_spectrum.json'), 'w', encoding='utf-8') as f:
json.dump(full_spec, f, indent=4)
return None to_3mf(temp_dir, filepath)