Dach im Vertikalschnitt: analytischer TS-Schnitt inkl. Schicht-Bändern
toSection behandelte Dächer bislang gar nicht (der Rust-Extraktor kennt nur Wände + Decken). appendRoofSections schneidet die Schnittebene jetzt TS-seitig mit den Dachflächen: Grundriss-Spur der Ebene per Cyrus–Beck gegen jedes konvexe Aufsichts-Polygon der Dachflächen geclippt → u-Intervall; Oberkante v(u) linear aus der Ebenengleichung (Newell), Unterkante um lotrechte Dicke / cos(Neigung) tiefer. Mehrschichtige Dächer (roofLayers) werden wie beim Decken-Schnitt in Bänder aussen→innen zerlegt, jedes mit Bauteil-Schraffur (Papier-Neutralisierung wie resolveCeilingSectionStyle). SectionComponentRef um 'roof' erweitert; attachCutStyles reicht Dach-Bänder unverändert durch (Stile vorab aufgelöst, keine Boolean-Dominanz). Bewusst nur SCHNITT-Polygone — Ansichts-/Silhouettenkanten des Dachs sind eine eigene Phase. +4 analytische Tests. 689/689 grün.
This commit is contained in:
@@ -0,0 +1,144 @@
|
|||||||
|
// Dach im Vertikalschnitt: appendRoofSections schneidet die Schnittebene
|
||||||
|
// analytisch mit den Dachflächen (der Rust-Extraktor kennt nur Wände/Decken).
|
||||||
|
// Geprüft werden u-Intervalle, Oberkanten-Verlauf (linear je Fläche), die
|
||||||
|
// vertikale Dicke (lotrechte Dicke / cos(Neigung)) und die Schicht-Bänder.
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { appendRoofSections } from "./toSection";
|
||||||
|
import type { SectionOutput, SectionPlaneSpec } from "./toSection";
|
||||||
|
import type { Project, Roof } from "../model/types";
|
||||||
|
|
||||||
|
const RECT: Roof["outline"] = [
|
||||||
|
{ x: -3, y: 0 },
|
||||||
|
{ x: 3, y: 0 },
|
||||||
|
{ x: 3, y: 4 },
|
||||||
|
{ x: -3, y: 4 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function roof(over: Partial<Roof> = {}): Roof {
|
||||||
|
return {
|
||||||
|
id: "R1",
|
||||||
|
type: "roof",
|
||||||
|
floorId: "eg",
|
||||||
|
categoryCode: "35",
|
||||||
|
outline: RECT,
|
||||||
|
shape: "sattel",
|
||||||
|
pitchDeg: 30,
|
||||||
|
overhang: 0,
|
||||||
|
ridgeAxis: "x",
|
||||||
|
baseElevation: 5, // explizit — kein Geschoss-Lookup nötig
|
||||||
|
thickness: 0.3,
|
||||||
|
...over,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function project(r: Roof, extra: Partial<Project> = {}): Project {
|
||||||
|
return {
|
||||||
|
id: "t",
|
||||||
|
name: "T",
|
||||||
|
lineStyles: [],
|
||||||
|
hatches: [],
|
||||||
|
components: [],
|
||||||
|
wallTypes: [],
|
||||||
|
drawingLevels: [],
|
||||||
|
layers: [],
|
||||||
|
walls: [],
|
||||||
|
doors: [],
|
||||||
|
roofs: [r],
|
||||||
|
...extra,
|
||||||
|
} as unknown as Project;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Schnittebene: Blick entlang +x, Spur = Modell-Gerade x=0 (u wächst mit y). */
|
||||||
|
const PLANE: SectionPlaneSpec = { point: [0, 0, 0], normal: [1, 0, 0] };
|
||||||
|
|
||||||
|
const emptyOutput = (): SectionOutput => ({ cutPolygons: [], visibleEdges: [], hiddenEdges: [] });
|
||||||
|
|
||||||
|
describe("appendRoofSections", () => {
|
||||||
|
it("Flachdach: EIN Rechteck-Band, Oberkante = Traufhöhe, Höhe = Dicke", () => {
|
||||||
|
const out = emptyOutput();
|
||||||
|
appendRoofSections(out, project(roof({ shape: "flach", pitchDeg: 0 })), PLANE);
|
||||||
|
expect(out.cutPolygons.length).toBe(1);
|
||||||
|
const cp = out.cutPolygons[0];
|
||||||
|
expect(cp.component).toEqual({ kind: "roof", index: 0 });
|
||||||
|
// u-Intervall = Tiefe 0..4; Oberkante 5, Unterkante 5 − 0.3 (cosθ = 1).
|
||||||
|
const us = cp.pts.map((p) => p[0]).sort((a, b) => a - b);
|
||||||
|
expect(us[0]).toBeCloseTo(0, 6);
|
||||||
|
expect(us[3]).toBeCloseTo(4, 6);
|
||||||
|
const vs = cp.pts.map((p) => p[1]).sort((a, b) => a - b);
|
||||||
|
expect(vs[3]).toBeCloseTo(5, 6);
|
||||||
|
expect(vs[0]).toBeCloseTo(5 - 0.3, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Satteldach quer zum First: zwei Bänder (steigend/fallend) mit Neigungs-Dicke", () => {
|
||||||
|
const out = emptyOutput();
|
||||||
|
appendRoofSections(out, project(roof()), PLANE);
|
||||||
|
expect(out.cutPolygons.length).toBe(2);
|
||||||
|
const tan30 = Math.tan((30 * Math.PI) / 180);
|
||||||
|
const cos30 = Math.cos((30 * Math.PI) / 180);
|
||||||
|
const ridgeZ = 5 + 2 * tan30; // First bei halber Tiefe (y=2)
|
||||||
|
// Alle Oberkanten-Werte je Band: an u=0/4 Traufe (5), an u=2 First.
|
||||||
|
const topAt = (cp: (typeof out.cutPolygons)[number], u: number): number =>
|
||||||
|
Math.max(...cp.pts.filter((p) => Math.abs(p[0] - u) < 1e-6).map((p) => p[1]));
|
||||||
|
const bands = out.cutPolygons;
|
||||||
|
const front = bands.find((b) => b.pts.some((p) => Math.abs(p[0]) < 1e-6));
|
||||||
|
const back = bands.find((b) => b.pts.some((p) => Math.abs(p[0] - 4) < 1e-6));
|
||||||
|
expect(front).toBeDefined();
|
||||||
|
expect(back).toBeDefined();
|
||||||
|
expect(topAt(front!, 0)).toBeCloseTo(5, 5);
|
||||||
|
expect(topAt(front!, 2)).toBeCloseTo(ridgeZ, 5);
|
||||||
|
expect(topAt(back!, 4)).toBeCloseTo(5, 5);
|
||||||
|
expect(topAt(back!, 2)).toBeCloseTo(ridgeZ, 5);
|
||||||
|
// Vertikale Banddicke = lotrechte Dicke / cos(Neigung).
|
||||||
|
const vAt = (cp: (typeof out.cutPolygons)[number], u: number): number[] =>
|
||||||
|
cp.pts.filter((p) => Math.abs(p[0] - u) < 1e-6).map((p) => p[1]);
|
||||||
|
const [hi, lo] = vAt(front!, 0).sort((a, b) => b - a);
|
||||||
|
expect(hi - lo).toBeCloseTo(0.3 / cos30, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Schnittspur ausserhalb des Dachs: keine Bänder", () => {
|
||||||
|
const out = emptyOutput();
|
||||||
|
// Spur x=0 — Dach komplett rechts davon (x 10..16).
|
||||||
|
const far = roof({
|
||||||
|
outline: [
|
||||||
|
{ x: 10, y: 0 },
|
||||||
|
{ x: 16, y: 0 },
|
||||||
|
{ x: 16, y: 4 },
|
||||||
|
{ x: 10, y: 4 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
appendRoofSections(out, project(far), PLANE);
|
||||||
|
expect(out.cutPolygons.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mehrschichtiges Dach (roofTypeId): je Schicht ein Band pro Fläche, aussen oben", () => {
|
||||||
|
const p = project(roof({ shape: "flach", pitchDeg: 0, roofTypeId: "rt1" }), {
|
||||||
|
hatches: [
|
||||||
|
{ id: "h1", name: "H", pattern: "diagonal", scale: 1, angle: 45, color: "#111111" },
|
||||||
|
],
|
||||||
|
components: [
|
||||||
|
{ id: "tile", name: "Ziegel", color: "#a03a2a", hatchId: "h1", joinPriority: 1 },
|
||||||
|
{ id: "ins", name: "Dämmung", color: "#ffffff", hatchId: "h1", joinPriority: 2 },
|
||||||
|
],
|
||||||
|
roofTypes: [
|
||||||
|
{
|
||||||
|
id: "rt1",
|
||||||
|
name: "Aufbau",
|
||||||
|
layers: [
|
||||||
|
{ componentId: "tile", thickness: 0.05 },
|
||||||
|
{ componentId: "ins", thickness: 0.25 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const out = emptyOutput();
|
||||||
|
appendRoofSections(out, p, PLANE);
|
||||||
|
expect(out.cutPolygons.length).toBe(2); // 1 Fläche × 2 Schichten
|
||||||
|
const tops = out.cutPolygons.map((cp) => Math.max(...cp.pts.map((q) => q[1])));
|
||||||
|
// Eindeckung oben (OK 5), Dämmung darunter (OK 5 − 0.05).
|
||||||
|
expect(Math.max(...tops)).toBeCloseTo(5, 6);
|
||||||
|
expect(Math.min(...tops)).toBeCloseTo(4.95, 6);
|
||||||
|
// Schicht-Stile sind aufgelöst (Schraffur des Bauteils).
|
||||||
|
expect(out.cutPolygons.every((cp) => cp.hatch?.pattern === "diagonal")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
+166
-2
@@ -18,10 +18,11 @@
|
|||||||
// v = absolute Höhe (world.y). Siehe src-tauri/render3d/src/section.rs.
|
// v = absolute Höhe (world.y). Siehe src-tauri/render3d/src/section.rs.
|
||||||
|
|
||||||
import type { Ceiling, DrawingLevel, Project, Vec2, Wall } from "../model/types";
|
import type { Ceiling, DrawingLevel, Project, Vec2, Wall } from "../model/types";
|
||||||
import { getCeilingType, getComponent, getWallType, openingsOfWall } from "../model/types";
|
import { getCeilingType, getComponent, getWallType, openingsOfWall, roofLayers } from "../model/types";
|
||||||
import { ceilingVerticalExtent, wallVerticalExtent } from "../model/wall";
|
import { ceilingVerticalExtent, wallVerticalExtent } from "../model/wall";
|
||||||
import { dot, leftNormal, normalize, sub } from "../model/geometry";
|
import { dot, leftNormal, normalize, sub } from "../model/geometry";
|
||||||
import { openingInterval, openingVerticalExtent } from "../geometry/opening";
|
import { openingInterval, openingVerticalExtent } from "../geometry/opening";
|
||||||
|
import { roofBaseElevation, roofGeometry } from "../geometry/roof";
|
||||||
import { projectToModel3d } from "./toWalls3d";
|
import { projectToModel3d } from "./toWalls3d";
|
||||||
import { loadEngine3d } from "../engine/engine3d";
|
import { loadEngine3d } from "../engine/engine3d";
|
||||||
import {
|
import {
|
||||||
@@ -36,7 +37,7 @@ import type { HatchRender } from "./generatePlan";
|
|||||||
|
|
||||||
/** Bauteil-Referenz eines Cut-Polygons/einer Kante (Art + Eingabe-Index). */
|
/** Bauteil-Referenz eines Cut-Polygons/einer Kante (Art + Eingabe-Index). */
|
||||||
export interface SectionComponentRef {
|
export interface SectionComponentRef {
|
||||||
kind: "wall" | "slab";
|
kind: "wall" | "slab" | "roof";
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,6 +290,13 @@ function attachCutStyles(output: SectionOutput, project: Project, plane: Section
|
|||||||
const bands: SectionCutPolygon[] = [];
|
const bands: SectionCutPolygon[] = [];
|
||||||
for (const cp of output.cutPolygons) {
|
for (const cp of output.cutPolygons) {
|
||||||
const ref = cp.component;
|
const ref = cp.component;
|
||||||
|
// Dach: Füllung/Schraffur sind bereits je Schicht in `appendRoofSections`
|
||||||
|
// aufgelöst; keine Boolean-Dominanz (joinPriority undefined) — unverändert
|
||||||
|
// übernehmen. WICHTIG vor dem Slab-Zweig (sonst falscher owner-Lookup).
|
||||||
|
if (ref.kind === "roof") {
|
||||||
|
bands.push(cp);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (ref.kind === "wall") {
|
if (ref.kind === "wall") {
|
||||||
const owner = walls[ref.index];
|
const owner = walls[ref.index];
|
||||||
// Wand: bei mehrschichtigem Wandtyp wird das EINE geschnittene Rechteck in
|
// Wand: bei mehrschichtigem Wandtyp wird das EINE geschnittene Rechteck in
|
||||||
@@ -767,6 +775,160 @@ export function splitWallLayers(
|
|||||||
* und blendet einen Fallback ein (pkg3d muss dann über `npm run build:engine3d`
|
* und blendet einen Fallback ein (pkg3d muss dann über `npm run build:engine3d`
|
||||||
* neu gebaut werden, damit `cut_section_json` verfügbar ist).
|
* neu gebaut werden, damit `cut_section_json` verfügbar ist).
|
||||||
*/
|
*/
|
||||||
|
/** Hex "#rrggbb" → RGB 0..1 (Fallback bei ungültigem Hex). */
|
||||||
|
function hexToRgb01(hex: string, fallback: [number, number, number]): [number, number, number] {
|
||||||
|
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
||||||
|
if (!m) return fallback;
|
||||||
|
const v = parseInt(m[1], 16);
|
||||||
|
return [((v >> 16) & 0xff) / 255, ((v >> 8) & 0xff) / 255, (v & 0xff) / 255];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Default-Albedo geschnittener Dächer (wie ROOF_RGB im 3D). */
|
||||||
|
const ROOF_CUT_RGB: [number, number, number] = [0.72, 0.45, 0.36];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schneidet die vertikale Schnittebene analytisch mit den DÄCHERN des Projekts
|
||||||
|
* und hängt die Schnitt-Polygone an `output.cutPolygons` an — der Rust-Extraktor
|
||||||
|
* (`cut_section_json`) kennt nur Wände + Decken, Dächer werden TS-seitig ergänzt.
|
||||||
|
*
|
||||||
|
* Vorgehen je Dach: die Grundriss-Spur der Schnittebene (Gerade durch
|
||||||
|
* `plane.point`, Richtung u-Achse) wird gegen jede DACHFLÄCHE (konvexes Polygon
|
||||||
|
* der Aufsicht, s. roofGeometry.planes) geclippt (Cyrus–Beck) → u-Intervall.
|
||||||
|
* Die Oberkante v(u) ist auf dem Intervall LINEAR (Ebenengleichung); die
|
||||||
|
* Unterkante liegt um die LOTRECHTE Dicke / cos(Flächenneigung) tiefer
|
||||||
|
* (vertikale Dicke). Mehrschichtige Dächer (roofLayers) werden wie beim
|
||||||
|
* Decken-Schnitt in Bänder von aussen (Eindeckung, oben) nach innen zerlegt,
|
||||||
|
* jedes mit Füllung/Schraffur seines Bauteils (Neutralisierung wie
|
||||||
|
* `resolveCeilingSectionStyle`: Papier-Hintergrund, solid → Tinte).
|
||||||
|
*
|
||||||
|
* Bewusst NUR Schnitt-Polygone (keine Ansichts-/Silhouettenkanten des Dachs
|
||||||
|
* hinter der Ebene) — Ansicht/Elevation des Dachs bleibt eine eigene Phase.
|
||||||
|
* Giebel (vertikale Endflächen) schneiden die vertikale Ebene nur in einer
|
||||||
|
* Linie (keine Fläche) und entfallen.
|
||||||
|
*/
|
||||||
|
export function appendRoofSections(
|
||||||
|
output: SectionOutput,
|
||||||
|
project: Project,
|
||||||
|
plane: SectionPlaneSpec,
|
||||||
|
): void {
|
||||||
|
const roofs = project.roofs ?? [];
|
||||||
|
if (roofs.length === 0) return;
|
||||||
|
// Grundriss-Spur: Basispunkt + u-Richtung (Modell-2D). world=[x,H,y] ⇒
|
||||||
|
// plane.point[0]=x, plane.point[2]=y; u-Achse = (−nz, nx) (s. section.rs).
|
||||||
|
const base: Vec2 = { x: plane.point[0], y: plane.point[2] };
|
||||||
|
const uDir = sectionUAxisModel(plane);
|
||||||
|
const EPS_U = 1e-6;
|
||||||
|
|
||||||
|
roofs.forEach((roof, index) => {
|
||||||
|
const eavesZ = roofBaseElevation(project, roof);
|
||||||
|
const geo = roofGeometry(roof, eavesZ);
|
||||||
|
const layers = roofLayers(project, roof);
|
||||||
|
const totalT = layers.reduce((s, l) => s + Math.max(0, l.thickness), 0);
|
||||||
|
if (totalT <= 1e-9) return;
|
||||||
|
|
||||||
|
for (const pl of geo.planes) {
|
||||||
|
const pts = pl.pts;
|
||||||
|
if (pts.length < 3) continue;
|
||||||
|
// Ebenengleichung z = z0 − (A(x−x0)+B(y−y0))/C über die Newell-Normale.
|
||||||
|
let A = 0;
|
||||||
|
let B = 0;
|
||||||
|
let C = 0;
|
||||||
|
for (let i = 0; i < pts.length; i++) {
|
||||||
|
const c = pts[i];
|
||||||
|
const d = pts[(i + 1) % pts.length];
|
||||||
|
A += (c[1] - d[1]) * (c[2] + d[2]);
|
||||||
|
B += (c[2] - d[2]) * (c[0] + d[0]);
|
||||||
|
C += (c[0] - d[0]) * (c[1] + d[1]);
|
||||||
|
}
|
||||||
|
if (Math.abs(C) < 1e-9) continue; // (nahezu) vertikale Fläche — kein Flächenschnitt
|
||||||
|
const nLen = Math.hypot(A, B, C);
|
||||||
|
const cosTheta = Math.abs(C) / (nLen || 1);
|
||||||
|
const zAt = (x: number, y: number): number =>
|
||||||
|
pts[0][2] - (A * (x - pts[0][0]) + B * (y - pts[0][1])) / C;
|
||||||
|
|
||||||
|
// Cyrus–Beck: Gerade base + t·uDir gegen das konvexe Aufsichts-Polygon.
|
||||||
|
// Orientierungsunabhängig: Innenseite je Kante über den Polygon-Schwerpunkt.
|
||||||
|
let cx = 0;
|
||||||
|
let cy = 0;
|
||||||
|
for (const p of pts) {
|
||||||
|
cx += p[0];
|
||||||
|
cy += p[1];
|
||||||
|
}
|
||||||
|
cx /= pts.length;
|
||||||
|
cy /= pts.length;
|
||||||
|
let tLo = -Infinity;
|
||||||
|
let tHi = Infinity;
|
||||||
|
let outside = false;
|
||||||
|
for (let i = 0; i < pts.length && !outside; i++) {
|
||||||
|
const a = pts[i];
|
||||||
|
const b = pts[(i + 1) % pts.length];
|
||||||
|
// Kanten-Normale, zum Schwerpunkt orientiert (Innenseite).
|
||||||
|
let ex = -(b[1] - a[1]);
|
||||||
|
let ey = b[0] - a[0];
|
||||||
|
if (ex * (cx - a[0]) + ey * (cy - a[1]) < 0) {
|
||||||
|
ex = -ex;
|
||||||
|
ey = -ey;
|
||||||
|
}
|
||||||
|
const denom = ex * uDir.x + ey * uDir.y;
|
||||||
|
const dist = ex * (base.x - a[0]) + ey * (base.y - a[1]);
|
||||||
|
if (Math.abs(denom) < 1e-12) {
|
||||||
|
if (dist < -1e-9) outside = true; // parallel ausserhalb
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const t = -dist / denom;
|
||||||
|
if (denom > 0) tLo = Math.max(tLo, t);
|
||||||
|
else tHi = Math.min(tHi, t);
|
||||||
|
}
|
||||||
|
if (outside || tHi - tLo <= EPS_U) continue;
|
||||||
|
|
||||||
|
const q = (t: number): Vec2 => ({ x: base.x + t * uDir.x, y: base.y + t * uDir.y });
|
||||||
|
const p0 = q(tLo);
|
||||||
|
const p1 = q(tHi);
|
||||||
|
const zTop0 = zAt(p0.x, p0.y);
|
||||||
|
const zTop1 = zAt(p1.x, p1.y);
|
||||||
|
|
||||||
|
// Schicht-Bänder von aussen (oben) nach innen, vertikale Dicke je Schicht.
|
||||||
|
let prefixV = 0;
|
||||||
|
for (const layer of layers) {
|
||||||
|
const t = Math.max(0, layer.thickness);
|
||||||
|
if (t <= 1e-9) continue;
|
||||||
|
const vT = t / Math.max(cosTheta, 1e-6);
|
||||||
|
const top0 = zTop0 - prefixV;
|
||||||
|
const top1 = zTop1 - prefixV;
|
||||||
|
prefixV += vT;
|
||||||
|
|
||||||
|
let fill: string | undefined;
|
||||||
|
let hatch: HatchRender | undefined;
|
||||||
|
let color = ROOF_CUT_RGB;
|
||||||
|
if (layer.componentId) {
|
||||||
|
try {
|
||||||
|
const comp = getComponent(project, layer.componentId);
|
||||||
|
const h = resolveHatch(project, comp.hatchId);
|
||||||
|
fill = h.pattern === "solid" ? HATCH_INK : HATCH_PAPER;
|
||||||
|
hatch = h;
|
||||||
|
color = hexToRgb01(comp.color, ROOF_CUT_RGB);
|
||||||
|
} catch {
|
||||||
|
fill = undefined;
|
||||||
|
hatch = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.cutPolygons.push({
|
||||||
|
component: { kind: "roof", index },
|
||||||
|
color,
|
||||||
|
pts: [
|
||||||
|
[tLo, top0],
|
||||||
|
[tHi, top1],
|
||||||
|
[tHi, top1 - vT],
|
||||||
|
[tLo, top0 - vT],
|
||||||
|
],
|
||||||
|
...(fill ? { fill } : {}),
|
||||||
|
...(hatch ? { hatch } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function computeSection(
|
export async function computeSection(
|
||||||
project: Project,
|
project: Project,
|
||||||
level: DrawingLevel,
|
level: DrawingLevel,
|
||||||
@@ -794,6 +956,8 @@ export async function computeSection(
|
|||||||
plane.normal[2],
|
plane.normal[2],
|
||||||
);
|
);
|
||||||
const output = JSON.parse(json) as SectionOutput;
|
const output = JSON.parse(json) as SectionOutput;
|
||||||
|
// Dächer TS-seitig ergänzen (der Rust-Extraktor kennt nur Wände + Decken).
|
||||||
|
appendRoofSections(output, project, plane);
|
||||||
attachCutStyles(output, project, plane);
|
attachCutStyles(output, project, plane);
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user