Compare commits

...

2 Commits

14 changed files with 284 additions and 87 deletions

View File

@ -8,6 +8,7 @@
- Shortcuts to access all Controls devices in a magix network. - Shortcuts to access all Controls devices in a magix network.
- `DeviceClient` properly evaluates lifecycle and logs - `DeviceClient` properly evaluates lifecycle and logs
- `PeerConnection` API for direct device-device binary sharing - `PeerConnection` API for direct device-device binary sharing
- DeviceDrawable2D intermediate visualization implementation
### Changed ### Changed
- Constructor properties return `DeviceState` in order to be able to subscribe to them - Constructor properties return `DeviceState` in order to be able to subscribe to them

View File

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

View File

@ -14,7 +14,7 @@ import space.kscience.dataforge.context.Context
/** /**
* Virtual [LimitSwitch] * A device that detects if a motor hits the end of its range
*/ */
public class LimitSwitch( public class LimitSwitch(
context: Context, context: Context,

View File

@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.emptyFlow
*/ */
private class VirtualDeviceState<T>( private class VirtualDeviceState<T>(
initialValue: T, initialValue: T,
private val callback: (T) -> Unit = {}, private val callback: (T) -> Unit = {}
) : MutableDeviceState<T> { ) : MutableDeviceState<T> {
private val flow = MutableStateFlow(initialValue) private val flow = MutableStateFlow(initialValue)
override val valueFlow: Flow<T> get() = flow override val valueFlow: Flow<T> get() = flow
@ -34,7 +34,7 @@ private class VirtualDeviceState<T>(
*/ */
public fun <T> MutableDeviceState( public fun <T> MutableDeviceState(
initialValue: T, initialValue: T,
callback: (T) -> Unit = {}, callback: (T) -> Unit = {}
): MutableDeviceState<T> = VirtualDeviceState(initialValue, callback) ): 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,27 +18,13 @@ kscience {
useContextReceivers() useContextReceivers()
commonMain { commonMain {
api(projects.controlsConstructor) api(projects.controlsConstructor)
api("io.github.koalaplot:koalaplot-core:0.6.0") api(libs.koala.plots)
}
}
kotlin {
sourceSets {
commonMain {
dependencies {
api(compose.foundation) api(compose.foundation)
api(compose.material3) api(compose.material3)
@OptIn(ExperimentalComposeLibrary::class) @OptIn(ExperimentalComposeLibrary::class)
api(compose.desktop.components.splitPane) api(compose.desktop.components.splitPane)
} }
} }
// jvmMain {
// dependencies {
// implementation(compose.desktop.currentOs)
// }
// }
}
}
readme { readme {

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,94 @@
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.DrawScope
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,
onDraw: DrawScope.() -> Unit = {},
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() }
}
onDraw()
}
}
}
}

View File

@ -37,8 +37,8 @@ kotlin{
} }
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions { compilerOptions {
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") freeCompilerArgs.addAll("-Xjvm-default=all")
} }
} }

View File

@ -1,19 +1,29 @@
package space.kscience.controls.demo.constructor 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.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.runtime.*
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.geometry.Size import androidx.compose.ui.geometry.Size
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.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive 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.*
import space.kscience.controls.constructor.devices.LimitSwitch import space.kscience.controls.constructor.devices.LimitSwitch
import space.kscience.controls.constructor.devices.StepDrive import space.kscience.controls.constructor.devices.StepDrive
@ -131,14 +141,20 @@ private class PlotterModel(
} }
} }
private val range = -1000..1000
@OptIn(ExperimentalSplitPaneApi::class)
suspend fun main() = application { suspend fun main() = application {
Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) { Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) {
window.minimumSize = Dimension(400, 400) window.minimumSize = Dimension(400, 400)
val points = remember { mutableStateListOf<PlotterPoint>() } val scope = rememberCoroutineScope()
var position by remember { mutableStateOf(XY<Meters>(0, 0)) }
LaunchedEffect(Unit) { var updateJob: Job? = remember { null }
var points by remember { mutableStateOf<List<PlotterPoint>>(emptyList()) }
val plotterModel = remember {
val context = Context { val context = Context {
plugin(DeviceManager) plugin(DeviceManager)
plugin(ClockManager) plugin(ClockManager)
@ -146,53 +162,81 @@ suspend fun main() = application {
/* Here goes the device definition block */ /* Here goes the device definition block */
val plotterModel = PlotterModel(context) { plotterPoint -> PlotterModel(context) { plotterPoint ->
points.add(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 */ /* Here goes the visualization block */
MaterialTheme { MaterialTheme {
Canvas(modifier = Modifier.fillMaxSize()) { HorizontalSplitPane {
fun toOffset(x: NumericalValue<Meters>, y: NumericalValue<Meters>): Offset { first(200.dp) {
val canvasX = (x - xRange.start) / (xRange.endInclusive - xRange.start) * size.width Column(modifier = Modifier.fillMaxHeight()) {
val canvasY = (y - yRange.start) / (yRange.endInclusive - yRange.start) * size.height Button({
return Offset(canvasX.toFloat(), canvasY.toFloat()) 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( fun toOffset(xy: XY<Meters>): Offset = Offset(xToPx(xy.x), yToPx(xy.y))
Color.LightGray,
topLeft = Offset(0f, center.y - 5f), observeState(plotterModel.y, "beam") { y ->
size = Size(size.width, 10f) RectangleDrawable2D(
position = Offset(size.width / 2, yToPx(y)),
rectangleSize = Size(size.width, 10f),
color = Color.LightGray
) )
drawCircle(Color.Black, radius = 10f, center = center)
points.forEach {
drawCircle(it.color, radius = 2f, center = toOffset(it.x, it.y))
}
}
}
} }
observeState(plotterModel.xy, "head") { xy ->
CircleDrawable2D(
position = toOffset(xy),
radius = 10f,
color = Color.Black
)
}
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 { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions { compilerOptions {
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") freeCompilerArgs.addAll("-Xjvm-default=all")
} }
} }

View File

@ -26,8 +26,8 @@ kotlin{
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions { compilerOptions {
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") 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-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" }
visionforge-compose-html = { module = "space.kscience:visionforge-compose-html", 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 # Buildscript

View File

@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import space.kscience.gradle.KScienceVersions
import space.kscience.gradle.Maturity import space.kscience.gradle.Maturity
plugins { plugins {
@ -17,19 +15,6 @@ dependencies {
implementation(spclibs.kotlinx.coroutines.jdk9) 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 maturity = Maturity.EXPERIMENTAL
} }