HyperbasisHyperbasisDocs
Home

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

PropertyTypeDescription
id*UUIDUnique identifier for the anchor
spaceId*UUIDParent space ID
transform*[Float]16 floats (4x4 matrix, column-major)
metadata[String: AnyCodableValue]Custom user data
createdAt*DateCreation timestamp
updatedAt*DateLast modification timestamp
deletedAtDate?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 always 1.0 for 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.position

Transform 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: Bool
Note

Soft 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() -> Bool

Error 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

EventTrigger
createdFirst save
movedTransform changed
updatedMetadata changed
deleteddeleteAnchor() called
restoredRestored 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)
}
Note

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