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(
name: "Spine",
condition: .when(platforms: [
.iOS,
.iOS
])
),
"SpineCppLite",
@ -38,7 +38,7 @@ let package = Package(
.target(
name: "Spine",
dependencies: [
"SpineCppLite", "SpineShadersStructs"
"SpineCppLite", "SpineShadersStructs",
],
path: "spine-ios/Sources/Spine"
),
@ -46,13 +46,13 @@ let package = Package(
name: "SpineCppLite",
path: "spine-ios/Sources/SpineCppLite",
linkerSettings: [
.linkedLibrary("c++"),
.linkedLibrary("c++")
]
),
.systemLibrary(
name: "SpineShadersStructs",
path: "spine-ios/Sources/SpineShadersStructs"
)
),
],
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
set -e
# Format C/C++ files with clang-format
echo "Formatting C/C++ files..."
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
pushd "$dir" > /dev/null
if [ ! -f ".clang-format" ]; then
echo "Error: .clang-format not found in formatters directory"
log_action "Checking for formatters/.clang-format"
if [ -f ".clang-format" ]; then
log_ok
else
log_fail
popd > /dev/null
exit 1
fi
@ -86,22 +91,16 @@ for cpp_dir in "${cpp_dirs[@]}"; do
fi
done
echo "Found ${#files[@]} C/C++ files to format"
# Format all files in one call - works for both Docker and native
echo "Formatting ${#files[@]} files..."
if ! clang-format -i -style=file:".clang-format" "${files[@]}" 2>&1; then
echo "Error: clang-format failed"
errors=1
log_action "Formatting ${#files[@]} C/C++ files"
if FORMAT_OUTPUT=$(clang-format -i -style=file:".clang-format" "${files[@]}" 2>&1); then
log_ok
else
errors=0
log_fail
log_error_output "$FORMAT_OUTPUT"
popd > /dev/null
exit 1
fi
if [ $errors -gt 0 ]; then
echo "Completed with $errors errors"
fi
echo "C/C++ formatting complete"
# Return to original directory
popd > /dev/null

View File

@ -1,42 +1,58 @@
#!/bin/bash
set -e
# Format C# files with dotnet-format
echo "Formatting C# files..."
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
# Store original directory
pushd "$dir" > /dev/null
# Copy .editorconfig to C# directories
cp .editorconfig ../spine-csharp/ 2>/dev/null || true
cp .editorconfig ../spine-monogame/ 2>/dev/null || true
cp .editorconfig ../spine-unity/ 2>/dev/null || true
# Format spine-csharp
log_action "Formatting spine-csharp"
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
# Format spine-monogame
log_action "Formatting spine-monogame"
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
# Format spine-unity - look for .cs files directly
if [ -d ../spine-unity ]; then
echo "Formatting spine-unity C# files directly..."
pushd ../spine-unity > /dev/null
# Find all .cs files and format them using dotnet format whitespace
log_action "Formatting spine-unity C# files"
pushd ../spine-unity > /dev/null
# 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
dotnet format whitespace --include "$file" --no-restore 2>/dev/null || true
done
popd > /dev/null
log_ok
else
log_skip
fi
popd > /dev/null
# Clean up .editorconfig files
rm -f ../spine-csharp/.editorconfig
rm -f ../spine-monogame/.editorconfig
rm -f ../spine-unity/.editorconfig
@ -44,5 +60,7 @@ if command -v dotnet &> /dev/null; then
# Return to original directory
popd > /dev/null
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

View File

@ -1,22 +1,44 @@
#!/bin/bash
set -e
# Format Dart files
echo "Formatting Dart files..."
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source logging utilities
source "$dir/logging/logging.sh"
log_title "Dart Formatting"
# Store original directory
pushd "$dir" > /dev/null
if command -v dart &> /dev/null; then
find .. -name "*.dart" \
dart_files=$(find .. -name "*.dart" \
-not -path "*/.*" \
-not -path "*/node_modules/*" \
-not -path "*/build/*" \
-exec dart format --page-width 120 {} +
-not -path "*/build/*" | wc -l | tr -d ' ')
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
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
# Return to original directory

View File

