diff --git a/core/nodeslicer/nodes.py b/core/nodeslicer/nodes.py index 0f1685f..3edad46 100644 --- a/core/nodeslicer/nodes.py +++ b/core/nodeslicer/nodes.py @@ -345,7 +345,8 @@ class SLICENODE_GetMesh(SliceNode): class SLICENODE_GetPaths(SliceNode): bl_label = "Get Paths" - attr_name: NfStringProperty(name="Extrusion Multiplier", default="e_mult") # pyright: ignore[reportInvalidTypeForm] + attr_name: NfStringProperty(name="Extrusion Attr", default="e_mult") # pyright: ignore[reportInvalidTypeForm] + f_attr_name: NfStringProperty(name="Feedrate Attr", default="f_rate") # pyright: ignore[reportInvalidTypeForm] def init(self, context): self.add_input("SLICESOCK_Object") @@ -354,15 +355,17 @@ class SLICENODE_GetPaths(SliceNode): def draw_buttons(self, context, layout): assert layout layout.prop(self, "attr_name") + layout.prop(self, "f_attr_name") def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths: obj: Any = self.resolve("Object") attr_name: str = self.attr_name + f_attr_name: str = self.f_attr_name from ...infra.blender.blender_mesh_capture import capture_mesh_edges - so, e_rates = capture_mesh_edges(obj, attr_name=attr_name) + so, e_rates, f_rates = capture_mesh_edges(obj, attr_name=attr_name, f_attr_name=f_attr_name) - return SlicePaths(so, extrusion_rate=e_rates) + return SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates) @register_class class SLICENODE_SliceShells(SliceNode): diff --git a/core/slice.py b/core/slice.py index 1389f98..cd351ff 100644 --- a/core/slice.py +++ b/core/slice.py @@ -11,8 +11,9 @@ SCALE = 1_000_000 class SlicePaths(list[np.ndarray]): groups: list[int] extrusion_rate: list[np.ndarray] + feedrate: list[np.ndarray] - def __init__(self, paths: list[np.ndarray] = [], groups: list[int] = [], extrusion_rate: list[np.ndarray] | None = None): + def __init__(self, paths: list[np.ndarray] = [], groups: list[int] = [], extrusion_rate: list[np.ndarray] | None = None, feedrate: list[np.ndarray] | None = None): super().__init__(paths) self.groups = [] @@ -24,18 +25,28 @@ class SlicePaths(list[np.ndarray]): self.extrusion_rate = list(extrusion_rate) else: self.extrusion_rate = [np.ones(len(p)) for p in paths] + + self.feedrate = [] + if feedrate: + self.feedrate = list(feedrate) + else: + self.feedrate = [np.zeros(len(p)) for p in paths] - def copy_with(self, paths: list[np.ndarray], groups: list[int] | None = None, extrusion_rate: list[np.ndarray] | None = None) -> SlicePaths: + def copy_with(self, paths: list[np.ndarray], groups: list[int] | None = None, extrusion_rate: list[np.ndarray] | None = None, feedrate: list[np.ndarray] | None = None) -> SlicePaths: """Returns a new SlicePaths object with new paths/groups but preserving all other metadata.""" 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) + if feedrate is None: + if len(paths) == len(self) and all(len(p) == len(op) for p, op in zip(paths, self)): + feedrate = self.feedrate + + new_obj = SlicePaths(paths, groups if groups is not None else self.groups, extrusion_rate, feedrate) # Copy any other attributes that might have been added for k, v in self.__dict__.items(): - if k not in ["groups", "extrusion_rate"]: + if k not in ["groups", "extrusion_rate", "feedrate"]: setattr(new_obj, k, v) return new_obj @@ -522,6 +533,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.feedrates = getattr(paths, "feedrate", [np.zeros(len(p)) for p in paths]) self.bed_center_x: float = 250. self.bed_center_y: float = 250. self.feedrate: float = 1200. @@ -580,6 +592,7 @@ class Pather(object): continue e_rates = self.extrusion_rates[i] + f_rates = self.feedrates[i] x0, y0, z0 = coords[0] @@ -592,11 +605,13 @@ class Pather(object): gcode.append(f"G0 X{x0:.3f} Y{y0:.3f} Z{z0:.3f}") old_x, old_y, old_z = x0, y0, z0 - fr = self.feedrate old_fr = 0 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_rates[j] + + # Per-segment feedrate value (not multiplier) + fr = f_rates[j] if f_rates[j] > 0 else self.feedrate old_x, old_y, old_z = p1 x, y, z = p2 diff --git a/infra/blender/blender_mesh_capture.py b/infra/blender/blender_mesh_capture.py index edab464..a9750a1 100644 --- a/infra/blender/blender_mesh_capture.py +++ b/infra/blender/blender_mesh_capture.py @@ -410,11 +410,12 @@ def _cached_capture_mesh_edges( mesh_name: str, matrix_tuple: tuple[float, ...], scene_scale: float, - attr_name: str = "e_mult" -) -> tuple[list[NDArray[float64]], list[NDArray[float64]]]: + attr_name: str = "e_mult", + f_attr_name: str = "f_rate" +) -> tuple[list[NDArray[float64]], 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. @@ -424,10 +425,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) @@ -439,6 +440,10 @@ def _cached_capture_mesh_edges( if attr_name in mesh.attributes: mesh.attributes[attr_name].data.foreach_get("value", e_mults) + f_rates = np.zeros(num_verts, dtype=np.float64) + if f_attr_name in mesh.attributes: + mesh.attributes[f_attr_name].data.foreach_get("value", f_rates) + 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} @@ -447,29 +452,34 @@ 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) + raw_f_rates = np.split(f_rates, split_at) indices_split = np.split(np.arange(num_verts), split_at) final_paths = [] final_e_mults = [] + final_f_rates = [] 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] + f_vals = raw_f_rates[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]])) + f_vals = np.concatenate((f_vals, [f_vals[0]])) final_paths.append(path_coords) final_e_mults.append(e_vals) + final_f_rates.append(f_vals) - return final_paths, final_e_mults + return final_paths, final_e_mults, final_f_rates finally: if hasattr(obj_eval, "to_mesh_clear"): obj_eval.to_mesh_clear() -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 [], [] +def capture_mesh_edges(obj: Object, attr_name: str = "e_mult", f_attr_name: str = "f_rate") -> tuple[list[NDArray[float64]], list[NDArray[float64]], list[NDArray[float64]]]: + if not obj or not obj.data: return [], [], [] if obj.mode == 'EDIT': obj.update_from_editmode() @@ -485,5 +495,6 @@ def capture_mesh_edges(obj: Object, attr_name: str = "e_mult") -> tuple[list[NDA obj.data.name, matrix_tuple, scene_scale, - attr_name + attr_name, + f_attr_name ) \ No newline at end of file diff --git a/nodes.blend b/nodes.blend index 5f0ed8e..a653e70 100644 Binary files a/nodes.blend and b/nodes.blend differ