Skip to content

Commit e39f073

Browse files
tomasfclaude
andcommitted
Add visualized() method to Loft for debugging layer positions
Shows each loft layer as a thin extruded slab at its Z position, with each layer colored distinctly (red, blue, green, orange, etc.) to make it easy to identify individual layers. Useful for verifying layer positions and shapes before running the full loft operation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 58f5199 commit e39f073

File tree

2 files changed

+70
-0
lines changed

2 files changed

+70
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import Foundation
2+
3+
public extension Loft {
4+
/// Produces a visualization of the loft layers without performing the actual loft operation.
5+
///
6+
/// Each layer is shown as a thin extruded shape at its Z position, with each layer
7+
/// colored distinctly to make it easy to identify individual layers. This is useful for
8+
/// debugging loft configurations and verifying that layers are positioned and shaped
9+
/// as expected before running the full loft operation.
10+
///
11+
/// The visualization shows:
12+
/// - Each 2D layer shape extruded to a thin slab at its Z height.
13+
/// - Each layer colored with a distinct color from a rotating palette.
14+
/// - Layers are placed in a separate visual part named "Visualized Loft Layers".
15+
///
16+
/// Configure appearance using the public Geometry modifiers:
17+
/// - `withVisualizationScale(_:)` adjusts the thickness of each layer slab.
18+
///
19+
func visualized() -> any Geometry3D {
20+
LoftVisualization(layers: layers)
21+
}
22+
}
23+
24+
fileprivate struct LoftVisualization: Shape3D {
25+
let layers: [Loft.Layer]
26+
27+
var body: any Geometry3D {
28+
@Environment(\.visualizationOptions.scale) var scale = 1.0
29+
let thickness = 0.001 * scale
30+
31+
Union {
32+
for (index, layer) in layers.enumerated() {
33+
layer.geometry()
34+
.extruded(height: thickness)
35+
.translated(z: layer.z - thickness / 2)
36+
.colored(Color.layerColors[index % Color.layerColors.count], alpha: 0.7)
37+
}
38+
}
39+
.inPart(named: "Visualized Loft Layers", type: .visual)
40+
}
41+
}
42+
43+
fileprivate extension Color {
44+
static let layerColors: [Color] = [.red, .blue, .green, .orange, .purple, .cyan, .magenta, .yellow]
45+
}

Tests/Tests/Loft.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,5 +130,30 @@ struct LoftTests {
130130
// Bounding box should span from the circle at bottom to rectangle at top
131131
#expect(m.boundingBox .init(minimum: [-10, -10, 0], maximum: [10, 10, 30]))
132132
}
133+
134+
@Test func `visualized loft shows layers at correct positions`() async throws {
135+
let loft = Loft {
136+
layer(z: 0) {
137+
Circle(diameter: 20)
138+
}
139+
layer(z: 10) {
140+
Rectangle([15, 15])
141+
.aligned(at: .center)
142+
}
143+
layer(z: 25) {
144+
Circle(diameter: 10)
145+
}
146+
}
147+
148+
let visualization = loft.visualized()
149+
try await visualization.writeVerificationModel(name: "loftVisualized")
150+
let m = try await visualization.measurements(for: .allParts)
151+
152+
// The visualization should span approximately from z=0 to z=25
153+
#expect(m.boundingBox!.minimum.z 0)
154+
#expect(m.boundingBox!.maximum.z 25)
155+
// Should have some volume (the extruded layer slabs)
156+
#expect(m.volume > 0)
157+
}
133158
}
134159

0 commit comments

Comments
 (0)