@ -1,21 +1,45 @@
#!/bin/bash
set -e
# Format Haxe files
echo "Formatting Haxe files..."
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source logging utilities
source "$dir/logging/logging.sh"
log_title "Haxe Formatting"
# Store original directory
pushd "$dir" > /dev/null
if command -v haxelib &> /dev/null && haxelib list formatter &> /dev/null; then
# Format spine-haxe directory
if [ -d ../spine-haxe ]; then
haxelib run formatter -s ../spine-haxe
if command -v haxelib &> /dev/null; then
log_action "Checking Haxe formatter availability"
if HAXELIB_OUTPUT=$(haxelib list formatter 2>&1); then
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
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
# Return to original directory

View File

@ -1,11 +1,13 @@
#!/bin/bash
set -e
# Format Java files with Eclipse formatter
echo "Formatting Java files..."
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source logging utilities
source "$dir/logging/logging.sh"
log_title "Java Formatting"
# Store original directory
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"
if [ ! -f "$jar_file" ] || [ "$src_file" -nt "$jar_file" ]; then
echo "Building Eclipse formatter..."
log_action "Building Eclipse formatter"
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
fi
@ -30,13 +40,22 @@ java_files=$(find ../spine-libgdx ../spine-android -name "*.java" -type f \
# Run the formatter
if [ -n "$java_files" ]; then
echo "Running Eclipse formatter on Java files..."
java -jar eclipse-formatter/target/eclipse-formatter-1.0.0-jar-with-dependencies.jar \
java_count=$(echo "$java_files" | wc -l | tr -d ' ')
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 \
$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
echo "Java formatting complete"
# Return to original directory
popd > /dev/null

View File

@ -1,22 +1,45 @@
#!/bin/bash
set -e
# Format Swift files
echo "Formatting Swift files..."
dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source logging utilities
source "$dir/logging/logging.sh"
log_title "Swift Formatting"
# Store original directory
pushd "$dir" > /dev/null
if command -v swift-format &> /dev/null; then
find .. -name "*.swift" \
swift_files=$(find .. -name "*.swift" -type f \
-not -path "*/.*" \
-not -path "*/build/*" \
-not -path "*/DerivedData/*" \
| xargs swift-format -i
-not -path "*/DerivedData/*" | wc -l | tr -d ' ')
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
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
# Return to original directory

View File

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

View File

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

View File

@ -49,36 +49,35 @@ NC='\033[0m' # No Color
# Main header for script/tool name
log_title() {
echo ""
echo -e "${BOLD}$1${NC}"
echo ""
echo -e "${GREEN}${BOLD}$1${NC}"
}
# Section headers for major phases
log_section() {
echo -e "${BOLD}${BLUE}$1${NC}"
}
# Individual actions/steps
# Individual actions/steps - inline result format
log_action() {
echo -e " $1..."
echo -n " $1... "
}
# Results - success/failure/info
# Results - success/failure/info (on same line)
log_ok() {
echo -e " ${GREEN}${NC} $1"
echo -e "${GREEN}${NC}"
}
log_fail() {
echo -e " ${RED}${NC} $1"
echo -e "${RED}${NC}"
}
log_warn() {
echo -e " ${YELLOW}!${NC} $1"
echo -e "${YELLOW}!${NC}"
}
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

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
```bash
log_title "Spine-C++ Test"
log_title "Spine-C++ Build"
```
### 2. Section (`log_section`)
- **Purpose**: Major phases or groups of operations
- **Style**: Bold blue text, no extra spacing
- **Usage**: Build, Test, Deploy, etc.
```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
### 2. Action + Result (inline format)
- **Purpose**: Individual operations with immediate result
- **Style**: Action on same line as result for density
- **Usage**: `log_action` followed immediately by `log_ok/fail/warn/skip`
```bash
log_action "Building all variants"
log_ok # Green ✓ on same line
log_action "Testing headless-test"
log_fail # Red ✗ on same line
```
### 4. Results
- **Purpose**: Outcome of operations
- **Style**: Indented with colored symbols
**Results**:
- `log_ok` - Green ✓ (success)
- `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
log_ok "Build completed" # Green ✓
log_fail "Build failed" # Red ✗
log_warn "Deprecated feature" # Yellow !
log_skip "Not supported on macOS" # Gray -
log_action "Building"
if BUILD_OUTPUT=$(command 2>&1); then
log_ok
else
log_fail
log_error_output "$BUILD_OUTPUT"
fi
```
### 5. Detail (`log_detail`)
- **Purpose**: Secondary information, error output, debug info
- **Purpose**: Secondary information, debug info (not errors)
- **Style**: Gray text, indented
- **Usage**: Additional context, error messages
- **Usage**: Additional context, platform info
```bash
log_detail "Platform: Darwin"
log_detail "$ERROR_OUTPUT"
log_detail "Branch: main"
```
### 6. Summary (`log_summary`)
@ -78,58 +81,90 @@ log_summary "✗ Tests failed (3/5)"
#!/bin/bash
source ../formatters/logging/logging.sh
log_title "Spine-C++ Test"
log_title "Spine-C++ Build"
log_detail "Platform: $(uname)"
log_section "Build"
log_action "Building all variants"
if BUILD_OUTPUT=$(./build.sh clean release 2>&1); then
log_ok "Build completed"
log_action "Configuring debug build"
if CMAKE_OUTPUT=$(cmake --preset=debug . 2>&1); then
log_ok
else
log_fail "Build failed"
log_detail "$BUILD_OUTPUT"
log_fail
log_error_output "$CMAKE_OUTPUT"
exit 1
fi
log_section "Test"
log_action "Testing headless-test"
if test_result; then
log_ok "headless-test"
log_action "Building"
if BUILD_OUTPUT=$(cmake --build --preset=debug 2>&1); then
log_ok
else
log_fail "headless-test - execution failed"
log_detail "$error_output"
log_fail
log_error_output "$BUILD_OUTPUT"
exit 1
fi
log_summary "✓ All tests passed (2/2)"
log_summary "✓ Build successful"
```
## Output Preview
**Success case:**
```
Spine-C++ Test
Spine-C++ Build
Platform: Darwin
Build
Building all variants...
✓ Build completed
Configuring debug build... ✓
Building... ✓
Test
Testing headless-test...
✓ headless-test
Testing headless-test-nostdcpp...
✓ headless-test-nostdcpp
✓ Build successful
```
✓ 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
1. **Capture output**: Use `OUTPUT=$(command 2>&1)` to capture both stdout and stderr
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
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
if BUILD_OUTPUT=$(./build.sh clean release 2>&1); then

View File

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

View File

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

View File

@ -35,7 +35,6 @@ EXPECTED_OUTPUT="=== SKELETON DATA ===
log_title "Spine-C++ Test"
log_detail "Platform: $(uname)"
log_section "Build"
log_action "Building all variants"
if BUILD_OUTPUT=$(./build.sh clean release 2>&1); then
log_ok "Build completed"
@ -45,8 +44,6 @@ else
exit 1
fi
log_section "Test"
test_count=0
pass_count=0

View File

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

View File

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

View File

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

View File

@ -2,14 +2,14 @@ import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController.init()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
override func awakeFromNib() {
let flutterViewController = FlutterViewController.init()
let windowFrame = self.frame
self.contentViewController = flutterViewController
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
log_section "Deploy"
log_action "Creating release package"
zip -r "spine-haxe-$VERSION.zip" \
if ZIP_OUTPUT=$(zip -r "spine-haxe-$VERSION.zip" \
haxelib.json \
LICENSE \
README.md \
spine-haxe > /dev/null 2>&1
log_ok "Package created"
spine-haxe 2>&1); then
log_ok
else
log_fail
log_error_output "$ZIP_OUTPUT"
exit 1
fi
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
log_ok "Package deployed"
if CURL_OUTPUT=$(curl -f -F "file=@spine-haxe-$VERSION.zip" "$HAXE_UPDATE_URL$BRANCH" 2>&1); then
log_ok
else
log_fail "Upload failed"
log_fail
log_error_output "$CURL_OUTPUT"
exit 1
fi

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import SwiftUI
import Spine
import SwiftUI
struct SimpleAnimation: View {
@ -46,10 +46,10 @@ struct SimpleAnimation: View {
var body: some View {
SpineView(
from: .bundle(atlasFileName: "spineboy-pma.atlas", skeletonFileName: "spineboy-pro.skel"),
// from: .http(
// 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")!
// ),
// from: .http(
// 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")!
// ),
controller: controller,
mode: .fit,
alignment: .center

View File

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

View File

@ -27,9 +27,9 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import SwiftUI
import Spine
import SpineCppLite
import SwiftUI
struct AnimationStateEvents: View {
@ -42,16 +42,18 @@ struct AnimationStateEvents: View {
controller.animationStateData.defaultMix = 0.2
let walk = controller.animationState.setAnimationByName(trackIndex: 0, animationName: "walk", loop: true)
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)
let run = controller.animationState.addAnimationByName(trackIndex: 0, animationName: "run", loop: true, delay: 0)
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
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 ?? "--"

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import SwiftUI
import Spine
import SwiftUI
struct DebugRendering: View {
@ -78,7 +78,8 @@ final class DebugRenderingModel: ObservableObject {
)
},
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
let position = controller.fromSkeletonCoordinates(
position: CGPointMake(CGFloat(bone.worldX), CGFloat(bone.worldY))

View File

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

View File

@ -27,9 +27,9 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import SwiftUI
import Spine
import SpineCppLite
import SwiftUI
struct DressUp: View {
@ -146,11 +146,11 @@ final class DressUpModel: ObservableObject {
customSkin?.dispose()
customSkin = Skin.create(name: "custom-skin")
for skinName in selectedSkins.keys {
if selectedSkins[skinName] == true {
if let skin = drawable.skeletonData.findSkin(name: skinName) {
customSkin?.addSkin(other: skin)
}
}
if selectedSkins[skinName] == true {
if let skin = drawable.skeletonData.findSkin(name: skinName) {
customSkin?.addSkin(other: skin)
}
}
}
drawable.skeleton.skin = customSkin
drawable.skeleton.setToSetupPose()

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import SwiftUI
import Spine
import SwiftUI
struct IKFollowing: View {
@ -86,13 +86,14 @@ final class IKFollowingModel: ObservableObject {
)
},
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 {
return
}
let bone = controller.skeleton.findBone(boneName: "crosshair")!
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.y = position.y
}

View File

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

View File

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

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import SwiftUI
import Spine
import SwiftUI
struct PlayPauseAnimation: View {
@ -46,10 +46,10 @@ struct PlayPauseAnimation: View {
var body: some View {
SpineView(
from: .bundle(atlasFileName: "dragon.atlas", skeletonFileName: "dragon-ess.skel"),
// from: .http(
// 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")!
// ),
// from: .http(
// 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")!
// ),
controller: controller,
boundsProvider: SkinAndAnimationBounds(animation: "flying")
)

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import SwiftUI
import Spine
import SwiftUI
struct SimpleAnimation: View {
@ -46,10 +46,10 @@ struct SimpleAnimation: View {
var body: some View {
SpineView(
from: .bundle(atlasFileName: "spineboy-pma.atlas", skeletonFileName: "spineboy-pro.skel"),
// from: .http(
// 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")!
// ),
// from: .http(
// 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")!
// ),
controller: controller,
mode: .fit,
alignment: .center

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import SwiftUI
import Spine
import SwiftUI
@main
struct SpineExampleApp: App {

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import Foundation
import CoreGraphics
import Foundation
/// 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``.
@ -82,7 +82,7 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
private let animation: 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
/// the bounding box of the skeleton. If no skins are given, the default skin is used.
@ -110,7 +110,7 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
}
}
drawable.skeleton.skin = customSkin
drawable.skeleton.setToSetupPose();
drawable.skeleton.setToSetupPose()
let animation = animation.flatMap { data.findAnimation(name: $0) }
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))
for i in 0..<steps {
drawable.update(delta: i > 0 ? Float(stepTime) : 0.0)
let bounds = drawable.skeleton.bounds;
let bounds = drawable.skeleton.bounds
minX = min(minX, bounds.x)
minY = min(minY, bounds.y)
maxX = max(maxX, minX + bounds.width)
maxY = max(maxY, minY + bounds.height)
}
} else {
let bounds = drawable.skeleton.bounds;
let bounds = drawable.skeleton.bounds
minX = bounds.x
minY = bounds.y
maxX = minX + bounds.width
@ -145,14 +145,15 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
drawable.update(delta: 0)
customSkin.dispose()
return CGRectMake(CGFloat(minX), CGFloat(minY), CGFloat(maxX - minX), CGFloat(maxY - minY))
}
}
}
/// How a view should be inscribed into another view.
@objc
public enum ContentMode: Int {
case fit /// 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.
case fit
/// 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.

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import UIKit
import MetalKit
import UIKit
extension MTLClearColor {
init(_ color: UIColor) {

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import SpineShadersStructs
import Foundation
import SpineShadersStructs
import simd
extension RenderCommand {

View File

@ -27,11 +27,11 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import CoreGraphics
import Foundation
import UIKit
import CoreGraphics
public extension SkeletonDrawableWrapper {
extension SkeletonDrawableWrapper {
/// Render the ``Skeleton`` to a `CGImage`
///
@ -39,9 +39,9 @@ public extension SkeletonDrawableWrapper {
/// - size: The size of the `CGImage` that should be rendered.
/// - 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
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(
controller: SpineController(disposeDrawableOnDeInit: false), // Doesn't own the drawable
controller: SpineController(disposeDrawableOnDeInit: false), // Doesn't own the drawable
backgroundColor: backgroundColor
)
spineView.frame = CGRect(origin: .zero, size: size)
@ -74,9 +74,13 @@ public extension SkeletonDrawableWrapper {
).union(.byteOrder32Little)
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let context = CGContext(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.")
guard
let context = CGContext(
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
}

View File

@ -29,9 +29,9 @@
import Foundation
import MetalKit
import SpineShadersStructs
import Spine
import SpineCppLite
import SpineShadersStructs
protocol SpineRendererDelegate: AnyObject {
func spineRendererWillUpdate(_ spineRenderer: SpineRenderer)
@ -67,7 +67,7 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
private var pipelineStatesByBlendMode = [Int: MTLRenderPipelineState]()
private static let numberOfBuffers = 3
private static let defaultBufferSize = 32 * 1024 // 32KB
private static let defaultBufferSize = 32 * 1024 // 32KB
private var buffers = [MTLBuffer]()
private let bufferingSemaphore = DispatchSemaphore(value: SpineRenderer.numberOfBuffers)
@ -87,16 +87,17 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
self.commandQueue = commandQueue
let bundle: Bundle
#if SWIFT_PACKAGE // SPM
bundle = .module
#else // CocoaPods
let bundleURL = Bundle(for: SpineRenderer.self).url(forResource: "SpineBundle", withExtension: "bundle")
bundle = Bundle(url: bundleURL!)!
#if SWIFT_PACKAGE // SPM
bundle = .module
#else // CocoaPods
let bundleURL = Bundle(for: SpineRenderer.self).url(forResource: "SpineBundle", withExtension: "bundle")
bundle = Bundle(url: bundleURL!)!
#endif
let defaultLibrary = try device.makeDefaultLibrary(bundle: bundle)
let textureLoader = MTKTextureLoader(device: device)
textures = try atlasPages
textures =
try atlasPages
.compactMap { $0.cgImage }
.map {
try textureLoader.newTexture(
@ -112,7 +113,7 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
SPINE_BLEND_MODE_NORMAL,
SPINE_BLEND_MODE_ADDITIVE,
SPINE_BLEND_MODE_MULTIPLY,
SPINE_BLEND_MODE_SCREEN
SPINE_BLEND_MODE_SCREEN,
]
for blendMode in blendModes {
let descriptor = MTLRenderPipelineDescriptor()
@ -157,12 +158,13 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
currentBufferIndex = (currentBufferIndex + 1) % SpineRenderer.numberOfBuffers
guard let renderCommands = dataSource?.renderCommands(self),
let commandBuffer = commandQueue.makeCommandBuffer(),
let renderPassDescriptor = view.currentRenderPassDescriptor,
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
// this can happen if,
// - 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
let commandBuffer = commandQueue.makeCommandBuffer(),
let renderPassDescriptor = view.currentRenderPassDescriptor,
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
else {
// this can happen if,
// - 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()
return
}
@ -224,7 +226,7 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
lastDraw = CACurrentMediaTime()
}
let delta = CACurrentMediaTime() - lastDraw
delegate?.spineRendererWillUpdate(self)
delegate?.spineRendererWillUpdate(self)
delegate?.spineRenderer(self, needsUpdate: delta)
lastDraw = CACurrentMediaTime()
delegate?.spineRendererDidUpdate(self)
@ -311,83 +313,83 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
}
private func increaseBuffersSize(to size: Int) {
buffers = (0 ..< SpineRenderer.numberOfBuffers).map { _ in
buffers = (0..<SpineRenderer.numberOfBuffers).map { _ in
device.makeBuffer(length: size, options: .storageModeShared)!
}
}
}
fileprivate extension BlendMode {
func sourceRGBBlendFactor(premultipliedAlpha: Bool) -> MTLBlendFactor {
switch self {
case SPINE_BLEND_MODE_NORMAL:
return premultipliedAlpha ? .one : .sourceAlpha
case SPINE_BLEND_MODE_ADDITIVE:
// additvie only needs sourceAlpha multiply if it is not pma
return premultipliedAlpha ? .one : .sourceAlpha
case SPINE_BLEND_MODE_MULTIPLY:
return .destinationColor
case SPINE_BLEND_MODE_SCREEN:
return .one
default:
return .one // Should never be called
}
}
extension BlendMode {
fileprivate func sourceRGBBlendFactor(premultipliedAlpha: Bool) -> MTLBlendFactor {
switch self {
case SPINE_BLEND_MODE_NORMAL:
return premultipliedAlpha ? .one : .sourceAlpha
case SPINE_BLEND_MODE_ADDITIVE:
// additvie only needs sourceAlpha multiply if it is not pma
return premultipliedAlpha ? .one : .sourceAlpha
case SPINE_BLEND_MODE_MULTIPLY:
return .destinationColor
case SPINE_BLEND_MODE_SCREEN:
return .one
default:
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
}
}
fileprivate 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 {
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 var destinationRGBBlendFactor: 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
}
}
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 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) {
isBlendingEnabled = true
sourceRGBBlendFactor = blendMode.sourceRGBBlendFactor(premultipliedAlpha: premultipliedAlpha)
sourceAlphaBlendFactor = blendMode.sourceAlphaBlendFactor
destinationRGBBlendFactor = blendMode.destinationRGBBlendFactor
destinationAlphaBlendFactor = blendMode.destinationAlphaBlendFactor
}
fileprivate func apply(blendMode: BlendMode, with premultipliedAlpha: Bool) {
isBlendingEnabled = true
sourceRGBBlendFactor = blendMode.sourceRGBBlendFactor(premultipliedAlpha: premultipliedAlpha)
sourceAlphaBlendFactor = blendMode.sourceAlphaBlendFactor
destinationRGBBlendFactor = blendMode.destinationRGBBlendFactor
destinationAlphaBlendFactor = blendMode.destinationAlphaBlendFactor
}
}

View File

@ -27,10 +27,10 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import CoreGraphics
import Foundation
import Spine
import SpineCppLite
import CoreGraphics
import UIKit
/// A ``SkeletonDrawableWrapper`` with ``SkeletonDrawable`` bundle loading, updating, and rendering an ``Atlas``, ``Skeleton``, and ``AnimationState``

View File

@ -28,8 +28,8 @@
*****************************************************************************/
import Foundation
import SwiftUI
import SpineCppLite
import SwiftUI
public var version: String {
return "\(majorVersion).\(minorVersion)"
@ -49,12 +49,12 @@ public var minorVersion: Int {
///
/// 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.
public extension Atlas {
extension Atlas {
/// 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.
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()
return try await Self.fromData(data: data) { name in
return try await FileSource.bundle(fileName: name, bundle: bundle).load()
@ -64,7 +64,7 @@ public extension Atlas {
/// Loads an ``Atlas`` from the file URL `atlasFile`.
///
/// 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()
return try await Self.fromData(data: data) { name in
let dir = atlasFile.deletingLastPathComponent()
@ -76,7 +76,7 @@ public extension Atlas {
/// Loads an ``Atlas`` from the http URL `atlasURL`.
///
/// 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()
return try await Self.fromData(data: data) { name in
let dir = atlasURL.deletingLastPathComponent()
@ -102,7 +102,7 @@ public extension Atlas {
}
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 {
guard let atlasPageFilePointer = spine_atlas_get_image_path(atlas, i) else {
@ -120,13 +120,13 @@ public extension Atlas {
}
}
public extension SkeletonData {
extension SkeletonData {
/// 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.
///
/// 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(
atlas: atlas,
data: try await FileSource.bundle(fileName: skeletonFileName, bundle: bundle).load(),
@ -137,7 +137,7 @@ public extension SkeletonData {
/// 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.
static func fromFile(atlas: Atlas, skeletonFile: URL) async throws -> SkeletonData {
public static func fromFile(atlas: Atlas, skeletonFile: URL) async throws -> SkeletonData {
return try fromData(
atlas: atlas,
data: try await FileSource.file(skeletonFile).load(),
@ -148,7 +148,7 @@ public extension SkeletonData {
/// 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.
static func fromHttp(atlas: Atlas, skeletonURL: URL) async throws -> SkeletonData {
public static func fromHttp(atlas: Atlas, skeletonURL: URL) async throws -> SkeletonData {
return try fromData(
atlas: atlas,
data: try await FileSource.http(skeletonURL).load(),
@ -159,8 +159,8 @@ public extension SkeletonData {
/// 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.
static func fromData(atlas: Atlas, data: Data) throws -> SkeletonData {
let result = try data.withUnsafeBytes{
public static func fromData(atlas: Atlas, data: Data) throws -> SkeletonData {
let result = try data.withUnsafeBytes {
try $0.withMemoryRebound(to: UInt8.self) { buffer in
guard let ptr = buffer.baseAddress else {
throw SpineError("Couldn't read atlas binary")
@ -192,11 +192,12 @@ public extension SkeletonData {
/// images.
///
/// 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
guard
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")
}
return result
@ -226,7 +227,7 @@ public extension SkeletonData {
}
}
internal extension SkeletonDrawable {
extension SkeletonDrawable {
func render() -> [RenderCommand] {
var commands = [RenderCommand]()
@ -240,13 +241,13 @@ internal extension SkeletonDrawable {
} else {
nativeCmd = nil
}
} while (nativeCmd != nil)
} while nativeCmd != nil
return commands
}
}
internal extension RenderCommand {
extension RenderCommand {
var numVertices: Int {
Int(spine_render_command_get_num_vertices(wrappee))
@ -268,7 +269,7 @@ internal extension RenderCommand {
return Array(buffer)
}
func colors(numVertices: Int) ->[Int32] {
func colors(numVertices: Int) -> [Int32] {
let num = numVertices
let ptr = spine_render_command_get_colors(wrappee)
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
/// 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))
}
}
// Helper
public extension CGRect {
extension CGRect {
/// Construct a `CGRect` from ``Bounds``
init(bounds: Bounds) {
public init(bounds: Bounds) {
self = CGRect(
x: CGFloat(bounds.x),
y: CGFloat(bounds.y),
@ -331,9 +332,9 @@ internal enum FileSource {
} else {
let lock = NSRecursiveLock()
nonisolated(unsafe)
var isCancelled = false
var isCancelled = false
nonisolated(unsafe)
var taskHolder:URLSessionDownloadTask? = nil
var taskHolder: URLSessionDownloadTask? = nil
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
let task = URLSession.shared.downloadTask(with: url) { temp, response, error in
@ -389,15 +390,15 @@ public struct SpineError: Error, CustomStringConvertible {
}
public extension SkeletonBounds {
static func create() -> SkeletonBounds {
extension SkeletonBounds {
public static func create() -> SkeletonBounds {
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)
}

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import Foundation
import CoreGraphics
import Foundation
import QuartzCore
import UIKit
@ -106,7 +106,7 @@ public final class SpineController: NSObject, ObservableObject {
deinit {
if disposeDrawableOnDeInit {
drawable?.dispose() // TODO move drawable out of view?
drawable?.dispose() // TODO move drawable out of view?
}
}
@ -145,8 +145,8 @@ public final class SpineController: NSObject, ObservableObject {
/// the skeleton coordinate system. See the `IKFollowing.swift` example how to use this
/// to move a bone based on user touch input.
public func toSkeletonCoordinates(position: CGPoint) -> CGPoint {
let x = position.x;
let y = position.y;
let x = position.x
let y = position.y
return CGPoint(
x: (x - viewSize.width / 2) / scaleX - offsetX,
y: (y - viewSize.height / 2) / scaleY - offsetY
@ -156,8 +156,8 @@ public final class SpineController: NSObject, ObservableObject {
/// 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.
public func fromSkeletonCoordinates(position: CGPoint) -> CGPoint {
let x = position.x;
let y = position.y;
let x = position.x
let y = position.y
return CGPoint(
x: (x + offsetX) * scaleX,
y: (y + offsetY) * scaleY

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import UIKit
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``.
///
@ -125,7 +125,9 @@ public final class SpineUIView: MTKView {
boundsProvider: BoundsProvider = SetupPoseBounds(),
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.
@ -152,7 +154,9 @@ public final class SpineUIView: MTKView {
boundsProvider: BoundsProvider = SetupPoseBounds(),
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.
@ -179,7 +183,9 @@ public final class SpineUIView: MTKView {
boundsProvider: BoundsProvider = SetupPoseBounds(),
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``.
@ -203,7 +209,9 @@ public final class SpineUIView: MTKView {
boundsProvider: BoundsProvider = SetupPoseBounds(),
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?) {

View File

@ -46,7 +46,7 @@ public struct SpineView: UIViewRepresentable {
private let mode: Spine.ContentMode
private let alignment: Spine.Alignment
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
private var isRendering: Bool?

View File

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

View File

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

View File

@ -17,7 +17,6 @@ fi
# Install dependencies if node_modules doesn't exist
if [ ! -d "node_modules" ]; then
log_section "Setup"
log_action "Installing dependencies"
if npm install > /tmp/npm-install.log 2>&1; then
log_ok "Dependencies installed"
@ -28,51 +27,42 @@ if [ ! -d "node_modules" ]; then
fi
fi
log_section "Analyzing API"
log_action "Analyzing Java API"
if output=$(npx -y tsx src/analyze-java-api.ts 2>&1); then
log_ok "Java API analysis completed"
log_ok
else
log_fail "Failed to analyze Java API"
log_fail
log_detail "$output"
exit 1
fi
log_section "Generating Serializer IR"
log_action "Generating intermediate representation"
if output=$(npx -y tsx src/generate-serializer-ir.ts 2>&1); then
log_ok "Serializer IR generated successfully"
log_ok
else
log_fail "Failed to generate serializer IR"
log_fail
log_detail "$output"
exit 1
fi
log_section "Generating Language-Specific Serializers"
log_action "Generating Java SkeletonSerializer"
if output=$(npx -y tsx src/generate-java-serializer.ts 2>&1); then
log_ok "Java serializer generated successfully"
log_action "Formatting Java code"
../formatters/format.sh java
log_ok "Java code formatted"
log_ok
else
log_fail "Failed to generate Java serializer"
log_detail "$output"
exit 1
fi
../formatters/format-java.sh
log_action "Generating C++ SkeletonSerializer"
if output=$(npx -y tsx src/generate-cpp-serializer.ts 2>&1); then
log_ok "C++ serializer generated successfully"
log_action "Formatting C++ code"
../formatters/format.sh cpp
log_ok "C++ code formatted"
else
log_fail "Failed to generate C++ serializer"
log_detail "$output"
exit 1
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
if [ ! -d "node_modules" ]; then
log_section "Setup"
log_action "Installing dependencies"
if npm install > /tmp/npm-install.log 2>&1; then
log_ok "Dependencies installed"
log_ok
else
log_fail "npm install failed"
log_fail
log_detail "$(cat /tmp/npm-install.log)"
exit 1
fi
fi
log_section "Test"
log_action "Running TypeScript test runner"
# Run the TypeScript headless test runner with all arguments