Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions BezierKit/BezierKitTests/Path+VectorBooleanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,8 @@ class PathVectorBooleanTests: XCTestCase {
XCTAssertTrue(a.contains(point, using: rule))
XCTAssertTrue(b.contains(point, using: rule))
XCTAssertTrue(result.contains(point, using: rule), "a union b should contain point that is in both a and b")
XCTAssertTrue(result.boundingBox.cgRect.insetBy(dx: -1, dy: -1).contains(a.boundingBox.cgRect), "resulting bounding box should contain a.boundingBox")
XCTAssertTrue(result.boundingBox.cgRect.insetBy(dx: -1, dy: -1).contains(b.boundingBox.cgRect), "resulting bounding box should contain b.boundingBox")
XCTAssertTrue(result.boundingBoxOfPath.cgRect.insetBy(dx: -1, dy: -1).contains(a.boundingBoxOfPath.cgRect), "resulting bounding box should contain a.boundingBoxOfPath")
XCTAssertTrue(result.boundingBoxOfPath.cgRect.insetBy(dx: -1, dy: -1).contains(b.boundingBoxOfPath.cgRect), "resulting bounding box should contain b.boundingBoxOfPath")
}

#endif
Expand Down Expand Up @@ -576,8 +576,8 @@ class PathVectorBooleanTests: XCTestCase {
let path = Path(cgPath: cgPath)
let result = path.crossingsRemoved(accuracy: 0.01)
// in practice .crossingsRemoved was cutting off most of the shape
XCTAssertEqual(path.boundingBox.size.x, result.boundingBox.size.x, accuracy: 1.0e-3)
XCTAssertEqual(path.boundingBox.size.y, result.boundingBox.size.y, accuracy: 1.0e-3)
XCTAssertEqual(path.boundingBoxOfPath.size.x, result.boundingBoxOfPath.size.x, accuracy: 1.0e-3)
XCTAssertEqual(path.boundingBoxOfPath.size.y, result.boundingBoxOfPath.size.y, accuracy: 1.0e-3)
XCTAssertEqual(result.components[0].numberOfElements, 5) // with crossings removed we should have 1 fewer curve (the last one)
}

Expand Down Expand Up @@ -605,8 +605,8 @@ class PathVectorBooleanTests: XCTestCase {
let path = Path(cgPath: cgPath)
let result = path.crossingsRemoved(accuracy: 1.0e-5)
// in practice .crossingsRemoved was cutting off most of the shape
XCTAssertEqual(path.boundingBox.size.x, result.boundingBox.size.x, accuracy: 1.0e-3)
XCTAssertEqual(path.boundingBox.size.y, result.boundingBox.size.y, accuracy: 1.0e-3)
XCTAssertEqual(path.boundingBoxOfPath.size.x, result.boundingBoxOfPath.size.x, accuracy: 1.0e-3)
XCTAssertEqual(path.boundingBoxOfPath.size.y, result.boundingBoxOfPath.size.y, accuracy: 1.0e-3)
}

