Skip to content

Commit

Permalink
Add RotatedAround
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasf committed Oct 6, 2024
1 parent eb48c29 commit 9f9da95
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 60 deletions.
39 changes: 39 additions & 0 deletions Sources/SwiftSCAD/Operations/Alignment/Geometry+Aligned.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation

public extension Geometry2D {
/// Aligns the geometry according to specified alignment criteria.
///
/// This method adjusts the position of the geometry so that its bounding box aligns to the coordinate system's origin. Usage of multiple alignment parameters allows for compound alignments, such as aligning to the center along the X-axis and to the top along the Y-axis.
///
/// - Parameter alignment: A list of alignment criteria specifying how the geometry should be aligned. Each alignment option targets a specific axis. If more than one alignment is passed for the same axis, the last one is used.
/// - Returns: A new geometry that is the result of applying the specified alignments to the original geometry.
///
/// Example:
/// ```
/// let square = Rectangle([10, 10])
/// .aligned(at: .centerX, .bottom)
/// ```
/// This example centers the square along the X-axis and aligns its bottom edge with the Y=0 line.
func aligned(at alignment: GeometryAlignment2D...) -> any Geometry2D {
Align2D(content: self, alignment: .init(merging: alignment))
}
}

public extension Geometry3D {
/// Aligns the geometry according to specified alignment criteria.
///
/// This method adjusts the position of the geometry so that its bounding box aligns to the coordinate system's origin. Usage of multiple alignment parameters allows for compound alignments, such as aligning to the center along the X-axis and to the top along the Z-axis.
///
/// - Parameter alignment: A variable list of alignment criteria specifying how the geometry should be aligned. Each alignment option targets a specific axis. If more than one alignment is passed for the same axis, the last one is used.
/// - Returns: A new geometry that is the result of applying the specified alignments to the original geometry.
///
/// Example:
/// ```
/// let box = Box([10, 10, 5])
/// .aligned(at: .centerX, .bottom)
/// ```
/// This example centers the square along the X-axis and aligns its bottom edge with the Z=0 line.
func aligned(at alignment: GeometryAlignment3D...) -> any Geometry3D {
Align3D(content: self, alignment: .init(merging: alignment))
}
}
4 changes: 2 additions & 2 deletions Sources/SwiftSCAD/Operations/Bounds/Resize2D.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ public extension Geometry2D {
private func resized(_ alignment: GeometryAlignment2D, _ calculator: @escaping (Vector2D) -> Vector2D) -> any Geometry2D {
return measuringBounds { geometry, box in
geometry
.translated(-box.minimum - alignment.factors * box.size)
.translated(alignment.offset(for: box))
.scaled(calculator(box.size) / box.size)
.translated(box.minimum + alignment.factors * box.size)
.translated(-alignment.offset(for: box))
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftSCAD/Operations/Bounds/Resize3D.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ public extension Geometry3D {
private func resized(_ alignment: GeometryAlignment3D, _ calculator: @escaping (Vector3D) -> Vector3D) -> any Geometry3D {
return measuringBounds { geometry, box in
geometry
.translated(-box.minimum - alignment.factors * box.size)
.translated(alignment.offset(for: box))
.scaled(calculator(box.size) / box.size)
.translated(box.minimum + alignment.factors * box.size)
.translated(-alignment.offset(for: box))
}
}

Expand Down
6 changes: 2 additions & 4 deletions Sources/SwiftSCAD/Operations/Stack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ internal struct Stack2D: Shape2D {
for geometry in items {
if let box = geometry.boundary(in: environment).boundingBox {
geometry
.translated(-box.minimum)
.translated((box.size * -alignment.factors).with(axis, as: offset))
.translated(alignment.offset(for: box).with(axis, as: offset))

offset += box.size[axis] + spacing
} else {
Expand All @@ -36,8 +35,7 @@ internal struct Stack3D: Shape3D {
for geometry in items {
if let box = geometry.boundary(in: environment).boundingBox {
geometry
.translated(-box.minimum)
.translated((box.size * -alignment.factors).with(axis, as: offset))
.translated(alignment.offset(for: box).with(axis, as: offset))

offset += box.size[axis] + spacing
} else {
Expand Down
71 changes: 71 additions & 0 deletions Sources/SwiftSCAD/Transformations/RotateAround.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation

public extension Geometry2D {
/// Rotates the geometry around a specified pivot point in 2D space.
///
/// This method rotates the geometry by the specified angle in the 2D plane. The rotation occurs around a pivot point, which is defined by alignment options. The pivot is determined based on the bounding box of the geometry.
///
/// - Parameters:
/// - angle: The angle to rotate the geometry. Defaults to `0°`.
/// - pivot: A list of alignment options that specify the pivot point for the rotation. For example, `.center` can be used to rotate the geometry around its center, while `.top` can rotate it around the top boundary.
///
/// - Returns: A new geometry that is the result of applying the specified rotation and pivot adjustments.
///
/// Example:
/// ```
/// Rectangle([10, 8])
/// .rotated(45°, around: .center)
/// ```
/// This example rotates the square 45 degrees around its center.
///
func rotated(
_ angle: Angle = 0°,
around pivot: GeometryAlignment2D...
) -> any Geometry2D {
measuringBounds { _, box in
let alignment = pivot.merged
self
.translated(alignment.offset(for: box))
.rotated(angle)
.translated(-alignment.offset(for: box))
}
}
}

public extension Geometry3D {
/// Rotates the geometry around a specified pivot point in 3D space.
///
/// This method rotates the geometry by the specified angles along the X, Y, and Z axes. When using multiple axes, the geometry is rotated around the axes in order (first X, then Y, then Z).
/// The rotation occurs around a pivot point, which is defined by alignment options. The pivot is determined based on the bounding box of the geometry.
///
/// - Parameters:
/// - x: The angle to rotate around the X-axis. Defaults to `0°`.
/// - y: The angle to rotate around the Y-axis. Defaults to `0°`.
/// - z: The angle to rotate around the Z-axis. Defaults to `0°`.
/// - pivot: A list of alignment options that specify the pivot point for the rotation. For example, `.center` can be used to rotate the geometry around its center, while `.top` can rotate it around the top boundary.
///
/// - Returns: A new geometry that is the result of applying the specified rotation and pivot adjustments.
///
/// Example:
/// ```
/// Box([10, 10, 5])
/// .rotated(x: 90°, around: .center)
/// ```
/// This example rotates the box 90 degrees around its center.
///
func rotated(
x: Angle = 0°,
y: Angle = 0°,
z: Angle = 0°,
around pivot: GeometryAlignment3D...
) -> any Geometry3D {
measuringBounds { _, box in
let alignment = pivot.merged
self
.translated(alignment.offset(for: box))
.rotated(x: x, y: y, z: z)
.translated(-alignment.offset(for: box))
}

}
}
15 changes: 15 additions & 0 deletions Sources/SwiftSCAD/Values/Alignment/AxisAlignment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

public enum AxisAlignment: Equatable, Sendable {
case min
case mid
case max

internal var factor: Double {
switch self {
case .min: 0.0
case .mid: 0.5
case .max: 1.0
}
}
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,5 @@
import Foundation

public extension Geometry2D {
/// Aligns the geometry according to specified alignment criteria.
///
/// This method adjusts the position of the geometry so that its bounding box aligns to the coordinate system's origin. Usage of multiple alignment parameters allows for compound alignments, such as aligning to the center along the X-axis and to the top along the Y-axis.
///
/// - Parameter alignment: A list of alignment criteria specifying how the geometry should be aligned. Each alignment option targets a specific axis. If more than one alignment is passed for the same axis, the last one is used.
/// - Returns: A new geometry that is the result of applying the specified alignments to the original geometry.
///
/// Example:
/// ```
/// let square = Rectangle([10, 10])
/// .aligned(at: .centerX, .bottom)
/// ```
/// This example centers the square along the X-axis and aligns its bottom edge with the Y=0 line.
func aligned(at alignment: GeometryAlignment2D...) -> any Geometry2D {
Align2D(content: self, alignment: .init(merging: alignment))
}
}

public extension Geometry3D {
/// Aligns the geometry according to specified alignment criteria.
///
/// This method adjusts the position of the geometry so that its bounding box aligns to the coordinate system's origin. Usage of multiple alignment parameters allows for compound alignments, such as aligning to the center along the X-axis and to the top along the Z-axis.
///
/// - Parameter alignment: A variable list of alignment criteria specifying how the geometry should be aligned. Each alignment option targets a specific axis. If more than one alignment is passed for the same axis, the last one is used.
/// - Returns: A new geometry that is the result of applying the specified alignments to the original geometry.
///
/// Example:
/// ```
/// let box = Box([10, 10, 5])
/// .aligned(at: .centerX, .bottom)
/// ```
/// This example centers the square along the X-axis and aligns its bottom edge with the Z=0 line.
func aligned(at alignment: GeometryAlignment3D...) -> any Geometry3D {
Align3D(content: self, alignment: .init(merging: alignment))
}
}

public extension GeometryAlignment2D {
/// Represents no alignment, leaving the geometry in its original position.
static let none = Self()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
import Foundation

public enum AxisAlignment: Equatable, Sendable {
case min
case mid
case max

internal var factor: Double {
switch self {
case .min: 0.0
case .mid: 0.5
case .max: 1.0
}
}
}
import Foundation

public struct GeometryAlignment2D: Equatable, Sendable {
internal let x: AxisAlignment?
Expand All @@ -31,6 +18,10 @@ public struct GeometryAlignment2D: Equatable, Sendable {
internal var factors: Vector2D {
.init(x?.factor ?? 0, y?.factor ?? 0)
}

internal func offset(for boundingBox: BoundingBox2D) -> Vector2D {
-boundingBox.minimum - factors * boundingBox.size
}
}

public struct GeometryAlignment3D: Equatable, Sendable {
Expand All @@ -53,6 +44,10 @@ public struct GeometryAlignment3D: Equatable, Sendable {
internal var factors: Vector3D {
.init(x?.factor ?? 0, y?.factor ?? 0, z?.factor ?? 0)
}

internal func offset(for boundingBox: BoundingBox3D) -> Vector3D {
-boundingBox.minimum - factors * boundingBox.size
}
}

internal extension [GeometryAlignment2D] {
Expand Down

0 comments on commit 9f9da95

Please sign in to comment.