2023-01-08 19:41:02 +01:00
from __future__ import annotations
2020-03-27 11:00:12 +01:00
import datetime
2022-12-31 15:52:55 +01:00
import math
2023-01-08 19:41:02 +01:00
from typing import Any
2022-12-31 15:52:55 +01:00
from xml . etree import ElementTree as ET
2019-11-23 18:38:23 +01:00
from affine import Affine
2022-12-31 15:52:55 +01:00
2019-11-23 18:38:23 +01:00
from boxes . extents import Extents
2020-03-28 16:36:47 +01:00
2019-11-23 18:38:23 +01:00
EPS = 1e-4
PADDING = 10
2023-01-02 00:32:42 +01:00
RANDOMIZE_COLORS = False # enable to ease check for continuity of paths
2019-11-23 18:38:23 +01:00
def points_equal ( x1 , y1 , x2 , y2 ) :
return abs ( x1 - x2 ) < EPS and abs ( y1 - y2 ) < EPS
def pdiff ( p1 , p2 ) :
x1 , y1 = p1
x2 , y2 = p2
return ( x1 - x2 , y1 - y2 )
class Surface :
2020-04-20 23:38:00 +02:00
scale = 1.0
invert_y = False
2023-01-08 19:41:02 +01:00
def __init__ ( self , fname ) - > None :
2019-11-23 18:38:23 +01:00
self . _fname = fname
2023-01-08 19:41:02 +01:00
self . parts : list [ Any ] = [ ]
2019-11-23 18:38:23 +01:00
self . _p = self . new_part ( " default " )
2023-02-07 21:54:38 +01:00
self . count = 0
2019-11-23 18:38:23 +01:00
2020-03-27 11:00:12 +01:00
def set_metadata ( self , metadata ) :
self . metadata = metadata
2019-11-23 18:38:23 +01:00
def flush ( self ) :
pass
def finish ( self ) :
pass
2020-04-20 23:38:00 +02:00
def _adjust_coordinates ( self ) :
extents = self . extents ( )
extents . xmin - = PADDING
extents . ymin - = PADDING
extents . xmax + = PADDING
extents . ymax + = PADDING
m = Affine . translation ( - extents . xmin , - extents . ymin )
if self . invert_y :
m = Affine . scale ( self . scale , - self . scale ) * m
2020-04-26 20:13:12 +02:00
m = Affine . translation ( 0 , self . scale * extents . height ) * m
2020-04-20 23:38:00 +02:00
else :
m = Affine . scale ( self . scale , self . scale ) * m
2021-10-09 13:59:24 +02:00
self . transform ( self . scale , m , self . invert_y )
2020-04-20 23:38:00 +02:00
return Extents ( 0 , 0 , extents . width * self . scale , extents . height * self . scale )
2019-11-23 18:38:23 +01:00
def render ( self , renderer ) :
renderer . init ( * * self . args )
for p in self . parts :
p . render ( renderer )
renderer . finish ( )
2021-10-09 13:59:24 +02:00
def transform ( self , f , m , invert_y = False ) :
2019-11-23 18:38:23 +01:00
for p in self . parts :
2021-10-09 13:59:24 +02:00
p . transform ( f , m , invert_y )
2019-11-23 18:38:23 +01:00
def new_part ( self , name = " part " ) :
if self . parts and len ( self . parts [ - 1 ] . pathes ) == 0 :
return self . _p
p = Part ( name )
self . parts . append ( p )
self . _p = p
return p
def append ( self , * path ) :
2023-02-07 21:54:38 +01:00
self . count + = 1
if self . count > 100000 :
raise ValueError ( " Too many lines " )
2019-11-23 18:38:23 +01:00
self . _p . append ( * path )
def stroke ( self , * * params ) :
return self . _p . stroke ( * * params )
def move_to ( self , * xy ) :
self . _p . move_to ( * xy )
def extents ( self ) :
if not self . parts :
return Extents ( )
return sum ( [ p . extents ( ) for p in self . parts ] )
class Part :
2023-01-08 19:41:02 +01:00
def __init__ ( self , name ) - > None :
2023-01-08 19:41:02 +01:00
self . pathes : list [ Any ] = [ ]
self . path : list [ Any ] = [ ]
2019-11-23 18:38:23 +01:00
def extents ( self ) :
if not self . pathes :
return Extents ( )
return sum ( [ p . extents ( ) for p in self . pathes ] )
2021-10-09 13:59:24 +02:00
def transform ( self , f , m , invert_y = False ) :
2019-11-23 18:38:23 +01:00
assert ( not self . path )
for p in self . pathes :
2021-10-09 13:59:24 +02:00
p . transform ( f , m , invert_y )
2019-11-23 18:38:23 +01:00
def append ( self , * path ) :
self . path . append ( list ( path ) )
def stroke ( self , * * params ) :
if len ( self . path ) == 0 :
return
# search for path ending at new start coordinates to append this path to
xy0 = self . path [ 0 ] [ 1 : 3 ]
2022-06-07 08:49:33 +02:00
if ( not points_equal ( * xy0 , * self . path [ - 1 ] [ 1 : 3 ] ) and
not self . path [ 0 ] [ 0 ] == " T " ) :
for p in reversed ( self . pathes ) :
xy1 = p . path [ - 1 ] [ 1 : 3 ]
2022-06-27 07:54:39 +02:00
if points_equal ( * xy0 , * xy1 ) and p . params == params :
2022-06-07 08:49:33 +02:00
p . path . extend ( self . path [ 1 : ] )
self . path = [ ]
return p
2019-11-23 18:38:23 +01:00
p = Path ( self . path , params )
self . pathes . append ( p )
self . path = [ ]
return p
def move_to ( self , * xy ) :
if len ( self . path ) == 0 :
self . path . append ( [ " M " , * xy ] )
elif self . path [ - 1 ] [ 0 ] == " M " :
self . path [ - 1 ] = [ " M " , * xy ]
else :
xy0 = self . path [ - 1 ] [ 1 : 3 ]
if not points_equal ( * xy0 , * xy ) :
self . path . append ( [ " M " , * xy ] )
class Path :
2023-01-08 19:41:02 +01:00
def __init__ ( self , path , params ) - > None :
2019-11-23 18:38:23 +01:00
self . path = path
self . params = params
2023-01-08 19:41:02 +01:00
def __repr__ ( self ) - > str :
2019-11-23 18:38:23 +01:00
l = len ( self . path )
# x1,y1 = self.path[0][1:3]
2022-03-30 05:02:20 +02:00
if l > 0 :
x2 , y2 = self . path [ - 1 ] [ 1 : 3 ]
return f " Path[ { l } ] to ( { x2 : .2f } , { y2 : .2f } ) "
return f " empty Path "
2019-11-23 18:38:23 +01:00
def extents ( self ) :
e = Extents ( )
for p in self . path :
e . add ( * p [ 1 : 3 ] )
2020-04-26 20:13:12 +02:00
if p [ 0 ] == ' T ' :
m , text , params = p [ 3 : ]
h = params [ ' fs ' ]
l = len ( text ) * h * 0.7
align = params . get ( ' align ' , ' left ' )
start , end = {
' left ' : ( 0 , 1 ) ,
' middle ' : ( - 0.5 , 0.5 ) ,
' end ' : ( - 1 , 0 ) ,
} [ align ]
for x in ( start * l , end * l ) :
for y in ( 0 , h ) :
x_ , y_ = m * ( x , y )
e . add ( x_ , y_ )
2019-11-23 18:38:23 +01:00
return e
2021-10-09 13:59:24 +02:00
def transform ( self , f , m , invert_y = False ) :
self . params [ " lw " ] * = f
2019-11-23 18:38:23 +01:00
for c in self . path :
C = c [ 0 ]
2020-04-20 23:38:00 +02:00
c [ 1 ] , c [ 2 ] = m * ( c [ 1 ] , c [ 2 ] )
2019-11-23 18:38:23 +01:00
if C == ' C ' :
2020-04-20 23:38:00 +02:00
c [ 3 ] , c [ 4 ] = m * ( c [ 3 ] , c [ 4 ] )
c [ 5 ] , c [ 6 ] = m * ( c [ 5 ] , c [ 6 ] )
if C == " T " :
c [ 3 ] = m * c [ 3 ]
if invert_y :
c [ 3 ] * = Affine . scale ( 1 , - 1 )
2019-11-23 18:38:23 +01:00
2022-03-20 00:21:57 +01:00
def faster_edges ( self , inner_corners ) :
if inner_corners == " backarc " :
return
2019-11-23 18:38:23 +01:00
for ( i , p ) in enumerate ( self . path ) :
if p [ 0 ] == " C " and i > 1 and i < len ( self . path ) - 1 :
if self . path [ i - 1 ] [ 0 ] == " L " and self . path [ i + 1 ] [ 0 ] == " L " :
p11 = self . path [ i - 2 ] [ 1 : 3 ]
p12 = self . path [ i - 1 ] [ 1 : 3 ]
p21 = p [ 1 : 3 ]
p22 = self . path [ i + 1 ] [ 1 : 3 ]
if ( ( ( p12 [ 0 ] - p21 [ 0 ] ) * * 2 + ( p12 [ 1 ] - p21 [ 1 ] ) * * 2 ) >
self . params [ " lw " ] * * 2 ) :
continue
lines_intersect , x , y = line_intersection ( ( p11 , p12 ) , ( p21 , p22 ) )
if lines_intersect :
self . path [ i - 1 ] = ( " L " , x , y )
2022-03-20 00:21:57 +01:00
if inner_corners == " loop " :
self . path [ i ] = ( " C " , x , y , * p12 , * p21 )
else :
self . path [ i ] = ( " L " , x , y )
# filter duplicates
2022-03-30 05:02:20 +02:00
if len ( self . path ) > 1 : # no need to find duplicates if only one element in path
self . path = [ p for n , p in enumerate ( self . path ) if p != self . path [ n - 1 ] ]
2019-11-23 18:38:23 +01:00
class Context :
2023-01-08 19:41:02 +01:00
def __init__ ( self , surface , * al , * * ad ) - > None :
2019-11-23 18:38:23 +01:00
self . _renderer = self . _dwg = surface
self . _bounds = Extents ( )
self . _padding = PADDING
2023-01-08 19:41:02 +01:00
self . _stack : list [ Any ] = [ ]
2019-11-23 18:38:23 +01:00
self . _m = Affine . translation ( 0 , 0 )
self . _xy = ( 0 , 0 )
self . _mxy = self . _m * self . _xy
self . _lw = 0
self . _rgb = ( 0 , 0 , 0 )
self . _ff = " sans-serif "
2020-04-20 23:38:00 +02:00
self . _fs = 10
2019-11-23 18:38:23 +01:00
self . _last_path = None
def _update_bounds_ ( self , mx , my ) :
self . _bounds . update ( mx , my )
def save ( self ) :
self . _stack . append (
( self . _m , self . _xy , self . _lw , self . _rgb , self . _mxy , self . _last_path )
)
self . _xy = ( 0 , 0 )
def restore ( self ) :
(
self . _m ,
self . _xy ,
self . _lw ,
self . _rgb ,
self . _mxy ,
self . _last_path ,
) = self . _stack . pop ( )
## transformations
def translate ( self , x , y ) :
self . _m * = Affine . translation ( x , y )
self . _xy = ( 0 , 0 )
def scale ( self , sx , sy ) :
self . _m * = Affine . scale ( sx , sy )
def rotate ( self , r ) :
self . _m * = Affine . rotation ( 180 * r / math . pi )
def set_line_width ( self , lw ) :
self . _lw = lw
def set_source_rgb ( self , r , g , b ) :
self . _rgb = ( r , g , b )
## path methods
def _line_to ( self , x , y ) :
self . _add_move ( )
x1 , y1 = self . _mxy
self . _xy = x , y
x2 , y2 = self . _mxy = self . _m * self . _xy
if not points_equal ( x1 , y1 , x2 , y2 ) :
self . _dwg . append ( " L " , x2 , y2 )
def _add_move ( self ) :
self . _dwg . move_to ( * self . _mxy )
def move_to ( self , x , y ) :
self . _xy = ( x , y )
self . _mxy = self . _m * self . _xy
def line_to ( self , x , y ) :
self . _line_to ( x , y )
def _arc ( self , xc , yc , radius , angle1 , angle2 , direction ) :
2020-04-26 17:49:13 +02:00
if abs ( angle1 - angle2 ) < EPS or radius < EPS :
return
2019-11-23 18:38:23 +01:00
x1 , y1 = radius * math . cos ( angle1 ) + xc , radius * math . sin ( angle1 ) + yc
x4 , y4 = radius * math . cos ( angle2 ) + xc , radius * math . sin ( angle2 ) + yc
# XXX direction seems not needed for small arcs
ax = x1 - xc
ay = y1 - yc
bx = x4 - xc
by = y4 - yc
q1 = ax * ax + ay * ay
q2 = q1 + ax * bx + ay * by
k2 = 4 / 3 * ( ( 2 * q1 * q2 ) * * 0.5 - q2 ) / ( ax * by - ay * bx )
x2 = xc + ax - k2 * ay
y2 = yc + ay + k2 * ax
x3 = xc + bx + k2 * by
y3 = yc + by - k2 * bx
mx1 , my1 = self . _m * ( x1 , y1 )
mx2 , my2 = self . _m * ( x2 , y2 )
mx3 , my3 = self . _m * ( x3 , y3 )
mx4 , my4 = self . _m * ( x4 , y4 )
mxc , myc = self . _m * ( xc , yc )
self . _add_move ( )
self . _dwg . append ( " C " , mx4 , my4 , mx2 , my2 , mx3 , my3 )
self . _xy = ( x4 , y4 )
self . _mxy = ( mx4 , my4 )
def arc ( self , xc , yc , radius , angle1 , angle2 ) :
self . _arc ( xc , yc , radius , angle1 , angle2 , 1 )
def arc_negative ( self , xc , yc , radius , angle1 , angle2 ) :
self . _arc ( xc , yc , radius , angle1 , angle2 , - 1 )
def curve_to ( self , x1 , y1 , x2 , y2 , x3 , y3 ) :
# mx0,my0 = self._m*self._xy
mx1 , my1 = self . _m * ( x1 , y1 )
mx2 , my2 = self . _m * ( x2 , y2 )
mx3 , my3 = self . _m * ( x3 , y3 )
self . _add_move ( )
self . _dwg . append ( " C " , mx3 , my3 , mx1 , my1 , mx2 , my2 ) # destination first!
self . _xy = ( x3 , y3 )
2020-10-18 11:08:03 +02:00
self . _mxy = ( mx3 , my3 )
2019-11-23 18:38:23 +01:00
def stroke ( self ) :
# print('stroke stack-level=',len(self._stack),'lastpath=',self._last_path,)
self . _last_path = self . _dwg . stroke ( rgb = self . _rgb , lw = self . _lw )
self . _xy = ( 0 , 0 )
def fill ( self ) :
self . _xy = ( 0 , 0 )
raise NotImplementedError ( )
2020-04-20 23:38:00 +02:00
def set_font ( self , style , bold = False , italic = False ) :
if style not in ( " serif " , " sans-serif " , " monospaced " ) :
raise ValueError ( " Unknown font style " )
self . _ff = ( style , bold , italic )
2019-11-23 18:38:23 +01:00
def set_font_size ( self , fs ) :
self . _fs = fs
def show_text ( self , text , * * args ) :
params = { " ff " : self . _ff , " fs " : self . _fs , " lw " : self . _lw , " rgb " : self . _rgb }
params . update ( args )
mx0 , my0 = self . _m * self . _xy
2020-04-20 23:38:00 +02:00
m = self . _m
self . _dwg . append ( " T " , mx0 , my0 , m , text , params )
2019-11-23 18:38:23 +01:00
def text_extents ( self , text ) :
fs = self . _fs
# XXX ugly hack! Fix Boxes.text() !
return ( 0 , 0 , 0.6 * fs * len ( text ) , 0.65 * fs , fs * 0.1 , 0 )
def rectangle ( self , x , y , width , height ) :
# todo: better check for empty path?
self . stroke ( )
self . move_to ( x , y )
self . line_to ( x + width , y )
self . line_to ( x + width , y + height )
self . line_to ( x , y + height )
self . line_to ( x , y )
self . stroke ( )
def get_current_point ( self ) :
return self . _xy
def flush ( self ) :
pass
# todo: check, if needed
# self.stroke()
## additional methods
def new_part ( self ) :
self . _dwg . new_part ( )
class SVGSurface ( Surface ) :
2020-04-20 23:38:00 +02:00
invert_y = True
fonts = {
' serif ' : ' TimesNewRoman, " Times New Roman " , Times, Baskerville, Georgia, serif ' ,
' sans-serif ' : ' " Helvetica Neue " , Helvetica, Arial, sans-serif ' ,
' monospaced ' : ' " Courier New " , Courier, " Lucida Sans Typewriter " '
}
2020-03-27 11:00:12 +01:00
def _addTag ( self , parent , tag , text , first = False ) :
if first :
t = ET . Element ( tag )
else :
t = ET . SubElement ( parent , tag )
t . text = text
t . tail = ' \n '
if first :
parent . insert ( 0 , t )
return t
def _add_metadata ( self , root ) :
md = self . metadata
# Add Inkscape style rdf meta data
root . set ( " xmlns:dc " , " http://purl.org/dc/elements/1.1/ " )
root . set ( " xmlns:cc " , " http://creativecommons.org/ns# " )
root . set ( " xmlns:rdf " , " http://www.w3.org/1999/02/22-rdf-syntax-ns# " )
title = " {group} - {name} " . format ( * * md )
date = datetime . datetime . now ( ) . strftime ( " % Y- % m- %d % H: % M: % S " )
m = self . _addTag ( root , " metadata " , ' \n ' , True )
r = ET . SubElement ( m , ' rdf:RDF ' )
w = ET . SubElement ( r , ' cc:Work ' )
w . text = ' \n '
self . _addTag ( w , ' dc:title ' , title )
self . _addTag ( w , ' dc:date ' , date )
if " url " in md and md [ " url " ] :
self . _addTag ( w , ' dc:source ' , md [ " url " ] )
2023-02-19 20:52:02 +01:00
self . _addTag ( w , ' dc:source ' , md [ " url_short " ] )
2020-03-27 11:00:12 +01:00
else :
self . _addTag ( w , ' dc:source ' , md [ " cli " ] )
desc = md [ " short_description " ] or " "
if " description " in md and md [ " description " ] :
desc + = " \n \n " + md [ " description " ]
desc + = " \n \n Created with Boxes.py (https://festi.info/boxes.py) \n "
desc + = " Command line: %s \n " % md [ " cli " ]
2023-02-19 20:52:02 +01:00
desc + = " Command line short: %s \n " % md [ " cli_short " ]
2020-03-27 11:00:12 +01:00
if md [ " url " ] :
desc + = " Url: %s \n " % md [ " url " ]
2023-02-19 20:52:02 +01:00
desc + = " Url short: %s \n " % md [ " url_short " ]
2020-03-27 11:00:12 +01:00
desc + = " SettingsUrl: %s \n " % md [ " url " ] . replace ( " &render=1 " , " " )
2023-02-19 20:52:02 +01:00
desc + = " SettingsUrl short: %s \n " % md [ " url_short " ] . replace ( " &render=1 " , " " )
2020-03-27 11:00:12 +01:00
self . _addTag ( w , ' dc:description ' , desc )
# title
self . _addTag ( root , " title " , md [ " name " ] , True )
# Add XML comment
txt = """
{ name } - { short_description }
""" .format(**md)
if md [ " description " ] :
txt + = """
{ description }
""" .format(**md)
txt + = """
Created with Boxes . py ( https : / / festi . info / boxes . py )
Creation date : { date }
""" .format(date=date, **md)
2023-02-19 20:52:02 +01:00
txt + = " Command line (remove spaces between dashes): %s \n " % md [ " cli_short " ]
2020-03-27 11:00:12 +01:00
if md [ " url " ] :
txt + = " Url: %s \n " % md [ " url " ]
2023-02-19 20:52:02 +01:00
txt + = " Url short: %s \n " % md [ " url_short " ]
2020-03-27 11:00:12 +01:00
txt + = " SettingsUrl: %s \n " % md [ " url " ] . replace ( " &render=1 " , " " )
2023-02-19 20:52:02 +01:00
txt + = " SettingsUrl short: %s \n " % md [ " url_short " ] . replace ( " &render=1 " , " " )
2022-05-15 16:06:24 +02:00
m = ET . Comment ( txt . replace ( " -- " , " - - " ) . replace ( " -- " , " - - " ) ) # ----
2020-03-27 11:00:12 +01:00
m . tail = ' \n '
root . insert ( 0 , m )
2022-03-20 00:21:57 +01:00
def finish ( self , inner_corners = " loop " ) :
2020-04-20 23:38:00 +02:00
extents = self . _adjust_coordinates ( )
w = extents . width * self . scale
h = extents . height * self . scale
2019-11-23 18:38:23 +01:00
2020-03-28 16:36:47 +01:00
nsmap = {
" dc " : " http://purl.org/dc/elements/1.1/ " ,
" cc " : " http://creativecommons.org/ns# " ,
" rdf " : " http://www.w3.org/1999/02/22-rdf-syntax-ns# " ,
" svg " : " http://www.w3.org/2000/svg " ,
" xlink " : " http://www.w3.org/1999/xlink " ,
" inkscape " : " http://www.inkscape.org/namespaces/inkscape " ,
}
ET . register_namespace ( " " , " http://www.w3.org/2000/svg " )
ET . register_namespace ( " xlink " , " http://www.w3.org/1999/xlink " )
svg = ET . Element ( ' svg ' , width = f " { w : .2f } mm " , height = f " { h : .2f } mm " ,
viewBox = f " 0.0 0.0 { w : .2f } { h : .2f } " ,
xmlns = " http://www.w3.org/2000/svg " )
for name , value in nsmap . items ( ) :
svg . set ( f " xmlns: { name } " , value )
svg . text = " \n "
tree = ET . ElementTree ( svg )
2020-03-27 11:00:12 +01:00
self . _add_metadata ( svg )
2020-03-28 16:36:47 +01:00
2019-11-23 18:38:23 +01:00
for i , part in enumerate ( self . parts ) :
if not part . pathes :
continue
2020-03-28 16:36:47 +01:00
g = ET . SubElement ( svg , " g " , id = f " p- { i } " ,
style = " fill:none;stroke-linecap:round;stroke-linejoin:round; " )
g . text = " \n "
g . tail = " \n "
2019-11-23 18:38:23 +01:00
for j , path in enumerate ( part . pathes ) :
p = [ ]
x , y = 0 , 0
2020-09-15 13:30:38 +02:00
start = None
last = None
2022-03-20 00:21:57 +01:00
path . faster_edges ( inner_corners )
2019-11-23 18:38:23 +01:00
for c in path . path :
x0 , y0 = x , y
C , x , y = c [ 0 : 3 ]
if C == " M " :
2020-09-15 13:30:38 +02:00
if start and points_equal ( start [ 1 ] , start [ 2 ] ,
last [ 1 ] , last [ 2 ] ) :
p . append ( " Z " )
start = c
2019-11-23 18:38:23 +01:00
p . append ( f " M { x : .3f } { y : .3f } " )
elif C == " L " :
2020-04-06 19:57:24 +02:00
if abs ( x - x0 ) < EPS :
p . append ( f " V { y : .3f } " )
elif abs ( y - y0 ) < EPS :
p . append ( f " H { x : .3f } " )
else :
p . append ( f " L { x : .3f } { y : .3f } " )
2019-11-23 18:38:23 +01:00
elif C == " C " :
x1 , y1 , x2 , y2 = c [ 3 : ]
p . append (
f " C { x1 : .3f } { y1 : .3f } { x2 : .3f } { y2 : .3f } { x : .3f } { y : .3f } "
)
elif C == " T " :
2020-04-20 23:38:00 +02:00
m , text , params = c [ 3 : ]
m = m * Affine . translation ( 0 , - params [ ' fs ' ] )
2023-01-13 15:32:32 +01:00
tm = " " . join ( f " { m [ i ] : .3f } " for i in ( 0 , 3 , 1 , 4 , 2 , 5 ) )
2020-04-20 23:38:00 +02:00
font , bold , italic = params [ ' ff ' ]
fontweight = ( " normal " , " bold " ) [ bool ( bold ) ]
fontstyle = ( " normal " , " italic " ) [ bool ( italic ) ]
style = f " font-family: { font } ; font-weight: { fontweight } ; font-style: { fontstyle } ; fill: { rgb_to_svg_color ( * params [ ' rgb ' ] ) } "
2020-03-28 16:36:47 +01:00
t = ET . SubElement ( g , " text " ,
2020-04-20 23:38:00 +02:00
#x=f"{x:.3f}", y=f"{y:.3f}",
transform = f " matrix( { tm } ) " ,
2020-03-28 16:36:47 +01:00
style = style )
t . text = text
t . set ( " font-size " , f " { params [ ' fs ' ] } px " )
2020-04-20 23:38:00 +02:00
t . set ( " text-anchor " , params . get ( ' align ' , ' left ' ) )
2022-05-26 12:29:52 +02:00
t . set ( " dominant-baseline " , ' hanging ' )
2019-11-23 18:38:23 +01:00
else :
print ( " Unknown " , c )
2020-09-15 13:30:38 +02:00
last = c
if start and start is not last and \
points_equal ( start [ 1 ] , start [ 2 ] , last [ 1 ] , last [ 2 ] ) :
p . append ( " Z " )
2019-11-23 18:38:23 +01:00
color = (
random_svg_color ( )
if RANDOMIZE_COLORS
else rgb_to_svg_color ( * path . params [ " rgb " ] )
)
2020-05-23 15:03:16 +02:00
if p and p [ - 1 ] [ 0 ] == " M " :
p . pop ( )
2020-03-28 16:36:47 +01:00
if p : # might be empty if only contains text
t = ET . SubElement ( g , " path " , d = " " . join ( p ) , stroke = color )
2021-10-09 13:59:24 +02:00
t . set ( " stroke-width " , f ' { path . params [ " lw " ] : .2f } ' )
2020-03-28 16:36:47 +01:00
t . tail = " \n "
t . tail = " \n "
2022-12-30 16:33:39 +01:00
tree . write ( open ( self . _fname , " wb " ) , encoding = " utf-8 " , xml_declaration = True , method = " xml " )
2019-11-23 18:38:23 +01:00
class PSSurface ( Surface ) :
2020-04-20 23:38:00 +02:00
scale = 72 / 25.4 # 72 dpi
fonts = {
( ' serif ' , False , False ) : ' Times-Roman ' ,
( ' serif ' , False , True ) : ' Times-Italic ' ,
( ' serif ' , True , False ) : ' Times-Bold ' ,
( ' serif ' , True , True ) : ' Times-BoldItalic ' ,
( ' sans-serif ' , False , False ) : ' Helvetica ' ,
( ' sans-serif ' , False , True ) : ' Helvetica-Oblique ' ,
( ' sans-serif ' , True , False ) : ' Helvetica-Bold ' ,
( ' sans-serif ' , True , True ) : ' Helvetica-BoldOblique ' ,
( ' monospaced ' , False , False ) : ' Courier ' ,
( ' monospaced ' , False , True ) : ' Courier-Oblique ' ,
( ' monospaced ' , True , False ) : ' Courier-Bold ' ,
( ' monospaced ' , True , True ) : ' Courier-BoldOblique ' ,
}
2019-11-23 18:38:23 +01:00
2021-05-27 22:50:10 +02:00
def _metadata ( self ) :
md = self . metadata
desc = " "
desc + = " %% Title: Boxes.py - {group} - {name} \n " . format ( * * md )
desc + = f ' %%CreationDate: { datetime . datetime . now ( ) . strftime ( " % Y- % m- %d % H: % M: % S " ) } \n '
desc + = f ' %%Keywords: boxes.py, laser, laser cutter \n '
desc + = f ' %%Creator: { md . get ( " url " ) or md [ " cli " ] } \n '
desc + = " %% CreatedBy: Boxes.py (https://festi.info/boxes.py) \n "
for line in ( md [ " short_description " ] or " " ) . split ( " \n " ) :
desc + = " %% %s \n " % line
desc + = " % \n "
if " description " in md and md [ " description " ] :
desc + = " % \n "
for line in md [ " description " ] . split ( " \n " ) :
desc + = " %% %s \n " % line
desc + = " % \n "
desc + = " %% Command line: %s \n " % md [ " cli " ]
2023-02-19 20:52:02 +01:00
desc + = " %% Command line short: %s \n " % md [ " cli_short " ]
2021-05-27 22:50:10 +02:00
if md [ " url " ] :
desc + = f ' %%Url: { md [ " url " ] } \n '
2023-02-19 20:52:02 +01:00
desc + = f ' %%Url short: { md [ " url_short " ] } \n '
2021-05-27 22:50:10 +02:00
desc + = f ' %%SettingsUrl: { md [ " url " ] . replace ( " &render=1 " , " " ) } \n '
2023-02-19 20:52:02 +01:00
desc + = f ' %%SettingsUrl short: { md [ " url_short " ] . replace ( " &render=1 " , " " ) } \n '
2021-05-27 22:50:10 +02:00
return desc
2022-03-20 00:21:57 +01:00
def finish ( self , inner_corners = " loop " ) :
2019-11-23 18:38:23 +01:00
2020-04-20 23:38:00 +02:00
extents = self . _adjust_coordinates ( )
w = extents . width
h = extents . height
2019-11-23 18:38:23 +01:00
2020-04-20 23:38:00 +02:00
f = open ( self . _fname , " w " , encoding = " latin1 " , errors = " replace " )
2019-11-23 18:38:23 +01:00
2023-02-24 14:32:34 +01:00
f . write ( f """ %!PS-Adobe-2.0 EPSF-2.0
% % BoundingBox : 0 0 { w : .0 f } { h : .0 f }
{ self . _metadata ( ) }
% % EndComments
2020-04-20 23:38:00 +02:00
1 setlinecap
1 setlinejoin
0.0 0.0 0.0 setrgbcolor
""" )
f . write ( """
/ ReEncode { % inFont outFont encoding | -
/ MyEncoding exch def
exch findfont
dup length dict
begin
{ def } forall
/ Encoding MyEncoding def
currentdict
end
definefont
} def
""" )
for font in self . fonts . values ( ) :
f . write ( f " / { font } / { font } -Latin1 ISOLatin1Encoding ReEncode \n " )
2019-11-23 18:38:23 +01:00
# f.write(f"%%DocumentMedia: \d+x\d+mm ((\d+) (\d+)) 0 \("
# dwg['width']=f'{w:.2f}mm'
# dwg['height']=f'{h:.2f}mm'
for i , part in enumerate ( self . parts ) :
if not part . pathes :
continue
for j , path in enumerate ( part . pathes ) :
p = [ ]
x , y = 0 , 0
2022-03-20 00:21:57 +01:00
path . faster_edges ( inner_corners )
2019-11-23 18:38:23 +01:00
for c in path . path :
x0 , y0 = x , y
C , x , y = c [ 0 : 3 ]
if C == " M " :
p . append ( f " { x : .3f } { y : .3f } moveto " )
elif C == " L " :
p . append ( f " { x : .3f } { y : .3f } lineto " )
elif C == " C " :
x1 , y1 , x2 , y2 = c [ 3 : ]
p . append (
f " { x1 : .3f } { y1 : .3f } { x2 : .3f } { y2 : .3f } { x : .3f } { y : .3f } curveto "
)
elif C == " T " :
2020-04-20 23:38:00 +02:00
m , text , params = c [ 3 : ]
2023-01-13 15:32:32 +01:00
tm = " " . join ( f " { m [ i ] : .3f } " for i in ( 0 , 3 , 1 , 4 , 2 , 5 ) )
2019-11-23 18:38:23 +01:00
text = text . replace ( " ( " , " r \ ( " ) . replace ( " ) " , r " \ ) " )
2023-01-13 15:32:32 +01:00
color = " " . join ( f " { c : .2f } " for c in params [ " rgb " ] )
2020-04-20 23:38:00 +02:00
align = params . get ( ' align ' , ' left ' )
f . write ( f " / { self . fonts [ params [ ' ff ' ] ] } -Latin1 findfont \n " )
f . write ( f " { params [ ' fs ' ] } scalefont \n " )
2019-11-23 18:38:23 +01:00
f . write ( " setfont \n " )
2020-04-20 23:38:00 +02:00
#f.write(f"currentfont /Encoding ISOLatin1Encoding put\n")
2019-11-23 18:38:23 +01:00
f . write ( f " { color } setrgbcolor \n " )
2020-04-20 23:38:00 +02:00
f . write ( " matrix currentmatrix " ) # save current matrix
f . write ( f " [ { tm } ] concat \n " )
if align == " left " :
f . write ( f " 0.0 \n " )
else :
f . write ( f " ( { text } ) stringwidth pop " )
if align == " middle " :
f . write ( f " -0.5 mul \n " )
else : # end
f . write ( f " neg \n " )
# offset y by descender
f . write ( " currentfont dup /FontBBox get 1 get \n " )
f . write ( " exch /FontMatrix get 3 get mul neg moveto \n " )
f . write ( f " ( { text } ) show \n " ) # text created by dup above
f . write ( " setmatrix \n \n " ) # restore matrix
2019-11-23 18:38:23 +01:00
else :
print ( " Unknown " , c )
color = (
random_svg_color ( )
if RANDOMIZE_COLORS
else rgb_to_svg_color ( * path . params [ " rgb " ] )
)
if p : # todo: might be empty since text is not implemented yet
2023-01-13 15:32:32 +01:00
color = " " . join ( f " { c : .2f } " for c in path . params [ " rgb " ] )
2019-11-23 18:38:23 +01:00
f . write ( " newpath \n " )
f . write ( " \n " . join ( p ) )
f . write ( " \n " )
2021-10-09 13:59:24 +02:00
f . write ( f " { path . params [ ' lw ' ] } setlinewidth \n " )
2019-11-23 18:38:23 +01:00
f . write ( f " { color } setrgbcolor \n " )
f . write ( " stroke \n \n " )
f . write (
"""
showpage
% % Trailer
% % EOF
"""
)
f . close ( )
2022-03-27 10:40:38 +02:00
class LBRN2Surface ( Surface ) :
2019-11-23 18:38:23 +01:00
2022-04-16 18:27:01 +02:00
2022-03-27 10:40:38 +02:00
invert_y = False
dbg = False
fonts = {
' serif ' : ' Times New Roman ' ,
' sans-serif ' : ' Arial ' ,
' monospaced ' : ' Courier New '
}
2022-06-05 11:17:50 +02:00
lbrn2_colors = [
0 , # Colors.OUTER_CUT (BLACK) --> Lightburn C00 (black)
1 , # Colors.INNER_CUT (BLUE) --> Lightburn C01 (blue)
3 , # Colors.ETCHING (GREEN) --> Lightburn C02 (green)
6 , # Colors.ETCHING_DEEP (CYAN) --> Lightburn C06 (cyan)
30 , # Colors.ANNOTATIONS (RED) --> Lightburn T1
7 , # Colors.OUTER_CUT (MAGENTA) --> Lightburn C07 (magenta)
4 , # Colors.OUTER_CUT (YELLOW) --> Lightburn C04 (yellow)
8 , # Colors.OUTER_CUT (WHITE) --> Lightburn C08 (grey)
]
2022-03-27 10:40:38 +02:00
def finish ( self , inner_corners = " loop " ) :
if self . dbg : print ( " LBRN2 save " )
extents = self . _adjust_coordinates ( )
w = extents . width * self . scale
h = extents . height * self . scale
svg = ET . Element ( ' LightBurnProject ' , AppVersion = " 1.0.06 " , FormatVersion = " 1 " , MaterialHeight = " 0 " , MirrorX = " False " , MirrorY = " False " )
svg . text = " \n "
num = 0
txtOffset = { }
tree = ET . ElementTree ( svg )
if self . dbg : print ( " 8 " , num )
2022-06-05 11:17:50 +02:00
cs = ET . SubElement ( svg , " CutSetting " , Type = " Cut " )
index = ET . SubElement ( cs , " index " , Value = " 3 " ) # green layer (ETCHING)
name = ET . SubElement ( cs , " name " , Value = " Etch " )
priority = ET . SubElement ( cs , " priority " , Value = " 0 " ) # is cut first
cs = ET . SubElement ( svg , " CutSetting " , Type = " Cut " )
index = ET . SubElement ( cs , " index " , Value = " 6 " ) # cyan layer (ETCHING_DEEP)
name = ET . SubElement ( cs , " name " , Value = " Deep Etch " )
priority = ET . SubElement ( cs , " priority " , Value = " 1 " ) # is cut second
cs = ET . SubElement ( svg , " CutSetting " , Type = " Cut " )
index = ET . SubElement ( cs , " index " , Value = " 7 " ) # magenta layer (MAGENTA)
name = ET . SubElement ( cs , " name " , Value = " C07 " )
priority = ET . SubElement ( cs , " priority " , Value = " 2 " ) # is cut third
cs = ET . SubElement ( svg , " CutSetting " , Type = " Cut " )
index = ET . SubElement ( cs , " index " , Value = " 4 " ) # yellow layer (YELLOW)
name = ET . SubElement ( cs , " name " , Value = " C04 " )
priority = ET . SubElement ( cs , " priority " , Value = " 3 " ) # is cut third
cs = ET . SubElement ( svg , " CutSetting " , Type = " Cut " )
index = ET . SubElement ( cs , " index " , Value = " 8 " ) # grey layer (WHITE)
name = ET . SubElement ( cs , " name " , Value = " C08 " )
priority = ET . SubElement ( cs , " priority " , Value = " 4 " ) # is cut fourth
cs = ET . SubElement ( svg , " CutSetting " , Type = " Cut " )
index = ET . SubElement ( cs , " index " , Value = " 1 " ) # blue layer (INNER_CUT)
name = ET . SubElement ( cs , " name " , Value = " Inner Cut " )
priority = ET . SubElement ( cs , " priority " , Value = " 5 " ) # is cut fifth
cs = ET . SubElement ( svg , " CutSetting " , Type = " Cut " )
index = ET . SubElement ( cs , " index " , Value = " 0 " ) # black layer (OUTER_CUT)
name = ET . SubElement ( cs , " name " , Value = " Outer Cut " )
priority = ET . SubElement ( cs , " priority " , Value = " 6 " ) # is cut sixth
cs = ET . SubElement ( svg , " CutSetting " , Type = " Tool " )
index = ET . SubElement ( cs , " index " , Value = " 30 " ) # T1 layer (ANNOTATIONS)
name = ET . SubElement ( cs , " name " , Value = " T1 " ) # tool layer do not support names
priority = ET . SubElement ( cs , " priority " , Value = " 7 " ) # is not cut at all
2022-03-27 10:40:38 +02:00
for i , part in enumerate ( self . parts ) :
if self . dbg : print ( " 7 " , num )
if not part . pathes :
continue
2022-04-16 18:27:01 +02:00
gp = ET . SubElement ( svg , " Shape " , Type = " Group " )
gp . text = " \n "
gp . tail = " \n "
children = ET . SubElement ( gp , " Children " )
children . text = " \n "
children . tail = " \n "
2022-03-27 10:40:38 +02:00
for j , path in enumerate ( part . pathes ) :
2022-06-05 11:17:50 +02:00
myColor = self . lbrn2_colors [ 4 * int ( path . params [ " rgb " ] [ 0 ] ) + 2 * int ( path . params [ " rgb " ] [ 1 ] ) + int ( path . params [ " rgb " ] [ 2 ] ) ]
2022-03-27 10:40:38 +02:00
p = [ ]
x , y = 0 , 0
C = " "
start = None
last = None
path . faster_edges ( inner_corners )
num = 0
cnt = 1
ende = len ( path . path ) - 1
if self . dbg :
for c in path . path :
print ( " 6 " , num , c )
num + = 1
2022-03-30 05:02:20 +02:00
num = 0
2022-03-27 10:40:38 +02:00
c = path . path [ num ]
C , x , y = c [ 0 : 3 ]
if self . dbg : print ( " ende: " , ende )
while num < ende or ( C == " T " and num < = ende ) : #len(path.path):
if self . dbg : print ( " 0 " , num )
c = path . path [ num ]
if self . dbg : print ( " first: " , num , c )
C , x , y = c [ 0 : 3 ]
if C == " M " :
if self . dbg : print ( " 1 " , num )
2022-06-05 11:17:50 +02:00
sh = ET . SubElement ( children , " Shape " , Type = " Path " , CutIndex = str ( myColor ) )
2022-03-27 10:40:38 +02:00
sh . text = " \n "
sh . tail = " \n "
vl = ET . SubElement ( sh , " VertList " )
vl . text = f " V { x : .3f } { y : .3f } c0x1c1x1 "
vl . tail = " \n "
pl = ET . SubElement ( sh , " PrimList " )
pl . text = " " #f"L{cnt} {cnt+1}"
pl . tail = " \n "
start = c
x0 , y0 = x , y
# do something with M
done = False
bspline = False
while done == False and num < ende : #len(path.path):
num + = 1
c = path . path [ num ]
if self . dbg : print ( " next: " , num , c )
C , x , y = c [ 0 : 3 ]
if C == " M " :
if start and points_equal ( start [ 1 ] , start [ 2 ] , x , y ) :
pl . text = " LineClosed "
start = c
cnt = 1
if self . dbg : print ( " next, because M " )
done = True
elif C == " T " :
if self . dbg : print ( " next, because T " )
done = True
else :
if C == " L " :
vl . text + = ( f " V { x : .3f } { y : .3f } c0x1c1x1 " )
pl . text + = f " L { cnt - 1 } { cnt } "
cnt + = 1
elif C == " C " :
x1 , y1 , x2 , y2 = c [ 3 : ]
if self . dbg : print ( " C: " , x0 , y0 , x1 , y1 , x , y , x2 , y2 )
vl . text + = ( f " V { x0 : .3f } { y0 : .3f } c0x { ( x1 ) : .3f } c0y { ( y1 ) : .3f } c1x1V { x : .3f } { y : .3f } c0x1c1x { ( x2 ) : .3f } c1y { ( y2 ) : .3f } " )
pl . text + = f " L { cnt - 1 } { cnt } B { cnt } { cnt + 1 } "
cnt + = 2
bspline = True
else :
print ( " unknown " , c )
if done == False :
x0 , y0 = x , y
if start and points_equal ( start [ 1 ] , start [ 2 ] , x0 , y0 ) :
if bspline == False :
pl . text = " LineClosed "
start = c
if self . dbg : print ( " 2 " , num )
elif C == " T " :
cnt = 1
#C = ""
if self . dbg : print ( " 3 " , num )
m , text , params = c [ 3 : ]
m = m * Affine . translation ( 0 , params [ ' fs ' ] )
if self . dbg : print ( " T: " , x , y , c )
num + = 1
font , bold , italic = params [ ' ff ' ]
if params . get ( ' font ' , ' Arial ' ) == ' Arial ' :
f = self . fonts [ font ]
else :
f = params . get ( ' font ' , ' Arial ' )
2022-06-05 11:17:50 +02:00
fontColor = self . lbrn2_colors [ 4 * int ( params [ " rgb " ] [ 0 ] ) + 2 * int ( params [ " rgb " ] [ 1 ] ) + int ( params [ " rgb " ] [ 2 ] ) ]
2022-03-27 10:40:38 +02:00
#alignment can be left|middle|end
if params . get ( ' align ' , ' left ' ) == ' middle ' :
hor = ' 1 '
else :
if params . get ( ' align ' , ' left ' ) == ' end ' :
hor = ' 2 '
else :
hor = ' 0 '
2022-06-05 11:17:50 +02:00
ver = 1 # vertical is always bottom, text is shifted in box class
2022-03-27 10:40:38 +02:00
pos = text . find ( ' % ' )
offs = 0
if pos > - 1 :
if self . dbg : print ( " p: " , pos , text [ pos + 1 : pos + 3 ] )
texttype = ' 2 '
if self . dbg : print ( " l " , len ( text [ pos + 1 : pos + 3 ] ) )
if text [ pos + 1 : pos + 2 ] . isnumeric ( ) :
if self . dbg : print ( " t0 " , text [ pos + 1 : pos + 3 ] )
if text [ pos + 1 : pos + 3 ] . isnumeric ( ) and len ( text [ pos + 1 : pos + 3 ] ) == 2 :
if self . dbg : print ( " t1 " )
if text [ pos : pos + 3 ] in txtOffset :
if self . dbg : print ( " t2 " )
offs = txtOffset [ text [ pos : pos + 3 ] ] + 1
else :
if self . dbg : print ( " t3 " )
offs = 0
txtOffset [ text [ pos : pos + 3 ] ] = offs
else :
if self . dbg : print ( " t4 " )
if text [ pos : pos + 2 ] in txtOffset :
if self . dbg : print ( " t5 " )
offs = txtOffset [ text [ pos : pos + 2 ] ] + 1
else :
offs = 0
if self . dbg : print ( " t6 " )
txtOffset [ text [ pos : pos + 2 ] ] = offs
else :
if self . dbg : print ( " t7 " )
texttype = ' 0 '
else :
texttype = ' 0 '
if self . dbg : print ( " t8 " )
if self . dbg : print ( " o: " , text , txtOffset , offs )
2022-03-28 20:55:22 +02:00
if not text :
if self . dbg : print ( " T: text with empty string - " , x , y , c )
else :
2022-04-16 18:27:01 +02:00
sh = ET . SubElement ( children , " Shape " , Type = " Text " , CutIndex = str ( fontColor ) , Font = f " { f } " , H = f " { ( params [ ' fs ' ] * 1.75 * 0.6086434 ) : .3f } " , Str = f " { text } " , Bold = f " { ' 1 ' if bold else ' 0 ' } " , Italic = f " { ' 1 ' if italic else ' 0 ' } " , Ah = f " { str ( hor ) } " , Av = f " { str ( ver ) } " , Eval = f " { texttype } " , VariableOffset = f " { str ( offs ) } " ) # 1mm = 1.75 Lightburn H units
2022-03-28 20:55:22 +02:00
sh . text = " \n "
sh . tail = " \n "
xf = ET . SubElement ( sh , " XForm " )
2023-01-13 15:32:32 +01:00
xf . text = " " . join ( f " { m [ i ] : .3f } " for i in ( 0 , 3 , 1 , 4 , 2 , 5 ) )
2022-03-28 20:55:22 +02:00
xf . tail = " \n "
2022-03-27 10:40:38 +02:00
else :
if self . dbg : print ( " 4 " , num )
print ( " next, because not M " )
num + = 1
url = self . metadata [ " url " ] . replace ( " &render=1 " , " " ) # remove render argument to get web form again
pl = ET . SubElement ( svg , " Notes " , ShowOnLoad = " 1 " , Notes = " File created by Boxes.py script, programmed by Florian Festi. \n Lightburn output by Klaus Steinhammer. \n \n URL with settings: \n " + str ( url ) )
pl . text = " "
pl . tail = " \n "
if self . dbg : print ( " 5 " , num )
2022-12-30 16:33:39 +01:00
tree . write ( open ( self . _fname , " wb " ) , encoding = " utf-8 " , xml_declaration = True , method = " xml " )
2019-11-23 18:38:23 +01:00
from random import random
def random_svg_color ( ) :
r , g , b = random ( ) , random ( ) , random ( )
return f " rgb( { r * 255 : .0f } , { g * 255 : .0f } , { b * 255 : .0f } ) "
def rgb_to_svg_color ( r , g , b ) :
return f " rgb( { r * 255 : .0f } , { g * 255 : .0f } , { b * 255 : .0f } ) "
def line_intersection ( line1 , line2 ) :
xdiff = ( line1 [ 0 ] [ 0 ] - line1 [ 1 ] [ 0 ] , line2 [ 0 ] [ 0 ] - line2 [ 1 ] [ 0 ] )
ydiff = ( line1 [ 0 ] [ 1 ] - line1 [ 1 ] [ 1 ] , line2 [ 0 ] [ 1 ] - line2 [ 1 ] [ 1 ] )
def det ( a , b ) :
return a [ 0 ] * b [ 1 ] - a [ 1 ] * b [ 0 ]
div = det ( xdiff , ydiff )
if div == 0 :
2023-01-02 00:32:42 +01:00
# todo: deal with parallel line intersection / overlay
2019-11-23 18:38:23 +01:00
return False , None , None
d = ( det ( * line1 ) , det ( * line2 ) )
x = det ( d , xdiff ) / div
y = det ( d , ydiff ) / div
on_segments = (
( x + EPS > = min ( line1 [ 0 ] [ 0 ] , line1 [ 1 ] [ 0 ] ) ) ,
( x + EPS > = min ( line2 [ 0 ] [ 0 ] , line2 [ 1 ] [ 0 ] ) ) ,
( x - EPS < = max ( line1 [ 0 ] [ 0 ] , line1 [ 1 ] [ 0 ] ) ) ,
( x - EPS < = max ( line2 [ 0 ] [ 0 ] , line2 [ 1 ] [ 0 ] ) ) ,
( y + EPS > = min ( line1 [ 0 ] [ 1 ] , line1 [ 1 ] [ 1 ] ) ) ,
( y + EPS > = min ( line2 [ 0 ] [ 1 ] , line2 [ 1 ] [ 1 ] ) ) ,
( y - EPS < = max ( line1 [ 0 ] [ 1 ] , line1 [ 1 ] [ 1 ] ) ) ,
( y - EPS < = max ( line2 [ 0 ] [ 1 ] , line2 [ 1 ] [ 1 ] ) ) ,
)
return min ( on_segments ) , x , y