249 lines
9.7 KiB
Python
249 lines
9.7 KiB
Python
from __future__ import annotations
|
|
import bpy
|
|
import os
|
|
|
|
from ..bpy_ns_types import NfBoolProperty
|
|
from ....registry import register_class
|
|
from .base import ExecSliceNode
|
|
|
|
@register_class
|
|
class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|
bl_label = "Gcode to Preview Mesh"
|
|
|
|
button_label = "Update Preview"
|
|
show_e_0: NfBoolProperty(name="Show Zero Extrusions", default=False) # pyright: ignore[reportInvalidTypeForm]
|
|
|
|
def init(self, context):
|
|
self._init_exec()
|
|
self.add_input("SLICESOCK_String", "Name")
|
|
self.add_multi_socket_group("SLICESOCK_Gcode", name="Gcode")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
super().draw_buttons(context, layout)
|
|
layout.prop(self, 'show_e_0')
|
|
|
|
def _process_gcode(self, gcode: str, mesh_name: str, operator=None):
|
|
if not gcode:
|
|
if mesh_name in bpy.data.objects:
|
|
obj = bpy.data.objects[mesh_name]
|
|
if obj.type == 'MESH':
|
|
obj.data.clear_geometry()
|
|
return
|
|
|
|
vertices = []
|
|
edges = []
|
|
e_rates = []
|
|
|
|
scale_length = 1.0
|
|
if bpy.context.scene and bpy.context.scene.unit_settings:
|
|
scale_length = bpy.context.scene.unit_settings.scale_length
|
|
scale_factor = 0.001 / scale_length
|
|
|
|
last_x, last_y, last_z = 0.0, 0.0, 0.0
|
|
last_e = 0.0
|
|
|
|
offset_x, offset_y = 0.0, 0.0
|
|
custom_default_extrusion_rate = 0.0
|
|
default_layer_height = 0.2
|
|
default_layer_width = 0.4
|
|
|
|
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
|
|
if ';' in line:
|
|
comment = line.split(';', 1)[1].strip()
|
|
if comment.startswith("OFFSET_X="):
|
|
try: offset_x = float(comment.split('=')[1])
|
|
except ValueError: pass
|
|
elif comment.startswith("OFFSET_Y="):
|
|
try: offset_y = float(comment.split('=')[1])
|
|
except ValueError: pass
|
|
elif comment.startswith("DEFAULT_EXTRUSION_RATE="):
|
|
try: custom_default_extrusion_rate = float(comment.split('=')[1])
|
|
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()
|
|
if not line_no_comment: continue
|
|
|
|
parts = line_no_comment.split()
|
|
if not parts: continue
|
|
cmd = parts[0].upper()
|
|
|
|
params = {}
|
|
for p in parts[1:]:
|
|
if len(p) < 2: continue
|
|
try:
|
|
# Scale coordinates by scene scale factor (mm to Blender units)
|
|
val = float(p[1:])
|
|
if p[0].upper() in 'XYZ':
|
|
val *= scale_factor
|
|
params[p[0].upper()] = val
|
|
except ValueError: continue
|
|
|
|
new_x = params.get('X', last_x)
|
|
new_y = params.get('Y', last_y)
|
|
new_z = params.get('Z', last_z)
|
|
new_e = params.get('E', last_e)
|
|
|
|
show = False
|
|
if self.show_e_0: show = True
|
|
if cmd == 'G1' and new_e > 0: show = True
|
|
|
|
if show:
|
|
# Subtract offsets (scaled to Blender units) to return to original space
|
|
ox, oy = offset_x * scale_factor, offset_y * scale_factor
|
|
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 * scale_length)
|
|
if seg_len_mm > 0 and custom_default_extrusion_rate > 0:
|
|
e_rate = (new_e / seg_len_mm) / custom_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 vertices:
|
|
return
|
|
|
|
# Create or find "Debug" mesh object
|
|
if mesh_name in bpy.data.objects:
|
|
obj = bpy.data.objects[mesh_name]
|
|
mesh = obj.data
|
|
mesh.clear_geometry()
|
|
else:
|
|
mesh = bpy.data.meshes.new(mesh_name)
|
|
obj = bpy.data.objects.new(mesh_name, mesh)
|
|
assert bpy.context.collection
|
|
bpy.context.collection.objects.link(obj)
|
|
|
|
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
|
|
ng_name = "Nonplanar Slicing Preview"
|
|
if ng_name not in bpy.data.node_groups:
|
|
# Try to load it from nodes.blend in the addon root
|
|
addon_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
|
blend_path = os.path.join(addon_dir, "nodes.blend")
|
|
if os.path.exists(blend_path):
|
|
try:
|
|
with bpy.data.libraries.load(blend_path) as (data_from, data_to):
|
|
if ng_name in data_from.node_groups:
|
|
data_to.node_groups = [ng_name]
|
|
except Exception as e:
|
|
if operator: operator.report({'WARNING'}, f"Failed to load preview node group: {e}")
|
|
|
|
if ng_name in bpy.data.node_groups:
|
|
ng = bpy.data.node_groups[ng_name]
|
|
# Check if already assigned
|
|
mod = None
|
|
for m in obj.modifiers:
|
|
if m.type == 'NODES' and m.node_group == ng: # type: ignore
|
|
mod = m
|
|
break
|
|
|
|
if not mod:
|
|
mod = obj.modifiers.new(name="Preview", type='NODES') # type: ignore
|
|
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):
|
|
gcodes = self.resolve_multi_input("Gcode")
|
|
base_mesh_name: str = self.resolve("Name")
|
|
|
|
if not base_mesh_name:
|
|
base_mesh_name = "GcodePreview"
|
|
|
|
# Filter out empty inputs but keep structure to index properly
|
|
valid_gcodes = [g for g in gcodes if g]
|
|
|
|
if not valid_gcodes:
|
|
if base_mesh_name in bpy.data.objects:
|
|
obj = bpy.data.objects[base_mesh_name]
|
|
if obj.type == 'MESH':
|
|
obj.data.clear_geometry()
|
|
return 'empty'
|
|
|
|
for i, gcode in enumerate(valid_gcodes):
|
|
mesh_name = base_mesh_name if i == 0 else f"{base_mesh_name}_{i+1}"
|
|
try:
|
|
self._process_gcode(gcode, mesh_name, operator)
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
if operator:
|
|
operator.report({'WARNING'}, f"Failed to preview G-code {i+1}: {e}")
|
|
|
|
return 'ok'
|