mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 10:06:47 +00:00
typing
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
from bpy.types import Panel
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
@@ -34,16 +35,21 @@ from blenderbim.bim.module.pset.data import (
|
||||
ProfilePsetsData,
|
||||
WorkSchedulePsetsData,
|
||||
)
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
def draw_property(prop, layout, copy_operator=None):
|
||||
def draw_property(
|
||||
prop: bpy.types.PropertyGroup, layout: bpy.types.UILayout, copy_operator: Optional[str] = None
|
||||
) -> None:
|
||||
if prop.value_type == "IfcPropertySingleValue":
|
||||
draw_single_property(prop, layout, copy_operator)
|
||||
elif prop.value_type == "IfcPropertyEnumeratedValue":
|
||||
draw_enumerated_property(prop, layout, copy_operator)
|
||||
|
||||
|
||||
def draw_single_property(prop, layout, copy_operator=None):
|
||||
def draw_single_property(
|
||||
prop: bpy.types.PropertyGroup, layout: bpy.types.UILayout, copy_operator: Optional[str] = None
|
||||
) -> None:
|
||||
value_name = prop.metadata.get_value_name(display_only=True)
|
||||
if not value_name:
|
||||
layout.label(text=prop["Name"])
|
||||
@@ -63,7 +69,9 @@ def draw_single_property(prop, layout, copy_operator=None):
|
||||
op.name = prop.metadata.name
|
||||
|
||||
|
||||
def draw_enumerated_property(prop, layout, copy_operator=None):
|
||||
def draw_enumerated_property(
|
||||
prop: bpy.types.PropertyGroup, layout: bpy.types.UILayout, copy_operator: Optional[str] = None
|
||||
) -> None:
|
||||
value_name = prop.metadata.get_value_name()
|
||||
if not value_name:
|
||||
layout.label(text=prop.metadata.name)
|
||||
@@ -78,13 +86,21 @@ def draw_enumerated_property(prop, layout, copy_operator=None):
|
||||
op.name = prop.metadata.name
|
||||
|
||||
|
||||
def get_active_pset_obj_name(context, obj_type):
|
||||
def get_active_pset_obj_name(context: bpy.types.Context, obj_type: tool.Ifc.OBJECT_TYPE) -> str:
|
||||
if obj_type in ("Object", "MaterialSet", "MaterialSetItem"):
|
||||
return context.active_object.name
|
||||
return ""
|
||||
|
||||
|
||||
def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_removing=True):
|
||||
def draw_psetqto_ui(
|
||||
context: bpy.types.Context,
|
||||
pset_id: int,
|
||||
pset: dict[str, Any],
|
||||
props: bpy.types.PropertyGroup,
|
||||
layout: bpy.types.UILayout,
|
||||
obj_type: tool.Ifc.OBJECT_TYPE,
|
||||
allow_removing: bool = True,
|
||||
) -> None:
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
if "is_expanded" not in pset:
|
||||
@@ -155,7 +171,9 @@ def draw_psetqto_ui(context, pset_id, pset, props, layout, obj_type, allow_remov
|
||||
row.label(text="No Properties Set")
|
||||
|
||||
|
||||
def draw_psetqto_editable_ui(box, props, prop):
|
||||
def draw_psetqto_editable_ui(
|
||||
box: bpy.types.UILayout, props: bpy.types.PropertyGroup, prop: bpy.types.PropertyGroup
|
||||
) -> None:
|
||||
row = box.row(align=True)
|
||||
draw_property(prop, row, copy_operator="bim.copy_property_to_selection")
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.schema
|
||||
import blenderbim.tool as tool
|
||||
from ifcopenshell.util.doc import get_entity_doc
|
||||
from typing import Union
|
||||
|
||||
@@ -17,7 +17,24 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
def copy_property_to_selection(ifc, pset, is_pset=True, obj=None, pset_name=None, prop_name=None, prop_value=None):
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional, Union, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
def copy_property_to_selection(
|
||||
ifc: tool.Ifc,
|
||||
pset: tool.Pset,
|
||||
obj: bpy.types.Object,
|
||||
pset_name: str,
|
||||
prop_name: str,
|
||||
prop_value: Any,
|
||||
is_pset: bool = True,
|
||||
) -> None:
|
||||
element = ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
@@ -30,7 +47,9 @@ def copy_property_to_selection(ifc, pset, is_pset=True, obj=None, pset_name=None
|
||||
ifc.run("pset.edit_qto", qto=ifc_pset, properties={prop_name: prop_value})
|
||||
|
||||
|
||||
def add_pset(ifc, pset, blender, obj_name, obj_type):
|
||||
def add_pset(
|
||||
ifc: tool.Ifc, pset: tool.Pset, blender: tool.Blender, obj_name: str, obj_type: tool.Ifc.OBJECT_TYPE
|
||||
) -> None:
|
||||
pset_name = pset.get_pset_name(obj_name, obj_type, pset_type="PSET")
|
||||
if obj_type == "Object":
|
||||
elements = [ifc.get_entity(obj) for obj in blender.get_selected_objects()]
|
||||
@@ -44,7 +63,14 @@ def add_pset(ifc, pset, blender, obj_name, obj_type):
|
||||
pset.enable_pset_editing(pset_id=0, pset_name=pset_name, pset_type="PSET", obj=obj_name, obj_type=obj_type)
|
||||
|
||||
|
||||
def enable_pset_editing(pset_tool, pset, pset_name, pset_type, obj_name, obj_type):
|
||||
def enable_pset_editing(
|
||||
pset_tool: tool.Pset,
|
||||
pset: ifcopenshell.entity_instance,
|
||||
pset_name: str,
|
||||
pset_type: tool.Pset.PSET_TYPE,
|
||||
obj_name: str,
|
||||
obj_type: tool.Ifc.OBJECT_TYPE,
|
||||
) -> None:
|
||||
props = pset_tool.get_pset_props(obj_name, obj_type)
|
||||
pset_tool.clear_blender_pset_properties(props)
|
||||
|
||||
@@ -63,6 +89,6 @@ def enable_pset_editing(pset_tool, pset, pset_name, pset_type, obj_name, obj_typ
|
||||
pset_tool.enable_proposed_pset(props, pset_name, pset_type, has_template)
|
||||
|
||||
|
||||
def add_proposed_prop(pset, obj_name, obj_type, name, value):
|
||||
def add_proposed_prop(pset: tool.Pset, obj_name: str, obj_type: tool.Ifc.OBJECT_TYPE, name: str, value: Any) -> None:
|
||||
props = pset.get_pset_props(obj_name, obj_type)
|
||||
pset.add_proposed_property(name, pset.cast_string_to_primitive(value), props)
|
||||
|
||||
@@ -32,6 +32,7 @@ from mathutils import Vector
|
||||
from pathlib import Path
|
||||
from blenderbim.bim.ifc import IFC_CONNECTED_TYPE
|
||||
from typing import Any, Optional, Union, Literal, Iterable, Callable
|
||||
from typing_extensions import assert_never
|
||||
|
||||
|
||||
VIEWPORT_ATTRIBUTES = [
|
||||
@@ -163,8 +164,14 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
|
||||
@classmethod
|
||||
def get_obj_ifc_definition_id(
|
||||
cls, obj: Optional[str] = None, obj_type: Optional[str] = None, context: Optional[bpy.types.Context] = None
|
||||
cls,
|
||||
obj: Optional[str] = None,
|
||||
obj_type: Optional[tool.Ifc.OBJECT_TYPE] = None,
|
||||
context: Optional[bpy.types.Context] = None,
|
||||
) -> Union[int, None]:
|
||||
# TODO: is it ever used as None?
|
||||
if obj_type is None:
|
||||
return None
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
if obj_type == "Object":
|
||||
@@ -200,6 +207,7 @@ class Blender(blenderbim.core.tool.Blender):
|
||||
elif obj_type == "Group":
|
||||
prop = context.scene.BIMGroupProperties
|
||||
return prop.groups[prop.active_group_index].ifc_definition_id
|
||||
assert_never(obj_type)
|
||||
|
||||
@classmethod
|
||||
def is_ifc_object(cls, obj: bpy.types.Object) -> bool:
|
||||
|
||||
@@ -26,10 +26,23 @@ import blenderbim.core.tool
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore, IFC_CONNECTED_TYPE
|
||||
from typing import Optional, Union, Any, final
|
||||
from typing import Optional, Union, Any, final, Literal
|
||||
|
||||
|
||||
class Ifc(blenderbim.core.tool.Ifc):
|
||||
OBJECT_TYPE = Literal[
|
||||
"Object",
|
||||
"Material",
|
||||
"MaterialSet",
|
||||
"MaterialSetItem",
|
||||
"Task",
|
||||
"Cost",
|
||||
"Resource",
|
||||
"Profile",
|
||||
"WorkSchedule",
|
||||
"Group",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def run(cls, command: str, **kwargs) -> Any:
|
||||
return ifcopenshell.api.run(command, IfcStore.get_file(), **kwargs)
|
||||
|
||||
@@ -18,14 +18,18 @@
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.attribute
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.bim.schema
|
||||
from typing import Union
|
||||
from typing import Union, Literal, Any
|
||||
from typing_extensions import assert_never
|
||||
|
||||
|
||||
class Pset(blenderbim.core.tool.Pset):
|
||||
PSET_TYPE = Literal["PSET", "QTO"]
|
||||
|
||||
@classmethod
|
||||
def get_element_pset(
|
||||
cls, element: ifcopenshell.entity_instance, pset_name: str
|
||||
@@ -35,7 +39,7 @@ class Pset(blenderbim.core.tool.Pset):
|
||||
return tool.Ifc.get().by_id(pset["id"])
|
||||
|
||||
@classmethod
|
||||
def get_pset_props(cls, obj, obj_type):
|
||||
def get_pset_props(cls, obj: str, obj_type: tool.Ifc.OBJECT_TYPE) -> bpy.types.PropertyGroup:
|
||||
if obj_type == "Object":
|
||||
return bpy.data.objects.get(obj).PsetProperties
|
||||
elif obj_type == "Material":
|
||||
@@ -54,9 +58,10 @@ class Pset(blenderbim.core.tool.Pset):
|
||||
return bpy.context.scene.WorkSchedulePsetProperties
|
||||
elif obj_type == "Group":
|
||||
return bpy.context.scene.GroupPsetProperties
|
||||
assert_never(obj_type)
|
||||
|
||||
@classmethod
|
||||
def get_pset_name(cls, obj, obj_type, pset_type="PSET"):
|
||||
def get_pset_name(cls, obj: str, obj_type: tool.Ifc.OBJECT_TYPE, pset_type: PSET_TYPE = "PSET") -> str:
|
||||
props = cls.get_pset_props(obj, obj_type)
|
||||
name = props.pset_name if pset_type == "PSET" else props.qto_name
|
||||
if name == "BBIM_CUSTOM":
|
||||
@@ -73,7 +78,7 @@ class Pset(blenderbim.core.tool.Pset):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_pset_empty(cls, pset):
|
||||
def is_pset_empty(cls, pset: ifcopenshell.entity_instance) -> bool:
|
||||
pset_dict = ifcopenshell.util.element.get_property_definition(pset)
|
||||
del pset_dict["id"]
|
||||
for value in pset_dict.values():
|
||||
@@ -82,14 +87,16 @@ class Pset(blenderbim.core.tool.Pset):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def enable_pset_editing(cls, pset_id=None, pset_name=None, pset_type=None, obj=None, obj_type=None):
|
||||
# TODO REFACTOR ONCE toll/CORE functions are available
|
||||
def enable_pset_editing(
|
||||
cls, pset_id: int, pset_name: str, pset_type: PSET_TYPE, obj: str, obj_type: tool.Ifc.OBJECT_TYPE
|
||||
) -> None:
|
||||
# TODO REFACTOR ONCE tool/CORE functions are available
|
||||
bpy.ops.bim.enable_pset_editing(
|
||||
pset_id=0, pset_name=cls.get_pset_name(obj, obj_type), pset_type="PSET", obj=obj, obj_type=obj_type
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def import_pset_from_existing(cls, pset, props):
|
||||
def import_pset_from_existing(cls, pset: ifcopenshell.entity_instance, props: bpy.types.PropertyGroup) -> None:
|
||||
pset_props = []
|
||||
if pset.is_a("IfcElementQuantity"):
|
||||
pset_props = pset.Quantities
|
||||
@@ -134,7 +141,7 @@ class Pset(blenderbim.core.tool.Pset):
|
||||
metadata.set_value(metadata.get_value_default() if metadata.is_null else value)
|
||||
|
||||
@classmethod
|
||||
def get_prop_template_primitive_type(cls, prop_template):
|
||||
def get_prop_template_primitive_type(cls, prop_template: ifcopenshell.entity_instance) -> str:
|
||||
if prop_template.TemplateType in ["Q_LENGTH", "Q_AREA", "Q_VOLUME", "Q_WEIGHT", "Q_TIME"]:
|
||||
return "float"
|
||||
elif prop_template.TemplateType == "Q_COUNT":
|
||||
@@ -144,7 +151,9 @@ class Pset(blenderbim.core.tool.Pset):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def import_enumerated_value_from_template(cls, prop_template, data, props):
|
||||
def import_enumerated_value_from_template(
|
||||
cls, prop_template: ifcopenshell.entity_instance, data: dict[str, Any], props: bpy.types.PropertyGroup
|
||||
) -> None:
|
||||
enum_items = [v.wrappedValue for v in prop_template.Enumerators.EnumerationValues]
|
||||
selected_enum_items = data.get(prop_template.Name, []) or []
|
||||
|
||||
@@ -167,7 +176,13 @@ class Pset(blenderbim.core.tool.Pset):
|
||||
new.is_selected = enum in selected_enum_items
|
||||
|
||||
@classmethod
|
||||
def import_single_value_from_template(cls, pset_template, prop_template, data, props):
|
||||
def import_single_value_from_template(
|
||||
cls,
|
||||
pset_template: ifcopenshell.entity_instance,
|
||||
prop_template: ifcopenshell.entity_instance,
|
||||
data: dict[str, Any],
|
||||
props: bpy.types.PropertyGroup,
|
||||
) -> None:
|
||||
prop = props.properties.add()
|
||||
prop.name = prop_template.Name
|
||||
prop.value_type = "IfcPropertySingleValue"
|
||||
@@ -207,7 +222,12 @@ class Pset(blenderbim.core.tool.Pset):
|
||||
blenderbim.bim.helper.add_attribute_description(metadata, prop_template)
|
||||
|
||||
@classmethod
|
||||
def import_pset_from_template(cls, pset_template, pset, props):
|
||||
def import_pset_from_template(
|
||||
cls,
|
||||
pset_template: ifcopenshell.entity_instance,
|
||||
pset: Union[ifcopenshell.entity_instance, None],
|
||||
props: bpy.types.PropertyGroup,
|
||||
) -> None:
|
||||
if pset:
|
||||
data = ifcopenshell.util.element.get_property_definition(pset)
|
||||
del data["id"]
|
||||
@@ -231,28 +251,32 @@ class Pset(blenderbim.core.tool.Pset):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def clear_blender_pset_properties(cls, props):
|
||||
def clear_blender_pset_properties(cls, props: bpy.types.PropertyGroup) -> None:
|
||||
props.properties.clear()
|
||||
|
||||
@classmethod
|
||||
def set_active_pset(cls, props, pset, has_template):
|
||||
def set_active_pset(
|
||||
cls, props: bpy.types.PropertyGroup, pset: ifcopenshell.entity_instance, has_template: bool
|
||||
) -> None:
|
||||
props.active_pset_id = pset.id()
|
||||
props.active_pset_name = pset.Name
|
||||
props.active_pset_has_template = has_template
|
||||
|
||||
@classmethod
|
||||
def enable_proposed_pset(cls, props, pset_name, pset_type, has_template):
|
||||
def enable_proposed_pset(
|
||||
cls, props: bpy.types.PropertyGroup, pset_name: str, pset_type: PSET_TYPE, has_template: bool
|
||||
) -> None:
|
||||
props.active_pset_id = 0
|
||||
props.active_pset_name = pset_name or "My_Data"
|
||||
props.active_pset_type = pset_type
|
||||
props.active_pset_has_template = has_template
|
||||
|
||||
@classmethod
|
||||
def get_pset_template(cls, name):
|
||||
def get_pset_template(cls, name: str) -> Union[ifcopenshell.entity_instance, None]:
|
||||
return blenderbim.bim.schema.ifc.psetqto.get_by_name(name)
|
||||
|
||||
@classmethod
|
||||
def add_proposed_property(cls, name, value, props):
|
||||
def add_proposed_property(cls, name: str, value: Any, props: bpy.types.PropertyGroup) -> None:
|
||||
if props.properties.get(name):
|
||||
return
|
||||
prop = props.properties.add()
|
||||
@@ -265,7 +289,7 @@ class Pset(blenderbim.core.tool.Pset):
|
||||
metadata.set_value(metadata.get_value_default() if metadata.is_null else value)
|
||||
|
||||
@classmethod
|
||||
def cast_string_to_primitive(cls, value: str):
|
||||
def cast_string_to_primitive(cls, value: str) -> Any:
|
||||
value = value.strip()
|
||||
if value.lower() == "true":
|
||||
return True
|
||||
|
||||
@@ -33,6 +33,8 @@ from typing import TypeVar, Union, Optional, Generator, Any, Literal, overload,
|
||||
if TYPE_CHECKING:
|
||||
from OCC.Core import TopoDS
|
||||
|
||||
IteratorOutput = Union["ShapeElementType", "utils.shape_tuple"]
|
||||
|
||||
T = TypeVar("T")
|
||||
ShapeElementType = Union[
|
||||
ifcopenshell_wrapper.BRepElement, ifcopenshell_wrapper.TriangulationElement, ifcopenshell_wrapper.SerializedElement
|
||||
@@ -273,7 +275,7 @@ class iterator(ifcopenshell_wrapper.Iterator):
|
||||
def get(self):
|
||||
return wrap_shape_creation(self.settings, ifcopenshell_wrapper.Iterator.get(self))
|
||||
|
||||
def __iter__(self) -> Generator[ShapeElementType, None, None]:
|
||||
def __iter__(self) -> Generator[IteratorOutput, None, None]:
|
||||
if self.initialize():
|
||||
while True:
|
||||
yield self.get()
|
||||
@@ -412,7 +414,19 @@ def create_shape(
|
||||
)
|
||||
|
||||
|
||||
def consume_iterator(it, with_progress=False):
|
||||
@overload
|
||||
def consume_iterator(it: iterator, with_progress: Literal[False] = False) -> Generator[IteratorOutput, None, None]: ...
|
||||
@overload
|
||||
def consume_iterator(
|
||||
it: iterator, with_progress: Literal[True]
|
||||
) -> Generator[tuple[int, IteratorOutput], None, None]: ...
|
||||
@overload
|
||||
def consume_iterator(
|
||||
it: iterator, with_progress: bool
|
||||
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: ...
|
||||
def consume_iterator(
|
||||
it: iterator, with_progress: bool = False
|
||||
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]:
|
||||
if it.initialize():
|
||||
while True:
|
||||
if with_progress:
|
||||
@@ -423,16 +437,49 @@ def consume_iterator(it, with_progress=False):
|
||||
break
|
||||
|
||||
|
||||
@overload
|
||||
def iterate(
|
||||
settings,
|
||||
file_or_filename,
|
||||
num_threads=1,
|
||||
include=None,
|
||||
exclude=None,
|
||||
with_progress=False,
|
||||
cache=None,
|
||||
settings: settings,
|
||||
file_or_filename: Union[file, str],
|
||||
num_threads: int = 1,
|
||||
include: Optional[Union[list[entity_instance], list[str]]] = None,
|
||||
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
|
||||
with_progress: Literal[False] = False,
|
||||
cache: Optional[serializers.hdf5] = None,
|
||||
geometry_library: GEOMETRY_LIBRARY = "opencascade",
|
||||
):
|
||||
) -> Generator[IteratorOutput, None, None]: ...
|
||||
@overload
|
||||
def iterate(
|
||||
settings: settings,
|
||||
file_or_filename: Union[file, str],
|
||||
num_threads: int = 1,
|
||||
include: Optional[Union[list[entity_instance], list[str]]] = None,
|
||||
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
|
||||
with_progress: Literal[True] = True,
|
||||
cache: Optional[serializers.hdf5] = None,
|
||||
geometry_library: GEOMETRY_LIBRARY = "opencascade",
|
||||
) -> Generator[tuple[int, IteratorOutput], None, None]: ...
|
||||
@overload
|
||||
def iterate(
|
||||
settings: settings,
|
||||
file_or_filename: Union[file, str],
|
||||
num_threads: int = 1,
|
||||
include: Optional[Union[list[entity_instance], list[str]]] = None,
|
||||
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
|
||||
with_progress: bool = False,
|
||||
cache: Optional[serializers.hdf5] = None,
|
||||
geometry_library: GEOMETRY_LIBRARY = "opencascade",
|
||||
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: ...
|
||||
def iterate(
|
||||
settings: settings,
|
||||
file_or_filename: Union[file, str],
|
||||
num_threads: int = 1,
|
||||
include: Optional[Union[list[entity_instance], list[str]]] = None,
|
||||
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
|
||||
with_progress: bool = False,
|
||||
cache: Optional[serializers.hdf5] = None,
|
||||
geometry_library: GEOMETRY_LIBRARY = "opencascade",
|
||||
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]:
|
||||
it = iterator(settings, file_or_filename, num_threads, include, exclude, geometry_library)
|
||||
if cache:
|
||||
hdf5_cache = serializers.hdf5(cache, settings)
|
||||
|
||||
@@ -40,6 +40,7 @@ except ImportError:
|
||||
|
||||
|
||||
class shape_tuple(NamedTuple):
|
||||
"""A tuple containing IfcOpenShell serialized element/shape and pythonOCC shape."""
|
||||
data: Union[ifcopenshell_wrapper.SerializedElement, ifcopenshell_wrapper.Serialization]
|
||||
geometry: TopoDS.TopoDS_Shape
|
||||
styles: tuple[tuple[float, float, float, float], ...]
|
||||
|
||||
Reference in New Issue
Block a user