Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
# Usage
- Place your part preferably in the middle of your print plate with known center X coordinates
- Place the sliced GCode in the same directory as the Python script
- Set *INPUT_FILE_NAME* to your GCode file name
- Set *LAYER_HEIGHT* to your slicing layer height. Important, because you don't set it correctly you'll get under- or over extrusions
- Set *WARNING_ANGLE* to the maximum angle your system can print at due to clearances
- Define your spline with *SPLINE_X* and *SPLINE_Z*. This array can contain an arbitrary number of points. Make sure the first X-coordinate is in the center of your part. Make sure the last z coordinate is higher or equal the highest z-coordiante in your GCode.
- *SPLINE = CubicSpline(SPLINE_Z, SPLINE_X, bc_type=((1, 0), (1, -np.pi/6)))* defines the spline. You can alter the last pair of of *bc_type* (here *1,-np.pi/6*). This defines the final angle of your spline in RAD.
- Set the options either in config.json and run the script with *python bend_gcode.py --json-config=config.json*
- Or, set the options that you require by command line: *python bend_gcode.py -i myinputfile.gcode ...*
- Use *python bend_gcode.py --help* to learn about the available options.
287 changes: 138 additions & 149 deletions bend_gcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,185 +3,174 @@
Created on Wed Jan 12 10:10:14 2022

