Compare commits
2 Commits
40fa7891d5
...
1664404948
| Author | SHA1 | Date | |
|---|---|---|---|
| 1664404948 | |||
| fa3dc7d7e1 |
+4
-3
@@ -598,7 +598,7 @@ export interface RenderAdapter {
|
||||
## Phase D — Werkzeugpalette komplett (alles V2-Plugins)
|
||||
Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Workflowtest + Commit `task(D*)`):
|
||||
- [x] D1 Ellipse+Bogen (core-drawing) — param: cx,cy,rx,ry,rotation; Interaktion: Center→RadiusX→RadiusY (3 Klicks oder Optionsfelder) *(2026-08-29: ellipseTool 3-Klick mit Rotation aus Klick2-Zugrichtung, Voll-Ellipse=64-Punkt-geschlossenes Polygon, Bogen via startAngle/endAngle-Options als offene Polyline; Log D1)*
|
||||
- [ ] D2 Spline (CV-basierte Bézier, Speicherung Kontrollpunkte, Render als interpolierte Polyline mit Glättung)
|
||||
- [x] D2 Spline (CV-basierte Bézier, Speicherung Kontrollpunkte, Render als interpolierte Polyline mit Glättung) *(2026-08-29: splineTool mit Catmull-Rom-Interpolation DURCH alle CVs (echte Glättung mit Schwingung an Ecken), Klick-Sequenz + ENTER ab 3 CVs, controlPoints-Metadaten; Log D2)*
|
||||
- [ ] D3 Point, XLine, Ray (construction-Layer-Tag `properties.construction:true`, Standard-Layer 'defpoints' auto-anlegen)
|
||||
- [ ] D4 MTEXT (Mehrzeilig,
|
||||
, fontSize/bold/italic Properties; Editor = InlineTextEditor erweitern)
|
||||
@@ -667,6 +667,7 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
||||
|
||||
| Datum | Task | Zusammenfassung | Tests | Commit | Notizen |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-08-29 | D2 | Spline-Tool: splineTool NEU in core-drawing v1.4.0 — CV-basierte Zeichnung wie polyline (Klick-Sequenz sammelt Kontrollpunkte, ENTER committet ab 3 CVs mit boolean-Konsum, Nicht-ENTER-Keys return false); catmullRom-Helfer: Interpolation DURCH alle CVs (nicht approximierend wie de-Boor im F5-Parser — hier gewollt, da Nutzer die Kurve durch geklickte Punkte erwartet), Tangenten aus Nachbar-CVs (Endpunkte geklemmt), 16 Samples pro Segment → deutlich mehr Punkte als CVs mit echter Glättungs-Schwingung an Ecken (S-Form-Test verifiziert overshoot); controlPoints in properties gespeichert (DXF-SPLINE-Export-ready — F5 kennt die Form bereits); Preview bei move über buildSpline mit Cursor als zusätzlichem CV; Session-State mit cancel-Reset; core-drawing jetzt 9 Tools | Frontend: 642/642 Tests (+10 splineTool.test.ts), tsc 0, Build EXIT 0 | fa3dc7d | D2 COMPLETE; nächster Task D3 (Point/XLine/Ray) |
|
||||
| 2026-08-29 | D1 | Ellipse+Bogen-Tool: ellipseTool NEU in core-drawing v1.3.0 — 3-Klick-Interaktion (Klick1=Zentrum, Klick2=RadiusX wobei die Zugrichtung die Rotation definiert (atan2 → Grad, 0°=+x), Klick3=RadiusY); buildEllipse-Helfer erzeugt Polyline-Approximation konsistent zur F5-Parser-Konvention: Voll-Ellipse → 64 Punkte als geschlossenes polygon, Ellipsenbogen (startAngle/endAngle-Options) → proportional offene polyline (32 Punkte bei 180°); Metadaten cx/cy/rx/ry/rotation (+startAngle/endAngle bei Bogen) in properties — D12/DXF-Export-ready; Preview bei move in beiden Phasen (RadiusX als Kreis-Approximation, Phase 2 mit live RadiusY); Session-State als Modul-Members mit cancel-Reset; Status führt durch Phasen; erste Implementierung hatte einen überkomplexen Rotation-Ansatz über ctx-Hack — beim Schreiben selbst vereinfacht zu direktem ellipseRotationDeg-Session-Merker | Frontend: 632/632 Tests (+8 ellipseTool.test.ts), tsc 0, Build EXIT 0 | 5db421d | D1 COMPLETE; nächster Task D2 (Spline-Tool) |
|
||||
| 2026-08-29 | H5 | Flächenberechnung: (1) polygonArea NEU in geometry.ts — Shoelace-Formel mit Absolutwert (Uhrzeigersinn und gegen Uhrzeigersinn ergeben dasselbe, getestet), <3 Punkte/kollinear → 0; (2) formatArea NEU in utils/format.ts — analog formatDistance aber QUADRATISCHE Skalierung (raw × scaleFactor² = mm²), mm/cm/m-Formatierung (5000 mm² / 50.0 cm² / 5.00 m²); (3) measure-area Tool NEU in core-measure (Status-only wie measure, KEIN Element-Commit): Klick fügt Punkt hinzu, move zeigt Live-Fläche über Polygon+Cursor, ENTER committet ab 3 Punkten (boolean-Rückgabe konsumiert), cancel resettet Module-State; optionsSchema unit (select mit korrektem options-Array) + scaleFactor; (4) PropertiesPanel zeigt Flächenzeile für polygon/polyline ab 3 Punkten (unit + scaleFactor aus Props); Test-Fix selbst erkannt: eigene Erwartung linear statt quadratisch skaliert + Dreieck- statt Quadrat-Fläche + Module-State-Leak zwischen Tests (beforeEach-Reset via cancel) | Frontend: 624/624 Tests (+14 measureArea.test.ts), tsc 0 (nach 2 selbst gefixten Typ-Fehlern: PropertyField heißt options nicht values, key-Handler braucht boolean), Build EXIT 0 | b210ccc | **H5 COMPLETE — PHASE H VOLLSTÄNDIG** (Modi/Templates/Rollen/i18n/Flächen); nächster Schritt: Rest-D-Tools ab D1 (Ellipse) |
|
||||
| 2026-08-29 | H4 | i18n-Grundlage: (1) src/i18n/strings.ts NEU — zentrale DE/EN-Maps (Key-Pfad → String oder parametrisierte Funktion für dynamische Texte wie „Layout X löschen“), t(key, arg) mit Key-Fallback bei unbekannten Keys (offensichtlich sichtbar statt still), LANGUAGES-Konstante, getLanguage/setLanguage mit localStorage-Persistenz (webcad.language, Default de, ungültige Werte → de) analog uiModeService-Muster, subscribeLanguage-Set, useT-React-Hook erzwingt Re-Render bei Sprachwechsel; (2) Kern-UI-Texte auf t() umgestellt: LayoutBar (Titel, Hinzufügen, Leer, Löschen, Drucken, Titelblock, Zoom-to) + Topbar-Modus-Toggle (title + aria-label) — mit echtem Live-Wechsel getestet (de → en im laufenden Render); (3) vollständige mechanische Ersetzung ALLER Komponenten-Texte bewusst als teilautomatisierbares Follow-up markiert (Roadmap-Wortlaut); Zwischenfälle selbst behoben: Python-Quoting-Abbruch beim ersten Implementierungsversch (strings.ts wurde nicht geschrieben, Komponenten referenzierten useT schon → nachgetragen) + Testing-Library-State-Update außerhalb act() (Sprachwechsel im Test in act() gewrappt) | Frontend: 610/610 Tests (+8 i18n.test.tsx), tsc 0, Build EXIT 0 | e6fdb39 | H4 COMPLETE; nächster Task H5 (Flächenberechnung Polygon m² + measure-area Tool) |
|
||||
@@ -743,5 +744,5 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
||||
- `frontend/src/utils/__tests__/format.test.ts` liegt in src statt tests/ — vereinheitlichen (kleiner Task in I2)
|
||||
|
||||
---
|
||||
*Nächster offener Schritt: **D2** (Spline-Tool: CV-basierte Bézier, Kontrollpunkt-Speicherung, Render als interpolierte Polyline mit Glättung). Danach: D3 Point/XLine/Ray, D4 MTEXT, D9 GripEditing, D10 array-path, D11 Snaps, D12 BlockAttribute, Phase I.*
|
||||
*Status 2026-08-29: Phasen B+A+C+D+E+F+G+H komplett, D1 fertig (Ellipse-Tool), BUG-1-Kollisionsklasse geschlossen. FE 632/632 Tests, BE 287/287, tsc/Build grün. Alle Commits auf Forgejo main. Offen: D2–D4/D9/D10-path/D11/D12, Phase I (Release).*
|
||||
*Nächster offener Schritt: **D3** (Point/XLine/Ray mit construction-Layer-Tag, Standard-Layer defpoints auto-anlegen). Danach: D4 MTEXT, D9 GripEditing, D10 array-path, D11 Snaps, D12 BlockAttribute, Phase I.*
|
||||
*Status 2026-08-29: Phasen B+A+C+D+E+F+G+H komplett, D1+D2 fertig (Ellipse- + Spline-Tool), BUG-1-Kollisionsklasse geschlossen. FE 642/642 Tests, BE 287/287, tsc/Build grün. Alle Commits auf Forgejo main. Offen: D3–D4/D9/D10-path/D11/D12, Phase I (Release).*
|
||||
|
||||
@@ -558,15 +558,119 @@ export const ellipseTool: ToolExtensionV2 = {
|
||||
},
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Task D2: Spline (CV-basiert) — Kontrollpunkt-Klick-Sequenz,
|
||||
// ENTER committet ab 3 CVs. Catmull-Rom-Interpolation (Kurve läuft
|
||||
// DURCH alle CVs, schwingt an Ecken = echte Glättung).
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
let splinePoints: Pt[] = [];
|
||||
|
||||
/**
|
||||
* Catmull-Rom-Spline-Interpolation: Für n CVs werden n-1 Segmente
|
||||
* mit je SEG Samples erzeugt. Tangenten aus Nachbarn (Endpunkte
|
||||
* geklemmt). Ergibt eine glatte Kurve durch alle CVs.
|
||||
*/
|
||||
function catmullRom(cvs: Pt[], seg = 16): Pt[] {
|
||||
if (cvs.length < 2) return [...cvs];
|
||||
const out: Pt[] = [];
|
||||
const n = cvs.length;
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
const p0 = cvs[Math.max(0, i - 1)];
|
||||
const p1 = cvs[i];
|
||||
const p2 = cvs[i + 1];
|
||||
const p3 = cvs[Math.min(n - 1, i + 2)];
|
||||
for (let s = (i === 0 ? 0 : 1); s <= seg; s++) {
|
||||
const t = s / seg;
|
||||
const t2 = t * t;
|
||||
const t3 = t2 * t;
|
||||
const x = 0.5 * ((2 * p1.x) + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3);
|
||||
const y = 0.5 * ((2 * p1.y) + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3);
|
||||
out.push({ x, y });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildSpline(cvs: Pt[], ctx: FullToolContext): CADElement {
|
||||
const stroke = (ctx.options.strokeColor as string | undefined) ?? '#e0e0f0';
|
||||
const layerId = (ctx.options.layerId as string | undefined) ?? 'layer-0';
|
||||
const pts = catmullRom(cvs);
|
||||
const xs = pts.map((p) => p.x), ys = pts.map((p) => p.y);
|
||||
const minX = Math.min(...xs), minY = Math.min(...ys);
|
||||
const maxX = Math.max(...xs), maxY = Math.max(...ys);
|
||||
return {
|
||||
id: uid('spline'),
|
||||
type: 'polyline',
|
||||
layerId,
|
||||
x: minX + (maxX - minX) / 2,
|
||||
y: minY + (maxY - minY) / 2,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
properties: {
|
||||
points: pts,
|
||||
controlPoints: [...cvs],
|
||||
stroke,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
} as unknown as CADElement;
|
||||
}
|
||||
|
||||
export const splineTool: ToolExtensionV2 = {
|
||||
manifest: {
|
||||
id: 'spline',
|
||||
label: 'Spline',
|
||||
icon: '\u2248',
|
||||
ribbonTab: 'canvas',
|
||||
tags: ['basic'],
|
||||
description: 'Spline zeichnen: Kontrollpunkte klicken, ENTER beendet',
|
||||
},
|
||||
optionsSchema: [{ key: 'strokeColor', label: 'Farbe', type: 'color' }],
|
||||
handlers: {
|
||||
down(e, ctx) {
|
||||
splinePoints.push(e.world);
|
||||
ctx.setStatus(`Spline: ${splinePoints.length} Kontrollpunkte – ENTER beendet`);
|
||||
},
|
||||
move(e, ctx) {
|
||||
const c = ctx as FullToolContext;
|
||||
if (splinePoints.length === 0) return;
|
||||
const pts = [...splinePoints, e.world];
|
||||
ctx.setPreview?.(buildSpline(pts, c));
|
||||
},
|
||||
up() { /* commit nur via ENTER */ },
|
||||
key(e, ctx) {
|
||||
if (e.key !== 'Enter' && e.key !== 'ENTER') return false;
|
||||
const c = ctx as FullToolContext;
|
||||
if (splinePoints.length < 3) {
|
||||
ctx.setStatus(`Spline benötigt mindestens 3 Kontrollpunkte (aktuell ${splinePoints.length})`);
|
||||
return true;
|
||||
}
|
||||
if (!c.doc) return false;
|
||||
const el = buildSpline(splinePoints, c);
|
||||
c.doc.transact(() => c.doc!.addElement(el));
|
||||
const count = splinePoints.length;
|
||||
splinePoints = [];
|
||||
ctx.setPreview?.(null);
|
||||
ctx.setStatus(`Spline erstellt (${count} CVs, ${el.id})`);
|
||||
return true;
|
||||
},
|
||||
cancel(ctx) {
|
||||
splinePoints = [];
|
||||
ctx.setPreview?.(null);
|
||||
ctx.setStatus('Spline abgebrochen');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const coreDrawingPlugin: PluginV2 = {
|
||||
manifest: {
|
||||
id: 'core-drawing',
|
||||
name: 'Zeichnen (Kern)',
|
||||
version: '1.3.0',
|
||||
version: '1.4.0',
|
||||
author: 'web-cad team',
|
||||
description: 'Zeichenwerkzeuge V2: Linie, Rechteck, Kreis, Bogen, Polylinie, Polygon, RevCloud.',
|
||||
category: 'tools',
|
||||
enabledByDefault: true,
|
||||
},
|
||||
tools: [lineTool, rectTool, circleTool, arcTool, polylineTool, polygonTool, revcloudTool, ellipseTool],
|
||||
tools: [lineTool, rectTool, circleTool, arcTool, polylineTool, polygonTool, revcloudTool, ellipseTool, splineTool],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Task D2 – Spline-Tool (CV-basiert): Kontrollpunkt-Klick-Sequenz,
|
||||
* ENTER committet ab 3 CVs. Render als interpolierte Polyline mit
|
||||
* Catmull-Rom-Glättung (Kurve läuft DURCH alle CVs), CVs in
|
||||
* properties.controlPoints gespeichert (DXF-SPLINE-Export-ready).
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { splineTool } from '../src/plugins/builtin/core-drawing';
|
||||
import { CADDocument } from '../src/kernel/document/CADDocument';
|
||||
import { createInMemoryDoc } from './helpers/inMemoryDoc';
|
||||
import type { CADElement, Pt } from '../src/types/cad.types';
|
||||
|
||||
function mkCtx(options: Record<string, unknown> = {}) {
|
||||
const { ydoc } = createInMemoryDoc();
|
||||
const doc = new CADDocument(ydoc);
|
||||
const previews: (CADElement | null)[] = [];
|
||||
const statuses: string[] = [];
|
||||
const ctx: any = {
|
||||
doc,
|
||||
options,
|
||||
setStatus: (m: string) => statuses.push(m),
|
||||
setPreview: (el: CADElement | null) => previews.push(el),
|
||||
};
|
||||
return { ctx, doc, previews, statuses };
|
||||
}
|
||||
|
||||
function pe(x: number, y: number): { world: Pt } {
|
||||
return { world: { x, y } } as never;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
splineTool.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never);
|
||||
});
|
||||
|
||||
describe('D2: splineTool', () => {
|
||||
it('4 CVs + ENTER erzeugt EINE geglaettete Polyline mit mehr Punkten als CVs', () => {
|
||||
const { ctx, doc } = mkCtx();
|
||||
const cvs = [[0, 0], [100, 100], [200, 0], [300, 100]];
|
||||
for (const [x, y] of cvs) splineTool.handlers.down!(pe(x, y), ctx);
|
||||
const committed = splineTool.handlers.key!({ key: 'Enter' } as never, ctx);
|
||||
expect(committed).toBe(true);
|
||||
const els = doc.getAllElements();
|
||||
expect(els).toHaveLength(1);
|
||||
const el = els[0];
|
||||
expect(el.type).toBe('polyline');
|
||||
const pts = el.properties.points as Pt[];
|
||||
expect(pts.length).toBeGreaterThan(8); // geglaettet, nicht nur CVs
|
||||
});
|
||||
|
||||
it('Kurve interpoliert DURCH die CVs: Start/Ende exakt, Zwischen-CVs liegen auf der Kurve', () => {
|
||||
const { ctx, doc } = mkCtx();
|
||||
for (const [x, y] of [[0, 0], [100, 100], [200, 0], [300, 100]]) {
|
||||
splineTool.handlers.down!(pe(x, y), ctx);
|
||||
}
|
||||
splineTool.handlers.key!({ key: 'Enter' } as never, ctx);
|
||||
const el = doc.getAllElements()[0];
|
||||
const pts = el.properties.points as Pt[];
|
||||
expect(pts[0]).toEqual({ x: 0, y: 0 });
|
||||
const last = pts[pts.length - 1];
|
||||
expect(last.x).toBeCloseTo(300, 5);
|
||||
expect(last.y).toBeCloseTo(100, 5);
|
||||
// Zwischenpunkt (100,100) muss auf der Kurve liegen (Catmull-Rom interpoliert):
|
||||
const near = pts.find((p) => Math.abs(p.x - 100) < 1.5 && Math.abs(p.y - 100) < 1.5);
|
||||
expect(near).toBeDefined();
|
||||
});
|
||||
|
||||
it('CVs werden in properties.controlPoints gespeichert', () => {
|
||||
const { ctx, doc } = mkCtx();
|
||||
for (const [x, y] of [[0, 0], [50, 80], [120, 40]]) {
|
||||
splineTool.handlers.down!(pe(x, y), ctx);
|
||||
}
|
||||
splineTool.handlers.key!({ key: 'Enter' } as never, ctx);
|
||||
const el = doc.getAllElements()[0];
|
||||
const cvs = el.properties.controlPoints as Pt[];
|
||||
expect(cvs).toHaveLength(3);
|
||||
expect(cvs[1]).toEqual({ x: 50, y: 80 });
|
||||
});
|
||||
|
||||
it('Glättung: Kurve weicht von der CV-Sehnen-Linie ab (keine Polyline durch CVs)', () => {
|
||||
const { ctx, doc } = mkCtx();
|
||||
// S-artige CVs: (0,0) (0,100) (100,100) (100,0) — Catmull-Rom schwingt um die Ecken
|
||||
for (const [x, y] of [[0, 0], [0, 100], [100, 100], [100, 0]]) {
|
||||
splineTool.handlers.down!(pe(x, y), ctx);
|
||||
}
|
||||
splineTool.handlers.key!({ key: 'Enter' } as never, ctx);
|
||||
const el = doc.getAllElements()[0];
|
||||
const pts = el.properties.points as Pt[];
|
||||
// Es muss Punkte geben, die ausserhalb der BBox der Sehnen-Verbindung liegen
|
||||
// (Glattung schwingt): minY < 0 oder maxY > 100 an irgendeiner Stelle
|
||||
const ys = pts.map((p) => p.y);
|
||||
const swingsBelow = Math.min(...ys) < -0.5;
|
||||
const swingsAbove = Math.max(...ys) > 100.5;
|
||||
expect(swingsBelow || swingsAbove).toBe(true);
|
||||
});
|
||||
|
||||
it('ENTER mit weniger als 3 CVs committet NICHT (Status erklaert Mindestzahl)', () => {
|
||||
const { ctx, doc, statuses } = mkCtx();
|
||||
splineTool.handlers.down!(pe(0, 0), ctx);
|
||||
splineTool.handlers.down!(pe(50, 50), ctx);
|
||||
const consumed = splineTool.handlers.key!({ key: 'Enter' } as never, ctx);
|
||||
expect(doc.getAllElements()).toHaveLength(0);
|
||||
expect(statuses[statuses.length - 1]).toContain('3');
|
||||
});
|
||||
|
||||
it('Nicht-ENTER-Keys werden nicht konsumiert (return false)', () => {
|
||||
const { ctx } = mkCtx();
|
||||
splineTool.handlers.down!(pe(0, 0), ctx);
|
||||
const consumed = splineTool.handlers.key!({ key: 'a' } as never, ctx);
|
||||
expect(consumed).toBe(false);
|
||||
});
|
||||
|
||||
it('Preview bei move ab 1 CV', () => {
|
||||
const { ctx, previews } = mkCtx();
|
||||
splineTool.handlers.down!(pe(0, 0), ctx);
|
||||
splineTool.handlers.move!(pe(50, 50), ctx);
|
||||
expect(previews.length).toBeGreaterThanOrEqual(1);
|
||||
const last = previews[previews.length - 1];
|
||||
expect(last).not.toBeNull();
|
||||
});
|
||||
|
||||
it('cancel resettet: naechste Sitzung startet frisch', () => {
|
||||
const { ctx, doc } = mkCtx();
|
||||
splineTool.handlers.down!(pe(0, 0), ctx);
|
||||
splineTool.handlers.down!(pe(50, 50), ctx);
|
||||
splineTool.handlers.cancel?.(ctx);
|
||||
splineTool.handlers.down!(pe(0, 0), ctx);
|
||||
splineTool.handlers.down!(pe(50, 50), ctx);
|
||||
splineTool.handlers.down!(pe(100, 0), ctx);
|
||||
splineTool.handlers.key!({ key: 'Enter' } as never, ctx);
|
||||
// 3 CVs der NEUEN Sitzung — controlPoints == 3 (alte 2 nicht gemischt)
|
||||
const el = doc.getAllElements()[0];
|
||||
expect((el.properties.controlPoints as Pt[]).length).toBe(3);
|
||||
});
|
||||
|
||||
it('Status zaehlt CVs mit', () => {
|
||||
const { ctx, statuses } = mkCtx();
|
||||
splineTool.handlers.down!(pe(0, 0), ctx);
|
||||
expect(statuses[0]).toContain('1');
|
||||
expect(statuses[0]).toContain('Spline');
|
||||
});
|
||||
|
||||
it('Manifest: id=spline, ribbonTab=canvas, tags basic', () => {
|
||||
expect(splineTool.manifest.id).toBe('spline');
|
||||
expect(splineTool.manifest.ribbonTab).toBe('canvas');
|
||||
expect((splineTool.manifest.tags ?? [])).toContain('basic');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user