Skip to content

Commit

Permalink
Move convexity into an environment modifier
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasf committed Oct 30, 2024
1 parent 276cafb commit ef9dd16
Show file tree
Hide file tree
Showing 18 changed files with 179 additions and 107 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ let path = BezierPath2D(startPoint: .zero)

Star(pointCount: 5, radius: 10, pointRadius: 1, centerSize: 4)
.usingDefaultFacets()
.extruded(along: path, convexity: 4)
.extruded(along: path)
.withPreviewConvexity(4)
.usingFacets(minAngle: 5°, minSize: 1)
.save(to: "example4")
```
Expand Down
18 changes: 4 additions & 14 deletions Sources/SwiftSCAD/Development/ForceRender.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import Foundation

fileprivate struct ForceRender<Geometry> {
let body: Geometry
let convexity: Int

let moduleName: String? = "render"
var moduleParameters: CodeFragment.Parameters {
["convexity": convexity]
}
}

extension ForceRender<any Geometry2D>: Geometry2D, WrappedGeometry2D {}
Expand All @@ -19,21 +14,16 @@ public extension Geometry2D {
///
/// In preview mode, this operation forces the generation of a mesh for this geometry. This approach is useful for ensuring accurate previews of geometries, especially when boolean operations are complex and slow to compute. It also helps in avoiding or working around preview artifacts that might arise.
///
/// - Parameters:
/// - 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 forceRendered(convexity: Int = 2) -> any Geometry2D {
ForceRender(body: self, convexity: convexity)
func forceRendered() -> any Geometry2D {
ForceRender(body: self)
}
}

public extension Geometry3D {
/// Force rendering
///
/// In preview mode, this operation forces the generation of a mesh for this geometry. This approach is useful for ensuring accurate previews of geometries, especially when boolean operations are complex and slow to compute. It also helps in avoiding or working around preview artifacts that might arise.
///
/// - Parameters:
/// - 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 forceRendered(convexity: Int = 2) -> any Geometry3D {
ForceRender(body: self, convexity: convexity)
func forceRendered() -> any Geometry3D {
ForceRender(body: self)
}
}
46 changes: 46 additions & 0 deletions Sources/SwiftSCAD/Environment/Values/PreviewConvexity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Foundation

public extension Environment {
private static let key = ValueKey("SwiftSCAD.PreviewConvexity")

/// The preview convexity currently set in the environment.
///
/// The `previewConvexity` parameter specifies the maximum number of front or back faces a ray might intersect in the geometry during preview rendering. This parameter is only relevant when using OpenCSG preview mode in OpenSCAD and has no effect on the final mesh output or rendering.
///
/// - Important: In OpenCSG preview mode, `previewConvexity` helps determine the correct visibility of complex shapes (e.g., tori or nested geometries). Adjust this parameter for accurate previews of intricate models but know that it is not necessary for final output generation.
///
/// - Returns: The preview convexity value as an `Int`, or `nil` if not set.
var previewConvexity: Int? {
self[Self.key] as? Int
}

/// Returns a new environment with a modified preview convexity value.
///
/// Use this method to apply a `previewConvexity` value to the environment, influencing the OpenCSG preview rendering in OpenSCAD.
///
/// - Parameter convexity: An optional `Int` specifying the maximum number of front or back faces a ray might intersect. Pass `nil` to remove the existing convexity setting.
/// - Returns: A new `Environment` with the specified preview convexity.
///
/// ### Example
/// ```swift
/// let environmentWithConvexity = environment.withPreviewConvexity(2)
/// ```
func withPreviewConvexity(_ convexity: Int?) -> Environment {
setting(key: Self.key, value: convexity)
}
}

