[orx-shapes] Extend circle inversion functionalities and update demos

This commit is contained in:
Edwin Jakobs
2025-08-20 21:38:58 +02:00
parent e17644a247
commit 307b3fc282
3 changed files with 138 additions and 11 deletions

View File

@@ -1,8 +1,14 @@
package org.openrndr.extra.shapes.primitives package org.openrndr.extra.shapes.primitives
import org.openrndr.math.GeometricPrimitive2D
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.shape.Circle import org.openrndr.shape.Circle
import org.openrndr.shape.Line2D
import org.openrndr.shape.LineSegment
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt import kotlin.math.sqrt
@@ -53,7 +59,7 @@ fun Circle.invert(point: Vector2): Vector2 {
* @return The inverted circle * @return The inverted circle
* @throws IllegalArgumentException if the circle to be inverted is centered at the center of the inverting circle * @throws IllegalArgumentException if the circle to be inverted is centered at the center of the inverting circle
*/ */
fun Circle.invert(circle: Circle): Circle { fun Circle.invert(circle: Circle): GeometricPrimitive2D {
// Vector from this circle's center to the other circle's center // Vector from this circle's center to the other circle's center
val v = circle.center - this.center val v = circle.center - this.center
@@ -73,8 +79,7 @@ fun Circle.invert(circle: Circle): Circle {
// Special case: the result would be a line, which we can't represent as a Circle // Special case: the result would be a line, which we can't represent as a Circle
// We'll approximate it as a very large circle // We'll approximate it as a very large circle
val direction = v.normalized val direction = v.normalized
val farPoint = this.center + direction * 1e6 return Line2D(this.center, direction)
return Circle(farPoint, 1e6)
} }
// Calculate power of the point (center of the inverting circle) with respect to the circle being inverted // Calculate power of the point (center of the inverting circle) with respect to the circle being inverted
@@ -101,7 +106,7 @@ fun Circle.invert(circle: Circle): Circle {
* @return The conformally inverted circle * @return The conformally inverted circle
* @throws IllegalArgumentException if the circle to be inverted is centered at the center of the inverting circle * @throws IllegalArgumentException if the circle to be inverted is centered at the center of the inverting circle
*/ */
fun Circle.invertConformal(circle: Circle): Circle { fun Circle.invertConformal(circle: Circle): GeometricPrimitive2D {
// Vector from this circle's center to the other circle's center // Vector from this circle's center to the other circle's center
val v = circle.center - this.center val v = circle.center - this.center
@@ -118,11 +123,8 @@ fun Circle.invertConformal(circle: Circle): Circle {
// Check if the circle to be inverted passes through the center of the inverting circle // Check if the circle to be inverted passes through the center of the inverting circle
if (abs(circle.radius - distance) < 1e-10) { if (abs(circle.radius - distance) < 1e-10) {
// Special case: the result would be a line, which we can't represent as a Circle
// We'll approximate it as a very large circle
val direction = v.normalized val direction = v.normalized
val farPoint = this.center + direction * 1e6 return Line2D(this.center, direction)
return Circle(farPoint, 1e6)
} }
// For conformal inversion that preserves tangency, we use the standard circle inversion formula // For conformal inversion that preserves tangency, we use the standard circle inversion formula
@@ -142,3 +144,59 @@ fun Circle.invertConformal(circle: Circle): Circle {
return Circle(newCenter, newRadius) return Circle(newCenter, newRadius)
} }
fun Circle.invert(segment: LineSegment): GeometricPrimitive2D {
val a = segment.start
val b = segment.end
val c = segment.position(0.5)
// Direction of the line (normalized)
val dir = (b - a)
val dirLen = dir.length
if (dirLen < 1e-10) {
// Degenerate segment: treat as a point inversion
return invert(a)
}
val u = dir / dirLen
// Foot of the perpendicular from circle center to the infinite line AB
val ao = center - a
val t = ao.dot(u)
val foot = a + u * t
val perpVec = center - foot
val dist = perpVec.length
// If the line passes through the center of inversion, it maps to a line
if (dist < 1e-10) {
val aInv = invert(a)
val bInv = invert(b)
return LineSegment(aInv, bInv)
}
// Inverse of a line not through the center is a circle passing through the center
val rPrime = (radius * radius) / (2.0 * dist)
val n = (foot - center).normalized // direction from center towards the line
val circleCenter = center + n * rPrime
// The circle radius equals rPrime (since it passes through the center)
val aInv = invert(a)
val bInv = invert(b)
val cInv = invert(c)
// Compute angles (in degrees) for the arc between inverted endpoints
val angleA = atan2(aInv.y - circleCenter.y, aInv.x - circleCenter.x) * 180.0 / kotlin.math.PI
val angleB = atan2(bInv.y - circleCenter.y, bInv.x - circleCenter.x) * 180.0 / kotlin.math.PI
val angleC = atan2(cInv.y - circleCenter.y, cInv.x - circleCenter.x) * 180.0 / kotlin.math.PI
var angle0 = min(angleA, angleB)
var angle1 = max(angleA, angleB)
if (angleC in angle0..angle1) {
angle1 -= 360.0
}
return Arc(circleCenter, rPrime, angle0, angle1).conjugate()
}

View File

@@ -25,7 +25,10 @@ fun main() = application {
drawer.clear(ColorRGBa.WHITE) drawer.clear(ColorRGBa.WHITE)
drawer.fill = ColorRGBa.BLACK drawer.fill = ColorRGBa.BLACK
drawer.stroke = null drawer.stroke = null
drawer.circle(mc.invertConformal(c)) val ci = mc.invertConformal(c)
when (ci) {
is Circle -> drawer.circle(ci)
}
break break
} }
} }
@@ -39,7 +42,10 @@ fun main() = application {
for (i in 0 until 10) { for (i in 0 until 10) {
val c = Circle(i * width / 10.0 + width / 20.0, j * height / 10.0 + height / 20.0, 36.0) val c = Circle(i * width / 10.0 + width / 20.0, j * height / 10.0 + height / 20.0, 36.0)
if (p !in c) { if (p !in c) {
drawer.circle(mc.invertConformal(c)) val ci = mc.invertConformal(c)
when (ci) {
is Circle -> drawer.circle(ci)
}
} }
} }
} }

View File

@@ -0,0 +1,63 @@
package primitives
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.isolated
import org.openrndr.extra.shapes.primitives.Arc
import org.openrndr.extra.shapes.primitives.grid
import org.openrndr.extra.shapes.primitives.invert
import org.openrndr.math.Polar
import org.openrndr.shape.Circle
import org.openrndr.shape.LineSegment
import kotlin.math.cos
import kotlin.math.sin
fun main() = application {
configure {
width = 720
height = 720
}
program {
extend {
val p = Polar(seconds * 36.0, 100.0).cartesian + drawer.bounds.center
val c = Circle(p, 180.0)
for (i in 0 until 10) {
val s = sin(seconds + i) * 0.25 + 0.25
drawer.fill = null
val ls = drawer.bounds.horizontal((i + 0.5) / 10.0).sub(0.5-s,0.5+s)
drawer.stroke = ColorRGBa.PINK
val cir = c.invert(ls)
when (cir) {
is Circle -> drawer.circle(cir)
is Arc -> drawer.contour(cir.contour)
is LineSegment -> drawer.lineSegment(cir)
else -> error("unsupported result")
}
}
drawer.isolated {
val pts = drawer.bounds.grid(10, 10).flatten().map {
c.invert(it.center)
}
drawer.fill = ColorRGBa.BLACK
drawer.stroke = null
drawer.circles(pts, 5.0)
}
for (i in 0 until 10) {
drawer.fill = null
val s = cos(seconds + i) * 0.25 + 0.25
val ls = drawer.bounds.vertical((i + 0.5) / 10.0).sub(0.5-s,0.5+s)
drawer.stroke = ColorRGBa.PINK
val cir = c.invert(ls)
when (cir) {
is Circle -> drawer.circle(cir)
is Arc -> drawer.contour(cir.contour)
is LineSegment -> drawer.lineSegment(cir)
else -> error("unsupported result")
}
}
}
}
}