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