Skip to content

Commit d5e867a

Browse files
committed
Merge branch 'dev'
2 parents fc07979 + 5a2507a commit d5e867a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1373
-663
lines changed

Sources/Cadova/Abstract Layer/Environment/Environment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import Foundation
1616
/// - keyPath: A key path to the value in `EnvironmentValues`, which determines the specific value to read.
1717
///
1818
@propertyWrapper public struct Environment<T: Sendable>: Sendable {
19-
private let getter: @Sendable (EnvironmentValues) -> T
19+
internal let getter: @Sendable (EnvironmentValues) -> T
2020

2121
public init() where T == EnvironmentValues {
2222
getter = { $0 }

Sources/Cadova/Abstract Layer/Environment/EnvironmentValues.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ public struct EnvironmentValues: Sendable {
1414
self.values = values
1515
}
1616

17+
init<T: Sendable>(keyPath: WritableKeyPath<Self, T>, value: T) {
18+
self.init()
19+
self[keyPath: keyPath] = value
20+
}
21+
1722
/// Returns a new environment by adding new values to the current environment.
1823
///
1924
/// - Parameter newValues: A dictionary of values to add to the environment.

Sources/Cadova/Abstract Layer/Geometry/ApplyMaterial.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ public extension Geometry3D {
3737
func colored(red: Double, green: Double, blue: Double, alpha: Double = 1) -> any Geometry3D {
3838
colored(.init(red: red, green: green, blue: blue, alpha: alpha))
3939
}
40+
41+
/// Applies a color to the geometry using a hexadecimal string.
42+
///
43+
/// - Parameter hex: A string containing 6 or 8 hexadecimal digits (optionally with a leading “#”).
44+
/// - Returns: A geometry instance with the given color applied.
45+
func colored(hex: String) -> any Geometry3D {
46+
colored(Color(hex: hex))
47+
}
4048
}
4149

4250
public extension Geometry3D {

Sources/Cadova/Abstract Layer/Geometry/BuildResult.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ internal extension BuildResult {
6262
}
6363
}
6464

65+
internal extension BuildResult<D2> {
66+
func promotedTo3D() -> BuildResult<D3> {
67+
replacing(node: .extrusion(node, type: .linear(height: 0.001)))
68+
}
69+
}
70+
6571
extension BuildResult: Geometry {
6672
public func build(in environment: EnvironmentValues, context: EvaluationContext) async throws -> D.BuildResult {
6773
self

Sources/Cadova/Abstract Layer/Geometry/CachingGeometryTypes.swift

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -63,55 +63,6 @@ struct CachedConcreteTransformer<D: Dimensionality, Key: CacheKey>: Geometry {
6363
}
6464
}
6565

66-
// Apply an arbitrary transformation to a body's concrete, returning a variable number
67-
// of resulting concretes, individually cached based on node + key + index
68-
69-
struct CachedConcreteArrayTransformer<D: Dimensionality, Key: CacheKey>: Geometry {
70-
let body: D.Geometry
71-
let key: Key
72-
let generator: @Sendable (D.Concrete) throws -> [D.Concrete]
73-
let resultHandler: @Sendable ([D.Geometry]) -> D.Geometry
74-
75-
init(
76-
body: D.Geometry,
77-
key: Key, generator: @Sendable @escaping (D.Concrete) throws -> [D.Concrete],
78-
resultHandler: @Sendable @escaping ([D.Geometry]) -> D.Geometry
79-
) {
80-
self.body = body
81-
self.key = key
82-
self.generator = generator
83-
self.resultHandler = resultHandler
84-
}
85-
86-
init(
87-
body: D.Geometry,
88-
name: String,
89-
parameters: any CacheKey...,
90-
generator: @Sendable @escaping (D.Concrete) throws -> [D.Concrete],
91-
resultHandler: @Sendable @escaping ([D.Geometry]) -> D.Geometry
92-
) where Key == LabeledCacheKey {
93-
self.init(
94-
body: body,
95-
key: LabeledCacheKey(operationName: name, parameters: parameters),
96-
generator: generator,
97-
resultHandler: resultHandler
98-
)
99-
}
100-
101-
func build(in environment: EnvironmentValues, context: EvaluationContext) async throws -> D.BuildResult {
102-
let bodyResult = try await context.buildResult(for: body, in: environment)
103-
let bakedKey = NodeCacheKey(base: key, node: bodyResult.node)
104-
105-
let geometries = try await context.multipartMaterializedResults(for: bakedKey, from: bodyResult) {
106-
let nodeResult = try await context.result(for: bodyResult.node)
107-
let concretes = try generator(nodeResult.concrete)
108-
return try concretes.map { try D.Node.Result($0) }
109-
}
110-
111-
return try await context.buildResult(for: resultHandler(geometries), in: environment)
112-
}
113-
}
114-
11566
// Apply an arbitrary transformation to a node, cached based on node + key
11667

11768
struct CachedNodeTransformer<D: Dimensionality, Input: Dimensionality>: Geometry {

Sources/Cadova/Abstract Layer/Operations/Duplication/RepeatAlong.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ extension Geometry {
9999
let count = Int(floor(availableLength / (boundsLength + minimumSpacing)))
100100
let step = availableLength / Double(count)
101101
self.repeated(along: axis, step: step, count: count + 1)
102+
.translated(D.Vector(axis, value: range.lowerBound))
102103
}
103104
}
104105
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import Foundation
2+
3+
internal struct Separate<D: Dimensionality>: Geometry {
4+
let body: D.Geometry
5+
let reader: @Sendable ([D.Geometry]) -> D.Geometry
6+
7+
public func build(in environment: EnvironmentValues, context: EvaluationContext) async throws -> D.BuildResult {
8+
let result = try await context.buildResult(for: body, in: environment)
9+
let partCount = try await context.result(for: .decompose(result.node)).parts.count
10+
let parts = (0..<partCount).map { SeparatedPart(body: body, index: $0) }
11+
return try await context.buildResult(for: reader(parts), in: environment)
12+
}
13+
}
14+
15+
internal struct SeparatedPart<D: Dimensionality>: Geometry {
16+
let body: D.Geometry
17+
let index: Int
18+
19+
public func build(in environment: EnvironmentValues, context: EvaluationContext) async throws -> D.BuildResult {
20+
try await context.buildResult(for: body, in: environment).modifyingNode {
21+
.select(.decompose($0), index: index)
22+
}
23+
}
24+
}
25+
26+
public extension Geometry {
27+
/// Splits the geometry into its disconnected components and passes them to a reader closure.
28+
///
29+
/// This method identifies and extracts all topologically disconnected parts of the geometry,
30+
/// such as individual shells or pieces that do not touch each other. The resulting components
31+
/// are passed to a closure, allowing you to process, rearrange, or visualize them as desired.
32+
///
33+
/// - Parameter reader: A closure that takes the array of separated components and returns a new geometry.
34+
/// - Returns: A new geometry built from the components returned by the `reader` closure.
35+
///
36+
/// ## Example
37+
/// ```swift
38+
/// model.separated { components in
39+
/// Stack(.x, spacing: 1) {
40+
/// for component in components {
41+
/// component
42+
/// }
43+
/// }
44+
/// }
45+
/// ```
46+
///
47+
/// In this example, each disconnected part of the model is extracted and displayed side-by-side
48+
/// along the X axis with a spacing of 1 mm.
49+
func separated(@GeometryBuilder<D> reader: @Sendable @escaping (_ components: [D.Geometry]) -> D.Geometry) -> D.Geometry {
50+
Separate(body: self, reader: reader)
51+
}
52+
}

Sources/Cadova/Abstract Layer/Operations/Split.swift

Lines changed: 5 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,10 @@ public extension Geometry3D {
2828
along plane: Plane,
2929
@GeometryBuilder3D reader: @Sendable @escaping (_ over: any Geometry3D, _ under: any Geometry3D) -> any Geometry3D
3030
) -> any Geometry3D {
31-
CachedConcreteArrayTransformer(body: self, name: "Cadova.SplitAlongPlane", parameters: plane) { input in
32-
let (a, b) = input.translate(-plane.offset)
33-
.split(by: plane.normal.unitVector, originOffset: 0)
34-
return [a, b]
35-
} resultHandler: { geometries in
36-
precondition(geometries.count == 2, "Split result should contain exactly two geometries")
37-
return reader(
38-
geometries[0].translated(plane.offset),
39-
geometries[1].translated(plane.offset)
40-
)
41-
}
31+
reader(
32+
GeometryNodeTransformer(body: self) { .trim($0, plane: plane) },
33+
GeometryNodeTransformer(body: self) { .trim($0, plane: plane.flipped) }
34+
)
4235
}
4336

4437
/// Splits the geometry into two parts along the specified plane and arranges them side-by-side.
@@ -94,19 +87,7 @@ public extension Geometry3D {
9487
@GeometryBuilder3D with mask: @escaping () -> any Geometry3D,
9588
@GeometryBuilder3D result: @Sendable @escaping (_ inside: any Geometry3D, _ outside: any Geometry3D) -> any Geometry3D
9689
) -> any Geometry3D {
97-
mask().readingConcrete { concrete, maskResult in
98-
CachedConcreteArrayTransformer(body: self, name: "Cadova.SplitWithMask", parameters: maskResult.node) { input in
99-
let (a, b) = input.split(by: concrete)
100-
return [a, b]
101-
} resultHandler: { geometries in
102-
precondition(geometries.count == 2, "Split result should contain exactly two geometries")
103-
104-
return result(
105-
geometries[0].mergingResultElements(with: maskResult.elements),
106-
geometries[1].mergingResultElements(with: maskResult.elements)
107-
)
108-
}
109-
}
90+
result(intersecting(mask()), subtracting(mask()))
11091
}
11192

11293
/// Trims the geometry along the specified plane, keeping only the portion facing the plane's normal direction.
@@ -128,35 +109,3 @@ public extension Geometry3D {
128109
GeometryNodeTransformer(body: self) { .trim($0, plane: plane) }
129110
}
130111
}
131-
132-
public extension Geometry {
133-
/// Splits the geometry into its disconnected components and passes them to a reader closure.
134-
///
135-
/// This method identifies and extracts all topologically disconnected parts of the geometry,
136-
/// such as individual shells or pieces that do not touch each other. The resulting components
137-
/// are passed to a closure, allowing you to process, rearrange, or visualize them as desired.
138-
///
139-
/// - Parameter reader: A closure that takes the array of separated components and returns a new geometry.
140-
/// - Returns: A new geometry built from the components returned by the `reader` closure.
141-
///
142-
/// ## Example
143-
/// ```swift
144-
/// model.separated { components in
145-
/// Stack(.x, spacing: 1) {
146-
/// for component in components {
147-
/// component
148-
/// }
149-
/// }
150-
/// }
151-
/// ```
152-
///
153-
/// In this example, each disconnected part of the model is extracted and displayed side-by-side
154-
/// along the X axis with a spacing of 1 mm.
155-
func separated(@GeometryBuilder<D> reader: @Sendable @escaping (_ components: [D.Geometry]) -> D.Geometry) -> D.Geometry {
156-
CachedConcreteArrayTransformer(body: self, name: "Cadova.Separate") {
157-
$0.decompose()
158-
} resultHandler: {
159-
reader($0)
160-
}
161-
}
162-
}

Sources/Cadova/Abstract Layer/Operations/Transformations/RotateAround.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,21 @@ public extension Geometry3D {
7777
.rotated(x: x, y: y, z: z)
7878
.translated(-translation)
7979
}
80+
}
8081

82+
/// Rotates the geometry around an arbitrary axis defined by a 3D line.
83+
///
84+
/// The axis of rotation is given by the line’s direction and passes through a point on the line.
85+
///
86+
/// - Parameters:
87+
/// - angle: The angle to rotate around the axis.
88+
/// - axis: A line in 3D defining the rotation axis (direction) and the point it passes through.
89+
/// - Returns: A new geometry that is the result of applying the specified rotation around the line.
90+
///
91+
func rotated(_ angle: Angle, around axis: Line<D3>) -> any Geometry3D {
92+
self
93+
.translated(-axis.point)
94+
.transformed(.rotation(angle: angle, around: axis.direction))
95+
.translated(axis.point)
8196
}
8297
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import Foundation
2+
3+
public struct BuildDirective: Sendable {
4+
internal let payload: Payload
5+
6+
internal enum Payload: Sendable {
7+
case geometry2D (any Geometry2D)
8+
case geometry3D (any Geometry3D)
9+
case model (Model)
10+
case options (ModelOptions)
11+
case environment (@Sendable (inout EnvironmentValues) -> ())
12+
}
13+
14+
var geometry2D: (any Geometry2D)? {
15+
if case .geometry2D(let geometry2D) = payload { geometry2D } else { nil }
16+
}
17+
18+
var geometry3D: (any Geometry3D)? {
19+
if case .geometry3D(let geometry3D) = payload { geometry3D } else { nil }
20+
}
21+
22+
var model: Model? {
23+
if case .model(let model) = payload { model } else { nil }
24+
}
25+
26+
var options: ModelOptions? {
27+
if case .options(let options) = payload { options } else { nil }
28+
}
29+
30+
var environment: ((inout EnvironmentValues) -> ())? {
31+
if case .environment(let environment) = payload { environment } else { nil }
32+
}
33+
}
34+
35+
// This is a bit of a hack to allow the use of Environment inside result builders
36+
public extension Environment<@Sendable (inout EnvironmentValues) -> ()> {
37+
init(_ builder: @Sendable @escaping (inout EnvironmentValues) -> ()) {
38+
getter = { _ in builder }
39+
}
40+
41+
init<Value: Sendable>(
42+
_ keyPath: WritableKeyPath<EnvironmentValues, Value>,
43+
_ value: Value
44+
){
45+
getter = { _ in { $0[keyPath: keyPath] = value }}
46+
}
47+
}

0 commit comments

Comments
 (0)