Feedback connection for client.
This commit is contained in:
parent
a87692ae1f
commit
87260cea86
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 ->
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user