mirror of
https://github.com/blender/blender-addons.git
synced 2025-08-20 13:22:58 +00:00
1920 lines
50 KiB
Python
1920 lines
50 KiB
Python
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
#
|
|
# 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.
|
|
#
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
# <pep8 compliant>
|
|
|
|
import re
|
|
import xml.dom.minidom
|
|
from math import cos, sin, tan, atan2, pi, ceil
|
|
|
|
import bpy
|
|
from mathutils import Vector, Matrix
|
|
|
|
from . import svg_colors
|
|
|
|
#### Common utilities ####
|
|
|
|
# TODO: "em" and "ex" aren't actually supported
|
|
SVGUnits = {"": 1.0,
|
|
"px": 1.0,
|
|
"in": 90.0,
|
|
"mm": 90.0 / 25.4,
|
|
"cm": 90.0 / 2.54,
|
|
"pt": 1.25,
|
|
"pc": 15.0,
|
|
"em": 1.0,
|
|
"ex": 1.0,
|
|
"INVALID": 1.0, # some DocBook files contain this
|
|
}
|
|
|
|
SVGEmptyStyles = {'useFill': None,
|
|
'fill': None}
|
|
|
|
def srgb_to_linearrgb(c):
|
|
if c < 0.04045:
|
|
return 0.0 if c < 0.0 else c * (1.0 / 12.92);
|
|
else:
|
|
return pow((c + 0.055) * (1.0 / 1.055), 2.4);
|
|
|
|
|
|
def SVGParseFloat(s, i=0):
|
|
"""
|
|
Parse first float value from string
|
|
|
|
Returns value as string
|
|
"""
|
|
|
|
start = i
|
|
n = len(s)
|
|
token = ''
|
|
|
|
# Skip leading whitespace characters
|
|
while i < n and (s[i].isspace() or s[i] == ','):
|
|
i += 1
|
|
|
|
if i == n:
|
|
return None, i
|
|
|
|
# Read sign
|
|
if s[i] == '-':
|
|
token += '-'
|
|
i += 1
|
|
elif s[i] == '+':
|
|
i += 1
|
|
|
|
# Read integer part
|
|
if s[i].isdigit():
|
|
while i < n and s[i].isdigit():
|
|
token += s[i]
|
|
i += 1
|
|
|
|
# Fractional part
|
|
if i < n and s[i] == '.':
|
|
token += '.'
|
|
i += 1
|
|
|
|
if s[i].isdigit():
|
|
while i < n and s[i].isdigit():
|
|
token += s[i]
|
|
i += 1
|
|
elif s[i].isspace() or s[i] == ',':
|
|
# Inkscape sometimes uses weird float format with missed
|
|
# fractional part after dot. Suppose zero fractional part
|
|
# for this case
|
|
pass
|
|
else:
|
|
raise Exception('Invalid float value near ' + s[start:start + 10])
|
|
|
|
# Degree
|
|
if i < n and (s[i] == 'e' or s[i] == 'E'):
|
|
token += s[i]
|
|
i += 1
|
|
if s[i] == '+' or s[i] == '-':
|
|
token += s[i]
|
|
i += 1
|
|
|
|
if s[i].isdigit():
|
|
while i < n and s[i].isdigit():
|
|
token += s[i]
|
|
i += 1
|
|
else:
|
|
raise Exception('Invalid float value near ' + s[start:start + 10])
|
|
|
|
return token, i
|
|
|
|
|
|
def SVGCreateCurve(context):
|
|
"""
|
|
Create new curve object to hold splines in
|
|
"""
|
|
|
|
cu = bpy.data.curves.new("Curve", 'CURVE')
|
|
obj = bpy.data.objects.new("Curve", cu)
|
|
|
|
context['collection'].objects.link(obj)
|
|
|
|
return obj
|
|
|
|
|
|
def SVGFinishCurve():
|
|
"""
|
|
Finish curve creation
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
def SVGFlipHandle(x, y, x1, y1):
|
|
"""
|
|
Flip handle around base point
|
|
"""
|
|
|
|
x = x + (x - x1)
|
|
y = y + (y - y1)
|
|
|
|
return x, y
|
|
|
|
|
|
def SVGParseCoord(coord, size):
|
|
"""
|
|
Parse coordinate component to common basis
|
|
|
|
Needed to handle coordinates set in cm, mm, iches..
|
|
"""
|
|
|
|
token, last_char = SVGParseFloat(coord)
|
|
val = float(token)
|
|
unit = coord[last_char:].strip() # strip() in case there is a space
|
|
|
|
if unit == '%':
|
|
return float(size) / 100.0 * val
|
|
else:
|
|
return val * SVGUnits[unit]
|
|
|
|
return val
|
|
|
|
|
|
def SVGRectFromNode(node, context):
|
|
"""
|
|
Get display rectangle from node
|
|
"""
|
|
|
|
w = context['rect'][0]
|
|
h = context['rect'][1]
|
|
|
|
if node.getAttribute('viewBox'):
|
|
viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
|
|
w = SVGParseCoord(viewBox[2], w)
|
|
h = SVGParseCoord(viewBox[3], h)
|
|
else:
|
|
if node.getAttribute('width'):
|
|
w = SVGParseCoord(node.getAttribute('width'), w)
|
|
|
|
if node.getAttribute('height'):
|
|
h = SVGParseCoord(node.getAttribute('height'), h)
|
|
|
|
return (w, h)
|
|
|
|
|
|
def SVGMatrixFromNode(node, context):
|
|
"""
|
|
Get transformation matrix from given node
|
|
"""
|
|
|
|
tagName = node.tagName.lower()
|
|
tags = ['svg:svg', 'svg:use', 'svg:symbol']
|
|
|
|
if tagName not in tags and 'svg:' + tagName not in tags:
|
|
return Matrix()
|
|
|
|
rect = context['rect']
|
|
has_user_coordinate = (len(context['rects']) > 1)
|
|
|
|
m = Matrix()
|
|
x = SVGParseCoord(node.getAttribute('x') or '0', rect[0])
|
|
y = SVGParseCoord(node.getAttribute('y') or '0', rect[1])
|
|
w = SVGParseCoord(node.getAttribute('width') or str(rect[0]), rect[0])
|
|
h = SVGParseCoord(node.getAttribute('height') or str(rect[1]), rect[1])
|
|
|
|
m = Matrix.Translation(Vector((x, y, 0.0)))
|
|
if has_user_coordinate:
|
|
if rect[0] != 0 and rect[1] != 0:
|
|
m = m @ Matrix.Scale(w / rect[0], 4, Vector((1.0, 0.0, 0.0)))
|
|
m = m @ Matrix.Scale(h / rect[1], 4, Vector((0.0, 1.0, 0.0)))
|
|
|
|
if node.getAttribute('viewBox'):
|
|
viewBox = node.getAttribute('viewBox').replace(',', ' ').split()
|
|
vx = SVGParseCoord(viewBox[0], w)
|
|
vy = SVGParseCoord(viewBox[1], h)
|
|
vw = SVGParseCoord(viewBox[2], w)
|
|
vh = SVGParseCoord(viewBox[3], h)
|
|
|
|
if vw == 0 or vh == 0:
|
|
return m
|
|
|
|
if has_user_coordinate or (w != 0 and h != 0):
|
|
sx = w / vw
|
|
sy = h / vh
|
|
scale = min(sx, sy)
|
|
else:
|
|
scale = 1.0
|
|
w = vw
|
|
h = vh
|
|
|
|
tx = (w - vw * scale) / 2
|
|
ty = (h - vh * scale) / 2
|
|
m = m @ Matrix.Translation(Vector((tx, ty, 0.0)))
|
|
|
|
m = m @ Matrix.Translation(Vector((-vx, -vy, 0.0)))
|
|
m = m @ Matrix.Scale(scale, 4, Vector((1.0, 0.0, 0.0)))
|
|
m = m @ Matrix.Scale(scale, 4, Vector((0.0, 1.0, 0.0)))
|
|
|
|
return m
|
|
|
|
|
|
def SVGParseTransform(transform):
|
|
"""
|
|
Parse transform string and return transformation matrix
|
|
"""
|
|
|
|
m = Matrix()
|
|
r = re.compile('\s*([A-z]+)\s*\((.*?)\)')
|
|
|
|
for match in r.finditer(transform):
|
|
func = match.group(1)
|
|
params = match.group(2)
|
|
params = params.replace(',', ' ').split()
|
|
|
|
proc = SVGTransforms.get(func)
|
|
if proc is None:
|
|
raise Exception('Unknown trasnform function: ' + func)
|
|
|
|
m = m @ proc(params)
|
|
|
|
return m
|
|
|
|
|
|
def SVGGetMaterial(color, context):
|
|
"""
|
|
Get material for specified color
|
|
"""
|
|
|
|
materials = context['materials']
|
|
rgb_re = re.compile('^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,(\d+)\s*\)\s*$')
|
|
|
|
if color in materials:
|
|
return materials[color]
|
|
|
|
diff = None
|
|
if color.startswith('#'):
|
|
color = color[1:]
|
|
|
|
if len(color) == 3:
|
|
color = color[0] * 2 + color[1] * 2 + color[2] * 2
|
|
|
|
diff = (int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16))
|
|
elif color in svg_colors.SVGColors:
|
|
diff = svg_colors.SVGColors[color]
|
|
elif rgb_re.match(color):
|
|
c = rgb_re.findall(color)[0]
|
|
diff = (float(c[0]), float(c[1]), float(c[2]))
|
|
else:
|
|
return None
|
|
|
|
diffuse_color = ([x / 255.0 for x in diff])
|
|
|
|
if context['do_colormanage']:
|
|
diffuse_color[0] = srgb_to_linearrgb(diffuse_color[0])
|
|
diffuse_color[1] = srgb_to_linearrgb(diffuse_color[1])
|
|
diffuse_color[2] = srgb_to_linearrgb(diffuse_color[2])
|
|
|
|
mat = bpy.data.materials.new(name='SVGMat')
|
|
mat.diffuse_color = diffuse_color
|
|
|
|
materials[color] = mat
|
|
|
|
return mat
|
|
|
|
|
|
def SVGTransformTranslate(params):
|
|
"""
|
|
translate SVG transform command
|
|
"""
|
|
|
|
tx = float(params[0])
|
|
ty = float(params[1]) if len(params) > 1 else 0.0
|
|
|
|
return Matrix.Translation(Vector((tx, ty, 0.0)))
|
|
|
|
|
|
def SVGTransformMatrix(params):
|
|
"""
|
|
matrix SVG transform command
|
|
"""
|
|
|
|
a = float(params[0])
|
|
b = float(params[1])
|
|
c = float(params[2])
|
|
d = float(params[3])
|
|
e = float(params[4])
|
|
f = float(params[5])
|
|
|
|
return Matrix(((a, c, 0.0, e),
|
|
(b, d, 0.0, f),
|
|
(0, 0, 1.0, 0),
|
|
(0, 0, 0.0, 1)))
|
|
|
|
|
|
def SVGTransformScale(params):
|
|
"""
|
|
scale SVG transform command
|
|
"""
|
|
|
|
sx = float(params[0])
|
|
sy = float(params[1]) if len(params) > 1 else sx
|
|
|
|
m = Matrix()
|
|
|
|
m = m @ Matrix.Scale(sx, 4, Vector((1.0, 0.0, 0.0)))
|
|
m = m @ Matrix.Scale(sy, 4, Vector((0.0, 1.0, 0.0)))
|
|
|
|
return m
|
|
|
|
|
|
def SVGTransformSkewX(params):
|
|
"""
|
|
skewX SVG transform command
|
|
"""
|
|
|
|
ang = float(params[0]) * pi / 180.0
|
|
|
|
return Matrix(((1.0, 0.0, 0.0),
|
|
(tan(ang), 1.0, 0.0),
|
|
(0.0, 0.0, 1.0))).to_4x4()
|
|
|
|
|
|
def SVGTransformSkewY(params):
|
|
"""
|
|
skewX SVG transform command
|
|
"""
|
|
|
|
ang = float(params[0]) * pi / 180.0
|
|
|
|
return Matrix(((1.0, tan(ang), 0.0),
|
|
(0.0, 1.0, 0.0),
|
|
(0.0, 0.0, 1.0))).to_4x4()
|
|
|
|
|
|
def SVGTransformRotate(params):
|
|
"""
|
|
skewX SVG transform command
|
|
"""
|
|
|
|
ang = float(params[0]) * pi / 180.0
|
|
cx = cy = 0.0
|
|
|
|
if len(params) >= 3:
|
|
cx = float(params[1])
|
|
cy = float(params[2])
|
|
|
|
tm = Matrix.Translation(Vector((cx, cy, 0.0)))
|
|
rm = Matrix.Rotation(ang, 4, Vector((0.0, 0.0, 1.0)))
|
|
|
|
return tm @ rm @ tm.inverted()
|
|
|
|
SVGTransforms = {'translate': SVGTransformTranslate,
|
|
'scale': SVGTransformScale,
|
|
'skewX': SVGTransformSkewX,
|
|
'skewY': SVGTransformSkewY,
|
|
'matrix': SVGTransformMatrix,
|
|
'rotate': SVGTransformRotate}
|
|
|
|
|
|
def SVGParseStyles(node, context):
|
|
"""
|
|
Parse node to get different styles for displaying geometries
|
|
(materilas, filling flags, etc..)
|
|
"""
|
|
|
|
styles = SVGEmptyStyles.copy()
|
|
|
|
style = node.getAttribute('style')
|
|
if style:
|
|
elems = style.split(';')
|
|
for elem in elems:
|
|
s = elem.split(':')
|
|
|
|
if len(s) != 2:
|
|
continue
|
|
|
|
name = s[0].strip().lower()
|
|
val = s[1].strip()
|
|
|
|
if name == 'fill':
|
|
val = val.lower()
|
|
if val == 'none':
|
|
styles['useFill'] = False
|
|
else:
|
|
styles['useFill'] = True
|
|
styles['fill'] = SVGGetMaterial(val, context)
|
|
|
|
if styles['useFill'] is None:
|
|
styles['useFill'] = True
|
|
styles['fill'] = SVGGetMaterial('#000', context)
|
|
|
|
return styles
|
|
|
|
if styles['useFill'] is None:
|
|
fill = node.getAttribute('fill')
|
|
if fill:
|
|
fill = fill.lower()
|
|
if fill == 'none':
|
|
styles['useFill'] = False
|
|
else:
|
|
styles['useFill'] = True
|
|
styles['fill'] = SVGGetMaterial(fill, context)
|
|
|
|
if styles['useFill'] is None and context['style']:
|
|
styles = context['style'].copy()
|
|
|
|
if styles['useFill'] is None:
|
|
styles['useFill'] = True
|
|
styles['fill'] = SVGGetMaterial('#000', context)
|
|
|
|
return styles
|
|
|
|
#### SVG path helpers ####
|
|
|
|
|
|
class SVGPathData:
|
|
"""
|
|
SVG Path data token supplier
|
|
"""
|
|
|
|
__slots__ = ('_data', # List of tokens
|
|
'_index', # Index of current token in tokens list
|
|
'_len') # Length of tokens list
|
|
|
|
def __init__(self, d):
|
|
"""
|
|
Initialize new path data supplier
|
|
|
|
d - the definition of the outline of a shape
|
|
"""
|
|
|
|
spaces = ' ,\t'
|
|
commands = {'m', 'l', 'h', 'v', 'c', 's', 'q', '', 't', 'a', 'z'}
|
|
tokens = []
|
|
|
|
i = 0
|
|
n = len(d)
|
|
while i < n:
|
|
c = d[i]
|
|
|
|
if c in spaces:
|
|
pass
|
|
elif c.lower() in commands:
|
|
tokens.append(c)
|
|
elif c in ['-', '.'] or c.isdigit():
|
|
token, last_char = SVGParseFloat(d, i)
|
|
tokens.append(token)
|
|
|
|
# in most cases len(token) and (last_char - i) are the same
|
|
# but with whitespace or ',' prefix they are not.
|
|
|
|
i += (last_char - i) - 1
|
|
|
|
i += 1
|
|
|
|
self._data = tokens
|
|
self._index = 0
|
|
self._len = len(tokens)
|
|
|
|
def eof(self):
|
|
"""
|
|
Check if end of data reached
|
|
"""
|
|
|
|
return self._index >= self._len
|
|
|
|
def cur(self):
|
|
"""
|
|
Return current token
|
|
"""
|
|
|
|
if self.eof():
|
|
return None
|
|
|
|
return self._data[self._index]
|
|
|
|
def lookupNext(self):
|
|
"""
|
|
get next token without moving pointer
|
|
"""
|
|
|
|
if self.eof():
|
|
return None
|
|
|
|
return self._data[self._index]
|
|
|
|
def next(self):
|
|
"""
|
|
Return current token and go to next one
|
|
"""
|
|
|
|
if self.eof():
|
|
return None
|
|
|
|
token = self._data[self._index]
|
|
self._index += 1
|
|
|
|
return token
|
|
|
|
def nextCoord(self):
|
|
"""
|
|
Return coordinate created from current token and move to next token
|
|
"""
|
|
|
|
token = self.next()
|
|
|
|
if token is None:
|
|
return None
|
|
|
|
return float(token)
|
|
|
|
|
|
class SVGPathParser:
|
|
"""
|
|
Parser of SVG path data
|
|
"""
|
|
|
|
__slots__ = ('_data', # Path data supplird
|
|
'_point', # Current point coorfinate
|
|
'_handle', # Last handle coordinate
|
|
'_splines', # List of all splies created during parsing
|
|
'_spline', # Currently handling spline
|
|
'_commands', # Hash of all supported path commands
|
|
'_use_fill', # Splines would be filled, so expected to be closed
|
|
)
|
|
|
|
def __init__(self, d, use_fill):
|
|
"""
|
|
Initialize path parser
|
|
|
|
d - the definition of the outline of a shape
|
|
"""
|
|
|
|
self._data = SVGPathData(d)
|
|
self._point = None # Current point
|
|
self._handle = None # Last handle
|
|
self._splines = [] # List of splines in path
|
|
self._spline = None # Current spline
|
|
self._use_fill = use_fill
|
|
|
|
self._commands = {'M': self._pathMoveTo,
|
|
'L': self._pathLineTo,
|
|
'H': self._pathLineTo,
|
|
'V': self._pathLineTo,
|
|
'C': self._pathCurveToCS,
|
|
'S': self._pathCurveToCS,
|
|
'Q': self._pathCurveToQT,
|
|
'T': self._pathCurveToQT,
|
|
'A': self._pathCurveToA,
|
|
'Z': self._pathClose,
|
|
|
|
'm': self._pathMoveTo,
|
|
'l': self._pathLineTo,
|
|
'h': self._pathLineTo,
|
|
'v': self._pathLineTo,
|
|
'c': self._pathCurveToCS,
|
|
's': self._pathCurveToCS,
|
|
'q': self._pathCurveToQT,
|
|
't': self._pathCurveToQT,
|
|
'a': self._pathCurveToA,
|
|
'z': self._pathClose}
|
|
|
|
def _getCoordPair(self, relative, point):
|
|
"""
|
|
Get next coordinate pair
|
|
"""
|
|
|
|
x = self._data.nextCoord()
|
|
y = self._data.nextCoord()
|
|
|
|
if relative and point is not None:
|
|
x += point[0]
|
|
y += point[1]
|
|
|
|
return x, y
|
|
|
|
def _appendPoint(self, x, y, handle_left=None, handle_left_type='VECTOR',
|
|
handle_right=None, handle_right_type='VECTOR'):
|
|
"""
|
|
Append point to spline
|
|
|
|
If there's no active spline, create one and set it's first point
|
|
to current point coordinate
|
|
"""
|
|
|
|
if self._spline is None:
|
|
self._spline = {'points': [],
|
|
'closed': False}
|
|
|
|
self._splines.append(self._spline)
|
|
|
|
if len(self._spline['points']) > 0:
|
|
# Not sure about specifications, but Illustrator could create
|
|
# last point at the same position, as start point (which was
|
|
# reached by MoveTo command) to set needed handle coords.
|
|
# It's also could use last point at last position to make path
|
|
# filled.
|
|
|
|
first = self._spline['points'][0]
|
|
if abs(first['x'] - x) < 1e-6 and abs(first['y'] - y) < 1e-6:
|
|
if handle_left is not None:
|
|
first['handle_left'] = handle_left
|
|
first['handle_left_type'] = 'FREE'
|
|
|
|
if handle_left_type != 'VECTOR':
|
|
first['handle_left_type'] = handle_left_type
|
|
|
|
if self._data.eof() or self._data.lookupNext().lower() == 'm':
|
|
self._spline['closed'] = True
|
|
|
|
return
|
|
|
|
last = self._spline['points'][-1]
|
|
if last['handle_right_type'] == 'VECTOR' and handle_left_type == 'FREE':
|
|
last['handle_right'] = (last['x'], last['y'])
|
|
last['handle_right_type'] = 'FREE'
|
|
|
|
point = {'x': x,
|
|
'y': y,
|
|
|
|
'handle_left': handle_left,
|
|
'handle_left_type': handle_left_type,
|
|
|
|
'handle_right': handle_right,
|
|
'handle_right_type': handle_right_type}
|
|
|
|
self._spline['points'].append(point)
|
|
|
|
def _updateHandle(self, handle=None, handle_type=None):
|
|
"""
|
|
Update right handle of previous point when adding new point to spline
|
|
"""
|
|
|
|
point = self._spline['points'][-1]
|
|
|
|
if handle_type is not None:
|
|
point['handle_right_type'] = handle_type
|
|
|
|
if handle is not None:
|
|
point['handle_right'] = handle
|
|
|
|
def _pathMoveTo(self, code):
|
|
"""
|
|
MoveTo path command
|
|
"""
|
|
|
|
relative = code.islower()
|
|
x, y = self._getCoordPair(relative, self._point)
|
|
|
|
self._spline = None # Flag to start new spline
|
|
self._point = (x, y)
|
|
|
|
cur = self._data.cur()
|
|
while cur is not None and not cur.isalpha():
|
|
x, y = self._getCoordPair(relative, self._point)
|
|
|
|
if self._spline is None:
|
|
self._appendPoint(self._point[0], self._point[1])
|
|
|
|
self._appendPoint(x, y)
|
|
|
|
self._point = (x, y)
|
|
cur = self._data.cur()
|
|
|
|
self._handle = None
|
|
|
|
def _pathLineTo(self, code):
|
|
"""
|
|
LineTo path command
|
|
"""
|
|
|
|
c = code.lower()
|
|
|
|
cur = self._data.cur()
|
|
while cur is not None and not cur.isalpha():
|
|
if c == 'l':
|
|
x, y = self._getCoordPair(code == 'l', self._point)
|
|
elif c == 'h':
|
|
x = self._data.nextCoord()
|
|
y = self._point[1]
|
|
else:
|
|
x = self._point[0]
|
|
y = self._data.nextCoord()
|
|
|
|
if code == 'h':
|
|
x += self._point[0]
|
|
elif code == 'v':
|
|
y += self._point[1]
|
|
|
|
if self._spline is None:
|
|
self._appendPoint(self._point[0], self._point[1])
|
|
|
|
self._appendPoint(x, y)
|
|
|
|
self._point = (x, y)
|
|
cur = self._data.cur()
|
|
|
|
self._handle = None
|
|
|
|
def _pathCurveToCS(self, code):
|
|
"""
|
|
Cubic BEZIER CurveTo path command
|
|
"""
|
|
|
|
c = code.lower()
|
|
cur = self._data.cur()
|
|
while cur is not None and not cur.isalpha():
|
|
if c == 'c':
|
|
x1, y1 = self._getCoordPair(code.islower(), self._point)
|
|
x2, y2 = self._getCoordPair(code.islower(), self._point)
|
|
else:
|
|
if self._handle is not None:
|
|
x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
|
|
self._handle[0], self._handle[1])
|
|
else:
|
|
x1, y1 = self._point
|
|
|
|
x2, y2 = self._getCoordPair(code.islower(), self._point)
|
|
|
|
x, y = self._getCoordPair(code.islower(), self._point)
|
|
|
|
if self._spline is None:
|
|
self._appendPoint(self._point[0], self._point[1],
|
|
handle_left_type='FREE', handle_left=self._point,
|
|
handle_right_type='FREE', handle_right=(x1, y1))
|
|
else:
|
|
self._updateHandle(handle=(x1, y1), handle_type='FREE')
|
|
|
|
self._appendPoint(x, y,
|
|
handle_left_type='FREE', handle_left=(x2, y2),
|
|
handle_right_type='FREE', handle_right=(x, y))
|
|
|
|
self._point = (x, y)
|
|
self._handle = (x2, y2)
|
|
cur = self._data.cur()
|
|
|
|
def _pathCurveToQT(self, code):
|
|
"""
|
|
Qyadracic BEZIER CurveTo path command
|
|
"""
|
|
|
|
c = code.lower()
|
|
cur = self._data.cur()
|
|
|
|
while cur is not None and not cur.isalpha():
|
|
if c == 'q':
|
|
x1, y1 = self._getCoordPair(code.islower(), self._point)
|
|
else:
|
|
if self._handle is not None:
|
|
x1, y1 = SVGFlipHandle(self._point[0], self._point[1],
|
|
self._handle[0], self._handle[1])
|
|
else:
|
|
x1, y1 = self._point
|
|
|
|
x, y = self._getCoordPair(code.islower(), self._point)
|
|
|
|
if self._spline is None:
|
|
self._appendPoint(self._point[0], self._point[1],
|
|
handle_left_type='FREE', handle_left=self._point,
|
|
handle_right_type='FREE', handle_right=self._point)
|
|
|
|
self._appendPoint(x, y,
|
|
handle_left_type='FREE', handle_left=(x1, y1),
|
|
handle_right_type='FREE', handle_right=(x, y))
|
|
|
|
self._point = (x, y)
|
|
self._handle = (x1, y1)
|
|
cur = self._data.cur()
|
|
|
|
def _calcArc(self, rx, ry, ang, fa, fs, x, y):
|
|
"""
|
|
Calc arc paths
|
|
|
|
Copied and adoptedfrom paths_svg2obj.py script for Blender 2.49
|
|
which is Copyright (c) jm soler juillet/novembre 2004-april 2009,
|
|
"""
|
|
|
|
cpx = self._point[0]
|
|
cpy = self._point[1]
|
|
rx = abs(rx)
|
|
ry = abs(ry)
|
|
px = abs((cos(ang) * (cpx - x) + sin(ang) * (cpy - y)) * 0.5) ** 2.0
|
|
py = abs((cos(ang) * (cpy - y) - sin(ang) * (cpx - x)) * 0.5) ** 2.0
|
|
rpx = rpy = 0.0
|
|
|
|
if abs(rx) > 0.0:
|
|
px = px / (rx ** 2.0)
|
|
|
|
if abs(ry) > 0.0:
|
|
rpy = py / (ry ** 2.0)
|
|
|
|
pl = rpx + rpy
|
|
if pl > 1.0:
|
|
pl = pl ** 0.5
|
|
rx *= pl
|
|
ry *= pl
|
|
|
|
carx = sarx = cary = sary = 0.0
|
|
|
|
if abs(rx) > 0.0:
|
|
carx = cos(ang) / rx
|
|
sarx = sin(ang) / rx
|
|
|
|
if abs(ry) > 0.0:
|
|
cary = cos(ang) / ry
|
|
sary = sin(ang) / ry
|
|
|
|
x0 = carx * cpx + sarx * cpy
|
|
y0 = -sary * cpx + cary * cpy
|
|
x1 = carx * x + sarx * y
|
|
y1 = -sary * x + cary * y
|
|
d = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)
|
|
|
|
if abs(d) > 0.0:
|
|
sq = 1.0 / d - 0.25
|
|
else:
|
|
sq = -0.25
|
|
|
|
if sq < 0.0:
|
|
sq = 0.0
|
|
|
|
sf = sq ** 0.5
|
|
if fs == fa:
|
|
sf = -sf
|
|
|
|
xc = 0.5 * (x0 + x1) - sf * (y1 - y0)
|
|
yc = 0.5 * (y0 + y1) + sf * (x1 - x0)
|
|
ang_0 = atan2(y0 - yc, x0 - xc)
|
|
ang_1 = atan2(y1 - yc, x1 - xc)
|
|
ang_arc = ang_1 - ang_0
|
|
|
|
if ang_arc < 0.0 and fs == 1:
|
|
ang_arc += 2.0 * pi
|
|
elif ang_arc > 0.0 and fs == 0:
|
|
ang_arc -= 2.0 * pi
|
|
|
|
n_segs = int(ceil(abs(ang_arc * 2.0 / (pi * 0.5 + 0.001))))
|
|
|
|
if self._spline is None:
|
|
self._appendPoint(cpx, cpy,
|
|
handle_left_type='FREE', handle_left=(cpx, cpy),
|
|
handle_right_type='FREE', handle_right=(cpx, cpy))
|
|
|
|
for i in range(n_segs):
|
|
ang0 = ang_0 + i * ang_arc / n_segs
|
|
ang1 = ang_0 + (i + 1) * ang_arc / n_segs
|
|
ang_demi = 0.25 * (ang1 - ang0)
|
|
t = 2.66666 * sin(ang_demi) * sin(ang_demi) / sin(ang_demi * 2.0)
|
|
x1 = xc + cos(ang0) - t * sin(ang0)
|
|
y1 = yc + sin(ang0) + t * cos(ang0)
|
|
x2 = xc + cos(ang1)
|
|
y2 = yc + sin(ang1)
|
|
x3 = x2 + t * sin(ang1)
|
|
y3 = y2 - t * cos(ang1)
|
|
|
|
coord1 = ((cos(ang) * rx) * x1 + (-sin(ang) * ry) * y1,
|
|
(sin(ang) * rx) * x1 + (cos(ang) * ry) * y1)
|
|
coord2 = ((cos(ang) * rx) * x3 + (-sin(ang) * ry) * y3,
|
|
(sin(ang) * rx) * x3 + (cos(ang) * ry) * y3)
|
|
coord3 = ((cos(ang) * rx) * x2 + (-sin(ang) * ry) * y2,
|
|
(sin(ang) * rx) * x2 + (cos(ang) * ry) * y2)
|
|
|
|
self._updateHandle(handle=coord1, handle_type='FREE')
|
|
|
|
self._appendPoint(coord3[0], coord3[1],
|
|
handle_left_type='FREE', handle_left=coord2,
|
|
handle_right_type='FREE', handle_right=coord3)
|
|
|
|
def _pathCurveToA(self, code):
|
|
"""
|
|
Elliptical arc CurveTo path command
|
|
"""
|
|
|
|
cur = self._data.cur()
|
|
|
|
while cur is not None and not cur.isalpha():
|
|
rx = float(self._data.next())
|
|
ry = float(self._data.next())
|
|
ang = float(self._data.next()) / 180 * pi
|
|
fa = float(self._data.next())
|
|
fs = float(self._data.next())
|
|
x, y = self._getCoordPair(code.islower(), self._point)
|
|
|
|
self._calcArc(rx, ry, ang, fa, fs, x, y)
|
|
|
|
self._point = (x, y)
|
|
self._handle = None
|
|
cur = self._data.cur()
|
|
|
|
def _pathClose(self, code):
|
|
"""
|
|
Close path command
|
|
"""
|
|
|
|
if self._spline:
|
|
self._spline['closed'] = True
|
|
|
|
cv = self._spline['points'][0]
|
|
self._point = (cv['x'], cv['y'])
|
|
|
|
def parse(self):
|
|
"""
|
|
Execute parser
|
|
"""
|
|
|
|
closed = False
|
|
|
|
while not self._data.eof():
|
|
code = self._data.next()
|
|
cmd = self._commands.get(code)
|
|
|
|
if cmd is None:
|
|
raise Exception('Unknown path command: {0}' . format(code))
|
|
|
|
if cmd in {'Z', 'z'}:
|
|
closed =True
|
|
else:
|
|
closed = False
|
|
|
|
cmd(code)
|
|
if self._use_fill and not closed:
|
|
self._pathClose('z')
|
|
|
|
def getSplines(self):
|
|
"""
|
|
Get splines definitions
|
|
"""
|
|
|
|
return self._splines
|
|
|
|
|
|
class SVGGeometry:
|
|
"""
|
|
Abstract SVG geometry
|
|
"""
|
|
|
|
__slots__ = ('_node', # XML node for geometry
|
|
'_context', # Global SVG context (holds matrices stack, i.e.)
|
|
'_creating') # Flag if geometry is already creating
|
|
# for this node
|
|
# need to detect cycles for USE node
|
|
|
|
def __init__(self, node, context):
|
|
"""
|
|
Initialize SVG geometry
|
|
"""
|
|
|
|
self._node = node
|
|
self._context = context
|
|
self._creating = False
|
|
|
|
if hasattr(node, 'getAttribute'):
|
|
defs = context['defines']
|
|
|
|
attr_id = node.getAttribute('id')
|
|
if attr_id and defs.get('#' + attr_id) is None:
|
|
defs['#' + attr_id] = self
|
|
|
|
className = node.getAttribute('class')
|
|
if className and defs.get(className) is None:
|
|
defs[className] = self
|
|
|
|
def _pushRect(self, rect):
|
|
"""
|
|
Push display rectangle
|
|
"""
|
|
|
|
self._context['rects'].append(rect)
|
|
self._context['rect'] = rect
|
|
|
|
def _popRect(self):
|
|
"""
|
|
Pop display rectangle
|
|
"""
|
|
|
|
self._context['rects'].pop()
|
|
self._context['rect'] = self._context['rects'][-1]
|
|
|
|
def _pushMatrix(self, matrix):
|
|
"""
|
|
Push transformation matrix
|
|
"""
|
|
|
|
self._context['transform'].append(matrix)
|
|
self._context['matrix'] = self._context['matrix'] @ matrix
|
|
|
|
def _popMatrix(self):
|
|
"""
|
|
Pop transformation matrix
|
|
"""
|
|
|
|
matrix = self._context['transform'].pop()
|
|
self._context['matrix'] = self._context['matrix'] @ matrix.inverted()
|
|
|
|
def _pushStyle(self, style):
|
|
"""
|
|
Push style
|
|
"""
|
|
|
|
self._context['styles'].append(style)
|
|
self._context['style'] = style
|
|
|
|
def _popStyle(self):
|
|
"""
|
|
Pop style
|
|
"""
|
|
|
|
self._context['styles'].pop()
|
|
self._context['style'] = self._context['styles'][-1]
|
|
|
|
def _transformCoord(self, point):
|
|
"""
|
|
Transform SVG-file coords
|
|
"""
|
|
|
|
v = Vector((point[0], point[1], 0.0))
|
|
|
|
return self._context['matrix'] @ v
|
|
|
|
def getNodeMatrix(self):
|
|
"""
|
|
Get transformation matrix of node
|
|
"""
|
|
|
|
return SVGMatrixFromNode(self._node, self._context)
|
|
|
|
def parse(self):
|
|
"""
|
|
Parse XML node to memory
|
|
"""
|
|
|
|
pass
|
|
|
|
def _doCreateGeom(self, instancing):
|
|
"""
|
|
Internal handler to create real geometries
|
|
"""
|
|
|
|
pass
|
|
|
|
def getTransformMatrix(self):
|
|
"""
|
|
Get matrix created from "transform" attribute
|
|
"""
|
|
|
|
transform = self._node.getAttribute('transform')
|
|
|
|
if transform:
|
|
return SVGParseTransform(transform)
|
|
|
|
return None
|
|
|
|
def createGeom(self, instancing):
|
|
"""
|
|
Create real geometries
|
|
"""
|
|
|
|
if self._creating:
|
|
return
|
|
|
|
self._creating = True
|
|
|
|
matrix = self.getTransformMatrix()
|
|
if matrix is not None:
|
|
self._pushMatrix(matrix)
|
|
|
|
self._doCreateGeom(instancing)
|
|
|
|
if matrix is not None:
|
|
self._popMatrix()
|
|
|
|
self._creating = False
|
|
|
|
|
|
class SVGGeometryContainer(SVGGeometry):
|
|
"""
|
|
Container of SVG geometries
|
|
"""
|
|
|
|
__slots__ = ('_geometries', # List of chold geometries
|
|
'_styles') # Styles, used for displaying
|
|
|
|
def __init__(self, node, context):
|
|
"""
|
|
Initialize SVG geometry container
|
|
"""
|
|
|
|
super().__init__(node, context)
|
|
|
|
self._geometries = []
|
|
self._styles = SVGEmptyStyles
|
|
|
|
def parse(self):
|
|
"""
|
|
Parse XML node to memory
|
|
"""
|
|
|
|
if type(self._node) is xml.dom.minidom.Element:
|
|
self._styles = SVGParseStyles(self._node, self._context)
|
|
|
|
self._pushStyle(self._styles)
|
|
|
|
for node in self._node.childNodes:
|
|
if type(node) is not xml.dom.minidom.Element:
|
|
continue
|
|
|
|
ob = parseAbstractNode(node, self._context)
|
|
if ob is not None:
|
|
self._geometries.append(ob)
|
|
|
|
self._popStyle()
|
|
|
|
def _doCreateGeom(self, instancing):
|
|
"""
|
|
Create real geometries
|
|
"""
|
|
|
|
for geom in self._geometries:
|
|
geom.createGeom(instancing)
|
|
|
|
def getGeometries(self):
|
|
"""
|
|
Get list of parsed geometries
|
|
"""
|
|
|
|
return self._geometries
|
|
|
|
|
|
class SVGGeometryPATH(SVGGeometry):
|
|
"""
|
|
SVG path geometry
|
|
"""
|
|
|
|
__slots__ = ('_splines', # List of splines after parsing
|
|
'_styles') # Styles, used for displaying
|
|
|
|
def __init__(self, node, context):
|
|
"""
|
|
Initialize SVG path
|
|
"""
|
|
|
|
super().__init__(node, context)
|
|
|
|
self._splines = []
|
|
self._styles = SVGEmptyStyles
|
|
|
|
def parse(self):
|
|
"""
|
|
Parse SVG path node
|
|
"""
|
|
|
|
d = self._node.getAttribute('d')
|
|
|
|
self._styles = SVGParseStyles(self._node, self._context)
|
|
|
|
pathParser = SVGPathParser(d, self._styles['useFill'])
|
|
pathParser.parse()
|
|
|
|
self._splines = pathParser.getSplines()
|
|
|
|
def _doCreateGeom(self, instancing):
|
|
"""
|
|
Create real geometries
|
|
"""
|
|
|
|
ob = SVGCreateCurve(self._context)
|
|
cu = ob.data
|
|
|
|
if self._node.getAttribute('id'):
|
|
cu.name = self._node.getAttribute('id')
|
|
|
|
if self._styles['useFill']:
|
|
cu.dimensions = '2D'
|
|
cu.materials.append(self._styles['fill'])
|
|
else:
|
|
cu.dimensions = '3D'
|
|
|
|
for spline in self._splines:
|
|
act_spline = None
|
|
for point in spline['points']:
|
|
co = self._transformCoord((point['x'], point['y']))
|
|
|
|
if act_spline is None:
|
|
cu.splines.new('BEZIER')
|
|
|
|
act_spline = cu.splines[-1]
|
|
act_spline.use_cyclic_u = spline['closed']
|
|
else:
|
|
act_spline.bezier_points.add(count=1)
|
|
|
|
bezt = act_spline.bezier_points[-1]
|
|
bezt.co = co
|
|
|
|
bezt.handle_left_type = point['handle_left_type']
|
|
if point['handle_left'] is not None:
|
|
handle = point['handle_left']
|
|
bezt.handle_left = self._transformCoord(handle)
|
|
|
|
bezt.handle_right_type = point['handle_right_type']
|
|
if point['handle_right'] is not None:
|
|
handle = point['handle_right']
|
|
bezt.handle_right = self._transformCoord(handle)
|
|
|
|
SVGFinishCurve()
|
|
|
|
|
|
class SVGGeometryDEFS(SVGGeometryContainer):
|
|
"""
|
|
Container for referenced elements
|
|
"""
|
|
|
|
def createGeom(self, instancing):
|
|
"""
|
|
Create real geometries
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class SVGGeometrySYMBOL(SVGGeometryContainer):
|
|
"""
|
|
Referenced element
|
|
"""
|
|
|
|
def _doCreateGeom(self, instancing):
|
|
"""
|
|
Create real geometries
|
|
"""
|
|
|
|
self._pushMatrix(self.getNodeMatrix())
|
|
|
|
super()._doCreateGeom(False)
|
|
|
|
self._popMatrix()
|
|
|
|
def createGeom(self, instancing):
|
|
"""
|
|
Create real geometries
|
|
"""
|
|
|
|
if not instancing:
|
|
return
|
|
|
|
super().createGeom(instancing)
|
|
|
|
|
|
class SVGGeometryG(SVGGeometryContainer):
|
|
"""
|
|
Geometry group
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class SVGGeometryUSE(SVGGeometry):
|
|
"""
|
|
User of referenced elements
|
|
"""
|
|
|
|
def _doCreateGeom(self, instancing):
|
|
"""
|
|
Create real geometries
|
|
"""
|
|
|
|
ref = self._node.getAttribute('xlink:href')
|
|
geom = self._context['defines'].get(ref)
|
|
|
|
if geom is not None:
|
|
rect = SVGRectFromNode(self._node, self._context)
|
|
self._pushRect(rect)
|
|
|
|
self._pushMatrix(self.getNodeMatrix())
|
|
|
|
geom.createGeom(True)
|
|
|
|
self._popMatrix()
|
|
|
|
self._popRect()
|
|
|
|
|
|
class SVGGeometryRECT(SVGGeometry):
|
|
"""
|
|
SVG rectangle
|
|
"""
|
|
|
|
__slots__ = ('_rect', # coordinate and dimensions of rectangle
|
|
'_radius', # Rounded corner radiuses
|
|
'_styles') # Styles, used for displaying
|
|
|
|
def __init__(self, node, context):
|
|
"""
|
|
Initialize new rectangle
|
|
"""
|
|
|
|
super().__init__(node, context)
|
|
|
|
self._rect = ('0', '0', '0', '0')
|
|
self._radius = ('0', '0')
|
|
self._styles = SVGEmptyStyles
|
|
|
|
def parse(self):
|
|
"""
|
|
Parse SVG rectangle node
|
|
"""
|
|
|
|
self._styles = SVGParseStyles(self._node, self._context)
|
|
|
|
rect = []
|
|
for attr in ['x', 'y', 'width', 'height']:
|
|
val = self._node.getAttribute(attr)
|
|
rect.append(val or '0')
|
|
|
|
self._rect = (rect)
|
|
|
|
rx = self._node.getAttribute('rx')
|
|
ry = self._node.getAttribute('ry')
|
|
|
|
self._radius = (rx, ry)
|
|
|
|
def _appendCorner(self, spline, coord, firstTime, rounded):
|
|
"""
|
|
Append new corner to rectangle
|
|
"""
|
|
|
|
handle = None
|
|
if len(coord) == 3:
|
|
handle = self._transformCoord(coord[2])
|
|
coord = (coord[0], coord[1])
|
|
|
|
co = self._transformCoord(coord)
|
|
|
|
if not firstTime:
|
|
spline.bezier_points.add(count=1)
|
|
|
|
bezt = spline.bezier_points[-1]
|
|
bezt.co = co
|
|
|
|
if rounded:
|
|
if handle:
|
|
bezt.handle_left_type = 'VECTOR'
|
|
bezt.handle_right_type = 'FREE'
|
|
|
|
bezt.handle_right = handle
|
|
else:
|
|
bezt.handle_left_type = 'FREE'
|
|
bezt.handle_right_type = 'VECTOR'
|
|
bezt.handle_left = co
|
|
|
|
else:
|
|
bezt.handle_left_type = 'VECTOR'
|
|
bezt.handle_right_type = 'VECTOR'
|
|
|
|
def _doCreateGeom(self, instancing):
|
|
"""
|
|
Create real geometries
|
|
"""
|
|
|
|
# Run-time parsing -- percents would be correct only if
|
|
# parsing them now
|
|
crect = self._context['rect']
|
|
rect = []
|
|
|
|
for i in range(4):
|
|
rect.append(SVGParseCoord(self._rect[i], crect[i % 2]))
|
|
|
|
r = self._radius
|
|
rx = ry = 0.0
|
|
|
|
if r[0] and r[1]:
|
|
rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2)
|
|
ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2)
|
|
elif r[0]:
|
|
rx = min(SVGParseCoord(r[0], rect[0]), rect[2] / 2)
|
|
ry = min(rx, rect[3] / 2)
|
|
rx = ry = min(rx, ry)
|
|
elif r[1]:
|
|
ry = min(SVGParseCoord(r[1], rect[1]), rect[3] / 2)
|
|
rx = min(ry, rect[2] / 2)
|
|
rx = ry = min(rx, ry)
|
|
|
|
radius = (rx, ry)
|
|
|
|
# Geometry creation
|
|
ob = SVGCreateCurve(self._context)
|
|
cu = ob.data
|
|
|
|
if self._styles['useFill']:
|
|
cu.dimensions = '2D'
|
|
cu.materials.append(self._styles['fill'])
|
|
else:
|
|
cu.dimensions = '3D'
|
|
|
|
cu.splines.new('BEZIER')
|
|
|
|
spline = cu.splines[-1]
|
|
spline.use_cyclic_u = True
|
|
|
|
x, y = rect[0], rect[1]
|
|
w, h = rect[2], rect[3]
|
|
rx, ry = radius[0], radius[1]
|
|
rounded = False
|
|
|
|
if rx or ry:
|
|
#
|
|
# 0 _______ 1
|
|
# / \
|
|
# / \
|
|
# 7 2
|
|
# | |
|
|
# | |
|
|
# 6 3
|
|
# \ /
|
|
# \ /
|
|
# 5 _______ 4
|
|
#
|
|
|
|
# Optional third component -- right handle coord
|
|
coords = [(x + rx, y),
|
|
(x + w - rx, y, (x + w, y)),
|
|
(x + w, y + ry),
|
|
(x + w, y + h - ry, (x + w, y + h)),
|
|
(x + w - rx, y + h),
|
|
(x + rx, y + h, (x, y + h)),
|
|
(x, y + h - ry),
|
|
(x, y + ry, (x, y))]
|
|
|
|
rounded = True
|
|
else:
|
|
coords = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
|
|
|
|
firstTime = True
|
|
for coord in coords:
|
|
self._appendCorner(spline, coord, firstTime, rounded)
|
|
firstTime = False
|
|
|
|
SVGFinishCurve()
|
|
|
|
|
|
class SVGGeometryELLIPSE(SVGGeometry):
|
|
"""
|
|
SVG ellipse
|
|
"""
|
|
|
|
__slots__ = ('_cx', # X-coordinate of center
|
|
'_cy', # Y-coordinate of center
|
|
'_rx', # X-axis radius of circle
|
|
'_ry', # Y-axis radius of circle
|
|
'_styles') # Styles, used for displaying
|
|
|
|
def __init__(self, node, context):
|
|
"""
|
|
Initialize new ellipse
|
|
"""
|
|
|
|
super().__init__(node, context)
|
|
|
|
self._cx = '0.0'
|
|
self._cy = '0.0'
|
|
self._rx = '0.0'
|
|
self._ry = '0.0'
|
|
self._styles = SVGEmptyStyles
|
|
|
|
def parse(self):
|
|
"""
|
|
Parse SVG ellipse node
|
|
"""
|
|
|
|
self._styles = SVGParseStyles(self._node, self._context)
|
|
|
|
self._cx = self._node.getAttribute('cx') or '0'
|
|
self._cy = self._node.getAttribute('cy') or '0'
|
|
self._rx = self._node.getAttribute('rx') or '0'
|
|
self._ry = self._node.getAttribute('ry') or '0'
|
|
|
|
def _doCreateGeom(self, instancing):
|
|
"""
|
|
Create real geometries
|
|
"""
|
|
|
|
# Run-time parsing -- percents would be correct only if
|
|
# parsing them now
|
|
crect = self._context['rect']
|
|
|
|
cx = SVGParseCoord(self._cx, crect[0])
|
|
cy = SVGParseCoord(self._cy, crect[1])
|
|
rx = SVGParseCoord(self._rx, crect[0])
|
|
ry = SVGParseCoord(self._ry, crect[1])
|
|
|
|
if not rx or not ry:
|
|
# Automaic handles will work incorrect in this case
|
|
return
|
|
|
|
# Create circle
|
|
ob = SVGCreateCurve(self._context)
|
|
cu = ob.data
|
|
|
|
if self._node.getAttribute('id'):
|
|
cu.name = self._node.getAttribute('id')
|
|
|
|
if self._styles['useFill']:
|
|
cu.dimensions = '2D'
|
|
cu.materials.append(self._styles['fill'])
|
|
else:
|
|
cu.dimensions = '3D'
|
|
|
|
coords = [((cx - rx, cy),
|
|
(cx - rx, cy + ry * 0.552),
|
|
(cx - rx, cy - ry * 0.552)),
|
|
|
|
((cx, cy - ry),
|
|
(cx - rx * 0.552, cy - ry),
|
|
(cx + rx * 0.552, cy - ry)),
|
|
|
|
((cx + rx, cy),
|
|
(cx + rx, cy - ry * 0.552),
|
|
(cx + rx, cy + ry * 0.552)),
|
|
|
|
((cx, cy + ry),
|
|
(cx + rx * 0.552, cy + ry),
|
|
(cx - rx * 0.552, cy + ry))]
|
|
|
|
spline = None
|
|
for coord in coords:
|
|
co = self._transformCoord(coord[0])
|
|
handle_left = self._transformCoord(coord[1])
|
|
handle_right = self._transformCoord(coord[2])
|
|
|
|
if spline is None:
|
|
cu.splines.new('BEZIER')
|
|
spline = cu.splines[-1]
|
|
spline.use_cyclic_u = True
|
|
else:
|
|
spline.bezier_points.add(count=1)
|
|
|
|
bezt = spline.bezier_points[-1]
|
|
bezt.co = co
|
|
bezt.handle_left_type = 'FREE'
|
|
bezt.handle_right_type = 'FREE'
|
|
bezt.handle_left = handle_left
|
|
bezt.handle_right = handle_right
|
|
|
|
SVGFinishCurve()
|
|
|
|
|
|
class SVGGeometryCIRCLE(SVGGeometryELLIPSE):
|
|
"""
|
|
SVG circle
|
|
"""
|
|
|
|
def parse(self):
|
|
"""
|
|
Parse SVG circle node
|
|
"""
|
|
|
|
self._styles = SVGParseStyles(self._node, self._context)
|
|
|
|
self._cx = self._node.getAttribute('cx') or '0'
|
|
self._cy = self._node.getAttribute('cy') or '0'
|
|
|
|
r = self._node.getAttribute('r') or '0'
|
|
self._rx = self._ry = r
|
|
|
|
|
|
class SVGGeometryLINE(SVGGeometry):
|
|
"""
|
|
SVG line
|
|
"""
|
|
|
|
__slots__ = ('_x1', # X-coordinate of beginning
|
|
'_y1', # Y-coordinate of beginning
|
|
'_x2', # X-coordinate of ending
|
|
'_y2') # Y-coordinate of ending
|
|
|
|
def __init__(self, node, context):
|
|
"""
|
|
Initialize new line
|
|
"""
|
|
|
|
super().__init__(node, context)
|
|
|
|
self._x1 = '0.0'
|
|
self._y1 = '0.0'
|
|
self._x2 = '0.0'
|
|
self._y2 = '0.0'
|
|
|
|
def parse(self):
|
|
"""
|
|
Parse SVG line node
|
|
"""
|
|
|
|
self._x1 = self._node.getAttribute('x1') or '0'
|
|
self._y1 = self._node.getAttribute('y1') or '0'
|
|
self._x2 = self._node.getAttribute('x2') or '0'
|
|
self._y2 = self._node.getAttribute('y2') or '0'
|
|
|
|
def _doCreateGeom(self, instancing):
|
|
"""
|
|
Create real geometries
|
|
"""
|
|
|
|
# Run-time parsing -- percents would be correct only if
|
|
# parsing them now
|
|
crect = self._context['rect']
|
|
|
|
x1 = SVGParseCoord(self._x1, crect[0])
|
|
y1 = SVGParseCoord(self._y1, crect[1])
|
|
x2 = SVGParseCoord(self._x2, crect[0])
|
|
y2 = SVGParseCoord(self._y2, crect[1])
|
|
|
|
# Create cline
|
|
ob = SVGCreateCurve(self._context)
|
|
cu = ob.data
|
|
|
|
coords = [(x1, y1), (x2, y2)]
|
|
spline = None
|
|
|
|
for coord in coords:
|
|
co = self._transformCoord(coord)
|
|
|
|
if spline is None:
|
|
cu.splines.new('BEZIER')
|
|
spline = cu.splines[-1]
|
|
spline.use_cyclic_u = True
|
|
else:
|
|
spline.bezier_points.add(count=1)
|
|
|
|
bezt = spline.bezier_points[-1]
|
|
bezt.co = co
|
|
bezt.handle_left_type = 'VECTOR'
|
|
bezt.handle_right_type = 'VECTOR'
|
|
|
|
SVGFinishCurve()
|
|
|
|
|
|
class SVGGeometryPOLY(SVGGeometry):
|
|
"""
|
|
Abstract class for handling poly-geometries
|
|
(polylines and polygons)
|
|
"""
|
|
|
|
__slots__ = ('_points', # Array of points for poly geometry
|
|
'_styles', # Styles, used for displaying
|
|
'_closed') # Should generated curve be closed?
|
|
|
|
def __init__(self, node, context):
|
|
"""
|
|
Initialize new poly geometry
|
|
"""
|
|
|
|
super().__init__(node, context)
|
|
|
|
self._points = []
|
|
self._styles = SVGEmptyStyles
|
|
self._closed = False
|
|
|
|
def parse(self):
|
|
"""
|
|
Parse poly node
|
|
"""
|
|
|
|
self._styles = SVGParseStyles(self._node, self._context)
|
|
|
|
points = self._node.getAttribute('points')
|
|
points = points.replace(',', ' ').replace('-', ' -')
|
|
points = points.split()
|
|
|
|
prev = None
|
|
self._points = []
|
|
|
|
for p in points:
|
|
if prev is None:
|
|
prev = p
|
|
else:
|
|
self._points.append((float(prev), float(p)))
|
|
prev = None
|
|
|
|
def _doCreateGeom(self, instancing):
|
|
"""
|
|
Create real geometries
|
|
"""
|
|
|
|
ob = SVGCreateCurve(self._context)
|
|
cu = ob.data
|
|
|
|
if self._closed and self._styles['useFill']:
|
|
cu.dimensions = '2D'
|
|
cu.materials.append(self._styles['fill'])
|
|
else:
|
|
cu.dimensions = '3D'
|
|
|
|
spline = None
|
|
|
|
for point in self._points:
|
|
co = self._transformCoord(point)
|
|
|
|
if spline is None:
|
|
cu.splines.new('BEZIER')
|
|
spline = cu.splines[-1]
|
|
spline.use_cyclic_u = self._closed
|
|
else:
|
|
spline.bezier_points.add(count=1)
|
|
|
|
bezt = spline.bezier_points[-1]
|
|
bezt.co = co
|
|
bezt.handle_left_type = 'VECTOR'
|
|
bezt.handle_right_type = 'VECTOR'
|
|
|
|
SVGFinishCurve()
|
|
|
|
|
|
class SVGGeometryPOLYLINE(SVGGeometryPOLY):
|
|
"""
|
|
SVG polyline geometry
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class SVGGeometryPOLYGON(SVGGeometryPOLY):
|
|
"""
|
|
SVG polygon geometry
|
|
"""
|
|
|
|
def __init__(self, node, context):
|
|
"""
|
|
Initialize new polygon geometry
|
|
"""
|
|
|
|
super().__init__(node, context)
|
|
|
|
self._closed = True
|
|
|
|
|
|
class SVGGeometrySVG(SVGGeometryContainer):
|
|
"""
|
|
Main geometry holder
|
|
"""
|
|
|
|
def _doCreateGeom(self, instancing):
|
|
"""
|
|
Create real geometries
|
|
"""
|
|
|
|
rect = SVGRectFromNode(self._node, self._context)
|
|
|
|
matrix = self.getNodeMatrix()
|
|
|
|
# Better Inkscape compatibility: match document origin with
|
|
# 3D space origin.
|
|
if self._node.getAttribute('inkscape:version'):
|
|
raw_height = self._node.getAttribute('height')
|
|
document_height = SVGParseCoord(raw_height, 1.0)
|
|
matrix = matrix @ matrix.Translation([0.0, -document_height , 0.0])
|
|
|
|
self._pushMatrix(matrix)
|
|
self._pushRect(rect)
|
|
|
|
super()._doCreateGeom(False)
|
|
|
|
self._popRect()
|
|
self._popMatrix()
|
|
|
|
|
|
class SVGLoader(SVGGeometryContainer):
|
|
"""
|
|
SVG file loader
|
|
"""
|
|
|
|
def getTransformMatrix(self):
|
|
"""
|
|
Get matrix created from "transform" attribute
|
|
"""
|
|
|
|
# SVG document doesn't support transform specification
|
|
# it can't even hold attributes
|
|
|
|
return None
|
|
|
|
def __init__(self, context, filepath, do_colormanage):
|
|
"""
|
|
Initialize SVG loader
|
|
"""
|
|
import os
|
|
|
|
svg_name = os.path.basename(filepath)
|
|
scene = context.scene
|
|
collection = bpy.data.collections.new(name=svg_name)
|
|
scene.collection.children.link(collection)
|
|
|
|
node = xml.dom.minidom.parse(filepath)
|
|
|
|
m = Matrix()
|
|
m = m @ Matrix.Scale(1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((1.0, 0.0, 0.0)))
|
|
m = m @ Matrix.Scale(-1.0 / 90.0 * 0.3048 / 12.0, 4, Vector((0.0, 1.0, 0.0)))
|
|
|
|
rect = (0, 0)
|
|
|
|
self._context = {'defines': {},
|
|
'transform': [],
|
|
'rects': [rect],
|
|
'rect': rect,
|
|
'matrix': m,
|
|
'materials': {},
|
|
'styles': [None],
|
|
'style': None,
|
|
'do_colormanage': do_colormanage,
|
|
'collection': collection}
|
|
|
|
super().__init__(node, self._context)
|
|
|
|
|
|
svgGeometryClasses = {
|
|
'svg': SVGGeometrySVG,
|
|
'path': SVGGeometryPATH,
|
|
'defs': SVGGeometryDEFS,
|
|
'symbol': SVGGeometrySYMBOL,
|
|
'use': SVGGeometryUSE,
|
|
'rect': SVGGeometryRECT,
|
|
'ellipse': SVGGeometryELLIPSE,
|
|
'circle': SVGGeometryCIRCLE,
|
|
'line': SVGGeometryLINE,
|
|
'polyline': SVGGeometryPOLYLINE,
|
|
'polygon': SVGGeometryPOLYGON,
|
|
'g': SVGGeometryG}
|
|
|
|
|
|
def parseAbstractNode(node, context):
|
|
name = node.tagName.lower()
|
|
|
|
if name.startswith('svg:'):
|
|
name = name[4:]
|
|
|
|
geomClass = svgGeometryClasses.get(name)
|
|
|
|
if geomClass is not None:
|
|
ob = geomClass(node, context)
|
|
ob.parse()
|
|
|
|
return ob
|
|
|
|
return None
|
|
|
|
|
|
def load_svg(context, filepath, do_colormanage):
|
|
"""
|
|
Load specified SVG file
|
|
"""
|
|
|
|
if bpy.ops.object.mode_set.poll():
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
loader = SVGLoader(context, filepath, do_colormanage)
|
|
loader.parse()
|
|
loader.createGeom(False)
|
|
|
|
|
|
def load(operator, context, filepath=""):
|
|
|
|
# error in code should raise exceptions but loading
|
|
# non SVG files can give useful messages.
|
|
do_colormanage = context.scene.display_settings.display_device != 'NONE'
|
|
try:
|
|
load_svg(context, filepath, do_colormanage)
|
|
except (xml.parsers.expat.ExpatError, UnicodeEncodeError) as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
operator.report({'WARNING'}, "Unable to parse XML, %s:%s for file %r" % (type(e).__name__, e, filepath))
|
|
return {'CANCELLED'}
|
|
|
|
return {'FINISHED'}
|