diff options
Diffstat (limited to 'release/scripts/freestyle/modules/freestyle/utils.py')
-rw-r--r-- | release/scripts/freestyle/modules/freestyle/utils.py | 282 |
1 files changed, 248 insertions, 34 deletions
diff --git a/release/scripts/freestyle/modules/freestyle/utils.py b/release/scripts/freestyle/modules/freestyle/utils.py index e0679a53954..d3eba187f70 100644 --- a/release/scripts/freestyle/modules/freestyle/utils.py +++ b/release/scripts/freestyle/modules/freestyle/utils.py @@ -22,28 +22,37 @@ writing. """ __all__ = ( - "ContextFunctions", + "angle_x_normal", "bound", "bounding_box", + "BoundingBox", + "ContextFunctions", + "curvature_from_stroke_vertex", "find_matching_vertex", - "getCurrentScene", "get_chain_length", + "get_object_name", + "get_strokes", "get_test_stroke", + "getCurrentScene", "integrate", + "is_poly_clockwise", "iter_distance_along_stroke", "iter_distance_from_camera", "iter_distance_from_object", "iter_material_value", "iter_t2d_along_stroke", + "material_from_fedge", + "normal_at_I0D", "pairwise", "phase_to_direction", "rgb_to_bw", + "simplify", "stroke_curvature", "stroke_normal", + "StrokeCollector", "tripplewise", ) - # module members from _freestyle import ( ContextFunctions, @@ -55,13 +64,29 @@ from _freestyle import ( from freestyle.types import ( Interface0DIterator, Stroke, + StrokeShader, StrokeVertexIterator, ) from mathutils import Vector from functools import lru_cache, namedtuple -from math import cos, sin, pi -from itertools import tee +from math import cos, sin, pi, atan2 +from itertools import tee, compress + + +# -- types -- # + +# A named tuple primitive used for storing data that has an upper and +# lower bound (e.g., thickness, range and certain other values) +class BoundedProperty(namedtuple("BoundedProperty", ["min", "max", "delta"])): + def __new__(cls, minimum, maximum, delta=None): + if delta is None: + delta = abs(maximum - minimum) + return super().__new__(cls, minimum, maximum, delta) + + def interpolate(self, val): + result = (self.max - val) / self.delta + return 1.0 - bound(0, result, 1) # -- real utility functions -- # @@ -79,6 +104,40 @@ def bound(lower, x, higher): return (lower if x <= lower else higher if x >= higher else x) +def get_strokes(): + """Get all strokes that are currently available""" + return tuple(map(Operators().get_stroke_from_index, range(Operators().get_strokes_size()))) + + +def is_poly_clockwise(stroke): + """True if the stroke is orientated in a clockwise way, False otherwise""" + v = sum((v2.point.x - v1.point.x) * (v1.point.y + v2.point.y) for v1, v2 in pairwise(stroke)) + v1, v2 = stroke[0], stroke[-1] + if (v1.point - v2.point).length > 1e-3: + v += (v2.point.x - v1.point.x) * (v1.point.y + v2.point.y) + return v > 0 + + +def get_object_name(stroke): + """Returns the name of the object that this stroke is drawn on.""" + fedge = stroke[0].fedge + if fedge is None: + return None + return fedge.viewedge.viewshape.name + + +def material_from_fedge(fe): + "get the diffuse rgba color from an FEdge" + if fe is None: + return None + if fe.is_smooth: + material = fe.material + else: + right, left = fe.material_right, fe.material_left + material = right if (right.priority > left.priority) else left + return material + + def bounding_box(stroke): """ Returns the maximum and minimum coordinates (the bounding box) of the stroke's vertices @@ -86,8 +145,57 @@ def bounding_box(stroke): x, y = zip(*(svert.point for svert in stroke)) return (Vector((min(x), min(y))), Vector((max(x), max(y)))) -# -- General helper functions -- # +def normal_at_I0D(it: Interface0DIterator) -> Vector: + """Normal at an Interface0D object. In contrast to Normal2DF0D this + function uses the actual data instead of underlying Fedge objects. + """ + if it.at_last and it.is_begin: + # corner-case + return Vector((0, 0)) + elif it.at_last: + it.decrement() + a, b = it.object, next(it) + elif it.is_begin: + a, b = it.object, next(it) + # give iterator back in original state + it.decrement() + elif it.is_end: + # just fail hard: this shouldn not happen + raise StopIteration() + else: + # this case sometimes has a small difference with Normal2DF0D (1e-3 -ish) + it.decrement() + a = it.object + curr, b = next(it), next(it) + # give iterator back in original state + it.decrement() + return (b.point - a.point).orthogonal().normalized() + + +def angle_x_normal(it: Interface0DIterator): + """unsigned angle between a Point's normal and the X axis, in radians""" + normal = normal_at_I0D(it) + return abs(atan2(normal[1], normal[0])) + + +def curvature_from_stroke_vertex(svert): + """The 3D curvature of an stroke vertex' underlying geometry + The result is None or in the range [-inf, inf]""" + c1 = svert.first_svertex.curvatures + c2 = svert.second_svertex.curvatures + if c1 is None and c2 is None: + Kr = None + elif c1 is None: + Kr = c2[4] + elif c2 is None: + Kr = c1[4] + else: + Kr = c1[4] + svert.t2d * (c2[4] - c1[4]) + return Kr + + +# -- General helper functions -- # @lru_cache(maxsize=32) def phase_to_direction(length): @@ -102,9 +210,122 @@ def phase_to_direction(length): results.append((phase, Vector((cos(2 * pi * phase), sin(2 * pi * phase))))) return results -# A named tuple primitive used for storing data that has an upper and -# lower bound (e.g., thickness, range and certain values) -BoundedProperty = namedtuple("BoundedProperty", ["min", "max", "delta"]) + +# -- simplification of a set of points; based on simplify.js by Vladimir Agafonkin -- +# https://mourner.github.io/simplify-js/ + +def getSquareSegmentDistance(p, p1, p2): + """ + Square distance between point and a segment + """ + x, y = p1 + + dx, dy = (p2 - p1) + + if dx or dy: + t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy) + + if t > 1: + x, y = p2 + elif t > 0: + x += dx * t + y += dy * t + + dx, dy = p.x - x, p.y - y + return dx * dx + dy * dy + + +def simplifyDouglasPeucker(points, tolerance): + length = len(points) + markers = [0] * length + + first = 0 + last = length - 1 + + first_stack = [] + last_stack = [] + + new_points = [] + + markers[first] = 1 + markers[last] = 1 + + while last: + max_sqdist = 0 + + for i in range(first, last): + sqdist = getSquareSegmentDistance(points[i], points[first], points[last]) + + if sqdist > max_sqdist: + index = i + max_sqdist = sqdist + + if max_sqdist > tolerance: + markers[index] = 1 + + first_stack.append(first) + last_stack.append(index) + + first_stack.append(index) + last_stack.append(last) + + first = first_stack.pop() if first_stack else None + last = last_stack.pop() if last_stack else None + + return tuple(compress(points, markers)) + + +def simplify(points, tolerance): + """Simplifies a set of points""" + return simplifyDouglasPeucker(points, tolerance * tolerance) + + +class BoundingBox: + """Object representing a bounding box consisting out of 2 2D vectors""" + + __slots__ = ( + "minimum", + "maximum", + "size", + "corners", + ) + + def __init__(self, minimum: Vector, maximum: Vector): + self.minimum = minimum + self.maximum = maximum + if len(minimum) != len(maximum): + raise TypeError("Expected two vectors of size 2, got", minimum, maximum) + self.size = len(minimum) + self.corners = (minimum, maximum) + + def __repr__(self): + return "BoundingBox(%r, %r)" % (self.minimum, self.maximum) + + @classmethod + def from_sequence(cls, sequence): + """BoundingBox from sequence of 2D or 3D Vector objects""" + x, y = zip(*sequence) + mini = Vector((min(x), min(y))) + maxi = Vector((max(x), max(y))) + return cls(mini, maxi) + + def inside(self, other): + """True if self inside other, False otherwise""" + if self.size != other.size: + raise TypeError("Expected two BoundingBox of the same size, got", self, other) + return (self.minimum.x >= other.minimum.x and self.minimum.y >= other.minimum.y and + self.maximum.x <= other.maximum.x and self.maximum.y <= other.maximum.y) + + +class StrokeCollector(StrokeShader): + "Collects and Stores stroke objects" + def __init__(self): + StrokeShader.__init__(self) + self.strokes = [] + + def shade(self, stroke): + self.strokes.append(stroke) + # -- helper functions for chaining -- # @@ -147,6 +368,7 @@ def find_matching_vertex(id, it): """Finds the matching vertex, or returns None.""" return next((ve for ve in it if ve.id == id), None) + # -- helper functions for iterating -- # def pairwise(iterable, types={Stroke, StrokeVertexIterator}): @@ -210,34 +432,36 @@ def iter_distance_from_object(stroke, location, range_min, range_max, normfac): def iter_material_value(stroke, func, attribute): - "Yields a specific material attribute from the vertex' underlying material." + """Yields a specific material attribute from the vertex' underlying material.""" it = Interface0DIterator(stroke) for svert in it: material = func(it) # main if attribute == 'LINE': value = rgb_to_bw(*material.line[0:3]) - elif attribute == 'ALPHA': - value = material.line[3] elif attribute == 'DIFF': value = rgb_to_bw(*material.diffuse[0:3]) elif attribute == 'SPEC': value = rgb_to_bw(*material.specular[0:3]) - # line seperate + # line separate elif attribute == 'LINE_R': value = material.line[0] elif attribute == 'LINE_G': value = material.line[1] elif attribute == 'LINE_B': value = material.line[2] - # diffuse seperate + elif attribute == 'LINE_A': + value = material.line[3] + # diffuse separate elif attribute == 'DIFF_R': value = material.diffuse[0] elif attribute == 'DIFF_G': value = material.diffuse[1] elif attribute == 'DIFF_B': value = material.diffuse[2] - # specular seperate + elif attribute == 'ALPHA': + value = material.diffuse[3] + # specular separate elif attribute == 'SPEC_R': value = material.specular[0] elif attribute == 'SPEC_G': @@ -250,8 +474,9 @@ def iter_material_value(stroke, func, attribute): raise ValueError("unexpected material attribute: " + attribute) yield (svert, value) + def iter_distance_along_stroke(stroke): - "Yields the absolute distance along the stroke up to the current vertex." + """Yields the absolute distance along the stroke up to the current vertex.""" distance = 0.0 # the positions need to be copied, because they are changed in the calling function points = tuple(svert.point.copy() for svert in stroke) @@ -260,8 +485,8 @@ def iter_distance_along_stroke(stroke): distance += (prev - curr).length yield distance -# -- mathmatical operations -- # +# -- mathematical operations -- # def stroke_curvature(it): """ @@ -293,6 +518,7 @@ def stroke_curvature(it): yield abs(K) + def stroke_normal(stroke): """ Compute the 2D normal at the stroke vertex pointed by the iterator @@ -302,31 +528,19 @@ def stroke_normal(stroke): The returned normals are dynamic: they update when the vertex position (and therefore the vertex normal) changes. - for use in geometry modifiers it is advised to + for use in geometry modifiers it is advised to cast this generator function to a tuple or list """ - n = len(stroke) - 1 - - for i, svert in enumerate(stroke): - if i == 0: - e = stroke[i + 1].point - svert.point - yield Vector((e[1], -e[0])).normalized() - elif i == n: - e = svert.point - stroke[i - 1].point - yield Vector((e[1], -e[0])).normalized() - else: - e1 = stroke[i + 1].point - svert.point - e2 = svert.point - stroke[i - 1].point - n1 = Vector((e1[1], -e1[0])).normalized() - n2 = Vector((e2[1], -e2[0])).normalized() - yield (n1 + n2).normalized() + it = iter(stroke) + yield from (normal_at_I0D(it) for _ in it) + def get_test_stroke(): """Returns a static stroke object for testing """ from freestyle.types import Stroke, Interface0DIterator, StrokeVertexIterator, SVertex, Id, StrokeVertex # points for our fake stroke points = (Vector((1.0, 5.0, 3.0)), Vector((1.0, 2.0, 9.0)), - Vector((6.0, 2.0, 3.0)), Vector((7.0, 2.0, 3.0)), + Vector((6.0, 2.0, 3.0)), Vector((7.0, 2.0, 3.0)), Vector((2.0, 6.0, 3.0)), Vector((2.0, 8.0, 3.0))) ids = (Id(0, 0), Id(1, 1), Id(2, 2), Id(3, 3), Id(4, 4), Id(5, 5)) |