This commit is contained in:
Nicolas Predella 2026-07-20 16:53:02 +03:00
parent 4fc503cf85
commit ae911884cc
7 changed files with 210 additions and 14 deletions

View file

@ -28,6 +28,11 @@ class SLICENODE_PathsToGcode(SliceNode):
intrinsics: SliceIntrinsics | None = self.resolve("Intrinsics") intrinsics: SliceIntrinsics | None = self.resolve("Intrinsics")
pather: Pather = Pather(paths) pather: Pather = Pather(paths)
if hasattr(paths, "layer_height"):
pather.layer_height = getattr(paths, "layer_height")
if hasattr(paths, "first_layer_height"):
pather.first_layer_height = getattr(paths, "first_layer_height")
if intrinsics: if intrinsics:
# Note: Pather currently uses bed_center_x/y as its internal attributes. # Note: Pather currently uses bed_center_x/y as its internal attributes.
# We map the bed size center to these attributes if provided. # We map the bed size center to these attributes if provided.

View file

@ -38,6 +38,7 @@ class SLICENODE_SliceShells(SliceNode):
def init(self, context): def init(self, context):
self.add_input("SLICESOCK_Mesh") self.add_input("SLICESOCK_Mesh")
self.add_input("SLICESOCK_Float", "Layer Height").value = 0.2 self.add_input("SLICESOCK_Float", "Layer Height").value = 0.2
self.add_input("SLICESOCK_Float", "First Layer Height").value = 0.2
self.add_input("SLICESOCK_VectorRotation", "Rotation") self.add_input("SLICESOCK_VectorRotation", "Rotation")
self.add_output("SLICESOCK_Paths") self.add_output("SLICESOCK_Paths")
@ -45,7 +46,21 @@ class SLICENODE_SliceShells(SliceNode):
if socket_name != "Paths": return SlicePaths() if socket_name != "Paths": return SlicePaths()
coll: SlicingCollection | SlicingGroup | None = self.resolve("Mesh") coll: SlicingCollection | SlicingGroup | None = self.resolve("Mesh")
layer_height: float = self.resolve("Layer Height") layer_height: float = self.resolve("Layer Height") or 0.2
fl_height: float | None = None
if hasattr(self, "inputs") and "First Layer Height" in self.inputs:
try:
fl_height = self.resolve("First Layer Height")
except Exception:
fl_height = None
else:
try:
fl_height = self.resolve("First Layer Height")
except Exception:
fl_height = None
if fl_height is not None and fl_height <= 0:
fl_height = None
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0) rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
if not coll: return SlicePaths() if not coll: return SlicePaths()
@ -71,15 +86,24 @@ class SLICENODE_SliceShells(SliceNode):
min_z = floor(min_z / layer_height) * layer_height min_z = floor(min_z / layer_height) * layer_height
max_z = merged_object.mesh[:,0:3,2:3].max() max_z = merged_object.mesh[:,0:3,2:3].max()
z_values = np.arange( h_first = fl_height if fl_height is not None else layer_height
min_z + layer_height/2, z_first = min_z + h_first
max_z, z_rest_start = min_z + h_first + layer_height
layer_height
) if z_rest_start < max_z:
z_rest = np.arange(z_rest_start, max_z, layer_height)
z_values = np.concatenate(([z_first], z_rest))
elif z_first < max_z:
z_values = np.array([z_first])
else:
z_values = np.array([])
slices_group: list[list[np.ndarray]] | None = slice_at_range([merged_object.mesh], z_values) slices_group: list[list[np.ndarray]] | None = slice_at_range([merged_object.mesh], z_values)
assert slices_group assert slices_group
slices = SlicePaths(slices_group[0]) slices = SlicePaths(slices_group[0])
slices.layer_height = layer_height
if fl_height is not None:
slices.first_layer_height = fl_height
if rotation and any(rotation): if rotation and any(rotation):
from mathutils import Euler from mathutils import Euler

View file

