Skip to content

Commit df87d5e

Browse files
committed
Add whileAligned operation and tests
1 parent 49a35f3 commit df87d5e

File tree

2 files changed

+65
-0
lines changed

2 files changed

+65
-0
lines changed

Sources/Cadova/Abstract Layer/Operations/Aligned.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,33 @@ public extension Geometry {
2424
child.translated(bounds.translation(for: .init(merging: alignment)))
2525
}
2626
}
27+
28+
/// Temporarily aligns this geometry while performing operations, then restores its original position.
29+
///
30+
/// This helper aligns the geometry according to the provided alignment options, evaluates `operations` in that
31+
/// aligned coordinate space, and then applies the inverse translation to return the result to its original space.
32+
///
33+
/// - Parameters:
34+
/// - alignment: A list of alignment criteria specifying how the geometry should be aligned before evaluating
35+
/// `operations`.
36+
/// - operations: A builder that produces geometry to be evaluated in the aligned space.
37+
/// - Returns: The resulting geometry mapped back into the original coordinate space.
38+
///
39+
/// Example:
40+
/// ```
41+
/// Rectangle(x: 20, y: 10)
42+
/// .whileAligned(at: .center) {
43+
/// $0.rotated(45°)
44+
/// }
45+
/// ```
46+
///
47+
func whileAligned(
48+
at alignment: D.Alignment...,
49+
@GeometryBuilder<D> do operations: @Sendable @escaping (D.Geometry) -> D.Geometry
50+
) -> D.Geometry {
51+
measuringBounds { child, bounds in
52+
let translation = bounds.translation(for: .init(merging: alignment))
53+
return operations(child.translated(translation)).translated(-translation)
54+
}
55+
}
2756
}

Tests/Tests/Alignment.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,42 @@ struct AlignmentTests {
261261
#expect(bounds?.maximum.y 0)
262262
}
263263

264+
// MARK: - whileAligned
265+
266+
@Test func `2D whileAligned matches rotated around center`() async throws {
267+
let geometry = Rectangle(x: 10, y: 6)
268+
.translated(x: 20, y: 10)
269+
let viaWhileAligned = geometry
270+
.whileAligned(at: .center) { $0.rotated(90°) }
271+
let viaPivot = geometry.rotated(90°, around: .center)
272+
273+
let whileBounds = try await viaWhileAligned.bounds
274+
let pivotBounds = try await viaPivot.bounds
275+
276+
#expect(whileBounds?.minimum.x pivotBounds?.minimum.x)
277+
#expect(whileBounds?.maximum.x pivotBounds?.maximum.x)
278+
#expect(whileBounds?.minimum.y pivotBounds?.minimum.y)
279+
#expect(whileBounds?.maximum.y pivotBounds?.maximum.y)
280+
}
281+
282+
@Test func `3D whileAligned matches rotated around center`() async throws {
283+
let geometry = Box(x: 10, y: 6, z: 4)
284+
.translated(x: 20, y: 10, z: 5)
285+
let viaWhileAligned = geometry
286+
.whileAligned(at: .center) { $0.rotated(y: 90°) }
287+
let viaPivot = geometry.rotated(y: 90°, around: .center)
288+
289+
let whileBounds = try await viaWhileAligned.bounds
290+
let pivotBounds = try await viaPivot.bounds
291+
292+
#expect(whileBounds?.minimum.x pivotBounds?.minimum.x)
293+
#expect(whileBounds?.maximum.x pivotBounds?.maximum.x)
294+
#expect(whileBounds?.minimum.y pivotBounds?.minimum.y)
295+
#expect(whileBounds?.maximum.y pivotBounds?.maximum.y)
296+
#expect(whileBounds?.minimum.z pivotBounds?.minimum.z)
297+
#expect(whileBounds?.maximum.z pivotBounds?.maximum.z)
298+
}
299+
264300
// MARK: - Edge Cases
265301

266302
@Test func `align none leaves geometry unchanged`() async throws {

0 commit comments

Comments
 (0)