239 lines
10 KiB
Python
239 lines
10 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("Unknown type {!r}".format(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("Unknown type {!r}".format(type))
|
|
else:
|
|
raise Exception("Could not create attribute {:s} of type {!r}".format(name, 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 information &
|
|
creates properties on the class with `getters` & `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, Doc-string.
|
|
('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("Key {:d} is out of range".format(key))
|
|
# 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("Unexpected index of type {!r}".format(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, Doc-string.
|
|
('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("Key {:d} is out of range".format(key))
|
|
# 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("Unexpected index of type {!r}".format(type(key)))
|