Fix ball on springs demo
This commit is contained in:
parent
4a5f5fab8c
commit
9f21a14f96
@ -4,8 +4,6 @@ import space.kscience.controls.constructor.*
|
|||||||
import space.kscience.controls.constructor.units.*
|
import space.kscience.controls.constructor.units.*
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,7 +15,6 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>(
|
|||||||
inertia: Double,
|
inertia: Double,
|
||||||
public val position: MutableDeviceState<NumericalValue<U>>,
|
public val position: MutableDeviceState<NumericalValue<U>>,
|
||||||
public val velocity: MutableDeviceState<NumericalValue<V>>,
|
public val velocity: MutableDeviceState<NumericalValue<V>>,
|
||||||
timerPrecision: Duration = 10.milliseconds,
|
|
||||||
) : ModelConstructor(context) {
|
) : ModelConstructor(context) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -27,7 +24,7 @@ public class Inertia<U : UnitsOfMeasurement, V : UnitsOfMeasurement>(
|
|||||||
|
|
||||||
private var currentForce = force.value
|
private var currentForce = force.value
|
||||||
|
|
||||||
private val movement = onTimer (timerPrecision) { prev, next ->
|
private val movement = onTimer { prev, next ->
|
||||||
val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS)
|
val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS)
|
||||||
|
|
||||||
// compute new value based on velocity and acceleration from the previous step
|
// compute new value based on velocity and acceleration from the previous step
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
package space.kscience.controls.constructor.models
|
||||||
|
|
||||||
|
import space.kscience.controls.constructor.*
|
||||||
|
import space.kscience.controls.constructor.units.*
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3D material point
|
||||||
|
*/
|
||||||
|
public class MaterialPoint(
|
||||||
|
context: Context,
|
||||||
|
force: DeviceState<XYZ<Newtons>>,
|
||||||
|
mass: NumericalValue<Kilograms>,
|
||||||
|
public val position: MutableDeviceState<XYZ<Meters>>,
|
||||||
|
public val velocity: MutableDeviceState<XYZ<MetersPerSecond>>,
|
||||||
|
) : ModelConstructor(context) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
registerState(position)
|
||||||
|
registerState(velocity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentForce = force.value
|
||||||
|
|
||||||
|
private val movement = onTimer(
|
||||||
|
reads = setOf(velocity, position),
|
||||||
|
writes = setOf(velocity, position)
|
||||||
|
) { prev, next ->
|
||||||
|
val dtSeconds = (next - prev).toDouble(DurationUnit.SECONDS)
|
||||||
|
|
||||||
|
// compute new value based on velocity and acceleration from the previous step
|
||||||
|
position.value += (velocity.value * dtSeconds).cast(Meters) +
|
||||||
|
(currentForce / mass.value * dtSeconds.pow(2) / 2).cast(Meters)
|
||||||
|
|
||||||
|
// compute new velocity based on acceleration on the previous step
|
||||||
|
velocity.value += (currentForce / mass.value * dtSeconds).cast(MetersPerSecond)
|
||||||
|
currentForce = force.value
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +0,0 @@
|
|||||||
package space.kscience.controls.constructor.models
|
|
||||||
|
|
||||||
import space.kscience.controls.constructor.units.NumericalValue
|
|
||||||
import space.kscience.controls.constructor.units.UnitsOfMeasurement
|
|
||||||
|
|
||||||
public data class XY<U: UnitsOfMeasurement>(val x: NumericalValue<U>, val y: NumericalValue<U>)
|
|
||||||
|
|
||||||
public data class XYZ<U: UnitsOfMeasurement>(val x: NumericalValue<U>, val y: NumericalValue<U>, val z: NumericalValue<U>)
|
|
@ -46,6 +46,8 @@ public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div(
|
|||||||
public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div(other: NumericalValue<U>): Double =
|
public operator fun <U : UnitsOfMeasurement> NumericalValue<U>.div(other: NumericalValue<U>): Double =
|
||||||
value / other.value
|
value / other.value
|
||||||
|
|
||||||
|
public operator fun <U: UnitsOfMeasurement> NumericalValue<U>.unaryMinus(): NumericalValue<U> = NumericalValue(-value)
|
||||||
|
|
||||||
|
|
||||||
private object NumericalValueMetaConverter : MetaConverter<NumericalValue<*>> {
|
private object NumericalValueMetaConverter : MetaConverter<NumericalValue<*>> {
|
||||||
override fun convert(obj: NumericalValue<*>): Meta = Meta(obj.value)
|
override fun convert(obj: NumericalValue<*>): Meta = Meta(obj.value)
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
package space.kscience.controls.constructor.units
|
||||||
|
|
||||||
|
public data class XY<U : UnitsOfMeasurement>(val x: NumericalValue<U>, val y: NumericalValue<U>)
|
||||||
|
|
||||||
|
public fun <U : UnitsOfMeasurement> XY(x: Number, y: Number): XY<U> = XY(NumericalValue(x), NumericalValue((y)))
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XY<U>.plus(other: XY<U>): XY<U> =
|
||||||
|
XY(x + other.x, y + other.y)
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XY<U>.times(c: Number): XY<U> = XY(x * c, y * c)
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XY<U>.div(c: Number): XY<U> = XY(x / c, y / c)
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XY<U>.unaryMinus(): XY<U> = XY(-x, -y)
|
||||||
|
|
||||||
|
public data class XYZ<U : UnitsOfMeasurement>(
|
||||||
|
val x: NumericalValue<U>,
|
||||||
|
val y: NumericalValue<U>,
|
||||||
|
val z: NumericalValue<U>,
|
||||||
|
)
|
||||||
|
|
||||||
|
public fun <U : UnitsOfMeasurement> XYZ(x: Number, y: Number, z: Number): XYZ<U> =
|
||||||
|
XYZ(NumericalValue(x), NumericalValue((y)), NumericalValue(z))
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST", "UNUSED_PARAMETER")
|
||||||
|
public fun <U : UnitsOfMeasurement, R : UnitsOfMeasurement> XYZ<U>.cast(units: R): XYZ<R> = this as XYZ<R>
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XYZ<U>.plus(other: XYZ<U>): XYZ<U> =
|
||||||
|
XYZ(x + other.x, y + other.y, z + other.z)
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XYZ<U>.minus(other: XYZ<U>): XYZ<U> =
|
||||||
|
XYZ(x - other.x, y - other.y, z - other.z)
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XYZ<U>.times(c: Number): XYZ<U> = XYZ(x * c, y * c, z * c)
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XYZ<U>.div(c: Number): XYZ<U> = XYZ(x / c, y / c, z / c)
|
||||||
|
|
||||||
|
public operator fun <U : UnitsOfMeasurement> XYZ<U>.unaryMinus(): XYZ<U> = XYZ(-x, -y, -z)
|
@ -1,105 +1,50 @@
|
|||||||
package space.kscience.controls.demo.constructor
|
package space.kscience.controls.demo.constructor
|
||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import space.kscience.controls.compose.asComposeState
|
import space.kscience.controls.compose.asComposeState
|
||||||
import space.kscience.controls.constructor.*
|
import space.kscience.controls.constructor.*
|
||||||
|
import space.kscience.controls.constructor.models.MaterialPoint
|
||||||
|
import space.kscience.controls.constructor.units.*
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
|
import java.awt.Dimension
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
import kotlin.time.DurationUnit
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class XY(val x: Double, val y: Double) {
|
|
||||||
companion object {
|
|
||||||
val ZERO = XY(0.0, 0.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val XY.length: Double get() = sqrt(x.pow(2) + y.pow(2))
|
|
||||||
|
|
||||||
private operator fun XY.plus(other: XY): XY = XY(x + other.x, y + other.y)
|
|
||||||
private operator fun XY.times(c: Double): XY = XY(x * c, y * c)
|
|
||||||
private operator fun XY.div(c: Double): XY = XY(x / c, y / c)
|
|
||||||
|
|
||||||
private class Spring(
|
private class Spring(
|
||||||
context: Context,
|
context: Context,
|
||||||
val k: Double,
|
val k: Double,
|
||||||
val l0: Double,
|
val l0: NumericalValue<Meters>,
|
||||||
val begin: DeviceState<XY>,
|
val begin: DeviceState<XYZ<Meters>>,
|
||||||
val end: DeviceState<XY>,
|
val end: DeviceState<XYZ<Meters>>,
|
||||||
) : ModelConstructor(context) {
|
) : ModelConstructor(context) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* vector from start to end
|
* Tension at the beginning point
|
||||||
*/
|
*/
|
||||||
val direction = combineState(begin, end) { begin: XY, end: XY ->
|
val tension: DeviceState<XYZ<Newtons>> = combineState(begin, end) { begin: XYZ<Meters>, end: XYZ<Meters> ->
|
||||||
val dx = end.x - begin.x
|
val delta = end - begin
|
||||||
val dy = end.y - begin.y
|
val l = sqrt(delta.x.value.pow(2) + delta.y.value.pow(2) + delta.z.value.pow(2))
|
||||||
val l = sqrt(dx.pow(2) + dy.pow(2))
|
((delta / l) * k * (l - l0.value)).cast(Newtons)
|
||||||
XY(dx / l, dy / l)
|
|
||||||
}
|
|
||||||
|
|
||||||
val tension: DeviceState<Double> = combineState(begin, end) { begin: XY, end: XY ->
|
|
||||||
val dx = end.x - begin.x
|
|
||||||
val dy = end.y - begin.y
|
|
||||||
k * sqrt(dx.pow(2) + dy.pow(2))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val beginForce = combineState(direction, tension) { direction: XY, tension: Double ->
|
|
||||||
direction * (tension)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val endForce = combineState(direction, tension) { direction: XY, tension: Double ->
|
|
||||||
direction * (-tension)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MaterialPoint(
|
|
||||||
context: Context,
|
|
||||||
val mass: Double,
|
|
||||||
val force: DeviceState<XY>,
|
|
||||||
val position: MutableDeviceState<XY>,
|
|
||||||
val velocity: MutableDeviceState<XY> = MutableDeviceState(XY.ZERO),
|
|
||||||
) : ModelConstructor(context, force, position, velocity) {
|
|
||||||
|
|
||||||
private val timer: TimerState = timer(2.milliseconds)
|
|
||||||
|
|
||||||
//TODO synchronize force change
|
|
||||||
|
|
||||||
private val movement = timer.onChange(
|
|
||||||
writes = setOf(position, velocity),
|
|
||||||
reads = setOf(force, velocity, position)
|
|
||||||
) { prev, next ->
|
|
||||||
val dt = (next - prev).toDouble(DurationUnit.SECONDS)
|
|
||||||
val a = force.value / mass
|
|
||||||
position.value += a * (dt * dt / 2) + velocity.value * dt
|
|
||||||
velocity.value += a * dt
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class BodyOnSprings(
|
private class BodyOnSprings(
|
||||||
context: Context,
|
context: Context,
|
||||||
mass: Double,
|
mass: NumericalValue<Kilograms>,
|
||||||
k: Double,
|
k: Double,
|
||||||
startPosition: XY,
|
startPosition: XYZ<Meters>,
|
||||||
l0: Double = 1.0,
|
l0: NumericalValue<Meters> = NumericalValue(1.0),
|
||||||
val xLeft: Double = -1.0,
|
val xLeft: Double = -1.0,
|
||||||
val xRight: Double = 1.0,
|
val xRight: Double = 1.0,
|
||||||
val yBottom: Double = -1.0,
|
val yBottom: Double = -1.0,
|
||||||
@ -110,22 +55,24 @@ private class BodyOnSprings(
|
|||||||
val height = yTop - yBottom
|
val height = yTop - yBottom
|
||||||
|
|
||||||
val position = stateOf(startPosition)
|
val position = stateOf(startPosition)
|
||||||
|
val velocity: MutableDeviceState<XYZ<MetersPerSecond>> = stateOf(XYZ(0, 0, 0))
|
||||||
|
|
||||||
private val leftAnchor = stateOf(XY(xLeft, (yTop + yBottom) / 2))
|
private val leftAnchor = stateOf(XYZ<Meters>(xLeft, (yTop + yBottom) / 2, 0.0))
|
||||||
|
|
||||||
val leftSpring = model(
|
val leftSpring = model(
|
||||||
Spring(context, k, l0, leftAnchor, position)
|
Spring(context, k, l0, leftAnchor, position)
|
||||||
)
|
)
|
||||||
|
|
||||||
private val rightAnchor = stateOf(XY(xRight, (yTop + yBottom) / 2))
|
private val rightAnchor = stateOf(XYZ<Meters>(xRight, (yTop + yBottom) / 2, 0.0))
|
||||||
|
|
||||||
val rightSpring = model(
|
val rightSpring = model(
|
||||||
Spring(context, k, l0, rightAnchor, position)
|
Spring(context, k, l0, rightAnchor, position)
|
||||||
)
|
)
|
||||||
|
|
||||||
val force: DeviceState<XY> = combineState(leftSpring.endForce, rightSpring.endForce) { left, right ->
|
val force: DeviceState<XYZ<Newtons>> =
|
||||||
left + right
|
combineState(leftSpring.tension, rightSpring.tension) { left: XYZ<Newtons>, right ->
|
||||||
}
|
-left - right
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val body = model(
|
val body = model(
|
||||||
@ -134,21 +81,23 @@ private class BodyOnSprings(
|
|||||||
mass = mass,
|
mass = mass,
|
||||||
force = force,
|
force = force,
|
||||||
position = position,
|
position = position,
|
||||||
|
velocity = velocity
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
val initialState = XY(0.1, 0.2)
|
val initialState = XYZ<Meters>(0.01, 0.1, 0)
|
||||||
|
|
||||||
Window(title = "Ball on springs", onCloseRequest = ::exitApplication) {
|
Window(title = "Ball on springs", onCloseRequest = ::exitApplication) {
|
||||||
|
window.minimumSize = Dimension(400, 400)
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
val context = remember {
|
val context = remember {
|
||||||
Context("simulation")
|
Context("simulation")
|
||||||
}
|
}
|
||||||
|
|
||||||
val model = remember {
|
val model = remember {
|
||||||
BodyOnSprings(context, 100.0, 1000.0, initialState)
|
BodyOnSprings(context, NumericalValue(10.0), 100.0, initialState)
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO add ability to freeze model
|
//TODO add ability to freeze model
|
||||||
@ -159,24 +108,23 @@ fun main() = application {
|
|||||||
// }.collect()
|
// }.collect()
|
||||||
// }
|
// }
|
||||||
|
|
||||||
val position: XY by model.body.position.asComposeState()
|
val position: XYZ<Meters> by model.body.position.asComposeState()
|
||||||
Box(Modifier.size(400.dp)) {
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
fun XYZ<Meters>.toOffset() = Offset(
|
||||||
fun XY.toOffset() = Offset(
|
((x.value - model.xLeft) / model.width * size.width).toFloat(),
|
||||||
center.x + (x / model.width * size.width).toFloat(),
|
((y.value - model.yBottom) / model.height * size.height).toFloat()
|
||||||
center.y - (y / model.height * size.height).toFloat()
|
|
||||||
)
|
|
||||||
|
|
||||||
drawCircle(
|
)
|
||||||
Color.Red, 10f, center = position.toOffset()
|
|
||||||
)
|
drawCircle(
|
||||||
drawLine(Color.Blue, model.leftSpring.begin.value.toOffset(), model.leftSpring.end.value.toOffset())
|
Color.Red, 10f, center = position.toOffset()
|
||||||
drawLine(
|
)
|
||||||
Color.Blue,
|
drawLine(Color.Blue, model.leftSpring.begin.value.toOffset(), model.leftSpring.end.value.toOffset())
|
||||||
model.rightSpring.begin.value.toOffset(),
|
drawLine(
|
||||||
model.rightSpring.end.value.toOffset()
|
Color.Blue,
|
||||||
)
|
model.rightSpring.begin.value.toOffset(),
|
||||||
}
|
model.rightSpring.end.value.toOffset()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user