Skip to content

Commit 0322566

Browse files
committed
Add 2D split/trimmed operations and refactor
- Add split(along: Line2D) and trimmed(along: Line2D) for 2D geometry - Add flipped property to Line2D - Move Direction.opposite to generic extension (works for any dimensionality) - Split Split.swift into Split2D.swift and Split3D.swift
1 parent 0f9e633 commit 0322566

File tree

4 files changed

+98
-5
lines changed

4 files changed

+98
-5
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import Foundation
2+
3+
public extension Geometry2D {
4+
/// Splits the geometry into two parts along the specified line.
5+
///
6+
/// This method slices the geometry in two using a given line and passes the resulting parts
7+
/// to a closure for further transformation or arrangement.
8+
///
9+
/// - Parameters:
10+
/// - line: The `Line2D` used to split the geometry.
11+
/// - reader: A closure that receives the two resulting geometry parts (on opposite sides of the line)
12+
/// and returns a new composed geometry. The first geometry is the side facing the clockwise
13+
/// normal of the line (right side relative to the line's direction).
14+
///
15+
/// - Returns: A new geometry resulting from the closure.
16+
///
17+
/// ## Example
18+
/// ```swift
19+
/// Circle(diameter: 10)
20+
/// .split(along: Line2D(point: [0, 2], direction: .x)) { a, b in
21+
/// a.colored(.red)
22+
/// b.colored(.blue)
23+
/// }
24+
/// ```
25+
///
26+
func split(
27+
along line: Line2D,
28+
@GeometryBuilder2D reader: @Sendable @escaping (_ right: any Geometry2D, _ left: any Geometry2D) -> any Geometry2D
29+
) -> any Geometry2D {
30+
reader(trimmed(along: line), trimmed(along: line.flipped))
31+
}
32+
33+
/// Trims the geometry along the specified line, keeping only the portion on the clockwise side.
34+
///
35+
/// This method behaves like a one-sided split: it cuts the geometry by a line and removes everything
36+
/// on the opposite side. The result is the portion of the geometry that remains on the clockwise side
37+
/// of the line (right side relative to the line's direction).
38+
///
39+
/// - Parameter line: The `Line2D` defining the trimming boundary.
40+
/// - Returns: A new geometry containing only the portion on the clockwise side of the line.
41+
///
42+
/// ## Example
43+
/// ```swift
44+
/// Circle(diameter: 10)
45+
/// .trimmed(along: Line2D.y) // Keeps the right half
46+
/// ```
47+
///
48+
func trimmed(along line: Line2D) -> any Geometry2D {
49+
measuringBounds { geometry, box in
50+
let mask = buildTrimMask(for: box, along: line)
51+
geometry.intersecting { mask }
52+
}
53+
}
54+
}
55+
56+
private func buildTrimMask(for box: BoundingBox2D, along line: Line2D) -> Polygon {
57+
let margin = 1.0
58+
let expandedMin = box.minimum - Vector2D(margin, margin)
59+
let expandedMax = box.maximum + Vector2D(margin, margin)
60+
61+
let corners = [
62+
expandedMin,
63+
Vector2D(expandedMax.x, expandedMin.y),
64+
expandedMax,
65+
Vector2D(expandedMin.x, expandedMax.y)
66+
]
67+
68+
// Project corners onto the line direction to find extent
69+
let projections = corners.map { corner in
70+
(corner - line.point) line.direction.unitVector
71+
}
72+
let minT = projections.min()! - margin
73+
let maxT = projections.max()! + margin
74+
75+
// Line endpoints within the bounding box
76+
let lineStart = line.point(at: minT)
77+
let lineEnd = line.point(at: maxT)
78+
79+
// Extend perpendicular to the line (clockwise normal side)
80+
let normal = line.direction.clockwiseNormal.unitVector
81+
let extent = (expandedMax - expandedMin).magnitude + margin
82+
83+
// Build a quad covering the clockwise side of the line
84+
return Polygon([
85+
lineStart,
86+
lineEnd,
87+
lineEnd + normal * extent,
88+
lineStart + normal * extent
89+
])
90+
}
File renamed without changes.

Sources/Cadova/Values/Spatial/Direction.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public extension Direction {
4949
init(bisecting a: D.Vector, _ b: D.Vector) {
5050
self.init((a.normalized + b.normalized) / 2.0)
5151
}
52+
53+
/// The opposite of this direction.
54+
var opposite: Self { Self(-unitVector) }
5255
}
5356

5457
public extension Direction <D3> {
@@ -59,11 +62,6 @@ public extension Direction <D3> {
5962
/// The Z component of the direction.
6063
var z: Double { unitVector.z }
6164

62-
/// The opposite of this direction
63-
var opposite: Self {
64-
Self(-unitVector)
65-
}
66-
6765
/// Creates a direction from x, y, z components.
6866
/// - Parameters:
6967
/// - x: The x component of the direction.

Sources/Cadova/Values/Spatial/Line.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ public extension Line<D2> {
143143
Line(point: point + direction.clockwiseNormal.unitVector * amount, direction: direction)
144144
}
145145

146+
/// Returns the same line with its direction reversed.
147+
var flipped: Self {
148+
Line(point: point, direction: direction.opposite)
149+
}
150+
146151
/// A line extending along the X axis from the origin.
147152
static let x = Line(axis: .x)
148153
/// A line extending along the Y axis from the origin.

0 commit comments

Comments
 (0)