#!/usr/bin/python
"""
    textogcode is a G-Code Engraving Generator for text file 
    (C) cadenote@hotmail.com <2019> 
    syntax --- see helpfile below

    Allows the generation of multiple lines of engraved text provided in a text file
    based upon code from:
    engrave-lines.py G-Code Engraving Generator for command-line 
    (C) ArcEye <2012>  <arceye at mgware dot co dot uk>
    and f-engrave G-Code Generator Copyright (C) <2017>  <Scorch>
    that are based upon code from engrave-11.py
    Copyright (C) <2008>  <Lawrence Glaister> <ve7it at shaw dot ca>
                     based on work by John Thornton  -- GUI framwork from arcbuddy.py
                     Ben Lipkowitz  (fenn)-- cxf2cnc.py v0.5 font parsing code

    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, see <http://www.gnu.org/licenses/>.
    Rev v1.0 cadenote<2019>
"""
# change this if you want to use another font
fontfile = "/usr/share/qcad/fonts/normallatin1.cxf"
#fontfile = "/usr/share/qcad/fonts/mi_freemono.cxf"

from Tkinter import *
from math import *
import os
import re
import sys
import string
import getopt

String =   ""
SafeZ =    2
XStart =   0
XIndentList = ""
YStart = 0
YLineOffset = 0
Depth =    0.1
XScale =   1
YScale =   1
CSpaceP =  25
WSpaceP=   100
Angle =    0
Mirror = 0
Flip = 0
#Preamble = u"G17 G21 G40 G90 G64 P0.003 F100"
Preamble = u"G17 G21 G40 G90 F400"
Postamble = u"M2"

stringlist = []
#=======================================================================

#=======================================================================
class Character:
    def __init__(self, key):
        self.key = key
        self.stroke_list = []

    def __repr__(self):
        return "%s" % (self.stroke_list)

    def get_xmax(self):
        try: return max([s.xmax for s in self.stroke_list[:]])
        except ValueError: return 0

    def get_ymax(self):
        try: return max([s.ymax for s in self.stroke_list[:]])
        except ValueError: return 0



#=======================================================================
class Line:

    def __init__(self, coords):
        self.xstart, self.ystart, self.xend, self.yend = coords
        self.xmax = max(self.xstart, self.xend)
        self.ymax = max(self.ystart, self.yend)

    def __repr__(self):
        return "Line([%s, %s, %s, %s])" % (self.xstart, self.ystart, self.xend, self.yend)



#cambiada con parse (f-engrave)
################################################################################
# This routine parses the .cxf font file and builds a font dictionary of       #
# line segment strokes required to cut each character.                         #
# Arcs (only used in some fonts) are converted to a number of line             #
# segments based on the angular length of the arc. Since the idea of           #
# this font description is to make it support independent x and y scaling,     #
# we do not use native arcs in the g-code.                                      #
################################################################################
def parse(file,segarc):
    font = {}
    key = None
    stroke_list = []
    xmax, ymax = 0, 0
    for text_in in file:
        text = text_in+" "
        # format for a typical letter (lower-case r):
        # #comment, with a blank line after it
        #
        # [r] 3  (or "[0072] r" where 0072 is the HEX value of the character)
        # L 0,0,0,6
        # L 0,6,2,6
        # A 2,5,1,0,90
        #
        end_char = len(text)
        if end_char and key: #save the character to our dictionary
            font[key] = Character(key)
            font[key].stroke_list = stroke_list
            font[key].xmax = xmax

        new_cmd = re.match('^\[(.*)\]\s', text)
        if new_cmd: #new character
            key_tmp = new_cmd.group(1)
            if len(new_cmd.group(1)) == 1:
                key = ord(key_tmp)
            else:
                if len(key_tmp) == 5:
                    key_tmp = key_tmp[1:]
                if len(key_tmp) == 4:
                    try:
                        key=int(key_tmp,16)
                    except:
                        key = None
                        stroke_list = []
                        xmax, ymax = 0, 0
                        continue
                else:
                    key = None
                    stroke_list = []
                    xmax, ymax = 0, 0
                    continue
            stroke_list = []
            xmax, ymax = 0, 0

        line_cmd = re.match('^L (.*)', text)
        if line_cmd:
            coords = line_cmd.group(1)
            coords = [float(n) for n in coords.split(',')]
            stroke_list += [Line(coords)]
            xmax = max(xmax, coords[0], coords[2])

        arc_cmd = re.match('^A (.*)', text)
        if arc_cmd:
            coords = arc_cmd.group(1)
            coords = [float(n) for n in coords.split(',')]
            xcenter, ycenter, radius, start_angle, end_angle = coords

            # since font defn has arcs as ccw, we need some font foo
            if ( end_angle < start_angle ):
                start_angle -= 360.0

            # approximate arc with line seg every "segarc" degrees
            segs = int((end_angle - start_angle) / segarc)+1
            angleincr = (end_angle - start_angle)/segs
            xstart = cos( radians(start_angle) ) * radius + xcenter
            ystart = sin( radians(start_angle) ) * radius + ycenter
            angle = start_angle
            for i in range(segs):
                angle += angleincr
                xend = cos( radians(angle) ) * radius + xcenter
                yend = sin( radians(angle) ) * radius + ycenter
                coords = [xstart,ystart,xend,yend]
                stroke_list += [Line(coords)]
                xmax = max(xmax, coords[0], coords[2])
                ymax = max(ymax, coords[1], coords[3])
                xstart = xend
                ystart = yend
    return font

