mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-27 10:51:13 +00:00
WIP refactor mesh UI into geometry module and minor fixes. See #1222.
This commit is contained in:
@@ -283,7 +283,6 @@ if bpy is not None:
|
|||||||
ui.BIM_PT_mvd,
|
ui.BIM_PT_mvd,
|
||||||
ui.BIM_PT_debug,
|
ui.BIM_PT_debug,
|
||||||
ui.BIM_PT_material,
|
ui.BIM_PT_material,
|
||||||
ui.BIM_PT_mesh,
|
|
||||||
ui.BIM_PT_presentation_layer_data,
|
ui.BIM_PT_presentation_layer_data,
|
||||||
ui.BIM_PT_object,
|
ui.BIM_PT_object,
|
||||||
ui.BIM_PT_object_material,
|
ui.BIM_PT_object_material,
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ classes = (
|
|||||||
operator.AddRepresentation,
|
operator.AddRepresentation,
|
||||||
operator.SwitchRepresentation,
|
operator.SwitchRepresentation,
|
||||||
operator.RemoveRepresentation,
|
operator.RemoveRepresentation,
|
||||||
operator.BakeParametricGeometry,
|
operator.UpdateMeshRepresentation,
|
||||||
operator.UpdateIfcRepresentation,
|
operator.UpdateParametricRepresentation,
|
||||||
operator.GetRepresentationIfcParameters,
|
operator.GetRepresentationIfcParameters,
|
||||||
ui.BIM_PT_representations,
|
ui.BIM_PT_representations,
|
||||||
|
ui.BIM_PT_mesh,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,19 +3,21 @@ from blenderbim.bim.ifc import IfcStore
|
|||||||
|
|
||||||
class Data:
|
class Data:
|
||||||
products = {}
|
products = {}
|
||||||
|
representations = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, product_id):
|
def load(cls, product_id):
|
||||||
file = IfcStore.get_file()
|
file = IfcStore.get_file()
|
||||||
if not file:
|
if not file:
|
||||||
return
|
return
|
||||||
cls.products[product_id] = {"Representations": {}}
|
cls.products[product_id] = []
|
||||||
product = file.by_id(product_id)
|
product = file.by_id(product_id)
|
||||||
if not product.Representation:
|
if not product.Representation:
|
||||||
return
|
return
|
||||||
for representation in product.Representation.Representations:
|
for representation in product.Representation.Representations:
|
||||||
c = representation.ContextOfItems
|
c = representation.ContextOfItems
|
||||||
cls.products[product_id]["Representations"][int(representation.id())] = {
|
rep_id = int(representation.id())
|
||||||
|
cls.representations[rep_id] = {
|
||||||
"RepresentationIdentifier": representation.RepresentationIdentifier,
|
"RepresentationIdentifier": representation.RepresentationIdentifier,
|
||||||
"RepresentationType": representation.RepresentationType,
|
"RepresentationType": representation.RepresentationType,
|
||||||
"ContextOfItems": {
|
"ContextOfItems": {
|
||||||
@@ -24,3 +26,4 @@ class Data:
|
|||||||
"TargetView": c.TargetView if c.is_a("IfcGeometricRepresentationSubContext") else "",
|
"TargetView": c.TargetView if c.is_a("IfcGeometricRepresentationSubContext") else "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cls.products[product_id].append(rep_id)
|
||||||
|
|||||||
@@ -15,50 +15,53 @@ from blenderbim.bim.module.geometry.data import Data
|
|||||||
class AddRepresentation(bpy.types.Operator):
|
class AddRepresentation(bpy.types.Operator):
|
||||||
bl_idname = "bim.add_representation"
|
bl_idname = "bim.add_representation"
|
||||||
bl_label = "Add Representation"
|
bl_label = "Add Representation"
|
||||||
|
obj: bpy.props.StringProperty()
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
obj = bpy.context.active_object
|
obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object
|
||||||
self.file = IfcStore.get_file()
|
self.file = IfcStore.get_file()
|
||||||
self.context_id = bpy.context.scene.BIMProperties.contexts
|
self.context_id = bpy.context.scene.BIMProperties.contexts
|
||||||
|
|
||||||
element = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
add_object_placement.Usecase(
|
||||||
usecase = add_representation.Usecase(
|
|
||||||
self.file,
|
self.file,
|
||||||
{
|
{
|
||||||
"context": self.file.by_id(int(self.context_id)),
|
"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
|
||||||
"geometry": obj.data,
|
"matrix": np.array(obj.matrix_world),
|
||||||
"total_items": max(1, len(obj.material_slots)),
|
|
||||||
},
|
},
|
||||||
)
|
).execute()
|
||||||
result = usecase.execute()
|
|
||||||
if not result:
|
|
||||||
print("Failed to write shape representation")
|
|
||||||
return {"FINISHED"}
|
|
||||||
|
|
||||||
usecase = assign_styles.Usecase(
|
if obj.data:
|
||||||
self.file,
|
result = add_representation.Usecase(
|
||||||
{
|
self.file,
|
||||||
"shape_representation": result,
|
{
|
||||||
"styles": [
|
"context": self.file.by_id(int(self.context_id)),
|
||||||
self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id)
|
"geometry": obj.data,
|
||||||
for s in obj.material_slots
|
"total_items": max(1, len(obj.material_slots)),
|
||||||
if s.material
|
},
|
||||||
],
|
).execute()
|
||||||
},
|
if not result:
|
||||||
)
|
print("Failed to write shape representation")
|
||||||
usecase.execute()
|
return {"FINISHED"}
|
||||||
|
assign_styles.Usecase(
|
||||||
|
self.file,
|
||||||
|
{
|
||||||
|
"shape_representation": result,
|
||||||
|
"styles": [
|
||||||
|
self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id)
|
||||||
|
for s in obj.material_slots
|
||||||
|
if s.material
|
||||||
|
],
|
||||||
|
},
|
||||||
|
).execute()
|
||||||
|
assign_representation.Usecase(
|
||||||
|
self.file, {"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id), "representation": result}
|
||||||
|
).execute()
|
||||||
|
|
||||||
usecase = assign_representation.Usecase(
|
existing_mesh = obj.data
|
||||||
self.file, {"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id), "representation": result}
|
mesh = obj.data.copy()
|
||||||
)
|
mesh.name = "{}/{}".format(self.context_id, result.id())
|
||||||
usecase.execute()
|
mesh.BIMMeshProperties.ifc_definition_id = int(result.id())
|
||||||
|
obj.data = mesh
|
||||||
existing_mesh = obj.data
|
|
||||||
existing_mesh.use_fake_user = True
|
|
||||||
mesh = obj.data.copy()
|
|
||||||
mesh.name = "{}/{}".format(self.context_id, result.id())
|
|
||||||
mesh.BIMMeshProperties.ifc_definition_id = int(result.id())
|
|
||||||
obj.data = mesh
|
|
||||||
Data.load(obj.BIMObjectProperties.ifc_definition_id)
|
Data.load(obj.BIMObjectProperties.ifc_definition_id)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@@ -117,44 +120,42 @@ class RemoveRepresentation(bpy.types.Operator):
|
|||||||
void_mesh = bpy.data.meshes.new("Void")
|
void_mesh = bpy.data.meshes.new("Void")
|
||||||
obj.data = void_mesh
|
obj.data = void_mesh
|
||||||
bpy.data.meshes.remove(mesh)
|
bpy.data.meshes.remove(mesh)
|
||||||
usecase = remove_representation.Usecase(self.file, {"representation": representation})
|
remove_representation.Usecase(self.file, {"representation": representation}).execute()
|
||||||
result = usecase.execute()
|
|
||||||
Data.load(obj.BIMObjectProperties.ifc_definition_id)
|
Data.load(obj.BIMObjectProperties.ifc_definition_id)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class BakeParametricGeometry(bpy.types.Operator):
|
class UpdateMeshRepresentation(bpy.types.Operator):
|
||||||
bl_idname = "bim.bake_parametric_geometry"
|
bl_idname = "bim.update_mesh_representation"
|
||||||
bl_label = "Bake Parametric Geometry"
|
bl_label = "Update Mesh Representation"
|
||||||
|
obj: bpy.props.StringProperty()
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
obj = bpy.context.active_object
|
obj = bpy.data.objects.get(self.obj) if self.obj else bpy.context.active_object
|
||||||
self.file = IfcStore.get_file()
|
self.file = IfcStore.get_file()
|
||||||
|
|
||||||
usecase = add_object_placement.Usecase(
|
add_object_placement.Usecase(
|
||||||
self.file,
|
self.file,
|
||||||
{
|
{
|
||||||
"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
|
"product": self.file.by_id(obj.BIMObjectProperties.ifc_definition_id),
|
||||||
"matrix": np.array(obj.matrix_world),
|
"matrix": np.array(obj.matrix_world),
|
||||||
},
|
},
|
||||||
)
|
).execute()
|
||||||
result = usecase.execute()
|
|
||||||
|
|
||||||
element = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
element = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id)
|
||||||
usecase = add_representation.Usecase(
|
result = add_representation.Usecase(
|
||||||
self.file,
|
self.file,
|
||||||
{
|
{
|
||||||
"context": element.ContextOfItems,
|
"context": element.ContextOfItems,
|
||||||
"geometry": obj.data,
|
"geometry": obj.data,
|
||||||
"total_items": max(1, len(obj.material_slots)),
|
"total_items": max(1, len(obj.material_slots)),
|
||||||
},
|
},
|
||||||
)
|
).execute()
|
||||||
result = usecase.execute()
|
|
||||||
if not result:
|
if not result:
|
||||||
print("Failed to write shape representation")
|
print("Failed to write shape representation")
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
usecase = assign_styles.Usecase(
|
assign_styles.Usecase(
|
||||||
self.file,
|
self.file,
|
||||||
{
|
{
|
||||||
"shape_representation": result,
|
"shape_representation": result,
|
||||||
@@ -164,9 +165,9 @@ class BakeParametricGeometry(bpy.types.Operator):
|
|||||||
if s.material
|
if s.material
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
).execute()
|
||||||
usecase.execute()
|
|
||||||
|
|
||||||
|
# TODO: move this into a replace_representation usecase or something
|
||||||
for inverse in self.file.get_inverse(element):
|
for inverse in self.file.get_inverse(element):
|
||||||
ifcopenshell.util.element.replace_attribute(inverse, element, result)
|
ifcopenshell.util.element.replace_attribute(inverse, element, result)
|
||||||
obj.data.BIMMeshProperties.ifc_definition_id = int(result.id())
|
obj.data.BIMMeshProperties.ifc_definition_id = int(result.id())
|
||||||
@@ -174,9 +175,9 @@ class BakeParametricGeometry(bpy.types.Operator):
|
|||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class UpdateIfcRepresentation(bpy.types.Operator):
|
class UpdateParametricRepresentation(bpy.types.Operator):
|
||||||
bl_idname = "bim.update_ifc_representation"
|
bl_idname = "bim.update_parametric_representation"
|
||||||
bl_label = "Update IFC Representation"
|
bl_label = "Update Parametric Representation"
|
||||||
index: bpy.props.IntProperty()
|
index: bpy.props.IntProperty()
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class BIM_PT_representations(Panel):
|
|||||||
if props.ifc_definition_id not in Data.products:
|
if props.ifc_definition_id not in Data.products:
|
||||||
Data.load(props.ifc_definition_id)
|
Data.load(props.ifc_definition_id)
|
||||||
|
|
||||||
representations = Data.products[props.ifc_definition_id]["Representations"]
|
representations = Data.products[props.ifc_definition_id]
|
||||||
if not representations:
|
if not representations:
|
||||||
layout.label(text="No representations found")
|
layout.label(text="No representations found")
|
||||||
|
|
||||||
@@ -26,7 +26,8 @@ class BIM_PT_representations(Panel):
|
|||||||
row.prop(bpy.context.scene.BIMProperties, "contexts", text="")
|
row.prop(bpy.context.scene.BIMProperties, "contexts", text="")
|
||||||
row.operator("bim.add_representation", icon="ADD", text="")
|
row.operator("bim.add_representation", icon="ADD", text="")
|
||||||
|
|
||||||
for ifc_definition_id, representation in representations.items():
|
for ifc_definition_id in representations:
|
||||||
|
representation = Data.representations[ifc_definition_id]
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
row.label(text=representation["ContextOfItems"]["ContextType"])
|
row.label(text=representation["ContextOfItems"]["ContextType"])
|
||||||
row.label(text=representation["ContextOfItems"]["ContextIdentifier"])
|
row.label(text=representation["ContextOfItems"]["ContextIdentifier"])
|
||||||
@@ -35,3 +36,34 @@ class BIM_PT_representations(Panel):
|
|||||||
row.operator("bim.switch_representation", icon="OUTLINER_DATA_MESH", text="").ifc_definition_id = ifc_definition_id
|
row.operator("bim.switch_representation", icon="OUTLINER_DATA_MESH", text="").ifc_definition_id = ifc_definition_id
|
||||||
row.operator("bim.remove_representation", icon="X", text="").ifc_definition_id = ifc_definition_id
|
row.operator("bim.remove_representation", icon="X", text="").ifc_definition_id = ifc_definition_id
|
||||||
|
|
||||||
|
|
||||||
|
class BIM_PT_mesh(Panel):
|
||||||
|
bl_label = "IFC Representation"
|
||||||
|
bl_idname = "BIM_PT_mesh"
|
||||||
|
bl_space_type = "PROPERTIES"
|
||||||
|
bl_region_type = "WINDOW"
|
||||||
|
bl_context = "data"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return (
|
||||||
|
context.active_object is not None
|
||||||
|
and context.active_object.type == "MESH"
|
||||||
|
and hasattr(context.active_object.data, "BIMMeshProperties")
|
||||||
|
)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
if not context.active_object.data:
|
||||||
|
return
|
||||||
|
layout = self.layout
|
||||||
|
props = context.active_object.data.BIMMeshProperties
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.operator("bim.get_representation_ifc_parameters")
|
||||||
|
row = layout.row()
|
||||||
|
row.operator("bim.update_mesh_representation")
|
||||||
|
for index, ifc_parameter in enumerate(props.ifc_parameters):
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.prop(ifc_parameter, "name", text="")
|
||||||
|
row.prop(ifc_parameter, "value", text="")
|
||||||
|
row.operator("bim.update_parametric_representation", icon="FILE_REFRESH", text="").index = index
|
||||||
|
|||||||
@@ -102,40 +102,10 @@ class AssignClass(bpy.types.Operator):
|
|||||||
"name": obj.name,
|
"name": obj.name,
|
||||||
},
|
},
|
||||||
).execute()
|
).execute()
|
||||||
|
obj.name = "{}/{}".format(product.is_a(), obj.name)
|
||||||
|
obj.BIMObjectProperties.ifc_definition_id = int(product.id())
|
||||||
|
|
||||||
add_object_placement.Usecase(
|
bpy.ops.bim.add_representation(obj=obj.name)
|
||||||
self.file,
|
|
||||||
{
|
|
||||||
"product": product,
|
|
||||||
"matrix": np.array(obj.matrix_world),
|
|
||||||
},
|
|
||||||
).execute()
|
|
||||||
|
|
||||||
if obj.data:
|
|
||||||
representation = add_representation.Usecase(
|
|
||||||
self.file,
|
|
||||||
{
|
|
||||||
"context": self.file.by_id(int(bpy.context.scene.BIMProperties.contexts)),
|
|
||||||
"geometry": obj.data,
|
|
||||||
"total_items": max(1, len(obj.material_slots)),
|
|
||||||
},
|
|
||||||
).execute()
|
|
||||||
|
|
||||||
assign_styles.Usecase(
|
|
||||||
self.file,
|
|
||||||
{
|
|
||||||
"shape_representation": representation,
|
|
||||||
"styles": [
|
|
||||||
self.file.by_id(s.material.BIMMaterialProperties.ifc_style_id)
|
|
||||||
for s in obj.material_slots
|
|
||||||
if s.material
|
|
||||||
],
|
|
||||||
},
|
|
||||||
).execute()
|
|
||||||
|
|
||||||
assign_representation.Usecase(
|
|
||||||
self.file, {"product": product, "representation": representation}
|
|
||||||
).execute()
|
|
||||||
|
|
||||||
relating_structure = None
|
relating_structure = None
|
||||||
for collection in obj.users_collection:
|
for collection in obj.users_collection:
|
||||||
@@ -154,8 +124,6 @@ class AssignClass(bpy.types.Operator):
|
|||||||
},
|
},
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
obj.name = "{}/{}".format(product.is_a(), obj.name)
|
|
||||||
obj.BIMObjectProperties.ifc_definition_id = int(product.id())
|
|
||||||
if bpy.context.scene.BIMProperties.ifc_product == "IfcElementType":
|
if bpy.context.scene.BIMProperties.ifc_product == "IfcElementType":
|
||||||
self.place_in_types_collection(obj)
|
self.place_in_types_collection(obj)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Usecase:
|
|||||||
if contains_elements:
|
if contains_elements:
|
||||||
related_elements = list(contains_elements[0].RelatedElements)
|
related_elements = list(contains_elements[0].RelatedElements)
|
||||||
related_elements.append(self.settings["product"])
|
related_elements.append(self.settings["product"])
|
||||||
contains_elements.RelatedElements = related_elements
|
contains_elements[0].RelatedElements = related_elements
|
||||||
else:
|
else:
|
||||||
contains_elements = self.file.create_entity(
|
contains_elements = self.file.create_entity(
|
||||||
"IfcRelContainedInSpatialStructure",
|
"IfcRelContainedInSpatialStructure",
|
||||||
|
|||||||
@@ -21,13 +21,12 @@ class AssignContainer(bpy.types.Operator):
|
|||||||
break
|
break
|
||||||
if not relating_structure:
|
if not relating_structure:
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
usecase = assign_container.Usecase(
|
assign_container.Usecase(
|
||||||
self.file,
|
self.file,
|
||||||
{
|
{
|
||||||
"product": self.file.by_id(props.ifc_definition_id),
|
"product": self.file.by_id(props.ifc_definition_id),
|
||||||
"relating_structure": relating_structure,
|
"relating_structure": relating_structure,
|
||||||
},
|
},
|
||||||
)
|
).execute()
|
||||||
usecase.execute()
|
|
||||||
Data.load(props.ifc_definition_id)
|
Data.load(props.ifc_definition_id)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|||||||
@@ -678,65 +678,6 @@ class BIM_PT_classifications(Panel):
|
|||||||
row.prop(props, "classifications")
|
row.prop(props, "classifications")
|
||||||
|
|
||||||
|
|
||||||
class BIM_PT_mesh(Panel):
|
|
||||||
bl_label = "IFC Representations"
|
|
||||||
bl_idname = "BIM_PT_mesh"
|
|
||||||
bl_space_type = "PROPERTIES"
|
|
||||||
bl_region_type = "WINDOW"
|
|
||||||
bl_context = "data"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return (
|
|
||||||
context.active_object is not None
|
|
||||||
and context.active_object.type == "MESH"
|
|
||||||
and hasattr(context.active_object.data, "BIMMeshProperties")
|
|
||||||
)
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
if not context.active_object.data:
|
|
||||||
return
|
|
||||||
layout = self.layout
|
|
||||||
props = context.active_object.data.BIMMeshProperties
|
|
||||||
|
|
||||||
layout.label(text="IFC Parameters:")
|
|
||||||
row = layout.row()
|
|
||||||
row.operator("bim.get_representation_ifc_parameters")
|
|
||||||
row = layout.row()
|
|
||||||
row.operator("bim.bake_parametric_geometry")
|
|
||||||
for index, ifc_parameter in enumerate(props.ifc_parameters):
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.prop(ifc_parameter, "name", text="")
|
|
||||||
row.prop(ifc_parameter, "value", text="")
|
|
||||||
row.operator("bim.update_ifc_representation", icon="FILE_REFRESH", text="").index = index
|
|
||||||
|
|
||||||
row = layout.row()
|
|
||||||
row.prop(props, "is_parametric")
|
|
||||||
row = layout.row()
|
|
||||||
row.prop(props, "is_native")
|
|
||||||
row = layout.row()
|
|
||||||
row.prop(props, "is_swept_solid")
|
|
||||||
|
|
||||||
row = layout.row()
|
|
||||||
row.operator("bim.add_swept_solid")
|
|
||||||
for index, swept_solid in enumerate(props.swept_solids):
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.prop(swept_solid, "name", text="")
|
|
||||||
row.operator("bim.remove_swept_solid", icon="X", text="").index = index
|
|
||||||
row = layout.row()
|
|
||||||
sub = row.row(align=True)
|
|
||||||
sub.operator("bim.assign_swept_solid_outer_curve").index = index
|
|
||||||
sub.operator("bim.select_swept_solid_outer_curve", icon="RESTRICT_SELECT_OFF", text="").index = index
|
|
||||||
sub = row.row(align=True)
|
|
||||||
sub.operator("bim.add_swept_solid_inner_curve").index = index
|
|
||||||
sub.operator("bim.select_swept_solid_inner_curves", icon="RESTRICT_SELECT_OFF", text="").index = index
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.operator("bim.assign_swept_solid_extrusion").index = index
|
|
||||||
row.operator("bim.select_swept_solid_extrusion", icon="RESTRICT_SELECT_OFF", text="").index = index
|
|
||||||
row = layout.row()
|
|
||||||
row.prop(props, "swept_solids")
|
|
||||||
|
|
||||||
|
|
||||||
class BIM_PT_presentation_layer_data(Panel):
|
class BIM_PT_presentation_layer_data(Panel):
|
||||||
bl_label = "IFC Presentation Layers"
|
bl_label = "IFC Presentation Layers"
|
||||||
bl_idname = "BIM_PT_presentation"
|
bl_idname = "BIM_PT_presentation"
|
||||||
|
|||||||
Reference in New Issue
Block a user