From cb6a477d96da9121942fff6773307956b323d513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomas=20Franze=CC=81n?= Date: Thu, 5 Sep 2024 15:34:35 +0200 Subject: [PATCH] Improve bezier paths --- .../Operations/Extrude/ExtrudedAlong.swift | 4 +- .../SwiftSCAD/Shapes/2D/Polygon/Polygon.swift | 4 + .../2D/Polygon/PolygonPointsProvider.swift | 9 ++ .../SwiftSCAD/Values/Bezier/BezierCurve.swift | 24 +++- .../Values/Bezier/BezierPath+Operations.swift | 114 ++++++++++++++++++ .../SwiftSCAD/Values/Bezier/BezierPath.swift | 23 +--- .../Values/Transforms/AffineTransform.swift | 2 + 7 files changed, 154 insertions(+), 26 deletions(-) create mode 100644 Sources/SwiftSCAD/Values/Bezier/BezierPath+Operations.swift diff --git a/Sources/SwiftSCAD/Operations/Extrude/ExtrudedAlong.swift b/Sources/SwiftSCAD/Operations/Extrude/ExtrudedAlong.swift index 20310f1..a5e50bf 100644 --- a/Sources/SwiftSCAD/Operations/Extrude/ExtrudedAlong.swift +++ b/Sources/SwiftSCAD/Operations/Extrude/ExtrudedAlong.swift @@ -11,9 +11,9 @@ public extension Geometry2D { /// - path: A 2D or 3D `BezierPath` representing the path along which to extrude the shape. /// - convexity: The maximum number of surfaces a straight line can intersect the result. This helps OpenSCAD preview the geometry correctly, but has no effect on final rendering. - func extruded(along path: BezierPath, convexity: Int = 2) -> any Geometry3D { + func extruded(along path: BezierPath, in range: ClosedRange? = nil, convexity: Int = 2) -> any Geometry3D { EnvironmentReader { environment in - let points = path.points(facets: environment.facets).map(\.vector3D) + let points = path.points(in: range ?? path.positionRange, facets: environment.facets).map(\.vector3D) let isClosed = points[0].distance(to: points.last!) < 0.0001 let rotations = ([.up] + points.paired().map { $1 - $0 }) .paired().map(AffineTransform3D.rotation(from:to:)) diff --git a/Sources/SwiftSCAD/Shapes/2D/Polygon/Polygon.swift b/Sources/SwiftSCAD/Shapes/2D/Polygon/Polygon.swift index e22dbec..87ff2df 100644 --- a/Sources/SwiftSCAD/Shapes/2D/Polygon/Polygon.swift +++ b/Sources/SwiftSCAD/Shapes/2D/Polygon/Polygon.swift @@ -87,6 +87,10 @@ public extension Polygon { .init(provider: ReversedPolygonPoints(innerProvider: pointsProvider)) } + public init(_ bezierPath: BezierPath2D, in range: ClosedRange) { + self.init(provider: BezierPathRange(bezierPath: bezierPath, range: range)) + } + static func +(_ lhs: Polygon, _ rhs: Polygon) -> Polygon { lhs.appending(rhs) } diff --git a/Sources/SwiftSCAD/Shapes/2D/Polygon/PolygonPointsProvider.swift b/Sources/SwiftSCAD/Shapes/2D/Polygon/PolygonPointsProvider.swift index e92b259..927c144 100644 --- a/Sources/SwiftSCAD/Shapes/2D/Polygon/PolygonPointsProvider.swift +++ b/Sources/SwiftSCAD/Shapes/2D/Polygon/PolygonPointsProvider.swift @@ -41,3 +41,12 @@ internal struct ReversedPolygonPoints: PolygonPointsProvider { innerProvider.points(in: environment).reversed() } } + +internal struct BezierPathRange: PolygonPointsProvider { + let bezierPath: BezierPath2D + let range: ClosedRange + + func points(in environment: Environment) -> [Vector2D] { + bezierPath.points(in: range, facets: environment.facets) + } +} diff --git a/Sources/SwiftSCAD/Values/Bezier/BezierCurve.swift b/Sources/SwiftSCAD/Values/Bezier/BezierCurve.swift index e8ad334..d0de3e5 100644 --- a/Sources/SwiftSCAD/Values/Bezier/BezierCurve.swift +++ b/Sources/SwiftSCAD/Values/Bezier/BezierCurve.swift @@ -8,7 +8,7 @@ internal struct BezierCurve : Sendable { self.controlPoints = controlPoints } - private func point(at fraction: Double) -> V { + internal func point(at fraction: Double) -> V { var workingPoints = controlPoints while workingPoints.count > 1 { workingPoints = workingPoints.paired().map { $0.point(alongLineTo: $1, at: fraction) } @@ -30,6 +30,26 @@ internal struct BezierCurve : Sendable { + points(in: midFraction.., segmentCount: Int) -> [V] { + let segmentLength = (range.upperBound - range.lowerBound) / Double(segmentCount) + return (0...segmentCount).map { f in + point(at: range.lowerBound + Double(f) * segmentLength) + } + } + + func points(in range: Range, facets: Environment.Facets) -> [V] { + guard controlPoints.count > 2 else { + return controlPoints + } + + switch facets { + case .fixed (let count): + return points(in: range, segmentCount: count) + case .dynamic(_, let minSize): + return points(in: range, segmentLength: minSize) + } + } + private func points(segmentLength: Double) -> [V] { return [point(at: 0)] + points(in: 0..<1, segmentLength: segmentLength) + [point(at: 1)] } @@ -54,7 +74,7 @@ internal struct BezierCurve : Sendable { } } - func transform(using transform: T) -> Self where T == V.Transform, T.Vector == V { + func transformed(using transform: T) -> Self where T == V.Transform, T.Vector == V { Self(controlPoints: controlPoints.map { transform.apply(to: $0) }) } } diff --git a/Sources/SwiftSCAD/Values/Bezier/BezierPath+Operations.swift b/Sources/SwiftSCAD/Values/Bezier/BezierPath+Operations.swift new file mode 100644 index 0000000..5087b13 --- /dev/null +++ b/Sources/SwiftSCAD/Values/Bezier/BezierPath+Operations.swift @@ -0,0 +1,114 @@ +import Foundation + +public extension BezierPath { + /// A typealias representing a position along a Bézier path. + /// + /// `BezierPath.Position` is a `Double` value that represents a fractional position along a Bézier path. + /// The integer part of the value represents the index of the Bézier curve within the path, + /// and the fractional part represents a position within that specific curve. + /// + /// For example: + /// - `0.0` represents the start of the first curve. + /// - `1.0` represents the start of the second curve. + /// - `1.5` represents the midpoint of the second curve. + /// + /// This type is used for navigating and interpolating points along a multi-curve Bézier path. + typealias Position = Double + + /// The valid range of positions within this path + var positionRange: ClosedRange { + 0...Position(curves.count) + } + + /// 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. + func points(facets: Environment.Facets) -> [V] { + return [startPoint] + curves.flatMap { + $0.points(facets: facets)[1...] + } + } + + /// Calculates the total length of the Bézier path. + /// + /// - Parameter facets: The desired level of detail for the generated points, which influences the accuracy + /// of the length calculation. More detailed facet values result in more points being generated, leading to a more + /// accurate length approximation. + /// - Returns: A `Double` value representing the total length of the Bézier path. + func length(facets: Environment.Facets) -> Double { + points(facets: facets) + .paired() + .map { $0.distance(to: $1) } + .reduce(0, +) + } + + /// 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. + func transformed(using transform: T) -> BezierPath where T.Vector == V, T == V.Transform { + BezierPath( + startPoint: transform.apply(to: startPoint), + curves: curves.map { $0.transformed(using: transform) } + ) + } + + /// Returns the point at a given position along the path + func point(at position: Position) -> V { + assert(positionRange ~= position) + guard !curves.isEmpty else { return startPoint } + + let curveIndex = min(Int(floor(position)), curves.count - 1) + let fraction = position - Double(curveIndex) + return curves[curveIndex].point(at: fraction) + } + + internal func points(in pathFractionRange: ClosedRange, facets: Environment.Facets) -> [V] { + let (fromCurveIndex, fromFraction) = pathFractionRange.lowerBound.indexAndFraction(curveCount: curves.count) + let (toCurveIndex, toFraction) = pathFractionRange.upperBound.indexAndFraction(curveCount: curves.count) + + return curves[fromCurveIndex...toCurveIndex].enumerated().flatMap { index, curve in + let startFraction = (index == fromCurveIndex) ? fromFraction : 0.0 + let endFraction = (index == toCurveIndex) ? toFraction : 1.0 + return curve.points(in: 0.. V.Transform { + guard !curves.isEmpty else { return .translation(startPoint) } + return .init( + points(in: 0...position, facets: facets).map(\.vector3D) + .paired().map(-) + .paired().map(AffineTransform3D.rotation(from:to:)) + .reduce(AffineTransform3D.identity) { $0.concatenated(with: $1) } + .translated(point(at: position).vector3D) + ) + } +} + +fileprivate extension BezierPath.Position { + func indexAndFraction(curveCount: Int) -> (Int, Double) { + if self < 0 { + return (0, self) + } else if self >= Double(curveCount) { + return (curveCount - 1, self - Double(curveCount - 1)) + } else { + let index = floor(self) + let fraction = self - index + return (Int(index), fraction) + } + } +} diff --git a/Sources/SwiftSCAD/Values/Bezier/BezierPath.swift b/Sources/SwiftSCAD/Values/Bezier/BezierPath.swift index 6a86323..229251d 100644 --- a/Sources/SwiftSCAD/Values/Bezier/BezierPath.swift +++ b/Sources/SwiftSCAD/Values/Bezier/BezierPath.swift @@ -16,7 +16,7 @@ public struct BezierPath : Sendable { curves.last?.controlPoints.last ?? startPoint } - private init(startPoint: V, curves: [BezierCurve]) { + internal init(startPoint: V, curves: [BezierCurve]) { self.startPoint = startPoint self.curves = curves } @@ -100,25 +100,4 @@ public struct BezierPath : Sendable { public func closed() -> BezierPath { addingLine(to: startPoint) } - - /// 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(using transform: T) -> BezierPath where T.Vector == V, T == V.Transform { - BezierPath( - startPoint: transform.apply(to: startPoint), - curves: curves.map { $0.transform(using: transform) } - ) - } } diff --git a/Sources/SwiftSCAD/Values/Transforms/AffineTransform.swift b/Sources/SwiftSCAD/Values/Transforms/AffineTransform.swift index 9f04a34..cd9a9bb 100644 --- a/Sources/SwiftSCAD/Values/Transforms/AffineTransform.swift +++ b/Sources/SwiftSCAD/Values/Transforms/AffineTransform.swift @@ -20,5 +20,7 @@ public protocol AffineTransform: Sendable { static func scaling(_ v: Vector) -> Self static func rotation(_ r: Rotation) -> Self + func translated(_ v: Vector) -> Self + init(_ transform3d: AffineTransform3D) }