v0.2.0-dev-22 #47

Merged
altavir merged 158 commits from dev into master 2021-07-17 11:04:22 +03:00
14 changed files with 271 additions and 205 deletions
Showing only changes of commit a7136d3eff - Show all commits

View File

@ -3,6 +3,8 @@
![Gradle build](https://github.com/mipt-npm/visionforge/workflows/Gradle%20build/badge.svg) ![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 # DataForge Visualization Platform
## Table of Contents ## Table of Contents

View File

@ -27,9 +27,11 @@ import org.apache.commons.math3.random.JDKRandomGenerator
import ru.mipt.npm.muon.monitor.Model import ru.mipt.npm.muon.monitor.Model
import ru.mipt.npm.muon.monitor.sim.Cos2TrackGenerator import ru.mipt.npm.muon.monitor.sim.Cos2TrackGenerator
import ru.mipt.npm.muon.monitor.sim.simulateOne import ru.mipt.npm.muon.monitor.sim.simulateOne
import io.ktor.response.respondText
import java.awt.Desktop import java.awt.Desktop
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import io.ktor.http.HttpStatusCode
private val generator = Cos2TrackGenerator(JDKRandomGenerator(223)) private val generator = Cos2TrackGenerator(JDKRandomGenerator(223))
@ -42,7 +44,7 @@ fun Application.module(context: Context = Global) {
install(DefaultHeaders) install(DefaultHeaders)
install(CallLogging) install(CallLogging)
install(ContentNegotiation) { install(ContentNegotiation) {
json(solidManager.visionManager.jsonFormat, ContentType.Application.Json) json()
} }
install(Routing) { install(Routing) {
get("/event") { get("/event") {
@ -50,7 +52,11 @@ fun Application.module(context: Context = Global) {
call.respond(event) call.respond(event)
} }
get("/geometry") { get("/geometry") {
call.respond(Model.buildGeometry()) call.respondText(
solidManager.visionManager.encodeToString(Model.buildGeometry()),
contentType = ContentType.Application.Json,
status = HttpStatusCode.OK
)
} }
static("/") { static("/") {
resources() resources()

View File

@ -66,7 +66,7 @@ public interface Vision : Configurable, Described {
/** /**
* Update this vision using external meta. Children are not updated. * Update this vision using external meta. Children are not updated.
*/ */
public fun update(change: Vision) public fun update(change: VisionChange)
override val descriptor: NodeDescriptor? override val descriptor: NodeDescriptor?

View File

@ -46,12 +46,13 @@ public open class VisionBase : Vision {
* The config is initialized and assigned on-demand. * The config is initialized and assigned on-demand.
* To avoid unnecessary allocations, one should access [getAllProperties] via [getProperty] instead. * To avoid unnecessary allocations, one should access [getAllProperties] via [getProperty] instead.
*/ */
override val config: Config override val config: Config by lazy {
get() = properties ?: Config().also { config -> properties ?: Config().also { config ->
properties = config.also { properties = config.also {
it.onChange(this) { name, _, _ -> propertyChanged(name) } it.onChange(this) { name, _, _ -> propertyChanged(name) }
} }
} }
}
@Transient @Transient
private val listeners = HashSet<PropertyListener>() private val listeners = HashSet<PropertyListener>()
@ -101,9 +102,9 @@ public open class VisionBase : Vision {
properties = null properties = null
} }
override fun update(change: Vision) { override fun update(change: VisionChange) {
if (change.properties != null) { change.propertyChange[Name.EMPTY]?.let {
config.update(change.config) config.update(it)
} }
} }

View File

@ -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<Vision> {
private val propertyChange = HashMap<Name, Config>()
private val childrenChange = HashMap<Name, Vision?>()
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<Name, @Serializable(MetaSerializer::class) Meta>,
val childrenChange: Map<Name, Vision?>) {
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<VisionChange> = 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()
}
}
}

View File

@ -1,5 +1,6 @@
package hep.dataforge.vision package hep.dataforge.vision
import hep.dataforge.meta.configure
import hep.dataforge.names.* import hep.dataforge.names.*
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -95,6 +96,9 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup {
attachChild(NameToken("@static", index = child.hashCode().toString()), child) 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() protected open fun createGroup(): VisionGroupBase = VisionGroupBase()
/** /**
@ -158,22 +162,19 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup {
} }
} }
override fun update(change: Vision) { override fun update(change: VisionChange) {
if (change is VisionGroup) {
//update stylesheet //update stylesheet
val changeStyleSheet = change.styleSheet // val changeStyleSheet = change.styleSheet
if (changeStyleSheet != null) { // if (changeStyleSheet != null) {
styleSheet { // styleSheet {
update(changeStyleSheet) // update(changeStyleSheet)
} // }
} // }
change.children.forEach { (token, child) -> change.propertyChange.forEach {(childName,configChange)->
when { get(childName)?.configure(configChange)
child is NullVision -> removeChild(token)
children.containsKey(token) -> children[token]!!.update(child)
else -> attachChild(token, child)
}
} }
change.childrenChange.forEach { (name, child) ->
set(name, child)
} }
super.update(change) super.update(change)
} }

View File

@ -25,7 +25,7 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) {
} }
} }
private val jsonFormat: Json public val jsonFormat: Json
get() = Json(defaultJson) { get() = Json(defaultJson) {
serializersModule = this@VisionManager.serializersModule serializersModule = this@VisionManager.serializersModule
} }

View File

@ -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<VisionSerializationProxy> {
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<Vision> = 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()
}
}
}

View File

@ -3,6 +3,7 @@ package hep.dataforge.vision.client
import hep.dataforge.context.* import hep.dataforge.context.*
import hep.dataforge.meta.Meta import hep.dataforge.meta.Meta
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionChange
import hep.dataforge.vision.VisionManager import hep.dataforge.vision.VisionManager
import hep.dataforge.vision.html.HtmlOutputScope import hep.dataforge.vision.html.HtmlOutputScope
import hep.dataforge.vision.html.HtmlOutputScope.Companion.OUTPUT_ENDPOINT_ATTRIBUTE import hep.dataforge.vision.html.HtmlOutputScope.Companion.OUTPUT_ENDPOINT_ATTRIBUTE
@ -36,7 +37,8 @@ public class VisionClient : AbstractPlugin() {
private fun getRenderers() = context.gather<ElementVisionRenderer>(ElementVisionRenderer.TYPE).values private fun getRenderers() = context.gather<ElementVisionRenderer>(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. * 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 -> onmessage = { messageEvent ->
val stringData: String? = messageEvent.data as? String val stringData: String? = messageEvent.data as? String
if (stringData != null) { if (stringData != null) {
val update = visionManager.decodeFromString(text) // console.info("Received WS update: $stringData")
vision.update(update) val dif = visionManager.jsonFormat
.decodeFromString(VisionChange.serializer(), stringData)
vision.update(dif)
} else { } else {
console.error("WebSocket message data is not a string") console.error("WebSocket message data is not a string")
} }

View File

@ -8,6 +8,7 @@ import hep.dataforge.meta.long
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.names.toName import hep.dataforge.names.toName
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionChange
import hep.dataforge.vision.VisionManager import hep.dataforge.vision.VisionManager
import hep.dataforge.vision.flowChanges import hep.dataforge.vision.flowChanges
import hep.dataforge.vision.html.* import hep.dataforge.vision.html.*
@ -126,8 +127,8 @@ public class VisionServer internal constructor(
application.log.debug("Opened server socket for $name") application.log.debug("Opened server socket for $name")
val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered") val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered")
try { try {
vision.flowChanges(this, updateInterval.milliseconds).collect { update -> vision.flowChanges(visionManager, updateInterval.milliseconds).collect { update ->
val json = visionManager.encodeToString(update) val json = VisionManager.defaultJson.encodeToString(VisionChange.serializer(), update)
outgoing.send(Frame.Text(json)) outgoing.send(Frame.Text(json))
} }
} catch (ex: Throwable) { } catch (ex: Throwable) {
@ -201,6 +202,7 @@ public fun Application.visionModule(context: Context, route: String = DEFAULT_PA
install(CallLogging) install(CallLogging)
} }
val visionManager = context.plugins.fetch(VisionManager)
routing { routing {
route(route) { 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) return VisionServer(visionManager, this, route)
} }

View File

@ -33,7 +33,7 @@ public class Composite(
get() = null get() = null
} }
public inline fun MutableVisionGroup.composite( public inline fun VisionContainerBuilder<Solid>.composite(
type: CompositeType, type: CompositeType,
name: String = "", name: String = "",
builder: SolidGroup.() -> Unit 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<Solid>.union(name: String = "", builder: SolidGroup.() -> Unit): Composite =
composite(CompositeType.UNION, name, builder = builder) composite(CompositeType.UNION, name, builder = builder)
public inline fun MutableVisionGroup.subtract(name: String = "", builder: SolidGroup.() -> Unit): Composite = public inline fun VisionContainerBuilder<Solid>.subtract(name: String = "", builder: SolidGroup.() -> Unit): Composite =
composite(CompositeType.SUBTRACT, name, builder = builder) composite(CompositeType.SUBTRACT, name, builder = builder)
public inline fun MutableVisionGroup.intersect(name: String = "", builder: SolidGroup.() -> Unit): Composite = public inline fun VisionContainerBuilder<Solid>.intersect(name: String = "", builder: SolidGroup.() -> Unit): Composite =
composite(CompositeType.INTERSECT, name, builder = builder) composite(CompositeType.INTERSECT, name, builder = builder)

View File

@ -5,8 +5,8 @@ import hep.dataforge.meta.descriptors.NodeDescriptor
import hep.dataforge.meta.float import hep.dataforge.meta.float
import hep.dataforge.meta.get import hep.dataforge.meta.get
import hep.dataforge.meta.node import hep.dataforge.meta.node
import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionBase import hep.dataforge.vision.VisionBase
import hep.dataforge.vision.VisionChange
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -21,16 +21,20 @@ public open class SolidBase : VisionBase(), Solid {
override var scale: Point3D? = null override var scale: Point3D? = null
override fun update(change: Vision) { override fun update(change: VisionChange) {
fun Meta.toVector(default: Float = 0f) = Point3D( updatePosition(change.properties)
super.update(change)
}
}
internal fun Meta.toVector(default: Float = 0f) = Point3D(
this[Solid.X_KEY].float ?: default, this[Solid.X_KEY].float ?: default,
this[Solid.Y_KEY].float ?: default, this[Solid.Y_KEY].float ?: default,
this[Solid.Z_KEY].float ?: default this[Solid.Z_KEY].float ?: default
) )
change.properties[Solid.POSITION_KEY].node?.toVector()?.let { position = it } internal fun Solid.updatePosition(meta: Meta?) {
change.properties[Solid.ROTATION].node?.toVector()?.let { rotation = it } meta[Solid.POSITION_KEY].node?.toVector()?.let { position = it }
change.properties[Solid.SCALE_KEY].node?.toVector(1f)?.let { scale = it } meta[Solid.ROTATION].node?.toVector()?.let { rotation = it }
super.update(change) meta[Solid.SCALE_KEY].node?.toVector(1f)?.let { scale = it }
}
} }

View File

@ -38,7 +38,7 @@ public class SolidGroup : VisionGroupBase(), Solid, PrototypeHolder {
/** /**
* Create or edit prototype node as a group * Create or edit prototype node as a group
*/ */
public fun prototypes(builder: MutableVisionGroup.() -> Unit): Unit { public fun prototypes(builder: VisionContainerBuilder<Solid>.() -> Unit): Unit {
(prototypes ?: Prototypes().also { (prototypes ?: Prototypes().also {
prototypes = it prototypes = it
it.parent = this it.parent = this
@ -65,6 +65,10 @@ public class SolidGroup : VisionGroupBase(), Solid, PrototypeHolder {
override fun createGroup(): SolidGroup = SolidGroup() override fun createGroup(): SolidGroup = SolidGroup()
override fun update(change: VisionChange) {
updatePosition(change.properties)
super.update(change)
}
public companion object { public companion object {
// val PROTOTYPES_KEY = NameToken("@prototypes") // val PROTOTYPES_KEY = NameToken("@prototypes")
@ -82,13 +86,13 @@ public fun SolidGroup(block: SolidGroup.() -> Unit): SolidGroup {
public tailrec fun PrototypeHolder.getPrototype(name: Name): Solid? = public tailrec fun PrototypeHolder.getPrototype(name: Name): Solid? =
prototypes?.get(name) as? Solid ?: (parent as? PrototypeHolder)?.getPrototype(name) 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<Vision>.group(name: Name = Name.EMPTY, action: SolidGroup.() -> Unit = {}): SolidGroup =
SolidGroup().apply(action).also { set(name, it) } SolidGroup().apply(action).also { set(name, it) }
/** /**
* Define a group with given [name], attach it to this parent and return 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<Vision>.group(name: String, action: SolidGroup.() -> Unit = {}): SolidGroup =
SolidGroup().apply(action).also { set(name, it) } SolidGroup().apply(action).also { set(name, it) }
/** /**

View File

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