diff options
author | Tamito Kajiyama <rd6t-kjym@asahi-net.or.jp> | 2015-05-31 11:46:58 +0300 |
---|---|---|
committer | Tamito Kajiyama <rd6t-kjym@asahi-net.or.jp> | 2015-05-31 17:16:45 +0300 |
commit | 3ca0870023bb71bc929925a8fc8d172c09df710f (patch) | |
tree | 8164c8c5bc86cd6e4344cd3ae625e26d18a566c6 /release/scripts/freestyle/modules | |
parent | 3100fbef5e0e3943eb53e451e00dd23a11bf3017 (diff) |
Improvements to the Freestyle Python API (needed by the SVG Exporter)
This patch adds some new functionality to the Freestyle Python API, notably:
- MaterialBP1D, checks whether the supplied arguments have the same material
- Fixes a potential crash in CurvePoint.fedge (due to NULL pointer)
- Makes (error handling in) boolean predicates more robust
- Adds a BoundingBox type, to make working with bounding boxes easier
- Adds several new functions (get_object_name, get_strokes, is_poly_clockwise, material_from_fedge)
- Adds a StrokeCollector StrokeShader, that collects all the strokes from a specific call to Operators.create()
- Adds hashing and rich comparison to the FrsMaterial type
These new features (most of them, anyway) are needed for making a more robust SVG exporter that supports holes in fills.
Reviewers: kjym3, campbellbarton
Subscribers: campbellbarton
Projects: #bf_blender
Differential Revision: https://developer.blender.org/D1245
Diffstat (limited to 'release/scripts/freestyle/modules')
4 files changed, 139 insertions, 38 deletions
diff --git a/release/scripts/freestyle/modules/freestyle/functions.py b/release/scripts/freestyle/modules/freestyle/functions.py index 48d9b2e2b39..426d344e8ab 100644 --- a/release/scripts/freestyle/modules/freestyle/functions.py +++ b/release/scripts/freestyle/modules/freestyle/functions.py @@ -189,11 +189,13 @@ class CurveMaterialF0D(UnaryFunction0DMaterial): priority is used to pick one of the two materials at material boundaries. - Note: expects instances of CurvePoint to be iterated over + Notes: expects instances of CurvePoint to be iterated over + can return None if no fedge can be found """ def __call__(self, inter): fe = inter.object.fedge - assert(fe is not None), "CurveMaterialF0D: fe is None" + if fe is None: + return None if fe.is_smooth: return fe.material else: diff --git a/release/scripts/freestyle/modules/freestyle/predicates.py b/release/scripts/freestyle/modules/freestyle/predicates.py index 2439cb0cf97..5cbe577b956 100644 --- a/release/scripts/freestyle/modules/freestyle/predicates.py +++ b/release/scripts/freestyle/modules/freestyle/predicates.py @@ -43,6 +43,7 @@ __all__ = ( "FalseUP0D", "FalseUP1D", "Length2DBP1D", + "MaterialBP1D", "NotBP1D", "NotUP1D", "ObjectNamesUP1D", @@ -150,12 +151,13 @@ from freestyle.functions import ( pyViewMapGradientNormF1D, ) +from freestyle.utils import material_from_fedge + import random # -- Unary predicates for 0D elements (vertices) -- # - class pyHigherCurvature2DAngleUP0D(UnaryPredicate0D): def __init__(self, a): UnaryPredicate0D.__init__(self) @@ -234,9 +236,10 @@ class AndUP1D(UnaryPredicate1D): def __init__(self, *predicates): UnaryPredicate1D.__init__(self) self.predicates = predicates - # there are cases in which only one predicate is supplied (in the parameter editor) - if len(self.predicates) < 1: - raise ValueError("Expected one or more UnaryPredicate1D, got ", len(predicates)) + correct_types = all(isinstance(p, UnaryPredicate1D) for p in self.predicates) + if not (correct_types and predicates): + raise TypeError("%s: Expected one or more UnaryPredicate1D, got %r" % + (self.__class__.__name__, self.predicates)) def __call__(self, inter): return all(pred(inter) for pred in self.predicates) @@ -246,9 +249,10 @@ class OrUP1D(UnaryPredicate1D): def __init__(self, *predicates): UnaryPredicate1D.__init__(self) self.predicates = predicates - # there are cases in which only one predicate is supplied (in the parameter editor) - if len(self.predicates) < 1: - raise ValueError("Expected one or more UnaryPredicate1D, got ", len(predicates)) + correct_types = all(isinstance(p, UnaryPredicate1D) for p in self.predicates) + if not (correct_types and predicates): + raise TypeError("%s: Expected one or more UnaryPredicate1D, got %r" % + (self.__class__.__name__, self.predicates)) def __call__(self, inter): return any(pred(inter) for pred in self.predicates) @@ -257,10 +261,10 @@ class OrUP1D(UnaryPredicate1D): class NotUP1D(UnaryPredicate1D): def __init__(self, pred): UnaryPredicate1D.__init__(self) - self.__pred = pred + self.predicate = pred def __call__(self, inter): - return not self.__pred(inter) + return not self.predicate(inter) class ObjectNamesUP1D(UnaryPredicate1D): @@ -563,32 +567,36 @@ class pyClosedCurveUP1D(UnaryPredicate1D): class AndBP1D(BinaryPredicate1D): def __init__(self, *predicates): BinaryPredicate1D.__init__(self) - self._predicates = predicates - if len(predicates) < 2: - raise ValueError("Expected two or more BinaryPredicate1D, got ", len(predictates)) + self.predicates = tuple(predicates) + correct_types = all(isinstance(p, BinaryPredicate1D) for p in self.predicates) + if not (correct_types and predicates): + raise TypeError("%s: Expected one or more BinaryPredicate1D, got %r" % + (self.__class__.__name__, self.predicates)) def __call__(self, i1, i2): - return all(pred(i1, i2) for pred in self._predicates) + return all(pred(i1, i2) for pred in self.predicates) class OrBP1D(BinaryPredicate1D): def __init__(self, *predicates): BinaryPredicate1D.__init__(self) - self._predicates = predicates - if len(predicates) < 2: - raise ValueError("Expected two or more BinaryPredicate1D, got ", len(predictates)) + self.predicates = tuple(predicates) + correct_types = all(isinstance(p, BinaryPredicate1D) for p in self.predicates) + if not (correct_types and predicates): + raise TypeError("%s: Expected one or more BinaryPredicate1D, got %r" % + (self.__class__.__name__, self.predicates)) def __call__(self, i1, i2): - return any(pred(i1, i2) for pred in self._predicates) + return any(pred(i1, i2) for pred in self.predicates) class NotBP1D(BinaryPredicate1D): def __init__(self, predicate): BinaryPredicate1D.__init__(self) - self._predicate = predicate + self.predicate = predicate def __call__(self, i1, i2): - return (not self._predicate(i1, i2)) + return (not self.predicate(i1, i2)) class pyZBP1D(BinaryPredicate1D): @@ -663,3 +671,10 @@ class pyShuffleBP1D(BinaryPredicate1D): def __call__(self, inter1, inter2): return (random.uniform(0, 1) < random.uniform(0, 1)) + +class MaterialBP1D(BinaryPredicate1D): + """Checks whether the two supplied ViewEdges have the same material.""" + def __call__(self, i1, i2): + fedges = (fe for ve in (i1, i2) for fe in (ve.first_fedge, ve.last_fedge)) + materials = {material_from_fedge(fe) for fe in fedges} + return len(materials) < 2 diff --git a/release/scripts/freestyle/modules/freestyle/shaders.py b/release/scripts/freestyle/modules/freestyle/shaders.py index 61365e8dd87..127db3fcd4b 100644 --- a/release/scripts/freestyle/modules/freestyle/shaders.py +++ b/release/scripts/freestyle/modules/freestyle/shaders.py @@ -138,7 +138,7 @@ from freestyle.predicates import ( from freestyle.utils import ( bound, - bounding_box, + BoundingBox, phase_to_direction, ) @@ -865,7 +865,7 @@ class pyBluePrintCirclesShader(StrokeShader): def shade(self, stroke): # get minimum and maximum coordinates - p_min, p_max = bounding_box(stroke) + p_min, p_max = BoundingBox.from_sequence(svert.point for svert in stroke).corners stroke.resample(32 * self.__turns) sv_nb = len(stroke) // self.__turns @@ -917,7 +917,7 @@ class pyBluePrintEllipsesShader(StrokeShader): self.__random_radius = random_radius def shade(self, stroke): - p_min, p_max = bounding_box(stroke) + p_min, p_max = BoundingBox.from_sequence(svert.point for svert in stroke).corners stroke.resample(32 * self.__turns) sv_nb = len(stroke) // self.__turns @@ -964,7 +964,7 @@ class pyBluePrintSquaresShader(StrokeShader): return # get minimum and maximum coordinates - p_min, p_max = bounding_box(stroke) + p_min, p_max = BoundingBox.from_sequence(svert.point for svert in stroke).corners stroke.resample(32 * self.__turns) num_segments = len(stroke) // self.__turns diff --git a/release/scripts/freestyle/modules/freestyle/utils.py b/release/scripts/freestyle/modules/freestyle/utils.py index 224734d5bfb..41d2297f723 100644 --- a/release/scripts/freestyle/modules/freestyle/utils.py +++ b/release/scripts/freestyle/modules/freestyle/utils.py @@ -22,24 +22,29 @@ writing. """ __all__ = ( - "ContextFunctions", "bound", - "bounding_box", + "BoundingBox", + "ContextFunctions", "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", "pairwise", "phase_to_direction", "rgb_to_bw", "stroke_curvature", "stroke_normal", + "StrokeCollector", "tripplewise", ) @@ -55,6 +60,7 @@ from _freestyle import ( from freestyle.types import ( Interface0DIterator, Stroke, + StrokeShader, StrokeVertexIterator, ) @@ -79,12 +85,38 @@ def bound(lower, x, higher): return (lower if x <= lower else higher if x >= higher else x) -def bounding_box(stroke): - """ - Returns the maximum and minimum coordinates (the bounding box) of the stroke's vertices - """ - x, y = zip(*(svert.point for svert in stroke)) - return (Vector((min(x), min(y))), Vector((max(x), max(y)))) +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 # -- General helper functions -- # @@ -106,6 +138,54 @@ def phase_to_direction(length): # lower bound (e.g., thickness, range and certain values) BoundedProperty = namedtuple("BoundedProperty", ["min", "max", "delta"]) + +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 -- # def get_chain_length(ve, orientation): @@ -147,6 +227,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,7 +291,7 @@ 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) @@ -252,8 +333,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) @@ -295,6 +377,7 @@ def stroke_curvature(it): yield abs(K) + def stroke_normal(stroke): """ Compute the 2D normal at the stroke vertex pointed by the iterator @@ -304,7 +387,7 @@ 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 @@ -323,12 +406,13 @@ def stroke_normal(stroke): n2 = Vector((e2[1], -e2[0])).normalized() yield (n1 + n2).normalized() + 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)) |