Forms implemented

This commit is contained in:
Alexander Nozik 2021-12-31 13:59:27 +03:00
parent 104e8f8f6f
commit 6b8e166978
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
27 changed files with 445 additions and 151 deletions

View File

@ -21,7 +21,7 @@ import space.kscience.visionforge.gdml.markLayers
import space.kscience.visionforge.gdml.toVision import space.kscience.visionforge.gdml.toVision
import space.kscience.visionforge.ring.ThreeCanvasWithControls import space.kscience.visionforge.ring.ThreeCanvasWithControls
import space.kscience.visionforge.ring.tab import space.kscience.visionforge.ring.tab
import space.kscience.visionforge.root import space.kscience.visionforge.setAsRoot
import space.kscience.visionforge.solid.Solid import space.kscience.visionforge.solid.Solid
import space.kscience.visionforge.solid.Solids import space.kscience.visionforge.solid.Solids
import styled.css import styled.css
@ -50,7 +50,7 @@ val GDMLApp = fc<GDMLAppProps>("GDMLApp") { props ->
name.endsWith(".gdml") || name.endsWith(".xml") -> { name.endsWith(".gdml") || name.endsWith(".xml") -> {
val gdml = Gdml.decodeFromString(data) val gdml = Gdml.decodeFromString(data)
gdml.toVision().apply { gdml.toVision().apply {
root(visionManager) setAsRoot(visionManager)
console.info("Marking layers for file $name") console.info("Marking layers for file $name")
markLayers() markLayers()
} }

View File

@ -1,35 +0,0 @@
plugins {
kotlin("jvm")
kotlin("jupyter.api")
id("com.github.johnrengelman.shadow") version "6.1.0"
}
repositories {
jcenter()
mavenCentral()
maven("https://repo.kotlin.link")
}
dependencies {
implementation(project(":demo:playground"))
}
tasks.withType<org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile> {
kotlinOptions {
jvmTarget = ru.mipt.npm.gradle.KScienceVersions.JVM_TARGET.toString()
}
}
extensions.findByType<JavaPluginExtension>()?.apply {
targetCompatibility = ru.mipt.npm.gradle.KScienceVersions.JVM_TARGET
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.processJupyterApiResources {
libraryProducers = listOf("playground.VisionForgePlayGroundForJupyter")
}
tasks.findByName("shadowJar")?.dependsOn("processJupyterApiResources")

View File

@ -5,7 +5,7 @@ import ru.mipt.npm.muon.monitor.Monitor.LOWER_LAYER_Z
import ru.mipt.npm.muon.monitor.Monitor.UPPER_LAYER_Z import ru.mipt.npm.muon.monitor.Monitor.UPPER_LAYER_Z
import space.kscience.visionforge.VisionManager import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.removeAll import space.kscience.visionforge.removeAll
import space.kscience.visionforge.root import space.kscience.visionforge.setAsRoot
import space.kscience.visionforge.setProperty import space.kscience.visionforge.setProperty
import space.kscience.visionforge.solid.* import space.kscience.visionforge.solid.*
import kotlin.math.PI import kotlin.math.PI
@ -37,7 +37,7 @@ class Model(val manager: VisionManager) {
var tracks: SolidGroup var tracks: SolidGroup
val root: SolidGroup = SolidGroup().apply { val root: SolidGroup = SolidGroup().apply {
root(this@Model.manager) setAsRoot(this@Model.manager)
material { material {
wireframe wireframe
color("darkgreen") color("darkgreen")

View File

@ -76,9 +76,6 @@ val MMApp = fc<MMAppProps>("Muon monitor") { props ->
attrs { attrs {
onClickFunction = { onClickFunction = {
context.launch { context.launch {
// val event = props.connection.get<Event>(
// "http://localhost:8080/event"
// )
val event = window.fetch( val event = window.fetch(
"http://localhost:8080/event", "http://localhost:8080/event",
RequestInit("GET") RequestInit("GET")

View File

@ -1,14 +1,13 @@
plugins { plugins {
kotlin("multiplatform") kotlin("multiplatform")
kotlin("jupyter.api")
id("com.github.johnrengelman.shadow") version "7.1.2"
} }
repositories { repositories {
jcenter() mavenCentral()
maven("https://kotlin.bintray.com/kotlinx") maven("https://jitpack.io")
maven("https://dl.bintray.com/kotlin/kotlin-eap") maven("https://repo.kotlin.link")
maven("https://dl.bintray.com/mipt-npm/dataforge")
maven("https://dl.bintray.com/mipt-npm/kscience")
maven("https://dl.bintray.com/mipt-npm/dev")
} }
kotlin { kotlin {
@ -28,6 +27,7 @@ kotlin {
} }
jvm { jvm {
withJava()
compilations.all { compilations.all {
kotlinOptions.jvmTarget = "11" kotlinOptions.jvmTarget = "11"
} }
@ -75,3 +75,9 @@ kotlin {
} }
} }
} }
tasks.withType<org.jetbrains.kotlinx.jupyter.api.plugin.tasks.JupyterApiResourcesTask> {
libraryProducers = listOf("space.kscience.visionforge.examples.VisionForgePlayGroundForJupyter")
}
tasks.findByName("shadowJar")?.dependsOn("processJupyterApiResources")

View File

@ -1,12 +1,9 @@
package playground package space.kscience.visionforge.examples
import kotlinx.html.div
import kotlinx.html.id
import kotlinx.html.script
import kotlinx.html.stream.createHTML import kotlinx.html.stream.createHTML
import kotlinx.html.unsafe
import org.jetbrains.kotlinx.jupyter.api.HTML import org.jetbrains.kotlinx.jupyter.api.HTML
import org.jetbrains.kotlinx.jupyter.api.libraries.* import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.gdml.Gdml import space.kscience.gdml.Gdml
@ -15,7 +12,7 @@ import space.kscience.visionforge.Vision
import space.kscience.visionforge.gdml.toVision import space.kscience.visionforge.gdml.toVision
import space.kscience.visionforge.html.HtmlVisionFragment import space.kscience.visionforge.html.HtmlVisionFragment
import space.kscience.visionforge.html.Page import space.kscience.visionforge.html.Page
import space.kscience.visionforge.html.embedVisionFragment import space.kscience.visionforge.html.embedAndRenderVisionFragment
import space.kscience.visionforge.plotly.PlotlyPlugin import space.kscience.visionforge.plotly.PlotlyPlugin
import space.kscience.visionforge.plotly.asVision import space.kscience.visionforge.plotly.asVision
import space.kscience.visionforge.solid.Solids import space.kscience.visionforge.solid.Solids
@ -29,27 +26,19 @@ public class VisionForgePlayGroundForJupyter : JupyterIntegration() {
plugin(PlotlyPlugin) plugin(PlotlyPlugin)
} }
private val jsBundle = ResourceFallbacksBundle(listOf(
ResourceLocation("js/visionforge-playground.js", ResourcePathType.CLASSPATH_PATH))
)
private val jsResource = LibraryResource(name = "VisionForge", type = ResourceType.JS, bundles = listOf(jsBundle))
private var counter = 0 private var counter = 0
private fun produceHtmlVisionString(fragment: HtmlVisionFragment) = createHTML().div { private fun produceHtmlVisionString(fragment: HtmlVisionFragment) = createHTML().apply {
val id = "visionforge.vision[${counter++}]" embedAndRenderVisionFragment(context.visionManager, counter++, fragment = fragment)
div { }.finalize()
this.id = id
embedVisionFragment(context.visionManager, fragment = fragment)
}
script {
type = "text/javascript"
unsafe { +"window.renderAllVisionsById(\"$id\");" }
}
}
override fun Builder.onLoaded() { override fun Builder.onLoaded() {
resource(jsResource)
resources {
js("VisionForge"){
classPath("js/visionforge-playground.js")
}
}
import( import(
"space.kscience.gdml.*", "space.kscience.gdml.*",

View File

@ -0,0 +1,67 @@
package space.kscience.visionforge.examples
import kotlinx.html.*
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.fetch
import space.kscience.dataforge.names.asName
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.visionOfForm
import space.kscience.visionforge.onPropertyChange
import space.kscience.visionforge.three.server.close
import space.kscience.visionforge.three.server.openInBrowser
import space.kscience.visionforge.three.server.serve
import space.kscience.visionforge.three.server.useScript
fun main() {
val visionManager = Global.fetch(VisionManager)
val server = visionManager.serve {
useScript("js/visionforge-playground.js")
page {
val form = visionOfForm("form") {
label {
htmlFor = "fname"
+"First name:"
}
br()
input {
type = InputType.text
id = "fname"
name = "fname"
value = "John"
}
br()
label {
htmlFor = "lname"
+"Last name:"
}
br()
input {
type = InputType.text
id = "lname"
name = "lname"
value = "Doe"
}
br()
br()
input {
type = InputType.submit
value = "Submit"
}
}
vision("form".asName(), form)
form.onPropertyChange {
println(this)
}
}
}
server.openInBrowser()
while (readln() != "exit") {
}
server.close()
}

View File

@ -226,7 +226,7 @@ fun main() {
} }
} }
}.toVision { }.toVision {
configure { parent, solid, material -> configure { _, solid, _ ->
//disable visibility for the world box //disable visibility for the world box
if(solid.name == "world"){ if(solid.name == "world"){
visible = false visible = false

View File

@ -6,9 +6,7 @@ import kotlinx.serialization.json.Json
import space.kscience.visionforge.solid.SolidGroup import space.kscience.visionforge.solid.SolidGroup
import space.kscience.visionforge.solid.Solids import space.kscience.visionforge.solid.Solids
@ExperimentalSerializationApi private val json = Json {
fun main() {
val schema = Json {
serializersModule = Solids.serializersModuleForSolids serializersModule = Solids.serializersModuleForSolids
prettyPrintIndent = " " prettyPrintIndent = " "
prettyPrint = true prettyPrint = true
@ -16,6 +14,10 @@ fun main() {
isLenient = true isLenient = true
coerceInputValues = true coerceInputValues = true
encodeDefaults = true encodeDefaults = true
}.encodeToSchema(SolidGroup.serializer(), generateDefinitions = false) }
@ExperimentalSerializationApi
fun main() {
val schema = json.encodeToSchema(SolidGroup.serializer(), generateDefinitions = false)
println(schema) println(schema)
} }

View File

@ -1,13 +1,11 @@
package ru.mipt.npm.sat package ru.mipt.npm.sat
import space.kscience.dataforge.meta.set import space.kscience.dataforge.meta.set
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.solid.* import space.kscience.visionforge.solid.*
import space.kscience.visionforge.style import space.kscience.visionforge.style
import space.kscience.visionforge.useStyle import space.kscience.visionforge.useStyle
import kotlin.math.PI import kotlin.math.PI
@DFExperimental
internal fun visionOfSatellite( internal fun visionOfSatellite(
layers: Int = 10, layers: Int = 10,
layerHeight: Number = 10, layerHeight: Number = 10,

View File

@ -6,7 +6,6 @@ import io.ktor.server.engine.embeddedServer
import kotlinx.html.stream.createHTML import kotlinx.html.stream.createHTML
import org.jetbrains.kotlinx.jupyter.api.HTML import org.jetbrains.kotlinx.jupyter.api.HTML
import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
@ -53,12 +52,6 @@ public abstract class JupyterPluginBase(
server = null server = null
} }
resources {
js("three") {
classPath("js/gdml-jupyter.js")
}
}
import( import(
"kotlinx.html.*", "kotlinx.html.*",
"space.kscience.visionforge.html.Page", "space.kscience.visionforge.html.Page",

View File

@ -59,7 +59,6 @@ include(
":demo:muon-monitor", ":demo:muon-monitor",
":demo:sat-demo", ":demo:sat-demo",
":demo:playground", ":demo:playground",
":demo:jupyter-playground",
":demo:plotly-fx", ":demo:plotly-fx",
":demo:js-playground", ":demo:js-playground",
":jupyter:visionforge-jupyter-base", ":jupyter:visionforge-jupyter-base",

View File

@ -83,7 +83,7 @@ public val ThreeCanvasWithControls: FC<ThreeCanvasWithControlsProps> = fc("Three
useEffect { useEffect {
props.context.launch { props.context.launch {
solid = props.builderOfSolid.await().also { solid = props.builderOfSolid.await().also {
it?.root(props.context.visionManager) it?.setAsRoot(props.context.visionManager)
} }
} }
} }

View File

@ -109,7 +109,7 @@ public open class VisionBase(
override fun hashCode(): Int = Meta.hashCode(this) override fun hashCode(): Int = Meta.hashCode(this)
} }
override val meta: ObservableMutableMeta get() = VisionProperties(Name.EMPTY) final override val meta: ObservableMutableMeta get() = VisionProperties(Name.EMPTY)
override fun getPropertyValue( override fun getPropertyValue(
name: Name, name: Name,

View File

@ -14,6 +14,17 @@ import space.kscience.dataforge.values.Null
import kotlin.jvm.Synchronized import kotlin.jvm.Synchronized
import kotlin.time.Duration import kotlin.time.Duration
/**
* Create a deep copy of given Vision without external connections.
*/
private fun Vision.deepCopy(): Vision {
//Assuming that unrooted visions are already isolated
val manager = this.manager ?: return this
//TODO replace by efficient deep copy
val json = manager.encodeToJsonElement(this)
return manager.decodeFromJson(json)
}
/** /**
* An update for a [Vision] or a [VisionGroup] * An update for a [Vision] or a [VisionGroup]
*/ */
@ -50,20 +61,14 @@ public class VisionChangeBuilder : VisionContainerBuilder<Vision> {
/** /**
* Isolate collected changes by creating detached copies of given visions * Isolate collected changes by creating detached copies of given visions
*/ */
public fun isolate(manager: VisionManager): VisionChange = VisionChange( public fun deepCopy(): VisionChange = VisionChange(
reset, reset,
vision?.isolate(manager), vision?.deepCopy(),
if (propertyChange.isEmpty()) null else propertyChange.seal(), if (propertyChange.isEmpty()) null else propertyChange.seal(),
if (children.isEmpty()) null else children.mapValues { it.value.isolate(manager) } if (children.isEmpty()) null else children.mapValues { it.value.deepCopy() }
) )
} }
private fun Vision.isolate(manager: VisionManager): Vision {
//TODO replace by efficient deep copy
val json = manager.encodeToJsonElement(this)
return manager.decodeFromJson(json)
}
/** /**
* @param delete 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
@ -78,8 +83,8 @@ public data class VisionChange(
public val children: Map<Name, VisionChange>? = null, public val children: Map<Name, VisionChange>? = null,
) )
public inline fun VisionChange(manager: VisionManager, block: VisionChangeBuilder.() -> Unit): VisionChange = public inline fun VisionChange(block: VisionChangeBuilder.() -> Unit): VisionChange =
VisionChangeBuilder().apply(block).isolate(manager) VisionChangeBuilder().apply(block).deepCopy()
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
@ -115,9 +120,10 @@ private fun CoroutineScope.collectChange(
} }
} }
@DFExperimental /**
* Generate a flow of changes of this vision and its children
*/
public fun Vision.flowChanges( public fun Vision.flowChanges(
manager: VisionManager,
collectionDuration: Duration, collectionDuration: Duration,
): Flow<VisionChange> = flow { ): Flow<VisionChange> = flow {
@ -126,7 +132,7 @@ public fun Vision.flowChanges(
collectChange(Name.EMPTY, this@flowChanges) { collector } collectChange(Name.EMPTY, this@flowChanges) { collector }
//Send initial vision state //Send initial vision state
val initialChange = VisionChange(vision = isolate(manager)) val initialChange = VisionChange(vision = deepCopy())
emit(initialChange) emit(initialChange)
while (currentCoroutineContext().isActive) { while (currentCoroutineContext().isActive) {
@ -135,7 +141,7 @@ public fun Vision.flowChanges(
//Propagate updates only if something is changed //Propagate updates only if something is changed
if (!collector.isEmpty()) { if (!collector.isEmpty()) {
//emit changes //emit changes
emit(collector.isolate(manager)) emit(collector.deepCopy())
//Reset the collector //Reset the collector
collector = VisionChangeBuilder() collector = VisionChangeBuilder()
} }

View File

@ -160,8 +160,9 @@ public open class VisionGroupBase(
internal class RootVisionGroup(override val manager: VisionManager) : VisionGroupBase() internal class RootVisionGroup(override val manager: VisionManager) : VisionGroupBase()
/** /**
* Designate this [VisionGroup] as a root group and assign a [VisionManager] as its parent * Designate this [VisionGroup] as a root and assign a [VisionManager] as its parent
*/ */
public fun Vision.root(manager: VisionManager) { public fun Vision.setAsRoot(manager: VisionManager) {
if (parent != null) error("This Vision already has a parent. It could not be set as root")
parent = RootVisionGroup(manager) parent = RootVisionGroup(manager)
} }

View File

@ -13,6 +13,10 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.toJson import space.kscience.dataforge.meta.toJson
import space.kscience.dataforge.meta.toMeta import space.kscience.dataforge.meta.toMeta
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.visionforge.html.VisionOfCheckbox
import space.kscience.visionforge.html.VisionOfHtmlForm
import space.kscience.visionforge.html.VisionOfNumberField
import space.kscience.visionforge.html.VisionOfTextField
import kotlin.reflect.KClass import kotlin.reflect.KClass
public class VisionManager(meta: Meta) : AbstractPlugin(meta) { public class VisionManager(meta: Meta) : AbstractPlugin(meta) {
@ -66,6 +70,10 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) {
default { VisionBase.serializer() } default { VisionBase.serializer() }
subclass(VisionBase.serializer()) subclass(VisionBase.serializer())
subclass(VisionGroupBase.serializer()) subclass(VisionGroupBase.serializer())
subclass(VisionOfNumberField.serializer())
subclass(VisionOfTextField.serializer())
subclass(VisionOfCheckbox.serializer())
subclass(VisionOfHtmlForm.serializer())
} }
} }

View File

@ -0,0 +1,26 @@
package space.kscience.visionforge.html
import kotlinx.html.FORM
import kotlinx.html.TagConsumer
import kotlinx.html.form
import kotlinx.html.id
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.node
@Serializable
@SerialName("html.form")
public class VisionOfHtmlForm(
public val formId: String,
) : VisionOfHtmlInput() {
public var values: Meta? by meta.node()
}
public inline fun <R> TagConsumer<R>.visionOfForm(id: String, crossinline builder: FORM.() -> Unit): VisionOfHtmlForm {
form {
this.id = id
builder()
}
return VisionOfHtmlForm(id)
}

View File

@ -0,0 +1,53 @@
package space.kscience.visionforge.html
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.boolean
import space.kscience.dataforge.meta.number
import space.kscience.dataforge.meta.string
import space.kscience.visionforge.VisionBase
@Serializable
public abstract class VisionOfHtmlInput : VisionBase() {
public var disabled: Boolean by meta.boolean(false)
}
@Serializable
@SerialName("html.text")
public class VisionOfTextField(
public val label: String? = null,
public val name: String? = null,
) : VisionOfHtmlInput() {
public var text: String? by meta.string()
}
@Serializable
@SerialName("html.checkbox")
public class VisionOfCheckbox(
public val label: String? = null,
public val name: String? = null,
) : VisionOfHtmlInput() {
public var checked: Boolean? by meta.boolean()
}
@Serializable
@SerialName("html.number")
public class VisionOfNumberField(
public val label: String? = null,
public val name: String? = null,
) : VisionOfHtmlInput() {
public var value: Number? by meta.number()
}
@Serializable
@SerialName("html.range")
public class VisionOfRangeField(
public val min: Double,
public val max: Double,
public val step: Double = 1.0,
public val label: String? = null,
public val name: String? = null,
) : VisionOfHtmlInput() {
public var value: Number? by meta.number()
}

View File

@ -11,7 +11,7 @@ import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.asName
import space.kscience.visionforge.Vision import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.root import space.kscience.visionforge.setAsRoot
import kotlin.collections.set import kotlin.collections.set
@DslMarker @DslMarker
@ -81,11 +81,11 @@ public abstract class VisionTagConsumer<R>(
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
public inline fun <T> TagConsumer<T>.vision( public inline fun <T> TagConsumer<T>.vision(
name: Name, name: Name,
visionProvider: VisionOutput.() -> Vision, @OptIn(DFExperimental::class) visionProvider: VisionOutput.() -> Vision,
): T { ): T {
val output = VisionOutput(manager) val output = VisionOutput(manager)
val vision = output.visionProvider() val vision = output.visionProvider()
vision.root(manager) vision.setAsRoot(manager)
return vision(name, vision, output.meta) return vision(name, vision, output.meta)
} }

View File

@ -0,0 +1,74 @@
package space.kscience.visionforge
import kotlinx.dom.clear
import kotlinx.html.TagConsumer
import kotlinx.html.dom.append
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.serializerOrNull
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.Named
import space.kscience.dataforge.misc.Type
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
import kotlin.reflect.KClass
import kotlin.reflect.cast
/**
* A browser renderer for a [Vision].
*/
@Type(ElementVisionRenderer.TYPE)
public interface ElementVisionRenderer : Named {
/**
* Give a [vision] integer rating based on this renderer capabilities. [ZERO_RATING] or negative values means that this renderer
* can't process a vision. The value of [DEFAULT_RATING] used for default renderer. Specialized renderers could specify
* higher value in order to "steal" rendering job
*/
public fun rateVision(vision: Vision): Int
/**
* Display the [vision] inside a given [element] replacing its current content.
* @param meta additional parameters for rendering container
*/
public fun render(element: Element, vision: Vision, meta: Meta = Meta.EMPTY)
public companion object {
public const val TYPE: String = "elementVisionRenderer"
public const val ZERO_RATING: Int = 0
public const val DEFAULT_RATING: Int = 10
}
}
/**
* A browser renderer for element of given type
*/
public class SingleTypeVisionRenderer<T : Vision>(
public val kClass: KClass<T>,
private val acceptRating: Int = ElementVisionRenderer.DEFAULT_RATING,
private val renderFunction: TagConsumer<HTMLElement>.(vision: T, meta: Meta) -> Unit,
) : ElementVisionRenderer {
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
override val name: Name
get() = kClass.serializerOrNull()?.descriptor?.serialName?.parseAsName()
?: kClass.toString().asName()
override fun rateVision(vision: Vision): Int =
if (vision::class == kClass) acceptRating else ElementVisionRenderer.ZERO_RATING
override fun render(element: Element, vision: Vision, meta: Meta) {
element.clear()
element.append {
renderFunction(kClass.cast(vision), meta)
}
}
}
public inline fun <reified T : Vision> ElementVisionRenderer(
acceptRating: Int = ElementVisionRenderer.DEFAULT_RATING,
noinline renderFunction: TagConsumer<HTMLElement>.(vision: T, meta: Meta) -> Unit,
): ElementVisionRenderer = SingleTypeVisionRenderer(T::class, acceptRating, renderFunction)

View File

@ -10,6 +10,9 @@ import org.w3c.dom.url.URL
import space.kscience.dataforge.context.* import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.html.RENDER_FUNCTION_NAME import space.kscience.visionforge.html.RENDER_FUNCTION_NAME
import space.kscience.visionforge.html.VisionTagConsumer import space.kscience.visionforge.html.VisionTagConsumer
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE
@ -19,6 +22,9 @@ import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_NAME_A
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
/**
* A Kotlin-browser plugin that renders visions based on provided renderers and governs communication with the server.
*/
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)
@ -102,10 +108,12 @@ public class VisionClient : AbstractPlugin() {
//Backward change propagation //Backward change propagation
var feedbackJob: Job? = null var feedbackJob: Job? = null
//Feedback changes aggregation time in milliseconds
val feedbackAggregationTime = meta["aggregationTime"]?.int ?: 300
onopen = { onopen = {
feedbackJob = vision.flowChanges( feedbackJob = vision.flowChanges(
visionManager, feedbackAggregationTime.milliseconds
300.milliseconds
).onEach { change -> ).onEach { change ->
send(visionManager.encodeToString(change)) send(visionManager.encodeToString(change))
}.launchIn(visionManager.context) }.launchIn(visionManager.context)
@ -180,6 +188,12 @@ public class VisionClient : AbstractPlugin() {
} }
} }
override fun content(target: String): Map<Name, Any> = if (target == ElementVisionRenderer.TYPE) mapOf(
numberVisionRenderer.name to numberVisionRenderer,
textVisionRenderer.name to textVisionRenderer,
formVisionRenderer.name to formVisionRenderer
) else super.content(target)
public companion object : PluginFactory<VisionClient> { public companion object : PluginFactory<VisionClient> {
override fun invoke(meta: Meta, context: Context): VisionClient = VisionClient() override fun invoke(meta: Meta, context: Context): VisionClient = VisionClient()

View File

@ -1,27 +0,0 @@
package space.kscience.visionforge
import org.w3c.dom.Element
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.Type
@Type(ElementVisionRenderer.TYPE)
public interface ElementVisionRenderer {
/**
* Give a [vision] integer rating based on this renderer capabilities. [ZERO_RATING] or negative values means that this renderer
* can't process a vision. The value of [DEFAULT_RATING] used for default renderer. Specialized renderers could specify
* higher value in order to "steal" rendering job
*/
public fun rateVision(vision: Vision): Int
/**
* Display the [vision] inside a given [element] replacing its current content
*/
public fun render(element: Element, vision: Vision, meta: Meta = Meta.EMPTY)
public companion object {
public const val TYPE: String = "elementVisionRenderer"
public const val ZERO_RATING: Int = 0
public const val DEFAULT_RATING: Int = 10
}
}

View File

@ -0,0 +1,101 @@
package space.kscience.visionforge
import kotlinx.browser.document
import kotlinx.html.InputType
import kotlinx.html.js.input
import kotlinx.html.js.label
import kotlinx.html.js.onChangeFunction
import org.w3c.dom.HTMLFormElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.get
import org.w3c.xhr.FormData
import space.kscience.dataforge.meta.DynamicMeta
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.valueSequence
import space.kscience.visionforge.html.VisionOfHtmlForm
import space.kscience.visionforge.html.VisionOfNumberField
import space.kscience.visionforge.html.VisionOfTextField
public val textVisionRenderer: ElementVisionRenderer = ElementVisionRenderer<VisionOfTextField> { vision, _ ->
val name = vision.name ?: "input[${vision.hashCode().toUInt()}]"
vision.label?.let {
label {
htmlFor = name
+it
}
}
input {
type = InputType.text
this.name = name
vision.useProperty(VisionOfTextField::text) {
value = it ?: ""
}
onChangeFunction = {
vision.text = value
}
}
}
public val numberVisionRenderer: ElementVisionRenderer = ElementVisionRenderer<VisionOfNumberField> { vision, _ ->
val name = vision.name ?: "input[${vision.hashCode().toUInt()}]"
vision.label?.let {
label {
htmlFor = name
+it
}
}
input {
type = InputType.text
this.name = name
vision.useProperty(VisionOfNumberField::value) {
value = it?.toDouble() ?: 0.0
}
onChangeFunction = {
vision.value = value.toDoubleOrNull()
}
}
}
internal fun FormData.toMeta(): Meta {
@Suppress("UNUSED_VARIABLE") val formData = this
//val res = js("Object.fromEntries(formData);")
val `object` = js("{}")
//language=JavaScript
js("""
formData.forEach(function(value, key){
// Reflect.has in favor of: object.hasOwnProperty(key)
if(!Reflect.has(object, key)){
object[key] = value;
return;
}
if(!Array.isArray(object[key])){
object[key] = [object[key]];
}
object[key].push(value);
});
""")
return DynamicMeta(`object`)
}
public val formVisionRenderer: ElementVisionRenderer = ElementVisionRenderer<VisionOfHtmlForm> { vision, _ ->
val form = document.getElementById(vision.formId) as? HTMLFormElement
?: error("An element with id = '${vision.formId} is not a form")
console.info("Adding hooks to form '$form'")
vision.useProperty(VisionOfHtmlForm::values) { values ->
val inputs = form.getElementsByTagName("input")
values?.valueSequence()?.forEach { (token, value) ->
(inputs[token.toString()] as? HTMLInputElement)?.value = value.toString()
}
}
form.onsubmit = { event ->
event.preventDefault()
val formData = FormData(form).toMeta()
console.log(formData.toString())
vision.values = formData
false
}
}

View File

@ -0,0 +1,22 @@
package space.kscience.visionforge
import org.w3c.xhr.FormData
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.stringList
import kotlin.test.Test
import kotlin.test.assertEquals
class FormTest {
@Test
fun testFormConversion() {
val fd = FormData()
fd.append("a", "22")
fd.append("b", "1")
fd.append("b", "2")
val meta = fd.toMeta()
assertEquals(22, meta["a"].int)
assertEquals(listOf("1","2"), meta["b"]?.stringList)
}
}

View File

@ -144,7 +144,7 @@ public class VisionServer internal constructor(
try { try {
withContext(visionManager.context.coroutineContext) { withContext(visionManager.context.coroutineContext) {
vision.flowChanges(visionManager, updateInterval.milliseconds).collect { update -> vision.flowChanges(updateInterval.milliseconds).collect { update ->
val json = visionManager.jsonFormat.encodeToString( val json = visionManager.jsonFormat.encodeToString(
VisionChange.serializer(), VisionChange.serializer(),
update update

View File

@ -20,7 +20,7 @@ class VisionUpdateTest {
val targetVision = SolidGroup { val targetVision = SolidGroup {
box(200,200,200, name = "origin") box(200,200,200, name = "origin")
} }
val dif = VisionChange(visionManager){ val dif = VisionChange{
group("top") { group("top") {
color(123) color(123)
box(100,100,100) box(100,100,100)
@ -36,7 +36,7 @@ class VisionUpdateTest {
@Test @Test
fun testVisionChangeSerialization(){ fun testVisionChangeSerialization(){
val change = VisionChange(visionManager){ val change = VisionChange{
group("top") { group("top") {
color(123) color(123)
box(100,100,100) box(100,100,100)