offset paths, execution highlighting only when manual exec
This commit is contained in:
parent
64eff798ac
commit
7e642f4c32
5 changed files with 168 additions and 70 deletions
|
|
@ -64,6 +64,10 @@ class SLICENODE_InputInt(InputNode):
|
|||
class SLICENODE_InputVectorRotation(InputNode):
|
||||
input_type = "VectorRotation"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputVector(InputNode):
|
||||
input_type = "Vector"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputObject(InputNode):
|
||||
input_type = "Object"
|
||||
|
|
@ -421,26 +425,24 @@ class SLICENODE_BufferShells(SliceNode):
|
|||
|
||||
if not paths: return SlicePaths()
|
||||
|
||||
if any(rotation):
|
||||
# De-rotate shells to Z-perpendicular plane
|
||||
mat = Euler(rotation).to_matrix().inverted()
|
||||
mat_np = np.array(mat)
|
||||
prepared_paths = [s @ mat_np.T for s in paths]
|
||||
else:
|
||||
prepared_paths = paths
|
||||
return paths.buffer(distance, miter=self.shape_option, rotation=rotation)
|
||||
|
||||
# Buffer
|
||||
res_paths, res_groups = slice.buffer_slice(prepared_paths, distance, miter=self.shape_option)
|
||||
@register_class
|
||||
class SLICENODE_PathOffset(SliceNode):
|
||||
bl_label = "Path Offset"
|
||||
|
||||
if any(rotation):
|
||||
# Re-rotate shells back
|
||||
mat_inv = Euler(rotation).to_matrix()
|
||||
mat_inv_np = np.array(mat_inv)
|
||||
final_paths = [s @ mat_inv_np.T for s in res_paths]
|
||||
else:
|
||||
final_paths = res_paths
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Paths", "Paths")
|
||||
self.add_input("SLICESOCK_Vector", "Offset")
|
||||
self.add_output("SLICESOCK_Paths", "Paths")
|
||||
|
||||
return SlicePaths(final_paths, res_groups)
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
|
||||
paths: SlicePaths = self.resolve("Paths")
|
||||
offset: Any = self.resolve("Offset") or (0.0, 0.0, 0.0)
|
||||
|
||||
if not paths: return SlicePaths()
|
||||
|
||||
return paths.offset(np.array(offset, dtype=np.float32))
|
||||
|
||||
@register_class
|
||||
class SLICENODE_BufferShellsMultiple(SliceNode):
|
||||
|
|
@ -478,19 +480,11 @@ class SLICENODE_BufferShellsMultiple(SliceNode):
|
|||
|
||||
if not paths: return SlicePaths()
|
||||
|
||||
if any(rotation):
|
||||
mat = Euler(rotation).to_matrix().inverted()
|
||||
mat_np = np.array(mat)
|
||||
prepared_paths = [s @ mat_np.T for s in paths]
|
||||
else:
|
||||
prepared_paths = paths
|
||||
|
||||
all_buffers: list[SlicePaths] = []
|
||||
|
||||
for i in range(count):
|
||||
dist = base_distance + (i * width_step)
|
||||
res_paths, res_groups = slice.buffer_slice(prepared_paths, dist, miter=self.shape_option)
|
||||
all_buffers.append(SlicePaths(res_paths, res_groups))
|
||||
all_buffers.append(paths.buffer(dist, miter=self.shape_option, rotation=rotation))
|
||||
|
||||
# Zip Logic: Interleave paths by group (Z-layer)
|
||||
all_groups = set()
|
||||
|
|
@ -511,13 +505,7 @@ class SLICENODE_BufferShellsMultiple(SliceNode):
|
|||
for p in group_paths:
|
||||
merged.append_with_group(p, g)
|
||||
|
||||
if any(rotation):
|
||||
mat_inv = Euler(rotation).to_matrix()
|
||||
mat_inv_np = np.array(mat_inv)
|
||||
final_paths = [s @ mat_inv_np.T for s in merged]
|
||||
return SlicePaths(final_paths, merged.groups)
|
||||
|
||||
return merged
|
||||
return paths.copy_with(merged, merged.groups)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_Infill(SliceNode):
|
||||
|
|
@ -554,32 +542,13 @@ class SLICENODE_Infill(SliceNode):
|
|||
|
||||
if not paths: return SlicePaths()
|
||||
|
||||
p: SlicePaths
|
||||
if any(rotation):
|
||||
# De-rotate shells to Z-perpendicular plane
|
||||
mat = Euler(rotation).to_matrix().inverted()
|
||||
mat_np = np.array(mat)
|
||||
p = SlicePaths([s @ mat_np.T for s in paths], paths.groups)
|
||||
else:
|
||||
p = paths
|
||||
|
||||
res_paths, res_groups = slice.infill_slice(
|
||||
p,
|
||||
return paths.infill(
|
||||
density,
|
||||
rotation_deg=infill_rotation,
|
||||
infill_type=self.infill_type # pyright: ignore[reportOptionalMemberAccess]
|
||||
infill_type=self.infill_type, # pyright: ignore[reportOptionalMemberAccess]
|
||||
layer_rotation=rotation
|
||||
)
|
||||
|
||||
if any(rotation):
|
||||
# Re-rotate shells back
|
||||
mat_inv = Euler(rotation).to_matrix()
|
||||
mat_inv_np = np.array(mat_inv)
|
||||
p = SlicePaths([s @ mat_inv_np.T for s in res_paths], res_groups)
|
||||
else:
|
||||
p = SlicePaths(res_paths, res_groups)
|
||||
|
||||
return p
|
||||
|
||||
@register_class
|
||||
class SLICENODE_PathsToGcode(SliceNode):
|
||||
bl_label = "Paths to Gcode"
|
||||
|
|
@ -644,6 +613,31 @@ class SLICENODE_AssignMetadata(SliceNode):
|
|||
lines.append(f'; printer_model = {printer}')
|
||||
return '\n'.join(lines)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GcodeRegexReplace(SliceNode):
|
||||
bl_label = "Gcode Regex Replace"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Gcode", "Gcode")
|
||||
self.add_input("SLICESOCK_String", "Find")
|
||||
self.add_input("SLICESOCK_String", "Replace")
|
||||
self.add_output("SLICESOCK_GcodeRo", "Gcode")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||
import re
|
||||
gcode = self.resolve("Gcode") or ""
|
||||
find = self.resolve("Find") or ""
|
||||
replace = self.resolve("Replace") or ""
|
||||
|
||||
if not find:
|
||||
return gcode
|
||||
|
||||
try:
|
||||
return re.sub(find, replace, gcode)
|
||||
except Exception as e:
|
||||
print(f"Regex error in node {self.name}: {e}")
|
||||
return gcode
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GcodeHelper(SliceNode):
|
||||
bl_label = "Gcode Helper"
|
||||
|
|
@ -771,14 +765,11 @@ class SLICENODE_JoinSlices(SliceNodeJoiner):
|
|||
|
||||
if not accumulated:
|
||||
accumulated = current
|
||||
current = SlicePaths()
|
||||
continue
|
||||
|
||||
# Boolean operation loses specific per-segment grouping as it merges polygons.
|
||||
# We return a new SlicePaths from the result.
|
||||
res = slice.boolean_slice(accumulated, current, self.operation)
|
||||
accumulated = SlicePaths(res)
|
||||
current = SlicePaths()
|
||||
accumulated = accumulated.boolean(current, self.operation)
|
||||
|
||||
return accumulated
|
||||
|
||||
|
|
@ -1696,7 +1687,11 @@ class SLICENODE_Executor(ExecSliceNode, SliceNodeJoiner):
|
|||
layout.prop(self, "auto_refresh")
|
||||
|
||||
def evaluate(self, operator=None):
|
||||
from ...infra.blender.blender_slice_nodes_generic import node_execution_feedback
|
||||
from ...infra.blender.blender_slice_nodes_generic import node_execution_feedback, feedback_disabled, execution_cache
|
||||
|
||||
# If auto_refresh is on, we don't want highlighting/redraws for upstream nodes either
|
||||
maybe_disabled = feedback_disabled if self.auto_refresh else lambda: execution_cache()
|
||||
|
||||
for i, job in enumerate(self.inputs):
|
||||
# Resolve the input to get the connected node(s)
|
||||
res = resolve_input(job)
|
||||
|
|
@ -1714,10 +1709,11 @@ class SLICENODE_Executor(ExecSliceNode, SliceNodeJoiner):
|
|||
operator.report({'INFO'}, f"Executing: {upstream_node.bl_label}")
|
||||
|
||||
try:
|
||||
with node_execution_feedback(upstream_node):
|
||||
exec_res = upstream_node.evaluate(operator=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}")
|
||||
with maybe_disabled():
|
||||
with node_execution_feedback(upstream_node):
|
||||
exec_res = upstream_node.evaluate(operator=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'}, f"Failed executing {upstream_node.bl_label}: {str(e)}")
|
||||
return 'error'
|
||||
|
|
|
|||
|
|
@ -100,13 +100,25 @@ class SLICESOCK_VectorRotation_Common(InputNodeSocket):
|
|||
bl_label = "VectorRotation"
|
||||
|
||||
value: bpy.props.FloatVectorProperty(name="Vector", size=3, default=(0.0, 0.0, 0.0), subtype='EULER') # pyright: ignore[reportInvalidTypeForm]
|
||||
bl_color = (0.39, 0.39, 0.78, 1.0)
|
||||
bl_color = (166/255, 99/255, 199/255, 1.0)
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_VectorRotation(SLICESOCK_VectorRotation_Common): ...
|
||||
@register_class
|
||||
class SLICESOCK_VectorRotationRo(ReadonlySocket, SLICESOCK_VectorRotation_Common): ...
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Vector_Common(InputNodeSocket):
|
||||
bl_label = "Vector"
|
||||
|
||||
value: bpy.props.FloatVectorProperty(name="Vector", size=3, default=(0.0, 0.0, 0.0)) # pyright: ignore[reportInvalidTypeForm]
|
||||
bl_color = (0.39, 0.39, 0.78, 1.0)
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Vector(SLICESOCK_Vector_Common): ...
|
||||
@register_class
|
||||
class SLICESOCK_VectorRo(ReadonlySocket, SLICESOCK_Vector_Common): ...
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Mesh(LabelNodeSocket):
|
||||
bl_label = "Mesh"
|
||||
|
|
|
|||
|
|
@ -85,8 +85,10 @@ def tree_update_handler(scene, depsgraph):
|
|||
if should_refresh:
|
||||
_is_updating = True
|
||||
try:
|
||||
with node_execution_feedback(node):
|
||||
node.evaluate()
|
||||
from ...infra.blender.blender_slice_nodes_generic import feedback_disabled
|
||||
with feedback_disabled():
|
||||
with node_execution_feedback(node):
|
||||
node.evaluate()
|
||||
except Exception as e:
|
||||
print(f"Auto-refresh failed: {e}")
|
||||
import traceback
|
||||
|
|
@ -155,6 +157,7 @@ class SLICENODE_MT_DataInput(Menu):
|
|||
layout.operator("node.add_node", text="Float").type = "SLICENODE_InputFloat"
|
||||
layout.operator("node.add_node", text="Integer").type = "SLICENODE_InputInt"
|
||||
layout.operator("node.add_node", text="Rotation").type = "SLICENODE_InputVectorRotation"
|
||||
layout.operator("node.add_node", text="Vector").type = "SLICENODE_InputVector"
|
||||
layout.separator()
|
||||
layout.operator("node.add_node", text="Object").type = "SLICENODE_InputObject"
|
||||
layout.operator("node.add_node", text="Overrides").type = "SLICENODE_InputOverrides"
|
||||
|
|
@ -178,6 +181,7 @@ class SLICENODE_MT_Geometry(Menu):
|
|||
layout.operator("node.add_node", text="Slice Shells").type = "SLICENODE_SliceShells"
|
||||
layout.operator("node.add_node", text="Buffer Shells").type = "SLICENODE_BufferShells"
|
||||
layout.operator("node.add_node", text="Buffer Shells Multiple").type = "SLICENODE_BufferShellsMultiple"
|
||||
layout.operator("node.add_node", text="Path Offset").type = "SLICENODE_PathOffset"
|
||||
layout.operator("node.add_node", text="Infill").type = "SLICENODE_Infill"
|
||||
layout.operator("node.add_node", text="Boolean Paths").type = "SLICENODE_JoinSlices"
|
||||
layout.separator()
|
||||
|
|
@ -194,6 +198,7 @@ class SLICENODE_MT_Gcode(Menu):
|
|||
assert layout
|
||||
layout.operator("node.add_node", text="Paths to Gcode").type = "SLICENODE_PathsToGcode"
|
||||
layout.operator("node.add_node", text="Gcode Block").type = "SLICENODE_GcodeBlock"
|
||||
layout.operator("node.add_node", text="Gcode Regex Replace").type = "SLICENODE_GcodeRegexReplace"
|
||||
layout.operator("node.add_node", text="Assign Metadata").type = "SLICENODE_AssignMetadata"
|
||||
layout.operator("node.add_node", text="Gcode Helper").type = "SLICENODE_GcodeHelper"
|
||||
layout.operator("node.add_node", text="Load Gcode").type = "SLICENODE_LoadGcode"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,75 @@ class SlicePaths(list[np.ndarray]):
|
|||
if groups: self.groups = list(groups)
|
||||
else: self.groups = list(range(0,len(paths)))
|
||||
|
||||
def copy_with(self, paths: list[np.ndarray], groups: list[int] | None = None) -> SlicePaths:
|
||||
"""Returns a new SlicePaths object with new paths/groups but preserving all other metadata."""
|
||||
new_obj = SlicePaths(paths, groups if groups is not None else self.groups)
|
||||
# Copy any other attributes that might have been added
|
||||
for k, v in self.__dict__.items():
|
||||
if k != "groups":
|
||||
setattr(new_obj, k, v)
|
||||
return new_obj
|
||||
|
||||
def offset(self, offset_vec: np.ndarray) -> SlicePaths:
|
||||
"""Returns a new SlicePaths object shifted by the given vector."""
|
||||
return self.copy_with([p + offset_vec for p in self])
|
||||
|
||||
def transform(self, matrix: np.ndarray) -> SlicePaths:
|
||||
"""Applies a transformation matrix to all paths."""
|
||||
return self.copy_with([p @ matrix.T for p in self])
|
||||
|
||||
def buffer(self, distance: float, miter: str = 'round', rotation: Any = None) -> SlicePaths:
|
||||
"""Buffers (offsets) the paths. Handles optional rotation for non-planar buffering."""
|
||||
p = self
|
||||
if rotation and any(rotation):
|
||||
from mathutils import Euler
|
||||
mat = Euler(rotation).to_matrix().inverted()
|
||||
p = self.transform(np.array(mat))
|
||||
|
||||
res_paths, res_groups = buffer_slice(p, distance, miter=miter)
|
||||
new_paths = SlicePaths(res_paths, res_groups)
|
||||
|
||||
# Copy other metadata from self
|
||||
for k, v in self.__dict__.items():
|
||||
if k != "groups":
|
||||
setattr(new_paths, k, v)
|
||||
|
||||
if rotation and any(rotation):
|
||||
from mathutils import Euler
|
||||
mat_inv = Euler(rotation).to_matrix()
|
||||
new_paths = new_paths.transform(np.array(mat_inv))
|
||||
|
||||
return new_paths
|
||||
|
||||
def infill(self, density: float, rotation_deg: float = 0.0, infill_type: str = 'GRID', layer_rotation: Any = None) -> SlicePaths:
|
||||
"""Generates infill for the paths."""
|
||||
p = self
|
||||
if layer_rotation and any(layer_rotation):
|
||||
from mathutils import Euler
|
||||
mat = Euler(layer_rotation).to_matrix().inverted()
|
||||
p = self.transform(np.array(mat))
|
||||
|
||||
res_paths, res_groups = infill_slice(p, density, rotation_deg=rotation_deg, infill_type=infill_type)
|
||||
new_paths = SlicePaths(res_paths, res_groups)
|
||||
|
||||
# Copy other metadata from self
|
||||
for k, v in self.__dict__.items():
|
||||
if k != "groups":
|
||||
setattr(new_paths, k, v)
|
||||
|
||||
if layer_rotation and any(layer_rotation):
|
||||
from mathutils import Euler
|
||||
mat_inv = Euler(layer_rotation).to_matrix()
|
||||
new_paths = new_paths.transform(np.array(mat_inv))
|
||||
|
||||
return new_paths
|
||||
|
||||
def boolean(self, other: SlicePaths, operation: str = 'UNION') -> SlicePaths:
|
||||
"""Performs a 2D boolean operation with another set of paths."""
|
||||
res = boolean_slice(self, other, operation)
|
||||
# Boolean operations merge paths across layers, so we don't preserve original grouping as precisely.
|
||||
return self.copy_with(res)
|
||||
|
||||
def extend_with_groups(self, other: SlicePaths | list[np.ndarray]):
|
||||
if isinstance(other, SlicePaths):
|
||||
self.extend(other)
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ class SliceNode(Node, AutoBlIdName):
|
|||
## Evaluation helpers
|
||||
|
||||
_current_execution_cache: _EvalCache | None = None
|
||||
_feedback_enabled: bool = True
|
||||
|
||||
@contextmanager
|
||||
def execution_cache():
|
||||
|
|
@ -100,8 +101,23 @@ def execution_cache():
|
|||
finally:
|
||||
_current_execution_cache = None
|
||||
|
||||
@contextmanager
|
||||
def feedback_disabled():
|
||||
global _feedback_enabled
|
||||
old = _feedback_enabled
|
||||
_feedback_enabled = False
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_feedback_enabled = old
|
||||
|
||||
@contextmanager
|
||||
def node_execution_feedback(node: bpy.types.Node):
|
||||
if not _feedback_enabled:
|
||||
with execution_cache():
|
||||
yield
|
||||
return
|
||||
|
||||
# Save original color
|
||||
old_use_color = node.use_custom_color
|
||||
old_color = node.color[:]
|
||||
|
|
@ -150,7 +166,7 @@ def resolve_input(
|
|||
assert socket.node
|
||||
|
||||
if cache is None:
|
||||
cache = getattr(socket.node, "_current_cache", _current_execution_cache)
|
||||
cache = getattr(socket.node, "_current_cache", None) or _current_execution_cache
|
||||
if visited is None:
|
||||
visited = getattr(socket.node, "_current_visited", None)
|
||||
|
||||
|
|
@ -222,7 +238,7 @@ def _resolve_output(
|
|||
cache[key] = value
|
||||
return value
|
||||
finally:
|
||||
if hasattr(socket.node, "_current_cache"): del socket.node._current_cache
|
||||
if hasattr(socket.node, "_current_visited"): del socket.node._current_visited
|
||||
socket.node._current_cache = None # type: ignore
|
||||
socket.node._current_visited = None # type: ignore
|
||||
finally:
|
||||
visited.remove(node_key)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue