Refactored Cylinder generation to be more maintainable, readable, and fix #640.
This commit is contained in:
parent
c2d25ee9d6
commit
edf279a06d
@ -40,14 +40,11 @@ import com.jme3.math.FastMath;
|
|||||||
import com.jme3.math.Vector3f;
|
import com.jme3.math.Vector3f;
|
||||||
import com.jme3.scene.Mesh;
|
import com.jme3.scene.Mesh;
|
||||||
import com.jme3.scene.VertexBuffer.Type;
|
import com.jme3.scene.VertexBuffer.Type;
|
||||||
import com.jme3.scene.mesh.IndexBuffer;
|
|
||||||
import com.jme3.util.BufferUtils;
|
import com.jme3.util.BufferUtils;
|
||||||
import static com.jme3.util.BufferUtils.*;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.FloatBuffer;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple cylinder, defined by it's height and radius.
|
* A simple cylinder, defined by its height and radius.
|
||||||
* (Ported to jME3)
|
* (Ported to jME3)
|
||||||
*
|
*
|
||||||
* @author Mark Powell
|
* @author Mark Powell
|
||||||
@ -127,10 +124,10 @@ public class Cylinder extends Mesh {
|
|||||||
* mapped to texture coordinates (0.5, 1), bottom to (0.5, 0). Thus you need
|
* mapped to texture coordinates (0.5, 1), bottom to (0.5, 0). Thus you need
|
||||||
* a suited distorted texture.
|
* a suited distorted texture.
|
||||||
*
|
*
|
||||||
* @param axisSamples
|
* @param axisSamples The number of vertices samples along the axis. It is equal to the number of segments + 1; so
|
||||||
* Number of triangle samples along the axis.
|
* that, for instance, 4 samples mean the cylinder will be made of 3 segments.
|
||||||
* @param radialSamples
|
* @param radialSamples The number of triangle samples along the radius. For instance, 4 means that the sides of the
|
||||||
* Number of triangle samples along the radial.
|
* cylinder are made of 4 rectangles, and the top and bottom are made of 4 triangles.
|
||||||
* @param radius
|
* @param radius
|
||||||
* The radius of the cylinder.
|
* The radius of the cylinder.
|
||||||
* @param height
|
* @param height
|
||||||
@ -201,193 +198,239 @@ public class Cylinder extends Mesh {
|
|||||||
/**
|
/**
|
||||||
* Rebuilds the cylinder based on a new set of parameters.
|
* Rebuilds the cylinder based on a new set of parameters.
|
||||||
*
|
*
|
||||||
* @param axisSamples the number of samples along the axis.
|
* @param axisSamples The number of vertices samples along the axis. It is equal to the number of segments + 1; so
|
||||||
* @param radialSamples the number of samples around the radial.
|
* that, for instance, 4 samples mean the cylinder will be made of 3 segments.
|
||||||
* @param radius the radius of the bottom of the cylinder.
|
* @param radialSamples The number of triangle samples along the radius. For instance, 4 means that the sides of the
|
||||||
* @param radius2 the radius of the top of the cylinder.
|
* cylinder are made of 4 rectangles, and the top and bottom are made of 4 triangles.
|
||||||
|
* @param topRadius the radius of the top of the cylinder.
|
||||||
|
* @param bottomRadius the radius of the bottom of the cylinder.
|
||||||
* @param height the cylinder's height.
|
* @param height the cylinder's height.
|
||||||
* @param closed should the cylinder have top and bottom surfaces.
|
* @param closed should the cylinder have top and bottom surfaces.
|
||||||
* @param inverted is the cylinder is meant to be viewed from the inside.
|
* @param inverted is the cylinder is meant to be viewed from the inside.
|
||||||
*/
|
*/
|
||||||
public void updateGeometry(int axisSamples, int radialSamples,
|
public void updateGeometry(int axisSamples, int radialSamples,
|
||||||
float radius, float radius2, float height, boolean closed, boolean inverted) {
|
float topRadius, float bottomRadius, float height, boolean closed, boolean inverted)
|
||||||
this.axisSamples = axisSamples;
|
{
|
||||||
|
// Ensure there's at least two axis samples and 3 radial samples, and positive geometries.
|
||||||
|
if( axisSamples < 2
|
||||||
|
|| radialSamples < 3
|
||||||
|
|| topRadius <= 0
|
||||||
|
|| bottomRadius <= 0
|
||||||
|
|| height <= 0 )
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.axisSamples = axisSamples;
|
||||||
this.radialSamples = radialSamples;
|
this.radialSamples = radialSamples;
|
||||||
this.radius = radius;
|
this.radius = bottomRadius;
|
||||||
this.radius2 = radius2;
|
this.radius2 = topRadius;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.closed = closed;
|
this.closed = closed;
|
||||||
this.inverted = inverted;
|
this.inverted = inverted;
|
||||||
|
|
||||||
// VertexBuffer pvb = getBuffer(Type.Position);
|
// Vertices : One per radial sample plus one duplicate for texture closing around the sides.
|
||||||
// VertexBuffer nvb = getBuffer(Type.Normal);
|
int verticesCount = axisSamples * (radialSamples +1);
|
||||||
// VertexBuffer tvb = getBuffer(Type.TexCoord);
|
// Triangles: Two per side rectangle, which is the product of numbers of samples.
|
||||||
axisSamples += (closed ? 2 : 0);
|
int trianglesCount = axisSamples * radialSamples * 2 ;
|
||||||
|
if( closed )
|
||||||
|
{
|
||||||
|
// If there are caps, add two additional rims and two summits.
|
||||||
|
verticesCount += 2 + 2 * (radialSamples +1);
|
||||||
|
// Add one triangle per radial sample, twice, to form the caps.
|
||||||
|
trianglesCount += 2 * radialSamples ;
|
||||||
|
}
|
||||||
|
|
||||||
// Vertices
|
// Compute the points along a unit circle:
|
||||||
int vertCount = axisSamples * (radialSamples + 1) + (closed ? 2 : 0);
|
float[][] circlePoints = new float[radialSamples+1][2];
|
||||||
|
for (int circlePoint = 0; circlePoint < radialSamples; circlePoint++)
|
||||||
setBuffer(Type.Position, 3, createVector3Buffer(getFloatBuffer(Type.Position), vertCount));
|
{
|
||||||
|
float angle = FastMath.TWO_PI / radialSamples * circlePoint;
|
||||||
// Normals
|
circlePoints[circlePoint][0] = FastMath.cos(angle);
|
||||||
setBuffer(Type.Normal, 3, createVector3Buffer(getFloatBuffer(Type.Normal), vertCount));
|
circlePoints[circlePoint][1] = FastMath.sin(angle);
|
||||||
|
|
||||||
// Texture co-ordinates
|
|
||||||
setBuffer(Type.TexCoord, 2, createVector2Buffer(vertCount));
|
|
||||||
|
|
||||||
int triCount = ((closed ? 2 : 0) + 2 * (axisSamples - 1)) * radialSamples;
|
|
||||||
|
|
||||||
setBuffer(Type.Index, 3, createShortBuffer(getShortBuffer(Type.Index), 3 * triCount));
|
|
||||||
|
|
||||||
// generate geometry
|
|
||||||
float inverseRadial = 1.0f / radialSamples;
|
|
||||||
float inverseAxisLess = 1.0f / (closed ? axisSamples - 3 : axisSamples - 1);
|
|
||||||
float inverseAxisLessTexture = 1.0f / (axisSamples - 1);
|
|
||||||
float halfHeight = 0.5f * height;
|
|
||||||
|
|
||||||
// Generate points on the unit circle to be used in computing the mesh
|
|
||||||
// points on a cylinder slice.
|
|
||||||
float[] sin = new float[radialSamples + 1];
|
|
||||||
float[] cos = new float[radialSamples + 1];
|
|
||||||
|
|
||||||
for (int radialCount = 0; radialCount < radialSamples; radialCount++) {
|
|
||||||
float angle = FastMath.TWO_PI * inverseRadial * radialCount;
|
|
||||||
cos[radialCount] = FastMath.cos(angle);
|
|
||||||
sin[radialCount] = FastMath.sin(angle);
|
|
||||||
}
|
}
|
||||||
sin[radialSamples] = sin[0];
|
// Add an additional point for closing the texture around the side of the cylinder.
|
||||||
cos[radialSamples] = cos[0];
|
circlePoints[radialSamples][0] = circlePoints[0][0];
|
||||||
|
circlePoints[radialSamples][1] = circlePoints[0][1];
|
||||||
|
|
||||||
// calculate normals
|
// Calculate normals.
|
||||||
Vector3f[] vNormals = null;
|
//
|
||||||
Vector3f vNormal = Vector3f.UNIT_Z;
|
// A---------B
|
||||||
|
// \ |
|
||||||
if ((height != 0.0f) && (radius != radius2)) {
|
// \ |
|
||||||
vNormals = new Vector3f[radialSamples];
|
// \ |
|
||||||
Vector3f vHeight = Vector3f.UNIT_Z.mult(height);
|
// D-----C
|
||||||
Vector3f vRadial = new Vector3f();
|
//
|
||||||
|
// Let be B and C the top and bottom points of the axis, and A and D the top and bottom edges.
|
||||||
for (int radialCount = 0; radialCount < radialSamples; radialCount++) {
|
// The normal in A and D is simply orthogonal to AD, which means we can get it once per sample.
|
||||||
vRadial.set(cos[radialCount], sin[radialCount], 0.0f);
|
//
|
||||||
Vector3f vRadius = vRadial.mult(radius);
|
Vector3f[] circleNormals = new Vector3f[radialSamples+1];
|
||||||
Vector3f vRadius2 = vRadial.mult(radius2);
|
for (int circlePoint = 0; circlePoint < radialSamples+1; circlePoint++)
|
||||||
Vector3f vMantle = vHeight.subtract(vRadius2.subtract(vRadius));
|
{
|
||||||
Vector3f vTangent = vRadial.cross(Vector3f.UNIT_Z);
|
// The normal is the orthogonal to the side, which can be got without trigonometry.
|
||||||
vNormals[radialCount] = vMantle.cross(vTangent).normalize();
|
// The edge direction is oriented so that it goes up by Height, and out by the radius difference; let's use
|
||||||
}
|
// those values in reverse order.
|
||||||
|
Vector3f normal = new Vector3f(height * circlePoints[circlePoint][0], height * circlePoints[circlePoint][1], bottomRadius - topRadius );
|
||||||
|
circleNormals[circlePoint] = normal.normalizeLocal();
|
||||||
}
|
}
|
||||||
|
|
||||||
FloatBuffer nb = getFloatBuffer(Type.Normal);
|
float[] vertices = new float[verticesCount * 3];
|
||||||
FloatBuffer pb = getFloatBuffer(Type.Position);
|
float[] normals = new float[verticesCount * 3];
|
||||||
FloatBuffer tb = getFloatBuffer(Type.TexCoord);
|
float[] textureCoords = new float[verticesCount * 2];
|
||||||
|
int currentIndex = 0;
|
||||||
|
|
||||||
// generate the cylinder itself
|
// Add a circle of points for each axis sample.
|
||||||
Vector3f tempNormal = new Vector3f();
|
for(int axisSample = 0; axisSample < axisSamples; axisSample++ )
|
||||||
for (int axisCount = 0, i = 0; axisCount < axisSamples; axisCount++, i++) {
|
{
|
||||||
float axisFraction;
|
float currentHeight = -height / 2 + height * axisSample / (axisSamples-1);
|
||||||
float axisFractionTexture;
|
float currentRadius = bottomRadius + (topRadius - bottomRadius) * axisSample / (axisSamples-1);
|
||||||
int topBottom = 0;
|
|
||||||
if (!closed) {
|
|
||||||
axisFraction = axisCount * inverseAxisLess; // in [0,1]
|
|
||||||
axisFractionTexture = axisFraction;
|
|
||||||
} else {
|
|
||||||
if (axisCount == 0) {
|
|
||||||
topBottom = -1; // bottom
|
|
||||||
axisFraction = 0;
|
|
||||||
axisFractionTexture = inverseAxisLessTexture;
|
|
||||||
} else if (axisCount == axisSamples - 1) {
|
|
||||||
topBottom = 1; // top
|
|
||||||
axisFraction = 1;
|
|
||||||
axisFractionTexture = 1 - inverseAxisLessTexture;
|
|
||||||
} else {
|
|
||||||
axisFraction = (axisCount - 1) * inverseAxisLess;
|
|
||||||
axisFractionTexture = axisCount * inverseAxisLessTexture;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// compute center of slice
|
for (int circlePoint = 0; circlePoint < radialSamples + 1; circlePoint++)
|
||||||
float z = -halfHeight + height * axisFraction;
|
{
|
||||||
Vector3f sliceCenter = new Vector3f(0, 0, z);
|
// Position, by multipliying the position on a unit circle with the current radius.
|
||||||
|
vertices[currentIndex*3] = circlePoints[circlePoint][0] * currentRadius;
|
||||||
|
vertices[currentIndex*3 +1] = circlePoints[circlePoint][1] * currentRadius;
|
||||||
|
vertices[currentIndex*3 +2] = currentHeight;
|
||||||
|
|
||||||
// compute slice vertices with duplication at end point
|
// Normal
|
||||||
int save = i;
|
Vector3f currentNormal = circleNormals[circlePoint];
|
||||||
for (int radialCount = 0; radialCount < radialSamples; radialCount++, i++) {
|
normals[currentIndex*3] = currentNormal.x;
|
||||||
float radialFraction = radialCount * inverseRadial; // in [0,1)
|
normals[currentIndex*3+1] = currentNormal.y;
|
||||||
tempNormal.set(cos[radialCount], sin[radialCount], 0.0f);
|
normals[currentIndex*3+2] = currentNormal.z;
|
||||||
|
|
||||||
if (vNormals != null) {
|
// Texture
|
||||||
vNormal = vNormals[radialCount];
|
// The X is the angular position of the point.
|
||||||
} else if (radius == radius2) {
|
textureCoords[currentIndex *2] = (float) circlePoint / radialSamples;
|
||||||
vNormal = tempNormal;
|
// Depending on whether there is a cap, the Y is either the height scaled to [0,1], or the radii of
|
||||||
}
|
// the cap count as well.
|
||||||
|
if (closed)
|
||||||
|
textureCoords[currentIndex *2 +1] = (bottomRadius + height / 2 + currentHeight) / (bottomRadius + height + topRadius);
|
||||||
|
else
|
||||||
|
textureCoords[currentIndex *2 +1] = height / 2 + currentHeight;
|
||||||
|
|
||||||
if (topBottom == 0) {
|
currentIndex++;
|
||||||
if (!inverted)
|
}
|
||||||
nb.put(vNormal.x).put(vNormal.y).put(vNormal.z);
|
}
|
||||||
else
|
|
||||||
nb.put(-vNormal.x).put(-vNormal.y).put(-vNormal.z);
|
|
||||||
} else {
|
|
||||||
nb.put(0).put(0).put(topBottom * (inverted ? -1 : 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
tempNormal.multLocal((radius - radius2) * axisFraction + radius2)
|
// If closed, add duplicate rims on top and bottom, with normals facing up and down.
|
||||||
.addLocal(sliceCenter);
|
if (closed)
|
||||||
pb.put(tempNormal.x).put(tempNormal.y).put(tempNormal.z);
|
{
|
||||||
|
// Bottom
|
||||||
|
for (int circlePoint = 0; circlePoint < radialSamples + 1; circlePoint++)
|
||||||
|
{
|
||||||
|
vertices[currentIndex*3] = circlePoints[circlePoint][0] * bottomRadius;
|
||||||
|
vertices[currentIndex*3 +1] = circlePoints[circlePoint][1] * bottomRadius;
|
||||||
|
vertices[currentIndex*3 +2] = -height/2;
|
||||||
|
|
||||||
tb.put((inverted ? 1 - radialFraction : radialFraction))
|
normals[currentIndex*3] = 0;
|
||||||
.put(axisFractionTexture);
|
normals[currentIndex*3+1] = 0;
|
||||||
}
|
normals[currentIndex*3+2] = -1;
|
||||||
|
|
||||||
BufferUtils.copyInternalVector3(pb, save, i);
|
textureCoords[currentIndex *2] = (float) circlePoint / radialSamples;
|
||||||
BufferUtils.copyInternalVector3(nb, save, i);
|
textureCoords[currentIndex *2 +1] = bottomRadius / (bottomRadius + height + topRadius);
|
||||||
|
|
||||||
tb.put((inverted ? 0.0f : 1.0f))
|
currentIndex++;
|
||||||
.put(axisFractionTexture);
|
}
|
||||||
|
// Top
|
||||||
|
for (int circlePoint = 0; circlePoint < radialSamples + 1; circlePoint++)
|
||||||
|
{
|
||||||
|
vertices[currentIndex*3] = circlePoints[circlePoint][0] * topRadius;
|
||||||
|
vertices[currentIndex*3 +1] = circlePoints[circlePoint][1] * topRadius;
|
||||||
|
vertices[currentIndex*3 +2] = height/2;
|
||||||
|
|
||||||
|
normals[currentIndex*3] = 0;
|
||||||
|
normals[currentIndex*3+1] = 0;
|
||||||
|
normals[currentIndex*3+2] = 1;
|
||||||
|
|
||||||
|
textureCoords[currentIndex *2] = (float) circlePoint / radialSamples;
|
||||||
|
textureCoords[currentIndex *2 +1] = (bottomRadius + height) / (bottomRadius + height + topRadius);
|
||||||
|
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the centers of the caps.
|
||||||
|
vertices[currentIndex*3] = 0;
|
||||||
|
vertices[currentIndex*3 +1] = 0;
|
||||||
|
vertices[currentIndex*3 +2] = -height/2;
|
||||||
|
|
||||||
|
normals[currentIndex*3] = 0;
|
||||||
|
normals[currentIndex*3+1] = 0;
|
||||||
|
normals[currentIndex*3+2] = -1;
|
||||||
|
|
||||||
|
textureCoords[currentIndex *2] = 0.5f;
|
||||||
|
textureCoords[currentIndex *2+1] = 0f;
|
||||||
|
|
||||||
|
currentIndex++;
|
||||||
|
|
||||||
|
vertices[currentIndex*3] = 0;
|
||||||
|
vertices[currentIndex*3 +1] = 0;
|
||||||
|
vertices[currentIndex*3 +2] = height/2;
|
||||||
|
|
||||||
|
normals[currentIndex*3] = 0;
|
||||||
|
normals[currentIndex*3+1] = 0;
|
||||||
|
normals[currentIndex*3+2] = 1;
|
||||||
|
|
||||||
|
textureCoords[currentIndex *2] = 0.5f;
|
||||||
|
textureCoords[currentIndex *2+1] = 1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (closed) {
|
// Add the triangles indexes.
|
||||||
pb.put(0).put(0).put(-halfHeight); // bottom center
|
short[] indices = new short[trianglesCount * 3];
|
||||||
nb.put(0).put(0).put(-1 * (inverted ? -1 : 1));
|
currentIndex = 0;
|
||||||
tb.put(0.5f).put(0);
|
for (short axisSample = 0; axisSample < axisSamples - 1; axisSample++)
|
||||||
pb.put(0).put(0).put(halfHeight); // top center
|
{
|
||||||
nb.put(0).put(0).put(1 * (inverted ? -1 : 1));
|
for (int circlePoint = 0; circlePoint < radialSamples; circlePoint++)
|
||||||
tb.put(0.5f).put(1);
|
{
|
||||||
}
|
indices[currentIndex++] = (short) (axisSample * (radialSamples + 1) + circlePoint);
|
||||||
|
indices[currentIndex++] = (short) (axisSample * (radialSamples + 1) + circlePoint + 1);
|
||||||
|
indices[currentIndex++] = (short) ((axisSample + 1) * (radialSamples + 1) + circlePoint);
|
||||||
|
|
||||||
IndexBuffer ib = getIndexBuffer();
|
indices[currentIndex++] = (short) ((axisSample + 1) * (radialSamples + 1) + circlePoint);
|
||||||
int index = 0;
|
indices[currentIndex++] = (short) (axisSample * (radialSamples + 1) + circlePoint + 1);
|
||||||
// Connectivity
|
indices[currentIndex++] = (short) ((axisSample + 1) * (radialSamples + 1) + circlePoint + 1);
|
||||||
for (int axisCount = 0, axisStart = 0; axisCount < axisSamples - 1; axisCount++) {
|
}
|
||||||
int i0 = axisStart;
|
}
|
||||||
int i1 = i0 + 1;
|
// Add caps if needed.
|
||||||
axisStart += radialSamples + 1;
|
if(closed)
|
||||||
int i2 = axisStart;
|
{
|
||||||
int i3 = i2 + 1;
|
short bottomCapIndex = (short) (verticesCount - 2);
|
||||||
for (int i = 0; i < radialSamples; i++) {
|
short topCapIndex = (short) (verticesCount - 1);
|
||||||
if (closed && axisCount == 0) {
|
|
||||||
if (!inverted) {
|
int bottomRowOffset = (axisSamples) * (radialSamples +1 );
|
||||||
ib.put(index++, i0++);
|
int topRowOffset = (axisSamples+1) * (radialSamples +1 );
|
||||||
ib.put(index++, vertCount - 2);
|
|
||||||
ib.put(index++, i1++);
|
for (int circlePoint = 0; circlePoint < radialSamples; circlePoint++)
|
||||||
} else {
|
{
|
||||||
ib.put(index++, i0++);
|
indices[currentIndex++] = (short) (bottomRowOffset + circlePoint +1);
|
||||||
ib.put(index++, i1++);
|
indices[currentIndex++] = (short) (bottomRowOffset + circlePoint);
|
||||||
ib.put(index++, vertCount - 2);
|
indices[currentIndex++] = bottomCapIndex;
|
||||||
}
|
|
||||||
} else if (closed && axisCount == axisSamples - 2) {
|
|
||||||
ib.put(index++, i2++);
|
indices[currentIndex++] = (short) (topRowOffset + circlePoint);
|
||||||
ib.put(index++, inverted ? vertCount - 1 : i3++);
|
indices[currentIndex++] = (short) (topRowOffset + circlePoint +1);
|
||||||
ib.put(index++, inverted ? i3++ : vertCount - 1);
|
indices[currentIndex++] = topCapIndex;
|
||||||
} else {
|
}
|
||||||
ib.put(index++, i0++);
|
}
|
||||||
ib.put(index++, inverted ? i2 : i1);
|
|
||||||
ib.put(index++, inverted ? i1 : i2);
|
// If inverted, the triangles and normals are all reverted.
|
||||||
ib.put(index++, i1++);
|
if (inverted)
|
||||||
ib.put(index++, inverted ? i2++ : i3++);
|
{
|
||||||
ib.put(index++, inverted ? i3++ : i2++);
|
for (int i = 0; i < indices.length / 2; i++)
|
||||||
}
|
{
|
||||||
}
|
short temp = indices[i];
|
||||||
}
|
indices[i] = indices[indices.length - 1 - i];
|
||||||
|
indices[indices.length - 1 - i] = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = 0; i< normals.length; i++)
|
||||||
|
{
|
||||||
|
normals[i] = -normals[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in the buffers.
|
||||||
|
setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(vertices));
|
||||||
|
setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(normals));
|
||||||
|
setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(textureCoords));
|
||||||
|
setBuffer(Type.Index, 3, BufferUtils.createShortBuffer(indices));
|
||||||
|
|
||||||
updateBound();
|
updateBound();
|
||||||
setStatic();
|
setStatic();
|
||||||
@ -418,6 +461,4 @@ public class Cylinder extends Mesh {
|
|||||||
capsule.write(closed, "closed", false);
|
capsule.write(closed, "closed", false);
|
||||||
capsule.write(inverted, "inverted", false);
|
capsule.write(inverted, "inverted", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user