[orx-shapes] Add circle inversion primitives and demo examples
This commit is contained in:
144
orx-shapes/src/commonMain/kotlin/primitives/CircleInversion.kt
Normal file
144
orx-shapes/src/commonMain/kotlin/primitives/CircleInversion.kt
Normal file
@@ -0,0 +1,144 @@
|
||||
package org.openrndr.extra.shapes.primitives
|
||||
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.shape.Circle
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sqrt
|
||||
|
||||
|
||||
/**
|
||||
* Performs circle inversion of a point.
|
||||
*
|
||||
* Circle inversion is a geometric transformation where a point is mapped to another point along the same ray from the center,
|
||||
* but at a distance that is inversely proportional to the original distance.
|
||||
*
|
||||
* The formula used is: P' = C + r²/|P-C|² * (P-C)
|
||||
* Where:
|
||||
* - P is the point to invert
|
||||
* - C is the center of the circle
|
||||
* - r is the radius of the circle
|
||||
* - P' is the inverted point
|
||||
*
|
||||
* @param point The point to invert
|
||||
* @return The inverted point
|
||||
*/
|
||||
fun Circle.invert(point: Vector2): Vector2 {
|
||||
// Vector from center to point
|
||||
val v = point - center
|
||||
|
||||
// Distance from center to point
|
||||
val distanceSquared = v.squaredLength
|
||||
|
||||
// If the point is at the center, we can't invert it
|
||||
if (distanceSquared < 1e-10) {
|
||||
throw IllegalArgumentException("Cannot invert a point at the center of the circle")
|
||||
}
|
||||
|
||||
// Calculate the inverted point
|
||||
val factor = (radius * radius) / distanceSquared
|
||||
return center + v * factor
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs circle inversion of another circle.
|
||||
*
|
||||
* Circle inversion maps a circle to another circle (or a line, which can be considered a circle with infinite radius).
|
||||
*
|
||||
* There are several cases:
|
||||
* 1. If the circle to be inverted passes through the center of the inverting circle, the result is a line
|
||||
* 2. If the circle to be inverted doesn't contain the center of the inverting circle, the result is another circle
|
||||
* 3. If the circle to be inverted contains the center of the inverting circle, the result is also a circle
|
||||
*
|
||||
* @param circle The circle to invert
|
||||
* @return The inverted circle
|
||||
* @throws IllegalArgumentException if the circle to be inverted is centered at the center of the inverting circle
|
||||
*/
|
||||
fun Circle.invert(circle: Circle): Circle {
|
||||
// Vector from this circle's center to the other circle's center
|
||||
val v = circle.center - this.center
|
||||
|
||||
// Distance between centers
|
||||
val distanceSquared = v.squaredLength
|
||||
|
||||
// If the circle to be inverted is centered at the center of the inverting circle, we can't invert it
|
||||
if (distanceSquared < 1e-10) {
|
||||
throw IllegalArgumentException("Cannot invert a circle centered at the center of the inverting circle")
|
||||
}
|
||||
|
||||
// Distance between centers
|
||||
val distance = sqrt(distanceSquared)
|
||||
|
||||
// Check if the circle to be inverted passes through the center of the inverting circle
|
||||
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 farPoint = this.center + direction * 1e6
|
||||
return Circle(farPoint, 1e6)
|
||||
}
|
||||
|
||||
// Calculate power of the point (center of the inverting circle) with respect to the circle being inverted
|
||||
// power = d² - r²
|
||||
val power = distanceSquared - circle.radius * circle.radius
|
||||
|
||||
// Calculate the new center
|
||||
val newCenterFactor = (this.radius * this.radius) / power
|
||||
val newCenter = this.center + v * newCenterFactor
|
||||
|
||||
// Calculate the new radius
|
||||
val newRadius = abs(this.radius * circle.radius / power) * distance
|
||||
|
||||
return Circle(newCenter, newRadius)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs conformal inversion of another circle.
|
||||
*
|
||||
* Conformal inversion is a special type of circle inversion that preserves tangency between circles.
|
||||
* If two circles are tangent, their images under conformal inversion will also be tangent.
|
||||
*
|
||||
* @param circle The circle to invert
|
||||
* @return The conformally inverted circle
|
||||
* @throws IllegalArgumentException if the circle to be inverted is centered at the center of the inverting circle
|
||||
*/
|
||||
fun Circle.invertConformal(circle: Circle): Circle {
|
||||
// Vector from this circle's center to the other circle's center
|
||||
val v = circle.center - this.center
|
||||
|
||||
// Distance between centers
|
||||
val distanceSquared = v.squaredLength
|
||||
|
||||
// If the circle to be inverted is centered at the center of the inverting circle, we can't invert it
|
||||
if (distanceSquared < 1e-10) {
|
||||
throw IllegalArgumentException("Cannot invert a circle centered at the center of the inverting circle")
|
||||
}
|
||||
|
||||
// Distance between centers
|
||||
val distance = sqrt(distanceSquared)
|
||||
|
||||
// Check if the circle to be inverted passes through the center of the inverting circle
|
||||
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 farPoint = this.center + direction * 1e6
|
||||
return Circle(farPoint, 1e6)
|
||||
}
|
||||
|
||||
// For conformal inversion that preserves tangency, we use the standard circle inversion formula
|
||||
// but with a specific calculation for the radius
|
||||
|
||||
// Calculate power of the point (center of the inverting circle) with respect to the circle being inverted
|
||||
// power = d² - r²
|
||||
val power = distanceSquared - circle.radius * circle.radius
|
||||
|
||||
// Calculate the new center
|
||||
val newCenterFactor = (this.radius * this.radius) / power
|
||||
val newCenter = this.center + v * newCenterFactor
|
||||
|
||||
// Calculate the new radius for conformal inversion
|
||||
// This is the key difference from regular inversion - the formula preserves tangency
|
||||
val newRadius = abs(this.radius * this.radius * circle.radius / power)
|
||||
|
||||
return Circle(newCenter, newRadius)
|
||||
}
|
||||
81
orx-shapes/src/commonTest/kotlin/TestCircleInvert.kt
Normal file
81
orx-shapes/src/commonTest/kotlin/TestCircleInvert.kt
Normal file
@@ -0,0 +1,81 @@
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.shape.Circle
|
||||
import org.openrndr.extra.shapes.primitives.invert
|
||||
import kotlin.math.abs
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TestCircleInvert {
|
||||
|
||||
@Test
|
||||
fun testInvertPointOutsideCircle() {
|
||||
val circle = Circle(100.0, 100.0, 50.0)
|
||||
val point = Vector2(200.0, 100.0) // Point outside the circle
|
||||
val inverted = circle.invert(point)
|
||||
|
||||
// The inverted point should be at (125.0, 100.0)
|
||||
// This is because:
|
||||
// - The point is 100 units away from the center
|
||||
// - The radius is 50
|
||||
// - The inverted distance is 50²/100 = 25
|
||||
// - So the inverted point is 25 units from the center in the same direction
|
||||
assertEquals(125.0, inverted.x, 1e-10)
|
||||
assertEquals(100.0, inverted.y, 1e-10)
|
||||
|
||||
// Verify the inversion property: |OPʹ| × |OP| = r²
|
||||
val distanceToPoint = (point - circle.center).length
|
||||
val distanceToInverted = (inverted - circle.center).length
|
||||
assertTrue(abs(distanceToPoint * distanceToInverted - circle.radius * circle.radius) < 1e-10)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInvertPointInsideCircle() {
|
||||
val circle = Circle(100.0, 100.0, 50.0)
|
||||
val point = Vector2(125.0, 100.0) // Point inside the circle
|
||||
val inverted = circle.invert(point)
|
||||
|
||||
// The inverted point should be at (200.0, 100.0)
|
||||
// This is because:
|
||||
// - The point is 25 units away from the center
|
||||
// - The radius is 50
|
||||
// - The inverted distance is 50²/25 = 100
|
||||
// - So the inverted point is 100 units from the center in the same direction
|
||||
assertEquals(200.0, inverted.x, 1e-10)
|
||||
assertEquals(100.0, inverted.y, 1e-10)
|
||||
|
||||
// Verify the inversion property: |OPʹ| × |OP| = r²
|
||||
val distanceToPoint = (point - circle.center).length
|
||||
val distanceToInverted = (inverted - circle.center).length
|
||||
assertTrue(abs(distanceToPoint * distanceToInverted - circle.radius * circle.radius) < 1e-10)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInvertPointOnCircle() {
|
||||
val circle = Circle(100.0, 100.0, 50.0)
|
||||
val point = Vector2(150.0, 100.0) // Point on the circle
|
||||
val inverted = circle.invert(point)
|
||||
|
||||
// The inverted point should be the same as the original point
|
||||
// This is because points on the circle invert to themselves
|
||||
assertEquals(150.0, inverted.x, 1e-10)
|
||||
assertEquals(100.0, inverted.y, 1e-10)
|
||||
|
||||
// Verify the inversion property: |OPʹ| × |OP| = r²
|
||||
val distanceToPoint = (point - circle.center).length
|
||||
val distanceToInverted = (inverted - circle.center).length
|
||||
assertTrue(abs(distanceToPoint * distanceToInverted - circle.radius * circle.radius) < 1e-10)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInvertPointAtCenter() {
|
||||
val circle = Circle(100.0, 100.0, 50.0)
|
||||
val point = Vector2(100.0, 100.0) // Point at the center
|
||||
|
||||
// Inverting a point at the center should throw an exception
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
circle.invert(point)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.shape.Circle
|
||||
import org.openrndr.extra.shapes.primitives.invertConformal
|
||||
import kotlin.math.abs
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TestCircleInvertConformal {
|
||||
|
||||
/**
|
||||
* Helper function to check if two circles are tangent
|
||||
*/
|
||||
private fun areTangent(circle1: Circle, circle2: Circle): Boolean {
|
||||
val centerDistance = (circle1.center - circle2.center).length
|
||||
val radiusSum = circle1.radius + circle2.radius
|
||||
val radiusDiff = abs(circle1.radius - circle2.radius)
|
||||
|
||||
// Circles are externally tangent if the distance between centers equals the sum of radii
|
||||
val externallyTangent = abs(centerDistance - radiusSum) < 1e-10
|
||||
|
||||
// Circles are internally tangent if the distance between centers equals the difference of radii
|
||||
val internallyTangent = abs(centerDistance - radiusDiff) < 1e-10
|
||||
|
||||
return externallyTangent || internallyTangent
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInvertConformalPreservesTangency() {
|
||||
// Create an inverting circle
|
||||
val invertingCircle = Circle(100.0, 100.0, 50.0)
|
||||
|
||||
// Create two externally tangent circles
|
||||
val circle1 = Circle(200.0, 100.0, 30.0)
|
||||
val circle2 = Circle(260.0, 100.0, 30.0)
|
||||
|
||||
// Verify that the circles are indeed tangent
|
||||
assertTrue(areTangent(circle1, circle2), "The test circles should be tangent")
|
||||
|
||||
// Perform conformal inversion
|
||||
val inverted1 = invertingCircle.invertConformal(circle1)
|
||||
val inverted2 = invertingCircle.invertConformal(circle2)
|
||||
|
||||
// Verify that the inverted circles are also tangent
|
||||
assertTrue(areTangent(inverted1, inverted2), "The inverted circles should remain tangent")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInvertConformalPreservesInternalTangency() {
|
||||
// Create an inverting circle
|
||||
val invertingCircle = Circle(100.0, 100.0, 50.0)
|
||||
|
||||
// Create two internally tangent circles
|
||||
// For internal tangency, one circle must be inside the other with their boundaries touching at exactly one point
|
||||
val circle1 = Circle(200.0, 100.0, 50.0)
|
||||
val circle2 = Circle(230.0, 100.0, 20.0) // Center is at distance (radius1 - radius2) from circle1's center
|
||||
|
||||
// Verify that the circles are indeed tangent
|
||||
assertTrue(areTangent(circle1, circle2), "The test circles should be internally tangent")
|
||||
|
||||
// Perform conformal inversion
|
||||
val inverted1 = invertingCircle.invertConformal(circle1)
|
||||
val inverted2 = invertingCircle.invertConformal(circle2)
|
||||
|
||||
// Verify that the inverted circles are also tangent
|
||||
assertTrue(areTangent(inverted1, inverted2), "The inverted circles should remain tangent")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInvertConformalWithCircleAtCenter() {
|
||||
// Create an inverting circle
|
||||
val invertingCircle = Circle(100.0, 100.0, 50.0)
|
||||
|
||||
// Create a circle centered at the center of the inverting circle
|
||||
val circle = Circle(100.0, 100.0, 20.0)
|
||||
|
||||
// Inverting a circle centered at the center of the inverting circle should throw an exception
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
invertingCircle.invertConformal(circle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package primitives
|
||||
|
||||
import org.openrndr.application
|
||||
import org.openrndr.extra.shapes.primitives.invert
|
||||
import org.openrndr.shape.Circle
|
||||
|
||||
fun main() = application {
|
||||
configure {
|
||||
width = 720
|
||||
height = 720
|
||||
}
|
||||
program {
|
||||
extend {
|
||||
val c = Circle(drawer.bounds.center, 100.0)
|
||||
drawer.circle(c.invert(mouse.position),10.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package primitives
|
||||
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extra.shapes.primitives.invert
|
||||
import org.openrndr.extra.shapes.primitives.invertConformal
|
||||
import org.openrndr.math.Polar
|
||||
import org.openrndr.shape.Circle
|
||||
|
||||
fun main() = application {
|
||||
configure {
|
||||
width = 720
|
||||
height = 720
|
||||
}
|
||||
program {
|
||||
extend {
|
||||
val p = Polar(seconds * 36.0, 100.0).cartesian + drawer.bounds.center
|
||||
val mc = Circle(p, 100.0)
|
||||
|
||||
// check if p is inside any of the circles
|
||||
for (j 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)
|
||||
if (p in c) {
|
||||
drawer.clear(ColorRGBa.WHITE)
|
||||
drawer.fill = ColorRGBa.BLACK
|
||||
drawer.stroke = null
|
||||
drawer.circle(mc.invertConformal(c))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
drawer.stroke = null
|
||||
drawer.fill = ColorRGBa.WHITE
|
||||
|
||||
for (j 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)
|
||||
if (p !in c) {
|
||||
drawer.circle(mc.invertConformal(c))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user