diff --git a/README.md b/README.md index 156e2ae3..fea0692c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ ![Gradle build](https://github.com/mipt-npm/visionforge/workflows/Gradle%20build/badge.svg) +[![Slack](https://img.shields.io/badge/slack-channel-green?logo=slack)](https://kotlinlang.slack.com/archives/CEXV2QWNM) + # DataForge Visualization Platform ## Table of Contents diff --git a/demo/muon-monitor/src/jvmMain/kotlin/ru/mipt/npm/muon/monitor/server/MMServer.kt b/demo/muon-monitor/src/jvmMain/kotlin/ru/mipt/npm/muon/monitor/server/MMServer.kt index b4a55ad5..a4c6354a 100644 --- a/demo/muon-monitor/src/jvmMain/kotlin/ru/mipt/npm/muon/monitor/server/MMServer.kt +++ b/demo/muon-monitor/src/jvmMain/kotlin/ru/mipt/npm/muon/monitor/server/MMServer.kt @@ -27,9 +27,11 @@ import org.apache.commons.math3.random.JDKRandomGenerator import ru.mipt.npm.muon.monitor.Model import ru.mipt.npm.muon.monitor.sim.Cos2TrackGenerator import ru.mipt.npm.muon.monitor.sim.simulateOne +import io.ktor.response.respondText import java.awt.Desktop import java.io.File import java.net.URI +import io.ktor.http.HttpStatusCode private val generator = Cos2TrackGenerator(JDKRandomGenerator(223)) @@ -42,7 +44,7 @@ fun Application.module(context: Context = Global) { install(DefaultHeaders) install(CallLogging) install(ContentNegotiation) { - json(solidManager.visionManager.jsonFormat, ContentType.Application.Json) + json() } install(Routing) { get("/event") { @@ -50,7 +52,11 @@ fun Application.module(context: Context = Global) { call.respond(event) } get("/geometry") { - call.respond(Model.buildGeometry()) + call.respondText( + solidManager.visionManager.encodeToString(Model.buildGeometry()), + contentType = ContentType.Application.Json, + status = HttpStatusCode.OK + ) } static("/") { resources() diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Vision.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Vision.kt index 51110598..5de49992 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Vision.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/Vision.kt @@ -66,7 +66,7 @@ public interface Vision : Configurable, Described { /** * Update this vision using external meta. Children are not updated. */ - public fun update(change: Vision) + public fun update(change: VisionChange) override val descriptor: NodeDescriptor? diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt index 492fee47..e52041fe 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionBase.kt @@ -46,12 +46,13 @@ public open class VisionBase : Vision { * The config is initialized and assigned on-demand. * To avoid unnecessary allocations, one should access [getAllProperties] via [getProperty] instead. */ - override val config: Config - get() = properties ?: Config().also { config -> + override val config: Config by lazy { + properties ?: Config().also { config -> properties = config.also { it.onChange(this) { name, _, _ -> propertyChanged(name) } } } + } @Transient private val listeners = HashSet() @@ -101,9 +102,9 @@ public open class VisionBase : Vision { properties = null } - override fun update(change: Vision) { - if (change.properties != null) { - config.update(change.config) + override fun update(change: VisionChange) { + change.propertyChange[Name.EMPTY]?.let { + config.update(it) } } diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionChange.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionChange.kt new file mode 100644 index 00000000..a51b4497 --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionChange.kt @@ -0,0 +1,123 @@ +package hep.dataforge.vision + +import hep.dataforge.meta.* +import hep.dataforge.names.Name +import hep.dataforge.names.plus +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.* +import kotlin.time.Duration + +/** + * An update for a [Vision] or a [VisionGroup] + */ +public class VisionChangeBuilder: VisionContainerBuilder { + private val propertyChange = HashMap() + private val childrenChange = HashMap() + + public fun isEmpty(): Boolean = propertyChange.isEmpty() && childrenChange.isEmpty() + + public fun propertyChanged(visionName: Name, propertyName: Name, item: MetaItem<*>?) { + propertyChange.getOrPut(visionName) { Config() }.setItem(propertyName, item) + } + + override fun set(name: Name, child: Vision?) { + childrenChange[name] = child + } + + /** + * Isolate collected changes by creating detached copies of given visions + */ + public fun isolate(manager: VisionManager): VisionChange = VisionChange( + propertyChange, + childrenChange.mapValues { it.value?.isolate(manager) } + ) +} + +private fun Vision.isolate(manager: VisionManager): Vision { + //TODO replace by efficient deep copy + val json = manager.encodeToJsonElement(this) + return manager.decodeFromJson(json) +} + +@Serializable +public data class VisionChange( + val propertyChange: Map, + val childrenChange: Map) { + public fun isEmpty(): Boolean = propertyChange.isEmpty() && childrenChange.isEmpty() + + /** + * A shortcut to the top level property dif + */ + public val properties: Meta? get() = propertyChange[Name.EMPTY] +} + +public inline fun VisionChange(manager: VisionManager, block: VisionChangeBuilder.() -> Unit): VisionChange = + VisionChangeBuilder().apply(block).isolate(manager) + + +private fun CoroutineScope.collectChange( + name: Name, + source: Vision, + mutex: Mutex, + collector: ()->VisionChangeBuilder, +) { + //Collect properties change + source.config.onChange(mutex) { propertyName, oldItem, newItem -> + if (oldItem != newItem) { + launch { + mutex.withLock { + collector().propertyChanged(name, propertyName, newItem) + } + } + } + } + + if (source is VisionGroup) { + //Subscribe for children changes + source.children.forEach { (token, child) -> + collectChange(name + token, child, mutex, collector) + } + //TODO update styles? + + //Subscribe for structure change + if (source is MutableVisionGroup) { + source.onStructureChange(mutex) { token, before, after -> + before?.removeChangeListener(mutex) + (before as? MutableVisionGroup)?.removeStructureChangeListener(mutex) + if (after != null) { + collectChange(name + token, after, mutex, collector) + } + collector()[name + token] = after + } + } + } +} + +@DFExperimental +public fun Vision.flowChanges( + manager: VisionManager, + collectionDuration: Duration, + scope: CoroutineScope = manager.context, +): Flow = flow { + val mutex = Mutex() + + var collector = VisionChangeBuilder() + scope.collectChange(Name.EMPTY, this@flowChanges, mutex){collector} + + while (true) { + //Wait for changes to accumulate + kotlinx.coroutines.delay(collectionDuration) + //Propagate updates only if something is changed + if (!collector.isEmpty()) { + //emit changes + emit(collector.isolate(manager)) + //Reset the collector + collector = VisionChangeBuilder() + } + } +} \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroupBase.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroupBase.kt index c7fb11c2..947adafd 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroupBase.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionGroupBase.kt @@ -1,5 +1,6 @@ package hep.dataforge.vision +import hep.dataforge.meta.configure import hep.dataforge.names.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -95,6 +96,9 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup { attachChild(NameToken("@static", index = child.hashCode().toString()), child) } + /** + * Create a vision group of the same type as this vision group. Do not attach it. + */ protected open fun createGroup(): VisionGroupBase = VisionGroupBase() /** @@ -158,22 +162,19 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup { } } - override fun update(change: Vision) { - if (change is VisionGroup) { - //update stylesheet - val changeStyleSheet = change.styleSheet - if (changeStyleSheet != null) { - styleSheet { - update(changeStyleSheet) - } - } - change.children.forEach { (token, child) -> - when { - child is NullVision -> removeChild(token) - children.containsKey(token) -> children[token]!!.update(child) - else -> attachChild(token, child) - } - } + override fun update(change: VisionChange) { + //update stylesheet +// val changeStyleSheet = change.styleSheet +// if (changeStyleSheet != null) { +// styleSheet { +// update(changeStyleSheet) +// } +// } + change.propertyChange.forEach {(childName,configChange)-> + get(childName)?.configure(configChange) + } + change.childrenChange.forEach { (name, child) -> + set(name, child) } super.update(change) } diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt index ffb26ccf..c39eb3d9 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt @@ -25,7 +25,7 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) { } } - private val jsonFormat: Json + public val jsonFormat: Json get() = Json(defaultJson) { serializersModule = this@VisionManager.serializersModule } diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/visionChange.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/visionChange.kt deleted file mode 100644 index 9e8b2ad8..00000000 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/visionChange.kt +++ /dev/null @@ -1,153 +0,0 @@ -package hep.dataforge.vision - -import hep.dataforge.meta.* -import hep.dataforge.meta.descriptors.NodeDescriptor -import hep.dataforge.names.Name -import hep.dataforge.names.isEmpty -import hep.dataforge.names.plus -import hep.dataforge.vision.VisionManager.Companion.visionSerializer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.serialization.* -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlin.time.Duration - - -public abstract class EmptyVision : Vision { - - @Suppress("SetterBackingFieldAssignment", "UNUSED_PARAMETER") - override var parent: VisionGroup? = null - set(value) { - //do nothing - } - - override val properties: Config? = null - - override val allProperties: Laminate - get() = Laminate(Meta.EMPTY) - - override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? = null - - override fun propertyChanged(name: Name) {} - - override fun onPropertyChange(owner: Any?, action: (Name) -> Unit) {} - - override fun removeChangeListener(owner: Any?) {} - - override fun update(change: Vision) { - error("Null vision should be removed, not updated") - } - - override val config: Config get() = Config() - override val descriptor: NodeDescriptor? get() = null -} - -/** - * An empty vision existing only for Vision tree change representation. [NullVision] should not be used outside update logic. - */ -@Serializable -@SerialName("vision.null") -public object NullVision : EmptyVision() - -/** - * Serialization proxy is used to create immutable reference for a given vision - */ -@Serializable(VisionSerializationProxy.Companion::class) -private class VisionSerializationProxy(val ref: Vision) : EmptyVision() { - companion object : KSerializer { - override val descriptor: SerialDescriptor = visionSerializer.descriptor - - @OptIn(ExperimentalSerializationApi::class) - override fun serialize(encoder: Encoder, value: VisionSerializationProxy) { - val serializer = encoder.serializersModule.getPolymorphic(Vision::class, value.ref) - ?: error("The polymorphic serializer is not provided for ") - serializer.serialize(encoder, value.ref) - } - - override fun deserialize(decoder: Decoder): VisionSerializationProxy = - VisionSerializationProxy(visionSerializer.deserialize(decoder)) - } -} - - -private fun MutableVisionGroup.getOrCreate(name: Name): Vision { - if (name.isEmpty()) return this - val existing = get(name) - return existing ?: VisionGroupBase().also { set(name, it) } -} - -private fun CoroutineScope.collectChange( - name: Name, - source: Vision, - mutex: Mutex, - target: () -> MutableVisionGroup, -) { - //Collect properties change - source.config.onChange(mutex){propertyName, oldItem, newItem-> - if(oldItem!= newItem){ - launch { - mutex.withLock { - target().getOrCreate(name).setProperty(propertyName, newItem) - } - } - } - } -// source.onPropertyChange(mutex) { propertyName -> -// launch { -// mutex.withLock { -// target().getOrCreate(name).setProperty(propertyName, source.getProperty(propertyName,false)) -// } -// } -// } - - val targetVision: Vision = target().getOrCreate(name) - - if (source is VisionGroup) { - check(targetVision is MutableVisionGroup) { "Collector for a group should be a group" } - //Subscribe for children changes - source.children.forEach { (token, child) -> - collectChange(name + token, child, mutex, target) - } - //TODO update styles? - - //Subscribe for structure change - if (source is MutableVisionGroup) { - source.onStructureChange(mutex) { token, before, after -> - before?.removeChangeListener(mutex) - (before as? MutableVisionGroup)?.removeStructureChangeListener(mutex) - if (after != null) { - targetVision[token] = VisionSerializationProxy(after) - collectChange(name + token, after, mutex, target) - } else { - targetVision[token] = NullVision - } - } - } - } -} - -@DFExperimental -public fun Vision.flowChanges(scope: CoroutineScope, collectionDuration: Duration): Flow = flow { - val mutex = Mutex() - - var collector = VisionGroupBase() - scope.collectChange(Name.EMPTY, this@flowChanges, mutex) { collector } - - while (true) { - //Wait for changes to accumulate - kotlinx.coroutines.delay(collectionDuration) - //Propagate updates only if something is changed - if (collector.children.isNotEmpty() || collector.properties?.isEmpty() != false) { - //emit changes - emit(collector) - //Reset the collector - collector = VisionGroupBase() - } - } -} \ No newline at end of file diff --git a/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/VisionClient.kt b/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/VisionClient.kt index d689e66f..df61586f 100644 --- a/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/VisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/VisionClient.kt @@ -3,6 +3,7 @@ package hep.dataforge.vision.client import hep.dataforge.context.* import hep.dataforge.meta.Meta import hep.dataforge.vision.Vision +import hep.dataforge.vision.VisionChange import hep.dataforge.vision.VisionManager import hep.dataforge.vision.html.HtmlOutputScope import hep.dataforge.vision.html.HtmlOutputScope.Companion.OUTPUT_ENDPOINT_ATTRIBUTE @@ -36,7 +37,8 @@ public class VisionClient : AbstractPlugin() { private fun getRenderers() = context.gather(ElementVisionRenderer.TYPE).values - public fun findRendererFor(vision: Vision): ElementVisionRenderer? = getRenderers().maxByOrNull { it.rateVision(vision) } + public fun findRendererFor(vision: Vision): ElementVisionRenderer? = + getRenderers().maxByOrNull { it.rateVision(vision) } /** * Fetch from server and render a vision, described in a given with [HtmlOutputScope.OUTPUT_CLASS] class. @@ -71,8 +73,10 @@ public class VisionClient : AbstractPlugin() { onmessage = { messageEvent -> val stringData: String? = messageEvent.data as? String if (stringData != null) { - val update = visionManager.decodeFromString(text) - vision.update(update) +// console.info("Received WS update: $stringData") + val dif = visionManager.jsonFormat + .decodeFromString(VisionChange.serializer(), stringData) + vision.update(dif) } else { console.error("WebSocket message data is not a string") } @@ -121,7 +125,7 @@ public fun VisionClient.fetchVisionsInChildren(element: Element, requestUpdates: /** * Fetch visions from the server for all elements with [HtmlOutputScope.OUTPUT_CLASS] class in the document body */ -public fun VisionClient.fetchAndRenderAllVisions(requestUpdates: Boolean = true){ +public fun VisionClient.fetchAndRenderAllVisions(requestUpdates: Boolean = true) { val element = document.body ?: error("Document does not have a body") fetchVisionsInChildren(element, requestUpdates) } \ No newline at end of file diff --git a/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt b/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt index 5ea07639..33b69d0c 100644 --- a/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt +++ b/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt @@ -8,6 +8,7 @@ import hep.dataforge.meta.long import hep.dataforge.names.Name import hep.dataforge.names.toName import hep.dataforge.vision.Vision +import hep.dataforge.vision.VisionChange import hep.dataforge.vision.VisionManager import hep.dataforge.vision.flowChanges import hep.dataforge.vision.html.* @@ -126,8 +127,8 @@ public class VisionServer internal constructor( application.log.debug("Opened server socket for $name") val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered") try { - vision.flowChanges(this, updateInterval.milliseconds).collect { update -> - val json = visionManager.encodeToString(update) + vision.flowChanges(visionManager, updateInterval.milliseconds).collect { update -> + val json = VisionManager.defaultJson.encodeToString(VisionChange.serializer(), update) outgoing.send(Frame.Text(json)) } } catch (ex: Throwable) { @@ -197,10 +198,11 @@ public fun Application.visionModule(context: Context, route: String = DEFAULT_PA } } - if(featureOrNull(CallLogging) == null){ + if (featureOrNull(CallLogging) == null) { install(CallLogging) } + val visionManager = context.plugins.fetch(VisionManager) routing { route(route) { @@ -210,8 +212,6 @@ public fun Application.visionModule(context: Context, route: String = DEFAULT_PA } } - val visionManager = context.plugins.fetch(VisionManager) - return VisionServer(visionManager, this, route) } diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Composite.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Composite.kt index 566be58a..b7e670d1 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Composite.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/Composite.kt @@ -33,7 +33,7 @@ public class Composite( get() = null } -public inline fun MutableVisionGroup.composite( +public inline fun VisionContainerBuilder.composite( type: CompositeType, name: String = "", builder: SolidGroup.() -> Unit @@ -58,11 +58,11 @@ public inline fun MutableVisionGroup.composite( } } -public inline fun MutableVisionGroup.union(name: String = "", builder: SolidGroup.() -> Unit): Composite = +public inline fun VisionContainerBuilder.union(name: String = "", builder: SolidGroup.() -> Unit): Composite = composite(CompositeType.UNION, name, builder = builder) -public inline fun MutableVisionGroup.subtract(name: String = "", builder: SolidGroup.() -> Unit): Composite = +public inline fun VisionContainerBuilder.subtract(name: String = "", builder: SolidGroup.() -> Unit): Composite = composite(CompositeType.SUBTRACT, name, builder = builder) -public inline fun MutableVisionGroup.intersect(name: String = "", builder: SolidGroup.() -> Unit): Composite = +public inline fun VisionContainerBuilder.intersect(name: String = "", builder: SolidGroup.() -> Unit): Composite = composite(CompositeType.INTERSECT, name, builder = builder) \ No newline at end of file diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidBase.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidBase.kt index f6d836a1..b9471724 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidBase.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidBase.kt @@ -5,8 +5,8 @@ import hep.dataforge.meta.descriptors.NodeDescriptor import hep.dataforge.meta.float import hep.dataforge.meta.get import hep.dataforge.meta.node -import hep.dataforge.vision.Vision import hep.dataforge.vision.VisionBase +import hep.dataforge.vision.VisionChange import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -21,16 +21,20 @@ public open class SolidBase : VisionBase(), Solid { override var scale: Point3D? = null - override fun update(change: Vision) { - fun Meta.toVector(default: Float = 0f) = Point3D( - this[Solid.X_KEY].float ?: default, - this[Solid.Y_KEY].float ?: default, - this[Solid.Z_KEY].float ?: default - ) - - change.properties[Solid.POSITION_KEY].node?.toVector()?.let { position = it } - change.properties[Solid.ROTATION].node?.toVector()?.let { rotation = it } - change.properties[Solid.SCALE_KEY].node?.toVector(1f)?.let { scale = it } + override fun update(change: VisionChange) { + updatePosition(change.properties) super.update(change) } +} + +internal fun Meta.toVector(default: Float = 0f) = Point3D( + this[Solid.X_KEY].float ?: default, + this[Solid.Y_KEY].float ?: default, + this[Solid.Z_KEY].float ?: default +) + +internal fun Solid.updatePosition(meta: Meta?) { + meta[Solid.POSITION_KEY].node?.toVector()?.let { position = it } + meta[Solid.ROTATION].node?.toVector()?.let { rotation = it } + meta[Solid.SCALE_KEY].node?.toVector(1f)?.let { scale = it } } \ No newline at end of file diff --git a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidGroup.kt b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidGroup.kt index 50b53cdc..3b58e11a 100644 --- a/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidGroup.kt +++ b/visionforge-solid/src/commonMain/kotlin/hep/dataforge/vision/solid/SolidGroup.kt @@ -38,7 +38,7 @@ public class SolidGroup : VisionGroupBase(), Solid, PrototypeHolder { /** * Create or edit prototype node as a group */ - public fun prototypes(builder: MutableVisionGroup.() -> Unit): Unit { + public fun prototypes(builder: VisionContainerBuilder.() -> Unit): Unit { (prototypes ?: Prototypes().also { prototypes = it it.parent = this @@ -65,6 +65,10 @@ public class SolidGroup : VisionGroupBase(), Solid, PrototypeHolder { override fun createGroup(): SolidGroup = SolidGroup() + override fun update(change: VisionChange) { + updatePosition(change.properties) + super.update(change) + } public companion object { // val PROTOTYPES_KEY = NameToken("@prototypes") @@ -82,13 +86,13 @@ public fun SolidGroup(block: SolidGroup.() -> Unit): SolidGroup { public tailrec fun PrototypeHolder.getPrototype(name: Name): Solid? = prototypes?.get(name) as? Solid ?: (parent as? PrototypeHolder)?.getPrototype(name) -public fun MutableVisionGroup.group(name: Name = Name.EMPTY, action: SolidGroup.() -> Unit = {}): SolidGroup = +public fun VisionContainerBuilder.group(name: Name = Name.EMPTY, action: SolidGroup.() -> Unit = {}): SolidGroup = SolidGroup().apply(action).also { set(name, it) } /** * Define a group with given [name], attach it to this parent and return it. */ -public fun MutableVisionGroup.group(name: String, action: SolidGroup.() -> Unit = {}): SolidGroup = +public fun VisionContainerBuilder.group(name: String, action: SolidGroup.() -> Unit = {}): SolidGroup = SolidGroup().apply(action).also { set(name, it) } /** diff --git a/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/VisionUpdateTest.kt b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/VisionUpdateTest.kt new file mode 100644 index 00000000..af8e49c7 --- /dev/null +++ b/visionforge-solid/src/commonTest/kotlin/hep/dataforge/vision/solid/VisionUpdateTest.kt @@ -0,0 +1,74 @@ +package hep.dataforge.vision.solid + +import hep.dataforge.context.Global +import hep.dataforge.meta.MetaItem +import hep.dataforge.names.toName +import hep.dataforge.vision.VisionChange +import hep.dataforge.vision.get +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class VisionUpdateTest { + val solidManager = Global.plugins.fetch(SolidManager) + val visionManager = solidManager.visionManager + + @Test + fun testVisionUpdate(){ + val targetVision = SolidGroup { + box(200,200,200, name = "origin") + } + val dif = VisionChange(visionManager){ + group("top") { + color(123) + box(100,100,100) + } + propertyChanged("top".toName(), SolidMaterial.MATERIAL_COLOR_KEY, MetaItem.of("red")) + propertyChanged("origin".toName(), SolidMaterial.MATERIAL_COLOR_KEY, MetaItem.of("red")) + } + targetVision.update(dif) + assertTrue { targetVision["top"] is SolidGroup } + assertEquals("red", (targetVision["origin"] as Solid).color) // Should work + assertEquals("#00007b", (targetVision["top"] as SolidGroup).color) // new item always takes precedence + } + + @Test + fun testVisionChangeSerialization(){ + val change = VisionChange(visionManager){ + group("top") { + color(123) + box(100,100,100) + } + propertyChanged("top".toName(), SolidMaterial.MATERIAL_COLOR_KEY, MetaItem.of("red")) + propertyChanged("origin".toName(), SolidMaterial.MATERIAL_COLOR_KEY, MetaItem.of("red")) + } + val serialized = visionManager.jsonFormat.encodeToString(VisionChange.serializer(), change) + println(serialized) + val reconstructed = visionManager.jsonFormat.decodeFromString(VisionChange.serializer(), serialized) + assertEquals(change.propertyChange,reconstructed.propertyChange) + } + + @Test + fun testDeserialization(){ + val str = """ + { + "propertyChange": { + "layer[4]": { + "material": { + "color": 123 + } + }, + "layer[2]": { + "material": { + } + } + }, + "childrenChange": { + } + } + """.trimIndent() + + val reconstructed = visionManager.jsonFormat.decodeFromString(VisionChange.serializer(), str) + } + +} \ No newline at end of file