#=======================================================================

def __init__(key):
    key = key
    stroke_list = []

def __repr__():
    return "%s" % (stroke_list)

def get_xmax():
    try: return max([s.xmax for s in stroke_list[:]])
    except ValueError: return 0

def get_ymax():
    try: return max([s.ymax for s in stroke_list[:]])
    except ValueError: return 0



#=======================================================================


def __init__( coords):
    xstart, ystart, xend, yend = coords
    xmax = max(xstart, xend)
    ymax = max(ystart, yend)

def __repr__():
    return "Line([%s, %s, %s, %s])" % (xstart, ystart, xend, yend)


#=======================================================================
def sanitize(string):
    retval = ''
    good=' ~!@#$%^&*_+=-{}[]|\:;"<>,./?'
    for char in string:
        if char.isalnum() or good.find(char) != -1:
            retval += char
        else: retval += ( ' 0x%02X ' %ord(char))
    return retval

#=======================================================================
# routine takes an x and a y in raw internal format
# x and y scales are applied and then x,y pt is rotated by angle
# Returns new x,y tuple
def Rotn(x,y,xscale,yscale,angle):
    Deg2Rad = 2.0 * pi / 360.0
    xx = x * xscale
    yy = y * yscale
    rad = sqrt(xx * xx + yy * yy)
    theta = atan2(yy,xx)
    newx=rad * cos(theta + angle*Deg2Rad)
    newy=rad * sin(theta + angle*Deg2Rad)
    return newx,newy
#======================================================================0
def Rutina(x,y):
    v28= x*XScale
    v29= y*YScale
    v30= sqrt(v28*v28+v29*v29)
    v31= atan2(v29,v28)
    v32= v30*cos(v31+radians(Angle))
    v33= v30*sin(v31+radians(Angle))
    rutx= v32+XX
    ruty= v33+YY
    return rutx,ruty
#=======================================================================

