Fixed all tests

This commit is contained in:
Alexander Nozik 2024-02-12 10:16:01 +03:00
parent 30ad680688
commit cbf3f4941a
26 changed files with 171 additions and 209 deletions

View File

@ -3,14 +3,14 @@ import space.kscience.gradle.useSPCTeam
plugins {
id("space.kscience.gradle.project")
// id("org.jetbrains.kotlinx.kover") version "0.5.0"
id("org.jetbrains.kotlinx.kover") version "0.5.0"
}
val dataforgeVersion by extra("0.8.0")
allprojects {
group = "space.kscience"
version = "0.4.0-dev-2"
version = "0.4.0-dev-3"
}
subprojects {

View File

@ -1,9 +1,6 @@
package ru.mipt.npm.root
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.set
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.plus
@ -365,7 +362,7 @@ private fun buildVolume(volume: DGeoVolume, context: RootToSolidContext): Solid?
}
return if (group.children.isEmpty()) {
null
} else if (group.items.size == 1 && group.properties.own == null) {
} else if (group.items.size == 1 && group.properties.own.isEmpty()) {
group.items.values.first().apply { parent = null }
} else {
group

View File

@ -7,7 +7,7 @@ import kotlinx.html.h2
import kotlinx.html.p
import space.kscience.visionforge.VisionControlEvent
import space.kscience.visionforge.html.*
import space.kscience.visionforge.onClick
import space.kscience.visionforge.onSubmit
import kotlin.time.Duration.Companion.seconds
@ -59,7 +59,7 @@ fun main() = serve {
vision {
button("Click me") {
onClick(context) {
onSubmit(context) {
pushEvent(this)
}
}

View File

@ -1,8 +1,6 @@
package space.kscience.visionforge
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.*
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
@ -14,16 +12,12 @@ public abstract class AbstractVision : Vision {
override var parent: Vision? = null
@SerialName("properties")
protected var propertiesInternal: MutableMeta? = null
@OptIn(ExperimentalSerializationApi::class)
@EncodeDefault(EncodeDefault.Mode.NEVER)
protected var propertiesInternal: MutableMeta = MutableMeta()
final override val properties: MutableVisionProperties by lazy {
object : AbstractVisionProperties(this) {
override var properties: MutableMeta?
get() = propertiesInternal
set(value) {
propertiesInternal = value
}
}
AbstractVisionProperties(this, propertiesInternal)
}
override val descriptor: MetaDescriptor? get() = null

View File

@ -34,11 +34,11 @@ public interface ControlVision : Vision {
/**
* @param payload The optional payload associated with the click event.
* An event for submitting changes
*/
@Serializable
@SerialName("control.click")
public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() {
@SerialName("control.submit")
public class VisionSubmitEvent(override val meta: Meta) : VisionControlEvent() {
public val payload: Meta get() = meta[::payload.name] ?: Meta.EMPTY
public val name: Name? get() = meta["name"].string?.parseAsName()
@ -46,28 +46,28 @@ public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() {
override fun toString(): String = meta.toString()
}
public fun VisionClickEvent(payload: Meta = Meta.EMPTY, name: Name? = null): VisionClickEvent = VisionClickEvent(
public fun VisionSubmitEvent(payload: Meta = Meta.EMPTY, name: Name? = null): VisionSubmitEvent = VisionSubmitEvent(
Meta {
VisionClickEvent::payload.name put payload
VisionClickEvent::name.name put name.toString()
VisionSubmitEvent::payload.name put payload
VisionSubmitEvent::name.name put name.toString()
}
)
public interface ClickControl : ControlVision {
public interface DataControl : ControlVision {
/**
* Create and dispatch a click event
* Create and dispatch submit event
*/
public suspend fun click(builder: MutableMeta.() -> Unit = {}) {
dispatchControlEvent(VisionClickEvent(Meta(builder)))
public suspend fun submit(builder: MutableMeta.() -> Unit = {}) {
dispatchControlEvent(VisionSubmitEvent(Meta(builder)))
}
}
/**
* Register listener
*/
public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClickEvent.() -> Unit): Job =
controlEventFlow.filterIsInstance<VisionClickEvent>().onEach(block).launchIn(scope)
public fun DataControl.onSubmit(scope: CoroutineScope, block: suspend VisionSubmitEvent.() -> Unit): Job =
controlEventFlow.filterIsInstance<VisionSubmitEvent>().onEach(block).launchIn(scope)
@Serializable

View File

@ -67,7 +67,7 @@ internal fun Vision.styleChanged(key: String, oldStyle: Meta?, newStyle: Meta?)
* List of names of styles applied to this object. Order matters. Not inherited.
*/
public var Vision.styles: List<String>
get() = properties.own?.getValue(Vision.STYLE_KEY)?.stringList ?: emptyList()
get() = properties.own[Vision.STYLE_KEY]?.stringList ?: emptyList()
set(value) {
properties.setValue(Vision.STYLE_KEY, value.map { it.asValue() }.asValue())
}
@ -83,7 +83,7 @@ public val Vision.styleSheet: StyleSheet get() = StyleSheet(this)
* The style with given name does not necessary exist at the moment.
*/
public fun Vision.useStyle(name: String, notify: Boolean = true) {
val newStyle = properties.own?.get(Vision.STYLE_KEY)?.value?.list?.plus(name.asValue()) ?: listOf(name.asValue())
val newStyle = properties.own[Vision.STYLE_KEY]?.value?.list?.plus(name.asValue()) ?: listOf(name.asValue())
properties.setValue(Vision.STYLE_KEY, newStyle.asValue(), notify)
}
@ -92,7 +92,7 @@ public fun Vision.useStyle(name: String, notify: Boolean = true) {
* Resolve a style with given name for given [Vision]. The style is not necessarily applied to this [Vision].
*/
public fun Vision.getStyle(name: String): Meta? =
properties.own?.get(StyleSheet.STYLESHEET_KEY + name) ?: parent?.getStyle(name)
properties.own[StyleSheet.STYLESHEET_KEY + name] ?: parent?.getStyle(name)
/**
* Resolve a property from all styles

View File

@ -1,9 +1,5 @@
package space.kscience.visionforge
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.context.warn
import space.kscience.dataforge.meta.asValue
@ -74,13 +70,3 @@ public var Vision.visible: Boolean?
set(value) {
properties.setValue(Vision.VISIBLE_KEY, value?.asValue())
}
/**
* Subscribe on property updates. The subscription is bound to the given scope and canceled when the scope is canceled
*/
public fun Vision.onPropertyChange(
scope: CoroutineScope,
callback: suspend (Name) -> Unit,
): Job = properties.changes.onEach {
callback(it)
}.launchIn(scope)

View File

@ -147,8 +147,8 @@ private fun CoroutineScope.collectChange(
) {
//Collect properties change
source.properties.changes.onEach { propertyName ->
val newItem = source.properties.own?.get(propertyName)
source.properties.flowChanges().onEach { propertyName ->
val newItem = source.properties.own[propertyName]
collector.propertyChanged(name, propertyName, newItem)
}.launchIn(this)

View File

@ -83,7 +83,7 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionCont
polymorphic(VisionEvent::class) {
subclass(VisionChange.serializer())
subclass(VisionMetaEvent.serializer())
subclass(VisionClickEvent.serializer())
subclass(VisionSubmitEvent.serializer())
subclass(VisionValueChangeEvent.serializer())
subclass(VisionInputEvent.serializer())
}

View File

@ -1,9 +1,8 @@
package space.kscience.visionforge
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.Transient
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
@ -15,7 +14,7 @@ public interface VisionProperties : MetaProvider {
/**
* Raw Visions own properties without styles, defaults, etc.
*/
public val own: Meta?
public val own: Meta
public val descriptor: MetaDescriptor?
@ -41,7 +40,7 @@ public interface VisionProperties : MetaProvider {
override fun get(name: Name): Meta? = get(name, null, null)
public val changes: Flow<Name>
public fun flowChanges(): Flow<Name>
/**
* Notify all listeners that a property has been changed and should be invalidated.
@ -112,7 +111,7 @@ private class VisionPropertiesItem(
override val items: Map<NameToken, MutableMeta>
get() {
val metaKeys = properties.own?.get(nodeName)?.items?.keys ?: emptySet()
val metaKeys = properties.own[nodeName]?.items?.keys ?: emptySet()
val descriptorKeys = descriptor?.nodes?.map { NameToken(it.key) } ?: emptySet()
val defaultKeys = default?.get(nodeName)?.items?.keys ?: emptySet()
val inheritFlag = descriptor?.inherited ?: inherit
@ -158,25 +157,12 @@ private class VisionPropertiesItem(
/**
* A base implementation of [MutableVisionProperties]
*/
public abstract class AbstractVisionProperties(
public open class AbstractVisionProperties(
public val vision: Vision,
final override val own: MutableMeta,
) : MutableVisionProperties {
override val descriptor: MetaDescriptor? get() = vision.descriptor
protected abstract var properties: MutableMeta?
override val own: Meta? get() = properties
@JvmSynchronized
protected fun getOrCreateProperties(): MutableMeta {
if (properties == null) {
//TODO check performance issues
val newProperties = MutableMeta()
properties = newProperties
}
return properties!!
}
private val descriptorCache = HashMap<Name, MetaDescriptor?>()
override fun getValue(
@ -184,7 +170,7 @@ public abstract class AbstractVisionProperties(
inherit: Boolean?,
includeStyles: Boolean?,
): Value? {
own?.get(name)?.value?.let { return it }
own[name]?.value?.let { return it }
val descriptor = descriptor?.let { descriptor -> descriptorCache.getOrPut(name) { descriptor[name] } }
val stylesFlag = includeStyles ?: descriptor?.usesStyles ?: true
@ -202,14 +188,26 @@ public abstract class AbstractVisionProperties(
override fun set(name: Name, node: Meta?, notify: Boolean) {
//ignore if the value is the same as existing
if (own?.get(name) == node) return
if (own[name] == node) return
if (name.isEmpty()) {
properties = node?.asMutableMeta()
if (node == null) {
own.items.keys.forEach {
remove(it.asName())
}
} else {
(own.items.keys - node.items.keys).forEach {
remove(it.asName())
}
node.items.forEach { (token, item) ->
set(token, item)
}
}
} else if (node == null) {
properties?.set(name, node)
own[name] = node
} else {
getOrCreateProperties()[name] = node
own[name] = node
}
if (notify) {
invalidate(name)
@ -218,12 +216,12 @@ public abstract class AbstractVisionProperties(
override fun setValue(name: Name, value: Value?, notify: Boolean) {
//ignore if the value is the same as existing
if (own?.getValue(name) == value) return
if (own.getValue(name) == value) return
if (value == null) {
properties?.get(name)?.value = null
own[name]?.value = null
} else {
getOrCreateProperties().setValue(name, value)
own.setValue(name, value)
}
if (notify) {
invalidate(name)
@ -231,12 +229,20 @@ public abstract class AbstractVisionProperties(
}
@Transient
protected val changesInternal: MutableSharedFlow<Name> = MutableSharedFlow(500, onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val changes: SharedFlow<Name> get() = changesInternal
protected val changesInternal: MutableSharedFlow<Name> = MutableSharedFlow()
override fun flowChanges(): Flow<Name> = changesInternal
override fun invalidate(propertyName: Name) {
//send update signal
changesInternal.tryEmit(propertyName)
val manager = vision.manager
if (manager != null) {
manager.context.launch {
changesInternal.emit(propertyName)
}
} else {
changesInternal.tryEmit(propertyName)
}
//notify children if there are any
if (vision is VisionGroup) {

View File

@ -18,7 +18,7 @@ public fun Vision.flowProperty(
): Flow<Meta> = flow {
//Pass initial value.
emit(properties.get(propertyName, inherit, includeStyles))
properties.changes.collect { name ->
properties.flowChanges().collect { name ->
if (name.startsWith(propertyName)) {
emit(properties.get(propertyName, inherit, includeStyles))
}
@ -41,7 +41,7 @@ public fun Vision.flowPropertyValue(
): Flow<Value?> = flow {
//Pass initial value.
emit(properties.getValue(propertyName, inherit, includeStyles))
properties.changes.collect { name ->
properties.flowChanges().collect { name ->
if (name.startsWith(propertyName)) {
emit(properties.getValue(propertyName, inherit, includeStyles))
}

View File

@ -8,8 +8,8 @@ import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.node
import space.kscience.dataforge.meta.string
import space.kscience.visionforge.ClickControl
import space.kscience.visionforge.onClick
import space.kscience.visionforge.DataControl
import space.kscience.visionforge.onSubmit
/**
* @param formId an id of the element in rendered DOM, this form is bound to
@ -18,7 +18,7 @@ import space.kscience.visionforge.onClick
@SerialName("html.form")
public class VisionOfHtmlForm(
public val formId: String,
) : VisionOfHtmlControl(), ClickControl {
) : VisionOfHtmlControl(), DataControl {
public var values: Meta? by properties.node()
}
@ -40,12 +40,12 @@ public inline fun <T, C : TagConsumer<T>> C.visionOfForm(
}
public fun VisionOfHtmlForm.onSubmit(scope: CoroutineScope, block: (Meta?) -> Unit): Job = onClick(scope) { block(payload) }
public fun VisionOfHtmlForm.onFormSubmit(scope: CoroutineScope, block: (Meta?) -> Unit): Job = onSubmit(scope) { block(payload) }
@Serializable
@SerialName("html.button")
public class VisionOfHtmlButton : VisionOfHtmlControl(), ClickControl {
public class VisionOfHtmlButton : VisionOfHtmlControl(), DataControl {
public var label: String? by properties.string()
}

View File

@ -2,6 +2,7 @@ package space.kscience.visionforge
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.dataforge.meta.Meta
@ -19,12 +20,12 @@ public fun Vision.useProperty(
propertyName: Name,
inherit: Boolean? = null,
includeStyles: Boolean? = null,
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."),
callback: (Meta) -> Unit,
): Job {
//Pass initial value.
callback(properties.get(propertyName, inherit, includeStyles))
return properties.changes.onEach { name ->
return properties.flowChanges().onEach { name ->
if (name.startsWith(propertyName)) {
callback(properties.get(propertyName, inherit, includeStyles))
}
@ -35,33 +36,41 @@ public fun Vision.useProperty(
propertyName: String,
inherit: Boolean? = null,
includeStyles: Boolean? = null,
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."),
callback: (Meta) -> Unit,
): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callback)
public fun <V : Vision, T> V.useProperty(
property: KProperty1<V, T>,
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."),
callback: V.(T) -> Unit,
): Job {
//Pass initial value.
callback(property.get(this))
return properties.flowChanges().onEach { name ->
if (name.startsWith(property.name.asName())) {
callback(property.get(this@useProperty))
}
}.launchIn(scope)
}
/**
* Subscribe on property updates. The subscription is bound to the given scope and canceled when the scope is canceled
*/
public fun Vision.onPropertyChange(
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."),
callback: suspend (Name) -> Unit,
): Job = properties.flowChanges().onEach {
callback(it)
}.launchIn(scope)
/**
* Observe changes to the specific property without passing the initial value.
*/
public fun <V : Vision, T> V.onPropertyChange(
property: KProperty1<V, T>,
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties. Use explicit scope."),
callback: suspend V.(T) -> Unit,
): Job = properties.changes.onEach { name ->
if (name.startsWith(property.name.asName())) {
callback(property.get(this))
}
}.launchIn(scope)
public fun <V : Vision, T> V.useProperty(
property: KProperty1<V, T>,
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
callback: V.(T) -> Unit,
): Job {
//Pass initial value.
): Job = properties.flowChanges().filter { it.startsWith(property.name.asName()) }.onEach {
callback(property.get(this))
return properties.changes.onEach { name ->
if (name.startsWith(property.name.asName())) {
callback(property.get(this@useProperty))
}
}.launchIn(scope)
}
}.launchIn(scope)

View File

@ -55,7 +55,7 @@ class HtmlTagTest {
div {
h2 { +"Properties" }
ul {
vision.properties.own?.items?.forEach {
vision.properties.own.items.forEach {
li {
a { +it.key.toString() }
p { +it.value.toString() }

View File

@ -1,11 +1,10 @@
package space.kscience.visionforge.meta
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.test.runTest
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.request
@ -92,7 +91,7 @@ internal class VisionPropertyTest {
}
@Test
fun testChildrenPropertyFlow() = runTest(timeout = 200.milliseconds) {
fun testChildrenPropertyFlow() = runTest(timeout = 500.milliseconds) {
val group = Global.request(VisionManager).group {
properties {
@ -109,17 +108,32 @@ internal class VisionPropertyTest {
val child = group.children["child"]!!
launch {
val list = child.flowPropertyValue("test", inherit = true).take(3).map { it?.int }.toList()
assertEquals(22, list.first())
//assertEquals(11, list[1]) //a race
assertEquals(33, list.last())
val semaphore = Semaphore(1, 1)
val changesFlow = child.flowPropertyValue("test", inherit = true).map {
semaphore.release()
it!!.int
}
//wait for subscription to be created
delay(5)
val collectedValues = ArrayList<Int>(5)
val collectorJob = changesFlow.onEach {
collectedValues.add(it)
}.launchIn(this)
assertEquals(22, child.properties["test", true].int)
semaphore.acquire()
child.properties.remove("test")
assertEquals(11, child.properties["test", true].int)
semaphore.acquire()
group.properties["test"] = 33
assertEquals(33, child.properties["test", true].int)
semaphore.acquire()
collectorJob.cancel()
assertEquals(listOf(22, 11, 33), collectedValues)
}
}

View File

@ -3,17 +3,11 @@ package space.kscience.visionforge
import kotlinx.dom.clear
import kotlinx.html.TagConsumer
import kotlinx.html.dom.append
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.serializerOrNull
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DfType
import space.kscience.dataforge.misc.Named
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
import kotlin.reflect.KClass
import kotlin.reflect.cast
@ -21,7 +15,7 @@ import kotlin.reflect.cast
* A browser renderer for a [Vision].
*/
@DfType(ElementVisionRenderer.TYPE)
public interface ElementVisionRenderer : Named {
public interface ElementVisionRenderer {
/**
* Give a [vision] integer rating based on this renderer capabilities. [ZERO_RATING] or negative values means that this renderer
@ -58,11 +52,6 @@ public class SingleTypeVisionRenderer<T : Vision>(
private val renderFunction: TagConsumer<HTMLElement>.(name: Name, client: VisionClient, vision: T, meta: Meta) -> Unit,
) : ElementVisionRenderer {
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
override val name: Name
get() = kClass.serializerOrNull()?.descriptor?.serialName?.parseAsName()
?: kClass.toString().asName()
override fun rateVision(vision: Vision): Int =
if (vision::class == kClass) acceptRating else ElementVisionRenderer.ZERO_RATING

View File

@ -21,6 +21,7 @@ import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
import space.kscience.visionforge.html.VisionTagConsumer
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE
@ -262,7 +263,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
rangeVisionRenderer,
formVisionRenderer,
buttonVisionRenderer
).associateByName()
).associateBy { it.toString().asName() }
} else super<AbstractPlugin>.content(target)
public companion object : PluginFactory<JsVisionClient> {
@ -321,7 +322,7 @@ public class VisionClientApplication(public val context: Context) : Application
client.renderers.joinToString(
prefix = "\n\t",
separator = "\n\t"
) { it.name.toString() }
) { it.toString() }
}"
}
val element = document.body ?: error("Document does not have a body")

View File

@ -70,7 +70,7 @@ internal val formVisionRenderer: ElementVisionRenderer =
event.preventDefault()
val formData = FormData(form).toMeta()
client.context.launch {
client.sendEvent(name, VisionClickEvent(name = name, payload = formData))
client.sendEvent(name, VisionSubmitEvent(name = name, payload = formData))
}
console.info("Sent form data: ${formData.toMap()}")
false
@ -83,7 +83,7 @@ internal val buttonVisionRenderer: ElementVisionRenderer =
button.subscribeToVision(vision)
button.onclick = {
client.context.launch {
client.sendEvent(name, VisionClickEvent(name = name))
client.sendEvent(name, VisionSubmitEvent(name = name))
}
}
vision.useProperty(VisionOfHtmlButton::label) {

View File

@ -8,12 +8,16 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.MutableMetaSerializer
import space.kscience.dataforge.meta.ObservableMeta
import space.kscience.dataforge.meta.asObservable
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.names.Name
import space.kscience.plotly.Plot
import space.kscience.plotly.Plotly
import space.kscience.plotly.PlotlyConfig
import space.kscience.visionforge.AbstractVisionProperties
import space.kscience.visionforge.MutableVisionProperties
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionBuilder
@ -32,32 +36,9 @@ public class VisionOfPlotly private constructor(
override var parent: Vision? = null
@Transient
override val properties: MutableVisionProperties = object : MutableVisionProperties {
override fun set(name: Name, node: Meta?, notify: Boolean) {
meta[name] = node
}
override val properties: MutableVisionProperties = object : AbstractVisionProperties(this, meta) {
override fun setValue(name: Name, value: Value?, notify: Boolean) {
meta.setValue(name, value)
}
override val own: Meta get() = meta
override val descriptor: MetaDescriptor? get() = this@VisionOfPlotly.descriptor
override fun get(
name: Name,
inherit: Boolean?,
includeStyles: Boolean?,
): MutableMeta = meta[name] ?: MutableMeta()
override fun getValue(
name: Name,
inherit: Boolean?,
includeStyles: Boolean?,
): Value? = meta.getValue(name)
override val changes: Flow<Name> = if (meta is ObservableMeta) {
override fun flowChanges(): Flow<Name> = if (meta is ObservableMeta) {
callbackFlow {
meta.onChange(this) {
launch {
@ -78,7 +59,7 @@ public class VisionOfPlotly private constructor(
}
override val descriptor: MetaDescriptor? = null // TODO add descriptor for Plot
override val descriptor: MetaDescriptor = Plot.descriptor
}
public fun Plot.asVision(): VisionOfPlotly = VisionOfPlotly(this)

View File

@ -2,6 +2,7 @@ package space.kscience.visionforge.solid
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.isEmpty
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.MutableVisionContainer
import space.kscience.visionforge.VisionBuilder
@ -55,7 +56,7 @@ public fun SolidGroup.smartComposite(
@VisionBuilder builder: SolidGroup.() -> Unit,
): Solid = if (type == CompositeType.GROUP) {
val group = SolidGroup().apply(builder)
if (name == null && group.properties.own == null) {
if (name == null && group.properties.own.isEmpty()) {
//append directly to group if no properties are defined
group.items.forEach { (_, value) ->
value.parent = null

View File

@ -177,7 +177,7 @@ internal fun float32Vector(
): ReadWriteProperty<Solid, Float32Vector3D?> =
object : ReadWriteProperty<Solid, Float32Vector3D?> {
override fun getValue(thisRef: Solid, property: KProperty<*>): Float32Vector3D? {
val item = thisRef.properties.own?.get(name) ?: return null
val item = thisRef.properties.own[name] ?: return null
//using dynamic property accessor because values could change
return object : Float32Vector3D {
override val x: Float get() = item[X_KEY]?.float ?: defaultX

View File

@ -6,9 +6,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.*
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.get
@ -51,21 +49,19 @@ public class SolidReference(
}
override val descriptor: MetaDescriptor get() = prototype.descriptor
@SerialName("properties")
private var propertiesInternal: MutableMeta? = null
@OptIn(ExperimentalSerializationApi::class)
@EncodeDefault(EncodeDefault.Mode.NEVER)
private val propertiesInternal: MutableMeta = MutableMeta()
override val properties: MutableVisionProperties by lazy {
object : AbstractVisionProperties(this) {
override var properties: MutableMeta?
get() = propertiesInternal
set(value) {
propertiesInternal = value
}
object : AbstractVisionProperties(this, propertiesInternal) {
override fun getValue(name: Name, inherit: Boolean?, includeStyles: Boolean?): Value? {
if (name == Vision.STYLE_KEY) {
return buildList {
properties?.getValue(Vision.STYLE_KEY)?.list?.forEach {
own.getValue(Vision.STYLE_KEY)?.list?.forEach {
add(it)
}
prototype.styles.forEach {
@ -74,18 +70,18 @@ public class SolidReference(
}.distinct().asValue()
}
//1. resolve own properties
properties?.getValue(name)?.let { return it }
own.getValue(name)?.let { return it }
val descriptor = descriptor?.get(name)
val inheritFlag = inherit ?: descriptor?.inherited ?: false
val stylesFlag = includeStyles ?: descriptor?.usesStyles ?: true
//2. Resolve prototype onw properties
prototype.properties.own?.getValue(name)?.let { return it }
prototype.properties.own.getValue(name)?.let { return it }
if (stylesFlag) {
//3. own styles
own?.getValue(Vision.STYLE_KEY)?.list?.forEach { styleName ->
own.getValue(Vision.STYLE_KEY)?.list?.forEach { styleName ->
getStyle(styleName.string)?.getValue(name)?.let { return it }
}
//4. prototype styles
@ -178,7 +174,8 @@ internal class SolidReferenceChild(
own.setValue(name, value)
}
override val changes: Flow<Name> get() = owner.properties.changes.filter { it.startsWith(childToken(childName)) }
override fun flowChanges(): Flow<Name> =
owner.properties.flowChanges().filter { it.startsWith(childToken(childName)) }
override fun invalidate(propertyName: Name) {
owner.properties.invalidate(childPropertyName(childName, propertyName))

View File

@ -1,18 +1,17 @@
package space.kscience.visionforge.solid
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import space.kscience.dataforge.meta.getValue
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.set
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.asName
import space.kscience.visionforge.*
import space.kscience.visionforge.getValue
import space.kscience.visionforge.styleSheet
import space.kscience.visionforge.styles
import space.kscience.visionforge.useStyle
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.milliseconds
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("UNUSED_VARIABLE")
@ -29,25 +28,14 @@ class SolidPropertyTest {
}
@Test
fun testColorUpdate() = runTest(timeout = 200.milliseconds) {
fun testColorUpdate() {
val box = Box(10.0f, 10.0f, 10.0f)
val c = CompletableDeferred<String?>()
val subscription = box.onPropertyChange(this) { key ->
if (key == SolidMaterial.MATERIAL_COLOR_KEY) {
c.complete(box.color.string)
}
}
delay(5)
box.material {
color("pink")
}
assertEquals("pink", c.await())
subscription.cancel()
assertEquals("pink", box.properties[SolidMaterial.MATERIAL_COLOR_KEY].string)
}
@Test

View File

@ -133,7 +133,7 @@ internal var Material.cached: Boolean
public fun Mesh.setMaterial(vision: Vision) {
if (
vision.properties.own?.get(SolidMaterial.MATERIAL_KEY) == null
vision.properties.own[SolidMaterial.MATERIAL_KEY] == null
&& vision.getStyleNodes(SolidMaterial.MATERIAL_KEY).isEmpty()
) {
//if this is a reference, use material of the prototype

View File

@ -14,7 +14,6 @@ import space.kscience.visionforge.compose.ComposeVisionRenderer
import space.kscience.visionforge.solid.*
import space.kscience.visionforge.solid.specifications.Canvas3DOptions
import space.kscience.visionforge.solid.three.compose.ThreeView
import space.kscience.visionforge.solid.three.set
import three.core.Object3D
import kotlin.collections.set
import kotlin.reflect.KClass
@ -85,7 +84,7 @@ public class ThreePlugin : AbstractPlugin(), ComposeVisionRenderer {
updatePosition(vision)
//obj.onChildrenChange()
if (observe) {
vision.properties.changes.onEach { name ->
vision.properties.flowChanges().onEach { name ->
if (
name.startsWith(Solid.POSITION_KEY) ||
name.startsWith(Solid.ROTATION_KEY) ||

View File

@ -138,7 +138,7 @@ public fun ThreeView(
scope = context,
rootMeta = vision.properties.root(),
getPropertyState = { name ->
if (vision.properties.own?.get(name) != null) {
if (vision.properties.own[name] != null) {
EditorPropertyState.Defined
} else if (vision.properties.root()[name] != null) {
// TODO differentiate
@ -148,7 +148,7 @@ public fun ThreeView(
}
},
name = Name.EMPTY,
updates = vision.properties.changes,
updates = vision.properties.flowChanges(),
rootDescriptor = vision.descriptor
)
vision.styles.takeIf { it.isNotEmpty() }?.let { styles ->