Implement new visualization

This commit is contained in:
Alexander Nozik 2024-07-22 18:46:58 +03:00
parent dc4f2c6126
commit fbf79f0a37
13 changed files with 280 additions and 86 deletions

View File

@ -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

View File

@ -7,7 +7,7 @@ plugins {
allprojects {
group = "space.kscience"
version = "0.4.0-dev-4"
version = "0.4.0-dev-5"
repositories{
google()
}

View File

@ -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)

View 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")
}
```

View File

@ -18,26 +18,12 @@ kscience {
useContextReceivers()
commonMain {
api(projects.controlsConstructor)
api("io.github.koalaplot:koalaplot-core:0.6.0")
}
}
kotlin {
sourceSets {
commonMain {
dependencies {
api(libs.koala.plots)
api(compose.foundation)
api(compose.material3)
@OptIn(ExperimentalComposeLibrary::class)
api(compose.desktop.components.splitPane)
}
}
// jvmMain {
// dependencies {
// implementation(compose.desktop.currentOs)
// }
// }
}
}

View File

@ -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))
}

View File

@ -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() }
}
}
}
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}
val center = toOffset(position.x, position.y)
}
second {
Device2DCanvas(modifier = Modifier.fillMaxSize()) {
fun xToPx(x: NumericalValue<Meters>): Float =
((x - xRange.start) / (xRange.endInclusive - xRange.start) * size.width).toFloat()
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))
observeState(plotterModel.y, "beam") { y ->
RectangleDrawable2D(
position = Offset(size.width / 2, yToPx(y)),
rectangleSize = Size(size.width, 10f),
color = Color.LightGray
)
}
drawCircle(Color.Black, radius = 10f, center = center)
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)
}
}
}
}
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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

View File

@ -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
}