blendercam/scripts/addons/cam/gcodeimportparser.py

516 wiersze
19 KiB
Python

#!/usr/bin/env python
# https://github.com/jonathanwin/yagv with no licence
# code modified from YAGV -yet another gcode viewer
# will assume release GNU release
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ***** END GPL LICENCE BLOCK *****
import bpy, bmesh
import math
import re
import numpy as np
np.set_printoptions(suppress=True) # suppress scientific notation in subdivide functions linspace
def import_gcode(context, filepath):
print("running read_some_data...")
scene = context.scene
mytool = scene.cam_import_gcode
import time
then = time.time()
parse = GcodeParser()
model = parse.parseFile(filepath)
if mytool.subdivide:
model.subdivide(mytool.max_segment_size)
model.classifySegments()
if mytool.split_layers:
model.draw(split_layers=True)
else:
model.draw(split_layers=False)
now = time.time()
print("importing Gcode took ", round(now - then, 1), "seconds")
return {'FINISHED'}
def segments_to_meshdata(segments): # edges only on extrusion
segs = segments
verts = []
edges = []
del_offset = 0 # to travel segs in a row, one gets deleted, need to keep track of index for edges
for i in range(len(segs)):
if i >= len(segs) - 1:
if segs[i].style == 'extrude':
verts.append([segs[i].coords['X'], segs[i].coords['Y'], segs[i].coords['Z']])
break
# start of extrusion for first time
if segs[i].style == 'travel' and segs[i + 1].style == 'extrude':
verts.append([segs[i].coords['X'], segs[i].coords['Y'], segs[i].coords['Z']])
verts.append([segs[i + 1].coords['X'], segs[i + 1].coords['Y'], segs[i + 1].coords['Z']])
edges.append([i - del_offset, (i - del_offset) + 1])
# mitte, current and next are extrusion, only add next, current is already in vert list
if segs[i].style == 'extrude' and segs[i + 1].style == 'extrude':
verts.append([segs[i + 1].coords['X'], segs[i + 1].coords['Y'], segs[i + 1].coords['Z']])
edges.append([i - del_offset, (i - del_offset) + 1])
if segs[i].style == 'travel' and segs[i + 1].style == 'travel':
del_offset += 1
return verts, edges
def obj_from_pydata(name, verts, edges=None, close=True, collection_name=None):
if edges is None:
# join vertices into one uninterrupted chain of edges.
edges = [[i, i + 1] for i in range(len(verts) - 1)]
if close:
edges.append([len(verts) - 1, 0]) # connect last to first
me = bpy.data.meshes.new(name)
me.from_pydata(verts, edges, [])
obj = bpy.data.objects.new(name, me)
# Move into collection if specified
if collection_name is not None: # make argument optional
# collection exists
collection = bpy.data.collections.get(collection_name)
if collection:
bpy.data.collections[collection_name].objects.link(obj)
else:
collection = bpy.data.collections.new(collection_name)
bpy.context.scene.collection.children.link(collection) # link collection to main scene
bpy.data.collections[collection_name].objects.link(obj)
obj.scale = (0.001, 0.001, 0.001)
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
if bpy.context.scene.cam_import_gcode.output == 'curve':
bpy.ops.object.convert(target='CURVE')
class GcodeParser:
comment = ""
# global, to access in other classes(to access RGB values in comment above when parsing M163).
# Theres probably better way
def __init__(self):
self.model = GcodeModel(self)
def parseFile(self, path):
# read the gcode file
with open(path, 'r') as f:
# init line counter
self.lineNb = 0
# for all lines
for line in f:
# inc line counter
self.lineNb += 1
# remove trailing linefeed
self.line = line.rstrip()
# parse a line
self.parseLine()
return self.model
def parseLine(self):
# strip comments:
bits = self.line.split(';', 1)
if (len(bits) > 1):
GcodeParser.comment = bits[1]
# extract & clean command
command = bits[0].strip()
s = ""
a = ""
a_old = ""
for i in range(len(command)): # check each character in the line
a = command[i]
if a.isupper() and a_old != ' ' and i > 0: # add a space if upper case letter and no space is found before
s += ' '
s += a
a_old = a
print(s)
command = s
# code is fist word, then args
comm = command.split(None, 1)
code = comm[0] if (len(comm) > 0) else None
args = comm[1] if (len(comm) > 1) else None
if code:
# convert all G01 and G00 to G1 and G0
if code == 'G01':
code = 'G1'
if code == 'G00':
code = 'G0'
if hasattr(self, "parse_" + code):
getattr(self, "parse_" + code)(args)
self.last_command = code
else:
if code[0] == "T":
self.model.toolnumber = int(code[1:])
print(self.model.toolnumber)
# if code doesn't start with a G but starts with a coordinate add the last command to the line
elif code[0] == 'X' or code[0] == 'Y' or code[0] == 'Z':
self.line = self.last_command + ' ' + self.line
self.parseLine() # parse this line again with the corrections
else:
pass
print("Unsupported gcode " + str(code))
def parseArgs(self, args):
dic = {}
if args:
bits = args.split()
for bit in bits:
letter = bit[0]
try:
coord = float(bit[1:])
except ValueError:
coord = 1
dic[letter] = coord
return dic
def parse_G1(self, args, type="G1"):
# G1: Controlled move
self.model.do_G1(self.parseArgs(args), type)
def parse_G0(self, args, type="G0"):
# G1: Controlled move
self.model.do_G1(self.parseArgs(args), type)
def parse_G90(self, args):
# G90: Set to Absolute Positioning
self.model.setRelative(False)
def parse_G91(self, args):
# G91: Set to Relative Positioning
self.model.setRelative(True)
def parse_G92(self, args):
# G92: Set Position
self.model.do_G92(self.parseArgs(args))
def warn(self, msg):
print("[WARN] Line %d: %s (Text:'%s')" % (self.lineNb, msg, self.line))
def error(self, msg):
print("[ERROR] Line %d: %s (Text:'%s')" % (self.lineNb, msg, self.line))
raise Exception("[ERROR] Line %d: %s (Text:'%s')" % (self.lineNb, msg, self.line))
class GcodeModel:
def __init__(self, parser):
# save parser for messages
self.parser = parser
# latest coordinates & extrusion relative to offset, feedrate
self.relative = {
"X": 0.0,
"Y": 0.0,
"Z": 0.0,
"F": 0.0,
"E": 0.0}
# offsets for relative coordinates and position reset (G92)
self.offset = {
"X": 0.0,
"Y": 0.0,
"Z": 0.0,
"E": 0.0}
# if true, args for move (G1) are given relatively (default: absolute)
self.isRelative = False
self.color = [0, 0, 0, 0, 0, 0, 0, 0] # RGBCMYKW
self.toolnumber = 0
# the segments
self.segments = []
self.layers = []
def do_G1(self, args, type):
# G0/G1: Rapid/Controlled move
# clone previous coords
coords = dict(self.relative)
# update changed coords
for axis in args.keys():
# print(coords)
if axis in coords:
if self.isRelative:
coords[axis] += args[axis]
else:
coords[axis] = args[axis]
else:
self.warn("Unknown axis '%s'" % axis)
# build segment
absolute = {
"X": self.offset["X"] + coords["X"],
"Y": self.offset["Y"] + coords["Y"],
"Z": self.offset["Z"] + coords["Z"],
"F": coords["F"] # no feedrate offset
}
# if gcode line has no E = travel move
# but still add E = 0 to segment (so coords dictionaries have same shape for subdividing linspace function)
if "E" not in args: # "E" in coords:
absolute["E"] = 0
else:
absolute["E"] = args["E"]
seg = Segment(
type,
absolute,
self.color,
self.toolnumber,
# self.layerIdx,
self.parser.lineNb,
self.parser.line)
# only add seg if XYZ changes (skips "G1 Fxxx" only lines and avoids double vertices inside Blender,
# because XYZ stays the same on such a segment.
if seg.coords['X'] != self.relative['X'] + self.offset["X"] or seg.coords['Y'] != self.relative['Y'] + \
self.offset["Y"] or seg.coords['Z'] != self.relative['Z'] + self.offset["Z"]:
self.addSegment(seg)
# update model coords
self.relative = coords
def do_G92(self, args):
# G92: Set Position
# this changes the current coords, without moving, so do not generate a segment
# no axes mentioned == all axes to 0
if not len(args.keys()):
args = {"X": 0.0, "Y": 0.0, "Z": 0.0} # , "E":0.0
# update specified axes
for axis in args.keys():
if axis in self.offset:
# transfer value from relative to offset
self.offset[axis] += self.relative[axis] - args[axis]
self.relative[axis] = args[axis]
else:
self.warn("Unknown axis '%s'" % axis)
def do_M163(self, args):
col = list(
self.color) # list() creates new list, otherwise you just change reference and all segs have same color
extr_idx = int(args['S']) # e.g. M163 S0 P1
weight = args['P']
# change CMYKW
col[extr_idx + 3] = weight # +3 weil ersten 3 stellen RGB sind, need only CMYKW values for extrude
self.color = col
# take RGB values for seg from last comment (above first M163 statement)
comment = eval(GcodeParser.comment) # string comment to list
# RGB = [GcodeParser.comment[1], GcodeParser.com
RGB = comment[:3]
self.color[:3] = RGB
def setRelative(self, isRelative):
self.isRelative = isRelative
def addSegment(self, segment):
self.segments.append(segment)
def warn(self, msg):
self.parser.warn(msg)
def error(self, msg):
self.parser.error(msg)
def classifySegments(self):
# start model at 0, act as prev_coords
coords = {
"X": 0.0,
"Y": 0.0,
"Z": 0.0,
"F": 0.0,
"E": 0.0}
# first layer at Z=0
currentLayerIdx = 0
currentLayerZ = 0 # better to use self.first_layer_height
layer = [] # add layer to model.layers
for i, seg in enumerate(self.segments):
# default style is travel (move, no extrusion)
style = "travel"
# no horizontal movement, but extruder movement: retraction/refill
# if (
# (seg.coords["X"] == coords["X"]) and
# (seg.coords["Y"] == coords["Y"]) and
# (seg.coords["Z"] == coords["Z"]) and
# (seg.coords["E"] != coords["E"]) ):
# style = "retract" if (seg.coords["E"] < coords["E"]) else "restore"
# some horizontal movement, and positive extruder movement: extrusion
if (
((seg.coords["X"] != coords["X"]) or (seg.coords["Y"] != coords["Y"]) or (
seg.coords["Z"] != coords["Z"]))): # != coords["E"]
style = "extrude"
# #force extrude if there is some movement
# segments to layer lists
# look ahead and if next seg has E and differenz Z, add new layer for current segment
if i == len(self.segments) - 1:
layer.append(seg)
currentLayerIdx += 1
seg.style = style
seg.layerIdx = currentLayerIdx
self.layers.append(layer) # add layer to list of Layers, used to later draw single layer objects
break
# positive extruder movement of next point in a different Z signals a layer change for this segment
if self.segments[i].coords["Z"] != currentLayerZ and self.segments[i + 1].coords["E"] > 0:
self.layers.append(
layer) # layer abschließen, add layer to list of Layers, used to later draw single layer objects
layer = [] # start new layer
currentLayerZ = seg.coords["Z"]
currentLayerIdx += 1
# lookback, previous point before texrsuion is part of new layer too, both create an edge
# set style and layer in segment
seg.style = style
seg.layerIdx = currentLayerIdx
layer.append(seg)
coords = seg.coords
def subdivide(self, subd_threshold):
# smart subdivide
# divide edge if > subd_threshold
# do it in parser to keep index order of vertex and travel/extrude info
# segmentation of path necessary for manipulation of color, continous deforming ect.
subdivided_segs = []
# start model at 0
coords = {
"X": 0.0,
"Y": 0.0,
"Z": 0.0,
"F": 0.0, # no interpolation
"E": 0.0}
for seg in self.segments:
# calc XYZ distance
d = (seg.coords["X"] - coords["X"]) ** 2
d += (seg.coords["Y"] - coords["Y"]) ** 2
d += (seg.coords["Z"] - coords["Z"]) ** 2
seg.distance = math.sqrt(d)
if seg.distance > subd_threshold:
subdivs = math.ceil(
seg.distance / subd_threshold) # ceil makes sure that linspace interval is at least 2
P1 = coords
P2 = seg.coords
# interpolated points
interp_coords = np.linspace(list(P1.values()), list(P2.values()), num=subdivs, endpoint=True)
for i in range(len(interp_coords)): # inteprolated points array back to segment object
new_coords = {"X": interp_coords[i][0], "Y": interp_coords[i][1], "Z": interp_coords[i][2],
"F": seg.coords["F"]}
# E/subdivs is for relative extrusion, absolute extrusion need "E":interp_coords[i][4]
# print("interp_coords_new:", new_coords)
if seg.coords["E"] > 0:
new_coords["E"] = round(seg.coords["E"] / (subdivs - 1), 5)
else:
new_coords["E"] = 0
# make sure P1 hasn't been written before, compare with previous line
if new_coords['X'] != coords['X'] or new_coords['Y'] != coords['Y'] or new_coords['Z'] != \
coords['Z']:
# write segment only if movement changes,
# avoid double coordinates due to same start and endpoint of linspace
new_seg = Segment(seg.type, new_coords, seg.color, seg.toolnumber, seg.lineNb, seg.line)
new_seg.layerIdx = seg.layerIdx
new_seg.style = seg.style
subdivided_segs.append(new_seg)
else:
subdivided_segs.append(seg)
coords = seg.coords # P1 becomes P2
self.segments = subdivided_segs
# create blender curve and vertex_info in text file(coords, style, color...)
def draw(self, split_layers=False):
if split_layers:
i = 0
for layer in self.layers:
verts, edges = segments_to_meshdata(layer)
if len(verts) > 0:
obj_from_pydata(str(i), verts, edges, close=False, collection_name="Layers")
i += 1
else:
verts, edges = segments_to_meshdata(self.segments)
obj_from_pydata("Gcode", verts, edges, close=False, collection_name="Layers")
class Segment:
def __init__(self, type, coords, color, toolnumber, lineNb, line):
self.type = type
self.coords = coords
self.color = color
self.toolnumber = toolnumber
self.lineNb = lineNb
self.line = line
self.style = None
self.layerIdx = None
def __str__(self):
return " <coords=%s, lineNb=%d, style=%s, layerIdx=%d, color=%s" % \
(str(self.coords), self.lineNb, self.style, self.layerIdx, str(self.color))
class Layer:
def __init__(self, Z):
self.Z = Z
self.segments = []
self.distance = None
self.extrudate = None
def __str__(self):
return "<Layer: Z=%f, len(segments)=%d>" % (self.Z, len(self.segments))
if __name__ == '__main__':
path = "test.gcode"
parser = GcodeParser()
model = parser.parseFile(path)