@ -44,6 +44,8 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
offset_x, offset_y = 0.0, 0.0 offset_x, offset_y = 0.0, 0.0
custom_default_extrusion_rate = 0.0 custom_default_extrusion_rate = 0.0
default_layer_height = 0.2
default_layer_width = 0.4
last_v_idx = -1 last_v_idx = -1
last_p2 = None last_p2 = None
@ -64,6 +66,12 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
elif comment.startswith("DEFAULT_EXTRUSION_RATE="): elif comment.startswith("DEFAULT_EXTRUSION_RATE="):
try: custom_default_extrusion_rate = float(comment.split('=')[1]) try: custom_default_extrusion_rate = float(comment.split('=')[1])
except ValueError: pass except ValueError: pass
elif comment.startswith("DEFAULT_LAYER_HEIGHT="):
try: default_layer_height = float(comment.split('=')[1])
except ValueError: pass
elif comment.startswith("DEFAULT_LAYER_WIDTH="):
try: default_layer_width = float(comment.split('=')[1])
except ValueError: pass
line_no_comment = line.split(';')[0].strip() line_no_comment = line.split(';')[0].strip()
if not line_no_comment: continue if not line_no_comment: continue
@ -171,16 +179,46 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
if ng_name in bpy.data.node_groups: if ng_name in bpy.data.node_groups:
ng = bpy.data.node_groups[ng_name] ng = bpy.data.node_groups[ng_name]
# Check if already assigned # Check if already assigned
has_preview = False mod = None
for mod in obj.modifiers: for m in obj.modifiers:
if mod.type == 'NODES' and mod.node_group == ng: # type: ignore if m.type == 'NODES' and m.node_group == ng: # type: ignore
has_preview = True mod = m
break break
if not has_preview: if not mod:
mod = obj.modifiers.new(name="Preview", type='NODES') # type: ignore mod = obj.modifiers.new(name="Preview", type='NODES') # type: ignore
mod.node_group = ng mod.node_group = ng
# Helper to find socket identifiers by name, falling back to specified key
def find_socket_key(node_group, name: str, fallback: str) -> str:
if hasattr(node_group, "interface") and node_group.interface:
try:
for item in node_group.interface.items:
if getattr(item, "item_type", None) == 'INPUT' and item.name == name:
return item.identifier
except Exception:
pass
if hasattr(node_group, "inputs") and node_group.inputs:
try:
for inp in node_group.inputs:
if inp.name == name:
return inp.identifier
except Exception:
pass
return fallback
height_key = find_socket_key(ng, "Layer Height", "Socket_4")
width_key = find_socket_key(ng, "Layer Width", "Socket_3")
param_key = find_socket_key(ng, "Parameter", "Socket_2")
# Set the values on the modifier
if height_key:
mod[height_key] = default_layer_height * scale_factor
if width_key:
mod[width_key] = default_layer_width * scale_factor
if param_key:
mod[param_key] = 10000.0 / scale_length
def evaluate(self, operator=None): def evaluate(self, operator=None):
gcodes = self.resolve_multi_input("Gcode") gcodes = self.resolve_multi_input("Gcode")
base_mesh_name: str = self.resolve("Name") base_mesh_name: str = self.resolve("Name")

View file

