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 typing import Any, TypeAlias
|
||||
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 ....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
|
||||
from ...functions.math_functions import MATH_OPERATIONS
|
||||
|
||||
_MATH_OPS = MATH_OPERATIONS
|
||||
|
||||
class SliceIntrinsics:
|
||||
bed_x: float
|
||||
|
|
@ -56,24 +57,6 @@ class InputNode(SliceNode):
|
|||
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"
|
||||
|
||||
|
|
@ -105,7 +88,6 @@ class MathNode(SliceNode):
|
|||
|
||||
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")
|
||||
|
||||
|
|
@ -135,7 +117,7 @@ class MathNode(SliceNode):
|
|||
a_raw = resolve_input(inputs[0]) if len(inputs) > 0 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:
|
||||
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
|
||||
class SLICENODE_GroupToList(SliceNode):
|
||||
bl_label = "Group to List"
|
||||
implicit_multivalue = False
|
||||
always_list_outputs = ["Collections"]
|
||||
|
||||
def init(self, context):
|
||||
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]:
|
||||
val = self.resolve("Group")
|
||||
|
|
@ -143,10 +144,11 @@ class SLICENODE_GroupToList(SliceNode):
|
|||
@register_class
|
||||
class SLICENODE_ListToGroup(SliceNode):
|
||||
bl_label = "List to Group"
|
||||
implicit_multivalue = False
|
||||
always_list_inputs = ["Collections"]
|
||||
|
||||
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")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup:
|
||||
|
|
@ -280,12 +282,13 @@ class SLICENODE_SeparateMesh(SliceNode):
|
|||
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,
|
||||
)
|
||||
sc = self.resolve("Mesh")
|
||||
a = self.resolve("Attribute")
|
||||
v = self.resolve("Value")
|
||||
res_sel, res_inv = _split_coll(sc, str(a or ""), float(v if v is not None else 0.0))
|
||||
if socket_name == "Inverted":
|
||||
return res_inv
|
||||
return res_sel
|
||||
|
||||
@register_class
|
||||
class SLICENODE_SeparateMeshByRange(SliceNode):
|
||||
|
|
@ -388,11 +391,14 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
|
|||
res.objects.append(so_new)
|
||||
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
|
||||
class SLICENODE_ArrangeMesh(SliceNode):
|
||||
bl_label = "Arrange Slicing Group"
|
||||
implicit_multivalue = False
|
||||
list_socket_mapping = [("Slicing Group", "Slicing Group")]
|
||||
|
||||
method: str = NfEnumProperty(
|
||||
|
|
@ -658,19 +664,14 @@ class SLICENODE_TransformGeometry(SliceNode):
|
|||
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,
|
||||
)
|
||||
g = self.resolve("Geometry")
|
||||
t = self.resolve("Translation")
|
||||
r = self.resolve("Rotation")
|
||||
s = self.resolve("Scale")
|
||||
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)
|
||||
|
||||
|
||||
@register_class
|
||||
|
|
@ -686,8 +687,6 @@ class SLICENODE_GetTransforms(SliceNode):
|
|||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||
from ....core.slice import extract_geometry_transforms
|
||||
from ....infra.blender.list_utils import is_input_a_list
|
||||
|
||||
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)
|
||||
|
|
@ -695,31 +694,10 @@ class SLICENODE_GetTransforms(SliceNode):
|
|||
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
|
||||
trans = extract_geometry_transforms(geom)
|
||||
if isinstance(trans, tuple) and len(trans) > idx:
|
||||
return trans[idx]
|
||||
return default
|
||||
|
||||
|
||||
@register_class
|
||||
|
|
@ -734,36 +712,31 @@ class SLICENODE_DropGeometry(SliceNode):
|
|||
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:
|
||||
item = self.resolve("Mesh")
|
||||
target_z = self.resolve("Z")
|
||||
if not item or getattr(item, "min_z", None) is None:
|
||||
return (0.0, 0.0, 0.0) if socket_name == "Translation" else item
|
||||
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)
|
||||
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
|
||||
)
|
||||
elif isinstance(copied_data, SlicingCollection):
|
||||
for so in copied_data.objects:
|
||||
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
|
||||
if socket_name == "Translation":
|
||||
return (0.0, 0.0, offset_z)
|
||||
return copied_data
|
||||
|
||||
|
||||
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")
|
||||
|
||||
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
|
||||
)
|
||||
g = self.resolve("Geometry")
|
||||
name = self.resolve("Name")
|
||||
return _rename_slicing_item(g, str(name)) if name else g
|
||||
|
||||
|
||||
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")
|
||||
|
||||
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]))
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
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)
|
||||
g = self.resolve("Geometry")
|
||||
s = self.resolve("Settings")
|
||||
props = _parse_settings_props(s)
|
||||
return _assign_properties_to_item(g, props)
|
||||
|
||||
|
|
|
|||
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
|
||||
class SLICENODE_FlattenPathList(SliceNode):
|
||||
bl_label = "Flatten Path List"
|
||||
implicit_multivalue = False
|
||||
always_list_inputs = ["Paths"]
|
||||
|
||||
mode: str = NfEnumProperty(
|
||||
|
|
@ -80,26 +81,22 @@ class SLICENODE_GetPaths(SliceNode):
|
|||
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
|
||||
o = self.resolve("Object")
|
||||
if not o:
|
||||
return SlicePaths()
|
||||
e_name = self.resolve("Extrusion Multiplier Attribute")
|
||||
f_name = self.resolve("Feedrate Attribute")
|
||||
extra = self.resolve("Extra Attributes")
|
||||
|
||||
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(),
|
||||
)
|
||||
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
|
||||
|
||||
def _slice_single_mesh_item(coll: Any, layer_height: float, fl_height: float | None, rotation: Any, ignore_holes: bool) -> SlicePaths:
|
||||
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", "Layer Height").value = 0.2
|
||||
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 _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
|
||||
lh_val = self.resolve("Layer Height")
|
||||
flh_val = self.resolve("First Layer Height")
|
||||
tot_val = self.resolve("Total Height")
|
||||
|
||||
flh_um = max(1, int(round(flh * 1000)))
|
||||
lh_um = max(1, int(round(lh * 1000)))
|
||||
tot_um = int(round(tot * 1000))
|
||||
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
|
||||
|
||||
layers: list[float] = []
|
||||
z_um = flh_um
|
||||
while z_um <= tot_um:
|
||||
layers.append(z_um / 1000.0)
|
||||
z_um += lh_um
|
||||
return layers
|
||||
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=[],
|
||||
)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_SliceShells(SliceNode):
|
||||
|
|
@ -341,6 +334,11 @@ class SLICENODE_SliceShells(SliceNode):
|
|||
layout.prop(self, "ignore_holes")
|
||||
|
||||
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))
|
||||
if hasattr(self, "inputs") and "Ignore Holes" in self.inputs:
|
||||
try:
|
||||
|
|
@ -350,19 +348,10 @@ class SLICENODE_SliceShells(SliceNode):
|
|||
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)
|
||||
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(),
|
||||
)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_SliceShellsLayers(SliceNode):
|
||||
|
|
@ -378,7 +367,7 @@ class SLICENODE_SliceShellsLayers(SliceNode):
|
|||
|
||||
def init(self, context):
|
||||
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_output("SLICESOCK_Paths")
|
||||
|
||||
|
|
@ -388,6 +377,10 @@ class SLICENODE_SliceShellsLayers(SliceNode):
|
|||
layout.prop(self, "ignore_holes")
|
||||
|
||||
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))
|
||||
if hasattr(self, "inputs") and "Ignore Holes" in self.inputs:
|
||||
try:
|
||||
|
|
@ -397,18 +390,8 @@ class SLICENODE_SliceShellsLayers(SliceNode):
|
|||
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 _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
|
||||
class SLICENODE_BufferShells(SliceNode):
|
||||
|
|
@ -435,18 +418,12 @@ class SLICENODE_BufferShells(SliceNode):
|
|||
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(),
|
||||
)
|
||||
p = self.resolve("Paths")
|
||||
if not p:
|
||||
return SlicePaths()
|
||||
dist = self.resolve("Distance")
|
||||
rot = self.resolve("Rotation")
|
||||
return p.buffer(float(dist if dist is not None else 0.0), miter=self.shape_option, rotation=rot)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_BufferCurve(SliceNode):
|
||||
|
|
@ -492,23 +469,17 @@ class SLICENODE_BufferCurve(SliceNode):
|
|||
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(),
|
||||
p = self.resolve("Paths")
|
||||
if not p:
|
||||
return SlicePaths()
|
||||
dist = self.resolve("Distance")
|
||||
rot = self.resolve("Rotation")
|
||||
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
|
||||
)
|
||||
|
||||
@register_class
|
||||
|
|
@ -537,21 +508,15 @@ class SLICENODE_OffsetCurve(SliceNode):
|
|||
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(),
|
||||
p = self.resolve("Paths")
|
||||
if not p:
|
||||
return SlicePaths()
|
||||
dist = self.resolve("Distance")
|
||||
rot = self.resolve("Rotation")
|
||||
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
|
||||
)
|
||||
|
||||
@register_class
|
||||
|
|
@ -565,18 +530,11 @@ class SLICENODE_PathOffset(SliceNode):
|
|||
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(),
|
||||
)
|
||||
p = self.resolve("Paths")
|
||||
if not p:
|
||||
return SlicePaths()
|
||||
off = self.resolve("Offset")
|
||||
return p.offset(np.array(off if off is not None else (0.0, 0.0, 0.0), dtype=np.float64))
|
||||
|
||||
@register_class
|
||||
class SLICENODE_RotatePath(SliceNode):
|
||||
|
|
@ -590,19 +548,13 @@ class SLICENODE_RotatePath(SliceNode):
|
|||
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(),
|
||||
)
|
||||
p = self.resolve("Paths")
|
||||
if not p:
|
||||
return SlicePaths()
|
||||
ang = self.resolve("Angle")
|
||||
cent = self.resolve("Center")
|
||||
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)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_BufferShellsMultiple(SliceNode):
|
||||
|
|
@ -633,30 +585,24 @@ class SLICENODE_BufferShellsMultiple(SliceNode):
|
|||
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)
|
||||
p = self.resolve("Paths")
|
||||
cnt = self.resolve("Count")
|
||||
c = int(cnt if cnt is not None else 1)
|
||||
if not p or c <= 0:
|
||||
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(
|
||||
_buf_multi,
|
||||
main_socket="Paths",
|
||||
secondary_sockets=["Distance", "Width Step", "Count", "Rotation"],
|
||||
is_vector=[False, False, False, True],
|
||||
socket_name=socket_name,
|
||||
default=SlicePaths(),
|
||||
)
|
||||
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()
|
||||
|
||||
@register_class
|
||||
class SLICENODE_Infill(SliceNode):
|
||||
|
|
@ -686,22 +632,17 @@ class SLICENODE_Infill(SliceNode):
|
|||
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(),
|
||||
p = self.resolve("Paths")
|
||||
if not p:
|
||||
return SlicePaths()
|
||||
dens = self.resolve("Density")
|
||||
inf_rot = self.resolve("Infill Rotation")
|
||||
rot = self.resolve("Layer Rotation")
|
||||
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
|
||||
)
|
||||
|
||||
@register_class
|
||||
|
|
@ -718,17 +659,11 @@ class SLICENODE_OverhangMask(SliceNode):
|
|||
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(),
|
||||
)
|
||||
p = self.resolve("Paths")
|
||||
if not p:
|
||||
return SlicePaths()
|
||||
tol = self.resolve("Tolerance")
|
||||
return p.get_overhang_mask(tolerance=float(tol if tol is not None else 0.05))
|
||||
|
||||
@register_class
|
||||
class SLICENODE_PathOperations(SliceNodeJoiner):
|
||||
|
|
@ -867,19 +802,12 @@ class SLICENODE_SimplifyPaths(SliceNode):
|
|||
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(),
|
||||
)
|
||||
p = self.resolve("Paths")
|
||||
tol = self.resolve("Tolerance")
|
||||
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)
|
||||
|
||||
|
||||
@register_class
|
||||
|
|
@ -917,23 +845,16 @@ class SLICENODE_SeamPlacement(SliceNode):
|
|||
layout.prop(self, "acquire_transforms")
|
||||
|
||||
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))
|
||||
|
||||
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(),
|
||||
)
|
||||
return p.place_seams(m_in, mode=self.mode.lower(), acquire_transforms=acquire_t, stagger_mm=s_val)
|
||||
|
||||
|
||||
@register_class
|
||||
|
|
@ -945,7 +866,7 @@ class SLICENODE_AnchorPaths(SliceNode):
|
|||
name="Reverse",
|
||||
description="Follow anchor paths in the opposite direction",
|
||||
default=False,
|
||||
update=notify_tree_changed,
|
||||
update=notify_tree_changed
|
||||
)
|
||||
|
||||
distance: float = NfFloatProperty(
|
||||
|
|
@ -953,7 +874,7 @@ class SLICENODE_AnchorPaths(SliceNode):
|
|||
description="Distance to follow along anchor paths",
|
||||
default=2.0,
|
||||
min=0.0,
|
||||
update=notify_tree_changed,
|
||||
update=notify_tree_changed
|
||||
)
|
||||
|
||||
def init(self, context):
|
||||
|
|
@ -968,30 +889,24 @@ class SLICENODE_AnchorPaths(SliceNode):
|
|||
layout.prop(self, "reverse")
|
||||
|
||||
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))
|
||||
|
||||
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(),
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
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 typing import Any
|
||||
import math
|
||||
import bpy
|
||||
import os
|
||||
import bpy
|
||||
|
||||
from ..bpy_ns_types import NfBoolProperty
|
||||
from ....registry import register_class
|
||||
from .base import ExecSliceNode
|
||||
from ...bpy_ns_types import NfBoolProperty
|
||||
from .....registry import register_class
|
||||
from ..base import ExecSliceNode
|
||||
from ....functions.preview_functions import parse_gcode_preview_geometry
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||
|
|
@ -22,7 +22,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
|
||||
def update(self) -> None:
|
||||
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:
|
||||
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")
|
||||
|
|
@ -39,128 +39,25 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
obj.data.clear_geometry()
|
||||
return
|
||||
|
||||
vertices = []
|
||||
edges = []
|
||||
e_rates = []
|
||||
|
||||
scale_length = 1.0
|
||||
if bpy.context.scene and bpy.context.scene.unit_settings:
|
||||
scale_length = bpy.context.scene.unit_settings.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
|
||||
last_p2 = None
|
||||
parsed = parse_gcode_preview_geometry(
|
||||
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]
|
||||
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:
|
||||
# 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
|
||||
vertices = parsed["vertices"]
|
||||
edges = parsed["edges"]
|
||||
e_rates = parsed["e_rates"]
|
||||
default_layer_height = parsed["default_layer_height"]
|
||||
prep_translation = parsed["prep_translation"]
|
||||
prep_rotation = parsed["prep_rotation"]
|
||||
prep_scale = parsed["prep_scale"]
|
||||
|
||||
if not vertices:
|
||||
return
|
||||
|
|
@ -194,7 +91,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
ng_name = "Nonplanar Slicing Preview"
|
||||
if ng_name not in real_bpy.data.node_groups:
|
||||
# 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")
|
||||
if os.path.exists(blend_path):
|
||||
try:
|
||||
|
|
@ -236,7 +133,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
mod["Socket_9"] = tuple(map(float, prep_scale))
|
||||
|
||||
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 []
|
||||
gcodes: list[str] = [str(g) for g in flatten_list(raw_gcodes) if g]
|
||||
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 typing import Any
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
import bpy
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
|
||||
from ..bpy_ns_types import NfBoolProperty, NfStringProperty
|
||||
from ....registry import register_class
|
||||
from .base import ExecSliceNode, SliceNodeJoiner
|
||||
from ...bpy_ns_types import NfStringProperty
|
||||
from .....registry import register_class
|
||||
from ..base import ExecSliceNode
|
||||
|
||||
@register_class
|
||||
class SLICENODE_PrinterList(ExecSliceNode):
|
||||
|
|
@ -15,7 +15,6 @@ class SLICENODE_PrinterList(ExecSliceNode):
|
|||
button_label = "Start Prints"
|
||||
|
||||
def _refresh(self):
|
||||
import json
|
||||
if not self.printers_json_path:
|
||||
return
|
||||
|
||||
|
|
@ -62,13 +61,12 @@ class SLICENODE_PrinterList(ExecSliceNode):
|
|||
layout.prop(self, "printers_json_path")
|
||||
|
||||
def evaluate(self, operator=None):
|
||||
import json
|
||||
from ....infra.networking.printer_backends import (
|
||||
from .....infra.networking.printer_backends import (
|
||||
PrusaLinkBackend,
|
||||
CrealityBackend,
|
||||
MoonrakerBackend
|
||||
)
|
||||
from ....infra.networking.gcode_utils import prepare_gcode_file
|
||||
from .....infra.networking.gcode_utils import prepare_gcode_file
|
||||
|
||||
self._refresh()
|
||||
|
||||
|
|
@ -100,7 +98,7 @@ class SLICENODE_PrinterList(ExecSliceNode):
|
|||
if not gcode:
|
||||
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 operator: operator.report({'ERROR'}, f"Printer List does not accept a list of Gcode on socket {sock.name}")
|
||||
continue
|
||||
|
|
@ -153,119 +151,3 @@ class SLICENODE_PrinterList(ExecSliceNode):
|
|||
return 'no gcode'
|
||||
|
||||
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:
|
||||
with execution_cache():
|
||||
from ...infra.blender.blender_slice_nodes_generic import NodeErrorReportingProxy
|
||||
wrapped_op = NodeErrorReportingProxy(self, node)
|
||||
res = node.evaluate(operator=wrapped_op)
|
||||
if res == 'ok':
|
||||
self.report({"INFO"}, "Execution Successful")
|
||||
from ...infra.error_reporting import node_execution_scope
|
||||
with node_execution_scope(node, operator=self) as scope:
|
||||
res = node.evaluate(operator=scope.operator)
|
||||
if res == 'ok':
|
||||
self.report({"INFO"}, "Execution Successful")
|
||||
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))
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
|
|
|||
|
|
@ -29,14 +29,6 @@ from .metadata import (
|
|||
from .pdf_utils import (
|
||||
PdfStream,
|
||||
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 (
|
||||
circumcircle_3pts,
|
||||
|
|
@ -76,14 +68,6 @@ __all__ = [
|
|||
"PdfStream",
|
||||
# PDF utilities
|
||||
"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
|
||||
"deduplicate_2d_edges",
|
||||
"extract_unique_top_view_edges",
|
||||
|
|
|
|||
|
|
@ -35,140 +35,6 @@ def escape_pdf_text(text: str) -> str:
|
|||
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
|
||||
|
|
|
|||
|
|
@ -46,14 +46,29 @@ This file provides a mapping of modules to their primary responsibilities and ke
|
|||
- `geometry.py`:
|
||||
- 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)
|
||||
|
||||
- `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.
|
||||
- `nodes/`:
|
||||
- `base.py`: Base classes (`SliceNode`, `SliceNodeJoiner`, `SliceNodeListJoiner`, `ExecSliceNode`), geometry wrappers, and evaluation utilities.
|
||||
- `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)
|
||||
|
||||
|
|
@ -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`).
|
||||
- `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`:
|
||||
- `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`:
|
||||
- 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`).
|
||||
- Profiling tools (`NodeProfiler`, `profile_node_execution`).
|
||||
- Core evaluation logic (`resolve_input`, `_resolve_output`).
|
||||
|
||||
- `debug.py`:
|
||||
- `@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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
from ..profiling import (
|
||||
NodeProfiler,
|
||||
get_node_type,
|
||||
profiler_context,
|
||||
profile_node_execution,
|
||||
show_in_text_editor,
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
def execution_cache():
|
||||
|
|
@ -450,10 +311,6 @@ def _get_group_stack(cache: _EvalCache | None = None) -> list[bpy.types.Node]:
|
|||
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
|
||||
class SLICENODE_Group(bpy.types.NodeCustomGroup, AutoBlIdName):
|
||||
bl_label = "Group"
|
||||
|
|
@ -637,106 +494,16 @@ def _follow_reroute(current: NodeSocket) -> NodeSocket:
|
|||
current = reroute_input.links[0].from_socket
|
||||
|
||||
return current
|
||||
|
||||
def _format_node_description(node: Any) -> str:
|
||||
if node is None:
|
||||
return ""
|
||||
name = getattr(node, "name", "")
|
||||
bl_label = getattr(node, "bl_label", "")
|
||||
bl_idname = getattr(node, "bl_idname", "") or type(node).__name__
|
||||
|
||||
label = bl_label or bl_idname
|
||||
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)
|
||||
from ..error_reporting import (
|
||||
_format_node_description,
|
||||
get_node_input_names,
|
||||
format_node_failure_message,
|
||||
attach_node_failure_info,
|
||||
NodeErrorReportingProxy,
|
||||
NodeErrorHandler,
|
||||
handle_node_errors,
|
||||
node_execution_scope,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
|
@ -969,20 +736,20 @@ def _resolve_output(
|
|||
eval_tb = None
|
||||
value = None
|
||||
try:
|
||||
value = socket.node.evaluate_output(
|
||||
socket.name,
|
||||
identifier=socket.identifier,
|
||||
socket=socket,
|
||||
cache=cache,
|
||||
visited=visited
|
||||
)
|
||||
with handle_node_errors(socket.node):
|
||||
value = socket.node.evaluate_output(
|
||||
socket.name,
|
||||
identifier=socket.identifier,
|
||||
socket=socket,
|
||||
cache=cache,
|
||||
visited=visited
|
||||
)
|
||||
cache[key] = value
|
||||
return value
|
||||
except Exception as e:
|
||||
eval_exc = e
|
||||
import traceback
|
||||
eval_tb = traceback.format_exc()
|
||||
attach_node_failure_info(socket.node, e)
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ def eval_multivalue(
|
|||
main_is_vector=main_is_vec,
|
||||
)
|
||||
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 [])
|
||||
attach_node_failure_info(node, e, fallback_inputs=known_inputs)
|
||||
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