@author: stefa
@author: jkneer
"""

import numpy as np
import math
from scipy.interpolate import CubicSpline
import matplotlib.pyplot as plt
import re
from collections import namedtuple

Point2D = namedtuple('Point2D', 'x y')
GCodeLine = namedtuple('GCodeLine', 'x y z e f')


################# USER INPUT PARAMETERS #########################

INPUT_FILE_NAME = "pipe_mk2.gcode"
OUTPUT_FILE_NAME = "BENT_" + INPUT_FILE_NAME
LAYER_HEIGHT = 0.3 #Layer height of the sliced gcode
WARNING_ANGLE = 30 #Maximum Angle printable with your setup

#2-point spline
SPLINE_X = [125, 95]
SPLINE_Z = [0, 140]

#4-point spline example
#SPLINE_X = [150, 156,144,150]
#SPLINE_Z = [0,30,60,90]


SPLINE = CubicSpline(SPLINE_Z, SPLINE_X, bc_type=((1, 0), (1, -np.pi/6))) #define spline with BC-conditions
#SPLINE = CubicSpline(SPLINE_Z, SPLINE_X, bc_type=((1, 0), (1, -0.5235988))) #bent 30°

DISCRETIZATION_LENGTH = 0.01 #discretization length for the spline length lookup table

################# USER INPUT PARAMETERS END #########################
import numpy as np
from scipy.interpolate import CubicSpline, PPoly

import pydantic_cli
from gcodebending import BendingConfig

SplineLookupTable = [0.0]
Point2D = namedtuple('Point2D', 'x y')
GCodeLine = namedtuple('GCodeLine', 'x y z e f')

nx = np.arange(0,SPLINE_Z[-1],1)

xs = np.arange(0,SPLINE_Z[-1],1)
fig, ax = plt.subplots(figsize=(6.5, 4))
ax.plot(SPLINE_X, SPLINE_Z, 'o', label='data')
ax.plot(SPLINE(xs), xs, label="S")
ax.set_xlim(0, 200)
ax.set_ylim(0, 200)
plt.gca().set_aspect('equal', adjustable='box')
# ax.legend(loc='lower left', ncol=2)
plt.show()
def plot_spline(X: tuple, Z: tuple, spline: PPoly) -> None:
"""Plot the spline"""
import matplotlib.pyplot as plt
xs = np.arange(0, Z[-1],1)
_, ax = plt.subplots(figsize=(6.5, 4))
ax.plot(X, Z, 'o', label='data')
ax.plot(spline(xs), xs, label="S")
ax.set_xlim(0, 200)
ax.set_ylim(0, 200)
plt.gca().set_aspect('equal', adjustable='box')
# ax.legend(loc='lower left', ncol=2)
plt.show()


def getNormalPoint(currentPoint: Point2D, derivative: float, distance: float) -> Point2D: #claculates the normal of a point on the spline
angle = np.arctan(derivative) + math.pi /2
def get_normalpoint(currentPoint: Point2D, derivative: float, distance: float) -> Point2D:
"""claculates the normal of a point on the spline"""
angle = np.arctan(derivative) + np.pi /2
return Point2D(currentPoint.x + distance * np.cos(angle), currentPoint.y + distance * np.sin(angle))

def parseGCode(currentLine: str) -> GCodeLine: #parse a G-Code line
def parse_gcode_line(currentLine: str) -> GCodeLine:
"""parse a G-Code line"""
thisLine = re.compile('(?i)^[gG][0-3](?:\s+x(?P<x>-?[0-9.]{1,15})|\s+y(?P<y>-?[0-9.]{1,15})|\s+z(?P<z>-?[0-9.]{1,15})|\s+e(?P<e>-?[0-9.]{1,15})|\s+f(?P<f>-?[0-9.]{1,15}))*')
lineEntries = thisLine.match(currentLine)
if lineEntries:
return GCodeLine(lineEntries.group('x'), lineEntries.group('y'), lineEntries.group('z'), lineEntries.group('e'), lineEntries.group('f'))

def writeLine(G, X, Y, Z, F = None, E = None): #write a line to the output file
def write_line(outputFile, G, X, Y, Z, F = None, E = None):
"""write a line to the output file"""
outputSting = "G" + str(int(G)) + " X" + str(round(X,5)) + " Y" + str(round(Y,5)) + " Z" + str(round(Z,3))
if E is not None:
outputSting = outputSting + " E" + str(round(float(E),5))
if F is not None:
outputSting = outputSting + " F" + str(int(float(F)))
outputFile.write(outputSting + "\n")

"""
# legacy - toooo slow!
def onSplineLength(Zheight) -> float: #calculates a new z height if the spline is followed
return Zheight #for debugging
discretizationLength = 0.01 #Steps taken to find the spline height
currentHeight = 0.00
currentLength = 0.00
while currentLength < Zheight:
currentLength += np.sqrt((SPLINE(currentHeight)-SPLINE(currentHeight+discretizationLength))**2 + discretizationLength**2)
currentHeight += discretizationLength
return currentHeight
"""
# about ~x30 faster than original
def on_spline_length(z:float, x_lookup: np.ndarray, disc_length: float) -> float:
"""Calcuate corrected height"""
res = np.where(x_lookup>=z)
try:
index = res[0][0]
except IndexError:
raise IndexError('Spline not defined high enough')
return index*disc_length

def onSplineLength(Zheight) -> float: #calculates a new z height if the spline is followed
for i in range(len(SplineLookupTable)):
height = SplineLookupTable[i]
if height >= Zheight:
return i * DISCRETIZATION_LENGTH
print("Error! Spline not defined high enough!")

def createSplineLookupTable():
heightSteps = np.arange(DISCRETIZATION_LENGTH, SPLINE_Z[-1], DISCRETIZATION_LENGTH)
for i in range(len(heightSteps)):
height = heightSteps[i]
SplineLookupTable.append(SplineLookupTable[i] + np.sqrt((SPLINE(height)-SPLINE(height-DISCRETIZATION_LENGTH))**2 + DISCRETIZATION_LENGTH**2))

def create_x_lookuptable(Z: tuple, disc_length: float, dx_spline: PPoly) -> np.ndarray:
"""Create a lookup table for sum(dx)"""
height = np.arange(Z[0], Z[-1], disc_length)
dx = dx_spline(height)
tx = np.cumsum( np.sqrt((dx[1:]-dx[:-1])**2 + disc_length**2) )
x = np.insert(tx, 0, 0.0)
return x


lastPosition = Point2D(0, 0)
currentZ = 0.0
lastZ = 0.0
currentLayer = 0
relativeMode = False
createSplineLookupTable()

with open(INPUT_FILE_NAME, "r") as gcodeFile, open(OUTPUT_FILE_NAME, "w+") as outputFile:
for currentLine in gcodeFile:
if currentLine[0] == ";": #if NOT a comment
outputFile.write(currentLine)
continue
if currentLine.find("G91 ") != -1: #filter relative commands
relativeMode = True
outputFile.write(currentLine)
continue
if currentLine.find("G90 ") != -1: #set absolute mode
relativeMode = False
outputFile.write(currentLine)
continue
if relativeMode: #if in relative mode don't do anything
outputFile.write(currentLine)
continue
currentLineCommands = parseGCode(currentLine)
if currentLineCommands is not None: #if current comannd is a valid gcode
if currentLineCommands.z is not None: #if there is a z height in the command
currentZ = float(currentLineCommands.z)

if currentLineCommands.x is None or currentLineCommands.y is None: #if command does not contain x and y movement it#s probably not a print move
if currentLineCommands.z is not None: #if there is only z movement (e.g. z-hop)
outputFile.write("G91\nG1 Z" + str(currentZ-lastZ))
if currentLineCommands.f is not None:
outputFile.write(" F" + str(currentLineCommands.f))
outputFile.write("\nG90\n")
lastZ = currentZ
continue

def main(cfg: BendingConfig):
# cfg = BendingConfig()

dx_spline = CubicSpline(cfg.spline_z, cfg.spline_x, bc_type=((1, 0), (1, cfg.bending_angle)))

if cfg.debug:
plot_spline(cfg.spline_x, cfg.spline_z, dx_spline)

lastPosition = Point2D(0, 0)
currentZ = 0.0
lastZ = 0.0
currentLayer = 0
relativeMode = False
SplineLookupTable = create_x_lookuptable(cfg.spline_z, cfg.discretization_length, dx_spline)


# TODO: this is the prototype of structure to be refactured to the new
# match-statement of python 3.10
with open(cfg.input, "r") as gcodeFile, open(cfg.output, "w+") as outputFile:
for currentLine in gcodeFile:
if currentLine[0] == ";": #if NOT a comment
outputFile.write(currentLine)
continue
currentPosition = Point2D(float(currentLineCommands.x), float(currentLineCommands.y))
midpointX = lastPosition.x + (currentPosition.x - lastPosition.x) / 2 #look for midpoint

distToSpline = midpointX - SPLINE_X[0]

#Correct the z-height if the spline gets followed
correctedZHeight = onSplineLength(currentZ)

angleSplineThisLayer = np.arctan(SPLINE(correctedZHeight, 1)) #inclination angle this layer

angleLastLayer = np.arctan(SPLINE(correctedZHeight - LAYER_HEIGHT, 1)) # inclination angle previous layer

heightDifference = np.sin(angleSplineThisLayer - angleLastLayer) * distToSpline * -1 # layer height difference

transformedGCode = getNormalPoint(Point2D(correctedZHeight, SPLINE(correctedZHeight)), SPLINE(correctedZHeight, 1), currentPosition.x - SPLINE_X[0])

#Check if a move is below Z = 0
if float(transformedGCode.x) <= 0.0:
print("Warning! Movement below build platform. Check your spline!")

#Detect unplausible moves
if transformedGCode.x < 0 or np.abs(transformedGCode.x - currentZ) > 50:
print("Warning! Possibly unplausible move detected on height " + str(currentZ) + " mm!")
if currentLine.find("G91 ") != -1: #filter relative commands
relativeMode = True
outputFile.write(currentLine)
continue
#Check for self intersection
if (LAYER_HEIGHT + heightDifference) < 0:
print("ERROR! Self intersection on height " + str(currentZ) + " mm! Check your spline!")
continue
if currentLine.find("G90 ") != -1: #set absolute mode
relativeMode = False
outputFile.write(currentLine)
continue
if relativeMode: #if in relative mode don't do anything
outputFile.write(currentLine)
continue
currentLineCommands = parse_gcode_line(currentLine)
if currentLineCommands is not None: #if current comannd is a valid gcode
if currentLineCommands.z is not None: #if there is a z height in the command
currentZ = float(currentLineCommands.z)

if currentLineCommands.x is None or currentLineCommands.y is None: #if command does not contain x and y movement it#s probably not a print move
if currentLineCommands.z is not None: #if there is only z movement (e.g. z-hop)
outputFile.write("G91\nG1 Z" + str(currentZ-lastZ))
if currentLineCommands.f is not None:
outputFile.write(" F" + str(currentLineCommands.f))
outputFile.write("\nG90\n")
lastZ = currentZ
continue
outputFile.write(currentLine)
continue
currentPosition = Point2D(float(currentLineCommands.x), float(currentLineCommands.y))
midpointX = lastPosition.x + (currentPosition.x - lastPosition.x) / 2 #look for midpoint

distToSpline = midpointX - cfg.spline_x[0]

#Correct the z-height if the spline gets followed
#correctedZHeight = onSplineLength(currentZ)
correctedZHeight = on_spline_length(currentZ, SplineLookupTable, cfg.discretization_length)

angleSplineThisLayer = np.arctan(dx_spline(correctedZHeight, 1)) #inclination angle this layer

angleLastLayer = np.arctan(dx_spline(correctedZHeight - cfg.layer_height, 1)) # inclination angle previous layer

heightDifference = np.sin(angleSplineThisLayer - angleLastLayer) * distToSpline * -1 # layer height difference

#Check the angle of the printed layer and warn if it's above the machine limit
if angleSplineThisLayer > (WARNING_ANGLE * np.pi / 180.):
print("Warning! Spline angle is", (angleSplineThisLayer * 180. / np.pi), "at height ", str(currentZ), " mm! Check your spline!")

if currentLineCommands.e is not None: #if this is a line with extrusion
"""if float(currentLineCommands.e) < 0.0:
print("Retraction")"""
extrusionAmount = float(currentLineCommands.e) * ((LAYER_HEIGHT + heightDifference)/LAYER_HEIGHT)
#outputFile.write(";was" + currentLineCommands.e + " is" + str(extrusionAmount) + " diff" + str(int(((LAYER_HEIGHT + heightDifference)/LAYER_HEIGHT)*100)) + "\n")
transformedGCode = get_normalpoint(Point2D(correctedZHeight, dx_spline(correctedZHeight)), dx_spline(correctedZHeight, 1), currentPosition.x - cfg.spline_x[0])

#Check if a move is below Z = 0
if float(transformedGCode.x) <= 0.0:
print("Warning! Movement below build platform. Check your spline!")

#Detect unplausible moves
if transformedGCode.x < 0 or np.abs(transformedGCode.x - currentZ) > 50:
print("Warning! Possibly unplausible move detected on height " + str(currentZ) + " mm!")
outputFile.write(currentLine)
continue
#Check for self intersection
if (cfg.layer_height + heightDifference) < 0:
print("ERROR! Self intersection on height " + str(currentZ) + " mm! Check your spline!")

#Check the angle of the printed layer and warn if it's above the machine limit
if angleSplineThisLayer > (cfg.warning_angle * np.pi / 180.):
print("Warning! Spline angle is", (angleSplineThisLayer * 180. / np.pi), "at height ", str(currentZ), " mm! Check your spline!")

if currentLineCommands.e is not None: #if this is a line with extrusion
"""if float(currentLineCommands.e) < 0.0:
print("Retraction")"""
extrusionAmount = float(currentLineCommands.e) * ((cfg.layer_height + heightDifference)/cfg.layer_height)
#outputFile.write(";was" + currentLineCommands.e + " is" + str(extrusionAmount) + " diff" + str(int(((LAYER_HEIGHT + heightDifference)/LAYER_HEIGHT)*100)) + "\n")
else:
extrusionAmount = None
write_line(outputFile, 1,transformedGCode.y, currentPosition.y, transformedGCode.x, None, extrusionAmount)
lastPosition = currentPosition
lastZ = currentZ
else:
extrusionAmount = None
writeLine(1,transformedGCode.y, currentPosition.y, transformedGCode.x, None, extrusionAmount)
lastPosition = currentPosition
lastZ = currentZ
else:
outputFile.write(currentLine)
print("GCode bending finished!")
outputFile.write(currentLine)
print("GCode bending finished!")


if __name__ == '__main__':
pydantic_cli.run_and_exit(BendingConfig, main, 'Bend GCode')
12 changes: 12 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"input": "tests/cylinder-vase.gcode",
"output": "tests/bent-cylinder-vase.gcode",

"layer_height": 0.3,
"warning_angle": 30.0,

"spline_x": [125, 95],
"spline_z": [0, 140],
"bending_angle": -0.52359877559,
"discretization_length": 0.01
}
1 change: 1 addition & 0 deletions gcodebending/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . config import BendingConfig
Loading