Revert "Freestyle: Built-in SVG exporter."
[blender.git] / release / scripts / freestyle / modules / freestyle / utils.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 """
20 This module contains helper functions used for Freestyle style module
21 writing.
22 """
23
24 __all__ = (
25     "ContextFunctions",
26     "bound",
27     "bounding_box",
28     "find_matching_vertex",
29     "getCurrentScene",
30     "get_chain_length",
31     "get_test_stroke",
32     "integrate",
33     "iter_distance_along_stroke",
34     "iter_distance_from_camera",
35     "iter_distance_from_object",
36     "iter_material_value",
37     "iter_t2d_along_stroke",
38     "pairwise",
39     "phase_to_direction",
40     "rgb_to_bw",
41     "stroke_curvature",
42     "stroke_normal",
43     "tripplewise",
44     )
45
46
47 # module members
48 from _freestyle import (
49     ContextFunctions,
50     getCurrentScene,
51     integrate,
52     )
53
54 # constructs for helper functions in Python
55 from freestyle.types import (
56     Interface0DIterator,
57     Stroke,
58     StrokeVertexIterator,
59     )
60
61 from mathutils import Vector
62 from functools import lru_cache, namedtuple
63 from math import cos, sin, pi
64 from itertools import tee
65
66
67 # -- real utility functions  -- #
68
69 def rgb_to_bw(r, g, b):
70     """Method to convert rgb to a bw intensity value."""
71     return 0.35 * r + 0.45 * g + 0.2 * b
72
73
74 def bound(lower, x, higher):
75     """Returns x bounded by a maximum and minimum value. Equivalent to:
76     return min(max(x, lower), higher)
77     """
78     # this is about 50% quicker than min(max(x, lower), higher)
79     return (lower if x <= lower else higher if x >= higher else x)
80
81
82 def bounding_box(stroke):
83     """
84     Returns the maximum and minimum coordinates (the bounding box) of the stroke's vertices
85     """
86     x, y = zip(*(svert.point for svert in stroke))
87     return (Vector((min(x), min(y))), Vector((max(x), max(y))))
88
89 # -- General helper functions -- #
90
91
92 @lru_cache(maxsize=32)
93 def phase_to_direction(length):
94     """
95     Returns a list of tuples each containing:
96     - the phase
97     - a Vector with the values of the cosine and sine of 2pi * phase  (the direction)
98     """
99     results = list()
100     for i in range(length):
101         phase = i / (length - 1)
102         results.append((phase, Vector((cos(2 * pi * phase), sin(2 * pi * phase)))))
103     return results
104
105 # A named tuple primitive used for storing data that has an upper and
106 # lower bound (e.g., thickness, range and certain values)
107 BoundedProperty = namedtuple("BoundedProperty", ["min", "max", "delta"])
108
109 # -- helper functions for chaining -- #
110
111 def get_chain_length(ve, orientation):
112     """Returns the 2d length of a given ViewEdge."""
113     from freestyle.chainingiterators import pyChainSilhouetteGenericIterator
114     length = 0.0
115     # setup iterator
116     _it = pyChainSilhouetteGenericIterator(False, False)
117     _it.begin = ve
118     _it.current_edge = ve
119     _it.orientation = orientation
120     _it.init()
121
122     # run iterator till end of chain
123     while not (_it.is_end):
124         length += _it.object.length_2d
125         if (_it.is_begin):
126             # _it has looped back to the beginning;
127             # break to prevent infinite loop
128             break
129         _it.increment()
130
131     # reset iterator
132     _it.begin = ve
133     _it.current_edge = ve
134     _it.orientation = orientation
135
136     # run iterator till begin of chain
137     if not _it.is_begin:
138         _it.decrement()
139         while not (_it.is_end or _it.is_begin):
140             length += _it.object.length_2d
141             _it.decrement()
142
143     return length
144
145
146 def find_matching_vertex(id, it):
147     """Finds the matching vertex, or returns None."""
148     return next((ve for ve in it if ve.id == id), None)
149
150 # -- helper functions for iterating -- #
151
152 def pairwise(iterable, types={Stroke, StrokeVertexIterator}):
153     """Yields a tuple containing the previous and current object """
154     # use .incremented() for types that support it
155     if type(iterable) in types:
156         it = iter(iterable)
157         return zip(it, it.incremented())
158     else:
159         a, b = tee(iterable)
160         next(b, None)
161         return zip(a, b)
162
163
164 def tripplewise(iterable):
165     """Yields a tuple containing the current object and its immediate neighbors """
166     a, b, c = tee(iterable)
167     next(b, None)
168     next(c, None)
169     return zip(a, b, c)
170
171
172 def iter_t2d_along_stroke(stroke):
173     """Yields the progress along the stroke."""
174     total = stroke.length_2d
175     distance = 0.0
176     # yield for the comparison from the first vertex to itself
177     yield 0.0
178     for prev, svert in pairwise(stroke):
179         distance += (prev.point - svert.point).length
180         yield min(distance / total, 1.0) if total != 0.0 else 0.0
181
182
183 def iter_distance_from_camera(stroke, range_min, range_max, normfac):
184     """
185     Yields the distance to the camera relative to the maximum
186     possible distance for every stroke vertex, constrained by
187     given minimum and maximum values.
188     """
189     for svert in stroke:
190         # length in the camera coordinate
191         distance = svert.point_3d.length
192         if range_min < distance < range_max:
193             yield (svert, (distance - range_min) / normfac)
194         else:
195             yield (svert, 0.0) if range_min > distance else (svert, 1.0)
196
197
198 def iter_distance_from_object(stroke, location, range_min, range_max, normfac):
199     """
200     yields the distance to the given object relative to the maximum
201     possible distance for every stroke vertex, constrained by
202     given minimum and maximum values.
203     """
204     for svert in stroke:
205         distance = (svert.point_3d - location).length  # in the camera coordinate
206         if range_min < distance < range_max:
207             yield (svert, (distance - range_min) / normfac)
208         else:
209             yield (svert, 0.0) if distance < range_min else (svert, 1.0)
210
211
212 def iter_material_value(stroke, func, attribute):
213     "Yields a specific material attribute from the vertex' underlying material."
214     it = Interface0DIterator(stroke)
215     for svert in it:
216         material = func(it)
217         # main
218         if attribute == 'LINE':
219             value = rgb_to_bw(*material.line[0:3])
220         elif attribute == 'DIFF':
221             value = rgb_to_bw(*material.diffuse[0:3])
222         elif attribute == 'SPEC':
223             value = rgb_to_bw(*material.specular[0:3])
224         # line seperate
225         elif attribute == 'LINE_R':
226             value = material.line[0]
227         elif attribute == 'LINE_G':
228             value = material.line[1]
229         elif attribute == 'LINE_B':
230             value = material.line[2]
231         elif attribute == 'LINE_A':
232             value = material.line[3]
233         # diffuse seperate
234         elif attribute == 'DIFF_R':
235             value = material.diffuse[0]
236         elif attribute == 'DIFF_G':
237             value = material.diffuse[1]
238         elif attribute == 'DIFF_B':
239             value = material.diffuse[2]
240         elif attribute == 'ALPHA':
241             value = material.diffuse[3]
242         # specular seperate
243         elif attribute == 'SPEC_R':
244             value = material.specular[0]
245         elif attribute == 'SPEC_G':
246             value = material.specular[1]
247         elif attribute == 'SPEC_B':
248             value = material.specular[2]
249         elif attribute == 'SPEC_HARDNESS':
250             value = material.shininess
251         else:
252             raise ValueError("unexpected material attribute: " + attribute)
253         yield (svert, value)
254
255 def iter_distance_along_stroke(stroke):
256     "Yields the absolute distance along the stroke up to the current vertex."
257     distance = 0.0
258     # the positions need to be copied, because they are changed in the calling function
259     points = tuple(svert.point.copy() for svert in stroke)
260     yield distance
261     for prev, curr in pairwise(points):
262         distance += (prev - curr).length
263         yield distance
264
265 # -- mathmatical operations -- #
266
267
268 def stroke_curvature(it):
269     """
270     Compute the 2D curvature at the stroke vertex pointed by the iterator 'it'.
271     K = 1 / R
272     where R is the radius of the circle going through the current vertex and its neighbors
273     """
274     for _ in it:
275         if (it.is_begin or it.is_end):
276             yield 0.0
277             continue
278         else:
279             it.decrement()
280             prev, current, succ = it.object.point.copy(), next(it).point.copy(), next(it).point.copy()
281             # return the iterator in an unchanged state
282             it.decrement()
283
284         ab = (current - prev)
285         bc = (succ - current)
286         ac = (prev - succ)
287
288         a, b, c = ab.length, bc.length, ac.length
289
290         try:
291             area = 0.5 * ab.cross(ac)
292             K = (4 * area) / (a * b * c)
293         except ZeroDivisionError:
294             K = 0.0
295
296         yield abs(K)
297
298 def stroke_normal(stroke):
299     """
300     Compute the 2D normal at the stroke vertex pointed by the iterator
301     'it'.  It is noted that Normal2DF0D computes normals based on
302     underlying FEdges instead, which is inappropriate for strokes when
303     they have already been modified by stroke geometry modifiers.
304
305     The returned normals are dynamic: they update when the
306     vertex position (and therefore the vertex normal) changes.
307     for use in geometry modifiers it is advised to 
308     cast this generator function to a tuple or list
309     """
310     n = len(stroke) - 1
311
312     for i, svert in enumerate(stroke):
313         if i == 0:
314             e = stroke[i + 1].point - svert.point
315             yield Vector((e[1], -e[0])).normalized()
316         elif i == n:
317             e = svert.point - stroke[i - 1].point
318             yield Vector((e[1], -e[0])).normalized()
319         else:
320             e1 = stroke[i + 1].point - svert.point
321             e2 = svert.point - stroke[i - 1].point
322             n1 = Vector((e1[1], -e1[0])).normalized()
323             n2 = Vector((e2[1], -e2[0])).normalized()
324             yield (n1 + n2).normalized()
325
326 def get_test_stroke():
327     """Returns a static stroke object for testing """
328     from freestyle.types import Stroke, Interface0DIterator, StrokeVertexIterator, SVertex, Id, StrokeVertex
329     # points for our fake stroke
330     points = (Vector((1.0, 5.0, 3.0)), Vector((1.0, 2.0, 9.0)),
331               Vector((6.0, 2.0, 3.0)), Vector((7.0, 2.0, 3.0)), 
332               Vector((2.0, 6.0, 3.0)), Vector((2.0, 8.0, 3.0)))
333     ids = (Id(0, 0), Id(1, 1), Id(2, 2), Id(3, 3), Id(4, 4), Id(5, 5))
334
335     stroke = Stroke()
336     it = iter(stroke)
337
338     for svert in map(SVertex, points, ids):
339         stroke.insert_vertex(StrokeVertex(svert), it)
340         it = iter(stroke)
341
342     stroke.update_length()
343     return stroke