Created
January 23, 2018 13:14
-
-
Save dniHze/bb67148fff801b69864f0cd959a6440b to your computer and use it in GitHub Desktop.
How to pack circles in rectange in Kotlin.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.* | |
import kotlin.math.* | |
data class Point(var x: Double, var y: Double) { | |
fun vect(p: Point) = Point(p.x - x, p.y - y) | |
fun norm() = sqrt(x.pow(2) + y.pow(2)) | |
fun dist(p: Point) = vect(p).norm() | |
fun add(p: Point) = Point(x + p.x, y + p.y) | |
fun mult(a: Double) = Point(x * a, y * a) | |
} | |
data class Circle(var radius: Double, val center: Point) { | |
fun distance(c: Circle) = center.dist(c.center) - radius - c.radius | |
} | |
class Packer<out T : Popular>( | |
private val circles: List<T>, | |
private val ratio: Double | |
) { | |
private fun compute(surface: Double): List<DistributedCircle<T>> { | |
val boundingR = sqrt(surface) * 100 | |
val w = sqrt(surface * ratio) | |
val h = w / ratio | |
fun inRect(radius: Double, center: Point): Boolean { | |
return when { | |
center.x - radius < -w / 2 -> false | |
center.x + radius > w / 2 -> false | |
center.y - radius < -h / 2 -> false | |
center.y + radius > h / 2 -> false | |
else -> true | |
} | |
} | |
fun boundingCircle(x0: Double, y0: Double, x1: Double, y1: Double): Circle { | |
val xm = abs((x1 - x0) * w) | |
val ym = abs((y1 - y0) * h) | |
val m = max(xm, ym) | |
val theta = asin(m / 4 / boundingR) | |
val r = boundingR * (cos(theta)) | |
return Circle(boundingR, Point(r * (y0 - y1) / 2 + (x0 + x1) * w / 4, | |
r * (x1 - x0) / 2 + (y0 + y1) * h / 4)) | |
} | |
fun corner(radius: Double, c1: Circle, c2: Circle): List<Circle> { | |
var u = c1.center.vect(c2.center) | |
val A = u.norm() | |
if (A == .0) { | |
return listOf() | |
} | |
u = u.mult(1 / A) | |
val B = c1.radius + radius | |
val C = c2.radius + radius | |
if (A > (B + C)) { | |
return listOf() | |
} | |
val x = (A + (B.pow(2) - C.pow(2)) / A) / 2 | |
val y = sqrt(B.pow(2) - x.pow(2)) | |
val base = c1.center.add(u.mult(x)) | |
val results = mutableListOf<Circle>() | |
val p1 = Point(base.x - u.y * y, base.y + u.x * y) | |
val p2 = Point(base.x + u.y * y, base.y - u.x * y) | |
if (inRect(radius, p1)) results.add(Circle(radius, p1)) | |
if (inRect(radius, p2)) results.add(Circle(radius, p2)) | |
return results | |
} | |
val default = circles.first() | |
val placed = mutableListOf<DistributedCircle<T>>( | |
DistributedCircle(boundingCircle(1.0, 1.0, 1.0, -1.0), default), | |
DistributedCircle(boundingCircle(1.0, -1.0, -1.0, -1.0), default), | |
DistributedCircle(boundingCircle(1.0, -1.0, -1.0, 1.0), default), | |
DistributedCircle(boundingCircle(-1.0, 1.0, 1.0, 1.0), default) | |
) | |
val unplaced = circles.toMutableList() | |
while (unplaced.isNotEmpty()) { | |
val lambda = Array(unplaced.size, { | |
-1e10 | |
}) | |
val circle = Array(unplaced.size, { | |
Circle(.0, Point(.0, .0)) | |
}) | |
for (i in unplaced.indices) { | |
var lambdaMin = 1e10 | |
for (j in placed.indices) | |
for (k in (j + 1) until (placed.size - 1)) { | |
val corners = corner(unplaced[i].getPopularityPercentage(), placed[j].circle, placed[k].circle) | |
for (c in corners.indices) { | |
var dMin = 1e10 | |
var isBroken = false | |
for (l in placed.indices) { | |
if (l == j || l == k) | |
continue | |
val d = placed[l].circle.distance(corners[c]) | |
if (d < 0) { | |
isBroken = true | |
break | |
} | |
if (d < dMin) { | |
dMin = d | |
} | |
} | |
if (isBroken) continue | |
if (dMin < lambdaMin) { | |
lambdaMin = dMin | |
lambda[i] = 1 - dMin / unplaced[i].getPopularityPercentage() | |
circle[i] = corners[c] | |
} | |
} | |
} | |
} | |
var lambdaMax = -1e10 | |
var iMax = -1 | |
for (i in unplaced.indices) { | |
if (lambda[i] > lambdaMax) { | |
lambdaMax = lambda[i] | |
iMax = i | |
} | |
} | |
if (iMax == -1) break | |
val value = unplaced[iMax] | |
unplaced.removeAt(iMax) | |
placed.add(DistributedCircle(circle[iMax], value)) | |
} | |
return placed.drop(4) | |
} | |
private fun solve(): List<DistributedCircle<T>> { | |
var surface = .0 | |
circles.forEach { | |
surface += PI * it.getPopularityPercentage().pow(2) | |
} | |
val limit = surface / 1000 | |
var step = surface / 2 | |
val result = mutableListOf<DistributedCircle<T>>() | |
while (step > limit) { | |
val placement = compute(surface) | |
if (placement.size != circles.size) { | |
surface += step | |
} else { | |
result.clear() | |
result.addAll(placement) | |
surface -= step | |
} | |
step /= 2 | |
} | |
return result | |
} | |
fun pack(): List<DistributedCircle<T>> { | |
return solve() | |
} | |
} | |
interface Popular { | |
fun getPopularityPercentage(): Double // 0.0..100.0 | |
} | |
data class DistributedCircle<out T : Popular> constructor( | |
val circle: Circle, | |
val data: T | |
) | |
class Distributor<T : Popular>(val height: Double, val width: Double) { | |
fun distibute(data: Array<T>): Array<DistributedCircle<T>> { | |
return distibute(data.toList()) | |
} | |
fun distibute(data: Collection<T>): Array<DistributedCircle<T>> { | |
val ratio = width / height | |
val packer = Packer(data.toList(), ratio) | |
val packedCircles = packer.pack() | |
// get sizes and bounds. | |
val mappedCenters = packedCircles.map { it.circle } | |
val minX = mappedCenters.map { it.center.x - it.radius }.min() ?: wtf() | |
val maxX = mappedCenters.map { it.center.x + it.radius }.max() ?: wtf() | |
val minY = mappedCenters.map { it.center.y - it.radius }.min() ?: wtf() | |
val maxY = mappedCenters.map { it.center.y + it.radius }.max() ?: wtf() | |
val dx = abs(minX) | |
val dy = abs(minY) | |
val w = abs(maxX) + abs(minX) | |
val h = abs(maxY) + abs(minY) | |
val scaleX = width / w | |
val scaleY = height / h | |
val finalScale = min(scaleX, scaleY) | |
packedCircles.forEach { | |
it.circle.center.x += dx | |
it.circle.center.y += dy | |
it.circle.center.x *= finalScale | |
it.circle.center.y *= finalScale | |
it.circle.radius *= finalScale | |
} | |
return packedCircles.toTypedArray() | |
} | |
private fun <K> wtf(): K = throw IllegalArgumentException() | |
} | |
data class Tag(private var popularity: Double = -1.0) : Popular { | |
init { | |
if (popularity < 0) { | |
popularity = Random().nextDouble() * 100.0 | |
} | |
} | |
override fun getPopularityPercentage() = popularity | |
} | |
fun main(args: Array<String>) { | |
val circles = 7 | |
val data = List(circles, { | |
Tag() | |
}) | |
data.forEach(::println) | |
println("---- ----") | |
val distributor = Distributor<Tag>(1920.0, 1080.0) | |
val distributedData = distributor.distibute(data).toList() | |
distributedData.forEach(::println) | |
println("---- ----") | |
distributedData.map { "(x - ${it.circle.center.x})^2 + (y - ${it.circle.center.y})^2 = ${it.circle.radius.pow(2)}" }.forEach(::println) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment