refactor
This commit is contained in:
parent
a889c03929
commit
bdf4a01e7f
@ -1,3 +1,4 @@
|
|||||||
|
import type { Renderer } from "./Renderer";
|
||||||
import { pointInsideRect, rectsCollide, v2, V2 } from "./V2";
|
import { pointInsideRect, rectsCollide, v2, V2 } from "./V2";
|
||||||
|
|
||||||
export class Board {
|
export class Board {
|
||||||
@ -16,81 +17,9 @@ export class Board {
|
|||||||
this.components.push({ def, pos });
|
this.components.push({ def, pos });
|
||||||
}
|
}
|
||||||
|
|
||||||
render(_canvas: HTMLCanvasElement, c: CanvasRenderingContext2D, offset: V2) {
|
render(r: Renderer) {
|
||||||
for (const comp of this.components) {
|
for (const comp of this.components) {
|
||||||
const {
|
r.drawComponent(comp, this.hoveredOverInput, this.hoveredOverOutput);
|
||||||
def: {
|
|
||||||
size: { x: w, y: h },
|
|
||||||
label,
|
|
||||||
inputs,
|
|
||||||
outputs,
|
|
||||||
},
|
|
||||||
pos,
|
|
||||||
} = comp;
|
|
||||||
|
|
||||||
const [x, y] = [pos.x + offset.x, pos.y + offset.y];
|
|
||||||
|
|
||||||
c.fillStyle = `#6abbde`;
|
|
||||||
c.fillRect(x, y, w, h);
|
|
||||||
c.strokeStyle = `#333333`;
|
|
||||||
c.lineWidth = 2;
|
|
||||||
c.strokeRect(x, y, w, h);
|
|
||||||
|
|
||||||
c.fillStyle = `#333333`;
|
|
||||||
c.font = "bold 16px monospace";
|
|
||||||
const textMetrix = c.measureText(label);
|
|
||||||
c.fillText(
|
|
||||||
label,
|
|
||||||
x + w / 2 - textMetrix.width / 2,
|
|
||||||
y + 13 + h / 2 - 16 / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
{
|
|
||||||
const pinSpace = h / (inputs.length + 1);
|
|
||||||
for (let i = 0; i < inputs.length; ++i) {
|
|
||||||
if (inputs[i] !== null) {
|
|
||||||
throw new Error("pin text not implemented");
|
|
||||||
}
|
|
||||||
c.fillStyle = `#333333`;
|
|
||||||
c.beginPath();
|
|
||||||
c.arc(x, y + (i + 1) * pinSpace, 4, 0, Math.PI * 2);
|
|
||||||
c.fill();
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.hoveredOverInput?.[0] === comp &&
|
|
||||||
this.hoveredOverInput[1] === i
|
|
||||||
) {
|
|
||||||
c.strokeStyle = `#bbbbbb`;
|
|
||||||
c.lineWidth = 2;
|
|
||||||
c.beginPath();
|
|
||||||
c.arc(x, y + (i + 1) * pinSpace, 5, 0, Math.PI * 2);
|
|
||||||
c.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const pinSpace = h / (outputs.length + 1);
|
|
||||||
for (let i = 0; i < outputs.length; ++i) {
|
|
||||||
if (outputs[i] !== null) {
|
|
||||||
throw new Error("pin text not implemented");
|
|
||||||
}
|
|
||||||
c.fillStyle = `#333333`;
|
|
||||||
c.beginPath();
|
|
||||||
c.arc(x + w, y + (i + 1) * pinSpace, 4, 0, Math.PI * 2);
|
|
||||||
c.fill();
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.hoveredOverOutput?.[0] === comp &&
|
|
||||||
this.hoveredOverOutput[1] === i
|
|
||||||
) {
|
|
||||||
c.strokeStyle = `#eee`;
|
|
||||||
c.lineWidth = 2;
|
|
||||||
c.beginPath();
|
|
||||||
c.arc(x + w, y + (i + 1) * pinSpace, 5, 0, Math.PI * 2);
|
|
||||||
c.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,7 +168,7 @@ export class ComponentRepo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Component = {
|
export type Component = {
|
||||||
def: ComponentDef;
|
def: ComponentDef;
|
||||||
pos: V2;
|
pos: V2;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { Board, ComponentRepo } from "./Board";
|
import { Board, ComponentRepo } from "./Board";
|
||||||
|
import { Renderer } from "./Renderer";
|
||||||
import type { State } from "./State";
|
import type { State } from "./State";
|
||||||
import { Normal } from "./states/Normal";
|
import { Normal } from "./states/Normal";
|
||||||
|
import { Panning } from "./states/Panning";
|
||||||
|
import { Placing } from "./states/Placing";
|
||||||
import { v2, V2 } from "./V2";
|
import { v2, V2 } from "./V2";
|
||||||
|
|
||||||
export class Cx {
|
export class Cx {
|
||||||
@ -9,59 +12,20 @@ export class Cx {
|
|||||||
private state = new Normal(this) as State;
|
private state = new Normal(this) as State;
|
||||||
private updateActions: (() => void)[] = [];
|
private updateActions: (() => void)[] = [];
|
||||||
|
|
||||||
private selectionRect: SelectionRect | null = null;
|
private selectionBox: SelectionBox | null = null;
|
||||||
private componentPlacer: ComponentPlacer | null = null;
|
private componentPlacer: ComponentPlacer | null = null;
|
||||||
|
|
||||||
public board = new Board();
|
public board = new Board();
|
||||||
public componentRepo = ComponentRepo.withDefaults();
|
public componentRepo = ComponentRepo.withDefaults();
|
||||||
|
|
||||||
render(canvas: HTMLCanvasElement) {
|
render(canvas: HTMLCanvasElement) {
|
||||||
const c = canvas.getContext("2d")!;
|
const r = new Renderer(canvas, this.offset);
|
||||||
|
|
||||||
c.imageSmoothingEnabled = false;
|
r.clear();
|
||||||
c.fillStyle = "#666";
|
r.drawGrid();
|
||||||
c.fillRect(0, 0, canvas.width, canvas.height);
|
this.board.render(r);
|
||||||
|
this.selectionBox?.render(r);
|
||||||
const dotSize = { x: 2, y: 2 };
|
this.componentPlacer?.render(r);
|
||||||
const gridSize = v2(20, 20);
|
|
||||||
|
|
||||||
c.fillStyle = "#111";
|
|
||||||
for (let y = 0; y < canvas.width / gridSize.x + 1; ++y) {
|
|
||||||
for (let x = 0; x < canvas.height / gridSize.y + 1; ++x) {
|
|
||||||
c.fillRect(
|
|
||||||
(this.offset.x % gridSize.x) + x * gridSize.x - dotSize.x / 2,
|
|
||||||
(this.offset.y % gridSize.y) + y * gridSize.y - dotSize.y / 2,
|
|
||||||
dotSize.x,
|
|
||||||
dotSize.y,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.board.render(canvas, c, this.offset);
|
|
||||||
|
|
||||||
if (this.selectionRect) {
|
|
||||||
const {
|
|
||||||
pos: { x, y },
|
|
||||||
size: { x: w, y: h },
|
|
||||||
} = this.selectionRect;
|
|
||||||
|
|
||||||
c.fillStyle = `#ff880088`;
|
|
||||||
c.fillRect(x, y, w, h);
|
|
||||||
c.strokeStyle = `#ff8800`;
|
|
||||||
c.lineWidth = 2;
|
|
||||||
c.strokeRect(x, y, w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.componentPlacer) {
|
|
||||||
const {
|
|
||||||
pos: { x, y },
|
|
||||||
size: { x: w, y: h },
|
|
||||||
} = this.componentPlacer;
|
|
||||||
|
|
||||||
c.strokeStyle = `#ffffff`;
|
|
||||||
c.lineWidth = 2;
|
|
||||||
c.strokeRect(x, y, w, h);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderIfNeeded(canvas: HTMLCanvasElement) {
|
renderIfNeeded(canvas: HTMLCanvasElement) {
|
||||||
@ -91,16 +55,20 @@ export class Cx {
|
|||||||
this.state.onKeyUp?.(key);
|
this.state.onKeyUp?.(key);
|
||||||
}
|
}
|
||||||
selectTool(tool: Tool) {
|
selectTool(tool: Tool) {
|
||||||
// this is very much a hack so that other tools
|
switch (tool) {
|
||||||
// can be selected from any tool, without me
|
case "pan":
|
||||||
// having to add that in every state.
|
this.transitionTo(new Panning(this));
|
||||||
if (!(this.state instanceof Normal)) {
|
break;
|
||||||
this.transitionTo(new Normal(this));
|
case "input":
|
||||||
|
case "output":
|
||||||
|
case "and":
|
||||||
|
case "or":
|
||||||
|
case "not":
|
||||||
|
this.transitionTo(new Placing(this, tool));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.transitionTo(new Normal(this));
|
||||||
}
|
}
|
||||||
this.state.selectTool?.(tool);
|
|
||||||
}
|
|
||||||
selectedTool(): Tool | null {
|
|
||||||
return this.state.selectedTool?.() ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addUpdateAction(action: () => void): object {
|
addUpdateAction(action: () => void): object {
|
||||||
@ -134,25 +102,25 @@ export class Cx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addSelectionRect(pos: V2) {
|
addSelectionRect(pos: V2) {
|
||||||
this.selectionRect = { pos, size: v2(0, 0) };
|
this.selectionBox = new SelectionBox(pos, v2(0, 0));
|
||||||
this.renderNeeded = true;
|
this.renderNeeded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeSelectionRect() {
|
removeSelectionRect() {
|
||||||
this.selectionRect = null;
|
this.selectionBox = null;
|
||||||
this.renderNeeded = true;
|
this.renderNeeded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
moveSelectionRect(deltaPos: V2) {
|
moveSelectionRect(deltaPos: V2) {
|
||||||
if (this.selectionRect) {
|
if (this.selectionBox) {
|
||||||
this.selectionRect.size.x += deltaPos.x;
|
this.selectionBox.size.x += deltaPos.x;
|
||||||
this.selectionRect.size.y += deltaPos.y;
|
this.selectionBox.size.y += deltaPos.y;
|
||||||
this.renderNeeded = true;
|
this.renderNeeded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addComponentPlacer(pos: V2, size: V2) {
|
addComponentPlacer(pos: V2, size: V2) {
|
||||||
this.componentPlacer = { pos, size };
|
this.componentPlacer = new ComponentPlacer(pos, size);
|
||||||
this.renderNeeded = true;
|
this.renderNeeded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,14 +143,26 @@ export class Cx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SelectionRect = {
|
export class SelectionBox {
|
||||||
pos: V2;
|
constructor(
|
||||||
size: V2;
|
public pos: V2,
|
||||||
};
|
public size: V2,
|
||||||
|
) {}
|
||||||
|
|
||||||
export type ComponentPlacer = {
|
render(r: Renderer) {
|
||||||
pos: V2;
|
r.drawSelectionBox(this.pos, this.size);
|
||||||
size: V2;
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export class ComponentPlacer {
|
||||||
|
constructor(
|
||||||
|
public pos: V2,
|
||||||
|
public size: V2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
render(r: Renderer) {
|
||||||
|
r.drawComponentPlacer(this.pos, this.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type Tool = string;
|
export type Tool = string;
|
||||||
|
|||||||
134
editor/src/editor/Renderer.ts
Normal file
134
editor/src/editor/Renderer.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import type { Component } from "./Board";
|
||||||
|
import { v2, type V2 } from "./V2";
|
||||||
|
|
||||||
|
export class Renderer {
|
||||||
|
private c: CanvasRenderingContext2D;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private canvas: HTMLCanvasElement,
|
||||||
|
private offset: V2,
|
||||||
|
) {
|
||||||
|
this.c = this.canvas.getContext("2d")!;
|
||||||
|
this.c.imageSmoothingEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
const { canvas, c } = this;
|
||||||
|
c.fillStyle = "#666";
|
||||||
|
c.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawGrid() {
|
||||||
|
const { canvas, c } = this;
|
||||||
|
|
||||||
|
const dotSize = { x: 2, y: 2 };
|
||||||
|
const gridSize = v2(20, 20);
|
||||||
|
|
||||||
|
c.fillStyle = "#111";
|
||||||
|
for (let y = 0; y < canvas.width / gridSize.x + 1; ++y) {
|
||||||
|
for (let x = 0; x < canvas.height / gridSize.y + 1; ++x) {
|
||||||
|
c.fillRect(
|
||||||
|
(this.offset.x % gridSize.x) + x * gridSize.x - dotSize.x / 2,
|
||||||
|
(this.offset.y % gridSize.y) + y * gridSize.y - dotSize.y / 2,
|
||||||
|
dotSize.x,
|
||||||
|
dotSize.y,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSelectionBox(pos: V2, size: V2) {
|
||||||
|
const { c } = this;
|
||||||
|
const { x, y } = pos;
|
||||||
|
const { x: w, y: h } = size;
|
||||||
|
c.fillStyle = `#ff880088`;
|
||||||
|
c.fillRect(x, y, w, h);
|
||||||
|
c.strokeStyle = `#ff8800`;
|
||||||
|
c.lineWidth = 2;
|
||||||
|
c.strokeRect(x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawComponentPlacer(pos: V2, size: V2) {
|
||||||
|
const { c } = this;
|
||||||
|
const { x, y } = pos;
|
||||||
|
const { x: w, y: h } = size;
|
||||||
|
c.strokeStyle = `#ffffff`;
|
||||||
|
c.lineWidth = 2;
|
||||||
|
c.strokeRect(x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawComponent(
|
||||||
|
comp: Component,
|
||||||
|
hoveredOverInput: [Component, number] | null,
|
||||||
|
hoveredOverOutput: [Component, number] | null,
|
||||||
|
) {
|
||||||
|
const { c, offset } = this;
|
||||||
|
const {
|
||||||
|
def: {
|
||||||
|
size: { x: w, y: h },
|
||||||
|
label,
|
||||||
|
inputs,
|
||||||
|
outputs,
|
||||||
|
},
|
||||||
|
pos,
|
||||||
|
} = comp;
|
||||||
|
|
||||||
|
const [x, y] = [pos.x + offset.x, pos.y + offset.y];
|
||||||
|
|
||||||
|
c.fillStyle = `#6abbde`;
|
||||||
|
c.fillRect(x, y, w, h);
|
||||||
|
c.strokeStyle = `#333333`;
|
||||||
|
c.lineWidth = 2;
|
||||||
|
c.strokeRect(x, y, w, h);
|
||||||
|
|
||||||
|
c.fillStyle = `#333333`;
|
||||||
|
c.font = "bold 16px monospace";
|
||||||
|
const textMetrix = c.measureText(label);
|
||||||
|
c.fillText(
|
||||||
|
label,
|
||||||
|
x + w / 2 - textMetrix.width / 2,
|
||||||
|
y + 13 + h / 2 - 16 / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const pinSpace = h / (inputs.length + 1);
|
||||||
|
for (let i = 0; i < inputs.length; ++i) {
|
||||||
|
if (inputs[i] !== null) {
|
||||||
|
throw new Error("pin text not implemented");
|
||||||
|
}
|
||||||
|
c.fillStyle = `#333333`;
|
||||||
|
c.beginPath();
|
||||||
|
c.arc(x, y + (i + 1) * pinSpace, 4, 0, Math.PI * 2);
|
||||||
|
c.fill();
|
||||||
|
|
||||||
|
if (hoveredOverInput?.[0] === comp && hoveredOverInput[1] === i) {
|
||||||
|
c.strokeStyle = `#bbbbbb`;
|
||||||
|
c.lineWidth = 2;
|
||||||
|
c.beginPath();
|
||||||
|
c.arc(x, y + (i + 1) * pinSpace, 5, 0, Math.PI * 2);
|
||||||
|
c.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const pinSpace = h / (outputs.length + 1);
|
||||||
|
for (let i = 0; i < outputs.length; ++i) {
|
||||||
|
if (outputs[i] !== null) {
|
||||||
|
throw new Error("pin text not implemented");
|
||||||
|
}
|
||||||
|
c.fillStyle = `#333333`;
|
||||||
|
c.beginPath();
|
||||||
|
c.arc(x + w, y + (i + 1) * pinSpace, 4, 0, Math.PI * 2);
|
||||||
|
c.fill();
|
||||||
|
|
||||||
|
if (hoveredOverOutput?.[0] === comp && hoveredOverOutput[1] === i) {
|
||||||
|
c.strokeStyle = `#eee`;
|
||||||
|
c.lineWidth = 2;
|
||||||
|
c.beginPath();
|
||||||
|
c.arc(x + w, y + (i + 1) * pinSpace, 5, 0, Math.PI * 2);
|
||||||
|
c.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +1,13 @@
|
|||||||
import type { Tool } from "./Cx";
|
import type { Tool } from "./Cx";
|
||||||
import type { V2_ } from "./V2";
|
import type { V2 } from "./V2";
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
enterState?(): void;
|
enterState?(): void;
|
||||||
leaveState?(): void;
|
leaveState?(): void;
|
||||||
onMouseDown?(pos: V2_): void;
|
onMouseDown?(pos: V2): void;
|
||||||
onMouseUp?(pos: V2_): void;
|
onMouseUp?(pos: V2): void;
|
||||||
onMouseMove?(deltaPos: V2_, pos: V2_): void;
|
onMouseMove?(deltaPos: V2, pos: V2): void;
|
||||||
onKeyDown?(key: string): void;
|
onKeyDown?(key: string): void;
|
||||||
onKeyUp?(key: string): void;
|
onKeyUp?(key: string): void;
|
||||||
selectTool?(tool: Tool): void;
|
|
||||||
selectedTool?(): Tool | null;
|
selectedTool?(): Tool | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,26 +2,11 @@ import type { Cx, Tool } from "../Cx";
|
|||||||
import type { V2 } from "../V2";
|
import type { V2 } from "../V2";
|
||||||
import type { State } from "../State";
|
import type { State } from "../State";
|
||||||
import { Panning } from "./Panning";
|
import { Panning } from "./Panning";
|
||||||
import { Placing } from "./Placing";
|
import { SelectingBox } from "./SelectingBox";
|
||||||
import { Selecting } from "./Selecting";
|
|
||||||
|
|
||||||
export class Normal implements State {
|
export class Normal implements State {
|
||||||
constructor(private cx: Cx) {}
|
constructor(private cx: Cx) {}
|
||||||
|
|
||||||
selectTool(tool: Tool): void {
|
|
||||||
switch (tool) {
|
|
||||||
case "pan":
|
|
||||||
this.cx.transitionTo(new Panning(this.cx));
|
|
||||||
break;
|
|
||||||
case "input":
|
|
||||||
case "output":
|
|
||||||
case "and":
|
|
||||||
case "or":
|
|
||||||
case "not":
|
|
||||||
this.cx.transitionTo(new Placing(this.cx, tool));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseDown(pos: V2): void {
|
onMouseDown(pos: V2): void {
|
||||||
if (
|
if (
|
||||||
this.cx.board.handleMouseClick(
|
this.cx.board.handleMouseClick(
|
||||||
@ -34,7 +19,7 @@ export class Normal implements State {
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this.cx.addSelectionRect(pos);
|
this.cx.addSelectionRect(pos);
|
||||||
this.cx.transitionTo(new Selecting(this.cx));
|
this.cx.transitionTo(new SelectingBox(this.cx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,6 @@
|
|||||||
import type { Cx, Tool } from "../Cx";
|
import type { Cx } from "../Cx";
|
||||||
import type { V2 } from "../V2";
|
|
||||||
import type { State } from "../State";
|
import type { State } from "../State";
|
||||||
import { Normal } from "./Normal";
|
|
||||||
|
|
||||||
export class Selecting implements State {
|
export class Selecting implements State {
|
||||||
constructor(private cx: Cx) {}
|
constructor(private cx: Cx) {}
|
||||||
|
|
||||||
onMouseUp(_pos: V2): void {
|
|
||||||
this.cx.removeSelectionRect();
|
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseMove(deltaPos: V2): void {
|
|
||||||
this.cx.moveSelectionRect(deltaPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedTool(): Tool | null {
|
|
||||||
return "select";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
21
editor/src/editor/states/SelectingBox.ts
Normal file
21
editor/src/editor/states/SelectingBox.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { Cx, Tool } from "../Cx";
|
||||||
|
import type { V2 } from "../V2";
|
||||||
|
import type { State } from "../State";
|
||||||
|
import { Normal } from "./Normal";
|
||||||
|
|
||||||
|
export class SelectingBox implements State {
|
||||||
|
constructor(private cx: Cx) {}
|
||||||
|
|
||||||
|
onMouseUp(_pos: V2): void {
|
||||||
|
this.cx.removeSelectionRect();
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(deltaPos: V2): void {
|
||||||
|
this.cx.moveSelectionRect(deltaPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedTool(): Tool | null {
|
||||||
|
return "select";
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user