-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve legibility when many elements are in close proximity.
Resolves #2.
- Loading branch information
Showing
8 changed files
with
369 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import Cocoa | ||
|
||
extension Array where Element: Positionable, Element: Equatable { | ||
|
||
/// Implements a naive algorithm to reduce "crowding", by filtering out | ||
/// elements that are positioned too closely together. | ||
/// | ||
/// If two elements have duplicate frames, one of the elements is removed. | ||
/// | ||
/// If two elements have frames that overlap more than | ||
/// `intersectionThreshold`%, the element with the smaller frame is | ||
/// removed. | ||
/// | ||
/// If two elements have frames that do not intersect, but do intersect | ||
/// with padding applied (see `paddingX` and `paddingY` parameters), the | ||
/// element with the smaller frame is removed. | ||
/// | ||
/// - Parameter intersectionThreshold: the percentage (expressed as a float | ||
/// ranging between 0 and 1) that two elements' frames need to overlap in | ||
/// order for the element with the smaller frame to be removed. | ||
/// | ||
/// - Parameter paddingX: the number of pixels to increase the width of an | ||
/// element's frame by, when testing to see if the padded frame intersects | ||
/// with another element. | ||
/// | ||
/// - Parameter paddingY: the number of pixels to increase the height of an | ||
/// element's frame by, when testing to see if the padded frame intersects | ||
/// with another element. | ||
func reducingCrowding(intersectionThreshold: CGFloat = 0.1, paddingX: CGFloat = 0, paddingY: CGFloat = 0) -> [Element] { | ||
|
||
var discard = [Element]() | ||
|
||
func nextPartialResult(accumulator: [Element], element: Element) -> [Element] { | ||
|
||
let predicate: (Element) -> Bool = { | ||
|
||
// An element that is already in the accumulator. | ||
let accumulated = $0 | ||
|
||
// The element currently under consideration for inclusion in | ||
// the accumulator. | ||
let candidate = element | ||
|
||
if candidate.frame == accumulated.frame { | ||
// Do not include the candidate in the accumulator. | ||
return true | ||
} | ||
|
||
let framesIntersect = candidate.frame.intersects(accumulated.frame) | ||
|
||
let percentageOverlapping = candidate.frame.percentageOverlapping(accumulated.frame) | ||
|
||
let paddedFramesIntersect = candidate.frame.insetBy(dx: -paddingX, dy: -paddingY).intersects(accumulated.frame) | ||
|
||
if (framesIntersect && percentageOverlapping >= intersectionThreshold) || (!framesIntersect && paddedFramesIntersect) { | ||
// To reduce crowding, only one element is kept (either the | ||
// candidate, or the accumulated). We choose the element | ||
// with the larger area. | ||
if candidate.frame.area < accumulated.frame.area { | ||
// The frame with the larger area is already in the | ||
// accumulator. Don't include the candidate. | ||
return true | ||
} else { | ||
// Include the candidate in the accumulator, because | ||
// its frame has a larger area. (However, the other | ||
// element is already in the accumulator, and needs to | ||
// be removed.) | ||
discard.append(accumulated) | ||
return false | ||
} | ||
} | ||
|
||
// No other criteria disqualified the candidate; add it to the | ||
// accumulator. | ||
return false | ||
} | ||
|
||
if accumulator.contains(where: predicate) { | ||
return accumulator | ||
} else { | ||
return accumulator + [element] | ||
} | ||
} | ||
|
||
return self.reduce([], nextPartialResult).filter { | ||
!discard.contains($0) | ||
} | ||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import Cocoa | ||
|
||
protocol Positionable { | ||
var frame: CGRect { get } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import XCTest | ||
@testable import Scoot | ||
|
||
func makeRect(width: CGFloat, height: CGFloat) -> CGRect { | ||
CGRect(origin: .zero, size: CGSize(width: width, height: height)) | ||
} | ||
|
||
class GeometryTests: XCTestCase { | ||
|
||
func testRectArea() { | ||
XCTAssertEqual(makeRect(width: 0, height: 0).area, 0) | ||
XCTAssertEqual(makeRect(width: 0, height: 1).area, 0) | ||
XCTAssertEqual(makeRect(width: 2, height: 4).area, 8) | ||
XCTAssertEqual(makeRect(width: 4, height: 2).area, 8) | ||
XCTAssertEqual(makeRect(width: -4, height: 2).area, 8) | ||
XCTAssertEqual(makeRect(width: 4, height: -2).area, 8) | ||
XCTAssertEqual(makeRect(width: 16, height: 4).area, 64) | ||
} | ||
|
||
func testRectNoOverlap() { | ||
let a = CGRect(x: 1, y: 1, width: 4, height: 4) | ||
let b = CGRect(x: 9, y: 9, width: 4, height: 4) | ||
|
||
XCTAssertEqual(a.percentageOverlapping(b), 0) | ||
|
||
let c = CGRect(x: 1, y: 1, width: 0, height: 0) | ||
let d = CGRect(x: 2, y: 2, width: 0, height: 0) | ||
|
||
XCTAssertEqual(c.percentageOverlapping(d), 0) | ||
} | ||
|
||
func testRectFullOverlap() { | ||
let a = CGRect(x: 0, y: 0, width: 4, height: 4) | ||
let b = CGRect(x: 0, y: 0, width: 4, height: 4) | ||
|
||
XCTAssertEqual(a.percentageOverlapping(b), 1) | ||
|
||
let c = CGRect(x: 0, y: 0, width: 4, height: 4) | ||
let d = CGRect(x: 1, y: 1, width: 2, height: 2) | ||
|
||
XCTAssertEqual(c.percentageOverlapping(d), 1) | ||
|
||
let e = CGRect(x: 1, y: 1, width: 2, height: 2) | ||
let f = CGRect(x: 0, y: 0, width: 4, height: 4) | ||
|
||
XCTAssertEqual(e.percentageOverlapping(f), 1) | ||
|
||
let g = CGRect(x: 0, y: 0, width: 4, height: 4) | ||
let h = CGRect(x: 2, y: 2, width: 2, height: 2) | ||
|
||
XCTAssertEqual(g.percentageOverlapping(h), 1) | ||
} | ||
|
||
func testRectPartialOverlap() { | ||
let a = CGRect(x: 0, y: 0, width: 4, height: 4) | ||
let b = CGRect(x: 2, y: 2, width: 4, height: 4) | ||
|
||
XCTAssertEqual(a.percentageOverlapping(b), 0.25) | ||
|
||
let c = CGRect(x: 0, y: 0, width: 4, height: 4) | ||
let d = CGRect(x: 3, y: 3, width: 4, height: 4) | ||
|
||
XCTAssertEqual(c.percentageOverlapping(d), 0.0625) | ||
|
||
let e = CGRect(x: 0, y: 0, width: 4, height: 4) | ||
let f = CGRect(x: 1, y: 1, width: 4, height: 4) | ||
|
||
XCTAssertEqual(e.percentageOverlapping(f), 0.5625) | ||
|
||
let g = CGRect(x: 1, y: 1, width: 4, height: 4) | ||
let h = CGRect(x: 1.2, y: 1.2, width: 4, height: 4) | ||
|
||
XCTAssertEqual(g.percentageOverlapping(h), 0.9025) | ||
|
||
let i = CGRect(x: -1, y: 0, width: 10, height: 10) | ||
let j = CGRect(x: 1, y: 0, width: 10, height: 10) | ||
|
||
XCTAssertEqual(i.percentageOverlapping(j), 0.8) | ||
|
||
let k = CGRect(x: -1, y: 0, width: 10, height: 10) | ||
let l = CGRect(x: 1, y: 0, width: 30, height: 10) | ||
|
||
XCTAssertEqual(k.percentageOverlapping(l), 0.4) | ||
} | ||
|
||
} |
Oops, something went wrong.