Refactor multi-socket group system and fix G-code generation metadata synchronization
This commit is contained in:
parent
5f49097f80
commit
5aad206c48
5 changed files with 168 additions and 118 deletions
|
|
@ -357,12 +357,12 @@ class SLICENODE_GetPaths(SliceNode):
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||||
obj: Any = self.resolve("Object")
|
obj: Any = self.resolve("Object")
|
||||||
attr_name: str = self.resolve("Extrusion Multiplier Attribute")
|
e_attr_name: str = self.resolve("Extrusion Multiplier Attribute")
|
||||||
f_attr_name: str = self.resolve("Feedrate Attribute")
|
f_attr_name: str = self.resolve("Feedrate Attribute")
|
||||||
extra_attrs: dict[str, str] = self.resolve("Extra Attributes")
|
extra_attrs: dict[str, str] = self.resolve("Extra Attributes")
|
||||||
|
|
||||||
from ...infra.blender.blender_mesh_capture import capture_mesh_edges
|
from ...infra.blender.blender_mesh_capture import capture_mesh_edges
|
||||||
so, e_rates, f_rates, extra_attrs_data = capture_mesh_edges(obj, attr_name=attr_name, f_attr_name=f_attr_name, extra_attrs=extra_attrs)
|
so, e_rates, f_rates, extra_attrs_data = capture_mesh_edges(obj, e_attr_name, f_attr_name, extra_attrs)
|
||||||
|
|
||||||
return SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
|
return SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
|
||||||
|
|
||||||
|
|
@ -723,7 +723,7 @@ class SliceNodeJoiner(SliceNode):
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_output(self.output_socket_type or self.socket_type)
|
self.add_output(self.output_socket_type or self.socket_type)
|
||||||
self.add_multi_input(self.socket_type)
|
self.add_multi_socket_group(self.socket_type)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_JoinGcode(SliceNodeJoiner):
|
class SLICENODE_JoinGcode(SliceNodeJoiner):
|
||||||
|
|
@ -997,7 +997,7 @@ class SLICENODE_GcodeToFile(ExecSliceNode):
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self._init_exec()
|
self._init_exec()
|
||||||
self.add_input("SLICESOCK_DirPath", "Folder")
|
self.add_input("SLICESOCK_DirPath", "Folder")
|
||||||
self.add_multi_input("SLICESOCK_Gcode", "Gcode")
|
self.add_multi_socket_group("SLICESOCK_Gcode", name="Gcode")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
super().draw_buttons(context, layout)
|
super().draw_buttons(context, layout)
|
||||||
|
|
@ -1156,7 +1156,7 @@ class ExecSliceNode_DatabaseSubmit(ExecSliceNode):
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self._init_exec()
|
self._init_exec()
|
||||||
self.add_multi_input("SLICESOCK_Gcode", "Gcode")
|
self.add_multi_socket_group("SLICESOCK_Gcode", name="Gcode")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
super().draw_buttons(context, layout)
|
super().draw_buttons(context, layout)
|
||||||
|
|
@ -1176,7 +1176,6 @@ class ExecSliceNode_DatabaseSubmit(ExecSliceNode):
|
||||||
import json
|
import json
|
||||||
|
|
||||||
if not gc:
|
if not gc:
|
||||||
if operator: operator.report({'ERROR'}, "No Gcode to send")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Parse Metadata from Gcode
|
# Parse Metadata from Gcode
|
||||||
|
|
@ -1250,10 +1249,8 @@ class ExecSliceNode_DatabaseSubmit(ExecSliceNode):
|
||||||
|
|
||||||
record_id = self._submit_to_db(token, fields, temp_path)
|
record_id = self._submit_to_db(token, fields, temp_path)
|
||||||
if operator: operator.report({'INFO'}, f"Submitted to {self._db_name} (Record: {record_id})")
|
if operator: operator.report({'INFO'}, f"Submitted to {self._db_name} (Record: {record_id})")
|
||||||
return 'ok'
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if operator: operator.report({'ERROR'}, f"{self._db_name} Error: {str(e)}")
|
if operator: operator.report({'ERROR'}, f"{self._db_name} Error: {str(e)}")
|
||||||
return 'error'
|
|
||||||
finally:
|
finally:
|
||||||
if temp_path.exists():
|
if temp_path.exists():
|
||||||
temp_path.unlink()
|
temp_path.unlink()
|
||||||
|
|
@ -1344,37 +1341,39 @@ class SLICENODE_PrusaslicerSlice(SliceNode):
|
||||||
bl_label = "PrusaSlicer Slice"
|
bl_label = "PrusaSlicer Slice"
|
||||||
|
|
||||||
prusaslicer_executable: NfStringProperty(name="PrusaSlicer Path", subtype='FILE_PATH', default='flatpak run com.prusa3d.PrusaSlicer') # pyright: ignore[reportInvalidTypeForm]
|
prusaslicer_executable: NfStringProperty(name="PrusaSlicer Path", subtype='FILE_PATH', default='flatpak run com.prusa3d.PrusaSlicer') # pyright: ignore[reportInvalidTypeForm]
|
||||||
gcode_result: NfStringProperty(name="Gcode Result", default="") # pyright: ignore[reportInvalidTypeForm]
|
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Mesh", "Mesh")
|
|
||||||
self.add_input("SLICESOCK_PrusaslicerConf", "Config")
|
self.add_input("SLICESOCK_PrusaslicerConf", "Config")
|
||||||
self.add_input("SLICESOCK_Settings", "Settings")
|
self.add_input("SLICESOCK_Settings", "Settings")
|
||||||
self.add_output("SLICESOCK_GcodeRo")
|
self.add_multi_socket_group("SLICESOCK_Mesh", "SLICESOCK_GcodeRo", name="Slicing Group")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
assert layout
|
assert layout
|
||||||
layout.prop(self, "prusaslicer_executable")
|
layout.prop(self, "prusaslicer_executable")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, identifier: str = "", **kwargs) -> Any:
|
||||||
if socket_name == "Gcode":
|
if identifier.startswith("Slicing Group_ms_out_"):
|
||||||
self.evaluate()
|
try:
|
||||||
return self.gcode_result
|
idx = int(identifier.split("_")[-1])
|
||||||
|
return self.evaluate_for_index(idx)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return ""
|
||||||
return super().evaluate_output(socket_name, **kwargs)
|
return super().evaluate_output(socket_name, **kwargs)
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
def evaluate_for_index(self, index: int, operator=None) -> str:
|
||||||
self.gcode_result = ""
|
mesh_groups: list[SlicingCollection | SlicingGroup] = self.resolve_multi_input("Slicing Group")
|
||||||
mesh_data: SlicingCollection | SlicingGroup = self.resolve("Mesh")
|
if index >= len(mesh_groups):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
mesh_data = mesh_groups[index]
|
||||||
|
if not mesh_data:
|
||||||
|
return ""
|
||||||
|
|
||||||
conf_data = self.resolve("Config")
|
conf_data = self.resolve("Config")
|
||||||
overrides = self.resolve("Settings")
|
overrides = self.resolve("Settings")
|
||||||
|
|
||||||
if not mesh_data:
|
|
||||||
if operator: operator.report({'ERROR'}, "No Mesh data connected")
|
|
||||||
return 'no object'
|
|
||||||
if not conf_data:
|
if not conf_data:
|
||||||
if operator: operator.report({'ERROR'}, "No Config connected")
|
return ""
|
||||||
return 'no config'
|
|
||||||
|
|
||||||
# Extract data from config dict
|
# Extract data from config dict
|
||||||
printer_id = conf_data['printer']
|
printer_id = conf_data['printer']
|
||||||
|
|
@ -1388,7 +1387,7 @@ class SLICENODE_PrusaslicerSlice(SliceNode):
|
||||||
else:
|
else:
|
||||||
slicing_group = SlicingGroup([])
|
slicing_group = SlicingGroup([])
|
||||||
slicing_group.collections = {mesh_data.objects[0].name: mesh_data}
|
slicing_group.collections = {mesh_data.objects[0].name: mesh_data}
|
||||||
|
|
||||||
# Offset by the bed center
|
# Offset by the bed center
|
||||||
def parse_bed_center(bed_shape_str: str) -> np.ndarray:
|
def parse_bed_center(bed_shape_str: str) -> np.ndarray:
|
||||||
try:
|
try:
|
||||||
|
|
@ -1403,13 +1402,12 @@ class SLICENODE_PrusaslicerSlice(SliceNode):
|
||||||
|
|
||||||
bed_shape = cache.profiles[printer_id].all_conf_dict.get('bed_shape', '0x0,250x0,250x210,0x210')
|
bed_shape = cache.profiles[printer_id].all_conf_dict.get('bed_shape', '0x0,250x0,250x210,0x210')
|
||||||
bed_center = parse_bed_center(bed_shape)
|
bed_center = parse_bed_center(bed_shape)
|
||||||
|
|
||||||
a_z = -slicing_group.min_z
|
a_z = -slicing_group.min_z
|
||||||
offset = np.array([bed_center[0], bed_center[1], a_z]) - np.array([slicing_group.center_xy[0], slicing_group.center_xy[1], 0])
|
offset = np.array([bed_center[0], bed_center[1], a_z]) - np.array([slicing_group.center_xy[0], slicing_group.center_xy[1], 0])
|
||||||
slicing_group.offset(offset)
|
slicing_group.offset(offset)
|
||||||
|
|
||||||
# Prepare overrides for generate_conf_writer
|
# Prepare overrides for generate_conf_writer
|
||||||
# The format expected by generate_conf_writer is overrides={'key': {'value': 'val'}}
|
|
||||||
writer_overrides = {k: {'value': v} for k, v in overrides.items()}
|
writer_overrides = {k: {'value': v} for k, v in overrides.items()}
|
||||||
|
|
||||||
# Generate the ConfigWriter
|
# Generate the ConfigWriter
|
||||||
|
|
@ -1426,72 +1424,49 @@ class SLICENODE_PrusaslicerSlice(SliceNode):
|
||||||
|
|
||||||
# Prepare 3mf path
|
# Prepare 3mf path
|
||||||
temp_dir = Path(tempfile.gettempdir())
|
temp_dir = Path(tempfile.gettempdir())
|
||||||
output_path = temp_dir / f"tmp.3mf"
|
output_path = temp_dir / f"tmp_{index}.3mf"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ...infra._3mf_writer._3mf import prepare_3mf
|
from ...infra._3mf_writer._3mf import prepare_3mf
|
||||||
prepare_3mf(output_path, slicing_group, conf_writer, z_gcodes=[])
|
prepare_3mf(output_path, slicing_group, conf_writer, z_gcodes=[])
|
||||||
|
|
||||||
# Actual slicing
|
|
||||||
exe_str = self.prusaslicer_executable
|
exe_str = self.prusaslicer_executable
|
||||||
if not exe_str:
|
if not exe_str: return ""
|
||||||
if operator: operator.report({'ERROR'}, "PrusaSlicer executable path is empty")
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
# Parse command (handles multi-word commands like flatpak)
|
|
||||||
cmd_parts = shlex.split(exe_str)
|
cmd_parts = shlex.split(exe_str)
|
||||||
if not cmd_parts:
|
if not cmd_parts: return ""
|
||||||
if operator: operator.report({'ERROR'}, "Invalid PrusaSlicer command")
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
# Try to resolve the first part as a path if it's not a command in PATH
|
|
||||||
if not shutil.which(cmd_parts[0]):
|
if not shutil.which(cmd_parts[0]):
|
||||||
abs_path = bpy.path.abspath(cmd_parts[0])
|
abs_path = bpy.path.abspath(cmd_parts[0])
|
||||||
if os.path.exists(abs_path):
|
if os.path.exists(abs_path):
|
||||||
cmd_parts[0] = abs_path
|
cmd_parts[0] = abs_path
|
||||||
|
|
||||||
# Final check if command exists
|
|
||||||
if not shutil.which(cmd_parts[0]):
|
if not shutil.which(cmd_parts[0]):
|
||||||
if operator: operator.report({'ERROR'}, f"Command or executable not found: {cmd_parts[0]}")
|
return ""
|
||||||
self.gcode_result = ""
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
gcode_path = output_path.with_suffix(".gcode")
|
gcode_path = output_path.with_suffix(".gcode")
|
||||||
|
|
||||||
# Command: <exe> --export-gcode <3mf_path> --output <gcode_path>
|
|
||||||
cmd = cmd_parts + ["--export-gcode", str(output_path), "--output", str(gcode_path)]
|
cmd = cmd_parts + ["--export-gcode", str(output_path), "--output", str(gcode_path)]
|
||||||
|
|
||||||
# Run PrusaSlicer
|
|
||||||
process = subprocess.run(cmd, capture_output=True, text=True)
|
process = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
if operator: operator.report({'ERROR'}, f"PrusaSlicer failed: {process.stderr}")
|
return ""
|
||||||
self.gcode_result = ""
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
# Read G-code
|
|
||||||
if gcode_path.exists():
|
if gcode_path.exists():
|
||||||
with open(gcode_path, 'r', encoding='utf-8') as f:
|
with open(gcode_path, 'r', encoding='utf-8') as f:
|
||||||
self.gcode_result = f.read()
|
gcode_result = f.read()
|
||||||
self.gcode_result += f"\n; binary_gcode = {binary_gcode_value}\n"
|
gcode_result += f"\n; binary_gcode = {binary_gcode_value}\n"
|
||||||
|
|
||||||
output_path.unlink()
|
output_path.unlink()
|
||||||
|
if gcode_path.exists(): gcode_path.unlink()
|
||||||
if operator:
|
|
||||||
operator.report({'INFO'}, f"Slicing complete. G-code generated.")
|
return gcode_result
|
||||||
else:
|
else:
|
||||||
if operator: operator.report({'ERROR'}, "G-code file was not generated by PrusaSlicer")
|
return ""
|
||||||
self.gcode_result = ""
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.gcode_result = ""
|
|
||||||
if operator: operator.report({'ERROR'}, f"Failed to slice: {str(e)}")
|
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return 'error'
|
return ""
|
||||||
|
|
||||||
return 'ok'
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_PrusaslicerPreview(ExecSliceNode):
|
class SLICENODE_PrusaslicerPreview(ExecSliceNode):
|
||||||
bl_label = "PrusaSlicer Preview"
|
bl_label = "PrusaSlicer Preview"
|
||||||
|
|
@ -1595,7 +1570,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
last_e = 0.0
|
last_e = 0.0
|
||||||
|
|
||||||
offset_x, offset_y = 0.0, 0.0
|
offset_x, offset_y = 0.0, 0.0
|
||||||
default_extrusion_rate = 0.017 # Default fallback
|
custom_default_extrusion_rate = 0.0
|
||||||
|
|
||||||
last_v_idx = -1
|
last_v_idx = -1
|
||||||
last_p2 = None
|
last_p2 = None
|
||||||
|
|
@ -1614,7 +1589,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
try: offset_y = float(comment.split('=')[1])
|
try: offset_y = float(comment.split('=')[1])
|
||||||
except ValueError: pass
|
except ValueError: pass
|
||||||
elif comment.startswith("DEFAULT_EXTRUSION_RATE="):
|
elif comment.startswith("DEFAULT_EXTRUSION_RATE="):
|
||||||
try: default_extrusion_rate = float(comment.split('=')[1])
|
try: custom_default_extrusion_rate = float(comment.split('=')[1])
|
||||||
except ValueError: pass
|
except ValueError: pass
|
||||||
|
|
||||||
line_no_comment = line.split(';')[0].strip()
|
line_no_comment = line.split(';')[0].strip()
|
||||||
|
|
@ -1651,8 +1626,8 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
|
|
||||||
# Calculate e_rate (proportion between E and default_extrusion_rate)
|
# Calculate e_rate (proportion between E and default_extrusion_rate)
|
||||||
seg_len_mm = segment_length_3d(p1, p2) * 1000.0
|
seg_len_mm = segment_length_3d(p1, p2) * 1000.0
|
||||||
if seg_len_mm > 0 and default_extrusion_rate > 0:
|
if seg_len_mm > 0 and custom_default_extrusion_rate > 0:
|
||||||
e_rate = (new_e / seg_len_mm) / default_extrusion_rate
|
e_rate = (new_e / seg_len_mm) / custom_default_extrusion_rate
|
||||||
else:
|
else:
|
||||||
e_rate = 1.0
|
e_rate = 1.0
|
||||||
|
|
||||||
|
|
@ -1744,7 +1719,7 @@ class SLICENODE_Executor(ExecSliceNode, SliceNodeJoiner):
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self._init_exec()
|
self._init_exec()
|
||||||
self.add_multi_input(self.socket_type, "Links")
|
self.add_multi_socket_group(self.socket_type, name="Links")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
super().draw_buttons(context, layout)
|
super().draw_buttons(context, layout)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import CollectionProperty
|
from bpy.props import CollectionProperty
|
||||||
from bpy.types import Property, PropertyGroup, bpy_struct
|
|
||||||
from .bpy_ns_types import NfStringProperty, NfFloatProperty, NfIntProperty, NfEnumProperty, NfPointerProperty, NfFloatVectorProperty
|
from .bpy_ns_types import NfStringProperty, NfFloatProperty, NfIntProperty, NfEnumProperty, NfPointerProperty, NfFloatVectorProperty
|
||||||
|
|
||||||
from ...registry import register_class
|
from ...registry import register_class
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from numpy import float64
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyclipper
|
import pyclipper
|
||||||
from typing import Any
|
from typing import Any, Literal
|
||||||
|
|
||||||
from .geometry import ndarray_lru_cache
|
from .geometry import ndarray_lru_cache
|
||||||
|
|
||||||
|
|
@ -111,13 +112,42 @@ class SlicePaths(list[np.ndarray]):
|
||||||
if isinstance(other, SlicePaths):
|
if isinstance(other, SlicePaths):
|
||||||
self.extend(other)
|
self.extend(other)
|
||||||
self.groups.extend(other.groups)
|
self.groups.extend(other.groups)
|
||||||
|
self.extrusion_rate.extend(other.extrusion_rate)
|
||||||
|
self.feedrate.extend(other.feedrate)
|
||||||
|
for k, v in other.extra_attributes.items():
|
||||||
|
if k in self.extra_attributes:
|
||||||
|
self.extra_attributes[k].extend(v)
|
||||||
|
else:
|
||||||
|
# Pad with zeros for existing paths if we didn't have this attribute
|
||||||
|
self.extra_attributes[k] = [np.zeros(len(p)) for p in self[:-len(other)]] + list(v)
|
||||||
|
# Pad existing attributes if 'other' didn't have them
|
||||||
|
for k in self.extra_attributes:
|
||||||
|
if k not in other.extra_attributes:
|
||||||
|
self.extra_attributes[k].extend([np.zeros(len(p)) for p in other])
|
||||||
else:
|
else:
|
||||||
self.extend(other)
|
self.extend(other)
|
||||||
self.groups.extend(list(range(0,len(other))))
|
self.groups.extend(list(range(0,len(other))))
|
||||||
|
self.extrusion_rate.extend([np.ones(len(p)) for p in other])
|
||||||
|
self.feedrate.extend([np.zeros(len(p)) for p in other])
|
||||||
|
for k in self.extra_attributes:
|
||||||
|
self.extra_attributes[k].extend([np.zeros(len(p)) for p in other])
|
||||||
|
|
||||||
def append_with_group(self, path: np.ndarray, group: int = 0):
|
def append_with_group(self, path: np.ndarray, group: int = 0, extrusion_rate: np.ndarray | None = None, feedrate: np.ndarray | None = None, extra_attributes: dict[str, np.ndarray] | None = None):
|
||||||
self.append(path)
|
self.append(path)
|
||||||
self.groups.append(group)
|
self.groups.append(group)
|
||||||
|
self.extrusion_rate.append(extrusion_rate if extrusion_rate is not None else np.ones(len(path)))
|
||||||
|
self.feedrate.append(feedrate if feedrate is not None else np.zeros(len(path)))
|
||||||
|
|
||||||
|
extra_attributes = extra_attributes or {}
|
||||||
|
for k, v in extra_attributes.items():
|
||||||
|
if k in self.extra_attributes:
|
||||||
|
self.extra_attributes[k].append(v)
|
||||||
|
else:
|
||||||
|
self.extra_attributes[k] = [np.zeros(len(p)) for p in self[:-1]] + [v]
|
||||||
|
|
||||||
|
for k in self.extra_attributes:
|
||||||
|
if k not in extra_attributes:
|
||||||
|
self.extra_attributes[k].append(np.zeros(len(path)))
|
||||||
|
|
||||||
def slice_at_range(models, z_values) -> list[list[np.ndarray]]:
|
def slice_at_range(models, z_values) -> list[list[np.ndarray]]:
|
||||||
if not models:
|
if not models:
|
||||||
|
|
@ -539,9 +569,26 @@ class Pather(object):
|
||||||
|
|
||||||
def __init__(self, paths: list[np.ndarray], **kwargs) -> None:
|
def __init__(self, paths: list[np.ndarray], **kwargs) -> None:
|
||||||
self.paths = paths
|
self.paths = paths
|
||||||
self.extrusion_rates = getattr(paths, "extrusion_rate", [np.ones(len(p)) for p in paths])
|
|
||||||
self.feedrates = getattr(paths, "feedrate", [np.zeros(len(p)) for p in paths])
|
# Robust initialization: ensure metadata lists match paths length
|
||||||
self.extra_attributes = getattr(paths, "extra_attributes", {})
|
e_rate = getattr(paths, "extrusion_rate", None)
|
||||||
|
if e_rate is not None and len(e_rate) == len(paths):
|
||||||
|
self.extrusion_rates = e_rate
|
||||||
|
else:
|
||||||
|
self.extrusion_rates = [np.ones(len(p)) for p in paths]
|
||||||
|
|
||||||
|
f_rate = getattr(paths, "feedrate", None)
|
||||||
|
if f_rate is not None and len(f_rate) == len(paths):
|
||||||
|
self.feedrates = f_rate
|
||||||
|
else:
|
||||||
|
self.feedrates = [np.zeros(len(p)) for p in paths]
|
||||||
|
|
||||||
|
extra = getattr(paths, "extra_attributes", None)
|
||||||
|
if extra is not None and all(len(v) == len(paths) for v in extra.values()):
|
||||||
|
self.extra_attributes = extra
|
||||||
|
else:
|
||||||
|
self.extra_attributes = {}
|
||||||
|
|
||||||
self.bed_center_x: float = 250.
|
self.bed_center_x: float = 250.
|
||||||
self.bed_center_y: float = 250.
|
self.bed_center_y: float = 250.
|
||||||
self.feedrate: float = 1200.
|
self.feedrate: float = 1200.
|
||||||
|
|
@ -599,8 +646,8 @@ class Pather(object):
|
||||||
if len(coords) < 2:
|
if len(coords) < 2:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
e_rates = self.extrusion_rates[i]
|
e_rate = self.extrusion_rates[i]
|
||||||
f_rates = self.feedrates[i]
|
f_rate = self.feedrates[i]
|
||||||
extras = {prefix: vals[i] for prefix, vals in self.extra_attributes.items()}
|
extras = {prefix: vals[i] for prefix, vals in self.extra_attributes.items()}
|
||||||
|
|
||||||
x0, y0, z0 = coords[0]
|
x0, y0, z0 = coords[0]
|
||||||
|
|
@ -618,10 +665,10 @@ class Pather(object):
|
||||||
old_extras = {prefix: None for prefix in extras}
|
old_extras = {prefix: None for prefix in extras}
|
||||||
for j, (p1, p2) in enumerate(zip(coords, coords[1:])):
|
for j, (p1, p2) in enumerate(zip(coords, coords[1:])):
|
||||||
seg_len: float = segment_length_3d(p1, p2)
|
seg_len: float = segment_length_3d(p1, p2)
|
||||||
e_segment = seg_len * self.extrusion_rate * e_rates[j]
|
e_segment = seg_len * self.extrusion_rate * e_rate[j]
|
||||||
|
|
||||||
# Per-segment feedrate value (not multiplier)
|
# Per-segment feedrate value (not multiplier)
|
||||||
fr = f_rates[j] if f_rates[j] > 0 else self.feedrate
|
fr = f_rate[j] if f_rate[j] > 0 else self.feedrate
|
||||||
|
|
||||||
old_x, old_y, old_z = p1
|
old_x, old_y, old_z = p1
|
||||||
x, y, z = p2
|
x, y, z = p2
|
||||||
|
|
|
||||||
|
|
@ -410,7 +410,7 @@ def _cached_capture_mesh_edges(
|
||||||
mesh_name: str,
|
mesh_name: str,
|
||||||
matrix_tuple: tuple[float, ...],
|
matrix_tuple: tuple[float, ...],
|
||||||
scene_scale: float,
|
scene_scale: float,
|
||||||
attr_name: str = "e_mult",
|
e_attr_name: str = "e_mult",
|
||||||
f_attr_name: str = "f_rate",
|
f_attr_name: str = "f_rate",
|
||||||
extra_attrs: dict[str, str] | None = None
|
extra_attrs: dict[str, str] | None = None
|
||||||
) -> tuple[list[NDArray[float64]], list[NDArray[float64]], list[NDArray[float64]], dict[str, list[NDArray[float64]]]]:
|
) -> tuple[list[NDArray[float64]], list[NDArray[float64]], list[NDArray[float64]], dict[str, list[NDArray[float64]]]]:
|
||||||
|
|
@ -438,8 +438,8 @@ def _cached_capture_mesh_edges(
|
||||||
mesh.vertices.foreach_get("co", verts.ravel())
|
mesh.vertices.foreach_get("co", verts.ravel())
|
||||||
|
|
||||||
e_mults = np.ones(num_verts, dtype=np.float64)
|
e_mults = np.ones(num_verts, dtype=np.float64)
|
||||||
if attr_name in mesh.attributes:
|
if e_attr_name in mesh.attributes:
|
||||||
mesh.attributes[attr_name].data.foreach_get("value", e_mults)
|
mesh.attributes[e_attr_name].data.foreach_get("value", e_mults)
|
||||||
|
|
||||||
f_rates = np.zeros(num_verts, dtype=np.float64)
|
f_rates = np.zeros(num_verts, dtype=np.float64)
|
||||||
if f_attr_name in mesh.attributes:
|
if f_attr_name in mesh.attributes:
|
||||||
|
|
@ -496,7 +496,7 @@ def _cached_capture_mesh_edges(
|
||||||
if hasattr(obj_eval, "to_mesh_clear"):
|
if hasattr(obj_eval, "to_mesh_clear"):
|
||||||
obj_eval.to_mesh_clear()
|
obj_eval.to_mesh_clear()
|
||||||
|
|
||||||
def capture_mesh_edges(obj: Object, attr_name: str = "e_mult", f_attr_name: str = "f_rate", extra_attrs: dict[str, str] | None = None) -> tuple[list[NDArray[float64]], list[NDArray[float64]], list[NDArray[float64]], dict[str, list[NDArray[float64]]]]:
|
def capture_mesh_edges(obj: Object, e_attr_name: str = "e_mult", f_attr_name: str = "f_rate", extra_attrs: dict[str, str] | None = None) -> tuple[list[NDArray[float64]], list[NDArray[float64]], list[NDArray[float64]], dict[str, list[NDArray[float64]]]]:
|
||||||
if not obj or not obj.data: return [], [], [], {}
|
if not obj or not obj.data: return [], [], [], {}
|
||||||
|
|
||||||
if obj.mode == 'EDIT':
|
if obj.mode == 'EDIT':
|
||||||
|
|
@ -513,7 +513,7 @@ def capture_mesh_edges(obj: Object, attr_name: str = "e_mult", f_attr_name: str
|
||||||
obj.data.name,
|
obj.data.name,
|
||||||
matrix_tuple,
|
matrix_tuple,
|
||||||
scene_scale,
|
scene_scale,
|
||||||
attr_name,
|
e_attr_name,
|
||||||
f_attr_name,
|
f_attr_name,
|
||||||
extra_attrs
|
extra_attrs
|
||||||
)
|
)
|
||||||
|
|
@ -27,7 +27,7 @@ class SliceNodeSocket(NodeSocket, AutoBlIdName):
|
||||||
from ...registry import get_label_by_name
|
from ...registry import get_label_by_name
|
||||||
class SliceNode(Node, AutoBlIdName):
|
class SliceNode(Node, AutoBlIdName):
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
self._update_multi_inputs()
|
self._update_multi_groups()
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ...
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ...
|
||||||
|
|
||||||
|
|
@ -36,50 +36,71 @@ class SliceNode(Node, AutoBlIdName):
|
||||||
visited = getattr(self, "_current_visited", None)
|
visited = getattr(self, "_current_visited", None)
|
||||||
return resolve_input(self.inputs.get(fieldname), cache=cache, visited=visited)
|
return resolve_input(self.inputs.get(fieldname), cache=cache, visited=visited)
|
||||||
|
|
||||||
def add_output(self, bl_idname: str, name: str = "", identifier: str = ""):
|
def add_output(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False):
|
||||||
eval_name = name or get_label_by_name(bl_idname)
|
eval_name = name or get_label_by_name(bl_idname)
|
||||||
return self.outputs.new(bl_idname, eval_name, identifier=identifier or eval_name)
|
sock = self.outputs.new(bl_idname, eval_name, identifier=identifier or eval_name)
|
||||||
|
sock.hide = hide
|
||||||
|
return sock
|
||||||
|
|
||||||
def add_input(self, bl_idname: str, name: str = "", identifier: str = ""):
|
def add_input(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False):
|
||||||
eval_name = name or get_label_by_name(bl_idname)
|
eval_name = name or get_label_by_name(bl_idname)
|
||||||
return self.inputs.new(bl_idname, eval_name, identifier=identifier or eval_name)
|
sock = self.inputs.new(bl_idname, eval_name, identifier=identifier or eval_name)
|
||||||
|
sock.hide = hide
|
||||||
|
return sock
|
||||||
|
|
||||||
def add_multi_input(self, bl_idname: str, name: str = "") -> None:
|
def add_multi_socket_group(self, in_type: str, out_type: str = "", name: str = "", in_name: str = "", out_name: str = ""):
|
||||||
if not self.get('multi_sockets'): self['multi_sockets'] = {}
|
if not self.get('multi_groups'): self['multi_groups'] = {}
|
||||||
if not self['multi_sockets'].get(name): self['multi_sockets'][name] = {'bl_idname': bl_idname, 'name': name or get_label_by_name(bl_idname)}
|
group_id = name or in_name or out_name or in_type
|
||||||
self._update_multi_inputs()
|
self['multi_groups'][group_id] = {
|
||||||
|
'in_type': in_type,
|
||||||
|
'out_type': out_type,
|
||||||
|
'in_name': in_name or name or get_label_by_name(in_type),
|
||||||
|
'out_name': out_name or name or (get_label_by_name(out_type) if out_type else ""),
|
||||||
|
}
|
||||||
|
self._update_multi_groups()
|
||||||
|
|
||||||
def resolve_multi_input(self, fieldname) -> Any:
|
def resolve_multi_input(self, fieldname) -> Any:
|
||||||
cache = getattr(self, "_current_cache", None)
|
cache = getattr(self, "_current_cache", None)
|
||||||
visited = getattr(self, "_current_visited", None)
|
visited = getattr(self, "_current_visited", None)
|
||||||
inputs = [i for i in list(self.inputs) if i.name == fieldname]
|
# Search by name or identifier prefix for multi-groups
|
||||||
|
inputs = [i for i in list(self.inputs) if i.name == fieldname or i.identifier.startswith(f"{fieldname}_ms_in_")]
|
||||||
return [resolve_input(r, cache=cache, visited=visited) for r in inputs]
|
return [resolve_input(r, cache=cache, visited=visited) for r in inputs]
|
||||||
|
|
||||||
def _update_multi_inputs(self):
|
def _update_multi_groups(self):
|
||||||
if not self.get('multi_sockets'): return
|
if not self.get('multi_groups'): return
|
||||||
for k, ms in self['multi_sockets'].items():
|
|
||||||
def is_active(input) -> bool:
|
for group_id, cfg in self['multi_groups'].items():
|
||||||
a: bool = input.is_linked or getattr(input, 'value', False)
|
in_type = cfg['in_type']
|
||||||
b: bool = getattr(inputs()[-1], 'value', False)
|
out_type = cfg.get('out_type', '')
|
||||||
return a or b
|
in_name = cfg['in_name']
|
||||||
|
out_name = cfg['out_name']
|
||||||
|
|
||||||
|
def get_ins(): return [i for i in self.inputs if i.identifier.startswith(f"{group_id}_ms_in_")]
|
||||||
|
def get_outs(): return [o for o in self.outputs if o.identifier.startswith(f"{group_id}_ms_out_")]
|
||||||
|
def is_active(s): return s.is_linked or bool(getattr(s, 'value', False))
|
||||||
|
|
||||||
def inputs():
|
ins = get_ins()
|
||||||
return [i for i in list(self.inputs) if i.name == ms['name']]
|
|
||||||
|
if not ins:
|
||||||
|
self.add_input(in_type, in_name, identifier=f"{group_id}_ms_in_0")
|
||||||
|
if out_type:
|
||||||
|
self.add_output(out_type, out_name, identifier=f"{group_id}_ms_out_0")
|
||||||
|
ins = get_ins()
|
||||||
|
|
||||||
socket_type = ms['bl_idname']
|
if is_active(ins[-1]):
|
||||||
name = ms['name']
|
idx = len(ins)
|
||||||
|
self.add_input(in_type, in_name, identifier=f"{group_id}_ms_in_{idx}")
|
||||||
|
if out_type:
|
||||||
|
self.add_output(out_type, out_name, identifier=f"{group_id}_ms_out_{idx}")
|
||||||
|
ins = get_ins()
|
||||||
|
|
||||||
if len(inputs()) == 0:
|
while len(ins) > 1:
|
||||||
self.add_input(socket_type, name, name+str(len(inputs())))
|
if not is_active(ins[-1]) and not is_active(ins[-2]):
|
||||||
return
|
self.inputs.remove(ins[-1])
|
||||||
|
outs = get_outs()
|
||||||
if is_active(inputs()[-1]):
|
if out_type and len(outs) >= len(ins):
|
||||||
self.add_input(socket_type, name, name+str(len(inputs())))
|
self.outputs.remove(outs[-1])
|
||||||
|
ins.pop()
|
||||||
while True:
|
|
||||||
if len(inputs()) < 2: break
|
|
||||||
if not is_active(inputs()[-1]) and not is_active(inputs()[-2]):
|
|
||||||
self.inputs.remove(inputs()[-1])
|
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -201,7 +222,9 @@ def _resolve_output(
|
||||||
visited = set() if visited is None else visited
|
visited = set() if visited is None else visited
|
||||||
|
|
||||||
assert socket.node
|
assert socket.node
|
||||||
key = (socket.node.as_pointer(), socket.node.name, socket.name)
|
# Use identifier instead of name for the cache key, as multiple sockets
|
||||||
|
# (especially in multi-groups) can share the same name but must have unique identifiers.
|
||||||
|
key = (socket.node.as_pointer(), socket.node.name, socket.identifier)
|
||||||
if key in cache: return cache[key]
|
if key in cache: return cache[key]
|
||||||
|
|
||||||
node_key = (socket.node.as_pointer(), socket.node.name)
|
node_key = (socket.node.as_pointer(), socket.node.name)
|
||||||
|
|
@ -234,7 +257,13 @@ def _resolve_output(
|
||||||
return default
|
return default
|
||||||
|
|
||||||
assert socket.node.evaluate_output
|
assert socket.node.evaluate_output
|
||||||
value = socket.node.evaluate_output(socket.name, cache=cache, visited=visited)
|
value = socket.node.evaluate_output(
|
||||||
|
socket.name,
|
||||||
|
identifier=socket.identifier,
|
||||||
|
socket=socket,
|
||||||
|
cache=cache,
|
||||||
|
visited=visited
|
||||||
|
)
|
||||||
cache[key] = value
|
cache[key] = value
|
||||||
return value
|
return value
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue