diff --git a/Sources/GeometriaClipping/2D/Geometry/Capsule2Parametric.swift b/Sources/GeometriaClipping/2D/Geometry/Capsule2Parametric.swift index 702925f2..b4305183 100644 --- a/Sources/GeometriaClipping/2D/Geometry/Capsule2Parametric.swift +++ b/Sources/GeometriaClipping/2D/Geometry/Capsule2Parametric.swift @@ -17,7 +17,7 @@ public struct Capsule2Parametric: ParametricClip2Geometry, public var isReversed: Bool - internal init( + public init( start: Vector, end: Vector, radius: Scalar, @@ -34,7 +34,7 @@ public struct Capsule2Parametric: ParametricClip2Geometry, ) } - internal init( + public init( start: Vector, startRadius: Scalar, end: Vector, @@ -60,10 +60,29 @@ public struct Capsule2Parametric: ParametricClip2Geometry, 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)), + var simplexes: [Simplex] = [] + + simplexes += + CircleArc2Simplex.splittingArcSegments( + startArc, + startPeriod: 0, + endPeriod: 0, + maxAbsoluteSweepAngle: .pi / 2 + ).map(Parametric2GeometrySimplex.circleArc2) + + simplexes += [ .lineSegment2(.init(lineSegment: tangents.0, startPeriod: 0, endPeriod: 0)), - .circleArc2(.init(circleArc: endArc, startPeriod: 0, endPeriod: 0)), + ] + + simplexes += + CircleArc2Simplex.splittingArcSegments( + endArc, + startPeriod: 0, + endPeriod: 0, + maxAbsoluteSweepAngle: .pi / 2 + ).map(Parametric2GeometrySimplex.circleArc2) + + simplexes += [ .lineSegment2(.init(lineSegment: tangents.1.reversed, startPeriod: 0, endPeriod: 0)), ] diff --git a/Sources/GeometriaClipping/2D/Graph/Simplex2Graph+Creation.swift b/Sources/GeometriaClipping/2D/Graph/Simplex2Graph+Creation.swift index 8cc843b1..ad81d0ef 100644 --- a/Sources/GeometriaClipping/2D/Graph/Simplex2Graph+Creation.swift +++ b/Sources/GeometriaClipping/2D/Graph/Simplex2Graph+Creation.swift @@ -114,6 +114,34 @@ extension Simplex2Graph { rhsIndex, intersection.other )) } + + // TODO: Figure out why moving the interference splitting to here breaks exclusive disjunctions + #if false + + // Compute vertex/edge interference intersections + for lhsSimplex in lhs.allSimplexes() { + for rhsSimplex in rhs.allSimplexes() { + if lhsSimplex.isOnSurface(rhsSimplex.start, toleranceSquared: toleranceSquared) { + let period = lhsSimplex.closestPeriod(to: rhsSimplex.start) + contours[lhsIndex].split(at: period) + } + if lhsSimplex.isOnSurface(rhsSimplex.end, toleranceSquared: toleranceSquared) { + let period = lhsSimplex.closestPeriod(to: rhsSimplex.end) + contours[lhsIndex].split(at: period) + } + + if rhsSimplex.isOnSurface(lhsSimplex.start, toleranceSquared: toleranceSquared) { + let period = rhsSimplex.closestPeriod(to: lhsSimplex.start) + contours[rhsIndex].split(at: period) + } + if rhsSimplex.isOnSurface(lhsSimplex.end, toleranceSquared: toleranceSquared) { + let period = rhsSimplex.closestPeriod(to: lhsSimplex.end) + contours[rhsIndex].split(at: period) + } + } + } + + #endif // #if false } } diff --git a/Sources/GeometriaClipping/2D/Graph/Simplex2Graph.swift b/Sources/GeometriaClipping/2D/Graph/Simplex2Graph.swift index 777df8ac..794265f2 100644 --- a/Sources/GeometriaClipping/2D/Graph/Simplex2Graph.swift +++ b/Sources/GeometriaClipping/2D/Graph/Simplex2Graph.swift @@ -511,7 +511,7 @@ public struct Simplex2Graph { case ( .circleArc(let lhsCenter, let lhsRadius, let lhsStart, let lhsSweep), .circleArc(let rhsCenter, let rhsRadius, let rhsStart, let rhsSweep) - ) where lhsCenter == rhsCenter && lhsRadius == rhsRadius: + ) where lhsCenter == rhsCenter && lhsRadius.isApproximatelyEqualFast(to: rhsRadius, tolerance: tolerance): let lhsSweep = AngleSweep(start: lhsStart, sweep: lhsSweep) let rhsSweep = AngleSweep(start: rhsStart, sweep: rhsSweep) @@ -651,7 +651,7 @@ public struct Simplex2Graph { case ( .circleArc(let lhsCenter, let lhsRadius, let lhsStartAngle, let lhsSweepAngle), .circleArc(let rhsCenter, let rhsRadius, let rhsStartAngle, let rhsSweepAngle) - ) where lhsCenter == rhsCenter && lhsRadius == rhsRadius: + ) where lhsCenter == rhsCenter && lhsRadius.isApproximatelyEqualFast(to: rhsRadius, tolerance: tolerance): let lhsSweep = AngleSweep(start: lhsStartAngle, sweep: lhsSweepAngle) let rhsSweep = AngleSweep(start: rhsStartAngle, sweep: rhsSweepAngle) diff --git a/Sources/GeometriaClipping/2D/Simplexes/CircleArc2Simplex.swift b/Sources/GeometriaClipping/2D/Simplexes/CircleArc2Simplex.swift index 6e7481fa..90a03167 100644 --- a/Sources/GeometriaClipping/2D/Simplexes/CircleArc2Simplex.swift +++ b/Sources/GeometriaClipping/2D/Simplexes/CircleArc2Simplex.swift @@ -289,3 +289,56 @@ public struct CircleArc2Simplex: Parametric2Simplex, Equata ) } } + +extension CircleArc2Simplex { + /// Returns an array of `CircleArc2Simplex` instances that span the given arc + /// with a given period, with the least number of arcs that are have a sweep + /// angle of at most `maxAbsoluteSweepAngle`. + public static func splittingArcSegments( + _ arc: CircleArc2, + startPeriod: Period, + endPeriod: Period, + maxAbsoluteSweepAngle: Scalar + ) -> [Self] { + var result: [Self] = [] + + let startAngle = arc.sweepAngle.radians + var totalAngle: Scalar = .zero + + let sign: Scalar = startAngle > .zero ? 1 : -1 + + var remaining = startAngle.magnitude + let step = maxAbsoluteSweepAngle.magnitude + + let periodRange = endPeriod - startPeriod + + while remaining > .zero { + defer { remaining -= step } + + let sweep: Scalar + if remaining < step { + sweep = remaining * sign + } else { + sweep = step * sign + } + + defer { totalAngle += sweep } + + let sPeriod: Period = startPeriod + periodRange * (totalAngle / startAngle) + let ePeriod: Period = startPeriod + periodRange * ((totalAngle + sweep) / startAngle) + + let simplex = Self( + center: arc.center, + radius: arc.radius, + startAngle: .init(radians: arc.startAngle.radians + totalAngle), + sweepAngle: .init(radians: sweep), + startPeriod: sPeriod, + endPeriod: ePeriod + ) + + result.append(simplex) + } + + return result + } +} diff --git a/Sources/TestCommons/TestFixture/TestFixture+Clipping.swift b/Sources/TestCommons/TestFixture/TestFixture+Clipping.swift index 3564fa1c..9b725e8a 100644 --- a/Sources/TestCommons/TestFixture/TestFixture+Clipping.swift +++ b/Sources/TestCommons/TestFixture/TestFixture+Clipping.swift @@ -4,6 +4,19 @@ import GeometriaClipping import XCTest public extension TestFixture { + func add( + _ value: [some ParametricClip2Geometry], + category: String, + style: P5Printer.Style? = nil, + file: StaticString = #file, + line: UInt = #line + ) where Vector.Scalar: CustomStringConvertible { + + for geometry in value { + add(geometry, category: category, style: style, file: file, line: line) + } + } + func add( _ value: T, category: String, diff --git a/Tests/GeometriaClippingTests/2D/Boolean/Union2ParametricTests.swift b/Tests/GeometriaClippingTests/2D/Boolean/Union2ParametricTests.swift index f1affcc7..79bcd27a 100644 --- a/Tests/GeometriaClippingTests/2D/Boolean/Union2ParametricTests.swift +++ b/Tests/GeometriaClippingTests/2D/Boolean/Union2ParametricTests.swift @@ -297,6 +297,68 @@ class Union2ParametricTests: XCTestCase { } } + func testUnion_capsuleSequence_short() { + let inputs = Capsule2Parametric.makeCapsuleSequence([ + (.init(x: -150, y: -10), 20.0), + (.init(x: 0, y: 10), 17.5), + (.init(x: 150, y: -10), 12.0), + ]) + + let sut = Union2Parametric(contours: inputs.flatMap({ $0.allContours() }), tolerance: accuracy) + + TestFixture.beginFixture(lineScale: 4.0, renderScale: 2.0) { fixture in + fixture.assertions(on: sut) + .assertAllSimplexes( + accuracy: accuracy, + [[GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -150.0, y: -10.0), radius: 20.0, startAngle: Angle(radians: 1.6868266425111282), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.0, endPeriod: 0.04449467435514434)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -150.0, y: -10.0), radius: 20.0, startAngle: Angle(radians: 3.257622969306025), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.04449467435514434, endPeriod: 0.08898934871028868)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -150.0, y: -10.0), radius: 20.0, startAngle: Angle(radians: 4.828419296100922), sweepAngle: Angle(radians: 0.03304243316088362)), startPeriod: 0.08898934871028868, endPeriod: 0.08992531493220812)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: -147.02957541460717, y: -29.778184390446302), end: Vector2(x: 0.46609629892996085, y: -7.626263827477199)), startPeriod: 0.08992531493220812, endPeriod: 0.3011676746423419)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: 0.46609629892996085, y: -7.626263827477199), end: Vector2(x: 148.84739761402307, y: -21.94451789482691)), startPeriod: 0.3011676746423419, endPeriod: 0.5122976787319162)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 150.0, y: -10.0), radius: 11.999999999999998, startAngle: Angle(radians: -1.6669948295818133), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.5122976787319162, endPeriod: 0.5389944833450028)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 150.0, y: -10.0), radius: 11.999999999999998, startAngle: Angle(radians: -0.09619850278691677), sweepAngle: Angle(radians: 1.498090267775381)), startPeriod: 0.5389944833450028, endPeriod: 0.5644555965965034)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: 152.01723120693762, y: 1.8292340520321524), end: Vector2(x: 2.9417955101173607, y: 27.25096632588022)), startPeriod: 0.5644555965965034, endPeriod: 0.7786405057835739)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 0.0, y: 10.0), radius: 17.5, startAngle: Angle(radians: 1.4018917649884644), sweepAngle: Angle(radians: 0.28493487752266383)), startPeriod: 0.7786405057835739, endPeriod: 0.7857027351923699)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: -2.0259774074152417, y: 27.382330555614313), end: Vector2(x: -152.3154027513317, y: 9.865520634987789)), startPeriod: 0.7857027351923699, endPeriod: 1.0))]] + ) + } + } + + func testUnion_capsuleSequence_short_2() { + let inputs = Capsule2Parametric.makeCapsuleSequence([ + (.init(x: -150, y: -10), 20.0), + (.init(x: -100, y: 10), 10.0), + (.init(x: -50, y: -30), 15.0), + ]) + + let sut = Union2Parametric(contours: inputs.flatMap({ $0.allContours() }), tolerance: accuracy) + + TestFixture.beginFixture(lineScale: 4.0, renderScale: 2.0) { fixture in + fixture.add(inputs, category: "inputs") + + fixture.assertions(on: sut) + .assertAllSimplexes( + accuracy: accuracy, + [[GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -150.0, y: -10.0), radius: 20.000000000000007, startAngle: Angle(radians: 1.7645232428256685), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.0, endPeriod: 0.09088908963259965)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -150.0, y: -10.0), radius: 20.000000000000007, startAngle: Angle(radians: 3.335319569620565), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.09088908963259965, endPeriod: 0.1817781792651993)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -150.0, y: -10.0), radius: 20.000000000000007, startAngle: Angle(radians: 4.906115896415462), sweepAngle: Angle(radians: 0.37355892216318587)), startPeriod: 0.1817781792651993, endPeriod: 0.20339296775202034)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: -139.25309983154597, y: -26.867250421135108), end: Vector2(x: -101.173233625772, y: -2.604818173805065)), startPeriod: 0.20339296775202034, endPeriod: 0.3340229889906092)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: -101.173233625772, y: -2.604818173805065), end: Vector2(x: -60.256447805953925, y: -40.94555975744241)), startPeriod: 0.3340229889906092, endPeriod: 0.49624757488940224)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -50.0, y: -30.0), radius: 14.999999999999995, startAngle: Angle(radians: -2.323703725089415), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.49624757488940224, endPeriod: 0.5644143921138519)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -50.0, y: -30.0), radius: 14.999999999999995, startAngle: Angle(radians: -0.7529073982945182), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.5644143921138519, endPeriod: 0.6325812093383015)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -50.0, y: -30.0), radius: 14.999999999999995, startAngle: Angle(radians: 0.8178889285003783), sweepAngle: Angle(radians: 0.15633291214193124)), startPeriod: 0.6325812093383015, endPeriod: 0.6393654861196774)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: -41.572820486729, y: -17.591025608411247), end: Vector2(x: -94.38188032448599, y: 18.272649594392494)), startPeriod: 0.6393654861196774, endPeriod: 0.824047961640725)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -100.0, y: 10.0), radius: 10.000000000000005, startAngle: Angle(radians: 0.9742218406423091), sweepAngle: Angle(radians: 0.7903014021833594)), startPeriod: 0.824047961640725, endPeriod: 0.8469120891391564)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: -101.92517422215806, y: 19.812935555395143), end: Vector2(x: -153.85034844431613, y: 9.625871110790285)), startPeriod: 0.8469120891391564, endPeriod: 1.0))]] + ) + } + } + + func testUnion_capsuleSequence_long() { + let inputs = Capsule2Parametric.makeCapsuleSequence([ + (.init(x: -150, y: -10), 20.0), + (.init(x: -100, y: 10), 10.0), + (.init(x: -50, y: -30), 15.0), + (.init(x: 0, y: 10), 17.5), + (.init(x: 50, y: 40), 12.5), + (.init(x: 100, y: 0), 10.0), + (.init(x: 150, y: 25), 12.0), + ]) + + let sut = Union2Parametric(contours: inputs.flatMap({ $0.allContours() }), tolerance: accuracy) + + TestFixture.beginFixture(lineScale: 4.0, renderScale: 2.0) { fixture in + fixture.add(inputs, category: "inputs") + + fixture.assertions(on: sut) + .assertAllSimplexes( + accuracy: accuracy, + [[GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -150.0, y: -10.0), radius: 20.000000000000007, startAngle: Angle(radians: 1.7645232428256685), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.0, endPeriod: 0.03860227068267195)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -150.0, y: -10.0), radius: 20.000000000000007, startAngle: Angle(radians: 3.335319569620565), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.03860227068267195, endPeriod: 0.0772045413653439)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -150.0, y: -10.0), radius: 20.000000000000007, startAngle: Angle(radians: 4.906115896415462), sweepAngle: Angle(radians: 0.37355892216318587)), startPeriod: 0.0772045413653439, endPeriod: 0.08638474021307992)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: -139.25309983154597, y: -26.867250421135108), end: Vector2(x: -101.173233625772, y: -2.604818173805065)), startPeriod: 0.08638474021307992, endPeriod: 0.1418657166374112)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: -101.173233625772, y: -2.604818173805065), end: Vector2(x: -60.256447805953925, y: -40.94555975744241)), startPeriod: 0.1418657166374112, endPeriod: 0.21076548669301826)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -50.0, y: -30.0), radius: 14.999999999999995, startAngle: Angle(radians: -2.323703725089415), sweepAngle: Angle(radians: 1.388594973661861)), startPeriod: 0.21076548669301826, endPeriod: 0.23635899528203405)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: -41.094036176534345, y: -42.069954779332065), end: Vector2(x: 10.324013069283792, y: -4.13051798537146)), startPeriod: 0.23635899528203405, endPeriod: 0.31487608447887894)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: 10.324013069283792, y: -4.13051798537146), end: Vector2(x: 49.92713695407643, y: 24.519321922490473)), startPeriod: 0.31487608447887894, endPeriod: 0.3749368851924291)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: 49.92713695407643, y: 24.519321922490473), end: Vector2(x: 94.06269078435623, y: -8.046636519554713)), startPeriod: 0.3749368851924291, endPeriod: 0.44233335914804245)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 100.0, y: 0.0), radius: 9.999999999999998, startAngle: Angle(radians: -2.2064839021622404), sweepAngle: Angle(radians: 1.0635504598831482)), startPeriod: 0.44233335914804245, endPeriod: 0.45540171881910824)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: 104.14927287150829, y: -9.09854574301659), end: Vector2(x: 154.97912744580995, y: 14.081745108380092)), startPeriod: 0.45540171881910824, endPeriod: 0.5240468696089267)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 150.0, y: 25.0), radius: 11.999999999999998, startAngle: Angle(radians: -1.1429334422790915), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.5240468696089267, endPeriod: 0.5472082320185299)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 150.0, y: 25.0), radius: 11.999999999999998, startAngle: Angle(radians: 0.427862884515805), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.5472082320185299, endPeriod: 0.570369594428133)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 150.0, y: 25.0), radius: 11.999999999999998, startAngle: Angle(radians: 1.9986592113107016), sweepAngle: Angle(radians: 0.07156944897000184)), startPeriod: 0.570369594428133, endPeriod: 0.5714248846123366)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: 144.25287255419005, y: 35.53425489161991), end: Vector2(x: 101.30206975776143, y: 12.101772944814893)), startPeriod: 0.5714248846123366, endPeriod: 0.6315438763267497)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: 101.30206975776143, y: 12.101772944814893), end: Vector2(x: 58.18383164150594, y: 49.448539551882405)), startPeriod: 0.6315438763267497, endPeriod: 0.7016360152702934)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 40.0), radius: 12.5, startAngle: Angle(radians: 0.8570020177151347), sweepAngle: Angle(radians: 1.1683590826247927)), startPeriod: 0.7016360152702934, endPeriod: 0.7195812577536341)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: 44.511608434583316, y: 51.2306526090278), end: Vector2(x: -7.683748191583341, y: 25.722913652638933)), startPeriod: 0.7195812577536341, endPeriod: 0.7909650913526259)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 0.0, y: 10.0), radius: 17.5, startAngle: Angle(radians: 2.025361100339926), sweepAngle: Angle(radians: 0.18112280182231472)), startPeriod: 0.7909650913526259, endPeriod: 0.7948597926519269)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 0.0, y: 10.0), radius: 17.5, startAngle: Angle(radians: 2.206483902162241), sweepAngle: Angle(radians: 0.07810673371241661)), startPeriod: 0.7948597926519269, endPeriod: 0.7965393293762557)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: -11.457364298108304, y: 23.227955372635382), end: Vector2(x: -51.108091807497736, y: -11.115434589194493)), startPeriod: 0.7965393293762557, endPeriod: 0.8609947529561204)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: -51.108091807497736, y: -11.115434589194493), end: Vector2(x: -94.38188032448599, y: 18.272649594392494)), startPeriod: 0.8609947529561204, endPeriod: 0.925269928003807)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: -100.0, y: 10.0), radius: 9.999999999999996, startAngle: Angle(radians: 0.9742218406423084), sweepAngle: Angle(radians: 0.7903014021833603)), startPeriod: 0.925269928003807, endPeriod: 0.9349807441445048)), GeometriaClipping.Parametric2GeometrySimplex>.lineSegment2(GeometriaClipping.LineSegment2Simplex>(lineSegment: LineSegment>(start: Vector2(x: -101.92517422215806, y: 19.812935555395143), end: Vector2(x: -153.85034844431613, y: 9.625871110790285)), startPeriod: 0.9349807441445048, endPeriod: 1.0))]] + ) + } + } + #if GEOMETRIA_PERFORMANCE_TESTS func testPerformance_overlapping_circles() { diff --git a/Tests/GeometriaClippingTests/2D/Geometry/Capsule2ParametricTests.swift b/Tests/GeometriaClippingTests/2D/Geometry/Capsule2ParametricTests.swift index c954eebe..5ec2aed2 100644 --- a/Tests/GeometriaClippingTests/2D/Geometry/Capsule2ParametricTests.swift +++ b/Tests/GeometriaClippingTests/2D/Geometry/Capsule2ParametricTests.swift @@ -31,9 +31,21 @@ class Capsule2ParametricTests: XCTestCase { center: .init(x: -150.0, y: -30.0), radius: 60.00000000000001, startAngle: Angle(radians: 2.1977925247947656), - sweepAngle: Angle(radians: 2.8148954755916673) + sweepAngle: Angle(radians: 1.5707963267948966) ), startPeriod: 0.0, + endPeriod: 0.09414335996574721 + ) + ), + .circleArc2( + .init( + circleArc: .init( + center: .init(x: -150.0, y: -30.0), + radius: 60.00000000000001, + startAngle: Angle(radians: 3.768588851589662), + sweepAngle: Angle(radians: 1.2440991487967707) + ), + startPeriod: 0.09414335996574721, endPeriod: 0.16870660664537054 ) ), @@ -53,9 +65,33 @@ class Capsule2ParametricTests: XCTestCase { center: .init(x: 70.0, y: 80.0), radius: 100.0, startAngle: Angle(radians: -1.2704973067931533), - sweepAngle: Angle(radians: 3.468289831587919) + sweepAngle: Angle(radians: 1.5707963267948966) ), startPeriod: 0.41113094230800334, + endPeriod: 0.5680365422509154 + ) + ), + .circleArc2( + .init( + circleArc: .init( + center: .init(x: 70.0, y: 80.0), + radius: 100.0, + startAngle: Angle(radians: 0.30029902000174324), + sweepAngle: Angle(radians: 1.5707963267948966) + ), + startPeriod: 0.5680365422509154, + endPeriod: 0.7249421421938274 + ) + ), + .circleArc2( + .init( + circleArc: .init( + center: .init(x: 70.0, y: 80.0), + radius: 100.0, + startAngle: Angle(radians: 1.8710953467966398), + sweepAngle: Angle(radians: 0.3266971779981258) + ), + startPeriod: 0.7249421421938274, endPeriod: 0.7575756643373672 ) ), @@ -107,9 +143,33 @@ class Capsule2ParametricTests: XCTestCase { center: .init(x: 70.0, y: 80.0), radius: 100.0, startAngle: Angle(radians: 2.1977925247947656), - sweepAngle: Angle(radians: -3.468289831587919) + sweepAngle: Angle(radians: -0.3266971779981258) ), startPeriod: 0.24242433566263277, + endPeriod: 0.27505785780617265 + ) + ), + .circleArc2( + .init( + circleArc: .init( + center: .init(x: 70.0, y: 80.0), + radius: 100.0, + startAngle: Angle(radians: 1.8710953467966398), + sweepAngle: Angle(radians: -1.5707963267948966) + ), + startPeriod: 0.27505785780617265, + endPeriod: 0.4319634577490846 + ) + ), + .circleArc2( + .init( + circleArc: .init( + center: .init(x: 70.0, y: 80.0), + radius: 100.0, + startAngle: Angle(radians: 0.30029902000174324), + sweepAngle: Angle(radians: -1.5707963267948966) + ), + startPeriod: 0.4319634577490846, endPeriod: 0.5888690576919966 ) ), @@ -129,9 +189,21 @@ class Capsule2ParametricTests: XCTestCase { center: .init(x: -150.0, y: -30.0), radius: 60.00000000000001, startAngle: Angle(radians: 5.012688000386433), - sweepAngle: Angle(radians: -2.8148954755916673) + sweepAngle: Angle(radians: -1.2440991487967707) ), startPeriod: 0.8312933933546295, + endPeriod: 0.9058566400342528 + ) + ), + .circleArc2( + .init( + circleArc: .init( + center: .init(x: -150.0, y: -30.0), + radius: 60.00000000000001, + startAngle: Angle(radians: 3.768588851589662), + sweepAngle: Angle(radians: -1.5707963267948966) + ), + startPeriod: 0.9058566400342528, endPeriod: 1.0 ) ), diff --git a/Tests/GeometriaClippingTests/2D/Simplexes/CircleArc2SimplexTests.swift b/Tests/GeometriaClippingTests/2D/Simplexes/CircleArc2SimplexTests.swift index cd3e3b11..a4a287dd 100644 --- a/Tests/GeometriaClippingTests/2D/Simplexes/CircleArc2SimplexTests.swift +++ b/Tests/GeometriaClippingTests/2D/Simplexes/CircleArc2SimplexTests.swift @@ -87,4 +87,100 @@ class CircleArc2SimplexTests: XCTestCase { assertEqual(sut.compute(at: 0.5), .init(x: 0.0, y: 10.0), accuracy: 1e-14) } + + func testSplittingArcSegments_fullCircle_zeroStartAngle_halfPiMaxSweep_positiveSweep() { + let arc = CircleArc2D( + center: .init(x: 50, y: 70), + radius: 200, + startAngle: 0, + sweepAngle: Angle.pi * 2.0 + ) + let sut = Sut.splittingArcSegments( + arc, + startPeriod: 0.0, + endPeriod: 1.0, + maxAbsoluteSweepAngle: .pi / 2.0 + ) + + TestFixture.beginFixture { fixture in + let asSimplexes = sut.map(Parametric2GeometrySimplex.circleArc2) + fixture.add(asSimplexes, category: "result") + + fixture.assertEquals( + sut.reduce(0.0, { $0 + $1.sweepAngle.radians }), + arc.sweepAngle.radians, + accuracy: 1e-13 + ) + + fixture.assertEquals( + asSimplexes, + accuracy: 1e-13, + [GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 0.0), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.0, endPeriod: 0.25)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 1.5707963267948966), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.25, endPeriod: 0.5)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 3.141592653589793), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.5, endPeriod: 0.75)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 4.71238898038469), sweepAngle: Angle(radians: 1.5707963267948966)), startPeriod: 0.75, endPeriod: 1.0))] + ) + } + } + + func testSplittingArcSegments_fullCircle_zeroStartAngle_halfPiMaxSweep_negativeSweep() { + let arc = CircleArc2D( + center: .init(x: 50, y: 70), + radius: 200, + startAngle: 0, + sweepAngle: -Angle.pi * 2.0 + ) + let sut = Sut.splittingArcSegments( + arc, + startPeriod: 0.0, + endPeriod: 1.0, + maxAbsoluteSweepAngle: .pi / 2.0 + ) + + TestFixture.beginFixture { fixture in + let asSimplexes = sut.map(Parametric2GeometrySimplex.circleArc2) + fixture.add(asSimplexes, category: "result") + + fixture.assertEquals( + sut.reduce(0.0, { $0 + $1.sweepAngle.radians }), + arc.sweepAngle.radians, + accuracy: 1e-13 + ) + + fixture.assertEquals( + asSimplexes, + accuracy: 1e-13, + [GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 0.0), sweepAngle: Angle(radians: -1.5707963267948966)), startPeriod: 0.0, endPeriod: 0.25)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: -1.5707963267948966), sweepAngle: Angle(radians: -1.5707963267948966)), startPeriod: 0.25, endPeriod: 0.5)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: -3.141592653589793), sweepAngle: Angle(radians: -1.5707963267948966)), startPeriod: 0.5, endPeriod: 0.75)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: -4.71238898038469), sweepAngle: Angle(radians: -1.5707963267948966)), startPeriod: 0.75, endPeriod: 1.0))] + ) + } + } + + func testSplittingArcSegments_twoThirdsCircle_fifthStartAngle_ninthPiMaxSweep_positiveSweep() { + let arc = CircleArc2D( + center: .init(x: 50, y: 70), + radius: 200, + startAngle: .pi / 5.0, + sweepAngle: Angle.pi * (3.0 / 2.0) + ) + let sut = Sut.splittingArcSegments( + arc, + startPeriod: 0.0, + endPeriod: 1.0, + maxAbsoluteSweepAngle: .pi / 9.0 + ) + + TestFixture.beginFixture { fixture in + let asSimplexes = sut.map(Parametric2GeometrySimplex.circleArc2) + fixture.add(asSimplexes, category: "result") + + fixture.assertEquals( + sut.reduce(0.0, { $0 + $1.sweepAngle.radians }), + arc.sweepAngle.radians, + accuracy: 1e-13 + ) + + fixture.assertEquals( + asSimplexes, + accuracy: 1e-13, + [GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 0.6283185307179586), sweepAngle: Angle(radians: 0.3490658503988659)), startPeriod: 0.0, endPeriod: 0.07407407407407407)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 0.9773843811168246), sweepAngle: Angle(radians: 0.3490658503988659)), startPeriod: 0.07407407407407407, endPeriod: 0.14814814814814814)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 1.3264502315156905), sweepAngle: Angle(radians: 0.3490658503988659)), startPeriod: 0.14814814814814814, endPeriod: 0.2222222222222222)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 1.6755160819145563), sweepAngle: Angle(radians: 0.3490658503988659)), startPeriod: 0.2222222222222222, endPeriod: 0.2962962962962963)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 2.0245819323134224), sweepAngle: Angle(radians: 0.3490658503988659)), startPeriod: 0.2962962962962963, endPeriod: 0.37037037037037035)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 2.373647782712288), sweepAngle: Angle(radians: 0.3490658503988659)), startPeriod: 0.37037037037037035, endPeriod: 0.4444444444444444)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 2.722713633111154), sweepAngle: Angle(radians: 0.3490658503988659)), startPeriod: 0.4444444444444444, endPeriod: 0.5185185185185185)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 3.07177948351002), sweepAngle: Angle(radians: 0.3490658503988659)), startPeriod: 0.5185185185185185, endPeriod: 0.5925925925925926)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 3.420845333908886), sweepAngle: Angle(radians: 0.3490658503988659)), startPeriod: 0.5925925925925926, endPeriod: 0.6666666666666666)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 3.7699111843077517), sweepAngle: Angle(radians: 0.3490658503988659)), startPeriod: 0.6666666666666666, endPeriod: 0.7407407407407407)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 4.118977034706617), sweepAngle: Angle(radians: 0.3490658503988659)), startPeriod: 0.7407407407407407, endPeriod: 0.8148148148148149)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 4.468042885105484), sweepAngle: Angle(radians: 0.3490658503988659)), startPeriod: 0.8148148148148149, endPeriod: 0.8888888888888888)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 4.817108735504349), sweepAngle: Angle(radians: 0.3490658503988659)), startPeriod: 0.8888888888888888, endPeriod: 0.9629629629629628)), GeometriaClipping.Parametric2GeometrySimplex>.circleArc2(GeometriaClipping.CircleArc2Simplex>(circleArc: CircleArc2>(center: Vector2(x: 50.0, y: 70.0), radius: 200.0, startAngle: Angle(radians: 5.166174585903215), sweepAngle: Angle(radians: 0.17453292519943325)), startPeriod: 0.9629629629629628, endPeriod: 1.0))] + ) + } + } } diff --git a/Tests/GeometriaClippingTests/TestSupport/Capsule2Parametric+Tests.swift b/Tests/GeometriaClippingTests/TestSupport/Capsule2Parametric+Tests.swift new file mode 100644 index 00000000..daecd744 --- /dev/null +++ b/Tests/GeometriaClippingTests/TestSupport/Capsule2Parametric+Tests.swift @@ -0,0 +1,29 @@ +import Geometria +import GeometriaClipping + +extension Capsule2Parametric where Vector == Vector2D { + static func makeCapsuleSequence(_ segments: [(point: Vector2D, radius: Double)]) -> [Self] { + guard var last = segments.first else { + return [] + } + + var result: [Self] = [] + + for next in segments.dropFirst() { + defer { last = next } + + let capsule = Self( + start: last.point, + startRadius: last.radius, + end: next.point, + endRadius: next.radius, + startPeriod: 0.0, + endPeriod: 1.0 + ) + + result.append(capsule) + } + + return result + } +}