'''SVG Write in TouchDesigner Class
Authors | matthew ragan
matthewragan.com
v2 Additional features/fixes added by:
kris northern - IG @krisnorthern
New Features:
I added a viewBox parameter to the export to address issues stemming
from how Inkscape and Illustrator differently handle the "User Units"
built into the SVG spec (this would result in the same SVG displaying
very differently in the two programs. The problem stems from Inkscape
using 96DPI as its native pixel space and Illustrator using 72DPI).
Issues: Currently the camera mode is only set to orthographic.
'''
import svgwrite
import numpy as np
import math
import webbrowser
CanvasWidth = float(parent.svg.par.Canvasmm1)
CanvasHeight = float(parent.svg.par.Canvasmm2)
xOffset = float(parent.svg.par.Canvasmm1)/2
yOffset = float(parent.svg.par.Canvasmm2)/2
strokeWidth = float(op('strokeWidth')['width'])*.30
strokeR = int(float(parent.svg.par.Rgbr*255))
strokeG = int(float(parent.svg.par.Rgbg*255))
strokeB = int(float(parent.svg.par.Rgbb*255))
Zoom = 1
Scalar = float(parent.svg.par.Canvasmm2) * Zoom
class Soptosvg:
'''
This class is intended to handle writing SVGs from SOPs in TouchDesigner.
'''
def __init__( self ):
''' This is the init method for the Soptosvg process
'''
self.Polylinesop = parent.svg.par.Polylinesop
self.Polygonsop = parent.svg.par.Polygonsop
self.Camera = parent.svg.par.Camera
self.UseCamera = parent.svg.par.Usecamera
self.Aspect = (parent.svg.par.Aspect1, parent.svg.par.Aspect2)
self.Svgtype = parent.svg.par.Svgtype
self.Filepath = "{dir}/{file}.svg"
self.Axidocumentation = "<http://wiki.evilmadscientist.com/AxiDraw>"
self.Axipdf = "<http://cdn.evilmadscientist.com/wiki/axidraw/software/AxiDraw_V33.pdf>"
self.Svgwritedocumentation = "<http://svgwrite.readthedocs.io/en/latest/svgwrite.html>"
print( "Sop to SVG Initialized" )
return
def WorldToCam(self, oldP):
'''Method to convert worldspace coords to cameraspace coords.
Args
-------------
oldP (tdu.Position) : the tdu.Position to convert to camera space.
Returns
-------------
newP (tuple) : tuple of x,y coordinates after camera projection.
'''
camera = op(self.Camera.eval())
view = camera.transform()
view.invert()
pers = camera.projection( self.Aspect[0].eval(), self.Aspect[1].eval() )
viewP = view * oldP
adjusted = pers * viewP
newX = adjusted.x/adjusted.z
newY = adjusted.y/adjusted.z
camScale = .48
if self.UseCamera:
newP = ((newX *camScale*CanvasWidth)+yOffset, (-newY *camScale*CanvasHeight)+yOffset)
else:
newP = ((newX *camScale)+yOffset, (-newY *camScale)+yOffset)
return newP
def Canvas_size(self):
''' This is a helper method to return the dimensions of the canvas.
Returns
---------------
canvassize (tupple) : a tupple of width and height dimensions measured in millimeters
'''
canvassize = (f'{CanvasWidth}mm',f'{CanvasHeight}mm')
return canvassize
def viewBox_size(self):
viewboxsize = (f'0, 0, {CanvasWidth}, {CanvasHeight}')
return viewboxsize
def SavePolyline(self, path, pline):
''' Polyline is an unclosed list of points that will create a single unfilled line
Returns
---------------
formatted_profile (str) : A formatted string populated with the with the supplied information
'''
prims = pline.prims
Canvassize = self.Canvas_size()
viewBoxDims = self.viewBox_size()
dwg = svgwrite.Drawing(path, profile='tiny', size=Canvassize , viewBox=viewBoxDims)
for item in prims:
if self.UseCamera:
newPoints = [self.WorldToCam(vert.point.P) for vert in item ]
else:
newPoints = [((vert.point.x*Scalar)+xOffset , (-vert.point.y*Scalar)+yOffset) for vert in item ]
newPoly = dwg.polyline(points=newPoints, stroke=svgwrite.rgb(strokeR, strokeG, strokeB), stroke_width=strokeWidth, fill='none')
dwg.add(newPoly)
dwg.save()
return
def SavePolygon(self, path, pgon):
''' This is a sample method.
Returns
---------------
formatted_profile (str) : A formatted string populated with the with the supplied information
'''
prims = pgon.prims
Canvassize = self.Canvas_size()
viewBoxDims = self.viewBox_size()
dwg = svgwrite.Drawing(path, profile='tiny', size=Canvassize , viewBox=viewBoxDims)
for item in prims:
if self.UseCamera:
newPoints = [self.WorldToCam(vert.point.P) for vert in item ]
else:
newPoints = [((vert.point.x*Scalar)+xOffset,(-vert.point.y*Scalar)+yOffset) for vert in item ]
newPoly = dwg.polyline(points=newPoints, stroke=svgwrite.rgb(strokeR, strokeG, strokeB), stroke_width=strokeWidth, fill='none')
dwg.add(newPoly)
dwg.save()
def SavePolygonAndPolygon(self, path, pline, pgon):
''' This is a sample method.
This sample method is intended to help illustrate what method docstrings should look like.
Notes
---------------
'self' does not need to be included in the Args section.
Args
---------------
name (str): A string name, with spaces as underscores
age (int): Age as full year measurements
height (float): Height in meters to 2 significant digits, ex: 1.45
Examples
---------------
Returns
---------------
formatted_profile (str) : A formatted string populated with the with the supplied information
'''
prims = pline.prims
pgonPrims = pgon.prims
plinePrims = pline.prims
Canvassize = self.Canvas_size()
viewBoxDims = self.viewBox_size()
dwg = svgwrite.Drawing(path, profile='tiny', size=Canvassize , viewBox=viewBoxDims)
for item in pgonPrims:
if self.UseCamera:
newPoints = [self.WorldToCam(vert.point.P) for vert in item ]
else:
newPoints = [((vert.point.x*Scalar)+xOffset,(-vert.point.y*Scalar)+yOffset) for vert in item ]
newPoly = dwg.polyline(points=newPoints, stroke=svgwrite.rgb(strokeR, strokeG, strokeB), stroke_width=strokeWidth, fill='none')
dwg.add(newPoly)
for item in plinePrims:
if self.UseCamera:
newPoints = [self.WorldToCam(vert.point.P) for vert in item ]
else:
newPoints = [((vert.point.x*Scalar)+xOffset,(-vert.point.y*Scalar)+yOffset) for vert in item ]
newPoly = dwg.polyline(points=newPoints, stroke=svgwrite.rgb(strokeR, strokeG, strokeB), stroke_width=strokeWidth, fill='none')
dwg.add(newPoly)
dwg.save()
return
def Par_check(self, svg_type):
''' Par_check() is an error handling method.
Par_check aims to ensrue that all parameters are correctly set up so we can advance to the
steps of creating our SVGs. This means checking to ensure that all needed fields are
completed in the TOX. If we pass all of the par check tests then we can move on to
writing our SVG file to disk.
Notes
---------------
'self' does not need to be included in the Args section.
Args
---------------
svg_type (str): the string name for the inteded type of output - polygon, polyline, both
Returns
---------------
ready (bool) : the results of a set of logical checks to that ensures all requisite
pars have been supplied for a sucessful write to disk for the file.
'''
ready = False
title = "We're off the RAILS!"
message = '''Hey there, things don't look totally right.
Check on these parameters to make sure everything is in order:\\n{}'''
buttons = ['okay']
checklist = []
# error handling for geometry permutations
# handling polyline saving
if self.Svgtype == 'pline':
if self.Polylinesop != None and op(self.Polylinesop).isSOP:
pass
else:
checklist.append( 'Missing Polygon SOP' )
# handling polygon saving
elif self.Svgtype == 'pgon':
if self.Polygonsop != None and op(self.Polygonsop).isSOP:
pass
else:
checklist.append( 'Missing Polyline SOP' )
# handling combined objects - polyline and polygon saving
elif self.Svgtype == 'both':
polyline = self.Polylinesop != None and op(self.Polylinesop).isSOP
polygon = self.Polygonsop != None and op(self.Polygonsop).isSOP
# both sops are present
if polyline and polygon:
pass
# missing polyline sop
elif polygon and not polyline:
checklist.append( 'Missing Polyline SOP' )
# missing polygon sop
elif polyline and not polygon:
checklist.append( 'Missing Polygon SOP' )
# missing both polyline and polygon sops
elif not polyline and not polygon:
checklist.append( 'Missing Polygon SOP')
checklist.append( 'Missing Polyline SOP')
# handling to check for a directory path
if parent.svg.par.Dir == None or parent.svg.par.Dir.val == '':
checklist.append( 'Missing Directory Path' )
else:
pass
# handling to check for a file path
if parent.svg.par.Filename == None or parent.svg.par.Filename.val == '':
checklist.append( 'Missing File name' )
else:
pass
# we're in the clear, everything is ready to go
if len(checklist) == 0:
ready = True
# correctly format message for ui.messageBox and warn user about missing elements
else:
ready = False
messageChecklist = '\\n'
for item in checklist:
messageChecklist += ' * {}\\n'.format(item)
message = message.format(messageChecklist)
ui.messageBox(title, message, buttons=buttons)
return ready
def Save(self):
''' This is the Save method, used to start the process of writing the svg to disk.
Based on settings in the tox's parameters the Save() method will utilize other
helper methods to correctly save out the file. Pragmatically, this means first
ensuring that all pars are correctly set up (error prevention), then the
appropriate calling of other methods to ensure that geometry is correclty
written to file.
Notes
---------------
none
Args
---------------
none
Returns
---------------
none
'''
# get the svg type
svgtype = self.Svgtype
# start with Par_check to see if we're ready to proced.
readyToContinue = self.Par_check( svgtype )
if readyToContinue:
filepath = self.Filepath.format( dir=parent.svg.par.Dir,
file=parent.svg.par.Filename)
if svgtype == 'pline':
self.SavePolyline( path=filepath,
pline=op(self.Polylinesop))
elif svgtype == 'pgon':
self.SavePolygon( path=filepath,
pgon=op(self.Polygonsop))
elif svgtype == 'both':
self.SavePolygonAndPolygon( path=filepath,
pline=op(self.Polylinesop),
pgon=op(self.Polygonsop))
else:
print("Woah... something is very wrong")
pass
print(filepath)
print(self.Polylinesop)
print(self.Svgtype)
else:
pass
return