HBAnchor
Represents a placed object in AR space with position, rotation, and custom metadata.
Overview
HBAnchor stores the transform (position + rotation + scale) of a placed object along with arbitrary metadata. Anchors belong to a space and persist across sessions.
public struct HBAnchor: Codable, Identifiable, Equatable {
public let id: UUID
public let spaceId: UUID
public var transform: [Float]
public var metadata: [String: AnyCodableValue]
public let createdAt: Date
public var updatedAt: Date
public var deletedAt: Date?
}Properties
| Property | Type | Description |
|---|---|---|
id* | UUID | Unique identifier for the anchor |
spaceId* | UUID | Parent space ID |
transform* | [Float] | 16 floats (4x4 matrix, column-major) |
metadata | [String: AnyCodableValue] | Custom user data |
createdAt* | Date | Creation timestamp |
updatedAt* | Date | Last modification timestamp |
deletedAt | Date? | Soft delete timestamp (nil if active) |
Initialization
From simd_float4x4 (typical usage)
let anchor = HBAnchor(
id: UUID(), // optional
spaceId: space.id, // required
transform: simdFloat4x4Matrix, // 4x4 transformation matrix
metadata: [ // optional
"text": .string("Buy milk"),
"color": .string("yellow"),
"priority": .int(1)
]
)From pre-flattened data (loading from storage)
let anchor = HBAnchor(
id: existingId,
spaceId: spaceId,
transform: [Float](repeating: 0, count: 16),
metadata: [:],
createdAt: originalDate,
updatedAt: modifiedDate,
deletedAt: nil
)Transform Handling
The transform is stored as a flat array of 16 floats in column-major order (matching simd_float4x4 memory layout).
Transform Matrix Layout
Column 0 Column 1 Column 2 Column 3
(Right axis) (Up axis) (Forward) (Position)
┌─────────────────────────────────────────────────┐
Row 0 │ [0] [4] [8] [12] │ ← X
Row 1 │ [1] [5] [9] [13] │ ← Y
Row 2 │ [2] [6] [10] [14] │ ← Z
Row 3 │ [3] [7] [11] [15] │ ← W (always 0,0,0,1)
└─────────────────────────────────────────────────┘
↑ ↑ ↑ ↑
Rotation Rotation Rotation Translation
Key points:
- •Position is at indices
[12, 13, 14](column 3, rows 0-2) - •Rotation is encoded in columns 0-2 (the 3x3 upper-left submatrix)
- •Index
[15]is always1.0for affine transforms
Reading Position from Transform
let transform: [Float] = anchor.transform
let x = transform[12] // X position
let y = transform[13] // Y position
let z = transform[14] // Z position
// Or use the convenience property:
let position: SIMD3<Float> = anchor.positionTransform Methods
// Flatten simd_float4x4 to [Float]
public static func flatten(_ matrix: simd_float4x4) -> [Float]
// Unflatten [Float] back to simd_float4x4
public static func unflatten(_ array: [Float]) throws -> simd_float4x4
// Get simd transform (throws if invalid)
public func simdTransform() throws -> simd_float4x4
// Get simd transform (returns nil if invalid)
public var validSimdTransform: simd_float4x4?
// Extract position from transform
public var position: SIMD3<Float>Metadata Methods
// Update single metadata value
public mutating func updateMetadata(key: String, value: AnyCodableValue?)
// Replace all metadata
public mutating func update(metadata: [String: AnyCodableValue])
// Get metadata value
public func metadata(forKey key: String) -> AnyCodableValue?
// Typed accessors
public func stringMetadata(forKey key: String) -> String?
public func intMetadata(forKey key: String) -> Int?
public func boolMetadata(forKey key: String) -> Bool?Soft Delete
// Mark as deleted
public mutating func markDeleted()
// Restore from soft delete
public mutating func restore()
// Check if deleted
public var isDeleted: BoolSoft delete sets deletedAt to the current time. The anchor remains in storage but is excluded from loadAnchors() by default. Use includeDeleted: true to include deleted anchors.
Validation
// Returns true if transform has exactly 16 elements
public func validate() -> BoolError Handling
public enum HBAnchorError: LocalizedError {
case invalidTransform(count: Int)
case alreadyDeleted
case metadataKeyNotFound(key: String)
}Versioning
Every change to an anchor is recorded as an event.
Event Types
| Event | Trigger |
|---|---|
created | First save |
moved | Transform changed |
updated | Metadata changed |
deleted | deleteAnchor() called |
restored | Restored from deletion or rollback |
Viewing History
let events = try await storage.history(anchorId: anchor.id)
for event in events {
switch event.type {
case .created:
print("Created at \(event.timestamp)")
case .moved:
print("Moved to \(event.transform ?? [])")
case .updated:
print("Updated metadata: \(event.metadata ?? [:])")
case .deleted:
print("Deleted")
case .restored:
print("Restored")
}
}Rollback
// Get current version
let events = try await storage.history(anchorId: anchor.id)
let currentVersion = events.last?.version ?? 0
// Roll back to version 2
if currentVersion > 2 {
try await storage.rollback(anchorId: anchor.id, toVersion: 2)
}Existing anchors from before versioning are automatically migrated. On first save after upgrade, a created event is generated using the anchor's original createdAt date.
Common Metadata Schemas
Here are recommended metadata patterns for different use cases:
Sticky Notes / Labels
[
"type": .string("sticky_note"),
"text": .string("Remember to buy milk"),
"color": .string("yellow"), // or hex: "#FFEB3B"
"fontSize": .int(16),
"completed": .bool(false)
]3D Models
[
"type": .string("model"),
"modelName": .string("chair_modern"),
"scale": .double(1.0),
"category": .string("furniture"),
"interactable": .bool(true)
]Waypoints / Navigation
[
"type": .string("waypoint"),
"label": .string("Exit"),
"order": .int(3), // sequence in path
"icon": .string("arrow_right"),
"isDestination": .bool(false)
]Measurements
[
"type": .string("measurement"),
"unit": .string("meters"),
"value": .double(2.45),
"endpointId": .string("uuid-of-other-anchor") // for line measurements
]Usage Examples
Create and save an anchor
let anchor = HBAnchor(
spaceId: currentSpace.id,
transform: entity.transform.matrix,
metadata: [
"type": .string("sticky_note"),
"text": .string("Remember to buy milk"),
"color": .string("yellow")
]
)
try await storage.save(anchor)Load and recreate anchors
let anchors = try await storage.loadAnchors(spaceId: space.id)
for anchor in anchors {
let transform = try anchor.simdTransform()
let position = anchor.position
if let text = anchor.stringMetadata(forKey: "text") {
let entity = createNoteEntity(text: text)
entity.transform.matrix = transform
arView.scene.addAnchor(AnchorEntity(world: position))
}
}Update an anchor
var anchor = try await storage.loadAnchor(id: noteId)!
anchor.updateMetadata(key: "text", value: .string("Updated text"))
anchor.updateMetadata(key: "completed", value: .bool(true))
try await storage.save(anchor)Delete an anchor
// Soft delete
try await storage.deleteAnchor(id: noteId)
// Permanently remove old deleted anchors
let thirtyDaysAgo = Calendar.current.date(byAdding: .day, value: -30, to: Date())!
try await storage.purgeDeletedAnchors(before: thirtyDaysAgo)Next Steps
- •AnyCodableValue - Flexible metadata values
- •HBStorage - Save and load anchors