Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make BezierPath generic for 2D+3D #9

Merged
merged 1 commit into from
Feb 27, 2024
Merged
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
4 changes: 2 additions & 2 deletions Sources/SwiftSCAD/Operations/Extrude/ExtrudedAlong.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

struct ExtrudeAlong: CoreGeometry3D {
let path: BezierPath
let path: BezierPath2D
let radius: Double
let body: any Geometry2D

Expand Down Expand Up @@ -73,7 +73,7 @@ public extension Geometry2D {
/// - Parameters:
/// - path: The bezier path to use as a guide in the X-Y plane.
/// - radius: The corner radius of the extruded geometry. This should be greater than or equal to the distance between the origin and the furthest point along the X axis of the 2D shape. For example, extruding `Rectangle([14, 3], center: .all)` requires at least 7 as the radius.
func extruded(along path: BezierPath, radius: Double) -> any Geometry3D {
func extruded(along path: BezierPath2D, radius: Double) -> any Geometry3D {
ExtrudeAlong(path: path, radius: radius, body: self)
}
}
2 changes: 1 addition & 1 deletion Sources/SwiftSCAD/Shapes/2D/CGPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ extension CGPath {

func polygons() -> [Polygon] {
var polygons: [Polygon] = []
var currentPath: BezierPath? = nil
var currentPath: BezierPath2D? = nil

applyWithBlock { pointer in
let element = pointer.pointee
Expand Down
6 changes: 3 additions & 3 deletions Sources/SwiftSCAD/Shapes/2D/Polygon/Polygon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ public struct Polygon: CoreGeometry2D {
self.init(provider: points)
}

/// Creates a new `Polygon` instance with the specified Bezier path.
/// Creates a new `Polygon` instance with the specified 2D Bezier path.
///
/// - Parameter bezierPath: A `BezierPath` that defines the shape of the polygon.
public init(_ bezierPath: BezierPath) {
/// - Parameter bezierPath: A `BezierPath2D` that defines the shape of the polygon.
public init(_ bezierPath: BezierPath2D) {
self.init(provider: bezierPath)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extension [Vector2D]: PolygonPointsProvider {
}
}

extension BezierPath: PolygonPointsProvider {
extension BezierPath2D: PolygonPointsProvider {
func points(in environment: Environment) -> [Vector2D] {
points(facets: environment.facets)
}
Expand Down
21 changes: 15 additions & 6 deletions Sources/SwiftSCAD/Transformations/Rotate.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import Foundation

struct Rotate3D: CoreGeometry3D {
let x: Angle
let y: Angle
let z: Angle
let rotation: Rotation3D
let body: any Geometry3D

func call(in environment: Environment) -> SCADCall {
return SCADCall(
name: "rotate",
params: ["a": [x, y, z]],
params: ["a": [rotation.x, rotation.y, rotation.z]],
body: body
)
}

var bodyTransform: AffineTransform3D {
.rotation(x: x, y: y, z: z)
.rotation(rotation)
}
}

Expand All @@ -30,7 +28,7 @@ public extension Geometry3D {
/// - z: The amount to rotate around the Z axis
/// - Returns: A rotated geometry
func rotated(x: Angle = 0°, y: Angle = 0°, z: Angle = 0°) -> any Geometry3D {
Rotate3D(x: x, y: y, z: z, body: self)
Rotate3D(rotation: .init(x: x, y: y, z: z), body: self)
}

/// Rotate around one axis
Expand All @@ -46,6 +44,17 @@ public extension Geometry3D {
case .z: return rotated(z: angle)
}
}

/// Rotate geometry
///
/// When using multiple axes, the geometry is rotated around the axes in order (first X, then Y, then Z).
///
/// - Parameters:
/// - rotation: The rotation
/// - Returns: A rotated geometry
func rotated(_ rotation: Rotation3D) -> any Geometry3D {
Rotate3D(rotation: rotation, body: self)
}
}


Expand Down
60 changes: 60 additions & 0 deletions Sources/SwiftSCAD/Values/Bezier/BezierCurve.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation

internal struct BezierCurve <V: Vector> {
let controlPoints: [V]

init(controlPoints: [V]) {
precondition(controlPoints.count >= 2)
self.controlPoints = controlPoints
}

private func point(at fraction: Double) -> V {
var workingPoints = controlPoints
while workingPoints.count > 1 {
workingPoints = workingPoints.paired().map { $0.point(alongLineTo: $1, at: fraction) }
}
return workingPoints[0]
}

private func points(in range: Range<Double>, segmentLength: Double) -> [V] {
let midFraction = (range.lowerBound + range.upperBound) / 2
let midPoint = point(at: midFraction)
let distance = point(at: range.lowerBound).distance(to: midPoint) + point(at: range.upperBound).distance(to: midPoint)

if (distance < segmentLength) || distance < 0.001 {
return []
}

return points(in: range.lowerBound..<midFraction, segmentLength: segmentLength)
+ [midPoint]
+ points(in: midFraction..<range.upperBound, segmentLength: segmentLength)
}

private func points(segmentLength: Double) -> [V] {
return [point(at: 0)] + points(in: 0..<1, segmentLength: segmentLength) + [point(at: 1)]
}

private func points(segmentCount: Int) -> [V] {
let segmentLength = 1.0 / Double(segmentCount)
return (0...segmentCount).map { f in
point(at: Double(f) * segmentLength)
}
}

func points(facets: Environment.Facets) -> [V] {
guard controlPoints.count > 2 else {
return controlPoints
}

switch facets {
case .fixed (let count):
return points(segmentCount: count)
case .dynamic(_, let minSize):
return points(segmentLength: minSize)
}
}

func transform<T: AffineTransform>(using transform: T) -> Self where T == V.Transform, T.Vector == V {
Self(controlPoints: controlPoints.map { transform.apply(to: $0) })
}
}
82 changes: 82 additions & 0 deletions Sources/SwiftSCAD/Values/Bezier/BezierPath+Debug.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Foundation

extension BezierPath {
/// Visualizes the bezier path for debugging purposes by generating a 3D representation. This method creates visual markers for control points and start points, and lines to represent the path and its control lines.
/// - Parameters:
/// - scale: A value that scales the size of markers and the thickness of lines.
/// - markerRotation: The rotation to use for markers. Set to nil to hide them.

public func visualize(scale: Double = 1, markerRotation: Rotation3D? = [45°, 0°, -45°]) -> any Geometry3D {
@UnionBuilder3D
func makeMarker(at location: V, text: String, transform: AffineTransform3D) -> any Geometry3D {
RoundedBox([4, 2, 0.1], center: .all, axis: .z, cornerRadius: 1)
.colored(.white)
.adding {
BasicText(text, font: .init(name: "Helvetica", size: 1), horizontalAlignment: .center, verticalAlignment: .center)
.extruded(height: 0.01)
.translated(z: 0.1)
.colored(.black)
}
.translated(y: 1)
.adding {
Sphere(radius: 0.2)
.colored(.black)
}
.transformed(transform)
.translated(location.vector3D)
}

func makeMarker(at location: V, curveIndex: Int, pointIndex: Int, transform: AffineTransform3D) -> any Geometry3D {
makeMarker(at: location, text: "c\(curveIndex + 1)p\(pointIndex + 1)", transform: transform)
}

func makeLine(from: V, to: V, thickness: Double) -> any Geometry3D {
Sphere(radius: thickness)
.translated(from.vector3D)
.adding {
Sphere(radius: thickness)
.translated(to.vector3D)
}
.convexHull()
.usingFacets(count: 3)
}

return EnvironmentReader { environment -> any Geometry3D in
if let markerRotation {
let transform = AffineTransform3D.scaling(scale).rotated(markerRotation)
for (curveIndex, curve) in curves.enumerated() {
for (pointIndex, controlPoint) in curve.controlPoints.dropFirst().enumerated() {
makeMarker(at: controlPoint, curveIndex: curveIndex, pointIndex: pointIndex, transform: transform)
}
}
makeMarker(at: startPoint, text: "Start", transform: transform)
}

// Lines between control points
for curve in curves {
for (cp1, cp2) in curve.controlPoints.paired() {
makeLine(from: cp1, to: cp2, thickness: 0.04 * scale)
.colored(.red, alpha: 0.2)
}
}

// Curves
for (v1, v2) in points(facets: environment.facets).paired() {
makeLine(from: v1, to: v2, thickness: 0.1 * scale)
.colored(.blue)
}
}
}
}

fileprivate extension Vector {
var vector3D: Vector3D {
if let v3d = self as? Vector3D {
return v3d
} else if let v2d = self as? Vector2D {
return .init(v2d, z: 0)
} else {
preconditionFailure()
}
}
}
93 changes: 93 additions & 0 deletions Sources/SwiftSCAD/Values/Bezier/BezierPath.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Foundation

public typealias BezierPath2D = BezierPath<Vector2D>
public typealias BezierPath3D = BezierPath<Vector3D>

/// A `BezierPath` represents a sequence of connected Bezier curves, forming a path.
///
/// You can create a `BezierPath` by providing a starting point and adding curves and line segments to the path. 2D paths can be used to create `Polygon` shapes.
///
/// To create a `BezierPath`, start with the `init(startPoint:)` initializer, specifying the starting point of the path. Then, you can chain calls to `addingLine(to:)`, `addingQuadraticCurve(controlPoint:end:)`, and `addingCubicCurve(controlPoint1:controlPoint2:end:)` to build a complete path.
public struct BezierPath <V: Vector> {
let startPoint: V
let curves: [BezierCurve<V>]

var endPoint: V {
curves.last?.controlPoints.last ?? startPoint
}

private init(startPoint: V, curves: [BezierCurve<V>]) {
self.startPoint = startPoint
self.curves = curves
}

/// Initializes a new `BezierPath` starting at the given point.
///
/// - Parameter startPoint: The starting point of the Bezier path.
public init(startPoint: V) {
self.init(startPoint: startPoint, curves: [])
}

func adding(curve: BezierCurve<V>) -> BezierPath {
let newCurves = curves + [curve]
return BezierPath(startPoint: startPoint, curves: newCurves)
}

/// Adds a line segment from the last point of the `BezierPath` to the specified point.
///
/// - Parameter point: The end point of the line segment to add.
/// - Returns: A new `BezierPath` instance with the added line segment.
public func addingLine(to point: V) -> BezierPath {
adding(curve: BezierCurve(controlPoints: [endPoint, point]))
}

/// Adds a quadratic Bezier curve to the `BezierPath`.
///
/// - Parameters:
/// - controlPoint: The control point of the quadratic Bezier curve.
/// - end: The end point of the quadratic Bezier curve.
/// - Returns: A new `BezierPath` instance with the added quadratic Bezier curve.
public func addingQuadraticCurve(controlPoint: V, end: V) -> BezierPath {
adding(curve: BezierCurve(controlPoints: [endPoint, controlPoint, end]))
}

/// Adds a cubic Bezier curve to the `BezierPath`.
///
/// - Parameters:
/// - controlPoint1: The first control point of the cubic Bezier curve.
/// - controlPoint2: The second control point of the cubic Bezier curve.
/// - end: The end point of the cubic Bezier curve.
/// - Returns: A new `BezierPath` instance with the added cubic Bezier curve.
public func addingCubicCurve(controlPoint1: V, controlPoint2: V, end: V) -> BezierPath {
adding(curve: BezierCurve(controlPoints: [endPoint, controlPoint1, controlPoint2, end]))
}

/// Adds a Bezier curve to the path using the specified control points. This method can be used to add curves with any number of control points beyond the basic line, quadratic, and cubic curves.
///
/// - Parameter controlPoints: A variadic list of control points defining the Bezier curve.
/// - Returns: A new `BezierPath` instance with the added Bezier curve.
public func addingCurve(_ controlPoints: V...) -> BezierPath {
adding(curve: BezierCurve(controlPoints: controlPoints))
}

/// Generates a sequence of points representing the path.
///
/// - Parameter facets: The desired level of detail for the generated points, affecting the smoothness of curves.
/// - Returns: An array of points that approximate the Bezier path.
public func points(facets: Environment.Facets) -> [V] {
return [startPoint] + curves.map { curve in
Array(curve.points(facets: facets)[1...])
}.joined()
}

/// Applies the given 2D affine transform to the `BezierPath`.
///
/// - Parameter transform: The affine transform to apply.
/// - Returns: A new `BezierPath` instance with the transformed points.
public func transform<T: AffineTransform>(using transform: T) -> BezierPath where T.Vector == V, T == V.Transform {
BezierPath(
startPoint: transform.apply(to: startPoint),
curves: curves.map { $0.transform(using: transform) }
)
}
}
Loading