Fix format-xx.sh files wrt logging, add proper Swift formatting configuration.

This commit is contained in:
Mario Zechner 2025-07-20 21:31:07 +02:00
parent 0dd86dfdc1
commit d409ff23ff
49 changed files with 890 additions and 688 deletions

View File

@ -27,7 +27,7 @@ let package = Package(
.byName( .byName(
name: "Spine", name: "Spine",
condition: .when(platforms: [ condition: .when(platforms: [
.iOS, .iOS
]) ])
), ),
"SpineCppLite", "SpineCppLite",
@ -38,7 +38,7 @@ let package = Package(
.target( .target(
name: "Spine", name: "Spine",
dependencies: [ dependencies: [
"SpineCppLite", "SpineShadersStructs" "SpineCppLite", "SpineShadersStructs",
], ],
path: "spine-ios/Sources/Spine" path: "spine-ios/Sources/Spine"
), ),
@ -46,13 +46,13 @@ let package = Package(
name: "SpineCppLite", name: "SpineCppLite",
path: "spine-ios/Sources/SpineCppLite", path: "spine-ios/Sources/SpineCppLite",
linkerSettings: [ linkerSettings: [
.linkedLibrary("c++"), .linkedLibrary("c++")
] ]
), ),
.systemLibrary( .systemLibrary(
name: "SpineShadersStructs", name: "SpineShadersStructs",
path: "spine-ios/Sources/SpineShadersStructs" path: "spine-ios/Sources/SpineShadersStructs"
) ),
], ],
cxxLanguageStandard: .cxx11 cxxLanguageStandard: .cxx11
) )

52
formatters/.swift-format Normal file
View File

@ -0,0 +1,52 @@
{
"version": 1,
"lineLength": 150,
"indentation": {
"spaces": 4
},
"tabWidth": 4,
"maximumBlankLines": 2,
"respectsExistingLineBreaks": true,
"lineBreakBeforeControlFlowKeywords": false,
"lineBreakBeforeEachArgument": false,
"lineBreakBeforeEachGenericRequirement": false,
"prioritizeKeepingFunctionOutputTogether": true,
"indentConditionalCompilationBlocks": true,
"lineBreakAroundMultilineExpressionChainComponents": false,
"rules": {
"AllPublicDeclarationsHaveDocumentation": false,
"AlwaysUseLowerCamelCase": true,
"AmbiguousTrailingClosureOverload": true,
"BeginDocumentationCommentWithOneLineSummary": false,
"DoNotUseSemicolons": true,
"DontRepeatTypeInStaticProperties": true,
"FileScopedDeclarationPrivacy": true,
"FullyIndirectEnum": true,
"GroupNumericLiterals": true,
"IdentifiersMustBeASCII": true,
"NeverForceUnwrap": false,
"NeverUseForceTry": false,
"NeverUseImplicitlyUnwrappedOptionals": false,
"NoAccessLevelOnExtensionDeclaration": true,
"NoBlockComments": true,
"NoCasesWithOnlyFallthrough": true,
"NoEmptyTrailingClosureParentheses": true,
"NoLabelsInCasePatterns": true,
"NoLeadingUnderscores": false,
"NoParensAroundConditions": true,
"NoVoidReturnOnFunctionSignature": true,
"OneCasePerLine": true,
"OneVariableDeclarationPerLine": true,
"OnlyOneTrailingClosureArgument": false,
"OrderedImports": true,
"ReturnVoidInsteadOfEmptyTuple": true,
"UseEarlyExits": false,
"UseLetInEveryBoundCaseVariable": true,
"UseShorthandTypeNames": true,
"UseSingleLinePropertyGetter": true,
"UseSynthesizedInitializer": true,
"UseTripleSlashForDocumentationComments": true,
"UseWhereClausesInForLoops": false,
"ValidateDocumentationComments": false
}
}

View File

