Skip to content

Commit abd0566

Browse files
committed
Add modifyingBodyAndParts to apply an operation to body and parts
Lets a single closure modify the main geometry and each matching part in one call, avoiding having to repeat the operation across both. Defaults to the .solid semantic filter, mirroring modifyingParts.
1 parent 36f50d2 commit abd0566

2 files changed

Lines changed: 52 additions & 0 deletions

File tree

Sources/Cadova/Abstract Layer/Geometry/Parts/PartModification.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,28 @@ public extension Geometry {
7272
PartModifier(body: self, predicate: { $0.semantic == type }, modifier: reader)
7373
}
7474

75+
/// Applies a transformation to the main geometry and to each part of the specified semantic.
76+
///
77+
/// This method runs the `reader` closure on the receiver's body geometry and on each part in the
78+
/// catalog whose semantic matches `type`. Use it to apply the same modification - such as a
79+
/// boolean operation, recoloring, or wrapping - to both the body and matching parts in a single
80+
/// pass, without writing the closure twice.
81+
///
82+
/// - Parameters:
83+
/// - type: The semantic of parts to modify. Defaults to `.solid`.
84+
/// - reader: A closure that receives 3D geometry (either the body or a part's combined geometry)
85+
/// and returns new geometry to replace it.
86+
/// - Returns: A geometry with the closure applied to the body and to each matching part.
87+
///
88+
func modifyingBodyAndParts(
89+
ofType type: PartSemantic = .solid,
90+
@GeometryBuilder<D3> reader: @Sendable @escaping (_ geometry: any Geometry3D) -> any Geometry3D
91+
) -> D3.Geometry where D == D3 {
92+
reader(self).modifyingParts(ofType: type) { partGeometry, _ in
93+
reader(partGeometry)
94+
}
95+
}
96+
7597
/// Removes the specified part from the part catalog.
7698
///
7799
/// This does not alter the base geometry of the receiver; it only removes the matching entry

Tests/Tests/Parts.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,36 @@ struct PartTests {
247247
#expect(try await geometry.measurements.volume 1016)
248248
}
249249

250+
@Test func `modifyingBodyAndParts applies operation to body and matching parts`() async throws {
251+
let boxPart = Part("box")
252+
let visualPart = Part("visual", semantic: .visual)
253+
254+
let geometry = Box(10)
255+
.adding {
256+
Box(4)
257+
.translated(x: 12)
258+
.inPart(boxPart)
259+
Box(4)
260+
.translated(x: 20)
261+
.inPart(visualPart)
262+
}
263+
.modifyingBodyAndParts {
264+
$0.subtracting {
265+
Box(x: 100, y: 1, z: 1)
266+
.translated(x: -10, y: 1, z: 1)
267+
}
268+
}
269+
270+
// Slab carves 10 from Box(10) and 4 from the solid Box(4) at x=12. The .visual part is
271+
// skipped because the default semantic filter is .solid, leaving it at its full volume of 64.
272+
#expect(try await geometry.partNames.sorted() == ["box", "visual"])
273+
#expect(try await geometry.mainModelMeasurements.volume 990) // 1000 - 10
274+
// body (990) + solid part (64 - 4 = 60), all disjoint; visual excluded by .solidParts scope
275+
#expect(try await geometry.measurements.volume 1050)
276+
// body (990) + solid part (60) + untouched visual part (64) = 1114
277+
#expect(try await geometry.measurements(for: .allParts).volume 1114)
278+
}
279+
250280
@Test func `removingParts removes all parts of semantic`() async throws {
251281
let box1Part = Part("box1")
252282
let box2Part = Part("box2")

0 commit comments

Comments
 (0)