Feedback connection for client.

This commit is contained in:
Alexander Nozik 2021-07-16 20:52:01 +03:00
parent a87692ae1f
commit 87260cea86
8 changed files with 125 additions and 88 deletions

View File

@ -22,6 +22,8 @@
- position, rotation and size moved to properties - position, rotation and size moved to properties
- prototypes moved to children - prototypes moved to children
- Immutable Solid instances - Immutable Solid instances
- Property listeners are not triggered if there are no changes.
- Feedback websocket connection in the client.
### Deprecated ### Deprecated

View File

@ -74,9 +74,12 @@ public open class VisionBase(
} }
override fun setProperty(name: Name, item: MetaItem?, notify: Boolean) { override fun setProperty(name: Name, item: MetaItem?, notify: Boolean) {
getOrCreateProperties().setItem(name, item) val oldItem = properties?.getItem(name)
if (notify) { if(oldItem!= item) {
invalidateProperty(name) getOrCreateProperties().setItem(name, item)
if (notify) {
invalidateProperty(name)
}
} }
} }

View File

@ -65,14 +65,14 @@ private fun Vision.isolate(manager: VisionManager): Vision {
} }
/** /**
* @param void flag showing that this vision child should be removed * @param delete flag showing that this vision child should be removed
* @param vision a new value for vision content * @param vision a new value for vision content
* @param properties updated properties * @param properties updated properties
* @param children a map of children changed in ths [VisionChange]. If a child to be removed, set [void] flag to true. * @param children a map of children changed in ths [VisionChange]. If a child to be removed, set [delete] flag to true.
*/ */
@Serializable @Serializable
public data class VisionChange( public data class VisionChange(
public val void: Boolean = false, public val delete: Boolean = false,
public val vision: Vision? = null, public val vision: Vision? = null,
@Serializable(MetaSerializer::class) public val properties: Meta? = null, @Serializable(MetaSerializer::class) public val properties: Meta? = null,
public val children: Map<Name, VisionChange>? = null, public val children: Map<Name, VisionChange>? = null,

View File

@ -134,7 +134,7 @@ public open class VisionGroupBase(
override fun update(change: VisionChange) { override fun update(change: VisionChange) {
change.children?.forEach { (name, change) -> change.children?.forEach { (name, change) ->
when { when {
change.void -> set(name, null) change.delete -> set(name, null)
change.vision != null -> set(name, change.vision) change.vision != null -> set(name, change.vision)
else -> get(name)?.update(change) else -> get(name)?.update(change)
} }

View File

@ -39,6 +39,8 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) {
public fun decodeFromString(string: String): Vision = jsonFormat.decodeFromString(visionSerializer, string) public fun decodeFromString(string: String): Vision = jsonFormat.decodeFromString(visionSerializer, string)
public fun encodeToString(vision: Vision): String = jsonFormat.encodeToString(visionSerializer, vision) public fun encodeToString(vision: Vision): String = jsonFormat.encodeToString(visionSerializer, vision)
public fun encodeToString(change: VisionChange): String =
jsonFormat.encodeToString(VisionChange.serializer(), change)
public fun decodeFromJson(json: JsonElement): Vision = jsonFormat.decodeFromJsonElement(visionSerializer, json) public fun decodeFromJson(json: JsonElement): Vision = jsonFormat.decodeFromJsonElement(visionSerializer, json)

View File

@ -2,6 +2,9 @@ package space.kscience.visionforge
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.w3c.dom.* import org.w3c.dom.*
import org.w3c.dom.url.URL import org.w3c.dom.url.URL
import space.kscience.dataforge.context.* import space.kscience.dataforge.context.*
@ -13,14 +16,14 @@ import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNEC
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_ENDPOINT_ATTRIBUTE import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_ENDPOINT_ATTRIBUTE
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_FETCH_ATTRIBUTE import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_FETCH_ATTRIBUTE
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_NAME_ATTRIBUTE import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_NAME_ATTRIBUTE
import kotlin.collections.set
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.time.Duration
public class VisionClient : AbstractPlugin() { public class VisionClient : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag override val tag: PluginTag get() = Companion.tag
private val visionManager: VisionManager by require(VisionManager) private val visionManager: VisionManager by require(VisionManager)
private val visionMap = HashMap<Element, Vision>() //private val visionMap = HashMap<Element, Vision>()
/** /**
* Up-going tree traversal in search for endpoint attribute * Up-going tree traversal in search for endpoint attribute
@ -53,11 +56,73 @@ public class VisionClient : AbstractPlugin() {
private fun Element.getFlag(attribute: String): Boolean = attributes[attribute]?.value != null private fun Element.getFlag(attribute: String): Boolean = attributes[attribute]?.value != null
private fun renderVision(element: Element, vision: Vision?, outputMeta: Meta) { private fun renderVision(name: String, element: Element, vision: Vision?, outputMeta: Meta) {
if (vision != null) { if (vision != null) {
visionMap[element] = vision
val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision") val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision")
renderer.render(element, vision, outputMeta) renderer.render(element, vision, outputMeta)
element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr ->
val wsUrl = if (attr.value.isBlank() || attr.value == "auto") {
val endpoint = resolveEndpoint(element)
logger.info { "Vision server is resolved to $endpoint" }
URL(endpoint).apply {
pathname += "/ws"
}
} else {
URL(attr.value)
}.apply {
protocol = "ws"
searchParams.append("name", name)
}
logger.info { "Updating vision data from $wsUrl" }
//Individual websocket for this element
WebSocket(wsUrl.toString()).apply {
onmessage = { messageEvent ->
val stringData: String? = messageEvent.data as? String
if (stringData != null) {
val change: VisionChange = visionManager.jsonFormat.decodeFromString(
VisionChange.serializer(),
stringData
)
if (change.vision != null) {
renderer.render(element, vision, outputMeta)
}
logger.debug { "Got update $change for output with name $name" }
vision.update(change)
} else {
console.error("WebSocket message data is not a string")
}
}
//Backward change propagation
var feedbackJob: Job? = null
onopen = {
feedbackJob = vision.flowChanges(
visionManager,
Duration.Companion.milliseconds(300)
).onEach { change ->
send(visionManager.encodeToString(change))
}.launchIn(visionManager.context)
console.info("WebSocket update channel established for output '$name'")
}
onclose = {
feedbackJob?.cancel()
console.info("WebSocket update channel closed for output '$name'")
}
onerror = {
feedbackJob?.cancel()
console.error("WebSocket update channel error for output '$name'")
}
}
}
} }
} }
@ -79,85 +144,39 @@ public class VisionClient : AbstractPlugin() {
visionManager.decodeFromString(it) visionManager.decodeFromString(it)
} }
if (embeddedVision != null) { when {
logger.info { "Found embedded vision for output with name $name" } embeddedVision != null -> {
renderVision(element, embeddedVision, outputMeta) logger.info { "Found embedded vision for output with name $name" }
} renderVision(name, element, embeddedVision, outputMeta)
element.attributes[OUTPUT_FETCH_ATTRIBUTE]?.let { attr ->
val fetchUrl = if (attr.value.isBlank() || attr.value == "auto") {
val endpoint = resolveEndpoint(element)
logger.info { "Vision server is resolved to $endpoint" }
URL(endpoint).apply {
pathname += "/vision"
}
} else {
URL(attr.value)
}.apply {
searchParams.append("name", name)
} }
element.attributes[OUTPUT_FETCH_ATTRIBUTE] != null -> {
val attr = element.attributes[OUTPUT_FETCH_ATTRIBUTE]!!
logger.info { "Fetching vision data from $fetchUrl" } val fetchUrl = if (attr.value.isBlank() || attr.value == "auto") {
window.fetch(fetchUrl).then { response -> val endpoint = resolveEndpoint(element)
if (response.ok) { logger.info { "Vision server is resolved to $endpoint" }
response.text().then { text -> URL(endpoint).apply {
val vision = visionManager.decodeFromString(text) pathname += "/vision"
renderVision(element, vision, outputMeta)
} }
} else { } else {
logger.error { "Failed to fetch initial vision state from $fetchUrl" } URL(attr.value)
}.apply {
searchParams.append("name", name)
} }
} logger.info { "Fetching vision data from $fetchUrl" }
} window.fetch(fetchUrl).then { response ->
if (response.ok) {
element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr -> response.text().then { text ->
val wsUrl = if (attr.value.isBlank() || attr.value == "auto") { val vision = visionManager.decodeFromString(text)
val endpoint = resolveEndpoint(element) renderVision(name, element, vision, outputMeta)
logger.info { "Vision server is resolved to $endpoint" }
URL(endpoint).apply {
pathname += "/ws"
}
} else {
URL(attr.value)
}.apply {
protocol = "ws"
searchParams.append("name", name)
}
logger.info { "Updating vision data from $wsUrl" }
WebSocket(wsUrl.toString()).apply {
onmessage = { messageEvent ->
val stringData: String? = messageEvent.data as? String
if (stringData != null) {
val change = visionManager.jsonFormat.decodeFromString(
VisionChange.serializer(),
stringData
)
if (change.vision != null) {
renderVision(element, change.vision, outputMeta)
} }
logger.debug { "Got update $change for output with name $name" }
visionMap[element]?.update(change)
?: console.info("Target vision for element $element with name $name not found")
} else { } else {
console.error("WebSocket message data is not a string") logger.error { "Failed to fetch initial vision state from $fetchUrl" }
} }
} }
onopen = {
console.info("WebSocket update channel established for output '$name'")
}
onclose = {
console.info("WebSocket update channel closed for output '$name'")
}
onerror = {
console.error("WebSocket update channel error for output '$name'")
}
} }
else -> error("No embedded vision data / fetch url for $name")
} }
} }

View File

@ -19,7 +19,9 @@ import io.ktor.server.engine.embeddedServer
import io.ktor.websocket.WebSockets import io.ktor.websocket.WebSockets
import io.ktor.websocket.webSocket import io.ktor.websocket.webSocket
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.stream.createHTML import kotlinx.html.stream.createHTML
@ -131,6 +133,15 @@ 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")
launch {
incoming.consumeEach {
val change = visionManager.jsonFormat.decodeFromString(
VisionChange.serializer(), it.data.decodeToString()
)
vision.update(change)
}
}
try { try {
withContext(visionManager.context.coroutineContext) { withContext(visionManager.context.coroutineContext) {
vision.flowChanges(visionManager, Duration.milliseconds(updateInterval)).collect { update -> vision.flowChanges(visionManager, Duration.milliseconds(updateInterval)).collect { update ->

View File

@ -14,17 +14,17 @@ import info.laht.threekt.math.Matrix4
import info.laht.threekt.math.Vector3 import info.laht.threekt.math.Vector3
import info.laht.threekt.objects.Mesh import info.laht.threekt.objects.Mesh
external class CSG { public external class CSG {
fun clone(): CSG public fun clone(): CSG
fun toPolygons(): Array<Polygon> public fun toPolygons(): Array<Polygon>
fun toGeometry(toMatrix: Matrix4): BufferGeometry public fun toGeometry(toMatrix: Matrix4): BufferGeometry
fun union(csg: CSG): CSG public fun union(csg: CSG): CSG
fun subtract(csg: CSG): CSG public fun subtract(csg: CSG): CSG
fun intersect(csg: CSG): CSG public fun intersect(csg: CSG): CSG
fun inverse(): CSG public fun inverse(): CSG
companion object { public companion object {
fun fromPolygons(polygons: Array<Polygon>): CSG fun fromPolygons(polygons: Array<Polygon>): CSG
fun fromGeometry(geom: BufferGeometry, objectIndex: dynamic = definedExternally): CSG fun fromGeometry(geom: BufferGeometry, objectIndex: dynamic = definedExternally): CSG
fun fromMesh(mesh: Mesh, objectIndex: dynamic = definedExternally): CSG fun fromMesh(mesh: Mesh, objectIndex: dynamic = definedExternally): CSG