public extension Geometry3D {
/// Applies a preview convexity setting to the geometry.
///
/// This method sets a `previewConvexity` value, specifying the maximum number of front or back faces a ray might intersect in the geometry during preview rendering. While SwiftSCAD does not use this value directly for geometry creation, OpenSCAD’s OpenCSG preview mode relies on it for accurate previews.
///
/// - Parameter convexity: An `Int` specifying the preview convexity.
/// - Returns: A modified geometry with the specified preview convexity.
///
func withPreviewConvexity(_ convexity: Int) -> any Geometry3D {
withEnvironment { enviroment in
enviroment.withPreviewConvexity(convexity)
}
}
}
27 changes: 24 additions & 3 deletions Sources/SwiftSCAD/Geometry/Protocols/CombinedGeometry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,48 @@ extension CombinedGeometry2D {
internal protocol CombinedGeometry3D: Geometry3D {
var moduleName: String { get }
var moduleParameters: CodeFragment.Parameters { get }
var supportsPreviewConvexity: Bool { get }
var children: [any Geometry3D] { get }
var boundaryMergeStrategy: Boundary3D.MergeStrategy { get }
var combination: GeometryCombination { get }
}

extension CombinedGeometry3D {
func childEnvironment(for environment: Environment) -> Environment {
supportsPreviewConvexity ? environment.withPreviewConvexity(nil) : environment
}

func codeFragment(in environment: Environment) -> CodeFragment {
.init(module: moduleName, parameters: moduleParameters, body: children.map { $0.codeFragment(in: environment) })
var params = moduleParameters
if let convexity = environment.previewConvexity, supportsPreviewConvexity {
params["convexity"] = convexity
}

return .init(
module: moduleName,
parameters: params,
body: children.map {
$0.codeFragment(in: childEnvironment(for: environment))
}
)
}

func boundary(in environment: Environment) -> Boundary3D {
let boundaries = children.map { $0.boundary(in: environment) }
let boundaries = children.map {
$0.boundary(in: childEnvironment(for: environment))
}
return boundaryMergeStrategy.apply(boundaries)
}

func elements(in environment: Environment) -> [ObjectIdentifier: any ResultElement] {
let allElements = children.map { $0.elements(in: environment) }
let allElements = children.map {
$0.elements(in: childEnvironment(for: environment))
}
return .init(combining: allElements, operation: combination)
}

var moduleParameters: CodeFragment.Parameters { [:] }
var supportsPreviewConvexity: Bool { false }
}

public enum GeometryCombination {
Expand Down
39 changes: 39 additions & 0 deletions Sources/SwiftSCAD/Geometry/Protocols/ExtrusionGeometry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation

internal protocol ExtrusionGeometry: Geometry3D {
var body: any Geometry2D { get }
var moduleName: String { get }
var moduleParameters: CodeFragment.Parameters { get }
func boundary(for boundary2D: Boundary2D, facets: Environment.Facets) -> Boundary3D
}

extension ExtrusionGeometry {
func bodyEnvironment(for environment: Environment) -> Environment {
environment.withPreviewConvexity(nil)
}

func codeFragment(in environment: Environment) -> CodeFragment {

var parameters = moduleParameters
if let previewConvexity = environment.previewConvexity {
parameters["convexity"] = previewConvexity
}

return CodeFragment(
module: moduleName,
parameters: parameters,
body: [body.codeFragment(in: bodyEnvironment(for: environment))]
)
}

func boundary(in environment: Environment) -> Bounds {
boundary(
for: body.boundary(in: bodyEnvironment(for: environment)),
facets: environment.facets
)
}

func elements(in environment: Environment) -> [ObjectIdentifier: any ResultElement] {
body.elements(in: bodyEnvironment(for: environment))
}
}
1 change: 1 addition & 0 deletions Sources/SwiftSCAD/Geometry/Protocols/Geometry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Foundation
// CombinedGeometry: Geometry containing multiple children, e.g. Union, Difference, MinkowskiSum
// TransformedGeometry: Single-child wrapper that applies a transform, e.g. Rotate, Translate
// WrappedGeometry: Generic single-child wrapper
// ExtrusionGeometry: 3D wrapper for 2D child
// Shape: User-facing

/// Two-dimensional geometry.
Expand Down
10 changes: 9 additions & 1 deletion Sources/SwiftSCAD/Geometry/Protocols/LeafGeometry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@ internal protocol LeafGeometry3D: Geometry3D {
var moduleName: String { get }
var moduleParameters: CodeFragment.Parameters { get }
var boundary: Bounds { get }
var supportsPreviewConvexity: Bool { get }
}

extension LeafGeometry3D {
public func codeFragment(in environment: Environment) -> CodeFragment {
.init(module: moduleName, parameters: moduleParameters, body: [])
var params = moduleParameters
if let convexity = environment.previewConvexity, supportsPreviewConvexity {
params["convexity"] = convexity
}

return .init(module: moduleName, parameters: params, body: [])
}

public func elements(in environment: Environment) -> [ObjectIdentifier: any ResultElement] {
Expand All @@ -38,4 +44,6 @@ extension LeafGeometry3D {
public func boundary(in environment: Environment) -> Bounds {
boundary
}

var supportsPreviewConvexity: Bool { false }
}
13 changes: 5 additions & 8 deletions Sources/SwiftSCAD/Operations/Extrude/ExtrudePolygon.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

private extension Polyhedron {
init(extruding shape: Polygon, along path: [AffineTransform3D], convexity: Int, environment: Environment) {
init(extruding shape: Polygon, along path: [AffineTransform3D], environment: Environment) {
struct Vertex: Hashable {
let step: Int
let pointIndex: Int
Expand All @@ -21,7 +21,7 @@ private extension Polyhedron {
let startFace = points.indices.map { Vertex(step: 0, pointIndex: $0) }
let endFace = points.indices.reversed().map { Vertex(step: path.endIndex - 1, pointIndex: $0) }

self.init(faces: sideFaces + [startFace, endFace], convexity: convexity) { vertex in
self.init(faces: sideFaces + [startFace, endFace]) { vertex in
path[vertex.step].apply(to: Vector3D(points[vertex.pointIndex]))
}
}
Expand All @@ -33,18 +33,17 @@ public extension Polygon {
/// - Parameters:
/// - path: An array of affine transforms representing the path along which the polygon will be extruded.
/// - steps: The number of steps to divide the interpolation between each pair of transforms in the `path`. Defaults to 1.
/// - 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.
///
/// - Returns: A 3D geometry resulting from extruding the polygon along the specified path.
///
/// - Note: The `path` array must contain at least two transforms, and `steps` must be at least 1.
func extruded(along path: [AffineTransform3D], steps: Int = 1, convexity: Int = 2) -> any Geometry3D {
func extruded(along path: [AffineTransform3D], steps: Int = 1) -> any Geometry3D {
readEnvironment { environment in
let expandedPath = [path[0]] + path.paired().flatMap { t1, t2 in
(1...steps).map { .linearInterpolation(t1, t2, factor: 1.0 / Double(steps) * Double($0)) }
}

Polyhedron(extruding: self, along: expandedPath, convexity: convexity, environment: environment)
Polyhedron(extruding: self, along: expandedPath, environment: environment)
}
}

Expand All @@ -53,12 +52,10 @@ public extension Polygon {
/// - Parameters:
/// - pitch: The Z distance between each turn of the helix
/// - height: The total height of the helix
/// - 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 extrudedAlongHelix(
pitch: Double,
height: Double,
convexity: Int = 2,
offset: ((Double) -> Double)? = nil
) -> any Geometry3D {
readEnvironment { environment in
Expand All @@ -74,7 +71,7 @@ public extension Polygon {
.translated(z: z)
}

self.extruded(along:path, convexity: convexity)
self.extruded(along:path)
}
}
}
5 changes: 2 additions & 3 deletions Sources/SwiftSCAD/Operations/Extrude/ExtrudedAlong.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ public extension Geometry2D {
///
/// - Parameters:
/// - 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<V: Vector>(along path: BezierPath<V>, in range: ClosedRange<BezierPath.Position>? = nil, convexity: Int = 2) -> any Geometry3D {
func extruded<V: Vector>(along path: BezierPath<V>, in range: ClosedRange<BezierPath.Position>? = nil) -> any Geometry3D {
path.readPoints(in: range) { rawPoints in
let points = rawPoints.map(\.vector3D)
let isClosed = points[0].distance(to: points.last!) < 0.0001
Expand Down Expand Up @@ -40,7 +39,7 @@ public extension Geometry2D {
let long = 1000.0
for segment in segments {
let distance = segment.origin.distance(to: segment.end)
self.extruded(height: distance + long * 2, convexity: convexity)
self.extruded(height: distance + long * 2)
.translated(z: -long)
.transformed(segment.originRotation)
.translated(segment.origin)
Expand Down
37 changes: 16 additions & 21 deletions Sources/SwiftSCAD/Operations/Extrude/LinearExtrude.swift
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
import Foundation

struct LinearExtrude: Geometry3D {
struct LinearExtrude: ExtrusionGeometry {
let body: any Geometry2D

let height: Double
let twist: Angle?
let scale: Vector2D
let convexity: Int

func codeFragment(in environment: Environment) -> CodeFragment {
.init(module: "linear_extrude", parameters: [
"height": height,
"twist": twist.map { -$0 } ?? 0°,
"scale": scale,
"convexity": convexity
], body: [body.codeFragment(in: environment)])
}

func boundary(in environment: Environment) -> Bounds {
body.boundary(in: environment)
.extruded(height: height, twist: twist ?? 0°, topScale: scale, facets: environment.facets)
}
let moduleName = "linear_extrude"
var moduleParameters: CodeFragment.Parameters {[
"height": height,
"twist": twist.map { -$0 } ?? 0°,
"scale": scale,
]}

func elements(in environment: Environment) -> [ObjectIdentifier: any ResultElement] {
body.elements(in: environment)
func boundary(for boundary2D: Boundary2D, facets: Environment.Facets) -> Boundary3D {
boundary2D.extruded(
height: height,
twist: twist ?? 0°,
topScale: scale,
facets: facets
)
}
}

Expand All @@ -33,8 +29,7 @@ public extension Geometry2D {
/// - height: The height of the resulting geometry, in the Z axis
/// - twist: The rotation of the top surface, gradually rotating the geometry around the Z axis, resulting in a twisted shape. Defaults to no twist. Note that the twist direction follows the right-hand rule, which is the opposite of OpenSCAD's behavior.
/// - scale: The final scale at the top of the extruded shape. The geometry is scaled linearly from 1.0 at the bottom.
/// - 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(height: Double, twist: Angle? = nil, scale: Vector2D = [1, 1], convexity: Int = 2) -> any Geometry3D {
LinearExtrude(body: self, height: height, twist: twist, scale: scale, convexity: convexity)
func extruded(height: Double, twist: Angle? = nil, scale: Vector2D = [1, 1]) -> any Geometry3D {
LinearExtrude(body: self, height: height, twist: twist, scale: scale)
}
}
Loading

0 comments on commit ef9dd16

Please sign in to comment.