|
| 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 | +} |
0 commit comments