mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Start simplifying collection tree to only place items in their container and only one level deep.
This should heavily simplify the complex logic around blender collections and give freedom to the user to do whatever they want.
This commit is contained in:
@@ -1602,108 +1602,13 @@ class IfcImporter:
|
||||
obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"]
|
||||
|
||||
def create_collections(self) -> None:
|
||||
self.create_spatial_decomposition_collections()
|
||||
if self.ifc_import_settings.collection_mode == "DECOMPOSITION":
|
||||
self.create_aggregate_and_nest_collections()
|
||||
elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION":
|
||||
pass
|
||||
|
||||
def create_spatial_decomposition_collections(self) -> None:
|
||||
for rel_aggregate in self.project["ifc"].IsDecomposedBy or []:
|
||||
self.create_spatial_decomposition_collection(self.project["blender"], rel_aggregate.RelatedObjects)
|
||||
|
||||
# Invalid IFCs may have orphaned spatial structure elements.
|
||||
orphaned_spaces = [e for e in self.spatial_elements if e.GlobalId not in self.collections]
|
||||
while orphaned_spaces:
|
||||
self.create_spatial_decomposition_collection(self.project["blender"], orphaned_spaces)
|
||||
orphaned_spaces = [e for e in self.spatial_elements if e.GlobalId not in self.collections]
|
||||
|
||||
for element in self.spatial_elements:
|
||||
collection = bpy.data.collections.new(tool.Loader.get_name(element))
|
||||
self.collections[element.GlobalId] = collection
|
||||
self.project["blender"].children.link(collection)
|
||||
tool.Loader.create_project_collection("Views")
|
||||
self.type_collection = tool.Loader.create_project_collection("Types")
|
||||
|
||||
def create_spatial_decomposition_collection(
|
||||
self, parent: Union[bpy.types.Collection, bpy.types.Object], related_objects: list[ifcopenshell.entity_instance]
|
||||
) -> None:
|
||||
for element in related_objects:
|
||||
if element not in self.spatial_elements:
|
||||
continue
|
||||
is_existing = False
|
||||
if self.has_existing_project:
|
||||
obj = tool.Ifc.get_object(element)
|
||||
if obj:
|
||||
is_existing = True
|
||||
collection = obj.BIMObjectProperties.collection
|
||||
self.collections[element.GlobalId] = collection
|
||||
if not is_existing:
|
||||
collection = bpy.data.collections.new(tool.Loader.get_name(element))
|
||||
self.collections[element.GlobalId] = collection
|
||||
parent.children.link(collection)
|
||||
if element.IsDecomposedBy:
|
||||
for rel_aggregate in element.IsDecomposedBy:
|
||||
self.create_spatial_decomposition_collection(collection, rel_aggregate.RelatedObjects)
|
||||
|
||||
def create_aggregate_and_nest_collections(self):
|
||||
if self.ifc_import_settings.has_filter:
|
||||
rel_aggregates = set()
|
||||
for element in self.elements:
|
||||
if decomposed_by := element.IsDecomposedBy:
|
||||
rel_aggregates.add(decomposed_by[0])
|
||||
elif decomposes := element.Decomposes:
|
||||
rel_aggregates.add(decomposes[0])
|
||||
elif nested_by := getattr(element, "IsNestedBy", []): # IFC2X3 does not have IsNestedBy
|
||||
if next((e for e in nested_by[0].RelatedObjects if not e.is_a("IfcPort")), None):
|
||||
rel_aggregates.add(nested_by[0])
|
||||
elif nests := getattr(element, "Nests", []):
|
||||
rel_aggregates.add(nests[0])
|
||||
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
|
||||
rel_aggregates.add(element.AdheresToElement[0])
|
||||
else:
|
||||
rel_aggregates = [
|
||||
r
|
||||
for r in self.file.by_type("IfcRelAggregates")
|
||||
if (relating_obj := r.RelatingObject).is_a("IfcElement") or relating_obj.is_a("IfcElementType")
|
||||
] + [
|
||||
r
|
||||
for r in self.file.by_type("IfcRelNests")
|
||||
if (
|
||||
(relating_obj := r.RelatingObject).is_a("IfcElement")
|
||||
or relating_obj.is_a("IfcElementType")
|
||||
or (relating_obj.is_a("IfcPositioningElement") and not relating_obj.is_a("IfcGrid"))
|
||||
)
|
||||
and [e for e in r.RelatedObjects if not e.is_a("IfcPort")]
|
||||
]
|
||||
if self.file.schema == "IFC4X3":
|
||||
rel_aggregates += [r for r in self.file.by_type("IfcRelAdheresToElement")]
|
||||
|
||||
if len(rel_aggregates) > 10000:
|
||||
# More than 10,000 collections makes Blender unhappy
|
||||
print("Skipping aggregate collections for performance.")
|
||||
self.ifc_import_settings.collection_mode = "SPATIAL_DECOMPOSITION"
|
||||
return
|
||||
|
||||
aggregates: dict[str, dict] = {}
|
||||
for rel_aggregate in rel_aggregates:
|
||||
element: ifcopenshell.entity_instance = getattr(rel_aggregate, "RelatingObject", None) or getattr(
|
||||
rel_aggregate, "RelatingElement"
|
||||
)
|
||||
collection = bpy.data.collections.new(tool.Loader.get_name(element))
|
||||
aggregates[element.GlobalId] = {"element": element, "collection": collection}
|
||||
self.collections[element.GlobalId] = collection
|
||||
|
||||
for global_id, aggregate in aggregates.items():
|
||||
parent = ifcopenshell.util.element.get_aggregate(aggregate["element"])
|
||||
if parent:
|
||||
self.collections[parent.GlobalId].children.link(aggregate["collection"])
|
||||
continue
|
||||
parent = ifcopenshell.util.element.get_container(aggregate["element"])
|
||||
if parent:
|
||||
self.collections[parent.GlobalId].children.link(aggregate["collection"])
|
||||
continue
|
||||
if aggregate["element"].is_a("IfcElementType"):
|
||||
self.type_collection.children.link(aggregate["collection"])
|
||||
continue
|
||||
self.project["blender"].children.link(aggregate["collection"])
|
||||
|
||||
def create_materials(self) -> None:
|
||||
for material in self.file.by_type("IfcMaterial"):
|
||||
self.create_material(material)
|
||||
@@ -1763,14 +1668,6 @@ class IfcImporter:
|
||||
self.place_object_in_collection(self.file.by_id(ifc_definition_id), obj)
|
||||
|
||||
def place_object_in_collection(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None:
|
||||
if self.ifc_import_settings.collection_mode == "DECOMPOSITION":
|
||||
self.place_object_in_decomposition_collection(element, obj)
|
||||
elif self.ifc_import_settings.collection_mode == "SPATIAL_DECOMPOSITION":
|
||||
self.place_object_in_spatial_decomposition_collection(element, obj)
|
||||
|
||||
def place_object_in_decomposition_collection(
|
||||
self, element: ifcopenshell.entity_instance, obj: bpy.types.Object
|
||||
) -> None:
|
||||
if element.is_a("IfcProject"):
|
||||
return
|
||||
elif element.is_a("IfcGridAxis"):
|
||||
@@ -1781,51 +1678,18 @@ class IfcImporter:
|
||||
obj.BIMObjectProperties.collection = collection
|
||||
collection.name = obj.name
|
||||
return collection.objects.link(obj)
|
||||
elif getattr(element, "Decomposes", None):
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
return self.collections[aggregate.GlobalId].objects.link(obj)
|
||||
elif getattr(element, "Nests", None) and not element.is_a("IfcPort"):
|
||||
nest = ifcopenshell.util.element.get_nest(element)
|
||||
return self.collections[nest.GlobalId].objects.link(obj)
|
||||
elif element.is_a("IfcSurfaceFeature") and self.file.schema == "IFC4X3":
|
||||
adherend = element.AdheresToElement[0].RelatingElement
|
||||
return self.collections[adherend.GlobalId].objects.link(obj)
|
||||
|
||||
return self.place_object_in_spatial_decomposition_collection(element, obj)
|
||||
|
||||
def place_object_in_spatial_decomposition_collection(
|
||||
self, element: ifcopenshell.entity_instance, obj: bpy.types.Object
|
||||
) -> None:
|
||||
if element.is_a("IfcProject"):
|
||||
return
|
||||
elif element.is_a("IfcGridAxis"):
|
||||
return
|
||||
elif element.GlobalId in self.collections:
|
||||
collection = self.collections[element.GlobalId]
|
||||
collection.BIMCollectionProperties.obj = obj
|
||||
obj.BIMObjectProperties.collection = collection
|
||||
return collection.objects.link(obj)
|
||||
elif element.is_a("IfcTypeObject"):
|
||||
return self.type_collection.objects.link(obj)
|
||||
elif element.is_a("IfcStructuralMember"):
|
||||
return self.structural_member_collection.objects.link(obj)
|
||||
elif element.is_a("IfcStructuralConnection"):
|
||||
return self.structural_connection_collection.objects.link(obj)
|
||||
elif container := ifcopenshell.util.element.get_container(element):
|
||||
self.collections[container.GlobalId].objects.link(obj)
|
||||
elif element.is_a("IfcAnnotation"):
|
||||
group = self.get_drawing_group(element)
|
||||
if group:
|
||||
return self.collections[group.GlobalId].objects.link(obj)
|
||||
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if container:
|
||||
if element.is_a("IfcGrid"): # TODO: refactor into a more holistic collection mode feature
|
||||
grid_collection = bpy.data.collections.get(obj.name)
|
||||
if grid_collection: # Just in case we run into invalid grids from Revit
|
||||
self.collections[container.GlobalId].children.link(grid_collection)
|
||||
grid_collection.objects.link(obj)
|
||||
else:
|
||||
self.collections[container.GlobalId].objects.link(obj)
|
||||
|
||||
else:
|
||||
self.ifc_import_settings.logger.warning("Warning: this object is outside the spatial hierarchy %s", element)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
@@ -2068,7 +1932,6 @@ class IfcImportSettings:
|
||||
self.should_filter_spatial_elements = True
|
||||
self.should_setup_viewport_camera = True
|
||||
self.elements: set[ifcopenshell.entity_instance] = set()
|
||||
self.collection_mode = "DECOMPOSITION"
|
||||
|
||||
@staticmethod
|
||||
def factory(context=None, input_file=None, logger=None):
|
||||
@@ -2080,7 +1943,6 @@ class IfcImportSettings:
|
||||
logger = logging.getLogger("ImportIFC")
|
||||
settings.logger = logger
|
||||
settings.diff_file = scene_diff.diff_json_file
|
||||
settings.collection_mode = props.collection_mode
|
||||
settings.should_use_cpu_multiprocessing = props.should_use_cpu_multiprocessing
|
||||
settings.merge_mode = props.merge_mode
|
||||
settings.should_merge_materials_by_colour = props.should_merge_materials_by_colour
|
||||
|
||||
@@ -125,15 +125,6 @@ class BIMProjectProperties(PropertyGroup):
|
||||
library_breadcrumb: CollectionProperty(name="Library Breadcrumb", type=StrProperty)
|
||||
library_elements: CollectionProperty(name="Library Elements", type=LibraryElement)
|
||||
active_library_element_index: IntProperty(name="Active Library Element Index")
|
||||
collection_mode: bpy.props.EnumProperty(
|
||||
items=[
|
||||
("DECOMPOSITION", "Decomposition", "Collections represent aggregates and spatial containers"),
|
||||
("SPATIAL_DECOMPOSITION", "Spatial Decomposition", "Collections represent spatial containers"),
|
||||
("IFC_CLASS", "IFC Class", "Collections represent IFC class"),
|
||||
("NONE", "None", "No collections are created"),
|
||||
],
|
||||
name="Collection Mode",
|
||||
)
|
||||
filter_mode: bpy.props.EnumProperty(
|
||||
items=[
|
||||
("NONE", "None", "No filtering is performed"),
|
||||
|
||||
@@ -104,7 +104,6 @@ class BIM_PT_project(Panel):
|
||||
|
||||
def draw_load_ui(self, context):
|
||||
pprops = context.scene.BIMProjectProperties
|
||||
prop_with_search(self.layout, pprops, "collection_mode")
|
||||
prop_with_search(self.layout, pprops, "filter_mode")
|
||||
if pprops.filter_mode in ["DECOMPOSITION", "IFC_CLASS", "IFC_TYPE"]:
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
@@ -17,13 +17,11 @@
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.util.element
|
||||
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.core.spatial
|
||||
import blenderbim.core.aggregate
|
||||
import blenderbim.tool as tool
|
||||
from typing import Union
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
class Collector(blenderbim.core.tool.Collector):
|
||||
@@ -33,164 +31,52 @@ class Collector(blenderbim.core.tool.Collector):
|
||||
and unlink them from any other"""
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
|
||||
object_collection = None
|
||||
collection_collection = None
|
||||
if element.is_a("IfcGridAxis"):
|
||||
element = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
|
||||
|
||||
object_collection = cls._get_own_collection(element, obj)
|
||||
if object_collection:
|
||||
collection_collection = cls._get_collection(element, obj)
|
||||
else:
|
||||
object_collection = cls._get_collection(element, obj)
|
||||
|
||||
# NOTE: calling `obj.users_collection` is expensive in large projects
|
||||
# since it's iterating over all collections under the hood
|
||||
|
||||
# ensure object is linked only to object_collection
|
||||
if obj.users_collection != (object_collection,):
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.unlink(obj)
|
||||
if object_collection is not None:
|
||||
object_collection.objects.link(obj)
|
||||
|
||||
# ensure object_collection is linked only to collection_collection
|
||||
if collection_collection and collection_collection.children.find(object_collection.name) == -1:
|
||||
if bpy.context.scene.collection.children.find(object_collection.name) != -1:
|
||||
bpy.context.scene.collection.children.unlink(object_collection)
|
||||
for collection in bpy.data.collections:
|
||||
if collection.children.find(object_collection.name) != -1:
|
||||
collection.children.unlink(object_collection)
|
||||
collection_collection.children.link(object_collection)
|
||||
|
||||
# If an aggregate or nested host loses all its children, it no longer needs its own collection
|
||||
if obj.BIMObjectProperties.collection and obj.BIMObjectProperties.collection != object_collection:
|
||||
bpy.data.collections.remove(obj.BIMObjectProperties.collection)
|
||||
|
||||
@classmethod
|
||||
def _get_own_collection(
|
||||
cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object
|
||||
) -> Union[bpy.types.Collection, None]:
|
||||
"""get or create own collection for the element if it's neccessary for it's type"""
|
||||
if element.is_a("IfcProject"):
|
||||
return cls._create_own_collection(obj)
|
||||
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
if element.is_a("IfcSpatialStructureElement"):
|
||||
return cls._create_own_collection(obj)
|
||||
else:
|
||||
if element.is_a("IfcSpatialStructureElement") or element.is_a("IfcExternalSpatialStructureElement"):
|
||||
return cls._create_own_collection(obj)
|
||||
|
||||
if element.is_a("IfcGrid"):
|
||||
return cls._create_own_collection(obj)
|
||||
|
||||
if element.is_a("IfcGridAxis"):
|
||||
if element.PartOfU:
|
||||
grid = element.PartOfU[0]
|
||||
axes = "UAxes"
|
||||
elif element.PartOfV:
|
||||
grid = element.PartOfV[0]
|
||||
axes = "VAxes"
|
||||
elif element.PartOfW:
|
||||
grid = element.PartOfW[0]
|
||||
axes = "WAxes"
|
||||
grid_obj = tool.Ifc.get_object(grid)
|
||||
if grid_obj:
|
||||
grid_col = cls._get_own_collection(grid, grid_obj)
|
||||
axes_col = next((c for c in grid_col.children if axes in c.name), None)
|
||||
return axes_col or bpy.data.collections.new(axes)
|
||||
|
||||
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
return cls._create_own_collection(obj)
|
||||
|
||||
if element.is_a("IfcStructuralMember"):
|
||||
return bpy.data.collections.get("Members") or bpy.data.collections.new("Members")
|
||||
|
||||
if element.is_a("IfcStructuralConnection"):
|
||||
return bpy.data.collections.get("Connections") or bpy.data.collections.new("Connections")
|
||||
|
||||
if getattr(element, "IsDecomposedBy", None):
|
||||
return cls._create_own_collection(obj)
|
||||
|
||||
if getattr(element, "IsNestedBy", None):
|
||||
if any(e for e in element.IsNestedBy[0].RelatedObjects if not e.is_a("IfcPort")):
|
||||
return cls._create_own_collection(obj)
|
||||
|
||||
if getattr(element, "HasSurfaceFeatures", None):
|
||||
return cls._create_own_collection(obj)
|
||||
|
||||
@classmethod
|
||||
def _get_collection(cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> bpy.types.Collection:
|
||||
"""get or create collection for the element based on it's type"""
|
||||
if element.is_a("IfcTypeObject"):
|
||||
return cls._create_project_child_collection("Types")
|
||||
|
||||
if element.is_a("IfcOpeningElement"):
|
||||
return cls._create_project_child_collection("IfcOpeningElements")
|
||||
|
||||
if element.is_a("IfcGridAxis"):
|
||||
if element.PartOfU:
|
||||
grid = element.PartOfU[0]
|
||||
axes = "UAxes"
|
||||
elif element.PartOfV:
|
||||
grid = element.PartOfV[0]
|
||||
axes = "VAxes"
|
||||
elif element.PartOfW:
|
||||
grid = element.PartOfW[0]
|
||||
axes = "WAxes"
|
||||
grid_obj = tool.Ifc.get_object(grid)
|
||||
if grid_obj:
|
||||
return grid_obj.BIMObjectProperties.collection
|
||||
|
||||
if element.is_a("IfcAnnotation"):
|
||||
if element.ObjectType == "DRAWING":
|
||||
return cls._create_project_child_collection("Views")
|
||||
for rel in element.HasAssignments or []:
|
||||
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
|
||||
for related_object in rel.RelatedObjects:
|
||||
if related_object.is_a("IfcAnnotation") and related_object.ObjectType == "DRAWING":
|
||||
drawing_obj = tool.Ifc.get_object(related_object)
|
||||
if drawing_obj:
|
||||
return drawing_obj.BIMObjectProperties.collection
|
||||
|
||||
if element.is_a("IfcStructuralItem"):
|
||||
return cls._create_project_child_collection("StructuralItems")
|
||||
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if aggregate:
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
if aggregate_obj:
|
||||
collection = aggregate_obj.BIMObjectProperties.collection
|
||||
if collection:
|
||||
return collection
|
||||
|
||||
nest = ifcopenshell.util.element.get_nest(element)
|
||||
if nest:
|
||||
nest_obj = tool.Ifc.get_object(nest)
|
||||
if nest_obj:
|
||||
collection = nest_obj.BIMObjectProperties.collection
|
||||
if collection:
|
||||
return collection
|
||||
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if container:
|
||||
if collection := cls._create_own_collection(obj):
|
||||
collection.objects.link(obj)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
elif element.is_a("IfcTypeProduct"):
|
||||
collection = cls._create_project_child_collection("IfcTypeProduct")
|
||||
collection.objects.link(obj)
|
||||
elif element.is_a("IfcOpeningElement"):
|
||||
collection = cls._create_project_child_collection("IfcOpeningElement")
|
||||
collection.objects.link(obj)
|
||||
elif element.is_a("IfcStructuralItem"):
|
||||
collection = cls._create_project_child_collection("IfcStructuralItem")
|
||||
collection.objects.link(obj)
|
||||
elif tool.Ifc.get_schema() == "IFC2X3" and element.is_a("IfcSpatialStructureElement"):
|
||||
if collection := cls._create_own_collection(obj):
|
||||
collection.objects.link(obj)
|
||||
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
||||
project_obj.BIMObjectProperties.collection.children.link(collection)
|
||||
elif tool.Ifc.get_schema() != "IFC2X3" and element.is_a("IfcSpatialElement"):
|
||||
if collection := cls._create_own_collection(obj):
|
||||
collection.objects.link(obj)
|
||||
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
||||
project_obj.BIMObjectProperties.collection.children.link(collection)
|
||||
elif container := ifcopenshell.util.element.get_container(element):
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
collection = container_obj.BIMObjectProperties.collection
|
||||
if collection:
|
||||
return collection
|
||||
|
||||
if element.is_a("IfcSurfaceFeature") and element.file.schema == "IFC4X3":
|
||||
adherend = element.AdheresToElement[0].RelatingElement
|
||||
adherend_obj = tool.Ifc.get_object(adherend)
|
||||
collection = adherend_obj.BIMObjectProperties.collection
|
||||
if collection:
|
||||
return collection
|
||||
|
||||
if element.is_a("IfcProject"):
|
||||
return bpy.context.scene.collection
|
||||
|
||||
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
||||
if project_obj:
|
||||
return project_obj.BIMObjectProperties.collection
|
||||
container_obj.BIMObjectProperties.collection.objects.link(obj)
|
||||
elif element.is_a("IfcAnnotation"):
|
||||
if element.ObjectType == "DRAWING":
|
||||
if collection := cls._create_own_collection(obj):
|
||||
collection.objects.link(obj)
|
||||
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
||||
project_obj.BIMObjectProperties.collection.children.link(collection)
|
||||
else:
|
||||
for rel in element.HasAssignments or []:
|
||||
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
|
||||
for related_object in rel.RelatedObjects:
|
||||
if related_object.is_a("IfcAnnotation") and related_object.ObjectType == "DRAWING":
|
||||
drawing_obj = tool.Ifc.get_object(related_object)
|
||||
if drawing_obj:
|
||||
drawing_obj.BIMObjectProperties.collection.objects.link(obj)
|
||||
else:
|
||||
collection = cls._create_project_child_collection("Unsorted")
|
||||
collection.objects.link(obj)
|
||||
|
||||
@classmethod
|
||||
def _create_project_child_collection(cls, name: str) -> bpy.types.Collection:
|
||||
@@ -207,7 +93,7 @@ class Collector(blenderbim.core.tool.Collector):
|
||||
def _create_own_collection(cls, obj: bpy.types.Object) -> bpy.types.Collection:
|
||||
"""get or create own collection for the element"""
|
||||
if obj.BIMObjectProperties.collection:
|
||||
return obj.BIMObjectProperties.collection
|
||||
return
|
||||
collection = bpy.data.collections.new(obj.name)
|
||||
obj.BIMObjectProperties.collection = collection
|
||||
collection.BIMCollectionProperties.obj = obj
|
||||
|
||||
Reference in New Issue
Block a user