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
This commit is contained in:
2015-05-31 17:46:58 +09:00
parent 3100fbef5e
commit 3ca0870023
6 changed files with 190 additions and 41 deletions

View File

@@ -189,11 +189,13 @@ class CurveMaterialF0D(UnaryFunction0DMaterial):
priority is used to pick one of the two materials at material priority is used to pick one of the two materials at material
boundaries. 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): def __call__(self, inter):
fe = inter.object.fedge fe = inter.object.fedge
assert(fe is not None), "CurveMaterialF0D: fe is None" if fe is None:
return None
if fe.is_smooth: if fe.is_smooth:
return fe.material return fe.material
else: else:

View File

@@ -43,6 +43,7 @@ __all__ = (
"FalseUP0D", "FalseUP0D",
"FalseUP1D", "FalseUP1D",
"Length2DBP1D", "Length2DBP1D",
"MaterialBP1D",
"NotBP1D", "NotBP1D",
"NotUP1D", "NotUP1D",
"ObjectNamesUP1D", "ObjectNamesUP1D",
@@ -150,12 +151,13 @@ from freestyle.functions import (
pyViewMapGradientNormF1D, pyViewMapGradientNormF1D,
) )
from freestyle.utils import material_from_fedge
import random import random
# -- Unary predicates for 0D elements (vertices) -- # # -- Unary predicates for 0D elements (vertices) -- #
class pyHigherCurvature2DAngleUP0D(UnaryPredicate0D): class pyHigherCurvature2DAngleUP0D(UnaryPredicate0D):
def __init__(self, a): def __init__(self, a):
UnaryPredicate0D.__init__(self) UnaryPredicate0D.__init__(self)
@@ -234,9 +236,10 @@ class AndUP1D(UnaryPredicate1D):
def __init__(self, *predicates): def __init__(self, *predicates):
UnaryPredicate1D.__init__(self) UnaryPredicate1D.__init__(self)
self.predicates = predicates self.predicates = predicates
# there are cases in which only one predicate is supplied (in the parameter editor) correct_types = all(isinstance(p, UnaryPredicate1D) for p in self.predicates)
if len(self.predicates) < 1: if not (correct_types and predicates):
raise ValueError("Expected one or more UnaryPredicate1D, got ", len(predicates)) raise TypeError("%s: Expected one or more UnaryPredicate1D, got %r" %
(self.__class__.__name__, self.predicates))
def __call__(self, inter): def __call__(self, inter):
return all(pred(inter) for pred in self.predicates) return all(pred(inter) for pred in self.predicates)
@@ -246,9 +249,10 @@ class OrUP1D(UnaryPredicate1D):
def __init__(self, *predicates): def __init__(self, *predicates):
UnaryPredicate1D.__init__(self) UnaryPredicate1D.__init__(self)
self.predicates = predicates self.predicates = predicates
# there are cases in which only one predicate is supplied (in the parameter editor) correct_types = all(isinstance(p, UnaryPredicate1D) for p in self.predicates)
if len(self.predicates) < 1: if not (correct_types and predicates):
raise ValueError("Expected one or more UnaryPredicate1D, got ", len(predicates)) raise TypeError("%s: Expected one or more UnaryPredicate1D, got %r" %
(self.__class__.__name__, self.predicates))
def __call__(self, inter): def __call__(self, inter):
return any(pred(inter) for pred in self.predicates) return any(pred(inter) for pred in self.predicates)
@@ -257,10 +261,10 @@ class OrUP1D(UnaryPredicate1D):
class NotUP1D(UnaryPredicate1D): class NotUP1D(UnaryPredicate1D):
def __init__(self, pred): def __init__(self, pred):
UnaryPredicate1D.__init__(self) UnaryPredicate1D.__init__(self)
self.__pred = pred self.predicate = pred
def __call__(self, inter): def __call__(self, inter):
return not self.__pred(inter) return not self.predicate(inter)
class ObjectNamesUP1D(UnaryPredicate1D): class ObjectNamesUP1D(UnaryPredicate1D):
@@ -563,32 +567,36 @@ class pyClosedCurveUP1D(UnaryPredicate1D):
class AndBP1D(BinaryPredicate1D): class AndBP1D(BinaryPredicate1D):
def __init__(self, *predicates): def __init__(self, *predicates):
BinaryPredicate1D.__init__(self) BinaryPredicate1D.__init__(self)
self._predicates = predicates self.predicates = tuple(predicates)
if len(predicates) < 2: correct_types = all(isinstance(p, BinaryPredicate1D) for p in self.predicates)
raise ValueError("Expected two or more BinaryPredicate1D, got ", len(predictates)) 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): 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): class OrBP1D(BinaryPredicate1D):
def __init__(self, *predicates): def __init__(self, *predicates):
BinaryPredicate1D.__init__(self) BinaryPredicate1D.__init__(self)
self._predicates = predicates self.predicates = tuple(predicates)
if len(predicates) < 2: correct_types = all(isinstance(p, BinaryPredicate1D) for p in self.predicates)
raise ValueError("Expected two or more BinaryPredicate1D, got ", len(predictates)) 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): 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): class NotBP1D(BinaryPredicate1D):
def __init__(self, predicate): def __init__(self, predicate):
BinaryPredicate1D.__init__(self) BinaryPredicate1D.__init__(self)
self._predicate = predicate self.predicate = predicate
def __call__(self, i1, i2): def __call__(self, i1, i2):
return (not self._predicate(i1, i2)) return (not self.predicate(i1, i2))
class pyZBP1D(BinaryPredicate1D): class pyZBP1D(BinaryPredicate1D):
@@ -663,3 +671,10 @@ class pyShuffleBP1D(BinaryPredicate1D):
def __call__(self, inter1, inter2): def __call__(self, inter1, inter2):
return (random.uniform(0, 1) < random.uniform(0, 1)) 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

