mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Black
This commit is contained in:
@@ -1129,7 +1129,6 @@ class IfcImporter:
|
||||
tool.Aggregate.constrain_all_parts_to_aggregate(relating_obj)
|
||||
bpy.context.scene.BIMAggregateProperties.aggregate_decorator = True
|
||||
|
||||
|
||||
def lock_scales(self) -> None:
|
||||
elements = set(self.file.by_type("IfcProduct"))
|
||||
while elements:
|
||||
|
||||
@@ -207,7 +207,6 @@ class AggregateDecorator:
|
||||
parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(aggregate))
|
||||
parts_objs = [tool.Ifc.get_object(p) for p in parts]
|
||||
|
||||
|
||||
indices, edges = create_bounding_box(parts_objs)
|
||||
self.line_shader.uniform_float("lineWidth", 0.5)
|
||||
self.draw_batch("LINES", indices, color, edges)
|
||||
@@ -230,9 +229,7 @@ class AggregateModeDecorator:
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_aggregate_empty, (context,), "WINDOW", "POST_VIEW")
|
||||
)
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_new_objects, (context,), "WINDOW", "POST_VIEW")
|
||||
)
|
||||
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_new_objects, (context,), "WINDOW", "POST_VIEW"))
|
||||
cls.is_installed = True
|
||||
|
||||
@classmethod
|
||||
@@ -352,12 +349,11 @@ class AggregateModeDecorator:
|
||||
new_objs = [o for o in new_objs if o.data]
|
||||
for obj in new_objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if (aggregate := ifcopenshell.util.element.get_aggregate(element)) == tool.Ifc.get_entity(props.editing_aggregate):
|
||||
if (aggregate := ifcopenshell.util.element.get_aggregate(element)) == tool.Ifc.get_entity(
|
||||
props.editing_aggregate
|
||||
):
|
||||
continue
|
||||
if element and element.is_a("IfcElement"):
|
||||
data = ItemDecorator.get_obj_data(obj)
|
||||
if data:
|
||||
self.draw_batch(
|
||||
"TRIS", data["verts"], transparent_color((1, 0, 0, 1)), data["tris"]
|
||||
)
|
||||
|
||||
self.draw_batch("TRIS", data["verts"], transparent_color((1, 0, 0, 1)), data["tris"])
|
||||
|
||||
@@ -375,6 +375,7 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator):
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
|
||||
bl_idname = "bim.disable_aggregate_mode"
|
||||
bl_label = "Disable Aggregate Mode"
|
||||
@@ -385,6 +386,7 @@ class BIM_OT_disable_aggregate_mode(bpy.types.Operator):
|
||||
bonsai.core.aggregate.disable_aggregate_mode(tool.Aggregate)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
|
||||
bl_idname = "bim.toggle_aggregate_mode_local_view"
|
||||
bl_label = "Toggle Aggregate Mode Local View"
|
||||
@@ -403,6 +405,7 @@ class BIM_OT_toggle_aggregate_mode_local_view(bpy.types.Operator):
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class BIM_OT_aggregate_assign_new_objects_in_aggregate_mode(bpy.types.Operator):
|
||||
bl_idname = "bim.aggregate_assign_new_objects_in_aggregate_mode"
|
||||
bl_label = "Aggregate Assign New Objects In Aggregate Mode"
|
||||
@@ -424,7 +427,9 @@ class BIM_OT_aggregate_assign_new_objects_in_aggregate_mode(bpy.types.Operator):
|
||||
new_objs = [o for o in new_objs if o.data]
|
||||
for obj in new_objs:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if (aggregate := ifcopenshell.util.element.get_aggregate(element)) == tool.Ifc.get_entity(props.editing_aggregate):
|
||||
if (aggregate := ifcopenshell.util.element.get_aggregate(element)) == tool.Ifc.get_entity(
|
||||
props.editing_aggregate
|
||||
):
|
||||
continue
|
||||
if element and element.is_a("IfcElement"):
|
||||
obj.select_set(True)
|
||||
@@ -439,4 +444,3 @@ class BIM_OT_aggregate_assign_new_objects_in_aggregate_mode(bpy.types.Operator):
|
||||
BIM_OT_aggregate_assign_object._execute(self, context)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -1858,7 +1858,7 @@ class OverrideEscape(bpy.types.Operator):
|
||||
bpy.ops.bim.hide_all_openings()
|
||||
elif context.scene.BIMAggregateProperties.in_aggregate_mode:
|
||||
bpy.ops.bim.disable_aggregate_mode()
|
||||
elif active_object:=context.active_object:
|
||||
elif active_object := context.active_object:
|
||||
if tool.Blender.Modifier.try_canceling_editing_modifier_parameters_or_path(active_object):
|
||||
pass
|
||||
return {"FINISHED"}
|
||||
@@ -1902,7 +1902,9 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly")
|
||||
elif obj in [o.obj for o in context.scene.BIMAggregateProperties.not_editing_objects]:
|
||||
obj.select_set(False)
|
||||
self.report({"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly")
|
||||
self.report(
|
||||
{"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly"
|
||||
)
|
||||
elif obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs:
|
||||
self.report({"ERROR"}, "Clipping planes cannot be edited")
|
||||
elif element:
|
||||
|
||||
@@ -25,7 +25,7 @@ import bmesh
|
||||
import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
import math
|
||||
from math import sin, cos, radians
|
||||
from math import sin, cos, radians
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
from mathutils import Vector, Matrix
|
||||
@@ -785,7 +785,7 @@ class ProductDecorator:
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
props = bpy.context.scene.BIMProductPreviewProperties # updated by model/polyline.py
|
||||
props = bpy.context.scene.BIMProductPreviewProperties # updated by model/polyline.py
|
||||
props.verts.clear()
|
||||
props.edges.clear()
|
||||
props.tris.clear()
|
||||
@@ -806,7 +806,7 @@ class ProductDecorator:
|
||||
props = context.scene.BIMProductPreviewProperties
|
||||
data = {}
|
||||
data["verts"] = [(*v.value_3d,) for v in props.verts]
|
||||
data["edges"] = [(int(e.value_2d[0]),int(e.value_2d[1])) for e in props.edges]
|
||||
data["edges"] = [(int(e.value_2d[0]), int(e.value_2d[1])) for e in props.edges]
|
||||
data["tris"] = [(int(t.value_3d[0]), int(t.value_3d[1]), int(t.value_3d[2])) for t in props.tris]
|
||||
return data
|
||||
|
||||
|
||||
@@ -200,6 +200,7 @@ def get_wall_preview_data(context, relating_type):
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_vertical_profile_preview_data(context, relating_type):
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
@@ -261,7 +262,7 @@ def get_vertical_profile_preview_data(context, relating_type):
|
||||
# Create extrusion bmesh
|
||||
bm = bmesh.new()
|
||||
|
||||
grouped_verts.append(grouped_verts[0]) # Close profile
|
||||
grouped_verts.append(grouped_verts[0]) # Close profile
|
||||
new_verts = [bm.verts.new(v) for v in grouped_verts]
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
|
||||
|
||||
@@ -296,11 +297,13 @@ def get_vertical_profile_preview_data(context, relating_type):
|
||||
# Add only profile edges
|
||||
edges = []
|
||||
for edge in bm.edges:
|
||||
if (edge.verts[0].co.z == min_z and edge.verts[1].co.z == min_z) or (edge.verts[0].co.z == max_z and edge.verts[1].co.z == max_z):
|
||||
if (edge.verts[0].co.z == min_z and edge.verts[1].co.z == min_z) or (
|
||||
edge.verts[0].co.z == max_z and edge.verts[1].co.z == max_z
|
||||
):
|
||||
edges.append(edge)
|
||||
# Add axis edge
|
||||
edges = [(edge.verts[0].index, edge.verts[1].index) for edge in edges]
|
||||
edges.append((len(verts)-1, len(verts)-2))
|
||||
edges.append((len(verts) - 1, len(verts) - 2))
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
@@ -327,7 +330,7 @@ def get_horizontal_profile_preview_data(context, relating_type):
|
||||
return
|
||||
for point in polyline_points:
|
||||
polyline_verts.append(Vector((point.x, point.y, point.z)))
|
||||
polyline_edges = [(i, i+1) for i in range(len(polyline_verts)-1)]
|
||||
polyline_edges = [(i, i + 1) for i in range(len(polyline_verts) - 1)]
|
||||
|
||||
# Get profile shape
|
||||
settings = ifcopenshell.geom.settings()
|
||||
@@ -372,26 +375,25 @@ def get_horizontal_profile_preview_data(context, relating_type):
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
|
||||
# Create profile curve
|
||||
scale_mat = Matrix.Scale(-1, 4, (1.0, 0.0, 0.0))
|
||||
grouped_verts = [scale_mat @ Vector(v) for v in grouped_verts]
|
||||
profile_curve = bpy.data.curves.new("Profile", type='CURVE')
|
||||
profile_curve = bpy.data.curves.new("Profile", type="CURVE")
|
||||
profile_curve.dimensions = "2D"
|
||||
profile_curve.splines.new('POLY')
|
||||
profile_curve.splines.new("POLY")
|
||||
profile_curve.splines[0].points.add(len(grouped_verts))
|
||||
|
||||
for i, point in enumerate(profile_curve.splines[0].points):
|
||||
if i == len(grouped_verts): # Close curve
|
||||
if i == len(grouped_verts): # Close curve
|
||||
point.co = Vector((*grouped_verts[0], 0))
|
||||
continue
|
||||
point.co = Vector((*grouped_verts[i], 0))
|
||||
profile_obj = bpy.data.objects.new("Profile", profile_curve)
|
||||
|
||||
# Create path curve with profile object as bevel
|
||||
path_curve = bpy.data.curves.new("Polyline", type='CURVE')
|
||||
path_curve = bpy.data.curves.new("Polyline", type="CURVE")
|
||||
path_curve.dimensions = "2D"
|
||||
path_curve.splines.new('POLY')
|
||||
path_curve.splines.new("POLY")
|
||||
path_curve.splines[0].points.add(len(polyline_verts) - 1)
|
||||
for i, point in enumerate(path_curve.splines[0].points):
|
||||
point.co = Vector((*polyline_verts[i], 0))
|
||||
@@ -400,12 +402,12 @@ def get_horizontal_profile_preview_data(context, relating_type):
|
||||
path_curve.bevel_object = profile_obj
|
||||
|
||||
# Convert path curve to mesh
|
||||
# This operation throws a warning when done during gpu drawing, so it was removed from the decorator file to be handled here
|
||||
# This operation throws a warning when done during gpu drawing, so it was removed from the decorator file to be handled here
|
||||
path_obj = bpy.data.objects.new("Preview", path_curve)
|
||||
context.scene.collection.objects.link(path_obj)
|
||||
bpy.context.view_layer.objects.active = path_obj
|
||||
dg = context.evaluated_depsgraph_get()
|
||||
path_obj = path_obj.evaluated_get(dg)
|
||||
path_obj = path_obj.evaluated_get(dg)
|
||||
me = path_obj.to_mesh()
|
||||
|
||||
# Create bmesh from path mesh
|
||||
@@ -812,13 +814,9 @@ class PolylineOperator:
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
def get_product_preview_data(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_isntance):
|
||||
if (
|
||||
tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() not in {"IfcColumnType"}
|
||||
):
|
||||
if tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() not in {"IfcColumnType"}:
|
||||
data = get_horizontal_profile_preview_data(context, relating_type)
|
||||
elif (
|
||||
tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() in {"IfcColumnType"}
|
||||
):
|
||||
elif tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() in {"IfcColumnType"}:
|
||||
data = get_vertical_profile_preview_data(context, relating_type)
|
||||
elif tool.Model.get_usage_type(relating_type) == "LAYER2":
|
||||
data = get_wall_preview_data(context, relating_type)
|
||||
@@ -832,7 +830,7 @@ class PolylineOperator:
|
||||
props.verts.clear()
|
||||
props.edges.clear()
|
||||
props.tris.clear()
|
||||
|
||||
|
||||
for vert in data["verts"]:
|
||||
v = props.verts.add()
|
||||
v.value_3d = vert
|
||||
|
||||
@@ -1145,7 +1145,6 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator):
|
||||
for profile1, profile2 in zip(profiles[:-1], profiles[1:]):
|
||||
DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"])
|
||||
|
||||
|
||||
def modal(self, context, event):
|
||||
if not self.relating_type:
|
||||
self.report({"WARNING"}, "You need to select a profile type.")
|
||||
@@ -1176,7 +1175,6 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator):
|
||||
size = len(items)
|
||||
context.scene.BIMModelProperties.offset_type = items[((index + 1) % size)]
|
||||
|
||||
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
wall_config = f"""Direction: {props.direction_sense}
|
||||
Offset Type: {props.offset_type}
|
||||
@@ -1222,4 +1220,3 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator):
|
||||
self.tool_state.use_default_container = True
|
||||
self.tool_state.plane_method = "XY"
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
@@ -821,12 +821,13 @@ class BIMPolylineProperties(PropertyGroup):
|
||||
insertion_polyline: bpy.props.CollectionProperty(type=Polyline)
|
||||
measurement_polyline: bpy.props.CollectionProperty(type=Polyline)
|
||||
|
||||
|
||||
class ProductPreviewItem(PropertyGroup):
|
||||
value_3d: bpy.props.FloatVectorProperty()
|
||||
value_2d: bpy.props.FloatVectorProperty(size=2)
|
||||
|
||||
|
||||
class BIMProductPreviewProperties(PropertyGroup):
|
||||
verts: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
edges: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
tris: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
|
||||
|
||||
@@ -591,7 +591,7 @@ class EditObjectUI:
|
||||
op = row.operator("bim.disable_aggregate_mode", text="", icon="X")
|
||||
op = row.operator("bim.toggle_aggregate_mode_local_view", text="", icon="ZOOM_SELECTED")
|
||||
op = row.operator("bim.aggregate_assign_new_objects_in_aggregate_mode", text="", icon="CUBE")
|
||||
|
||||
|
||||
text = format_ifc_camel_case(AuthoringData.data["active_class"])
|
||||
layout.label(text=f"{text} Edit Tools:", icon="RESTRICT_SELECT_OFF")
|
||||
cls.draw_parameter_adjustments(context)
|
||||
@@ -954,7 +954,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
def hotkey_S_A(self):
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
relating_type_id = AuthoringData.data["relating_type_id_current"]
|
||||
relating_type_class = AuthoringData.data["ifc_class_current"]
|
||||
relating_type_class = AuthoringData.data["ifc_class_current"]
|
||||
if relating_type_id is None:
|
||||
self.report({"ERROR"}, "No relating type selected")
|
||||
return
|
||||
@@ -993,7 +993,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
):
|
||||
bpy.ops.bim.draw_polyline_slab("INVOKE_DEFAULT")
|
||||
elif (
|
||||
relating_type_id and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "PROFILE" and relating_type_class not in {"IfcColumnType"}
|
||||
relating_type_id
|
||||
and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "PROFILE"
|
||||
and relating_type_class not in {"IfcColumnType"}
|
||||
):
|
||||
bpy.ops.bim.draw_polyline_profile("INVOKE_DEFAULT")
|
||||
else:
|
||||
|
||||
@@ -197,7 +197,9 @@ def draw_psetqto_ui(
|
||||
row = box.row(align=True)
|
||||
row.scale_y = 0.8
|
||||
row.label(text=prop["Name"])
|
||||
op = row.operator("bim.select_similar", text=get_display_value(nominal_value), icon="NONE", emboss=False)
|
||||
op = row.operator(
|
||||
"bim.select_similar", text=get_display_value(nominal_value), icon="NONE", emboss=False
|
||||
)
|
||||
op.key = '"' + pset["Name"].replace('"', '\\"') + '"."' + prop["Name"].replace('"', '\\"') + '"'
|
||||
# calculate sum of all selected objects
|
||||
if active_operator:
|
||||
|
||||
@@ -246,9 +246,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
)
|
||||
should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False)
|
||||
spatial_elements_unselectable: BoolProperty(
|
||||
name="Make Spatial Elements Unselectable By Default",
|
||||
default=True,
|
||||
description="If disabled, it will be possible to select spatial elements in the 3D viewport.\nIt is recommended to keep this `Enabled`, as this can have unintended consequences")
|
||||
name="Make Spatial Elements Unselectable By Default",
|
||||
default=True,
|
||||
description="If disabled, it will be possible to select spatial elements in the 3D viewport.\nIt is recommended to keep this `Enabled`, as this can have unintended consequences",
|
||||
)
|
||||
decorations_colour: bpy.props.FloatVectorProperty(
|
||||
name="Decorations Color", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
|
||||
)
|
||||
@@ -1191,15 +1192,16 @@ def draw_custom_context_menu(self: bpy.types.Menu, context: bpy.types.Context) -
|
||||
url_op = layout.operator("bim.open_uri", icon="URL", text="Online IFC Documentation")
|
||||
url_op.uri = url
|
||||
|
||||
|
||||
class BIM_PT_decorators_overlay(Panel):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'HEADER'
|
||||
bl_parent_id = 'VIEW3D_PT_overlay'
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "HEADER"
|
||||
bl_parent_id = "VIEW3D_PT_overlay"
|
||||
bl_label = "Bonsai Decorators"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode == 'OBJECT'
|
||||
return context.mode == "OBJECT"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
@@ -86,12 +86,17 @@ def add_part_to_object(
|
||||
assign_object(ifc, aggregator, collector, relating_obj=obj, related_obj=part_obj)
|
||||
blender.set_active_object(obj)
|
||||
|
||||
def enable_aggregate_mode(aggregator: tool.Aggregate, obj: bpy.types.Object,):
|
||||
|
||||
def enable_aggregate_mode(
|
||||
aggregator: tool.Aggregate,
|
||||
obj: bpy.types.Object,
|
||||
):
|
||||
if aggregator.get_aggregate_mode():
|
||||
disable_aggregate_mode(aggregator)
|
||||
|
||||
aggregator.enable_aggregate_mode(obj)
|
||||
|
||||
|
||||
def disable_aggregate_mode(aggregator: tool.Aggregate):
|
||||
aggregator.disable_aggregate_mode()
|
||||
|
||||
|
||||
@@ -1795,9 +1795,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_bvh_tree(cls, obj:bpy.types.Object) -> BVHTree:
|
||||
def get_bvh_tree(cls, obj: bpy.types.Object) -> BVHTree:
|
||||
bm = tool.Blender.get_bmesh_for_mesh(obj.data)
|
||||
bm.transform(obj.matrix_world)
|
||||
return BVHTree.FromBMesh(bm)
|
||||
|
||||
|
||||
@@ -983,8 +983,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
tool.Loader.load_indexed_colour_map(rep, mesh)
|
||||
|
||||
ios_edges_values = [
|
||||
(e.vertices[0], e.vertices[1]) in ios_edges
|
||||
or (e.vertices[1], e.vertices[0]) in ios_edges
|
||||
(e.vertices[0], e.vertices[1]) in ios_edges or (e.vertices[1], e.vertices[0]) in ios_edges
|
||||
for e in mesh.edges
|
||||
]
|
||||
tool.Blender.Attribute.fill_attribute(mesh, "ios_edges", "EDGE", "BOOLEAN", ios_edges_values)
|
||||
|
||||
@@ -206,7 +206,7 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
|
||||
bm.free()
|
||||
snapping_points = []
|
||||
sorted_points = sorted(points, key=lambda x : x[0])
|
||||
sorted_points = sorted(points, key=lambda x: x[0])
|
||||
for p in sorted_points:
|
||||
point = copy.deepcopy(p)
|
||||
snapping_points.append(point[1])
|
||||
|
||||
@@ -354,7 +354,6 @@ class Root(bonsai.core.tool.Root):
|
||||
tool.Aggregate.constrain_all_parts_to_aggregate(tool.Ifc.get_object(new_aggregate[0]))
|
||||
tool.Blender.select_and_activate_single_object(bpy.context, tool.Ifc.get_object(new_aggregate[0]))
|
||||
|
||||
|
||||
@classmethod
|
||||
def run_geometry_add_representation(
|
||||
cls,
|
||||
|
||||
Reference in New Issue
Block a user