big refactor
This commit is contained in:
parent
1b763fc11c
commit
8c8acaf45e
108 changed files with 5646 additions and 2205 deletions
15
core/functions/__init__.py
Normal file
15
core/functions/__init__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from . import mesh_functions
|
||||||
|
from . import path_functions
|
||||||
|
from . import gcode_functions
|
||||||
|
from . import math_functions
|
||||||
|
from . import preview_functions
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"mesh_functions",
|
||||||
|
"path_functions",
|
||||||
|
"gcode_functions",
|
||||||
|
"math_functions",
|
||||||
|
"preview_functions",
|
||||||
|
]
|
||||||
120
core/functions/gcode_functions.py
Normal file
120
core/functions/gcode_functions.py
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
from ..slice import Pather
|
||||||
|
from ...infra.blender.list_utils import flatten_list, is_list_data
|
||||||
|
|
||||||
|
|
||||||
|
def paths_to_gcode(p: Any, intr: Any, use_header: bool = True) -> str:
|
||||||
|
if not p:
|
||||||
|
return ""
|
||||||
|
pather: Pather = Pather(p)
|
||||||
|
if hasattr(p, "layer_height"):
|
||||||
|
pather.layer_height = getattr(p, "layer_height")
|
||||||
|
if hasattr(p, "first_layer_height"):
|
||||||
|
pather.first_layer_height = getattr(p, "first_layer_height")
|
||||||
|
|
||||||
|
if intr:
|
||||||
|
pather.bed_center_x = intr.bed_x / 2.0
|
||||||
|
pather.bed_center_y = intr.bed_y / 2.0
|
||||||
|
pather.feedrate = intr.feedrate
|
||||||
|
pather.travel_feedrate = intr.travel_feedrate
|
||||||
|
pather.extrusion_rate = intr.extrusion_rate
|
||||||
|
pather.retraction = intr.retraction
|
||||||
|
pather.retraction_feedrate = getattr(intr, "retraction_feedrate", 0.0)
|
||||||
|
pather.lift = intr.lift
|
||||||
|
|
||||||
|
return pather.to_gcode(include_header=use_header)
|
||||||
|
|
||||||
|
|
||||||
|
def format_gcode_metadata(name: Any, filament_type: Any, printer_model: Any) -> str:
|
||||||
|
lines = []
|
||||||
|
if name:
|
||||||
|
lines.append(f'; objects_info = {{"objects": [{{"name": "{name}", "polygon": []}}]}}')
|
||||||
|
if filament_type:
|
||||||
|
lines.append(f'; filament_type = {filament_type}')
|
||||||
|
if printer_model:
|
||||||
|
lines.append(f'; printer_model = {printer_model}')
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def regex_replace_gcode(gc: Any, find_pattern: Any, replace_pattern: Any, node_name: str = "") -> str:
|
||||||
|
gc_str = str(gc or "")
|
||||||
|
f_str = str(find_pattern or "")
|
||||||
|
r_str = str(replace_pattern or "")
|
||||||
|
if not f_str or not gc_str:
|
||||||
|
return gc_str
|
||||||
|
try:
|
||||||
|
def subst(match):
|
||||||
|
if match.group(1):
|
||||||
|
return '$'
|
||||||
|
elif match.group(2):
|
||||||
|
return '\\g<' + match.group(2) + '>'
|
||||||
|
elif match.group(3):
|
||||||
|
return '\\g<' + match.group(3) + '>'
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
rep = re.sub(r'(\$\$)|(?:\$\{([^}]+)\})|(?:\$([a-zA-Z0-9_]+))', subst, r_str)
|
||||||
|
return re.sub(f_str, rep, gc_str)
|
||||||
|
except Exception as e:
|
||||||
|
if node_name:
|
||||||
|
print(f"Regex error in node {node_name}: {e}")
|
||||||
|
return gc_str
|
||||||
|
|
||||||
|
|
||||||
|
def format_gcode_command(command: str, parameters: str = "") -> str:
|
||||||
|
cmd = command
|
||||||
|
params = parameters.strip()
|
||||||
|
if params:
|
||||||
|
if cmd == 'T':
|
||||||
|
return f"{cmd}{params}"
|
||||||
|
return f"{cmd} {params}"
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
def join_gcode_inputs(raw_inputs: list[Any]) -> Any:
|
||||||
|
has_list = any(is_list_data(inp) for inp in raw_inputs)
|
||||||
|
|
||||||
|
if not has_list:
|
||||||
|
flattened = flatten_list(raw_inputs)
|
||||||
|
gcode_input: list[str] = [str(gc) for gc in flattened if gc]
|
||||||
|
return "\n".join(gcode_input)
|
||||||
|
|
||||||
|
# There is at least one list input
|
||||||
|
list_lengths = [len(inp) for inp in raw_inputs if is_list_data(inp)]
|
||||||
|
if len(set(list_lengths)) > 1:
|
||||||
|
raise ValueError(
|
||||||
|
f"Join Gcode: all input lists must have the same length, got lengths {list_lengths}"
|
||||||
|
)
|
||||||
|
|
||||||
|
n_elements = list_lengths[0] if list_lengths else 0
|
||||||
|
result: list[str] = []
|
||||||
|
for i in range(n_elements):
|
||||||
|
row_parts: list[str] = []
|
||||||
|
for inp in raw_inputs:
|
||||||
|
if is_list_data(inp):
|
||||||
|
val = inp[i]
|
||||||
|
else:
|
||||||
|
val = inp
|
||||||
|
if is_list_data(val):
|
||||||
|
val_str = "\n".join(str(s) for s in flatten_list(val) if s)
|
||||||
|
if val_str:
|
||||||
|
row_parts.append(val_str)
|
||||||
|
elif val:
|
||||||
|
row_parts.append(str(val))
|
||||||
|
result.append("\n".join(row_parts))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def load_gcode_file(path: str) -> str:
|
||||||
|
if not path:
|
||||||
|
return ""
|
||||||
|
abs_path = bpy.path.abspath(path)
|
||||||
|
if os.path.exists(abs_path):
|
||||||
|
with open(abs_path, 'r', encoding='utf-8') as f:
|
||||||
|
return f.read()
|
||||||
|
return ""
|
||||||
32
core/functions/math_functions.py
Normal file
32
core/functions/math_functions.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
MATH_OPERATIONS: dict[str, Any] = {
|
||||||
|
'ADD': lambda a, b, t: a + b,
|
||||||
|
'SUBTRACT': lambda a, b, t: a - b,
|
||||||
|
'MULTIPLY': lambda a, b, t: a * b,
|
||||||
|
'DIVIDE': lambda a, b, t: a / b if b != 0 else 0,
|
||||||
|
'ABS': lambda a, b, t: abs(a),
|
||||||
|
'NEGATE': lambda a, b, t: -a,
|
||||||
|
'POWER': lambda a, b, t: t(a ** b) if not (a == 0 and b < 0) else 0,
|
||||||
|
'MIN': lambda a, b, t: min(a, b),
|
||||||
|
'MAX': lambda a, b, t: max(a, b),
|
||||||
|
'SIGN': lambda a, b, t: (a > 0) - (a < 0),
|
||||||
|
'DIVIDE_ROUND': lambda a, b, t: t(round(a / b)) if b != 0 else 0,
|
||||||
|
'DIVIDE_FLOOR': lambda a, b, t: a // b if b != 0 else 0,
|
||||||
|
'DIVIDE_TRUNCATE': lambda a, b, t: t(a / b) if b != 0 else 0,
|
||||||
|
'MODULO_FLOORED': lambda a, b, t: a % b if b != 0 else 0,
|
||||||
|
'MODULO_TRUNCATED': lambda a, b, t: a - (b * t(a / b)) if b != 0 else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_math_operation(operation: str, val_a: Any, val_b: Any, math_type: type = float) -> Any:
|
||||||
|
op_fn = MATH_OPERATIONS.get(operation)
|
||||||
|
if not op_fn:
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
a_num = math_type(val_a if val_a is not None else 0)
|
||||||
|
b_num = math_type(val_b if val_b is not None else 0)
|
||||||
|
return op_fn(a_num, b_num, math_type)
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
258
core/functions/mesh_functions.py
Normal file
258
core/functions/mesh_functions.py
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import copy
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ...infra.blender.blender_mesh_capture import PdfProperties, SlicingCollection, SlicingGroup, SlicingObject
|
||||||
|
from ..slice import SlicePaths
|
||||||
|
from ..pdf_exporter.geometry import transform_slice_paths
|
||||||
|
from ..geometry import update_transform
|
||||||
|
from ..pdf_exporter.metadata import get_first_attribute_value, resolve_object_properties
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_slicing_object(so: SlicingObject) -> SlicingObject:
|
||||||
|
new_so = SlicingObject()
|
||||||
|
new_so.name = so.name
|
||||||
|
new_so.parent = so.parent
|
||||||
|
new_so.mesh = so.mesh.copy() if so.mesh is not None else np.empty((0, 4, 3))
|
||||||
|
new_so.attributes = {}
|
||||||
|
for k, v in so.attributes.items():
|
||||||
|
if isinstance(v, dict):
|
||||||
|
new_so.attributes[k] = {
|
||||||
|
"domain": v.get("domain", "face"),
|
||||||
|
"type": v.get("type", "string"),
|
||||||
|
"values": v["values"].copy() if isinstance(v.get("values"), np.ndarray) else v.get("values")
|
||||||
|
}
|
||||||
|
elif isinstance(v, np.ndarray):
|
||||||
|
new_so.attributes[k] = v.copy()
|
||||||
|
elif isinstance(v, list):
|
||||||
|
new_so.attributes[k] = list(v)
|
||||||
|
else:
|
||||||
|
new_so.attributes[k] = copy.deepcopy(v)
|
||||||
|
new_so.transforms = [dict(t) for t in getattr(so, "transforms", [])]
|
||||||
|
new_so.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
|
||||||
|
new_so.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
|
||||||
|
return new_so
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_slicing_collection(sc: SlicingCollection) -> SlicingCollection:
|
||||||
|
new_sc = SlicingCollection([], sc.name)
|
||||||
|
new_sc.objects = [_copy_slicing_object(so) for so in sc.objects]
|
||||||
|
new_sc.transforms = [dict(t) for t in getattr(sc, "transforms", [])]
|
||||||
|
return new_sc
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_slicing_group(sg: SlicingGroup) -> SlicingGroup:
|
||||||
|
new_sg = SlicingGroup([])
|
||||||
|
new_sg.collections = {k: _copy_slicing_collection(sc) for k, sc in sg.collections.items()}
|
||||||
|
new_sg.transforms = [dict(t) for t in getattr(sg, "transforms", [])]
|
||||||
|
return new_sg
|
||||||
|
|
||||||
|
|
||||||
|
def _transform_geometry_item(geom: Any, t_arr: np.ndarray, r_arr: np.ndarray, s_arr: np.ndarray) -> Any:
|
||||||
|
if geom is None:
|
||||||
|
return None
|
||||||
|
elif isinstance(geom, SlicePaths):
|
||||||
|
copied_data = transform_slice_paths(geom, t_arr, r_arr, s_arr)
|
||||||
|
copied_data.transforms = update_transform(getattr(geom, "transforms", []), t_arr, r_arr, s_arr)
|
||||||
|
return copied_data
|
||||||
|
elif isinstance(geom, (list, tuple)):
|
||||||
|
return [_transform_geometry_item(item, t_arr, r_arr, s_arr) for item in geom]
|
||||||
|
elif isinstance(geom, SlicingGroup):
|
||||||
|
copied_data = _copy_slicing_group(geom)
|
||||||
|
for col in copied_data.collections.values():
|
||||||
|
if any(s != 1.0 for s in s_arr):
|
||||||
|
col.scale(s_arr)
|
||||||
|
if any(r != 0.0 for r in r_arr):
|
||||||
|
col.rotate(r_arr)
|
||||||
|
if any(t != 0.0 for t in t_arr):
|
||||||
|
col.offset(t_arr)
|
||||||
|
col.transforms = update_transform(getattr(col, "transforms", []), t_arr, r_arr, s_arr)
|
||||||
|
for so in col.objects:
|
||||||
|
so.transforms = update_transform(getattr(so, "transforms", []), t_arr, r_arr, s_arr)
|
||||||
|
|
||||||
|
copied_data.transforms = update_transform(getattr(geom, "transforms", []), t_arr, r_arr, s_arr)
|
||||||
|
return copied_data
|
||||||
|
elif isinstance(geom, SlicingCollection):
|
||||||
|
copied_data = _copy_slicing_collection(geom)
|
||||||
|
if any(s != 1.0 for s in s_arr):
|
||||||
|
copied_data.scale(s_arr)
|
||||||
|
if any(r != 0.0 for r in r_arr):
|
||||||
|
copied_data.rotate(r_arr)
|
||||||
|
if any(t != 0.0 for t in t_arr):
|
||||||
|
copied_data.offset(t_arr)
|
||||||
|
|
||||||
|
copied_data.transforms = update_transform(getattr(geom, "transforms", []), t_arr, r_arr, s_arr)
|
||||||
|
for so in copied_data.objects:
|
||||||
|
so.transforms = update_transform(getattr(so, "transforms", []), t_arr, r_arr, s_arr)
|
||||||
|
return copied_data
|
||||||
|
elif isinstance(geom, SlicingObject):
|
||||||
|
copied_data = _copy_slicing_object(geom)
|
||||||
|
if any(s != 1.0 for s in s_arr):
|
||||||
|
copied_data.scale(s_arr)
|
||||||
|
if any(r != 0.0 for r in r_arr):
|
||||||
|
copied_data.rotate(r_arr)
|
||||||
|
if any(t != 0.0 for t in t_arr):
|
||||||
|
copied_data.offset(t_arr)
|
||||||
|
|
||||||
|
copied_data.transforms = update_transform(getattr(geom, "transforms", []), t_arr, r_arr, s_arr)
|
||||||
|
return copied_data
|
||||||
|
elif hasattr(geom, "location") and hasattr(geom, "rotation_euler") and hasattr(geom, "scale") and not hasattr(geom, "mesh"):
|
||||||
|
from ...infra.blender.blender_slice_nodes_generic import object_to_mesh
|
||||||
|
mesh_geom = object_to_mesh(geom, include_children=True)
|
||||||
|
return _transform_geometry_item(mesh_geom, t_arr, r_arr, s_arr)
|
||||||
|
return geom
|
||||||
|
|
||||||
|
|
||||||
|
def format_name_template(template: str, so: Any, default_old_name: str = "") -> str:
|
||||||
|
if not template or "{" not in template or "}" not in template:
|
||||||
|
return template
|
||||||
|
|
||||||
|
def replace_match(match: re.Match[str]) -> str:
|
||||||
|
attr_name = match.group(1).strip()
|
||||||
|
val = get_first_attribute_value(so, attr_name)
|
||||||
|
if val is None and attr_name in ("Name", "name"):
|
||||||
|
val = getattr(so, "name", "") or default_old_name
|
||||||
|
if val is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(val, float):
|
||||||
|
return f"{val:g}"
|
||||||
|
return str(val)
|
||||||
|
|
||||||
|
return re.sub(r"\{([^}]+)\}", replace_match, template)
|
||||||
|
|
||||||
|
|
||||||
|
def _rename_slicing_item(geom: Any, new_name: str) -> Any:
|
||||||
|
if geom is None or not new_name:
|
||||||
|
return geom
|
||||||
|
elif isinstance(geom, SlicePaths):
|
||||||
|
copied = geom.copy_with(paths=list(geom))
|
||||||
|
target_name = format_name_template(new_name, copied, default_old_name=getattr(copied, "name", ""))
|
||||||
|
copied.name = target_name
|
||||||
|
return copied
|
||||||
|
elif isinstance(geom, (list, tuple)):
|
||||||
|
return [_rename_slicing_item(item, f"{new_name}_{i}" if (len(geom) > 1 and "{" not in new_name) else new_name) for i, item in enumerate(geom)]
|
||||||
|
elif isinstance(geom, SlicingGroup):
|
||||||
|
copied = _copy_slicing_group(geom)
|
||||||
|
old_colls = dict(copied.collections)
|
||||||
|
new_colls = {}
|
||||||
|
for i, (old_key, coll) in enumerate(old_colls.items()):
|
||||||
|
coll_old_name = getattr(coll, "name", old_key)
|
||||||
|
target_name = format_name_template(new_name, coll, default_old_name=coll_old_name)
|
||||||
|
coll_new_name = f"{target_name}_{i}" if (len(old_colls) > 1 and "{" not in new_name and target_name == new_name) else target_name
|
||||||
|
new_colls[coll_new_name] = _rename_slicing_item(coll, coll_new_name)
|
||||||
|
copied.collections = new_colls
|
||||||
|
return copied
|
||||||
|
elif isinstance(geom, SlicingCollection):
|
||||||
|
copied = _copy_slicing_collection(geom)
|
||||||
|
target_name = format_name_template(new_name, copied, default_old_name=getattr(copied, "name", ""))
|
||||||
|
copied.name = target_name
|
||||||
|
for i, so in enumerate(copied.objects):
|
||||||
|
so.parent = target_name
|
||||||
|
so_old_name = getattr(so, "name", "")
|
||||||
|
so_target = format_name_template(new_name, so, default_old_name=so_old_name)
|
||||||
|
so.name = f"{so_target}_{i}" if (len(copied.objects) > 1 and "{" not in new_name and so_target == new_name) else so_target
|
||||||
|
return copied
|
||||||
|
elif isinstance(geom, SlicingObject):
|
||||||
|
copied = _copy_slicing_object(geom)
|
||||||
|
target_name = format_name_template(new_name, copied, default_old_name=getattr(copied, "name", ""))
|
||||||
|
copied.name = target_name
|
||||||
|
return copied
|
||||||
|
return geom
|
||||||
|
|
||||||
|
|
||||||
|
def _assign_pdf_properties_to_item(geom: Any, line_width: float, line_color: tuple[float, float, float] | None, show_name: bool) -> Any:
|
||||||
|
if geom is None:
|
||||||
|
return None
|
||||||
|
elif isinstance(geom, SlicePaths):
|
||||||
|
copied = geom.copy_with(paths=list(geom))
|
||||||
|
copied.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name)
|
||||||
|
return copied
|
||||||
|
elif isinstance(geom, (list, tuple)):
|
||||||
|
return [_assign_pdf_properties_to_item(item, line_width, line_color, show_name) for item in geom]
|
||||||
|
elif isinstance(geom, SlicingGroup):
|
||||||
|
copied = _copy_slicing_group(geom)
|
||||||
|
new_colls = {}
|
||||||
|
for k, coll in copied.collections.items():
|
||||||
|
new_colls[k] = _assign_pdf_properties_to_item(coll, line_width, line_color, show_name)
|
||||||
|
copied.collections = new_colls
|
||||||
|
return copied
|
||||||
|
elif isinstance(geom, SlicingCollection):
|
||||||
|
copied = _copy_slicing_collection(geom)
|
||||||
|
copied.objects = [_assign_pdf_properties_to_item(so, line_width, line_color, show_name) for so in copied.objects]
|
||||||
|
return copied
|
||||||
|
elif isinstance(geom, SlicingObject):
|
||||||
|
copied = _copy_slicing_object(geom)
|
||||||
|
copied.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name)
|
||||||
|
return copied
|
||||||
|
return geom
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_settings_props(raw_settings: Any) -> list[tuple[str, str]]:
|
||||||
|
props: list[tuple[str, str]] = []
|
||||||
|
if raw_settings:
|
||||||
|
if isinstance(raw_settings, dict):
|
||||||
|
for k, v in raw_settings.items():
|
||||||
|
props.append((str(k), str(v)))
|
||||||
|
elif isinstance(raw_settings, (list, tuple)):
|
||||||
|
for item in raw_settings:
|
||||||
|
if isinstance(item, tuple) and len(item) == 2:
|
||||||
|
props.append((str(item[0]), str(item[1])))
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
for k, v in item.items():
|
||||||
|
props.append((str(k), str(v)))
|
||||||
|
else:
|
||||||
|
props.append(("", str(item)))
|
||||||
|
return props
|
||||||
|
|
||||||
|
|
||||||
|
def _is_single_settings_spec(val: Any) -> bool:
|
||||||
|
if isinstance(val, dict):
|
||||||
|
return True
|
||||||
|
if isinstance(val, (list, tuple)):
|
||||||
|
if len(val) > 0 and all(isinstance(x, tuple) and len(x) == 2 and not isinstance(x[0], (list, dict, tuple)) for x in val):
|
||||||
|
return True
|
||||||
|
if len(val) == 0:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _assign_properties_to_item(geom: Any, props: list[tuple[str, str]]) -> Any:
|
||||||
|
if geom is None:
|
||||||
|
return None
|
||||||
|
elif isinstance(geom, SlicePaths):
|
||||||
|
copied = geom.copy_with(paths=list(geom))
|
||||||
|
copied.pdf_object_properties = resolve_object_properties(copied, props)
|
||||||
|
return copied
|
||||||
|
elif isinstance(geom, (list, tuple)):
|
||||||
|
return [_assign_properties_to_item(item, props) for item in geom]
|
||||||
|
elif isinstance(geom, SlicingGroup):
|
||||||
|
copied = _copy_slicing_group(geom)
|
||||||
|
for coll in copied.collections.values():
|
||||||
|
for obj in coll.objects:
|
||||||
|
obj.pdf_object_properties = resolve_object_properties(obj, props)
|
||||||
|
return copied
|
||||||
|
elif isinstance(geom, SlicingCollection):
|
||||||
|
copied = _copy_slicing_collection(geom)
|
||||||
|
for obj in copied.objects:
|
||||||
|
obj.pdf_object_properties = resolve_object_properties(obj, props)
|
||||||
|
return copied
|
||||||
|
elif isinstance(geom, SlicingObject):
|
||||||
|
copied = _copy_slicing_object(geom)
|
||||||
|
copied.pdf_object_properties = resolve_object_properties(geom, props)
|
||||||
|
return copied
|
||||||
|
return geom
|
||||||
|
|
||||||
|
|
||||||
|
# Public aliases
|
||||||
|
copy_slicing_object = _copy_slicing_object
|
||||||
|
copy_slicing_collection = _copy_slicing_collection
|
||||||
|
copy_slicing_group = _copy_slicing_group
|
||||||
|
transform_geometry_item = _transform_geometry_item
|
||||||
|
rename_slicing_item = _rename_slicing_item
|
||||||
|
assign_pdf_properties_to_item = _assign_pdf_properties_to_item
|
||||||
|
parse_settings_props = _parse_settings_props
|
||||||
|
is_single_settings_spec = _is_single_settings_spec
|
||||||
|
assign_properties_to_item = _assign_properties_to_item
|
||||||
196
core/functions/path_functions.py
Normal file
196
core/functions/path_functions.py
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ...infra.blender.blender_mesh_capture import SlicingCollection, SlicingGroup, SlicingObject
|
||||||
|
from ..slice import SlicePaths, slice_at_range
|
||||||
|
|
||||||
|
|
||||||
|
def _finish_slice_paths(
|
||||||
|
merged_object: Any,
|
||||||
|
coll: Any,
|
||||||
|
objects: list[Any],
|
||||||
|
z_values: np.ndarray,
|
||||||
|
rotation: Any,
|
||||||
|
ignore_holes: bool,
|
||||||
|
fl_height: float | None,
|
||||||
|
layer_height: float,
|
||||||
|
) -> SlicePaths:
|
||||||
|
import sys
|
||||||
|
path_mod = sys.modules.get("NodeSlicer.core.nodeslicer.nodes.path") or sys.modules.get("core.nodeslicer.nodes.path")
|
||||||
|
sar_func = getattr(path_mod, "slice_at_range", slice_at_range) if path_mod else slice_at_range
|
||||||
|
slices_group: list[list[np.ndarray]] | None = sar_func(
|
||||||
|
[merged_object.mesh],
|
||||||
|
z_values,
|
||||||
|
ignore_holes=ignore_holes
|
||||||
|
)
|
||||||
|
if not slices_group or not slices_group[0]:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
paths = slices_group[0]
|
||||||
|
z_to_group: dict[int, int] = {}
|
||||||
|
groups = []
|
||||||
|
for p in paths:
|
||||||
|
if len(p) > 0:
|
||||||
|
z_k = int(round(p[0, 2] * 1000))
|
||||||
|
if z_k not in z_to_group:
|
||||||
|
z_to_group[z_k] = len(z_to_group)
|
||||||
|
groups.append(z_to_group[z_k])
|
||||||
|
else:
|
||||||
|
groups.append(0)
|
||||||
|
|
||||||
|
slices = SlicePaths(paths, groups=groups)
|
||||||
|
slices.layer_height = layer_height
|
||||||
|
if fl_height is not None:
|
||||||
|
slices.first_layer_height = fl_height
|
||||||
|
slices.transforms = list(getattr(coll, "transforms", [])) or list(getattr(merged_object, "transforms", []))
|
||||||
|
|
||||||
|
names = []
|
||||||
|
if objects:
|
||||||
|
for o in objects:
|
||||||
|
n = getattr(o, "name", "")
|
||||||
|
if n:
|
||||||
|
if n.endswith("_merged"):
|
||||||
|
n = n[:-7]
|
||||||
|
if n and n not in names:
|
||||||
|
names.append(n)
|
||||||
|
if not names and hasattr(coll, "name") and getattr(coll, "name"):
|
||||||
|
names.append(str(getattr(coll, "name")))
|
||||||
|
|
||||||
|
if names:
|
||||||
|
slices.name = names[0]
|
||||||
|
slices.objects_info = {"objects": [{"name": n, "polygon": []} for n in names]}
|
||||||
|
|
||||||
|
if len(z_values) > 0 and fl_height is not None and layer_height > 0:
|
||||||
|
mult = fl_height / layer_height
|
||||||
|
z_first_um = int(round(z_values[0] * 1000))
|
||||||
|
for i, p in enumerate(slices):
|
||||||
|
if len(p) > 0 and int(round(p[0, 2] * 1000)) == z_first_um:
|
||||||
|
slices.extrusion_rate[i] = slices.extrusion_rate[i] * mult
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
has_rot = bool(rotation and any(rotation))
|
||||||
|
if has_rot:
|
||||||
|
from mathutils import Euler
|
||||||
|
mat_inv = np.array(Euler(rotation).to_matrix())
|
||||||
|
slices = slices.transform(mat_inv)
|
||||||
|
slices = slices.align_seams(mode="sharpest")
|
||||||
|
|
||||||
|
return slices
|
||||||
|
|
||||||
|
|
||||||
|
def _slice_single_mesh_item(coll: Any, layer_height: float, fl_height: float | None, rotation: Any, ignore_holes: bool) -> SlicePaths:
|
||||||
|
if not coll:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
# Handle SlicingGroup, SlicingCollection, SlicingObject
|
||||||
|
if isinstance(coll, SlicingGroup):
|
||||||
|
objects = []
|
||||||
|
for c in coll.collections.values():
|
||||||
|
objects.extend(c.objects)
|
||||||
|
elif isinstance(coll, SlicingCollection):
|
||||||
|
objects = list(coll.objects)
|
||||||
|
elif isinstance(coll, SlicingObject):
|
||||||
|
objects = [coll]
|
||||||
|
else:
|
||||||
|
objects = getattr(coll, "objects", [coll] if hasattr(coll, "mesh") else [])
|
||||||
|
|
||||||
|
merged_object = SlicingObject.merge(objects)
|
||||||
|
if merged_object.mesh is None or merged_object.mesh.size == 0:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
has_rot = bool(rotation and any(rotation))
|
||||||
|
if has_rot:
|
||||||
|
from mathutils import Euler
|
||||||
|
mat = np.array(Euler(rotation).to_matrix().inverted())
|
||||||
|
merged_object.mesh[:, :3, :] = merged_object.mesh[:, :3, :] @ mat.T
|
||||||
|
merged_object.mesh[:, 3:4, :] = merged_object.mesh[:, 3:4, :] @ mat.T
|
||||||
|
|
||||||
|
min_z = float(merged_object.mesh[:, :3, 2].min())
|
||||||
|
if abs(min_z) < 1e-6:
|
||||||
|
min_z = 0.0
|
||||||
|
max_z = float(merged_object.mesh[:, :3, 2].max())
|
||||||
|
|
||||||
|
h_first = fl_height if fl_height is not None else layer_height
|
||||||
|
|
||||||
|
min_z_um = int(round(min_z * 1000))
|
||||||
|
lh_um = int(round(layer_height * 1000))
|
||||||
|
flh_um = int(round(h_first * 1000))
|
||||||
|
max_z_um = int(round(max_z * 1000))
|
||||||
|
|
||||||
|
z_first_um = min_z_um + flh_um
|
||||||
|
z_rest_start_um = z_first_um + lh_um
|
||||||
|
|
||||||
|
if z_first_um <= max_z_um:
|
||||||
|
if z_rest_start_um <= max_z_um and lh_um > 0:
|
||||||
|
z_um_values = np.arange(z_rest_start_um, max_z_um + 1, lh_um, dtype=np.int64)
|
||||||
|
z_values = np.concatenate(([z_first_um], z_um_values)) / 1000.0
|
||||||
|
else:
|
||||||
|
z_values = np.array([z_first_um], dtype=np.float64) / 1000.0
|
||||||
|
else:
|
||||||
|
z_values = np.array([], dtype=np.float64)
|
||||||
|
|
||||||
|
return _finish_slice_paths(merged_object, coll, objects, z_values, rotation, ignore_holes, fl_height, layer_height)
|
||||||
|
|
||||||
|
|
||||||
|
def _slice_single_mesh_item_with_layers(coll: Any, layers: Any, rotation: Any, ignore_holes: bool) -> SlicePaths:
|
||||||
|
if not coll or layers is None:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
# Handle SlicingGroup, SlicingCollection, SlicingObject
|
||||||
|
if isinstance(coll, SlicingGroup):
|
||||||
|
objects = []
|
||||||
|
for c in coll.collections.values():
|
||||||
|
objects.extend(c.objects)
|
||||||
|
elif isinstance(coll, SlicingCollection):
|
||||||
|
objects = list(coll.objects)
|
||||||
|
elif isinstance(coll, SlicingObject):
|
||||||
|
objects = [coll]
|
||||||
|
else:
|
||||||
|
objects = getattr(coll, "objects", [coll] if hasattr(coll, "mesh") else [])
|
||||||
|
|
||||||
|
merged_object = SlicingObject.merge(objects)
|
||||||
|
if merged_object.mesh is None or merged_object.mesh.size == 0:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
has_rot = bool(rotation and any(rotation))
|
||||||
|
if has_rot:
|
||||||
|
from mathutils import Euler
|
||||||
|
mat = np.array(Euler(rotation).to_matrix().inverted())
|
||||||
|
merged_object.mesh[:, :3, :] = merged_object.mesh[:, :3, :] @ mat.T
|
||||||
|
merged_object.mesh[:, 3:4, :] = merged_object.mesh[:, 3:4, :] @ mat.T
|
||||||
|
|
||||||
|
min_z = float(merged_object.mesh[:, :3, 2].min())
|
||||||
|
if abs(min_z) < 1e-6:
|
||||||
|
min_z = 0.0
|
||||||
|
max_z = float(merged_object.mesh[:, :3, 2].max())
|
||||||
|
|
||||||
|
min_z_um = int(round(min_z * 1000))
|
||||||
|
max_z_um = int(round(max_z * 1000))
|
||||||
|
|
||||||
|
raw_layers = layers if isinstance(layers, (list, tuple, np.ndarray)) else [layers]
|
||||||
|
valid_z: list[float] = []
|
||||||
|
for z in raw_layers:
|
||||||
|
try:
|
||||||
|
zf = float(z)
|
||||||
|
z_um = int(round(zf * 1000))
|
||||||
|
if min_z_um <= z_um <= max_z_um:
|
||||||
|
valid_z.append(zf)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not valid_z:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
z_values = np.array(valid_z, dtype=np.float64)
|
||||||
|
fl_height = float(z_values[0]) if len(z_values) > 0 else None
|
||||||
|
layer_height = float(z_values[1] - z_values[0]) if len(z_values) > 1 else (fl_height or 0.2)
|
||||||
|
|
||||||
|
return _finish_slice_paths(merged_object, coll, objects, z_values, rotation, ignore_holes, fl_height, layer_height)
|
||||||
|
|
||||||
|
|
||||||
|
# Public aliases
|
||||||
|
slice_single_mesh_item = _slice_single_mesh_item
|
||||||
|
finish_slice_paths = _finish_slice_paths
|
||||||
|
slice_single_mesh_item_with_layers = _slice_single_mesh_item_with_layers
|
||||||
143
core/functions/preview_functions.py
Normal file
143
core/functions/preview_functions.py
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import math
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def parse_gcode_preview_geometry(
|
||||||
|
gcode: str,
|
||||||
|
scale_factor: float = 0.001,
|
||||||
|
scale_length: float = 1.0,
|
||||||
|
show_e_0: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Parses G-code text and extracts vertices, edges, and e_rates for mesh preview generation.
|
||||||
|
Returns a dict with:
|
||||||
|
vertices: list[tuple[float, float, float]]
|
||||||
|
edges: list[tuple[int, int]]
|
||||||
|
e_rates: list[float]
|
||||||
|
default_layer_height: float
|
||||||
|
prep_translation: list[float]
|
||||||
|
prep_rotation: list[float]
|
||||||
|
prep_scale: list[float]
|
||||||
|
"""
|
||||||
|
vertices: list[tuple[float, float, float]] = []
|
||||||
|
edges: list[tuple[int, int]] = []
|
||||||
|
e_rates: list[float] = []
|
||||||
|
|
||||||
|
last_x, last_y, last_z = 0.0, 0.0, 0.0
|
||||||
|
last_e = 0.0
|
||||||
|
|
||||||
|
offset_x, offset_y = 0.0, 0.0
|
||||||
|
custom_default_extrusion_rate = 0.0
|
||||||
|
default_layer_height = 0.2
|
||||||
|
default_layer_width = 0.4
|
||||||
|
|
||||||
|
last_v_idx = -1
|
||||||
|
last_p2 = None
|
||||||
|
|
||||||
|
prep_translation = [0.0, 0.0, 0.0]
|
||||||
|
prep_rotation = [0.0, 0.0, 0.0]
|
||||||
|
prep_scale = [1.0, 1.0, 1.0]
|
||||||
|
|
||||||
|
for line in gcode.splitlines():
|
||||||
|
# Parse metadata from comments
|
||||||
|
if ';' in line:
|
||||||
|
comment = line.split(';', 1)[1].strip()
|
||||||
|
if comment.startswith("OFFSET_X="):
|
||||||
|
try: offset_x = float(comment.split('=')[1])
|
||||||
|
except ValueError: pass
|
||||||
|
elif comment.startswith("OFFSET_Y="):
|
||||||
|
try: offset_y = float(comment.split('=')[1])
|
||||||
|
except ValueError: pass
|
||||||
|
elif comment.startswith("DEFAULT_EXTRUSION_RATE="):
|
||||||
|
try: custom_default_extrusion_rate = float(comment.split('=')[1])
|
||||||
|
except ValueError: pass
|
||||||
|
elif comment.startswith("DEFAULT_LAYER_HEIGHT="):
|
||||||
|
try: default_layer_height = float(comment.split('=')[1])
|
||||||
|
except ValueError: pass
|
||||||
|
elif comment.startswith("DEFAULT_LAYER_WIDTH="):
|
||||||
|
try: default_layer_width = float(comment.split('=')[1])
|
||||||
|
except ValueError: pass
|
||||||
|
elif comment.startswith("PREPARATION_TOTAL_TRANSLATION="):
|
||||||
|
try:
|
||||||
|
vals = [float(v) for v in comment.split('=')[1].split(',')]
|
||||||
|
if len(vals) >= 3: prep_translation = vals[:3]
|
||||||
|
except ValueError: pass
|
||||||
|
elif comment.startswith("PREPARATION_TOTAL_ROTATION="):
|
||||||
|
try:
|
||||||
|
vals = [float(v) for v in comment.split('=')[1].split(',')]
|
||||||
|
if len(vals) >= 3: prep_rotation = vals[:3]
|
||||||
|
except ValueError: pass
|
||||||
|
elif comment.startswith("PREPARATION_TOTAL_SCALE="):
|
||||||
|
try:
|
||||||
|
vals = [float(v) for v in comment.split('=')[1].split(',')]
|
||||||
|
if len(vals) >= 3: prep_scale = vals[:3]
|
||||||
|
except ValueError: pass
|
||||||
|
|
||||||
|
line_no_comment = line.split(';')[0].strip()
|
||||||
|
if not line_no_comment: continue
|
||||||
|
|
||||||
|
parts = line_no_comment.split()
|
||||||
|
if not parts: continue
|
||||||
|
cmd = parts[0].upper()
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
for p in parts[1:]:
|
||||||
|
if len(p) < 2: continue
|
||||||
|
try:
|
||||||
|
val = float(p[1:])
|
||||||
|
if p[0].upper() in 'XYZ':
|
||||||
|
val *= scale_factor
|
||||||
|
params[p[0].upper()] = val
|
||||||
|
except ValueError: continue
|
||||||
|
|
||||||
|
new_x = params.get('X', last_x)
|
||||||
|
new_y = params.get('Y', last_y)
|
||||||
|
new_z = params.get('Z', last_z)
|
||||||
|
new_e = params.get('E', last_e)
|
||||||
|
|
||||||
|
show = False
|
||||||
|
if show_e_0: show = True
|
||||||
|
if cmd == 'G1' and new_e > 0: show = True
|
||||||
|
|
||||||
|
if show:
|
||||||
|
ox, oy = offset_x * scale_factor, offset_y * scale_factor
|
||||||
|
p1 = (last_x - ox, last_y - oy, last_z)
|
||||||
|
p2 = (new_x - ox, new_y - oy, new_z)
|
||||||
|
|
||||||
|
seg_len_mm = math.dist(p1, p2) * (1000.0 * scale_length)
|
||||||
|
if seg_len_mm > 0 and custom_default_extrusion_rate > 0:
|
||||||
|
e_rate = (new_e / seg_len_mm) / custom_default_extrusion_rate
|
||||||
|
else:
|
||||||
|
e_rate = 1.0
|
||||||
|
|
||||||
|
if last_v_idx != -1 and p1 == last_p2:
|
||||||
|
v2_idx = len(vertices)
|
||||||
|
vertices.append(p2)
|
||||||
|
edges.append((last_v_idx, v2_idx))
|
||||||
|
last_v_idx = v2_idx
|
||||||
|
else:
|
||||||
|
v1_idx = len(vertices)
|
||||||
|
v2_idx = v1_idx + 1
|
||||||
|
vertices.extend([p1, p2])
|
||||||
|
edges.append((v1_idx, v2_idx))
|
||||||
|
last_v_idx = v2_idx
|
||||||
|
|
||||||
|
e_rates.append(e_rate)
|
||||||
|
last_p2 = p2
|
||||||
|
else:
|
||||||
|
last_v_idx = -1
|
||||||
|
last_p2 = None
|
||||||
|
|
||||||
|
last_x, last_y, last_z = new_x, new_y, new_z
|
||||||
|
last_e = new_e
|
||||||
|
|
||||||
|
return {
|
||||||
|
"vertices": vertices,
|
||||||
|
"edges": edges,
|
||||||
|
"e_rates": e_rates,
|
||||||
|
"default_layer_height": default_layer_height,
|
||||||
|
"prep_translation": prep_translation,
|
||||||
|
"prep_rotation": prep_rotation,
|
||||||
|
"prep_scale": prep_scale,
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Any, TypeAlias
|
from typing import Any, TypeAlias
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from ..bpy_ns_types import NfEnumProperty, notify_tree_changed
|
|
||||||
|
|
||||||
|
from ..bpy_ns_types import NfEnumProperty
|
||||||
from ...slice import Pather, slice_at_range, SlicePaths
|
from ...slice import Pather, slice_at_range, SlicePaths
|
||||||
from ....infra.blender.blender_mesh_capture import SlicingCollection, SlicingObject, capture_mesh_edges, SlicingGroup
|
from ....infra.blender.blender_mesh_capture import SlicingCollection, SlicingObject, capture_mesh_edges, SlicingGroup
|
||||||
from ....infra.blender.blender_bridge import get_all_children
|
from ....infra.blender.blender_bridge import get_all_children
|
||||||
from ....infra.blender.blender_slice_nodes_generic import SliceNode, resolve_input
|
from ....infra.blender.blender_slice_nodes_generic import SliceNode, resolve_input
|
||||||
|
from ...functions.math_functions import MATH_OPERATIONS
|
||||||
|
|
||||||
|
_MATH_OPS = MATH_OPERATIONS
|
||||||
|
|
||||||
class SliceIntrinsics:
|
class SliceIntrinsics:
|
||||||
bed_x: float
|
bed_x: float
|
||||||
|
|
@ -56,24 +57,6 @@ class InputNode(SliceNode):
|
||||||
if out is None: return None
|
if out is None: return None
|
||||||
return getattr(out, "value", getattr(out, "default_value", None))
|
return getattr(out, "value", getattr(out, "default_value", None))
|
||||||
|
|
||||||
_MATH_OPS: dict[str, Any] = {
|
|
||||||
'ADD': lambda a, b, t: a + b,
|
|
||||||
'SUBTRACT': lambda a, b, t: a - b,
|
|
||||||
'MULTIPLY': lambda a, b, t: a * b,
|
|
||||||
'DIVIDE': lambda a, b, t: a / b if b != 0 else 0,
|
|
||||||
'ABS': lambda a, b, t: abs(a),
|
|
||||||
'NEGATE': lambda a, b, t: -a,
|
|
||||||
'POWER': lambda a, b, t: t(a ** b) if not (a == 0 and b < 0) else 0,
|
|
||||||
'MIN': lambda a, b, t: min(a, b),
|
|
||||||
'MAX': lambda a, b, t: max(a, b),
|
|
||||||
'SIGN': lambda a, b, t: (a > 0) - (a < 0),
|
|
||||||
'DIVIDE_ROUND': lambda a, b, t: t(round(a / b)) if b != 0 else 0,
|
|
||||||
'DIVIDE_FLOOR': lambda a, b, t: a // b if b != 0 else 0,
|
|
||||||
'DIVIDE_TRUNCATE': lambda a, b, t: t(a / b) if b != 0 else 0,
|
|
||||||
'MODULO_FLOORED': lambda a, b, t: a % b if b != 0 else 0,
|
|
||||||
'MODULO_TRUNCATED': lambda a, b, t: a - (b * t(a / b)) if b != 0 else 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
class MathNode(SliceNode):
|
class MathNode(SliceNode):
|
||||||
bl_label = "Float Math"
|
bl_label = "Float Math"
|
||||||
|
|
||||||
|
|
@ -105,7 +88,6 @@ class MathNode(SliceNode):
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_output(self.socket_type, "Value")
|
self.add_output(self.socket_type, "Value")
|
||||||
|
|
||||||
self.add_input(self.socket_type, "Value")
|
self.add_input(self.socket_type, "Value")
|
||||||
self.add_input(self.socket_type, "Value")
|
self.add_input(self.socket_type, "Value")
|
||||||
|
|
||||||
|
|
@ -135,7 +117,7 @@ class MathNode(SliceNode):
|
||||||
a_raw = resolve_input(inputs[0]) if len(inputs) > 0 else None
|
a_raw = resolve_input(inputs[0]) if len(inputs) > 0 else None
|
||||||
b_raw = resolve_input(inputs[1]) if len(inputs) > 1 else None
|
b_raw = resolve_input(inputs[1]) if len(inputs) > 1 else None
|
||||||
|
|
||||||
op_fn = _MATH_OPS.get(self.operation)
|
op_fn = MATH_OPERATIONS.get(self.operation)
|
||||||
if not op_fn:
|
if not op_fn:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any, TypeAlias
|
||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ..bpy_ns_types import NfEnumProperty, notify_tree_changed
|
||||||
|
|
||||||
|
from ...slice import Pather, slice_at_range, SlicePaths
|
||||||
|
from ....infra.blender.blender_mesh_capture import SlicingCollection, SlicingObject, capture_mesh_edges, SlicingGroup
|
||||||
|
from ....infra.blender.blender_bridge import get_all_children
|
||||||
|
from ....infra.blender.blender_slice_nodes_generic import SliceNode, resolve_input
|
||||||
|
|
||||||
|
class SliceIntrinsics:
|
||||||
|
bed_x: float
|
||||||
|
bed_y: float
|
||||||
|
feedrate: float
|
||||||
|
travel_feedrate: float
|
||||||
|
extrusion_rate: float
|
||||||
|
retraction: float
|
||||||
|
lift: float
|
||||||
|
retraction_feedrate: float
|
||||||
|
|
||||||
|
def __init__(self, bed_x: float = 250.0, bed_y: float = 250.0, feedrate: float = 1200.0, travel_feedrate: float = 2400.0, extrusion_rate: float = 0.017, retraction: float = 0.0, lift: float = 0.0, retraction_feedrate: float = 0.0):
|
||||||
|
self.bed_x = bed_x
|
||||||
|
self.bed_y = bed_y
|
||||||
|
self.feedrate = feedrate
|
||||||
|
self.travel_feedrate = travel_feedrate
|
||||||
|
self.extrusion_rate = extrusion_rate
|
||||||
|
self.retraction = retraction
|
||||||
|
self.lift = lift
|
||||||
|
self.retraction_feedrate = retraction_feedrate
|
||||||
|
|
||||||
|
class InputNode(SliceNode):
|
||||||
|
implicit_multivalue: bool = False
|
||||||
|
input_type: str = ""
|
||||||
|
socket_type: str = ""
|
||||||
|
|
||||||
|
def __init_subclass__(cls, **kwargs):
|
||||||
|
super().__init_subclass__(**kwargs)
|
||||||
|
if "bl_label" not in cls.__dict__: cls.bl_label = cls.input_type
|
||||||
|
|
||||||
|
def init(self, context) -> None:
|
||||||
|
stype = self.socket_type or (f"NodeSocket{self.bl_label}" if not self.bl_label.startswith("NodeSocket") else self.bl_label)
|
||||||
|
self.add_output(stype)
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
if self.outputs:
|
||||||
|
sock = self.outputs[0]
|
||||||
|
if hasattr(sock, "default_value"):
|
||||||
|
layout.prop(sock, "default_value", text="")
|
||||||
|
elif hasattr(sock, "value"):
|
||||||
|
layout.prop(sock, "value", text="")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs):
|
||||||
|
out = self.outputs.get(socket_name) or (self.outputs[0] if self.outputs else None)
|
||||||
|
if out is None: return None
|
||||||
|
return getattr(out, "value", getattr(out, "default_value", None))
|
||||||
|
|
||||||
|
_MATH_OPS: dict[str, Any] = {
|
||||||
|
'ADD': lambda a, b, t: a + b,
|
||||||
|
'SUBTRACT': lambda a, b, t: a - b,
|
||||||
|
'MULTIPLY': lambda a, b, t: a * b,
|
||||||
|
'DIVIDE': lambda a, b, t: a / b if b != 0 else 0,
|
||||||
|
'ABS': lambda a, b, t: abs(a),
|
||||||
|
'NEGATE': lambda a, b, t: -a,
|
||||||
|
'POWER': lambda a, b, t: t(a ** b) if not (a == 0 and b < 0) else 0,
|
||||||
|
'MIN': lambda a, b, t: min(a, b),
|
||||||
|
'MAX': lambda a, b, t: max(a, b),
|
||||||
|
'SIGN': lambda a, b, t: (a > 0) - (a < 0),
|
||||||
|
'DIVIDE_ROUND': lambda a, b, t: t(round(a / b)) if b != 0 else 0,
|
||||||
|
'DIVIDE_FLOOR': lambda a, b, t: a // b if b != 0 else 0,
|
||||||
|
'DIVIDE_TRUNCATE': lambda a, b, t: t(a / b) if b != 0 else 0,
|
||||||
|
'MODULO_FLOORED': lambda a, b, t: a % b if b != 0 else 0,
|
||||||
|
'MODULO_TRUNCATED': lambda a, b, t: a - (b * t(a / b)) if b != 0 else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
class MathNode(SliceNode):
|
||||||
|
bl_label = "Float Math"
|
||||||
|
|
||||||
|
operation: str = NfEnumProperty(
|
||||||
|
name="Operation",
|
||||||
|
items=[
|
||||||
|
('ADD', "Add", "Add the two values"),
|
||||||
|
('SUBTRACT', "Subtract", "Subtract the second value from the first"),
|
||||||
|
('MULTIPLY', "Multiply", "Multiply the two values"),
|
||||||
|
('DIVIDE', "Divide", "Division"),
|
||||||
|
('ABS', "Absolute", "Absolute value of the first value"),
|
||||||
|
('NEGATE', "Negate", "Negate the first value"),
|
||||||
|
('POWER', "Power", "First value raised to the power of the second"),
|
||||||
|
('MIN', "Minimum", "The smaller of the two values"),
|
||||||
|
('MAX', "Maximum", "The larger of the two values"),
|
||||||
|
('SIGN', "Sign", "The sign of the first value (-1, 0, or 1)"),
|
||||||
|
('DIVIDE_ROUND', "Divide Round", "Division rounded to the nearest integer"),
|
||||||
|
('DIVIDE_FLOOR', "Divide Floor", "Division rounded down"),
|
||||||
|
('DIVIDE_TRUNCATE', "Divide Truncate", "Division rounded towards zero"),
|
||||||
|
('MODULO_FLOORED', "Floored Modulo", "Modulo using floor division"),
|
||||||
|
('MODULO_TRUNCATED', "Truncated Modulo", "Modulo using truncated division"),
|
||||||
|
],
|
||||||
|
default='ADD'
|
||||||
|
)
|
||||||
|
|
||||||
|
math_type: TypeAlias = float
|
||||||
|
socket_label: str = "Float"
|
||||||
|
socket_type: str = "NodeSocketFloat"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_output(self.socket_type, "Value")
|
||||||
|
|
||||||
|
self.add_input(self.socket_type, "Value")
|
||||||
|
self.add_input(self.socket_type, "Value")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "operation", text="")
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
if self.inputs:
|
||||||
|
a_list = is_socket_linked_to_list(self.inputs[0])
|
||||||
|
b_list = is_socket_linked_to_list(self.inputs[1]) if len(self.inputs) > 1 else False
|
||||||
|
set_socket_shape(self.inputs[0], "LIST" if a_list else "CIRCLE")
|
||||||
|
if len(self.inputs) > 1:
|
||||||
|
set_socket_shape(self.inputs[1], "LIST" if b_list else "CIRCLE")
|
||||||
|
if self.outputs:
|
||||||
|
set_socket_shape(self.outputs[0], "LIST" if (a_list or b_list) else "CIRCLE")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
t = self.math_type
|
||||||
|
inputs = getattr(self, "inputs", [])
|
||||||
|
a_raw = self.resolve(inputs[0] if len(inputs) > 0 else 0)
|
||||||
|
b_raw = self.resolve(inputs[1] if len(inputs) > 1 else 1)
|
||||||
|
|
||||||
|
op_fn = _MATH_OPS.get(self.operation)
|
||||||
|
if not op_fn:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
a_num = t(a_raw if a_raw is not None else 0)
|
||||||
|
b_num = t(b_raw if b_raw is not None else 0)
|
||||||
|
return op_fn(a_num, b_num, t)
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
class SliceNodeJoiner(SliceNode):
|
||||||
|
implicit_multivalue: bool = False
|
||||||
|
socket_type: str = ""
|
||||||
|
output_socket_type: str = ""
|
||||||
|
socket_label: str = ""
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_output(self.output_socket_type or self.socket_type, name=self.socket_label)
|
||||||
|
self.add_multi_socket_group(self.socket_type, name=self.socket_label)
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
for inp in self.inputs:
|
||||||
|
if getattr(inp, "name", "") == self.socket_label or getattr(inp, "identifier", "").startswith(f"{self.socket_label}_ms_in_"):
|
||||||
|
is_list = is_socket_linked_to_list(inp)
|
||||||
|
set_socket_shape(inp, "LIST" if is_list else "CIRCLE")
|
||||||
|
|
||||||
|
class SliceNodeListJoiner(SliceNode):
|
||||||
|
implicit_multivalue: bool = False
|
||||||
|
socket_type: str = ""
|
||||||
|
output_socket_type: str = ""
|
||||||
|
socket_label: str = ""
|
||||||
|
output_socket_name: str = ""
|
||||||
|
is_list_output: bool = True
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_output(self.output_socket_type or self.socket_type, name=self.output_socket_name or self.socket_label, display_shape='LIST')
|
||||||
|
self.add_multi_socket_group(self.socket_type, name=self.socket_label)
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
for inp in self.inputs:
|
||||||
|
if getattr(inp, "name", "") == self.socket_label or getattr(inp, "identifier", "").startswith(f"{self.socket_label}_ms_in_"):
|
||||||
|
is_list = is_socket_linked_to_list(inp)
|
||||||
|
set_socket_shape(inp, "LIST" if is_list else "CIRCLE")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **kwargs) -> list[Any]:
|
||||||
|
from ....infra.blender.list_utils import flatten_list, is_list_data
|
||||||
|
items: list[Any] = []
|
||||||
|
|
||||||
|
def _append_val(v: Any) -> None:
|
||||||
|
if v is not None:
|
||||||
|
if is_list_data(v):
|
||||||
|
items.extend(flatten_list(v))
|
||||||
|
else:
|
||||||
|
items.append(v)
|
||||||
|
|
||||||
|
target_inputs = [
|
||||||
|
i for i in getattr(self, "inputs", [])
|
||||||
|
if getattr(i, "name", "") == self.socket_label or getattr(i, "identifier", "").startswith(f"{self.socket_label}_ms_in_")
|
||||||
|
]
|
||||||
|
|
||||||
|
if "resolve_multi_input" in self.__dict__ or not target_inputs:
|
||||||
|
for val in self.resolve_multi_input(self.socket_label):
|
||||||
|
_append_val(val)
|
||||||
|
return items
|
||||||
|
|
||||||
|
cache = getattr(self, "_current_cache", None)
|
||||||
|
visited = getattr(self, "_current_visited", None)
|
||||||
|
from ....infra.blender.blender_slice_nodes_generic import resolve_input
|
||||||
|
|
||||||
|
for inp in target_inputs:
|
||||||
|
if not getattr(inp, "is_linked", False):
|
||||||
|
val = getattr(inp, "value", None)
|
||||||
|
if val is None or val == "":
|
||||||
|
val = getattr(inp, "default_value", None)
|
||||||
|
if val is None or val == "":
|
||||||
|
continue
|
||||||
|
if type(val).__name__ == "SlicePaths" and len(val) == 0:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
val = resolve_input(inp, cache=cache, visited=visited, fieldname=self.socket_label, node=self)
|
||||||
|
|
||||||
|
_append_val(val)
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
class ExecSliceNode(SliceNode):
|
||||||
|
button_label: str = 'Execute'
|
||||||
|
|
||||||
|
def _init_exec(self):
|
||||||
|
self.add_output("SLICESOCK_ExecutionLink")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.operator("slicenode.execute", text=self.button_label).node_name = self.name
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
if socket_name == "Execution Link":
|
||||||
|
return self
|
||||||
|
return None
|
||||||
|
|
@ -1,274 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Any
|
|
||||||
import numpy as np
|
|
||||||
import bpy
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ..bpy_ns_types import NfBoolProperty, NfEnumProperty, NfStringProperty
|
|
||||||
from ....registry import register_class
|
|
||||||
from .base import SliceNode, SliceIntrinsics, Pather, SliceNodeJoiner, SliceNodeListJoiner
|
|
||||||
from ....infra.blender.list_utils import eval_element_wise, eval_element_wise_multivalue, prepare_indexed_inputs, flatten_list, is_list_data
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_GcodesToList(SliceNodeListJoiner):
|
|
||||||
bl_label = "Gcodes to List"
|
|
||||||
socket_type = "NodeSocketString"
|
|
||||||
socket_label = "Gcode"
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_PathsToGcode(SliceNode):
|
|
||||||
bl_label = "Paths to Gcode"
|
|
||||||
list_socket_mapping = [("Paths", "Gcode")]
|
|
||||||
|
|
||||||
use_header: bool = NfBoolProperty(name="Header", default=True)
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self.add_input("SLICESOCK_Paths")
|
|
||||||
self.add_input("SLICESOCK_Intrinsics")
|
|
||||||
self.add_output("NodeSocketString", name="Gcode")
|
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
|
||||||
assert layout
|
|
||||||
layout.prop(self, "use_header")
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
|
||||||
def _paths_to_gcode(p: Any, intr: Any) -> str:
|
|
||||||
if not p:
|
|
||||||
return ""
|
|
||||||
pather: Pather = Pather(p)
|
|
||||||
if hasattr(p, "layer_height"):
|
|
||||||
pather.layer_height = getattr(p, "layer_height")
|
|
||||||
if hasattr(p, "first_layer_height"):
|
|
||||||
pather.first_layer_height = getattr(p, "first_layer_height")
|
|
||||||
|
|
||||||
if intr:
|
|
||||||
pather.bed_center_x = intr.bed_x / 2.0
|
|
||||||
pather.bed_center_y = intr.bed_y / 2.0
|
|
||||||
pather.feedrate = intr.feedrate
|
|
||||||
pather.travel_feedrate = intr.travel_feedrate
|
|
||||||
pather.extrusion_rate = intr.extrusion_rate
|
|
||||||
pather.retraction = intr.retraction
|
|
||||||
pather.retraction_feedrate = getattr(intr, "retraction_feedrate", 0.0)
|
|
||||||
pather.lift = intr.lift
|
|
||||||
|
|
||||||
return pather.to_gcode(include_header=self.use_header)
|
|
||||||
|
|
||||||
return self.eval_multivalue(
|
|
||||||
_paths_to_gcode,
|
|
||||||
main_socket="Paths",
|
|
||||||
secondary_sockets=["Intrinsics"],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default="",
|
|
||||||
)
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_GcodeBlock(SliceNode):
|
|
||||||
bl_label = "Gcode Block"
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self.add_output("NodeSocketString", "Gcode")
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
|
||||||
return self.outputs['Gcode'].value
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_AssignMetadata(SliceNode):
|
|
||||||
bl_label = "Assign Metadata"
|
|
||||||
list_socket_mapping = [("Name", "Gcode")]
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self.add_input("NodeSocketString", "Name")
|
|
||||||
self.add_input("NodeSocketString", "Filament Type")
|
|
||||||
self.add_input("NodeSocketString", "Printer Model")
|
|
||||||
self.add_output("NodeSocketString", "Gcode")
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
|
||||||
def _meta_one(n: Any, f: Any, p: Any) -> str:
|
|
||||||
lines = []
|
|
||||||
if n:
|
|
||||||
lines.append(f'; objects_info = {{"objects": [{{"name": "{n}", "polygon": []}}]}}')
|
|
||||||
if f:
|
|
||||||
lines.append(f'; filament_type = {f}')
|
|
||||||
if p:
|
|
||||||
lines.append(f'; printer_model = {p}')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
return self.eval_multivalue(
|
|
||||||
_meta_one,
|
|
||||||
main_socket="Name",
|
|
||||||
secondary_sockets=["Filament Type", "Printer Model"],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default="",
|
|
||||||
)
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_GcodeRegexReplace(SliceNode):
|
|
||||||
bl_label = "Gcode Regex Replace"
|
|
||||||
list_socket_mapping = [("Gcode", "Gcode")]
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self.add_input("NodeSocketString", "Gcode")
|
|
||||||
self.add_input("NodeSocketString", "Find")
|
|
||||||
self.add_input("NodeSocketString", "Replace")
|
|
||||||
self.add_output("NodeSocketString", "Gcode")
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
|
||||||
import re
|
|
||||||
|
|
||||||
def _replace_one(gc: Any, f_pattern: Any, r_pattern: Any) -> str:
|
|
||||||
gc_str = str(gc or "")
|
|
||||||
f_str = str(f_pattern or "")
|
|
||||||
r_str = str(r_pattern or "")
|
|
||||||
if not f_str or not gc_str:
|
|
||||||
return gc_str
|
|
||||||
try:
|
|
||||||
# Convert $1, $2, ${name} etc to Python backreferences \g<1>, \g<2>, \g<name>
|
|
||||||
def subst(match):
|
|
||||||
if match.group(1): return '$'
|
|
||||||
elif match.group(2): return '\\g<' + match.group(2) + '>'
|
|
||||||
elif match.group(3): return '\\g<' + match.group(3) + '>'
|
|
||||||
return match.group(0)
|
|
||||||
|
|
||||||
rep = re.sub(r'(\$\$)|(?:\$\{([^}]+)\})|(?:\$([a-zA-Z0-9_]+))', subst, r_str)
|
|
||||||
return re.sub(f_str, rep, gc_str)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Regex error in node {self.name}: {e}")
|
|
||||||
return gc_str
|
|
||||||
|
|
||||||
return self.eval_multivalue(
|
|
||||||
_replace_one,
|
|
||||||
main_socket="Gcode",
|
|
||||||
secondary_sockets=["Find", "Replace"],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default="",
|
|
||||||
)
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_GcodeHelper(SliceNode):
|
|
||||||
bl_label = "Gcode Helper"
|
|
||||||
|
|
||||||
command: str = NfEnumProperty(
|
|
||||||
name="Command",
|
|
||||||
items=[
|
|
||||||
('G0', "G0: Rapid Move", "Fast movement without extrusion"),
|
|
||||||
('G1', "G1: Linear Move", "Movement with extrusion"),
|
|
||||||
('G4', "G4: Dwell", "Wait for time (e.g. P500 for 500ms or S5 for 5s)"),
|
|
||||||
('G28', "G28: Home All", "Home all axes"),
|
|
||||||
('G90', "G90: Absolute Pos", "Set to absolute positioning"),
|
|
||||||
('G91', "G91: Relative Pos", "Set to relative positioning"),
|
|
||||||
('G92', "G92: Set Position", "Set the current position to specific values (e.g. G92 E0)"),
|
|
||||||
('M0', "M0: Stop/Wait", "Stop and wait for user button press"),
|
|
||||||
('M25', "M25: Pause SD", "Pause SD print (Marlin/RepRap)"),
|
|
||||||
('M104', "M104: Set Temp", "Set hotend temperature"),
|
|
||||||
('M109', "M109: Wait Temp", "Set hotend temperature and wait"),
|
|
||||||
('M140', "M140: Set Bed Temp", "Set bed temperature"),
|
|
||||||
('M190', "M190: Wait Bed Temp", "Set bed temperature and wait"),
|
|
||||||
('M106', "M106: Fan On", "Turn on cooling fan"),
|
|
||||||
('M107', "M107: Fan Off", "Turn off cooling fan"),
|
|
||||||
('M117', "M117: Message", "Display message on printer LCD"),
|
|
||||||
('M220', "M220: Speed Factor", "Set speed percentage (e.g. S110 for 110%)"),
|
|
||||||
('M221', "M221: Flow Factor", "Set flow percentage (e.g. S95 for 95%)"),
|
|
||||||
('M300', "M300: Beep", "Play beep sound"),
|
|
||||||
('M400', "M400: Wait Finish", "Wait for all current moves to finish"),
|
|
||||||
('M600', "M600: Color Change", "Filament change / Color change"),
|
|
||||||
('M82', "M82: Abs Extrusion", "Set extrusion to absolute mode"),
|
|
||||||
('M83', "M83: Rel Extrusion", "Set extrusion to relative mode"),
|
|
||||||
('PAUSE', "PAUSE: Klipper Pause", "Pause print (Klipper macro)"),
|
|
||||||
('T', "T: Change Extruder", "Switch to a different extruder (e.g. T0, T1)"),
|
|
||||||
],
|
|
||||||
default='G1'
|
|
||||||
)
|
|
||||||
|
|
||||||
parameters: str = NfStringProperty(name="Params", default="")
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self.add_output("NodeSocketString")
|
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
|
||||||
assert layout
|
|
||||||
layout.prop(self, "command", text="")
|
|
||||||
# Commands that don't need parameters
|
|
||||||
no_params = {'G28', 'G90', 'G91', 'M0', 'M25', 'M107', 'M400', 'M600', 'M82', 'M83', 'PAUSE'}
|
|
||||||
if self.command not in no_params:
|
|
||||||
layout.prop(self, "parameters", text="Parameters")
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
|
||||||
cmd = self.command
|
|
||||||
params = self.parameters.strip()
|
|
||||||
if params:
|
|
||||||
# T commands usually don't have a space (e.g. T0)
|
|
||||||
if cmd == 'T':
|
|
||||||
return f"{cmd}{params}"
|
|
||||||
return f"{cmd} {params}"
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_JoinGcode(SliceNodeJoiner):
|
|
||||||
bl_label = "Join Gcode"
|
|
||||||
socket_type = "NodeSocketString"
|
|
||||||
output_socket_type = "NodeSocketString"
|
|
||||||
socket_label = "Gcode"
|
|
||||||
|
|
||||||
def update(self) -> None:
|
|
||||||
super().update()
|
|
||||||
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
|
||||||
has_list = any(
|
|
||||||
is_socket_linked_to_list(inp)
|
|
||||||
for inp in self.inputs
|
|
||||||
if getattr(inp, "name", "") == self.socket_label or getattr(inp, "identifier", "").startswith(f"{self.socket_label}_ms_in_")
|
|
||||||
)
|
|
||||||
out_sock = self.outputs.get(self.socket_label)
|
|
||||||
if out_sock:
|
|
||||||
set_socket_shape(out_sock, "LIST" if has_list else "CIRCLE")
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
|
||||||
raw_inputs = self.resolve_multi_input(self.socket_label)
|
|
||||||
has_list = any(is_list_data(inp) for inp in raw_inputs)
|
|
||||||
|
|
||||||
if not has_list:
|
|
||||||
flattened = flatten_list(raw_inputs)
|
|
||||||
gcode_input: list[str] = [str(gc) for gc in flattened if gc]
|
|
||||||
return "\n".join(gcode_input)
|
|
||||||
|
|
||||||
# There is at least one list input
|
|
||||||
list_lengths = [len(inp) for inp in raw_inputs if is_list_data(inp)]
|
|
||||||
if len(set(list_lengths)) > 1:
|
|
||||||
raise ValueError(
|
|
||||||
f"Join Gcode: all input lists must have the same length, got lengths {list_lengths}"
|
|
||||||
)
|
|
||||||
|
|
||||||
n_elements = list_lengths[0] if list_lengths else 0
|
|
||||||
result: list[str] = []
|
|
||||||
for i in range(n_elements):
|
|
||||||
row_parts: list[str] = []
|
|
||||||
for inp in raw_inputs:
|
|
||||||
if is_list_data(inp):
|
|
||||||
val = inp[i]
|
|
||||||
else:
|
|
||||||
val = inp
|
|
||||||
if is_list_data(val):
|
|
||||||
val_str = "\n".join(str(s) for s in flatten_list(val) if s)
|
|
||||||
if val_str:
|
|
||||||
row_parts.append(val_str)
|
|
||||||
elif val:
|
|
||||||
row_parts.append(str(val))
|
|
||||||
result.append("\n".join(row_parts))
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_LoadGcode(SliceNode):
|
|
||||||
bl_label = "Load Gcode File"
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self.add_input("NodeSocketStringFilePath", "Filepath")
|
|
||||||
self.add_output("NodeSocketString", name= "Gcode")
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
|
||||||
path = self.resolve("Filepath")
|
|
||||||
if not path: return ""
|
|
||||||
abs_path = bpy.path.abspath(path)
|
|
||||||
if os.path.exists(abs_path):
|
|
||||||
with open(abs_path, 'r', encoding='utf-8') as f:
|
|
||||||
return f.read()
|
|
||||||
return ""
|
|
||||||
21
core/nodeslicer/nodes/gcode/__init__.py
Normal file
21
core/nodeslicer/nodes/gcode/__init__.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .gcodes_to_list import SLICENODE_GcodesToList
|
||||||
|
from .paths_to_gcode import SLICENODE_PathsToGcode
|
||||||
|
from .gcode_block import SLICENODE_GcodeBlock
|
||||||
|
from .assign_metadata import SLICENODE_AssignMetadata
|
||||||
|
from .gcode_regex_replace import SLICENODE_GcodeRegexReplace
|
||||||
|
from .gcode_helper import SLICENODE_GcodeHelper
|
||||||
|
from .join_gcode import SLICENODE_JoinGcode
|
||||||
|
from .load_gcode import SLICENODE_LoadGcode
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"SLICENODE_GcodesToList",
|
||||||
|
"SLICENODE_PathsToGcode",
|
||||||
|
"SLICENODE_GcodeBlock",
|
||||||
|
"SLICENODE_AssignMetadata",
|
||||||
|
"SLICENODE_GcodeRegexReplace",
|
||||||
|
"SLICENODE_GcodeHelper",
|
||||||
|
"SLICENODE_JoinGcode",
|
||||||
|
"SLICENODE_LoadGcode",
|
||||||
|
]
|
||||||
29
core/nodeslicer/nodes/gcode/assign_metadata.py
Normal file
29
core/nodeslicer/nodes/gcode/assign_metadata.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
from ....functions.gcode_functions import format_gcode_metadata
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_AssignMetadata(SliceNode):
|
||||||
|
bl_label = "Assign Metadata"
|
||||||
|
list_socket_mapping = [("Name", "Gcode")]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("NodeSocketString", "Name")
|
||||||
|
self.add_input("NodeSocketString", "Filament Type")
|
||||||
|
self.add_input("NodeSocketString", "Printer Model")
|
||||||
|
self.add_output("NodeSocketString", "Gcode")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _meta_one(n: Any, f: Any, p: Any) -> str:
|
||||||
|
return format_gcode_metadata(n, f, p)
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_meta_one,
|
||||||
|
main_socket="Name",
|
||||||
|
secondary_sockets=["Filament Type", "Printer Model"],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default="",
|
||||||
|
)
|
||||||
13
core/nodeslicer/nodes/gcode/gcode_block.py
Normal file
13
core/nodeslicer/nodes/gcode/gcode_block.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GcodeBlock(SliceNode):
|
||||||
|
bl_label = "Gcode Block"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_output("NodeSocketString", "Gcode")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||||
|
return self.outputs['Gcode'].value
|
||||||
58
core/nodeslicer/nodes/gcode/gcode_helper.py
Normal file
58
core/nodeslicer/nodes/gcode/gcode_helper.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfEnumProperty, NfStringProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
from ....functions.gcode_functions import format_gcode_command
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GcodeHelper(SliceNode):
|
||||||
|
bl_label = "Gcode Helper"
|
||||||
|
|
||||||
|
command: str = NfEnumProperty(
|
||||||
|
name="Command",
|
||||||
|
items=[
|
||||||
|
('G0', "G0: Rapid Move", "Fast movement without extrusion"),
|
||||||
|
('G1', "G1: Linear Move", "Movement with extrusion"),
|
||||||
|
('G4', "G4: Dwell", "Wait for time (e.g. P500 for 500ms or S5 for 5s)"),
|
||||||
|
('G28', "G28: Home All", "Home all axes"),
|
||||||
|
('G90', "G90: Absolute Pos", "Set to absolute positioning"),
|
||||||
|
('G91', "G91: Relative Pos", "Set to relative positioning"),
|
||||||
|
('G92', "G92: Set Position", "Set the current position to specific values (e.g. G92 E0)"),
|
||||||
|
('M0', "M0: Stop/Wait", "Stop and wait for user button press"),
|
||||||
|
('M25', "M25: Pause SD", "Pause SD print (Marlin/RepRap)"),
|
||||||
|
('M104', "M104: Set Temp", "Set hotend temperature"),
|
||||||
|
('M109', "M109: Wait Temp", "Set hotend temperature and wait"),
|
||||||
|
('M140', "M140: Set Bed Temp", "Set bed temperature"),
|
||||||
|
('M190', "M190: Wait Bed Temp", "Set bed temperature and wait"),
|
||||||
|
('M106', "M106: Fan On", "Turn on cooling fan"),
|
||||||
|
('M107', "M107: Fan Off", "Turn off cooling fan"),
|
||||||
|
('M117', "M117: Message", "Display message on printer LCD"),
|
||||||
|
('M220', "M220: Speed Factor", "Set speed percentage (e.g. S110 for 110%)"),
|
||||||
|
('M221', "M221: Flow Factor", "Set flow percentage (e.g. S95 for 95%)"),
|
||||||
|
('M300', "M300: Beep", "Play beep sound"),
|
||||||
|
('M400', "M400: Wait Finish", "Wait for all current moves to finish"),
|
||||||
|
('M600', "M600: Color Change", "Filament change / Color change"),
|
||||||
|
('M82', "M82: Abs Extrusion", "Set extrusion to absolute mode"),
|
||||||
|
('M83', "M83: Rel Extrusion", "Set extrusion to relative mode"),
|
||||||
|
('PAUSE', "PAUSE: Klipper Pause", "Pause print (Klipper macro)"),
|
||||||
|
('T', "T: Change Extruder", "Switch to a different extruder (e.g. T0, T1)"),
|
||||||
|
],
|
||||||
|
default='G1'
|
||||||
|
)
|
||||||
|
|
||||||
|
parameters: str = NfStringProperty(name="Params", default="")
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_output("NodeSocketString")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "command", text="")
|
||||||
|
# Commands that don't need parameters
|
||||||
|
no_params = {'G28', 'G90', 'G91', 'M0', 'M25', 'M107', 'M400', 'M600', 'M82', 'M83', 'PAUSE'}
|
||||||
|
if self.command not in no_params:
|
||||||
|
layout.prop(self, "parameters", text="Parameters")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||||
|
return format_gcode_command(self.command, self.parameters)
|
||||||
29
core/nodeslicer/nodes/gcode/gcode_regex_replace.py
Normal file
29
core/nodeslicer/nodes/gcode/gcode_regex_replace.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
from ....functions.gcode_functions import regex_replace_gcode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GcodeRegexReplace(SliceNode):
|
||||||
|
bl_label = "Gcode Regex Replace"
|
||||||
|
list_socket_mapping = [("Gcode", "Gcode")]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("NodeSocketString", "Gcode")
|
||||||
|
self.add_input("NodeSocketString", "Find")
|
||||||
|
self.add_input("NodeSocketString", "Replace")
|
||||||
|
self.add_output("NodeSocketString", "Gcode")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _replace_one(gc: Any, f_pattern: Any, r_pattern: Any) -> str:
|
||||||
|
return regex_replace_gcode(gc, f_pattern, r_pattern, node_name=getattr(self, "name", ""))
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_replace_one,
|
||||||
|
main_socket="Gcode",
|
||||||
|
secondary_sockets=["Find", "Replace"],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default="",
|
||||||
|
)
|
||||||
9
core/nodeslicer/nodes/gcode/gcodes_to_list.py
Normal file
9
core/nodeslicer/nodes/gcode/gcodes_to_list.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNodeListJoiner
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GcodesToList(SliceNodeListJoiner):
|
||||||
|
bl_label = "Gcodes to List"
|
||||||
|
socket_type = "NodeSocketString"
|
||||||
|
socket_label = "Gcode"
|
||||||
29
core/nodeslicer/nodes/gcode/join_gcode.py
Normal file
29
core/nodeslicer/nodes/gcode/join_gcode.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNodeJoiner
|
||||||
|
from ....functions.gcode_functions import join_gcode_inputs
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_JoinGcode(SliceNodeJoiner):
|
||||||
|
bl_label = "Join Gcode"
|
||||||
|
socket_type = "NodeSocketString"
|
||||||
|
output_socket_type = "NodeSocketString"
|
||||||
|
socket_label = "Gcode"
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from .....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
has_list = any(
|
||||||
|
is_socket_linked_to_list(inp)
|
||||||
|
for inp in self.inputs
|
||||||
|
if getattr(inp, "name", "") == self.socket_label or getattr(inp, "identifier", "").startswith(f"{self.socket_label}_ms_in_")
|
||||||
|
)
|
||||||
|
out_sock = self.outputs.get(self.socket_label)
|
||||||
|
if out_sock:
|
||||||
|
set_socket_shape(out_sock, "LIST" if has_list else "CIRCLE")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
raw_inputs = self.resolve_multi_input(self.socket_label)
|
||||||
|
return join_gcode_inputs(raw_inputs)
|
||||||
17
core/nodeslicer/nodes/gcode/load_gcode.py
Normal file
17
core/nodeslicer/nodes/gcode/load_gcode.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
from ....functions.gcode_functions import load_gcode_file
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_LoadGcode(SliceNode):
|
||||||
|
bl_label = "Load Gcode File"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("NodeSocketStringFilePath", "Filepath")
|
||||||
|
self.add_output("NodeSocketString", name="Gcode")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||||
|
path = self.resolve("Filepath")
|
||||||
|
return load_gcode_file(path)
|
||||||
35
core/nodeslicer/nodes/gcode/paths_to_gcode.py
Normal file
35
core/nodeslicer/nodes/gcode/paths_to_gcode.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfBoolProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
from ....functions.gcode_functions import paths_to_gcode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_PathsToGcode(SliceNode):
|
||||||
|
bl_label = "Paths to Gcode"
|
||||||
|
list_socket_mapping = [("Paths", "Gcode")]
|
||||||
|
|
||||||
|
use_header: bool = NfBoolProperty(name="Header", default=True)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths")
|
||||||
|
self.add_input("SLICESOCK_Intrinsics")
|
||||||
|
self.add_output("NodeSocketString", name="Gcode")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "use_header")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _paths_to_gcode(p: Any, intr: Any) -> str:
|
||||||
|
return paths_to_gcode(p, intr, use_header=bool(self.use_header))
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_paths_to_gcode,
|
||||||
|
main_socket="Paths",
|
||||||
|
secondary_sockets=["Intrinsics"],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
@ -1,184 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Any
|
|
||||||
import bpy
|
|
||||||
import os
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from ..bpy_ns_types import NfStringProperty, notify_tree_changed, NfEnumProperty, NfPointerProperty
|
|
||||||
from ....registry import register_class
|
|
||||||
from .base import InputNode, SliceNode, SliceIntrinsics, SliceNodeListJoiner
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_InputString(InputNode):
|
|
||||||
bl_label = "String"
|
|
||||||
socket_type = "NodeSocketString"
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_InputFloat(InputNode):
|
|
||||||
bl_label = "Float"
|
|
||||||
socket_type = "NodeSocketFloat"
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_InputInt(InputNode):
|
|
||||||
bl_label = "Integer"
|
|
||||||
socket_type = "NodeSocketInt"
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_InputVectorRotation(InputNode):
|
|
||||||
bl_label = "Vector Rotation"
|
|
||||||
socket_type = "NodeSocketRotation"
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_InputVector(InputNode):
|
|
||||||
bl_label = "Vector"
|
|
||||||
socket_type = "NodeSocketVector"
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_InputObject(InputNode):
|
|
||||||
bl_label = "Object"
|
|
||||||
socket_type = "NodeSocketObject"
|
|
||||||
bl_icon = "OBJECT_DATA"
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_ObjectsToList(SliceNodeListJoiner):
|
|
||||||
bl_label = "Objects to List"
|
|
||||||
socket_type = "NodeSocketObject"
|
|
||||||
socket_label = "Object"
|
|
||||||
output_socket_name = "Objects"
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_CollectionObjects(SliceNode):
|
|
||||||
bl_label = "Collection Objects"
|
|
||||||
|
|
||||||
collection: bpy.types.Collection | None = NfPointerProperty(
|
|
||||||
name="Collection",
|
|
||||||
type=bpy.types.Collection,
|
|
||||||
update=notify_tree_changed
|
|
||||||
)
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self._update_outputs()
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
super().update()
|
|
||||||
self._update_outputs()
|
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
|
||||||
assert layout
|
|
||||||
layout.prop(self, "collection", text="")
|
|
||||||
|
|
||||||
def _update_outputs(self):
|
|
||||||
col = getattr(self, "collection", None)
|
|
||||||
if not col or not hasattr(col, "objects"):
|
|
||||||
self.outputs.clear()
|
|
||||||
return
|
|
||||||
|
|
||||||
objs = [obj for obj in col.objects if getattr(obj, "type", "") == 'MESH']
|
|
||||||
expected_names = sorted([obj.name for obj in objs])
|
|
||||||
|
|
||||||
# Remove outputs that are no longer in the collection
|
|
||||||
to_remove = [o for o in self.outputs if o.name not in expected_names]
|
|
||||||
for o in to_remove:
|
|
||||||
self.outputs.remove(o)
|
|
||||||
|
|
||||||
# Add new outputs
|
|
||||||
for name in expected_names:
|
|
||||||
if name not in self.outputs:
|
|
||||||
self.add_output("NodeSocketObject", name, identifier=name)
|
|
||||||
|
|
||||||
# Re-order to match sorted alphabetical order
|
|
||||||
if hasattr(self.outputs, "find") and hasattr(self.outputs, "move"):
|
|
||||||
for i, name in enumerate(expected_names):
|
|
||||||
current_index = self.outputs.find(name)
|
|
||||||
if current_index != -1 and current_index != i:
|
|
||||||
self.outputs.move(current_index, i)
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **kwargs) -> Any:
|
|
||||||
if self.collection and socket_name in bpy.data.objects:
|
|
||||||
return bpy.data.objects[socket_name]
|
|
||||||
return None
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_InputSettings(InputNode):
|
|
||||||
input_type = "Settings"
|
|
||||||
bl_icon = "OBJECT_DATA"
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_InputPrinter(SliceNode):
|
|
||||||
bl_label = "Printer"
|
|
||||||
bl_icon = "OUTPUT"
|
|
||||||
|
|
||||||
def update_label(self, context):
|
|
||||||
self.label = self.printer_label
|
|
||||||
notify_tree_changed(self, context)
|
|
||||||
|
|
||||||
printer_label: str = NfStringProperty(name="Label", default="Printer", update=update_label)
|
|
||||||
|
|
||||||
host_type: str = NfEnumProperty(
|
|
||||||
name="Type",
|
|
||||||
items=[
|
|
||||||
('PRUSALINK', "PrusaLink", "PrusaLink API"),
|
|
||||||
('CREALITY', "Creality", "Creality Cloud API"),
|
|
||||||
('MOONRAKER', "Moonraker", "Moonraker (Klipper) API"),
|
|
||||||
],
|
|
||||||
default='PRUSALINK'
|
|
||||||
)
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self.add_output("SLICESOCK_Printer")
|
|
||||||
self.add_input("NodeSocketString", "Host").value = "localhost"
|
|
||||||
self.add_input("NodeSocketInt", "Port").value = 80
|
|
||||||
self.add_input("NodeSocketString", "Prefix")
|
|
||||||
self.add_input("NodeSocketString", "Username")
|
|
||||||
self.add_input("NodeSocketString", "Password")
|
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
|
||||||
layout.prop(self, "printer_label") # pyright: ignore[reportOptionalMemberAccess]
|
|
||||||
layout.prop(self, "host_type") # pyright: ignore[reportOptionalMemberAccess]
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
|
||||||
from ....infra.networking.printer_backends import PrinterConfig
|
|
||||||
return PrinterConfig(
|
|
||||||
host=self.resolve("Host"),
|
|
||||||
port=self.resolve("Port"),
|
|
||||||
prefix=self.resolve("Prefix"),
|
|
||||||
label=self.printer_label,
|
|
||||||
username=self.resolve("Username"),
|
|
||||||
password=self.resolve("Password"),
|
|
||||||
host_type=self.host_type
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_InputIntrinsics(SliceNode):
|
|
||||||
|
|
||||||
bl_label = "Printer Intrinsics"
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self.add_output("SLICESOCK_Intrinsics")
|
|
||||||
self.add_input("NodeSocketFloat", "Bed X").value = 250.0
|
|
||||||
self.add_input("NodeSocketFloat", "Bed Y").value = 210.0
|
|
||||||
self.add_input("NodeSocketFloat", "Feedrate").value = 1200.0
|
|
||||||
self.add_input("NodeSocketFloat", "Travel Feedrate").value = 2400.0
|
|
||||||
self.add_input("NodeSocketFloat", "Extrusion Rate").value = 0.035
|
|
||||||
self.add_input("NodeSocketFloat", "Retraction").value = 0.0
|
|
||||||
self.add_input("NodeSocketFloat", "Retraction Feedrate").value = 0.0
|
|
||||||
self.add_input("NodeSocketFloat", "Lift").value = 0.0
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SliceIntrinsics:
|
|
||||||
ret_fr = 0.0
|
|
||||||
try:
|
|
||||||
ret_fr = self.resolve("Retraction Feedrate") or 0.0
|
|
||||||
except Exception:
|
|
||||||
ret_fr = 0.0
|
|
||||||
|
|
||||||
return SliceIntrinsics(
|
|
||||||
bed_x=self.resolve("Bed X"),
|
|
||||||
bed_y=self.resolve("Bed Y"),
|
|
||||||
feedrate=self.resolve("Feedrate"),
|
|
||||||
travel_feedrate=self.resolve("Travel Feedrate"),
|
|
||||||
extrusion_rate=self.resolve("Extrusion Rate"),
|
|
||||||
retraction=self.resolve("Retraction"),
|
|
||||||
lift=self.resolve("Lift"),
|
|
||||||
retraction_feedrate=ret_fr
|
|
||||||
)
|
|
||||||
28
core/nodeslicer/nodes/inputs/__init__.py
Normal file
28
core/nodeslicer/nodes/inputs/__init__.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
from .input_string import SLICENODE_InputString
|
||||||
|
from .input_float import SLICENODE_InputFloat
|
||||||
|
from .input_int import SLICENODE_InputInt
|
||||||
|
from .input_vector_rotation import SLICENODE_InputVectorRotation
|
||||||
|
from .input_vector import SLICENODE_InputVector
|
||||||
|
from .input_object import SLICENODE_InputObject
|
||||||
|
from .objects_to_list import SLICENODE_ObjectsToList
|
||||||
|
from .collection_objects import SLICENODE_CollectionObjects
|
||||||
|
from .input_settings import SLICENODE_InputSettings
|
||||||
|
from .input_printer import SLICENODE_InputPrinter
|
||||||
|
from .input_intrinsics import SLICENODE_InputIntrinsics
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"SLICENODE_InputString",
|
||||||
|
"SLICENODE_InputFloat",
|
||||||
|
"SLICENODE_InputInt",
|
||||||
|
"SLICENODE_InputVectorRotation",
|
||||||
|
"SLICENODE_InputVector",
|
||||||
|
"SLICENODE_InputObject",
|
||||||
|
"SLICENODE_ObjectsToList",
|
||||||
|
"SLICENODE_CollectionObjects",
|
||||||
|
"SLICENODE_InputSettings",
|
||||||
|
"SLICENODE_InputPrinter",
|
||||||
|
"SLICENODE_InputIntrinsics",
|
||||||
|
]
|
||||||
59
core/nodeslicer/nodes/inputs/collection_objects.py
Normal file
59
core/nodeslicer/nodes/inputs/collection_objects.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
from ...bpy_ns_types import notify_tree_changed, NfPointerProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_CollectionObjects(SliceNode):
|
||||||
|
bl_label = "Collection Objects"
|
||||||
|
|
||||||
|
collection: bpy.types.Collection | None = NfPointerProperty(
|
||||||
|
name="Collection",
|
||||||
|
type=bpy.types.Collection,
|
||||||
|
update=notify_tree_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self._update_outputs()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
super().update()
|
||||||
|
self._update_outputs()
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "collection", text="")
|
||||||
|
|
||||||
|
def _update_outputs(self):
|
||||||
|
col = getattr(self, "collection", None)
|
||||||
|
if not col or not hasattr(col, "objects"):
|
||||||
|
self.outputs.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
objs = [obj for obj in col.objects if getattr(obj, "type", "") == 'MESH']
|
||||||
|
expected_names = sorted([obj.name for obj in objs])
|
||||||
|
|
||||||
|
# Remove outputs that are no longer in the collection
|
||||||
|
to_remove = [o for o in self.outputs if o.name not in expected_names]
|
||||||
|
for o in to_remove:
|
||||||
|
self.outputs.remove(o)
|
||||||
|
|
||||||
|
# Add new outputs
|
||||||
|
for name in expected_names:
|
||||||
|
if name not in self.outputs:
|
||||||
|
self.add_output("NodeSocketObject", name, identifier=name)
|
||||||
|
|
||||||
|
# Re-order to match sorted alphabetical order
|
||||||
|
if hasattr(self.outputs, "find") and hasattr(self.outputs, "move"):
|
||||||
|
for i, name in enumerate(expected_names):
|
||||||
|
current_index = self.outputs.find(name)
|
||||||
|
if current_index != -1 and current_index != i:
|
||||||
|
self.outputs.move(current_index, i)
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **kwargs) -> Any:
|
||||||
|
if self.collection and socket_name in bpy.data.objects:
|
||||||
|
return bpy.data.objects[socket_name]
|
||||||
|
return None
|
||||||
8
core/nodeslicer/nodes/inputs/input_float.py
Normal file
8
core/nodeslicer/nodes/inputs/input_float.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import InputNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputFloat(InputNode):
|
||||||
|
bl_label = "Float"
|
||||||
|
socket_type = "NodeSocketFloat"
|
||||||
8
core/nodeslicer/nodes/inputs/input_int.py
Normal file
8
core/nodeslicer/nodes/inputs/input_int.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import InputNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputInt(InputNode):
|
||||||
|
bl_label = "Integer"
|
||||||
|
socket_type = "NodeSocketInt"
|
||||||
37
core/nodeslicer/nodes/inputs/input_intrinsics.py
Normal file
37
core/nodeslicer/nodes/inputs/input_intrinsics.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SliceIntrinsics
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputIntrinsics(SliceNode):
|
||||||
|
bl_label = "Printer Intrinsics"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_output("SLICESOCK_Intrinsics")
|
||||||
|
self.add_input("NodeSocketFloat", "Bed X").value = 250.0
|
||||||
|
self.add_input("NodeSocketFloat", "Bed Y").value = 210.0
|
||||||
|
self.add_input("NodeSocketFloat", "Feedrate").value = 1200.0
|
||||||
|
self.add_input("NodeSocketFloat", "Travel Feedrate").value = 2400.0
|
||||||
|
self.add_input("NodeSocketFloat", "Extrusion Rate").value = 0.035
|
||||||
|
self.add_input("NodeSocketFloat", "Retraction").value = 0.0
|
||||||
|
self.add_input("NodeSocketFloat", "Retraction Feedrate").value = 0.0
|
||||||
|
self.add_input("NodeSocketFloat", "Lift").value = 0.0
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SliceIntrinsics:
|
||||||
|
ret_fr = 0.0
|
||||||
|
try:
|
||||||
|
ret_fr = self.resolve("Retraction Feedrate") or 0.0
|
||||||
|
except Exception:
|
||||||
|
ret_fr = 0.0
|
||||||
|
|
||||||
|
return SliceIntrinsics(
|
||||||
|
bed_x=self.resolve("Bed X"),
|
||||||
|
bed_y=self.resolve("Bed Y"),
|
||||||
|
feedrate=self.resolve("Feedrate"),
|
||||||
|
travel_feedrate=self.resolve("Travel Feedrate"),
|
||||||
|
extrusion_rate=self.resolve("Extrusion Rate"),
|
||||||
|
retraction=self.resolve("Retraction"),
|
||||||
|
lift=self.resolve("Lift"),
|
||||||
|
retraction_feedrate=ret_fr
|
||||||
|
)
|
||||||
9
core/nodeslicer/nodes/inputs/input_object.py
Normal file
9
core/nodeslicer/nodes/inputs/input_object.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import InputNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputObject(InputNode):
|
||||||
|
bl_label = "Object"
|
||||||
|
socket_type = "NodeSocketObject"
|
||||||
|
bl_icon = "OBJECT_DATA"
|
||||||
51
core/nodeslicer/nodes/inputs/input_printer.py
Normal file
51
core/nodeslicer/nodes/inputs/input_printer.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfStringProperty, notify_tree_changed, NfEnumProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputPrinter(SliceNode):
|
||||||
|
bl_label = "Printer"
|
||||||
|
bl_icon = "OUTPUT"
|
||||||
|
|
||||||
|
def update_label(self, context):
|
||||||
|
self.label = self.printer_label
|
||||||
|
notify_tree_changed(self, context)
|
||||||
|
|
||||||
|
printer_label: str = NfStringProperty(name="Label", default="Printer", update=update_label)
|
||||||
|
|
||||||
|
host_type: str = NfEnumProperty(
|
||||||
|
name="Type",
|
||||||
|
items=[
|
||||||
|
('PRUSALINK', "PrusaLink", "PrusaLink API"),
|
||||||
|
('CREALITY', "Creality", "Creality Cloud API"),
|
||||||
|
('MOONRAKER', "Moonraker", "Moonraker (Klipper) API"),
|
||||||
|
],
|
||||||
|
default='PRUSALINK'
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_output("SLICESOCK_Printer")
|
||||||
|
self.add_input("NodeSocketString", "Host").value = "localhost"
|
||||||
|
self.add_input("NodeSocketInt", "Port").value = 80
|
||||||
|
self.add_input("NodeSocketString", "Prefix")
|
||||||
|
self.add_input("NodeSocketString", "Username")
|
||||||
|
self.add_input("NodeSocketString", "Password")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
layout.prop(self, "printer_label") # pyright: ignore[reportOptionalMemberAccess]
|
||||||
|
layout.prop(self, "host_type") # pyright: ignore[reportOptionalMemberAccess]
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
from .....infra.networking.printer_backends import PrinterConfig
|
||||||
|
return PrinterConfig(
|
||||||
|
host=self.resolve("Host"),
|
||||||
|
port=self.resolve("Port"),
|
||||||
|
prefix=self.resolve("Prefix"),
|
||||||
|
label=self.printer_label,
|
||||||
|
username=self.resolve("Username"),
|
||||||
|
password=self.resolve("Password"),
|
||||||
|
host_type=self.host_type
|
||||||
|
)
|
||||||
8
core/nodeslicer/nodes/inputs/input_settings.py
Normal file
8
core/nodeslicer/nodes/inputs/input_settings.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import InputNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputSettings(InputNode):
|
||||||
|
input_type = "Settings"
|
||||||
|
bl_icon = "OBJECT_DATA"
|
||||||
8
core/nodeslicer/nodes/inputs/input_string.py
Normal file
8
core/nodeslicer/nodes/inputs/input_string.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import InputNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputString(InputNode):
|
||||||
|
bl_label = "String"
|
||||||
|
socket_type = "NodeSocketString"
|
||||||
8
core/nodeslicer/nodes/inputs/input_vector.py
Normal file
8
core/nodeslicer/nodes/inputs/input_vector.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import InputNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputVector(InputNode):
|
||||||
|
bl_label = "Vector"
|
||||||
|
socket_type = "NodeSocketVector"
|
||||||
8
core/nodeslicer/nodes/inputs/input_vector_rotation.py
Normal file
8
core/nodeslicer/nodes/inputs/input_vector_rotation.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import InputNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_InputVectorRotation(InputNode):
|
||||||
|
bl_label = "Vector Rotation"
|
||||||
|
socket_type = "NodeSocketRotation"
|
||||||
10
core/nodeslicer/nodes/inputs/objects_to_list.py
Normal file
10
core/nodeslicer/nodes/inputs/objects_to_list.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNodeListJoiner
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_ObjectsToList(SliceNodeListJoiner):
|
||||||
|
bl_label = "Objects to List"
|
||||||
|
socket_type = "NodeSocketObject"
|
||||||
|
socket_label = "Object"
|
||||||
|
output_socket_name = "Objects"
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
from ....registry import register_class
|
|
||||||
from .base import MathNode
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_FloatMath(MathNode):
|
|
||||||
bl_label = "Math"
|
|
||||||
math_type = float
|
|
||||||
socket_label: str = "Float"
|
|
||||||
socket_type: str = "NodeSocketFloat"
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_IntMath(MathNode):
|
|
||||||
bl_label = "Integer Math"
|
|
||||||
math_type = int
|
|
||||||
socket_label: str = "Int"
|
|
||||||
socket_type: str = "NodeSocketInt"
|
|
||||||
9
core/nodeslicer/nodes/math/__init__.py
Normal file
9
core/nodeslicer/nodes/math/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .float_math import SLICENODE_FloatMath
|
||||||
|
from .int_math import SLICENODE_IntMath
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"SLICENODE_FloatMath",
|
||||||
|
"SLICENODE_IntMath",
|
||||||
|
]
|
||||||
10
core/nodeslicer/nodes/math/float_math.py
Normal file
10
core/nodeslicer/nodes/math/float_math.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import MathNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_FloatMath(MathNode):
|
||||||
|
bl_label = "Math"
|
||||||
|
math_type = float
|
||||||
|
socket_label: str = "Float"
|
||||||
|
socket_type: str = "NodeSocketFloat"
|
||||||
10
core/nodeslicer/nodes/math/int_math.py
Normal file
10
core/nodeslicer/nodes/math/int_math.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import MathNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_IntMath(MathNode):
|
||||||
|
bl_label = "Integer Math"
|
||||||
|
math_type = int
|
||||||
|
socket_label: str = "Int"
|
||||||
|
socket_type: str = "NodeSocketInt"
|
||||||
|
|
@ -103,11 +103,12 @@ class SLICENODE_JoinMeshGroups(SliceNodeJoiner):
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_GroupToList(SliceNode):
|
class SLICENODE_GroupToList(SliceNode):
|
||||||
bl_label = "Group to List"
|
bl_label = "Group to List"
|
||||||
|
implicit_multivalue = False
|
||||||
always_list_outputs = ["Collections"]
|
always_list_outputs = ["Collections"]
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Mesh", "Group")
|
self.add_input("SLICESOCK_Mesh", "Group")
|
||||||
self.add_output("SLICESOCK_Mesh", "Collections", display_shape='LIST')
|
self.add_output("SLICESOCK_Mesh", "Collections")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> list[SlicingCollection]:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> list[SlicingCollection]:
|
||||||
val = self.resolve("Group")
|
val = self.resolve("Group")
|
||||||
|
|
@ -143,10 +144,11 @@ class SLICENODE_GroupToList(SliceNode):
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_ListToGroup(SliceNode):
|
class SLICENODE_ListToGroup(SliceNode):
|
||||||
bl_label = "List to Group"
|
bl_label = "List to Group"
|
||||||
|
implicit_multivalue = False
|
||||||
always_list_inputs = ["Collections"]
|
always_list_inputs = ["Collections"]
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Mesh", "Collections", display_shape='LIST')
|
self.add_input("SLICESOCK_Mesh", "Collections")
|
||||||
self.add_output("SLICESOCK_Mesh", "Group")
|
self.add_output("SLICESOCK_Mesh", "Group")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup:
|
||||||
|
|
@ -280,12 +282,13 @@ class SLICENODE_SeparateMesh(SliceNode):
|
||||||
res_inverted.objects.append(so_inv)
|
res_inverted.objects.append(so_inv)
|
||||||
return res_selection, res_inverted
|
return res_selection, res_inverted
|
||||||
|
|
||||||
return self.eval_multivalue(
|
sc = self.resolve("Mesh")
|
||||||
lambda sc, a, v: _split_coll(sc, str(a or ""), float(v if v is not None else 0.0)),
|
a = self.resolve("Attribute")
|
||||||
main_socket="Mesh",
|
v = self.resolve("Value")
|
||||||
secondary_sockets=["Attribute", "Value"],
|
res_sel, res_inv = _split_coll(sc, str(a or ""), float(v if v is not None else 0.0))
|
||||||
socket_name=socket_name,
|
if socket_name == "Inverted":
|
||||||
)
|
return res_inv
|
||||||
|
return res_sel
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_SeparateMeshByRange(SliceNode):
|
class SLICENODE_SeparateMeshByRange(SliceNode):
|
||||||
|
|
@ -388,11 +391,14 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
|
||||||
res.objects.append(so_new)
|
res.objects.append(so_new)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
return self.eval_multivalue(_split_coll, "Mesh", ["Attribute"])
|
sc = self.resolve("Mesh")
|
||||||
|
attr = self.resolve("Attribute")
|
||||||
|
return _split_coll(sc, str(attr or ""))
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_ArrangeMesh(SliceNode):
|
class SLICENODE_ArrangeMesh(SliceNode):
|
||||||
bl_label = "Arrange Slicing Group"
|
bl_label = "Arrange Slicing Group"
|
||||||
|
implicit_multivalue = False
|
||||||
list_socket_mapping = [("Slicing Group", "Slicing Group")]
|
list_socket_mapping = [("Slicing Group", "Slicing Group")]
|
||||||
|
|
||||||
method: str = NfEnumProperty(
|
method: str = NfEnumProperty(
|
||||||
|
|
@ -658,19 +664,14 @@ 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:
|
||||||
def _transform_one(g: Any, t: Any, r: Any, s: Any) -> Any:
|
g = self.resolve("Geometry")
|
||||||
t_arr = np.array(t if t is not None else (0.0, 0.0, 0.0), dtype=np.float64)
|
t = self.resolve("Translation")
|
||||||
r_arr = np.array(r if r is not None else (0.0, 0.0, 0.0), dtype=np.float64)
|
r = self.resolve("Rotation")
|
||||||
s_arr = np.array(s if s is not None else (1.0, 1.0, 1.0), dtype=np.float64)
|
s = self.resolve("Scale")
|
||||||
return _transform_geometry_item(g, t_arr, r_arr, s_arr)
|
t_arr = np.array(t if t is not None else (0.0, 0.0, 0.0), dtype=np.float64)
|
||||||
|
r_arr = np.array(r if r is not None else (0.0, 0.0, 0.0), dtype=np.float64)
|
||||||
return self.eval_multivalue(
|
s_arr = np.array(s if s is not None else (1.0, 1.0, 1.0), dtype=np.float64)
|
||||||
_transform_one,
|
return _transform_geometry_item(g, t_arr, r_arr, s_arr)
|
||||||
main_socket="Geometry",
|
|
||||||
secondary_sockets=["Translation", "Rotation", "Scale"],
|
|
||||||
is_vector=[True, True, True],
|
|
||||||
socket_name=socket_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
@ -686,8 +687,6 @@ class SLICENODE_GetTransforms(SliceNode):
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
from ....core.slice import extract_geometry_transforms
|
from ....core.slice import extract_geometry_transforms
|
||||||
from ....infra.blender.list_utils import is_input_a_list
|
|
||||||
|
|
||||||
sock_lower = socket_name.lower()
|
sock_lower = socket_name.lower()
|
||||||
default = (1.0, 1.0, 1.0) if sock_lower == "scale" else (0.0, 0.0, 0.0)
|
default = (1.0, 1.0, 1.0) if sock_lower == "scale" else (0.0, 0.0, 0.0)
|
||||||
idx = 0 if sock_lower == "translation" else (1 if sock_lower == "rotation" else 2)
|
idx = 0 if sock_lower == "translation" else (1 if sock_lower == "rotation" else 2)
|
||||||
|
|
@ -695,31 +694,10 @@ class SLICENODE_GetTransforms(SliceNode):
|
||||||
geom = self.resolve("Geometry")
|
geom = self.resolve("Geometry")
|
||||||
if geom is None:
|
if geom is None:
|
||||||
return default
|
return default
|
||||||
|
trans = extract_geometry_transforms(geom)
|
||||||
is_list = is_input_a_list(geom, is_vector=False)
|
if isinstance(trans, tuple) and len(trans) > idx:
|
||||||
if not is_list and isinstance(geom, (list, tuple)):
|
return trans[idx]
|
||||||
if type(geom).__name__ != "SlicePaths" and not getattr(geom, "is_slice_paths", False):
|
return default
|
||||||
is_list = True
|
|
||||||
elif not is_list and hasattr(geom, "__iter__") and not hasattr(geom, "shape") and not isinstance(geom, (str, bytes, dict, set)) and type(geom).__name__ != "SlicePaths" and not getattr(geom, "is_slice_paths", False) and not hasattr(geom, "mesh") and not hasattr(geom, "objects") and not hasattr(geom, "collections") and not hasattr(geom, "location"):
|
|
||||||
is_list = True
|
|
||||||
|
|
||||||
if is_list:
|
|
||||||
items = list(geom)
|
|
||||||
if len(items) == 0:
|
|
||||||
return []
|
|
||||||
results = []
|
|
||||||
for item in items:
|
|
||||||
trans = extract_geometry_transforms(item)
|
|
||||||
if isinstance(trans, tuple) and len(trans) > idx:
|
|
||||||
results.append(trans[idx])
|
|
||||||
else:
|
|
||||||
results.append(default)
|
|
||||||
return results
|
|
||||||
else:
|
|
||||||
trans = extract_geometry_transforms(geom)
|
|
||||||
if isinstance(trans, tuple) and len(trans) > idx:
|
|
||||||
return trans[idx]
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
@ -734,36 +712,31 @@ class SLICENODE_DropGeometry(SliceNode):
|
||||||
self.add_output("NodeSocketVector", "Translation")
|
self.add_output("NodeSocketVector", "Translation")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
def _drop_item(item: Any, target_z: Any) -> tuple[Any, tuple[float, float, float]]:
|
item = self.resolve("Mesh")
|
||||||
if not item or getattr(item, "min_z", None) is None:
|
target_z = self.resolve("Z")
|
||||||
return item, (0.0, 0.0, 0.0)
|
if not item or getattr(item, "min_z", None) is None:
|
||||||
z_float = float(target_z if target_z is not None else 0.0)
|
return (0.0, 0.0, 0.0) if socket_name == "Translation" else item
|
||||||
min_z = round(item.min_z, 6)
|
z_float = float(target_z if target_z is not None else 0.0)
|
||||||
offset_z = float(z_float - min_z)
|
min_z = round(item.min_z, 6)
|
||||||
if isinstance(item, SlicingGroup):
|
offset_z = float(z_float - min_z)
|
||||||
copied_data = _copy_slicing_group(item)
|
if isinstance(item, SlicingGroup):
|
||||||
else:
|
copied_data = _copy_slicing_group(item)
|
||||||
copied_data = _copy_slicing_collection(item)
|
else:
|
||||||
drop_vec = np.array([0.0, 0.0, offset_z], dtype=np.float64)
|
copied_data = _copy_slicing_collection(item)
|
||||||
copied_data.offset(drop_vec)
|
drop_vec = np.array([0.0, 0.0, offset_z], dtype=np.float64)
|
||||||
copied_data.transforms = update_transform(getattr(copied_data, "transforms", []), t_arr=drop_vec)
|
copied_data.offset(drop_vec)
|
||||||
if isinstance(copied_data, SlicingGroup):
|
copied_data.transforms = update_transform(getattr(copied_data, "transforms", []), t_arr=drop_vec)
|
||||||
for col in copied_data.collections.values():
|
if isinstance(copied_data, SlicingGroup):
|
||||||
col.transforms = update_transform(getattr(col, "transforms", []), t_arr=drop_vec)
|
for col in copied_data.collections.values():
|
||||||
for so in col.objects:
|
col.transforms = update_transform(getattr(col, "transforms", []), t_arr=drop_vec)
|
||||||
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
|
for so in col.objects:
|
||||||
elif isinstance(copied_data, SlicingCollection):
|
|
||||||
for so in copied_data.objects:
|
|
||||||
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
|
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
|
||||||
return copied_data, (0.0, 0.0, offset_z)
|
elif isinstance(copied_data, SlicingCollection):
|
||||||
|
for so in copied_data.objects:
|
||||||
return self.eval_multivalue(
|
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
|
||||||
_drop_item,
|
if socket_name == "Translation":
|
||||||
main_socket="Mesh",
|
return (0.0, 0.0, offset_z)
|
||||||
secondary_sockets=["Z"],
|
return copied_data
|
||||||
socket_name=socket_name,
|
|
||||||
default=(0.0, 0.0, 0.0) if socket_name == "Translation" else None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def format_name_template(template: str, so: Any, default_old_name: str = "") -> str:
|
def format_name_template(template: str, so: Any, default_old_name: str = "") -> str:
|
||||||
|
|
@ -837,12 +810,9 @@ class SLICENODE_RenameMesh(SliceNode):
|
||||||
self.add_output("SLICESOCK_Mesh", "Geometry")
|
self.add_output("SLICESOCK_Mesh", "Geometry")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
return self.eval_multivalue(
|
g = self.resolve("Geometry")
|
||||||
lambda g, name: _rename_slicing_item(g, str(name)) if name else g,
|
name = self.resolve("Name")
|
||||||
main_socket="Geometry",
|
return _rename_slicing_item(g, str(name)) if name else g
|
||||||
secondary_sockets=["Name"],
|
|
||||||
socket_name=socket_name
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _assign_pdf_properties_to_item(geom: Any, line_width: float, line_color: tuple[float, float, float] | None, show_name: bool) -> Any:
|
def _assign_pdf_properties_to_item(geom: Any, line_width: float, line_color: tuple[float, float, float] | None, show_name: bool) -> Any:
|
||||||
|
|
@ -899,14 +869,12 @@ class SLICENODE_AssignPdfProperties(SliceNode):
|
||||||
layout.prop(self, "show_name")
|
layout.prop(self, "show_name")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
g = self.resolve("Geometry")
|
||||||
|
line_w = self.resolve("Line Width")
|
||||||
lc = (float(self.line_color[0]), float(self.line_color[1]), float(self.line_color[2]))
|
lc = (float(self.line_color[0]), float(self.line_color[1]), float(self.line_color[2]))
|
||||||
sn = bool(self.show_name)
|
sn = bool(self.show_name)
|
||||||
|
w = float(line_w) if line_w is not None else float(self.line_width)
|
||||||
def _assign_one(g: Any, line_w: Any) -> Any:
|
return _assign_pdf_properties_to_item(g, w, lc, sn)
|
||||||
w = float(line_w) if line_w is not None else float(self.line_width)
|
|
||||||
return _assign_pdf_properties_to_item(g, w, lc, sn)
|
|
||||||
|
|
||||||
return self.eval_multivalue(_assign_one, "Geometry", ["Line Width"], socket_name=socket_name)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_settings_props(raw_settings: Any) -> list[tuple[str, str]]:
|
def _parse_settings_props(raw_settings: Any) -> list[tuple[str, str]]:
|
||||||
|
|
@ -976,9 +944,8 @@ class SLICENODE_AssignPropertyToObject(SliceNode):
|
||||||
self.add_output("SLICESOCK_Mesh", "Geometry")
|
self.add_output("SLICESOCK_Mesh", "Geometry")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
def _assign_one(g: Any, s: Any) -> Any:
|
g = self.resolve("Geometry")
|
||||||
props = _parse_settings_props(s)
|
s = self.resolve("Settings")
|
||||||
return _assign_properties_to_item(g, props)
|
props = _parse_settings_props(s)
|
||||||
|
return _assign_properties_to_item(g, props)
|
||||||
return self.eval_multivalue(_assign_one, "Geometry", ["Settings"], socket_name=socket_name)
|
|
||||||
|
|
||||||
|
|
|
||||||
77
core/nodeslicer/nodes/mesh/__init__.py
Normal file
77
core/nodeslicer/nodes/mesh/__init__.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .get_mesh import SLICENODE_GetMesh
|
||||||
|
from .meshes_to_list import SLICENODE_MeshesToList
|
||||||
|
from .join_mesh import SLICENODE_JoinMesh
|
||||||
|
from .join_mesh_groups import SLICENODE_JoinMeshGroups
|
||||||
|
from .group_to_list import SLICENODE_GroupToList
|
||||||
|
from .list_to_group import SLICENODE_ListToGroup
|
||||||
|
from .separate_mesh import SLICENODE_SeparateMesh
|
||||||
|
from .separate_mesh_by_range import SLICENODE_SeparateMeshByRange
|
||||||
|
from .arrange_mesh import SLICENODE_ArrangeMesh
|
||||||
|
from .transform_geometry import SLICENODE_TransformGeometry
|
||||||
|
from .get_transforms import SLICENODE_GetTransforms
|
||||||
|
from .drop_geometry import SLICENODE_DropGeometry
|
||||||
|
from .rename_mesh import SLICENODE_RenameMesh
|
||||||
|
from .assign_pdf_properties import SLICENODE_AssignPdfProperties
|
||||||
|
from .assign_property_to_object import SLICENODE_AssignPropertyToObject
|
||||||
|
|
||||||
|
# Re-export reusable functions for backward compatibility with tests/external callers
|
||||||
|
from ....functions.mesh_functions import (
|
||||||
|
_copy_slicing_object,
|
||||||
|
_copy_slicing_collection,
|
||||||
|
_copy_slicing_group,
|
||||||
|
_transform_geometry_item,
|
||||||
|
format_name_template,
|
||||||
|
_rename_slicing_item,
|
||||||
|
_assign_pdf_properties_to_item,
|
||||||
|
_parse_settings_props,
|
||||||
|
_is_single_settings_spec,
|
||||||
|
_assign_properties_to_item,
|
||||||
|
copy_slicing_object,
|
||||||
|
copy_slicing_collection,
|
||||||
|
copy_slicing_group,
|
||||||
|
transform_geometry_item,
|
||||||
|
rename_slicing_item,
|
||||||
|
assign_pdf_properties_to_item,
|
||||||
|
parse_settings_props,
|
||||||
|
is_single_settings_spec,
|
||||||
|
assign_properties_to_item,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"SLICENODE_GetMesh",
|
||||||
|
"SLICENODE_MeshesToList",
|
||||||
|
"SLICENODE_JoinMesh",
|
||||||
|
"SLICENODE_JoinMeshGroups",
|
||||||
|
"SLICENODE_GroupToList",
|
||||||
|
"SLICENODE_ListToGroup",
|
||||||
|
"SLICENODE_SeparateMesh",
|
||||||
|
"SLICENODE_SeparateMeshByRange",
|
||||||
|
"SLICENODE_ArrangeMesh",
|
||||||
|
"SLICENODE_TransformGeometry",
|
||||||
|
"SLICENODE_GetTransforms",
|
||||||
|
"SLICENODE_DropGeometry",
|
||||||
|
"SLICENODE_RenameMesh",
|
||||||
|
"SLICENODE_AssignPdfProperties",
|
||||||
|
"SLICENODE_AssignPropertyToObject",
|
||||||
|
"_copy_slicing_object",
|
||||||
|
"_copy_slicing_collection",
|
||||||
|
"_copy_slicing_group",
|
||||||
|
"_transform_geometry_item",
|
||||||
|
"format_name_template",
|
||||||
|
"_rename_slicing_item",
|
||||||
|
"_assign_pdf_properties_to_item",
|
||||||
|
"_parse_settings_props",
|
||||||
|
"_is_single_settings_spec",
|
||||||
|
"_assign_properties_to_item",
|
||||||
|
"copy_slicing_object",
|
||||||
|
"copy_slicing_collection",
|
||||||
|
"copy_slicing_group",
|
||||||
|
"transform_geometry_item",
|
||||||
|
"rename_slicing_item",
|
||||||
|
"assign_pdf_properties_to_item",
|
||||||
|
"parse_settings_props",
|
||||||
|
"is_single_settings_spec",
|
||||||
|
"assign_properties_to_item",
|
||||||
|
]
|
||||||
170
core/nodeslicer/nodes/mesh/arrange_mesh.py
Normal file
170
core/nodeslicer/nodes/mesh/arrange_mesh.py
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfEnumProperty, NfBoolProperty, notify_tree_changed
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicingCollection, SlicingGroup, SlicingObject
|
||||||
|
from ....functions.mesh_functions import _copy_slicing_group
|
||||||
|
from .....infra.blender.list_utils import is_list_data
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_ArrangeMesh(SliceNode):
|
||||||
|
bl_label = "Arrange Slicing Group"
|
||||||
|
list_socket_mapping = [("Slicing Group", "Slicing Group")]
|
||||||
|
|
||||||
|
method: str = NfEnumProperty(
|
||||||
|
name="Method",
|
||||||
|
items=[
|
||||||
|
('NFP', "NFP (Default)", "Nest using No-Fit Polygon placer + First-Fit selection"),
|
||||||
|
('BL', "Bottom-Left", "Nest using Bottom-Left placer + First-Fit selection"),
|
||||||
|
('DJD', "DJD Heuristic", "Nest using No-Fit Polygon placer + DJD Heuristic selection"),
|
||||||
|
],
|
||||||
|
default='NFP',
|
||||||
|
update=notify_tree_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
allow_rotations: bool = NfBoolProperty(
|
||||||
|
name="Allow Rotations",
|
||||||
|
default=True,
|
||||||
|
update=notify_tree_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Mesh", "Slicing Group")
|
||||||
|
self.add_input("NodeSocketFloat", "Bed X").value = 250.0
|
||||||
|
self.add_input("NodeSocketFloat", "Bed Y").value = 250.0
|
||||||
|
self.add_input("NodeSocketFloat", "Spacing").value = 2.0
|
||||||
|
self.add_output("SLICESOCK_Mesh", "Slicing Group")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "method", text="")
|
||||||
|
layout.prop(self, "allow_rotations")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup | None:
|
||||||
|
if socket_name != "Slicing Group":
|
||||||
|
return None
|
||||||
|
|
||||||
|
mesh_data: SlicingCollection | SlicingGroup | None = self.resolve("Slicing Group")
|
||||||
|
if not mesh_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
bed_x: float = self.resolve("Bed X") or 250.0
|
||||||
|
bed_y: float = self.resolve("Bed Y") or 250.0
|
||||||
|
spacing: float = self.resolve("Spacing") or 2.0
|
||||||
|
|
||||||
|
# Convert input to SlicingGroup if it's a SlicingCollection or list
|
||||||
|
is_list = is_list_data(mesh_data)
|
||||||
|
if isinstance(mesh_data, SlicingGroup):
|
||||||
|
slicing_group = mesh_data
|
||||||
|
elif is_list:
|
||||||
|
slicing_group = SlicingGroup([])
|
||||||
|
for idx, item in enumerate(mesh_data):
|
||||||
|
if isinstance(item, SlicingGroup):
|
||||||
|
for k, sc in item.collections.items():
|
||||||
|
slicing_group.collections[f"{k}_{idx}"] = sc
|
||||||
|
elif isinstance(item, SlicingCollection):
|
||||||
|
col_name = item.name or f"mesh_{idx}"
|
||||||
|
slicing_group.collections[col_name] = item
|
||||||
|
elif isinstance(item, SlicingObject):
|
||||||
|
sc = SlicingCollection([item], item.name or f"mesh_{idx}")
|
||||||
|
slicing_group.collections[sc.name] = sc
|
||||||
|
else:
|
||||||
|
slicing_group = SlicingGroup([])
|
||||||
|
slicing_group.collections = {mesh_data.name: mesh_data}
|
||||||
|
|
||||||
|
# Create a deep copy of the group to avoid mutating cached inputs
|
||||||
|
copied_group = _copy_slicing_group(slicing_group)
|
||||||
|
|
||||||
|
# Prepare the list of items to pack
|
||||||
|
import pynest2d
|
||||||
|
|
||||||
|
items_list = []
|
||||||
|
collection_map = {}
|
||||||
|
for name, sc in copied_group.collections.items():
|
||||||
|
if sc.min_x is None or sc.max_x is None or sc.min_y is None or sc.max_y is None:
|
||||||
|
continue
|
||||||
|
w_mm = sc.max_x - sc.min_x
|
||||||
|
h_mm = sc.max_y - sc.min_y
|
||||||
|
if w_mm <= 0 or h_mm <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
w = pynest2d.mm(w_mm)
|
||||||
|
h = pynest2d.mm(h_mm)
|
||||||
|
item = pynest2d.Rectangle(w, h)
|
||||||
|
items_list.append(item)
|
||||||
|
collection_map[id(item)] = (name, sc)
|
||||||
|
|
||||||
|
if not items_list:
|
||||||
|
return copied_group
|
||||||
|
|
||||||
|
# Define the bin
|
||||||
|
bin_w = pynest2d.mm(bed_x)
|
||||||
|
bin_h = pynest2d.mm(bed_y)
|
||||||
|
bin_center = pynest2d.Point(pynest2d.mm(bed_x / 2.0), pynest2d.mm(bed_y / 2.0))
|
||||||
|
box = pynest2d.Box(bin_w, bin_h, bin_center)
|
||||||
|
|
||||||
|
# Spacing in internal units
|
||||||
|
spacing_units = pynest2d.mm(spacing)
|
||||||
|
|
||||||
|
# Configure and run the selected placer
|
||||||
|
placer = getattr(self, "method", "NFP")
|
||||||
|
allow_rot = getattr(self, "allow_rotations", True)
|
||||||
|
|
||||||
|
if placer == 'BL':
|
||||||
|
config = pynest2d.BLConfig()
|
||||||
|
config.allow_rotations = allow_rot
|
||||||
|
pynest2d.nest_blp(items_list, box, spacing_units, config)
|
||||||
|
elif placer == 'DJD':
|
||||||
|
placer_config = pynest2d.NfpConfig()
|
||||||
|
if not allow_rot:
|
||||||
|
placer_config.rotations = [0.0]
|
||||||
|
pynest2d.nest_djd(items_list, box, spacing_units, placer_config)
|
||||||
|
else: # 'NFP'
|
||||||
|
config = pynest2d.NfpConfig()
|
||||||
|
if not allow_rot:
|
||||||
|
config.rotations = [0.0]
|
||||||
|
pynest2d.nest(items_list, box, spacing_units, config)
|
||||||
|
|
||||||
|
# Apply the resulting translations and rotations to the copied collections
|
||||||
|
for item in items_list:
|
||||||
|
bin_id = item.binId()
|
||||||
|
if bin_id == 0:
|
||||||
|
name, sc = collection_map[id(item)]
|
||||||
|
trans = item.translation()
|
||||||
|
rot = item.rotation()
|
||||||
|
|
||||||
|
tx = trans.x() / pynest2d.MM_IN_COORDS
|
||||||
|
ty = trans.y() / pynest2d.MM_IN_COORDS
|
||||||
|
|
||||||
|
min_x = sc.min_x
|
||||||
|
min_y = sc.min_y
|
||||||
|
|
||||||
|
cos_val = np.cos(rot)
|
||||||
|
sin_val = np.sin(rot)
|
||||||
|
|
||||||
|
for so in sc.objects:
|
||||||
|
if so.mesh is None or so.mesh.size == 0:
|
||||||
|
continue
|
||||||
|
vx = so.mesh[:, :3, 0]
|
||||||
|
vy = so.mesh[:, :3, 1]
|
||||||
|
|
||||||
|
vx_new = (vx - min_x) * cos_val - (vy - min_y) * sin_val + tx
|
||||||
|
vy_new = (vx - min_x) * sin_val + (vy - min_y) * cos_val + ty
|
||||||
|
|
||||||
|
so.mesh[:, :3, 0] = vx_new
|
||||||
|
so.mesh[:, :3, 1] = vy_new
|
||||||
|
|
||||||
|
nx = so.mesh[:, 3, 0]
|
||||||
|
ny = so.mesh[:, 3, 1]
|
||||||
|
|
||||||
|
nx_new = nx * cos_val - ny * sin_val
|
||||||
|
ny_new = nx * sin_val + ny * cos_val
|
||||||
|
|
||||||
|
so.mesh[:, 3, 0] = nx_new
|
||||||
|
so.mesh[:, 3, 1] = ny_new
|
||||||
|
else:
|
||||||
|
name, sc = collection_map[id(item)]
|
||||||
|
if is_list:
|
||||||
|
return list(copied_group.collections.values())
|
||||||
|
return copied_group
|
||||||
43
core/nodeslicer/nodes/mesh/assign_pdf_properties.py
Normal file
43
core/nodeslicer/nodes/mesh/assign_pdf_properties.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfFloatProperty, NfFloatVectorProperty, NfBoolProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
from ....functions.mesh_functions import _assign_pdf_properties_to_item
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_AssignPdfProperties(SliceNode):
|
||||||
|
bl_label = "Assign PDF Properties"
|
||||||
|
list_socket_mapping = [("Geometry", "Geometry")]
|
||||||
|
|
||||||
|
line_width: float = NfFloatProperty(name="Line Width (mm)", default=0.0, min=0.0, max=50.0)
|
||||||
|
line_color: tuple[float, float, float] = NfFloatVectorProperty(
|
||||||
|
name="Line Color",
|
||||||
|
subtype='COLOR',
|
||||||
|
default=(0.0, 0.0, 0.0),
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
size=3
|
||||||
|
)
|
||||||
|
show_name: bool = NfBoolProperty(name="Show Name", default=False)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Mesh", "Geometry")
|
||||||
|
self.add_input("NodeSocketFloat", "Line Width")
|
||||||
|
self.add_output("SLICESOCK_Mesh", "Geometry")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "line_color")
|
||||||
|
layout.prop(self, "show_name")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
lc = (float(self.line_color[0]), float(self.line_color[1]), float(self.line_color[2]))
|
||||||
|
sn = bool(self.show_name)
|
||||||
|
|
||||||
|
def _assign_one(g: Any, line_w: Any) -> Any:
|
||||||
|
w = float(line_w) if line_w is not None else float(self.line_width)
|
||||||
|
return _assign_pdf_properties_to_item(g, w, lc, sn)
|
||||||
|
|
||||||
|
return self.eval_multivalue(_assign_one, "Geometry", ["Line Width"], socket_name=socket_name)
|
||||||
23
core/nodeslicer/nodes/mesh/assign_property_to_object.py
Normal file
23
core/nodeslicer/nodes/mesh/assign_property_to_object.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
from ....functions.mesh_functions import _parse_settings_props, _assign_properties_to_item
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_AssignPropertyToObject(SliceNode):
|
||||||
|
bl_label = "Assign PDF Object Properties"
|
||||||
|
list_socket_mapping = [("Geometry", "Geometry")]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Mesh", "Geometry")
|
||||||
|
self.add_input("SLICESOCK_Settings", "Settings")
|
||||||
|
self.add_output("SLICESOCK_Mesh", "Geometry")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _assign_one(g: Any, s: Any) -> Any:
|
||||||
|
props = _parse_settings_props(s)
|
||||||
|
return _assign_properties_to_item(g, props)
|
||||||
|
|
||||||
|
return self.eval_multivalue(_assign_one, "Geometry", ["Settings"], socket_name=socket_name)
|
||||||
51
core/nodeslicer/nodes/mesh/drop_geometry.py
Normal file
51
core/nodeslicer/nodes/mesh/drop_geometry.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicingCollection, SlicingGroup
|
||||||
|
from ....functions.mesh_functions import _copy_slicing_group, _copy_slicing_collection
|
||||||
|
from ....geometry import update_transform
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_DropGeometry(SliceNode):
|
||||||
|
bl_label = "Drop Geometry"
|
||||||
|
list_socket_mapping = [("Mesh", ["Mesh", "Translation"])]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Mesh", "Mesh")
|
||||||
|
self.add_input("NodeSocketFloat", "Z")
|
||||||
|
self.add_output("SLICESOCK_Mesh", "Mesh")
|
||||||
|
self.add_output("NodeSocketVector", "Translation")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _drop_item(item: Any, target_z: Any) -> tuple[Any, tuple[float, float, float]]:
|
||||||
|
if not item or getattr(item, "min_z", None) is None:
|
||||||
|
return item, (0.0, 0.0, 0.0)
|
||||||
|
z_float = float(target_z if target_z is not None else 0.0)
|
||||||
|
min_z = round(item.min_z, 6)
|
||||||
|
offset_z = float(z_float - min_z)
|
||||||
|
if isinstance(item, SlicingGroup):
|
||||||
|
copied_data = _copy_slicing_group(item)
|
||||||
|
else:
|
||||||
|
copied_data = _copy_slicing_collection(item)
|
||||||
|
drop_vec = np.array([0.0, 0.0, offset_z], dtype=np.float64)
|
||||||
|
copied_data.offset(drop_vec)
|
||||||
|
copied_data.transforms = update_transform(getattr(copied_data, "transforms", []), t_arr=drop_vec)
|
||||||
|
if isinstance(copied_data, SlicingGroup):
|
||||||
|
for col in copied_data.collections.values():
|
||||||
|
col.transforms = update_transform(getattr(col, "transforms", []), t_arr=drop_vec)
|
||||||
|
for so in col.objects:
|
||||||
|
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
|
||||||
|
elif isinstance(copied_data, SlicingCollection):
|
||||||
|
for so in copied_data.objects:
|
||||||
|
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
|
||||||
|
return copied_data, (0.0, 0.0, offset_z)
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_drop_item,
|
||||||
|
main_socket="Mesh",
|
||||||
|
secondary_sockets=["Z"],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=(0.0, 0.0, 0.0) if socket_name == "Translation" else None
|
||||||
|
)
|
||||||
30
core/nodeslicer/nodes/mesh/get_mesh.py
Normal file
30
core/nodeslicer/nodes/mesh/get_mesh.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfBoolProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GetMesh(SliceNode):
|
||||||
|
bl_label = "Get Mesh"
|
||||||
|
list_socket_mapping = [("Object", "Mesh")]
|
||||||
|
|
||||||
|
include_children: bool = NfBoolProperty(name="Include Children", default=True)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("NodeSocketObject")
|
||||||
|
self.add_output("SLICESOCK_Mesh")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "include_children")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
if socket_name != "Mesh": return None
|
||||||
|
|
||||||
|
obj: Any = self.resolve("Object")
|
||||||
|
if not obj: return None
|
||||||
|
|
||||||
|
from .....infra.blender.blender_slice_nodes_generic import object_to_mesh
|
||||||
|
return object_to_mesh(obj, include_children=self.include_children)
|
||||||
52
core/nodeslicer/nodes/mesh/get_transforms.py
Normal file
52
core/nodeslicer/nodes/mesh/get_transforms.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
from ....slice import extract_geometry_transforms
|
||||||
|
from .....infra.blender.list_utils import is_input_a_list
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GetTransforms(SliceNode):
|
||||||
|
bl_label = "Get Transforms"
|
||||||
|
list_socket_mapping = [("Geometry", ["Translation", "Rotation", "Scale"])]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Geometry", "Geometry")
|
||||||
|
self.add_output("NodeSocketVector", "Translation")
|
||||||
|
self.add_output("NodeSocketRotation", "Rotation")
|
||||||
|
self.add_output("NodeSocketVector", "Scale")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
sock_lower = socket_name.lower()
|
||||||
|
default = (1.0, 1.0, 1.0) if sock_lower == "scale" else (0.0, 0.0, 0.0)
|
||||||
|
idx = 0 if sock_lower == "translation" else (1 if sock_lower == "rotation" else 2)
|
||||||
|
|
||||||
|
geom = self.resolve("Geometry")
|
||||||
|
if geom is None:
|
||||||
|
return default
|
||||||
|
|
||||||
|
is_list = is_input_a_list(geom, is_vector=False)
|
||||||
|
if not is_list and isinstance(geom, (list, tuple)):
|
||||||
|
if type(geom).__name__ != "SlicePaths" and not getattr(geom, "is_slice_paths", False):
|
||||||
|
is_list = True
|
||||||
|
elif not is_list and hasattr(geom, "__iter__") and not hasattr(geom, "shape") and not isinstance(geom, (str, bytes, dict, set)) and type(geom).__name__ != "SlicePaths" and not getattr(geom, "is_slice_paths", False) and not hasattr(geom, "mesh") and not hasattr(geom, "objects") and not hasattr(geom, "collections") and not hasattr(geom, "location"):
|
||||||
|
is_list = True
|
||||||
|
|
||||||
|
if is_list:
|
||||||
|
items = list(geom)
|
||||||
|
if len(items) == 0:
|
||||||
|
return []
|
||||||
|
results = []
|
||||||
|
for item in items:
|
||||||
|
trans = extract_geometry_transforms(item)
|
||||||
|
if isinstance(trans, tuple) and len(trans) > idx:
|
||||||
|
results.append(trans[idx])
|
||||||
|
else:
|
||||||
|
results.append(default)
|
||||||
|
return results
|
||||||
|
else:
|
||||||
|
trans = extract_geometry_transforms(geom)
|
||||||
|
if isinstance(trans, tuple) and len(trans) > idx:
|
||||||
|
return trans[idx]
|
||||||
|
return default
|
||||||
44
core/nodeslicer/nodes/mesh/group_to_list.py
Normal file
44
core/nodeslicer/nodes/mesh/group_to_list.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicingCollection, SlicingGroup, SlicingObject
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GroupToList(SliceNode):
|
||||||
|
bl_label = "Group to List"
|
||||||
|
always_list_outputs = ["Collections"]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Mesh", "Group")
|
||||||
|
self.add_output("SLICESOCK_Mesh", "Collections", display_shape='LIST')
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> list[SlicingCollection]:
|
||||||
|
val = self.resolve("Group")
|
||||||
|
if val is None:
|
||||||
|
val = self.resolve("Collections")
|
||||||
|
if val is None and hasattr(self, "inputs") and len(self.inputs) > 0:
|
||||||
|
val = self.resolve(self.inputs[0].name)
|
||||||
|
if val is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if isinstance(val, SlicingGroup):
|
||||||
|
return list(val.collections.values())
|
||||||
|
|
||||||
|
if isinstance(val, (list, tuple)):
|
||||||
|
res: list[SlicingCollection] = []
|
||||||
|
for item in val:
|
||||||
|
if isinstance(item, SlicingGroup):
|
||||||
|
res.extend(item.collections.values())
|
||||||
|
elif isinstance(item, SlicingCollection):
|
||||||
|
res.append(item)
|
||||||
|
elif isinstance(item, SlicingObject):
|
||||||
|
res.append(SlicingCollection([item], getattr(item, "name", "Collection")))
|
||||||
|
return res
|
||||||
|
|
||||||
|
if isinstance(val, SlicingCollection):
|
||||||
|
return [val]
|
||||||
|
|
||||||
|
if isinstance(val, SlicingObject):
|
||||||
|
return [SlicingCollection([val], getattr(val, "name", "Collection"))]
|
||||||
|
|
||||||
|
return []
|
||||||
32
core/nodeslicer/nodes/mesh/join_mesh.py
Normal file
32
core/nodeslicer/nodes/mesh/join_mesh.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNodeJoiner, SlicingCollection, SlicingObject
|
||||||
|
from .....infra.blender.list_utils import flatten_list
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_JoinMesh(SliceNodeJoiner):
|
||||||
|
bl_label = "Join Mesh Parts"
|
||||||
|
socket_type = "SLICESOCK_Mesh"
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection:
|
||||||
|
from .....infra.blender.blender_slice_nodes_generic import resolve_input
|
||||||
|
all_colls = []
|
||||||
|
for inp in self.inputs:
|
||||||
|
if getattr(inp, "is_linked", False) or getattr(inp, "value", None):
|
||||||
|
val = resolve_input(inp)
|
||||||
|
if val:
|
||||||
|
all_colls.extend(flatten_list(val))
|
||||||
|
|
||||||
|
first_coll = all_colls[0] if all_colls else None
|
||||||
|
merged_coll = SlicingCollection([], first_coll.name if first_coll else '')
|
||||||
|
for coll in all_colls:
|
||||||
|
if hasattr(coll, "objects"):
|
||||||
|
merged_coll.objects += list(coll.objects)
|
||||||
|
elif isinstance(coll, SlicingObject):
|
||||||
|
merged_coll.objects.append(coll)
|
||||||
|
for t in getattr(coll, "transforms", []):
|
||||||
|
if t not in merged_coll.transforms:
|
||||||
|
merged_coll.transforms.append(dict(t))
|
||||||
|
|
||||||
|
return merged_coll
|
||||||
37
core/nodeslicer/nodes/mesh/join_mesh_groups.py
Normal file
37
core/nodeslicer/nodes/mesh/join_mesh_groups.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNodeJoiner, SlicingGroup
|
||||||
|
from .....infra.blender.list_utils import flatten_list
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_JoinMeshGroups(SliceNodeJoiner):
|
||||||
|
bl_label = "Join Mesh Groups"
|
||||||
|
socket_type = "SLICESOCK_Mesh"
|
||||||
|
is_list_output = True
|
||||||
|
always_list_outputs = ["Mesh"]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_output(self.output_socket_type or self.socket_type, display_shape='LIST')
|
||||||
|
self.add_multi_socket_group(self.socket_type)
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup:
|
||||||
|
from .....infra.blender.blender_slice_nodes_generic import resolve_input
|
||||||
|
group: SlicingGroup = SlicingGroup([])
|
||||||
|
|
||||||
|
all_colls = []
|
||||||
|
for input_socket in self.inputs:
|
||||||
|
if getattr(input_socket, "is_linked", False) or getattr(input_socket, "value", None):
|
||||||
|
val = resolve_input(input_socket)
|
||||||
|
if val:
|
||||||
|
all_colls.extend(flatten_list(val))
|
||||||
|
|
||||||
|
for idx, coll in enumerate(all_colls):
|
||||||
|
col_name = getattr(coll, "name", "")
|
||||||
|
if not col_name and getattr(coll, "objects", None):
|
||||||
|
col_name = coll.objects[0].name
|
||||||
|
if not col_name:
|
||||||
|
col_name = f"collection_{idx}"
|
||||||
|
group.collections[col_name] = coll
|
||||||
|
|
||||||
|
return group
|
||||||
57
core/nodeslicer/nodes/mesh/list_to_group.py
Normal file
57
core/nodeslicer/nodes/mesh/list_to_group.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicingCollection, SlicingGroup, SlicingObject
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_ListToGroup(SliceNode):
|
||||||
|
bl_label = "List to Group"
|
||||||
|
always_list_inputs = ["Collections"]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Mesh", "Collections", display_shape='LIST')
|
||||||
|
self.add_output("SLICESOCK_Mesh", "Group")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup:
|
||||||
|
val = self.resolve("Collections")
|
||||||
|
if val is None:
|
||||||
|
val = self.resolve("Group")
|
||||||
|
if val is None and hasattr(self, "inputs") and len(self.inputs) > 0:
|
||||||
|
val = self.resolve(self.inputs[0].name)
|
||||||
|
if val is None:
|
||||||
|
return SlicingGroup([])
|
||||||
|
|
||||||
|
if isinstance(val, SlicingGroup):
|
||||||
|
return val
|
||||||
|
|
||||||
|
items = val if isinstance(val, (list, tuple)) else [val]
|
||||||
|
group: SlicingGroup = SlicingGroup([])
|
||||||
|
|
||||||
|
for idx, item in enumerate(items):
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
if isinstance(item, SlicingGroup):
|
||||||
|
if hasattr(item, "transforms") and item.transforms and not group.transforms:
|
||||||
|
group.transforms = list(item.transforms)
|
||||||
|
for k, sc in item.collections.items():
|
||||||
|
name = k
|
||||||
|
if name in group.collections:
|
||||||
|
name = f"{name}_{idx}"
|
||||||
|
group.collections[name] = sc
|
||||||
|
elif isinstance(item, SlicingCollection):
|
||||||
|
if hasattr(item, "transforms") and item.transforms and not group.transforms:
|
||||||
|
group.transforms = list(item.transforms)
|
||||||
|
name = item.name or f"collection_{idx}"
|
||||||
|
if name in group.collections:
|
||||||
|
name = f"{name}_{idx}"
|
||||||
|
group.collections[name] = item
|
||||||
|
elif isinstance(item, SlicingObject):
|
||||||
|
if hasattr(item, "transforms") and item.transforms and not group.transforms:
|
||||||
|
group.transforms = list(item.transforms)
|
||||||
|
name = getattr(item, "name", f"object_{idx}")
|
||||||
|
if name in group.collections:
|
||||||
|
name = f"{name}_{idx}"
|
||||||
|
sc = SlicingCollection([item], name)
|
||||||
|
group.collections[name] = sc
|
||||||
|
|
||||||
|
return group
|
||||||
11
core/nodeslicer/nodes/mesh/meshes_to_list.py
Normal file
11
core/nodeslicer/nodes/mesh/meshes_to_list.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNodeListJoiner
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_MeshesToList(SliceNodeListJoiner):
|
||||||
|
bl_label = "Meshes to List"
|
||||||
|
socket_type = "SLICESOCK_Mesh"
|
||||||
|
socket_label = "Mesh"
|
||||||
|
output_socket_name = "Meshes"
|
||||||
24
core/nodeslicer/nodes/mesh/rename_mesh.py
Normal file
24
core/nodeslicer/nodes/mesh/rename_mesh.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
from ....functions.mesh_functions import _rename_slicing_item
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_RenameMesh(SliceNode):
|
||||||
|
bl_label = "Rename Mesh"
|
||||||
|
list_socket_mapping = [("Geometry", "Geometry")]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Mesh", "Geometry")
|
||||||
|
self.add_input("NodeSocketString", "Name")
|
||||||
|
self.add_output("SLICESOCK_Mesh", "Geometry")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
return self.eval_multivalue(
|
||||||
|
lambda g, name: _rename_slicing_item(g, str(name)) if name else g,
|
||||||
|
main_socket="Geometry",
|
||||||
|
secondary_sockets=["Name"],
|
||||||
|
socket_name=socket_name
|
||||||
|
)
|
||||||
103
core/nodeslicer/nodes/mesh/separate_mesh.py
Normal file
103
core/nodeslicer/nodes/mesh/separate_mesh.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import copy
|
||||||
|
from typing import Any
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfEnumProperty, notify_tree_changed
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicingCollection, SlicingObject
|
||||||
|
from .....infra.blender.blender_mesh_capture import PdfProperties
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_SeparateMesh(SliceNode):
|
||||||
|
bl_label = "Separate Mesh"
|
||||||
|
list_socket_mapping = [("Mesh", ["Selection", "Inverted"])]
|
||||||
|
|
||||||
|
comparison: str = NfEnumProperty(
|
||||||
|
name="Comparison",
|
||||||
|
items=[
|
||||||
|
('EQUAL', "Equal", "=="),
|
||||||
|
('NOT_EQUAL', "Not Equal", "!="),
|
||||||
|
('GREATER_THAN', "Greater Than", ">"),
|
||||||
|
('LESS_THAN', "Less Than", "<"),
|
||||||
|
('GREATER_EQUAL', "Greater Equal", ">="),
|
||||||
|
('LESS_EQUAL', "Less Equal", "<="),
|
||||||
|
],
|
||||||
|
default='EQUAL',
|
||||||
|
update=notify_tree_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Mesh")
|
||||||
|
self.add_input("NodeSocketString", "Attribute")
|
||||||
|
self.add_input("NodeSocketFloat", "Value")
|
||||||
|
self.add_output("SLICESOCK_Mesh", "Selection")
|
||||||
|
self.add_output("SLICESOCK_Mesh", "Inverted")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "comparison", text="")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _split_coll(sc: SlicingCollection, a_name: str, comp_val: float) -> tuple[SlicingCollection, SlicingCollection]:
|
||||||
|
res_selection = SlicingCollection([], sc.name + "_selection")
|
||||||
|
res_inverted = SlicingCollection([], sc.name + "_inverted")
|
||||||
|
|
||||||
|
for so in sc.objects:
|
||||||
|
if not a_name or a_name not in so.attributes:
|
||||||
|
res_inverted.objects.append(so)
|
||||||
|
continue
|
||||||
|
|
||||||
|
attr = so.attributes[a_name]
|
||||||
|
vals = attr["values"]
|
||||||
|
|
||||||
|
mask = np.zeros(len(so.mesh), dtype=bool)
|
||||||
|
if self.comparison == 'EQUAL': mask = vals == comp_val
|
||||||
|
elif self.comparison == 'NOT_EQUAL': mask = vals != comp_val
|
||||||
|
elif self.comparison == 'GREATER_THAN': mask = vals > comp_val
|
||||||
|
elif self.comparison == 'LESS_THAN': mask = vals < comp_val
|
||||||
|
elif self.comparison == 'GREATER_EQUAL': mask = vals >= comp_val
|
||||||
|
elif self.comparison == 'LESS_EQUAL': mask = vals <= comp_val
|
||||||
|
|
||||||
|
if np.all(mask):
|
||||||
|
res_selection.objects.append(so)
|
||||||
|
elif not np.any(mask):
|
||||||
|
res_inverted.objects.append(so)
|
||||||
|
else:
|
||||||
|
so_sel = SlicingObject()
|
||||||
|
so_sel.name = so.name + "_sel"
|
||||||
|
so_sel.parent = so.parent
|
||||||
|
so_sel.mesh = so.mesh[mask]
|
||||||
|
so_sel.transforms = [dict(t) for t in getattr(so, "transforms", [])]
|
||||||
|
so_sel.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
|
||||||
|
so_sel.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
|
||||||
|
for k, v in so.attributes.items():
|
||||||
|
so_sel.attributes[k] = {
|
||||||
|
"domain": v["domain"],
|
||||||
|
"type": v["type"],
|
||||||
|
"values": v["values"][mask]
|
||||||
|
}
|
||||||
|
res_selection.objects.append(so_sel)
|
||||||
|
|
||||||
|
so_inv = SlicingObject()
|
||||||
|
so_inv.name = so.name + "_inv"
|
||||||
|
so_inv.parent = so.parent
|
||||||
|
so_inv.mesh = so.mesh[~mask]
|
||||||
|
so_inv.transforms = [dict(t) for t in getattr(so, "transforms", [])]
|
||||||
|
so_inv.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
|
||||||
|
so_inv.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
|
||||||
|
for k, v in so.attributes.items():
|
||||||
|
so_inv.attributes[k] = {
|
||||||
|
"domain": v["domain"],
|
||||||
|
"type": v["type"],
|
||||||
|
"values": v["values"][~mask]
|
||||||
|
}
|
||||||
|
res_inverted.objects.append(so_inv)
|
||||||
|
return res_selection, res_inverted
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
lambda sc, a, v: _split_coll(sc, str(a or ""), float(v if v is not None else 0.0)),
|
||||||
|
main_socket="Mesh",
|
||||||
|
secondary_sockets=["Attribute", "Value"],
|
||||||
|
socket_name=socket_name,
|
||||||
|
)
|
||||||
109
core/nodeslicer/nodes/mesh/separate_mesh_by_range.py
Normal file
109
core/nodeslicer/nodes/mesh/separate_mesh_by_range.py
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import copy
|
||||||
|
from typing import Any
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfIntProperty, notify_tree_changed
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicingCollection, SlicingObject
|
||||||
|
from .....infra.blender.blender_mesh_capture import PdfProperties
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_SeparateMeshByRange(SliceNode):
|
||||||
|
bl_label = "Separate Mesh by Range"
|
||||||
|
|
||||||
|
start_val: int = NfIntProperty(name="Start", default=0, update=notify_tree_changed)
|
||||||
|
end_val: int = NfIntProperty(name="End", default=1, update=notify_tree_changed)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Mesh")
|
||||||
|
self.add_input("NodeSocketString", "Attribute")
|
||||||
|
self._update_outputs()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
super().update()
|
||||||
|
self._update_outputs()
|
||||||
|
|
||||||
|
def _update_outputs(self):
|
||||||
|
start = int(self.start_val) if isinstance(getattr(self, "start_val", None), (int, float)) else 0
|
||||||
|
end_val = int(self.end_val) if isinstance(getattr(self, "end_val", None), (int, float)) else 1
|
||||||
|
end = max(start, end_val)
|
||||||
|
expected_ids = [str(i) for i in range(start, end + 1)]
|
||||||
|
|
||||||
|
from .....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
inp = self.inputs.get("Mesh") if hasattr(self, "inputs") else None
|
||||||
|
is_list = is_socket_linked_to_list(inp) if inp is not None else False
|
||||||
|
if inp is not None:
|
||||||
|
set_socket_shape(inp, "LIST" if is_list else "CIRCLE")
|
||||||
|
|
||||||
|
attr_inp = self.inputs.get("Attribute") if hasattr(self, "inputs") else None
|
||||||
|
if attr_inp is not None:
|
||||||
|
set_socket_shape(attr_inp, "LIST" if is_socket_linked_to_list(attr_inp) else "CIRCLE")
|
||||||
|
|
||||||
|
# Remove outputs that are no longer in the range
|
||||||
|
to_remove = [o for o in self.outputs if o.identifier not in expected_ids]
|
||||||
|
for o in to_remove:
|
||||||
|
self.outputs.remove(o)
|
||||||
|
|
||||||
|
# Add new outputs
|
||||||
|
for id_str in expected_ids:
|
||||||
|
if id_str not in self.outputs:
|
||||||
|
self.add_output("SLICESOCK_Mesh", id_str, identifier=id_str, display_shape="LIST" if is_list else "CIRCLE")
|
||||||
|
else:
|
||||||
|
set_socket_shape(self.outputs[id_str], "LIST" if is_list else "CIRCLE")
|
||||||
|
|
||||||
|
# Re-order to match range
|
||||||
|
for i, id_str in enumerate(expected_ids):
|
||||||
|
current_index = self.outputs.find(id_str)
|
||||||
|
if current_index != -1 and current_index != i:
|
||||||
|
self.outputs.move(current_index, i)
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
col = layout.column(align=True)
|
||||||
|
col.prop(self, "start_val")
|
||||||
|
col.prop(self, "end_val")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
try:
|
||||||
|
target_val = int(socket_name)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
start = int(self.start_val) if isinstance(getattr(self, "start_val", None), (int, float)) else 0
|
||||||
|
end_val = int(self.end_val) if isinstance(getattr(self, "end_val", None), (int, float)) else 1
|
||||||
|
end = max(start, end_val)
|
||||||
|
total_parts = end - start + 1
|
||||||
|
num_digits = max(2, len(str(total_parts)))
|
||||||
|
i = target_val - start
|
||||||
|
|
||||||
|
def _split_coll(sc: SlicingCollection, a_name: str) -> SlicingCollection:
|
||||||
|
res = SlicingCollection([], f"{sc.name}_{target_val}")
|
||||||
|
for so in sc.objects:
|
||||||
|
if not a_name or a_name not in so.attributes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
attr = so.attributes[a_name]
|
||||||
|
vals = attr["values"]
|
||||||
|
|
||||||
|
mask = vals == target_val
|
||||||
|
|
||||||
|
if np.any(mask):
|
||||||
|
so_new = SlicingObject()
|
||||||
|
base_mesh_name = so.name
|
||||||
|
so_new.name = f"{base_mesh_name}{i:0{num_digits}d}"
|
||||||
|
so_new.parent = so.parent
|
||||||
|
so_new.mesh = so.mesh[mask]
|
||||||
|
so_new.transforms = [dict(t) for t in getattr(so, "transforms", [])]
|
||||||
|
so_new.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
|
||||||
|
so_new.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
|
||||||
|
for k, v in so.attributes.items():
|
||||||
|
so_new.attributes[k] = {
|
||||||
|
"domain": v["domain"],
|
||||||
|
"type": v["type"],
|
||||||
|
"values": v["values"][mask]
|
||||||
|
}
|
||||||
|
res.objects.append(so_new)
|
||||||
|
return res
|
||||||
|
|
||||||
|
return self.eval_multivalue(_split_coll, "Mesh", ["Attribute"])
|
||||||
34
core/nodeslicer/nodes/mesh/transform_geometry.py
Normal file
34
core/nodeslicer/nodes/mesh/transform_geometry.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
from ....functions.mesh_functions import _transform_geometry_item
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_TransformGeometry(SliceNode):
|
||||||
|
bl_label = "Transform Geometry"
|
||||||
|
list_socket_mapping = [("Geometry", "Geometry")]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Geometry", "Geometry")
|
||||||
|
self.add_input("NodeSocketVector", "Translation")
|
||||||
|
self.add_input("NodeSocketRotation", "Rotation")
|
||||||
|
self.add_input("NodeSocketVector", "Scale").value = (1.0, 1.0, 1.0)
|
||||||
|
self.add_output("SLICESOCK_Geometry", "Geometry")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _transform_one(g: Any, t: Any, r: Any, s: Any) -> Any:
|
||||||
|
t_arr = np.array(t if t is not None else (0.0, 0.0, 0.0), dtype=np.float64)
|
||||||
|
r_arr = np.array(r if r is not None else (0.0, 0.0, 0.0), dtype=np.float64)
|
||||||
|
s_arr = np.array(s if s is not None else (1.0, 1.0, 1.0), dtype=np.float64)
|
||||||
|
return _transform_geometry_item(g, t_arr, r_arr, s_arr)
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_transform_one,
|
||||||
|
main_socket="Geometry",
|
||||||
|
secondary_sockets=["Translation", "Rotation", "Scale"],
|
||||||
|
is_vector=[True, True, True],
|
||||||
|
socket_name=socket_name,
|
||||||
|
)
|
||||||
|
|
@ -1,703 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Any
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
|
||||||
import bpy
|
|
||||||
import tempfile
|
|
||||||
import re
|
|
||||||
import json
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from ..bpy_ns_types import NfStringProperty, NfFloatProperty, NfEnumProperty, NfFloatVectorProperty, NfBoolProperty
|
|
||||||
from ....registry import register_class
|
|
||||||
from .base import ExecSliceNode, SlicePaths, SliceNode, SlicingCollection, SlicingGroup
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_GcodeToFile(ExecSliceNode):
|
|
||||||
bl_label = "Gcode To File"
|
|
||||||
button_label = "Generate File"
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self._init_exec()
|
|
||||||
self.add_input("NodeSocketString", "Folder")
|
|
||||||
self.add_input("NodeSocketString", "Name")
|
|
||||||
self.add_multi_socket_group("NodeSocketString", name="Gcode")
|
|
||||||
|
|
||||||
def update(self) -> None:
|
|
||||||
super().update()
|
|
||||||
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
|
||||||
for inp in self.inputs:
|
|
||||||
if getattr(inp, "name", "") == "Gcode" or getattr(inp, "identifier", "").startswith("Gcode_ms_in_"):
|
|
||||||
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
|
||||||
super().draw_buttons(context, layout)
|
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
|
||||||
from ....infra.networking.gcode_utils import get_gcode_name_from_metadata, prepare_gcode_file
|
|
||||||
from ....infra.blender.list_utils import flatten_list
|
|
||||||
|
|
||||||
raw_gcodes = self.resolve_multi_input('Gcode')
|
|
||||||
gcodes: list[str] = [str(gc) for gc in flatten_list(raw_gcodes) if gc]
|
|
||||||
|
|
||||||
folder: str = self.resolve("Folder")
|
|
||||||
if not gcodes:
|
|
||||||
if operator: operator.report({'ERROR'}, "No Gcode to save")
|
|
||||||
return 'error'
|
|
||||||
if not folder:
|
|
||||||
if operator: operator.report({'ERROR'}, "No Folder provided")
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
base_name: str = self.resolve("Name")
|
|
||||||
total_parts: int = len(gcodes)
|
|
||||||
num_digits: int = max(2, len(str(total_parts)))
|
|
||||||
|
|
||||||
is_name_list = isinstance(base_name, (list, tuple))
|
|
||||||
if is_name_list:
|
|
||||||
if len(base_name) == 1:
|
|
||||||
base_name = str(base_name[0])
|
|
||||||
is_name_list = False
|
|
||||||
elif len(base_name) != total_parts:
|
|
||||||
msg = f"Input list length mismatch: Gcode has {total_parts} items, but Name has {len(base_name)} items."
|
|
||||||
if operator: operator.report({'ERROR'}, msg)
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
for i, gc in enumerate(gcodes):
|
|
||||||
if is_name_list:
|
|
||||||
name = str(base_name[i])
|
|
||||||
elif base_name:
|
|
||||||
if total_parts > 1:
|
|
||||||
name = f"{base_name}{i:0{num_digits}d}"
|
|
||||||
else:
|
|
||||||
name = str(base_name)
|
|
||||||
else:
|
|
||||||
name = get_gcode_name_from_metadata(gc, default=f'object_{i}')
|
|
||||||
|
|
||||||
# User's requested logic: .split('.')[0]
|
|
||||||
folder_path = Path(bpy.path.abspath(f"//{folder}"))
|
|
||||||
filepath_abs = folder_path / name
|
|
||||||
dirname = os.path.dirname(filepath_abs)
|
|
||||||
basename = os.path.basename(filepath_abs)
|
|
||||||
name_part = basename.split('.')[0]
|
|
||||||
os.makedirs(dirname, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
file_bytes, filename = prepare_gcode_file(gc, name_part)
|
|
||||||
filepath = os.path.join(dirname, filename)
|
|
||||||
with open(filepath, "wb") as f:
|
|
||||||
f.write(file_bytes)
|
|
||||||
if filename.endswith(".bgcode"):
|
|
||||||
if operator: operator.report({'INFO'}, f"Binary G-code generated at {filepath}")
|
|
||||||
else:
|
|
||||||
if operator: operator.report({'INFO'}, f"G-code saved to {filepath}")
|
|
||||||
except Exception as e:
|
|
||||||
if operator: operator.report({'ERROR'}, str(e))
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
return 'ok'
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_GcodeToTextBlock(ExecSliceNode):
|
|
||||||
bl_label = "Gcode to Text Block"
|
|
||||||
button_label = "Update Text Block"
|
|
||||||
|
|
||||||
text_block_name: str = NfStringProperty(name="Text Block Name", default="NodeSlicer_Gcode.gcode")
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self._init_exec()
|
|
||||||
self.add_input("NodeSocketString", "Gcode")
|
|
||||||
|
|
||||||
def update(self) -> None:
|
|
||||||
super().update()
|
|
||||||
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
|
||||||
inp = self.inputs.get("Gcode")
|
|
||||||
if inp:
|
|
||||||
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
|
||||||
super().draw_buttons(context, layout)
|
|
||||||
layout.prop(self, "text_block_name")
|
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
|
||||||
from ....infra.blender.list_utils import flatten_list, is_list_data
|
|
||||||
raw = self.resolve("Gcode")
|
|
||||||
if not raw:
|
|
||||||
return 'ok'
|
|
||||||
|
|
||||||
gcodes = [str(g) for g in flatten_list(raw) if g] if is_list_data(raw) else ([str(raw)] if raw else [])
|
|
||||||
if not gcodes:
|
|
||||||
return 'ok'
|
|
||||||
|
|
||||||
base_name = self.text_block_name
|
|
||||||
name_root, ext = os.path.splitext(base_name)
|
|
||||||
total_parts = len(gcodes)
|
|
||||||
num_digits = max(2, len(str(total_parts)))
|
|
||||||
|
|
||||||
for i, gcode in enumerate(gcodes):
|
|
||||||
name = f"{name_root}_{i:0{num_digits}d}{ext}" if total_parts > 1 else base_name
|
|
||||||
text_block = bpy.data.texts.get(name)
|
|
||||||
if text_block is None:
|
|
||||||
text_block = bpy.data.texts.new(name=name)
|
|
||||||
text_block.from_string(gcode)
|
|
||||||
|
|
||||||
# Attempt to show in an open text editor
|
|
||||||
assert bpy.context.window_manager
|
|
||||||
for window in bpy.context.window_manager.windows:
|
|
||||||
for area in window.screen.areas:
|
|
||||||
if area.type == 'TEXT_EDITOR':
|
|
||||||
for space in area.spaces:
|
|
||||||
if space.type == 'TEXT_EDITOR':
|
|
||||||
space.text = text_block
|
|
||||||
|
|
||||||
return 'ok'
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_StartPrint(ExecSliceNode):
|
|
||||||
bl_label = "Start Print"
|
|
||||||
button_label = "Send to Printer"
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self._init_exec()
|
|
||||||
self.add_input("NodeSocketString", "Gcode")
|
|
||||||
self.add_input("SLICESOCK_Printer")
|
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
|
||||||
from ....infra.blender.list_utils import is_list_data
|
|
||||||
|
|
||||||
raw_gcode = self.resolve("Gcode")
|
|
||||||
if is_list_data(raw_gcode):
|
|
||||||
if operator: operator.report({'ERROR'}, "Start Print does not accept a list of Gcode")
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
from ....infra.networking.printer_backends import (
|
|
||||||
PrinterConfig,
|
|
||||||
PrusaLinkBackend,
|
|
||||||
CrealityBackend,
|
|
||||||
MoonrakerBackend
|
|
||||||
)
|
|
||||||
|
|
||||||
gcode: str = str(raw_gcode or "")
|
|
||||||
config: PrinterConfig | None = self.resolve("Printer")
|
|
||||||
|
|
||||||
if not gcode:
|
|
||||||
if operator: operator.report({'ERROR'}, "No Gcode to send")
|
|
||||||
return 'no gcode'
|
|
||||||
if not config:
|
|
||||||
if operator: operator.report({'ERROR'}, "No Printer configuration")
|
|
||||||
return 'no printer'
|
|
||||||
|
|
||||||
# Instantiate correct backend
|
|
||||||
backend = None
|
|
||||||
if config.host_type == 'PRUSALINK':
|
|
||||||
backend = PrusaLinkBackend(config.host, config.port, config.prefix, api_key=config.password)
|
|
||||||
elif config.host_type == 'CREALITY':
|
|
||||||
backend = CrealityBackend(config.host, config.port, config.prefix)
|
|
||||||
elif config.host_type == 'MOONRAKER':
|
|
||||||
backend = MoonrakerBackend(config.host, config.port, config.prefix)
|
|
||||||
|
|
||||||
if not backend:
|
|
||||||
if operator: operator.report({'ERROR'}, f"Unsupported host type: {config.host_type}")
|
|
||||||
return 'unsupported'
|
|
||||||
|
|
||||||
from ....infra.networking.gcode_utils import prepare_gcode_file
|
|
||||||
|
|
||||||
# Write to temp file to send
|
|
||||||
name_part = f"NodeSlicer_{config.label}".replace(" ", "_")
|
|
||||||
try:
|
|
||||||
file_bytes, filename = prepare_gcode_file(gcode, name_part)
|
|
||||||
except Exception as e:
|
|
||||||
if operator: operator.report({'ERROR'}, f"Failed to prepare G-code: {str(e)}")
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
suffix = Path(filename).suffix
|
|
||||||
with tempfile.NamedTemporaryFile(mode='wb', suffix=suffix, delete=False) as tf:
|
|
||||||
tf.write(file_bytes)
|
|
||||||
temp_path = Path(tf.name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
backend.start_print(temp_path, filename)
|
|
||||||
if operator: operator.report({'INFO'}, f"Print started on {config.label}")
|
|
||||||
except Exception as e:
|
|
||||||
if operator: operator.report({'ERROR'}, f"Failed to start print: {str(e)}")
|
|
||||||
return 'error'
|
|
||||||
finally:
|
|
||||||
if temp_path.exists():
|
|
||||||
temp_path.unlink()
|
|
||||||
|
|
||||||
return 'ok'
|
|
||||||
|
|
||||||
class ExecSliceNode_DatabaseSubmit(ExecSliceNode):
|
|
||||||
record_id: str = NfStringProperty(name="Record ID", default="")
|
|
||||||
|
|
||||||
_db_name = "Database"
|
|
||||||
_token_prop_name = "token_path"
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self._init_exec()
|
|
||||||
self.add_input("NodeSocketString", "Gcode")
|
|
||||||
self.add_input("SLICESOCK_Settings", "Metadata")
|
|
||||||
|
|
||||||
def update(self) -> None:
|
|
||||||
super().update()
|
|
||||||
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
|
||||||
inp = self.inputs.get("Gcode")
|
|
||||||
if inp:
|
|
||||||
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
|
||||||
super().draw_buttons(context, layout)
|
|
||||||
layout.prop(self, "record_id")
|
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
|
||||||
from ....infra.networking.bgcode import convert_to_bgcode
|
|
||||||
from ....infra.networking.gcode_utils import get_gcode_name_from_metadata, parse_gcode_metadata
|
|
||||||
from ....infra.blender.list_utils import flatten_list
|
|
||||||
|
|
||||||
gcodes: list[str] = [str(gc) for gc in flatten_list(self.resolve_multi_input("Gcode")) if gc]
|
|
||||||
|
|
||||||
for gc in gcodes:
|
|
||||||
if not gc:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Parse Metadata from Gcode
|
|
||||||
part_name = get_gcode_name_from_metadata(gc)
|
|
||||||
fields = parse_gcode_metadata(gc)
|
|
||||||
fields["Part ID / Name"] = part_name
|
|
||||||
|
|
||||||
# Detect binary trigger in gcode comments
|
|
||||||
is_binary = "; binary_gcode = 1" in gc
|
|
||||||
|
|
||||||
filename = part_name + (".bgcode" if is_binary else ".gcode")
|
|
||||||
|
|
||||||
token_path = getattr(self, self._token_prop_name, "")
|
|
||||||
token = ""
|
|
||||||
if token_path:
|
|
||||||
abs_token_path = bpy.path.abspath(token_path)
|
|
||||||
if os.path.exists(abs_token_path):
|
|
||||||
try:
|
|
||||||
with open(abs_token_path, 'r', encoding='utf-8') as f:
|
|
||||||
token = f.read().strip()
|
|
||||||
except Exception as e:
|
|
||||||
if operator: operator.report({'ERROR'}, f"Failed to read token file: {str(e)}")
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
if not self._check_config(token):
|
|
||||||
if operator: operator.report({'ERROR'}, f"{self._db_name} configuration incomplete")
|
|
||||||
return 'incomplete config'
|
|
||||||
|
|
||||||
temp_dir = tempfile.TemporaryDirectory()
|
|
||||||
temp_path = Path(temp_dir.name) / filename
|
|
||||||
|
|
||||||
try:
|
|
||||||
if is_binary:
|
|
||||||
content = convert_to_bgcode(gc, part_name, True)
|
|
||||||
with open(temp_path, "wb") as f:
|
|
||||||
f.write(content)
|
|
||||||
else:
|
|
||||||
with open(temp_path, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(gc)
|
|
||||||
|
|
||||||
record_id = self._submit_to_db(token, fields, temp_path)
|
|
||||||
if operator: operator.report({'INFO'}, f"Submitted to {self._db_name} (Record: {record_id})")
|
|
||||||
except Exception as e:
|
|
||||||
if operator: operator.report({'ERROR'}, f"{self._db_name} Error: {str(e)}")
|
|
||||||
finally:
|
|
||||||
if temp_path.exists():
|
|
||||||
temp_path.unlink()
|
|
||||||
temp_dir.cleanup()
|
|
||||||
|
|
||||||
def _check_config(self, token: str) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _submit_to_db(self, token: str, fields: dict[str, Any], temp_path: Path) -> str:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_BaserowSubmit(ExecSliceNode_DatabaseSubmit):
|
|
||||||
bl_label = "Baserow Submit"
|
|
||||||
button_label = "Submit to Baserow"
|
|
||||||
|
|
||||||
host: str = NfStringProperty(name="Host", default="https://baserow.io")
|
|
||||||
token_path: str = NfStringProperty(name="Token File", subtype='FILE_PATH', default="")
|
|
||||||
table_id: str = NfStringProperty(name="Table ID", default="")
|
|
||||||
|
|
||||||
_db_name = "Baserow"
|
|
||||||
_token_prop_name = "token_path"
|
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
|
||||||
layout.prop(self, "host")
|
|
||||||
layout.prop(self, "token_path")
|
|
||||||
layout.prop(self, "table_id")
|
|
||||||
super().draw_buttons(context, layout)
|
|
||||||
|
|
||||||
def _check_config(self, token):
|
|
||||||
return bool(token and self.table_id and self.host)
|
|
||||||
|
|
||||||
def _submit_to_db(self, token, fields, temp_path):
|
|
||||||
from ....infra.networking.baserow_backend import BaserowBackend
|
|
||||||
db = BaserowBackend(self.host, token)
|
|
||||||
|
|
||||||
if self.record_id:
|
|
||||||
try:
|
|
||||||
rid = int(self.record_id)
|
|
||||||
res = db.upsert_record(self.table_id, rid, fields)
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError("Record ID must be an integer")
|
|
||||||
else:
|
|
||||||
res = db.create_record(self.table_id, fields)
|
|
||||||
|
|
||||||
record_id = int(res['id'])
|
|
||||||
db.upload_attachment(self.table_id, record_id, 'GCODE', temp_path)
|
|
||||||
return str(record_id)
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_AirtableSubmit(ExecSliceNode_DatabaseSubmit):
|
|
||||||
bl_label = "Airtable Submit"
|
|
||||||
button_label = "Submit to Airtable"
|
|
||||||
|
|
||||||
pat_path: str = NfStringProperty(name="PAT File", subtype='FILE_PATH', default="")
|
|
||||||
base_id: str = NfStringProperty(name="Base ID", default="")
|
|
||||||
table_id: str = NfStringProperty(name="Table ID", default="")
|
|
||||||
|
|
||||||
_db_name = "Airtable"
|
|
||||||
_token_prop_name = "pat_path"
|
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
|
||||||
layout.prop(self, "pat_path")
|
|
||||||
layout.prop(self, "base_id")
|
|
||||||
layout.prop(self, "table_id")
|
|
||||||
super().draw_buttons(context, layout)
|
|
||||||
|
|
||||||
def _check_config(self, token):
|
|
||||||
return bool(token and self.base_id and self.table_id)
|
|
||||||
|
|
||||||
def _submit_to_db(self, token, fields, temp_path):
|
|
||||||
from ....infra.networking.airtable_backend import AirtableInterface, AirtableRecord
|
|
||||||
db = AirtableInterface(token)
|
|
||||||
|
|
||||||
if self.record_id:
|
|
||||||
record: AirtableRecord = {"id": self.record_id, "fields": fields}
|
|
||||||
res = db.update_records(self.base_id, self.table_id, [record])
|
|
||||||
record_id = res[0]['id']
|
|
||||||
else:
|
|
||||||
res = db.create_record(self.base_id, self.table_id, fields)
|
|
||||||
record_id = res['id']
|
|
||||||
|
|
||||||
db.upload_attachment(self.base_id, record_id, 'GCODE', temp_path)
|
|
||||||
return str(record_id)
|
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_MeshToPdf(ExecSliceNode):
|
|
||||||
bl_label = "Mesh to PDF"
|
|
||||||
button_label = "Generate PDF"
|
|
||||||
|
|
||||||
template: str = NfStringProperty(
|
|
||||||
name="Template",
|
|
||||||
default="{object_name}-{size_x_mm}x{size_y_mm}-{today_date_iso}.pdf",
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def filename_template(self) -> str:
|
|
||||||
return getattr(self, "template", "{object_name}-{size_x_mm}x{size_y_mm}-{today_date_iso}.pdf")
|
|
||||||
|
|
||||||
@filename_template.setter
|
|
||||||
def filename_template(self, value: str) -> None:
|
|
||||||
self.template = value
|
|
||||||
|
|
||||||
draw_wireframe: bool = NfBoolProperty(name="Wireframe", default=True)
|
|
||||||
draw_boundary: bool = NfBoolProperty(name="Boundary (1 Face)", default=False)
|
|
||||||
draw_contour: bool = NfBoolProperty(name="Contour", default=False)
|
|
||||||
draw_crease: bool = NfBoolProperty(name="Crease Angle", default=False)
|
|
||||||
crease_angle: float = NfFloatProperty(name="Angle (°)", default=30.0, min=0.0, max=180.0)
|
|
||||||
draw_bbox: bool = NfBoolProperty(name="Bounding Box", default=False)
|
|
||||||
draw_sa2_holes: bool = NfBoolProperty(name="Holes (Ø)", default=False)
|
|
||||||
show_lineart: bool = NfBoolProperty(name="Lineart Visibility", default=False)
|
|
||||||
show_sa2: bool = NfBoolProperty(name="SA2 Settings", default=False)
|
|
||||||
draw_grid_xy: bool = NfBoolProperty(name="XY Grid (Top)", default=True)
|
|
||||||
draw_grid_xz: bool = NfBoolProperty(name="XZ Grid (Front)", default=False)
|
|
||||||
draw_grid_yz: bool = NfBoolProperty(name="YZ Grid (Side)", default=False)
|
|
||||||
show_grids: bool = NfBoolProperty(name="Grids", default=False)
|
|
||||||
|
|
||||||
draw_header: bool = NfBoolProperty(name="Technical Header", default=True)
|
|
||||||
show_header: bool = NfBoolProperty(name="Technical Drawing Header", default=False)
|
|
||||||
header_company: str = NfStringProperty(name="Company", default="")
|
|
||||||
header_part_name: str = NfStringProperty(name="Part Name", default="")
|
|
||||||
header_drawing_number: str = NfStringProperty(name="Drawing No.", default="")
|
|
||||||
header_author: str = NfStringProperty(name="Author", default="")
|
|
||||||
header_revision: str = NfStringProperty(name="Revision", default="A")
|
|
||||||
header_material: str = NfStringProperty(name="Material", default="")
|
|
||||||
header_tolerance: str = NfStringProperty(name="Tolerance", default="")
|
|
||||||
|
|
||||||
font_size: float = NfFloatProperty(name="Font Size (mm)", default=0.0, min=0.0, max=100.0)
|
|
||||||
margin: float = NfFloatProperty(name="Margin (mm)", default=10.0, min=0.0, max=500.0)
|
|
||||||
|
|
||||||
bg_color: tuple[float, float, float] = NfFloatVectorProperty(
|
|
||||||
name="Background",
|
|
||||||
subtype='COLOR',
|
|
||||||
default=(1.0, 1.0, 1.0),
|
|
||||||
min=0.0,
|
|
||||||
max=1.0,
|
|
||||||
size=3
|
|
||||||
)
|
|
||||||
|
|
||||||
wire_color: tuple[float, float, float] = NfFloatVectorProperty(
|
|
||||||
name="Wireframe",
|
|
||||||
subtype='COLOR',
|
|
||||||
default=(0.0, 0.0, 0.0),
|
|
||||||
min=0.0,
|
|
||||||
max=1.0,
|
|
||||||
size=3
|
|
||||||
)
|
|
||||||
|
|
||||||
bbox_color: tuple[float, float, float] = NfFloatVectorProperty(
|
|
||||||
name="BBox Color",
|
|
||||||
subtype='COLOR',
|
|
||||||
default=(0.0, 0.0, 1.0),
|
|
||||||
min=0.0,
|
|
||||||
max=1.0,
|
|
||||||
size=3
|
|
||||||
)
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self._init_exec()
|
|
||||||
self.add_input("SLICESOCK_DirPath", "Folder")
|
|
||||||
self.add_input("SLICESOCK_Settings", "Settings")
|
|
||||||
self.add_input("SLICESOCK_Mesh", "Geometry")
|
|
||||||
self.add_input("SLICESOCK_Paths", "Paths")
|
|
||||||
|
|
||||||
def update(self) -> None:
|
|
||||||
super().update()
|
|
||||||
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
|
||||||
for name in ("Geometry", "Paths"):
|
|
||||||
inp = self.inputs.get(name)
|
|
||||||
if inp:
|
|
||||||
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
|
||||||
super().draw_buttons(context, layout)
|
|
||||||
|
|
||||||
layout.prop(self, "template")
|
|
||||||
|
|
||||||
# Technical Drawing Header subpanel (collapsible, collapsed by default)
|
|
||||||
layout.prop(self, "show_header", text="Technical Drawing Header", icon='TRIA_DOWN' if self.show_header else 'TRIA_RIGHT')
|
|
||||||
if self.show_header:
|
|
||||||
box = layout.box()
|
|
||||||
box.prop(self, "draw_header")
|
|
||||||
box.prop(self, "header_company")
|
|
||||||
box.prop(self, "header_part_name")
|
|
||||||
box.prop(self, "header_drawing_number")
|
|
||||||
box.prop(self, "header_author")
|
|
||||||
box.prop(self, "header_revision")
|
|
||||||
box.prop(self, "header_material")
|
|
||||||
box.prop(self, "header_tolerance")
|
|
||||||
|
|
||||||
# Lineart Visibility subpanel (collapsible, collapsed by default)
|
|
||||||
layout.prop(self, "show_lineart", text="Lineart Visibility", icon='TRIA_DOWN' if self.show_lineart else 'TRIA_RIGHT')
|
|
||||||
if self.show_lineart:
|
|
||||||
box = layout.box()
|
|
||||||
box.prop(self, "draw_wireframe")
|
|
||||||
box.prop(self, "draw_boundary")
|
|
||||||
box.prop(self, "draw_contour")
|
|
||||||
box.prop(self, "draw_crease")
|
|
||||||
if self.draw_crease:
|
|
||||||
box.prop(self, "crease_angle")
|
|
||||||
|
|
||||||
# SA2 Settings subpanel (collapsible, collapsed by default)
|
|
||||||
layout.prop(self, "show_sa2", text="SA2 Settings", icon='TRIA_DOWN' if self.show_sa2 else 'TRIA_RIGHT')
|
|
||||||
if self.show_sa2:
|
|
||||||
box = layout.box()
|
|
||||||
box.prop(self, "draw_sa2_holes")
|
|
||||||
|
|
||||||
# Grid Display subpanel
|
|
||||||
layout.prop(self, "show_grids", text="Grids", icon='TRIA_DOWN' if self.show_grids else 'TRIA_RIGHT')
|
|
||||||
if self.show_grids:
|
|
||||||
box = layout.box()
|
|
||||||
box.prop(self, "draw_grid_xy")
|
|
||||||
box.prop(self, "draw_grid_xz")
|
|
||||||
box.prop(self, "draw_grid_yz")
|
|
||||||
|
|
||||||
# Display settings
|
|
||||||
layout.prop(self, "draw_bbox")
|
|
||||||
if self.draw_bbox:
|
|
||||||
layout.prop(self, "bbox_color")
|
|
||||||
|
|
||||||
# Styling settings
|
|
||||||
layout.prop(self, "font_size")
|
|
||||||
layout.prop(self, "margin")
|
|
||||||
layout.prop(self, "bg_color")
|
|
||||||
layout.prop(self, "wire_color")
|
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
|
||||||
import datetime
|
|
||||||
from ...pdf_exporter import (
|
|
||||||
extract_unique_top_view_edges,
|
|
||||||
export_mesh_to_pdf,
|
|
||||||
format_pdf_filename_template,
|
|
||||||
get_first_object_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
geom = self.resolve("Geometry")
|
|
||||||
slice_paths = self.resolve("Paths")
|
|
||||||
folder: str = self.resolve("Folder") or ""
|
|
||||||
|
|
||||||
# Collect raw geometry items
|
|
||||||
raw_geom_items: list[Any] = []
|
|
||||||
if geom is not None:
|
|
||||||
raw_list = geom if isinstance(geom, list) else [geom]
|
|
||||||
raw_geom_items = [g for g in raw_list if g is not None]
|
|
||||||
|
|
||||||
# Collect slice path items
|
|
||||||
slice_paths_items: list[Any] = []
|
|
||||||
if slice_paths is not None:
|
|
||||||
is_sp_inst = isinstance(slice_paths, SlicePaths)
|
|
||||||
raw_list = slice_paths if (isinstance(slice_paths, list) and not is_sp_inst) else [slice_paths]
|
|
||||||
slice_paths_items = [p for p in raw_list if p is not None]
|
|
||||||
|
|
||||||
if not raw_geom_items and not slice_paths_items:
|
|
||||||
if operator: operator.report({'ERROR'}, "No Geometry or Paths connected")
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
if not folder:
|
|
||||||
if operator: operator.report({'ERROR'}, "No Folder provided")
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
abs_folder = folder
|
|
||||||
if hasattr(bpy, "path") and hasattr(bpy.path, "abspath"):
|
|
||||||
try:
|
|
||||||
res = bpy.path.abspath(f"//{folder}" if not folder.startswith(("/", "\\")) and ":" not in folder else folder)
|
|
||||||
if isinstance(res, str) and not type(res).__module__.startswith("unittest.mock"):
|
|
||||||
abs_folder = res
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
folder_path = Path(abs_folder)
|
|
||||||
|
|
||||||
has_raw_geom = len(raw_geom_items) > 0
|
|
||||||
primary_items = raw_geom_items if has_raw_geom else slice_paths_items
|
|
||||||
|
|
||||||
is_single_pdf_file = len(primary_items) == 1 and folder_path.suffix.lower() == ".pdf"
|
|
||||||
|
|
||||||
# Automatic scene scale determination
|
|
||||||
scale_str = "1:1"
|
|
||||||
try:
|
|
||||||
if hasattr(bpy, "context") and hasattr(bpy.context, "scene") and bpy.context.scene:
|
|
||||||
scale_len = getattr(bpy.context.scene.unit_settings, "scale_length", 1.0)
|
|
||||||
if scale_len == 1.0 or scale_len == 0.001:
|
|
||||||
scale_str = "1:1"
|
|
||||||
elif scale_len > 1.0:
|
|
||||||
scale_str = f"{scale_len:g}:1"
|
|
||||||
elif scale_len > 0.0:
|
|
||||||
scale_str = f"1:{1.0 / scale_len:g}"
|
|
||||||
except Exception:
|
|
||||||
scale_str = "1:1"
|
|
||||||
|
|
||||||
header_info = {
|
|
||||||
"company": self.header_company,
|
|
||||||
"part_name": self.header_part_name,
|
|
||||||
"drawing_number": self.header_drawing_number,
|
|
||||||
"author": self.header_author,
|
|
||||||
"revision": self.header_revision,
|
|
||||||
"material": self.header_material,
|
|
||||||
"tolerance": self.header_tolerance,
|
|
||||||
"date": datetime.date.today().isoformat(),
|
|
||||||
"scale": scale_str,
|
|
||||||
}
|
|
||||||
|
|
||||||
template = getattr(self, "template", None) or getattr(self, "filename_template", None) or "{object_name}-{size_x_mm}x{size_y_mm}-{today_date_iso}.pdf"
|
|
||||||
|
|
||||||
for i, target_item in enumerate(primary_items):
|
|
||||||
if has_raw_geom:
|
|
||||||
current_raw_geom = target_item
|
|
||||||
current_slice_paths = slice_paths
|
|
||||||
else:
|
|
||||||
current_raw_geom = None
|
|
||||||
current_slice_paths = target_item
|
|
||||||
|
|
||||||
current_header_info = dict(header_info)
|
|
||||||
part_name_prop = (self.header_part_name or "").strip()
|
|
||||||
if not part_name_prop:
|
|
||||||
default_part_name = (
|
|
||||||
get_first_object_name(target_item)
|
|
||||||
or get_first_object_name(current_raw_geom)
|
|
||||||
or get_first_object_name(current_slice_paths)
|
|
||||||
)
|
|
||||||
current_header_info["part_name"] = default_part_name
|
|
||||||
|
|
||||||
edges_2d = np.empty((0, 2, 2), dtype=np.float64)
|
|
||||||
if current_raw_geom is not None:
|
|
||||||
raw_e = extract_unique_top_view_edges(current_raw_geom)
|
|
||||||
if len(raw_e) > 0:
|
|
||||||
edges_2d = raw_e
|
|
||||||
if current_slice_paths is not None:
|
|
||||||
sp_e = extract_unique_top_view_edges(current_slice_paths)
|
|
||||||
if len(sp_e) > 0:
|
|
||||||
edges_2d = np.concatenate([edges_2d, sp_e], axis=0) if len(edges_2d) > 0 else sp_e
|
|
||||||
|
|
||||||
try:
|
|
||||||
exporter = export_mesh_to_pdf(
|
|
||||||
edges_2d=edges_2d,
|
|
||||||
filepath=None,
|
|
||||||
draw_wireframe=self.draw_wireframe,
|
|
||||||
draw_boundary=self.draw_boundary,
|
|
||||||
draw_contour=self.draw_contour,
|
|
||||||
draw_crease=self.draw_crease,
|
|
||||||
draw_bbox=self.draw_bbox,
|
|
||||||
draw_sa2_holes=self.draw_sa2_holes,
|
|
||||||
draw_header=self.draw_header,
|
|
||||||
header_info=current_header_info,
|
|
||||||
crease_angle=self.crease_angle,
|
|
||||||
margin_mm=self.margin,
|
|
||||||
font_size_mm=self.font_size * 2.834645669291339,
|
|
||||||
stroke_color=(float(self.wire_color[0]), float(self.wire_color[1]), float(self.wire_color[2])),
|
|
||||||
bg_color=(float(self.bg_color[0]), float(self.bg_color[1]), float(self.bg_color[2])),
|
|
||||||
bbox_color=(float(self.bbox_color[0]), float(self.bbox_color[1]), float(self.bbox_color[2])),
|
|
||||||
raw_geometry=current_raw_geom,
|
|
||||||
slice_paths=current_slice_paths,
|
|
||||||
draw_grid_xy=getattr(self, "draw_grid_xy", True),
|
|
||||||
draw_grid_xz=getattr(self, "draw_grid_xz", False),
|
|
||||||
draw_grid_yz=getattr(self, "draw_grid_yz", False),
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_single_pdf_file:
|
|
||||||
os.makedirs(folder_path.parent, exist_ok=True)
|
|
||||||
filepath = str(folder_path)
|
|
||||||
else:
|
|
||||||
out_dir = folder_path if folder_path.suffix == "" else folder_path.parent
|
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
|
||||||
if isinstance(target_item, SlicingGroup) and target_item.collections:
|
|
||||||
raw_name = next(iter(target_item.collections.keys()))
|
|
||||||
elif hasattr(target_item, 'name') and getattr(target_item, 'name'):
|
|
||||||
raw_name = str(getattr(target_item, 'name'))
|
|
||||||
else:
|
|
||||||
raw_name = f"object_{i}"
|
|
||||||
|
|
||||||
object_name = raw_name.split('.')[0] if raw_name else f"object_{i}"
|
|
||||||
filename = format_pdf_filename_template(
|
|
||||||
template=template,
|
|
||||||
object_name=object_name,
|
|
||||||
size_x_mm=round(exporter.page_w_mm),
|
|
||||||
size_y_mm=round(exporter.page_h_mm),
|
|
||||||
today_date_iso=datetime.date.today().isoformat(),
|
|
||||||
target_item=target_item,
|
|
||||||
header_info=current_header_info,
|
|
||||||
)
|
|
||||||
filepath = str(out_dir / filename)
|
|
||||||
|
|
||||||
if len(edges_2d) == 0:
|
|
||||||
if operator: operator.report({'WARNING'}, f"Geometry for {os.path.basename(filepath)} has no valid 2D top-view wireframe edges")
|
|
||||||
|
|
||||||
exporter.export(filepath)
|
|
||||||
if operator: operator.report({'INFO'}, f"Mesh PDF saved to {filepath}")
|
|
||||||
except Exception as e:
|
|
||||||
if operator: operator.report({'ERROR'}, f"PDF Generation Error: {str(e)}")
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
return 'ok'
|
|
||||||
|
|
||||||
|
|
||||||
# Alias for backward compatibility
|
|
||||||
SLICENODE_WireframeToPdf = SLICENODE_MeshToPdf
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
19
core/nodeslicer/nodes/output/__init__.py
Normal file
19
core/nodeslicer/nodes/output/__init__.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .base import ExecSliceNode_DatabaseSubmit
|
||||||
|
from .gcode_to_file import SLICENODE_GcodeToFile
|
||||||
|
from .gcode_to_text_block import SLICENODE_GcodeToTextBlock
|
||||||
|
from .start_print import SLICENODE_StartPrint
|
||||||
|
from .baserow_submit import SLICENODE_BaserowSubmit
|
||||||
|
from .airtable_submit import SLICENODE_AirtableSubmit
|
||||||
|
from .mesh_to_pdf import SLICENODE_MeshToPdf
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ExecSliceNode_DatabaseSubmit",
|
||||||
|
"SLICENODE_GcodeToFile",
|
||||||
|
"SLICENODE_GcodeToTextBlock",
|
||||||
|
"SLICENODE_StartPrint",
|
||||||
|
"SLICENODE_BaserowSubmit",
|
||||||
|
"SLICENODE_AirtableSubmit",
|
||||||
|
"SLICENODE_MeshToPdf",
|
||||||
|
]
|
||||||
41
core/nodeslicer/nodes/output/airtable_submit.py
Normal file
41
core/nodeslicer/nodes/output/airtable_submit.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfStringProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from .base import ExecSliceNode_DatabaseSubmit
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_AirtableSubmit(ExecSliceNode_DatabaseSubmit):
|
||||||
|
bl_label = "Airtable Submit"
|
||||||
|
button_label = "Submit to Airtable"
|
||||||
|
|
||||||
|
pat_path: str = NfStringProperty(name="PAT File", subtype='FILE_PATH', default="")
|
||||||
|
base_id: str = NfStringProperty(name="Base ID", default="")
|
||||||
|
table_id: str = NfStringProperty(name="Table ID", default="")
|
||||||
|
|
||||||
|
_db_name = "Airtable"
|
||||||
|
_token_prop_name = "pat_path"
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
layout.prop(self, "pat_path")
|
||||||
|
layout.prop(self, "base_id")
|
||||||
|
layout.prop(self, "table_id")
|
||||||
|
super().draw_buttons(context, layout)
|
||||||
|
|
||||||
|
def _check_config(self, token):
|
||||||
|
return bool(token and self.base_id and self.table_id)
|
||||||
|
|
||||||
|
def _submit_to_db(self, token, fields, temp_path):
|
||||||
|
from .....infra.networking.airtable_backend import AirtableInterface, AirtableRecord
|
||||||
|
db = AirtableInterface(token)
|
||||||
|
|
||||||
|
if self.record_id:
|
||||||
|
record: AirtableRecord = {"id": self.record_id, "fields": fields}
|
||||||
|
res = db.update_records(self.base_id, self.table_id, [record])
|
||||||
|
record_id = res[0]['id']
|
||||||
|
else:
|
||||||
|
res = db.create_record(self.base_id, self.table_id, fields)
|
||||||
|
record_id = res['id']
|
||||||
|
|
||||||
|
db.upload_attachment(self.base_id, record_id, 'GCODE', temp_path)
|
||||||
|
return str(record_id)
|
||||||
95
core/nodeslicer/nodes/output/base.py
Normal file
95
core/nodeslicer/nodes/output/base.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfStringProperty
|
||||||
|
from ..base import ExecSliceNode
|
||||||
|
|
||||||
|
class ExecSliceNode_DatabaseSubmit(ExecSliceNode):
|
||||||
|
record_id: str = NfStringProperty(name="Record ID", default="")
|
||||||
|
|
||||||
|
_db_name = "Database"
|
||||||
|
_token_prop_name = "token_path"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self._init_exec()
|
||||||
|
self.add_input("NodeSocketString", "Gcode")
|
||||||
|
self.add_input("SLICESOCK_Settings", "Metadata")
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from .....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
inp = self.inputs.get("Gcode")
|
||||||
|
if inp:
|
||||||
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
super().draw_buttons(context, layout)
|
||||||
|
layout.prop(self, "record_id")
|
||||||
|
|
||||||
|
def evaluate(self, operator=None):
|
||||||
|
from .....infra.networking.bgcode import convert_to_bgcode
|
||||||
|
from .....infra.networking.gcode_utils import get_gcode_name_from_metadata, parse_gcode_metadata
|
||||||
|
from .....infra.blender.list_utils import flatten_list
|
||||||
|
|
||||||
|
gcodes: list[str] = [str(gc) for gc in flatten_list(self.resolve_multi_input("Gcode")) if gc]
|
||||||
|
|
||||||
|
for gc in gcodes:
|
||||||
|
if not gc:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse Metadata from Gcode
|
||||||
|
part_name = get_gcode_name_from_metadata(gc)
|
||||||
|
fields = parse_gcode_metadata(gc)
|
||||||
|
fields["Part ID / Name"] = part_name
|
||||||
|
|
||||||
|
# Detect binary trigger in gcode comments
|
||||||
|
is_binary = "; binary_gcode = 1" in gc
|
||||||
|
|
||||||
|
filename = part_name + (".bgcode" if is_binary else ".gcode")
|
||||||
|
|
||||||
|
token_path = getattr(self, self._token_prop_name, "")
|
||||||
|
token = ""
|
||||||
|
if token_path:
|
||||||
|
abs_token_path = bpy.path.abspath(token_path)
|
||||||
|
if os.path.exists(abs_token_path):
|
||||||
|
try:
|
||||||
|
with open(abs_token_path, 'r', encoding='utf-8') as f:
|
||||||
|
token = f.read().strip()
|
||||||
|
except Exception as e:
|
||||||
|
if operator: operator.report({'ERROR'}, f"Failed to read token file: {str(e)}")
|
||||||
|
return 'error'
|
||||||
|
|
||||||
|
if not self._check_config(token):
|
||||||
|
if operator: operator.report({'ERROR'}, f"{self._db_name} configuration incomplete")
|
||||||
|
return 'incomplete config'
|
||||||
|
|
||||||
|
temp_dir = tempfile.TemporaryDirectory()
|
||||||
|
temp_path = Path(temp_dir.name) / filename
|
||||||
|
|
||||||
|
try:
|
||||||
|
if is_binary:
|
||||||
|
content = convert_to_bgcode(gc, part_name, True)
|
||||||
|
with open(temp_path, "wb") as f:
|
||||||
|
f.write(content)
|
||||||
|
else:
|
||||||
|
with open(temp_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(gc)
|
||||||
|
|
||||||
|
record_id = self._submit_to_db(token, fields, temp_path)
|
||||||
|
if operator: operator.report({'INFO'}, f"Submitted to {self._db_name} (Record: {record_id})")
|
||||||
|
except Exception as e:
|
||||||
|
if operator: operator.report({'ERROR'}, f"{self._db_name} Error: {str(e)}")
|
||||||
|
finally:
|
||||||
|
if temp_path.exists():
|
||||||
|
temp_path.unlink()
|
||||||
|
temp_dir.cleanup()
|
||||||
|
|
||||||
|
def _check_config(self, token: str) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _submit_to_db(self, token: str, fields: dict[str, Any], temp_path: Path) -> str:
|
||||||
|
return ""
|
||||||
43
core/nodeslicer/nodes/output/baserow_submit.py
Normal file
43
core/nodeslicer/nodes/output/baserow_submit.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfStringProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from .base import ExecSliceNode_DatabaseSubmit
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_BaserowSubmit(ExecSliceNode_DatabaseSubmit):
|
||||||
|
bl_label = "Baserow Submit"
|
||||||
|
button_label = "Submit to Baserow"
|
||||||
|
|
||||||
|
host: str = NfStringProperty(name="Host", default="https://baserow.io")
|
||||||
|
token_path: str = NfStringProperty(name="Token File", subtype='FILE_PATH', default="")
|
||||||
|
table_id: str = NfStringProperty(name="Table ID", default="")
|
||||||
|
|
||||||
|
_db_name = "Baserow"
|
||||||
|
_token_prop_name = "token_path"
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
layout.prop(self, "host")
|
||||||
|
layout.prop(self, "token_path")
|
||||||
|
layout.prop(self, "table_id")
|
||||||
|
super().draw_buttons(context, layout)
|
||||||
|
|
||||||
|
def _check_config(self, token):
|
||||||
|
return bool(token and self.table_id and self.host)
|
||||||
|
|
||||||
|
def _submit_to_db(self, token, fields, temp_path):
|
||||||
|
from .....infra.networking.baserow_backend import BaserowBackend
|
||||||
|
db = BaserowBackend(self.host, token)
|
||||||
|
|
||||||
|
if self.record_id:
|
||||||
|
try:
|
||||||
|
rid = int(self.record_id)
|
||||||
|
res = db.upsert_record(self.table_id, rid, fields)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("Record ID must be an integer")
|
||||||
|
else:
|
||||||
|
res = db.create_record(self.table_id, fields)
|
||||||
|
|
||||||
|
record_id = int(res['id'])
|
||||||
|
db.upload_attachment(self.table_id, record_id, 'GCODE', temp_path)
|
||||||
|
return str(record_id)
|
||||||
90
core/nodeslicer/nodes/output/gcode_to_file.py
Normal file
90
core/nodeslicer/nodes/output/gcode_to_file.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import ExecSliceNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GcodeToFile(ExecSliceNode):
|
||||||
|
bl_label = "Gcode To File"
|
||||||
|
button_label = "Generate File"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self._init_exec()
|
||||||
|
self.add_input("NodeSocketString", "Folder")
|
||||||
|
self.add_input("NodeSocketString", "Name")
|
||||||
|
self.add_multi_socket_group("NodeSocketString", name="Gcode")
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from .....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
for inp in self.inputs:
|
||||||
|
if getattr(inp, "name", "") == "Gcode" or getattr(inp, "identifier", "").startswith("Gcode_ms_in_"):
|
||||||
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
super().draw_buttons(context, layout)
|
||||||
|
|
||||||
|
def evaluate(self, operator=None):
|
||||||
|
from .....infra.networking.gcode_utils import get_gcode_name_from_metadata, prepare_gcode_file
|
||||||
|
from .....infra.blender.list_utils import flatten_list
|
||||||
|
|
||||||
|
raw_gcodes = self.resolve_multi_input('Gcode')
|
||||||
|
gcodes: list[str] = [str(gc) for gc in flatten_list(raw_gcodes) if gc]
|
||||||
|
|
||||||
|
folder: str = self.resolve("Folder")
|
||||||
|
if not gcodes:
|
||||||
|
if operator: operator.report({'ERROR'}, "No Gcode to save")
|
||||||
|
return 'error'
|
||||||
|
if not folder:
|
||||||
|
if operator: operator.report({'ERROR'}, "No Folder provided")
|
||||||
|
return 'error'
|
||||||
|
|
||||||
|
base_name: str = self.resolve("Name")
|
||||||
|
total_parts: int = len(gcodes)
|
||||||
|
num_digits: int = max(2, len(str(total_parts)))
|
||||||
|
|
||||||
|
is_name_list = isinstance(base_name, (list, tuple))
|
||||||
|
if is_name_list:
|
||||||
|
if len(base_name) == 1:
|
||||||
|
base_name = str(base_name[0])
|
||||||
|
is_name_list = False
|
||||||
|
elif len(base_name) != total_parts:
|
||||||
|
msg = f"Input list length mismatch: Gcode has {total_parts} items, but Name has {len(base_name)} items."
|
||||||
|
if operator: operator.report({'ERROR'}, msg)
|
||||||
|
return 'error'
|
||||||
|
|
||||||
|
for i, gc in enumerate(gcodes):
|
||||||
|
if is_name_list:
|
||||||
|
name = str(base_name[i])
|
||||||
|
elif base_name:
|
||||||
|
if total_parts > 1:
|
||||||
|
name = f"{base_name}{i:0{num_digits}d}"
|
||||||
|
else:
|
||||||
|
name = str(base_name)
|
||||||
|
else:
|
||||||
|
name = get_gcode_name_from_metadata(gc, default=f'object_{i}')
|
||||||
|
|
||||||
|
folder_path = Path(bpy.path.abspath(f"//{folder}"))
|
||||||
|
filepath_abs = folder_path / name
|
||||||
|
dirname = os.path.dirname(filepath_abs)
|
||||||
|
basename = os.path.basename(filepath_abs)
|
||||||
|
name_part = basename.split('.')[0]
|
||||||
|
os.makedirs(dirname, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_bytes, filename = prepare_gcode_file(gc, name_part)
|
||||||
|
filepath = os.path.join(dirname, filename)
|
||||||
|
with open(filepath, "wb") as f:
|
||||||
|
f.write(file_bytes)
|
||||||
|
if filename.endswith(".bgcode"):
|
||||||
|
if operator: operator.report({'INFO'}, f"Binary G-code generated at {filepath}")
|
||||||
|
else:
|
||||||
|
if operator: operator.report({'INFO'}, f"G-code saved to {filepath}")
|
||||||
|
except Exception as e:
|
||||||
|
if operator: operator.report({'ERROR'}, str(e))
|
||||||
|
return 'error'
|
||||||
|
|
||||||
|
return 'ok'
|
||||||
62
core/nodeslicer/nodes/output/gcode_to_text_block.py
Normal file
62
core/nodeslicer/nodes/output/gcode_to_text_block.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import os
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfStringProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import ExecSliceNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GcodeToTextBlock(ExecSliceNode):
|
||||||
|
bl_label = "Gcode to Text Block"
|
||||||
|
button_label = "Update Text Block"
|
||||||
|
|
||||||
|
text_block_name: str = NfStringProperty(name="Text Block Name", default="NodeSlicer_Gcode.gcode")
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self._init_exec()
|
||||||
|
self.add_input("NodeSocketString", "Gcode")
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from .....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
inp = self.inputs.get("Gcode")
|
||||||
|
if inp:
|
||||||
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
super().draw_buttons(context, layout)
|
||||||
|
layout.prop(self, "text_block_name")
|
||||||
|
|
||||||
|
def evaluate(self, operator=None):
|
||||||
|
from .....infra.blender.list_utils import flatten_list, is_list_data
|
||||||
|
raw = self.resolve("Gcode")
|
||||||
|
if not raw:
|
||||||
|
return 'ok'
|
||||||
|
|
||||||
|
gcodes = [str(g) for g in flatten_list(raw) if g] if is_list_data(raw) else ([str(raw)] if raw else [])
|
||||||
|
if not gcodes:
|
||||||
|
return 'ok'
|
||||||
|
|
||||||
|
base_name = self.text_block_name
|
||||||
|
name_root, ext = os.path.splitext(base_name)
|
||||||
|
total_parts = len(gcodes)
|
||||||
|
num_digits = max(2, len(str(total_parts)))
|
||||||
|
|
||||||
|
for i, gcode in enumerate(gcodes):
|
||||||
|
name = f"{name_root}_{i:0{num_digits}d}{ext}" if total_parts > 1 else base_name
|
||||||
|
text_block = bpy.data.texts.get(name)
|
||||||
|
if text_block is None:
|
||||||
|
text_block = bpy.data.texts.new(name=name)
|
||||||
|
text_block.from_string(gcode)
|
||||||
|
|
||||||
|
# Attempt to show in an open text editor
|
||||||
|
assert bpy.context.window_manager
|
||||||
|
for window in bpy.context.window_manager.windows:
|
||||||
|
for area in window.screen.areas:
|
||||||
|
if area.type == 'TEXT_EDITOR':
|
||||||
|
for space in area.spaces:
|
||||||
|
if space.type == 'TEXT_EDITOR':
|
||||||
|
space.text = text_block
|
||||||
|
|
||||||
|
return 'ok'
|
||||||
321
core/nodeslicer/nodes/output/mesh_to_pdf.py
Normal file
321
core/nodeslicer/nodes/output/mesh_to_pdf.py
Normal file
|
|
@ -0,0 +1,321 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfStringProperty, NfFloatProperty, NfFloatVectorProperty, NfBoolProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import ExecSliceNode, SlicePaths, SlicingCollection, SlicingGroup
|
||||||
|
from ....pdf_exporter import (
|
||||||
|
extract_unique_top_view_edges,
|
||||||
|
export_mesh_to_pdf,
|
||||||
|
format_pdf_filename_template,
|
||||||
|
get_first_object_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_MeshToPdf(ExecSliceNode):
|
||||||
|
bl_label = "Mesh to PDF"
|
||||||
|
button_label = "Generate PDF"
|
||||||
|
|
||||||
|
template: str = NfStringProperty(
|
||||||
|
name="Template",
|
||||||
|
default="{object_name}-{size_x_mm}x{size_y_mm}-{today_date_iso}.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename_template(self) -> str:
|
||||||
|
return getattr(self, "template", "{object_name}-{size_x_mm}x{size_y_mm}-{today_date_iso}.pdf")
|
||||||
|
|
||||||
|
@filename_template.setter
|
||||||
|
def filename_template(self, value: str) -> None:
|
||||||
|
self.template = value
|
||||||
|
|
||||||
|
draw_wireframe: bool = NfBoolProperty(name="Wireframe", default=True)
|
||||||
|
draw_boundary: bool = NfBoolProperty(name="Boundary (1 Face)", default=False)
|
||||||
|
draw_contour: bool = NfBoolProperty(name="Contour", default=False)
|
||||||
|
draw_crease: bool = NfBoolProperty(name="Crease Angle", default=False)
|
||||||
|
crease_angle: float = NfFloatProperty(name="Angle (°)", default=30.0, min=0.0, max=180.0)
|
||||||
|
draw_bbox: bool = NfBoolProperty(name="Bounding Box", default=False)
|
||||||
|
draw_sa2_holes: bool = NfBoolProperty(name="Holes (Ø)", default=False)
|
||||||
|
show_lineart: bool = NfBoolProperty(name="Lineart Visibility", default=False)
|
||||||
|
show_sa2: bool = NfBoolProperty(name="SA2 Settings", default=False)
|
||||||
|
draw_grid_xy: bool = NfBoolProperty(name="XY Grid (Top)", default=True)
|
||||||
|
draw_grid_xz: bool = NfBoolProperty(name="XZ Grid (Front)", default=False)
|
||||||
|
draw_grid_yz: bool = NfBoolProperty(name="YZ Grid (Side)", default=False)
|
||||||
|
show_grids: bool = NfBoolProperty(name="Grids", default=False)
|
||||||
|
|
||||||
|
draw_header: bool = NfBoolProperty(name="Technical Header", default=True)
|
||||||
|
show_header: bool = NfBoolProperty(name="Technical Drawing Header", default=False)
|
||||||
|
header_company: str = NfStringProperty(name="Company", default="")
|
||||||
|
header_part_name: str = NfStringProperty(name="Part Name", default="")
|
||||||
|
header_drawing_number: str = NfStringProperty(name="Drawing No.", default="")
|
||||||
|
header_author: str = NfStringProperty(name="Author", default="")
|
||||||
|
header_revision: str = NfStringProperty(name="Revision", default="A")
|
||||||
|
header_material: str = NfStringProperty(name="Material", default="")
|
||||||
|
header_tolerance: str = NfStringProperty(name="Tolerance", default="")
|
||||||
|
|
||||||
|
font_size: float = NfFloatProperty(name="Font Size (mm)", default=0.0, min=0.0, max=100.0)
|
||||||
|
margin: float = NfFloatProperty(name="Margin (mm)", default=10.0, min=0.0, max=500.0)
|
||||||
|
|
||||||
|
bg_color: tuple[float, float, float] = NfFloatVectorProperty(
|
||||||
|
name="Background",
|
||||||
|
subtype='COLOR',
|
||||||
|
default=(1.0, 1.0, 1.0),
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
size=3
|
||||||
|
)
|
||||||
|
|
||||||
|
wire_color: tuple[float, float, float] = NfFloatVectorProperty(
|
||||||
|
name="Wireframe",
|
||||||
|
subtype='COLOR',
|
||||||
|
default=(0.0, 0.0, 0.0),
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
size=3
|
||||||
|
)
|
||||||
|
|
||||||
|
bbox_color: tuple[float, float, float] = NfFloatVectorProperty(
|
||||||
|
name="BBox Color",
|
||||||
|
subtype='COLOR',
|
||||||
|
default=(0.0, 0.0, 1.0),
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
size=3
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self._init_exec()
|
||||||
|
self.add_input("SLICESOCK_DirPath", "Folder")
|
||||||
|
self.add_input("SLICESOCK_Settings", "Settings")
|
||||||
|
self.add_input("SLICESOCK_Mesh", "Geometry")
|
||||||
|
self.add_input("SLICESOCK_Paths", "Paths")
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
from .....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
|
for name in ("Geometry", "Paths"):
|
||||||
|
inp = self.inputs.get(name)
|
||||||
|
if inp:
|
||||||
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
super().draw_buttons(context, layout)
|
||||||
|
|
||||||
|
layout.prop(self, "template")
|
||||||
|
|
||||||
|
# Technical Drawing Header subpanel (collapsible, collapsed by default)
|
||||||
|
layout.prop(self, "show_header", text="Technical Drawing Header", icon='TRIA_DOWN' if self.show_header else 'TRIA_RIGHT')
|
||||||
|
if self.show_header:
|
||||||
|
box = layout.box()
|
||||||
|
box.prop(self, "draw_header")
|
||||||
|
box.prop(self, "header_company")
|
||||||
|
box.prop(self, "header_part_name")
|
||||||
|
box.prop(self, "header_drawing_number")
|
||||||
|
box.prop(self, "header_author")
|
||||||
|
box.prop(self, "header_revision")
|
||||||
|
box.prop(self, "header_material")
|
||||||
|
box.prop(self, "header_tolerance")
|
||||||
|
|
||||||
|
# Lineart Visibility subpanel (collapsible, collapsed by default)
|
||||||
|
layout.prop(self, "show_lineart", text="Lineart Visibility", icon='TRIA_DOWN' if self.show_lineart else 'TRIA_RIGHT')
|
||||||
|
if self.show_lineart:
|
||||||
|
box = layout.box()
|
||||||
|
box.prop(self, "draw_wireframe")
|
||||||
|
box.prop(self, "draw_boundary")
|
||||||
|
box.prop(self, "draw_contour")
|
||||||
|
box.prop(self, "draw_crease")
|
||||||
|
if self.draw_crease:
|
||||||
|
box.prop(self, "crease_angle")
|
||||||
|
|
||||||
|
# SA2 Settings subpanel (collapsible, collapsed by default)
|
||||||
|
layout.prop(self, "show_sa2", text="SA2 Settings", icon='TRIA_DOWN' if self.show_sa2 else 'TRIA_RIGHT')
|
||||||
|
if self.show_sa2:
|
||||||
|
box = layout.box()
|
||||||
|
box.prop(self, "draw_sa2_holes")
|
||||||
|
|
||||||
|
# Grid Display subpanel
|
||||||
|
layout.prop(self, "show_grids", text="Grids", icon='TRIA_DOWN' if self.show_grids else 'TRIA_RIGHT')
|
||||||
|
if self.show_grids:
|
||||||
|
box = layout.box()
|
||||||
|
box.prop(self, "draw_grid_xy")
|
||||||
|
box.prop(self, "draw_grid_xz")
|
||||||
|
box.prop(self, "draw_grid_yz")
|
||||||
|
|
||||||
|
# Display settings
|
||||||
|
layout.prop(self, "draw_bbox")
|
||||||
|
if self.draw_bbox:
|
||||||
|
layout.prop(self, "bbox_color")
|
||||||
|
|
||||||
|
# Styling settings
|
||||||
|
layout.prop(self, "font_size")
|
||||||
|
layout.prop(self, "margin")
|
||||||
|
layout.prop(self, "bg_color")
|
||||||
|
layout.prop(self, "wire_color")
|
||||||
|
|
||||||
|
def evaluate(self, operator=None):
|
||||||
|
geom = self.resolve("Geometry")
|
||||||
|
slice_paths = self.resolve("Paths")
|
||||||
|
folder: str = self.resolve("Folder") or ""
|
||||||
|
|
||||||
|
# Collect raw geometry items
|
||||||
|
raw_geom_items: list[Any] = []
|
||||||
|
if geom is not None:
|
||||||
|
raw_list = geom if isinstance(geom, list) else [geom]
|
||||||
|
raw_geom_items = [g for g in raw_list if g is not None]
|
||||||
|
|
||||||
|
# Collect slice path items
|
||||||
|
slice_paths_items: list[Any] = []
|
||||||
|
if slice_paths is not None:
|
||||||
|
is_sp_inst = isinstance(slice_paths, SlicePaths)
|
||||||
|
raw_list = slice_paths if (isinstance(slice_paths, list) and not is_sp_inst) else [slice_paths]
|
||||||
|
slice_paths_items = [p for p in raw_list if p is not None]
|
||||||
|
|
||||||
|
if not raw_geom_items and not slice_paths_items:
|
||||||
|
if operator: operator.report({'ERROR'}, "No Geometry or Paths connected")
|
||||||
|
return 'error'
|
||||||
|
|
||||||
|
if not folder:
|
||||||
|
if operator: operator.report({'ERROR'}, "No Folder provided")
|
||||||
|
return 'error'
|
||||||
|
|
||||||
|
abs_folder = folder
|
||||||
|
if hasattr(bpy, "path") and hasattr(bpy.path, "abspath"):
|
||||||
|
try:
|
||||||
|
res = bpy.path.abspath(f"//{folder}" if not folder.startswith(("/", "\\")) and ":" not in folder else folder)
|
||||||
|
if isinstance(res, str) and not type(res).__module__.startswith("unittest.mock"):
|
||||||
|
abs_folder = res
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
folder_path = Path(abs_folder)
|
||||||
|
|
||||||
|
has_raw_geom = len(raw_geom_items) > 0
|
||||||
|
primary_items = raw_geom_items if has_raw_geom else slice_paths_items
|
||||||
|
|
||||||
|
is_single_pdf_file = len(primary_items) == 1 and folder_path.suffix.lower() == ".pdf"
|
||||||
|
|
||||||
|
# Automatic scene scale determination
|
||||||
|
scale_str = "1:1"
|
||||||
|
try:
|
||||||
|
if hasattr(bpy, "context") and hasattr(bpy.context, "scene") and bpy.context.scene:
|
||||||
|
scale_len = getattr(bpy.context.scene.unit_settings, "scale_length", 1.0)
|
||||||
|
if scale_len == 1.0 or scale_len == 0.001:
|
||||||
|
scale_str = "1:1"
|
||||||
|
elif scale_len > 1.0:
|
||||||
|
scale_str = f"{scale_len:g}:1"
|
||||||
|
elif scale_len > 0.0:
|
||||||
|
scale_str = f"1:{1.0 / scale_len:g}"
|
||||||
|
except Exception:
|
||||||
|
scale_str = "1:1"
|
||||||
|
|
||||||
|
header_info = {
|
||||||
|
"company": self.header_company,
|
||||||
|
"part_name": self.header_part_name,
|
||||||
|
"drawing_number": self.header_drawing_number,
|
||||||
|
"author": self.header_author,
|
||||||
|
"revision": self.header_revision,
|
||||||
|
"material": self.header_material,
|
||||||
|
"tolerance": self.header_tolerance,
|
||||||
|
"date": datetime.date.today().isoformat(),
|
||||||
|
"scale": scale_str,
|
||||||
|
}
|
||||||
|
|
||||||
|
template = getattr(self, "template", None) or getattr(self, "filename_template", None) or "{object_name}-{size_x_mm}x{size_y_mm}-{today_date_iso}.pdf"
|
||||||
|
|
||||||
|
for i, target_item in enumerate(primary_items):
|
||||||
|
if has_raw_geom:
|
||||||
|
current_raw_geom = target_item
|
||||||
|
current_slice_paths = slice_paths
|
||||||
|
else:
|
||||||
|
current_raw_geom = None
|
||||||
|
current_slice_paths = target_item
|
||||||
|
|
||||||
|
current_header_info = dict(header_info)
|
||||||
|
part_name_prop = (self.header_part_name or "").strip()
|
||||||
|
if not part_name_prop:
|
||||||
|
default_part_name = (
|
||||||
|
get_first_object_name(target_item)
|
||||||
|
or get_first_object_name(current_raw_geom)
|
||||||
|
or get_first_object_name(current_slice_paths)
|
||||||
|
)
|
||||||
|
current_header_info["part_name"] = default_part_name
|
||||||
|
|
||||||
|
edges_2d = np.empty((0, 2, 2), dtype=np.float64)
|
||||||
|
if current_raw_geom is not None:
|
||||||
|
raw_e = extract_unique_top_view_edges(current_raw_geom)
|
||||||
|
if len(raw_e) > 0:
|
||||||
|
edges_2d = raw_e
|
||||||
|
if current_slice_paths is not None:
|
||||||
|
sp_e = extract_unique_top_view_edges(current_slice_paths)
|
||||||
|
if len(sp_e) > 0:
|
||||||
|
edges_2d = np.concatenate([edges_2d, sp_e], axis=0) if len(edges_2d) > 0 else sp_e
|
||||||
|
|
||||||
|
try:
|
||||||
|
exporter = export_mesh_to_pdf(
|
||||||
|
edges_2d=edges_2d,
|
||||||
|
filepath=None,
|
||||||
|
draw_wireframe=self.draw_wireframe,
|
||||||
|
draw_boundary=self.draw_boundary,
|
||||||
|
draw_contour=self.draw_contour,
|
||||||
|
draw_crease=self.draw_crease,
|
||||||
|
draw_bbox=self.draw_bbox,
|
||||||
|
draw_sa2_holes=self.draw_sa2_holes,
|
||||||
|
draw_header=self.draw_header,
|
||||||
|
header_info=current_header_info,
|
||||||
|
crease_angle=self.crease_angle,
|
||||||
|
margin_mm=self.margin,
|
||||||
|
font_size_mm=self.font_size * 2.834645669291339,
|
||||||
|
stroke_color=(float(self.wire_color[0]), float(self.wire_color[1]), float(self.wire_color[2])),
|
||||||
|
bg_color=(float(self.bg_color[0]), float(self.bg_color[1]), float(self.bg_color[2])),
|
||||||
|
bbox_color=(float(self.bbox_color[0]), float(self.bbox_color[1]), float(self.bbox_color[2])),
|
||||||
|
raw_geometry=current_raw_geom,
|
||||||
|
slice_paths=current_slice_paths,
|
||||||
|
draw_grid_xy=getattr(self, "draw_grid_xy", True),
|
||||||
|
draw_grid_xz=getattr(self, "draw_grid_xz", False),
|
||||||
|
draw_grid_yz=getattr(self, "draw_grid_yz", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_single_pdf_file:
|
||||||
|
os.makedirs(folder_path.parent, exist_ok=True)
|
||||||
|
filepath = str(folder_path)
|
||||||
|
else:
|
||||||
|
out_dir = folder_path if folder_path.suffix == "" else folder_path.parent
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
if isinstance(target_item, SlicingGroup) and target_item.collections:
|
||||||
|
raw_name = next(iter(target_item.collections.keys()))
|
||||||
|
elif hasattr(target_item, 'name') and getattr(target_item, 'name'):
|
||||||
|
raw_name = str(getattr(target_item, 'name'))
|
||||||
|
else:
|
||||||
|
raw_name = f"object_{i}"
|
||||||
|
|
||||||
|
object_name = raw_name.split('.')[0] if raw_name else f"object_{i}"
|
||||||
|
filename = format_pdf_filename_template(
|
||||||
|
template=template,
|
||||||
|
object_name=object_name,
|
||||||
|
size_x_mm=round(exporter.page_w_mm),
|
||||||
|
size_y_mm=round(exporter.page_h_mm),
|
||||||
|
today_date_iso=datetime.date.today().isoformat(),
|
||||||
|
target_item=target_item,
|
||||||
|
header_info=current_header_info,
|
||||||
|
)
|
||||||
|
filepath = str(out_dir / filename)
|
||||||
|
|
||||||
|
if len(edges_2d) == 0:
|
||||||
|
if operator: operator.report({'WARNING'}, f"Geometry for {os.path.basename(filepath)} has no valid 2D top-view wireframe edges")
|
||||||
|
|
||||||
|
exporter.export(filepath)
|
||||||
|
if operator: operator.report({'INFO'}, f"Mesh PDF saved to {filepath}")
|
||||||
|
except Exception as e:
|
||||||
|
if operator: operator.report({'ERROR'}, f"PDF Generation Error: {str(e)}")
|
||||||
|
return 'error'
|
||||||
|
|
||||||
|
return 'ok'
|
||||||
|
|
||||||
|
|
||||||
|
# Alias for backward compatibility
|
||||||
|
SLICENODE_WireframeToPdf = SLICENODE_MeshToPdf
|
||||||
81
core/nodeslicer/nodes/output/start_print.py
Normal file
81
core/nodeslicer/nodes/output/start_print.py
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import ExecSliceNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_StartPrint(ExecSliceNode):
|
||||||
|
bl_label = "Start Print"
|
||||||
|
button_label = "Send to Printer"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self._init_exec()
|
||||||
|
self.add_input("NodeSocketString", "Gcode")
|
||||||
|
self.add_input("SLICESOCK_Printer")
|
||||||
|
|
||||||
|
def evaluate(self, operator=None):
|
||||||
|
from .....infra.blender.list_utils import is_list_data
|
||||||
|
|
||||||
|
raw_gcode = self.resolve("Gcode")
|
||||||
|
if is_list_data(raw_gcode):
|
||||||
|
if operator: operator.report({'ERROR'}, "Start Print does not accept a list of Gcode")
|
||||||
|
return 'error'
|
||||||
|
|
||||||
|
from .....infra.networking.printer_backends import (
|
||||||
|
PrinterConfig,
|
||||||
|
PrusaLinkBackend,
|
||||||
|
CrealityBackend,
|
||||||
|
MoonrakerBackend
|
||||||
|
)
|
||||||
|
|
||||||
|
gcode: str = str(raw_gcode or "")
|
||||||
|
config: PrinterConfig | None = self.resolve("Printer")
|
||||||
|
|
||||||
|
if not gcode:
|
||||||
|
if operator: operator.report({'ERROR'}, "No Gcode to send")
|
||||||
|
return 'no gcode'
|
||||||
|
if not config:
|
||||||
|
if operator: operator.report({'ERROR'}, "No Printer configuration")
|
||||||
|
return 'no printer'
|
||||||
|
|
||||||
|
# Instantiate correct backend
|
||||||
|
backend = None
|
||||||
|
if config.host_type == 'PRUSALINK':
|
||||||
|
backend = PrusaLinkBackend(config.host, config.port, config.prefix, api_key=config.password)
|
||||||
|
elif config.host_type == 'CREALITY':
|
||||||
|
backend = CrealityBackend(config.host, config.port, config.prefix)
|
||||||
|
elif config.host_type == 'MOONRAKER':
|
||||||
|
backend = MoonrakerBackend(config.host, config.port, config.prefix)
|
||||||
|
|
||||||
|
if not backend:
|
||||||
|
if operator: operator.report({'ERROR'}, f"Unsupported host type: {config.host_type}")
|
||||||
|
return 'unsupported'
|
||||||
|
|
||||||
|
from .....infra.networking.gcode_utils import prepare_gcode_file
|
||||||
|
|
||||||
|
# Write to temp file to send
|
||||||
|
name_part = f"NodeSlicer_{config.label}".replace(" ", "_")
|
||||||
|
try:
|
||||||
|
file_bytes, filename = prepare_gcode_file(gcode, name_part)
|
||||||
|
except Exception as e:
|
||||||
|
if operator: operator.report({'ERROR'}, f"Failed to prepare G-code: {str(e)}")
|
||||||
|
return 'error'
|
||||||
|
|
||||||
|
suffix = Path(filename).suffix
|
||||||
|
with tempfile.NamedTemporaryFile(mode='wb', suffix=suffix, delete=False) as tf:
|
||||||
|
tf.write(file_bytes)
|
||||||
|
temp_path = Path(tf.name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
backend.start_print(temp_path, filename)
|
||||||
|
if operator: operator.report({'INFO'}, f"Print started on {config.label}")
|
||||||
|
except Exception as e:
|
||||||
|
if operator: operator.report({'ERROR'}, f"Failed to start print: {str(e)}")
|
||||||
|
return 'error'
|
||||||
|
finally:
|
||||||
|
if temp_path.exists():
|
||||||
|
temp_path.unlink()
|
||||||
|
|
||||||
|
return 'ok'
|
||||||
|
|
@ -17,6 +17,7 @@ class SLICENODE_PathsToList(SliceNodeListJoiner):
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_FlattenPathList(SliceNode):
|
class SLICENODE_FlattenPathList(SliceNode):
|
||||||
bl_label = "Flatten Path List"
|
bl_label = "Flatten Path List"
|
||||||
|
implicit_multivalue = False
|
||||||
always_list_inputs = ["Paths"]
|
always_list_inputs = ["Paths"]
|
||||||
|
|
||||||
mode: str = NfEnumProperty(
|
mode: str = NfEnumProperty(
|
||||||
|
|
@ -80,26 +81,22 @@ class SLICENODE_GetPaths(SliceNode):
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
from ....infra.blender.blender_mesh_capture import capture_mesh_edges
|
from ....infra.blender.blender_mesh_capture import capture_mesh_edges
|
||||||
|
|
||||||
def _get_single_path(o: Any, e_name: Any, f_name: Any, extra: Any) -> SlicePaths:
|
o = self.resolve("Object")
|
||||||
if not o:
|
if not o:
|
||||||
return SlicePaths()
|
return SlicePaths()
|
||||||
so, e_rates, f_rates, extra_attrs_data = capture_mesh_edges(o, str(e_name or ""), str(f_name or ""), extra)
|
e_name = self.resolve("Extrusion Multiplier Attribute")
|
||||||
sp = SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
|
f_name = self.resolve("Feedrate Attribute")
|
||||||
if hasattr(o, "transforms"):
|
extra = self.resolve("Extra Attributes")
|
||||||
sp.transforms = list(getattr(o, "transforms", []))
|
|
||||||
if hasattr(o, "name") and getattr(o, "name"):
|
|
||||||
name_val = str(getattr(o, "name"))
|
|
||||||
sp.name = name_val
|
|
||||||
sp.objects_info = {"objects": [{"name": name_val, "polygon": []}]}
|
|
||||||
return sp
|
|
||||||
|
|
||||||
return self.eval_multivalue(
|
so, e_rates, f_rates, extra_attrs_data = capture_mesh_edges(o, str(e_name or ""), str(f_name or ""), extra)
|
||||||
_get_single_path,
|
sp = SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
|
||||||
main_socket="Object",
|
if hasattr(o, "transforms"):
|
||||||
secondary_sockets=["Extrusion Multiplier Attribute", "Feedrate Attribute", "Extra Attributes"],
|
sp.transforms = list(getattr(o, "transforms", []))
|
||||||
socket_name=socket_name,
|
if hasattr(o, "name") and getattr(o, "name"):
|
||||||
default=SlicePaths(),
|
name_val = str(getattr(o, "name"))
|
||||||
)
|
sp.name = name_val
|
||||||
|
sp.objects_info = {"objects": [{"name": name_val, "polygon": []}]}
|
||||||
|
return sp
|
||||||
|
|
||||||
def _slice_single_mesh_item(coll: Any, layer_height: float, fl_height: float | None, rotation: Any, ignore_holes: bool) -> SlicePaths:
|
def _slice_single_mesh_item(coll: Any, layer_height: float, fl_height: float | None, rotation: Any, ignore_holes: bool) -> SlicePaths:
|
||||||
if not coll:
|
if not coll:
|
||||||
|
|
@ -290,32 +287,28 @@ class SLICENODE_Layers(SliceNode):
|
||||||
self.add_input("NodeSocketFloat", "First Layer Height").value = 0.2
|
self.add_input("NodeSocketFloat", "First Layer Height").value = 0.2
|
||||||
self.add_input("NodeSocketFloat", "Layer Height").value = 0.2
|
self.add_input("NodeSocketFloat", "Layer Height").value = 0.2
|
||||||
self.add_input("NodeSocketFloat", "Total Height").value = 250.0
|
self.add_input("NodeSocketFloat", "Total Height").value = 250.0
|
||||||
self.add_output("NodeSocketFloat", "Layers", display_shape='LIST')
|
self.add_output("NodeSocketFloat", "Layers")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
def _calc_layers(lh_val: Any, flh_val: Any, tot_val: Any) -> list[float]:
|
lh_val = self.resolve("Layer Height")
|
||||||
flh = float(flh_val) if flh_val is not None and float(flh_val) > 0 else 0.2
|
flh_val = self.resolve("First Layer Height")
|
||||||
lh = float(lh_val) if lh_val is not None and float(lh_val) > 0 else 0.2
|
tot_val = self.resolve("Total Height")
|
||||||
tot = float(tot_val) if tot_val is not None and float(tot_val) > 0 else 250.0
|
|
||||||
|
|
||||||
flh_um = max(1, int(round(flh * 1000)))
|
flh = float(flh_val) if flh_val is not None and float(flh_val) > 0 else 0.2
|
||||||
lh_um = max(1, int(round(lh * 1000)))
|
lh = float(lh_val) if lh_val is not None and float(lh_val) > 0 else 0.2
|
||||||
tot_um = int(round(tot * 1000))
|
tot = float(tot_val) if tot_val is not None and float(tot_val) > 0 else 250.0
|
||||||
|
|
||||||
layers: list[float] = []
|
flh_um = max(1, int(round(flh * 1000)))
|
||||||
z_um = flh_um
|
lh_um = max(1, int(round(lh * 1000)))
|
||||||
while z_um <= tot_um:
|
tot_um = int(round(tot * 1000))
|
||||||
layers.append(z_um / 1000.0)
|
|
||||||
z_um += lh_um
|
layers: list[float] = []
|
||||||
return layers
|
z_um = flh_um
|
||||||
|
while z_um <= tot_um:
|
||||||
|
layers.append(z_um / 1000.0)
|
||||||
|
z_um += lh_um
|
||||||
|
return layers
|
||||||
|
|
||||||
return self.eval_multivalue(
|
|
||||||
_calc_layers,
|
|
||||||
main_socket="Layer Height",
|
|
||||||
secondary_sockets=["First Layer Height", "Total Height"],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default=[],
|
|
||||||
)
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_SliceShells(SliceNode):
|
class SLICENODE_SliceShells(SliceNode):
|
||||||
|
|
@ -341,6 +334,11 @@ class SLICENODE_SliceShells(SliceNode):
|
||||||
layout.prop(self, "ignore_holes")
|
layout.prop(self, "ignore_holes")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
item = self.resolve("Mesh")
|
||||||
|
lh_val = self.resolve("Layer Height")
|
||||||
|
flh_val = self.resolve("First Layer Height")
|
||||||
|
rot_val = self.resolve("Rotation")
|
||||||
|
|
||||||
ignore_holes: bool = bool(getattr(self, "ignore_holes", False))
|
ignore_holes: bool = bool(getattr(self, "ignore_holes", False))
|
||||||
if hasattr(self, "inputs") and "Ignore Holes" in self.inputs:
|
if hasattr(self, "inputs") and "Ignore Holes" in self.inputs:
|
||||||
try:
|
try:
|
||||||
|
|
@ -350,19 +348,10 @@ class SLICENODE_SliceShells(SliceNode):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _slice_one(item: Any, lh_val: Any, flh_val: Any, rot_val: Any) -> SlicePaths:
|
lh = float(lh_val) if lh_val is not None and float(lh_val) > 0 else 0.2
|
||||||
lh = float(lh_val) if lh_val is not None and float(lh_val) > 0 else 0.2
|
flh = float(flh_val) if flh_val is not None and float(flh_val) > 0 else None
|
||||||
flh = float(flh_val) if flh_val is not None and float(flh_val) > 0 else None
|
return _slice_single_mesh_item(item, lh, flh, rot_val, ignore_holes)
|
||||||
return _slice_single_mesh_item(item, lh, flh, rot_val, ignore_holes)
|
|
||||||
|
|
||||||
return self.eval_multivalue(
|
|
||||||
_slice_one,
|
|
||||||
main_socket="Mesh",
|
|
||||||
secondary_sockets=["Layer Height", "First Layer Height", "Rotation"],
|
|
||||||
is_vector=[False, False, True],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default=SlicePaths(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_SliceShellsLayers(SliceNode):
|
class SLICENODE_SliceShellsLayers(SliceNode):
|
||||||
|
|
@ -378,7 +367,7 @@ class SLICENODE_SliceShellsLayers(SliceNode):
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Mesh")
|
self.add_input("SLICESOCK_Mesh")
|
||||||
self.add_input("NodeSocketFloat", "Layers", display_shape='LIST')
|
self.add_input("NodeSocketFloat", "Layers")
|
||||||
self.add_input("NodeSocketRotation", "Rotation")
|
self.add_input("NodeSocketRotation", "Rotation")
|
||||||
self.add_output("SLICESOCK_Paths")
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
|
|
@ -388,6 +377,10 @@ class SLICENODE_SliceShellsLayers(SliceNode):
|
||||||
layout.prop(self, "ignore_holes")
|
layout.prop(self, "ignore_holes")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
item = self.resolve("Mesh")
|
||||||
|
layers_val = self.resolve("Layers")
|
||||||
|
rot_val = self.resolve("Rotation")
|
||||||
|
|
||||||
ignore_holes: bool = bool(getattr(self, "ignore_holes", False))
|
ignore_holes: bool = bool(getattr(self, "ignore_holes", False))
|
||||||
if hasattr(self, "inputs") and "Ignore Holes" in self.inputs:
|
if hasattr(self, "inputs") and "Ignore Holes" in self.inputs:
|
||||||
try:
|
try:
|
||||||
|
|
@ -397,18 +390,8 @@ class SLICENODE_SliceShellsLayers(SliceNode):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _slice_one(item: Any, layers_val: Any, rot_val: Any) -> SlicePaths:
|
return _slice_single_mesh_item_with_layers(item, layers_val, rot_val, ignore_holes)
|
||||||
return _slice_single_mesh_item_with_layers(item, layers_val, rot_val, ignore_holes)
|
|
||||||
|
|
||||||
return self.eval_multivalue(
|
|
||||||
_slice_one,
|
|
||||||
main_socket="Mesh",
|
|
||||||
secondary_sockets=["Layers", "Rotation"],
|
|
||||||
is_vector=[False, True],
|
|
||||||
is_atomic=[True, False],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default=SlicePaths(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_BufferShells(SliceNode):
|
class SLICENODE_BufferShells(SliceNode):
|
||||||
|
|
@ -435,18 +418,12 @@ class SLICENODE_BufferShells(SliceNode):
|
||||||
layout.prop(self, "shape_option")
|
layout.prop(self, "shape_option")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
def _buf(p: Any, dist: Any, rot: Any) -> SlicePaths:
|
p = self.resolve("Paths")
|
||||||
if not p: return SlicePaths()
|
if not p:
|
||||||
return p.buffer(float(dist if dist is not None else 0.0), miter=self.shape_option, rotation=rot)
|
return SlicePaths()
|
||||||
|
dist = self.resolve("Distance")
|
||||||
return self.eval_multivalue(
|
rot = self.resolve("Rotation")
|
||||||
_buf,
|
return p.buffer(float(dist if dist is not None else 0.0), miter=self.shape_option, rotation=rot)
|
||||||
main_socket="Paths",
|
|
||||||
secondary_sockets=["Distance", "Rotation"],
|
|
||||||
is_vector=[False, True],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default=SlicePaths(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_BufferCurve(SliceNode):
|
class SLICENODE_BufferCurve(SliceNode):
|
||||||
|
|
@ -492,23 +469,17 @@ class SLICENODE_BufferCurve(SliceNode):
|
||||||
layout.prop(self, "single_sided")
|
layout.prop(self, "single_sided")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
def _buf(p: Any, dist: Any, rot: Any) -> SlicePaths:
|
p = self.resolve("Paths")
|
||||||
if not p: return SlicePaths()
|
if not p:
|
||||||
return p.buffer_curve(
|
return SlicePaths()
|
||||||
float(dist if dist is not None else 0.0),
|
dist = self.resolve("Distance")
|
||||||
cap_style=self.cap_style,
|
rot = self.resolve("Rotation")
|
||||||
join_style=self.join_style,
|
return p.buffer_curve(
|
||||||
single_sided=self.single_sided,
|
float(dist if dist is not None else 0.0),
|
||||||
rotation=rot
|
cap_style=self.cap_style,
|
||||||
)
|
join_style=self.join_style,
|
||||||
|
single_sided=self.single_sided,
|
||||||
return self.eval_multivalue(
|
rotation=rot
|
||||||
_buf,
|
|
||||||
main_socket="Paths",
|
|
||||||
secondary_sockets=["Distance", "Rotation"],
|
|
||||||
is_vector=[False, True],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default=SlicePaths(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
@ -537,21 +508,15 @@ class SLICENODE_OffsetCurve(SliceNode):
|
||||||
layout.prop(self, "join_style")
|
layout.prop(self, "join_style")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
def _off(p: Any, dist: Any, rot: Any) -> SlicePaths:
|
p = self.resolve("Paths")
|
||||||
if not p: return SlicePaths()
|
if not p:
|
||||||
return p.offset_curve(
|
return SlicePaths()
|
||||||
float(dist if dist is not None else 0.0),
|
dist = self.resolve("Distance")
|
||||||
join_style=cast(Literal['round', 'mitre'], self.join_style),
|
rot = self.resolve("Rotation")
|
||||||
rotation=rot
|
return p.offset_curve(
|
||||||
)
|
float(dist if dist is not None else 0.0),
|
||||||
|
join_style=cast(Literal['round', 'mitre'], self.join_style),
|
||||||
return self.eval_multivalue(
|
rotation=rot
|
||||||
_off,
|
|
||||||
main_socket="Paths",
|
|
||||||
secondary_sockets=["Distance", "Rotation"],
|
|
||||||
is_vector=[False, True],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default=SlicePaths(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
@ -565,18 +530,11 @@ class SLICENODE_PathOffset(SliceNode):
|
||||||
self.add_output("SLICESOCK_Paths", "Paths")
|
self.add_output("SLICESOCK_Paths", "Paths")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
def _off(p: Any, off: Any) -> SlicePaths:
|
p = self.resolve("Paths")
|
||||||
if not p: return SlicePaths()
|
if not p:
|
||||||
return p.offset(np.array(off if off is not None else (0.0, 0.0, 0.0), dtype=np.float64))
|
return SlicePaths()
|
||||||
|
off = self.resolve("Offset")
|
||||||
return self.eval_multivalue(
|
return p.offset(np.array(off if off is not None else (0.0, 0.0, 0.0), dtype=np.float64))
|
||||||
_off,
|
|
||||||
main_socket="Paths",
|
|
||||||
secondary_sockets=["Offset"],
|
|
||||||
is_vector=[True],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default=SlicePaths(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_RotatePath(SliceNode):
|
class SLICENODE_RotatePath(SliceNode):
|
||||||
|
|
@ -590,19 +548,13 @@ class SLICENODE_RotatePath(SliceNode):
|
||||||
self.add_output("SLICESOCK_Paths", "Paths")
|
self.add_output("SLICESOCK_Paths", "Paths")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
def _rot(p: Any, ang: Any, cent: Any) -> SlicePaths:
|
p = self.resolve("Paths")
|
||||||
if not p: return SlicePaths()
|
if not p:
|
||||||
center_vec = np.array(cent, dtype=np.float64) if cent and any(cent) else None
|
return SlicePaths()
|
||||||
return p.rotate(float(ang if ang is not None else 0.0), center=center_vec)
|
ang = self.resolve("Angle")
|
||||||
|
cent = self.resolve("Center")
|
||||||
return self.eval_multivalue(
|
center_vec = np.array(cent, dtype=np.float64) if cent and any(cent) else None
|
||||||
_rot,
|
return p.rotate(float(ang if ang is not None else 0.0), center=center_vec)
|
||||||
main_socket="Paths",
|
|
||||||
secondary_sockets=["Angle", "Center"],
|
|
||||||
is_vector=[False, True],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default=SlicePaths(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_BufferShellsMultiple(SliceNode):
|
class SLICENODE_BufferShellsMultiple(SliceNode):
|
||||||
|
|
@ -633,30 +585,24 @@ class SLICENODE_BufferShellsMultiple(SliceNode):
|
||||||
layout.prop(self, "shape_option")
|
layout.prop(self, "shape_option")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
def _buf_multi(p: Any, b_dist: Any, w_step: Any, cnt: Any, rot: Any) -> SlicePaths:
|
p = self.resolve("Paths")
|
||||||
c = int(cnt if cnt is not None else 1)
|
cnt = self.resolve("Count")
|
||||||
if not p or c <= 0:
|
c = int(cnt if cnt is not None else 1)
|
||||||
return SlicePaths()
|
if not p or c <= 0:
|
||||||
dist = float(b_dist if b_dist is not None else 0.0)
|
|
||||||
step = float(w_step if w_step is not None else 0.4)
|
|
||||||
if socket_name == "Outermost Path":
|
|
||||||
outer_dist = max(dist, dist + (c - 1) * step) if c > 1 else dist
|
|
||||||
return p.buffer(outer_dist, miter=self.shape_option, rotation=rot)
|
|
||||||
elif socket_name == "Innermost Path":
|
|
||||||
inner_dist = min(dist, dist + (c - 1) * step) if c > 1 else dist
|
|
||||||
return p.buffer(inner_dist, miter=self.shape_option, rotation=rot)
|
|
||||||
elif socket_name == "Paths":
|
|
||||||
return p.buffer_multiple(dist, c, step, miter=self.shape_option, rotation=rot)
|
|
||||||
return SlicePaths()
|
return SlicePaths()
|
||||||
|
dist = float(self.resolve("Distance") or 0.0)
|
||||||
|
step = float(self.resolve("Width Step") or 0.4)
|
||||||
|
rot = self.resolve("Rotation")
|
||||||
|
|
||||||
return self.eval_multivalue(
|
if socket_name == "Outermost Path":
|
||||||
_buf_multi,
|
outer_dist = max(dist, dist + (c - 1) * step) if c > 1 else dist
|
||||||
main_socket="Paths",
|
return p.buffer(outer_dist, miter=self.shape_option, rotation=rot)
|
||||||
secondary_sockets=["Distance", "Width Step", "Count", "Rotation"],
|
elif socket_name == "Innermost Path":
|
||||||
is_vector=[False, False, False, True],
|
inner_dist = min(dist, dist + (c - 1) * step) if c > 1 else dist
|
||||||
socket_name=socket_name,
|
return p.buffer(inner_dist, miter=self.shape_option, rotation=rot)
|
||||||
default=SlicePaths(),
|
elif socket_name == "Paths":
|
||||||
)
|
return p.buffer_multiple(dist, c, step, miter=self.shape_option, rotation=rot)
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_Infill(SliceNode):
|
class SLICENODE_Infill(SliceNode):
|
||||||
|
|
@ -686,22 +632,17 @@ class SLICENODE_Infill(SliceNode):
|
||||||
layout.prop(self, "infill_type", text="")
|
layout.prop(self, "infill_type", text="")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
def _inf(p: Any, dens: Any, inf_rot: Any, rot: Any) -> SlicePaths:
|
p = self.resolve("Paths")
|
||||||
if not p: return SlicePaths()
|
if not p:
|
||||||
return p.infill(
|
return SlicePaths()
|
||||||
float(dens if dens is not None else 2.0),
|
dens = self.resolve("Density")
|
||||||
rotation_deg=float(inf_rot if inf_rot is not None else 0.0),
|
inf_rot = self.resolve("Infill Rotation")
|
||||||
infill_type=self.infill_type,
|
rot = self.resolve("Layer Rotation")
|
||||||
layer_rotation=rot
|
return p.infill(
|
||||||
)
|
float(dens if dens is not None else 2.0),
|
||||||
|
rotation_deg=float(inf_rot if inf_rot is not None else 0.0),
|
||||||
return self.eval_multivalue(
|
infill_type=self.infill_type,
|
||||||
_inf,
|
layer_rotation=rot
|
||||||
main_socket="Paths",
|
|
||||||
secondary_sockets=["Density", "Infill Rotation", "Layer Rotation"],
|
|
||||||
is_vector=[False, False, True],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default=SlicePaths(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
@ -718,17 +659,11 @@ class SLICENODE_OverhangMask(SliceNode):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
def _mask(p: Any, tol: Any) -> SlicePaths:
|
p = self.resolve("Paths")
|
||||||
if not p: return SlicePaths()
|
if not p:
|
||||||
return p.get_overhang_mask(tolerance=float(tol if tol is not None else 0.05))
|
return SlicePaths()
|
||||||
|
tol = self.resolve("Tolerance")
|
||||||
return self.eval_multivalue(
|
return p.get_overhang_mask(tolerance=float(tol if tol is not None else 0.05))
|
||||||
_mask,
|
|
||||||
main_socket="Paths",
|
|
||||||
secondary_sockets=["Tolerance"],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default=SlicePaths(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_PathOperations(SliceNodeJoiner):
|
class SLICENODE_PathOperations(SliceNodeJoiner):
|
||||||
|
|
@ -867,19 +802,12 @@ class SLICENODE_SimplifyPaths(SliceNode):
|
||||||
layout.prop(self, "preserve_topology")
|
layout.prop(self, "preserve_topology")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
def _simp(p: Any, tol: Any) -> SlicePaths:
|
p = self.resolve("Paths")
|
||||||
t_val = float(tol if tol is not None else 0.0)
|
tol = self.resolve("Tolerance")
|
||||||
if not p or t_val <= 0:
|
t_val = float(tol if tol is not None else 0.0)
|
||||||
return p if isinstance(p, SlicePaths) else SlicePaths()
|
if not p or t_val <= 0:
|
||||||
return p.simplify(t_val, preserve_topology=self.preserve_topology)
|
return p if isinstance(p, SlicePaths) else SlicePaths()
|
||||||
|
return p.simplify(t_val, preserve_topology=self.preserve_topology)
|
||||||
return self.eval_multivalue(
|
|
||||||
_simp,
|
|
||||||
main_socket="Paths",
|
|
||||||
secondary_sockets=["Tolerance"],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default=SlicePaths(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
@ -917,23 +845,16 @@ class SLICENODE_SeamPlacement(SliceNode):
|
||||||
layout.prop(self, "acquire_transforms")
|
layout.prop(self, "acquire_transforms")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
p = self.resolve("Paths")
|
||||||
|
if not p:
|
||||||
|
return SlicePaths()
|
||||||
|
m_in = self.resolve("Mesh")
|
||||||
|
stg = self.resolve("Seam Staggering")
|
||||||
|
s_val = float(stg if stg is not None else 0.0)
|
||||||
|
if not m_in and abs(s_val) < 1e-6:
|
||||||
|
return p
|
||||||
acquire_t = bool(getattr(self, "acquire_transforms", True))
|
acquire_t = bool(getattr(self, "acquire_transforms", True))
|
||||||
|
return p.place_seams(m_in, mode=self.mode.lower(), acquire_transforms=acquire_t, stagger_mm=s_val)
|
||||||
def _place(p: Any, m_in: Any, stg: Any) -> SlicePaths:
|
|
||||||
if not p:
|
|
||||||
return SlicePaths()
|
|
||||||
s_val = float(stg if stg is not None else 0.0)
|
|
||||||
if not m_in and abs(s_val) < 1e-6:
|
|
||||||
return p
|
|
||||||
return p.place_seams(m_in, mode=self.mode.lower(), acquire_transforms=acquire_t, stagger_mm=s_val)
|
|
||||||
|
|
||||||
return self.eval_multivalue(
|
|
||||||
_place,
|
|
||||||
main_socket="Paths",
|
|
||||||
secondary_sockets=["Mesh", "Seam Staggering"],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default=SlicePaths(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
@ -945,7 +866,7 @@ class SLICENODE_AnchorPaths(SliceNode):
|
||||||
name="Reverse",
|
name="Reverse",
|
||||||
description="Follow anchor paths in the opposite direction",
|
description="Follow anchor paths in the opposite direction",
|
||||||
default=False,
|
default=False,
|
||||||
update=notify_tree_changed,
|
update=notify_tree_changed
|
||||||
)
|
)
|
||||||
|
|
||||||
distance: float = NfFloatProperty(
|
distance: float = NfFloatProperty(
|
||||||
|
|
@ -953,7 +874,7 @@ class SLICENODE_AnchorPaths(SliceNode):
|
||||||
description="Distance to follow along anchor paths",
|
description="Distance to follow along anchor paths",
|
||||||
default=2.0,
|
default=2.0,
|
||||||
min=0.0,
|
min=0.0,
|
||||||
update=notify_tree_changed,
|
update=notify_tree_changed
|
||||||
)
|
)
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
|
|
@ -968,30 +889,24 @@ class SLICENODE_AnchorPaths(SliceNode):
|
||||||
layout.prop(self, "reverse")
|
layout.prop(self, "reverse")
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
paths = self.resolve("Paths")
|
||||||
|
if not paths:
|
||||||
|
return SlicePaths()
|
||||||
|
if not isinstance(paths, SlicePaths) and not getattr(paths, "is_slice_paths", False):
|
||||||
|
return SlicePaths()
|
||||||
|
anchor_paths = self.resolve("Anchor Paths")
|
||||||
|
if not anchor_paths:
|
||||||
|
return paths
|
||||||
|
dist_val = self.resolve("Distance")
|
||||||
|
rev_val = self.resolve("Reverse")
|
||||||
rev_prop = bool(getattr(self, "reverse", False))
|
rev_prop = bool(getattr(self, "reverse", False))
|
||||||
|
d = float(dist_val if dist_val is not None else getattr(self, "distance", 2.0))
|
||||||
def _anchor_one(paths: Any, anchor_paths: Any, dist_val: Any, rev_val: Any) -> SlicePaths:
|
is_rev = bool(rev_val) if rev_val is not None else rev_prop
|
||||||
if not paths:
|
if hasattr(self, "inputs"):
|
||||||
return SlicePaths()
|
rev_sock = self.inputs.get("Reverse")
|
||||||
if not isinstance(paths, SlicePaths) and not getattr(paths, "is_slice_paths", False):
|
if rev_sock is not None and not getattr(rev_sock, "is_linked", False):
|
||||||
return SlicePaths()
|
is_rev = bool(getattr(rev_sock, "value", rev_prop)) or rev_prop
|
||||||
if not anchor_paths:
|
return paths.anchor(anchor_paths, distance=d, reverse=is_rev)
|
||||||
return paths
|
|
||||||
d = float(dist_val if dist_val is not None else getattr(self, "distance", 2.0))
|
|
||||||
is_rev = bool(rev_val) if rev_val is not None else rev_prop
|
|
||||||
if hasattr(self, "inputs"):
|
|
||||||
rev_sock = self.inputs.get("Reverse")
|
|
||||||
if rev_sock is not None and not getattr(rev_sock, "is_linked", False):
|
|
||||||
is_rev = bool(getattr(rev_sock, "value", rev_prop)) or rev_prop
|
|
||||||
return paths.anchor(anchor_paths, distance=d, reverse=is_rev)
|
|
||||||
|
|
||||||
return self.eval_multivalue(
|
|
||||||
_anchor_one,
|
|
||||||
main_socket="Paths",
|
|
||||||
secondary_sockets=["Anchor Paths", "Distance", "Reverse"],
|
|
||||||
socket_name=socket_name,
|
|
||||||
default=SlicePaths(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
62
core/nodeslicer/nodes/path/__init__.py
Normal file
62
core/nodeslicer/nodes/path/__init__.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# Re-export slice functions for compatibility (including test patching)
|
||||||
|
from ....slice import slice_at_range, align_path_seam_sharpest, is_closed_path
|
||||||
|
from ....functions.path_functions import (
|
||||||
|
_slice_single_mesh_item,
|
||||||
|
_finish_slice_paths,
|
||||||
|
_slice_single_mesh_item_with_layers,
|
||||||
|
slice_single_mesh_item,
|
||||||
|
finish_slice_paths,
|
||||||
|
slice_single_mesh_item_with_layers,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Nodes
|
||||||
|
from .paths_to_list import SLICENODE_PathsToList
|
||||||
|
from .flatten_path_list import SLICENODE_FlattenPathList
|
||||||
|
from .get_paths import SLICENODE_GetPaths
|
||||||
|
from .layers import SLICENODE_Layers
|
||||||
|
from .slice_shells import SLICENODE_SliceShells
|
||||||
|
from .slice_shells_layers import SLICENODE_SliceShellsLayers
|
||||||
|
from .buffer_shells import SLICENODE_BufferShells
|
||||||
|
from .buffer_curve import SLICENODE_BufferCurve
|
||||||
|
from .offset_curve import SLICENODE_OffsetCurve
|
||||||
|
from .path_offset import SLICENODE_PathOffset
|
||||||
|
from .rotate_path import SLICENODE_RotatePath
|
||||||
|
from .buffer_shells_multiple import SLICENODE_BufferShellsMultiple
|
||||||
|
from .infill import SLICENODE_Infill
|
||||||
|
from .overhang_mask import SLICENODE_OverhangMask
|
||||||
|
from .path_operations import SLICENODE_PathOperations
|
||||||
|
from .simplify_paths import SLICENODE_SimplifyPaths
|
||||||
|
from .seam_placement import SLICENODE_SeamPlacement
|
||||||
|
from .anchor_paths import SLICENODE_AnchorPaths
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"slice_at_range",
|
||||||
|
"align_path_seam_sharpest",
|
||||||
|
"is_closed_path",
|
||||||
|
"_slice_single_mesh_item",
|
||||||
|
"_finish_slice_paths",
|
||||||
|
"_slice_single_mesh_item_with_layers",
|
||||||
|
"slice_single_mesh_item",
|
||||||
|
"finish_slice_paths",
|
||||||
|
"slice_single_mesh_item_with_layers",
|
||||||
|
"SLICENODE_PathsToList",
|
||||||
|
"SLICENODE_FlattenPathList",
|
||||||
|
"SLICENODE_GetPaths",
|
||||||
|
"SLICENODE_Layers",
|
||||||
|
"SLICENODE_SliceShells",
|
||||||
|
"SLICENODE_SliceShellsLayers",
|
||||||
|
"SLICENODE_BufferShells",
|
||||||
|
"SLICENODE_BufferCurve",
|
||||||
|
"SLICENODE_OffsetCurve",
|
||||||
|
"SLICENODE_PathOffset",
|
||||||
|
"SLICENODE_RotatePath",
|
||||||
|
"SLICENODE_BufferShellsMultiple",
|
||||||
|
"SLICENODE_Infill",
|
||||||
|
"SLICENODE_OverhangMask",
|
||||||
|
"SLICENODE_PathOperations",
|
||||||
|
"SLICENODE_SimplifyPaths",
|
||||||
|
"SLICENODE_SeamPlacement",
|
||||||
|
"SLICENODE_AnchorPaths",
|
||||||
|
]
|
||||||
62
core/nodeslicer/nodes/path/anchor_paths.py
Normal file
62
core/nodeslicer/nodes/path/anchor_paths.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
from typing import Any
|
||||||
|
from ...bpy_ns_types import NfBoolProperty, NfFloatProperty, notify_tree_changed
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_AnchorPaths(SliceNode):
|
||||||
|
bl_label = "Anchor Paths"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
|
reverse: bool = NfBoolProperty(
|
||||||
|
name="Reverse",
|
||||||
|
description="Follow anchor paths in the opposite direction",
|
||||||
|
default=False,
|
||||||
|
update=notify_tree_changed,
|
||||||
|
)
|
||||||
|
|
||||||
|
distance: float = NfFloatProperty(
|
||||||
|
name="Distance",
|
||||||
|
description="Distance to follow along anchor paths",
|
||||||
|
default=2.0,
|
||||||
|
min=0.0,
|
||||||
|
update=notify_tree_changed,
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths", "Paths")
|
||||||
|
self.add_input("SLICESOCK_Paths", "Anchor Paths")
|
||||||
|
self.add_input("NodeSocketFloat", "Distance").value = 2.0
|
||||||
|
self.add_input("NodeSocketBool", "Reverse").value = False
|
||||||
|
self.add_output("SLICESOCK_Paths", "Paths")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "reverse")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
rev_prop = bool(getattr(self, "reverse", False))
|
||||||
|
|
||||||
|
def _anchor_one(paths: Any, anchor_paths: Any, dist_val: Any, rev_val: Any) -> SlicePaths:
|
||||||
|
if not paths:
|
||||||
|
return SlicePaths()
|
||||||
|
if not isinstance(paths, SlicePaths) and not getattr(paths, "is_slice_paths", False):
|
||||||
|
return SlicePaths()
|
||||||
|
if not anchor_paths:
|
||||||
|
return paths
|
||||||
|
d = float(dist_val if dist_val is not None else getattr(self, "distance", 2.0))
|
||||||
|
is_rev = bool(rev_val) if rev_val is not None else rev_prop
|
||||||
|
if hasattr(self, "inputs"):
|
||||||
|
rev_sock = self.inputs.get("Reverse")
|
||||||
|
if rev_sock is not None and not getattr(rev_sock, "is_linked", False):
|
||||||
|
is_rev = bool(getattr(rev_sock, "value", rev_prop)) or rev_prop
|
||||||
|
return paths.anchor(anchor_paths, distance=d, reverse=is_rev)
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_anchor_one,
|
||||||
|
main_socket="Paths",
|
||||||
|
secondary_sockets=["Anchor Paths", "Distance", "Reverse"],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
68
core/nodeslicer/nodes/path/buffer_curve.py
Normal file
68
core/nodeslicer/nodes/path/buffer_curve.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
from typing import Any
|
||||||
|
from ...bpy_ns_types import NfEnumProperty, NfBoolProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_BufferCurve(SliceNode):
|
||||||
|
bl_label = "Buffer Curve"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
|
cap_style: str = NfEnumProperty(
|
||||||
|
name="Cap Style",
|
||||||
|
items=[
|
||||||
|
('round', "Round", ""),
|
||||||
|
('flat', "Flat", ""),
|
||||||
|
('square', "Square", "")
|
||||||
|
],
|
||||||
|
default='round'
|
||||||
|
)
|
||||||
|
|
||||||
|
join_style: str = NfEnumProperty(
|
||||||
|
name="Join Style",
|
||||||
|
items=[
|
||||||
|
('round', "Round", ""),
|
||||||
|
('mitre', "Mitre", ""),
|
||||||
|
('bevel', "Bevel", "")
|
||||||
|
],
|
||||||
|
default='round'
|
||||||
|
)
|
||||||
|
|
||||||
|
single_sided: bool = NfBoolProperty(
|
||||||
|
name="Single Sided",
|
||||||
|
description="Buffer only on one side of the curve",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths")
|
||||||
|
self.add_input("NodeSocketFloat", "Distance").value = 0.0
|
||||||
|
self.add_input("NodeSocketRotation", "Rotation")
|
||||||
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "cap_style")
|
||||||
|
layout.prop(self, "join_style")
|
||||||
|
layout.prop(self, "single_sided")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _buf(p: Any, dist: Any, rot: Any) -> SlicePaths:
|
||||||
|
if not p: return SlicePaths()
|
||||||
|
return p.buffer_curve(
|
||||||
|
float(dist if dist is not None else 0.0),
|
||||||
|
cap_style=self.cap_style,
|
||||||
|
join_style=self.join_style,
|
||||||
|
single_sided=self.single_sided,
|
||||||
|
rotation=rot
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_buf,
|
||||||
|
main_socket="Paths",
|
||||||
|
secondary_sockets=["Distance", "Rotation"],
|
||||||
|
is_vector=[False, True],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
43
core/nodeslicer/nodes/path/buffer_shells.py
Normal file
43
core/nodeslicer/nodes/path/buffer_shells.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from typing import Any
|
||||||
|
from ...bpy_ns_types import NfEnumProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_BufferShells(SliceNode):
|
||||||
|
bl_label = "Buffer Shells"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
|
shape_option: str = NfEnumProperty(
|
||||||
|
name="Miter",
|
||||||
|
items=[
|
||||||
|
('round', "Round", ""),
|
||||||
|
('square', "Square", ""),
|
||||||
|
('flat', "Flat", "")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths")
|
||||||
|
self.add_input("NodeSocketFloat", "Distance").value = 0
|
||||||
|
self.add_input("NodeSocketRotation", "Rotation")
|
||||||
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "shape_option")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _buf(p: Any, dist: Any, rot: Any) -> SlicePaths:
|
||||||
|
if not p: return SlicePaths()
|
||||||
|
return p.buffer(float(dist if dist is not None else 0.0), miter=self.shape_option, rotation=rot)
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_buf,
|
||||||
|
main_socket="Paths",
|
||||||
|
secondary_sockets=["Distance", "Rotation"],
|
||||||
|
is_vector=[False, True],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
59
core/nodeslicer/nodes/path/buffer_shells_multiple.py
Normal file
59
core/nodeslicer/nodes/path/buffer_shells_multiple.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
from typing import Any
|
||||||
|
from ...bpy_ns_types import NfEnumProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_BufferShellsMultiple(SliceNode):
|
||||||
|
bl_label = "Buffer Shells Multiple"
|
||||||
|
list_socket_mapping = [("Paths", ["Paths", "Outermost Path", "Innermost Path"])]
|
||||||
|
|
||||||
|
shape_option: str = NfEnumProperty(
|
||||||
|
name="Miter",
|
||||||
|
items=[
|
||||||
|
('round', "Round", ""),
|
||||||
|
('square', "Square", ""),
|
||||||
|
('flat', "Flat", "")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths")
|
||||||
|
self.add_input("NodeSocketFloat", "Distance").value = 0
|
||||||
|
self.add_input("NodeSocketFloat", "Width Step").value = 0.4
|
||||||
|
self.add_input("NodeSocketInt", "Count").value = 2
|
||||||
|
self.add_input("NodeSocketRotation", "Rotation")
|
||||||
|
self.add_output("SLICESOCK_Paths", "Paths")
|
||||||
|
self.add_output("SLICESOCK_Paths", "Outermost Path")
|
||||||
|
self.add_output("SLICESOCK_Paths", "Innermost Path")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "shape_option")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _buf_multi(p: Any, b_dist: Any, w_step: Any, cnt: Any, rot: Any) -> SlicePaths:
|
||||||
|
c = int(cnt if cnt is not None else 1)
|
||||||
|
if not p or c <= 0:
|
||||||
|
return SlicePaths()
|
||||||
|
dist = float(b_dist if b_dist is not None else 0.0)
|
||||||
|
step = float(w_step if w_step is not None else 0.4)
|
||||||
|
if socket_name == "Outermost Path":
|
||||||
|
outer_dist = max(dist, dist + (c - 1) * step) if c > 1 else dist
|
||||||
|
return p.buffer(outer_dist, miter=self.shape_option, rotation=rot)
|
||||||
|
elif socket_name == "Innermost Path":
|
||||||
|
inner_dist = min(dist, dist + (c - 1) * step) if c > 1 else dist
|
||||||
|
return p.buffer(inner_dist, miter=self.shape_option, rotation=rot)
|
||||||
|
elif socket_name == "Paths":
|
||||||
|
return p.buffer_multiple(dist, c, step, miter=self.shape_option, rotation=rot)
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_buf_multi,
|
||||||
|
main_socket="Paths",
|
||||||
|
secondary_sockets=["Distance", "Width Step", "Count", "Rotation"],
|
||||||
|
is_vector=[False, False, False, True],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
53
core/nodeslicer/nodes/path/flatten_path_list.py
Normal file
53
core/nodeslicer/nodes/path/flatten_path_list.py
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
from ...bpy_ns_types import NfEnumProperty, notify_tree_changed
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
from .....infra.blender.list_utils import is_list_data, flatten_list
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_FlattenPathList(SliceNode):
|
||||||
|
bl_label = "Flatten Path List"
|
||||||
|
always_list_inputs = ["Paths"]
|
||||||
|
|
||||||
|
mode: str = NfEnumProperty(
|
||||||
|
name="Mode",
|
||||||
|
items=[
|
||||||
|
('ZIP', "Zip", "Interleave paths layer by layer across all list items"),
|
||||||
|
('EXTEND', "Extend", "Append paths sequentially from all list items"),
|
||||||
|
],
|
||||||
|
default='ZIP',
|
||||||
|
update=notify_tree_changed,
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths", "Paths", display_shape='LIST')
|
||||||
|
self.add_output("SLICESOCK_Paths", "Paths")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "mode", text="")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||||
|
val = self.resolve("Paths")
|
||||||
|
if val is None:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
if not is_list_data(val):
|
||||||
|
if isinstance(val, SlicePaths) or getattr(val, "is_slice_paths", False):
|
||||||
|
return val
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
raw_items = flatten_list(val)
|
||||||
|
paths_list: list[SlicePaths] = [
|
||||||
|
item for item in raw_items
|
||||||
|
if isinstance(item, SlicePaths) or getattr(item, "is_slice_paths", False)
|
||||||
|
]
|
||||||
|
|
||||||
|
if not paths_list:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
mode = str(getattr(self, "mode", "ZIP")).upper()
|
||||||
|
if mode == "EXTEND":
|
||||||
|
return SlicePaths.extend_paths(paths_list)
|
||||||
|
else:
|
||||||
|
return SlicePaths.zip_paths(paths_list)
|
||||||
43
core/nodeslicer/nodes/path/get_paths.py
Normal file
43
core/nodeslicer/nodes/path/get_paths.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from typing import Any
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_GetPaths(SliceNode):
|
||||||
|
bl_label = "Get Paths"
|
||||||
|
list_socket_mapping = [("Object", "Paths")]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("NodeSocketObject")
|
||||||
|
self.add_input("NodeSocketString", "Extrusion Multiplier Attribute").value = 'e_mult'
|
||||||
|
self.add_input("NodeSocketString", "Feedrate Attribute").value = 'f_rate'
|
||||||
|
self.add_input("SLICESOCK_Settings", "Extra Attributes")
|
||||||
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
from .....infra.blender.blender_mesh_capture import capture_mesh_edges
|
||||||
|
|
||||||
|
def _get_single_path(o: Any, e_name: Any, f_name: Any, extra: Any) -> SlicePaths:
|
||||||
|
if not o:
|
||||||
|
return SlicePaths()
|
||||||
|
so, e_rates, f_rates, extra_attrs_data = capture_mesh_edges(o, str(e_name or ""), str(f_name or ""), extra)
|
||||||
|
sp = SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
|
||||||
|
if hasattr(o, "transforms"):
|
||||||
|
sp.transforms = list(getattr(o, "transforms", []))
|
||||||
|
if hasattr(o, "name") and getattr(o, "name"):
|
||||||
|
name_val = str(getattr(o, "name"))
|
||||||
|
sp.name = name_val
|
||||||
|
sp.objects_info = {"objects": [{"name": name_val, "polygon": []}]}
|
||||||
|
return sp
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_get_single_path,
|
||||||
|
main_socket="Object",
|
||||||
|
secondary_sockets=["Extrusion Multiplier Attribute", "Feedrate Attribute", "Extra Attributes"],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
51
core/nodeslicer/nodes/path/infill.py
Normal file
51
core/nodeslicer/nodes/path/infill.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
from typing import Any
|
||||||
|
from ...bpy_ns_types import NfEnumProperty, notify_tree_changed
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_Infill(SliceNode):
|
||||||
|
bl_label = "Infill"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
|
infill_type: str = NfEnumProperty(
|
||||||
|
name="Type",
|
||||||
|
items=[
|
||||||
|
('GRID', "Grid", "Cross-hatch grid"),
|
||||||
|
('RECTILINEAR', "Rectilinear", "Alternating parallel lines"),
|
||||||
|
('LINES', "Lines", "Parallel lines in one direction"),
|
||||||
|
],
|
||||||
|
default='GRID',
|
||||||
|
update=notify_tree_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths")
|
||||||
|
self.add_input("NodeSocketFloat", "Density").value = 2.0
|
||||||
|
self.add_input("NodeSocketFloat", "Infill Rotation").value = 0.0
|
||||||
|
self.add_input("NodeSocketRotation", "Layer Rotation")
|
||||||
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "infill_type", text="")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _inf(p: Any, dens: Any, inf_rot: Any, rot: Any) -> SlicePaths:
|
||||||
|
if not p: return SlicePaths()
|
||||||
|
return p.infill(
|
||||||
|
float(dens if dens is not None else 2.0),
|
||||||
|
rotation_deg=float(inf_rot if inf_rot is not None else 0.0),
|
||||||
|
infill_type=self.infill_type,
|
||||||
|
layer_rotation=rot
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_inf,
|
||||||
|
main_socket="Paths",
|
||||||
|
secondary_sockets=["Density", "Infill Rotation", "Layer Rotation"],
|
||||||
|
is_vector=[False, False, True],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
42
core/nodeslicer/nodes/path/layers.py
Normal file
42
core/nodeslicer/nodes/path/layers.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
from typing import Any
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_Layers(SliceNode):
|
||||||
|
bl_label = "Layers"
|
||||||
|
is_list_output = True
|
||||||
|
always_list_outputs = ["Layers"]
|
||||||
|
list_socket_mapping = [("Layer Height", "Layers")]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("NodeSocketFloat", "First Layer Height").value = 0.2
|
||||||
|
self.add_input("NodeSocketFloat", "Layer Height").value = 0.2
|
||||||
|
self.add_input("NodeSocketFloat", "Total Height").value = 250.0
|
||||||
|
self.add_output("NodeSocketFloat", "Layers", display_shape='LIST')
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _calc_layers(lh_val: Any, flh_val: Any, tot_val: Any) -> list[float]:
|
||||||
|
flh = float(flh_val) if flh_val is not None and float(flh_val) > 0 else 0.2
|
||||||
|
lh = float(lh_val) if lh_val is not None and float(lh_val) > 0 else 0.2
|
||||||
|
tot = float(tot_val) if tot_val is not None and float(tot_val) > 0 else 250.0
|
||||||
|
|
||||||
|
flh_um = max(1, int(round(flh * 1000)))
|
||||||
|
lh_um = max(1, int(round(lh * 1000)))
|
||||||
|
tot_um = int(round(tot * 1000))
|
||||||
|
|
||||||
|
layers: list[float] = []
|
||||||
|
z_um = flh_um
|
||||||
|
while z_um <= tot_um:
|
||||||
|
layers.append(z_um / 1000.0)
|
||||||
|
z_um += lh_um
|
||||||
|
return layers
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_calc_layers,
|
||||||
|
main_socket="Layer Height",
|
||||||
|
secondary_sockets=["First Layer Height", "Total Height"],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=[],
|
||||||
|
)
|
||||||
48
core/nodeslicer/nodes/path/offset_curve.py
Normal file
48
core/nodeslicer/nodes/path/offset_curve.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
from typing import Any, cast, Literal
|
||||||
|
from ...bpy_ns_types import NfEnumProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_OffsetCurve(SliceNode):
|
||||||
|
bl_label = "Offset Curve"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
|
join_style: str = NfEnumProperty(
|
||||||
|
name="Join Style",
|
||||||
|
items=[
|
||||||
|
('round', "Round", ""),
|
||||||
|
('mitre', "Mitre", ""),
|
||||||
|
('bevel', "Bevel", "")
|
||||||
|
],
|
||||||
|
default='round'
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths")
|
||||||
|
self.add_input("NodeSocketFloat", "Distance").value = 0.0
|
||||||
|
self.add_input("NodeSocketRotation", "Rotation")
|
||||||
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "join_style")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _off(p: Any, dist: Any, rot: Any) -> SlicePaths:
|
||||||
|
if not p: return SlicePaths()
|
||||||
|
return p.offset_curve(
|
||||||
|
float(dist if dist is not None else 0.0),
|
||||||
|
join_style=cast(Literal['round', 'mitre'], self.join_style),
|
||||||
|
rotation=rot
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_off,
|
||||||
|
main_socket="Paths",
|
||||||
|
secondary_sockets=["Distance", "Rotation"],
|
||||||
|
is_vector=[False, True],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
30
core/nodeslicer/nodes/path/overhang_mask.py
Normal file
30
core/nodeslicer/nodes/path/overhang_mask.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
from typing import Any
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_OverhangMask(SliceNode):
|
||||||
|
bl_label = "Overhang Mask"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths")
|
||||||
|
self.add_input("NodeSocketFloat", "Tolerance").value = 0.05
|
||||||
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _mask(p: Any, tol: Any) -> SlicePaths:
|
||||||
|
if not p: return SlicePaths()
|
||||||
|
return p.get_overhang_mask(tolerance=float(tol if tol is not None else 0.05))
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_mask,
|
||||||
|
main_socket="Paths",
|
||||||
|
secondary_sockets=["Tolerance"],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
29
core/nodeslicer/nodes/path/path_offset.py
Normal file
29
core/nodeslicer/nodes/path/path_offset.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from typing import Any
|
||||||
|
import numpy as np
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_PathOffset(SliceNode):
|
||||||
|
bl_label = "Path Offset"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths", "Paths")
|
||||||
|
self.add_input("NodeSocketVector", "Offset")
|
||||||
|
self.add_output("SLICESOCK_Paths", "Paths")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _off(p: Any, off: Any) -> SlicePaths:
|
||||||
|
if not p: return SlicePaths()
|
||||||
|
return p.offset(np.array(off if off is not None else (0.0, 0.0, 0.0), dtype=np.float64))
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_off,
|
||||||
|
main_socket="Paths",
|
||||||
|
secondary_sockets=["Offset"],
|
||||||
|
is_vector=[True],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
128
core/nodeslicer/nodes/path/path_operations.py
Normal file
128
core/nodeslicer/nodes/path/path_operations.py
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
from typing import Any
|
||||||
|
from ...bpy_ns_types import NfEnumProperty, NfBoolProperty, notify_tree_changed
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNodeJoiner, SlicePaths
|
||||||
|
from .....infra.blender.list_utils import (
|
||||||
|
eval_element_wise,
|
||||||
|
prepare_indexed_inputs,
|
||||||
|
is_list_data,
|
||||||
|
set_socket_shape,
|
||||||
|
is_socket_linked_to_list,
|
||||||
|
flatten_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_PathOperations(SliceNodeJoiner):
|
||||||
|
bl_label = "Path Operations"
|
||||||
|
socket_type = "SLICESOCK_Paths"
|
||||||
|
|
||||||
|
operation: str = NfEnumProperty(
|
||||||
|
name="Operation",
|
||||||
|
items=[
|
||||||
|
('ZIP', "Zip paths", "Interleave paths from all inputs layer by layer"),
|
||||||
|
('EXTEND', "Extend paths", "Append paths sequentially"),
|
||||||
|
('MAX_COVERAGE', "Maximum coverage", "Union of all paths"),
|
||||||
|
('MIN_COVERAGE', "Minimum coverage", "Intersection of paths per layer (single paths preserved)"),
|
||||||
|
('INTERSECTION', "Intersection", "Intersection of all paths"),
|
||||||
|
('SUBTRACT', "Subtract", "Subtract subsequent paths from first input"),
|
||||||
|
('XOR', "Exclusive or", "Exclusive or of all paths"),
|
||||||
|
],
|
||||||
|
default='ZIP',
|
||||||
|
update=notify_tree_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
connect_along_boundary: bool = NfBoolProperty(
|
||||||
|
name="Connect along boundary",
|
||||||
|
description="Connect clipped segments along the boundary of the clipping polygon",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
acquire_transforms: bool = NfBoolProperty(
|
||||||
|
name="Acquire Transforms",
|
||||||
|
description="Apply transforms from primary path metadata to subsequent paths so they line up as in the viewport",
|
||||||
|
default=True,
|
||||||
|
update=notify_tree_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
super().update()
|
||||||
|
if self.inputs:
|
||||||
|
first_inp = self.inputs[0]
|
||||||
|
is_list = is_socket_linked_to_list(first_inp)
|
||||||
|
set_socket_shape(first_inp, "LIST" if is_list else "CIRCLE")
|
||||||
|
for inp in self.inputs[1:]:
|
||||||
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
||||||
|
if self.outputs:
|
||||||
|
set_socket_shape(self.outputs[0], "LIST" if is_list else "CIRCLE")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "operation", text="")
|
||||||
|
if self.operation in ['MIN_COVERAGE', 'INTERSECTION', 'SUBTRACT']:
|
||||||
|
layout.prop(self, "connect_along_boundary")
|
||||||
|
layout.prop(self, "acquire_transforms")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
from .....infra.blender.blender_slice_nodes_generic import resolve_input
|
||||||
|
|
||||||
|
if not self.inputs:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
def _is_valid_paths_input(val: Any) -> bool:
|
||||||
|
if val is None:
|
||||||
|
return False
|
||||||
|
if isinstance(val, SlicePaths):
|
||||||
|
return len(val) > 0
|
||||||
|
if is_list_data(val):
|
||||||
|
flat = flatten_list(val)
|
||||||
|
return any(isinstance(x, SlicePaths) and len(x) > 0 for x in flat)
|
||||||
|
return False
|
||||||
|
|
||||||
|
first_val = resolve_input(self.inputs[0]) if getattr(self.inputs[0], "is_linked", False) else None
|
||||||
|
|
||||||
|
active_other_inputs: list[tuple[str, Any]] = []
|
||||||
|
for input_socket in self.inputs[1:]:
|
||||||
|
if getattr(input_socket, "is_linked", False):
|
||||||
|
val = resolve_input(input_socket)
|
||||||
|
if _is_valid_paths_input(val):
|
||||||
|
active_other_inputs.append((getattr(input_socket, "name", "Paths"), val))
|
||||||
|
|
||||||
|
acquire_t = bool(getattr(self, "acquire_transforms", True))
|
||||||
|
|
||||||
|
if not active_other_inputs:
|
||||||
|
def _single_op(p: Any) -> Any:
|
||||||
|
if isinstance(p, SlicePaths):
|
||||||
|
return p
|
||||||
|
return SlicePaths()
|
||||||
|
if first_val is None:
|
||||||
|
return SlicePaths()
|
||||||
|
return eval_element_wise(_single_op, first_val)
|
||||||
|
|
||||||
|
other_names = [item[0] for item in active_other_inputs]
|
||||||
|
other_vals = [item[1] for item in active_other_inputs]
|
||||||
|
|
||||||
|
is_list, p0_items, other_lists = prepare_indexed_inputs(
|
||||||
|
first_val,
|
||||||
|
*other_vals,
|
||||||
|
input_names=other_names
|
||||||
|
)
|
||||||
|
|
||||||
|
def _run_op_at_index(p0: Any, *others: Any) -> SlicePaths:
|
||||||
|
valid_others = [p for p in others if isinstance(p, SlicePaths) and len(p) > 0]
|
||||||
|
inputs_data = [p for p in [p0] + valid_others if isinstance(p, SlicePaths) and len(p) > 0]
|
||||||
|
if not inputs_data:
|
||||||
|
return SlicePaths()
|
||||||
|
return SlicePaths.path_operations(
|
||||||
|
inputs_data,
|
||||||
|
operation=self.operation,
|
||||||
|
connect_along_boundary=self.connect_along_boundary,
|
||||||
|
acquire_transforms=acquire_t
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_list:
|
||||||
|
return [
|
||||||
|
_run_op_at_index(p0_items[i], *(other_lists[k][i] for k in range(len(other_lists))))
|
||||||
|
for i in range(len(p0_items))
|
||||||
|
]
|
||||||
|
return _run_op_at_index(p0_items[0], *(other_lists[k][0] for k in range(len(other_lists))))
|
||||||
9
core/nodeslicer/nodes/path/paths_to_list.py
Normal file
9
core/nodeslicer/nodes/path/paths_to_list.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNodeListJoiner
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_PathsToList(SliceNodeListJoiner):
|
||||||
|
bl_label = "Paths to List"
|
||||||
|
socket_type = "SLICESOCK_Paths"
|
||||||
|
socket_label = "Paths"
|
||||||
31
core/nodeslicer/nodes/path/rotate_path.py
Normal file
31
core/nodeslicer/nodes/path/rotate_path.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
from typing import Any
|
||||||
|
import numpy as np
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_RotatePath(SliceNode):
|
||||||
|
bl_label = "Rotate Path"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths", "Paths")
|
||||||
|
self.add_input("NodeSocketFloat", "Angle").value = 0.0
|
||||||
|
self.add_input("NodeSocketVector", "Center")
|
||||||
|
self.add_output("SLICESOCK_Paths", "Paths")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _rot(p: Any, ang: Any, cent: Any) -> SlicePaths:
|
||||||
|
if not p: return SlicePaths()
|
||||||
|
center_vec = np.array(cent, dtype=np.float64) if cent and any(cent) else None
|
||||||
|
return p.rotate(float(ang if ang is not None else 0.0), center=center_vec)
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_rot,
|
||||||
|
main_socket="Paths",
|
||||||
|
secondary_sockets=["Angle", "Center"],
|
||||||
|
is_vector=[False, True],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
58
core/nodeslicer/nodes/path/seam_placement.py
Normal file
58
core/nodeslicer/nodes/path/seam_placement.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
from typing import Any
|
||||||
|
from ...bpy_ns_types import NfEnumProperty, NfBoolProperty, notify_tree_changed
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_SeamPlacement(SliceNode):
|
||||||
|
bl_label = "Seam Placement"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
|
mode: str = NfEnumProperty(
|
||||||
|
name="Mode",
|
||||||
|
items=[
|
||||||
|
('CLOSEST', "Closest to Seam", "Reposition seam to intersection nearest to existing seam"),
|
||||||
|
('FIRST', "First Intersection", "Reposition seam to first intersection along path traversal"),
|
||||||
|
('CENTROID', "Mesh Centroid", "Reposition seam to intersection nearest to mesh center"),
|
||||||
|
],
|
||||||
|
default='CLOSEST',
|
||||||
|
update=notify_tree_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
acquire_transforms: bool = NfBoolProperty(
|
||||||
|
name="Acquire Transforms",
|
||||||
|
description="Apply transforms from paths metadata to the cutting mesh so they line up as in the viewport",
|
||||||
|
default=True,
|
||||||
|
update=notify_tree_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths")
|
||||||
|
self.add_input("SLICESOCK_Mesh")
|
||||||
|
self.add_input("NodeSocketFloat", "Seam Staggering").value = 0.0
|
||||||
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "mode", text="")
|
||||||
|
layout.prop(self, "acquire_transforms")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
acquire_t = bool(getattr(self, "acquire_transforms", True))
|
||||||
|
|
||||||
|
def _place(p: Any, m_in: Any, stg: Any) -> SlicePaths:
|
||||||
|
if not p:
|
||||||
|
return SlicePaths()
|
||||||
|
s_val = float(stg if stg is not None else 0.0)
|
||||||
|
if not m_in and abs(s_val) < 1e-6:
|
||||||
|
return p
|
||||||
|
return p.place_seams(m_in, mode=self.mode.lower(), acquire_transforms=acquire_t, stagger_mm=s_val)
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_place,
|
||||||
|
main_socket="Paths",
|
||||||
|
secondary_sockets=["Mesh", "Seam Staggering"],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
40
core/nodeslicer/nodes/path/simplify_paths.py
Normal file
40
core/nodeslicer/nodes/path/simplify_paths.py
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
from typing import Any
|
||||||
|
from ...bpy_ns_types import NfBoolProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_SimplifyPaths(SliceNode):
|
||||||
|
bl_label = "Simplify Paths"
|
||||||
|
list_socket_mapping = [("Paths", "Paths")]
|
||||||
|
|
||||||
|
preserve_topology: bool = NfBoolProperty(
|
||||||
|
name="Preserve Topology",
|
||||||
|
description="Prevents swapping of contained/containing geometry during simplification",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths")
|
||||||
|
self.add_input("NodeSocketFloat", "Tolerance").value = 0.1
|
||||||
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "preserve_topology")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
def _simp(p: Any, tol: Any) -> SlicePaths:
|
||||||
|
t_val = float(tol if tol is not None else 0.0)
|
||||||
|
if not p or t_val <= 0:
|
||||||
|
return p if isinstance(p, SlicePaths) else SlicePaths()
|
||||||
|
return p.simplify(t_val, preserve_topology=self.preserve_topology)
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_simp,
|
||||||
|
main_socket="Paths",
|
||||||
|
secondary_sockets=["Tolerance"],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
53
core/nodeslicer/nodes/path/slice_shells.py
Normal file
53
core/nodeslicer/nodes/path/slice_shells.py
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
from typing import Any
|
||||||
|
from ...bpy_ns_types import NfBoolProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
from ....functions.path_functions import _slice_single_mesh_item
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_SliceShells(SliceNode):
|
||||||
|
bl_label = "Slice Shells"
|
||||||
|
list_socket_mapping = [("Mesh", "Paths")]
|
||||||
|
|
||||||
|
ignore_holes: bool = NfBoolProperty(
|
||||||
|
name="Ignore Holes",
|
||||||
|
description="Ignore interior hole boundaries and generate only outer shells",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Mesh")
|
||||||
|
self.add_input("NodeSocketFloat", "Layer Height").value = 0.2
|
||||||
|
self.add_input("NodeSocketFloat", "First Layer Height").value = 0.2
|
||||||
|
self.add_input("NodeSocketRotation", "Rotation")
|
||||||
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
if hasattr(self, "ignore_holes"):
|
||||||
|
layout.prop(self, "ignore_holes")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
ignore_holes: bool = bool(getattr(self, "ignore_holes", False))
|
||||||
|
if hasattr(self, "inputs") and "Ignore Holes" in self.inputs:
|
||||||
|
try:
|
||||||
|
val = self.resolve("Ignore Holes")
|
||||||
|
if val is not None:
|
||||||
|
ignore_holes = bool(val)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _slice_one(item: Any, lh_val: Any, flh_val: Any, rot_val: Any) -> SlicePaths:
|
||||||
|
lh = float(lh_val) if lh_val is not None and float(lh_val) > 0 else 0.2
|
||||||
|
flh = float(flh_val) if flh_val is not None and float(flh_val) > 0 else None
|
||||||
|
return _slice_single_mesh_item(item, lh, flh, rot_val, ignore_holes)
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_slice_one,
|
||||||
|
main_socket="Mesh",
|
||||||
|
secondary_sockets=["Layer Height", "First Layer Height", "Rotation"],
|
||||||
|
is_vector=[False, False, True],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
52
core/nodeslicer/nodes/path/slice_shells_layers.py
Normal file
52
core/nodeslicer/nodes/path/slice_shells_layers.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
from typing import Any
|
||||||
|
from ...bpy_ns_types import NfBoolProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNode, SlicePaths
|
||||||
|
from ....functions.path_functions import _slice_single_mesh_item_with_layers
|
||||||
|
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_SliceShellsLayers(SliceNode):
|
||||||
|
bl_label = "Slice Shells (Layers)"
|
||||||
|
always_list_inputs = ["Layers"]
|
||||||
|
list_socket_mapping = [("Mesh", "Paths")]
|
||||||
|
|
||||||
|
ignore_holes: bool = NfBoolProperty(
|
||||||
|
name="Ignore Holes",
|
||||||
|
description="Ignore interior hole boundaries and generate only outer shells",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Mesh")
|
||||||
|
self.add_input("NodeSocketFloat", "Layers", display_shape='LIST')
|
||||||
|
self.add_input("NodeSocketRotation", "Rotation")
|
||||||
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
if hasattr(self, "ignore_holes"):
|
||||||
|
layout.prop(self, "ignore_holes")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||||
|
ignore_holes: bool = bool(getattr(self, "ignore_holes", False))
|
||||||
|
if hasattr(self, "inputs") and "Ignore Holes" in self.inputs:
|
||||||
|
try:
|
||||||
|
val = self.resolve("Ignore Holes")
|
||||||
|
if val is not None:
|
||||||
|
ignore_holes = bool(val)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _slice_one(item: Any, layers_val: Any, rot_val: Any) -> SlicePaths:
|
||||||
|
return _slice_single_mesh_item_with_layers(item, layers_val, rot_val, ignore_holes)
|
||||||
|
|
||||||
|
return self.eval_multivalue(
|
||||||
|
_slice_one,
|
||||||
|
main_socket="Mesh",
|
||||||
|
secondary_sockets=["Layers", "Rotation"],
|
||||||
|
is_vector=[False, True],
|
||||||
|
is_atomic=[True, False],
|
||||||
|
socket_name=socket_name,
|
||||||
|
default=SlicePaths(),
|
||||||
|
)
|
||||||
7
core/nodeslicer/nodes/preview/__init__.py
Normal file
7
core/nodeslicer/nodes/preview/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .gcode_to_mesh import SLICENODE_GcodeToMesh
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"SLICENODE_GcodeToMesh",
|
||||||
|
]
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import math
|
|
||||||
import bpy
|
|
||||||
import os
|
import os
|
||||||
|
import bpy
|
||||||
|
|
||||||
from ..bpy_ns_types import NfBoolProperty
|
from ...bpy_ns_types import NfBoolProperty
|
||||||
from ....registry import register_class
|
from .....registry import register_class
|
||||||
from .base import ExecSliceNode
|
from ..base import ExecSliceNode
|
||||||
|
from ....functions.preview_functions import parse_gcode_preview_geometry
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_GcodeToMesh(ExecSliceNode):
|
class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
|
|
@ -22,7 +22,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
super().update()
|
super().update()
|
||||||
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
from .....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
|
||||||
for inp in self.inputs:
|
for inp in self.inputs:
|
||||||
if getattr(inp, "name", "") == "Gcode" or getattr(inp, "identifier", "").startswith("Gcode_ms_in_"):
|
if getattr(inp, "name", "") == "Gcode" or getattr(inp, "identifier", "").startswith("Gcode_ms_in_"):
|
||||||
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
|
||||||
|
|
@ -39,128 +39,25 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
obj.data.clear_geometry()
|
obj.data.clear_geometry()
|
||||||
return
|
return
|
||||||
|
|
||||||
vertices = []
|
|
||||||
edges = []
|
|
||||||
e_rates = []
|
|
||||||
|
|
||||||
scale_length = 1.0
|
scale_length = 1.0
|
||||||
if bpy.context.scene and bpy.context.scene.unit_settings:
|
if bpy.context.scene and bpy.context.scene.unit_settings:
|
||||||
scale_length = bpy.context.scene.unit_settings.scale_length
|
scale_length = bpy.context.scene.unit_settings.scale_length
|
||||||
scale_factor = 0.001 / scale_length
|
scale_factor = 0.001 / scale_length
|
||||||
|
|
||||||
last_x, last_y, last_z = 0.0, 0.0, 0.0
|
|
||||||
last_e = 0.0
|
|
||||||
|
|
||||||
offset_x, offset_y = 0.0, 0.0
|
|
||||||
custom_default_extrusion_rate = 0.0
|
|
||||||
default_layer_height = 0.2
|
|
||||||
default_layer_width = 0.4
|
|
||||||
|
|
||||||
last_v_idx = -1
|
parsed = parse_gcode_preview_geometry(
|
||||||
last_p2 = None
|
gcode,
|
||||||
|
scale_factor=scale_factor,
|
||||||
|
scale_length=scale_length,
|
||||||
|
show_e_0=bool(getattr(self, "show_e_0", False)),
|
||||||
|
)
|
||||||
|
|
||||||
prep_translation = [0.0, 0.0, 0.0]
|
vertices = parsed["vertices"]
|
||||||
prep_rotation = [0.0, 0.0, 0.0]
|
edges = parsed["edges"]
|
||||||
prep_scale = [1.0, 1.0, 1.0]
|
e_rates = parsed["e_rates"]
|
||||||
|
default_layer_height = parsed["default_layer_height"]
|
||||||
for line in gcode.splitlines():
|
prep_translation = parsed["prep_translation"]
|
||||||
# Parse metadata from comments
|
prep_rotation = parsed["prep_rotation"]
|
||||||
if ';' in line:
|
prep_scale = parsed["prep_scale"]
|
||||||
comment = line.split(';', 1)[1].strip()
|
|
||||||
if comment.startswith("OFFSET_X="):
|
|
||||||
try: offset_x = float(comment.split('=')[1])
|
|
||||||
except ValueError: pass
|
|
||||||
elif comment.startswith("OFFSET_Y="):
|
|
||||||
try: offset_y = float(comment.split('=')[1])
|
|
||||||
except ValueError: pass
|
|
||||||
elif comment.startswith("DEFAULT_EXTRUSION_RATE="):
|
|
||||||
try: custom_default_extrusion_rate = float(comment.split('=')[1])
|
|
||||||
except ValueError: pass
|
|
||||||
elif comment.startswith("DEFAULT_LAYER_HEIGHT="):
|
|
||||||
try: default_layer_height = float(comment.split('=')[1])
|
|
||||||
except ValueError: pass
|
|
||||||
elif comment.startswith("DEFAULT_LAYER_WIDTH="):
|
|
||||||
try: default_layer_width = float(comment.split('=')[1])
|
|
||||||
except ValueError: pass
|
|
||||||
elif comment.startswith("PREPARATION_TOTAL_TRANSLATION="):
|
|
||||||
try:
|
|
||||||
vals = [float(v) for v in comment.split('=')[1].split(',')]
|
|
||||||
if len(vals) >= 3: prep_translation = vals[:3]
|
|
||||||
except ValueError: pass
|
|
||||||
elif comment.startswith("PREPARATION_TOTAL_ROTATION="):
|
|
||||||
try:
|
|
||||||
vals = [float(v) for v in comment.split('=')[1].split(',')]
|
|
||||||
if len(vals) >= 3: prep_rotation = vals[:3]
|
|
||||||
except ValueError: pass
|
|
||||||
elif comment.startswith("PREPARATION_TOTAL_SCALE="):
|
|
||||||
try:
|
|
||||||
vals = [float(v) for v in comment.split('=')[1].split(',')]
|
|
||||||
if len(vals) >= 3: prep_scale = vals[:3]
|
|
||||||
except ValueError: pass
|
|
||||||
|
|
||||||
line_no_comment = line.split(';')[0].strip()
|
|
||||||
if not line_no_comment: continue
|
|
||||||
|
|
||||||
parts = line_no_comment.split()
|
|
||||||
if not parts: continue
|
|
||||||
cmd = parts[0].upper()
|
|
||||||
|
|
||||||
params = {}
|
|
||||||
for p in parts[1:]:
|
|
||||||
if len(p) < 2: continue
|
|
||||||
try:
|
|
||||||
# Scale coordinates by scene scale factor (mm to Blender units)
|
|
||||||
val = float(p[1:])
|
|
||||||
if p[0].upper() in 'XYZ':
|
|
||||||
val *= scale_factor
|
|
||||||
params[p[0].upper()] = val
|
|
||||||
except ValueError: continue
|
|
||||||
|
|
||||||
new_x = params.get('X', last_x)
|
|
||||||
new_y = params.get('Y', last_y)
|
|
||||||
new_z = params.get('Z', last_z)
|
|
||||||
new_e = params.get('E', last_e)
|
|
||||||
|
|
||||||
show = False
|
|
||||||
if getattr(self, "show_e_0", False): show = True
|
|
||||||
if cmd == 'G1' and new_e > 0: show = True
|
|
||||||
|
|
||||||
if show:
|
|
||||||
# Subtract offsets (scaled to Blender units) to return to original space
|
|
||||||
ox, oy = offset_x * scale_factor, offset_y * scale_factor
|
|
||||||
p1 = (last_x - ox, last_y - oy, last_z)
|
|
||||||
p2 = (new_x - ox, new_y - oy, new_z)
|
|
||||||
|
|
||||||
# Calculate e_rate (proportion between E and default_extrusion_rate)
|
|
||||||
seg_len_mm = math.dist(p1, p2) * (1000.0 * scale_length)
|
|
||||||
if seg_len_mm > 0 and custom_default_extrusion_rate > 0:
|
|
||||||
e_rate = (new_e / seg_len_mm) / custom_default_extrusion_rate
|
|
||||||
else:
|
|
||||||
e_rate = 1.0
|
|
||||||
|
|
||||||
if last_v_idx != -1 and p1 == last_p2:
|
|
||||||
# Connect to previous segment
|
|
||||||
v2_idx = len(vertices)
|
|
||||||
vertices.append(p2)
|
|
||||||
edges.append((last_v_idx, v2_idx))
|
|
||||||
last_v_idx = v2_idx
|
|
||||||
else:
|
|
||||||
# Start new path
|
|
||||||
v1_idx = len(vertices)
|
|
||||||
v2_idx = v1_idx + 1
|
|
||||||
vertices.extend([p1, p2])
|
|
||||||
edges.append((v1_idx, v2_idx))
|
|
||||||
last_v_idx = v2_idx
|
|
||||||
|
|
||||||
e_rates.append(e_rate)
|
|
||||||
last_p2 = p2
|
|
||||||
else:
|
|
||||||
# Break path connection on travel or non-shown move
|
|
||||||
last_v_idx = -1
|
|
||||||
last_p2 = None
|
|
||||||
|
|
||||||
last_x, last_y, last_z = new_x, new_y, new_z
|
|
||||||
last_e = new_e
|
|
||||||
|
|
||||||
if not vertices:
|
if not vertices:
|
||||||
return
|
return
|
||||||
|
|
@ -194,7 +91,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
ng_name = "Nonplanar Slicing Preview"
|
ng_name = "Nonplanar Slicing Preview"
|
||||||
if ng_name not in real_bpy.data.node_groups:
|
if ng_name not in real_bpy.data.node_groups:
|
||||||
# Try to load it from nodes.blend in the addon root
|
# Try to load it from nodes.blend in the addon root
|
||||||
addon_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
addon_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
|
||||||
blend_path = os.path.join(addon_dir, "nodes.blend")
|
blend_path = os.path.join(addon_dir, "nodes.blend")
|
||||||
if os.path.exists(blend_path):
|
if os.path.exists(blend_path):
|
||||||
try:
|
try:
|
||||||
|
|
@ -236,7 +133,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
mod["Socket_9"] = tuple(map(float, prep_scale))
|
mod["Socket_9"] = tuple(map(float, prep_scale))
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
def evaluate(self, operator=None):
|
||||||
from ....infra.blender.list_utils import flatten_list
|
from .....infra.blender.list_utils import flatten_list
|
||||||
raw_gcodes = self.resolve_multi_input("Gcode") or []
|
raw_gcodes = self.resolve_multi_input("Gcode") or []
|
||||||
gcodes: list[str] = [str(g) for g in flatten_list(raw_gcodes) if g]
|
gcodes: list[str] = [str(g) for g in flatten_list(raw_gcodes) if g]
|
||||||
base_mesh_name: str = self.resolve("Name")
|
base_mesh_name: str = self.resolve("Name")
|
||||||
15
core/nodeslicer/nodes/system/__init__.py
Normal file
15
core/nodeslicer/nodes/system/__init__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .printer_list import SLICENODE_PrinterList
|
||||||
|
from .join_execution import SLICENODE_JoinExecution
|
||||||
|
from .executor import SLICENODE_Executor
|
||||||
|
from .join_settings import SLICENODE_JoinSettings
|
||||||
|
from .test import SLICENODE_Test
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"SLICENODE_PrinterList",
|
||||||
|
"SLICENODE_JoinExecution",
|
||||||
|
"SLICENODE_Executor",
|
||||||
|
"SLICENODE_JoinSettings",
|
||||||
|
"SLICENODE_Test",
|
||||||
|
]
|
||||||
74
core/nodeslicer/nodes/system/executor.py
Normal file
74
core/nodeslicer/nodes/system/executor.py
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...bpy_ns_types import NfBoolProperty
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import ExecSliceNode, SliceNodeJoiner
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_Executor(ExecSliceNode, SliceNodeJoiner):
|
||||||
|
bl_label = "Executor"
|
||||||
|
button_label = "Execute All"
|
||||||
|
socket_type = "SLICESOCK_ExecutionLink"
|
||||||
|
|
||||||
|
auto_refresh: bool = NfBoolProperty(name="Auto-refresh", default=False)
|
||||||
|
log: bool = NfBoolProperty(name="Monitor Performance", default=False, description="Monitor performance statistics to open text editor")
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self._init_exec()
|
||||||
|
self.add_multi_socket_group(self.socket_type, name="Links")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
super().draw_buttons(context, layout)
|
||||||
|
layout.prop(self, "auto_refresh")
|
||||||
|
layout.prop(self, "log")
|
||||||
|
|
||||||
|
def evaluate(self, operator=None):
|
||||||
|
from .....infra.blender.blender_slice_nodes_generic import (
|
||||||
|
execution_cache,
|
||||||
|
resolve_input
|
||||||
|
)
|
||||||
|
from .....infra.profiling import (
|
||||||
|
profiler_context,
|
||||||
|
show_in_text_editor,
|
||||||
|
)
|
||||||
|
from .....infra.error_reporting import node_execution_scope
|
||||||
|
|
||||||
|
profiling_enabled = getattr(self, "log", False)
|
||||||
|
|
||||||
|
def _run_jobs():
|
||||||
|
for i, job in enumerate(self.inputs):
|
||||||
|
# Resolve the input to get the connected node(s)
|
||||||
|
res = resolve_input(job)
|
||||||
|
if res is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ensure we have a list of jobs to process (JoinExecution returns a list)
|
||||||
|
jobs_to_run = res if isinstance(res, list) else [res]
|
||||||
|
|
||||||
|
for upstream_node in jobs_to_run:
|
||||||
|
if not hasattr(upstream_node, "evaluate"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if operator:
|
||||||
|
operator.report({'INFO'}, f"Executing: {upstream_node.bl_label}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with execution_cache():
|
||||||
|
with node_execution_scope(upstream_node, operator=operator) as scope:
|
||||||
|
exec_res = upstream_node.evaluate(operator=scope.operator)
|
||||||
|
if exec_res != 'ok' and exec_res is not None:
|
||||||
|
if operator: operator.report({'WARNING'}, f"Node {upstream_node.bl_label} returned {exec_res}")
|
||||||
|
except Exception as e:
|
||||||
|
if operator: operator.report({'ERROR'}, str(e))
|
||||||
|
return 'error'
|
||||||
|
|
||||||
|
return 'ok'
|
||||||
|
|
||||||
|
if profiling_enabled:
|
||||||
|
with profiler_context() as profiler:
|
||||||
|
result = _run_jobs()
|
||||||
|
table_str = profiler.format_table()
|
||||||
|
show_in_text_editor("Performance", table_str)
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return _run_jobs()
|
||||||
20
core/nodeslicer/nodes/system/join_execution.py
Normal file
20
core/nodeslicer/nodes/system/join_execution.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import ExecSliceNode, SliceNodeJoiner
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_JoinExecution(SliceNodeJoiner):
|
||||||
|
bl_label = "Join Execution"
|
||||||
|
socket_type = "SLICESOCK_ExecutionLink"
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> list[ExecSliceNode]:
|
||||||
|
from .....infra.blender.blender_slice_nodes_generic import resolve_input
|
||||||
|
nodes: list[ExecSliceNode] = []
|
||||||
|
for input in self.inputs:
|
||||||
|
res = resolve_input(input)
|
||||||
|
if isinstance(res, ExecSliceNode):
|
||||||
|
nodes.append(res)
|
||||||
|
elif isinstance(res, list):
|
||||||
|
nodes.extend(res)
|
||||||
|
return nodes
|
||||||
19
core/nodeslicer/nodes/system/join_settings.py
Normal file
19
core/nodeslicer/nodes/system/join_settings.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import SliceNodeJoiner
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_JoinSettings(SliceNodeJoiner):
|
||||||
|
bl_label = "Join Settings"
|
||||||
|
socket_type = "SLICESOCK_Settings"
|
||||||
|
output_socket_type = "SLICESOCK_SettingsRo"
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> dict[str, str]:
|
||||||
|
from .....infra.blender.blender_slice_nodes_generic import resolve_input
|
||||||
|
merged: dict[str, str] = {}
|
||||||
|
for input in self.inputs:
|
||||||
|
val = resolve_input(input)
|
||||||
|
if isinstance(val, dict):
|
||||||
|
merged.update(val)
|
||||||
|
return merged
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Any
|
import json
|
||||||
from pathlib import Path
|
|
||||||
import os
|
import os
|
||||||
import bpy
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy
|
||||||
|
|
||||||
from ..bpy_ns_types import NfBoolProperty, NfStringProperty
|
from ...bpy_ns_types import NfStringProperty
|
||||||
from ....registry import register_class
|
from .....registry import register_class
|
||||||
from .base import ExecSliceNode, SliceNodeJoiner
|
from ..base import ExecSliceNode
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_PrinterList(ExecSliceNode):
|
class SLICENODE_PrinterList(ExecSliceNode):
|
||||||
|
|
@ -15,7 +15,6 @@ class SLICENODE_PrinterList(ExecSliceNode):
|
||||||
button_label = "Start Prints"
|
button_label = "Start Prints"
|
||||||
|
|
||||||
def _refresh(self):
|
def _refresh(self):
|
||||||
import json
|
|
||||||
if not self.printers_json_path:
|
if not self.printers_json_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -62,13 +61,12 @@ class SLICENODE_PrinterList(ExecSliceNode):
|
||||||
layout.prop(self, "printers_json_path")
|
layout.prop(self, "printers_json_path")
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
def evaluate(self, operator=None):
|
||||||
import json
|
from .....infra.networking.printer_backends import (
|
||||||
from ....infra.networking.printer_backends import (
|
|
||||||
PrusaLinkBackend,
|
PrusaLinkBackend,
|
||||||
CrealityBackend,
|
CrealityBackend,
|
||||||
MoonrakerBackend
|
MoonrakerBackend
|
||||||
)
|
)
|
||||||
from ....infra.networking.gcode_utils import prepare_gcode_file
|
from .....infra.networking.gcode_utils import prepare_gcode_file
|
||||||
|
|
||||||
self._refresh()
|
self._refresh()
|
||||||
|
|
||||||
|
|
@ -100,7 +98,7 @@ class SLICENODE_PrinterList(ExecSliceNode):
|
||||||
if not gcode:
|
if not gcode:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
from ....infra.blender.list_utils import is_list_data
|
from .....infra.blender.list_utils import is_list_data
|
||||||
if is_list_data(gcode):
|
if is_list_data(gcode):
|
||||||
if operator: operator.report({'ERROR'}, f"Printer List does not accept a list of Gcode on socket {sock.name}")
|
if operator: operator.report({'ERROR'}, f"Printer List does not accept a list of Gcode on socket {sock.name}")
|
||||||
continue
|
continue
|
||||||
|
|
@ -153,119 +151,3 @@ class SLICENODE_PrinterList(ExecSliceNode):
|
||||||
return 'no gcode'
|
return 'no gcode'
|
||||||
|
|
||||||
return 'ok'
|
return 'ok'
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_JoinExecution(SliceNodeJoiner):
|
|
||||||
bl_label = "Join Execution"
|
|
||||||
socket_type = "SLICESOCK_ExecutionLink"
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> list[ExecSliceNode]:
|
|
||||||
from ....infra.blender.blender_slice_nodes_generic import resolve_input
|
|
||||||
nodes: list[ExecSliceNode] = []
|
|
||||||
for input in self.inputs:
|
|
||||||
res = resolve_input(input)
|
|
||||||
if isinstance(res, ExecSliceNode):
|
|
||||||
nodes.append(res)
|
|
||||||
elif isinstance(res, list):
|
|
||||||
nodes.extend(res)
|
|
||||||
return nodes
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_Executor(ExecSliceNode, SliceNodeJoiner):
|
|
||||||
bl_label = "Executor"
|
|
||||||
button_label = "Execute All"
|
|
||||||
socket_type = "SLICESOCK_ExecutionLink"
|
|
||||||
|
|
||||||
auto_refresh: bool = NfBoolProperty(name="Auto-refresh", default=False)
|
|
||||||
log: bool = NfBoolProperty(name="Monitor Performance", default=False, description="Monitor performance statistics to open text editor")
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self._init_exec()
|
|
||||||
self.add_multi_socket_group(self.socket_type, name="Links")
|
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
|
||||||
super().draw_buttons(context, layout)
|
|
||||||
layout.prop(self, "auto_refresh")
|
|
||||||
layout.prop(self, "log")
|
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
|
||||||
from ....infra.blender.blender_slice_nodes_generic import (
|
|
||||||
execution_cache,
|
|
||||||
profiler_context,
|
|
||||||
profile_node_execution,
|
|
||||||
show_in_text_editor,
|
|
||||||
resolve_input
|
|
||||||
)
|
|
||||||
|
|
||||||
profiling_enabled = getattr(self, "log", False)
|
|
||||||
|
|
||||||
def _run_jobs():
|
|
||||||
for i, job in enumerate(self.inputs):
|
|
||||||
# Resolve the input to get the connected node(s)
|
|
||||||
res = resolve_input(job)
|
|
||||||
if res is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Ensure we have a list of jobs to process (JoinExecution returns a list)
|
|
||||||
jobs_to_run = res if isinstance(res, list) else [res]
|
|
||||||
|
|
||||||
for upstream_node in jobs_to_run:
|
|
||||||
if not hasattr(upstream_node, "evaluate"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if operator:
|
|
||||||
operator.report({'INFO'}, f"Executing: {upstream_node.bl_label}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with execution_cache():
|
|
||||||
with profile_node_execution(upstream_node):
|
|
||||||
from ....infra.blender.blender_slice_nodes_generic import NodeErrorReportingProxy
|
|
||||||
wrapped_op = NodeErrorReportingProxy(operator, upstream_node)
|
|
||||||
exec_res = upstream_node.evaluate(operator=wrapped_op)
|
|
||||||
if exec_res != 'ok' and exec_res is not None:
|
|
||||||
if operator: operator.report({'WARNING'}, f"Node {upstream_node.bl_label} returned {exec_res}")
|
|
||||||
except Exception as e:
|
|
||||||
from ....infra.blender.blender_slice_nodes_generic import attach_node_failure_info
|
|
||||||
attach_node_failure_info(upstream_node, e)
|
|
||||||
if operator: operator.report({'ERROR'}, str(e))
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
return 'ok'
|
|
||||||
|
|
||||||
if profiling_enabled:
|
|
||||||
with profiler_context() as profiler:
|
|
||||||
result = _run_jobs()
|
|
||||||
table_str = profiler.format_table()
|
|
||||||
show_in_text_editor("Performance", table_str)
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
return _run_jobs()
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_JoinSettings(SliceNodeJoiner):
|
|
||||||
bl_label = "Join Settings"
|
|
||||||
socket_type = "SLICESOCK_Settings"
|
|
||||||
output_socket_type = "SLICESOCK_SettingsRo"
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> dict[str, str]:
|
|
||||||
from ....infra.blender.blender_slice_nodes_generic import resolve_input
|
|
||||||
merged: dict[str, str] = {}
|
|
||||||
for input in self.inputs:
|
|
||||||
val = resolve_input(input)
|
|
||||||
if isinstance(val, dict):
|
|
||||||
merged.update(val)
|
|
||||||
return merged
|
|
||||||
|
|
||||||
@register_class
|
|
||||||
class SLICENODE_Test(ExecSliceNode):
|
|
||||||
bl_label = "Test"
|
|
||||||
|
|
||||||
button_label = "Test"
|
|
||||||
|
|
||||||
def init(self, context):
|
|
||||||
self._init_exec()
|
|
||||||
self.add_input("NodeSocketString")
|
|
||||||
|
|
||||||
def evaluate(self, operator=None):
|
|
||||||
_ = self.resolve("Gcode")
|
|
||||||
return 'ok'
|
|
||||||
18
core/nodeslicer/nodes/system/test.py
Normal file
18
core/nodeslicer/nodes/system/test.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .....registry import register_class
|
||||||
|
from ..base import ExecSliceNode
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_Test(ExecSliceNode):
|
||||||
|
bl_label = "Test"
|
||||||
|
|
||||||
|
button_label = "Test"
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self._init_exec()
|
||||||
|
self.add_input("NodeSocketString")
|
||||||
|
|
||||||
|
def evaluate(self, operator=None):
|
||||||
|
_ = self.resolve("Gcode")
|
||||||
|
return 'ok'
|
||||||
|
|
@ -719,14 +719,12 @@ class SLICENODE_OT_Execute(Operator):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with execution_cache():
|
with execution_cache():
|
||||||
from ...infra.blender.blender_slice_nodes_generic import NodeErrorReportingProxy
|
from ...infra.error_reporting import node_execution_scope
|
||||||
wrapped_op = NodeErrorReportingProxy(self, node)
|
with node_execution_scope(node, operator=self) as scope:
|
||||||
res = node.evaluate(operator=wrapped_op)
|
res = node.evaluate(operator=scope.operator)
|
||||||
if res == 'ok':
|
if res == 'ok':
|
||||||
self.report({"INFO"}, "Execution Successful")
|
self.report({"INFO"}, "Execution Successful")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from ...infra.blender.blender_slice_nodes_generic import attach_node_failure_info
|
|
||||||
attach_node_failure_info(node, e)
|
|
||||||
self.report({"ERROR"}, str(e))
|
self.report({"ERROR"}, str(e))
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,6 @@ from .metadata import (
|
||||||
from .pdf_utils import (
|
from .pdf_utils import (
|
||||||
PdfStream,
|
PdfStream,
|
||||||
escape_pdf_text,
|
escape_pdf_text,
|
||||||
pdf_set_graphics_state,
|
|
||||||
pdf_set_line_width,
|
|
||||||
pdf_set_line_cap_join,
|
|
||||||
pdf_set_rgb_color,
|
|
||||||
pdf_set_font,
|
|
||||||
pdf_text_begin,
|
|
||||||
pdf_text_end,
|
|
||||||
pdf_text_show,
|
|
||||||
)
|
)
|
||||||
from .sa2_measures import (
|
from .sa2_measures import (
|
||||||
circumcircle_3pts,
|
circumcircle_3pts,
|
||||||
|
|
@ -76,14 +68,6 @@ __all__ = [
|
||||||
"PdfStream",
|
"PdfStream",
|
||||||
# PDF utilities
|
# PDF utilities
|
||||||
"escape_pdf_text",
|
"escape_pdf_text",
|
||||||
"pdf_set_graphics_state",
|
|
||||||
"pdf_set_line_width",
|
|
||||||
"pdf_set_line_cap_join",
|
|
||||||
"pdf_set_rgb_color",
|
|
||||||
"pdf_set_font",
|
|
||||||
"pdf_text_begin",
|
|
||||||
"pdf_text_end",
|
|
||||||
"pdf_text_show",
|
|
||||||
# Geometry
|
# Geometry
|
||||||
"deduplicate_2d_edges",
|
"deduplicate_2d_edges",
|
||||||
"extract_unique_top_view_edges",
|
"extract_unique_top_view_edges",
|
||||||
|
|
|
||||||
|
|
@ -35,140 +35,6 @@ def escape_pdf_text(text: str) -> str:
|
||||||
return text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
|
return text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Standalone PDF Operator Helpers (kept for backward compatibility)
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
def pdf_move_to(x_pt: float, y_pt: float) -> str:
|
|
||||||
"""Generate PDF move-to operator."""
|
|
||||||
return f"{x_pt:.2f} {y_pt:.2f} m"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_line_to(x_pt: float, y_pt: float) -> str:
|
|
||||||
"""Generate PDF line-to operator."""
|
|
||||||
return f"{x_pt:.2f} {y_pt:.2f} l"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_move_line(x1_pt: float, y1_pt: float, x2_pt: float, y2_pt: float) -> str:
|
|
||||||
"""Generate PDF move-to and line-to sequence."""
|
|
||||||
return f"{x1_pt:.2f} {y1_pt:.2f} m {x2_pt:.2f} {y2_pt:.2f} l"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_stroke() -> str:
|
|
||||||
"""Generate PDF stroke operator."""
|
|
||||||
return "S"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_text_show(text: str) -> str:
|
|
||||||
"""Generate PDF text show operator with escaped text."""
|
|
||||||
return f"({escape_pdf_text(text)}) Tj"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_text_begin() -> str:
|
|
||||||
"""Generate PDF begin text operator."""
|
|
||||||
return "BT"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_text_end() -> str:
|
|
||||||
"""Generate PDF end text operator."""
|
|
||||||
return "ET"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_set_font(font_name: str, size_pt: float) -> str:
|
|
||||||
"""Generate PDF font selection operator."""
|
|
||||||
return f"/{font_name} {size_pt:.1f} Tf"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_set_rgb_color(r: float, g: float, b: float, fill: bool = True) -> str:
|
|
||||||
"""
|
|
||||||
Generate PDF color setting operator.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
r: Red component (0.0-1.0)
|
|
||||||
g: Green component (0.0-1.0)
|
|
||||||
b: Blue component (0.0-1.0)
|
|
||||||
fill: True for fill color (rg), False for stroke color (RG)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
PDF color operator string.
|
|
||||||
"""
|
|
||||||
color_op = "rg" if fill else "RG"
|
|
||||||
return f"{r:.3f} {g:.3f} {b:.3f} {color_op}"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_set_line_width(width_pt: float) -> str:
|
|
||||||
"""Generate PDF line width operator."""
|
|
||||||
return f"{width_pt:.2f} w"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_set_line_cap_join(cap: int = 1, join: int = 1) -> str:
|
|
||||||
"""
|
|
||||||
Generate PDF line cap and join style operators.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cap: 0=butt, 1=round, 2=projecting square
|
|
||||||
join: 0=miter, 1=round, 2=bevel
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
PDF line style operator string.
|
|
||||||
"""
|
|
||||||
return f"{cap} j {join} J"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_set_graphics_state(name: str) -> str:
|
|
||||||
"""Generate PDF graphics state selection operator."""
|
|
||||||
return f"/{name} gs"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_transform_matrix(a: float, b: float, c: float, d: float, e: float, f: float) -> str:
|
|
||||||
"""Generate PDF transformation matrix operator."""
|
|
||||||
return f"{a:.2f} {b:.2f} {c:.2f} {d:.2f} {e:.2f} {f:.2f} Tm"
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_arrowhead(
|
|
||||||
tip_x_pt: float,
|
|
||||||
tip_y_pt: float,
|
|
||||||
direction_x: float,
|
|
||||||
direction_y: float,
|
|
||||||
arrow_len_pt: float,
|
|
||||||
arrow_half_w_pt: float,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Generate PDF commands for an arrowhead.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tip_x_pt: X coordinate of arrow tip
|
|
||||||
tip_y_pt: Y coordinate of arrow tip
|
|
||||||
direction_x: X direction vector (normalized)
|
|
||||||
direction_y: Y direction vector (normalized)
|
|
||||||
arrow_len_pt: Length of arrow in points
|
|
||||||
arrow_half_w_pt: Half-width of arrow in points
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
PDF commands to draw the arrowhead.
|
|
||||||
"""
|
|
||||||
# Arrow base (opposite of tip direction)
|
|
||||||
base_x = tip_x_pt - direction_x * arrow_len_pt
|
|
||||||
base_y = tip_y_pt - direction_y * arrow_len_pt
|
|
||||||
|
|
||||||
# Perpendicular vectors for arrow wings
|
|
||||||
perp_x = direction_y * arrow_half_w_pt
|
|
||||||
perp_y = -direction_x * arrow_half_w_pt
|
|
||||||
|
|
||||||
# Left wing
|
|
||||||
left_x = base_x + perp_x
|
|
||||||
left_y = base_y + perp_y
|
|
||||||
|
|
||||||
# Right wing
|
|
||||||
right_x = base_x - perp_x
|
|
||||||
right_y = base_y - perp_y
|
|
||||||
|
|
||||||
return (
|
|
||||||
f"{tip_x_pt:.2f} {tip_y_pt:.2f} m "
|
|
||||||
f"{left_x:.2f} {left_y:.2f} l "
|
|
||||||
f"{right_x:.2f} {right_y:.2f} l S"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Color type alias
|
# Color type alias
|
||||||
|
|
|
||||||
|
|
@ -46,14 +46,29 @@ This file provides a mapping of modules to their primary responsibilities and ke
|
||||||
- `geometry.py`:
|
- `geometry.py`:
|
||||||
- Geometry utility functions.
|
- Geometry utility functions.
|
||||||
|
|
||||||
|
## `/core/functions` (Pure Logic & Reusable Functions)
|
||||||
|
|
||||||
|
- `mesh_functions.py`: Reusable mesh transformations, deep copying, attribute evaluation, naming templates, and PDF property assignment.
|
||||||
|
- `path_functions.py`: Pure-logic slicing functions (`_slice_single_mesh_item`, `_finish_slice_paths`, `_slice_single_mesh_item_with_layers`).
|
||||||
|
- `gcode_functions.py`: G-code generation, metadata formatting, regex replacement, command formatting, join logic, and file loading.
|
||||||
|
- `math_functions.py`: Math operations lookup and evaluation for float and int math nodes.
|
||||||
|
- `preview_functions.py`: G-code parsing logic extracting vertices, edges, extrusions, and preparation metadata.
|
||||||
|
|
||||||
## `/core/nodeslicer` (Node Definitions & Tree Infrastructure)
|
## `/core/nodeslicer` (Node Definitions & Tree Infrastructure)
|
||||||
|
|
||||||
- `tree.py`:
|
- `tree.py`:
|
||||||
- `SLICETREE_NodeTree`: Custom node tree type for NodeSlicer nodes.
|
- `SLICETREE_NodeTree`: Custom node tree type for NodeSlicer nodes.
|
||||||
|
|
||||||
- `nodes/path.py`:
|
- `nodes/`:
|
||||||
- `SLICENODE_Layers`: Generates bounded list of layer slicing heights.
|
- `base.py`: Base classes (`SliceNode`, `SliceNodeJoiner`, `SliceNodeListJoiner`, `ExecSliceNode`), geometry wrappers, and evaluation utilities.
|
||||||
- `SLICENODE_SliceShellsLayers`: Slices meshes using an explicit list of layer heights for multi-object layer alignment.
|
- `inputs/`: Individual input node modules (`input_string`, `input_float`, `input_int`, `input_vector`, `input_vector_rotation`, `input_object`, `objects_to_list`, `collection_objects`, `input_settings`, `input_printer`, `input_intrinsics`).
|
||||||
|
- `math/`: Math node modules (`float_math`, `int_math`).
|
||||||
|
- `mesh/`: Mesh processing node modules (`arrange_mesh`, `assign_pdf_properties`, `assign_property_to_object`, `drop_geometry`, `get_mesh`, `get_transforms`, `group_to_list`, `join_mesh_groups`, `join_mesh`, `list_to_group`, `meshes_to_list`, `rename_mesh`, `separate_mesh_by_range`, `separate_mesh`, `transform_geometry`).
|
||||||
|
- `path/`: Slicing and toolpath generation node modules (`anchor_paths`, `buffer_curve`, `buffer_shells`, `buffer_shells_multiple`, `flatten_path_list`, `get_paths`, `infill`, `layers`, `offset_curve`, `overhang_mask`, `path_offset`, `path_operations`, `paths_to_list`, `rotate_path`, `seam_placement`, `simplify_paths`, `slice_shells`, `slice_shells_layers`).
|
||||||
|
- `gcode/`: G-code operations and generation node modules (`export_gcode`, `gcode_find_replace`, `gcode_to_list`, `gcode_to_path`, `join_gcode`, `load_gcode`, `modify_gcode`, `paths_to_gcode`).
|
||||||
|
- `output/`: Output and external export node modules (`airtable_submit`, `baserow_submit`, `gcode_to_file`, `gcode_to_text_block`, `mesh_to_pdf`, `start_print`).
|
||||||
|
- `system/`: Execution and flow control node modules (`executor`, `join_execution`, `join_settings`, `printer_list`, `test`).
|
||||||
|
- `preview/`: G-code preview and visualization node modules (`gcode_to_mesh`).
|
||||||
|
|
||||||
## `/core/pdf_exporter` (PDF Export & Technical Marking Engine)
|
## `/core/pdf_exporter` (PDF Export & Technical Marking Engine)
|
||||||
|
|
||||||
|
|
@ -64,7 +79,17 @@ This file provides a mapping of modules to their primary responsibilities and ke
|
||||||
- `export_mesh_to_pdf`: Class for building scalable 1:1 vector PDF files. Uses a modular element architecture with element subclasses (`PDFElement`, `PDFLegendElement`, `PDFHeaderTableElement`, `PDFGridElement`, `PDFGeometryElement`, `PDFBoundingBoxElement`, `PDFSA2MeasuresElement`, `PDFObjectLabelsElement`, `PDFBackgroundElement`).
|
- `export_mesh_to_pdf`: Class for building scalable 1:1 vector PDF files. Uses a modular element architecture with element subclasses (`PDFElement`, `PDFLegendElement`, `PDFHeaderTableElement`, `PDFGridElement`, `PDFGeometryElement`, `PDFBoundingBoxElement`, `PDFSA2MeasuresElement`, `PDFObjectLabelsElement`, `PDFBackgroundElement`).
|
||||||
- `format_pdf_filename_template`: Formats file naming templates with placeholders for object names, whole PDF dimensions in mm (`{size_x_mm}`, `{size_y_mm}`), and ISO dates (`{today_date_iso}`).
|
- `format_pdf_filename_template`: Formats file naming templates with placeholders for object names, whole PDF dimensions in mm (`{size_x_mm}`, `{size_y_mm}`), and ISO dates (`{today_date_iso}`).
|
||||||
|
|
||||||
## `/infra` (Blender Integration)
|
## `/infra` (Blender Integration & Infrastructure)
|
||||||
|
|
||||||
|
- `error_reporting/`:
|
||||||
|
- `formatter.py`: Node description and socket introspection (`get_node_input_names`, `format_node_failure_message`, `_format_node_description`).
|
||||||
|
- `proxy.py`: Operator error reporting proxy (`NodeErrorReportingProxy`, `wrap_operator_for_node`).
|
||||||
|
- `handler.py`: Modular error handler (`attach_node_failure_info`, `NodeErrorHandler`, `handle_node_errors`, `node_execution_scope`).
|
||||||
|
|
||||||
|
- `profiling/`:
|
||||||
|
- `node_profiler.py`: Core profiler class (`NodeProfiler`, `get_node_type`, global profiler tracking).
|
||||||
|
- `execution.py`: Modular profiling execution context and decorator (`profiler_context`, `profile_node_execution`, `NodeProfileExecution`).
|
||||||
|
- `text_editor.py`: Display profiler table in Blender Text Editor (`show_in_text_editor`).
|
||||||
|
|
||||||
- `blender_mesh_capture.py`:
|
- `blender_mesh_capture.py`:
|
||||||
- `PdfProperties`: Dataclass holding PDF line width and line color attributes for a `SlicingObject`.
|
- `PdfProperties`: Dataclass holding PDF line width and line color attributes for a `SlicingObject`.
|
||||||
|
|
@ -74,8 +99,7 @@ This file provides a mapping of modules to their primary responsibilities and ke
|
||||||
- `blender_slice_nodes_generic.py`:
|
- `blender_slice_nodes_generic.py`:
|
||||||
- Base classes for NodeSlicer nodes (`SliceNode`) and sockets (`SliceNodeSocket`).
|
- Base classes for NodeSlicer nodes (`SliceNode`) and sockets (`SliceNodeSocket`).
|
||||||
- Native Blender socket type mappings (`NodeSocketFloat`, `NodeSocketInt`, `NodeSocketVector`, `NodeSocketRotation`, `NodeSocketString`, `NodeSocketObject`, `NodeSocketGeometry`).
|
- Native Blender socket type mappings (`NodeSocketFloat`, `NodeSocketInt`, `NodeSocketVector`, `NodeSocketRotation`, `NodeSocketString`, `NodeSocketObject`, `NodeSocketGeometry`).
|
||||||
- Core evaluation logic (`resolve_input`).
|
- Core evaluation logic (`resolve_input`, `_resolve_output`).
|
||||||
- Profiling tools (`NodeProfiler`, `profile_node_execution`).
|
|
||||||
|
|
||||||
- `debug.py`:
|
- `debug.py`:
|
||||||
- `@debug`: Decorator for Node classes, `evaluate_output` methods, or functions to dump execution state, input sockets, properties, outputs, and errors.
|
- `@debug`: Decorator for Node classes, `evaluate_output` methods, or functions to dump execution state, input sockets, properties, outputs, and errors.
|
||||||
|
|
|
||||||
111
doc/MODULES.sync-conflict-20261015-141222-M62EUDG.md
Normal file
111
doc/MODULES.sync-conflict-20261015-141222-M62EUDG.md
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
# Module Reference
|
||||||
|
|
||||||
|
This file provides a mapping of modules to their primary responsibilities and key symbols.
|
||||||
|
|
||||||
|
## Root
|
||||||
|
|
||||||
|
- `__init__.py`:
|
||||||
|
- **Entry Point**: Extension registration (`register`, `unregister`).
|
||||||
|
- **Node Tree**: `SLICETREE_NodeTree` class.
|
||||||
|
- **Sockets**: Definitions for all `SLICESOCK_*` classes.
|
||||||
|
- **Nodes**: Definitions for all `SLICENODE_*` classes.
|
||||||
|
- **Evaluation Engine**:
|
||||||
|
- `resolve_input()`: Recursively evaluates linked sockets.
|
||||||
|
- `_resolve_output()`: Computes node outputs with a caching system (`_EvalCache`) to avoid redundant calculations.
|
||||||
|
- `_follow_reroute()`: Handles Blender's Reroute nodes during evaluation.
|
||||||
|
|
||||||
|
- `registry.py`:
|
||||||
|
- **Auto-Registration**: Utility to track classes decorated with `@register_class`.
|
||||||
|
|
||||||
|
- `blender_manifest.toml`:
|
||||||
|
- Metadata for the Blender Extension platform.
|
||||||
|
|
||||||
|
- `pyproject.toml`:
|
||||||
|
- Standard Python project configuration.
|
||||||
|
|
||||||
|
## `/core` (Geometry & Slicing)
|
||||||
|
|
||||||
|
- `slice.py`:
|
||||||
|
- `slice_at_range()`: Top-level slicing function that iterates through Z-heights using `manifold3d` for guaranteed watertight solid slicing (resolving coplanar and floating-point degeneracies) with a fallback to `trimesh` multiplane slicing for open sheets (supports `ignore_holes` option).
|
||||||
|
- `_slice_single_mesh()`: Mesh slicer implementing `manifold3d` solid slicing and `trimesh` multiplane intersection with micro-epsilon perturbation at boundary faces.
|
||||||
|
- `align_path_seam_sharpest()`: High-performance vectorized heuristic that positions the start vertex (seam) of closed polygon loops at the back of the object (+Y) with corner sharpness preference.
|
||||||
|
- `SlicePaths.align_seams()`: Method on `SlicePaths` to align path seams to the back (+Y) while preserving per-vertex attributes and metadata.
|
||||||
|
- `Pather`: Class that converts toolpaths into G-code strings. Supports per-segment extrusion rates, feedrates, and custom extra attributes.
|
||||||
|
- `segments_to_slice()`: Utility to convert raw intersection segments into continuous paths (closed polygons are processed via Pyclipper, supports `ignore_holes`, open paths remain open).
|
||||||
|
- `link_segments()`: Logic for joining loose segments into continuous paths.
|
||||||
|
- `buffer_slice()`: Offset utility using PyclipperOffset. Supports rotated offsetting.
|
||||||
|
- `buffer_multiple_slice()`: High-performance single-pass multi-shell concentric offset utility for slicing layers.
|
||||||
|
- `infill_slice()`: Generates rotated grid or rectilinear infill patterns, ensuring layer-to-layer consistency.
|
||||||
|
- `boolean_slice()`: Performs 2D boolean operations (Union, Intersection, Subtract, XOR) on slice sets with hole support.
|
||||||
|
- `path_operations_slice()`: Unified dispatcher for multi-input path operations (`ZIP`, `EXTEND`, `MAX_COVERAGE`, `MIN_COVERAGE`, `SUBTRACT`, `XOR`). Supports `acquire_transforms` to align secondary paths with primary transforms.
|
||||||
|
- `apply_transforms_to_slice_paths()`: Applies a list of transformation dicts (scale, rotation, translation) to a `SlicePaths` object.
|
||||||
|
- `extract_geometry_transforms()`: Consolidates transformation metadata from `SlicePaths`, `SlicingObject`, `SlicingCollection`, `SlicingGroup`, or Blender objects into translation, rotation (radians), and scale tuples.
|
||||||
|
- `minimum_coverage_slice()`: Computes layer-by-layer intersection across multiple inputs while preserving single-path/input layers.
|
||||||
|
- `maximum_coverage_slice()`: Computes layer-by-layer union across multiple inputs.
|
||||||
|
|
||||||
|
- `geometry.py`:
|
||||||
|
- Geometry utility functions.
|
||||||
|
|
||||||
|
## `/core/nodeslicer` (Node Definitions & Tree Infrastructure)
|
||||||
|
|
||||||
|
- `tree.py`:
|
||||||
|
- `SLICETREE_NodeTree`: Custom node tree type for NodeSlicer nodes.
|
||||||
|
|
||||||
|
- `nodes/path.py`:
|
||||||
|
- `SLICENODE_Layers`: Generates bounded list of layer slicing heights.
|
||||||
|
- `SLICENODE_SliceShellsLayers`: Slices meshes using an explicit list of layer heights for multi-object layer alignment.
|
||||||
|
|
||||||
|
## `/core/pdf_exporter` (PDF Export & Technical Marking Engine)
|
||||||
|
|
||||||
|
- `sa2_measures.py`:
|
||||||
|
- `render_sa2_measures_pdf()`: ISO 129-1 technical marking engine.
|
||||||
|
- `extract_slicing_object_features()`: Feature detection for circular bores (Ø), arcs (R), chamfers (C), and steps.
|
||||||
|
- `writer.py`:
|
||||||
|
- `export_mesh_to_pdf`: Class for building scalable 1:1 vector PDF files. Uses a modular element architecture with element subclasses (`PDFElement`, `PDFLegendElement`, `PDFHeaderTableElement`, `PDFGridElement`, `PDFGeometryElement`, `PDFBoundingBoxElement`, `PDFSA2MeasuresElement`, `PDFObjectLabelsElement`, `PDFBackgroundElement`).
|
||||||
|
- `format_pdf_filename_template`: Formats file naming templates with placeholders for object names, whole PDF dimensions in mm (`{size_x_mm}`, `{size_y_mm}`), and ISO dates (`{today_date_iso}`).
|
||||||
|
|
||||||
|
## `/infra` (Blender Integration)
|
||||||
|
|
||||||
|
- `blender_mesh_capture.py`:
|
||||||
|
- `PdfProperties`: Dataclass holding PDF line width and line color attributes for a `SlicingObject`.
|
||||||
|
- `SlicingObject`: Represents a single mesh as a NumPy triangle array.
|
||||||
|
- `capture_mesh_edges()`: Extracts edges from a Blender mesh for path-based workflows. Supports capturing extra per-vertex attributes.
|
||||||
|
|
||||||
|
- `blender_slice_nodes_generic.py`:
|
||||||
|
- Base classes for NodeSlicer nodes (`SliceNode`) and sockets (`SliceNodeSocket`).
|
||||||
|
- Native Blender socket type mappings (`NodeSocketFloat`, `NodeSocketInt`, `NodeSocketVector`, `NodeSocketRotation`, `NodeSocketString`, `NodeSocketObject`, `NodeSocketGeometry`).
|
||||||
|
- Core evaluation logic (`resolve_input`, `resolve_multi_input`).
|
||||||
|
- Implicit element-wise multivalue evaluation and draw-time socket visual synchronization.
|
||||||
|
|
||||||
|
- `multivalue.py`:
|
||||||
|
- `evaluate_node_multivalue()`: Transparent execution engine that broadcasts input lists across single-element `evaluate_output` implementations.
|
||||||
|
|
||||||
|
- `node_profiler.py`:
|
||||||
|
- `NodeProfiler`, `profile_node_execution`, `profiler_context`: Execution timing and profiling tools for node evaluation.
|
||||||
|
|
||||||
|
- `node_errors.py`:
|
||||||
|
- `format_node_failure_message()`, `attach_node_failure_info()`, `NodeErrorReportingProxy`: Error reporting proxy and diagnostics for node evaluation failures.
|
||||||
|
|
||||||
|
- `node_groups.py`:
|
||||||
|
- `SLICENODE_Group`: Node group instance evaluation.
|
||||||
|
- `execution_cache`: Node group execution caching and reroute traversal.
|
||||||
|
|
||||||
|
- `node_base.py`:
|
||||||
|
- `AutoBlIdName`: Metaclass ensuring consistent `bl_idname` naming across node and socket definitions.
|
||||||
|
|
||||||
|
- `debug.py`:
|
||||||
|
- `@debug`: Decorator for Node classes, `evaluate_output` methods, or functions to dump execution state, input sockets, properties, outputs, and errors.
|
||||||
|
- `dump_node_execution()`: Serializes node execution state to JSON, formatted text report, and Python pickle.
|
||||||
|
- `get_last_debug_dump()` / `get_last_debug_text()` / `load_last_inputs()`: Utilities to review and reload the most recent execution dump for debugging.
|
||||||
|
- `debug_session()`: Context manager enabling execution capture across node evaluations.
|
||||||
|
- `SLICENODE_OT_DebugDump`: Blender operator (`slicenode.debug_dump`) to dump the active node's inputs on demand.
|
||||||
|
|
||||||
|
- `list_utils.py`:
|
||||||
|
- `is_list_data()`: Type discrimination distinguishing list/tuple collections from atomic objects (such as `SlicePaths`).
|
||||||
|
- `is_vector_value()`: Distinguishes atomic 3D vectors/rotations from lists of vectors.
|
||||||
|
- `is_input_a_list()`: Determines if a raw input is a list, accounting for vector socket types.
|
||||||
|
- `flatten_list()`: Recursive flattener for multi-input joiners and batch sinks.
|
||||||
|
- `eval_element_wise()`: Vectorized applicator executing operations element-wise on lists or directly on single items.
|
||||||
|
- `prepare_indexed_inputs()`: Normalizes and matches secondary inputs index-per-index against the primary input, broadcasting single values or validating that list lengths match exactly (raising `ValueError` on mismatch).
|
||||||
|
- `eval_element_wise_multivalue()`: General element-wise operator executing multi-argument operations with index matching and broadcasting.
|
||||||
|
- `set_socket_shape()` / `is_socket_linked_to_list()` / `is_output_socket_a_list()` / `sync_node_list_sockets()` / `sync_tree_socket_shapes()`: Dynamic socket visual appearance management (`LIST` vs `CIRCLE`) and tree-wide propagation.
|
||||||
6
functions/__init__.py
Normal file
6
functions/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
"""Top-level convenience alias for core.functions."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
from ..core import functions as _core_functions
|
||||||
|
|
||||||
|
sys.modules[__name__] = _core_functions
|
||||||
|
|
@ -273,152 +273,13 @@ class SliceNode(Node, AutoBlIdName):
|
||||||
|
|
||||||
_current_execution_cache: _EvalCache | None = None
|
_current_execution_cache: _EvalCache | None = None
|
||||||
|
|
||||||
def get_node_type(node: Any) -> str:
|
from ..profiling import (
|
||||||
if hasattr(node, "bl_label") and node.bl_label:
|
NodeProfiler,
|
||||||
return str(node.bl_label)
|
get_node_type,
|
||||||
if hasattr(node, "bl_idname") and node.bl_idname:
|
profiler_context,
|
||||||
return str(node.bl_idname)
|
profile_node_execution,
|
||||||
return type(node).__name__
|
show_in_text_editor,
|
||||||
|
)
|
||||||
class NodeProfiler:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
# Maps node_type -> [execution_count, total_self_time_seconds]
|
|
||||||
self.stats: dict[str, list[float]] = {}
|
|
||||||
self._stack: list[dict[str, Any]] = []
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def profile_node(self, node: Any):
|
|
||||||
node_type = get_node_type(node)
|
|
||||||
now = time.perf_counter()
|
|
||||||
|
|
||||||
# Pause parent node's self-timer
|
|
||||||
if self._stack:
|
|
||||||
parent = self._stack[-1]
|
|
||||||
parent["self_time"] += (now - parent["start_time"])
|
|
||||||
|
|
||||||
frame = {
|
|
||||||
"node_type": node_type,
|
|
||||||
"start_time": now,
|
|
||||||
"self_time": 0.0,
|
|
||||||
}
|
|
||||||
self._stack.append(frame)
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
end_time = time.perf_counter()
|
|
||||||
popped = self._stack.pop()
|
|
||||||
total_node_self_time = popped["self_time"] + (end_time - popped["start_time"])
|
|
||||||
|
|
||||||
if node_type not in self.stats:
|
|
||||||
self.stats[node_type] = [0.0, 0.0]
|
|
||||||
self.stats[node_type][0] += 1.0
|
|
||||||
self.stats[node_type][1] += total_node_self_time
|
|
||||||
|
|
||||||
# Resume parent node's timer
|
|
||||||
if self._stack:
|
|
||||||
self._stack[-1]["start_time"] = time.perf_counter()
|
|
||||||
|
|
||||||
def format_table(self) -> str:
|
|
||||||
headers = ["node_type", "execution_count", "avg_execution_time", "tot_execution_time"]
|
|
||||||
|
|
||||||
if not self.stats:
|
|
||||||
col_widths = [20, 15, 18, 18]
|
|
||||||
header_line = " | ".join(headers[i].ljust(col_widths[i]) for i in range(4))
|
|
||||||
separator_line = "-+-".join("-" * col_widths[i] for i in range(4))
|
|
||||||
return f"{header_line}\n{separator_line}\n(No nodes executed)"
|
|
||||||
|
|
||||||
sorted_stats = sorted(self.stats.items(), key=lambda item: item[1][1], reverse=True)
|
|
||||||
|
|
||||||
rows: list[list[str]] = []
|
|
||||||
tot_count = 0
|
|
||||||
tot_time_all = 0.0
|
|
||||||
|
|
||||||
for node_type, (count, tot_time) in sorted_stats:
|
|
||||||
icount = int(count)
|
|
||||||
tot_count += icount
|
|
||||||
tot_time_all += tot_time
|
|
||||||
avg_time = tot_time / icount if icount > 0 else 0.0
|
|
||||||
rows.append([
|
|
||||||
node_type,
|
|
||||||
str(icount),
|
|
||||||
f"{avg_time:.6f}s",
|
|
||||||
f"{tot_time:.6f}s",
|
|
||||||
])
|
|
||||||
|
|
||||||
col_widths = [
|
|
||||||
max(len(headers[i]), max(len(row[i]) for row in rows))
|
|
||||||
for i in range(4)
|
|
||||||
]
|
|
||||||
col_widths[0] = max(col_widths[0], len("Total"))
|
|
||||||
col_widths[1] = max(col_widths[1], len(str(tot_count)))
|
|
||||||
col_widths[3] = max(col_widths[3], len(f"{tot_time_all:.6f}s"))
|
|
||||||
|
|
||||||
lines: list[str] = []
|
|
||||||
header_line = " | ".join(headers[i].ljust(col_widths[i]) for i in range(4))
|
|
||||||
separator_line = "-+-".join("-" * col_widths[i] for i in range(4))
|
|
||||||
|
|
||||||
lines.append(header_line)
|
|
||||||
lines.append(separator_line)
|
|
||||||
for row in rows:
|
|
||||||
lines.append(" | ".join([
|
|
||||||
row[0].ljust(col_widths[0]),
|
|
||||||
row[1].rjust(col_widths[1]),
|
|
||||||
row[2].rjust(col_widths[2]),
|
|
||||||
row[3].rjust(col_widths[3]),
|
|
||||||
]))
|
|
||||||
lines.append(separator_line)
|
|
||||||
total_row = [
|
|
||||||
"Total".ljust(col_widths[0]),
|
|
||||||
str(tot_count).rjust(col_widths[1]),
|
|
||||||
"-".rjust(col_widths[2]),
|
|
||||||
f"{tot_time_all:.6f}s".rjust(col_widths[3]),
|
|
||||||
]
|
|
||||||
lines.append(" | ".join(total_row))
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
_current_profiler: NodeProfiler | None = None
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def profiler_context():
|
|
||||||
global _current_profiler
|
|
||||||
old = _current_profiler
|
|
||||||
profiler = NodeProfiler()
|
|
||||||
_current_profiler = profiler
|
|
||||||
try:
|
|
||||||
yield profiler
|
|
||||||
finally:
|
|
||||||
_current_profiler = old
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def profile_node_execution(node: Any):
|
|
||||||
if _current_profiler is None:
|
|
||||||
yield
|
|
||||||
return
|
|
||||||
with _current_profiler.profile_node(node):
|
|
||||||
yield
|
|
||||||
|
|
||||||
def show_in_text_editor(name: str, content: str) -> Any:
|
|
||||||
try:
|
|
||||||
text_block = bpy.data.texts.get(name)
|
|
||||||
if text_block is None:
|
|
||||||
text_block = bpy.data.texts.new(name=name)
|
|
||||||
text_block.from_string(content)
|
|
||||||
|
|
||||||
wm = getattr(bpy.context, "window_manager", None)
|
|
||||||
if wm and hasattr(wm, "windows"):
|
|
||||||
for window in wm.windows:
|
|
||||||
if hasattr(window, "screen") and window.screen and hasattr(window.screen, "areas"):
|
|
||||||
for area in window.screen.areas:
|
|
||||||
if getattr(area, "type", "") == 'TEXT_EDITOR':
|
|
||||||
for space in getattr(area, "spaces", []):
|
|
||||||
if getattr(space, "type", "") == 'TEXT_EDITOR':
|
|
||||||
space.text = text_block
|
|
||||||
return text_block
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error displaying in text editor: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def execution_cache():
|
def execution_cache():
|
||||||
|
|
@ -450,10 +311,6 @@ def _get_group_stack(cache: _EvalCache | None = None) -> list[bpy.types.Node]:
|
||||||
cache['_group_stack'] = []
|
cache['_group_stack'] = []
|
||||||
return cache['_group_stack']
|
return cache['_group_stack']
|
||||||
|
|
||||||
def _get_current_group_node(cache: _EvalCache | None = None) -> bpy.types.Node | None:
|
|
||||||
stack = _get_group_stack(cache)
|
|
||||||
return stack[-1] if stack else None
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_Group(bpy.types.NodeCustomGroup, AutoBlIdName):
|
class SLICENODE_Group(bpy.types.NodeCustomGroup, AutoBlIdName):
|
||||||
bl_label = "Group"
|
bl_label = "Group"
|
||||||
|
|
@ -637,106 +494,16 @@ def _follow_reroute(current: NodeSocket) -> NodeSocket:
|
||||||
current = reroute_input.links[0].from_socket
|
current = reroute_input.links[0].from_socket
|
||||||
|
|
||||||
return current
|
return current
|
||||||
|
from ..error_reporting import (
|
||||||
def _format_node_description(node: Any) -> str:
|
_format_node_description,
|
||||||
if node is None:
|
get_node_input_names,
|
||||||
return ""
|
format_node_failure_message,
|
||||||
name = getattr(node, "name", "")
|
attach_node_failure_info,
|
||||||
bl_label = getattr(node, "bl_label", "")
|
NodeErrorReportingProxy,
|
||||||
bl_idname = getattr(node, "bl_idname", "") or type(node).__name__
|
NodeErrorHandler,
|
||||||
|
handle_node_errors,
|
||||||
label = bl_label or bl_idname
|
node_execution_scope,
|
||||||
if name and label and name != label:
|
)
|
||||||
return f"'{name}' ({label})"
|
|
||||||
elif name:
|
|
||||||
return f"'{name}'"
|
|
||||||
elif label:
|
|
||||||
return f"'{label}'"
|
|
||||||
return str(node)
|
|
||||||
|
|
||||||
|
|
||||||
def get_node_input_names(node: Any, fallback_inputs: list[str] | None = None) -> list[str]:
|
|
||||||
"""Retrieves all input socket names for a node, preserving order without duplicates."""
|
|
||||||
names: list[str] = []
|
|
||||||
inputs = getattr(node, "inputs", None)
|
|
||||||
if inputs:
|
|
||||||
for s in inputs:
|
|
||||||
s_name = getattr(s, "name", "")
|
|
||||||
if s_name and s_name not in names:
|
|
||||||
names.append(s_name)
|
|
||||||
elif not s_name:
|
|
||||||
ident = getattr(s, "identifier", "")
|
|
||||||
if ident and ident not in names:
|
|
||||||
names.append(ident)
|
|
||||||
if not names and fallback_inputs:
|
|
||||||
for s_name in fallback_inputs:
|
|
||||||
if s_name and s_name not in names:
|
|
||||||
names.append(s_name)
|
|
||||||
if not names:
|
|
||||||
mapping = getattr(node, "list_socket_mapping", None)
|
|
||||||
if mapping:
|
|
||||||
for item in mapping:
|
|
||||||
m_in = item[0] if isinstance(item, (list, tuple)) and item else None
|
|
||||||
if m_in and m_in not in names:
|
|
||||||
names.append(m_in)
|
|
||||||
return names
|
|
||||||
|
|
||||||
|
|
||||||
def format_node_failure_message(node: Any, exc_or_msg: Any, fallback_inputs: list[str] | None = None) -> str:
|
|
||||||
"""Formats a concise, informative execution failure message specifying the node and input names."""
|
|
||||||
node_str = _format_node_description(node) or "Unknown Node"
|
|
||||||
input_names = get_node_input_names(node, fallback_inputs=fallback_inputs)
|
|
||||||
inputs_str = ", ".join(input_names) if input_names else "(None)"
|
|
||||||
msg = str(exc_or_msg)
|
|
||||||
if msg.startswith("Execution failed at node "):
|
|
||||||
return msg
|
|
||||||
return f"Execution failed at node {node_str} [Inputs: {inputs_str}]: {msg}"
|
|
||||||
|
|
||||||
|
|
||||||
def attach_node_failure_info(node: Any, exc: Exception, fallback_inputs: list[str] | None = None) -> Exception:
|
|
||||||
"""Enriches an exception in-place with the failing node and input socket names, preventing duplicate wrapping."""
|
|
||||||
if getattr(exc, "_nodeslicer_node", None) is not None:
|
|
||||||
return exc
|
|
||||||
try:
|
|
||||||
setattr(exc, "_nodeslicer_node", node)
|
|
||||||
input_names = get_node_input_names(node, fallback_inputs=fallback_inputs)
|
|
||||||
setattr(exc, "_nodeslicer_inputs", input_names)
|
|
||||||
|
|
||||||
node_str = _format_node_description(node) or "Unknown Node"
|
|
||||||
inputs_str = ", ".join(input_names) if input_names else "(None)"
|
|
||||||
prefix = f"Execution failed at node {node_str} [Inputs: {inputs_str}]: "
|
|
||||||
|
|
||||||
if exc.args:
|
|
||||||
orig_msg = str(exc.args[0])
|
|
||||||
if not orig_msg.startswith("Execution failed at node "):
|
|
||||||
exc.args = (prefix + orig_msg, *exc.args[1:])
|
|
||||||
else:
|
|
||||||
exc.args = (prefix + type(exc).__name__,)
|
|
||||||
|
|
||||||
print(f"[NodeSlicer ERROR] {str(exc)}")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return exc
|
|
||||||
|
|
||||||
|
|
||||||
class NodeErrorReportingProxy:
|
|
||||||
"""Wraps Blender operator to ensure any operator.report({'ERROR'}, ...) calls
|
|
||||||
always tell which node failed and the names of its inputs."""
|
|
||||||
def __init__(self, operator: Any, node: Any):
|
|
||||||
self._operator = operator
|
|
||||||
self._node = node
|
|
||||||
|
|
||||||
def report(self, type_set: set[str], message: str) -> None:
|
|
||||||
if self._operator and hasattr(self._operator, "report"):
|
|
||||||
if "ERROR" in type_set:
|
|
||||||
formatted_msg = format_node_failure_message(self._node, message)
|
|
||||||
print(f"[NodeSlicer ERROR] {formatted_msg}")
|
|
||||||
self._operator.report(type_set, formatted_msg)
|
|
||||||
else:
|
|
||||||
self._operator.report(type_set, message)
|
|
||||||
|
|
||||||
def __getattr__(self, name: str) -> Any:
|
|
||||||
return getattr(self._operator, name)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -969,20 +736,20 @@ def _resolve_output(
|
||||||
eval_tb = None
|
eval_tb = None
|
||||||
value = None
|
value = None
|
||||||
try:
|
try:
|
||||||
value = socket.node.evaluate_output(
|
with handle_node_errors(socket.node):
|
||||||
socket.name,
|
value = socket.node.evaluate_output(
|
||||||
identifier=socket.identifier,
|
socket.name,
|
||||||
socket=socket,
|
identifier=socket.identifier,
|
||||||
cache=cache,
|
socket=socket,
|
||||||
visited=visited
|
cache=cache,
|
||||||
)
|
visited=visited
|
||||||
|
)
|
||||||
cache[key] = value
|
cache[key] = value
|
||||||
return value
|
return value
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
eval_exc = e
|
eval_exc = e
|
||||||
import traceback
|
import traceback
|
||||||
eval_tb = traceback.format_exc()
|
eval_tb = traceback.format_exc()
|
||||||
attach_node_failure_info(socket.node, e)
|
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,7 @@ def eval_multivalue(
|
||||||
main_is_vector=main_is_vec,
|
main_is_vector=main_is_vec,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
from .blender_slice_nodes_generic import attach_node_failure_info
|
from ..error_reporting import attach_node_failure_info
|
||||||
known_inputs = ([main_socket] if main_socket else []) + (secondary_sockets or [])
|
known_inputs = ([main_socket] if main_socket else []) + (secondary_sockets or [])
|
||||||
attach_node_failure_info(node, e, fallback_inputs=known_inputs)
|
attach_node_failure_info(node, e, fallback_inputs=known_inputs)
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
211
infra/blender/multivalue.py
Normal file
211
infra/blender/multivalue.py
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any, Callable
|
||||||
|
from bpy.types import NodeSocket
|
||||||
|
from .list_utils import is_input_a_list, is_socket_vector_type, prepare_indexed_inputs
|
||||||
|
from .node_errors import attach_node_failure_info
|
||||||
|
|
||||||
|
def evaluate_node_multivalue(
|
||||||
|
node: Any,
|
||||||
|
raw_evaluate_fn: Callable[..., Any],
|
||||||
|
socket_name: str,
|
||||||
|
**kwargs: Any
|
||||||
|
) -> Any:
|
||||||
|
"""Implicitly evaluates raw_evaluate_fn element-wise across list inputs.
|
||||||
|
If no inputs are lists, executes raw_evaluate_fn once and returns the result.
|
||||||
|
If list lengths mismatch (and are not length 1 / scalar), raises ValueError.
|
||||||
|
"""
|
||||||
|
# Populate inputs if empty in unit tests
|
||||||
|
if hasattr(node, "init") and (not hasattr(node, "inputs") or len(node.inputs) == 0):
|
||||||
|
try:
|
||||||
|
node.init(None)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
inputs_col = getattr(node, "inputs", None)
|
||||||
|
inputs_list = list(inputs_col) if inputs_col is not None else []
|
||||||
|
|
||||||
|
# Check if instance has a mocked resolve method (e.g. in unit tests)
|
||||||
|
is_mocked = hasattr(node, "__dict__") and "resolve" in node.__dict__
|
||||||
|
mock_resolver = node.__dict__["resolve"] if is_mocked else None
|
||||||
|
|
||||||
|
# Step 1: Collect raw values for all input sockets
|
||||||
|
input_keys: list[Any] = []
|
||||||
|
raw_inputs: dict[Any, Any] = {}
|
||||||
|
key_to_name: dict[Any, str] = {}
|
||||||
|
name_to_keys: dict[str, list[Any]] = {}
|
||||||
|
|
||||||
|
for idx, sock in enumerate(inputs_list):
|
||||||
|
s_name = getattr(sock, "name", getattr(sock, "identifier", f"input_{idx}"))
|
||||||
|
key = sock
|
||||||
|
input_keys.append(key)
|
||||||
|
key_to_name[key] = s_name
|
||||||
|
name_to_keys.setdefault(s_name, []).append(key)
|
||||||
|
|
||||||
|
# Check list_socket_mapping for any mapped inputs not present in inputs_list
|
||||||
|
mapping = getattr(node, "list_socket_mapping", None)
|
||||||
|
if mapping:
|
||||||
|
for item in mapping:
|
||||||
|
m_in = item[0] if isinstance(item, (list, tuple)) and item else None
|
||||||
|
if m_in and m_in not in name_to_keys:
|
||||||
|
input_keys.append(m_in)
|
||||||
|
key_to_name[m_in] = m_in
|
||||||
|
name_to_keys.setdefault(m_in, []).append(m_in)
|
||||||
|
|
||||||
|
def _query_val(key: Any) -> Any:
|
||||||
|
if mock_resolver is not None:
|
||||||
|
s_name = key_to_name[key]
|
||||||
|
# If multiple sockets share the same name (e.g. MathNode), query by socket object first
|
||||||
|
if len(name_to_keys.get(s_name, [])) > 1:
|
||||||
|
try:
|
||||||
|
val = mock_resolver(key)
|
||||||
|
if val is not None:
|
||||||
|
return val
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Otherwise query by socket name
|
||||||
|
try:
|
||||||
|
val = mock_resolver(s_name)
|
||||||
|
if val is not None:
|
||||||
|
return val
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return mock_resolver(key)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return node.resolve(key)
|
||||||
|
|
||||||
|
for key in input_keys:
|
||||||
|
raw_inputs[key] = _query_val(key)
|
||||||
|
|
||||||
|
if not input_keys:
|
||||||
|
return raw_evaluate_fn(node, socket_name, **kwargs)
|
||||||
|
|
||||||
|
# Step 2: Determine list status and vector/atomic flags for each input
|
||||||
|
always_in = set(getattr(node, "always_list_inputs", []) or [])
|
||||||
|
has_any_list = False
|
||||||
|
is_vec_map: dict[Any, bool] = {}
|
||||||
|
is_list_map: dict[Any, bool] = {}
|
||||||
|
|
||||||
|
for key in input_keys:
|
||||||
|
s_name = key_to_name[key]
|
||||||
|
val = raw_inputs[key]
|
||||||
|
is_vec = is_socket_vector_type(node, s_name)
|
||||||
|
is_vec_map[key] = is_vec
|
||||||
|
is_atom = s_name in always_in
|
||||||
|
|
||||||
|
is_list = is_input_a_list(val, is_vector=is_vec)
|
||||||
|
if is_atom:
|
||||||
|
# Atomic list inputs (e.g. Layers) are only multivalue if nested
|
||||||
|
is_nested = (
|
||||||
|
isinstance(val, (list, tuple))
|
||||||
|
and len(val) > 0
|
||||||
|
and isinstance(val[0], (list, tuple))
|
||||||
|
)
|
||||||
|
is_list = is_nested
|
||||||
|
|
||||||
|
is_list_map[key] = is_list
|
||||||
|
if is_list:
|
||||||
|
has_any_list = True
|
||||||
|
|
||||||
|
# Fast path: no list inputs
|
||||||
|
if not has_any_list:
|
||||||
|
return raw_evaluate_fn(node, socket_name, **kwargs)
|
||||||
|
|
||||||
|
# Step 3: Identify main socket key
|
||||||
|
main_key: Any = None
|
||||||
|
if mapping and len(mapping) > 0 and mapping[0][0]:
|
||||||
|
mapped_name = mapping[0][0]
|
||||||
|
matching_keys = name_to_keys.get(mapped_name, [])
|
||||||
|
if matching_keys:
|
||||||
|
main_key = matching_keys[0]
|
||||||
|
|
||||||
|
if main_key is None or main_key not in raw_inputs:
|
||||||
|
# Pick the first key that is a list
|
||||||
|
main_key = next((k for k in input_keys if is_list_map.get(k, False)), input_keys[0])
|
||||||
|
|
||||||
|
main_val = raw_inputs[main_key]
|
||||||
|
main_is_vec = is_vec_map.get(main_key, False)
|
||||||
|
main_name = key_to_name[main_key]
|
||||||
|
|
||||||
|
# Prepare secondary inputs
|
||||||
|
sec_keys = [k for k in input_keys if k != main_key]
|
||||||
|
sec_names = [key_to_name[k] for k in sec_keys]
|
||||||
|
sec_vals = [raw_inputs[k] for k in sec_keys]
|
||||||
|
sec_vec_flags = [is_vec_map[k] for k in sec_keys]
|
||||||
|
sec_atom_flags = [key_to_name[k] in always_in for k in sec_keys]
|
||||||
|
|
||||||
|
try:
|
||||||
|
main_is_list, main_items, sec_broadcasted = prepare_indexed_inputs(
|
||||||
|
main_val,
|
||||||
|
*sec_vals,
|
||||||
|
is_vector=sec_vec_flags,
|
||||||
|
input_names=sec_names,
|
||||||
|
is_atomic=sec_atom_flags,
|
||||||
|
main_is_vector=main_is_vec,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
attach_node_failure_info(node, e, fallback_inputs=[key_to_name[k] for k in input_keys])
|
||||||
|
raise
|
||||||
|
|
||||||
|
n = len(main_items)
|
||||||
|
if n == 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Map broadcasted lists back to keys
|
||||||
|
broadcasted_by_key: dict[Any, list[Any]] = {main_key: main_items}
|
||||||
|
for idx, sec_k in enumerate(sec_keys):
|
||||||
|
broadcasted_by_key[sec_k] = sec_broadcasted[idx]
|
||||||
|
|
||||||
|
# Step 4: Element-wise execution
|
||||||
|
results: list[Any] = []
|
||||||
|
node._in_multivalue_eval = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
for i in range(n):
|
||||||
|
elem_ctx: dict[Any, Any] = {}
|
||||||
|
for key in input_keys:
|
||||||
|
b_val = broadcasted_by_key[key][i]
|
||||||
|
elem_ctx[key] = b_val
|
||||||
|
# Also index by string name and identifier
|
||||||
|
s_name = key_to_name[key]
|
||||||
|
# If multiple sockets have the same name, the first one set in elem_ctx is default,
|
||||||
|
# but socket-object indexing disambiguates.
|
||||||
|
if s_name not in elem_ctx:
|
||||||
|
elem_ctx[s_name] = b_val
|
||||||
|
if hasattr(key, "identifier"):
|
||||||
|
elem_ctx[key.identifier] = b_val
|
||||||
|
|
||||||
|
node._eval_iteration_ctx = elem_ctx
|
||||||
|
|
||||||
|
if is_mocked:
|
||||||
|
# Intercept mocked resolver to return element values
|
||||||
|
def _mock_elem_resolve(label, _ctx=elem_ctx, _orig=mock_resolver):
|
||||||
|
if label in _ctx:
|
||||||
|
return _ctx[label]
|
||||||
|
name = getattr(label, "name", None)
|
||||||
|
if name in _ctx:
|
||||||
|
return _ctx[name]
|
||||||
|
ident = getattr(label, "identifier", None)
|
||||||
|
if ident in _ctx:
|
||||||
|
return _ctx[ident]
|
||||||
|
return _orig(label)
|
||||||
|
|
||||||
|
node.resolve = _mock_elem_resolve
|
||||||
|
|
||||||
|
item_res = raw_evaluate_fn(node, socket_name, **kwargs)
|
||||||
|
results.append(item_res)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
attach_node_failure_info(node, e, fallback_inputs=[key_to_name[k] for k in input_keys])
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
node._in_multivalue_eval = False
|
||||||
|
node._eval_iteration_ctx = None
|
||||||
|
if is_mocked:
|
||||||
|
node.resolve = mock_resolver
|
||||||
|
elif hasattr(node, "__dict__") and "resolve" in node.__dict__:
|
||||||
|
node.__dict__.pop("resolve", None)
|
||||||
|
|
||||||
|
return results
|
||||||
130
infra/blender/node_profiler.py
Normal file
130
infra/blender/node_profiler.py
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
def get_node_type(node: Any) -> str:
|
||||||
|
if hasattr(node, "bl_label") and node.bl_label:
|
||||||
|
return str(node.bl_label)
|
||||||
|
if hasattr(node, "bl_idname") and node.bl_idname:
|
||||||
|
return str(node.bl_idname)
|
||||||
|
return type(node).__name__
|
||||||
|
|
||||||
|
class NodeProfiler:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# Maps node_type -> [execution_count, total_self_time_seconds]
|
||||||
|
self.stats: dict[str, list[float]] = {}
|
||||||
|
self._stack: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def profile_node(self, node: Any):
|
||||||
|
node_type = get_node_type(node)
|
||||||
|
now = time.perf_counter()
|
||||||
|
|
||||||
|
# Pause parent node's self-timer
|
||||||
|
if self._stack:
|
||||||
|
parent = self._stack[-1]
|
||||||
|
parent["self_time"] += (now - parent["start_time"])
|
||||||
|
|
||||||
|
frame = {
|
||||||
|
"node_type": node_type,
|
||||||
|
"start_time": now,
|
||||||
|
"self_time": 0.0,
|
||||||
|
}
|
||||||
|
self._stack.append(frame)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
end_time = time.perf_counter()
|
||||||
|
popped = self._stack.pop()
|
||||||
|
total_node_self_time = popped["self_time"] + (end_time - popped["start_time"])
|
||||||
|
|
||||||
|
if node_type not in self.stats:
|
||||||
|
self.stats[node_type] = [0.0, 0.0]
|
||||||
|
self.stats[node_type][0] += 1.0
|
||||||
|
self.stats[node_type][1] += total_node_self_time
|
||||||
|
|
||||||
|
# Resume parent node's timer
|
||||||
|
if self._stack:
|
||||||
|
self._stack[-1]["start_time"] = time.perf_counter()
|
||||||
|
|
||||||
|
def format_table(self) -> str:
|
||||||
|
headers = ["node_type", "execution_count", "avg_execution_time", "tot_execution_time"]
|
||||||
|
|
||||||
|
if not self.stats:
|
||||||
|
col_widths = [20, 15, 18, 18]
|
||||||
|
header_line = " | ".join(headers[i].ljust(col_widths[i]) for i in range(4))
|
||||||
|
separator_line = "-+-".join("-" * col_widths[i] for i in range(4))
|
||||||
|
return f"{header_line}\n{separator_line}\n(No nodes executed)"
|
||||||
|
|
||||||
|
sorted_stats = sorted(self.stats.items(), key=lambda item: item[1][1], reverse=True)
|
||||||
|
|
||||||
|
rows: list[list[str]] = []
|
||||||
|
tot_count = 0
|
||||||
|
tot_time_all = 0.0
|
||||||
|
|
||||||
|
for node_type, (count, tot_time) in sorted_stats:
|
||||||
|
icount = int(count)
|
||||||
|
tot_count += icount
|
||||||
|
tot_time_all += tot_time
|
||||||
|
avg_time = tot_time / icount if icount > 0 else 0.0
|
||||||
|
rows.append([
|
||||||
|
node_type,
|
||||||
|
str(icount),
|
||||||
|
f"{avg_time:.6f}s",
|
||||||
|
f"{tot_time:.6f}s",
|
||||||
|
])
|
||||||
|
|
||||||
|
col_widths = [
|
||||||
|
max(len(headers[i]), max(len(row[i]) for row in rows))
|
||||||
|
for i in range(4)
|
||||||
|
]
|
||||||
|
col_widths[0] = max(col_widths[0], len("Total"))
|
||||||
|
col_widths[1] = max(col_widths[1], len(str(tot_count)))
|
||||||
|
col_widths[3] = max(col_widths[3], len(f"{tot_time_all:.6f}s"))
|
||||||
|
|
||||||
|
lines: list[str] = []
|
||||||
|
header_line = " | ".join(headers[i].ljust(col_widths[i]) for i in range(4))
|
||||||
|
separator_line = "-+-".join("-" * col_widths[i] for i in range(4))
|
||||||
|
|
||||||
|
lines.append(header_line)
|
||||||
|
lines.append(separator_line)
|
||||||
|
for row in rows:
|
||||||
|
lines.append(" | ".join([
|
||||||
|
row[0].ljust(col_widths[0]),
|
||||||
|
row[1].rjust(col_widths[1]),
|
||||||
|
row[2].rjust(col_widths[2]),
|
||||||
|
row[3].rjust(col_widths[3]),
|
||||||
|
]))
|
||||||
|
lines.append(separator_line)
|
||||||
|
total_row = [
|
||||||
|
"Total".ljust(col_widths[0]),
|
||||||
|
str(tot_count).rjust(col_widths[1]),
|
||||||
|
"-".rjust(col_widths[2]),
|
||||||
|
f"{tot_time_all:.6f}s".rjust(col_widths[3]),
|
||||||
|
]
|
||||||
|
lines.append(" | ".join(total_row))
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
_current_profiler: NodeProfiler | None = None
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def profiler_context():
|
||||||
|
global _current_profiler
|
||||||
|
old = _current_profiler
|
||||||
|
profiler = NodeProfiler()
|
||||||
|
_current_profiler = profiler
|
||||||
|
try:
|
||||||
|
yield profiler
|
||||||
|
finally:
|
||||||
|
_current_profiler = old
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def profile_node_execution(node: Any):
|
||||||
|
if _current_profiler is None:
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
with _current_profiler.profile_node(node):
|
||||||
|
yield
|
||||||
29
infra/error_reporting/__init__.py
Normal file
29
infra/error_reporting/__init__.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .formatter import (
|
||||||
|
_format_node_description,
|
||||||
|
get_node_input_names,
|
||||||
|
format_node_failure_message,
|
||||||
|
)
|
||||||
|
from .proxy import (
|
||||||
|
NodeErrorReportingProxy,
|
||||||
|
wrap_operator_for_node,
|
||||||
|
)
|
||||||
|
from .handler import (
|
||||||
|
attach_node_failure_info,
|
||||||
|
NodeErrorHandler,
|
||||||
|
handle_node_errors,
|
||||||
|
node_execution_scope,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"_format_node_description",
|
||||||
|
"get_node_input_names",
|
||||||
|
"format_node_failure_message",
|
||||||
|
"NodeErrorReportingProxy",
|
||||||
|
"wrap_operator_for_node",
|
||||||
|
"attach_node_failure_info",
|
||||||
|
"NodeErrorHandler",
|
||||||
|
"handle_node_errors",
|
||||||
|
"node_execution_scope",
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue