Skip to content

Commit

Permalink
Add NaturalUpDirection
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasf committed Aug 29, 2024
1 parent b5b5572 commit 3a00d63
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 25 deletions.
123 changes: 123 additions & 0 deletions Sources/SwiftSCAD/Environment/NaturalUpDirection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import Foundation

internal extension Environment {
private static let key = ValueKey(rawValue: "SwiftSCAD.NaturalUpDirection")

struct NaturalUpDirectionData {
let direction: Vector3D
let transform: AffineTransform3D
}

var naturalUpDirectionData: NaturalUpDirectionData? {
self[Self.key] as? NaturalUpDirectionData
}

func settingNaturalUpDirectionData(_ direction: NaturalUpDirectionData?) -> Environment {
setting(key: Self.key, value: direction)
}
}

public extension Environment {
/// The natural up direction for the current environment.
///
/// This computed property returns the up direction as a `Vector3D` if it has been set.
/// The direction is adjusted based on the environment's current transformation.
/// If no up direction is defined, it returns `nil`.
///
/// The returned vector represents the "natural" up direction in the current orientation,
/// taking into account any transformations that have been applied to the environment.
///
/// - Returns: A `Vector3D` representing the natural up direction, or `nil` if not set.
///
var naturalUpDirection: Vector3D? {
naturalUpDirectionData.map { upDirection in
let upTransform = upDirection.transform.inverse.concatenated(with: transform.inverse)
return (upTransform.apply(to: upDirection.direction) - upTransform.apply(to: .zero)).normalized
}
}

/// The angle of the natural up direction relative to the XY plane, if defined.
///
/// This computed property returns the angle between the positive X-axis and the
/// projection of the natural up direction onto the XY plane. If the natural up
/// direction is set, the method calculates the angle in the XY plane. If the
/// natural up direction is not set, the method returns `nil`.
///
/// - Returns: An optional `Angle` representing the angle of the natural up direction
/// in the XY plane, or `nil` if the natural up direction is not set.
///
var naturalUpDirectionXYAngle: Angle? {
naturalUpDirection.map { Vector2D.zero.angle(to: $0.xy) }
}

/// Sets the natural up direction relative to the environment's local coordinate system.
///
/// This method assigns a new natural up direction to the environment, expressed as a `Vector3D`.
/// The direction is specified relative to the environment's current local coordinate system.
/// If the `direction` is `nil`, the natural up direction is cleared, effectively removing
/// any specific orientation considerations from the environment.
///
/// - Parameter direction: A `Vector3D` representing the new natural up direction, or `nil` to remove it.
/// - Returns: A new `Environment` instance with the updated natural up direction.
///
func settingNaturalUpDirection(_ direction: Vector3D?) -> Environment {
settingNaturalUpDirectionData(direction.map {
.init(direction: $0, transform: transform)
})
}
}

public extension Geometry3D {
/// Sets the natural up direction for the geometry.
///
/// This method defines the direction that is considered "up" in the natural orientation
/// of the geometry. This is particularly useful for 3D printing applications, where
/// the up direction affects how overhangs are to be compensated for. You can read this value
/// through `Environment.naturalUpDirection`, where it's been transformed to match that
/// coordinate system.
///
/// - Parameter direction: The `Vector3D` representing the up direction in this geometry's
/// coordinate system. The default value is `.up`.
/// - Returns: A new instance of `Geometry3D` with the natural up direction set in its
/// environment.
///
/// - See Also: `Teardrop`
///
func definingNaturalUpDirection(_ direction: Vector3D = .up) -> any Geometry3D {
withEnvironment { environment in
environment.settingNaturalUpDirection(direction)
}
}

/// Removes the natural up direction for the geometry.
///
/// This method undefines the previously set natural up direction.
/// This is useful if you want to remove any specific orientation
/// considerations and revert to a state where the natural up
/// direction is undefined.
///
/// - Returns: A new instance of `Geometry3D` with the natural up direction unset.
///
func clearingNaturalUpDirection() -> any Geometry3D {
withEnvironment { environment in
environment.settingNaturalUpDirection(nil)
}
}
}

