Using specialized VisionChange
This commit is contained in:
parent
8a4779e9c4
commit
a7136d3eff
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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?
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.propertyChange.forEach {(childName,configChange)->
|
||||||
change.children.forEach { (token, child) ->
|
get(childName)?.configure(configChange)
|
||||||
when {
|
}
|
||||||
child is NullVision -> removeChild(token)
|
change.childrenChange.forEach { (name, child) ->
|
||||||
children.containsKey(token) -> children[token]!!.update(child)
|
set(name, child)
|
||||||
else -> attachChild(token, child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
super.update(change)
|
super.update(change)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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")
|
||||||
}
|
}
|
||||||
@ -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
|
* 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")
|
val element = document.body ?: error("Document does not have a body")
|
||||||
fetchVisionsInChildren(element, requestUpdates)
|
fetchVisionsInChildren(element, requestUpdates)
|
||||||
}
|
}
|
@ -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) {
|
||||||
@ -197,10 +198,11 @@ public fun Application.visionModule(context: Context, route: String = DEFAULT_PA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(featureOrNull(CallLogging) == null){
|
if (featureOrNull(CallLogging) == null) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
@ -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)
|
||||||
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 }
|
|
||||||
super.update(change)
|
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 }
|
||||||
}
|
}
|
@ -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) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user