Files
2026-08-20 06:17:39 +08:00

472 lines
20 KiB
Python

'''
block.py
Copyright (C) 2020 - 2025 Phillip A Carter
Copyright (C) 2020 - 2024 Gregory D Carl
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 2 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.
'''
import os
import sys
import math
import re
import gettext
import linuxcnc
from rs274.glcanon import GlCanonDraw as DRAW
for f in sys.path:
if '/lib/python' in f:
if '/usr' in f:
localeDir = 'usr/share/locale'
else:
localeDir = os.path.join(f'{f.split("/lib")[0]}', 'share', 'locale')
break
gettext.install("linuxcnc", localedir=localeDir)
STAT = linuxcnc.stat()
gcodePattern = re.compile(
r"(;row(?P<OP>.[^X]+) *)?"
r"([Gg] *(?P<G1>\d{1,2}) *)?"
r"([Gg] *(?P<G2>\d{1,2}) *)?"
r"([Xx] *(?P<X>-*\d+\.?\d*) *)?"
r"([Yy] *(?P<Y>-*\d+\.?\d*) *)?"
r"([Zz] *(?P<Z>(-*\d+\.?\d*)|([^;^(]*)) *)?"
r"([Ii] *(?P<I>-*\d+\.?\d*) *)?"
r"([Jj] *(?P<J>-*\d+\.?\d*) *)?"
r"([Mm] *(?P<M>\d{1,3}) *)?"
r"([Pp] *(?P<P>-*\d+\.?\d*) *)?"
r"([Qq] *(?P<Q>-*\d+\.?\d*) *)?"
r"((?P<C>[;(].+) *)?"
)
# Conv is the upstream calling module
def preview(Conv, fNgc, isConvBlock, window, columns, rows, cSpacing, rSpacing,
xOffset, yOffset, angle, scale, rotation, doMirror, doFlip):
''' preview the new block '''
# validate inputs
error = ''
msg1 = _('entry is invalid')
valid, columns = Conv.conv_is_int(columns)
if not valid:
msg0 = _('COLUMNS NUMBER')
error += f'{msg0} {msg1}\n\n'
valid, cSpacing = Conv.conv_is_float(cSpacing)
if not valid and cSpacing:
msg0 = _('COLUMNS SPACING')
error += f'{msg0} {msg1}\n\n'
valid, rows = Conv.conv_is_int(rows)
if not valid:
msg0 = _('ROWS NUMBER')
error += f'{msg0} {msg1}\n\n'
valid, rSpacing = Conv.conv_is_float(rSpacing)
if not valid and rSpacing:
msg0 = _('ROWS SPACING')
error += f'{msg0} {msg1}\n\n'
valid, xOffset = Conv.conv_is_float(xOffset)
if not valid and xOffset:
msg0 = _('X OFFSET')
error += f'{msg0} {msg1}\n\n'
valid, yOffset = Conv.conv_is_float(yOffset)
if not valid and yOffset:
msg0 = _('Y OFFSET')
error += f'{msg0} {msg1}\n\n'
valid, angle = Conv.conv_is_float(angle)
if not valid and angle:
msg0 = _('PATTERN ANGLE')
error += f'{msg0} {msg1}\n\n'
valid, scale = Conv.conv_is_float(scale)
if not valid:
msg0 = _('SHAPE SCALE')
error += f'{msg0} {msg1}\n\n'
valid, rotation = Conv.conv_is_float(rotation)
if not valid and rotation:
msg0 = _('SHAPE ROTATION')
error += f'{msg0} {msg1}\n\n'
if error:
return error
# validate some entry values
if columns <= 0:
msg = _('COLUMNS NUMBER cannot be zero')
error += f'{msg}\n\n'
if rows <= 0:
msg = _('ROWS NUMBER cannot be zero')
error += f'{msg}\n\n'
if columns > 1 and not cSpacing:
msg = _('COLUMNS SPACING is required')
error += f'{msg}\n\n'
if rows > 1 and not rSpacing:
msg = _('ROWS SPACING is required')
error += f'{msg}\n\n'
if scale <= 0:
msg = _('SCALE cannot be zero')
error += f'{msg}\n\n'
if error:
return error
if isConvBlock:
# this is a valid conversational block version:
# get the inputs from the conversational entries
# get current mirror and flip state from the file
# get the gcode from the file
inputsOld, preCode, gCode, postCode = get_v2_code(fNgc)
inputs = [cSpacing, rSpacing, columns, rows, xOffset, yOffset, angle, scale, rotation,
inputsOld[9], inputsOld[10], inputsOld[11], inputsOld[12]]
# files converted from v1 will not have the mid points set yet
# get the midpoints
if inputsOld[11].strip() == '0' and inputsOld[12].strip() == '0':
xMid, yMid = get_midpoints(window)
# need the midpoints of the first shape only
if columns > 1:
xMid -= (columns - 1) * cSpacing / 2
if rows > 1:
yMid -= (rows - 1) * rSpacing / 2
inputs[11] = f"{xMid:0.4f}"
inputs[12] = f"{yMid:0.4f}"
else:
# this is not a conversational block:
# get the inputs from the conversational entries
# set the mirror and flip state to off
# get the gcode from the file
inputs = [cSpacing, rSpacing, columns, rows, xOffset, yOffset, angle, scale, rotation, 1, 1]
preCode, gCode, postCode = get_raw_code(fNgc)
# get the midpoints
xMid, yMid = get_midpoints(window)
inputs.extend([f"{xMid:0.4f}", f"{yMid:0.4f}"])
write_block(fNgc, inputs, preCode, gCode, postCode, doMirror, doFlip)
def write_block(file, inputs, preCode, gCode, postCode, doMirror=False, doFlip=False):
''' write the gcode for the block '''
# write the file
with open(file, 'w') as ngc:
cSpacing = float(inputs[0])
rSpacing = float(inputs[1])
columns = int(inputs[2])
rows = int(inputs[3])
xOffset = float(inputs[4])
yOffset = float(inputs[5])
angle = float(inputs[6]) # pattern angle
scale = float(inputs[7])
rotation = float(inputs[8]) # shape rotation
mirrored = int(inputs[9])
flipped = int(inputs[10])
xMid = float(inputs[11])
yMid = float(inputs[12])
outCode = [] # temporary storage for output gcode
templateWritten = False # write all subsequent output gcode to temporary storage
# set mirror
mirror = -1 if (doMirror and mirrored != -1) or (not doMirror and mirrored == -1) else 1
# set flip
flip = -1 if (doFlip and flipped != -1) or (not doFlip and flipped == -1) else 1
outCode.append('\n;CONV_BLOCK GCODE')
# iterate through the column and row combination
for row in range(rows):
for col in range(columns):
# set start coordinates
xStart, yStart = rotate_point(angle, (col * cSpacing * scale, row * rSpacing * scale))
xStart += xOffset
yStart += yOffset
if templateWritten:
ngc.write(f'\n;row{row} col{col} - origin: X{xStart:0.5f} Y{yStart:0.5f}\n')
else:
outCode.append(f'\n;row{row} col{col} - origin: X{xStart:0.4f} Y{yStart:0.4f}')
gCurrent = None # the current active gcode
xCurrent = 0.0 # the current original x coordinate
yCurrent = 0.0 # the current original y coordinate
previous = {} # the previous original and rotated x and y coordinates
# iterate through the gcode
for line in gCode:
line = line.strip()
# create the code dictionary
code = gcodePattern.match(line).groupdict()
# set the current gcode
if code['G1']:
gCurrent = code['G1'] if code['G1'] in ['00', '01', '02', '03'] else None
elif code['M']:
gCurrent = None
# if rapid, line, or arc
if gCurrent in ['00', '01', '02', '03']:
# reverse arc direction only if one of mirror or flip
if mirror != flip:
if 'G02' in line:
line = line.replace('G02', 'G03')
elif 'G03' in line:
line = line.replace('G03', 'G02')
# set the current x and y coordinates
xCurrent = xCurrent if not code['X'] else float(code['X'])
yCurrent = yCurrent if not code['Y'] else float(code['Y'])
# rotate end point for pattern angle
x, y = rotate_point(angle, (xCurrent * scale * mirror, yCurrent * scale * flip))
# rotate end point for shape rotation
x, y = rotate_point(rotation, (x, y), (xMid, yMid))
# save the calculated x and y points
line=line.replace(f"x{code['X']}", f' X{xStart + x:0.5f}').replace(f"X{code['X']}", f' X{xStart + x:0.5f}')
line=line.replace(f"y{code['Y']}", f' Y{yStart + y:0.5f}').replace(f"Y{code['Y']}", f' Y{yStart + y:0.5f}')
# if this is an arc
if code['I'] or code['J']:
# set new i and j relative coordinates
ir = 0.0 if not code['I'] else float(code['I'])
yr = 0.0 if not code['J'] else float(code['J'])
# set arc absolute center points
xCenter = previous['xOriginal'] + ir
yCenter = previous['yOriginal'] + yr
# rotate arc center point for pattern angle
xCenter, yCenter = rotate_point(angle, (xCenter * scale * mirror, yCenter * scale * flip))
# rotate arc center point for shape rotation
xCenter, yCenter = rotate_point(rotation, (xCenter, yCenter), (xMid, yMid))
# set the relative i and j cordinates
i = xCenter - previous['xRotated']
j = yCenter - previous['yRotated']
# save the calculated i and j coordinates
line=line.replace(f"i{code['I']}", f' I{i:0.5f}').replace(f"I{code['I']}", f' I{i:0.5f}')
line=line.replace(f"j{code['J']}", f' J{j:0.5f}').replace(f"J{code['J']}", f' J{j:0.5f}')
previous['xOriginal'] = xCurrent
previous['yOriginal'] = yCurrent
previous['xRotated'] = x
previous['yRotated'] = y
# write output gcode to file
if templateWritten:
ngc.write(f'{line}\n')
# write output gcode to temporary storage and test if template is ready to be written
else:
if mirror or flip:
outCode.append(line)
inputs[9] = mirror
inputs[10] = flip
# write the header to the file
ngc.write(';conversational block V2\n')
# write the version 2 template to the file
ngc.write(f'\n;CONV_BLOCK TEMPLATE START *****************************\n')
ngc.write(f'\n;CONV_BLOCK INPUTS\n;')
ins = f'{float(inputs[0]):0.4f}'
for i in range(1, len(inputs)):
if i in [2, 3, 9, 10]:
ins += f', {inputs[i]}'
else:
ins += f', {float(inputs[i]):0.4f}'
#ins += f', {inputs[i]}'
ngc.write(f'{ins}')
ngc.write(f'\n\n;CONV_BLOCK PRECODE\n')
for line in preCode:
ngc.write(f';{line.replace(";;", ";")}\n')
ngc.write('\n;CONV_BLOCK GCODE\n')
for line in gCode:
ngc.write(f';{line.replace(";;", ";")}\n')
ngc.write('\n;CONV_BLOCK POSTCODE\n')
for line in postCode:
ngc.write(f';{line.replace(";;", ";")}\n')
ngc.write(f';CONV_BLOCK TEMPLATE END *******************************\n\n')
# write the precode to the file
ngc.write(f'\n;CONV_BLOCK PRECODE\n')
for line in preCode:
ngc.write(f'{line}\n')
for line in outCode:
ngc.write(f'{line}\n')
# write all subsequent output gcode directly to file
templateWritten = True
else:
outCode.append(line)
# write the postcode to the file
ngc.write('\n;CONV_BLOCK POSTCODE\n')
for line in postCode:
ngc.write(f'{line}\n')
ngc.write(';qtplasmac filtered G-code file\n')
return False
def rotate_point(rotation, point, origin=(0, 0)):
''' rotate a point counterclockwise by rotation degrees '''
angle = math.radians(rotation)
xP, yP = point
xO, yO = origin
x = xO + math.cos(angle) * (xP - xO) - math.sin(angle) * (yP - yO)
y = yO + math.sin(angle) * (xP - xO) + math.cos(angle) * (yP - yO)
return (x, y)
def get_midpoints(window):
''' retrive the midpoints of the shape '''
STAT.poll()
mult = 1
if STAT.linear_units == 1:
mult = 25.4
xMid = DRAW.extents_info(window)[0][0] * mult - STAT.g5x_offset[0]
yMid = DRAW.extents_info(window)[0][1] * mult - STAT.g5x_offset[1]
return xMid, yMid
def set_to_upper_case(data):
''' set gcode words to upper case '''
tmp = ''
keep = False
for d in data:
if d in '#(':
keep = True
tmp += d
elif d in '>)':
keep = False
tmp += d
else:
if keep:
tmp += d
else:
tmp += d.upper()
return tmp
def get_raw_code(file):
''' read qtplasmac format file to allow block manipulation
file has been filtered so no preprocessing is required '''
preCode, gCode, postCode, outCode = [], [], [], 'precode'
with open(file, 'r') as inCode:
raw = inCode.read()
pierce = raw.count('M03')
m03 = False
with open(file, 'r') as inCode:
for line in inCode:
line = line.strip()
if 'M03' in line:
m03 = True
if outCode == 'precode':
preCode.append(line)
if '#<_hal[plasmac.cut-feed-rate]>' in line:
outCode = 'gcode'
elif outCode == 'gcode':
# no need to process comments or empty lines
if line and line[0] in ';(':
gCode.append(line)
continue
elif 'M05' in line:
if m03: # or 'postamble' in line:
m03 = False
pierce -= 1
if not pierce:
outCode = 'postcode'
gCode.append(line)
else:
gCode.append(line)
else:
if 'M68' in line:
gCode.append(line)
else:
postCode.append(line)
return(preCode, gCode, postCode)
def get_v2_code(file):
''' read new format conversational block files '''
inputs, preCode, gCode, postCode, outCode = [], [], [], [], None
with open(file, 'r') as inCode:
for line in inCode:
line = line.strip()
if outCode is None:
if ';CONV_BLOCK INPUTS' in line:
outCode = 'inputs'
elif outCode == 'inputs':
if ';CONV_BLOCK PRECODE' in line:
outCode = 'precode'
elif line:
inputs = line[1:].split(', ')
elif outCode == 'precode':
if ';CONV_BLOCK GCODE' in line:
outCode = 'gcode'
elif line:
preCode.append(line[1:])
elif outCode == 'gcode':
if ';CONV_BLOCK POSTCODE' in line:
outCode = 'postCode'
elif line:
gCode.append(line[1:])
elif outCode == 'postCode':
if ';CONV_BLOCK TEMPLATE END' in line:
outCode = 'completed'
else:
postCode.append(line[1:])
return(inputs, preCode, gCode, postCode)
def convert_v1_code(file):
''' convert old format conversational block files '''
inputs, preCode, gCode, postCode, outCode = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [], [], [], None
mirror = '*#<blk_scale>*#<shape_mirror>]'
flip = '*#<blk_scale>*#<shape_flip>]'
with open(file, 'r') as raw:
check = raw.read().upper()
pierces = sum(check.count(x) for x in ('M3', 'M03')) - check.count('M30')
with open(file, 'r') as inCode:
for line in inCode:
# strip leading and trailing whitespace
line = line.strip()
# set to uppercase
line = set_to_upper_case(line)
# add leading 0's to short G & M codes
if len(line) == 2 and line[0] in 'gmGM':
line = f'{line[0].upper()}0{line[1]}'
elif len(line) > 3 and ('g' in line or 'G' in line or 'm' in line or 'M' in line):
tmp = ''
while len(line) > 1:
if line[0] in 'gmGM' and line[1].isdigit() and not line[2].isdigit():
tmp += f'{line[0].upper()}0{line[1]}'
line = line[2:]
else:
tmp += line[0]
line = line[1:]
tmp += line
line = tmp
# get old inputs
if outCode is None:
if line[:6] in ['g10 l2', 'G10 L2']:
outCode = 'precode'
if line.startswith('#<array_x_offset>'):
inputs[0] = line.split("=")[1].strip()
elif line.startswith('#<array_y_offset>'):
inputs[1] = line.split("=")[1].strip()
elif line.startswith('#<array_columns>'):
inputs[2] = line.split('=')[1].strip()
elif line.startswith('#<array_rows>'):
inputs[3] = line.split('=')[1].strip()
elif line.startswith('#<origin_x_offset>'):
inputs[4] = line.split("=")[1].strip()
elif line.startswith('#<origin_y_offset>'):
inputs[5] = line.split("=")[1].strip()
elif line.startswith('#<array_angle>'):
inputs[6] = line.split('=')[1].strip()
elif line.startswith('#<blk_scale>'):
inputs[7] = line.split('=')[1].strip()
elif line.startswith('#<shape_angle>'):
inputs[8] = line.split('=')[1].strip()
elif line.startswith('#<shape_mirror>'):
inputs[9] = line.split('=')[1].strip()
elif line.startswith('#<shape_flip>'):
inputs[10] = line.split('=')[1].strip()
elif outCode == 'precode':
if line[:3] in ['g00', 'G00']:
outCode = 'gcode'
gCode.append(line.replace('[', '').replace(mirror, '').replace(flip, ''))
elif line:
preCode.append(line)
elif outCode == 'gcode':
if line[:3] in ['m05', 'M05']:
pierces -= 1
if not pierces:
outCode = 'postcode'
gCode.append(line)
elif line:
gCode.append(line.replace('[', '').replace(mirror, '').replace(flip, ''))
elif outCode == 'postcode':
if '#<this_col>' in line:
outCode = 'invalid'
postCode.append('M02')
break
elif line[:3] in ['m68', 'M68']:
gCode.append(line)
elif line:
postCode.append(line)
return inputs, preCode, gCode, postCode