diff --git a/Sources/SwiftSCAD/Operations/Extrude/Chamfer.swift b/Sources/SwiftSCAD/Operations/Extrude/Chamfer.swift new file mode 100644 index 0000000..c74e747 --- /dev/null +++ b/Sources/SwiftSCAD/Operations/Extrude/Chamfer.swift @@ -0,0 +1,36 @@ +import Foundation + +public extension Geometry2D { + @UnionBuilder3D private func extrudedLayered(height: Double, chamferHeight: Double, chamferDepth: Double, layerHeight: Double) -> any Geometry3D { + let layerCount = Int(ceil(chamferHeight / layerHeight)) + let effectiveChamferHeight = Double(layerCount) * layerHeight + + for l in 0...layerCount { + let z = Double(l) * layerHeight + let inset = Double(l) / Double(layerCount) * chamferDepth + offset(amount: -inset, style: .round) + .extruded(height: height - effectiveChamferHeight + z) + } + } + + @UnionBuilder3D private func extrudedConvex(height: Double, chamferHeight: Double, chamferDepth: Double) -> any Geometry3D { + self + .extruded(height: max(height - chamferHeight, 0.001)) + .adding { + self + .offset(amount: -chamferDepth, style: .round) + .extruded(height: 0.001) + .translated(z: height - 0.001) + } + .convexHull() + } + + @UnionBuilder3D internal func chamferEdgeMask(height: Double, chamferWidth: Double, chamferHeight: Double, method: EdgeProfile.Method) -> any Geometry3D { + switch method { + case .layered (let layerHeight): + extrudedLayered(height: height, chamferHeight: chamferHeight, chamferDepth: chamferWidth, layerHeight: layerHeight) + case .convexHull: + extrudedConvex(height: height, chamferHeight: chamferHeight, chamferDepth: chamferWidth) + } + } +} diff --git a/Sources/SwiftSCAD/Operations/Extrude/EdgeProfile.swift b/Sources/SwiftSCAD/Operations/Extrude/EdgeProfile.swift new file mode 100644 index 0000000..6399d65 --- /dev/null +++ b/Sources/SwiftSCAD/Operations/Extrude/EdgeProfile.swift @@ -0,0 +1,94 @@ +import Foundation + +/// The shape of the edge of an extruded shape +public enum EdgeProfile: Equatable { + /// A sharp edge without modification + case sharp + /// A rounded edge + case fillet (radius: Double) + /// A flat chamfered edge + /// - Parameters: + /// - width: The depth of the chamfer in the X/Y axes + /// - topEdge: The depth of the chamfer in the Z axis + case chamfer (width: Double, height: Double) + + /// Methods for building an extruded shape with modified edges + public enum Method { + /// Divide the extrusion into distinct layers with a given thickness. While less elegant and more expensive to render, it is suitable for non-convex shapes. Layers work well for 3D printing, as the printing process inherently occurs in layers. + case layered (height: Double) + /// Create a smooth, non-layered shape. It is often computationally less intensive and results in a more aesthetically pleasing form but only works as expected for convex shapes. + case convexHull + } +} + +public extension EdgeProfile { + /// A 45° chamfered edge + static func chamfer(size: Double) -> EdgeProfile { + .chamfer(width: size, height: size) + } + + /// A chamfered edge with a given width and angle + /// - Parameters: + /// - width: The depth of the chamfer in the X/Y axes + /// - angle: An angle between 0° and 90°, measured from the top of the extrusion + static func chamfer(width: Double, angle: Angle) -> EdgeProfile { + assert((0°..<90°).contains(angle), "Chamfer angle must be between 0° and 90°") + return .chamfer(width: width, height: width * tan(angle)) + } + + /// A chamfered edge with a given height and angle + /// - Parameters: + /// - height: The depth of the chamfer in the Z axis + /// - angle: An angle between 0° and 90°, measured from the top of the extrusion + static func chamfer(height: Double, angle: Angle) -> EdgeProfile { + assert((0°..<90°).contains(angle), "Chamfer angle must be between 0° and 90°") + return .chamfer(width: height / tan(angle), height: height) + } +} + +public extension Geometry2D { + + /// Extrudes a 2D geometry into a 3D shape with chamfers or fillets along the top and/or bottom edges + /// + /// This method allows you to create a 3D shape by extruding the 2D geometry. + /// The method of extrusion can be selected based on the desired characteristics + /// of the resulting shape. + /// + /// - Parameters: + /// - height: The height of the extrusion. + /// - topEdge: The profile of the top edge. + /// - bottomEdge: The profile of the bottom edge. + /// - method: The method of extrusion, either `.layered(thickness:)` or `.convexHull`. + /// - `.layered`: This method divides the extrusion into distinct layers with a given thickness. While less elegant and more expensive to render, it is suitable for non-convex shapes. Layers work well for 3D printing, as the printing process inherently occurs in layers. + /// - `.convexHull`: This method creates a smooth, non-layered shape. It is often computationally less intensive and results in a more aesthetically pleasing form but only works as expected for convex shapes. + /// - Returns: The extruded 3D geometry. + + func extruded(height: Double, topEdge: EdgeProfile = .sharp, bottomEdge: EdgeProfile = .sharp, method: EdgeProfile.Method) -> any Geometry3D { + extruded(height: height) + .intersection { + if topEdge != .sharp { + edgeMask(height: height, edgeProfile: topEdge, method: method) + } + } + .intersection { + if bottomEdge != .sharp { + edgeMask(height: height, edgeProfile: bottomEdge, method: method) + .scaled(z: -1) + .translated(z: height) + } + } + } +} + +private extension Geometry2D { + func edgeMask(height: Double, edgeProfile: EdgeProfile, method: EdgeProfile.Method) -> any Geometry3D { + switch edgeProfile { + case .sharp: + extruded(height: height) + case .fillet (let radius): + filletEdgeMask(height: height, topRadius: radius, method: method) + case .chamfer (let chamferWidth, let chamferHeight): + chamferEdgeMask(height: height, chamferWidth: chamferWidth, chamferHeight: chamferHeight, method: method) + } + } +} diff --git a/Sources/SwiftSCAD/Operations/Extrude.swift b/Sources/SwiftSCAD/Operations/Extrude/Extrude.swift similarity index 94% rename from Sources/SwiftSCAD/Operations/Extrude.swift rename to Sources/SwiftSCAD/Operations/Extrude/Extrude.swift index df97212..d067312 100644 --- a/Sources/SwiftSCAD/Operations/Extrude.swift +++ b/Sources/SwiftSCAD/Operations/Extrude/Extrude.swift @@ -65,14 +65,3 @@ public extension Geometry2D { .rotated(z: angles.lowerBound) } } - -public enum ExtrusionZSides { - case top - case bottom - case both -} - -public enum ExtrusionMethod { - case layered (height: Double) - case convexHull -} diff --git a/Sources/SwiftSCAD/Operations/ExtrudeAlong.swift b/Sources/SwiftSCAD/Operations/Extrude/ExtrudedAlong.swift similarity index 97% rename from Sources/SwiftSCAD/Operations/ExtrudeAlong.swift rename to Sources/SwiftSCAD/Operations/Extrude/ExtrudedAlong.swift index 95fc19b..8b3ac0c 100644 --- a/Sources/SwiftSCAD/Operations/ExtrudeAlong.swift +++ b/Sources/SwiftSCAD/Operations/Extrude/ExtrudedAlong.swift @@ -1,10 +1,3 @@ -// -// ExtrudeAlong.swift -// GeoTest -// -// Created by Tomas Franzén on 2021-07-07. -// - import Foundation struct ExtrudeAlong: CoreGeometry3D { diff --git a/Sources/SwiftSCAD/Operations/ExtrudedHull.swift b/Sources/SwiftSCAD/Operations/Extrude/ExtrudedHull.swift similarity index 100% rename from Sources/SwiftSCAD/Operations/ExtrudedHull.swift rename to Sources/SwiftSCAD/Operations/Extrude/ExtrudedHull.swift diff --git a/Sources/SwiftSCAD/Operations/Extrude/Fillet.swift b/Sources/SwiftSCAD/Operations/Extrude/Fillet.swift new file mode 100644 index 0000000..d362e9e --- /dev/null +++ b/Sources/SwiftSCAD/Operations/Extrude/Fillet.swift @@ -0,0 +1,40 @@ +import Foundation + +public extension Geometry2D { + @UnionBuilder3D private func extrudedLayered(height: Double, topRadius radius: Double, layerHeight: Double) -> any Geometry3D { + let layerCount = Int(ceil(radius / layerHeight)) + let effectiveRadius = Double(layerCount) * layerHeight + + for l in 0.. any Geometry3D { + EnvironmentReader3D { environment in + let facetsPerRev = environment.facets.facetCount(circleRadius: radius) + let facetCount = max(Int(ceil(Double(facetsPerRev) / 4.0)), 1) + + let slices = (0...facetCount).mapUnion { f in + let angle = (Double(f) / Double(facetCount)) * 90° + let inset = cos(angle) * radius - radius + let zOffset = sin(angle) * radius + offset(amount: inset, style: .round) + .extruded(height: height - radius + zOffset) + } + + return slices.convexHull() + } + } + + @UnionBuilder3D internal func filletEdgeMask(height: Double, topRadius radius: Double, method: EdgeProfile.Method) -> any Geometry3D { + switch method { + case .layered (let layerHeight): + extrudedLayered(height: height, topRadius: radius, layerHeight: layerHeight) + case .convexHull: + extrudedConvex(height: height, topRadius: radius) + } + } +} diff --git a/Sources/SwiftSCAD/Operations/ExtrudeChamfered.swift b/Sources/SwiftSCAD/Operations/ExtrudeChamfered.swift deleted file mode 100644 index 0538572..0000000 --- a/Sources/SwiftSCAD/Operations/ExtrudeChamfered.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Foundation - -public extension Geometry2D { - @UnionBuilder3D private func extrudedLayeredTopChamfer(height: Double, chamferHeight: Double, chamferDepth: Double, layerHeight: Double) -> any Geometry3D { - let layerCount = Int(ceil(chamferHeight / layerHeight)) - let effectiveChamferHeight = Double(layerCount) * layerHeight - - for l in 0...layerCount { - let z = Double(l) * layerHeight - let inset = Double(l) / Double(layerCount) * chamferDepth - offset(amount: -inset, style: .round) - .extruded(height: height - effectiveChamferHeight + z) - } - } - - @UnionBuilder3D private func extrudedConvexTopChamfer(height: Double, chamferHeight: Double, chamferDepth: Double) -> any Geometry3D { - self - .extruded(height: max(height - chamferHeight, 0.001)) - .adding { - self - .offset(amount: -chamferDepth, style: .round) - .extruded(height: 0.001) - .translated(z: height - 0.001) - } - .convexHull() - } - - @UnionBuilder3D private func extrudedTopChamfer(height: Double, chamferHeight: Double, chamferDepth: Double, method: ExtrusionMethod) -> any Geometry3D { - switch method { - case .layered (let layerHeight): - extrudedLayeredTopChamfer(height: height, chamferHeight: chamferHeight, chamferDepth: chamferDepth, layerHeight: layerHeight) - case .convexHull: - extrudedConvexTopChamfer(height: height, chamferHeight: chamferHeight, chamferDepth: chamferDepth) - } - } - - /// Extrudes a 2D geometry with a chamfer based on the given parameters. - /// - Parameters: - /// - height: The height of the extrusion. - /// - chamferHeight: The size of the chamfer in the Z axis. - /// - chamferDepth: The size of the chamfer in the X and Y axes. - /// - method: The extrusion method. - /// - sides: Specifies which sides to chamfer in the extrusion. - /// - Returns: The extruded 3D geometry. - func extruded(height: Double, chamferHeight: Double, chamferDepth: Double, method: ExtrusionMethod, sides: ExtrusionZSides = .top) -> any Geometry3D { - switch sides { - case .top: - return extrudedTopChamfer(height: height, chamferHeight: chamferHeight, chamferDepth: chamferDepth, method: method) - case .bottom: - return extrudedTopChamfer(height: height, chamferHeight: chamferHeight, chamferDepth: chamferDepth, method: method) - .scaled(z: -1) - .translated(z: height) - case .both: - return extruded(height: height / 2, chamferHeight: chamferHeight, chamferDepth: chamferDepth, method: method, sides: .top) - .translated(z: height / 2) - .adding { - extruded(height: height / 2, chamferHeight: chamferHeight, chamferDepth: chamferDepth, method: method, sides: .bottom) - } - } - } - - /// Extrudes a 2D geometry with a chamfer based on the given parameters. - /// - Parameters: - /// - height: The height of the extrusion. - /// - chamferSize: The size of the chamfer. - /// - method: The extrusion method. - /// - sides: Specifies which sides to chamfer in the extrusion. - /// - Returns: The extruded 3D geometry. - func extruded(height: Double, chamferSize: Double, method: ExtrusionMethod, sides: ExtrusionZSides = .top) -> any Geometry3D { - extruded(height: height, chamferHeight: chamferSize, chamferDepth: chamferSize, method: method, sides: sides) - } -} diff --git a/Sources/SwiftSCAD/Operations/ExtrudeRounded.swift b/Sources/SwiftSCAD/Operations/ExtrudeRounded.swift deleted file mode 100644 index c63b55f..0000000 --- a/Sources/SwiftSCAD/Operations/ExtrudeRounded.swift +++ /dev/null @@ -1,78 +0,0 @@ -import Foundation - -public extension Geometry2D { - @UnionBuilder3D private func extrudedLayered(height: Double, topRadius radius: Double, layerHeight: Double) -> any Geometry3D { - let layerCount = Int(ceil(radius / layerHeight)) - let effectiveRadius = Double(layerCount) * layerHeight - - for l in 0.. any Geometry3D { - EnvironmentReader3D { environment in - let facetCount = environment.facets.facetCount(circleRadius: radius) - - let slices = (0...facetCount).mapUnion { f in - let angle = (Double(f) / Double(facetCount)) * 90° - let inset = cos(angle) * radius - radius - let zOffset = sin(angle) * radius - offset(amount: inset, style: .round) - .extruded(height: height - radius + zOffset) - } - - return slices.convexHull() - } - } - - @UnionBuilder3D private func extruded(height: Double, topRadius radius: Double, method: ExtrusionMethod) -> any Geometry3D { - switch method { - case .layered (let layerHeight): - extrudedLayered(height: height, topRadius: radius, layerHeight: layerHeight) - case .convexHull: - extrudedConvex(height: height, topRadius: radius) - } - } - - /// Extrudes a 2D geometry into a 3D shape with a specified top radius. - /// - /// This method allows you to create a 3D shape by extruding the 2D geometry. - /// The top radius parameter enables a smooth transition to the top surface. - /// The method of extrusion can be selected based on the desired characteristics - /// of the resulting shape. - /// - /// - Parameters: - /// - height: The height of the extrusion. - /// - radius: The top radius of the extrusion. - /// - method: The method of extrusion, either `.layered(thickness:)` or `.convexHull`. - /// - `.layered`: This method divides the extrusion into distinct layers with a given thickness. While less elegant and more expensive to render, it is suitable for non-convex shapes. Layers work well for 3D printing, as the printing process inherently occurs in layers. - /// - `.convexHull`: This method creates a smooth, non-layered shape. It is computationally less intensive and results in a more aesthetically pleasing form but is only applicable for convex shapes. - /// - sides: Specifies which sides to chamfer (top, bottom, or both). - /// - Returns: The extruded 3D geometry. - func extruded(height: Double, radius: Double, method: ExtrusionMethod, sides: ExtrusionZSides = .top) -> any Geometry3D { - switch sides { - case .top: - return extruded(height: height, topRadius: radius, method: method) - case .bottom: - return extruded(height: height, topRadius: radius, method: method) - .scaled(z: -1) - .translated(z: height) - case .both: - return Union { - extruded(height: height / 2, radius: radius, method: method, sides: .top) - .translated(z: height / 2) - extruded(height: height / 2, radius: radius, method: method, sides: .bottom) - } - } - } -} - -public extension Geometry2D { - @available(*, deprecated, message: "Use extruded(height:radius:method:sides:) with .layered method instead") - func extruded(height: Double, radius: Double, layerHeight: Double, sides: ExtrusionZSides = .top) -> any Geometry3D { - extruded(height: height, radius: radius, method: .layered(height: layerHeight), sides: sides) - } -}