@ -664,6 +664,12 @@ def boolean_slice(subject: SlicePaths, object: SlicePaths, operation: str = 'UNI
results.groups.extend([group] * len(layer_results)) results.groups.extend([group] * len(layer_results))
group += 1 group += 1
for attr in ["layer_height", "layer_width"]:
if hasattr(subject, attr):
setattr(results, attr, getattr(subject, attr))
elif hasattr(object, attr):
setattr(results, attr, getattr(object, attr))
return results return results
def geometry_to_paths(geom: BaseGeometry, z: float) -> list[np.ndarray]: def geometry_to_paths(geom: BaseGeometry, z: float) -> list[np.ndarray]:
@ -1215,9 +1221,26 @@ class Pather(object):
self.feedrate: float = 1200. self.feedrate: float = 1200.
self.travel_feedrate: float = 2400. self.travel_feedrate: float = 2400.
self.extrusion_rate: float = 0.017 self.extrusion_rate: float = 0.017
self.layer_height: float = 0.2
self.first_layer_height: float | None = None
self._layer_width: float | None = None
self.retraction: float = 0.0 self.retraction: float = 0.0
self.lift: float = 0.0 self.lift: float = 0.0
@property
def layer_width(self) -> float:
if self._layer_width is not None:
return self._layer_width
if self.layer_height > 0:
import math
filament_area = math.pi * (1.75 / 2.0) ** 2
return (self.extrusion_rate * filament_area) / self.layer_height
return 0.4
@layer_width.setter
def layer_width(self, val: float | None):
self._layer_width = val
def to_gcode(self, include_header: bool = True) -> str: def to_gcode(self, include_header: bool = True) -> str:
if not self.paths: if not self.paths:
return "" return ""
@ -1254,13 +1277,19 @@ class Pather(object):
f";DEFAULT_FEEDRATE={self.feedrate}", f";DEFAULT_FEEDRATE={self.feedrate}",
f";DEFAULT_TRAVEL_FEEDRATE={self.travel_feedrate}", f";DEFAULT_TRAVEL_FEEDRATE={self.travel_feedrate}",
f";DEFAULT_EXTRUSION_RATE={self.extrusion_rate}", f";DEFAULT_EXTRUSION_RATE={self.extrusion_rate}",
f";DEFAULT_LAYER_HEIGHT={self.layer_height:.6f}",
]
if self.first_layer_height is not None:
header.append(f";DEFAULT_FIRST_LAYER_HEIGHT={self.first_layer_height:.6f}")
header.extend([
f";DEFAULT_LAYER_WIDTH={self.layer_width:.6f}",
f";OFFSET_X={offset_x:.6f}", f";OFFSET_X={offset_x:.6f}",
f";OFFSET_Y={offset_y:.6f}", f";OFFSET_Y={offset_y:.6f}",
"G21 ; set units to mm", "G21 ; set units to mm",
"G90 ; absolute XYZ positioning", "G90 ; absolute XYZ positioning",
"M83 ; relative extrusion mode", "M83 ; relative extrusion mode",
"G92 E0", "G92 E0",
] ])
gcode += header gcode += header
# Safely parse retraction and lift fields. # Safely parse retraction and lift fields.

View file

@ -48,7 +48,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
- **Integer**: Single integer input. - **Integer**: Single integer input.
- **String**: Single string/path input. - **String**: Single string/path input.
- **Rotation**: Input for Euler rotation vectors. - **Rotation**: Input for Euler rotation vectors.
- **Printer Intrinsics**: Configures bed size, extrusion rates, retraction, and Z-lift. - **Printer Intrinsics**: Configures bed size, extrusion/travel rates, retraction, and Z-lift.
- **Gcode Block**: Manual text area for entering raw G-code. - **Gcode Block**: Manual text area for entering raw G-code.
### Geometry Processing ### Geometry Processing

View file

@ -34,7 +34,10 @@ class SliceNode(Node, AutoBlIdName):
def resolve(self, fieldname) -> Any: def resolve(self, fieldname) -> Any:
cache = getattr(self, "_current_cache", None) cache = getattr(self, "_current_cache", None)
visited = getattr(self, "_current_visited", None) visited = getattr(self, "_current_visited", None)
return resolve_input(self.inputs.get(fieldname), cache=cache, visited=visited) inputs = getattr(self, "inputs", None)
if inputs is None:
return None
return resolve_input(inputs.get(fieldname), cache=cache, visited=visited)
def add_output(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = ""): def add_output(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = ""):
eval_name = name or get_label_by_name(bl_idname) eval_name = name or get_label_by_name(bl_idname)

View file