def code(arg, visit, last):

    global SafeZ
    global XStart
    global XX
    global XLineOffRC
    global XIndentList
    global YStart
    global YY
    global YLineOffset
    global YLineOffRC
    global Depth
    global XScale
    global YScale
    global CSpaceP
    global WSpaceP
    global Angle
    global Mirror
    global Flip
    global Ocode
    global Preamble
    global Postamble
    global stringlist

    String = arg
    try:
    	miString= String.decode('utf-8')
    except:
    	miString =String

    str1 = ""
    #erase old gcode as needed
    gcode = []
    
    file = open(fontfile)
  
    oldx = oldy = -99990.0      
    XX = XStart - (XLineOffRC * visit)
    YY = YStart - (YLineOffRC * visit)           
    if visit != 0:
        # all we need is new X and Y for subsequent lines
        gcode.append(u"(===================================================================)")
        gcode.append(u'( Line %d )' %(visit))
        gcode.append(u'( Grabando: %s" )' % miString )
	if Ocode:
	       	gcode.append(u'#1002 = %.4f  ( X Start )' % XX)
	       	gcode.append(u'#1003 = %.4f  ( Y Start )' % YY)
        gcode.append(u"(===================================================================)")
        
    else:
        gcode.append(u'( Code generated by textogcode )')
        gcode.append(u'( by cadenote@hotmail.com <2019> )')
        gcode.append(u'( Fontfile: %s )' %(fontfile))
        # write out subroutine for rotation logic just once at head
	if Ocode:
		gcode.append(u"(===================================================================)")
		gcode.append(u"(Subroutine to handle x,y rotation about 0,0)")
		gcode.append(u"(input x,y get scaled, rotated then offset )")
		gcode.append(u"( [#1 = 0 or 1 for a G0 or G1 type of move], [#2=x], [#3=y])")
		gcode.append(u"o9000 sub")
		if debug:
			gcode.append(u"(print, entro x,y: [#2] , [#3])")
		gcode.append(u"  #28 = [#2 * #1004]  ( scaled x )")
		gcode.append(u"  #29 = [#3 * #1005]  ( scaled y )")
		gcode.append(u"  #30 = [SQRT[#28 * #28 + #29 * #29 ]]   ( dist from 0 to x,y )")
		gcode.append(u"  #31 = [ATAN[#29]/[#28]]                ( direction to  x,y )")
		if debug:
			gcode.append(u"(print, direccion a x,y: #31 distancia #30)")
		gcode.append(u"  #32 = [#30 * cos[#31 + #1006]]     ( rotated x )")
		gcode.append(u"  #33 = [#30 * sin[#31 + #1006]]     ( rotated y )")
		gcode.append(u"  #34= [#32+#1002] #35=[#33+#1003]")
		gcode.append(u"  o9010 if [#1 LT 0.5]" )
		gcode.append(u"    G00 X[#34] Y[#35]")
		gcode.append(u"  o9010 else")
		gcode.append(u"    G01 X[#34] Y[#35]")
		gcode.append(u"  o9010 endif")
		if debug:
			gcode.append(u"(print, obtengo rotx #32 roty #33  x:[#34] y:[#35])")
		gcode.append(u"o9000 endsub")
		gcode.append(u"(===================================================================)")
    
        	gcode.append(u"#1000 = %.4f" %(SafeZ))
        	gcode.append(u'#1001 = %.4f  ( Engraving Depth Z )' %(Depth))
              
        	gcode.append(u'#1002 = %.4f  ( X Start )' %(XX))
        	gcode.append(u'#1003 = %.4f  ( Y Start )' %(YY))
        	gcode.append(u'#1004 = %.4f  ( X Scale )' %(XScale))
        	gcode.append(u'#1005 = %.4f  ( Y Scale )' %(YScale))
        	gcode.append(u'#1006 = %.4f  ( Angle )' %(Angle))
        gcode.append(Preamble)
        gcode.append(u'( Line %d )' %(visit))
        gcode.append(u'( Engraving: "%s")' %(miString) )
        
    	if Ocode:    	
		gcode.append( u'G0 Z#1000')
	else:
		gcode.append( u"G0 Z%.4f" % SafeZ)

#llamo con arcseg 20
    font = parse(file,20)          # build stroke lists from font file
    file.close()

    font_line_height = max(font[key].get_ymax() for key in font)
    font_word_space =  max(font[key].get_xmax() for key in font) * (WSpaceP/100.0)
    font_char_space = font_word_space * (CSpaceP /100.0)

    xoffset = 0                 # distance along raw string in font units

    for char in miString:
        if char == ' ':
            xoffset += font_word_space
            continue
	gcode.append (u"(cogido= %s)" % char)
	micaracter=ord(char)
        try:
            gcode.append(u"(y sanitizado es'%s y en hex_font= #%04X')" % (sanitize(char), micaracter))
	    # empieza el trazo
            first_stroke = True 
            for stroke in font[micaracter].stroke_list:
