diff --git a/orx-shapes/src/commonMain/kotlin/primitives/CircleTangents.kt b/orx-shapes/src/commonMain/kotlin/primitives/CircleTangents.kt new file mode 100644 index 00000000..0eedacc3 --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/primitives/CircleTangents.kt @@ -0,0 +1,81 @@ +package org.openrndr.extra.shapes.primitives + +import org.openrndr.math.Polar +import org.openrndr.math.Vector2 +import org.openrndr.math.asDegrees +import org.openrndr.shape.Circle +import org.openrndr.shape.Circle.Companion.INVALID +import kotlin.math.acos +import kotlin.math.sqrt + +/** + * Calculates the tangent lines between this circle and another circle. + * + * Depending on the value of [isInner], the method computes either the inner + * tangents or outer tangents between the two circles. + * + * @param other The other circle with which to calculate tangents. + * @param isInner If true, computes inner tangents where the tangents go + * through the region between the two circles. If false (default), computes + * outer tangents. + * @return A list of pairs of points where each pair represents a tangent line. + * Each pair contains one point on this circle and the corresponding point on the other circle. + * Returns an empty list if no valid tangent lines exist between the circles. + */ +fun Circle.tangents(other: Circle, isInner: Boolean = false): List> { + if (this == INVALID || other == INVALID) { + return listOf() + } + + val distSq = center.squaredDistanceTo(other.center) + + if (isInner) { + if (sqrt(distSq) <= radius + other.radius) { + return listOf() // circles too close + } + } else { + val rDiff = radius - other.radius + if (distSq <= rDiff * rDiff) { + return listOf() // nested circles + } + } + + val otherRadiusSigned = if (isInner) -other.radius else other.radius + val hyp = other.center - center // hypotenuse + val adj = radius - otherRadiusSigned // adjacent + val a = hyp * adj + val b = hyp.perpendicular() * sqrt(distSq - adj * adj) + val v1 = (a - b) / distSq + val v2 = (a + b) / distSq + + return listOf( + Pair(center + v1 * radius, other.center + v1 * otherRadiusSigned), + Pair(center + v2 * radius, other.center + v2 * otherRadiusSigned) + ) +} + +/** + * Computes the tangent points from a given external point to a circle. + * + * If the circle is invalid (e.g., has an undefined radius or center), + * the function returns a pair of infinite vectors. + * + * @param point The external point from which tangents to the circle are calculated. + * @return A pair of [Vector2] representing the two points on the circle + * where the tangents from the given external point touch the circle. + */ +fun Circle.tangents(point: Vector2): Pair { + if (this == INVALID) { + return Pair(Vector2.INFINITY, Vector2.INFINITY) + } + val v = Polar.fromVector(point - center) + val b = v.radius + val theta = (acos(radius / b)).asDegrees + val d1 = v.theta + theta + val d2 = v.theta - theta + + val tp = center + Polar(d1, radius).cartesian + val tp2 = center + Polar(d2, radius).cartesian + + return Pair(tp, tp2) +}