v0.2.0-dev-22 #47
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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?
|
||||
|
||||
|
@ -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<PropertyListener>()
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.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>(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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ public class Composite(
|
||||
get() = null
|
||||
}
|
||||
|
||||
public inline fun MutableVisionGroup.composite(
|
||||
public inline fun VisionContainerBuilder<Solid>.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<Solid>.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<Solid>.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<Solid>.intersect(name: String = "", builder: SolidGroup.() -> Unit): Composite =
|
||||
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.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 }
|
||||
}
|
@ -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<Solid>.() -> 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<Vision>.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<Vision>.group(name: String, action: SolidGroup.() -> Unit = {}): SolidGroup =
|
||||
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