#               gcode.append("(%f,%f to %f,%f)" %(stroke.xstart,stroke.ystart,stroke.xend,stroke.yend ))
                dx = oldx - stroke.xstart
                dy = oldy - stroke.ystart
                dist = sqrt(dx*dx + dy*dy)

                x1 = stroke.xstart + xoffset
                y1 = stroke.ystart
                if Mirror == 1:
                    x1 = -x1
                if Flip == 1:
                    y1 = -y1
		X1,Y1 = Rutina (x1,y1)
		if debug:
			gcode.append(u"(envio x1=%.4f y1=%.4f recibo X1=%.4f Y1=%.4f')" % (x1,y1,X1,Y1))
                # check and see if we need to move to a new discontinuous start point
                if (dist > 0.001) or first_stroke:
                    first_stroke = False
		    if Ocode:
                    	#lift engraver, rapid to start of stroke, drop tool
                    	gcode.append(u"G0 Z#1000")
                    	gcode.append(u'o9000 call [0] [%.4f] [%.4f]' %(x1,y1))
                    	gcode.append(u"G1 Z#1001")
		    else:
			gcode.append(u"G0 Z%.4f" % SafeZ)
			gcode.append(u"G0 X%.4f Y%.4f" % (X1,Y1))
			gcode.append(u"G1 Z%.4f" % Depth)
                x2 = stroke.xend + xoffset
                y2 = stroke.yend
                if Mirror == 1:
                    x2 = -x2
                if Flip == 1:
                    y2 = -y2
		X2,Y2 = Rutina (x2,y2)
		if debug:
			gcode.append(u"(envio x2=%.4f y2=%.4f recibo X2=%.4f Y2=%.4f')" % (x2,y2,X2,Y2))
		if Ocode:
                	gcode.append(u'o9000 call [1] [%.4f] [%.4f]' %(x2,y2))
		else:
			gcode.append(u"G1 X%.4f Y%.4f" % (X2,Y2))

                oldx, oldy = stroke.xend, stroke.yend

            # move over for next character
            char_width = font[micaracter].get_xmax()
            xoffset += font_char_space + char_width

        except KeyError:
           gcode.append(u"(warning: character '0x%02X' not found in font defn)" % micaracter)

        gcode.append(u"")       # blank line after every char block
    if Ocode:
	gcode.append( u'G0 Z#1000')     # final engraver up
    else:
	gcode.append(u"G0 Z%.4f" % SafeZ)    	
    if last:
        gcode.append(Postamble)
  
    for line in gcode:
            sys.stdout.write(line.encode('utf-8')+'\n')



################################################################################################################

def help_message():
    print ''' textogcode is a G-Code Engraving Generator for text file 
    	      (C) cadenote@hotmail.com <2019> 
	      Allows the generation of multiple lines of engraved text provided in a text file'''
            
    print '''textogcode -d -X -x -i -Y -y -S -s -Z -D -C -W -M -F -o -P -p -t
       Options: 
       -h   Display this help message
       -A   Angle value                         Defaults to 0
       -X   Start X value                       Defaults to 0
       -Y   Start Y value                       Defaults to 0
       -y   Y offset between lines              Defaults to 0
       -S   X Scale                             Defaults to 1
       -s   Y Scale                             Defaults to 1       
       -Z   Safe Z for moves                    Defaults to 2mm
       -D   Z depth for engraving               Defaults to 0.1mm
       -C   Character Space %                   Defaults to 25%
       -W   Word Space %                        Defaults to 100%
       -M   Mirror                              Defaults to 0 (No)
       -F   Flip                                Defaults to 0 (No)
       -o   O-Code				Defaults to 1 (Yes)
       -P   Preamble g code                     Defaults to "G17 G21 G40 G90 G64 P0.003 F50"
       -p   Postamble g code                    Defaults to "M2"
       -t   Nombre de fichero de texto
      Ejemplos
      ./textogcode -y11 -C0.8 -S0.3 -s0.3 -A90 -t'si.txt' > test.ngc
	o
      ./textogcode -X7.5 -A90 -Y12.75 -y5.25 -S0.4 -s0.5 -Z2 -D0.15 -t'si.txt' > test.ngc
    '''
    sys.exit(0)

#===============================================================================================================

def main():
	
    # need to declare the globals because we want to write to them
    # otherwise python will create a local of the same name and
    # not change the global - stupid python
    global SafeZ
    global XStart
    global XX
    global XLineOffRC
    global XIndentList
    global YStart
    global YY
    global YLineOffset
    global YLineOffRC
    global Depth
    global XScale
    global YScale
    global CSpaceP
    global WSpaceP
    global Angle
    global Mirror
    global Flip
    global Ocode 
    global Preamble
    global Postamble
    global stringlist
    global debug

    debug = 0
    Ocode = 1

    try:
        options, xarguments = getopt.getopt(sys.argv[1:], 'hd:A:X:Y:y:S:s:Z:D:C:W:M:F:o:P:p:L:t:')
    except getopt.error:
        print 'Error: You tried to use an unknown option. Try `engrave-lines.py -h\' for more information.'
        sys.exit(0)
        
    if len(sys.argv[1:]) == 0:
        help_message()
        sys.exit(0)    
    
    for a in options[:]:
        if a[0] == '-h':
            help_message()
            sys.exit(0)
