Skip to content

Commit

Permalink
Improve polygon self-intersection test and add line segments tests
Browse files Browse the repository at this point in the history
  • Loading branch information
iby committed Jul 5, 2019
1 parent 6d9e882 commit aceeb03
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 82 deletions.
4 changes: 4 additions & 0 deletions Metron.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
17FD051CBBE0D4CA2EE9CA37 /* Corner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD04272A5CBE2F8A2CBACD /* Corner.swift */; };
17FD052C5F492326F58ABAEC /* CoordinateSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD09361D239018E7F80B7D /* CoordinateSystem.swift */; };
17FD0567DE161BDE7DAB0C69 /* Square.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD06098D71B0C119723C87 /* Square.swift */; };
17FD062264AE87E2E1DBE6DB /* LineSegmentTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0D6FD911DB09F9457BC5 /* LineSegmentTest.swift */; };
17FD06C2F9146A204B8A8701 /* CornerPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD054EC3163EA3653332A2 /* CornerPosition.swift */; };
17FD06CB597B4E0E923665DD /* LineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD020AD14A77B2F7CA527B /* LineSegment.swift */; };
17FD07852E97C5E2F1F8A43B /* CGVector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FD0F6D6A6B8F4C0619887D /* CGVector.swift */; };
Expand Down Expand Up @@ -98,6 +99,7 @@
17FD0AFB0E6B9F7C1F9EA618 /* Demo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Demo.plist; sourceTree = "<group>"; };
17FD0BA0752204C91ADB3C25 /* Edge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Edge.swift; sourceTree = "<group>"; };
17FD0C776D9B44C3484681CC /* Triangle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Triangle.swift; sourceTree = "<group>"; };
17FD0D6FD911DB09F9457BC5 /* LineSegmentTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LineSegmentTest.swift; sourceTree = "<group>"; };
17FD0DC842E479E8FF85A43B /* .travis.yml */ = {isa = PBXFileReference; lastKnownFileType = file.yml; path = .travis.yml; sourceTree = "<group>"; };
17FD0DDBF7AADC02B5A70AD5 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
17FD0DDE6E505A3222304E75 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
Expand Down Expand Up @@ -248,6 +250,7 @@
children = (
17FD0694950A7240F4CCDCC8 /* PolygonTest.swift */,
17FD07381F6A003FF9433268 /* Test.swift */,
17FD0D6FD911DB09F9457BC5 /* LineSegmentTest.swift */,
);
path = Test;
sourceTree = "<group>";
Expand Down Expand Up @@ -455,6 +458,7 @@
files = (
17FD049E93AC67C10A713A9E /* Test.swift in Sources */,
E62C8DE12210782700E0A783 /* PolygonTest.swift in Sources */,
17FD062264AE87E2E1DBE6DB /* LineSegmentTest.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
8 changes: 5 additions & 3 deletions source/Metron/Extension/CGRect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,15 @@ extension CGRect: Shape {
}

extension CGRect: PolygonType {
public var edgeCount: Int {
return 4
}
public var points: [CGPoint] {
return CoordinateSystem.default.corners.map { self.corner($0) }
}

public var lineSegments: [LineSegment] {
return CoordinateSystem.default.edges.map { lineSegment(for: $0) }
}

public var edgeCount: Int {
return 4
}
}
3 changes: 1 addition & 2 deletions source/Metron/Geometry/General.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ extension Comparable {
/// - returns: true if the value is between the
/// provided lower and upper limits.
public func between(lower: Self, upper: Self) -> Bool {
return self >= min(lower, upper) &&
self <= max(lower, upper)
return self >= min(lower, upper) && self <= max(lower, upper)
}
}

Expand Down
66 changes: 25 additions & 41 deletions source/Metron/Shape/Line.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ public extension Line {
definition = .vertical(x: a.x)
} else {
let slope = (a.y - b.y) / dx
definition = .sloped(slope: slope,
yIntercept: (slope * -a.x) + a.y)
definition = .sloped(slope: slope, yIntercept: (slope * -a.x) + a.y)
}
}

Expand Down Expand Up @@ -70,17 +69,13 @@ public extension Line {

/// - returns: True if this line is vertical.
public var isVertical: Bool {
if case .vertical(_) = definition {
return true
}
if case .vertical(_) = definition { return true }
return false
}

/// - returns: True if this line is horizontal (slope equals zero).
public var isHorizontal: Bool {
if case let .sloped(slope, _) = definition {
return abs(slope) == 0.0
}
if case let .sloped(slope, _) = definition { return abs(slope) == 0.0 }
return false
}

Expand All @@ -97,32 +92,22 @@ public extension Line {
/// x = (y-b)/m
public func point(atY y: CGFloat) -> CGPoint? {
switch definition {
case let .sloped(slope, yIntercept):
guard slope != 0 else { return nil }
return CGPoint(x: (y - yIntercept) / slope, y: y)
case let .vertical(x):
return CGPoint(x: x, y: y)
case let .sloped(slope, yIntercept): return slope == 0 ? nil : CGPoint(x: (y - yIntercept) / slope, y: y)
case let .vertical(x): return CGPoint(x: x, y: y)
}
}

/// - returns: The value for x on this line where y is 0.
public var xIntercept: CGFloat? {
switch definition {
case let .sloped(slope, yIntercept):
guard slope != 0 else { return nil }
return -yIntercept / slope
case let .vertical(x):
return x
case let .sloped(slope, yIntercept): return slope == 0 ? nil : -yIntercept / slope
case let .vertical(x): return x
}
}

/// - returns: The segment of this line that is between the provided points.
public func segment(between p1: CGPoint, and p2: CGPoint) -> LineSegment? {
let a = point(atX: p1.x) ?? point(atY: p1.y)
let b = point(atX: p2.x) ?? point(atY: p2.y)
if let a = a, let b = b {
return LineSegment(a: a, b: b)
}
if let a = point(atX: p1.x) ?? point(atY: p1.y), let b = point(atX: p2.x) ?? point(atY: p2.y) { return LineSegment(a: a, b: b) }
return nil
}

Expand All @@ -143,40 +128,39 @@ public extension Line {
/// Will be nil for parallel lines.
public func intersection(with line: Line) -> CGPoint? {
switch (self.definition, line.definition) {
case let (.sloped(slope1, yIntercept1),
.sloped(slope2, yIntercept2)):
let dSlope = slope1 - slope2
guard dSlope != 0.0 else { return nil } // parallel
return point(atX: (yIntercept2 - yIntercept1) / dSlope)
case let (.vertical(x), .sloped(slope, yIntercept)),
let (.sloped(slope, yIntercept), .vertical(x)):
case let (.sloped(slope1, yIntercept1), .sloped(slope2, yIntercept2)):
return { $0 == 0 ? nil : point(atX: (yIntercept2 - yIntercept1) / $0) }(slope1 - slope2) // Zero slope-diff means lines are parallel.
case let (.vertical(x), .sloped(slope, yIntercept)), let (.sloped(slope, yIntercept), .vertical(x)):
return CGPoint(x: x, y: slope * x + yIntercept)
default: return nil
default:
return nil
}
}

/// - returns: The intersection of this `Line` with the provided `LineSegment`.
/// Similar to intersection(with line:…), but this also checks if a
/// found intersection is also between the line segment's start and end points.
public func intersection(with lineSegment: LineSegment) -> CGPoint? {
let line = lineSegment.line
if let intersection = self.intersection(with: line) {
if let intersection: CGPoint = self.intersection(with: lineSegment.line) {
return lineSegment.contains(intersection) ? intersection : nil
} else if lineSegment.isVertical, let intersection = point(atX: lineSegment.minX) {
} else if lineSegment.isVertical, let intersection: CGPoint = point(atX: lineSegment.minX) {
return lineSegment.contains(intersection) ? intersection : nil
} else {
return nil
}
return nil
}

/// - returns: true if this `Line` runs parallel along the provided `Line`.
/// Always true for two vertical lines. True for sloped lines with
/// equal slopes.
public func isParallel(to line: Line) -> Bool {
switch (self.definition, line.definition) {
switch (definition, line.definition) {
case let (.sloped(slope1, _), .sloped(slope2, _)):
return slope1 == slope2
case (.vertical, .vertical): return true
default: return false
case (.vertical, .vertical):
return true
default:
return false
}
}

Expand Down Expand Up @@ -204,11 +188,11 @@ extension Line.Definition: Equatable {
public static func ==(lhs: Line.Definition, rhs: Line.Definition) -> Bool {
switch (lhs, rhs) {
case let (.sloped(lhsSlope, lhsYIntercept), .sloped(rhsSlope, rhsYIntercept)):
return lhsSlope == rhsSlope &&
lhsYIntercept == rhsYIntercept
return lhsSlope == rhsSlope && lhsYIntercept == rhsYIntercept
case let (.vertical(lhsX), .vertical(rhsX)):
return lhsX == rhsX
default: return false
default:
return false
}
}
}
Expand Down
48 changes: 20 additions & 28 deletions source/Metron/Shape/Polygon.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import CoreGraphics

/**
* Represents any shape that can be defined as a `Polygon`,
* i.e. a number (> 2) of connected points.
*/
/// Represents any shape that can be defined as a `Polygon`, i.e., where the number of connected points if >= 3.
public protocol PolygonType: Shape {
var edgeCount: Int { get }
var points: [CGPoint] { get }
var lineSegments: [LineSegment] { get }

/// The number of edges the polygon has.
var edgeCount: Int { get }
}


extension PolygonType {
/// - returns: A `Polygon` representing this type.
/// - returns: A `Polygon` representing this type. Todo: Deprecate and use convenience Polygon init.
public var polygon: Polygon {
return Polygon(points: points)
}

public var edgeCount: Int {
return self.points.count
}
}

/// A `Polygon` is a shape existing of at least three connected points (and thus of at least three sides).
Expand All @@ -32,12 +35,9 @@ public struct Polygon: PolygonType {

public extension Polygon {

/// Initializes a `Polygon` given a number of `LineSegments`.
/// The first lineSegment is taken as starting point,
/// from which a connecting lineSegment is saught,
/// until all lineSegments are connected.
/// Then the default initializer is called, passing
/// all points of the connected lineSegments.
/// Initializes a `Polygon` given a number of `LineSegments`. The first lineSegment is taken as starting
/// point, from which a connecting lineSegment is sought, until all lineSegments are connected. Then the default
/// initializer is called, passing all points of the connected lineSegments.
public init?(lineSegments: [LineSegment]) {
var remainingLineSegments = lineSegments
var points = [CGPoint]()
Expand All @@ -59,29 +59,22 @@ public extension Polygon {

/// The individual line segments between consecutive points of this polygon.
public var lineSegments: [LineSegment] {
var lineSegments = [LineSegment]()
(0..<edgeCount).forEach { pointIndex in
let point = points[pointIndex]
let nextPoint = points[(pointIndex + 1) % edgeCount]
lineSegments.append(LineSegment(a: point, b: nextPoint))
}
return lineSegments
}

/// The number of edges of this polygon.
public var edgeCount: Int {
return points.count
// Todo: Must be cached.
return zip(points, points.suffix(from: 1) + points.prefix(1)).map({ LineSegment(a: $0, b: $1) })
}

/// - returns: True if line segments of this polygon intersect each other.
public var isSelfIntersecting: Bool {
// Might be implemented more efficiently
var lineSegments = self.lineSegments

// Todo: Might be implemented more efficiently…
var lineSegments: [LineSegment] = self.lineSegments

while let segment = lineSegments.popLast() {
if let _ = lineSegments.first(where: { $0.intersection(with: segment) != nil }) {
if lineSegments.contains(where: { $0.intersection(with: segment) != nil && !$0.connects(with: segment) }) {
return true
}
}

return false
}

Expand All @@ -107,7 +100,6 @@ public extension Polygon {
}

extension Polygon: Drawable {

public var path: CGPath? {
let path = CGMutablePath()
path.addLines(between: self.points)
Expand Down
8 changes: 5 additions & 3 deletions source/Metron/Shape/Square.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,18 @@ extension Square: Shape {
}

extension Square: PolygonType {
public var edgeCount: Int {
return 4
}
public var points: [CGPoint] {
let rect = self.rect
return CoordinateSystem.default.corners.map { rect.corner($0) }
}

public var lineSegments: [LineSegment] {
return rect.lineSegments
}

public var edgeCount: Int {
return 4
}
}

// MARK: CustomDebugStringConvertible
Expand Down
9 changes: 4 additions & 5 deletions source/Metron/Shape/Triangle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -266,18 +266,17 @@ public extension Triangle {
// MARK: Polygon

extension Triangle: PolygonType {

public var edgeCount: Int {
return 3
}

public var points: [CGPoint] {
return vertices.asArray
}

public var lineSegments: [LineSegment] {
return sides.asArray
}

public var edgeCount: Int {
return 3
}
}

extension Triangle: Drawable {
Expand Down
27 changes: 27 additions & 0 deletions source/Metron/Test/LineSegmentTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@testable import Metron
import CoreGraphics
import Foundation
import Nimble
import Quick

internal class LineSegmentSpec: Spec {
override internal func spec() {
it("can check if contains a point") {
var segments: [LineSegment] = [LineSegment((0, 0), (10, 0)), LineSegment((0, 0), (10, 10)), LineSegment((0, 0), (0, 10))]
segments += segments.map({ LineSegment(a: $0.b, b: $0.a) })

segments.forEach({
expect($0.contains($0.a)) == true
expect($0.contains($0.b)) == true
expect($0.contains($0.midpoint)) == true
})
}

it("can calculate intersection point with another segment") {
expect(LineSegment((0, 0), (10, 10)).intersection(with: LineSegment((0, 10), (10, 0)))) == CGPoint(5, 5)
expect(LineSegment((10, 10), (0, 0)).intersection(with: LineSegment((10, 0), (0, 10)))) == CGPoint(5, 5)
expect(LineSegment((0, 0), (10, 10)).intersection(with: LineSegment((0, 0), (10, -10)))) == CGPoint(0, 0)
expect(LineSegment((0, 0), (10, 10)).intersection(with: LineSegment((10, 10), (20, 20)))).to(beNil()) // Because parallel…
}
}
}
11 changes: 11 additions & 0 deletions source/Metron/Test/PolygonTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,16 @@ internal class PolygonSpec: Spec {

expect(polygon.lineSegments) == segments
}

it("can check if self intersecting") {
expect(Metron.Polygon(points: Array([(0, 0), (10, 5), (10, 0), (0, 5)])).isSelfIntersecting) == true
expect(Metron.Polygon(points: Array([(0, 0), (10, 0), (10, 5), (0, 5)])).isSelfIntersecting) == false
}

it("can calculate area") {
expect(Metron.Polygon(points: Array([(0, 0), (10, 0), (10, 5)])).area) == 25
expect(Metron.Polygon(points: Array([(0, 0), (10, 0), (10, 5), (0, 5)])).area) == 50
expect(Metron.Polygon(points: Array([(0, 0), (10, 5), (10, 0), (0, 5)])).area.isNaN) == true // Self-intersecting polygon area is not available.
}
}
}

0 comments on commit aceeb03

Please sign in to comment.