Skip to content

Commit 4bd4ab7

Browse files
committed
Add Ruler visualization for measuring distances
1 parent e8d255e commit 4bd4ab7

File tree

3 files changed

+98
-2
lines changed

3 files changed

+98
-2
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ internal extension MeasurementScope {
151151

152152
let partResults = try await additionalParts.asyncMap { try await context.result(for: $0.node) }
153153

154-
let foo = [main3D.concrete] + partResults.map(\.concrete)
155-
return foo as! [D.Concrete]
154+
let concretes = [main3D.concrete] + partResults.map(\.concrete)
155+
return concretes as! [D.Concrete]
156156
}
157157
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import Foundation
2+
3+
/// A ruler visualization for measuring distances in 3D space.
4+
///
5+
/// The ruler draws a line along the X axis with notches at regular intervals and numeric labels.
6+
/// Use transforms to position and orient the ruler as needed.
7+
///
8+
/// ```swift
9+
/// Ruler(length: 100, interval: 10)
10+
/// .rotated(z: 90°) // Orient along Y axis
11+
/// .translated(x: 50)
12+
/// ```
13+
///
14+
/// The thickness of the ruler elements is controlled by the visualization scale in the environment.
15+
/// Use `withVisualizationScale(_:)` to adjust it.
16+
///
17+
public struct Ruler: Shape3D {
18+
let length: Double
19+
let interval: Double
20+
21+
/// Creates a ruler with the specified length and interval.
22+
///
23+
/// - Parameters:
24+
/// - length: The total length of the ruler along the X axis.
25+
/// - interval: The distance between notches.
26+
///
27+
public init(length: Double, interval: Double) {
28+
self.length = length
29+
self.interval = interval
30+
}
31+
32+
public var body: any Geometry3D {
33+
@Environment(\.visualizationOptions.scale) var scale = 1.0
34+
@Environment(\.visualizationOptions.primaryColor) var color = .rulerDefault
35+
@Environment(\.visualizationOptions.labelsEnabled) var showLabels = true
36+
37+
let thickness = 0.1 * scale
38+
let notchHeight = 0.5 * scale
39+
let majorNotchHeight = 1.0 * scale
40+
41+
Box(x: length, y: thickness, z: thickness)
42+
.colored(color)
43+
.aligned(at: .minX)
44+
.adding {
45+
// Notches and labels
46+
let notchCount = Int(length / interval)
47+
for i in 0...notchCount {
48+
let x = Double(i) * interval
49+
let isMajor = i % 5 == 0
50+
let height = isMajor ? majorNotchHeight : notchHeight
51+
52+
// Notch
53+
Box(x: thickness, y: thickness, z: height)
54+
.aligned(at: .minZ)
55+
.translated(x: x)
56+
.colored(color)
57+
58+
// Label at major notches
59+
if isMajor && showLabels {
60+
RulerLabel(value: x)
61+
.scaled(scale)
62+
.rotated(x: 90°)
63+
.translated(x: x, z: majorNotchHeight + 0.5 * scale)
64+
}
65+
}
66+
}
67+
.withFontSize(1.5 * scale)
68+
.withTextAlignment(horizontal: .center, vertical: .bottom)
69+
.inPart(.visualizedRuler)
70+
}
71+
}
72+
73+
fileprivate struct RulerLabel: Shape3D {
74+
let value: Double
75+
76+
var body: any Geometry3D {
77+
@Environment(\.visualizationOptions.scale) var scale = 1.0
78+
Text(String(format: "%g", value))
79+
.measuringBounds { text, bounds in
80+
Stack(.z, alignment: .center) {
81+
Stadium(bounds.size + [1.0, 0.6] * scale)
82+
.extruded(height: 0.01)
83+
.colored(.white, alpha: 0.5)
84+
85+
text.extruded(height: 0.01)
86+
.colored(.black)
87+
}
88+
.aligned(at: .minY)
89+
}
90+
}
91+
}
92+
93+
fileprivate extension Color {
94+
static let rulerDefault: Self = .white
95+
}

Sources/Cadova/Concrete Layer/Part.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ internal extension Part {
110110
static let visualizedPath = Part("Visualized Path", semantic: .visual)
111111
static let visualizedDirection = Part("Visualized Direction", semantic: .visual)
112112
static let visualizedLoftLayers = Part("Visualized Loft Layers", semantic: .visual)
113+
static let visualizedRuler = Part("Visualized Ruler", semantic: .visual)
113114
}
114115

115116
internal struct PartCatalog: ResultElement {

0 commit comments

Comments
 (0)