diff --git a/.space/CODEOWNERS b/.space/CODEOWNERS deleted file mode 100644 index 9f836ea..0000000 --- a/.space/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -./space/* "Project Admin" diff --git a/build.gradle.kts b/build.gradle.kts index 6423c3c..50a61d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,10 +7,7 @@ plugins { allprojects { group = "space.kscience" - version = "0.4.0-dev-3" - repositories{ - maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") - } + version = "0.4.0-dev-4" } ksciencePublish { diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt index 03a1e0e..17294f1 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt @@ -1,8 +1,5 @@ package space.kscience.controls.constructor -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.spec.DevicePropertySpec @@ -25,29 +22,24 @@ public abstract class DeviceConstructor( context: Context, meta: Meta = Meta.EMPTY, ) : DeviceGroup(context, meta), StateContainer { - private val _stateDescriptors: MutableList = mutableListOf() - override val stateDescriptors: List get() = _stateDescriptors + private val _stateDescriptors: MutableSet = mutableSetOf() + override val stateDescriptors: Set get() = _stateDescriptors override fun registerState(stateDescriptor: StateDescriptor) { _stateDescriptors.add(stateDescriptor) } - override fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) { - super.registerProperty(descriptor, state) - registerState(PropertyStateDescriptor(this, descriptor.name, state)) + override fun unregisterState(stateDescriptor: StateDescriptor) { + _stateDescriptors.remove(stateDescriptor) } - /** - * Bind an action to a [DeviceState]. [onChange] block is performed on each state change - * - * Optionally provide [writes] - a set of states that this change affects. - */ - public fun DeviceState.onChange( - vararg writes: DeviceState<*>, - reads: Collection>, - onChange: suspend (T) -> Unit, - ): Job = valueFlow.onEach(onChange).launchIn(this@DeviceConstructor).also { - registerState(ConnectionStateDescriptor(setOf(this, *reads.toTypedArray()), setOf(*writes))) + override fun registerProperty( + converter: MetaConverter, + descriptor: PropertyDescriptor, + state: DeviceState, + ) { + super.registerProperty(converter, descriptor, state) + registerState(StatePropertyDescriptor(this, descriptor.name, state)) } } @@ -84,6 +76,7 @@ public fun DeviceConstructor.device( * Register a property and provide a direct reader for it */ public fun > DeviceConstructor.property( + converter: MetaConverter, state: S, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, @@ -91,7 +84,7 @@ public fun > DeviceConstructor.property( PropertyDelegateProvider { _: DeviceConstructor, property -> val name = nameOverride ?: property.name val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) - registerProperty(descriptor, state) + registerProperty(converter, descriptor, state) ReadOnlyProperty { _: DeviceConstructor, _ -> state } @@ -108,7 +101,8 @@ public fun DeviceConstructor.property( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, ): PropertyDelegateProvider>> = property( - DeviceState.external(this, metaConverter, readInterval, initialState, reader), + metaConverter, + DeviceState.external(this, readInterval, initialState, reader), descriptorBuilder, nameOverride, ) @@ -125,7 +119,8 @@ public fun DeviceConstructor.mutableProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null, ): PropertyDelegateProvider>> = property( - DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), + metaConverter, + DeviceState.external(this, readInterval, initialState, reader, writer), descriptorBuilder, nameOverride, ) @@ -140,7 +135,8 @@ public fun DeviceConstructor.virtualProperty( nameOverride: String? = null, callback: (T) -> Unit = {}, ): PropertyDelegateProvider>> = property( - DeviceState.internal(metaConverter, initialState, callback), + metaConverter, + MutableDeviceState(initialState, callback), descriptorBuilder, nameOverride, ) @@ -153,7 +149,7 @@ public fun DeviceConstructor.deviceProperty( property: DevicePropertySpec, initialValue: T, ): PropertyDelegateProvider>> = - property(device.propertyAsState(property, initialValue)) + property(property.converter, device.propertyAsState(property, initialValue)) /** * Bind existing property provided by specification to this device @@ -163,4 +159,4 @@ public fun DeviceConstructor.deviceProperty( property: MutableDevicePropertySpec, initialValue: T, ): PropertyDelegateProvider>> = - property(device.mutablePropertyAsState(property, initialValue)) + property(property.converter, device.mutablePropertyAsState(property, initialValue)) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt index ef886dc..ddac115 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -1,14 +1,12 @@ package space.kscience.controls.constructor import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* import space.kscience.controls.api.* import space.kscience.controls.api.DeviceLifecycleState.* import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install +import space.kscience.controls.spec.DevicePropertySpec import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.Laminate import space.kscience.dataforge.meta.Meta @@ -30,12 +28,21 @@ public open class DeviceGroup( override val meta: Meta, ) : DeviceHub, CachingDevice { - internal class Property( - val state: DeviceState<*>, + private class Property( + val state: DeviceState, + val converter: MetaConverter, val descriptor: PropertyDescriptor, - ) + ) { + val valueAsMeta get() = converter.convert(state.value) - internal class Action( + fun setMeta(meta: Meta) { + check(state is MutableDeviceState) { "Can't write to read-only property" } + + state.value = converter.read(meta) + } + } + + private class Action( val invoke: suspend (Meta?) -> Meta?, val descriptor: ActionDescriptor, ) @@ -81,16 +88,20 @@ public open class DeviceGroup( return device } - private val properties: MutableMap = hashMapOf() + private val properties: MutableMap> = hashMapOf() /** * Register a new property based on [DeviceState]. Properties could be modified dynamically */ - public open fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) { + public open fun registerProperty( + converter: MetaConverter, + descriptor: PropertyDescriptor, + state: DeviceState, + ) { val name = descriptor.name.parseAsName() require(properties[name] == null) { "Can't add property with name $name. It already exists." } - properties[name] = Property(state, descriptor) - state.metaFlow.onEach { + properties[name] = Property(state, converter, descriptor) + state.valueFlow.map(converter::convert).onEach { sharedMessageFlow.emit( PropertyChangedMessage( descriptor.name, @@ -109,19 +120,18 @@ public open class DeviceGroup( get() = actions.values.map { it.descriptor } override suspend fun readProperty(propertyName: String): Meta = - properties[propertyName.parseAsName()]?.state?.valueAsMeta + properties[propertyName.parseAsName()]?.valueAsMeta ?: error("Property with name $propertyName not found") - override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.state?.valueAsMeta + override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.valueAsMeta override suspend fun invalidate(propertyName: String) { //does nothing for this implementation } override suspend fun writeProperty(propertyName: String, value: Meta) { - val property = (properties[propertyName.parseAsName()]?.state as? MutableDeviceState) - ?: error("Property with name $propertyName not found") - property.valueAsMeta = value + val property = properties[propertyName.parseAsName()] ?: error("Property with name $propertyName not found") + property.setMeta(value) } @@ -164,6 +174,10 @@ public open class DeviceGroup( } } +public fun DeviceGroup.registerProperty(propertySpec: DevicePropertySpec<*, T>, state: DeviceState) { + registerProperty(propertySpec.converter, propertySpec.descriptor, state) +} + public fun DeviceManager.registerDeviceGroup( name: String = "@group", meta: Meta = Meta.EMPTY, @@ -234,10 +248,12 @@ public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() - */ public fun DeviceGroup.registerProperty( name: String, + converter: MetaConverter, state: DeviceState, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ) { registerProperty( + converter, PropertyDescriptor(name).apply(descriptorBuilder), state ) @@ -248,10 +264,12 @@ public fun DeviceGroup.registerProperty( */ public fun DeviceGroup.registerMutableProperty( name: String, + converter: MetaConverter, state: MutableDeviceState, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ) { registerProperty( + converter, PropertyDescriptor(name).apply(descriptorBuilder), state ) @@ -269,7 +287,7 @@ public fun DeviceGroup.registerVirtualProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, callback: (T) -> Unit = {}, ): MutableDeviceState { - val state = DeviceState.internal(converter, initialValue, callback) - registerMutableProperty(name, state, descriptorBuilder) + val state = MutableDeviceState(initialValue, callback) + registerMutableProperty(name, converter, state, descriptorBuilder) return state } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt index 6d44422..484b471 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceModel.kt @@ -1,14 +1,32 @@ package space.kscience.controls.constructor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.newCoroutineContext import space.kscience.dataforge.context.Context +import kotlin.coroutines.CoroutineContext -public abstract class DeviceModel(override val context: Context) : StateContainer { +public abstract class DeviceModel( + final override val context: Context, + vararg dependencies: DeviceState<*>, +) : StateContainer, CoroutineScope { - private val _stateDescriptors: MutableList = mutableListOf() + override val coroutineContext: CoroutineContext = context.newCoroutineContext(SupervisorJob()) - override val stateDescriptors: List get() = _stateDescriptors + + private val _stateDescriptors: MutableSet = mutableSetOf().apply { + dependencies.forEach { + add(StateNodeDescriptor(it)) + } + } + + override val stateDescriptors: Set get() = _stateDescriptors override fun registerState(stateDescriptor: StateDescriptor) { _stateDescriptors.add(stateDescriptor) } + + override fun unregisterState(stateDescriptor: StateDescriptor) { + _stateDescriptors.remove(stateDescriptor) + } } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt index 8a2914b..e74c4c0 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -6,15 +6,12 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.MetaConverter import kotlin.reflect.KProperty /** * An observable state of a device */ public interface DeviceState { - public val converter: MetaConverter public val value: T public val valueFlow: Flow @@ -24,9 +21,6 @@ public interface DeviceState { public companion object } -public val DeviceState.metaFlow: Flow get() = valueFlow.map(converter::convert) - -public val DeviceState.valueAsMeta: Meta get() = converter.convert(value) public operator fun DeviceState.getValue(thisRef: Any?, property: KProperty<*>): T = value @@ -47,12 +41,6 @@ public operator fun MutableDeviceState.setValue(thisRef: Any?, property: this.value = value } -public var MutableDeviceState.valueAsMeta: Meta - get() = converter.convert(value) - set(arg) { - value = converter.read(arg) - } - /** * Device state with a value that depends on other device states */ @@ -65,17 +53,15 @@ public interface DeviceStateWithDependencies : DeviceState { */ public fun DeviceState.Companion.map( state: DeviceState, - converter: MetaConverter, mapper: (T) -> R, + mapper: (T) -> R, ): DeviceStateWithDependencies = object : DeviceStateWithDependencies { override val dependencies = listOf(state) - override val converter: MetaConverter = converter - override val value: R get() = mapper(state.value) override val valueFlow: Flow = state.valueFlow.map(mapper) - override fun toString(): String = "DeviceState.map(arg=${state}, converter=$converter)" + override fun toString(): String = "DeviceState.map(arg=${state})" } /** @@ -84,13 +70,10 @@ public fun DeviceState.Companion.map( public fun DeviceState.Companion.combine( state1: DeviceState, state2: DeviceState, - converter: MetaConverter, mapper: (T1, T2) -> R, ): DeviceStateWithDependencies = object : DeviceStateWithDependencies { override val dependencies = listOf(state1, state2) - override val converter: MetaConverter = converter - override val value: R get() = mapper(state1.value, state2.value) override val valueFlow: Flow = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt index b8f40b5..30a0299 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/StateDescriptor.kt @@ -1,10 +1,14 @@ package space.kscience.controls.constructor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.runningFold import space.kscience.controls.api.Device import space.kscience.controls.manager.ClockManager import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.request -import space.kscience.dataforge.meta.MetaConverter import kotlin.time.Duration /** @@ -15,7 +19,7 @@ public sealed interface StateDescriptor /** * A binding that exposes device property as read-only state */ -public class PropertyStateDescriptor( +public class StatePropertyDescriptor( public val device: Device, public val propertyName: String, public val state: DeviceState, @@ -24,51 +28,172 @@ public class PropertyStateDescriptor( /** * A binding for independent state like a timer */ -public class StateBinding( +public class StateNodeDescriptor( public val state: DeviceState, ) : StateDescriptor -public class ConnectionStateDescriptor( +public class StateConnectionDescriptor( public val reads: Collection>, public val writes: Collection>, ) : StateDescriptor -public interface StateContainer : ContextAware { - public val stateDescriptors: List +public interface StateContainer : ContextAware, CoroutineScope { + public val stateDescriptors: Set public fun registerState(stateDescriptor: StateDescriptor) + public fun unregisterState(stateDescriptor: StateDescriptor) + + + /** + * Bind an action to a [DeviceState]. [onChange] block is performed on each state change + * + * Optionally provide [writes] - a set of states that this change affects. + */ + public fun DeviceState.onNext( + vararg writes: DeviceState<*>, + alsoReads: Collection> = emptySet(), + onChange: suspend (T) -> Unit, + ): Job = valueFlow.onEach(onChange).launchIn(this@StateContainer).also { + registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes))) + } + + public fun DeviceState.onChange( + vararg writes: DeviceState<*>, + alsoReads: Collection> = emptySet(), + onChange: suspend (prev: T, next: T) -> Unit, + ): Job = valueFlow.runningFold(Pair(value, value)) { pair, next -> + Pair(pair.second, next) + }.onEach { pair -> + if (pair.first != pair.second) { + onChange(pair.first, pair.second) + } + }.launchIn(this@StateContainer).also { + registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes))) + } } /** * Register a [state] in this container. The state is not registered as a device property if [this] is a [DeviceConstructor] */ public fun > StateContainer.state(state: D): D { - registerState(StateBinding(state)) + registerState(StateNodeDescriptor(state)) return state } /** * Create a register a [MutableDeviceState] with a given [converter] */ -public fun StateContainer.state(converter: MetaConverter, initialValue: T): MutableDeviceState = state( - DeviceState.internal(converter, initialValue) +public fun StateContainer.mutableState(initialValue: T): MutableDeviceState = state( + MutableDeviceState(initialValue) ) -/** - * Create a register a mutable [Double] state - */ -public fun StateContainer.doubleState(initialValue: Double): MutableDeviceState = state( - MetaConverter.double, initialValue -) - -/** - * Create a register a mutable [String] state - */ -public fun StateContainer.stringState(initialValue: String): MutableDeviceState = state( - MetaConverter.string, initialValue -) +public fun StateContainer.model(model: T): T { + model.stateDescriptors.forEach { + registerState(it) + } + return model +} /** * Create and register a timer state. */ public fun StateContainer.timer(tick: Duration): TimerState = state(TimerState(context.request(ClockManager), tick)) + + +public fun StateContainer.mapState( + state: DeviceState, + transformation: (T) -> R, +): DeviceStateWithDependencies = state(DeviceState.map(state, transformation)) + +/** + * Create a new state by combining two existing ones + */ +public fun StateContainer.combineState( + first: DeviceState, + second: DeviceState, + transformation: (T1, T2) -> R, +): DeviceState = state(DeviceState.combine(first, second, transformation)) + + +/** + * Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically + * transferred onto [targetState], but not vise versa. + * + * On resulting [Job] cancel the binding is unregistered + */ +public fun StateContainer.bindTo(sourceState: DeviceState, targetState: MutableDeviceState): Job { + val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState)) + registerState(descriptor) + return sourceState.valueFlow.onEach { + targetState.value = it + }.launchIn(this).apply { + invokeOnCompletion { + unregisterState(descriptor) + } + } +} + +/** + * Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically + * transferred onto [targetState] via [transformation], but not vise versa. + * + * On resulting [Job] cancel the binding is unregistered + */ +public fun StateContainer.transformTo( + sourceState: DeviceState, + targetState: MutableDeviceState, + transformation: suspend (T) -> R, +): Job { + val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState)) + registerState(descriptor) + return sourceState.valueFlow.onEach { + targetState.value = transformation(it) + }.launchIn(this).apply { + invokeOnCompletion { + unregisterState(descriptor) + } + } +} + +/** + * Register [StateDescriptor] that combines values from [sourceState1] and [sourceState2] using [transformation]. + * + * On resulting [Job] cancel the binding is unregistered + */ +public fun StateContainer.combineTo( + sourceState1: DeviceState, + sourceState2: DeviceState, + targetState: MutableDeviceState, + transformation: suspend (T1, T2) -> R, +): Job { + val descriptor = StateConnectionDescriptor(setOf(sourceState1, sourceState2), setOf(targetState)) + registerState(descriptor) + return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach { + targetState.value = it + }.launchIn(this).apply { + invokeOnCompletion { + unregisterState(descriptor) + } + } +} + +/** + * Register [StateDescriptor] that combines values from [sourceStates] using [transformation]. + * + * On resulting [Job] cancel the binding is unregistered + */ +public inline fun StateContainer.combineTo( + sourceStates: Collection>, + targetState: MutableDeviceState, + noinline transformation: suspend (Array) -> R, +): Job { + val descriptor = StateConnectionDescriptor(sourceStates, setOf(targetState)) + registerState(descriptor) + return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach { + targetState.value = it + }.launchIn(this).apply { + invokeOnCompletion { + unregisterState(descriptor) + } + } +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt index 8f36079..baf9646 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt @@ -7,8 +7,6 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.datetime.Instant import space.kscience.controls.manager.ClockManager -import space.kscience.controls.spec.instant -import space.kscience.dataforge.meta.MetaConverter import kotlin.time.Duration /** @@ -23,7 +21,6 @@ public class TimerState( public val clockManager: ClockManager, public val tick: Duration, ) : DeviceState { - override val converter: MetaConverter get() = MetaConverter.instant private val clock = MutableStateFlow(clockManager.clock.now()) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt index 37be770..f5c4480 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt @@ -15,7 +15,7 @@ import space.kscience.dataforge.meta.MetaConverter * A copy-free [DeviceState] bound to a device property */ private open class BoundDeviceState( - override val converter: MetaConverter, + val converter: MetaConverter, val device: Device, val propertyName: String, initialValue: T, diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt index d1d557f..aabacfd 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/exoticState.kt @@ -2,7 +2,6 @@ package space.kscience.controls.constructor import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import space.kscience.dataforge.meta.MetaConverter /** @@ -17,7 +16,6 @@ public class DoubleInRangeState( require(initialValue in range) { "Initial value should be in range" } } - override val converter: MetaConverter = MetaConverter.double private val _valueFlow = MutableStateFlow(initialValue) @@ -32,18 +30,18 @@ public class DoubleInRangeState( /** * A state showing that the range is on its lower boundary */ - public val atStart: DeviceState = DeviceState.map(this, MetaConverter.boolean) { + public val atStart: DeviceState = DeviceState.map(this) { it <= range.start } /** * A state showing that the range is on its higher boundary */ - public val atEnd: DeviceState = DeviceState.map(this, MetaConverter.boolean) { + public val atEnd: DeviceState = DeviceState.map(this) { it >= range.endInclusive } - override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)" + override fun toString(): String = "DoubleRangeState(range=$range)" } @@ -55,5 +53,5 @@ public fun StateContainer.doubleInRangeState( initialValue: Double, range: ClosedFloatingPointRange, ): DoubleInRangeState = DoubleInRangeState(initialValue, range).also { - registerState(StateBinding(it)) + registerState(StateNodeDescriptor(it)) } \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt index c670a23..9b4a15f 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt @@ -4,13 +4,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import space.kscience.dataforge.meta.MetaConverter import kotlin.time.Duration private open class ExternalState( val scope: CoroutineScope, - override val converter: MetaConverter, val readInterval: Duration, initialValue: T, val reader: suspend () -> T, @@ -26,7 +24,7 @@ private open class ExternalState( override val value: T get() = flow.value override val valueFlow: Flow get() = flow - override fun toString(): String = "ExternalState(converter=$converter)" + override fun toString(): String = "ExternalState()" } /** @@ -34,20 +32,18 @@ private open class ExternalState( */ public fun DeviceState.Companion.external( scope: CoroutineScope, - converter: MetaConverter, readInterval: Duration, initialValue: T, reader: suspend () -> T, -): DeviceState = ExternalState(scope, converter, readInterval, initialValue, reader) +): DeviceState = ExternalState(scope, readInterval, initialValue, reader) private class MutableExternalState( scope: CoroutineScope, - converter: MetaConverter, readInterval: Duration, initialValue: T, reader: suspend () -> T, val writer: suspend (T) -> Unit, -) : ExternalState(scope, converter, readInterval, initialValue, reader), MutableDeviceState { +) : ExternalState(scope, readInterval, initialValue, reader), MutableDeviceState { override var value: T get() = super.value set(value) { @@ -62,9 +58,8 @@ private class MutableExternalState( */ public fun DeviceState.Companion.external( scope: CoroutineScope, - converter: MetaConverter, readInterval: Duration, initialValue: T, reader: suspend () -> T, writer: suspend (T) -> Unit, -): MutableDeviceState = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer) \ No newline at end of file +): MutableDeviceState = MutableExternalState(scope, readInterval, initialValue, reader, writer) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt index 434c074..2bd9322 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt @@ -2,22 +2,19 @@ package space.kscience.controls.constructor import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import space.kscience.dataforge.meta.MetaConverter private class StateFlowAsState( - override val converter: MetaConverter, val flow: MutableStateFlow, ) : MutableDeviceState { override var value: T by flow::value override val valueFlow: Flow get() = flow - override fun toString(): String = "FlowAsState(converter=$converter)" + override fun toString(): String = "FlowAsState()" } /** * Create a read-only [DeviceState] that wraps [MutableStateFlow]. * No data copy is performed. */ -public fun MutableStateFlow.asDeviceState(converter: MetaConverter): DeviceState = - StateFlowAsState(converter, this) \ No newline at end of file +public fun MutableStateFlow.asDeviceState(): MutableDeviceState = StateFlowAsState(this) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt index ed2d5d1..11c778d 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt @@ -2,7 +2,6 @@ package space.kscience.controls.constructor import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import space.kscience.dataforge.meta.MetaConverter /** * A [MutableDeviceState] that does not correspond to a physical state @@ -10,7 +9,6 @@ import space.kscience.dataforge.meta.MetaConverter * @param callback a synchronous callback that could be used without a scope */ private class VirtualDeviceState( - override val converter: MetaConverter, initialValue: T, private val callback: (T) -> Unit = {}, ) : MutableDeviceState { @@ -24,7 +22,7 @@ private class VirtualDeviceState( callback(value) } - override fun toString(): String = "VirtualDeviceState(converter=$converter)" + override fun toString(): String = "VirtualDeviceState()" } @@ -33,8 +31,7 @@ private class VirtualDeviceState( * * @param callback a synchronous callback that could be used without a scope */ -public fun DeviceState.Companion.internal( - converter: MetaConverter, +public fun MutableDeviceState( initialValue: T, callback: (T) -> Unit = {}, -): MutableDeviceState = VirtualDeviceState(converter, initialValue, callback) +): MutableDeviceState = VirtualDeviceState(initialValue, callback) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt index e2f6331..4aef3ff 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt @@ -22,6 +22,7 @@ public interface DeviceHub : Provider { } else { emptyMap() } + //TODO send message on device change public companion object } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/converters.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt similarity index 62% rename from controls-core/src/commonMain/kotlin/space/kscience/controls/spec/converters.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt index 89a28da..4297d20 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/converters.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.spec +package space.kscience.controls.misc import kotlinx.datetime.Instant import space.kscience.dataforge.meta.* @@ -12,9 +12,9 @@ public fun Double.asMeta(): Meta = Meta(asValue()) * Generate a nullable [MetaConverter] from non-nullable one */ public fun MetaConverter.nullable(): MetaConverter = object : MetaConverter { - override fun convert(obj: T?): Meta = obj?.let { this@nullable.convert(it) }?: Meta(Null) + override fun convert(obj: T?): Meta = obj?.let { this@nullable.convert(it) } ?: Meta(Null) - override fun readOrNull(source: Meta): T? = if(source.value == Null) null else this@nullable.readOrNull(source) + override fun readOrNull(source: Meta): T? = if (source.value == Null) null else this@nullable.readOrNull(source) } @@ -38,4 +38,16 @@ private object InstantConverter : MetaConverter { override fun convert(obj: Instant): Meta = Meta(obj.toString()) } -public val MetaConverter.Companion.instant: MetaConverter get() = InstantConverter \ No newline at end of file +public val MetaConverter.Companion.instant: MetaConverter get() = InstantConverter + +private object DoubleRangeConverter : MetaConverter> { + override fun readOrNull(source: Meta): ClosedFloatingPointRange? = source.value?.doubleArray?.let { (start, end)-> + start..end + } + + override fun convert( + obj: ClosedFloatingPointRange, + ): Meta = Meta(doubleArrayOf(obj.start, obj.endInclusive).asValue()) +} + +public val MetaConverter.Companion.doubleRange: MetaConverter> get() = DoubleRangeConverter \ No newline at end of file diff --git a/controls-core/src/commonTest/kotlin/space/kscience/controls/api/MessageTest.kt b/controls-core/src/commonTest/kotlin/space/kscience/controls/api/MessageTest.kt index 719738a..269b140 100644 --- a/controls-core/src/commonTest/kotlin/space/kscience/controls/api/MessageTest.kt +++ b/controls-core/src/commonTest/kotlin/space/kscience/controls/api/MessageTest.kt @@ -1,9 +1,8 @@ package space.kscience.controls.api -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import space.kscience.controls.spec.asMeta +import space.kscience.controls.misc.asMeta import kotlin.test.Test import kotlin.test.assertEquals diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt index 859bce2..f4a232e 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/DeviceClient.kt @@ -16,8 +16,6 @@ import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.names.Name -import space.kscience.dataforge.names.length -import space.kscience.dataforge.names.startsWith import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.api.send import space.kscience.magix.api.subscribe @@ -121,12 +119,18 @@ public suspend fun MagixEndpoint.remoteDevice( deviceEndpoint: String, deviceName: Name, ): DeviceClient = coroutineScope { - val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint)).map { it.second } + val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint)) + .map { it.second } + .filter { + it.sourceDevice == null || it.sourceDevice == deviceName + } val deferredDescriptorMessage = CompletableDeferred() launch { - deferredDescriptorMessage.complete(subscription.filterIsInstance().first()) + deferredDescriptorMessage.complete( + subscription.filterIsInstance().first() + ) } send( @@ -157,12 +161,6 @@ public suspend fun MagixEndpoint.remoteDevice( } } -private class MapBasedDeviceHub(val deviceMap: Map, val prefix: Name) : DeviceHub { - override val devices: Map - get() = deviceMap.filterKeys { name: Name -> name == prefix || (name.startsWith(prefix) && name.length == prefix.length + 1) } - -} - /** * Create a dynamic [DeviceHub] from incoming messages */ diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt index 5fe99c3..2ffad2a 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt @@ -1,6 +1,7 @@ package space.kscience.controls.client import com.benasher44.uuid.uuid4 +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn @@ -56,7 +57,7 @@ public fun DeviceManager.launchMagixService( ) } }.catch { error -> - logger.error(error) { "Error while responding to message: ${error.message}" } + if (error !is CancellationException) logger.error(error) { "Error while responding to message: ${error.message}" } }.launchIn(this) hubMessageFlow().onEach { payload -> diff --git a/controls-vision/src/commonMain/kotlin/BooleanIndicatorVision.kt b/controls-vision/src/commonMain/kotlin/BooleanIndicatorVision.kt deleted file mode 100644 index 8783228..0000000 --- a/controls-vision/src/commonMain/kotlin/BooleanIndicatorVision.kt +++ /dev/null @@ -1,24 +0,0 @@ -package space.kscience.controls.vision - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import space.kscience.dataforge.meta.boolean -import space.kscience.visionforge.AbstractVision -import space.kscience.visionforge.Vision -import space.kscience.visionforge.html.VisionOfHtml - -/** - * A [Vision] that shows an indicator - */ -@Serializable -@SerialName("controls.indicator") -public class BooleanIndicatorVision : AbstractVision(), VisionOfHtml { - public val isOn: Boolean by properties.boolean(false) -} - -///** -// * A [Vision] that allows both showing the value and changing it -// */ -//public interface RegulatorVision: IndicatorVision{ -// -//} \ No newline at end of file diff --git a/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt b/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt index 6395d53..d9ec746 100644 --- a/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt +++ b/controls-vision/src/commonMain/kotlin/ControlVisionPlugin.kt @@ -13,6 +13,7 @@ public expect class ControlVisionPlugin: VisionPlugin{ internal val controlsVisionSerializersModule = SerializersModule { polymorphic(Vision::class) { - subclass(BooleanIndicatorVision.serializer()) + subclass(IndicatorVision.serializer()) + subclass(SliderVision.serializer()) } } \ No newline at end of file diff --git a/controls-vision/src/commonMain/kotlin/controlsVisions.kt b/controls-vision/src/commonMain/kotlin/controlsVisions.kt new file mode 100644 index 0000000..1b633ed --- /dev/null +++ b/controls-vision/src/commonMain/kotlin/controlsVisions.kt @@ -0,0 +1,35 @@ +package space.kscience.controls.vision + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import space.kscience.controls.misc.doubleRange +import space.kscience.dataforge.meta.MetaConverter +import space.kscience.dataforge.meta.convertable +import space.kscience.dataforge.meta.double +import space.kscience.dataforge.meta.string +import space.kscience.visionforge.AbstractControlVision +import space.kscience.visionforge.AbstractVision +import space.kscience.visionforge.Vision + +/** + * A [Vision] that shows a colored indicator + */ +@Serializable +@SerialName("controls.indicator") +public class IndicatorVision : AbstractVision() { + public val color: String? by properties.string() +} + +@Serializable +@SerialName("controls.slider") +public class SliderVision : AbstractControlVision() { + public var position: Double? by properties.double() + public var range: ClosedFloatingPointRange? by properties.convertable(MetaConverter.doubleRange) +} + +///** +// * A [Vision] that allows both showing the value and changing it +// */ +//public interface RegulatorVision: IndicatorVision{ +// +//} \ No newline at end of file diff --git a/controls-vision/src/commonMain/kotlin/plotExtensions.kt b/controls-vision/src/commonMain/kotlin/plotExtensions.kt index 88b870e..8eeb9a4 100644 --- a/controls-vision/src/commonMain/kotlin/plotExtensions.kt +++ b/controls-vision/src/commonMain/kotlin/plotExtensions.kt @@ -145,7 +145,7 @@ private fun Trace.updateFromState( public fun Plot.plotDeviceState( context: Context, state: DeviceState, - extractValue: T.() -> Value = { state.converter.convert(this).value ?: Null }, + extractValue: (T) -> Value = { Value.of(it) }, maxAge: Duration = defaultMaxAge, maxPoints: Int = defaultMaxPoints, minPoints: Int = defaultMinPoints, diff --git a/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt index d08772c..b966791 100644 --- a/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt +++ b/controls-vision/src/jsMain/kotlin/ControlsVisionPlugin.js.kt @@ -10,7 +10,34 @@ import space.kscience.dataforge.names.asName import space.kscience.visionforge.VisionPlugin import space.kscience.visionforge.html.ElementVisionRenderer -private val indicatorRenderer = ElementVisionRenderer { name, vision: BooleanIndicatorVision, meta -> + +private val indicatorRenderer = ElementVisionRenderer { name, vision: IndicatorVision, meta -> +// val ledSize = vision.properties["size"].int ?: 15 +// val color = vision.color ?: "LightGray" +// div("controls-indicator") { +// style = """ +// +// @keyframes blink { +// 0% { box-shadow: 0 0 10px; } +// 50% { box-shadow: 0 0 30px; } +// 100% { box-shadow: 0 0 10px; } +// } +// +// display: inline-block; +// margin: ${ledSize}px; +// width: ${ledSize}px; +// height: ${ledSize}px; +// border-radius: 50%; +// +// background: $color; +// border: 1px solid darken($color,5%); +// color: $color; +// animation: blink 3s infinite; +// """.trimIndent() +// } +} + +private val sliderRenderer = ElementVisionRenderer { name, vision: SliderVision, meta -> } @@ -22,7 +49,8 @@ public actual class ControlVisionPlugin : VisionPlugin() { override fun content(target: String): Map = when (target) { ElementVisionRenderer.TYPE -> mapOf( - "indicator".asName() to indicatorRenderer + "indicator".asName() to indicatorRenderer, + "slider".asName() to sliderRenderer ) else -> super.content(target) diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt index e76cfe0..a598d4b 100644 --- a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt +++ b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt @@ -64,7 +64,7 @@ class VirtualCarController : Controller(), ContextAware { //mongoStorageJob = deviceManager.storeMessages(DefaultAsynchronousMongoClientFactory) //Launch device client and connect it to the server val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost") - deviceManager.launchMagixService(deviceEndpoint) + deviceManager.launchMagixService(deviceEndpoint, "car") } } diff --git a/demo/constructor/build.gradle.kts b/demo/constructor/build.gradle.kts index 6909639..8628dd1 100644 --- a/demo/constructor/build.gradle.kts +++ b/demo/constructor/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.compose.ExperimentalComposeLibrary import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode @@ -11,12 +12,15 @@ kscience { withJava() } useKtor() + useSerialization() useContextReceivers() - dependencies { - api(projects.controlsVision) + commonMain { + implementation(projects.controlsVision) + implementation(projects.controlsConstructor) +// implementation("io.github.koalaplot:koalaplot-core:0.6.0") } jvmMain { - implementation("io.ktor:ktor-server-cio") +// implementation("io.ktor:ktor-server-cio") implementation(spclibs.logback.classic) } } @@ -26,6 +30,8 @@ kotlin { jvmMain { dependencies { implementation(compose.desktop.currentOs) + @OptIn(ExperimentalComposeLibrary::class) + implementation(compose.desktop.components.splitPane) } } } diff --git a/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt new file mode 100644 index 0000000..bb15c49 --- /dev/null +++ b/demo/constructor/src/jvmMain/kotlin/BodyOnSprings.kt @@ -0,0 +1,185 @@ +package space.kscience.controls.demo.constructor + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +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.serialization.Serializable +import space.kscience.controls.constructor.* +import space.kscience.dataforge.context.Context +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.math.pow +import kotlin.math.sqrt +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit + +@Serializable +data class XY(val x: Double, val y: Double) { + companion object { + val ZERO = XY(0.0, 0.0) + } +} + +operator fun XY.plus(other: XY): XY = XY(x + other.x, y + other.y) +operator fun XY.times(c: Double): XY = XY(x * c, y * c) +operator fun XY.div(c: Double): XY = XY(x / c, y / c) +// +//class XYPosition(context: Context, x0: Double, y0: Double) : DeviceModel(context) { +// val x: MutableDeviceState = mutableState(x0) +// val y: MutableDeviceState = mutableState(y0) +// +// val xy = combineState(x, y) { x, y -> XY(x, y) } +//} + +class Spring( + context: Context, + val k: Double, + val l0: Double, + val begin: DeviceState, + val end: DeviceState, +) : DeviceConstructor(context) { + + val length = combineState(begin, end) { begin, end -> + sqrt((end.y - begin.y).pow(2) + (end.x - begin.x).pow(2)) + } + + val tension: DeviceState = mapState(length) { l -> + val delta = l - l0 + k * delta + } + + /** + * direction from start to end + */ + val direction = combineState(begin, end) { begin, end -> + val dx = end.x - begin.x + val dy = end.y - begin.y + val l = sqrt((end.y - begin.y).pow(2) + (end.x - begin.x).pow(2)) + XY(dx / l, dy / l) + } + + val beginForce = combineState(direction, tension) { direction: XY, tension: Double -> + direction * (tension) + } + + + val endForce = combineState(direction, tension) { direction: XY, tension: Double -> + direction * (-tension) + } +} + +class MaterialPoint( + context: Context, + val mass: Double, + val force: DeviceState, + val position: MutableDeviceState, + val velocity: MutableDeviceState = MutableDeviceState(XY.ZERO), +) : DeviceModel(context, force) { + + private val timer: TimerState = timer(2.milliseconds) + + private val movement = timer.onChange( + position, velocity, + alsoReads = 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 + } +} + + +class BodyOnSprings( + context: Context, + mass: Double, + k: Double, + startPosition: XY, + l0: Double = 1.0, + val xLeft: Double = 0.0, + val xRight: Double = 2.0, + val yBottom: Double = 0.0, + val yTop: Double = 2.0, +) : DeviceConstructor(context) { + + val width = xRight - xLeft + val height = yTop - yBottom + + val position = mutableState(startPosition) + + private val leftAnchor = mutableState(XY(xLeft, yTop + yBottom / 2)) + + val leftSpring by device( + Spring(context, k, l0, leftAnchor, position) + ) + + private val rightAnchor = mutableState(XY(xRight, yTop + yBottom / 2)) + + val rightSpring by device( + Spring(context, k, l0, rightAnchor, position) + ) + + val force: DeviceState = combineState(leftSpring.endForce, rightSpring.endForce) { left, rignt -> + left + rignt + } + + + val body = model( + MaterialPoint( + context = context, + mass = mass, + force = force, + position = position + ) + ) +} + +@Composable +fun DeviceState.collect( + coroutineContext: CoroutineContext = EmptyCoroutineContext, +): State = valueFlow.collectAsState(value, coroutineContext) + +fun main() = application { + val initialState = XY(1.1, 1.1) + + Window(title = "Ball on springs", onCloseRequest = ::exitApplication) { + MaterialTheme { + val context = remember { + Context("simulation") + } + + val model = remember { + BodyOnSprings(context, 100.0, 1000.0, initialState) + } + + val position: XY by model.body.position.collect() + Box(Modifier.size(400.dp)) { + Canvas(modifier = Modifier.fillMaxSize()) { + fun XY.toOffset() = Offset( + (x / model.width * size.width).toFloat(), + (y / model.height * size.height).toFloat() + ) + + drawCircle( + Color.Red, 10f, center = position.toOffset() + ) + drawLine(Color.Blue, model.leftSpring.begin.value.toOffset(), model.leftSpring.end.value.toOffset()) + drawLine( + Color.Blue, + model.rightSpring.begin.value.toOffset(), + model.rightSpring.end.value.toOffset() + ) + } + } + } + } +} \ No newline at end of file diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt index 106b077..b0282d0 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt @@ -12,6 +12,8 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withTimeout import space.kscience.controls.api.DeviceHub import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.misc.asMeta +import space.kscience.controls.misc.duration import space.kscience.controls.ports.AsynchronousPort import space.kscience.controls.ports.KtorTcpPort import space.kscience.controls.ports.send diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 601b9d1..617933a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -79,6 +79,7 @@ visionforge-jupiter = { module = "space.kscience:visionforge-jupyter", version.r visionforge-plotly = { module = "space.kscience:visionforge-plotly", version.ref = "visionforge" } visionforge-markdown = { module = "space.kscience:visionforge-markdown", 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" } # Buildscript