diff --git a/CHANGELOG.md b/CHANGELOG.md index da932ff..afe1e5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased * **Added** switch data file exporter for Moho (formerly Anime Studio) and OpenToonz ([issue #69](https://github.com/DanielSWolf/rhubarb-lip-sync/issues/69)) +* **Added** support for Spine 3.8 beta ([issue #74](https://github.com/DanielSWolf/rhubarb-lip-sync/issues/74)) * **Improved** animation rule for OW sound: animating it as E-F rather than F. ## Version 1.9.1 diff --git a/extras/EsotericSoftwareSpine/CMakeLists.txt b/extras/EsotericSoftwareSpine/CMakeLists.txt index 28786d2..c341cdd 100644 --- a/extras/EsotericSoftwareSpine/CMakeLists.txt +++ b/extras/EsotericSoftwareSpine/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.2) add_custom_target( rhubarbForSpine ALL - "./gradlew" "jar" + "./gradlew" "shadowJar" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" COMMENT "Building Rhubarb for Spine through Gradle." ) diff --git a/extras/EsotericSoftwareSpine/build.gradle b/extras/EsotericSoftwareSpine/build.gradle deleted file mode 100644 index 21e6aa7..0000000 --- a/extras/EsotericSoftwareSpine/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -def getVersion() { - // Dynamically read version from CMake file - String text = new File('../../appInfo.cmake').getText('UTF-8') - String major = (text =~ /appVersionMajor\s+(\d+)/)[0][1] - String minor = (text =~ /appVersionMinor\s+(\d+)/)[0][1] - String patch = (text =~ /appVersionPatch\s+(\d+)/)[0][1] - String suffix = (text =~ /appVersionSuffix\s+"(.*?)"/)[0][1] - String result = "${major}.${minor}.${patch}${suffix}" - return result -} - -group 'com.rhubarb_lip_sync' -version = getVersion() - -buildscript { - ext.kotlin_version = '1.1.60' - - repositories { - mavenCentral() - } - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -apply plugin: 'kotlin' - -repositories { - mavenCentral() - jcenter() -} - -dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" - compile 'com.beust:klaxon:0.30' - compile 'org.apache.commons:commons-lang3:3.7' - compile 'no.tornado:tornadofx:1.7.12' -} - -compileKotlin { - kotlinOptions.jvmTarget = '1.8' -} -compileTestKotlin { - kotlinOptions.jvmTarget = '1.8' -} - -jar { - manifest { - attributes 'Main-Class': 'com.rhubarb_lip_sync.rhubarb_for_spine.MainKt' - } - - // This line of code recursively collects and copies all of a project's files - // and adds them to the JAR itself. One can extend this task, to skip certain - // files or particular types at will - from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } -} \ No newline at end of file diff --git a/extras/EsotericSoftwareSpine/build.gradle.kts b/extras/EsotericSoftwareSpine/build.gradle.kts new file mode 100644 index 0000000..97a95f1 --- /dev/null +++ b/extras/EsotericSoftwareSpine/build.gradle.kts @@ -0,0 +1,55 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.io.File +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + kotlin("jvm") version "1.3.41" + id("com.github.johnrengelman.shadow") version "5.1.0" +} + +fun getVersion(): String { + // Dynamically read version from CMake file + val file = File(rootDir.parentFile.parentFile, "appInfo.cmake") + val text = file.readText() + val major = Regex("""appVersionMajor\s+(\d+)""").find(text)!!.groupValues[1] + val minor = Regex("""appVersionMinor\s+(\d+)""").find(text)!!.groupValues[1] + val patch = Regex("""appVersionPatch\s+(\d+)""").find(text)!!.groupValues[1] + val suffix = Regex("""appVersionSuffix\s+"(.*?)"""").find(text)!!.groupValues[1] + return "$major.$minor.$patch$suffix" +} + +group = "com.rhubarb_lip_sync" +version = getVersion() + +repositories { + mavenCentral() + jcenter() +} + +dependencies { + implementation(kotlin("stdlib-jdk8")) + implementation("com.beust:klaxon:5.0.1") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("no.tornado:tornadofx:1.7.19") + testImplementation("org.junit.jupiter:junit-jupiter:5.5.0") + testCompile("org.assertj:assertj-core:3.11.1") +} + +tasks.withType { + kotlinOptions.jvmTarget = "1.8" +} + +tasks.test { + useJUnitPlatform() +} + +tasks.shadowJar { + dependsOn(tasks.test) + + // Modified by shadow plugin + archiveClassifier.set(null as String?) + + manifest { + attributes("Main-Class" to "com.rhubarb_lip_sync.rhubarb_for_spine.MainKt") + } +} diff --git a/extras/EsotericSoftwareSpine/gradle/wrapper/gradle-wrapper.jar b/extras/EsotericSoftwareSpine/gradle/wrapper/gradle-wrapper.jar index 27768f1..94336fc 100644 Binary files a/extras/EsotericSoftwareSpine/gradle/wrapper/gradle-wrapper.jar and b/extras/EsotericSoftwareSpine/gradle/wrapper/gradle-wrapper.jar differ diff --git a/extras/EsotericSoftwareSpine/gradle/wrapper/gradle-wrapper.properties b/extras/EsotericSoftwareSpine/gradle/wrapper/gradle-wrapper.properties index 62e1e30..e0c4de3 100644 --- a/extras/EsotericSoftwareSpine/gradle/wrapper/gradle-wrapper.properties +++ b/extras/EsotericSoftwareSpine/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/extras/EsotericSoftwareSpine/settings.gradle b/extras/EsotericSoftwareSpine/settings.gradle deleted file mode 100644 index dc991a3..0000000 --- a/extras/EsotericSoftwareSpine/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = 'rhubarb-for-spine' - diff --git a/extras/EsotericSoftwareSpine/settings.gradle.kts b/extras/EsotericSoftwareSpine/settings.gradle.kts new file mode 100644 index 0000000..6e921c8 --- /dev/null +++ b/extras/EsotericSoftwareSpine/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "rhubarb-for-spine" diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/AnimationFileModel.kt similarity index 97% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/AnimationFileModel.kt index 1f5c884..cb8edd5 100644 --- a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AnimationFileModel.kt +++ b/extras/EsotericSoftwareSpine/src/main/kotlin/AnimationFileModel.kt @@ -6,6 +6,7 @@ import javafx.beans.property.SimpleListProperty import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleStringProperty import javafx.collections.ObservableList +import tornadofx.asObservable import java.nio.file.Path import tornadofx.getValue import tornadofx.observable @@ -61,7 +62,7 @@ class AnimationFileModel(val parentModel: MainModel, animationFilePath: Path, pr audioFileModel = AudioFileModel(event, this, executor, reportResult) return@map audioFileModel } - .observable() + .asObservable() ) val audioFileModels: ObservableList by audioFileModelsProperty @@ -97,7 +98,7 @@ class AnimationFileModel(val parentModel: MainModel, animationFilePath: Path, pr } init { - slots = spineJson.slots.observable() + slots = spineJson.slots.asObservable() mouthSlot = spineJson.guessMouthSlot() } diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/AudioFileModel.kt similarity index 100% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/AudioFileModel.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/AudioFileModel.kt diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/EndUserException.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/EndUserException.kt similarity index 100% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/EndUserException.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/EndUserException.kt diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/ErrorProperty.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/ErrorProperty.kt similarity index 100% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/ErrorProperty.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/ErrorProperty.kt diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainApp.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/MainApp.kt similarity index 100% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainApp.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/MainApp.kt diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/MainModel.kt similarity index 100% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainModel.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/MainModel.kt diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/MainView.kt similarity index 98% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/MainView.kt index 7a67e91..abdd109 100644 --- a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MainView.kt +++ b/extras/EsotericSoftwareSpine/src/main/kotlin/MainView.kt @@ -115,13 +115,13 @@ class MainView : View() { placeholder = Label("There are no events with associated audio files.") columnResizePolicy = SmartResize.POLICY column("Event", AudioFileModel::eventNameProperty) - .weigthedWidth(1.0) + .weightedWidth(1.0) column("Animation name", AudioFileModel::animationNameProperty) - .weigthedWidth(1.0) + .weightedWidth(1.0) column("Audio file", AudioFileModel::displayFilePathProperty) - .weigthedWidth(1.0) + .weightedWidth(1.0) column("Dialog", AudioFileModel::dialogProperty).apply { - weigthedWidth(3.0) + weightedWidth(3.0) // Make dialog column wrap setCellFactory { tableColumn -> return@setCellFactory TableCell().also { cell -> @@ -138,7 +138,7 @@ class MainView : View() { } } column("Status", AudioFileModel::audioFileStateProperty).apply { - weigthedWidth(1.0) + weightedWidth(1.0) setCellFactory { return@setCellFactory object : TableCell() { override fun updateItem(state: AudioFileState?, empty: Boolean) { @@ -176,7 +176,7 @@ class MainView : View() { } } column("", AudioFileModel::actionLabelProperty).apply { - weigthedWidth(1.0) + weightedWidth(1.0) // Show button setCellFactory { return@setCellFactory object : TableCell() { @@ -254,4 +254,4 @@ class MainView : View() { ) } } -} \ No newline at end of file +} diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthCue.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/MouthCue.kt similarity index 100% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthCue.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/MouthCue.kt diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthNaming.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/MouthNaming.kt similarity index 100% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthNaming.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/MouthNaming.kt diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthShape.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/MouthShape.kt similarity index 100% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/MouthShape.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/MouthShape.kt diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/RhubarbTask.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/RhubarbTask.kt similarity index 97% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/RhubarbTask.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/RhubarbTask.kt index 0694e79..d615ef2 100644 --- a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/RhubarbTask.kt +++ b/extras/EsotericSoftwareSpine/src/main/kotlin/RhubarbTask.kt @@ -1,9 +1,6 @@ package com.rhubarb_lip_sync.rhubarb_for_spine import com.beust.klaxon.JsonObject -import com.beust.klaxon.array -import com.beust.klaxon.double -import com.beust.klaxon.string import com.beust.klaxon.Parser as JsonParser import org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS import java.io.* @@ -78,7 +75,7 @@ class RhubarbTask( } } - private val jsonParser = JsonParser() + private val jsonParser = JsonParser.default() private fun parseJsonObject(jsonString: String): JsonObject { return jsonParser.parse(StringReader(jsonString)) as JsonObject } diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/SpineJson.kt similarity index 83% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/SpineJson.kt index 2fe77b8..22d4bef 100644 --- a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/SpineJson.kt +++ b/extras/EsotericSoftwareSpine/src/main/kotlin/SpineJson.kt @@ -17,13 +17,18 @@ class SpineJson(private val filePath: Path) { throw EndUserException("File '$filePath' does not exist.") } try { - json = Parser().parse(filePath.toString()) as JsonObject + json = Parser.default().parse(filePath.toString()) as JsonObject } catch (e: Exception) { throw EndUserException("Wrong file format. This is not a valid JSON file.") } skeleton = json.obj("skeleton") ?: throw EndUserException("JSON file is corrupted.") - val skins = json.obj("skins") ?: throw EndUserException("JSON file doesn't contain skins.") - defaultSkin = skins.obj("default") ?: throw EndUserException("JSON file doesn't have a default skin.") + val skins = json["skins"] ?: throw EndUserException("JSON file doesn't contain skins.") + defaultSkin = when (skins) { + is JsonObject -> skins.obj("default") + is JsonArray<*> -> (skins as JsonArray).find { it.string("name") == "default" } + else -> null + } ?: throw EndUserException("JSON file doesn't have a default skin.") + validateProperties() } @@ -35,7 +40,7 @@ class SpineJson(private val filePath: Path) { private val imagesDirectoryPath: Path get() { val relativeImagesDirectory = skeleton.string("images") ?: throw EndUserException("JSON file is incomplete: Images path is missing." - + "Make sure to check 'Nonessential data' when exporting.") + + " Make sure to check 'Nonessential data' when exporting.") val imagesDirectoryPath = fileDirectoryPath.resolve(relativeImagesDirectory).normalize() if (!Files.exists(imagesDirectoryPath)) { @@ -49,7 +54,7 @@ class SpineJson(private val filePath: Path) { val audioDirectoryPath: Path get() { val relativeAudioDirectory = skeleton.string("audio") ?: throw EndUserException("JSON file is incomplete: Audio path is missing." - + "Make sure to check 'Nonessential data' when exporting.") + + " Make sure to check 'Nonessential data' when exporting.") val audioDirectoryPath = fileDirectoryPath.resolve(relativeAudioDirectory).normalize() if (!Files.exists(audioDirectoryPath)) { @@ -91,7 +96,9 @@ class SpineJson(private val filePath: Path) { } fun getSlotAttachmentNames(slotName: String): List { - val attachments = defaultSkin.obj(slotName) ?: JsonObject() + val attachments = defaultSkin.obj(slotName) + ?: defaultSkin.obj("attachments")?.obj(slotName) + ?: JsonObject() return attachments.map { it.key } } @@ -142,8 +149,11 @@ class SpineJson(private val filePath: Path) { animationNames.add(animationName) } + override fun toString(): String { + return json.toJsonString(prettyPrint = true) + } + fun save() { - var string = json.toJsonString(prettyPrint = true) - Files.write(filePath, listOf(string), StandardCharsets.UTF_8) + Files.write(filePath, listOf(toString()), StandardCharsets.UTF_8) } } \ No newline at end of file diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/classLocation.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/classLocation.kt similarity index 100% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/classLocation.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/classLocation.kt diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/main.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/main.kt similarity index 100% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/main.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/main.kt diff --git a/extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/tools.kt b/extras/EsotericSoftwareSpine/src/main/kotlin/tools.kt similarity index 100% rename from extras/EsotericSoftwareSpine/src/main/kotlin/com/rhubarb_lip_sync/rhubarb_for_spine/tools.kt rename to extras/EsotericSoftwareSpine/src/main/kotlin/tools.kt diff --git a/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/audio/.gitkeep b/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/audio/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/images/.gitkeep b/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/matt-3.7-essential.json b/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/matt-3.7-essential.json new file mode 100644 index 0000000..a69b47c --- /dev/null +++ b/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/matt-3.7-essential.json @@ -0,0 +1,121 @@ +{ +"skeleton": { "hash": "voNIQumqp3+UQAl32SwHzLMEDaI", "spine": "3.7.04-beta", "width": 795, "height": 1249.62 }, +"bones": [ + { "name": "root" }, + { "name": "torso", "parent": "root", "length": 394.49, "rotation": 90, "y": 100 }, + { "name": "head", "parent": "torso", "length": 515.83, "x": 390 }, + { "name": "legs", "parent": "torso", "length": 79.85, "rotation": 180, "x": -6 } +], +"slots": [ + { "name": "legs", "bone": "legs", "attachment": "legs" }, + { "name": "torso", "bone": "torso", "attachment": "torso" }, + { "name": "head", "bone": "head", "attachment": "head" }, + { "name": "mouth", "bone": "head", "attachment": "mouth_d" } +], +"skins": { + "default": { + "head": { + "head": { "x": 305.19, "y": -3.37, "rotation": -90, "width": 795, "height": 908 } + }, + "legs": { + "legs": { "x": 20.93, "y": 9.45, "rotation": 90, "width": 602, "height": 147 } + }, + "mouth": { + "mouth_a": { "x": -53.21, "y": -2.6, "rotation": -90, "width": 118, "height": 27 }, + "mouth_b": { "x": -38.68, "y": -0.88, "rotation": -90, "width": 170, "height": 59 }, + "mouth_c": { "x": -45.57, "y": -2.21, "rotation": -90, "width": 145, "height": 71 }, + "mouth_d": { "x": -50.58, "y": -16.55, "rotation": -90, "width": 122, "height": 91 }, + "mouth_e": { "x": -47.51, "y": 1.69, "rotation": -90, "width": 105, "height": 73 }, + "mouth_f": { "x": -42.7, "y": -1.9, "rotation": -90, "width": 55, "height": 54 }, + "mouth_g": { "x": -42.77, "y": 2.56, "rotation": -90, "width": 141, "height": 37 }, + "mouth_h": { "x": -44.53, "y": 1.07, "rotation": -90, "width": 141, "height": 71 } + }, + "torso": { + "torso": { "x": 185.9, "y": 0.39, "rotation": -90, "width": 741, "height": 449 } + } + } +}, +"events": { + "doornail": { + "string": "Marley was dead: to begin with. There is no doubt whatever about that. The register of his burial was signed by the clergyman, the clerk, the undertaker, and the chief mourner. Scrooge signed it: and Scrooge's name was good upon 'Change, for anything he chose to put his hand to. Old Marley was as dead as a door-nail.Mind! I don't mean to say that I know, of my own knowledge, what there is particularly dead about a door-nail. I might have been inclined, myself, to regard a coffin-nail as the deadest piece of ironmongery in the trade. But the wisdom of our ancestors is in the simile; and my unhallowed hands shall not disturb it, or the Country's done for. You will therefore permit me to repeat, emphatically, that Marley was as dead as a door-nail.", + "audio": "doornail.wav" + }, + "hi": { "audio": "hi.wav" } +}, +"animations": { + "say_test": { + "slots": { + "mouth": { + "attachment": [ + { "time": 0, "name": "mouth_a" }, + { "time": 0.1, "name": "mouth_b" }, + { "time": 0.2, "name": "mouth_c" }, + { "time": 0.2667, "name": "mouth_d" }, + { "time": 0.3667, "name": "mouth_c" }, + { "time": 0.4333, "name": "mouth_a" }, + { "time": 0.5333, "name": "mouth_e" }, + { "time": 0.6, "name": "mouth_f" }, + { "time": 0.7, "name": "mouth_e" }, + { "time": 0.8, "name": "mouth_g" }, + { "time": 0.8667, "name": "mouth_c" }, + { "time": 0.9667, "name": "mouth_h" }, + { "time": 1.0667, "name": "mouth_a" } + ] + } + }, + "events": [ + { "time": 0.8667, "name": "doornail", "string": "" } + ] + }, + "shake_head": { + "bones": { + "head": { + "rotate": [ + { + "time": 0, + "angle": 0, + "curve": [ 0.25, 0, 0.75, 1 ] + }, + { + "time": 0.1667, + "angle": 10.02, + "curve": [ 0.25, 0, 0.75, 1 ] + }, + { + "time": 0.5, + "angle": -9.37, + "curve": [ 0.25, 0, 0.75, 1 ] + }, + { + "time": 0.8333, + "angle": 10.39, + "curve": [ 0.574, 0, 0.666, 1 ] + }, + { "time": 1.5, "angle": 0 } + ] + } + } + }, + "walk": { + "bones": { + "torso": { + "translate": [ + { + "time": 0, + "x": 0, + "y": 0, + "curve": [ 0, 0.5, 0.75, 1 ] + }, + { + "time": 0.1333, + "x": 0, + "y": 30, + "curve": [ 0.25, 0, 1, 0.49 ] + }, + { "time": 0.2667, "x": 0, "y": 0 } + ] + } + } + } +} +} \ No newline at end of file diff --git a/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/matt-3.7.json b/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/matt-3.7.json new file mode 100644 index 0000000..cd600ac --- /dev/null +++ b/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/matt-3.7.json @@ -0,0 +1,95 @@ +{ +"skeleton": { "hash": "nWA5IiZBBeDJ6tKyTnjtIfu1GXE", "spine": "3.7.94", "width": 795, "height": 1249.62, "images": "./images/", "audio": "./audio/" }, +"bones": [ + { "name": "root" }, + { "name": "torso", "parent": "root", "length": 394.49, "rotation": 90, "y": 100 }, + { "name": "head", "parent": "torso", "length": 515.83, "x": 390 }, + { "name": "legs", "parent": "torso", "length": 79.85, "rotation": 180, "x": -6 } +], +"slots": [ + { "name": "legs", "bone": "legs", "attachment": "legs" }, + { "name": "torso", "bone": "torso", "attachment": "torso" }, + { "name": "head", "bone": "head", "attachment": "head" }, + { "name": "mouth", "bone": "head", "attachment": "mouth_c" } +], +"skins": { + "default": { + "head": { + "head": { "x": 305.19, "y": -3.37, "rotation": -90, "width": 795, "height": 908 } + }, + "legs": { + "legs": { "x": 20.93, "y": 9.45, "rotation": 90, "width": 602, "height": 147 } + }, + "mouth": { + "mouth_a": { "x": -53.21, "y": -2.6, "rotation": -90, "width": 118, "height": 27 }, + "mouth_b": { "x": -38.68, "y": -0.88, "rotation": -90, "width": 170, "height": 59 }, + "mouth_c": { "x": -45.57, "y": -2.21, "rotation": -90, "width": 145, "height": 71 }, + "mouth_d": { "x": -50.58, "y": -16.55, "rotation": -90, "width": 122, "height": 91 }, + "mouth_e": { "x": -47.51, "y": 1.69, "rotation": -90, "width": 105, "height": 73 }, + "mouth_f": { "x": -42.7, "y": -1.9, "rotation": -90, "width": 55, "height": 54 }, + "mouth_g": { "x": -42.77, "y": 2.56, "rotation": -90, "width": 141, "height": 37 }, + "mouth_h": { "x": -44.53, "y": 1.07, "rotation": -90, "width": 141, "height": 71 } + }, + "torso": { + "torso": { "x": 185.9, "y": 0.39, "rotation": -90, "width": 741, "height": 449 } + } + } +}, +"events": { + "1-have-you-heard": { "audio": "1-have-you-heard.wav" }, + "2-it's-a-tool": { "audio": "2-it's-a-tool.wav" }, + "3-and-now-you-can": { "audio": "3-and-now-you-can.wav" } +}, +"animations": { + "shake_head": { + "bones": { + "head": { + "rotate": [ + { + "time": 0, + "angle": 0, + "curve": [ 0.25, 0, 0.75, 1 ] + }, + { + "time": 0.1667, + "angle": 10.02, + "curve": [ 0.25, 0, 0.75, 1 ] + }, + { + "time": 0.5, + "angle": -9.37, + "curve": [ 0.25, 0, 0.75, 1 ] + }, + { + "time": 0.8333, + "angle": 10.39, + "curve": [ 0.574, 0, 0.666, 1 ] + }, + { "time": 1.5, "angle": 0 } + ] + } + } + }, + "walk": { + "bones": { + "torso": { + "translate": [ + { + "time": 0, + "x": 0, + "y": 0, + "curve": [ 0, 0.5, 0.75, 1 ] + }, + { + "time": 0.1333, + "x": 0, + "y": 30, + "curve": [ 0.25, 0, 1, 0.49 ] + }, + { "time": 0.2667, "x": 0, "y": 0 } + ] + } + } + } +} +} \ No newline at end of file diff --git a/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/matt-3.8-essential.json b/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/matt-3.8-essential.json new file mode 100644 index 0000000..5c63e75 --- /dev/null +++ b/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/matt-3.8-essential.json @@ -0,0 +1,72 @@ +{ +"skeleton": { "hash": "rSEJPpMBeapC2jv56cUew+IkQd0", "spine": "3.8.42-beta", "x": -394.13, "y": -0.43, "width": 795, "height": 1249.62 }, +"bones": [ + { "name": "root" }, + { "name": "torso", "parent": "root", "length": 394.49, "rotation": 90, "y": 100 }, + { "name": "head", "parent": "torso", "length": 515.83, "x": 390 }, + { "name": "legs", "parent": "torso", "length": 79.85, "rotation": 180, "x": -6 } +], +"slots": [ + { "name": "legs", "bone": "legs", "attachment": "legs" }, + { "name": "torso", "bone": "torso", "attachment": "torso" }, + { "name": "head", "bone": "head", "attachment": "head" }, + { "name": "mouth", "bone": "head", "attachment": "mouth_c" } +], +"skins": [ + { + "name": "default", + "attachments": { + "mouth": { + "mouth_a": { "x": -53.21, "y": -2.6, "rotation": -90, "width": 118, "height": 27 }, + "mouth_b": { "x": -38.68, "y": -0.88, "rotation": -90, "width": 170, "height": 59 }, + "mouth_c": { "x": -45.57, "y": -2.21, "rotation": -90, "width": 145, "height": 71 }, + "mouth_d": { "x": -50.58, "y": -16.55, "rotation": -90, "width": 122, "height": 91 }, + "mouth_e": { "x": -47.51, "y": 1.69, "rotation": -90, "width": 105, "height": 73 }, + "mouth_f": { "x": -42.7, "y": -1.9, "rotation": -90, "width": 55, "height": 54 }, + "mouth_g": { "x": -42.77, "y": 2.56, "rotation": -90, "width": 141, "height": 37 }, + "mouth_h": { "x": -44.53, "y": 1.07, "rotation": -90, "width": 141, "height": 71 } + }, + "head": { + "head": { "x": 305.19, "y": -3.37, "rotation": -90, "width": 795, "height": 908 } + }, + "legs": { + "legs": { "x": 20.93, "y": 9.45, "rotation": 90, "width": 602, "height": 147 } + }, + "torso": { + "torso": { "x": 185.9, "y": 0.39, "rotation": -90, "width": 741, "height": 449 } + } + } + } +], +"events": { + "1-have-you-heard": { "audio": "1-have-you-heard.wav" }, + "2-it's-a-tool": { "audio": "2-it's-a-tool.wav" }, + "3-and-now-you-can": { "audio": "3-and-now-you-can.wav" } +}, +"animations": { + "shake_head": { + "bones": { + "head": { + "rotate": [ + { "curve": 0.25, "c3": 0.75 }, + { "time": 0.1667, "angle": 10.02, "curve": 0.25, "c3": 0.75 }, + { "time": 0.5, "angle": -9.37, "curve": 0.25, "c3": 0.75 }, + { "time": 0.8333, "angle": 10.39, "curve": 0.574, "c3": 0.666 }, + { "time": 1.5 } + ] + } + } + }, + "walk": { + "bones": { + "torso": { + "translate": [ + { "curve": 0, "c2": 0.5, "c3": 0.75 }, + { "time": 0.1333, "y": 30, "curve": 0.25, "c4": 0.49 }, + { "time": 0.2667 } + ] + } + } + } +} +} \ No newline at end of file diff --git a/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/matt-3.8.json b/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/matt-3.8.json new file mode 100644 index 0000000..583d70b --- /dev/null +++ b/extras/EsotericSoftwareSpine/src/test/data/jsonFiles/matt-3.8.json @@ -0,0 +1,81 @@ +{ +"skeleton": { + "hash": "sH1atSHvppLIr/A6E6H7PXWiU4s", + "spine": "3.8.42-beta", + "x": -394.13, + "y": -0.43, + "width": 795, + "height": 1249.62, + "images": "./images/", + "audio": "./audio/" +}, +"bones": [ + { "name": "root" }, + { "name": "torso", "parent": "root", "length": 394.49, "rotation": 90, "y": 100 }, + { "name": "head", "parent": "torso", "length": 515.83, "x": 390 }, + { "name": "legs", "parent": "torso", "length": 79.85, "rotation": 180, "x": -6 } +], +"slots": [ + { "name": "legs", "bone": "legs", "attachment": "legs" }, + { "name": "torso", "bone": "torso", "attachment": "torso" }, + { "name": "head", "bone": "head", "attachment": "head" }, + { "name": "mouth", "bone": "head", "attachment": "mouth_c" } +], +"skins": [ + { + "name": "default", + "attachments": { + "mouth": { + "mouth_a": { "x": -53.21, "y": -2.6, "rotation": -90, "width": 118, "height": 27 }, + "mouth_b": { "x": -38.68, "y": -0.88, "rotation": -90, "width": 170, "height": 59 }, + "mouth_c": { "x": -45.57, "y": -2.21, "rotation": -90, "width": 145, "height": 71 }, + "mouth_d": { "x": -50.58, "y": -16.55, "rotation": -90, "width": 122, "height": 91 }, + "mouth_e": { "x": -47.51, "y": 1.69, "rotation": -90, "width": 105, "height": 73 }, + "mouth_f": { "x": -42.7, "y": -1.9, "rotation": -90, "width": 55, "height": 54 }, + "mouth_g": { "x": -42.77, "y": 2.56, "rotation": -90, "width": 141, "height": 37 }, + "mouth_h": { "x": -44.53, "y": 1.07, "rotation": -90, "width": 141, "height": 71 } + }, + "head": { + "head": { "x": 305.19, "y": -3.37, "rotation": -90, "width": 795, "height": 908 } + }, + "legs": { + "legs": { "x": 20.93, "y": 9.45, "rotation": 90, "width": 602, "height": 147 } + }, + "torso": { + "torso": { "x": 185.9, "y": 0.39, "rotation": -90, "width": 741, "height": 449 } + } + } + } +], +"events": { + "1-have-you-heard": { "audio": "1-have-you-heard.wav" }, + "2-it's-a-tool": { "audio": "2-it's-a-tool.wav" }, + "3-and-now-you-can": { "audio": "3-and-now-you-can.wav" } +}, +"animations": { + "shake_head": { + "bones": { + "head": { + "rotate": [ + { "curve": 0.25, "c3": 0.75 }, + { "time": 0.1667, "angle": 10.02, "curve": 0.25, "c3": 0.75 }, + { "time": 0.5, "angle": -9.37, "curve": 0.25, "c3": 0.75 }, + { "time": 0.8333, "angle": 10.39, "curve": 0.574, "c3": 0.666 }, + { "time": 1.5 } + ] + } + } + }, + "walk": { + "bones": { + "torso": { + "translate": [ + { "curve": 0, "c2": 0.5, "c3": 0.75 }, + { "time": 0.1333, "y": 30, "curve": 0.25, "c4": 0.49 }, + { "time": 0.2667 } + ] + } + } + } +} +} \ No newline at end of file diff --git a/extras/EsotericSoftwareSpine/src/test/kotlin/SpineJsonTest.kt b/extras/EsotericSoftwareSpine/src/test/kotlin/SpineJsonTest.kt new file mode 100644 index 0000000..166054f --- /dev/null +++ b/extras/EsotericSoftwareSpine/src/test/kotlin/SpineJsonTest.kt @@ -0,0 +1,69 @@ +package com.rhubarb_lip_sync.rhubarb_for_spine + +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import java.nio.file.Paths +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.catchThrowable + +class SpineJsonTest { + @Nested + inner class `file format 3_7` { + @Test + fun `correctly reads valid file`() { + val path = Paths.get("src/test/data/jsonFiles/matt-3.7.json").toAbsolutePath() + val spine = SpineJson(path) + + assertThat(spine.audioDirectoryPath) + .isEqualTo(Paths.get("src/test/data/jsonFiles/audio").toAbsolutePath()) + assertThat(spine.frameRate).isEqualTo(30.0) + assertThat(spine.slots).containsExactly("legs", "torso", "head", "mouth") + assertThat(spine.guessMouthSlot()).isEqualTo("mouth") + assertThat(spine.audioEvents).containsExactly( + SpineJson.AudioEvent("1-have-you-heard", "1-have-you-heard.wav", null), + SpineJson.AudioEvent("2-it's-a-tool", "2-it's-a-tool.wav", null), + SpineJson.AudioEvent("3-and-now-you-can", "3-and-now-you-can.wav", null) + ) + assertThat(spine.getSlotAttachmentNames("mouth")).isEqualTo(('a'..'h').map{ "mouth_$it" }) + assertThat(spine.animationNames).containsExactly("shake_head", "walk") + } + + @Test + fun `throws on file without nonessential data`() { + val path = Paths.get("src/test/data/jsonFiles/matt-3.7-essential.json").toAbsolutePath() + val throwable = catchThrowable { SpineJson(path) } + assertThat(throwable) + .hasMessage("JSON file is incomplete: Images path is missing. Make sure to check 'Nonessential data' when exporting.") + } + } + + @Nested + inner class `file format 3_8` { + @Test + fun `correctly reads valid file`() { + val path = Paths.get("src/test/data/jsonFiles/matt-3.8.json").toAbsolutePath() + val spine = SpineJson(path) + + assertThat(spine.audioDirectoryPath) + .isEqualTo(Paths.get("src/test/data/jsonFiles/audio").toAbsolutePath()) + assertThat(spine.frameRate).isEqualTo(30.0) + assertThat(spine.slots).containsExactly("legs", "torso", "head", "mouth") + assertThat(spine.guessMouthSlot()).isEqualTo("mouth") + assertThat(spine.audioEvents).containsExactly( + SpineJson.AudioEvent("1-have-you-heard", "1-have-you-heard.wav", null), + SpineJson.AudioEvent("2-it's-a-tool", "2-it's-a-tool.wav", null), + SpineJson.AudioEvent("3-and-now-you-can", "3-and-now-you-can.wav", null) + ) + assertThat(spine.getSlotAttachmentNames("mouth")).isEqualTo(('a'..'h').map{ "mouth_$it" }) + assertThat(spine.animationNames).containsExactly("shake_head", "walk") + } + + @Test + fun `throws on file without nonessential data`() { + val path = Paths.get("src/test/data/jsonFiles/matt-3.8-essential.json").toAbsolutePath() + val throwable = catchThrowable { SpineJson(path) } + assertThat(throwable) + .hasMessage("JSON file is incomplete: Images path is missing. Make sure to check 'Nonessential data' when exporting.") + } + } +} diff --git a/extras/EsotericSoftwareSpine/src/test/resources/junit-platform.properties b/extras/EsotericSoftwareSpine/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..d265fd8 --- /dev/null +++ b/extras/EsotericSoftwareSpine/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.testinstance.lifecycle.default = per_class