#  hidden debug option for testing            
    for a in options[:]:
        if a[0] == '-d' and a[1] != '':
            debug = int(a[1])
            print'debug set to %d' %(debug)
	    print "Opciones obtenidas"
	    for elemento in options[:]:
		print elemento
            options.remove(a)
            break

    for a in options[:]:
        if a[0] == '-X' and a[1] != '':
            XStart = float(a[1])
            if debug:            
                print'X = %.4f' %(XStart)
            options.remove(a)
            break

    for a in options[:]:
        if a[0] == '-Y' and a[1] != '':
            YStart = float(a[1])
            if debug:
                print'Y = %.4f' %(YStart)
            options.remove(a)
            break

    for a in options[:]:
        if a[0] == '-y' and a[1] != '':
            YLineOffset = float(a[1])
            if debug:
                print'y = %.4f' %(YLineOffset)
            options.remove(a)
            break
            
    for a in options[:]:
        if a[0] == '-S' and a[1] != '':
            XScale = float(a[1])
            if debug:
                print'S = %.4f' %(XScale)
            options.remove(a)
            break            
  
    for a in options[:]:
        if a[0] == '-s' and a[1] != '':
            YScale = float(a[1])
            if debug:
                print's = %.4f' %(YScale)
            options.remove(a)
            break              
  
    for a in options[:]:
        if a[0] == '-Z' and a[1] != '':
            SafeZ = float(a[1])
            if debug:
                print'Z = %.4f' %(SafeZ)
            options.remove(a)
            break  
  
    for a in options[:]:
        if a[0] == '-D' and a[1] != '':
            Depth = float(a[1])
            if debug:
                print'D = %.4f' %(Depth)
            options.remove(a)
            break    
  
    for a in options[:]:
        if a[0] == '-C' and a[1] != '':
            CSpaceP = float(a[1])
            if debug:
                print'C = %.4f' %(CSpaceP)
            options.remove(a)
            break      

    for a in options[:]:
        if a[0] == '-W' and a[1] != '':
            WSpaceP = float(a[1])    
            if debug:
                print'W = %.4f' %(WSpaceP)
            options.remove(a)
            break      

    for a in options[:]:
        if a[0] == '-A' and a[1] != '':
            Angle = float(a[1])
            if debug:
                print'A = %.4f' %(Angle)
            options.remove(a)
            break  
            
            
    for a in options[:]:
        if a[0] == '-M' and a[1] != '':
            Mirror = float(a[1])
            if debug:
                print'M = %.4f' %(Mirror)
            options.remove(a)
            break  
              
    for a in options[:]:
        if a[0] == '-F' and a[1] != '':
            Flip = float(a[1])
            if debug:
                print'F = %.4f' %(Flip)
            options.remove(a)
            break  

    for a in options[:]:
        if a[0] == '-o' and a[1] != '':
            Ocode = float(a[1])
            if debug:
                print'O = %.4f' %(Ocode)
            options.remove(a)
            break  

    for a in options[:]:
        if a[0] == '-P' and a[1] != '':
            Preamble = a[1]
            if debug:
                print'P = %s' %(a[1])
            options.remove(a)
            break  

    for a in options[:]:
        if a[0] == '-p' and a[1] != '':            
            Postamble = a[1]
            if debug:
                print'p = %s' %(a[1])
            options.remove(a)
            break  


    for a in options[:]:
        if a[0] == '-t' and a[1] != '':
            fichero=a[1]
	    contenido=open(fichero)
	    for texto in contenido:
		tex=texto.rstrip()
		stringlist.append(tex)
            if debug:
                print't = %s' %(a[1])
		print 'El contenido de %s es' % fichero
		for texto in contenido:
			print texto.rstrip()
	    contenido.close()
            options.remove(a)
            break  

    XLineOffRC,YLineOffRC = Rotn(0,YLineOffset,XScale,YScale,Angle)
    if debug :
	print 'XLineOffRC= %f YLineOffRC= %f YLineOffset= %f XScale= %f YScale= %f Angle= %f' % (XLineOffRC , YLineOffRC , YLineOffset , XScale, YScale, Angle)
    for index, item in enumerate(stringlist):
        code(item,index, index == (len(stringlist) - 1) )
   
            
#===============================================================================================
            
if __name__ == "__main__":
	    main()

#===============================================================================================END