@ -435,5 +435,102 @@ class TestSlice(unittest.TestCase):
self.assertNotIn("E-", gcode) self.assertNotIn("E-", gcode)
self.assertNotIn("Z0.700", gcode) self.assertNotIn("Z0.700", gcode)
def test_pather_travel_feedrate(self):
from NodeSlicer.core.slice import Pather
path1 = np.array([
[10.0, 10.0, 0.2],
[20.0, 10.0, 0.2]
])
path2 = np.array([
[30.0, 30.0, 0.2],
[40.0, 30.0, 0.2]
])
pather = Pather([path1, path2])
pather.feedrate = 1200.0
pather.travel_feedrate = 3000.0
pather.bed_center_x = 25.0
pather.bed_center_y = 20.0
# Verify travel feedrate is in the header comments
gcode_with_header = pather.to_gcode(include_header=True)
self.assertIn(";DEFAULT_TRAVEL_FEEDRATE=3000.0", gcode_with_header)
self.assertIn(";DEFAULT_FEEDRATE=1200.0", gcode_with_header)
# Verify G0 travel moves use the travel_feedrate
gcode = pather.to_gcode(include_header=False)
# The first move should travel to the start of path1 at travel_feedrate
self.assertIn("G0 X10.000 Y10.000 Z0.200 F3000.0", gcode)
# The transition travel move to the start of path2 should also use travel_feedrate
self.assertIn("G0 X30.000 Y30.000 F3000.0", gcode)
def test_pather_layer_metadata(self):
import math
from NodeSlicer.core.slice import Pather, SlicePaths
path1 = np.array([
[10.0, 10.0, 0.2],
[20.0, 10.0, 0.2]
])
slices = SlicePaths([path1])
slices.layer_height = 0.25
pather = Pather(slices)
pather.layer_height = getattr(slices, "layer_height")
pather.extrusion_rate = 0.035
expected_width = (0.035 * math.pi * (1.75 / 2.0) ** 2) / 0.25
gcode = pather.to_gcode(include_header=True)
self.assertIn(";DEFAULT_LAYER_HEIGHT=0.250000", gcode)
self.assertIn(f";DEFAULT_LAYER_WIDTH={expected_width:.6f}", gcode)
def test_first_layer_height(self):
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_SliceShells
from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject, SlicingCollection
# Create a simple box mesh [8, 4, 3] from Z=0 to Z=1
vertices = np.array([
[0.0, 0.0, 0.0],
[10.0, 0.0, 0.0],
[10.0, 10.0, 0.0],
[0.0, 10.0, 0.0],
[0.0, 0.0, 1.0],
[10.0, 0.0, 1.0],
[10.0, 10.0, 1.0],
[0.0, 10.0, 1.0],
])
faces = [
[0, 1, 2], [0, 2, 3],
[4, 6, 5], [4, 7, 6],
[0, 4, 5], [0, 5, 1],
[1, 5, 6], [1, 6, 2],
[2, 6, 7], [2, 7, 3],
[3, 7, 4], [3, 4, 0]
]
triangles = np.zeros((len(faces), 4, 3))
for i, face in enumerate(faces):
triangles[i, 0] = vertices[face[0]]
triangles[i, 1] = vertices[face[1]]
triangles[i, 2] = vertices[face[2]]
sobj = SlicingObject()
sobj.mesh = triangles
scoll = SlicingCollection([], "test")
scoll.objects = [sobj]
node = SLICENODE_SliceShells()
# Test without inputs (simulating missing socket resolution)
node.resolve = lambda name: scoll if name == "Mesh" else (0.2 if name == "Layer Height" else (0.3 if name == "First Layer Height" else None))
slices = node.evaluate_output("Paths")
self.assertTrue(len(slices) > 0)
# First slice Z should be 0.3 / 2 = 0.15
self.assertAlmostEqual(slices[0][0, 2], 0.15)
self.assertEqual(getattr(slices, "first_layer_height", None), 0.3)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()