@ -1,16 +1,21 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Format C/C++ files with clang-format
echo "Formatting C/C++ files..."
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source logging utilities
source "$dir/logging/logging.sh"
log_title "C/C++ Formatting"
# Store original directory # Store original directory
pushd "$dir" > /dev/null pushd "$dir" > /dev/null
if [ ! -f ".clang-format" ]; then log_action "Checking for formatters/.clang-format"
echo "Error: .clang-format not found in formatters directory" if [ -f ".clang-format" ]; then
log_ok
else
log_fail
popd > /dev/null popd > /dev/null
exit 1 exit 1
fi fi
@ -22,47 +27,47 @@ cpp_dirs=(
"../spine-cpp/src/spine" "../spine-cpp/src/spine"
"../spine-cpp/spine-cpp-lite" "../spine-cpp/spine-cpp-lite"
"../spine-cpp/tests" "../spine-cpp/tests"
# spine-c # spine-c
"../spine-c/include" "../spine-c/include"
"../spine-c/src" "../spine-c/src"
"../spine-c/src/generated" "../spine-c/src/generated"
"../spine-c/tests" "../spine-c/tests"
# spine-godot # spine-godot
"../spine-godot/spine_godot" "../spine-godot/spine_godot"
# spine-ue # spine-ue
"../spine-ue/Source/SpineUE" "../spine-ue/Source/SpineUE"
"../spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Public" "../spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Public"
"../spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Private" "../spine-ue/Plugins/SpinePlugin/Source/SpinePlugin/Private"
"../spine-ue/Plugins/SpinePlugin/Source/SpineEditorPlugin/Public" "../spine-ue/Plugins/SpinePlugin/Source/SpineEditorPlugin/Public"
"../spine-ue/Plugins/SpinePlugin/Source/SpineEditorPlugin/Private" "../spine-ue/Plugins/SpinePlugin/Source/SpineEditorPlugin/Private"
# spine-glfw # spine-glfw
"../spine-glfw/src" "../spine-glfw/src"
"../spine-glfw/example" "../spine-glfw/example"
# spine-sdl # spine-sdl
"../spine-sdl/src" "../spine-sdl/src"
"../spine-sdl/example" "../spine-sdl/example"
# spine-sfml # spine-sfml
"../spine-sfml/c/src/spine" "../spine-sfml/c/src/spine"
"../spine-sfml/c/example" "../spine-sfml/c/example"
"../spine-sfml/cpp/src/spine" "../spine-sfml/cpp/src/spine"
"../spine-sfml/cpp/example" "../spine-sfml/cpp/example"
# spine-cocos2dx # spine-cocos2dx
"../spine-cocos2dx/spine-cocos2dx/src/spine" "../spine-cocos2dx/spine-cocos2dx/src/spine"
"../spine-cocos2dx/example/Classes" "../spine-cocos2dx/example/Classes"
# spine-ios # spine-ios
"../spine-ios/Sources/SpineCppLite" "../spine-ios/Sources/SpineCppLite"
"../spine-ios/Sources/SpineCppLite/include" "../spine-ios/Sources/SpineCppLite/include"
"../spine-ios/Sources/SpineShadersStructs" "../spine-ios/Sources/SpineShadersStructs"
"../spine-ios/Example/Spine iOS Example" "../spine-ios/Example/Spine iOS Example"
# spine-flutter # spine-flutter
"../spine-flutter/ios/Classes" "../spine-flutter/ios/Classes"
"../spine-flutter/macos/Classes" "../spine-flutter/macos/Classes"
@ -86,22 +91,16 @@ for cpp_dir in "${cpp_dirs[@]}"; do
fi fi
done done
echo "Found ${#files[@]} C/C++ files to format"
# Format all files in one call - works for both Docker and native log_action "Formatting ${#files[@]} C/C++ files"
echo "Formatting ${#files[@]} files..." if FORMAT_OUTPUT=$(clang-format -i -style=file:".clang-format" "${files[@]}" 2>&1); then
if ! clang-format -i -style=file:".clang-format" "${files[@]}" 2>&1; then log_ok
echo "Error: clang-format failed"
errors=1
else else
errors=0 log_fail
log_error_output "$FORMAT_OUTPUT"
popd > /dev/null
exit 1
fi fi
if [ $errors -gt 0 ]; then
echo "Completed with $errors errors"
fi
echo "C/C++ formatting complete"
# Return to original directory # Return to original directory
popd > /dev/null popd > /dev/null

View File

@ -1,48 +1,66 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Format C# files with dotnet-format
echo "Formatting C# files..."
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source logging utilities
source "$dir/logging/logging.sh"
log_title "C# Formatting"
if command -v dotnet &> /dev/null; then if command -v dotnet &> /dev/null; then
# Store original directory # Store original directory
pushd "$dir" > /dev/null pushd "$dir" > /dev/null
# Copy .editorconfig to C# directories
cp .editorconfig ../spine-csharp/ 2>/dev/null || true cp .editorconfig ../spine-csharp/ 2>/dev/null || true
cp .editorconfig ../spine-monogame/ 2>/dev/null || true cp .editorconfig ../spine-monogame/ 2>/dev/null || true
cp .editorconfig ../spine-unity/ 2>/dev/null || true cp .editorconfig ../spine-unity/ 2>/dev/null || true
# Format spine-csharp # Format spine-csharp
log_action "Formatting spine-csharp"
pushd ../spine-csharp > /dev/null pushd ../spine-csharp > /dev/null
dotnet format spine-csharp.csproj --no-restore --verbosity quiet 2>/dev/null || echo "Warning: Some issues with spine-csharp formatting" if DOTNET_OUTPUT=$(dotnet format spine-csharp.csproj --no-restore --verbosity quiet 2>&1); then
log_ok
else
log_warn
log_detail "$DOTNET_OUTPUT"
fi
popd > /dev/null popd > /dev/null
# Format spine-monogame # Format spine-monogame
log_action "Formatting spine-monogame"
pushd ../spine-monogame > /dev/null pushd ../spine-monogame > /dev/null
dotnet format --no-restore --verbosity quiet 2>/dev/null || echo "Warning: Some issues with spine-monogame formatting" if DOTNET_OUTPUT=$(dotnet format --no-restore --verbosity quiet 2>&1); then
log_ok
else
log_warn
log_detail "$DOTNET_OUTPUT"
fi
popd > /dev/null popd > /dev/null
# Format spine-unity - look for .cs files directly # Format spine-unity - look for .cs files directly
if [ -d ../spine-unity ]; then log_action "Formatting spine-unity C# files"
echo "Formatting spine-unity C# files directly..." pushd ../spine-unity > /dev/null
pushd ../spine-unity > /dev/null # Find all .cs files and format them using dotnet format whitespace
# Find all .cs files and format them using dotnet format whitespace cs_files=$(find . -name "*.cs" -type f -not -path "./Library/*" -not -path "./Temp/*" -not -path "./obj/*" -not -path "./bin/*" | wc -l | tr -d ' ')
if [ "$cs_files" -gt 0 ]; then
find . -name "*.cs" -type f -not -path "./Library/*" -not -path "./Temp/*" -not -path "./obj/*" -not -path "./bin/*" | while read -r file; do find . -name "*.cs" -type f -not -path "./Library/*" -not -path "./Temp/*" -not -path "./obj/*" -not -path "./bin/*" | while read -r file; do
dotnet format whitespace --include "$file" --no-restore 2>/dev/null || true dotnet format whitespace --include "$file" --no-restore 2>/dev/null || true
done done
popd > /dev/null log_ok
else
log_skip
fi fi
popd > /dev/null
# Clean up .editorconfig files
rm -f ../spine-csharp/.editorconfig rm -f ../spine-csharp/.editorconfig
rm -f ../spine-monogame/.editorconfig rm -f ../spine-monogame/.editorconfig
rm -f ../spine-unity/.editorconfig rm -f ../spine-unity/.editorconfig
# Return to original directory # Return to original directory
popd > /dev/null popd > /dev/null
else else
echo "Warning: dotnet not found. Skipping C# formatting." log_fail
log_error_output "dotnet not found. Please install .NET SDK to format C# files."
exit 1
fi fi

View File

@ -1,22 +1,44 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Format Dart files
echo "Formatting Dart files..."
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source logging utilities
source "$dir/logging/logging.sh"
log_title "Dart Formatting"
# Store original directory # Store original directory
pushd "$dir" > /dev/null pushd "$dir" > /dev/null
if command -v dart &> /dev/null; then if command -v dart &> /dev/null; then
find .. -name "*.dart" \ dart_files=$(find .. -name "*.dart" \
-not -path "*/.*" \ -not -path "*/.*" \
-not -path "*/node_modules/*" \ -not -path "*/node_modules/*" \
-not -path "*/build/*" \ -not -path "*/build/*" | wc -l | tr -d ' ')
-exec dart format --page-width 120 {} +
if [ "$dart_files" -gt 0 ]; then
log_action "Formatting $dart_files Dart files"
if DART_OUTPUT=$(find .. -name "*.dart" \
-not -path "*/.*" \
-not -path "*/node_modules/*" \
-not -path "*/build/*" \
-exec dart format --page-width 120 {} + 2>&1); then
log_ok
else
log_fail
log_error_output "$DART_OUTPUT"
popd > /dev/null
exit 1
fi
else
log_action "Formatting Dart files"
log_skip
fi
else else
echo "Warning: dart not found. Skipping Dart formatting." log_fail
log_error_output "dart not found. Please install Dart SDK to format Dart files."
exit 1
fi fi
# Return to original directory # Return to original directory

View File

@ -1,21 +1,45 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Format Haxe files
echo "Formatting Haxe files..."
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source logging utilities
source "$dir/logging/logging.sh"
log_title "Haxe Formatting"
# Store original directory # Store original directory
pushd "$dir" > /dev/null pushd "$dir" > /dev/null
if command -v haxelib &> /dev/null && haxelib list formatter &> /dev/null; then if command -v haxelib &> /dev/null; then
# Format spine-haxe directory log_action "Checking Haxe formatter availability"
if [ -d ../spine-haxe ]; then if HAXELIB_OUTPUT=$(haxelib list formatter 2>&1); then
haxelib run formatter -s ../spine-haxe log_ok
# Format spine-haxe directory
if [ -d ../spine-haxe ]; then
log_action "Formatting spine-haxe directory"
if FORMATTER_OUTPUT=$(haxelib run formatter -s ../spine-haxe 2>&1); then
log_ok
else
log_fail
log_error_output "$FORMATTER_OUTPUT"
popd > /dev/null
exit 1
fi
else
log_action "Formatting spine-haxe directory"
log_skip
fi
else
log_fail
log_error_output "Haxe formatter not found. Install with: haxelib install formatter"
exit 1
fi fi
else else
echo "Warning: haxe formatter not found. Install with: haxelib install formatter" log_fail
log_error_output "haxelib not found. Please install Haxe to format Haxe files."
exit 1
fi fi
# Return to original directory # Return to original directory

View File

@ -1,11 +1,13 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Format Java files with Eclipse formatter
echo "Formatting Java files..."
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source logging utilities
source "$dir/logging/logging.sh"
log_title "Java Formatting"
# Store original directory # Store original directory
pushd "$dir" > /dev/null pushd "$dir" > /dev/null
@ -14,9 +16,17 @@ jar_file="eclipse-formatter/target/eclipse-formatter-1.0.0-jar-with-dependencies
src_file="eclipse-formatter/src/main/java/com/esotericsoftware/spine/formatter/EclipseFormatter.java" src_file="eclipse-formatter/src/main/java/com/esotericsoftware/spine/formatter/EclipseFormatter.java"
if [ ! -f "$jar_file" ] || [ "$src_file" -nt "$jar_file" ]; then if [ ! -f "$jar_file" ] || [ "$src_file" -nt "$jar_file" ]; then
echo "Building Eclipse formatter..." log_action "Building Eclipse formatter"
pushd eclipse-formatter > /dev/null pushd eclipse-formatter > /dev/null
mvn -q clean package if MVN_OUTPUT=$(mvn -q clean package 2>&1); then
log_ok
else
log_fail
log_error_output "$MVN_OUTPUT"
popd > /dev/null
popd > /dev/null
exit 1
fi
popd > /dev/null popd > /dev/null
fi fi
@ -30,13 +40,22 @@ java_files=$(find ../spine-libgdx ../spine-android -name "*.java" -type f \
# Run the formatter # Run the formatter
if [ -n "$java_files" ]; then if [ -n "$java_files" ]; then
echo "Running Eclipse formatter on Java files..." java_count=$(echo "$java_files" | wc -l | tr -d ' ')
java -jar eclipse-formatter/target/eclipse-formatter-1.0.0-jar-with-dependencies.jar \ log_action "Formatting $java_count Java files"
if FORMATTER_OUTPUT=$(java -jar eclipse-formatter/target/eclipse-formatter-1.0.0-jar-with-dependencies.jar \
eclipse-formatter.xml \ eclipse-formatter.xml \
$java_files $java_files 2>&1); then
log_ok
else
log_fail
log_error_output "$FORMATTER_OUTPUT"
popd > /dev/null
exit 1
fi
else
log_action "Formatting Java files"
log_skip
fi fi
echo "Java formatting complete"
# Return to original directory # Return to original directory
popd > /dev/null popd > /dev/null

View File

@ -1,22 +1,45 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Format Swift files
echo "Formatting Swift files..."
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source logging utilities
source "$dir/logging/logging.sh"
log_title "Swift Formatting"
# Store original directory # Store original directory
pushd "$dir" > /dev/null pushd "$dir" > /dev/null
if command -v swift-format &> /dev/null; then if command -v swift-format &> /dev/null; then
find .. -name "*.swift" \
swift_files=$(find .. -name "*.swift" -type f \
-not -path "*/.*" \ -not -path "*/.*" \
-not -path "*/build/*" \ -not -path "*/build/*" \
-not -path "*/DerivedData/*" \ -not -path "*/DerivedData/*" | wc -l | tr -d ' ')
| xargs swift-format -i
if [ "$swift_files" -gt 0 ]; then
log_action "Formatting $swift_files Swift files"
if SWIFT_OUTPUT=$(find .. -name "*.swift" -type f \
-not -path "*/.*" \
-not -path "*/build/*" \
-not -path "*/DerivedData/*" \
-print0 | xargs -0 swift-format --in-place --configuration .swift-format 2>&1); then
log_ok
else
log_fail
log_error_output "$SWIFT_OUTPUT"
popd > /dev/null
exit 1
fi
else
log_action "Formatting Swift files"
log_skip
fi
else else
echo "Warning: swift-format not found. Install from https://github.com/apple/swift-format" log_fail
log_error_output "swift-format not found. Install via brew install swift-format"
exit 1
fi fi
# Return to original directory # Return to original directory

View File

@ -1,29 +1,53 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Format TypeScript files with tsfmt
echo "Formatting TypeScript files..."
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source logging utilities
source "$dir/logging/logging.sh"
log_title "TypeScript Formatting"
# Store original directory # Store original directory
pushd "$dir" > /dev/null pushd "$dir" > /dev/null
# Check if tsfmt.json files match # Check if tsfmt.json files match
if ! cmp -s ../spine-ts/tsfmt.json ../tests/tsfmt.json; then log_action "Checking tsfmt.json consistency"
echo -e "\033[1;31mERROR: spine-ts/tsfmt.json and tests/tsfmt.json differ!\033[0m" if CMP_OUTPUT=$(cmp -s ../spine-ts/tsfmt.json ../tests/tsfmt.json 2>&1); then
echo -e "\033[1;31mPlease sync them to ensure consistent formatting.\033[0m" log_ok
else
log_fail
log_error_output "spine-ts/tsfmt.json and tests/tsfmt.json differ!"
log_detail "Please sync them to ensure consistent formatting."
popd > /dev/null popd > /dev/null
exit 1 exit 1
fi fi
# Format TypeScript files # Format TypeScript files
log_action "Formatting spine-ts TypeScript files"
pushd ../spine-ts > /dev/null pushd ../spine-ts > /dev/null
npm run format if NPM_OUTPUT=$(npm run format 2>&1); then
log_ok
else
log_fail
log_error_output "$NPM_OUTPUT"
popd > /dev/null
popd > /dev/null
exit 1
fi
popd > /dev/null popd > /dev/null
log_action "Formatting tests TypeScript files"
pushd ../tests > /dev/null pushd ../tests > /dev/null
npm run format if NPM_OUTPUT=$(npm run format 2>&1); then
log_ok
else
log_fail
log_error_output "$NPM_OUTPUT"
popd > /dev/null
popd > /dev/null
exit 1
fi
popd > /dev/null popd > /dev/null
# Return to original directory # Return to original directory

View File

@ -89,7 +89,8 @@ while [[ $# -gt 0 ]]; do
shift shift
;; ;;
*) *)
log_fail "Unknown option: $1" log_fail
log_error_output "Unknown option: $1"
log_detail "Use --help for usage information" log_detail "Use --help for usage information"
exit 1 exit 1
;; ;;
@ -98,54 +99,33 @@ done
log_title "Code Formatting" log_title "Code Formatting"
# Call individual formatter scripts # Call individual formatter scripts (they handle their own logging)
if [ "$FORMAT_CPP" = true ]; then if [ "$FORMAT_CPP" = true ]; then
log_section "C/C++"
log_action "Formatting C/C++ files"
"$dir/format-cpp.sh" "$dir/format-cpp.sh"
log_ok "C/C++ formatting completed"
fi fi
if [ "$FORMAT_JAVA" = true ]; then if [ "$FORMAT_JAVA" = true ]; then
log_section "Java"
log_action "Formatting Java files"
"$dir/format-java.sh" "$dir/format-java.sh"
log_ok "Java formatting completed"
fi fi
if [ "$FORMAT_CSHARP" = true ]; then if [ "$FORMAT_CSHARP" = true ]; then
log_section "C#"
log_action "Formatting C# files"
"$dir/format-csharp.sh" "$dir/format-csharp.sh"
log_ok "C# formatting completed"
fi fi
if [ "$FORMAT_TS" = true ]; then if [ "$FORMAT_TS" = true ]; then
log_section "TypeScript"
log_action "Formatting TypeScript files"
"$dir/format-ts.sh" "$dir/format-ts.sh"
log_ok "TypeScript formatting completed"
fi fi
if [ "$FORMAT_DART" = true ]; then if [ "$FORMAT_DART" = true ]; then
log_section "Dart"
log_action "Formatting Dart files"
"$dir/format-dart.sh" "$dir/format-dart.sh"
log_ok "Dart formatting completed"
fi fi
if [ "$FORMAT_HAXE" = true ]; then if [ "$FORMAT_HAXE" = true ]; then
log_section "Haxe"
log_action "Formatting Haxe files"
"$dir/format-haxe.sh" "$dir/format-haxe.sh"
log_ok "Haxe formatting completed"
fi fi
if [ "$FORMAT_SWIFT" = true ]; then if [ "$FORMAT_SWIFT" = true ]; then
log_section "Swift"
log_action "Formatting Swift files"
"$dir/format-swift.sh" "$dir/format-swift.sh"
log_ok "Swift formatting completed"
fi fi
log_summary "✓ All formatting completed" log_summary "✓ All formatting completed"

View File

@ -43,42 +43,41 @@ NC='\033[0m' # No Color
# Design principles: # Design principles:
# 1. Minimal visual noise - use color sparingly for emphasis # 1. Minimal visual noise - use color sparingly for emphasis
# 2. Clear hierarchy - different levels of information have different treatments # 2. Clear hierarchy - different levels of information have different treatments
# 3. Consistent spacing - clean vertical rhythm # 3. Consistent spacing - clean vertical rhythm
# 4. Accessible - readable without colors # 4. Accessible - readable without colors
# Main header for script/tool name # Main header for script/tool name
log_title() { log_title() {
echo "" echo -e "${GREEN}${BOLD}$1${NC}"
echo -e "${BOLD}$1${NC}"
echo ""
} }
# Section headers for major phases
log_section() {
echo -e "${BOLD}${BLUE}$1${NC}"
}
# Individual actions/steps # Individual actions/steps - inline result format
log_action() { log_action() {
echo -e " $1..." echo -n " $1... "
} }
# Results - success/failure/info # Results - success/failure/info (on same line)
log_ok() { log_ok() {
echo -e " ${GREEN}${NC} $1" echo -e "${GREEN}${NC}"
} }
log_fail() { log_fail() {
echo -e " ${RED}${NC} $1" echo -e "${RED}${NC}"
} }
log_warn() { log_warn() {
echo -e " ${YELLOW}!${NC} $1" echo -e "${YELLOW}!${NC}"
} }
log_skip() { log_skip() {
echo -e " ${GRAY}-${NC} $1" echo -e "${GRAY}-${NC}"
}
# For errors, show full output prominently (not grayed)
log_error_output() {
echo "$1"
} }
# Final summary # Final summary

View File

@ -18,48 +18,51 @@ This guide defines the terminal output style for all bash scripts in the Spine R
- **Usage**: Once at the beginning of script execution - **Usage**: Once at the beginning of script execution
```bash ```bash
log_title "Spine-C++ Test" log_title "Spine-C++ Build"
``` ```
### 2. Section (`log_section`) ### 2. Action + Result (inline format)
- **Purpose**: Major phases or groups of operations - **Purpose**: Individual operations with immediate result
- **Style**: Bold blue text, no extra spacing - **Style**: Action on same line as result for density
- **Usage**: Build, Test, Deploy, etc. - **Usage**: `log_action` followed immediately by `log_ok/fail/warn/skip`
```bash
log_section "Build"
log_section "Test"
```
### 3. Action (`log_action`)
- **Purpose**: Individual operations in progress
- **Style**: Indented, followed by "..."
- **Usage**: Before starting an operation
```bash ```bash
log_action "Building all variants" log_action "Building all variants"
log_action "Testing headless-test" log_ok # Green ✓ on same line
log_action "Testing headless-test"
log_fail # Red ✗ on same line
``` ```
### 4. Results **Results**:
- **Purpose**: Outcome of operations - `log_ok` - Green ✓ (success)
- **Style**: Indented with colored symbols - `log_fail` - Red ✗ (failure)
- `log_warn` - Yellow ! (warning)
- `log_skip` - Gray - (skipped)
### 4. Error Output (`log_error_output`)
- **Purpose**: Full error output when operations fail
- **Style**: Normal text (not grayed), prominent
- **Usage**: Show command output after failures
```bash ```bash
log_ok "Build completed" # Green ✓ log_action "Building"
log_fail "Build failed" # Red ✗ if BUILD_OUTPUT=$(command 2>&1); then
log_warn "Deprecated feature" # Yellow ! log_ok
log_skip "Not supported on macOS" # Gray - else
log_fail
log_error_output "$BUILD_OUTPUT"
fi
``` ```
### 5. Detail (`log_detail`) ### 5. Detail (`log_detail`)
- **Purpose**: Secondary information, error output, debug info - **Purpose**: Secondary information, debug info (not errors)
- **Style**: Gray text, indented - **Style**: Gray text, indented
- **Usage**: Additional context, error messages - **Usage**: Additional context, platform info
```bash ```bash
log_detail "Platform: Darwin" log_detail "Platform: Darwin"
log_detail "$ERROR_OUTPUT" log_detail "Branch: main"
``` ```
### 6. Summary (`log_summary`) ### 6. Summary (`log_summary`)
@ -78,58 +81,90 @@ log_summary "✗ Tests failed (3/5)"
#!/bin/bash #!/bin/bash
source ../formatters/logging/logging.sh source ../formatters/logging/logging.sh
log_title "Spine-C++ Test" log_title "Spine-C++ Build"
log_detail "Platform: $(uname)" log_detail "Platform: $(uname)"
log_section "Build" log_action "Configuring debug build"
log_action "Building all variants" if CMAKE_OUTPUT=$(cmake --preset=debug . 2>&1); then
if BUILD_OUTPUT=$(./build.sh clean release 2>&1); then log_ok
log_ok "Build completed"
else else
log_fail "Build failed" log_fail
log_detail "$BUILD_OUTPUT" log_error_output "$CMAKE_OUTPUT"
exit 1 exit 1
fi fi
log_section "Test" log_action "Building"
log_action "Testing headless-test" if BUILD_OUTPUT=$(cmake --build --preset=debug 2>&1); then
if test_result; then log_ok
log_ok "headless-test"
else else
log_fail "headless-test - execution failed" log_fail
log_detail "$error_output" log_error_output "$BUILD_OUTPUT"
exit 1
fi fi
log_summary "✓ All tests passed (2/2)" log_summary "✓ Build successful"
``` ```
## Output Preview ## Output Preview
**Success case:**
``` ```
Spine-C++ Test Spine-C++ Build
Platform: Darwin Platform: Darwin
Build Configuring debug build... ✓
Building all variants... Building... ✓
✓ Build completed
Test ✓ Build successful
Testing headless-test... ```
✓ headless-test
Testing headless-test-nostdcpp...
✓ headless-test-nostdcpp
✓ All tests passed (2/2) **Failure case:**
```
Spine-C++ Build
Platform: Darwin
Configuring debug build... ✗
CMake Error: Could not find cmake file...
(full error output shown prominently)
``` ```
## Error Handling Best Practices ## Error Handling Best Practices
1. **Capture output**: Use `OUTPUT=$(command 2>&1)` to capture both stdout and stderr 1. **Capture output**: Use `OUTPUT=$(command 2>&1)` to capture both stdout and stderr
2. **Check exit codes**: Always check if critical operations succeeded 2. **Check exit codes**: Always check if critical operations succeeded
3. **Show details on failure**: Use `log_detail` to show error output 3. **Show errors prominently**: Use `log_error_output` for command failures (not grayed)
4. **Fail fast**: Exit immediately on critical failures 4. **Fail fast**: Exit immediately on critical failures
5. **Clear error messages**: Make failure reasons obvious 5. **Use inline results**: `log_action` + `log_ok/fail` for dense, scannable output
## Calling Other Scripts
When calling other bash scripts that have their own logging:
1. **Trust their logging**: Don't wrap calls in redundant log actions
2. **Capture output for errors**: Use `OUTPUT=$(script.sh 2>&1)` to capture output and only show on failure
3. **Let them handle success**: Avoid "script completed" messages when the script shows its own status
**Good**:
```bash
# Let the script handle its own logging
../formatters/format.sh cpp
# Or capture output and only show on error
if output=$(./setup.sh 2>&1); then
log_ok "Setup completed"
else
log_fail "Setup failed"
log_detail "$output"
fi
```
**Avoid**:
```bash
# This creates duplicate logging
log_action "Formatting C++ files"
../formatters/format.sh cpp
log_ok "C++ formatting completed"
```
```bash ```bash
if BUILD_OUTPUT=$(./build.sh clean release 2>&1); then if BUILD_OUTPUT=$(./build.sh clean release 2>&1); then

View File

@ -34,34 +34,30 @@ fi
# Run codegen if requested # Run codegen if requested
if [ "$1" = "codegen" ]; then if [ "$1" = "codegen" ]; then
log_title "Spine-C Code Generation" log_title "spine-c code generation"
log_section "Generate"
log_action "Generating C bindings" log_action "Generating C bindings"
if CODEGEN_OUTPUT=$(npx -y tsx codegen/src/index.ts 2>&1); then if CODEGEN_OUTPUT=$(npx -y tsx codegen/src/index.ts 2>&1); then
log_ok "Code generation completed" log_ok
else else
log_fail "Code generation failed" log_fail
log_detail "$CODEGEN_OUTPUT" log_error_output "$CODEGEN_OUTPUT"
exit 1 exit 1
fi fi
log_section "Format"
log_action "Formatting generated C++ files"
../formatters/format.sh cpp ../formatters/format.sh cpp
log_summary "✓ Code generation successful" log_summary "✓ Code generation successful"
exit 0 exit 0
fi fi
log_title "Spine-C Build" log_title "spine-c build"
# Clean only if explicitly requested # Clean only if explicitly requested
if [ "$1" = "clean" ]; then if [ "$1" = "clean" ]; then
log_section "Clean" log_action "Cleaning build directory"
log_action "Removing build directory"
rm -rf build rm -rf build
log_ok "Cleaned" log_ok
fi fi
# Determine build type # Determine build type
@ -71,23 +67,21 @@ if [ "$1" = "release" ]; then
fi fi
# Configure and build # Configure and build
log_section "Configure"
log_action "Configuring $BUILD_TYPE build" log_action "Configuring $BUILD_TYPE build"
if CMAKE_OUTPUT=$(cmake --preset=$BUILD_TYPE . 2>&1); then if CMAKE_OUTPUT=$(cmake --preset=$BUILD_TYPE . 2>&1); then
log_ok "Configured" log_ok
else else
log_fail "Configuration failed" log_fail
log_detail "$CMAKE_OUTPUT" log_error_output "$CMAKE_OUTPUT"
exit 1 exit 1
fi fi
log_section "Build"
log_action "Building" log_action "Building"
if BUILD_OUTPUT=$(cmake --build --preset=$BUILD_TYPE 2>&1); then if BUILD_OUTPUT=$(cmake --build --preset=$BUILD_TYPE 2>&1); then
log_ok "Build completed" log_ok
else else
log_fail "Build failed" log_fail
log_detail "$BUILD_OUTPUT" log_error_output "$BUILD_OUTPUT"
exit 1 exit 1
fi fi

View File

@ -33,34 +33,31 @@ for arg in "$@"; do
esac esac
done done
log_title "Spine-C++ Build" log_title "spine-cpp build"
# Clean if requested # Clean if requested
if [ "$CLEAN" = "true" ]; then if [ "$CLEAN" = "true" ]; then
log_section "Clean" log_action "Cleaning build directory"
log_action "Removing build directory"
rm -rf build rm -rf build
log_ok "Cleaned" log_ok
fi fi
# Configure and build # Configure and build
log_section "Configure"
log_action "Configuring $BUILD_TYPE build" log_action "Configuring $BUILD_TYPE build"
if CMAKE_OUTPUT=$(cmake --preset=$BUILD_TYPE $NOFILEIO . 2>&1); then if CMAKE_OUTPUT=$(cmake --preset=$BUILD_TYPE $NOFILEIO . 2>&1); then
log_ok "Configured" log_ok
else else
log_fail "Configuration failed" log_fail
log_detail "$CMAKE_OUTPUT" log_error_output "$CMAKE_OUTPUT"
exit 1 exit 1
fi fi
log_section "Build"
log_action "Building" log_action "Building"
if BUILD_OUTPUT=$(cmake --build --preset=$BUILD_TYPE 2>&1); then if BUILD_OUTPUT=$(cmake --build --preset=$BUILD_TYPE 2>&1); then
log_ok "Build completed" log_ok
else else
log_fail "Build failed" log_fail
log_detail "$BUILD_OUTPUT" log_error_output "$BUILD_OUTPUT"
exit 1 exit 1
fi fi

View File

@ -3,7 +3,7 @@
# #
# Tests all spine-cpp build variants with spineboy example data: # Tests all spine-cpp build variants with spineboy example data:
# - headless-test (regular dynamic) # - headless-test (regular dynamic)
# - headless-test-nostdcpp (nostdcpp dynamic) # - headless-test-nostdcpp (nostdcpp dynamic)
# - headless-test-static (regular static, Linux only) # - headless-test-static (regular static, Linux only)
# - headless-test-nostdcpp-static (nostdcpp static, Linux only) # - headless-test-nostdcpp-static (nostdcpp static, Linux only)
@ -35,7 +35,6 @@ EXPECTED_OUTPUT="=== SKELETON DATA ===
log_title "Spine-C++ Test" log_title "Spine-C++ Test"
log_detail "Platform: $(uname)" log_detail "Platform: $(uname)"
log_section "Build"
log_action "Building all variants" log_action "Building all variants"
if BUILD_OUTPUT=$(./build.sh clean release 2>&1); then if BUILD_OUTPUT=$(./build.sh clean release 2>&1); then
log_ok "Build completed" log_ok "Build completed"
@ -45,8 +44,6 @@ else
exit 1 exit 1
fi fi
log_section "Test"
test_count=0 test_count=0
pass_count=0 pass_count=0
@ -54,12 +51,12 @@ for exe in build/headless-test*; do
if [ -f "$exe" ] && [ -x "$exe" ]; then if [ -f "$exe" ] && [ -x "$exe" ]; then
exe_name=$(basename "$exe") exe_name=$(basename "$exe")
log_action "Testing $exe_name" log_action "Testing $exe_name"
test_count=$((test_count + 1)) test_count=$((test_count + 1))
if OUTPUT=$("$exe" $SPINEBOY_SKEL $SPINEBOY_ATLAS $SPINEBOY_ANIM 2>&1); then if OUTPUT=$("$exe" $SPINEBOY_SKEL $SPINEBOY_ATLAS $SPINEBOY_ANIM 2>&1); then
actual_output=$(echo "$OUTPUT" | head -10) actual_output=$(echo "$OUTPUT" | head -10)
if [ "$actual_output" = "$EXPECTED_OUTPUT" ]; then if [ "$actual_output" = "$EXPECTED_OUTPUT" ]; then
log_ok "$exe_name" log_ok "$exe_name"
pass_count=$((pass_count + 1)) pass_count=$((pass_count + 1))

View File

@ -1,13 +1,13 @@
import UIKit
import Flutter import Flutter
import UIKit
@UIApplicationMain @UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
} }

