297 lines
9.8 KiB
Python
297 lines
9.8 KiB
Python
|
#!/usr/bin/env python
|
||
|
# coding: utf-8
|
||
|
|
||
|
"""
|
||
|
Copyright (C) 2018 Rich Pang, rpang.contact@gmail.com.
|
||
|
|
||
|
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.
|
||
|
"""
|
||
|
|
||
|
from __future__ import division
|
||
|
|
||
|
from copy import deepcopy
|
||
|
import inkex, cubicsuperpath, pathmodifier, simplestyle, simplepath, simpletransform
|
||
|
import numpy as np
|
||
|
|
||
|
|
||
|
# rename common numpy operations
|
||
|
abs = np.abs
|
||
|
sin = np.sin
|
||
|
cos = np.cos
|
||
|
tan = np.tan
|
||
|
exp = np.exp
|
||
|
log = np.log
|
||
|
log10 = np.log10
|
||
|
|
||
|
pi = np.pi
|
||
|
|
||
|
|
||
|
__version__ = '0.1'
|
||
|
|
||
|
inkex.localize()
|
||
|
|
||
|
def split(l, sizes):
|
||
|
"""Split a list into sublists of specific sizes."""
|
||
|
if not sum(sizes) == len(l):
|
||
|
raise ValueError('sum(sizes) must equal len(l)')
|
||
|
|
||
|
sub_lists = []
|
||
|
ctr = 0
|
||
|
for size in sizes:
|
||
|
sub_lists.append(l[ctr:ctr+size])
|
||
|
ctr += size
|
||
|
|
||
|
return sub_lists
|
||
|
|
||
|
|
||
|
class Travel(inkex.Effect):
|
||
|
|
||
|
def __init__(self):
|
||
|
|
||
|
# initialize parent class
|
||
|
inkex.Effect.__init__(self)
|
||
|
|
||
|
# get params entered by user
|
||
|
self.OptionParser.add_option(
|
||
|
'', '--x_scale', action='store', type='float', dest='x_scale', default=0, help='x scale')
|
||
|
self.OptionParser.add_option(
|
||
|
'', '--y_scale', action='store', type='float', dest='y_scale', default=0, help='y scale')
|
||
|
self.OptionParser.add_option(
|
||
|
'', '--t_start', action='store', type='float', dest='t_start', default=0, help='t start')
|
||
|
self.OptionParser.add_option(
|
||
|
'', '--t_end', action='store', type='float', dest='t_end', default=1, help='t_end')
|
||
|
self.OptionParser.add_option(
|
||
|
'', '--n_steps', action='store', type='int', dest='n_steps', default=10, help='num steps')
|
||
|
self.OptionParser.add_option(
|
||
|
'', '--fps', action='store', type='float', dest='fps', default=0, help='fps')
|
||
|
self.OptionParser.add_option(
|
||
|
'', '--dt', action='store', type='float', dest='dt', default=0, help='dt')
|
||
|
self.OptionParser.add_option(
|
||
|
'', '--x_eqn', action='store', type='string', dest='x_eqn', default='', help='x')
|
||
|
self.OptionParser.add_option(
|
||
|
'', '--y_eqn', action='store', type='string', dest='y_eqn', default='', help='y')
|
||
|
self.OptionParser.add_option(
|
||
|
'', '--x_size_eqn', action='store', type='string', dest='x_size_eqn', default='', help='x size')
|
||
|
self.OptionParser.add_option(
|
||
|
'', '--y_size_eqn', action='store', type='string', dest='y_size_eqn', default='', help='y size')
|
||
|
self.OptionParser.add_option(
|
||
|
'', '--theta_eqn', action='store', type='string', dest='theta_eqn', default='', help='theta')
|
||
|
self.OptionParser.add_option(
|
||
|
'', '--active-tab', action='store', type='string', dest='active_tab', default='options', help='active tab')
|
||
|
|
||
|
def effect(self):
|
||
|
|
||
|
# get user-entered params
|
||
|
x_scale = self.options.x_scale
|
||
|
y_scale = self.options.y_scale
|
||
|
|
||
|
t_start = self.options.t_start
|
||
|
t_end = self.options.t_end
|
||
|
n_steps = self.options.n_steps
|
||
|
fps = self.options.fps
|
||
|
dt = self.options.dt
|
||
|
|
||
|
x_eqn = self.options.x_eqn
|
||
|
y_eqn = self.options.y_eqn
|
||
|
|
||
|
x_size_eqn = self.options.x_size_eqn
|
||
|
y_size_eqn = self.options.y_size_eqn
|
||
|
|
||
|
theta_eqn = self.options.theta_eqn
|
||
|
|
||
|
# get doc root
|
||
|
svg = self.document.getroot()
|
||
|
doc_w = self.unittouu(svg.get('width'))
|
||
|
doc_h = self.unittouu(svg.get('height'))
|
||
|
|
||
|
# get selected items and validate
|
||
|
selected = pathmodifier.zSort(self.document.getroot(), self.selected.keys())
|
||
|
|
||
|
if not selected:
|
||
|
inkex.errormsg('Exactly two objects must be selected: a rect and a template. See "help" for details.')
|
||
|
return
|
||
|
elif len(selected) != 2:
|
||
|
inkex.errormsg('Exactly two objects must be selected: a rect and a template. See "help" for details.')
|
||
|
return
|
||
|
|
||
|
# rect
|
||
|
rect = self.selected[selected[0]]
|
||
|
|
||
|
if not rect.tag.endswith('rect'):
|
||
|
inkex.errormsg('Bottom object must be rect. See "help" for usage.')
|
||
|
return
|
||
|
|
||
|
# object
|
||
|
obj = self.selected[selected[1]]
|
||
|
|
||
|
if not (obj.tag.endswith('path') or obj.tag.endswith('g')):
|
||
|
inkex.errormsg('Template object must be path or group of paths. See "help" for usage.')
|
||
|
return
|
||
|
if obj.tag.endswith('g'):
|
||
|
children = obj.getchildren()
|
||
|
if not all([ch.tag.endswith('path') for ch in children]):
|
||
|
msg = 'All elements of group must be paths, but they are: '
|
||
|
msg += ', '.join(['{}'.format(ch) for ch in children])
|
||
|
inkex.errormsg(msg)
|
||
|
return
|
||
|
objs = children
|
||
|
is_group = True
|
||
|
else:
|
||
|
objs = [obj]
|
||
|
is_group = False
|
||
|
|
||
|
# get rect params
|
||
|
w = float(rect.get('width'))
|
||
|
h = float(rect.get('height'))
|
||
|
|
||
|
x_rect = float(rect.get('x'))
|
||
|
y_rect = float(rect.get('y'))
|
||
|
|
||
|
# lower left corner
|
||
|
x_0 = x_rect
|
||
|
y_0 = y_rect + h
|
||
|
|
||
|
# get object path(s)
|
||
|
obj_ps = [simplepath.parsePath(obj_.get('d')) for obj_ in objs]
|
||
|
n_segs = [len(obj_p_) for obj_p_ in obj_ps]
|
||
|
obj_p = sum(obj_ps, [])
|
||
|
|
||
|
# compute travel parameters
|
||
|
if not n_steps:
|
||
|
# compute dt
|
||
|
if dt == 0:
|
||
|
dt = 1./fps
|
||
|
ts = np.arange(t_start, t_end, dt)
|
||
|
else:
|
||
|
ts = np.linspace(t_start, t_end, n_steps)
|
||
|
|
||
|
# compute xs, ys, stretches, and rotations in arbitrary coordinates
|
||
|
xs = np.nan * np.zeros(len(ts))
|
||
|
ys = np.nan * np.zeros(len(ts))
|
||
|
x_sizes = np.nan * np.zeros(len(ts))
|
||
|
y_sizes = np.nan * np.zeros(len(ts))
|
||
|
thetas = np.nan * np.zeros(len(ts))
|
||
|
|
||
|
for ctr, t in enumerate(ts):
|
||
|
xs[ctr] = eval(x_eqn)
|
||
|
ys[ctr] = eval(y_eqn)
|
||
|
x_sizes[ctr] = eval(x_size_eqn)
|
||
|
y_sizes[ctr] = eval(y_size_eqn)
|
||
|
thetas[ctr] = eval(theta_eqn) * pi / 180
|
||
|
|
||
|
# ensure no Infs
|
||
|
if np.any(np.isinf(xs)):
|
||
|
raise Exception('Inf detected in x(t), please remove.')
|
||
|
return
|
||
|
if np.any(np.isinf(ys)):
|
||
|
raise Exception('Inf detected in y(t), please remove.')
|
||
|
return
|
||
|
if np.any(np.isinf(x_sizes)):
|
||
|
raise Exception('Inf detected in x_size(t), please remove.')
|
||
|
return
|
||
|
if np.any(np.isinf(y_sizes)):
|
||
|
raise Exception('Inf detected in y_size(t), please remove.')
|
||
|
return
|
||
|
if np.any(np.isinf(thetas)):
|
||
|
raise Exception('Inf detected in theta(t), please remove.')
|
||
|
return
|
||
|
|
||
|
# convert to screen coordinates
|
||
|
xs *= (w/x_scale)
|
||
|
xs += x_0
|
||
|
|
||
|
ys *= (-h/y_scale) # neg sign to invert y for inkscape screen
|
||
|
ys += y_0
|
||
|
|
||
|
# get obj center
|
||
|
b_box = simpletransform.refinedBBox(cubicsuperpath.CubicSuperPath(obj_p))
|
||
|
c_x = 0.5 * (b_box[0] + b_box[1])
|
||
|
c_y = 0.5 * (b_box[2] + b_box[3])
|
||
|
|
||
|
# get rotation anchor
|
||
|
if any([k.endswith('transform-center-x') for k in obj.keys()]):
|
||
|
k_r_x = [k for k in obj.keys() if k.endswith('transform-center-x')][0]
|
||
|
k_r_y = [k for k in obj.keys() if k.endswith('transform-center-y')][0]
|
||
|
r_x = c_x + float(obj.get(k_r_x))
|
||
|
r_y = c_y - float(obj.get(k_r_y))
|
||
|
else:
|
||
|
r_x, r_y = c_x, c_y
|
||
|
|
||
|
paths = []
|
||
|
|
||
|
# compute new paths
|
||
|
for x, y, x_size, y_size, theta in zip(xs, ys, x_sizes, y_sizes, thetas):
|
||
|
|
||
|
path = deepcopy(obj_p)
|
||
|
|
||
|
# move to origin
|
||
|
simplepath.translatePath(path, -x_0, -y_0)
|
||
|
|
||
|
# move rotation anchor accordingly
|
||
|
r_x_1 = r_x - x_0
|
||
|
r_y_1 = r_y - y_0
|
||
|
|
||
|
# scale
|
||
|
simplepath.scalePath(path, x_size, y_size)
|
||
|
|
||
|
# scale rotation anchor accordingly
|
||
|
r_x_2 = r_x_1 * x_size
|
||
|
r_y_2 = r_y_1 * y_size
|
||
|
|
||
|
# move to final location
|
||
|
simplepath.translatePath(path, x, y)
|
||
|
|
||
|
# move rotation anchor accordingly
|
||
|
r_x_3 = r_x_2 + x
|
||
|
r_y_3 = r_y_2 + y
|
||
|
|
||
|
# rotate
|
||
|
simplepath.rotatePath(path, -theta, cx=r_x_3, cy=r_y_3)
|
||
|
|
||
|
paths.append(path)
|
||
|
|
||
|
parent = self.current_layer
|
||
|
group = inkex.etree.SubElement(parent, inkex.addNS('g', 'svg'), {})
|
||
|
|
||
|
for path in paths:
|
||
|
|
||
|
if is_group:
|
||
|
group_ = inkex.etree.SubElement(group, inkex.addNS('g', 'svg'), {})
|
||
|
path_components = split(path, n_segs)
|
||
|
|
||
|
for path_component, child in zip(path_components, children):
|
||
|
attribs = {
|
||
|
k: child.get(k) for k in child.keys()
|
||
|
}
|
||
|
|
||
|
attribs['d'] = simplepath.formatPath(path_component)
|
||
|
|
||
|
child_copy = inkex.etree.SubElement(group_, child.tag, attribs)
|
||
|
|
||
|
else:
|
||
|
attribs = {
|
||
|
k: obj.get(k) for k in obj.keys()
|
||
|
}
|
||
|
|
||
|
attribs['d'] = simplepath.formatPath(path)
|
||
|
|
||
|
obj_copy = inkex.etree.SubElement(group, obj.tag, attribs)
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
e = Travel()
|
||
|
e.affect()
|
||
|
|