diff --git a/infra/_3mf_writer/_3mf.py b/infra/_3mf_writer/_3mf.py
index 743e9de..071f45e 100644
--- a/infra/_3mf_writer/_3mf.py
+++ b/infra/_3mf_writer/_3mf.py
@@ -1,18 +1,21 @@
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
+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__))
-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 +31,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 +67,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'\n')
+ file.write('\n')
file.write(f'\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'\n')
- file.write(f'\n')
+ file.write('\n')
+ file.write('\n')
# Write metadata entries using list comprehension
metadata_entries: list[tuple[str, str]] = [
@@ -86,7 +99,7 @@ def write_model_xml(group: SlicingGroup, filename: str):
file.writelines([f' {value}\n' for name, value in metadata_entries])
# Write resources element and object using list comprehension
- file.write(f' \n')
+ file.write(' \n')
verts_template = np.vectorize(lambda x, y, z: '\n' % (x, y, z))
idx_template = np.vectorize(lambda a, b, c: '\n' % (a, b, c))
@@ -101,40 +114,41 @@ def write_model_xml(group: SlicingGroup, filename: str):
if uv.size and t_idx.size:
file.write(f' \n')
+ file.write(' \n')
+ file.write(' \n')
- file.write(f' \n')
+ file.write(' \n')
# Write build element
- file.write(f' \n')
- ox, oy, oz = group.object_offset[0], group.object_offset[1], group.object_offset[2]
+ file.write(' \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' \n' ])
- file.write(f' \n')
+ file.writelines([f' \n'])
+ file.write(' \n')
# Close the model tag
- file.write(f'\n')
+ file.write('\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 +165,116 @@ 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:
- source_folder = os.path.join(script_dir, 'prusaslicer_3mf')
- temp_dir = tempfile.mkdtemp()
- shutil.copytree(source_folder, temp_dir, dirs_exist_ok=True)
+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,
+ ]
- os.makedirs(os.path.join(temp_dir, '3D'), exist_ok=True)
- os.makedirs(os.path.join(temp_dir, 'Metadata'), exist_ok=True)
+ 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', []))
+ ]
- write_model_xml(geoms, os.path.join(temp_dir, '3D', '3dmodel.model'))
+ 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
+ }
- 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)
+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)
- return None
\ No newline at end of file
+
+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')
+ 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_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'))
+
+ 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)
+
+ to_3mf(temp_dir, filepath)
\ No newline at end of file