-
Notifications
You must be signed in to change notification settings - Fork 21
/
axonometric_projection.py
216 lines (176 loc) · 8.01 KB
/
axonometric_projection.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import math
import sys
import inkex
from inkex.transforms import Transform
sys.path.append('/usr/share/inkscape/extensions')
inkex.localization.localize()
class IsometricProjectionTools(inkex.Effect):
"""
Convert a flat 2D projection to one of the three visible sides in an
isometric projection, and vice versa.
"""
attrTransformCenterX = inkex.addNS('transform-center-x', 'inkscape')
attrTransformCenterY = inkex.addNS('transform-center-y', 'inkscape')
def __init__(self):
"""
Constructor.
"""
inkex.Effect.__init__(self)
self.arg_parser.add_argument(
'-c', '--conversion',
dest='conversion', default='top',
help='Conversion to perform: (top|left|right)')
# Note: adding `type=bool` for the reverse option seems to break it when used
# from within Inkscape. Not sure why.
self.arg_parser.add_argument(
'-r', '--reverse',
dest='reverse', default="false",
help='Reverse the transformation from isometric projection '
'to flat 2D')
self.arg_parser.add_argument(
'-i', '--orthoangle_1', type=float,
dest='orthoangle_1', default="30",
help='Angle in degrees')
self.arg_parser.add_argument(
'-j', '--orthoangle_2', type=float,
dest='orthoangle_2',
help='Second angle in degrees, for dimetric projections')
def __initConstants(self, angle_1, angle_2):
angle_left = angle_1
self.rad_l = math.radians(angle_left)
self.cos_l = math.cos(self.rad_l)
self.sin_l = math.sin(self.rad_l)
if angle_2 is None or angle_2 == angle_1:
# Dimetric, where the two angles are the same. This includes the 30° isometric view.
self.rad_r = self.rad_l
self.cos_r = self.cos_l
self.sin_r = self.sin_l
self.top_c = -self.cos_l
self.top_d = self.sin_l
else:
# Trimetric, where the left and right angle differ.
angle_right = angle_2
self.rad_r = math.radians(angle_right)
self.cos_r = math.cos(self.rad_r)
self.sin_r = math.sin(self.rad_r)
# Combined values for trimetric transform of top piece.
self.rad_lp_r = math.radians(angle_left + angle_right)
self.sin_lp_r = math.sin(self.rad_lp_r)
self.shear_factor_top = math.sin(self.rad_lp_r)
self.tan_shear_top = math.tan(self.rad_l + self.rad_r - math.pi / 2)
self.top_c = self.shear_factor_top * (self.cos_l * self.tan_shear_top - self.sin_l)
self.top_d = self.shear_factor_top * (self.sin_l * self.tan_shear_top + self.cos_l)
# Combined affine transformation matrices. The bottom row of these 3×3
# matrices is omitted; it is always [0, 0, 1].
self.transformations = {
# From 2D to isometric top down view (dimetric, isometric):
# * scale vertically by cos(∠)
# * shear horizontally by -∠
# * rotate clock-wise ∠
#
# From 2D to isometric top down view (trimetric):
# * scale vertically by sin(∠(left) + ∠(right)
# * shear horizontally by ∠(left) + ∠(right) - 90°
# * rotate clock-wise ∠
'to_top': Transform(((self.cos_l, self.top_c, 0),
(self.sin_l, self.top_d, 0))),
# From 2D to axonometric left-hand side view:
# * scale horizontally by cos(∠)
# * shear vertically by -∠
'to_left': Transform(((self.cos_l, 0, 0),
(self.sin_l, 1, 0))),
# From 2D to axonometric right-hand side view:
# * scale horizontally by cos(∠)
# * shear vertically by ∠
'to_right': Transform(((self.cos_r , 0, 0),
(-self.sin_r, 1, 0)))
}
# The inverse matrices of the above perform the reverse transformations.
self.transformations['from_top'] = -self.transformations['to_top']
self.transformations['from_left'] = -self.transformations['to_left']
self.transformations['from_right'] = -self.transformations['to_right']
def getTransformCenter(self, midpoint, node):
"""
Find the transformation center of an object. If the user set it
manually by dragging it in Inkscape, those coordinates are used.
Otherwise, an attempt is made to find the center of the object's
bounding box.
"""
c_x = node.get(self.attrTransformCenterX)
c_y = node.get(self.attrTransformCenterY)
# Default to dead-center.
if c_x is None:
c_x = 0.0
else:
c_x = float(c_x)
if c_y is None:
c_y = 0.0
else:
c_y = float(c_y)
x = midpoint[0] + c_x
y = midpoint[1] - c_y
return [x, y]
def translateBetweenPoints(self, tr, here, there):
"""
Add a translation to a matrix that moves between two points.
"""
x = there[0] - here[0]
y = there[1] - here[1]
tr.add_translate(x, y)
def moveTransformationCenter(self, node, midpoint, center_new):
"""
If a transformation center is manually set on the node, move it to
match the transformation performed on the node.
"""
c_x = node.get(self.attrTransformCenterX)
c_y = node.get(self.attrTransformCenterY)
if c_x is not None:
x = str(center_new[0] - midpoint[0])
node.set(self.attrTransformCenterX, x)
if c_y is not None:
y = str(midpoint[1] - center_new[1])
node.set(self.attrTransformCenterY, y)
def effect(self):
"""
Apply the transformation. If an element already has a transformation
attribute, it will be combined with the transformation matrix for the
requested conversion.
"""
self.__initConstants(self.options.orthoangle_1, self.options.orthoangle_2)
if self.options.reverse == "true":
conversion = "from_" + self.options.conversion
else:
conversion = "to_" + self.options.conversion
if len(self.svg.selected) == 0:
inkex.errormsg(_("Please select an object to perform the "
"isometric projection transformation on."))
return
# Default to the flat 2D to isometric top down view conversion if an
# invalid identifier is passed.
effect_matrix = self.transformations.get(
conversion, self.transformations.get('to_top'))
for id, node in self.svg.selected.items():
bbox = node.bounding_box()
midpoint = [bbox.center_x, bbox.center_y]
center_old = self.getTransformCenter(midpoint, node)
transform = Transform(node.get("transform"))
# Combine our transformation matrix with any pre-existing
# transform.
tr = effect_matrix @ transform
# Compute the location of the transformation center after applying
# the transformation matrix.
center_new = center_old[:]
#Transform(matrix).apply_to_point(center_new)
tr.apply_to_point(center_new)
tr.apply_to_point(midpoint)
# Add a translation transformation that will move the object to
# keep its transformation center in the same place.
self.translateBetweenPoints(tr, center_new, center_old)
node.set('transform', str(tr))
# Adjust the transformation center.
self.moveTransformationCenter(node, midpoint, center_new)
# Create effect instance and apply it.
effect = IsometricProjectionTools()
effect.run()