up
This commit is contained in:
parent
1d0147ce73
commit
5f49097f80
8 changed files with 93 additions and 48 deletions
|
|
@ -74,8 +74,8 @@ class SLICENODE_InputObject(InputNode):
|
||||||
bl_icon = "OBJECT_DATA"
|
bl_icon = "OBJECT_DATA"
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_InputOverrides(InputNode):
|
class SLICENODE_InputSettings(InputNode):
|
||||||
input_type = "Overrides"
|
input_type = "Settings"
|
||||||
bl_icon = "OBJECT_DATA"
|
bl_icon = "OBJECT_DATA"
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
@ -345,27 +345,26 @@ class SLICENODE_GetMesh(SliceNode):
|
||||||
class SLICENODE_GetPaths(SliceNode):
|
class SLICENODE_GetPaths(SliceNode):
|
||||||
bl_label = "Get Paths"
|
bl_label = "Get Paths"
|
||||||
|
|
||||||
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):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Object")
|
self.add_input("SLICESOCK_Object")
|
||||||
|
self.add_input("SLICESOCK_String", "Extrusion Multiplier Attribute").value = 'e_mult'
|
||||||
|
self.add_input("SLICESOCK_String", "Feedrate Attribute").value = 'f_rate'
|
||||||
|
self.add_input("SLICESOCK_Settings", "Extra Attributes")
|
||||||
self.add_output("SLICESOCK_Paths")
|
self.add_output("SLICESOCK_Paths")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
assert layout
|
pass
|
||||||
layout.prop(self, "attr_name")
|
|
||||||
layout.prop(self, "f_attr_name")
|
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||||
obj: Any = self.resolve("Object")
|
obj: Any = self.resolve("Object")
|
||||||
attr_name: str = self.attr_name
|
attr_name: str = self.resolve("Extrusion Multiplier Attribute")
|
||||||
f_attr_name: str = self.f_attr_name
|
f_attr_name: str = self.resolve("Feedrate Attribute")
|
||||||
|
extra_attrs: dict[str, str] = self.resolve("Extra Attributes")
|
||||||
|
|
||||||
from ...infra.blender.blender_mesh_capture import capture_mesh_edges
|
from ...infra.blender.blender_mesh_capture import capture_mesh_edges
|
||||||
so, e_rates, f_rates = capture_mesh_edges(obj, attr_name=attr_name, f_attr_name=f_attr_name)
|
so, e_rates, f_rates, extra_attrs_data = capture_mesh_edges(obj, attr_name=attr_name, f_attr_name=f_attr_name, extra_attrs=extra_attrs)
|
||||||
|
|
||||||
return SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates)
|
return SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_SliceShells(SliceNode):
|
class SLICENODE_SliceShells(SliceNode):
|
||||||
|
|
@ -643,6 +642,15 @@ class SLICENODE_GcodeRegexReplace(SliceNode):
|
||||||
return gcode
|
return gcode
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Convert $1, $2, ${name} etc to Python backreferences \g<1>, \g<2>, \g<name>
|
||||||
|
def subst(match):
|
||||||
|
if match.group(1): return '$'
|
||||||
|
elif match.group(2): return '\\g<' + match.group(2) + '>'
|
||||||
|
elif match.group(3): return '\\g<' + match.group(3) + '>'
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
replace = re.sub(r'(\$\$)|(?:\$\{([^}]+)\})|(?:\$([a-zA-Z0-9_]+))', subst, replace)
|
||||||
|
|
||||||
return re.sub(find, replace, gcode)
|
return re.sub(find, replace, gcode)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Regex error in node {self.name}: {e}")
|
print(f"Regex error in node {self.name}: {e}")
|
||||||
|
|
@ -731,10 +739,10 @@ class SLICENODE_JoinGcode(SliceNodeJoiner):
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_JoinOverrides(SliceNodeJoiner):
|
class SLICENODE_JoinSettings(SliceNodeJoiner):
|
||||||
bl_label = "Join Overrides"
|
bl_label = "Join Settings"
|
||||||
socket_type = "SLICESOCK_Overrides"
|
socket_type = "SLICESOCK_Settings"
|
||||||
output_socket_type = "SLICESOCK_OverridesRo"
|
output_socket_type = "SLICESOCK_SettingsRo"
|
||||||
|
|
||||||
def evaluate_output(self, socket_name: str, **_kwargs) -> dict[str, str]:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> dict[str, str]:
|
||||||
merged: dict[str, str] = {}
|
merged: dict[str, str] = {}
|
||||||
|
|
@ -1341,7 +1349,7 @@ class SLICENODE_PrusaslicerSlice(SliceNode):
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Mesh", "Mesh")
|
self.add_input("SLICESOCK_Mesh", "Mesh")
|
||||||
self.add_input("SLICESOCK_PrusaslicerConf", "Config")
|
self.add_input("SLICESOCK_PrusaslicerConf", "Config")
|
||||||
self.add_input("SLICESOCK_Overrides", "Overrides")
|
self.add_input("SLICESOCK_Settings", "Settings")
|
||||||
self.add_output("SLICESOCK_GcodeRo")
|
self.add_output("SLICESOCK_GcodeRo")
|
||||||
|
|
||||||
def draw_buttons(self, context, layout):
|
def draw_buttons(self, context, layout):
|
||||||
|
|
@ -1359,7 +1367,7 @@ class SLICENODE_PrusaslicerSlice(SliceNode):
|
||||||
mesh_data: SlicingCollection | SlicingGroup = self.resolve("Mesh")
|
mesh_data: SlicingCollection | SlicingGroup = self.resolve("Mesh")
|
||||||
|
|
||||||
conf_data = self.resolve("Config")
|
conf_data = self.resolve("Config")
|
||||||
overrides = self.resolve("Overrides")
|
overrides = self.resolve("Settings")
|
||||||
|
|
||||||
if not mesh_data:
|
if not mesh_data:
|
||||||
if operator: operator.report({'ERROR'}, "No Mesh data connected")
|
if operator: operator.report({'ERROR'}, "No Mesh data connected")
|
||||||
|
|
|
||||||
|
|
@ -153,10 +153,8 @@ class SLICESOCK_PrusaslicerConf(LabelNodeSocket):
|
||||||
default = None
|
default = None
|
||||||
bl_color = (0.95, 0., 0., 1.0) # #F26C21
|
bl_color = (0.95, 0., 0., 1.0) # #F26C21
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class OverrideItem(bpy.types.PropertyGroup):
|
class SettingItem(bpy.types.PropertyGroup):
|
||||||
field: NfStringProperty(name="Field", default="") # pyright: ignore[reportInvalidTypeForm]
|
field: NfStringProperty(name="Field", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||||
value_str: NfStringProperty(name="Value", default="") # pyright: ignore[reportInvalidTypeForm]
|
value_str: NfStringProperty(name="Value", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
|
|
@ -170,10 +168,10 @@ class OverrideItem(bpy.types.PropertyGroup):
|
||||||
row.prop(self, "value_str", text="")
|
row.prop(self, "value_str", text="")
|
||||||
|
|
||||||
from ...infra.blender.mixins import CollectionMixin
|
from ...infra.blender.mixins import CollectionMixin
|
||||||
class SLICESOCK_Overrides_Common(CollectionMixin, InputNodeSocket):
|
class SLICESOCK_Settings_Common(CollectionMixin, InputNodeSocket):
|
||||||
bl_label = "Overrides"
|
bl_label = "Settings"
|
||||||
|
|
||||||
items: CollectionProperty(type=OverrideItem) # pyright: ignore[reportInvalidTypeForm]
|
items: CollectionProperty(type=SettingItem) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
bl_color = (1.0, 1.0, 0.0, 1.0) # Yellow
|
bl_color = (1.0, 1.0, 0.0, 1.0) # Yellow
|
||||||
|
|
||||||
|
|
@ -186,9 +184,9 @@ class SLICESOCK_Overrides_Common(CollectionMixin, InputNodeSocket):
|
||||||
return overrides
|
return overrides
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICESOCK_Overrides(SLICESOCK_Overrides_Common): ...
|
class SLICESOCK_Settings(SLICESOCK_Settings_Common): ...
|
||||||
@register_class
|
@register_class
|
||||||
class SLICESOCK_OverridesRo(ReadonlySocket, SLICESOCK_Overrides_Common): ...
|
class SLICESOCK_SettingsRo(ReadonlySocket, SLICESOCK_Settings_Common): ...
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICESOCK_ExecutionLink(LabelNodeSocket):
|
class SLICESOCK_ExecutionLink(LabelNodeSocket):
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ class SLICENODE_MT_DataInput(Menu):
|
||||||
layout.operator("node.add_node", text="Vector").type = "SLICENODE_InputVector"
|
layout.operator("node.add_node", text="Vector").type = "SLICENODE_InputVector"
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator("node.add_node", text="Object").type = "SLICENODE_InputObject"
|
layout.operator("node.add_node", text="Object").type = "SLICENODE_InputObject"
|
||||||
layout.operator("node.add_node", text="Overrides").type = "SLICENODE_InputOverrides"
|
layout.operator("node.add_node", text="Settings").type = "SLICENODE_InputSettings"
|
||||||
layout.operator("node.add_node", text="PrusaSlicer Config").type = "SLICENODE_PrusaslicerConf"
|
layout.operator("node.add_node", text="PrusaSlicer Config").type = "SLICENODE_PrusaslicerConf"
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator("node.add_node", text="Printer").type = "SLICENODE_InputPrinter"
|
layout.operator("node.add_node", text="Printer").type = "SLICENODE_InputPrinter"
|
||||||
|
|
@ -215,7 +215,7 @@ class SLICENODE_MT_Joiners(Menu):
|
||||||
layout.operator("node.add_node", text="Join Mesh Groups").type = "SLICENODE_JoinMeshGroups"
|
layout.operator("node.add_node", text="Join Mesh Groups").type = "SLICENODE_JoinMeshGroups"
|
||||||
layout.operator("node.add_node", text="Join Paths").type = "SLICENODE_JoinPaths"
|
layout.operator("node.add_node", text="Join Paths").type = "SLICENODE_JoinPaths"
|
||||||
layout.operator("node.add_node", text="Join Gcode").type = "SLICENODE_JoinGcode"
|
layout.operator("node.add_node", text="Join Gcode").type = "SLICENODE_JoinGcode"
|
||||||
layout.operator("node.add_node", text="Join Overrides").type = "SLICENODE_JoinOverrides"
|
layout.operator("node.add_node", text="Join Settings").type = "SLICENODE_JoinSettings"
|
||||||
layout.operator("node.add_node", text="Join Execution").type = "SLICENODE_JoinExecution"
|
layout.operator("node.add_node", text="Join Execution").type = "SLICENODE_JoinExecution"
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,9 @@ class SlicePaths(list[np.ndarray]):
|
||||||
groups: list[int]
|
groups: list[int]
|
||||||
extrusion_rate: list[np.ndarray]
|
extrusion_rate: list[np.ndarray]
|
||||||
feedrate: list[np.ndarray]
|
feedrate: list[np.ndarray]
|
||||||
|
extra_attributes: dict[str, list[np.ndarray]]
|
||||||
|
|
||||||
def __init__(self, paths: list[np.ndarray] = [], groups: list[int] = [], extrusion_rate: list[np.ndarray] | None = None, feedrate: 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, extra_attributes: dict[str, list[np.ndarray]] | None = None):
|
||||||
super().__init__(paths)
|
super().__init__(paths)
|
||||||
|
|
||||||
self.groups = []
|
self.groups = []
|
||||||
|
|
@ -31,8 +32,10 @@ class SlicePaths(list[np.ndarray]):
|
||||||
self.feedrate = list(feedrate)
|
self.feedrate = list(feedrate)
|
||||||
else:
|
else:
|
||||||
self.feedrate = [np.zeros(len(p)) for p in paths]
|
self.feedrate = [np.zeros(len(p)) for p in paths]
|
||||||
|
|
||||||
|
self.extra_attributes = extra_attributes or {}
|
||||||
|
|
||||||
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:
|
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, extra_attributes: dict[str, list[np.ndarray]] | None = None) -> SlicePaths:
|
||||||
"""Returns a new SlicePaths object with new paths/groups but preserving all other metadata."""
|
"""Returns a new SlicePaths object with new paths/groups but preserving all other metadata."""
|
||||||
if extrusion_rate is None:
|
if extrusion_rate is None:
|
||||||
# Only reuse extrusion_rate if path lengths match exactly
|
# Only reuse extrusion_rate if path lengths match exactly
|
||||||
|
|
@ -42,11 +45,15 @@ class SlicePaths(list[np.ndarray]):
|
||||||
if feedrate is None:
|
if feedrate is None:
|
||||||
if len(paths) == len(self) and all(len(p) == len(op) for p, op in zip(paths, self)):
|
if len(paths) == len(self) and all(len(p) == len(op) for p, op in zip(paths, self)):
|
||||||
feedrate = self.feedrate
|
feedrate = self.feedrate
|
||||||
|
|
||||||
|
if extra_attributes is None:
|
||||||
|
if len(paths) == len(self) and all(len(p) == len(op) for p, op in zip(paths, self)):
|
||||||
|
extra_attributes = self.extra_attributes
|
||||||
|
|
||||||
new_obj = SlicePaths(paths, groups if groups is not None else self.groups, extrusion_rate, feedrate)
|
new_obj = SlicePaths(paths, groups if groups is not None else self.groups, extrusion_rate, feedrate, extra_attributes)
|
||||||
# Copy any other attributes that might have been added
|
# Copy any other attributes that might have been added
|
||||||
for k, v in self.__dict__.items():
|
for k, v in self.__dict__.items():
|
||||||
if k not in ["groups", "extrusion_rate", "feedrate"]:
|
if k not in ["groups", "extrusion_rate", "feedrate", "extra_attributes"]:
|
||||||
setattr(new_obj, k, v)
|
setattr(new_obj, k, v)
|
||||||
return new_obj
|
return new_obj
|
||||||
|
|
||||||
|
|
@ -534,6 +541,7 @@ class Pather(object):
|
||||||
self.paths = paths
|
self.paths = paths
|
||||||
self.extrusion_rates = getattr(paths, "extrusion_rate", [np.ones(len(p)) for p in 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.feedrates = getattr(paths, "feedrate", [np.zeros(len(p)) for p in paths])
|
||||||
|
self.extra_attributes = getattr(paths, "extra_attributes", {})
|
||||||
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.feedrate: float = 1200.
|
||||||
|
|
@ -593,6 +601,7 @@ class Pather(object):
|
||||||
|
|
||||||
e_rates = self.extrusion_rates[i]
|
e_rates = self.extrusion_rates[i]
|
||||||
f_rates = self.feedrates[i]
|
f_rates = self.feedrates[i]
|
||||||
|
extras = {prefix: vals[i] for prefix, vals in self.extra_attributes.items()}
|
||||||
|
|
||||||
x0, y0, z0 = coords[0]
|
x0, y0, z0 = coords[0]
|
||||||
|
|
||||||
|
|
@ -606,6 +615,7 @@ class Pather(object):
|
||||||
old_x, old_y, old_z = x0, y0, z0
|
old_x, old_y, old_z = x0, y0, z0
|
||||||
|
|
||||||
old_fr = 0
|
old_fr = 0
|
||||||
|
old_extras = {prefix: None for prefix in extras}
|
||||||
for j, (p1, p2) in enumerate(zip(coords, coords[1:])):
|
for j, (p1, p2) in enumerate(zip(coords, coords[1:])):
|
||||||
seg_len: float = segment_length_3d(p1, p2)
|
seg_len: float = segment_length_3d(p1, p2)
|
||||||
e_segment = seg_len * self.extrusion_rate * e_rates[j]
|
e_segment = seg_len * self.extrusion_rate * e_rates[j]
|
||||||
|
|
@ -624,6 +634,12 @@ class Pather(object):
|
||||||
if e_segment: cur_gcode += f" E{e_segment:.5f}"
|
if e_segment: cur_gcode += f" E{e_segment:.5f}"
|
||||||
if not old_fr == fr: cur_gcode += f" F{fr:.3f}"
|
if not old_fr == fr: cur_gcode += f" F{fr:.3f}"
|
||||||
|
|
||||||
|
for prefix, evals in extras.items():
|
||||||
|
val = evals[j]
|
||||||
|
if not old_extras[prefix] == val:
|
||||||
|
cur_gcode += f" {prefix}{val:.3f}"
|
||||||
|
old_extras[prefix] = val
|
||||||
|
|
||||||
gcode += [cur_gcode]
|
gcode += [cur_gcode]
|
||||||
|
|
||||||
max_z = max(max_z, z)
|
max_z = max(max_z, z)
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ This file provides a mapping of modules to their primary responsibilities and ke
|
||||||
- `slice.py`:
|
- `slice.py`:
|
||||||
- `slice_at_range()`: Top-level slicing function that iterates through Z-heights.
|
- `slice_at_range()`: Top-level slicing function that iterates through Z-heights.
|
||||||
- `slice_at_z()`: Vectorized plane-mesh intersection using NumPy.
|
- `slice_at_z()`: Vectorized plane-mesh intersection using NumPy.
|
||||||
- `Pather`: Class that converts toolpaths into G-code strings.
|
- `Pather`: Class that converts toolpaths into G-code strings. Supports per-segment extrusion rates, feedrates, and custom extra attributes.
|
||||||
- `segments_to_slice()`: Utility to convert raw intersection segments into closed loops using Pyclipper.
|
- `segments_to_slice()`: Utility to convert raw intersection segments into closed loops using Pyclipper.
|
||||||
- `link_segments()`: Logic for joining loose segments into continuous paths.
|
- `link_segments()`: Logic for joining loose segments into continuous paths.
|
||||||
- `buffer_slice()`: Offset utility using PyclipperOffset. Supports rotated offsetting.
|
- `buffer_slice()`: Offset utility using PyclipperOffset. Supports rotated offsetting.
|
||||||
|
|
@ -42,7 +42,7 @@ This file provides a mapping of modules to their primary responsibilities and ke
|
||||||
|
|
||||||
- `blender_mesh_capture.py`:
|
- `blender_mesh_capture.py`:
|
||||||
- `SlicingObject`: Represents a single mesh as a NumPy triangle array.
|
- `SlicingObject`: Represents a single mesh as a NumPy triangle array.
|
||||||
- `capture_mesh_edges()`: Extracts edges from a Blender mesh for path-based workflows.
|
- `capture_mesh_edges()`: Extracts edges from a Blender mesh for path-based workflows. Supports capturing extra per-vertex attributes.
|
||||||
|
|
||||||
- `blender_slice_nodes_generic.py`:
|
- `blender_slice_nodes_generic.py`:
|
||||||
- Base classes for NodeSlicer nodes (`SliceNode`) and sockets (`SliceNodeSocket`).
|
- Base classes for NodeSlicer nodes (`SliceNode`) and sockets (`SliceNodeSocket`).
|
||||||
|
|
|
||||||
10
doc/NODES.md
10
doc/NODES.md
|
|
@ -12,8 +12,8 @@
|
||||||
| `SLICESOCK_Mesh` | `SlicingObject` | NumPy NDArray (Triangles) | Internal mesh data (Transformed/Scaled) |
|
| `SLICESOCK_Mesh` | `SlicingObject` | NumPy NDArray (Triangles) | Internal mesh data (Transformed/Scaled) |
|
||||||
| `SLICESOCK_Paths` | `SlicePaths` (list) | List of paths (N, 3) + groups | Generic toolpaths or slice boundaries |
|
| `SLICESOCK_Paths` | `SlicePaths` (list) | List of paths (N, 3) + groups | Generic toolpaths or slice boundaries |
|
||||||
| `SLICESOCK_Gcode` | `str` | Multi-line string | Generated G-code text |
|
| `SLICESOCK_Gcode` | `str` | Multi-line string | Generated G-code text |
|
||||||
| `SLICESOCK_Overrides` | `dict[str, str]` | Python dictionary | Mapping of configuration fields to values |
|
| `SLICESOCK_Settings` | `dict[str, str]` | Python dictionary | Mapping of configuration fields to values |
|
||||||
| `SLICESOCK_OverridesRo` | `dict[str, str]` | Python dictionary | Read-only merged configuration overrides |
|
| `SLICESOCK_SettingsRo` | `dict[str, str]` | Python dictionary | Read-only merged configuration overrides |
|
||||||
|
|
||||||
## Detailed Data Types
|
## Detailed Data Types
|
||||||
|
|
||||||
|
|
@ -28,6 +28,7 @@ The `SlicingObject` class wraps a high-performance NumPy representation of a Ble
|
||||||
A custom list subclass `SlicePaths(list[np.ndarray])` containing toolpaths.
|
A custom list subclass `SlicePaths(list[np.ndarray])` containing toolpaths.
|
||||||
- **Content**: Each `np.ndarray` has shape `(N, 3)`.
|
- **Content**: Each `np.ndarray` has shape `(N, 3)`.
|
||||||
- **Groups**: Maintains a `.groups` list of integers mapping each path to a layer or logical group.
|
- **Groups**: Maintains a `.groups` list of integers mapping each path to a layer or logical group.
|
||||||
|
- **Extra Attributes**: Maintains an `.extra_attributes` dictionary mapping G-code prefixes (e.g., `S`, `M`) to lists of NumPy arrays, enabling per-segment custom parameters.
|
||||||
- **Z-Awareness**: Coordinates are stored as 3D `(X, Y, Z)` to maintain the correct layer height.
|
- **Z-Awareness**: Coordinates are stored as 3D `(X, Y, Z)` to maintain the correct layer height.
|
||||||
- **Topology**: Generated and cleaned using `pyclipper`.
|
- **Topology**: Generated and cleaned using `pyclipper`.
|
||||||
|
|
||||||
|
|
@ -53,6 +54,9 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
||||||
### Geometry Processing
|
### Geometry Processing
|
||||||
- **Get Mesh**: Converts a Blender Object into internal `SlicingObject` format.
|
- **Get Mesh**: Converts a Blender Object into internal `SlicingObject` format.
|
||||||
- **Get Paths**: Extracts existing edges from a mesh as paths.
|
- **Get Paths**: Extracts existing edges from a mesh as paths.
|
||||||
|
- **Extrusion Multiplier Attribute**: Mesh attribute to use for per-segment extrusion rates.
|
||||||
|
- **Feedrate Attribute**: Mesh attribute to use for per-segment feedrates.
|
||||||
|
- **Extra Attributes**: Dictionary mapping mesh attribute names to custom G-code prefixes (e.g., `opening_size` -> `S`).
|
||||||
- **Slice Shells**: Slices a `SlicingObject` into 2D `SlicePaths` at a given layer height.
|
- **Slice Shells**: Slices a `SlicingObject` into 2D `SlicePaths` at a given layer height.
|
||||||
- **Buffer Shells**: Offsets/buffers 2D `SlicePaths`. Supports rotated offsetting and multiple miter types.
|
- **Buffer Shells**: Offsets/buffers 2D `SlicePaths`. Supports rotated offsetting and multiple miter types.
|
||||||
- **Infill**: Generates a cross-hatch grid or parallel lines clipped by boundary paths.
|
- **Infill**: Generates a cross-hatch grid or parallel lines clipped by boundary paths.
|
||||||
|
|
@ -66,7 +70,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
||||||
- **Join Mesh**: Merges multiple `SlicingObject` instances into one.
|
- **Join Mesh**: Merges multiple `SlicingObject` instances into one.
|
||||||
- **Join Paths**: Combines multiple `SlicePaths`. Supports Sequential or Interleaved modes.
|
- **Join Paths**: Combines multiple `SlicePaths`. Supports Sequential or Interleaved modes.
|
||||||
- **Join Gcode**: Concatenates multiple G-code strings.
|
- **Join Gcode**: Concatenates multiple G-code strings.
|
||||||
- **Join Overrides**: Sequentially merges multiple override dictionaries. Later inputs overwrite earlier ones for matching keys.
|
- **Join Settings**: Sequentially merges multiple override dictionaries. Later inputs overwrite earlier ones for matching keys.
|
||||||
- **Integer Math**: Simple integer operations (Add, Multiply, etc.).
|
- **Integer Math**: Simple integer operations (Add, Multiply, etc.).
|
||||||
|
|
||||||
### Action & I/O
|
### Action & I/O
|
||||||
|
|
|
||||||
|
|
@ -411,11 +411,12 @@ def _cached_capture_mesh_edges(
|
||||||
matrix_tuple: tuple[float, ...],
|
matrix_tuple: tuple[float, ...],
|
||||||
scene_scale: float,
|
scene_scale: float,
|
||||||
attr_name: str = "e_mult",
|
attr_name: str = "e_mult",
|
||||||
f_attr_name: str = "f_rate"
|
f_attr_name: str = "f_rate",
|
||||||
) -> tuple[list[NDArray[float64]], list[NDArray[float64]], list[NDArray[float64]]]:
|
extra_attrs: dict[str, str] | None = None
|
||||||
|
) -> tuple[list[NDArray[float64]], list[NDArray[float64]], list[NDArray[float64]], dict[str, list[NDArray[float64]]]]:
|
||||||
# This helper expects that the object and mesh exist and are accessible via bpy.data
|
# This helper expects that the object and mesh exist and are accessible via bpy.data
|
||||||
obj = bpy.data.objects.get(obj_name)
|
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,
|
# 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.
|
# but we assume this is called when we know we need to re-capture.
|
||||||
|
|
@ -425,10 +426,10 @@ def _cached_capture_mesh_edges(
|
||||||
obj_eval = obj.evaluated_get(depsgraph)
|
obj_eval = obj.evaluated_get(depsgraph)
|
||||||
mesh = obj_eval.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
mesh = obj_eval.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
||||||
except Exception:
|
except Exception:
|
||||||
return [], [], []
|
return [], [], [], {}
|
||||||
|
|
||||||
if not mesh or not mesh.vertices:
|
if not mesh or not mesh.vertices:
|
||||||
return [], [], []
|
return [], [], [], {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
world_mat = np.array(matrix_tuple).reshape(4, 4)
|
world_mat = np.array(matrix_tuple).reshape(4, 4)
|
||||||
|
|
@ -443,6 +444,14 @@ def _cached_capture_mesh_edges(
|
||||||
f_rates = np.zeros(num_verts, dtype=np.float64)
|
f_rates = np.zeros(num_verts, dtype=np.float64)
|
||||||
if f_attr_name in mesh.attributes:
|
if f_attr_name in mesh.attributes:
|
||||||
mesh.attributes[f_attr_name].data.foreach_get("value", f_rates)
|
mesh.attributes[f_attr_name].data.foreach_get("value", f_rates)
|
||||||
|
|
||||||
|
captured_extras = {}
|
||||||
|
if extra_attrs:
|
||||||
|
for attr_key, prefix in extra_attrs.items():
|
||||||
|
vals = np.zeros(num_verts, dtype=np.float64)
|
||||||
|
if attr_key in mesh.attributes:
|
||||||
|
mesh.attributes[attr_key].data.foreach_get("value", vals)
|
||||||
|
captured_extras[prefix] = vals
|
||||||
|
|
||||||
tx_verts = (verts @ world_mat[:3, :3].T + world_mat[:3, 3]) * scene_scale
|
tx_verts = (verts @ world_mat[:3, :3].T + world_mat[:3, 3]) * scene_scale
|
||||||
|
|
||||||
|
|
@ -453,33 +462,42 @@ def _cached_capture_mesh_edges(
|
||||||
raw_paths = np.split(tx_verts, split_at)
|
raw_paths = np.split(tx_verts, split_at)
|
||||||
raw_e_mults = np.split(e_mults, split_at)
|
raw_e_mults = np.split(e_mults, split_at)
|
||||||
raw_f_rates = np.split(f_rates, split_at)
|
raw_f_rates = np.split(f_rates, split_at)
|
||||||
|
raw_extras = {prefix: np.split(vals, split_at) for prefix, vals in captured_extras.items()}
|
||||||
indices_split = np.split(np.arange(num_verts), split_at)
|
indices_split = np.split(np.arange(num_verts), split_at)
|
||||||
|
|
||||||
final_paths = []
|
final_paths = []
|
||||||
final_e_mults = []
|
final_e_mults = []
|
||||||
final_f_rates = []
|
final_f_rates = []
|
||||||
|
final_extras = {prefix: [] for prefix in captured_extras}
|
||||||
for i, path_coords in enumerate(raw_paths):
|
for i, path_coords in enumerate(raw_paths):
|
||||||
if path_coords.shape[0] == 0:
|
if path_coords.shape[0] == 0:
|
||||||
continue
|
continue
|
||||||
idx = indices_split[i]
|
idx = indices_split[i]
|
||||||
e_vals = raw_e_mults[i]
|
e_vals = raw_e_mults[i]
|
||||||
f_vals = raw_f_rates[i]
|
f_vals = raw_f_rates[i]
|
||||||
|
p_extras = {prefix: raw_extras[prefix][i] for prefix in captured_extras}
|
||||||
|
|
||||||
if len(idx) > 2 and tuple(sorted((idx[0], idx[-1]))) in edges_set:
|
if len(idx) > 2 and tuple(sorted((idx[0], idx[-1]))) in edges_set:
|
||||||
path_coords = np.vstack((path_coords, path_coords[0]))
|
path_coords = np.vstack((path_coords, path_coords[0]))
|
||||||
e_vals = np.concatenate((e_vals, [e_vals[0]]))
|
e_vals = np.concatenate((e_vals, [e_vals[0]]))
|
||||||
f_vals = np.concatenate((f_vals, [f_vals[0]]))
|
f_vals = np.concatenate((f_vals, [f_vals[0]]))
|
||||||
|
for prefix in captured_extras:
|
||||||
|
p_extras[prefix] = np.concatenate((p_extras[prefix], [p_extras[prefix][0]]))
|
||||||
|
|
||||||
final_paths.append(path_coords)
|
final_paths.append(path_coords)
|
||||||
final_e_mults.append(e_vals)
|
final_e_mults.append(e_vals)
|
||||||
final_f_rates.append(f_vals)
|
final_f_rates.append(f_vals)
|
||||||
|
for prefix in captured_extras:
|
||||||
|
final_extras[prefix].append(p_extras[prefix])
|
||||||
|
|
||||||
return final_paths, final_e_mults, final_f_rates
|
return final_paths, final_e_mults, final_f_rates, final_extras
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if hasattr(obj_eval, "to_mesh_clear"):
|
if hasattr(obj_eval, "to_mesh_clear"):
|
||||||
obj_eval.to_mesh_clear()
|
obj_eval.to_mesh_clear()
|
||||||
|
|
||||||
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]]]:
|
def capture_mesh_edges(obj: Object, attr_name: str = "e_mult", f_attr_name: str = "f_rate", extra_attrs: dict[str, str] | None = None) -> tuple[list[NDArray[float64]], list[NDArray[float64]], list[NDArray[float64]], dict[str, list[NDArray[float64]]]]:
|
||||||
if not obj or not obj.data: return [], [], []
|
if not obj or not obj.data: return [], [], [], {}
|
||||||
|
|
||||||
if obj.mode == 'EDIT':
|
if obj.mode == 'EDIT':
|
||||||
obj.update_from_editmode()
|
obj.update_from_editmode()
|
||||||
|
|
@ -496,5 +514,6 @@ def capture_mesh_edges(obj: Object, attr_name: str = "e_mult", f_attr_name: str
|
||||||
matrix_tuple,
|
matrix_tuple,
|
||||||
scene_scale,
|
scene_scale,
|
||||||
attr_name,
|
attr_name,
|
||||||
f_attr_name
|
f_attr_name,
|
||||||
|
extra_attrs
|
||||||
)
|
)
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
from ..core.nodeslicer.sockets import OverrideItem
|
from ..core.nodeslicer.sockets import SettingItem
|
||||||
from ..registry import register_class
|
from ..registry import register_class
|
||||||
|
|
||||||
extruder_options: list[tuple[str, str, str]] = [
|
extruder_options: list[tuple[str, str, str]] = [
|
||||||
|
|
@ -29,4 +29,4 @@ class SlicerObjectPropertyGroup(bpy.types.PropertyGroup):
|
||||||
object_type: NfEnumProperty(name="Part type", default="ModelPart", items=object_type_options) # pyright: ignore[reportInvalidTypeForm]
|
object_type: NfEnumProperty(name="Part type", default="ModelPart", items=object_type_options) # pyright: ignore[reportInvalidTypeForm]
|
||||||
extruder: NfEnumProperty(name="Extruder", default="0", items=extruder_options) # pyright: ignore[reportInvalidTypeForm]
|
extruder: NfEnumProperty(name="Extruder", default="0", items=extruder_options) # pyright: ignore[reportInvalidTypeForm]
|
||||||
search_term: NfStringProperty(name="Search") # pyright: ignore[reportInvalidTypeForm]
|
search_term: NfStringProperty(name="Search") # pyright: ignore[reportInvalidTypeForm]
|
||||||
modifiers: bpy.props.CollectionProperty(type=OverrideItem) # pyright: ignore[reportInvalidTypeForm]
|
modifiers: bpy.props.CollectionProperty(type=SettingItem) # pyright: ignore[reportInvalidTypeForm]
|
||||||
Loading…
Add table
Reference in a new issue