public extension Geometry2D {
/// Removes the natural up direction for the geometry.
///
/// This method undefines the previously set natural up direction.
/// This is useful if you want to remove any specific orientation
/// considerations and revert to a state where the natural up
/// direction is undefined.
///
/// - Returns: A new instance of `Geometry2D` with the natural up direction unset.
///
func clearingNaturalUpDirection() -> any Geometry2D {
withEnvironment { environment in
environment.settingNaturalUpDirection(nil)
}
}
}
8 changes: 4 additions & 4 deletions Sources/SwiftSCAD/Operations/Boolean/Intersection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public extension Geometry2D {
/// - with: The other geometry to intersect with this
/// - Returns: The intersection (overlap) of this geometry and the input

func intersection(@UnionBuilder2D with other: () -> any Geometry2D) -> any Geometry2D {
Intersection2D(children: [self, other()])
func intersection(@SequenceBuilder2D with other: () -> [any Geometry2D]) -> any Geometry2D {
Intersection2D(children: [self] + other())
}

func intersection(_ other: any Geometry2D...) -> any Geometry2D {
Expand All @@ -65,8 +65,8 @@ public extension Geometry3D {
/// - with: The other geometry to intersect with this
/// - Returns: The intersection (overlap) of this geometry and the input

func intersection(@UnionBuilder3D with other: () -> any Geometry3D) -> any Geometry3D {
Intersection3D(children: [self, other()])
func intersection(@SequenceBuilder3D with other: () -> [any Geometry3D]) -> any Geometry3D {
Intersection3D(children: [self] + other())
}

func intersection(_ other: any Geometry3D...) -> any Geometry3D {
Expand Down
40 changes: 20 additions & 20 deletions Sources/SwiftSCAD/Shapes/2D/Overhang/Teardrop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import Foundation

/// A teardrop shape used for 3D printing.
///
/// A teardrop is commonly used in place of a circle in 3D printing designs where the top of the circle would have a steep overhang.
/// By replacing the circular geometry with a teardrop, it becomes more suitable for printing, especially without support structures.
/// The `bridged` style employs bridging at the top of the originally intended circle, creating a more circle-like shape while still being printable.
/// A teardrop shape is often used in place of a circle in 3D printing designs to reduce steep overhangs that are difficult to print.
/// By replacing circular geometry with a teardrop, the design becomes more suitable for printing, especially without the need for support structures.
/// The `bridged` style introduces bridging at the top of the originally intended circle, resulting in a shape that retains a more circular appearance while remaining printable.
///
/// If `Environment.naturalUpDirection` is set, the Teardrop will automatically rotate so that its point or bridge faces upward.
///
/// ```
/// // Example of creating a teardrop shape with a diameter of 10, angle of 30°, and full style.
Expand Down Expand Up @@ -34,26 +36,24 @@ public struct Teardrop: Shape2D {
let y = sin(angle) * diameter/2
let diagonal = diameter / sin(angle)

let base = Union {
EnvironmentReader { environment in
Circle(diameter: diameter)

Rectangle(diagonal)
.rotated(-angle)
.translated(x: -x, y: y)
.intersection {
.adding {
Rectangle(diagonal)
.rotated(angle + 90°)
.translated(x: x, y: y)
}
}
.rotated(-angle)
.translated(x: -x, y: y)
.intersection {
Rectangle(diagonal)
.rotated(angle + 90°)
.translated(x: x, y: y)

if style == .bridged {
return base.intersection {
Rectangle(diameter)
.aligned(at: .center)
}
} else {
return base
if style == .bridged {
Rectangle(diameter)
.aligned(at: .center)
}
}
}
.rotated(environment.naturalUpDirectionXYAngle.map { $0 - 90° } ?? .zero)
}
}

Expand Down
Loading

0 comments on commit 3a00d63

Please sign in to comment.