Skip to content

Commit fcfc41f

Browse files
committed
Add APIs for reading individual tag members
1 parent cfa4070 commit fcfc41f

2 files changed

Lines changed: 104 additions & 0 deletions

File tree

Sources/Cadova/Abstract Layer/Geometry/References/Tag.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,52 @@ extension Tag: Geometry {
8080
}
8181
}
8282

83+
public extension Tag {
84+
/// Reads the individual geometry members associated with this tag and provides them for further composition.
85+
///
86+
/// This is similar to using the tag directly as geometry, except the tagged definitions are passed to the
87+
/// `reader` closure as separate geometries instead of being unioned first. Each geometry is positioned in the
88+
/// same coordinate system as a direct tag reference, preserving the world-space transform captured when tagged.
89+
///
90+
/// - Parameter reader: A closure that receives all geometries currently associated with this tag.
91+
/// - Returns: A geometry object resulting from the `reader` closure.
92+
func readingMembers<Output: Dimensionality>(
93+
@GeometryBuilder<Output> _ reader: @Sendable @escaping (_ members: [D3.Geometry]) -> Output.Geometry
94+
) -> Output.Geometry {
95+
TagGeometryReader(tag: self, reader: reader)
96+
}
97+
98+
/// Applies a transform to each individual geometry member associated with this tag and unions the results.
99+
///
100+
/// This is a convenience wrapper around `readingMembers` for the common case where every tagged definition should
101+
/// be processed independently.
102+
///
103+
/// - Parameter transform: A closure called once for each tagged geometry member.
104+
/// - Returns: The union of all geometries returned by `transform`.
105+
func map<Output: Dimensionality>(
106+
@GeometryBuilder<Output> _ transform: @Sendable @escaping (_ member: D3.Geometry) -> Output.Geometry
107+
) -> Output.Geometry {
108+
readingMembers { members in
109+
for member in members {
110+
transform(member)
111+
}
112+
}
113+
}
114+
}
115+
116+
internal struct TagGeometryReader<Output: Dimensionality>: Geometry {
117+
let tag: Tag
118+
let reader: @Sendable ([D3.Geometry]) -> Output.Geometry
119+
120+
func build(in environment: EnvironmentValues, context: EvaluationContext) async throws -> Output.BuildResult {
121+
let geometries = environment.buildResults(for: tag).map {
122+
$0.transformed(environment.transform.inverse)
123+
}
124+
return try await context.buildResult(for: reader(geometries), in: environment)
125+
.modifyingElement(ReferenceState.self) { $0.read(tag: tag) }
126+
}
127+
}
128+
83129
internal struct TagGeometry: Geometry {
84130
let body: any Geometry3D
85131
let tag: Tag

Tests/Tests/Tags.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,62 @@ struct TagTests {
5757

5858
#expect(try await geometry.measurements.volume 19)
5959
}
60+
61+
@Test func `tag can read definitions as separate members`() async throws {
62+
let sharedTag = Tag("shared tag")
63+
64+
let geometry = Box(x: 21, y: 1, z: 1)
65+
.subtracting {
66+
sharedTag
67+
}
68+
.adding {
69+
Box(1)
70+
.tagged(sharedTag)
71+
.subtracting { Box(1) }
72+
.translated(x: 5)
73+
}
74+
.adding {
75+
Box(1)
76+
.tagged(sharedTag)
77+
.subtracting { Box(1) }
78+
.translated(x: 15)
79+
}
80+
.adding {
81+
sharedTag.readingMembers { members in
82+
for (index, member) in members.enumerated() {
83+
member.translated(y: Double(index) * 10)
84+
}
85+
}
86+
}
87+
88+
let bounds = try await geometry.bounds
89+
#expect(bounds?.minimum.x 0)
90+
#expect(bounds?.maximum.x 21)
91+
#expect(bounds?.maximum.y 11)
92+
#expect(try await geometry.measurements.volume 21)
93+
}
94+
95+
@Test func `tag can map each member separately`() async throws {
96+
let sharedTag = Tag("shared tag")
97+
98+
let geometry = Box(1)
99+
.tagged(sharedTag)
100+
.translated(x: 5)
101+
.adding {
102+
Box(1)
103+
.tagged(sharedTag)
104+
.translated(x: 15)
105+
}
106+
.adding {
107+
sharedTag.map { member in
108+
member.translated(z: 10)
109+
}
110+
}
111+
112+
let bounds = try await geometry.bounds
113+
#expect(bounds?.minimum.x 5)
114+
#expect(bounds?.maximum.x 16)
115+
#expect(bounds?.maximum.z 11)
116+
#expect(try await geometry.measurements.volume 4)
117+
}
60118
}

0 commit comments

Comments
 (0)