up
This commit is contained in:
parent
1a7bbaf9e5
commit
241b277895
8 changed files with 1067 additions and 881 deletions
887
__init__.py
887
__init__.py
|
|
@ -1,899 +1,26 @@
|
|||
from __future__ import annotations
|
||||
from numpy._typing._array_like import NDArray
|
||||
from numpy import float64, ndarray
|
||||
import bpy
|
||||
from . import registry
|
||||
from .core.nodeslicer import tree
|
||||
|
||||
bl_info = {
|
||||
"name": "NodeSlicer",
|
||||
"author": "OpenAI",
|
||||
"author": "Nicolas Predella",
|
||||
"version": (0, 1, 0),
|
||||
"blender": (3, 6, 0),
|
||||
"location": "Node Editor > Add",
|
||||
"description": "Very small custom nodes example",
|
||||
"description": "Prepare GCodes with Nodes",
|
||||
"category": "Node",
|
||||
}
|
||||
|
||||
from math import floor
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import bpy
|
||||
import os
|
||||
import numpy as np
|
||||
from mathutils import Euler, Matrix
|
||||
|
||||
from bpy.types import NodeTree, Operator, Menu
|
||||
from bpy.props import PointerProperty, StringProperty, FloatProperty, IntProperty
|
||||
|
||||
from .registry import register_class
|
||||
from .core import slice
|
||||
from .core.slice import Pather, slice_at_range, SlicePaths
|
||||
from .infra.blender_mesh_capture import SlicingObject, capture_mesh_edges
|
||||
from .infra.blender_slice_nodes_generic import SliceNode, SliceNodeSocket, resolve_input
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bpy.stub_internal.rna_enums import OperatorReturnItems
|
||||
|
||||
class SliceIntrinsics:
|
||||
bed_x: float
|
||||
bed_y: float
|
||||
feedrate: float
|
||||
extrusion_rate: float
|
||||
|
||||
def __init__(self, bed_x=250.0, bed_y=250.0, feedrate=1200.0, extrusion_rate=0.017):
|
||||
self.bed_x = bed_x
|
||||
self.bed_y = bed_y
|
||||
self.feedrate = feedrate
|
||||
self.extrusion_rate = extrusion_rate
|
||||
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Node tree
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@register_class
|
||||
class SLICETREE_NodeTree(NodeTree):
|
||||
bl_idname = "SLICETREE_NodeTree"
|
||||
bl_label = "NodeSlicer Tree"
|
||||
bl_icon = "NODETREE"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Sockets
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
class LabelNodeSocket(SliceNodeSocket):
|
||||
default = ""
|
||||
def draw(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||
assert layout
|
||||
layout.label(text=text or self.name)
|
||||
|
||||
class InputNodeSocket(SliceNodeSocket):
|
||||
def draw(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||
assert layout
|
||||
if not self.is_output and self.is_linked:
|
||||
layout.label(text=text or self.name)
|
||||
else:
|
||||
layout.prop(self, "value", text=text or self.name)
|
||||
|
||||
def _draw_as_ro(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||
layout.label(text=text or self.name)
|
||||
|
||||
def _draw_as_rw(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||
layout.prop(self, "value", text=text or self.name)
|
||||
|
||||
class ReadonlySocket():
|
||||
def draw(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||
return self._draw_as_ro(context, layout, node, text) # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Object(InputNodeSocket):
|
||||
bl_label = "Object"
|
||||
|
||||
value: PointerProperty(
|
||||
name="Object",
|
||||
type=bpy.types.Object,
|
||||
) # pyright: ignore[reportInvalidTypeForm]
|
||||
color = (0.9, 0.7, 0.2, 1.0)
|
||||
|
||||
|
||||
class SLICESOCK_Float_Common(InputNodeSocket):
|
||||
bl_label = "Float"
|
||||
|
||||
value: FloatProperty(name="Float", default=0) # pyright: ignore[reportInvalidTypeForm]
|
||||
color = (161/255, 161/255, 161/255, 1.0)
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_FloatRo(ReadonlySocket, SLICESOCK_Float_Common): ...
|
||||
@register_class
|
||||
class SLICESOCK_Float(SLICESOCK_Float_Common): ...
|
||||
|
||||
|
||||
class SLICESOCK_Int_Common(InputNodeSocket):
|
||||
bl_label = "Integer"
|
||||
|
||||
value: IntProperty(name="Int", default=0) # pyright: ignore[reportInvalidTypeForm]
|
||||
color = (161/255, 161/255, 161/255, 1.0)
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Int(SLICESOCK_Int_Common): ...
|
||||
@register_class
|
||||
class SLICESOCK_IntRo(ReadonlySocket, SLICESOCK_Int_Common): ...
|
||||
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_String(InputNodeSocket):
|
||||
bl_label = "String"
|
||||
|
||||
value: StringProperty(name="String", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||
color = (0.4, 0.7, 1.0, 1.0)
|
||||
|
||||
@register_class
|
||||
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]
|
||||
color = (0.39, 0.39, 0.78, 1.0)
|
||||
@register_class
|
||||
class SLICESOCK_VectorRotation(SLICESOCK_VectorRotation_Common): ...
|
||||
@register_class
|
||||
class SLICESOCK_VectorRotationRo(ReadonlySocket, SLICESOCK_VectorRotation_Common): ...
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Mesh(LabelNodeSocket):
|
||||
bl_label = "Mesh"
|
||||
default = None
|
||||
color = (0, 214/255, 163/255, 1.0)
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Paths(LabelNodeSocket):
|
||||
bl_label = "Paths"
|
||||
default = SlicePaths()
|
||||
color = (214/255, 0, 120/255, 1.0)
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Gcode(LabelNodeSocket):
|
||||
bl_label = "Gcode"
|
||||
default = ""
|
||||
color = (1., 1., 1., 1.)
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Intrinsics(LabelNodeSocket):
|
||||
bl_label = "Intrinsics"
|
||||
default = None
|
||||
color = (0.2, 0.2, 0.2, 1.0)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Nodes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Basic Inputs
|
||||
|
||||
class InputNode(SliceNode):
|
||||
input_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:
|
||||
self.add_output(f"SLICESOCK_{self.bl_label}")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs):
|
||||
return self.outputs.get(socket_name).value
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputString(InputNode):
|
||||
input_type = "String"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputFloat(InputNode):
|
||||
input_type = "Float"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputInt(InputNode):
|
||||
input_type = "Int"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputVectorRotation(InputNode):
|
||||
input_type = "VectorRotation"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputObject(InputNode):
|
||||
input_type = "Object"
|
||||
bl_icon = "OBJECT_DATA"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputIntrinsics(SliceNode):
|
||||
bl_label = "Printer Intrinsics"
|
||||
|
||||
bed_x: FloatProperty(name="Bed X", default=250.0) # pyright: ignore[reportInvalidTypeForm]
|
||||
bed_y: FloatProperty(name="Bed Y", default=250.0) # pyright: ignore[reportInvalidTypeForm]
|
||||
feedrate: FloatProperty(name="Feedrate", default=1200.0) # pyright: ignore[reportInvalidTypeForm]
|
||||
extrusion_rate: FloatProperty(name="Extrusion Rate", default=0.017, precision=4) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def init(self, context):
|
||||
self.add_output("SLICESOCK_Intrinsics")
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "bed_x")
|
||||
layout.prop(self, "bed_y")
|
||||
layout.prop(self, "feedrate")
|
||||
layout.prop(self, "extrusion_rate")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SliceIntrinsics:
|
||||
return SliceIntrinsics(
|
||||
bed_x=self.bed_x,
|
||||
bed_y=self.bed_y,
|
||||
feedrate=self.feedrate,
|
||||
extrusion_rate=self.extrusion_rate
|
||||
)
|
||||
|
||||
# Math
|
||||
|
||||
@register_class
|
||||
class SLICENODE_IntMath(SliceNode):
|
||||
bl_label = "Integer Math"
|
||||
|
||||
operation: bpy.props.EnumProperty(
|
||||
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", "Integer 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'
|
||||
) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def init(self, context):
|
||||
self.add_output("SLICESOCK_IntRo", "Value")
|
||||
|
||||
self.add_input("SLICESOCK_Int", "Value")
|
||||
self.add_input("SLICESOCK_Int", "Value")
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "operation", text="")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> int:
|
||||
a: int = resolve_input(self.inputs[0])
|
||||
b: int = resolve_input(self.inputs[1])
|
||||
|
||||
op = self.operation
|
||||
try:
|
||||
if op == 'ADD': return a + b
|
||||
if op == 'SUBTRACT': return a - b
|
||||
if op == 'MULTIPLY': return a * b
|
||||
if op == 'DIVIDE': return a // b if b != 0 else 0
|
||||
if op == 'ABS': return abs(a)
|
||||
if op == 'NEGATE': return -a
|
||||
if op == 'POWER': return int(a ** b) if not (a == 0 and b < 0) else 0
|
||||
if op == 'MIN': return min(a, b)
|
||||
if op == 'MAX': return max(a, b)
|
||||
if op == 'SIGN': return (a > 0) - (a < 0)
|
||||
if op == 'DIVIDE_ROUND': return int(round(a / b)) if b != 0 else 0
|
||||
if op == 'DIVIDE_FLOOR': return a // b if b != 0 else 0
|
||||
if op == 'DIVIDE_TRUNCATE': return int(a / b) if b != 0 else 0
|
||||
if op == 'MODULO_FLOORED': return a % b if b != 0 else 0
|
||||
if op == 'MODULO_TRUNCATED': return a - (b * int(a / b)) if b != 0 else 0
|
||||
except Exception:
|
||||
return 0
|
||||
return 0
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GetMesh(SliceNode):
|
||||
bl_label = "Get Mesh"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Object")
|
||||
self.add_output("SLICESOCK_Mesh")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject | None:
|
||||
if socket_name != "Mesh": return None
|
||||
|
||||
obj: Any = resolve_input(self.inputs.get("Object"))
|
||||
|
||||
so = SlicingObject(obj, obj.name)
|
||||
|
||||
return so
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GetPaths(SliceNode):
|
||||
bl_label = "Get Paths"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Object")
|
||||
self.add_output("SLICESOCK_Paths")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||
obj: Any = resolve_input(self.inputs.get("Object"))
|
||||
|
||||
so: list[np.ndarray] = capture_mesh_edges(obj)
|
||||
|
||||
return SlicePaths(so)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_SliceShells(SliceNode):
|
||||
bl_label = "Slice Shells"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Mesh")
|
||||
self.add_input("SLICESOCK_Float", "Layer Height").value = 0.2
|
||||
self.add_output("SLICESOCK_Paths")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||
if socket_name != "Paths": return SlicePaths()
|
||||
|
||||
slicingmesh: SlicingObject | None = resolve_input(self.inputs.get("Mesh"))
|
||||
layer_height: float = resolve_input(self.inputs.get("Layer Height"))
|
||||
|
||||
if not slicingmesh: return SlicePaths()
|
||||
|
||||
min = slicingmesh.mesh[:,0:3,2:3].min()
|
||||
min = floor(min / layer_height) * layer_height
|
||||
max = slicingmesh.mesh[:,0:3,2:3].max()
|
||||
|
||||
z_values = np.arange(min, max + layer_height, layer_height)
|
||||
|
||||
slices_group: list[list[np.ndarray]] | None = slice_at_range([slicingmesh.mesh], z_values)
|
||||
assert slices_group
|
||||
slices = slices_group[0]
|
||||
|
||||
return SlicePaths(slices)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_BufferShells(SliceNode):
|
||||
bl_label = "Buffer Shells"
|
||||
|
||||
shape_option: bpy.props.EnumProperty(
|
||||
name="Miter",
|
||||
items=[
|
||||
('round', "Round", ""),
|
||||
('square', "Square", ""),
|
||||
('flat', "Flat", "")
|
||||
]
|
||||
) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Paths")
|
||||
self.add_input("SLICESOCK_Float", "Distance").value = 0
|
||||
self.add_input("SLICESOCK_VectorRotation", "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) -> SlicePaths:
|
||||
if socket_name != "Paths": return SlicePaths()
|
||||
|
||||
paths: SlicePaths = resolve_input(self.inputs.get("Paths"))
|
||||
distance: float = resolve_input(self.inputs.get("Distance")) or 0.0
|
||||
rotation: Any = resolve_input(self.inputs.get("Rotation")) or (0.0, 0.0, 0.0)
|
||||
|
||||
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
|
||||
|
||||
# Buffer
|
||||
res_paths, res_groups = slice.buffer_slice(prepared_paths, distance, miter=self.shape_option)
|
||||
|
||||
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
|
||||
|
||||
return SlicePaths(final_paths, res_groups)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_Infill(SliceNode):
|
||||
bl_label = "Infill"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Paths")
|
||||
self.add_input("SLICESOCK_Float", "Density").value = 2.0
|
||||
self.add_input("SLICESOCK_Float", "Infill Rotation").value = 0.0
|
||||
self.add_input("SLICESOCK_VectorRotation", "Layer Rotation")
|
||||
self.add_output("SLICESOCK_Paths")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||
if socket_name != "Paths": return SlicePaths()
|
||||
|
||||
paths: SlicePaths = resolve_input(self.inputs.get("Paths"))
|
||||
density: float = resolve_input(self.inputs.get("Density")) or 2.0
|
||||
infill_rotation: float = resolve_input(self.inputs.get("Infill Rotation")) or 0.0
|
||||
rotation: Any = resolve_input(self.inputs.get("Layer Rotation")) or (0.0, 0.0, 0.0)
|
||||
|
||||
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, density, rotation_deg=infill_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"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Paths")
|
||||
self.add_input("SLICESOCK_Intrinsics")
|
||||
self.add_output("SLICESOCK_Gcode")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||
paths: list[np.ndarray] = resolve_input(self.inputs.get("Paths"))
|
||||
intrinsics: SliceIntrinsics | None = resolve_input(self.inputs.get("Intrinsics"))
|
||||
|
||||
pather: Pather = Pather(paths)
|
||||
if intrinsics:
|
||||
# Note: Pather currently uses bed_center_x/y as its internal attributes.
|
||||
# We map the bed size center to these attributes if provided.
|
||||
pather.bed_center_x = intrinsics.bed_x / 2.0
|
||||
pather.bed_center_y = intrinsics.bed_y / 2.0
|
||||
pather.feedrate = intrinsics.feedrate
|
||||
pather.extrusion_rate = intrinsics.extrusion_rate
|
||||
|
||||
gcode: str = pather.to_gcode()
|
||||
return gcode
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GcodeBlock(SliceNode):
|
||||
bl_label = "Gcode Block"
|
||||
|
||||
gcode: StringProperty(name="Gcode", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def init(self, context):
|
||||
self.add_output("SLICESOCK_Gcode")
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "gcode", text="")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||
return self.gcode
|
||||
|
||||
### Joiners
|
||||
|
||||
class SliceNodeJoiner(SliceNode):
|
||||
socket_type: str = ""
|
||||
output_socket_type: str = ""
|
||||
|
||||
def init(self, context):
|
||||
self.add_output(self.output_socket_type or self.socket_type)
|
||||
|
||||
def update(self) -> None:
|
||||
def is_active(input) -> bool:
|
||||
return input.is_linked or getattr(input, 'value', False)
|
||||
|
||||
if len(self.inputs) == 0:
|
||||
self.add_input(self.socket_type)
|
||||
return
|
||||
|
||||
if is_active(self.inputs[-1]):
|
||||
self.add_input(self.socket_type)
|
||||
|
||||
while True:
|
||||
if len(self.inputs) < 2: break
|
||||
if not is_active(self.inputs[-1]) and not is_active(self.inputs[-2]):
|
||||
self.inputs.remove(self.inputs[-2])
|
||||
continue
|
||||
break
|
||||
|
||||
@register_class
|
||||
class SLICENODE_JoinGcode(SliceNodeJoiner):
|
||||
bl_label = "Join Gcode"
|
||||
socket_type = "SLICESOCK_Gcode"
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||
gcode_input: list[str] = []
|
||||
for input in self.inputs:
|
||||
gcode_input += [resolve_input(input)] or [""]
|
||||
return "\n".join(gcode_input)
|
||||
|
||||
|
||||
@register_class
|
||||
class SLICENODE_JoinSlices(SliceNodeJoiner):
|
||||
bl_label = "Join Slices"
|
||||
socket_type = "SLICESOCK_Paths"
|
||||
|
||||
operation: bpy.props.EnumProperty(
|
||||
name="Operation",
|
||||
items=[
|
||||
('UNION', "Union", "Union of the slices"),
|
||||
('INTERSECTION', "Intersection", "Intersection of the slices"),
|
||||
('DIFFERENCE', "Subtract", "Clipping of the first slice"),
|
||||
('XOR', "Exclusive or", "Exclusive or of the slices"),
|
||||
],
|
||||
default='UNION'
|
||||
) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "operation", text="")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||
accumulated: SlicePaths = SlicePaths()
|
||||
current: SlicePaths = SlicePaths()
|
||||
for input in self.inputs:
|
||||
current = resolve_input(input)
|
||||
|
||||
if not current:
|
||||
continue
|
||||
|
||||
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()
|
||||
|
||||
return accumulated
|
||||
|
||||
@register_class
|
||||
class SLICENODE_JoinMesh(SliceNodeJoiner):
|
||||
bl_label = "Join Mesh"
|
||||
socket_type = "SLICESOCK_Mesh"
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject:
|
||||
merged_mesh = SlicingObject()
|
||||
|
||||
for input in self.inputs:
|
||||
mesh =resolve_input(input)
|
||||
if mesh:
|
||||
merged_mesh.mesh = np.concatenate([merged_mesh.mesh, mesh.mesh], axis=0)
|
||||
|
||||
return merged_mesh
|
||||
|
||||
@register_class
|
||||
class SLICENODE_JoinPaths(SliceNodeJoiner):
|
||||
bl_label = "Join Paths"
|
||||
socket_type = "SLICESOCK_Paths"
|
||||
|
||||
mode: bpy.props.EnumProperty(
|
||||
name="Mode",
|
||||
items=[
|
||||
('EXTEND', "Extend", "Append paths sequentially"),
|
||||
('ZIP', "Zip", "Interleave paths from all inputs"),
|
||||
],
|
||||
default='EXTEND'
|
||||
) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "mode", text="")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||
inputs_data: list[SlicePaths] = []
|
||||
for input in self.inputs:
|
||||
paths = resolve_input(input)
|
||||
if paths:
|
||||
inputs_data.append(paths)
|
||||
|
||||
if not inputs_data:
|
||||
return SlicePaths()
|
||||
|
||||
if self.mode == 'EXTEND':
|
||||
merged = SlicePaths()
|
||||
for paths in inputs_data:
|
||||
merged.extend_with_groups(paths)
|
||||
return merged
|
||||
|
||||
elif self.mode == 'ZIP':
|
||||
# Group-based Zip: sequentially add each group from all inputs.
|
||||
# E.g. Group 1 (In1), Group 1 (In2), Group 2 (In1), Group 2 (In2)...
|
||||
all_groups = set()
|
||||
grouped_inputs = []
|
||||
for p_list in inputs_data:
|
||||
d = {}
|
||||
for i, g in enumerate(p_list.groups):
|
||||
all_groups.add(g)
|
||||
d.setdefault(g, []).append(p_list[i])
|
||||
grouped_inputs.append(d)
|
||||
|
||||
sorted_groups = sorted(list(all_groups))
|
||||
|
||||
merged = SlicePaths()
|
||||
for g in sorted_groups:
|
||||
for d in grouped_inputs:
|
||||
group_paths = d.get(g, [])
|
||||
for p in group_paths:
|
||||
merged.append_with_group(p, g)
|
||||
return merged
|
||||
|
||||
return SlicePaths()
|
||||
|
||||
### I/O
|
||||
|
||||
@register_class
|
||||
class SLICENODE_LoadGcode(SliceNode):
|
||||
bl_label = "Load Gcode File"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_String", "Filepath")
|
||||
self.add_output("SLICESOCK_Gcode")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||
path = resolve_input(self.inputs.get("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 ""
|
||||
|
||||
class ExecSliceNode(SliceNode):
|
||||
button_label: str = 'Execute'
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.operator("slicenode.execute", text=self.button_label).node_name = self.name
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GcodeToFile(ExecSliceNode):
|
||||
bl_label = "Gcode To File"
|
||||
|
||||
button_label = "Generate File"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Gcode")
|
||||
self.add_input("SLICESOCK_String", "Filename")
|
||||
|
||||
def evaluate(self):
|
||||
gcode: str = resolve_input(self.inputs.get("Gcode"))
|
||||
filename: str = resolve_input(self.inputs.get("Filename"))
|
||||
|
||||
filepath = bpy.path.abspath(filename)
|
||||
os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
|
||||
|
||||
with open(filepath, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(gcode)
|
||||
|
||||
return 'ok'
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||
bl_label = "Gcode to Mesh"
|
||||
|
||||
button_label = "Update Debug Mesh"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Gcode")
|
||||
|
||||
def evaluate(self):
|
||||
gcode: str = resolve_input(self.inputs.get("Gcode"))
|
||||
if not gcode:
|
||||
return 'empty'
|
||||
|
||||
segments = []
|
||||
last_x, last_y, last_z = 0.0, 0.0, 0.0
|
||||
last_e = 0.0
|
||||
|
||||
offset_x, offset_y = 0.0, 0.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
|
||||
|
||||
line_no_comment = line.split(';')[0].strip()
|
||||
if not line_no_comment: continue
|
||||
|
||||
parts = line_no_comment.split()
|
||||
cmd = parts[0].upper()
|
||||
|
||||
params = {}
|
||||
for p in parts[1:]:
|
||||
if len(p) < 2: continue
|
||||
try:
|
||||
# Scale coordinates by 0.001 (mm to Blender units)
|
||||
val = float(p[1:])
|
||||
if p[0].upper() in 'XYZ':
|
||||
val *= 0.001
|
||||
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)
|
||||
|
||||
if cmd == 'G1':
|
||||
is_extruding = new_e > 0
|
||||
|
||||
if is_extruding:
|
||||
# Subtract offsets (scaled to Blender units) to return to original space
|
||||
ox, oy = offset_x * 0.001, offset_y * 0.001
|
||||
segments.append((
|
||||
(last_x - ox, last_y - oy, last_z),
|
||||
(new_x - ox, new_y - oy, new_z)
|
||||
))
|
||||
|
||||
last_x, last_y, last_z = new_x, new_y, new_z
|
||||
last_e = new_e
|
||||
|
||||
if not segments:
|
||||
return 'no segments'
|
||||
|
||||
# Create or find "Debug" mesh object
|
||||
mesh_name = "Debug"
|
||||
if mesh_name in bpy.data.objects:
|
||||
obj = bpy.data.objects[mesh_name]
|
||||
mesh = obj.data
|
||||
mesh.clear_geometry()
|
||||
else:
|
||||
mesh = bpy.data.meshes.new(mesh_name)
|
||||
obj = bpy.data.objects.new(mesh_name, mesh)
|
||||
assert bpy.context.collection
|
||||
bpy.context.collection.objects.link(obj)
|
||||
|
||||
vertices = []
|
||||
edges = []
|
||||
for i, (p1, p2) in enumerate(segments):
|
||||
vertices.extend([p1, p2])
|
||||
edges.append((2*i, 2*i + 1))
|
||||
|
||||
mesh.from_pydata(vertices, edges, [])
|
||||
mesh.update()
|
||||
|
||||
# Assign Geometry Nodes nodegroup for preview
|
||||
ng_name = "Nonplanar Slicing Preview"
|
||||
if ng_name in bpy.data.node_groups:
|
||||
ng = bpy.data.node_groups[ng_name]
|
||||
# Check if already assigned
|
||||
has_preview = False
|
||||
for mod in obj.modifiers:
|
||||
if mod.type == 'NODES' and mod.node_group == ng: # type: ignore
|
||||
has_preview = True
|
||||
break
|
||||
|
||||
if not has_preview:
|
||||
mod = obj.modifiers.new(name="Preview", type='NODES') # type: ignore
|
||||
mod.node_group = ng
|
||||
|
||||
return 'ok'
|
||||
|
||||
@register_class
|
||||
class SLICENODE_Test(ExecSliceNode):
|
||||
bl_label = "Test"
|
||||
|
||||
button_label = "Test"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Gcode")
|
||||
|
||||
def evaluate(self):
|
||||
_ = resolve_input(self.inputs.get("Gcode"))
|
||||
return 'ok'
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Operator
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@register_class
|
||||
class SLICENODE_OT_Execute(Operator):
|
||||
bl_idname = "slicenode.execute"
|
||||
bl_label = "Execute"
|
||||
bl_description = "Execute"
|
||||
|
||||
node_name: bpy.props.StringProperty() # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def execute(self, context) -> set['OperatorReturnItems']:
|
||||
node_tree = context.space_data.edit_tree # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
|
||||
node = node_tree.nodes[self.node_name]
|
||||
_ = node.evaluate()
|
||||
|
||||
self.report({"INFO"}, f"Execution Successful")
|
||||
return {"FINISHED"}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Menu
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@register_class
|
||||
class SLICENODE_MT_Menu(Menu):
|
||||
bl_idname = "SLICENODE_MT_Menu"
|
||||
bl_label = "SliceNodes"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
assert layout
|
||||
layout.operator("node.add_node", text="String").type = "SLICENODE_InputString"
|
||||
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="Integer Math").type = "SLICENODE_IntMath"
|
||||
layout.operator("node.add_node", text="Object").type = "SLICENODE_InputObject"
|
||||
layout.operator("node.add_node", text="Printer Intrinsics").type = "SLICENODE_InputIntrinsics"
|
||||
layout.operator("node.add_node", text="Test").type = "SLICENODE_Test"
|
||||
layout.operator("node.add_node", text="Get Mesh").type = "SLICENODE_GetMesh"
|
||||
layout.operator("node.add_node", text="Get Paths").type = "SLICENODE_GetPaths"
|
||||
layout.separator()
|
||||
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="Infill").type = "SLICENODE_Infill"
|
||||
layout.operator("node.add_node", text="Paths to Gcode").type = "SLICENODE_PathsToGcode"
|
||||
layout.separator()
|
||||
layout.operator("node.add_node", text="Gcode Block").type = "SLICENODE_GcodeBlock"
|
||||
layout.operator("node.add_node", text="Load Gcode").type = "SLICENODE_LoadGcode"
|
||||
layout.separator()
|
||||
layout.operator("node.add_node", text="Join Gcode").type = "SLICENODE_JoinGcode"
|
||||
layout.operator("node.add_node", text="Join Mesh").type = "SLICENODE_JoinMesh"
|
||||
layout.operator("node.add_node", text="Join Paths").type = "SLICENODE_JoinPaths"
|
||||
layout.operator("node.add_node", text="Boolean Paths").type = "SLICENODE_JoinSlices"
|
||||
layout.separator()
|
||||
layout.operator("node.add_node", text="Gcode to File").type = "SLICENODE_GcodeToFile"
|
||||
layout.operator("node.add_node", text="Gcode to Mesh").type = "SLICENODE_GcodeToMesh"
|
||||
|
||||
def draw_add_menu(self, context):
|
||||
space = context.space_data
|
||||
tree = getattr(space, "edit_tree", None)
|
||||
if tree and tree.bl_idname == "SLICETREE_NodeTree":
|
||||
self.layout.separator()
|
||||
self.layout.menu("SLICENODE_MT_Menu")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Registration
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
### Load collected modules
|
||||
modules = registry.get()
|
||||
|
||||
def register():
|
||||
registry.blender_register_classes()
|
||||
bpy.types.NODE_MT_add.append(draw_add_menu)
|
||||
|
||||
bpy.types.NODE_MT_add.append(tree.draw_add_menu)
|
||||
print('Nodeslicer Loaded')
|
||||
|
||||
def unregister():
|
||||
bpy.types.NODE_MT_add.remove(draw_add_menu)
|
||||
bpy.types.NODE_MT_add.remove(tree.draw_add_menu)
|
||||
registry.blender_unregister_classes()
|
||||
|
||||
print('Nodeslicer Unloaded')
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
0
core/nodeslicer/__init__.py
Normal file
0
core/nodeslicer/__init__.py
Normal file
792
core/nodeslicer/nodes.py
Normal file
792
core/nodeslicer/nodes.py
Normal file
|
|
@ -0,0 +1,792 @@
|
|||
from __future__ import annotations
|
||||
import os
|
||||
from math import floor
|
||||
from typing import Any
|
||||
import bpy
|
||||
import numpy as np
|
||||
from mathutils import Euler
|
||||
from bpy.props import StringProperty, FloatProperty, IntProperty
|
||||
|
||||
from ...registry import register_class
|
||||
from .. import slice
|
||||
from ..slice import Pather, slice_at_range, SlicePaths
|
||||
from ...infra.blender_mesh_capture import SlicingObject, capture_mesh_edges
|
||||
from ...infra.blender_slice_nodes_generic import SliceNode, resolve_input
|
||||
|
||||
class SliceIntrinsics:
|
||||
bed_x: float
|
||||
bed_y: float
|
||||
feedrate: float
|
||||
extrusion_rate: float
|
||||
|
||||
def __init__(self, bed_x=250.0, bed_y=250.0, feedrate=1200.0, extrusion_rate=0.017):
|
||||
self.bed_x = bed_x
|
||||
self.bed_y = bed_y
|
||||
self.feedrate = feedrate
|
||||
self.extrusion_rate = extrusion_rate
|
||||
|
||||
# Basic Inputs
|
||||
|
||||
class InputNode(SliceNode):
|
||||
input_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:
|
||||
self.add_output(f"SLICESOCK_{self.bl_label}")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs):
|
||||
return self.outputs.get(socket_name).value
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputString(InputNode):
|
||||
input_type = "String"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputFloat(InputNode):
|
||||
input_type = "Float"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputInt(InputNode):
|
||||
input_type = "Int"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputVectorRotation(InputNode):
|
||||
input_type = "VectorRotation"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputObject(InputNode):
|
||||
input_type = "Object"
|
||||
bl_icon = "OBJECT_DATA"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputIntrinsics(SliceNode):
|
||||
bl_label = "Printer Intrinsics"
|
||||
|
||||
bed_x: FloatProperty(name="Bed X", default=250.0) # pyright: ignore[reportInvalidTypeForm]
|
||||
bed_y: FloatProperty(name="Bed Y", default=250.0) # pyright: ignore[reportInvalidTypeForm]
|
||||
feedrate: FloatProperty(name="Feedrate", default=1200.0) # pyright: ignore[reportInvalidTypeForm]
|
||||
extrusion_rate: FloatProperty(name="Extrusion Rate", default=0.017, precision=4) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def init(self, context):
|
||||
self.add_output("SLICESOCK_Intrinsics")
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "bed_x")
|
||||
layout.prop(self, "bed_y")
|
||||
layout.prop(self, "feedrate")
|
||||
layout.prop(self, "extrusion_rate")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SliceIntrinsics:
|
||||
return SliceIntrinsics(
|
||||
bed_x=self.bed_x,
|
||||
bed_y=self.bed_y,
|
||||
feedrate=self.feedrate,
|
||||
extrusion_rate=self.extrusion_rate
|
||||
)
|
||||
|
||||
# Math
|
||||
|
||||
@register_class
|
||||
class SLICENODE_IntMath(SliceNode):
|
||||
bl_label = "Integer Math"
|
||||
|
||||
operation: bpy.props.EnumProperty(
|
||||
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", "Integer 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'
|
||||
) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def init(self, context):
|
||||
self.add_output("SLICESOCK_IntRo", "Value")
|
||||
|
||||
self.add_input("SLICESOCK_Int", "Value")
|
||||
self.add_input("SLICESOCK_Int", "Value")
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "operation", text="")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> int:
|
||||
a: int = resolve_input(self.inputs[0])
|
||||
b: int = resolve_input(self.inputs[1])
|
||||
|
||||
op = self.operation
|
||||
try:
|
||||
if op == 'ADD': return a + b
|
||||
if op == 'SUBTRACT': return a - b
|
||||
if op == 'MULTIPLY': return a * b
|
||||
if op == 'DIVIDE': return a // b if b != 0 else 0
|
||||
if op == 'ABS': return abs(a)
|
||||
if op == 'NEGATE': return -a
|
||||
if op == 'POWER': return int(a ** b) if not (a == 0 and b < 0) else 0
|
||||
if op == 'MIN': return min(a, b)
|
||||
if op == 'MAX': return max(a, b)
|
||||
if op == 'SIGN': return (a > 0) - (a < 0)
|
||||
if op == 'DIVIDE_ROUND': return int(round(a / b)) if b != 0 else 0
|
||||
if op == 'DIVIDE_FLOOR': return a // b if b != 0 else 0
|
||||
if op == 'DIVIDE_TRUNCATE': return int(a / b) if b != 0 else 0
|
||||
if op == 'MODULO_FLOORED': return a % b if b != 0 else 0
|
||||
if op == 'MODULO_TRUNCATED': return a - (b * int(a / b)) if b != 0 else 0
|
||||
except Exception:
|
||||
return 0
|
||||
return 0
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GetMesh(SliceNode):
|
||||
bl_label = "Get Mesh"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Object")
|
||||
self.add_output("SLICESOCK_Mesh")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject | None:
|
||||
if socket_name != "Mesh": return None
|
||||
|
||||
obj: Any = resolve_input(self.inputs.get("Object"))
|
||||
|
||||
so = SlicingObject(obj, obj.name)
|
||||
|
||||
return so
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GetPaths(SliceNode):
|
||||
bl_label = "Get Paths"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Object")
|
||||
self.add_output("SLICESOCK_Paths")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||
obj: Any = resolve_input(self.inputs.get("Object"))
|
||||
|
||||
so: list[np.ndarray] = capture_mesh_edges(obj)
|
||||
|
||||
return SlicePaths(so)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_SliceShells(SliceNode):
|
||||
bl_label = "Slice Shells"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Mesh")
|
||||
self.add_input("SLICESOCK_Float", "Layer Height").value = 0.2
|
||||
self.add_output("SLICESOCK_Paths")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||
if socket_name != "Paths": return SlicePaths()
|
||||
|
||||
slicingmesh: SlicingObject | None = resolve_input(self.inputs.get("Mesh"))
|
||||
layer_height: float = resolve_input(self.inputs.get("Layer Height"))
|
||||
|
||||
if not slicingmesh: return SlicePaths()
|
||||
|
||||
min = slicingmesh.mesh[:,0:3,2:3].min()
|
||||
min = floor(min / layer_height) * layer_height
|
||||
max = slicingmesh.mesh[:,0:3,2:3].max()
|
||||
|
||||
z_values = np.arange(min, max + layer_height, layer_height)
|
||||
|
||||
slices_group: list[list[np.ndarray]] | None = slice_at_range([slicingmesh.mesh], z_values)
|
||||
assert slices_group
|
||||
slices = slices_group[0]
|
||||
|
||||
return SlicePaths(slices)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_BufferShells(SliceNode):
|
||||
bl_label = "Buffer Shells"
|
||||
|
||||
shape_option: bpy.props.EnumProperty(
|
||||
name="Miter",
|
||||
items=[
|
||||
('round', "Round", ""),
|
||||
('square', "Square", ""),
|
||||
('flat', "Flat", "")
|
||||
]
|
||||
) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Paths")
|
||||
self.add_input("SLICESOCK_Float", "Distance").value = 0
|
||||
self.add_input("SLICESOCK_VectorRotation", "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) -> SlicePaths:
|
||||
if socket_name != "Paths": return SlicePaths()
|
||||
|
||||
paths: SlicePaths = resolve_input(self.inputs.get("Paths"))
|
||||
distance: float = resolve_input(self.inputs.get("Distance")) or 0.0
|
||||
rotation: Any = resolve_input(self.inputs.get("Rotation")) or (0.0, 0.0, 0.0)
|
||||
|
||||
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
|
||||
|
||||
# Buffer
|
||||
res_paths, res_groups = slice.buffer_slice(prepared_paths, distance, miter=self.shape_option)
|
||||
|
||||
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
|
||||
|
||||
return SlicePaths(final_paths, res_groups)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_BufferShellsMultiple(SliceNode):
|
||||
bl_label = "Buffer Shells Multiple"
|
||||
|
||||
shape_option: bpy.props.EnumProperty(
|
||||
name="Miter",
|
||||
items=[
|
||||
('round', "Round", ""),
|
||||
('square', "Square", ""),
|
||||
('flat', "Flat", "")
|
||||
]
|
||||
) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Paths")
|
||||
self.add_input("SLICESOCK_Float", "Distance").value = 0
|
||||
self.add_input("SLICESOCK_Float", "Width Step").value = 0.4
|
||||
self.add_input("SLICESOCK_Int", "Count").value = 2
|
||||
self.add_input("SLICESOCK_VectorRotation", "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) -> SlicePaths:
|
||||
if socket_name != "Paths": return SlicePaths()
|
||||
|
||||
paths: SlicePaths = resolve_input(self.inputs.get("Paths"))
|
||||
base_distance: float = resolve_input(self.inputs.get("Distance")) or 0.0
|
||||
width_step: float = resolve_input(self.inputs.get("Width Step")) or 0.0
|
||||
count: int = resolve_input(self.inputs.get("Count")) or 1
|
||||
rotation: Any = resolve_input(self.inputs.get("Rotation")) or (0.0, 0.0, 0.0)
|
||||
|
||||
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_res_paths = []
|
||||
all_res_groups = []
|
||||
|
||||
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_res_paths.extend(res_paths)
|
||||
all_res_groups.extend(res_groups)
|
||||
|
||||
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 all_res_paths]
|
||||
else:
|
||||
final_paths = all_res_paths
|
||||
|
||||
return SlicePaths(final_paths, all_res_groups)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_Infill(SliceNode):
|
||||
bl_label = "Infill"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Paths")
|
||||
self.add_input("SLICESOCK_Float", "Density").value = 2.0
|
||||
self.add_input("SLICESOCK_Float", "Infill Rotation").value = 0.0
|
||||
self.add_input("SLICESOCK_VectorRotation", "Layer Rotation")
|
||||
self.add_output("SLICESOCK_Paths")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||
if socket_name != "Paths": return SlicePaths()
|
||||
|
||||
paths: SlicePaths = resolve_input(self.inputs.get("Paths"))
|
||||
density: float = resolve_input(self.inputs.get("Density")) or 2.0
|
||||
infill_rotation: float = resolve_input(self.inputs.get("Infill Rotation")) or 0.0
|
||||
rotation: Any = resolve_input(self.inputs.get("Layer Rotation")) or (0.0, 0.0, 0.0)
|
||||
|
||||
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, density, rotation_deg=infill_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"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Paths")
|
||||
self.add_input("SLICESOCK_Intrinsics")
|
||||
self.add_output("SLICESOCK_Gcode")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||
paths: list[np.ndarray] = resolve_input(self.inputs.get("Paths"))
|
||||
intrinsics: SliceIntrinsics | None = resolve_input(self.inputs.get("Intrinsics"))
|
||||
|
||||
pather: Pather = Pather(paths)
|
||||
if intrinsics:
|
||||
# Note: Pather currently uses bed_center_x/y as its internal attributes.
|
||||
# We map the bed size center to these attributes if provided.
|
||||
pather.bed_center_x = intrinsics.bed_x / 2.0
|
||||
pather.bed_center_y = intrinsics.bed_y / 2.0
|
||||
pather.feedrate = intrinsics.feedrate
|
||||
pather.extrusion_rate = intrinsics.extrusion_rate
|
||||
|
||||
gcode: str = pather.to_gcode()
|
||||
return gcode
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GcodeBlock(SliceNode):
|
||||
bl_label = "Gcode Block"
|
||||
|
||||
gcode: StringProperty(name="Gcode", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def init(self, context):
|
||||
self.add_output("SLICESOCK_Gcode")
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "gcode", text="")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||
return self.gcode
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GcodeHelper(SliceNode):
|
||||
bl_label = "Gcode Helper"
|
||||
|
||||
command: bpy.props.EnumProperty(
|
||||
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'
|
||||
) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
parameters: StringProperty(name="Params", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def init(self, context):
|
||||
self.add_output("SLICESOCK_Gcode")
|
||||
|
||||
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
|
||||
|
||||
### Joiners
|
||||
|
||||
class SliceNodeJoiner(SliceNode):
|
||||
socket_type: str = ""
|
||||
output_socket_type: str = ""
|
||||
|
||||
def init(self, context):
|
||||
self.add_output(self.output_socket_type or self.socket_type)
|
||||
|
||||
def update(self) -> None:
|
||||
def is_active(input) -> bool:
|
||||
return input.is_linked or getattr(input, 'value', False)
|
||||
|
||||
if len(self.inputs) == 0:
|
||||
self.add_input(self.socket_type)
|
||||
return
|
||||
|
||||
if is_active(self.inputs[-1]):
|
||||
self.add_input(self.socket_type)
|
||||
|
||||
while True:
|
||||
if len(self.inputs) < 2: break
|
||||
if not is_active(self.inputs[-1]) and not is_active(self.inputs[-2]):
|
||||
self.inputs.remove(self.inputs[-2])
|
||||
continue
|
||||
break
|
||||
|
||||
@register_class
|
||||
class SLICENODE_JoinGcode(SliceNodeJoiner):
|
||||
bl_label = "Join Gcode"
|
||||
socket_type = "SLICESOCK_Gcode"
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||
gcode_input: list[str] = []
|
||||
for input in self.inputs:
|
||||
gcode_input += [resolve_input(input)] or [""]
|
||||
return "\n".join(gcode_input)
|
||||
|
||||
|
||||
@register_class
|
||||
class SLICENODE_JoinSlices(SliceNodeJoiner):
|
||||
bl_label = "Join Slices"
|
||||
socket_type = "SLICESOCK_Paths"
|
||||
|
||||
operation: bpy.props.EnumProperty(
|
||||
name="Operation",
|
||||
items=[
|
||||
('UNION', "Union", "Union of the slices"),
|
||||
('INTERSECTION', "Intersection", "Intersection of the slices"),
|
||||
('DIFFERENCE', "Subtract", "Clipping of the first slice"),
|
||||
('XOR', "Exclusive or", "Exclusive or of the slices"),
|
||||
],
|
||||
default='UNION'
|
||||
) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "operation", text="")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||
accumulated: SlicePaths = SlicePaths()
|
||||
current: SlicePaths = SlicePaths()
|
||||
for input in self.inputs:
|
||||
current = resolve_input(input)
|
||||
|
||||
if not current:
|
||||
continue
|
||||
|
||||
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()
|
||||
|
||||
return accumulated
|
||||
|
||||
@register_class
|
||||
class SLICENODE_JoinMesh(SliceNodeJoiner):
|
||||
bl_label = "Join Mesh"
|
||||
socket_type = "SLICESOCK_Mesh"
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject:
|
||||
merged_mesh = SlicingObject()
|
||||
|
||||
for input in self.inputs:
|
||||
mesh =resolve_input(input)
|
||||
if mesh:
|
||||
merged_mesh.mesh = np.concatenate([merged_mesh.mesh, mesh.mesh], axis=0)
|
||||
|
||||
return merged_mesh
|
||||
|
||||
@register_class
|
||||
class SLICENODE_JoinPaths(SliceNodeJoiner):
|
||||
bl_label = "Join Paths"
|
||||
socket_type = "SLICESOCK_Paths"
|
||||
|
||||
mode: bpy.props.EnumProperty(
|
||||
name="Mode",
|
||||
items=[
|
||||
('EXTEND', "Extend", "Append paths sequentially"),
|
||||
('ZIP', "Zip", "Interleave paths from all inputs"),
|
||||
],
|
||||
default='EXTEND'
|
||||
) # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "mode", text="")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||
inputs_data: list[SlicePaths] = []
|
||||
for input in self.inputs:
|
||||
paths = resolve_input(input)
|
||||
if paths:
|
||||
inputs_data.append(paths)
|
||||
|
||||
if not inputs_data:
|
||||
return SlicePaths()
|
||||
|
||||
if self.mode == 'EXTEND':
|
||||
merged = SlicePaths()
|
||||
for paths in inputs_data:
|
||||
merged.extend_with_groups(paths)
|
||||
return merged
|
||||
|
||||
elif self.mode == 'ZIP':
|
||||
# Group-based Zip: sequentially add each group from all inputs.
|
||||
# E.g. Group 1 (In1), Group 1 (In2), Group 2 (In1), Group 2 (In2)...
|
||||
all_groups = set()
|
||||
grouped_inputs = []
|
||||
for p_list in inputs_data:
|
||||
d = {}
|
||||
for i, g in enumerate(p_list.groups):
|
||||
all_groups.add(g)
|
||||
d.setdefault(g, []).append(p_list[i])
|
||||
grouped_inputs.append(d)
|
||||
|
||||
sorted_groups = sorted(list(all_groups))
|
||||
|
||||
merged = SlicePaths()
|
||||
for g in sorted_groups:
|
||||
for d in grouped_inputs:
|
||||
group_paths = d.get(g, [])
|
||||
for p in group_paths:
|
||||
merged.append_with_group(p, g)
|
||||
return merged
|
||||
|
||||
return SlicePaths()
|
||||
|
||||
### I/O
|
||||
|
||||
@register_class
|
||||
class SLICENODE_LoadGcode(SliceNode):
|
||||
bl_label = "Load Gcode File"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_FilePath", "Filepath")
|
||||
self.add_output("SLICESOCK_Gcode")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||
path = resolve_input(self.inputs.get("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 ""
|
||||
|
||||
class ExecSliceNode(SliceNode):
|
||||
button_label: str = 'Execute'
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.operator("slicenode.execute", text=self.button_label).node_name = self.name
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GcodeToFile(ExecSliceNode):
|
||||
bl_label = "Gcode To File"
|
||||
|
||||
button_label = "Generate File"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Gcode")
|
||||
self.add_input("SLICESOCK_FilePath", "Filename")
|
||||
|
||||
def evaluate(self):
|
||||
gcode: str = resolve_input(self.inputs.get("Gcode"))
|
||||
filename: str = resolve_input(self.inputs.get("Filename"))
|
||||
|
||||
filepath = bpy.path.abspath(filename)
|
||||
os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
|
||||
|
||||
with open(filepath, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(gcode)
|
||||
|
||||
return 'ok'
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||
bl_label = "Gcode to Mesh"
|
||||
|
||||
button_label = "Update Debug Mesh"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Gcode")
|
||||
|
||||
def evaluate(self):
|
||||
gcode: str = resolve_input(self.inputs.get("Gcode"))
|
||||
if not gcode:
|
||||
return 'empty'
|
||||
|
||||
segments = []
|
||||
last_x, last_y, last_z = 0.0, 0.0, 0.0
|
||||
last_e = 0.0
|
||||
|
||||
offset_x, offset_y = 0.0, 0.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
|
||||
|
||||
line_no_comment = line.split(';')[0].strip()
|
||||
if not line_no_comment: continue
|
||||
|
||||
parts = line_no_comment.split()
|
||||
cmd = parts[0].upper()
|
||||
|
||||
params = {}
|
||||
for p in parts[1:]:
|
||||
if len(p) < 2: continue
|
||||
try:
|
||||
# Scale coordinates by 0.001 (mm to Blender units)
|
||||
val = float(p[1:])
|
||||
if p[0].upper() in 'XYZ':
|
||||
val *= 0.001
|
||||
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)
|
||||
|
||||
if cmd == 'G1':
|
||||
is_extruding = new_e > 0
|
||||
|
||||
if is_extruding:
|
||||
# Subtract offsets (scaled to Blender units) to return to original space
|
||||
ox, oy = offset_x * 0.001, offset_y * 0.001
|
||||
segments.append((
|
||||
(last_x - ox, last_y - oy, last_z),
|
||||
(new_x - ox, new_y - oy, new_z)
|
||||
))
|
||||
|
||||
last_x, last_y, last_z = new_x, new_y, new_z
|
||||
last_e = new_e
|
||||
|
||||
if not segments:
|
||||
return 'no segments'
|
||||
|
||||
# Create or find "Debug" mesh object
|
||||
mesh_name = "Debug"
|
||||
if mesh_name in bpy.data.objects:
|
||||
obj = bpy.data.objects[mesh_name]
|
||||
mesh = obj.data
|
||||
mesh.clear_geometry()
|
||||
else:
|
||||
mesh = bpy.data.meshes.new(mesh_name)
|
||||
obj = bpy.data.objects.new(mesh_name, mesh)
|
||||
assert bpy.context.collection
|
||||
bpy.context.collection.objects.link(obj)
|
||||
|
||||
vertices = []
|
||||
edges = []
|
||||
for i, (p1, p2) in enumerate(segments):
|
||||
vertices.extend([p1, p2])
|
||||
edges.append((2*i, 2*i + 1))
|
||||
|
||||
mesh.from_pydata(vertices, edges, [])
|
||||
mesh.update()
|
||||
|
||||
# Assign Geometry Nodes nodegroup for preview
|
||||
ng_name = "Nonplanar Slicing Preview"
|
||||
if ng_name in bpy.data.node_groups:
|
||||
ng = bpy.data.node_groups[ng_name]
|
||||
# Check if already assigned
|
||||
has_preview = False
|
||||
for mod in obj.modifiers:
|
||||
if mod.type == 'NODES' and mod.node_group == ng: # type: ignore
|
||||
has_preview = True
|
||||
break
|
||||
|
||||
if not has_preview:
|
||||
mod = obj.modifiers.new(name="Preview", type='NODES') # type: ignore
|
||||
mod.node_group = ng
|
||||
|
||||
return 'ok'
|
||||
|
||||
@register_class
|
||||
class SLICENODE_Test(ExecSliceNode):
|
||||
bl_label = "Test"
|
||||
|
||||
button_label = "Test"
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Gcode")
|
||||
|
||||
def evaluate(self):
|
||||
_ = resolve_input(self.inputs.get("Gcode"))
|
||||
return 'ok'
|
||||
116
core/nodeslicer/sockets.py
Normal file
116
core/nodeslicer/sockets.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
from __future__ import annotations
|
||||
from typing import Any
|
||||
import bpy
|
||||
from bpy.props import PointerProperty, StringProperty, FloatProperty, IntProperty
|
||||
|
||||
from ...registry import register_class
|
||||
from ...infra.blender_slice_nodes_generic import SliceNodeSocket
|
||||
from ..slice import SlicePaths
|
||||
|
||||
class LabelNodeSocket(SliceNodeSocket):
|
||||
default = ""
|
||||
def draw(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||
assert layout
|
||||
layout.label(text=text or self.name)
|
||||
|
||||
class InputNodeSocket(SliceNodeSocket):
|
||||
def draw(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||
assert layout
|
||||
if not self.is_output and self.is_linked:
|
||||
layout.label(text=text or self.name)
|
||||
else:
|
||||
layout.prop(self, "value", text=text or self.name)
|
||||
|
||||
def _draw_as_ro(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||
layout.label(text=text or self.name)
|
||||
|
||||
def _draw_as_rw(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||
layout.prop(self, "value", text=text or self.name)
|
||||
|
||||
class ReadonlySocket():
|
||||
def draw(self, context: Any, layout: Any, node: Any, text: str | None):
|
||||
return self._draw_as_ro(context, layout, node, text) # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Object(InputNodeSocket):
|
||||
bl_label = "Object"
|
||||
|
||||
value: PointerProperty(
|
||||
name="Object",
|
||||
type=bpy.types.Object,
|
||||
) # pyright: ignore[reportInvalidTypeForm]
|
||||
color = (0.9, 0.7, 0.2, 1.0)
|
||||
|
||||
|
||||
class SLICESOCK_Float_Common(InputNodeSocket):
|
||||
bl_label = "Float"
|
||||
|
||||
value: FloatProperty(name="Float", default=0) # pyright: ignore[reportInvalidTypeForm]
|
||||
color = (161/255, 161/255, 161/255, 1.0)
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_FloatRo(ReadonlySocket, SLICESOCK_Float_Common): ...
|
||||
@register_class
|
||||
class SLICESOCK_Float(SLICESOCK_Float_Common): ...
|
||||
|
||||
|
||||
class SLICESOCK_Int_Common(InputNodeSocket):
|
||||
bl_label = "Integer"
|
||||
|
||||
value: IntProperty(name="Int", default=0) # pyright: ignore[reportInvalidTypeForm]
|
||||
color = (161/255, 161/255, 161/255, 1.0)
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Int(SLICESOCK_Int_Common): ...
|
||||
@register_class
|
||||
class SLICESOCK_IntRo(ReadonlySocket, SLICESOCK_Int_Common): ...
|
||||
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_String(InputNodeSocket):
|
||||
bl_label = "String"
|
||||
|
||||
value: StringProperty(name="String", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||
color = (0.4, 0.7, 1.0, 1.0)
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_FilePath(InputNodeSocket):
|
||||
bl_label = "Filepath"
|
||||
|
||||
value: StringProperty(name="Filepath", default="", subtype='FILE_PATH') # pyright: ignore[reportInvalidTypeForm]
|
||||
color = (0.4, 0.7, 1.0, 1.0)
|
||||
|
||||
@register_class
|
||||
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]
|
||||
color = (0.39, 0.39, 0.78, 1.0)
|
||||
@register_class
|
||||
class SLICESOCK_VectorRotation(SLICESOCK_VectorRotation_Common): ...
|
||||
@register_class
|
||||
class SLICESOCK_VectorRotationRo(ReadonlySocket, SLICESOCK_VectorRotation_Common): ...
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Mesh(LabelNodeSocket):
|
||||
bl_label = "Mesh"
|
||||
default = None
|
||||
color = (0, 214/255, 163/255, 1.0)
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Paths(LabelNodeSocket):
|
||||
bl_label = "Paths"
|
||||
default = SlicePaths()
|
||||
color = (214/255, 0, 120/255, 1.0)
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Gcode(LabelNodeSocket):
|
||||
bl_label = "Gcode"
|
||||
default = ""
|
||||
color = (1., 1., 1., 1.)
|
||||
|
||||
@register_class
|
||||
class SLICESOCK_Intrinsics(LabelNodeSocket):
|
||||
bl_label = "Intrinsics"
|
||||
default = None
|
||||
color = (0.2, 0.2, 0.2, 1.0)
|
||||
75
core/nodeslicer/tree.py
Normal file
75
core/nodeslicer/tree.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
import bpy
|
||||
from bpy.types import NodeTree, Operator, Menu
|
||||
|
||||
from ...registry import register_class
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bpy.stub_internal.rna_enums import OperatorReturnItems
|
||||
|
||||
@register_class
|
||||
class SLICETREE_NodeTree(NodeTree):
|
||||
bl_idname = "SLICETREE_NodeTree"
|
||||
bl_label = "NodeSlicer Tree"
|
||||
bl_icon = "NODETREE"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_OT_Execute(Operator):
|
||||
bl_idname = "slicenode.execute"
|
||||
bl_label = "Execute"
|
||||
bl_description = "Execute"
|
||||
|
||||
node_name: bpy.props.StringProperty() # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def execute(self, context) -> set['OperatorReturnItems']:
|
||||
node_tree = context.space_data.edit_tree # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
|
||||
node = node_tree.nodes[self.node_name]
|
||||
_ = node.evaluate()
|
||||
|
||||
self.report({"INFO"}, f"Execution Successful")
|
||||
return {"FINISHED"}
|
||||
|
||||
@register_class
|
||||
class SLICENODE_MT_Menu(Menu):
|
||||
bl_idname = "SLICENODE_MT_Menu"
|
||||
bl_label = "SliceNodes"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
assert layout
|
||||
layout.operator("node.add_node", text="String").type = "SLICENODE_InputString"
|
||||
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="Integer Math").type = "SLICENODE_IntMath"
|
||||
layout.operator("node.add_node", text="Object").type = "SLICENODE_InputObject"
|
||||
layout.operator("node.add_node", text="Printer Intrinsics").type = "SLICENODE_InputIntrinsics"
|
||||
layout.operator("node.add_node", text="Test").type = "SLICENODE_Test"
|
||||
layout.operator("node.add_node", text="Get Mesh").type = "SLICENODE_GetMesh"
|
||||
layout.operator("node.add_node", text="Get Paths").type = "SLICENODE_GetPaths"
|
||||
layout.separator()
|
||||
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="Infill").type = "SLICENODE_Infill"
|
||||
layout.operator("node.add_node", text="Paths to Gcode").type = "SLICENODE_PathsToGcode"
|
||||
layout.separator()
|
||||
layout.operator("node.add_node", text="Gcode Block").type = "SLICENODE_GcodeBlock"
|
||||
layout.operator("node.add_node", text="Gcode Helper").type = "SLICENODE_GcodeHelper"
|
||||
layout.operator("node.add_node", text="Load Gcode").type = "SLICENODE_LoadGcode"
|
||||
layout.separator()
|
||||
layout.operator("node.add_node", text="Join Gcode").type = "SLICENODE_JoinGcode"
|
||||
layout.operator("node.add_node", text="Join Mesh").type = "SLICENODE_JoinMesh"
|
||||
layout.operator("node.add_node", text="Join Paths").type = "SLICENODE_JoinPaths"
|
||||
layout.operator("node.add_node", text="Boolean Paths").type = "SLICENODE_JoinSlices"
|
||||
layout.separator()
|
||||
layout.operator("node.add_node", text="Gcode to File").type = "SLICENODE_GcodeToFile"
|
||||
layout.operator("node.add_node", text="Gcode to Mesh").type = "SLICENODE_GcodeToMesh"
|
||||
|
||||
def draw_add_menu(self, context):
|
||||
space = context.space_data
|
||||
tree = getattr(space, "edit_tree", None)
|
||||
if tree and tree.bl_idname == "SLICETREE_NodeTree":
|
||||
self.layout.separator()
|
||||
self.layout.menu("SLICENODE_MT_Menu")
|
||||
19
gcode_snippets/mk3s_footer.gcode
Normal file
19
gcode_snippets/mk3s_footer.gcode
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
M107
|
||||
;TYPE:Custom
|
||||
; Filament-specific end gcode
|
||||
G1 Z21 F720 ; Move print head up
|
||||
G1 X0 Y200 F3600 ; park
|
||||
G1 Z69 F720 ; Move print head further up
|
||||
G4 ; wait
|
||||
M221 S100 ; reset flow
|
||||
M900 K0 ; reset LA
|
||||
|
||||
M104 S0 ; turn off temperature
|
||||
M140 S0 ; turn off heatbed
|
||||
M107 ; turn off fan
|
||||
M84 ; disable motors
|
||||
|
||||
M73 P100 R0
|
||||
M73 Q100 S0
|
||||
|
||||
M84 ; disable motors
|
||||
55
gcode_snippets/mk3s_header.gcode
Normal file
55
gcode_snippets/mk3s_header.gcode
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
; generated by PrusaSlicer 2.9.4 on 2026-05-10 at 11:51:50 UTC
|
||||
|
||||
;
|
||||
|
||||
; external perimeters extrusion width = 0.45mm
|
||||
; perimeters extrusion width = 0.45mm
|
||||
; infill extrusion width = 0.45mm
|
||||
; solid infill extrusion width = 0.45mm
|
||||
; top infill extrusion width = 0.40mm
|
||||
; first layer extrusion width = 0.42mm
|
||||
|
||||
M73 P0 R24
|
||||
M73 Q0 S24
|
||||
M201 X1000 Y1000 Z200 E5000 ; sets maximum accelerations, mm/sec^2
|
||||
M203 X200 Y200 Z12 E120 ; sets maximum feedrates, mm / sec
|
||||
M204 S1250 T1250 ; sets acceleration (S) and retract acceleration (R), mm/sec^2
|
||||
M205 X8.00 Y8.00 Z0.40 E4.50 ; sets the jerk limits, mm/sec
|
||||
M205 S0 T0 ; sets the minimum extruding and travel feed rate, mm/sec
|
||||
|
||||
; printing object Cube.016 id:0 copy 0
|
||||
; stop printing object Cube.016 id:0 copy 0
|
||||
|
||||
;TYPE:Custom
|
||||
M862.3 P "MK3S" ; printer model check
|
||||
M862.1 P0.4 ; nozzle diameter check
|
||||
M115 U3.14.1 ; tell printer latest fw version
|
||||
G90 ; use absolute coordinates
|
||||
M83 ; extruder relative mode
|
||||
M104 S215 ; set extruder temp
|
||||
M140 S60 ; set bed temp
|
||||
M190 S60 ; wait for bed temp
|
||||
M109 S215 ; wait for extruder temp
|
||||
G28 W ; home all without mesh bed level
|
||||
G80 X112.246 Y92.2458 W25.5083 H25.5083 ; mesh bed levelling
|
||||
|
||||
G1 Z0.2 F720
|
||||
G1 Y-3 F1000 ; go outside print area
|
||||
G92 E0
|
||||
G1 X60 E9 F1000 ; intro line
|
||||
G1 X100 E12.5 F1000 ; intro line
|
||||
|
||||
G92 E0
|
||||
M221 S95
|
||||
|
||||
; Don't change E values below. Excessive value can damage the printer.
|
||||
|
||||
M907 E538 ; set extruder motor current
|
||||
G21 ; set units to millimeters
|
||||
G90 ; use absolute coordinates
|
||||
M83 ; use relative distances for extrusion
|
||||
|
||||
M900 K0.05 ; Filament gcode LA 1.5
|
||||
M900 K30 ; Filament gcode LA 1.0
|
||||
|
||||
M108
|
||||
2
gcode_snippets/redefine_z_0-1.gcode
Normal file
2
gcode_snippets/redefine_z_0-1.gcode
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
G0 Z0.1 F720 ; Move to the desired physical Z clearance height
|
||||
G92 Z0 ; Redefine this position as Z=0
|
||||
Loading…
Add table
Reference in a new issue