adproperties now has transforms list; assembler can make transformed skeleton

new fun: LinearSearch(By)
This commit is contained in:
minjaesong
2019-01-06 22:43:50 +09:00
parent 32afb2e2e5
commit 344e4ebdab
8 changed files with 145 additions and 40 deletions

View File

@@ -1,5 +1,6 @@
package net.torvald.spriteassembler
import net.torvald.terrarum.linearSearchBy
import java.io.InputStream
import java.io.Reader
import java.util.*
@@ -18,19 +19,27 @@ data class Animation(val name: String, val delay: Float, val row: Int, val frame
override fun toString() = "$name delay: $delay, row: $row, frames: $frames, skeleton: ${skeleton.name}"
}
/** Later the 'translate' can be changed so that it represents affine transformation (Matrix2d) */
data class Transform(val joint: Joint, val translate: ADPropertyObject.Vector2i) {
override fun toString() = "$joint transform: $translate"
fun getTransformVector() = joint.position + translate
}
class ADProperties {
private val javaProp = Properties()
/** Every key is CAPITALISED */
private val propTable = HashMap<String, List<ADPropertyObject>>()
/** list of bodyparts used by all the skeletons */
/** list of bodyparts used by all the skeletons (HEAD, UPPER_TORSO, LOWER_TORSO) */
lateinit var bodyparts: List<String>; private set
lateinit var bodypartFiles: List<String>; private set
/** properties that are being used as skeletons */
/** properties that are being used as skeletons (SKELETON_STAND) */
lateinit var skeletons: HashMap<String, Skeleton>; private set
/** properties that are recognised as animations */
/** properties that are recognised as animations (ANIM_RUN, ANIM)IDLE) */
lateinit var animations: HashMap<String, Animation>; private set
/** an "animation frame" property (ANIM_RUN_1, ANIM_RUN_2) */
lateinit var transforms: HashMap<String, List<Transform>>; private set
private val reservedProps = listOf("SPRITESHEET", "EXTENSION")
private val animMustContain = listOf("DELAY", "ROW", "SKELETON")
@@ -40,6 +49,8 @@ class ADProperties {
private val animFrameSuffixRegex = Regex("""_[0-9]+""")
private val ALL_JOINT = Joint("ALL", ADPropertyObject.Vector2i(0, 0))
constructor(reader: Reader) {
javaProp.load(reader)
continueLoad()
@@ -59,13 +70,14 @@ class ADProperties {
}
// set reserved values for the animation: filename, extension
baseFilename = get("SPRITESHEET")!![0].variable
extension = get("EXTENSION")!![0].variable
baseFilename = get("SPRITESHEET")[0].variable
extension = get("EXTENSION")[0].variable
val bodyparts = HashSet<String>()
val skeletons = HashMap<String, Skeleton>()
val animations = HashMap<String, Animation>()
val animFrames = HashMap<String, Int>()
val transforms = HashMap<String, List<Transform>>()
// scan every props, write down anim frames for later use
propTable.keys.forEach {
if (animFrameSuffixRegex.containsMatchIn(it)) {
@@ -94,16 +106,16 @@ class ADProperties {
// if it is indeed anim, populate animations list
if (propsHashMap.containsKey("SKELETON")) {
val skeletonName = propsHashMap["SKELETON"] as String
val skeletonDef = get(skeletonName) ?: throw Error("Skeleton definition for $skeletonName not found")
val skeletonDef = get(skeletonName)
skeletons.put(skeletonName, Skeleton(skeletonName, skeletonDef.toJoints()))
animations.put(s, Animation(
skeletons[skeletonName] = Skeleton(skeletonName, skeletonDef.toJoints())
animations[s] = Animation(
s,
propsHashMap["DELAY"] as Float,
(propsHashMap["ROW"] as Float).toInt(),
animFrames[s]!!,
Skeleton(skeletonName, skeletonDef.toJoints())
))
)
}
}
@@ -114,13 +126,44 @@ class ADProperties {
}
}
// populate transforms
animations.forEach { t, u ->
for (fc in 1..u.frames) {
val frameName = "${t}_$fc"
val prop = get(frameName)
var emptyList = prop.size == 1 && prop[0].variable.isEmpty()
val transformList = if (!emptyList) {
List(prop.size) { index ->
val jointNameToSearch = prop[index].variable.toUpperCase()
val joint = if (jointNameToSearch == "ALL")
ALL_JOINT
else
u.skeleton.joints.linearSearchBy { it.name == jointNameToSearch }
?: throw NullPointerException("No such joint: $jointNameToSearch")
val translate = prop[index].input as ADPropertyObject.Vector2i
Transform(joint, translate)
}
}
else {
// to make real empty list
List(0) { Transform(ALL_JOINT, ADPropertyObject.Vector2i(0, 0)) }
}
transforms[frameName] = transformList
}
}
this.bodyparts = bodyparts.toList().sorted()
this.skeletons = skeletons
this.animations = animations
this.bodypartFiles = this.bodyparts.map { toFilename(it) }
this.transforms = transforms
}
operator fun get(identifier: String) = propTable[identifier.toUpperCase()]
operator fun get(identifier: String) = propTable[identifier.toUpperCase()]!!
val keys
get() = propTable.keys
fun containsKey(key: String) = propTable.containsKey(key)
@@ -129,14 +172,14 @@ class ADProperties {
fun toFilename(partName: String) =
"${this.baseFilename}${partName.toLowerCase()}${this.extension}"
fun getAnimByFrameName(frameName: String): Animation {
return animations[getAnimNameFromFrame(frameName)]!!
}
fun getAnimByFrameName(frameName: String) = animations[getAnimNameFromFrame(frameName)]!!
fun getSkeleton(name: String) = skeletons[name]!!
fun getTransform(name: String) = transforms[name]!!
private fun getAnimNameFromFrame(s: String) = s.substring(0 until s.lastIndexOf('_'))
private fun List<ADPropertyObject>.toJoints() = List(this.size) {
Joint(this[it].variable, this[it].input!! as ADPropertyObject.Vector2i)
Joint(this[it].variable.toUpperCase(), this[it].input!! as ADPropertyObject.Vector2i)
}
}
@@ -167,7 +210,7 @@ class ADPropertyObject(propertyRaw: String) {
if (isADvariable(propertyRaw)) {
variable = propPair[0]
val inputStr = propPair[1]!!
val inputStr = propPair[1]
if (isADivec2(inputStr)) {
type = ADPropertyType.IVEC2
@@ -211,7 +254,9 @@ class ADPropertyObject(propertyRaw: String) {
}
data class Vector2i(var x: Int, var y: Int) {
override fun toString() = "$x, $y"
override fun toString() = "($x, $y)"
operator fun plus(other: Vector2i) = Vector2i(this.x + other.x, this.y + other.y)
}
enum class ADPropertyType {

View File

@@ -97,6 +97,14 @@ Remember that 'variables' are contained within 'properties'
|ROW|variable: float|which row the animation goes in the spritesheet|
|SKELETON|variable: string_pair|Which skeleton this animation uses
#### Transforms
Things like ```LEG_RIGHT -1,0``` within ```ANIM_RUN_3``` are called 'Transform'
|Name|Type|Meaning|
|---|---|---|
|ALL|variable: ivec2|Shifts (translates) everything by set value|
### Notes
* All indices are one-based
@@ -104,6 +112,7 @@ Remember that 'variables' are contained within 'properties'
## Operation
* Each line describes transformation
* Transformation are applied sequentially from left to right. In other words, their order matters. Be wary of the clipping that may occur!
* Transformation are applied sequentially from left to right. In other words, their order matters. Be wary of the clipping that may occur! (really?)
* The Field is an identifier the game code -- sprite assembler -- recognises.
* The Field of animation's name is the name the game code looks for. Example: ```this.setAnim("ANIM_RUN")```
* Coord system is Y-Up, meaning Y=0 is the bottommost position.

View File

@@ -1,6 +1,6 @@
package net.torvald.spriteassembler
import net.torvald.terrarum.AppLoader.printdbg
import net.torvald.terrarum.linearSearch
import java.awt.Image
import java.awt.Toolkit
import java.awt.image.BufferedImage
@@ -16,6 +16,7 @@ object AssembleFrameAWT {
operator fun invoke(properties: ADProperties, frameName: String, assembleConfig: AssembleConfig = AssembleConfig()) {
val theAnim = properties.getAnimByFrameName(frameName)
val skeleton = theAnim.skeleton.joints.reversed()
val transforms = properties.getTransform(frameName)
val bodyparts = Array<Image?>(skeleton.size) {
// if file does not exist, null it
val file = File("assets/" + properties.toFilename(skeleton[it].name))
@@ -33,9 +34,13 @@ object AssembleFrameAWT {
val canvas = BufferedImage(assembleConfig.fw, assembleConfig.fh, BufferedImage.TYPE_4BYTE_ABGR)
//printdbg(this, "==============================")
properties[frameName].forEach { printdbg(this, it) }
println("Frame name: $frameName")
transforms.forEach { println(it) }
println("==========================")
println("Transformed skeleton:")
AssembleFrameBase.makeTransformList(skeleton, transforms).forEach { (name, transform) ->
println("$name transformedOut: $transform")
}
}
}
@@ -46,4 +51,28 @@ object AssembleFrameAWT {
* @param ox Origin-X, leftmost point being zero
* @param oy Origin-Y, bottommost point being zero
*/
data class AssembleConfig(val fw: Int = 48, val fh: Int = 56, val ox: Int = 29, val oy: Int = 0)
data class AssembleConfig(val fw: Int = 48, val fh: Int = 56, val ox: Int = 29, val oy: Int = 0)
object AssembleFrameBase {
/**
* Returns joints list with tranform applied.
* @param skeleton list of joints
* @param transform ordered list of transforms should be applied. First come first serve.
* @return List of pairs that contains joint name on left, final transform value on right
*/
fun makeTransformList(joints: List<Joint>, transforms: List<Transform>): List<Pair<String, ADPropertyObject.Vector2i>> {
// make our mutable list
val transformOutput = ArrayList<Pair<String, ADPropertyObject.Vector2i>>()
joints.forEach {
transformOutput.add(it.name to it.position)
}
// process transform queue
transforms.forEach { transform ->
val jointToMoveIndex = transformOutput.linearSearch { it.first == transform.joint.name }!!
transformOutput[jointToMoveIndex] = transformOutput[jointToMoveIndex].first to transform.getTransformVector()
}
return transformOutput.toList()
}
}

View File

@@ -26,10 +26,11 @@ class SpriteAssemblerApp : JFrame() {
private val panelBodypartsList = JList<String>()
private val panelImageFilesList = JList<String>()
private val panelSkeletonsList = JList<String>()
private val panelTransformsList = JList<String>()
private val panelCode = JTextPane()
private val statBar = JTextArea("Null.")
private var adProperties: ADProperties? = null
private lateinit var adProperties: ADProperties
private val props = Properties()
private val lang = Properties()
@@ -93,12 +94,14 @@ class SpriteAssemblerApp : JFrame() {
panelBodypartsList.model = DefaultListModel()
panelImageFilesList.model = DefaultListModel()
panelSkeletonsList.model = DefaultListModel()
panelTransformsList.model = DefaultListModel()
val panelPartsList = JTabbedPane(JTabbedPane.TOP)
panelPartsList.add("Animations", JScrollPane(panelAnimationsList))
panelPartsList.add("Bodyparts", JScrollPane(panelBodypartsList))
panelPartsList.add("Images", JScrollPane(panelImageFilesList))
panelPartsList.add("Skeletons", JScrollPane(panelSkeletonsList))
panelPartsList.add("Transforms", JScrollPane(panelTransformsList))
val panelDataView = JSplitPane(JSplitPane.VERTICAL_SPLIT, JScrollPane(panelProperties), panelPartsList)
@@ -116,7 +119,7 @@ class SpriteAssemblerApp : JFrame() {
val propRoot = DefaultMutableTreeNode("Properties")
adProperties?.forEach { s, list ->
adProperties.forEach { s, list ->
// build tree node for the properties display
val propNode = DefaultMutableTreeNode(s)
propRoot.add(propNode)
@@ -133,21 +136,25 @@ class SpriteAssemblerApp : JFrame() {
panelSkeletonsList.model = DefaultListModel()
// populate animations view
adProperties!!.animations.forEach {
adProperties.animations.forEach {
(panelAnimationsList.model as DefaultListModel).addElement("${it.value}")
}
// populate bodyparts view
adProperties!!.bodyparts.forEach { partName ->
adProperties.bodyparts.forEach { partName ->
(panelBodypartsList.model as DefaultListModel).addElement(partName)
}
// populate image file list view
adProperties!!.bodypartFiles.forEach { partName ->
adProperties.bodypartFiles.forEach { partName ->
(panelImageFilesList.model as DefaultListModel).addElement(partName)
}
// populate skeletons view
adProperties!!.skeletons.forEach {
adProperties.skeletons.forEach {
(panelSkeletonsList.model as DefaultListModel).addElement("${it.value}")
}
// populate transforms view
adProperties.transforms.forEach {
(panelTransformsList.model as DefaultListModel).addElement("$it")
}
}
catch (fehler: Throwable) {
displayError("ERROR_PARSE_FAIL", fehler)

View File

@@ -21,12 +21,12 @@ SKELETON_STAND=HEADGEAR 0,32;HAIR_FORE 0,32;\
# skeleton_stand is used for testing purpose
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
ANIM_RUN_1=LEG_RIGHT 1,-1;LEG_LEFT -1,0
ANIM_RUN_2=ALL 0,-1;LEG_RIGHT 0,1;LEG_LEFT 0,-1
ANIM_RUN_3=LEG_RIGHT -1,0;LEG_LEFT 1,-1
ANIM_RUN_4=ALL 0,-1;LEG_RIGHT 0,-1;LEG_LEFT 0,1
ANIM_RUN_1=LEG_RIGHT 1,1;FOOT_RIGHT 1,1;LEG_LEFT -1,0;FOOT_LEFT -1,0
ANIM_RUN_2=ALL 0,1;LEG_RIGHT 0,-1;FOOT_RIGHT 0,-1;LEG_LEFT 0,1;FOOT_LEFT 0,1
ANIM_RUN_3=LEG_RIGHT -1,0;FOOT_RIGHT -1,0;LEG_LEFT 1,1;FOOT_LEFT 1,1
ANIM_RUN_4=ALL 0,1;LEG_RIGHT 0,1;LEG_LEFT 0,-1
ANIM_IDLE=DELAY 2;ROW 1;SKELETON SKELETON_STAND
ANIM_IDLE_1=
! ANIM_IDLE_1 will not make any transformation
ANIM_IDLE_2=UPPER_TORSO 0,-1
ANIM_IDLE_2=UPPER_TORSO 0,-1;HEAD 0,-1;ARM_REST_LEFT 0,-1;HAND_REST_LEFT 0,-1;ARM_REST_RIGHT 0,-1;HAND_REST_RIGHT 0,-1

View File

@@ -724,3 +724,18 @@ fun interpolateLinear(scale: Double, startValue: Double, endValue: Double): Doub
}
return (1.0 - scale) * startValue + scale * endValue
}
fun <T> List<T>.linearSearch(selector: (T) -> Boolean): Int? {
this.forEachIndexed { index, it ->
if (selector.invoke(it)) return index
}
return null
}
fun <T> List<T>.linearSearchBy(selector: (T) -> Boolean): T? {
this.forEach {
if (selector.invoke(it)) return it
}
return null
}

View File

@@ -32,15 +32,15 @@ class ADLParsingTest {
# skeleton_stand is used for testing purpose
ANIM_RUN=DELAY 0.15;ROW 2;SKELETON SKELETON_STAND
ANIM_RUN_1=LEG_RIGHT 1,-1;LEG_LEFT -1,0
ANIM_RUN_2=ALL 0,-1;LEG_RIGHT 0,1;LEG_LEFT 0,-1
ANIM_RUN_3=LEG_RIGHT -1,0;LEG_LEFT 1,-1
ANIM_RUN_4=ALL 0,-1;LEG_RIGHT 0,-1;LEG_LEFT 0,1
ANIM_RUN_1=LEG_RIGHT 1,1;FOOT_RIGHT 1,1;LEG_LEFT -1,0;FOOT_LEFT -1,0
ANIM_RUN_2=ALL 0,1;LEG_RIGHT 0,-1;FOOT_RIGHT 0,-1;LEG_LEFT 0,1;FOOT_LEFT 0,1
ANIM_RUN_3=LEG_RIGHT -1,0;FOOT_RIGHT -1,0;LEG_LEFT 1,1;FOOT_LEFT 1,1
ANIM_RUN_4=ALL 0,1;LEG_RIGHT 0,1;LEG_LEFT 0,-1
ANIM_IDLE=DELAY 2;ROW 1;SKELETON SKELETON_STAND
ANIM_IDLE_1=
! ANIM_IDLE_1 will not make any transformation
ANIM_IDLE_2=UPPER_TORSO 0,-1
ANIM_IDLE_2=UPPER_TORSO 0,-1;HEAD 0,-1;ARM_REST_LEFT 0,-1;HAND_REST_LEFT 0,-1;ARM_REST_RIGHT 0,-1;HAND_REST_RIGHT 0,-1
""".trimIndent()
operator fun invoke() {

View File

@@ -1,7 +1,7 @@
package net.torvald.terrarum.tests
import net.torvald.spriteassembler.ADProperties
import net.torvald.spriteassembler.AssembleFrameGdxPixmap
import net.torvald.spriteassembler.AssembleFrameAWT
import java.io.StringReader
/**
@@ -11,7 +11,7 @@ class SpriteAssemblerTest {
operator fun invoke() {
val properties = ADProperties(StringReader(ADLParsingTest().TEST_STR))
AssembleFrameGdxPixmap.invoke(properties, "ANIM_RUN_1")
AssembleFrameAWT.invoke(properties, "ANIM_RUN_1")
}
}