up
This commit is contained in:
parent
4fc503cf85
commit
ae911884cc
7 changed files with 210 additions and 14 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue