diff --git a/www/symbols/sparkles.square.filled.on.square.purple.svg b/www/symbols/sparkles.square.filled.on.square.svg
similarity index 94%
rename from www/symbols/sparkles.square.filled.on.square.purple.svg
rename to www/symbols/sparkles.square.filled.on.square.svg
index e29b7952..27f60c8c 100644
--- a/www/symbols/sparkles.square.filled.on.square.purple.svg
+++ b/www/symbols/sparkles.square.filled.on.square.svg
@@ -6,7 +6,7 @@ PUBLIC "-//W3C//DTD SVG 1.1//EN"
diff --git a/www/xr.css b/www/xr.css
deleted file mode 100644
index bd38972c..00000000
--- a/www/xr.css
+++ /dev/null
@@ -1,16 +0,0 @@
-@media (prefers-color-scheme: dark) {
- body {
- background-color: #222;
- }
-}
-
-body {
- text-align: center;
-}
-
-img {
- position: absolute;
- left: 0;
- bottom: 0;
- width: 100%;
-}
diff --git a/www/xr.html b/www/xr.html
deleted file mode 100644
index 38788bcd..00000000
--- a/www/xr.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
XR — AdvantageScope
-
-
-
AdvantageScope XR is coming soon!
-
docs.advantagescope.org/xr
-
-
-
diff --git a/www/xr.webp b/www/xr.webp
deleted file mode 100644
index 00a9e332..00000000
Binary files a/www/xr.webp and /dev/null differ
diff --git a/www/xrClient.css b/www/xrClient.css
new file mode 100644
index 00000000..c315ce18
--- /dev/null
+++ b/www/xrClient.css
@@ -0,0 +1,107 @@
+body {
+ margin: 0;
+ overflow: hidden;
+ background-color: transparent;
+}
+
+div.container {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100vw;
+ height: 100vh;
+ border: none;
+}
+
+canvas {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+}
+
+/* Source: https://freecodez.com/post/7pt17rk */
+
+div.spinner-cubes-container {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%) scale(50%);
+ user-select: none;
+ transition: all 0.1s ease-in-out;
+ opacity: 0;
+}
+
+div.spinner-cubes-container.visible {
+ transform: translate(-50%, -50%) scale(100%);
+ opacity: 1;
+}
+
+div.spinner-cubes {
+ width: 44px;
+ height: 44px;
+ transform-style: preserve-3d;
+}
+
+div.spinner-cubes-container.animating div.spinner-cubes {
+ animation: spinner-cubes 6s infinite ease-in-out;
+}
+
+div.spinner-cubes > div {
+ height: 100%;
+ position: absolute;
+ width: 100%;
+ background-color: #00aaff33;
+ border: 2px solid #00aaff;
+ box-sizing: border-box;
+}
+
+div.spinner-cubes div:nth-of-type(1) {
+ transform: translateZ(-22px) rotateY(180deg);
+}
+
+div.spinner-cubes div:nth-of-type(2) {
+ transform: rotateY(-270deg) translateX(50%);
+ transform-origin: top right;
+}
+
+div.spinner-cubes div:nth-of-type(3) {
+ transform: rotateY(270deg) translateX(-50%);
+ transform-origin: center left;
+}
+
+div.spinner-cubes div:nth-of-type(4) {
+ transform: rotateX(90deg) translateY(-50%);
+ transform-origin: top center;
+}
+
+div.spinner-cubes div:nth-of-type(5) {
+ transform: rotateX(-90deg) translateY(50%);
+ transform-origin: bottom center;
+}
+
+div.spinner-cubes div:nth-of-type(6) {
+ transform: translateZ(22px);
+}
+
+@keyframes spinner-cubes {
+ 0% {
+ transform: rotate(0deg) rotateX(-35deg) rotateY(45deg);
+ }
+
+ 25% {
+ transform: rotate(180deg) rotateX(-215deg) rotateY(-135deg);
+ }
+
+ 50% {
+ transform: rotate(0deg) rotateX(-35deg) rotateY(45deg);
+ }
+
+ 75% {
+ transform: rotate(180deg) rotateX(145deg) rotateY(225deg);
+ }
+
+ 100% {
+ transform: rotate(0deg) rotateX(-35deg) rotateY(45deg);
+ }
+}
diff --git a/www/xrClient.html b/www/xrClient.html
new file mode 100644
index 00000000..b3b58fc1
--- /dev/null
+++ b/www/xrClient.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
XR Client — AdvantageScope XR
+
+
+
+
+
+
+
+
diff --git a/www/xrControls.css b/www/xrControls.css
new file mode 100644
index 00000000..e9f70877
--- /dev/null
+++ b/www/xrControls.css
@@ -0,0 +1,77 @@
+body {
+ overflow: hidden;
+}
+
+@media (prefers-color-scheme: dark) {
+ body {
+ background-color: #222;
+ }
+}
+
+div.qr-container {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ height: 100%;
+ max-width: 50%;
+ aspect-ratio: 1;
+ background-color: #f3f3f3;
+}
+
+@media (prefers-color-scheme: dark) {
+ div.qr-container {
+ background-color: #1b1b1b;
+ }
+}
+
+div.controls {
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ height: 100%;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+div.controls > div {
+ text-align: center;
+ padding: 10px 15px 10px 15px;
+}
+
+div.controls > div.title {
+ font-weight: bold;
+ font-size: 8.5vh;
+}
+
+div.controls > div.details {
+ font-size: 4.5vh;
+ font-style: italic;
+}
+
+table {
+ width: 100%;
+ table-layout: fixed;
+}
+
+td {
+ text-align: center;
+ font-size: 5vh;
+ font-weight: lighter;
+ padding-bottom: 8px;
+}
+
+td.right {
+ text-align: right;
+}
+
+select {
+ font-size: 5vh;
+ width: calc(100% - 16px);
+ margin: 4px;
+}
+
+input[type="checkbox"] {
+ width: 30px;
+ height: 30px;
+ filter: saturate(0%);
+}
diff --git a/www/xrControls.html b/www/xrControls.html
new file mode 100644
index 00000000..c0bbc593
--- /dev/null
+++ b/www/xrControls.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
XR Controls — AdvantageScope
+
+
+
+
+
Scan the code to start AdvantageScope XR
+
+ Requires an iPhone or iPad running iOS 16 or later. Devices must be connected to the same Wi-Fi network. Check
+ the documentation for details.
+
+
+
+
+
diff --git a/xr/AdvantageScopeXR.xcodeproj/project.pbxproj b/xr/AdvantageScopeXR.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..b7cdda7b
--- /dev/null
+++ b/xr/AdvantageScopeXR.xcodeproj/project.pbxproj
@@ -0,0 +1,700 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 70;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 8C103A852D093F9900FAE4C0 /* ARRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C103A822D093F9900FAE4C0 /* ARRenderer.swift */; };
+ 8C103A862D093F9900FAE4C0 /* ARShaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 8C103A832D093F9900FAE4C0 /* ARShaders.metal */; };
+ 8C1672E62D0BB126004100D8 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C1672E52D0BB125004100D8 /* Constants.swift */; };
+ 8C1A9EB92C11695700DCF1E3 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 8C1A9EB82C11695700DCF1E3 /* Starscream */; };
+ 8C2126132D05014700C92898 /* WebOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2126122D05014700C92898 /* WebOverlay.swift */; };
+ 8C2759A22C0995BD0067FD80 /* ControlsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2759A12C0995BD0067FD80 /* ControlsMenu.swift */; };
+ 8C2759A42C09960A0067FD80 /* Banner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2759A32C09960A0067FD80 /* Banner.swift */; };
+ 8C28075D2D07CD35008F4721 /* ARManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C28075C2D07CD33008F4721 /* ARManager.swift */; };
+ 8C362AC42C0B600400AD3091 /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C362AC32C0B600400AD3091 /* Networking.swift */; };
+ 8C6767DB2C11551C00277484 /* QRScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C6767DA2C11551C00277484 /* QRScanner.swift */; };
+ 8CBC9A722D140E6A000EFB70 /* AdvantageScopeXR.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CBC9A712D140E68000EFB70 /* AdvantageScopeXR.swift */; };
+ 8CBC9A732D140E6A000EFB70 /* AdvantageScopeXR.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CBC9A712D140E68000EFB70 /* AdvantageScopeXR.swift */; };
+ 8CBC9A742D14B355000EFB70 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8CBFA6DF2BF8663E009AA8CD /* Assets.xcassets */; };
+ 8CBFA6DE2BF8663D009AA8CD /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CBFA6DD2BF8663D009AA8CD /* ContentView.swift */; };
+ 8CBFA6E02BF8663E009AA8CD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8CBFA6DF2BF8663E009AA8CD /* Assets.xcassets */; };
+ 8CCB36702D0F8A0900F516D8 /* RecordButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CCB366F2D0F8A0600F516D8 /* RecordButton.swift */; };
+ 8CF342672D14065200291788 /* AdvantageScopeXRClip.app in Embed App Clips */ = {isa = PBXBuildFile; fileRef = 8CF342582D14065100291788 /* AdvantageScopeXRClip.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ 8CF3426E2D1406BF00291788 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 8CF3426D2D1406BF00291788 /* Starscream */; };
+ 8CF342702D1408E200291788 /* RecordButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CCB366F2D0F8A0600F516D8 /* RecordButton.swift */; };
+ 8CF342712D1408E700291788 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C1672E52D0BB125004100D8 /* Constants.swift */; };
+ 8CF342722D1408F000291788 /* ARManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C28075C2D07CD33008F4721 /* ARManager.swift */; };
+ 8CF342732D1408F400291788 /* ARRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C103A822D093F9900FAE4C0 /* ARRenderer.swift */; };
+ 8CF342742D1408F700291788 /* ARShaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 8C103A832D093F9900FAE4C0 /* ARShaders.metal */; };
+ 8CF342752D14090400291788 /* WebOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2126122D05014700C92898 /* WebOverlay.swift */; };
+ 8CF342762D14090900291788 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CBFA6DD2BF8663D009AA8CD /* ContentView.swift */; };
+ 8CF342772D14090C00291788 /* ControlsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2759A12C0995BD0067FD80 /* ControlsMenu.swift */; };
+ 8CF342782D14091000291788 /* Banner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2759A32C09960A0067FD80 /* Banner.swift */; };
+ 8CF342792D14091300291788 /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C362AC32C0B600400AD3091 /* Networking.swift */; };
+ 8CF3427A2D14091700291788 /* QRScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C6767DA2C11551C00277484 /* QRScanner.swift */; };
+ 8CF3427D2D140A1200291788 /* ARShaderTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 8C103A842D093F9900FAE4C0 /* ARShaderTypes.h */; };
+ 8CF3427E2D140A1200291788 /* ARShaderTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 8C103A842D093F9900FAE4C0 /* ARShaderTypes.h */; };
+ 8CF3427F2D140A1600291788 /* AdvantageScopeXR-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 8C103A8A2D093FD200FAE4C0 /* AdvantageScopeXR-Bridging-Header.h */; };
+ 8CF342802D140A1600291788 /* AdvantageScopeXR-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 8C103A8A2D093FD200FAE4C0 /* AdvantageScopeXR-Bridging-Header.h */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 8CF342652D14065200291788 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 8CBFA6D02BF8663D009AA8CD /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 8CF342572D14065100291788;
+ remoteInfo = AdvantageScopeXRClip;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 8CB5DDE62C11677F00D9B341 /* Embed App Clips */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "$(CONTENTS_FOLDER_PATH)/AppClips";
+ dstSubfolderSpec = 16;
+ files = (
+ 8CF342672D14065200291788 /* AdvantageScopeXRClip.app in Embed App Clips */,
+ );
+ name = "Embed App Clips";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 8C103A822D093F9900FAE4C0 /* ARRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARRenderer.swift; sourceTree = "
"; };
+ 8C103A832D093F9900FAE4C0 /* ARShaders.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = ARShaders.metal; sourceTree = ""; };
+ 8C103A842D093F9900FAE4C0 /* ARShaderTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARShaderTypes.h; sourceTree = ""; };
+ 8C103A8A2D093FD200FAE4C0 /* AdvantageScopeXR-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AdvantageScopeXR-Bridging-Header.h"; sourceTree = ""; };
+ 8C1672E52D0BB125004100D8 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; };
+ 8C2126122D05014700C92898 /* WebOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebOverlay.swift; sourceTree = ""; };
+ 8C2126142D05044700C92898 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
+ 8C2759A12C0995BD0067FD80 /* ControlsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsMenu.swift; sourceTree = ""; };
+ 8C2759A32C09960A0067FD80 /* Banner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Banner.swift; sourceTree = ""; };
+ 8C28075C2D07CD33008F4721 /* ARManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARManager.swift; sourceTree = ""; };
+ 8C362AC32C0B600400AD3091 /* Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = ""; };
+ 8C6767DA2C11551C00277484 /* QRScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScanner.swift; sourceTree = ""; };
+ 8CBC9A712D140E68000EFB70 /* AdvantageScopeXR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvantageScopeXR.swift; sourceTree = ""; };
+ 8CBFA6D82BF8663D009AA8CD /* AdvantageScopeXR.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AdvantageScopeXR.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 8CBFA6DD2BF8663D009AA8CD /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ 8CBFA6DF2BF8663E009AA8CD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 8CCB366F2D0F8A0600F516D8 /* RecordButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordButton.swift; sourceTree = ""; };
+ 8CF342582D14065100291788 /* AdvantageScopeXRClip.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AdvantageScopeXRClip.app; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
+ 8CF342682D14065200291788 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Info.plist,
+ );
+ target = 8CF342572D14065100291788 /* AdvantageScopeXRClip */;
+ };
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ 8CF342592D14065100291788 /* AdvantageScopeXRClip */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (8CF342682D14065200291788 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = AdvantageScopeXRClip; sourceTree = ""; };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 8CBFA6D52BF8663D009AA8CD /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8C1A9EB92C11695700DCF1E3 /* Starscream in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 8CF342552D14065100291788 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8CF3426E2D1406BF00291788 /* Starscream in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 8CBFA6CF2BF8663C009AA8CD = {
+ isa = PBXGroup;
+ children = (
+ 8CBFA6DA2BF8663D009AA8CD /* AdvantageScopeXR */,
+ 8CF342592D14065100291788 /* AdvantageScopeXRClip */,
+ 8CF3426C2D1406BF00291788 /* Frameworks */,
+ 8CBFA6D92BF8663D009AA8CD /* Products */,
+ );
+ sourceTree = "";
+ };
+ 8CBFA6D92BF8663D009AA8CD /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 8CBFA6D82BF8663D009AA8CD /* AdvantageScopeXR.app */,
+ 8CF342582D14065100291788 /* AdvantageScopeXRClip.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 8CBFA6DA2BF8663D009AA8CD /* AdvantageScopeXR */ = {
+ isa = PBXGroup;
+ children = (
+ 8CBC9A712D140E68000EFB70 /* AdvantageScopeXR.swift */,
+ 8CCB366F2D0F8A0600F516D8 /* RecordButton.swift */,
+ 8C1672E52D0BB125004100D8 /* Constants.swift */,
+ 8C103A8A2D093FD200FAE4C0 /* AdvantageScopeXR-Bridging-Header.h */,
+ 8C28075C2D07CD33008F4721 /* ARManager.swift */,
+ 8C103A822D093F9900FAE4C0 /* ARRenderer.swift */,
+ 8C103A832D093F9900FAE4C0 /* ARShaders.metal */,
+ 8C103A842D093F9900FAE4C0 /* ARShaderTypes.h */,
+ 8C2126142D05044700C92898 /* Info.plist */,
+ 8C2126122D05014700C92898 /* WebOverlay.swift */,
+ 8CBFA6DF2BF8663E009AA8CD /* Assets.xcassets */,
+ 8CBFA6DD2BF8663D009AA8CD /* ContentView.swift */,
+ 8C2759A12C0995BD0067FD80 /* ControlsMenu.swift */,
+ 8C2759A32C09960A0067FD80 /* Banner.swift */,
+ 8C362AC32C0B600400AD3091 /* Networking.swift */,
+ 8C6767DA2C11551C00277484 /* QRScanner.swift */,
+ );
+ path = AdvantageScopeXR;
+ sourceTree = "";
+ };
+ 8CF3426C2D1406BF00291788 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXHeadersBuildPhase section */
+ 8CF3427B2D140A0800291788 /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8CF342802D140A1600291788 /* AdvantageScopeXR-Bridging-Header.h in Headers */,
+ 8CF3427E2D140A1200291788 /* ARShaderTypes.h in Headers */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 8CF3427C2D140A0C00291788 /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8CF3427F2D140A1600291788 /* AdvantageScopeXR-Bridging-Header.h in Headers */,
+ 8CF3427D2D140A1200291788 /* ARShaderTypes.h in Headers */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+ 8CBFA6D72BF8663D009AA8CD /* AdvantageScopeXR */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 8CBFA6E62BF8663E009AA8CD /* Build configuration list for PBXNativeTarget "AdvantageScopeXR" */;
+ buildPhases = (
+ 8CF3427C2D140A0C00291788 /* Headers */,
+ 8CBFA6D42BF8663D009AA8CD /* Sources */,
+ 8CBFA6D52BF8663D009AA8CD /* Frameworks */,
+ 8CBFA6D62BF8663D009AA8CD /* Resources */,
+ 8CB5DDE62C11677F00D9B341 /* Embed App Clips */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 8CF342662D14065200291788 /* PBXTargetDependency */,
+ );
+ name = AdvantageScopeXR;
+ packageProductDependencies = (
+ 8C1A9EB82C11695700DCF1E3 /* Starscream */,
+ );
+ productName = "XR Testing";
+ productReference = 8CBFA6D82BF8663D009AA8CD /* AdvantageScopeXR.app */;
+ productType = "com.apple.product-type.application";
+ };
+ 8CF342572D14065100291788 /* AdvantageScopeXRClip */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 8CF342692D14065200291788 /* Build configuration list for PBXNativeTarget "AdvantageScopeXRClip" */;
+ buildPhases = (
+ 8CF3427B2D140A0800291788 /* Headers */,
+ 8CF342542D14065100291788 /* Sources */,
+ 8CF342552D14065100291788 /* Frameworks */,
+ 8CF342562D14065100291788 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ 8CF342592D14065100291788 /* AdvantageScopeXRClip */,
+ );
+ name = AdvantageScopeXRClip;
+ packageProductDependencies = (
+ 8CF3426D2D1406BF00291788 /* Starscream */,
+ );
+ productName = AdvantageScopeXRClip;
+ productReference = 8CF342582D14065100291788 /* AdvantageScopeXRClip.app */;
+ productType = "com.apple.product-type.application.on-demand-install-capable";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 8CBFA6D02BF8663D009AA8CD /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1620;
+ LastUpgradeCheck = 1610;
+ TargetAttributes = {
+ 8CBFA6D72BF8663D009AA8CD = {
+ CreatedOnToolsVersion = 15.4;
+ };
+ 8CF342572D14065100291788 = {
+ CreatedOnToolsVersion = 16.2;
+ };
+ };
+ };
+ buildConfigurationList = 8CBFA6D32BF8663D009AA8CD /* Build configuration list for PBXProject "AdvantageScopeXR" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 8CBFA6CF2BF8663C009AA8CD;
+ packageReferences = (
+ 8C1A9EB72C11695700DCF1E3 /* XCRemoteSwiftPackageReference "Starscream" */,
+ );
+ productRefGroup = 8CBFA6D92BF8663D009AA8CD /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 8CBFA6D72BF8663D009AA8CD /* AdvantageScopeXR */,
+ 8CF342572D14065100291788 /* AdvantageScopeXRClip */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 8CBFA6D62BF8663D009AA8CD /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8CBFA6E02BF8663E009AA8CD /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 8CF342562D14065100291788 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8CBC9A742D14B355000EFB70 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 8CBFA6D42BF8663D009AA8CD /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8CBC9A722D140E6A000EFB70 /* AdvantageScopeXR.swift in Sources */,
+ 8CBFA6DE2BF8663D009AA8CD /* ContentView.swift in Sources */,
+ 8C1672E62D0BB126004100D8 /* Constants.swift in Sources */,
+ 8C103A852D093F9900FAE4C0 /* ARRenderer.swift in Sources */,
+ 8C103A862D093F9900FAE4C0 /* ARShaders.metal in Sources */,
+ 8C28075D2D07CD35008F4721 /* ARManager.swift in Sources */,
+ 8C362AC42C0B600400AD3091 /* Networking.swift in Sources */,
+ 8C2759A42C09960A0067FD80 /* Banner.swift in Sources */,
+ 8C6767DB2C11551C00277484 /* QRScanner.swift in Sources */,
+ 8CCB36702D0F8A0900F516D8 /* RecordButton.swift in Sources */,
+ 8C2759A22C0995BD0067FD80 /* ControlsMenu.swift in Sources */,
+ 8C2126132D05014700C92898 /* WebOverlay.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 8CF342542D14065100291788 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8CBC9A732D140E6A000EFB70 /* AdvantageScopeXR.swift in Sources */,
+ 8CF342722D1408F000291788 /* ARManager.swift in Sources */,
+ 8CF342792D14091300291788 /* Networking.swift in Sources */,
+ 8CF342732D1408F400291788 /* ARRenderer.swift in Sources */,
+ 8CF3427A2D14091700291788 /* QRScanner.swift in Sources */,
+ 8CF342762D14090900291788 /* ContentView.swift in Sources */,
+ 8CF342702D1408E200291788 /* RecordButton.swift in Sources */,
+ 8CF342752D14090400291788 /* WebOverlay.swift in Sources */,
+ 8CF342712D1408E700291788 /* Constants.swift in Sources */,
+ 8CF342782D14091000291788 /* Banner.swift in Sources */,
+ 8CF342772D14090C00291788 /* ControlsMenu.swift in Sources */,
+ 8CF342742D1408F700291788 /* ARShaders.metal in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 8CF342662D14065200291788 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 8CF342572D14065100291788 /* AdvantageScopeXRClip */;
+ targetProxy = 8CF342652D14065200291788 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ 8CBFA6E42BF8663E009AA8CD /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 8CBFA6E52BF8663E009AA8CD /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 8CBFA6E72BF8663E009AA8CD /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 6S3UQC528P;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = AdvantageScopeXR/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = "AScope XR";
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
+ INFOPLIST_KEY_NSCameraUsageDescription = "AdvantageScope XR uses the camera to show augmented reality content.";
+ INFOPLIST_KEY_NSLocalNetworkUsageDescription = "AdvantageScope XR uses the local network to communicate with the AdvantageScope desktop app.";
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UIRequiredDeviceCapabilities = arkit;
+ INFOPLIST_KEY_UIRequiresFullScreen = YES;
+ INFOPLIST_KEY_UIStatusBarHidden = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ LIBRARY_SEARCH_PATHS = "$(inherited)";
+ MARKETING_VERSION = 1.0.0;
+ PRODUCT_BUNDLE_IDENTIFIER = org.littletonrobotics.advantagescopexr;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = "AdvantageScopeXR/AdvantageScopeXR-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 8CBFA6E82BF8663E009AA8CD /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 6S3UQC528P;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = AdvantageScopeXR/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = "AScope XR";
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
+ INFOPLIST_KEY_NSCameraUsageDescription = "AdvantageScope XR uses the camera to show augmented reality content.";
+ INFOPLIST_KEY_NSLocalNetworkUsageDescription = "AdvantageScope XR uses the local network to communicate with the AdvantageScope desktop app.";
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UIRequiredDeviceCapabilities = arkit;
+ INFOPLIST_KEY_UIRequiresFullScreen = YES;
+ INFOPLIST_KEY_UIStatusBarHidden = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0.0;
+ PRODUCT_BUNDLE_IDENTIFIER = org.littletonrobotics.advantagescopexr;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = "AdvantageScopeXR/AdvantageScopeXR-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ 8CF3426A2D14065200291788 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = AdvantageScopeXRClip/AdvantageScopeXRClip.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 6S3UQC528P;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = AdvantageScopeXRClip/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = "AScope XR";
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
+ INFOPLIST_KEY_NSCameraUsageDescription = "AdvantageScope XR uses the camera to show augmented reality content.";
+ INFOPLIST_KEY_NSLocalNetworkUsageDescription = "AdvantageScope XR uses the local network to communicate with the AdvantageScope desktop app.";
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UIRequiredDeviceCapabilities = arkit;
+ INFOPLIST_KEY_UIRequiresFullScreen = YES;
+ INFOPLIST_KEY_UIStatusBarHidden = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0.0;
+ PRODUCT_BUNDLE_IDENTIFIER = org.littletonrobotics.advantagescopexr.Clip;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG APPCLIP";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = "AdvantageScopeXR/AdvantageScopeXR-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 8CF3426B2D14065200291788 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = AdvantageScopeXRClip/AdvantageScopeXRClip.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 6S3UQC528P;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = AdvantageScopeXRClip/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = "AScope XR";
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
+ INFOPLIST_KEY_NSCameraUsageDescription = "AdvantageScope XR uses the camera to show augmented reality content.";
+ INFOPLIST_KEY_NSLocalNetworkUsageDescription = "AdvantageScope XR uses the local network to communicate with the AdvantageScope desktop app.";
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UIRequiredDeviceCapabilities = arkit;
+ INFOPLIST_KEY_UIRequiresFullScreen = YES;
+ INFOPLIST_KEY_UIStatusBarHidden = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0.0;
+ PRODUCT_BUNDLE_IDENTIFIER = org.littletonrobotics.advantagescopexr.Clip;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = APPCLIP;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = "AdvantageScopeXR/AdvantageScopeXR-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 8CBFA6D32BF8663D009AA8CD /* Build configuration list for PBXProject "AdvantageScopeXR" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 8CBFA6E42BF8663E009AA8CD /* Debug */,
+ 8CBFA6E52BF8663E009AA8CD /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 8CBFA6E62BF8663E009AA8CD /* Build configuration list for PBXNativeTarget "AdvantageScopeXR" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 8CBFA6E72BF8663E009AA8CD /* Debug */,
+ 8CBFA6E82BF8663E009AA8CD /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 8CF342692D14065200291788 /* Build configuration list for PBXNativeTarget "AdvantageScopeXRClip" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 8CF3426A2D14065200291788 /* Debug */,
+ 8CF3426B2D14065200291788 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCRemoteSwiftPackageReference section */
+ 8C1A9EB72C11695700DCF1E3 /* XCRemoteSwiftPackageReference "Starscream" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/daltoniam/Starscream.git";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 4.0.8;
+ };
+ };
+/* End XCRemoteSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 8C1A9EB82C11695700DCF1E3 /* Starscream */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 8C1A9EB72C11695700DCF1E3 /* XCRemoteSwiftPackageReference "Starscream" */;
+ productName = Starscream;
+ };
+ 8CF3426D2D1406BF00291788 /* Starscream */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 8C1A9EB72C11695700DCF1E3 /* XCRemoteSwiftPackageReference "Starscream" */;
+ productName = Starscream;
+ };
+/* End XCSwiftPackageProductDependency section */
+ };
+ rootObject = 8CBFA6D02BF8663D009AA8CD /* Project object */;
+}
diff --git a/xr/AdvantageScopeXR.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/xr/AdvantageScopeXR.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..919434a6
--- /dev/null
+++ b/xr/AdvantageScopeXR.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/xr/AdvantageScopeXR.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/xr/AdvantageScopeXR.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/xr/AdvantageScopeXR.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/xr/AdvantageScopeXR.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/xr/AdvantageScopeXR.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 00000000..0c67376e
--- /dev/null
+++ b/xr/AdvantageScopeXR.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/xr/AdvantageScopeXR.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/xr/AdvantageScopeXR.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 00000000..4fd8c926
--- /dev/null
+++ b/xr/AdvantageScopeXR.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,15 @@
+{
+ "originHash" : "944cd831f17e85433d017fd073e72b3807f32c74a97f0653b8cd2ea5cc713c88",
+ "pins" : [
+ {
+ "identity" : "starscream",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/daltoniam/Starscream.git",
+ "state" : {
+ "revision" : "c6bfd1af48efcc9a9ad203665db12375ba6b145a",
+ "version" : "4.0.8"
+ }
+ }
+ ],
+ "version" : 3
+}
diff --git a/xr/AdvantageScopeXR.xcodeproj/xcshareddata/xcschemes/AdvantageScopeXR.xcscheme b/xr/AdvantageScopeXR.xcodeproj/xcshareddata/xcschemes/AdvantageScopeXR.xcscheme
new file mode 100644
index 00000000..f7a42368
--- /dev/null
+++ b/xr/AdvantageScopeXR.xcodeproj/xcshareddata/xcschemes/AdvantageScopeXR.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/xr/AdvantageScopeXR.xcodeproj/xcshareddata/xcschemes/AdvantageScopeXRClip.xcscheme b/xr/AdvantageScopeXR.xcodeproj/xcshareddata/xcschemes/AdvantageScopeXRClip.xcscheme
new file mode 100644
index 00000000..ffdc3ca8
--- /dev/null
+++ b/xr/AdvantageScopeXR.xcodeproj/xcshareddata/xcschemes/AdvantageScopeXRClip.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/xr/AdvantageScopeXR/ARManager.swift b/xr/AdvantageScopeXR/ARManager.swift
new file mode 100644
index 00000000..d2256223
--- /dev/null
+++ b/xr/AdvantageScopeXR/ARManager.swift
@@ -0,0 +1,204 @@
+import ARKit
+import MetalKit
+
+extension MTKView : RenderDestinationProvider {
+}
+
+class ARManager: NSObject, ARSessionDelegate, MTKViewDelegate {
+ var appState: AppState? = nil
+ var webOverlay: WebOverlay? = nil
+ let session = ARSession()
+ let view = MTKView()
+
+ private let arConfig = ARWorldTrackingConfiguration()
+ private var renderer: ARRenderer! = nil
+ private var viewportSize: CGSize = CGSize()
+ private var frameCallbacks: [(_ frame: ARFrame) -> Void] = []
+ private var cachedAnchors: [ARAnchor] = []
+
+ override init() {
+ super.init()
+
+ // Initialize AR session
+ session.delegate = self
+ arConfig.worldAlignment = .gravity
+ arConfig.planeDetection = .horizontal
+ session.run(arConfig)
+
+ // Initialize view
+ view.device = MTLCreateSystemDefaultDevice()
+ view.backgroundColor = UIColor.clear
+ view.delegate = self
+ viewportSize = view.bounds.size
+ guard view.device != nil else {
+ print("Metal is not supported on this device")
+ return
+ }
+
+ // Start renderer
+ renderer = ARRenderer(session: session, metalDevice: view.device!, renderDestination: view)
+ renderer.drawRectResized(size: view.bounds.size)
+ }
+
+ func addFrameCallback(_ callback: @escaping (_ frame: ARFrame) -> Void) {
+ frameCallbacks.append(callback)
+ }
+
+ func recalibrate() {
+ cachedAnchors = []
+ session.run(arConfig, options: [.resetTracking, .resetSceneReconstruction, .removeExistingAnchors])
+ }
+
+ // MARK: - MTKViewDelegate
+
+ func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
+ viewportSize = size
+ renderer.drawRectResized(size: size)
+ }
+
+ func draw(in view: MTKView) {
+ renderer.update()
+ }
+
+ // MARK: - ARSessionDelegate
+
+ func session(_ session: ARSession, didUpdate frame: ARFrame) {
+ // Get interface orientation
+ var orientation: UIInterfaceOrientation = .portrait
+ let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
+ if (windowScene != nil) {
+ orientation = windowScene!.interfaceOrientation
+ }
+
+ // Get camera transform
+ var cameraTransform = frame.camera.transform
+ switch (orientation) {
+ case .portrait:
+ cameraTransform *= simd_float4x4(
+ [ 0, 1, 0, 0],
+ [-1, 0, 0, 0],
+ [ 0, 0, 1, 0],
+ [ 0, 0, 0, 1]
+ )
+ case .portraitUpsideDown:
+ cameraTransform *= simd_float4x4(
+ [0, -1, 0, 0],
+ [1, 0, 0, 0],
+ [0, 0, 1, 0],
+ [0, 0, 0, 1]
+ )
+ case .landscapeLeft:
+ cameraTransform *= simd_float4x4(
+ [-1, 0, 0, 0],
+ [ 0, -1, 0, 0],
+ [ 0, 0, 1, 0],
+ [ 0, 0, 0, 1]
+ )
+ default:
+ break
+ }
+
+ // Update camera data
+ var cameraData = Dictionary()
+ cameraData["projection"] = simdFloat4x4ToArray(m: frame.camera.projectionMatrix(for: orientation, viewportSize: viewportSize, zNear: 0.15, zFar: 50.0))
+ cameraData["worldInverse"] = simdFloat4x4ToArray(m: simd_inverse(cameraTransform))
+ cameraData["position"] = [cameraTransform.columns.3.x, cameraTransform.columns.3.y, cameraTransform.columns.3.z]
+
+ // Get frame size
+ var frameSize = Array()
+ frameSize.append(Int(frame.camera.imageResolution.width))
+ frameSize.append(Int(frame.camera.imageResolution.height))
+
+ // Get lighting data
+ var lightingData = Dictionary()
+ lightingData["grain"] = frame.cameraGrainIntensity;
+ lightingData["intensity"] = 1.0;
+ lightingData["temperature"] = 4500.0;
+ if let lightEstimate = frame.lightEstimate {
+ lightingData["intensity"] = lightEstimate.ambientIntensity / 1000.0
+ lightingData["temperature"] = lightEstimate.ambientColorTemperature
+ }
+
+ // Run raycast
+ var raycastData = Dictionary()
+ raycastData["isValid"] = false
+ if (frame.camera.trackingState == .normal) {
+ let raycastResults = session.raycast(frame.raycastQuery(from: CGPoint(x: 0.5, y: 0.5), allowing: .existingPlaneGeometry, alignment: .horizontal))
+ if (!raycastResults.isEmpty && raycastResults[0].anchor != nil) {
+ let result = raycastResults[0]
+ let anchor = result.anchor!
+ let pointTransform = result.worldTransform
+ let anchorTransform = anchor.transform
+
+ // Save results
+ raycastData["isValid"] = true
+ raycastData["position"] = [
+ pointTransform.columns.3.x - anchorTransform.columns.3.x,
+ pointTransform.columns.3.y - anchorTransform.columns.3.y,
+ pointTransform.columns.3.z - anchorTransform.columns.3.z
+ ]
+ raycastData["anchorId"] = result.anchor!.identifier.uuidString
+
+ // Start tracking anchor if new
+ if (!cachedAnchors.contains(where: { $0.identifier == result.anchor!.identifier })) {
+ cachedAnchors.append(anchor)
+ }
+ }
+ }
+
+ // Get anchors
+ var anchorData = Dictionary()
+ for anchor in cachedAnchors {
+ let transform = anchor.transform
+ anchorData[anchor.identifier.uuidString] = [transform.columns.3.x, transform.columns.3.y, transform.columns.3.z]
+ }
+
+ // Publish data
+ var renderData = Dictionary()
+ renderData["camera"] = cameraData
+ renderData["frameSize"] = frameSize
+ renderData["lighting"] = lightingData
+ renderData["raycast"] = raycastData
+ renderData["anchors"] = anchorData
+ webOverlay?.render(renderData)
+
+ // Update tracking state
+ if (appState != nil) {
+ appState!.trackingReady = frame.camera.trackingState == .normal
+ }
+
+ // Run frame callbacks
+ for callback in frameCallbacks {
+ callback(frame)
+ }
+ }
+
+ func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
+ var newCachedAnchors: [ARAnchor] = []
+ for cachedAnchor in cachedAnchors {
+ var found = false;
+ for anchor in anchors {
+ if anchor.identifier == cachedAnchor.identifier {
+ found = true;
+ newCachedAnchors.append(anchor)
+ break
+ }
+ }
+ if (!found) {
+ newCachedAnchors.append(cachedAnchor)
+ }
+ }
+ cachedAnchors = newCachedAnchors
+ }
+
+ func simdFloat4x4ToArray(m: simd_float4x4) -> [Float] {
+ return [m.columns.0.x, m.columns.0.y, m.columns.0.z, m.columns.0.w,
+ m.columns.1.x, m.columns.1.y, m.columns.1.z, m.columns.1.w,
+ m.columns.2.x, m.columns.2.y, m.columns.2.z, m.columns.2.w,
+ m.columns.3.x, m.columns.3.y, m.columns.3.z, m.columns.3.w]
+ }
+
+ func simdFloat3ToArray(v: simd_float3) -> [Float] {
+ return [v.x, v.y, v.z]
+ }
+}
diff --git a/xr/AdvantageScopeXR/ARRenderer.swift b/xr/AdvantageScopeXR/ARRenderer.swift
new file mode 100644
index 00000000..5aca6dfe
--- /dev/null
+++ b/xr/AdvantageScopeXR/ARRenderer.swift
@@ -0,0 +1,298 @@
+import ARKit
+
+protocol RenderDestinationProvider {
+ var currentRenderPassDescriptor: MTLRenderPassDescriptor? { get }
+ var currentDrawable: CAMetalDrawable? { get }
+ var colorPixelFormat: MTLPixelFormat { get set }
+ var depthStencilPixelFormat: MTLPixelFormat { get set }
+ var sampleCount: Int { get set }
+}
+
+// The max number of command buffers in flight
+let kMaxBuffersInFlight: Int = 3
+
+// The 16 byte aligned size of our uniform structures
+let kAlignedSharedUniformsSize: Int = (MemoryLayout.size & ~0xFF) + 0x100
+
+// Vertex data for an image plane
+let kImagePlaneVertexData: [Float] = [
+ -1.0, -1.0, 0.0, 1.0,
+ 1.0, -1.0, 1.0, 1.0,
+ -1.0, 1.0, 0.0, 0.0,
+ 1.0, 1.0, 1.0, 0.0,
+]
+
+
+class ARRenderer {
+ let session: ARSession
+ let device: MTLDevice
+ let inFlightSemaphore = DispatchSemaphore(value: kMaxBuffersInFlight)
+ var renderDestination: RenderDestinationProvider
+
+ // Metal objects
+ var commandQueue: MTLCommandQueue!
+ var sharedUniformBuffer: MTLBuffer!
+ var anchorUniformBuffer: MTLBuffer!
+ var imagePlaneVertexBuffer: MTLBuffer!
+ var capturedImagePipelineState: MTLRenderPipelineState!
+ var capturedImageDepthState: MTLDepthStencilState!
+ var capturedImageTextureY: CVMetalTexture?
+ var capturedImageTextureCbCr: CVMetalTexture?
+
+ // Captured image texture cache
+ var capturedImageTextureCache: CVMetalTextureCache!
+
+ // Used to determine _uniformBufferStride each frame.
+ // This is the current frame number modulo kMaxBuffersInFlight
+ var uniformBufferIndex: Int = 0
+
+ // Offset within _sharedUniformBuffer to set for the current frame
+ var sharedUniformBufferOffset: Int = 0
+
+ // Addresses to write shared uniforms to each frame
+ var sharedUniformBufferAddress: UnsafeMutableRawPointer!
+
+ // The current viewport size
+ var viewportSize: CGSize = CGSize()
+
+ // Flag for viewport size changes
+ var viewportSizeDidChange: Bool = false
+
+ init(session: ARSession, metalDevice device: MTLDevice, renderDestination: RenderDestinationProvider) {
+ self.session = session
+ self.device = device
+ self.renderDestination = renderDestination
+ loadMetal()
+ }
+
+ func drawRectResized(size: CGSize) {
+ viewportSize = size
+ viewportSizeDidChange = true
+ }
+
+ func update() {
+ // Wait to ensure only kMaxBuffersInFlight are getting processed by any stage in the Metal
+ // pipeline (App, Metal, Drivers, GPU, etc)
+ let _ = inFlightSemaphore.wait(timeout: DispatchTime.distantFuture)
+
+ // Create a new command buffer for each renderpass to the current drawable
+ if let commandBuffer = commandQueue.makeCommandBuffer() {
+ commandBuffer.label = "MyCommand"
+
+ // Add completion handler which signal _inFlightSemaphore when Metal and the GPU has fully
+ // finished processing the commands we're encoding this frame. This indicates when the
+ // dynamic buffers, that we're writing to this frame, will no longer be needed by Metal
+ // and the GPU.
+ // Retain our CVMetalTextures for the duration of the rendering cycle. The MTLTextures
+ // we use from the CVMetalTextures are not valid unless their parent CVMetalTextures
+ // are retained. Since we may release our CVMetalTexture ivars during the rendering
+ // cycle, we must retain them separately here.
+ var textures = [capturedImageTextureY, capturedImageTextureCbCr]
+ commandBuffer.addCompletedHandler{ [weak self] commandBuffer in
+ if let strongSelf = self {
+ strongSelf.inFlightSemaphore.signal()
+ }
+ textures.removeAll()
+ }
+
+ updateBufferStates()
+ updateGameState()
+
+ if let renderPassDescriptor = renderDestination.currentRenderPassDescriptor, let currentDrawable = renderDestination.currentDrawable, let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) {
+
+ renderEncoder.label = "MyRenderEncoder"
+
+ drawCapturedImage(renderEncoder: renderEncoder)
+
+ // We're done encoding commands
+ renderEncoder.endEncoding()
+
+ // Schedule a present once the framebuffer is complete using the current drawable
+ commandBuffer.present(currentDrawable)
+ }
+
+ // Finalize rendering here & push the command buffer to the GPU
+ commandBuffer.commit()
+ }
+ }
+
+ // MARK: - Private
+
+ func loadMetal() {
+ // Create and load our basic Metal state objects
+
+ // Set the default formats needed to render
+ renderDestination.depthStencilPixelFormat = .depth32Float_stencil8
+ renderDestination.colorPixelFormat = .bgra8Unorm
+ renderDestination.sampleCount = 1
+
+ // Calculate our uniform buffer sizes. We allocate kMaxBuffersInFlight instances for uniform
+ // storage in a single buffer. This allows us to update uniforms in a ring (i.e. triple
+ // buffer the uniforms) so that the GPU reads from one slot in the ring wil the CPU writes
+ // to another. Uniform storage must be aligned (to 256 bytes) to meet the requirements to be
+ // an argument in the constant address space of our shading functions.
+ let sharedUniformBufferSize = kAlignedSharedUniformsSize * kMaxBuffersInFlight
+
+ // Create and allocate our uniform buffer objects. Indicate shared storage so that both the
+ // CPU can access the buffer
+ sharedUniformBuffer = device.makeBuffer(length: sharedUniformBufferSize, options: .storageModeShared)
+ sharedUniformBuffer.label = "SharedUniformBuffer"
+
+ // Create a vertex buffer with our image plane vertex data.
+ let imagePlaneVertexDataCount = kImagePlaneVertexData.count * MemoryLayout.size
+ imagePlaneVertexBuffer = device.makeBuffer(bytes: kImagePlaneVertexData, length: imagePlaneVertexDataCount, options: [])
+ imagePlaneVertexBuffer.label = "ImagePlaneVertexBuffer"
+
+ // Load all the shader files with a metal file extension in the project
+ let defaultLibrary = device.makeDefaultLibrary()!
+
+ let capturedImageVertexFunction = defaultLibrary.makeFunction(name: "capturedImageVertexTransform")!
+ let capturedImageFragmentFunction = defaultLibrary.makeFunction(name: "capturedImageFragmentShader")!
+
+ // Create a vertex descriptor for our image plane vertex buffer
+ let imagePlaneVertexDescriptor = MTLVertexDescriptor()
+
+ // Positions.
+ imagePlaneVertexDescriptor.attributes[0].format = .float2
+ imagePlaneVertexDescriptor.attributes[0].offset = 0
+ imagePlaneVertexDescriptor.attributes[0].bufferIndex = Int(kBufferIndexMeshPositions.rawValue)
+
+ // Texture coordinates.
+ imagePlaneVertexDescriptor.attributes[1].format = .float2
+ imagePlaneVertexDescriptor.attributes[1].offset = 8
+ imagePlaneVertexDescriptor.attributes[1].bufferIndex = Int(kBufferIndexMeshPositions.rawValue)
+
+ // Buffer Layout
+ imagePlaneVertexDescriptor.layouts[0].stride = 16
+ imagePlaneVertexDescriptor.layouts[0].stepRate = 1
+ imagePlaneVertexDescriptor.layouts[0].stepFunction = .perVertex
+
+ // Create a pipeline state for rendering the captured image
+ let capturedImagePipelineStateDescriptor = MTLRenderPipelineDescriptor()
+ capturedImagePipelineStateDescriptor.label = "MyCapturedImagePipeline"
+ capturedImagePipelineStateDescriptor.rasterSampleCount = renderDestination.sampleCount
+ capturedImagePipelineStateDescriptor.vertexFunction = capturedImageVertexFunction
+ capturedImagePipelineStateDescriptor.fragmentFunction = capturedImageFragmentFunction
+ capturedImagePipelineStateDescriptor.vertexDescriptor = imagePlaneVertexDescriptor
+ capturedImagePipelineStateDescriptor.colorAttachments[0].pixelFormat = renderDestination.colorPixelFormat
+ capturedImagePipelineStateDescriptor.depthAttachmentPixelFormat = renderDestination.depthStencilPixelFormat
+ capturedImagePipelineStateDescriptor.stencilAttachmentPixelFormat = renderDestination.depthStencilPixelFormat
+
+ do {
+ try capturedImagePipelineState = device.makeRenderPipelineState(descriptor: capturedImagePipelineStateDescriptor)
+ } catch let error {
+ print("Failed to created captured image pipeline state, error \(error)")
+ }
+
+ let capturedImageDepthStateDescriptor = MTLDepthStencilDescriptor()
+ capturedImageDepthStateDescriptor.depthCompareFunction = .always
+ capturedImageDepthStateDescriptor.isDepthWriteEnabled = false
+ capturedImageDepthState = device.makeDepthStencilState(descriptor: capturedImageDepthStateDescriptor)
+
+ // Create captured image texture cache
+ var textureCache: CVMetalTextureCache?
+ CVMetalTextureCacheCreate(nil, nil, device, nil, &textureCache)
+ capturedImageTextureCache = textureCache
+
+ // Create the command queue
+ commandQueue = device.makeCommandQueue()
+ }
+
+ func updateBufferStates() {
+ // Update the location(s) to which we'll write to in our dynamically changing Metal buffers for
+ // the current frame (i.e. update our slot in the ring buffer used for the current frame)
+
+ uniformBufferIndex = (uniformBufferIndex + 1) % kMaxBuffersInFlight
+ sharedUniformBufferOffset = kAlignedSharedUniformsSize * uniformBufferIndex
+ sharedUniformBufferAddress = sharedUniformBuffer.contents().advanced(by: sharedUniformBufferOffset)
+ }
+
+ func updateGameState() {
+ // Update any game state
+
+ guard let currentFrame = session.currentFrame else {
+ return
+ }
+
+ updateCapturedImageTextures(frame: currentFrame)
+
+ if viewportSizeDidChange {
+ viewportSizeDidChange = false
+
+ updateImagePlane(frame: currentFrame)
+ }
+ }
+
+ func updateCapturedImageTextures(frame: ARFrame) {
+ // Create two textures (Y and CbCr) from the provided frame's captured image
+ let pixelBuffer = frame.capturedImage
+
+ if (CVPixelBufferGetPlaneCount(pixelBuffer) < 2) {
+ return
+ }
+
+ capturedImageTextureY = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat:.r8Unorm, planeIndex:0)
+ capturedImageTextureCbCr = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat:.rg8Unorm, planeIndex:1)
+ }
+
+ func createTexture(fromPixelBuffer pixelBuffer: CVPixelBuffer, pixelFormat: MTLPixelFormat, planeIndex: Int) -> CVMetalTexture? {
+ let width = CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex)
+ let height = CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex)
+
+ var texture: CVMetalTexture? = nil
+ let status = CVMetalTextureCacheCreateTextureFromImage(nil, capturedImageTextureCache, pixelBuffer, nil, pixelFormat, width, height, planeIndex, &texture)
+
+ if status != kCVReturnSuccess {
+ texture = nil
+ }
+
+ return texture
+ }
+
+ func updateImagePlane(frame: ARFrame) {
+ // Get interface orientation
+ var orientation: UIInterfaceOrientation = .portrait
+ let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
+ if (windowScene != nil) {
+ orientation = windowScene!.interfaceOrientation
+ }
+
+ // Update the texture coordinates of our image plane to aspect fill the viewport
+ let displayToCameraTransform = frame.displayTransform(for: orientation, viewportSize: viewportSize).inverted()
+
+ let vertexData = imagePlaneVertexBuffer.contents().assumingMemoryBound(to: Float.self)
+ for index in 0...3 {
+ let textureCoordIndex = 4 * index + 2
+ let textureCoord = CGPoint(x: CGFloat(kImagePlaneVertexData[textureCoordIndex]), y: CGFloat(kImagePlaneVertexData[textureCoordIndex + 1]))
+ let transformedCoord = textureCoord.applying(displayToCameraTransform)
+ vertexData[textureCoordIndex] = Float(transformedCoord.x)
+ vertexData[textureCoordIndex + 1] = Float(transformedCoord.y)
+ }
+ }
+
+ func drawCapturedImage(renderEncoder: MTLRenderCommandEncoder) {
+ guard let textureY = capturedImageTextureY, let textureCbCr = capturedImageTextureCbCr else {
+ return
+ }
+
+ // Push a debug group allowing us to identify render commands in the GPU Frame Capture tool
+ renderEncoder.pushDebugGroup("DrawCapturedImage")
+
+ // Set render command encoder state
+ renderEncoder.setCullMode(.none)
+ renderEncoder.setRenderPipelineState(capturedImagePipelineState)
+ renderEncoder.setDepthStencilState(capturedImageDepthState)
+
+ // Set mesh's vertex buffers
+ renderEncoder.setVertexBuffer(imagePlaneVertexBuffer, offset: 0, index: Int(kBufferIndexMeshPositions.rawValue))
+
+ // Set any textures read/sampled from our render pipeline
+ renderEncoder.setFragmentTexture(CVMetalTextureGetTexture(textureY), index: Int(kTextureIndexY.rawValue))
+ renderEncoder.setFragmentTexture(CVMetalTextureGetTexture(textureCbCr), index: Int(kTextureIndexCbCr.rawValue))
+
+ // Draw each submesh of our mesh
+ renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
+
+ renderEncoder.popDebugGroup()
+ }
+}
diff --git a/xr/AdvantageScopeXR/ARShaderTypes.h b/xr/AdvantageScopeXR/ARShaderTypes.h
new file mode 100644
index 00000000..552ea73e
--- /dev/null
+++ b/xr/AdvantageScopeXR/ARShaderTypes.h
@@ -0,0 +1,48 @@
+//
+// Header containing types and enum constants shared between Metal shaders and C/ObjC source
+//
+#ifndef ShaderTypes_h
+#define ShaderTypes_h
+
+#include
+
+// Buffer index values shared between shader and C code to ensure Metal shader buffer inputs match
+// Metal API buffer set calls
+typedef enum BufferIndices {
+ kBufferIndexMeshPositions = 0,
+ kBufferIndexMeshGenerics = 1,
+ kBufferIndexInstanceUniforms = 2,
+ kBufferIndexSharedUniforms = 3
+} BufferIndices;
+
+// Attribute index values shared between shader and C code to ensure Metal shader vertex
+// attribute indices match the Metal API vertex descriptor attribute indices
+typedef enum VertexAttributes {
+ kVertexAttributePosition = 0,
+ kVertexAttributeTexcoord = 1,
+ kVertexAttributeNormal = 2
+} VertexAttributes;
+
+// Texture index values shared between shader and C code to ensure Metal shader texture indices
+// match indices of Metal API texture set calls
+typedef enum TextureIndices {
+ kTextureIndexColor = 0,
+ kTextureIndexY = 1,
+ kTextureIndexCbCr = 2
+} TextureIndices;
+
+// Structure shared between shader and C code to ensure the layout of shared uniform data accessed in
+// Metal shaders matches the layout of uniform data set in C code
+typedef struct {
+ // Camera Uniforms
+ matrix_float4x4 projectionMatrix;
+ matrix_float4x4 viewMatrix;
+
+ // Lighting Properties
+ vector_float3 ambientLightColor;
+ vector_float3 directionalLightDirection;
+ vector_float3 directionalLightColor;
+ float materialShininess;
+} SharedUniforms;
+
+#endif /* ShaderTypes_h */
diff --git a/xr/AdvantageScopeXR/ARShaders.metal b/xr/AdvantageScopeXR/ARShaders.metal
new file mode 100644
index 00000000..48b7d4d4
--- /dev/null
+++ b/xr/AdvantageScopeXR/ARShaders.metal
@@ -0,0 +1,71 @@
+#include
+#include
+
+// Include header shared between this Metal shader code and C code executing Metal API commands
+#import "ARShaderTypes.h"
+
+using namespace metal;
+
+typedef struct {
+ float2 position [[attribute(kVertexAttributePosition)]];
+ float2 texCoord [[attribute(kVertexAttributeTexcoord)]];
+} ImageVertex;
+
+
+typedef struct {
+ float4 position [[position]];
+ float2 texCoord;
+} ImageColorInOut;
+
+
+// Captured image vertex function
+vertex ImageColorInOut capturedImageVertexTransform(ImageVertex in [[stage_in]]) {
+ ImageColorInOut out;
+
+ // Pass through the image vertex's position
+ out.position = float4(in.position, 0.0, 1.0);
+
+ // Pass through the texture coordinate
+ out.texCoord = in.texCoord;
+
+ return out;
+}
+
+// Captured image fragment function
+fragment float4 capturedImageFragmentShader(ImageColorInOut in [[stage_in]],
+ texture2d capturedImageTextureY [[ texture(kTextureIndexY) ]],
+ texture2d capturedImageTextureCbCr [[ texture(kTextureIndexCbCr) ]]) {
+
+ constexpr sampler colorSampler(mip_filter::linear,
+ mag_filter::linear,
+ min_filter::linear);
+
+ const float4x4 ycbcrToRGBTransform = float4x4(
+ float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f),
+ float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f),
+ float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f),
+ float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)
+ );
+
+ // Sample Y and CbCr textures to get the YCbCr color at the given texture coordinate
+ float4 ycbcr = float4(capturedImageTextureY.sample(colorSampler, in.texCoord).r,
+ capturedImageTextureCbCr.sample(colorSampler, in.texCoord).rg, 1.0);
+
+ // Return converted RGB color
+ return ycbcrToRGBTransform * ycbcr;
+}
+
+
+typedef struct {
+ float3 position [[attribute(kVertexAttributePosition)]];
+ float2 texCoord [[attribute(kVertexAttributeTexcoord)]];
+ half3 normal [[attribute(kVertexAttributeNormal)]];
+} Vertex;
+
+
+typedef struct {
+ float4 position [[position]];
+ float4 color;
+ half3 eyePosition;
+ half3 normal;
+} ColorInOut;
diff --git a/xr/AdvantageScopeXR/AdvantageScopeXR-Bridging-Header.h b/xr/AdvantageScopeXR/AdvantageScopeXR-Bridging-Header.h
new file mode 100644
index 00000000..48340c68
--- /dev/null
+++ b/xr/AdvantageScopeXR/AdvantageScopeXR-Bridging-Header.h
@@ -0,0 +1 @@
+#import "ARShaderTypes.h"
diff --git a/xr/AdvantageScopeXR/AdvantageScopeXR.swift b/xr/AdvantageScopeXR/AdvantageScopeXR.swift
new file mode 100644
index 00000000..ccd120a7
--- /dev/null
+++ b/xr/AdvantageScopeXR/AdvantageScopeXR.swift
@@ -0,0 +1,10 @@
+import SwiftUI
+
+@main
+struct AdvantageScopeXR: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}
diff --git a/xr/AdvantageScopeXR/Assets.xcassets/AccentColor.colorset/Contents.json b/xr/AdvantageScopeXR/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 00000000..3470d841
--- /dev/null
+++ b/xr/AdvantageScopeXR/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.898",
+ "green" : "0.169",
+ "red" : "0.004"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/xr/AdvantageScopeXR/Assets.xcassets/AppIcon.appiconset/AdvantageScope-ios.png b/xr/AdvantageScopeXR/Assets.xcassets/AppIcon.appiconset/AdvantageScope-ios.png
new file mode 100644
index 00000000..2852d204
Binary files /dev/null and b/xr/AdvantageScopeXR/Assets.xcassets/AppIcon.appiconset/AdvantageScope-ios.png differ
diff --git a/xr/AdvantageScopeXR/Assets.xcassets/AppIcon.appiconset/Contents.json b/xr/AdvantageScopeXR/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..98c7376b
--- /dev/null
+++ b/xr/AdvantageScopeXR/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "filename" : "AdvantageScope-ios.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/xr/AdvantageScopeXR/Assets.xcassets/Contents.json b/xr/AdvantageScopeXR/Assets.xcassets/Contents.json
new file mode 100644
index 00000000..73c00596
--- /dev/null
+++ b/xr/AdvantageScopeXR/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/xr/AdvantageScopeXR/Banner.swift b/xr/AdvantageScopeXR/Banner.swift
new file mode 100644
index 00000000..a07c274c
--- /dev/null
+++ b/xr/AdvantageScopeXR/Banner.swift
@@ -0,0 +1,38 @@
+import SwiftUI
+
+struct Banner: View {
+ @EnvironmentObject var appState: AppState
+
+ var body: some View {
+ Text(text())
+ .multilineTextAlignment(.center)
+ .padding(10)
+ .frame(maxWidth: .infinity)
+ .background(.thinMaterial)
+ .opacity(show() ? 1 : 0)
+ .persistentSystemOverlays(show() ? .visible : .hidden)
+ .animation(.easeInOut(duration: 0.25), value: show())
+ }
+
+ private func text() -> String {
+ if (appState.scanningQR) {
+ return "Scan QR code displayed in AdvantageScope."
+ } else if (appState.serverIncompatibility == .serverTooOld) {
+ return "Update AdvantageScope to the latest version, then try again."
+ } else if (appState.serverIncompatibility == .serverTooNew) {
+ return "Update AdvantageScope XR to the latest version, then try again."
+ } else if (!appState.serverConnected ) {
+ return "Searching for server..."
+ } else if (!appState.trackingReady || appState.calibrationText == "$TRACKING_WARNING") {
+ return "Move " + UIDevice.current.model + " to detect environment."
+ } else if (!appState.calibrationText.isEmpty) {
+ return appState.calibrationText
+ } else {
+ return ""
+ }
+ }
+
+ private func show() -> Bool {
+ return text() != ""
+ }
+}
diff --git a/xr/AdvantageScopeXR/Constants.swift b/xr/AdvantageScopeXR/Constants.swift
new file mode 100644
index 00000000..91faa7f6
--- /dev/null
+++ b/xr/AdvantageScopeXR/Constants.swift
@@ -0,0 +1,5 @@
+struct Constants {
+ static let nativeHostCompatibility = 0
+ static let qrPrefix = "https://appclip.apple.com/id?p=org.littletonrobotics.advantagescopexr.Clip&c="
+ static let serverPort = 56328
+}
diff --git a/xr/AdvantageScopeXR/ContentView.swift b/xr/AdvantageScopeXR/ContentView.swift
new file mode 100644
index 00000000..a57cdb9d
--- /dev/null
+++ b/xr/AdvantageScopeXR/ContentView.swift
@@ -0,0 +1,100 @@
+import SwiftUI
+import MetalKit
+
+class AppState : ObservableObject {
+ @Published var showControls = true
+ @Published var trackingReady = false
+ @Published var calibrationText = ""
+
+ #if APPCLIP
+ @Published var scanningQR = false
+ #else
+ @Published var scanningQR = true
+ #endif
+
+ @Published var serverIncompatibility: NativeHostIncompatibility = .none
+ @Published var serverAddresses: [String] = []
+ @Published var serverConnected = false
+}
+
+struct ContentView : View {
+ @StateObject private var appState = AppState()
+ @StateObject private var recordingPreviewState = RecordingPreviewState()
+ @State private var arManager = ARManager()
+
+ private let networking = Networking()
+ private let qrScanner = QRScanner()
+ private let webOverlay = WebOverlay()
+
+ var body: some View {
+ ARViewContainer(arManager: $arManager)
+ .ignoresSafeArea(.all)
+
+ // Web overlay
+ .overlay(
+ webOverlay
+ .ignoresSafeArea(.all)
+ .allowsHitTesting(false)
+ .opacity(showWebOverlay() ? 1 : 0)
+ .animation(.easeInOut(duration: 0.25), value: showWebOverlay())
+ )
+
+ // UI overlays
+ .safeAreaInset(edge: .top) {
+ ControlsMenu(requestCalibration: webOverlay.requestCalibration)
+ .environmentObject(recordingPreviewState)
+ }
+ .safeAreaInset(edge: .bottom) {
+ Banner()
+ }
+ .environmentObject(appState)
+
+ // Recording preview
+ .fullScreenCover(isPresented: $recordingPreviewState.showFullScreen) {
+ recordingPreviewState.view.ignoresSafeArea(.all)
+ }
+ .sheet(isPresented: $recordingPreviewState.showSheet) {
+ recordingPreviewState.view.ignoresSafeArea(.all)
+ }
+
+ // Event handling
+ .onTapGesture(coordinateSpace: .global) { location in
+ if (!appState.calibrationText.isEmpty && location.y > 150) {
+ webOverlay.userTap()
+ } else {
+ appState.showControls.toggle()
+ }
+ }
+ .onAppear() {
+ networking.start(appState, webOverlay)
+ arManager.appState = appState
+ arManager.webOverlay = webOverlay
+ arManager.addFrameCallback(qrScanner.processFrame)
+ webOverlay.messageHandler.arManager = arManager
+ qrScanner.start(appState)
+ }
+ .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) {activity in
+ if (activity.webpageURL != nil) {
+ qrScanner.parseURL(activity.webpageURL!)
+ }
+ }
+ }
+
+ private func showWebOverlay() -> Bool {
+ return !appState.scanningQR && appState.serverConnected
+ }
+}
+
+struct ARViewContainer: UIViewRepresentable {
+ @Binding var arManager: ARManager
+
+ func makeUIView(context: Context) -> MTKView {
+ return arManager.view
+ }
+
+ func updateUIView(_ uiView: MTKView, context: Context) {}
+}
+
+#Preview {
+ ContentView()
+}
diff --git a/xr/AdvantageScopeXR/ControlsMenu.swift b/xr/AdvantageScopeXR/ControlsMenu.swift
new file mode 100644
index 00000000..35a35f10
--- /dev/null
+++ b/xr/AdvantageScopeXR/ControlsMenu.swift
@@ -0,0 +1,46 @@
+import SwiftUI
+import ReplayKit
+
+struct ControlsMenu: View {
+ @EnvironmentObject var appState: AppState
+ let requestCalibration: () -> Void
+
+ var body: some View {
+ HStack {
+ #if !APPCLIP
+ Button("Scan", systemImage: "qrcode") {
+ appState.scanningQR.toggle()
+ }
+ .opacity(appState.scanningQR ? 0.5 : 1)
+ .animation(.easeInOut(duration: 0.1), value: appState.scanningQR)
+ #endif
+
+ RecordButton()
+
+ Button("Recalibrate", systemImage: "scope") {
+ requestCalibration()
+ }
+ }
+ .buttonStyle(ControlButton(highlight: false))
+ .padding()
+ .statusBarHidden(true)
+ .opacity(appState.showControls ? 1 : 0)
+ .animation(.easeInOut(duration: 0.25), value: appState.showControls)
+ }
+}
+
+struct ControlButton : ButtonStyle {
+ let highlight: Bool
+
+ func makeBody(configuration: Configuration) -> some View {
+ configuration.label
+ .padding(10)
+ .foregroundStyle(highlight ? Color.white : Color.primary)
+ .background(highlight ? AnyShapeStyle(Color.red) : AnyShapeStyle(.thinMaterial))
+ .clipShape(Capsule())
+ .controlSize(.large)
+ .opacity(configuration.isPressed ? 0.75 : 1)
+ .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
+ .animation(.none, value: highlight)
+ }
+}
diff --git a/xr/AdvantageScopeXR/Info.plist b/xr/AdvantageScopeXR/Info.plist
new file mode 100644
index 00000000..af8c3e45
--- /dev/null
+++ b/xr/AdvantageScopeXR/Info.plist
@@ -0,0 +1,15 @@
+
+
+
+
+ ITSAppUsesNonExemptEncryption
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoadsInWebContent
+
+ NSAllowsLocalNetworking
+
+
+
+
diff --git a/xr/AdvantageScopeXR/Networking.swift b/xr/AdvantageScopeXR/Networking.swift
new file mode 100644
index 00000000..884c7be4
--- /dev/null
+++ b/xr/AdvantageScopeXR/Networking.swift
@@ -0,0 +1,91 @@
+import Starscream
+import SwiftUI
+import Combine
+
+enum NativeHostIncompatibility {
+ case none
+ case serverTooOld
+ case serverTooNew
+}
+
+class Networking : WebSocketDelegate {
+ private var appState: AppState! = nil
+ private var webOverlay: WebOverlay! = nil
+ private var addressSubscriber: Cancellable?
+
+ private var socket: WebSocket?
+ private var reconnecting = false
+ private var currentServerAddress: String?
+ private var attemptCount = 0
+
+ private let maxQueuedCommands = 500
+ private var queuedCommands: [Data] = []
+
+ func start(_ appState: AppState, _ webOverlay: WebOverlay) {
+ self.appState = appState
+ self.webOverlay = webOverlay
+ addressSubscriber = appState.$serverAddresses.sink() {address in
+ // Force disconnect and reconnect to new address
+ // Always runs once at startup for initial connection
+ self.disconnected()
+ }
+ }
+
+ private func startConnection() {
+ if (appState.serverAddresses.isEmpty) {
+ disconnected()
+ return
+ }
+ attemptCount += 1
+ currentServerAddress = appState.serverAddresses[attemptCount % appState.serverAddresses.count]
+ var request = URLRequest(url: URL(string: "ws://" + currentServerAddress! + ":" + String(Constants.serverPort) + "/ws")!)
+ request.timeoutInterval = 0.5
+ socket = WebSocket(request: request)
+ socket?.delegate = self
+ socket?.connect()
+ }
+
+ private func connected() {
+ if (!appState.serverConnected) {
+ appState.serverConnected = true
+ webOverlay.load(currentServerAddress!)
+ queuedCommands = []
+ }
+ }
+
+ private func disconnected() {
+ if (appState.serverConnected) {
+ appState.serverConnected = false
+ }
+ if (reconnecting) {
+ return
+ }
+ reconnecting = true
+ self.socket?.forceDisconnect()
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+ self.reconnecting = false
+ self.startConnection()
+ }
+ }
+
+ func didReceive(event: WebSocketEvent, client: any WebSocketClient) {
+ switch event {
+ case .binary(let data):
+ if (webOverlay.isWebViewReady()) {
+ for command in queuedCommands {
+ webOverlay.setReceivedCommand(command, isQueued: true)
+ }
+ queuedCommands.removeAll()
+ webOverlay.setReceivedCommand(data, isQueued: false)
+ } else if (queuedCommands.count < maxQueuedCommands) {
+ queuedCommands.append(data)
+ }
+ case .connected:
+ connected()
+ case .disconnected, .cancelled, .error, .peerClosed:
+ disconnected()
+ case .text(_), .ping(_), .pong(_), .viabilityChanged(_), .reconnectSuggested(_):
+ break
+ }
+ }
+}
diff --git a/xr/AdvantageScopeXR/QRScanner.swift b/xr/AdvantageScopeXR/QRScanner.swift
new file mode 100644
index 00000000..567565d4
--- /dev/null
+++ b/xr/AdvantageScopeXR/QRScanner.swift
@@ -0,0 +1,82 @@
+import ARKit
+
+class QRScanner {
+ private var started = false
+ private var appState: AppState! = nil
+ private var processing = false
+
+ func start(_ appState: AppState) {
+ started = true
+ self.appState = appState
+ }
+
+ func processFrame(_ frame: ARFrame) {
+ if (!started || !appState.scanningQR || processing) {
+ return
+ }
+
+ let requestHandler = VNImageRequestHandler(cvPixelBuffer: frame.capturedImage)
+ processing = true
+ DispatchQueue.global(qos: .utility).async() {
+ let request = VNDetectBarcodesRequest()
+ request.symbologies = [.qr]
+ do {
+ try requestHandler.perform([request])
+ } catch {}
+
+ if (request.results != nil &&
+ !request.results!.isEmpty &&
+ request.results![0].payloadStringValue != nil) {
+ let value = request.results![0].payloadStringValue!
+ if (value.starts(with: Constants.qrPrefix)) {
+ self.parseURL(value)
+ }
+ }
+ self.processing = false
+ }
+ }
+
+ func parseURL(_ url: String) {
+ parseURL(NSURLComponents(string: url))
+ }
+
+ func parseURL(_ url: URL) {
+ parseURL(NSURLComponents(url: url, resolvingAgainstBaseURL: false))
+ }
+
+ private func parseURL(_ components: NSURLComponents?) {
+ // Parse components
+ var nativeHostCompatibility: Int? = nil
+ var addresses: [String] = []
+ components?.queryItems?.forEach {item in
+ if (item.value != nil) {
+ switch (item.name) {
+ case "c":
+ nativeHostCompatibility = Int(item.value!)
+ case "a":
+ addresses = item.value!.split(separator: "_").map{ String($0) }
+ default:
+ break
+ }
+ }
+ }
+
+ // Get server incompatibility value
+ if (nativeHostCompatibility == nil) {
+ return
+ }
+ var incompatibilityValue: NativeHostIncompatibility = .none
+ if (nativeHostCompatibility! < Constants.nativeHostCompatibility) {
+ incompatibilityValue = .serverTooOld
+ } else if (nativeHostCompatibility! > Constants.nativeHostCompatibility) {
+ incompatibilityValue = .serverTooNew
+ }
+
+ // Update app state
+ DispatchQueue.main.async() {
+ self.appState.scanningQR = false
+ self.appState.serverIncompatibility = incompatibilityValue
+ self.appState.serverAddresses = addresses
+ }
+ }
+}
diff --git a/xr/AdvantageScopeXR/RecordButton.swift b/xr/AdvantageScopeXR/RecordButton.swift
new file mode 100644
index 00000000..ad0a6dc2
--- /dev/null
+++ b/xr/AdvantageScopeXR/RecordButton.swift
@@ -0,0 +1,92 @@
+import SwiftUI
+import ReplayKit
+
+class RecordingPreviewState : ObservableObject {
+ @Published var view: RPPreviewView!
+ @Published var showFullScreen = false
+ @Published var showSheet = false
+}
+
+struct RecordButton: View {
+ @EnvironmentObject var appState: AppState
+ @EnvironmentObject var recordingPreviewState: RecordingPreviewState
+ @State private var recording = false
+
+ var body: some View {
+ Button("Record", systemImage: recording ? "stop.circle": "record.circle") {
+ // Immediately update button style and stop recording
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ recording.toggle()
+ if (!recording) {
+ stopRecording()
+ }
+ }
+
+ // When starting, automatically hide controls for clean UI
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+ if (recording) {
+ appState.showControls = false
+ }
+ }
+
+ // After controls are hidden, start recording
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.55) {
+ if (recording) {
+ startRecording()
+ }
+ }
+ }
+ .buttonStyle(ControlButton(highlight: recording))
+ .animation(.none, value: recording)
+ }
+
+ private func startRecording() {
+ RPScreenRecorder.shared().startRecording { error in
+ if (error != nil) {
+ recording = false
+ }
+ }
+ }
+
+ private func stopRecording() {
+ RPScreenRecorder.shared().stopRecording { preview, error in
+ guard let preview = preview else { return }
+ recordingPreviewState.view = RPPreviewView(controller: preview, showFullScreen: $recordingPreviewState.showFullScreen, showSheet: $recordingPreviewState.showSheet)
+ if (UIDevice.current.userInterfaceIdiom == .pad) {
+ recordingPreviewState.showSheet = true
+ } else {
+ recordingPreviewState.showFullScreen = true
+ }
+ }
+ }
+}
+
+struct RPPreviewView: UIViewControllerRepresentable {
+ let controller: RPPreviewViewController
+ @Binding var showFullScreen: Bool
+ @Binding var showSheet: Bool
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ func makeUIViewController(context: Context) -> RPPreviewViewController {
+ controller.previewControllerDelegate = context.coordinator
+ return controller
+ }
+
+ func updateUIViewController(_ uiView: RPPreviewViewController, context: Context) {}
+
+ class Coordinator: NSObject, RPPreviewViewControllerDelegate {
+ var parent: RPPreviewView
+
+ init(_ parent: RPPreviewView) {
+ self.parent = parent
+ }
+
+ func previewControllerDidFinish(_ previewController: RPPreviewViewController) {
+ parent.showFullScreen = false
+ parent.showSheet = false
+ }
+ }
+}
diff --git a/xr/AdvantageScopeXR/WebOverlay.swift b/xr/AdvantageScopeXR/WebOverlay.swift
new file mode 100644
index 00000000..b5c9365d
--- /dev/null
+++ b/xr/AdvantageScopeXR/WebOverlay.swift
@@ -0,0 +1,99 @@
+import SwiftUI
+import WebKit
+
+struct WebOverlay: UIViewRepresentable {
+ @EnvironmentObject var appState: AppState
+ private var webView: WKWebView!
+ let messageHandler = ScriptMessageHandler()
+
+ init() {
+ let contentController = WKUserContentController()
+ contentController.add(messageHandler, name: "asxr")
+ let config = WKWebViewConfiguration()
+ config.userContentController = contentController
+
+ webView = WKWebView(frame: CGRect(), configuration: config)
+ webView.isOpaque = false
+ webView.backgroundColor = UIColor.clear
+ webView.scrollView.backgroundColor = UIColor.clear
+ webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+
+ // Enable remote inspection with Safari for debug builds
+ #if DEBUG
+ if webView.responds(to: Selector(("setInspectable:"))) {
+ webView.perform(Selector(("setInspectable:")), with: true)
+ }
+ #endif
+ }
+
+ func makeUIView(context: Context) -> WKWebView {
+ messageHandler.appState = appState
+ return webView;
+ }
+
+ func updateUIView(_ uiView: WKWebView, context: Context) {}
+
+ func load(_ serverAddress: String) {
+ let url = URL(string: "http://" + serverAddress + ":" + String(Constants.serverPort))
+ if (url != nil) {
+ webView.load(URLRequest(url: url!))
+ }
+ }
+
+ func isWebViewReady() -> Bool {
+ return webView.url != nil && !webView.isLoading
+ }
+
+ // MARK: - JS Outgoing Messages
+
+ func setReceivedCommand(_ data: Data, isQueued: Bool) {
+ guard (isWebViewReady()) else { return }
+ let base64Data = data.base64EncodedString()
+ webView.evaluateJavaScript("setCommand(\"\(base64Data)\", \(isQueued ? "true" : "false"))")
+ }
+
+ func render(_ data: Dictionary) {
+ guard (isWebViewReady()) else { return }
+ do {
+ let json = try JSONSerialization.data(withJSONObject: data, options: JSONSerialization.WritingOptions(rawValue: 0))
+ let jsonData = NSString(data: json, encoding: String.Encoding.utf8.rawValue)!
+ webView.evaluateJavaScript("render(\(jsonData))")
+ } catch {
+ print("Failed to serialize JSON")
+ }
+ }
+
+ func requestCalibration() {
+ guard (isWebViewReady()) else { return }
+ webView.evaluateJavaScript("requestCalibration()")
+ }
+
+ func userTap() {
+ guard (isWebViewReady()) else { return }
+ webView.evaluateJavaScript("userTap()")
+ }
+}
+
+class ScriptMessageHandler: NSObject, WKScriptMessageHandler {
+ var appState: AppState? = nil
+ var arManager: ARManager? = nil
+
+ func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+ guard (message.name == "asxr") else { return }
+ let messageBody = message.body as! NSDictionary
+ let name = messageBody["name"] as! String
+ let data = messageBody["data"]
+
+ switch (name) {
+ case "setCalibrationText":
+ guard (appState != nil) else { return }
+ appState!.calibrationText = data as! String
+ case "showControls":
+ appState!.showControls = data as! Bool
+ case "recalibrate":
+ arManager?.recalibrate()
+ default:
+ break
+ }
+ }
+}
diff --git a/xr/AdvantageScopeXRClip/AdvantageScopeXRClip.entitlements b/xr/AdvantageScopeXRClip/AdvantageScopeXRClip.entitlements
new file mode 100644
index 00000000..3830f04e
--- /dev/null
+++ b/xr/AdvantageScopeXRClip/AdvantageScopeXRClip.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.developer.parent-application-identifiers
+
+ $(AppIdentifierPrefix)org.littletonrobotics.advantagescopexr
+
+
+
diff --git a/xr/AdvantageScopeXRClip/Info.plist b/xr/AdvantageScopeXRClip/Info.plist
new file mode 100644
index 00000000..cfcefb70
--- /dev/null
+++ b/xr/AdvantageScopeXRClip/Info.plist
@@ -0,0 +1,29 @@
+
+
+
+
+ ITSAppUsesNonExemptEncryption
+
+ NSAppClip
+
+ NSAppClipRequestEphemeralUserNotification
+
+ NSAppClipRequestLocationConfirmation
+
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoadsInWebContent
+
+ NSAllowsLocalNetworking
+
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+
+
+