Feedback connection for client.
This commit is contained in:
parent
a87692ae1f
commit
87260cea86
@ -22,6 +22,8 @@
|
||||
- position, rotation and size moved to properties
|
||||
- prototypes moved to children
|
||||
- Immutable Solid instances
|
||||
- Property listeners are not triggered if there are no changes.
|
||||
- Feedback websocket connection in the client.
|
||||
|
||||
### Deprecated
|
||||
|
||||
|
@ -74,11 +74,14 @@ public open class VisionBase(
|
||||
}
|
||||
|
||||
override fun setProperty(name: Name, item: MetaItem?, notify: Boolean) {
|
||||
val oldItem = properties?.getItem(name)
|
||||
if(oldItem!= item) {
|
||||
getOrCreateProperties().setItem(name, item)
|
||||
if (notify) {
|
||||
invalidateProperty(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val descriptor: NodeDescriptor? get() = null
|
||||
|
||||
|
@ -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 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
|
||||
public data class VisionChange(
|
||||
public val void: Boolean = false,
|
||||
public val delete: Boolean = false,
|
||||
public val vision: Vision? = null,
|
||||
@Serializable(MetaSerializer::class) public val properties: Meta? = null,
|
||||
public val children: Map<Name, VisionChange>? = null,
|
||||
|
@ -134,7 +134,7 @@ public open class VisionGroupBase(
|
||||
override fun update(change: VisionChange) {
|
||||
change.children?.forEach { (name, change) ->
|
||||
when {
|
||||
change.void -> set(name, null)
|
||||
change.delete -> set(name, null)
|
||||
change.vision != null -> set(name, change.vision)
|
||||
else -> get(name)?.update(change)
|
||||
}
|
||||
|
@ -39,6 +39,8 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) {
|
||||
public fun decodeFromString(string: String): Vision = jsonFormat.decodeFromString(visionSerializer, string)
|
||||
|
||||
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)
|
||||
|
||||
|
@ -2,6 +2,9 @@ package space.kscience.visionforge
|
||||
|
||||
import kotlinx.browser.document
|
||||
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.url.URL
|
||||
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_FETCH_ATTRIBUTE
|
||||
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_NAME_ATTRIBUTE
|
||||
import kotlin.collections.set
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.time.Duration
|
||||
|
||||
public class VisionClient : AbstractPlugin() {
|
||||
override val tag: PluginTag get() = Companion.tag
|
||||
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
|
||||
@ -53,11 +56,73 @@ public class VisionClient : AbstractPlugin() {
|
||||
|
||||
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) {
|
||||
visionMap[element] = vision
|
||||
val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision")
|
||||
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,12 +144,13 @@ public class VisionClient : AbstractPlugin() {
|
||||
visionManager.decodeFromString(it)
|
||||
}
|
||||
|
||||
if (embeddedVision != null) {
|
||||
when {
|
||||
embeddedVision != null -> {
|
||||
logger.info { "Found embedded vision for output with name $name" }
|
||||
renderVision(element, embeddedVision, outputMeta)
|
||||
renderVision(name, element, embeddedVision, outputMeta)
|
||||
}
|
||||
|
||||
element.attributes[OUTPUT_FETCH_ATTRIBUTE]?.let { attr ->
|
||||
element.attributes[OUTPUT_FETCH_ATTRIBUTE] != null -> {
|
||||
val attr = element.attributes[OUTPUT_FETCH_ATTRIBUTE]!!
|
||||
|
||||
val fetchUrl = if (attr.value.isBlank() || attr.value == "auto") {
|
||||
val endpoint = resolveEndpoint(element)
|
||||
@ -103,61 +169,14 @@ public class VisionClient : AbstractPlugin() {
|
||||
if (response.ok) {
|
||||
response.text().then { text ->
|
||||
val vision = visionManager.decodeFromString(text)
|
||||
renderVision(element, vision, outputMeta)
|
||||
renderVision(name, element, vision, outputMeta)
|
||||
}
|
||||
} else {
|
||||
logger.error { "Failed to fetch initial vision state from $fetchUrl" }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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" }
|
||||
|
||||
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 {
|
||||
console.error("WebSocket message data is not a string")
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,9 @@ import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.websocket.WebSockets
|
||||
import io.ktor.websocket.webSocket
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.stream.createHTML
|
||||
@ -131,6 +133,15 @@ 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")
|
||||
|
||||
launch {
|
||||
incoming.consumeEach {
|
||||
val change = visionManager.jsonFormat.decodeFromString(
|
||||
VisionChange.serializer(), it.data.decodeToString()
|
||||
)
|
||||
vision.update(change)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
withContext(visionManager.context.coroutineContext) {
|
||||
vision.flowChanges(visionManager, Duration.milliseconds(updateInterval)).collect { update ->
|
||||
|
@ -14,17 +14,17 @@ import info.laht.threekt.math.Matrix4
|
||||
import info.laht.threekt.math.Vector3
|
||||
import info.laht.threekt.objects.Mesh
|
||||
|
||||
external class CSG {
|
||||
fun clone(): CSG
|
||||
fun toPolygons(): Array<Polygon>
|
||||
fun toGeometry(toMatrix: Matrix4): BufferGeometry
|
||||
fun union(csg: CSG): CSG
|
||||
fun subtract(csg: CSG): CSG
|
||||
fun intersect(csg: CSG): CSG
|
||||
fun inverse(): CSG
|
||||
public external class CSG {
|
||||
public fun clone(): CSG
|
||||
public fun toPolygons(): Array<Polygon>
|
||||
public fun toGeometry(toMatrix: Matrix4): BufferGeometry
|
||||
public fun union(csg: CSG): CSG
|
||||
public fun subtract(csg: CSG): CSG
|
||||
public fun intersect(csg: CSG): CSG
|
||||
public fun inverse(): CSG
|
||||
|
||||
|
||||
companion object {
|
||||
public companion object {
|
||||
fun fromPolygons(polygons: Array<Polygon>): CSG
|
||||
fun fromGeometry(geom: BufferGeometry, objectIndex: dynamic = definedExternally): CSG
|
||||
fun fromMesh(mesh: Mesh, objectIndex: dynamic = definedExternally): CSG
|
||||
|
Loading…
Reference in New Issue
Block a user