@webspatial/platform-visionos 1.7.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/web-spatial/JSBCommand.swift +1 -0
- package/web-spatial/WebMsgCommand.swift +26 -2
- package/web-spatial/WebSpatialApp.swift +26 -8
- package/web-spatial/manifest.swift +1 -1
- package/web-spatial/model/SpatialScene.swift +178 -39
- package/web-spatial/model/Spatialized2DElement.swift +2 -11
- package/web-spatial/model/SpatializedElement.swift +38 -0
- package/web-spatial/model/SpatializedStatic3DElement.swift +27 -8
- package/web-spatial/model/dynamic3d/EntityAnimationManager.swift +356 -0
- package/web-spatial/model/dynamic3d/EntityAnimationSession.swift +162 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationCommand.swift +15 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationManager.swift +263 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationObject.swift +399 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationTypes.swift +45 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationWriteAdapter.swift +65 -0
- package/web-spatial/model/element-motion/SpatializedElementMotionBridgeTypes.swift +43 -0
- package/web-spatial/model/element-motion/SpatializedElementMotionTimelineSampler.swift +99 -0
- package/web-spatial/model/element-motion/SpatializedElementMotionTiming.swift +99 -0
- package/web-spatial/model/element-motion/SpatializedElementMotionTransformTypes.swift +22 -0
- package/web-spatial/protocol/SpatializedElementContainer.swift +1 -1
- package/web-spatial/view/SpatialNavView.swift +37 -12
- package/web-spatial/view/SpatialSceneContentView.swift +9 -12
- package/web-spatial/view/Spatialized2DElementView.swift +10 -17
- package/web-spatial/view/SpatializedDynamic3DView.swift +2 -6
- package/web-spatial/view/SpatializedElementView.swift +3 -2
- package/web-spatial/view/SpatializedStatic3DView.swift +32 -33
- package/web-spatial/view/view-modifier/OrbitModifier.swift +84 -0
- package/web-spatial/webview/SpatialWebController.swift +26 -18
- package/web-spatial/webview/SpatialWebViewModel.swift +12 -7
- package/web-spatial.xcodeproj/project.pbxproj +29 -8
- package/web-spatial.xcodeproj/xcshareddata/xcschemes/web-spatial.xcscheme +1 -1
- package/web-spatialTests/NavigationCleanupTests.swift +2 -1
- package/web-spatialTests/SpatializedElementAnimationManagerTests.swift +1466 -0
- package/web-spatialTests/SpatializedElementMotionTimelineSamplerTests.swift +117 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Spatial
|
|
3
|
+
|
|
4
|
+
struct SpatializedElementAnimationWriteAdapter {
|
|
5
|
+
func currentAffineTransform(for element: SpatializedElement) -> AffineTransform3D {
|
|
6
|
+
element.transform
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
func applyAffine(_ affine: AffineTransform3D, to element: SpatializedElement) {
|
|
10
|
+
element.transform = affine
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
func baselineOpacity(for element: SpatializedElement) -> Double {
|
|
14
|
+
element.opacity
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func applyOpacity(_ opacity: Double, to element: SpatializedElement) {
|
|
18
|
+
element.opacity = opacity
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func acquireMask(on element: SpatializedElement, animationId: String, animatesTransform: Bool, animatesOpacity: Bool) -> Bool {
|
|
22
|
+
guard canAcquireMask(on: element, animationId: animationId, animatesTransform: animatesTransform, animatesOpacity: animatesOpacity) else {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if animatesTransform {
|
|
27
|
+
element.animatingMask.acquire(transform: animationId)
|
|
28
|
+
}
|
|
29
|
+
if animatesOpacity {
|
|
30
|
+
element.animatingMask.acquire(opacity: animationId)
|
|
31
|
+
}
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Checks whether all requested animated fields are free or already owned by this animation.
|
|
36
|
+
private func canAcquireMask(on element: SpatializedElement, animationId: String, animatesTransform: Bool, animatesOpacity: Bool) -> Bool {
|
|
37
|
+
if animatesTransform,
|
|
38
|
+
let owner = element.animatingMask.transformAnimationId,
|
|
39
|
+
owner != animationId
|
|
40
|
+
{
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if animatesOpacity,
|
|
45
|
+
let owner = element.animatingMask.opacityAnimationId,
|
|
46
|
+
owner != animationId
|
|
47
|
+
{
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func releaseMaskAndApplyPending(on element: SpatializedElement, animationId: String) {
|
|
55
|
+
element.animatingMask.release(animationId: animationId)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func shouldAllowTransformWrite(on element: SpatializedElement, animationId: String) -> Bool {
|
|
59
|
+
element.animatingMask.transformAnimationId == nil || element.animatingMask.transformAnimationId == animationId
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func shouldAllowOpacityWrite(on element: SpatializedElement, animationId: String) -> Bool {
|
|
63
|
+
element.animatingMask.opacityAnimationId == nil || element.animatingMask.opacityAnimationId == animationId
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// MARK: - Timeline wire types (matches JS SpatializedMotionTimeline)
|
|
4
|
+
|
|
5
|
+
struct SpatializedMotionKeyframePayload: Decodable {
|
|
6
|
+
let at: Double
|
|
7
|
+
let value: Double
|
|
8
|
+
let timingFunction: String?
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
struct SpatializedMotionTrackPayload: Decodable {
|
|
12
|
+
let property: String
|
|
13
|
+
let keyframes: [SpatializedMotionKeyframePayload]
|
|
14
|
+
let timingFunction: String?
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
struct SpatializedMotionTimelinePayload: Decodable {
|
|
18
|
+
let duration: Double
|
|
19
|
+
let delay: Double?
|
|
20
|
+
let playbackRate: Double?
|
|
21
|
+
let loop: SpatializedMotionLoopConfig?
|
|
22
|
+
let tracks: [SpatializedMotionTrackPayload]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// MARK: - Event Payloads
|
|
26
|
+
|
|
27
|
+
/// Payload structure matching the JS-side spatialized motion values.
|
|
28
|
+
struct SpatializedMotionValuesPayload: Encodable {
|
|
29
|
+
let transform: SpatializedMotionTransformPayload?
|
|
30
|
+
let opacity: Double?
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
struct SpatializedMotionTransformPayload: Encodable {
|
|
34
|
+
let translate: Vec3Payload?
|
|
35
|
+
let rotate: Vec3Payload?
|
|
36
|
+
let scale: Vec3Payload?
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
struct Vec3Payload: Encodable {
|
|
40
|
+
let x: Double?
|
|
41
|
+
let y: Double?
|
|
42
|
+
let z: Double?
|
|
43
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// MARK: - Timeline sampler (parity with Web evaluateMotionTimeline)
|
|
4
|
+
|
|
5
|
+
/// Per-track timeline sampling for native spatialized element motion.
|
|
6
|
+
final class SpatializedElementMotionTimelineSampler {
|
|
7
|
+
let duration: Double
|
|
8
|
+
private let tracks: [SpatializedMotionTrackPayload]
|
|
9
|
+
private let baselineTransform: SpatializedMotionTransformComponents
|
|
10
|
+
private let baselineOpacity: Double
|
|
11
|
+
|
|
12
|
+
private(set) var animatesTransform = false
|
|
13
|
+
private(set) var animatesOpacity = false
|
|
14
|
+
|
|
15
|
+
init(timeline: SpatializedMotionTimelinePayload, baselineTransform: SpatializedMotionTransformComponents, baselineOpacity: Double) {
|
|
16
|
+
duration = timeline.duration
|
|
17
|
+
tracks = timeline.tracks
|
|
18
|
+
self.baselineTransform = baselineTransform
|
|
19
|
+
self.baselineOpacity = baselineOpacity
|
|
20
|
+
for track in tracks {
|
|
21
|
+
if track.property == "opacity" {
|
|
22
|
+
animatesOpacity = true
|
|
23
|
+
}
|
|
24
|
+
if track.property.hasPrefix("transform.") {
|
|
25
|
+
animatesTransform = true
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func sampleTransformAndOpacity(at timeSec: Double) -> (transform: SpatializedMotionTransformComponents, opacity: Double) {
|
|
31
|
+
let t = min(max(timeSec, 0), duration)
|
|
32
|
+
var transform = baselineTransform
|
|
33
|
+
var opacity = baselineOpacity
|
|
34
|
+
|
|
35
|
+
for track in tracks {
|
|
36
|
+
let value = sampleTrack(track, at: t)
|
|
37
|
+
applyScalar(property: track.property, value: value, transform: &transform, opacity: &opacity)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (transform, opacity)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private func sampleTrack(_ track: SpatializedMotionTrackPayload, at timeSec: Double) -> Double {
|
|
44
|
+
let frames = track.keyframes.sorted { $0.at < $1.at }
|
|
45
|
+
guard let first = frames.first else { return 0 }
|
|
46
|
+
guard let last = frames.last else { return 0 }
|
|
47
|
+
|
|
48
|
+
if timeSec <= first.at { return first.value }
|
|
49
|
+
if timeSec >= last.at { return last.value }
|
|
50
|
+
|
|
51
|
+
for i in 0 ..< (frames.count - 1) {
|
|
52
|
+
let a = frames[i]
|
|
53
|
+
let b = frames[i + 1]
|
|
54
|
+
if timeSec >= a.at, timeSec <= b.at {
|
|
55
|
+
let span = b.at - a.at
|
|
56
|
+
if span <= 0 { return b.value }
|
|
57
|
+
let linear = (timeSec - a.at) / span
|
|
58
|
+
let eased = SpatializedMotionTimingFunction
|
|
59
|
+
.from(name: a.timingFunction ?? track.timingFunction ?? "linear")
|
|
60
|
+
.evaluate(linear)
|
|
61
|
+
return a.value + (b.value - a.value) * eased
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return last.value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private func applyScalar(
|
|
69
|
+
property: String,
|
|
70
|
+
value: Double,
|
|
71
|
+
transform: inout SpatializedMotionTransformComponents,
|
|
72
|
+
opacity: inout Double
|
|
73
|
+
) {
|
|
74
|
+
switch property {
|
|
75
|
+
case "opacity":
|
|
76
|
+
opacity = value
|
|
77
|
+
case "transform.translate.x":
|
|
78
|
+
transform.translateX = value
|
|
79
|
+
case "transform.translate.y":
|
|
80
|
+
transform.translateY = value
|
|
81
|
+
case "transform.translate.z":
|
|
82
|
+
transform.translateZ = value
|
|
83
|
+
case "transform.rotate.x":
|
|
84
|
+
transform.rotateX = value
|
|
85
|
+
case "transform.rotate.y":
|
|
86
|
+
transform.rotateY = value
|
|
87
|
+
case "transform.rotate.z":
|
|
88
|
+
transform.rotateZ = value
|
|
89
|
+
case "transform.scale.x":
|
|
90
|
+
transform.scaleX = value
|
|
91
|
+
case "transform.scale.y":
|
|
92
|
+
transform.scaleY = value
|
|
93
|
+
case "transform.scale.z":
|
|
94
|
+
transform.scaleZ = value
|
|
95
|
+
default:
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// MARK: - Loop Configuration
|
|
4
|
+
|
|
5
|
+
/// Loop configuration decoded from JS bridge.
|
|
6
|
+
/// Supports: true (reset loop), { reverse: true } (reverse loop), false/nil (play once).
|
|
7
|
+
enum SpatializedMotionLoopConfig: Decodable {
|
|
8
|
+
case none
|
|
9
|
+
case resetLoop
|
|
10
|
+
case reverseLoop
|
|
11
|
+
|
|
12
|
+
init(from decoder: Decoder) throws {
|
|
13
|
+
let container = try decoder.singleValueContainer()
|
|
14
|
+
|
|
15
|
+
// Try bool first
|
|
16
|
+
if let boolVal = try? container.decode(Bool.self) {
|
|
17
|
+
self = boolVal ? .resetLoop : .none
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Try object { reverse?: Bool }
|
|
22
|
+
struct LoopObject: Decodable {
|
|
23
|
+
let reverse: Bool?
|
|
24
|
+
}
|
|
25
|
+
if let obj = try? container.decode(LoopObject.self) {
|
|
26
|
+
self = (obj.reverse == true) ? .reverseLoop : .resetLoop
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
self = .none
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// MARK: - Timing Functions
|
|
35
|
+
|
|
36
|
+
/// Cubic bezier timing functions for manual frame interpolation.
|
|
37
|
+
enum SpatializedMotionTimingFunction {
|
|
38
|
+
case linear
|
|
39
|
+
case easeIn
|
|
40
|
+
case easeOut
|
|
41
|
+
case easeInOut
|
|
42
|
+
|
|
43
|
+
/// Evaluate the timing function at progress t (0...1) -> output (0...1)
|
|
44
|
+
func evaluate(_ t: Double) -> Double {
|
|
45
|
+
switch self {
|
|
46
|
+
case .linear:
|
|
47
|
+
return t
|
|
48
|
+
case .easeIn:
|
|
49
|
+
// cubic-bezier(0.42, 0, 1, 1)
|
|
50
|
+
return cubicBezier(t, x1: 0.42, y1: 0.0, x2: 1.0, y2: 1.0)
|
|
51
|
+
case .easeOut:
|
|
52
|
+
// cubic-bezier(0, 0, 0.58, 1)
|
|
53
|
+
return cubicBezier(t, x1: 0.0, y1: 0.0, x2: 0.58, y2: 1.0)
|
|
54
|
+
case .easeInOut:
|
|
55
|
+
// cubic-bezier(0.42, 0, 0.58, 1)
|
|
56
|
+
return cubicBezier(t, x1: 0.42, y1: 0.0, x2: 0.58, y2: 1.0)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Solve cubic bezier curve for Y given input T (time fraction).
|
|
61
|
+
private func cubicBezier(_ t: Double, x1: Double, y1: Double, x2: Double, y2: Double) -> Double {
|
|
62
|
+
if x1 == 0 && y1 == 0 && x2 == 1 && y2 == 1 {
|
|
63
|
+
return t
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Newton-Raphson to solve for bezier parameter given x = t
|
|
67
|
+
var guessT = t
|
|
68
|
+
for _ in 0 ..< 8 {
|
|
69
|
+
let currentX = sampleCurveX(guessT, x1: x1, x2: x2)
|
|
70
|
+
let derivative = sampleCurveDerivativeX(guessT, x1: x1, x2: x2)
|
|
71
|
+
if abs(derivative) < 1e-6 { break }
|
|
72
|
+
guessT -= (currentX - t) / derivative
|
|
73
|
+
}
|
|
74
|
+
guessT = max(0.0, min(1.0, guessT))
|
|
75
|
+
return sampleCurveY(guessT, y1: y1, y2: y2)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private func sampleCurveX(_ t: Double, x1: Double, x2: Double) -> Double {
|
|
79
|
+
return ((1.0 - 3.0 * x2 + 3.0 * x1) * t + (3.0 * x2 - 6.0 * x1)) * t * t + 3.0 * x1 * t
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private func sampleCurveY(_ t: Double, y1: Double, y2: Double) -> Double {
|
|
83
|
+
return ((1.0 - 3.0 * y2 + 3.0 * y1) * t + (3.0 * y2 - 6.0 * y1)) * t * t + 3.0 * y1 * t
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private func sampleCurveDerivativeX(_ t: Double, x1: Double, x2: Double) -> Double {
|
|
87
|
+
return (3.0 * (1.0 - 3.0 * x2 + 3.0 * x1) * t + 2.0 * (3.0 * x2 - 6.0 * x1)) * t + 3.0 * x1
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static func from(name: String) -> SpatializedMotionTimingFunction {
|
|
91
|
+
switch name {
|
|
92
|
+
case "linear": return .linear
|
|
93
|
+
case "easeIn": return .easeIn
|
|
94
|
+
case "easeOut": return .easeOut
|
|
95
|
+
case "easeInOut": return .easeInOut
|
|
96
|
+
default: return .easeInOut
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// MARK: - Transform Components
|
|
4
|
+
|
|
5
|
+
/// Fully resolved transform components used for interpolation and composition.
|
|
6
|
+
struct SpatializedMotionTransformComponents {
|
|
7
|
+
var translateX: Double
|
|
8
|
+
var translateY: Double
|
|
9
|
+
var translateZ: Double
|
|
10
|
+
var rotateX: Double // degrees
|
|
11
|
+
var rotateY: Double // degrees
|
|
12
|
+
var rotateZ: Double // degrees
|
|
13
|
+
var scaleX: Double
|
|
14
|
+
var scaleY: Double
|
|
15
|
+
var scaleZ: Double
|
|
16
|
+
|
|
17
|
+
static let identity = SpatializedMotionTransformComponents(
|
|
18
|
+
translateX: 0, translateY: 0, translateZ: 0,
|
|
19
|
+
rotateX: 0, rotateY: 0, rotateZ: 0,
|
|
20
|
+
scaleX: 1, scaleY: 1, scaleZ: 1
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -4,5 +4,5 @@ protocol SpatializedElementContainer {
|
|
|
4
4
|
func addChild(_ spatializedElement: SpatializedElement)
|
|
5
5
|
func removeChild(_ spatializedElement: SpatializedElement)
|
|
6
6
|
func getChildren() -> [String: SpatializedElement]
|
|
7
|
-
func
|
|
7
|
+
func getChildren<T: SpatializedElement>(ofType type: T.Type) -> [T]
|
|
8
8
|
}
|
|
@@ -76,13 +76,11 @@ struct SpatialNavView: View {
|
|
|
76
76
|
@State private var showEnter: Double = 1
|
|
77
77
|
@State private var showNav: Double = 0
|
|
78
78
|
@State private var showUrl: Double = 0
|
|
79
|
+
@State private var canGoBack = false
|
|
80
|
+
@State private var canGoForward = false
|
|
81
|
+
@State private var navigationStateListenerID: UUID?
|
|
79
82
|
@Namespace var hoverNamespace
|
|
80
83
|
|
|
81
|
-
func checkButtonState() {
|
|
82
|
-
canGoBack = model!.getController().webview!.canGoBack
|
|
83
|
-
canGoForward = model!.getController().webview!.canGoForward
|
|
84
|
-
}
|
|
85
|
-
|
|
86
84
|
func goBack() {
|
|
87
85
|
spatialScene.resetForNavigation()
|
|
88
86
|
model?.getController().webview?.goBack()
|
|
@@ -107,9 +105,30 @@ struct SpatialNavView: View {
|
|
|
107
105
|
return model?.getController().webview?.url
|
|
108
106
|
}
|
|
109
107
|
|
|
110
|
-
|
|
108
|
+
func checkButtonState() {
|
|
109
|
+
canGoBack = model?.getController().webview?.canGoBack ?? false
|
|
110
|
+
canGoForward = model?.getController().webview?.canGoForward ?? false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func registerNavigationStateListener() {
|
|
114
|
+
guard navigationStateListenerID == nil else { return }
|
|
111
115
|
|
|
112
|
-
|
|
116
|
+
let listener: (SpatialWebViewState) -> Void = { state in
|
|
117
|
+
guard case .didUpdateNavigationState = state else { return }
|
|
118
|
+
Task { @MainActor in
|
|
119
|
+
self.checkButtonState()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
navigationStateListenerID = model?.addStateListener(listener)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func unregisterNavigationStateListener() {
|
|
127
|
+
guard let navigationStateListenerID else { return }
|
|
128
|
+
|
|
129
|
+
model?.removeStateListener(navigationStateListenerID)
|
|
130
|
+
self.navigationStateListenerID = nil
|
|
131
|
+
}
|
|
113
132
|
|
|
114
133
|
var navHoverGroup: HoverEffectGroup {
|
|
115
134
|
HoverEffectGroup(hoverNamespace)
|
|
@@ -128,6 +147,7 @@ struct SpatialNavView: View {
|
|
|
128
147
|
children: Image("arrow_left"),
|
|
129
148
|
clearBackGround: true
|
|
130
149
|
)
|
|
150
|
+
.opacity(self.canGoBack ? 1 : 0.5)
|
|
131
151
|
.disabled(!(self.canGoBack))
|
|
132
152
|
NavButton(
|
|
133
153
|
action: { self.goForward()
|
|
@@ -135,7 +155,8 @@ struct SpatialNavView: View {
|
|
|
135
155
|
children: Image("arrow_right"),
|
|
136
156
|
clearBackGround: true
|
|
137
157
|
)
|
|
138
|
-
.
|
|
158
|
+
.opacity(self.canGoForward ? 1 : 0.5)
|
|
159
|
+
.disabled(!(self.canGoForward))
|
|
139
160
|
NavButton(action: { self.reload() }, children: Image("refresh"), clearBackGround: true)
|
|
140
161
|
NavDivider()
|
|
141
162
|
}
|
|
@@ -160,13 +181,15 @@ struct SpatialNavView: View {
|
|
|
160
181
|
},
|
|
161
182
|
children: Image("arrow_left")
|
|
162
183
|
)
|
|
184
|
+
.opacity(self.canGoBack ? 1 : 0.5)
|
|
163
185
|
.disabled(!(self.canGoBack))
|
|
164
186
|
NavButton(
|
|
165
187
|
action: { self.goForward()
|
|
166
188
|
},
|
|
167
189
|
children: Image("arrow_right")
|
|
168
190
|
)
|
|
169
|
-
.
|
|
191
|
+
.opacity(self.canGoForward ? 1 : 0.5)
|
|
192
|
+
.disabled(!(self.canGoForward))
|
|
170
193
|
NavButton(action: { self.reload() }, children: Image("refresh"))
|
|
171
194
|
NavDivider()
|
|
172
195
|
}
|
|
@@ -238,9 +261,11 @@ struct SpatialNavView: View {
|
|
|
238
261
|
.opacity(showUrl)
|
|
239
262
|
}
|
|
240
263
|
.onAppear {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
264
|
+
checkButtonState()
|
|
265
|
+
registerNavigationStateListener()
|
|
266
|
+
}
|
|
267
|
+
.onDisappear {
|
|
268
|
+
unregisterNavigationStateListener()
|
|
244
269
|
}
|
|
245
270
|
}
|
|
246
271
|
|
|
@@ -32,31 +32,28 @@ struct SpatialSceneContentView: View {
|
|
|
32
32
|
.frame(width: width, height: height)
|
|
33
33
|
.offset(z: -1)
|
|
34
34
|
|
|
35
|
-
let childrenOfSpatialized2DElement
|
|
35
|
+
let childrenOfSpatialized2DElement = spatialScene.getChildren(ofType: Spatialized2DElement.self)
|
|
36
36
|
|
|
37
37
|
ForEach(childrenOfSpatialized2DElement, id: \.id) { child in
|
|
38
|
-
SpatializedElementView(parentScrollOffset: spatialScene.scrollOffset) {
|
|
39
|
-
Spatialized2DElementView()
|
|
38
|
+
SpatializedElementView(parentScrollOffset: spatialScene.scrollOffset, spatializedElement: child) {
|
|
39
|
+
Spatialized2DElementView(spatialized2DElement: child)
|
|
40
40
|
}
|
|
41
|
-
.environment(child)
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
let childrenOfSpatializedStatic3DElement
|
|
43
|
+
let childrenOfSpatializedStatic3DElement = spatialScene.getChildren(ofType: SpatializedStatic3DElement.self)
|
|
45
44
|
|
|
46
45
|
ForEach(childrenOfSpatializedStatic3DElement, id: \.id) { child in
|
|
47
|
-
SpatializedElementView(parentScrollOffset: spatialScene.scrollOffset) {
|
|
48
|
-
SpatializedStatic3DView()
|
|
46
|
+
SpatializedElementView(parentScrollOffset: spatialScene.scrollOffset, spatializedElement: child) {
|
|
47
|
+
SpatializedStatic3DView(spatializedStatic3DElement: child)
|
|
49
48
|
}
|
|
50
|
-
.environment(child)
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
let childrenOfSpatializedDynamic3DElement
|
|
51
|
+
let childrenOfSpatializedDynamic3DElement = spatialScene.getChildren(ofType: SpatializedDynamic3DElement.self)
|
|
54
52
|
|
|
55
53
|
ForEach(childrenOfSpatializedDynamic3DElement, id: \.id) { child in
|
|
56
|
-
SpatializedElementView(parentScrollOffset: spatialScene.scrollOffset) {
|
|
57
|
-
SpatializedDynamic3DView()
|
|
54
|
+
SpatializedElementView(parentScrollOffset: spatialScene.scrollOffset, spatializedElement: child) {
|
|
55
|
+
SpatializedDynamic3DView(spatializedDynamic3DElement: child)
|
|
58
56
|
}
|
|
59
|
-
.environment(child)
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
59
|
}
|
|
@@ -19,13 +19,9 @@ class Spatialized2DViewGestureData {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
struct Spatialized2DElementView: View {
|
|
22
|
-
|
|
22
|
+
let spatialized2DElement: Spatialized2DElement
|
|
23
23
|
@Environment(SpatialScene.self) var spatialScene: SpatialScene
|
|
24
24
|
|
|
25
|
-
private var spatialized2DElement: Spatialized2DElement {
|
|
26
|
-
return spatializedElement as! Spatialized2DElement
|
|
27
|
-
}
|
|
28
|
-
|
|
29
25
|
@State private var gestureData = Spatialized2DViewGestureData()
|
|
30
26
|
|
|
31
27
|
var body: some View {
|
|
@@ -40,30 +36,27 @@ struct Spatialized2DElementView: View {
|
|
|
40
36
|
)
|
|
41
37
|
.simultaneousGesture(spatialized2DElement.scrollPageEnabled ? dragWebGesture : nil)
|
|
42
38
|
|
|
43
|
-
let childrenOfSpatialized2DElement
|
|
39
|
+
let childrenOfSpatialized2DElement = spatialized2DElement.getChildren(ofType: Spatialized2DElement.self)
|
|
44
40
|
|
|
45
41
|
ForEach(childrenOfSpatialized2DElement, id: \.id) { child in
|
|
46
|
-
SpatializedElementView(parentScrollOffset: spatialized2DElement.scrollOffset) {
|
|
47
|
-
Spatialized2DElementView()
|
|
42
|
+
SpatializedElementView(parentScrollOffset: spatialized2DElement.scrollOffset, spatializedElement: child) {
|
|
43
|
+
Spatialized2DElementView(spatialized2DElement: child)
|
|
48
44
|
}
|
|
49
|
-
.environment(child)
|
|
50
45
|
}
|
|
51
46
|
|
|
52
|
-
let childrenOfSpatializedStatic3DElement
|
|
47
|
+
let childrenOfSpatializedStatic3DElement = spatialized2DElement.getChildren(ofType: SpatializedStatic3DElement.self)
|
|
53
48
|
ForEach(childrenOfSpatializedStatic3DElement, id: \.id) { child in
|
|
54
|
-
SpatializedElementView(parentScrollOffset: spatialized2DElement.scrollOffset) {
|
|
55
|
-
SpatializedStatic3DView()
|
|
49
|
+
SpatializedElementView(parentScrollOffset: spatialized2DElement.scrollOffset, spatializedElement: child) {
|
|
50
|
+
SpatializedStatic3DView(spatializedStatic3DElement: child)
|
|
56
51
|
}
|
|
57
|
-
.environment(child)
|
|
58
52
|
}
|
|
59
53
|
|
|
60
|
-
let childrenOfSpatializedDynamic3DElement
|
|
54
|
+
let childrenOfSpatializedDynamic3DElement = spatialized2DElement.getChildren(ofType: SpatializedDynamic3DElement.self)
|
|
61
55
|
|
|
62
56
|
ForEach(childrenOfSpatializedDynamic3DElement, id: \.id) { child in
|
|
63
|
-
SpatializedElementView(parentScrollOffset: spatialized2DElement.scrollOffset) {
|
|
64
|
-
SpatializedDynamic3DView()
|
|
57
|
+
SpatializedElementView(parentScrollOffset: spatialized2DElement.scrollOffset, spatializedElement: child) {
|
|
58
|
+
SpatializedDynamic3DView(spatializedDynamic3DElement: child)
|
|
65
59
|
}
|
|
66
|
-
.environment(child)
|
|
67
60
|
}
|
|
68
61
|
}
|
|
69
62
|
}
|
|
@@ -2,16 +2,12 @@ import RealityKit
|
|
|
2
2
|
import SwiftUI
|
|
3
3
|
|
|
4
4
|
struct SpatializedDynamic3DView: View {
|
|
5
|
-
|
|
5
|
+
let spatializedDynamic3DElement: SpatializedDynamic3DElement
|
|
6
6
|
@Environment(SpatialScene.self) var spatialScene: SpatialScene
|
|
7
7
|
@State private var isDrag = false
|
|
8
8
|
@State private var isRotate = false
|
|
9
9
|
@State private var isScale = false
|
|
10
10
|
|
|
11
|
-
private var spatializedDynamic3DElement: SpatializedDynamic3DElement {
|
|
12
|
-
return spatializedElement as! SpatializedDynamic3DElement
|
|
13
|
-
}
|
|
14
|
-
|
|
15
11
|
var spatialTapEvent: some Gesture {
|
|
16
12
|
SpatialTapGesture(count: 1).targetedToAnyEntity()
|
|
17
13
|
.onEnded { value in
|
|
@@ -63,7 +59,7 @@ struct SpatializedDynamic3DView: View {
|
|
|
63
59
|
}
|
|
64
60
|
|
|
65
61
|
private func makeRotateGesture3D() -> RotateGesture3D {
|
|
66
|
-
guard let raw =
|
|
62
|
+
guard let raw = spatializedDynamic3DElement.rotateConstrainedToAxis else {
|
|
67
63
|
return RotateGesture3D()
|
|
68
64
|
}
|
|
69
65
|
let dx = Double(raw.x)
|
|
@@ -7,7 +7,7 @@ final class GestureState {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
struct SpatializedElementView<Content: View>: View {
|
|
10
|
-
|
|
10
|
+
let spatializedElement: SpatializedElement
|
|
11
11
|
@Environment(SpatialScene.self) var spatialScene: SpatialScene
|
|
12
12
|
|
|
13
13
|
var parentScrollOffset: Vec2
|
|
@@ -15,8 +15,9 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
15
15
|
|
|
16
16
|
@State private var gestureState = GestureState()
|
|
17
17
|
|
|
18
|
-
init(parentScrollOffset: Vec2, @ViewBuilder content: () -> Content) {
|
|
18
|
+
init(parentScrollOffset: Vec2, spatializedElement: SpatializedElement, @ViewBuilder content: () -> Content) {
|
|
19
19
|
self.parentScrollOffset = parentScrollOffset
|
|
20
|
+
self.spatializedElement = spatializedElement
|
|
20
21
|
self.content = content()
|
|
21
22
|
}
|
|
22
23
|
|