View File

@@ -138,7 +138,7 @@ from freestyle.predicates import (
from freestyle.utils import ( from freestyle.utils import (
bound, bound,
bounding_box, BoundingBox,
phase_to_direction, phase_to_direction,
) )
@@ -865,7 +865,7 @@ class pyBluePrintCirclesShader(StrokeShader):
def shade(self, stroke): def shade(self, stroke):
# get minimum and maximum coordinates # 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) stroke.resample(32 * self.__turns)
sv_nb = len(stroke) // self.__turns sv_nb = len(stroke) // self.__turns
@@ -917,7 +917,7 @@ class pyBluePrintEllipsesShader(StrokeShader):
self.__random_radius = random_radius self.__random_radius = random_radius
def shade(self, stroke): 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) stroke.resample(32 * self.__turns)
sv_nb = len(stroke) // self.__turns sv_nb = len(stroke) // self.__turns
@@ -964,7 +964,7 @@ class pyBluePrintSquaresShader(StrokeShader):
return return
# get minimum and maximum coordinates # 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) stroke.resample(32 * self.__turns)
num_segments = len(stroke) // self.__turns num_segments = len(stroke) // self.__turns

View File

@@ -22,24 +22,29 @@ writing.
""" """
__all__ = ( __all__ = (
"ContextFunctions",
"bound", "bound",
"bounding_box", "BoundingBox",
"ContextFunctions",
"find_matching_vertex", "find_matching_vertex",
"getCurrentScene",
"get_chain_length", "get_chain_length",
"get_object_name",
"get_strokes",
"get_test_stroke", "get_test_stroke",
"getCurrentScene",
"integrate", "integrate",
"is_poly_clockwise",
"iter_distance_along_stroke", "iter_distance_along_stroke",
"iter_distance_from_camera", "iter_distance_from_camera",
"iter_distance_from_object", "iter_distance_from_object",
"iter_material_value", "iter_material_value",
"iter_t2d_along_stroke", "iter_t2d_along_stroke",
"material_from_fedge",
"pairwise", "pairwise",
"phase_to_direction", "phase_to_direction",
"rgb_to_bw", "rgb_to_bw",
"stroke_curvature", "stroke_curvature",
"stroke_normal", "stroke_normal",
"StrokeCollector",
"tripplewise", "tripplewise",
) )
@@ -55,6 +60,7 @@ from _freestyle import (
from freestyle.types import ( from freestyle.types import (
Interface0DIterator, Interface0DIterator,
Stroke, Stroke,
StrokeShader,
StrokeVertexIterator, StrokeVertexIterator,
) )
@@ -79,12 +85,38 @@ def bound(lower, x, higher):
return (lower if x <= lower else higher if x >= higher else x) return (lower if x <= lower else higher if x >= higher else x)
def bounding_box(stroke): def get_strokes():
""" """Get all strokes that are currently available"""
Returns the maximum and minimum coordinates (the bounding box) of the stroke's vertices return tuple(map(Operators().get_stroke_from_index, range(Operators().get_strokes_size())))
"""
x, y = zip(*(svert.point for svert in stroke))
return (Vector((min(x), min(y))), Vector((max(x), max(y)))) 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 -- # # -- General helper functions -- #
@@ -106,6 +138,54 @@ def phase_to_direction(length):
# lower bound (e.g., thickness, range and certain values) # lower bound (e.g., thickness, range and certain values)
BoundedProperty = namedtuple("BoundedProperty", ["min", "max", "delta"]) 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 -- # # -- helper functions for chaining -- #
def get_chain_length(ve, orientation): def get_chain_length(ve, orientation):
@@ -147,6 +227,7 @@ def find_matching_vertex(id, it):
"""Finds the matching vertex, or returns None.""" """Finds the matching vertex, or returns None."""
return next((ve for ve in it if ve.id == id), None) return next((ve for ve in it if ve.id == id), None)
# -- helper functions for iterating -- # # -- helper functions for iterating -- #
def pairwise(iterable, types={Stroke, StrokeVertexIterator}): 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): 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) it = Interface0DIterator(stroke)
for svert in it: for svert in it:
material = func(it) material = func(it)
@@ -252,8 +333,9 @@ def iter_material_value(stroke, func, attribute):
raise ValueError("unexpected material attribute: " + attribute) raise ValueError("unexpected material attribute: " + attribute)
yield (svert, value) yield (svert, value)
def iter_distance_along_stroke(stroke): 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 distance = 0.0
# the positions need to be copied, because they are changed in the calling function # the positions need to be copied, because they are changed in the calling function
points = tuple(svert.point.copy() for svert in stroke) points = tuple(svert.point.copy() for svert in stroke)
@@ -295,6 +377,7 @@ def stroke_curvature(it):
yield abs(K) yield abs(K)
def stroke_normal(stroke): def stroke_normal(stroke):
""" """
Compute the 2D normal at the stroke vertex pointed by the iterator Compute the 2D normal at the stroke vertex pointed by the iterator
@@ -323,6 +406,7 @@ def stroke_normal(stroke):
n2 = Vector((e2[1], -e2[0])).normalized() n2 = Vector((e2[1], -e2[0])).normalized()
yield (n1 + n2).normalized() yield (n1 + n2).normalized()
def get_test_stroke(): def get_test_stroke():
"""Returns a static stroke object for testing """ """Returns a static stroke object for testing """
from freestyle.types import Stroke, Interface0DIterator, StrokeVertexIterator, SVertex, Id, StrokeVertex from freestyle.types import Stroke, Interface0DIterator, StrokeVertexIterator, SVertex, Id, StrokeVertex

