mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 06:58:56 +00:00
Purge obsolete search operators in favour of new search filters, colour legend system.
This commit is contained in:
@@ -161,7 +161,7 @@ def add_layout_hotkey_operator(layout, text, hotkey, description):
|
|||||||
|
|
||||||
|
|
||||||
# TODO: move to operator
|
# TODO: move to operator
|
||||||
def create_annotation_occurence(context):
|
def create_annotation_occurrence(context):
|
||||||
props = context.scene.BIMAnnotationProperties
|
props = context.scene.BIMAnnotationProperties
|
||||||
relating_type = tool.Ifc.get().by_id(int(props.relating_type_id))
|
relating_type = tool.Ifc.get().by_id(int(props.relating_type_id))
|
||||||
object_type = props.object_type
|
object_type = props.object_type
|
||||||
@@ -189,9 +189,8 @@ def create_annotation_occurence(context):
|
|||||||
|
|
||||||
def create_annotation():
|
def create_annotation():
|
||||||
props = bpy.context.scene.BIMAnnotationProperties
|
props = bpy.context.scene.BIMAnnotationProperties
|
||||||
create_type_occurence = props.relating_type_id != "0"
|
if props.relating_type_id != "0":
|
||||||
if create_type_occurence:
|
create_annotation_occurrence(bpy.context)
|
||||||
create_annotation_occurence(bpy.context)
|
|
||||||
else:
|
else:
|
||||||
object_type = props.object_type
|
object_type = props.object_type
|
||||||
bpy.ops.bim.add_annotation(object_type=object_type, data_type=ANNOTATION_TYPES_DATA[object_type][-1])
|
bpy.ops.bim.add_annotation(object_type=object_type, data_type=ANNOTATION_TYPES_DATA[object_type][-1])
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ classes = (
|
|||||||
operator.AddFilterGroup,
|
operator.AddFilterGroup,
|
||||||
operator.AddToIfcGroup,
|
operator.AddToIfcGroup,
|
||||||
operator.ColourByProperty,
|
operator.ColourByProperty,
|
||||||
operator.ColourByAttribute,
|
|
||||||
operator.ColourByClass,
|
|
||||||
operator.ColourByPset,
|
|
||||||
operator.FilterModelElements,
|
operator.FilterModelElements,
|
||||||
operator.IfcSelector,
|
operator.IfcSelector,
|
||||||
operator.LoadColourscheme,
|
operator.LoadColourscheme,
|
||||||
@@ -42,10 +39,8 @@ classes = (
|
|||||||
operator.SaveSearch,
|
operator.SaveSearch,
|
||||||
operator.SaveSelectorQuery,
|
operator.SaveSelectorQuery,
|
||||||
operator.Search,
|
operator.Search,
|
||||||
operator.SelectAttribute,
|
|
||||||
operator.SelectGlobalId,
|
operator.SelectGlobalId,
|
||||||
operator.SelectIfcClass,
|
operator.SelectIfcClass,
|
||||||
operator.SelectPset,
|
|
||||||
operator.SelectSimilar,
|
operator.SelectSimilar,
|
||||||
operator.ShowAllElements,
|
operator.ShowAllElements,
|
||||||
operator.ToggleFilterSelection,
|
operator.ToggleFilterSelection,
|
||||||
|
|||||||
@@ -58,19 +58,6 @@ colour_list = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def does_keyword_exist(pattern, string, context):
|
|
||||||
string = str(string)
|
|
||||||
props = context.scene.BIMSearchProperties
|
|
||||||
if props.should_use_regex and props.should_ignorecase and re.search(pattern, string, flags=re.IGNORECASE):
|
|
||||||
return True
|
|
||||||
elif props.should_use_regex and re.search(pattern, string):
|
|
||||||
return True
|
|
||||||
elif props.should_ignorecase and string.lower() == pattern.lower():
|
|
||||||
return True
|
|
||||||
elif string == pattern:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class AddFilterGroup(Operator):
|
class AddFilterGroup(Operator):
|
||||||
bl_idname = "bim.add_filter_group"
|
bl_idname = "bim.add_filter_group"
|
||||||
bl_label = "Add Filter Group"
|
bl_label = "Add Filter Group"
|
||||||
@@ -336,291 +323,13 @@ class SelectIfcClass(Operator):
|
|||||||
ifc_class: StringProperty()
|
ifc_class: StringProperty()
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
ifc_class = self.ifc_class
|
for element in tool.Ifc.get().by_type(self.ifc_class):
|
||||||
|
obj = tool.Ifc.get_object(element)
|
||||||
if not ifc_class:
|
if obj:
|
||||||
self.report({"ERROR"}, "Set IFC Class for search.")
|
|
||||||
return {"CANCELLED"}
|
|
||||||
|
|
||||||
sch = tool.Ifc.schema()
|
|
||||||
try:
|
|
||||||
sch.declaration_by_name(ifc_class)
|
|
||||||
except RuntimeError:
|
|
||||||
self.report({"ERROR"}, f"IFC Class '{ifc_class}' is not found in schema {sch.name()}.")
|
|
||||||
return {"CANCELLED"}
|
|
||||||
|
|
||||||
self.file = IfcStore.get_file()
|
|
||||||
n_objects = 0
|
|
||||||
for obj in context.visible_objects:
|
|
||||||
if not obj.BIMObjectProperties.ifc_definition_id or obj.is_library_indirect:
|
|
||||||
continue
|
|
||||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
|
||||||
if does_keyword_exist(ifc_class, element.is_a(), context):
|
|
||||||
obj.select_set(True)
|
obj.select_set(True)
|
||||||
n_objects += 1
|
|
||||||
self.report({"INFO"}, f"{n_objects} objects selected.")
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
class SelectAttribute(Operator):
|
|
||||||
"""Click to select all objects that match with the given Attribute Name and Value"""
|
|
||||||
|
|
||||||
bl_idname = "bim.select_attribute"
|
|
||||||
bl_label = "Select Attribute"
|
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
|
||||||
|
|
||||||
attribute_name: bpy.props.StringProperty(default="")
|
|
||||||
attribute_value: bpy.props.StringProperty(default="")
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
attribute_name = self.attribute_name or context.scene.BIMSearchProperties.search_attribute_name
|
|
||||||
attribute_value = self.attribute_value
|
|
||||||
|
|
||||||
if not attribute_name:
|
|
||||||
self.report({"ERROR"}, "Set Attribute Name for search.")
|
|
||||||
return {"CANCELLED"}
|
|
||||||
|
|
||||||
self.file = IfcStore.get_file()
|
|
||||||
n_objects = 0
|
|
||||||
for obj in context.visible_objects:
|
|
||||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
|
||||||
continue
|
|
||||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
|
||||||
if context.scene.BIMSearchProperties.should_ignorecase:
|
|
||||||
data = element.get_info()
|
|
||||||
value = next((v for k, v in data.items() if k.lower() == attribute_name.lower()), None)
|
|
||||||
else:
|
|
||||||
value = getattr(element, attribute_name, None)
|
|
||||||
if does_keyword_exist(attribute_value, value, context):
|
|
||||||
obj.select_set(True)
|
|
||||||
n_objects += 1
|
|
||||||
self.report({"INFO"}, f"{n_objects} objects selected.")
|
|
||||||
return {"FINISHED"}
|
|
||||||
|
|
||||||
|
|
||||||
class SelectPset(Operator):
|
|
||||||
"""Click to select all objects that match with the given Pset Name, Properties Name and Value"""
|
|
||||||
|
|
||||||
bl_idname = "bim.select_pset"
|
|
||||||
bl_label = "Select Pset"
|
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
|
||||||
|
|
||||||
pset_name: bpy.props.StringProperty(default="")
|
|
||||||
prop_name: bpy.props.StringProperty(default="")
|
|
||||||
pset_value: bpy.props.StringProperty(default="")
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
search_pset_name = self.pset_name or context.scene.BIMSearchProperties.search_pset_name
|
|
||||||
search_prop_name = self.prop_name or context.scene.BIMSearchProperties.search_prop_name
|
|
||||||
pattern = self.pset_value
|
|
||||||
|
|
||||||
if not search_pset_name:
|
|
||||||
self.report({"ERROR"}, "Set PSet name for search.")
|
|
||||||
return {"CANCELLED"}
|
|
||||||
if not search_prop_name:
|
|
||||||
self.report({"ERROR"}, "Set Property name for search.")
|
|
||||||
return {"CANCELLED"}
|
|
||||||
|
|
||||||
self.file = IfcStore.get_file()
|
|
||||||
props = context.scene.BIMSearchProperties
|
|
||||||
n_objects = 0
|
|
||||||
for obj in context.visible_objects:
|
|
||||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
|
||||||
continue
|
|
||||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
|
||||||
psets = ifcopenshell.util.element.get_psets(element)
|
|
||||||
if search_pset_name == "":
|
|
||||||
props = {}
|
|
||||||
[props.update(p) for p in psets.values()]
|
|
||||||
else:
|
|
||||||
props = None
|
|
||||||
if context.scene.BIMSearchProperties.should_ignorecase:
|
|
||||||
props = props or next((v for k, v in psets.items() if k.lower() == search_pset_name.lower()), {})
|
|
||||||
value = str(next((v for k, v in props.items() if k.lower() == search_prop_name.lower()), None))
|
|
||||||
else:
|
|
||||||
props = props or psets.get(search_pset_name, {})
|
|
||||||
value = props.get(search_prop_name, None)
|
|
||||||
if does_keyword_exist(pattern, value, context):
|
|
||||||
obj.select_set(True)
|
|
||||||
n_objects += 1
|
|
||||||
self.report({"INFO"}, f"{n_objects} objects selected.")
|
|
||||||
return {"FINISHED"}
|
|
||||||
|
|
||||||
|
|
||||||
class ColourByAttribute(Operator):
|
|
||||||
"""Click to colour different objects according to given Attribute Name"""
|
|
||||||
|
|
||||||
bl_idname = "bim.colour_by_attribute"
|
|
||||||
bl_label = "Colour by Attribute"
|
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
|
||||||
|
|
||||||
attribute_name: bpy.props.StringProperty(default="")
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
IfcStore.begin_transaction(self)
|
|
||||||
self.store_state(context)
|
|
||||||
result = self._execute(context)
|
|
||||||
IfcStore.add_transaction_operation(self)
|
|
||||||
IfcStore.end_transaction(self)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _execute(self, context):
|
|
||||||
attribute_name = self.attribute_name or context.scene.BIMSearchProperties.search_attribute_name
|
|
||||||
|
|
||||||
if not attribute_name:
|
|
||||||
self.report({"ERROR"}, "Set Attribute Name for search.")
|
|
||||||
return {"CANCELLED"}
|
|
||||||
|
|
||||||
self.file = IfcStore.get_file()
|
|
||||||
colours = cycle(colour_list)
|
|
||||||
values = {}
|
|
||||||
for obj in context.visible_objects:
|
|
||||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
|
||||||
continue
|
|
||||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
|
||||||
if context.scene.BIMSearchProperties.should_ignorecase:
|
|
||||||
data = element.get_info()
|
|
||||||
value = next((v for k, v in data.items() if k.lower() == attribute_name.lower()), None)
|
|
||||||
else:
|
|
||||||
value = getattr(element, attribute_name, None)
|
|
||||||
if value not in values:
|
|
||||||
values[value] = next(colours)
|
|
||||||
obj.color = values[value]
|
|
||||||
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
|
|
||||||
if areas:
|
|
||||||
areas[0].spaces[0].shading.color_type = "OBJECT"
|
|
||||||
return {"FINISHED"}
|
|
||||||
|
|
||||||
def store_state(self, context):
|
|
||||||
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
|
|
||||||
if areas:
|
|
||||||
self.transaction_data = {"area": areas[0], "color_type": areas[0].spaces[0].shading.color_type}
|
|
||||||
|
|
||||||
def rollback(self, data):
|
|
||||||
if data:
|
|
||||||
data["area"].spaces[0].shading.color_type = data["color_type"]
|
|
||||||
|
|
||||||
def commit(self, data):
|
|
||||||
if data:
|
|
||||||
data["area"].spaces[0].shading.color_type = "OBJECT"
|
|
||||||
|
|
||||||
|
|
||||||
class ColourByPset(Operator):
|
|
||||||
"""Click to colour different objects according to given Prop Name"""
|
|
||||||
|
|
||||||
bl_idname = "bim.colour_by_pset"
|
|
||||||
bl_label = "Colour by Pset"
|
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
|
||||||
|
|
||||||
pset_name: bpy.props.StringProperty(default="")
|
|
||||||
prop_name: bpy.props.StringProperty(default="")
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
IfcStore.begin_transaction(self)
|
|
||||||
self.store_state(context)
|
|
||||||
result = self._execute(context)
|
|
||||||
IfcStore.add_transaction_operation(self)
|
|
||||||
IfcStore.end_transaction(self)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _execute(self, context):
|
|
||||||
search_pset_name = self.pset_name or context.scene.BIMSearchProperties.search_pset_name
|
|
||||||
search_prop_name = self.prop_name or context.scene.BIMSearchProperties.search_prop_name
|
|
||||||
|
|
||||||
if not search_pset_name:
|
|
||||||
self.report({"ERROR"}, "Set PSet name for search.")
|
|
||||||
return {"CANCELLED"}
|
|
||||||
if not search_prop_name:
|
|
||||||
self.report({"ERROR"}, "Set Property name for search.")
|
|
||||||
return {"CANCELLED"}
|
|
||||||
|
|
||||||
self.file = IfcStore.get_file()
|
|
||||||
colours = cycle(colour_list)
|
|
||||||
values = {}
|
|
||||||
for obj in context.visible_objects:
|
|
||||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
|
||||||
continue
|
|
||||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
|
||||||
psets = ifcopenshell.util.element.get_psets(element)
|
|
||||||
if search_pset_name == "":
|
|
||||||
props = {}
|
|
||||||
[props.update(p) for p in psets.values()]
|
|
||||||
else:
|
|
||||||
props = None
|
|
||||||
if context.scene.BIMSearchProperties.should_ignorecase:
|
|
||||||
props = props or next((v for k, v in psets.items() if k.lower() == search_pset_name.lower()), {})
|
|
||||||
value = str(next((v for k, v in props.items() if k.lower() == search_prop_name.lower()), None))
|
|
||||||
else:
|
|
||||||
props = props or psets.get(search_pset_name, {})
|
|
||||||
value = str(props.get(search_prop_name, None))
|
|
||||||
if value not in values:
|
|
||||||
values[value] = next(colours)
|
|
||||||
obj.color = values[value]
|
|
||||||
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
|
|
||||||
if areas:
|
|
||||||
areas[0].spaces[0].shading.color_type = "OBJECT"
|
|
||||||
return {"FINISHED"}
|
|
||||||
|
|
||||||
def store_state(self, context):
|
|
||||||
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
|
|
||||||
if areas:
|
|
||||||
self.transaction_data = {"area": areas[0], "color_type": areas[0].spaces[0].shading.color_type}
|
|
||||||
|
|
||||||
def rollback(self, data):
|
|
||||||
if data:
|
|
||||||
data["area"].spaces[0].shading.color_type = data["color_type"]
|
|
||||||
|
|
||||||
def commit(self, data):
|
|
||||||
if data:
|
|
||||||
data["area"].spaces[0].shading.color_type = "OBJECT"
|
|
||||||
|
|
||||||
|
|
||||||
class ColourByClass(Operator):
|
|
||||||
"""Click to colour different objects according to their IFC Classes"""
|
|
||||||
|
|
||||||
bl_idname = "bim.colour_by_class"
|
|
||||||
bl_label = "Colour by Class"
|
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
IfcStore.begin_transaction(self)
|
|
||||||
self.store_state(context)
|
|
||||||
result = self._execute(context)
|
|
||||||
IfcStore.add_transaction_operation(self)
|
|
||||||
IfcStore.end_transaction(self)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _execute(self, context):
|
|
||||||
self.file = IfcStore.get_file()
|
|
||||||
colours = cycle(colour_list)
|
|
||||||
ifc_classes = {}
|
|
||||||
for obj in context.visible_objects:
|
|
||||||
if not obj.BIMObjectProperties.ifc_definition_id:
|
|
||||||
continue
|
|
||||||
element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
|
||||||
ifc_class = element.is_a()
|
|
||||||
if ifc_class not in ifc_classes:
|
|
||||||
ifc_classes[ifc_class] = next(colours)
|
|
||||||
obj.color = ifc_classes[ifc_class]
|
|
||||||
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
|
|
||||||
if areas:
|
|
||||||
areas[0].spaces[0].shading.color_type = "OBJECT"
|
|
||||||
return {"FINISHED"}
|
|
||||||
|
|
||||||
def store_state(self, context):
|
|
||||||
areas = [a for a in context.screen.areas if a.type == "VIEW_3D"]
|
|
||||||
if areas:
|
|
||||||
self.transaction_data = {"area": areas[0], "color_type": areas[0].spaces[0].shading.color_type}
|
|
||||||
|
|
||||||
def rollback(self, data):
|
|
||||||
if data:
|
|
||||||
data["area"].spaces[0].shading.color_type = data["color_type"]
|
|
||||||
|
|
||||||
def commit(self, data):
|
|
||||||
if data:
|
|
||||||
data["area"].spaces[0].shading.color_type = "OBJECT"
|
|
||||||
|
|
||||||
|
|
||||||
class ResetObjectColours(Operator):
|
class ResetObjectColours(Operator):
|
||||||
"""Reset the colour of visible objects"""
|
"""Reset the colour of visible objects"""
|
||||||
|
|
||||||
|
|||||||
@@ -136,15 +136,6 @@ class BIMSearchProperties(PropertyGroup):
|
|||||||
colourscheme_query: StringProperty(name="Colourscheme Query", default="class")
|
colourscheme_query: StringProperty(name="Colourscheme Query", default="class")
|
||||||
colourscheme: CollectionProperty(type=BIMColour)
|
colourscheme: CollectionProperty(type=BIMColour)
|
||||||
active_colourscheme_index: IntProperty(name="Active Colourscheme Index")
|
active_colourscheme_index: IntProperty(name="Active Colourscheme Index")
|
||||||
should_use_regex: BoolProperty(name="Search With Regex", default=False)
|
|
||||||
should_ignorecase: BoolProperty(name="Search Ignoring Case", default=True)
|
|
||||||
global_id: StringProperty(name="GlobalId")
|
|
||||||
ifc_class: StringProperty(name="IFC Class")
|
|
||||||
search_attribute_name: StringProperty(name="Search Attribute Name")
|
|
||||||
search_attribute_value: StringProperty(name="Search Attribute Value")
|
|
||||||
search_pset_name: StringProperty(name="Search Pset Name")
|
|
||||||
search_prop_name: StringProperty(name="Search Prop Name")
|
|
||||||
search_pset_value: StringProperty(name="Search Pset Value")
|
|
||||||
filter_type: StringProperty(name="Filter Type")
|
filter_type: StringProperty(name="Filter Type")
|
||||||
filter_classes: CollectionProperty(type=BIMFilterClasses, name="Filter Classes")
|
filter_classes: CollectionProperty(type=BIMFilterClasses, name="Filter Classes")
|
||||||
filter_classes_index: IntProperty(name="Filter Classes Index")
|
filter_classes_index: IntProperty(name="Filter Classes Index")
|
||||||
|
|||||||
@@ -95,44 +95,6 @@ class BIM_PT_search(Panel):
|
|||||||
|
|
||||||
return # Temporary for now whilst searching is being upgraded.
|
return # Temporary for now whilst searching is being upgraded.
|
||||||
|
|
||||||
row = self.layout.row()
|
|
||||||
row.prop(props, "should_use_regex")
|
|
||||||
row = self.layout.row()
|
|
||||||
row.prop(props, "should_ignorecase")
|
|
||||||
|
|
||||||
row = self.layout.row(align=True)
|
|
||||||
row.operator("bim.reset_object_colours", icon="BRUSH_DATA")
|
|
||||||
|
|
||||||
row = self.layout.row(align=True)
|
|
||||||
row.prop(props, "global_id", text="", icon="TRACKER")
|
|
||||||
row.operator("bim.select_global_id", text="", icon="VIEWZOOM").global_id = props.global_id
|
|
||||||
|
|
||||||
row = self.layout.row(align=True)
|
|
||||||
row.prop(props, "ifc_class", text="", icon="OBJECT_DATA")
|
|
||||||
row.operator("bim.select_ifc_class", text="", icon="VIEWZOOM").ifc_class = props.ifc_class
|
|
||||||
row.operator("bim.colour_by_class", text="", icon="BRUSH_DATA")
|
|
||||||
|
|
||||||
row = self.layout.row(align=True)
|
|
||||||
row.prop(props, "search_attribute_name", text="", icon="PROPERTIES")
|
|
||||||
row.prop(props, "search_attribute_value", text="")
|
|
||||||
op = row.operator("bim.select_attribute", text="", icon="VIEWZOOM")
|
|
||||||
op.attribute_name = props.search_attribute_name
|
|
||||||
op.attribute_value = props.search_attribute_value
|
|
||||||
op = row.operator("bim.colour_by_attribute", text="", icon="BRUSH_DATA")
|
|
||||||
op.attribute_name = props.search_attribute_name
|
|
||||||
|
|
||||||
row = self.layout.row(align=True)
|
|
||||||
row.prop(props, "search_pset_name", text="", icon="COPY_ID")
|
|
||||||
row.prop(props, "search_prop_name", text="")
|
|
||||||
row.prop(props, "search_pset_value", text="")
|
|
||||||
op = row.operator("bim.select_pset", text="", icon="VIEWZOOM")
|
|
||||||
op.pset_name = props.search_pset_name
|
|
||||||
op.prop_name = props.search_prop_name
|
|
||||||
op.pset_value = props.search_pset_value
|
|
||||||
op = row.operator("bim.colour_by_pset", text="", icon="BRUSH_DATA")
|
|
||||||
op.pset_name = props.search_pset_name
|
|
||||||
op.prop_name = props.search_prop_name
|
|
||||||
|
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
row.operator("bim.activate_ifc_class_filter", icon="FILTER")
|
row.operator("bim.activate_ifc_class_filter", icon="FILTER")
|
||||||
row.operator("bim.activate_ifc_building_storey_filter", icon="FILTER")
|
row.operator("bim.activate_ifc_building_storey_filter", icon="FILTER")
|
||||||
|
|||||||
Reference in New Issue
Block a user