@@ -16,9 +16,45 @@ import Foundation
1616/// “used” in the current tree so it can be resolved in a later pass. If a tag remains unresolved at the top level,
1717/// a warning is printed.
1818///
19- /// - Coordinate systems:
20- /// - Referencing a tag reproduces the tagged geometry at the same world-space location and orientation it had at the
21- /// time of tagging. This means the geometry appears where you would expect, relative to the current transform context.
19+ /// ## World-anchored references
20+ ///
21+ /// A tag reference reproduces its geometry at the world-space position it had at the time of tagging,
22+ /// **regardless of where the reference appears in the tree**. This is the central property of tags:
23+ /// they are anchored to a captured world position, so transforms applied to ancestor geometry around
24+ /// a reference do not move it.
25+ ///
26+ /// ```swift
27+ /// let part = Tag()
28+ /// let model = Box(1)
29+ /// .translated(x: 5) // tagged box is at world (5...6)
30+ /// .tagged(part)
31+ /// .adding {
32+ /// // The Union below would normally translate everything inside it by +100,
33+ /// // but the reference stays anchored at its captured world position (5...6).
34+ /// Union { part }.translated(x: 100)
35+ /// }
36+ /// ```
37+ ///
38+ /// ## Transforms applied directly to a reference
39+ ///
40+ /// Transforms chained directly onto a tag reference *do* move it. They are applied in the
41+ /// **local coordinate frame** at the call site — the same way transforms on regular geometry behave:
42+ ///
43+ /// ```swift
44+ /// part.translated(x: 10) // reference moves +10 along local X
45+ /// part.translated(x: 10).rotated(...) // chained transforms compose in local frame
46+ /// ```
47+ ///
48+ /// If the reference sits inside a rotated parent, the direct transforms see that rotation as the
49+ /// local frame, so `.translated(x: 10)` moves along the parent's local X — not world X.
50+ ///
51+ /// The distinction is:
52+ /// - **Direct chains** on a tag/reference (via `translated`, `rotated`, etc.) compose into a single
53+ /// transform and move the reference, in the local frame.
54+ /// - **Outer wrappers** (transforms applied to a parent containing a reference) flow through the
55+ /// environment and are cancelled, preserving the world anchor.
56+ ///
57+ /// This means `tag.translated(x: 10)` translates; but `Group { tag }.translated(x: 10)` does not.
2258///
2359public struct Tag : Hashable , Sendable {
2460 internal let id = UUID ( )
@@ -43,8 +79,16 @@ public struct Tag: Hashable, Sendable {
4379public extension Geometry3D {
4480 /// Attaches a tag to this geometry, allowing it to be referenced elsewhere in the same model.
4581 ///
46- /// The tagged geometry is recorded in its current coordinate system so that later references to the tag reproduce
47- /// the same geometry at the same world-space location and orientation.
82+ /// The tagged geometry is recorded in its **current** coordinate system, meaning the world-space position
83+ /// it has at the point where `tagged(_:)` is called — including any transforms applied to it earlier in
84+ /// the chain. References to this tag later reproduce the geometry at that same world position.
85+ ///
86+ /// ```swift
87+ /// // The tagged box is captured at world (5...6) — the translation is part of the anchor.
88+ /// Box(1).translated(x: 5).tagged(myTag)
89+ /// ```
90+ ///
91+ /// See ``Tag`` for how references behave when transforms are applied to them.
4892 ///
4993 /// - Multiple definitions:
5094 /// - You can tag multiple geometries with the same `Tag`. When that tag is referenced, all tagged geometries are
@@ -64,6 +108,10 @@ public extension Geometry3D {
64108/// location and orientation it had at the time of tagging. If the tag was applied to multiple geometries,
65109/// the referenced result is the merged (unioned) combination of all of them.
66110///
111+ /// Transforms applied to ancestor geometry around the reference are cancelled by the world-anchor logic,
112+ /// while transforms chained directly onto the reference (via `translated`, `rotated`, etc.) move it
113+ /// relative to its anchor. See ``Tag`` for a full discussion of these two cases.
114+ ///
67115/// - Undefined tags:
68116/// - If the tag has not yet been defined in the model, this produces an empty geometry placeholder and marks the
69117/// tag as “used” so it can be resolved in a later pass. If the tag is still undefined at the top level, a
@@ -78,6 +126,48 @@ extension Tag: Geometry {
78126 return try await context. buildResult ( for: output, in: environment)
79127 . modifyingElement ( ReferenceState . self) { $0. read ( tag: self ) }
80128 }
129+
130+ /// Applies a transform to this tag reference.
131+ ///
132+ /// Transforms chained directly onto a tag reference (e.g. `tag.translated(x: 10)`) are applied
133+ /// to the world-anchored geometry, so they move it as expected. Transforms applied to a parent
134+ /// geometry that contains a tag reference still flow through the environment and are cancelled
135+ /// by the world-anchor logic, preserving the captured position.
136+ ///
137+ public func transformed( _ transform: Transform3D ) -> any Geometry3D {
138+ if transform. isIdentity {
139+ return self
140+ }
141+ return TagReference ( tag: self , transform: transform)
142+ }
143+ }
144+
145+ /// A tag reference with applied modifications.
146+ ///
147+ /// Currently this carries an explicit transform; additional kinds of per-reference modifications
148+ /// (e.g. material or color overrides) can be added here without affecting `Tag`'s identity semantics.
149+ ///
150+ internal struct TagReference : Geometry {
151+ let tag : Tag
152+ let transform : Transform3D
153+
154+ func transformed( _ additional: Transform3D ) -> any Geometry3D {
155+ if additional. isIdentity {
156+ return self
157+ }
158+ return TagReference ( tag: tag, transform: transform. transformed ( additional) )
159+ }
160+
161+ func build( in environment: EnvironmentValues , context: EvaluationContext ) async throws -> D3 . BuildResult {
162+ let output = Union {
163+ environment. buildResults ( for: tag)
164+ }
165+ . transformed ( environment. transform. inverse)
166+ . transformed ( transform)
167+
168+ return try await context. buildResult ( for: output, in: environment)
169+ . modifyingElement ( ReferenceState . self) { $0. read ( tag: tag) }
170+ }
81171}
82172
83173public extension Tag {
0 commit comments