Merge remote-tracking branch 'remotes/origin/bugfix/74-spine-beta-support' into develop
This commit is contained in:
commit
e57fc97811
|
@ -3,6 +3,7 @@
|
||||||
## Unreleased
|
## 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** 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.
|
* **Improved** animation rule for OW sound: animating it as E-F rather than F.
|
||||||
|
|
||||||
## Version 1.9.1
|
## Version 1.9.1
|
||||||
|
|
|
@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.2)
|
||||||
|
|
||||||
add_custom_target(
|
add_custom_target(
|
||||||
rhubarbForSpine ALL
|
rhubarbForSpine ALL
|
||||||
"./gradlew" "jar"
|
"./gradlew" "shadowJar"
|
||||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
COMMENT "Building Rhubarb for Spine through Gradle."
|
COMMENT "Building Rhubarb for Spine through Gradle."
|
||||||
)
|
)
|
||||||
|
|
|
@ -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) } }
|
|
||||||
}
|
|
|
@ -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<KotlinCompile> {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
|
@ -1,5 +1,5 @@
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
rootProject.name = 'rhubarb-for-spine'
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
rootProject.name = "rhubarb-for-spine"
|
|
@ -6,6 +6,7 @@ import javafx.beans.property.SimpleListProperty
|
||||||
import javafx.beans.property.SimpleObjectProperty
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
import javafx.beans.property.SimpleStringProperty
|
import javafx.beans.property.SimpleStringProperty
|
||||||
import javafx.collections.ObservableList
|
import javafx.collections.ObservableList
|
||||||
|
import tornadofx.asObservable
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import tornadofx.getValue
|
import tornadofx.getValue
|
||||||
import tornadofx.observable
|
import tornadofx.observable
|
||||||
|
@ -61,7 +62,7 @@ class AnimationFileModel(val parentModel: MainModel, animationFilePath: Path, pr
|
||||||
audioFileModel = AudioFileModel(event, this, executor, reportResult)
|
audioFileModel = AudioFileModel(event, this, executor, reportResult)
|
||||||
return@map audioFileModel
|
return@map audioFileModel
|
||||||
}
|
}
|
||||||
.observable()
|
.asObservable()
|
||||||
)
|
)
|
||||||
val audioFileModels: ObservableList<AudioFileModel> by audioFileModelsProperty
|
val audioFileModels: ObservableList<AudioFileModel> by audioFileModelsProperty
|
||||||
|
|
||||||
|
@ -97,7 +98,7 @@ class AnimationFileModel(val parentModel: MainModel, animationFilePath: Path, pr
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
slots = spineJson.slots.observable()
|
slots = spineJson.slots.asObservable()
|
||||||
mouthSlot = spineJson.guessMouthSlot()
|
mouthSlot = spineJson.guessMouthSlot()
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,13 +115,13 @@ class MainView : View() {
|
||||||
placeholder = Label("There are no events with associated audio files.")
|
placeholder = Label("There are no events with associated audio files.")
|
||||||
columnResizePolicy = SmartResize.POLICY
|
columnResizePolicy = SmartResize.POLICY
|
||||||
column("Event", AudioFileModel::eventNameProperty)
|
column("Event", AudioFileModel::eventNameProperty)
|
||||||
.weigthedWidth(1.0)
|
.weightedWidth(1.0)
|
||||||
column("Animation name", AudioFileModel::animationNameProperty)
|
column("Animation name", AudioFileModel::animationNameProperty)
|
||||||
.weigthedWidth(1.0)
|
.weightedWidth(1.0)
|
||||||
column("Audio file", AudioFileModel::displayFilePathProperty)
|
column("Audio file", AudioFileModel::displayFilePathProperty)
|
||||||
.weigthedWidth(1.0)
|
.weightedWidth(1.0)
|
||||||
column("Dialog", AudioFileModel::dialogProperty).apply {
|
column("Dialog", AudioFileModel::dialogProperty).apply {
|
||||||
weigthedWidth(3.0)
|
weightedWidth(3.0)
|
||||||
// Make dialog column wrap
|
// Make dialog column wrap
|
||||||
setCellFactory { tableColumn ->
|
setCellFactory { tableColumn ->
|
||||||
return@setCellFactory TableCell<AudioFileModel, String>().also { cell ->
|
return@setCellFactory TableCell<AudioFileModel, String>().also { cell ->
|
||||||
|
@ -138,7 +138,7 @@ class MainView : View() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
column("Status", AudioFileModel::audioFileStateProperty).apply {
|
column("Status", AudioFileModel::audioFileStateProperty).apply {
|
||||||
weigthedWidth(1.0)
|
weightedWidth(1.0)
|
||||||
setCellFactory {
|
setCellFactory {
|
||||||
return@setCellFactory object : TableCell<AudioFileModel, AudioFileState>() {
|
return@setCellFactory object : TableCell<AudioFileModel, AudioFileState>() {
|
||||||
override fun updateItem(state: AudioFileState?, empty: Boolean) {
|
override fun updateItem(state: AudioFileState?, empty: Boolean) {
|
||||||
|
@ -176,7 +176,7 @@ class MainView : View() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
column("", AudioFileModel::actionLabelProperty).apply {
|
column("", AudioFileModel::actionLabelProperty).apply {
|
||||||
weigthedWidth(1.0)
|
weightedWidth(1.0)
|
||||||
// Show button
|
// Show button
|
||||||
setCellFactory {
|
setCellFactory {
|
||||||
return@setCellFactory object : TableCell<AudioFileModel, String>() {
|
return@setCellFactory object : TableCell<AudioFileModel, String>() {
|
|
@ -1,9 +1,6 @@
|
||||||
package com.rhubarb_lip_sync.rhubarb_for_spine
|
package com.rhubarb_lip_sync.rhubarb_for_spine
|
||||||
|
|
||||||
import com.beust.klaxon.JsonObject
|
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 com.beust.klaxon.Parser as JsonParser
|
||||||
import org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS
|
import org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
@ -78,7 +75,7 @@ class RhubarbTask(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val jsonParser = JsonParser()
|
private val jsonParser = JsonParser.default()
|
||||||
private fun parseJsonObject(jsonString: String): JsonObject {
|
private fun parseJsonObject(jsonString: String): JsonObject {
|
||||||
return jsonParser.parse(StringReader(jsonString)) as JsonObject
|
return jsonParser.parse(StringReader(jsonString)) as JsonObject
|
||||||
}
|
}
|
|
@ -17,13 +17,18 @@ class SpineJson(private val filePath: Path) {
|
||||||
throw EndUserException("File '$filePath' does not exist.")
|
throw EndUserException("File '$filePath' does not exist.")
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
json = Parser().parse(filePath.toString()) as JsonObject
|
json = Parser.default().parse(filePath.toString()) as JsonObject
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw EndUserException("Wrong file format. This is not a valid JSON file.")
|
throw EndUserException("Wrong file format. This is not a valid JSON file.")
|
||||||
}
|
}
|
||||||
skeleton = json.obj("skeleton") ?: throw EndUserException("JSON file is corrupted.")
|
skeleton = json.obj("skeleton") ?: throw EndUserException("JSON file is corrupted.")
|
||||||
val skins = json.obj("skins") ?: throw EndUserException("JSON file doesn't contain skins.")
|
val skins = json["skins"] ?: throw EndUserException("JSON file doesn't contain skins.")
|
||||||
defaultSkin = skins.obj("default") ?: throw EndUserException("JSON file doesn't have a default skin.")
|
defaultSkin = when (skins) {
|
||||||
|
is JsonObject -> skins.obj("default")
|
||||||
|
is JsonArray<*> -> (skins as JsonArray<JsonObject>).find { it.string("name") == "default" }
|
||||||
|
else -> null
|
||||||
|
} ?: throw EndUserException("JSON file doesn't have a default skin.")
|
||||||
|
|
||||||
validateProperties()
|
validateProperties()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +40,7 @@ class SpineJson(private val filePath: Path) {
|
||||||
private val imagesDirectoryPath: Path get() {
|
private val imagesDirectoryPath: Path get() {
|
||||||
val relativeImagesDirectory = skeleton.string("images")
|
val relativeImagesDirectory = skeleton.string("images")
|
||||||
?: throw EndUserException("JSON file is incomplete: Images path is missing."
|
?: 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()
|
val imagesDirectoryPath = fileDirectoryPath.resolve(relativeImagesDirectory).normalize()
|
||||||
if (!Files.exists(imagesDirectoryPath)) {
|
if (!Files.exists(imagesDirectoryPath)) {
|
||||||
|
@ -49,7 +54,7 @@ class SpineJson(private val filePath: Path) {
|
||||||
val audioDirectoryPath: Path get() {
|
val audioDirectoryPath: Path get() {
|
||||||
val relativeAudioDirectory = skeleton.string("audio")
|
val relativeAudioDirectory = skeleton.string("audio")
|
||||||
?: throw EndUserException("JSON file is incomplete: Audio path is missing."
|
?: 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()
|
val audioDirectoryPath = fileDirectoryPath.resolve(relativeAudioDirectory).normalize()
|
||||||
if (!Files.exists(audioDirectoryPath)) {
|
if (!Files.exists(audioDirectoryPath)) {
|
||||||
|
@ -91,7 +96,9 @@ class SpineJson(private val filePath: Path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSlotAttachmentNames(slotName: String): List<String> {
|
fun getSlotAttachmentNames(slotName: String): List<String> {
|
||||||
val attachments = defaultSkin.obj(slotName) ?: JsonObject()
|
val attachments = defaultSkin.obj(slotName)
|
||||||
|
?: defaultSkin.obj("attachments")?.obj(slotName)
|
||||||
|
?: JsonObject()
|
||||||
return attachments.map { it.key }
|
return attachments.map { it.key }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,8 +149,11 @@ class SpineJson(private val filePath: Path) {
|
||||||
animationNames.add(animationName)
|
animationNames.add(animationName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return json.toJsonString(prettyPrint = true)
|
||||||
|
}
|
||||||
|
|
||||||
fun save() {
|
fun save() {
|
||||||
var string = json.toJsonString(prettyPrint = true)
|
Files.write(filePath, listOf(toString()), StandardCharsets.UTF_8)
|
||||||
Files.write(filePath, listOf(string), StandardCharsets.UTF_8)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
junit.jupiter.testinstance.lifecycle.default = per_class
|
Loading…
Reference in New Issue