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:
Tamito Kajiyama
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
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:

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -30,6 +30,9 @@
extern "C" {
#endif
#include "BLI_hash_mm2a.h"
///////////////////////////////////////////////////////////////////////////////////////////
//-------------------MODULE INITIALIZATION--------------------------------
@@ -478,6 +481,48 @@ static PyGetSetDef BPy_FrsMaterial_getseters[] = {
{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 ------------------------------*/
PyTypeObject FrsMaterial_Type = {
@@ -494,7 +539,7 @@ PyTypeObject FrsMaterial_Type = {
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
(hashfunc)FrsMaterial_hash, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
@@ -504,7 +549,7 @@ PyTypeObject FrsMaterial_Type = {
FrsMaterial_doc, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
(richcmpfunc)BPy_FrsMaterial_richcmpr, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
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();
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,