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:
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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))
|
||||
|
||||
|
@@ -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 */
|
||||
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user