mirror of
https://github.com/jhasse/poly2tri.git
synced 2024-11-30 01:03:30 +01:00
CDT work; removed manual point drawing; added model; fixed bugs
This commit is contained in:
parent
b31d115774
commit
dc70f1ca78
@ -236,9 +236,9 @@
|
||||
7.85981 -0.78467
|
||||
7.79516 -0.409667
|
||||
7.49774 -0.151043
|
||||
7.84688.042924
|
||||
8.23481.314479
|
||||
8.64861.702414
|
||||
7.84688 0.042924
|
||||
8.23481 0.314479
|
||||
8.64861 0.702414
|
||||
8.70034 1.09035
|
||||
8.41585 1.42656
|
||||
8.11843 1.62053
|
||||
|
10
data/star.dat
Normal file
10
data/star.dat
Normal file
@ -0,0 +1,10 @@
|
||||
350 75
|
||||
379 161
|
||||
469 161
|
||||
397 215
|
||||
423 301
|
||||
350 250
|
||||
277 301
|
||||
303 215
|
||||
231 161
|
||||
321 161
|
16
data/strange.dat
Normal file
16
data/strange.dat
Normal file
@ -0,0 +1,16 @@
|
||||
400 472
|
||||
500 392
|
||||
520 272
|
||||
460 232
|
||||
580 212
|
||||
480 152
|
||||
360 172
|
||||
360 52
|
||||
300 112
|
||||
200 32
|
||||
120 92
|
||||
200 72
|
||||
340 272
|
||||
208 212
|
||||
180 352
|
||||
300 312
|
@ -39,6 +39,7 @@ import scala.io.Source
|
||||
import seidel.Triangulator
|
||||
import shapes.{Segment, Point, Triangle}
|
||||
import earClip.EarClip
|
||||
import cdt.CDT
|
||||
|
||||
// TODO: Lots of documentation!
|
||||
|
||||
@ -73,6 +74,11 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
||||
|
||||
val nazcaMonkey = "data/nazca_monkey.dat"
|
||||
val bird = "data/bird.dat"
|
||||
val snake = "data/i.snake"
|
||||
val star = "data/star.dat"
|
||||
val strange = "data/strange.dat"
|
||||
val i18 = "data/i.18"
|
||||
|
||||
var currentModel = nazcaMonkey
|
||||
|
||||
def init(container: GameContainer) {
|
||||
@ -171,9 +177,10 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
||||
if(c == 'm') drawMap = !drawMap
|
||||
if(c == '1') {currentModel = nazcaMonkey; selectModel}
|
||||
if(c == '2') {currentModel = bird; selectModel}
|
||||
if(c == '3') {poly; earClipPoly}
|
||||
if(c == '4') snake
|
||||
if(c == '5') star
|
||||
if(c == '3') {currentModel = strange; selectModel}
|
||||
if(c == '4') {currentModel = snake; selectModel}
|
||||
if(c == '5') {currentModel = star; selectModel}
|
||||
if(c == '6') {currentModel = i18; selectModel}
|
||||
if(c == 's') drawSegs = !drawSegs
|
||||
if(c == 'e') {drawEarClip = !drawEarClip; selectModel}
|
||||
if(c == 'h') {hertelMehlhorn = !hertelMehlhorn; selectModel}
|
||||
@ -185,147 +192,26 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
||||
loadModel(nazcaMonkey, 4.5f, Point(400, 300), 1500)
|
||||
case "data/bird.dat" =>
|
||||
loadModel(bird, 25f, Point(400, 300), 350)
|
||||
case "data/i.snake" =>
|
||||
loadModel(snake, 10f, Point(600, 300), 10)
|
||||
case "data/star.dat" =>
|
||||
loadModel(star, -1f, Point(0f, 0f), 10)
|
||||
case "data/strange.dat" =>
|
||||
loadModel(strange, -1f, Point(0f, 0f), 15)
|
||||
case "data/i.18" =>
|
||||
loadModel(i18, 15f, Point(500f, 400f), 20)
|
||||
case _ =>
|
||||
assert(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Test #1
|
||||
def poly {
|
||||
|
||||
val p1 = Point(400,472)
|
||||
val p2 = Point(500,392)
|
||||
val p3 = Point(520,272)
|
||||
val p4 = Point(460,232)
|
||||
val p5 = Point(580,212)
|
||||
val p6 = Point(480,152)
|
||||
val p7 = Point(360,172)
|
||||
val p8 = Point(360,52)
|
||||
val p9 = Point(300,112)
|
||||
val p10 = Point(200,32)
|
||||
val p11 = Point(120,92)
|
||||
val p12 = Point(200,72)
|
||||
val p13 = Point(340,272)
|
||||
val p14 = Point(208,212)
|
||||
val p15 = Point(180,352)
|
||||
val p16 = Point(300,312)
|
||||
|
||||
segments = new ArrayBuffer[Segment]
|
||||
segments += new Segment(p16, p1)
|
||||
segments += new Segment(p9, p10)
|
||||
segments += new Segment(p13, p14)
|
||||
segments += new Segment(p5, p6)
|
||||
segments += new Segment(p2, p3)
|
||||
segments += new Segment(p1, p2)
|
||||
segments += new Segment(p4, p5)
|
||||
segments += new Segment(p7, p8)
|
||||
segments += new Segment(p8, p9)
|
||||
segments += new Segment(p10, p11)
|
||||
segments += new Segment(p11, p12)
|
||||
segments += new Segment(p12, p13)
|
||||
segments += new Segment(p3, p4)
|
||||
segments += new Segment(p15, p16)
|
||||
segments += new Segment(p14, p15)
|
||||
segments += new Segment(p6, p7)
|
||||
|
||||
tesselator = new Triangulator(segments)
|
||||
tesselator.buildTriangles = hertelMehlhorn
|
||||
|
||||
val t1 = System.nanoTime
|
||||
tesselator process
|
||||
val t2 = System.nanoTime
|
||||
println
|
||||
println("**Poly1**")
|
||||
println("Poly2Tri total (ms) = " + (t2-t1)*1e-6)
|
||||
|
||||
}
|
||||
|
||||
def star {
|
||||
|
||||
val p1 = Point(350,75)
|
||||
val p2 = Point(379,161)
|
||||
val p3 = Point(469,161)
|
||||
val p4 = Point(397,215)
|
||||
val p5 = Point(423,301)
|
||||
val p6 = Point(350,250)
|
||||
val p7 = Point(277,301)
|
||||
val p8 = Point(303,215)
|
||||
val p9 = Point(231,161)
|
||||
val p10 = Point(321,161)
|
||||
|
||||
segments = new ArrayBuffer[Segment]
|
||||
segments += new Segment(p1, p2)
|
||||
segments += new Segment(p2, p3)
|
||||
segments += new Segment(p3, p4)
|
||||
segments += new Segment(p4, p5)
|
||||
segments += new Segment(p5, p6)
|
||||
segments += new Segment(p6, p7)
|
||||
segments += new Segment(p7, p8)
|
||||
segments += new Segment(p8, p9)
|
||||
segments += new Segment(p9, p10)
|
||||
segments += new Segment(p10, p1)
|
||||
|
||||
tesselator = new Triangulator(segments)
|
||||
tesselator.buildTriangles = hertelMehlhorn
|
||||
|
||||
val t1 = System.nanoTime
|
||||
tesselator process
|
||||
val t2 = System.nanoTime
|
||||
println
|
||||
println("**Star**")
|
||||
println("Poly2Tri total (ms) = " + (t2-t1)*1e-6)
|
||||
}
|
||||
|
||||
// Test #2
|
||||
def snake {
|
||||
|
||||
val scale = 10.0f
|
||||
val displace = 100
|
||||
val p1 = Point(10,1)*scale+displace
|
||||
val p2 = Point(20,10)*scale+displace
|
||||
val p3 = Point(30,1)*scale+displace
|
||||
val p4 = Point(40,10)*scale+displace
|
||||
val p5 = Point(50,1)*scale+displace
|
||||
val p6 = Point(50,10)*scale+displace
|
||||
val p7 = Point(40,20)*scale+displace
|
||||
val p8 = Point(30,10)*scale+displace
|
||||
val p9 = Point(20,20)*scale+displace
|
||||
val p10 = Point(10,10)*scale+displace
|
||||
val p11 = Point(1,20)*scale+displace
|
||||
val p12 = Point(1,10)*scale+displace
|
||||
|
||||
segments = new ArrayBuffer[Segment]
|
||||
segments += new Segment(p1, p2)
|
||||
segments += new Segment(p2, p3)
|
||||
segments += new Segment(p3, p4)
|
||||
segments += new Segment(p4, p5)
|
||||
segments += new Segment(p5, p6)
|
||||
segments += new Segment(p6, p7)
|
||||
segments += new Segment(p7, p8)
|
||||
segments += new Segment(p8, p9)
|
||||
segments += new Segment(p9, p10)
|
||||
segments += new Segment(p10, p11)
|
||||
segments += new Segment(p11, p12)
|
||||
segments += new Segment(p12, p1)
|
||||
|
||||
tesselator = new Triangulator(segments)
|
||||
tesselator.buildTriangles = hertelMehlhorn
|
||||
|
||||
val t1 = System.nanoTime
|
||||
tesselator process
|
||||
val t2 = System.nanoTime
|
||||
println
|
||||
println("**Snake**")
|
||||
println("Poly2Tri total (ms) = " + (t2-t1)*1e-6)
|
||||
|
||||
}
|
||||
|
||||
def loadModel(model: String, scale: Float, center: Point, maxTriangles: Int) {
|
||||
|
||||
println("*** " + model + " ***")
|
||||
|
||||
polyX = new ArrayBuffer[Float]
|
||||
polyY = new ArrayBuffer[Float]
|
||||
val points = new ArrayBuffer[Point]
|
||||
|
||||
val angle = Math.Pi
|
||||
for (line <- Source.fromFile(model).getLines) {
|
||||
@ -337,24 +223,20 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
||||
// Transform the shape
|
||||
polyX += (Math.cos(angle)*x - Math.sin(angle)*y).toFloat * scale + center.x
|
||||
polyY += (Math.sin(angle)*x + Math.cos(angle)*y).toFloat * scale + center.y
|
||||
points += new Point(polyX.last, polyY.last)
|
||||
} else {
|
||||
throw new Exception("Bad input file")
|
||||
}
|
||||
}
|
||||
|
||||
points.foreach(println)
|
||||
segments = new ArrayBuffer[Segment]
|
||||
|
||||
var i = 0
|
||||
val numPoints = polyX.size
|
||||
while(i < polyX.size-2) {
|
||||
val p1 = new Point(polyX(i), polyY(i))
|
||||
val p2 = new Point(polyX(i+1), polyY(i+1))
|
||||
segments += new Segment(p1, p2)
|
||||
i += 1
|
||||
}
|
||||
|
||||
for(i <- 0 until polyX.size-1)
|
||||
segments += new Segment(points(i), points(i+1))
|
||||
// Connect the end points
|
||||
val p1 = segments(0).p
|
||||
val p2 = segments(segments.length-1).q
|
||||
segments += new Segment(p2, p1)
|
||||
segments += new Segment(points.first, points.last)
|
||||
|
||||
val cdt = CDT.init(points)
|
||||
println(cdt.points.size + "," + cdt.segments.size)
|
||||
|
||||
println("Number of points = " + polyX.size)
|
||||
println
|
||||
@ -379,8 +261,14 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
||||
earClipResults = new Array[poly2tri.earClip.Triangle](maxTriangles)
|
||||
|
||||
for(i <- 0 until earClipResults.size) earClipResults(i) = new poly2tri.earClip.Triangle
|
||||
val xVerts = polyX.toArray.reverse
|
||||
val yVerts = polyY.toArray.reverse
|
||||
|
||||
var xVerts = polyX.toArray
|
||||
var yVerts = polyY.toArray
|
||||
|
||||
if(currentModel != strange) {
|
||||
xVerts = xVerts.reverse
|
||||
yVerts = yVerts.reverse
|
||||
}
|
||||
|
||||
val t1 = System.nanoTime
|
||||
earClip.triangulatePolygon(xVerts, yVerts, xVerts.size, earClipResults)
|
||||
@ -392,54 +280,4 @@ class Poly2TriDemo extends BasicGame("Poly2Tri") {
|
||||
}
|
||||
}
|
||||
|
||||
def earClipPoly {
|
||||
|
||||
val polyX = Array(400f, 500f, 520f, 460f, 580f, 480f, 360f, 360f, 300f, 200f, 120f, 200f, 340f, 208f, 180f, 300f)
|
||||
val polyY = Array(472f, 392f, 272f, 232f, 212f, 152f, 172f, 52f, 112f, 32f, 92f, 72f, 272f, 212f, 352f, 312f)
|
||||
|
||||
val earClipResults = new Array[poly2tri.earClip.Triangle](14)
|
||||
for(i <- 0 until earClipResults.size) earClipResults(i) = new poly2tri.earClip.Triangle
|
||||
val t1 = System.nanoTime
|
||||
earClip.triangulatePolygon(polyX, polyY, polyX.size, earClipResults)
|
||||
val t2 = System.nanoTime
|
||||
println("Earclip total (ms) = " + (t2-t1)*1e-6)
|
||||
}
|
||||
|
||||
def earClipSnake {
|
||||
|
||||
val polyX = Array(200f, 300f, 400f, 500f, 600f, 600f, 500f, 400f, 300f, 200f, 110f, 110f)
|
||||
val polyY = Array(110f, 200f, 110f, 200f, 110f, 200f, 300f, 200f, 300f, 200f, 300f, 200f)
|
||||
|
||||
val earClipResults = new Array[poly2tri.earClip.Triangle](14)
|
||||
for(i <- 0 until earClipResults.size) earClipResults(i) = new poly2tri.earClip.Triangle
|
||||
val t1 = System.nanoTime
|
||||
earClip.triangulatePolygon(polyX, polyY, polyX.size, earClipResults)
|
||||
val t2 = System.nanoTime
|
||||
println("Earclip total (ms) = " + (t2-t1)*1e-6)
|
||||
}
|
||||
|
||||
def earClipStar {
|
||||
|
||||
val p1 = Point(350,75)
|
||||
val p2 = Point(379,161)
|
||||
val p3 = Point(469,161)
|
||||
val p4 = Point(397,215)
|
||||
val p5 = Point(423,301)
|
||||
val p6 = Point(350,250)
|
||||
val p7 = Point(277,301)
|
||||
val p8 = Point(303,215)
|
||||
val p9 = Point(231,161)
|
||||
val p10 = Point(321,161)
|
||||
|
||||
val polyX = Array(350f, 379f, 469f, 397f, 423f, 350f, 277f, 303f, 231f, 321f)
|
||||
val polyY = Array(75f, 161f, 161f, 215f, 301f, 250f, 301f,215f, 161f, 161f)
|
||||
|
||||
val earClipResults = new Array[poly2tri.earClip.Triangle](14)
|
||||
for(i <- 0 until earClipResults.size) earClipResults(i) = new poly2tri.earClip.Triangle
|
||||
val t1 = System.nanoTime
|
||||
earClip.triangulatePolygon(polyX, polyY, polyX.size, earClipResults)
|
||||
val t2 = System.nanoTime
|
||||
println("Earclip total (ms) = " + (t2-t1)*1e-6)
|
||||
}
|
||||
|
||||
}
|
@ -40,69 +40,73 @@ import utils.Util
|
||||
* See: Domiter, V. and Žalik, B.(2008)'Sweep-line algorithm for constrained Delaunay triangulation',
|
||||
* International Journal of Geographical Information Science,22:4,449 — 462
|
||||
*/
|
||||
class CDT(segments: ArrayBuffer[Segment]) {
|
||||
object CDT {
|
||||
|
||||
// Inital triangle factor
|
||||
val ALPHA = 0.3f
|
||||
|
||||
def init(points: ArrayBuffer[Point]): CDT = {
|
||||
|
||||
var xmax, xmin = 0f
|
||||
var ymax, ymin = 0f
|
||||
|
||||
// Calculate bounds
|
||||
for(i <- 0 until points.size) {
|
||||
points(i) = shearTransform(points(i))
|
||||
val p = points(i)
|
||||
if(p.x > xmax) xmax = p.x
|
||||
if(p.x < xmin) xmin = p.x
|
||||
if(p.y > ymax) ymax = p.x
|
||||
if(p.y < ymin) ymin = p.x
|
||||
}
|
||||
|
||||
val deltaX = ALPHA * (xmax - xmin)
|
||||
val deltaY = ALPHA * (ymax - ymin)
|
||||
val p1 = Point(xmin - deltaX, ymin - deltaY)
|
||||
val p2 = Point(xmax - deltaX, ymin - deltaY)
|
||||
|
||||
val initialTriangle = new Triangle(Array(p2, points(0), p1), null)
|
||||
val segments = initSegments(points)
|
||||
val sortedPoints = pointSort(points)
|
||||
new CDT(sortedPoints, segments, initialTriangle)
|
||||
}
|
||||
|
||||
// Create segments and connect end points
|
||||
private def initSegments(points: ArrayBuffer[Point]): List[Segment] = {
|
||||
var segments = List[Segment]()
|
||||
for(i <- 0 until points.size-1)
|
||||
segments = new Segment(points(i), points(i+1)) :: segments
|
||||
segments = new Segment(points.first, points.last) :: segments
|
||||
segments
|
||||
}
|
||||
|
||||
// Insertion sort is one of the fastest algorithms for sorting arrays containing
|
||||
// fewer than ten elements, or for lists that are already mostly sorted.
|
||||
// Merge sort: O(n log n)
|
||||
private def pointSort(pts: ArrayBuffer[Point]): List[Point] = {
|
||||
if(pts.size < 10)
|
||||
Util.insertSort((p1: Point, p2: Point) => p1 > p2)(pts).toList
|
||||
else
|
||||
Util.msort((p1: Point, p2: Point) => p1 > p2)(pts.toList)
|
||||
}
|
||||
|
||||
// Prevents any two distinct endpoints from lying on a common horizontal line, and avoiding
|
||||
// the degenerate case. See Mark de Berg et al, Chapter 6.3
|
||||
//val SHEER = 0.0001f
|
||||
private def shearTransform(point: Point) = Point(point.x, point.y + point.x * 0.0001f)
|
||||
|
||||
}
|
||||
|
||||
class CDT(val points: List[Point], val segments: List[Segment], initialTriangle: Triangle) {
|
||||
|
||||
// The initial triangle
|
||||
var initialTriangle: Triangle = null
|
||||
// The point list
|
||||
val points = init
|
||||
// The triangle mesh
|
||||
val mesh = new Mesh(initialTriangle)
|
||||
|
||||
// Used to compute inital triangle
|
||||
private val ALPHA = 0.3f
|
||||
|
||||
// Sweep points; build mesh
|
||||
sweep
|
||||
// Finalize triangulation
|
||||
finalization
|
||||
|
||||
// Initialize and sort point list
|
||||
private def init: List[Point] = {
|
||||
|
||||
var xmax, xmin = segments(0).p.x
|
||||
var ymax, ymin = segments(0).p.y
|
||||
val pts = new ArrayBuffer[Point]
|
||||
|
||||
for(i <- 0 until segments.size) {
|
||||
|
||||
val p = segments(i).p
|
||||
val q = segments(i).q
|
||||
|
||||
if(p.x > xmax) xmax = p.x
|
||||
if(q.x > xmax) xmax = q.x
|
||||
if(p.x < xmin) xmin = p.x
|
||||
if(q.x < xmin) xmin = q.x
|
||||
|
||||
if(p.y > ymax) ymax = p.x
|
||||
if(q.y > ymax) ymax = q.x
|
||||
if(p.y < ymin) ymin = p.x
|
||||
if(q.y < ymin) ymin = q.x
|
||||
|
||||
pts += shearTransform(p)
|
||||
pts += shearTransform(q)
|
||||
}
|
||||
|
||||
var points: List[Point] = null
|
||||
|
||||
if(pts.size < 10)
|
||||
// Insertion sort is one of the fastest algorithms for sorting arrays containing
|
||||
// fewer than ten elements, or for lists that are already mostly sorted.
|
||||
points = Util.insertSort((p1: Point, p2: Point) => p1 > p2)(pts).toList
|
||||
else
|
||||
// Merge sort: O(n log n)
|
||||
points = Util.msort((p1: Point, p2: Point) => p1 > p2)(pts.toList)
|
||||
|
||||
val deltaX = ALPHA * (xmax - xmin)
|
||||
val deltaY = ALPHA * (ymax - ymin)
|
||||
|
||||
val p1 = Point(xmin - deltaX, ymin - deltaY)
|
||||
val p2 = Point(xmax - deltaX, ymin - deltaY)
|
||||
|
||||
initialTriangle = new Triangle(Array(p2, points(0), p1), null)
|
||||
points
|
||||
}
|
||||
|
||||
// Implement sweep-line paradigm
|
||||
private def sweep {
|
||||
}
|
||||
@ -110,8 +114,5 @@ class CDT(segments: ArrayBuffer[Segment]) {
|
||||
private def finalization {
|
||||
}
|
||||
|
||||
// Prevents any two distinct endpoints from lying on a common horizontal line, and avoiding
|
||||
// the degenerate case. See Mark de Berg et al, Chapter 6.3
|
||||
//val SHEER = 0.0001f
|
||||
def shearTransform(point: Point) = Point(point.x, point.y + point.x * 0.0001f)
|
||||
def triangles = mesh.map
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ package org.poly2tri.cdt
|
||||
|
||||
import scala.collection.mutable.HashSet
|
||||
|
||||
import shapes.Point
|
||||
import shapes.{Point, Triangle}
|
||||
|
||||
class Mesh(initialTriangle: Triangle) {
|
||||
|
||||
|
@ -30,18 +30,16 @@
|
||||
*/
|
||||
package org.poly2tri.shapes
|
||||
|
||||
object Event extends Enumeration {
|
||||
val point, edge = Value
|
||||
}
|
||||
|
||||
case class Point(val x: Float, val y: Float) {
|
||||
|
||||
// Pointers to next and previous points in Monontone Mountain
|
||||
var next, prev: Point = null
|
||||
// The setment this point belongs to
|
||||
var segment: Segment = null
|
||||
// Point type for CDT
|
||||
var eventType: Event.Value = _
|
||||
// Edge event pointer for CDT
|
||||
var eEvent: Segment = null
|
||||
// Point event pointer for CDT
|
||||
var pEvent: Segment = null
|
||||
|
||||
@inline def -(p: Point) = Point(x - p.x, y - p.y)
|
||||
@inline def +(p: Point) = Point(x + p.x, y + p.y)
|
||||
|
@ -35,9 +35,6 @@ import scala.collection.mutable.{ArrayBuffer}
|
||||
// Represents a simple polygon's edge
|
||||
class Segment(var p: Point, var q: Point) {
|
||||
|
||||
p.segment = this
|
||||
q.segment = this
|
||||
|
||||
// Pointers used for building trapezoidal map
|
||||
var above, below: Trapezoid = null
|
||||
// Montone mountain points
|
||||
@ -53,27 +50,4 @@ class Segment(var p: Point, var q: Point) {
|
||||
// Determines if this segment lies below the given point
|
||||
def < (point: Point) = (Math.floor(point.y) > Math.floor(slope * point.x + b))
|
||||
|
||||
// Assign point type for CDT
|
||||
if(p.y > q.y)
|
||||
pEdge
|
||||
else if (p.y < q.y)
|
||||
pPoint
|
||||
else
|
||||
if(p.x < q.x)
|
||||
pPoint
|
||||
else if (p.x > q.x)
|
||||
pEdge
|
||||
else
|
||||
throw new Exception("Invalid segment")
|
||||
|
||||
private def pPoint {
|
||||
p.eventType = Event.point
|
||||
q.eventType = Event.edge
|
||||
}
|
||||
|
||||
private def pEdge {
|
||||
p.eventType = Event.edge
|
||||
q.eventType = Event.point
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,8 +30,6 @@
|
||||
*/
|
||||
package org.poly2tri.shapes
|
||||
|
||||
import shapes.Point
|
||||
|
||||
// Triangle-based data structures are know to have better performance than quad-edge structures
|
||||
// See: J. Shewchuk, "Triangle: Engineering a 2D Quality Mesh Generator and Delaunay Triangulator"
|
||||
// "Triangulations in CGAL"
|
||||
|
Loading…
Reference in New Issue
Block a user