Skip to content

visionOS 2 additions #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Foundation
import RealityKit

extension AccessibilityComponent {
init(from element: ElementNode, in context: ComponentContentBuilder<some ComponentRegistry>.Context<some RootRegistry>) throws {
init(from element: ElementNode, in context: _ComponentContentBuilder<some ComponentRegistry>.Context<some RootRegistry>) throws {
self.init()

self.label = element.attributeValue(for: "label").flatMap(LocalizedStringResource.init(stringLiteral:))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import LiveViewNativeCore
import RealityKit

extension AnchoringComponent {
init(from element: ElementNode, in context: ComponentContentBuilder<some ComponentRegistry>.Context<some RootRegistry>) throws {
init(from element: ElementNode, in context: _ComponentContentBuilder<some ComponentRegistry>.Context<some RootRegistry>) throws {
let target = try element.attributeValue(AnchoringComponent.Target.self, for: "target")
if let trackingMode = try? element.attributeValue(AnchoringComponent.TrackingMode.self, for: "trackingMode") {
self.init(target, trackingMode: trackingMode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import LiveViewNativeCore
import RealityKit

extension CollisionComponent {
init<C: ComponentRegistry>(from element: ElementNode, in context: ComponentContentBuilder<C>.Context<some RootRegistry>) throws {
let shapes = try ComponentContentBuilder<C>.buildChildren(of: element, with: ShapeResourceContentBuilder.self, in: context)
init<C: ComponentRegistry>(from element: ElementNode, in context: _ComponentContentBuilder<C>.Context<some RootRegistry>) throws {
let shapes = try _ComponentContentBuilder<C>.buildChildren(of: element, with: ShapeResourceContentBuilder.self, in: context)
let isStatic = element.attributeBoolean(for: "isStatic")
let filter = try? element.attributeValue(CollisionFilter.self, for: "filter")
let mode = try? element.attributeValue(Mode.self, for: "mode")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import LiveViewNative
import RealityKit

extension GroundingShadowComponent {
init(from element: ElementNode, in context: ComponentContentBuilder<some ComponentRegistry>.Context<some RootRegistry>) throws {
init(from element: ElementNode, in context: _ComponentContentBuilder<some ComponentRegistry>.Context<some RootRegistry>) throws {
self.init(
castsShadow: element.attributeBoolean(for: "castsShadow")
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// HoverEffectComponent.swift
//
//
// Created by Carson Katri on 6/12/24.
//

import LiveViewNative
import RealityKit
import SwiftUI

extension HoverEffectComponent {
init(from element: ElementNode, in context: _ComponentContentBuilder<some ComponentRegistry>.Context<some RootRegistry>) throws {
if #available(visionOS 2, *) {
switch element.attributeValue(for: "hoverEffect") {
case "spotlight":
self.init(
.spotlight(.init(
color: (try? element.attributeValue(Color.self, for: "color")).flatMap(UIColor.init),
strength: (try? element.attributeValue(Float.self, for: "strength")) ?? 1
))
)
case "highlight":
self.init(
.highlight(.init(
color: (try? element.attributeValue(Color.self, for: "color")).flatMap(UIColor.init),
strength: (try? element.attributeValue(Float.self, for: "strength")) ?? 1
))
)
default:
self.init()
}
} else {
self.init()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import LiveViewNative
import RealityKit

extension OpacityComponent {
init(from element: ElementNode, in context: ComponentContentBuilder<some ComponentRegistry>.Context<some RootRegistry>) throws {
init(from element: ElementNode, in context: _ComponentContentBuilder<some ComponentRegistry>.Context<some RootRegistry>) throws {
self.init(
opacity: (try? element.attributeValue(Float.self, for: "opacity")) ?? 1
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import LiveViewNativeCore
import RealityKit

extension PhysicsBodyComponent {
init<C: ComponentRegistry>(from element: ElementNode, in context: ComponentContentBuilder<C>.Context<some RootRegistry>) throws {
init<C: ComponentRegistry>(from element: ElementNode, in context: _ComponentContentBuilder<C>.Context<some RootRegistry>) throws {
if let mass = try? element.attributeValue(Float.self, for: "mass") {
self.init(
shapes: try ComponentContentBuilder<C>.buildChildren(of: element, with: ShapeResourceContentBuilder.self, in: context),
shapes: try _ComponentContentBuilder<C>.buildChildren(of: element, with: ShapeResourceContentBuilder.self, in: context),
mass: mass,
material: try? PhysicsMaterialResource.generate(from: element.attribute(named: "material"), on: element),
mode: (try? element.attributeValue(PhysicsBodyMode.self, for: "mode")) ?? .dynamic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import OSLog

private let logger = Logger(subsystem: "LiveViewNativeRealityKit", category: "ComponentContentBuilder")

struct ComponentContentBuilder<Components: ComponentRegistry>: ComponentRegistry {
enum TagName: RawRepresentable {
public struct _ComponentContentBuilder<Components: ComponentRegistry>: ComponentRegistry {
public enum TagName: RawRepresentable {
case builtin(Builtin)
case custom(Components.TagName)

enum Builtin: String {
public enum Builtin: String {
case group = "Group"

case anchoringComponent = "AnchoringComponent"
Expand All @@ -30,9 +30,9 @@ struct ComponentContentBuilder<Components: ComponentRegistry>: ComponentRegistry
case hoverEffectComponent = "HoverEffectComponent"
}

typealias RawValue = String
public typealias RawValue = String

init?(rawValue: String) {
public init?(rawValue: String) {
if let builtin = Builtin.init(rawValue: rawValue) {
self = .builtin(builtin)
} else if let custom = Components.TagName.init(rawValue: rawValue) {
Expand All @@ -42,7 +42,7 @@ struct ComponentContentBuilder<Components: ComponentRegistry>: ComponentRegistry
}
}

var rawValue: RawValue {
public var rawValue: RawValue {
switch self {
case .builtin(let builtin):
builtin.rawValue
Expand All @@ -52,7 +52,7 @@ struct ComponentContentBuilder<Components: ComponentRegistry>: ComponentRegistry
}
}

static func lookup<R: RootRegistry>(_ tag: TagName, element: LiveViewNative.ElementNode, context: Context<R>) -> [any Component] {
public static func lookup<R: RootRegistry>(_ tag: TagName, element: LiveViewNative.ElementNode, context: Context<R>) -> [any Component] {
do {
switch tag {
case let .builtin(builtin):
Expand All @@ -75,7 +75,7 @@ struct ComponentContentBuilder<Components: ComponentRegistry>: ComponentRegistry
case .collisionComponent:
return [try CollisionComponent(from: element, in: context)]
case .hoverEffectComponent:
return [HoverEffectComponent()]
return [try HoverEffectComponent(from: element, in: context)]
}
case .custom:
return try Self.build([element.node], with: Components.self, in: context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import OSLog

private let logger = Logger(subsystem: "LiveViewNativeRealityKit", category: "EntityContentBuilder")

struct EntityContentBuilder<Entities: EntityRegistry, Components: ComponentRegistry>: EntityRegistry {
enum TagName: RawRepresentable {
public struct EntityContentBuilder<Entities: EntityRegistry, Components: ComponentRegistry>: EntityRegistry {
public enum TagName: RawRepresentable {
case builtin(Builtin)
case custom(Entities.TagName)

typealias RawValue = String
public typealias RawValue = String

enum Builtin: String {
public enum Builtin: String {
case group = "Group"
case entity = "Entity"
case modelEntity = "ModelEntity"
Expand All @@ -32,7 +32,7 @@ struct EntityContentBuilder<Entities: EntityRegistry, Components: ComponentRegis
case viewAttachmentEntity = "ViewAttachmentEntity"
}

init?(rawValue: RawValue) {
public init?(rawValue: RawValue) {
if let builtin = Builtin(rawValue: rawValue) {
self = .builtin(builtin)
} else if let custom = Entities.TagName.init(rawValue: rawValue) {
Expand All @@ -42,7 +42,7 @@ struct EntityContentBuilder<Entities: EntityRegistry, Components: ComponentRegis
}
}

var rawValue: RawValue {
public var rawValue: RawValue {
switch self {
case .builtin(let builtin):
builtin.rawValue
Expand All @@ -52,7 +52,7 @@ struct EntityContentBuilder<Entities: EntityRegistry, Components: ComponentRegis
}
}

static func lookup<R: RootRegistry>(_ tag: TagName, element: LiveViewNative.ElementNode, context: Context<R>) -> Content {
public static func lookup<R: RootRegistry>(_ tag: TagName, element: LiveViewNative.ElementNode, context: Context<R>) -> Content {
let entity: Entity
do {
switch tag {
Expand All @@ -63,7 +63,7 @@ struct EntityContentBuilder<Entities: EntityRegistry, Components: ComponentRegis
return children
case .entity:
if element.attribute(named: "url") != nil || element.attribute(named: "named") != nil {
entity = AsyncEntity(from: element, in: context)
entity = try AsyncEntity(from: element, in: context)
} else {
entity = Entity()
}
Expand Down Expand Up @@ -179,6 +179,22 @@ extension Entity {
if let asyncEntity = self as? AsyncEntity {
try asyncEntity.updateResolvedEntity(with: element, in: context)
}

if element.attributeBoolean(for: "generateCollisionShapes") {
if element.attributeBoolean(for: .init(namespace: "generateCollisionShapes", name: "convex")) {
// convex mesh collision shapes
self.generateConvexCollisionShapes(
recursive: element.attributeBoolean(for: .init(namespace: "generateCollisionShapes", name: "recursive")),
static: element.attributeBoolean(for: .init(namespace: "generateCollisionShapes", name: "static"))
)
} else {
// simple box collision shapes
self.generateCollisionShapes(
recursive: element.attributeBoolean(for: .init(namespace: "generateCollisionShapes", name: "recursive")),
static: element.attributeBoolean(for: .init(namespace: "generateCollisionShapes", name: "static"))
)
}
}
}

func applyChildren<R: RootRegistry, E: EntityRegistry, C: ComponentRegistry>(
Expand Down Expand Up @@ -238,3 +254,19 @@ extension Entity {
}
}
}

extension Entity {
func generateConvexCollisionShapes(
recursive: Bool,
static isStatic: Bool
) {
if let mesh = self.components[ModelComponent.self]?.mesh {
self.components.set(CollisionComponent(shapes: [.generateConvex(from: mesh)], isStatic: isStatic))
}
if recursive {
for child in children {
child.generateConvexCollisionShapes(recursive: recursive, static: isStatic)
}
}
}
}
86 changes: 72 additions & 14 deletions Sources/LiveViewNativeRealityKit/Entities/Custom/AsyncEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,91 @@ import LiveViewNative
final class AsyncEntity: Entity {
var setupTask: Task<(), Error>? = nil

static var entityCache = [URL:Entity]()

init<E: EntityRegistry, C: ComponentRegistry>(
from element: ElementNode,
in context: EntityContentBuilder<E, C>.Context<some RootRegistry>
) {
) throws {
AsyncEntityComponent.registerComponent()

super.init()

let loadSync = element.attributeBoolean(for: "loadSync")

if let url = element.attributeValue(for: "url").flatMap({ URL(string: $0, relativeTo: context.url) }) {
setupTask = Task { [weak self] in
let (fileURL, _) = try await URLSession.shared.download(from: url)
let correctedExtensionURL = fileURL.deletingPathExtension().appendingPathExtension(for: .usdz)
try FileManager.default.moveItem(at: fileURL, to: correctedExtensionURL)
if let cached = Self.entityCache[url] {
let entity = cached.clone(recursive: true)
entity.components.set(AsyncEntityComponent.remote(url))
self.addChild(entity)
} else if loadSync {
let group = DispatchGroup()
group.enter()

do {
let entity = try await Entity(contentsOf: correctedExtensionURL)
entity.components.set(AsyncEntityComponent.remote(url))
self?.addChild(entity)

try self?.updateResolvedEntity(with: element, in: context)
var fileURL: Result<URL, Error>!

let downloadTask = URLSession.shared.downloadTask(with: url) { result, _, error in
if let result {
fileURL = .success(result)
} else {
fileURL = .failure(error!)
}
group.leave()
}
downloadTask.resume()

try FileManager.default.removeItem(at: correctedExtensionURL)
group.wait()

switch fileURL! {
case .success(let fileURL):
let correctedExtensionURL = fileURL.deletingPathExtension().appendingPathExtension(for: .usdz)
try FileManager.default.moveItem(at: fileURL, to: correctedExtensionURL)

do {
let entity = try Entity.load(contentsOf: correctedExtensionURL)
Self.entityCache[url] = entity.clone(recursive: true)
entity.components.set(AsyncEntityComponent.remote(url))
self.addChild(entity)

try self.updateResolvedEntity(with: element, in: context)
} catch {
try FileManager.default.removeItem(at: correctedExtensionURL)
throw error
}

try FileManager.default.removeItem(at: correctedExtensionURL)
case .failure(let error):
throw error
}
} else {
setupTask = Task { [weak self] in
let (fileURL, _) = try await URLSession.shared.download(from: url)
let correctedExtensionURL = fileURL.deletingPathExtension().appendingPathExtension(for: .usdz)
try FileManager.default.moveItem(at: fileURL, to: correctedExtensionURL)

do {
let entity = try await Entity(contentsOf: correctedExtensionURL)
Self.entityCache[url] = entity.clone(recursive: true)
entity.components.set(AsyncEntityComponent.remote(url))
self?.addChild(entity)

try self?.updateResolvedEntity(with: element, in: context)
} catch {
try FileManager.default.removeItem(at: correctedExtensionURL)
throw error
}
try FileManager.default.removeItem(at: correctedExtensionURL)
}
}
} else if let named = element.attributeValue(for: "named") {
setupTask = Task { [weak self] in
do {
if loadSync {
let entity = try Entity.load(named: named)
entity.components.set(AsyncEntityComponent.named(named))
self.addChild(entity)

try self.updateResolvedEntity(with: element, in: context)
} else {
setupTask = Task { [weak self] in
let entity = try await Entity(named: named)
entity.components.set(AsyncEntityComponent.named(named))
self?.addChild(entity)
Expand Down
2 changes: 1 addition & 1 deletion Sources/LiveViewNativeRealityKit/RealityKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public struct CustomizableRealityKitRegistry<
public static func lookup(_ name: TagName, element: ElementNode) -> some View {
switch name {
case .realityView:
_RealityView<Root, Entities, ComponentContentBuilder<Components>>()
_RealityView<Root, Entities, _ComponentContentBuilder<Components>>()
}
}
}
Expand Down
Loading