This extends the `GreasePencilDrawing` rna type using python. The goal is to add an API that allows developers to transition to the new grease pencil API a bit more smoothly. Adds the following endpoints to the `GreasePencilDrawing`: * `drawing.strokes`: Returns a list/slice of `GreasePencilStroke`s in the drawing. Adds a python class `GreasePencilStroke`: * `stroke.points`: Returns a list/slice of `GreasePencilStrokePoint`s. * Getters/Setters of attributes for this stroke: * `stroke.cyclic` * `stroke.material_index` * `stroke.select` * `stroke.softness` (used to be `hardness`) * `stroke.start_cap` * `stroke.end_cap` * `stroke.curve_type`: The type of curve: `POLY`,`BEZIER`,`CATMULL_ROM`,`NURBS`. * `stroke.aspect_ratio` * `stroke.fill_opacity` * `stroke.fill_color` * `stroke.time_start` * High-level functions: * `stroke.add_points(count)`: Adds `count` points at the end of the stroke. * `stroke.remove_points(count)`: Removes `count` points from the end of the stroke. Note that this will not remove the stroke if the count is greater than the number of points in the stroke. A stroke has at least 1 point. Removing strokes can be done from the drawing. Adds a python class `GreasePencilStrokePoint`: * Getters/Setters of attributes for this point: * `position` * `radius` * `opacity` * `select` * `vertex_color` * `rotation` * `delta_time` Note that `GreasePencilStroke` and `GreasePencilStrokePoint` are not stored in the file and don't have an RNA API. This means that they are not compatible with e.g. `layout.prop`. This API should not be used for performance critical code. It's likely even slower than the python API from 4.2. There will be migration documentation for addon developers here: https://developer.blender.org/docs/release_notes/4.3/grease_pencil/#python-api-changes Pull Request: https://projects.blender.org/blender/blender/pulls/125599
238 lines
9.9 KiB
Python
238 lines
9.9 KiB
Python
# SPDX-FileCopyrightText: 2024 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
class AttributeGetterSetter:
|
|
"""
|
|
Helper class to get and set attributes at an index for a domain.
|
|
"""
|
|
|
|
def __init__(self, attributes, index, domain):
|
|
self._attributes = attributes
|
|
self._index = index
|
|
self._domain = domain
|
|
|
|
def _get_attribute(self, name, type, default):
|
|
if attribute := self._attributes.get(name):
|
|
if type in {'FLOAT', 'INT', 'STRING', 'BOOLEAN', 'INT8', 'INT32_2D', 'QUATERNION', 'FLOAT4X4'}:
|
|
return attribute.data[self._index].value
|
|
elif type == 'FLOAT_VECTOR':
|
|
return attribute.data[self._index].vector
|
|
elif type in {'FLOAT_COLOR', 'BYTE_COLOR'}:
|
|
return attribute.data[self._index].color
|
|
else:
|
|
raise Exception(f"Unkown type {type}")
|
|
return default
|
|
|
|
def _set_attribute(self, name, type, value):
|
|
if attribute := self._attributes.get(name, self._attributes.new(name, type, self._domain)):
|
|
if type in {'FLOAT', 'INT', 'STRING', 'BOOLEAN', 'INT8', 'INT32_2D', 'QUATERNION', 'FLOAT4X4'}:
|
|
attribute.data[self._index].value = value
|
|
elif type == 'FLOAT_VECTOR':
|
|
attribute.data[self._index].vector = value
|
|
elif type in {'FLOAT_COLOR', 'BYTE_COLOR'}:
|
|
attribute.data[self._index].color = value
|
|
else:
|
|
raise Exception(f"Unkown type {type}")
|
|
else:
|
|
raise Exception(f"Could not create attribute {name} of type {type}")
|
|
|
|
|
|
def def_prop_for_attribute(attr_name, type, default, doc):
|
|
"""
|
|
Creates a property that can read and write an attribute.
|
|
"""
|
|
|
|
def fget(self):
|
|
# Define getter callback for property.
|
|
return self._get_attribute(attr_name, type, default)
|
|
|
|
def fset(self, value):
|
|
# Define setter callback for property.
|
|
self._set_attribute(attr_name, type, value)
|
|
prop = property(fget=fget, fset=fset, doc=doc)
|
|
return prop
|
|
|
|
|
|
def DefAttributeGetterSetters(attributes_list):
|
|
"""
|
|
A class decorator that reads a list of attribute infos and creates properties on the class with getters and setters.
|
|
"""
|
|
def wrapper(cls):
|
|
for prop_name, attr_name, type, default, doc in attributes_list:
|
|
prop = def_prop_for_attribute(attr_name, type, default, doc)
|
|
setattr(cls, prop_name, prop)
|
|
return cls
|
|
return wrapper
|
|
|
|
|
|
# Define the list of attributes that should be exposed as read/write properties on the class.
|
|
@DefAttributeGetterSetters([
|
|
# Property Name, Attribute Name, Type, Default Value, Docstring
|
|
('position', 'position', 'FLOAT_VECTOR', (0.0, 0.0, 0.0), "The position of the point (in local space)."),
|
|
('radius', 'radius', 'FLOAT', 0.01, "The radius of the point."),
|
|
('opacity', 'opacity', 'FLOAT', 0.0, "The opacity of the point."),
|
|
('select', '.selection', 'BOOLEAN', True, "The selection state for this point."),
|
|
('vertex_color', 'vertex_color', 'FLOAT_COLOR', (0.0, 0.0, 0.0, 0.0),
|
|
"The color for this point. The alpha value is used as a mix factor with the base color of the stroke."),
|
|
('rotation', 'rotation', 'FLOAT', 0.0, "The rotation for this point. Used to rotate textures."),
|
|
('delta_time', 'delta_time', 'FLOAT', 0.0, "The time delta in seconds since the start of the stroke."),
|
|
])
|
|
class GreasePencilStrokePoint(AttributeGetterSetter):
|
|
"""
|
|
A helper class to get access to stroke point data.
|
|
"""
|
|
|
|
def __init__(self, drawing, point_index):
|
|
super().__init__(drawing.attributes, point_index, 'POINT')
|
|
|
|
|
|
class GreasePencilStrokePointSlice:
|
|
"""
|
|
A helper class that represents a slice of GreasePencilStrokePoint's.
|
|
"""
|
|
|
|
def __init__(self, drawing, start, stop):
|
|
self._drawing = drawing
|
|
self._start = start
|
|
self._stop = stop
|
|
self._size = stop - start
|
|
|
|
def __len__(self):
|
|
return self._size
|
|
|
|
def _is_valid_index(self, key):
|
|
if self._size <= 0:
|
|
return False
|
|
if key < 0:
|
|
# Support indexing from the end.
|
|
return abs(key) <= self._size
|
|
return abs(key) < self._size
|
|
|
|
def __getitem__(self, key):
|
|
if isinstance(key, int):
|
|
if not self._is_valid_index(key):
|
|
raise IndexError(f"Key {key} is out of range")
|
|
# Turn the key into an index.
|
|
point_i = self._start + (key % self._size)
|
|
return GreasePencilStrokePoint(self._drawing, point_i)
|
|
elif isinstance(key, slice):
|
|
if key.step is not None and key.step != 1:
|
|
raise ValueError("Step values != 1 not supported")
|
|
# Default to 0 and size for the start and stop values.
|
|
start = key.start if key.start is not None else 0
|
|
stop = key.stop if key.stop is not None else self._size
|
|
# Wrap negative indices.
|
|
start = self._size + start if start < 0 else start
|
|
stop = self._size + stop if stop < 0 else stop
|
|
# Clamp start and stop.
|
|
start = max(0, min(start, self._size))
|
|
stop = max(0, min(stop, self._size))
|
|
return GreasePencilStrokePointSlice(self._drawing, self._start + start, self._start + stop)
|
|
else:
|
|
raise TypeError(f"Unexpected index of type {type(key)}")
|
|
|
|
|
|
# Define the list of attributes that should be exposed as read/write properties on the class.
|
|
@DefAttributeGetterSetters([
|
|
# Property Name, Attribute Name, Type, Default Value, Docstring
|
|
('cyclic', 'cyclic', 'BOOLEAN', False, "The closed state for this stroke."),
|
|
('material_index', 'material_index', 'INT', 0, "The index of the material for this stroke."),
|
|
('select', '.selection', 'BOOLEAN', True, "The selection state for this stroke."),
|
|
('softness', 'softness', 'FLOAT', 0.0, "Used by the renderer to generate a soft gradient from the stroke center line to the edges."),
|
|
('start_cap', 'start_cap', 'INT8', 0, "The type of start cap of this stroke."),
|
|
('end_cap', 'end_cap', 'INT8', 0, "The type of end cap of this stroke."),
|
|
('curve_type', 'curve_type', 'INT8', 0, "The type of curve."),
|
|
('aspect_ratio', 'aspect_ratio', 'FLOAT', 1.0, "The aspect ratio (x/y) used for textures. "),
|
|
('fill_opacity', 'fill_opacity', 'FLOAT', 0.0, "The opacity of the fill."),
|
|
('fill_color', 'fill_color', 'FLOAT_COLOR', (0.0, 0.0, 0.0, 0.0), "The color of the fill."),
|
|
('time_start', 'init_time', 'FLOAT', 0.0, "A time value for when the stroke was created."),
|
|
])
|
|
class GreasePencilStroke(AttributeGetterSetter):
|
|
"""
|
|
A helper class to get access to stroke data.
|
|
"""
|
|
|
|
def __init__(self, drawing, curve_index, points_start_index, points_end_index):
|
|
super().__init__(drawing.attributes, curve_index, 'CURVE')
|
|
self._drawing = drawing
|
|
self._curve_index = curve_index
|
|
self._points_start_index = points_start_index
|
|
self._points_end_index = points_end_index
|
|
|
|
@property
|
|
def points(self):
|
|
"""
|
|
Return a slice of points in the stroke.
|
|
"""
|
|
return GreasePencilStrokePointSlice(self._drawing, self._points_start_index, self._points_end_index)
|
|
|
|
def add_points(self, count):
|
|
"""
|
|
Add new points at the end of the stroke and returns the new points as a list.
|
|
"""
|
|
previous_end = self._points_end_index
|
|
new_size = self._points_end_index - self._points_start_index + count
|
|
self._drawing.resize_curves(sizes=[new_size], indices=[self._curve_index])
|
|
self._points_end_index = self._points_start_index + new_size
|
|
return GreasePencilStrokePointSlice(self._drawing, previous_end, self._points_end_index)
|
|
|
|
def remove_points(self, count):
|
|
"""
|
|
Remove points at the end of the stroke.
|
|
"""
|
|
new_size = self._points_end_index - self._points_start_index - count
|
|
# A stroke need to have at least one point.
|
|
if new_size < 1:
|
|
new_size = 1
|
|
self._drawing.resize_curves(sizes=[new_size], indices=[self._curve_index])
|
|
self._points_end_index = self._points_start_index + new_size
|
|
|
|
|
|
class GreasePencilStrokeSlice:
|
|
"""
|
|
A helper class that represents a slice of GreasePencilStroke's.
|
|
"""
|
|
|
|
def __init__(self, drawing, start, stop):
|
|
self._drawing = drawing
|
|
self._curve_offsets = drawing.curve_offsets
|
|
self._start = start
|
|
self._stop = stop
|
|
self._size = stop - start
|
|
|
|
def __len__(self):
|
|
return self._size
|
|
|
|
def _is_valid_index(self, key):
|
|
if self._size <= 0:
|
|
return False
|
|
if key < 0:
|
|
# Support indexing from the end.
|
|
return abs(key) <= self._size
|
|
return abs(key) < self._size
|
|
|
|
def __getitem__(self, key):
|
|
if isinstance(key, int):
|
|
if not self._is_valid_index(key):
|
|
raise IndexError(f"Key {key} is out of range")
|
|
# Turn the key into an index.
|
|
curve_i = self._start + (key % self._size)
|
|
offsets = self._curve_offsets
|
|
return GreasePencilStroke(self._drawing, curve_i, offsets[curve_i].value, offsets[curve_i + 1].value)
|
|
elif isinstance(key, slice):
|
|
if key.step is not None and key.step != 1:
|
|
raise ValueError("Step values != 1 not supported")
|
|
# Default to 0 and size for the start and stop values.
|
|
start = key.start if key.start is not None else 0
|
|
stop = key.stop if key.stop is not None else self._size
|
|
# Wrap negative indices.
|
|
start = self._size + start if start < 0 else start
|
|
stop = self._size + stop if stop < 0 else stop
|
|
# Clamp start and stop.
|
|
start = max(0, min(start, self._size))
|
|
stop = max(0, min(stop, self._size))
|
|
return GreasePencilStrokeSlice(self._drawing, self._start + start, self._start + stop)
|
|
else:
|
|
raise TypeError(f"Unexpected index of type {type(key)}")
|