Implement new visualization
This commit is contained in:
parent
dc4f2c6126
commit
fbf79f0a37
@ -8,6 +8,7 @@
|
||||
- Shortcuts to access all Controls devices in a magix network.
|
||||
- `DeviceClient` properly evaluates lifecycle and logs
|
||||
- `PeerConnection` API for direct device-device binary sharing
|
||||
- DeviceDrawable2D intermediate visualization implementation
|
||||
|
||||
### Changed
|
||||
- Constructor properties return `DeviceState` in order to be able to subscribe to them
|
||||
|
@ -7,7 +7,7 @@ plugins {
|
||||
|
||||
allprojects {
|
||||
group = "space.kscience"
|
||||
version = "0.4.0-dev-4"
|
||||
version = "0.4.0-dev-5"
|
||||
repositories{
|
||||
google()
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.emptyFlow
|
||||
*/
|
||||
private class VirtualDeviceState<T>(
|
||||
initialValue: T,
|
||||
private val callback: (T) -> Unit = {},
|
||||
private val callback: (T) -> Unit = {}
|
||||
) : MutableDeviceState<T> {
|
||||
private val flow = MutableStateFlow(initialValue)
|
||||
override val valueFlow: Flow<T> get() = flow
|
||||
@ -34,7 +34,7 @@ private class VirtualDeviceState<T>(
|
||||
*/
|
||||
public fun <T> MutableDeviceState(
|
||||
initialValue: T,
|
||||
callback: (T) -> Unit = {},
|
||||
callback: (T) -> Unit = {}
|
||||
): MutableDeviceState<T> = VirtualDeviceState(initialValue, callback)
|
||||
|
||||
|
||||
|
21
controls-visualisation-compose/README.md
Normal file
21
controls-visualisation-compose/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Module controls-visualisation-compose
|
||||
|
||||
Visualisation extension using compose-multiplatform
|
||||
|
||||
## Usage
|
||||
|
||||
## Artifact:
|
||||
|
||||
The Maven coordinates of this project are `space.kscience:controls-visualisation-compose:0.4.0-dev-4`.
|
||||
|
||||
**Gradle Kotlin DSL:**
|
||||
```kotlin
|
||||
repositories {
|
||||
maven("https://repo.kotlin.link")
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("space.kscience:controls-visualisation-compose:0.4.0-dev-4")
|
||||
}
|
||||
```
|
@ -18,25 +18,11 @@ kscience {
|
||||
useContextReceivers()
|
||||
commonMain {
|
||||
api(projects.controlsConstructor)
|
||||
api("io.github.koalaplot:koalaplot-core:0.6.0")
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api(compose.foundation)
|
||||
api(compose.material3)
|
||||
@OptIn(ExperimentalComposeLibrary::class)
|
||||
api(compose.desktop.components.splitPane)
|
||||
}
|
||||
}
|
||||
// jvmMain {
|
||||
// dependencies {
|
||||
// implementation(compose.desktop.currentOs)
|
||||
// }
|
||||
// }
|
||||
api(libs.koala.plots)
|
||||
api(compose.foundation)
|
||||
api(compose.material3)
|
||||
@OptIn(ExperimentalComposeLibrary::class)
|
||||
api(compose.desktop.components.splitPane)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,64 @@
|
||||
package space.kscience.controls.compose
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
|
||||
/**
|
||||
* A single 2D drawable
|
||||
*/
|
||||
@Immutable
|
||||
public sealed interface DeviceDrawable2D {
|
||||
|
||||
public fun DrawScope.draw()
|
||||
|
||||
override fun equals(other: Any?): Boolean
|
||||
}
|
||||
|
||||
@Immutable
|
||||
public data class CircleDrawable2D(val position: Offset, val radius: Float, val color: Color) : DeviceDrawable2D {
|
||||
override fun DrawScope.draw() {
|
||||
drawCircle(color, radius = radius, center = position)
|
||||
}
|
||||
}
|
||||
|
||||
@Drawable2DBuilder
|
||||
public fun DeviceDrawable2DStore.circle(id: String, position: Offset, radius: Float, color: Color) {
|
||||
emit(id, CircleDrawable2D(position, radius, color))
|
||||
}
|
||||
|
||||
@Immutable
|
||||
public data class RectangleDrawable2D(
|
||||
val position: Offset,
|
||||
val rectangleSize: Size,
|
||||
val color: Color,
|
||||
val rotateDegrees: Float = 0f,
|
||||
) : DeviceDrawable2D {
|
||||
override fun DrawScope.draw() {
|
||||
rotate(rotateDegrees) {
|
||||
drawRect(
|
||||
color = color,
|
||||
topLeft = Offset(
|
||||
(position.x - rectangleSize.width / 2),
|
||||
(position.y - rectangleSize.height / 2)
|
||||
),
|
||||
size = Size(rectangleSize.width, rectangleSize.height)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Drawable2DBuilder
|
||||
public fun DeviceDrawable2DStore.rectangle(
|
||||
id: String,
|
||||
position: Offset,
|
||||
rectangleSize: Size,
|
||||
color: Color,
|
||||
rotateDegrees: Float = 0f,
|
||||
) {
|
||||
emit(id, RectangleDrawable2D(position, rectangleSize, color, rotateDegrees))
|
||||
}
|
||||
|
@ -0,0 +1,91 @@
|
||||
package space.kscience.controls.compose
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.constructor.DeviceState
|
||||
import space.kscience.controls.spec.DevicePropertySpec
|
||||
import space.kscience.controls.spec.propertyFlow
|
||||
|
||||
@DslMarker
|
||||
public annotation class Drawable2DBuilder
|
||||
|
||||
@Drawable2DBuilder
|
||||
public class DeviceDrawable2DStore(public val scope: CoroutineScope, public val size: Size) {
|
||||
public val drawableFlow: MutableStateFlow<Map<String, DeviceDrawable2D>> = MutableStateFlow(emptyMap())
|
||||
}
|
||||
|
||||
public fun DeviceDrawable2DStore.emit(id: String, drawable2D: DeviceDrawable2D){
|
||||
drawableFlow.value += (id to drawable2D)
|
||||
}
|
||||
|
||||
public fun DeviceDrawable2DStore.emitAll(drawables: Map<String, DeviceDrawable2D>){
|
||||
drawableFlow.value += drawables
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fill drawables from a flow
|
||||
*/
|
||||
public fun DeviceDrawable2DStore.observe(id: String, flow: Flow<DeviceDrawable2D>): Job = flow.onEach {
|
||||
drawableFlow.value += (id to it)
|
||||
}.launchIn(scope)
|
||||
|
||||
/**
|
||||
* Observe single [DeviceState]
|
||||
*/
|
||||
public fun <T> DeviceDrawable2DStore.observeState(
|
||||
state: DeviceState<T>,
|
||||
id: String = state.toString(),
|
||||
transform: suspend DeviceDrawable2DStore.(T) -> DeviceDrawable2D,
|
||||
): Job = observe(id, state.valueFlow.map { transform(this, it) })
|
||||
|
||||
/**
|
||||
* Observe a single [Device] property
|
||||
*/
|
||||
public fun <T, D : Device, P : DevicePropertySpec<D, T>> DeviceDrawable2DStore.observeProperty(
|
||||
device: D,
|
||||
devicePropertySpec: DevicePropertySpec<D, T>,
|
||||
id: String = devicePropertySpec.toString(),
|
||||
transform: suspend DeviceDrawable2DStore.(T) -> DeviceDrawable2D,
|
||||
): Job = observe(id, device.propertyFlow(devicePropertySpec).map { transform(this, it) })
|
||||
|
||||
@Composable
|
||||
public fun Device2DCanvas(
|
||||
modifier: Modifier = Modifier,
|
||||
flowBuilder: suspend DeviceDrawable2DStore.() -> Unit,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var canvasSize by remember { mutableStateOf(Size(100f, 100f)) }
|
||||
|
||||
val store = remember(canvasSize) {
|
||||
DeviceDrawable2DStore(coroutineScope, canvasSize).apply {
|
||||
coroutineScope.launch {
|
||||
flowBuilder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val drawables by store.drawableFlow.collectAsState()
|
||||
|
||||
key(store) {
|
||||
Canvas(modifier.onGloballyPositioned {
|
||||
canvasSize = it.size.toSize()
|
||||
}) {
|
||||
clipRect {
|
||||
drawables.values.forEach {
|
||||
with(it) { draw() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -37,8 +37,8 @@ kotlin{
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll("-Xjvm-default=all")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,29 @@
|
||||
package space.kscience.controls.demo.constructor
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
|
||||
import org.jetbrains.compose.splitpane.HorizontalSplitPane
|
||||
import space.kscience.controls.compose.*
|
||||
import space.kscience.controls.constructor.*
|
||||
import space.kscience.controls.constructor.devices.LimitSwitch
|
||||
import space.kscience.controls.constructor.devices.StepDrive
|
||||
@ -121,24 +131,30 @@ private class PlotterModel(
|
||||
context = context,
|
||||
xDrive = xDrive,
|
||||
yDrive = yDrive,
|
||||
xStartLimit = LimitSwitch(context,x.atStart),
|
||||
xEndLimit = LimitSwitch(context,x.atEnd),
|
||||
yStartLimit = LimitSwitch(context,x.atStart),
|
||||
yEndLimit = LimitSwitch(context,x.atEnd),
|
||||
xStartLimit = LimitSwitch(context, x.atStart),
|
||||
xEndLimit = LimitSwitch(context, x.atEnd),
|
||||
yStartLimit = LimitSwitch(context, x.atStart),
|
||||
yEndLimit = LimitSwitch(context, x.atEnd),
|
||||
) { color ->
|
||||
println("Point X: ${x.value.value}, Y: ${y.value.value}, color: $color")
|
||||
callback(PlotterPoint(x.value, y.value, color))
|
||||
}
|
||||
}
|
||||
|
||||
private val range = -1000..1000
|
||||
|
||||
@OptIn(ExperimentalSplitPaneApi::class)
|
||||
suspend fun main() = application {
|
||||
Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) {
|
||||
window.minimumSize = Dimension(400, 400)
|
||||
|
||||
val points = remember { mutableStateListOf<PlotterPoint>() }
|
||||
var position by remember { mutableStateOf(XY<Meters>(0, 0)) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
var updateJob: Job? = remember { null }
|
||||
|
||||
var points by remember { mutableStateOf<List<PlotterPoint>>(emptyList()) }
|
||||
|
||||
val plotterModel = remember {
|
||||
val context = Context {
|
||||
plugin(DeviceManager)
|
||||
plugin(ClockManager)
|
||||
@ -146,53 +162,81 @@ suspend fun main() = application {
|
||||
|
||||
/* Here goes the device definition block */
|
||||
|
||||
val plotterModel = PlotterModel(context) { plotterPoint ->
|
||||
points.add(plotterPoint)
|
||||
PlotterModel(context) { plotterPoint ->
|
||||
points += plotterPoint
|
||||
}
|
||||
|
||||
/* Start visualization program */
|
||||
|
||||
plotterModel.xy.valueFlow.onEach {
|
||||
position = it
|
||||
}.launchIn(this)
|
||||
|
||||
/* run program */
|
||||
|
||||
|
||||
val range = -1000..1000
|
||||
// plotterModel.plotter.modernArt(range, range)
|
||||
plotterModel.plotter.square(range, range)
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Here goes the visualization block */
|
||||
|
||||
MaterialTheme {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
fun toOffset(x: NumericalValue<Meters>, y: NumericalValue<Meters>): Offset {
|
||||
val canvasX = (x - xRange.start) / (xRange.endInclusive - xRange.start) * size.width
|
||||
val canvasY = (y - yRange.start) / (yRange.endInclusive - yRange.start) * size.height
|
||||
return Offset(canvasX.toFloat(), canvasY.toFloat())
|
||||
HorizontalSplitPane {
|
||||
first(200.dp) {
|
||||
Column(modifier = Modifier.fillMaxHeight()) {
|
||||
Button({
|
||||
updateJob?.cancel()
|
||||
updateJob = scope.launch {
|
||||
plotterModel.plotter.square(range, range)
|
||||
}
|
||||
}, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Rectangle")
|
||||
}
|
||||
Button({
|
||||
updateJob?.cancel()
|
||||
updateJob = scope.launch {
|
||||
plotterModel.plotter.modernArt(range, range)
|
||||
}
|
||||
}, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Modern Art")
|
||||
}
|
||||
Button({
|
||||
updateJob?.cancel()
|
||||
}, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Stop")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
second {
|
||||
Device2DCanvas(modifier = Modifier.fillMaxSize()) {
|
||||
fun xToPx(x: NumericalValue<Meters>): Float =
|
||||
((x - xRange.start) / (xRange.endInclusive - xRange.start) * size.width).toFloat()
|
||||
|
||||
val center = toOffset(position.x, position.y)
|
||||
fun yToPx(y: NumericalValue<Meters>): Float =
|
||||
((y - yRange.start) / (yRange.endInclusive - yRange.start) * size.height).toFloat()
|
||||
|
||||
|
||||
drawRect(
|
||||
Color.LightGray,
|
||||
topLeft = Offset(0f, center.y - 5f),
|
||||
size = Size(size.width, 10f)
|
||||
)
|
||||
fun toOffset(xy: XY<Meters>): Offset = Offset(xToPx(xy.x), yToPx(xy.y))
|
||||
|
||||
drawCircle(Color.Black, radius = 10f, center = center)
|
||||
observeState(plotterModel.y, "beam") { y ->
|
||||
RectangleDrawable2D(
|
||||
position = Offset(size.width / 2, yToPx(y)),
|
||||
rectangleSize = Size(size.width, 10f),
|
||||
color = Color.LightGray
|
||||
)
|
||||
}
|
||||
|
||||
observeState(plotterModel.xy, "head") { xy ->
|
||||
CircleDrawable2D(
|
||||
position = toOffset(xy),
|
||||
radius = 10f,
|
||||
color = Color.Black
|
||||
)
|
||||
}
|
||||
|
||||
points.forEach {
|
||||
drawCircle(it.color, radius = 2f, center = toOffset(it.x, it.y))
|
||||
snapshotFlow { points }.onEach {
|
||||
it.forEachIndexed { index, plotterPoint ->
|
||||
circle(
|
||||
"point[$index]",
|
||||
Offset(xToPx(plotterPoint.x), yToPx(plotterPoint.y)),
|
||||
radius = 5f,
|
||||
color = plotterPoint.color
|
||||
)
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -21,8 +21,8 @@ kotlin{
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll("-Xjvm-default=all")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,8 +26,8 @@ kotlin{
|
||||
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll("-Xjvm-default=all")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,9 @@ visionforge-markdown = { module = "space.kscience:visionforge-markdown", version
|
||||
visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" }
|
||||
visionforge-compose-html = { module = "space.kscience:visionforge-compose-html", version.ref = "visionforge" }
|
||||
|
||||
sciprog-maps-compose = "space.kscience:maps-kt-compose:0.3.0"
|
||||
sciprog-maps-compose = { module = "space.kscience:maps-kt-compose", version = "0.3.0" }
|
||||
|
||||
koala-plots = { module = "io.github.koalaplot:koalaplot-core", version = "0.6.1" }
|
||||
|
||||
# Buildscript
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import space.kscience.gradle.KScienceVersions
|
||||
import space.kscience.gradle.Maturity
|
||||
|
||||
plugins {
|
||||
@ -17,19 +15,6 @@ dependencies {
|
||||
implementation(spclibs.kotlinx.coroutines.jdk9)
|
||||
}
|
||||
|
||||
//java {
|
||||
// sourceCompatibility = KScienceVersions.JVM_TARGET
|
||||
// targetCompatibility = KScienceVersions.JVM_TARGET
|
||||
//}
|
||||
|
||||
|
||||
//FIXME https://youtrack.jetbrains.com/issue/KT-52815/Compiler-option-Xjdk-release-fails-to-compile-mixed-projects
|
||||
tasks.withType<KotlinCompile>{
|
||||
kotlinOptions {
|
||||
freeCompilerArgs -= "-Xjdk-release=11"
|
||||
}
|
||||
}
|
||||
|
||||
readme{
|
||||
readme {
|
||||
maturity = Maturity.EXPERIMENTAL
|
||||
}
|
Loading…
Reference in New Issue
Block a user