View File

@ -5,6 +5,5 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
} }

View File

@ -3,7 +3,7 @@ import FlutterMacOS
@NSApplicationMain @NSApplicationMain
class AppDelegate: FlutterAppDelegate { class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true return true
} }
} }

View File

@ -2,14 +2,14 @@ import Cocoa
import FlutterMacOS import FlutterMacOS
class MainFlutterWindow: NSWindow { class MainFlutterWindow: NSWindow {
override func awakeFromNib() { override func awakeFromNib() {
let flutterViewController = FlutterViewController.init() let flutterViewController = FlutterViewController.init()
let windowFrame = self.frame let windowFrame = self.frame
self.contentViewController = flutterViewController self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true) self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController) RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib() super.awakeFromNib()
} }
} }

View File

@ -26,18 +26,24 @@ if echo "$COMMIT_MSG" | grep -qE '^\[haxe\] Release [0-9]+\.[0-9]+\.[0-9]+$'; th
if [ ! -z "$HAXE_UPDATE_URL" ] && [ ! -z "$BRANCH" ]; then if [ ! -z "$HAXE_UPDATE_URL" ] && [ ! -z "$BRANCH" ]; then
log_section "Deploy" log_section "Deploy"
log_action "Creating release package" log_action "Creating release package"
zip -r "spine-haxe-$VERSION.zip" \ if ZIP_OUTPUT=$(zip -r "spine-haxe-$VERSION.zip" \
haxelib.json \ haxelib.json \
LICENSE \ LICENSE \
README.md \ README.md \
spine-haxe > /dev/null 2>&1 spine-haxe 2>&1); then
log_ok "Package created" log_ok
else
log_fail
log_error_output "$ZIP_OUTPUT"
exit 1
fi
log_action "Uploading to $HAXE_UPDATE_URL$BRANCH" log_action "Uploading to $HAXE_UPDATE_URL$BRANCH"
if curl -f -F "file=@spine-haxe-$VERSION.zip" "$HAXE_UPDATE_URL$BRANCH" > /dev/null 2>&1; then if CURL_OUTPUT=$(curl -f -F "file=@spine-haxe-$VERSION.zip" "$HAXE_UPDATE_URL$BRANCH" 2>&1); then
log_ok "Package deployed" log_ok
else else
log_fail "Upload failed" log_fail
log_error_output "$CURL_OUTPUT"
exit 1 exit 1
fi fi

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import SwiftUI
import Spine import Spine
import SwiftUI
struct SimpleAnimation: View { struct SimpleAnimation: View {
@ -46,10 +46,10 @@ struct SimpleAnimation: View {
var body: some View { var body: some View {
SpineView( SpineView(
from: .bundle(atlasFileName: "spineboy-pma.atlas", skeletonFileName: "spineboy-pro.skel"), from: .bundle(atlasFileName: "spineboy-pma.atlas", skeletonFileName: "spineboy-pro.skel"),
// from: .http( // from: .http(
// atlasURL: URL(string: "https://github.com/esotericsoftware/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/spineboy/spineboy.atlas")!, // atlasURL: URL(string: "https://github.com/esotericsoftware/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/spineboy/spineboy.atlas")!,
// skeletonURL: URL(string: "https://github.com/esotericsoftware/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/spineboy/spineboy-pro.skel")! // skeletonURL: URL(string: "https://github.com/esotericsoftware/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/spineboy/spineboy-pro.skel")!
// ), // ),
controller: controller, controller: controller,
mode: .fit, mode: .fit,
alignment: .center alignment: .center

View File

@ -5,8 +5,8 @@
// Created by Denis Andrašec on 29.05.24. // Created by Denis Andrašec on 29.05.24.
// //
import SwiftUI
import SpineCppLite import SpineCppLite
import SwiftUI
@main @main
struct Spine_iOS_ExampleApp: App { struct Spine_iOS_ExampleApp: App {

View File

@ -27,12 +27,12 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import SwiftUI
import Spine import Spine
import SpineCppLite import SpineCppLite
import SwiftUI
struct AnimationStateEvents: View { struct AnimationStateEvents: View {
@StateObject @StateObject
var controller = SpineController( var controller = SpineController(
onInitialized: { controller in onInitialized: { controller in
@ -42,23 +42,25 @@ struct AnimationStateEvents: View {
controller.animationStateData.defaultMix = 0.2 controller.animationStateData.defaultMix = 0.2
let walk = controller.animationState.setAnimationByName(trackIndex: 0, animationName: "walk", loop: true) let walk = controller.animationState.setAnimationByName(trackIndex: 0, animationName: "walk", loop: true)
controller.animationStateWrapper.setTrackEntryListener(entry: walk) { type, entry, event in controller.animationStateWrapper.setTrackEntryListener(entry: walk) { type, entry, event in
print("Walk animation event \(type)"); print("Walk animation event \(type)")
} }
controller.animationState.addAnimationByName(trackIndex: 0, animationName: "jump", loop: false, delay: 2) controller.animationState.addAnimationByName(trackIndex: 0, animationName: "jump", loop: false, delay: 2)
let run = controller.animationState.addAnimationByName(trackIndex: 0, animationName: "run", loop: true, delay: 0) let run = controller.animationState.addAnimationByName(trackIndex: 0, animationName: "run", loop: true, delay: 0)
controller.animationStateWrapper.setTrackEntryListener(entry: run) { type, entry, event in controller.animationStateWrapper.setTrackEntryListener(entry: run) { type, entry, event in
print("Run animation event \(type)"); print("Run animation event \(type)")
} }
controller.animationStateWrapper.setStateListener { type, entry, event in controller.animationStateWrapper.setStateListener { type, entry, event in
if type == SPINE_EVENT_TYPE_EVENT, let event { if type == SPINE_EVENT_TYPE_EVENT, let event {
print("User event: { name: \(event.data.name ?? "--"), intValue: \(event.intValue), floatValue: \(event.floatValue), stringValue: \(event.stringValue ?? "--") }") print(
"User event: { name: \(event.data.name ?? "--"), intValue: \(event.intValue), floatValue: \(event.floatValue), stringValue: \(event.stringValue ?? "--") }"
)
} }
} }
let current = controller.animationState.getCurrent(trackIndex: 0)?.animation.name ?? "--" let current = controller.animationState.getCurrent(trackIndex: 0)?.animation.name ?? "--"
print("Current: \(current)") print("Current: \(current)")
} }
) )
var body: some View { var body: some View {
VStack { VStack {
Text("See output in console!") Text("See output in console!")

View File

@ -27,14 +27,14 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import SwiftUI
import Spine import Spine
import SwiftUI
struct DebugRendering: View { struct DebugRendering: View {
@StateObject @StateObject
var model = DebugRenderingModel() var model = DebugRenderingModel()
var body: some View { var body: some View {
ZStack { ZStack {
Color.red.ignoresSafeArea() Color.red.ignoresSafeArea()
@ -61,13 +61,13 @@ struct DebugRendering: View {
} }
final class DebugRenderingModel: ObservableObject { final class DebugRenderingModel: ObservableObject {
@Published @Published
var controller: SpineController! var controller: SpineController!
@Published @Published
var boneRects = [BoneRect]() var boneRects = [BoneRect]()
init() { init() {
controller = SpineController( controller = SpineController(
onInitialized: { controller in onInitialized: { controller in
@ -77,8 +77,9 @@ final class DebugRenderingModel: ObservableObject {
loop: true loop: true
) )
}, },
onAfterPaint: { onAfterPaint: {
[weak self] controller in guard let self else { return } [weak self] controller in
guard let self else { return }
boneRects = controller.drawable.skeleton.bones.map { bone in boneRects = controller.drawable.skeleton.bones.map { bone in
let position = controller.fromSkeletonCoordinates( let position = controller.fromSkeletonCoordinates(
position: CGPointMake(CGFloat(bone.worldX), CGFloat(bone.worldY)) position: CGPointMake(CGFloat(bone.worldX), CGFloat(bone.worldY))

View File

@ -27,11 +27,11 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import SwiftUI
import Spine import Spine
import SwiftUI
struct DisableRendering: View { struct DisableRendering: View {
@StateObject @StateObject
var controller = SpineController( var controller = SpineController(
onInitialized: { controller in onInitialized: { controller in
@ -42,10 +42,10 @@ struct DisableRendering: View {
) )
} }
) )
@State @State
var isRendering: Bool? var isRendering: Bool?
var body: some View { var body: some View {
VStack { VStack {
List { List {
@ -54,7 +54,7 @@ struct DisableRendering: View {
Text("Rendering is disabled when the spine view moves out of the viewport, preserving CPU/GPU resources.") Text("Rendering is disabled when the spine view moves out of the viewport, preserving CPU/GPU resources.")
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
SpineView( SpineView(
from: .bundle(atlasFileName: "spineboy-pma.atlas", skeletonFileName: "spineboy-pro.skel"), from: .bundle(atlasFileName: "spineboy-pma.atlas", skeletonFileName: "spineboy-pro.skel"),
controller: controller, controller: controller,
@ -69,13 +69,13 @@ struct DisableRendering: View {
isRendering = false isRendering = false
print("rendering disabled") print("rendering disabled")
} }
Text("Foo") Text("Foo")
.frame(minHeight: 400) .frame(minHeight: 400)
Text("Bar") Text("Bar")
.frame(minHeight: 400) .frame(minHeight: 400)
Text("Baz") Text("Baz")
.frame(minHeight: 400) .frame(minHeight: 400)
} }

View File

@ -27,15 +27,15 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import SwiftUI
import Spine import Spine
import SpineCppLite import SpineCppLite
import SwiftUI
struct DressUp: View { struct DressUp: View {
@StateObject @StateObject
var model = DressUpModel() var model = DressUpModel()
var body: some View { var body: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
List { List {
@ -51,9 +51,9 @@ struct DressUp: View {
} }
} }
.listStyle(.plain) .listStyle(.plain)
Divider() Divider()
if let drawable = model.drawable { if let drawable = model.drawable {
SpineView( SpineView(
from: .drawable(drawable), from: .drawable(drawable),
@ -74,23 +74,23 @@ struct DressUp: View {
} }
final class DressUpModel: ObservableObject { final class DressUpModel: ObservableObject {
let thumbnailSize = CGSize(width: 200, height: 200) let thumbnailSize = CGSize(width: 200, height: 200)
@Published @Published
var controller: SpineController var controller: SpineController
@Published @Published
var drawable: SkeletonDrawableWrapper? var drawable: SkeletonDrawableWrapper?
@Published @Published
var skinImages = [String: CGImage]() var skinImages = [String: CGImage]()
@Published @Published
var selectedSkins = [String: Bool]() var selectedSkins = [String: Bool]()
private var customSkin: Skin? private var customSkin: Skin?
init() { init() {
controller = SpineController( controller = SpineController(
onInitialized: { controller in onInitialized: { controller in
@ -129,28 +129,28 @@ final class DressUpModel: ObservableObject {
} }
} }
} }
deinit { deinit {
drawable?.dispose() drawable?.dispose()
customSkin?.dispose() customSkin?.dispose()
} }
func toggleSkin(skinName: String) { func toggleSkin(skinName: String) {
if let drawable { if let drawable {
toggleSkin(skinName: skinName, drawable: drawable) toggleSkin(skinName: skinName, drawable: drawable)
} }
} }
func toggleSkin(skinName: String, drawable: SkeletonDrawableWrapper) { func toggleSkin(skinName: String, drawable: SkeletonDrawableWrapper) {
selectedSkins[skinName] = !(selectedSkins[skinName] ?? false) selectedSkins[skinName] = !(selectedSkins[skinName] ?? false)
customSkin?.dispose() customSkin?.dispose()
customSkin = Skin.create(name: "custom-skin") customSkin = Skin.create(name: "custom-skin")
for skinName in selectedSkins.keys { for skinName in selectedSkins.keys {
if selectedSkins[skinName] == true { if selectedSkins[skinName] == true {
if let skin = drawable.skeletonData.findSkin(name: skinName) { if let skin = drawable.skeletonData.findSkin(name: skinName) {
customSkin?.addSkin(other: skin) customSkin?.addSkin(other: skin)
} }
} }
} }
drawable.skeleton.skin = customSkin drawable.skeleton.skin = customSkin
drawable.skeleton.setToSetupPose() drawable.skeleton.setToSetupPose()

View File

@ -27,14 +27,14 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import SwiftUI
import Spine import Spine
import SwiftUI
struct IKFollowing: View { struct IKFollowing: View {
@StateObject @StateObject
var model = IKFollowingModel() var model = IKFollowingModel()
var body: some View { var body: some View {
SpineView( SpineView(
from: .bundle(atlasFileName: "spineboy-pma.atlas", skeletonFileName: "spineboy-pro.skel"), from: .bundle(atlasFileName: "spineboy-pma.atlas", skeletonFileName: "spineboy-pro.skel"),
@ -64,13 +64,13 @@ struct IKFollowing: View {
} }
final class IKFollowingModel: ObservableObject { final class IKFollowingModel: ObservableObject {
@Published @Published
var controller: SpineController! var controller: SpineController!
@Published @Published
var crossHairPosition: CGPoint? var crossHairPosition: CGPoint?
init() { init() {
controller = SpineController( controller = SpineController(
onInitialized: { controller in onInitialized: { controller in
@ -85,14 +85,15 @@ final class IKFollowingModel: ObservableObject {
loop: true loop: true
) )
}, },
onAfterUpdateWorldTransforms: { onAfterUpdateWorldTransforms: {
[weak self] controller in guard let self else { return } [weak self] controller in
guard let self else { return }
guard let worldPosition = self.crossHairPosition else { guard let worldPosition = self.crossHairPosition else {
return return
} }
let bone = controller.skeleton.findBone(boneName: "crosshair")! let bone = controller.skeleton.findBone(boneName: "crosshair")!
if let parent = bone.parent { if let parent = bone.parent {
let position = parent.worldToLocal(worldX: Float(worldPosition.x), worldY: Float(worldPosition.y)) let position = parent.worldToLocal(worldX: Float(worldPosition.x), worldY: Float(worldPosition.y))
bone.x = position.x bone.x = position.x
bone.y = position.y bone.y = position.y
} }

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import SwiftUI
import Spine import Spine
import SwiftUI
struct MainView: View { struct MainView: View {
var body: some View { var body: some View {

View File

@ -27,28 +27,28 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import SwiftUI
import Spine import Spine
import SwiftUI
struct Physics: View { struct Physics: View {
@StateObject @StateObject
var model = PhysicsModel() var model = PhysicsModel()
var body: some View { var body: some View {
ZStack { ZStack {
Color(red: 51 / 255, green: 51 / 255, blue: 51 / 255).ignoresSafeArea() Color(red: 51 / 255, green: 51 / 255, blue: 51 / 255).ignoresSafeArea()
SpineView( SpineView(
from: .bundle(atlasFileName: "celestial-circus-pma.atlas", skeletonFileName: "celestial-circus-pro.skel"), from: .bundle(atlasFileName: "celestial-circus-pma.atlas", skeletonFileName: "celestial-circus-pro.skel"),
controller: model.controller controller: model.controller
) )
.gesture( .gesture(
DragGesture(minimumDistance: 0) DragGesture(minimumDistance: 0)
.onChanged { gesture in .onChanged { gesture in
model.updateBonePosition(position: gesture.location) model.updateBonePosition(position: gesture.location)
} }
) )
} }
.navigationTitle("Physics (drag anywhere)") .navigationTitle("Physics (drag anywhere)")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
@ -59,16 +59,16 @@ struct Physics: View {
} }
final class PhysicsModel: ObservableObject { final class PhysicsModel: ObservableObject {
@Published @Published
var controller: SpineController! var controller: SpineController!
@Published @Published
var mousePosition: CGPoint? var mousePosition: CGPoint?
@Published @Published
var lastMousePosition: CGPoint? var lastMousePosition: CGPoint?
init() { init() {
controller = SpineController( controller = SpineController(
onInitialized: { controller in onInitialized: { controller in
@ -84,8 +84,9 @@ final class PhysicsModel: ObservableObject {
) )
}, },
onAfterUpdateWorldTransforms: { onAfterUpdateWorldTransforms: {
[weak self] controller in guard let self else { return } [weak self] controller in
guard let self else { return }
guard let lastMousePosition else { guard let lastMousePosition else {
self.lastMousePosition = mousePosition self.lastMousePosition = mousePosition
return return
@ -102,7 +103,7 @@ final class PhysicsModel: ObservableObject {
} }
) )
} }
func updateBonePosition(position: CGPoint) { func updateBonePosition(position: CGPoint) {
mousePosition = controller.toSkeletonCoordinates(position: position) mousePosition = controller.toSkeletonCoordinates(position: position)
} }

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import SwiftUI
import Spine import Spine
import SwiftUI
struct PlayPauseAnimation: View { struct PlayPauseAnimation: View {
@ -46,10 +46,10 @@ struct PlayPauseAnimation: View {
var body: some View { var body: some View {
SpineView( SpineView(
from: .bundle(atlasFileName: "dragon.atlas", skeletonFileName: "dragon-ess.skel"), from: .bundle(atlasFileName: "dragon.atlas", skeletonFileName: "dragon-ess.skel"),
// from: .http( // from: .http(
// atlasURL: URL(string: "https://github.com/esotericsoftware/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/dragon/dragon.atlas")!, // atlasURL: URL(string: "https://github.com/esotericsoftware/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/dragon/dragon.atlas")!,
// skeletonURL: URL(string: "https://github.com/esotericsoftware/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/dragon/dragon-ess.skel")! // skeletonURL: URL(string: "https://github.com/esotericsoftware/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/dragon/dragon-ess.skel")!
// ), // ),
controller: controller, controller: controller,
boundsProvider: SkinAndAnimationBounds(animation: "flying") boundsProvider: SkinAndAnimationBounds(animation: "flying")
) )

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import SwiftUI
import Spine import Spine
import SwiftUI
struct SimpleAnimation: View { struct SimpleAnimation: View {
@ -46,10 +46,10 @@ struct SimpleAnimation: View {
var body: some View { var body: some View {
SpineView( SpineView(
from: .bundle(atlasFileName: "spineboy-pma.atlas", skeletonFileName: "spineboy-pro.skel"), from: .bundle(atlasFileName: "spineboy-pma.atlas", skeletonFileName: "spineboy-pro.skel"),
// from: .http( // from: .http(
// atlasURL: URL(string: "https://github.com/esotericsoftware/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/spineboy/spineboy.atlas")!, // atlasURL: URL(string: "https://github.com/esotericsoftware/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/spineboy/spineboy.atlas")!,
// skeletonURL: URL(string: "https://github.com/esotericsoftware/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/spineboy/spineboy-pro.skel")! // skeletonURL: URL(string: "https://github.com/esotericsoftware/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/spineboy/spineboy-pro.skel")!
// ), // ),
controller: controller, controller: controller,
mode: .fit, mode: .fit,
alignment: .center alignment: .center

View File

@ -31,11 +31,11 @@ import SwiftUI
struct SimpleAnimationViewControllerRepresentable: UIViewControllerRepresentable { struct SimpleAnimationViewControllerRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = SimpleAnimationViewController typealias UIViewControllerType = SimpleAnimationViewController
func makeUIViewController(context: Context) -> SimpleAnimationViewController { func makeUIViewController(context: Context) -> SimpleAnimationViewController {
return SimpleAnimationViewController() return SimpleAnimationViewController()
} }
func updateUIViewController(_ uiViewController: SimpleAnimationViewController, context: Context) { func updateUIViewController(_ uiViewController: SimpleAnimationViewController, context: Context) {
// //
} }

View File

@ -27,12 +27,12 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import SwiftUI
import Spine import Spine
import SwiftUI
@main @main
struct SpineExampleApp: App { struct SpineExampleApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
NavigationView { NavigationView {

View File

@ -39,20 +39,20 @@ public typealias AnimationStateListener = (_ type: EventType, _ entry: TrackEntr
@objc(SpineAnimationStateWrapper) @objc(SpineAnimationStateWrapper)
@objcMembers @objcMembers
public final class AnimationStateWrapper: NSObject { public final class AnimationStateWrapper: NSObject {
public let animationState: AnimationState public let animationState: AnimationState
public let aninationStateEvents: AnimationStateEvents public let aninationStateEvents: AnimationStateEvents
private var trackEntryListeners = [spine_track_entry: AnimationStateListener]() private var trackEntryListeners = [spine_track_entry: AnimationStateListener]()
private var stateListener: AnimationStateListener? private var stateListener: AnimationStateListener?
public init(animationState: AnimationState, aninationStateEvents: AnimationStateEvents) { public init(animationState: AnimationState, aninationStateEvents: AnimationStateEvents) {
self.animationState = animationState self.animationState = animationState
self.aninationStateEvents = aninationStateEvents self.aninationStateEvents = aninationStateEvents
super.init() super.init()
} }
/// The listener for events generated by the provided ``TrackEntry``, or nil. /// The listener for events generated by the provided ``TrackEntry``, or nil.
/// ///
/// A track entry returned from ``AnimationState/setAnimation(trackIndex:animation:loop:)`` is already the current animation /// A track entry returned from ``AnimationState/setAnimation(trackIndex:animation:loop:)`` is already the current animation
@ -64,18 +64,18 @@ public final class AnimationStateWrapper: NSObject {
trackEntryListeners.removeValue(forKey: entry.wrappee) trackEntryListeners.removeValue(forKey: entry.wrappee)
} }
} }
/// Increments each track entry ``TrackEntry/trackTime``, setting queued animations as current if needed. /// Increments each track entry ``TrackEntry/trackTime``, setting queued animations as current if needed.
public func update(delta: Float) { public func update(delta: Float) {
animationState.update(delta: delta) animationState.update(delta: delta)
let numEvents = spine_animation_state_events_get_num_events(aninationStateEvents.wrappee) let numEvents = spine_animation_state_events_get_num_events(aninationStateEvents.wrappee)
for i in 0..<numEvents { for i in 0..<numEvents {
let type = aninationStateEvents.getEventType(index: i) let type = aninationStateEvents.getEventType(index: i)
let entry = aninationStateEvents.getTrackEntry(index: i) let entry = aninationStateEvents.getTrackEntry(index: i)
let event = aninationStateEvents.getEvent(index: i) let event = aninationStateEvents.getEvent(index: i)
if let trackEntryListener = trackEntryListeners[entry.wrappee] { if let trackEntryListener = trackEntryListeners[entry.wrappee] {
trackEntryListener(type, entry, event) trackEntryListener(type, entry, event)
} }
@ -88,7 +88,7 @@ public final class AnimationStateWrapper: NSObject {
} }
aninationStateEvents.reset() aninationStateEvents.reset()
} }
/// The listener for events generated for all tracks managed by the ``AnimationState``, or nil. /// The listener for events generated for all tracks managed by the ``AnimationState``, or nil.
/// ///
/// A track entry returned from ``AnimationState/setAnimation(trackIndex:animation:loop:)`` is already the current animation /// A track entry returned from ``AnimationState/setAnimation(trackIndex:animation:loop:)`` is already the current animation

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import Foundation
import CoreGraphics import CoreGraphics
import Foundation
/// Base class for bounds providers. A bounds provider calculates the axis aligned bounding box /// Base class for bounds providers. A bounds provider calculates the axis aligned bounding box
/// used to scale and fit a skeleton inside the bounds of a ``SpineUIView``. /// used to scale and fit a skeleton inside the bounds of a ``SpineUIView``.
@ -42,7 +42,7 @@ public protocol BoundsProvider {
@objc(SpineSetupPoseBounds) @objc(SpineSetupPoseBounds)
@objcMembers @objcMembers
public final class SetupPoseBounds: NSObject, BoundsProvider { public final class SetupPoseBounds: NSObject, BoundsProvider {
public override init() { public override init() {
super.init() super.init()
} }
@ -79,10 +79,10 @@ public final class RawBounds: NSObject, BoundsProvider {
@objc(SpineSkinAndAnimationBounds) @objc(SpineSkinAndAnimationBounds)
@objcMembers @objcMembers
public final class SkinAndAnimationBounds: NSObject, BoundsProvider { public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
private let animation: String? private let animation: String?
private let skins: [String] private let skins: [String]
private let stepTime: TimeInterval; private let stepTime: TimeInterval
/// Constructs a new provider that will use the given `skins` and `animation` to calculate /// Constructs a new provider that will use the given `skins` and `animation` to calculate
/// the bounding box of the skeleton. If no skins are given, the default skin is used. /// the bounding box of the skeleton. If no skins are given, the default skin is used.
@ -98,7 +98,7 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
self.stepTime = stepTime self.stepTime = stepTime
super.init() super.init()
} }
public func computeBounds(for drawable: SkeletonDrawableWrapper) -> CGRect { public func computeBounds(for drawable: SkeletonDrawableWrapper) -> CGRect {
let data = drawable.skeletonData let data = drawable.skeletonData
let oldSkin: Skin? = drawable.skeleton.skin let oldSkin: Skin? = drawable.skeleton.skin
@ -110,7 +110,7 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
} }
} }
drawable.skeleton.skin = customSkin drawable.skeleton.skin = customSkin
drawable.skeleton.setToSetupPose(); drawable.skeleton.setToSetupPose()
let animation = animation.flatMap { data.findAnimation(name: $0) } let animation = animation.flatMap { data.findAnimation(name: $0) }
var minX = Float.Magnitude.greatestFiniteMagnitude var minX = Float.Magnitude.greatestFiniteMagnitude
@ -122,14 +122,14 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
let steps = Int(max(Double(animation.duration) / stepTime, 1.0)) let steps = Int(max(Double(animation.duration) / stepTime, 1.0))
for i in 0..<steps { for i in 0..<steps {
drawable.update(delta: i > 0 ? Float(stepTime) : 0.0) drawable.update(delta: i > 0 ? Float(stepTime) : 0.0)
let bounds = drawable.skeleton.bounds; let bounds = drawable.skeleton.bounds
minX = min(minX, bounds.x) minX = min(minX, bounds.x)
minY = min(minY, bounds.y) minY = min(minY, bounds.y)
maxX = max(maxX, minX + bounds.width) maxX = max(maxX, minX + bounds.width)
maxY = max(maxY, minY + bounds.height) maxY = max(maxY, minY + bounds.height)
} }
} else { } else {
let bounds = drawable.skeleton.bounds; let bounds = drawable.skeleton.bounds
minX = bounds.x minX = bounds.x
minY = bounds.y minY = bounds.y
maxX = minX + bounds.width maxX = minX + bounds.width
@ -137,7 +137,7 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
} }
drawable.skeleton.setSkinByName(skinName: "default") drawable.skeleton.setSkinByName(skinName: "default")
drawable.animationState.clearTracks() drawable.animationState.clearTracks()
if let oldSkin { if let oldSkin {
drawable.skeleton.skin = oldSkin drawable.skeleton.skin = oldSkin
} }
@ -145,14 +145,15 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
drawable.update(delta: 0) drawable.update(delta: 0)
customSkin.dispose() customSkin.dispose()
return CGRectMake(CGFloat(minX), CGFloat(minY), CGFloat(maxX - minX), CGFloat(maxY - minY)) return CGRectMake(CGFloat(minX), CGFloat(minY), CGFloat(maxX - minX), CGFloat(maxY - minY))
} }
} }
/// How a view should be inscribed into another view. /// How a view should be inscribed into another view.
@objc @objc
public enum ContentMode: Int { public enum ContentMode: Int {
case fit /// As large as possible while still containing the source view entirely within the target view. case fit
case fill /// Fill the target view by distorting the source's aspect ratio. /// As large as possible while still containing the source view entirely within the target view.
case fill/// Fill the target view by distorting the source's aspect ratio.
} }
/// How a view should aligned withing another view. /// How a view should aligned withing another view.
@ -167,7 +168,7 @@ public enum Alignment: Int {
case bottomLeft case bottomLeft
case bottomCenter case bottomCenter
case bottomRight case bottomRight
internal var x: CGFloat { internal var x: CGFloat {
switch self { switch self {
case .topLeft, .centerLeft, .bottomLeft: return -1.0 case .topLeft, .centerLeft, .bottomLeft: return -1.0
@ -175,7 +176,7 @@ public enum Alignment: Int {
case .topRight, .centerRight, .bottomRight: return 1.0 case .topRight, .centerRight, .bottomRight: return 1.0
} }
} }
internal var y: CGFloat { internal var y: CGFloat {
switch self { switch self {
case .topLeft, .topCenter, .topRight: return -1.0 case .topLeft, .topCenter, .topRight: return -1.0

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import UIKit
import MetalKit import MetalKit
import UIKit
extension MTLClearColor { extension MTLClearColor {
init(_ color: UIColor) { init(_ color: UIColor) {
@ -36,9 +36,9 @@ extension MTLClearColor {
var green: CGFloat = 0 var green: CGFloat = 0
var blue: CGFloat = 0 var blue: CGFloat = 0
var alpha: CGFloat = 0 var alpha: CGFloat = 0
color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
self.init(red: Double(red), green: Double(green), blue: Double(blue), alpha: Double(alpha)) self.init(red: Double(red), green: Double(green), blue: Double(blue), alpha: Double(alpha))
} }
} }

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import SpineShadersStructs
import Foundation import Foundation
import SpineShadersStructs
import simd import simd
extension RenderCommand { extension RenderCommand {
@ -55,7 +55,7 @@ extension RenderCommand {
) )
vertices.append(vertex) vertices.append(vertex)
} }
return vertices return vertices
} }

View File

@ -27,21 +27,21 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import CoreGraphics
import Foundation import Foundation
import UIKit import UIKit
import CoreGraphics
public extension SkeletonDrawableWrapper { extension SkeletonDrawableWrapper {
/// Render the ``Skeleton`` to a `CGImage` /// Render the ``Skeleton`` to a `CGImage`
/// ///
/// Parameters: /// Parameters:
/// - size: The size of the `CGImage` that should be rendered. /// - size: The size of the `CGImage` that should be rendered.
/// - backgroundColor: the background color of the image /// - backgroundColor: the background color of the image
/// - scaleFactor: The scale factor. Set this to `UIScreen.main.scale` if you want to show the image in a view /// - scaleFactor: The scale factor. Set this to `UIScreen.main.scale` if you want to show the image in a view
func renderToImage(size: CGSize, backgroundColor: UIColor, scaleFactor: CGFloat = 1) throws -> CGImage? { public func renderToImage(size: CGSize, backgroundColor: UIColor, scaleFactor: CGFloat = 1) throws -> CGImage? {
let spineView = SpineUIView( let spineView = SpineUIView(
controller: SpineController(disposeDrawableOnDeInit: false), // Doesn't own the drawable controller: SpineController(disposeDrawableOnDeInit: false), // Doesn't own the drawable
backgroundColor: backgroundColor backgroundColor: backgroundColor
) )
spineView.frame = CGRect(origin: .zero, size: size) spineView.frame = CGRect(origin: .zero, size: size)
@ -49,12 +49,12 @@ public extension SkeletonDrawableWrapper {
spineView.enableSetNeedsDisplay = false spineView.enableSetNeedsDisplay = false
spineView.framebufferOnly = false spineView.framebufferOnly = false
spineView.contentScaleFactor = scaleFactor spineView.contentScaleFactor = scaleFactor
try spineView.load(drawable: self) try spineView.load(drawable: self)
spineView.renderer?.waitUntilCompleted = true spineView.renderer?.waitUntilCompleted = true
spineView.delegate?.draw(in: spineView) spineView.delegate?.draw(in: spineView)
guard let texture = spineView.currentDrawable?.texture else { guard let texture = spineView.currentDrawable?.texture else {
throw SpineError("Could not read texture.") throw SpineError("Could not read texture.")
} }
@ -65,18 +65,22 @@ public extension SkeletonDrawableWrapper {
defer { defer {
data.deallocate() data.deallocate()
} }
let region = MTLRegionMake2D(0, 0, width, height) let region = MTLRegionMake2D(0, 0, width, height)
texture.getBytes(data, bytesPerRow: rowBytes, from: region, mipmapLevel: 0) texture.getBytes(data, bytesPerRow: rowBytes, from: region, mipmapLevel: 0)
let bitmapInfo = CGBitmapInfo( let bitmapInfo = CGBitmapInfo(
rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue
).union(.byteOrder32Little) ).union(.byteOrder32Little)
let colorSpace = CGColorSpaceCreateDeviceRGB() let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let context = CGContext(data: data, width: width, height: height, bitsPerComponent: 8, bytesPerRow: rowBytes, space: colorSpace, bitmapInfo: bitmapInfo.rawValue), guard
let cgImage = context.makeImage() else { let context = CGContext(
throw SpineError("Could not create image.") data: data, width: width, height: height, bitsPerComponent: 8, bytesPerRow: rowBytes, space: colorSpace,
bitmapInfo: bitmapInfo.rawValue),
let cgImage = context.makeImage()
else {
throw SpineError("Could not create image.")
} }
return cgImage return cgImage
} }

View File

@ -35,13 +35,13 @@ import MetalKit
/// Persistent Objects /// Persistent Objects
/// https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/PersistentObjects.html#//apple_ref/doc/uid/TP40016642-CH3-SW1 /// https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/PersistentObjects.html#//apple_ref/doc/uid/TP40016642-CH3-SW1
internal final class SpineObjects { internal final class SpineObjects {
static let shared = SpineObjects() static let shared = SpineObjects()
internal lazy var device: MTLDevice = { internal lazy var device: MTLDevice = {
MTLCreateSystemDefaultDevice()! MTLCreateSystemDefaultDevice()!
}() }()
internal lazy var commandQueue: MTLCommandQueue = { internal lazy var commandQueue: MTLCommandQueue = {
device.makeCommandQueue()! device.makeCommandQueue()!
}() }()

View File

@ -29,18 +29,18 @@
import Foundation import Foundation
import MetalKit import MetalKit
import SpineShadersStructs
import Spine import Spine
import SpineCppLite import SpineCppLite
import SpineShadersStructs
protocol SpineRendererDelegate: AnyObject { protocol SpineRendererDelegate: AnyObject {
func spineRendererWillUpdate(_ spineRenderer: SpineRenderer) func spineRendererWillUpdate(_ spineRenderer: SpineRenderer)
func spineRenderer(_ spineRenderer: SpineRenderer, needsUpdate delta: TimeInterval) func spineRenderer(_ spineRenderer: SpineRenderer, needsUpdate delta: TimeInterval)
func spineRendererDidUpdate(_ spineRenderer: SpineRenderer) func spineRendererDidUpdate(_ spineRenderer: SpineRenderer)
func spineRendererWillDraw(_ spineRenderer: SpineRenderer) func spineRendererWillDraw(_ spineRenderer: SpineRenderer)
func spineRendererDidDraw(_ spineRenderer: SpineRenderer) func spineRendererDidDraw(_ spineRenderer: SpineRenderer)
func spineRendererDidUpdate(_ spineRenderer: SpineRenderer, scaleX: CGFloat, scaleY: CGFloat, offsetX: CGFloat, offsetY: CGFloat, size: CGSize) func spineRendererDidUpdate(_ spineRenderer: SpineRenderer, scaleX: CGFloat, scaleY: CGFloat, offsetX: CGFloat, offsetY: CGFloat, size: CGSize)
} }
@ -50,11 +50,11 @@ protocol SpineRendererDataSource: AnyObject {
} }
internal final class SpineRenderer: NSObject, MTKViewDelegate { internal final class SpineRenderer: NSObject, MTKViewDelegate {
private let device: MTLDevice private let device: MTLDevice
private let textures: [MTLTexture] private let textures: [MTLTexture]
private let commandQueue: MTLCommandQueue private let commandQueue: MTLCommandQueue
private var sizeInPoints: CGSize = .zero private var sizeInPoints: CGSize = .zero
private var viewPortSize = vector_uint2(0, 0) private var viewPortSize = vector_uint2(0, 0)
private var transform = SpineTransform( private var transform = SpineTransform(
@ -65,17 +65,17 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
internal var lastDraw: CFTimeInterval = 0 internal var lastDraw: CFTimeInterval = 0
internal var waitUntilCompleted = false internal var waitUntilCompleted = false
private var pipelineStatesByBlendMode = [Int: MTLRenderPipelineState]() private var pipelineStatesByBlendMode = [Int: MTLRenderPipelineState]()
private static let numberOfBuffers = 3 private static let numberOfBuffers = 3
private static let defaultBufferSize = 32 * 1024 // 32KB private static let defaultBufferSize = 32 * 1024 // 32KB
private var buffers = [MTLBuffer]() private var buffers = [MTLBuffer]()
private let bufferingSemaphore = DispatchSemaphore(value: SpineRenderer.numberOfBuffers) private let bufferingSemaphore = DispatchSemaphore(value: SpineRenderer.numberOfBuffers)
private var currentBufferIndex: Int = 0 private var currentBufferIndex: Int = 0
weak var dataSource: SpineRendererDataSource? weak var dataSource: SpineRendererDataSource?
weak var delegate: SpineRendererDelegate? weak var delegate: SpineRendererDelegate?
internal init( internal init(
device: MTLDevice, device: MTLDevice,
commandQueue: MTLCommandQueue, commandQueue: MTLCommandQueue,
@ -85,18 +85,19 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
) throws { ) throws {
self.device = device self.device = device
self.commandQueue = commandQueue self.commandQueue = commandQueue
let bundle: Bundle let bundle: Bundle
#if SWIFT_PACKAGE // SPM #if SWIFT_PACKAGE // SPM
bundle = .module bundle = .module
#else // CocoaPods #else // CocoaPods
let bundleURL = Bundle(for: SpineRenderer.self).url(forResource: "SpineBundle", withExtension: "bundle") let bundleURL = Bundle(for: SpineRenderer.self).url(forResource: "SpineBundle", withExtension: "bundle")
bundle = Bundle(url: bundleURL!)! bundle = Bundle(url: bundleURL!)!
#endif #endif
let defaultLibrary = try device.makeDefaultLibrary(bundle: bundle) let defaultLibrary = try device.makeDefaultLibrary(bundle: bundle)
let textureLoader = MTKTextureLoader(device: device) let textureLoader = MTKTextureLoader(device: device)
textures = try atlasPages textures =
try atlasPages
.compactMap { $0.cgImage } .compactMap { $0.cgImage }
.map { .map {
try textureLoader.newTexture( try textureLoader.newTexture(
@ -107,12 +108,12 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
] ]
) )
} }
let blendModes = [ let blendModes = [
SPINE_BLEND_MODE_NORMAL, SPINE_BLEND_MODE_NORMAL,
SPINE_BLEND_MODE_ADDITIVE, SPINE_BLEND_MODE_ADDITIVE,
SPINE_BLEND_MODE_MULTIPLY, SPINE_BLEND_MODE_MULTIPLY,
SPINE_BLEND_MODE_SCREEN SPINE_BLEND_MODE_SCREEN,
] ]
for blendMode in blendModes { for blendMode in blendModes {
let descriptor = MTLRenderPipelineDescriptor() let descriptor = MTLRenderPipelineDescriptor()
@ -125,15 +126,15 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
) )
pipelineStatesByBlendMode[Int(blendMode.rawValue)] = try device.makeRenderPipelineState(descriptor: descriptor) pipelineStatesByBlendMode[Int(blendMode.rawValue)] = try device.makeRenderPipelineState(descriptor: descriptor)
} }
super.init() super.init()
increaseBuffersSize(to: SpineRenderer.defaultBufferSize) increaseBuffersSize(to: SpineRenderer.defaultBufferSize)
} }
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
guard let spineView = view as? SpineUIView else { return } guard let spineView = view as? SpineUIView else { return }
sizeInPoints = CGSize(width: size.width / UIScreen.main.scale, height: size.height / UIScreen.main.scale) sizeInPoints = CGSize(width: size.width / UIScreen.main.scale, height: size.height / UIScreen.main.scale)
viewPortSize = vector_uint2(UInt32(size.width), UInt32(size.height)) viewPortSize = vector_uint2(UInt32(size.width), UInt32(size.height))
setTransform( setTransform(
@ -142,35 +143,36 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
alignment: spineView.alignment alignment: spineView.alignment
) )
} }
func draw(in view: MTKView) { func draw(in view: MTKView) {
guard dataSource?.isPlaying(self) ?? false else { guard dataSource?.isPlaying(self) ?? false else {
lastDraw = CACurrentMediaTime() lastDraw = CACurrentMediaTime()
return return
} }
callNeedsUpdate() callNeedsUpdate()
// Tripple Buffering // Tripple Buffering
// Source: https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/TripleBuffering.html#//apple_ref/doc/uid/TP40016642-CH5-SW1 // Source: https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/TripleBuffering.html#//apple_ref/doc/uid/TP40016642-CH5-SW1
bufferingSemaphore.wait() bufferingSemaphore.wait()
currentBufferIndex = (currentBufferIndex + 1) % SpineRenderer.numberOfBuffers currentBufferIndex = (currentBufferIndex + 1) % SpineRenderer.numberOfBuffers
guard let renderCommands = dataSource?.renderCommands(self), guard let renderCommands = dataSource?.renderCommands(self),
let commandBuffer = commandQueue.makeCommandBuffer(), let commandBuffer = commandQueue.makeCommandBuffer(),
let renderPassDescriptor = view.currentRenderPassDescriptor, let renderPassDescriptor = view.currentRenderPassDescriptor,
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
// this can happen if, else {
// - CAMetalLayer is configured with drawable timeout, and CAMetalLayer is run out of Drawable // this can happen if,
// - CAMetalLayer is added to the window with frame size of zero or incorrect layout constraint -> currentRenderPassDescriptor is null // - CAMetalLayer is configured with drawable timeout, and CAMetalLayer is run out of Drawable
// - CAMetalLayer is added to the window with frame size of zero or incorrect layout constraint -> currentRenderPassDescriptor is null
bufferingSemaphore.signal() bufferingSemaphore.signal()
return return
} }
delegate?.spineRendererWillDraw(self) delegate?.spineRendererWillDraw(self)
draw(renderCommands: renderCommands, renderEncoder: renderEncoder, in: view) draw(renderCommands: renderCommands, renderEncoder: renderEncoder, in: view)
delegate?.spineRendererDidDraw(self) delegate?.spineRendererDidDraw(self)
renderEncoder.endEncoding() renderEncoder.endEncoding()
view.currentDrawable.flatMap { view.currentDrawable.flatMap {
commandBuffer.present($0) commandBuffer.present($0)
@ -183,14 +185,14 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
commandBuffer.waitUntilCompleted() commandBuffer.waitUntilCompleted()
} }
} }
private func setTransform(bounds: CGRect, mode: Spine.ContentMode, alignment: Spine.Alignment) { private func setTransform(bounds: CGRect, mode: Spine.ContentMode, alignment: Spine.Alignment) {
let x = -bounds.minX - bounds.width / 2.0 let x = -bounds.minX - bounds.width / 2.0
let y = -bounds.minY - bounds.height / 2.0 let y = -bounds.minY - bounds.height / 2.0
var scaleX: CGFloat = 1.0 var scaleX: CGFloat = 1.0
var scaleY: CGFloat = 1.0 var scaleY: CGFloat = 1.0
switch mode { switch mode {
case .fit: case .fit:
scaleX = min(sizeInPoints.width / bounds.width, sizeInPoints.height / bounds.height) scaleX = min(sizeInPoints.width / bounds.width, sizeInPoints.height / bounds.height)
@ -199,16 +201,16 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
scaleX = max(sizeInPoints.width / bounds.width, sizeInPoints.height / bounds.height) scaleX = max(sizeInPoints.width / bounds.width, sizeInPoints.height / bounds.height)
scaleY = scaleX scaleY = scaleX
} }
let offsetX = abs(sizeInPoints.width - bounds.width * scaleX) / 2 * alignment.x let offsetX = abs(sizeInPoints.width - bounds.width * scaleX) / 2 * alignment.x
let offsetY = abs(sizeInPoints.height - bounds.height * scaleY) / 2 * alignment.y let offsetY = abs(sizeInPoints.height - bounds.height * scaleY) / 2 * alignment.y
transform = SpineTransform( transform = SpineTransform(
translation: vector_float2(Float(x), Float(y)), translation: vector_float2(Float(x), Float(y)),
scale: vector_float2(Float(scaleX * UIScreen.main.scale), Float(scaleY * UIScreen.main.scale)), scale: vector_float2(Float(scaleX * UIScreen.main.scale), Float(scaleY * UIScreen.main.scale)),
offset: vector_float2(Float(offsetX * UIScreen.main.scale), Float(offsetY * UIScreen.main.scale)) offset: vector_float2(Float(offsetX * UIScreen.main.scale), Float(offsetY * UIScreen.main.scale))
) )
delegate?.spineRendererDidUpdate( delegate?.spineRendererDidUpdate(
self, self,
scaleX: scaleX, scaleX: scaleX,
@ -218,37 +220,37 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
size: sizeInPoints size: sizeInPoints
) )
} }
private func callNeedsUpdate() { private func callNeedsUpdate() {
if lastDraw == 0 { if lastDraw == 0 {
lastDraw = CACurrentMediaTime() lastDraw = CACurrentMediaTime()
} }
let delta = CACurrentMediaTime() - lastDraw let delta = CACurrentMediaTime() - lastDraw
delegate?.spineRendererWillUpdate(self) delegate?.spineRendererWillUpdate(self)
delegate?.spineRenderer(self, needsUpdate: delta) delegate?.spineRenderer(self, needsUpdate: delta)
lastDraw = CACurrentMediaTime() lastDraw = CACurrentMediaTime()
delegate?.spineRendererDidUpdate(self) delegate?.spineRendererDidUpdate(self)
} }
private func draw(renderCommands: [RenderCommand], renderEncoder: MTLRenderCommandEncoder, in view: MTKView) { private func draw(renderCommands: [RenderCommand], renderEncoder: MTLRenderCommandEncoder, in view: MTKView) {
let allVertices = renderCommands.map { renderCommand in let allVertices = renderCommands.map { renderCommand in
Array(renderCommand.getVertices()) Array(renderCommand.getVertices())
} }
let vertices = allVertices.flatMap { $0 } let vertices = allVertices.flatMap { $0 }
let verticesSize = MemoryLayout<SpineVertex>.stride * vertices.count let verticesSize = MemoryLayout<SpineVertex>.stride * vertices.count
guard verticesSize > 0 else { guard verticesSize > 0 else {
return return
} }
var vertexBuffer = buffers[currentBufferIndex] var vertexBuffer = buffers[currentBufferIndex]
var vertexBufferSize = vertexBuffer.length var vertexBufferSize = vertexBuffer.length
if vertexBufferSize < verticesSize { if vertexBufferSize < verticesSize {
increaseBuffersSize(to: verticesSize) increaseBuffersSize(to: verticesSize)
vertexBuffer = buffers[currentBufferIndex] vertexBuffer = buffers[currentBufferIndex]
} }
renderEncoder.setViewport( renderEncoder.setViewport(
MTLViewport( MTLViewport(
originX: 0.0, originX: 0.0,
@ -259,9 +261,9 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
zfar: 1.0 zfar: 1.0
) )
) )
memcpy(vertexBuffer.contents(), vertices, verticesSize) memcpy(vertexBuffer.contents(), vertices, verticesSize)
renderEncoder.setVertexBuffer( renderEncoder.setVertexBuffer(
vertexBuffer, vertexBuffer,
offset: 0, offset: 0,
@ -277,7 +279,7 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
length: MemoryLayout.size(ofValue: viewPortSize), length: MemoryLayout.size(ofValue: viewPortSize),
index: Int(SpineVertexInputIndexViewportSize.rawValue) index: Int(SpineVertexInputIndexViewportSize.rawValue)
) )
// Buffer Bindings // Buffer Bindings
// https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/BufferBindings.html#//apple_ref/doc/uid/TP40016642-CH28-SW3 // https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/BufferBindings.html#//apple_ref/doc/uid/TP40016642-CH28-SW3
var vertexStart = 0 var vertexStart = 0
@ -286,9 +288,9 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
continue continue
} }
renderEncoder.setRenderPipelineState(pipelineState) renderEncoder.setRenderPipelineState(pipelineState)
let vertices = allVertices[index] let vertices = allVertices[index]
let textureIndex = Int(renderCommand.atlasPage) let textureIndex = Int(renderCommand.atlasPage)
if textures.indices.contains(textureIndex) { if textures.indices.contains(textureIndex) {
renderEncoder.setFragmentTexture( renderEncoder.setFragmentTexture(
@ -296,7 +298,7 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
index: Int(SpineTextureIndexBaseColor.rawValue) index: Int(SpineTextureIndexBaseColor.rawValue)
) )
} }
renderEncoder.drawPrimitives( renderEncoder.drawPrimitives(
type: .triangle, type: .triangle,
vertexStart: vertexStart, vertexStart: vertexStart,
@ -305,89 +307,89 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
vertexStart += vertices.count vertexStart += vertices.count
} }
} }
private func getPipelineState(blendMode: BlendMode) -> MTLRenderPipelineState? { private func getPipelineState(blendMode: BlendMode) -> MTLRenderPipelineState? {
pipelineStatesByBlendMode[Int(blendMode.rawValue)] pipelineStatesByBlendMode[Int(blendMode.rawValue)]
} }
private func increaseBuffersSize(to size: Int) { private func increaseBuffersSize(to size: Int) {
buffers = (0 ..< SpineRenderer.numberOfBuffers).map { _ in buffers = (0..<SpineRenderer.numberOfBuffers).map { _ in
device.makeBuffer(length: size, options: .storageModeShared)! device.makeBuffer(length: size, options: .storageModeShared)!
} }
} }
} }
fileprivate extension BlendMode { extension BlendMode {
func sourceRGBBlendFactor(premultipliedAlpha: Bool) -> MTLBlendFactor { fileprivate func sourceRGBBlendFactor(premultipliedAlpha: Bool) -> MTLBlendFactor {
switch self { switch self {
case SPINE_BLEND_MODE_NORMAL: case SPINE_BLEND_MODE_NORMAL:
return premultipliedAlpha ? .one : .sourceAlpha return premultipliedAlpha ? .one : .sourceAlpha
case SPINE_BLEND_MODE_ADDITIVE: case SPINE_BLEND_MODE_ADDITIVE:
// additvie only needs sourceAlpha multiply if it is not pma // additvie only needs sourceAlpha multiply if it is not pma
return premultipliedAlpha ? .one : .sourceAlpha return premultipliedAlpha ? .one : .sourceAlpha
case SPINE_BLEND_MODE_MULTIPLY: case SPINE_BLEND_MODE_MULTIPLY:
return .destinationColor return .destinationColor
case SPINE_BLEND_MODE_SCREEN: case SPINE_BLEND_MODE_SCREEN:
return .one return .one
default: default:
return .one // Should never be called return .one // Should never be called
} }
} }
var sourceAlphaBlendFactor: MTLBlendFactor {
// pma and non-pma has no-relation ship with alpha blending
switch self {
case SPINE_BLEND_MODE_NORMAL:
return .one
case SPINE_BLEND_MODE_ADDITIVE:
return .one
case SPINE_BLEND_MODE_MULTIPLY:
return .oneMinusSourceAlpha
case SPINE_BLEND_MODE_SCREEN:
return .oneMinusSourceColor
default:
return .one // Should never be called
}
}
var destinationRGBBlendFactor: MTLBlendFactor { fileprivate var sourceAlphaBlendFactor: MTLBlendFactor {
switch self { // pma and non-pma has no-relation ship with alpha blending
case SPINE_BLEND_MODE_NORMAL: switch self {
return .oneMinusSourceAlpha case SPINE_BLEND_MODE_NORMAL:
case SPINE_BLEND_MODE_ADDITIVE: return .one
return .one case SPINE_BLEND_MODE_ADDITIVE:
case SPINE_BLEND_MODE_MULTIPLY: return .one
return .oneMinusSourceAlpha case SPINE_BLEND_MODE_MULTIPLY:
case SPINE_BLEND_MODE_SCREEN: return .oneMinusSourceAlpha
return .oneMinusSourceColor case SPINE_BLEND_MODE_SCREEN:
default: return .oneMinusSourceColor
return .one // Should never be called default:
} return .one // Should never be called
} }
}
var destinationAlphaBlendFactor: MTLBlendFactor { fileprivate var destinationRGBBlendFactor: MTLBlendFactor {
switch self { switch self {
case SPINE_BLEND_MODE_NORMAL: case SPINE_BLEND_MODE_NORMAL:
return .oneMinusSourceAlpha return .oneMinusSourceAlpha
case SPINE_BLEND_MODE_ADDITIVE: case SPINE_BLEND_MODE_ADDITIVE:
return .one return .one
case SPINE_BLEND_MODE_MULTIPLY: case SPINE_BLEND_MODE_MULTIPLY:
return .oneMinusSourceAlpha return .oneMinusSourceAlpha
case SPINE_BLEND_MODE_SCREEN: case SPINE_BLEND_MODE_SCREEN:
return .oneMinusSourceColor return .oneMinusSourceColor
default: default:
return .one // Should never be called return .one // Should never be called
} }
} }
fileprivate var destinationAlphaBlendFactor: MTLBlendFactor {
switch self {
case SPINE_BLEND_MODE_NORMAL:
return .oneMinusSourceAlpha
case SPINE_BLEND_MODE_ADDITIVE:
return .one
case SPINE_BLEND_MODE_MULTIPLY:
return .oneMinusSourceAlpha
case SPINE_BLEND_MODE_SCREEN:
return .oneMinusSourceColor
default:
return .one // Should never be called
}
}
} }
fileprivate extension MTLRenderPipelineColorAttachmentDescriptor { extension MTLRenderPipelineColorAttachmentDescriptor {
func apply(blendMode: BlendMode, with premultipliedAlpha: Bool) { fileprivate func apply(blendMode: BlendMode, with premultipliedAlpha: Bool) {
isBlendingEnabled = true isBlendingEnabled = true
sourceRGBBlendFactor = blendMode.sourceRGBBlendFactor(premultipliedAlpha: premultipliedAlpha) sourceRGBBlendFactor = blendMode.sourceRGBBlendFactor(premultipliedAlpha: premultipliedAlpha)
sourceAlphaBlendFactor = blendMode.sourceAlphaBlendFactor sourceAlphaBlendFactor = blendMode.sourceAlphaBlendFactor
destinationRGBBlendFactor = blendMode.destinationRGBBlendFactor destinationRGBBlendFactor = blendMode.destinationRGBBlendFactor
destinationAlphaBlendFactor = blendMode.destinationAlphaBlendFactor destinationAlphaBlendFactor = blendMode.destinationAlphaBlendFactor
} }
} }

View File

@ -27,10 +27,10 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import CoreGraphics
import Foundation import Foundation
import Spine import Spine
import SpineCppLite import SpineCppLite
import CoreGraphics
import UIKit import UIKit
/// A ``SkeletonDrawableWrapper`` with ``SkeletonDrawable`` bundle loading, updating, and rendering an ``Atlas``, ``Skeleton``, and ``AnimationState`` /// A ``SkeletonDrawableWrapper`` with ``SkeletonDrawable`` bundle loading, updating, and rendering an ``Atlas``, ``Skeleton``, and ``AnimationState``
@ -54,19 +54,19 @@ import UIKit
@objc(SpineSkeletonDrawableWrapper) @objc(SpineSkeletonDrawableWrapper)
@objcMembers @objcMembers
public final class SkeletonDrawableWrapper: NSObject { public final class SkeletonDrawableWrapper: NSObject {
public let atlas: Atlas public let atlas: Atlas
public let atlasPages: [UIImage] public let atlasPages: [UIImage]
public let skeletonData: SkeletonData public let skeletonData: SkeletonData
public let skeletonDrawable: SkeletonDrawable public let skeletonDrawable: SkeletonDrawable
public let skeleton: Skeleton public let skeleton: Skeleton
public let animationStateData: AnimationStateData public let animationStateData: AnimationStateData
public let animationState: AnimationState public let animationState: AnimationState
public let animationStateWrapper: AnimationStateWrapper public let animationStateWrapper: AnimationStateWrapper
internal var disposed = false internal var disposed = false
/// Constructs a new skeleton drawable from the `atlasFileName` and `skeletonFileName` from the `main` bundle /// Constructs a new skeleton drawable from the `atlasFileName` and `skeletonFileName` from the `main` bundle
/// or the optionally provided `bundle`. /// or the optionally provided `bundle`.
/// ///
@ -84,7 +84,7 @@ public final class SkeletonDrawableWrapper: NSObject {
skeletonData: skeletonData skeletonData: skeletonData
) )
} }
/// Constructs a new skeleton drawable from the `atlasFile` and `skeletonFile`. /// Constructs a new skeleton drawable from the `atlasFile` and `skeletonFile`.
/// ///
/// Throws an `Error` in case the data could not be loaded. /// Throws an `Error` in case the data could not be loaded.
@ -100,7 +100,7 @@ public final class SkeletonDrawableWrapper: NSObject {
skeletonData: skeletonData skeletonData: skeletonData
) )
} }
/// Constructs a new skeleton drawable wrapper from the http `atlasUrl` and `skeletonUrl`. /// Constructs a new skeleton drawable wrapper from the http `atlasUrl` and `skeletonUrl`.
/// ///
/// Throws an `Error` in case the data could not be loaded. /// Throws an `Error` in case the data could not be loaded.
@ -116,27 +116,27 @@ public final class SkeletonDrawableWrapper: NSObject {
skeletonData: skeletonData skeletonData: skeletonData
) )
} }
public init(atlas: Atlas, atlasPages: [UIImage], skeletonData: SkeletonData) throws { public init(atlas: Atlas, atlasPages: [UIImage], skeletonData: SkeletonData) throws {
self.atlas = atlas self.atlas = atlas
self.atlasPages = atlasPages self.atlasPages = atlasPages
self.skeletonData = skeletonData self.skeletonData = skeletonData
guard let nativeSkeletonDrawable = spine_skeleton_drawable_create(skeletonData.wrappee) else { guard let nativeSkeletonDrawable = spine_skeleton_drawable_create(skeletonData.wrappee) else {
throw SpineError("Could not load native skeleton drawable") throw SpineError("Could not load native skeleton drawable")
} }
skeletonDrawable = SkeletonDrawable(nativeSkeletonDrawable) skeletonDrawable = SkeletonDrawable(nativeSkeletonDrawable)
guard let nativeSkeleton = spine_skeleton_drawable_get_skeleton(skeletonDrawable.wrappee) else { guard let nativeSkeleton = spine_skeleton_drawable_get_skeleton(skeletonDrawable.wrappee) else {
throw SpineError("Could not load native skeleton") throw SpineError("Could not load native skeleton")
} }
skeleton = Skeleton(nativeSkeleton) skeleton = Skeleton(nativeSkeleton)
guard let nativeAnimationStateData = spine_skeleton_drawable_get_animation_state_data(skeletonDrawable.wrappee) else { guard let nativeAnimationStateData = spine_skeleton_drawable_get_animation_state_data(skeletonDrawable.wrappee) else {
throw SpineError("Could not load native animation state data") throw SpineError("Could not load native animation state data")
} }
animationStateData = AnimationStateData(nativeAnimationStateData) animationStateData = AnimationStateData(nativeAnimationStateData)
guard let nativeAnimationState = spine_skeleton_drawable_get_animation_state(skeletonDrawable.wrappee) else { guard let nativeAnimationState = spine_skeleton_drawable_get_animation_state(skeletonDrawable.wrappee) else {
throw SpineError("Could not load native animation state") throw SpineError("Could not load native animation state")
} }
@ -148,27 +148,27 @@ public final class SkeletonDrawableWrapper: NSObject {
skeleton.updateWorldTransform(physics: SPINE_PHYSICS_NONE) skeleton.updateWorldTransform(physics: SPINE_PHYSICS_NONE)
super.init() super.init()
} }
/// Updates the ``AnimationState`` using the `delta` time given in seconds, applies the /// Updates the ``AnimationState`` using the `delta` time given in seconds, applies the
/// animation state to the ``Skeleton`` and updates the world transforms of the skeleton /// animation state to the ``Skeleton`` and updates the world transforms of the skeleton
/// to calculate its current pose. /// to calculate its current pose.
public func update(delta: Float) { public func update(delta: Float) {
if disposed { return } if disposed { return }
animationStateWrapper.update(delta: delta) animationStateWrapper.update(delta: delta)
animationState.apply(skeleton: skeleton) animationState.apply(skeleton: skeleton)
skeleton.update(delta: delta) skeleton.update(delta: delta)
skeleton.updateWorldTransform(physics: SPINE_PHYSICS_UPDATE) skeleton.updateWorldTransform(physics: SPINE_PHYSICS_UPDATE)
} }
public func dispose() { public func dispose() {
if disposed { return } if disposed { return }
disposed = true disposed = true
atlas.dispose() atlas.dispose()
skeletonData.dispose() skeletonData.dispose()
skeletonDrawable.dispose() skeletonDrawable.dispose()
} }
} }

View File

@ -28,8 +28,8 @@
*****************************************************************************/ *****************************************************************************/
import Foundation import Foundation
import SwiftUI
import SpineCppLite import SpineCppLite
import SwiftUI
public var version: String { public var version: String {
return "\(majorVersion).\(minorVersion)" return "\(majorVersion).\(minorVersion)"
@ -49,22 +49,22 @@ public var minorVersion: Int {
/// ///
/// Use the static methods ``Atlas/fromBundle(_:bundle:)``, ``Atlas/fromFile(_:)``, and ``Atlas/fromHttp(_:)`` to load an atlas. Call ``Atlas/dispose()` /// Use the static methods ``Atlas/fromBundle(_:bundle:)``, ``Atlas/fromFile(_:)``, and ``Atlas/fromHttp(_:)`` to load an atlas. Call ``Atlas/dispose()`
/// when the atlas is no longer in use to release its resources. /// when the atlas is no longer in use to release its resources.
public extension Atlas { extension Atlas {
/// Loads an ``Atlas`` from the file with name `atlasFileName` in the `main` bundle or the optionally provided [bundle]. /// Loads an ``Atlas`` from the file with name `atlasFileName` in the `main` bundle or the optionally provided [bundle].
/// ///
/// Throws an `Error` in case the atlas could not be loaded. /// Throws an `Error` in case the atlas could not be loaded.
static func fromBundle(_ atlasFileName: String, bundle: Bundle = .main) async throws -> (Atlas, [UIImage]) { public static func fromBundle(_ atlasFileName: String, bundle: Bundle = .main) async throws -> (Atlas, [UIImage]) {
let data = try await FileSource.bundle(fileName: atlasFileName, bundle: bundle).load() let data = try await FileSource.bundle(fileName: atlasFileName, bundle: bundle).load()
return try await Self.fromData(data: data) { name in return try await Self.fromData(data: data) { name in
return try await FileSource.bundle(fileName: name, bundle: bundle).load() return try await FileSource.bundle(fileName: name, bundle: bundle).load()
} }
} }
/// Loads an ``Atlas`` from the file URL `atlasFile`. /// Loads an ``Atlas`` from the file URL `atlasFile`.
/// ///
/// Throws an `Error` in case the atlas could not be loaded. /// Throws an `Error` in case the atlas could not be loaded.
static func fromFile(_ atlasFile: URL) async throws -> (Atlas, [UIImage]) { public static func fromFile(_ atlasFile: URL) async throws -> (Atlas, [UIImage]) {
let data = try await FileSource.file(atlasFile).load() let data = try await FileSource.file(atlasFile).load()
return try await Self.fromData(data: data) { name in return try await Self.fromData(data: data) { name in
let dir = atlasFile.deletingLastPathComponent() let dir = atlasFile.deletingLastPathComponent()
@ -72,11 +72,11 @@ public extension Atlas {
return try await FileSource.file(file).load() return try await FileSource.file(file).load()
} }
} }
/// Loads an ``Atlas`` from the http URL `atlasURL`. /// Loads an ``Atlas`` from the http URL `atlasURL`.
/// ///
/// Throws an `Error` in case the atlas could not be loaded. /// Throws an `Error` in case the atlas could not be loaded.
static func fromHttp(_ atlasURL: URL) async throws -> (Atlas, [UIImage]) { public static func fromHttp(_ atlasURL: URL) async throws -> (Atlas, [UIImage]) {
let data = try await FileSource.http(atlasURL).load() let data = try await FileSource.http(atlasURL).load()
return try await Self.fromData(data: data) { name in return try await Self.fromData(data: data) { name in
let dir = atlasURL.deletingLastPathComponent() let dir = atlasURL.deletingLastPathComponent()
@ -84,7 +84,7 @@ public extension Atlas {
return try await FileSource.http(file).load() return try await FileSource.http(file).load()
} }
} }
private static func fromData(data: Data, loadFile: (_ name: String) async throws -> Data) async throws -> (Atlas, [UIImage]) { private static func fromData(data: Data, loadFile: (_ name: String) async throws -> Data) async throws -> (Atlas, [UIImage]) {
guard let atlasData = String(data: data, encoding: .utf8) else { guard let atlasData = String(data: data, encoding: .utf8) else {
throw SpineError("Couldn't read atlas bytes as utf8 string") throw SpineError("Couldn't read atlas bytes as utf8 string")
@ -100,10 +100,10 @@ public extension Atlas {
spine_atlas_dispose(atlas) spine_atlas_dispose(atlas)
throw SpineError("Couldn't load atlas: \(message)") throw SpineError("Couldn't load atlas: \(message)")
} }
var atlasPages = [UIImage]() var atlasPages = [UIImage]()
let numImagePaths = spine_atlas_get_num_image_paths(atlas); let numImagePaths = spine_atlas_get_num_image_paths(atlas)
for i in 0..<numImagePaths { for i in 0..<numImagePaths {
guard let atlasPageFilePointer = spine_atlas_get_image_path(atlas, i) else { guard let atlasPageFilePointer = spine_atlas_get_image_path(atlas, i) else {
continue continue
@ -115,52 +115,52 @@ public extension Atlas {
} }
atlasPages.append(image) atlasPages.append(image)
} }
return (Atlas(atlas), atlasPages) return (Atlas(atlas), atlasPages)
} }
} }
public extension SkeletonData { extension SkeletonData {
/// Loads a ``SkeletonData`` from the file with name `skeletonFileName` in the main bundle or the optionally provided `bundle`. /// Loads a ``SkeletonData`` from the file with name `skeletonFileName` in the main bundle or the optionally provided `bundle`.
/// Uses the provided ``Atlas`` to resolve attachment images. /// Uses the provided ``Atlas`` to resolve attachment images.
/// ///
/// Throws an `Error` in case the skeleton data could not be loaded. /// Throws an `Error` in case the skeleton data could not be loaded.
static func fromBundle(atlas: Atlas, skeletonFileName: String, bundle: Bundle = .main) async throws -> SkeletonData { public static func fromBundle(atlas: Atlas, skeletonFileName: String, bundle: Bundle = .main) async throws -> SkeletonData {
return try fromData( return try fromData(
atlas: atlas, atlas: atlas,
data: try await FileSource.bundle(fileName: skeletonFileName, bundle: bundle).load(), data: try await FileSource.bundle(fileName: skeletonFileName, bundle: bundle).load(),
isJson: skeletonFileName.hasSuffix(".json") isJson: skeletonFileName.hasSuffix(".json")
) )
} }
/// Loads a ``SkeletonData`` from the file URL `skeletonFile`. Uses the provided ``Atlas`` to resolve attachment images. /// Loads a ``SkeletonData`` from the file URL `skeletonFile`. Uses the provided ``Atlas`` to resolve attachment images.
/// ///
/// Throws an `Error` in case the skeleton data could not be loaded. /// Throws an `Error` in case the skeleton data could not be loaded.
static func fromFile(atlas: Atlas, skeletonFile: URL) async throws -> SkeletonData { public static func fromFile(atlas: Atlas, skeletonFile: URL) async throws -> SkeletonData {
return try fromData( return try fromData(
atlas: atlas, atlas: atlas,
data: try await FileSource.file(skeletonFile).load(), data: try await FileSource.file(skeletonFile).load(),
isJson: skeletonFile.absoluteString.hasSuffix(".json") isJson: skeletonFile.absoluteString.hasSuffix(".json")
) )
} }
/// Loads a ``SkeletonData`` from the http URL `skeletonFile`. Uses the provided ``Atlas`` to resolve attachment images. /// Loads a ``SkeletonData`` from the http URL `skeletonFile`. Uses the provided ``Atlas`` to resolve attachment images.
/// ///
/// Throws an `Error` in case the skeleton data could not be loaded. /// Throws an `Error` in case the skeleton data could not be loaded.
static func fromHttp(atlas: Atlas, skeletonURL: URL) async throws -> SkeletonData { public static func fromHttp(atlas: Atlas, skeletonURL: URL) async throws -> SkeletonData {
return try fromData( return try fromData(
atlas: atlas, atlas: atlas,
data: try await FileSource.http(skeletonURL).load(), data: try await FileSource.http(skeletonURL).load(),
isJson: skeletonURL.absoluteString.hasSuffix(".json") isJson: skeletonURL.absoluteString.hasSuffix(".json")
) )
} }
/// Loads a ``SkeletonData`` from the ``binary`` skeleton `Data`, using the provided ``Atlas`` to resolve attachment images. /// Loads a ``SkeletonData`` from the ``binary`` skeleton `Data`, using the provided ``Atlas`` to resolve attachment images.
/// ///
/// Throws an `Error` in case the skeleton data could not be loaded. /// Throws an `Error` in case the skeleton data could not be loaded.
static func fromData(atlas: Atlas, data: Data) throws -> SkeletonData { public static func fromData(atlas: Atlas, data: Data) throws -> SkeletonData {
let result = try data.withUnsafeBytes{ let result = try data.withUnsafeBytes {
try $0.withMemoryRebound(to: UInt8.self) { buffer in try $0.withMemoryRebound(to: UInt8.self) { buffer in
guard let ptr = buffer.baseAddress else { guard let ptr = buffer.baseAddress else {
throw SpineError("Couldn't read atlas binary") throw SpineError("Couldn't read atlas binary")
@ -187,16 +187,17 @@ public extension SkeletonData {
} }
return SkeletonData(data) return SkeletonData(data)
} }
/// Loads a ``SkeletonData`` from the `json` string, using the provided ``Atlas`` to resolve attachment /// Loads a ``SkeletonData`` from the `json` string, using the provided ``Atlas`` to resolve attachment
/// images. /// images.
/// ///
/// Throws an `Error` in case the atlas could not be loaded. /// Throws an `Error` in case the atlas could not be loaded.
static func fromJson(atlas: Atlas, json: String) throws -> SkeletonData { public static func fromJson(atlas: Atlas, json: String) throws -> SkeletonData {
let result = try json.utf8CString.withUnsafeBufferPointer { buffer in let result = try json.utf8CString.withUnsafeBufferPointer { buffer in
guard guard
let basePtr = buffer.baseAddress, let basePtr = buffer.baseAddress,
let result = spine_skeleton_data_load_json(atlas.wrappee, basePtr) else { let result = spine_skeleton_data_load_json(atlas.wrappee, basePtr)
else {
throw SpineError("Couldn't load skeleton data json") throw SpineError("Couldn't load skeleton data json")
} }
return result return result
@ -213,7 +214,7 @@ public extension SkeletonData {
} }
return SkeletonData(data) return SkeletonData(data)
} }
private static func fromData(atlas: Atlas, data: Data, isJson: Bool) throws -> SkeletonData { private static func fromData(atlas: Atlas, data: Data, isJson: Bool) throws -> SkeletonData {
if isJson { if isJson {
guard let json = String(data: data, encoding: .utf8) else { guard let json = String(data: data, encoding: .utf8) else {
@ -226,12 +227,12 @@ public extension SkeletonData {
} }
} }
internal extension SkeletonDrawable { extension SkeletonDrawable {
func render() -> [RenderCommand] { func render() -> [RenderCommand] {
var commands = [RenderCommand]() var commands = [RenderCommand]()
if disposed { return commands } if disposed { return commands }
var nativeCmd = spine_skeleton_drawable_render(wrappee) var nativeCmd = spine_skeleton_drawable_render(wrappee)
repeat { repeat {
if let ncmd = nativeCmd { if let ncmd = nativeCmd {
@ -240,18 +241,18 @@ internal extension SkeletonDrawable {
} else { } else {
nativeCmd = nil nativeCmd = nil
} }
} while (nativeCmd != nil) } while nativeCmd != nil
return commands return commands
} }
} }
internal extension RenderCommand { extension RenderCommand {
var numVertices: Int { var numVertices: Int {
Int(spine_render_command_get_num_vertices(wrappee)) Int(spine_render_command_get_num_vertices(wrappee))
} }
func positions(numVertices: Int) -> [Float] { func positions(numVertices: Int) -> [Float] {
let num = numVertices * 2 let num = numVertices * 2
let ptr = spine_render_command_get_positions(wrappee) let ptr = spine_render_command_get_positions(wrappee)
@ -259,7 +260,7 @@ internal extension RenderCommand {
let buffer = UnsafeBufferPointer(start: validPtr, count: num) let buffer = UnsafeBufferPointer(start: validPtr, count: num)
return Array(buffer) return Array(buffer)
} }
func uvs(numVertices: Int) -> [Float] { func uvs(numVertices: Int) -> [Float] {
let num = numVertices * 2 let num = numVertices * 2
let ptr = spine_render_command_get_uvs(wrappee) let ptr = spine_render_command_get_uvs(wrappee)
@ -267,8 +268,8 @@ internal extension RenderCommand {
let buffer = UnsafeBufferPointer(start: validPtr, count: num) let buffer = UnsafeBufferPointer(start: validPtr, count: num)
return Array(buffer) return Array(buffer)
} }
func colors(numVertices: Int) ->[Int32] { func colors(numVertices: Int) -> [Int32] {
let num = numVertices let num = numVertices
let ptr = spine_render_command_get_colors(wrappee) let ptr = spine_render_command_get_colors(wrappee)
guard let validPtr = ptr else { return [] } guard let validPtr = ptr else { return [] }
@ -277,21 +278,21 @@ internal extension RenderCommand {
} }
} }
public extension Skin { extension Skin {
/// Constructs a new empty ``Skin`` using the given `name`. Skins constructed this way must be manually disposed via the `dispose` method /// Constructs a new empty ``Skin`` using the given `name`. Skins constructed this way must be manually disposed via the `dispose` method
/// if they are no longer used. /// if they are no longer used.
static func create(name: String) -> Skin { public static func create(name: String) -> Skin {
return Skin(spine_skin_create(name)) return Skin(spine_skin_create(name))
} }
} }
// Helper // Helper
public extension CGRect { extension CGRect {
/// Construct a `CGRect` from ``Bounds`` /// Construct a `CGRect` from ``Bounds``
init(bounds: Bounds) { public init(bounds: Bounds) {
self = CGRect( self = CGRect(
x: CGFloat(bounds.x), x: CGFloat(bounds.x),
y: CGFloat(bounds.y), y: CGFloat(bounds.y),
@ -305,7 +306,7 @@ internal enum FileSource {
case bundle(fileName: String, bundle: Bundle = .main) case bundle(fileName: String, bundle: Bundle = .main)
case file(URL) case file(URL)
case http(URL) case http(URL)
internal func load() async throws -> Data { internal func load() async throws -> Data {
switch self { switch self {
case .bundle(let fileName, let bundle): case .bundle(let fileName, let bundle):
@ -314,7 +315,7 @@ internal enum FileSource {
throw SpineError("Provide both file name and file extension") throw SpineError("Provide both file name and file extension")
} }
let name = components.dropLast(1).joined(separator: ".") let name = components.dropLast(1).joined(separator: ".")
guard let fileUrl = bundle.url(forResource: name, withExtension: String(ext)) else { guard let fileUrl = bundle.url(forResource: name, withExtension: String(ext)) else {
throw SpineError("Could not load file with name \(name) from bundle") throw SpineError("Could not load file with name \(name) from bundle")
} }
@ -331,9 +332,9 @@ internal enum FileSource {
} else { } else {
let lock = NSRecursiveLock() let lock = NSRecursiveLock()
nonisolated(unsafe) nonisolated(unsafe)
var isCancelled = false var isCancelled = false
nonisolated(unsafe) nonisolated(unsafe)
var taskHolder:URLSessionDownloadTask? = nil var taskHolder: URLSessionDownloadTask? = nil
return try await withTaskCancellationHandler { return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in try await withCheckedThrowingContinuation { continuation in
let task = URLSession.shared.downloadTask(with: url) { temp, response, error in let task = URLSession.shared.downloadTask(with: url) { temp, response, error in
@ -380,25 +381,25 @@ internal enum FileSource {
} }
public struct SpineError: Error, CustomStringConvertible { public struct SpineError: Error, CustomStringConvertible {
public let description: String public let description: String
internal init(_ description: String) { internal init(_ description: String) {
self.description = description self.description = description
} }
} }
public extension SkeletonBounds { extension SkeletonBounds {
static func create() -> SkeletonBounds { public static func create() -> SkeletonBounds {
return SkeletonBounds(spine_skeleton_bounds_create()) return SkeletonBounds(spine_skeleton_bounds_create())
} }
} }
@objc public extension Atlas { @objc extension Atlas {
var imagePathCount:Int32 { public var imagePathCount: Int32 {
spine_atlas_get_num_image_paths(wrappee) spine_atlas_get_num_image_paths(wrappee)
} }
} }

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import Foundation
import CoreGraphics import CoreGraphics
import Foundation
import QuartzCore import QuartzCore
import UIKit import UIKit
@ -63,27 +63,27 @@ public typealias SpineControllerCallback = (_ controller: SpineController) -> Vo
/// Per default, ``SkeletonDrawableWrapper`` is disposed when ``SpineController`` is deinitialized. You can disable this behaviour with the ``disposeDrawableOnDeInit`` contructor parameter. /// Per default, ``SkeletonDrawableWrapper`` is disposed when ``SpineController`` is deinitialized. You can disable this behaviour with the ``disposeDrawableOnDeInit`` contructor parameter.
@objcMembers @objcMembers
public final class SpineController: NSObject, ObservableObject { public final class SpineController: NSObject, ObservableObject {
public internal(set) var drawable: SkeletonDrawableWrapper! public internal(set) var drawable: SkeletonDrawableWrapper!
private let onInitialized: SpineControllerCallback? private let onInitialized: SpineControllerCallback?
private let onBeforeUpdateWorldTransforms: SpineControllerCallback? private let onBeforeUpdateWorldTransforms: SpineControllerCallback?
private let onAfterUpdateWorldTransforms: SpineControllerCallback? private let onAfterUpdateWorldTransforms: SpineControllerCallback?
private let onBeforePaint: SpineControllerCallback? private let onBeforePaint: SpineControllerCallback?
private let onAfterPaint: SpineControllerCallback? private let onAfterPaint: SpineControllerCallback?
private let disposeDrawableOnDeInit: Bool private let disposeDrawableOnDeInit: Bool
private var scaleX: CGFloat = 1 private var scaleX: CGFloat = 1
private var scaleY: CGFloat = 1 private var scaleY: CGFloat = 1
private var offsetX: CGFloat = 0 private var offsetX: CGFloat = 0
private var offsetY: CGFloat = 0 private var offsetY: CGFloat = 0
@Published @Published
public private(set) var isPlaying: Bool = true public private(set) var isPlaying: Bool = true
@Published @Published
public private(set) var viewSize: CGSize = .zero public private(set) var viewSize: CGSize = .zero
/// Constructs a new ``SpineUIview`` controller. See the class documentation of ``SpineWidgetController`` for information on /// Constructs a new ``SpineUIview`` controller. See the class documentation of ``SpineWidgetController`` for information on
/// the optional arguments. /// the optional arguments.
public init( public init(
@ -100,80 +100,80 @@ public final class SpineController: NSObject, ObservableObject {
self.onBeforePaint = onBeforePaint self.onBeforePaint = onBeforePaint
self.onAfterPaint = onAfterPaint self.onAfterPaint = onAfterPaint
self.disposeDrawableOnDeInit = disposeDrawableOnDeInit self.disposeDrawableOnDeInit = disposeDrawableOnDeInit
super.init() super.init()
} }
deinit { deinit {
if disposeDrawableOnDeInit { if disposeDrawableOnDeInit {
drawable?.dispose() // TODO move drawable out of view? drawable?.dispose() // TODO move drawable out of view?
} }
} }
/// The ``Atlas`` from which images to render the skeleton are sourced. /// The ``Atlas`` from which images to render the skeleton are sourced.
public var atlas: Atlas { public var atlas: Atlas {
drawable.atlas drawable.atlas
} }
/// The setup-pose data used by the skeleton. /// The setup-pose data used by the skeleton.
public var skeletonData: SkeletonData { public var skeletonData: SkeletonData {
drawable.skeletonData drawable.skeletonData
} }
/// The ``Skeleton`` /// The ``Skeleton``
public var skeleton: Skeleton { public var skeleton: Skeleton {
drawable.skeleton drawable.skeleton
} }
/// The mixing information used by the ``AnimationState`` /// The mixing information used by the ``AnimationState``
public var animationStateData: AnimationStateData { public var animationStateData: AnimationStateData {
drawable.animationStateData drawable.animationStateData
} }
/// The ``AnimationState`` used to manage animations that are being applied to the /// The ``AnimationState`` used to manage animations that are being applied to the
/// skeleton. /// skeleton.
public var animationState: AnimationState { public var animationState: AnimationState {
drawable.animationState drawable.animationState
} }
/// The ``AnimationStateWrapper`` used to hold ``AnimationState``, register ``AnimationStateListener`` and call ``AnimationStateWrapper/update(delta:)`` /// The ``AnimationStateWrapper`` used to hold ``AnimationState``, register ``AnimationStateListener`` and call ``AnimationStateWrapper/update(delta:)``
public var animationStateWrapper: AnimationStateWrapper { public var animationStateWrapper: AnimationStateWrapper {
drawable.animationStateWrapper drawable.animationStateWrapper
} }
/// Transforms the coordinates given in the ``SpineUIView`` coordinate system in `position` to /// Transforms the coordinates given in the ``SpineUIView`` coordinate system in `position` to
/// the skeleton coordinate system. See the `IKFollowing.swift` example how to use this /// the skeleton coordinate system. See the `IKFollowing.swift` example how to use this
/// to move a bone based on user touch input. /// to move a bone based on user touch input.
public func toSkeletonCoordinates(position: CGPoint) -> CGPoint { public func toSkeletonCoordinates(position: CGPoint) -> CGPoint {
let x = position.x; let x = position.x
let y = position.y; let y = position.y
return CGPoint( return CGPoint(
x: (x - viewSize.width / 2) / scaleX - offsetX, x: (x - viewSize.width / 2) / scaleX - offsetX,
y: (y - viewSize.height / 2) / scaleY - offsetY y: (y - viewSize.height / 2) / scaleY - offsetY
) )
} }
/// Transforms the coordinates given in skeleton coordinate system to /// Transforms the coordinates given in skeleton coordinate system to
/// the the ``SpineUIView`` coordinates. See the `DebugRendering.swift` example hot to use this to draw rectangles over skeleton bones for debugging purposes. /// the the ``SpineUIView`` coordinates. See the `DebugRendering.swift` example hot to use this to draw rectangles over skeleton bones for debugging purposes.
public func fromSkeletonCoordinates(position: CGPoint) -> CGPoint { public func fromSkeletonCoordinates(position: CGPoint) -> CGPoint {
let x = position.x; let x = position.x
let y = position.y; let y = position.y
return CGPoint( return CGPoint(
x: (x + offsetX) * scaleX, x: (x + offsetX) * scaleX,
y: (y + offsetY) * scaleY y: (y + offsetY) * scaleY
) )
} }
/// Pauses updating and rendering the skeleton. /// Pauses updating and rendering the skeleton.
public func pause() { public func pause() {
isPlaying = false isPlaying = false
} }
/// Resumes updating and rendering the skeleton. /// Resumes updating and rendering the skeleton.
public func resume() { public func resume() {
isPlaying = true isPlaying = true
} }
internal func load(atlasFile: String, skeletonFile: String, bundle: Bundle = .main) async throws { internal func load(atlasFile: String, skeletonFile: String, bundle: Bundle = .main) async throws {
let atlasAndPages = try await Atlas.fromBundle(atlasFile, bundle: bundle) let atlasAndPages = try await Atlas.fromBundle(atlasFile, bundle: bundle)
let skeletonData = try await SkeletonData.fromBundle( let skeletonData = try await SkeletonData.fromBundle(
@ -190,23 +190,23 @@ public final class SpineController: NSObject, ObservableObject {
self.drawable = skeletonDrawableWrapper self.drawable = skeletonDrawableWrapper
} }
} }
internal func initialize() { internal func initialize() {
onInitialized?(self) onInitialized?(self)
} }
} }
extension SpineController: SpineRendererDelegate { extension SpineController: SpineRendererDelegate {
func spineRendererWillDraw(_ spineRenderer: SpineRenderer) { func spineRendererWillDraw(_ spineRenderer: SpineRenderer) {
onBeforePaint?(self) onBeforePaint?(self)
} }
func spineRendererDidDraw(_ spineRenderer: SpineRenderer) { func spineRendererDidDraw(_ spineRenderer: SpineRenderer) {
onAfterPaint?(self) onAfterPaint?(self)
} }
func spineRendererDidUpdate(_ spineRenderer: SpineRenderer, scaleX: CGFloat, scaleY: CGFloat, offsetX: CGFloat, offsetY: CGFloat, size: CGSize) { func spineRendererDidUpdate(_ spineRenderer: SpineRenderer, scaleX: CGFloat, scaleY: CGFloat, offsetX: CGFloat, offsetY: CGFloat, size: CGSize) {
self.scaleX = scaleX self.scaleX = scaleX
self.scaleY = scaleY self.scaleY = scaleY
@ -217,27 +217,27 @@ extension SpineController: SpineRendererDelegate {
} }
extension SpineController: SpineRendererDataSource { extension SpineController: SpineRendererDataSource {
func spineRendererWillUpdate(_ spineRenderer: SpineRenderer) { func spineRendererWillUpdate(_ spineRenderer: SpineRenderer) {
onBeforeUpdateWorldTransforms?(self) onBeforeUpdateWorldTransforms?(self)
} }
func spineRendererDidUpdate(_ spineRenderer: SpineRenderer) { func spineRendererDidUpdate(_ spineRenderer: SpineRenderer) {
onAfterUpdateWorldTransforms?(self) onAfterUpdateWorldTransforms?(self)
} }
func spineRenderer(_ spineRenderer: SpineRenderer, needsUpdate delta: TimeInterval) { func spineRenderer(_ spineRenderer: SpineRenderer, needsUpdate delta: TimeInterval) {
drawable?.update(delta: Float(delta)) drawable?.update(delta: Float(delta))
} }
func isPlaying(_ spineRenderer: SpineRenderer) -> Bool { func isPlaying(_ spineRenderer: SpineRenderer) -> Bool {
return isPlaying return isPlaying
} }
func skeletonDrawable(_ spineRenderer: SpineRenderer) -> SkeletonDrawableWrapper { func skeletonDrawable(_ spineRenderer: SpineRenderer) -> SkeletonDrawableWrapper {
return drawable return drawable
} }
func renderCommands(_ spineRenderer: SpineRenderer) -> [RenderCommand] { func renderCommands(_ spineRenderer: SpineRenderer) -> [RenderCommand] {
return drawable?.skeletonDrawable.render() ?? [] return drawable?.skeletonDrawable.render() ?? []
} }

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import UIKit
import MetalKit import MetalKit
import UIKit
/// A ``UIView`` to display a Spine skeleton. The skeleton can be loaded from a bundle, local files, http, or a pre-loaded ``SkeletonDrawableWrapper``. /// A ``UIView`` to display a Spine skeleton. The skeleton can be loaded from a bundle, local files, http, or a pre-loaded ``SkeletonDrawableWrapper``.
/// ///
@ -40,15 +40,15 @@ import MetalKit
/// This is a direct subclass of ``MTKView`` and is using `Metal` to render the skeleton. /// This is a direct subclass of ``MTKView`` and is using `Metal` to render the skeleton.
@objc @objc
public final class SpineUIView: MTKView { public final class SpineUIView: MTKView {
let controller: SpineController let controller: SpineController
let mode: Spine.ContentMode let mode: Spine.ContentMode
let alignment: Spine.Alignment let alignment: Spine.Alignment
let boundsProvider: BoundsProvider let boundsProvider: BoundsProvider
internal var computedBounds: CGRect = .zero internal var computedBounds: CGRect = .zero
internal var renderer: SpineRenderer? internal var renderer: SpineRenderer?
@objc internal init( @objc internal init(
controller: SpineController = SpineController(), controller: SpineController = SpineController(),
mode: Spine.ContentMode = .fit, mode: Spine.ContentMode = .fit,
@ -60,12 +60,12 @@ public final class SpineUIView: MTKView {
self.mode = mode self.mode = mode
self.alignment = alignment self.alignment = alignment
self.boundsProvider = boundsProvider self.boundsProvider = boundsProvider
super.init(frame: .zero, device: SpineObjects.shared.device) super.init(frame: .zero, device: SpineObjects.shared.device)
clearColor = MTLClearColor(backgroundColor) clearColor = MTLClearColor(backgroundColor)
isOpaque = backgroundColor != .clear isOpaque = backgroundColor != .clear
} }
/// An initializer that constructs a new ``SpineUIView`` from a ``SpineViewSource``. /// An initializer that constructs a new ``SpineUIView`` from a ``SpineViewSource``.
/// ///
/// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow /// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow
@ -98,7 +98,7 @@ public final class SpineUIView: MTKView {
} }
} }
} }
/// A convenience initializer that constructs a new ``SpineUIView`` from bundled files. /// A convenience initializer that constructs a new ``SpineUIView`` from bundled files.
/// ///
/// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow /// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow
@ -125,9 +125,11 @@ public final class SpineUIView: MTKView {
boundsProvider: BoundsProvider = SetupPoseBounds(), boundsProvider: BoundsProvider = SetupPoseBounds(),
backgroundColor: UIColor = .clear backgroundColor: UIColor = .clear
) { ) {
self.init(from: .bundle(atlasFileName: atlasFileName, skeletonFileName: skeletonFileName, bundle: bundle), controller: controller, mode: mode, alignment: alignment, boundsProvider: boundsProvider, backgroundColor: backgroundColor) self.init(
from: .bundle(atlasFileName: atlasFileName, skeletonFileName: skeletonFileName, bundle: bundle), controller: controller, mode: mode,
alignment: alignment, boundsProvider: boundsProvider, backgroundColor: backgroundColor)
} }
/// A convenience initializer that constructs a new ``SpineUIView`` from file URLs. /// A convenience initializer that constructs a new ``SpineUIView`` from file URLs.
/// ///
/// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow /// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow
@ -152,9 +154,11 @@ public final class SpineUIView: MTKView {
boundsProvider: BoundsProvider = SetupPoseBounds(), boundsProvider: BoundsProvider = SetupPoseBounds(),
backgroundColor: UIColor = .clear backgroundColor: UIColor = .clear
) { ) {
self.init(from: .file(atlasFile: atlasFile, skeletonFile: skeletonFile), controller: controller, mode: mode, alignment: alignment, boundsProvider: boundsProvider, backgroundColor: backgroundColor) self.init(
from: .file(atlasFile: atlasFile, skeletonFile: skeletonFile), controller: controller, mode: mode, alignment: alignment,
boundsProvider: boundsProvider, backgroundColor: backgroundColor)
} }
/// A convenience initializer that constructs a new ``SpineUIView`` from HTTP. /// A convenience initializer that constructs a new ``SpineUIView`` from HTTP.
/// ///
/// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow /// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow
@ -179,9 +183,11 @@ public final class SpineUIView: MTKView {
boundsProvider: BoundsProvider = SetupPoseBounds(), boundsProvider: BoundsProvider = SetupPoseBounds(),
backgroundColor: UIColor = .clear backgroundColor: UIColor = .clear
) { ) {
self.init(from: .http(atlasURL: atlasURL, skeletonURL: skeletonURL), controller: controller, mode: mode, alignment: alignment, boundsProvider: boundsProvider, backgroundColor: backgroundColor) self.init(
from: .http(atlasURL: atlasURL, skeletonURL: skeletonURL), controller: controller, mode: mode, alignment: alignment,
boundsProvider: boundsProvider, backgroundColor: backgroundColor)
} }
/// A convenience initializer that constructs a new ``SpineUIView`` with a ``SkeletonDrawableWrapper``. /// A convenience initializer that constructs a new ``SpineUIView`` with a ``SkeletonDrawableWrapper``.
/// ///
/// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow /// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow
@ -203,17 +209,19 @@ public final class SpineUIView: MTKView {
boundsProvider: BoundsProvider = SetupPoseBounds(), boundsProvider: BoundsProvider = SetupPoseBounds(),
backgroundColor: UIColor = .clear backgroundColor: UIColor = .clear
) { ) {
self.init(from: .drawable(drawable), controller: controller, mode: mode, alignment: alignment, boundsProvider: boundsProvider, backgroundColor: backgroundColor) self.init(
from: .drawable(drawable), controller: controller, mode: mode, alignment: alignment, boundsProvider: boundsProvider,
backgroundColor: backgroundColor)
} }
internal override init(frame frameRect: CGRect, device: MTLDevice?) { internal override init(frame frameRect: CGRect, device: MTLDevice?) {
fatalError("init(frame: device:) has not been implemented. Use init() instead.") fatalError("init(frame: device:) has not been implemented. Use init() instead.")
} }
internal required init(coder: NSCoder) { internal required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented. Use init() instead.") fatalError("init(coder:) has not been implemented. Use init() instead.")
} }
/// Disable or enable rendering. Disable it when the spine view is out of bounds and you want to preserve CPU/GPU resources. /// Disable or enable rendering. Disable it when the spine view is out of bounds and you want to preserve CPU/GPU resources.
public var isRendering: Bool { public var isRendering: Bool {
get { !super.isPaused } get { !super.isPaused }
@ -227,7 +235,7 @@ public final class SpineUIView: MTKView {
} }
extension SpineUIView { extension SpineUIView {
internal func load(drawable: SkeletonDrawableWrapper) throws { internal func load(drawable: SkeletonDrawableWrapper) throws {
controller.drawable = drawable controller.drawable = drawable
computedBounds = boundsProvider.computeBounds(for: drawable) computedBounds = boundsProvider.computeBounds(for: drawable)
@ -236,7 +244,7 @@ extension SpineUIView {
) )
controller.initialize() controller.initialize()
} }
private func initRenderer(atlasPages: [UIImage]) throws { private func initRenderer(atlasPages: [UIImage]) throws {
renderer = try SpineRenderer( renderer = try SpineRenderer(
device: SpineObjects.shared.device, device: SpineObjects.shared.device,
@ -265,7 +273,7 @@ public enum SpineViewSource {
case file(atlasFile: URL, skeletonFile: URL) case file(atlasFile: URL, skeletonFile: URL)
case http(atlasURL: URL, skeletonURL: URL) case http(atlasURL: URL, skeletonURL: URL)
case drawable(SkeletonDrawableWrapper) case drawable(SkeletonDrawableWrapper)
internal func loadDrawable() async throws -> SkeletonDrawableWrapper { internal func loadDrawable() async throws -> SkeletonDrawableWrapper {
switch self { switch self {
case .bundle(let atlasFileName, let skeletonFileName, let bundle): case .bundle(let atlasFileName, let skeletonFileName, let bundle):

View File

@ -38,7 +38,7 @@ import SwiftUI
/// ///
/// This is a ``UIViewRepresentable`` of `SpineUIView`. /// This is a ``UIViewRepresentable`` of `SpineUIView`.
public struct SpineView: UIViewRepresentable { public struct SpineView: UIViewRepresentable {
public typealias UIViewType = SpineUIView public typealias UIViewType = SpineUIView
private let source: SpineViewSource private let source: SpineViewSource
@ -46,11 +46,11 @@ public struct SpineView: UIViewRepresentable {
private let mode: Spine.ContentMode private let mode: Spine.ContentMode
private let alignment: Spine.Alignment private let alignment: Spine.Alignment
private let boundsProvider: BoundsProvider private let boundsProvider: BoundsProvider
private let backgroundColor: UIColor // Not using `SwiftUI.Color`, as briging to `UIColor` prior iOS 14 might not always work. private let backgroundColor: UIColor // Not using `SwiftUI.Color`, as briging to `UIColor` prior iOS 14 might not always work.
@Binding @Binding
private var isRendering: Bool? private var isRendering: Bool?
/// An initializer that constructs a new ``SpineView`` from a ``SpineViewSource``. /// An initializer that constructs a new ``SpineView`` from a ``SpineViewSource``.
/// ///
/// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow /// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow
@ -85,7 +85,7 @@ public struct SpineView: UIViewRepresentable {
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
_isRendering = isRendering _isRendering = isRendering
} }
public func makeUIView(context: Context) -> SpineUIView { public func makeUIView(context: Context) -> SpineUIView {
return SpineUIView( return SpineUIView(
from: source, from: source,
@ -96,7 +96,7 @@ public struct SpineView: UIViewRepresentable {
backgroundColor: backgroundColor backgroundColor: backgroundColor
) )
} }
public func updateUIView(_ uiView: SpineUIView, context: Context) { public func updateUIView(_ uiView: SpineUIView, context: Context) {
if let isRendering { if let isRendering {
uiView.isRendering = isRendering uiView.isRendering = isRendering

View File

@ -6,9 +6,9 @@
// //
#if canImport(Spine)
@_exported import Spine
#endif
@_exported import SpineCppLite @_exported import SpineCppLite
@_exported import SpineShadersStructs @_exported import SpineShadersStructs
#if canImport(Spine)
@_exported import Spine
#endif

View File

@ -16,21 +16,19 @@ fi
log_title "Spine-TS Build" log_title "Spine-TS Build"
log_detail "Branch: $BRANCH" log_detail "Branch: $BRANCH"
log_section "Setup"
log_action "Installing dependencies" log_action "Installing dependencies"
if npm install > /tmp/npm-install.log 2>&1; then if npm install > /tmp/npm-install.log 2>&1; then
log_ok "Dependencies installed" log_ok
else else
log_fail "npm install failed" log_fail
log_detail "$(cat /tmp/npm-install.log)" log_error_output "$(cat /tmp/npm-install.log)"
exit 1 exit 1
fi fi
if ! [ -z "$TS_UPDATE_URL" ] && ! [ -z "$BRANCH" ]; if ! [ -z "$TS_UPDATE_URL" ] && ! [ -z "$BRANCH" ];
then then
log_section "Deploy"
log_action "Creating artifacts zip" log_action "Creating artifacts zip"
zip -j spine-ts.zip \ if ZIP_OUTPUT=$(zip -j spine-ts.zip \
spine-core/dist/iife/* \ spine-core/dist/iife/* \
spine-canvas/dist/iife/* \ spine-canvas/dist/iife/* \
spine-webgl/dist/iife/* \ spine-webgl/dist/iife/* \
@ -51,13 +49,20 @@ then
spine-phaser-v3/dist/esm/* \ spine-phaser-v3/dist/esm/* \
spine-phaser-v4/dist/esm/* \ spine-phaser-v4/dist/esm/* \
spine-webcomponents/dist/esm/* \ spine-webcomponents/dist/esm/* \
spine-player/css/spine-player.css > /dev/null 2>&1 spine-player/css/spine-player.css 2>&1); then
log_ok
else
log_fail
log_error_output "$ZIP_OUTPUT"
exit 1
fi
log_action "Uploading to $TS_UPDATE_URL$BRANCH" log_action "Uploading to $TS_UPDATE_URL$BRANCH"
if curl -f -F "file=@spine-ts.zip" "$TS_UPDATE_URL$BRANCH" > /dev/null 2>&1; then if CURL_OUTPUT=$(curl -f -F "file=@spine-ts.zip" "$TS_UPDATE_URL$BRANCH" 2>&1); then
log_ok "Artifacts deployed" log_ok
else else
log_fail "Upload failed" log_fail
log_error_output "$CURL_OUTPUT"
exit 1 exit 1
fi fi

View File

@ -17,7 +17,6 @@ fi
# Install dependencies if node_modules doesn't exist # Install dependencies if node_modules doesn't exist
if [ ! -d "node_modules" ]; then if [ ! -d "node_modules" ]; then
log_section "Setup"
log_action "Installing dependencies" log_action "Installing dependencies"
if npm install > /tmp/npm-install.log 2>&1; then if npm install > /tmp/npm-install.log 2>&1; then
log_ok "Dependencies installed" log_ok "Dependencies installed"
@ -28,51 +27,42 @@ if [ ! -d "node_modules" ]; then
fi fi
fi fi
log_section "Analyzing API"
log_action "Analyzing Java API" log_action "Analyzing Java API"
if output=$(npx -y tsx src/analyze-java-api.ts 2>&1); then if output=$(npx -y tsx src/analyze-java-api.ts 2>&1); then
log_ok "Java API analysis completed" log_ok
else else
log_fail "Failed to analyze Java API" log_fail
log_detail "$output" log_detail "$output"
exit 1 exit 1
fi fi
log_section "Generating Serializer IR"
log_action "Generating intermediate representation" log_action "Generating intermediate representation"
if output=$(npx -y tsx src/generate-serializer-ir.ts 2>&1); then if output=$(npx -y tsx src/generate-serializer-ir.ts 2>&1); then
log_ok "Serializer IR generated successfully" log_ok
else else
log_fail "Failed to generate serializer IR" log_fail
log_detail "$output" log_detail "$output"
exit 1 exit 1
fi fi
log_section "Generating Language-Specific Serializers"
log_action "Generating Java SkeletonSerializer" log_action "Generating Java SkeletonSerializer"
if output=$(npx -y tsx src/generate-java-serializer.ts 2>&1); then if output=$(npx -y tsx src/generate-java-serializer.ts 2>&1); then
log_ok "Java serializer generated successfully" log_ok
log_action "Formatting Java code"
../formatters/format.sh java
log_ok "Java code formatted"
else else
log_fail "Failed to generate Java serializer" log_fail "Failed to generate Java serializer"
log_detail "$output" log_detail "$output"
exit 1 exit 1
fi fi
../formatters/format-java.sh
log_action "Generating C++ SkeletonSerializer" log_action "Generating C++ SkeletonSerializer"
if output=$(npx -y tsx src/generate-cpp-serializer.ts 2>&1); then if output=$(npx -y tsx src/generate-cpp-serializer.ts 2>&1); then
log_ok "C++ serializer generated successfully" log_ok "C++ serializer generated successfully"
log_action "Formatting C++ code"
../formatters/format.sh cpp
log_ok "C++ code formatted"
else else
log_fail "Failed to generate C++ serializer" log_fail "Failed to generate C++ serializer"
log_detail "$output" log_detail "$output"
exit 1 exit 1
fi fi
../formatters/format-cpp.sh
log_summary "✓ Serializer generation and formatting completed successfully" log_summary "✓ Serializer generation completed successfully"

View File

@ -16,18 +16,16 @@ fi
# Install dependencies if node_modules doesn't exist # Install dependencies if node_modules doesn't exist
if [ ! -d "node_modules" ]; then if [ ! -d "node_modules" ]; then
log_section "Setup"
log_action "Installing dependencies" log_action "Installing dependencies"
if npm install > /tmp/npm-install.log 2>&1; then if npm install > /tmp/npm-install.log 2>&1; then
log_ok "Dependencies installed" log_ok
else else
log_fail "npm install failed" log_fail
log_detail "$(cat /tmp/npm-install.log)" log_detail "$(cat /tmp/npm-install.log)"
exit 1 exit 1
fi fi
fi fi
log_section "Test"
log_action "Running TypeScript test runner" log_action "Running TypeScript test runner"
# Run the TypeScript headless test runner with all arguments # Run the TypeScript headless test runner with all arguments