MotionCommands.kt
src/jvmMain/kotlin/afield/animation/mocap/MotionCommands.kt · 80 lines
@file:afield.runtime.completion.CompletionCategory("Animation")
package afield.animation.mocap
import afield.math.linalg.Vec3
import org.json.JSONObject
import kotlin.math.min
enum class MotionOperation
enum class MotionOperation { PROMPT, ROOT_PLAN, JOINT_TARGETS, ATTACH, UPDATE_ATTACHMENT, DETACH, IMPULSE, ADVANCE, RESET }
data class MotionCapabilities(val backend: String, val operations: Set<MotionOperation>, val details: JSONObject)
fun require(operation: MotionOperation)
fun require(operation: MotionOperation) {
require(operation in operations) { "$backend does not support $operation. Supported: $operations" }
}
companion object
fun fromJson(j: JSONObject)
fun fromJson(j: JSONObject) = MotionCapabilities(j.getString("backend"),
j.getJSONArray("operations").strings().map { MotionOperation.valueOf(it) }.toSet(), j)
}
}
data class JointTarget(val joint: String, val time: Double, val position: List<Double>, val orientation: List<Double>? = null)
Kinematic conditioning, in world coordinates; orientation is optional and separately recorded.
init
init {
require(joint.isNotBlank() && time.isFinite() && time >= 0 && position.size == 3 && position.all(Double::isFinite))
require(orientation == null || orientation.size == 4 && orientation.all(Double::isFinite) &&
kotlin.math.abs(orientation.sumOf { it * it } - 1) < .002)
}
fun json()
fun json() = JSONObject().put("joint", joint).put("time", time).put("position", position)
.put("orientation", orientation ?: JSONObject.NULL)
}
data class MotionAttachment
Force capped spring/damper. A break requires a sustained criterion; saturation alone never detaches.
data class MotionAttachment(
val id: String, val body: String, val localPoint: List<Double>, val anchor: List<Double>,
val stiffness: Double = 30.0, val damping: Double = 5.0, val maximumForce: Double = 50.0,
val engagementSeconds: Double = .2, val breakForce: Double? = null, val breakError: Double? = null,
val breakDuration: Double = .1,
val anchorVelocity: List<Double> = listOf(0.0, 0.0, 0.0),
) {
init
init {
require(id.isNotBlank() && body.isNotBlank())
require(localPoint.size == 3 && anchor.size == 3 && (localPoint + anchor).all(Double::isFinite))
require(anchorVelocity.size == 3 && anchorVelocity.all(Double::isFinite))
require(listOf(stiffness, damping, maximumForce, engagementSeconds, breakDuration).all { it.isFinite() && it >= 0 })
require(maximumForce > 0 && breakDuration > 0)
require(breakForce == null || breakForce.isFinite() && breakForce > 0)
require(breakError == null || breakError.isFinite() && breakError > 0)
}
fun json()
fun json() = JSONObject().put("id", id).put("body", body).put("localPoint", localPoint).put("anchor", anchor)
.put("stiffness", stiffness).put("damping", damping).put("maximumForce", maximumForce)
.put("engagementSeconds", engagementSeconds).put("breakForce", breakForce ?: JSONObject.NULL)
.put("anchorVelocity", anchorVelocity)
.put("breakError", breakError ?: JSONObject.NULL).put("breakDuration", breakDuration)
.put("mode", if (breakForce == null && breakError == null) "saturating" else "breakable")
}
data class PointWrench(val force: Vec3, val torque: Vec3, val velocity: Vec3, val rawMagnitude: Double, val powerWatts: Double)
data class PointWrench(val force: Vec3, val torque: Vec3, val velocity: Vec3, val rawMagnitude: Double, val powerWatts: Double)
fun pointTether(point: Vec3, centreOfMass: Vec3, comVelocity: Vec3, angularVelocity: Vec3,
Inspectable point-force math, independent of a simulator. Inputs are in one world frame, SI.
point is measured from the actual COM, not the display joint or rigid-body origin.
The simulator adapter must add this wrench to other forces and apply it for the documented step.
fun pointTether(point: Vec3, centreOfMass: Vec3, comVelocity: Vec3, angularVelocity: Vec3,
anchor: Vec3, anchorVelocity: Vec3 = Vec3(), stiffness: Double, damping: Double,
maximumForce: Double, engagement: Double = 1.0): PointWrench {
require(listOf(stiffness, damping, maximumForce, engagement).all(Double::isFinite))
require(stiffness >= 0 && damping >= 0 && maximumForce > 0 && engagement in 0.0..1.0)
listOf(point, centreOfMass, comVelocity, angularVelocity, anchor, anchorVelocity).forEach {
require(it.x.isFinite() && it.y.isFinite() && it.z.isFinite())
}
val radius = Vec3(point).sub(centreOfMass)
val velocity = Vec3(angularVelocity).cross(radius).add(comVelocity)
val force = Vec3(anchor).sub(point).mul(stiffness.toFloat())
.sub(Vec3(velocity).sub(anchorVelocity).mul(damping.toFloat())).mul(engagement.toFloat())
val magnitude = force.length().toDouble()
force.mul(min(1.0, maximumForce / magnitude.coerceAtLeast(1e-30)).toFloat())
return PointWrench(force, Vec3(radius).cross(force), velocity, magnitude, force.dot(velocity).toDouble())
}