forked from kscience/visionforge
A lot of small refactoring in html
This commit is contained in:
parent
a38d70bade
commit
734d1e1168
@ -18,6 +18,13 @@ kscience{
|
|||||||
val ktorVersion: String by rootProject.extra
|
val ktorVersion: String by rootProject.extra
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
js{
|
||||||
|
browser {
|
||||||
|
webpackTask {
|
||||||
|
this.outputFileName = "visionforge-solid.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
val jsBrowserDistribution by tasks.getting
|
val jsBrowserDistribution by tasks.getting
|
||||||
|
|
||||||
|
@ -6,34 +6,10 @@ import hep.dataforge.vision.client.fetchAndRenderAllVisions
|
|||||||
import hep.dataforge.vision.solid.three.ThreePlugin
|
import hep.dataforge.vision.solid.three.ThreePlugin
|
||||||
import kotlinx.browser.window
|
import kotlinx.browser.window
|
||||||
|
|
||||||
//private class SatDemoApp : Application {
|
|
||||||
//
|
|
||||||
// override fun start(state: Map<String, Any>) {
|
|
||||||
// val element = document.getElementById("canvas") as? HTMLElement
|
|
||||||
// ?: error("Element with id 'canvas' not found on page")
|
|
||||||
// val three = Global.plugins.fetch(ThreePlugin)
|
|
||||||
//
|
|
||||||
// val sat = visionOfSatellite(
|
|
||||||
// ySegments = 3,
|
|
||||||
// )
|
|
||||||
// three.render(element, sat){
|
|
||||||
// minSize = 500
|
|
||||||
// axes{
|
|
||||||
// size = 500.0
|
|
||||||
// visible = true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//fun main() {
|
|
||||||
// startApplication(::SatDemoApp)
|
|
||||||
//}
|
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
//Loading three-js renderer
|
//Loading three-js renderer
|
||||||
Global.plugins.load(ThreePlugin)
|
Global.plugins.load(ThreePlugin)
|
||||||
|
//Fetch from server and render visions for all outputs
|
||||||
window.onload = {
|
window.onload = {
|
||||||
Global.plugins.fetch(VisionClient).fetchAndRenderAllVisions()
|
Global.plugins.fetch(VisionClient).fetchAndRenderAllVisions()
|
||||||
}
|
}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
<title>Three js demo for particle physics</title>
|
|
||||||
<script type="text/javascript" src="sat-demo.js"></script>
|
|
||||||
</head>
|
|
||||||
<body class="application">
|
|
||||||
<div id="canvas"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -2,12 +2,9 @@ package ru.mipt.npm.sat
|
|||||||
|
|
||||||
|
|
||||||
import hep.dataforge.context.Global
|
import hep.dataforge.context.Global
|
||||||
import hep.dataforge.names.asName
|
|
||||||
import hep.dataforge.names.toName
|
import hep.dataforge.names.toName
|
||||||
import hep.dataforge.vision.VisionManager
|
import hep.dataforge.vision.VisionManager
|
||||||
import hep.dataforge.vision.server.close
|
import hep.dataforge.vision.server.*
|
||||||
import hep.dataforge.vision.server.serve
|
|
||||||
import hep.dataforge.vision.server.show
|
|
||||||
import hep.dataforge.vision.solid.Solid
|
import hep.dataforge.vision.solid.Solid
|
||||||
import hep.dataforge.vision.solid.SolidManager
|
import hep.dataforge.vision.solid.SolidManager
|
||||||
import hep.dataforge.vision.solid.color
|
import hep.dataforge.vision.solid.color
|
||||||
@ -15,8 +12,8 @@ import io.ktor.util.KtorExperimentalAPI
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.html.div
|
||||||
import kotlinx.html.h1
|
import kotlinx.html.h1
|
||||||
import kotlinx.html.script
|
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@OptIn(KtorExperimentalAPI::class)
|
@OptIn(KtorExperimentalAPI::class)
|
||||||
@ -30,27 +27,27 @@ fun main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val server = context.plugins.fetch(VisionManager).serve {
|
val server = context.plugins.fetch(VisionManager).serve {
|
||||||
header {
|
useScript("visionforge-solid.js")
|
||||||
script {
|
useCss("css/styles.css")
|
||||||
src = "sat-demo.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
page {
|
page {
|
||||||
h1 { +"Satellite detector demo" }
|
div("flex-column") {
|
||||||
vision("main".asName(), sat)
|
h1 { +"Satellite detector demo" }
|
||||||
}
|
vision(sat)
|
||||||
launch {
|
|
||||||
delay(1000)
|
|
||||||
while (isActive) {
|
|
||||||
val target = "layer[${Random.nextInt(1,10)}].segment[${Random.nextInt(3)},${Random.nextInt(3)}]".toName()
|
|
||||||
(sat[target] as? Solid)?.color("red")
|
|
||||||
delay(300)
|
|
||||||
(sat[target] as? Solid)?.color = "green"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
server.show()
|
server.show()
|
||||||
|
|
||||||
|
context.launch {
|
||||||
|
while (isActive) {
|
||||||
|
val target = "layer[${Random.nextInt(1,11)}].segment[${Random.nextInt(3)},${Random.nextInt(3)}]".toName()
|
||||||
|
(sat[target] as? Solid)?.color("red")
|
||||||
|
delay(300)
|
||||||
|
(sat[target] as? Solid)?.color = "green"
|
||||||
|
delay(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
println("Press Enter to close server")
|
println("Press Enter to close server")
|
||||||
while (readLine()!="exit"){
|
while (readLine()!="exit"){
|
||||||
//
|
//
|
||||||
|
16
demo/sat-demo/src/jvmMain/resources/css/styles.css
Normal file
16
demo/sat-demo/src/jvmMain/resources/css/styles.css
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
body{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-column{
|
||||||
|
width: calc(100% - 15px);
|
||||||
|
height: calc(100% - 15px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visionforge-output{
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
@ -16,12 +16,12 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
api("hep.dataforge:dataforge-context:$dataforgeVersion")
|
api("hep.dataforge:dataforge-context:$dataforgeVersion")
|
||||||
api("org.jetbrains.kotlinx:kotlinx-html:$htmlVersion")
|
api("org.jetbrains.kotlinx:kotlinx-html:$htmlVersion")
|
||||||
|
api("org.jetbrains:kotlin-css:1.0.0-$kotlinWrappersVersion")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsMain {
|
jsMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api("org.jetbrains:kotlin-extensions:1.0.1-$kotlinWrappersVersion")
|
api("org.jetbrains:kotlin-extensions:1.0.1-$kotlinWrappersVersion")
|
||||||
api("org.jetbrains:kotlin-css:1.0.0-$kotlinWrappersVersion")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ public class VisionChangeBuilder : VisionContainerBuilder<Vision> {
|
|||||||
propertyChange,
|
propertyChange,
|
||||||
childrenChange.mapValues { it.value?.isolate(manager) }
|
childrenChange.mapValues { it.value?.isolate(manager) }
|
||||||
)
|
)
|
||||||
|
//TODO optimize isolation for visions without parents?
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Vision.isolate(manager: VisionManager): Vision {
|
private fun Vision.isolate(manager: VisionManager): Vision {
|
||||||
@ -77,12 +78,15 @@ private fun CoroutineScope.collectChange(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
coroutineContext[Job]?.invokeOnCompletion {
|
||||||
|
source.config.removeListener(mutex)
|
||||||
|
}
|
||||||
|
|
||||||
if (source is VisionGroup) {
|
if (source is VisionGroup) {
|
||||||
//Subscribe for children changes
|
//Subscribe for children changes
|
||||||
source.children.forEach { (token, child) ->
|
source.children.forEach { (token, child) ->
|
||||||
collectChange(name + token, child, mutex, collector)
|
collectChange(name + token, child, mutex, collector)
|
||||||
}
|
}
|
||||||
//TODO update styles?
|
|
||||||
|
|
||||||
//Subscribe for structure change
|
//Subscribe for structure change
|
||||||
if (source is MutableVisionGroup) {
|
if (source is MutableVisionGroup) {
|
||||||
@ -94,6 +98,9 @@ private fun CoroutineScope.collectChange(
|
|||||||
}
|
}
|
||||||
collector()[name + token] = after
|
collector()[name + token] = after
|
||||||
}
|
}
|
||||||
|
coroutineContext[Job]?.invokeOnCompletion {
|
||||||
|
source.removeStructureChangeListener(mutex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,23 +109,24 @@ private fun CoroutineScope.collectChange(
|
|||||||
public fun Vision.flowChanges(
|
public fun Vision.flowChanges(
|
||||||
manager: VisionManager,
|
manager: VisionManager,
|
||||||
collectionDuration: Duration,
|
collectionDuration: Duration,
|
||||||
scope: CoroutineScope = manager.context,
|
|
||||||
): Flow<VisionChange> = flow {
|
): Flow<VisionChange> = flow {
|
||||||
val mutex = Mutex()
|
supervisorScope {
|
||||||
|
val mutex = Mutex()
|
||||||
|
|
||||||
var collector = VisionChangeBuilder()
|
var collector = VisionChangeBuilder()
|
||||||
scope.collectChange(Name.EMPTY, this@flowChanges, mutex) { collector }
|
collectChange(Name.EMPTY, this@flowChanges, mutex) { collector }
|
||||||
|
|
||||||
while (currentCoroutineContext().isActive) {
|
while (currentCoroutineContext().isActive) {
|
||||||
//Wait for changes to accumulate
|
//Wait for changes to accumulate
|
||||||
delay(collectionDuration)
|
delay(collectionDuration)
|
||||||
//Propagate updates only if something is changed
|
//Propagate updates only if something is changed
|
||||||
if (!collector.isEmpty()) {
|
if (!collector.isEmpty()) {
|
||||||
//emit changes
|
//emit changes
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
emit(collector.isolate(manager))
|
emit(collector.isolate(manager))
|
||||||
//Reset the collector
|
//Reset the collector
|
||||||
collector = VisionChangeBuilder()
|
collector = VisionChangeBuilder()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package hep.dataforge.vision.html
|
package hep.dataforge.vision.html
|
||||||
|
|
||||||
|
import hep.dataforge.meta.Meta
|
||||||
import hep.dataforge.names.Name
|
import hep.dataforge.names.Name
|
||||||
import hep.dataforge.vision.Vision
|
import hep.dataforge.vision.Vision
|
||||||
import kotlinx.html.FlowContent
|
import kotlinx.html.FlowContent
|
||||||
@ -13,7 +14,7 @@ public class BindingOutputTagConsumer<T, V : Vision>(
|
|||||||
private val _bindings = HashMap<Name, V>()
|
private val _bindings = HashMap<Name, V>()
|
||||||
public val bindings: Map<Name, V> get() = _bindings
|
public val bindings: Map<Name, V> get() = _bindings
|
||||||
|
|
||||||
override fun FlowContent.renderVision(name: Name, vision: V) {
|
override fun FlowContent.renderVision(name: Name, vision: V, outputMeta: Meta) {
|
||||||
_bindings[name] = vision
|
_bindings[name] = vision
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
package hep.dataforge.vision.html
|
package hep.dataforge.vision.html
|
||||||
|
|
||||||
|
import hep.dataforge.meta.*
|
||||||
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.VisionManager
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An HTML div wrapper that includes the output [name] and inherited [render] function
|
* A placeholder object to attach inline vision builders.
|
||||||
*/
|
*/
|
||||||
public class OutputDiv<in V : Vision>(
|
@DFExperimental
|
||||||
private val div: DIV,
|
public class VisionOutput {
|
||||||
public val name: Name,
|
public var meta: Meta = Meta.EMPTY
|
||||||
public val render: (V) -> Unit,
|
|
||||||
) : HtmlBlockTag by div
|
public inline fun meta(block: MetaBuilder.() -> Unit) {
|
||||||
|
this.meta = Meta(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modified [TagConsumer] that allows rendering output fragments and visions in them
|
* Modified [TagConsumer] that allows rendering output fragments and visions in them
|
||||||
@ -26,34 +32,55 @@ public abstract class OutputTagConsumer<R, V : Vision>(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a vision inside the output fragment
|
* Render a vision inside the output fragment
|
||||||
|
* @param name name of the output container
|
||||||
|
* @param vision an object to be rendered
|
||||||
|
* @param outputMeta optional configuration for the output container
|
||||||
*/
|
*/
|
||||||
protected abstract fun FlowContent.renderVision(name: Name, vision: V)
|
protected abstract fun FlowContent.renderVision(name: Name, vision: V, outputMeta: Meta)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a placeholder for an output window
|
* Create a placeholder for a vision output with optional [Vision] in it
|
||||||
*/
|
*/
|
||||||
public fun <T> TagConsumer<T>.visionOutput(
|
public fun <T> TagConsumer<T>.vision(
|
||||||
name: Name,
|
name: Name,
|
||||||
block: OutputDiv<V>.() -> Unit = {},
|
vision: V? = null,
|
||||||
|
outputMeta: Meta = Meta.EMPTY,
|
||||||
): T = div {
|
): T = div {
|
||||||
id = resolveId(name)
|
id = resolveId(name)
|
||||||
classes = setOf(OUTPUT_CLASS)
|
classes = setOf(OUTPUT_CLASS)
|
||||||
attributes[OUTPUT_NAME_ATTRIBUTE] = name.toString()
|
attributes[OUTPUT_NAME_ATTRIBUTE] = name.toString()
|
||||||
OutputDiv<V>(this, name) { renderVision(name, it) }.block()
|
if (!outputMeta.isEmpty()) {
|
||||||
}
|
//Hard-code output configuration
|
||||||
|
script {
|
||||||
public fun <T> TagConsumer<T>.visionOutput(
|
attributes["class"] = OUTPUT_META_CLASS
|
||||||
name: String,
|
unsafe {
|
||||||
block: OutputDiv<V>.() -> Unit = {},
|
+VisionManager.defaultJson.encodeToString(MetaSerializer, outputMeta)
|
||||||
): T = visionOutput(name.toName(), block)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public fun <T> TagConsumer<T>.vision(name: Name, vision: V): Unit {
|
|
||||||
visionOutput(name) {
|
|
||||||
render(vision)
|
|
||||||
}
|
}
|
||||||
|
vision?.let { renderVision(name, it, outputMeta) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(DFExperimental::class)
|
||||||
|
public inline fun <T> TagConsumer<T>.vision(
|
||||||
|
name: Name,
|
||||||
|
visionProvider: VisionOutput.() -> V,
|
||||||
|
): T {
|
||||||
|
val output = VisionOutput()
|
||||||
|
val vision = output.visionProvider()
|
||||||
|
return vision(name, vision, output.meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(DFExperimental::class)
|
||||||
|
public inline fun <T> TagConsumer<T>.vision(
|
||||||
|
name: String,
|
||||||
|
visionProvider: VisionOutput.() -> V,
|
||||||
|
): T = vision(name.toName(), visionProvider)
|
||||||
|
|
||||||
|
public inline fun <T> TagConsumer<T>.vision(
|
||||||
|
vision: V,
|
||||||
|
): T = vision("vision[${vision.hashCode()}]".toName(), vision)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the resulting object produced by [TagConsumer]
|
* Process the resulting object produced by [TagConsumer]
|
||||||
*/
|
*/
|
||||||
@ -67,6 +94,9 @@ public abstract class OutputTagConsumer<R, V : Vision>(
|
|||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
public const val OUTPUT_CLASS: String = "visionforge-output"
|
public const val OUTPUT_CLASS: String = "visionforge-output"
|
||||||
|
public const val OUTPUT_META_CLASS: String = "visionforge-output-meta"
|
||||||
|
public const val OUTPUT_DATA_CLASS: String = "visionforge-output-data"
|
||||||
|
|
||||||
public const val OUTPUT_NAME_ATTRIBUTE: String = "data-output-name"
|
public const val OUTPUT_NAME_ATTRIBUTE: String = "data-output-name"
|
||||||
public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint"
|
public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint"
|
||||||
public const val DEFAULT_ENDPOINT: String = "."
|
public const val DEFAULT_ENDPOINT: String = "."
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
package hep.dataforge.vision.html
|
package hep.dataforge.vision.html
|
||||||
|
|
||||||
|
import hep.dataforge.meta.Meta
|
||||||
import hep.dataforge.names.Name
|
import hep.dataforge.names.Name
|
||||||
import hep.dataforge.vision.Vision
|
import hep.dataforge.vision.Vision
|
||||||
|
import hep.dataforge.vision.VisionManager
|
||||||
import kotlinx.html.FlowContent
|
import kotlinx.html.FlowContent
|
||||||
import kotlinx.html.TagConsumer
|
import kotlinx.html.TagConsumer
|
||||||
|
import kotlinx.html.script
|
||||||
import kotlinx.html.stream.createHTML
|
import kotlinx.html.stream.createHTML
|
||||||
|
import kotlinx.html.unsafe
|
||||||
|
|
||||||
public typealias HtmlVisionRenderer<V> = FlowContent.(V) -> Unit
|
public typealias HtmlVisionRenderer<V> = FlowContent.(V, Meta) -> Unit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [OutputTagConsumer] that directly renders given [Vision] using provided [renderer]
|
* An [OutputTagConsumer] that directly renders given [Vision] using provided [renderer]
|
||||||
@ -16,8 +20,18 @@ public class StaticOutputTagConsumer<R, V : Vision>(
|
|||||||
prefix: String? = null,
|
prefix: String? = null,
|
||||||
private val renderer: HtmlVisionRenderer<V>,
|
private val renderer: HtmlVisionRenderer<V>,
|
||||||
) : OutputTagConsumer<R, V>(root, prefix) {
|
) : OutputTagConsumer<R, V>(root, prefix) {
|
||||||
|
override fun FlowContent.renderVision(name: Name, vision: V, outputMeta: Meta): Unit = renderer(vision, outputMeta)
|
||||||
|
|
||||||
override fun FlowContent.renderVision(name: Name, vision: V): Unit = renderer(vision)
|
public companion object {
|
||||||
|
public fun embed(manager: VisionManager): HtmlVisionRenderer<Vision> = { vision: Vision, _: Meta ->
|
||||||
|
script {
|
||||||
|
attributes["class"] = OUTPUT_DATA_CLASS
|
||||||
|
unsafe {
|
||||||
|
+manager.encodeToString(vision)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <T : Any> HtmlVisionFragment<Vision>.renderToObject(
|
public fun <T : Any> HtmlVisionFragment<Vision>.renderToObject(
|
||||||
@ -26,5 +40,21 @@ public fun <T : Any> HtmlVisionFragment<Vision>.renderToObject(
|
|||||||
renderer: HtmlVisionRenderer<Vision>,
|
renderer: HtmlVisionRenderer<Vision>,
|
||||||
): T = StaticOutputTagConsumer(root, prefix, renderer).apply(content).finalize()
|
): T = StaticOutputTagConsumer(root, prefix, renderer).apply(content).finalize()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an object to HTML embedding the data as script bodies
|
||||||
|
*/
|
||||||
|
public fun <T : Any> HtmlVisionFragment<Vision>.embedToObject(
|
||||||
|
manager: VisionManager,
|
||||||
|
root: TagConsumer<T>,
|
||||||
|
prefix: String? = null,
|
||||||
|
): T = renderToObject(root, prefix, StaticOutputTagConsumer.embed(manager))
|
||||||
|
|
||||||
public fun HtmlVisionFragment<Vision>.renderToString(renderer: HtmlVisionRenderer<Vision>): String =
|
public fun HtmlVisionFragment<Vision>.renderToString(renderer: HtmlVisionRenderer<Vision>): String =
|
||||||
renderToObject(createHTML(), null, renderer)
|
renderToObject(createHTML(), null, renderer)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a fragment to a string, embedding all visions data
|
||||||
|
*/
|
||||||
|
public fun HtmlVisionFragment<Vision>.embedToString(manager: VisionManager): String =
|
||||||
|
embedToObject(manager, createHTML())
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package hep.dataforge.vision.html
|
package hep.dataforge.vision.html
|
||||||
|
|
||||||
|
import hep.dataforge.meta.DFExperimental
|
||||||
import hep.dataforge.meta.configure
|
import hep.dataforge.meta.configure
|
||||||
import hep.dataforge.meta.set
|
import hep.dataforge.meta.set
|
||||||
import hep.dataforge.vision.Vision
|
import hep.dataforge.vision.Vision
|
||||||
@ -10,14 +11,18 @@ import kotlin.test.Test
|
|||||||
|
|
||||||
class HtmlTagTest {
|
class HtmlTagTest {
|
||||||
|
|
||||||
fun OutputDiv<Vision>.visionBase(block: VisionBase.() -> Unit) =
|
@OptIn(DFExperimental::class)
|
||||||
render(VisionBase().apply(block))
|
fun VisionOutput.base(block: VisionBase.() -> Unit) =
|
||||||
|
VisionBase().apply(block)
|
||||||
|
|
||||||
val fragment = buildVisionFragment {
|
val fragment = buildVisionFragment {
|
||||||
div {
|
div {
|
||||||
h1 { +"Head" }
|
h1 { +"Head" }
|
||||||
visionOutput("ddd") {
|
vision("ddd") {
|
||||||
visionBase {
|
meta{
|
||||||
|
"metaProperty" put 87
|
||||||
|
}
|
||||||
|
base {
|
||||||
configure {
|
configure {
|
||||||
set("myProp", 82)
|
set("myProp", 82)
|
||||||
set("otherProp", false)
|
set("otherProp", false)
|
||||||
@ -27,7 +32,7 @@ class HtmlTagTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val simpleVisionRenderer: HtmlVisionRenderer<Vision> = { vision ->
|
val simpleVisionRenderer: HtmlVisionRenderer<Vision> = { vision, _ ->
|
||||||
div {
|
div {
|
||||||
h2 { +"Properties" }
|
h2 { +"Properties" }
|
||||||
ul {
|
ul {
|
||||||
@ -41,7 +46,7 @@ class HtmlTagTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val groupRenderer: HtmlVisionRenderer<VisionGroup> = { group ->
|
val groupRenderer: HtmlVisionRenderer<VisionGroup> = { group, _ ->
|
||||||
p { +"This is group" }
|
p { +"This is group" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,8 +2,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.meta.get
|
import hep.dataforge.meta.MetaSerializer
|
||||||
import hep.dataforge.meta.node
|
|
||||||
import hep.dataforge.vision.Vision
|
import hep.dataforge.vision.Vision
|
||||||
import hep.dataforge.vision.VisionChange
|
import hep.dataforge.vision.VisionChange
|
||||||
import hep.dataforge.vision.VisionManager
|
import hep.dataforge.vision.VisionManager
|
||||||
@ -42,16 +41,31 @@ public class VisionClient : AbstractPlugin() {
|
|||||||
public fun findRendererFor(vision: Vision): ElementVisionRenderer? =
|
public fun findRendererFor(vision: Vision): ElementVisionRenderer? =
|
||||||
getRenderers().maxByOrNull { it.rateVision(vision) }
|
getRenderers().maxByOrNull { it.rateVision(vision) }
|
||||||
|
|
||||||
|
private fun Element.getEmbeddedData(className: String): String? = getElementsByClassName(className)[0]?.innerHTML
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch from server and render a vision, described in a given with [OutputTagConsumer.OUTPUT_CLASS] class.
|
* Fetch from server and render a vision, described in a given with [OutputTagConsumer.OUTPUT_CLASS] class.
|
||||||
*/
|
*/
|
||||||
public fun fetchAndRenderVision(element: Element, requestUpdates: Boolean = true) {
|
public fun renderVisionAt(element: Element, requestUpdates: Boolean = true) {
|
||||||
val name = resolveName(element) ?: error("The element is not a vision output")
|
val name = resolveName(element) ?: error("The element is not a vision output")
|
||||||
console.info("Found DF output with name $name")
|
console.info("Found DF output with name $name")
|
||||||
if (!element.classList.contains(OutputTagConsumer.OUTPUT_CLASS)) error("The element $element is not an output element")
|
if (!element.classList.contains(OutputTagConsumer.OUTPUT_CLASS)) error("The element $element is not an output element")
|
||||||
val endpoint = resolveEndpoint(element)
|
val endpoint = resolveEndpoint(element)
|
||||||
console.info("Vision server is resolved to $endpoint")
|
console.info("Vision server is resolved to $endpoint")
|
||||||
|
|
||||||
|
val outputMeta = element.getEmbeddedData(OutputTagConsumer.OUTPUT_META_CLASS)?.let {
|
||||||
|
VisionManager.defaultJson.decodeFromString(MetaSerializer, it)
|
||||||
|
} ?: Meta.EMPTY
|
||||||
|
|
||||||
|
//Trying to render embedded vision
|
||||||
|
val embeddedVision = element.getEmbeddedData(OutputTagConsumer.OUTPUT_DATA_CLASS)?.let {
|
||||||
|
visionManager.decodeFromString(it)
|
||||||
|
}
|
||||||
|
if (embeddedVision != null) {
|
||||||
|
val renderer = findRendererFor(embeddedVision) ?: error("Could nof find renderer for $embeddedVision")
|
||||||
|
renderer.render(element, embeddedVision, outputMeta)
|
||||||
|
}
|
||||||
|
|
||||||
val fetchUrl = URL(endpoint).apply {
|
val fetchUrl = URL(endpoint).apply {
|
||||||
searchParams.append("name", name)
|
searchParams.append("name", name)
|
||||||
pathname += "/vision"
|
pathname += "/vision"
|
||||||
@ -63,15 +77,14 @@ public class VisionClient : AbstractPlugin() {
|
|||||||
response.text().then { text ->
|
response.text().then { text ->
|
||||||
val vision = visionManager.decodeFromString(text)
|
val vision = visionManager.decodeFromString(text)
|
||||||
val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision")
|
val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision")
|
||||||
val rendererConfiguration = vision.properties[RENDERER_CONFIGURATION_META_KEY].node ?: Meta.EMPTY
|
renderer.render(element, vision, outputMeta)
|
||||||
renderer.render(element, vision, rendererConfiguration)
|
|
||||||
if (requestUpdates) {
|
if (requestUpdates) {
|
||||||
val wsUrl = URL(endpoint).apply {
|
val wsUrl = URL(endpoint).apply {
|
||||||
pathname += "/ws"
|
pathname += "/ws"
|
||||||
protocol = "ws"
|
protocol = "ws"
|
||||||
searchParams.append("name", name)
|
searchParams.append("name", name)
|
||||||
}
|
}
|
||||||
val ws = WebSocket(wsUrl.toString()).apply {
|
WebSocket(wsUrl.toString()).apply {
|
||||||
onmessage = { messageEvent ->
|
onmessage = { messageEvent ->
|
||||||
val stringData: String? = messageEvent.data as? String
|
val stringData: String? = messageEvent.data as? String
|
||||||
if (stringData != null) {
|
if (stringData != null) {
|
||||||
@ -106,8 +119,6 @@ public class VisionClient : AbstractPlugin() {
|
|||||||
|
|
||||||
public companion object : PluginFactory<VisionClient> {
|
public companion object : PluginFactory<VisionClient> {
|
||||||
|
|
||||||
public const val RENDERER_CONFIGURATION_META_KEY: String = "@renderer"
|
|
||||||
|
|
||||||
override fun invoke(meta: Meta, context: Context): VisionClient = VisionClient()
|
override fun invoke(meta: Meta, context: Context): VisionClient = VisionClient()
|
||||||
|
|
||||||
override val tag: PluginTag = PluginTag(name = "vision.client", group = PluginTag.DATAFORGE_GROUP)
|
override val tag: PluginTag = PluginTag(name = "vision.client", group = PluginTag.DATAFORGE_GROUP)
|
||||||
@ -123,7 +134,7 @@ public fun VisionClient.fetchVisionsInChildren(element: Element, requestUpdates:
|
|||||||
val elements = element.getElementsByClassName(OutputTagConsumer.OUTPUT_CLASS)
|
val elements = element.getElementsByClassName(OutputTagConsumer.OUTPUT_CLASS)
|
||||||
console.info("Finished search for outputs. Found ${elements.length} items")
|
console.info("Finished search for outputs. Found ${elements.length} items")
|
||||||
elements.asList().forEach { child ->
|
elements.asList().forEach { child ->
|
||||||
fetchAndRenderVision(child, requestUpdates)
|
renderVisionAt(child, requestUpdates)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
//package hep.dataforge.vision.export
|
||||||
|
//
|
||||||
|
//package kscience.plotly
|
||||||
|
//
|
||||||
|
//import kotlinx.html.*
|
||||||
|
//import kotlinx.html.stream.createHTML
|
||||||
|
//
|
||||||
|
///**
|
||||||
|
// * A custom HTML fragment including plotly container reference
|
||||||
|
// */
|
||||||
|
//public class PlotlyFragment(public val render: FlowContent.(renderer: PlotlyRenderer) -> Unit)
|
||||||
|
//
|
||||||
|
///**
|
||||||
|
// * A complete page including headers and title
|
||||||
|
// */
|
||||||
|
//public data class PlotlyPage(
|
||||||
|
// val headers: Collection<HtmlFragment>,
|
||||||
|
// val fragment: PlotlyFragment,
|
||||||
|
// val title: String = "Plotly.kt",
|
||||||
|
// val renderer: PlotlyRenderer = StaticPlotlyRenderer
|
||||||
|
//) {
|
||||||
|
// public fun render(): String = createHTML().html {
|
||||||
|
// head {
|
||||||
|
// meta {
|
||||||
|
// charset = "utf-8"
|
||||||
|
// }
|
||||||
|
// title(this@PlotlyPage.title)
|
||||||
|
// headers.distinct().forEach { it.visit(consumer) }
|
||||||
|
// }
|
||||||
|
// body {
|
||||||
|
// fragment.render(this, renderer)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//public fun Plotly.fragment(content: FlowContent.(renderer: PlotlyRenderer) -> Unit): PlotlyFragment = PlotlyFragment(content)
|
||||||
|
//
|
||||||
|
///**
|
||||||
|
// * Create a complete page including plots
|
||||||
|
// */
|
||||||
|
//public fun Plotly.page(
|
||||||
|
// vararg headers: HtmlFragment = arrayOf(cdnPlotlyHeader),
|
||||||
|
// title: String = "Plotly.kt",
|
||||||
|
// renderer: PlotlyRenderer = StaticPlotlyRenderer,
|
||||||
|
// content: FlowContent.(renderer: PlotlyRenderer) -> Unit
|
||||||
|
//): PlotlyPage = PlotlyPage(headers.toList(), fragment(content), title, renderer)
|
||||||
|
//
|
||||||
|
///**
|
||||||
|
// * Convert an html plot fragment to page
|
||||||
|
// */
|
||||||
|
//public fun PlotlyFragment.toPage(
|
||||||
|
// vararg headers: HtmlFragment = arrayOf(cdnPlotlyHeader),
|
||||||
|
// title: String = "Plotly.kt",
|
||||||
|
// renderer: PlotlyRenderer = StaticPlotlyRenderer
|
||||||
|
//): PlotlyPage = PlotlyPage(headers.toList(), this, title, renderer)
|
||||||
|
//
|
||||||
|
///**
|
||||||
|
// * Convert a plot to the sigle-plot page
|
||||||
|
// */
|
||||||
|
//public fun Plot.toPage(
|
||||||
|
// vararg headers: HtmlFragment = arrayOf(cdnPlotlyHeader),
|
||||||
|
// config: PlotlyConfig = PlotlyConfig.empty(),
|
||||||
|
// title: String = "Plotly.kt",
|
||||||
|
// renderer: PlotlyRenderer = StaticPlotlyRenderer
|
||||||
|
//): PlotlyPage = PlotlyFragment {
|
||||||
|
// plot(this@toPage, config = config, renderer = renderer)
|
||||||
|
//}.toPage(*headers, title = title)
|
@ -1,10 +1,7 @@
|
|||||||
package hep.dataforge.vision.server
|
package hep.dataforge.vision.server
|
||||||
|
|
||||||
import hep.dataforge.context.Context
|
import hep.dataforge.context.Context
|
||||||
import hep.dataforge.meta.Config
|
import hep.dataforge.meta.*
|
||||||
import hep.dataforge.meta.Configurable
|
|
||||||
import hep.dataforge.meta.boolean
|
|
||||||
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
|
||||||
@ -25,21 +22,16 @@ import io.ktor.http.content.static
|
|||||||
import io.ktor.http.withCharset
|
import io.ktor.http.withCharset
|
||||||
import io.ktor.response.respond
|
import io.ktor.response.respond
|
||||||
import io.ktor.response.respondText
|
import io.ktor.response.respondText
|
||||||
import io.ktor.routing.application
|
import io.ktor.routing.*
|
||||||
import io.ktor.routing.get
|
|
||||||
import io.ktor.routing.route
|
|
||||||
import io.ktor.routing.routing
|
|
||||||
import io.ktor.server.cio.CIO
|
import io.ktor.server.cio.CIO
|
||||||
import io.ktor.server.engine.ApplicationEngine
|
import io.ktor.server.engine.ApplicationEngine
|
||||||
import io.ktor.server.engine.embeddedServer
|
import io.ktor.server.engine.embeddedServer
|
||||||
import io.ktor.util.KtorExperimentalAPI
|
import io.ktor.util.KtorExperimentalAPI
|
||||||
import io.ktor.util.error
|
|
||||||
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.flow.catch
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
import kotlinx.html.stream.createHTML
|
import kotlinx.html.stream.createHTML
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
@ -91,7 +83,70 @@ public class VisionServer internal constructor(
|
|||||||
return visionMap
|
return visionMap
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun page(
|
/**
|
||||||
|
* Server a map of visions without providing explicit html page for them
|
||||||
|
*/
|
||||||
|
@OptIn(DFExperimental::class)
|
||||||
|
public fun serveVisions(route: Route, visions: Map<Name, Vision>): Unit = route {
|
||||||
|
application.log.info("Serving visions $visions at $route")
|
||||||
|
|
||||||
|
//Update websocket
|
||||||
|
webSocket("ws") {
|
||||||
|
val name: String = call.request.queryParameters["name"]
|
||||||
|
?: error("Vision name is not defined in parameters")
|
||||||
|
|
||||||
|
application.log.debug("Opened server socket for $name")
|
||||||
|
val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered")
|
||||||
|
try {
|
||||||
|
withContext(visionManager.context.coroutineContext) {
|
||||||
|
vision.flowChanges(visionManager, updateInterval.milliseconds).collect { update ->
|
||||||
|
val json = VisionManager.defaultJson.encodeToString(
|
||||||
|
VisionChange.serializer(),
|
||||||
|
update
|
||||||
|
)
|
||||||
|
outgoing.send(Frame.Text(json))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
application.log.info("WebSocket update channel for $name is closed with exception: $t")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Plots in their json representation
|
||||||
|
get("vision") {
|
||||||
|
val name: String = call.request.queryParameters["name"]
|
||||||
|
?: error("Vision name is not defined in parameters")
|
||||||
|
|
||||||
|
val vision: Vision? = visions[name.toName()]
|
||||||
|
if (vision == null) {
|
||||||
|
call.respond(HttpStatusCode.NotFound, "Vision with name '$name' not found")
|
||||||
|
} else {
|
||||||
|
call.respondText(
|
||||||
|
visionManager.encodeToString(vision),
|
||||||
|
contentType = ContentType.Application.Json,
|
||||||
|
status = HttpStatusCode.OK
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serv visions in a given [route] without providing a page template
|
||||||
|
*/
|
||||||
|
public fun serveVisions(route: String, visions: Map<Name, Vision>): Unit {
|
||||||
|
application.routing {
|
||||||
|
route(rootRoute) {
|
||||||
|
route(route) {
|
||||||
|
serveVisions(this, visions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve a page, potentially containing any number of visions at a given [route] with given [headers].
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public fun servePage(
|
||||||
visionFragment: HtmlVisionFragment<Vision>,
|
visionFragment: HtmlVisionFragment<Vision>,
|
||||||
route: String = DEFAULT_PAGE,
|
route: String = DEFAULT_PAGE,
|
||||||
title: String = "VisionForge server page '$route'",
|
title: String = "VisionForge server page '$route'",
|
||||||
@ -100,6 +155,7 @@ public class VisionServer internal constructor(
|
|||||||
val visions = HashMap<Name, Vision>()
|
val visions = HashMap<Name, Vision>()
|
||||||
|
|
||||||
val cachedHtml: String? = if (cacheFragments) {
|
val cachedHtml: String? = if (cacheFragments) {
|
||||||
|
//Create and cache page html and map of visions
|
||||||
createHTML(true).html {
|
createHTML(true).html {
|
||||||
visions.putAll(buildPage(visionFragment, title, headers))
|
visions.putAll(buildPage(visionFragment, title, headers))
|
||||||
}
|
}
|
||||||
@ -110,43 +166,17 @@ public class VisionServer internal constructor(
|
|||||||
application.routing {
|
application.routing {
|
||||||
route(rootRoute) {
|
route(rootRoute) {
|
||||||
route(route) {
|
route(route) {
|
||||||
//Update websocket
|
serveVisions(this, visions)
|
||||||
webSocket("ws") {
|
|
||||||
val name: String = call.request.queryParameters["name"]
|
|
||||||
?: error("Vision name is not defined in parameters")
|
|
||||||
|
|
||||||
application.log.debug("Opened server socket for $name")
|
|
||||||
val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered")
|
|
||||||
vision.flowChanges(visionManager, updateInterval.milliseconds).onEach { update ->
|
|
||||||
val json = VisionManager.defaultJson.encodeToString(VisionChange.serializer(), update)
|
|
||||||
outgoing.send(Frame.Text(json))
|
|
||||||
}.catch { ex ->
|
|
||||||
application.log.error(ex)
|
|
||||||
}.launchIn(visionManager.context).join()
|
|
||||||
}
|
|
||||||
//Plots in their json representation
|
|
||||||
get("vision") {
|
|
||||||
val name: String = call.request.queryParameters["name"]
|
|
||||||
?: error("Vision name is not defined in parameters")
|
|
||||||
|
|
||||||
val vision: Vision? = visions[name.toName()]
|
|
||||||
if (vision == null) {
|
|
||||||
call.respond(HttpStatusCode.NotFound, "Vision with name '$name' not found")
|
|
||||||
} else {
|
|
||||||
call.respondText(
|
|
||||||
visionManager.encodeToString(vision),
|
|
||||||
contentType = ContentType.Application.Json,
|
|
||||||
status = HttpStatusCode.OK
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//filled pages
|
//filled pages
|
||||||
get {
|
get {
|
||||||
if (cachedHtml == null) {
|
if (cachedHtml == null) {
|
||||||
|
//re-create html and vision list on each call
|
||||||
call.respondHtml {
|
call.respondHtml {
|
||||||
|
visions.clear()
|
||||||
visions.putAll(buildPage(visionFragment, title, headers))
|
visions.putAll(buildPage(visionFragment, title, headers))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
//Use cached html
|
||||||
call.respondText(cachedHtml, ContentType.Text.Html.withCharset(Charsets.UTF_8))
|
call.respondText(cachedHtml, ContentType.Text.Html.withCharset(Charsets.UTF_8))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,13 +185,16 @@ public class VisionServer internal constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A shortcut method to easily create Complete pages filled with visions
|
||||||
|
*/
|
||||||
public fun page(
|
public fun page(
|
||||||
route: String = DEFAULT_PAGE,
|
route: String = DEFAULT_PAGE,
|
||||||
title: String = "VisionForge server page '$route'",
|
title: String = "VisionForge server page '$route'",
|
||||||
headers: List<HtmlFragment> = emptyList(),
|
headers: List<HtmlFragment> = emptyList(),
|
||||||
content: OutputTagConsumer<*, Vision>.() -> Unit,
|
content: OutputTagConsumer<*, Vision>.() -> Unit,
|
||||||
) {
|
) {
|
||||||
page(buildVisionFragment(content), route, title, headers)
|
servePage(buildVisionFragment(content), route, title, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -171,11 +204,33 @@ public class VisionServer internal constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use a script with given [src] as a global header for all pages.
|
||||||
|
*/
|
||||||
|
public inline fun VisionServer.useScript(src: String, crossinline block: SCRIPT.() -> Unit = {}) {
|
||||||
|
header {
|
||||||
|
script {
|
||||||
|
type = "text/javascript"
|
||||||
|
this.src = src
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public inline fun VisionServer.useCss(href: String, crossinline block: LINK.() -> Unit = {}) {
|
||||||
|
header {
|
||||||
|
link {
|
||||||
|
rel = "stylesheet"
|
||||||
|
this.href = href
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach plotly application to given server
|
* Attach plotly application to given server
|
||||||
*/
|
*/
|
||||||
public fun Application.visionModule(context: Context, route: String = DEFAULT_PAGE): VisionServer {
|
public fun Application.visionServer(context: Context, route: String = DEFAULT_PAGE): VisionServer {
|
||||||
if (featureOrNull(WebSockets) == null) {
|
if (featureOrNull(WebSockets) == null) {
|
||||||
install(WebSockets)
|
install(WebSockets)
|
||||||
}
|
}
|
||||||
@ -209,7 +264,7 @@ public fun VisionManager.serve(
|
|||||||
port: Int = 7777,
|
port: Int = 7777,
|
||||||
block: VisionServer.() -> Unit,
|
block: VisionServer.() -> Unit,
|
||||||
): ApplicationEngine = context.embeddedServer(CIO, port, host) {
|
): ApplicationEngine = context.embeddedServer(CIO, port, host) {
|
||||||
visionModule(context).apply(block)
|
visionServer(context).apply(block)
|
||||||
}.start()
|
}.start()
|
||||||
|
|
||||||
public fun ApplicationEngine.show() {
|
public fun ApplicationEngine.show() {
|
||||||
|
Loading…
Reference in New Issue
Block a user