Merge branch '4.2' into 4.3-beta

# Conflicts:
#	spine-unity/Assets/Spine/package.json
This commit is contained in:
Nathan Sweet 2025-05-21 10:29:28 -04:00
commit 9b596c3856
53 changed files with 816 additions and 1823 deletions

1
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1 @@
* [ ] I confirm this contribution is made under the Esoteric Software LLC [CLA](http://esotericsoftware.com/licenses/cla.txt).

View File

@ -49,7 +49,7 @@ jobs:
path: spine-godot/example-v4-extension/bin/windows/*.dll
build-linux-x86_64:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:

View File

@ -65,7 +65,7 @@ jobs:
path: spine-godot/godot/bin/**/*
godot-editor-linux:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
@ -161,7 +161,7 @@ jobs:
path: spine-godot/godot/bin/macos.zip
godot-template-linux:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:

1
.gitignore vendored
View File

@ -221,3 +221,4 @@ spine-godot/build/version.txt
spine-godot/vc140.pdb
spine-godot/example-v4-extension/bin
spine-godot/example-v4-extension/MoltenVK.xcframework
spine-flutter/example/android/app/.cxx

View File

@ -175,6 +175,7 @@
2. Add a `RenderExistingMeshGraphic` component.
3. In the `RenderExistingMeshGraphic` component Inspector at `Reference Skeleton Graphic` assign the original `SkeletonGraphic` object.
4. At `Replacement Material` assign e.g. the included _SkeletonGraphicDefaultOutline_ material to replace all materials with this material. Alternatively, if `Multiple CanvasRenderers` is enabled at the reference SkeletonGraphic, you can add entries to the `Replacement Materials` list and at each entry assign the original SkeletonGraphic material (e.g. _SkeletonGraphicDefault_) to be replaced and the respective `Replacement Material` (e.g. _SkeletonGraphicDefaultOutline_).
- Added option for unsafe direct data loading when loading skeleton binary data to avoid some allocations, enabled via build define `SPINE_ALLOW_UNSAFE`. This define can be set via Spine Preferences, setting `Unsafe Build Defines - Direct data access`. The define is disabled by default to maintain existing behaviour. Changed asmdef setting for spine-unity assembly to allow unsafe code, has no effect other than allowing setting the `SPINE_ALLOW_UNSAFE` define.
- **Breaking changes**

View File

@ -1,3 +1,6 @@
# 4.2.36
- Support for 16KB page alignement on Android. You must specify the NDK version in the build.gradle file of your app's Android project. See https://github.com/EsotericSoftware/spine-runtimes/issues/2849
# 4.2.35
- Port of commit f1e0f0f: Fixed animation not being mixed out in some cases.

View File

@ -11,7 +11,7 @@ buildscript {
dependencies {
// The Android Gradle Plugin knows how to build native code with the NDK.
classpath 'com.android.tools.build:gradle:7.1.2'
classpath 'com.android.tools.build:gradle:8.5.1'
}
}
@ -33,7 +33,7 @@ android {
// Bumping the plugin ndkVersion requires all clients of this plugin to bump
// the version in their app and to download a newer version of the NDK.
ndkVersion "21.1.6352462"
ndkVersion "28.1.13356709"
// Invoke the shared CMake build with the Android Gradle Plugin.
externalNativeBuild {
@ -56,6 +56,6 @@ android {
}
defaultConfig {
minSdkVersion 16
minSdkVersion 21
}
}

View File

@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related

View File

@ -1,3 +1,9 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
@ -21,13 +22,11 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
// ndkVersion flutter.ndkVersion
ndkVersion "28.1.13356709"
namespace "com.esotericsoftware.spine.android"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -44,7 +43,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.spine_flutter_example"
applicationId "com.esotericsoftware.spine.android"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion flutter.minSdkVersion
@ -65,7 +64,3 @@ android {
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.spine_flutter_example">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.

View File

@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.spine_flutter_example">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="spine_flutter_example"
android:name="${applicationName}"

View File

@ -1,4 +1,4 @@
package com.example.example
package com.esotericsoftware.spine.android
import io.flutter.embedding.android.FlutterActivity

View File

@ -1,6 +0,0 @@
package com.example.spine_flutter_example
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.spine_flutter_example">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.

View File

@ -1,16 +1,3 @@
buildscript {
ext.kotlin_version = '1.6.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip

View File

@ -1,11 +1,25 @@
include ':app'
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" // apply true
id "com.android.application" version "8.5.1" apply false
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}
include ":app"

View File

@ -13,18 +13,18 @@ packages:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.0"
collection:
dependency: transitive
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.19.1"
crypto:
dependency: transitive
description:
@ -53,10 +53,10 @@ packages:
dependency: "direct main"
description:
name: flame
sha256: "2a2352741500ce47823dcf212f06b23e9bdb622454eab90244ee6da58e23b488"
sha256: f9e7a100c25f8d6bfd143bf325a9689c509216cd1c8133ce4684955c56770de7
url: "https://pub.dev"
source: hosted
version: "1.15.0"
version: "1.28.1"
flutter:
dependency: "direct main"
description: flutter
@ -114,26 +114,26 @@ packages:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.8.0"
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.16.0"
ordered_set:
dependency: transitive
description:
name: ordered_set
sha256: "3fedcc9121b3ba24c0a84f32da2989c42e36c159b73feadbc2f402dc55966b81"
sha256: dc68b8f1abc7115b81cf890bf7d2ece4ed1d95e0f3e486ab4b64ab3d16d2ea42
url: "https://pub.dev"
source: hosted
version: "5.0.1"
version: "7.0.0"
path:
dependency: transitive
description:
@ -154,7 +154,7 @@ packages:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
version: "0.0.0"
source_span:
dependency: transitive
description:
@ -169,7 +169,7 @@ packages:
path: ".."
relative: true
source: path
version: "4.2.34"
version: "4.2.35"
string_scanner:
dependency: transitive
description:
@ -211,5 +211,5 @@ packages:
source: hosted
version: "0.7.5"
sdks:
dart: ">=3.3.0-0 <4.0.0"
flutter: ">=3.16.0"
dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.27.1"

View File

@ -14,7 +14,7 @@ dependencies:
spine_flutter:
path: ../
cupertino_icons: ^1.0.6
flame: ^1.10.1
flame: ^1.28.1
raw_image_provider: ^0.2.0
dev_dependencies:

View File

@ -1,6 +1,6 @@
name: spine_flutter
description: The official Spine Flutter Runtime to load, display and interact with Spine animations.
version: 4.2.35
version: 4.2.36
homepage: https://esotericsoftware.com
repository: https://github.com/esotericsoftware/spine-runtimes
issue_tracker: https://github.com/esotericsoftware/spine-runtimes/issues

View File

@ -132,7 +132,6 @@ namespace Spine {
float attachmentColorR, attachmentColorG, attachmentColorB, attachmentColorA;
object textureObject = null;
int verticesCount = 0;
float[] vertices = this.vertices;
int indicesCount = 0;
int[] indices = null;
float[] uvs = null;
@ -211,9 +210,10 @@ namespace Spine {
darkColor.A = premultipliedAlpha ? (byte)255 : (byte)0;
// clip
float[] usedVertices = vertices;
if (clipper.IsClipping) {
clipper.ClipTriangles(vertices, indices, indicesCount, uvs);
vertices = clipper.ClippedVertices.Items;
clipper.ClipTriangles(usedVertices, indices, indicesCount, uvs);
usedVertices = clipper.ClippedVertices.Items;
verticesCount = clipper.ClippedVertices.Count >> 1;
indices = clipper.ClippedTriangles.Items;
indicesCount = clipper.ClippedTriangles.Count;
@ -240,8 +240,8 @@ namespace Spine {
for (int ii = 0, v = 0, nn = verticesCount << 1; v < nn; ii++, v += 2) {
itemVertices[ii].Color = color;
itemVertices[ii].Color2 = darkColor;
itemVertices[ii].Position.X = vertices[v];
itemVertices[ii].Position.Y = vertices[v + 1];
itemVertices[ii].Position.X = usedVertices[v];
itemVertices[ii].Position.Y = usedVertices[v + 1];
itemVertices[ii].Position.Z = attachmentZOffset;
itemVertices[ii].TextureCoordinate.X = uvs[v];
itemVertices[ii].TextureCoordinate.Y = uvs[v + 1];

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-ts",
"version": "4.2.81",
"version": "4.2.82",
"description": "The official Spine Runtimes for the web.",
"type": "module",
"files": [
@ -85,9 +85,9 @@
"@types/offscreencanvas": "^2019.6.4",
"concurrently": "^7.6.0",
"copyfiles": "^2.4.1",
"esbuild": "^0.16.4",
"live-server": "^1.2.2",
"esbuild": "^0.25.4",
"alive-server": "^1.3.0",
"rimraf": "^3.0.2",
"typescript": "5.6.2"
}
}
}

View File

@ -22,6 +22,7 @@ sed -i '' "s/$currentVersion/$newVersion/" spine-pixi-v8/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-player/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-threejs/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-webgl/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-webcomponents/package.json
rm package-lock.json
rm -rf node_modules/@esotericsoftware

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-canvas",
"version": "4.2.81",
"version": "4.2.82",
"description": "The official Spine Runtimes for the web.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,6 +31,6 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.81"
"@esotericsoftware/spine-core": "4.2.82"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-canvaskit",
"version": "4.2.81",
"version": "4.2.82",
"description": "The official Spine Runtimes for CanvasKit for NodeJS",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,7 +31,7 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.81",
"@esotericsoftware/spine-core": "4.2.82",
"canvaskit-wasm": "0.39.1"
},
"devDependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-core",
"version": "4.2.81",
"version": "4.2.82",
"description": "The official Spine Runtimes for the web.",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-phaser-v3",
"version": "4.2.81",
"version": "4.2.82",
"description": "The official Spine Runtimes for the Phaser v3.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,9 +31,9 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-canvas": "4.2.81",
"@esotericsoftware/spine-core": "4.2.81",
"@esotericsoftware/spine-webgl": "4.2.81"
"@esotericsoftware/spine-canvas": "4.2.82",
"@esotericsoftware/spine-core": "4.2.82",
"@esotericsoftware/spine-webgl": "4.2.82"
},
"devDependencies": {
"phaser": "^3.60.0"

View File

@ -370,7 +370,10 @@ class SpineAtlasFile extends Phaser.Loader.MultiFile {
}
}
let basePath = file.src.match(/^.*\//) ?? "";
let basePath = (file.src.match(/^.*\//) ?? "").toString();
if (this.loader.path && this.loader.path.length > 0 && basePath.startsWith(this.loader.path))
basePath = basePath.slice(this.loader.path.length);
for (var i = 0; i < textures.length; i++) {
var url = basePath + textures[i];
var key = file.key + "!" + textures[i];

View File

@ -27,12 +27,9 @@
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
declare global {
var require: any;
}
if (typeof window !== 'undefined' && window.Phaser) {
let prevRequire = window.require;
window.require = (x: string) => {
(window as any).require = (x: string) => {
if (prevRequire) return prevRequire(x);
else if (x === "Phaser") return window.Phaser;
}

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-phaser-v4",
"version": "4.2.81",
"version": "4.2.82",
"description": "The official Spine Runtimes for the Phaser v4.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,9 +31,9 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-canvas": "4.2.81",
"@esotericsoftware/spine-core": "4.2.81",
"@esotericsoftware/spine-webgl": "4.2.81"
"@esotericsoftware/spine-canvas": "4.2.82",
"@esotericsoftware/spine-core": "4.2.82",
"@esotericsoftware/spine-webgl": "4.2.82"
},
"devDependencies": {
"phaser": "^4.0.0-rc.1"

View File

@ -362,7 +362,10 @@ class SpineAtlasFile extends Phaser.Loader.MultiFile {
}
}
let basePath = file.src.match(/^.*\//) ?? "";
let basePath = (file.src.match(/^.*\//) ?? "").toString();
if (this.loader.path && this.loader.path.length > 0 && basePath.startsWith(this.loader.path))
basePath = basePath.slice(this.loader.path.length);
for (var i = 0; i < textures.length; i++) {
var url = basePath + textures[i];
var key = file.key + "!" + textures[i];

View File

@ -27,12 +27,9 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
declare global {
var require: any;
}
if (typeof window !== 'undefined' && window.Phaser) {
let prevRequire = window.require;
window.require = (x: string) => {
(window as any).require = (x: string) => {
if (prevRequire) return prevRequire(x);
else if (x === "Phaser") return window.Phaser;
}

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-pixi-v7",
"version": "4.2.81",
"version": "4.2.82",
"description": "The official Spine Runtimes for the web PixiJS v7.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,7 +31,7 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.81"
"@esotericsoftware/spine-core": "4.2.82"
},
"peerDependencies": {
"@pixi/core": "^7.2.4",

View File

@ -27,16 +27,11 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
declare global {
var require: any;
var PIXI: any;
}
if (typeof window !== 'undefined' && window.PIXI) {
if (typeof window !== 'undefined' && (window as any).PIXI) {
let prevRequire = window.require;
window.require = (x: string) => {
(window as any).require = (x: string) => {
if (prevRequire) return prevRequire(x);
else if (x.startsWith("@pixi/")) return window.PIXI;
else if (x.startsWith("@pixi/")) return (window as any).PIXI;
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-pixi-v8",
"version": "4.2.81",
"version": "4.2.82",
"description": "The official Spine Runtimes for PixiJS v8.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,7 +31,7 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.81"
"@esotericsoftware/spine-core": "4.2.82"
},
"peerDependencies": {
"pixi.js": "^8.4.0"

View File

@ -27,16 +27,11 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
declare global {
var require: any;
var PIXI: any;
}
if (typeof window !== 'undefined' && window.PIXI) {
if (typeof window !== 'undefined' && (window as any).PIXI) {
const prevRequire = window.require;
(window as any).require = (x: string) => {
if (prevRequire) return prevRequire(x);
else if (x.startsWith("@pixi/") || x.startsWith("pixi.js")) return window.PIXI;
else if (x.startsWith("@pixi/") || x.startsWith("pixi.js")) return (window as any).PIXI;
};
}

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-player",
"version": "4.2.81",
"version": "4.2.82",
"description": "The official Spine Runtimes for the web.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,6 +31,6 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-webgl": "4.2.81"
"@esotericsoftware/spine-webgl": "4.2.82"
}
}

View File

@ -151,6 +151,9 @@ export interface SpinePlayerConfig {
filter settings from the texture atlas are used. Default: true */
mipmaps?: boolean
/* Optional: Whether the player responds to user click/touch (play/pause, or control bones). Default: true */
interactive?: boolean
/* Optional: List of bone names that the user can drag to position. Default: none */
controlBones?: string[]
@ -239,6 +242,7 @@ export class SpinePlayer implements Disposable {
private previousViewport: Viewport = {} as Viewport;
private viewportTransitionStart = 0;
private eventListeners: Array<{ target: any, event: any, func: any }> = [];
private input?: Input;
constructor (parent: HTMLElement | string, private config: SpinePlayerConfig) {
let parentDom = typeof parent === "string" ? document.getElementById(parent) : parent;
@ -286,6 +290,7 @@ export class SpinePlayer implements Disposable {
var eventListener = this.eventListeners[i];
eventListener.target.removeEventListener(eventListener.event, eventListener.func);
}
this.input?.dispose();
this.parent.removeChild(this.dom);
this.disposed = true;
}
@ -312,6 +317,7 @@ export class SpinePlayer implements Disposable {
if (config.premultipliedAlpha === void 0) config.premultipliedAlpha = true;
if (config.preserveDrawingBuffer === void 0) config.preserveDrawingBuffer = false;
if (config.mipmaps === void 0) config.mipmaps = true;
if (config.interactive === void 0) config.interactive = true;
if (!config.debug) config.debug = {
bones: false,
clipping: false,
@ -592,57 +598,61 @@ export class SpinePlayer implements Disposable {
let skeleton = this.skeleton!;
let renderer = this.sceneRenderer!;
let closest = function (x: number, y: number): Bone | null {
mouse.set(x, canvas.clientHeight - y, 0)
offset.x = offset.y = 0;
let bestDistance = 24, index = 0;
let best: Bone | null = null;
for (let i = 0; i < controlBones.length; i++) {
selectedBones[i] = null;
let bone = skeleton.findBone(controlBones[i]);
if (!bone) continue;
let distance = renderer.camera.worldToScreen(
coords.set(bone.worldX, bone.worldY, 0),
canvas.clientWidth, canvas.clientHeight).distance(mouse);
if (distance < bestDistance) {
bestDistance = distance;
best = bone;
index = i;
offset.x = coords.x - mouse.x;
offset.y = coords.y - mouse.y;
}
}
if (best) selectedBones[index] = best;
return best;
};
new Input(canvas).addListener({
down: (x, y) => {
target = closest(x, y);
},
up: () => {
if (target)
target = null;
else if (config.showControls)
(this.paused ? this.play() : this.pause());
},
dragged: (x, y) => {
if (target) {
x = MathUtils.clamp(x + offset.x, 0, canvas.clientWidth)
y = MathUtils.clamp(y - offset.y, 0, canvas.clientHeight);
renderer.camera.screenToWorld(coords.set(x, y, 0), canvas.clientWidth, canvas.clientHeight);
if (target.parent) {
target.parent.worldToLocal(position.set(coords.x - skeleton.x, coords.y - skeleton.y));
target.x = position.x;
target.y = position.y;
} else {
target.x = coords.x - skeleton.x;
target.y = coords.y - skeleton.y;
if (config.interactive) {
let closest = function (x: number, y: number): Bone | null {
mouse.set(x, canvas.clientHeight - y, 0)
offset.x = offset.y = 0;
let bestDistance = 24, index = 0;
let best: Bone | null = null;
for (let i = 0; i < controlBones.length; i++) {
selectedBones[i] = null;
let bone = skeleton.findBone(controlBones[i]);
if (!bone) continue;
let distance = renderer.camera.worldToScreen(
coords.set(bone.worldX, bone.worldY, 0),
canvas.clientWidth, canvas.clientHeight).distance(mouse);
if (distance < bestDistance) {
bestDistance = distance;
best = bone;
index = i;
offset.x = coords.x - mouse.x;
offset.y = coords.y - mouse.y;
}
}
},
moved: (x, y) => closest(x, y)
});
if (best) selectedBones[index] = best;
return best;
};
this.input = new Input(canvas);
this.input.addListener({
down: (x, y) => {
target = closest(x, y);
},
up: () => {
if (target)
target = null;
else if (config.showControls)
(this.paused ? this.play() : this.pause());
},
dragged: (x, y) => {
if (target) {
x = MathUtils.clamp(x + offset.x, 0, canvas.clientWidth)
y = MathUtils.clamp(y - offset.y, 0, canvas.clientHeight);
renderer.camera.screenToWorld(coords.set(x, y, 0), canvas.clientWidth, canvas.clientHeight);
if (target.parent) {
target.parent.worldToLocal(position.set(coords.x - skeleton.x, coords.y - skeleton.y));
target.x = position.x;
target.y = position.y;
} else {
target.x = coords.x - skeleton.x;
target.y = coords.y - skeleton.y;
}
}
},
moved: (x, y) => closest(x, y)
});
}
if (config.showControls) {
// For manual hover to work, we need to disable hidding controls if the mouse/touch entered the clickable area of a child of the controls.

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-threejs",
"version": "4.2.81",
"version": "4.2.82",
"description": "The official Spine Runtimes for the web.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,7 +31,7 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.81"
"@esotericsoftware/spine-core": "4.2.82"
},
"devDependencies": {
"@types/three": "0.162.0"

View File

@ -27,16 +27,11 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
declare global {
var require: any;
var THREE: any;
}
if (typeof window !== 'undefined' && window.THREE) {
if (typeof window !== 'undefined' && (window as any).THREE) {
let prevRequire = window.require;
window.require = (x: string) => {
(window as any).require = (x: string) => {
if (prevRequire) return prevRequire(x);
else if (x === "three") return window.THREE;
else if (x === "three") return (window as any).THREE;
}
}

View File

@ -231,11 +231,11 @@
></spine-skeleton>
</div>
<div class="split-right">
If you want to preserve the original scale, you can use <code>fit="none"</code>.
If you want to preserve the original scale, you can use <code>fit="none"</code> (center the bounds) or <code>fit="origin"</code> (center the skeleton origin).
In combination with that, you can use the <code>scale</code> attribute to set your desired scale.
<br>
<br>
Other fit modes are <code>width</code>, <code>height</code>, <code>cover</code>, and <code>scaleDown</code>.
Other fit modes are <code>width</code>, <code>height</code>, <code>cover</code>, <code>scaleDown</code>..
</div>
</div>
@ -280,12 +280,20 @@
<div class="split-top split">
<div class="split-left">
<style>
.custom-class {
width: 150px;
height: 150px;
border: 1px solid green;
border-radius: 10px;
box-shadow: -5px 5px 3px rgba(255, 0, 0, 0.3);
}
</style>
<spine-skeleton
atlas="/assets/spineboy-pma.atlas"
skeleton="/assets/spineboy-pro.skel"
animation="walk"
height="150"
width="150"
class="custom-class"
></spine-skeleton>
<spine-skeleton
atlas="/assets/spineboy-pma.atlas"
@ -301,10 +309,9 @@
></spine-skeleton>
</div>
<div class="split-right">
If you want to manually size the Spine widget, specify the <code>width</code> and <code>height</code> attributes in pixels (without the "px" unit).
By default, the widget occupy zero width and height.
If you want to manually size the Spine widget, you can style the component using the <code>style</code> or <code>class</code> attribute, which provides more styling options.
<br>
<br>
If you prefer, you can style the component using the <code>style</code> attribute, which provides more styling options.
</div>
</div>
@ -356,11 +363,6 @@
<div class="split-top split">
<div class="split-left">
The <code>origin</code> mode centers the animation's world origin with the center of the HTML element.
<br>
You are responsible for scaling the skeleton when using this mode.
<br>
<br>
Move the origin by a percentage of the div's width and height using the <code>x-axis</code> and <code>y-axis</code> attributes, respectively.
</div>
<div class="split-right">
@ -368,7 +370,7 @@
atlas="/assets/vine-pma.atlas"
skeleton="/assets/vine-pro.skel"
animation="grow"
mode="origin"
fit="origin"
scale=".5"
y-axis="-.5"
></spine-skeleton>
@ -383,7 +385,7 @@
atlas="/assets/vine-pma.atlas"
skeleton="/assets/vine-pro.skel"
animation="grow"
mode="origin"
fit="origin"
scale=".5"
y-axis="-.5"
></spine-skeleton>
@ -765,10 +767,11 @@
<li><code>mixDuration</code>: the mix duration between this animation and the previous one (not used for the first animation on a track)</li>
</ol>
<p>To loop a track once it reaches the end, add the special group <code>[loop, trackNumber]</code>, where:</p>
<p>To loop a track once it reaches the end, add the special group <code>[loop, trackNumber, repeatDelay]</code>, where:</p>
<ul>
<li><code>loop</code>: identifies this as a loop instruction</li>
<li><code>trackNumber</code>: the number of the track to loop</li>
<li><code>repeatDelay</code>: the number of seconds to wait after the last animation is completed before repeating the loop</li>
</ul>
<p>The parameters of the first group on each track are passed to the <code>setAnimation</code> method, while the remaining groups use <code>addAnimation</code>.</p>
@ -1346,7 +1349,7 @@ function removeDiv() {
Click the button below to toggle the spinner.
<br>
<br>
<input type="button" value="Spinner ON" onclick="toggleSpinner(this)">
<input type="button" value="Spinner OFF" onclick="toggleSpinner(this)">
</div>
</div>
@ -1355,7 +1358,6 @@ function removeDiv() {
async function reloadWidget(element) {
element.disabled = true;
await widget.whenReady;
const skeleton = widget.skeleton;
widget.loading = true;
setTimeout(() => {
element.disabled = false;
@ -1364,7 +1366,7 @@ function removeDiv() {
}
function toggleSpinner(element) {
widget.noSpinner = !widget.noSpinner;
element.value = widget.noSpinner ? "Spinner ON" : "Spinner OFF";
element.value = widget.noSpinner ? "Spinner OFF" : "Spinner ON";
}
</script>
@ -1420,6 +1422,8 @@ function toggleSpinner(element) {
<div class="split-left" style="width: 80%; box-sizing: border-box; min-height: 0;">
It's very easy to display your different skins and animations. Simply create a table and use the <code>skin</code> and <code>animation</code> attributes.
<br>
<code>skin</code> accepts a comma separated list of skin names. The skins will be combined in a new one, from the first to the last. If multiple skins set the same slot, the latest in the list will be used.
</div>
<div class="skin-grid">
@ -2042,19 +2046,16 @@ skins.forEach((skin, i) => {
<div class="split" style="width: 100%; flex-direction: column;">
<div class="split-left" style="width: 80%; box-sizing: border-box; min-height: 0;">
When the widget (or the parent element) enters the viewport, the callback <code>onScreenFunction</code> is invoked.
<br>
<br>
By default, the callback does two things:
<ul>
<li>sets <code>onScreenAtLeastOnce</code> to <code>true</code> when the widget enters the viewport for the first time</li>
<li>if <code>start-when-visible</code> is set, the widget's <code>start</code> method is invoked the first time the widget enters the viewport, and the assets are loaded at that moment.</li>
</ul>
<br>
The assets of the coin below are loaded only when the widget enters the viewport.
<br>
<br>
You can overwrite the <code>onScreenFunction</code> behavior. For example, the raptor below changes its animation every time the widget enters the viewport.
When the widget (or its parent element) enters the viewport, two things happen:<br>
<ul>
<li>the widget's <code>onScreenAtLeastOnce</code> property is set to <code>true</code></li>
<li>the widget's <code>onScreenFunction</code> callback is invoked</li>
</ul>
By default, <code>onScreenFunction</code> invokes the widget's <code>start</code> method if the widget has the <code>start-when-visible</code> attribute set, and this occurs only the first time it enters the viewport.<br>
<br>
The assets of the coin below are loaded only when the widget enters the viewport.<br>
<br>
You can override the <code>onScreenFunction</code> behavior. For example, the raptor below changes its animation every time the widget enters the viewport.
</div>
<div class="skin-grid">
@ -2077,7 +2078,6 @@ skins.forEach((skin, i) => {
<script>
(async () => {
const raptorWidget = await spine.getSpineWidget("coin-with-raptor").whenReady;
let raptorWalking = true;
raptorWidget.onScreenFunction = widget => {
raptorWalking = !raptorWalking;
@ -2096,7 +2096,14 @@ skins.forEach((skin, i) => {
<script>
escapeHTMLandInject(`
<spine-skeleton
identifier="coin"
atlas="/assets/coin-pma.atlas"
skeleton="/assets/coin-pro.skel"
animation="animation"
start-when-visible
></spine-skeleton>
<spine-skeleton
identifier="coin-with-raptor"
atlas="/assets/raptor-pma.atlas"
skeleton="/assets/raptor-pro.skel"
animation="walk"
@ -2689,8 +2696,6 @@ tank.beforeUpdateWorldTransforms = (delta, skeleton, state) => {
<div class="split-left" style="overflow-y: auto; width: 100px; height: 200px; transform: translateZ(0);">
<spine-overlay
overlay-id="scroll"
scrollable
no-auto-parent-transform
overflow-top=".2"
overflow-bottom=".2"
overflow-left=".2"
@ -3271,7 +3276,7 @@ const darkPicker = document.getElementById("dark-picker");
<li><code>followOpacity</code>: the element opacity is connected to the slot alpha</li>
<li><code>followScale</code>: the element scale is connected to the slot scale</li>
<li><code>followRotation</code>: the element rotation is connected to the slot rotation</li>
<li><code>followAttachmentAttach</code>: the element is shown/hidden depending if the slot contains an attachment or not</li>
<li><code>followVisibility</code>: the element is shown/hidden depending if the slot contains an attachment or not</li>
<li><code>hideAttachment</code>: the slot attachment is hidden as if the element replaced the attachment</li>
</ul>
</li>
@ -3296,10 +3301,10 @@ const darkPicker = document.getElementById("dark-picker");
<script>
(async () => {
const widget = await spine.getSpineWidget("potty").whenReady;
widget.followSlot("rain/rain-color", document.getElementById("rain/rain-color"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-white", document.getElementById("rain/rain-white"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-blue", document.getElementById("rain/rain-blue"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-green", document.getElementById("rain/rain-green"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-color", document.getElementById("rain/rain-color"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-white", document.getElementById("rain/rain-white"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-blue", document.getElementById("rain/rain-blue"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-green", document.getElementById("rain/rain-green"), { followVisibility: false, hideAttachment: true });
})();
</script>
@ -3323,10 +3328,10 @@ const darkPicker = document.getElementById("dark-picker");
(async () => {
const widget = await spine.getSpineWidget("potty").whenReady;
widget.followSlot("rain/rain-color", document.getElementById("rain/rain-color"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-white", document.getElementById("rain/rain-white"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-blue", document.getElementById("rain/rain-blue"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-green", document.getElementById("rain/rain-green"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-color", document.getElementById("rain/rain-color"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-white", document.getElementById("rain/rain-white"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-blue", document.getElementById("rain/rain-blue"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-green", document.getElementById("rain/rain-green"), { followVisibility: false, hideAttachment: true });
})();`);</script>
</code></pre>
</div>
@ -3370,10 +3375,10 @@ const darkPicker = document.getElementById("dark-picker");
<script>
(async () => {
const widget = await spine.getSpineWidget("potty2").whenReady;
widget.followSlot("rain/rain-color", spine.getSpineWidget("potty2-1"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-white", spine.getSpineWidget("potty2-2"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-blue", spine.getSpineWidget("potty2-3"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-green", spine.getSpineWidget("potty2-4"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-color", spine.getSpineWidget("potty2-1"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-white", spine.getSpineWidget("potty2-2"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-blue", spine.getSpineWidget("potty2-3"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-green", spine.getSpineWidget("potty2-4"), { followVisibility: false, hideAttachment: true });
})();
</script>
@ -3397,10 +3402,10 @@ const darkPicker = document.getElementById("dark-picker");
(async () => {
const widget = await spine.getSpineWidget("potty2").whenReady;
widget.followSlot("rain/rain-color", spine.getSpineWidget("potty2-1"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-white", spine.getSpineWidget("potty2-2"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-blue", spine.getSpineWidget("potty2-3"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-green", spine.getSpineWidget("potty2-4"), { followAttachmentAttach: false, hideAttachment: true });
widget.followSlot("rain/rain-color", spine.getSpineWidget("potty2-1"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-white", spine.getSpineWidget("potty2-2"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-blue", spine.getSpineWidget("potty2-3"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-green", spine.getSpineWidget("potty2-4"), { followVisibility: false, hideAttachment: true });
})();`);</script>
</code></pre>
</div>

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-webcomponents",
"version": "4.2.80",
"version": "4.2.82",
"description": "The official Spine webcomponents.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,6 +31,6 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-webgl": "4.2.80"
"@esotericsoftware/spine-webgl": "4.2.82"
}
}

View File

@ -149,6 +149,8 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
private lastCanvasBaseWidth = 0;
private lastCanvasBaseHeight = 0;
private zIndex?: number;
private disposed = false;
private loaded = false;
@ -164,13 +166,14 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
*
* In order to fix this behaviour, it is necessary to insert a dedicated `spine-overlay` webcomponent as a direct child of the container.
* Moreover, it is necessary to perform the following actions:
* 1) The scrollable container must have a `transform` css attribute. If it hasn't this attribute the `spine-overlay` will add it for you.
* If your scrollable container has already this css attribute, or if you prefer to add it by yourself (example: `transform: translateZ(0);`), set the `no-auto-parent-transform` to the `spine-overlay`.
* 1) The appendedToBody container must have a `transform` css attribute. If it hasn't this attribute the `spine-overlay` will add it for you.
* If your appendedToBody container has already this css attribute, or if you prefer to add it by yourself (example: `transform: translateZ(0);`), set the `no-auto-parent-transform` to the `spine-overlay`.
* 2) The `spine-overlay` must have an `overlay-id` attribute. Choose the value you prefer.
* 3) Each `spine-skeleton` must have an `overlay-id` attribute. The same as the hosting `spine-overlay`.
* Connected to `scrollable` attribute.
* Connected to `appendedToBody` attribute.
*/
private appendedToBody = true;
private hasParentTransform = true;
readonly time = new TimeKeeper();
@ -257,6 +260,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
widget.onScreen = isIntersecting;
if (isIntersecting) {
widget.onScreenFunction(widget);
widget.onScreenAtLeastOnce = true;
}
}
}
@ -267,8 +271,10 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
// Alternatively, we can store the body size, check the current body size in the loop (like the translateCanvas), and
// if they differs call the resizeCallback. I already tested it, and it works. ResizeObserver should be more efficient.
if (this.appendedToBody) {
// if the element is scrollable, the user does not disable translate tweak, and the parent did not have already a transform, add the tweak
if (this.appendedToBody && !this.noAutoParentTransform && getComputedStyle(this.parentElement!).transform === "none") {
// if the element is appendedToBody, the user does not disable translate tweak, and the parent did not have already a transform, add the tweak
if (this.hasCssTweakOff()) {
this.hasParentTransform = false;
} else {
this.parentElement!.style.transform = `translateZ(0)`;
}
this.resizeObserver = new ResizeObserver(this.resizedCallback);
@ -376,6 +382,8 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
this.parentElement!.appendChild(this);
}
}
this.updateZIndexIfNecessary(widget);
}
/**
@ -475,7 +483,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
const tempVector = new Vector3();
for (const widget of this.widgets) {
const { skeleton, pma, bounds, mode, debug, offsetX, offsetY, xAxis, yAxis, dragX, dragY, fit, noSpinner, onScreen, loading, clip, isDraggable } = widget;
const { skeleton, pma, bounds, debug, offsetX, offsetY, dragX, dragY, fit, noSpinner, loading, clip, isDraggable } = widget;
if (widget.isOffScreenAndWasMoved()) continue;
const elementRef = widget.getHostElement();
@ -489,7 +497,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
divBounds.y -= offsetTopForOverlay;
}
const { padLeft, padRight, padTop, padBottom } = widget
const { padLeft, padRight, padTop, padBottom, xAxis, yAxis } = widget
const paddingShiftHorizontal = (padLeft - padRight) / 2;
const paddingShiftVertical = (padTop - padBottom) / 2;
@ -508,7 +516,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
if (clip) startScissor(divBounds);
if (loading) {
if (noSpinner) {
if (!noSpinner) {
if (!widget.loadingScreen) widget.loadingScreen = new LoadingScreen(renderer);
widget.loadingScreen!.drawInCoordinates(divOriginX, divOriginY);
}
@ -517,7 +525,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
}
if (skeleton) {
if (mode === "inside") {
if (fit !== "origin") {
let { x: ax, y: ay, width: aw, height: ah } = bounds;
if (aw <= 0 || ah <= 0) continue;
@ -583,8 +591,9 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
}
}
const worldOffsetX = divOriginX + offsetX + dragX;
const worldOffsetY = divOriginY + offsetY + dragY;
// const worldOffsetX = divOriginX + offsetX + dragX;
const worldOffsetX = divOriginX + offsetX * window.devicePixelRatio + dragX;
const worldOffsetY = divOriginY + offsetY * window.devicePixelRatio + dragY;
widget.worldX = worldOffsetX;
widget.worldY = worldOffsetY;
@ -626,12 +635,10 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
renderer.circle(true, root.x + worldOffsetX, root.y + worldOffsetY, 10, red);
// show shifted origin
const originX = worldOffsetX - dragX - offsetX;
const originY = worldOffsetY - dragY - offsetY;
renderer.circle(true, originX, originY, 10, green);
renderer.circle(true, divOriginX, divOriginY, 10, green);
// show line from origin to bounds center
renderer.line(originX, originY, bbCenterX, bbCenterY, green);
renderer.line(divOriginX, divOriginY, bbCenterX, bbCenterY, green);
}
if (clip) endScissor();
@ -646,7 +653,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
if (widget.isOffScreenAndWasMoved() || !widget.skeleton) continue;
for (const boneFollower of widget.boneFollowerList) {
const { slot, bone, element, followAttachmentAttach, followRotation, followOpacity, followScale } = boneFollower;
const { slot, bone, element, followVisibility, followRotation, followOpacity, followScale } = boneFollower;
const { worldX, worldY } = widget;
this.worldToScreen(this.tempFollowBoneVector, bone.worldX + worldX, bone.worldY + worldY);
@ -667,7 +674,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
element.style.display = ""
if (followAttachmentAttach && !slot.attachment) {
if (followVisibility && !slot.attachment) {
element.style.opacity = "0";
} else if (followOpacity) {
element.style.opacity = `${slot.color.a}`;
@ -943,7 +950,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
private updateWidgetScales () {
for (const widget of this.widgets) {
// inside mode scale automatically to fit the skeleton within its parent
if (widget.mode !== "origin" && widget.fit !== "none") continue;
if (widget.fit !== "origin" && widget.fit !== "none") continue;
const skeleton = widget.skeleton;
if (!skeleton) continue;
@ -958,6 +965,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
}
}
// this function is invoked each frame - pay attention to what you add here
private translateCanvas () {
let scrollPositionX = -this.overflowLeftSize;
let scrollPositionY = -this.overflowTopSize;
@ -967,9 +975,9 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
scrollPositionY += window.scrollY;
} else {
// Ideally this should be the only scrollable case (no-auto-parent-transform not enabled or at least an ancestor has transform)
// I'd like to get rid of the code below
if (!this.hasCssTweakOff()) {
// Ideally this should be the only appendedToBody case (no-auto-parent-transform not enabled or at least an ancestor has transform)
// I'd like to get rid of the else case
if (this.hasParentTransform) {
scrollPositionX += this.parentElement!.scrollLeft;
scrollPositionY += this.parentElement!.scrollTop;
} else {
@ -979,7 +987,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
let offsetParent = this.offsetParent;
do {
if (offsetParent === document.body) break;
if (offsetParent === null || offsetParent === document.body) break;
const htmlOffsetParentElement = offsetParent as HTMLElement;
if (htmlOffsetParentElement.style.position === "fixed" || htmlOffsetParentElement.style.position === "sticky" || htmlOffsetParentElement.style.position === "absolute") {
@ -1000,6 +1008,23 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
this.canvas.style.transform = `translate(${scrollPositionX}px,${scrollPositionY}px)`;
}
private updateZIndexIfNecessary (element: HTMLElement) {
let parent: HTMLElement | null = element;
let zIndex: undefined | number;
do {
let currentZIndex = parseInt(getComputedStyle(parent).zIndex);
// searching the shallowest z-index
if (!isNaN(currentZIndex)) zIndex = currentZIndex;
parent = parent.parentElement;
} while (parent && parent !== document.body)
if (zIndex && (!this.zIndex || this.zIndex < zIndex)) {
this.zIndex = zIndex;
this.div.style.zIndex = `${this.zIndex}`;
}
}
/*
* Other utilities
*/

View File

@ -49,6 +49,7 @@ import {
RegionAttachment,
MeshAttachment,
Bone,
Skin,
} from "@esotericsoftware/spine-webgl";
import { AttributeTypes, castValue, isBase64, Rectangle } from "./wcUtils.js";
import { SpineWebComponentOverlay } from "./SpineWebComponentOverlay.js";
@ -56,9 +57,12 @@ import { SpineWebComponentOverlay } from "./SpineWebComponentOverlay.js";
type UpdateSpineWidgetFunction = (delta: number, skeleton: Skeleton, state: AnimationState) => void;
export type OffScreenUpdateBehaviourType = "pause" | "update" | "pose";
export type ModeType = "inside" | "origin";
export type FitType = "fill" | "width" | "height" | "contain" | "cover" | "none" | "scaleDown";
export type AnimationsInfo = Record<string, { cycle?: boolean, animations: Array<AnimationsType> }>;
export type FitType = "fill" | "width" | "height" | "contain" | "cover" | "none" | "scaleDown" | "origin";
export type AnimationsInfo = Record<string, {
cycle?: boolean,
repeatDelay?: number;
animations: Array<AnimationsType>
}>;
export type AnimationsType = { animationName: string | "#EMPTY#", loop?: boolean, delay?: number, mixDuration?: number };
export type CursorEventType = "down" | "up" | "enter" | "leave" | "move" | "drag";
export type CursorEventTypesInput = Exclude<CursorEventType, "enter" | "leave">;
@ -73,9 +77,8 @@ interface WidgetAttributes {
animation?: string
animations?: AnimationsInfo
defaultMix?: number
skin?: string
skin?: string[]
fit: FitType
mode: ModeType
xAxis: number
yAxis: number
offsetX: number
@ -217,14 +220,14 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
* Optional: The name of the skin to be set
* Connected to `skin` attribute.
*/
public get skin (): string | undefined {
public get skin (): string[] | undefined {
return this._skin;
}
public set skin (value: string | undefined) {
public set skin (value: string[] | undefined) {
this._skin = value;
this.initWidget();
}
private _skin?: string
private _skin?: string[]
/**
* Specify the way the skeleton is sized within the element automatically changing its `scaleX` and `scaleY`.
@ -236,19 +239,11 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
* - `cover`: as small as possible while still covering the entire element container.
* - `scaleDown`: scale the skeleton down to ensure that the skeleton fits within the element container.
* - `none`: display the skeleton without autoscaling it.
* - `origin`: the skeleton origin is centered with the element container regardless of the bounds.
* Connected to `fit` attribute.
*/
public fit: FitType = "contain";
/**
* Specify the way the skeleton is centered within the element container:
* - `inside`: the skeleton bounds center is centered with the element container (Default)
* - `origin`: the skeleton origin is centered with the element container regardless of the bounds.
* Origin does not allow to specify any {@link fit} type and guarantee the skeleton to not be autoscaled.
* Connected to `mode` attribute.
*/
public mode: ModeType = "inside";
/**
* The x offset of the skeleton world origin x axis as a percentage of the element container width
* Connected to `x-axis` attribute.
@ -562,16 +557,11 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
/**
* A callback invoked each time the element container enters the screen viewport.
* By default, the callback call the {@link start} method the first time the widget
* enters the screen viewport.
* enters the screen viewport and {@link startWhenVisible} is `true`.
*/
public onScreenFunction: (widget: SpineWebComponentSkeleton) => void = async (widget) => {
if (widget.loading && !widget.onScreenAtLeastOnce) {
widget.onScreenAtLeastOnce = true;
if (widget.manualStart && widget.startWhenVisible) {
widget.start();
}
}
if (widget.loading && !widget.onScreenAtLeastOnce && widget.manualStart && widget.startWhenVisible)
widget.start()
}
/**
@ -706,7 +696,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
animations: { propertyName: "animations", type: "animationsInfo", defaultValue: undefined },
"animation-bounds": { propertyName: "animationsBound", type: "array-string", defaultValue: undefined },
"default-mix": { propertyName: "defaultMix", type: "number", defaultValue: 0 },
skin: { propertyName: "skin", type: "string" },
skin: { propertyName: "skin", type: "array-string" },
width: { propertyName: "width", type: "number", defaultValue: -1 },
height: { propertyName: "height", type: "number", defaultValue: -1 },
isdraggable: { propertyName: "isDraggable", type: "boolean" },
@ -732,7 +722,6 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
clip: { propertyName: "clip", type: "boolean" },
pages: { propertyName: "pages", type: "array-number" },
fit: { propertyName: "fit", type: "fitType", defaultValue: "contain" },
mode: { propertyName: "mode", type: "modeType", defaultValue: "inside" },
offscreen: { propertyName: "offScreenUpdateBehaviour", type: "offScreenUpdateBehaviourType", defaultValue: "pause" },
}
@ -876,7 +865,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
* @returns The `HTMLElement` where the widget is hosted.
*/
public getHostElement (): HTMLElement {
return (this.width <= 0 || this.width <= 0) && !this.getAttribute("style")
return (this.width <= 0 || this.width <= 0) && !this.getAttribute("style") && !this.getAttribute("class")
? this.parentElement!
: this;
}
@ -1009,18 +998,28 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
// skeleton.scaleX = this.dprScale;
// skeleton.scaleY = this.dprScale;
this.loading = false;
// the bounds are calculated the first time, if no custom bound is provided
this.initWidget(this.bounds.width <= 0 || this.bounds.height <= 0);
this.loading = false;
return this;
}
private initWidget (forceRecalculate = false) {
if (this.loading) return;
const { skeleton, state, animation, animations: animationsInfo, skin, defaultMix } = this;
if (skin) {
skeleton?.setSkinByName(skin);
if (skin.length === 1) {
skeleton?.setSkinByName(skin[0]);
} else {
const customSkin = new Skin("custom");
for (const s of skin) customSkin.addSkin(skeleton?.data.findSkin(s) as Skin);
skeleton?.setSkin(customSkin);
}
skeleton?.setSlotsToSetupPose();
}
@ -1028,7 +1027,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
state.data.defaultMix = defaultMix;
if (animationsInfo) {
for (const [trackIndexString, { cycle, animations }] of Object.entries(animationsInfo)) {
for (const [trackIndexString, { cycle, animations, repeatDelay }] of Object.entries(animationsInfo)) {
const cycleFn = () => {
const trackIndex = Number(trackIndexString);
for (const [index, { animationName, delay, loop, mixDuration }] of animations.entries()) {
@ -1050,7 +1049,15 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
if (mixDuration) track.mixDuration = mixDuration;
if (cycle && index === animations.length - 1) {
track.listener = { complete: () => cycleFn() };
track.listener = {
complete: () => {
if (repeatDelay)
setTimeout(() => cycleFn(), 1000 * repeatDelay);
else
cycleFn();
delete track.listener?.complete;
}
};
};
}
}
@ -1068,22 +1075,13 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
}
private render (): void {
let width;
let height;
if (this.width === -1 || this.height === -1) {
width = "0";
height = "0";
} else {
width = `${this.width}px`
height = `${this.height}px`
}
let noSize = (!this.getAttribute("style") && !this.getAttribute("class"));
this.root.innerHTML = `
<style>
:host {
position: relative;
display: inline-block;
width: ${width};
height: ${height};
${noSize ? "width: 0; height: 0;" : ""}
}
</style>
`;
@ -1233,10 +1231,10 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
* Other utilities
*/
public boneFollowerList: Array<{ slot: Slot, bone: Bone, element: HTMLElement, followAttachmentAttach: boolean, followRotation: boolean, followOpacity: boolean, followScale: boolean, hideAttachment: boolean }> = [];
public followSlot (slotName: string | Slot, element: HTMLElement, options: { followAttachmentAttach?: boolean, followRotation?: boolean, followOpacity?: boolean, followScale?: boolean, hideAttachment?: boolean } = {}) {
public boneFollowerList: Array<{ slot: Slot, bone: Bone, element: HTMLElement, followVisibility: boolean, followRotation: boolean, followOpacity: boolean, followScale: boolean, hideAttachment: boolean }> = [];
public followSlot (slotName: string | Slot, element: HTMLElement, options: { followVisibility?: boolean, followRotation?: boolean, followOpacity?: boolean, followScale?: boolean, hideAttachment?: boolean } = {}) {
const {
followAttachmentAttach = false,
followVisibility = false,
followRotation = true,
followOpacity = true,
followScale = true,
@ -1255,7 +1253,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
element.style.left = '0px';
element.style.display = 'none';
this.boneFollowerList.push({ slot, bone: slot.bone, element, followAttachmentAttach, followRotation, followOpacity, followScale, hideAttachment });
this.boneFollowerList.push({ slot, bone: slot.bone, element, followVisibility, followRotation, followOpacity, followScale, hideAttachment });
this.overlay.addSlotFollowerElement(element);
}
public unfollowSlot (element: HTMLElement): HTMLElement | undefined {

View File

@ -27,10 +27,10 @@
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import { AnimationsInfo, FitType, ModeType, OffScreenUpdateBehaviourType } from "./SpineWebComponentSkeleton.js";
import { AnimationsInfo, FitType, OffScreenUpdateBehaviourType } from "./SpineWebComponentSkeleton.js";
const animatonTypeRegExp = /\[([^\]]+)\]/g;
export type AttributeTypes = "string" | "number" | "boolean" | "array-number" | "array-string" | "object" | "fitType" | "modeType" | "offScreenUpdateBehaviourType" | "animationsInfo";
export type AttributeTypes = "string" | "number" | "boolean" | "array-number" | "array-string" | "object" | "fitType" | "offScreenUpdateBehaviourType" | "animationsInfo";
export function castValue (type: AttributeTypes, value: string | null, defaultValue?: any) {
switch (type) {
@ -48,8 +48,6 @@ export function castValue (type: AttributeTypes, value: string | null, defaultVa
return castObject(value, defaultValue);
case "fitType":
return isFitType(value) ? value : defaultValue;
case "modeType":
return isModeType(value) ? value : defaultValue;
case "offScreenUpdateBehaviourType":
return isOffScreenUpdateBehaviourType(value) ? value : defaultValue;
case "animationsInfo":
@ -104,7 +102,7 @@ function castToAnimationsInfo (value: string | null): AnimationsInfo | undefined
if (!matches) return undefined;
return matches.reduce((obj, group) => {
const [trackIndexStringOrLoopDefinition, animationNameOrTrackIndexStringCycle, loop, delayString, mixDurationString] = group.slice(1, -1).split(',').map(v => v.trim());
const [trackIndexStringOrLoopDefinition, animationNameOrTrackIndexStringCycle, loopOrRepeatDelay, delayString, mixDurationString] = group.slice(1, -1).split(',').map(v => v.trim());
if (trackIndexStringOrLoopDefinition === "loop") {
if (!Number.isInteger(Number(animationNameOrTrackIndexStringCycle))) {
@ -112,6 +110,15 @@ function castToAnimationsInfo (value: string | null): AnimationsInfo | undefined
}
const animationInfoObject = obj[animationNameOrTrackIndexStringCycle] ||= { animations: [] };
animationInfoObject.cycle = true;
if (loopOrRepeatDelay !== undefined) {
const repeatDelay = Number(loopOrRepeatDelay);
if (Number.isNaN(repeatDelay)) {
throw new Error(`If present, duration of last animation of cycle in ${group} must be a positive integer number, instead it is ${loopOrRepeatDelay}. Original value: ${value}`);
}
animationInfoObject.repeatDelay = repeatDelay;
}
return obj;
}
@ -139,7 +146,7 @@ function castToAnimationsInfo (value: string | null): AnimationsInfo | undefined
const animationInfoObject = obj[trackIndexStringOrLoopDefinition] ||= { animations: [] };
animationInfoObject.animations.push({
animationName: animationNameOrTrackIndexStringCycle,
loop: loop.trim().toLowerCase() === "true",
loop: (loopOrRepeatDelay || "").trim().toLowerCase() === "true",
delay,
mixDuration,
});
@ -155,7 +162,8 @@ function isFitType (value: string | null): value is FitType {
value === "contain" ||
value === "cover" ||
value === "none" ||
value === "scaleDown"
value === "scaleDown" ||
value === "origin"
);
}
@ -167,12 +175,6 @@ function isOffScreenUpdateBehaviourType (value: string | null): value is OffScre
);
}
function isModeType (value: string | null): value is ModeType {
return (
value === "inside" ||
value === "origin"
);
}
const base64RegExp = /^(([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==))$/;
export function isBase64 (str: string) {
return base64RegExp.test(str);

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-webgl",
"version": "4.2.81",
"version": "4.2.82",
"description": "The official Spine Runtimes for the web.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,6 +31,6 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.81"
"@esotericsoftware/spine-core": "4.2.82"
}
}

View File

@ -83,6 +83,8 @@ namespace Spine.Unity.Editor {
}
public static class SpineBuildEnvUtility {
public const string SPINE_ALLOW_UNSAFE_CODE = "SPINE_ALLOW_UNSAFE";
static bool IsInvalidGroup (BuildTargetGroup group) {
int gi = (int)group;
return
@ -99,15 +101,18 @@ namespace Spine.Unity.Editor {
if (IsInvalidGroup(group))
continue;
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group);
if (!defines.Contains(define)) {
wasDefineAdded = true;
if (defines.EndsWith(";", System.StringComparison.Ordinal))
defines += define;
else
defines += ";" + define;
try {
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group);
if (!defines.Contains(define)) {
wasDefineAdded = true;
if (defines.EndsWith(";", System.StringComparison.Ordinal))
defines += define;
else
defines += ";" + define;
PlayerSettings.SetScriptingDefineSymbolsForGroup(group, defines);
PlayerSettings.SetScriptingDefineSymbolsForGroup(group, defines);
}
} catch (System.Exception) {
}
}
Debug.LogWarning("Please ignore errors \"PlayerSettings Validation: Requested build target group doesn't exist\" above");
@ -127,15 +132,18 @@ namespace Spine.Unity.Editor {
if (IsInvalidGroup(group))
continue;
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group);
if (defines.Contains(define)) {
wasDefineRemoved = true;
if (defines.Contains(define + ";"))
defines = defines.Replace(define + ";", "");
else
defines = defines.Replace(define, "");
try {
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group);
if (defines.Contains(define)) {
wasDefineRemoved = true;
if (defines.Contains(define + ";"))
defines = defines.Replace(define + ";", "");
else
defines = defines.Replace(define, "");
PlayerSettings.SetScriptingDefineSymbolsForGroup(group, defines);
PlayerSettings.SetScriptingDefineSymbolsForGroup(group, defines);
}
} catch (System.Exception) {
}
}

View File

@ -39,6 +39,14 @@
#define HAS_ON_POSTPROCESS_PREFAB
#endif
#if UNITY_2021_2_OR_NEWER
#define TEXT_ASSET_HAS_GET_DATA_BYTES
#endif
#if TEXT_ASSET_HAS_GET_DATA_BYTES
#define HAS_ANY_UNSAFE_OPTIONS
#endif
using System.Threading;
using UnityEditor;
using UnityEngine;
@ -356,6 +364,18 @@ namespace Spine.Unity.Editor {
}
#endif
#if HAS_ANY_UNSAFE_OPTIONS
GUILayout.Space(20);
EditorGUILayout.LabelField("Unsafe Build Defines", EditorStyles.boldLabel);
using (new GUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel(new GUIContent("Direct data access", "Allow unsafe direct data access. Currently affects reading .skel.bytes files, reading with fewer allocations."));
if (GUILayout.Button("Enable", GUILayout.Width(64)))
SpineBuildEnvUtility.EnableBuildDefine(SpineBuildEnvUtility.SPINE_ALLOW_UNSAFE_CODE);
if (GUILayout.Button("Disable", GUILayout.Width(64)))
SpineBuildEnvUtility.DisableBuildDefine(SpineBuildEnvUtility.SPINE_ALLOW_UNSAFE_CODE);
}
#endif
#if SPINE_TK2D_DEFINE
bool isTK2DDefineSet = true;
#else

View File

@ -1,4 +1,5 @@
{
"name": "spine-unity",
"references": [ "spine-csharp" ]
"references": [ "spine-csharp" ],
"allowUnsafeCode": true
}

View File

@ -27,14 +27,39 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
//#define SPINE_ALLOW_UNSAFE // note: this define can be set via Edit - Preferences - Spine.
#if UNITY_2021_2_OR_NEWER
#define TEXT_ASSET_HAS_GET_DATA_BYTES
#endif
#if SPINE_ALLOW_UNSAFE && TEXT_ASSET_HAS_GET_DATA_BYTES
#define UNSAFE_DIRECT_ACCESS_TEXT_ASSET_DATA
#endif
using System;
using System.Collections.Generic;
using System.IO;
#if UNSAFE_DIRECT_ACCESS_TEXT_ASSET_DATA
using Unity.Collections;
#endif
using UnityEngine;
using CompatibilityProblemInfo = Spine.Unity.SkeletonDataCompatibility.CompatibilityProblemInfo;
namespace Spine.Unity {
#if UNSAFE_DIRECT_ACCESS_TEXT_ASSET_DATA
public static class TextAssetExtensions {
public static Stream GetStreamUnsafe (this TextAsset textAsset) {
NativeArray<byte> dataNativeArray = textAsset.GetData<byte>();
return dataNativeArray.GetUnmanagedMemoryStream();
}
public static unsafe UnmanagedMemoryStream GetUnmanagedMemoryStream<T> (this NativeArray<T> nativeArray) where T : struct {
return new UnmanagedMemoryStream((byte*)global::Unity.Collections.LowLevel.Unsafe.
NativeArrayUnsafeUtility.GetUnsafeReadOnlyPtr(nativeArray), nativeArray.Length);
}
}
#endif
[CreateAssetMenu(fileName = "New SkeletonDataAsset", menuName = "Spine/SkeletonData Asset")]
public class SkeletonDataAsset : ScriptableObject {
@ -188,9 +213,15 @@ namespace Spine.Unity {
SkeletonData loadedSkeletonData = null;
try {
if (hasBinaryExtension)
if (hasBinaryExtension) {
#if UNSAFE_DIRECT_ACCESS_TEXT_ASSET_DATA
using (Stream stream = skeletonJSON.GetStreamUnsafe()) {
loadedSkeletonData = SkeletonDataAsset.ReadSkeletonData(stream, attachmentLoader, skeletonDataScale);
}
#else
loadedSkeletonData = SkeletonDataAsset.ReadSkeletonData(skeletonJSON.bytes, attachmentLoader, skeletonDataScale);
else
#endif
} else
loadedSkeletonData = SkeletonDataAsset.ReadSkeletonData(skeletonJSON.text, attachmentLoader, skeletonDataScale);
} catch (Exception ex) {
if (!quiet)
@ -287,6 +318,13 @@ namespace Spine.Unity {
}
}
internal static SkeletonData ReadSkeletonData (Stream assetStream, AttachmentLoader attachmentLoader, float scale) {
SkeletonBinary binary = new SkeletonBinary(attachmentLoader) {
Scale = scale
};
return binary.ReadSkeletonData(assetStream);
}
internal static SkeletonData ReadSkeletonData (string text, AttachmentLoader attachmentLoader, float scale) {
StringReader input = new StringReader(text);
SkeletonJson json = new SkeletonJson(attachmentLoader) {

View File

@ -27,6 +27,16 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
//#define SPINE_ALLOW_UNSAFE // note: this define can be set via Edit - Preferences - Spine.
#if UNITY_2021_2_OR_NEWER
#define TEXT_ASSET_HAS_GET_DATA_BYTES
#endif
#if SPINE_ALLOW_UNSAFE && TEXT_ASSET_HAS_GET_DATA_BYTES
#define UNSAFE_DIRECT_ACCESS_TEXT_ASSET_DATA
#endif
using System;
using System.Collections.Generic;
using System.IO;
@ -106,8 +116,12 @@ namespace Spine.Unity {
if (fileVersion.sourceType == SourceType.Binary) {
try {
using (MemoryStream memStream = new MemoryStream(asset.bytes)) {
fileVersion.rawVersion = SkeletonBinary.GetVersionString(memStream);
#if UNSAFE_DIRECT_ACCESS_TEXT_ASSET_DATA
using (Stream stream = asset.GetStreamUnsafe()) {
#else
using (MemoryStream stream = new MemoryStream(asset.bytes)) {
#endif
fileVersion.rawVersion = SkeletonBinary.GetVersionString(stream);
}
} catch (System.Exception e) {
problemDescription = string.Format("Failed to read '{0}'. It is likely not a binary Spine SkeletonData file.\n{1}", asset.name, e);
@ -162,8 +176,11 @@ namespace Spine.Unity {
}
public static bool IsJsonFile (TextAsset file) {
#if TEXT_ASSET_HAS_GET_DATA_BYTES
var content = file.GetData<byte>();
#else
byte[] content = file.bytes;
#endif
// check for binary skeleton version number string, starts after 8 byte hash
char majorVersionChar = compatibleBinaryVersions[0][0].ToString()[0];
if (content.Length > 10 && content[9] == majorVersionChar && content[10] == '.')