Initial Commit
This commit is contained in:
1
packages/foundation/random/noise/src/simplex/index.ts
Normal file
1
packages/foundation/random/noise/src/simplex/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SimplexNoise, simplex2, simplex3, simplex4 } from "./simplex";
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { SimplexNoise } from "./simplex.js";
|
||||
|
||||
describe("SimplexNoise default seed", () => {
|
||||
it("constructs with default seed (no args)", { timeout: 5000 }, () => {
|
||||
const s = new SimplexNoise();
|
||||
expect(Number.isFinite(s.noise2(0.3, 0.4))).toBe(true);
|
||||
expect(Number.isFinite(s.noise3(0.3, 0.4, 0.5))).toBe(true);
|
||||
expect(Number.isFinite(s.noise4(0.3, 0.4, 0.5, 0.6))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Extra coverage for SimplexNoise.noise3 — the six tetrahedron selection
|
||||
* branches require x0/y0/z0 orderings that the main test suite doesn't hit.
|
||||
* For small positive inputs, i=j=k=0 so x0≈xin, y0≈yin, z0≈zin.
|
||||
*/
|
||||
describe("SimplexNoise noise3 tetrahedron branches", () => {
|
||||
const s = new SimplexNoise(7);
|
||||
|
||||
it("x0 >= y0 >= z0 branch", { timeout: 5000 }, () => {
|
||||
const v = s.noise3(0.6, 0.4, 0.1);
|
||||
expect(Number.isFinite(v)).toBe(true);
|
||||
});
|
||||
|
||||
it("x0 >= z0 > y0 branch", { timeout: 5000 }, () => {
|
||||
const v = s.noise3(0.6, 0.1, 0.4);
|
||||
expect(Number.isFinite(v)).toBe(true);
|
||||
});
|
||||
|
||||
it("z0 > x0 >= y0 branch", { timeout: 5000 }, () => {
|
||||
const v = s.noise3(0.4, 0.1, 0.6);
|
||||
expect(Number.isFinite(v)).toBe(true);
|
||||
});
|
||||
|
||||
it("y0 > z0 > x0 branch (the previously uncovered one)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const v = s.noise3(0.1, 0.6, 0.4);
|
||||
expect(Number.isFinite(v)).toBe(true);
|
||||
});
|
||||
|
||||
it("z0 > y0 > x0 branch", { timeout: 5000 }, () => {
|
||||
const v = s.noise3(0.1, 0.4, 0.6);
|
||||
expect(Number.isFinite(v)).toBe(true);
|
||||
});
|
||||
|
||||
it("y0 >= x0, y0 >= z0 else branch", { timeout: 5000 }, () => {
|
||||
const v = s.noise3(0.3, 0.6, 0.1);
|
||||
expect(Number.isFinite(v)).toBe(true);
|
||||
});
|
||||
});
|
||||
54
packages/foundation/random/noise/src/simplex/simplex.md
Normal file
54
packages/foundation/random/noise/src/simplex/simplex.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# simplex/simplex
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `simplex` module implements Ken Perlin's Simplex noise (2001) in 2D, 3D, and 4D. Simplex noise is the preferred replacement for classic Perlin noise when you need higher dimensional output: it scales O(N²) instead of O(2^N) in evaluation cost and avoids the axis-aligned anisotropies that plague Perlin noise in 3D+.
|
||||
|
||||
Output is approximately in `[-1, 1]`, after the normalization constants (70 for 2D, 32 for 3D, 27 for 4D) baked into each routine.
|
||||
|
||||
## Features
|
||||
|
||||
- `SimplexNoise` class — reusable seeded permutation table
|
||||
- `simplex2` / `simplex3` / `simplex4` — one-shot functional APIs
|
||||
- Uses the 12-gradient table for 2D/3D and a 32-gradient table for 4D
|
||||
- Deterministic per seed
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { simplex2 } from "@suaveplan/noise";
|
||||
|
||||
const v = simplex2("world", 1.5, 2.3);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { SimplexNoise } from "@suaveplan/noise";
|
||||
|
||||
const s = new SimplexNoise("clouds");
|
||||
// Animated clouds: use the 4th dimension as time.
|
||||
for (let y = 0; y < 256; y += 1) {
|
||||
for (let x = 0; x < 256; x += 1) {
|
||||
const v = s.noise4(x * 0.01, y * 0.01, 0, performance.now() * 0.0001);
|
||||
// ... render ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new SimplexNoise(seed)` — constructs the permutation table once.
|
||||
- `noise2(x, y) / noise3(x, y, z) / noise4(x, y, z, w): number`
|
||||
- `simplex2/3/4(seed, ...coords): number` — functional convenience.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The 4D simplex-corner selection uses rank-based selection from Gustavson's reference implementation. The `corner{2,3,4}` protected helpers compute the radial falloff — points outside a per-dimension radius contribute zero — then dot with the gradient. Normalisation constants come directly from Perlin's reference code and ensure output stays within `[-1, 1]` for typical inputs.
|
||||
|
||||
### Algorithmic Notes
|
||||
|
||||
The 2D and 3D simplex implementations use the Ken Perlin 2001 reformulation with a 256-entry pre-shuffled permutation table derived deterministically from the supplied seed. Gradient selection uses bitmask indexing into a fixed gradient table so the hot path avoids conditionals. Output range is `[-1, 1]` in all dimensions; callers that want `[0, 1]` should map via `(v + 1) / 2` at the call site.
|
||||
40
packages/foundation/random/noise/src/simplex/simplex.test.ts
Normal file
40
packages/foundation/random/noise/src/simplex/simplex.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { SimplexNoise, simplex2, simplex3, simplex4 } from "./simplex.js";
|
||||
|
||||
describe("SimplexNoise", () => {
|
||||
it("is deterministic", { timeout: 5000 }, () => {
|
||||
const a = new SimplexNoise(1);
|
||||
const b = new SimplexNoise(1);
|
||||
expect(a.noise2(0.3, 0.7)).toBe(b.noise2(0.3, 0.7));
|
||||
expect(a.noise3(0.1, 0.2, 0.3)).toBe(b.noise3(0.1, 0.2, 0.3));
|
||||
expect(a.noise4(0.1, 0.2, 0.3, 0.4)).toBe(b.noise4(0.1, 0.2, 0.3, 0.4));
|
||||
});
|
||||
|
||||
it("output roughly in [-1, 1]", { timeout: 5000 }, () => {
|
||||
const s = new SimplexNoise(2);
|
||||
for (let i = 0; i < 200; i += 1) {
|
||||
const v2 = s.noise2(i * 0.15, i * 0.25);
|
||||
const v3 = s.noise3(i * 0.15, i * 0.25, i * 0.35);
|
||||
const v4 = s.noise4(i * 0.1, i * 0.2, i * 0.3, i * 0.4);
|
||||
expect(v2).toBeGreaterThan(-1.05);
|
||||
expect(v2).toBeLessThan(1.05);
|
||||
expect(v3).toBeGreaterThan(-1.05);
|
||||
expect(v3).toBeLessThan(1.05);
|
||||
expect(v4).toBeGreaterThan(-1.05);
|
||||
expect(v4).toBeLessThan(1.05);
|
||||
}
|
||||
});
|
||||
|
||||
it("varies across different seeds", { timeout: 5000 }, () => {
|
||||
expect(simplex2(1, 0.3, 0.7)).not.toBe(simplex2(2, 0.3, 0.7));
|
||||
});
|
||||
|
||||
it("functional wrappers match class", { timeout: 5000 }, () => {
|
||||
const s = new SimplexNoise("k");
|
||||
expect(simplex2("k", 0.2, 0.3)).toBe(s.noise2(0.2, 0.3));
|
||||
expect(simplex3("k", 0.2, 0.3, 0.4)).toBe(s.noise3(0.2, 0.3, 0.4));
|
||||
expect(simplex4("k", 0.2, 0.3, 0.4, 0.5)).toBe(
|
||||
s.noise4(0.2, 0.3, 0.4, 0.5)
|
||||
);
|
||||
});
|
||||
});
|
||||
381
packages/foundation/random/noise/src/simplex/simplex.ts
Normal file
381
packages/foundation/random/noise/src/simplex/simplex.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
import { buildPermutation } from "../utils/permutation.js";
|
||||
|
||||
const F2 = 0.5 * (Math.sqrt(3) - 1);
|
||||
const G2 = (3 - Math.sqrt(3)) / 6;
|
||||
const F3 = 1 / 3;
|
||||
const G3 = 1 / 6;
|
||||
const F4 = (Math.sqrt(5) - 1) / 4;
|
||||
const G4 = (5 - Math.sqrt(5)) / 20;
|
||||
|
||||
const GRAD3 = [
|
||||
[1, 1, 0],
|
||||
[-1, 1, 0],
|
||||
[1, -1, 0],
|
||||
[-1, -1, 0],
|
||||
[1, 0, 1],
|
||||
[-1, 0, 1],
|
||||
[1, 0, -1],
|
||||
[-1, 0, -1],
|
||||
[0, 1, 1],
|
||||
[0, -1, 1],
|
||||
[0, 1, -1],
|
||||
[0, -1, -1],
|
||||
] as const;
|
||||
|
||||
const GRAD4 = [
|
||||
[0, 1, 1, 1],
|
||||
[0, 1, 1, -1],
|
||||
[0, 1, -1, 1],
|
||||
[0, 1, -1, -1],
|
||||
[0, -1, 1, 1],
|
||||
[0, -1, 1, -1],
|
||||
[0, -1, -1, 1],
|
||||
[0, -1, -1, -1],
|
||||
[1, 0, 1, 1],
|
||||
[1, 0, 1, -1],
|
||||
[1, 0, -1, 1],
|
||||
[1, 0, -1, -1],
|
||||
[-1, 0, 1, 1],
|
||||
[-1, 0, 1, -1],
|
||||
[-1, 0, -1, 1],
|
||||
[-1, 0, -1, -1],
|
||||
[1, 1, 0, 1],
|
||||
[1, 1, 0, -1],
|
||||
[1, -1, 0, 1],
|
||||
[1, -1, 0, -1],
|
||||
[-1, 1, 0, 1],
|
||||
[-1, 1, 0, -1],
|
||||
[-1, -1, 0, 1],
|
||||
[-1, -1, 0, -1],
|
||||
[1, 1, 1, 0],
|
||||
[1, 1, -1, 0],
|
||||
[1, -1, 1, 0],
|
||||
[1, -1, -1, 0],
|
||||
[-1, 1, 1, 0],
|
||||
[-1, 1, -1, 0],
|
||||
[-1, -1, 1, 0],
|
||||
[-1, -1, -1, 0],
|
||||
] as const;
|
||||
|
||||
function dot2(
|
||||
g: readonly [number, number, number],
|
||||
x: number,
|
||||
y: number
|
||||
): number {
|
||||
return g[0] * x + g[1] * y;
|
||||
}
|
||||
function dot3(
|
||||
g: readonly [number, number, number],
|
||||
x: number,
|
||||
y: number,
|
||||
z: number
|
||||
): number {
|
||||
return g[0] * x + g[1] * y + g[2] * z;
|
||||
}
|
||||
function dot4(
|
||||
g: readonly [number, number, number, number],
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
w: number
|
||||
): number {
|
||||
return g[0] * x + g[1] * y + g[2] * z + g[3] * w;
|
||||
}
|
||||
|
||||
/** Simplex noise (Ken Perlin, 2001) in 2D/3D/4D with seeded permutation. */
|
||||
export class SimplexNoise {
|
||||
protected readonly perm: Uint8Array;
|
||||
|
||||
public constructor(seed: string | number = 0) {
|
||||
this.perm = buildPermutation(seed);
|
||||
}
|
||||
|
||||
public noise2(xin: number, yin: number): number {
|
||||
const s = (xin + yin) * F2;
|
||||
const i = Math.floor(xin + s);
|
||||
const j = Math.floor(yin + s);
|
||||
const t = (i + j) * G2;
|
||||
const X0 = i - t;
|
||||
const Y0 = j - t;
|
||||
const x0 = xin - X0;
|
||||
const y0 = yin - Y0;
|
||||
let i1: number;
|
||||
let j1: number;
|
||||
if (x0 > y0) {
|
||||
i1 = 1;
|
||||
j1 = 0;
|
||||
} else {
|
||||
i1 = 0;
|
||||
j1 = 1;
|
||||
}
|
||||
const x1 = x0 - i1 + G2;
|
||||
const y1 = y0 - j1 + G2;
|
||||
const x2 = x0 - 1.0 + 2.0 * G2;
|
||||
const y2 = y0 - 1.0 + 2.0 * G2;
|
||||
const ii = i & 0xff;
|
||||
const jj = j & 0xff;
|
||||
const gi0 = (this.perm[ii + (this.perm[jj] as number)] as number) % 12;
|
||||
const gi1 =
|
||||
(this.perm[ii + i1 + (this.perm[jj + j1] as number)] as number) %
|
||||
12;
|
||||
const gi2 =
|
||||
(this.perm[ii + 1 + (this.perm[jj + 1] as number)] as number) % 12;
|
||||
const n0 = this.corner2(x0, y0, gi0);
|
||||
const n1 = this.corner2(x1, y1, gi1);
|
||||
const n2 = this.corner2(x2, y2, gi2);
|
||||
return 70.0 * (n0 + n1 + n2);
|
||||
}
|
||||
|
||||
protected corner2(x: number, y: number, gi: number): number {
|
||||
let t = 0.5 - x * x - y * y;
|
||||
if (t < 0) return 0;
|
||||
t *= t;
|
||||
return (
|
||||
t * t * dot2(GRAD3[gi] as readonly [number, number, number], x, y)
|
||||
);
|
||||
}
|
||||
|
||||
public noise3(xin: number, yin: number, zin: number): number {
|
||||
const s = (xin + yin + zin) * F3;
|
||||
const i = Math.floor(xin + s);
|
||||
const j = Math.floor(yin + s);
|
||||
const k = Math.floor(zin + s);
|
||||
const t = (i + j + k) * G3;
|
||||
const X0 = i - t;
|
||||
const Y0 = j - t;
|
||||
const Z0 = k - t;
|
||||
const x0 = xin - X0;
|
||||
const y0 = yin - Y0;
|
||||
const z0 = zin - Z0;
|
||||
let i1: number;
|
||||
let j1: number;
|
||||
let k1: number;
|
||||
let i2: number;
|
||||
let j2: number;
|
||||
let k2: number;
|
||||
if (x0 >= y0) {
|
||||
if (y0 >= z0) {
|
||||
i1 = 1;
|
||||
j1 = 0;
|
||||
k1 = 0;
|
||||
i2 = 1;
|
||||
j2 = 1;
|
||||
k2 = 0;
|
||||
} else if (x0 >= z0) {
|
||||
i1 = 1;
|
||||
j1 = 0;
|
||||
k1 = 0;
|
||||
i2 = 1;
|
||||
j2 = 0;
|
||||
k2 = 1;
|
||||
} else {
|
||||
i1 = 0;
|
||||
j1 = 0;
|
||||
k1 = 1;
|
||||
i2 = 1;
|
||||
j2 = 0;
|
||||
k2 = 1;
|
||||
}
|
||||
} else if (y0 < z0) {
|
||||
i1 = 0;
|
||||
j1 = 0;
|
||||
k1 = 1;
|
||||
i2 = 0;
|
||||
j2 = 1;
|
||||
k2 = 1;
|
||||
} else if (x0 < z0) {
|
||||
i1 = 0;
|
||||
j1 = 1;
|
||||
k1 = 0;
|
||||
i2 = 0;
|
||||
j2 = 1;
|
||||
k2 = 1;
|
||||
} else {
|
||||
i1 = 0;
|
||||
j1 = 1;
|
||||
k1 = 0;
|
||||
i2 = 1;
|
||||
j2 = 1;
|
||||
k2 = 0;
|
||||
}
|
||||
const x1 = x0 - i1 + G3;
|
||||
const y1 = y0 - j1 + G3;
|
||||
const z1 = z0 - k1 + G3;
|
||||
const x2 = x0 - i2 + 2 * G3;
|
||||
const y2 = y0 - j2 + 2 * G3;
|
||||
const z2 = z0 - k2 + 2 * G3;
|
||||
const x3 = x0 - 1 + 3 * G3;
|
||||
const y3 = y0 - 1 + 3 * G3;
|
||||
const z3 = z0 - 1 + 3 * G3;
|
||||
const ii = i & 0xff;
|
||||
const jj = j & 0xff;
|
||||
const kk = k & 0xff;
|
||||
const p = this.perm;
|
||||
const gi0 =
|
||||
(p[ii + (p[jj + (p[kk] as number)] as number)] as number) % 12;
|
||||
const gi1 =
|
||||
(p[
|
||||
ii + i1 + (p[jj + j1 + (p[kk + k1] as number)] as number)
|
||||
] as number) % 12;
|
||||
const gi2 =
|
||||
(p[
|
||||
ii + i2 + (p[jj + j2 + (p[kk + k2] as number)] as number)
|
||||
] as number) % 12;
|
||||
const gi3 =
|
||||
(p[
|
||||
ii + 1 + (p[jj + 1 + (p[kk + 1] as number)] as number)
|
||||
] as number) % 12;
|
||||
const n0 = this.corner3(x0, y0, z0, gi0);
|
||||
const n1 = this.corner3(x1, y1, z1, gi1);
|
||||
const n2 = this.corner3(x2, y2, z2, gi2);
|
||||
const n3 = this.corner3(x3, y3, z3, gi3);
|
||||
return 32 * (n0 + n1 + n2 + n3);
|
||||
}
|
||||
|
||||
protected corner3(x: number, y: number, z: number, gi: number): number {
|
||||
let t = 0.6 - x * x - y * y - z * z;
|
||||
if (t < 0) return 0;
|
||||
t *= t;
|
||||
return (
|
||||
t *
|
||||
t *
|
||||
dot3(GRAD3[gi] as readonly [number, number, number], x, y, z)
|
||||
);
|
||||
}
|
||||
|
||||
public noise4(x: number, y: number, z: number, w: number): number {
|
||||
const s = (x + y + z + w) * F4;
|
||||
const i = Math.floor(x + s);
|
||||
const j = Math.floor(y + s);
|
||||
const k = Math.floor(z + s);
|
||||
const l = Math.floor(w + s);
|
||||
const t = (i + j + k + l) * G4;
|
||||
const x0 = x - (i - t);
|
||||
const y0 = y - (j - t);
|
||||
const z0 = z - (k - t);
|
||||
const w0 = w - (l - t);
|
||||
// Rank-based simplex selection
|
||||
const c: Array<[number, number]> = [
|
||||
[x0, 0],
|
||||
[y0, 1],
|
||||
[z0, 2],
|
||||
[w0, 3],
|
||||
];
|
||||
c.sort((a, b) => (b[0] as number) - (a[0] as number));
|
||||
const rank = [0, 0, 0, 0];
|
||||
// c is built from 4 literal entries above; casts avoid optional-chain
|
||||
// branches that istanbul cannot reach.
|
||||
rank[(c[0] as [number, number])[1]] = 3;
|
||||
rank[(c[1] as [number, number])[1]] = 2;
|
||||
rank[(c[2] as [number, number])[1]] = 1;
|
||||
const off = (r: number): [number, number, number, number] => [
|
||||
/* istanbul ignore next -- rank is 4-element literal, ?? 0 unreachable */
|
||||
(rank[0] ?? 0) >= r ? 1 : 0,
|
||||
/* istanbul ignore next -- rank is 4-element literal, ?? 0 unreachable */
|
||||
(rank[1] ?? 0) >= r ? 1 : 0,
|
||||
/* istanbul ignore next -- rank is 4-element literal, ?? 0 unreachable */
|
||||
(rank[2] ?? 0) >= r ? 1 : 0,
|
||||
/* istanbul ignore next -- rank is 4-element literal, ?? 0 unreachable */
|
||||
(rank[3] ?? 0) >= r ? 1 : 0,
|
||||
];
|
||||
const o1 = off(3);
|
||||
const o2 = off(2);
|
||||
const o3 = off(1);
|
||||
const ii = i & 0xff;
|
||||
const jj = j & 0xff;
|
||||
const kk = k & 0xff;
|
||||
const ll = l & 0xff;
|
||||
const p = this.perm;
|
||||
const hash = (di: number, dj: number, dk: number, dl: number): number =>
|
||||
(p[
|
||||
ii +
|
||||
di +
|
||||
(p[
|
||||
jj +
|
||||
dj +
|
||||
(p[kk + dk + (p[ll + dl] as number)] as number)
|
||||
] as number)
|
||||
] as number) % 32;
|
||||
const gi0 = hash(0, 0, 0, 0);
|
||||
const gi1 = hash(o1[0], o1[1], o1[2], o1[3]);
|
||||
const gi2 = hash(o2[0], o2[1], o2[2], o2[3]);
|
||||
const gi3 = hash(o3[0], o3[1], o3[2], o3[3]);
|
||||
const gi4 = hash(1, 1, 1, 1);
|
||||
const n0 = this.corner4(x0, y0, z0, w0, gi0);
|
||||
const n1 = this.corner4(
|
||||
x0 - o1[0] + G4,
|
||||
y0 - o1[1] + G4,
|
||||
z0 - o1[2] + G4,
|
||||
w0 - o1[3] + G4,
|
||||
gi1
|
||||
);
|
||||
const n2 = this.corner4(
|
||||
x0 - o2[0] + 2 * G4,
|
||||
y0 - o2[1] + 2 * G4,
|
||||
z0 - o2[2] + 2 * G4,
|
||||
w0 - o2[3] + 2 * G4,
|
||||
gi2
|
||||
);
|
||||
const n3 = this.corner4(
|
||||
x0 - o3[0] + 3 * G4,
|
||||
y0 - o3[1] + 3 * G4,
|
||||
z0 - o3[2] + 3 * G4,
|
||||
w0 - o3[3] + 3 * G4,
|
||||
gi3
|
||||
);
|
||||
const n4 = this.corner4(
|
||||
x0 - 1 + 4 * G4,
|
||||
y0 - 1 + 4 * G4,
|
||||
z0 - 1 + 4 * G4,
|
||||
w0 - 1 + 4 * G4,
|
||||
gi4
|
||||
);
|
||||
return 27 * (n0 + n1 + n2 + n3 + n4);
|
||||
}
|
||||
|
||||
protected corner4(
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
w: number,
|
||||
gi: number
|
||||
): number {
|
||||
let t = 0.6 - x * x - y * y - z * z - w * w;
|
||||
if (t < 0) return 0;
|
||||
t *= t;
|
||||
return (
|
||||
t *
|
||||
t *
|
||||
dot4(
|
||||
GRAD4[gi] as readonly [number, number, number, number],
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
w
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function simplex2(seed: string | number, x: number, y: number): number {
|
||||
return new SimplexNoise(seed).noise2(x, y);
|
||||
}
|
||||
|
||||
export function simplex3(
|
||||
seed: string | number,
|
||||
x: number,
|
||||
y: number,
|
||||
z: number
|
||||
): number {
|
||||
return new SimplexNoise(seed).noise3(x, y, z);
|
||||
}
|
||||
|
||||
export function simplex4(
|
||||
seed: string | number,
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
w: number
|
||||
): number {
|
||||
return new SimplexNoise(seed).noise4(x, y, z, w);
|
||||
}
|
||||
Reference in New Issue
Block a user