func testCrossingsRemovedThirdRealWorldCase() {
Expand Down
20 changes: 10 additions & 10 deletions BezierKit/BezierKitTests/PathComponentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,9 @@ class PathComponentTests: XCTestCase {
}

func testBoundingBox() {
let p = PathComponent(curves: [line1, line2])
XCTAssertEqual(p.boundingBox, BoundingBox(min: CGPoint(x: 1.0, y: -1.0), max: CGPoint(x: 13.0, y: 5.0))) // just the union of the two bounding boxes
}

func testBoundingBoxOfPath() {
let point1 = CGPoint(x: 3, y: -2)
let pointComponent = PathComponent(points: [point1], orders: [0])
XCTAssertEqual(pointComponent.boundingBoxOfPath, BoundingBox(p1: CGPoint(x: 3, y: -2), p2: CGPoint(x: 3, y: -2)))
XCTAssertEqual(pointComponent.boundingBox, BoundingBox(p1: CGPoint(x: 3, y: -2), p2: CGPoint(x: 3, y: -2)))

let line = LineSegment(p0: CGPoint(x: 1, y: 2),
p1: CGPoint(x: 5, y: 3))
Expand All @@ -41,10 +36,15 @@ class PathComponentTests: XCTestCase {
p2: CGPoint(x: -1, y: 4),
p3: CGPoint(x: 1, y: 2))

XCTAssertEqual(PathComponent(curve: line).boundingBoxOfPath, BoundingBox(p1: CGPoint(x: 1, y: 2), p2: CGPoint(x: 5, y: 3)))
XCTAssertEqual(PathComponent(curve: quadratic).boundingBoxOfPath, BoundingBox(p1: CGPoint(x: 3, y: 3), p2: CGPoint(x: 5, y: 6)))
XCTAssertEqual(PathComponent(curve: cubic).boundingBoxOfPath, BoundingBox(p1: CGPoint(x: -1, y: 2), p2: CGPoint(x: 3, y: 6)))
XCTAssertEqual(PathComponent(curves: [line, quadratic, cubic]).boundingBoxOfPath, BoundingBox(p1: CGPoint(x: -1, y: 2), p2: CGPoint(x: 5, y: 6)))
XCTAssertEqual(PathComponent(curve: line).boundingBox, BoundingBox(p1: CGPoint(x: 1, y: 2), p2: CGPoint(x: 5, y: 3)))
XCTAssertEqual(PathComponent(curve: quadratic).boundingBox, BoundingBox(p1: CGPoint(x: 3, y: 3), p2: CGPoint(x: 5, y: 6)))
XCTAssertEqual(PathComponent(curve: cubic).boundingBox, BoundingBox(p1: CGPoint(x: -1, y: 2), p2: CGPoint(x: 3, y: 6)))
XCTAssertEqual(PathComponent(curves: [line, quadratic, cubic]).boundingBox, BoundingBox(p1: CGPoint(x: -1, y: 2), p2: CGPoint(x: 5, y: 6)))
}

func testBoundingBoxOfPath() {
let p = PathComponent(curves: [line1, line2])
XCTAssertEqual(p.boundingBoxOfPath, BoundingBox(min: CGPoint(x: 1.0, y: -1.0), max: CGPoint(x: 13.0, y: 5.0))) // just the union of the two bounding boxes
}

func testOffset() {
Expand Down
26 changes: 21 additions & 5 deletions BezierKit/BezierKitTests/PathTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ class PathTests: XCTestCase {
control2: CGPoint(x: 212.02163105179878, y: 108.14905966376985))
let path = Path(cgPath: cgPath)

XCTAssertFalse(path.boundingBox.contains(point)) // the point is not even in the bounding box of the path!
XCTAssertFalse(path.boundingBoxOfPath.contains(point)) // the point is not even in the tight bounding box of the path!
XCTAssertFalse(path.contains(point, using: .evenOdd))
XCTAssertFalse(path.contains(point, using: .winding))
}
Expand Down Expand Up @@ -919,19 +919,35 @@ class PathTests: XCTestCase {

#endif

func testBoundingBoxOfPath() {
XCTAssertEqual(Path().boundingBoxOfPath, BoundingBox.empty)
func testBoundingBox() {
XCTAssertEqual(Path().boundingBox, BoundingBox.empty)
let quad1 = QuadraticCurve(p0: CGPoint(x: 1, y: 2),
p1: CGPoint(x: 2, y: 4),
p2: CGPoint(x: 3, y: 2))
let quad2 = QuadraticCurve(p0: CGPoint(x: 3, y: 2),
p1: CGPoint(x: 2, y: 0),
p2: CGPoint(x: 1, y: 2))
let path1 = Path(curve: quad1)
XCTAssertEqual(path1.boundingBoxOfPath, BoundingBox(p1: CGPoint(x: 1, y: 2), p2: CGPoint(x: 3, y: 4)))
XCTAssertEqual(path1.boundingBox, BoundingBox(p1: CGPoint(x: 1, y: 2), p2: CGPoint(x: 3, y: 4)))
let path2 = Path(components: [PathComponent(curve: quad1),
PathComponent(curve: quad2)])
XCTAssertEqual(path2.boundingBoxOfPath, BoundingBox(p1: CGPoint(x: 1, y: 0), p2: CGPoint(x: 3, y: 4)))
XCTAssertEqual(path2.boundingBox, BoundingBox(p1: CGPoint(x: 1, y: 0), p2: CGPoint(x: 3, y: 4)))
#if canImport(CoreGraphics)
let cgPath = CGMutablePath()
cgPath.move(to: CGPoint(x: 1, y: 2))
cgPath.addQuadCurve(to: CGPoint(x: 3, y: 2), control: CGPoint(x: 2, y: 4))
XCTAssertEqual(Path(cgPath: cgPath).boundingBox.cgRect, cgPath.boundingBox)
#endif
}

func testBoundingBoxOfPath() {
XCTAssertEqual(Path().boundingBoxOfPath, BoundingBox.empty)
#if canImport(CoreGraphics)
let cgPath = CGMutablePath()
cgPath.move(to: CGPoint(x: 1, y: 2))
cgPath.addQuadCurve(to: CGPoint(x: 3, y: 2), control: CGPoint(x: 2, y: 4))
XCTAssertEqual(Path(cgPath: cgPath).boundingBoxOfPath.cgRect, cgPath.boundingBoxOfPath)
#endif
}

#if !os(WASI)
Expand Down
2 changes: 1 addition & 1 deletion BezierKit/Library/Path+Projection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public extension Path {
private func searchForClosestLocation(to point: CGPoint, maximumDistance: CGFloat, requireBest: Bool) -> (point: CGPoint, location: IndexedPathLocation)? {
// sort the components by proximity to avoid searching distant components later on
let tuples: [ComponentTuple] = self.components.enumerated().map { i, component in
let boundingBox = component.boundingBox
let boundingBox = component.boundingBoxOfPath
let upper = boundingBox.upperBoundOfDistance(to: point)
return (component: component, index: i, upperBound: upper)
}.sorted(by: { $0.upperBound < $1.upperBound })
Expand Down
7 changes: 4 additions & 3 deletions BezierKit/Library/Path.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,12 @@ open class Path: NSObject, @unchecked Sendable {
return self.components.isEmpty // components are not allowed to be empty
}

/// The bounding box of the path. The bounding box is the smallest rectangle completely enclosing all points in the path, including control points for Bézier cubic and quadratic curves.
public var boundingBox: BoundingBox {
return self.lock.sync { self._boundingBox }
}

/// the smallest bounding box completely enclosing the points of the path, includings its control points.
/// The path bounding box of the path. The path bounding box is the smallest rectangle completely enclosing all points in the path, *not* including control points for Bézier cubic and quadratic curves.
public var boundingBoxOfPath: BoundingBox {
return self.lock.sync { self._boundingBoxOfPath }
}
Expand Down Expand Up @@ -132,7 +133,7 @@ open class Path: NSObject, @unchecked Sendable {
}

public func intersections(with other: Path, accuracy: CGFloat = BezierKit.defaultIntersectionAccuracy) -> [PathIntersection] {
guard self.boundingBox.overlaps(other.boundingBox) else {
guard self.boundingBoxOfPath.overlaps(other.boundingBoxOfPath) else {
return []
}
var intersections: [PathIntersection] = []
Expand Down Expand Up @@ -339,7 +340,7 @@ open class Path: NSObject, @unchecked Sendable {
var owner: PathComponent?
for outer in outerComponents.keys {
if let owner = owner {
guard outer.boundingBox.intersection(owner.boundingBox) == outer.boundingBox else { continue }
guard outer.boundingBoxOfPath.intersection(owner.boundingBoxOfPath) == outer.boundingBoxOfPath else { continue }
}
if outer.contains(component.startingPoint, using: rule) {
owner = outer
Expand Down
2 changes: 1 addition & 1 deletion BezierKit/Library/PathComponent+WindingCount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ internal extension PathComponent {
}

func windingCount(at point: CGPoint) -> Int {
guard self.isClosed, self.boundingBox.contains(point) else {
guard self.isClosed, self.boundingBoxOfPath.contains(point) else {
return 0
}
var windingCount: Int = 0
Expand Down
16 changes: 9 additions & 7 deletions BezierKit/Library/PathComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ open class PathComponent: NSObject, Reversible, Transformable, @unchecked Sendab

private var _hash: Int?

private lazy var _boundingBoxOfPath: BoundingBox = {
var boundingBoxOfPath = BoundingBox.empty
private lazy var _boundingBox: BoundingBox = {
var boundingBox = BoundingBox.empty
points.withUnsafeBufferPointer { buffer in
for point in buffer {
boundingBoxOfPath.union(point)
boundingBox.union(point)
}
}
return boundingBoxOfPath
return boundingBox
}()

internal var bvh: BoundingBoxHierarchy {
Expand Down Expand Up @@ -238,12 +238,14 @@ open class PathComponent: NSObject, Reversible, Transformable, @unchecked Sendab
return self.curves.reduce(0.0) { $0 + $1.length() }
}

public var boundingBox: BoundingBox {
/// The path bounding box of the path component. The path bounding box is the smallest rectangle completely enclosing all points in the path component, *not* including control points for Bézier cubic and quadratic curves.
public var boundingBoxOfPath: BoundingBox {
return self.bvh.boundingBox
}

public var boundingBoxOfPath: BoundingBox {
return self.lock.sync { _boundingBoxOfPath }
/// The bounding box of the path component. The bounding box is the smallest rectangle completely enclosing all points in the path component, including control points for Bézier cubic and quadratic curves.
public var boundingBox: BoundingBox {
return self.lock.sync { _boundingBox }
}

public var isClosed: Bool {
Expand Down
Loading