View File

@@ -30,6 +30,9 @@
extern "C" { extern "C" {
#endif #endif
#include "BLI_hash_mm2a.h"
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
//-------------------MODULE INITIALIZATION-------------------------------- //-------------------MODULE INITIALIZATION--------------------------------
@@ -478,6 +481,48 @@ static PyGetSetDef BPy_FrsMaterial_getseters[] = {
{NULL, NULL, NULL, NULL, NULL} /* Sentinel */ {NULL, NULL, NULL, NULL, NULL} /* Sentinel */
}; };
static PyObject *BPy_FrsMaterial_richcmpr(PyObject *objectA, PyObject *objectB, int comparison_type)
{
const BPy_FrsMaterial *matA = NULL, *matB = NULL;
bool result = 0;
if (!BPy_FrsMaterial_Check(objectA) || !BPy_FrsMaterial_Check(objectB)) {
if (comparison_type == Py_NE) {
Py_RETURN_TRUE;
}
else {
Py_RETURN_FALSE;
}
}
matA = (BPy_FrsMaterial *)objectA;
matB = (BPy_FrsMaterial *)objectB;
switch (comparison_type) {
case Py_NE:
result = (*matA->m) != (*matB->m);
break;
case Py_EQ:
result = (*matA->m) == (*matB->m);
break;
default:
PyErr_SetString(PyExc_TypeError, "Material does not support this comparison type");
return NULL;
}
if (result == true) {
Py_RETURN_TRUE;
}
else {
Py_RETURN_FALSE;
}
}
static Py_hash_t FrsMaterial_hash(PyObject *self)
{
return (Py_uhash_t)BLI_hash_mm2((const unsigned char *)self, sizeof(*self), 0);
}
/*-----------------------BPy_FrsMaterial type definition ------------------------------*/ /*-----------------------BPy_FrsMaterial type definition ------------------------------*/
PyTypeObject FrsMaterial_Type = { PyTypeObject FrsMaterial_Type = {
@@ -494,7 +539,7 @@ PyTypeObject FrsMaterial_Type = {
0, /* tp_as_number */ 0, /* tp_as_number */
0, /* tp_as_sequence */ 0, /* tp_as_sequence */
0, /* tp_as_mapping */ 0, /* tp_as_mapping */
0, /* tp_hash */ (hashfunc)FrsMaterial_hash, /* tp_hash */
0, /* tp_call */ 0, /* tp_call */
0, /* tp_str */ 0, /* tp_str */
0, /* tp_getattro */ 0, /* tp_getattro */
@@ -504,7 +549,7 @@ PyTypeObject FrsMaterial_Type = {
FrsMaterial_doc, /* tp_doc */ FrsMaterial_doc, /* tp_doc */
0, /* tp_traverse */ 0, /* tp_traverse */
0, /* tp_clear */ 0, /* tp_clear */
0, /* tp_richcompare */ (richcmpfunc)BPy_FrsMaterial_richcmpr, /* tp_richcompare */
0, /* tp_weaklistoffset */ 0, /* tp_weaklistoffset */
0, /* tp_iter */ 0, /* tp_iter */
0, /* tp_iternext */ 0, /* tp_iternext */

View File

@@ -188,7 +188,10 @@ static PyObject *CurvePoint_fedge_get(BPy_CurvePoint *self, void *UNUSED(closure
{ {
SVertex *A = self->cp->A(); SVertex *A = self->cp->A();
Interface0D *B = (Interface0D *)self->cp->B(); Interface0D *B = (Interface0D *)self->cp->B();
return Any_BPy_Interface1D_from_Interface1D(*(A->getFEdge(*B))); // B can be NULL under certain circumstances
if (B)
return Any_BPy_Interface1D_from_Interface1D(*(A->getFEdge(*B)));
Py_RETURN_NONE;
} }
PyDoc_STRVAR(CurvePoint_t2d_doc, PyDoc_STRVAR(CurvePoint_t2d_doc,