variable extrusion
This commit is contained in:
parent
d448fa20cc
commit
3080f65695
5 changed files with 117 additions and 52 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import bpy
|
||||
from bpy.props import PointerProperty, StringProperty, FloatProperty, IntProperty, EnumProperty, BoolProperty, CollectionProperty
|
||||
from bpy.props import PointerProperty, StringProperty, FloatProperty, IntProperty, EnumProperty, BoolProperty, FloatVectorProperty, CollectionProperty
|
||||
|
||||
from . import tree
|
||||
|
||||
|
|
@ -65,4 +65,5 @@ NfIntProperty = NfProperty(IntProperty)
|
|||
NfFloatProperty = NfProperty(FloatProperty)
|
||||
NfEnumProperty = NfProperty(EnumProperty)
|
||||
NfBoolProperty = NfProperty(BoolProperty)
|
||||
NfPointerProperty = NfProperty(PointerProperty)
|
||||
NfPointerProperty = NfProperty(PointerProperty)
|
||||
NfFloatVectorProperty = NfProperty(FloatVectorProperty)
|
||||
|
|
@ -345,17 +345,24 @@ class SLICENODE_GetMesh(SliceNode):
|
|||
class SLICENODE_GetPaths(SliceNode):
|
||||
bl_label = "Get Paths"
|
||||
|
||||
attr_name: NfStringProperty(name="Extrusion Multiplier", default="e_mult") # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Object")
|
||||
self.add_output("SLICESOCK_Paths")
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "attr_name")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||
obj: Any = self.resolve("Object")
|
||||
attr_name: str = self.attr_name
|
||||
|
||||
from ...infra.blender.blender_mesh_capture import capture_mesh_edges
|
||||
so: list[np.ndarray] = capture_mesh_edges(obj)
|
||||
so, e_rates = capture_mesh_edges(obj, attr_name=attr_name)
|
||||
|
||||
return SlicePaths(so)
|
||||
return SlicePaths(so, extrusion_rate=e_rates)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_SliceShells(SliceNode):
|
||||
|
|
@ -1569,11 +1576,21 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
obj.data.clear_geometry()
|
||||
return 'empty'
|
||||
|
||||
segments = []
|
||||
vertices = []
|
||||
edges = []
|
||||
e_rates = []
|
||||
|
||||
last_x, last_y, last_z = 0.0, 0.0, 0.0
|
||||
last_e = 0.0
|
||||
|
||||
offset_x, offset_y = 0.0, 0.0
|
||||
default_extrusion_rate = 0.017 # Default fallback
|
||||
|
||||
last_v_idx = -1
|
||||
last_p2 = None
|
||||
|
||||
def segment_length_3d(p1, p2) -> float:
|
||||
return ((p2[0]-p1[0])**2 + (p2[1]-p1[1])**2 + (p2[2]-p1[2])**2)**0.5
|
||||
|
||||
for line in gcode.splitlines():
|
||||
# Parse metadata from comments
|
||||
|
|
@ -1585,6 +1602,9 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
elif comment.startswith("OFFSET_Y="):
|
||||
try: offset_y = float(comment.split('=')[1])
|
||||
except ValueError: pass
|
||||
elif comment.startswith("DEFAULT_EXTRUSION_RATE="):
|
||||
try: default_extrusion_rate = float(comment.split('=')[1])
|
||||
except ValueError: pass
|
||||
|
||||
line_no_comment = line.split(';')[0].strip()
|
||||
if not line_no_comment: continue
|
||||
|
|
@ -1615,15 +1635,41 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
if show:
|
||||
# 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)
|
||||
))
|
||||
p1 = (last_x - ox, last_y - oy, last_z)
|
||||
p2 = (new_x - ox, new_y - oy, new_z)
|
||||
|
||||
# Calculate e_rate (proportion between E and default_extrusion_rate)
|
||||
seg_len_mm = segment_length_3d(p1, p2) * 1000.0
|
||||
if seg_len_mm > 0 and default_extrusion_rate > 0:
|
||||
e_rate = (new_e / seg_len_mm) / default_extrusion_rate
|
||||
else:
|
||||
e_rate = 1.0
|
||||
|
||||
if last_v_idx != -1 and p1 == last_p2:
|
||||
# Connect to previous segment
|
||||
v2_idx = len(vertices)
|
||||
vertices.append(p2)
|
||||
edges.append((last_v_idx, v2_idx))
|
||||
last_v_idx = v2_idx
|
||||
else:
|
||||
# Start new path
|
||||
v1_idx = len(vertices)
|
||||
v2_idx = v1_idx + 1
|
||||
vertices.extend([p1, p2])
|
||||
edges.append((v1_idx, v2_idx))
|
||||
last_v_idx = v2_idx
|
||||
|
||||
e_rates.append(e_rate)
|
||||
last_p2 = p2
|
||||
else:
|
||||
# Break path connection on travel or non-shown move
|
||||
last_v_idx = -1
|
||||
last_p2 = None
|
||||
|
||||
last_x, last_y, last_z = new_x, new_y, new_z
|
||||
last_e = new_e
|
||||
|
||||
if not segments:
|
||||
if not vertices:
|
||||
return 'no segments'
|
||||
|
||||
# Create or find "Debug" mesh object
|
||||
|
|
@ -1637,13 +1683,15 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
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, [])
|
||||
|
||||
# Create e_mult attribute on edges
|
||||
if "e_mult" not in mesh.attributes:
|
||||
mesh.attributes.new(name="e_mult", type='FLOAT', domain='EDGE')
|
||||
|
||||
e_mult_attr = mesh.attributes["e_mult"]
|
||||
e_mult_attr.data.foreach_set("value", e_rates)
|
||||
|
||||
mesh.update()
|
||||
|
||||
# Assign Geometry Nodes nodegroup for preview
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from typing import Any
|
|||
import bpy
|
||||
from bpy.props import CollectionProperty
|
||||
from bpy.types import Property, PropertyGroup, bpy_struct
|
||||
from .bpy_ns_types import NfStringProperty, NfFloatProperty, NfIntProperty, NfEnumProperty, NfPointerProperty
|
||||
from .bpy_ns_types import NfStringProperty, NfFloatProperty, NfIntProperty, NfEnumProperty, NfPointerProperty, NfFloatVectorProperty
|
||||
|
||||
from ...registry import register_class
|
||||
from ...infra.blender.blender_slice_nodes_generic import SliceNodeSocket
|
||||
|
|
@ -99,7 +99,7 @@ class SLICESOCK_DirPath(InputNodeSocket):
|
|||
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]
|
||||
value: NfFloatVectorProperty(name="Vector", size=3, default=(0.0, 0.0, 0.0), subtype='EULER') # pyright: ignore[reportInvalidTypeForm]
|
||||
bl_color = (166/255, 99/255, 199/255, 1.0)
|
||||
|
||||
@register_class
|
||||
|
|
@ -111,7 +111,7 @@ class SLICESOCK_VectorRotationRo(ReadonlySocket, SLICESOCK_VectorRotation_Common
|
|||
class SLICESOCK_Vector_Common(InputNodeSocket):
|
||||
bl_label = "Vector"
|
||||
|
||||
value: bpy.props.FloatVectorProperty(name="Vector", size=3, default=(0.0, 0.0, 0.0)) # pyright: ignore[reportInvalidTypeForm]
|
||||
value: NfFloatVectorProperty(name="Vector", size=3, default=(0.0, 0.0, 0.0)) # pyright: ignore[reportInvalidTypeForm]
|
||||
bl_color = (0.39, 0.39, 0.78, 1.0)
|
||||
|
||||
@register_class
|
||||
|
|
|
|||
|
|
@ -10,20 +10,32 @@ SCALE = 1_000_000
|
|||
|
||||
class SlicePaths(list[np.ndarray]):
|
||||
groups: list[int]
|
||||
extrusion_rate: list[np.ndarray]
|
||||
|
||||
def __init__(self, paths: list[np.ndarray] = [], groups: list[int] = []):
|
||||
def __init__(self, paths: list[np.ndarray] = [], groups: list[int] = [], extrusion_rate: list[np.ndarray] | None = None):
|
||||
super().__init__(paths)
|
||||
|
||||
self.groups = []
|
||||
if groups: self.groups = list(groups)
|
||||
else: self.groups = list(range(0,len(paths)))
|
||||
|
||||
self.extrusion_rate = []
|
||||
if extrusion_rate:
|
||||
self.extrusion_rate = list(extrusion_rate)
|
||||
else:
|
||||
self.extrusion_rate = [np.ones(len(p)) for p in paths]
|
||||
|
||||
def copy_with(self, paths: list[np.ndarray], groups: list[int] | None = None) -> SlicePaths:
|
||||
def copy_with(self, paths: list[np.ndarray], groups: list[int] | None = None, extrusion_rate: list[np.ndarray] | None = None) -> SlicePaths:
|
||||
"""Returns a new SlicePaths object with new paths/groups but preserving all other metadata."""
|
||||
new_obj = SlicePaths(paths, groups if groups is not None else self.groups)
|
||||
if extrusion_rate is None:
|
||||
# Only reuse extrusion_rate if path lengths match exactly
|
||||
if len(paths) == len(self) and all(len(p) == len(op) for p, op in zip(paths, self)):
|
||||
extrusion_rate = self.extrusion_rate
|
||||
|
||||
new_obj = SlicePaths(paths, groups if groups is not None else self.groups, extrusion_rate)
|
||||
# Copy any other attributes that might have been added
|
||||
for k, v in self.__dict__.items():
|
||||
if k != "groups":
|
||||
if k not in ["groups", "extrusion_rate"]:
|
||||
setattr(new_obj, k, v)
|
||||
return new_obj
|
||||
|
||||
|
|
@ -44,12 +56,7 @@ class SlicePaths(list[np.ndarray]):
|
|||
p = self.transform(np.array(mat))
|
||||
|
||||
res_paths, res_groups = buffer_slice(p, distance, miter=miter)
|
||||
new_paths = SlicePaths(res_paths, res_groups)
|
||||
|
||||
# Copy other metadata from self
|
||||
for k, v in self.__dict__.items():
|
||||
if k != "groups":
|
||||
setattr(new_paths, k, v)
|
||||
new_paths = self.copy_with(res_paths, res_groups)
|
||||
|
||||
if rotation and any(rotation):
|
||||
from mathutils import Euler
|
||||
|
|
@ -67,12 +74,7 @@ class SlicePaths(list[np.ndarray]):
|
|||
p = self.transform(np.array(mat))
|
||||
|
||||
res_paths, res_groups = infill_slice(p, density, rotation_deg=rotation_deg, infill_type=infill_type)
|
||||
new_paths = SlicePaths(res_paths, res_groups)
|
||||
|
||||
# Copy other metadata from self
|
||||
for k, v in self.__dict__.items():
|
||||
if k != "groups":
|
||||
setattr(new_paths, k, v)
|
||||
new_paths = self.copy_with(res_paths, res_groups)
|
||||
|
||||
if layer_rotation and any(layer_rotation):
|
||||
from mathutils import Euler
|
||||
|
|
@ -519,6 +521,7 @@ class Pather(object):
|
|||
|
||||
def __init__(self, paths: list[np.ndarray], **kwargs) -> None:
|
||||
self.paths = paths
|
||||
self.extrusion_rates = getattr(paths, "extrusion_rate", [np.ones(len(p)) for p in paths])
|
||||
self.bed_center_x: float = 250.
|
||||
self.bed_center_y: float = 250.
|
||||
self.feedrate: float = 1200.
|
||||
|
|
@ -533,10 +536,10 @@ class Pather(object):
|
|||
min_xy = all_pts[:, :2].min(axis=0)
|
||||
max_xy = all_pts[:, :2].max(axis=0)
|
||||
center_xy = (min_xy + max_xy) / 2.0
|
||||
|
||||
|
||||
offset_x = self.bed_center_x - center_xy[0]
|
||||
offset_y = self.bed_center_y - center_xy[1]
|
||||
|
||||
|
||||
# Apply offset to all paths
|
||||
centered_paths = []
|
||||
for path in self.paths:
|
||||
|
|
@ -557,8 +560,8 @@ class Pather(object):
|
|||
header: list[str] = [
|
||||
"; Generated from NodeSlicer",
|
||||
f";TYPE:Custom",
|
||||
f";FEEDRATE={self.feedrate}",
|
||||
f";EXTRUSION_RATE={self.extrusion_rate}",
|
||||
f";DEFAULT_FEEDRATE={self.feedrate}",
|
||||
f";DEFAULT_EXTRUSION_RATE={self.extrusion_rate}",
|
||||
f";OFFSET_X={offset_x:.6f}",
|
||||
f";OFFSET_Y={offset_y:.6f}",
|
||||
"G21 ; set units to mm",
|
||||
|
|
@ -571,11 +574,13 @@ class Pather(object):
|
|||
last_z = -1.0
|
||||
max_z = 0
|
||||
|
||||
for line in centered_paths:
|
||||
for i, line in enumerate(centered_paths):
|
||||
coords = line.tolist()
|
||||
if len(coords) < 2:
|
||||
continue
|
||||
|
||||
e_rates = self.extrusion_rates[i]
|
||||
|
||||
x0, y0, z0 = coords[0]
|
||||
|
||||
if abs(z0 - last_z) > 0.001:
|
||||
|
|
@ -589,9 +594,9 @@ class Pather(object):
|
|||
|
||||
fr = self.feedrate
|
||||
old_fr = 0
|
||||
for p1, p2 in zip(coords, coords[1:]):
|
||||
for j, (p1, p2) in enumerate(zip(coords, coords[1:])):
|
||||
seg_len: float = segment_length_3d(p1, p2)
|
||||
e_segment = seg_len * self.extrusion_rate
|
||||
e_segment = seg_len * self.extrusion_rate * e_rates[j]
|
||||
|
||||
old_x, old_y, old_z = p1
|
||||
x, y, z = p2
|
||||
|
|
@ -608,5 +613,5 @@ class Pather(object):
|
|||
|
||||
max_z = max(max_z, z)
|
||||
old_fr = fr
|
||||
|
||||
|
||||
return "\n".join(gcode)
|
||||
|
|
@ -409,11 +409,12 @@ def _cached_capture_mesh_edges(
|
|||
obj_name: str,
|
||||
mesh_name: str,
|
||||
matrix_tuple: tuple[float, ...],
|
||||
scene_scale: float
|
||||
) -> list[NDArray[float64]]:
|
||||
scene_scale: float,
|
||||
attr_name: str = "e_mult"
|
||||
) -> tuple[list[NDArray[float64]], list[NDArray[float64]]]:
|
||||
# This helper expects that the object and mesh exist and are accessible via bpy.data
|
||||
obj = bpy.data.objects.get(obj_name)
|
||||
if not obj: return []
|
||||
if not obj: return [], []
|
||||
|
||||
# We don't use obj_eval here directly as it's not hashable,
|
||||
# but we assume this is called when we know we need to re-capture.
|
||||
|
|
@ -423,10 +424,10 @@ def _cached_capture_mesh_edges(
|
|||
obj_eval = obj.evaluated_get(depsgraph)
|
||||
mesh = obj_eval.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
||||
except Exception:
|
||||
return []
|
||||
return [], []
|
||||
|
||||
if not mesh or not mesh.vertices:
|
||||
return []
|
||||
return [], []
|
||||
|
||||
try:
|
||||
world_mat = np.array(matrix_tuple).reshape(4, 4)
|
||||
|
|
@ -434,6 +435,10 @@ def _cached_capture_mesh_edges(
|
|||
verts = np.empty((num_verts, 3), dtype=np.float64)
|
||||
mesh.vertices.foreach_get("co", verts.ravel())
|
||||
|
||||
e_mults = np.ones(num_verts, dtype=np.float64)
|
||||
if attr_name in mesh.attributes:
|
||||
mesh.attributes[attr_name].data.foreach_get("value", e_mults)
|
||||
|
||||
tx_verts = (verts @ world_mat[:3, :3].T + world_mat[:3, 3]) * scene_scale
|
||||
|
||||
edges_set = {tuple(sorted(e.vertices)) for e in mesh.edges}
|
||||
|
|
@ -441,25 +446,30 @@ def _cached_capture_mesh_edges(
|
|||
|
||||
split_at = np.where(~connected)[0] + 1
|
||||
raw_paths = np.split(tx_verts, split_at)
|
||||
raw_e_mults = np.split(e_mults, split_at)
|
||||
indices_split = np.split(np.arange(num_verts), split_at)
|
||||
|
||||
final_paths = []
|
||||
final_e_mults = []
|
||||
for i, path_coords in enumerate(raw_paths):
|
||||
if path_coords.shape[0] == 0:
|
||||
continue
|
||||
idx = indices_split[i]
|
||||
e_vals = raw_e_mults[i]
|
||||
if len(idx) > 2 and tuple(sorted((idx[0], idx[-1]))) in edges_set:
|
||||
path_coords = np.vstack((path_coords, path_coords[0]))
|
||||
e_vals = np.concatenate((e_vals, [e_vals[0]]))
|
||||
final_paths.append(path_coords)
|
||||
final_e_mults.append(e_vals)
|
||||
|
||||
return final_paths
|
||||
return final_paths, final_e_mults
|
||||
|
||||
finally:
|
||||
if hasattr(obj_eval, "to_mesh_clear"):
|
||||
obj_eval.to_mesh_clear()
|
||||
|
||||
def capture_mesh_edges(obj: Object) -> list[NDArray[float64]]:
|
||||
if not obj or not obj.data: return []
|
||||
def capture_mesh_edges(obj: Object, attr_name: str = "e_mult") -> tuple[list[NDArray[float64]], list[NDArray[float64]]]:
|
||||
if not obj or not obj.data: return [], []
|
||||
|
||||
if obj.mode == 'EDIT':
|
||||
obj.update_from_editmode()
|
||||
|
|
@ -474,5 +484,6 @@ def capture_mesh_edges(obj: Object) -> list[NDArray[float64]]:
|
|||
obj.name,
|
||||
obj.data.name,
|
||||
matrix_tuple,
|
||||
scene_scale
|
||||
scene_scale,
|
||||
attr_name
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue