mirror of
https://github.com/m5stack/StackChan.git
synced 2026-04-29 20:04:23 +00:00
app code 4/27
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,60 @@
|
||||
source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git'
|
||||
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '16.6'
|
||||
|
||||
$iOSVersion = '16.6'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.build_configurations.each do |config|
|
||||
config.build_settings["EXCLUDED_ARCHS[sdk=*]"] = "armv7"
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = $iOSVersion
|
||||
end
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
if Gem::Version.new($iOSVersion) > Gem::Version.new(config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'])
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = $iOSVersion
|
||||
end
|
||||
end
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,867 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0E2BFCBD2F2C51FE00E21BCA /* StackChanRobot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2BFCBC2F2C51F600E21BCA /* StackChanRobot.swift */; };
|
||||
0E2BFCBF2F2C525E00E21BCA /* ViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2BFCBE2F2C525400E21BCA /* ViewFactory.swift */; };
|
||||
0E2BFCC32F2C566600E21BCA /* DanceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2BFCC22F2C566600E21BCA /* DanceList.swift */; };
|
||||
0E2BFCC52F2C566B00E21BCA /* ExpressionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2BFCC42F2C566B00E21BCA /* ExpressionData.swift */; };
|
||||
0E2BFCC82F2C59AD00E21BCA /* Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2BFCC72F2C59AD00E21BCA /* Extension.swift */; };
|
||||
0E2BFD962F2C908200E21BCA /* StackChanRotaryRobot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2BFD952F2C907100E21BCA /* StackChanRotaryRobot.swift */; };
|
||||
0E319DC52F504DD600B8C136 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E319DC42F504DD600B8C136 /* SceneDelegate.swift */; };
|
||||
0E49C11D2F349BB300419137 /* StackChanArView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E49C11C2F349B9B00419137 /* StackChanArView.swift */; };
|
||||
0EF7F6292F2B40D7008CDEB0 /* NativeBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF7F6282F2B40C6008CDEB0 /* NativeBridge.swift */; };
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
ABF1068D156BACD9687833B5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61533E6EB1B159D304737D3A /* Pods_RunnerTests.framework */; };
|
||||
FEAAEB50E782E6F211E46BE8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69CC735B711452B07B9BC3CD /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
014B92591A4905CFCFF58C45 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
0E2BFCBC2F2C51F600E21BCA /* StackChanRobot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackChanRobot.swift; sourceTree = "<group>"; };
|
||||
0E2BFCBE2F2C525400E21BCA /* ViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewFactory.swift; sourceTree = "<group>"; };
|
||||
0E2BFCC22F2C566600E21BCA /* DanceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DanceList.swift; sourceTree = "<group>"; };
|
||||
0E2BFCC42F2C566B00E21BCA /* ExpressionData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpressionData.swift; sourceTree = "<group>"; };
|
||||
0E2BFCC72F2C59AD00E21BCA /* Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extension.swift; sourceTree = "<group>"; };
|
||||
0E2BFD952F2C907100E21BCA /* StackChanRotaryRobot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackChanRotaryRobot.swift; sourceTree = "<group>"; };
|
||||
0E319DC42F504DD600B8C136 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
0E49C11C2F349B9B00419137 /* StackChanArView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackChanArView.swift; sourceTree = "<group>"; };
|
||||
0EF7F6282F2B40C6008CDEB0 /* NativeBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeBridge.swift; sourceTree = "<group>"; };
|
||||
0EF7F7242F2B4498008CDEB0 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
61533E6EB1B159D304737D3A /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
69CC735B711452B07B9BC3CD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
83DB239E5DAC7976B8DC7B7A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
86F9532C8B82DB90BB2B967F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
8D86E24B4CB62314C14A6BF4 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; explicitFileType = text.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
99E681E7B6E33A054304AA9A /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
D9756F4DAC76C779F6DADE83 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
0E2BFD5D2F2C6AD200E21BCA /* Exceptions for "3DModel" folder in "Runner" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
StackChanModel.scn,
|
||||
);
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
};
|
||||
0E2BFD5E2F2C6AD200E21BCA /* Exceptions for "3DModel" folder in "RunnerTests" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
StackChanModel.scn,
|
||||
);
|
||||
target = 331C8080294A63A400263BE5 /* RunnerTests */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
0E2BFCC12F2C538400E21BCA /* 3DModel */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
0E2BFD5D2F2C6AD200E21BCA /* Exceptions for "3DModel" folder in "Runner" target */,
|
||||
0E2BFD5E2F2C6AD200E21BCA /* Exceptions for "3DModel" folder in "RunnerTests" target */,
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = 3DModel;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
15389107E87A87969889C15C /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
ABF1068D156BACD9687833B5 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FEAAEB50E782E6F211E46BE8 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
0E2BFCBB2F2C51EB00E21BCA /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0E49C11C2F349B9B00419137 /* StackChanArView.swift */,
|
||||
0E2BFD952F2C907100E21BCA /* StackChanRotaryRobot.swift */,
|
||||
0E2BFCBE2F2C525400E21BCA /* ViewFactory.swift */,
|
||||
0E2BFCBC2F2C51F600E21BCA /* StackChanRobot.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0E2BFCC62F2C599E00E21BCA /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0E2BFCC72F2C59AD00E21BCA /* Extension.swift */,
|
||||
0EF7F6282F2B40C6008CDEB0 /* NativeBridge.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0EF7F6242F2B3D35008CDEB0 /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0E2BFCC42F2C566B00E21BCA /* ExpressionData.swift */,
|
||||
0E2BFCC22F2C566600E21BCA /* DanceList.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6A84775745706C15EDCCC2AC /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
69CC735B711452B07B9BC3CD /* Pods_Runner.framework */,
|
||||
61533E6EB1B159D304737D3A /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
F9C6E422CF624A714D6A795D /* Pods */,
|
||||
6A84775745706C15EDCCC2AC /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0E319DC42F504DD600B8C136 /* SceneDelegate.swift */,
|
||||
0E2BFCC62F2C599E00E21BCA /* Utils */,
|
||||
0E2BFCC12F2C538400E21BCA /* 3DModel */,
|
||||
0E2BFCBB2F2C51EB00E21BCA /* View */,
|
||||
0EF7F7242F2B4498008CDEB0 /* Runner.entitlements */,
|
||||
0EF7F6242F2B3D35008CDEB0 /* Model */,
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F9C6E422CF624A714D6A795D /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
86F9532C8B82DB90BB2B967F /* Pods-Runner.debug.xcconfig */,
|
||||
014B92591A4905CFCFF58C45 /* Pods-Runner.release.xcconfig */,
|
||||
83DB239E5DAC7976B8DC7B7A /* Pods-Runner.profile.xcconfig */,
|
||||
D9756F4DAC76C779F6DADE83 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
99E681E7B6E33A054304AA9A /* Pods-RunnerTests.release.xcconfig */,
|
||||
8D86E24B4CB62314C14A6BF4 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
06679B67EF6ED2EE02C6AE1F /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
15389107E87A87969889C15C /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
6B1F6EB6486B9A29A8A7AE57 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
BCD9052275896B9385CAEC4A /* [CP] Embed Pods Frameworks */,
|
||||
62354403C2A37F66E2DA6570 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
06679B67EF6ED2EE02C6AE1F /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n";
|
||||
};
|
||||
62354403C2A37F66E2DA6570 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
6B1F6EB6486B9A29A8A7AE57 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "export PATH=\"/opt/homebrew/bin:$PATH\"\nexport CMAKE=\"/opt/homebrew/bin/cmake\"\nexport FLUTTER_BUILD_NATIVE_ASSETS=\"false\"\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
|
||||
};
|
||||
BCD9052275896B9385CAEC4A /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0E2BFCC82F2C59AD00E21BCA /* Extension.swift in Sources */,
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
0E2BFCC52F2C566B00E21BCA /* ExpressionData.swift in Sources */,
|
||||
0E2BFCBF2F2C525E00E21BCA /* ViewFactory.swift in Sources */,
|
||||
0E2BFD962F2C908200E21BCA /* StackChanRotaryRobot.swift in Sources */,
|
||||
0E49C11D2F349BB300419137 /* StackChanArView.swift in Sources */,
|
||||
0EF7F6292F2B40D7008CDEB0 /* NativeBridge.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
0E2BFCC32F2C566600E21BCA /* DanceList.swift in Sources */,
|
||||
0E2BFCBD2F2C51FE00E21BCA /* StackChanRobot.swift in Sources */,
|
||||
0E319DC52F504DD600B8C136 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = 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_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_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
EXCLUDED_ARCHS = "";
|
||||
"EXCLUDED_ARCHS[sdk=*]" = armv7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
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 = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
DEVELOPMENT_TEAM = NG678HLKHZ;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
"$(PROJECT_DIR)/Runner",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.m5stack.StackChan;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D9756F4DAC76C779F6DADE83 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.m5stack.stackchan.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 99E681E7B6E33A054304AA9A /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.m5stack.stackchan.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 8D86E24B4CB62314C14A6BF4 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.m5stack.stackchan.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = 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_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_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
EXCLUDED_ARCHS = "";
|
||||
"EXCLUDED_ARCHS[sdk=*]" = armv7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
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 = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = 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_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_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
EXCLUDED_ARCHS = "";
|
||||
"EXCLUDED_ARCHS[sdk=*]" = armv7;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
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 = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = NG678HLKHZ;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
"$(PROJECT_DIR)/Runner",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.m5stack.StackChan;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
DEVELOPMENT_TEAM = NG678HLKHZ;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
"$(PROJECT_DIR)/Runner",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.m5stack.StackChan;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C8088294A63A400263BE5 /* Debug */,
|
||||
331C8089294A63A400263BE5 /* Release */,
|
||||
331C808A294A63A400263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
import Flutter
|
||||
import CoreLocation
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate,FlutterImplicitEngineDelegate {
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app_logo.jpg",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
@@ -0,0 +1,5 @@
|
||||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24504"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-255" y="7"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GeneratedPluginRegistrant_h
|
||||
#define GeneratedPluginRegistrant_h
|
||||
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface GeneratedPluginRegistrant : NSObject
|
||||
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
#endif /* GeneratedPluginRegistrant_h */
|
||||
@@ -0,0 +1,154 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
|
||||
#if __has_include(<audio_session/AudioSessionPlugin.h>)
|
||||
#import <audio_session/AudioSessionPlugin.h>
|
||||
#else
|
||||
@import audio_session;
|
||||
#endif
|
||||
|
||||
#if __has_include(<camera_avfoundation/CameraPlugin.h>)
|
||||
#import <camera_avfoundation/CameraPlugin.h>
|
||||
#else
|
||||
@import camera_avfoundation;
|
||||
#endif
|
||||
|
||||
#if __has_include(<ffmpeg_kit_flutter_new/FFmpegKitFlutterPlugin.h>)
|
||||
#import <ffmpeg_kit_flutter_new/FFmpegKitFlutterPlugin.h>
|
||||
#else
|
||||
@import ffmpeg_kit_flutter_new;
|
||||
#endif
|
||||
|
||||
#if __has_include(<file_picker/FilePickerPlugin.h>)
|
||||
#import <file_picker/FilePickerPlugin.h>
|
||||
#else
|
||||
@import file_picker;
|
||||
#endif
|
||||
|
||||
#if __has_include(<flutter_angle/FlutterAnglePlugin.h>)
|
||||
#import <flutter_angle/FlutterAnglePlugin.h>
|
||||
#else
|
||||
@import flutter_angle;
|
||||
#endif
|
||||
|
||||
#if __has_include(<flutter_blue_plus_darwin/FlutterBluePlusPlugin.h>)
|
||||
#import <flutter_blue_plus_darwin/FlutterBluePlusPlugin.h>
|
||||
#else
|
||||
@import flutter_blue_plus_darwin;
|
||||
#endif
|
||||
|
||||
#if __has_include(<geolocator_apple/GeolocatorPlugin.h>)
|
||||
#import <geolocator_apple/GeolocatorPlugin.h>
|
||||
#else
|
||||
@import geolocator_apple;
|
||||
#endif
|
||||
|
||||
#if __has_include(<google_mlkit_commons/GoogleMlKitCommonsPlugin.h>)
|
||||
#import <google_mlkit_commons/GoogleMlKitCommonsPlugin.h>
|
||||
#else
|
||||
@import google_mlkit_commons;
|
||||
#endif
|
||||
|
||||
#if __has_include(<google_mlkit_face_detection/GoogleMlKitFaceDetectionPlugin.h>)
|
||||
#import <google_mlkit_face_detection/GoogleMlKitFaceDetectionPlugin.h>
|
||||
#else
|
||||
@import google_mlkit_face_detection;
|
||||
#endif
|
||||
|
||||
#if __has_include(<just_audio/JustAudioPlugin.h>)
|
||||
#import <just_audio/JustAudioPlugin.h>
|
||||
#else
|
||||
@import just_audio;
|
||||
#endif
|
||||
|
||||
#if __has_include(<mobile_scanner/MobileScannerPlugin.h>)
|
||||
#import <mobile_scanner/MobileScannerPlugin.h>
|
||||
#else
|
||||
@import mobile_scanner;
|
||||
#endif
|
||||
|
||||
#if __has_include(<music_feature_analyzer/MusicFeatureAnalyzerPlugin.h>)
|
||||
#import <music_feature_analyzer/MusicFeatureAnalyzerPlugin.h>
|
||||
#else
|
||||
@import music_feature_analyzer;
|
||||
#endif
|
||||
|
||||
#if __has_include(<network_info_plus/FPPNetworkInfoPlusPlugin.h>)
|
||||
#import <network_info_plus/FPPNetworkInfoPlusPlugin.h>
|
||||
#else
|
||||
@import network_info_plus;
|
||||
#endif
|
||||
|
||||
#if __has_include(<opus_codec_ios/OpusFlutterIosPlugin.h>)
|
||||
#import <opus_codec_ios/OpusFlutterIosPlugin.h>
|
||||
#else
|
||||
@import opus_codec_ios;
|
||||
#endif
|
||||
|
||||
#if __has_include(<package_info_plus/FPPPackageInfoPlusPlugin.h>)
|
||||
#import <package_info_plus/FPPPackageInfoPlusPlugin.h>
|
||||
#else
|
||||
@import package_info_plus;
|
||||
#endif
|
||||
|
||||
#if __has_include(<permission_handler_apple/PermissionHandlerPlugin.h>)
|
||||
#import <permission_handler_apple/PermissionHandlerPlugin.h>
|
||||
#else
|
||||
@import permission_handler_apple;
|
||||
#endif
|
||||
|
||||
#if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>)
|
||||
#import <shared_preferences_foundation/SharedPreferencesPlugin.h>
|
||||
#else
|
||||
@import shared_preferences_foundation;
|
||||
#endif
|
||||
|
||||
#if __has_include(<three_js_sensors/TJSSensorsPlugin.h>)
|
||||
#import <three_js_sensors/TJSSensorsPlugin.h>
|
||||
#else
|
||||
@import three_js_sensors;
|
||||
#endif
|
||||
|
||||
#if __has_include(<video_player_avfoundation/FVPVideoPlayerPlugin.h>)
|
||||
#import <video_player_avfoundation/FVPVideoPlayerPlugin.h>
|
||||
#else
|
||||
@import video_player_avfoundation;
|
||||
#endif
|
||||
|
||||
#if __has_include(<webview_flutter_wkwebview/WebViewFlutterPlugin.h>)
|
||||
#import <webview_flutter_wkwebview/WebViewFlutterPlugin.h>
|
||||
#else
|
||||
@import webview_flutter_wkwebview;
|
||||
#endif
|
||||
|
||||
@implementation GeneratedPluginRegistrant
|
||||
|
||||
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
||||
[AudioSessionPlugin registerWithRegistrar:[registry registrarForPlugin:@"AudioSessionPlugin"]];
|
||||
[CameraPlugin registerWithRegistrar:[registry registrarForPlugin:@"CameraPlugin"]];
|
||||
[FFmpegKitFlutterPlugin registerWithRegistrar:[registry registrarForPlugin:@"FFmpegKitFlutterPlugin"]];
|
||||
[FilePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FilePickerPlugin"]];
|
||||
[FlutterAnglePlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterAnglePlugin"]];
|
||||
[FlutterBluePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterBluePlusPlugin"]];
|
||||
[GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]];
|
||||
[GoogleMlKitCommonsPlugin registerWithRegistrar:[registry registrarForPlugin:@"GoogleMlKitCommonsPlugin"]];
|
||||
[GoogleMlKitFaceDetectionPlugin registerWithRegistrar:[registry registrarForPlugin:@"GoogleMlKitFaceDetectionPlugin"]];
|
||||
[JustAudioPlugin registerWithRegistrar:[registry registrarForPlugin:@"JustAudioPlugin"]];
|
||||
[MobileScannerPlugin registerWithRegistrar:[registry registrarForPlugin:@"MobileScannerPlugin"]];
|
||||
[MusicFeatureAnalyzerPlugin registerWithRegistrar:[registry registrarForPlugin:@"MusicFeatureAnalyzerPlugin"]];
|
||||
[FPPNetworkInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPNetworkInfoPlusPlugin"]];
|
||||
[OpusFlutterIosPlugin registerWithRegistrar:[registry registrarForPlugin:@"OpusFlutterIosPlugin"]];
|
||||
[FPPPackageInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPPackageInfoPlusPlugin"]];
|
||||
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
|
||||
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
|
||||
[TJSSensorsPlugin registerWithRegistrar:[registry registrarForPlugin:@"TJSSensorsPlugin"]];
|
||||
[FVPVideoPlayerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FVPVideoPlayerPlugin"]];
|
||||
[WebViewFlutterPlugin registerWithRegistrar:[registry registrarForPlugin:@"WebViewFlutterPlugin"]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>StackChan World</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.m5stack.StackChan</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>stackchan</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.1.4</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>11</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>The Bluetooth permission is required for searching and connecting to the StackChan device.</string>
|
||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||
<string>The Bluetooth permission is required for searching and connecting to the StackChan device.</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_stackchan-mpc._tcp</string>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We need access to the camera to take photos and record videos.</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>This app requires access to the local network to communicate with devices.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>We need access to the microphone to capture audio data.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Save the photo to the album</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We need access to the photo library to save your photos and videos.</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
<string>bluetooth-le</string>
|
||||
<string>arkit</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>com.apple.developer.networking.wifi-info</key>
|
||||
<true/>
|
||||
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>You need to grant location permission to view the name of the WiFi you are currently connected to.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>You need to grant location permission to view the name of the WiFi you are currently connected to.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// Dance.swift
|
||||
// StackChan
|
||||
//
|
||||
// Created by 袁智鸿 on 2026/1/16.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct DanceList: Codable, Identifiable {
|
||||
var danceData: [DanceData]?
|
||||
var danceIndex: Int?
|
||||
var danceName: String?
|
||||
|
||||
var id: String = UUID().uuidString
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case danceData
|
||||
case danceIndex
|
||||
case danceName
|
||||
}
|
||||
}
|
||||
|
||||
struct DanceData : Codable,Identifiable {
|
||||
var leftEye: ExpressionItem // Left eye, default weight = 100
|
||||
var rightEye: ExpressionItem // Right eye, default weight = 100
|
||||
var mouth: ExpressionItem // Mouth, default weight = 0
|
||||
var yawServo: MotionDataItem // Yaw rotation, angle range (-1280 ~ 1280), default 0
|
||||
var pitchServo: MotionDataItem // Pitch movement, angle range (0 ~ 900), default 0
|
||||
|
||||
var leftRgbColor: String = "#00000000"
|
||||
var rightRgbColor: String = "#00000000"
|
||||
|
||||
var durationMs: Int // Duration in milliseconds, default 1000
|
||||
var id: String = UUID().uuidString
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case leftEye, rightEye, mouth, yawServo, pitchServo, leftRgbColor, rightRgbColor, durationMs
|
||||
}
|
||||
|
||||
static func from(jsonString: String) -> DanceData? {
|
||||
guard !jsonString.isEmpty else {
|
||||
print("JSON string is empty")
|
||||
return nil
|
||||
}
|
||||
guard let jsonData = jsonString.data(using: .utf8) else {
|
||||
print("Failed to convert string to UTF-8 data")
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
let danceData = try decoder.decode(DanceData.self, from: jsonData)
|
||||
return danceData
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
leftEye = try container.decode(ExpressionItem.self, forKey: .leftEye)
|
||||
rightEye = try container.decode(ExpressionItem.self, forKey: .rightEye)
|
||||
mouth = try container.decode(ExpressionItem.self, forKey: .mouth)
|
||||
yawServo = try container.decode(MotionDataItem.self, forKey: .yawServo)
|
||||
pitchServo = try container.decode(MotionDataItem.self, forKey: .pitchServo)
|
||||
leftRgbColor = try container.decodeIfPresent(String.self, forKey: .leftRgbColor) ?? "#00000000"
|
||||
rightRgbColor = try container.decodeIfPresent(String.self, forKey: .rightRgbColor) ?? "#00000000"
|
||||
durationMs = try container.decode(Int.self, forKey: .durationMs)
|
||||
id = UUID().uuidString
|
||||
}
|
||||
|
||||
init(
|
||||
leftEye: ExpressionItem,
|
||||
rightEye: ExpressionItem,
|
||||
mouth: ExpressionItem,
|
||||
yawServo: MotionDataItem,
|
||||
pitchServo: MotionDataItem,
|
||||
leftRgbColor: String = "#00000000",
|
||||
rightRgbColor: String = "#00000000",
|
||||
durationMs: Int,
|
||||
id: String = UUID().uuidString
|
||||
) {
|
||||
self.leftEye = leftEye
|
||||
self.rightEye = rightEye
|
||||
self.mouth = mouth
|
||||
self.yawServo = yawServo
|
||||
self.pitchServo = pitchServo
|
||||
self.leftRgbColor = leftRgbColor
|
||||
self.rightRgbColor = rightRgbColor
|
||||
self.durationMs = durationMs
|
||||
self.id = id
|
||||
}
|
||||
|
||||
func copy() -> DanceData {
|
||||
DanceData(
|
||||
leftEye: self.leftEye.copy(),
|
||||
rightEye: self.rightEye.copy(),
|
||||
mouth: self.mouth.copy(),
|
||||
yawServo: self.yawServo.copy(),
|
||||
pitchServo: self.pitchServo.copy(),
|
||||
leftRgbColor: self.leftRgbColor,
|
||||
rightRgbColor: self.rightRgbColor,
|
||||
durationMs: self.durationMs,
|
||||
id: UUID().uuidString
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ExpressionData : Codable {
|
||||
var type: String = "bleAvatar"
|
||||
var leftEye: ExpressionItem
|
||||
var rightEye: ExpressionItem
|
||||
var mouth: ExpressionItem
|
||||
}
|
||||
|
||||
struct ExpressionItem : Codable {
|
||||
var x: Int = 0
|
||||
var y: Int = 0
|
||||
var rotation: Int = 0
|
||||
var weight: Int = 0
|
||||
var size: Int = 0
|
||||
|
||||
func copy() -> ExpressionItem {
|
||||
ExpressionItem(
|
||||
x: self.x,
|
||||
y: self.y,
|
||||
rotation: self.rotation,
|
||||
weight: self.weight,
|
||||
size: self.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct MotionData : Codable {
|
||||
var type: String = "bleMotion"
|
||||
var pitchServo: MotionDataItem
|
||||
var yawServo: MotionDataItem
|
||||
|
||||
func toJsonString() -> String {
|
||||
let encoder = JSONEncoder()
|
||||
if let jsonData = try? encoder.encode(self),
|
||||
let jsonString = String(data: jsonData, encoding: .utf8) {
|
||||
return jsonString
|
||||
}
|
||||
return "{}"
|
||||
}
|
||||
}
|
||||
|
||||
struct MotionDataItem: Codable {
|
||||
var angle: Int = 0
|
||||
var speed: Int = 500
|
||||
var rotate: Int = 0
|
||||
|
||||
init() {
|
||||
self.angle = 0
|
||||
self.speed = 500
|
||||
self.rotate = 0
|
||||
}
|
||||
|
||||
init(angle: Int, speed: Int = 500) {
|
||||
self.angle = angle
|
||||
self.speed = speed
|
||||
self.rotate = 0
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case angle
|
||||
case speed
|
||||
case rotate
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
if angle != 0 {
|
||||
try container.encode(angle, forKey: .angle)
|
||||
try container.encode(speed, forKey: .speed)
|
||||
} else if rotate != 0 {
|
||||
try container.encode(rotate, forKey: .rotate)
|
||||
try container.encode(speed, forKey: .speed)
|
||||
} else {
|
||||
try container.encode(angle, forKey: .angle)
|
||||
try container.encode(speed, forKey: .speed)
|
||||
}
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.angle = try container.decodeIfPresent(Int.self, forKey: .angle) ?? 0
|
||||
self.speed = try container.decodeIfPresent(Int.self, forKey: .speed) ?? 500
|
||||
self.rotate = try container.decodeIfPresent(Int.self, forKey: .rotate) ?? 0
|
||||
}
|
||||
|
||||
func copy() -> MotionDataItem {
|
||||
MotionDataItem(
|
||||
angle: self.angle,
|
||||
speed: self.speed,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.networking.wifi-info</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,79 @@
|
||||
import Flutter
|
||||
import NetworkExtension
|
||||
import CoreLocation
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: FlutterSceneDelegate,CLLocationManagerDelegate {
|
||||
|
||||
private let locationManager = CLLocationManager()
|
||||
|
||||
override func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
if let controller = window?.rootViewController as? FlutterViewController {
|
||||
NativeBridge.shared.setup(with: controller)
|
||||
NativeBridge.shared.setMethodCallHandler { [weak self] call, result in
|
||||
self?.handleMethodCall(call: call, result: result)
|
||||
}
|
||||
registerNativeViews(with: controller)
|
||||
}
|
||||
locationManager.delegate = self
|
||||
}
|
||||
|
||||
private func registerNativeViews(with controller: FlutterViewController) {
|
||||
guard let registrar = controller.registrar(forPlugin: "stack_chan") else {
|
||||
return
|
||||
}
|
||||
let robotFactory =
|
||||
StackChanRobotViewFactory(messenger: controller.binaryMessenger)
|
||||
registrar.register(robotFactory, withId: "stackchan_robot_view")
|
||||
|
||||
let rotaryFactory =
|
||||
StackChanRotaryRobotViewFactory(messenger: controller.binaryMessenger)
|
||||
registrar.register(rotaryFactory, withId: "stackchan_rotary_robot_view")
|
||||
|
||||
let arViewFactory = StackChanArViewFactory(messenger: controller.binaryMessenger)
|
||||
registrar.register(arViewFactory, withId: "stackchan_ar_view")
|
||||
}
|
||||
|
||||
private func handleMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
let method = Method.fromString(call.method)
|
||||
switch method {
|
||||
case .wifiName:
|
||||
getWifiName()
|
||||
case .stopPlayPCM:
|
||||
NativeBridge.shared.stopPlayPCM()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func getWifiName() {
|
||||
switch locationManager.authorizationStatus {
|
||||
case .authorizedWhenInUse, .authorizedAlways:
|
||||
fetchWifiInfo()
|
||||
break
|
||||
case .denied, .restricted:
|
||||
break
|
||||
case .notDetermined:
|
||||
locationManager.requestWhenInUseAuthorization()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchWifiInfo() {
|
||||
NEHotspotNetwork.fetchCurrent { network in
|
||||
if let wifiName = network?.ssid {
|
||||
NativeBridge.shared.sendMessage(method: .wifiName, wifiName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
print(manager.authorizationStatus)
|
||||
if manager.authorizationStatus == .authorizedWhenInUse ||
|
||||
manager.authorizationStatus == .authorizedAlways {
|
||||
fetchWifiInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import simd
|
||||
|
||||
extension UIApplication {
|
||||
func endEditing() {
|
||||
sendAction(#selector(UIResponder.resignFirstResponder),
|
||||
to: nil, from: nil, for: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Color {
|
||||
init?(hex: String) {
|
||||
var hexString = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||
if hexString.hasPrefix("#") {
|
||||
hexString.removeFirst()
|
||||
}
|
||||
|
||||
let r, g, b, a: Double
|
||||
|
||||
switch hexString.count {
|
||||
case 3: // RGB
|
||||
let chars = Array(hexString)
|
||||
r = Double(strtoul(String([chars[0], chars[0]]), nil, 16)) / 255
|
||||
g = Double(strtoul(String([chars[1], chars[1]]), nil, 16)) / 255
|
||||
b = Double(strtoul(String([chars[2], chars[2]]), nil, 16)) / 255
|
||||
a = 1.0
|
||||
|
||||
case 4: // RGBA
|
||||
let chars = Array(hexString)
|
||||
r = Double(strtoul(String([chars[0], chars[0]]), nil, 16)) / 255
|
||||
g = Double(strtoul(String([chars[1], chars[1]]), nil, 16)) / 255
|
||||
b = Double(strtoul(String([chars[2], chars[2]]), nil, 16)) / 255
|
||||
a = Double(strtoul(String([chars[3], chars[3]]), nil, 16)) / 255
|
||||
|
||||
case 6: // RRGGBB
|
||||
var value: UInt64 = 0
|
||||
Scanner(string: hexString).scanHexInt64(&value)
|
||||
r = Double((value & 0xFF0000) >> 16) / 255
|
||||
g = Double((value & 0x00FF00) >> 8) / 255
|
||||
b = Double(value & 0x0000FF) / 255
|
||||
a = 1.0
|
||||
|
||||
case 8: // AARRGGBB
|
||||
var value: UInt64 = 0
|
||||
Scanner(string: hexString).scanHexInt64(&value)
|
||||
a = Double((value & 0xFF000000) >> 24) / 255
|
||||
r = Double((value & 0x00FF0000) >> 16) / 255
|
||||
g = Double((value & 0x0000FF00) >> 8) / 255
|
||||
b = Double(value & 0x000000FF) / 255
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
self.init(red: r, green: g, blue: b, opacity: a)
|
||||
}
|
||||
|
||||
func toHex() -> String {
|
||||
let uiColor = UIColor(self)
|
||||
var r: CGFloat = 0
|
||||
var g: CGFloat = 0
|
||||
var b: CGFloat = 0
|
||||
var a: CGFloat = 0
|
||||
guard uiColor.getRed(&r, green: &g, blue: &b, alpha: &a) else { return "#00000000" }
|
||||
|
||||
if a < 1.0 {
|
||||
return String(format: "#%02X%02X%02X%02X",
|
||||
Int(a * 255),
|
||||
Int(r * 255),
|
||||
Int(g * 255),
|
||||
Int(b * 255))
|
||||
} else {
|
||||
return String(format: "#%02X%02X%02X",
|
||||
Int(r * 255),
|
||||
Int(g * 255),
|
||||
Int(b * 255))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension UIImage {
|
||||
func scaledToFill(_ targetSize: CGSize) -> UIImage {
|
||||
let renderer = UIGraphicsImageRenderer(size: targetSize)
|
||||
return renderer.image { _ in
|
||||
self.draw(in: CGRect(origin: .zero, size: targetSize))
|
||||
}
|
||||
}
|
||||
/// Optional image compression method
|
||||
/// - Parameters:
|
||||
/// - resolutionSize: Target resolution (optional). If nil, no cropping or scaling is applied
|
||||
/// - memorySize: Target memory size in MB (optional). If nil, no memory compression is applied
|
||||
/// - cropCenter: Whether to crop from center. Default false = aspect-fit scaling
|
||||
func compress(to resolutionSize: CGSize? = nil, memorySize: Float? = nil, cropCenter: Bool = false) -> Data? {
|
||||
var scaledImage = self
|
||||
|
||||
// 1. Crop or scale based on target resolution
|
||||
if let resolutionSize = resolutionSize {
|
||||
if cropCenter {
|
||||
// 1. Calculate scale to ensure the image fully covers the target resolution
|
||||
let scale = max(resolutionSize.width / size.width, resolutionSize.height / size.height)
|
||||
let scaledSize = CGSize(width: size.width * scale, height: size.height * scale)
|
||||
|
||||
// 2. Calculate offset to ensure center cropping
|
||||
let originX = (scaledSize.width - resolutionSize.width) / 2
|
||||
let originY = (scaledSize.height - resolutionSize.height) / 2
|
||||
|
||||
// 3. Start drawing
|
||||
UIGraphicsBeginImageContextWithOptions(resolutionSize, false, 1.0)
|
||||
self.draw(in: CGRect(x: -originX, y: -originY, width: scaledSize.width, height: scaledSize.height))
|
||||
scaledImage = UIGraphicsGetImageFromCurrentImageContext() ?? self
|
||||
UIGraphicsEndImageContext()
|
||||
} else {
|
||||
// Aspect-fit scaling
|
||||
let scale = min(resolutionSize.width / size.width, resolutionSize.height / size.height)
|
||||
let newSize = CGSize(width: size.width * scale, height: size.height * scale)
|
||||
UIGraphicsBeginImageContextWithOptions(resolutionSize, false, 1.0)
|
||||
self.draw(in: CGRect(origin: .zero, size: newSize))
|
||||
scaledImage = UIGraphicsGetImageFromCurrentImageContext() ?? self
|
||||
UIGraphicsEndImageContext()
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Compress based on target memory size
|
||||
guard let memorySize = memorySize else {
|
||||
return scaledImage.jpegData(compressionQuality: 1)
|
||||
}
|
||||
|
||||
let maxBytes = Int(memorySize * 1024 * 1024) // MB -> Bytes
|
||||
var compression: CGFloat = 1.0
|
||||
var imageData = scaledImage.jpegData(compressionQuality: compression)
|
||||
|
||||
// Keep compressing until size requirement is met or compression limit is reached
|
||||
while let data = imageData, data.count > maxBytes, compression > 0.01 {
|
||||
compression *= 0.7
|
||||
imageData = scaledImage.jpegData(compressionQuality: compression)
|
||||
}
|
||||
|
||||
return imageData
|
||||
}
|
||||
/// Compress image only based on target memory size, keeping resolution and aspect ratio unchanged
|
||||
/// - Parameter memorySize: Target memory size in MB
|
||||
/// - Returns: Compressed JPEG data
|
||||
func compress(toMemorySize memorySize: Float) -> Data? {
|
||||
let maxBytes = Int(memorySize * 1024 * 1024)
|
||||
var compression: CGFloat = 1.0
|
||||
var imageData = self.jpegData(compressionQuality: compression)
|
||||
|
||||
while let data = imageData, data.count > maxBytes, compression > 0.01 {
|
||||
compression *= 0.7
|
||||
imageData = self.jpegData(compressionQuality: compression)
|
||||
}
|
||||
return imageData
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
func jsonPrint() {
|
||||
guard let data = self.data(using: .utf8) else {
|
||||
print(self)
|
||||
return
|
||||
}
|
||||
do {
|
||||
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
|
||||
let prettyData = try JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted, .sortedKeys])
|
||||
if let prettyString = String(data: prettyData, encoding: .utf8) {
|
||||
print("✅ JSON formatted output:\n\(prettyString)")
|
||||
} else {
|
||||
print(self)
|
||||
}
|
||||
} catch {
|
||||
print("❌ Invalid JSON format: \(error.localizedDescription)")
|
||||
print(self)
|
||||
}
|
||||
}
|
||||
|
||||
func leftPadding(toLength: Int, withPad character: Character) -> String {
|
||||
if count < toLength {
|
||||
return String(repeatElement(character, count: toLength - count)) + self
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a MAC address string into 6-byte Data
|
||||
func macToData() -> Data? {
|
||||
// Remove separators such as ":" or "-"
|
||||
let cleaned = self.replacingOccurrences(of: "[:\\-]", with: "", options: .regularExpression)
|
||||
|
||||
// Must be exactly 12 hexadecimal characters
|
||||
guard cleaned.count == 12 else { return nil }
|
||||
|
||||
var data = Data()
|
||||
var index = cleaned.startIndex
|
||||
for _ in 0..<6 {
|
||||
let nextIndex = cleaned.index(index, offsetBy: 2)
|
||||
let byteString = String(cleaned[index..<nextIndex])
|
||||
guard let byte = UInt8(byteString, radix: 16) else { return nil }
|
||||
data.append(byte)
|
||||
index = nextIndex
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func toData() -> Data? {
|
||||
return self.data(using: .utf8)
|
||||
}
|
||||
|
||||
func toColor() -> Color {
|
||||
let hexString = self.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let color = Color(hex: hexString) {
|
||||
return color
|
||||
}
|
||||
return .clear
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Encodable {
|
||||
func toDictionary() -> [String: Any]? {
|
||||
guard let data = try? JSONEncoder().encode(self),
|
||||
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
return nil
|
||||
}
|
||||
return dict
|
||||
}
|
||||
|
||||
func toListDictionary() -> [[String: Any]]? {
|
||||
if let singleDict = self.toDictionary() {
|
||||
return [singleDict]
|
||||
}
|
||||
if let arrayData = try? JSONEncoder().encode(self),
|
||||
let jsonArray = try? JSONSerialization.jsonObject(with: arrayData) as? [[String: Any]] {
|
||||
return jsonArray
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toJsonString(prettyPrinted: Bool = false) -> String {
|
||||
let encoder = JSONEncoder()
|
||||
if prettyPrinted {
|
||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||
}
|
||||
do {
|
||||
let data = try encoder.encode(self)
|
||||
return String(data: data, encoding: .utf8) ?? "{}"
|
||||
} catch {
|
||||
print("❌ JSON serialization failed: \(error)")
|
||||
return "{}"
|
||||
}
|
||||
}
|
||||
|
||||
func toData() -> Data? {
|
||||
let encoder = JSONEncoder()
|
||||
do {
|
||||
return try encoder.encode(self)
|
||||
} catch {
|
||||
print("❌ Failed to convert JSON to Data: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension UIColor {
|
||||
convenience init?(hex: String) {
|
||||
var hexString = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||
if hexString.hasPrefix("#") {
|
||||
hexString.removeFirst()
|
||||
}
|
||||
|
||||
let r, g, b, a: CGFloat
|
||||
|
||||
switch hexString.count {
|
||||
case 3: // RGB
|
||||
let chars = Array(hexString)
|
||||
r = CGFloat(strtoul(String([chars[0], chars[0]]), nil, 16)) / 255
|
||||
g = CGFloat(strtoul(String([chars[1], chars[1]]), nil, 16)) / 255
|
||||
b = CGFloat(strtoul(String([chars[2], chars[2]]), nil, 16)) / 255
|
||||
a = 1.0
|
||||
case 4: // RGBA
|
||||
let chars = Array(hexString)
|
||||
r = CGFloat(strtoul(String([chars[0], chars[0]]), nil, 16)) / 255
|
||||
g = CGFloat(strtoul(String([chars[1], chars[1]]), nil, 16)) / 255
|
||||
b = CGFloat(strtoul(String([chars[2], chars[2]]), nil, 16)) / 255
|
||||
a = CGFloat(strtoul(String([chars[3], chars[3]]), nil, 16)) / 255
|
||||
case 6: // RRGGBB
|
||||
var value: UInt64 = 0
|
||||
Scanner(string: hexString).scanHexInt64(&value)
|
||||
r = CGFloat((value & 0xFF0000) >> 16) / 255
|
||||
g = CGFloat((value & 0x00FF00) >> 8) / 255
|
||||
b = CGFloat(value & 0x0000FF) / 255
|
||||
a = 1.0
|
||||
case 8: // AARRGGBB
|
||||
var value: UInt64 = 0
|
||||
Scanner(string: hexString).scanHexInt64(&value)
|
||||
a = CGFloat((value & 0xFF000000) >> 24) / 255
|
||||
r = CGFloat((value & 0x00FF0000) >> 16) / 255
|
||||
g = CGFloat((value & 0x0000FF00) >> 8) / 255
|
||||
b = CGFloat(value & 0x000000FF) / 255
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
self.init(red: r, green: g, blue: b, alpha: a)
|
||||
}
|
||||
}
|
||||
|
||||
extension CGRect {
|
||||
var minDimension: CGFloat {
|
||||
min(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension View {
|
||||
|
||||
@ViewBuilder
|
||||
func rippleDiffusion() -> some View {
|
||||
TimelineView(.animation) { timeline in
|
||||
// Calculate time interval
|
||||
let time = timeline.date.timeIntervalSinceReferenceDate
|
||||
ZStack {
|
||||
// Multiple ripple cycles
|
||||
ForEach(0..<3) { i in
|
||||
let progress = (time + Double(i) * 0.5).truncatingRemainder(dividingBy: 1.5) / 1.5
|
||||
Circle()
|
||||
.stroke(Color.blue.opacity(1 - progress), lineWidth: 2)
|
||||
.scaleEffect(0.5 + progress * 2)
|
||||
}
|
||||
}
|
||||
.drawingGroup()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func glassEffectCircle() -> some View {
|
||||
if #available(iOS 26.0, *) {
|
||||
self.glassEffect()
|
||||
} else {
|
||||
self.background(
|
||||
Circle()
|
||||
.fill(.ultraThinMaterial)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func glassEffectRegular(cornerRadius : CGFloat) -> some View {
|
||||
if #available(iOS 26.0, *) {
|
||||
self.glassEffect(.regular,in: RoundedRectangle(cornerRadius: cornerRadius))
|
||||
} else {
|
||||
self
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(.ultraThinMaterial)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func presentationBackgroundClear() -> some View {
|
||||
if #available(iOS 26.0, *) {
|
||||
self.presentationBackground(.clear)
|
||||
} else {
|
||||
self.presentationBackground(.ultraThinMaterial)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func glassButtonStyle() -> some View {
|
||||
if #available(iOS 26.0, *) {
|
||||
self.buttonStyle(.glass)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func glassProminentButtonStyle() -> some View {
|
||||
if #available(iOS 26.0, *) {
|
||||
self.buttonStyle(.glassProminent)
|
||||
} else {
|
||||
self.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
||||
func hideKeyboard() {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import Foundation
|
||||
import Flutter
|
||||
import AVFoundation
|
||||
|
||||
class NativeBridge {
|
||||
static let shared = NativeBridge()
|
||||
|
||||
private var channel: FlutterMethodChannel?
|
||||
private var audioPlayChannel: FlutterBasicMessageChannel?
|
||||
private weak var flutterViewController: FlutterViewController?
|
||||
|
||||
private let channelName = "com.m5stack.stackchan/native"
|
||||
private let audioPlayChannelName = "com.m5stack.stackchan/audio_play"
|
||||
|
||||
private var audioEngine: AVAudioEngine?
|
||||
private var audioPlayerNode: AVAudioPlayerNode?
|
||||
private let sampleRate: Double = 16000.0
|
||||
private let channels: AVAudioChannelCount = 1
|
||||
private var isAudioInitialized = false
|
||||
|
||||
private let audioQueue = DispatchQueue(label: "com.stackchan.audio", qos: .userInitiated)
|
||||
private let audioFormat: AVAudioFormat? = AVAudioFormat(
|
||||
commonFormat: .pcmFormatFloat32,
|
||||
sampleRate: 16_000,
|
||||
channels: 1,
|
||||
interleaved: true
|
||||
)
|
||||
|
||||
private init() {}
|
||||
|
||||
func setup(with viewController: FlutterViewController) {
|
||||
self.flutterViewController = viewController
|
||||
let binaryMessenger = viewController.binaryMessenger
|
||||
|
||||
channel = FlutterMethodChannel(name: channelName, binaryMessenger: binaryMessenger)
|
||||
audioPlayChannel = FlutterBasicMessageChannel(
|
||||
name: audioPlayChannelName,
|
||||
binaryMessenger: binaryMessenger,
|
||||
codec: FlutterBinaryCodec()
|
||||
)
|
||||
|
||||
audioPlayChannel?.setMessageHandler { [weak self] message, reply in
|
||||
guard let self = self, let data = message as? Data else {
|
||||
reply(nil)
|
||||
return
|
||||
}
|
||||
self.audioQueue.async { [weak self] in
|
||||
self?.playAudio(pcmData: data)
|
||||
}
|
||||
reply(nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func playAudio(pcmData: Data) {
|
||||
guard let audioFormat = audioFormat else {
|
||||
print("❌ 音频格式初始化失败")
|
||||
return
|
||||
}
|
||||
|
||||
if !isAudioInitialized {
|
||||
guard setupAudioSession() else {
|
||||
print("❌ 会话初始化失败")
|
||||
return
|
||||
}
|
||||
guard setupAudioEngine() else {
|
||||
print("❌ 引擎初始化失败")
|
||||
return
|
||||
}
|
||||
isAudioInitialized = true
|
||||
print("✅ 音频初始化完成")
|
||||
}
|
||||
|
||||
guard let engine = audioEngine, let playerNode = audioPlayerNode else {
|
||||
resetAudio()
|
||||
return
|
||||
}
|
||||
|
||||
if !engine.isRunning {
|
||||
do {
|
||||
try engine.start()
|
||||
} catch {
|
||||
print("❌ 引擎启动失败: \(error)")
|
||||
resetAudio()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var floatBuffer = pcmData.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> [Float] in
|
||||
let int16Buffer = bytes.bindMemory(to: Int16.self)
|
||||
var floats = [Float](repeating: 0, count: int16Buffer.count)
|
||||
//强制放大3倍
|
||||
for i in 0..<int16Buffer.count {
|
||||
floats[i] = min(max(Float(int16Buffer[i]) / Float(Int16.max) * 3.0, -1.0), 1.0)
|
||||
}
|
||||
return floats
|
||||
}
|
||||
|
||||
guard let buffer = AVAudioPCMBuffer(pcmFormat: audioFormat,
|
||||
frameCapacity: AVAudioFrameCount(floatBuffer.count)) else { return }
|
||||
|
||||
buffer.frameLength = buffer.frameCapacity
|
||||
memcpy(buffer.floatChannelData![0], &floatBuffer, floatBuffer.count * MemoryLayout<Float>.size)
|
||||
|
||||
playerNode.scheduleBuffer(buffer)
|
||||
if !playerNode.isPlaying {
|
||||
playerNode.play()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 音频会话(无-50错误)
|
||||
private func setupAudioSession() -> Bool {
|
||||
do {
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
try session.setCategory(.playback, mode: .default)
|
||||
try session.setActive(true)
|
||||
return true
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("❌ 音频会话错误:\(nsError.code) - \(nsError.localizedDescription)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 引擎初始化(修复-10868核心)
|
||||
private func setupAudioEngine() -> Bool {
|
||||
guard let audioFormat = audioFormat else {
|
||||
print("❌ 音频格式为空")
|
||||
return false
|
||||
}
|
||||
|
||||
let engine = AVAudioEngine()
|
||||
let playerNode = AVAudioPlayerNode()
|
||||
engine.attach(playerNode)
|
||||
|
||||
// 格式统一,不会崩溃
|
||||
engine.connect(playerNode, to: engine.mainMixerNode, format: audioFormat)
|
||||
|
||||
do {
|
||||
try engine.start()
|
||||
} catch {
|
||||
print("❌ 引擎启动失败: \(error)")
|
||||
return false
|
||||
}
|
||||
|
||||
self.audioEngine = engine
|
||||
self.audioPlayerNode = playerNode
|
||||
return true
|
||||
}
|
||||
|
||||
private func resetAudio() {
|
||||
audioPlayerNode?.stop()
|
||||
audioEngine?.stop()
|
||||
audioEngine = nil
|
||||
audioPlayerNode = nil
|
||||
isAudioInitialized = false
|
||||
}
|
||||
|
||||
func stopPlayPCM() {
|
||||
audioQueue.async { [weak self] in
|
||||
self?.resetAudio()
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(method: Method,_ arguments: Any? = nil,_ completion: ((Any?) -> Void)? = nil) {
|
||||
guard method != .unknown else {
|
||||
print("⚠️ 未知方法")
|
||||
completion?(nil)
|
||||
return
|
||||
}
|
||||
channel?.invokeMethod(method.rawValue, arguments: arguments) { result in
|
||||
if let error = result as? FlutterError {
|
||||
print("❌ 发送失败:\(error)")
|
||||
}
|
||||
completion?(result)
|
||||
}
|
||||
}
|
||||
|
||||
func setMethodCallHandler(handler: @escaping FlutterMethodCallHandler) {
|
||||
channel?.setMethodCallHandler(handler)
|
||||
}
|
||||
}
|
||||
|
||||
enum Method: String, CaseIterable {
|
||||
case wifiName
|
||||
case unknown
|
||||
case stopPlayPCM
|
||||
|
||||
static func fromString(_ name: String) -> Method {
|
||||
return Method(rawValue: name) ?? .unknown
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
//
|
||||
// StackChanArView.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by 袁智鸿 on 2026/2/5.
|
||||
//
|
||||
|
||||
import RealityKit
|
||||
import ARKit
|
||||
|
||||
class StackChanArView : NSObject, FlutterPlatformView, ARSessionDelegate, ARSCNViewDelegate {
|
||||
|
||||
private var arView: ARSCNView
|
||||
private var channel: FlutterMethodChannel?
|
||||
private var expressionChannel: FlutterEventChannel?
|
||||
private var frameChannel: FlutterEventChannel?
|
||||
private var expressionStreamHandler: ExpressionStreamHandler?
|
||||
private var frameStreamHandler: FrameStreamHandler?
|
||||
private var decorate: Int = 0
|
||||
private var captureScreen: Bool = false
|
||||
private let methodChannelName = "com.stackchan.ar.view"
|
||||
private var lastCaptureTime: TimeInterval = 0
|
||||
private let stackChanTargetSize = CGSize(width: 320, height: 240)
|
||||
private var faceAnchorNode: SCNNode?
|
||||
private var currentDecorationNode: SCNNode?
|
||||
private var expressionLayer = ExpressionLayer(data: ExpressionData(leftEye: ExpressionItem(), rightEye: ExpressionItem(), mouth: ExpressionItem()),reverse: true)
|
||||
private let emotionThresholds = EmotionThresholds()
|
||||
private var lastSendTime: Date = Date(timeIntervalSince1970: 0)
|
||||
|
||||
func view() -> UIView {
|
||||
return arView
|
||||
}
|
||||
|
||||
init(
|
||||
frame: CGRect,
|
||||
viewId: Int64,
|
||||
messenger: FlutterBinaryMessenger,
|
||||
args: Any?
|
||||
) {
|
||||
arView = ARSCNView(frame: frame)
|
||||
arView.contentMode = .scaleAspectFit
|
||||
arView.autoresizingMask = [.flexibleWidth,.flexibleHeight]
|
||||
super.init()
|
||||
initializeChannels(viewId: viewId, messenger: messenger)
|
||||
setupARSession()
|
||||
}
|
||||
|
||||
private func initializeChannels(viewId: Int64, messenger: FlutterBinaryMessenger) {
|
||||
let methodChannelName = "\(methodChannelName)_\(viewId)"
|
||||
channel = FlutterMethodChannel(name: methodChannelName, binaryMessenger: messenger)
|
||||
channel?.setMethodCallHandler { [weak self] call, result in
|
||||
self?.handleMethodCall(call, result: result)
|
||||
}
|
||||
|
||||
// 表情数据事件通道
|
||||
let expressionChannelName = "\(methodChannelName)_expression"
|
||||
expressionStreamHandler = ExpressionStreamHandler()
|
||||
expressionChannel = FlutterEventChannel(name: expressionChannelName, binaryMessenger: messenger)
|
||||
expressionChannel?.setStreamHandler(expressionStreamHandler)
|
||||
|
||||
// 帧数据事件通道
|
||||
let frameChannelName = "\(methodChannelName)_frame"
|
||||
frameStreamHandler = FrameStreamHandler()
|
||||
frameChannel = FlutterEventChannel(name: frameChannelName, binaryMessenger: messenger)
|
||||
frameChannel?.setStreamHandler(frameStreamHandler)
|
||||
}
|
||||
|
||||
private func setupARSession() {
|
||||
guard ARFaceTrackingConfiguration.isSupported else {
|
||||
print("设备不支持面部追踪")
|
||||
return
|
||||
}
|
||||
let configuration = ARFaceTrackingConfiguration()
|
||||
configuration.isLightEstimationEnabled = true
|
||||
if let format = ARFaceTrackingConfiguration.supportedVideoFormats.last {
|
||||
configuration.videoFormat = format
|
||||
}
|
||||
configuration.videoHDRAllowed = true
|
||||
arView.automaticallyUpdatesLighting = true
|
||||
arView.session.delegate = self
|
||||
arView.delegate = self
|
||||
arView.session.run(configuration, options: [.resetTracking,.removeExistingAnchors])
|
||||
}
|
||||
|
||||
private func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "dispose":
|
||||
dispose()
|
||||
result(nil)
|
||||
case "setDecorate":
|
||||
if let decorateValue = call.arguments as? Int {
|
||||
decorate = decorateValue
|
||||
if let faceNode = self.faceAnchorNode {
|
||||
self.updateDecorationOnNode(node: faceNode, decorate: decorate)
|
||||
}
|
||||
}
|
||||
result(nil)
|
||||
case "setCaptureScreen":
|
||||
if let captureValue = call.arguments as? Bool {
|
||||
captureScreen = captureValue
|
||||
}
|
||||
result(nil)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
/// 面部数据
|
||||
func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
|
||||
DispatchQueue.main.async {
|
||||
self.emotionDetection(session: session, anchors: anchors)
|
||||
}
|
||||
}
|
||||
|
||||
private func emotionDetection(session: ARSession, anchors: [ARAnchor]) {
|
||||
if let anchor = anchors.first {
|
||||
guard let faceAnchor = anchor as? ARFaceAnchor else { return }
|
||||
let faceData = buildExpressionData(faceAnchor: faceAnchor)
|
||||
let headData = detectHeadData(session:session,faceAnchor: faceAnchor)
|
||||
self.updateDecoration(expressionData: faceData)
|
||||
|
||||
let now = Date()
|
||||
if now.timeIntervalSince(self.lastSendTime) >= 0.5 {
|
||||
let danceData = DanceData(leftEye: faceData.leftEye, rightEye: faceData.rightEye, mouth: faceData.mouth, yawServo: headData.yawServo, pitchServo: headData.pitchServo, durationMs: 1000)
|
||||
let jsonString = danceData.toJsonString()
|
||||
self.expressionStreamHandler?.sendExpressionData(jsonString)
|
||||
lastSendTime = now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func detectHeadData(session: ARSession,faceAnchor: ARFaceAnchor) -> MotionData {
|
||||
let faceTransform = faceAnchor.transform
|
||||
guard let cameraTransform = session.currentFrame?.camera.transform else {
|
||||
return MotionData(pitchServo: MotionDataItem(angle: 0, speed: 500),
|
||||
yawServo: MotionDataItem(angle: 0, speed: 500))
|
||||
}
|
||||
let relativeTransform = simd_mul(simd_inverse(cameraTransform), faceTransform)
|
||||
let relativeMatrix = SCNMatrix4(relativeTransform)
|
||||
let pitch = atan2(relativeMatrix.m31, relativeMatrix.m33) // Vertical rotation (pitch)
|
||||
let yaw = asin(-relativeMatrix.m32) // Horizontal rotation (yaw)
|
||||
let pitchDeg = pitch * 180.0 / .pi
|
||||
let yawDeg = yaw * 180.0 / .pi
|
||||
let yawServoAngle = max(-1280, min(1280, Int(-yawDeg * 20)))
|
||||
let pitchServoAngle = max(0, min(900, Int(-pitchDeg * 10)))
|
||||
let pitchItem = MotionDataItem(angle: pitchServoAngle, speed: 500)
|
||||
let yawItem = MotionDataItem(angle: yawServoAngle, speed: 500)
|
||||
return MotionData(pitchServo: pitchItem, yawServo: yawItem)
|
||||
}
|
||||
|
||||
private func isAmazed(blendShapes: [ARFaceAnchor.BlendShapeLocation: NSNumber]) -> Bool {
|
||||
let jawOpen = blendShapes[.jawOpen]?.floatValue ?? 0
|
||||
let eyeWideLeft = blendShapes[.eyeWideLeft]?.floatValue ?? 0
|
||||
let eyeWideRight = blendShapes[.eyeWideRight]?.floatValue ?? 0
|
||||
let browInnerUp = blendShapes[.browInnerUp]?.floatValue ?? 0
|
||||
let mouthFunnel = blendShapes[.mouthFunnel]?.floatValue ?? 0
|
||||
|
||||
// Amazed traits: wide eyes + raised brows + (open mouth or O-shape)
|
||||
let isEyesWide = (eyeWideLeft + eyeWideRight) / 2 > emotionThresholds.amazed.eyeWide
|
||||
let isBrowRaised = browInnerUp > emotionThresholds.amazed.browInnerUp
|
||||
let isMouthAction = jawOpen > emotionThresholds.amazed.jawOpen ||
|
||||
mouthFunnel > emotionThresholds.amazed.mouthFunnel
|
||||
|
||||
return isEyesWide && isBrowRaised && isMouthAction
|
||||
}
|
||||
|
||||
private func buildExpressionData(faceAnchor: ARFaceAnchor) -> ExpressionData {
|
||||
let blendShapes = faceAnchor.blendShapes
|
||||
let eyeBlinkLeft = blendShapes[.eyeBlinkLeft]?.floatValue ?? 0
|
||||
let leftEyeWeight = max(0, min(100, Int((1.0 - eyeBlinkLeft) * 100)))
|
||||
let eyeBlinkRight = blendShapes[.eyeBlinkRight]?.floatValue ?? 0
|
||||
let rightEyeWeight = max(0, min(100, Int((1.0 - eyeBlinkRight) * 100)))
|
||||
let leftEye = ExpressionItem(
|
||||
x: max(-100, min(100, Int(faceAnchor.lookAtPoint.x * 800))),
|
||||
y: max(-100, min(100, Int(-faceAnchor.lookAtPoint.y * 500))),
|
||||
rotation: 0,
|
||||
weight: leftEyeWeight
|
||||
)
|
||||
let rightEye = ExpressionItem(
|
||||
x: max(-100, min(100, Int(faceAnchor.lookAtPoint.x * 800))),
|
||||
y: max(-100, min(100, Int(-faceAnchor.lookAtPoint.y * 500))),
|
||||
rotation: 0,
|
||||
weight: rightEyeWeight
|
||||
)
|
||||
let jawOpen = blendShapes[.jawOpen]?.floatValue ?? 0
|
||||
let mouthSmileLeft = blendShapes[.mouthSmileLeft]?.floatValue ?? 0
|
||||
let mouthSmileRight = blendShapes[.mouthSmileRight]?.floatValue ?? 0
|
||||
let mouthX = max(-100, min(100, Int((mouthSmileRight - mouthSmileLeft) * 100)))
|
||||
let mouthWeight = max(0, min(100, Int(jawOpen * 100)))
|
||||
let mouth = ExpressionItem(
|
||||
x: mouthX,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
weight: mouthWeight
|
||||
)
|
||||
var expressionData = ExpressionData(leftEye: leftEye,
|
||||
rightEye: rightEye,
|
||||
mouth: mouth)
|
||||
if isHappy(blendShapes: blendShapes) {
|
||||
expressionData.leftEye.weight -= 35
|
||||
expressionData.leftEye.rotation = -2150
|
||||
expressionData.rightEye.weight -= 35
|
||||
expressionData.rightEye.rotation = 2150
|
||||
}
|
||||
if isAnger(blendShapes: blendShapes) {
|
||||
expressionData.leftEye.rotation = 450
|
||||
expressionData.rightEye.rotation = -450
|
||||
}
|
||||
return expressionData
|
||||
}
|
||||
|
||||
|
||||
private func isAnger(blendShapes: [ARFaceAnchor.BlendShapeLocation: NSNumber]) -> Bool {
|
||||
// Brow features
|
||||
let browDownLeft = blendShapes[.browDownLeft]?.floatValue ?? 0
|
||||
let browDownRight = blendShapes[.browDownRight]?.floatValue ?? 0
|
||||
|
||||
// Eye features
|
||||
let eyeSquintLeft = blendShapes[.eyeSquintLeft]?.floatValue ?? 0
|
||||
let eyeSquintRight = blendShapes[.eyeSquintRight]?.floatValue ?? 0
|
||||
|
||||
// Mouth features
|
||||
let mouthFrownLeft = blendShapes[.mouthFrownLeft]?.floatValue ?? 0
|
||||
let mouthFrownRight = blendShapes[.mouthFrownRight]?.floatValue ?? 0
|
||||
let mouthPressLeft = blendShapes[.mouthPressLeft]?.floatValue ?? 0
|
||||
let mouthPressRight = blendShapes[.mouthPressRight]?.floatValue ?? 0
|
||||
|
||||
// Nose features
|
||||
let noseSneerLeft = blendShapes[.noseSneerLeft]?.floatValue ?? 0
|
||||
let noseSneerRight = blendShapes[.noseSneerRight]?.floatValue ?? 0
|
||||
|
||||
// Calculate averages
|
||||
let avgBrowDown = (browDownLeft + browDownRight) / 2
|
||||
let avgEyeSquint = (eyeSquintLeft + eyeSquintRight) / 2
|
||||
let avgMouthFrown = (mouthFrownLeft + mouthFrownRight) / 2
|
||||
let avgMouthPress = (mouthPressLeft + mouthPressRight) / 2
|
||||
let avgNoseSneer = (noseSneerLeft + noseSneerRight) / 2
|
||||
|
||||
// Anger scoring system
|
||||
var angerScore = 0
|
||||
|
||||
if avgBrowDown > emotionThresholds.anger.browDown { angerScore += 3 }
|
||||
if avgEyeSquint > emotionThresholds.anger.eyeSquint { angerScore += 2 }
|
||||
if avgMouthFrown > emotionThresholds.anger.mouthFrown { angerScore += 2 }
|
||||
if avgMouthPress > emotionThresholds.anger.mouthPress { angerScore += 1 }
|
||||
if avgNoseSneer > emotionThresholds.anger.noseSneer { angerScore += 1 }
|
||||
|
||||
// Must reach threshold and include brow-down feature
|
||||
return angerScore >= emotionThresholds.anger.minScore &&
|
||||
avgBrowDown > emotionThresholds.anger.browDown
|
||||
}
|
||||
|
||||
private func isTired(blendShapes: [ARFaceAnchor.BlendShapeLocation: NSNumber]) -> Bool {
|
||||
let eyeBlinkLeft = blendShapes[.eyeBlinkLeft]?.floatValue ?? 0
|
||||
let eyeBlinkRight = blendShapes[.eyeBlinkRight]?.floatValue ?? 0
|
||||
let eyeSquintLeft = blendShapes[.eyeSquintLeft]?.floatValue ?? 0
|
||||
let eyeSquintRight = blendShapes[.eyeSquintRight]?.floatValue ?? 0
|
||||
|
||||
// Tired traits: eyes closed or squinting
|
||||
let eyesClosed = (eyeBlinkLeft > emotionThresholds.tired.eyeClose &&
|
||||
eyeBlinkRight > emotionThresholds.tired.eyeClose) ||
|
||||
(eyeSquintLeft > emotionThresholds.tired.eyeSquint &&
|
||||
eyeSquintRight > emotionThresholds.tired.eyeSquint)
|
||||
|
||||
return eyesClosed
|
||||
}
|
||||
|
||||
private func isHappy(blendShapes: [ARFaceAnchor.BlendShapeLocation: NSNumber]) -> Bool {
|
||||
let smileLeft = blendShapes[.mouthSmileLeft]?.floatValue ?? 0
|
||||
let smileRight = blendShapes[.mouthSmileRight]?.floatValue ?? 0
|
||||
let eyeSquintLeft = blendShapes[.eyeSquintLeft]?.floatValue ?? 0
|
||||
let eyeSquintRight = blendShapes[.eyeSquintRight]?.floatValue ?? 0
|
||||
let cheekSquintLeft = blendShapes[.cheekSquintLeft]?.floatValue ?? 0
|
||||
let cheekSquintRight = blendShapes[.cheekSquintRight]?.floatValue ?? 0
|
||||
|
||||
// Calculate overall smile intensity
|
||||
let smileIntensity = (smileLeft + smileRight) / 2
|
||||
let eyeSquintIntensity = (eyeSquintLeft + eyeSquintRight) / 2
|
||||
let cheekSquintIntensity = (cheekSquintLeft + cheekSquintRight) / 2
|
||||
|
||||
// Happy expression requires a clear smile with eye muscle involvement
|
||||
return smileIntensity > emotionThresholds.happy.smile &&
|
||||
(eyeSquintIntensity > emotionThresholds.happy.eyeSquint ||
|
||||
cheekSquintIntensity > emotionThresholds.happy.cheekSquint)
|
||||
}
|
||||
|
||||
private func isShy(faceAnchor: ARFaceAnchor, blendShapes: [ARFaceAnchor.BlendShapeLocation: NSNumber]) -> Bool {
|
||||
// 1. Slight or clear head tilt downward
|
||||
let transform = faceAnchor.transform
|
||||
let rotation = SCNMatrix4(transform)
|
||||
let pitch = asin(-rotation.m32) // 上下旋转
|
||||
let isHeadDown = pitch > emotionThresholds.shy.headPitch
|
||||
|
||||
// 2. Mouth closed with a slight smile
|
||||
let mouthClose = blendShapes[.mouthClose]?.floatValue ?? 0
|
||||
let smileLeft = blendShapes[.mouthSmileLeft]?.floatValue ?? 0
|
||||
let smileRight = blendShapes[.mouthSmileRight]?.floatValue ?? 0
|
||||
let smileIntensity = (smileLeft + smileRight) / 2
|
||||
let isMouthClosedSmile = mouthClose > emotionThresholds.shy.mouthPress && smileIntensity > emotionThresholds.shy.smile
|
||||
|
||||
// 3. Eyes looking sideways or downward
|
||||
let lookAt = faceAnchor.lookAtPoint
|
||||
let isLookingSideways = abs(lookAt.x) > emotionThresholds.gaze.xThreshold // Looking left or right
|
||||
let isLookingDown = lookAt.y < -emotionThresholds.gaze.yThreshold // Looking downward
|
||||
|
||||
return isHeadDown && isMouthClosedSmile && (isLookingSideways || isLookingDown)
|
||||
}
|
||||
|
||||
func renderer(_ renderer: any SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
|
||||
guard anchor is ARFaceAnchor else { return nil }
|
||||
let node = SCNNode()
|
||||
self.faceAnchorNode = node
|
||||
updateDecorationOnNode(node: node, decorate: decorate)
|
||||
return node
|
||||
}
|
||||
|
||||
private func updateDecorationOnNode(node: SCNNode, decorate: Int) {
|
||||
currentDecorationNode?.removeFromParentNode()
|
||||
|
||||
if decorate == 1 {
|
||||
let container = SCNNode()
|
||||
|
||||
let stackChanModelNode = createStackChanModel()
|
||||
container.addChildNode(stackChanModelNode)
|
||||
|
||||
let expressionPlaneNode = createPlane()
|
||||
container.addChildNode(expressionPlaneNode)
|
||||
|
||||
node.addChildNode(container)
|
||||
currentDecorationNode = container
|
||||
} else if decorate == 2 {
|
||||
let noseNode = createEmojiNoseNode(emoji: "🐽")
|
||||
node.addChildNode(noseNode)
|
||||
currentDecorationNode = noseNode
|
||||
}
|
||||
}
|
||||
|
||||
private func createEmojiNoseNode(emoji: String) -> SCNNode {
|
||||
let size = CGSize(width: 300, height: 300)
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0)
|
||||
(emoji as NSString).draw(in: CGRect(origin: .zero, size: size),
|
||||
withAttributes: [.font: UIFont.systemFont(ofSize: size.width - 20)])
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
let nosePlane = SCNPlane(width: 0.05, height: 0.05)
|
||||
nosePlane.firstMaterial?.diffuse.contents = image
|
||||
nosePlane.firstMaterial?.isDoubleSided = true
|
||||
|
||||
let noseNode = SCNNode(geometry: nosePlane)
|
||||
noseNode.name = "noseNode"
|
||||
noseNode.position = SCNVector3(0, 0, 0.07)
|
||||
return noseNode
|
||||
}
|
||||
|
||||
private func createPlane() -> SCNNode {
|
||||
let plane = SCNPlane(width: 0.16, height: 0.12)
|
||||
|
||||
let layerWidth = plane.width * 1000
|
||||
let layerHeight = plane.height * 1000
|
||||
expressionLayer.frame = CGRect(origin: .zero, size: CGSize(width: layerWidth, height: layerHeight))
|
||||
expressionLayer.setNeedsDisplay()
|
||||
|
||||
let material = SCNMaterial()
|
||||
material.diffuse.contents = UIColor.black
|
||||
plane.materials = [material]
|
||||
|
||||
let planeNode = SCNNode(geometry: plane)
|
||||
planeNode.name = "expressionPlane"
|
||||
planeNode.position = SCNVector3(0, 0.03, 0.07)
|
||||
return planeNode
|
||||
}
|
||||
|
||||
private func createStackChanModel() -> SCNNode {
|
||||
guard let scene = SCNScene(named: "StackChanModel.scn"),
|
||||
let modelNode = scene.rootNode.childNodes.first else {
|
||||
print("no model")
|
||||
return SCNNode()
|
||||
}
|
||||
modelNode.name = "StackChanModel"
|
||||
modelNode.scale = SCNVector3(0.004, 0.004, 0.004)
|
||||
modelNode.opacity = 0.4
|
||||
modelNode.position = SCNVector3(0, 0.03, 0)
|
||||
modelNode.eulerAngles = SCNVector3Zero
|
||||
modelNode.eulerAngles.x = -Float.pi / 2
|
||||
|
||||
//隐藏一部分
|
||||
if let foundation = modelNode.childNode(withName: "_00_stackchan450_3",recursively: false),let centralComponent = modelNode.childNode(withName: "_00_stackchan450_2", recursively: false) {
|
||||
foundation.opacity = 0
|
||||
centralComponent.opacity = 0
|
||||
}
|
||||
|
||||
return modelNode
|
||||
}
|
||||
|
||||
private func getGazeDirection(faceAnchor: ARFaceAnchor) -> String {
|
||||
let lookAtPoint = faceAnchor.lookAtPoint
|
||||
var direction = ""
|
||||
|
||||
if lookAtPoint.x < -emotionThresholds.gaze.xThreshold {
|
||||
direction += "Left"
|
||||
} else if lookAtPoint.x > emotionThresholds.gaze.xThreshold {
|
||||
direction += "Right"
|
||||
}
|
||||
|
||||
if lookAtPoint.y < -emotionThresholds.gaze.yThreshold {
|
||||
direction += "Down"
|
||||
} else if lookAtPoint.y > emotionThresholds.gaze.yThreshold {
|
||||
direction += "Up"
|
||||
}
|
||||
|
||||
return direction.isEmpty ? "Looking Forward" : direction + " Look"
|
||||
}
|
||||
|
||||
private func getHeadDirection(faceAnchor: ARFaceAnchor) -> String {
|
||||
let transform = faceAnchor.transform
|
||||
let rotation = SCNMatrix4(transform)
|
||||
let yaw = atan2(rotation.m31, rotation.m33)
|
||||
let pitch = asin(-rotation.m32)
|
||||
|
||||
var horizontal = ""
|
||||
var vertical = ""
|
||||
|
||||
if yaw < -emotionThresholds.head.yawThreshold {
|
||||
horizontal = "Left"
|
||||
} else if yaw > emotionThresholds.head.yawThreshold {
|
||||
horizontal = "Right"
|
||||
}
|
||||
|
||||
// Correct vertical direction
|
||||
if pitch < -emotionThresholds.head.pitchThreshold {
|
||||
vertical = "Up"
|
||||
} else if pitch > emotionThresholds.head.pitchThreshold {
|
||||
vertical = "Down"
|
||||
}
|
||||
|
||||
if horizontal.isEmpty && vertical.isEmpty {
|
||||
return "Head Facing Forward"
|
||||
} else {
|
||||
return "Head Facing " + vertical + horizontal
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var expressionRenderer: UIGraphicsImageRenderer = {
|
||||
let format = UIGraphicsImageRendererFormat.default()
|
||||
format.scale = UIScreen.main.scale
|
||||
format.opaque = false
|
||||
return UIGraphicsImageRenderer(
|
||||
size: expressionLayer.bounds.size,
|
||||
format: format
|
||||
)
|
||||
}()
|
||||
|
||||
/// 输出画面
|
||||
func renderer(_ renderer: any SCNSceneRenderer, updateAtTime time: TimeInterval) {
|
||||
if captureScreen {
|
||||
if time - lastCaptureTime >= 0.5 {
|
||||
lastCaptureTime = time
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let renderedImage = arView.snapshot()
|
||||
if let jpedData = renderedImage.compress(to: stackChanTargetSize,memorySize: 0.02,cropCenter: true) {
|
||||
self.frameStreamHandler?.sendFrameData(jpedData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 绘制机器人表情
|
||||
private func updateDecoration(expressionData: ExpressionData) {
|
||||
DispatchQueue.main.async {
|
||||
if self.decorate == 1 {
|
||||
let scene = self.arView.scene
|
||||
guard let planeNode = scene.rootNode.childNode(withName: "expressionPlane", recursively: true),
|
||||
let plane = planeNode.geometry as? SCNPlane else {
|
||||
return
|
||||
}
|
||||
self.expressionLayer.data = expressionData
|
||||
self.expressionLayer.setNeedsDisplay()
|
||||
let originalImage = self.expressionRenderer.image { ctx in
|
||||
self.expressionLayer.render(in: ctx.cgContext)
|
||||
}
|
||||
let image = UIImage(
|
||||
cgImage: originalImage.cgImage!,
|
||||
scale: originalImage.scale,
|
||||
orientation: .upMirrored
|
||||
)
|
||||
plane.materials.first?.diffuse.contents = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dispose() {
|
||||
arView.session.pause()
|
||||
channel?.setMethodCallHandler(nil)
|
||||
expressionChannel?.setStreamHandler(nil)
|
||||
frameChannel?.setStreamHandler(nil)
|
||||
arView.scene.rootNode.childNodes.forEach { $0.removeFromParentNode() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ExpressionStreamHandler: NSObject,FlutterStreamHandler {
|
||||
|
||||
private var eventSink: FlutterEventSink?
|
||||
|
||||
func sendExpressionData(_ data: String) {
|
||||
guard let sink = eventSink else { return }
|
||||
DispatchQueue.main.async {
|
||||
sink(data)
|
||||
}
|
||||
}
|
||||
|
||||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
||||
eventSink = events
|
||||
return nil
|
||||
}
|
||||
|
||||
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||
eventSink = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
class FrameStreamHandler: NSObject, FlutterStreamHandler {
|
||||
|
||||
private var eventSink: FlutterEventSink?
|
||||
|
||||
func sendFrameData(_ data: Data) {
|
||||
guard let sink = eventSink else { return }
|
||||
|
||||
// 确保在主线程发送数据
|
||||
DispatchQueue.main.async {
|
||||
sink(FlutterStandardTypedData(bytes: data))
|
||||
}
|
||||
}
|
||||
|
||||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
||||
eventSink = events
|
||||
return nil
|
||||
}
|
||||
|
||||
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||
eventSink = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private struct EmotionThresholds {
|
||||
// Happy emotion thresholds
|
||||
struct Happy {
|
||||
let smile: Float = 0.3
|
||||
let eyeSquint: Float = 0.15
|
||||
let cheekSquint: Float = 0.1
|
||||
}
|
||||
|
||||
// Shy emotion thresholds
|
||||
struct Shy {
|
||||
let headPitch: Float = 0.08
|
||||
let eyeSquint: Float = 0.1
|
||||
let mouthPress: Float = 0.25
|
||||
let smile: Float = 0.15
|
||||
}
|
||||
|
||||
// Amazed emotion thresholds
|
||||
struct Amazed {
|
||||
let eyeWide: Float = 0.4
|
||||
let browInnerUp: Float = 0.3
|
||||
let jawOpen: Float = 0.4
|
||||
let mouthFunnel: Float = 0.3
|
||||
}
|
||||
|
||||
// Angry emotion thresholds
|
||||
struct Anger {
|
||||
let browDown: Float = 0.35
|
||||
let eyeSquint: Float = 0.25
|
||||
let mouthFrown: Float = 0.2
|
||||
let mouthPress: Float = 0.2
|
||||
let noseSneer: Float = 0.15
|
||||
let minScore: Int = 5
|
||||
}
|
||||
|
||||
// Tired emotion thresholds
|
||||
struct Tired {
|
||||
let eyeClose: Float = 0.7
|
||||
let eyeSquint: Float = 0.5
|
||||
let jawOpen: Float = 0.3
|
||||
}
|
||||
|
||||
// Gaze detection thresholds
|
||||
struct Gaze {
|
||||
let xThreshold: Float = 0.02
|
||||
let yThreshold: Float = 0.02
|
||||
}
|
||||
|
||||
// Head direction thresholds
|
||||
struct Head {
|
||||
let yawThreshold: Float = 0.25
|
||||
let pitchThreshold: Float = 0.25
|
||||
}
|
||||
|
||||
let happy = Happy()
|
||||
let shy = Shy()
|
||||
let amazed = Amazed()
|
||||
let anger = Anger()
|
||||
let tired = Tired()
|
||||
let gaze = Gaze()
|
||||
let head = Head()
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
//
|
||||
// StackChanRobot.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by 袁智鸿 on 2026/1/30.
|
||||
//
|
||||
import SceneKit
|
||||
|
||||
class StackChanRobot: NSObject, FlutterPlatformView, FlutterStreamHandler {
|
||||
|
||||
private let sceneView: SCNView
|
||||
private let methodChannel: FlutterMethodChannel
|
||||
|
||||
private var currentDanceData: DanceData?
|
||||
private let expressionLayer = ExpressionLayer(data: ExpressionData(leftEye: ExpressionItem(weight: 100), rightEye: ExpressionItem(weight: 100), mouth: ExpressionItem()))
|
||||
private let planeNodeName = "expressionPlane"
|
||||
private let rotateKey = "autoRotate"
|
||||
private var topLook: Bool = false
|
||||
private let methodChannelName = "com.stackchan.robot.method"
|
||||
|
||||
private var defaultCameraNode: SCNNode?
|
||||
private var topCameraNode: SCNNode?
|
||||
|
||||
init(
|
||||
frame: CGRect,
|
||||
viewId: Int64,
|
||||
messenger: FlutterBinaryMessenger,
|
||||
args: Any?
|
||||
) {
|
||||
self.sceneView = SCNView(frame: frame)
|
||||
self.methodChannel = FlutterMethodChannel(
|
||||
name: methodChannelName + "_\(viewId)",
|
||||
binaryMessenger: messenger
|
||||
)
|
||||
super.init()
|
||||
methodChannel.setMethodCallHandler(handleMethodCall)
|
||||
setupSceneView()
|
||||
setupInitialScene()
|
||||
}
|
||||
|
||||
func view() -> UIView {
|
||||
return sceneView
|
||||
}
|
||||
|
||||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||
return nil
|
||||
}
|
||||
|
||||
private func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "updateDanceData":
|
||||
if let json = call.arguments as? String {
|
||||
updateDanceData(from: json)
|
||||
result(nil)
|
||||
} else {
|
||||
result(FlutterError(
|
||||
code: "INVALID_ARGS",
|
||||
message: "Expected JSON string",
|
||||
details: nil
|
||||
))
|
||||
}
|
||||
case "setTopLook":
|
||||
if let topLook = call.arguments as? Bool {
|
||||
self.topLook = topLook
|
||||
setupCamera()
|
||||
result(nil)
|
||||
} else {
|
||||
result(FlutterError(
|
||||
code: "INVALID_ARGS",
|
||||
message: "Expected boolean value",
|
||||
details: nil
|
||||
))
|
||||
}
|
||||
case "setAllowsCameraControl":
|
||||
if let allowsControl = call.arguments as? Bool {
|
||||
sceneView.allowsCameraControl = allowsControl
|
||||
result(nil)
|
||||
} else {
|
||||
result(FlutterError(
|
||||
code: "INVALID_ARGS",
|
||||
message: "Expected boolean value",
|
||||
details: nil
|
||||
))
|
||||
}
|
||||
case "dispose":
|
||||
cleanup()
|
||||
result(nil)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupSceneView() {
|
||||
sceneView.antialiasingMode = .multisampling4X
|
||||
sceneView.autoenablesDefaultLighting = true
|
||||
sceneView.allowsCameraControl = false
|
||||
sceneView.backgroundColor = .clear
|
||||
sceneView.isPlaying = true
|
||||
}
|
||||
|
||||
private func setupInitialScene() {
|
||||
guard let scene = SCNScene(named: "StackChanModel.scn") else {
|
||||
print("Failed to load StackChanModel.scn")
|
||||
return
|
||||
}
|
||||
scene.rootNode.eulerAngles = SCNVector3Zero
|
||||
scene.rootNode.eulerAngles.x = -Float.pi / 2
|
||||
scene.rootNode.position.y = scene.rootNode.position.y + 25
|
||||
scene.rootNode.position.z = scene.rootNode.position.z - 35
|
||||
|
||||
if let rootNode = scene.rootNode.childNodes.first {
|
||||
setupRobotHierarchy(rootNode: rootNode, scene: scene)
|
||||
|
||||
if let defaultCamera = scene.rootNode.childNode(withName: "camera", recursively: true) {
|
||||
defaultCameraNode = defaultCamera
|
||||
} else {
|
||||
defaultCameraNode = createDefaultCameraNode(rootNode: rootNode)
|
||||
rootNode.addChildNode(defaultCameraNode!)
|
||||
}
|
||||
sceneView.pointOfView = defaultCameraNode
|
||||
}
|
||||
sceneView.scene = scene
|
||||
}
|
||||
|
||||
private func createDefaultCameraNode(rootNode: SCNNode) -> SCNNode {
|
||||
let cameraNode = SCNNode()
|
||||
cameraNode.name = "defaultCamera"
|
||||
let camera = SCNCamera()
|
||||
camera.zFar = 200
|
||||
cameraNode.camera = camera
|
||||
cameraNode.position = SCNVector3(x: 0, y: -100, z: 0)
|
||||
let lookAtConstraint = SCNLookAtConstraint(target: rootNode)
|
||||
lookAtConstraint.isGimbalLockEnabled = true
|
||||
cameraNode.constraints = [lookAtConstraint]
|
||||
return cameraNode
|
||||
}
|
||||
|
||||
private func createTopCameraNode(rootNode: SCNNode) -> SCNNode {
|
||||
let cameraNode = SCNNode()
|
||||
cameraNode.name = "leftTopCamera"
|
||||
let camera = SCNCamera()
|
||||
camera.zFar = 300
|
||||
cameraNode.camera = camera
|
||||
cameraNode.position = SCNVector3(x: 0, y: -100, z: 70)
|
||||
let lookAtConstraint = SCNLookAtConstraint(target: rootNode)
|
||||
lookAtConstraint.isGimbalLockEnabled = true
|
||||
cameraNode.constraints = [lookAtConstraint]
|
||||
return cameraNode
|
||||
}
|
||||
|
||||
private func setupRobotHierarchy(rootNode: SCNNode, scene: SCNScene) {
|
||||
guard let foundation = rootNode.childNode(withName: "_00_stackchan450_3", recursively: false),
|
||||
let centralComponent = rootNode.childNode(withName: "_00_stackchan450_2", recursively: false),
|
||||
let head = rootNode.childNode(withName: "_00_stackchan450_1", recursively: false) else {
|
||||
return
|
||||
}
|
||||
|
||||
let yawAxis = SCNNode()
|
||||
yawAxis.name = "yawAxis"
|
||||
let centralWorldPos = centralComponent.worldPosition
|
||||
yawAxis.worldPosition.z = centralWorldPos.z + 15
|
||||
foundation.addChildNode(yawAxis)
|
||||
|
||||
let centralWorldTransform = centralComponent.worldTransform
|
||||
yawAxis.addChildNode(centralComponent)
|
||||
centralComponent.setWorldTransform(centralWorldTransform)
|
||||
|
||||
// Setup pitch axis for head movement
|
||||
let headWorldTransform = head.worldTransform
|
||||
let pitchAxis = SCNNode()
|
||||
pitchAxis.name = "pitchAxis"
|
||||
pitchAxis.worldPosition.z = pitchAxis.worldPosition.z - 20
|
||||
centralComponent.addChildNode(pitchAxis)
|
||||
pitchAxis.addChildNode(head)
|
||||
head.setWorldTransform(headWorldTransform)
|
||||
|
||||
// Add expression plane to head
|
||||
addExpressionPlane(to: head)
|
||||
}
|
||||
|
||||
private func addExpressionPlane(to head: SCNNode) {
|
||||
let plane = SCNPlane(width: 42, height: 32)
|
||||
let magnification: CGFloat = 5
|
||||
let size = CGSize(width: magnification * plane.width, height: magnification * plane.height)
|
||||
|
||||
expressionLayer.frame = CGRect(origin: .zero, size: size)
|
||||
expressionLayer.setNeedsDisplay()
|
||||
let material = SCNMaterial()
|
||||
plane.materials = [material]
|
||||
|
||||
let planeNode = SCNNode(geometry: plane)
|
||||
planeNode.name = planeNodeName
|
||||
planeNode.position = head.position
|
||||
planeNode.position.z = planeNode.position.z - 4.5
|
||||
|
||||
head.addChildNode(planeNode)
|
||||
}
|
||||
|
||||
private func setupCamera() {
|
||||
guard let scene = sceneView.scene,
|
||||
let rootNode = scene.rootNode.childNodes.first else {
|
||||
return
|
||||
}
|
||||
rootNode.childNodes.filter { $0.name == "leftTopCamera" }.forEach { $0.removeFromParentNode() }
|
||||
if topLook {
|
||||
if topCameraNode == nil {
|
||||
topCameraNode = createTopCameraNode(rootNode: rootNode)
|
||||
rootNode.addChildNode(topCameraNode!)
|
||||
}
|
||||
sceneView.pointOfView = topCameraNode
|
||||
} else {
|
||||
if defaultCameraNode == nil {
|
||||
defaultCameraNode = createDefaultCameraNode(rootNode: rootNode)
|
||||
rootNode.addChildNode(defaultCameraNode!)
|
||||
}
|
||||
sceneView.pointOfView = defaultCameraNode
|
||||
}
|
||||
}
|
||||
|
||||
private func updateDanceData(from json: String) {
|
||||
guard let danceData = DanceData.from(jsonString: json) else {
|
||||
return
|
||||
}
|
||||
|
||||
currentDanceData = danceData
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.applyDanceData(danceData)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyDanceData(_ data: DanceData) {
|
||||
guard let scene = sceneView.scene,
|
||||
let rootNode = scene.rootNode.childNodes.first else {
|
||||
return
|
||||
}
|
||||
|
||||
// Update servo positions
|
||||
updateServos(rootNode: rootNode, data: data)
|
||||
|
||||
// Update RGB color
|
||||
updateRGBColor(rootNode: rootNode, data: data)
|
||||
|
||||
// Update expression
|
||||
updateExpression(data: data)
|
||||
}
|
||||
private func updateServos(rootNode: SCNNode, data: DanceData) {
|
||||
|
||||
if let yawAxis = rootNode.childNode(withName: "yawAxis", recursively: true),
|
||||
let pitchAxis = rootNode.childNode(withName: "pitchAxis", recursively: true) {
|
||||
|
||||
yawAxis.removeAction(forKey: rotateKey)
|
||||
|
||||
// Update yaw (rotation around Y axis)
|
||||
if data.yawServo.rotate == 0 {
|
||||
let clampedYaw = max(-128, min(128, data.yawServo.angle / 10))
|
||||
let yawRadians = Float(clampedYaw) * Float.pi / 180.0
|
||||
yawAxis.rotation = SCNVector4(0, 1, 0, yawRadians)
|
||||
} else {
|
||||
let rotateSpeed = max(-100, min(100, data.yawServo.rotate / 10))
|
||||
let radiansPerSecond = Float(rotateSpeed) / 100.0 * Float.pi * 2
|
||||
|
||||
let rotateAction = SCNAction.customAction(duration: .infinity) { node, _ in
|
||||
let deltaTime: Float = 1.0 / 60.0
|
||||
node.eulerAngles.y += radiansPerSecond * deltaTime
|
||||
}
|
||||
yawAxis.runAction(rotateAction, forKey: rotateKey)
|
||||
}
|
||||
|
||||
// Update pitch (head tilt)
|
||||
let clampedPitch = max(0, min(90, data.pitchServo.angle / 10))
|
||||
let pitchRadians = Float(clampedPitch) * Float.pi / 180.0
|
||||
pitchAxis.eulerAngles.x = -pitchRadians
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRGBColor(rootNode: SCNNode, data: DanceData) {
|
||||
rootNode.enumerateChildNodes { node, _ in
|
||||
if let materials = node.geometry?.materials {
|
||||
for material in materials {
|
||||
if material.name == "MTL12" {
|
||||
if let color = UIColor(hex: data.leftRgbColor) {
|
||||
material.emission.contents = color
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateExpression(data: DanceData) {
|
||||
guard let planeNode = sceneView.scene?.rootNode.childNode(withName: planeNodeName, recursively: true),
|
||||
let plane = planeNode.geometry as? SCNPlane else {
|
||||
return
|
||||
}
|
||||
|
||||
let expressionData = ExpressionData(
|
||||
leftEye: data.leftEye,
|
||||
rightEye: data.rightEye,
|
||||
mouth: data.mouth
|
||||
)
|
||||
|
||||
expressionLayer.data = expressionData
|
||||
expressionLayer.setNeedsDisplay()
|
||||
|
||||
let newImage = expressionRenderer().image { ctx in
|
||||
self.expressionLayer.render(in: ctx.cgContext)
|
||||
}
|
||||
|
||||
plane.firstMaterial?.diffuse.contents = newImage
|
||||
}
|
||||
|
||||
private func expressionRenderer() -> UIGraphicsImageRenderer {
|
||||
let format = UIGraphicsImageRendererFormat.default()
|
||||
format.scale = UIScreen.main.scale
|
||||
format.opaque = false
|
||||
return UIGraphicsImageRenderer(
|
||||
size: expressionLayer.bounds.size,
|
||||
format: format
|
||||
)
|
||||
}
|
||||
|
||||
private func cleanup() {
|
||||
// Stop all animations
|
||||
sceneView.scene?.rootNode.childNodes.forEach { node in
|
||||
node.removeAllActions()
|
||||
node.removeFromParentNode()
|
||||
}
|
||||
|
||||
// Clean up scene
|
||||
sceneView.scene = nil
|
||||
sceneView.isPlaying = false
|
||||
|
||||
// Remove method call handler
|
||||
methodChannel.setMethodCallHandler(nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ExpressionLayer: CALayer {
|
||||
var data: ExpressionData
|
||||
|
||||
let reverse: Bool
|
||||
|
||||
init(data: ExpressionData, reverse: Bool = false) {
|
||||
self.data = data
|
||||
self.reverse = reverse
|
||||
super.init()
|
||||
self.contentsScale = UIScreen.main.scale
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
if let layer = layer as? ExpressionLayer {
|
||||
self.data = layer.data
|
||||
self.reverse = layer.reverse
|
||||
} else {
|
||||
self.data = ExpressionData(leftEye: ExpressionItem(), rightEye: ExpressionItem(), mouth: ExpressionItem())
|
||||
self.reverse = false
|
||||
}
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func draw(in ctx: CGContext) {
|
||||
let rect = self.frame
|
||||
|
||||
// Background
|
||||
ctx.setFillColor(UIColor.black.withAlphaComponent(0.7).cgColor)
|
||||
ctx.fill(rect)
|
||||
|
||||
let eyeSize = rect.width / 10
|
||||
|
||||
func drawEye(_ item: ExpressionItem, at point: CGPoint) {
|
||||
|
||||
// Calculate scale based on size (-100 to 100)
|
||||
// 0 -> 1.0 (keep current size)
|
||||
// -100 -> 0.5 (half normal radius)
|
||||
// 100 -> 2.0 (double normal radius)
|
||||
let clampedSize = max(-100, min(100, item.size))
|
||||
let sizeScale: CGFloat
|
||||
if clampedSize >= 0 {
|
||||
sizeScale = 1.0 + CGFloat(clampedSize) / 100.0
|
||||
} else {
|
||||
sizeScale = 1.0 + CGFloat(clampedSize) / 200.0
|
||||
}
|
||||
|
||||
let scaledEyeSize = eyeSize * sizeScale
|
||||
|
||||
let visibleHeight = scaledEyeSize * (CGFloat(item.weight) / 100)
|
||||
|
||||
let centerX = point.x + CGFloat(item.x / 10) + eyeSize / 2
|
||||
let centerY = point.y + CGFloat(item.y / 10) + eyeSize / 2
|
||||
let eyeRect = CGRect(
|
||||
x: centerX - scaledEyeSize / 2,
|
||||
y: centerY - scaledEyeSize / 2,
|
||||
width: scaledEyeSize,
|
||||
height: scaledEyeSize
|
||||
)
|
||||
|
||||
ctx.saveGState()
|
||||
|
||||
// Rotation
|
||||
let rotationDegrees = CGFloat(item.rotation) / 10.0
|
||||
let center = CGPoint(x: eyeRect.midX, y: eyeRect.midY)
|
||||
ctx.translateBy(x: center.x, y: center.y)
|
||||
ctx.rotate(by: rotationDegrees * .pi / 180)
|
||||
ctx.translateBy(x: -center.x, y: -center.y)
|
||||
|
||||
// Clip height
|
||||
let maskRect = CGRect(
|
||||
x: eyeRect.minX,
|
||||
y: eyeRect.maxY - visibleHeight,
|
||||
width: scaledEyeSize,
|
||||
height: visibleHeight
|
||||
)
|
||||
ctx.addRect(maskRect)
|
||||
ctx.clip()
|
||||
|
||||
ctx.setFillColor(UIColor.white.cgColor)
|
||||
ctx.fillEllipse(in: eyeRect)
|
||||
|
||||
ctx.restoreGState()
|
||||
}
|
||||
|
||||
let eyeY = (rect.height * 0.4) - (eyeSize / 2)
|
||||
let leftEyePoint = CGPoint(x: (rect.width / 4) - (eyeSize / 2), y: eyeY)
|
||||
let rightEyePoint = CGPoint(x: (rect.width / 4 * 3) - (eyeSize / 2), y: eyeY)
|
||||
|
||||
|
||||
if reverse {
|
||||
// Temporarily swap rotation angles
|
||||
let leftEyeRotation = data.leftEye.rotation
|
||||
let rightEyeRotation = data.rightEye.rotation
|
||||
|
||||
var leftEye = data.leftEye
|
||||
var rightEye = data.rightEye
|
||||
|
||||
leftEye.rotation = rightEyeRotation
|
||||
rightEye.rotation = leftEyeRotation
|
||||
|
||||
drawEye(leftEye, at: rightEyePoint)
|
||||
drawEye(rightEye, at: leftEyePoint)
|
||||
} else {
|
||||
drawEye(data.leftEye, at: leftEyePoint)
|
||||
drawEye(data.rightEye, at: rightEyePoint)
|
||||
}
|
||||
|
||||
// Draw mouth
|
||||
ctx.saveGState()
|
||||
|
||||
let width = rect.width * 0.3 - CGFloat(data.mouth.weight / 10)
|
||||
let height = 3 + CGFloat(data.mouth.weight) * 0.2
|
||||
let x = ((rect.width - width) / 2) + CGFloat(data.mouth.x / 10)
|
||||
let y = (rect.height * 0.65) + CGFloat(data.mouth.y / 10)
|
||||
|
||||
let rotationDegrees = CGFloat(data.mouth.rotation) / 10.0
|
||||
let center = CGPoint(x: x + width / 2, y: y + height / 2)
|
||||
ctx.translateBy(x: center.x, y: center.y)
|
||||
ctx.rotate(by: rotationDegrees * .pi / 180)
|
||||
ctx.translateBy(x: -center.x, y: -center.y)
|
||||
|
||||
let mouthRect = CGRect(x: x, y: y, width: width, height: height)
|
||||
let mouthPath = UIBezierPath(roundedRect: mouthRect, cornerRadius: height / 2)
|
||||
ctx.addPath(mouthPath.cgPath)
|
||||
ctx.setFillColor(UIColor.white.cgColor)
|
||||
ctx.fillPath()
|
||||
|
||||
ctx.restoreGState()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
//
|
||||
// StackChanRotaryRobot.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by 袁智鸿 on 2026/1/30.
|
||||
//
|
||||
import SceneKit
|
||||
|
||||
class StackChanRotaryRobot: NSObject, FlutterPlatformView, FlutterStreamHandler {
|
||||
|
||||
private let expressionLayer = ExpressionLayer(data: ExpressionData(leftEye: ExpressionItem(weight: 100), rightEye: ExpressionItem(weight: 100), mouth: ExpressionItem()))
|
||||
|
||||
private let planeNodeName = "expressionPlane"
|
||||
|
||||
private let sceneView: SCNView
|
||||
|
||||
func view() -> UIView {
|
||||
return sceneView
|
||||
}
|
||||
|
||||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||
return nil
|
||||
}
|
||||
|
||||
init(
|
||||
frame: CGRect,
|
||||
viewId: Int64,
|
||||
messenger: FlutterBinaryMessenger,
|
||||
args: Any?
|
||||
) {
|
||||
self.sceneView = SCNView(frame: frame)
|
||||
super.init()
|
||||
setupSceneView()
|
||||
setupInitialScene()
|
||||
}
|
||||
|
||||
|
||||
private func setupSceneView() {
|
||||
sceneView.antialiasingMode = .multisampling4X
|
||||
sceneView.autoenablesDefaultLighting = true
|
||||
sceneView.allowsCameraControl = false
|
||||
sceneView.backgroundColor = .clear
|
||||
sceneView.isPlaying = true
|
||||
}
|
||||
|
||||
|
||||
private func setupInitialScene() {
|
||||
guard let scene = SCNScene(named: "StackChanModel.scn") else {
|
||||
print("Failed to load StackChanModel.scn")
|
||||
return
|
||||
}
|
||||
scene.rootNode.eulerAngles = SCNVector3Zero
|
||||
scene.rootNode.eulerAngles.x = -Float.pi / 2
|
||||
scene.rootNode.position.y = scene.rootNode.position.y + 25
|
||||
scene.rootNode.position.z = scene.rootNode.position.z - 45
|
||||
|
||||
let clampedPitch = max(0, min(900, 200))
|
||||
let pitchRatio = Float(clampedPitch) / 900.0
|
||||
let pitchAngle = -Float.pi / 2 * (1 + pitchRatio)
|
||||
scene.rootNode.eulerAngles.x = pitchAngle
|
||||
|
||||
if let rootNode = scene.rootNode.childNodes.first {
|
||||
setupRobotHierarchy(rootNode: rootNode, scene: scene)
|
||||
}
|
||||
sceneView.scene = scene
|
||||
}
|
||||
|
||||
private func setupRobotHierarchy(rootNode: SCNNode, scene: SCNScene) {
|
||||
guard let foundation = rootNode.childNode(withName: "_00_stackchan450_3", recursively: false),
|
||||
let centralComponent = rootNode.childNode(withName: "_00_stackchan450_2", recursively: false),
|
||||
let head = rootNode.childNode(withName: "_00_stackchan450_1", recursively: false) else {
|
||||
return
|
||||
}
|
||||
|
||||
let yawAxis = SCNNode()
|
||||
yawAxis.name = "yawAxis"
|
||||
let centralWorldPos = centralComponent.worldPosition
|
||||
yawAxis.worldPosition.z = centralWorldPos.z + 15
|
||||
foundation.addChildNode(yawAxis)
|
||||
|
||||
let centralWorldTransform = centralComponent.worldTransform
|
||||
yawAxis.addChildNode(centralComponent)
|
||||
centralComponent.setWorldTransform(centralWorldTransform)
|
||||
|
||||
// Setup pitch axis for head movement
|
||||
let headWorldTransform = head.worldTransform
|
||||
let pitchAxis = SCNNode()
|
||||
pitchAxis.name = "pitchAxis"
|
||||
pitchAxis.worldPosition.z = pitchAxis.worldPosition.z - 20
|
||||
centralComponent.addChildNode(pitchAxis)
|
||||
pitchAxis.addChildNode(head)
|
||||
head.setWorldTransform(headWorldTransform)
|
||||
|
||||
// Add expression plane to head
|
||||
addExpressionPlane(to: head)
|
||||
|
||||
// 旋转
|
||||
let rotateAction = SCNAction.rotateBy(x: 0, y: CGFloat(2 * Double.pi), z: 0, duration: 5)
|
||||
let repeatAction = SCNAction.repeatForever(rotateAction)
|
||||
scene.rootNode.runAction(repeatAction)
|
||||
}
|
||||
|
||||
private func addExpressionPlane(to head: SCNNode) {
|
||||
let plane = SCNPlane(width: 42, height: 32)
|
||||
let magnification: CGFloat = 5
|
||||
let size = CGSize(width: magnification * plane.width, height: magnification * plane.height)
|
||||
expressionLayer.frame = CGRect(origin: .zero, size: size)
|
||||
expressionLayer.setNeedsDisplay()
|
||||
let newImage = expressionRenderer().image { ctx in
|
||||
self.expressionLayer.render(in: ctx.cgContext)
|
||||
}
|
||||
let material = SCNMaterial()
|
||||
material.diffuse.contents = newImage
|
||||
plane.materials = [material]
|
||||
let planeNode = SCNNode(geometry: plane)
|
||||
planeNode.name = planeNodeName
|
||||
planeNode.position = head.position
|
||||
planeNode.position.z = planeNode.position.z - 4.5
|
||||
head.addChildNode(planeNode)
|
||||
}
|
||||
|
||||
private func expressionRenderer() -> UIGraphicsImageRenderer {
|
||||
let format = UIGraphicsImageRendererFormat.default()
|
||||
format.scale = UIScreen.main.scale
|
||||
format.opaque = false
|
||||
return UIGraphicsImageRenderer(
|
||||
size: expressionLayer.bounds.size,
|
||||
format: format
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// ViewFactory.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by 袁智鸿 on 2026/1/30.
|
||||
//
|
||||
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
class StackChanRobotViewFactory: NSObject, FlutterPlatformViewFactory {
|
||||
|
||||
private let messenger: FlutterBinaryMessenger
|
||||
|
||||
init(messenger: FlutterBinaryMessenger) {
|
||||
self.messenger = messenger
|
||||
super.init()
|
||||
}
|
||||
|
||||
func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> any FlutterPlatformView {
|
||||
return StackChanRobot(frame: frame, viewId: viewId, messenger: messenger, args: args)
|
||||
}
|
||||
|
||||
func createArgsCodec() -> any FlutterMessageCodec & NSObjectProtocol {
|
||||
return FlutterStandardMessageCodec.sharedInstance()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class StackChanRotaryRobotViewFactory: NSObject, FlutterPlatformViewFactory {
|
||||
private let messenger: FlutterBinaryMessenger
|
||||
|
||||
init(messenger: FlutterBinaryMessenger) {
|
||||
self.messenger = messenger
|
||||
super.init()
|
||||
}
|
||||
|
||||
func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> any FlutterPlatformView {
|
||||
return StackChanRotaryRobot(frame: frame, viewId: viewId, messenger: messenger, args: args)
|
||||
}
|
||||
|
||||
func createArgsCodec() -> any FlutterMessageCodec & NSObjectProtocol {
|
||||
return FlutterStandardMessageCodec.sharedInstance()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class StackChanArViewFactory: NSObject, FlutterPlatformViewFactory {
|
||||
private let messenger: FlutterBinaryMessenger
|
||||
|
||||
init(messenger: FlutterBinaryMessenger) {
|
||||
self.messenger = messenger
|
||||
super.init()
|
||||
}
|
||||
|
||||
func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> any FlutterPlatformView {
|
||||
return StackChanArView(frame: frame, viewId: viewId, messenger: messenger, args: args)
|
||||
}
|
||||
|
||||
func createArgsCodec() -> any FlutterMessageCodec & NSObjectProtocol {
|
||||
return FlutterStandardMessageCodec.sharedInstance()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
|
||||
func testExample() {
|
||||
// If you add code to the Runner application, consider adding tests here.
|
||||
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user