From 82a84f3919e37015d8030dada12edd14ab024dfc Mon Sep 17 00:00:00 2001 From: Luiz Fernando Silva Date: Sun, 18 Aug 2024 09:44:11 -0300 Subject: [PATCH] [GeometriaClipping] Adding Capsule2Parametric shape --- Sources/Geometria/2D/CircleArc2.swift | 39 +++++ .../2D/Protocols/Vector/Vector2Signed.swift | 2 +- .../2D/Geometry/Capsule2Parametric.swift | 92 ++++++++++++ .../2D/{ => Geometry}/Circle2Parametric.swift | 0 .../2D/{ => Geometry}/Compound2Periodic.swift | 0 .../LinePolygon2Parametric.swift | 0 .../2D/Geometry/Capsule2ParametricTests.swift | 142 ++++++++++++++++++ .../Circle2ParametricTests.swift | 0 .../Compound2ParametricTests.swift | 0 .../LinePolygon2ParametricTests.swift | 0 10 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 Sources/GeometriaClipping/2D/Geometry/Capsule2Parametric.swift rename Sources/GeometriaClipping/2D/{ => Geometry}/Circle2Parametric.swift (100%) rename Sources/GeometriaClipping/2D/{ => Geometry}/Compound2Periodic.swift (100%) rename Sources/GeometriaClipping/2D/{ => Geometry}/LinePolygon2Parametric.swift (100%) create mode 100644 Tests/GeometriaClippingTests/2D/Geometry/Capsule2ParametricTests.swift rename Tests/GeometriaClippingTests/2D/{ => Geometry}/Circle2ParametricTests.swift (100%) rename Tests/GeometriaClippingTests/2D/{ => Geometry}/Compound2ParametricTests.swift (100%) rename Tests/GeometriaClippingTests/2D/{ => Geometry}/LinePolygon2ParametricTests.swift (100%) diff --git a/Sources/Geometria/2D/CircleArc2.swift b/Sources/Geometria/2D/CircleArc2.swift index d914ddb0..e84e97b3 100644 --- a/Sources/Geometria/2D/CircleArc2.swift +++ b/Sources/Geometria/2D/CircleArc2.swift @@ -109,6 +109,45 @@ public struct CircleArc2: GeometricType, CustomStringConver sweepAngle: sweepAngle1 ) } + + /// Creates a new circular arc that fits the given start/end points on the + /// circumference of the arc, and a center point. + /// + /// The sweep angle is chosen to be the clockwise sweep angle that connects + /// startAngle to endAngle. + /// + /// - note: The initializer assumes that `center` is equally distant to both + /// `startPoint` and `endPoint`. + public init( + clockwiseAngleToCenter center: Vector, + startPoint: Vector, + endPoint: Vector + ) { + let radius = center.distance(to: startPoint) + let startAngle = center.angle(to: startPoint) + let endAngle = center.angle(to: endPoint) + + let (sweepAngle1, sweepAngle2) = startAngle.relativeAngles(to: endAngle) + + let sweepAngle: Angle + switch (sweepAngle1.radians > 0, sweepAngle2.radians > 0) { + case (false, true): + sweepAngle = sweepAngle2 + + case (true, false): + sweepAngle = sweepAngle1 + + case (true, true), (false, false): + sweepAngle = sweepAngle1 + } + + self.init( + center: center, + radius: radius, + startAngle: startAngle, + sweepAngle: sweepAngle + ) + } } extension CircleArc2: Equatable where Vector: Equatable { } diff --git a/Sources/Geometria/2D/Protocols/Vector/Vector2Signed.swift b/Sources/Geometria/2D/Protocols/Vector/Vector2Signed.swift index 06de37e4..fd2222bc 100644 --- a/Sources/Geometria/2D/Protocols/Vector/Vector2Signed.swift +++ b/Sources/Geometria/2D/Protocols/Vector/Vector2Signed.swift @@ -17,7 +17,7 @@ public protocol Vector2Signed: Vector2Multiplicative & VectorSigned where SubVec mutating func formLeftRotated() /// Returns a vector that represents this vector's point, rotated 90º - /// clockwise clockwise relative to the origin. + /// clockwise relative to the origin. func rightRotated() -> Self /// Rotates this vector 90º clockwise relative to the origin. diff --git a/Sources/GeometriaClipping/2D/Geometry/Capsule2Parametric.swift b/Sources/GeometriaClipping/2D/Geometry/Capsule2Parametric.swift new file mode 100644 index 00000000..702925f2 --- /dev/null +++ b/Sources/GeometriaClipping/2D/Geometry/Capsule2Parametric.swift @@ -0,0 +1,92 @@ +import Geometria + +/// A parametric geometry that is defined by line segments connecting two circular +/// arcs at the end-points. +public struct Capsule2Parametric: ParametricClip2Geometry, Equatable { + public typealias Scalar = Vector.Scalar + public typealias Simplex = Parametric2GeometrySimplex + public typealias Contour = Parametric2Contour + + public var start: Vector + public var startRadius: Scalar + public var end: Vector + public var endRadius: Scalar + + public var startPeriod: Period + public var endPeriod: Period + + public var isReversed: Bool + + internal init( + start: Vector, + end: Vector, + radius: Scalar, + startPeriod: Period, + endPeriod: Period + ) { + self.init( + start: start, + startRadius: radius, + end: end, + endRadius: radius, + startPeriod: startPeriod, + endPeriod: endPeriod + ) + } + + internal init( + start: Vector, + startRadius: Scalar, + end: Vector, + endRadius: Scalar, + startPeriod: Period, + endPeriod: Period + ) { + self.start = start + self.startRadius = startRadius + self.end = end + self.endRadius = endRadius + self.startPeriod = startPeriod + self.endPeriod = endPeriod + self.isReversed = false + } + + public func allContours() -> [Contour] { + let startCircle = Circle2(center: start, radius: startRadius) + let endCircle = Circle2(center: end, radius: endRadius) + + let tangents = startCircle.outerTangents(to: endCircle) + + let startArc = CircleArc2(clockwiseAngleToCenter: start, startPoint: tangents.1.start, endPoint: tangents.0.start) + let endArc = CircleArc2(clockwiseAngleToCenter: end, startPoint: tangents.0.end, endPoint: tangents.1.end) + + let simplexes: [Simplex] = [ + .circleArc2(.init(circleArc: startArc, startPeriod: 0, endPeriod: 0)), + .lineSegment2(.init(lineSegment: tangents.0, startPeriod: 0, endPeriod: 0)), + .circleArc2(.init(circleArc: endArc, startPeriod: 0, endPeriod: 0)), + .lineSegment2(.init(lineSegment: tangents.1.reversed, startPeriod: 0, endPeriod: 0)), + ] + + let contour = Contour( + normalizing: simplexes, + startPeriod: startPeriod, + endPeriod: endPeriod + ) + + if isReversed { + return [ + contour.reversed() + ] + } else { + return [ + contour + ] + } + } + + public func reversed() -> Capsule2Parametric { + var copy = self + copy.isReversed = !isReversed + return copy + } +} diff --git a/Sources/GeometriaClipping/2D/Circle2Parametric.swift b/Sources/GeometriaClipping/2D/Geometry/Circle2Parametric.swift similarity index 100% rename from Sources/GeometriaClipping/2D/Circle2Parametric.swift rename to Sources/GeometriaClipping/2D/Geometry/Circle2Parametric.swift diff --git a/Sources/GeometriaClipping/2D/Compound2Periodic.swift b/Sources/GeometriaClipping/2D/Geometry/Compound2Periodic.swift similarity index 100% rename from Sources/GeometriaClipping/2D/Compound2Periodic.swift rename to Sources/GeometriaClipping/2D/Geometry/Compound2Periodic.swift diff --git a/Sources/GeometriaClipping/2D/LinePolygon2Parametric.swift b/Sources/GeometriaClipping/2D/Geometry/LinePolygon2Parametric.swift similarity index 100% rename from Sources/GeometriaClipping/2D/LinePolygon2Parametric.swift rename to Sources/GeometriaClipping/2D/Geometry/LinePolygon2Parametric.swift diff --git a/Tests/GeometriaClippingTests/2D/Geometry/Capsule2ParametricTests.swift b/Tests/GeometriaClippingTests/2D/Geometry/Capsule2ParametricTests.swift new file mode 100644 index 00000000..c954eebe --- /dev/null +++ b/Tests/GeometriaClippingTests/2D/Geometry/Capsule2ParametricTests.swift @@ -0,0 +1,142 @@ +import Geometria +import TestCommons +import XCTest + +@testable import GeometriaClipping + +class Capsule2ParametricTests: XCTestCase { + typealias Sut = Capsule2Parametric + let accuracy: Double = 1e-12 + + func testEphemeral() { + let sut = Sut( + start: .init(x: -150, y: -30), + startRadius: 60, + end: .init(x: 70, y: 80), + endRadius: 100, + startPeriod: 0.0, + endPeriod: 1.0 + ) + + TestFixture.beginFixture { fixture in + fixture.add(sut, category: "input") + + fixture.assertions(on: sut) + .assertSimplexes( + accuracy: accuracy, + [ + .circleArc2( + .init( + circleArc: .init( + center: .init(x: -150.0, y: -30.0), + radius: 60.00000000000001, + startAngle: Angle(radians: 2.1977925247947656), + sweepAngle: Angle(radians: 2.8148954755916673) + ), + startPeriod: 0.0, + endPeriod: 0.16870660664537054 + ) + ), + .lineSegment2( + .init( + lineSegment: .init( + start: .init(x: -132.2516485101565, y: -87.31488479786879), + end: .init(x: 99.5805858164058, y: -15.524807996447976) + ), + startPeriod: 0.16870660664537054, + endPeriod: 0.41113094230800334 + ) + ), + .circleArc2( + .init( + circleArc: .init( + center: .init(x: 70.0, y: 80.0), + radius: 100.0, + startAngle: Angle(radians: -1.2704973067931533), + sweepAngle: Angle(radians: 3.468289831587919) + ), + startPeriod: 0.41113094230800334, + endPeriod: 0.7575756643373672 + ) + ), + .lineSegment2( + .init( + lineSegment: .init( + start: .init(x: 11.328505092685084, y: 160.97935345099341), + end: .init(x: -185.20289694438895, y: 18.58761207059606) + ), + startPeriod: 0.7575756643373672, + endPeriod: 1.0 + ) + ), + ] + ) + } + } + + func testReversed() { + let sut = Sut( + start: .init(x: -150, y: -30), + startRadius: 60, + end: .init(x: 70, y: 80), + endRadius: 100, + startPeriod: 0.0, + endPeriod: 1.0 + ) + + TestFixture.beginFixture { fixture in + fixture.add(sut, category: "input") + + fixture.assertions(on: sut.reversed()) + .assertSimplexes( + accuracy: accuracy, + [ + .lineSegment2( + .init( + lineSegment: .init( + start: .init(x: -185.20289694438895, y: 18.58761207059606), + end: .init(x: 11.328505092685084, y: 160.97935345099341) + ), + startPeriod: 0.0, + endPeriod: 0.24242433566263277 + ) + ), + .circleArc2( + .init( + circleArc: .init( + center: .init(x: 70.0, y: 80.0), + radius: 100.0, + startAngle: Angle(radians: 2.1977925247947656), + sweepAngle: Angle(radians: -3.468289831587919) + ), + startPeriod: 0.24242433566263277, + endPeriod: 0.5888690576919966 + ) + ), + .lineSegment2( + .init( + lineSegment: .init( + start: .init(x: 99.5805858164058, y: -15.524807996447976), + end: .init(x: -132.2516485101565, y: -87.31488479786879) + ), + startPeriod: 0.5888690576919966, + endPeriod: 0.8312933933546295 + ) + ), + .circleArc2( + .init( + circleArc: .init( + center: .init(x: -150.0, y: -30.0), + radius: 60.00000000000001, + startAngle: Angle(radians: 5.012688000386433), + sweepAngle: Angle(radians: -2.8148954755916673) + ), + startPeriod: 0.8312933933546295, + endPeriod: 1.0 + ) + ), + ] + ) + } + } +} diff --git a/Tests/GeometriaClippingTests/2D/Circle2ParametricTests.swift b/Tests/GeometriaClippingTests/2D/Geometry/Circle2ParametricTests.swift similarity index 100% rename from Tests/GeometriaClippingTests/2D/Circle2ParametricTests.swift rename to Tests/GeometriaClippingTests/2D/Geometry/Circle2ParametricTests.swift diff --git a/Tests/GeometriaClippingTests/2D/Compound2ParametricTests.swift b/Tests/GeometriaClippingTests/2D/Geometry/Compound2ParametricTests.swift similarity index 100% rename from Tests/GeometriaClippingTests/2D/Compound2ParametricTests.swift rename to Tests/GeometriaClippingTests/2D/Geometry/Compound2ParametricTests.swift diff --git a/Tests/GeometriaClippingTests/2D/LinePolygon2ParametricTests.swift b/Tests/GeometriaClippingTests/2D/Geometry/LinePolygon2ParametricTests.swift similarity index 100% rename from Tests/GeometriaClippingTests/2D/LinePolygon2ParametricTests.swift rename to Tests/GeometriaClippingTests/2D/Geometry/LinePolygon2ParametricTests.swift