obj to path redo
This commit is contained in:
parent
cf7749b9b7
commit
0e6b6f4b92
4 changed files with 108 additions and 116 deletions
71
__init__.py
71
__init__.py
|
|
@ -1,4 +1,6 @@
|
|||
from __future__ import annotations
|
||||
from numpy._typing._array_like import NDArray
|
||||
from numpy import float64
|
||||
|
||||
bl_info = {
|
||||
"name": "NodeSlicer",
|
||||
|
|
@ -16,14 +18,14 @@ import bpy
|
|||
import os
|
||||
import numpy as np
|
||||
|
||||
from bpy.types import NodeTree, Node, NodeSocket, Operator, Menu
|
||||
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
|
||||
from .infra.blender_mesh_capture import SlicingObject, capture_mesh_edges
|
||||
from .infra.blender_slice_nodes_generic import SliceNodeSocket
|
||||
from .infra.blender_slice_nodes_generic import SliceNode, SliceNodeSocket, resolve_input
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bpy.stub_internal.rna_enums import OperatorReturnItems
|
||||
|
|
@ -136,21 +138,6 @@ class SLICESOCK_Gcode(LabelNodeSocket):
|
|||
# Nodes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
class SliceNode(Node, AutoBlIdName):
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ...
|
||||
|
||||
def add_output(self, bl_idname: str, name: str | None = None):
|
||||
from .registry import get_label_by_name
|
||||
return self.outputs.new(bl_idname, name or get_label_by_name(bl_idname))
|
||||
|
||||
def add_input(self, bl_idname: str, name: str | None = None, multi: bool = False):
|
||||
from .registry import get_label_by_name
|
||||
sock = self.inputs.new(bl_idname, name or get_label_by_name(bl_idname))
|
||||
if multi:
|
||||
sock.link_limit = 0
|
||||
sock.display_shape = 'LIST'
|
||||
return sock
|
||||
|
||||
# Basic Inputs
|
||||
|
||||
class InputNode(SliceNode):
|
||||
|
|
@ -178,6 +165,11 @@ class SLICENODE_InputFloat(InputNode):
|
|||
class SLICENODE_InputInt(InputNode):
|
||||
input_type = "Int"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputObject(InputNode):
|
||||
input_type = "Object"
|
||||
bl_icon = "OBJECT_DATA"
|
||||
|
||||
# Math
|
||||
|
||||
@register_class
|
||||
|
|
@ -217,8 +209,8 @@ class SLICENODE_IntMath(SliceNode):
|
|||
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])
|
||||
a: int = resolve_input(self.inputs[0])
|
||||
b: int = resolve_input(self.inputs[1])
|
||||
|
||||
op = self.operation
|
||||
try:
|
||||
|
|
@ -241,11 +233,6 @@ class SLICENODE_IntMath(SliceNode):
|
|||
return 0
|
||||
return 0
|
||||
|
||||
@register_class
|
||||
class SLICENODE_InputObject(InputNode):
|
||||
bl_label = "Object"
|
||||
bl_icon = "OBJECT_DATA"
|
||||
|
||||
@register_class
|
||||
class SLICENODE_GetMesh(SliceNode):
|
||||
bl_label = "Get Mesh"
|
||||
|
|
@ -257,7 +244,7 @@ class SLICENODE_GetMesh(SliceNode):
|
|||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject | None:
|
||||
if socket_name != "Mesh": return None
|
||||
|
||||
obj: Any = _resolve_input(self.inputs.get("Object"))
|
||||
obj: Any = resolve_input(self.inputs.get("Object"))
|
||||
|
||||
so = SlicingObject(obj, obj.name)
|
||||
|
||||
|
|
@ -272,9 +259,9 @@ class SLICENODE_GetPaths(SliceNode):
|
|||
self.add_output("SLICESOCK_Paths")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
|
||||
obj: Any = _resolve_input(self.inputs.get("Object"))
|
||||
obj: Any = resolve_input(self.inputs.get("Object"))
|
||||
|
||||
so = capture_mesh_edges(obj)
|
||||
so: list[NDArray[float64]] = capture_mesh_edges(obj)
|
||||
|
||||
return so
|
||||
|
||||
|
|
@ -290,8 +277,8 @@ class SLICENODE_SliceShells(SliceNode):
|
|||
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
|
||||
if socket_name != "Slices": return []
|
||||
|
||||
slicingmesh: SlicingObject | None = _resolve_input(self.inputs.get("Mesh"))
|
||||
layer_height: float = _resolve_input(self.inputs.get("Layer Height"))
|
||||
slicingmesh: SlicingObject | None = resolve_input(self.inputs.get("Mesh"))
|
||||
layer_height: float = resolve_input(self.inputs.get("Layer Height"))
|
||||
|
||||
if not slicingmesh: return []
|
||||
|
||||
|
|
@ -333,8 +320,8 @@ class SLICENODE_BufferShells(SliceNode):
|
|||
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
|
||||
if socket_name != "Slices": return []
|
||||
|
||||
slices: list[np.ndarray] = _resolve_input(self.inputs.get("Slices"))
|
||||
distance: float = _resolve_input(self.inputs.get("Distance"))
|
||||
slices: list[np.ndarray] = resolve_input(self.inputs.get("Slices"))
|
||||
distance: float = resolve_input(self.inputs.get("Distance"))
|
||||
|
||||
buffered_slices = slice.buffer_slice(slices, distance, miter=self.shape_option)
|
||||
|
||||
|
|
@ -349,7 +336,7 @@ class SLICENODE_SlicesToPaths(SliceNode):
|
|||
self.add_output("SLICESOCK_Paths")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]:
|
||||
slices: list[np.ndarray] = _resolve_input(self.inputs.get("Slices"))
|
||||
slices: list[np.ndarray] = resolve_input(self.inputs.get("Slices"))
|
||||
return slices
|
||||
|
||||
@register_class
|
||||
|
|
@ -361,7 +348,7 @@ class SLICENODE_PathsToGcode(SliceNode):
|
|||
self.add_output("SLICESOCK_Gcode")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||
paths: list[np.ndarray] = _resolve_input(self.inputs.get("Paths"))
|
||||
paths: list[np.ndarray] = resolve_input(self.inputs.get("Paths"))
|
||||
pather: Pather = Pather(paths)
|
||||
gcode: str = pather.to_gcode()
|
||||
return gcode
|
||||
|
|
@ -417,7 +404,7 @@ class SLICENODE_JoinGcode(SliceNodeJoiner):
|
|||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||
gcode_input: list[str] = []
|
||||
for input in self.inputs:
|
||||
gcode_input += [_resolve_input(input)] or [""]
|
||||
gcode_input += [resolve_input(input)] or [""]
|
||||
return "\n".join(gcode_input)
|
||||
|
||||
|
||||
|
|
@ -445,7 +432,7 @@ class SLICENODE_JoinSlices(SliceNodeJoiner):
|
|||
accumulated: list[np.ndarray] = []
|
||||
current: list[np.ndarray] = []
|
||||
for input in self.inputs:
|
||||
current =_resolve_input(input)
|
||||
current =resolve_input(input)
|
||||
|
||||
if not current:
|
||||
continue
|
||||
|
|
@ -469,7 +456,7 @@ class SLICENODE_JoinMesh(SliceNodeJoiner):
|
|||
merged_mesh = SlicingObject()
|
||||
|
||||
for input in self.inputs:
|
||||
mesh =_resolve_input(input)
|
||||
mesh =resolve_input(input)
|
||||
if mesh:
|
||||
merged_mesh.mesh = np.concatenate([merged_mesh.mesh, mesh.mesh], axis=0)
|
||||
|
||||
|
|
@ -484,7 +471,7 @@ class SLICENODE_JoinPaths(SliceNodeJoiner):
|
|||
merged_paths = []
|
||||
|
||||
for input in self.inputs:
|
||||
paths = _resolve_input(input)
|
||||
paths = resolve_input(input)
|
||||
if paths:
|
||||
merged_paths.extend(paths)
|
||||
|
||||
|
|
@ -501,7 +488,7 @@ class SLICENODE_LoadGcode(SliceNode):
|
|||
self.add_output("SLICESOCK_Gcode")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||
path = _resolve_input(self.inputs.get("Filepath"))
|
||||
path = resolve_input(self.inputs.get("Filepath"))
|
||||
if not path: return ""
|
||||
abs_path = bpy.path.abspath(path)
|
||||
if os.path.exists(abs_path):
|
||||
|
|
@ -527,8 +514,8 @@ class SLICENODE_GcodeToFile(ExecSliceNode):
|
|||
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"))
|
||||
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)
|
||||
|
|
@ -548,7 +535,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
self.add_input("SLICESOCK_Gcode")
|
||||
|
||||
def evaluate(self):
|
||||
gcode: str = _resolve_input(self.inputs.get("Gcode"))
|
||||
gcode: str = resolve_input(self.inputs.get("Gcode"))
|
||||
if not gcode:
|
||||
return 'empty'
|
||||
|
||||
|
|
@ -628,7 +615,7 @@ class SLICENODE_Test(ExecSliceNode):
|
|||
self.add_input("SLICESOCK_Gcode")
|
||||
|
||||
def evaluate(self):
|
||||
_ = _resolve_input(self.inputs.get("Gcode"))
|
||||
_ = resolve_input(self.inputs.get("Gcode"))
|
||||
return 'ok'
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -301,75 +301,60 @@ def group_segments_by_z(segments: np.ndarray) -> dict[int, np.ndarray]:
|
|||
############################################################
|
||||
|
||||
class Pather(object):
|
||||
FEEDRATE: float = 1200. # mm/min
|
||||
EXTRUSION_RATE: float = 0.017 # E per mm of path
|
||||
|
||||
def __init__(self, paths: list[np.ndarray], **kwargs) -> None:
|
||||
self.paths = paths
|
||||
self.bed_center_x: float = 250.
|
||||
self.bed_center_y: float = 250.
|
||||
self.feedrate: float = 1200.
|
||||
self.extrusion_rate: float = 0.017
|
||||
|
||||
def to_gcode(self) -> str:
|
||||
"""
|
||||
Convert a list of paths (3D numpy arrays) into G-code.
|
||||
"""
|
||||
if not self.paths:
|
||||
return ""
|
||||
|
||||
def z_key(path) -> float:
|
||||
return path[0, 2]
|
||||
|
||||
def segment_length_3d(p1, p2) -> float:
|
||||
dx = p2[0] - p1[0]
|
||||
dy = p2[1] - p1[1]
|
||||
dz = p2[2] - p1[2]
|
||||
return (dx * dx + dy * dy + dz * dz) ** 0.5
|
||||
|
||||
# Sort paths by Z height
|
||||
lines = sorted(self.paths, key=z_key)
|
||||
|
||||
gcode: list[str] = [
|
||||
"; Generated from NodeSlicer Paths (via Pyclipper)",
|
||||
f"; FEEDRATE={self.FEEDRATE}",
|
||||
f"; EXTRUSION_RATE={self.EXTRUSION_RATE}",
|
||||
"; Generated from NodeSlicer",
|
||||
f";TYPE:Custom",
|
||||
f";FEEDRATE={self.feedrate}",
|
||||
f";EXTRUSION_RATE={self.extrusion_rate}",
|
||||
"G21 ; set units to mm",
|
||||
"G90 ; absolute XYZ positioning",
|
||||
"M82 ; absolute extrusion mode",
|
||||
"M83 ; relative extrusion mode",
|
||||
"G92 E0",
|
||||
]
|
||||
|
||||
e = 0.0
|
||||
last_z = -1.0
|
||||
|
||||
for line in lines:
|
||||
for line in self.paths:
|
||||
coords = line.tolist()
|
||||
if len(coords) < 2:
|
||||
continue
|
||||
|
||||
x0, y0, z0 = coords[0]
|
||||
|
||||
# Layer change markers for PrusaSlicer
|
||||
if abs(z0 - last_z) > 0.001:
|
||||
gcode.append(f";LAYER_CHANGE")
|
||||
gcode.append(f";Z:{z0:.3f}")
|
||||
gcode.append(f";BEFORE_LAYER_CHANGE")
|
||||
gcode.append(f"G92 E0") # Reset E on layer change
|
||||
e = 0.0
|
||||
gcode.append(f"G1 Z{z0:.3f} F720")
|
||||
gcode.append(f";AFTER_LAYER_CHANGE")
|
||||
last_z = z0
|
||||
|
||||
# Travel move to start of this path
|
||||
gcode.append(f"G0 X{x0:.3f} Y{y0:.3f} Z{z0:.3f}")
|
||||
gcode.append(";TYPE:External perimeter")
|
||||
|
||||
# Extrude along the line
|
||||
for p1, p2 in zip(coords, coords[1:]):
|
||||
seg_len: float = segment_length_3d(p1, p2)
|
||||
e += seg_len * self.EXTRUSION_RATE
|
||||
# Calculate extrusion for this segment only
|
||||
e_segment = seg_len * self.extrusion_rate
|
||||
x, y, z = p2
|
||||
gcode.append(
|
||||
f"G1 X{x:.3f} Y{y:.3f} Z{z:.3f} E{e:.5f} F{self.FEEDRATE}"
|
||||
f"G1 X{x:.3f} Y{y:.3f} Z{z:.3f} E{e_segment:.5f} F{self.feedrate}"
|
||||
)
|
||||
|
||||
return "\n".join(gcode)
|
||||
|
|
@ -350,7 +350,9 @@ def objects_to_tris(objects: list[Object], scale) -> (NDArray[tuple[int, int, in
|
|||
def capture_mesh_edges(obj: Object) -> list[NDArray[float64]]:
|
||||
try:
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
mesh: Mesh = obj.to_mesh(depsgraph=depsgraph, preserve_all_data_layers=True)
|
||||
obj_eval = obj.evaluated_get(depsgraph)
|
||||
mesh = obj_eval.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
||||
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
|
@ -358,47 +360,49 @@ def capture_mesh_edges(obj: Object) -> list[NDArray[float64]]:
|
|||
return []
|
||||
|
||||
try:
|
||||
assert bpy.context.scene
|
||||
assert bpy.context.scene.unit_settings
|
||||
scene_scale: float = bpy.context.scene.unit_settings.scale_length
|
||||
scale = 1000.0 * scene_scale
|
||||
if not mesh.vertices:
|
||||
return []
|
||||
|
||||
# Get vertices
|
||||
# 1. Scaling & Matrix Setup
|
||||
scale = 1000.0 * bpy.context.scene.unit_settings.scale_length
|
||||
world_mat = np.array(obj.matrix_world)
|
||||
|
||||
# 2. Extract and Transform Coordinates
|
||||
num_verts = len(mesh.vertices)
|
||||
verts_flat = np.empty(num_verts * 3, dtype=np.float64)
|
||||
mesh.vertices.foreach_get("co", verts_flat)
|
||||
verts = verts_flat.reshape(-1, 3)
|
||||
verts = np.empty((num_verts, 3), dtype=np.float64)
|
||||
mesh.vertices.foreach_get("co", verts.ravel())
|
||||
|
||||
# Apply world matrix transformation (consistent with objects_to_tris)
|
||||
mat = np.array(obj.matrix_world.transposed())
|
||||
homogeneous_verts = np.hstack((verts, np.ones((num_verts, 1))))
|
||||
tx_verts = (homogeneous_verts @ mat)[:, :3] * scale
|
||||
# World transform: (V * R.T) + T
|
||||
tx_verts = (verts @ world_mat[:3, :3].T + world_mat[:3, 3]) * scale
|
||||
|
||||
# Identify connectivity between consecutive indices
|
||||
num_edges = len(mesh.edges)
|
||||
connected = np.zeros(num_verts - 1, dtype=bool)
|
||||
# 3. Build connectivity lookup
|
||||
# Only need edges where v2 = v1 + 1 or loop-back
|
||||
edges_set = {tuple(sorted(e.vertices)) for e in mesh.edges}
|
||||
|
||||
if num_edges > 0:
|
||||
edges_flat = np.empty(num_edges * 2, dtype=np.int32)
|
||||
mesh.edges.foreach_get("vertices", edges_flat)
|
||||
edges = edges_flat.reshape(-1, 2)
|
||||
# Identify gaps in sequential connectivity
|
||||
# connected[i] is True if vertex i is connected to i+1
|
||||
connected = np.array([(i, i + 1) in edges_set for i in range(num_verts - 1)])
|
||||
|
||||
v_min = np.min(edges, axis=1)
|
||||
v_max = np.max(edges, axis=1)
|
||||
|
||||
# Check for edges connecting vertex i and i+1
|
||||
mask = (v_max == v_min + 1)
|
||||
consecutive_v_min = v_min[mask]
|
||||
|
||||
# Ensure indices are within bounds for the 'connected' array
|
||||
valid_mask = (consecutive_v_min >= 0) & (consecutive_v_min < num_verts - 1)
|
||||
connected[consecutive_v_min[valid_mask]] = True
|
||||
|
||||
# Split vertices into paths where connectivity is interrupted
|
||||
# 4. Split into segments (Maintains original index order)
|
||||
split_at = np.where(~connected)[0] + 1
|
||||
paths = np.split(tx_verts, split_at)
|
||||
raw_paths = np.split(tx_verts, split_at)
|
||||
indices_split = np.split(np.arange(num_verts), split_at)
|
||||
|
||||
final_paths = []
|
||||
for i, path_coords in enumerate(raw_paths):
|
||||
if path_coords.shape[0] == 0:
|
||||
continue
|
||||
|
||||
# 5. Handle Loop Closure
|
||||
# Only check if the segment could potentially be a loop (indices > 2)
|
||||
idx = indices_split[i]
|
||||
if len(idx) > 2 and tuple(sorted((idx[0], idx[-1]))) in edges_set:
|
||||
path_coords = np.vstack((path_coords, path_coords[0]))
|
||||
|
||||
final_paths.append(path_coords)
|
||||
|
||||
return final_paths
|
||||
|
||||
return [p for p in paths if p.size > 0]
|
||||
finally:
|
||||
# Compatibility with Blender 2.8+ to avoid memory leaks
|
||||
# We check if the attribute exists as it's not used elsewhere in this file
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from __future__ import annotations
|
||||
import typing
|
||||
|
||||
from bpy.types import NodeSocket
|
||||
from typing import Any
|
||||
from bpy.types import NodeSocket, Node
|
||||
|
||||
class NodeException(Exception): ...
|
||||
|
||||
_EvalCache = dict[tuple[int, str], typing.Any]
|
||||
_EvalCache = dict[tuple[int, str], Any]
|
||||
|
||||
class AutoBlIdName:
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
|
|
@ -14,11 +15,26 @@ class AutoBlIdName:
|
|||
cls.bl_idname = cls.__name__
|
||||
|
||||
class SliceNodeSocket(NodeSocket, AutoBlIdName):
|
||||
default: typing.Any = None
|
||||
default: Any = None
|
||||
color = (1., 1., 1., 1.)
|
||||
|
||||
def draw_color(self, context, node) -> tuple[float, float, float, float]: return self.color # pyright: ignore[reportIncompatibleMethodOverride]
|
||||
|
||||
class SliceNode(Node, AutoBlIdName):
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ...
|
||||
|
||||
def add_output(self, bl_idname: str, name: str | None = None):
|
||||
from ..registry import get_label_by_name
|
||||
return self.outputs.new(bl_idname, name or get_label_by_name(bl_idname))
|
||||
|
||||
def add_input(self, bl_idname: str, name: str | None = None, multi: bool = False):
|
||||
from ..registry import get_label_by_name
|
||||
sock = self.inputs.new(bl_idname, name or get_label_by_name(bl_idname))
|
||||
if multi:
|
||||
sock.link_limit = 0
|
||||
sock.display_shape = 'LIST'
|
||||
return sock
|
||||
|
||||
## Evaluation helpers
|
||||
|
||||
def _follow_reroute(socket: NodeSocket) -> NodeSocket:
|
||||
|
|
@ -40,7 +56,7 @@ def _follow_reroute(socket: NodeSocket) -> NodeSocket:
|
|||
|
||||
return current
|
||||
|
||||
def _resolve_input(
|
||||
def resolve_input(
|
||||
socket: SliceNodeSocket | None,
|
||||
*,
|
||||
cache: _EvalCache | None = None,
|
||||
|
|
@ -69,7 +85,7 @@ def _resolve_input(
|
|||
def _resolve_output(
|
||||
socket: NodeSocket,
|
||||
*,
|
||||
default: typing.Any = None,
|
||||
default: Any = None,
|
||||
cache: _EvalCache | None = None,
|
||||
visited: set[tuple[int, str]] | None = None,
|
||||
):
|
||||
|
|
@ -97,7 +113,7 @@ def _resolve_output(
|
|||
break
|
||||
|
||||
if input_to_bypass:
|
||||
value = _resolve_input(typing.cast(Any, input_to_bypass), cache=cache, visited=visited)
|
||||
value = resolve_input(typing.cast(Any, input_to_bypass), cache=cache, visited=visited)
|
||||
cache[key] = value
|
||||
return value
|
||||
return default
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue