Tueren-Schwung: Live-Update + Hinge-Position auf Wand-Innenkante
- Schwung-Curves-Block VOR dem Volume-Replace-Pfad — sonst greifen openAngle/hingeSide/swingInvert-Aenderungen nicht (Volume-Anzahl bleibt gleich → continue → Swing-Skip). - Hinge-Punkt sitzt nicht mehr auf der Wand-Achse sondern auf der Wand-Innenkante (half_d * inside-Richtung). Tuerblatt + Bogen beginnen damit an der Wandflucht statt in der Wandmitte. - Rotation-Vorzeichen korrigiert (rad = open_angle * sweep_sign * aus_sign) damit Tuere wirklich ins Innere schwingt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+216
-5
@@ -67,6 +67,13 @@ _KEY_AKTIVE_DARSTELLUNG = "dossier_aktive_darstellung" # doc-level global overr
|
||||
_KEY_OEFF_STYLE_ID = "dossier_oeff_style_id" # per-Object: referenziert einen Style aus dossier_oeff_styles
|
||||
_KEY_OEFF_STYLES = "dossier_oeff_styles" # JSON-Liste aller Fenster/Tueren-Styles
|
||||
_KEY_OEFF_STYLE_ACTIVE = "dossier_oeff_style_active" # zuletzt benutzte Style-ID (pro typ)
|
||||
_KEY_OEFF_TUER_TYP = "dossier_oeff_tuer_typ" # "normal" | "wandoeffnung"
|
||||
_KEY_OEFF_HINGE_SIDE = "dossier_oeff_hinge_side" # "links" | "rechts" — Bandseite (welche Tuerflueg-Seite)
|
||||
_KEY_OEFF_OPEN_ANGLE = "dossier_oeff_open_angle" # float Grad 0–180 (Plan-Oeffnungswinkel)
|
||||
_KEY_OEFF_SWING_INVERT = "dossier_oeff_swing_invert" # "1"/"0" — flippt die Schwung-Richtung
|
||||
|
||||
_OEFF_TUER_TYPEN = ("normal", "wandoeffnung")
|
||||
_OEFF_HINGE_SIDES = ("links", "rechts")
|
||||
|
||||
_OEFF_DARSTELLUNGEN = ("einfach", "standard", "detail")
|
||||
|
||||
@@ -101,6 +108,7 @@ _OEFF_STYLE_FIELDS = (
|
||||
"rahmenB", "rahmenTiefe", "rahmenOffset",
|
||||
"fluegel", "simsAus", "glas",
|
||||
"darstellung", "tuerRahmen",
|
||||
"tuerTyp", "hingeSide", "openAngle",
|
||||
)
|
||||
|
||||
_OEFF_DEFAULT_STYLES = [
|
||||
@@ -133,7 +141,14 @@ _OEFF_DEFAULT_STYLES = [
|
||||
"breite": 0.90, "hoehe": 2.10, "brueest": 0.00,
|
||||
"rahmenB": 0.06, "rahmenTiefe": 0.08, "rahmenOffset": 0.05,
|
||||
"fluegel": 1, "simsAus": "ohne", "glas": True,
|
||||
"darstellung": "standard", "tuerRahmen": "zarge"},
|
||||
"darstellung": "standard", "tuerRahmen": "zarge",
|
||||
"tuerTyp": "normal", "hingeSide": "links", "openAngle": 90.0},
|
||||
{"name": "Wandöffnung", "typ": "tuer",
|
||||
"breite": 1.20, "hoehe": 2.10, "brueest": 0.00,
|
||||
"rahmenB": 0.04, "rahmenTiefe": 0.04, "rahmenOffset": 0.05,
|
||||
"fluegel": 1, "simsAus": "ohne", "glas": False,
|
||||
"darstellung": "standard", "tuerRahmen": "zarge",
|
||||
"tuerTyp": "wandoeffnung", "hingeSide": "links", "openAngle": 0.0},
|
||||
]
|
||||
|
||||
|
||||
@@ -599,6 +614,15 @@ def _layer_path_volume(doc, geschoss_name):
|
||||
return _layer_path_axis(doc, geschoss_name)
|
||||
|
||||
|
||||
def _layer_path_oeff_swing(doc, geschoss_name):
|
||||
"""Türschwung-Linien — eigener Sublayer (Code 23) damit User die
|
||||
Schwung-Bögen unabhängig von den Türen-Volumes ausblenden kann."""
|
||||
sub = _find_ebene_sublayer_name(doc, ["schwung", "tuerschwung"],
|
||||
"23", "Türschwung",
|
||||
default_color="#888888", default_lw=0.13)
|
||||
return "{}::{}".format(geschoss_name, sub)
|
||||
|
||||
|
||||
def _layer_path_decke(doc, geschoss_name):
|
||||
"""Decken-Outline + Volumen — Sublayer 'DECKEN' (Code 30)."""
|
||||
sub = _find_ebene_sublayer_name(doc, ["decke"], "30", "DECKEN",
|
||||
@@ -1884,6 +1908,8 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
|
||||
oeff_referenz=None, oeff_darstellung=None,
|
||||
oeff_aussenseite=None, oeff_tuer_rahmen=None,
|
||||
oeff_rahmen_offset=None, oeff_style_id=None,
|
||||
oeff_tuer_typ=None, oeff_hinge_side=None, oeff_open_angle=None,
|
||||
oeff_swing_invert=None,
|
||||
geschoss_end=None, treppe_breite=None,
|
||||
treppe_n=None, treppe_referenz=None,
|
||||
treppe_modus=None, treppe_lauf_d=None, treppe_art=None,
|
||||
@@ -1967,6 +1993,19 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over,
|
||||
if oeff_style_id is not None:
|
||||
try: obj_attrs.SetUserString(_KEY_OEFF_STYLE_ID, str(oeff_style_id) or "")
|
||||
except Exception: pass
|
||||
if oeff_tuer_typ is not None and oeff_tuer_typ in _OEFF_TUER_TYPEN:
|
||||
obj_attrs.SetUserString(_KEY_OEFF_TUER_TYP, oeff_tuer_typ)
|
||||
if oeff_hinge_side is not None and oeff_hinge_side in _OEFF_HINGE_SIDES:
|
||||
obj_attrs.SetUserString(_KEY_OEFF_HINGE_SIDE, oeff_hinge_side)
|
||||
if oeff_open_angle is not None:
|
||||
try:
|
||||
v = max(0.0, min(180.0, float(oeff_open_angle)))
|
||||
obj_attrs.SetUserString(_KEY_OEFF_OPEN_ANGLE, "{:.2f}".format(v))
|
||||
except Exception: pass
|
||||
if oeff_swing_invert is not None:
|
||||
try: obj_attrs.SetUserString(_KEY_OEFF_SWING_INVERT,
|
||||
"1" if bool(oeff_swing_invert) else "0")
|
||||
except Exception: pass
|
||||
# --- Treppen-Felder ---
|
||||
if geschoss_end is not None:
|
||||
obj_attrs.SetUserString(_KEY_GESCHOSS_END, geschoss_end or "")
|
||||
@@ -2130,6 +2169,15 @@ def _read_meta(obj):
|
||||
except Exception: oro = 0.05
|
||||
if oro < 0: oro = 0.0
|
||||
ostyle = a.GetUserString(_KEY_OEFF_STYLE_ID) or ""
|
||||
ottyp = a.GetUserString(_KEY_OEFF_TUER_TYP) or "normal"
|
||||
if ottyp not in _OEFF_TUER_TYPEN: ottyp = "normal"
|
||||
ohinge = a.GetUserString(_KEY_OEFF_HINGE_SIDE) or "links"
|
||||
if ohinge not in _OEFF_HINGE_SIDES: ohinge = "links"
|
||||
try: oangle = float(a.GetUserString(_KEY_OEFF_OPEN_ANGLE) or "90")
|
||||
except Exception: oangle = 90.0
|
||||
if oangle < 0: oangle = 0.0
|
||||
elif oangle > 180: oangle = 180.0
|
||||
oswinv = (a.GetUserString(_KEY_OEFF_SWING_INVERT) == "1")
|
||||
# Treppen-Felder
|
||||
gend = a.GetUserString(_KEY_GESCHOSS_END) or ""
|
||||
try: tb = float(a.GetUserString(_KEY_TREPPE_BREITE) or "1.0")
|
||||
@@ -2251,6 +2299,10 @@ def _read_meta(obj):
|
||||
"oeff_tuer_rahmen": otrah,
|
||||
"oeff_rahmen_offset": oro,
|
||||
"oeff_style_id": ostyle,
|
||||
"oeff_tuer_typ": ottyp,
|
||||
"oeff_hinge_side": ohinge,
|
||||
"oeff_open_angle": oangle,
|
||||
"oeff_swing_invert": oswinv,
|
||||
"geschoss_end": gend,
|
||||
"treppe_breite": tb,
|
||||
"treppe_n": tn,
|
||||
@@ -2471,6 +2523,109 @@ def _resolve_rahmen_perp_range(half_d, rahmen_tiefe, rahmen_pos):
|
||||
return lo, hi
|
||||
|
||||
|
||||
def _make_tuer_swing_curves(axis_curve, point_on_axis, wall_dicke,
|
||||
oeff_meta, base_z):
|
||||
"""Generiert die 2D-Plan-Schwung-Linien einer Tuer:
|
||||
- 1 Linie: Tuerblatt im geoeffneten Zustand (vom Scharnier um
|
||||
open_angle gedreht)
|
||||
- 1 Arc: Schwung-Bogen vom geschlossenen Endpunkt zum offenen
|
||||
Endpunkt
|
||||
Alle Kurven liegen auf Floor-Level (z = base_z).
|
||||
|
||||
Returns [Curve] oder leere Liste wenn Tuer-Typ='wandoeffnung'
|
||||
oder Oeffnung keine Tuer ist."""
|
||||
import math
|
||||
if oeff_meta.get("oeff_typ") != "tuer": return []
|
||||
if oeff_meta.get("oeff_tuer_typ", "normal") != "normal": return []
|
||||
frame = _oeff_axis_frame(axis_curve, point_on_axis)
|
||||
if frame is None: return []
|
||||
pt, tan, perp = frame
|
||||
breite = float(oeff_meta.get("oeff_breite", 0.9))
|
||||
rahmen_b = float(oeff_meta.get("oeff_rahmen_b", 0.06))
|
||||
darstellung = oeff_meta.get("oeff_darstellung", "standard")
|
||||
aussenseite = oeff_meta.get("oeff_aussenseite", "rechts")
|
||||
aus_sign = +1 if aussenseite == "rechts" else -1
|
||||
hinge_side = oeff_meta.get("oeff_hinge_side", "links")
|
||||
try: open_angle = float(oeff_meta.get("oeff_open_angle", 90))
|
||||
except Exception: open_angle = 90.0
|
||||
if open_angle <= 0: return [] # tuer geschlossen — keine Schwung-Curves
|
||||
swing_invert = bool(oeff_meta.get("oeff_swing_invert", False))
|
||||
|
||||
half_b = breite * 0.5
|
||||
# Lichte-Breite je nach Darstellung: einfach = volle Breite,
|
||||
# standard/detail = Innenmass abzueglich Rahmen
|
||||
if darstellung == "einfach":
|
||||
leaf_t_lo, leaf_t_hi = -half_b, +half_b
|
||||
else:
|
||||
leaf_t_lo, leaf_t_hi = -half_b + rahmen_b, +half_b - rahmen_b
|
||||
leaf_len = leaf_t_hi - leaf_t_lo
|
||||
if leaf_len <= 1e-6: return []
|
||||
|
||||
# Scharnier-Tangentenkoordinate (along axis)
|
||||
if hinge_side == "links":
|
||||
hinge_t = leaf_t_lo
|
||||
# Schwung-Richtung: vom Scharnier nach aussen → Aussenseite-Vorzeichen
|
||||
# Linke Bandseite: Tuer schwingt entlang +tan-Richtung im geschlossenen
|
||||
# Zustand, perpendikulaer ins Innere (= -aus_sign perp) im offenen.
|
||||
sweep_sign = -1 # gegen Uhrzeigersinn um Scharnier wenn aus +Z gesehen
|
||||
else:
|
||||
hinge_t = leaf_t_hi
|
||||
sweep_sign = +1
|
||||
|
||||
# Hinge-Punkt: sitzt auf der WAND-INNENKANTE (nicht auf der Achse).
|
||||
# Innen-Richtung: aussenseite='rechts' (aus_sign=+1) bedeutet aussen
|
||||
# liegt in box-perp-Richtung (tan.Y, -tan.X). Innen ist dann das
|
||||
# Gegenstueck (-tan.Y, +tan.X). aus_sign=-1 flippt das.
|
||||
half_d = float(wall_dicke) * 0.5
|
||||
inside_x = aus_sign * (-tan.Y)
|
||||
inside_y = aus_sign * tan.X
|
||||
z0 = float(base_z)
|
||||
hinge_pt = rg.Point3d(
|
||||
pt.X + hinge_t * tan.X + half_d * inside_x,
|
||||
pt.Y + hinge_t * tan.Y + half_d * inside_y,
|
||||
z0)
|
||||
# Geschlossener Endpunkt (Tuer-Blatt in Wand-Flucht, parallel zur Achse):
|
||||
closed_pt = rg.Point3d(
|
||||
pt.X + (hinge_t + sweep_sign * leaf_len) * tan.X + half_d * inside_x,
|
||||
pt.Y + (hinge_t + sweep_sign * leaf_len) * tan.Y + half_d * inside_y,
|
||||
z0)
|
||||
# Offener Endpunkt: hinge + leaf_len * direction(sweep_angle)
|
||||
# Direction startet bei +sweep_sign * tan (geschlossen) und rotiert
|
||||
# um Z-Achse um sweep_sign * (-aus_sign) * open_angle Grad — Tuer
|
||||
# schwingt in den Innenraum (-aus_sign * perp).
|
||||
rad = math.radians(open_angle) * sweep_sign * aus_sign
|
||||
if swing_invert: rad = -rad
|
||||
cos_r, sin_r = math.cos(rad), math.sin(rad)
|
||||
base_dx = sweep_sign * tan.X
|
||||
base_dy = sweep_sign * tan.Y
|
||||
open_dx = base_dx * cos_r - base_dy * sin_r
|
||||
open_dy = base_dx * sin_r + base_dy * cos_r
|
||||
open_pt = rg.Point3d(hinge_pt.X + open_dx * leaf_len,
|
||||
hinge_pt.Y + open_dy * leaf_len, z0)
|
||||
|
||||
curves = []
|
||||
# 1) Tuerblatt-Linie (Hinge → Open)
|
||||
try:
|
||||
leaf_line = rg.LineCurve(hinge_pt, open_pt)
|
||||
curves.append(leaf_line)
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] swing leaf:", ex)
|
||||
# 2) Schwung-Arc — Mittelpunkt = Scharnier, Radius = Tuerblatt-Laenge.
|
||||
# Plane mit X→closed_pt + Y→open_pt; Arc(plane, radius, angle) startet
|
||||
# auf der X-Achse und sweept CCW um angle Radian. Damit liegt der
|
||||
# Bogen genau auf einem Kreis um das Scharnier.
|
||||
try:
|
||||
dir_closed = closed_pt - hinge_pt
|
||||
dir_open = open_pt - hinge_pt
|
||||
arc_plane = rg.Plane(hinge_pt, dir_closed, dir_open)
|
||||
arc = rg.Arc(arc_plane, leaf_len, math.radians(open_angle))
|
||||
if arc.IsValid:
|
||||
curves.append(rg.ArcCurve(arc))
|
||||
except Exception as ex:
|
||||
print("[ELEMENTE] swing arc:", ex)
|
||||
return curves
|
||||
|
||||
|
||||
def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base_z):
|
||||
"""Baut die einzelnen Brep-Pieces der Oeffnung — Rahmen (single Brep
|
||||
via Boolean-Differenz), Mittelpfosten (pro Fluegel), Glas, Sims aussen,
|
||||
@@ -2680,7 +2835,7 @@ SOURCE_TYPES = ("wand_axis", "decke_outline", "dach_outline",
|
||||
"stuetze_point", "traeger_axis",
|
||||
"raum_outline", "decke_aussparung_outline")
|
||||
VOLUME_TYPES = ("wand_volume", "decke_volume", "dach_volume",
|
||||
"oeffnung_volume", "treppe_volume",
|
||||
"oeffnung_volume", "oeffnung_swing", "treppe_volume",
|
||||
"stuetze_volume", "traeger_volume",
|
||||
"raum_stamp", "raum_fill")
|
||||
# Oeffnungs-Cutout: Boolean-Difference aus Wand. Zusaetzlich kriegt die
|
||||
@@ -4458,6 +4613,30 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
||||
# Aenderung (z.B. Fluegel-Wechsel) Fallback auf Delete+Add.
|
||||
op_layer = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name))
|
||||
for op_meta, pt_loc, op_uk in opening_jobs:
|
||||
# Schwung-Curves IMMER zuerst regenerieren — unabhaengig davon
|
||||
# ob die Volume-Pieces sich geaendert haben. Andernfalls greifen
|
||||
# Aenderungen an openAngle/hingeSide/swingInvert nicht weil der
|
||||
# Replace-Pfad weiter unten via `continue` rausspringt.
|
||||
if op_meta.get("oeff_typ") == "tuer":
|
||||
old_sw = list(_find_objects_by_wall_id(doc, op_meta["id"],
|
||||
"oeffnung_swing"))
|
||||
for o, _m in old_sw:
|
||||
try: doc.Objects.Delete(o.Id, True)
|
||||
except Exception: pass
|
||||
swings = _make_tuer_swing_curves(geom, pt_loc, meta["dicke"],
|
||||
op_meta, op_uk)
|
||||
if swings:
|
||||
sw_layer_idx = _ensure_layer(doc,
|
||||
_layer_path_oeff_swing(doc, geschoss_name))
|
||||
for crv in swings:
|
||||
sw_attrs = Rhino.DocObjects.ObjectAttributes()
|
||||
sw_attrs.LayerIndex = sw_layer_idx
|
||||
_attach_meta(sw_attrs, op_meta["id"], "oeffnung_swing",
|
||||
op_meta["geschoss"], meta["dicke"], "", "",
|
||||
oeff_typ="tuer",
|
||||
oeff_parent=op_meta.get("oeff_parent"))
|
||||
doc.Objects.AddCurve(crv, sw_attrs)
|
||||
|
||||
old_objs = list(_find_objects_by_wall_id(doc, op_meta["id"],
|
||||
"oeffnung_volume"))
|
||||
pieces = _make_oeffnung_pieces(geom, pt_loc, meta["dicke"],
|
||||
@@ -4494,7 +4673,11 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
||||
oeff_aussenseite=op_meta.get("oeff_aussenseite"),
|
||||
oeff_tuer_rahmen=op_meta.get("oeff_tuer_rahmen"),
|
||||
oeff_rahmen_offset=op_meta.get("oeff_rahmen_offset"),
|
||||
oeff_style_id=op_meta.get("oeff_style_id"))
|
||||
oeff_style_id=op_meta.get("oeff_style_id"),
|
||||
oeff_tuer_typ=op_meta.get("oeff_tuer_typ"),
|
||||
oeff_hinge_side=op_meta.get("oeff_hinge_side"),
|
||||
oeff_open_angle=op_meta.get("oeff_open_angle"),
|
||||
oeff_swing_invert=op_meta.get("oeff_swing_invert"))
|
||||
doc.Objects.AddBrep(pbrep, op_attrs)
|
||||
|
||||
# Source-Layer migrieren + Volumen-Layer-Index ermitteln
|
||||
@@ -5107,6 +5290,10 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
"tuerRahmen": meta.get("oeff_tuer_rahmen", "zarge"),
|
||||
"rahmenOffset": meta.get("oeff_rahmen_offset", 0.05),
|
||||
"styleId": meta.get("oeff_style_id", ""),
|
||||
"tuerTyp": meta.get("oeff_tuer_typ", "normal"),
|
||||
"hingeSide": meta.get("oeff_hinge_side", "links"),
|
||||
"openAngle": meta.get("oeff_open_angle", 90.0),
|
||||
"swingInvert": bool(meta.get("oeff_swing_invert", False)),
|
||||
})
|
||||
elif meta["type"] == "treppe_axis":
|
||||
gs = _geschoss_by_id(doc, meta["geschoss"])
|
||||
@@ -6174,7 +6361,10 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
oeff_aussenseite=detected_aussen,
|
||||
oeff_darstellung=darst_def,
|
||||
oeff_tuer_rahmen=tuer_rahmen_def,
|
||||
oeff_style_id=pending_sid)
|
||||
oeff_style_id=pending_sid,
|
||||
oeff_tuer_typ="normal",
|
||||
oeff_hinge_side="links",
|
||||
oeff_open_angle=90.0)
|
||||
# Oeffnungs-Punkt auf UK+Brueestung-Hoehe platzieren (= visuell auf
|
||||
# Unterkante Oeffnung). Constraint vergleicht spaeter pt.Z mit
|
||||
# UK+brueest — wenn der Punkt am axis.Z=0 saesse, wuerde der erste
|
||||
@@ -8614,6 +8804,17 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
# Stil das eben gemachte Field-Edit wegmaecken).
|
||||
o_style_id = p.get("styleId",
|
||||
old_meta.get("oeff_style_id", "")) or ""
|
||||
# Tueren-Schwung-Felder
|
||||
o_tuer_typ = p.get("tuerTyp", old_meta.get("oeff_tuer_typ", "normal"))
|
||||
if o_tuer_typ not in _OEFF_TUER_TYPEN: o_tuer_typ = "normal"
|
||||
o_hinge = p.get("hingeSide", old_meta.get("oeff_hinge_side", "links"))
|
||||
if o_hinge not in _OEFF_HINGE_SIDES: o_hinge = "links"
|
||||
try: o_angle = float(p.get("openAngle",
|
||||
old_meta.get("oeff_open_angle", 90)))
|
||||
except Exception: o_angle = 90.0
|
||||
o_angle = max(0.0, min(180.0, o_angle))
|
||||
o_swinv = bool(p.get("swingInvert",
|
||||
old_meta.get("oeff_swing_invert", False)))
|
||||
if p.get("styleId") and p.get("styleId") != old_meta.get("oeff_style_id"):
|
||||
# Style wurde NEU gewaehlt → seine Werte applizieren
|
||||
styles = list_oeff_styles(doc)
|
||||
@@ -8632,6 +8833,12 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
if "darstellung" in stl: odarst = stl["darstellung"]
|
||||
if otyp == "tuer" and "tuerRahmen" in stl:
|
||||
otrah = stl["tuerRahmen"]
|
||||
if otyp == "tuer" and "tuerTyp" in stl:
|
||||
o_tuer_typ = stl["tuerTyp"]
|
||||
if otyp == "tuer" and "hingeSide" in stl:
|
||||
o_hinge = stl["hingeSide"]
|
||||
if otyp == "tuer" and "openAngle" in stl:
|
||||
o_angle = float(stl["openAngle"])
|
||||
set_active_oeff_style_id(doc, otyp, p["styleId"])
|
||||
attrs = axis_obj.Attributes
|
||||
_attach_meta(attrs, wall_id, "oeffnung_point",
|
||||
@@ -8652,7 +8859,11 @@ class ElementeBridge(panel_base.BaseBridge):
|
||||
oeff_aussenseite=oauss,
|
||||
oeff_tuer_rahmen=otrah,
|
||||
oeff_rahmen_offset=oro,
|
||||
oeff_style_id=o_style_id)
|
||||
oeff_style_id=o_style_id,
|
||||
oeff_tuer_typ=o_tuer_typ,
|
||||
oeff_hinge_side=o_hinge,
|
||||
oeff_open_angle=o_angle,
|
||||
oeff_swing_invert=o_swinv)
|
||||
axis_obj.Attributes = attrs
|
||||
axis_obj.CommitChanges()
|
||||
parent_id = old_meta.get("oeff_parent", "")
|
||||
|
||||
Reference in New Issue
Block a user