Skip to content

Instantly share code, notes, and snippets.

@durul
Created October 25, 2025 14:26
Show Gist options
  • Select an option

  • Save durul/70efad754c2c7f6fd21fed30c7f935e6 to your computer and use it in GitHub Desktop.

Select an option

Save durul/70efad754c2c7f6fd21fed30c7f935e6 to your computer and use it in GitHub Desktop.

πŸ–ŠοΈ Logitech Muse Stylus Integration

Complete visionOS implementation for tracking Logitech Muse stylus with real-time position updates, button input, and haptic feedback.

πŸš€ Quick Start

1. Prerequisites

  • visionOS 26+
  • Logitech Muse stylus
  • Xcode 26+

2. Required Permissions

Add to Info.plist:

<key>NSAccessoryTrackingUsageDescription</key>
<string>tracking accessory movements to carve into virtual clay</string>

3. Setup Scene

// In WindowsDemoApp.swift
ImmersiveSpace(id: "LogitechMuse") {
    ImmersiveStylusView()
}
.immersionStyle(selection: $selectedImmersionStyle, in: .mixed, .progressive, .full)

4. Connect Stylus

  1. Open Vision Pro Settings β†’ Bluetooth
  2. Pair Logitech Muse
  3. Launch app
  4. Open "Logitech Muse" immersive space

✨ Features

  • βœ… Real-time 6DOF tracking - Position + Orientation
  • βœ… Visual indicator - 2mm white sphere at stylus tip
  • βœ… Button input - Primary (Green) / Secondary (Blue)
  • βœ… Haptic feedback - Vibration on button press
  • βœ… Auto cleanup - Handles connect/disconnect gracefully

🎯 Key Concepts

SpatialTrackingSession

// CRITICAL: Store session to prevent deallocation!
// SpatialTrackingSession.run() is blocking - if local var only,
// it might be deallocated and tracking stops
@State private var trackingSession: SpatialTrackingSession?

.task {
    let session = SpatialTrackingSession()
    trackingSession = session  // Keep alive!
    await session.run(configuration)
}

Anchor Hierarchy

RootEntity
  └─ AnchorEntity (.accessory)  ← ARKit tracks this
      └─ ModelEntity (tip)      ← Moves automatically with anchor!

Button Handling

input.buttons[.stylusPrimaryButton]?.pressedInput.pressedDidChangeHandler = { _, _, pressed in
    Task { @MainActor in  // CRITICAL: Main thread for UI!
        if pressed {
            changeTipColor(to: .systemGreen)
            playHaptic()
        }
    }
}

πŸ› Common Issues

"session.run() returned"

Solution: Store session in @State:

@State private var trackingSession: SpatialTrackingSession?
trackingSession = session  // In .task

"ARKitAnchorComponent MISSING!"

Solution:

  1. Check stylus is connected via Bluetooth
  2. Verify Info.plist permissions
  3. Wait ~200 frames after connection

Tip indicator doesn't move

Solution: Add as child of anchor, not root:

anchor.addChild(tipSphere)  // βœ… CORRECT
root.addChild(tipSphere)    // ❌ WRONG - won't move!

πŸ”§ Configuration

Tip Size

let tipSphere = ModelEntity(
    mesh: .generateSphere(radius: 0.002),  // 2mm
    materials: [material]
)

Tracking Mode

  // CRITICAL: Create AnchorEntity with accessory anchoring
  // This tells ARKit to track this entity's position to match the stylus
let anchor = AnchorEntity(
    .accessory(from: source, location: location),
    trackingMode: .predicted,  // Options: .predicted, .continuous, .once
    physicsSimulation: .none
)

Haptic Intensity

CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),  // 0.0-1.0
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)   // 0.0-1.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment